diff --git a/.gitattributes b/.gitattributes
index e69de29..176a458 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -0,0 +1 @@
+* text=auto
diff --git a/.github/RELEASE_CHECKLIST.md b/.github/RELEASE_CHECKLIST.md
new file mode 100644
index 0000000..e9230d8
--- /dev/null
+++ b/.github/RELEASE_CHECKLIST.md
@@ -0,0 +1,19 @@
+# Release checklist
+
+Continuous integration checks what a machine without a GPU can: the tests on the oldest and newest supported Unity, drawn by a software renderer, and player builds for every platform. The rest is checked by hand before each release, most important first.
+
+### Before merging the release pull request
+
+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`.
+
+### After merging
+
+1. Tag the squashed commit on `main` with the version alone, such as `2.0.0`.
+2. Export the signed tarball of the package from the Package Manager.
+3. Draft the GitHub release from the tag, named like `v2.0.0 (signed)`, with the changelog section as its notes and the tarball attached.
diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml
new file mode 100644
index 0000000..7eb3b23
--- /dev/null
+++ b/.github/workflows/continuous-integration.yml
@@ -0,0 +1,164 @@
+name: Continuous integration
+
+on:
+ pull_request:
+ paths-ignore:
+ - '**.md'
+ - '**.jpg'
+ - '**.png'
+ push:
+ branches:
+ - main
+ paths-ignore:
+ - '**.md'
+ - '**.jpg'
+ - '**.png'
+ workflow_dispatch:
+
+# A newer push to the same branch or pull request replaces the run in progress.
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
+permissions:
+ contents: read
+ # The test runner reports its results as a check run.
+ checks: write
+
+# A Personal license needs UNITY_LICENSE, UNITY_EMAIL and UNITY_PASSWORD; a Pro license UNITY_SERIAL instead of the first.
+env:
+ UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }}
+ UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }}
+ UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }}
+ UNITY_SERIAL: ${{ secrets.UNITY_SERIAL }}
+
+jobs:
+ tests:
+ name: Tests on ${{ matrix.unityVersion }}
+ runs-on: ubuntu-latest
+ timeout-minutes: 60
+ strategy:
+ fail-fast: false
+ matrix:
+ include:
+ - unityVersion: 2022.3.62f3
+ # Also compiles and tests the Profiler counters, which exist only with this optional package.
+ - unityVersion: 6000.5.11f1
+ optionalPackages: '{"com.unity.profiling.core": "1.0.3"}'
+ steps:
+ - name: Free disk space for the Unity image
+ uses: jlumbroso/free-disk-space@ceedf095f4ec1a097402bc6bd80831f2e1a6fde6 # v2.0.0
+ with:
+ large-packages: false
+
+ - name: Check out the repository
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+
+ # Without it, Unity takes the project for a new one and adds its template packages to the manifest.
+ - name: Set the Unity version of the project
+ env:
+ UNITY_VERSION: ${{ matrix.unityVersion }}
+ run: |
+ mkdir -p UnityProject/ProjectSettings
+ printf 'm_EditorVersion: %s\n' "$UNITY_VERSION" > UnityProject/ProjectSettings/ProjectVersion.txt
+
+ - name: Add the optional packages
+ if: matrix.optionalPackages
+ env:
+ OPTIONAL_PACKAGES: ${{ matrix.optionalPackages }}
+ run: |
+ manifest=UnityProject/Packages/manifest.json
+ jq --argjson packages "$OPTIONAL_PACKAGES" '.dependencies += $packages' "$manifest" > manifest.tmp
+ mv manifest.tmp "$manifest"
+
+ - name: Restore the Library folder
+ uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
+ with:
+ path: UnityProject/Library
+ key: Library-tests-${{ matrix.unityVersion }}-${{ hashFiles('UnityPackage/**', 'UnityProject/**') }}
+ restore-keys: Library-tests-${{ matrix.unityVersion }}-
+
+ - name: Run the edit and play mode tests
+ uses: game-ci/unity-test-runner@32e57712352b500e17974b245a6dce9e11a73213 # v4.4.0
+ env:
+ # Code coverage crashes play mode tests on Unity 6, see game-ci/unity-test-runner#301. The action's
+ # coverageEnabled input passes --no-coverageEnabled, which the CLI rejects, so it's turned off here instead.
+ GAME_CI_COVERAGE_ENABLED: false
+ with:
+ unityVersion: ${{ matrix.unityVersion }}
+ cliVersion: v0.1.69
+ projectPath: UnityProject
+ testMode: all
+ githubToken: ${{ github.token }}
+ checkName: Test results on ${{ matrix.unityVersion }}
+
+ - name: Check that the package compiles without warnings
+ if: always()
+ run: |
+ warnings=$(grep -hs "warning CS" artifacts/*.log | grep -e "com.reromanlee.wireframes" -e "UnityPackage" | sort -u || true)
+ if [ -n "$warnings" ]; then
+ echo "$warnings"
+ echo "::error::The package compiled with warnings."
+ exit 1
+ fi
+
+ - name: Upload the test results and timings
+ if: always()
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
+ with:
+ name: Test results on ${{ matrix.unityVersion }}
+ path: artifacts
+
+ builds:
+ name: ${{ matrix.targetPlatform }} build on ${{ matrix.unityVersion }}
+ runs-on: ubuntu-latest
+ timeout-minutes: 90
+ strategy:
+ fail-fast: false
+ matrix:
+ include:
+ # Tier 1 platforms on the oldest and the newest supported Unity.
+ - { unityVersion: 2022.3.62f3, targetPlatform: WebGL }
+ - { unityVersion: 2022.3.62f3, targetPlatform: Android }
+ - { unityVersion: 6000.5.11f1, targetPlatform: WebGL }
+ - { unityVersion: 6000.5.11f1, targetPlatform: Android }
+ # Tier 2 platforms, checked to build on the newest Unity.
+ - { unityVersion: 6000.5.11f1, targetPlatform: iOS }
+ - { unityVersion: 6000.5.11f1, targetPlatform: StandaloneOSX }
+ - { unityVersion: 6000.5.11f1, targetPlatform: StandaloneLinux64 }
+ steps:
+ - name: Free disk space for the Unity image
+ uses: jlumbroso/free-disk-space@ceedf095f4ec1a097402bc6bd80831f2e1a6fde6 # v2.0.0
+ with:
+ large-packages: false
+
+ - name: Check out the repository
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+
+ # Without it, Unity takes the project for a new one and adds its template packages to the manifest.
+ - name: Set the Unity version of the project
+ env:
+ UNITY_VERSION: ${{ matrix.unityVersion }}
+ run: |
+ mkdir -p UnityProject/ProjectSettings
+ printf 'm_EditorVersion: %s\n' "$UNITY_VERSION" > UnityProject/ProjectSettings/ProjectVersion.txt
+
+ # Leaves only built-in modules and the local package, so a build never waits on Unity's package registry.
+ - name: Remove the test-only packages
+ run: |
+ manifest=UnityProject/Packages/manifest.json
+ jq 'del(.dependencies["com.unity.test-framework.performance"]) | del(.testables)' "$manifest" > manifest.tmp
+ mv manifest.tmp "$manifest"
+
+ - name: Add the package samples to the project
+ run: cp -r UnityPackage/Samples~ UnityProject/Assets/Samples
+
+ # Only checks that the player builds; nothing is uploaded or released.
+ - name: Build the Shape Gallery sample
+ uses: game-ci/unity-builder@adaf7cd8f42fd48fbbf2d4f5f67ca30d87f819f1 # v5.0.1
+ with:
+ unityVersion: ${{ matrix.unityVersion }}
+ targetPlatform: ${{ matrix.targetPlatform }}
+ projectPath: UnityProject
+ buildMethod: reromanlee.Wireframes.ContinuousIntegration.PlayerBuild.Build
+ versioning: None
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 03771be..0d57a7f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,47 +1,61 @@
-# Changelog
-
-All notable changes to this package are documented in this file.
-
-The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this package adheres to
-[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
-
-## [1.0.0] - 2026-09-24
-
-### Added
-
-- Shapes: polylines (`IPolyline`, created as polylines, polygons or triangles), rectangles, rounded rectangles,
- circles, ellipses, stars, spheres, ellipsoids, spiked spheres, cylinders, cones, capsules, stadiums, frustums and
- pyramids.
-- `IRigidShape`, shared by every shape except lines and polylines: one bone, a local and world position and rotation,
- and a `Color`. Changing the bone keeps the shape's world position, rotation and size.
-- `IAxialShape`, shared by long shapes: `Length`, and end B through `LocalEnd` and `WorldEnd`.
-- Create methods that take bones first and work in their local space, such as `CreateLine(boneA, boneB)` and
- `CreateCircle(bone, localCenter, radius)`.
-- Polylines have a bone and a color for every point.
-- Shape Gallery sample, a scene with every shape. The Stress Test sample spawns every kind of shape.
-- `WireframeCamera`, a component that draws everything its camera renders as wireframe, under the Built-in Render
- Pipeline or any Scriptable Render Pipeline.
-
-### Changed
-
-- The package lives in the repository's `UnityPackage` folder. Install it from
- `https://github.com/reromanlee/Wireframes.git?path=/UnityPackage`.
-- Boxes have a center, a rotation and a signed `Size`. Their corner properties still work, and
- `CreateBox(center, rotation, size)` creates a rotated box.
-- Changing a box's bone keeps its world rotation instead of aligning the box to the new bone's axes.
- `CreateBox(bone, localCornerA, localCornerB)` creates a box aligned to a bone.
-- `CreateBox()` creates a 1 by 1 by 1 cube instead of a box with both corners at the origin.
-
-## [0.1.0] - 2026-09-22
-
-### Added
-
-- `LineContainer`, which draws all of its shapes with one GPU-skinned mesh. It is disposed by `Dispose()` or when
- the scene it was created in unloads, and accepts an optional material.
-- Lines (`ILine`) with local and world positions, colors and a bone for each endpoint.
-- Boxes (`IBox`) with local and world corners, one bone and `SetColor`.
-- `IShape`, shared by every shape: `SetColor`, `Dispose` and `IsDisposed`.
-- Shapes whose bone is destroyed stay in place and switch to world space.
-- Edits are applied once per frame and upload only what changed.
-- Stress Test sample.
-- Edit mode and play mode tests.
+# Wireframes 2.0.0
+
+### What's new
+
+1. Shapes follow their bones through the package's own shader skinning instead of a `SkinnedMeshRenderer`, which is faster on WebGL and no longer depends on the GPU Skinning player setting.
+2. The package's material draws under the Built-in Render Pipeline, URP and HDRP, hides, shows or fades lines behind other geometry through the `Occlusion` setting, and draws transparent colors through `UseAlpha`.
+3. Containers and shapes work in Edit Mode, drawing in the Scene and Game views without ever being saved into scenes.
+4. Unity 2022.3 is supported, where 1.0 needed Unity 6.
+5. `IsVisible` hides a shape or a whole container without disposing it, and without allocating.
+6. `WireframeContainerSettings` sets a container's name, material, layer, persistence across scenes, reserved capacity, occlusion and transparency.
+7. Containers split shapes into chunks of up to 65,535 vertices with 16-bit indices, hold any number of shapes, and give memory back as shapes are disposed.
+8. `container.Statistics` reports shapes, vertices, edges, bones, chunks and memory without allocating, Profiler markers time each step, and Profiler counters add up every container when the `com.unity.profiling.core` package is installed.
+9. Server builds, missing shaders, destroyed bones and failing shapes are handled without exceptions reaching Unity's rendering, and each problem is logged once with a `[Wireframes]` prefix.
+10. Steady frames allocate nothing and edits upload only what they change, which tests enforce; continuous integration runs the tests on Unity 2022.3 and 6000.5 and builds for WebGL, Android, iOS, macOS and Linux.
+11. The README is rewritten, with detailed Features, Performance, Usage and Migration pages in the `Documentation` folder.
+
+### Breaking changes
+
+1. `LineContainer` is now `WireframeContainer`, `ILineContainer` is removed, and a material is passed through `WireframeContainerSettings`.
+2. A custom material's shader must move vertices with `WireframesSkin` from the package's `Wireframes.hlsl`.
+3. `Segments` and `Sides` are now `SegmentCount` and `SideCount`, and the `segments`, `sides` and star `points` parameters are now `segmentCount`, `sideCount` and `pointCount`.
+4. `LocalEnd` and `WorldEnd` of long shapes are now `LocalEndB` and `WorldEndB`, next to the new `LocalEndA` and `WorldEndA`.
+5. Edits and bone moves are applied right before each camera renders, instead of at the end of `LateUpdate`.
+6. Bones must be Transforms in a scene, and resolutions are capped at 1,024 segments or sides and 512 star points.
+7. Calls from other threads throw `InvalidOperationException` in the Editor and development builds.
+
+### Known issues
+
+1. Stereo rendering for XR is untested.
+
+# Wireframes 1.0.0
+
+### What's new
+
+1. New shapes: polylines (`IPolyline`, created as polylines, polygons or triangles), rectangles, rounded rectangles, circles, ellipses, stars, spheres, ellipsoids, spiked spheres, cylinders, cones, capsules, stadiums, frustums and pyramids.
+2. `IRigidShape`, shared by every shape except lines and polylines: one bone, a local and world position and rotation, and a `Color`. Changing the bone keeps the shape's world position, rotation and size.
+3. `IAxialShape`, shared by long shapes: `Length`, and end B through `LocalEnd` and `WorldEnd`.
+4. Create methods that take bones first and work in their local space, such as `CreateLine(boneA, boneB)` and `CreateCircle(bone, localCenter, radius)`.
+5. Polylines have a bone and a color for every point.
+6. `WireframeCamera`, a component that draws everything its camera renders as wireframe, under the Built-in Render Pipeline or any Scriptable Render Pipeline.
+7. The Shape Gallery sample, a scene with every shape, and a Stress Test sample that spawns every kind of shape.
+
+### Changes
+
+1. The package lives in the repository's `UnityPackage` folder; install it from `https://github.com/reromanlee/Wireframes.git?path=/UnityPackage`.
+2. Boxes have a center, a rotation and a signed `Size`. Their corner properties still work, and `CreateBox(center, rotation, size)` creates a rotated box.
+3. Changing a box's bone keeps its world rotation instead of aligning the box to the new bone's axes; `CreateBox(bone, localCornerA, localCornerB)` creates a box aligned to a bone.
+4. `CreateBox()` creates a 1 by 1 by 1 cube instead of a box with both corners at the origin.
+
+# Wireframes 0.1.0
+
+### What's new
+
+1. `LineContainer`, which draws all of its shapes with one GPU-skinned mesh. It is disposed by `Dispose()` or when the scene it was created in unloads, and accepts an optional material.
+2. Lines (`ILine`) with local and world positions, colors and a bone for each endpoint.
+3. Boxes (`IBox`) with local and world corners, one bone and `SetColor`.
+4. `IShape`, shared by every shape: `SetColor`, `Dispose` and `IsDisposed`.
+5. Shapes whose bone is destroyed stay in place and switch to world space.
+6. Edits are applied once per frame and upload only what changed.
+7. The Stress Test sample.
+8. Edit Mode and Play Mode tests.
diff --git a/Documentation/FEATURES.md b/Documentation/FEATURES.md
new file mode 100644
index 0000000..e62c65e
--- /dev/null
+++ b/Documentation/FEATURES.md
@@ -0,0 +1,100 @@
+# Features
+
+Everything Wireframes does, most important first. How to use each part is in [USAGE.md](USAGE.md).
+
+### Shapes follow Transforms on the GPU
+
+A shape attaches to Transforms, called bones, and moves, turns and scales with them from then on. Once per frame, right before a camera renders, the package reads each bone's local-to-world matrix and uploads it to a float texture; the vertex shader moves every vertex by its bone's matrix.
+
+1. A bone costs the same whether one shape or a thousand follow it.
+2. Lines and polylines have a bone per point, so they can join anything; other shapes have one bone and move rigidly with it, like a child Transform.
+3. Changing a shape's bone keeps it where it is in the world, like reparenting a Transform.
+4. When a bone is destroyed, its shapes stay where they were drawn last and switch to world space.
+5. Any Transform in a scene can be a bone. Nothing is added to it, and a prefab asset is refused with an `ArgumentException`.
+
+### 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. |
+
+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.
+
+### 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.
+
+### Steady frames allocate nothing
+
+Frames that move bones, edit shapes, hide and show them, or change nothing allocate no managed memory, which tests enforce. Freed space is reused by the next shape of the same size and packed away once it fills half a buffer, and memory past the reserved capacity is given back as shapes are disposed.
+
+### Containers
+
+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. |
+| `Layer` | 0 | Layer of the container's GameObjects, for camera culling masks. |
+| `PersistAcrossScenes` | false | Keeps the container when other scenes load. |
+| `VertexCapacity`, `EdgeCapacity` | 0 | Room reserved up front, so a known load never grows the buffers. |
+| `Name` | "Wireframes" | Name of the container's GameObject. |
+
+Shapes are stored in chunks, meshes of up to 65,535 vertices with 16-bit indices, created as they fill; a shape bigger than a chunk gets a mesh of its own with 32-bit indices. An unreserved chunk that stays empty for 5 seconds is released.
+
+### Render pipelines and platforms
+
+1. One shader draws under the Built-in Render Pipeline, URP and HDRP, picked by the active pipeline.
+2. Windows, Android and WebGL are tested on devices before each release; macOS, iOS and Linux are built by continuous integration.
+3. Without a graphics device, as in server builds, or without a usable shader, shapes keep working and nothing is drawn or uploaded.
+4. A custom material works once its shader skins vertices with the package's `Wireframes.hlsl`.
+5. The shader carries Unity's stereo rendering macros for XR, which is untested.
+
+### Occlusion and transparency
+
+`WireframeOcclusion.Hide` depth-tests lines like any other object, `Show` draws them over everything, and `Fade` draws their hidden parts dimmer, like the Scene view's handles, at the cost of a second draw call. `UseAlpha` blends every color by its alpha, in any occlusion mode.
+
+### 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.
+
+### Visibility
+
+`shape.IsVisible = false` hides a shape without disposing it: it keeps its place, bones and settings, and draws again as it is then once shown. `container.IsVisible = false` hides every shape of a container and costs nothing while it lasts. Neither allocates.
+
+### Statistics and profiling
+
+1. `container.Statistics` reports shapes, hidden shapes, vertices, edges, bones, chunks, and CPU and GPU memory, without allocating.
+2. Profiler markers named `Wireframes.*` time each step: reading bones, uploading them, writing shapes, uploading meshes, compacting, resizing buffers, and creating and releasing chunks.
+3. With the `com.unity.profiling.core` package installed, Profiler counters add up every container's shapes, vertices, edges, bones, chunks and GPU memory.
+
+### Reliability
+
+1. A Create call that fails leaves nothing behind.
+2. A shape that fails to update is reported once and skipped, and the others keep updating.
+3. No exception escapes into Unity's rendering.
+4. Warnings and errors start with `[Wireframes]` and are logged once per cause.
+5. In the Editor and development builds, calls from other threads throw and positions that aren't finite numbers are reported; release builds skip both checks.
+6. Everything works with domain reload disabled.
+
+### Wireframe camera
+
+The `WireframeCamera` component draws everything its camera renders as wireframe, like the Scene view's Wireframe draw mode, while other cameras draw normally. It works under every render pipeline, and needs a graphics API with wireframe rendering, which OpenGL ES and WebGL lack.
diff --git a/Documentation/MIGRATION.md b/Documentation/MIGRATION.md
new file mode 100644
index 0000000..948dba4
--- /dev/null
+++ b/Documentation/MIGRATION.md
@@ -0,0 +1,47 @@
+# Migrating from 1.x to 2.0
+
+Wireframes 2.0 draws shapes through its own shader skinning instead of a `SkinnedMeshRenderer`, and renames the container. Most projects only need the renames; the other changes matter to projects with custom materials, or that depend on when edits are uploaded. Most important first.
+
+### Renames
+
+1. **`LineContainer` is now `WireframeContainer`,** and `ILineContainer` is gone. A container's options moved into `WireframeContainerSettings`:
+
+ ```csharp
+ // 1.x
+ var wireframes = new LineContainer(material);
+
+ // 2.0
+ var wireframes = new WireframeContainer(new WireframeContainerSettings { Material = material });
+ ```
+
+2. **Resolution members end in `Count`:** `Segments` is now `SegmentCount` and `Sides` is `SideCount`, and the parameters `segments`, `sides` and the star's `points` are now `segmentCount`, `sideCount` and `pointCount`. Only calls that name those arguments change.
+3. **Long shapes name both ends:** `LocalEnd` and `WorldEnd` are now `LocalEndB` and `WorldEndB`, next to the new `LocalEndA` and `WorldEndA`.
+
+Create methods became extension methods, declared in a factory class next to each shape, such as `CircleFactory`, so calls like `container.CreateCircle(...)` stay as they are with `using reromanlee.Wireframes;`.
+
+### Custom materials
+
+In 1.x, a `SkinnedMeshRenderer` moved the vertices, so any vertex color shader worked. In 2.0 the shader moves them: a custom material's shader has to call `WireframesSkin` from the package's include file, or shapes are drawn in their bones' local space around the world origin. [USAGE.md](USAGE.md#custom-materials) shows the vertex program.
+
+The package's material now works under URP and HDRP too, and draws transparent or always-on-top lines through the `UseAlpha` and `Occlusion` settings, so a custom material made only for those reasons can go.
+
+### When edits show up
+
+1.x uploaded edits at the end of `LateUpdate`. 2.0 uploads them, and reads the bones, right before each camera renders, so edits made after `LateUpdate` but before rendering now show up in the same frame. Edits made after rendering, such as in `WaitForEndOfFrame`, still show up in the next one.
+
+### Bones
+
+1. No component is added to bones anymore. A destroyed bone is noticed when the bones are read, and its shapes stay where they were last drawn, switching to world space the next time they are used.
+2. A bone must be a Transform in a scene: a prefab asset throws `ArgumentException`.
+
+### Limits and checks
+
+1. Resolutions are capped at 1,024 segments or sides and 512 star points, and larger values throw `ArgumentOutOfRangeException`.
+2. Calls from other threads, never supported, now throw `InvalidOperationException` in the Editor and development builds.
+
+### What changes around the container
+
+1. A container draws with a `MeshRenderer` per chunk of up to 65,535 vertices, instead of one `SkinnedMeshRenderer`.
+2. The **GPU Skinning** player setting no longer matters, and WebGL no longer skins shapes on the CPU.
+3. Unity 2022.3 is supported, where 1.x needed Unity 6.
+4. Containers also work in Edit Mode, never saved into scenes.
diff --git a/Documentation/PERFORMANCE.md b/Documentation/PERFORMANCE.md
new file mode 100644
index 0000000..6e5a63a
--- /dev/null
+++ b/Documentation/PERFORMANCE.md
@@ -0,0 +1,81 @@
+# Performance
+
+What Wireframes costs, measured, and the design behind the numbers. Tips to keep it light are at the end.
+
+### How it stays fast
+
+1. **Moving shapes costs one matrix per bone.** Right before rendering, the package reads each bone's local-to-world matrix and uploads all of them in one float texture, and the vertex shader moves every vertex. A bone costs the same whether one shape or a thousand follow it.
+2. **Edits cost only what they change.** Each edited shape is written once per frame, however many of its properties changed, and only the changed ranges of the buffers are uploaded.
+3. **Steady frames allocate nothing,** which tests enforce for frames that move bones, edit shapes, hide and show them, or change nothing.
+4. **Draw calls stay few.** Shapes share chunks, meshes of up to 65,535 vertices, and each chunk is one draw call per camera, or two with `WireframeOcclusion.Fade`. Nothing is created per shape or per bone: no GameObjects, no components.
+5. **Work is spread over frames.** Buffers double when full, freed space goes to the next shape of the same size, and a chunk is compacted once freed space fills half of it, one chunk per frame at most.
+
+### CPU time
+
+Measured by the package's performance tests in the Editor, on an Intel Core i7-12700K, as the median of 10 frames. Each row is one frame: its edits plus the flush that uploads them. The Editor runs scripts on Mono, slower than IL2CPP player builds.
+
+| Frame, on 100 bones | Unity 6000.5 | Unity 2022.3 |
+|---|---|---|
+| Nothing changed, 10,000 lines | 24 µs | 22 µs |
+| Every bone moved, 10,000 lines | 78 µs | 95 µs |
+| 1,000 scattered line color edits | 0.26 ms | 0.30 ms |
+| 100 scattered sphere radius edits | 0.93 ms | 1.07 ms |
+| Dispose half of 10,000 lines | 0.49 ms | 0.44 ms |
+| Dispose half of 1,000 spheres | 1.46 ms | 1.47 ms |
+| Create 10,000 lines and upload them | 11.3 ms | 14.7 ms |
+| Create 1,000 spheres, 96,000 vertices, and upload them | 17.9 ms | 19.0 ms |
+
+Frames that change nothing, move bones or edit shapes allocate nothing. Creating shapes allocates their objects, about 2 per line and 2 per sphere, and disposing thousands of shapes at once allocates a handful of times as internal lists grow.
+
+### Devices
+
+The Stress Test sample, 10,000 lines, 1,000 boxes and 2,000 other shapes on 100 orbiting bones, in builds made with Unity 6000.5. Times are whole frames, from Unity's `FrameTimingManager`.
+
+| Device | Graphics API | Main thread | GPU |
+|---|---|---|---|
+| Desktop, Intel Core i7-12700K, NVIDIA RTX 4070 Ti | Direct3D 12 | 0.40 ms | 0.46 ms |
+| The same desktop, in a Chromium browser | WebGL 2 | 0.49 ms | Not reported |
+| Poco X3 NFC, Snapdragon 732G, Adreno 618 | Vulkan | 4.24 ms | 5.64 ms |
+
+### Compared with skinned meshes
+
+Up to 1.0, shapes were drawn by a `SkinnedMeshRenderer`. Before 2.0 replaced it with the package's own shader skinning, a prototype compared the two on the same scenes, alternating between them:
+
+1. **WebGL 2**, where Unity skins meshes on the CPU: frames took 0.6 ms of main thread instead of 1.0 ms at 114,000 vertices, and 0.7 ms instead of 2.2 ms at 454,000. These are rough, from a few samples each.
+2. **Windows**: the same CPU time, and 0.10 ms of GPU time instead of 0.15 ms at 114,000 vertices, and 0.21 ms instead of 0.27 ms at 454,000.
+3. **Android**, on the Poco X3 NFC with Vulkan: the same GPU time, and 72.9 frames per second instead of 70.3 at 454,000 vertices.
+
+### Memory
+
+1. **On the GPU,** a vertex takes 20 bytes, for its position, color and bone index. An edge takes 4 bytes of indices, or 8 in a chunk made for a shape of more than 65,535 vertices, and a bone takes 48 bytes of the bone texture, which holds 256 bones per 12 KB row.
+2. **On the CPU,** a vertex takes the same 20 bytes, an edge about 24 bytes with its bookkeeping, and the bone texture as much as on the GPU; shapes and bones add a few bytes each.
+3. **Buffers start small,** at 256 vertices and 128 edges per chunk, and double as needed. Memory past the reserved capacity is given back as shapes are disposed, and a chunk that stays empty for 5 seconds is released.
+
+| Shape, at the default resolution | Vertices | Edges |
+|---|---|---|
+| Line | 2 | 1 |
+| Polyline of n points | n | n - 1, or n when closed |
+| Box | 8 | 12 |
+| Rectangle | 4 | 4 |
+| Rounded rectangle | 36 | 36 |
+| Circle, ellipse | 32 | 32 |
+| Star with 5 points | 10 | 10 |
+| Sphere, ellipsoid | 96 | 96 |
+| Spiked sphere with 4, 6 or 8 spikes | 8, 14 or 14 | 18, 36 or 36 |
+| Spiked sphere with 12 or 20 spikes | 32 | 90 |
+| Cylinder | 64 | 68 |
+| Cone | 33 | 36 |
+| Capsule | 122 | 132 |
+| Stadium | 34 | 34 |
+| Frustum with n sides | 2n | 3n |
+| Pyramid | 5 | 8 |
+
+### Tips
+
+1. **Move bones rather than editing points.** A moving bone uploads its 48 bytes, while an edit rewrites every vertex of its shape.
+2. **Reserve capacity for a known load** with `VertexCapacity` and `EdgeCapacity`, so buffers never grow mid-game.
+3. **Hide shapes that come back** with `IsVisible` instead of disposing and creating them again.
+4. **Lower `segmentCount`** for small or distant round shapes.
+5. **Group shapes in few containers.** Each container has its own bone texture and draw calls.
+6. **Use `Fade` where it helps,** since it doubles a container's draw calls.
+7. **Hide whole containers that are off screen.** Shapes follow arbitrary Transforms, so their meshes have fixed bounds of ±1,000 km and are never frustum-culled.
diff --git a/Documentation/USAGE.md b/Documentation/USAGE.md
new file mode 100644
index 0000000..217c939
--- /dev/null
+++ b/Documentation/USAGE.md
@@ -0,0 +1,197 @@
+# Usage
+
+How to use Wireframes, from the first container to custom shaders. What each part does is in [FEATURES.md](FEATURES.md).
+
+### Containers
+
+`new WireframeContainer()` creates a GameObject named **Wireframes** in the active scene, which draws every shape created in the container.
+
+1. The container lives until you call `Dispose()` or the scene it was created in unloads. Either way, every shape it created is disposed too, and `IsDisposed` becomes true.
+2. `new WireframeContainer(settings)` takes a `WireframeContainerSettings`, read once, so changing it later doesn't affect the container.
+3. The container never destroys a material passed in its settings.
+4. Create containers and shapes on the main thread, from `Awake`, `OnEnable`, `Start` or later, not from constructors or field initializers. In the Editor and development builds, calls from other threads throw `InvalidOperationException`.
+
+Settings are serializable, so a script can show them in the Inspector:
+
+```csharp
+[SerializeField] private WireframeContainerSettings _settings = new();
+
+private void OnEnable()
+{
+ _wireframes = new WireframeContainer(_settings);
+}
+```
+
+### Creating shapes
+
+Every shape except lines and polylines has the same kinds of Create methods:
+
+| Kind | Example | Space |
+|---|---|---|
+| No arguments | `CreateCylinder()` | A white shape of unit size at the world origin: radius 0.5, sizes 1, length 1. |
+| The usual form | `CreateCylinder(endA, endB, radius)` | World space. |
+| The same with a bone first | `CreateCylinder(bone, localEndA, localEndB, radius)` | The bone's local space. |
+| Position and rotation | `CreateCylinder(position, rotation, length, radius)` | World space. |
+
+A few shapes add an obvious extra, such as `CreateCircle(center, normal, radius)` or a stadium with an explicit normal.
+
+Lines and polylines are made of points: they are created from world positions, or from one bone per point, each point sitting at its bone's origin. `CreateLine()` starts with both ends at the world origin.
+
+### Bones and spaces
+
+A **bone** is any Transform in a scene that a shape follows; `null` means world space. A prefab asset can't be a bone and throws `ArgumentException`.
+
+1. Lines and polylines have a bone for every point, because points can go anywhere and still make a line.
+2. Every other shape has one bone and moves rigidly with it, like a child Transform (`IRigidShape`). Its position and rotation are relative to that bone, and its sizes are in the bone's units, so it also scales with the bone.
+3. `Local*` properties are relative to the bone, or world space without a bone.
+4. `World*` properties convert through the bone's current pose, like `Transform.position`.
+5. Changing a bone keeps the shape where it is in the world, like reparenting a Transform: points keep their world position, and other shapes also keep their world rotation and size. From then on it moves with the new bone.
+
+**Rule of thumb: create a shape on its bone when you know the bone, or set the bone first and then the position in whichever space you mean.**
+
+| You want | Write |
+|---|---|
+| A line joining two Transforms | `CreateLine(a, b)` |
+| A point at an offset from a bone, such as a sword tip | `line.BoneB = sword; line.LocalPositionB = new Vector3(0f, 1f, 0f);` |
+| A point pinned where something was hit, moving with it | `line.BoneA = hitTransform; line.WorldPositionA = hit.point;` |
+| A shape that stops following | `shape.Bone = null;` (it stays where it is) |
+
+### A shape's axes
+
+A shape's rotation turns its own axes:
+
+1. **+Y is the normal of flat shapes.** Rectangles, circles, ellipses, stars and stadiums lie in their XZ plane, so with no rotation they lie flat on the ground, or flat around their bone.
+2. **+Z is the axis of long shapes** (`IAxialShape`: cylinders, cones, capsules, stadiums, frustums and pyramids). They start at end A, at their position, and run `Length` along +Z to end B. Setting one end keeps the other where it is: the axis turns by the smallest rotation and `Length` follows. Setting the position moves the whole shape.
+3. Other shapes are centered on their position.
+4. `Quaternion.LookRotation(axis, normal)` is the rotation that points a shape's +Z along `axis` with +Y toward `normal`.
+5. A shape created from two points turns so that its +Y stays as close to world up, or its bone's up, as it can. A stadium or ellipse created that way lies as flat as it can.
+6. Capsule and stadium ends are the centers of their spheres or circles, as in `Physics.CapsuleCast`. When one sphere holds the other, only the bigger one is drawn.
+
+### Sizes and resolution
+
+1. Sizes are in the bone's units. Changing a shape's bone multiplies them by the ratio of the two bones' scales, so the shape keeps its size in the world, exactly for uniformly scaled bones.
+2. Sizes are used as given: a negative size mirrors the shape, and zero collapses it. A box keeps a signed `Size`, so both of its corners come back exactly as they were set.
+3. Round shapes take `segmentCount`, the number of straight pieces a full circle is drawn with: 32 by default, from 3 to 1,024. Cylinders, cones, capsules, stadiums and rounded rectangles need a multiple of 4, because their lines meet their rings at quarter points, and arcs use their share of it.
+4. Frustums take a `sideCount` from 3 to 1,024 (4 by default), stars a `pointCount` from 3 to 512 (5 by default), and spiked spheres a `spikeCount` of 4, 6, 8, 12 or 20 (12 by default).
+5. Those counts, and the number of points of a polyline, are fixed when a shape is created. To change them, create a new shape.
+
+### Colors
+
+1. Lines and polylines have a color per point, blended along the edges. `SetColor` sets them all.
+2. Every other shape has one `Color`, which `SetColor` sets too.
+3. Colors are stored with 8 bits per channel, so values above 1 are clamped: no HDR glow.
+4. In linear color space, the package's shader converts vertex colors so they look the same as that `Color` on a material.
+5. Alpha is ignored unless the container's `UseAlpha` setting is on.
+
+### When edits show up
+
+Setters only record the change, and each edited shape is queued once, however many of its properties change. Right before a camera renders, the container writes the queued shapes, uploads what changed and reads its bones, so everything edited or moved earlier in the frame shows up in that frame, whether it happened in `Update`, `LateUpdate`, animation or physics. Changes made after rendering, such as in `WaitForEndOfFrame`, show up in the next frame.
+
+### Visibility
+
+`shape.IsVisible = false` hides a shape without disposing it. It keeps its place, bones and settings, takes edits while hidden, and draws again as it is then when shown. `container.IsVisible = false` hides every shape of the container, and a hidden container does no per-frame work until it is shown again. Hiding and showing allocate nothing, which makes them the cheap way to toggle shapes that come back.
+
+### Destroyed bones
+
+When a bone's GameObject is destroyed, the shapes and points on it stay exactly where they were last drawn, at the same size. The next time you use such a shape, its bone reads as `null` and it continues in world space from there. A move of the bone in the frame it was destroyed, after that frame's render, is lost.
+
+### Disposing shapes
+
+`shape.Dispose()` removes a shape and frees its space for the next shape of the same size. After that, `IsDisposed` is true and every other member throws `ObjectDisposedException`. Disposing again does nothing.
+
+### Edit Mode
+
+Containers work the same in Edit Mode, drawing in the Scene and Game views, which suits previews from `[ExecuteAlways]` scripts:
+
+```csharp
+[ExecuteAlways]
+public sealed class BoundsPreview : MonoBehaviour
+{
+ private WireframeContainer _wireframes;
+
+ private void OnEnable()
+ {
+ _wireframes = new WireframeContainer();
+ _wireframes.CreateBox(transform, new Vector3(-0.5f, -0.5f, -0.5f), new Vector3(0.5f, 0.5f, 0.5f));
+ }
+
+ private void OnDisable()
+ {
+ _wireframes.Dispose();
+ }
+}
+```
+
+1. An Edit Mode container is never saved into its scene and never marks it as changed.
+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.
+
+### 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.
+
+```hlsl
+#include "Packages/com.reromanlee.wireframes/Runtime/Shaders/Wireframes.hlsl"
+
+struct Attributes
+{
+ float3 position : POSITION;
+ half4 color : COLOR;
+ float bone : TEXCOORD0;
+};
+
+struct Varyings
+{
+ float4 positionCS : SV_POSITION;
+ half4 color : COLOR;
+};
+
+Varyings Vertex(Attributes input)
+{
+ Varyings output;
+ float3 world = WireframesSkin(input.position, input.bone);
+ output.positionCS = mul(UNITY_MATRIX_VP, float4(world, 1.0));
+ output.color = half4(WireframesColor(input.color.rgb), input.color.a);
+ return output;
+}
+```
+
+HDRP renders relative to the camera, so there the world position goes through `GetCameraRelativePositionWS` before `TransformWorldToHClip`. The package's own shader, `Runtime/Resources/reromanlee.Wireframes/Unlit.shader`, has complete passes for the Built-in Render Pipeline, URP and HDRP to start from. The `Occlusion` and `UseAlpha` settings apply to the package's material only.
+
+### Statistics and profiling
+
+`container.Statistics` returns what a container holds and the memory it uses, without allocating, so it can be read every frame. `ChunkCount` is also the number of draw calls per camera, or twice that with `WireframeOcclusion.Fade`.
+
+```csharp
+_wireframes.CreateLine(_hand, _target);
+_wireframes.CreateCircle(_target, Vector3.zero, 1f);
+_wireframes.CreateSphere(_target, Vector3.zero, 0.5f).IsVisible = false;
+
+// Shapes: 3 (1 hidden), vertices: 130, edges: 33, bones: 2, chunks: 1, CPU memory: 25 KB, GPU memory: 17 KB
+Debug.Log(_wireframes.Statistics);
+```
+
+1. In the Profiler, the package's work shows up under markers named `Wireframes.*`, in the Render category.
+2. With the `com.unity.profiling.core` package installed, the Profiler counters `Wireframes Shapes`, `Vertices`, `Edges`, `Bones`, `Chunks` and `GPU Memory` add up every container; add them to a module in the Profiler's Module Editor.
+3. Warnings and errors start with `[Wireframes]` and are logged once per cause.
+
+### Wireframe camera
+
+Add the **WireframeCamera** component to a Camera to draw everything that camera renders as wireframe, like the Scene view's Wireframe draw mode, while other cameras draw normally. It follows the render pipeline if it changes at runtime, though the frame in which the pipeline switches is drawn without wireframe. It uses `GL.wireframe`, which OpenGL ES and WebGL don't support.
+
+### Samples
+
+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.
+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
+
+To run the package's tests in the Test Runner, add the package to `testables` in `Packages/manifest.json`:
+
+```json
+"testables": ["com.reromanlee.wireframes"]
+```
+
+The timing tests run when the project also has the `com.unity.test-framework.performance` package, and the Profiler counter test when it has `com.unity.profiling.core`.
diff --git a/README.md b/README.md
index 2ab4698..3290d60 100644
--- a/README.md
+++ b/README.md
@@ -1,266 +1,116 @@
-# Wireframes for Unity
+### About Wireframes
-Wireframe shapes for Unity 6 that follow Transforms at no per-frame CPU cost to your code.
+Wireframe shapes for Unity that follow Transforms on the GPU: create a shape once, and it moves with its bones.
-You create a shape once, attach it to any Transforms ("bones"), and it moves with them from then on. The GPU does the moving, through the same skinning Unity uses for animated characters. Every shape in a container is one mesh, drawn in one draw call.
+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.
-
+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.
-## Why not `Debug.DrawLine`?
+### Features
-| | `Debug.DrawLine` / Gizmos | Wireframes |
-|---|---|---|
-| Moving shapes | Call it again every frame, for every line | Create once; bones move them on the GPU |
-| CPU cost per frame | Grows with the number of lines | No work from the package while nothing is edited |
-| Player builds | Not drawn | Drawn |
+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.
-## Requirements
+Detailed about features - see [FEATURES.md](Documentation/FEATURES.md).
-- Unity 6 (6000.0) or later.
-- The default material is an unlit vertex-color shader, written for and tested with the Built-in Render Pipeline. With URP or HDRP, if it doesn't render, pass your own material to the container; any shader that outputs vertex colors works.
+### Performance
-## Installation
+**A frame where nothing changed costs about 25 µs** of CPU time for 10,000 lines on 100 bones, one where every bone moved about 0.1 ms, and one with 1,000 color edits about 0.3 ms, all without allocating. Each mesh of up to 65,535 vertices is one draw call, and a vertex takes 20 bytes on the GPU.
-In **Window > Package Manager**, choose **+ > Install package from git URL** and enter:
+**The Stress Test sample, 13,000 shapes on 100 moving bones,** runs whole frames in 0.40 ms of main thread and 0.46 ms of GPU time on Windows (RTX 4070 Ti, Direct3D 12), 0.49 ms of main thread in a WebGL 2 build, and 4.24 ms of main thread and 5.64 ms of GPU time on Android (Poco X3 NFC, Vulkan).
+
+
+
+Detailed about performance - see [PERFORMANCE.md](Documentation/PERFORMANCE.md).
+
+### Requirements
+
+1. Unity 2022.3 or later.
+2. The Built-in Render Pipeline, URP or HDRP.
+3. Shader model 3.5: Direct3D 11 or 12, Vulkan, Metal, OpenGL Core, OpenGL ES 3.0 or WebGL 2.
+
+### Limitations
+
+1. Lines are always 1 pixel wide.
+2. Colors have 8 bits per channel, so no HDR.
+3. A shape has one bone, or lines and polylines one per point.
+4. Resolution and point counts are fixed at creation.
+5. Rings take up to 1,024 segments.
+6. Shapes are never frustum-culled.
+7. Shapes are used from the main thread only.
+8. Custom materials skin with the package's HLSL.
+
+### How to install
+
+**From the git URL:** in **Window > Package Manager**, choose **+ > Install package from git URL** and enter the URL below. Add `#2.0.0` to its end to stay on that version.
```
https://github.com/reromanlee/Wireframes.git?path=/UnityPackage
```
-Or add it to `Packages/manifest.json`:
+**From a tarball:** download the `.tgz` file of the [latest release](https://github.com/reromanlee/Wireframes/releases/latest), then in **Window > Package Manager** choose **+ > Install package from tarball** and pick it.
-```json
-"com.reromanlee.wireframes": "https://github.com/reromanlee/Wireframes.git?path=/UnityPackage"
-```
+Upgrading from 1.x - see [MIGRATION.md](Documentation/MIGRATION.md).
-The package lives in the repository's `UnityPackage` folder, which the `path` parameter points to.
+### How to use
-## Quick start
+**Create a `WireframeContainer`, then create shapes in it on the bones they follow.** A shape keeps following its bones until you dispose it or its container, with nothing to call per frame. Disposing the container disposes its shapes, and so does unloading the scene it was created in.
```csharp
using reromanlee.Wireframes;
using UnityEngine;
-public class TargetingLine : MonoBehaviour
+public sealed class TargetingLine : MonoBehaviour
{
[SerializeField] private Transform _hand;
[SerializeField] private Transform _target;
- private LineContainer _wireframes;
+ private WireframeContainer _wireframes;
- private void Start()
+ private void OnEnable()
{
- _wireframes = new LineContainer();
+ _wireframes = new WireframeContainer();
// A line from the hand to the target that follows both.
_wireframes.CreateLine(_hand, _target).SetColor(Color.cyan);
- // A box around the target that moves, rotates and scales with it.
- _wireframes.CreateBox(_target, new Vector3(-0.5f, -0.5f, -0.5f), new Vector3(0.5f, 0.5f, 0.5f));
-
- // A circle of radius 1 lying flat around the target.
- _wireframes.CreateCircle(_target, Vector3.zero, 1f).SetColor(Color.yellow);
+ // A circle of radius 1 lying flat around the target, moving, turning and scaling with it.
+ _wireframes.CreateCircle(_target, Vector3.zero, 1f).Color = Color.yellow;
}
- private void OnDestroy()
+ private void OnDisable()
{
_wireframes.Dispose();
}
}
```
-There is no `Update`: every shape follows `_hand` and `_target` on its own.
-
-## 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, points)` | 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, sides)` | 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. |
-
-## Concepts
-
-### Container
-
-`new LineContainer()` creates a GameObject named **Wireframes** in the active scene, with one skinned mesh for all of its shapes.
-
-- It lives until you call `Dispose()` or its scene unloads. Either way, every shape it created is disposed too, and `IsDisposed` becomes true.
-- `new LineContainer(material)` draws with your material instead. The container never destroys a material you pass in.
-- Create containers and shapes on the main thread, from `Awake`, `Start` or later, not from constructors or field initializers.
-
-### Bones and spaces
-
-A **bone** is any Transform a shape follows. `null` means world space.
-
-- Lines and polylines have a bone for every point, because points can go anywhere and still make a line.
-- Every other shape has one bone and moves rigidly with it, like a child Transform (`IRigidShape`). It has a position and a rotation relative to that bone, and its sizes are in the bone's units, so it also scales with the bone.
-- `Local*` properties are relative to the bone. Without a bone they are world space.
-- `World*` properties convert through the bone's current pose, like `Transform.position`.
-- Changing a bone keeps the shape where it is in the world, like reparenting a Transform: points keep their world position, and other shapes also keep their world rotation and size. From then on it moves with the new bone.
-
-**Rule of thumb: create a shape on its bone when you know the bone, or set the bone first and then the position in whichever space you mean.**
-
-| You want | Write |
-|---|---|
-| A line joining two transforms | `CreateLine(a, b)` |
-| A point at an offset from a bone, e.g. a sword tip | `line.BoneB = sword; line.LocalPositionB = new Vector3(0f, 1f, 0f);` |
-| A point pinned where something was hit, moving with it | `line.BoneA = hitTransform; line.WorldPositionA = hit.point;` |
-| A shape that stops following | `shape.Bone = null;` (it stays where it is) |
-
-### Create methods
-
-Every shape except lines and polylines has the same kinds of Create methods:
-
-| Kind | Example | Space |
-|---|---|---|
-| No arguments | `CreateCylinder()` | A white shape of unit size at the world origin: radius 0.5, sizes 1, length 1. |
-| The usual form | `CreateCylinder(endA, endB, radius)` | World space. |
-| The same with a bone first | `CreateCylinder(bone, localEndA, localEndB, radius)` | The bone's local space. |
-| Position and rotation | `CreateCylinder(position, rotation, length, radius)` | World space. |
-
-A few shapes add an obvious extra, such as `CreateCircle(center, normal, radius)` or a stadium with an explicit normal.
-
-Lines and polylines are made of points: they are created from world positions, or from one bone per point, each point sitting at its bone's origin. `CreateLine()` starts with both ends at the world origin.
-
-### A shape's axes
-
-A shape's rotation turns its own axes:
-
-- **+Y is the normal of flat shapes.** Rectangles, circles, ellipses, stars and stadiums lie in their XZ plane, so with no rotation they lie flat on the ground, or flat around their bone.
-- **+Z is the axis of long shapes** (`IAxialShape`: cylinders, cones, capsules, stadiums, frustums and pyramids). They start at end A, at their position, and run `Length` along +Z to end B. Setting `LocalEnd` or `WorldEnd` moves end B: the axis turns by the smallest rotation and `Length` follows.
-- Other shapes are centered on their position.
-- `Quaternion.LookRotation(axis, normal)` is the rotation that points a shape's +Z along `axis` with +Y toward `normal`.
-- A shape created from two points turns so that its +Y stays as close to world up, or its bone's up, as it can. A stadium or ellipse created that way lies as flat as it can.
-- Capsule and stadium ends are the centers of their spheres or circles, as in `Physics.CapsuleCast`. When one sphere holds the other, only the bigger one is drawn.
+**Settings choose how a container draws, and properties change a shape at any time.** Every edit shows up in the next render, however many properties change, and hiding a shape keeps it for later.
-### Sizes and resolution
-
-- Sizes are in the bone's units. Changing a shape's bone multiplies them by the ratio of the two bones' scales, so the shape keeps its size in the world. That is exact for uniformly scaled bones.
-- Sizes are used as given: a negative size mirrors the shape, and zero collapses it. A box keeps a signed `Size`, so both of its corners come back exactly as they were set.
-- Round shapes take `segments`, the number of straight pieces a full circle is drawn with: 32 by default, at least 3. Arcs use their share: capsule caps and stadium ends get half, rounded rectangle corners a quarter. Cylinders, cones, capsules, stadiums and rounded rectangles need a multiple of 4, because their lines meet their rings at quarter points.
-- Round shapes are drawn like Unity's gizmos, with rings and a few lines: a sphere is three circles and a cylinder is two rings joined by four lines. Shapes with corners draw every edge.
-- The number of segments, sides, star points and spikes, and the number of points of a polyline, are fixed when a shape is created. To change them, create a new shape.
-
-### Colors
-
-- Lines and polylines have a color per point, blended along the edges. `SetColor` sets them all.
-- Every other shape has one `Color`. `SetColor` sets it too.
-- Colors are stored with 8 bits per channel, so values above 1 are clamped: no HDR glow.
-- In linear color space (the Unity 6 default), the default shader converts vertex colors so they look the same as that `Color` on a material.
-- The default material is opaque, so alpha has no effect.
-
-### When edits show up
-
-Setters only record the change. Each edited shape is queued once per frame, however many properties you set. The queue is uploaded at the end of `LateUpdate`, after scripts with the default execution order and before Unity renders, so edits appear in the same frame. Edits made later in the frame, such as in `WaitForEndOfFrame` or camera callbacks, appear in the next one.
-
-### Destroyed bones
-
-When a bone's GameObject is destroyed, the shapes and points attached to it stay exactly where they were, at the same size, and switch to world space: their bone becomes `null`. The shapes themselves stay alive.
-
-This works through a small hidden component that the package adds to every GameObject used as a bone and removes when no shape uses it anymore. If a bone's GameObject is destroyed without ever having been active, Unity doesn't notify that component. The package still notices, but the bone's last pose is lost, so its shapes use their local values as world ones and a warning is logged.
-
-### Disposing shapes
-
-`shape.Dispose()` removes a shape and frees its space for the next shape with the same number of vertices. After that, `IsDisposed` is true and every other member throws `ObjectDisposedException`.
-
-## Wireframe camera
-
-Add the **WireframeCamera** component to a Camera to draw everything that camera renders as wireframe, like the Scene view's Wireframe draw mode, while other cameras draw normally.
-
-- It works under the Built-in Render Pipeline and any Scriptable Render Pipeline (URP, HDRP or custom), and follows the pipeline if it changes at runtime. The frame in which the pipeline switches is drawn without wireframe.
-- It uses `GL.wireframe`, so the graphics API must support wireframe rendering; OpenGL ES and WebGL don't.
-- The package's own shapes are lines already, so they look the same through it.
-
-## Performance
-
-- **One mesh, one draw call and one skinned renderer per container**, however many shapes and shape types it holds.
-- **Moving bones costs the package nothing.** Unity computes one matrix per bone and skins the vertices on the GPU.
-- **Edits cost only what changed.** Each edited shape is written once per frame, and only the changed parts of the GPU buffers are uploaded. Editing a shape rewrites all of its vertices, so a big sphere costs more to edit than a line.
-- **Adding and removing shapes is cheap on average.** Buffers double in size when full and never shrink. Freed space is reused by the next shape with the same number of vertices, and it is packed away once it fills half the buffer.
-- **Memory:** 20 bytes per vertex on the GPU (position, color, bone index), plus a CPU copy, and 2 indices per edge. Indices start 16-bit and switch to 32-bit automatically once the vertex buffer outgrows them.
-
-| Shape, with the default resolution | Vertices | Edges |
-|---|---|---|
-| Line | 2 | 1 |
-| Polyline of n points | n | n - 1, or n when closed |
-| Box | 8 | 12 |
-| Rectangle | 4 | 4 |
-| Rounded rectangle | 36 | 36 |
-| Circle, ellipse | 32 | 32 |
-| Star with 5 points | 10 | 10 |
-| Sphere, ellipsoid | 96 | 96 |
-| Spiked sphere with 4, 6 or 8 spikes | 8, 14 or 14 | 18, 36 or 36 |
-| Spiked sphere with 12 or 20 spikes | 32 | 90 |
-| Cylinder | 64 | 68 |
-| Cone | 33 | 36 |
-| Capsule | 122 | 132 |
-| Stadium | 34 | 34 |
-| Frustum with n sides | 2n | 3n |
-| Pyramid | 5 | 8 |
-
-Measured by the package's performance tests in the Editor on a desktop PC (Unity 6000.5, Direct3D 12), on 100 bones:
-
-| Operation | 10,000 lines | 1,000 spheres (96,000 vertices) |
-|---|---|---|
-| Create the shapes on their bones | 4.6 ms | 3.4 ms |
-| First upload | 1.9 ms | 6.8 ms |
-| A frame where nothing changed | 0.1 µs | 0.1 µs |
-| A frame where all 100 bones moved | 2.3 µs | 0.2 µs |
-| Scattered edits: 1,000 line colors, 100 sphere radii | 0.1 ms, plus 0.3 ms upload | 0.01 ms, plus 0.7 ms upload |
-| Dispose half of the shapes | 0.4 ms, plus 0.6 ms upload | 1.4 ms, plus 6.4 ms upload |
-
-Two things to keep in mind:
-
-- **GPU skinning must be on.** If a project disables it in Player Settings, or the platform has no compute shaders (for example WebGL), Unity skins on the CPU every frame, at a cost that grows with the vertex count.
-- **Shapes are never frustum-culled.** Shapes follow arbitrary Transforms, so the mesh uses fixed bounds of ±1,000 km around the world origin instead of recomputing them every frame.
-
-## Limitations
-
-- Lines are always 1 pixel wide.
-- The default material is opaque and depth-tested. For transparency or always-on-top lines, pass your own material.
-- Main thread only. Shapes are drawn in Play mode and in builds, not in Edit mode.
-- One bone per shape, except for lines and polylines, which have one per point.
-- A shape's resolution and a polyline's number of points can't change after it is created.
-
-## Samples
-
-Import them from the package's **Samples** tab in the Package Manager.
-
-**Shape Gallery** lays out every shape in three rows, each turning with its own bone. Open its **ShapeGallery** scene and enter Play mode: the camera frames the rows against a dim background.
-
-**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
-
-To run the package's tests in the Test Runner, add the package to `testables` in `Packages/manifest.json`:
+```csharp
+_wireframes = new WireframeContainer(new WireframeContainerSettings { Occlusion = WireframeOcclusion.Fade });
-```json
-"testables": ["com.reromanlee.wireframes"]
+ISphere sphere = _wireframes.CreateSphere(_target, Vector3.zero, 0.5f);
+sphere.Radius = 2f;
+sphere.IsVisible = false;
```
-## Roadmap
+Detailed about usage - see [USAGE.md](Documentation/USAGE.md).
-- Arrows, camera frustums and more shapes, all drawn by the same mesh.
-- Text drawn with lines.
-- Built-in transparent and always-on-top materials.
+### License
-## License
+Made by Roman Likhadievski ([reromanlee.com](https://reromanlee.com)).
-[MIT](LICENSE.md)
+Licensed under MIT - see [LICENSE.md](LICENSE.md).
diff --git a/UnityPackage/Editor.meta b/UnityPackage/Editor.meta
new file mode 100644
index 0000000..f4bd918
--- /dev/null
+++ b/UnityPackage/Editor.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 56ceb271f26f6631eff17196aa8d32fd
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/UnityPackage/Editor/AssemblyInfo.cs b/UnityPackage/Editor/AssemblyInfo.cs
new file mode 100644
index 0000000..6acaf20
--- /dev/null
+++ b/UnityPackage/Editor/AssemblyInfo.cs
@@ -0,0 +1,3 @@
+using System.Runtime.CompilerServices;
+
+[assembly: InternalsVisibleTo("reromanlee.Wireframes.Tests.Editor")]
diff --git a/UnityPackage/Editor/AssemblyInfo.cs.meta b/UnityPackage/Editor/AssemblyInfo.cs.meta
new file mode 100644
index 0000000..02c5424
--- /dev/null
+++ b/UnityPackage/Editor/AssemblyInfo.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: cb32bf5d1512b7a80e7d47d92d0f380c
\ No newline at end of file
diff --git a/UnityPackage/Editor/EditModeLifecycle.cs b/UnityPackage/Editor/EditModeLifecycle.cs
new file mode 100644
index 0000000..b49a2e7
--- /dev/null
+++ b/UnityPackage/Editor/EditModeLifecycle.cs
@@ -0,0 +1,107 @@
+using System.Collections.Generic;
+using UnityEditor;
+using UnityEditor.SceneManagement;
+using UnityEngine;
+using UnityEngine.SceneManagement;
+
+namespace reromanlee.Wireframes.Editor
+{
+ ///
+ /// Gives containers created in Edit Mode the lifetime a scene object would have. They are disposed before scripts
+ /// reload, which would otherwise leave their GameObjects behind without their state, and when the scene they were
+ /// created in closes, unless they persist across scenes. Their idle chunks are released from the editor loop, where
+ /// objects may be destroyed right away. Entering or leaving Play Mode alone disposes nothing.
+ ///
+ [InitializeOnLoad]
+ internal static class EditModeLifecycle
+ {
+ static EditModeLifecycle()
+ {
+ AssemblyReloadEvents.beforeAssemblyReload += DisposeEveryContainer;
+ EditorSceneManager.sceneClosing += OnSceneClosing;
+ EditorApplication.update += ReleaseIdleChunks;
+ }
+
+ /// Disposes every container, Play Mode ones included, destroying their objects right away.
+ internal static void DisposeEveryContainer()
+ {
+ IReadOnlyList proxies = MeshProxy.LiveProxies;
+ // No frame comes after a reload starts, so nothing may be left for the end of one.
+ UnityObjects.DestroysImmediately = true;
+ try
+ {
+ for (int i = proxies.Count - 1; i >= 0; i--)
+ {
+ proxies[i].Container.Dispose();
+ }
+ }
+ finally
+ {
+ UnityObjects.DestroysImmediately = false;
+ }
+ }
+
+ ///
+ /// Disposes the Edit Mode containers of a scene that closes, and moves the persistent ones into another open
+ /// scene, since closing a scene destroys even the objects it never saves.
+ ///
+ internal static void OnSceneClosing(Scene scene, bool removingScene)
+ {
+ // Entering Play Mode closes the edited scene, and leaving it closes the played ones; neither disposes anything.
+ if (Application.isPlaying || EditorApplication.isPlayingOrWillChangePlaymode)
+ {
+ return;
+ }
+ IReadOnlyList proxies = MeshProxy.LiveProxies;
+ for (int i = proxies.Count - 1; i >= 0; i--)
+ {
+ MeshProxy proxy = proxies[i];
+ if (!proxy.IsEditMode || proxy.HomeScene != scene)
+ {
+ continue;
+ }
+ if (proxy.PersistsAcrossScenes)
+ {
+ MoveToAnotherScene(proxy, scene);
+ }
+ else
+ {
+ proxy.Container.Dispose();
+ }
+ }
+ }
+
+ /// Releases the idle chunks of every container while not in Play Mode, which releases its own.
+ internal static void ReleaseIdleChunks()
+ {
+ if (Application.isPlaying)
+ {
+ return;
+ }
+ IReadOnlyList proxies = MeshProxy.LiveProxies;
+ float time = Time.realtimeSinceStartup;
+ for (int i = proxies.Count - 1; i >= 0; i--)
+ {
+ proxies[i].ReleaseIdleChunks(time);
+ }
+ }
+
+ ///
+ /// Moves into another loaded scene. When none is left, as when a scene replaces all
+ /// open ones, the proxy stays out of any scene, where it keeps drawing.
+ ///
+ private static void MoveToAnotherScene(MeshProxy proxy, Scene closing)
+ {
+ for (int i = 0; i < SceneManager.sceneCount; i++)
+ {
+ Scene scene = SceneManager.GetSceneAt(i);
+ if (scene.isLoaded && scene != closing)
+ {
+ SceneManager.MoveGameObjectToScene(proxy.gameObject, scene);
+ proxy.HomeScene = scene;
+ return;
+ }
+ }
+ }
+ }
+}
diff --git a/UnityPackage/Editor/EditModeLifecycle.cs.meta b/UnityPackage/Editor/EditModeLifecycle.cs.meta
new file mode 100644
index 0000000..8d1f77a
--- /dev/null
+++ b/UnityPackage/Editor/EditModeLifecycle.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 29ee05747cac57fd2263b490e307a3ed
\ No newline at end of file
diff --git a/UnityPackage/Editor/reromanlee.Wireframes.Editor.asmdef b/UnityPackage/Editor/reromanlee.Wireframes.Editor.asmdef
new file mode 100644
index 0000000..5a97294
--- /dev/null
+++ b/UnityPackage/Editor/reromanlee.Wireframes.Editor.asmdef
@@ -0,0 +1,18 @@
+{
+ "name": "reromanlee.Wireframes.Editor",
+ "rootNamespace": "reromanlee.Wireframes.Editor",
+ "references": [
+ "reromanlee.Wireframes"
+ ],
+ "includePlatforms": [
+ "Editor"
+ ],
+ "excludePlatforms": [],
+ "allowUnsafeCode": false,
+ "overrideReferences": false,
+ "precompiledReferences": [],
+ "autoReferenced": true,
+ "defineConstraints": [],
+ "versionDefines": [],
+ "noEngineReferences": false
+}
diff --git a/UnityPackage/Editor/reromanlee.Wireframes.Editor.asmdef.meta b/UnityPackage/Editor/reromanlee.Wireframes.Editor.asmdef.meta
new file mode 100644
index 0000000..6b11845
--- /dev/null
+++ b/UnityPackage/Editor/reromanlee.Wireframes.Editor.asmdef.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: 14222136f3057885a71657de5caf1352
+AssemblyDefinitionImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/UnityPackage/Runtime/AssemblyInfo.cs b/UnityPackage/Runtime/AssemblyInfo.cs
index 333da64..ebc5f83 100644
--- a/UnityPackage/Runtime/AssemblyInfo.cs
+++ b/UnityPackage/Runtime/AssemblyInfo.cs
@@ -1,4 +1,6 @@
using System.Runtime.CompilerServices;
+[assembly: InternalsVisibleTo("reromanlee.Wireframes.Editor")]
[assembly: InternalsVisibleTo("reromanlee.Wireframes.Tests.Editor")]
[assembly: InternalsVisibleTo("reromanlee.Wireframes.Tests.Runtime")]
+[assembly: InternalsVisibleTo("reromanlee.Wireframes.Tests.Performance")]
diff --git a/UnityPackage/Runtime/LineContainer.meta b/UnityPackage/Runtime/Container.meta
similarity index 100%
rename from UnityPackage/Runtime/LineContainer.meta
rename to UnityPackage/Runtime/Container.meta
diff --git a/UnityPackage/Runtime/Container/WireframeContainer.cs b/UnityPackage/Runtime/Container/WireframeContainer.cs
new file mode 100644
index 0000000..a9afe1e
--- /dev/null
+++ b/UnityPackage/Runtime/Container/WireframeContainer.cs
@@ -0,0 +1,148 @@
+using System;
+using UnityEngine;
+using Object = UnityEngine.Object;
+
+namespace reromanlee.Wireframes
+{
+ ///
+ /// Creates shapes and draws them with GPU-skinned meshes of up to 65,535 vertices each, plus one of its own for any
+ /// bigger shape. Disposing the container, or unloading the scene it was created in, disposes every shape it created.
+ ///
+ ///
+ /// Main thread only. Each shape type adds its Create methods as extension methods, declared in a factory class next
+ /// to the shape, such as . The container creates a GameObject in the active scene; edits
+ /// made to its shapes are uploaded right before a camera renders them. Without a graphics device, as in server
+ /// builds, or without a usable shader, shapes keep working but nothing is drawn or uploaded. Bones must be scene
+ /// objects: Create methods and bone setters throw for a prefab asset.
+ ///
+ /// Containers 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, though
+ /// the script reload that Play Mode starts with by default does.
+ ///
+ ///
+ public sealed class WireframeContainer : IDisposable
+ {
+ private readonly MeshProxy _proxy;
+ private bool _isDisposed;
+
+ /// Creates a container with the default settings.
+ public WireframeContainer() : this(null)
+ {
+ }
+
+ ///
+ /// Creates a container from , or from the default settings when it is null. The
+ /// settings are read once, so changing them later doesn't affect the container.
+ ///
+ /// A setting is outside its valid range.
+ public WireframeContainer(WireframeContainerSettings settings)
+ {
+ MainThread.Check();
+ settings ??= new WireframeContainerSettings();
+ settings.Validate(nameof(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);
+ 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)
+ {
+ Object.DontDestroyOnLoad(proxyObject);
+ }
+ _proxy = proxyObject.AddComponent();
+ _proxy.Initialize(this, settings);
+ }
+ catch
+ {
+ UnityObjects.Destroy(proxyObject);
+ throw;
+ }
+ }
+
+ /// True once the container was disposed or its scene was unloaded.
+ public bool IsDisposed
+ {
+ get => _isDisposed;
+ }
+
+ /// What the container holds and the memory it uses right now. Reading it allocates nothing.
+ /// The container is disposed.
+ public WireframeStatistics Statistics
+ {
+ get
+ {
+ MainThread.Check();
+ return Proxy.GetStatistics();
+ }
+ }
+
+ ///
+ /// False hides every shape of the container without disposing anything, and costs nothing while it lasts: edits
+ /// made meanwhile are applied when it is shown again. Each shape's own still
+ /// counts. True by default.
+ ///
+ /// The container is disposed.
+ public bool IsVisible
+ {
+ get
+ {
+ MainThread.Check();
+ return Proxy.IsVisible;
+ }
+ set
+ {
+ MainThread.Check();
+ Proxy.IsVisible = value;
+ }
+ }
+
+ internal MeshProxy Proxy
+ {
+ get
+ {
+ if (_isDisposed)
+ {
+ throw new ObjectDisposedException(nameof(WireframeContainer));
+ }
+ return _proxy;
+ }
+ }
+
+ /// Removes the container and disposes every shape it created. Calling it again does nothing.
+ public void Dispose()
+ {
+ MainThread.Check();
+ if (_isDisposed)
+ {
+ return;
+ }
+ try
+ {
+ _proxy.Shutdown();
+ }
+ finally
+ {
+ UnityObjects.Destroy(_proxy.gameObject);
+ }
+ }
+
+ /// The proxy that a factory adds a new shape to, after checking .
+ internal static MeshProxy ProxyOf(WireframeContainer container)
+ {
+ MainThread.Check();
+ if (container == null)
+ {
+ throw new ArgumentNullException(nameof(container));
+ }
+ return container.Proxy;
+ }
+
+ internal void OnProxyShutdown()
+ {
+ _isDisposed = true;
+ }
+ }
+}
diff --git a/UnityPackage/Runtime/LineContainer/LineContainer.cs.meta b/UnityPackage/Runtime/Container/WireframeContainer.cs.meta
similarity index 100%
rename from UnityPackage/Runtime/LineContainer/LineContainer.cs.meta
rename to UnityPackage/Runtime/Container/WireframeContainer.cs.meta
diff --git a/UnityPackage/Runtime/Container/WireframeContainerSettings.cs b/UnityPackage/Runtime/Container/WireframeContainerSettings.cs
new file mode 100644
index 0000000..8dca4af
--- /dev/null
+++ b/UnityPackage/Runtime/Container/WireframeContainerSettings.cs
@@ -0,0 +1,147 @@
+using System;
+using UnityEngine;
+
+namespace reromanlee.Wireframes
+{
+ ///
+ /// How a is created. It is serializable, so a script can expose it in the Inspector.
+ ///
+ [Serializable]
+ public sealed class WireframeContainerSettings
+ {
+ private const string DefaultName = "Wireframes";
+ private const int MaxLayer = 31;
+
+ [Tooltip("Name of the container's GameObject in the Hierarchy.")]
+ [SerializeField] private string _name = DefaultName;
+
+ [Tooltip("Material to draw with. None uses the package's vertex color material. The container never destroys it.")]
+ [SerializeField] private Material _material;
+
+ [Tooltip("Layer of the container's GameObjects, so cameras can include or exclude wireframes with their culling mask.")]
+ [SerializeField, Range(0, MaxLayer)] private int _layer;
+
+ [Tooltip("Keeps the container when other scenes load, instead of disposing it with the scene it was created in.")]
+ [SerializeField] private bool _persistAcrossScenes;
+
+ [Tooltip("Vertices reserved up front, so a load known in advance never grows the buffers later.")]
+ [SerializeField, Min(0)] private int _vertexCapacity;
+
+ [Tooltip("Edges reserved up front, so a load known in advance never grows the buffers later.")]
+ [SerializeField, Min(0)] private int _edgeCapacity;
+
+ [Tooltip("What is drawn of lines that other geometry hides. Ignored with a custom material.")]
+ [SerializeField] private WireframeOcclusion _occlusion = WireframeOcclusion.Hide;
+
+ [Tooltip("Blends each color by its alpha. Ignored with a custom material.")]
+ [SerializeField] private bool _useAlpha;
+
+ /// Name of the container's GameObject in the Hierarchy. Null or empty uses "Wireframes".
+ public string Name
+ {
+ get => _name;
+ set => _name = value;
+ }
+
+ ///
+ /// Material to draw with, or null for the package's vertex color material. The container never destroys a
+ /// material passed here.
+ ///
+ public Material Material
+ {
+ get => _material;
+ set => _material = value;
+ }
+
+ ///
+ /// Layer of the container's GameObjects, from 0 to 31, so cameras can include or exclude wireframes with their
+ /// culling mask.
+ ///
+ public int Layer
+ {
+ get => _layer;
+ set => _layer = value;
+ }
+
+ ///
+ /// True keeps the container when other scenes load, like Object.DontDestroyOnLoad in Play Mode and when
+ /// scenes are opened or closed in Edit Mode. False, the default, disposes it with the scene that was active when
+ /// it was created.
+ ///
+ public bool PersistAcrossScenes
+ {
+ get => _persistAcrossScenes;
+ set => _persistAcrossScenes = value;
+ }
+
+ ///
+ /// Vertices reserved up front, 0 or more, so a load known in advance never grows the buffers later. Reserved
+ /// memory stays until the container is disposed; memory past it is given back as shapes are disposed.
+ ///
+ public int VertexCapacity
+ {
+ get => _vertexCapacity;
+ set => _vertexCapacity = value;
+ }
+
+ ///
+ /// Edges reserved up front, 0 or more, so a load known in advance never grows the buffers later. Reserved memory
+ /// stays until the container is disposed; memory past it is given back as shapes are disposed.
+ ///
+ public int EdgeCapacity
+ {
+ get => _edgeCapacity;
+ set => _edgeCapacity = value;
+ }
+
+ ///
+ /// What is drawn of lines that other geometry hides: nothing (the default), everything, or a dimmer line. It
+ /// applies to the package's material and is ignored with a custom .
+ ///
+ public WireframeOcclusion Occlusion
+ {
+ get => _occlusion;
+ set => _occlusion = value;
+ }
+
+ ///
+ /// True blends each color by its alpha, drawing the lines as transparent; false, the default, draws them opaque.
+ /// It applies to the package's material and is ignored with a custom .
+ ///
+ public bool UseAlpha
+ {
+ get => _useAlpha;
+ set => _useAlpha = value;
+ }
+
+ internal string ResolvedName
+ {
+ get => string.IsNullOrEmpty(_name) ? DefaultName : _name;
+ }
+
+ /// Throws for settings outside their valid range, naming as the argument.
+ internal void Validate(string parameterName)
+ {
+ if (_layer < 0 || _layer > MaxLayer)
+ {
+ throw new ArgumentOutOfRangeException(
+ parameterName, _layer, $"{nameof(Layer)} must be between 0 and {MaxLayer}.");
+ }
+ if (_vertexCapacity < 0)
+ {
+ throw new ArgumentOutOfRangeException(
+ parameterName, _vertexCapacity, $"{nameof(VertexCapacity)} can't be negative.");
+ }
+ if (_edgeCapacity < 0)
+ {
+ throw new ArgumentOutOfRangeException(
+ parameterName, _edgeCapacity, $"{nameof(EdgeCapacity)} can't be negative.");
+ }
+ if (_occlusion < WireframeOcclusion.Hide || _occlusion > WireframeOcclusion.Fade)
+ {
+ throw new ArgumentOutOfRangeException(
+ parameterName, _occlusion, $"{nameof(Occlusion)} isn't a {nameof(WireframeOcclusion)} value.");
+ }
+ }
+ }
+}
diff --git a/UnityPackage/Runtime/Container/WireframeContainerSettings.cs.meta b/UnityPackage/Runtime/Container/WireframeContainerSettings.cs.meta
new file mode 100644
index 0000000..91de0cf
--- /dev/null
+++ b/UnityPackage/Runtime/Container/WireframeContainerSettings.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 7865d9499286d508fb152c91efeb703b
\ No newline at end of file
diff --git a/UnityPackage/Runtime/Container/WireframeOcclusion.cs b/UnityPackage/Runtime/Container/WireframeOcclusion.cs
new file mode 100644
index 0000000..c129ae9
--- /dev/null
+++ b/UnityPackage/Runtime/Container/WireframeOcclusion.cs
@@ -0,0 +1,15 @@
+namespace reromanlee.Wireframes
+{
+ /// What a container draws of lines that other geometry hides.
+ public enum WireframeOcclusion
+ {
+ /// Hidden parts are not drawn, like any other object.
+ Hide,
+
+ /// Lines are drawn over everything, so hidden parts show at full strength.
+ Show,
+
+ /// Hidden parts are drawn dimmer, like the Scene view's handles. It takes a second draw call.
+ Fade
+ }
+}
diff --git a/UnityPackage/Runtime/Container/WireframeOcclusion.cs.meta b/UnityPackage/Runtime/Container/WireframeOcclusion.cs.meta
new file mode 100644
index 0000000..63dc497
--- /dev/null
+++ b/UnityPackage/Runtime/Container/WireframeOcclusion.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: f1a6c8c469a6e13906618b5a616c11b1
\ No newline at end of file
diff --git a/UnityPackage/Runtime/Container/WireframeStatistics.cs b/UnityPackage/Runtime/Container/WireframeStatistics.cs
new file mode 100644
index 0000000..68ab944
--- /dev/null
+++ b/UnityPackage/Runtime/Container/WireframeStatistics.cs
@@ -0,0 +1,63 @@
+namespace reromanlee.Wireframes
+{
+ ///
+ /// What a container holds and the memory it uses, read from . Reading it
+ /// walks the container's chunks and allocates nothing.
+ ///
+ public readonly struct WireframeStatistics
+ {
+ internal WireframeStatistics(
+ int shapeCount,
+ int hiddenShapeCount,
+ int vertexCount,
+ int edgeCount,
+ int boneCount,
+ int chunkCount,
+ long cpuMemory,
+ long gpuMemory)
+ {
+ ShapeCount = shapeCount;
+ HiddenShapeCount = hiddenShapeCount;
+ VertexCount = vertexCount;
+ EdgeCount = edgeCount;
+ BoneCount = boneCount;
+ ChunkCount = chunkCount;
+ CpuMemory = cpuMemory;
+ GpuMemory = gpuMemory;
+ }
+
+ /// Shapes in the container, hidden ones included.
+ public int ShapeCount { get; }
+
+ /// Shapes hidden with .
+ public int HiddenShapeCount { get; }
+
+ /// Vertices the shapes use.
+ public int VertexCount { get; }
+
+ /// Edges drawn, which leaves out those of hidden shapes.
+ public int EdgeCount { get; }
+
+ /// Distinct Transforms the shapes follow.
+ public int BoneCount { get; }
+
+ ///
+ /// Meshes the shapes are split into, of up to 65,535 vertices each. Every chunk is one draw call per camera, or
+ /// two with .
+ ///
+ public int ChunkCount { get; }
+
+ /// Bytes of CPU memory in the container's buffers, about; the shapes' own objects aren't counted.
+ public long CpuMemory { get; }
+
+ /// Bytes of GPU memory in the container's vertex, index and bone buffers.
+ public long GpuMemory { get; }
+
+ public override string ToString()
+ {
+ return $"Shapes: {ShapeCount} ({HiddenShapeCount} hidden), vertices: {VertexCount}, edges: {EdgeCount}, " +
+ $"bones: {BoneCount}, chunks: {ChunkCount}, CPU memory: {CpuMemory / 1024} KB, " +
+ $"GPU memory: {GpuMemory / 1024} KB";
+ }
+ }
+}
diff --git a/UnityPackage/Runtime/Container/WireframeStatistics.cs.meta b/UnityPackage/Runtime/Container/WireframeStatistics.cs.meta
new file mode 100644
index 0000000..79dbf48
--- /dev/null
+++ b/UnityPackage/Runtime/Container/WireframeStatistics.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 5441ba86ba206f9725d06dab077e64ef
\ No newline at end of file
diff --git a/UnityPackage/Runtime/Core/AxialShape.cs b/UnityPackage/Runtime/Core/AxialShape.cs
index 804c279..bdaff49 100644
--- a/UnityPackage/Runtime/Core/AxialShape.cs
+++ b/UnityPackage/Runtime/Core/AxialShape.cs
@@ -12,13 +12,12 @@ internal abstract class AxialShape : RigidShape, IAxialShape
private float _length;
protected AxialShape(
- MeshProxy proxy,
int vertexCount,
- int[] edgePattern,
+ EdgeSource edgeSource,
Transform bone,
Vector3 localPosition,
Quaternion localRotation,
- float length) : base(proxy, vertexCount, edgePattern, bone, localPosition, localRotation)
+ float length) : base(vertexCount, edgeSource, bone, localPosition, localRotation)
{
_length = length;
}
@@ -27,27 +26,43 @@ public float Length
{
get
{
- ThrowIfDisposed();
+ EnsureUsable();
return _length;
}
set
{
- ThrowIfDisposed();
+ EnsureUsable();
_length = value;
MarkDirty(DirtyFlags.Positions);
}
}
- public Vector3 LocalEnd
+ public Vector3 LocalEndA
{
get
{
- ThrowIfDisposed();
+ EnsureUsable();
+ return LocalPosition;
+ }
+ set
+ {
+ EnsureUsable();
+ Vector3 endB = LocalEndB;
+ LocalPosition = value;
+ LocalEndB = endB;
+ }
+ }
+
+ public Vector3 LocalEndB
+ {
+ get
+ {
+ EnsureUsable();
return LocalPosition + LocalRotation * new Vector3(0f, 0f, _length);
}
set
{
- ThrowIfDisposed();
+ EnsureUsable();
Vector3 axis = value - LocalPosition;
float length = axis.magnitude;
// An end on top of end A has no direction, so the rotation stays as it is.
@@ -62,17 +77,31 @@ public Vector3 LocalEnd
}
}
- public Vector3 WorldEnd
+ public Vector3 WorldEndA
+ {
+ get
+ {
+ EnsureUsable();
+ return ToWorld(Bone, LocalEndA);
+ }
+ set
+ {
+ EnsureUsable();
+ LocalEndA = ToLocal(Bone, value);
+ }
+ }
+
+ public Vector3 WorldEndB
{
get
{
- ThrowIfDisposed();
- return ToWorld(Bone, LocalEnd);
+ EnsureUsable();
+ return ToWorld(Bone, LocalEndB);
}
set
{
- ThrowIfDisposed();
- LocalEnd = ToLocal(Bone, value);
+ EnsureUsable();
+ LocalEndB = ToLocal(Bone, value);
}
}
diff --git a/UnityPackage/Runtime/Core/AxisRings.cs b/UnityPackage/Runtime/Core/AxisRings.cs
index 4ace073..d924c1e 100644
--- a/UnityPackage/Runtime/Core/AxisRings.cs
+++ b/UnityPackage/Runtime/Core/AxisRings.cs
@@ -15,22 +15,22 @@ internal static class AxisRings
/// Writes the three rings, each with a third of the positions, with the given radius along each axis.
internal static void Write(Span positions, Vector3 radii)
{
- int segments = positions.Length / RingCount;
+ int segmentCount = positions.Length / RingCount;
Vector3 x = new(radii.x, 0f, 0f);
Vector3 y = new(0f, radii.y, 0f);
Vector3 z = new(0f, 0f, radii.z);
- Ring.Write(positions.Slice(0, segments), Vector3.zero, z, x);
- Ring.Write(positions.Slice(segments, segments), Vector3.zero, y, x);
- Ring.Write(positions.Slice(segments * 2, segments), Vector3.zero, y, z);
+ Ring.Write(positions.Slice(0, segmentCount), Vector3.zero, z, x);
+ Ring.Write(positions.Slice(segmentCount, segmentCount), Vector3.zero, y, x);
+ Ring.Write(positions.Slice(segmentCount * 2, segmentCount), Vector3.zero, y, z);
}
- private static int[] BuildEdges(int segments)
+ private static int[] BuildEdges(int segmentCount)
{
- int[] pattern = new int[segments * RingCount * 2];
+ int[] pattern = new int[segmentCount * RingCount * 2];
int cursor = 0;
for (int ring = 0; ring < RingCount; ring++)
{
- Ring.AddEdges(pattern, ref cursor, ring * segments, segments);
+ Ring.AddEdges(pattern, ref cursor, ring * segmentCount, segmentCount);
}
return pattern;
}
diff --git a/UnityPackage/Runtime/Core/BoneRegistry.cs b/UnityPackage/Runtime/Core/BoneRegistry.cs
deleted file mode 100644
index 1ca20a9..0000000
--- a/UnityPackage/Runtime/Core/BoneRegistry.cs
+++ /dev/null
@@ -1,161 +0,0 @@
-using System;
-using System.Collections.Generic;
-using UnityEngine;
-
-namespace reromanlee.Wireframes
-{
- ///
- /// Maps the transforms that shapes follow to bone slots of one skinned mesh, counting the endpoints using each.
- /// Slot 0 is the chunk's own transform and stands for world space. Freed slots are reused, so no other vertex
- /// ever needs its bone index rewritten.
- ///
- internal sealed class BoneRegistry : IDisposable
- {
- private const int InitialCapacity = 16;
-
- private readonly Transform _root;
- private readonly Action _onBoneDestroyed;
- private readonly Dictionary _slots = new();
- private readonly Stack _freeSlots = new();
- // Watchers whose Awake hasn't run: Unity skips OnDestroy for them, so only these are checked every flush.
- private readonly List _sleepers = new();
-
- private Transform[] _transforms = new Transform[InitialCapacity];
- private BoneWatcher[] _watchers = new BoneWatcher[InitialCapacity];
- private int[] _referenceCounts = new int[InitialCapacity];
- private int _end = 1;
-
- internal BoneRegistry(Transform root, Action onBoneDestroyed)
- {
- _root = root;
- _onBoneDestroyed = onBoneDestroyed;
- Array.Fill(_transforms, root);
- IsDirty = true;
- }
-
- /// Bones for the renderer, sized to the capacity. Unused slots hold the root.
- internal Transform[] Transforms
- {
- get => _transforms;
- }
-
- /// True when changed since the renderer last received it.
- internal bool IsDirty { get; private set; }
-
- /// Number of distinct transforms in use, not counting the root.
- internal int Count
- {
- get => _slots.Count;
- }
-
- internal void ClearDirty()
- {
- IsDirty = false;
- }
-
- /// Returns the slot for , registering it on first use. Null means world space.
- internal int Acquire(Transform bone)
- {
- if (bone == null)
- {
- return 0;
- }
- if (_slots.TryGetValue(bone, out int slot))
- {
- _referenceCounts[slot]++;
- return slot;
- }
- slot = _freeSlots.Count > 0 ? _freeSlots.Pop() : _end++;
- EnsureCapacity(slot + 1);
- BoneWatcher watcher = BoneWatcher.Watch(bone, this);
- _slots.Add(bone, slot);
- _transforms[slot] = bone;
- _watchers[slot] = watcher;
- _referenceCounts[slot] = 1;
- if (!watcher.IsAwake)
- {
- _sleepers.Add(watcher);
- }
- IsDirty = true;
- return slot;
- }
-
- internal void Release(int slot)
- {
- if (slot == 0 || --_referenceCounts[slot] > 0)
- {
- return;
- }
- BoneWatcher watcher = _watchers[slot];
- // Dictionary lookups compare instance IDs, so this also works for a transform that was just destroyed.
- _slots.Remove(_transforms[slot]);
- _transforms[slot] = _root;
- _watchers[slot] = null;
- _freeSlots.Push(slot);
- _sleepers.Remove(watcher);
- watcher.RemoveListener(this);
- IsDirty = true;
- }
-
- /// Called by a watcher whose bone is being destroyed.
- internal void OnBoneDestroyed(Transform bone)
- {
- _onBoneDestroyed(bone);
- }
-
- /// Catches bones that were destroyed before their GameObject was ever active.
- internal void PollSleepers()
- {
- for (int i = _sleepers.Count - 1; i >= 0; i--)
- {
- BoneWatcher watcher = _sleepers[i];
- if (watcher.IsAwake)
- {
- _sleepers.RemoveAt(i);
- }
- else if (watcher == null)
- {
- _sleepers.RemoveAt(i);
- Debug.LogWarning(
- "Wireframes: a bone was destroyed before its GameObject was ever active, so its last pose is " +
- "unknown. The endpoints attached to it now use their local offsets as world positions.");
- _onBoneDestroyed(watcher.Bone);
- }
- }
- }
-
- public void Dispose()
- {
- for (int slot = 1; slot < _end; slot++)
- {
- BoneWatcher watcher = _watchers[slot];
- if (!ReferenceEquals(watcher, null))
- {
- watcher.RemoveListener(this);
- _watchers[slot] = null;
- }
- }
- _slots.Clear();
- _sleepers.Clear();
- _freeSlots.Clear();
- }
-
- private void EnsureCapacity(int capacity)
- {
- int oldCapacity = _transforms.Length;
- if (capacity <= oldCapacity)
- {
- return;
- }
- int newCapacity = oldCapacity;
- while (newCapacity < capacity)
- {
- newCapacity *= 2;
- }
- Array.Resize(ref _transforms, newCapacity);
- Array.Fill(_transforms, _root, oldCapacity, newCapacity - oldCapacity);
- Array.Resize(ref _watchers, newCapacity);
- Array.Resize(ref _referenceCounts, newCapacity);
- }
- }
-}
diff --git a/UnityPackage/Runtime/Core/BoneWatcher.cs b/UnityPackage/Runtime/Core/BoneWatcher.cs
deleted file mode 100644
index d409e40..0000000
--- a/UnityPackage/Runtime/Core/BoneWatcher.cs
+++ /dev/null
@@ -1,79 +0,0 @@
-using System.Collections.Generic;
-using UnityEngine;
-
-namespace reromanlee.Wireframes
-{
- ///
- /// Hidden component added to GameObjects used as bones. Its OnDestroy runs while the transform can still be read,
- /// so attached endpoints can detach without moving. It removes itself once no registry uses the bone.
- ///
- [AddComponentMenu("")]
- internal sealed class BoneWatcher : MonoBehaviour
- {
- private static readonly List Candidates = new();
-
- private readonly List _registries = new();
- private bool _destroyRequested;
-
- internal Transform Bone { get; private set; }
-
- internal bool IsAwake { get; private set; }
-
- internal static BoneWatcher Watch(Transform bone, BoneRegistry registry)
- {
- BoneWatcher watcher = null;
- bone.GetComponents(Candidates);
- foreach (BoneWatcher candidate in Candidates)
- {
- // A watcher already scheduled for destruction can't be revived, so a fresh one is added instead.
- if (!candidate._destroyRequested)
- {
- watcher = candidate;
- break;
- }
- }
- Candidates.Clear();
-
- if (watcher == null)
- {
- watcher = bone.gameObject.AddComponent();
- watcher.hideFlags = HideFlags.HideInInspector;
- watcher.Bone = bone;
- }
- watcher._registries.Add(registry);
- return watcher;
- }
-
- internal void RemoveListener(BoneRegistry registry)
- {
- _registries.Remove(registry);
- // The null check covers a watcher that is already gone together with its bone.
- if (_registries.Count == 0 && !_destroyRequested && this != null)
- {
- _destroyRequested = true;
- UnityObjects.Destroy(this);
- }
- }
-
- private void Awake()
- {
- IsAwake = true;
- }
-
- private void OnDestroy()
- {
- if (_destroyRequested || _registries.Count == 0)
- {
- return;
- }
- _destroyRequested = true;
- // Registries release the bone while being notified, which edits the list, so they are notified from a copy.
- BoneRegistry[] registries = _registries.ToArray();
- _registries.Clear();
- foreach (BoneRegistry registry in registries)
- {
- registry.OnBoneDestroyed(Bone);
- }
- }
- }
-}
diff --git a/UnityPackage/Runtime/Core/BoneWatcher.cs.meta b/UnityPackage/Runtime/Core/BoneWatcher.cs.meta
deleted file mode 100644
index 95200e3..0000000
--- a/UnityPackage/Runtime/Core/BoneWatcher.cs.meta
+++ /dev/null
@@ -1,2 +0,0 @@
-fileFormatVersion: 2
-guid: d1777ab170b84e4b9790709b14a44771
diff --git a/UnityPackage/Runtime/Core/CirclePoints.cs b/UnityPackage/Runtime/Core/CirclePoints.cs
new file mode 100644
index 0000000..88a21bb
--- /dev/null
+++ b/UnityPackage/Runtime/Core/CirclePoints.cs
@@ -0,0 +1,41 @@
+using System;
+using System.Runtime.CompilerServices;
+using UnityEngine;
+
+namespace reromanlee.Wireframes
+{
+ ///
+ /// Points of a unit circle at equal angles, starting at angle 0 and turning from +X toward +Y, worked out one after
+ /// another instead of kept in a table. Each step rotates the last point in double precision, so even a full turn of
+ /// thousands of steps stays far closer than float precision can tell.
+ ///
+ internal struct CirclePoints
+ {
+ private readonly double _stepCosine;
+ private readonly double _stepSine;
+ private double _x;
+ private double _y;
+
+ /// Points in a full turn.
+ /// Index of the first point to return.
+ internal CirclePoints(int count, int first = 0)
+ {
+ double step = 2.0 * Math.PI / count;
+ _stepCosine = Math.Cos(step);
+ _stepSine = Math.Sin(step);
+ _x = Math.Cos(step * first);
+ _y = Math.Sin(step * first);
+ }
+
+ /// Returns the current point and moves on to the next one: (cosine, sine) of its angle.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ internal Vector2 Next()
+ {
+ Vector2 point = new((float)_x, (float)_y);
+ double x = _x * _stepCosine - _y * _stepSine;
+ _y = _x * _stepSine + _y * _stepCosine;
+ _x = x;
+ return point;
+ }
+ }
+}
diff --git a/UnityPackage/Runtime/Core/CirclePoints.cs.meta b/UnityPackage/Runtime/Core/CirclePoints.cs.meta
new file mode 100644
index 0000000..2eff73a
--- /dev/null
+++ b/UnityPackage/Runtime/Core/CirclePoints.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 54f40c841f0745ad305b403dd8d4841d
\ No newline at end of file
diff --git a/UnityPackage/Runtime/Core/DirtyRanges.cs b/UnityPackage/Runtime/Core/DirtyRanges.cs
index 6455098..9a41114 100644
--- a/UnityPackage/Runtime/Core/DirtyRanges.cs
+++ b/UnityPackage/Runtime/Core/DirtyRanges.cs
@@ -1,5 +1,4 @@
using System;
-using System.Collections.Generic;
using UnityEngine;
namespace reromanlee.Wireframes
@@ -13,11 +12,14 @@ internal sealed class DirtyRanges
private const int MergeGap = 64;
// Past this many separate ranges, a single covering upload is cheaper.
private const int MaxRanges = 16;
-
- private static readonly Comparer ByStart = Comparer.Create((a, b) => a.start.CompareTo(b.start));
+ // Past this many collected ranges, they are merged as they come, so the list stays small when no flush clears
+ // it, such as while nothing renders.
+ private const int MaxCollectedRanges = 256;
private RangeInt[] _ranges = new RangeInt[16];
private int _count;
+ // Once so many ranges came in that they collapsed into one covering range, later ones only widen it.
+ private bool _isCovering;
internal int Count
{
@@ -35,9 +37,28 @@ internal void Add(int start, int length)
{
return;
}
+ if (_isCovering)
+ {
+ RangeInt covering = _ranges[0];
+ int coveringStart = Math.Min(covering.start, start);
+ _ranges[0] = new RangeInt(coveringStart, Math.Max(covering.end, start + length) - coveringStart);
+ return;
+ }
if (_count == _ranges.Length)
{
- Array.Resize(ref _ranges, _count * 2);
+ if (_count >= MaxCollectedRanges)
+ {
+ _isCovering = Merge() == 1;
+ if (_isCovering)
+ {
+ Add(start, length);
+ return;
+ }
+ }
+ if (_count == _ranges.Length)
+ {
+ Array.Resize(ref _ranges, _count * 2);
+ }
}
_ranges[_count++] = new RangeInt(start, length);
}
@@ -45,6 +66,7 @@ internal void Add(int start, int length)
internal void Clear()
{
_count = 0;
+ _isCovering = false;
}
/// Sorts and merges the collected ranges in place and returns how many remain.
@@ -54,7 +76,7 @@ internal int Merge()
{
return _count;
}
- Array.Sort(_ranges, 0, _count, ByStart);
+ SortByStart();
int merged = 0;
RangeInt current = _ranges[0];
for (int i = 1; i < _count; i++)
@@ -79,5 +101,24 @@ internal int Merge()
_count = merged;
return merged;
}
+
+ ///
+ /// Insertion sort by start: there are at most a few hundred ranges, often already in order, and unlike
+ /// Array.Sort with a comparer, it allocates nothing.
+ ///
+ private void SortByStart()
+ {
+ for (int i = 1; i < _count; i++)
+ {
+ RangeInt range = _ranges[i];
+ int j = i - 1;
+ while (j >= 0 && _ranges[j].start > range.start)
+ {
+ _ranges[j + 1] = _ranges[j];
+ j--;
+ }
+ _ranges[j + 1] = range;
+ }
+ }
}
}
diff --git a/UnityPackage/Runtime/Core/EdgeList.cs b/UnityPackage/Runtime/Core/EdgeList.cs
index 2731d4e..6025e6b 100644
--- a/UnityPackage/Runtime/Core/EdgeList.cs
+++ b/UnityPackage/Runtime/Core/EdgeList.cs
@@ -8,11 +8,16 @@ namespace reromanlee.Wireframes
///
internal sealed class EdgeList
{
- private const int InitialCapacity = 128;
+ private int[] _indices;
+ private IEdgeOwner[] _owners;
+ private int[] _ownerEdges;
- private int[] _indices = new int[InitialCapacity * 2];
- private IEdgeOwner[] _owners = new IEdgeOwner[InitialCapacity];
- private int[] _ownerEdges = new int[InitialCapacity];
+ internal EdgeList(int capacity)
+ {
+ _indices = new int[capacity * 2];
+ _owners = new IEdgeOwner[capacity];
+ _ownerEdges = new int[capacity];
+ }
internal int Count { get; private set; }
@@ -33,14 +38,22 @@ internal void EnsureCapacity(int capacity)
{
return;
}
- int newCapacity = _owners.Length;
+ int newCapacity = Math.Max(_owners.Length, 1);
while (newCapacity < capacity)
{
newCapacity *= 2;
}
- Array.Resize(ref _indices, newCapacity * 2);
- Array.Resize(ref _owners, newCapacity);
- Array.Resize(ref _ownerEdges, newCapacity);
+ Resize(newCapacity);
+ }
+
+ /// Lowers the capacity to , but never below the edges it holds.
+ internal void Shrink(int capacity)
+ {
+ capacity = Math.Max(capacity, Count);
+ if (capacity < _owners.Length)
+ {
+ Resize(capacity);
+ }
}
/// Appends an edge of and returns its slot. Its indices are set later.
@@ -77,5 +90,12 @@ internal bool RemoveAt(int slot)
_owners[last] = null;
return moved;
}
+
+ private void Resize(int capacity)
+ {
+ Array.Resize(ref _indices, capacity * 2);
+ Array.Resize(ref _owners, capacity);
+ Array.Resize(ref _ownerEdges, capacity);
+ }
}
}
diff --git a/UnityPackage/Runtime/Core/EdgeSource.cs b/UnityPackage/Runtime/Core/EdgeSource.cs
new file mode 100644
index 0000000..c5cf830
--- /dev/null
+++ b/UnityPackage/Runtime/Core/EdgeSource.cs
@@ -0,0 +1,44 @@
+namespace reromanlee.Wireframes
+{
+ ///
+ /// Where a shape's edge pattern comes from: an array of its own, or a that shares one
+ /// between the live shapes of the same resolution. The shape takes the pattern when it is attached and gives it
+ /// back when it is disposed.
+ ///
+ internal readonly struct EdgeSource
+ {
+ private readonly int[] _pattern;
+ private readonly PatternCache _cache;
+ private readonly int _resolution;
+
+ internal EdgeSource(PatternCache cache, int resolution)
+ {
+ _pattern = null;
+ _cache = cache;
+ _resolution = resolution;
+ }
+
+ private EdgeSource(int[] pattern)
+ {
+ _pattern = pattern;
+ _cache = null;
+ _resolution = 0;
+ }
+
+ /// A pattern owned by one shape: pairs of local vertex indices, one pair per edge.
+ public static implicit operator EdgeSource(int[] pattern)
+ {
+ return new EdgeSource(pattern);
+ }
+
+ internal int[] Acquire()
+ {
+ return _cache != null ? _cache.Acquire(_resolution) : _pattern;
+ }
+
+ internal void Release()
+ {
+ _cache?.Release(_resolution);
+ }
+ }
+}
diff --git a/UnityPackage/Runtime/Core/EdgeSource.cs.meta b/UnityPackage/Runtime/Core/EdgeSource.cs.meta
new file mode 100644
index 0000000..c0dee0e
--- /dev/null
+++ b/UnityPackage/Runtime/Core/EdgeSource.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: f46e60cc29a7836d428ad0d8ba856509
\ No newline at end of file
diff --git a/UnityPackage/Runtime/Core/IShapeHost.cs b/UnityPackage/Runtime/Core/IShapeHost.cs
new file mode 100644
index 0000000..a5c286d
--- /dev/null
+++ b/UnityPackage/Runtime/Core/IShapeHost.cs
@@ -0,0 +1,26 @@
+namespace reromanlee.Wireframes
+{
+ /// What holds a shape once it is attached: a chunk that draws it, or a host that only keeps it.
+ internal interface IShapeHost
+ {
+ /// Registry of the bones that the host's shapes follow.
+ BoneRegistry Bones { get; }
+
+ /// Queues to be written on the next flush.
+ void Enqueue(Shape shape);
+
+ void Remove(Shape shape);
+
+ /// Draws the edges of again after .
+ void Show(Shape shape);
+
+ /// Stops drawing the edges of , keeping everything else it has.
+ void Hide(Shape shape);
+
+ ///
+ /// Holds again after it was removed from this host to change its size, and returns the
+ /// host that holds it now: this one when it has room, another one otherwise.
+ ///
+ IShapeHost Reattach(Shape shape);
+ }
+}
diff --git a/UnityPackage/Runtime/Core/IShapeHost.cs.meta b/UnityPackage/Runtime/Core/IShapeHost.cs.meta
new file mode 100644
index 0000000..0077169
--- /dev/null
+++ b/UnityPackage/Runtime/Core/IShapeHost.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: d18d7f2b3e89a9662224139ca00a29b7
\ No newline at end of file
diff --git a/UnityPackage/Runtime/Core/MainThread.cs b/UnityPackage/Runtime/Core/MainThread.cs
new file mode 100644
index 0000000..cbc681c
--- /dev/null
+++ b/UnityPackage/Runtime/Core/MainThread.cs
@@ -0,0 +1,37 @@
+using System;
+using System.Diagnostics;
+using System.Threading;
+using UnityEngine;
+
+namespace reromanlee.Wireframes
+{
+ ///
+ /// Catches calls from other threads in the Editor and development builds, where they would race the render-time
+ /// flush and corrupt shapes without a word. Release builds leave the check out.
+ ///
+ internal static class MainThread
+ {
+ private const int Unknown = -1;
+
+ private static int _id = Unknown;
+
+ /// The caller isn't on Unity's main thread.
+ [Conditional("UNITY_EDITOR"), Conditional("DEVELOPMENT_BUILD")]
+ internal static void Check()
+ {
+ if (_id != Unknown && Thread.CurrentThread.ManagedThreadId != _id)
+ {
+ throw new InvalidOperationException("Wireframes can only be used from Unity's main thread.");
+ }
+ }
+
+ [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
+#if UNITY_EDITOR
+ [UnityEditor.InitializeOnLoadMethod]
+#endif
+ private static void Capture()
+ {
+ _id = Thread.CurrentThread.ManagedThreadId;
+ }
+ }
+}
diff --git a/UnityPackage/Runtime/Core/MainThread.cs.meta b/UnityPackage/Runtime/Core/MainThread.cs.meta
new file mode 100644
index 0000000..d795024
--- /dev/null
+++ b/UnityPackage/Runtime/Core/MainThread.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: e1cc87f791cbea88a792c9dc5befd072
\ No newline at end of file
diff --git a/UnityPackage/Runtime/Core/MeshChunk.cs b/UnityPackage/Runtime/Core/MeshChunk.cs
deleted file mode 100644
index 7526415..0000000
--- a/UnityPackage/Runtime/Core/MeshChunk.cs
+++ /dev/null
@@ -1,445 +0,0 @@
-using System;
-using UnityEngine;
-using UnityEngine.Rendering;
-
-namespace reromanlee.Wireframes
-{
- ///
- /// One skinned mesh holding many shapes. Shapes keep their own state; on each flush the chunk writes the queued
- /// shapes into CPU copies of the vertex and index buffers and uploads only the ranges that changed.
- ///
- internal sealed class MeshChunk : IDisposable
- {
- private const string ObjectName = "Chunk";
- private const int InitialVertexCapacity = 256;
- private const int InitialShapeCapacity = 64;
- // Index 0xFFFF is left unused because some graphics APIs reserve it for primitive restart.
- private const int MaxUInt16Vertices = ushort.MaxValue;
- // Freed vertex blocks are packed away once they fill half the buffer, but never in small meshes.
- private const int CompactionThreshold = 1024;
-
- private const int PositionStream = 0;
- private const int ColorStream = 1;
- private const int SkinStream = 2;
-
- // The layout Unity itself builds for one bone per vertex; skinned meshes must keep this stream arrangement.
- private static readonly VertexAttributeDescriptor[] VertexLayout =
- {
- new(VertexAttribute.Position, VertexAttributeFormat.Float32, 3, PositionStream),
- new(VertexAttribute.Color, VertexAttributeFormat.UNorm8, 4, ColorStream),
- new(VertexAttribute.BlendIndices, VertexAttributeFormat.UInt32, 1, SkinStream)
- };
-
- // Shapes follow arbitrary transforms, so the bounds are fixed and large instead of recomputed every frame.
- private static readonly Bounds FixedBounds = new(Vector3.zero, Vector3.one * 2000000f);
-
- private const MeshUpdateFlags UploadFlags =
- MeshUpdateFlags.DontValidateIndices |
- MeshUpdateFlags.DontRecalculateBounds |
- MeshUpdateFlags.DontResetBoneBounds;
-
- private readonly SkinnedMeshRenderer _renderer;
- private readonly Mesh _mesh;
-
- private readonly VertexAllocator _allocator = new();
- private readonly EdgeList _edges = new();
- private readonly BoneRegistry _bones;
-
- private readonly DirtyRanges _positionRanges = new();
- private readonly DirtyRanges _colorRanges = new();
- private readonly DirtyRanges _skinRanges = new();
- private readonly DirtyRanges _edgeRanges = new();
-
- // CPU copies of the three vertex streams, sized to the vertex capacity.
- private Vector3[] _positions = new Vector3[InitialVertexCapacity];
- private Color32[] _colors = new Color32[InitialVertexCapacity];
- private uint[] _skin = new uint[InitialVertexCapacity];
- private ushort[] _shortIndices = Array.Empty();
- private Matrix4x4[] _bindposes = Array.Empty();
-
- private Shape[] _shapes = new Shape[InitialShapeCapacity];
- private int _shapeCount;
- private Shape[] _pending = new Shape[InitialShapeCapacity];
- private int _pendingCount;
-
- // What the mesh holds right now, so the flush knows when a GPU buffer has to be recreated.
- private int _meshVertexCapacity;
- private int _meshIndexCapacity;
- private IndexFormat _meshIndexFormat;
- private int _meshEdgeCount = -1;
-
- internal MeshChunk(Transform parent, Material material)
- {
- GameObject chunkObject = new(ObjectName) { hideFlags = HideFlags.NotEditable };
- Transform root = chunkObject.transform;
- root.SetParent(parent, false);
-
- _bones = new BoneRegistry(root, OnBoneDestroyed);
-
- _mesh = new Mesh { name = ObjectName };
- _mesh.MarkDynamic();
- _mesh.subMeshCount = 1;
- _mesh.bounds = FixedBounds;
-
- _renderer = chunkObject.AddComponent();
- _renderer.sharedMaterial = material;
- _renderer.rootBone = root;
- _renderer.quality = SkinQuality.Bone1;
- _renderer.updateWhenOffscreen = false;
- _renderer.skinnedMotionVectors = false;
- _renderer.motionVectorGenerationMode = MotionVectorGenerationMode.ForceNoMotion;
- _renderer.shadowCastingMode = ShadowCastingMode.Off;
- _renderer.receiveShadows = false;
- _renderer.lightProbeUsage = LightProbeUsage.Off;
- _renderer.reflectionProbeUsage = ReflectionProbeUsage.Off;
- _renderer.allowOcclusionWhenDynamic = false;
-
- // Creates the buffers and hands the mesh to the renderer.
- Flush();
- }
-
- internal BoneRegistry Bones
- {
- get => _bones;
- }
-
- internal Mesh Mesh
- {
- get => _mesh;
- }
-
- internal SkinnedMeshRenderer Renderer
- {
- get => _renderer;
- }
-
- internal VertexAllocator Allocator
- {
- get => _allocator;
- }
-
- internal int ShapeCount
- {
- get => _shapeCount;
- }
-
- internal int EdgeCount
- {
- get => _edges.Count;
- }
-
- internal void Add(Shape shape)
- {
- // Capacity is checked before anything changes, so a device limit leaves the chunk untouched.
- EnsureVertexCapacity(_allocator.EndAfterAllocate(shape.VertexCount));
- EnsureEdgeCapacity(_edges.Count + shape.EdgeCount);
-
- shape.VertexStart = _allocator.Allocate(shape.VertexCount);
- for (int edge = 0; edge < shape.EdgeCount; edge++)
- {
- shape.SetEdgeSlot(edge, _edges.Add(shape, edge));
- }
- if (_shapeCount == _shapes.Length)
- {
- Array.Resize(ref _shapes, _shapeCount * 2);
- }
- shape.ShapeIndex = _shapeCount;
- _shapes[_shapeCount++] = shape;
- }
-
- internal void Remove(Shape shape)
- {
- // The last edge moves into each freed slot; later edges of this same shape may be among them, and
- // their owner callback keeps the shape's slots current while the loop runs.
- for (int edge = 0; edge < shape.EdgeCount; edge++)
- {
- int slot = shape.GetEdgeSlot(edge);
- if (_edges.RemoveAt(slot))
- {
- _edgeRanges.Add(slot, 1);
- }
- }
- _allocator.Free(shape.VertexStart, shape.VertexCount);
-
- int last = --_shapeCount;
- Shape moved = _shapes[last];
- _shapes[shape.ShapeIndex] = moved;
- moved.ShapeIndex = shape.ShapeIndex;
- _shapes[last] = null;
- }
-
- internal void Enqueue(Shape shape)
- {
- if (_pendingCount == _pending.Length)
- {
- Array.Resize(ref _pending, _pendingCount * 2);
- }
- _pending[_pendingCount++] = shape;
- }
-
- /// Writes queued shapes into the buffers and uploads what changed.
- internal void Flush()
- {
- _bones.PollSleepers();
- if (_allocator.FreeCount >= CompactionThreshold && _allocator.FreeCount * 2 > _allocator.End)
- {
- Compact();
- }
- WritePendingShapes();
-
- // Bones go first: Unity doesn't range-check bone indices, so skin data must never reference a missing bone.
- UploadBones();
- bool recreated = UploadVertices();
- recreated |= UploadIndices();
- if (recreated)
- {
- // Re-assigning the mesh makes the renderer drop skinning buffers sized for the old one.
- _renderer.sharedMesh = null;
- _renderer.sharedMesh = _mesh;
- _renderer.localBounds = FixedBounds;
- }
- }
-
- public void Dispose()
- {
- for (int i = 0; i < _shapeCount; i++)
- {
- _shapes[i].Detach();
- _shapes[i] = null;
- }
- _shapeCount = 0;
- Array.Clear(_pending, 0, _pendingCount);
- _pendingCount = 0;
- _bones.Dispose();
- // The chunk's GameObject is a child of the proxy and goes away with it; the mesh is an asset and doesn't.
- UnityObjects.Destroy(_mesh);
- }
-
- private void OnBoneDestroyed(Transform bone)
- {
- for (int i = 0; i < _shapeCount; i++)
- {
- _shapes[i].OnBoneDestroyed(bone);
- }
- }
-
- /// Hands out vertex blocks again from vertex 0, packing live shapes together.
- private void Compact()
- {
- _allocator.Reset();
- for (int i = 0; i < _shapeCount; i++)
- {
- Shape shape = _shapes[i];
- shape.VertexStart = _allocator.Allocate(shape.VertexCount);
- shape.MarkDirty(DirtyFlags.All);
- }
- }
-
- private void WritePendingShapes()
- {
- for (int i = 0; i < _pendingCount; i++)
- {
- Shape shape = _pending[i];
- _pending[i] = null;
- if (shape.IsDisposed)
- {
- continue;
- }
-
- DirtyFlags dirty = shape.TakeDirty();
- int start = shape.VertexStart;
- int count = shape.VertexCount;
- if ((dirty & DirtyFlags.Positions) != 0)
- {
- shape.WritePositions(new Span(_positions, start, count));
- _positionRanges.Add(start, count);
- }
- if ((dirty & DirtyFlags.Colors) != 0)
- {
- shape.WriteColors(new Span(_colors, start, count));
- _colorRanges.Add(start, count);
- }
- if ((dirty & DirtyFlags.Bones) != 0)
- {
- shape.WriteBones(new Span(_skin, start, count));
- _skinRanges.Add(start, count);
- }
- if ((dirty & DirtyFlags.Edges) != 0)
- {
- shape.WriteEdges(_edges, _edgeRanges);
- }
- }
- _pendingCount = 0;
- }
-
- private void UploadBones()
- {
- if (!_bones.IsDirty)
- {
- return;
- }
- Transform[] transforms = _bones.Transforms;
- if (_bindposes.Length != transforms.Length)
- {
- // Endpoints are stored relative to their bone, so every bind pose is identity.
- _bindposes = new Matrix4x4[transforms.Length];
- Array.Fill(_bindposes, Matrix4x4.identity);
- _mesh.bindposes = _bindposes;
- }
- _renderer.bones = transforms;
- _bones.ClearDirty();
- }
-
- /// True when the vertex buffer was recreated.
- private bool UploadVertices()
- {
- int capacity = _positions.Length;
- if (_meshVertexCapacity == capacity)
- {
- UploadRanges(_positionRanges, _positions, PositionStream);
- UploadRanges(_colorRanges, _colors, ColorStream);
- UploadRanges(_skinRanges, _skin, SkinStream);
- return false;
- }
-
- // A resized buffer starts with garbage, and garbage bone indices crash skinning, so every stream is
- // rewritten in full.
- _mesh.SetVertexBufferParams(capacity, VertexLayout);
- _mesh.SetVertexBufferData(_positions, 0, 0, capacity, PositionStream, UploadFlags);
- _mesh.SetVertexBufferData(_colors, 0, 0, capacity, ColorStream, UploadFlags);
- _mesh.SetVertexBufferData(_skin, 0, 0, capacity, SkinStream, UploadFlags);
- _positionRanges.Clear();
- _colorRanges.Clear();
- _skinRanges.Clear();
- _meshVertexCapacity = capacity;
- // The sub-mesh records the vertex range it uses, so it has to be set again.
- _meshEdgeCount = -1;
- return true;
- }
-
- private void UploadRanges(DirtyRanges ranges, T[] data, int stream) where T : struct
- {
- int count = ranges.Merge();
- for (int i = 0; i < count; i++)
- {
- RangeInt range = ranges[i];
- _mesh.SetVertexBufferData(data, range.start, range.start, range.length, stream, UploadFlags);
- }
- ranges.Clear();
- }
-
- /// True when the index buffer was recreated.
- private bool UploadIndices()
- {
- int edgeCount = _edges.Count;
- int indexCapacity = _edges.Capacity * 2;
- IndexFormat format = _meshVertexCapacity > MaxUInt16Vertices ? IndexFormat.UInt32 : IndexFormat.UInt16;
- bool recreated = false;
-
- if (_meshIndexCapacity != indexCapacity || _meshIndexFormat != format)
- {
- _mesh.SetIndexBufferParams(indexCapacity, format);
- _meshIndexCapacity = indexCapacity;
- _meshIndexFormat = format;
- _edgeRanges.Clear();
- UploadIndexRange(0, edgeCount * 2);
- _meshEdgeCount = -1;
- recreated = true;
- }
- else
- {
- int count = _edgeRanges.Merge();
- for (int i = 0; i < count; i++)
- {
- // Slots freed after they were marked may now lie past the last edge; those aren't drawn.
- RangeInt range = _edgeRanges[i];
- int end = Math.Min(range.end, edgeCount);
- UploadIndexRange(range.start * 2, (end - range.start) * 2);
- }
- _edgeRanges.Clear();
- }
-
- if (_meshEdgeCount != edgeCount)
- {
- SubMeshDescriptor subMesh = new(0, edgeCount * 2, MeshTopology.Lines)
- {
- firstVertex = 0,
- vertexCount = _meshVertexCapacity,
- bounds = FixedBounds
- };
- _mesh.SetSubMesh(0, subMesh, UploadFlags);
- _meshEdgeCount = edgeCount;
- }
- return recreated;
- }
-
- private void UploadIndexRange(int start, int count)
- {
- if (count <= 0)
- {
- return;
- }
- int[] indices = _edges.Indices;
- if (_meshIndexFormat == IndexFormat.UInt32)
- {
- _mesh.SetIndexBufferData(indices, start, start, count, UploadFlags);
- return;
- }
- if (_shortIndices.Length != _meshIndexCapacity)
- {
- _shortIndices = new ushort[_meshIndexCapacity];
- }
- for (int i = start; i < start + count; i++)
- {
- _shortIndices[i] = (ushort)indices[i];
- }
- _mesh.SetIndexBufferData(_shortIndices, start, start, count, UploadFlags);
- }
-
- private void EnsureVertexCapacity(int required)
- {
- int capacity = _positions.Length;
- if (required <= capacity)
- {
- return;
- }
- while (capacity < required)
- {
- capacity *= 2;
- }
- // Positions are the widest stream.
- CheckBufferSize((long)capacity * 12);
- // New elements default to zero, which is bone slot 0 (world space), a valid index for skinning.
- Array.Resize(ref _positions, capacity);
- Array.Resize(ref _colors, capacity);
- Array.Resize(ref _skin, capacity);
- }
-
- private void EnsureEdgeCapacity(int required)
- {
- int capacity = _edges.Capacity;
- if (required <= capacity)
- {
- return;
- }
- while (capacity < required)
- {
- capacity *= 2;
- }
- // Two 32-bit indices per edge at worst.
- CheckBufferSize((long)capacity * 8);
- _edges.EnsureCapacity(capacity);
- }
-
- private static void CheckBufferSize(long bytes)
- {
- // Without a graphics device (batch mode with -nographics) the limit reads as zero.
- long limit = SystemInfo.maxGraphicsBufferSize;
- if (limit > 0 && bytes > limit)
- {
- throw new InvalidOperationException(
- $"Wireframes needs a {bytes}-byte GPU buffer, but this device allows at most {limit} bytes.");
- }
- }
- }
-}
diff --git a/UnityPackage/Runtime/Core/MeshProxy.cs b/UnityPackage/Runtime/Core/MeshProxy.cs
deleted file mode 100644
index 4b4f5be..0000000
--- a/UnityPackage/Runtime/Core/MeshProxy.cs
+++ /dev/null
@@ -1,102 +0,0 @@
-using System;
-using System.Collections.Generic;
-using UnityEngine;
-
-namespace reromanlee.Wireframes
-{
- ///
- /// Component on the container's GameObject. It applies queued shape edits once per frame and releases every
- /// resource when its GameObject is destroyed, including when the scene unloads.
- ///
- [AddComponentMenu("")]
- [DisallowMultipleComponent]
- [DefaultExecutionOrder(ExecutionOrder)]
- internal sealed class MeshProxy : MonoBehaviour
- {
- // After default-order scripts' LateUpdate, and still before Unity skins meshes later in the frame.
- private const int ExecutionOrder = 32000;
- private const string ShaderName = "reromanlee/Wireframes/VertexColors";
-
- private readonly List _chunks = new();
- private LineContainer _container;
- private Material _material;
- private bool _ownsMaterial;
- private bool _isShutDown;
-
- internal IReadOnlyList Chunks
- {
- get => _chunks;
- }
-
- internal void Initialize(LineContainer container, Material material)
- {
- _container = container;
- if (material != null)
- {
- _material = material;
- }
- else
- {
- // The shader sits in a Resources folder, which keeps it in player builds.
- Shader shader = Shader.Find(ShaderName);
- if (shader == null)
- {
- throw new InvalidOperationException($"Shader '{ShaderName}' was not found.");
- }
- _material = new Material(shader) { name = ShaderName };
- _ownsMaterial = true;
- }
- _chunks.Add(new MeshChunk(transform, _material));
- }
-
- /// Adds to a chunk and returns that chunk.
- internal MeshChunk Attach(Shape shape)
- {
- // One chunk covers every size a device can hold; choosing a chunk with room would go here.
- MeshChunk chunk = _chunks[0];
- chunk.Add(shape);
- return chunk;
- }
-
- internal void Flush()
- {
- for (int i = 0; i < _chunks.Count; i++)
- {
- _chunks[i].Flush();
- }
- }
-
- internal void Shutdown()
- {
- if (_isShutDown)
- {
- return;
- }
- _isShutDown = true;
- for (int i = 0; i < _chunks.Count; i++)
- {
- _chunks[i].Dispose();
- }
- _chunks.Clear();
- if (_ownsMaterial)
- {
- UnityObjects.Destroy(_material);
- }
- _material = null;
- if (_container != null)
- {
- _container.OnProxyShutdown();
- }
- }
-
- private void LateUpdate()
- {
- Flush();
- }
-
- private void OnDestroy()
- {
- Shutdown();
- }
- }
-}
diff --git a/UnityPackage/Runtime/Core/PatternCache.cs b/UnityPackage/Runtime/Core/PatternCache.cs
index 9275960..44a6004 100644
--- a/UnityPackage/Runtime/Core/PatternCache.cs
+++ b/UnityPackage/Runtime/Core/PatternCache.cs
@@ -4,12 +4,13 @@
namespace reromanlee.Wireframes
{
///
- /// Edge patterns that depend only on a shape's resolution. Shapes only read their pattern, so each one is built
- /// on first use and then shared by every shape with the same resolution.
+ /// Edge patterns of one shape type by resolution, built on first use and shared by every live shape with that
+ /// resolution. A pattern is dropped when the last shape using it is disposed, so the cache only ever holds what
+ /// live shapes need.
///
internal sealed class PatternCache
{
- private readonly Dictionary _patterns = new();
+ private readonly Dictionary _entries = new();
private readonly Func _build;
internal PatternCache(Func build)
@@ -17,14 +18,48 @@ internal PatternCache(Func build)
_build = build;
}
- internal int[] Get(int resolution)
+ /// The pattern for , taken from the cache once the shape is attached.
+ internal EdgeSource Get(int resolution)
{
- if (!_patterns.TryGetValue(resolution, out int[] pattern))
+ return new EdgeSource(this, resolution);
+ }
+
+ internal bool Contains(int resolution)
+ {
+ return _entries.ContainsKey(resolution);
+ }
+
+ internal int[] Acquire(int resolution)
+ {
+ if (!_entries.TryGetValue(resolution, out Entry entry))
+ {
+ entry.Pattern = _build(resolution);
+ }
+ entry.UserCount++;
+ _entries[resolution] = entry;
+ return entry.Pattern;
+ }
+
+ internal void Release(int resolution)
+ {
+ if (!_entries.TryGetValue(resolution, out Entry entry))
{
- pattern = _build(resolution);
- _patterns.Add(resolution, pattern);
+ return;
}
- return pattern;
+ if (--entry.UserCount == 0)
+ {
+ _entries.Remove(resolution);
+ }
+ else
+ {
+ _entries[resolution] = entry;
+ }
+ }
+
+ private struct Entry
+ {
+ public int[] Pattern;
+ public int UserCount;
}
}
}
diff --git a/UnityPackage/Runtime/Core/PointShape.cs b/UnityPackage/Runtime/Core/PointShape.cs
index 27ca802..faeefaf 100644
--- a/UnityPackage/Runtime/Core/PointShape.cs
+++ b/UnityPackage/Runtime/Core/PointShape.cs
@@ -9,61 +9,66 @@ namespace reromanlee.Wireframes
///
internal abstract class PointShape : Shape
{
- protected PointShape(MeshProxy proxy, int pointCount, int[] edgePattern) : base(proxy, pointCount, edgePattern)
+ protected PointShape(int pointCount, EdgeSource edgeSource) : base(pointCount, edgeSource)
{
}
public Vector3 GetLocalPosition(int index)
{
- ThrowIfDisposed();
- return Point(index).LocalPosition;
+ EnsureUsable();
+ return LivePoint(index).LocalPosition;
}
public void SetLocalPosition(int index, Vector3 position)
{
- ThrowIfDisposed();
- Point(index).LocalPosition = position;
+ EnsureUsable();
+ LivePoint(index).LocalPosition = position;
MarkDirty(DirtyFlags.Positions);
}
public Vector3 GetWorldPosition(int index)
{
- ThrowIfDisposed();
- ref ShapePoint point = ref Point(index);
+ EnsureUsable();
+ ref ShapePoint point = ref LivePoint(index);
return ToWorld(point.Bone, point.LocalPosition);
}
public void SetWorldPosition(int index, Vector3 position)
{
- ThrowIfDisposed();
- ref ShapePoint point = ref Point(index);
+ EnsureUsable();
+ ref ShapePoint point = ref LivePoint(index);
point.LocalPosition = ToLocal(point.Bone, position);
MarkDirty(DirtyFlags.Positions);
}
public Color GetColor(int index)
{
- ThrowIfDisposed();
+ EnsureUsable();
return Point(index).Color;
}
public void SetColor(int index, Color color)
{
- ThrowIfDisposed();
+ EnsureUsable();
Point(index).Color = color;
MarkDirty(DirtyFlags.Colors);
}
public Transform GetBone(int index)
{
- ThrowIfDisposed();
- return Point(index).Bone;
+ EnsureUsable();
+ return LivePoint(index).Bone;
}
public void SetBone(int index, Transform bone)
{
- ThrowIfDisposed();
- ref ShapePoint point = ref Point(index);
+ EnsureUsable();
+ bone = CheckBone(bone, nameof(bone));
+ ref ShapePoint point = ref LivePoint(index);
+ if (ReferenceEquals(bone, point.Bone))
+ {
+ return;
+ }
Vector3 worldPosition = ToWorld(point.Bone, point.LocalPosition);
ReplaceBone(ref point.Bone, ref point.BoneSlot, bone);
point.LocalPosition = ToLocal(point.Bone, worldPosition);
@@ -72,7 +77,7 @@ public void SetBone(int index, Transform bone)
public override void SetColor(Color color)
{
- ThrowIfDisposed();
+ EnsureUsable();
for (int i = 0; i < VertexCount; i++)
{
Point(i).Color = color;
@@ -96,23 +101,20 @@ internal override void WriteColors(Span colors)
}
}
- internal override void WriteBones(Span bones)
+ internal override void WriteBoneIndices(Span boneIndices)
{
- for (int i = 0; i < bones.Length; i++)
+ for (int i = 0; i < boneIndices.Length; i++)
{
- bones[i] = (uint)Point(i).BoneSlot;
+ boneIndices[i] = Point(i).BoneSlot;
}
}
- internal override void OnBoneDestroyed(Transform bone)
+ protected override void AcquireBones(BoneRegistry bones)
{
for (int i = 0; i < VertexCount; i++)
{
- // Reference comparison: a destroyed bone also compares equal to every unattached (null) point.
- if (ReferenceEquals(Point(i).Bone, bone))
- {
- SetBone(i, null);
- }
+ ref ShapePoint point = ref Point(i);
+ point.BoneSlot = bones.Acquire(point.Bone);
}
}
@@ -124,14 +126,34 @@ protected override void ReleaseBones(BoneRegistry bones)
}
}
- /// Attaches point to , keeping its local position.
- protected void AttachPoint(int index, Transform bone)
+ ///
+ /// Sets the bone of point while the shape is being built, keeping its local position.
+ ///
+ protected void InitializeBone(int index, Transform bone, string parameterName)
{
- ref ShapePoint point = ref Point(index);
- ReplaceBone(ref point.Bone, ref point.BoneSlot, bone);
+ Point(index).Bone = CheckBone(bone, parameterName);
}
/// Storage of point .
protected abstract ref ShapePoint Point(int index);
+
+ ///
+ /// Storage of point , switched to world space first if its bone was destroyed. Only the
+ /// point asked for is checked, so members of long polylines don't scan every point.
+ ///
+ private ref ShapePoint LivePoint(int index)
+ {
+ ref ShapePoint point = ref Point(index);
+ if (IsDestroyed(point.Bone))
+ {
+ // The slot still holds the bone's last pose, so the point keeps its world position.
+ point.LocalPosition = Bones.MatrixOf(point.BoneSlot).MultiplyPoint3x4(point.LocalPosition);
+ Bones.Release(point.BoneSlot);
+ point.Bone = null;
+ point.BoneSlot = BoneRegistry.WorldSlot;
+ MarkDirty(DirtyFlags.Positions | DirtyFlags.Bones);
+ }
+ return ref point;
+ }
}
}
diff --git a/UnityPackage/Runtime/Core/RectangleShape.cs b/UnityPackage/Runtime/Core/RectangleShape.cs
index 700f430..a100d1b 100644
--- a/UnityPackage/Runtime/Core/RectangleShape.cs
+++ b/UnityPackage/Runtime/Core/RectangleShape.cs
@@ -11,13 +11,12 @@ internal abstract class RectangleShape : RigidShape
private Vector2 _size;
protected RectangleShape(
- MeshProxy proxy,
int vertexCount,
- int[] edgePattern,
+ EdgeSource edgeSource,
Transform bone,
Vector3 localCenter,
Quaternion localRotation,
- Vector2 size) : base(proxy, vertexCount, edgePattern, bone, localCenter, localRotation)
+ Vector2 size) : base(vertexCount, edgeSource, bone, localCenter, localRotation)
{
_size = size;
}
@@ -26,12 +25,12 @@ public Vector2 Size
{
get
{
- ThrowIfDisposed();
+ EnsureUsable();
return _size;
}
set
{
- ThrowIfDisposed();
+ EnsureUsable();
_size = value;
MarkDirty(DirtyFlags.Positions);
}
@@ -41,12 +40,12 @@ public Vector3 LocalCornerA
{
get
{
- ThrowIfDisposed();
+ EnsureUsable();
return LocalPosition - LocalRotation * HalfSize();
}
set
{
- ThrowIfDisposed();
+ EnsureUsable();
Vector3 cornerB = LocalCornerB;
SpanCorners(OntoPlane(value, cornerB), cornerB);
}
@@ -56,12 +55,12 @@ public Vector3 LocalCornerB
{
get
{
- ThrowIfDisposed();
+ EnsureUsable();
return LocalPosition + LocalRotation * HalfSize();
}
set
{
- ThrowIfDisposed();
+ EnsureUsable();
Vector3 cornerA = LocalCornerA;
SpanCorners(cornerA, OntoPlane(value, cornerA));
}
@@ -71,12 +70,12 @@ public Vector3 WorldCornerA
{
get
{
- ThrowIfDisposed();
+ EnsureUsable();
return ToWorld(Bone, LocalCornerA);
}
set
{
- ThrowIfDisposed();
+ EnsureUsable();
LocalCornerA = ToLocal(Bone, value);
}
}
@@ -85,16 +84,22 @@ public Vector3 WorldCornerB
{
get
{
- ThrowIfDisposed();
+ EnsureUsable();
return ToWorld(Bone, LocalCornerB);
}
set
{
- ThrowIfDisposed();
+ EnsureUsable();
LocalCornerB = ToLocal(Bone, value);
}
}
+ /// Size of the XZ rectangle spanned by two corners; their difference in height is ignored.
+ internal static Vector2 FlatSpan(Vector3 cornerA, Vector3 cornerB)
+ {
+ return new Vector2(cornerB.x - cornerA.x, cornerB.z - cornerA.z);
+ }
+
protected override void ScaleSizes(float factor)
{
_size *= factor;
diff --git a/UnityPackage/Runtime/Core/RigidShape.cs b/UnityPackage/Runtime/Core/RigidShape.cs
index 20c5421..7a70914 100644
--- a/UnityPackage/Runtime/Core/RigidShape.cs
+++ b/UnityPackage/Runtime/Core/RigidShape.cs
@@ -16,14 +16,13 @@ internal abstract class RigidShape : Shape, IRigidShape
private int _boneSlot;
protected RigidShape(
- MeshProxy proxy,
int vertexCount,
- int[] edgePattern,
+ EdgeSource edgeSource,
Transform bone,
Vector3 localPosition,
- Quaternion localRotation) : base(proxy, vertexCount, edgePattern)
+ Quaternion localRotation) : base(vertexCount, edgeSource)
{
- ReplaceBone(ref _bone, ref _boneSlot, bone);
+ _bone = CheckBone(bone, nameof(bone));
_localPosition = localPosition;
_localRotation = Quaternion.Normalize(localRotation);
}
@@ -32,12 +31,12 @@ public Vector3 LocalPosition
{
get
{
- ThrowIfDisposed();
+ EnsureUsable();
return _localPosition;
}
set
{
- ThrowIfDisposed();
+ EnsureUsable();
_localPosition = value;
MarkDirty(DirtyFlags.Positions);
}
@@ -47,12 +46,12 @@ public Vector3 WorldPosition
{
get
{
- ThrowIfDisposed();
+ EnsureUsable();
return ToWorld(_bone, _localPosition);
}
set
{
- ThrowIfDisposed();
+ EnsureUsable();
_localPosition = ToLocal(_bone, value);
MarkDirty(DirtyFlags.Positions);
}
@@ -62,12 +61,12 @@ public Quaternion LocalRotation
{
get
{
- ThrowIfDisposed();
+ EnsureUsable();
return _localRotation;
}
set
{
- ThrowIfDisposed();
+ EnsureUsable();
// Normalizing also turns a zero quaternion, such as default, into no rotation.
_localRotation = Quaternion.Normalize(value);
MarkDirty(DirtyFlags.Positions);
@@ -78,12 +77,12 @@ public Quaternion WorldRotation
{
get
{
- ThrowIfDisposed();
+ EnsureUsable();
return ToWorld(_bone, _localRotation);
}
set
{
- ThrowIfDisposed();
+ EnsureUsable();
_localRotation = ToLocal(_bone, Quaternion.Normalize(value));
MarkDirty(DirtyFlags.Positions);
}
@@ -93,12 +92,12 @@ public Color Color
{
get
{
- ThrowIfDisposed();
+ EnsureUsable();
return _color;
}
set
{
- ThrowIfDisposed();
+ EnsureUsable();
_color = value;
MarkDirty(DirtyFlags.Colors);
}
@@ -108,12 +107,17 @@ public Transform Bone
{
get
{
- ThrowIfDisposed();
+ EnsureUsable();
return _bone;
}
set
{
- ThrowIfDisposed();
+ EnsureUsable();
+ value = CheckBone(value, nameof(value));
+ if (ReferenceEquals(value, _bone))
+ {
+ return;
+ }
Vector3 worldPosition = ToWorld(_bone, _localPosition);
Quaternion worldRotation = ToWorld(_bone, _localRotation);
float oldScale = UniformScale(_bone);
@@ -149,18 +153,14 @@ internal sealed override void WriteColors(Span colors)
colors.Fill(_color);
}
- internal sealed override void WriteBones(Span bones)
+ internal sealed override void WriteBoneIndices(Span boneIndices)
{
- bones.Fill((uint)_boneSlot);
+ boneIndices.Fill(_boneSlot);
}
- internal sealed override void OnBoneDestroyed(Transform bone)
+ protected sealed override void AcquireBones(BoneRegistry bones)
{
- // Reference comparison: a destroyed bone also compares equal to null, which means "no bone".
- if (ReferenceEquals(_bone, bone))
- {
- Bone = null;
- }
+ _boneSlot = bones.Acquire(_bone);
}
protected sealed override void ReleaseBones(BoneRegistry bones)
@@ -168,6 +168,27 @@ protected sealed override void ReleaseBones(BoneRegistry bones)
bones.Release(_boneSlot);
}
+ protected sealed override void DetachFromDestroyedBones()
+ {
+ if (!IsDestroyed(_bone))
+ {
+ return;
+ }
+ // The slot still holds the bone's last pose, so the shape keeps its world position, rotation and size.
+ Matrix4x4 pose = Bones.MatrixOf(_boneSlot);
+ _localPosition = pose.MultiplyPoint3x4(_localPosition);
+ _localRotation = Quaternion.Normalize(pose.rotation * _localRotation);
+ float scale = UniformScale(pose.lossyScale);
+ if (scale > 0f && scale != 1f)
+ {
+ ScaleSizes(scale);
+ }
+ Bones.Release(_boneSlot);
+ _bone = null;
+ _boneSlot = BoneRegistry.WorldSlot;
+ MarkDirty(DirtyFlags.Positions | DirtyFlags.Bones);
+ }
+
/// Writes the shape's points along its own axes, before its rotation and position are applied.
protected abstract void WriteShape(Span positions);
@@ -187,11 +208,11 @@ private static Quaternion ToLocal(Transform bone, Quaternion world)
/// A bone's overall scale: the geometric mean of its lossy scale, so a uniform scale comes back as is.
private static float UniformScale(Transform bone)
{
- if (bone == null)
- {
- return 1f;
- }
- Vector3 scale = bone.lossyScale;
+ return bone != null ? UniformScale(bone.lossyScale) : 1f;
+ }
+
+ private static float UniformScale(Vector3 scale)
+ {
return (float)Math.Pow(Math.Abs((double)scale.x * scale.y * scale.z), 1.0 / 3.0);
}
}
diff --git a/UnityPackage/Runtime/Core/Ring.cs b/UnityPackage/Runtime/Core/Ring.cs
index 0983f3c..8e03535 100644
--- a/UnityPackage/Runtime/Core/Ring.cs
+++ b/UnityPackage/Runtime/Core/Ring.cs
@@ -1,5 +1,4 @@
using System;
-using System.Collections.Generic;
using UnityEngine;
namespace reromanlee.Wireframes
@@ -7,36 +6,39 @@ namespace reromanlee.Wireframes
/// Points and edges of the closed rings that round shapes are made of.
internal static class Ring
{
- internal const int DefaultSegments = 32;
+ internal const int DefaultSegmentCount = 32;
+
+ /// Most segments a ring can have, which keeps a shape and its shared edge pattern to a sane size.
+ internal const int MaxSegmentCount = 1024;
/// Edges of shapes that are one closed ring, by vertex count.
internal static readonly PatternCache Patterns = new(BuildEdges);
- // Cosine and sine around a unit circle for each segment count in use, shared by every ring with that count.
- private static readonly Dictionary UnitCircles = new();
-
- /// Returns once it is checked, so it can be used in a base constructor call.
- internal static int CheckSegments(int segments)
+ /// Returns once it is checked, so it can be used in a base constructor call.
+ internal static int CheckSegmentCount(int segmentCount)
{
- if (segments < 3)
+ if (segmentCount < 3 || segmentCount > MaxSegmentCount)
{
- throw new ArgumentOutOfRangeException(nameof(segments), segments, "A ring needs at least 3 segments.");
+ throw new ArgumentOutOfRangeException(
+ nameof(segmentCount), segmentCount, $"A ring needs 3 to {MaxSegmentCount} segments.");
}
- return segments;
+ return segmentCount;
}
///
- /// Returns once it is checked to be a multiple of 4, which shapes need when their
+ /// Returns once it is checked to be a multiple of 4, which shapes need when their
/// lines meet a ring at its quarter points.
///
- internal static int CheckQuarterSegments(int segments)
+ internal static int CheckQuarterSegmentCount(int segmentCount)
{
- if (segments < 4 || segments % 4 != 0)
+ if (segmentCount < 4 || segmentCount % 4 != 0 || segmentCount > MaxSegmentCount)
{
throw new ArgumentOutOfRangeException(
- nameof(segments), segments, "This shape needs a positive multiple of 4 segments.");
+ nameof(segmentCount),
+ segmentCount,
+ $"This shape needs a multiple of 4 segments, from 4 to {MaxSegmentCount}.");
}
- return segments;
+ return segmentCount;
}
///
@@ -46,10 +48,11 @@ internal static int CheckQuarterSegments(int segments)
///
internal static void Write(Span positions, Vector3 center, Vector3 axisU, Vector3 axisV)
{
- Vector2[] circle = UnitCircle(positions.Length);
+ CirclePoints circle = new(positions.Length);
for (int i = 0; i < positions.Length; i++)
{
- positions[i] = center + axisU * circle[i].x + axisV * circle[i].y;
+ Vector2 point = circle.Next();
+ positions[i] = center + axisU * point.x + axisV * point.y;
}
}
@@ -65,25 +68,6 @@ internal static void AddEdges(int[] pattern, ref int cursor, int first, int coun
}
}
- ///
- /// Cosine and sine of angles evenly spread over a full turn, starting at 0. The table
- /// is shared, so callers must not change it.
- ///
- internal static Vector2[] UnitCircle(int count)
- {
- if (!UnitCircles.TryGetValue(count, out Vector2[] circle))
- {
- circle = new Vector2[count];
- for (int i = 0; i < count; i++)
- {
- double angle = 2.0 * Math.PI * i / count;
- circle[i] = new Vector2((float)Math.Cos(angle), (float)Math.Sin(angle));
- }
- UnitCircles.Add(count, circle);
- }
- return circle;
- }
-
private static int[] BuildEdges(int count)
{
int[] pattern = new int[count * 2];
diff --git a/UnityPackage/Runtime/Core/Shape.cs b/UnityPackage/Runtime/Core/Shape.cs
index 641a6e7..8103305 100644
--- a/UnityPackage/Runtime/Core/Shape.cs
+++ b/UnityPackage/Runtime/Core/Shape.cs
@@ -4,57 +4,168 @@
namespace reromanlee.Wireframes
{
///
- /// Base of every shape. It owns the shape's vertex block and edges inside a chunk, queues edits for the next
- /// flush and handles disposal, so derived shapes only describe their geometry.
+ /// Base of every shape. Derived constructors only check and store the shape's state; then takes
+ /// its bones and edge pattern and hands it to a host, so a shape that fails to build leaves nothing behind. It
+ /// queues edits for the next flush and handles disposal, so derived shapes only describe their geometry.
///
internal abstract class Shape : IShape, IEdgeOwner
{
- private readonly int[] _edgePattern;
- private readonly int[] _edgeSlots;
- private MeshChunk _chunk;
+ private EdgeSource _edgeSource;
+ private int[] _edgePattern;
+ private int[] _edgeSlots;
+ private IShapeHost _host;
private DirtyFlags _dirty;
+ private bool _isHidden;
- /// Pairs of local vertex indices, one pair per edge.
- protected Shape(MeshProxy proxy, int vertexCount, int[] edgePattern)
+ protected Shape(int vertexCount, EdgeSource edgeSource)
{
VertexCount = vertexCount;
- _edgePattern = edgePattern;
- _edgeSlots = new int[edgePattern.Length / 2];
- _chunk = proxy.Attach(this);
- MarkDirty(DirtyFlags.All);
+ _edgeSource = edgeSource;
}
public bool IsDisposed
{
- get => _chunk == null;
+ get => _host == null;
+ }
+
+ public bool IsVisible
+ {
+ get
+ {
+ EnsureUsable();
+ return !_isHidden;
+ }
+ set
+ {
+ EnsureUsable();
+ bool isVisible = !_isHidden;
+ if (value == isVisible)
+ {
+ return;
+ }
+ // Shown only once the host managed to add the edges, so a failure leaves the shape hidden.
+ if (value)
+ {
+ _host.Show(this);
+ _isHidden = false;
+ }
+ else
+ {
+ _isHidden = true;
+ _host.Hide(this);
+ }
+ }
+ }
+
+ /// The chunk that draws the shape, or null when it is disposed or nothing can draw it.
+ internal MeshChunk Chunk
+ {
+ get => _host as MeshChunk;
+ }
+
+ /// True while the shape is hidden, when its edges aren't in its chunk's drawn edges.
+ internal bool IsHidden
+ {
+ get => _isHidden;
}
- internal int VertexCount { get; }
+ internal int VertexCount { get; private set; }
internal int VertexStart { get; set; }
- /// Position in the chunk's shape list.
+ /// Position in its host's shape list.
internal int ShapeIndex { get; set; }
+ /// Position in its chunk's queue of shapes to write, or -1 while not queued there.
+ internal int PendingIndex { get; set; } = -1;
+
internal int EdgeCount
{
get => _edgeSlots.Length;
}
+ /// True once a problem with the shape was logged, so each shape logs at most one.
+ internal bool HasReportedProblem { get; set; }
+
+ /// The registry of the bones this shape follows.
+ protected BoneRegistry Bones
+ {
+ get => _host.Bones;
+ }
+
public void Dispose()
{
- if (_chunk == null)
+ MainThread.Check();
+ if (_host == null)
{
return;
}
- MeshChunk chunk = _chunk;
- chunk.Remove(this);
- ReleaseBones(chunk.Bones);
- _chunk = null;
+ IShapeHost host = _host;
+ host.Remove(this);
+ ReleaseBones(host.Bones);
+ _edgeSource.Release();
+ _host = null;
}
public abstract void SetColor(Color color);
+ ///
+ /// Takes the shape's edge pattern and bones and adds it to . If that fails, everything
+ /// taken is given back.
+ ///
+ internal void Attach(MeshProxy proxy)
+ {
+ _edgePattern = _edgeSource.Acquire();
+ _edgeSlots = new int[_edgePattern.Length / 2];
+ BoneRegistry bones = proxy.Bones;
+ AcquireBones(bones);
+ try
+ {
+ _host = proxy.Attach(this);
+ }
+ catch
+ {
+ ReleaseBones(bones);
+ _edgeSource.Release();
+ throw;
+ }
+ 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
+ /// fits, and everything is written again on the next flush. If no room can be found, the shape ends up disposed
+ /// and the exception is rethrown.
+ ///
+ internal void Resize(int vertexCount, EdgeSource edgeSource)
+ {
+ IShapeHost host = _host;
+ host.Remove(this);
+ _edgeSource.Release();
+ VertexCount = vertexCount;
+ _edgeSource = edgeSource;
+ _edgePattern = edgeSource.Acquire();
+ if (_edgeSlots.Length != _edgePattern.Length / 2)
+ {
+ _edgeSlots = new int[_edgePattern.Length / 2];
+ }
+ // Removing it dropped its queue entry, so it has to be queued again wherever it lands.
+ _dirty = DirtyFlags.None;
+ try
+ {
+ _host = host.Reattach(this);
+ }
+ catch
+ {
+ ReleaseBones(host.Bones);
+ _edgeSource.Release();
+ _host = null;
+ throw;
+ }
+ MarkDirty(DirtyFlags.All);
+ }
+
internal int GetEdgeSlot(int edge)
{
return _edgeSlots[edge];
@@ -75,7 +186,7 @@ internal void MarkDirty(DirtyFlags flags)
{
if (_dirty == DirtyFlags.None)
{
- _chunk.Enqueue(this);
+ _host.Enqueue(this);
}
_dirty |= flags;
}
@@ -87,10 +198,11 @@ internal DirtyFlags TakeDirty()
return dirty;
}
- /// Marks the shape disposed when its whole chunk goes away.
+ /// Marks the shape disposed when its whole host goes away, bone registry included.
internal void Detach()
{
- _chunk = null;
+ _edgeSource.Release();
+ _host = null;
}
internal void WriteEdges(EdgeList edges, DirtyRanges ranges)
@@ -107,36 +219,74 @@ internal void WriteEdges(EdgeList edges, DirtyRanges ranges)
internal abstract void WriteColors(Span colors);
- internal abstract void WriteBones(Span bones);
+ internal abstract void WriteBoneIndices(Span boneIndices);
- /// Detaches whatever follows , keeping it in place if the bone can still be read.
- internal abstract void OnBoneDestroyed(Transform bone);
+ /// Registers the bones the shape was built with, when it is attached.
+ protected abstract void AcquireBones(BoneRegistry bones);
protected abstract void ReleaseBones(BoneRegistry bones);
- protected void ThrowIfDisposed()
+ ///
+ /// Throws off the main thread (in the Editor and development builds) or once the shape is disposed, and
+ /// otherwise lets it notice a bone that was destroyed, so every member sees the shape in world space from then on.
+ ///
+ protected void EnsureUsable()
{
- if (_chunk == null)
+ MainThread.Check();
+ if (_host == null)
{
throw new ObjectDisposedException(GetType().Name);
}
+ DetachFromDestroyedBones();
}
- /// Points a bone field at and moves its registry slot along.
+ ///
+ /// Switches whatever follows a destroyed bone to world space, where its bone slot's last matrix put it. Until
+ /// then that frozen matrix keeps drawing it in place.
+ ///
+ protected virtual void DetachFromDestroyedBones()
+ {
+ }
+
+ ///
+ /// Points a bone field at , already checked with , and moves its
+ /// registry slot along.
+ ///
protected void ReplaceBone(ref Transform bone, ref int slot, Transform value)
{
- // Destroyed transforms compare equal to null; they are stored as null so they are never read again.
- if (value == null)
- {
- value = null;
- }
// Acquiring first keeps a bone that is re-assigned from being released and re-registered in between.
- int newSlot = _chunk.Bones.Acquire(value);
- _chunk.Bones.Release(slot);
+ int newSlot = _host.Bones.Acquire(value);
+ _host.Bones.Release(slot);
bone = value;
slot = newSlot;
}
+ ///
+ /// Returns once it is checked to be a Transform in a scene, or null for no bone and for
+ /// a destroyed one, which is how bone fields store them so they are never read again.
+ ///
+ /// isn't in a scene, such as a prefab asset.
+ protected static Transform CheckBone(Transform bone, string parameterName)
+ {
+ if (bone == null)
+ {
+ return null;
+ }
+ if (!bone.gameObject.scene.IsValid())
+ {
+ throw new ArgumentException(
+ $"'{bone.name}' isn't in a scene. Shapes can follow scene objects only, not prefab assets.",
+ parameterName);
+ }
+ return bone;
+ }
+
+ /// True when was a transform that has since been destroyed.
+ protected static bool IsDestroyed(Transform bone)
+ {
+ return !ReferenceEquals(bone, null) && bone == null;
+ }
+
protected static Vector3 ToWorld(Transform bone, Vector3 local)
{
return bone != null ? bone.TransformPoint(local) : local;
diff --git a/UnityPackage/Runtime/Core/ShapeDefaults.cs b/UnityPackage/Runtime/Core/ShapeDefaults.cs
new file mode 100644
index 0000000..4a4c6e0
--- /dev/null
+++ b/UnityPackage/Runtime/Core/ShapeDefaults.cs
@@ -0,0 +1,11 @@
+namespace reromanlee.Wireframes
+{
+ ///
+ /// Sizes of shapes created without arguments, matching Unity's primitives: 1 unit across and 1 unit long.
+ ///
+ internal static class ShapeDefaults
+ {
+ internal const float Size = 1f;
+ internal const float Radius = 0.5f;
+ }
+}
diff --git a/UnityPackage/Runtime/Core/ShapeDefaults.cs.meta b/UnityPackage/Runtime/Core/ShapeDefaults.cs.meta
new file mode 100644
index 0000000..63da80a
--- /dev/null
+++ b/UnityPackage/Runtime/Core/ShapeDefaults.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 7279dde30578b9b0f8420674950dbb39
\ No newline at end of file
diff --git a/UnityPackage/Runtime/Core/UnityObjects.cs b/UnityPackage/Runtime/Core/UnityObjects.cs
index 395a1f1..626e738 100644
--- a/UnityPackage/Runtime/Core/UnityObjects.cs
+++ b/UnityPackage/Runtime/Core/UnityObjects.cs
@@ -4,6 +4,12 @@ namespace reromanlee.Wireframes
{
internal static class UnityObjects
{
+ ///
+ /// True makes destroy right away in Play Mode too, for work that runs where no later frame
+ /// would do it, such as right before scripts reload.
+ ///
+ internal static bool DestroysImmediately { get; set; }
+
/// Destroys with the call Unity allows in the current mode.
public static void Destroy(Object target)
{
@@ -11,7 +17,7 @@ public static void Destroy(Object target)
{
return;
}
- if (Application.isPlaying)
+ if (Application.isPlaying && !DestroysImmediately)
{
Object.Destroy(target);
}
diff --git a/UnityPackage/Runtime/Core/WireframesCounters.cs b/UnityPackage/Runtime/Core/WireframesCounters.cs
new file mode 100644
index 0000000..a368d53
--- /dev/null
+++ b/UnityPackage/Runtime/Core/WireframesCounters.cs
@@ -0,0 +1,71 @@
+using System.Diagnostics;
+#if WIREFRAMES_PROFILING_CORE
+using System.Collections.Generic;
+using Unity.Profiling;
+using UnityEngine;
+#endif
+
+namespace reromanlee.Wireframes
+{
+ ///
+ /// Profiler counters with the totals of every container, to add to a module in the Profiler's Module Editor. They
+ /// exist only in projects with the com.unity.profiling.core package, and count only in the Editor and development
+ /// builds.
+ ///
+ internal static class WireframesCounters
+ {
+#if WIREFRAMES_PROFILING_CORE
+ private static readonly ProfilerCounterValue Shapes =
+ new(ProfilerCategory.Render, "Wireframes Shapes", ProfilerMarkerDataUnit.Count);
+ private static readonly ProfilerCounterValue Vertices =
+ new(ProfilerCategory.Render, "Wireframes Vertices", ProfilerMarkerDataUnit.Count);
+ private static readonly ProfilerCounterValue Edges =
+ new(ProfilerCategory.Render, "Wireframes Edges", ProfilerMarkerDataUnit.Count);
+ private static readonly ProfilerCounterValue Bones =
+ new(ProfilerCategory.Render, "Wireframes Bones", ProfilerMarkerDataUnit.Count);
+ private static readonly ProfilerCounterValue Chunks =
+ new(ProfilerCategory.Render, "Wireframes Chunks", ProfilerMarkerDataUnit.Count);
+ private static readonly ProfilerCounterValue GpuMemory =
+ new(ProfilerCategory.Render, "Wireframes GPU Memory", ProfilerMarkerDataUnit.Bytes);
+
+ private static int _lastFrame = -1;
+#endif
+
+ /// Sums every container into the counters, at most once per frame.
+ [Conditional("WIREFRAMES_PROFILING_CORE")]
+ internal static void Update()
+ {
+#if WIREFRAMES_PROFILING_CORE && ENABLE_PROFILER
+ int frame = Time.frameCount;
+ if (frame == _lastFrame)
+ {
+ return;
+ }
+ _lastFrame = frame;
+ int shapes = 0;
+ int vertices = 0;
+ int edges = 0;
+ int bones = 0;
+ int chunks = 0;
+ long gpuMemory = 0;
+ IReadOnlyList proxies = MeshProxy.LiveProxies;
+ for (int i = 0; i < proxies.Count; i++)
+ {
+ WireframeStatistics statistics = proxies[i].GetStatistics();
+ shapes += statistics.ShapeCount;
+ vertices += statistics.VertexCount;
+ edges += statistics.EdgeCount;
+ bones += statistics.BoneCount;
+ chunks += statistics.ChunkCount;
+ gpuMemory += statistics.GpuMemory;
+ }
+ Shapes.Value = shapes;
+ Vertices.Value = vertices;
+ Edges.Value = edges;
+ Bones.Value = bones;
+ Chunks.Value = chunks;
+ GpuMemory.Value = gpuMemory;
+#endif
+ }
+ }
+}
diff --git a/UnityPackage/Runtime/Core/WireframesCounters.cs.meta b/UnityPackage/Runtime/Core/WireframesCounters.cs.meta
new file mode 100644
index 0000000..aaf9d15
--- /dev/null
+++ b/UnityPackage/Runtime/Core/WireframesCounters.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: fddde7a2e2572f6a72d8574c77343fdb
\ No newline at end of file
diff --git a/UnityPackage/Runtime/Core/WireframesLog.cs b/UnityPackage/Runtime/Core/WireframesLog.cs
new file mode 100644
index 0000000..23808cf
--- /dev/null
+++ b/UnityPackage/Runtime/Core/WireframesLog.cs
@@ -0,0 +1,23 @@
+using System;
+using UnityEngine;
+using Object = UnityEngine.Object;
+
+namespace reromanlee.Wireframes
+{
+ /// The package's console messages, all starting with [Wireframes] so they are easy to find and filter.
+ internal static class WireframesLog
+ {
+ private const string Prefix = "[Wireframes] ";
+
+ internal static void Warning(string message, Object context = null)
+ {
+ Debug.LogWarning(Prefix + message, context);
+ }
+
+ /// Logs as an error, followed by and its stack trace.
+ internal static void Error(string message, Exception exception, Object context = null)
+ {
+ Debug.LogError($"{Prefix}{message}\n{exception}", context);
+ }
+ }
+}
diff --git a/UnityPackage/Runtime/Core/WireframesLog.cs.meta b/UnityPackage/Runtime/Core/WireframesLog.cs.meta
new file mode 100644
index 0000000..42e8c53
--- /dev/null
+++ b/UnityPackage/Runtime/Core/WireframesLog.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 19d59a5e3750660e4cb59d48ef973b7d
\ No newline at end of file
diff --git a/UnityPackage/Runtime/Core/WireframesMarkers.cs b/UnityPackage/Runtime/Core/WireframesMarkers.cs
new file mode 100644
index 0000000..b7da74e
--- /dev/null
+++ b/UnityPackage/Runtime/Core/WireframesMarkers.cs
@@ -0,0 +1,38 @@
+using Unity.Profiling;
+
+namespace reromanlee.Wireframes
+{
+ ///
+ /// The package's Profiler markers, all named Wireframes.* in the Render category, so a capture shows where its time
+ /// goes. Outside the Editor and development builds they cost next to nothing.
+ ///
+ internal static class WireframesMarkers
+ {
+ /// Everything a container does right before a render.
+ internal static readonly ProfilerMarker Flush = new(ProfilerCategory.Render, "Wireframes.Flush");
+
+ /// Reading the local-to-world matrix of every bone in use.
+ internal static readonly ProfilerMarker ReadBones = new(ProfilerCategory.Render, "Wireframes.ReadBones");
+
+ /// Writing the bone matrices into the bone texture and uploading it.
+ internal static readonly ProfilerMarker UploadBones = new(ProfilerCategory.Render, "Wireframes.UploadBones");
+
+ /// Writing the queued shapes into the CPU copies of the buffers.
+ internal static readonly ProfilerMarker WriteShapes = new(ProfilerCategory.Render, "Wireframes.WriteShapes");
+
+ /// Uploading the changed ranges of the vertex and index buffers.
+ internal static readonly ProfilerMarker UploadMesh = new(ProfilerCategory.Render, "Wireframes.UploadMesh");
+
+ /// Packing a chunk's live shapes together, which rewrites all of them.
+ internal static readonly ProfilerMarker Compact = new(ProfilerCategory.Render, "Wireframes.Compact");
+
+ /// Growing or shrinking buffers, which copies them and recreates the GPU ones.
+ internal static readonly ProfilerMarker ResizeBuffers = new(ProfilerCategory.Render, "Wireframes.ResizeBuffers");
+
+ /// Creating a chunk's mesh and GameObject.
+ internal static readonly ProfilerMarker CreateChunk = new(ProfilerCategory.Render, "Wireframes.CreateChunk");
+
+ /// Releasing chunks that stayed empty.
+ internal static readonly ProfilerMarker ReleaseChunks = new(ProfilerCategory.Render, "Wireframes.ReleaseChunks");
+ }
+}
diff --git a/UnityPackage/Runtime/Core/WireframesMarkers.cs.meta b/UnityPackage/Runtime/Core/WireframesMarkers.cs.meta
new file mode 100644
index 0000000..30a2d92
--- /dev/null
+++ b/UnityPackage/Runtime/Core/WireframesMarkers.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 7587e672592fb930528687eecd67652d
\ No newline at end of file
diff --git a/UnityPackage/Runtime/LineContainer/ILineContainer.cs b/UnityPackage/Runtime/LineContainer/ILineContainer.cs
deleted file mode 100644
index b9791f5..0000000
--- a/UnityPackage/Runtime/LineContainer/ILineContainer.cs
+++ /dev/null
@@ -1,15 +0,0 @@
-using System;
-
-namespace reromanlee.Wireframes
-{
- ///
- /// Creates shapes and draws all of them with one GPU-skinned mesh. Disposing the container, or unloading the
- /// scene it was created in, disposes every shape it created.
- ///
- /// Each shape type declares its factory methods in a partial file next to the shape.
- public partial interface ILineContainer : IDisposable
- {
- /// True once the container was disposed or its scene was unloaded.
- bool IsDisposed { get; }
- }
-}
diff --git a/UnityPackage/Runtime/LineContainer/ILineContainer.cs.meta b/UnityPackage/Runtime/LineContainer/ILineContainer.cs.meta
deleted file mode 100644
index 540f313..0000000
--- a/UnityPackage/Runtime/LineContainer/ILineContainer.cs.meta
+++ /dev/null
@@ -1,2 +0,0 @@
-fileFormatVersion: 2
-guid: fbdffc2373483be41af8a9f0a76dc889
\ No newline at end of file
diff --git a/UnityPackage/Runtime/LineContainer/LineContainer.cs b/UnityPackage/Runtime/LineContainer/LineContainer.cs
deleted file mode 100644
index 740bf47..0000000
--- a/UnityPackage/Runtime/LineContainer/LineContainer.cs
+++ /dev/null
@@ -1,76 +0,0 @@
-using System;
-using UnityEngine;
-
-namespace reromanlee.Wireframes
-{
- ///
- ///
- /// Main thread only. The container creates a GameObject in the active scene; edits made to its shapes are
- /// uploaded once per frame at the end of LateUpdate.
- ///
- public sealed partial class LineContainer : ILineContainer
- {
- private const string ProxyName = "Wireframes";
-
- // Shapes created without arguments are sized like Unity's primitives: 1 unit across and 1 unit long.
- private const float UnitSize = 1f;
- private const float UnitRadius = 0.5f;
-
- private readonly MeshProxy _proxy;
- private bool _isDisposed;
-
- /// Creates a container that draws with the package's vertex color material.
- public LineContainer() : this(null)
- {
- }
-
- /// Creates a container that draws with , which stays owned by the caller.
- public LineContainer(Material material)
- {
- // The proxy lives in the active scene, so unloading that scene disposes the container.
- GameObject proxyObject = new(ProxyName) { hideFlags = HideFlags.NotEditable };
- try
- {
- _proxy = proxyObject.AddComponent();
- _proxy.Initialize(this, material);
- }
- catch
- {
- UnityObjects.Destroy(proxyObject);
- throw;
- }
- }
-
- public bool IsDisposed
- {
- get => _isDisposed;
- }
-
- internal MeshProxy Proxy
- {
- get
- {
- if (_isDisposed)
- {
- throw new ObjectDisposedException(nameof(LineContainer));
- }
- return _proxy;
- }
- }
-
- public void Dispose()
- {
- if (_isDisposed)
- {
- return;
- }
- _proxy.Shutdown();
- UnityObjects.Destroy(_proxy.gameObject);
- }
-
- internal void OnProxyShutdown()
- {
- _isDisposed = true;
- }
- }
-}
diff --git a/UnityPackage/Runtime/Rendering.meta b/UnityPackage/Runtime/Rendering.meta
new file mode 100644
index 0000000..d3a1aea
--- /dev/null
+++ b/UnityPackage/Runtime/Rendering.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 44e3578807ea7cdbec282fab1f50e70f
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/UnityPackage/Runtime/Rendering/BoneRegistry.cs b/UnityPackage/Runtime/Rendering/BoneRegistry.cs
new file mode 100644
index 0000000..3649df3
--- /dev/null
+++ b/UnityPackage/Runtime/Rendering/BoneRegistry.cs
@@ -0,0 +1,183 @@
+using System;
+using System.Collections.Generic;
+using UnityEngine;
+
+namespace reromanlee.Wireframes
+{
+ ///
+ /// Maps the transforms that shapes follow to bone slots, counting the uses of each, and keeps every slot's latest
+ /// local-to-world matrix for the shader. Slot 0 is the identity and stands for world space. When a transform is
+ /// destroyed, its slot stops being read and keeps its last matrix, which holds the shapes on it in place until each
+ /// of them switches to world space and releases the slot. No component is ever added to a bone.
+ ///
+ internal sealed class BoneRegistry
+ {
+ internal const int WorldSlot = 0;
+ private const int InitialCapacity = 16;
+
+ private readonly Dictionary _slots = new();
+ private readonly Stack _freeSlots = new();
+
+ private Transform[] _transforms = new Transform[InitialCapacity];
+ private Matrix4x4[] _matrices = new Matrix4x4[InitialCapacity];
+ private int[] _referenceCounts = new int[InitialCapacity];
+
+ // Slots whose transform is still alive, read every frame, and each slot's position among them (-1 when not).
+ private int[] _liveSlots = new int[InitialCapacity];
+ private int[] _livePositions = new int[InitialCapacity];
+ private int _liveCount;
+ private int _end = WorldSlot + 1;
+
+ internal BoneRegistry()
+ {
+ _matrices[WorldSlot] = Matrix4x4.identity;
+ Array.Fill(_livePositions, -1);
+ HasChanges = true;
+ }
+
+ /// One past the highest slot handed out so far.
+ internal int End
+ {
+ get => _end;
+ }
+
+ /// Number of distinct transforms in use, not counting world space.
+ internal int Count
+ {
+ get => _slots.Count;
+ }
+
+ /// Bytes of CPU memory the registry takes, about.
+ internal long CpuMemory
+ {
+ get
+ {
+ // Per slot: its transform, matrix, reference count and two live-list positions; per bone, a map entry.
+ long slotSize = IntPtr.Size + 16 * sizeof(float) + 3 * sizeof(int);
+ long entrySize = IntPtr.Size + 3 * sizeof(int);
+ return _transforms.Length * slotSize + (long)_slots.Count * entrySize;
+ }
+ }
+
+ /// Each slot's latest matrix, sized to the capacity.
+ internal Matrix4x4[] Matrices
+ {
+ get => _matrices;
+ }
+
+ /// True when a matrix changed since .
+ internal bool HasChanges { get; private set; }
+
+ internal Matrix4x4 MatrixOf(int slot)
+ {
+ return _matrices[slot];
+ }
+
+ internal void ClearChanges()
+ {
+ HasChanges = false;
+ }
+
+ ///
+ /// Returns the slot for , registering it on first use. Null means world space; callers
+ /// pass a destroyed transform as null.
+ ///
+ internal int Acquire(Transform bone)
+ {
+ if (bone is null)
+ {
+ return WorldSlot;
+ }
+ if (_slots.TryGetValue(bone, out int slot))
+ {
+ _referenceCounts[slot]++;
+ return slot;
+ }
+ slot = _freeSlots.Count > 0 ? _freeSlots.Pop() : _end++;
+ EnsureCapacity(slot + 1);
+ _slots.Add(bone, slot);
+ _transforms[slot] = bone;
+ _referenceCounts[slot] = 1;
+ // Read right away, so a bone destroyed before the next frame still leaves a pose to hold its shapes.
+ _matrices[slot] = bone.localToWorldMatrix;
+ _livePositions[slot] = _liveCount;
+ _liveSlots[_liveCount++] = slot;
+ HasChanges = true;
+ return slot;
+ }
+
+ internal void Release(int slot)
+ {
+ if (slot == WorldSlot || --_referenceCounts[slot] > 0)
+ {
+ return;
+ }
+ // Dictionary lookups compare instance IDs, so this also works for a transform that was destroyed.
+ _slots.Remove(_transforms[slot]);
+ _transforms[slot] = null;
+ StopReading(slot);
+ _freeSlots.Push(slot);
+ }
+
+ ///
+ /// Reads the matrix of every live bone and notes whether any changed. A destroyed bone's slot stops being read
+ /// and keeps its last matrix.
+ ///
+ internal void ReadMatrices()
+ {
+ using (WireframesMarkers.ReadBones.Auto())
+ {
+ // Backwards, so a slot that stops being read swaps in one that was already read.
+ for (int i = _liveCount - 1; i >= 0; i--)
+ {
+ int slot = _liveSlots[i];
+ Transform bone = _transforms[slot];
+ if (bone == null)
+ {
+ StopReading(slot);
+ continue;
+ }
+ Matrix4x4 matrix = bone.localToWorldMatrix;
+ if (!matrix.Equals(_matrices[slot]))
+ {
+ _matrices[slot] = matrix;
+ HasChanges = true;
+ }
+ }
+ }
+ }
+
+ private void StopReading(int slot)
+ {
+ int position = _livePositions[slot];
+ if (position < 0)
+ {
+ return;
+ }
+ int last = _liveSlots[--_liveCount];
+ _liveSlots[position] = last;
+ _livePositions[last] = position;
+ _livePositions[slot] = -1;
+ }
+
+ private void EnsureCapacity(int capacity)
+ {
+ int oldCapacity = _transforms.Length;
+ if (capacity <= oldCapacity)
+ {
+ return;
+ }
+ int newCapacity = oldCapacity;
+ while (newCapacity < capacity)
+ {
+ newCapacity *= 2;
+ }
+ Array.Resize(ref _transforms, newCapacity);
+ Array.Resize(ref _matrices, newCapacity);
+ Array.Resize(ref _referenceCounts, newCapacity);
+ Array.Resize(ref _liveSlots, newCapacity);
+ Array.Resize(ref _livePositions, newCapacity);
+ Array.Fill(_livePositions, -1, oldCapacity, newCapacity - oldCapacity);
+ }
+ }
+}
diff --git a/UnityPackage/Runtime/Core/BoneRegistry.cs.meta b/UnityPackage/Runtime/Rendering/BoneRegistry.cs.meta
similarity index 100%
rename from UnityPackage/Runtime/Core/BoneRegistry.cs.meta
rename to UnityPackage/Runtime/Rendering/BoneRegistry.cs.meta
diff --git a/UnityPackage/Runtime/Rendering/BoneTexture.cs b/UnityPackage/Runtime/Rendering/BoneTexture.cs
new file mode 100644
index 0000000..da7da23
--- /dev/null
+++ b/UnityPackage/Runtime/Rendering/BoneTexture.cs
@@ -0,0 +1,96 @@
+using System;
+using Unity.Collections;
+using UnityEngine;
+
+namespace reromanlee.Wireframes
+{
+ ///
+ /// The float texture that shaders read bone matrices from: three texels per bone, the rows of its local-to-world
+ /// matrix, and bones per texture row, as Wireframes.hlsl expects. A float texture works on
+ /// every platform, WebGL included, where structured buffers are not available to vertex shaders.
+ ///
+ internal sealed class BoneTexture : IDisposable
+ {
+ internal const int BonesPerRow = 256;
+ private const int TexelsPerBone = 3;
+ // Four 32-bit floats.
+ private const int TexelSize = 16;
+
+ internal static readonly int PropertyId = Shader.PropertyToID("_WireframesBones");
+
+ private Texture2D _texture;
+
+ internal Texture2D Texture
+ {
+ get => _texture;
+ }
+
+ ///
+ /// Bytes the texture takes, on the GPU and again on the CPU, where Unity keeps a copy that each upload writes into.
+ ///
+ internal long Memory
+ {
+ get => _texture != null ? (long)_texture.width * _texture.height * TexelSize : 0;
+ }
+
+ ///
+ /// Writes every slot's matrix and uploads the texture when a matrix changed or the texture had to grow. Returns
+ /// true when a new texture was created, which the renderers then have to be given.
+ ///
+ internal bool Upload(BoneRegistry bones)
+ {
+ bool created = EnsureCapacity(bones.End);
+ if (!created && !bones.HasChanges)
+ {
+ return false;
+ }
+ using (WireframesMarkers.UploadBones.Auto())
+ {
+ NativeArray texels = _texture.GetPixelData(0);
+ Matrix4x4[] matrices = bones.Matrices;
+ int end = bones.End;
+ for (int slot = 0; slot < end; slot++)
+ {
+ Matrix4x4 matrix = matrices[slot];
+ int texel = slot / BonesPerRow * BonesPerRow * TexelsPerBone + slot % BonesPerRow * TexelsPerBone;
+ texels[texel] = new Vector4(matrix.m00, matrix.m01, matrix.m02, matrix.m03);
+ texels[texel + 1] = new Vector4(matrix.m10, matrix.m11, matrix.m12, matrix.m13);
+ texels[texel + 2] = new Vector4(matrix.m20, matrix.m21, matrix.m22, matrix.m23);
+ }
+ _texture.Apply(false, false);
+ }
+ bones.ClearChanges();
+ return created;
+ }
+
+ public void Dispose()
+ {
+ UnityObjects.Destroy(_texture);
+ _texture = null;
+ }
+
+ /// True when a new texture was created.
+ private bool EnsureCapacity(int slots)
+ {
+ int rows = Math.Max(1, (slots + BonesPerRow - 1) / BonesPerRow);
+ if (_texture != null && _texture.height >= rows)
+ {
+ return false;
+ }
+ int height = _texture != null ? _texture.height : 1;
+ while (height < rows)
+ {
+ height *= 2;
+ }
+ UnityObjects.Destroy(_texture);
+ _texture = new Texture2D(BonesPerRow * TexelsPerBone, height, TextureFormat.RGBAFloat, false, true)
+ {
+ name = "Wireframes Bones",
+ filterMode = FilterMode.Point,
+ wrapMode = TextureWrapMode.Clamp,
+ hideFlags = HideFlags.HideAndDontSave
+ };
+ return true;
+ }
+ }
+}
diff --git a/UnityPackage/Runtime/Rendering/BoneTexture.cs.meta b/UnityPackage/Runtime/Rendering/BoneTexture.cs.meta
new file mode 100644
index 0000000..7fc8805
--- /dev/null
+++ b/UnityPackage/Runtime/Rendering/BoneTexture.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 156c1de0e84982811bd96a1da06dad41
\ No newline at end of file
diff --git a/UnityPackage/Runtime/Rendering/ChunkAllocator.cs b/UnityPackage/Runtime/Rendering/ChunkAllocator.cs
new file mode 100644
index 0000000..75a4628
--- /dev/null
+++ b/UnityPackage/Runtime/Rendering/ChunkAllocator.cs
@@ -0,0 +1,237 @@
+using System;
+using System.Collections.Generic;
+using UnityEngine;
+
+namespace reromanlee.Wireframes
+{
+ ///
+ /// A container's chunks. Every new shape goes where room already exists, else into a chunk that can grow, else
+ /// into a new chunk, so a buffer only grows or appears when the others are full; a shape too big for any chunk
+ /// gets a large chunk of its own. Chunks reserved by the capacity settings stay for the container's lifetime, and
+ /// the others are released after staying empty for seconds.
+ ///
+ internal sealed class ChunkAllocator : IDisposable
+ {
+ /// Seconds an unreserved chunk stays empty before it is released, so passing gaps don't churn chunks.
+ internal const float ReleaseDelay = 5f;
+
+ private readonly List _chunks = new();
+ private readonly Transform _parent;
+ private readonly Material[] _materials;
+ private readonly int _layer;
+ private readonly BoneRegistry _bones;
+ private readonly MaterialPropertyBlock _propertyBlock = new();
+ // The first chunks are the reserved ones; releasing others keeps them in front.
+ private int _reservedCount;
+ private int _lastCompactionFrame = -1;
+
+ /// Transform the chunks' GameObjects are created under; destroying it destroys them.
+ /// Materials every chunk draws with; the allocator leaves them to their owner.
+ internal ChunkAllocator(Transform parent, Material[] materials, int layer, BoneRegistry bones)
+ {
+ _parent = parent;
+ _materials = materials;
+ _layer = layer;
+ _bones = bones;
+ }
+
+ internal IReadOnlyList Chunks
+ {
+ get => _chunks;
+ }
+
+ /// Transform the chunks' GameObjects are created under.
+ internal Transform Parent
+ {
+ get => _parent;
+ }
+
+ /// Materials every chunk draws with.
+ internal Material[] Materials
+ {
+ get => _materials;
+ }
+
+ internal int Layer
+ {
+ get => _layer;
+ }
+
+ internal BoneRegistry Bones
+ {
+ get => _bones;
+ }
+
+ ///
+ /// Creates chunks with room for vertices and edges,
+ /// split evenly between as few chunks as that takes. They are never released.
+ ///
+ internal void Reserve(int vertexCount, int edgeCount)
+ {
+ if (vertexCount <= 0 && edgeCount <= 0)
+ {
+ return;
+ }
+ int chunkCount = Math.Max(1, CeilingDivide(vertexCount, MeshChunk.MaxVertexCount));
+ int vertices = Math.Max(MeshChunk.InitialVertexCapacity, CeilingDivide(vertexCount, chunkCount));
+ int edges = Math.Max(MeshChunk.InitialEdgeCapacity, CeilingDivide(edgeCount, chunkCount));
+ for (int i = 0; i < chunkCount; i++)
+ {
+ CreateChunk(vertices, edges);
+ _reservedCount++;
+ }
+ }
+
+ /// Adds to a chunk and returns that chunk.
+ internal MeshChunk Attach(Shape shape)
+ {
+ int vertexCount = shape.VertexCount;
+ MeshChunk chunk = FindChunk(vertexCount);
+ if (chunk == null)
+ {
+ chunk = vertexCount > MeshChunk.MaxVertexCount
+ ? CreateChunk(vertexCount, Math.Max(MeshChunk.InitialEdgeCapacity, shape.EdgeCount))
+ : CreateChunk(GrownCapacity(MeshChunk.InitialVertexCapacity, vertexCount), MeshChunk.InitialEdgeCapacity);
+ }
+ chunk.Add(shape);
+ return chunk;
+ }
+
+ /// Hands the bone texture to every chunk, including the ones created later.
+ internal void SetBoneTexture(Texture texture)
+ {
+ _propertyBlock.SetTexture(BoneTexture.PropertyId, texture);
+ for (int i = 0; i < _chunks.Count; i++)
+ {
+ _chunks[i].Renderer.SetPropertyBlock(_propertyBlock);
+ }
+ }
+
+ /// Compacts at most one chunk per frame, then writes and uploads every chunk's queued shapes.
+ internal void Flush()
+ {
+ CompactOneChunk();
+ for (int i = 0; i < _chunks.Count; i++)
+ {
+ _chunks[i].Flush();
+ }
+ }
+
+ ///
+ /// Releases the unreserved chunks that stayed empty for seconds up to
+ /// . Their GameObjects are destroyed with , so this must
+ /// not run where Unity forbids destroying objects right away, such as in a render callback in Edit Mode.
+ ///
+ /// Seconds on a clock that keeps running while the game is paused.
+ internal void ReleaseIdleChunks(float time)
+ {
+ for (int i = _chunks.Count - 1; i >= _reservedCount; i--)
+ {
+ MeshChunk chunk = _chunks[i];
+ if (chunk.ShapeCount > 0)
+ {
+ continue;
+ }
+ if (float.IsNaN(chunk.EmptySince))
+ {
+ chunk.EmptySince = time;
+ continue;
+ }
+ if (time - chunk.EmptySince >= ReleaseDelay)
+ {
+ using (WireframesMarkers.ReleaseChunks.Auto())
+ {
+ _chunks.RemoveAt(i);
+ chunk.Dispose();
+ UnityObjects.Destroy(chunk.Renderer.gameObject);
+ }
+ }
+ }
+ }
+
+ /// Disposes every chunk and its shapes. The chunks' GameObjects go away with the parent.
+ public void Dispose()
+ {
+ for (int i = 0; i < _chunks.Count; i++)
+ {
+ _chunks[i].Dispose();
+ }
+ _chunks.Clear();
+ _reservedCount = 0;
+ }
+
+ private MeshChunk FindChunk(int vertexCount)
+ {
+ // Oldest chunks first, so newer ones are the ones that empty out and get released.
+ bool isLarge = vertexCount > MeshChunk.MaxVertexCount;
+ for (int i = 0; i < _chunks.Count; i++)
+ {
+ MeshChunk chunk = _chunks[i];
+ // An idle large chunk is reused only when the shape fills at least half of it.
+ if (chunk.IsLarge == isLarge && chunk.HasRoomFor(vertexCount)
+ && (!isLarge || chunk.VertexCapacity <= vertexCount * 2L))
+ {
+ return chunk;
+ }
+ }
+ if (isLarge)
+ {
+ return null;
+ }
+ for (int i = 0; i < _chunks.Count; i++)
+ {
+ MeshChunk chunk = _chunks[i];
+ if (!chunk.IsLarge && chunk.CanGrowToFit(vertexCount))
+ {
+ return chunk;
+ }
+ }
+ return null;
+ }
+
+ private MeshChunk CreateChunk(int vertexCapacity, int edgeCapacity)
+ {
+ using (WireframesMarkers.CreateChunk.Auto())
+ {
+ MeshChunk chunk = new(this, vertexCapacity, edgeCapacity);
+ chunk.Renderer.SetPropertyBlock(_propertyBlock);
+ _chunks.Add(chunk);
+ return chunk;
+ }
+ }
+
+ private void CompactOneChunk()
+ {
+ // Compacting rewrites a whole chunk, so spreading it over frames keeps each frame's cost bounded.
+ int frame = Time.frameCount;
+ if (frame == _lastCompactionFrame)
+ {
+ return;
+ }
+ for (int i = 0; i < _chunks.Count; i++)
+ {
+ if (_chunks[i].NeedsCompaction)
+ {
+ _chunks[i].Compact();
+ _lastCompactionFrame = frame;
+ return;
+ }
+ }
+ }
+
+ /// doubled until it holds , up to a chunk's limit.
+ private static int GrownCapacity(int capacity, int required)
+ {
+ while (capacity < required)
+ {
+ capacity *= 2;
+ }
+ return Math.Min(capacity, MeshChunk.MaxVertexCount);
+ }
+
+ private static int CeilingDivide(int value, int divisor)
+ {
+ return (int)((value + (long)divisor - 1) / divisor);
+ }
+ }
+}
diff --git a/UnityPackage/Runtime/Rendering/ChunkAllocator.cs.meta b/UnityPackage/Runtime/Rendering/ChunkAllocator.cs.meta
new file mode 100644
index 0000000..3d748b2
--- /dev/null
+++ b/UnityPackage/Runtime/Rendering/ChunkAllocator.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: d0e46d53b02cc94624ce6c0ba42686b4
\ No newline at end of file
diff --git a/UnityPackage/Runtime/Rendering/HeadlessHost.cs b/UnityPackage/Runtime/Rendering/HeadlessHost.cs
new file mode 100644
index 0000000..0571598
--- /dev/null
+++ b/UnityPackage/Runtime/Rendering/HeadlessHost.cs
@@ -0,0 +1,103 @@
+using System;
+
+namespace reromanlee.Wireframes
+{
+ ///
+ /// Holds the shapes of a container that can't draw: without a graphics device, as in server builds, or without a
+ /// usable shader. Shapes keep working as usual, but nothing is stored for the GPU and nothing is uploaded.
+ ///
+ internal sealed class HeadlessHost : IShapeHost, IDisposable
+ {
+ private const int InitialCapacity = 64;
+
+ private Shape[] _shapes = new Shape[InitialCapacity];
+ private int _shapeCount;
+ private int _hiddenShapeCount;
+ private int _vertexCount;
+
+ internal HeadlessHost(BoneRegistry bones)
+ {
+ Bones = bones;
+ }
+
+ public BoneRegistry Bones { get; }
+
+ internal int ShapeCount
+ {
+ get => _shapeCount;
+ }
+
+ internal int HiddenShapeCount
+ {
+ get => _hiddenShapeCount;
+ }
+
+ /// Vertices the shapes would use if they were drawn.
+ internal int VertexCount
+ {
+ get => _vertexCount;
+ }
+
+ internal void Add(Shape shape)
+ {
+ if (_shapeCount == _shapes.Length)
+ {
+ Array.Resize(ref _shapes, _shapeCount * 2);
+ }
+ shape.ShapeIndex = _shapeCount;
+ _shapes[_shapeCount++] = shape;
+ _vertexCount += shape.VertexCount;
+ if (shape.IsHidden)
+ {
+ _hiddenShapeCount++;
+ }
+ }
+
+ public void Enqueue(Shape shape)
+ {
+ // Nothing is drawn, so there is nothing to write; the shape stays marked and never queues again.
+ }
+
+ public void Remove(Shape shape)
+ {
+ int last = --_shapeCount;
+ Shape moved = _shapes[last];
+ _shapes[shape.ShapeIndex] = moved;
+ moved.ShapeIndex = shape.ShapeIndex;
+ _shapes[last] = null;
+ _vertexCount -= shape.VertexCount;
+ if (shape.IsHidden)
+ {
+ _hiddenShapeCount--;
+ }
+ }
+
+ public void Show(Shape shape)
+ {
+ _hiddenShapeCount--;
+ }
+
+ public void Hide(Shape shape)
+ {
+ _hiddenShapeCount++;
+ }
+
+ public IShapeHost Reattach(Shape shape)
+ {
+ Add(shape);
+ return this;
+ }
+
+ public void Dispose()
+ {
+ for (int i = 0; i < _shapeCount; i++)
+ {
+ _shapes[i].Detach();
+ _shapes[i] = null;
+ }
+ _shapeCount = 0;
+ _hiddenShapeCount = 0;
+ _vertexCount = 0;
+ }
+ }
+}
diff --git a/UnityPackage/Runtime/Rendering/HeadlessHost.cs.meta b/UnityPackage/Runtime/Rendering/HeadlessHost.cs.meta
new file mode 100644
index 0000000..406f607
--- /dev/null
+++ b/UnityPackage/Runtime/Rendering/HeadlessHost.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: f14780e79cd26cfc670dd59c804c6a34
\ No newline at end of file
diff --git a/UnityPackage/Runtime/Rendering/MeshChunk.cs b/UnityPackage/Runtime/Rendering/MeshChunk.cs
new file mode 100644
index 0000000..ba4bf25
--- /dev/null
+++ b/UnityPackage/Runtime/Rendering/MeshChunk.cs
@@ -0,0 +1,725 @@
+using System;
+using UnityEngine;
+using UnityEngine.Rendering;
+
+namespace reromanlee.Wireframes
+{
+ ///
+ /// One mesh holding many shapes, drawn with its own draw call. Shapes keep their own state; on each flush the chunk
+ /// writes the queued shapes into CPU copies of the vertex and index buffers and uploads only the ranges that
+ /// changed. Each vertex is a position relative to its bone plus that bone's slot, which the shader turns into a
+ /// world position.
+ ///
+ ///
+ /// A chunk grows up to vertices, so its indices stay 16-bit. A chunk created with more
+ /// is a large chunk: it keeps 32-bit indices for a shape bigger than that and never grows.
+ ///
+ internal sealed class MeshChunk : IShapeHost, IDisposable
+ {
+ ///
+ /// Most vertices a chunk with 16-bit indices holds. Index 0xFFFF stays unused because some graphics APIs reserve
+ /// it for primitive restart.
+ ///
+ internal const int MaxVertexCount = ushort.MaxValue;
+
+ internal const int InitialVertexCapacity = 256;
+ internal const int InitialEdgeCapacity = 128;
+
+ private const string ObjectName = "Chunk";
+ private const int InitialShapeCapacity = 64;
+ // Freed vertex blocks are packed away once they fill half the buffer, but never in small meshes.
+ private const int CompactionThreshold = 1024;
+ private const int PositionSize = 12;
+ // Position, color and bone slot.
+ private const int VertexSize = PositionSize + 4 + 4;
+
+ private const int PositionStream = 0;
+ private const int ColorStream = 1;
+ private const int BoneStream = 2;
+
+ private static readonly VertexAttributeDescriptor[] VertexLayout =
+ {
+ new(VertexAttribute.Position, VertexAttributeFormat.Float32, 3, PositionStream),
+ new(VertexAttribute.Color, VertexAttributeFormat.UNorm8, 4, ColorStream),
+ new(VertexAttribute.TexCoord0, VertexAttributeFormat.Float32, 1, BoneStream)
+ };
+
+ // Shapes follow arbitrary transforms, so the bounds are fixed and large instead of recomputed every frame.
+ private static readonly Bounds FixedBounds = new(Vector3.zero, Vector3.one * 2000000f);
+
+ private const MeshUpdateFlags UploadFlags = MeshUpdateFlags.DontValidateIndices | MeshUpdateFlags.DontRecalculateBounds;
+
+ private readonly ChunkAllocator _owner;
+ private readonly MeshRenderer _renderer;
+ private readonly Mesh _mesh;
+ private readonly BoneRegistry _bones;
+
+ private readonly VertexAllocator _allocator = new();
+ private readonly EdgeList _edges;
+
+ private readonly DirtyRanges _positionRanges = new();
+ private readonly DirtyRanges _colorRanges = new();
+ private readonly DirtyRanges _boneRanges = new();
+ private readonly DirtyRanges _edgeRanges = new();
+
+ // Growth stops here; shrinking stops at the capacities the chunk was created with.
+ private readonly int _vertexLimit;
+ private readonly int _minimumVertexCapacity;
+ private readonly int _minimumEdgeCapacity;
+
+ // CPU copies of the three vertex streams, sized to the vertex capacity.
+ private Vector3[] _positions;
+ private Color32[] _colors;
+ private float[] _boneIndices;
+ private ushort[] _shortIndices = Array.Empty();
+
+ private Shape[] _shapes = new Shape[InitialShapeCapacity];
+ private int _shapeCount;
+ private int _hiddenShapeCount;
+ private Shape[] _pending = new Shape[InitialShapeCapacity];
+ private int _pendingCount;
+
+ // What the mesh holds right now, so the flush knows when a GPU buffer has to be recreated.
+ private int _meshVertexCapacity;
+ private int _meshIndexCapacity;
+ private IndexFormat _meshIndexFormat;
+ private int _meshEdgeCount = -1;
+
+ ///
+ /// The allocator the chunk belongs to, which gives it its parent, layer, bones and materials. Every material draws
+ /// all of the mesh; the chunk leaves them to their owner.
+ ///
+ ///
+ /// Vertices the chunk starts with. Past , it is a large chunk.
+ ///
+ /// Edges the chunk starts with.
+ internal MeshChunk(ChunkAllocator owner, int vertexCapacity, int edgeCapacity)
+ {
+ _owner = owner;
+ _vertexLimit = Math.Max(vertexCapacity, MaxVertexCount);
+ // Checked before anything is created, so a device limit leaves nothing behind.
+ CheckBufferSize((long)vertexCapacity * PositionSize);
+ CheckBufferSize((long)edgeCapacity * 2 * IndexSize);
+ _minimumVertexCapacity = vertexCapacity;
+ _minimumEdgeCapacity = edgeCapacity;
+ _positions = new Vector3[vertexCapacity];
+ _colors = new Color32[vertexCapacity];
+ _boneIndices = new float[vertexCapacity];
+ _edges = new EdgeList(edgeCapacity);
+ _bones = owner.Bones;
+
+ // Same flags as the container's GameObject, so an Edit Mode chunk is never saved either.
+ Transform parent = owner.Parent;
+ GameObject chunkObject = new(ObjectName) { hideFlags = parent.gameObject.hideFlags, layer = owner.Layer };
+ chunkObject.transform.SetParent(parent, false);
+
+ // Owned and destroyed by the chunk, so it is kept from saving and from unloading as an unused asset.
+ _mesh = new Mesh { name = ObjectName, hideFlags = HideFlags.DontSave };
+ _mesh.MarkDynamic();
+ _mesh.subMeshCount = 1;
+ _mesh.bounds = FixedBounds;
+ chunkObject.AddComponent().sharedMesh = _mesh;
+
+ _renderer = chunkObject.AddComponent();
+ // A renderer with more materials than sub-meshes draws its last sub-mesh once per extra material.
+ _renderer.sharedMaterials = owner.Materials;
+ _renderer.motionVectorGenerationMode = MotionVectorGenerationMode.ForceNoMotion;
+ _renderer.shadowCastingMode = ShadowCastingMode.Off;
+ _renderer.receiveShadows = false;
+ _renderer.lightProbeUsage = LightProbeUsage.Off;
+ _renderer.reflectionProbeUsage = ReflectionProbeUsage.Off;
+ _renderer.allowOcclusionWhenDynamic = false;
+
+ // Creates the buffers.
+ Flush();
+ }
+
+ public BoneRegistry Bones
+ {
+ get => _bones;
+ }
+
+ internal Mesh Mesh
+ {
+ get => _mesh;
+ }
+
+ internal MeshRenderer Renderer
+ {
+ get => _renderer;
+ }
+
+ internal VertexAllocator Allocator
+ {
+ get => _allocator;
+ }
+
+ /// True for a chunk made for a shape bigger than .
+ internal bool IsLarge
+ {
+ get => _vertexLimit > MaxVertexCount;
+ }
+
+ internal int ShapeCount
+ {
+ get => _shapeCount;
+ }
+
+ internal int HiddenShapeCount
+ {
+ get => _hiddenShapeCount;
+ }
+
+ /// Vertices that live shapes use, leaving out freed blocks that aren't reused yet.
+ internal int UsedVertexCount
+ {
+ get => _allocator.End - _allocator.FreeCount;
+ }
+
+ /// Bytes of CPU memory in the chunk's buffers and lists, about.
+ internal long CpuMemory
+ {
+ get
+ {
+ // An edge slot holds two indices, its owner and the owner's edge number, then its 16-bit copy.
+ long edgeSize = 3 * sizeof(int) + IntPtr.Size + 2 * sizeof(ushort);
+ return (long)_positions.Length * VertexSize
+ + _edges.Capacity * edgeSize
+ + (long)(_shapes.Length + _pending.Length) * IntPtr.Size;
+ }
+ }
+
+ /// Bytes of GPU memory in the chunk's vertex and index buffers.
+ internal long GpuMemory
+ {
+ get => (long)_meshVertexCapacity * VertexSize + (long)_meshIndexCapacity * IndexSize;
+ }
+
+ /// Entries in the queue of shapes to write on the next flush, including those emptied by removals.
+ internal int PendingCount
+ {
+ get => _pendingCount;
+ }
+
+ /// Entries the queue has room for before it grows.
+ internal int PendingCapacity
+ {
+ get => _pending.Length;
+ }
+
+ /// Vertices the last flush uploaded, counted once for each stream they were uploaded to.
+ internal int UploadedVertexCount { get; private set; }
+
+ /// Indices the last flush uploaded.
+ internal int UploadedIndexCount { get; private set; }
+
+ internal int EdgeCount
+ {
+ get => _edges.Count;
+ }
+
+ internal int VertexCapacity
+ {
+ get => _positions.Length;
+ }
+
+ internal int EdgeCapacity
+ {
+ get => _edges.Capacity;
+ }
+
+ /// True once freed vertex blocks take up enough of the chunk to be worth packing away.
+ internal bool NeedsCompaction
+ {
+ get => _allocator.FreeCount >= CompactionThreshold && _allocator.FreeCount * 2 > _allocator.End;
+ }
+
+ ///
+ /// Time the chunk was first seen empty, kept by its ; NaN while it holds shapes.
+ ///
+ internal float EmptySince { get; set; } = float.NaN;
+
+ /// CPU copy of every vertex position, relative to its bone.
+ internal Vector3[] Positions
+ {
+ get => _positions;
+ }
+
+ /// CPU copy of every vertex's bone slot.
+ internal float[] BoneIndices
+ {
+ get => _boneIndices;
+ }
+
+ private int IndexSize
+ {
+ get => IsLarge ? sizeof(uint) : sizeof(ushort);
+ }
+
+ /// True when vertices fit in the buffers as they are.
+ internal bool HasRoomFor(int vertexCount)
+ {
+ return _allocator.EndAfterAllocate(vertexCount) <= _positions.Length;
+ }
+
+ /// True when vertices fit once the buffers grow to their limit.
+ internal bool CanGrowToFit(int vertexCount)
+ {
+ return _allocator.EndAfterAllocate(vertexCount) <= _vertexLimit;
+ }
+
+ internal void Add(Shape shape)
+ {
+ // Capacity is checked before anything changes, so a device limit leaves the chunk untouched.
+ EnsureVertexCapacity(_allocator.EndAfterAllocate(shape.VertexCount));
+ if (!shape.IsHidden)
+ {
+ EnsureEdgeCapacity(_edges.Count + shape.EdgeCount);
+ }
+
+ shape.VertexStart = _allocator.Allocate(shape.VertexCount);
+ if (shape.IsHidden)
+ {
+ _hiddenShapeCount++;
+ }
+ else
+ {
+ AddEdges(shape);
+ }
+ if (_shapeCount == _shapes.Length)
+ {
+ Array.Resize(ref _shapes, _shapeCount * 2);
+ }
+ shape.ShapeIndex = _shapeCount;
+ _shapes[_shapeCount++] = shape;
+ EmptySince = float.NaN;
+ }
+
+ public void Remove(Shape shape)
+ {
+ if (shape.IsHidden)
+ {
+ _hiddenShapeCount--;
+ }
+ else
+ {
+ RemoveEdges(shape);
+ }
+ _allocator.Free(shape.VertexStart, shape.VertexCount);
+ // Its queue entry is emptied, so a shape that comes back is never queued twice.
+ int pending = shape.PendingIndex;
+ if (pending >= 0 && pending < _pendingCount && _pending[pending] == shape)
+ {
+ _pending[pending] = null;
+ }
+ shape.PendingIndex = -1;
+
+ int last = --_shapeCount;
+ Shape moved = _shapes[last];
+ _shapes[shape.ShapeIndex] = moved;
+ moved.ShapeIndex = shape.ShapeIndex;
+ _shapes[last] = null;
+ }
+
+ public void Show(Shape shape)
+ {
+ EnsureEdgeCapacity(_edges.Count + shape.EdgeCount);
+ AddEdges(shape);
+ _hiddenShapeCount--;
+ shape.MarkDirty(DirtyFlags.Edges);
+ }
+
+ public void Hide(Shape shape)
+ {
+ RemoveEdges(shape);
+ _hiddenShapeCount++;
+ }
+
+ public IShapeHost Reattach(Shape shape)
+ {
+ return _owner.Attach(shape);
+ }
+
+ public void Enqueue(Shape shape)
+ {
+ if (_pendingCount == _pending.Length)
+ {
+ // Without renders to flush the queue, the entries of removed shapes would pile up in it; each shape of
+ // the chunk is queued at most once, so dropping them keeps the queue within the chunk's shape count.
+ CompactPending();
+ if (_pendingCount == _pending.Length)
+ {
+ Array.Resize(ref _pending, _pendingCount * 2);
+ }
+ }
+ shape.PendingIndex = _pendingCount;
+ _pending[_pendingCount++] = shape;
+ }
+
+ /// Writes queued shapes into the buffers and uploads what changed.
+ internal void Flush()
+ {
+ using (WireframesMarkers.WriteShapes.Auto())
+ {
+ WritePendingShapes();
+ }
+ using (WireframesMarkers.UploadMesh.Auto())
+ {
+ UploadedVertexCount = 0;
+ UploadedIndexCount = 0;
+ UploadVertices();
+ UploadIndices();
+ }
+ }
+
+ ///
+ /// Hands out vertex blocks again from vertex 0, packing live shapes together, and gives back memory that the
+ /// packed shapes leave unused. Every shape is written and uploaded again on the next flush.
+ ///
+ internal void Compact()
+ {
+ using (WireframesMarkers.Compact.Auto())
+ {
+ _allocator.Reset();
+ for (int i = 0; i < _shapeCount; i++)
+ {
+ Shape shape = _shapes[i];
+ shape.VertexStart = _allocator.Allocate(shape.VertexCount);
+ shape.MarkDirty(DirtyFlags.All);
+ }
+
+ int vertexCapacity = ShrunkCapacity(_positions.Length, _allocator.End, _minimumVertexCapacity);
+ if (vertexCapacity < _positions.Length)
+ {
+ ResizeVertexStreams(vertexCapacity);
+ }
+ int edgeCapacity = ShrunkCapacity(_edges.Capacity, _edges.Count, _minimumEdgeCapacity);
+ if (edgeCapacity < _edges.Capacity)
+ {
+ using (WireframesMarkers.ResizeBuffers.Auto())
+ {
+ _edges.Shrink(edgeCapacity);
+ }
+ }
+ }
+ }
+
+ public void Dispose()
+ {
+ for (int i = 0; i < _shapeCount; i++)
+ {
+ _shapes[i].Detach();
+ _shapes[i] = null;
+ }
+ _shapeCount = 0;
+ _hiddenShapeCount = 0;
+ Array.Clear(_pending, 0, _pendingCount);
+ _pendingCount = 0;
+ // Play Mode destroys objects at the end of the frame, and a render before that must not draw the chunk.
+ if (_renderer != null)
+ {
+ _renderer.enabled = false;
+ }
+ // The chunk's GameObject belongs to its owner, which destroys it; the mesh is an asset and goes here.
+ UnityObjects.Destroy(_mesh);
+ }
+
+ private void WritePendingShapes()
+ {
+ for (int i = 0; i < _pendingCount; i++)
+ {
+ Shape shape = _pending[i];
+ // Removed shapes leave an empty entry behind.
+ if (shape == null)
+ {
+ continue;
+ }
+ _pending[i] = null;
+ shape.PendingIndex = -1;
+ try
+ {
+ WriteShape(shape, shape.TakeDirty());
+ }
+ catch (Exception exception)
+ {
+ // One broken shape must not stop the others from drawing; it keeps what it last wrote.
+ if (!shape.HasReportedProblem)
+ {
+ shape.HasReportedProblem = true;
+ WireframesLog.Error(
+ $"A {shape.GetType().Name} failed to update and was skipped.", exception, _renderer);
+ }
+ }
+ }
+ _pendingCount = 0;
+ }
+
+ private void WriteShape(Shape shape, DirtyFlags dirty)
+ {
+ int start = shape.VertexStart;
+ int count = shape.VertexCount;
+ if ((dirty & DirtyFlags.Positions) != 0)
+ {
+ shape.WritePositions(new Span(_positions, start, count));
+ _positionRanges.Add(start, count);
+ CheckPositions(shape);
+ }
+ if ((dirty & DirtyFlags.Colors) != 0)
+ {
+ shape.WriteColors(new Span(_colors, start, count));
+ _colorRanges.Add(start, count);
+ }
+ if ((dirty & DirtyFlags.Bones) != 0)
+ {
+ shape.WriteBoneIndices(new Span(_boneIndices, start, count));
+ _boneRanges.Add(start, count);
+ }
+ // A hidden shape has no edge slots; showing it marks its edges again.
+ if ((dirty & DirtyFlags.Edges) != 0 && !shape.IsHidden)
+ {
+ shape.WriteEdges(_edges, _edgeRanges);
+ }
+ }
+
+ private void AddEdges(Shape shape)
+ {
+ for (int edge = 0; edge < shape.EdgeCount; edge++)
+ {
+ shape.SetEdgeSlot(edge, _edges.Add(shape, edge));
+ }
+ }
+
+ private void RemoveEdges(Shape shape)
+ {
+ // The last edge moves into each freed slot; later edges of this same shape may be among them, and
+ // their owner callback keeps the shape's slots current while the loop runs.
+ for (int edge = 0; edge < shape.EdgeCount; edge++)
+ {
+ int slot = shape.GetEdgeSlot(edge);
+ if (_edges.RemoveAt(slot))
+ {
+ _edgeRanges.Add(slot, 1);
+ }
+ }
+ }
+
+ /// Warns once per shape about positions that aren't finite, in the Editor and development builds.
+ [System.Diagnostics.Conditional("UNITY_EDITOR"), System.Diagnostics.Conditional("DEVELOPMENT_BUILD")]
+ private void CheckPositions(Shape shape)
+ {
+ if (shape.HasReportedProblem)
+ {
+ return;
+ }
+ int end = shape.VertexStart + shape.VertexCount;
+ for (int i = shape.VertexStart; i < end; i++)
+ {
+ Vector3 position = _positions[i];
+ if (!float.IsFinite(position.x) || !float.IsFinite(position.y) || !float.IsFinite(position.z))
+ {
+ shape.HasReportedProblem = true;
+ WireframesLog.Warning(
+ $"A {shape.GetType().Name} has a position that isn't a finite number, so it can't be drawn "
+ + "correctly. Check the positions, sizes and rotations set on it.",
+ _renderer);
+ return;
+ }
+ }
+ }
+
+ /// Drops the empty entries that removed shapes left in the queue.
+ private void CompactPending()
+ {
+ int kept = 0;
+ for (int i = 0; i < _pendingCount; i++)
+ {
+ Shape shape = _pending[i];
+ if (shape != null)
+ {
+ shape.PendingIndex = kept;
+ _pending[kept++] = shape;
+ }
+ }
+ Array.Clear(_pending, kept, _pendingCount - kept);
+ _pendingCount = kept;
+ }
+
+ private void UploadVertices()
+ {
+ int capacity = _positions.Length;
+ if (_meshVertexCapacity == capacity)
+ {
+ UploadRanges(_positionRanges, _positions, PositionStream);
+ UploadRanges(_colorRanges, _colors, ColorStream);
+ UploadRanges(_boneRanges, _boneIndices, BoneStream);
+ return;
+ }
+
+ // A resized buffer starts with garbage, so every stream is rewritten in full.
+ using (WireframesMarkers.ResizeBuffers.Auto())
+ {
+ _mesh.SetVertexBufferParams(capacity, VertexLayout);
+ _mesh.SetVertexBufferData(_positions, 0, 0, capacity, PositionStream, UploadFlags);
+ _mesh.SetVertexBufferData(_colors, 0, 0, capacity, ColorStream, UploadFlags);
+ _mesh.SetVertexBufferData(_boneIndices, 0, 0, capacity, BoneStream, UploadFlags);
+ }
+ UploadedVertexCount += capacity * 3;
+ _positionRanges.Clear();
+ _colorRanges.Clear();
+ _boneRanges.Clear();
+ _meshVertexCapacity = capacity;
+ // The sub-mesh records the vertex range it uses, so it has to be set again.
+ _meshEdgeCount = -1;
+ }
+
+ private void UploadRanges(DirtyRanges ranges, T[] data, int stream) where T : struct
+ {
+ int count = ranges.Merge();
+ for (int i = 0; i < count; i++)
+ {
+ RangeInt range = ranges[i];
+ _mesh.SetVertexBufferData(data, range.start, range.start, range.length, stream, UploadFlags);
+ UploadedVertexCount += range.length;
+ }
+ ranges.Clear();
+ }
+
+ private void UploadIndices()
+ {
+ int edgeCount = _edges.Count;
+ int indexCapacity = _edges.Capacity * 2;
+ IndexFormat format = IsLarge ? IndexFormat.UInt32 : IndexFormat.UInt16;
+
+ if (_meshIndexCapacity != indexCapacity || _meshIndexFormat != format)
+ {
+ using (WireframesMarkers.ResizeBuffers.Auto())
+ {
+ _mesh.SetIndexBufferParams(indexCapacity, format);
+ _meshIndexCapacity = indexCapacity;
+ _meshIndexFormat = format;
+ _edgeRanges.Clear();
+ UploadIndexRange(0, edgeCount * 2);
+ _meshEdgeCount = -1;
+ }
+ }
+ else
+ {
+ int count = _edgeRanges.Merge();
+ for (int i = 0; i < count; i++)
+ {
+ // Slots freed after they were marked may now lie past the last edge; those aren't drawn.
+ RangeInt range = _edgeRanges[i];
+ int end = Math.Min(range.end, edgeCount);
+ UploadIndexRange(range.start * 2, (end - range.start) * 2);
+ }
+ _edgeRanges.Clear();
+ }
+
+ if (_meshEdgeCount != edgeCount)
+ {
+ SubMeshDescriptor subMesh = new(0, edgeCount * 2, MeshTopology.Lines)
+ {
+ firstVertex = 0,
+ vertexCount = _meshVertexCapacity,
+ bounds = FixedBounds
+ };
+ _mesh.SetSubMesh(0, subMesh, UploadFlags);
+ _meshEdgeCount = edgeCount;
+ }
+ }
+
+ private void UploadIndexRange(int start, int count)
+ {
+ if (count <= 0)
+ {
+ return;
+ }
+ UploadedIndexCount += count;
+ int[] indices = _edges.Indices;
+ if (_meshIndexFormat == IndexFormat.UInt32)
+ {
+ _mesh.SetIndexBufferData(indices, start, start, count, UploadFlags);
+ return;
+ }
+ if (_shortIndices.Length != _meshIndexCapacity)
+ {
+ _shortIndices = new ushort[_meshIndexCapacity];
+ }
+ for (int i = start; i < start + count; i++)
+ {
+ _shortIndices[i] = (ushort)indices[i];
+ }
+ _mesh.SetIndexBufferData(_shortIndices, start, start, count, UploadFlags);
+ }
+
+ private void EnsureVertexCapacity(int required)
+ {
+ int capacity = _positions.Length;
+ if (required <= capacity)
+ {
+ return;
+ }
+ if (required > _vertexLimit)
+ {
+ throw new InvalidOperationException(
+ $"A chunk holds at most {_vertexLimit} vertices, but {required} were requested.");
+ }
+ capacity = Math.Max(capacity, 1);
+ while (capacity < required)
+ {
+ capacity *= 2;
+ }
+ ResizeVertexStreams(Math.Min(capacity, _vertexLimit));
+ }
+
+ private void ResizeVertexStreams(int capacity)
+ {
+ using (WireframesMarkers.ResizeBuffers.Auto())
+ {
+ // New elements default to zero, which is bone slot 0 (world space), a valid index for the shader.
+ Array.Resize(ref _positions, capacity);
+ Array.Resize(ref _colors, capacity);
+ Array.Resize(ref _boneIndices, capacity);
+ }
+ }
+
+ private void EnsureEdgeCapacity(int required)
+ {
+ int capacity = _edges.Capacity;
+ if (required <= capacity)
+ {
+ return;
+ }
+ capacity = Math.Max(capacity, 1);
+ while (capacity < required)
+ {
+ capacity *= 2;
+ }
+ CheckBufferSize((long)capacity * 2 * IndexSize);
+ using (WireframesMarkers.ResizeBuffers.Auto())
+ {
+ _edges.EnsureCapacity(capacity);
+ }
+ }
+
+ ///
+ /// The capacity to shrink to: twice what is used once that is at most a quarter, so it takes doubling the load
+ /// to grow again, and never below .
+ ///
+ private static int ShrunkCapacity(int capacity, int used, int minimum)
+ {
+ return used <= capacity / 4 ? Math.Min(capacity, Math.Max(minimum, used * 2)) : capacity;
+ }
+
+ private static void CheckBufferSize(long bytes)
+ {
+ // Without a graphics device (batch mode with -nographics) the limit reads as zero.
+ long limit = SystemInfo.maxGraphicsBufferSize;
+ if (limit > 0 && bytes > limit)
+ {
+ throw new InvalidOperationException(
+ $"Wireframes needs a {bytes}-byte GPU buffer, but this device allows at most {limit} bytes.");
+ }
+ }
+ }
+}
diff --git a/UnityPackage/Runtime/Core/MeshChunk.cs.meta b/UnityPackage/Runtime/Rendering/MeshChunk.cs.meta
similarity index 100%
rename from UnityPackage/Runtime/Core/MeshChunk.cs.meta
rename to UnityPackage/Runtime/Rendering/MeshChunk.cs.meta
diff --git a/UnityPackage/Runtime/Rendering/MeshProxy.cs b/UnityPackage/Runtime/Rendering/MeshProxy.cs
new file mode 100644
index 0000000..a1f04c3
--- /dev/null
+++ b/UnityPackage/Runtime/Rendering/MeshProxy.cs
@@ -0,0 +1,330 @@
+using System;
+using System.Collections.Generic;
+using UnityEngine;
+using UnityEngine.Rendering;
+using UnityEngine.SceneManagement;
+
+namespace reromanlee.Wireframes
+{
+ ///
+ /// Component on the container's GameObject, in Play and Edit Mode alike. Right before each render, it applies queued
+ /// shape edits and reads the bones' matrices, so everything that moved or changed earlier in the frame shows up in
+ /// that frame. It releases every resource when its GameObject is destroyed, including when the scene unloads. Where
+ /// nothing can draw, it keeps the shapes in a and does no rendering work at all.
+ ///
+ [AddComponentMenu("")]
+ [DisallowMultipleComponent]
+ [ExecuteAlways]
+ internal sealed class MeshProxy : MonoBehaviour
+ {
+ private static readonly List Live = new();
+ private static bool _hasWarnedAboutShader;
+
+ private readonly BoneRegistry _bones = new();
+ private readonly BoneTexture _boneTexture = new();
+ private ChunkAllocator _chunks;
+ private HeadlessHost _headless;
+ private WireframeContainer _container;
+ private Material[] _materials;
+ private bool _ownsMaterials;
+ private bool _isShutDown;
+ private bool _hasLoggedFlushFailure;
+
+ /// Makes new containers act as they do without a graphics device, so tests can cover server builds.
+ internal static bool SimulateNoGraphics { get; set; }
+
+ /// The proxy of every container that isn't disposed yet, for editor code that manages their lifetime.
+ internal static IReadOnlyList LiveProxies
+ {
+ get => Live;
+ }
+
+ internal WireframeContainer Container
+ {
+ get => _container;
+ }
+
+ /// True for a container created in Edit Mode, which the editor disposes itself.
+ internal bool IsEditMode { get; private set; }
+
+ internal bool PersistsAcrossScenes { get; private set; }
+
+ ///
+ /// The scene the container belongs to. It still identifies that scene after the proxy leaves it, as it does when
+ /// Play Mode reloads the scene and the proxy, never saved, stays outside any scene.
+ ///
+ internal Scene HomeScene { get; set; }
+
+ /// True when the container draws nothing, because there is no graphics device or no usable shader.
+ internal bool IsHeadless
+ {
+ get => _headless != null;
+ }
+
+ ///
+ /// False deactivates the container's GameObject: its chunks aren't drawn and, with the render callbacks it drops,
+ /// nothing is flushed until it is shown again, when every edit made meanwhile is applied.
+ ///
+ internal bool IsVisible
+ {
+ get => gameObject.activeSelf;
+ set => gameObject.SetActive(value);
+ }
+
+ internal IReadOnlyList Chunks
+ {
+ get => _chunks != null ? _chunks.Chunks : Array.Empty();
+ }
+
+ internal ChunkAllocator ChunkAllocator
+ {
+ get => _chunks;
+ }
+
+ internal HeadlessHost HeadlessHost
+ {
+ get => _headless;
+ }
+
+ /// The materials every chunk draws with; null when headless.
+ internal Material[] Materials
+ {
+ get => _materials;
+ }
+
+ internal BoneRegistry Bones
+ {
+ get => _bones;
+ }
+
+ internal BoneTexture BoneTexture
+ {
+ get => _boneTexture;
+ }
+
+ internal void Initialize(WireframeContainer container, WireframeContainerSettings settings)
+ {
+ _container = container;
+ IsEditMode = !Application.isPlaying;
+ PersistsAcrossScenes = settings.PersistAcrossScenes;
+ HomeScene = gameObject.scene;
+ Live.Add(this);
+ if (!TryCreateMaterials(settings))
+ {
+ _headless = new HeadlessHost(_bones);
+ return;
+ }
+ _chunks = new ChunkAllocator(transform, _materials, settings.Layer, _bones);
+ _chunks.Reserve(settings.VertexCapacity, settings.EdgeCapacity);
+ // Uploads the bone texture and hands it to the chunks.
+ Flush();
+ }
+
+ /// Attaches a newly built and returns it. If that fails, nothing of it remains.
+ internal T Add(T shape) where T : Shape
+ {
+ shape.Attach(this);
+ return shape;
+ }
+
+ /// Hands to a chunk, or to the headless host, and returns it.
+ internal IShapeHost Attach(Shape shape)
+ {
+ if (_headless != null)
+ {
+ _headless.Add(shape);
+ return _headless;
+ }
+ return _chunks.Attach(shape);
+ }
+
+ ///
+ /// Releases the chunks that stayed empty long enough; see for where
+ /// this may run.
+ ///
+ internal void ReleaseIdleChunks(float time)
+ {
+ if (!_isShutDown)
+ {
+ _chunks?.ReleaseIdleChunks(time);
+ }
+ }
+
+ /// Counts what the container holds by walking its chunks, without allocating.
+ internal WireframeStatistics GetStatistics()
+ {
+ if (_headless != null)
+ {
+ return new WireframeStatistics(
+ _headless.ShapeCount, _headless.HiddenShapeCount, _headless.VertexCount, 0, _bones.Count, 0,
+ _bones.CpuMemory, 0);
+ }
+ int shapes = 0;
+ int hiddenShapes = 0;
+ int vertices = 0;
+ int edges = 0;
+ long cpuMemory = _bones.CpuMemory + _boneTexture.Memory;
+ long gpuMemory = _boneTexture.Memory;
+ IReadOnlyList chunks = _chunks.Chunks;
+ for (int i = 0; i < chunks.Count; i++)
+ {
+ MeshChunk chunk = chunks[i];
+ shapes += chunk.ShapeCount;
+ hiddenShapes += chunk.HiddenShapeCount;
+ vertices += chunk.UsedVertexCount;
+ edges += chunk.EdgeCount;
+ cpuMemory += chunk.CpuMemory;
+ gpuMemory += chunk.GpuMemory;
+ }
+ return new WireframeStatistics(
+ shapes, hiddenShapes, vertices, edges, _bones.Count, chunks.Count, cpuMemory, gpuMemory);
+ }
+
+ /// Reads the bones, writes queued shapes and uploads what changed.
+ internal void Flush()
+ {
+ if (_chunks == null)
+ {
+ return;
+ }
+ using (WireframesMarkers.Flush.Auto())
+ {
+ _bones.ReadMatrices();
+ if (_boneTexture.Upload(_bones))
+ {
+ _chunks.SetBoneTexture(_boneTexture.Texture);
+ }
+ _chunks.Flush();
+ }
+ }
+
+ internal void Shutdown()
+ {
+ if (_isShutDown)
+ {
+ return;
+ }
+ _isShutDown = true;
+ Live.Remove(this);
+ // The container learns first, so it reads as disposed even if releasing something below throws.
+ _container?.OnProxyShutdown();
+ _chunks?.Dispose();
+ _headless?.Dispose();
+ _boneTexture.Dispose();
+ if (_ownsMaterials)
+ {
+ foreach (Material material in _materials)
+ {
+ UnityObjects.Destroy(material);
+ }
+ }
+ _materials = null;
+ }
+
+ ///
+ /// Picks the materials to draw with, or returns false when nothing can draw: without a graphics device, silently,
+ /// as that is expected in server builds, and without a usable shader, with one warning per session.
+ ///
+ private bool TryCreateMaterials(WireframeContainerSettings settings)
+ {
+ if (SystemInfo.graphicsDeviceType == GraphicsDeviceType.Null || SimulateNoGraphics)
+ {
+ return false;
+ }
+ if (settings.Material != null)
+ {
+ _materials = new[] { settings.Material };
+ return true;
+ }
+ // The shader sits in a Resources folder, which keeps it in player builds.
+ Shader shader = Shader.Find(WireframeMaterials.ShaderName);
+ if (shader == null || !shader.isSupported)
+ {
+ WarnAboutShader(shader == null
+ ? $"The shader '{WireframeMaterials.ShaderName}' isn't in this build"
+ : $"The shader '{WireframeMaterials.ShaderName}' isn't supported on {SystemInfo.graphicsDeviceType}");
+ return false;
+ }
+ _materials = WireframeMaterials.Create(shader, settings.Occlusion, settings.UseAlpha);
+ _ownsMaterials = true;
+ return true;
+ }
+
+ private void WarnAboutShader(string problem)
+ {
+ if (_hasWarnedAboutShader)
+ {
+ return;
+ }
+ _hasWarnedAboutShader = true;
+ WireframesLog.Warning($"{problem}, so wireframes won't be drawn. Their shapes still work.", this);
+ }
+
+ [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
+ private static void ResetWarnings()
+ {
+ // Needed when domain reload is disabled, so each Play Mode session warns again.
+ _hasWarnedAboutShader = false;
+ }
+
+ 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;
+ }
+
+ private void OnDisable()
+ {
+ RenderPipelineManager.beginContextRendering -= OnBeginContextRendering;
+ Camera.onPreCull -= OnPreCullCamera;
+ }
+
+ private void OnDestroy()
+ {
+ Shutdown();
+ }
+
+ private void OnBeginContextRendering(ScriptableRenderContext context, List cameras)
+ {
+ FlushBeforeRendering();
+ }
+
+ private void OnPreCullCamera(Camera camera)
+ {
+ FlushBeforeRendering();
+ }
+
+ private void FlushBeforeRendering()
+ {
+ if (_isShutDown)
+ {
+ return;
+ }
+ try
+ {
+ WireframesCounters.Update();
+ if (_chunks == null)
+ {
+ return;
+ }
+ Flush();
+ // Play Mode only, where destroying waits for the end of the frame and is allowed in a render callback.
+ // In Edit Mode, the editor's update loop releases them instead.
+ if (Application.isPlaying)
+ {
+ ReleaseIdleChunks(Time.realtimeSinceStartup);
+ }
+ }
+ catch (Exception exception)
+ {
+ // An exception must never escape into Unity's rendering, nor flood the console once every frame.
+ if (!_hasLoggedFlushFailure)
+ {
+ _hasLoggedFlushFailure = true;
+ WireframesLog.Error("Updating the wireframes before a render failed.", exception, this);
+ }
+ }
+ }
+ }
+}
diff --git a/UnityPackage/Runtime/Core/MeshProxy.cs.meta b/UnityPackage/Runtime/Rendering/MeshProxy.cs.meta
similarity index 100%
rename from UnityPackage/Runtime/Core/MeshProxy.cs.meta
rename to UnityPackage/Runtime/Rendering/MeshProxy.cs.meta
diff --git a/UnityPackage/Runtime/Rendering/WireframeMaterials.cs b/UnityPackage/Runtime/Rendering/WireframeMaterials.cs
new file mode 100644
index 0000000..d047b06
--- /dev/null
+++ b/UnityPackage/Runtime/Rendering/WireframeMaterials.cs
@@ -0,0 +1,79 @@
+using UnityEngine;
+using UnityEngine.Rendering;
+
+namespace reromanlee.Wireframes
+{
+ ///
+ /// Builds the package's materials for a container's settings. Every look is the same shader with a different depth
+ /// test and blending; with , a second material draws the same mesh again, only
+ /// where other geometry hides it, and dimmer.
+ ///
+ internal static class WireframeMaterials
+ {
+ internal const string ShaderName = "reromanlee/Wireframes/Unlit";
+
+ // Share of a color's alpha that hidden parts keep with Fade.
+ private const float FadedAlpha = 0.3f;
+
+ private static readonly int DepthTestId = Shader.PropertyToID("_ZTest");
+ private static readonly int DepthWriteId = Shader.PropertyToID("_ZWrite");
+ private static readonly int SourceBlendId = Shader.PropertyToID("_SrcBlend");
+ private static readonly int DestinationBlendId = Shader.PropertyToID("_DstBlend");
+ private static readonly int VertexAlphaId = Shader.PropertyToID("_WireframesVertexAlpha");
+ private static readonly int AlphaScaleId = Shader.PropertyToID("_WireframesAlphaScale");
+
+ /// The renderer's materials: the lines, then with Fade the dimmed hidden parts. The caller owns them.
+ internal static Material[] Create(Shader shader, WireframeOcclusion occlusion, bool useAlpha)
+ {
+ Material lines = CreateMaterial(shader, "Wireframes");
+ if (occlusion == WireframeOcclusion.Show)
+ {
+ // Drawn after the scene's opaque geometry, over all of it.
+ SetState(lines, CompareFunction.Always, false, useAlpha);
+ SetQueue(lines, RenderQueue.Transparent);
+ }
+ else
+ {
+ SetState(lines, CompareFunction.LessEqual, !useAlpha, useAlpha);
+ SetQueue(lines, useAlpha ? RenderQueue.Transparent : RenderQueue.Geometry);
+ }
+ SetAlpha(lines, useAlpha, 1f);
+ if (occlusion != WireframeOcclusion.Fade)
+ {
+ return new[] { lines };
+ }
+
+ // Only where the depth buffer already holds something closer: the parts that geometry hides.
+ Material hidden = CreateMaterial(shader, "Wireframes Hidden");
+ SetState(hidden, CompareFunction.Greater, false, true);
+ SetQueue(hidden, RenderQueue.Transparent);
+ SetAlpha(hidden, useAlpha, FadedAlpha);
+ return new[] { lines, hidden };
+ }
+
+ private static Material CreateMaterial(Shader shader, string name)
+ {
+ return new Material(shader) { name = name, hideFlags = HideFlags.DontSave };
+ }
+
+ private static void SetState(Material material, CompareFunction depthTest, bool depthWrite, bool blend)
+ {
+ material.SetFloat(DepthTestId, (float)depthTest);
+ material.SetFloat(DepthWriteId, depthWrite ? 1f : 0f);
+ material.SetFloat(SourceBlendId, (float)(blend ? BlendMode.SrcAlpha : BlendMode.One));
+ material.SetFloat(DestinationBlendId, (float)(blend ? BlendMode.OneMinusSrcAlpha : BlendMode.Zero));
+ }
+
+ private static void SetQueue(Material material, RenderQueue queue)
+ {
+ material.renderQueue = (int)queue;
+ material.SetOverrideTag("RenderType", queue == RenderQueue.Geometry ? "Opaque" : "Transparent");
+ }
+
+ private static void SetAlpha(Material material, bool useVertexAlpha, float scale)
+ {
+ material.SetFloat(VertexAlphaId, useVertexAlpha ? 1f : 0f);
+ material.SetFloat(AlphaScaleId, scale);
+ }
+ }
+}
diff --git a/UnityPackage/Runtime/Rendering/WireframeMaterials.cs.meta b/UnityPackage/Runtime/Rendering/WireframeMaterials.cs.meta
new file mode 100644
index 0000000..a0533c0
--- /dev/null
+++ b/UnityPackage/Runtime/Rendering/WireframeMaterials.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 543a46b730814377e0dddbe0737e9247
\ No newline at end of file
diff --git a/UnityPackage/Runtime/Resources/reromanlee.Wireframes/Unlit.shader b/UnityPackage/Runtime/Resources/reromanlee.Wireframes/Unlit.shader
new file mode 100644
index 0000000..5645fb5
--- /dev/null
+++ b/UnityPackage/Runtime/Resources/reromanlee.Wireframes/Unlit.shader
@@ -0,0 +1,208 @@
+Shader "reromanlee/Wireframes/Unlit"
+{
+ // Unlit vertex colors on Wireframes meshes. Each pipeline gets its own SubShader: URP's and HDRP's only exist in
+ // projects that have that pipeline, and the last one serves the Built-in Render Pipeline.
+ Properties
+ {
+ [Enum(UnityEngine.Rendering.CompareFunction)] _ZTest ("Depth Test", Float) = 4
+ [Enum(Off, 0, On, 1)] _ZWrite ("Depth Write", Float) = 1
+ [Enum(UnityEngine.Rendering.BlendMode)] _SrcBlend ("Source Blend", Float) = 1
+ [Enum(UnityEngine.Rendering.BlendMode)] _DstBlend ("Destination Blend", Float) = 0
+ [ToggleUI] _WireframesVertexAlpha ("Use Vertex Alpha", Float) = 0
+ _WireframesAlphaScale ("Alpha Scale", Range(0, 1)) = 1
+ }
+
+ SubShader
+ {
+ PackageRequirements
+ {
+ "com.unity.render-pipelines.universal"
+ }
+
+ Tags { "RenderPipeline" = "UniversalPipeline" "RenderType" = "Opaque" "Queue" = "Geometry" }
+
+ Pass
+ {
+ Name "Wireframes"
+ Tags { "LightMode" = "SRPDefaultUnlit" }
+
+ ZTest [_ZTest]
+ ZWrite [_ZWrite]
+ Blend [_SrcBlend] [_DstBlend]
+ Cull Off
+
+ HLSLPROGRAM
+ #pragma target 3.5
+ #pragma vertex Vertex
+ #pragma fragment Fragment
+
+ #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
+ #include "Packages/com.reromanlee.wireframes/Runtime/Shaders/Wireframes.hlsl"
+
+ CBUFFER_START(UnityPerMaterial)
+ half _WireframesVertexAlpha;
+ half _WireframesAlphaScale;
+ CBUFFER_END
+
+ struct Attributes
+ {
+ float3 position : POSITION;
+ half4 color : COLOR;
+ float bone : TEXCOORD0;
+ UNITY_VERTEX_INPUT_INSTANCE_ID
+ };
+
+ struct Varyings
+ {
+ float4 positionCS : SV_POSITION;
+ half4 color : COLOR;
+ UNITY_VERTEX_OUTPUT_STEREO
+ };
+
+ Varyings Vertex(Attributes input)
+ {
+ Varyings output;
+ UNITY_SETUP_INSTANCE_ID(input);
+ UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(output);
+ output.positionCS = TransformWorldToHClip(WireframesSkin(input.position, input.bone));
+ half alpha = lerp(1.0h, input.color.a, _WireframesVertexAlpha) * _WireframesAlphaScale;
+ output.color = half4(WireframesColor(input.color.rgb), alpha);
+ return output;
+ }
+
+ half4 Fragment(Varyings input) : SV_Target
+ {
+ return input.color;
+ }
+ ENDHLSL
+ }
+ }
+
+ SubShader
+ {
+ PackageRequirements
+ {
+ "com.unity.render-pipelines.high-definition"
+ }
+
+ Tags { "RenderPipeline" = "HDRenderPipeline" "RenderType" = "Opaque" "Queue" = "Geometry" }
+
+ Pass
+ {
+ Name "Wireframes"
+ Tags { "LightMode" = "ForwardOnly" }
+
+ ZTest [_ZTest]
+ ZWrite [_ZWrite]
+ Blend [_SrcBlend] [_DstBlend]
+ Cull Off
+
+ HLSLPROGRAM
+ #pragma target 4.5
+ #pragma only_renderers d3d11 playstation xboxone xboxseries vulkan metal switch
+ #pragma vertex Vertex
+ #pragma fragment Fragment
+
+ #include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Common.hlsl"
+ #include "Packages/com.unity.render-pipelines.high-definition/Runtime/ShaderLibrary/ShaderVariables.hlsl"
+ #include "Packages/com.reromanlee.wireframes/Runtime/Shaders/Wireframes.hlsl"
+
+ CBUFFER_START(UnityPerMaterial)
+ half _WireframesVertexAlpha;
+ half _WireframesAlphaScale;
+ CBUFFER_END
+
+ struct Attributes
+ {
+ float3 position : POSITION;
+ half4 color : COLOR;
+ float bone : TEXCOORD0;
+ UNITY_VERTEX_INPUT_INSTANCE_ID
+ };
+
+ struct Varyings
+ {
+ float4 positionCS : SV_POSITION;
+ half4 color : COLOR;
+ UNITY_VERTEX_OUTPUT_STEREO
+ };
+
+ Varyings Vertex(Attributes input)
+ {
+ Varyings output;
+ UNITY_SETUP_INSTANCE_ID(input);
+ UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(output);
+ // HDRP renders relative to the camera.
+ float3 world = WireframesSkin(input.position, input.bone);
+ output.positionCS = TransformWorldToHClip(GetCameraRelativePositionWS(world));
+ half alpha = lerp(1.0h, input.color.a, _WireframesVertexAlpha) * _WireframesAlphaScale;
+ output.color = half4(WireframesColor(input.color.rgb), alpha);
+ return output;
+ }
+
+ half4 Fragment(Varyings input) : SV_Target
+ {
+ return input.color;
+ }
+ ENDHLSL
+ }
+ }
+
+ SubShader
+ {
+ Tags { "RenderType" = "Opaque" "Queue" = "Geometry" }
+
+ Pass
+ {
+ Name "Wireframes"
+
+ ZTest [_ZTest]
+ ZWrite [_ZWrite]
+ Blend [_SrcBlend] [_DstBlend]
+ Cull Off
+
+ CGPROGRAM
+ #pragma target 3.5
+ #pragma vertex Vertex
+ #pragma fragment Fragment
+
+ #include "UnityCG.cginc"
+ #include "Packages/com.reromanlee.wireframes/Runtime/Shaders/Wireframes.hlsl"
+
+ half _WireframesVertexAlpha;
+ half _WireframesAlphaScale;
+
+ struct Attributes
+ {
+ float3 position : POSITION;
+ half4 color : COLOR;
+ float bone : TEXCOORD0;
+ UNITY_VERTEX_INPUT_INSTANCE_ID
+ };
+
+ struct Varyings
+ {
+ float4 positionCS : SV_POSITION;
+ half4 color : COLOR;
+ UNITY_VERTEX_OUTPUT_STEREO
+ };
+
+ Varyings Vertex(Attributes input)
+ {
+ Varyings output;
+ UNITY_SETUP_INSTANCE_ID(input);
+ UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(output);
+ output.positionCS = mul(UNITY_MATRIX_VP, float4(WireframesSkin(input.position, input.bone), 1.0));
+ half alpha = lerp(1.0h, input.color.a, _WireframesVertexAlpha) * _WireframesAlphaScale;
+ output.color = half4(WireframesColor(input.color.rgb), alpha);
+ return output;
+ }
+
+ half4 Fragment(Varyings input) : SV_Target
+ {
+ return input.color;
+ }
+ ENDCG
+ }
+ }
+}
diff --git a/UnityPackage/Runtime/Resources/reromanlee.Wireframes/VertexColors.shader.meta b/UnityPackage/Runtime/Resources/reromanlee.Wireframes/Unlit.shader.meta
similarity index 80%
rename from UnityPackage/Runtime/Resources/reromanlee.Wireframes/VertexColors.shader.meta
rename to UnityPackage/Runtime/Resources/reromanlee.Wireframes/Unlit.shader.meta
index 56f9a7f..cbe998c 100644
--- a/UnityPackage/Runtime/Resources/reromanlee.Wireframes/VertexColors.shader.meta
+++ b/UnityPackage/Runtime/Resources/reromanlee.Wireframes/Unlit.shader.meta
@@ -1,5 +1,5 @@
fileFormatVersion: 2
-guid: 78e8aacfb9ff39f4abe263ee40e07643
+guid: d4544708320996e8d49dc8ee6b8f59ed
ShaderImporter:
externalObjects: {}
defaultTextures: []
diff --git a/UnityPackage/Runtime/Resources/reromanlee.Wireframes/VertexColors.shader b/UnityPackage/Runtime/Resources/reromanlee.Wireframes/VertexColors.shader
deleted file mode 100644
index 5b762b8..0000000
--- a/UnityPackage/Runtime/Resources/reromanlee.Wireframes/VertexColors.shader
+++ /dev/null
@@ -1,56 +0,0 @@
-Shader "reromanlee/Wireframes/VertexColors" {
-
- Properties {
-
- }
-
- SubShader {
-
- Tags {
- "RenderType"="Opaque"
- }
-
- LOD 100
-
- Pass {
-
- CGPROGRAM
-
- #pragma vertex vert
- #pragma fragment frag
-
- #include "UnityCG.cginc"
-
- struct appdata {
- float4 vertex : POSITION;
- half4 color : COLOR;
- };
-
- struct v2f {
- float4 vertex : SV_POSITION;
- half4 color : COLOR;
- };
-
- v2f vert (appdata v) {
- v2f o;
- o.vertex = UnityObjectToClipPos(v.vertex);
- o.color = v.color;
- // Unity passes vertex colors through unconverted, so in linear projects they are converted here to
- // match how the same Color looks on a material.
- #if !defined(UNITY_COLORSPACE_GAMMA)
- o.color.rgb = GammaToLinearSpace(o.color.rgb);
- #endif
- return o;
- }
-
- half4 frag (v2f i) : SV_Target {
- return i.color;
- }
-
- ENDCG
-
- }
-
- }
-
-}
\ No newline at end of file
diff --git a/UnityPackage/Runtime/Shaders.meta b/UnityPackage/Runtime/Shaders.meta
new file mode 100644
index 0000000..2b00e2a
--- /dev/null
+++ b/UnityPackage/Runtime/Shaders.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: ad7b5a51d235e113b5af906db072a410
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/UnityPackage/Runtime/Shaders/Wireframes.hlsl b/UnityPackage/Runtime/Shaders/Wireframes.hlsl
new file mode 100644
index 0000000..31c2070
--- /dev/null
+++ b/UnityPackage/Runtime/Shaders/Wireframes.hlsl
@@ -0,0 +1,36 @@
+#ifndef REROMANLEE_WIREFRAMES_INCLUDED
+#define REROMANLEE_WIREFRAMES_INCLUDED
+
+// Skinning for Wireframes meshes, usable from any render pipeline. Every vertex stores its position relative to one
+// bone and that bone's index in TEXCOORD0; the package uploads each bone's local-to-world matrix into
+// _WireframesBones once per frame, as three texels (the matrix rows) per bone and 256 bones per texture row.
+// Bone 0 is the identity, which stands for world space.
+
+#define WIREFRAMES_BONES_PER_ROW 256u
+
+Texture2D _WireframesBones;
+
+// World position of a vertex stored relative to its bone.
+float3 WireframesSkin(float3 position, float bone)
+{
+ uint slot = (uint)(bone + 0.5);
+ int3 texel = int3((slot % WIREFRAMES_BONES_PER_ROW) * 3u, slot / WIREFRAMES_BONES_PER_ROW, 0);
+ float4 local = float4(position, 1.0);
+ return float3(
+ dot(_WireframesBones.Load(texel), local),
+ dot(_WireframesBones.Load(texel + int3(1, 0, 0)), local),
+ dot(_WireframesBones.Load(texel + int3(2, 0, 0)), local));
+}
+
+// Vertex colors reach shaders unconverted, so in linear color space they are converted here to look the same as that
+// color on a material.
+half3 WireframesColor(half3 color)
+{
+#if defined(UNITY_COLORSPACE_GAMMA)
+ return color;
+#else
+ return color * (color * (color * 0.305306011h + 0.682171111h) + 0.012522878h);
+#endif
+}
+
+#endif
diff --git a/UnityPackage/Runtime/Shaders/Wireframes.hlsl.meta b/UnityPackage/Runtime/Shaders/Wireframes.hlsl.meta
new file mode 100644
index 0000000..85a4677
--- /dev/null
+++ b/UnityPackage/Runtime/Shaders/Wireframes.hlsl.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: 89bd75423cceea077971c0cd9978d40f
+ShaderIncludeImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/UnityPackage/Runtime/Shapes/Box/Box.cs b/UnityPackage/Runtime/Shapes/Box/Box.cs
index 961a198..fca9fd1 100644
--- a/UnityPackage/Runtime/Shapes/Box/Box.cs
+++ b/UnityPackage/Runtime/Shapes/Box/Box.cs
@@ -18,8 +18,8 @@ internal sealed class Box : RigidShape, IBox
private Vector3 _size;
- internal Box(MeshProxy proxy, Transform bone, Vector3 localCenter, Quaternion localRotation, Vector3 size)
- : base(proxy, CornerCount, EdgePattern, bone, localCenter, localRotation)
+ internal Box(Transform bone, Vector3 localCenter, Quaternion localRotation, Vector3 size)
+ : base(CornerCount, EdgePattern, bone, localCenter, localRotation)
{
_size = size;
}
@@ -28,12 +28,12 @@ public Vector3 Size
{
get
{
- ThrowIfDisposed();
+ EnsureUsable();
return _size;
}
set
{
- ThrowIfDisposed();
+ EnsureUsable();
_size = value;
MarkDirty(DirtyFlags.Positions);
}
@@ -43,12 +43,12 @@ public Vector3 LocalCornerA
{
get
{
- ThrowIfDisposed();
+ EnsureUsable();
return LocalPosition - LocalRotation * (_size * 0.5f);
}
set
{
- ThrowIfDisposed();
+ EnsureUsable();
SetCorners(value, LocalCornerB);
}
}
@@ -57,12 +57,12 @@ public Vector3 LocalCornerB
{
get
{
- ThrowIfDisposed();
+ EnsureUsable();
return LocalPosition + LocalRotation * (_size * 0.5f);
}
set
{
- ThrowIfDisposed();
+ EnsureUsable();
SetCorners(LocalCornerA, value);
}
}
@@ -71,12 +71,12 @@ public Vector3 WorldCornerA
{
get
{
- ThrowIfDisposed();
+ EnsureUsable();
return ToWorld(Bone, LocalCornerA);
}
set
{
- ThrowIfDisposed();
+ EnsureUsable();
LocalCornerA = ToLocal(Bone, value);
}
}
@@ -85,12 +85,12 @@ public Vector3 WorldCornerB
{
get
{
- ThrowIfDisposed();
+ EnsureUsable();
return ToWorld(Bone, LocalCornerB);
}
set
{
- ThrowIfDisposed();
+ EnsureUsable();
LocalCornerB = ToLocal(Bone, value);
}
}
diff --git a/UnityPackage/Runtime/Shapes/Box/BoxFactory.cs b/UnityPackage/Runtime/Shapes/Box/BoxFactory.cs
new file mode 100644
index 0000000..693bbcd
--- /dev/null
+++ b/UnityPackage/Runtime/Shapes/Box/BoxFactory.cs
@@ -0,0 +1,41 @@
+using UnityEngine;
+
+namespace reromanlee.Wireframes
+{
+ /// Creates boxes in a .
+ public static class BoxFactory
+ {
+ /// Creates a white, world-aligned unit cube centered on the world origin.
+ public static IBox CreateBox(this WireframeContainer container)
+ {
+ MeshProxy proxy = WireframeContainer.ProxyOf(container);
+ return proxy.Add(new Box(null, Vector3.zero, Quaternion.identity, Vector3.one * ShapeDefaults.Size));
+ }
+
+ /// Creates a white, world-aligned box spanned by two opposite world-space corners.
+ public static IBox CreateBox(this WireframeContainer container, Vector3 cornerA, Vector3 cornerB)
+ {
+ MeshProxy proxy = WireframeContainer.ProxyOf(container);
+ return proxy.Add(new Box(null, (cornerA + cornerB) * 0.5f, Quaternion.identity, cornerB - cornerA));
+ }
+
+ ///
+ /// Creates a white box that follows , aligned to the bone's axes and spanned by two
+ /// opposite corners in the bone's local space.
+ ///
+ public static IBox CreateBox(
+ this WireframeContainer container, Transform bone, Vector3 localCornerA, Vector3 localCornerB)
+ {
+ MeshProxy proxy = WireframeContainer.ProxyOf(container);
+ Vector3 center = (localCornerA + localCornerB) * 0.5f;
+ return proxy.Add(new Box(bone, center, Quaternion.identity, localCornerB - localCornerA));
+ }
+
+ /// Creates a white box in world space from its center, rotation and size.
+ public static IBox CreateBox(this WireframeContainer container, Vector3 center, Quaternion rotation, Vector3 size)
+ {
+ MeshProxy proxy = WireframeContainer.ProxyOf(container);
+ return proxy.Add(new Box(null, center, rotation, size));
+ }
+ }
+}
diff --git a/UnityPackage/Runtime/Shapes/Box/LineContainer.Box.cs.meta b/UnityPackage/Runtime/Shapes/Box/BoxFactory.cs.meta
similarity index 100%
rename from UnityPackage/Runtime/Shapes/Box/LineContainer.Box.cs.meta
rename to UnityPackage/Runtime/Shapes/Box/BoxFactory.cs.meta
diff --git a/UnityPackage/Runtime/Shapes/Box/ILineContainer.Box.cs b/UnityPackage/Runtime/Shapes/Box/ILineContainer.Box.cs
deleted file mode 100644
index c648566..0000000
--- a/UnityPackage/Runtime/Shapes/Box/ILineContainer.Box.cs
+++ /dev/null
@@ -1,22 +0,0 @@
-using UnityEngine;
-
-namespace reromanlee.Wireframes
-{
- public partial interface ILineContainer
- {
- /// Creates a white, world-aligned unit cube centered on the world origin.
- IBox CreateBox();
-
- /// Creates a white, world-aligned box spanned by two opposite world-space corners.
- IBox CreateBox(Vector3 cornerA, Vector3 cornerB);
-
- ///
- /// Creates a white box that follows , aligned to the bone's axes and spanned by two
- /// opposite corners in the bone's local space.
- ///
- IBox CreateBox(Transform bone, Vector3 localCornerA, Vector3 localCornerB);
-
- /// Creates a white box in world space from its center, rotation and size.
- IBox CreateBox(Vector3 center, Quaternion rotation, Vector3 size);
- }
-}
diff --git a/UnityPackage/Runtime/Shapes/Box/ILineContainer.Box.cs.meta b/UnityPackage/Runtime/Shapes/Box/ILineContainer.Box.cs.meta
deleted file mode 100644
index 8fca1d0..0000000
--- a/UnityPackage/Runtime/Shapes/Box/ILineContainer.Box.cs.meta
+++ /dev/null
@@ -1,2 +0,0 @@
-fileFormatVersion: 2
-guid: eadf2f41fe8646e4bf183547aa1d3139
diff --git a/UnityPackage/Runtime/Shapes/Box/LineContainer.Box.cs b/UnityPackage/Runtime/Shapes/Box/LineContainer.Box.cs
deleted file mode 100644
index 2fbbb87..0000000
--- a/UnityPackage/Runtime/Shapes/Box/LineContainer.Box.cs
+++ /dev/null
@@ -1,28 +0,0 @@
-using UnityEngine;
-
-namespace reromanlee.Wireframes
-{
- public sealed partial class LineContainer
- {
- public IBox CreateBox()
- {
- return new Box(Proxy, null, Vector3.zero, Quaternion.identity, Vector3.one * UnitSize);
- }
-
- public IBox CreateBox(Vector3 cornerA, Vector3 cornerB)
- {
- return new Box(Proxy, null, (cornerA + cornerB) * 0.5f, Quaternion.identity, cornerB - cornerA);
- }
-
- public IBox CreateBox(Transform bone, Vector3 localCornerA, Vector3 localCornerB)
- {
- Vector3 center = (localCornerA + localCornerB) * 0.5f;
- return new Box(Proxy, bone, center, Quaternion.identity, localCornerB - localCornerA);
- }
-
- public IBox CreateBox(Vector3 center, Quaternion rotation, Vector3 size)
- {
- return new Box(Proxy, null, center, rotation, size);
- }
- }
-}
diff --git a/UnityPackage/Runtime/Shapes/Capsule/Capsule.cs b/UnityPackage/Runtime/Shapes/Capsule/Capsule.cs
index 36a9ca3..6c9dd5a 100644
--- a/UnityPackage/Runtime/Shapes/Capsule/Capsule.cs
+++ b/UnityPackage/Runtime/Shapes/Capsule/Capsule.cs
@@ -12,18 +12,16 @@ internal sealed class Capsule : AxialShape, ICapsule
private float _radiusB;
internal Capsule(
- MeshProxy proxy,
Transform bone,
Vector3 localPosition,
Quaternion localRotation,
float length,
float radiusA,
float radiusB,
- int segments)
+ int segmentCount)
: base(
- proxy,
- CountVertices(Ring.CheckQuarterSegments(segments)),
- Patterns.Get(segments),
+ CountVertices(Ring.CheckQuarterSegmentCount(segmentCount)),
+ Patterns.Get(segmentCount),
bone,
localPosition,
localRotation,
@@ -37,12 +35,12 @@ public float RadiusA
{
get
{
- ThrowIfDisposed();
+ EnsureUsable();
return _radiusA;
}
set
{
- ThrowIfDisposed();
+ EnsureUsable();
_radiusA = value;
MarkDirty(DirtyFlags.Positions);
}
@@ -52,22 +50,22 @@ public float RadiusB
{
get
{
- ThrowIfDisposed();
+ EnsureUsable();
return _radiusB;
}
set
{
- ThrowIfDisposed();
+ EnsureUsable();
_radiusB = value;
MarkDirty(DirtyFlags.Positions);
}
}
- public int Segments
+ public int SegmentCount
{
get
{
- ThrowIfDisposed();
+ EnsureUsable();
return (VertexCount + 6) / 4;
}
}
@@ -75,8 +73,8 @@ public int Segments
// Vertices: the ring on sphere A, the ring on sphere B, then the inside points of cap A's arcs and cap B's arcs.
protected override void WriteShape(Span positions, float length)
{
- int segments = (positions.Length + 6) / 4;
- int capVertices = segments - 3;
+ int segmentCount = (positions.Length + 6) / 4;
+ int capVertices = segmentCount - 3;
// Built along +Z for the sizes without their signs, then mirrored when the length is negative.
float span = Mathf.Abs(length);
float radiusA = Mathf.Abs(_radiusA);
@@ -84,10 +82,10 @@ protected override void WriteShape(Span positions, float length)
Vector3 centerB = new(0f, 0f, span);
Hull.Tangent(radiusA, radiusB, span, out float sine, out float cosine);
- Span ringA = positions.Slice(0, segments);
- Span ringB = positions.Slice(segments, segments);
- Span capA = positions.Slice(segments * 2, capVertices);
- Span capB = positions.Slice(segments * 2 + capVertices, capVertices);
+ Span ringA = positions.Slice(0, segmentCount);
+ Span ringB = positions.Slice(segmentCount, segmentCount);
+ Span capA = positions.Slice(segmentCount * 2, capVertices);
+ Span capB = positions.Slice(segmentCount * 2 + capVertices, capVertices);
WriteRing(ringA, Vector3.zero, radiusA, sine, cosine);
WriteRing(ringB, centerB, radiusB, sine, cosine);
WriteCap(capA, Vector3.zero, radiusA, Vector3.back, Mathf.Atan2(cosine, sine));
@@ -123,10 +121,10 @@ protected override void ScaleCrossSection(float factor)
_radiusB *= factor;
}
- private static int CountVertices(int segments)
+ private static int CountVertices(int segmentCount)
{
// Two rings, and two caps whose arcs share the ring points and each cap's pole.
- return segments * 4 - 6;
+ return segmentCount * 4 - 6;
}
///
@@ -165,30 +163,30 @@ private static void WriteCap(Span cap, Vector3 center, float radius, Ve
}
}
- private static int[] BuildEdges(int segments)
+ private static int[] BuildEdges(int segmentCount)
{
- int[] pattern = new int[(segments * 4 + SideLineCount) * 2];
+ int[] pattern = new int[(segmentCount * 4 + SideLineCount) * 2];
int cursor = 0;
- Ring.AddEdges(pattern, ref cursor, 0, segments);
- Ring.AddEdges(pattern, ref cursor, segments, segments);
- // The side lines join the rings at their quarter points, which is why segments must be a multiple of 4.
+ Ring.AddEdges(pattern, ref cursor, 0, segmentCount);
+ Ring.AddEdges(pattern, ref cursor, segmentCount, segmentCount);
+ // The side lines join the rings at their quarter points, which is why the segment count must be a multiple of 4.
for (int line = 0; line < SideLineCount; line++)
{
- int vertex = line * segments / SideLineCount;
+ int vertex = line * segmentCount / SideLineCount;
pattern[cursor++] = vertex;
- pattern[cursor++] = segments + vertex;
+ pattern[cursor++] = segmentCount + vertex;
}
- int capVertices = segments - 3;
- AddCapEdges(pattern, ref cursor, 0, segments * 2, segments);
- AddCapEdges(pattern, ref cursor, segments, segments * 2 + capVertices, segments);
+ int capVertices = segmentCount - 3;
+ AddCapEdges(pattern, ref cursor, 0, segmentCount * 2, segmentCount);
+ AddCapEdges(pattern, ref cursor, segmentCount, segmentCount * 2 + capVertices, segmentCount);
return pattern;
}
/// Adds the edges of a cap's two arcs, in the order writes their points.
- private static void AddCapEdges(int[] pattern, ref int cursor, int ring, int inside, int segments)
+ private static void AddCapEdges(int[] pattern, ref int cursor, int ring, int inside, int segmentCount)
{
- int half = segments / 2;
- int quarter = segments / 4;
+ int half = segmentCount / 2;
+ int quarter = segmentCount / 4;
int pole = inside + quarter - 1;
// From the ring's +Y point over the pole to its -Y point.
diff --git a/UnityPackage/Runtime/Shapes/Capsule/CapsuleFactory.cs b/UnityPackage/Runtime/Shapes/Capsule/CapsuleFactory.cs
new file mode 100644
index 0000000..f6116f2
--- /dev/null
+++ b/UnityPackage/Runtime/Shapes/Capsule/CapsuleFactory.cs
@@ -0,0 +1,97 @@
+using UnityEngine;
+
+namespace reromanlee.Wireframes
+{
+ /// Creates capsules in a .
+ public static class CapsuleFactory
+ {
+ ///
+ /// Creates a white capsule with spheres of radius 0.5 centered on the world origin and 1 unit along +Z.
+ ///
+ public static ICapsule CreateCapsule(this WireframeContainer container)
+ {
+ MeshProxy proxy = WireframeContainer.ProxyOf(container);
+ return proxy.Add(new Capsule(
+ null,
+ Vector3.zero,
+ Quaternion.identity,
+ ShapeDefaults.Size,
+ ShapeDefaults.Radius,
+ ShapeDefaults.Radius,
+ Ring.DefaultSegmentCount));
+ }
+
+ ///
+ /// Creates a white capsule around two spheres of the same radius, centered on two world positions, like the
+ /// capsule of Physics.CapsuleCast. Its spin around the axis keeps the capsule's +Y as close to world up
+ /// as it can.
+ ///
+ /// Number of straight pieces each ring is drawn with, a multiple of 4 from 4 to 1024.
+ public static ICapsule CreateCapsule(
+ this WireframeContainer container,
+ Vector3 centerA,
+ Vector3 centerB,
+ float radius,
+ int segmentCount = Ring.DefaultSegmentCount)
+ {
+ return container.CreateCapsule(centerA, centerB, radius, radius, segmentCount);
+ }
+
+ ///
+ /// Creates a white capsule around two spheres centered on two world positions, each with its own radius. Its
+ /// spin around the axis keeps the capsule's +Y as close to world up as it can.
+ ///
+ /// Number of straight pieces each ring is drawn with, a multiple of 4 from 4 to 1024.
+ public static ICapsule CreateCapsule(
+ this WireframeContainer container,
+ Vector3 centerA,
+ Vector3 centerB,
+ float radiusA,
+ float radiusB,
+ int segmentCount = Ring.DefaultSegmentCount)
+ {
+ MeshProxy proxy = WireframeContainer.ProxyOf(container);
+ Quaternion rotation = ShapeRotations.FromAxis(centerB - centerA, Vector3.up);
+ float length = Vector3.Distance(centerA, centerB);
+ return proxy.Add(new Capsule(null, centerA, rotation, length, radiusA, radiusB, segmentCount));
+ }
+
+ ///
+ /// Creates a white capsule that follows , around two spheres centered on positions in the
+ /// bone's local space. Its spin around the axis keeps the capsule's +Y as close to the bone's up as it can.
+ ///
+ /// Number of straight pieces each ring is drawn with, a multiple of 4 from 4 to 1024.
+ public static ICapsule CreateCapsule(
+ this WireframeContainer container,
+ Transform bone,
+ Vector3 localCenterA,
+ Vector3 localCenterB,
+ float radiusA,
+ float radiusB,
+ int segmentCount = Ring.DefaultSegmentCount)
+ {
+ MeshProxy proxy = WireframeContainer.ProxyOf(container);
+ Quaternion rotation = ShapeRotations.FromAxis(localCenterB - localCenterA, Vector3.up);
+ float length = Vector3.Distance(localCenterA, localCenterB);
+ return proxy.Add(new Capsule(bone, localCenterA, rotation, length, radiusA, radiusB, segmentCount));
+ }
+
+ ///
+ /// Creates a white capsule in world space whose first sphere is centered on and
+ /// whose second sphere is centered along the +Z axis of .
+ ///
+ /// Number of straight pieces each ring is drawn with, a multiple of 4 from 4 to 1024.
+ public static ICapsule CreateCapsule(
+ this WireframeContainer container,
+ Vector3 position,
+ Quaternion rotation,
+ float length,
+ float radiusA,
+ float radiusB,
+ int segmentCount = Ring.DefaultSegmentCount)
+ {
+ MeshProxy proxy = WireframeContainer.ProxyOf(container);
+ return proxy.Add(new Capsule(null, position, rotation, length, radiusA, radiusB, segmentCount));
+ }
+ }
+}
diff --git a/UnityPackage/Runtime/Shapes/Capsule/LineContainer.Capsule.cs.meta b/UnityPackage/Runtime/Shapes/Capsule/CapsuleFactory.cs.meta
similarity index 100%
rename from UnityPackage/Runtime/Shapes/Capsule/LineContainer.Capsule.cs.meta
rename to UnityPackage/Runtime/Shapes/Capsule/CapsuleFactory.cs.meta
diff --git a/UnityPackage/Runtime/Shapes/Capsule/ICapsule.cs b/UnityPackage/Runtime/Shapes/Capsule/ICapsule.cs
index dcfe9e6..53fa91f 100644
--- a/UnityPackage/Runtime/Shapes/Capsule/ICapsule.cs
+++ b/UnityPackage/Runtime/Shapes/Capsule/ICapsule.cs
@@ -16,6 +16,6 @@ public interface ICapsule : IAxialShape
///
/// Number of straight pieces each ring is drawn with, fixed at creation. Each arc over a cap is half of them.
///
- int Segments { get; }
+ int SegmentCount { get; }
}
}
diff --git a/UnityPackage/Runtime/Shapes/Capsule/ILineContainer.Capsule.cs b/UnityPackage/Runtime/Shapes/Capsule/ILineContainer.Capsule.cs
deleted file mode 100644
index 01d0200..0000000
--- a/UnityPackage/Runtime/Shapes/Capsule/ILineContainer.Capsule.cs
+++ /dev/null
@@ -1,54 +0,0 @@
-using UnityEngine;
-
-namespace reromanlee.Wireframes
-{
- public partial interface ILineContainer
- {
- ///
- /// Creates a white capsule with spheres of radius 0.5 centered on the world origin and 1 unit along +Z.
- ///
- ICapsule CreateCapsule();
-
- ///
- /// Creates a white capsule around two spheres of the same radius, centered on two world positions, like the
- /// capsule of Physics.CapsuleCast. Its spin around the axis keeps the capsule's +Y as close to world up
- /// as it can.
- ///
- /// Number of straight pieces each ring is drawn with, a positive multiple of 4.
- ICapsule CreateCapsule(Vector3 centerA, Vector3 centerB, float radius, int segments = Ring.DefaultSegments);
-
- ///
- /// Creates a white capsule around two spheres centered on two world positions, each with its own radius. Its
- /// spin around the axis keeps the capsule's +Y as close to world up as it can.
- ///
- /// Number of straight pieces each ring is drawn with, a positive multiple of 4.
- ICapsule CreateCapsule(
- Vector3 centerA, Vector3 centerB, float radiusA, float radiusB, int segments = Ring.DefaultSegments);
-
- ///
- /// Creates a white capsule that follows , around two spheres centered on positions in the
- /// bone's local space. Its spin around the axis keeps the capsule's +Y as close to the bone's up as it can.
- ///
- /// Number of straight pieces each ring is drawn with, a positive multiple of 4.
- ICapsule CreateCapsule(
- Transform bone,
- Vector3 localCenterA,
- Vector3 localCenterB,
- float radiusA,
- float radiusB,
- int segments = Ring.DefaultSegments);
-
- ///
- /// Creates a white capsule in world space whose first sphere is centered on and
- /// whose second sphere is centered along the +Z axis of .
- ///
- /// Number of straight pieces each ring is drawn with, a positive multiple of 4.
- ICapsule CreateCapsule(
- Vector3 position,
- Quaternion rotation,
- float length,
- float radiusA,
- float radiusB,
- int segments = Ring.DefaultSegments);
- }
-}
diff --git a/UnityPackage/Runtime/Shapes/Capsule/ILineContainer.Capsule.cs.meta b/UnityPackage/Runtime/Shapes/Capsule/ILineContainer.Capsule.cs.meta
deleted file mode 100644
index 3656b98..0000000
--- a/UnityPackage/Runtime/Shapes/Capsule/ILineContainer.Capsule.cs.meta
+++ /dev/null
@@ -1,2 +0,0 @@
-fileFormatVersion: 2
-guid: 95fb1496714de0868333a8c002f372ce
\ No newline at end of file
diff --git a/UnityPackage/Runtime/Shapes/Capsule/LineContainer.Capsule.cs b/UnityPackage/Runtime/Shapes/Capsule/LineContainer.Capsule.cs
deleted file mode 100644
index 72517d8..0000000
--- a/UnityPackage/Runtime/Shapes/Capsule/LineContainer.Capsule.cs
+++ /dev/null
@@ -1,50 +0,0 @@
-using UnityEngine;
-
-namespace reromanlee.Wireframes
-{
- public sealed partial class LineContainer
- {
- public ICapsule CreateCapsule()
- {
- return new Capsule(
- Proxy, null, Vector3.zero, Quaternion.identity, UnitSize, UnitRadius, UnitRadius, Ring.DefaultSegments);
- }
-
- public ICapsule CreateCapsule(Vector3 centerA, Vector3 centerB, float radius, int segments = Ring.DefaultSegments)
- {
- return CreateCapsule(centerA, centerB, radius, radius, segments);
- }
-
- public ICapsule CreateCapsule(
- Vector3 centerA, Vector3 centerB, float radiusA, float radiusB, int segments = Ring.DefaultSegments)
- {
- Quaternion rotation = ShapeRotations.FromAxis(centerB - centerA, Vector3.up);
- float length = Vector3.Distance(centerA, centerB);
- return new Capsule(Proxy, null, centerA, rotation, length, radiusA, radiusB, segments);
- }
-
- public ICapsule CreateCapsule(
- Transform bone,
- Vector3 localCenterA,
- Vector3 localCenterB,
- float radiusA,
- float radiusB,
- int segments = Ring.DefaultSegments)
- {
- Quaternion rotation = ShapeRotations.FromAxis(localCenterB - localCenterA, Vector3.up);
- float length = Vector3.Distance(localCenterA, localCenterB);
- return new Capsule(Proxy, bone, localCenterA, rotation, length, radiusA, radiusB, segments);
- }
-
- public ICapsule CreateCapsule(
- Vector3 position,
- Quaternion rotation,
- float length,
- float radiusA,
- float radiusB,
- int segments = Ring.DefaultSegments)
- {
- return new Capsule(Proxy, null, position, rotation, length, radiusA, radiusB, segments);
- }
- }
-}
diff --git a/UnityPackage/Runtime/Shapes/Circle/Circle.cs b/UnityPackage/Runtime/Shapes/Circle/Circle.cs
index 71dd604..96fe276 100644
--- a/UnityPackage/Runtime/Shapes/Circle/Circle.cs
+++ b/UnityPackage/Runtime/Shapes/Circle/Circle.cs
@@ -8,13 +8,12 @@ internal sealed class Circle : RigidShape, ICircle
private float _radius;
internal Circle(
- MeshProxy proxy,
Transform bone,
Vector3 localCenter,
Quaternion localRotation,
float radius,
- int segments)
- : base(proxy, Ring.CheckSegments(segments), Ring.Patterns.Get(segments), bone, localCenter, localRotation)
+ int segmentCount)
+ : base(Ring.CheckSegmentCount(segmentCount), Ring.Patterns.Get(segmentCount), bone, localCenter, localRotation)
{
_radius = radius;
}
@@ -23,22 +22,22 @@ public float Radius
{
get
{
- ThrowIfDisposed();
+ EnsureUsable();
return _radius;
}
set
{
- ThrowIfDisposed();
+ EnsureUsable();
_radius = value;
MarkDirty(DirtyFlags.Positions);
}
}
- public int Segments
+ public int SegmentCount
{
get
{
- ThrowIfDisposed();
+ EnsureUsable();
return VertexCount;
}
}
diff --git a/UnityPackage/Runtime/Shapes/Circle/CircleFactory.cs b/UnityPackage/Runtime/Shapes/Circle/CircleFactory.cs
new file mode 100644
index 0000000..05d40db
--- /dev/null
+++ b/UnityPackage/Runtime/Shapes/Circle/CircleFactory.cs
@@ -0,0 +1,67 @@
+using UnityEngine;
+
+namespace reromanlee.Wireframes
+{
+ /// Creates circles in a .
+ public static class CircleFactory
+ {
+ /// Creates a white circle of radius 0.5 around the world origin, lying flat in the XZ plane.
+ public static ICircle CreateCircle(this WireframeContainer container)
+ {
+ MeshProxy proxy = WireframeContainer.ProxyOf(container);
+ return proxy.Add(new Circle(
+ null, Vector3.zero, Quaternion.identity, ShapeDefaults.Radius, Ring.DefaultSegmentCount));
+ }
+
+ /// Creates a white circle around a world position, lying flat in the world's XZ plane.
+ /// Number of straight pieces the circle is drawn with, from 3 to 1024.
+ public static ICircle CreateCircle(
+ this WireframeContainer container, Vector3 center, float radius, int segmentCount = Ring.DefaultSegmentCount)
+ {
+ MeshProxy proxy = WireframeContainer.ProxyOf(container);
+ return proxy.Add(new Circle(null, center, Quaternion.identity, radius, segmentCount));
+ }
+
+ ///
+ /// Creates a white circle that follows , around a position in the bone's local space
+ /// and lying flat in the bone's XZ plane.
+ ///
+ /// Number of straight pieces the circle is drawn with, from 3 to 1024.
+ public static ICircle CreateCircle(
+ this WireframeContainer container,
+ Transform bone,
+ Vector3 localCenter,
+ float radius,
+ int segmentCount = Ring.DefaultSegmentCount)
+ {
+ MeshProxy proxy = WireframeContainer.ProxyOf(container);
+ return proxy.Add(new Circle(bone, localCenter, Quaternion.identity, radius, segmentCount));
+ }
+
+ /// Creates a white circle in world space, lying in the XZ plane of .
+ /// Number of straight pieces the circle is drawn with, from 3 to 1024.
+ public static ICircle CreateCircle(
+ this WireframeContainer container,
+ Vector3 center,
+ Quaternion rotation,
+ float radius,
+ int segmentCount = Ring.DefaultSegmentCount)
+ {
+ MeshProxy proxy = WireframeContainer.ProxyOf(container);
+ return proxy.Add(new Circle(null, center, rotation, radius, segmentCount));
+ }
+
+ /// Creates a white circle around a world position, facing .
+ /// Number of straight pieces the circle is drawn with, from 3 to 1024.
+ public static ICircle CreateCircle(
+ this WireframeContainer container,
+ Vector3 center,
+ Vector3 normal,
+ float radius,
+ int segmentCount = Ring.DefaultSegmentCount)
+ {
+ MeshProxy proxy = WireframeContainer.ProxyOf(container);
+ return proxy.Add(new Circle(null, center, ShapeRotations.FromNormal(normal), radius, segmentCount));
+ }
+ }
+}
diff --git a/UnityPackage/Runtime/Shapes/Circle/LineContainer.Circle.cs.meta b/UnityPackage/Runtime/Shapes/Circle/CircleFactory.cs.meta
similarity index 100%
rename from UnityPackage/Runtime/Shapes/Circle/LineContainer.Circle.cs.meta
rename to UnityPackage/Runtime/Shapes/Circle/CircleFactory.cs.meta
diff --git a/UnityPackage/Runtime/Shapes/Circle/ICircle.cs b/UnityPackage/Runtime/Shapes/Circle/ICircle.cs
index 280cf52..b32c096 100644
--- a/UnityPackage/Runtime/Shapes/Circle/ICircle.cs
+++ b/UnityPackage/Runtime/Shapes/Circle/ICircle.cs
@@ -10,6 +10,6 @@ public interface ICircle : IRigidShape
float Radius { get; set; }
/// Number of straight pieces the circle is drawn with, fixed at creation.
- int Segments { get; }
+ int SegmentCount { get; }
}
}
diff --git a/UnityPackage/Runtime/Shapes/Circle/ILineContainer.Circle.cs b/UnityPackage/Runtime/Shapes/Circle/ILineContainer.Circle.cs
deleted file mode 100644
index 8f083bb..0000000
--- a/UnityPackage/Runtime/Shapes/Circle/ILineContainer.Circle.cs
+++ /dev/null
@@ -1,29 +0,0 @@
-using UnityEngine;
-
-namespace reromanlee.Wireframes
-{
- public partial interface ILineContainer
- {
- /// Creates a white circle of radius 0.5 around the world origin, lying flat in the XZ plane.
- ICircle CreateCircle();
-
- /// Creates a white circle around a world position, lying flat in the world's XZ plane.
- /// Number of straight pieces the circle is drawn with, at least 3.
- ICircle CreateCircle(Vector3 center, float radius, int segments = Ring.DefaultSegments);
-
- ///
- /// Creates a white circle that follows , around a position in the bone's local space
- /// and lying flat in the bone's XZ plane.
- ///
- /// Number of straight pieces the circle is drawn with, at least 3.
- ICircle CreateCircle(Transform bone, Vector3 localCenter, float radius, int segments = Ring.DefaultSegments);
-
- /// Creates a white circle in world space, lying in the XZ plane of .
- /// Number of straight pieces the circle is drawn with, at least 3.
- ICircle CreateCircle(Vector3 center, Quaternion rotation, float radius, int segments = Ring.DefaultSegments);
-
- /// Creates a white circle around a world position, facing .
- /// Number of straight pieces the circle is drawn with, at least 3.
- ICircle CreateCircle(Vector3 center, Vector3 normal, float radius, int segments = Ring.DefaultSegments);
- }
-}
diff --git a/UnityPackage/Runtime/Shapes/Circle/ILineContainer.Circle.cs.meta b/UnityPackage/Runtime/Shapes/Circle/ILineContainer.Circle.cs.meta
deleted file mode 100644
index ead2564..0000000
--- a/UnityPackage/Runtime/Shapes/Circle/ILineContainer.Circle.cs.meta
+++ /dev/null
@@ -1,2 +0,0 @@
-fileFormatVersion: 2
-guid: 9fbd924ae6778a91e32b922e586b1674
\ No newline at end of file
diff --git a/UnityPackage/Runtime/Shapes/Circle/LineContainer.Circle.cs b/UnityPackage/Runtime/Shapes/Circle/LineContainer.Circle.cs
deleted file mode 100644
index e2461a4..0000000
--- a/UnityPackage/Runtime/Shapes/Circle/LineContainer.Circle.cs
+++ /dev/null
@@ -1,32 +0,0 @@
-using UnityEngine;
-
-namespace reromanlee.Wireframes
-{
- public sealed partial class LineContainer
- {
- public ICircle CreateCircle()
- {
- return new Circle(Proxy, null, Vector3.zero, Quaternion.identity, UnitRadius, Ring.DefaultSegments);
- }
-
- public ICircle CreateCircle(Vector3 center, float radius, int segments = Ring.DefaultSegments)
- {
- return new Circle(Proxy, null, center, Quaternion.identity, radius, segments);
- }
-
- public ICircle CreateCircle(Transform bone, Vector3 localCenter, float radius, int segments = Ring.DefaultSegments)
- {
- return new Circle(Proxy, bone, localCenter, Quaternion.identity, radius, segments);
- }
-
- public ICircle CreateCircle(Vector3 center, Quaternion rotation, float radius, int segments = Ring.DefaultSegments)
- {
- return new Circle(Proxy, null, center, rotation, radius, segments);
- }
-
- public ICircle CreateCircle(Vector3 center, Vector3 normal, float radius, int segments = Ring.DefaultSegments)
- {
- return new Circle(Proxy, null, center, ShapeRotations.FromNormal(normal), radius, segments);
- }
- }
-}
diff --git a/UnityPackage/Runtime/Shapes/Cone/Cone.cs b/UnityPackage/Runtime/Shapes/Cone/Cone.cs
index fa67a68..a2b4320 100644
--- a/UnityPackage/Runtime/Shapes/Cone/Cone.cs
+++ b/UnityPackage/Runtime/Shapes/Cone/Cone.cs
@@ -12,17 +12,15 @@ internal sealed class Cone : AxialShape, ICone
private float _radius;
internal Cone(
- MeshProxy proxy,
Transform bone,
Vector3 localPosition,
Quaternion localRotation,
float length,
float radius,
- int segments)
+ int segmentCount)
: base(
- proxy,
- Ring.CheckQuarterSegments(segments) + TipCount,
- Patterns.Get(segments),
+ Ring.CheckQuarterSegmentCount(segmentCount) + TipCount,
+ Patterns.Get(segmentCount),
bone,
localPosition,
localRotation,
@@ -35,36 +33,36 @@ public float Radius
{
get
{
- ThrowIfDisposed();
+ EnsureUsable();
return _radius;
}
set
{
- ThrowIfDisposed();
+ EnsureUsable();
_radius = value;
MarkDirty(DirtyFlags.Positions);
}
}
- public int Segments
+ public int SegmentCount
{
get
{
- ThrowIfDisposed();
+ EnsureUsable();
return VertexCount - TipCount;
}
}
protected override void WriteShape(Span positions, float length)
{
- int segments = positions.Length - TipCount;
+ int segmentCount = positions.Length - TipCount;
// The base ring starts on +Y and passes +X a quarter turn later; the tip comes last.
Ring.Write(
- positions.Slice(0, segments),
+ positions.Slice(0, segmentCount),
new Vector3(0f, 0f, length),
new Vector3(0f, _radius, 0f),
new Vector3(_radius, 0f, 0f));
- positions[segments] = Vector3.zero;
+ positions[segmentCount] = Vector3.zero;
}
protected override void ScaleCrossSection(float factor)
@@ -72,16 +70,16 @@ protected override void ScaleCrossSection(float factor)
_radius *= factor;
}
- private static int[] BuildEdges(int segments)
+ private static int[] BuildEdges(int segmentCount)
{
- int[] pattern = new int[(segments + SideLineCount) * 2];
+ int[] pattern = new int[(segmentCount + SideLineCount) * 2];
int cursor = 0;
- Ring.AddEdges(pattern, ref cursor, 0, segments);
- // The side lines meet the ring at its quarter points, which is why segments must be a multiple of 4.
+ Ring.AddEdges(pattern, ref cursor, 0, segmentCount);
+ // The side lines meet the ring at its quarter points, which is why the segment count must be a multiple of 4.
for (int line = 0; line < SideLineCount; line++)
{
- pattern[cursor++] = segments;
- pattern[cursor++] = line * segments / SideLineCount;
+ pattern[cursor++] = segmentCount;
+ pattern[cursor++] = line * segmentCount / SideLineCount;
}
return pattern;
}
diff --git a/UnityPackage/Runtime/Shapes/Cone/ConeFactory.cs b/UnityPackage/Runtime/Shapes/Cone/ConeFactory.cs
new file mode 100644
index 0000000..9fdfbac
--- /dev/null
+++ b/UnityPackage/Runtime/Shapes/Cone/ConeFactory.cs
@@ -0,0 +1,74 @@
+using UnityEngine;
+
+namespace reromanlee.Wireframes
+{
+ /// Creates cones in a .
+ public static class ConeFactory
+ {
+ /// Creates a white cone with its tip at the world origin and a base of radius 0.5, 1 unit along +Z.
+ public static ICone CreateCone(this WireframeContainer container)
+ {
+ MeshProxy proxy = WireframeContainer.ProxyOf(container);
+ return proxy.Add(new Cone(
+ null,
+ Vector3.zero,
+ Quaternion.identity,
+ ShapeDefaults.Size,
+ ShapeDefaults.Radius,
+ Ring.DefaultSegmentCount));
+ }
+
+ ///
+ /// Creates a white cone from a tip to the center of its base, both world positions. Its spin around the axis
+ /// keeps the cone's +Y as close to world up as it can.
+ ///
+ /// Number of straight pieces the base ring is drawn with, a multiple of 4 from 4 to 1024.
+ public static ICone CreateCone(
+ this WireframeContainer container,
+ Vector3 tip,
+ Vector3 baseCenter,
+ float radius,
+ int segmentCount = Ring.DefaultSegmentCount)
+ {
+ MeshProxy proxy = WireframeContainer.ProxyOf(container);
+ Quaternion rotation = ShapeRotations.FromAxis(baseCenter - tip, Vector3.up);
+ return proxy.Add(new Cone(null, tip, rotation, Vector3.Distance(tip, baseCenter), radius, segmentCount));
+ }
+
+ ///
+ /// Creates a white cone that follows , from a tip to the center of its base, both in the
+ /// bone's local space. Its spin around the axis keeps the cone's +Y as close to the bone's up as it can.
+ ///
+ /// Number of straight pieces the base ring is drawn with, a multiple of 4 from 4 to 1024.
+ public static ICone CreateCone(
+ this WireframeContainer container,
+ Transform bone,
+ Vector3 localTip,
+ Vector3 localBaseCenter,
+ float radius,
+ int segmentCount = Ring.DefaultSegmentCount)
+ {
+ MeshProxy proxy = WireframeContainer.ProxyOf(container);
+ Quaternion rotation = ShapeRotations.FromAxis(localBaseCenter - localTip, Vector3.up);
+ float length = Vector3.Distance(localTip, localBaseCenter);
+ return proxy.Add(new Cone(bone, localTip, rotation, length, radius, segmentCount));
+ }
+
+ ///
+ /// Creates a white cone in world space with its tip at and its base
+ /// along the +Z axis of .
+ ///
+ /// Number of straight pieces the base ring is drawn with, a multiple of 4 from 4 to 1024.
+ public static ICone CreateCone(
+ this WireframeContainer container,
+ Vector3 position,
+ Quaternion rotation,
+ float length,
+ float radius,
+ int segmentCount = Ring.DefaultSegmentCount)
+ {
+ MeshProxy proxy = WireframeContainer.ProxyOf(container);
+ return proxy.Add(new Cone(null, position, rotation, length, radius, segmentCount));
+ }
+ }
+}
diff --git a/UnityPackage/Runtime/Shapes/Cone/LineContainer.Cone.cs.meta b/UnityPackage/Runtime/Shapes/Cone/ConeFactory.cs.meta
similarity index 100%
rename from UnityPackage/Runtime/Shapes/Cone/LineContainer.Cone.cs.meta
rename to UnityPackage/Runtime/Shapes/Cone/ConeFactory.cs.meta
diff --git a/UnityPackage/Runtime/Shapes/Cone/ICone.cs b/UnityPackage/Runtime/Shapes/Cone/ICone.cs
index 53da520..0230ff7 100644
--- a/UnityPackage/Runtime/Shapes/Cone/ICone.cs
+++ b/UnityPackage/Runtime/Shapes/Cone/ICone.cs
@@ -10,6 +10,6 @@ public interface ICone : IAxialShape
float Radius { get; set; }
/// Number of straight pieces the base ring is drawn with, fixed at creation.
- int Segments { get; }
+ int SegmentCount { get; }
}
}
diff --git a/UnityPackage/Runtime/Shapes/Cone/ILineContainer.Cone.cs b/UnityPackage/Runtime/Shapes/Cone/ILineContainer.Cone.cs
deleted file mode 100644
index cb33597..0000000
--- a/UnityPackage/Runtime/Shapes/Cone/ILineContainer.Cone.cs
+++ /dev/null
@@ -1,33 +0,0 @@
-using UnityEngine;
-
-namespace reromanlee.Wireframes
-{
- public partial interface ILineContainer
- {
- /// Creates a white cone with its tip at the world origin and a base of radius 0.5, 1 unit along +Z.
- ICone CreateCone();
-
- ///
- /// Creates a white cone from a tip to the center of its base, both world positions. Its spin around the axis
- /// keeps the cone's +Y as close to world up as it can.
- ///
- /// Number of straight pieces the base ring is drawn with, a positive multiple of 4.
- ICone CreateCone(Vector3 tip, Vector3 baseCenter, float radius, int segments = Ring.DefaultSegments);
-
- ///
- /// Creates a white cone that follows , from a tip to the center of its base, both in the
- /// bone's local space. Its spin around the axis keeps the cone's +Y as close to the bone's up as it can.
- ///
- /// Number of straight pieces the base ring is drawn with, a positive multiple of 4.
- ICone CreateCone(
- Transform bone, Vector3 localTip, Vector3 localBaseCenter, float radius, int segments = Ring.DefaultSegments);
-
- ///
- /// Creates a white cone in world space with its tip at and its base
- /// along the +Z axis of .
- ///
- /// Number of straight pieces the base ring is drawn with, a positive multiple of 4.
- ICone CreateCone(
- Vector3 position, Quaternion rotation, float length, float radius, int segments = Ring.DefaultSegments);
- }
-}
diff --git a/UnityPackage/Runtime/Shapes/Cone/ILineContainer.Cone.cs.meta b/UnityPackage/Runtime/Shapes/Cone/ILineContainer.Cone.cs.meta
deleted file mode 100644
index 644018f..0000000
--- a/UnityPackage/Runtime/Shapes/Cone/ILineContainer.Cone.cs.meta
+++ /dev/null
@@ -1,2 +0,0 @@
-fileFormatVersion: 2
-guid: 1d518634536946723ec2ea5c8aeeb67b
\ No newline at end of file
diff --git a/UnityPackage/Runtime/Shapes/Cone/LineContainer.Cone.cs b/UnityPackage/Runtime/Shapes/Cone/LineContainer.Cone.cs
deleted file mode 100644
index 26f7fb8..0000000
--- a/UnityPackage/Runtime/Shapes/Cone/LineContainer.Cone.cs
+++ /dev/null
@@ -1,32 +0,0 @@
-using UnityEngine;
-
-namespace reromanlee.Wireframes
-{
- public sealed partial class LineContainer
- {
- public ICone CreateCone()
- {
- return new Cone(Proxy, null, Vector3.zero, Quaternion.identity, UnitSize, UnitRadius, Ring.DefaultSegments);
- }
-
- public ICone CreateCone(Vector3 tip, Vector3 baseCenter, float radius, int segments = Ring.DefaultSegments)
- {
- Quaternion rotation = ShapeRotations.FromAxis(baseCenter - tip, Vector3.up);
- return new Cone(Proxy, null, tip, rotation, Vector3.Distance(tip, baseCenter), radius, segments);
- }
-
- public ICone CreateCone(
- Transform bone, Vector3 localTip, Vector3 localBaseCenter, float radius, int segments = Ring.DefaultSegments)
- {
- Quaternion rotation = ShapeRotations.FromAxis(localBaseCenter - localTip, Vector3.up);
- float length = Vector3.Distance(localTip, localBaseCenter);
- return new Cone(Proxy, bone, localTip, rotation, length, radius, segments);
- }
-
- public ICone CreateCone(
- Vector3 position, Quaternion rotation, float length, float radius, int segments = Ring.DefaultSegments)
- {
- return new Cone(Proxy, null, position, rotation, length, radius, segments);
- }
- }
-}
diff --git a/UnityPackage/Runtime/Shapes/Cylinder/Cylinder.cs b/UnityPackage/Runtime/Shapes/Cylinder/Cylinder.cs
index 01f190d..9fd969b 100644
--- a/UnityPackage/Runtime/Shapes/Cylinder/Cylinder.cs
+++ b/UnityPackage/Runtime/Shapes/Cylinder/Cylinder.cs
@@ -12,17 +12,15 @@ internal sealed class Cylinder : AxialShape, ICylinder
private float _radius;
internal Cylinder(
- MeshProxy proxy,
Transform bone,
Vector3 localPosition,
Quaternion localRotation,
float length,
float radius,
- int segments)
+ int segmentCount)
: base(
- proxy,
- Ring.CheckQuarterSegments(segments) * RingCount,
- Patterns.Get(segments),
+ Ring.CheckQuarterSegmentCount(segmentCount) * RingCount,
+ Patterns.Get(segmentCount),
bone,
localPosition,
localRotation,
@@ -35,34 +33,34 @@ public float Radius
{
get
{
- ThrowIfDisposed();
+ EnsureUsable();
return _radius;
}
set
{
- ThrowIfDisposed();
+ EnsureUsable();
_radius = value;
MarkDirty(DirtyFlags.Positions);
}
}
- public int Segments
+ public int SegmentCount
{
get
{
- ThrowIfDisposed();
+ EnsureUsable();
return VertexCount / RingCount;
}
}
protected override void WriteShape(Span positions, float length)
{
- int segments = positions.Length / RingCount;
+ int segmentCount = positions.Length / RingCount;
// Each ring starts on +Y and passes +X a quarter turn later.
Vector3 up = new(0f, _radius, 0f);
Vector3 right = new(_radius, 0f, 0f);
- Ring.Write(positions.Slice(0, segments), Vector3.zero, up, right);
- Ring.Write(positions.Slice(segments, segments), new Vector3(0f, 0f, length), up, right);
+ Ring.Write(positions.Slice(0, segmentCount), Vector3.zero, up, right);
+ Ring.Write(positions.Slice(segmentCount, segmentCount), new Vector3(0f, 0f, length), up, right);
}
protected override void ScaleCrossSection(float factor)
@@ -70,18 +68,18 @@ protected override void ScaleCrossSection(float factor)
_radius *= factor;
}
- private static int[] BuildEdges(int segments)
+ private static int[] BuildEdges(int segmentCount)
{
- int[] pattern = new int[(segments * RingCount + SideLineCount) * 2];
+ int[] pattern = new int[(segmentCount * RingCount + SideLineCount) * 2];
int cursor = 0;
- Ring.AddEdges(pattern, ref cursor, 0, segments);
- Ring.AddEdges(pattern, ref cursor, segments, segments);
- // The side lines join the rings at their quarter points, which is why segments must be a multiple of 4.
+ Ring.AddEdges(pattern, ref cursor, 0, segmentCount);
+ Ring.AddEdges(pattern, ref cursor, segmentCount, segmentCount);
+ // The side lines join the rings at their quarter points, which is why the segment count must be a multiple of 4.
for (int line = 0; line < SideLineCount; line++)
{
- int vertex = line * segments / SideLineCount;
+ int vertex = line * segmentCount / SideLineCount;
pattern[cursor++] = vertex;
- pattern[cursor++] = segments + vertex;
+ pattern[cursor++] = segmentCount + vertex;
}
return pattern;
}
diff --git a/UnityPackage/Runtime/Shapes/Cylinder/CylinderFactory.cs b/UnityPackage/Runtime/Shapes/Cylinder/CylinderFactory.cs
new file mode 100644
index 0000000..b3fac88
--- /dev/null
+++ b/UnityPackage/Runtime/Shapes/Cylinder/CylinderFactory.cs
@@ -0,0 +1,74 @@
+using UnityEngine;
+
+namespace reromanlee.Wireframes
+{
+ /// Creates cylinders in a .
+ public static class CylinderFactory
+ {
+ /// Creates a white cylinder of radius 0.5 from the world origin, 1 unit along +Z.
+ public static ICylinder CreateCylinder(this WireframeContainer container)
+ {
+ MeshProxy proxy = WireframeContainer.ProxyOf(container);
+ return proxy.Add(new Cylinder(
+ null,
+ Vector3.zero,
+ Quaternion.identity,
+ ShapeDefaults.Size,
+ ShapeDefaults.Radius,
+ Ring.DefaultSegmentCount));
+ }
+
+ ///
+ /// Creates a white cylinder between two world positions. Its spin around the axis keeps the cylinder's +Y as
+ /// close to world up as it can.
+ ///
+ /// Number of straight pieces each ring is drawn with, a multiple of 4 from 4 to 1024.
+ public static ICylinder CreateCylinder(
+ this WireframeContainer container,
+ Vector3 endA,
+ Vector3 endB,
+ float radius,
+ int segmentCount = Ring.DefaultSegmentCount)
+ {
+ MeshProxy proxy = WireframeContainer.ProxyOf(container);
+ Quaternion rotation = ShapeRotations.FromAxis(endB - endA, Vector3.up);
+ return proxy.Add(new Cylinder(null, endA, rotation, Vector3.Distance(endA, endB), radius, segmentCount));
+ }
+
+ ///
+ /// Creates a white cylinder that follows , between two positions in the bone's local
+ /// space. Its spin around the axis keeps the cylinder's +Y as close to the bone's up as it can.
+ ///
+ /// Number of straight pieces each ring is drawn with, a multiple of 4 from 4 to 1024.
+ public static ICylinder CreateCylinder(
+ this WireframeContainer container,
+ Transform bone,
+ Vector3 localEndA,
+ Vector3 localEndB,
+ float radius,
+ int segmentCount = Ring.DefaultSegmentCount)
+ {
+ MeshProxy proxy = WireframeContainer.ProxyOf(container);
+ Quaternion rotation = ShapeRotations.FromAxis(localEndB - localEndA, Vector3.up);
+ float length = Vector3.Distance(localEndA, localEndB);
+ return proxy.Add(new Cylinder(bone, localEndA, rotation, length, radius, segmentCount));
+ }
+
+ ///
+ /// Creates a white cylinder in world space that starts at and runs
+ /// along the +Z axis of .
+ ///
+ /// Number of straight pieces each ring is drawn with, a multiple of 4 from 4 to 1024.
+ public static ICylinder CreateCylinder(
+ this WireframeContainer container,
+ Vector3 position,
+ Quaternion rotation,
+ float length,
+ float radius,
+ int segmentCount = Ring.DefaultSegmentCount)
+ {
+ MeshProxy proxy = WireframeContainer.ProxyOf(container);
+ return proxy.Add(new Cylinder(null, position, rotation, length, radius, segmentCount));
+ }
+ }
+}
diff --git a/UnityPackage/Runtime/Shapes/Cylinder/LineContainer.Cylinder.cs.meta b/UnityPackage/Runtime/Shapes/Cylinder/CylinderFactory.cs.meta
similarity index 100%
rename from UnityPackage/Runtime/Shapes/Cylinder/LineContainer.Cylinder.cs.meta
rename to UnityPackage/Runtime/Shapes/Cylinder/CylinderFactory.cs.meta
diff --git a/UnityPackage/Runtime/Shapes/Cylinder/ICylinder.cs b/UnityPackage/Runtime/Shapes/Cylinder/ICylinder.cs
index 9d62ef6..389b8af 100644
--- a/UnityPackage/Runtime/Shapes/Cylinder/ICylinder.cs
+++ b/UnityPackage/Runtime/Shapes/Cylinder/ICylinder.cs
@@ -9,6 +9,6 @@ public interface ICylinder : IAxialShape
float Radius { get; set; }
/// Number of straight pieces each ring is drawn with, fixed at creation.
- int Segments { get; }
+ int SegmentCount { get; }
}
}
diff --git a/UnityPackage/Runtime/Shapes/Cylinder/ILineContainer.Cylinder.cs b/UnityPackage/Runtime/Shapes/Cylinder/ILineContainer.Cylinder.cs
deleted file mode 100644
index 2e4d85b..0000000
--- a/UnityPackage/Runtime/Shapes/Cylinder/ILineContainer.Cylinder.cs
+++ /dev/null
@@ -1,33 +0,0 @@
-using UnityEngine;
-
-namespace reromanlee.Wireframes
-{
- public partial interface ILineContainer
- {
- /// Creates a white cylinder of radius 0.5 from the world origin, 1 unit along +Z.
- ICylinder CreateCylinder();
-
- ///
- /// Creates a white cylinder between two world positions. Its spin around the axis keeps the cylinder's +Y as
- /// close to world up as it can.
- ///
- /// Number of straight pieces each ring is drawn with, a positive multiple of 4.
- ICylinder CreateCylinder(Vector3 endA, Vector3 endB, float radius, int segments = Ring.DefaultSegments);
-
- ///
- /// Creates a white cylinder that follows , between two positions in the bone's local
- /// space. Its spin around the axis keeps the cylinder's +Y as close to the bone's up as it can.
- ///
- /// Number of straight pieces each ring is drawn with, a positive multiple of 4.
- ICylinder CreateCylinder(
- Transform bone, Vector3 localEndA, Vector3 localEndB, float radius, int segments = Ring.DefaultSegments);
-
- ///
- /// Creates a white cylinder in world space that starts at and runs
- /// along the +Z axis of .
- ///
- /// Number of straight pieces each ring is drawn with, a positive multiple of 4.
- ICylinder CreateCylinder(
- Vector3 position, Quaternion rotation, float length, float radius, int segments = Ring.DefaultSegments);
- }
-}
diff --git a/UnityPackage/Runtime/Shapes/Cylinder/ILineContainer.Cylinder.cs.meta b/UnityPackage/Runtime/Shapes/Cylinder/ILineContainer.Cylinder.cs.meta
deleted file mode 100644
index 8e7edd5..0000000
--- a/UnityPackage/Runtime/Shapes/Cylinder/ILineContainer.Cylinder.cs.meta
+++ /dev/null
@@ -1,2 +0,0 @@
-fileFormatVersion: 2
-guid: 6fc91468e22e591f8418e1e1464de003
\ No newline at end of file
diff --git a/UnityPackage/Runtime/Shapes/Cylinder/LineContainer.Cylinder.cs b/UnityPackage/Runtime/Shapes/Cylinder/LineContainer.Cylinder.cs
deleted file mode 100644
index fca950c..0000000
--- a/UnityPackage/Runtime/Shapes/Cylinder/LineContainer.Cylinder.cs
+++ /dev/null
@@ -1,31 +0,0 @@
-using UnityEngine;
-
-namespace reromanlee.Wireframes
-{
- public sealed partial class LineContainer
- {
- public ICylinder CreateCylinder()
- {
- return new Cylinder(Proxy, null, Vector3.zero, Quaternion.identity, UnitSize, UnitRadius, Ring.DefaultSegments);
- }
-
- public ICylinder CreateCylinder(Vector3 endA, Vector3 endB, float radius, int segments = Ring.DefaultSegments)
- {
- Quaternion rotation = ShapeRotations.FromAxis(endB - endA, Vector3.up);
- return new Cylinder(Proxy, null, endA, rotation, Vector3.Distance(endA, endB), radius, segments);
- }
-
- public ICylinder CreateCylinder(
- Transform bone, Vector3 localEndA, Vector3 localEndB, float radius, int segments = Ring.DefaultSegments)
- {
- Quaternion rotation = ShapeRotations.FromAxis(localEndB - localEndA, Vector3.up);
- return new Cylinder(Proxy, bone, localEndA, rotation, Vector3.Distance(localEndA, localEndB), radius, segments);
- }
-
- public ICylinder CreateCylinder(
- Vector3 position, Quaternion rotation, float length, float radius, int segments = Ring.DefaultSegments)
- {
- return new Cylinder(Proxy, null, position, rotation, length, radius, segments);
- }
- }
-}
diff --git a/UnityPackage/Runtime/Shapes/Ellipse/Ellipse.cs b/UnityPackage/Runtime/Shapes/Ellipse/Ellipse.cs
index d5613b2..0df1070 100644
--- a/UnityPackage/Runtime/Shapes/Ellipse/Ellipse.cs
+++ b/UnityPackage/Runtime/Shapes/Ellipse/Ellipse.cs
@@ -8,13 +8,12 @@ internal sealed class Ellipse : RigidShape, IEllipse
private Vector2 _radii;
internal Ellipse(
- MeshProxy proxy,
Transform bone,
Vector3 localCenter,
Quaternion localRotation,
Vector2 radii,
- int segments)
- : base(proxy, Ring.CheckSegments(segments), Ring.Patterns.Get(segments), bone, localCenter, localRotation)
+ int segmentCount)
+ : base(Ring.CheckSegmentCount(segmentCount), Ring.Patterns.Get(segmentCount), bone, localCenter, localRotation)
{
_radii = radii;
}
@@ -23,22 +22,22 @@ public Vector2 Radii
{
get
{
- ThrowIfDisposed();
+ EnsureUsable();
return _radii;
}
set
{
- ThrowIfDisposed();
+ EnsureUsable();
_radii = value;
MarkDirty(DirtyFlags.Positions);
}
}
- public int Segments
+ public int SegmentCount
{
get
{
- ThrowIfDisposed();
+ EnsureUsable();
return VertexCount;
}
}
diff --git a/UnityPackage/Runtime/Shapes/Ellipse/EllipseFactory.cs b/UnityPackage/Runtime/Shapes/Ellipse/EllipseFactory.cs
new file mode 100644
index 0000000..9835980
--- /dev/null
+++ b/UnityPackage/Runtime/Shapes/Ellipse/EllipseFactory.cs
@@ -0,0 +1,86 @@
+using UnityEngine;
+
+namespace reromanlee.Wireframes
+{
+ /// Creates ellipses in a .
+ public static class EllipseFactory
+ {
+ ///
+ /// Creates a white ellipse around the world origin, lying flat in the XZ plane, 1 unit long along Z and
+ /// 0.5 wide along X.
+ ///
+ public static IEllipse CreateEllipse(this WireframeContainer container)
+ {
+ MeshProxy proxy = WireframeContainer.ProxyOf(container);
+ Vector2 radii = new(ShapeDefaults.Radius * 0.5f, ShapeDefaults.Radius);
+ return proxy.Add(new Ellipse(null, Vector3.zero, Quaternion.identity, radii, Ring.DefaultSegmentCount));
+ }
+
+ ///
+ /// Creates a white ellipse whose long axis runs between two world positions, with as
+ /// its other semi-axis. It lies as flat as it can: its normal stays as close to world up as the tips allow.
+ ///
+ /// Number of straight pieces the ellipse is drawn with, from 3 to 1024.
+ public static IEllipse CreateEllipse(
+ this WireframeContainer container,
+ Vector3 tipA,
+ Vector3 tipB,
+ float radius,
+ int segmentCount = Ring.DefaultSegmentCount)
+ {
+ return container.CreateEllipse(tipA, tipB, radius, Vector3.up, segmentCount);
+ }
+
+ ///
+ /// Creates a white ellipse whose long axis runs between two world positions, with as
+ /// its other semi-axis, facing as close to as the tips allow.
+ ///
+ /// Number of straight pieces the ellipse is drawn with, from 3 to 1024.
+ public static IEllipse CreateEllipse(
+ this WireframeContainer container,
+ Vector3 tipA,
+ Vector3 tipB,
+ float radius,
+ Vector3 normal,
+ int segmentCount = Ring.DefaultSegmentCount)
+ {
+ MeshProxy proxy = WireframeContainer.ProxyOf(container);
+ Quaternion rotation = ShapeRotations.FromAxis(tipB - tipA, normal);
+ Vector2 radii = new(radius, Vector3.Distance(tipA, tipB) * 0.5f);
+ return proxy.Add(new Ellipse(null, (tipA + tipB) * 0.5f, rotation, radii, segmentCount));
+ }
+
+ ///
+ /// Creates a white ellipse that follows , whose long axis runs between two positions in
+ /// the bone's local space. It lies as flat in the bone's XZ plane as the tips allow.
+ ///
+ /// Number of straight pieces the ellipse is drawn with, from 3 to 1024.
+ public static IEllipse CreateEllipse(
+ this WireframeContainer container,
+ Transform bone,
+ Vector3 localTipA,
+ Vector3 localTipB,
+ float radius,
+ int segmentCount = Ring.DefaultSegmentCount)
+ {
+ MeshProxy proxy = WireframeContainer.ProxyOf(container);
+ Quaternion rotation = ShapeRotations.FromAxis(localTipB - localTipA, Vector3.up);
+ Vector2 radii = new(radius, Vector3.Distance(localTipA, localTipB) * 0.5f);
+ return proxy.Add(new Ellipse(bone, (localTipA + localTipB) * 0.5f, rotation, radii, segmentCount));
+ }
+
+ /// Creates a white ellipse in world space, lying in the XZ plane of .
+ /// Semi-axes: x along the rotation's X axis and y along its Z axis.
+ /// Number of straight pieces the ellipse is drawn with, from 3 to 1024.
+ public static IEllipse CreateEllipse(
+ this WireframeContainer container,
+ Vector3 center,
+ Quaternion rotation,
+ Vector2 radii,
+ int segmentCount = Ring.DefaultSegmentCount)
+ {
+ MeshProxy proxy = WireframeContainer.ProxyOf(container);
+ return proxy.Add(new Ellipse(null, center, rotation, radii, segmentCount));
+ }
+ }
+}
diff --git a/UnityPackage/Runtime/Shapes/Ellipse/LineContainer.Ellipse.cs.meta b/UnityPackage/Runtime/Shapes/Ellipse/EllipseFactory.cs.meta
similarity index 100%
rename from UnityPackage/Runtime/Shapes/Ellipse/LineContainer.Ellipse.cs.meta
rename to UnityPackage/Runtime/Shapes/Ellipse/EllipseFactory.cs.meta
diff --git a/UnityPackage/Runtime/Shapes/Ellipse/IEllipse.cs b/UnityPackage/Runtime/Shapes/Ellipse/IEllipse.cs
index ee81b26..8c336f7 100644
--- a/UnityPackage/Runtime/Shapes/Ellipse/IEllipse.cs
+++ b/UnityPackage/Runtime/Shapes/Ellipse/IEllipse.cs
@@ -9,6 +9,6 @@ public interface IEllipse : IRigidShape
Vector2 Radii { get; set; }
/// Number of straight pieces the ellipse is drawn with, fixed at creation.
- int Segments { get; }
+ int SegmentCount { get; }
}
}
diff --git a/UnityPackage/Runtime/Shapes/Ellipse/ILineContainer.Ellipse.cs b/UnityPackage/Runtime/Shapes/Ellipse/ILineContainer.Ellipse.cs
deleted file mode 100644
index 8c2c551..0000000
--- a/UnityPackage/Runtime/Shapes/Ellipse/ILineContainer.Ellipse.cs
+++ /dev/null
@@ -1,41 +0,0 @@
-using UnityEngine;
-
-namespace reromanlee.Wireframes
-{
- public partial interface ILineContainer
- {
- ///
- /// Creates a white ellipse around the world origin, lying flat in the XZ plane, 1 unit long along Z and
- /// 0.5 wide along X.
- ///
- IEllipse CreateEllipse();
-
- ///
- /// Creates a white ellipse whose long axis runs between two world positions, with as
- /// its other semi-axis. It lies as flat as it can: its normal stays as close to world up as the tips allow.
- ///
- /// Number of straight pieces the ellipse is drawn with, at least 3.
- IEllipse CreateEllipse(Vector3 tipA, Vector3 tipB, float radius, int segments = Ring.DefaultSegments);
-
- ///
- /// Creates a white ellipse whose long axis runs between two world positions, with as
- /// its other semi-axis, facing as close to as the tips allow.
- ///
- /// Number of straight pieces the ellipse is drawn with, at least 3.
- IEllipse CreateEllipse(
- Vector3 tipA, Vector3 tipB, float radius, Vector3 normal, int segments = Ring.DefaultSegments);
-
- ///
- /// Creates a white ellipse that follows , whose long axis runs between two positions in
- /// the bone's local space. It lies as flat in the bone's XZ plane as the tips allow.
- ///
- /// Number of straight pieces the ellipse is drawn with, at least 3.
- IEllipse CreateEllipse(
- Transform bone, Vector3 localTipA, Vector3 localTipB, float radius, int segments = Ring.DefaultSegments);
-
- /// Creates a white ellipse in world space, lying in the XZ plane of .
- /// Semi-axes: x along the rotation's X axis and y along its Z axis.
- /// Number of straight pieces the ellipse is drawn with, at least 3.
- IEllipse CreateEllipse(Vector3 center, Quaternion rotation, Vector2 radii, int segments = Ring.DefaultSegments);
- }
-}
diff --git a/UnityPackage/Runtime/Shapes/Ellipse/ILineContainer.Ellipse.cs.meta b/UnityPackage/Runtime/Shapes/Ellipse/ILineContainer.Ellipse.cs.meta
deleted file mode 100644
index bd0808f..0000000
--- a/UnityPackage/Runtime/Shapes/Ellipse/ILineContainer.Ellipse.cs.meta
+++ /dev/null
@@ -1,2 +0,0 @@
-fileFormatVersion: 2
-guid: 5bc9d54e1da3471123cfc10067fd2021
\ No newline at end of file
diff --git a/UnityPackage/Runtime/Shapes/Ellipse/LineContainer.Ellipse.cs b/UnityPackage/Runtime/Shapes/Ellipse/LineContainer.Ellipse.cs
deleted file mode 100644
index 0e78362..0000000
--- a/UnityPackage/Runtime/Shapes/Ellipse/LineContainer.Ellipse.cs
+++ /dev/null
@@ -1,39 +0,0 @@
-using UnityEngine;
-
-namespace reromanlee.Wireframes
-{
- public sealed partial class LineContainer
- {
- public IEllipse CreateEllipse()
- {
- Vector2 radii = new(UnitRadius * 0.5f, UnitRadius);
- return new Ellipse(Proxy, null, Vector3.zero, Quaternion.identity, radii, Ring.DefaultSegments);
- }
-
- public IEllipse CreateEllipse(Vector3 tipA, Vector3 tipB, float radius, int segments = Ring.DefaultSegments)
- {
- return CreateEllipse(tipA, tipB, radius, Vector3.up, segments);
- }
-
- public IEllipse CreateEllipse(
- Vector3 tipA, Vector3 tipB, float radius, Vector3 normal, int segments = Ring.DefaultSegments)
- {
- Quaternion rotation = ShapeRotations.FromAxis(tipB - tipA, normal);
- Vector2 radii = new(radius, Vector3.Distance(tipA, tipB) * 0.5f);
- return new Ellipse(Proxy, null, (tipA + tipB) * 0.5f, rotation, radii, segments);
- }
-
- public IEllipse CreateEllipse(
- Transform bone, Vector3 localTipA, Vector3 localTipB, float radius, int segments = Ring.DefaultSegments)
- {
- Quaternion rotation = ShapeRotations.FromAxis(localTipB - localTipA, Vector3.up);
- Vector2 radii = new(radius, Vector3.Distance(localTipA, localTipB) * 0.5f);
- return new Ellipse(Proxy, bone, (localTipA + localTipB) * 0.5f, rotation, radii, segments);
- }
-
- public IEllipse CreateEllipse(Vector3 center, Quaternion rotation, Vector2 radii, int segments = Ring.DefaultSegments)
- {
- return new Ellipse(Proxy, null, center, rotation, radii, segments);
- }
- }
-}
diff --git a/UnityPackage/Runtime/Shapes/Ellipsoid/Ellipsoid.cs b/UnityPackage/Runtime/Shapes/Ellipsoid/Ellipsoid.cs
index d83fe28..443cb73 100644
--- a/UnityPackage/Runtime/Shapes/Ellipsoid/Ellipsoid.cs
+++ b/UnityPackage/Runtime/Shapes/Ellipsoid/Ellipsoid.cs
@@ -8,16 +8,14 @@ internal sealed class Ellipsoid : RigidShape, IEllipsoid
private Vector3 _radii;
internal Ellipsoid(
- MeshProxy proxy,
Transform bone,
Vector3 localCenter,
Quaternion localRotation,
Vector3 radii,
- int segments)
+ int segmentCount)
: base(
- proxy,
- Ring.CheckSegments(segments) * AxisRings.RingCount,
- AxisRings.Patterns.Get(segments),
+ Ring.CheckSegmentCount(segmentCount) * AxisRings.RingCount,
+ AxisRings.Patterns.Get(segmentCount),
bone,
localCenter,
localRotation)
@@ -29,22 +27,22 @@ public Vector3 Radii
{
get
{
- ThrowIfDisposed();
+ EnsureUsable();
return _radii;
}
set
{
- ThrowIfDisposed();
+ EnsureUsable();
_radii = value;
MarkDirty(DirtyFlags.Positions);
}
}
- public int Segments
+ public int SegmentCount
{
get
{
- ThrowIfDisposed();
+ EnsureUsable();
return VertexCount / AxisRings.RingCount;
}
}
diff --git a/UnityPackage/Runtime/Shapes/Ellipsoid/EllipsoidFactory.cs b/UnityPackage/Runtime/Shapes/Ellipsoid/EllipsoidFactory.cs
new file mode 100644
index 0000000..1a94d89
--- /dev/null
+++ b/UnityPackage/Runtime/Shapes/Ellipsoid/EllipsoidFactory.cs
@@ -0,0 +1,69 @@
+using UnityEngine;
+
+namespace reromanlee.Wireframes
+{
+ /// Creates ellipsoids in a .
+ public static class EllipsoidFactory
+ {
+ ///
+ /// Creates a white ellipsoid around the world origin, 1 unit long along Z and 0.5 wide along X and Y.
+ ///
+ public static IEllipsoid CreateEllipsoid(this WireframeContainer container)
+ {
+ MeshProxy proxy = WireframeContainer.ProxyOf(container);
+ Vector3 radii = new(ShapeDefaults.Radius * 0.5f, ShapeDefaults.Radius * 0.5f, ShapeDefaults.Radius);
+ return proxy.Add(new Ellipsoid(null, Vector3.zero, Quaternion.identity, radii, Ring.DefaultSegmentCount));
+ }
+
+ ///
+ /// Creates a white ellipsoid whose long axis runs between two world positions, with
+ /// as its other two semi-axes. Its +Y stays as close to world up as the tips allow.
+ ///
+ /// Number of straight pieces each ellipse is drawn with, from 3 to 1024.
+ public static IEllipsoid CreateEllipsoid(
+ this WireframeContainer container,
+ Vector3 tipA,
+ Vector3 tipB,
+ float radius,
+ int segmentCount = Ring.DefaultSegmentCount)
+ {
+ MeshProxy proxy = WireframeContainer.ProxyOf(container);
+ Quaternion rotation = ShapeRotations.FromAxis(tipB - tipA, Vector3.up);
+ Vector3 radii = new(radius, radius, Vector3.Distance(tipA, tipB) * 0.5f);
+ return proxy.Add(new Ellipsoid(null, (tipA + tipB) * 0.5f, rotation, radii, segmentCount));
+ }
+
+ ///
+ /// Creates a white ellipsoid that follows , whose long axis runs between two positions in
+ /// the bone's local space, with as its other two semi-axes.
+ ///
+ /// Number of straight pieces each ellipse is drawn with, from 3 to 1024.
+ public static IEllipsoid CreateEllipsoid(
+ this WireframeContainer container,
+ Transform bone,
+ Vector3 localTipA,
+ Vector3 localTipB,
+ float radius,
+ int segmentCount = Ring.DefaultSegmentCount)
+ {
+ MeshProxy proxy = WireframeContainer.ProxyOf(container);
+ Quaternion rotation = ShapeRotations.FromAxis(localTipB - localTipA, Vector3.up);
+ Vector3 radii = new(radius, radius, Vector3.Distance(localTipA, localTipB) * 0.5f);
+ return proxy.Add(new Ellipsoid(bone, (localTipA + localTipB) * 0.5f, rotation, radii, segmentCount));
+ }
+
+ /// Creates a white ellipsoid in world space, turned by .
+ /// Semi-axes along the rotation's X, Y and Z axes.
+ /// Number of straight pieces each ellipse is drawn with, from 3 to 1024.
+ public static IEllipsoid CreateEllipsoid(
+ this WireframeContainer container,
+ Vector3 center,
+ Quaternion rotation,
+ Vector3 radii,
+ int segmentCount = Ring.DefaultSegmentCount)
+ {
+ MeshProxy proxy = WireframeContainer.ProxyOf(container);
+ return proxy.Add(new Ellipsoid(null, center, rotation, radii, segmentCount));
+ }
+ }
+}
diff --git a/UnityPackage/Runtime/Shapes/Ellipsoid/LineContainer.Ellipsoid.cs.meta b/UnityPackage/Runtime/Shapes/Ellipsoid/EllipsoidFactory.cs.meta
similarity index 100%
rename from UnityPackage/Runtime/Shapes/Ellipsoid/LineContainer.Ellipsoid.cs.meta
rename to UnityPackage/Runtime/Shapes/Ellipsoid/EllipsoidFactory.cs.meta
diff --git a/UnityPackage/Runtime/Shapes/Ellipsoid/IEllipsoid.cs b/UnityPackage/Runtime/Shapes/Ellipsoid/IEllipsoid.cs
index d359785..469e84b 100644
--- a/UnityPackage/Runtime/Shapes/Ellipsoid/IEllipsoid.cs
+++ b/UnityPackage/Runtime/Shapes/Ellipsoid/IEllipsoid.cs
@@ -12,6 +12,6 @@ public interface IEllipsoid : IRigidShape
Vector3 Radii { get; set; }
/// Number of straight pieces each ellipse is drawn with, fixed at creation.
- int Segments { get; }
+ int SegmentCount { get; }
}
}
diff --git a/UnityPackage/Runtime/Shapes/Ellipsoid/ILineContainer.Ellipsoid.cs b/UnityPackage/Runtime/Shapes/Ellipsoid/ILineContainer.Ellipsoid.cs
deleted file mode 100644
index af637c4..0000000
--- a/UnityPackage/Runtime/Shapes/Ellipsoid/ILineContainer.Ellipsoid.cs
+++ /dev/null
@@ -1,33 +0,0 @@
-using UnityEngine;
-
-namespace reromanlee.Wireframes
-{
- public partial interface ILineContainer
- {
- ///
- /// Creates a white ellipsoid around the world origin, 1 unit long along Z and 0.5 wide along X and Y.
- ///
- IEllipsoid CreateEllipsoid();
-
- ///
- /// Creates a white ellipsoid whose long axis runs between two world positions, with
- /// as its other two semi-axes. Its +Y stays as close to world up as the tips allow.
- ///
- /// Number of straight pieces each ellipse is drawn with, at least 3.
- IEllipsoid CreateEllipsoid(Vector3 tipA, Vector3 tipB, float radius, int segments = Ring.DefaultSegments);
-
- ///
- /// Creates a white ellipsoid that follows , whose long axis runs between two positions in
- /// the bone's local space, with as its other two semi-axes.
- ///
- /// Number of straight pieces each ellipse is drawn with, at least 3.
- IEllipsoid CreateEllipsoid(
- Transform bone, Vector3 localTipA, Vector3 localTipB, float radius, int segments = Ring.DefaultSegments);
-
- /// Creates a white ellipsoid in world space, turned by .
- /// Semi-axes along the rotation's X, Y and Z axes.
- /// Number of straight pieces each ellipse is drawn with, at least 3.
- IEllipsoid CreateEllipsoid(
- Vector3 center, Quaternion rotation, Vector3 radii, int segments = Ring.DefaultSegments);
- }
-}
diff --git a/UnityPackage/Runtime/Shapes/Ellipsoid/ILineContainer.Ellipsoid.cs.meta b/UnityPackage/Runtime/Shapes/Ellipsoid/ILineContainer.Ellipsoid.cs.meta
deleted file mode 100644
index e4a82bb..0000000
--- a/UnityPackage/Runtime/Shapes/Ellipsoid/ILineContainer.Ellipsoid.cs.meta
+++ /dev/null
@@ -1,2 +0,0 @@
-fileFormatVersion: 2
-guid: 8447c528bb4f95bc415e723288e921f2
\ No newline at end of file
diff --git a/UnityPackage/Runtime/Shapes/Ellipsoid/LineContainer.Ellipsoid.cs b/UnityPackage/Runtime/Shapes/Ellipsoid/LineContainer.Ellipsoid.cs
deleted file mode 100644
index 976b3ad..0000000
--- a/UnityPackage/Runtime/Shapes/Ellipsoid/LineContainer.Ellipsoid.cs
+++ /dev/null
@@ -1,34 +0,0 @@
-using UnityEngine;
-
-namespace reromanlee.Wireframes
-{
- public sealed partial class LineContainer
- {
- public IEllipsoid CreateEllipsoid()
- {
- Vector3 radii = new(UnitRadius * 0.5f, UnitRadius * 0.5f, UnitRadius);
- return new Ellipsoid(Proxy, null, Vector3.zero, Quaternion.identity, radii, Ring.DefaultSegments);
- }
-
- public IEllipsoid CreateEllipsoid(Vector3 tipA, Vector3 tipB, float radius, int segments = Ring.DefaultSegments)
- {
- Quaternion rotation = ShapeRotations.FromAxis(tipB - tipA, Vector3.up);
- Vector3 radii = new(radius, radius, Vector3.Distance(tipA, tipB) * 0.5f);
- return new Ellipsoid(Proxy, null, (tipA + tipB) * 0.5f, rotation, radii, segments);
- }
-
- public IEllipsoid CreateEllipsoid(
- Transform bone, Vector3 localTipA, Vector3 localTipB, float radius, int segments = Ring.DefaultSegments)
- {
- Quaternion rotation = ShapeRotations.FromAxis(localTipB - localTipA, Vector3.up);
- Vector3 radii = new(radius, radius, Vector3.Distance(localTipA, localTipB) * 0.5f);
- return new Ellipsoid(Proxy, bone, (localTipA + localTipB) * 0.5f, rotation, radii, segments);
- }
-
- public IEllipsoid CreateEllipsoid(
- Vector3 center, Quaternion rotation, Vector3 radii, int segments = Ring.DefaultSegments)
- {
- return new Ellipsoid(Proxy, null, center, rotation, radii, segments);
- }
- }
-}
diff --git a/UnityPackage/Runtime/Shapes/Frustum/Frustum.cs b/UnityPackage/Runtime/Shapes/Frustum/Frustum.cs
index 7283968..03110bc 100644
--- a/UnityPackage/Runtime/Shapes/Frustum/Frustum.cs
+++ b/UnityPackage/Runtime/Shapes/Frustum/Frustum.cs
@@ -12,15 +12,14 @@ internal sealed class Frustum : AxialShape, IFrustum
private float _radiusB;
internal Frustum(
- MeshProxy proxy,
Transform bone,
Vector3 localPosition,
Quaternion localRotation,
float length,
float radiusA,
float radiusB,
- int sides)
- : base(proxy, CountVertices(sides), Patterns.Get(sides), bone, localPosition, localRotation, length)
+ int sideCount)
+ : base(CountVertices(sideCount), Patterns.Get(sideCount), bone, localPosition, localRotation, length)
{
_radiusA = radiusA;
_radiusB = radiusB;
@@ -30,12 +29,12 @@ public float RadiusA
{
get
{
- ThrowIfDisposed();
+ EnsureUsable();
return _radiusA;
}
set
{
- ThrowIfDisposed();
+ EnsureUsable();
_radiusA = value;
MarkDirty(DirtyFlags.Positions);
}
@@ -45,37 +44,37 @@ public float RadiusB
{
get
{
- ThrowIfDisposed();
+ EnsureUsable();
return _radiusB;
}
set
{
- ThrowIfDisposed();
+ EnsureUsable();
_radiusB = value;
MarkDirty(DirtyFlags.Positions);
}
}
- public int Sides
+ public int SideCount
{
get
{
- ThrowIfDisposed();
+ EnsureUsable();
return VertexCount / PolygonCount;
}
}
protected override void WriteShape(Span positions, float length)
{
- int sides = positions.Length / PolygonCount;
- for (int corner = 0; corner < sides; corner++)
+ int sideCount = positions.Length / PolygonCount;
+ for (int corner = 0; corner < sideCount; corner++)
{
// Half a side past -Y, so the bottom side is level.
- float angle = Mathf.PI * (2f * corner + 1f) / sides - Mathf.PI * 0.5f;
+ float angle = Mathf.PI * (2f * corner + 1f) / sideCount - Mathf.PI * 0.5f;
float cos = Mathf.Cos(angle);
float sin = Mathf.Sin(angle);
positions[corner] = new Vector3(cos * _radiusA, sin * _radiusA, 0f);
- positions[sides + corner] = new Vector3(cos * _radiusB, sin * _radiusB, length);
+ positions[sideCount + corner] = new Vector3(cos * _radiusB, sin * _radiusB, length);
}
}
@@ -85,25 +84,26 @@ protected override void ScaleCrossSection(float factor)
_radiusB *= factor;
}
- private static int CountVertices(int sides)
+ private static int CountVertices(int sideCount)
{
- if (sides < 3)
+ if (sideCount < 3 || sideCount > Ring.MaxSegmentCount)
{
- throw new ArgumentOutOfRangeException(nameof(sides), sides, "A frustum needs at least 3 sides.");
+ throw new ArgumentOutOfRangeException(
+ nameof(sideCount), sideCount, $"A frustum needs 3 to {Ring.MaxSegmentCount} sides.");
}
- return sides * PolygonCount;
+ return sideCount * PolygonCount;
}
- private static int[] BuildEdges(int sides)
+ private static int[] BuildEdges(int sideCount)
{
- int[] pattern = new int[sides * 3 * 2];
+ int[] pattern = new int[sideCount * 3 * 2];
int cursor = 0;
- Ring.AddEdges(pattern, ref cursor, 0, sides);
- Ring.AddEdges(pattern, ref cursor, sides, sides);
- for (int corner = 0; corner < sides; corner++)
+ Ring.AddEdges(pattern, ref cursor, 0, sideCount);
+ Ring.AddEdges(pattern, ref cursor, sideCount, sideCount);
+ for (int corner = 0; corner < sideCount; corner++)
{
pattern[cursor++] = corner;
- pattern[cursor++] = sides + corner;
+ pattern[cursor++] = sideCount + corner;
}
return pattern;
}
diff --git a/UnityPackage/Runtime/Shapes/Frustum/FrustumFactory.cs b/UnityPackage/Runtime/Shapes/Frustum/FrustumFactory.cs
new file mode 100644
index 0000000..5146621
--- /dev/null
+++ b/UnityPackage/Runtime/Shapes/Frustum/FrustumFactory.cs
@@ -0,0 +1,79 @@
+using UnityEngine;
+
+namespace reromanlee.Wireframes
+{
+ /// Creates frustums, prisms and regular pyramids in a .
+ public static class FrustumFactory
+ {
+ private 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
+ /// 0.5 out at end B.
+ ///
+ public static IFrustum CreateFrustum(this WireframeContainer container)
+ {
+ MeshProxy proxy = WireframeContainer.ProxyOf(container);
+ return proxy.Add(new Frustum(
+ null,
+ Vector3.zero,
+ Quaternion.identity,
+ ShapeDefaults.Size,
+ ShapeDefaults.Radius * 0.5f,
+ ShapeDefaults.Radius,
+ DefaultSideCount));
+ }
+
+ ///
+ /// Creates a white frustum between the centers of its two polygons, both world positions. Its spin around the
+ /// axis keeps the frustum's +Y as close to world up as it can.
+ ///
+ /// Number of sides of each polygon, from 3 to 1024.
+ public static IFrustum CreateFrustum(
+ this WireframeContainer container, Vector3 endA, Vector3 endB, float radiusA, float radiusB, int sideCount)
+ {
+ MeshProxy proxy = WireframeContainer.ProxyOf(container);
+ Quaternion rotation = ShapeRotations.FromAxis(endB - endA, Vector3.up);
+ return proxy.Add(new Frustum(
+ null, endA, rotation, Vector3.Distance(endA, endB), radiusA, radiusB, sideCount));
+ }
+
+ ///
+ /// Creates a white frustum that follows , between the centers of its two polygons in the
+ /// bone's local space. Its spin around the axis keeps the frustum's +Y as close to the bone's up as it can.
+ ///
+ /// Number of sides of each polygon, from 3 to 1024.
+ public static IFrustum CreateFrustum(
+ this WireframeContainer container,
+ Transform bone,
+ Vector3 localEndA,
+ Vector3 localEndB,
+ float radiusA,
+ float radiusB,
+ int sideCount)
+ {
+ MeshProxy proxy = WireframeContainer.ProxyOf(container);
+ Quaternion rotation = ShapeRotations.FromAxis(localEndB - localEndA, Vector3.up);
+ float length = Vector3.Distance(localEndA, localEndB);
+ return proxy.Add(new Frustum(bone, localEndA, rotation, length, radiusA, radiusB, sideCount));
+ }
+
+ ///
+ /// Creates a white frustum in world space that starts at and runs
+ /// along the +Z axis of .
+ ///
+ /// Number of sides of each polygon, from 3 to 1024.
+ public static IFrustum CreateFrustum(
+ this WireframeContainer container,
+ Vector3 position,
+ Quaternion rotation,
+ float length,
+ float radiusA,
+ float radiusB,
+ int sideCount)
+ {
+ MeshProxy proxy = WireframeContainer.ProxyOf(container);
+ return proxy.Add(new Frustum(null, position, rotation, length, radiusA, radiusB, sideCount));
+ }
+ }
+}
diff --git a/UnityPackage/Runtime/Shapes/Frustum/LineContainer.Frustum.cs.meta b/UnityPackage/Runtime/Shapes/Frustum/FrustumFactory.cs.meta
similarity index 100%
rename from UnityPackage/Runtime/Shapes/Frustum/LineContainer.Frustum.cs.meta
rename to UnityPackage/Runtime/Shapes/Frustum/FrustumFactory.cs.meta
diff --git a/UnityPackage/Runtime/Shapes/Frustum/IFrustum.cs b/UnityPackage/Runtime/Shapes/Frustum/IFrustum.cs
index 849def7..2e2d21f 100644
--- a/UnityPackage/Runtime/Shapes/Frustum/IFrustum.cs
+++ b/UnityPackage/Runtime/Shapes/Frustum/IFrustum.cs
@@ -14,6 +14,6 @@ public interface IFrustum : IAxialShape
float RadiusB { get; set; }
/// Number of sides of each polygon, fixed at creation.
- int Sides { get; }
+ int SideCount { get; }
}
}
diff --git a/UnityPackage/Runtime/Shapes/Frustum/ILineContainer.Frustum.cs b/UnityPackage/Runtime/Shapes/Frustum/ILineContainer.Frustum.cs
deleted file mode 100644
index d9dac8d..0000000
--- a/UnityPackage/Runtime/Shapes/Frustum/ILineContainer.Frustum.cs
+++ /dev/null
@@ -1,36 +0,0 @@
-using UnityEngine;
-
-namespace reromanlee.Wireframes
-{
- public partial interface ILineContainer
- {
- ///
- /// Creates a white square frustum from the world origin, 1 unit along +Z, with corners 0.25 out at end A and
- /// 0.5 out at end B.
- ///
- IFrustum CreateFrustum();
-
- ///
- /// Creates a white frustum between the centers of its two polygons, both world positions. Its spin around the
- /// axis keeps the frustum's +Y as close to world up as it can.
- ///
- /// Number of sides of each polygon, at least 3.
- IFrustum CreateFrustum(Vector3 endA, Vector3 endB, float radiusA, float radiusB, int sides);
-
- ///
- /// Creates a white frustum that follows , between the centers of its two polygons in the
- /// bone's local space. Its spin around the axis keeps the frustum's +Y as close to the bone's up as it can.
- ///
- /// Number of sides of each polygon, at least 3.
- IFrustum CreateFrustum(
- Transform bone, Vector3 localEndA, Vector3 localEndB, float radiusA, float radiusB, int sides);
-
- ///
- /// Creates a white frustum in world space that starts at and runs
- /// along the +Z axis of .
- ///
- /// Number of sides of each polygon, at least 3.
- IFrustum CreateFrustum(
- Vector3 position, Quaternion rotation, float length, float radiusA, float radiusB, int sides);
- }
-}
diff --git a/UnityPackage/Runtime/Shapes/Frustum/ILineContainer.Frustum.cs.meta b/UnityPackage/Runtime/Shapes/Frustum/ILineContainer.Frustum.cs.meta
deleted file mode 100644
index 402898f..0000000
--- a/UnityPackage/Runtime/Shapes/Frustum/ILineContainer.Frustum.cs.meta
+++ /dev/null
@@ -1,2 +0,0 @@
-fileFormatVersion: 2
-guid: 35d2821e8858609efb4860ecf6e49966
\ No newline at end of file
diff --git a/UnityPackage/Runtime/Shapes/Frustum/LineContainer.Frustum.cs b/UnityPackage/Runtime/Shapes/Frustum/LineContainer.Frustum.cs
deleted file mode 100644
index adaee0b..0000000
--- a/UnityPackage/Runtime/Shapes/Frustum/LineContainer.Frustum.cs
+++ /dev/null
@@ -1,35 +0,0 @@
-using UnityEngine;
-
-namespace reromanlee.Wireframes
-{
- public sealed partial class LineContainer
- {
- private const int UnitFrustumSides = 4;
-
- public IFrustum CreateFrustum()
- {
- return new Frustum(
- Proxy, null, Vector3.zero, Quaternion.identity, UnitSize, UnitRadius * 0.5f, UnitRadius, UnitFrustumSides);
- }
-
- public IFrustum CreateFrustum(Vector3 endA, Vector3 endB, float radiusA, float radiusB, int sides)
- {
- Quaternion rotation = ShapeRotations.FromAxis(endB - endA, Vector3.up);
- return new Frustum(Proxy, null, endA, rotation, Vector3.Distance(endA, endB), radiusA, radiusB, sides);
- }
-
- public IFrustum CreateFrustum(
- Transform bone, Vector3 localEndA, Vector3 localEndB, float radiusA, float radiusB, int sides)
- {
- Quaternion rotation = ShapeRotations.FromAxis(localEndB - localEndA, Vector3.up);
- float length = Vector3.Distance(localEndA, localEndB);
- return new Frustum(Proxy, bone, localEndA, rotation, length, radiusA, radiusB, sides);
- }
-
- public IFrustum CreateFrustum(
- Vector3 position, Quaternion rotation, float length, float radiusA, float radiusB, int sides)
- {
- return new Frustum(Proxy, null, position, rotation, length, radiusA, radiusB, sides);
- }
- }
-}
diff --git a/UnityPackage/Runtime/Shapes/IAxialShape.cs b/UnityPackage/Runtime/Shapes/IAxialShape.cs
index 26d1678..1e07a1f 100644
--- a/UnityPackage/Runtime/Shapes/IAxialShape.cs
+++ b/UnityPackage/Runtime/Shapes/IAxialShape.cs
@@ -4,21 +4,31 @@ namespace reromanlee.Wireframes
{
///
/// A long shape that runs from end A, at , along the +Z axis of its rotation
- /// to end B.
+ /// to end B. Setting one end keeps the other where it is, while setting the position moves the whole shape.
///
public interface IAxialShape : IRigidShape
{
/// Distance from end A to end B, in the bone's units.
float Length { get; set; }
+ ///
+ /// End A relative to , or in world space without a bone; the same point as
+ /// . Setting it keeps end B where it is: the axis turns by the smallest
+ /// rotation and follows.
+ ///
+ Vector3 LocalEndA { get; set; }
+
///
/// End B relative to , or in world space without a bone. Setting it keeps end A,
/// turns the axis toward the new point by the smallest rotation, which keeps the spin around the axis as far
/// as it can, and sets .
///
- Vector3 LocalEnd { get; set; }
+ Vector3 LocalEndB { get; set; }
+
+ /// End A in world space, converted through the bone's current pose. Setting it works like .
+ Vector3 WorldEndA { get; set; }
- /// End B in world space, converted through the bone's current pose. Setting it works like .
- Vector3 WorldEnd { get; set; }
+ /// End B in world space, converted through the bone's current pose. Setting it works like .
+ Vector3 WorldEndB { get; set; }
}
}
diff --git a/UnityPackage/Runtime/Shapes/IRigidShape.cs b/UnityPackage/Runtime/Shapes/IRigidShape.cs
index 4d86381..93ad5bb 100644
--- a/UnityPackage/Runtime/Shapes/IRigidShape.cs
+++ b/UnityPackage/Runtime/Shapes/IRigidShape.cs
@@ -33,6 +33,7 @@ public interface IRigidShape : IShape
/// exact for uniformly scaled bones. If the bone is destroyed, the shape stays where it was and becomes world
/// space.
///
+ /// The Transform isn't in a scene, such as a prefab asset.
Transform Bone { get; set; }
}
}
diff --git a/UnityPackage/Runtime/Shapes/IShape.cs b/UnityPackage/Runtime/Shapes/IShape.cs
index 2227aae..88c116a 100644
--- a/UnityPackage/Runtime/Shapes/IShape.cs
+++ b/UnityPackage/Runtime/Shapes/IShape.cs
@@ -4,7 +4,8 @@
namespace reromanlee.Wireframes
{
///
- /// A shape drawn by an . Disposing it removes it from the container.
+ /// A shape drawn by a . Disposing it removes it from the container. Shape
+ /// interfaces are implemented only by the package, so members may be added to them in minor versions.
/// Every member except and throws
/// once the shape is disposed.
///
@@ -13,6 +14,12 @@ public interface IShape : IDisposable
/// True once the shape or its container was disposed.
bool IsDisposed { get; }
+ ///
+ /// False hides the shape without disposing it: it keeps its place, bones and every setting, and draws again as it
+ /// is then when shown. Hiding and showing allocate nothing. True by default.
+ ///
+ bool IsVisible { get; set; }
+
/// Sets the color of every vertex of the shape.
void SetColor(Color color);
}
diff --git a/UnityPackage/Runtime/Shapes/Line/ILine.cs b/UnityPackage/Runtime/Shapes/Line/ILine.cs
index bb5205b..aff68e9 100644
--- a/UnityPackage/Runtime/Shapes/Line/ILine.cs
+++ b/UnityPackage/Runtime/Shapes/Line/ILine.cs
@@ -29,12 +29,14 @@ public interface ILine : IShape
/// Transform that endpoint A follows, or null for world space. Changing it keeps the endpoint's world position,
/// like reparenting a Transform. If the bone is destroyed, the endpoint stays where it was and becomes world space.
///
+ /// The Transform isn't in a scene, such as a prefab asset.
Transform BoneA { get; set; }
///
/// Transform that endpoint B follows, or null for world space. Changing it keeps the endpoint's world position,
/// like reparenting a Transform. If the bone is destroyed, the endpoint stays where it was and becomes world space.
///
+ /// The Transform isn't in a scene, such as a prefab asset.
Transform BoneB { get; set; }
}
}
diff --git a/UnityPackage/Runtime/Shapes/Line/ILineContainer.Line.cs b/UnityPackage/Runtime/Shapes/Line/ILineContainer.Line.cs
deleted file mode 100644
index ceba40f..0000000
--- a/UnityPackage/Runtime/Shapes/Line/ILineContainer.Line.cs
+++ /dev/null
@@ -1,19 +0,0 @@
-using UnityEngine;
-
-namespace reromanlee.Wireframes
-{
- public partial interface ILineContainer
- {
- /// Creates a white line with both endpoints at the world origin.
- ILine CreateLine();
-
- /// Creates a white line between two world positions.
- ILine CreateLine(Vector3 positionA, Vector3 positionB);
-
- ///
- /// Creates a white line from the origin of to the origin of ,
- /// with each endpoint following its bone. A null bone puts that endpoint at the world origin.
- ///
- ILine CreateLine(Transform boneA, Transform boneB);
- }
-}
diff --git a/UnityPackage/Runtime/Shapes/Line/ILineContainer.Line.cs.meta b/UnityPackage/Runtime/Shapes/Line/ILineContainer.Line.cs.meta
deleted file mode 100644
index 4f95f10..0000000
--- a/UnityPackage/Runtime/Shapes/Line/ILineContainer.Line.cs.meta
+++ /dev/null
@@ -1,2 +0,0 @@
-fileFormatVersion: 2
-guid: f0a53655b78c40188727dc0cca76cf35
diff --git a/UnityPackage/Runtime/Shapes/Line/Line.cs b/UnityPackage/Runtime/Shapes/Line/Line.cs
index d1d58f3..04bd614 100644
--- a/UnityPackage/Runtime/Shapes/Line/Line.cs
+++ b/UnityPackage/Runtime/Shapes/Line/Line.cs
@@ -10,17 +10,17 @@ internal sealed class Line : PointShape, ILine
private ShapePoint _pointA;
private ShapePoint _pointB;
- internal Line(MeshProxy proxy, Vector3 positionA, Vector3 positionB) : base(proxy, LineVertexCount, EdgePattern)
+ internal Line(Vector3 positionA, Vector3 positionB) : base(LineVertexCount, EdgePattern)
{
_pointA = new ShapePoint(positionA);
_pointB = new ShapePoint(positionB);
}
/// Creates a line whose endpoints sit at the origins of their bones.
- internal Line(MeshProxy proxy, Transform boneA, Transform boneB) : this(proxy, Vector3.zero, Vector3.zero)
+ internal Line(Transform boneA, Transform boneB) : this(Vector3.zero, Vector3.zero)
{
- AttachPoint(0, boneA);
- AttachPoint(1, boneB);
+ InitializeBone(0, boneA, nameof(boneA));
+ InitializeBone(1, boneB, nameof(boneB));
}
public Vector3 LocalPositionA
diff --git a/UnityPackage/Runtime/Shapes/Line/LineContainer.Line.cs b/UnityPackage/Runtime/Shapes/Line/LineContainer.Line.cs
deleted file mode 100644
index 281c5de..0000000
--- a/UnityPackage/Runtime/Shapes/Line/LineContainer.Line.cs
+++ /dev/null
@@ -1,22 +0,0 @@
-using UnityEngine;
-
-namespace reromanlee.Wireframes
-{
- public sealed partial class LineContainer
- {
- public ILine CreateLine()
- {
- return new Line(Proxy, Vector3.zero, Vector3.zero);
- }
-
- public ILine CreateLine(Vector3 positionA, Vector3 positionB)
- {
- return new Line(Proxy, positionA, positionB);
- }
-
- public ILine CreateLine(Transform boneA, Transform boneB)
- {
- return new Line(Proxy, boneA, boneB);
- }
- }
-}
diff --git a/UnityPackage/Runtime/Shapes/Line/LineFactory.cs b/UnityPackage/Runtime/Shapes/Line/LineFactory.cs
new file mode 100644
index 0000000..32e53d8
--- /dev/null
+++ b/UnityPackage/Runtime/Shapes/Line/LineFactory.cs
@@ -0,0 +1,32 @@
+using UnityEngine;
+
+namespace reromanlee.Wireframes
+{
+ /// Creates lines in a .
+ public static class LineFactory
+ {
+ /// Creates a white line with both endpoints at the world origin.
+ public static ILine CreateLine(this WireframeContainer container)
+ {
+ MeshProxy proxy = WireframeContainer.ProxyOf(container);
+ return proxy.Add(new Line(Vector3.zero, Vector3.zero));
+ }
+
+ /// Creates a white line between two world positions.
+ public static ILine CreateLine(this WireframeContainer container, Vector3 positionA, Vector3 positionB)
+ {
+ MeshProxy proxy = WireframeContainer.ProxyOf(container);
+ return proxy.Add(new Line(positionA, positionB));
+ }
+
+ ///
+ /// Creates a white line from the origin of to the origin of ,
+ /// with each endpoint following its bone. A null bone puts that endpoint at the world origin.
+ ///
+ public static ILine CreateLine(this WireframeContainer container, Transform boneA, Transform boneB)
+ {
+ MeshProxy proxy = WireframeContainer.ProxyOf(container);
+ return proxy.Add(new Line(boneA, boneB));
+ }
+ }
+}
diff --git a/UnityPackage/Runtime/Shapes/Line/LineContainer.Line.cs.meta b/UnityPackage/Runtime/Shapes/Line/LineFactory.cs.meta
similarity index 100%
rename from UnityPackage/Runtime/Shapes/Line/LineContainer.Line.cs.meta
rename to UnityPackage/Runtime/Shapes/Line/LineFactory.cs.meta
diff --git a/UnityPackage/Runtime/Shapes/Polyline/ILineContainer.Polyline.cs b/UnityPackage/Runtime/Shapes/Polyline/ILineContainer.Polyline.cs
deleted file mode 100644
index 261fa6c..0000000
--- a/UnityPackage/Runtime/Shapes/Polyline/ILineContainer.Polyline.cs
+++ /dev/null
@@ -1,41 +0,0 @@
-using System.Collections.Generic;
-using UnityEngine;
-
-namespace reromanlee.Wireframes
-{
- public partial interface ILineContainer
- {
- /// Creates a white, open polyline through at least 2 world positions.
- IPolyline CreatePolyline(params Vector3[] points);
-
- /// Creates a white, open polyline through at least 2 world positions.
- IPolyline CreatePolyline(IReadOnlyList points);
-
- ///
- /// Creates a white, open polyline through the origins of at least 2 bones, with each point following its bone.
- /// A null bone puts that point at the world origin.
- ///
- IPolyline CreatePolyline(params Transform[] bones);
-
- /// Creates a white, closed polyline through at least 3 world positions.
- IPolyline CreatePolygon(params Vector3[] points);
-
- /// Creates a white, closed polyline through at least 3 world positions.
- IPolyline CreatePolygon(IReadOnlyList points);
-
- ///
- /// Creates a white, closed polyline through the origins of at least 3 bones, with each point following its
- /// bone. A null bone puts that point at the world origin.
- ///
- IPolyline CreatePolygon(params Transform[] bones);
-
- /// Creates a white triangle, a closed polyline through 3 world positions.
- IPolyline CreateTriangle(Vector3 pointA, Vector3 pointB, Vector3 pointC);
-
- ///
- /// Creates a white triangle through the origins of 3 bones, with each corner following its bone. A null bone
- /// puts that corner at the world origin.
- ///
- IPolyline CreateTriangle(Transform boneA, Transform boneB, Transform boneC);
- }
-}
diff --git a/UnityPackage/Runtime/Shapes/Polyline/ILineContainer.Polyline.cs.meta b/UnityPackage/Runtime/Shapes/Polyline/ILineContainer.Polyline.cs.meta
deleted file mode 100644
index 1c500eb..0000000
--- a/UnityPackage/Runtime/Shapes/Polyline/ILineContainer.Polyline.cs.meta
+++ /dev/null
@@ -1,2 +0,0 @@
-fileFormatVersion: 2
-guid: 657f8efd0d95dd3000118faba964534e
\ No newline at end of file
diff --git a/UnityPackage/Runtime/Shapes/Polyline/IPolyline.cs b/UnityPackage/Runtime/Shapes/Polyline/IPolyline.cs
index 1dbf142..7ca03ff 100644
--- a/UnityPackage/Runtime/Shapes/Polyline/IPolyline.cs
+++ b/UnityPackage/Runtime/Shapes/Polyline/IPolyline.cs
@@ -42,6 +42,7 @@ public interface IPolyline : IShape
/// Sets the Transform a point follows, or null for world space. Changing it keeps the point's world position,
/// like reparenting a Transform. If the bone is destroyed, the point stays where it was and becomes world space.
///
+ /// The Transform isn't in a scene, such as a prefab asset.
void SetBone(int index, Transform bone);
}
}
diff --git a/UnityPackage/Runtime/Shapes/Polyline/LineContainer.Polyline.cs b/UnityPackage/Runtime/Shapes/Polyline/LineContainer.Polyline.cs
deleted file mode 100644
index 3c002b4..0000000
--- a/UnityPackage/Runtime/Shapes/Polyline/LineContainer.Polyline.cs
+++ /dev/null
@@ -1,48 +0,0 @@
-using System.Collections.Generic;
-using UnityEngine;
-
-namespace reromanlee.Wireframes
-{
- public sealed partial class LineContainer
- {
- public IPolyline CreatePolyline(params Vector3[] points)
- {
- return new Polyline(Proxy, points, false);
- }
-
- public IPolyline CreatePolyline(IReadOnlyList points)
- {
- return new Polyline(Proxy, points, false);
- }
-
- public IPolyline CreatePolyline(params Transform[] bones)
- {
- return new Polyline(Proxy, bones, false);
- }
-
- public IPolyline CreatePolygon(params Vector3[] points)
- {
- return new Polyline(Proxy, points, true);
- }
-
- public IPolyline CreatePolygon(IReadOnlyList points)
- {
- return new Polyline(Proxy, points, true);
- }
-
- public IPolyline CreatePolygon(params Transform[] bones)
- {
- return new Polyline(Proxy, bones, true);
- }
-
- public IPolyline CreateTriangle(Vector3 pointA, Vector3 pointB, Vector3 pointC)
- {
- return new Polyline(Proxy, new[] { pointA, pointB, pointC }, true);
- }
-
- public IPolyline CreateTriangle(Transform boneA, Transform boneB, Transform boneC)
- {
- return new Polyline(Proxy, new[] { boneA, boneB, boneC }, true);
- }
- }
-}
diff --git a/UnityPackage/Runtime/Shapes/Polyline/Polyline.cs b/UnityPackage/Runtime/Shapes/Polyline/Polyline.cs
index 025bb13..94fbeb5 100644
--- a/UnityPackage/Runtime/Shapes/Polyline/Polyline.cs
+++ b/UnityPackage/Runtime/Shapes/Polyline/Polyline.cs
@@ -9,8 +9,8 @@ internal sealed class Polyline : PointShape, IPolyline
private readonly ShapePoint[] _points;
private readonly bool _isClosed;
- internal Polyline(MeshProxy proxy, IReadOnlyList points, bool isClosed)
- : base(proxy, CheckCount(points, isClosed, nameof(points)), BuildEdges(points.Count, isClosed))
+ internal Polyline(IReadOnlyList points, bool isClosed)
+ : base(CheckCount(points, isClosed, nameof(points)), BuildEdges(points.Count, isClosed))
{
_isClosed = isClosed;
_points = new ShapePoint[points.Count];
@@ -21,15 +21,15 @@ internal Polyline(MeshProxy proxy, IReadOnlyList points, bool isClosed)
}
/// Creates a polyline whose points sit at the origins of their bones.
- internal Polyline(MeshProxy proxy, IReadOnlyList bones, bool isClosed)
- : base(proxy, CheckCount(bones, isClosed, nameof(bones)), BuildEdges(bones.Count, isClosed))
+ internal Polyline(IReadOnlyList bones, bool isClosed)
+ : base(CheckCount(bones, isClosed, nameof(bones)), BuildEdges(bones.Count, isClosed))
{
_isClosed = isClosed;
_points = new ShapePoint[bones.Count];
for (int i = 0; i < _points.Length; i++)
{
_points[i] = new ShapePoint(Vector3.zero);
- AttachPoint(i, bones[i]);
+ InitializeBone(i, bones[i], nameof(bones));
}
}
@@ -37,7 +37,7 @@ public int PointCount
{
get
{
- ThrowIfDisposed();
+ EnsureUsable();
return _points.Length;
}
}
@@ -46,7 +46,7 @@ public bool IsClosed
{
get
{
- ThrowIfDisposed();
+ EnsureUsable();
return _isClosed;
}
}
diff --git a/UnityPackage/Runtime/Shapes/Polyline/PolylineFactory.cs b/UnityPackage/Runtime/Shapes/Polyline/PolylineFactory.cs
new file mode 100644
index 0000000..e3f7db9
--- /dev/null
+++ b/UnityPackage/Runtime/Shapes/Polyline/PolylineFactory.cs
@@ -0,0 +1,76 @@
+using System.Collections.Generic;
+using UnityEngine;
+
+namespace reromanlee.Wireframes
+{
+ /// Creates polylines, polygons and triangles in a .
+ public static class PolylineFactory
+ {
+ /// Creates a white, open polyline through at least 2 world positions.
+ public static IPolyline CreatePolyline(this WireframeContainer container, params Vector3[] points)
+ {
+ MeshProxy proxy = WireframeContainer.ProxyOf(container);
+ return proxy.Add(new Polyline(points, false));
+ }
+
+ /// Creates a white, open polyline through at least 2 world positions.
+ public static IPolyline CreatePolyline(this WireframeContainer container, IReadOnlyList points)
+ {
+ MeshProxy proxy = WireframeContainer.ProxyOf(container);
+ return proxy.Add(new Polyline(points, false));
+ }
+
+ ///
+ /// Creates a white, open polyline through the origins of at least 2 bones, with each point following its bone.
+ /// A null bone puts that point at the world origin.
+ ///
+ public static IPolyline CreatePolyline(this WireframeContainer container, params Transform[] bones)
+ {
+ MeshProxy proxy = WireframeContainer.ProxyOf(container);
+ return proxy.Add(new Polyline(bones, false));
+ }
+
+ /// Creates a white, closed polyline through at least 3 world positions.
+ public static IPolyline CreatePolygon(this WireframeContainer container, params Vector3[] points)
+ {
+ MeshProxy proxy = WireframeContainer.ProxyOf(container);
+ return proxy.Add(new Polyline(points, true));
+ }
+
+ /// Creates a white, closed polyline through at least 3 world positions.
+ public static IPolyline CreatePolygon(this WireframeContainer container, IReadOnlyList points)
+ {
+ MeshProxy proxy = WireframeContainer.ProxyOf(container);
+ return proxy.Add(new Polyline(points, true));
+ }
+
+ ///
+ /// Creates a white, closed polyline through the origins of at least 3 bones, with each point following its
+ /// bone. A null bone puts that point at the world origin.
+ ///
+ public static IPolyline CreatePolygon(this WireframeContainer container, params Transform[] bones)
+ {
+ MeshProxy proxy = WireframeContainer.ProxyOf(container);
+ return proxy.Add(new Polyline(bones, true));
+ }
+
+ /// Creates a white triangle, a closed polyline through 3 world positions.
+ public static IPolyline CreateTriangle(
+ this WireframeContainer container, Vector3 pointA, Vector3 pointB, Vector3 pointC)
+ {
+ MeshProxy proxy = WireframeContainer.ProxyOf(container);
+ return proxy.Add(new Polyline(new[] { pointA, pointB, pointC }, true));
+ }
+
+ ///
+ /// Creates a white triangle through the origins of 3 bones, with each corner following its bone. A null bone
+ /// puts that corner at the world origin.
+ ///
+ public static IPolyline CreateTriangle(
+ this WireframeContainer container, Transform boneA, Transform boneB, Transform boneC)
+ {
+ MeshProxy proxy = WireframeContainer.ProxyOf(container);
+ return proxy.Add(new Polyline(new[] { boneA, boneB, boneC }, true));
+ }
+ }
+}
diff --git a/UnityPackage/Runtime/Shapes/Polyline/LineContainer.Polyline.cs.meta b/UnityPackage/Runtime/Shapes/Polyline/PolylineFactory.cs.meta
similarity index 100%
rename from UnityPackage/Runtime/Shapes/Polyline/LineContainer.Polyline.cs.meta
rename to UnityPackage/Runtime/Shapes/Polyline/PolylineFactory.cs.meta
diff --git a/UnityPackage/Runtime/Shapes/Pyramid/ILineContainer.Pyramid.cs b/UnityPackage/Runtime/Shapes/Pyramid/ILineContainer.Pyramid.cs
deleted file mode 100644
index 6cac5fe..0000000
--- a/UnityPackage/Runtime/Shapes/Pyramid/ILineContainer.Pyramid.cs
+++ /dev/null
@@ -1,32 +0,0 @@
-using UnityEngine;
-
-namespace reromanlee.Wireframes
-{
- public partial interface ILineContainer
- {
- /// Creates a white pyramid with its tip at the world origin and a 1 by 1 base, 1 unit along +Z.
- IPyramid CreatePyramid();
-
- ///
- /// Creates a white pyramid from a tip to the center of its base, both world positions. Its spin around the axis
- /// keeps the base's Y side as close to world up as it can; pointing straight up or down, the base is aligned
- /// with the world's X and Z axes.
- ///
- /// Width of the base along the pyramid's X axis and height along its Y axis.
- IPyramid CreatePyramid(Vector3 tip, Vector3 baseCenter, Vector2 baseSize);
-
- ///
- /// Creates a white pyramid that follows , from a tip to the center of its base, both in
- /// the bone's local space. Its spin around the axis keeps the base's Y side as close to the bone's up as it can.
- ///
- /// Width of the base along the pyramid's X axis and height along its Y axis.
- IPyramid CreatePyramid(Transform bone, Vector3 localTip, Vector3 localBaseCenter, Vector2 baseSize);
-
- ///
- /// Creates a white pyramid in world space with its tip at and its base
- /// along the +Z axis of .
- ///
- /// Width of the base along the rotation's X axis and height along its Y axis.
- IPyramid CreatePyramid(Vector3 position, Quaternion rotation, float length, Vector2 baseSize);
- }
-}
diff --git a/UnityPackage/Runtime/Shapes/Pyramid/ILineContainer.Pyramid.cs.meta b/UnityPackage/Runtime/Shapes/Pyramid/ILineContainer.Pyramid.cs.meta
deleted file mode 100644
index c3d60a0..0000000
--- a/UnityPackage/Runtime/Shapes/Pyramid/ILineContainer.Pyramid.cs.meta
+++ /dev/null
@@ -1,2 +0,0 @@
-fileFormatVersion: 2
-guid: 08562fc7acae5831b2fec288085eef1e
\ No newline at end of file
diff --git a/UnityPackage/Runtime/Shapes/Pyramid/LineContainer.Pyramid.cs b/UnityPackage/Runtime/Shapes/Pyramid/LineContainer.Pyramid.cs
deleted file mode 100644
index 0b767ec..0000000
--- a/UnityPackage/Runtime/Shapes/Pyramid/LineContainer.Pyramid.cs
+++ /dev/null
@@ -1,29 +0,0 @@
-using UnityEngine;
-
-namespace reromanlee.Wireframes
-{
- public sealed partial class LineContainer
- {
- public IPyramid CreatePyramid()
- {
- return new Pyramid(Proxy, null, Vector3.zero, Quaternion.identity, UnitSize, Vector2.one * UnitSize);
- }
-
- public IPyramid CreatePyramid(Vector3 tip, Vector3 baseCenter, Vector2 baseSize)
- {
- Quaternion rotation = ShapeRotations.FromAxis(baseCenter - tip, Vector3.up);
- return new Pyramid(Proxy, null, tip, rotation, Vector3.Distance(tip, baseCenter), baseSize);
- }
-
- public IPyramid CreatePyramid(Transform bone, Vector3 localTip, Vector3 localBaseCenter, Vector2 baseSize)
- {
- Quaternion rotation = ShapeRotations.FromAxis(localBaseCenter - localTip, Vector3.up);
- return new Pyramid(Proxy, bone, localTip, rotation, Vector3.Distance(localTip, localBaseCenter), baseSize);
- }
-
- public IPyramid CreatePyramid(Vector3 position, Quaternion rotation, float length, Vector2 baseSize)
- {
- return new Pyramid(Proxy, null, position, rotation, length, baseSize);
- }
- }
-}
diff --git a/UnityPackage/Runtime/Shapes/Pyramid/Pyramid.cs b/UnityPackage/Runtime/Shapes/Pyramid/Pyramid.cs
index 0462c4f..9a3719b 100644
--- a/UnityPackage/Runtime/Shapes/Pyramid/Pyramid.cs
+++ b/UnityPackage/Runtime/Shapes/Pyramid/Pyramid.cs
@@ -17,12 +17,11 @@ internal sealed class Pyramid : AxialShape, IPyramid
private Vector2 _baseSize;
internal Pyramid(
- MeshProxy proxy,
Transform bone,
Vector3 localPosition,
Quaternion localRotation,
float length,
- Vector2 baseSize) : base(proxy, PyramidVertexCount, EdgePattern, bone, localPosition, localRotation, length)
+ Vector2 baseSize) : base(PyramidVertexCount, EdgePattern, bone, localPosition, localRotation, length)
{
_baseSize = baseSize;
}
@@ -31,12 +30,12 @@ public Vector2 BaseSize
{
get
{
- ThrowIfDisposed();
+ EnsureUsable();
return _baseSize;
}
set
{
- ThrowIfDisposed();
+ EnsureUsable();
_baseSize = value;
MarkDirty(DirtyFlags.Positions);
}
diff --git a/UnityPackage/Runtime/Shapes/Pyramid/PyramidFactory.cs b/UnityPackage/Runtime/Shapes/Pyramid/PyramidFactory.cs
new file mode 100644
index 0000000..df89e6e
--- /dev/null
+++ b/UnityPackage/Runtime/Shapes/Pyramid/PyramidFactory.cs
@@ -0,0 +1,64 @@
+using UnityEngine;
+
+namespace reromanlee.Wireframes
+{
+ /// Creates pyramids with rectangular bases in a .
+ public static class PyramidFactory
+ {
+ /// Creates a white pyramid with its tip at the world origin and a 1 by 1 base, 1 unit along +Z.
+ public static IPyramid CreatePyramid(this WireframeContainer container)
+ {
+ MeshProxy proxy = WireframeContainer.ProxyOf(container);
+ Vector2 baseSize = Vector2.one * ShapeDefaults.Size;
+ return proxy.Add(new Pyramid(null, Vector3.zero, Quaternion.identity, ShapeDefaults.Size, baseSize));
+ }
+
+ ///
+ /// Creates a white pyramid from a tip to the center of its base, both world positions. Its spin around the axis
+ /// keeps the base's Y side as close to world up as it can; pointing straight up or down, the base is aligned
+ /// with the world's X and Z axes.
+ ///
+ /// Width of the base along the pyramid's X axis and height along its Y axis.
+ public static IPyramid CreatePyramid(
+ this WireframeContainer container, Vector3 tip, Vector3 baseCenter, Vector2 baseSize)
+ {
+ MeshProxy proxy = WireframeContainer.ProxyOf(container);
+ Quaternion rotation = ShapeRotations.FromAxis(baseCenter - tip, Vector3.up);
+ return proxy.Add(new Pyramid(null, tip, rotation, Vector3.Distance(tip, baseCenter), baseSize));
+ }
+
+ ///
+ /// Creates a white pyramid that follows , from a tip to the center of its base, both in
+ /// the bone's local space. Its spin around the axis keeps the base's Y side as close to the bone's up as it can.
+ ///
+ /// Width of the base along the pyramid's X axis and height along its Y axis.
+ public static IPyramid CreatePyramid(
+ this WireframeContainer container,
+ Transform bone,
+ Vector3 localTip,
+ Vector3 localBaseCenter,
+ Vector2 baseSize)
+ {
+ MeshProxy proxy = WireframeContainer.ProxyOf(container);
+ Quaternion rotation = ShapeRotations.FromAxis(localBaseCenter - localTip, Vector3.up);
+ float length = Vector3.Distance(localTip, localBaseCenter);
+ return proxy.Add(new Pyramid(bone, localTip, rotation, length, baseSize));
+ }
+
+ ///
+ /// Creates a white pyramid in world space with its tip at and its base
+ /// along the +Z axis of .
+ ///
+ /// Width of the base along the rotation's X axis and height along its Y axis.
+ public static IPyramid CreatePyramid(
+ this WireframeContainer container,
+ Vector3 position,
+ Quaternion rotation,
+ float length,
+ Vector2 baseSize)
+ {
+ MeshProxy proxy = WireframeContainer.ProxyOf(container);
+ return proxy.Add(new Pyramid(null, position, rotation, length, baseSize));
+ }
+ }
+}
diff --git a/UnityPackage/Runtime/Shapes/Pyramid/LineContainer.Pyramid.cs.meta b/UnityPackage/Runtime/Shapes/Pyramid/PyramidFactory.cs.meta
similarity index 100%
rename from UnityPackage/Runtime/Shapes/Pyramid/LineContainer.Pyramid.cs.meta
rename to UnityPackage/Runtime/Shapes/Pyramid/PyramidFactory.cs.meta
diff --git a/UnityPackage/Runtime/Shapes/Rectangle/ILineContainer.Rectangle.cs b/UnityPackage/Runtime/Shapes/Rectangle/ILineContainer.Rectangle.cs
deleted file mode 100644
index 1d62804..0000000
--- a/UnityPackage/Runtime/Shapes/Rectangle/ILineContainer.Rectangle.cs
+++ /dev/null
@@ -1,25 +0,0 @@
-using UnityEngine;
-
-namespace reromanlee.Wireframes
-{
- public partial interface ILineContainer
- {
- /// Creates a white 1 by 1 rectangle around the world origin, lying flat in the XZ plane.
- IRectangle CreateRectangle();
-
- ///
- /// Creates a white, world-aligned rectangle lying flat in the XZ plane, spanned by two opposite world-space
- /// corners. If the corners differ in height, the rectangle lies halfway between them.
- ///
- IRectangle CreateRectangle(Vector3 cornerA, Vector3 cornerB);
-
- ///
- /// Creates a white rectangle that follows , lying flat in the bone's XZ plane and spanned
- /// by two opposite corners in the bone's local space.
- ///
- IRectangle CreateRectangle(Transform bone, Vector3 localCornerA, Vector3 localCornerB);
-
- /// Creates a white rectangle in world space, lying in the XZ plane of .
- IRectangle CreateRectangle(Vector3 center, Quaternion rotation, Vector2 size);
- }
-}
diff --git a/UnityPackage/Runtime/Shapes/Rectangle/ILineContainer.Rectangle.cs.meta b/UnityPackage/Runtime/Shapes/Rectangle/ILineContainer.Rectangle.cs.meta
deleted file mode 100644
index 69cd0d2..0000000
--- a/UnityPackage/Runtime/Shapes/Rectangle/ILineContainer.Rectangle.cs.meta
+++ /dev/null
@@ -1,2 +0,0 @@
-fileFormatVersion: 2
-guid: eb69f482a3a257187859ebc244068ec3
\ No newline at end of file
diff --git a/UnityPackage/Runtime/Shapes/Rectangle/LineContainer.Rectangle.cs b/UnityPackage/Runtime/Shapes/Rectangle/LineContainer.Rectangle.cs
deleted file mode 100644
index ff7b6f6..0000000
--- a/UnityPackage/Runtime/Shapes/Rectangle/LineContainer.Rectangle.cs
+++ /dev/null
@@ -1,35 +0,0 @@
-using UnityEngine;
-
-namespace reromanlee.Wireframes
-{
- public sealed partial class LineContainer
- {
- public IRectangle CreateRectangle()
- {
- return new Rectangle(Proxy, null, Vector3.zero, Quaternion.identity, Vector2.one * UnitSize);
- }
-
- public IRectangle CreateRectangle(Vector3 cornerA, Vector3 cornerB)
- {
- Vector3 center = (cornerA + cornerB) * 0.5f;
- return new Rectangle(Proxy, null, center, Quaternion.identity, FlatSpan(cornerA, cornerB));
- }
-
- public IRectangle CreateRectangle(Transform bone, Vector3 localCornerA, Vector3 localCornerB)
- {
- Vector3 center = (localCornerA + localCornerB) * 0.5f;
- return new Rectangle(Proxy, bone, center, Quaternion.identity, FlatSpan(localCornerA, localCornerB));
- }
-
- public IRectangle CreateRectangle(Vector3 center, Quaternion rotation, Vector2 size)
- {
- return new Rectangle(Proxy, null, center, rotation, size);
- }
-
- /// Size of the XZ rectangle spanned by two corners; their difference in height is ignored.
- private static Vector2 FlatSpan(Vector3 cornerA, Vector3 cornerB)
- {
- return new Vector2(cornerB.x - cornerA.x, cornerB.z - cornerA.z);
- }
- }
-}
diff --git a/UnityPackage/Runtime/Shapes/Rectangle/Rectangle.cs b/UnityPackage/Runtime/Shapes/Rectangle/Rectangle.cs
index 4252c27..18404d9 100644
--- a/UnityPackage/Runtime/Shapes/Rectangle/Rectangle.cs
+++ b/UnityPackage/Runtime/Shapes/Rectangle/Rectangle.cs
@@ -10,8 +10,8 @@ internal sealed class Rectangle : RectangleShape, IRectangle
// Corner 0 is A and corner 2 is B.
private static readonly int[] EdgePattern = { 0, 1, 1, 2, 2, 3, 3, 0 };
- internal Rectangle(MeshProxy proxy, Transform bone, Vector3 localCenter, Quaternion localRotation, Vector2 size)
- : base(proxy, CornerCount, EdgePattern, bone, localCenter, localRotation, size)
+ internal Rectangle(Transform bone, Vector3 localCenter, Quaternion localRotation, Vector2 size)
+ : base(CornerCount, EdgePattern, bone, localCenter, localRotation, size)
{
}
diff --git a/UnityPackage/Runtime/Shapes/Rectangle/RectangleFactory.cs b/UnityPackage/Runtime/Shapes/Rectangle/RectangleFactory.cs
new file mode 100644
index 0000000..15ff2ce
--- /dev/null
+++ b/UnityPackage/Runtime/Shapes/Rectangle/RectangleFactory.cs
@@ -0,0 +1,48 @@
+using UnityEngine;
+
+namespace reromanlee.Wireframes
+{
+ /// Creates rectangles in a .
+ public static class RectangleFactory
+ {
+ /// Creates a white 1 by 1 rectangle around the world origin, lying flat in the XZ plane.
+ public static IRectangle CreateRectangle(this WireframeContainer container)
+ {
+ MeshProxy proxy = WireframeContainer.ProxyOf(container);
+ return proxy.Add(new Rectangle(null, Vector3.zero, Quaternion.identity, Vector2.one * ShapeDefaults.Size));
+ }
+
+ ///
+ /// Creates a white, world-aligned rectangle lying flat in the XZ plane, spanned by two opposite world-space
+ /// corners. If the corners differ in height, the rectangle lies halfway between them.
+ ///
+ public static IRectangle CreateRectangle(this WireframeContainer container, Vector3 cornerA, Vector3 cornerB)
+ {
+ MeshProxy proxy = WireframeContainer.ProxyOf(container);
+ Vector3 center = (cornerA + cornerB) * 0.5f;
+ return proxy.Add(new Rectangle(
+ null, center, Quaternion.identity, RectangleShape.FlatSpan(cornerA, cornerB)));
+ }
+
+ ///
+ /// Creates a white rectangle that follows , lying flat in the bone's XZ plane and spanned
+ /// by two opposite corners in the bone's local space.
+ ///
+ public static IRectangle CreateRectangle(
+ this WireframeContainer container, Transform bone, Vector3 localCornerA, Vector3 localCornerB)
+ {
+ MeshProxy proxy = WireframeContainer.ProxyOf(container);
+ Vector3 center = (localCornerA + localCornerB) * 0.5f;
+ Vector2 size = RectangleShape.FlatSpan(localCornerA, localCornerB);
+ return proxy.Add(new Rectangle(bone, center, Quaternion.identity, size));
+ }
+
+ /// Creates a white rectangle in world space, lying in the XZ plane of .
+ public static IRectangle CreateRectangle(
+ this WireframeContainer container, Vector3 center, Quaternion rotation, Vector2 size)
+ {
+ MeshProxy proxy = WireframeContainer.ProxyOf(container);
+ return proxy.Add(new Rectangle(null, center, rotation, size));
+ }
+ }
+}
diff --git a/UnityPackage/Runtime/Shapes/Rectangle/LineContainer.Rectangle.cs.meta b/UnityPackage/Runtime/Shapes/Rectangle/RectangleFactory.cs.meta
similarity index 100%
rename from UnityPackage/Runtime/Shapes/Rectangle/LineContainer.Rectangle.cs.meta
rename to UnityPackage/Runtime/Shapes/Rectangle/RectangleFactory.cs.meta
diff --git a/UnityPackage/Runtime/Shapes/RoundedRectangle/ILineContainer.RoundedRectangle.cs b/UnityPackage/Runtime/Shapes/RoundedRectangle/ILineContainer.RoundedRectangle.cs
deleted file mode 100644
index f20fe3d..0000000
--- a/UnityPackage/Runtime/Shapes/RoundedRectangle/ILineContainer.RoundedRectangle.cs
+++ /dev/null
@@ -1,46 +0,0 @@
-using UnityEngine;
-
-namespace reromanlee.Wireframes
-{
- public partial interface ILineContainer
- {
- ///
- /// Creates a white 1 by 1 rectangle with corners of radius 0.25 around the world origin, lying flat in the XZ
- /// plane.
- ///
- IRoundedRectangle CreateRoundedRectangle();
-
- ///
- /// Creates a white, world-aligned rounded rectangle lying flat in the XZ plane, spanned by two opposite
- /// world-space corners. If the corners differ in height, the rectangle lies halfway between them.
- ///
- ///
- /// Number of straight pieces a full circle of the corners is drawn with, a positive multiple of 4.
- ///
- IRoundedRectangle CreateRoundedRectangle(
- Vector3 cornerA, Vector3 cornerB, float cornerRadius, int segments = Ring.DefaultSegments);
-
- ///
- /// Creates a white rounded rectangle that follows , lying flat in the bone's XZ plane
- /// and spanned by two opposite corners in the bone's local space.
- ///
- ///
- /// Number of straight pieces a full circle of the corners is drawn with, a positive multiple of 4.
- ///
- IRoundedRectangle CreateRoundedRectangle(
- Transform bone,
- Vector3 localCornerA,
- Vector3 localCornerB,
- float cornerRadius,
- int segments = Ring.DefaultSegments);
-
- ///
- /// Creates a white rounded rectangle in world space, lying in the XZ plane of .
- ///
- ///
- /// Number of straight pieces a full circle of the corners is drawn with, a positive multiple of 4.
- ///
- IRoundedRectangle CreateRoundedRectangle(
- Vector3 center, Quaternion rotation, Vector2 size, float cornerRadius, int segments = Ring.DefaultSegments);
- }
-}
diff --git a/UnityPackage/Runtime/Shapes/RoundedRectangle/ILineContainer.RoundedRectangle.cs.meta b/UnityPackage/Runtime/Shapes/RoundedRectangle/ILineContainer.RoundedRectangle.cs.meta
deleted file mode 100644
index c434d63..0000000
--- a/UnityPackage/Runtime/Shapes/RoundedRectangle/ILineContainer.RoundedRectangle.cs.meta
+++ /dev/null
@@ -1,2 +0,0 @@
-fileFormatVersion: 2
-guid: 4d116700a802b8c3a0d9ed4202455f8b
\ No newline at end of file
diff --git a/UnityPackage/Runtime/Shapes/RoundedRectangle/IRoundedRectangle.cs b/UnityPackage/Runtime/Shapes/RoundedRectangle/IRoundedRectangle.cs
index 9ce294e..d8cab2f 100644
--- a/UnityPackage/Runtime/Shapes/RoundedRectangle/IRoundedRectangle.cs
+++ b/UnityPackage/Runtime/Shapes/RoundedRectangle/IRoundedRectangle.cs
@@ -13,6 +13,6 @@ public interface IRoundedRectangle : IRectangle
/// Number of straight pieces a full circle of the corners is drawn with, fixed at creation. Each corner is a
/// quarter of them.
///
- int Segments { get; }
+ int SegmentCount { get; }
}
}
diff --git a/UnityPackage/Runtime/Shapes/RoundedRectangle/LineContainer.RoundedRectangle.cs b/UnityPackage/Runtime/Shapes/RoundedRectangle/LineContainer.RoundedRectangle.cs
deleted file mode 100644
index d5871be..0000000
--- a/UnityPackage/Runtime/Shapes/RoundedRectangle/LineContainer.RoundedRectangle.cs
+++ /dev/null
@@ -1,41 +0,0 @@
-using UnityEngine;
-
-namespace reromanlee.Wireframes
-{
- public sealed partial class LineContainer
- {
- private const float UnitCornerRadius = 0.25f;
-
- public IRoundedRectangle CreateRoundedRectangle()
- {
- return new RoundedRectangle(
- Proxy, null, Vector3.zero, Quaternion.identity, Vector2.one * UnitSize, UnitCornerRadius, Ring.DefaultSegments);
- }
-
- public IRoundedRectangle CreateRoundedRectangle(
- Vector3 cornerA, Vector3 cornerB, float cornerRadius, int segments = Ring.DefaultSegments)
- {
- Vector3 center = (cornerA + cornerB) * 0.5f;
- Vector2 size = FlatSpan(cornerA, cornerB);
- return new RoundedRectangle(Proxy, null, center, Quaternion.identity, size, cornerRadius, segments);
- }
-
- public IRoundedRectangle CreateRoundedRectangle(
- Transform bone,
- Vector3 localCornerA,
- Vector3 localCornerB,
- float cornerRadius,
- int segments = Ring.DefaultSegments)
- {
- Vector3 center = (localCornerA + localCornerB) * 0.5f;
- Vector2 size = FlatSpan(localCornerA, localCornerB);
- return new RoundedRectangle(Proxy, bone, center, Quaternion.identity, size, cornerRadius, segments);
- }
-
- public IRoundedRectangle CreateRoundedRectangle(
- Vector3 center, Quaternion rotation, Vector2 size, float cornerRadius, int segments = Ring.DefaultSegments)
- {
- return new RoundedRectangle(Proxy, null, center, rotation, size, cornerRadius, segments);
- }
- }
-}
diff --git a/UnityPackage/Runtime/Shapes/RoundedRectangle/RoundedRectangle.cs b/UnityPackage/Runtime/Shapes/RoundedRectangle/RoundedRectangle.cs
index 5152fcb..77de07a 100644
--- a/UnityPackage/Runtime/Shapes/RoundedRectangle/RoundedRectangle.cs
+++ b/UnityPackage/Runtime/Shapes/RoundedRectangle/RoundedRectangle.cs
@@ -11,17 +11,15 @@ internal sealed class RoundedRectangle : RectangleShape, IRoundedRectangle
private float _cornerRadius;
internal RoundedRectangle(
- MeshProxy proxy,
Transform bone,
Vector3 localCenter,
Quaternion localRotation,
Vector2 size,
float cornerRadius,
- int segments)
+ int segmentCount)
: base(
- proxy,
- Ring.CheckQuarterSegments(segments) + CornerCount,
- Patterns.Get(segments),
+ Ring.CheckQuarterSegmentCount(segmentCount) + CornerCount,
+ Patterns.Get(segmentCount),
bone,
localCenter,
localRotation,
@@ -34,30 +32,30 @@ public float CornerRadius
{
get
{
- ThrowIfDisposed();
+ EnsureUsable();
return _cornerRadius;
}
set
{
- ThrowIfDisposed();
+ EnsureUsable();
_cornerRadius = value;
MarkDirty(DirtyFlags.Positions);
}
}
- public int Segments
+ public int SegmentCount
{
get
{
- ThrowIfDisposed();
+ EnsureUsable();
return VertexCount - CornerCount;
}
}
protected override void WriteShape(Span positions)
{
- int segments = positions.Length - CornerCount;
- int quarter = segments / 4;
+ int segmentCount = positions.Length - CornerCount;
+ int quarter = segmentCount / 4;
Vector2 size = Size;
// Built for the absolute size, then mirrored by its signs.
float halfX = Mathf.Abs(size.x) * 0.5f;
@@ -65,7 +63,6 @@ protected override void WriteShape(Span positions)
float mirrorX = size.x < 0f ? -1f : 1f;
float mirrorZ = size.y < 0f ? -1f : 1f;
float radius = Mathf.Clamp(_cornerRadius, 0f, Mathf.Min(halfX, halfZ));
- Vector2[] circle = Ring.UnitCircle(segments);
// Going around from +Z toward +X, the corners are (+X, +Z), (+X, -Z), (-X, -Z) and (-X, +Z); each is a
// quarter of a circle around a point inset by the radius.
@@ -74,9 +71,10 @@ protected override void WriteShape(Span positions)
{
float centerX = corner < 2 ? halfX - radius : radius - halfX;
float centerZ = corner == 0 || corner == 3 ? halfZ - radius : radius - halfZ;
+ CirclePoints arc = new(segmentCount, corner * quarter);
for (int step = 0; step <= quarter; step++)
{
- Vector2 direction = circle[(corner * quarter + step) % segments];
+ Vector2 direction = arc.Next();
positions[vertex++] = new Vector3(
(centerX + direction.y * radius) * mirrorX,
0f,
@@ -91,11 +89,11 @@ protected override void ScaleSizes(float factor)
_cornerRadius *= factor;
}
- private static int[] BuildEdges(int segments)
+ private static int[] BuildEdges(int segmentCount)
{
- int quarter = segments / 4;
+ int quarter = segmentCount / 4;
int cornerVertices = quarter + 1;
- int[] pattern = new int[(segments + CornerCount) * 2];
+ int[] pattern = new int[(segmentCount + CornerCount) * 2];
int cursor = 0;
for (int corner = 0; corner < CornerCount; corner++)
{
diff --git a/UnityPackage/Runtime/Shapes/RoundedRectangle/RoundedRectangleFactory.cs b/UnityPackage/Runtime/Shapes/RoundedRectangle/RoundedRectangleFactory.cs
new file mode 100644
index 0000000..8dc2def
--- /dev/null
+++ b/UnityPackage/Runtime/Shapes/RoundedRectangle/RoundedRectangleFactory.cs
@@ -0,0 +1,81 @@
+using UnityEngine;
+
+namespace reromanlee.Wireframes
+{
+ /// Creates rounded rectangles in a .
+ public static class RoundedRectangleFactory
+ {
+ private 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
+ /// plane.
+ ///
+ public static IRoundedRectangle CreateRoundedRectangle(this WireframeContainer container)
+ {
+ MeshProxy proxy = WireframeContainer.ProxyOf(container);
+ Vector2 size = Vector2.one * ShapeDefaults.Size;
+ return proxy.Add(new RoundedRectangle(
+ null, Vector3.zero, Quaternion.identity, size, DefaultCornerRadius, Ring.DefaultSegmentCount));
+ }
+
+ ///
+ /// Creates a white, world-aligned rounded rectangle lying flat in the XZ plane, spanned by two opposite
+ /// world-space corners. If the corners differ in height, the rectangle lies halfway between them.
+ ///
+ ///
+ /// Number of straight pieces a full circle of the corners is drawn with, a multiple of 4 from 4 to 1024.
+ ///
+ public static IRoundedRectangle CreateRoundedRectangle(
+ this WireframeContainer container,
+ Vector3 cornerA,
+ Vector3 cornerB,
+ float cornerRadius,
+ int segmentCount = Ring.DefaultSegmentCount)
+ {
+ MeshProxy proxy = WireframeContainer.ProxyOf(container);
+ Vector3 center = (cornerA + cornerB) * 0.5f;
+ Vector2 size = RectangleShape.FlatSpan(cornerA, cornerB);
+ return proxy.Add(new RoundedRectangle(null, center, Quaternion.identity, size, cornerRadius, segmentCount));
+ }
+
+ ///
+ /// Creates a white rounded rectangle that follows , lying flat in the bone's XZ plane
+ /// and spanned by two opposite corners in the bone's local space.
+ ///
+ ///
+ /// Number of straight pieces a full circle of the corners is drawn with, a multiple of 4 from 4 to 1024.
+ ///
+ public static IRoundedRectangle CreateRoundedRectangle(
+ this WireframeContainer container,
+ Transform bone,
+ Vector3 localCornerA,
+ Vector3 localCornerB,
+ float cornerRadius,
+ int segmentCount = Ring.DefaultSegmentCount)
+ {
+ MeshProxy proxy = WireframeContainer.ProxyOf(container);
+ Vector3 center = (localCornerA + localCornerB) * 0.5f;
+ Vector2 size = RectangleShape.FlatSpan(localCornerA, localCornerB);
+ return proxy.Add(new RoundedRectangle(bone, center, Quaternion.identity, size, cornerRadius, segmentCount));
+ }
+
+ ///
+ /// Creates a white rounded rectangle in world space, lying in the XZ plane of .
+ ///
+ ///
+ /// Number of straight pieces a full circle of the corners is drawn with, a multiple of 4 from 4 to 1024.
+ ///
+ public static IRoundedRectangle CreateRoundedRectangle(
+ this WireframeContainer container,
+ Vector3 center,
+ Quaternion rotation,
+ Vector2 size,
+ float cornerRadius,
+ int segmentCount = Ring.DefaultSegmentCount)
+ {
+ MeshProxy proxy = WireframeContainer.ProxyOf(container);
+ return proxy.Add(new RoundedRectangle(null, center, rotation, size, cornerRadius, segmentCount));
+ }
+ }
+}
diff --git a/UnityPackage/Runtime/Shapes/RoundedRectangle/LineContainer.RoundedRectangle.cs.meta b/UnityPackage/Runtime/Shapes/RoundedRectangle/RoundedRectangleFactory.cs.meta
similarity index 100%
rename from UnityPackage/Runtime/Shapes/RoundedRectangle/LineContainer.RoundedRectangle.cs.meta
rename to UnityPackage/Runtime/Shapes/RoundedRectangle/RoundedRectangleFactory.cs.meta
diff --git a/UnityPackage/Runtime/Shapes/Sphere/ILineContainer.Sphere.cs b/UnityPackage/Runtime/Shapes/Sphere/ILineContainer.Sphere.cs
deleted file mode 100644
index a9ed8c8..0000000
--- a/UnityPackage/Runtime/Shapes/Sphere/ILineContainer.Sphere.cs
+++ /dev/null
@@ -1,25 +0,0 @@
-using UnityEngine;
-
-namespace reromanlee.Wireframes
-{
- public partial interface ILineContainer
- {
- /// Creates a white sphere of radius 0.5 around the world origin.
- ISphere CreateSphere();
-
- /// Creates a white, world-aligned sphere around a world position.
- /// Number of straight pieces each great circle is drawn with, at least 3.
- ISphere CreateSphere(Vector3 center, float radius, int segments = Ring.DefaultSegments);
-
- ///
- /// Creates a white sphere that follows , around a position in the bone's local space and
- /// aligned to the bone's axes.
- ///
- /// Number of straight pieces each great circle is drawn with, at least 3.
- ISphere CreateSphere(Transform bone, Vector3 localCenter, float radius, int segments = Ring.DefaultSegments);
-
- /// Creates a white sphere in world space, with its great circles turned by .
- /// Number of straight pieces each great circle is drawn with, at least 3.
- ISphere CreateSphere(Vector3 center, Quaternion rotation, float radius, int segments = Ring.DefaultSegments);
- }
-}
diff --git a/UnityPackage/Runtime/Shapes/Sphere/ILineContainer.Sphere.cs.meta b/UnityPackage/Runtime/Shapes/Sphere/ILineContainer.Sphere.cs.meta
deleted file mode 100644
index 3fd6022..0000000
--- a/UnityPackage/Runtime/Shapes/Sphere/ILineContainer.Sphere.cs.meta
+++ /dev/null
@@ -1,2 +0,0 @@
-fileFormatVersion: 2
-guid: 62d3973d3489cc71e36f61ad9af1a889
\ No newline at end of file
diff --git a/UnityPackage/Runtime/Shapes/Sphere/ISphere.cs b/UnityPackage/Runtime/Shapes/Sphere/ISphere.cs
index c37413b..78713b0 100644
--- a/UnityPackage/Runtime/Shapes/Sphere/ISphere.cs
+++ b/UnityPackage/Runtime/Shapes/Sphere/ISphere.cs
@@ -10,6 +10,6 @@ public interface ISphere : IRigidShape
float Radius { get; set; }
/// Number of straight pieces each great circle is drawn with, fixed at creation.
- int Segments { get; }
+ int SegmentCount { get; }
}
}
diff --git a/UnityPackage/Runtime/Shapes/Sphere/LineContainer.Sphere.cs b/UnityPackage/Runtime/Shapes/Sphere/LineContainer.Sphere.cs
deleted file mode 100644
index f864ab0..0000000
--- a/UnityPackage/Runtime/Shapes/Sphere/LineContainer.Sphere.cs
+++ /dev/null
@@ -1,27 +0,0 @@
-using UnityEngine;
-
-namespace reromanlee.Wireframes
-{
- public sealed partial class LineContainer
- {
- public ISphere CreateSphere()
- {
- return new Sphere(Proxy, null, Vector3.zero, Quaternion.identity, UnitRadius, Ring.DefaultSegments);
- }
-
- public ISphere CreateSphere(Vector3 center, float radius, int segments = Ring.DefaultSegments)
- {
- return new Sphere(Proxy, null, center, Quaternion.identity, radius, segments);
- }
-
- public ISphere CreateSphere(Transform bone, Vector3 localCenter, float radius, int segments = Ring.DefaultSegments)
- {
- return new Sphere(Proxy, bone, localCenter, Quaternion.identity, radius, segments);
- }
-
- public ISphere CreateSphere(Vector3 center, Quaternion rotation, float radius, int segments = Ring.DefaultSegments)
- {
- return new Sphere(Proxy, null, center, rotation, radius, segments);
- }
- }
-}
diff --git a/UnityPackage/Runtime/Shapes/Sphere/Sphere.cs b/UnityPackage/Runtime/Shapes/Sphere/Sphere.cs
index 59f6f12..835be28 100644
--- a/UnityPackage/Runtime/Shapes/Sphere/Sphere.cs
+++ b/UnityPackage/Runtime/Shapes/Sphere/Sphere.cs
@@ -8,16 +8,14 @@ internal sealed class Sphere : RigidShape, ISphere
private float _radius;
internal Sphere(
- MeshProxy proxy,
Transform bone,
Vector3 localCenter,
Quaternion localRotation,
float radius,
- int segments)
+ int segmentCount)
: base(
- proxy,
- Ring.CheckSegments(segments) * AxisRings.RingCount,
- AxisRings.Patterns.Get(segments),
+ Ring.CheckSegmentCount(segmentCount) * AxisRings.RingCount,
+ AxisRings.Patterns.Get(segmentCount),
bone,
localCenter,
localRotation)
@@ -29,22 +27,22 @@ public float Radius
{
get
{
- ThrowIfDisposed();
+ EnsureUsable();
return _radius;
}
set
{
- ThrowIfDisposed();
+ EnsureUsable();
_radius = value;
MarkDirty(DirtyFlags.Positions);
}
}
- public int Segments
+ public int SegmentCount
{
get
{
- ThrowIfDisposed();
+ EnsureUsable();
return VertexCount / AxisRings.RingCount;
}
}
diff --git a/UnityPackage/Runtime/Shapes/Sphere/SphereFactory.cs b/UnityPackage/Runtime/Shapes/Sphere/SphereFactory.cs
new file mode 100644
index 0000000..da7dde8
--- /dev/null
+++ b/UnityPackage/Runtime/Shapes/Sphere/SphereFactory.cs
@@ -0,0 +1,54 @@
+using UnityEngine;
+
+namespace reromanlee.Wireframes
+{
+ /// Creates spheres in a .
+ public static class SphereFactory
+ {
+ /// Creates a white sphere of radius 0.5 around the world origin.
+ public static ISphere CreateSphere(this WireframeContainer container)
+ {
+ MeshProxy proxy = WireframeContainer.ProxyOf(container);
+ return proxy.Add(new Sphere(
+ null, Vector3.zero, Quaternion.identity, ShapeDefaults.Radius, Ring.DefaultSegmentCount));
+ }
+
+ /// Creates a white, world-aligned sphere around a world position.
+ /// Number of straight pieces each great circle is drawn with, from 3 to 1024.
+ public static ISphere CreateSphere(
+ this WireframeContainer container, Vector3 center, float radius, int segmentCount = Ring.DefaultSegmentCount)
+ {
+ MeshProxy proxy = WireframeContainer.ProxyOf(container);
+ return proxy.Add(new Sphere(null, center, Quaternion.identity, radius, segmentCount));
+ }
+
+ ///
+ /// Creates a white sphere that follows , around a position in the bone's local space and
+ /// aligned to the bone's axes.
+ ///
+ /// Number of straight pieces each great circle is drawn with, from 3 to 1024.
+ public static ISphere CreateSphere(
+ this WireframeContainer container,
+ Transform bone,
+ Vector3 localCenter,
+ float radius,
+ int segmentCount = Ring.DefaultSegmentCount)
+ {
+ MeshProxy proxy = WireframeContainer.ProxyOf(container);
+ return proxy.Add(new Sphere(bone, localCenter, Quaternion.identity, radius, segmentCount));
+ }
+
+ /// Creates a white sphere in world space, with its great circles turned by .
+ /// Number of straight pieces each great circle is drawn with, from 3 to 1024.
+ public static ISphere CreateSphere(
+ this WireframeContainer container,
+ Vector3 center,
+ Quaternion rotation,
+ float radius,
+ int segmentCount = Ring.DefaultSegmentCount)
+ {
+ MeshProxy proxy = WireframeContainer.ProxyOf(container);
+ return proxy.Add(new Sphere(null, center, rotation, radius, segmentCount));
+ }
+ }
+}
diff --git a/UnityPackage/Runtime/Shapes/Sphere/LineContainer.Sphere.cs.meta b/UnityPackage/Runtime/Shapes/Sphere/SphereFactory.cs.meta
similarity index 100%
rename from UnityPackage/Runtime/Shapes/Sphere/LineContainer.Sphere.cs.meta
rename to UnityPackage/Runtime/Shapes/Sphere/SphereFactory.cs.meta
diff --git a/UnityPackage/Runtime/Shapes/SpikedSphere/ILineContainer.SpikedSphere.cs b/UnityPackage/Runtime/Shapes/SpikedSphere/ILineContainer.SpikedSphere.cs
deleted file mode 100644
index 070a7dd..0000000
--- a/UnityPackage/Runtime/Shapes/SpikedSphere/ILineContainer.SpikedSphere.cs
+++ /dev/null
@@ -1,30 +0,0 @@
-using UnityEngine;
-
-namespace reromanlee.Wireframes
-{
- public partial interface ILineContainer
- {
- ///
- /// Creates a white spiked sphere around the world origin: a dodecahedron with corners 0.25 from the center and
- /// 12 spikes reaching 0.5.
- ///
- ISpikedSphere CreateSpikedSphere();
-
- /// Creates a white, world-aligned spiked sphere around a world position.
- /// 4, 6, 8, 12 or 20: the number of faces of the Platonic solid it is built on.
- ISpikedSphere CreateSpikedSphere(Vector3 center, float baseRadius, float spikeLength, int spikeCount);
-
- ///
- /// Creates a white spiked sphere that follows , around a position in the bone's local
- /// space and aligned to the bone's axes.
- ///
- /// 4, 6, 8, 12 or 20: the number of faces of the Platonic solid it is built on.
- ISpikedSphere CreateSpikedSphere(
- Transform bone, Vector3 localCenter, float baseRadius, float spikeLength, int spikeCount);
-
- /// Creates a white spiked sphere in world space, turned by .
- /// 4, 6, 8, 12 or 20: the number of faces of the Platonic solid it is built on.
- ISpikedSphere CreateSpikedSphere(
- Vector3 center, Quaternion rotation, float baseRadius, float spikeLength, int spikeCount);
- }
-}
diff --git a/UnityPackage/Runtime/Shapes/SpikedSphere/ILineContainer.SpikedSphere.cs.meta b/UnityPackage/Runtime/Shapes/SpikedSphere/ILineContainer.SpikedSphere.cs.meta
deleted file mode 100644
index ace89b6..0000000
--- a/UnityPackage/Runtime/Shapes/SpikedSphere/ILineContainer.SpikedSphere.cs.meta
+++ /dev/null
@@ -1,2 +0,0 @@
-fileFormatVersion: 2
-guid: be8c37819a9dec721110b0d151e81e1e
\ No newline at end of file
diff --git a/UnityPackage/Runtime/Shapes/SpikedSphere/LineContainer.SpikedSphere.cs b/UnityPackage/Runtime/Shapes/SpikedSphere/LineContainer.SpikedSphere.cs
deleted file mode 100644
index c326a96..0000000
--- a/UnityPackage/Runtime/Shapes/SpikedSphere/LineContainer.SpikedSphere.cs
+++ /dev/null
@@ -1,32 +0,0 @@
-using UnityEngine;
-
-namespace reromanlee.Wireframes
-{
- public sealed partial class LineContainer
- {
- private const int UnitSpikeCount = 12;
-
- public ISpikedSphere CreateSpikedSphere()
- {
- float half = UnitRadius * 0.5f;
- return new SpikedSphere(Proxy, null, Vector3.zero, Quaternion.identity, half, half, UnitSpikeCount);
- }
-
- public ISpikedSphere CreateSpikedSphere(Vector3 center, float baseRadius, float spikeLength, int spikeCount)
- {
- return new SpikedSphere(Proxy, null, center, Quaternion.identity, baseRadius, spikeLength, spikeCount);
- }
-
- public ISpikedSphere CreateSpikedSphere(
- Transform bone, Vector3 localCenter, float baseRadius, float spikeLength, int spikeCount)
- {
- return new SpikedSphere(Proxy, bone, localCenter, Quaternion.identity, baseRadius, spikeLength, spikeCount);
- }
-
- public ISpikedSphere CreateSpikedSphere(
- Vector3 center, Quaternion rotation, float baseRadius, float spikeLength, int spikeCount)
- {
- return new SpikedSphere(Proxy, null, center, rotation, baseRadius, spikeLength, spikeCount);
- }
- }
-}
diff --git a/UnityPackage/Runtime/Shapes/SpikedSphere/SpikedSphere.cs b/UnityPackage/Runtime/Shapes/SpikedSphere/SpikedSphere.cs
index 33171bc..4b5dc93 100644
--- a/UnityPackage/Runtime/Shapes/SpikedSphere/SpikedSphere.cs
+++ b/UnityPackage/Runtime/Shapes/SpikedSphere/SpikedSphere.cs
@@ -10,19 +10,17 @@ internal sealed class SpikedSphere : RigidShape, ISpikedSphere
private float _spikeLength;
internal SpikedSphere(
- MeshProxy proxy,
Transform bone,
Vector3 localCenter,
Quaternion localRotation,
float baseRadius,
float spikeLength,
int spikeCount)
- : this(proxy, bone, localCenter, localRotation, baseRadius, spikeLength, SolidFor(spikeCount))
+ : this(bone, localCenter, localRotation, baseRadius, spikeLength, SolidFor(spikeCount))
{
}
private SpikedSphere(
- MeshProxy proxy,
Transform bone,
Vector3 localCenter,
Quaternion localRotation,
@@ -30,7 +28,6 @@ private SpikedSphere(
float spikeLength,
PlatonicSolid solid)
: base(
- proxy,
solid.Corners.Length + solid.Faces.Length,
solid.EdgePattern,
bone,
@@ -46,12 +43,12 @@ public float BaseRadius
{
get
{
- ThrowIfDisposed();
+ EnsureUsable();
return _baseRadius;
}
set
{
- ThrowIfDisposed();
+ EnsureUsable();
_baseRadius = value;
MarkDirty(DirtyFlags.Positions);
}
@@ -61,12 +58,12 @@ public float SpikeLength
{
get
{
- ThrowIfDisposed();
+ EnsureUsable();
return _spikeLength;
}
set
{
- ThrowIfDisposed();
+ EnsureUsable();
_spikeLength = value;
MarkDirty(DirtyFlags.Positions);
}
@@ -76,7 +73,7 @@ public int SpikeCount
{
get
{
- ThrowIfDisposed();
+ EnsureUsable();
return _solid.Faces.Length;
}
}
diff --git a/UnityPackage/Runtime/Shapes/SpikedSphere/SpikedSphereFactory.cs b/UnityPackage/Runtime/Shapes/SpikedSphere/SpikedSphereFactory.cs
new file mode 100644
index 0000000..37c0f18
--- /dev/null
+++ b/UnityPackage/Runtime/Shapes/SpikedSphere/SpikedSphereFactory.cs
@@ -0,0 +1,62 @@
+using UnityEngine;
+
+namespace reromanlee.Wireframes
+{
+ /// Creates spiked spheres in a .
+ public static class SpikedSphereFactory
+ {
+ private const int DefaultSpikeCount = 12;
+
+ ///
+ /// Creates a white spiked sphere around the world origin: a dodecahedron with corners 0.25 from the center and
+ /// 12 spikes reaching 0.5.
+ ///
+ public static ISpikedSphere CreateSpikedSphere(this WireframeContainer container)
+ {
+ MeshProxy proxy = WireframeContainer.ProxyOf(container);
+ float half = ShapeDefaults.Radius * 0.5f;
+ return proxy.Add(new SpikedSphere(null, Vector3.zero, Quaternion.identity, half, half, DefaultSpikeCount));
+ }
+
+ /// Creates a white, world-aligned spiked sphere around a world position.
+ /// 4, 6, 8, 12 or 20: the number of faces of the Platonic solid it is built on.
+ public static ISpikedSphere CreateSpikedSphere(
+ this WireframeContainer container, Vector3 center, float baseRadius, float spikeLength, int spikeCount)
+ {
+ MeshProxy proxy = WireframeContainer.ProxyOf(container);
+ return proxy.Add(new SpikedSphere(null, center, Quaternion.identity, baseRadius, spikeLength, spikeCount));
+ }
+
+ ///
+ /// Creates a white spiked sphere that follows , around a position in the bone's local
+ /// space and aligned to the bone's axes.
+ ///
+ /// 4, 6, 8, 12 or 20: the number of faces of the Platonic solid it is built on.
+ public static ISpikedSphere CreateSpikedSphere(
+ this WireframeContainer container,
+ Transform bone,
+ Vector3 localCenter,
+ float baseRadius,
+ float spikeLength,
+ int spikeCount)
+ {
+ MeshProxy proxy = WireframeContainer.ProxyOf(container);
+ return proxy.Add(new SpikedSphere(
+ bone, localCenter, Quaternion.identity, baseRadius, spikeLength, spikeCount));
+ }
+
+ /// Creates a white spiked sphere in world space, turned by .
+ /// 4, 6, 8, 12 or 20: the number of faces of the Platonic solid it is built on.
+ public static ISpikedSphere CreateSpikedSphere(
+ this WireframeContainer container,
+ Vector3 center,
+ Quaternion rotation,
+ float baseRadius,
+ float spikeLength,
+ int spikeCount)
+ {
+ MeshProxy proxy = WireframeContainer.ProxyOf(container);
+ return proxy.Add(new SpikedSphere(null, center, rotation, baseRadius, spikeLength, spikeCount));
+ }
+ }
+}
diff --git a/UnityPackage/Runtime/Shapes/SpikedSphere/LineContainer.SpikedSphere.cs.meta b/UnityPackage/Runtime/Shapes/SpikedSphere/SpikedSphereFactory.cs.meta
similarity index 100%
rename from UnityPackage/Runtime/Shapes/SpikedSphere/LineContainer.SpikedSphere.cs.meta
rename to UnityPackage/Runtime/Shapes/SpikedSphere/SpikedSphereFactory.cs.meta
diff --git a/UnityPackage/Runtime/Shapes/Stadium/ILineContainer.Stadium.cs b/UnityPackage/Runtime/Shapes/Stadium/ILineContainer.Stadium.cs
deleted file mode 100644
index 7986250..0000000
--- a/UnityPackage/Runtime/Shapes/Stadium/ILineContainer.Stadium.cs
+++ /dev/null
@@ -1,68 +0,0 @@
-using UnityEngine;
-
-namespace reromanlee.Wireframes
-{
- public partial interface ILineContainer
- {
- ///
- /// Creates a white stadium lying flat in the XZ plane, around circles of radius 0.5 centered on the world origin
- /// and 1 unit along +Z.
- ///
- IStadium CreateStadium();
-
- ///
- /// Creates a white stadium around two circles of the same radius, centered on two world positions. It lies as
- /// flat as it can: its normal stays as close to world up as the centers allow.
- ///
- /// Number of straight pieces a full circle would be drawn with, a positive multiple of 4.
- IStadium CreateStadium(Vector3 centerA, Vector3 centerB, float radius, int segments = Ring.DefaultSegments);
-
- ///
- /// Creates a white stadium around two circles centered on two world positions, each with its own radius. It lies
- /// as flat as it can: its normal stays as close to world up as the centers allow.
- ///
- /// Number of straight pieces a full circle would be drawn with, a positive multiple of 4.
- IStadium CreateStadium(
- Vector3 centerA, Vector3 centerB, float radiusA, float radiusB, int segments = Ring.DefaultSegments);
-
- ///
- /// Creates a white stadium around two circles centered on two world positions, facing as close to
- /// as the centers allow.
- ///
- /// Number of straight pieces a full circle would be drawn with, a positive multiple of 4.
- IStadium CreateStadium(
- Vector3 centerA,
- Vector3 centerB,
- float radiusA,
- float radiusB,
- Vector3 normal,
- int segments = Ring.DefaultSegments);
-
- ///
- /// Creates a white stadium that follows , around two circles centered on positions in the
- /// bone's local space. It lies as flat in the bone's XZ plane as the centers allow.
- ///
- /// Number of straight pieces a full circle would be drawn with, a positive multiple of 4.
- IStadium CreateStadium(
- Transform bone,
- Vector3 localCenterA,
- Vector3 localCenterB,
- float radiusA,
- float radiusB,
- int segments = Ring.DefaultSegments);
-
- ///
- /// Creates a white stadium in world space, lying in the XZ plane of , whose first
- /// circle is centered on and whose second is centered
- /// along the rotation's +Z axis.
- ///
- /// Number of straight pieces a full circle would be drawn with, a positive multiple of 4.
- IStadium CreateStadium(
- Vector3 position,
- Quaternion rotation,
- float length,
- float radiusA,
- float radiusB,
- int segments = Ring.DefaultSegments);
- }
-}
diff --git a/UnityPackage/Runtime/Shapes/Stadium/ILineContainer.Stadium.cs.meta b/UnityPackage/Runtime/Shapes/Stadium/ILineContainer.Stadium.cs.meta
deleted file mode 100644
index 0ad649a..0000000
--- a/UnityPackage/Runtime/Shapes/Stadium/ILineContainer.Stadium.cs.meta
+++ /dev/null
@@ -1,2 +0,0 @@
-fileFormatVersion: 2
-guid: b521760b3eb0215a705f55082fa72623
\ No newline at end of file
diff --git a/UnityPackage/Runtime/Shapes/Stadium/IStadium.cs b/UnityPackage/Runtime/Shapes/Stadium/IStadium.cs
index 241cb6b..318a710 100644
--- a/UnityPackage/Runtime/Shapes/Stadium/IStadium.cs
+++ b/UnityPackage/Runtime/Shapes/Stadium/IStadium.cs
@@ -15,6 +15,6 @@ public interface IStadium : IAxialShape
///
/// Number of straight pieces a full circle would be drawn with, fixed at creation. Each end is half of them.
///
- int Segments { get; }
+ int SegmentCount { get; }
}
}
diff --git a/UnityPackage/Runtime/Shapes/Stadium/LineContainer.Stadium.cs b/UnityPackage/Runtime/Shapes/Stadium/LineContainer.Stadium.cs
deleted file mode 100644
index 24d27ab..0000000
--- a/UnityPackage/Runtime/Shapes/Stadium/LineContainer.Stadium.cs
+++ /dev/null
@@ -1,61 +0,0 @@
-using UnityEngine;
-
-namespace reromanlee.Wireframes
-{
- public sealed partial class LineContainer
- {
- public IStadium CreateStadium()
- {
- return new Stadium(
- Proxy, null, Vector3.zero, Quaternion.identity, UnitSize, UnitRadius, UnitRadius, Ring.DefaultSegments);
- }
-
- public IStadium CreateStadium(Vector3 centerA, Vector3 centerB, float radius, int segments = Ring.DefaultSegments)
- {
- return CreateStadium(centerA, centerB, radius, radius, Vector3.up, segments);
- }
-
- public IStadium CreateStadium(
- Vector3 centerA, Vector3 centerB, float radiusA, float radiusB, int segments = Ring.DefaultSegments)
- {
- return CreateStadium(centerA, centerB, radiusA, radiusB, Vector3.up, segments);
- }
-
- public IStadium CreateStadium(
- Vector3 centerA,
- Vector3 centerB,
- float radiusA,
- float radiusB,
- Vector3 normal,
- int segments = Ring.DefaultSegments)
- {
- Quaternion rotation = ShapeRotations.FromAxis(centerB - centerA, normal);
- float length = Vector3.Distance(centerA, centerB);
- return new Stadium(Proxy, null, centerA, rotation, length, radiusA, radiusB, segments);
- }
-
- public IStadium CreateStadium(
- Transform bone,
- Vector3 localCenterA,
- Vector3 localCenterB,
- float radiusA,
- float radiusB,
- int segments = Ring.DefaultSegments)
- {
- Quaternion rotation = ShapeRotations.FromAxis(localCenterB - localCenterA, Vector3.up);
- float length = Vector3.Distance(localCenterA, localCenterB);
- return new Stadium(Proxy, bone, localCenterA, rotation, length, radiusA, radiusB, segments);
- }
-
- public IStadium CreateStadium(
- Vector3 position,
- Quaternion rotation,
- float length,
- float radiusA,
- float radiusB,
- int segments = Ring.DefaultSegments)
- {
- return new Stadium(Proxy, null, position, rotation, length, radiusA, radiusB, segments);
- }
- }
-}
diff --git a/UnityPackage/Runtime/Shapes/Stadium/Stadium.cs b/UnityPackage/Runtime/Shapes/Stadium/Stadium.cs
index 2be4bbe..709c3b9 100644
--- a/UnityPackage/Runtime/Shapes/Stadium/Stadium.cs
+++ b/UnityPackage/Runtime/Shapes/Stadium/Stadium.cs
@@ -11,19 +11,17 @@ internal sealed class Stadium : AxialShape, IStadium
private float _radiusB;
internal Stadium(
- MeshProxy proxy,
Transform bone,
Vector3 localPosition,
Quaternion localRotation,
float length,
float radiusA,
float radiusB,
- int segments)
+ int segmentCount)
: base(
- proxy,
- Ring.CheckQuarterSegments(segments) + ArcCount,
+ Ring.CheckQuarterSegmentCount(segmentCount) + ArcCount,
// One closed loop: arc A, the -X side, arc B and the +X side back to the start.
- Ring.Patterns.Get(segments + ArcCount),
+ Ring.Patterns.Get(segmentCount + ArcCount),
bone,
localPosition,
localRotation,
@@ -37,12 +35,12 @@ public float RadiusA
{
get
{
- ThrowIfDisposed();
+ EnsureUsable();
return _radiusA;
}
set
{
- ThrowIfDisposed();
+ EnsureUsable();
_radiusA = value;
MarkDirty(DirtyFlags.Positions);
}
@@ -52,22 +50,22 @@ public float RadiusB
{
get
{
- ThrowIfDisposed();
+ EnsureUsable();
return _radiusB;
}
set
{
- ThrowIfDisposed();
+ EnsureUsable();
_radiusB = value;
MarkDirty(DirtyFlags.Positions);
}
}
- public int Segments
+ public int SegmentCount
{
get
{
- ThrowIfDisposed();
+ EnsureUsable();
return VertexCount - ArcCount;
}
}
diff --git a/UnityPackage/Runtime/Shapes/Stadium/StadiumFactory.cs b/UnityPackage/Runtime/Shapes/Stadium/StadiumFactory.cs
new file mode 100644
index 0000000..6eb13a4
--- /dev/null
+++ b/UnityPackage/Runtime/Shapes/Stadium/StadiumFactory.cs
@@ -0,0 +1,115 @@
+using UnityEngine;
+
+namespace reromanlee.Wireframes
+{
+ /// Creates stadiums, the flat outlines of capsules, in a .
+ public static class StadiumFactory
+ {
+ ///
+ /// Creates a white stadium lying flat in the XZ plane, around circles of radius 0.5 centered on the world origin
+ /// and 1 unit along +Z.
+ ///
+ public static IStadium CreateStadium(this WireframeContainer container)
+ {
+ MeshProxy proxy = WireframeContainer.ProxyOf(container);
+ return proxy.Add(new Stadium(
+ null,
+ Vector3.zero,
+ Quaternion.identity,
+ ShapeDefaults.Size,
+ ShapeDefaults.Radius,
+ ShapeDefaults.Radius,
+ Ring.DefaultSegmentCount));
+ }
+
+ ///
+ /// Creates a white stadium around two circles of the same radius, centered on two world positions. It lies as
+ /// flat as it can: its normal stays as close to world up as the centers allow.
+ ///
+ /// Number of straight pieces a full circle would be drawn with, a multiple of 4 from 4 to 1024.
+ public static IStadium CreateStadium(
+ this WireframeContainer container,
+ Vector3 centerA,
+ Vector3 centerB,
+ float radius,
+ int segmentCount = Ring.DefaultSegmentCount)
+ {
+ return container.CreateStadium(centerA, centerB, radius, radius, Vector3.up, segmentCount);
+ }
+
+ ///
+ /// Creates a white stadium around two circles centered on two world positions, each with its own radius. It lies
+ /// as flat as it can: its normal stays as close to world up as the centers allow.
+ ///
+ /// Number of straight pieces a full circle would be drawn with, a multiple of 4 from 4 to 1024.
+ public static IStadium CreateStadium(
+ this WireframeContainer container,
+ Vector3 centerA,
+ Vector3 centerB,
+ float radiusA,
+ float radiusB,
+ int segmentCount = Ring.DefaultSegmentCount)
+ {
+ return container.CreateStadium(centerA, centerB, radiusA, radiusB, Vector3.up, segmentCount);
+ }
+
+ ///
+ /// Creates a white stadium around two circles centered on two world positions, facing as close to
+ /// as the centers allow.
+ ///
+ /// Number of straight pieces a full circle would be drawn with, a multiple of 4 from 4 to 1024.
+ public static IStadium CreateStadium(
+ this WireframeContainer container,
+ Vector3 centerA,
+ Vector3 centerB,
+ float radiusA,
+ float radiusB,
+ Vector3 normal,
+ int segmentCount = Ring.DefaultSegmentCount)
+ {
+ MeshProxy proxy = WireframeContainer.ProxyOf(container);
+ Quaternion rotation = ShapeRotations.FromAxis(centerB - centerA, normal);
+ float length = Vector3.Distance(centerA, centerB);
+ return proxy.Add(new Stadium(null, centerA, rotation, length, radiusA, radiusB, segmentCount));
+ }
+
+ ///
+ /// Creates a white stadium that follows , around two circles centered on positions in the
+ /// bone's local space. It lies as flat in the bone's XZ plane as the centers allow.
+ ///
+ /// Number of straight pieces a full circle would be drawn with, a multiple of 4 from 4 to 1024.
+ public static IStadium CreateStadium(
+ this WireframeContainer container,
+ Transform bone,
+ Vector3 localCenterA,
+ Vector3 localCenterB,
+ float radiusA,
+ float radiusB,
+ int segmentCount = Ring.DefaultSegmentCount)
+ {
+ MeshProxy proxy = WireframeContainer.ProxyOf(container);
+ Quaternion rotation = ShapeRotations.FromAxis(localCenterB - localCenterA, Vector3.up);
+ float length = Vector3.Distance(localCenterA, localCenterB);
+ return proxy.Add(new Stadium(bone, localCenterA, rotation, length, radiusA, radiusB, segmentCount));
+ }
+
+ ///
+ /// Creates a white stadium in world space, lying in the XZ plane of , whose first
+ /// circle is centered on and whose second is centered
+ /// along the rotation's +Z axis.
+ ///
+ /// Number of straight pieces a full circle would be drawn with, a multiple of 4 from 4 to 1024.
+ public static IStadium CreateStadium(
+ this WireframeContainer container,
+ Vector3 position,
+ Quaternion rotation,
+ float length,
+ float radiusA,
+ float radiusB,
+ int segmentCount = Ring.DefaultSegmentCount)
+ {
+ MeshProxy proxy = WireframeContainer.ProxyOf(container);
+ return proxy.Add(new Stadium(null, position, rotation, length, radiusA, radiusB, segmentCount));
+ }
+ }
+}
diff --git a/UnityPackage/Runtime/Shapes/Stadium/LineContainer.Stadium.cs.meta b/UnityPackage/Runtime/Shapes/Stadium/StadiumFactory.cs.meta
similarity index 100%
rename from UnityPackage/Runtime/Shapes/Stadium/LineContainer.Stadium.cs.meta
rename to UnityPackage/Runtime/Shapes/Stadium/StadiumFactory.cs.meta
diff --git a/UnityPackage/Runtime/Shapes/Star/ILineContainer.Star.cs b/UnityPackage/Runtime/Shapes/Star/ILineContainer.Star.cs
deleted file mode 100644
index 1048e33..0000000
--- a/UnityPackage/Runtime/Shapes/Star/ILineContainer.Star.cs
+++ /dev/null
@@ -1,34 +0,0 @@
-using UnityEngine;
-
-namespace reromanlee.Wireframes
-{
- public partial interface ILineContainer
- {
- ///
- /// Creates a white five-pointed star around the world origin, lying flat in the XZ plane, with an outer radius
- /// of 0.5 and an inner radius of 0.2.
- ///
- IStar CreateStar();
-
- ///
- /// Creates a white star around a world position, lying flat in the world's XZ plane with its first point along
- /// +Z.
- ///
- /// Number of points, at least 3.
- IStar CreateStar(Vector3 center, float innerRadius, float outerRadius, int points);
-
- ///
- /// Creates a white star that follows , around a position in the bone's local space and
- /// lying flat in the bone's XZ plane with its first point along the bone's +Z.
- ///
- /// Number of points, at least 3.
- IStar CreateStar(Transform bone, Vector3 localCenter, float innerRadius, float outerRadius, int points);
-
- ///
- /// Creates a white star in world space, lying in the XZ plane of with its first point
- /// along the rotation's +Z.
- ///
- /// Number of points, at least 3.
- IStar CreateStar(Vector3 center, Quaternion rotation, float innerRadius, float outerRadius, int points);
- }
-}
diff --git a/UnityPackage/Runtime/Shapes/Star/ILineContainer.Star.cs.meta b/UnityPackage/Runtime/Shapes/Star/ILineContainer.Star.cs.meta
deleted file mode 100644
index a598bf1..0000000
--- a/UnityPackage/Runtime/Shapes/Star/ILineContainer.Star.cs.meta
+++ /dev/null
@@ -1,2 +0,0 @@
-fileFormatVersion: 2
-guid: d2f4b5290b36cb3a2929121260b9f0dc
\ No newline at end of file
diff --git a/UnityPackage/Runtime/Shapes/Star/LineContainer.Star.cs b/UnityPackage/Runtime/Shapes/Star/LineContainer.Star.cs
deleted file mode 100644
index 915fb4e..0000000
--- a/UnityPackage/Runtime/Shapes/Star/LineContainer.Star.cs
+++ /dev/null
@@ -1,31 +0,0 @@
-using UnityEngine;
-
-namespace reromanlee.Wireframes
-{
- public sealed partial class LineContainer
- {
- private const float UnitStarInnerRadius = 0.2f;
- private const int UnitStarPoints = 5;
-
- public IStar CreateStar()
- {
- return new Star(
- Proxy, null, Vector3.zero, Quaternion.identity, UnitStarInnerRadius, UnitRadius, UnitStarPoints);
- }
-
- public IStar CreateStar(Vector3 center, float innerRadius, float outerRadius, int points)
- {
- return new Star(Proxy, null, center, Quaternion.identity, innerRadius, outerRadius, points);
- }
-
- public IStar CreateStar(Transform bone, Vector3 localCenter, float innerRadius, float outerRadius, int points)
- {
- return new Star(Proxy, bone, localCenter, Quaternion.identity, innerRadius, outerRadius, points);
- }
-
- public IStar CreateStar(Vector3 center, Quaternion rotation, float innerRadius, float outerRadius, int points)
- {
- return new Star(Proxy, null, center, rotation, innerRadius, outerRadius, points);
- }
- }
-}
diff --git a/UnityPackage/Runtime/Shapes/Star/Star.cs b/UnityPackage/Runtime/Shapes/Star/Star.cs
index cfb88d5..f6b2a67 100644
--- a/UnityPackage/Runtime/Shapes/Star/Star.cs
+++ b/UnityPackage/Runtime/Shapes/Star/Star.cs
@@ -5,18 +5,19 @@ namespace reromanlee.Wireframes
{
internal sealed class Star : RigidShape, IStar
{
+ internal const int MaxPointCount = Ring.MaxSegmentCount / 2;
+
private float _innerRadius;
private float _outerRadius;
internal Star(
- MeshProxy proxy,
Transform bone,
Vector3 localCenter,
Quaternion localRotation,
float innerRadius,
float outerRadius,
- int points)
- : base(proxy, CountVertices(points), Ring.Patterns.Get(points * 2), bone, localCenter, localRotation)
+ int pointCount)
+ : base(CountVertices(pointCount), Ring.Patterns.Get(pointCount * 2), bone, localCenter, localRotation)
{
_innerRadius = innerRadius;
_outerRadius = outerRadius;
@@ -26,12 +27,12 @@ public float InnerRadius
{
get
{
- ThrowIfDisposed();
+ EnsureUsable();
return _innerRadius;
}
set
{
- ThrowIfDisposed();
+ EnsureUsable();
_innerRadius = value;
MarkDirty(DirtyFlags.Positions);
}
@@ -41,12 +42,12 @@ public float OuterRadius
{
get
{
- ThrowIfDisposed();
+ EnsureUsable();
return _outerRadius;
}
set
{
- ThrowIfDisposed();
+ EnsureUsable();
_outerRadius = value;
MarkDirty(DirtyFlags.Positions);
}
@@ -56,7 +57,7 @@ public int PointCount
{
get
{
- ThrowIfDisposed();
+ EnsureUsable();
return VertexCount / 2;
}
}
@@ -64,11 +65,12 @@ public int PointCount
protected override void WriteShape(Span positions)
{
// A ring that alternates between the tips and the corners between them, starting with a tip on +Z.
- Vector2[] circle = Ring.UnitCircle(positions.Length);
+ CirclePoints circle = new(positions.Length);
for (int i = 0; i < positions.Length; i++)
{
+ Vector2 point = circle.Next();
float radius = i % 2 == 0 ? _outerRadius : _innerRadius;
- positions[i] = new Vector3(circle[i].y * radius, 0f, circle[i].x * radius);
+ positions[i] = new Vector3(point.y * radius, 0f, point.x * radius);
}
}
@@ -78,13 +80,15 @@ protected override void ScaleSizes(float factor)
_outerRadius *= factor;
}
- private static int CountVertices(int points)
+ private static int CountVertices(int pointCount)
{
- if (points < 3)
+ // Its tips and corners make one ring.
+ if (pointCount < 3 || pointCount > MaxPointCount)
{
- throw new ArgumentOutOfRangeException(nameof(points), points, "A star needs at least 3 points.");
+ throw new ArgumentOutOfRangeException(
+ nameof(pointCount), pointCount, $"A star needs 3 to {MaxPointCount} points.");
}
- return points * 2;
+ return pointCount * 2;
}
}
}
diff --git a/UnityPackage/Runtime/Shapes/Star/StarFactory.cs b/UnityPackage/Runtime/Shapes/Star/StarFactory.cs
new file mode 100644
index 0000000..cf33301
--- /dev/null
+++ b/UnityPackage/Runtime/Shapes/Star/StarFactory.cs
@@ -0,0 +1,68 @@
+using UnityEngine;
+
+namespace reromanlee.Wireframes
+{
+ /// Creates stars in a .
+ public static class StarFactory
+ {
+ private const float DefaultInnerRadius = 0.2f;
+ private const int DefaultPointCount = 5;
+
+ ///
+ /// Creates a white five-pointed star around the world origin, lying flat in the XZ plane, with an outer radius
+ /// of 0.5 and an inner radius of 0.2.
+ ///
+ public static IStar CreateStar(this WireframeContainer container)
+ {
+ MeshProxy proxy = WireframeContainer.ProxyOf(container);
+ return proxy.Add(new Star(
+ null, Vector3.zero, Quaternion.identity, DefaultInnerRadius, ShapeDefaults.Radius, DefaultPointCount));
+ }
+
+ ///
+ /// Creates a white star around a world position, lying flat in the world's XZ plane with its first point along
+ /// +Z.
+ ///
+ /// Number of points, from 3 to 512.
+ public static IStar CreateStar(
+ this WireframeContainer container, Vector3 center, float innerRadius, float outerRadius, int pointCount)
+ {
+ MeshProxy proxy = WireframeContainer.ProxyOf(container);
+ return proxy.Add(new Star(null, center, Quaternion.identity, innerRadius, outerRadius, pointCount));
+ }
+
+ ///
+ /// Creates a white star that follows , around a position in the bone's local space and
+ /// lying flat in the bone's XZ plane with its first point along the bone's +Z.
+ ///
+ /// Number of points, from 3 to 512.
+ public static IStar CreateStar(
+ this WireframeContainer container,
+ Transform bone,
+ Vector3 localCenter,
+ float innerRadius,
+ float outerRadius,
+ int pointCount)
+ {
+ MeshProxy proxy = WireframeContainer.ProxyOf(container);
+ return proxy.Add(new Star(bone, localCenter, Quaternion.identity, innerRadius, outerRadius, pointCount));
+ }
+
+ ///
+ /// Creates a white star in world space, lying in the XZ plane of with its first point
+ /// along the rotation's +Z.
+ ///
+ /// Number of points, from 3 to 512.
+ public static IStar CreateStar(
+ this WireframeContainer container,
+ Vector3 center,
+ Quaternion rotation,
+ float innerRadius,
+ float outerRadius,
+ int pointCount)
+ {
+ MeshProxy proxy = WireframeContainer.ProxyOf(container);
+ return proxy.Add(new Star(null, center, rotation, innerRadius, outerRadius, pointCount));
+ }
+ }
+}
diff --git a/UnityPackage/Runtime/Shapes/Star/LineContainer.Star.cs.meta b/UnityPackage/Runtime/Shapes/Star/StarFactory.cs.meta
similarity index 100%
rename from UnityPackage/Runtime/Shapes/Star/LineContainer.Star.cs.meta
rename to UnityPackage/Runtime/Shapes/Star/StarFactory.cs.meta
diff --git a/UnityPackage/Runtime/reromanlee.Wireframes.asmdef b/UnityPackage/Runtime/reromanlee.Wireframes.asmdef
index d516564..fac2a60 100644
--- a/UnityPackage/Runtime/reromanlee.Wireframes.asmdef
+++ b/UnityPackage/Runtime/reromanlee.Wireframes.asmdef
@@ -1,7 +1,9 @@
{
"name": "reromanlee.Wireframes",
"rootNamespace": "reromanlee.Wireframes",
- "references": [],
+ "references": [
+ "Unity.Profiling.Core"
+ ],
"includePlatforms": [],
"excludePlatforms": [],
"allowUnsafeCode": false,
@@ -9,6 +11,12 @@
"precompiledReferences": [],
"autoReferenced": true,
"defineConstraints": [],
- "versionDefines": [],
+ "versionDefines": [
+ {
+ "name": "com.unity.profiling.core",
+ "expression": "1.0.0",
+ "define": "WIREFRAMES_PROFILING_CORE"
+ }
+ ],
"noEngineReferences": false
}
\ No newline at end of file
diff --git a/UnityPackage/Samples~/ShapeGallery/ShapeGallery.cs b/UnityPackage/Samples~/ShapeGallery/ShapeGallery.cs
index 1057374..b2ef7d0 100644
--- a/UnityPackage/Samples~/ShapeGallery/ShapeGallery.cs
+++ b/UnityPackage/Samples~/ShapeGallery/ShapeGallery.cs
@@ -18,12 +18,12 @@ public sealed class ShapeGallery : MonoBehaviour
private readonly List _bones = new();
private readonly List _names = new();
- private LineContainer _container;
+ private WireframeContainer _container;
private GUIStyle _nameStyle;
private void Start()
{
- _container = new LineContainer();
+ _container = new WireframeContainer();
// Flat shapes lie in their bone's XZ plane, so their bones are tipped over to face the camera.
Quaternion facing = Quaternion.Euler(-90f, 0f, 0f);
Quaternion tilted = Quaternion.Euler(-20f, 0f, 0f);
diff --git a/UnityPackage/Samples~/ShapeGallery/ShapeGallery.unity b/UnityPackage/Samples~/ShapeGallery/ShapeGallery.unity
index fc2b27e..088f9c8 100644
--- a/UnityPackage/Samples~/ShapeGallery/ShapeGallery.unity
+++ b/UnityPackage/Samples~/ShapeGallery/ShapeGallery.unity
@@ -13,7 +13,7 @@ OcclusionCullingSettings:
--- !u!104 &2
RenderSettings:
m_ObjectHideFlags: 0
- serializedVersion: 10
+ serializedVersion: 9
m_Fog: 0
m_FogColor: {r: 0.5, g: 0.5, b: 0.5, a: 1}
m_FogMode: 3
@@ -42,8 +42,8 @@ RenderSettings:
--- !u!157 &3
LightmapSettings:
m_ObjectHideFlags: 0
- serializedVersion: 13
- m_BakeOnSceneLoad: 0
+ serializedVersion: 12
+ m_GIWorkflowMode: 1
m_GISettings:
serializedVersion: 2
m_BounceScale: 1
@@ -66,6 +66,9 @@ LightmapSettings:
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
diff --git a/UnityPackage/Samples~/StressTest/StressTest.cs b/UnityPackage/Samples~/StressTest/StressTest.cs
index 0743b9d..598a14a 100644
--- a/UnityPackage/Samples~/StressTest/StressTest.cs
+++ b/UnityPackage/Samples~/StressTest/StressTest.cs
@@ -24,7 +24,7 @@ public sealed class StressTest : MonoBehaviour
[Tooltip("Lines recolored every frame, to measure the cost of edits.")]
[SerializeField, Min(0)] private int _recolorPerFrame;
- private LineContainer _container;
+ private WireframeContainer _container;
private Transform[] _bones;
private Vector3[] _orbitAxes;
private ILine[] _lines;
@@ -33,7 +33,7 @@ public sealed class StressTest : MonoBehaviour
private void Start()
{
- _container = new LineContainer();
+ _container = new WireframeContainer();
_bones = new Transform[_boneCount];
_orbitAxes = new Vector3[_boneCount];
diff --git a/UnityPackage/Tests/Editor/BoneValidationTests.cs b/UnityPackage/Tests/Editor/BoneValidationTests.cs
new file mode 100644
index 0000000..ef7c551
--- /dev/null
+++ b/UnityPackage/Tests/Editor/BoneValidationTests.cs
@@ -0,0 +1,62 @@
+using System;
+using NUnit.Framework;
+using UnityEditor;
+using UnityEngine;
+using Object = UnityEngine.Object;
+
+namespace reromanlee.Wireframes.Tests
+{
+ /// Bones must be scene objects: a prefab asset never moves, so following one is always a mistake.
+ public class BoneValidationTests
+ {
+ private const string PrefabPath = "Assets/WireframesBoneValidationTest.prefab";
+
+ private WireframeContainer _container;
+ private Transform _prefabBone;
+ private GameObject _sceneBone;
+
+ [SetUp]
+ public void CreateBones()
+ {
+ GameObject source = new("Prefab Bone");
+ _prefabBone = PrefabUtility.SaveAsPrefabAsset(source, PrefabPath).transform;
+ Object.DestroyImmediate(source);
+ _sceneBone = new GameObject("Scene Bone");
+ _container = new WireframeContainer();
+ }
+
+ [TearDown]
+ public void DestroyBones()
+ {
+ _container.Dispose();
+ Object.DestroyImmediate(_sceneBone);
+ AssetDatabase.DeleteAsset(PrefabPath);
+ }
+
+ [Test]
+ public void CreatingAShapeOnAPrefabAsset_ThrowsAndLeavesNothingBehind()
+ {
+ Assert.Throws(() => _container.CreateCircle(_prefabBone, Vector3.zero, 1f));
+ ArgumentException exception = Assert.Throws(
+ () => _container.CreateLine(_sceneBone.transform, _prefabBone));
+ Assert.Throws(() => _container.CreatePolyline(_sceneBone.transform, _prefabBone));
+
+ Assert.That(exception.ParamName, Is.EqualTo("boneB"));
+ Assert.That(_container.Proxy.Chunks, Is.Empty);
+ Assert.That(_container.Proxy.Bones.Count, Is.Zero);
+ }
+
+ [Test]
+ public void SettingAPrefabAssetAsBone_ThrowsAndKeepsTheBone()
+ {
+ ICircle circle = _container.CreateCircle(_sceneBone.transform, Vector3.zero, 1f);
+ ILine line = _container.CreateLine();
+
+ Assert.Throws(() => circle.Bone = _prefabBone);
+ Assert.Throws(() => line.BoneA = _prefabBone);
+
+ Assert.That(circle.Bone, Is.SameAs(_sceneBone.transform));
+ Assert.That(line.BoneA, Is.Null);
+ }
+ }
+}
diff --git a/UnityPackage/Tests/Editor/BoneValidationTests.cs.meta b/UnityPackage/Tests/Editor/BoneValidationTests.cs.meta
new file mode 100644
index 0000000..55588f7
--- /dev/null
+++ b/UnityPackage/Tests/Editor/BoneValidationTests.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 1314ed380c2017ba6c34aa602c822edb
\ No newline at end of file
diff --git a/UnityPackage/Tests/Editor/CirclePointsTests.cs b/UnityPackage/Tests/Editor/CirclePointsTests.cs
new file mode 100644
index 0000000..d89c351
--- /dev/null
+++ b/UnityPackage/Tests/Editor/CirclePointsTests.cs
@@ -0,0 +1,34 @@
+using System;
+using NUnit.Framework;
+using UnityEngine;
+
+namespace reromanlee.Wireframes.Tests
+{
+ public class CirclePointsTests
+ {
+ [Test]
+ public void EveryPoint_MatchesItsCosineAndSine([Values(3, 4, 32, 1000, 1024)] int count)
+ {
+ CirclePoints circle = new(count);
+
+ for (int i = 0; i < count; i++)
+ {
+ double angle = 2.0 * Math.PI * i / count;
+ Vector2 point = circle.Next();
+ Assert.That(point.x, Is.EqualTo((float)Math.Cos(angle)).Within(1e-6f), $"Point {i} of {count}");
+ Assert.That(point.y, Is.EqualTo((float)Math.Sin(angle)).Within(1e-6f), $"Point {i} of {count}");
+ }
+ }
+
+ [Test]
+ public void First_StartsPartWayAround()
+ {
+ CirclePoints circle = new(8, 2);
+
+ Vector2 point = circle.Next();
+
+ Assert.That(point.x, Is.EqualTo(0f).Within(1e-6f));
+ Assert.That(point.y, Is.EqualTo(1f).Within(1e-6f));
+ }
+ }
+}
diff --git a/UnityPackage/Tests/Editor/CirclePointsTests.cs.meta b/UnityPackage/Tests/Editor/CirclePointsTests.cs.meta
new file mode 100644
index 0000000..bc544e9
--- /dev/null
+++ b/UnityPackage/Tests/Editor/CirclePointsTests.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: c19f70acab4bf95674777385dc8286c6
\ No newline at end of file
diff --git a/UnityPackage/Tests/Editor/DirtyRangesTests.cs b/UnityPackage/Tests/Editor/DirtyRangesTests.cs
index f69da2c..c1649a8 100644
--- a/UnityPackage/Tests/Editor/DirtyRangesTests.cs
+++ b/UnityPackage/Tests/Editor/DirtyRangesTests.cs
@@ -34,6 +34,37 @@ public void Merge_CollapsesManyScatteredRangesIntoOne()
Assert.That(ranges[0].length, Is.EqualTo(39002));
}
+ [Test]
+ public void Add_WithoutFlushes_KeepsTheListSmallAndCoversEveryRange()
+ {
+ DirtyRanges ranges = new();
+ for (int i = 0; i < 100000; i++)
+ {
+ ranges.Add(i * 1000, 2);
+ }
+
+ Assert.That(ranges.Count, Is.LessThanOrEqualTo(256));
+ Assert.That(ranges.Merge(), Is.EqualTo(1));
+ Assert.That(ranges[0].start, Is.Zero);
+ Assert.That(ranges[0].end, Is.EqualTo(99999 * 1000 + 2));
+ }
+
+ [Test]
+ public void Clear_EndsTheCoveringRange()
+ {
+ DirtyRanges ranges = new();
+ for (int i = 0; i < 1000; i++)
+ {
+ ranges.Add(i * 1000, 2);
+ }
+
+ ranges.Clear();
+ ranges.Add(0, 2);
+ ranges.Add(50000, 2);
+
+ Assert.That(ranges.Merge(), Is.EqualTo(2));
+ }
+
[Test]
public void Add_IgnoresEmptyRanges()
{
diff --git a/UnityPackage/Tests/Editor/EdgeListTests.cs b/UnityPackage/Tests/Editor/EdgeListTests.cs
index 229d46a..f28cc57 100644
--- a/UnityPackage/Tests/Editor/EdgeListTests.cs
+++ b/UnityPackage/Tests/Editor/EdgeListTests.cs
@@ -17,7 +17,7 @@ public void OnEdgeMoved(int edge, int slot)
[Test]
public void RemoveAt_MovesLastEdgeIntoFreedSlot()
{
- EdgeList edges = new();
+ EdgeList edges = new(4);
Owner first = new();
Owner second = new();
first.Slots[0] = edges.Add(first, 0);
@@ -38,7 +38,7 @@ public void RemoveAt_MovesLastEdgeIntoFreedSlot()
[Test]
public void RemoveAt_LastSlotMovesNothing()
{
- EdgeList edges = new();
+ EdgeList edges = new(4);
Owner owner = new();
edges.Add(owner, 0);
int last = edges.Add(owner, 1);
@@ -51,7 +51,7 @@ public void RemoveAt_LastSlotMovesNothing()
public void RemovingEveryEdgeOfOneOwner_LeavesOtherOwnersIntact()
{
// Mirrors MeshChunk.Remove: an owner's later edge can be moved into one of its own earlier slots.
- EdgeList edges = new();
+ EdgeList edges = new(4);
Owner removed = new();
Owner kept = new();
for (int edge = 0; edge < 3; edge++)
@@ -75,7 +75,7 @@ public void RemovingEveryEdgeOfOneOwner_LeavesOtherOwnersIntact()
[Test]
public void EnsureCapacity_DoublesAndKeepsEdges()
{
- EdgeList edges = new();
+ EdgeList edges = new(4);
Owner owner = new();
int slot = edges.Add(owner, 0);
edges.Set(slot, 3, 4);
@@ -88,5 +88,25 @@ public void EnsureCapacity_DoublesAndKeepsEdges()
Assert.That(edges.Indices[0], Is.EqualTo(3));
Assert.That(edges.Indices[1], Is.EqualTo(4));
}
+
+ [Test]
+ public void Shrink_KeepsEdgesAndNeverDropsBelowTheirCount()
+ {
+ EdgeList edges = new(64);
+ Owner owner = new();
+ for (int edge = 0; edge < 3; edge++)
+ {
+ edges.Set(edges.Add(owner, edge), edge, edge + 1);
+ }
+
+ edges.Shrink(8);
+ Assert.That(edges.Capacity, Is.EqualTo(8));
+ Assert.That(edges.Indices.Length, Is.EqualTo(16));
+ Assert.That(edges.Indices[4], Is.EqualTo(2));
+ Assert.That(edges.Indices[5], Is.EqualTo(3));
+
+ edges.Shrink(1);
+ Assert.That(edges.Capacity, Is.EqualTo(3));
+ }
}
}
diff --git a/UnityPackage/Tests/Editor/EditModeTests.cs b/UnityPackage/Tests/Editor/EditModeTests.cs
new file mode 100644
index 0000000..8b0d5f7
--- /dev/null
+++ b/UnityPackage/Tests/Editor/EditModeTests.cs
@@ -0,0 +1,244 @@
+using System.Collections;
+using System.Collections.Generic;
+using NUnit.Framework;
+using reromanlee.Wireframes.Editor;
+using UnityEditor;
+using UnityEditor.SceneManagement;
+using UnityEngine;
+using UnityEngine.SceneManagement;
+using UnityEngine.TestTools;
+using Object = UnityEngine.Object;
+
+namespace reromanlee.Wireframes.Tests
+{
+ /// Containers in Edit Mode: drawn, never saved, and disposed with their scene or before scripts reload.
+ 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