diff --git a/.github/RELEASE_CHECKLIST.md b/.github/RELEASE_CHECKLIST.md index e9230d8..675e497 100644 --- a/.github/RELEASE_CHECKLIST.md +++ b/.github/RELEASE_CHECKLIST.md @@ -7,10 +7,11 @@ Continuous integration checks what a machine without a GPU can: the tests on the 1. Every check of the pull request passes: the tests on both Unity versions, and the WebGL, Android, iOS, macOS and Linux builds. 2. The whole test suite passes locally on a real GPU, on the oldest and the newest supported Unity. 3. The Stress Test sample runs smoothly in Windows, Android and WebGL builds, with frame, main thread and GPU times no worse than the previous release's. -4. The Shape Gallery sample and all three occlusion modes look right on the same devices, in the Built-in, URP and HDRP pipelines. -5. A Profiler capture of an Android development build under load shows no new spikes in the `Wireframes.*` markers and no allocations in steady frames. -6. The timings in the test results artifacts are no worse than the previous release's. -7. `package.json` has the new version, `CHANGELOG.md` has its section, and the README and `Documentation/` describe the release; a major release also updates `Documentation/MIGRATION.md`. +4. Both scenes of the Shape Gallery sample and all three occlusion modes look right on the same devices, in the Built-in, URP and HDRP pipelines. +5. In the Editor, under each pipeline, shape components draw in the Scene and Game views and in Prefab Mode, Draw As Gizmo shapes show only in the Scene view and in the Game view with Gizmos on, and clicking a wireframe never selects a hidden container. +6. A Profiler capture of an Android development build under load shows no new spikes in the `Wireframes.*` markers and no allocations in steady frames. +7. The timings in the test results artifacts are no worse than the previous release's. +8. `package.json` has the new version, `CHANGELOG.md` has its section, and the README and `Documentation/` describe the release; a major release also updates `Documentation/MIGRATION.md`. ### After merging diff --git a/CHANGELOG.md b/CHANGELOG.md index 0d57a7f..e979aee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,23 @@ +# Wireframes (Unreleased) + +### What's new + +1. Shape components draw a shape on their GameObject without code, in Edit Mode, Play Mode and builds: **Add Component > Wireframes** has one for every shape, such as `WireframeSphere` and `WireframeLine`. +2. Every change to a component shows up in the next render, made in the Inspector, by undo, animation or a script, and components do no work per frame. +3. Disabling a component or its GameObject hides its shape at no cost, and enabling it again allocates nothing. +4. Components share containers by occlusion, transparency and layer, so many of them draw in a few draw calls; a color with alpha below 1 draws transparent, and shapes draw on their GameObject's layer. +5. `DrawAsGizmo` draws a component's shape like a gizmo, in the Scene view and in the Game view while its Gizmos button is on, and builds leave it out. +6. Prefab Mode draws the components of the prefab being edited. +7. The Shape Gallery sample has a ComponentGallery scene made of components, and continuous integration builds it too. +8. A container without shapes skips its work before each render once its empty chunks are released. + +### Known issues + +1. The Hierarchy's Scene visibility toggles don't hide components' wireframes, and the Gizmos menu's per-component checkboxes don't affect gizmo shapes. +2. Clicking a component's wireframe in the Scene view doesn't select its GameObject. +3. A script that changes a GameObject's layer moves its components' shapes to that layer on their next change. +4. Stereo rendering for XR is untested. + # Wireframes 2.0.0 ### What's new diff --git a/Documentation/FEATURES.md b/Documentation/FEATURES.md index e62c65e..c36f704 100644 --- a/Documentation/FEATURES.md +++ b/Documentation/FEATURES.md @@ -14,28 +14,42 @@ A shape attaches to Transforms, called bones, and moves, turns and scales with t ### 17 shapes -| Shape | Usual Create method | What it is | -|---|---|---| -| `ILine` | `CreateLine(a, b)` | A segment; each end has its own bone. | -| `IPolyline` | `CreatePolyline(points)`, `CreatePolygon(points)`, `CreateTriangle(a, b, c)` | Points joined in order, open or closed; each point has its own bone and color. | -| `IBox` | `CreateBox(cornerA, cornerB)` | A box with a center, a rotation and a `Size`. | -| `IRectangle` | `CreateRectangle(cornerA, cornerB)` | A flat rectangle. | -| `IRoundedRectangle` | `CreateRoundedRectangle(cornerA, cornerB, cornerRadius)` | A flat rectangle with round corners. | -| `ICircle` | `CreateCircle(center, radius)` | A flat circle; `CreateCircle(center, normal, radius)` faces a direction. | -| `IEllipse` | `CreateEllipse(tipA, tipB, radius)` | A flat ellipse whose long axis runs between two tips. | -| `IStar` | `CreateStar(center, innerRadius, outerRadius, pointCount)` | A flat star. | -| `ISphere` | `CreateSphere(center, radius)` | Three great circles. | -| `IEllipsoid` | `CreateEllipsoid(tipA, tipB, radius)` | Three ellipses, with a radius along each axis. | -| `ISpikedSphere` | `CreateSpikedSphere(center, baseRadius, spikeLength, spikeCount)` | A 3D star: a Platonic solid with a spike on each face, so 4, 6, 8, 12 or 20 spikes. | -| `ICylinder` | `CreateCylinder(endA, endB, radius)` | Two rings joined by four lines. | -| `ICone` | `CreateCone(tip, baseCenter, radius)` | A base ring joined to the tip by four lines. | -| `ICapsule` | `CreateCapsule(centerA, centerB, radius)` | Two spheres wrapped together, like `Physics.CapsuleCast`; each end can have its own radius. | -| `IStadium` | `CreateStadium(centerA, centerB, radius)` | The flat outline of a capsule. | -| `IFrustum` | `CreateFrustum(endA, endB, radiusA, radiusB, sideCount)` | Two regular polygons joined at every corner. Prisms and regular pyramids are frustums too. | -| `IPyramid` | `CreatePyramid(tip, baseCenter, baseSize)` | A rectangular base joined to a tip, like a camera's view without its near plane. | +| Shape | Usual Create method | Component | What it is | +|---|---|---|---| +| `ILine` | `CreateLine(a, b)` | `WireframeLine` | A segment; each end has its own bone. | +| `IPolyline` | `CreatePolyline(points)`, `CreatePolygon(points)`, `CreateTriangle(a, b, c)` | `WireframePolyline` | Points joined in order, open or closed; each point has its own bone and color. | +| `IBox` | `CreateBox(cornerA, cornerB)` | `WireframeBox` | A box with a center, a rotation and a `Size`. | +| `IRectangle` | `CreateRectangle(cornerA, cornerB)` | `WireframeRectangle` | A flat rectangle. | +| `IRoundedRectangle` | `CreateRoundedRectangle(cornerA, cornerB, cornerRadius)` | `WireframeRoundedRectangle` | A flat rectangle with round corners. | +| `ICircle` | `CreateCircle(center, radius)` | `WireframeCircle` | A flat circle; `CreateCircle(center, normal, radius)` faces a direction. | +| `IEllipse` | `CreateEllipse(tipA, tipB, radius)` | `WireframeEllipse` | A flat ellipse whose long axis runs between two tips. | +| `IStar` | `CreateStar(center, innerRadius, outerRadius, pointCount)` | `WireframeStar` | A flat star. | +| `ISphere` | `CreateSphere(center, radius)` | `WireframeSphere` | Three great circles. | +| `IEllipsoid` | `CreateEllipsoid(tipA, tipB, radius)` | `WireframeEllipsoid` | Three ellipses, with a radius along each axis. | +| `ISpikedSphere` | `CreateSpikedSphere(center, baseRadius, spikeLength, spikeCount)` | `WireframeSpikedSphere` | A 3D star: a Platonic solid with a spike on each face, so 4, 6, 8, 12 or 20 spikes. | +| `ICylinder` | `CreateCylinder(endA, endB, radius)` | `WireframeCylinder` | Two rings joined by four lines. | +| `ICone` | `CreateCone(tip, baseCenter, radius)` | `WireframeCone` | A base ring joined to the tip by four lines. | +| `ICapsule` | `CreateCapsule(centerA, centerB, radius)` | `WireframeCapsule` | Two spheres wrapped together, like `Physics.CapsuleCast`; each end can have its own radius. | +| `IStadium` | `CreateStadium(centerA, centerB, radius)` | `WireframeStadium` | The flat outline of a capsule. | +| `IFrustum` | `CreateFrustum(endA, endB, radiusA, radiusB, sideCount)` | `WireframeFrustum` | Two regular polygons joined at every corner. Prisms and regular pyramids are frustums too. | +| `IPyramid` | `CreatePyramid(tip, baseCenter, baseSize)` | `WireframePyramid` | A rectangular base joined to a tip, like a camera's view without its near plane. | Every shape except lines and polylines also has a Create method on a bone, in its local space, one from a position and a rotation, and one without arguments for a white shape of unit size. Round shapes are drawn like Unity's gizmos, with rings and a few lines, from 3 to 1,024 segments per ring. +### Shape components + +Every shape has a component that draws it on its GameObject without any code, the same in Edit Mode, Play Mode and builds. + +1. The shape follows its GameObject's Transform like a mesh, scale included. Rigid shapes take a `Center` and a `Rotation` relative to it, or for long shapes the two ends of their axis and a `Roll` around it; points of lines and polylines can follow other Transforms. +2. Every change shows up in the next render, made in the Inspector, by undo, a prefab revert, animation or a script. Components do no work per frame, so a moving GameObject costs nothing more than a moving bone. +3. Disabling a component, or its GameObject, hides its shape and costs nothing, and enabling it again allocates nothing. +4. Components share containers, one per combination of occlusion, transparency, layer and gizmo drawing, so hundreds of them draw in a few draw calls. A color with alpha below 1 draws transparent, and a shape draws on its GameObject's layer. +5. `DrawAsGizmo` draws a shape like a gizmo: in the Scene view, and in the Game view only while its Gizmos button is on. Builds leave such shapes out. +6. Prefab Mode draws the components of the prefab being edited, in its own scene. +7. Counts that runtime shapes fix at creation, such as `SegmentCount`, can change on a component, which creates its shape again. + +The Hierarchy's Scene visibility toggles don't hide components' wireframes, and the Gizmos menu's per-component checkboxes don't affect gizmo shapes. + ### Edits upload only what they change A setter only records the change, and each edited shape is written once per frame however many of its properties changed. Only the changed ranges of the GPU buffers are uploaded, and moving bones uploads nothing but their matrices. Tests check that editing one shape among many uploads that shape alone. @@ -49,7 +63,7 @@ Frames that move bones, edit shapes, hide and show them, or change nothing alloc A `WireframeContainer` creates and draws shapes, and disposes them when it is disposed or when the scene it was created in unloads. `WireframeContainerSettings` chooses how, and is serializable, so a script can show it in the Inspector. | Setting | Default | What it does | -|---|---|---| +|---|---|---|---| | `Occlusion` | `Hide` | What is drawn of lines that other geometry hides: nothing, everything, or a dimmer line. | | `UseAlpha` | false | Blends each color by its alpha instead of drawing opaque lines. | | `Material` | none | Draws with your material instead of the package's. | @@ -74,7 +88,7 @@ Shapes are stored in chunks, meshes of up to 65,535 vertices with 16-bit indices ### Play Mode, Edit Mode and builds -Containers and shapes work the same in Edit Mode, drawing in the Scene and Game views. There, a container is never saved into its scene and never marks it as changed; it is disposed before scripts reload and when its scene closes, unless it persists across scenes. Switching between Edit and Play Mode alone never disposes a container. +Containers and shapes work the same in Edit Mode, drawing in the Scene and Game views. There, a container is never saved into its scene and never marks it as changed; it is disposed before scripts reload and when its scene closes, unless it persists across scenes. Switching between Edit and Play Mode alone never disposes a container. Shape components draw in Edit Mode as soon as they are added. ### Visibility diff --git a/Documentation/USAGE.md b/Documentation/USAGE.md index 217c939..715839a 100644 --- a/Documentation/USAGE.md +++ b/Documentation/USAGE.md @@ -126,6 +126,42 @@ public sealed class BoundsPreview : MonoBehaviour 2. It is disposed before scripts reload, after which Unity calls `OnEnable` again, and when its scene closes, unless it persists across scenes. 3. Switching between Edit and Play Mode alone never disposes a container, though the script reload that Play Mode starts with by default does. +### Components + +To draw a shape without writing code, add its component to a GameObject: **Add Component > Wireframes** has one for every shape, from **Line** to **Pyramid**. It draws there right away, in Edit Mode, Play Mode and builds, and follows the GameObject's Transform like a mesh, scale included. Its fields place the shape relative to the GameObject: + +| Components | Place the shape with | +|---|---| +| `WireframeBox`, `WireframeRectangle`, `WireframeRoundedRectangle`, `WireframeCircle`, `WireframeEllipse`, `WireframeStar`, `WireframeSphere`, `WireframeEllipsoid`, `WireframeSpikedSphere` | **Center** and **Rotation**, in Euler angles. Flat shapes lie flat on the GameObject with no rotation. | +| `WireframeCylinder`, `WireframeCone`, `WireframeCapsule`, `WireframeStadium`, `WireframeFrustum`, `WireframePyramid` | **End A** and **End B**, the ends of the shape's axis, and **Roll** around it. A cone's or pyramid's tip is end A. With no roll, the shape's +Y stays as close to the GameObject's up as the axis allows. | +| `WireframeLine`, `WireframePolyline` | A **Bone** and a **Position** for each point: the point follows that Transform, or the GameObject when it has none. | + +1. **Every change shows up in the next render,** made in the Inspector, by undo, a prefab revert, animation or a script. A count that runtime shapes fix at creation, such as **Segment Count**, creates the shape again, once you finish typing it, and invalid counts snap to the nearest valid one. +2. **`enabled` shows and hides the shape.** Disabling the component or its GameObject costs nothing, and enabling it again allocates nothing. +3. **One color per shape.** **Color** colors all of it, and an alpha below 1 draws it transparent; **Occlusion** works like the container setting. Shapes draw on their GameObject's layer, for camera culling masks. +4. **Draw As Gizmo** draws the shape like a gizmo: in the Scene view, and in the Game view only while its **Gizmos** button is on. Other cameras never draw it, and builds leave it out. To leave out a whole GameObject meant for debugging, tag it **EditorOnly**, which strips it from builds with its children. +5. **A destroyed bone counts as none,** so its point follows the GameObject from the next render on. A bone has to be in a scene: one that isn't, such as a prefab asset, is reported once and the GameObject followed instead. +6. **New components draw what the Create methods without arguments draw,** relative to the GameObject: a white shape of unit size, with long shapes running 1 unit along +Z. A line runs 1 unit forward, and a polyline starts as a small triangle; it draws nothing with fewer than 2 points, or 3 when closed. + +Scripts set the same properties, and counts outside their range throw `ArgumentOutOfRangeException`, as Create methods do: + +```csharp +WireframeLine line = gameObject.AddComponent(); +line.BoneB = target; +line.PositionB = Vector3.up; +line.Color = Color.cyan; + +WireframeSphere sphere = gameObject.AddComponent(); +sphere.Radius = 2f; +sphere.Occlusion = WireframeOcclusion.Show; +``` + +Components share containers, one per combination of occlusion, transparency, layer and **Draw As Gizmo**, so many components draw in a few draw calls. Those containers are never saved, stay out of the Hierarchy, and go away with their last component. Prefab Mode draws the components of the prefab being edited in its own scene. + +1. The Hierarchy's Scene visibility toggles don't hide components' wireframes, and the per-component checkboxes of the **Gizmos** menu don't affect gizmo shapes. +2. A script that changes `gameObject.layer` moves the shape to that layer on the component's next change; a change in the Inspector moves it at once. +3. Clicking a wireframe in the Scene view doesn't select its GameObject. + ### Custom materials A custom material draws a container when its shader moves vertices with `WireframesSkin` from the package's include file. Each vertex arrives relative to its bone, with the bone's index in `TEXCOORD0`, and the package sets the `_WireframesBones` texture on every renderer. `WireframesColor` converts vertex colors in linear color space, as the package's shader does. @@ -183,7 +219,7 @@ Add the **WireframeCamera** component to a Camera to draw everything that camera Import them from the package's **Samples** tab in the Package Manager. -1. **Shape Gallery** lays out every shape in three rows, each turning with its own bone. Open its **ShapeGallery** scene and enter Play Mode. +1. **Shape Gallery** lays out every shape in three rows, each turning with its own bone. Open its **ShapeGallery** scene and enter Play Mode. Its **ComponentGallery** scene has the same shapes made of components, drawn as soon as it opens, and turning in Play Mode. 2. **Stress Test** spawns 10,000 lines, 1,000 boxes and 2,000 other shapes on 100 orbiting bones. Add the `StressTest` component to an empty GameObject in a scene with a camera, and enter Play Mode with the Profiler open. Raise **Recolor Per Frame** to measure the cost of edits. ### Tests diff --git a/README.md b/README.md index 3290d60..74855f0 100644 --- a/README.md +++ b/README.md @@ -6,22 +6,25 @@ Wireframe shapes for Unity that follow Transforms on the GPU: create a shape onc Lines, polylines, boxes, circles, spheres, capsules, cones and more attach to any Transforms, called bones. The package uploads each bone's matrix once per frame and the vertex shader moves every vertex, so moving shapes cost your code nothing and an edit uploads only what it changed. +Without writing code, add a shape component to a GameObject: it draws there at once, in Edit Mode too, and shows every change to its fields as you make it. + Shapes draw the same in Play Mode, Edit Mode and player builds, under the Built-in Render Pipeline, URP and HDRP, from desktops and phones to WebGL, while server builds keep them working without drawing. ### Features 1. Shapes follow Transforms on the GPU. 2. 17 shapes, from lines to capsules. -3. Edits upload only what they change. -4. Steady frames allocate nothing. -5. Built-in Render Pipeline, URP and HDRP. -6. Windows, Android, WebGL and more. -7. Play Mode, Edit Mode and builds. -8. Hidden lines hide, show or fade. -9. Shapes hide without being disposed. -10. Statistics and Profiler markers. -11. Destroyed bones leave shapes in place. -12. A camera that draws in wireframe. +3. Shape components, edited live in Edit Mode. +4. Edits upload only what they change. +5. Steady frames allocate nothing. +6. Built-in Render Pipeline, URP and HDRP. +7. Windows, Android, WebGL and more. +8. Play Mode, Edit Mode and builds. +9. Hidden lines hide, show or fade. +10. Shapes hide without being disposed. +11. Statistics and Profiler markers. +12. Destroyed bones leave shapes in place. +13. A camera that draws in wireframe. Detailed about features - see [FEATURES.md](Documentation/FEATURES.md). @@ -107,6 +110,8 @@ sphere.Radius = 2f; sphere.IsVisible = false; ``` +**Or add a shape component to a GameObject, with no code at all.** **Add Component > Wireframes** has one for every shape, from **Line** to **Pyramid**. It draws on its GameObject right away, in Edit Mode, Play Mode and builds, follows it like a mesh, and shows each change to its fields in the next render; disabling it hides the shape. + Detailed about usage - see [USAGE.md](Documentation/USAGE.md). ### License diff --git a/UnityPackage/Editor/ComponentRefresh.cs b/UnityPackage/Editor/ComponentRefresh.cs new file mode 100644 index 0000000..2434e1a --- /dev/null +++ b/UnityPackage/Editor/ComponentRefresh.cs @@ -0,0 +1,46 @@ +using UnityEditor; +using UnityEditorInternal; + +namespace reromanlee.Wireframes.Editor +{ + /// + /// Keeps shape components in step with what happens in the editor without a callback on the components: it runs the + /// refreshes that OnValidate had to put off, refreshes every component after undo and redo and the ones whose + /// GameObject changed layer, and repaints the views after a change in Edit Mode, which doesn't render on its own. + /// + [InitializeOnLoad] + internal static class ComponentRefresh + { + static ComponentRefresh() + { + EditorApplication.update += Update; + Undo.undoRedoPerformed += ShapeComponents.RefreshAll; + ObjectChangeEvents.changesPublished += OnChangesPublished; + } + + /// Runs the refreshes that were put off, then repaints the views if a change asked for it. + internal static void Update() + { + ShapeComponents.RefreshDeferred(); + if (ShapeComponents.TakeRepaintRequest()) + { + InternalEditorUtility.RepaintAllViews(); + } + } + + private static void OnChangesPublished(ref ObjectChangeEventStream stream) + { + for (int i = 0; i < stream.length; i++) + { + // A layer change is one of a GameObject's properties, or of a whole hierarchy's when it spreads to children. + ObjectChangeKind kind = stream.GetEventType(i); + if (kind == ObjectChangeKind.ChangeGameObjectOrComponentProperties + || kind == ObjectChangeKind.ChangeGameObjectStructureHierarchy) + { + ShapeComponents.RefreshLayers(); + return; + } + } + } + } +} diff --git a/UnityPackage/Editor/ComponentRefresh.cs.meta b/UnityPackage/Editor/ComponentRefresh.cs.meta new file mode 100644 index 0000000..abcd0c6 --- /dev/null +++ b/UnityPackage/Editor/ComponentRefresh.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: af0222b26c6d7b47912e51ed9e01b3fc diff --git a/UnityPackage/Editor/IntegerOptionsDrawer.cs b/UnityPackage/Editor/IntegerOptionsDrawer.cs new file mode 100644 index 0000000..a73700a --- /dev/null +++ b/UnityPackage/Editor/IntegerOptionsDrawer.cs @@ -0,0 +1,33 @@ +using System; +using UnityEditor; +using UnityEngine; + +namespace reromanlee.Wireframes.Editor +{ + /// Draws an int field marked with as a popup of its options. + [CustomPropertyDrawer(typeof(IntegerOptionsAttribute))] + internal sealed class IntegerOptionsDrawer : PropertyDrawer + { + private GUIContent[] _labels; + + public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) + { + if (property.propertyType != SerializedPropertyType.Integer) + { + EditorGUI.PropertyField(position, property, label); + return; + } + int[] options = ((IntegerOptionsAttribute)attribute).Options; + _labels ??= Array.ConvertAll(options, option => new GUIContent(option.ToString())); + + label = EditorGUI.BeginProperty(position, label, property); + EditorGUI.BeginChangeCheck(); + int value = EditorGUI.IntPopup(position, label, property.intValue, _labels, options); + if (EditorGUI.EndChangeCheck()) + { + property.intValue = value; + } + EditorGUI.EndProperty(); + } + } +} diff --git a/UnityPackage/Editor/IntegerOptionsDrawer.cs.meta b/UnityPackage/Editor/IntegerOptionsDrawer.cs.meta new file mode 100644 index 0000000..841240a --- /dev/null +++ b/UnityPackage/Editor/IntegerOptionsDrawer.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 491953cb5dd49d0cd38bb1c5f9b66e5d diff --git a/UnityPackage/Editor/WireframePolylineEditor.cs b/UnityPackage/Editor/WireframePolylineEditor.cs new file mode 100644 index 0000000..d3f0212 --- /dev/null +++ b/UnityPackage/Editor/WireframePolylineEditor.cs @@ -0,0 +1,25 @@ +using UnityEditor; +using Object = UnityEngine.Object; + +namespace reromanlee.Wireframes.Editor +{ + /// The polyline's Inspector, which also says when it has too few points to draw anything. + [CustomEditor(typeof(WireframePolyline))] + [CanEditMultipleObjects] + internal sealed class WireframePolylineEditor : UnityEditor.Editor + { + public override void OnInspectorGUI() + { + DrawDefaultInspector(); + foreach (Object selected in targets) + { + if (selected is WireframePolyline polyline && !polyline.CanCreateShape) + { + EditorGUILayout.HelpBox( + "A polyline needs at least 2 points, or 3 when closed, so nothing is drawn.", MessageType.Warning); + return; + } + } + } + } +} diff --git a/UnityPackage/Editor/WireframePolylineEditor.cs.meta b/UnityPackage/Editor/WireframePolylineEditor.cs.meta new file mode 100644 index 0000000..9d2bffe --- /dev/null +++ b/UnityPackage/Editor/WireframePolylineEditor.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 064c71df880de40445d6e04731c69104 diff --git a/UnityPackage/Runtime/Components.meta b/UnityPackage/Runtime/Components.meta new file mode 100644 index 0000000..865c7ca --- /dev/null +++ b/UnityPackage/Runtime/Components.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 18faf912d53fbe7235a718fe4d505d0f +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/UnityPackage/Runtime/Components/GizmoCameras.cs b/UnityPackage/Runtime/Components/GizmoCameras.cs new file mode 100644 index 0000000..322c3f3 --- /dev/null +++ b/UnityPackage/Runtime/Components/GizmoCameras.cs @@ -0,0 +1,44 @@ +using System; +using UnityEngine; + +namespace reromanlee.Wireframes +{ + /// + /// Decides which cameras draw the shapes of components drawn as gizmos: the ones Unity draws gizmos for. + /// + internal static class GizmoCameras + { + /// The check behind , which tests replace. + internal static Func Filter { get; set; } = DrawsGizmos; + + /// The camera filter of the containers that hold gizmo shapes. + internal static bool ShouldDraw(Camera camera) + { + return Filter(camera); + } + + /// + /// True for a Scene view camera while the Scene view's Gizmos button is on, and for a game camera while the Game + /// view that renders it has its Gizmos button on. Other cameras, such as previews and reflection probes, never + /// draw gizmos, and outside the Editor no camera does. + /// + internal static bool DrawsGizmos(Camera camera) + { +#if UNITY_EDITOR + switch (camera.cameraType) + { + case CameraType.SceneView: + return UnityEditor.Handles.ShouldRenderGizmos(); + case CameraType.Game: + // A Scene view renders game cameras too, for its camera preview, which shows no gizmos. + return UnityEditor.SceneView.currentDrawingSceneView == null + && UnityEditor.Handles.ShouldRenderGizmos(); + default: + return false; + } +#else + return false; +#endif + } + } +} diff --git a/UnityPackage/Runtime/Components/GizmoCameras.cs.meta b/UnityPackage/Runtime/Components/GizmoCameras.cs.meta new file mode 100644 index 0000000..bb5c982 --- /dev/null +++ b/UnityPackage/Runtime/Components/GizmoCameras.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 02c6e11ec10f7fc31f9a07d0bf2530a6 diff --git a/UnityPackage/Runtime/Components/IntegerOptionsAttribute.cs b/UnityPackage/Runtime/Components/IntegerOptionsAttribute.cs new file mode 100644 index 0000000..2b1ca35 --- /dev/null +++ b/UnityPackage/Runtime/Components/IntegerOptionsAttribute.cs @@ -0,0 +1,18 @@ +using UnityEngine; + +namespace reromanlee.Wireframes +{ + /// + /// Shows an int field in the Inspector as a choice between , for counts that take only a few + /// values. + /// + internal sealed class IntegerOptionsAttribute : PropertyAttribute + { + internal IntegerOptionsAttribute(params int[] options) + { + Options = options; + } + + internal int[] Options { get; } + } +} diff --git a/UnityPackage/Runtime/Components/IntegerOptionsAttribute.cs.meta b/UnityPackage/Runtime/Components/IntegerOptionsAttribute.cs.meta new file mode 100644 index 0000000..c512819 --- /dev/null +++ b/UnityPackage/Runtime/Components/IntegerOptionsAttribute.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: b44e5bca3c24b2a3de8419f32f773b9d diff --git a/UnityPackage/Runtime/Components/ShapeComponents.cs b/UnityPackage/Runtime/Components/ShapeComponents.cs new file mode 100644 index 0000000..d22c09d --- /dev/null +++ b/UnityPackage/Runtime/Components/ShapeComponents.cs @@ -0,0 +1,142 @@ +using System.Collections.Generic; +using System.Diagnostics; +using UnityEngine; + +namespace reromanlee.Wireframes +{ + /// + /// Keeps track of shape components for the events that refresh many of them at once: the enabled components, the + /// refreshes that OnValidate has to put off until the editor's next update, and whether the editor's views need a + /// repaint to show a change made in Edit Mode. + /// + internal static class ShapeComponents + { + private static readonly List Enabled = new(); + private static readonly List Deferred = new(); + private static bool _isRepaintRequested; + + /// Every enabled shape component, in no particular order. + internal static IReadOnlyList EnabledComponents + { + get => Enabled; + } + + internal static void Add(WireframeShape component) + { + component.EnabledIndex = Enabled.Count; + Enabled.Add(component); + } + + internal static void Remove(WireframeShape component) + { + int index = component.EnabledIndex; + if (index < 0) + { + return; + } + int last = Enabled.Count - 1; + WireframeShape moved = Enabled[last]; + Enabled[index] = moved; + moved.EnabledIndex = index; + Enabled.RemoveAt(last); + component.EnabledIndex = -1; + } + + /// + /// Puts off refreshing until , once however often it + /// is asked. + /// + internal static void Defer(WireframeShape component) + { + if (component.IsDeferred) + { + return; + } + component.IsDeferred = true; + Deferred.Add(component); + } + + /// Refreshes the components whose refresh was put off, for the editor's update loop. + internal static void RefreshDeferred() + { + // Refreshes put off while these run wait for the next update. + int count = Deferred.Count; + if (count == 0) + { + return; + } + for (int i = 0; i < count; i++) + { + WireframeShape component = Deferred[i]; + component.IsDeferred = false; + // A component destroyed meanwhile has nothing left to refresh. + if (component != null) + { + component.Refresh(); + } + } + Deferred.RemoveRange(0, count); + } + + /// Refreshes every enabled component, as after undo and redo, which can change any of them. + internal static void RefreshAll() + { + for (int i = Enabled.Count - 1; i >= 0; i--) + { + Enabled[i].Refresh(); + } + } + + /// + /// Refreshes the enabled components whose GameObject moved to another layer, which no callback reports. + /// + internal static void RefreshLayers() + { + for (int i = Enabled.Count - 1; i >= 0; i--) + { + WireframeShape component = Enabled[i]; + if (component.IsOnStaleLayer) + { + component.Refresh(); + } + } + } + + /// + /// Writes the fields of the point shapes in into their shapes again, after its flush found + /// bones destroyed, so points that followed one follow their GameObject instead in the same frame. + /// + internal static void RefreshAfterBonesDestroyed(MeshProxy proxy) + { + for (int i = Enabled.Count - 1; i >= 0; i--) + { + if (Enabled[i] is WireframePointShape component && component.SharedContainer != null + && component.SharedContainer.Container == proxy.Container) + { + component.RefreshInPlace(); + } + } + } + + /// + /// Asks for the editor's views to be repainted on its next update, so a change made outside the Inspector shows up + /// in Edit Mode too. Play Mode renders every frame anyway. + /// + [Conditional("UNITY_EDITOR")] + internal static void RequestRepaint() + { + if (!Application.isPlaying) + { + _isRepaintRequested = true; + } + } + + /// Returns whether a repaint was asked for since the last call, and clears the request. + internal static bool TakeRepaintRequest() + { + bool isRequested = _isRepaintRequested; + _isRepaintRequested = false; + return isRequested; + } + } +} diff --git a/UnityPackage/Runtime/Components/ShapeComponents.cs.meta b/UnityPackage/Runtime/Components/ShapeComponents.cs.meta new file mode 100644 index 0000000..8bfb4ff --- /dev/null +++ b/UnityPackage/Runtime/Components/ShapeComponents.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 0cd2935fe0e993d72a743acab83c92b0 diff --git a/UnityPackage/Runtime/Components/ShapeCounts.cs b/UnityPackage/Runtime/Components/ShapeCounts.cs new file mode 100644 index 0000000..335337c --- /dev/null +++ b/UnityPackage/Runtime/Components/ShapeCounts.cs @@ -0,0 +1,112 @@ +using System; +using UnityEngine; + +namespace reromanlee.Wireframes +{ + /// + /// The counts that shape components pass on to their runtime shapes, which fix them at creation: checks that throw + /// for scripts, and corrections to the nearest valid count for serialized fields. + /// + internal static class ShapeCounts + { + private const int MinSegmentCount = 3; + private const int MinQuarterSegmentCount = 4; + private const int MinPointCount = 3; + private const int MinSideCount = 3; + + private static readonly int[] SpikeCounts = { 4, 6, 8, 12, 20 }; + + /// The count is outside 3 to 1024. + internal static int CheckSegmentCount(int value) + { + if (value < MinSegmentCount || value > Ring.MaxSegmentCount) + { + throw new ArgumentOutOfRangeException( + nameof(value), value, $"A ring needs {MinSegmentCount} to {Ring.MaxSegmentCount} segments."); + } + return value; + } + + /// The count isn't a multiple of 4 from 4 to 1024. + internal static int CheckQuarterSegmentCount(int value) + { + if (value < MinQuarterSegmentCount || value % 4 != 0 || value > Ring.MaxSegmentCount) + { + throw new ArgumentOutOfRangeException( + nameof(value), + value, + $"This shape needs a multiple of 4 segments, from {MinQuarterSegmentCount} to {Ring.MaxSegmentCount}."); + } + return value; + } + + /// The count is outside 3 to 512. + internal static int CheckPointCount(int value) + { + if (value < MinPointCount || value > Star.MaxPointCount) + { + throw new ArgumentOutOfRangeException( + nameof(value), value, $"A star needs {MinPointCount} to {Star.MaxPointCount} points."); + } + return value; + } + + /// The count is outside 3 to 1024. + internal static int CheckSideCount(int value) + { + if (value < MinSideCount || value > Ring.MaxSegmentCount) + { + throw new ArgumentOutOfRangeException( + nameof(value), value, $"A frustum needs {MinSideCount} to {Ring.MaxSegmentCount} sides."); + } + return value; + } + + /// The count isn't 4, 6, 8, 12 or 20. + internal static int CheckSpikeCount(int value) + { + if (!PlatonicSolid.HasFaceCount(value)) + { + throw new ArgumentOutOfRangeException( + nameof(value), value, "A spiked sphere needs 4, 6, 8, 12 or 20 spikes."); + } + return value; + } + + internal static int ClampSegmentCount(int value) + { + return Mathf.Clamp(value, MinSegmentCount, Ring.MaxSegmentCount); + } + + /// The nearest multiple of 4 from 4 to 1024, rounding halfway counts up. + internal static int ClampQuarterSegmentCount(int value) + { + int clamped = Mathf.Clamp(value, MinQuarterSegmentCount, Ring.MaxSegmentCount); + return (clamped + 2) / 4 * 4; + } + + internal static int ClampPointCount(int value) + { + return Mathf.Clamp(value, MinPointCount, Star.MaxPointCount); + } + + internal static int ClampSideCount(int value) + { + return Mathf.Clamp(value, MinSideCount, Ring.MaxSegmentCount); + } + + /// The nearest spike count a spiked sphere can have, the smaller one when two are as near. + internal static int ClampSpikeCount(int value) + { + int nearest = SpikeCounts[0]; + for (int i = 1; i < SpikeCounts.Length; i++) + { + if (Math.Abs((long)SpikeCounts[i] - value) < Math.Abs((long)nearest - value)) + { + nearest = SpikeCounts[i]; + } + } + return nearest; + } + } +} diff --git a/UnityPackage/Runtime/Components/ShapeCounts.cs.meta b/UnityPackage/Runtime/Components/ShapeCounts.cs.meta new file mode 100644 index 0000000..ac925ff --- /dev/null +++ b/UnityPackage/Runtime/Components/ShapeCounts.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: cbf230fd03d451fd95b92c13b028117e diff --git a/UnityPackage/Runtime/Components/SharedContainer.cs b/UnityPackage/Runtime/Components/SharedContainer.cs new file mode 100644 index 0000000..1142045 --- /dev/null +++ b/UnityPackage/Runtime/Components/SharedContainer.cs @@ -0,0 +1,22 @@ +namespace reromanlee.Wireframes +{ + /// A container that shape components share, made for one . + internal sealed class SharedContainer + { + internal SharedContainer(SharedContainerKey key, WireframeContainer container) + { + Key = key; + Container = container; + } + + internal SharedContainerKey Key { get; } + + /// + /// The container, replaced by a new one when something else disposed it, such as a closing scene. + /// + internal WireframeContainer Container { get; set; } + + /// Components whose shape is in the container, or waits to go back into it. + internal int UserCount { get; set; } + } +} diff --git a/UnityPackage/Runtime/Components/SharedContainer.cs.meta b/UnityPackage/Runtime/Components/SharedContainer.cs.meta new file mode 100644 index 0000000..177a283 --- /dev/null +++ b/UnityPackage/Runtime/Components/SharedContainer.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: b78b946fd5dee8b944d5f34f1ed7b8de diff --git a/UnityPackage/Runtime/Components/SharedContainerKey.cs b/UnityPackage/Runtime/Components/SharedContainerKey.cs new file mode 100644 index 0000000..aa34741 --- /dev/null +++ b/UnityPackage/Runtime/Components/SharedContainerKey.cs @@ -0,0 +1,58 @@ +using System; +using UnityEngine.SceneManagement; + +namespace reromanlee.Wireframes +{ + /// + /// What decides which container a shape component draws with: the stage it is drawn in, the settings that a container + /// applies to all of its shapes, and whether they draw as gizmos. Components with equal keys share one container. + /// + internal readonly struct SharedContainerKey : IEquatable + { + /// + /// Scene of a stage of its own, such as a prefab open in Prefab Mode, or an invalid scene for the main stage. + /// + internal SharedContainerKey(Scene stage, WireframeOcclusion occlusion, bool usesAlpha, int layer, bool drawsAsGizmo) + { + Stage = stage; + Occlusion = occlusion; + UsesAlpha = usesAlpha; + Layer = layer; + DrawsAsGizmo = drawsAsGizmo; + } + + internal Scene Stage { get; } + + internal WireframeOcclusion Occlusion { get; } + + internal bool UsesAlpha { get; } + + internal int Layer { get; } + + internal bool DrawsAsGizmo { get; } + + public bool Equals(SharedContainerKey other) + { + return Stage == other.Stage && Occlusion == other.Occlusion && UsesAlpha == other.UsesAlpha + && Layer == other.Layer && DrawsAsGizmo == other.DrawsAsGizmo; + } + + public override bool Equals(object obj) + { + return obj is SharedContainerKey other && Equals(other); + } + + public override int GetHashCode() + { + unchecked + { + // Scene handles stopped being ints in Unity 6, while Scene hashes its handle in every version. + int hash = Stage.GetHashCode(); + hash = hash * 31 + (int)Occlusion; + hash = hash * 31 + (UsesAlpha ? 1 : 0); + hash = hash * 31 + Layer; + return hash * 31 + (DrawsAsGizmo ? 1 : 0); + } + } + } +} diff --git a/UnityPackage/Runtime/Components/SharedContainerKey.cs.meta b/UnityPackage/Runtime/Components/SharedContainerKey.cs.meta new file mode 100644 index 0000000..51d842c --- /dev/null +++ b/UnityPackage/Runtime/Components/SharedContainerKey.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: a9381a235703855fb45a32923d7b64be diff --git a/UnityPackage/Runtime/Components/SharedContainers.cs b/UnityPackage/Runtime/Components/SharedContainers.cs new file mode 100644 index 0000000..303413f --- /dev/null +++ b/UnityPackage/Runtime/Components/SharedContainers.cs @@ -0,0 +1,94 @@ +using System.Collections.Generic; +using UnityEngine; +using UnityEngine.SceneManagement; + +namespace reromanlee.Wireframes +{ + /// + /// The containers that shape components share: one per , created by the first + /// component that needs it and disposed once the last one lets go, and kept out of the Hierarchy. Containers of the + /// main stage persist across scenes, since their components can be in any open scene, while a stage of its own, + /// such as Prefab Mode, gets containers in its own scene, the only one its cameras draw. + /// + internal static class SharedContainers + { + private const string ContainerName = "Wireframe Components"; + + private static readonly Dictionary Containers = new(); + + /// Number of containers that components share right now. + internal static int Count + { + get => Containers.Count; + } + + /// + /// Returns the container for and counts the caller among its users, creating it first + /// when there is none or when something else disposed it. + /// + internal static SharedContainer Acquire(SharedContainerKey key) + { + if (!Containers.TryGetValue(key, out SharedContainer shared)) + { + shared = new SharedContainer(key, Create(key)); + Containers.Add(key, shared); + } + else if (shared.Container.IsDisposed) + { + // The other users notice their lost shapes on their next refresh and build them again. + shared.Container = Create(key); + } + shared.UserCount++; + return shared; + } + + /// + /// Stops counting a user of , and disposes it when that was the last one. + /// + internal static void Release(SharedContainer shared) + { + if (--shared.UserCount > 0) + { + return; + } + Containers.Remove(shared.Key); + shared.Container.Dispose(); + } + + /// + /// The stage that is drawn in: the scene of a stage of its own, such as a prefab open + /// in Prefab Mode, or an invalid scene for the main stage. + /// + internal static Scene StageOf(GameObject gameObject) + { +#if UNITY_EDITOR + Scene scene = gameObject.scene; + if (UnityEditor.SceneManagement.EditorSceneManager.IsPreviewScene(scene)) + { + return scene; + } +#endif + return default; + } + + private static WireframeContainer Create(SharedContainerKey key) + { + bool isMainStage = !key.Stage.IsValid(); + WireframeContainerSettings settings = new() + { + Name = ContainerName, + Occlusion = key.Occlusion, + UseAlpha = key.UsesAlpha, + Layer = key.Layer, + PersistAcrossScenes = isMainStage + }; + WireframeContainer container = new(settings, key.Stage, true); + container.Proxy.BonesDestroyed = ShapeComponents.RefreshAfterBonesDestroyed; + if (key.DrawsAsGizmo) + { + container.Proxy.CameraFilter = GizmoCameras.ShouldDraw; + } + return container; + } + } +} diff --git a/UnityPackage/Runtime/Components/SharedContainers.cs.meta b/UnityPackage/Runtime/Components/SharedContainers.cs.meta new file mode 100644 index 0000000..f6c68d1 --- /dev/null +++ b/UnityPackage/Runtime/Components/SharedContainers.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 2acc507ef481185f33317a794d7b6004 diff --git a/UnityPackage/Runtime/Components/WireframeAxialShape.cs b/UnityPackage/Runtime/Components/WireframeAxialShape.cs new file mode 100644 index 0000000..f9ac825 --- /dev/null +++ b/UnityPackage/Runtime/Components/WireframeAxialShape.cs @@ -0,0 +1,104 @@ +using UnityEngine; + +namespace reromanlee.Wireframes +{ + /// + /// Base of the long shape components: cylinders, cones, capsules, stadiums, frustums and pyramids. The shape runs from + /// to , both relative to its GameObject, and turns around that axis by + /// . With no roll, the shape's +Y stays as close to the GameObject's up as the axis allows, as when + /// a long shape is created from two points. + /// + public abstract class WireframeAxialShape : WireframeShape + { + [Tooltip("Where the shape's axis starts, relative to the GameObject: the tip of a cone or pyramid.")] + [SerializeField] private Vector3 _endA; + + [Tooltip("Where the shape's axis ends, relative to the GameObject: the center of a cone's or pyramid's base.")] + [SerializeField] private Vector3 _endB = new(0f, 0f, ShapeDefaults.Size); + + [Tooltip("Turn of the shape around its axis, in degrees.")] + [SerializeField] private float _roll; + + private protected WireframeAxialShape() + { + } + + /// + /// Where the shape's axis starts, relative to the GameObject, in its local units: the tip of a cone or pyramid, + /// and the center of a capsule's or stadium's first circle. The GameObject's origin by default. + /// + public Vector3 EndA + { + get => _endA; + set + { + _endA = value; + if (TryGetShape(out AxialShape shape)) + { + ApplyAxis(shape); + } + } + } + + /// + /// Where the shape's axis ends, relative to the GameObject, in its local units: the center of a cone's or + /// pyramid's base, and of a capsule's or stadium's second circle. 1 unit along +Z by default. + /// + public Vector3 EndB + { + get => _endB; + set + { + _endB = value; + if (TryGetShape(out AxialShape shape)) + { + ApplyAxis(shape); + } + } + } + + /// + /// Turn of the shape around its axis, in degrees. With no roll, the shape's +Y stays as close to the GameObject's + /// up as the axis allows. 0 by default. + /// + public float Roll + { + get => _roll; + set + { + _roll = value; + if (TryGetShape(out AxialShape shape)) + { + shape.LocalRotation = AxisRotation; + } + } + } + + /// Rotation that points the shape's +Z from end A to end B, then turns it by the roll. + private protected Quaternion AxisRotation + { + get => ShapeRotations.FromAxis(_endB - _endA, Vector3.up) * Quaternion.AngleAxis(_roll, Vector3.forward); + } + + private protected float AxisLength + { + get => Vector3.Distance(_endA, _endB); + } + + internal sealed override void ApplyTo(Shape shape) + { + ApplyAxis((AxialShape)shape); + ApplySizesTo(shape); + } + + /// Writes the shape's own fields, such as its radii, into . + internal abstract void ApplySizesTo(Shape shape); + + private void ApplyAxis(AxialShape shape) + { + shape.LocalPosition = _endA; + shape.LocalRotation = AxisRotation; + shape.Length = AxisLength; + } + } +} diff --git a/UnityPackage/Runtime/Components/WireframeAxialShape.cs.meta b/UnityPackage/Runtime/Components/WireframeAxialShape.cs.meta new file mode 100644 index 0000000..c83ff99 --- /dev/null +++ b/UnityPackage/Runtime/Components/WireframeAxialShape.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 9daeed1b06c9c8f48c84f394fae30624 diff --git a/UnityPackage/Runtime/Components/WireframeCenteredShape.cs b/UnityPackage/Runtime/Components/WireframeCenteredShape.cs new file mode 100644 index 0000000..a701032 --- /dev/null +++ b/UnityPackage/Runtime/Components/WireframeCenteredShape.cs @@ -0,0 +1,73 @@ +using UnityEngine; + +namespace reromanlee.Wireframes +{ + /// + /// Base of the shape components centered on a point: boxes, rectangles, circles, ellipses, stars, spheres, + /// ellipsoids and spiked spheres. and place the shape relative to its + /// GameObject, and flat shapes lie in the XZ plane of their rotation, so with no rotation they lie flat on it. + /// + public abstract class WireframeCenteredShape : WireframeShape + { + [Tooltip("Center of the shape relative to the GameObject.")] + [SerializeField] private Vector3 _center; + + [Tooltip("Rotation of the shape relative to the GameObject, in Euler angles.")] + [SerializeField] private Vector3 _rotation; + + private protected WireframeCenteredShape() + { + } + + /// Center of the shape relative to the GameObject, in its local units. + public Vector3 Center + { + get => _center; + set + { + _center = value; + if (TryGetShape(out RigidShape shape)) + { + shape.LocalPosition = value; + } + } + } + + /// + /// Rotation of the shape relative to the GameObject. It is kept as the Euler angles of , + /// and a zero quaternion such as default means no rotation. + /// + public Quaternion Rotation + { + get => Quaternion.Euler(_rotation); + set => EulerAngles = Quaternion.Normalize(value).eulerAngles; + } + + /// + /// Rotation of the shape relative to the GameObject in Euler angles, in degrees, as the Inspector shows it. + /// + public Vector3 EulerAngles + { + get => _rotation; + set + { + _rotation = value; + if (TryGetShape(out RigidShape shape)) + { + shape.LocalRotation = Quaternion.Euler(value); + } + } + } + + internal sealed override void ApplyTo(Shape shape) + { + RigidShape rigid = (RigidShape)shape; + rigid.LocalPosition = _center; + rigid.LocalRotation = Quaternion.Euler(_rotation); + ApplySizesTo(shape); + } + + /// Writes the shape's own fields, such as its sizes, into . + internal abstract void ApplySizesTo(Shape shape); + } +} diff --git a/UnityPackage/Runtime/Components/WireframeCenteredShape.cs.meta b/UnityPackage/Runtime/Components/WireframeCenteredShape.cs.meta new file mode 100644 index 0000000..93350ac --- /dev/null +++ b/UnityPackage/Runtime/Components/WireframeCenteredShape.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 6c797b6a734a070db839a6233e59e282 diff --git a/UnityPackage/Runtime/Components/WireframePointShape.cs b/UnityPackage/Runtime/Components/WireframePointShape.cs new file mode 100644 index 0000000..333e3a6 --- /dev/null +++ b/UnityPackage/Runtime/Components/WireframePointShape.cs @@ -0,0 +1,43 @@ +using UnityEngine; + +namespace reromanlee.Wireframes +{ + /// + /// Base of the shape components made of points: lines and polylines. Each point follows a bone of its own at a + /// position relative to it, or the GameObject when it has none. A bone that is destroyed counts as none, so its point + /// follows the GameObject from the next render on. + /// + public abstract class WireframePointShape : WireframeShape + { + private bool _hasReportedBone; + + private protected WireframePointShape() + { + } + + /// + /// The Transform a point follows: , or the GameObject's own when there is none, when it was + /// destroyed, or when it isn't in a scene, such as a prefab asset, which is reported once. + /// + private protected Transform ResolveBone(Transform bone) + { + if (bone == null) + { + return transform; + } + if (!bone.gameObject.scene.IsValid()) + { + if (!_hasReportedBone) + { + _hasReportedBone = true; + WireframesLog.Warning( + $"'{bone.name}' isn't in a scene, so a point of the {GetType().Name} on '{name}' follows its " + + "GameObject instead. Points can follow scene objects only, not prefab assets.", + this); + } + return transform; + } + return bone; + } + } +} diff --git a/UnityPackage/Runtime/Components/WireframePointShape.cs.meta b/UnityPackage/Runtime/Components/WireframePointShape.cs.meta new file mode 100644 index 0000000..d9f7379 --- /dev/null +++ b/UnityPackage/Runtime/Components/WireframePointShape.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: af46c9a99d20579bbb419c574b4af712 diff --git a/UnityPackage/Runtime/Components/WireframeShape.cs b/UnityPackage/Runtime/Components/WireframeShape.cs new file mode 100644 index 0000000..ccb2be3 --- /dev/null +++ b/UnityPackage/Runtime/Components/WireframeShape.cs @@ -0,0 +1,411 @@ +using System; +using UnityEngine; +using UnityEngine.SceneManagement; + +namespace reromanlee.Wireframes +{ + /// + /// Base of the components that draw a shape on their GameObject, the same in Edit Mode, Play Mode and builds. The + /// shape follows the GameObject's Transform like a mesh, and every change made to the component, in the Inspector, + /// by undo, animation or a script, shows up in the next render. Disabling the component or its GameObject hides the + /// shape, which then costs nothing, and enabling it again allocates nothing. + /// + /// + /// Components share containers, one per combination of , transparency, layer and + /// , so many shapes draw in a few draw calls. A shape draws on its GameObject's layer, and a + /// with alpha below 1 draws it transparent. Main thread only; shape components are made by the + /// package only. + /// + [ExecuteAlways] + [HelpURL(HelpUrl)] + public abstract class WireframeShape : MonoBehaviour + { + internal const string HelpUrl = "https://github.com/reromanlee/Wireframes/blob/main/Documentation/USAGE.md#components"; + + [Tooltip("Color of the shape's lines. Alpha below 1 draws them transparent.")] + [SerializeField] private Color _color = Color.white; + + [Tooltip("What is drawn of the lines that other geometry hides: nothing, everything, or a dimmer line.")] + [SerializeField] private WireframeOcclusion _occlusion = WireframeOcclusion.Hide; + + [Tooltip("Draws the shape like a gizmo: in the Scene view, and in the Game view only while its Gizmos button is on. " + + "Builds leave it out.")] + [SerializeField] private bool _drawAsGizmo; + + private Shape _shape; + private SharedContainer _container; + private int _builtSignature; + private bool _hasReportedProblem; + + private protected WireframeShape() + { + } + + /// Color of the shape's lines. Alpha below 1 draws them transparent. White by default. + public Color Color + { + get => _color; + set + { + // Crossing alpha 1 moves the shape between an opaque and a transparent container. + bool keepsContainer = (_color.a < 1f) == (value.a < 1f); + _color = value; + if (!keepsContainer) + { + Refresh(); + } + else if (TryGetShape(out Shape shape)) + { + shape.SetColor(value); + } + } + } + + /// + /// What is drawn of the lines that other geometry hides: nothing, everything, or a dimmer line. + /// by default. + /// + /// The value isn't a . + public WireframeOcclusion Occlusion + { + get => _occlusion; + set + { + if (!IsDefined(value)) + { + throw new ArgumentOutOfRangeException( + nameof(value), value, $"{nameof(Occlusion)} isn't a {nameof(WireframeOcclusion)} value."); + } + _occlusion = value; + Refresh(); + } + } + + /// + /// True draws the shape like a gizmo: in Scene views, and in the Game view only while its Gizmos button is on. Other + /// cameras never draw it, and builds create nothing for it. False, the default, draws it like any other object. + /// + public bool DrawAsGizmo + { + get => _drawAsGizmo; + set + { + _drawAsGizmo = value; + Refresh(); + } + } + + /// Position in the list of enabled components, or -1 while the component is disabled. + internal int EnabledIndex { get; set; } = -1; + + /// True while a refresh that OnValidate put off waits for the editor's next update. + internal bool IsDeferred { get; set; } + + /// True when the GameObject's layer changed since the shape went into its container. + internal bool IsOnStaleLayer + { + get => _container != null && _container.Key.Layer != gameObject.layer; + } + + /// The runtime shape that draws the component, or null when there is none yet. + internal Shape Shape + { + get => _shape; + } + + /// The container the shape is in or goes back into, or null when there is none yet. + internal SharedContainer SharedContainer + { + get => _container; + } + + /// + /// A number that changes whenever the fields need a new runtime shape, such as when a count that shapes fix at + /// creation changes. + /// + internal virtual int BuildSignature + { + get => 0; + } + + /// + /// False while the fields can't make a shape, such as a polyline with too few points; nothing is drawn then. + /// + internal virtual bool CanCreateShape + { + get => true; + } + + /// + /// False when nothing is drawn: for fields that can't make a shape, or for a gizmo outside the Editor. + /// + private bool IsDrawn + { +#if UNITY_EDITOR + get => CanCreateShape; +#else + get => !_drawAsGizmo && CanCreateShape; +#endif + } + + /// + /// Brings the shape in line with the fields while the component is enabled: creates it, moves it into another + /// container, creates it again after a count changed, or writes the fields into it. A disabled component does + /// nothing and catches up once it is enabled. A failure is logged once and leaves nothing drawn. + /// + internal void Refresh() + { + if (EnabledIndex < 0) + { + return; + } + try + { + RefreshEnabled(); + _hasReportedProblem = false; + ShapeComponents.RequestRepaint(); + } + catch (Exception exception) + { + DisposeShape(); + ReportProblem(exception); + } + } + + /// + /// Writes the fields into the drawn shape, for callbacks where creating and destroying objects is forbidden, such + /// as rendering. A refresh that needs more waits for the editor's next update, or for the next refresh in builds. + /// + internal void RefreshInPlace() + { + if (EnabledIndex < 0) + { + return; + } + if (!NeedsStructuralRefresh()) + { + ApplyFields(); + return; + } +#if UNITY_EDITOR + ShapeComponents.Defer(this); +#endif + } + + /// + /// Returns the drawn shape for an edit that can go straight into it. Otherwise refreshes, which catches up a + /// component that lost its shape and does nothing while it is disabled, and returns false. + /// + internal bool TryGetShape(out T shape) where T : Shape + { + if (_shape is T drawn && !drawn.IsDisposed && !drawn.IsSuspended) + { + shape = drawn; + ShapeComponents.RequestRepaint(); + return true; + } + shape = null; + Refresh(); + return false; + } + + /// + /// Creates the runtime shape from the fields, following , the GameObject's Transform. + /// + internal abstract Shape CreateShape(Transform bone); + + /// + /// Writes every field except the color into , made by . + /// + internal abstract void ApplyTo(Shape shape); + + /// + /// Brings fields that the runtime shape can't take, such as counts out of range, to their nearest valid value. + /// + internal virtual void Sanitize() + { + } + + private void OnEnable() + { + ShapeComponents.Add(this); + Refresh(); + } + + private void OnDisable() + { + ShapeComponents.Remove(this); + if (_shape != null && !_shape.IsDisposed && !_shape.IsSuspended) + { + _shape.Suspend(); + } + } + + private void OnDestroy() + { + DisposeShape(); + } + + private void OnValidate() + { + SanitizeFields(); + if (EnabledIndex < 0) + { + return; + } + // Unity forbids creating and destroying objects here, which moving the shape or creating it may do. + if (NeedsStructuralRefresh()) + { + ShapeComponents.Defer(this); + return; + } + ApplyFields(); + } + + private void OnDidApplyAnimationProperties() + { + if (EnabledIndex < 0) + { + return; + } + if (!NeedsStructuralRefresh()) + { + ApplyFields(); + return; + } +#if UNITY_EDITOR + // Previewing an animation in Edit Mode samples it where destroying objects may be forbidden. + if (!Application.isPlaying) + { + ShapeComponents.Defer(this); + return; + } +#endif + Refresh(); + } + + private void RefreshEnabled() + { + SanitizeFields(); + if (_shape != null && _shape.IsDisposed) + { + // Its container went away under it, as when its scene closed, so it is created again. + _shape = null; + } + if (!IsDrawn) + { + _shape?.Dispose(); + _shape = null; + return; + } + + SharedContainerKey key = CurrentKey(); + if (_container == null || !_container.Key.Equals(key) || _container.Container.IsDisposed) + { + // Acquired before the old one is released, so a container that both use is never disposed in between. + SharedContainer next = SharedContainers.Acquire(key); + if (_shape != null && !_shape.IsSuspended) + { + _shape.Suspend(); + } + if (_container != null) + { + SharedContainers.Release(_container); + } + _container = next; + } + + if (_shape != null && _builtSignature != BuildSignature) + { + _shape.Dispose(); + _shape = null; + } + MeshProxy proxy = _container.Container.Proxy; + if (_shape == null) + { + _builtSignature = BuildSignature; + _shape = proxy.Add(CreateShape(transform)); + } + else if (_shape.IsSuspended) + { + _shape.Resume(proxy); + } + _shape.SetColor(_color); + ApplyTo(_shape); + } + + /// + /// Writes the fields into the drawn shape, for changes that need neither a new shape nor another container. + /// + private void ApplyFields() + { + try + { + _shape.SetColor(_color); + ApplyTo(_shape); + ShapeComponents.RequestRepaint(); + } + catch (Exception exception) + { + ReportProblem(exception); +#if UNITY_EDITOR + // OnValidate forbids destroying objects, so the refresh that clears the shape away comes later. + ShapeComponents.Defer(this); +#else + DisposeShape(); +#endif + } + } + + /// True when the fields need more than writing them into the drawn shape. + private bool NeedsStructuralRefresh() + { + return _shape == null || _shape.IsDisposed || _shape.IsSuspended || !IsDrawn + || _container.Container.IsDisposed || !_container.Key.Equals(CurrentKey()) + || _builtSignature != BuildSignature; + } + + private SharedContainerKey CurrentKey() + { + Scene stage = SharedContainers.StageOf(gameObject); + return new SharedContainerKey(stage, _occlusion, _color.a < 1f, gameObject.layer, _drawAsGizmo); + } + + private void SanitizeFields() + { + if (!IsDefined(_occlusion)) + { + _occlusion = WireframeOcclusion.Hide; + } + Sanitize(); + } + + private void DisposeShape() + { + Shape shape = _shape; + SharedContainer container = _container; + _shape = null; + _container = null; + shape?.Dispose(); + if (container != null) + { + SharedContainers.Release(container); + } + } + + private void ReportProblem(Exception exception) + { + if (_hasReportedProblem) + { + return; + } + _hasReportedProblem = true; + WireframesLog.Error($"The {GetType().Name} on '{name}' failed to update, so it isn't drawn.", exception, this); + } + + private static bool IsDefined(WireframeOcclusion occlusion) + { + return occlusion >= WireframeOcclusion.Hide && occlusion <= WireframeOcclusion.Fade; + } + } +} diff --git a/UnityPackage/Runtime/Components/WireframeShape.cs.meta b/UnityPackage/Runtime/Components/WireframeShape.cs.meta new file mode 100644 index 0000000..9c64ba1 --- /dev/null +++ b/UnityPackage/Runtime/Components/WireframeShape.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 551eb88bb81fdfa9aaf213b86e99c5f9 diff --git a/UnityPackage/Runtime/Container/WireframeContainer.cs b/UnityPackage/Runtime/Container/WireframeContainer.cs index a9afe1e..af1c539 100644 --- a/UnityPackage/Runtime/Container/WireframeContainer.cs +++ b/UnityPackage/Runtime/Container/WireframeContainer.cs @@ -1,5 +1,6 @@ using System; using UnityEngine; +using UnityEngine.SceneManagement; using Object = UnityEngine.Object; namespace reromanlee.Wireframes @@ -36,7 +37,14 @@ public WireframeContainer() : this(null) /// settings are read once, so changing them later doesn't affect the container. /// /// A setting is outside its valid range. - public WireframeContainer(WireframeContainerSettings settings) + public WireframeContainer(WireframeContainerSettings settings) : this(settings, default, false) + { + } + + /// Creates a container for the package's own use, such as the ones that shape components share. + /// Scene the container belongs to, or an invalid scene for the active one. + /// True keeps the container's GameObjects out of the Hierarchy. + internal WireframeContainer(WireframeContainerSettings settings, Scene scene, bool isHiddenInHierarchy) { MainThread.Check(); settings ??= new WireframeContainerSettings(); @@ -44,11 +52,20 @@ public WireframeContainer(WireframeContainerSettings settings) // In Edit Mode it is kept out of the saved scene, which also keeps the scene from being marked as changed. HideFlags hideFlags = HideFlags.NotEditable | (Application.isPlaying ? HideFlags.None : HideFlags.DontSave); + if (isHiddenInHierarchy) + { + hideFlags |= HideFlags.HideInHierarchy; + } GameObject proxyObject = new(settings.ResolvedName) { hideFlags = hideFlags, layer = settings.Layer }; try { - // Otherwise the proxy lives in the active scene, and unloading that scene disposes the container. - if (settings.PersistAcrossScenes && Application.isPlaying) + // Without a scene of its own and unless it persists, the proxy lives in the active scene, and unloading + // that scene disposes the container. + if (scene.IsValid()) + { + SceneManager.MoveGameObjectToScene(proxyObject, scene); + } + else if (settings.PersistAcrossScenes && Application.isPlaying) { Object.DontDestroyOnLoad(proxyObject); } diff --git a/UnityPackage/Runtime/Core/PointShape.cs b/UnityPackage/Runtime/Core/PointShape.cs index faeefaf..b0a0b96 100644 --- a/UnityPackage/Runtime/Core/PointShape.cs +++ b/UnityPackage/Runtime/Core/PointShape.cs @@ -114,6 +114,11 @@ protected override void AcquireBones(BoneRegistry bones) for (int i = 0; i < VertexCount; i++) { ref ShapePoint point = ref Point(i); + // A bone destroyed while the shape was suspended left no pose to hold it by, so it resumes in world space. + if (IsDestroyed(point.Bone)) + { + point.Bone = null; + } point.BoneSlot = bones.Acquire(point.Bone); } } diff --git a/UnityPackage/Runtime/Core/RigidShape.cs b/UnityPackage/Runtime/Core/RigidShape.cs index 7a70914..6b4690e 100644 --- a/UnityPackage/Runtime/Core/RigidShape.cs +++ b/UnityPackage/Runtime/Core/RigidShape.cs @@ -160,6 +160,11 @@ internal sealed override void WriteBoneIndices(Span boneIndices) protected sealed override void AcquireBones(BoneRegistry bones) { + // A bone destroyed while the shape was suspended left no pose to hold it by, so it resumes in world space. + if (IsDestroyed(_bone)) + { + _bone = null; + } _boneSlot = bones.Acquire(_bone); } diff --git a/UnityPackage/Runtime/Core/Shape.cs b/UnityPackage/Runtime/Core/Shape.cs index 8103305..7330d9b 100644 --- a/UnityPackage/Runtime/Core/Shape.cs +++ b/UnityPackage/Runtime/Core/Shape.cs @@ -16,6 +16,7 @@ internal abstract class Shape : IShape, IEdgeOwner private IShapeHost _host; private DirtyFlags _dirty; private bool _isHidden; + private bool _isSuspended; protected Shape(int vertexCount, EdgeSource edgeSource) { @@ -25,7 +26,16 @@ protected Shape(int vertexCount, EdgeSource edgeSource) public bool IsDisposed { - get => _host == null; + get => _host == null && !_isSuspended; + } + + /// + /// True while the shape is out of every host after : neither disposed nor usable until + /// . + /// + internal bool IsSuspended + { + get => _isSuspended; } public bool IsVisible @@ -96,6 +106,13 @@ protected BoneRegistry Bones public void Dispose() { MainThread.Check(); + if (_isSuspended) + { + // Its host already took everything else back. + _isSuspended = false; + _edgeSource.Release(); + return; + } if (_host == null) { return; @@ -132,6 +149,43 @@ internal void Attach(MeshProxy proxy) MarkDirty(DirtyFlags.All); } + /// + /// Takes the shape out of its host without disposing it: the host takes back its vertices and edges and the + /// bones are released, while the shape keeps its state and edge pattern, so allocates + /// nothing to put it back. + /// + internal void Suspend() + { + IShapeHost host = _host; + host.Remove(this); + ReleaseBones(host.Bones); + // Removing it dropped its queue entry, so it has to be queued again wherever it lands. + _dirty = DirtyFlags.None; + _host = null; + _isSuspended = true; + } + + /// + /// Puts a suspended shape into , the one it left or another, and queues all of it to be + /// written. If that fails, it stays suspended. + /// + internal void Resume(MeshProxy proxy) + { + BoneRegistry bones = proxy.Bones; + AcquireBones(bones); + try + { + _host = proxy.Attach(this); + } + catch + { + ReleaseBones(bones); + throw; + } + _isSuspended = false; + MarkDirty(DirtyFlags.All); + } + /// /// Changes the shape's vertex count and edge pattern while it stays the same object, for shapes whose number of /// points changes. Derived shapes update their own points and bones first. Its host moves it where the new size diff --git a/UnityPackage/Runtime/Rendering/BoneRegistry.cs b/UnityPackage/Runtime/Rendering/BoneRegistry.cs index 3649df3..1294a7e 100644 --- a/UnityPackage/Runtime/Rendering/BoneRegistry.cs +++ b/UnityPackage/Runtime/Rendering/BoneRegistry.cs @@ -27,6 +27,7 @@ internal sealed class BoneRegistry private int[] _livePositions = new int[InitialCapacity]; private int _liveCount; private int _end = WorldSlot + 1; + private bool _hasDestroyedBones; internal BoneRegistry() { @@ -135,6 +136,7 @@ internal void ReadMatrices() if (bone == null) { StopReading(slot); + _hasDestroyedBones = true; continue; } Matrix4x4 matrix = bone.localToWorldMatrix; @@ -147,6 +149,14 @@ internal void ReadMatrices() } } + /// Returns whether reading the bones found any destroyed since the last call, and clears it. + internal bool TakeDestroyedBones() + { + bool hasDestroyedBones = _hasDestroyedBones; + _hasDestroyedBones = false; + return hasDestroyedBones; + } + private void StopReading(int slot) { int position = _livePositions[slot]; diff --git a/UnityPackage/Runtime/Rendering/ChunkAllocator.cs b/UnityPackage/Runtime/Rendering/ChunkAllocator.cs index 75a4628..0649b62 100644 --- a/UnityPackage/Runtime/Rendering/ChunkAllocator.cs +++ b/UnityPackage/Runtime/Rendering/ChunkAllocator.cs @@ -62,6 +62,12 @@ internal BoneRegistry Bones get => _bones; } + /// + /// True creates chunks with their renderers switched off, for a container whose camera filter decides when they + /// draw. + /// + internal bool HidesNewChunks { get; set; } + /// /// Creates chunks with room for vertices and edges, /// split evenly between as few chunks as that takes. They are never released. @@ -195,6 +201,7 @@ private MeshChunk CreateChunk(int vertexCapacity, int edgeCapacity) { MeshChunk chunk = new(this, vertexCapacity, edgeCapacity); chunk.Renderer.SetPropertyBlock(_propertyBlock); + chunk.Renderer.forceRenderingOff = HidesNewChunks; _chunks.Add(chunk); return chunk; } diff --git a/UnityPackage/Runtime/Rendering/MeshProxy.cs b/UnityPackage/Runtime/Rendering/MeshProxy.cs index a1f04c3..6d7dd1b 100644 --- a/UnityPackage/Runtime/Rendering/MeshProxy.cs +++ b/UnityPackage/Runtime/Rendering/MeshProxy.cs @@ -26,9 +26,11 @@ internal sealed class MeshProxy : MonoBehaviour private HeadlessHost _headless; private WireframeContainer _container; private Material[] _materials; + private Func _cameraFilter; private bool _ownsMaterials; private bool _isShutDown; private bool _hasLoggedFlushFailure; + private bool _hasLoggedFilterFailure; /// Makes new containers act as they do without a graphics device, so tests can cover server builds. internal static bool SimulateNoGraphics { get; set; } @@ -102,6 +104,43 @@ internal BoneTexture BoneTexture get => _boneTexture; } + /// + /// Called during a flush whose bones turned out to include destroyed ones, right after they are read, so the + /// shapes on them can be changed in time for the upload. Containers that shape components share use it. + /// + internal Action BonesDestroyed { get; set; } + + /// + /// Decides for each camera whether the chunks draw, for a container that only some cameras should draw, such as + /// one of gizmo shapes; null, the default, draws them for every camera. The chunks are switched off between + /// renders, and switched on only while a camera that the filter accepts renders. + /// + internal Func CameraFilter + { + get => _cameraFilter; + set + { + bool isFiltered = value != null; + if (isFiltered != (_cameraFilter != null) && isActiveAndEnabled) + { + if (isFiltered) + { + SubscribeToCameras(); + } + else + { + UnsubscribeFromCameras(); + } + } + _cameraFilter = value; + if (_chunks != null) + { + _chunks.HidesNewChunks = isFiltered; + } + SetChunksDrawn(!isFiltered); + } + } + internal void Initialize(WireframeContainer container, WireframeContainerSettings settings) { _container = container; @@ -190,6 +229,10 @@ internal void Flush() using (WireframesMarkers.Flush.Auto()) { _bones.ReadMatrices(); + if (_bones.TakeDestroyedBones()) + { + BonesDestroyed?.Invoke(this); + } if (_boneTexture.Upload(_bones)) { _chunks.SetBoneTexture(_boneTexture.Texture); @@ -272,12 +315,35 @@ private void OnEnable() // Each fires only under its own pipeline, so both stay subscribed and a pipeline switch needs no handling. RenderPipelineManager.beginContextRendering += OnBeginContextRendering; Camera.onPreCull += OnPreCullCamera; + if (_cameraFilter != null) + { + SubscribeToCameras(); + } } private void OnDisable() { RenderPipelineManager.beginContextRendering -= OnBeginContextRendering; Camera.onPreCull -= OnPreCullCamera; + UnsubscribeFromCameras(); + } + + /// + /// Subscribes to every camera's render, for the camera filter. The Built-in Render Pipeline goes through + /// and , a Scriptable one through these two. + /// + private void SubscribeToCameras() + { + RenderPipelineManager.beginCameraRendering += OnBeginCameraRendering; + RenderPipelineManager.endCameraRendering += OnEndCameraRendering; + Camera.onPostRender += OnPostRenderCamera; + } + + private void UnsubscribeFromCameras() + { + RenderPipelineManager.beginCameraRendering -= OnBeginCameraRendering; + RenderPipelineManager.endCameraRendering -= OnEndCameraRendering; + Camera.onPostRender -= OnPostRenderCamera; } private void OnDestroy() @@ -293,6 +359,60 @@ private void OnBeginContextRendering(ScriptableRenderContext context, List + /// Switches the chunks on for when the camera filter accepts it, and off otherwise. + /// + private void FilterChunksFor(Camera camera) + { + bool isDrawn = false; + try + { + isDrawn = !_isShutDown && _cameraFilter(camera); + } + catch (Exception exception) + { + // Like a failed flush, it must never escape into Unity's rendering nor flood the console. + if (!_hasLoggedFilterFailure) + { + _hasLoggedFilterFailure = true; + WireframesLog.Error("Deciding which cameras draw the wireframes failed.", exception, this); + } + } + SetChunksDrawn(isDrawn); + } + + private void SetChunksDrawn(bool isDrawn) + { + if (_chunks == null) + { + return; + } + IReadOnlyList chunks = _chunks.Chunks; + for (int i = 0; i < chunks.Count; i++) + { + chunks[i].Renderer.forceRenderingOff = !isDrawn; + } } private void FlushBeforeRendering() @@ -304,7 +424,8 @@ private void FlushBeforeRendering() try { WireframesCounters.Update(); - if (_chunks == null) + // Without chunks there is nothing to draw, as in a container whose shapes are all gone for now. + if (_chunks == null || _chunks.Chunks.Count == 0) { return; } diff --git a/UnityPackage/Runtime/Shapes/Box/WireframeBox.cs b/UnityPackage/Runtime/Shapes/Box/WireframeBox.cs new file mode 100644 index 0000000..ee13924 --- /dev/null +++ b/UnityPackage/Runtime/Shapes/Box/WireframeBox.cs @@ -0,0 +1,40 @@ +using UnityEngine; + +namespace reromanlee.Wireframes +{ + /// + /// Draws a box on its GameObject, centered on , like a BoxCollider. + /// + [AddComponentMenu("Wireframes/Box")] + [ExecuteAlways] + [HelpURL(HelpUrl)] + public sealed class WireframeBox : WireframeCenteredShape + { + [Tooltip("Size along the box's own axes, in the GameObject's local units.")] + [SerializeField] private Vector3 _size = Vector3.one * ShapeDefaults.Size; + + /// Size along the box's own axes, in the GameObject's local units. 1 by 1 by 1 by default. + public Vector3 Size + { + get => _size; + set + { + _size = value; + if (TryGetShape(out Box box)) + { + box.Size = value; + } + } + } + + internal override Shape CreateShape(Transform bone) + { + return new Box(bone, Center, Rotation, _size); + } + + internal override void ApplySizesTo(Shape shape) + { + ((Box)shape).Size = _size; + } + } +} diff --git a/UnityPackage/Runtime/Shapes/Box/WireframeBox.cs.meta b/UnityPackage/Runtime/Shapes/Box/WireframeBox.cs.meta new file mode 100644 index 0000000..d3564ff --- /dev/null +++ b/UnityPackage/Runtime/Shapes/Box/WireframeBox.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: f0beb658a12cfefdf628000edb88382b diff --git a/UnityPackage/Runtime/Shapes/Capsule/WireframeCapsule.cs b/UnityPackage/Runtime/Shapes/Capsule/WireframeCapsule.cs new file mode 100644 index 0000000..eb6a2d2 --- /dev/null +++ b/UnityPackage/Runtime/Shapes/Capsule/WireframeCapsule.cs @@ -0,0 +1,89 @@ +using System; +using UnityEngine; + +namespace reromanlee.Wireframes +{ + /// + /// Draws a capsule on its GameObject: two spheres, centered on the ends of its axis, wrapped together like the capsule + /// of Physics.CapsuleCast. Each end can have its own radius. + /// + [AddComponentMenu("Wireframes/Capsule")] + [ExecuteAlways] + [HelpURL(HelpUrl)] + public sealed class WireframeCapsule : WireframeAxialShape + { + [Tooltip("Radius of the sphere around end A, in the GameObject's local units.")] + [SerializeField] private float _radiusA = ShapeDefaults.Radius; + + [Tooltip("Radius of the sphere around end B, in the GameObject's local units.")] + [SerializeField] private float _radiusB = ShapeDefaults.Radius; + + [Tooltip("Number of straight pieces each ring is drawn with, a multiple of 4 from 4 to 1024.")] + [SerializeField, Delayed] private int _segmentCount = Ring.DefaultSegmentCount; + + /// Radius of the sphere around end A, in the GameObject's local units. 0.5 by default. + public float RadiusA + { + get => _radiusA; + set + { + _radiusA = value; + if (TryGetShape(out Capsule capsule)) + { + capsule.RadiusA = value; + } + } + } + + /// Radius of the sphere around end B, in the GameObject's local units. 0.5 by default. + public float RadiusB + { + get => _radiusB; + set + { + _radiusB = value; + if (TryGetShape(out Capsule capsule)) + { + capsule.RadiusB = value; + } + } + } + + /// + /// Number of straight pieces each ring is drawn with, a multiple of 4 from 4 to 1024; each arc over a cap is half + /// of them. 32 by default. Changing it creates the shape again. + /// + /// The count isn't a multiple of 4 from 4 to 1024. + public int SegmentCount + { + get => _segmentCount; + set + { + _segmentCount = ShapeCounts.CheckQuarterSegmentCount(value); + Refresh(); + } + } + + internal override int BuildSignature + { + get => _segmentCount; + } + + internal override Shape CreateShape(Transform bone) + { + return new Capsule(bone, EndA, AxisRotation, AxisLength, _radiusA, _radiusB, _segmentCount); + } + + internal override void ApplySizesTo(Shape shape) + { + Capsule capsule = (Capsule)shape; + capsule.RadiusA = _radiusA; + capsule.RadiusB = _radiusB; + } + + internal override void Sanitize() + { + _segmentCount = ShapeCounts.ClampQuarterSegmentCount(_segmentCount); + } + } +} diff --git a/UnityPackage/Runtime/Shapes/Capsule/WireframeCapsule.cs.meta b/UnityPackage/Runtime/Shapes/Capsule/WireframeCapsule.cs.meta new file mode 100644 index 0000000..d22d6b9 --- /dev/null +++ b/UnityPackage/Runtime/Shapes/Capsule/WireframeCapsule.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 49ecf785519c39d854b1e041d0b010d5 diff --git a/UnityPackage/Runtime/Shapes/Circle/WireframeCircle.cs b/UnityPackage/Runtime/Shapes/Circle/WireframeCircle.cs new file mode 100644 index 0000000..9443f8b --- /dev/null +++ b/UnityPackage/Runtime/Shapes/Circle/WireframeCircle.cs @@ -0,0 +1,70 @@ +using System; +using UnityEngine; + +namespace reromanlee.Wireframes +{ + /// + /// Draws a flat circle on its GameObject, centered on in the XZ plane of + /// its rotation. + /// + [AddComponentMenu("Wireframes/Circle")] + [ExecuteAlways] + [HelpURL(HelpUrl)] + public sealed class WireframeCircle : WireframeCenteredShape + { + [Tooltip("Radius in the GameObject's local units.")] + [SerializeField] private float _radius = ShapeDefaults.Radius; + + [Tooltip("Number of straight pieces the circle is drawn with, from 3 to 1024.")] + [SerializeField, Delayed] private int _segmentCount = Ring.DefaultSegmentCount; + + /// Radius in the GameObject's local units. 0.5 by default. + public float Radius + { + get => _radius; + set + { + _radius = value; + if (TryGetShape(out Circle circle)) + { + circle.Radius = value; + } + } + } + + /// + /// Number of straight pieces the circle is drawn with, from 3 to 1024. 32 by default. Changing it creates the + /// shape again. + /// + /// The count is outside 3 to 1024. + public int SegmentCount + { + get => _segmentCount; + set + { + _segmentCount = ShapeCounts.CheckSegmentCount(value); + Refresh(); + } + } + + internal override int BuildSignature + { + get => _segmentCount; + } + + internal override Shape CreateShape(Transform bone) + { + return new Circle(bone, Center, Rotation, _radius, _segmentCount); + } + + internal override void ApplySizesTo(Shape shape) + { + ((Circle)shape).Radius = _radius; + } + + internal override void Sanitize() + { + _segmentCount = ShapeCounts.ClampSegmentCount(_segmentCount); + } + } +} diff --git a/UnityPackage/Runtime/Shapes/Circle/WireframeCircle.cs.meta b/UnityPackage/Runtime/Shapes/Circle/WireframeCircle.cs.meta new file mode 100644 index 0000000..dce0ec4 --- /dev/null +++ b/UnityPackage/Runtime/Shapes/Circle/WireframeCircle.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 61424b534b5e3153b1a13495afcfb41c diff --git a/UnityPackage/Runtime/Shapes/Cone/WireframeCone.cs b/UnityPackage/Runtime/Shapes/Cone/WireframeCone.cs new file mode 100644 index 0000000..e074ec3 --- /dev/null +++ b/UnityPackage/Runtime/Shapes/Cone/WireframeCone.cs @@ -0,0 +1,70 @@ +using System; +using UnityEngine; + +namespace reromanlee.Wireframes +{ + /// + /// Draws a cone on its GameObject, with its tip at and the center of its base at + /// : a base ring joined to the tip by four lines. + /// + [AddComponentMenu("Wireframes/Cone")] + [ExecuteAlways] + [HelpURL(HelpUrl)] + public sealed class WireframeCone : WireframeAxialShape + { + [Tooltip("Radius of the base in the GameObject's local units.")] + [SerializeField] private float _radius = ShapeDefaults.Radius; + + [Tooltip("Number of straight pieces the base ring is drawn with, a multiple of 4 from 4 to 1024.")] + [SerializeField, Delayed] private int _segmentCount = Ring.DefaultSegmentCount; + + /// Radius of the base in the GameObject's local units. 0.5 by default. + public float Radius + { + get => _radius; + set + { + _radius = value; + if (TryGetShape(out Cone cone)) + { + cone.Radius = value; + } + } + } + + /// + /// Number of straight pieces the base ring is drawn with, a multiple of 4 from 4 to 1024. 32 by default. Changing + /// it creates the shape again. + /// + /// The count isn't a multiple of 4 from 4 to 1024. + public int SegmentCount + { + get => _segmentCount; + set + { + _segmentCount = ShapeCounts.CheckQuarterSegmentCount(value); + Refresh(); + } + } + + internal override int BuildSignature + { + get => _segmentCount; + } + + internal override Shape CreateShape(Transform bone) + { + return new Cone(bone, EndA, AxisRotation, AxisLength, _radius, _segmentCount); + } + + internal override void ApplySizesTo(Shape shape) + { + ((Cone)shape).Radius = _radius; + } + + internal override void Sanitize() + { + _segmentCount = ShapeCounts.ClampQuarterSegmentCount(_segmentCount); + } + } +} diff --git a/UnityPackage/Runtime/Shapes/Cone/WireframeCone.cs.meta b/UnityPackage/Runtime/Shapes/Cone/WireframeCone.cs.meta new file mode 100644 index 0000000..0654812 --- /dev/null +++ b/UnityPackage/Runtime/Shapes/Cone/WireframeCone.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 1cf8d35972784aa79ac78bbd1ee4678b diff --git a/UnityPackage/Runtime/Shapes/Cylinder/WireframeCylinder.cs b/UnityPackage/Runtime/Shapes/Cylinder/WireframeCylinder.cs new file mode 100644 index 0000000..c5288b4 --- /dev/null +++ b/UnityPackage/Runtime/Shapes/Cylinder/WireframeCylinder.cs @@ -0,0 +1,69 @@ +using System; +using UnityEngine; + +namespace reromanlee.Wireframes +{ + /// + /// Draws a cylinder on its GameObject: a ring around each end of its axis, joined by four lines. + /// + [AddComponentMenu("Wireframes/Cylinder")] + [ExecuteAlways] + [HelpURL(HelpUrl)] + public sealed class WireframeCylinder : WireframeAxialShape + { + [Tooltip("Radius in the GameObject's local units.")] + [SerializeField] private float _radius = ShapeDefaults.Radius; + + [Tooltip("Number of straight pieces each ring is drawn with, a multiple of 4 from 4 to 1024.")] + [SerializeField, Delayed] private int _segmentCount = Ring.DefaultSegmentCount; + + /// Radius in the GameObject's local units. 0.5 by default. + public float Radius + { + get => _radius; + set + { + _radius = value; + if (TryGetShape(out Cylinder cylinder)) + { + cylinder.Radius = value; + } + } + } + + /// + /// Number of straight pieces each ring is drawn with, a multiple of 4 from 4 to 1024. 32 by default. Changing it + /// creates the shape again. + /// + /// The count isn't a multiple of 4 from 4 to 1024. + public int SegmentCount + { + get => _segmentCount; + set + { + _segmentCount = ShapeCounts.CheckQuarterSegmentCount(value); + Refresh(); + } + } + + internal override int BuildSignature + { + get => _segmentCount; + } + + internal override Shape CreateShape(Transform bone) + { + return new Cylinder(bone, EndA, AxisRotation, AxisLength, _radius, _segmentCount); + } + + internal override void ApplySizesTo(Shape shape) + { + ((Cylinder)shape).Radius = _radius; + } + + internal override void Sanitize() + { + _segmentCount = ShapeCounts.ClampQuarterSegmentCount(_segmentCount); + } + } +} diff --git a/UnityPackage/Runtime/Shapes/Cylinder/WireframeCylinder.cs.meta b/UnityPackage/Runtime/Shapes/Cylinder/WireframeCylinder.cs.meta new file mode 100644 index 0000000..449c831 --- /dev/null +++ b/UnityPackage/Runtime/Shapes/Cylinder/WireframeCylinder.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: f0dec41d539947209f6d3650d88fb97a diff --git a/UnityPackage/Runtime/Shapes/Ellipse/WireframeEllipse.cs b/UnityPackage/Runtime/Shapes/Ellipse/WireframeEllipse.cs new file mode 100644 index 0000000..f8c977b --- /dev/null +++ b/UnityPackage/Runtime/Shapes/Ellipse/WireframeEllipse.cs @@ -0,0 +1,73 @@ +using System; +using UnityEngine; + +namespace reromanlee.Wireframes +{ + /// + /// Draws a flat ellipse on its GameObject, centered on in the XZ plane of + /// its rotation. + /// + [AddComponentMenu("Wireframes/Ellipse")] + [ExecuteAlways] + [HelpURL(HelpUrl)] + public sealed class WireframeEllipse : WireframeCenteredShape + { + [Tooltip("Radius along the ellipse's X axis and along its Z axis, in the GameObject's local units.")] + [SerializeField] private Vector2 _radii = new(ShapeDefaults.Radius * 0.5f, ShapeDefaults.Radius); + + [Tooltip("Number of straight pieces the ellipse is drawn with, from 3 to 1024.")] + [SerializeField, Delayed] private int _segmentCount = Ring.DefaultSegmentCount; + + /// + /// Semi-axes in the GameObject's local units: x is the radius along the ellipse's X axis and y along its Z axis. + /// 0.25 by 0.5 by default. + /// + public Vector2 Radii + { + get => _radii; + set + { + _radii = value; + if (TryGetShape(out Ellipse ellipse)) + { + ellipse.Radii = value; + } + } + } + + /// + /// Number of straight pieces the ellipse is drawn with, from 3 to 1024. 32 by default. Changing it creates the + /// shape again. + /// + /// The count is outside 3 to 1024. + public int SegmentCount + { + get => _segmentCount; + set + { + _segmentCount = ShapeCounts.CheckSegmentCount(value); + Refresh(); + } + } + + internal override int BuildSignature + { + get => _segmentCount; + } + + internal override Shape CreateShape(Transform bone) + { + return new Ellipse(bone, Center, Rotation, _radii, _segmentCount); + } + + internal override void ApplySizesTo(Shape shape) + { + ((Ellipse)shape).Radii = _radii; + } + + internal override void Sanitize() + { + _segmentCount = ShapeCounts.ClampSegmentCount(_segmentCount); + } + } +} diff --git a/UnityPackage/Runtime/Shapes/Ellipse/WireframeEllipse.cs.meta b/UnityPackage/Runtime/Shapes/Ellipse/WireframeEllipse.cs.meta new file mode 100644 index 0000000..339b280 --- /dev/null +++ b/UnityPackage/Runtime/Shapes/Ellipse/WireframeEllipse.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: dac5b0a1e62a5b6ef3e3f8ab92b2e8d6 diff --git a/UnityPackage/Runtime/Shapes/Ellipsoid/WireframeEllipsoid.cs b/UnityPackage/Runtime/Shapes/Ellipsoid/WireframeEllipsoid.cs new file mode 100644 index 0000000..4400c01 --- /dev/null +++ b/UnityPackage/Runtime/Shapes/Ellipsoid/WireframeEllipsoid.cs @@ -0,0 +1,74 @@ +using System; +using UnityEngine; + +namespace reromanlee.Wireframes +{ + /// + /// Draws an ellipsoid on its GameObject as three ellipses around , one in + /// each plane of its axes. + /// + [AddComponentMenu("Wireframes/Ellipsoid")] + [ExecuteAlways] + [HelpURL(HelpUrl)] + public sealed class WireframeEllipsoid : WireframeCenteredShape + { + [Tooltip("Radius along the ellipsoid's X, Y and Z axes, in the GameObject's local units.")] + [SerializeField] private Vector3 _radii = + new(ShapeDefaults.Radius * 0.5f, ShapeDefaults.Radius * 0.5f, ShapeDefaults.Radius); + + [Tooltip("Number of straight pieces each ellipse is drawn with, from 3 to 1024.")] + [SerializeField, Delayed] private int _segmentCount = Ring.DefaultSegmentCount; + + /// + /// Semi-axes along the ellipsoid's X, Y and Z axes, in the GameObject's local units. 0.25, 0.25 and 0.5 by + /// default. + /// + public Vector3 Radii + { + get => _radii; + set + { + _radii = value; + if (TryGetShape(out Ellipsoid ellipsoid)) + { + ellipsoid.Radii = value; + } + } + } + + /// + /// Number of straight pieces each ellipse is drawn with, from 3 to 1024. 32 by default. Changing it creates the + /// shape again. + /// + /// The count is outside 3 to 1024. + public int SegmentCount + { + get => _segmentCount; + set + { + _segmentCount = ShapeCounts.CheckSegmentCount(value); + Refresh(); + } + } + + internal override int BuildSignature + { + get => _segmentCount; + } + + internal override Shape CreateShape(Transform bone) + { + return new Ellipsoid(bone, Center, Rotation, _radii, _segmentCount); + } + + internal override void ApplySizesTo(Shape shape) + { + ((Ellipsoid)shape).Radii = _radii; + } + + internal override void Sanitize() + { + _segmentCount = ShapeCounts.ClampSegmentCount(_segmentCount); + } + } +} diff --git a/UnityPackage/Runtime/Shapes/Ellipsoid/WireframeEllipsoid.cs.meta b/UnityPackage/Runtime/Shapes/Ellipsoid/WireframeEllipsoid.cs.meta new file mode 100644 index 0000000..64fa7b1 --- /dev/null +++ b/UnityPackage/Runtime/Shapes/Ellipsoid/WireframeEllipsoid.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: afb73b3f8e393ab7277248a5f6539959 diff --git a/UnityPackage/Runtime/Shapes/Frustum/FrustumFactory.cs b/UnityPackage/Runtime/Shapes/Frustum/FrustumFactory.cs index 5146621..ed3a535 100644 --- a/UnityPackage/Runtime/Shapes/Frustum/FrustumFactory.cs +++ b/UnityPackage/Runtime/Shapes/Frustum/FrustumFactory.cs @@ -5,7 +5,7 @@ namespace reromanlee.Wireframes /// Creates frustums, prisms and regular pyramids in a . public static class FrustumFactory { - private const int DefaultSideCount = 4; + internal const int DefaultSideCount = 4; /// /// Creates a white square frustum from the world origin, 1 unit along +Z, with corners 0.25 out at end A and diff --git a/UnityPackage/Runtime/Shapes/Frustum/WireframeFrustum.cs b/UnityPackage/Runtime/Shapes/Frustum/WireframeFrustum.cs new file mode 100644 index 0000000..5f86109 --- /dev/null +++ b/UnityPackage/Runtime/Shapes/Frustum/WireframeFrustum.cs @@ -0,0 +1,92 @@ +using System; +using UnityEngine; + +namespace reromanlee.Wireframes +{ + /// + /// Draws a frustum on its GameObject: two regular polygons around the ends of its axis, joined at every corner. Prisms + /// and regular pyramids are frustums too. + /// + [AddComponentMenu("Wireframes/Frustum")] + [ExecuteAlways] + [HelpURL(HelpUrl)] + public sealed class WireframeFrustum : WireframeAxialShape + { + [Tooltip("Distance from end A to the corners of its polygon, in the GameObject's local units.")] + [SerializeField] private float _radiusA = ShapeDefaults.Radius * 0.5f; + + [Tooltip("Distance from end B to the corners of its polygon, in the GameObject's local units.")] + [SerializeField] private float _radiusB = ShapeDefaults.Radius; + + [Tooltip("Number of sides of each polygon, from 3 to 1024.")] + [SerializeField, Delayed] private int _sideCount = FrustumFactory.DefaultSideCount; + + /// + /// Distance from end A to the corners of its polygon, in the GameObject's local units. 0.25 by default. + /// + public float RadiusA + { + get => _radiusA; + set + { + _radiusA = value; + if (TryGetShape(out Frustum frustum)) + { + frustum.RadiusA = value; + } + } + } + + /// + /// Distance from end B to the corners of its polygon, in the GameObject's local units. 0.5 by default. + /// + public float RadiusB + { + get => _radiusB; + set + { + _radiusB = value; + if (TryGetShape(out Frustum frustum)) + { + frustum.RadiusB = value; + } + } + } + + /// + /// Number of sides of each polygon, from 3 to 1024. 4 by default. Changing it creates the shape again. + /// + /// The count is outside 3 to 1024. + public int SideCount + { + get => _sideCount; + set + { + _sideCount = ShapeCounts.CheckSideCount(value); + Refresh(); + } + } + + internal override int BuildSignature + { + get => _sideCount; + } + + internal override Shape CreateShape(Transform bone) + { + return new Frustum(bone, EndA, AxisRotation, AxisLength, _radiusA, _radiusB, _sideCount); + } + + internal override void ApplySizesTo(Shape shape) + { + Frustum frustum = (Frustum)shape; + frustum.RadiusA = _radiusA; + frustum.RadiusB = _radiusB; + } + + internal override void Sanitize() + { + _sideCount = ShapeCounts.ClampSideCount(_sideCount); + } + } +} diff --git a/UnityPackage/Runtime/Shapes/Frustum/WireframeFrustum.cs.meta b/UnityPackage/Runtime/Shapes/Frustum/WireframeFrustum.cs.meta new file mode 100644 index 0000000..fa0cf42 --- /dev/null +++ b/UnityPackage/Runtime/Shapes/Frustum/WireframeFrustum.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 7fcdf95a81a03b7c449f86cdfddc82d4 diff --git a/UnityPackage/Runtime/Shapes/Line/WireframeLine.cs b/UnityPackage/Runtime/Shapes/Line/WireframeLine.cs new file mode 100644 index 0000000..fff490a --- /dev/null +++ b/UnityPackage/Runtime/Shapes/Line/WireframeLine.cs @@ -0,0 +1,109 @@ +using UnityEngine; + +namespace reromanlee.Wireframes +{ + /// + /// Draws a line on its GameObject between two points, each following its own bone, or the GameObject when it has none. + /// By default it runs from the GameObject's origin to 1 unit along its +Z. + /// + [AddComponentMenu("Wireframes/Line")] + [ExecuteAlways] + [HelpURL(HelpUrl)] + public sealed class WireframeLine : WireframePointShape + { + [Tooltip("Transform that point A follows. None follows the GameObject.")] + [SerializeField] private Transform _boneA; + + [Tooltip("Point A relative to its bone, or to the GameObject without one.")] + [SerializeField] private Vector3 _positionA; + + [Tooltip("Transform that point B follows. None follows the GameObject.")] + [SerializeField] private Transform _boneB; + + [Tooltip("Point B relative to its bone, or to the GameObject without one.")] + [SerializeField] private Vector3 _positionB = new(0f, 0f, ShapeDefaults.Size); + + /// + /// Transform that point A follows, or null to follow the GameObject. A destroyed bone counts as none, and so does + /// one that isn't in a scene, such as a prefab asset. + /// + public Transform BoneA + { + get => _boneA; + set + { + _boneA = value; + if (TryGetShape(out Line line)) + { + line.BoneA = ResolveBone(value); + line.LocalPositionA = _positionA; + } + } + } + + /// + /// Point A relative to , or to the GameObject without one. The origin by default. + /// + public Vector3 PositionA + { + get => _positionA; + set + { + _positionA = value; + if (TryGetShape(out Line line)) + { + line.LocalPositionA = value; + } + } + } + + /// + /// Transform that point B follows, or null to follow the GameObject. A destroyed bone counts as none, and so does + /// one that isn't in a scene, such as a prefab asset. + /// + public Transform BoneB + { + get => _boneB; + set + { + _boneB = value; + if (TryGetShape(out Line line)) + { + line.BoneB = ResolveBone(value); + line.LocalPositionB = _positionB; + } + } + } + + /// + /// Point B relative to , or to the GameObject without one. 1 unit along +Z by default. + /// + public Vector3 PositionB + { + get => _positionB; + set + { + _positionB = value; + if (TryGetShape(out Line line)) + { + line.LocalPositionB = value; + } + } + } + + internal override Shape CreateShape(Transform bone) + { + return new Line(ResolveBone(_boneA), ResolveBone(_boneB)); + } + + internal override void ApplyTo(Shape shape) + { + // Each bone goes first, since changing it keeps the point's world position rather than its local one. + Line line = (Line)shape; + line.BoneA = ResolveBone(_boneA); + line.LocalPositionA = _positionA; + line.BoneB = ResolveBone(_boneB); + line.LocalPositionB = _positionB; + } + } +} diff --git a/UnityPackage/Runtime/Shapes/Line/WireframeLine.cs.meta b/UnityPackage/Runtime/Shapes/Line/WireframeLine.cs.meta new file mode 100644 index 0000000..ecb8497 --- /dev/null +++ b/UnityPackage/Runtime/Shapes/Line/WireframeLine.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: e25f2c254941571aae80be93269e2674 diff --git a/UnityPackage/Runtime/Shapes/Polyline/Polyline.cs b/UnityPackage/Runtime/Shapes/Polyline/Polyline.cs index 94fbeb5..9e8ea2e 100644 --- a/UnityPackage/Runtime/Shapes/Polyline/Polyline.cs +++ b/UnityPackage/Runtime/Shapes/Polyline/Polyline.cs @@ -20,6 +20,18 @@ internal Polyline(IReadOnlyList points, bool isClosed) } } + /// Creates a polyline of white points at the world origin. + internal Polyline(int pointCount, bool isClosed) + : base(CheckCount(pointCount, isClosed, nameof(pointCount)), BuildEdges(pointCount, isClosed)) + { + _isClosed = isClosed; + _points = new ShapePoint[pointCount]; + for (int i = 0; i < _points.Length; i++) + { + _points[i] = new ShapePoint(Vector3.zero); + } + } + /// Creates a polyline whose points sit at the origins of their bones. internal Polyline(IReadOnlyList bones, bool isClosed) : base(CheckCount(bones, isClosed, nameof(bones)), BuildEdges(bones.Count, isClosed)) @@ -66,13 +78,18 @@ private static int CheckCount(IReadOnlyList items, bool isClosed, string p { throw new ArgumentNullException(parameterName); } + return CheckCount(items.Count, isClosed, parameterName); + } + + private static int CheckCount(int pointCount, bool isClosed, string parameterName) + { int minimum = isClosed ? 3 : 2; - if (items.Count < minimum) + if (pointCount < minimum) { throw new ArgumentException( $"{(isClosed ? "A closed" : "An open")} polyline needs at least {minimum} points.", parameterName); } - return items.Count; + return pointCount; } private static int[] BuildEdges(int pointCount, bool isClosed) diff --git a/UnityPackage/Runtime/Shapes/Polyline/WireframePolyline.cs b/UnityPackage/Runtime/Shapes/Polyline/WireframePolyline.cs new file mode 100644 index 0000000..e48b023 --- /dev/null +++ b/UnityPackage/Runtime/Shapes/Polyline/WireframePolyline.cs @@ -0,0 +1,170 @@ +using System; +using UnityEngine; + +namespace reromanlee.Wireframes +{ + /// + /// Draws a polyline on its GameObject: points joined in order, each following its own bone, or the GameObject when it + /// has none. Closed, it joins the last point back to the first, as in a polygon or a triangle. It draws nothing with + /// fewer than 2 points, or 3 when closed. By default it is a small triangle lying flat on the GameObject. + /// + [AddComponentMenu("Wireframes/Polyline")] + [ExecuteAlways] + [HelpURL(HelpUrl)] + public sealed class WireframePolyline : WireframePointShape + { + private const float DefaultRadius = 0.5f; + + [Tooltip("Points in order, each relative to its bone, or to the GameObject without one.")] + [SerializeField] private PolylinePoint[] _points = TrianglePoints(); + + [Tooltip("Joins the last point back to the first, as in a polygon.")] + [SerializeField] private bool _isClosed = true; + + /// + /// Number of points. Raising it adds points at the GameObject's origin, and lowering it drops the last ones. With + /// fewer than 2, or 3 when closed, nothing is drawn. Changing it creates the shape again. + /// + /// The count is negative. + public int PointCount + { + get => _points.Length; + set + { + if (value < 0) + { + throw new ArgumentOutOfRangeException( + nameof(value), value, "A polyline can't have a negative number of points."); + } + Array.Resize(ref _points, value); + Refresh(); + } + } + + /// + /// True joins the last point back to the first, as in a polygon. True by default. Changing it creates the shape + /// again. + /// + public bool IsClosed + { + get => _isClosed; + set + { + _isClosed = value; + Refresh(); + } + } + + internal override int BuildSignature + { + get => _points.Length * 2 + (_isClosed ? 1 : 0); + } + + internal override bool CanCreateShape + { + get => _points.Length >= (_isClosed ? 3 : 2); + } + + /// + /// Returns the Transform that point follows, or null when it follows the GameObject. + /// + /// The index is outside the points. + public Transform GetBone(int index) + { + return Point(index).Bone; + } + + /// + /// Sets the Transform that point follows, or null to follow the GameObject. A destroyed bone + /// counts as none, and so does one that isn't in a scene, such as a prefab asset. + /// + /// The index is outside the points. + public void SetBone(int index, Transform bone) + { + ref PolylinePoint point = ref Point(index); + point.Bone = bone; + if (TryGetShape(out Polyline polyline)) + { + polyline.SetBone(index, ResolveBone(bone)); + polyline.SetLocalPosition(index, point.Position); + } + } + + /// + /// Returns point relative to its bone, or to the GameObject without one. + /// + /// The index is outside the points. + public Vector3 GetPosition(int index) + { + return Point(index).Position; + } + + /// + /// Sets point relative to its bone, or to the GameObject without one. + /// + /// The index is outside the points. + public void SetPosition(int index, Vector3 position) + { + Point(index).Position = position; + if (TryGetShape(out Polyline polyline)) + { + polyline.SetLocalPosition(index, position); + } + } + + internal override Shape CreateShape(Transform bone) + { + return new Polyline(_points.Length, _isClosed); + } + + internal override void ApplyTo(Shape shape) + { + // Each bone goes first, since changing it keeps the point's world position rather than its local one. + Polyline polyline = (Polyline)shape; + for (int i = 0; i < _points.Length; i++) + { + polyline.SetBone(i, ResolveBone(_points[i].Bone)); + polyline.SetLocalPosition(i, _points[i].Position); + } + } + + internal override void Sanitize() + { + _points ??= Array.Empty(); + } + + private ref PolylinePoint Point(int index) + { + if ((uint)index >= (uint)_points.Length) + { + throw new ArgumentOutOfRangeException(nameof(index), index, $"The polyline has {_points.Length} points."); + } + return ref _points[index]; + } + + /// + /// An equilateral triangle lying flat around the GameObject's origin, with a corner on its +Z axis. + /// + private static PolylinePoint[] TrianglePoints() + { + PolylinePoint[] points = new PolylinePoint[3]; + CirclePoints circle = new(points.Length); + for (int i = 0; i < points.Length; i++) + { + Vector2 point = circle.Next(); + points[i].Position = new Vector3(point.y, 0f, point.x) * DefaultRadius; + } + return points; + } + + [Serializable] + private struct PolylinePoint + { + [Tooltip("Transform the point follows. None follows the GameObject.")] + public Transform Bone; + + [Tooltip("Position relative to the bone, or to the GameObject without one.")] + public Vector3 Position; + } + } +} diff --git a/UnityPackage/Runtime/Shapes/Polyline/WireframePolyline.cs.meta b/UnityPackage/Runtime/Shapes/Polyline/WireframePolyline.cs.meta new file mode 100644 index 0000000..141bf34 --- /dev/null +++ b/UnityPackage/Runtime/Shapes/Polyline/WireframePolyline.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 538e232bcc792e0b2a677ed0f42b2db8 diff --git a/UnityPackage/Runtime/Shapes/Pyramid/WireframePyramid.cs b/UnityPackage/Runtime/Shapes/Pyramid/WireframePyramid.cs new file mode 100644 index 0000000..7efa9ac --- /dev/null +++ b/UnityPackage/Runtime/Shapes/Pyramid/WireframePyramid.cs @@ -0,0 +1,44 @@ +using UnityEngine; + +namespace reromanlee.Wireframes +{ + /// + /// Draws a pyramid on its GameObject, with its tip at and the center of its + /// rectangular base at , like a camera's view without its near plane. + /// + [AddComponentMenu("Wireframes/Pyramid")] + [ExecuteAlways] + [HelpURL(HelpUrl)] + public sealed class WireframePyramid : WireframeAxialShape + { + [Tooltip("Width of the base along the pyramid's X axis and height along its Y axis, in local units.")] + [SerializeField] private Vector2 _baseSize = Vector2.one * ShapeDefaults.Size; + + /// + /// Width of the base along the pyramid's X axis and height along its Y axis, in the GameObject's local units. 1 by + /// 1 by default. + /// + public Vector2 BaseSize + { + get => _baseSize; + set + { + _baseSize = value; + if (TryGetShape(out Pyramid pyramid)) + { + pyramid.BaseSize = value; + } + } + } + + internal override Shape CreateShape(Transform bone) + { + return new Pyramid(bone, EndA, AxisRotation, AxisLength, _baseSize); + } + + internal override void ApplySizesTo(Shape shape) + { + ((Pyramid)shape).BaseSize = _baseSize; + } + } +} diff --git a/UnityPackage/Runtime/Shapes/Pyramid/WireframePyramid.cs.meta b/UnityPackage/Runtime/Shapes/Pyramid/WireframePyramid.cs.meta new file mode 100644 index 0000000..483851b --- /dev/null +++ b/UnityPackage/Runtime/Shapes/Pyramid/WireframePyramid.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 7e298f4cb2b2208ba842d77775fad949 diff --git a/UnityPackage/Runtime/Shapes/Rectangle/WireframeRectangle.cs b/UnityPackage/Runtime/Shapes/Rectangle/WireframeRectangle.cs new file mode 100644 index 0000000..7d81ab9 --- /dev/null +++ b/UnityPackage/Runtime/Shapes/Rectangle/WireframeRectangle.cs @@ -0,0 +1,44 @@ +using UnityEngine; + +namespace reromanlee.Wireframes +{ + /// + /// Draws a flat rectangle on its GameObject, centered on in the XZ plane of + /// its rotation. + /// + [AddComponentMenu("Wireframes/Rectangle")] + [ExecuteAlways] + [HelpURL(HelpUrl)] + public sealed class WireframeRectangle : WireframeCenteredShape + { + [Tooltip("Width along the rectangle's X axis and depth along its Z axis, in the GameObject's local units.")] + [SerializeField] private Vector2 _size = Vector2.one * ShapeDefaults.Size; + + /// + /// Width along the rectangle's X axis and depth along its Z axis, in the GameObject's local units. 1 by 1 by + /// default. + /// + public Vector2 Size + { + get => _size; + set + { + _size = value; + if (TryGetShape(out Rectangle rectangle)) + { + rectangle.Size = value; + } + } + } + + internal override Shape CreateShape(Transform bone) + { + return new Rectangle(bone, Center, Rotation, _size); + } + + internal override void ApplySizesTo(Shape shape) + { + ((Rectangle)shape).Size = _size; + } + } +} diff --git a/UnityPackage/Runtime/Shapes/Rectangle/WireframeRectangle.cs.meta b/UnityPackage/Runtime/Shapes/Rectangle/WireframeRectangle.cs.meta new file mode 100644 index 0000000..9583cdc --- /dev/null +++ b/UnityPackage/Runtime/Shapes/Rectangle/WireframeRectangle.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 8832ff90d3a928029358050ead70ec40 diff --git a/UnityPackage/Runtime/Shapes/RoundedRectangle/RoundedRectangleFactory.cs b/UnityPackage/Runtime/Shapes/RoundedRectangle/RoundedRectangleFactory.cs index 8dc2def..46b06f7 100644 --- a/UnityPackage/Runtime/Shapes/RoundedRectangle/RoundedRectangleFactory.cs +++ b/UnityPackage/Runtime/Shapes/RoundedRectangle/RoundedRectangleFactory.cs @@ -5,7 +5,7 @@ namespace reromanlee.Wireframes /// Creates rounded rectangles in a . public static class RoundedRectangleFactory { - private const float DefaultCornerRadius = 0.25f; + internal const float DefaultCornerRadius = 0.25f; /// /// Creates a white 1 by 1 rectangle with corners of radius 0.25 around the world origin, lying flat in the XZ diff --git a/UnityPackage/Runtime/Shapes/RoundedRectangle/WireframeRoundedRectangle.cs b/UnityPackage/Runtime/Shapes/RoundedRectangle/WireframeRoundedRectangle.cs new file mode 100644 index 0000000..6efb358 --- /dev/null +++ b/UnityPackage/Runtime/Shapes/RoundedRectangle/WireframeRoundedRectangle.cs @@ -0,0 +1,95 @@ +using System; +using UnityEngine; + +namespace reromanlee.Wireframes +{ + /// + /// Draws a flat rectangle with round corners on its GameObject, centered on + /// in the XZ plane of its rotation. + /// + [AddComponentMenu("Wireframes/Rounded Rectangle")] + [ExecuteAlways] + [HelpURL(HelpUrl)] + public sealed class WireframeRoundedRectangle : WireframeCenteredShape + { + [Tooltip("Width along the rectangle's X axis and depth along its Z axis, in the GameObject's local units.")] + [SerializeField] private Vector2 _size = Vector2.one * ShapeDefaults.Size; + + [Tooltip("Radius of the corners, drawn between 0 and half the shorter side.")] + [SerializeField] private float _cornerRadius = RoundedRectangleFactory.DefaultCornerRadius; + + [Tooltip("Number of straight pieces a full circle of the corners is drawn with, a multiple of 4 from 4 to 1024.")] + [SerializeField, Delayed] private int _segmentCount = Ring.DefaultSegmentCount; + + /// + /// Width along the rectangle's X axis and depth along its Z axis, in the GameObject's local units. 1 by 1 by + /// default. + /// + public Vector2 Size + { + get => _size; + set + { + _size = value; + if (TryGetShape(out RoundedRectangle rectangle)) + { + rectangle.Size = value; + } + } + } + + /// + /// Radius of the corners in the GameObject's local units, drawn clamped between 0, for sharp corners, and half the + /// shorter side. 0.25 by default. + /// + public float CornerRadius + { + get => _cornerRadius; + set + { + _cornerRadius = value; + if (TryGetShape(out RoundedRectangle rectangle)) + { + rectangle.CornerRadius = value; + } + } + } + + /// + /// Number of straight pieces a full circle of the corners is drawn with, a multiple of 4 from 4 to 1024; each + /// corner is a quarter of them. 32 by default. Changing it creates the shape again. + /// + /// The count isn't a multiple of 4 from 4 to 1024. + public int SegmentCount + { + get => _segmentCount; + set + { + _segmentCount = ShapeCounts.CheckQuarterSegmentCount(value); + Refresh(); + } + } + + internal override int BuildSignature + { + get => _segmentCount; + } + + internal override Shape CreateShape(Transform bone) + { + return new RoundedRectangle(bone, Center, Rotation, _size, _cornerRadius, _segmentCount); + } + + internal override void ApplySizesTo(Shape shape) + { + RoundedRectangle rectangle = (RoundedRectangle)shape; + rectangle.Size = _size; + rectangle.CornerRadius = _cornerRadius; + } + + internal override void Sanitize() + { + _segmentCount = ShapeCounts.ClampQuarterSegmentCount(_segmentCount); + } + } +} diff --git a/UnityPackage/Runtime/Shapes/RoundedRectangle/WireframeRoundedRectangle.cs.meta b/UnityPackage/Runtime/Shapes/RoundedRectangle/WireframeRoundedRectangle.cs.meta new file mode 100644 index 0000000..eb3d7f9 --- /dev/null +++ b/UnityPackage/Runtime/Shapes/RoundedRectangle/WireframeRoundedRectangle.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 5e7758f3d8f5b1aa6c50d32de4b72c5f diff --git a/UnityPackage/Runtime/Shapes/Sphere/WireframeSphere.cs b/UnityPackage/Runtime/Shapes/Sphere/WireframeSphere.cs new file mode 100644 index 0000000..d1eda4a --- /dev/null +++ b/UnityPackage/Runtime/Shapes/Sphere/WireframeSphere.cs @@ -0,0 +1,70 @@ +using System; +using UnityEngine; + +namespace reromanlee.Wireframes +{ + /// + /// Draws a sphere on its GameObject as three great circles around , like a + /// SphereCollider. + /// + [AddComponentMenu("Wireframes/Sphere")] + [ExecuteAlways] + [HelpURL(HelpUrl)] + public sealed class WireframeSphere : WireframeCenteredShape + { + [Tooltip("Radius in the GameObject's local units.")] + [SerializeField] private float _radius = ShapeDefaults.Radius; + + [Tooltip("Number of straight pieces each circle is drawn with, from 3 to 1024.")] + [SerializeField, Delayed] private int _segmentCount = Ring.DefaultSegmentCount; + + /// Radius in the GameObject's local units. 0.5 by default. + public float Radius + { + get => _radius; + set + { + _radius = value; + if (TryGetShape(out Sphere sphere)) + { + sphere.Radius = value; + } + } + } + + /// + /// Number of straight pieces each circle is drawn with, from 3 to 1024. 32 by default. Changing it creates the + /// shape again. + /// + /// The count is outside 3 to 1024. + public int SegmentCount + { + get => _segmentCount; + set + { + _segmentCount = ShapeCounts.CheckSegmentCount(value); + Refresh(); + } + } + + internal override int BuildSignature + { + get => _segmentCount; + } + + internal override Shape CreateShape(Transform bone) + { + return new Sphere(bone, Center, Rotation, _radius, _segmentCount); + } + + internal override void ApplySizesTo(Shape shape) + { + ((Sphere)shape).Radius = _radius; + } + + internal override void Sanitize() + { + _segmentCount = ShapeCounts.ClampSegmentCount(_segmentCount); + } + } +} diff --git a/UnityPackage/Runtime/Shapes/Sphere/WireframeSphere.cs.meta b/UnityPackage/Runtime/Shapes/Sphere/WireframeSphere.cs.meta new file mode 100644 index 0000000..9e7bc0f --- /dev/null +++ b/UnityPackage/Runtime/Shapes/Sphere/WireframeSphere.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 131fa8e49aa389f114be4c80ed61eae6 diff --git a/UnityPackage/Runtime/Shapes/SpikedSphere/SpikedSphereFactory.cs b/UnityPackage/Runtime/Shapes/SpikedSphere/SpikedSphereFactory.cs index 37c0f18..b73635d 100644 --- a/UnityPackage/Runtime/Shapes/SpikedSphere/SpikedSphereFactory.cs +++ b/UnityPackage/Runtime/Shapes/SpikedSphere/SpikedSphereFactory.cs @@ -5,7 +5,7 @@ namespace reromanlee.Wireframes /// Creates spiked spheres in a . public static class SpikedSphereFactory { - private const int DefaultSpikeCount = 12; + internal const int DefaultSpikeCount = 12; /// /// Creates a white spiked sphere around the world origin: a dodecahedron with corners 0.25 from the center and diff --git a/UnityPackage/Runtime/Shapes/SpikedSphere/WireframeSpikedSphere.cs b/UnityPackage/Runtime/Shapes/SpikedSphere/WireframeSpikedSphere.cs new file mode 100644 index 0000000..deef11b --- /dev/null +++ b/UnityPackage/Runtime/Shapes/SpikedSphere/WireframeSpikedSphere.cs @@ -0,0 +1,94 @@ +using System; +using UnityEngine; + +namespace reromanlee.Wireframes +{ + /// + /// Draws a spiked sphere on its GameObject, a 3D star around : a Platonic + /// solid with a spike on each face. + /// + [AddComponentMenu("Wireframes/Spiked Sphere")] + [ExecuteAlways] + [HelpURL(HelpUrl)] + public sealed class WireframeSpikedSphere : WireframeCenteredShape + { + [Tooltip("Distance from the center to the corners of the solid, in the GameObject's local units.")] + [SerializeField] private float _baseRadius = ShapeDefaults.Radius * 0.5f; + + [Tooltip("How far the spike tips reach beyond the base radius, in the GameObject's local units.")] + [SerializeField] private float _spikeLength = ShapeDefaults.Radius * 0.5f; + + [Tooltip("Number of spikes, one on each face of a Platonic solid: 4, 6, 8, 12 or 20.")] + [SerializeField, IntegerOptions(4, 6, 8, 12, 20)] private int _spikeCount = SpikedSphereFactory.DefaultSpikeCount; + + /// + /// Distance from the center to the corners of the solid, in the GameObject's local units. 0.25 by default. + /// + public float BaseRadius + { + get => _baseRadius; + set + { + _baseRadius = value; + if (TryGetShape(out SpikedSphere sphere)) + { + sphere.BaseRadius = value; + } + } + } + + /// + /// How far the spike tips reach beyond , in the GameObject's local units. 0.25 by + /// default. + /// + public float SpikeLength + { + get => _spikeLength; + set + { + _spikeLength = value; + if (TryGetShape(out SpikedSphere sphere)) + { + sphere.SpikeLength = value; + } + } + } + + /// + /// Number of spikes: 4, 6, 8, 12 or 20, one on each face of a tetrahedron, cube, octahedron, dodecahedron or + /// icosahedron. 12 by default. Changing it creates the shape again. + /// + /// The count isn't 4, 6, 8, 12 or 20. + public int SpikeCount + { + get => _spikeCount; + set + { + _spikeCount = ShapeCounts.CheckSpikeCount(value); + Refresh(); + } + } + + internal override int BuildSignature + { + get => _spikeCount; + } + + internal override Shape CreateShape(Transform bone) + { + return new SpikedSphere(bone, Center, Rotation, _baseRadius, _spikeLength, _spikeCount); + } + + internal override void ApplySizesTo(Shape shape) + { + SpikedSphere sphere = (SpikedSphere)shape; + sphere.BaseRadius = _baseRadius; + sphere.SpikeLength = _spikeLength; + } + + internal override void Sanitize() + { + _spikeCount = ShapeCounts.ClampSpikeCount(_spikeCount); + } + } +} diff --git a/UnityPackage/Runtime/Shapes/SpikedSphere/WireframeSpikedSphere.cs.meta b/UnityPackage/Runtime/Shapes/SpikedSphere/WireframeSpikedSphere.cs.meta new file mode 100644 index 0000000..fc6fc59 --- /dev/null +++ b/UnityPackage/Runtime/Shapes/SpikedSphere/WireframeSpikedSphere.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: dd22f754dd2c82a95907f3aab39b48d3 diff --git a/UnityPackage/Runtime/Shapes/Stadium/WireframeStadium.cs b/UnityPackage/Runtime/Shapes/Stadium/WireframeStadium.cs new file mode 100644 index 0000000..cceb79f --- /dev/null +++ b/UnityPackage/Runtime/Shapes/Stadium/WireframeStadium.cs @@ -0,0 +1,89 @@ +using System; +using UnityEngine; + +namespace reromanlee.Wireframes +{ + /// + /// Draws a stadium on its GameObject, the flat outline of a capsule: two circles centered on the ends of its axis, + /// wrapped together in the XZ plane of the shape. With no roll it lies as flat on the GameObject as the axis allows. + /// + [AddComponentMenu("Wireframes/Stadium")] + [ExecuteAlways] + [HelpURL(HelpUrl)] + public sealed class WireframeStadium : WireframeAxialShape + { + [Tooltip("Radius of the circle around end A, in the GameObject's local units.")] + [SerializeField] private float _radiusA = ShapeDefaults.Radius; + + [Tooltip("Radius of the circle around end B, in the GameObject's local units.")] + [SerializeField] private float _radiusB = ShapeDefaults.Radius; + + [Tooltip("Number of straight pieces a full circle would be drawn with, a multiple of 4 from 4 to 1024.")] + [SerializeField, Delayed] private int _segmentCount = Ring.DefaultSegmentCount; + + /// Radius of the circle around end A, in the GameObject's local units. 0.5 by default. + public float RadiusA + { + get => _radiusA; + set + { + _radiusA = value; + if (TryGetShape(out Stadium stadium)) + { + stadium.RadiusA = value; + } + } + } + + /// Radius of the circle around end B, in the GameObject's local units. 0.5 by default. + public float RadiusB + { + get => _radiusB; + set + { + _radiusB = value; + if (TryGetShape(out Stadium stadium)) + { + stadium.RadiusB = value; + } + } + } + + /// + /// Number of straight pieces a full circle would be drawn with, a multiple of 4 from 4 to 1024; each end is half + /// of them. 32 by default. Changing it creates the shape again. + /// + /// The count isn't a multiple of 4 from 4 to 1024. + public int SegmentCount + { + get => _segmentCount; + set + { + _segmentCount = ShapeCounts.CheckQuarterSegmentCount(value); + Refresh(); + } + } + + internal override int BuildSignature + { + get => _segmentCount; + } + + internal override Shape CreateShape(Transform bone) + { + return new Stadium(bone, EndA, AxisRotation, AxisLength, _radiusA, _radiusB, _segmentCount); + } + + internal override void ApplySizesTo(Shape shape) + { + Stadium stadium = (Stadium)shape; + stadium.RadiusA = _radiusA; + stadium.RadiusB = _radiusB; + } + + internal override void Sanitize() + { + _segmentCount = ShapeCounts.ClampQuarterSegmentCount(_segmentCount); + } + } +} diff --git a/UnityPackage/Runtime/Shapes/Stadium/WireframeStadium.cs.meta b/UnityPackage/Runtime/Shapes/Stadium/WireframeStadium.cs.meta new file mode 100644 index 0000000..6953826 --- /dev/null +++ b/UnityPackage/Runtime/Shapes/Stadium/WireframeStadium.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 1729bfce2795fcdde49ea60b3040db59 diff --git a/UnityPackage/Runtime/Shapes/Star/StarFactory.cs b/UnityPackage/Runtime/Shapes/Star/StarFactory.cs index cf33301..3e7b75e 100644 --- a/UnityPackage/Runtime/Shapes/Star/StarFactory.cs +++ b/UnityPackage/Runtime/Shapes/Star/StarFactory.cs @@ -5,8 +5,8 @@ namespace reromanlee.Wireframes /// Creates stars in a . public static class StarFactory { - private const float DefaultInnerRadius = 0.2f; - private const int DefaultPointCount = 5; + internal const float DefaultInnerRadius = 0.2f; + internal const int DefaultPointCount = 5; /// /// Creates a white five-pointed star around the world origin, lying flat in the XZ plane, with an outer radius diff --git a/UnityPackage/Runtime/Shapes/Star/WireframeStar.cs b/UnityPackage/Runtime/Shapes/Star/WireframeStar.cs new file mode 100644 index 0000000..04108b7 --- /dev/null +++ b/UnityPackage/Runtime/Shapes/Star/WireframeStar.cs @@ -0,0 +1,90 @@ +using System; +using UnityEngine; + +namespace reromanlee.Wireframes +{ + /// + /// Draws a flat star on its GameObject, centered on in the XZ plane of its + /// rotation, with a tip on its +Z axis. + /// + [AddComponentMenu("Wireframes/Star")] + [ExecuteAlways] + [HelpURL(HelpUrl)] + public sealed class WireframeStar : WireframeCenteredShape + { + [Tooltip("Distance from the center to the corners between the points, in the GameObject's local units.")] + [SerializeField] private float _innerRadius = StarFactory.DefaultInnerRadius; + + [Tooltip("Distance from the center to the tips of the points, in the GameObject's local units.")] + [SerializeField] private float _outerRadius = ShapeDefaults.Radius; + + [Tooltip("Number of points, from 3 to 512.")] + [SerializeField, Delayed] private int _pointCount = StarFactory.DefaultPointCount; + + /// + /// Distance from the center to the corners between the points, in the GameObject's local units. 0.2 by default. + /// + public float InnerRadius + { + get => _innerRadius; + set + { + _innerRadius = value; + if (TryGetShape(out Star star)) + { + star.InnerRadius = value; + } + } + } + + /// + /// Distance from the center to the tips of the points, in the GameObject's local units. 0.5 by default. + /// + public float OuterRadius + { + get => _outerRadius; + set + { + _outerRadius = value; + if (TryGetShape(out Star star)) + { + star.OuterRadius = value; + } + } + } + + /// Number of points, from 3 to 512. 5 by default. Changing it creates the shape again. + /// The count is outside 3 to 512. + public int PointCount + { + get => _pointCount; + set + { + _pointCount = ShapeCounts.CheckPointCount(value); + Refresh(); + } + } + + internal override int BuildSignature + { + get => _pointCount; + } + + internal override Shape CreateShape(Transform bone) + { + return new Star(bone, Center, Rotation, _innerRadius, _outerRadius, _pointCount); + } + + internal override void ApplySizesTo(Shape shape) + { + Star star = (Star)shape; + star.InnerRadius = _innerRadius; + star.OuterRadius = _outerRadius; + } + + internal override void Sanitize() + { + _pointCount = ShapeCounts.ClampPointCount(_pointCount); + } + } +} diff --git a/UnityPackage/Runtime/Shapes/Star/WireframeStar.cs.meta b/UnityPackage/Runtime/Shapes/Star/WireframeStar.cs.meta new file mode 100644 index 0000000..c564e11 --- /dev/null +++ b/UnityPackage/Runtime/Shapes/Star/WireframeStar.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 30ad628530e14eace180489e3d724c81 diff --git a/UnityPackage/Samples~/ShapeGallery/ComponentGallery.unity b/UnityPackage/Samples~/ShapeGallery/ComponentGallery.unity new file mode 100644 index 0000000..9dcac90 --- /dev/null +++ b/UnityPackage/Samples~/ShapeGallery/ComponentGallery.unity @@ -0,0 +1,1542 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!29 &1 +OcclusionCullingSettings: + m_ObjectHideFlags: 0 + serializedVersion: 2 + m_OcclusionBakeSettings: + smallestOccluder: 5 + smallestHole: 0.25 + backfaceThreshold: 100 + m_SceneGUID: 00000000000000000000000000000000 + m_OcclusionCullingData: {fileID: 0} +--- !u!104 &2 +RenderSettings: + m_ObjectHideFlags: 0 + serializedVersion: 9 + m_Fog: 0 + m_FogColor: {r: 0.5, g: 0.5, b: 0.5, a: 1} + m_FogMode: 3 + m_FogDensity: 0.01 + m_LinearFogStart: 0 + m_LinearFogEnd: 300 + m_AmbientSkyColor: {r: 0.06, g: 0.06, b: 0.08, a: 1} + m_AmbientEquatorColor: {r: 0.114, g: 0.125, b: 0.133, a: 1} + m_AmbientGroundColor: {r: 0.047, g: 0.043, b: 0.035, a: 1} + m_AmbientIntensity: 1 + m_AmbientMode: 3 + m_SubtractiveShadowColor: {r: 0.42, g: 0.478, b: 0.627, a: 1} + m_SkyboxMaterial: {fileID: 0} + m_HaloStrength: 0.5 + m_FlareStrength: 1 + m_FlareFadeSpeed: 3 + m_HaloTexture: {fileID: 0} + m_SpotCookie: {fileID: 10001, guid: 0000000000000000e000000000000000, type: 0} + m_DefaultReflectionMode: 0 + m_DefaultReflectionResolution: 128 + m_ReflectionBounces: 1 + m_ReflectionIntensity: 1 + m_CustomReflection: {fileID: 0} + m_Sun: {fileID: 0} + m_UseRadianceAmbientProbe: 0 +--- !u!157 &3 +LightmapSettings: + m_ObjectHideFlags: 0 + serializedVersion: 12 + m_GIWorkflowMode: 1 + m_GISettings: + serializedVersion: 2 + m_BounceScale: 1 + m_IndirectOutputScale: 1 + m_AlbedoBoost: 1 + m_EnvironmentLightingMode: 0 + m_EnableBakedLightmaps: 1 + m_EnableRealtimeLightmaps: 0 + m_LightmapEditorSettings: + serializedVersion: 12 + m_Resolution: 2 + m_BakeResolution: 40 + m_AtlasSize: 1024 + m_AO: 0 + m_AOMaxDistance: 1 + m_CompAOExponent: 1 + m_CompAOExponentDirect: 0 + m_ExtractAmbientOcclusion: 0 + m_Padding: 2 + m_LightmapParameters: {fileID: 0} + m_LightmapsBakeMode: 1 + m_TextureCompression: 1 + m_FinalGather: 0 + m_FinalGatherFiltering: 1 + m_FinalGatherRayCount: 256 + m_ReflectionCompression: 2 + m_MixedBakeMode: 2 + m_BakeBackend: 2 + m_PVRSampling: 1 + m_PVRDirectSampleCount: 32 + m_PVRSampleCount: 512 + m_PVRBounces: 2 + m_PVREnvironmentSampleCount: 256 + m_PVREnvironmentReferencePointCount: 2048 + m_PVRFilteringMode: 1 + m_PVRDenoiserTypeDirect: 1 + m_PVRDenoiserTypeIndirect: 1 + m_PVRDenoiserTypeAO: 1 + m_PVRFilterTypeDirect: 0 + m_PVRFilterTypeIndirect: 0 + m_PVRFilterTypeAO: 0 + m_PVREnvironmentMIS: 1 + m_PVRCulling: 1 + m_PVRFilteringGaussRadiusDirect: 1 + m_PVRFilteringGaussRadiusIndirect: 1 + m_PVRFilteringGaussRadiusAO: 1 + m_PVRFilteringAtrousPositionSigmaDirect: 0.5 + m_PVRFilteringAtrousPositionSigmaIndirect: 2 + m_PVRFilteringAtrousPositionSigmaAO: 1 + m_ExportTrainingData: 0 + m_TrainingDataDestination: TrainingData + m_LightProbeSampleCountMultiplier: 4 + m_LightingDataAsset: {fileID: 20201, guid: 0000000000000000f000000000000000, type: 0} + m_LightingSettings: {fileID: 0} +--- !u!196 &4 +NavMeshSettings: + serializedVersion: 2 + m_ObjectHideFlags: 0 + m_BuildSettings: + serializedVersion: 3 + agentTypeID: 0 + agentRadius: 0.5 + agentHeight: 2 + agentSlope: 45 + agentClimb: 0.4 + ledgeDropHeight: 0 + maxJumpAcrossDistance: 0 + minRegionArea: 2 + manualCellSize: 0 + cellSize: 0.16666667 + manualTileSize: 0 + tileSize: 256 + buildHeightMesh: 0 + maxJobWorkers: 0 + preserveTilesOutsideBounds: 0 + debug: + m_Flags: 0 + m_NavMeshData: {fileID: 0} +--- !u!1 &87000795 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 87000796} + - component: {fileID: 87000797} + m_Layer: 0 + m_Name: Ellipsoid + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &87000796 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 87000795} + serializedVersion: 2 + m_LocalRotation: {x: -0.1736482, y: 0, z: 0, w: 0.9848078} + m_LocalPosition: {x: -4.5, y: 3, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 226881397} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!114 &87000797 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 87000795} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: afb73b3f8e393ab7277248a5f6539959, type: 3} + m_Name: + m_EditorClassIdentifier: + _color: {r: 0.7, g: 1, b: 0.39999998, a: 1} + _occlusion: 0 + _drawAsGizmo: 0 + _center: {x: 0, y: 0, z: 0} + _rotation: {x: 0, y: 0, z: 0} + _radii: {x: 0.5, y: 0.9, z: 0.5} + _segmentCount: 32 +--- !u!1 &102627796 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 102627797} + - component: {fileID: 102627798} + m_Layer: 0 + m_Name: Ellipse + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &102627797 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 102627796} + serializedVersion: 2 + m_LocalRotation: {x: -0.7071068, y: 0, z: 0, w: 0.7071068} + m_LocalPosition: {x: 6, y: 6, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 226881397} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!114 &102627798 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 102627796} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: dac5b0a1e62a5b6ef3e3f8ab92b2e8d6, type: 3} + m_Name: + m_EditorClassIdentifier: + _color: {r: 0.39999998, g: 0.39999998, b: 1, a: 1} + _occlusion: 0 + _drawAsGizmo: 0 + _center: {x: 0, y: 0, z: 0} + _rotation: {x: 0, y: 0, z: 0} + _radii: {x: 0.45, y: 0.9} + _segmentCount: 32 +--- !u!1 &200859484 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 200859485} + - component: {fileID: 200859486} + m_Layer: 0 + m_Name: Frustum (6 sides) + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &200859485 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 200859484} + serializedVersion: 2 + m_LocalRotation: {x: -0.1736482, y: 0, z: 0, w: 0.9848078} + m_LocalPosition: {x: 6, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 226881397} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!114 &200859486 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 200859484} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 7fcdf95a81a03b7c449f86cdfddc82d4, type: 3} + m_Name: + m_EditorClassIdentifier: + _color: {r: 0.5714285, g: 0.39999998, b: 1, a: 1} + _occlusion: 0 + _drawAsGizmo: 0 + _endA: {x: 0, y: -0.8, z: 0} + _endB: {x: 0, y: 0.8, z: 0} + _roll: 0 + _radiusA: 0.5 + _radiusB: 0.8 + _sideCount: 6 +--- !u!1 &226881395 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 226881397} + - component: {fileID: 226881396} + m_Layer: 0 + m_Name: Component Gallery + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!114 &226881396 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 226881395} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: e4eb65bde79804e1a4af770ab97f7756, type: 3} + m_Name: + m_EditorClassIdentifier: + _turnSpeed: 30 +--- !u!4 &226881397 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 226881395} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: + - {fileID: 1052470524} + - {fileID: 813077810} + - {fileID: 1997186479} + - {fileID: 514862326} + - {fileID: 765750505} + - {fileID: 1700027900} + - {fileID: 102627797} + - {fileID: 988887756} + - {fileID: 1133602109} + - {fileID: 266178291} + - {fileID: 766651462} + - {fileID: 87000796} + - {fileID: 1477612128} + - {fileID: 1474208380} + - {fileID: 1841872805} + - {fileID: 551532538} + - {fileID: 1502267687} + - {fileID: 1833630923} + - {fileID: 527058433} + - {fileID: 1386570040} + - {fileID: 282367002} + - {fileID: 847776589} + - {fileID: 200859485} + - {fileID: 1150338161} + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!1 &266178290 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 266178291} + - component: {fileID: 266178292} + m_Layer: 0 + m_Name: Box + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &266178291 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 266178290} + serializedVersion: 2 + m_LocalRotation: {x: -0.1736482, y: 0, z: 0, w: 0.9848078} + m_LocalPosition: {x: -10.5, y: 3, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 226881397} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!114 &266178292 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 266178290} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: f0beb658a12cfefdf628000edb88382b, type: 3} + m_Name: + m_EditorClassIdentifier: + _color: {r: 1, g: 0.39999998, b: 0.39999998, a: 1} + _occlusion: 0 + _drawAsGizmo: 0 + _center: {x: 0, y: 0, z: 0} + _rotation: {x: 0, y: 0, z: 0} + _size: {x: 1.2, y: 1.2, z: 1.2} +--- !u!1 &282367001 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 282367002} + - component: {fileID: 282367003} + m_Layer: 0 + m_Name: Tapered Capsule + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &282367002 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 282367001} + serializedVersion: 2 + m_LocalRotation: {x: -0.1736482, y: 0, z: 0, w: 0.9848078} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 226881397} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!114 &282367003 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 282367001} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 49ecf785519c39d854b1e041d0b010d5, type: 3} + m_Name: + m_EditorClassIdentifier: + _color: {r: 0.39999998, g: 1, b: 0.7428571, a: 1} + _occlusion: 0 + _drawAsGizmo: 0 + _endA: {x: 0, y: -0.4, z: 0} + _endB: {x: 0, y: 0.48000002, z: 0} + _roll: 0 + _radiusA: 0.5 + _radiusB: 0.25 + _segmentCount: 32 +--- !u!1 &514862325 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 514862326} + - component: {fileID: 514862327} + m_Layer: 0 + m_Name: Rectangle + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &514862326 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 514862325} + serializedVersion: 2 + m_LocalRotation: {x: -0.7071068, y: 0, z: 0, w: 0.7071068} + m_LocalPosition: {x: -3, y: 6, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 226881397} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!114 &514862327 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 514862325} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 8832ff90d3a928029358050ead70ec40, type: 3} + m_Name: + m_EditorClassIdentifier: + _color: {r: 0.39999998, g: 1, b: 0.39999998, a: 1} + _occlusion: 0 + _drawAsGizmo: 0 + _center: {x: 0, y: 0, z: 0} + _rotation: {x: 0, y: 0, z: 0} + _size: {x: 1.8, y: 1.2} +--- !u!1 &527058432 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 527058433} + - component: {fileID: 527058434} + m_Layer: 0 + m_Name: Cone + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &527058433 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 527058432} + serializedVersion: 2 + m_LocalRotation: {x: -0.1736482, y: 0, z: 0, w: 0.9848078} + m_LocalPosition: {x: -6, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 226881397} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!114 &527058434 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 527058432} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 1cf8d35972784aa79ac78bbd1ee4678b, type: 3} + m_Name: + m_EditorClassIdentifier: + _color: {r: 1, g: 0.9142858, b: 0.39999998, a: 1} + _occlusion: 0 + _drawAsGizmo: 0 + _endA: {x: 0, y: 0.8, z: 0} + _endB: {x: 0, y: -0.8, z: 0} + _roll: 0 + _radius: 0.6 + _segmentCount: 32 +--- !u!1 &551532537 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 551532538} + - component: {fileID: 551532539} + m_Layer: 0 + m_Name: Spiked Sphere (12) + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &551532538 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 551532537} + serializedVersion: 2 + m_LocalRotation: {x: -0.1736482, y: 0, z: 0, w: 0.9848078} + m_LocalPosition: {x: 7.5, y: 3, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 226881397} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!114 &551532539 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 551532537} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: dd22f754dd2c82a95907f3aab39b48d3, type: 3} + m_Name: + m_EditorClassIdentifier: + _color: {r: 0.7, g: 0.39999998, b: 1, a: 1} + _occlusion: 0 + _drawAsGizmo: 0 + _center: {x: 0, y: 0, z: 0} + _rotation: {x: 0, y: 0, z: 0} + _baseRadius: 0.4 + _spikeLength: 0.5 + _spikeCount: 12 +--- !u!1 &765750504 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 765750505} + - component: {fileID: 765750506} + m_Layer: 0 + m_Name: Rounded Rectangle + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &765750505 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 765750504} + serializedVersion: 2 + m_LocalRotation: {x: -0.7071068, y: 0, z: 0, w: 0.7071068} + m_LocalPosition: {x: 0, y: 6, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 226881397} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!114 &765750506 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 765750504} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5e7758f3d8f5b1aa6c50d32de4b72c5f, type: 3} + m_Name: + m_EditorClassIdentifier: + _color: {r: 0.39999998, g: 1, b: 0.8, a: 1} + _occlusion: 0 + _drawAsGizmo: 0 + _center: {x: 0, y: 0, z: 0} + _rotation: {x: 0, y: 0, z: 0} + _size: {x: 1.8, y: 1.2} + _cornerRadius: 0.3 + _segmentCount: 32 +--- !u!1 &766651461 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 766651462} + - component: {fileID: 766651463} + m_Layer: 0 + m_Name: Sphere + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &766651462 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 766651461} + serializedVersion: 2 + m_LocalRotation: {x: -0.1736482, y: 0, z: 0, w: 0.9848078} + m_LocalPosition: {x: -7.5, y: 3, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 226881397} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!114 &766651463 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 766651461} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 131fa8e49aa389f114be4c80ed61eae6, type: 3} + m_Name: + m_EditorClassIdentifier: + _color: {r: 1, g: 0.85, b: 0.39999998, a: 1} + _occlusion: 0 + _drawAsGizmo: 0 + _center: {x: 0, y: 0, z: 0} + _rotation: {x: 0, y: 0, z: 0} + _radius: 0.8 + _segmentCount: 32 +--- !u!1 &813077809 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 813077810} + - component: {fileID: 813077811} + m_Layer: 0 + m_Name: Polyline + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &813077810 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 813077809} + serializedVersion: 2 + m_LocalRotation: {x: -0.7071068, y: 0, z: 0, w: 0.7071068} + m_LocalPosition: {x: -9, y: 6, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 226881397} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!114 &813077811 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 813077809} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 538e232bcc792e0b2a677ed0f42b2db8, type: 3} + m_Name: + m_EditorClassIdentifier: + _color: {r: 1, g: 0.8, b: 0.39999998, a: 1} + _occlusion: 0 + _drawAsGizmo: 0 + _points: + - Bone: {fileID: 0} + Position: {x: -0.9, y: 0, z: -0.6} + - Bone: {fileID: 0} + Position: {x: -0.3, y: 0, z: 0.6} + - Bone: {fileID: 0} + Position: {x: 0.3, y: 0, z: -0.6} + - Bone: {fileID: 0} + Position: {x: 0.9, y: 0, z: 0.6} + _isClosed: 0 +--- !u!1 &847776588 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 847776589} + - component: {fileID: 847776590} + m_Layer: 0 + m_Name: Frustum + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &847776589 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 847776588} + serializedVersion: 2 + m_LocalRotation: {x: -0.1736482, y: 0, z: 0, w: 0.9848078} + m_LocalPosition: {x: 3, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 226881397} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!114 &847776590 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 847776588} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 7fcdf95a81a03b7c449f86cdfddc82d4, type: 3} + m_Name: + m_EditorClassIdentifier: + _color: {r: 0.39999998, g: 0.742857, b: 1, a: 1} + _occlusion: 0 + _drawAsGizmo: 0 + _endA: {x: 0, y: -0.8, z: 0} + _endB: {x: 0, y: 0.8, z: 0} + _roll: 0 + _radiusA: 0.8 + _radiusB: 0.4 + _sideCount: 4 +--- !u!1 &988887755 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 988887756} + - component: {fileID: 988887757} + m_Layer: 0 + m_Name: Star + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &988887756 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 988887755} + serializedVersion: 2 + m_LocalRotation: {x: -0.7071068, y: 0, z: 0, w: 0.7071068} + m_LocalPosition: {x: 9, y: 6, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 226881397} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!114 &988887757 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 988887755} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 30ad628530e14eace180489e3d724c81, type: 3} + m_Name: + m_EditorClassIdentifier: + _color: {r: 0.8000002, g: 0.39999998, b: 1, a: 1} + _occlusion: 0 + _drawAsGizmo: 0 + _center: {x: 0, y: 0, z: 0} + _rotation: {x: 0, y: 0, z: 0} + _innerRadius: 0.35 + _outerRadius: 0.9 + _pointCount: 5 +--- !u!1 &1019189419 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1019189422} + - component: {fileID: 1019189421} + - component: {fileID: 1019189420} + m_Layer: 0 + m_Name: Main Camera + m_TagString: MainCamera + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!81 &1019189420 +AudioListener: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1019189419} + m_Enabled: 1 +--- !u!20 &1019189421 +Camera: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1019189419} + m_Enabled: 1 + serializedVersion: 2 + m_ClearFlags: 2 + m_BackGroundColor: {r: 0.06, g: 0.06, b: 0.08, a: 1} + m_projectionMatrixMode: 1 + m_GateFitMode: 2 + m_FOVAxisMode: 0 + m_Iso: 200 + m_ShutterSpeed: 0.005 + m_Aperture: 16 + m_FocusDistance: 10 + m_FocalLength: 50 + m_BladeCount: 5 + m_Curvature: {x: 2, y: 11} + m_BarrelClipping: 0.25 + m_Anamorphism: 0 + m_SensorSize: {x: 36, y: 24} + m_LensShift: {x: 0, y: 0} + m_NormalizedViewPortRect: + serializedVersion: 2 + x: 0 + y: 0 + width: 1 + height: 1 + near clip plane: 0.3 + far clip plane: 100 + field of view: 45 + orthographic: 0 + orthographic size: 5 + m_Depth: 0 + m_CullingMask: + serializedVersion: 2 + m_Bits: 4294967295 + m_RenderingPath: -1 + m_TargetTexture: {fileID: 0} + m_TargetDisplay: 0 + m_TargetEye: 3 + m_HDR: 1 + m_AllowMSAA: 1 + m_AllowDynamicResolution: 0 + m_ForceIntoRT: 0 + m_OcclusionCulling: 1 + m_StereoConvergence: 10 + m_StereoSeparation: 0.022 +--- !u!4 &1019189422 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1019189419} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 3, z: -21} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!1 &1052470523 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1052470524} + - component: {fileID: 1052470525} + m_Layer: 0 + m_Name: Line + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &1052470524 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1052470523} + serializedVersion: 2 + m_LocalRotation: {x: -0.7071068, y: 0, z: 0, w: 0.7071068} + m_LocalPosition: {x: -12, y: 6, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 226881397} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!114 &1052470525 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1052470523} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: e25f2c254941571aae80be93269e2674, type: 3} + m_Name: + m_EditorClassIdentifier: + _color: {r: 1, g: 0.39999998, b: 0.39999998, a: 1} + _occlusion: 0 + _drawAsGizmo: 0 + _boneA: {fileID: 0} + _positionA: {x: -0.8, y: 0, z: -0.8} + _boneB: {fileID: 0} + _positionB: {x: 0.8, y: 0, z: 0.8} +--- !u!1 &1133602108 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1133602109} + - component: {fileID: 1133602110} + m_Layer: 0 + m_Name: Stadium + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &1133602109 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1133602108} + serializedVersion: 2 + m_LocalRotation: {x: -0.7071068, y: 0, z: 0, w: 0.7071068} + m_LocalPosition: {x: 12, y: 6, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 226881397} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!114 &1133602110 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1133602108} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 1729bfce2795fcdde49ea60b3040db59, type: 3} + m_Name: + m_EditorClassIdentifier: + _color: {r: 1, g: 0.39999998, b: 0.7999999, a: 1} + _occlusion: 0 + _drawAsGizmo: 0 + _endA: {x: 0, y: 0, z: -0.5} + _endB: {x: 0, y: 0, z: 0.5} + _roll: 0 + _radiusA: 0.45 + _radiusB: 0.25 + _segmentCount: 32 +--- !u!1 &1150338160 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1150338161} + - component: {fileID: 1150338162} + m_Layer: 0 + m_Name: Pyramid + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &1150338161 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1150338160} + serializedVersion: 2 + m_LocalRotation: {x: -0.1736482, y: 0, z: 0, w: 0.9848078} + m_LocalPosition: {x: 9, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 226881397} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!114 &1150338162 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1150338160} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 7e298f4cb2b2208ba842d77775fad949, type: 3} + m_Name: + m_EditorClassIdentifier: + _color: {r: 1, g: 0.39999998, b: 0.9142858, a: 1} + _occlusion: 0 + _drawAsGizmo: 0 + _endA: {x: 0, y: 0.8, z: 0} + _endB: {x: 0, y: -0.8, z: 0} + _roll: 0 + _baseSize: {x: 1.4, y: 1.4} +--- !u!1 &1386570039 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1386570040} + - component: {fileID: 1386570041} + m_Layer: 0 + m_Name: Capsule + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &1386570040 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1386570039} + serializedVersion: 2 + m_LocalRotation: {x: -0.1736482, y: 0, z: 0, w: 0.9848078} + m_LocalPosition: {x: -3, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 226881397} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!114 &1386570041 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1386570039} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 49ecf785519c39d854b1e041d0b010d5, type: 3} + m_Name: + m_EditorClassIdentifier: + _color: {r: 0.5714285, g: 1, b: 0.39999998, a: 1} + _occlusion: 0 + _drawAsGizmo: 0 + _endA: {x: 0, y: -0.4, z: 0} + _endB: {x: 0, y: 0.4, z: 0} + _roll: 0 + _radiusA: 0.45 + _radiusB: 0.45 + _segmentCount: 32 +--- !u!1 &1474208379 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1474208380} + - component: {fileID: 1474208381} + m_Layer: 0 + m_Name: Spiked Sphere (6) + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &1474208380 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1474208379} + serializedVersion: 2 + m_LocalRotation: {x: -0.1736482, y: 0, z: 0, w: 0.9848078} + m_LocalPosition: {x: 1.5, y: 3, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 226881397} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!114 &1474208381 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1474208379} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: dd22f754dd2c82a95907f3aab39b48d3, type: 3} + m_Name: + m_EditorClassIdentifier: + _color: {r: 0.39999998, g: 1, b: 1, a: 1} + _occlusion: 0 + _drawAsGizmo: 0 + _center: {x: 0, y: 0, z: 0} + _rotation: {x: 0, y: 0, z: 0} + _baseRadius: 0.4 + _spikeLength: 0.5 + _spikeCount: 6 +--- !u!1 &1477612127 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1477612128} + - component: {fileID: 1477612129} + m_Layer: 0 + m_Name: Spiked Sphere (4) + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &1477612128 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1477612127} + serializedVersion: 2 + m_LocalRotation: {x: -0.1736482, y: 0, z: 0, w: 0.9848078} + m_LocalPosition: {x: -1.5, y: 3, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 226881397} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!114 &1477612129 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1477612127} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: dd22f754dd2c82a95907f3aab39b48d3, type: 3} + m_Name: + m_EditorClassIdentifier: + _color: {r: 0.39999998, g: 1, b: 0.54999995, a: 1} + _occlusion: 0 + _drawAsGizmo: 0 + _center: {x: 0, y: 0, z: 0} + _rotation: {x: 0, y: 0, z: 0} + _baseRadius: 0.4 + _spikeLength: 0.5 + _spikeCount: 4 +--- !u!1 &1502267686 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1502267687} + - component: {fileID: 1502267688} + m_Layer: 0 + m_Name: Spiked Sphere (20) + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &1502267687 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1502267686} + serializedVersion: 2 + m_LocalRotation: {x: -0.1736482, y: 0, z: 0, w: 0.9848078} + m_LocalPosition: {x: 10.5, y: 3, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 226881397} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!114 &1502267688 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1502267686} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: dd22f754dd2c82a95907f3aab39b48d3, type: 3} + m_Name: + m_EditorClassIdentifier: + _color: {r: 1, g: 0.39999998, b: 0.85, a: 1} + _occlusion: 0 + _drawAsGizmo: 0 + _center: {x: 0, y: 0, z: 0} + _rotation: {x: 0, y: 0, z: 0} + _baseRadius: 0.4 + _spikeLength: 0.5 + _spikeCount: 20 +--- !u!1 &1700027899 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1700027900} + - component: {fileID: 1700027901} + m_Layer: 0 + m_Name: Circle + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &1700027900 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1700027899} + serializedVersion: 2 + m_LocalRotation: {x: -0.7071068, y: 0, z: 0, w: 0.7071068} + m_LocalPosition: {x: 3, y: 6, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 226881397} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!114 &1700027901 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1700027899} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 61424b534b5e3153b1a13495afcfb41c, type: 3} + m_Name: + m_EditorClassIdentifier: + _color: {r: 0.39999998, g: 0.7999999, b: 1, a: 1} + _occlusion: 0 + _drawAsGizmo: 0 + _center: {x: 0, y: 0, z: 0} + _rotation: {x: 0, y: 0, z: 0} + _radius: 0.8 + _segmentCount: 32 +--- !u!1 &1833630922 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1833630923} + - component: {fileID: 1833630924} + m_Layer: 0 + m_Name: Cylinder + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &1833630923 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1833630922} + serializedVersion: 2 + m_LocalRotation: {x: -0.1736482, y: 0, z: 0, w: 0.9848078} + m_LocalPosition: {x: -9, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 226881397} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!114 &1833630924 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1833630922} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: f0dec41d539947209f6d3650d88fb97a, type: 3} + m_Name: + m_EditorClassIdentifier: + _color: {r: 1, g: 0.39999998, b: 0.39999998, a: 1} + _occlusion: 0 + _drawAsGizmo: 0 + _endA: {x: 0, y: -0.8, z: 0} + _endB: {x: 0, y: 0.8, z: 0} + _roll: 0 + _radius: 0.5 + _segmentCount: 32 +--- !u!1 &1841872804 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1841872805} + - component: {fileID: 1841872806} + m_Layer: 0 + m_Name: Spiked Sphere (8) + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &1841872805 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1841872804} + serializedVersion: 2 + m_LocalRotation: {x: -0.1736482, y: 0, z: 0, w: 0.9848078} + m_LocalPosition: {x: 4.5, y: 3, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 226881397} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!114 &1841872806 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1841872804} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: dd22f754dd2c82a95907f3aab39b48d3, type: 3} + m_Name: + m_EditorClassIdentifier: + _color: {r: 0.39999998, g: 0.54999995, b: 1, a: 1} + _occlusion: 0 + _drawAsGizmo: 0 + _center: {x: 0, y: 0, z: 0} + _rotation: {x: 0, y: 0, z: 0} + _baseRadius: 0.4 + _spikeLength: 0.5 + _spikeCount: 8 +--- !u!1 &1997186478 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1997186479} + - component: {fileID: 1997186480} + m_Layer: 0 + m_Name: Triangle + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &1997186479 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1997186478} + serializedVersion: 2 + m_LocalRotation: {x: -0.7071068, y: 0, z: 0, w: 0.7071068} + m_LocalPosition: {x: -6, y: 6, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 226881397} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!114 &1997186480 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1997186478} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 538e232bcc792e0b2a677ed0f42b2db8, type: 3} + m_Name: + m_EditorClassIdentifier: + _color: {r: 0.79999995, g: 1, b: 0.39999998, a: 1} + _occlusion: 0 + _drawAsGizmo: 0 + _points: + - Bone: {fileID: 0} + Position: {x: -0.9, y: 0, z: -0.7} + - Bone: {fileID: 0} + Position: {x: 0.9, y: 0, z: -0.7} + - Bone: {fileID: 0} + Position: {x: 0, y: 0, z: 0.9} + _isClosed: 1 +--- !u!1660057539 &9223372036854775807 +SceneRoots: + m_ObjectHideFlags: 0 + m_Roots: + - {fileID: 1019189422} + - {fileID: 226881397} diff --git a/UnityPackage/Samples~/ShapeGallery/ComponentGallery.unity.meta b/UnityPackage/Samples~/ShapeGallery/ComponentGallery.unity.meta new file mode 100644 index 0000000..ccc7ca5 --- /dev/null +++ b/UnityPackage/Samples~/ShapeGallery/ComponentGallery.unity.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 019d7cc3b5322d24497a340eabe2c39d +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/UnityPackage/Samples~/ShapeGallery/Turntable.cs b/UnityPackage/Samples~/ShapeGallery/Turntable.cs new file mode 100644 index 0000000..984b1de --- /dev/null +++ b/UnityPackage/Samples~/ShapeGallery/Turntable.cs @@ -0,0 +1,23 @@ +using UnityEngine; + +namespace reromanlee.Wireframes.Samples +{ + /// + /// Turns each child around the world's up axis in Play Mode. In the ComponentGallery scene, it shows that shape + /// components follow their GameObjects with no work of their own. + /// + public sealed class Turntable : MonoBehaviour + { + [Tooltip("Degrees per second each child turns around the world's up axis.")] + [SerializeField] private float _turnSpeed = 30f; + + private void Update() + { + float angle = _turnSpeed * Time.deltaTime; + for (int i = 0; i < transform.childCount; i++) + { + transform.GetChild(i).Rotate(0f, angle, 0f, Space.World); + } + } + } +} diff --git a/UnityPackage/Samples~/ShapeGallery/Turntable.cs.meta b/UnityPackage/Samples~/ShapeGallery/Turntable.cs.meta new file mode 100644 index 0000000..acdbd92 --- /dev/null +++ b/UnityPackage/Samples~/ShapeGallery/Turntable.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: e4eb65bde79804e1a4af770ab97f7756 diff --git a/UnityPackage/Tests/Editor/CenterProbe.cs b/UnityPackage/Tests/Editor/CenterProbe.cs new file mode 100644 index 0000000..e8eb3ad --- /dev/null +++ b/UnityPackage/Tests/Editor/CenterProbe.cs @@ -0,0 +1,57 @@ +using UnityEngine; + +namespace reromanlee.Wireframes.Tests +{ + /// Renders the world around the origin with a camera of its own and reads back what was drawn there. + internal static class CenterProbe + { + private const int Size = 32; + private const float HalfExtent = 4f; + + /// + /// Renders with an orthographic camera that looks along +Z at the origin, 8 units across, and returns the red at + /// the center. Everything the probe creates is destroyed before it returns. + /// + internal static float RedAtCenter() + { + RenderTexture target = new(Size, Size, 24); + Texture2D readback = new(Size, Size, TextureFormat.RGBA32, false); + GameObject cameraObject = new("Probe Camera"); + try + { + Camera camera = cameraObject.AddComponent(); + camera.enabled = false; + camera.allowMSAA = false; + camera.orthographic = true; + camera.orthographicSize = HalfExtent; + camera.transform.position = new Vector3(0f, 0f, -10f); + camera.clearFlags = CameraClearFlags.SolidColor; + camera.backgroundColor = Color.black; + camera.targetTexture = target; + camera.Render(); + camera.targetTexture = null; + + RenderTexture previous = RenderTexture.active; + RenderTexture.active = target; + readback.ReadPixels(new Rect(0f, 0f, Size, Size), 0, 0); + readback.Apply(); + RenderTexture.active = previous; + float red = 0f; + for (int y = Size / 2 - 1; y <= Size / 2; y++) + { + for (int x = Size / 2 - 1; x <= Size / 2; x++) + { + red = Mathf.Max(red, readback.GetPixel(x, y).r); + } + } + return red; + } + finally + { + Object.DestroyImmediate(cameraObject); + Object.DestroyImmediate(readback); + Object.DestroyImmediate(target); + } + } + } +} diff --git a/UnityPackage/Tests/Editor/CenterProbe.cs.meta b/UnityPackage/Tests/Editor/CenterProbe.cs.meta new file mode 100644 index 0000000..4dc9ffe --- /dev/null +++ b/UnityPackage/Tests/Editor/CenterProbe.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 196d4345ca68902971310e20797e1166 diff --git a/UnityPackage/Tests/Editor/ComponentEditModeTests.cs b/UnityPackage/Tests/Editor/ComponentEditModeTests.cs new file mode 100644 index 0000000..4b31d9b --- /dev/null +++ b/UnityPackage/Tests/Editor/ComponentEditModeTests.cs @@ -0,0 +1,259 @@ +using System.Collections.Generic; +using System.Text.RegularExpressions; +using NUnit.Framework; +using reromanlee.Wireframes.Editor; +using UnityEditor; +using UnityEditor.SceneManagement; +using UnityEngine; +using UnityEngine.TestTools; +using Object = UnityEngine.Object; + +namespace reromanlee.Wireframes.Tests +{ + /// + /// Shape components in Edit Mode: drawn without entering Play Mode, kept in step with the Inspector, undo and layer + /// changes, drawn as gizmos when asked, and drawn in Prefab Mode's own scene. + /// + public class ComponentEditModeTests + { + private const string PrefabPath = "Assets/WireframesComponentTest.prefab"; + + private readonly List _objects = new(); + + [SetUp] + public void OpenEmptyScene() + { + EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Single); + } + + [TearDown] + public void DestroyEverything() + { + GizmoCameras.Filter = GizmoCameras.DrawsGizmos; + if (PrefabStageUtility.GetCurrentPrefabStage() != null) + { + StageUtility.GoToMainStage(); + } + for (int i = _objects.Count - 1; i >= 0; i--) + { + if (_objects[i] != null) + { + Object.DestroyImmediate(_objects[i]); + } + } + _objects.Clear(); + AssetDatabase.DeleteAsset(PrefabPath); + ShapeComponents.TakeRepaintRequest(); + } + + [Test] + public void Component_DrawsInEditModeWithAContainerThatIsNeverSaved() + { + WireframeLine line = AddRedLineAcrossTheCenter(); + + Assert.That(CenterProbe.RedAtCenter(), Is.GreaterThan(0.5f)); + GameObject proxyObject = line.SharedContainer.Container.Proxy.gameObject; + HideFlags hidden = HideFlags.DontSave | HideFlags.HideInHierarchy; + Assert.That(proxyObject.hideFlags & hidden, Is.EqualTo(hidden)); + Assert.That(proxyObject.transform.GetChild(0).gameObject.hideFlags & hidden, Is.EqualTo(hidden)); + } + + [Test] + public void InspectorEdit_ShowsUpRightAway() + { + WireframeSphere sphere = Add(); + SerializedObject serialized = new(sphere); + + serialized.FindProperty("_radius").floatValue = 2f; + serialized.ApplyModifiedProperties(); + + Assert.That(((Sphere)sphere.Shape).Radius, Is.EqualTo(2f)); + Assert.That(sphere.IsDeferred, Is.False); + } + + [Test] + public void InspectorCountEdit_IsAppliedOnTheEditorsNextUpdate() + { + WireframeSphere sphere = Add(); + SerializedObject serialized = new(sphere); + + serialized.FindProperty("_segmentCount").intValue = 8; + serialized.ApplyModifiedProperties(); + + // Creating the shape again may create objects, which OnValidate forbids. + Assert.That(sphere.Shape.VertexCount, Is.EqualTo(96)); + Assert.That(sphere.IsDeferred, Is.True); + + ComponentRefresh.Update(); + + Assert.That(sphere.Shape.VertexCount, Is.EqualTo(24)); + Assert.That(sphere.IsDeferred, Is.False); + } + + [Test] + public void InspectorCountEdit_IsCorrectedToTheNearestValidCount() + { + WireframeCylinder cylinder = Add(); + SerializedObject serialized = new(cylinder); + + serialized.FindProperty("_segmentCount").intValue = 18; + serialized.ApplyModifiedProperties(); + ComponentRefresh.Update(); + + Assert.That(cylinder.SegmentCount, Is.EqualTo(20)); + Assert.That(((Cylinder)cylinder.Shape).SegmentCount, Is.EqualTo(20)); + } + + [Test] + public void Undo_BringsTheShapeBack() + { + WireframeSphere sphere = Add(); + Undo.RecordObject(sphere, "Change Radius"); + sphere.Radius = 3f; + Undo.FlushUndoRecordObjects(); + + Undo.PerformUndo(); + + Assert.That(sphere.Radius, Is.EqualTo(0.5f)); + Assert.That(((Sphere)sphere.Shape).Radius, Is.EqualTo(0.5f)); + } + + [Test] + public void EditInEditMode_AsksForARepaint() + { + WireframeSphere sphere = Add(); + ShapeComponents.TakeRepaintRequest(); + + sphere.Radius = 2f; + + Assert.That(ShapeComponents.TakeRepaintRequest(), Is.True); + Assert.That(ShapeComponents.TakeRepaintRequest(), Is.False); + } + + [Test] + public void GizmoShape_IsLeftOutOfCamerasThatDrawNoGizmos() + { + WireframeLine line = AddRedLineAcrossTheCenter(); + line.DrawAsGizmo = true; + + // A camera rendered on its own, outside any Scene or Game view, draws no gizmos. + Assert.That(CenterProbe.RedAtCenter(), Is.LessThan(0.1f)); + Assert.That(line.SharedContainer.Container.Proxy.Chunks[0].Renderer.forceRenderingOff, Is.True); + + GizmoCameras.Filter = camera => true; + + Assert.That(CenterProbe.RedAtCenter(), Is.GreaterThan(0.5f)); + Assert.That(line.SharedContainer.Container.Proxy.Chunks[0].Renderer.forceRenderingOff, Is.True); + } + + [Test] + public void LayerChange_MovesTheShapeToAContainerOnThatLayer() + { + WireframeBox box = Add(); + WireframeContainer before = box.SharedContainer.Container; + + box.gameObject.layer = 5; + ShapeComponents.RefreshLayers(); + + Assert.That(box.SharedContainer.Key.Layer, Is.EqualTo(5)); + Assert.That(box.SharedContainer.Container.Proxy.Chunks[0].Renderer.gameObject.layer, Is.EqualTo(5)); + Assert.That(before.IsDisposed, Is.True); + } + + [Test] + public void ScriptReload_LetsComponentsCreateTheirShapesAgain() + { + WireframeSphere sphere = Add(); + WireframeContainer before = sphere.SharedContainer.Container; + + EditModeLifecycle.DisposeEveryContainer(); + Assert.That(sphere.Shape.IsDisposed, Is.True); + sphere.Radius = 1f; + + Assert.That(sphere.Shape.IsDisposed, Is.False); + Assert.That(sphere.SharedContainer.Container, Is.Not.SameAs(before)); + Assert.That(((Sphere)sphere.Shape).Radius, Is.EqualTo(1f)); + } + + [Test] + public void DestroyingTheLastComponent_DisposesItsContainerRightAway() + { + WireframeSphere sphere = Add(); + WireframeContainer container = sphere.SharedContainer.Container; + GameObject proxyObject = container.Proxy.gameObject; + + Object.DestroyImmediate(sphere.gameObject); + + Assert.That(container.IsDisposed, Is.True); + Assert.That(proxyObject == null, Is.True); + } + + [Test] + public void ClosingTheScene_DisposesTheContainerOfItsComponents() + { + WireframeSphere sphere = Add(); + WireframeContainer container = sphere.SharedContainer.Container; + + EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Single); + + Assert.That(container.IsDisposed, Is.True); + } + + [Test] + public void BoneOutsideAScene_IsReportedOnceAndTheGameObjectFollowedInstead() + { + GameObject source = Track(new GameObject("Bone Asset")); + GameObject asset = PrefabUtility.SaveAsPrefabAsset(source, PrefabPath); + WireframeLine line = Add(); + + LogAssert.Expect(LogType.Warning, new Regex("isn't in a scene")); + line.BoneB = asset.transform; + line.PositionB = Vector3.up; + + Line shape = (Line)line.Shape; + Assert.That(shape.BoneB, Is.SameAs(line.transform)); + Assert.That(shape.LocalPositionB, Is.EqualTo(Vector3.up)); + } + + [Test] + public void PrefabMode_DrawsComponentsInTheStagesOwnScene() + { + GameObject source = Track(new GameObject("Prefab Source")); + source.AddComponent(); + PrefabUtility.SaveAsPrefabAsset(source, PrefabPath); + Object.DestroyImmediate(source); + + PrefabStage stage = PrefabStageUtility.OpenPrefab(PrefabPath); + WireframeSphere sphere = stage.prefabContentsRoot.GetComponent(); + WireframeContainer container = sphere.SharedContainer.Container; + + Assert.That(sphere.SharedContainer.Key.Stage, Is.EqualTo(stage.scene)); + Assert.That(container.Proxy.gameObject.scene, Is.EqualTo(stage.scene)); + + StageUtility.GoToMainStage(); + + Assert.That(container.IsDisposed, Is.True); + } + + private T Add() where T : WireframeShape + { + return Track(new GameObject(typeof(T).Name)).AddComponent(); + } + + /// A red line from (-2, 0, 0) to (2, 0, 0), across the center that reads. + private WireframeLine AddRedLineAcrossTheCenter() + { + WireframeLine line = Add(); + line.Color = Color.red; + line.PositionA = new Vector3(-2f, 0f, 0f); + line.PositionB = new Vector3(2f, 0f, 0f); + return line; + } + + private T Track(T target) where T : Object + { + _objects.Add(target); + return target; + } + } +} diff --git a/UnityPackage/Tests/Editor/ComponentEditModeTests.cs.meta b/UnityPackage/Tests/Editor/ComponentEditModeTests.cs.meta new file mode 100644 index 0000000..d56f178 --- /dev/null +++ b/UnityPackage/Tests/Editor/ComponentEditModeTests.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 76d0636b54bb3b04940ea87bde19d8ca diff --git a/UnityPackage/Tests/Editor/EditModeTests.cs b/UnityPackage/Tests/Editor/EditModeTests.cs index 8b0d5f7..c021098 100644 --- a/UnityPackage/Tests/Editor/EditModeTests.cs +++ b/UnityPackage/Tests/Editor/EditModeTests.cs @@ -15,8 +15,6 @@ namespace reromanlee.Wireframes.Tests public class EditModeTests { private const string ScenePath = "Assets/WireframesEditModeTest.unity"; - private const int Size = 32; - private const float HalfExtent = 4f; private readonly List _containers = new(); private readonly List _objects = new(); @@ -69,7 +67,7 @@ public void Container_DrawsWhenACameraRendersAndItsBoneTextureGrows() { WireframeContainer container = CreateContainer(); container.CreateLine(new Vector3(-2f, 0f, 0f), new Vector3(2f, 0f, 0f)).SetColor(Color.red); - Assert.That(RedAtCenter(), Is.GreaterThan(0.5f)); + Assert.That(CenterProbe.RedAtCenter(), Is.GreaterThan(0.5f)); // More bones than one texture row holds, so the texture is replaced during the next render. for (int i = 0; i < BoneTexture.BonesPerRow + 1; i++) @@ -79,7 +77,7 @@ public void Container_DrawsWhenACameraRendersAndItsBoneTextureGrows() container.CreateLine(bone, bone); } - Assert.That(RedAtCenter(), Is.GreaterThan(0.5f)); + Assert.That(CenterProbe.RedAtCenter(), Is.GreaterThan(0.5f)); Assert.That(container.Proxy.BoneTexture.Texture.height, Is.EqualTo(2)); } @@ -106,7 +104,7 @@ public void PersistentContainer_SurvivesItsSceneClosingAndKeepsDrawing() EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Single); Assert.That(container.IsDisposed, Is.False); - Assert.That(RedAtCenter(), Is.GreaterThan(0.5f)); + Assert.That(CenterProbe.RedAtCenter(), Is.GreaterThan(0.5f)); } [Test] @@ -207,38 +205,5 @@ private T Track(T target) where T : Object _objects.Add(target); return target; } - - /// Renders the world around the origin with a camera and returns the red at the center. - private float RedAtCenter() - { - RenderTexture target = Track(new RenderTexture(Size, Size, 24)); - Texture2D readback = Track(new Texture2D(Size, Size, TextureFormat.RGBA32, false)); - Camera camera = Track(new GameObject("Camera")).AddComponent(); - camera.enabled = false; - camera.allowMSAA = false; - camera.orthographic = true; - camera.orthographicSize = HalfExtent; - camera.transform.position = new Vector3(0f, 0f, -10f); - camera.clearFlags = CameraClearFlags.SolidColor; - camera.backgroundColor = Color.black; - camera.targetTexture = target; - camera.Render(); - camera.targetTexture = null; - - RenderTexture previous = RenderTexture.active; - RenderTexture.active = target; - readback.ReadPixels(new Rect(0f, 0f, Size, Size), 0, 0); - readback.Apply(); - RenderTexture.active = previous; - float red = 0f; - for (int y = Size / 2 - 1; y <= Size / 2; y++) - { - for (int x = Size / 2 - 1; x <= Size / 2; x++) - { - red = Mathf.Max(red, readback.GetPixel(x, y).r); - } - } - return red; - } } } diff --git a/UnityPackage/Tests/Runtime/ShapeComponentTests.cs b/UnityPackage/Tests/Runtime/ShapeComponentTests.cs new file mode 100644 index 0000000..1516a35 --- /dev/null +++ b/UnityPackage/Tests/Runtime/ShapeComponentTests.cs @@ -0,0 +1,481 @@ +using System; +using NUnit.Framework; +using UnityEngine; +using UnityEngine.TestTools.Constraints; +using Is = UnityEngine.TestTools.Constraints.Is; +using Object = UnityEngine.Object; + +namespace reromanlee.Wireframes.Tests +{ + /// + /// Shape components: drawn as soon as they are added, kept in step with their fields and GameObjects, hidden when + /// disabled, and sharing containers by their settings. + /// + public class ShapeComponentTests : WireframesTestBase + { + [Test] + public void AddedComponent_DrawsItsShapeRightAway() + { + WireframeSphere sphere = AddShape(new Vector3(1f, 2f, 3f)); + + Assert.That(sphere.Shape, Is.Not.Null); + Assert.That(sphere.Shape.IsSuspended, Is.False); + Assert.That(ShapeCountOf(sphere.SharedContainer.Container), Is.EqualTo(1)); + // The first circle starts on +Z, half a unit from the center. + AssertApproximately(new Vector3(1f, 2f, 3.5f), Bake(sphere)[0]); + } + + [Test] + public void ComponentsWithTheSameSettings_ShareAContainer() + { + int containerCount = SharedContainers.Count; + WireframeSphere first = AddShape(); + WireframeBox second = AddShape(); + WireframeLine onTop = AddShape(); + onTop.Occlusion = WireframeOcclusion.Show; + WireframeCircle transparent = AddShape(); + transparent.Color = new Color(1f, 1f, 1f, 0.5f); + WireframeStar layered = AddShape(Vector3.zero, 5); + + Assert.That(second.SharedContainer, Is.SameAs(first.SharedContainer)); + Assert.That(onTop.SharedContainer.Key.Occlusion, Is.EqualTo(WireframeOcclusion.Show)); + Assert.That(transparent.SharedContainer.Key.UsesAlpha, Is.True); + Assert.That(layered.SharedContainer.Key.Layer, Is.EqualTo(5)); + Assert.That(SharedContainers.Count, Is.EqualTo(containerCount + 4)); + + WireframeContainer shared = first.SharedContainer.Container; + Object.DestroyImmediate(first.gameObject); + Assert.That(shared.IsDisposed, Is.False); + Object.DestroyImmediate(second.gameObject); + Assert.That(shared.IsDisposed, Is.True); + Assert.That(SharedContainers.Count, Is.EqualTo(containerCount + 3)); + } + + [Test] + public void PropertyEdits_ShowUpInTheShape() + { + WireframeSphere sphere = AddShape(); + + sphere.Radius = 2f; + sphere.Center = new Vector3(0f, 1f, 0f); + sphere.EulerAngles = new Vector3(90f, 0f, 0f); + + // Turning 90 degrees around X takes the first circle's start from +Z to -Y. + AssertApproximately(new Vector3(0f, -1f, 0f), Bake(sphere)[0]); + AssertApproximately(Quaternion.Euler(90f, 0f, 0f), sphere.Rotation); + } + + [Test] + public void Shape_FollowsItsGameObject() + { + WireframeCircle circle = AddShape(); + + circle.transform.SetPositionAndRotation(new Vector3(0f, 0f, 5f), Quaternion.Euler(0f, 90f, 0f)); + circle.transform.localScale = Vector3.one * 2f; + + // The circle starts on its +Z, half a unit out, which the GameObject scales to 1 and turns to +X. + AssertApproximately(new Vector3(1f, 0f, 5f), Bake(circle)[0]); + } + + [Test] + public void DisabledComponent_GivesBackItsShapeAndComesBackAsItIsThen() + { + WireframeSphere sphere = AddShape(); + WireframeContainer container = sphere.SharedContainer.Container; + + sphere.enabled = false; + sphere.Radius = 3f; + + Assert.That(sphere.Shape.IsSuspended, Is.True); + Assert.That(ShapeCountOf(container), Is.Zero); + Assert.That(container.Proxy.Bones.Count, Is.Zero); + + sphere.enabled = true; + + Assert.That(ShapeCountOf(container), Is.EqualTo(1)); + AssertApproximately(new Vector3(0f, 0f, 3f), Bake(sphere)[0]); + } + + [Test] + public void DeactivatedGameObject_HidesTheShapeToo() + { + WireframeBox box = AddShape(); + + box.gameObject.SetActive(false); + Assert.That(box.Shape.IsSuspended, Is.True); + + box.gameObject.SetActive(true); + Assert.That(box.Shape.IsSuspended, Is.False); + } + + [Test] + public void TogglingAComponent_AllocatesNothing() + { + WireframeSphere sphere = AddShape(); + WireframeContainer container = sphere.SharedContainer.Container; + // Warms up every path once. + sphere.enabled = false; + sphere.enabled = true; + container.Proxy.Flush(); + + Assert.That(() => + { + sphere.enabled = false; + container.Proxy.Flush(); + sphere.enabled = true; + container.Proxy.Flush(); + }, Is.Not.AllocatingGCMemory()); + } + + [Test] + public void PropertyEdits_AllocateNothing() + { + WireframeCapsule capsule = AddShape(); + WireframeContainer container = capsule.SharedContainer.Container; + capsule.RadiusA = 0.2f; + container.Proxy.Flush(); + + Assert.That(() => + { + capsule.RadiusA = 0.3f; + capsule.EndB = new Vector3(0f, 2f, 0f); + capsule.Roll = 45f; + capsule.Color = Color.red; + container.Proxy.Flush(); + }, Is.Not.AllocatingGCMemory()); + } + + [Test] + public void SteadyFramesWithMovingComponents_AllocateNothing() + { + WireframeShape[] shapes = + { + AddShape(), AddShape(), AddShape(), + AddShape() + }; + WireframeContainer container = shapes[0].SharedContainer.Container; + container.Proxy.Flush(); + + Assert.That(() => + { + foreach (WireframeShape shape in shapes) + { + shape.transform.Translate(0.1f, 0f, 0f); + } + container.Proxy.Flush(); + }, Is.Not.AllocatingGCMemory()); + } + + [Test] + public void TransparentColor_MovesTheShapeToATransparentContainerAndBack() + { + WireframeCircle circle = AddShape(); + WireframeContainer opaque = circle.SharedContainer.Container; + + circle.Color = new Color(0f, 1f, 0f, 0.5f); + + Assert.That(circle.SharedContainer.Key.UsesAlpha, Is.True); + Assert.That(ShapeCountOf(circle.SharedContainer.Container), Is.EqualTo(1)); + Assert.That(opaque.IsDisposed, Is.True); + + circle.Color = Color.green; + + Assert.That(circle.SharedContainer.Key.UsesAlpha, Is.False); + Assert.That(ShapeCountOf(circle.SharedContainer.Container), Is.EqualTo(1)); + } + + [Test] + public void ChangingACount_CreatesTheShapeAgain() + { + WireframeSphere sphere = AddShape(); + Shape before = sphere.Shape; + + sphere.SegmentCount = 8; + + Assert.That(sphere.Shape, Is.Not.SameAs(before)); + Assert.That(before.IsDisposed, Is.True); + Assert.That(sphere.Shape.VertexCount, Is.EqualTo(24)); + Assert.That(ShapeCountOf(sphere.SharedContainer.Container), Is.EqualTo(1)); + } + + [Test] + public void InvalidValues_ThrowAndChangeNothing() + { + WireframeCylinder cylinder = AddShape(); + WireframeSpikedSphere spikedSphere = AddShape(); + WireframeStar star = AddShape(); + WireframeFrustum frustum = AddShape(); + WireframeCircle circle = AddShape(); + WireframePolyline polyline = AddShape(); + + Assert.Throws(() => cylinder.SegmentCount = 30); + Assert.Throws(() => spikedSphere.SpikeCount = 5); + Assert.Throws(() => star.PointCount = 2); + Assert.Throws(() => frustum.SideCount = 1025); + Assert.Throws(() => circle.SegmentCount = 2); + Assert.Throws(() => circle.Occlusion = (WireframeOcclusion)7); + Assert.Throws(() => polyline.PointCount = -1); + + Assert.That(cylinder.SegmentCount, Is.EqualTo(32)); + Assert.That(spikedSphere.SpikeCount, Is.EqualTo(12)); + Assert.That(circle.Occlusion, Is.EqualTo(WireframeOcclusion.Hide)); + Assert.That(polyline.PointCount, Is.EqualTo(3)); + } + + [Test] + public void SerializedCounts_AreCorrectedToTheNearestValidOne() + { + Assert.That(ShapeCounts.ClampSegmentCount(2), Is.EqualTo(3)); + Assert.That(ShapeCounts.ClampSegmentCount(5000), Is.EqualTo(1024)); + Assert.That(ShapeCounts.ClampQuarterSegmentCount(0), Is.EqualTo(4)); + Assert.That(ShapeCounts.ClampQuarterSegmentCount(33), Is.EqualTo(32)); + Assert.That(ShapeCounts.ClampQuarterSegmentCount(34), Is.EqualTo(36)); + Assert.That(ShapeCounts.ClampQuarterSegmentCount(5000), Is.EqualTo(1024)); + Assert.That(ShapeCounts.ClampPointCount(600), Is.EqualTo(512)); + Assert.That(ShapeCounts.ClampSideCount(0), Is.EqualTo(3)); + Assert.That(ShapeCounts.ClampSpikeCount(1), Is.EqualTo(4)); + Assert.That(ShapeCounts.ClampSpikeCount(7), Is.EqualTo(6)); + Assert.That(ShapeCounts.ClampSpikeCount(11), Is.EqualTo(12)); + Assert.That(ShapeCounts.ClampSpikeCount(100), Is.EqualTo(20)); + } + + [Test] + public void CenteredComponents_DrawWhatTheirCreateMethodsDraw() + { + WireframeContainer container = CreateContainer(); + Vector3 center = new(1f, 2f, 3f); + Vector3 eulerAngles = new(10f, 20f, 30f); + Quaternion rotation = Quaternion.Euler(eulerAngles); + + WireframeBox box = Centered(center, eulerAngles); + box.Size = new Vector3(1f, 2f, 3f); + AssertSameVertices(container, container.CreateBox(center, rotation, box.Size), box); + + WireframeRectangle rectangle = Centered(center, eulerAngles); + rectangle.Size = new Vector2(2f, 1f); + AssertSameVertices(container, container.CreateRectangle(center, rotation, rectangle.Size), rectangle); + + WireframeRoundedRectangle rounded = Centered(center, eulerAngles); + rounded.Size = new Vector2(2f, 1f); + rounded.CornerRadius = 0.3f; + rounded.SegmentCount = 16; + AssertSameVertices( + container, container.CreateRoundedRectangle(center, rotation, rounded.Size, 0.3f, 16), rounded); + + WireframeCircle circle = Centered(center, eulerAngles); + circle.Radius = 1.5f; + circle.SegmentCount = 12; + AssertSameVertices(container, container.CreateCircle(center, rotation, 1.5f, 12), circle); + + WireframeEllipse ellipse = Centered(center, eulerAngles); + ellipse.Radii = new Vector2(0.5f, 1f); + ellipse.SegmentCount = 10; + AssertSameVertices(container, container.CreateEllipse(center, rotation, ellipse.Radii, 10), ellipse); + + WireframeStar star = Centered(center, eulerAngles); + star.InnerRadius = 0.3f; + star.OuterRadius = 1f; + star.PointCount = 6; + AssertSameVertices(container, container.CreateStar(center, rotation, 0.3f, 1f, 6), star); + + WireframeSphere sphere = Centered(center, eulerAngles); + sphere.Radius = 1.2f; + sphere.SegmentCount = 9; + AssertSameVertices(container, container.CreateSphere(center, rotation, 1.2f, 9), sphere); + + WireframeEllipsoid ellipsoid = Centered(center, eulerAngles); + ellipsoid.Radii = new Vector3(0.5f, 1f, 1.5f); + ellipsoid.SegmentCount = 8; + AssertSameVertices(container, container.CreateEllipsoid(center, rotation, ellipsoid.Radii, 8), ellipsoid); + + WireframeSpikedSphere spikedSphere = Centered(center, eulerAngles); + spikedSphere.BaseRadius = 0.4f; + spikedSphere.SpikeLength = 0.6f; + spikedSphere.SpikeCount = 8; + AssertSameVertices(container, container.CreateSpikedSphere(center, rotation, 0.4f, 0.6f, 8), spikedSphere); + } + + [Test] + public void AxialComponents_DrawWhatTheirCreateMethodsDraw() + { + WireframeContainer container = CreateContainer(); + Vector3 endA = new(1f, 0f, 2f); + Vector3 endB = new(2f, 3f, 1f); + + WireframeCylinder cylinder = Axial(endA, endB); + cylinder.Radius = 0.7f; + cylinder.SegmentCount = 12; + AssertSameVertices(container, container.CreateCylinder(endA, endB, 0.7f, 12), cylinder); + + WireframeCone cone = Axial(endA, endB); + cone.Radius = 0.7f; + cone.SegmentCount = 8; + AssertSameVertices(container, container.CreateCone(endA, endB, 0.7f, 8), cone); + + WireframeCapsule capsule = Axial(endA, endB); + capsule.RadiusA = 0.4f; + capsule.RadiusB = 0.6f; + capsule.SegmentCount = 16; + AssertSameVertices(container, container.CreateCapsule(endA, endB, 0.4f, 0.6f, 16), capsule); + + WireframeStadium stadium = Axial(endA, endB); + stadium.RadiusA = 0.4f; + stadium.RadiusB = 0.6f; + stadium.SegmentCount = 16; + AssertSameVertices(container, container.CreateStadium(endA, endB, 0.4f, 0.6f, 16), stadium); + + WireframeFrustum frustum = Axial(endA, endB); + frustum.RadiusA = 0.4f; + frustum.RadiusB = 0.8f; + frustum.SideCount = 5; + AssertSameVertices(container, container.CreateFrustum(endA, endB, 0.4f, 0.8f, 5), frustum); + + WireframePyramid pyramid = Axial(endA, endB); + pyramid.BaseSize = new Vector2(1f, 0.5f); + AssertSameVertices(container, container.CreatePyramid(endA, endB, pyramid.BaseSize), pyramid); + } + + [Test] + public void Roll_TurnsTheShapeAroundItsAxis() + { + Vector3 endA = new(1f, 0f, 2f); + Vector3 endB = new(2f, 3f, 1f); + WireframeFrustum unrolled = Axial(endA, endB); + WireframeFrustum rolled = Axial(endA, endB); + + rolled.Roll = 90f; + + Quaternion roll = Quaternion.AngleAxis(90f, endB - endA); + Vector3[] expected = Bake(unrolled); + Vector3[] actual = Bake(rolled); + for (int i = 0; i < expected.Length; i++) + { + AssertApproximately(endA + roll * (expected[i] - endA), actual[i]); + } + } + + [Test] + public void Line_JoinsPointsOnTheirBonesOrItsGameObject() + { + WireframeLine line = AddShape(new Vector3(1f, 0f, 0f)); + Transform target = CreateBone(new Vector3(0f, 5f, 0f), Quaternion.identity); + + line.PositionA = new Vector3(0f, 1f, 0f); + line.BoneB = target; + line.PositionB = new Vector3(0f, 0f, 1f); + + Vector3[] vertices = Bake(line); + AssertApproximately(new Vector3(1f, 1f, 0f), vertices[0]); + AssertApproximately(new Vector3(0f, 5f, 1f), vertices[1]); + + target.position = new Vector3(0f, 6f, 0f); + + AssertApproximately(new Vector3(0f, 6f, 1f), Bake(line)[1]); + } + + [Test] + public void DestroyedBone_LeavesItsPointOnTheGameObjectInTheSameFlush() + { + WireframeLine line = AddShape(new Vector3(1f, 0f, 0f)); + Transform target = CreateBone(new Vector3(0f, 5f, 0f), Quaternion.identity); + line.BoneB = target; + line.PositionB = new Vector3(0f, 0f, 1f); + WireframeContainer container = line.SharedContainer.Container; + container.Proxy.Flush(); + + Object.DestroyImmediate(target.gameObject); + + // The flush that finds the bone destroyed moves the point before uploading anything. + AssertApproximately(new Vector3(1f, 0f, 1f), Bake(line)[1]); + Assert.That(container.Proxy.Bones.Count, Is.EqualTo(1)); + } + + [Test] + public void Polyline_DrawsNothingWithTooFewPoints() + { + WireframePolyline polyline = AddShape(); + Assert.That(polyline.Shape, Is.Not.Null); + + polyline.PointCount = 2; + + Assert.That(polyline.Shape, Is.Null); + + polyline.IsClosed = false; + + Assert.That(polyline.Shape, Is.Not.Null); + Assert.That(BakeEdges(polyline.SharedContainer.Container, polyline.Shape), Has.Length.EqualTo(1)); + } + + [Test] + public void Polyline_PointsFollowTheirBonesOrItsGameObject() + { + WireframePolyline polyline = AddShape(new Vector3(0f, 1f, 0f)); + Transform bone = CreateBone(new Vector3(3f, 0f, 0f), Quaternion.identity); + + polyline.PointCount = 4; + polyline.SetBone(3, bone); + polyline.SetPosition(3, new Vector3(0f, 0f, 1f)); + + Vector3[] vertices = Bake(polyline); + Assert.That(vertices, Has.Length.EqualTo(4)); + // The default triangle has a corner on +Z, half a unit from the GameObject. + AssertApproximately(new Vector3(0f, 1f, 0.5f), vertices[0]); + AssertApproximately(new Vector3(3f, 0f, 1f), vertices[3]); + Assert.That(polyline.GetBone(3), Is.SameAs(bone)); + Assert.Throws(() => polyline.SetPosition(4, Vector3.zero)); + } + + [Test] + public void GizmoShape_GoesIntoAContainerThatCamerasFilter() + { + WireframeSphere gizmo = AddShape(); + WireframeSphere plain = AddShape(); + + gizmo.DrawAsGizmo = true; + + Assert.That(gizmo.SharedContainer, Is.Not.SameAs(plain.SharedContainer)); + Assert.That(gizmo.SharedContainer.Container.Proxy.CameraFilter, Is.Not.Null); + Assert.That(ChunkOf(gizmo.SharedContainer.Container).Renderer.forceRenderingOff, Is.True); + Assert.That(ChunkOf(plain.SharedContainer.Container).Renderer.forceRenderingOff, Is.False); + } + + private T AddShape(Vector3 position = default, int layer = 0) where T : WireframeShape + { + GameObject owner = Track(new GameObject(typeof(T).Name) { layer = layer }); + owner.transform.position = position; + return owner.AddComponent(); + } + + private T Centered(Vector3 center, Vector3 eulerAngles) where T : WireframeCenteredShape + { + T shape = AddShape(); + shape.Center = center; + shape.EulerAngles = eulerAngles; + return shape; + } + + private T Axial(Vector3 endA, Vector3 endB) where T : WireframeAxialShape + { + T shape = AddShape(); + shape.EndA = endA; + shape.EndB = endB; + return shape; + } + + private static Vector3[] Bake(WireframeShape component) + { + return BakeShape(component.SharedContainer.Container, component.Shape); + } + + private static void AssertSameVertices(WireframeContainer container, IShape expected, WireframeShape actual) + { + Vector3[] expectedVertices = BakeShape(container, expected); + Vector3[] actualVertices = Bake(actual); + Assert.That(actualVertices, Has.Length.EqualTo(expectedVertices.Length), actual.GetType().Name); + for (int i = 0; i < expectedVertices.Length; i++) + { + AssertApproximately(expectedVertices[i], actualVertices[i]); + } + } + } +} diff --git a/UnityPackage/Tests/Runtime/ShapeComponentTests.cs.meta b/UnityPackage/Tests/Runtime/ShapeComponentTests.cs.meta new file mode 100644 index 0000000..4ed6905 --- /dev/null +++ b/UnityPackage/Tests/Runtime/ShapeComponentTests.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: aba8cb557b7f4af4c65ed9cd0e43eae7 diff --git a/UnityPackage/Tests/Runtime/ShapeTransferTests.cs b/UnityPackage/Tests/Runtime/ShapeTransferTests.cs new file mode 100644 index 0000000..e13b736 --- /dev/null +++ b/UnityPackage/Tests/Runtime/ShapeTransferTests.cs @@ -0,0 +1,119 @@ +using NUnit.Framework; +using UnityEngine; +using UnityEngine.TestTools.Constraints; +using Is = UnityEngine.TestTools.Constraints.Is; +using Object = UnityEngine.Object; + +namespace reromanlee.Wireframes.Tests +{ + /// Shapes taken out of their container without being disposed, and put back there or into another one. + public class ShapeTransferTests : WireframesTestBase + { + [Test] + public void SuspendedShape_GivesBackItsRoomAndResumesAsItWas() + { + WireframeContainer container = CreateContainer(); + Transform bone = CreateBone(new Vector3(1f, 2f, 3f), Quaternion.identity); + ISphere sphere = container.CreateSphere(bone, Vector3.zero, 1f, 8); + ILine kept = container.CreateLine(Vector3.zero, Vector3.one); + container.Proxy.Flush(); + Shape shape = (Shape)sphere; + + shape.Suspend(); + container.Proxy.Flush(); + + Assert.That(shape.IsSuspended, Is.True); + Assert.That(sphere.IsDisposed, Is.False); + Assert.That(ShapeCountOf(container), Is.EqualTo(1)); + Assert.That(container.Proxy.Bones.Count, Is.Zero); + Assert.That(ChunkOf(kept).Mesh.GetIndices(0), Has.Length.EqualTo(2)); + + shape.Resume(container.Proxy); + + Assert.That(shape.IsSuspended, Is.False); + Assert.That(sphere.Radius, Is.EqualTo(1f)); + // The first circle starts on the sphere's +Z. + AssertApproximately(new Vector3(1f, 2f, 4f), BakeShape(container, sphere)[0]); + Assert.That(ChunkOf(kept).Mesh.GetIndices(0), Has.Length.EqualTo(2 + 24 * 2)); + } + + [Test] + public void ResumingInAnotherContainer_MovesTheShapeThere() + { + WireframeContainer first = CreateContainer(); + WireframeContainer second = CreateContainer(); + Transform bone = CreateBone(new Vector3(0f, 1f, 0f), Quaternion.identity); + ICircle circle = first.CreateCircle(bone, Vector3.zero, 2f, 16); + Shape shape = (Shape)circle; + + shape.Suspend(); + shape.Resume(second.Proxy); + + Assert.That(ShapeCountOf(first), Is.Zero); + Assert.That(ShapeCountOf(second), Is.EqualTo(1)); + Assert.That(first.Proxy.Bones.Count, Is.Zero); + Assert.That(second.Proxy.Bones.Count, Is.EqualTo(1)); + AssertApproximately(new Vector3(0f, 1f, 2f), BakeShape(second, circle)[0]); + } + + [Test] + public void SuspendingAndResuming_AllocatesNothing() + { + WireframeContainer first = CreateContainer(); + WireframeContainer second = CreateContainer(); + Transform bone = CreateBone(Vector3.zero, Quaternion.identity); + Shape shape = (Shape)first.CreateSphere(bone, Vector3.zero, 1f); + first.Proxy.Flush(); + + void MoveThereAndBack() + { + shape.Suspend(); + shape.Resume(second.Proxy); + second.Proxy.Flush(); + shape.Suspend(); + shape.Resume(first.Proxy); + first.Proxy.Flush(); + } + + // Warms up every path, and lets each list grow once to what a round trip needs between flushes. + MoveThereAndBack(); + MoveThereAndBack(); + + Assert.That(MoveThereAndBack, Is.Not.AllocatingGCMemory()); + } + + [Test] + public void DisposingASuspendedShape_ReleasesItsEdgePattern() + { + WireframeContainer container = CreateContainer(); + // A segment count no other test uses, so no other shape shares the pattern. + ICircle circle = container.CreateCircle(Vector3.zero, 1f, 7); + Shape shape = (Shape)circle; + shape.Suspend(); + + Assert.That(Ring.Patterns.Contains(7), Is.True); + + circle.Dispose(); + + Assert.That(circle.IsDisposed, Is.True); + Assert.That(shape.IsSuspended, Is.False); + Assert.That(Ring.Patterns.Contains(7), Is.False); + } + + [Test] + public void BoneDestroyedWhileSuspended_LeavesTheShapeInWorldSpaceOnceResumed() + { + WireframeContainer container = CreateContainer(); + Transform bone = CreateBone(new Vector3(5f, 0f, 0f), Quaternion.identity); + ICircle circle = container.CreateCircle(bone, Vector3.zero, 1f); + Shape shape = (Shape)circle; + shape.Suspend(); + + Object.DestroyImmediate(bone.gameObject); + shape.Resume(container.Proxy); + + Assert.That(circle.Bone, Is.Null); + Assert.That(container.Proxy.Bones.Count, Is.Zero); + } + } +} diff --git a/UnityPackage/Tests/Runtime/ShapeTransferTests.cs.meta b/UnityPackage/Tests/Runtime/ShapeTransferTests.cs.meta new file mode 100644 index 0000000..5129907 --- /dev/null +++ b/UnityPackage/Tests/Runtime/ShapeTransferTests.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 5a091616d2dc278e24fe2541fd2deb58 diff --git a/UnityPackage/package.json b/UnityPackage/package.json index acbe795..e35daa5 100644 --- a/UnityPackage/package.json +++ b/UnityPackage/package.json @@ -2,7 +2,7 @@ "name": "com.reromanlee.wireframes", "version": "2.0.0", "displayName": "Wireframes", - "description": "Wireframe shapes that follow Transforms through GPU skinning: lines, polylines, boxes, circles, spheres, capsules, cones and more. Create a shape once and it moves with its bones on the GPU, with no per-frame work from your code, in Play and Edit Mode, under the Built-in Render Pipeline, URP and HDRP.", + "description": "Wireframe shapes that follow Transforms through GPU skinning: lines, polylines, boxes, circles, spheres, capsules, cones and more. Create a shape once and it moves with its bones on the GPU, with no per-frame work from your code, in Play and Edit Mode, under the Built-in Render Pipeline, URP and HDRP. Shape components draw them on GameObjects without code, edited live in Edit Mode.", "unity": "2022.3", "license": "MIT", "documentationUrl": "https://github.com/reromanlee/Wireframes#readme", @@ -26,7 +26,7 @@ "samples": [ { "displayName": "Shape Gallery", - "description": "A scene with every shape the package draws, laid out in rows and turning with their bones.", + "description": "Every shape the package draws, laid out in rows: one scene creates them from code and turns their bones, another is made of shape components that draw in Edit Mode.", "path": "Samples~/ShapeGallery" }, { diff --git a/UnityProject/Assets/Editor/PlayerBuild.cs b/UnityProject/Assets/Editor/PlayerBuild.cs index f4b97b5..bc017a2 100644 --- a/UnityProject/Assets/Editor/PlayerBuild.cs +++ b/UnityProject/Assets/Editor/PlayerBuild.cs @@ -7,19 +7,28 @@ namespace reromanlee.Wireframes.ContinuousIntegration { /// - /// Builds the Shape Gallery sample for the active build target. Continuous integration copies the package samples - /// into Assets/Samples, then GameCI's builder calls with -executeMethod. + /// Builds the Shape Gallery sample for the active build target, starting in its ShapeGallery scene, with its + /// ComponentGallery scene too. Continuous integration copies the package samples into Assets/Samples, then GameCI's + /// builder calls with -executeMethod. /// public static class PlayerBuild { - private const string ScenePath = "Assets/Samples/ShapeGallery/ShapeGallery.unity"; + private static readonly string[] ScenePaths = + { + "Assets/Samples/ShapeGallery/ShapeGallery.unity", + "Assets/Samples/ShapeGallery/ComponentGallery.unity" + }; /// Builds to the path in GameCI's -customBuildPath argument; throwing makes Unity exit with code 1. public static void Build() { - if (!File.Exists(ScenePath)) + foreach (string scenePath in ScenePaths) { - throw new BuildFailedException($"{ScenePath} is missing; copy the package samples into Assets/Samples."); + if (!File.Exists(scenePath)) + { + throw new BuildFailedException( + $"{scenePath} is missing; copy the package samples into Assets/Samples."); + } } BuildTarget target = EditorUserBuildSettings.activeBuildTarget; if (target == BuildTarget.Android) @@ -31,7 +40,7 @@ public static void Build() BuildReport report = BuildPipeline.BuildPlayer(new BuildPlayerOptions { - scenes = new[] { ScenePath }, + scenes = ScenePaths, target = target, targetGroup = BuildPipeline.GetBuildTargetGroup(target), locationPathName = ValueOf("-customBuildPath")