diff options
Diffstat (limited to 'Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Drawing/CommandBuilder.cs')
-rw-r--r-- | Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Drawing/CommandBuilder.cs | 3062 |
1 files changed, 3062 insertions, 0 deletions
diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Drawing/CommandBuilder.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Drawing/CommandBuilder.cs new file mode 100644 index 0000000..7932907 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Drawing/CommandBuilder.cs @@ -0,0 +1,3062 @@ +using System; +using Unity.Collections.LowLevel.Unsafe; +using Unity.Mathematics; +using Unity.Jobs.LowLevel.Unsafe; +using UnityEngine; +using System.Collections.Generic; +using Unity.Burst; +using UnityEngine.Profiling; +using Unity.Collections; +using Unity.Jobs; +using UnityEngine.Rendering; +using System.Runtime.InteropServices; + +namespace Pathfinding.Drawing { + using static DrawingData; + using BitPackedMeta = DrawingData.BuilderData.BitPackedMeta; + using Pathfinding.Drawing.Text; + using Unity.Profiling; + + /// <summary> + /// Specifies text alignment relative to an anchor point. + /// + /// <code> + /// Draw.Label2D(transform.position, "Hello World", 14, LabelAlignment.TopCenter); + /// </code> + /// <code> + /// // Draw the label 20 pixels below the object + /// Draw.Label2D(transform.position, "Hello World", 14, LabelAlignment.TopCenter.withPixelOffset(0, -20)); + /// </code> + /// + /// See: <see cref="Draw.Label2D"/> + /// See: <see cref="Draw.Label3D"/> + /// </summary> + public struct LabelAlignment { + /// <summary> + /// Where on the text's bounding box to anchor the text. + /// + /// The pivot is specified in relative coordinates, where (0,0) is the bottom left corner and (1,1) is the top right corner. + /// </summary> + public float2 relativePivot; + /// <summary>How much to move the text in screen-space</summary> + public float2 pixelOffset; + + public static readonly LabelAlignment TopLeft = new LabelAlignment { relativePivot = new float2(0.0f, 1.0f), pixelOffset = new float2(0, 0) }; + public static readonly LabelAlignment MiddleLeft = new LabelAlignment { relativePivot = new float2(0.0f, 0.5f), pixelOffset = new float2(0, 0) }; + public static readonly LabelAlignment BottomLeft = new LabelAlignment { relativePivot = new float2(0.0f, 0.0f), pixelOffset = new float2(0, 0) }; + public static readonly LabelAlignment BottomCenter = new LabelAlignment { relativePivot = new float2(0.5f, 0.0f), pixelOffset = new float2(0, 0) }; + public static readonly LabelAlignment BottomRight = new LabelAlignment { relativePivot = new float2(1.0f, 0.0f), pixelOffset = new float2(0, 0) }; + public static readonly LabelAlignment MiddleRight = new LabelAlignment { relativePivot = new float2(1.0f, 0.5f), pixelOffset = new float2(0, 0) }; + public static readonly LabelAlignment TopRight = new LabelAlignment { relativePivot = new float2(1.0f, 1.0f), pixelOffset = new float2(0, 0) }; + public static readonly LabelAlignment TopCenter = new LabelAlignment { relativePivot = new float2(0.5f, 1.0f), pixelOffset = new float2(0, 0) }; + public static readonly LabelAlignment Center = new LabelAlignment { relativePivot = new float2(0.5f, 0.5f), pixelOffset = new float2(0, 0) }; + + /// <summary> + /// Moves the text by the specified amount of pixels in screen-space. + /// + /// <code> + /// // Draw the label 20 pixels below the object + /// Draw.Label2D(transform.position, "Hello World", 14, LabelAlignment.TopCenter.withPixelOffset(0, -20)); + /// </code> + /// </summary> + public LabelAlignment withPixelOffset (float x, float y) { + return new LabelAlignment { + relativePivot = this.relativePivot, + pixelOffset = new float2(x, y), + }; + } + } + + /// <summary>Maximum allowed delay for a job that is drawing to a command buffer</summary> + public enum AllowedDelay { + /// <summary> + /// If the job is not complete at the end of the frame, drawing will block until it is completed. + /// This is recommended for most jobs that are expected to complete within a single frame. + /// </summary> + EndOfFrame, + /// <summary> + /// Wait indefinitely for the job to complete, and only submit the results for rendering once it is done. + /// This is recommended for long running jobs that may take many frames to complete. + /// </summary> + Infinite, + } + + /// <summary>Some static fields that need to be in a separate class because Burst doesn't support them</summary> + static class CommandBuilderSamplers { + internal static readonly ProfilerMarker MarkerConvert = new ProfilerMarker("Convert"); + internal static readonly ProfilerMarker MarkerSetLayout = new ProfilerMarker("SetLayout"); + internal static readonly ProfilerMarker MarkerUpdateVertices = new ProfilerMarker("UpdateVertices"); + internal static readonly ProfilerMarker MarkerUpdateIndices = new ProfilerMarker("UpdateIndices"); + internal static readonly ProfilerMarker MarkerSubmesh = new ProfilerMarker("Submesh"); + internal static readonly ProfilerMarker MarkerUpdateBuffer = new ProfilerMarker("UpdateComputeBuffer"); + + internal static readonly ProfilerMarker MarkerProcessCommands = new ProfilerMarker("Commands"); + internal static readonly ProfilerMarker MarkerCreateTriangles = new ProfilerMarker("CreateTriangles"); + } + + /// <summary> + /// Builder for drawing commands. + /// You can use this to queue many drawing commands. The commands will be queued for rendering when you call the Dispose method. + /// It is recommended that you use the <a href="https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/using-statement">using statement</a> which automatically calls the Dispose method. + /// + /// <code> + /// // Create a new CommandBuilder + /// using (var draw = DrawingManager.GetBuilder()) { + /// // Use the exact same API as the global Draw class + /// draw.WireBox(Vector3.zero, Vector3.one); + /// } + /// </code> + /// + /// Warning: You must call either <see cref="Dispose"/> or <see cref="DiscardAndDispose"/> when you are done with this object to avoid memory leaks. + /// </summary> + [StructLayout(LayoutKind.Sequential)] + [BurstCompile] + public partial struct CommandBuilder : IDisposable { + // Note: Many fields/methods are explicitly marked as private. This is because doxygen otherwise thinks they are public by default (like struct members are in c++) + + [NativeDisableUnsafePtrRestriction] + internal unsafe UnsafeAppendBuffer* buffer; + + private GCHandle gizmos; + + [NativeSetThreadIndex] + private int threadIndex; + + private DrawingData.BuilderData.BitPackedMeta uniqueID; + + internal unsafe CommandBuilder(UnsafeAppendBuffer* buffer, GCHandle gizmos, int threadIndex, DrawingData.BuilderData.BitPackedMeta uniqueID) { + this.buffer = buffer; + this.gizmos = gizmos; + this.threadIndex = threadIndex; + this.uniqueID = uniqueID; + } + + + internal CommandBuilder(DrawingData gizmos, Hasher hasher, RedrawScope frameRedrawScope, RedrawScope customRedrawScope, bool isGizmos, bool isBuiltInCommandBuilder, int sceneModeVersion) { + // We need to use a GCHandle instead of a normal reference to be able to pass this object to burst compiled function pointers. + // The NativeSetClassTypeToNullOnSchedule unfortunately only works together with the job system, not with raw functions. + this.gizmos = GCHandle.Alloc(gizmos, GCHandleType.Normal); + + threadIndex = 0; + uniqueID = gizmos.data.Reserve(isBuiltInCommandBuilder); + gizmos.data.Get(uniqueID).Init(hasher, frameRedrawScope, customRedrawScope, isGizmos, gizmos.GetNextDrawOrderIndex(), sceneModeVersion); + unsafe { + buffer = gizmos.data.Get(uniqueID).bufferPtr; + } + } + + internal unsafe int BufferSize { + get { + return buffer->Length; + } + set { + buffer->Length = value; + } + } + + /// <summary> + /// Wrapper for drawing in the XY plane. + /// + /// <code> + /// var p1 = new Vector2(0, 1); + /// var p2 = new Vector2(5, 7); + /// + /// // Draw it in the XY plane + /// Draw.xy.Line(p1, p2); + /// + /// // Draw it in the XZ plane + /// Draw.xz.Line(p1, p2); + /// </code> + /// + /// See: 2d-drawing (view in online documentation for working links) + /// See: <see cref="Draw.xz"/> + /// </summary> + public CommandBuilder2D xy => new CommandBuilder2D(this, true); + + /// <summary> + /// Wrapper for drawing in the XZ plane. + /// + /// <code> + /// var p1 = new Vector2(0, 1); + /// var p2 = new Vector2(5, 7); + /// + /// // Draw it in the XY plane + /// Draw.xy.Line(p1, p2); + /// + /// // Draw it in the XZ plane + /// Draw.xz.Line(p1, p2); + /// </code> + /// + /// See: 2d-drawing (view in online documentation for working links) + /// See: <see cref="Draw.xy"/> + /// </summary> + public CommandBuilder2D xz => new CommandBuilder2D(this, false); + + static readonly float3 DEFAULT_UP = new float3(0, 1, 0); + + /// <summary> + /// Can be set to render specifically to these cameras. + /// If you set this property to an array of cameras then this command builder will only be rendered + /// to the specified cameras. Setting this property bypasses <see cref="Drawing.DrawingManager.allowRenderToRenderTextures"/>. + /// The camera will be rendered to even if it renders to a render texture. + /// + /// A null value indicates that all valid cameras should be rendered to. This is the default value. + /// + /// <code> + /// var draw = DrawingManager.GetBuilder(true); + /// + /// draw.cameraTargets = new Camera[] { myCamera }; + /// // This sphere will only be rendered to myCamera + /// draw.WireSphere(Vector3.zero, 0.5f, Color.black); + /// draw.Dispose(); + /// </code> + /// + /// See: advanced (view in online documentation for working links) + /// </summary> + public Camera[] cameraTargets { + get { + if (gizmos.IsAllocated && gizmos.Target != null) { + var target = gizmos.Target as DrawingData; + if (target.data.StillExists(uniqueID)) { + return target.data.Get(uniqueID).meta.cameraTargets; + } + } + throw new System.Exception("Cannot get cameraTargets because the command builder has already been disposed or does not exist."); + } + set { + if (uniqueID.isBuiltInCommandBuilder) throw new System.Exception("You cannot set the camera targets for a built-in command builder. Create a custom command builder instead."); + if (gizmos.IsAllocated && gizmos.Target != null) { + var target = gizmos.Target as DrawingData; + if (!target.data.StillExists(uniqueID)) { + throw new System.Exception("Cannot set cameraTargets because the command builder has already been disposed or does not exist."); + } + target.data.Get(uniqueID).meta.cameraTargets = value; + } + } + } + + /// <summary>Submits this command builder for rendering</summary> + public void Dispose () { + if (uniqueID.isBuiltInCommandBuilder) throw new System.Exception("You cannot dispose a built-in command builder"); + DisposeInternal(); + } + + /// <summary> + /// Disposes this command builder after the given job has completed. + /// + /// This is convenient if you are using the entity-component-system/burst in Unity and don't know exactly when the job will complete. + /// + /// You will not be able to use this command builder on the main thread anymore. + /// + /// See: job-system (view in online documentation for working links) + /// </summary> + /// <param name="dependency">The job that must complete before this command builder is disposed.</param> + /// <param name="allowedDelay">Whether to block on this dependency before rendering the current frame or not. + /// If the job is expected to complete during a single frame, leave at the default of \reflink{AllowedDelay.EndOfFrame}. + /// But if the job is expected to take multiple frames to complete, you can set this to \reflink{AllowedDelay.Infinite}.</param> + public void DisposeAfter (JobHandle dependency, AllowedDelay allowedDelay = AllowedDelay.EndOfFrame) { + if (!gizmos.IsAllocated) throw new System.Exception("You cannot dispose an invalid command builder. Are you trying to dispose it twice?"); + try { + if (gizmos.IsAllocated && gizmos.Target != null) { + var target = gizmos.Target as DrawingData; + if (!target.data.StillExists(uniqueID)) { + throw new System.Exception("Cannot dispose the command builder because the drawing manager has been destroyed"); + } + target.data.Get(uniqueID).SubmitWithDependency(gizmos, dependency, allowedDelay); + } + } finally { + this = default; + } + } + + internal void DisposeInternal () { + if (!gizmos.IsAllocated) throw new System.Exception("You cannot dispose an invalid command builder. Are you trying to dispose it twice?"); + try { + if (gizmos.IsAllocated && gizmos.Target != null) { + var target = gizmos.Target as DrawingData; + if (!target.data.StillExists(uniqueID)) { + throw new System.Exception("Cannot dispose the command builder because the drawing manager has been destroyed"); + } + target.data.Get(uniqueID).Submit(gizmos.Target as DrawingData); + } + } finally { + gizmos.Free(); + this = default; + } + } + + /// <summary> + /// Discards the contents of this command builder without rendering anything. + /// If you are not going to draw anything (i.e. you do not call the <see cref="Dispose"/> method) then you must call this method to avoid + /// memory leaks. + /// </summary> + public void DiscardAndDispose () { + if (uniqueID.isBuiltInCommandBuilder) throw new System.Exception("You cannot dispose a built-in command builder"); + DiscardAndDisposeInternal(); + } + + internal void DiscardAndDisposeInternal () { + try { + if (gizmos.IsAllocated && gizmos.Target != null) { + var target = gizmos.Target as DrawingData; + if (!target.data.StillExists(uniqueID)) { + throw new System.Exception("Cannot dispose the command builder because the drawing manager has been destroyed"); + } + target.data.Release(uniqueID); + } + } finally { + if (gizmos.IsAllocated) gizmos.Free(); + this = default; + } + } + + /// <summary> + /// Pre-allocates the internal buffer to an additional size bytes. + /// This can give you a minor performance boost if you are drawing a lot of things. + /// + /// Note: Only resizes the buffer for the current thread. + /// </summary> + public void Preallocate (int size) { + Reserve(size); + } + + /// <summary>Internal rendering command</summary> + [System.Flags] + internal enum Command { + PushColorInline = 1 << 8, + PushColor = 0, + PopColor, + PushMatrix, + PushSetMatrix, + PopMatrix, + Line, + Circle, + CircleXZ, + Disc, + DiscXZ, + SphereOutline, + Box, + WirePlane, + WireBox, + SolidTriangle, + PushPersist, + PopPersist, + Text, + Text3D, + PushLineWidth, + PopLineWidth, + CaptureState, + } + + internal struct TriangleData { + public float3 a, b, c; + } + + /// <summary>Holds rendering data for a line</summary> + internal struct LineData { + public float3 a, b; + } + + internal struct LineDataV3 { + public Vector3 a, b; + } + + /// <summary>Holds rendering data for a circle</summary> + internal struct CircleXZData { + public float3 center; + public float radius, startAngle, endAngle; + } + + /// <summary>Holds rendering data for a circle</summary> + internal struct CircleData { + public float3 center; + public float3 normal; + public float radius; + } + + /// <summary>Holds rendering data for a sphere</summary> + internal struct SphereData { + public float3 center; + public float radius; + } + + /// <summary>Holds rendering data for a box</summary> + internal struct BoxData { + public float3 center; + public float3 size; + } + + internal struct PlaneData { + public float3 center; + public quaternion rotation; + public float2 size; + } + + internal struct PersistData { + public float endTime; + } + + internal struct LineWidthData { + public float pixels; + public bool automaticJoins; + } + + + + internal struct TextData { + public float3 center; + public LabelAlignment alignment; + public float sizeInPixels; + public int numCharacters; + } + + internal struct TextData3D { + public float3 center; + public quaternion rotation; + public LabelAlignment alignment; + public float size; + public int numCharacters; + } + + /// <summary>Ensures the buffer has room for at least N more bytes</summary> + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + private void Reserve (int additionalSpace) { + unsafe { + if (Unity.Burst.CompilerServices.Hint.Unlikely(threadIndex >= 0)) { +#if ENABLE_UNITY_COLLECTIONS_CHECKS + if (threadIndex < 0 || threadIndex >= JobsUtility.MaxJobThreadCount) throw new System.Exception("Thread index outside the expected range"); + if (threadIndex > 0 && uniqueID.isBuiltInCommandBuilder) throw new System.Exception("You should use a custom command builder when using the Unity Job System. Take a look at the documentation for more info."); + if (buffer == null) throw new System.Exception("CommandBuilder does not have a valid buffer. Is it properly initialized?"); + + // Exploit the fact that right after this package has drawn gizmos the buffers will be empty + // and the next task is that Unity will render its own internal gizmos. + // We can therefore easily (and without a high performance cost) + // trap accidental Draw.* calls from OnDrawGizmos functions + // by doing this check when the first Reserve call is made. + AssertNotRendering(); +#endif + + buffer += threadIndex; + threadIndex = -1; + } + + var newLength = buffer->Length + additionalSpace; + if (newLength > buffer->Capacity) { +#if ENABLE_UNITY_COLLECTIONS_CHECKS + // This really should run every time we access the buffer... but that would be a bit slow + // This code will catch the error eventually. + AssertBufferExists(); + const int MAX_BUFFER_SIZE = 1024 * 1024 * 256; // 256 MB + if (buffer->Length * 2 > MAX_BUFFER_SIZE) { + throw new System.Exception("CommandBuilder buffer is very large. Are you trying to draw things in an infinite loop?"); + } +#endif + buffer->SetCapacity(math.max(newLength, buffer->Length * 2)); + } + } + } + + [BurstDiscard] + private void AssertBufferExists () { + if (!gizmos.IsAllocated || gizmos.Target == null || !(gizmos.Target as DrawingData).data.StillExists(uniqueID)) { + // This command builder is invalid, clear all data on it to prevent it being used again + this = default; + throw new System.Exception("This command builder no longer exists. Are you trying to draw to a command builder which has already been disposed?"); + } + } + + [BurstDiscard] + static void AssertNotRendering () { + // Some checking to see if drawing is being done from inside OnDrawGizmos + // This check is relatively fast (about 0.05 ms), but we still do it only every 128th frame for performance reasons + if (!GizmoContext.drawingGizmos && !JobsUtility.IsExecutingJob && (Time.renderedFrameCount & 127) == 0) { + // Inspect the stack-trace to be able to provide more helpful error messages + var st = StackTraceUtility.ExtractStackTrace(); + if (st.Contains("OnDrawGizmos")) { + throw new System.Exception("You are trying to use Draw.* functions from within Unity's OnDrawGizmos function. Use this package's gizmo callbacks instead (see the documentation)."); + } + } + } + + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + internal void Reserve<A>() where A : struct { + Reserve(UnsafeUtility.SizeOf<Command>() + UnsafeUtility.SizeOf<A>()); + } + + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + internal void Reserve<A, B>() where A : struct where B : struct { + Reserve(UnsafeUtility.SizeOf<Command>() * 2 + UnsafeUtility.SizeOf<A>() + UnsafeUtility.SizeOf<B>()); + } + + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + internal void Reserve<A, B, C>() where A : struct where B : struct where C : struct { + Reserve(UnsafeUtility.SizeOf<Command>() * 3 + UnsafeUtility.SizeOf<A>() + UnsafeUtility.SizeOf<B>() + UnsafeUtility.SizeOf<C>()); + } + + /// <summary> + /// Converts a Color to a Color32. + /// This method is faster than Unity's native color conversion, especially when using Burst. + /// </summary> + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + internal static unsafe uint ConvertColor (Color color) { + // If SSE2 is supported (which it is on essentially all X86 CPUs) + // then we can use a much faster conversion from Color to Color32. + // This will only be possible inside Burst. + if (Unity.Burst.Intrinsics.X86.Sse2.IsSse2Supported) { + // Convert from 0-1 float range to 0-255 integer range + var ci = (int4)(255 * new float4(color.r, color.g, color.b, color.a) + 0.5f); + var v32 = new Unity.Burst.Intrinsics.v128(ci.x, ci.y, ci.z, ci.w); + // Convert four 32-bit numbers to four 16-bit numbers + var v16 = Unity.Burst.Intrinsics.X86.Sse2.packs_epi32(v32, v32); + // Convert four 16-bit numbers to four 8-bit numbers + var v8 = Unity.Burst.Intrinsics.X86.Sse2.packus_epi16(v16, v16); + return v8.UInt0; + } else { + // If we don't have SSE2 (most likely we are not running inside Burst), + // then we will do a manual conversion from Color to Color32. + // This is significantly faster than just casting to a Color32. + var r = (uint)Mathf.Clamp((int)(color.r*255f + 0.5f), 0, 255); + var g = (uint)Mathf.Clamp((int)(color.g*255f + 0.5f), 0, 255); + var b = (uint)Mathf.Clamp((int)(color.b*255f + 0.5f), 0, 255); + var a = (uint)Mathf.Clamp((int)(color.a*255f + 0.5f), 0, 255); + return (a << 24) | (b << 16) | (g << 8) | r; + } + } + + internal unsafe void Add<T>(T value) where T : struct { + int num = UnsafeUtility.SizeOf<T>(); + var buffer = this.buffer; + var bufferSize = buffer->Length; + // We assume this because the Reserve function has already taken care of that. + // This removes a few branches from the assembly when running in burst. + Unity.Burst.CompilerServices.Hint.Assume(buffer->Ptr != null); + Unity.Burst.CompilerServices.Hint.Assume(buffer->Ptr + bufferSize != null); + + unsafe { + UnsafeUtility.CopyStructureToPtr(ref value, (void*)((byte*)buffer->Ptr + bufferSize)); + buffer->Length = bufferSize + num; + } + } + + public struct ScopeMatrix : IDisposable { + internal CommandBuilder builder; + public void Dispose () { +#if ENABLE_UNITY_COLLECTIONS_CHECKS + if (!builder.gizmos.IsAllocated || !(builder.gizmos.Target is DrawingData data) || !data.data.StillExists(builder.uniqueID)) throw new System.InvalidOperationException("The drawing instance this matrix scope belongs to no longer exists. Matrix scopes cannot survive for longer than a frame unless you have a custom drawing instance. Are you using a matrix scope inside a coroutine?"); +#endif + unsafe { + builder.PopMatrix(); + builder.buffer = null; + } + } + } + + public struct ScopeColor : IDisposable { + internal CommandBuilder builder; + public void Dispose () { +#if ENABLE_UNITY_COLLECTIONS_CHECKS + if (!builder.gizmos.IsAllocated || !(builder.gizmos.Target is DrawingData data) || !data.data.StillExists(builder.uniqueID)) throw new System.InvalidOperationException("The drawing instance this color scope belongs to no longer exists. Color scopes cannot survive for longer than a frame unless you have a custom drawing instance. Are you using a color scope inside a coroutine?"); +#endif + unsafe { + builder.PopColor(); + builder.buffer = null; + } + } + } + + public struct ScopePersist : IDisposable { + internal CommandBuilder builder; + public void Dispose () { +#if ENABLE_UNITY_COLLECTIONS_CHECKS + if (!builder.gizmos.IsAllocated || !(builder.gizmos.Target is DrawingData data) || !data.data.StillExists(builder.uniqueID)) throw new System.InvalidOperationException("The drawing instance this persist scope belongs to no longer exists. Persist scopes cannot survive for longer than a frame unless you have a custom drawing instance. Are you using a persist scope inside a coroutine?"); +#endif + unsafe { + builder.PopDuration(); + builder.buffer = null; + } + } + } + + /// <summary> + /// Scope that does nothing. + /// Used for optimization in standalone builds. + /// </summary> + public struct ScopeEmpty : IDisposable { + public void Dispose () { + } + } + + public struct ScopeLineWidth : IDisposable { + internal CommandBuilder builder; + public void Dispose () { +#if ENABLE_UNITY_COLLECTIONS_CHECKS + if (!builder.gizmos.IsAllocated || !(builder.gizmos.Target is DrawingData data) || !data.data.StillExists(builder.uniqueID)) throw new System.InvalidOperationException("The drawing instance this line width scope belongs to no longer exists. Line width scopes cannot survive for longer than a frame unless you have a custom drawing instance. Are you using a line width scope inside a coroutine?"); +#endif + unsafe { + builder.PopLineWidth(); + builder.buffer = null; + } + } + } + + /// <summary> + /// Scope to draw multiple things with an implicit matrix transformation. + /// All coordinates for items drawn inside the scope will be multiplied by the matrix. + /// If WithMatrix scopes are nested then coordinates are multiplied by all nested matrices in order. + /// + /// <code> + /// using (Draw.InLocalSpace(transform)) { + /// // Draw a box at (0,0,0) relative to the current object + /// // This means it will show up at the object's position + /// Draw.WireBox(Vector3.zero, Vector3.one); + /// } + /// + /// // Equivalent code using the lower level WithMatrix scope + /// using (Draw.WithMatrix(transform.localToWorldMatrix)) { + /// Draw.WireBox(Vector3.zero, Vector3.one); + /// } + /// </code> + /// + /// See: <see cref="InLocalSpace"/> + /// </summary> + [BurstDiscard] + public ScopeMatrix WithMatrix (Matrix4x4 matrix) { + PushMatrix(matrix); + // TODO: Keep track of alive scopes and prevent dispose unless all scopes have been disposed + unsafe { + return new ScopeMatrix { builder = this }; + } + } + + /// <summary> + /// Scope to draw multiple things with an implicit matrix transformation. + /// All coordinates for items drawn inside the scope will be multiplied by the matrix. + /// If WithMatrix scopes are nested then coordinates are multiplied by all nested matrices in order. + /// + /// <code> + /// using (Draw.InLocalSpace(transform)) { + /// // Draw a box at (0,0,0) relative to the current object + /// // This means it will show up at the object's position + /// Draw.WireBox(Vector3.zero, Vector3.one); + /// } + /// + /// // Equivalent code using the lower level WithMatrix scope + /// using (Draw.WithMatrix(transform.localToWorldMatrix)) { + /// Draw.WireBox(Vector3.zero, Vector3.one); + /// } + /// </code> + /// + /// See: <see cref="InLocalSpace"/> + /// </summary> + [BurstDiscard] + public ScopeMatrix WithMatrix (float3x3 matrix) { + PushMatrix(new float4x4(matrix, float3.zero)); + // TODO: Keep track of alive scopes and prevent dispose unless all scopes have been disposed + unsafe { + return new ScopeMatrix { builder = this }; + } + } + + /// <summary> + /// Scope to draw multiple things with the same color. + /// + /// <code> + /// void Update () { + /// using (Draw.WithColor(Color.red)) { + /// Draw.Line(new Vector3(0, 0, 0), new Vector3(1, 1, 1)); + /// Draw.Line(new Vector3(0, 0, 0), new Vector3(0, 1, 2)); + /// } + /// } + /// </code> + /// + /// Any command that is passed an explicit color parameter will override this color. + /// If another color scope is nested inside this one then that scope will override this color. + /// </summary> + [BurstDiscard] + public ScopeColor WithColor (Color color) { + PushColor(color); + unsafe { + return new ScopeColor { builder = this }; + } + } + + /// <summary> + /// Scope to draw multiple things for a longer period of time. + /// + /// Normally drawn items will only be rendered for a single frame. + /// Using a persist scope you can make the items be drawn for any amount of time. + /// + /// <code> + /// void Update () { + /// using (Draw.WithDuration(1.0f)) { + /// var offset = Time.time; + /// Draw.Line(new Vector3(offset, 0, 0), new Vector3(offset, 0, 1)); + /// } + /// } + /// </code> + /// + /// Note: Outside of play mode the duration is measured against Unity's Time.realtimeSinceStartup. + /// + /// Warning: It is recommended not to use this inside a DrawGizmos callback since DrawGizmos is called every frame anyway. + /// </summary> + /// <param name="duration">How long the drawn items should persist in seconds.</param> + + [BurstDiscard] + public ScopePersist WithDuration (float duration) { + PushDuration(duration); + unsafe { + return new ScopePersist { builder = this }; + } + } + + /// <summary> + /// Scope to draw multiple things with a given line width. + /// + /// Note that the line join algorithm is a quite simple one optimized for speed. It normally looks good on a 2D plane, but if the polylines curve a lot in 3D space then + /// it can look odd from some angles. + /// + /// [Open online documentation to see images] + /// + /// In the picture the top row has automaticJoins enabled and in the bottom row it is disabled. + /// </summary> + /// <param name="pixels">Line width in pixels</param> + /// <param name="automaticJoins">If true then sequences of lines that are adjacent will be automatically joined at their vertices. This typically produces nicer polylines without weird gaps.</param> + [BurstDiscard] + public ScopeLineWidth WithLineWidth (float pixels, bool automaticJoins = true) { + PushLineWidth(pixels, automaticJoins); + unsafe { + return new ScopeLineWidth { builder = this }; + } + } + + /// <summary> + /// Scope to draw multiple things relative to a transform object. + /// All coordinates for items drawn inside the scope will be multiplied by the transform's localToWorldMatrix. + /// + /// <code> + /// void Update () { + /// using (Draw.InLocalSpace(transform)) { + /// // Draw a box at (0,0,0) relative to the current object + /// // This means it will show up at the object's position + /// // The box is also rotated and scaled with the transform + /// Draw.WireBox(Vector3.zero, Vector3.one); + /// } + /// } + /// </code> + /// + /// [Open online documentation to see videos] + /// </summary> + [BurstDiscard] + public ScopeMatrix InLocalSpace (Transform transform) { + return WithMatrix(transform.localToWorldMatrix); + } + + /// <summary> + /// Scope to draw multiple things in screen space of a camera. + /// If you draw 2D coordinates (i.e. (x,y,0)) they will be projected onto a plane approximately [2*near clip plane of the camera] world units in front of the camera (but guaranteed to be between the near and far planes). + /// + /// The lower left corner of the camera is (0,0,0) and the upper right is (camera.pixelWidth, camera.pixelHeight, 0) + /// + /// Note: As a corollary, the centers of pixels are offset by 0.5. So for example the center of the top left pixel is at (0.5, 0.5, 0). + /// Therefore, if you want to draw 1 pixel wide lines in screen space, you may want to offset the coordinates by 0.5 pixels. + /// + /// See: <see cref="InLocalSpace"/> + /// See: <see cref="WithMatrix"/> + /// </summary> + [BurstDiscard] + public ScopeMatrix InScreenSpace (Camera camera) { + return WithMatrix(camera.cameraToWorldMatrix * camera.nonJitteredProjectionMatrix.inverse * Matrix4x4.TRS(new Vector3(-1.0f, -1.0f, 0), Quaternion.identity, new Vector3(2.0f/camera.pixelWidth, 2.0f/camera.pixelHeight, 1))); + } + + /// <summary> + /// Multiply all coordinates until the next PopMatrix with the given matrix. + /// This differs from <see cref="PushSetMatrix"/> in that this stacks with all previously pushed matrices while <see cref="PushSetMatrix"/> does not. + /// </summary> + public void PushMatrix (Matrix4x4 matrix) { + Reserve<float4x4>(); + Add(Command.PushMatrix); + Add(matrix); + } + + /// <summary> + /// Multiply all coordinates until the next PopMatrix with the given matrix. + /// This differs from <see cref="PushSetMatrix"/> in that this stacks with all previously pushed matrices while <see cref="PushSetMatrix"/> does not. + /// </summary> + public void PushMatrix (float4x4 matrix) { + Reserve<float4x4>(); + Add(Command.PushMatrix); + Add(matrix); + } + + /// <summary> + /// Multiply all coordinates until the next PopMatrix with the given matrix. + /// This differs from <see cref="PushMatrix"/> in that this sets the current matrix directly while <see cref="PushMatrix"/> stacks with all previously pushed matrices. + /// </summary> + public void PushSetMatrix (Matrix4x4 matrix) { + Reserve<float4x4>(); + Add(Command.PushSetMatrix); + Add((float4x4)matrix); + } + + /// <summary> + /// Multiply all coordinates until the next PopMatrix with the given matrix. + /// This differs from <see cref="PushMatrix"/> in that this sets the current matrix directly while <see cref="PushMatrix"/> stacks with all previously pushed matrices. + /// </summary> + public void PushSetMatrix (float4x4 matrix) { + Reserve<float4x4>(); + Add(Command.PushSetMatrix); + Add(matrix); + } + + /// <summary>Pops a matrix from the stack</summary> + public void PopMatrix () { + Reserve(4); + Add(Command.PopMatrix); + } + + /// <summary> + /// Draws everything until the next PopColor with the given color. + /// Any command that is passed an explicit color parameter will override this color. + /// If another color scope is nested inside this one then that scope will override this color. + /// </summary> + public void PushColor (Color color) { + Reserve<Color32>(); + Add(Command.PushColor); + Add(ConvertColor(color)); + } + + /// <summary>Pops a color from the stack</summary> + public void PopColor () { + Reserve(4); + Add(Command.PopColor); + } + + /// <summary> + /// Draws everything until the next PopDuration for a number of seconds. + /// Warning: This is not recommended inside a DrawGizmos callback since DrawGizmos is called every frame anyway. + /// </summary> + public void PushDuration (float duration) { + Reserve<PersistData>(); + Add(Command.PushPersist); + // We must use the BurstTime variable which is updated more rarely than Time.time. + // This is necessary because this code may be called from a burst job or from a different thread. + // Time.time can only be accessed in the main thread. + Add(new PersistData { endTime = SharedDrawingData.BurstTime.Data + duration }); + } + + /// <summary>Pops a duration scope from the stack</summary> + public void PopDuration () { + Reserve(4); + Add(Command.PopPersist); + } + + /// <summary> + /// Draws everything until the next PopPersist for a number of seconds. + /// Warning: This is not recommended inside a DrawGizmos callback since DrawGizmos is called every frame anyway. + /// + /// Deprecated: Renamed to <see cref="PushDuration"/> + /// </summary> + [System.Obsolete("Renamed to PushDuration for consistency")] + public void PushPersist (float duration) { + PushDuration(duration); + } + + /// <summary> + /// Pops a persist scope from the stack. + /// Deprecated: Renamed to <see cref="PopDuration"/> + /// </summary> + [System.Obsolete("Renamed to PopDuration for consistency")] + public void PopPersist () { + PopDuration(); + } + + /// <summary> + /// Draws all lines until the next PopLineWidth with a given line width in pixels. + /// + /// Note that the line join algorithm is a quite simple one optimized for speed. It normally looks good on a 2D plane, but if the polylines curve a lot in 3D space then + /// it can look odd from some angles. + /// + /// [Open online documentation to see images] + /// + /// In the picture the top row has automaticJoins enabled and in the bottom row it is disabled. + /// </summary> + /// <param name="pixels">Line width in pixels</param> + /// <param name="automaticJoins">If true then sequences of lines that are adjacent will be automatically joined at their vertices. This typically produces nicer polylines without weird gaps.</param> + public void PushLineWidth (float pixels, bool automaticJoins = true) { + if (pixels < 0) throw new System.ArgumentOutOfRangeException("pixels", "Line width must be positive"); + + Reserve<LineWidthData>(); + Add(Command.PushLineWidth); + Add(new LineWidthData { pixels = pixels, automaticJoins = automaticJoins }); + } + + /// <summary>Pops a line width scope from the stack</summary> + public void PopLineWidth () { + Reserve(4); + Add(Command.PopLineWidth); + } + + /// <summary> + /// Draws a line between two points. + /// + /// [Open online documentation to see images] + /// + /// <code> + /// void Update () { + /// Draw.Line(Vector3.zero, Vector3.up); + /// } + /// </code> + /// </summary> + public void Line (float3 a, float3 b) { + Reserve<LineData>(); + Add(Command.Line); + Add(new LineData { a = a, b = b }); + } + + /// <summary> + /// Draws a line between two points. + /// + /// [Open online documentation to see images] + /// + /// <code> + /// void Update () { + /// Draw.Line(Vector3.zero, Vector3.up); + /// } + /// </code> + /// </summary> + public void Line (Vector3 a, Vector3 b) { + Reserve<LineData>(); + // Add(Command.Line); + // Add(new LineDataV3 { a = a, b = b }); + + // The code below is equivalent to the commented out code above. + // But drawing lines is the most common operation so it needs to be really fast. + // Having this hardcoded improves line rendering performance by about 8%. + var bufferSize = BufferSize; + + unsafe { + var newLen = bufferSize + 4 + 24; +#if ENABLE_UNITY_COLLECTIONS_CHECKS + UnityEngine.Assertions.Assert.IsTrue(newLen <= buffer->Capacity); +#endif + var ptr = (byte*)buffer->Ptr + bufferSize; + *(Command*)ptr = Command.Line; + var lineData = (LineDataV3*)(ptr + 4); + lineData->a = a; + lineData->b = b; + buffer->Length = newLen; + } + } + + /// <summary> + /// Draws a line between two points. + /// + /// [Open online documentation to see images] + /// + /// <code> + /// void Update () { + /// Draw.Line(Vector3.zero, Vector3.up); + /// } + /// </code> + /// </summary> + public void Line (Vector3 a, Vector3 b, Color color) { + Reserve<Color32, LineData>(); + // Add(Command.Line | Command.PushColorInline); + // Add(ConvertColor(color)); + // Add(new LineDataV3 { a = a, b = b }); + + // The code below is equivalent to the code which is commented out above. + // But drawing lines is the most common operation so it needs to be really fast + // Having this hardcoded improves line rendering performance by about 8%. + var bufferSize = BufferSize; + + unsafe { + var newLen = bufferSize + 4 + 24 + 4; +#if ENABLE_UNITY_COLLECTIONS_CHECKS + UnityEngine.Assertions.Assert.IsTrue(newLen <= buffer->Capacity); +#endif + var ptr = (byte*)buffer->Ptr + bufferSize; + *(Command*)ptr = Command.Line | Command.PushColorInline; + *(uint*)(ptr + 4) = ConvertColor(color); + var lineData = (LineDataV3*)(ptr + 8); + lineData->a = a; + lineData->b = b; + buffer->Length = newLen; + } + } + + /// <summary> + /// Draws a ray starting at a point and going in the given direction. + /// The ray will end at origin + direction. + /// + /// [Open online documentation to see images] + /// + /// <code> + /// Draw.Ray(Vector3.zero, Vector3.up); + /// </code> + /// </summary> + public void Ray (float3 origin, float3 direction) { + Line(origin, origin + direction); + } + + /// <summary> + /// Draws a ray with a given length. + /// + /// [Open online documentation to see images] + /// + /// <code> + /// Draw.Ray(Camera.main.ScreenPointToRay(Vector3.zero), 10); + /// </code> + /// </summary> + public void Ray (Ray ray, float length) { + Line(ray.origin, ray.origin + ray.direction * length); + } + + /// <summary> + /// Draws an arc between two points. + /// + /// The rendered arc is the shortest arc between the two points. + /// The radius of the arc will be equal to the distance between center and start. + /// + /// [Open online documentation to see images] + /// <code> + /// float a1 = Mathf.PI*0.9f; + /// float a2 = Mathf.PI*0.1f; + /// var arcStart = new float3(Mathf.Cos(a1), 0, Mathf.Sin(a1)); + /// var arcEnd = new float3(Mathf.Cos(a2), 0, Mathf.Sin(a2)); + /// Draw.Arc(new float3(0, 0, 0), arcStart, arcEnd, color); + /// </code> + /// + /// See: <see cref="CommandBuilder2D.Circle(float3,float,float,float)"/> + /// </summary> + /// <param name="center">Center of the imaginary circle that the arc is part of.</param> + /// <param name="start">Starting point of the arc.</param> + /// <param name="end">End point of the arc.</param> + public void Arc (float3 center, float3 start, float3 end) { + var d1 = start - center; + var d2 = end - center; + var normal = math.cross(d2, d1); + + if (math.any(normal != 0) && math.all(math.isfinite(normal))) { + var m = Matrix4x4.TRS(center, Quaternion.LookRotation(d1, normal), Vector3.one); + var angle = Vector3.SignedAngle(d1, d2, normal) * Mathf.Deg2Rad; + PushMatrix(m); + CircleXZInternal(float3.zero, math.length(d1), 90 * Mathf.Deg2Rad, 90 * Mathf.Deg2Rad - angle); + PopMatrix(); + } + } + + /// <summary> + /// Draws a circle in the XZ plane. + /// + /// You can draw an arc by supplying the startAngle and endAngle parameters. + /// + /// [Open online documentation to see images] + /// + /// See: <see cref="Circle(float3,float3,float)"/> + /// See: <see cref="CircleXY(float3,float,float,float)"/> + /// See: <see cref="Arc(float3,float3,float3)"/> + /// </summary> + /// <param name="center">Center of the circle or arc.</param> + /// <param name="radius">Radius of the circle or arc.</param> + /// <param name="startAngle">Starting angle in radians. 0 corrsponds to the positive X axis.</param> + /// <param name="endAngle">End angle in radians.</param> + [System.Obsolete("Use Draw.xz.Circle instead")] + public void CircleXZ (float3 center, float radius, float startAngle = 0f, float endAngle = 2 * Mathf.PI) { + CircleXZInternal(center, radius, startAngle, endAngle); + } + + internal void CircleXZInternal (float3 center, float radius, float startAngle = 0f, float endAngle = 2 * Mathf.PI) { + Reserve<CircleXZData>(); + Add(Command.CircleXZ); + Add(new CircleXZData { center = center, radius = radius, startAngle = startAngle, endAngle = endAngle }); + } + + internal void CircleXZInternal (float3 center, float radius, float startAngle, float endAngle, Color color) { + Reserve<Color32, CircleXZData>(); + Add(Command.CircleXZ | Command.PushColorInline); + Add(ConvertColor(color)); + Add(new CircleXZData { center = center, radius = radius, startAngle = startAngle, endAngle = endAngle }); + } + + internal static readonly float4x4 XZtoXYPlaneMatrix = float4x4.RotateX(-math.PI*0.5f); + internal static readonly float4x4 XZtoYZPlaneMatrix = float4x4.RotateZ(math.PI*0.5f); + + /// <summary> + /// Draws a circle in the XY plane. + /// + /// You can draw an arc by supplying the startAngle and endAngle parameters. + /// + /// [Open online documentation to see images] + /// + /// See: <see cref="Circle(float3,float3,float)"/> + /// See: <see cref="Arc(float3,float3,float3)"/> + /// </summary> + /// <param name="center">Center of the circle or arc.</param> + /// <param name="radius">Radius of the circle or arc.</param> + /// <param name="startAngle">Starting angle in radians. 0 corrsponds to the positive X axis.</param> + /// <param name="endAngle">End angle in radians.</param> + [System.Obsolete("Use Draw.xy.Circle instead")] + public void CircleXY (float3 center, float radius, float startAngle = 0f, float endAngle = 2 * Mathf.PI) { + PushMatrix(XZtoXYPlaneMatrix); + CircleXZ(new float3(center.x, -center.z, center.y), radius, startAngle, endAngle); + PopMatrix(); + } + + /// <summary> + /// Draws a circle. + /// + /// [Open online documentation to see images] + /// + /// Note: This overload does not allow you to draw an arc. For that purpose use <see cref="Arc"/>, <see cref="CircleXY"/> or <see cref="CircleXZ"/> instead. + /// </summary> + public void Circle (float3 center, float3 normal, float radius) { + Reserve<CircleData>(); + Add(Command.Circle); + Add(new CircleData { center = center, normal = normal, radius = radius }); + } + + /// <summary> + /// Draws a solid arc between two points. + /// + /// The rendered arc is the shortest arc between the two points. + /// The radius of the arc will be equal to the distance between center and start. + /// + /// [Open online documentation to see images] + /// <code> + /// float a1 = Mathf.PI*0.9f; + /// float a2 = Mathf.PI*0.1f; + /// var arcStart = new float3(Mathf.Cos(a1), 0, Mathf.Sin(a1)); + /// var arcEnd = new float3(Mathf.Cos(a2), 0, Mathf.Sin(a2)); + /// Draw.SolidArc(new float3(0, 0, 0), arcStart, arcEnd, color); + /// </code> + /// + /// See: <see cref="CommandBuilder2D.SolidCircle(float3,float,float,float)"/> + /// </summary> + /// <param name="center">Center of the imaginary circle that the arc is part of.</param> + /// <param name="start">Starting point of the arc.</param> + /// <param name="end">End point of the arc.</param> + public void SolidArc (float3 center, float3 start, float3 end) { + var d1 = start - center; + var d2 = end - center; + var normal = math.cross(d2, d1); + + if (math.any(normal)) { + var m = Matrix4x4.TRS(center, Quaternion.LookRotation(d1, normal), Vector3.one); + var angle = Vector3.SignedAngle(d1, d2, normal) * Mathf.Deg2Rad; + PushMatrix(m); + SolidCircleXZInternal(float3.zero, math.length(d1), 90 * Mathf.Deg2Rad, 90 * Mathf.Deg2Rad - angle); + PopMatrix(); + } + } + + /// <summary> + /// Draws a disc in the XZ plane. + /// + /// You can draw an arc by supplying the startAngle and endAngle parameters. + /// + /// [Open online documentation to see images] + /// + /// See: <see cref="SolidCircle(float3,float3,float)"/> + /// See: <see cref="CommandBuilder2D.SolidCircle(float3,float,float,float)"/> + /// See: <see cref="SolidArc(float3,float3,float3)"/> + /// </summary> + /// <param name="center">Center of the disc or solid arc.</param> + /// <param name="radius">Radius of the disc or solid arc.</param> + /// <param name="startAngle">Starting angle in radians. 0 corrsponds to the positive X axis.</param> + /// <param name="endAngle">End angle in radians.</param> + [System.Obsolete("Use Draw.xz.SolidCircle instead")] + public void SolidCircleXZ (float3 center, float radius, float startAngle = 0f, float endAngle = 2 * Mathf.PI) { + SolidCircleXZInternal(center, radius, startAngle, endAngle); + } + + internal void SolidCircleXZInternal (float3 center, float radius, float startAngle = 0f, float endAngle = 2 * Mathf.PI) { + Reserve<CircleXZData>(); + Add(Command.DiscXZ); + Add(new CircleXZData { center = center, radius = radius, startAngle = startAngle, endAngle = endAngle }); + } + + internal void SolidCircleXZInternal (float3 center, float radius, float startAngle, float endAngle, Color color) { + Reserve<Color32, CircleXZData>(); + Add(Command.DiscXZ | Command.PushColorInline); + Add(ConvertColor(color)); + Add(new CircleXZData { center = center, radius = radius, startAngle = startAngle, endAngle = endAngle }); + } + + /// <summary> + /// Draws a disc in the XY plane. + /// + /// You can draw an arc by supplying the startAngle and endAngle parameters. + /// + /// [Open online documentation to see images] + /// + /// See: <see cref="SolidCircle(float3,float3,float)"/> + /// See: <see cref="CommandBuilder2D.SolidCircle(float3,float,float,float)"/> + /// See: <see cref="SolidArc(float3,float3,float3)"/> + /// </summary> + /// <param name="center">Center of the disc or solid arc.</param> + /// <param name="radius">Radius of the disc or solid arc.</param> + /// <param name="startAngle">Starting angle in radians. 0 corrsponds to the positive X axis.</param> + /// <param name="endAngle">End angle in radians.</param> + [System.Obsolete("Use Draw.xy.SolidCircle instead")] + public void SolidCircleXY (float3 center, float radius, float startAngle = 0f, float endAngle = 2 * Mathf.PI) { + PushMatrix(XZtoXYPlaneMatrix); + SolidCircleXZInternal(new float3(center.x, -center.z, center.y), radius, startAngle, endAngle); + PopMatrix(); + } + + /// <summary> + /// Draws a disc. + /// + /// [Open online documentation to see images] + /// + /// Note: This overload does not allow you to draw an arc. For that purpose use <see cref="SolidArc"/> or <see cref="CommandBuilder2D.SolidCircle(float3,float,float,float)"/> instead. + /// </summary> + public void SolidCircle (float3 center, float3 normal, float radius) { + Reserve<CircleData>(); + Add(Command.Disc); + Add(new CircleData { center = center, normal = normal, radius = radius }); + } + + /// <summary> + /// Draws a circle outline around a sphere. + /// + /// Visually, this is a circle that always faces the camera, and is resized automatically to fit the sphere. + /// + /// [Open online documentation to see images] + /// </summary> + public void SphereOutline (float3 center, float radius) { + Reserve<SphereData>(); + Add(Command.SphereOutline); + Add(new SphereData { center = center, radius = radius }); + } + + /// <summary> + /// Draws a cylinder. + /// The cylinder's bottom circle will be centered at the bottom parameter and similarly for the top circle. + /// + /// <code> + /// // Draw a tilted cylinder between the points (0,0,0) and (1,1,1) with a radius of 0.5 + /// Draw.WireCylinder(Vector3.zero, Vector3.one, 0.5f, Color.black); + /// </code> + /// + /// [Open online documentation to see images] + /// </summary> + public void WireCylinder (float3 bottom, float3 top, float radius) { + WireCylinder(bottom, top - bottom, math.length(top - bottom), radius); + } + + /// <summary> + /// Draws a cylinder. + /// + /// <code> + /// // Draw a two meter tall cylinder at the world origin with a radius of 0.5 + /// Draw.WireCylinder(Vector3.zero, Vector3.up, 2, 0.5f, Color.black); + /// </code> + /// + /// [Open online documentation to see images] + /// </summary> + /// <param name="position">The center of the cylinder's "bottom" circle.</param> + /// <param name="up">The cylinder's main axis. Does not have to be normalized. If zero, nothing will be drawn.</param> + /// <param name="height">The length of the cylinder, as measured along it's main axis.</param> + /// <param name="radius">The radius of the cylinder.</param> + public void WireCylinder (float3 position, float3 up, float height, float radius) { + up = math.normalizesafe(up); + if (math.all(up == 0) || math.any(math.isnan(up)) || math.isnan(height) || math.isnan(radius)) return; + + OrthonormalBasis(up, out var basis1, out var basis2); + + PushMatrix(new float4x4( + new float4(basis1 * radius, 0), + new float4(up * height, 0), + new float4(basis2 * radius, 0), + new float4(position, 1) + )); + + CircleXZInternal(float3.zero, 1); + if (height > 0) { + CircleXZInternal(new float3(0, 1, 0), 1); + Line(new float3(1, 0, 0), new float3(1, 1, 0)); + Line(new float3(-1, 0, 0), new float3(-1, 1, 0)); + Line(new float3(0, 0, 1), new float3(0, 1, 1)); + Line(new float3(0, 0, -1), new float3(0, 1, -1)); + } + PopMatrix(); + } + + /// <summary> + /// Constructs an orthonormal basis from a single normal vector. + /// + /// This is similar to math.orthonormal_basis, but it tries harder to be continuous in its input. + /// In contrast, math.orthonormal_basis has a tendency to jump around even with small changes to the normal. + /// + /// It's not as fast as math.orthonormal_basis, though. + /// </summary> + static void OrthonormalBasis (float3 normal, out float3 basis1, out float3 basis2) { + basis1 = math.cross(normal, new float3(1, 1, 1)); + if (math.all(basis1 == 0)) basis1 = math.cross(normal, new float3(-1, 1, 1)); + basis1 = math.normalizesafe(basis1); + basis2 = math.cross(normal, basis1); + } + + /// <summary> + /// Draws a capsule with a (start,end) parameterization. + /// + /// The behavior of this method matches common Unity APIs such as Physics.CheckCapsule. + /// + /// <code> + /// // Draw a tilted capsule between the points (0,0,0) and (1,1,1) with a radius of 0.5 + /// Draw.WireCapsule(Vector3.zero, Vector3.one, 0.5f, Color.black); + /// </code> + /// + /// [Open online documentation to see images] + /// </summary> + /// <param name="start">Center of the start hemisphere of the capsule.</param> + /// <param name="end">Center of the end hemisphere of the capsule.</param> + /// <param name="radius">Radius of the capsule.</param> + public void WireCapsule (float3 start, float3 end, float radius) { + var dir = end - start; + var length = math.length(dir); + + if (length < 0.0001) { + // The endpoints are the same, we can't draw a capsule from this because we don't know its orientation. + // Draw a sphere as a fallback + WireSphere(start, radius); + } else { + var normalized_dir = dir / length; + + WireCapsule(start - normalized_dir*radius, normalized_dir, length + 2*radius, radius); + } + } + + // TODO: Change to center, up, height parameterization + /// <summary> + /// Draws a capsule with a (position,direction/length) parameterization. + /// + /// <code> + /// // Draw a capsule that touches the y=0 plane, is 2 meters tall and has a radius of 0.5 + /// Draw.WireCapsule(Vector3.zero, Vector3.up, 2.0f, 0.5f, Color.black); + /// </code> + /// + /// [Open online documentation to see images] + /// </summary> + /// <param name="position">One endpoint of the capsule. This is at the edge of the capsule, not at the center of one of the hemispheres.</param> + /// <param name="direction">The main axis of the capsule. Does not have to be normalized. If zero, nothing will be drawn.</param> + /// <param name="length">Distance between the two endpoints of the capsule. The length will be clamped to be at least 2*radius.</param> + /// <param name="radius">The radius of the capsule.</param> + public void WireCapsule (float3 position, float3 direction, float length, float radius) { + direction = math.normalizesafe(direction); + if (math.all(direction == 0) || math.any(math.isnan(direction)) || math.isnan(length) || math.isnan(radius)) return; + + if (radius <= 0) { + Line(position, position + direction * length); + } else { + length = math.max(length, radius*2); + OrthonormalBasis(direction, out var basis1, out var basis2); + + PushMatrix(new float4x4( + new float4(basis1, 0), + new float4(direction, 0), + new float4(basis2, 0), + new float4(position, 1) + )); + CircleXZInternal(new float3(0, radius, 0), radius); + PushMatrix(XZtoXYPlaneMatrix); + CircleXZInternal(new float3(0, 0, radius), radius, Mathf.PI, 2 * Mathf.PI); + PopMatrix(); + PushMatrix(XZtoYZPlaneMatrix); + CircleXZInternal(new float3(radius, 0, 0), radius, Mathf.PI*0.5f, Mathf.PI*1.5f); + PopMatrix(); + if (length > 0) { + var upperY = length - radius; + var lowerY = radius; + CircleXZInternal(new float3(0, upperY, 0), radius); + PushMatrix(XZtoXYPlaneMatrix); + CircleXZInternal(new float3(0, 0, upperY), radius, 0, Mathf.PI); + PopMatrix(); + PushMatrix(XZtoYZPlaneMatrix); + CircleXZInternal(new float3(upperY, 0, 0), radius, -Mathf.PI*0.5f, Mathf.PI*0.5f); + PopMatrix(); + Line(new float3(radius, lowerY, 0), new float3(radius, upperY, 0)); + Line(new float3(-radius, lowerY, 0), new float3(-radius, upperY, 0)); + Line(new float3(0, lowerY, radius), new float3(0, upperY, radius)); + Line(new float3(0, lowerY, -radius), new float3(0, upperY, -radius)); + } + PopMatrix(); + } + } + + /// <summary> + /// Draws a wire sphere. + /// + /// [Open online documentation to see images] + /// + /// <code> + /// // Draw a wire sphere at the origin with a radius of 0.5 + /// Draw.WireSphere(Vector3.zero, 0.5f, Color.black); + /// </code> + /// + /// See: <see cref="Circle"/> + /// </summary> + public void WireSphere (float3 position, float radius) { + SphereOutline(position, radius); + Circle(position, new float3(1, 0, 0), radius); + Circle(position, new float3(0, 1, 0), radius); + Circle(position, new float3(0, 0, 1), radius); + } + + /// <summary> + /// Draws lines through a sequence of points. + /// + /// [Open online documentation to see images] + /// <code> + /// // Draw a square + /// Draw.Polyline(new [] { new Vector3(0, 0, 0), new Vector3(1, 0, 0), new Vector3(1, 1, 0), new Vector3(0, 1, 0) }, true); + /// </code> + /// </summary> + /// <param name="points">Sequence of points to draw lines through</param> + /// <param name="cycle">If true a line will be drawn from the last point in the sequence back to the first point.</param> + [BurstDiscard] + public void Polyline (List<Vector3> points, bool cycle = false) { + for (int i = 0; i < points.Count - 1; i++) { + Line(points[i], points[i+1]); + } + if (cycle && points.Count > 1) Line(points[points.Count - 1], points[0]); + } + + /// <summary> + /// Draws lines through a sequence of points. + /// + /// [Open online documentation to see images] + /// <code> + /// // Draw a square + /// Draw.Polyline(new [] { new Vector3(0, 0, 0), new Vector3(1, 0, 0), new Vector3(1, 1, 0), new Vector3(0, 1, 0) }, true); + /// </code> + /// </summary> + /// <param name="points">Sequence of points to draw lines through</param> + /// <param name="cycle">If true a line will be drawn from the last point in the sequence back to the first point.</param> + public void Polyline<T>(T points, bool cycle = false) where T : IReadOnlyList<float3> { + for (int i = 0; i < points.Count - 1; i++) { + Line(points[i], points[i+1]); + } + if (cycle && points.Count > 1) Line(points[points.Count - 1], points[0]); + } + + /// <summary> + /// Draws lines through a sequence of points. + /// + /// [Open online documentation to see images] + /// <code> + /// // Draw a square + /// Draw.Polyline(new [] { new Vector3(0, 0, 0), new Vector3(1, 0, 0), new Vector3(1, 1, 0), new Vector3(0, 1, 0) }, true); + /// </code> + /// </summary> + /// <param name="points">Sequence of points to draw lines through</param> + /// <param name="cycle">If true a line will be drawn from the last point in the sequence back to the first point.</param> + [BurstDiscard] + public void Polyline (Vector3[] points, bool cycle = false) { + for (int i = 0; i < points.Length - 1; i++) { + Line(points[i], points[i+1]); + } + if (cycle && points.Length > 1) Line(points[points.Length - 1], points[0]); + } + + /// <summary> + /// Draws lines through a sequence of points. + /// + /// [Open online documentation to see images] + /// <code> + /// // Draw a square + /// Draw.Polyline(new [] { new Vector3(0, 0, 0), new Vector3(1, 0, 0), new Vector3(1, 1, 0), new Vector3(0, 1, 0) }, true); + /// </code> + /// </summary> + /// <param name="points">Sequence of points to draw lines through</param> + /// <param name="cycle">If true a line will be drawn from the last point in the sequence back to the first point.</param> + [BurstDiscard] + public void Polyline (float3[] points, bool cycle = false) { + for (int i = 0; i < points.Length - 1; i++) { + Line(points[i], points[i+1]); + } + if (cycle && points.Length > 1) Line(points[points.Length - 1], points[0]); + } + + /// <summary> + /// Draws lines through a sequence of points. + /// + /// [Open online documentation to see images] + /// <code> + /// // Draw a square + /// Draw.Polyline(new [] { new Vector3(0, 0, 0), new Vector3(1, 0, 0), new Vector3(1, 1, 0), new Vector3(0, 1, 0) }, true); + /// </code> + /// </summary> + /// <param name="points">Sequence of points to draw lines through</param> + /// <param name="cycle">If true a line will be drawn from the last point in the sequence back to the first point.</param> + public void Polyline (NativeArray<float3> points, bool cycle = false) { + for (int i = 0; i < points.Length - 1; i++) { + Line(points[i], points[i+1]); + } + if (cycle && points.Length > 1) Line(points[points.Length - 1], points[0]); + } + + /// <summary>Determines the symbol to use for <see cref="PolylineWithSymbol"/></summary> + public enum SymbolDecoration { + /// <summary> + /// No symbol. + /// + /// Space will still be reserved, but no symbol will be drawn. + /// Can be used to draw dashed lines. + /// + /// [Open online documentation to see images] + /// </summary> + None, + /// <summary> + /// An arrowhead symbol. + /// + /// [Open online documentation to see images] + /// </summary> + ArrowHead, + /// <summary> + /// A circle symbol. + /// + /// [Open online documentation to see images] + /// </summary> + Circle, + } + + /// <summary> + /// Draws a dashed line between two points. + /// + /// <code> + /// Draw.DashedPolyline(points, 0.1f, 0.1f, color); + /// </code> + /// + /// [Open online documentation to see images] + /// + /// Warning: An individual line segment is drawn for each dash. This means that performance may suffer if you make the dash + gap distance too small. + /// But for most use cases the performance is nothing to worry about. + /// + /// See: <see cref="DashedPolyline"/> + /// See: <see cref="PolylineWithSymbol"/> + /// </summary> + public void DashedLine (float3 a, float3 b, float dash, float gap) { + var p = new PolylineWithSymbol(SymbolDecoration.None, gap, 0, dash + gap); + p.MoveTo(ref this, a); + p.MoveTo(ref this, b); + } + + /// <summary> + /// Draws a dashed line through a sequence of points. + /// + /// <code> + /// Draw.DashedPolyline(points, 0.1f, 0.1f, color); + /// </code> + /// + /// [Open online documentation to see images] + /// + /// Warning: An individual line segment is drawn for each dash. This means that performance may suffer if you make the dash + gap distance too small. + /// But for most use cases the performance is nothing to worry about. + /// + /// If you have a different collection type, or you do not have the points in a collection at all, then you can use the <see cref="PolylineWithSymbol"/> struct directly. + /// + /// <code> + /// using (Draw.WithColor(color)) { + /// var dash = 0.1f; + /// var gap = 0.1f; + /// var p = new CommandBuilder.PolylineWithSymbol(CommandBuilder.SymbolDecoration.None, gap, 0, dash + gap); + /// for (int i = 0; i < points.Count; i++) { + /// p.MoveTo(ref Draw.editor, points[i]); + /// } + /// } + /// </code> + /// + /// See: <see cref="DashedLine"/> + /// See: <see cref="PolylineWithSymbol"/> + /// </summary> + public void DashedPolyline (List<Vector3> points, float dash, float gap) { + var p = new PolylineWithSymbol(SymbolDecoration.None, gap, 0, dash + gap); + for (int i = 0; i < points.Count; i++) { + p.MoveTo(ref this, points[i]); + } + } + + /// <summary> + /// Helper for drawing a polyline with symbols at regular intervals. + /// + /// <code> + /// var generator = new CommandBuilder.PolylineWithSymbol(CommandBuilder.SymbolDecoration.Circle, 0.2f, 0.0f, 0.47f); + /// generator.MoveTo(ref Draw.editor, new float3(-0.5f, 0, -0.5f)); + /// generator.MoveTo(ref Draw.editor, new float3(0.5f, 0, 0.5f)); + /// </code> + /// + /// [Open online documentation to see images] + /// + /// [Open online documentation to see images] + /// + /// You can also draw a dashed line using this struct, but for common cases you can use the <see cref="DashedPolyline"/> helper function instead. + /// + /// <code> + /// using (Draw.WithColor(color)) { + /// var dash = 0.1f; + /// var gap = 0.1f; + /// var p = new CommandBuilder.PolylineWithSymbol(CommandBuilder.SymbolDecoration.None, gap, 0, dash + gap); + /// for (int i = 0; i < points.Count; i++) { + /// p.MoveTo(ref Draw.editor, points[i]); + /// } + /// } + /// </code> + /// + /// [Open online documentation to see images] + /// </summary> + public struct PolylineWithSymbol { + float3 prev; + float offset; + readonly float symbolSize; + readonly float symbolSpacing; + readonly float symbolPadding; + readonly float symbolOffset; + readonly SymbolDecoration symbol; + readonly bool reverseSymbols; + bool odd; + + /// <summary>Create a new polyline with symbol generator.</summary> + /// <param name="symbol">The symbol to use</param> + /// <param name="symbolSize">The size of the symbol. In case of a circle, this is the diameter.</param> + /// <param name="symbolPadding">The padding on both sides of the symbol between the symbol and the line.</param> + /// <param name="symbolSpacing">The spacing between symbols. This is the distance between the centers of the symbols.</param> + /// <param name="reverseSymbols">If true, the symbols will be reversed. For cicles this has no effect, but arrowhead symbols will be reversed.</param> + public PolylineWithSymbol(SymbolDecoration symbol, float symbolSize, float symbolPadding, float symbolSpacing, bool reverseSymbols = false) { + if (symbolSpacing <= math.FLT_MIN_NORMAL) throw new System.ArgumentOutOfRangeException(nameof(symbolSpacing), "Symbol spacing must be greater than zero"); + if (symbolSize <= math.FLT_MIN_NORMAL) throw new System.ArgumentOutOfRangeException(nameof(symbolSize), "Symbol size must be greater than zero"); + if (symbolPadding < 0) throw new System.ArgumentOutOfRangeException(nameof(symbolPadding), "Symbol padding must non-negative"); + + this.prev = float3.zero; + this.symbol = symbol; + this.symbolSize = symbolSize; + this.symbolPadding = symbolPadding; + this.symbolSpacing = math.max(0, symbolSpacing - symbolPadding * 2f - symbolSize); + this.reverseSymbols = reverseSymbols; + symbolOffset = symbol == SymbolDecoration.ArrowHead ? -0.25f * symbolSize : 0; + if (reverseSymbols) { + symbolOffset = -symbolOffset; + } + symbolOffset += 0.5f * symbolSize; + offset = -1; + odd = false; + } + + /// <summary> + /// Move to a new point. + /// + /// This will draw the symbols and line segments between the previous point and the new point. + /// </summary> + /// <param name="draw">The command builder to draw to. You can use a built-in builder like \reflink{Draw.editor} or \reflink{Draw.ingame}, or use a custom one.</param> + /// <param name="next">The next point in the polyline to move to.</param> + public void MoveTo (ref CommandBuilder draw, float3 next) { + if (offset == -1) { + offset = this.symbolSpacing * 0.5f; + prev = next; + return; + } + var len = math.length(next - prev); + var invLen = math.rcp(len); + var dir = next - prev; + float3 up = default; + if (symbol != SymbolDecoration.None) { + up = math.normalizesafe(math.cross(dir, math.cross(dir, new float3(0, 1, 0)))); + if (math.all(up == 0f)) { + up = new float3(0, 0, 1); + } + } + if (reverseSymbols) dir = -dir; + if (offset > 0 && !odd) { + draw.Line(prev, math.lerp(prev, next, math.min(offset * invLen, 1))); + } + while (offset < len) { + if (odd) { + var pLast = math.lerp(prev, next, offset * invLen); + offset += symbolSpacing; + var p = math.lerp(prev, next, math.min(offset * invLen, 1)); + draw.Line(pLast, p); + offset += symbolPadding; + } else { + var p = math.lerp(prev, next, (offset + symbolOffset) * invLen); + switch (symbol) { + case SymbolDecoration.None: + break; + case SymbolDecoration.ArrowHead: + draw.Arrowhead(p, dir, up, symbolSize); + break; + case SymbolDecoration.Circle: + default: + draw.Circle(p, up, symbolSize * 0.5f); + break; + } + offset += symbolSize + symbolPadding; + } + odd = !odd; + } + offset -= len; + prev = next; + } + } + + /// <summary> + /// Draws the outline of a box which is axis-aligned. + /// + /// [Open online documentation to see images] + /// </summary> + /// <param name="center">Center of the box</param> + /// <param name="size">Width of the box along all dimensions</param> + public void WireBox (float3 center, float3 size) { + Reserve<BoxData>(); + Add(Command.WireBox); + Add(new BoxData { center = center, size = size }); + } + + /// <summary> + /// Draws the outline of a box. + /// + /// [Open online documentation to see images] + /// </summary> + /// <param name="center">Center of the box</param> + /// <param name="rotation">Rotation of the box</param> + /// <param name="size">Width of the box along all dimensions</param> + public void WireBox (float3 center, quaternion rotation, float3 size) { + PushMatrix(float4x4.TRS(center, rotation, size)); + WireBox(float3.zero, new float3(1, 1, 1)); + PopMatrix(); + } + + /// <summary> + /// Draws the outline of a box. + /// + /// [Open online documentation to see images] + /// </summary> + public void WireBox (Bounds bounds) { + WireBox(bounds.center, bounds.size); + } + + /// <summary> + /// Draws a wire mesh. + /// Every single edge of the mesh will be drawn using a <see cref="Line"/> command. + /// + /// <code> + /// var go = GameObject.CreatePrimitive(PrimitiveType.Sphere); + /// go.transform.position = new Vector3(0, 0, 0); + /// using (Draw.InLocalSpace(go.transform)) { + /// Draw.WireMesh(go.GetComponent<MeshFilter>().sharedMesh, color); + /// } + /// </code> + /// [Open online documentation to see images] + /// + /// See: <see cref="SolidMesh(Mesh)"/> + /// + /// Version: Supported in Unity 2020.1 or later. + /// </summary> + public void WireMesh (Mesh mesh) { +#if UNITY_2020_1_OR_NEWER + if (mesh == null) throw new System.ArgumentNullException(); + + // Use a burst compiled function to draw the lines + // This is significantly faster than pure C# (about 5x). + var meshDataArray = Mesh.AcquireReadOnlyMeshData(mesh); + var meshData = meshDataArray[0]; + + JobWireMesh.JobWireMeshFunctionPointer(ref meshData, ref this); + meshDataArray.Dispose(); +#else + Debug.LogError("The WireMesh method is only suppored in Unity 2020.1 or later"); +#endif + } + + /// <summary> + /// Draws a wire mesh. + /// Every single edge of the mesh will be drawn using a <see cref="Line"/> command. + /// + /// <code> + /// var go = GameObject.CreatePrimitive(PrimitiveType.Sphere); + /// go.transform.position = new Vector3(0, 0, 0); + /// using (Draw.InLocalSpace(go.transform)) { + /// Draw.WireMesh(go.GetComponent<MeshFilter>().sharedMesh, color); + /// } + /// </code> + /// [Open online documentation to see images] + /// + /// See: <see cref="SolidMesh(Mesh)"/> + /// + /// Version: Supported in Unity 2020.1 or later. + /// </summary> + public void WireMesh (NativeArray<float3> vertices, NativeArray<int> triangles) { +#if UNITY_2020_1_OR_NEWER + unsafe { + JobWireMesh.WireMesh((float3*)vertices.GetUnsafeReadOnlyPtr(), (int*)triangles.GetUnsafeReadOnlyPtr(), vertices.Length, triangles.Length, ref this); + } +#else + Debug.LogError("The WireMesh method is only suppored in Unity 2020.1 or later"); +#endif + } + +#if UNITY_2020_1_OR_NEWER + /// <summary>Helper job for <see cref="WireMesh"/></summary> + [BurstCompile] + class JobWireMesh { + public delegate void JobWireMeshDelegate(ref Mesh.MeshData rawMeshData, ref CommandBuilder draw); + + public static readonly JobWireMeshDelegate JobWireMeshFunctionPointer = BurstCompiler.CompileFunctionPointer<JobWireMeshDelegate>(Execute).Invoke; + + [BurstCompile] + public static unsafe void WireMesh (float3* verts, int* indices, int vertexCount, int indexCount, ref CommandBuilder draw) { + // Ignore warning about NativeHashMap being obsolete in early versions of the collections package. + // It works just fine, and in later versions the NativeHashMap is not obsolete. + #pragma warning disable 618 + var seenEdges = new NativeHashMap<int2, bool>(indexCount, Allocator.Temp); + #pragma warning restore 618 + for (int i = 0; i < indexCount; i += 3) { + var a = indices[i]; + var b = indices[i+1]; + var c = indices[i+2]; + if (a < 0 || b < 0 || c < 0 || a >= vertexCount || b >= vertexCount || c >= vertexCount) { + throw new Exception("Invalid vertex index. Index out of bounds"); + } + int v1, v2; + + // Draw each edge of the triangle. + // Check so that we do not draw an edge twice. + v1 = math.min(a, b); + v2 = math.max(a, b); + if (!seenEdges.ContainsKey(new int2(v1, v2))) { + seenEdges.Add(new int2(v1, v2), true); + draw.Line(verts[v1], verts[v2]); + } + + v1 = math.min(b, c); + v2 = math.max(b, c); + if (!seenEdges.ContainsKey(new int2(v1, v2))) { + seenEdges.Add(new int2(v1, v2), true); + draw.Line(verts[v1], verts[v2]); + } + + v1 = math.min(c, a); + v2 = math.max(c, a); + if (!seenEdges.ContainsKey(new int2(v1, v2))) { + seenEdges.Add(new int2(v1, v2), true); + draw.Line(verts[v1], verts[v2]); + } + } + } + + [BurstCompile] + [AOT.MonoPInvokeCallback(typeof(JobWireMeshDelegate))] + static void Execute (ref Mesh.MeshData rawMeshData, ref CommandBuilder draw) { + int maxIndices = 0; + for (int subMeshIndex = 0; subMeshIndex < rawMeshData.subMeshCount; subMeshIndex++) { + maxIndices = math.max(maxIndices, rawMeshData.GetSubMesh(subMeshIndex).indexCount); + } + var tris = new NativeArray<int>(maxIndices, Allocator.Temp, NativeArrayOptions.UninitializedMemory); + var verts = new NativeArray<Vector3>(rawMeshData.vertexCount, Allocator.Temp, NativeArrayOptions.UninitializedMemory); + rawMeshData.GetVertices(verts); + + for (int subMeshIndex = 0; subMeshIndex < rawMeshData.subMeshCount; subMeshIndex++) { + var submesh = rawMeshData.GetSubMesh(subMeshIndex); + rawMeshData.GetIndices(tris, subMeshIndex); + unsafe { + WireMesh((float3*)verts.GetUnsafeReadOnlyPtr(), (int*)tris.GetUnsafeReadOnlyPtr(), verts.Length, submesh.indexCount, ref draw); + } + } + } + } +#endif + + /// <summary> + /// Draws a solid mesh. + /// The mesh will be drawn with a solid color. + /// + /// <code> + /// var go = GameObject.CreatePrimitive(PrimitiveType.Sphere); + /// go.transform.position = new Vector3(0, 0, 0); + /// using (Draw.InLocalSpace(go.transform)) { + /// Draw.SolidMesh(go.GetComponent<MeshFilter>().sharedMesh, color); + /// } + /// </code> + /// [Open online documentation to see images] + /// + /// Note: This method is not thread safe and must not be used from the Unity Job System. + /// TODO: Are matrices handled? + /// + /// See: <see cref="WireMesh(Mesh)"/> + /// </summary> + public void SolidMesh (Mesh mesh) { + SolidMeshInternal(mesh, false); + } + + void SolidMeshInternal (Mesh mesh, bool temporary, Color color) { + PushColor(color); + SolidMeshInternal(mesh, temporary); + PopColor(); + } + + + void SolidMeshInternal (Mesh mesh, bool temporary) { + var g = gizmos.Target as DrawingData; + + g.data.Get(uniqueID).meshes.Add(new SubmittedMesh { + mesh = mesh, + temporary = temporary, + }); + // Internally we need to make sure to capture the current state + // (which includes the current matrix and color) so that it + // can be applied to the mesh. + Reserve(4); + Add(Command.CaptureState); + } + + /// <summary> + /// Draws a solid mesh with the given vertices. + /// + /// [Open online documentation to see images] + /// + /// Note: This method is not thread safe and must not be used from the Unity Job System. + /// TODO: Are matrices handled? + /// </summary> + [BurstDiscard] + public void SolidMesh (List<Vector3> vertices, List<int> triangles, List<Color> colors) { + if (vertices.Count != colors.Count) throw new System.ArgumentException("Number of colors must be the same as the number of vertices"); + + // TODO: Is this mesh getting recycled at all? + var g = gizmos.Target as DrawingData; + var mesh = g.GetMesh(vertices.Count); + + // Set all data on the mesh + mesh.Clear(); + mesh.SetVertices(vertices); + mesh.SetTriangles(triangles, 0); + mesh.SetColors(colors); + // Upload all data + mesh.UploadMeshData(false); + SolidMeshInternal(mesh, true); + } + + /// <summary> + /// Draws a solid mesh with the given vertices. + /// + /// [Open online documentation to see images] + /// + /// Note: This method is not thread safe and must not be used from the Unity Job System. + /// TODO: Are matrices handled? + /// </summary> + [BurstDiscard] + public void SolidMesh (Vector3[] vertices, int[] triangles, Color[] colors, int vertexCount, int indexCount) { + if (vertices.Length != colors.Length) throw new System.ArgumentException("Number of colors must be the same as the number of vertices"); + + // TODO: Is this mesh getting recycled at all? + var g = gizmos.Target as DrawingData; + var mesh = g.GetMesh(vertices.Length); + + // Set all data on the mesh + mesh.Clear(); + mesh.SetVertices(vertices, 0, vertexCount); + mesh.SetTriangles(triangles, 0, indexCount, 0); + mesh.SetColors(colors, 0, vertexCount); + // Upload all data + mesh.UploadMeshData(false); + SolidMeshInternal(mesh, true); + } + + /// <summary> + /// Draws a 3D cross. + /// + /// [Open online documentation to see images] + /// </summary> + public void Cross (float3 position, float size = 1) { + size *= 0.5f; + Line(position - new float3(size, 0, 0), position + new float3(size, 0, 0)); + Line(position - new float3(0, size, 0), position + new float3(0, size, 0)); + Line(position - new float3(0, 0, size), position + new float3(0, 0, size)); + } + + /// <summary> + /// Draws a cross in the XZ plane. + /// + /// [Open online documentation to see images] + /// </summary> + [System.Obsolete("Use Draw.xz.Cross instead")] + public void CrossXZ (float3 position, float size = 1) { + size *= 0.5f; + Line(position - new float3(size, 0, 0), position + new float3(size, 0, 0)); + Line(position - new float3(0, 0, size), position + new float3(0, 0, size)); + } + + /// <summary> + /// Draws a cross in the XY plane. + /// + /// [Open online documentation to see images] + /// </summary> + [System.Obsolete("Use Draw.xy.Cross instead")] + public void CrossXY (float3 position, float size = 1) { + size *= 0.5f; + Line(position - new float3(size, 0, 0), position + new float3(size, 0, 0)); + Line(position - new float3(0, size, 0), position + new float3(0, size, 0)); + } + + /// <summary>Returns a point on a cubic bezier curve. t is clamped between 0 and 1</summary> + public static float3 EvaluateCubicBezier (float3 p0, float3 p1, float3 p2, float3 p3, float t) { + t = math.clamp(t, 0, 1); + float tr = 1-t; + return tr*tr*tr * p0 + 3 * tr*tr * t * p1 + 3 * tr * t*t * p2 + t*t*t * p3; + } + + /// <summary> + /// Draws a cubic bezier curve. + /// + /// [Open online documentation to see images] + /// + /// [Open online documentation to see images] + /// + /// TODO: Currently uses a fixed resolution of 20 segments. Resolution should depend on the distance to the camera. + /// + /// See: https://en.wikipedia.org/wiki/Bezier_curve + /// </summary> + /// <param name="p0">Start point</param> + /// <param name="p1">First control point</param> + /// <param name="p2">Second control point</param> + /// <param name="p3">End point</param> + public void Bezier (float3 p0, float3 p1, float3 p2, float3 p3) { + float3 prev = p0; + + for (int i = 1; i <= 20; i++) { + float t = i/20.0f; + float3 p = EvaluateCubicBezier(p0, p1, p2, p3, t); + Line(prev, p); + prev = p; + } + } + + /// <summary> + /// Draws a smooth curve through a list of points. + /// + /// A catmull-rom spline is equivalent to a bezier curve with control points determined by an algorithm. + /// In fact, this package displays catmull-rom splines by first converting them to bezier curves. + /// + /// [Open online documentation to see images] + /// + /// See: https://en.wikipedia.org/wiki/Centripetal_Catmull%E2%80%93Rom_spline + /// See: <see cref="CatmullRom(float3,float3,float3,float3)"/> + /// </summary> + /// <param name="points">The curve will smoothly pass through each point in the list in order.</param> + public void CatmullRom (List<Vector3> points) { + if (points.Count < 2) return; + + if (points.Count == 2) { + Line(points[0], points[1]); + } else { + // count >= 3 + var count = points.Count; + // Draw first curve, this is special because the first two control points are the same + CatmullRom(points[0], points[0], points[1], points[2]); + for (int i = 0; i + 3 < count; i++) { + CatmullRom(points[i], points[i+1], points[i+2], points[i+3]); + } + // Draw last curve + CatmullRom(points[count-3], points[count-2], points[count-1], points[count-1]); + } + } + + /// <summary> + /// Draws a centripetal catmull rom spline. + /// + /// The curve starts at p1 and ends at p2. + /// + /// [Open online documentation to see images] + /// [Open online documentation to see images] + /// + /// See: <see cref="CatmullRom(List<Vector3>)"/> + /// </summary> + /// <param name="p0">First control point</param> + /// <param name="p1">Second control point. Start of the curve.</param> + /// <param name="p2">Third control point. End of the curve.</param> + /// <param name="p3">Fourth control point.</param> + public void CatmullRom (float3 p0, float3 p1, float3 p2, float3 p3) { + // References used: + // p.266 GemsV1 + // + // tension is often set to 0.5 but you can use any reasonable value: + // http://www.cs.cmu.edu/~462/projects/assn2/assn2/catmullRom.pdf + // + // bias and tension controls: + // http://local.wasp.uwa.edu.au/~pbourke/miscellaneous/interpolation/ + + // We will convert the catmull rom spline to a bezier curve for simplicity. + // The end result of this will be a conversion matrix where we transform catmull rom control points + // into the equivalent bezier curve control points. + + // Conversion matrix + // ================= + + // A centripetal catmull rom spline can be separated into the following terms: + // 1 * p1 + + // t * (-0.5 * p0 + 0.5*p2) + + // t*t * (p0 - 2.5*p1 + 2.0*p2 + 0.5*t2) + + // t*t*t * (-0.5*p0 + 1.5*p1 - 1.5*p2 + 0.5*p3) + // + // Matrix form: + // 1 t t^2 t^3 + // {0, -1/2, 1, -1/2} + // {1, 0, -5/2, 3/2} + // {0, 1/2, 2, -3/2} + // {0, 0, -1/2, 1/2} + + // Transposed matrix: + // M_1 = {{0, 1, 0, 0}, {-1/2, 0, 1/2, 0}, {1, -5/2, 2, -1/2}, {-1/2, 3/2, -3/2, 1/2}} + + // A bezier spline can be separated into the following terms: + // (-t^3 + 3 t^2 - 3 t + 1) * c0 + + // (3t^3 - 6*t^2 + 3t) * c1 + + // (3t^2 - 3t^3) * c2 + + // t^3 * c3 + // + // Matrix form: + // 1 t t^2 t^3 + // {1, -3, 3, -1} + // {0, 3, -6, 3} + // {0, 0, 3, -3} + // {0, 0, 0, 1} + + // Transposed matrix: + // M_2 = {{1, 0, 0, 0}, {-3, 3, 0, 0}, {3, -6, 3, 0}, {-1, 3, -3, 1}} + + // Thus a bezier curve can be evaluated using the expression + // output1 = T * M_1 * c + // where T = [1, t, t^2, t^3] and c being the control points c = [c0, c1, c2, c3]^T + // + // and a catmull rom spline can be evaluated using + // + // output2 = T * M_2 * p + // where T = same as before and p = [p0, p1, p2, p3]^T + // + // We can solve for c in output1 = output2 + // T * M_1 * c = T * M_2 * p + // M_1 * c = M_2 * p + // c = M_1^(-1) * M_2 * p + // Thus a conversion matrix from p to c is M_1^(-1) * M_2 + // This can be calculated and the result is the following matrix: + // + // {0, 1, 0, 0} + // {-1/6, 1, 1/6, 0} + // {0, 1/6, 1, -1/6} + // {0, 0, 1, 0} + // ------------------------------------------------------------------ + // + // Using this we can calculate c = M_1^(-1) * M_2 * p + var c0 = p1; + var c1 = (-p0 + 6*p1 + 1*p2)*(1/6.0f); + var c2 = (p1 + 6*p2 - p3)*(1/6.0f); + var c3 = p2; + + // And finally draw the bezier curve which is equivalent to the desired catmull-rom spline + Bezier(c0, c1, c2, c3); + } + + /// <summary> + /// Draws an arrow between two points. + /// + /// The size of the head defaults to 20% of the length of the arrow. + /// + /// [Open online documentation to see images] + /// + /// See: <see cref="ArrowheadArc"/> + /// See: <see cref="Arrow(float3,float3,float3,float)"/> + /// See: <see cref="ArrowRelativeSizeHead"/> + /// </summary> + /// <param name="from">Base of the arrow.</param> + /// <param name="to">Head of the arrow.</param> + public void Arrow (float3 from, float3 to) { + ArrowRelativeSizeHead(from, to, DEFAULT_UP, 0.2f); + } + + /// <summary> + /// Draws an arrow between two points. + /// + /// [Open online documentation to see images] + /// + /// See: <see cref="ArrowRelativeSizeHead"/> + /// See: <see cref="ArrowheadArc"/> + /// </summary> + /// <param name="from">Base of the arrow.</param> + /// <param name="to">Head of the arrow.</param> + /// <param name="up">Up direction of the world, the arrowhead plane will be as perpendicular as possible to this direction. Defaults to Vector3.up.</param> + /// <param name="headSize">The size of the arrowhead in world units.</param> + public void Arrow (float3 from, float3 to, float3 up, float headSize) { + var length_sq = math.lengthsq(to - from); + + if (length_sq > 0.000001f) { + ArrowRelativeSizeHead(from, to, up, headSize * math.rsqrt(length_sq)); + } + } + + /// <summary> + /// Draws an arrow between two points with a head that varies with the length of the arrow. + /// + /// [Open online documentation to see images] + /// + /// See: <see cref="ArrowheadArc"/> + /// See: <see cref="Arrow"/> + /// </summary> + /// <param name="from">Base of the arrow.</param> + /// <param name="to">Head of the arrow.</param> + /// <param name="up">Up direction of the world, the arrowhead plane will be as perpendicular as possible to this direction.</param> + /// <param name="headFraction">The length of the arrowhead is the distance between from and to multiplied by this fraction. Should be between 0 and 1.</param> + public void ArrowRelativeSizeHead (float3 from, float3 to, float3 up, float headFraction) { + Line(from, to); + var dir = to - from; + + var normal = math.cross(dir, up); + // Pick a different up direction if the direction happened to be colinear with that one. + if (math.all(normal == 0)) normal = math.cross(new float3(1, 0, 0), dir); + // Pick a different up direction if up=(1,0,0) and thus the above check would have generated a zero vector again + if (math.all(normal == 0)) normal = math.cross(new float3(0, 1, 0), dir); + normal = math.normalizesafe(normal) * math.length(dir); + + Line(to, to - (dir + normal) * headFraction); + Line(to, to - (dir - normal) * headFraction); + } + + /// <summary> + /// Draws an arrowhead at a point. + /// + /// <code> + /// Draw.Arrowhead(Vector3.zero, Vector3.forward, 0.75f, color); + /// </code> + /// [Open online documentation to see images] + /// + /// See: <see cref="Arrow"/> + /// See: <see cref="ArrowRelativeSizeHead"/> + /// </summary> + /// <param name="center">Center of the arrowhead.</param> + /// <param name="direction">Direction the arrow is pointing.</param> + /// <param name="radius">Distance from the center to each corner of the arrowhead.</param> + public void Arrowhead (float3 center, float3 direction, float radius) { + Arrowhead(center, direction, DEFAULT_UP, radius); + } + + /// <summary> + /// Draws an arrowhead at a point. + /// + /// <code> + /// Draw.Arrowhead(Vector3.zero, Vector3.forward, 0.75f, color); + /// </code> + /// [Open online documentation to see images] + /// + /// See: <see cref="Arrow"/> + /// See: <see cref="ArrowRelativeSizeHead"/> + /// </summary> + /// <param name="center">Center of the arrowhead.</param> + /// <param name="direction">Direction the arrow is pointing.</param> + /// <param name="up">Up direction of the world, the arrowhead plane will be as perpendicular as possible to this direction. Defaults to Vector3.up. Must be normalized.</param> + /// <param name="radius">Distance from the center to each corner of the arrowhead.</param> + public void Arrowhead (float3 center, float3 direction, float3 up, float radius) { + if (math.all(direction == 0)) return; + direction = math.normalizesafe(direction); + var normal = math.cross(direction, up); + const float SinPiOver3 = 0.866025f; + const float CosPiOver3 = 0.5f; + var circleCenter = center - radius * (1 - CosPiOver3)*0.5f * direction; + var p1 = circleCenter + radius * direction; + var p2 = circleCenter - radius * CosPiOver3 * direction + radius * SinPiOver3 * normal; + var p3 = circleCenter - radius * CosPiOver3 * direction - radius * SinPiOver3 * normal; + Line(p1, p2); + Line(p2, circleCenter); + Line(circleCenter, p3); + Line(p3, p1); + } + + /// <summary> + /// Draws an arrowhead centered around a circle. + /// + /// This can be used to for example show the direction a character is moving in. + /// + /// [Open online documentation to see images] + /// + /// Note: In the image above the arrowhead is the only part that is drawn by this method. The cylinder is only included for context. + /// + /// See: <see cref="Arrow"/> + /// </summary> + /// <param name="origin">Point around which the arc is centered</param> + /// <param name="direction">Direction the arrow is pointing</param> + /// <param name="offset">Distance from origin that the arrow starts.</param> + /// <param name="width">Width of the arrowhead in degrees (defaults to 60). Should be between 0 and 90.</param> + public void ArrowheadArc (float3 origin, float3 direction, float offset, float width = 60) { + if (!math.any(direction)) return; + if (offset < 0) throw new System.ArgumentOutOfRangeException(nameof(offset)); + if (offset == 0) return; + + var rot = Quaternion.LookRotation(direction, DEFAULT_UP); + PushMatrix(Matrix4x4.TRS(origin, rot, Vector3.one)); + var a1 = math.PI * 0.5f - width * (0.5f * Mathf.Deg2Rad); + var a2 = math.PI * 0.5f + width * (0.5f * Mathf.Deg2Rad); + CircleXZInternal(float3.zero, offset, a1, a2); + var p1 = new float3(math.cos(a1), 0, math.sin(a1)) * offset; + var p2 = new float3(math.cos(a2), 0, math.sin(a2)) * offset; + const float sqrt2 = 1.4142f; + var p3 = new float3(0, 0, sqrt2 * offset); + Line(p1, p3); + Line(p3, p2); + PopMatrix(); + } + + /// <summary> + /// Draws a grid of lines. + /// + /// <code> + /// Draw.xz.WireGrid(Vector3.zero, new int2(3, 3), new float2(1, 1), color); + /// </code> + /// [Open online documentation to see images] + /// </summary> + /// <param name="center">Center of the grid</param> + /// <param name="rotation">Rotation of the grid. The grid will be aligned to the X and Z axes of the rotation.</param> + /// <param name="cells">Number of cells of the grid. Should be greater than 0.</param> + /// <param name="totalSize">Total size of the grid along the X and Z axes.</param> + public void WireGrid (float3 center, quaternion rotation, int2 cells, float2 totalSize) { + cells = math.max(cells, new int2(1, 1)); + PushMatrix(float4x4.TRS(center, rotation, new Vector3(totalSize.x, 0, totalSize.y))); + int w = cells.x; + int h = cells.y; + for (int i = 0; i <= w; i++) Line(new float3(i/(float)w - 0.5f, 0, -0.5f), new float3(i/(float)w - 0.5f, 0, 0.5f)); + for (int i = 0; i <= h; i++) Line(new float3(-0.5f, 0, i/(float)h - 0.5f), new float3(0.5f, 0, i/(float)h - 0.5f)); + PopMatrix(); + } + + /// <summary> + /// Draws a triangle outline. + /// + /// <code> + /// Draw.WireTriangle(new Vector3(-0.5f, 0, 0), new Vector3(0, 1, 0), new Vector3(0.5f, 0, 0), Color.black); + /// </code> + /// [Open online documentation to see images] + /// + /// See: <see cref="Draw.WirePlane(float3,quaternion,float2)"/> + /// See: <see cref="WirePolygon"/> + /// See: <see cref="SolidTriangle"/> + /// </summary> + /// <param name="a">First corner of the triangle</param> + /// <param name="b">Second corner of the triangle</param> + /// <param name="c">Third corner of the triangle</param> + public void WireTriangle (float3 a, float3 b, float3 c) { + Line(a, b); + Line(b, c); + Line(c, a); + } + + /// <summary> + /// Draws a rectangle outline. + /// The rectangle will be aligned to the X and Z axes. + /// + /// <code> + /// Draw.xz.WireRectangle(new Vector3(0f, 0, 0), new Vector2(1, 1), Color.black); + /// </code> + /// [Open online documentation to see images] + /// + /// See: <see cref="WirePolygon"/> + /// </summary> + [System.Obsolete("Use Draw.xz.WireRectangle instead")] + public void WireRectangleXZ (float3 center, float2 size) { + WireRectangle(center, quaternion.identity, size); + } + + /// <summary> + /// Draws a rectangle outline. + /// The rectangle will be oriented along the rotation's X and Z axes. + /// + /// <code> + /// Draw.WireRectangle(new Vector3(0f, 0, 0), Quaternion.identity, new Vector2(1, 1), Color.black); + /// </code> + /// [Open online documentation to see images] + /// + /// This is identical to <see cref="Draw.WirePlane(float3,quaternion,float2)"/>, but this name is added for consistency. + /// + /// See: <see cref="WirePolygon"/> + /// </summary> + public void WireRectangle (float3 center, quaternion rotation, float2 size) { + WirePlane(center, rotation, size); + } + + /// <summary> + /// Draws a rectangle outline. + /// The rectangle corners are assumed to be in XY space. + /// This is particularly useful when combined with <see cref="InScreenSpace"/>. + /// + /// <code> + /// using (Draw.InScreenSpace(Camera.main)) { + /// Draw.xy.WireRectangle(new Rect(10, 10, 100, 100), Color.black); + /// } + /// </code> + /// [Open online documentation to see images] + /// + /// See: <see cref="WireRectangleXZ"/> + /// See: <see cref="WireRectangle(float3,quaternion,float2)"/> + /// See: <see cref="WirePolygon"/> + /// </summary> + [System.Obsolete("Use Draw.xy.WireRectangle instead")] + public void WireRectangle (Rect rect) { + xy.WireRectangle(rect); + } + + + /// <summary> + /// Draws a triangle outline. + /// + /// <code> + /// Draw.WireTriangle(Vector3.zero, Quaternion.identity, 0.5f, color); + /// </code> + /// [Open online documentation to see images] + /// + /// Note: This is a convenience wrapper for <see cref="WirePolygon(float3,int,quaternion,float)"/> + /// + /// See: <see cref="WireTriangle(float3,float3,float3)"/> + /// </summary> + /// <param name="center">Center of the triangle.</param> + /// <param name="rotation">Rotation of the triangle. The first vertex will be radius units in front of center as seen from the rotation's point of view.</param> + /// <param name="radius">Distance from the center to each vertex.</param> + public void WireTriangle (float3 center, quaternion rotation, float radius) { + WirePolygon(center, 3, rotation, radius); + } + + /// <summary> + /// Draws a pentagon outline. + /// + /// <code> + /// Draw.WirePentagon(Vector3.zero, Quaternion.identity, 0.5f, color); + /// </code> + /// [Open online documentation to see images] + /// + /// Note: This is a convenience wrapper for <see cref="WirePolygon(float3,int,quaternion,float)"/> + /// </summary> + /// <param name="center">Center of the polygon.</param> + /// <param name="rotation">Rotation of the polygon. The first vertex will be radius units in front of center as seen from the rotation's point of view.</param> + /// <param name="radius">Distance from the center to each vertex.</param> + public void WirePentagon (float3 center, quaternion rotation, float radius) { + WirePolygon(center, 5, rotation, radius); + } + + /// <summary> + /// Draws a hexagon outline. + /// + /// <code> + /// Draw.WireHexagon(Vector3.zero, Quaternion.identity, 0.5f, color); + /// </code> + /// [Open online documentation to see images] + /// + /// Note: This is a convenience wrapper for <see cref="WirePolygon(float3,int,quaternion,float)"/> + /// </summary> + /// <param name="center">Center of the polygon.</param> + /// <param name="rotation">Rotation of the polygon. The first vertex will be radius units in front of center as seen from the rotation's point of view.</param> + /// <param name="radius">Distance from the center to each vertex.</param> + public void WireHexagon (float3 center, quaternion rotation, float radius) { + WirePolygon(center, 6, rotation, radius); + } + + /// <summary> + /// Draws a regular polygon outline. + /// + /// <code> + /// Draw.WirePolygon(new Vector3(-0.5f, 0, +0.5f), 3, Quaternion.identity, 0.4f, color); + /// Draw.WirePolygon(new Vector3(+0.5f, 0, +0.5f), 4, Quaternion.identity, 0.4f, color); + /// Draw.WirePolygon(new Vector3(-0.5f, 0, -0.5f), 5, Quaternion.identity, 0.4f, color); + /// Draw.WirePolygon(new Vector3(+0.5f, 0, -0.5f), 6, Quaternion.identity, 0.4f, color); + /// </code> + /// [Open online documentation to see images] + /// + /// See: <see cref="WireTriangle"/> + /// See: <see cref="WirePentagon"/> + /// See: <see cref="WireHexagon"/> + /// </summary> + /// <param name="center">Center of the polygon.</param> + /// <param name="vertices">Number of corners (and sides) of the polygon.</param> + /// <param name="rotation">Rotation of the polygon. The first vertex will be radius units in front of center as seen from the rotation's point of view.</param> + /// <param name="radius">Distance from the center to each vertex.</param> + public void WirePolygon (float3 center, int vertices, quaternion rotation, float radius) { + PushMatrix(float4x4.TRS(center, rotation, new float3(radius, radius, radius))); + float3 prev = new float3(0, 0, 1); + for (int i = 1; i <= vertices; i++) { + float a = 2 * math.PI * (i / (float)vertices); + var p = new float3(math.sin(a), 0, math.cos(a)); + Line(prev, p); + prev = p; + } + PopMatrix(); + } + + /// <summary> + /// Draws a solid rectangle. + /// The rectangle corners are assumed to be in XY space. + /// This is particularly useful when combined with <see cref="InScreenSpace"/>. + /// + /// Behind the scenes this is implemented using <see cref="SolidPlane"/>. + /// + /// <code> + /// using (Draw.InScreenSpace(Camera.main)) { + /// Draw.xy.SolidRectangle(new Rect(10, 10, 100, 100), Color.black); + /// } + /// </code> + /// [Open online documentation to see images] + /// + /// See: <see cref="WireRectangleXZ"/> + /// See: <see cref="WireRectangle(float3,quaternion,float2)"/> + /// See: <see cref="SolidBox"/> + /// </summary> + [System.Obsolete("Use Draw.xy.SolidRectangle instead")] + public void SolidRectangle (Rect rect) { + xy.SolidRectangle(rect); + } + + /// <summary> + /// Draws a solid plane. + /// + /// <code> + /// Draw.SolidPlane(new float3(0, 0, 0), new float3(0, 1, 0), 1.0f, color); + /// </code> + /// [Open online documentation to see images] + /// </summary> + /// <param name="center">Center of the visualized plane.</param> + /// <param name="normal">Direction perpendicular to the plane. If this is (0,0,0) then nothing will be rendered.</param> + /// <param name="size">Width and height of the visualized plane.</param> + public void SolidPlane (float3 center, float3 normal, float2 size) { + if (math.any(normal)) { + SolidPlane(center, Quaternion.LookRotation(calculateTangent(normal), normal), size); + } + } + + /// <summary> + /// Draws a solid plane. + /// + /// The plane will lie in the XZ plane with respect to the rotation. + /// + /// <code> + /// Draw.SolidPlane(new float3(0, 0, 0), new float3(0, 1, 0), 1.0f, color); + /// </code> + /// [Open online documentation to see images] + /// </summary> + /// <param name="center">Center of the visualized plane.</param> + /// <param name="size">Width and height of the visualized plane.</param> + public void SolidPlane (float3 center, quaternion rotation, float2 size) { + PushMatrix(float4x4.TRS(center, rotation, new float3(size.x, 0, size.y))); + Reserve<BoxData>(); + Add(Command.Box); + Add(new BoxData { center = 0, size = 1 }); + PopMatrix(); + } + + /// <summary>Returns an arbitrary vector which is orthogonal to the given one</summary> + private static float3 calculateTangent (float3 normal) { + var tangent = math.cross(new float3(0, 1, 0), normal); + + if (math.all(tangent == 0)) tangent = math.cross(new float3(1, 0, 0), normal); + return tangent; + } + + /// <summary> + /// Draws a wire plane. + /// + /// <code> + /// Draw.WirePlane(new float3(0, 0, 0), new float3(0, 1, 0), 1.0f, color); + /// </code> + /// [Open online documentation to see images] + /// </summary> + /// <param name="center">Center of the visualized plane.</param> + /// <param name="normal">Direction perpendicular to the plane. If this is (0,0,0) then nothing will be rendered.</param> + /// <param name="size">Width and height of the visualized plane.</param> + public void WirePlane (float3 center, float3 normal, float2 size) { + if (math.any(normal)) { + WirePlane(center, Quaternion.LookRotation(calculateTangent(normal), normal), size); + } + } + + /// <summary> + /// Draws a wire plane. + /// + /// This is identical to <see cref="WireRectangle(float3,quaternion,float2)"/>, but it is included for consistency. + /// + /// <code> + /// Draw.WirePlane(new float3(0, 0, 0), new float3(0, 1, 0), 1.0f, color); + /// </code> + /// [Open online documentation to see images] + /// </summary> + /// <param name="center">Center of the visualized plane.</param> + /// <param name="rotation">Rotation of the plane. The plane will lie in the XZ plane with respect to the rotation.</param> + /// <param name="size">Width and height of the visualized plane.</param> + public void WirePlane (float3 center, quaternion rotation, float2 size) { + Reserve<PlaneData>(); + Add(Command.WirePlane); + Add(new PlaneData { center = center, rotation = rotation, size = size }); + } + + /// <summary> + /// Draws a plane and a visualization of its normal. + /// + /// <code> + /// Draw.PlaneWithNormal(new float3(0, 0, 0), new float3(0, 1, 0), 1.0f, color); + /// </code> + /// [Open online documentation to see images] + /// </summary> + /// <param name="center">Center of the visualized plane.</param> + /// <param name="normal">Direction perpendicular to the plane. If this is (0,0,0) then nothing will be rendered.</param> + /// <param name="size">Width and height of the visualized plane.</param> + public void PlaneWithNormal (float3 center, float3 normal, float2 size) { + if (math.any(normal)) { + PlaneWithNormal(center, Quaternion.LookRotation(calculateTangent(normal), normal), size); + } + } + + /// <summary> + /// Draws a plane and a visualization of its normal. + /// + /// <code> + /// Draw.PlaneWithNormal(new float3(0, 0, 0), new float3(0, 1, 0), 1.0f, color); + /// </code> + /// [Open online documentation to see images] + /// </summary> + /// <param name="center">Center of the visualized plane.</param> + /// <param name="rotation">Rotation of the plane. The plane will lie in the XZ plane with respect to the rotation.</param> + /// <param name="size">Width and height of the visualized plane.</param> + public void PlaneWithNormal (float3 center, quaternion rotation, float2 size) { + SolidPlane(center, rotation, size); + WirePlane(center, rotation, size); + ArrowRelativeSizeHead(center, center + math.mul(rotation, new float3(0, 1, 0)) * 0.5f, math.mul(rotation, new float3(0, 0, 1)), 0.2f); + } + + /// <summary> + /// Draws a solid triangle. + /// + /// <code> + /// Draw.xy.SolidTriangle(new float2(-0.43f, -0.25f), new float2(0, 0.5f), new float2(0.43f, -0.25f), color); + /// </code> + /// [Open online documentation to see images] + /// + /// Note: If you are going to be drawing lots of triangles it's better to use <see cref="Draw.SolidMesh"/> instead as it will be more efficient. + /// + /// See: <see cref="Draw.SolidMesh"/> + /// See: <see cref="Draw.WireTriangle"/> + /// </summary> + /// <param name="a">First corner of the triangle.</param> + /// <param name="b">Second corner of the triangle.</param> + /// <param name="c">Third corner of the triangle.</param> + public void SolidTriangle (float3 a, float3 b, float3 c) { + Reserve<TriangleData>(); + Add(Command.SolidTriangle); + Add(new TriangleData { a = a, b = b, c = c }); + } + + /// <summary> + /// Draws a solid box. + /// + /// <code> + /// Draw.SolidBox(new float3(0, 0, 0), new float3(1, 1, 1), color); + /// </code> + /// [Open online documentation to see images] + /// </summary> + /// <param name="center">Center of the box</param> + /// <param name="size">Width of the box along all dimensions</param> + public void SolidBox (float3 center, float3 size) { + Reserve<BoxData>(); + Add(Command.Box); + Add(new BoxData { center = center, size = size }); + } + + /// <summary> + /// Draws a solid box. + /// + /// <code> + /// Draw.SolidBox(new float3(0, 0, 0), new float3(1, 1, 1), color); + /// </code> + /// [Open online documentation to see images] + /// </summary> + /// <param name="bounds">Bounding box of the box</param> + public void SolidBox (Bounds bounds) { + SolidBox(bounds.center, bounds.size); + } + + /// <summary> + /// Draws a solid box. + /// + /// <code> + /// Draw.SolidBox(new float3(0, 0, 0), new float3(1, 1, 1), color); + /// </code> + /// [Open online documentation to see images] + /// </summary> + /// <param name="center">Center of the box</param> + /// <param name="rotation">Rotation of the box</param> + /// <param name="size">Width of the box along all dimensions</param> + public void SolidBox (float3 center, quaternion rotation, float3 size) { + PushMatrix(float4x4.TRS(center, rotation, size)); + SolidBox(float3.zero, Vector3.one); + PopMatrix(); + } + + /// <summary> + /// Draws a label in 3D space. + /// + /// The default alignment is <see cref="Drawing.LabelAlignment.MiddleLeft"/>. + /// + /// <code> + /// Draw.Label3D(new float3(0.2f, -1f, 0.2f), Quaternion.Euler(45, -110, -90), "Label", 1, LabelAlignment.Center, color); + /// </code> + /// [Open online documentation to see images] + /// + /// See: Label3D(float3,quaternion,string,float,LabelAlignment) + /// + /// Note: Only ASCII is supported since the built-in font texture only includes ASCII. Other characters will be rendered as question marks (?). + /// </summary> + /// <param name="position">Position in 3D space.</param> + /// <param name="rotation">Rotation in 3D space.</param> + /// <param name="text">Text to display.</param> + /// <param name="size">World size of the text. For large sizes an SDF (signed distance field) font is used and for small sizes a normal font texture is used.</param> + public void Label3D (float3 position, quaternion rotation, string text, float size) { + Label3D(position, rotation, text, size, LabelAlignment.MiddleLeft); + } + + /// <summary> + /// Draws a label in 3D space. + /// + /// <code> + /// Draw.Label3D(new float3(0.2f, -1f, 0.2f), Quaternion.Euler(45, -110, -90), "Label", 1, LabelAlignment.Center, color); + /// </code> + /// [Open online documentation to see images] + /// + /// See: Label3D(float3,quaternion,string,float) + /// + /// Note: Only ASCII is supported since the built-in font texture only includes ASCII. Other characters will be rendered as question marks (?). + /// + /// Note: This method cannot be used in burst since managed strings are not suppported in burst. However, you can use the separate Label3D overload which takes a FixedString. + /// </summary> + /// <param name="position">Position in 3D space.</param> + /// <param name="rotation">Rotation in 3D space.</param> + /// <param name="text">Text to display.</param> + /// <param name="size">World size of the text. For large sizes an SDF (signed distance field) font is used and for small sizes a normal font texture is used.</param> + /// <param name="alignment">How to align the text relative to the given position.</param> + public void Label3D (float3 position, quaternion rotation, string text, float size, LabelAlignment alignment) { + AssertBufferExists(); + var g = gizmos.Target as DrawingData; + Reserve<TextData3D>(); + Add(Command.Text3D); + Add(new TextData3D { center = position, rotation = rotation, numCharacters = text.Length, size = size, alignment = alignment }); + + Reserve(UnsafeUtility.SizeOf<System.UInt16>() * text.Length); + for (int i = 0; i < text.Length; i++) { + char c = text[i]; + System.UInt16 index = (System.UInt16)g.fontData.GetIndex(c); + Add(index); + } + } + + /// <summary> + /// Draws a label in 3D space aligned with the camera. + /// + /// The default alignment is <see cref="Drawing.LabelAlignment.MiddleLeft"/>. + /// + /// <code> + /// Draw.Label2D(Vector3.zero, "Label", 48, LabelAlignment.Center, color); + /// </code> + /// [Open online documentation to see images] + /// + /// See: Label2D(float3,string,float,LabelAlignment) + /// + /// Note: Only ASCII is supported since the built-in font texture only includes ASCII. Other characters will be rendered as question marks (?). + /// </summary> + /// <param name="position">Position in 3D space.</param> + /// <param name="text">Text to display.</param> + /// <param name="sizeInPixels">Size of the text in screen pixels. For large sizes an SDF (signed distance field) font is used and for small sizes a normal font texture is used.</param> + public void Label2D (float3 position, string text, float sizeInPixels = 14) { + Label2D(position, text, sizeInPixels, LabelAlignment.MiddleLeft); + } + + /// <summary> + /// Draws a label in 3D space aligned with the camera. + /// + /// <code> + /// Draw.Label2D(Vector3.zero, "Label", 48, LabelAlignment.Center, color); + /// </code> + /// [Open online documentation to see images] + /// + /// See: Label2D(float3,string,float) + /// + /// Note: Only ASCII is supported since the built-in font texture only includes ASCII. Other characters will be rendered as question marks (?). + /// + /// Note: This method cannot be used in burst since managed strings are not suppported in burst. However, you can use the separate Label2D overload which takes a FixedString. + /// </summary> + /// <param name="position">Position in 3D space.</param> + /// <param name="text">Text to display.</param> + /// <param name="sizeInPixels">Size of the text in screen pixels. For large sizes an SDF (signed distance field) font is used and for small sizes a normal font texture is used.</param> + /// <param name="alignment">How to align the text relative to the given position.</param> + public void Label2D (float3 position, string text, float sizeInPixels, LabelAlignment alignment) { + AssertBufferExists(); + var g = gizmos.Target as DrawingData; + Reserve<TextData>(); + Add(Command.Text); + Add(new TextData { center = position, numCharacters = text.Length, sizeInPixels = sizeInPixels, alignment = alignment }); + + Reserve(UnsafeUtility.SizeOf<System.UInt16>() * text.Length); + for (int i = 0; i < text.Length; i++) { + char c = text[i]; + System.UInt16 index = (System.UInt16)g.fontData.GetIndex(c); + Add(index); + } + } + + #region Label2DFixedString + /// <summary> + /// Draws a label in 3D space aligned with the camera. + /// + /// <code> + /// // This part can be inside a burst job + /// for (int i = 0; i < 10; i++) { + /// Unity.Collections.FixedString32Bytes text = $"X = {i}"; + /// builder.Label2D(new float3(i, 0, 0), ref text, 12, LabelAlignment.Center); + /// } + /// </code> + /// [Open online documentation to see images] + /// + /// See: Label2D(float3,string,float) + /// + /// Note: Only ASCII is supported since the built-in font texture only includes ASCII. Other characters will be rendered as question marks (?). + /// + /// Note: This method requires the Unity.Collections package version 0.8 or later. + /// </summary> + /// <param name="position">Position in 3D space.</param> + /// <param name="text">Text to display.</param> + /// <param name="sizeInPixels">Size of the text in screen pixels. For large sizes an SDF (signed distance field) font is used and for small sizes a normal font texture is used.</param> + public void Label2D (float3 position, ref FixedString32Bytes text, float sizeInPixels = 14) { + Label2D(position, ref text, sizeInPixels, LabelAlignment.MiddleLeft); + } + + /// <summary>\copydocref{Label2D(float3,FixedString32Bytes,float)}</summary> + public void Label2D (float3 position, ref FixedString64Bytes text, float sizeInPixels = 14) { + Label2D(position, ref text, sizeInPixels, LabelAlignment.MiddleLeft); + } + + /// <summary>\copydocref{Label2D(float3,FixedString32Bytes,float)}</summary> + public void Label2D (float3 position, ref FixedString128Bytes text, float sizeInPixels = 14) { + Label2D(position, ref text, sizeInPixels, LabelAlignment.MiddleLeft); + } + + /// <summary>\copydocref{Label2D(float3,FixedString32Bytes,float)}</summary> + public void Label2D (float3 position, ref FixedString512Bytes text, float sizeInPixels = 14) { + Label2D(position, ref text, sizeInPixels, LabelAlignment.MiddleLeft); + } + + /// <summary> + /// Draws a label in 3D space aligned with the camera. + /// + /// <code> + /// // This part can be inside a burst job + /// for (int i = 0; i < 10; i++) { + /// Unity.Collections.FixedString32Bytes text = $"X = {i}"; + /// builder.Label2D(new float3(i, 0, 0), ref text, 12, LabelAlignment.Center); + /// } + /// </code> + /// [Open online documentation to see images] + /// + /// See: Label2D(float3,string,float) + /// + /// Note: Only ASCII is supported since the built-in font texture only includes ASCII. Other characters will be rendered as question marks (?). + /// + /// Note: This method requires the Unity.Collections package version 0.8 or later. + /// </summary> + /// <param name="position">Position in 3D space.</param> + /// <param name="text">Text to display.</param> + /// <param name="sizeInPixels">Size of the text in screen pixels. For large sizes an SDF (signed distance field) font is used and for small sizes a normal font texture is used.</param> + /// <param name="alignment">How to align the text relative to the given position.</param> + public void Label2D (float3 position, ref FixedString32Bytes text, float sizeInPixels, LabelAlignment alignment) { +#if MODULE_COLLECTIONS_0_12_0_OR_NEWER + unsafe { + Label2D(position, text.GetUnsafePtr(), text.Length, sizeInPixels, alignment); + } +#else + Debug.LogError("The Label2D method which takes FixedStrings requires the Unity.Collections package version 0.12 or newer"); +#endif + } + + /// <summary>\copydocref{Label2D(float3,FixedString32Bytes,float,LabelAlignment)}</summary> + public void Label2D (float3 position, ref FixedString64Bytes text, float sizeInPixels, LabelAlignment alignment) { +#if MODULE_COLLECTIONS_0_12_0_OR_NEWER + unsafe { + Label2D(position, text.GetUnsafePtr(), text.Length, sizeInPixels, alignment); + } +#else + Debug.LogError("The Label2D method which takes FixedStrings requires the Unity.Collections package version 0.12 or newer"); +#endif + } + + /// <summary>\copydocref{Label2D(float3,FixedString32Bytes,float,LabelAlignment)}</summary> + public void Label2D (float3 position, ref FixedString128Bytes text, float sizeInPixels, LabelAlignment alignment) { +#if MODULE_COLLECTIONS_0_12_0_OR_NEWER + unsafe { + Label2D(position, text.GetUnsafePtr(), text.Length, sizeInPixels, alignment); + } +#else + Debug.LogError("The Label2D method which takes FixedStrings requires the Unity.Collections package version 0.12 or newer"); +#endif + } + + /// <summary>\copydocref{Label2D(float3,FixedString32Bytes,float,LabelAlignment)}</summary> + public void Label2D (float3 position, ref FixedString512Bytes text, float sizeInPixels, LabelAlignment alignment) { +#if MODULE_COLLECTIONS_0_12_0_OR_NEWER + unsafe { + Label2D(position, text.GetUnsafePtr(), text.Length, sizeInPixels, alignment); + } +#else + Debug.LogError("The Label2D method which takes FixedStrings requires the Unity.Collections package version 0.12 or newer"); +#endif + } + + /// <summary>\copydocref{Label2D(float3,FixedString32Bytes,float,LabelAlignment)}</summary> + internal unsafe void Label2D (float3 position, byte* text, int byteCount, float sizeInPixels, LabelAlignment alignment) { +#if MODULE_COLLECTIONS_0_12_0_OR_NEWER + AssertBufferExists(); + Reserve<TextData>(); + Add(Command.Text); + Add(new TextData { center = position, numCharacters = byteCount, sizeInPixels = sizeInPixels, alignment = alignment }); + + Reserve(UnsafeUtility.SizeOf<System.UInt16>() * byteCount); + for (int i = 0; i < byteCount; i++) { + // The first 128 elements in the font data are guaranteed to be laid out as ascii. + // We use this since we cannot use the dynamic font lookup. + System.UInt16 c = *(text + i); + if (c >= 128) c = (System.UInt16) '?'; + if (c == (byte)'\n') c = SDFLookupData.Newline; + // Ignore carriage return instead of printing them as '?'. Windows encodes newlines as \r\n. + if (c == (byte)'\r') continue; + Add(c); + } +#endif + } + #endregion + + #region Label3DFixedString + /// <summary> + /// Draws a label in 3D space. + /// + /// <code> + /// // This part can be inside a burst job + /// for (int i = 0; i < 10; i++) { + /// Unity.Collections.FixedString32Bytes text = $"X = {i}"; + /// builder.Label3D(new float3(i, 0, 0), quaternion.identity, ref text, 1, LabelAlignment.Center); + /// } + /// </code> + /// [Open online documentation to see images] + /// + /// See: Label3D(float3,quaternion,string,float) + /// + /// Note: Only ASCII is supported since the built-in font texture only includes ASCII. Other characters will be rendered as question marks (?). + /// + /// Note: This method requires the Unity.Collections package version 0.8 or later. + /// </summary> + /// <param name="position">Position in 3D space.</param> + /// <param name="rotation">Rotation in 3D space.</param> + /// <param name="text">Text to display.</param> + /// <param name="size">World size of the text. For large sizes an SDF (signed distance field) font is used and for small sizes a normal font texture is used.</param> + public void Label3D (float3 position, quaternion rotation, ref FixedString32Bytes text, float size) { + Label3D(position, rotation, ref text, size, LabelAlignment.MiddleLeft); + } + + /// <summary>\copydocref{Label3D(float3,quaternion,FixedString32Bytes,float)}</summary> + public void Label3D (float3 position, quaternion rotation, ref FixedString64Bytes text, float size) { + Label3D(position, rotation, ref text, size, LabelAlignment.MiddleLeft); + } + + /// <summary>\copydocref{Label3D(float3,quaternion,FixedString32Bytes,float)}</summary> + public void Label3D (float3 position, quaternion rotation, ref FixedString128Bytes text, float size) { + Label3D(position, rotation, ref text, size, LabelAlignment.MiddleLeft); + } + + /// <summary>\copydocref{Label3D(float3,quaternion,FixedString32Bytes,float)}</summary> + public void Label3D (float3 position, quaternion rotation, ref FixedString512Bytes text, float size) { + Label3D(position, rotation, ref text, size, LabelAlignment.MiddleLeft); + } + + /// <summary> + /// Draws a label in 3D space. + /// + /// <code> + /// // This part can be inside a burst job + /// for (int i = 0; i < 10; i++) { + /// Unity.Collections.FixedString32Bytes text = $"X = {i}"; + /// builder.Label3D(new float3(i, 0, 0), quaternion.identity, ref text, 1, LabelAlignment.Center); + /// } + /// </code> + /// [Open online documentation to see images] + /// + /// See: Label3D(float3,quaternion,string,float) + /// + /// Note: Only ASCII is supported since the built-in font texture only includes ASCII. Other characters will be rendered as question marks (?). + /// + /// Note: This method requires the Unity.Collections package version 0.8 or later. + /// </summary> + /// <param name="position">Position in 3D space.</param> + /// <param name="rotation">Rotation in 3D space.</param> + /// <param name="text">Text to display.</param> + /// <param name="size">World size of the text. For large sizes an SDF (signed distance field) font is used and for small sizes a normal font texture is used.</param> + /// <param name="alignment">How to align the text relative to the given position.</param> + public void Label3D (float3 position, quaternion rotation, ref FixedString32Bytes text, float size, LabelAlignment alignment) { +#if MODULE_COLLECTIONS_0_12_0_OR_NEWER + unsafe { + Label3D(position, rotation, text.GetUnsafePtr(), text.Length, size, alignment); + } +#else + Debug.LogError("The Label3D method which takes FixedStrings requires the Unity.Collections package version 0.12 or newer"); +#endif + } + + /// <summary>\copydocref{Label3D(float3,quaternion,FixedString32Bytes,float,LabelAlignment)}</summary> + public void Label3D (float3 position, quaternion rotation, ref FixedString64Bytes text, float size, LabelAlignment alignment) { +#if MODULE_COLLECTIONS_0_12_0_OR_NEWER + unsafe { + Label3D(position, rotation, text.GetUnsafePtr(), text.Length, size, alignment); + } +#else + Debug.LogError("The Label3D method which takes FixedStrings requires the Unity.Collections package version 0.12 or newer"); +#endif + } + + /// <summary>\copydocref{Label3D(float3,quaternion,FixedString32Bytes,float,LabelAlignment)}</summary> + public void Label3D (float3 position, quaternion rotation, ref FixedString128Bytes text, float size, LabelAlignment alignment) { +#if MODULE_COLLECTIONS_0_12_0_OR_NEWER + unsafe { + Label3D(position, rotation, text.GetUnsafePtr(), text.Length, size, alignment); + } +#else + Debug.LogError("The Label3D method which takes FixedStrings requires the Unity.Collections package version 0.12 or newer"); +#endif + } + + /// <summary>\copydocref{Label3D(float3,quaternion,FixedString32Bytes,float,LabelAlignment)}</summary> + public void Label3D (float3 position, quaternion rotation, ref FixedString512Bytes text, float size, LabelAlignment alignment) { +#if MODULE_COLLECTIONS_0_12_0_OR_NEWER + unsafe { + Label3D(position, rotation, text.GetUnsafePtr(), text.Length, size, alignment); + } +#else + Debug.LogError("The Label3D method which takes FixedStrings requires the Unity.Collections package version 0.12 or newer"); +#endif + } + + /// <summary>\copydocref{Label3D(float3,quaternion,FixedString32Bytes,float,LabelAlignment)}</summary> + internal unsafe void Label3D (float3 position, quaternion rotation, byte* text, int byteCount, float size, LabelAlignment alignment) { +#if MODULE_COLLECTIONS_0_12_0_OR_NEWER + AssertBufferExists(); + Reserve<TextData3D>(); + Add(Command.Text3D); + Add(new TextData3D { center = position, rotation = rotation, numCharacters = byteCount, size = size, alignment = alignment }); + + Reserve(UnsafeUtility.SizeOf<System.UInt16>() * byteCount); + for (int i = 0; i < byteCount; i++) { + // The first 128 elements in the font data are guaranteed to be laid out as ascii. + // We use this since we cannot use the dynamic font lookup. + System.UInt16 c = *(text + i); + if (c >= 128) c = (System.UInt16) '?'; + if (c == (byte)'\n') c = SDFLookupData.Newline; + Add(c); + } +#endif + } + #endregion + } +} |