diff --git a/.github/RELEASE_CHECKLIST.md b/.github/RELEASE_CHECKLIST.md index 675e497..1d10141 100644 --- a/.github/RELEASE_CHECKLIST.md +++ b/.github/RELEASE_CHECKLIST.md @@ -7,11 +7,12 @@ Continuous integration checks what a machine without a GPU can: the tests on the 1. Every check of the pull request passes: the tests on both Unity versions, and the WebGL, Android, iOS, macOS and Linux builds. 2. The whole test suite passes locally on a real GPU, on the oldest and the newest supported Unity. 3. The Stress Test sample runs smoothly in Windows, Android and WebGL builds, with frame, main thread and GPU times no worse than the previous release's. -4. Both scenes of the Shape Gallery sample and all three occlusion modes look right on the same devices, in the Built-in, URP and HDRP pipelines. +4. All three scenes of the Shape Gallery sample and all three occlusion modes look right on the same devices, in the Built-in, URP and HDRP pipelines. 5. In the Editor, under each pipeline, shape components draw in the Scene and Game views and in Prefab Mode, Draw As Gizmo shapes show only in the Scene view and in the Game view with Gizmos on, and clicking a wireframe never selects a hidden container. -6. A Profiler capture of an Android development build under load shows no new spikes in the `Wireframes.*` markers and no allocations in steady frames. -7. The timings in the test results artifacts are no worse than the previous release's. -8. `package.json` has the new version, `CHANGELOG.md` has its section, and the README and `Documentation/` describe the release; a major release also updates `Documentation/MIGRATION.md`. +6. In the Editor, with the package installed from git, the Glyph Editor shows the Default Font and Default Symbols read-only and duplicates them to Assets. On the copy, editing glyphs with the mouse, the keyboard and the details panel, undo, Generate Enum and Ctrl+S all work, and a text drawing with the copy follows every edit. +7. A Profiler capture of an Android development build under load shows no new spikes in the `Wireframes.*` markers and no allocations in steady frames. +8. The timings in the test results artifacts are no worse than the previous release's. +9. `package.json` has the new version, `CHANGELOG.md` has its section, and the README and `Documentation/` describe the release; a major release also updates `Documentation/MIGRATION.md`. ### After merging diff --git a/.github/glyph-editor-1.jpg b/.github/glyph-editor-1.jpg new file mode 100644 index 0000000..72c1890 Binary files /dev/null and b/.github/glyph-editor-1.jpg differ diff --git a/.github/glyph-editor-2.jpg b/.github/glyph-editor-2.jpg new file mode 100644 index 0000000..b3ee3da Binary files /dev/null and b/.github/glyph-editor-2.jpg differ diff --git a/.github/glyphs.jpg b/.github/glyphs.jpg new file mode 100644 index 0000000..67e751c Binary files /dev/null and b/.github/glyphs.jpg differ diff --git a/.github/shapes.jpg b/.github/shapes.jpg new file mode 100644 index 0000000..5d9859e Binary files /dev/null and b/.github/shapes.jpg differ diff --git a/.github/stress-test.jpg b/.github/stress-test.jpg new file mode 100644 index 0000000..46d600c Binary files /dev/null and b/.github/stress-test.jpg differ diff --git a/.github/wireframes-shapes.jpg b/.github/wireframes-shapes.jpg deleted file mode 100644 index a83790c..0000000 Binary files a/.github/wireframes-shapes.jpg and /dev/null differ diff --git a/.github/wireframes-stress-test.jpg b/.github/wireframes-stress-test.jpg deleted file mode 100644 index 445a4fb..0000000 Binary files a/.github/wireframes-stress-test.jpg and /dev/null differ diff --git a/CHANGELOG.md b/CHANGELOG.md index e979aee..a4ed30a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,22 +1,29 @@ -# Wireframes (Unreleased) +# Wireframes 2.1.0 ### What's new 1. Shape components draw a shape on their GameObject without code, in Edit Mode, Play Mode and builds: **Add Component > Wireframes** has one for every shape, such as `WireframeSphere` and `WireframeLine`. -2. Every change to a component shows up in the next render, made in the Inspector, by undo, animation or a script, and components do no work per frame. -3. Disabling a component or its GameObject hides its shape at no cost, and enabling it again allocates nothing. -4. Components share containers by occlusion, transparency and layer, so many of them draw in a few draw calls; a color with alpha below 1 draws transparent, and shapes draw on their GameObject's layer. -5. `DrawAsGizmo` draws a component's shape like a gizmo, in the Scene view and in the Game view while its Gizmos button is on, and builds leave it out. -6. Prefab Mode draws the components of the prefab being edited. -7. The Shape Gallery sample has a ComponentGallery scene made of components, and continuous integration builds it too. -8. A container without shapes skips its work before each render once its empty chunks are released. +2. Text and symbols are drawn with lines like every other shape, in 2D or 3D: `CreateText` and `CreateSymbol` create them in a container, and the `WireframeText` and `WireframeSymbol` components draw them on GameObjects. +3. The package's Default Glyphs draw the 95 printable ASCII characters with its Default Font, and 23 symbols, such as `DefaultSymbols.Warning`, with its Default Symbols. +4. The Glyph Editor, in **Window > Wireframes > Glyph Editor**, shows a glyph pack's characters and symbols in a gallery and edits each glyph's lines on a canvas or as exact X and Y values, with undo, while texts and symbols that draw with the pack follow each edit. +5. Every change to a component shows up in the next render, made in the Inspector, by undo, animation or a script, and components do no work per frame. +6. Text is aligned in its bounds, wraps at spaces or runs past them, has proportional or monospace characters with character and line spacing, and changes every frame without allocating through `SetText(ReadOnlySpan)`. +7. Glyph packs, made with **Create > Wireframes > Glyph Pack**, are listed in a `WireframeGlyphs`, where the first pack that has a glyph draws it and the Inspector says which glyphs each pack overrides; code names symbols by the enum the Glyph Editor generates for each pack, so no lookup compares strings. +8. Disabling a component or its GameObject hides its shape at no cost, and enabling it again allocates nothing. +9. Components share containers by occlusion, transparency and layer, so many of them draw in a few draw calls; a color with alpha below 1 draws transparent, and shapes draw on their GameObject's layer. +10. `DrawAsGizmo` draws a component's shape like a gizmo, in the Scene view and in the Game view while its Gizmos button is on, and builds leave it out. +11. Prefab Mode draws the components of the prefab being edited. +12. A character or symbol the glyphs lack is drawn as `?`, with one warning for each. +13. The Shape Gallery sample names its shapes with text and has two more scenes, ComponentGallery, made of components, and TextAndSymbols, and continuous integration builds all three. +14. A container without shapes skips its work before each render once its empty chunks are released. ### Known issues 1. The Hierarchy's Scene visibility toggles don't hide components' wireframes, and the Gizmos menu's per-component checkboxes don't affect gizmo shapes. 2. Clicking a component's wireframe in the Scene view doesn't select its GameObject. 3. A script that changes a GameObject's layer moves its components' shapes to that layer on their next change. -4. Stereo rendering for XR is untested. +4. Text has no kerning, and the Default Font has only the printable ASCII characters. +5. Stereo rendering for XR is untested. # Wireframes 2.0.0 diff --git a/Documentation/FEATURES.md b/Documentation/FEATURES.md index c36f704..cfee49a 100644 --- a/Documentation/FEATURES.md +++ b/Documentation/FEATURES.md @@ -12,7 +12,7 @@ A shape attaches to Transforms, called bones, and moves, turns and scales with t 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 +### 19 shapes | Shape | Usual Create method | Component | What it is | |---|---|---|---| @@ -33,8 +33,10 @@ A shape attaches to Transforms, called bones, and moves, turns and scales with t | `IStadium` | `CreateStadium(centerA, centerB, radius)` | `WireframeStadium` | The flat outline of a capsule. | | `IFrustum` | `CreateFrustum(endA, endB, radiusA, radiusB, sideCount)` | `WireframeFrustum` | Two regular polygons joined at every corner. Prisms and regular pyramids are frustums too. | | `IPyramid` | `CreatePyramid(tip, baseCenter, baseSize)` | `WireframePyramid` | A rectangular base joined to a tip, like a camera's view without its near plane. | +| `IText` | `CreateText(position, rotation, text)` | `WireframeText` | Lines of text drawn with glyphs, aligned in bounds, proportional or monospace. | +| `ISymbol` | `CreateSymbol(position, rotation, symbol)` | `WireframeSymbol` | A symbol drawn with a glyph, named by a generated enum, such as `DefaultSymbols.Heart`. | -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. +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, or with only what it draws for texts and symbols. Round shapes are drawn like Unity's gizmos, with rings and a few lines, from 3 to 1,024 segments per ring. ### Shape components @@ -50,13 +52,29 @@ Every shape has a component that draws it on its GameObject without any code, th The Hierarchy's Scene visibility toggles don't hide components' wireframes, and the Gizmos menu's per-component checkboxes don't affect gizmo shapes. +### Text and symbols + +Texts and symbols draw glyphs, made of lines like every other shape, so they stand anywhere in 2D or 3D, follow their bones and draw in the same containers. + +1. The package's Default Glyphs draw the 95 printable ASCII characters with its Default Font, a single-line design, and 23 symbols with its Default Symbols, from arrows and checks to media buttons. Texts, symbols and new components use them unless given glyphs of their own. +2. Text is laid out in bounds: each line aligned left, center or right, the lines together top, middle or bottom, wrapped at spaces or left to run past the bounds, with character and line spacing in glyph boxes. +3. Characters are proportional, as wide as their lines, or monospace, a whole glyph box each. +4. `SetText(ReadOnlySpan)` changes a text without allocating once its buffers fit, and a text keeps spare room, so most changes rewrite it in place. +5. A character or symbol the glyphs lack is drawn as `?`, with one warning for each. +6. Glyph packs hold characters, found by character, and symbols, found by a keyword. A glyph list puts packs in priority order, the first pack that has a glyph draws it, and its Inspector says which glyphs each pack overrides. +7. Code names symbols by an enum generated for each pack, whose members are their keywords' hashes, so no lookup compares strings. +8. The Glyph Editor shows a pack's glyphs in a gallery, with characters and symbols apart, and edits each glyph's strokes on a canvas with a grid, guides and snapping, or point by point with exact X and Y values, with undo. Texts and symbols drawing with the pack follow each edit at once. +9. Packs installed from a registry, git or a tarball are read-only, and the Glyph Editor duplicates them into the project to edit them. + +Text has no kerning, and the Default Font covers ASCII only, so other characters need a pack of your own. + ### 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. +Frames that move bones, edit shapes, rewrite texts, hide and show shapes, 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 diff --git a/Documentation/PERFORMANCE.md b/Documentation/PERFORMANCE.md index 6e5a63a..028fab4 100644 --- a/Documentation/PERFORMANCE.md +++ b/Documentation/PERFORMANCE.md @@ -69,6 +69,8 @@ Up to 1.0, shapes were drawn by a `SkinnedMeshRenderer`. Before 2.0 replaced it | Stadium | 34 | 34 | | Frustum with n sides | 2n | 3n | | Pyramid | 5 | 8 | +| Text | About 7 per character of the Default Font, rounded up to a power of two, at least 16 | About 5 per character of the Default Font | +| Symbol | 16 for most Default Symbols, and 32 or 64 for round ones | 1 to 34 for the Default Symbols | ### Tips @@ -76,6 +78,7 @@ Up to 1.0, shapes were drawn by a `SkinnedMeshRenderer`. Before 2.0 replaced it 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. +5. **Rewrite a text with `SetText`** rather than creating another: it keeps room to grow, so most changes rewrite it in place without allocating. +6. **Group shapes in few containers.** Each container has its own bone texture and draw calls. +7. **Use `Fade` where it helps,** since it doubles a container's draw calls. +8. **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 index 715839a..439be8d 100644 --- a/Documentation/USAGE.md +++ b/Documentation/USAGE.md @@ -37,6 +37,8 @@ A few shapes add an obvious extra, such as `CreateCircle(center, normal, radius) 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. +Texts and symbols take what they draw last: `CreateText(text)`, `CreateText(position, rotation, text)` and `CreateText(bone, localPosition, localRotation, text)`, and the same three for `CreateSymbol` with a member of a symbol enum. + ### 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`. @@ -62,10 +64,11 @@ 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. +3. **Texts and symbols stand in their XY plane** and read from its -Z side, so with no rotation they face a camera looking along +Z. +4. Other shapes are centered on their position. +5. `Quaternion.LookRotation(axis, normal)` is the rotation that points a shape's +Z along `axis` with +Y toward `normal`. +6. 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. +7. 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 @@ -135,13 +138,14 @@ To draw a shape without writing code, add its component to a GameObject: **Add C | `WireframeBox`, `WireframeRectangle`, `WireframeRoundedRectangle`, `WireframeCircle`, `WireframeEllipse`, `WireframeStar`, `WireframeSphere`, `WireframeEllipsoid`, `WireframeSpikedSphere` | **Center** and **Rotation**, in Euler angles. Flat shapes lie flat on the GameObject with no rotation. | | `WireframeCylinder`, `WireframeCone`, `WireframeCapsule`, `WireframeStadium`, `WireframeFrustum`, `WireframePyramid` | **End A** and **End B**, the ends of the shape's axis, and **Roll** around it. A cone's or pyramid's tip is end A. With no roll, the shape's +Y stays as close to the GameObject's up as the axis allows. | | `WireframeLine`, `WireframePolyline` | A **Bone** and a **Position** for each point: the point follows that Transform, or the GameObject when it has none. | +| `WireframeText`, `WireframeSymbol` | **Center** and **Rotation**, standing in the GameObject's XY plane and read from its -Z side. **Text**, **Glyphs** and the layout fields match [`IText`](#text), and **Symbol** lists the symbols of the component's glyphs by name. | 1. **Every change shows up in the next render,** made in the Inspector, by undo, a prefab revert, animation or a script. A count that runtime shapes fix at creation, such as **Segment Count**, creates the shape again, once you finish typing it, and invalid counts snap to the nearest valid one. 2. **`enabled` shows and hides the shape.** Disabling the component or its GameObject costs nothing, and enabling it again allocates nothing. 3. **One color per shape.** **Color** colors all of it, and an alpha below 1 draws it transparent; **Occlusion** works like the container setting. Shapes draw on their GameObject's layer, for camera culling masks. 4. **Draw As Gizmo** draws the shape like a gizmo: in the Scene view, and in the Game view only while its **Gizmos** button is on. Other cameras never draw it, and builds leave it out. To leave out a whole GameObject meant for debugging, tag it **EditorOnly**, which strips it from builds with its children. 5. **A destroyed bone counts as none,** so its point follows the GameObject from the next render on. A bone has to be in a scene: one that isn't, such as a prefab asset, is reported once and the GameObject followed instead. -6. **New components draw what the Create methods without arguments draw,** relative to the GameObject: a white shape of unit size, with long shapes running 1 unit along +Z. A line runs 1 unit forward, and a polyline starts as a small triangle; it draws nothing with fewer than 2 points, or 3 when closed. +6. **New components draw what the Create methods without arguments draw,** relative to the GameObject: a white shape of unit size, with long shapes running 1 unit along +Z. A line runs 1 unit forward, and a polyline starts as a small triangle; it draws nothing with fewer than 2 points, or 3 when closed. A text starts as "Text" and a symbol as the star, both drawn with the package's Default Glyphs. Scripts set the same properties, and counts outside their range throw `ArgumentOutOfRangeException`, as Create methods do: @@ -162,6 +166,67 @@ Components share containers, one per combination of occlusion, transparency, lay 2. A script that changes `gameObject.layer` moves the shape to that layer on the component's next change; a change in the Inspector moves it at once. 3. Clicking a wireframe in the Scene view doesn't select its GameObject. +### Text + +`CreateText(position, rotation, text)` creates a text, and `CreateText(bone, localPosition, localRotation, text)` one that follows a bone. A text stands in the XY plane of its rotation and reads from the -Z side, so with no rotation it faces a camera looking along +Z. + +```csharp +IText label = _wireframes.CreateText(_target, new Vector3(0f, 1.5f, 0f), Quaternion.identity, "Target"); +label.CharacterSize = 0.5f; +label.SetColor(Color.cyan); +``` + +1. **Sizes are in glyph boxes.** `CharacterSize` is the height of a glyph box in the bone's units, 1 by default, and each line is one box tall. `CharacterSpacing` adds room after each character and `LineSpacing` between lines, both in boxes: 0.1 and 0 by default. +2. **`CharacterWidth` sets how far each character moves the next one along.** `Proportional`, the default, gives a character the width of its lines plus the spacing, and a space its pack's space width. `Monospace` gives every character a whole glyph box, so columns line up; with the Default Font, a `CharacterSpacing` of about -0.45 spaces it like a terminal. +3. **Lines are aligned in `Bounds`,** a rectangle centered on the text's position, 4 by 1 by default: each line by `HorizontalAlignment`, and all of them together by `VerticalAlignment`. With `Overflow` set to `Wrap`, a line wider than the bounds breaks at its last space that fits, or inside a word wider than the bounds; otherwise lines stay as written and run past the bounds. +4. **`\n` starts a new line,** `\r\n` counts once, a tab takes 4 spaces, and other control characters are skipped. +5. **A character the glyphs lack is drawn as `?`,** with one warning for each such character. `glyphs.Contains(character)` tells beforehand. +6. **`SetText` changes the text without allocating** once the text's buffers fit it, so a value can be rewritten every frame. Reading `Text` after `SetText` creates the string once. + +```csharp +private readonly char[] _digits = new char[11]; + +private void Update() +{ + _score.TryFormat(_digits, out int length); + _scoreText.SetText(_digits.AsSpan(0, length)); +} +``` + +### Symbols + +`CreateSymbol(position, rotation, symbol)` creates a symbol named by a member of the enum the Glyph Editor generates for a glyph pack, such as `DefaultSymbols.Heart`, and `CreateSymbol(bone, localPosition, localRotation, symbol)` one that follows a bone. Its glyph box is `Size` across, 1 by default, centered on its position, and stands in the XY plane of its rotation like a text. + +```csharp +ISymbol marker = _wireframes.CreateSymbol(_target, new Vector3(0f, 2f, 0f), Quaternion.identity, DefaultSymbols.Warning); +marker.SetColor(Color.yellow); +marker.SetSymbol(DefaultSymbols.Check); +``` + +1. A symbol of the same name from any pack of its glyphs counts, so the enum of one pack can name a symbol that another pack draws instead. +2. `GetSymbol()` returns the symbol as a member of any symbol enum, or as an undefined value when that enum has no member of its name. +3. The enum's `None` member draws nothing, and a symbol the glyphs lack is drawn as `?`, with one warning. + +### Glyph packs + +Texts and symbols draw **glyphs**: lines in a box from 0 to 1 on both axes, whose baseline is 0.25 up from the bottom. A **glyph pack**, `WireframeGlyphPack`, holds characters, found by their character, and symbols, found by their keyword. A **glyph list**, `WireframeGlyphs`, lists packs from the highest priority down: the first pack that has a character or symbol draws it, so a pack overrides the packs below it, and the list's Inspector says which glyphs each pack overrides. + +1. **The package's Default Glyphs,** `WireframeGlyphs.Default`, list its Default Font, with the 95 printable ASCII characters, and its Default Symbols, with 23 symbols from `ArrowUp` to `Bolt`. Texts and symbols without glyphs of their own draw with them, and new components get them. +2. **To draw with glyphs of your own,** create a pack with **Create > Wireframes > Glyph Pack** and edit it in the Glyph Editor. Then create a list with **Create > Wireframes > Glyphs**, put your pack first and the package's packs below it, and set the list as the `Glyphs` of your texts, symbols and components. +3. **Code names symbols by the enum** that the Glyph Editor's **Generate Enum** writes next to the pack: a member per symbol, valued by its keyword's hash, plus `None`. Keywords are C# identifiers, and lookups compare hashes, never strings. Generate the enum again after adding, renaming or reordering symbols; the button stands out while the enum is out of date. +4. **Packs installed from a registry, git or a tarball are read-only,** as Unity keeps those packages. **Duplicate to Assets** in the Glyph Editor copies one into your project, where you can edit it. + +### Glyph Editor + +**Window > Wireframes > Glyph Editor** edits glyph packs; double-clicking a pack, or its Inspector's **Open in Glyph Editor** button, opens it there. + +1. **The gallery** shows the pack's characters, sorted by character, and its symbols, in pack order or by name with **A–Z**, each named below its tile, and the search field filters both. **+** adds a character or a symbol, and **+ ASCII** adds an empty glyph for every printable ASCII character the pack lacks. Right-click a tile to rename, duplicate, copy, paste or delete its glyph, and drag symbols into another order, which is their enum's order. +2. **Clicking a tile opens its glyph** on the canvas beside the gallery. Click a point to select it, and drag it to move it, snapped to the grid unless Shift is held. Ctrl+click, or Cmd+click on macOS, adds a point after the end of the selected stroke, or starts a stroke when none is selected, and double-clicking a line inserts a point there. Delete removes the selected point, the arrow keys nudge it by a grid step, the wheel zooms, the middle button pans, F frames the box, and Esc closes the glyph. +3. **The details panel** lists each stroke's points as exact X and Y values, adds, reorders and deletes strokes and points, and closes a stroke back to its first point. It also centers the glyph horizontally or vertically: symbols suit both, while characters should stay on the baseline. Proportional text measures characters by their lines, so only monospace text cares where a character sits across its box. +4. **The preview strip** draws sample text with the pack alone, proportional or monospace, and lists the characters the pack lacks instead of warning about them. +5. **Pack Settings** sets the enum's name and namespace, the space width, the x-height and cap-height guides and the grid, and **Generate Enum** writes the enum. +6. Every edit can be undone, texts and symbols that draw with the pack show each edit at once, and Ctrl+S saves the pack. + ### 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. @@ -219,7 +284,7 @@ Add the **WireframeCamera** component to a Camera to draw everything that camera Import them from the package's **Samples** tab in the Package Manager. -1. **Shape Gallery** lays out every shape in three rows, each turning with its own bone. Open its **ShapeGallery** scene and enter Play Mode. Its **ComponentGallery** scene has the same shapes made of components, drawn as soon as it opens, and turning in Play Mode. +1. **Shape Gallery** lays out the package's shapes in three rows, each turning with its own bone and named below it with text. Open its **ShapeGallery** scene and enter Play Mode. Its **ComponentGallery** scene has the same shapes made of components, drawn as soon as it opens, and turning in Play Mode. Its **TextAndSymbols** scene shows every default symbol, text in both character widths, wrapped and aligned, and a clock and a symbol drawn from code. 2. **Stress Test** spawns 10,000 lines, 1,000 boxes and 2,000 other shapes on 100 orbiting bones. Add the `StressTest` component to an empty GameObject in a scene with a camera, and enter Play Mode with the Profiler open. Raise **Recolor Per Frame** to measure the cost of edits. ### Tests diff --git a/README.md b/README.md index 74855f0..2fef950 100644 --- a/README.md +++ b/README.md @@ -2,10 +2,14 @@ Wireframe shapes for Unity that follow Transforms on the GPU: create a shape once, and it moves with its bones. -Every shape, from the Shape Gallery sample +Every shape, from the Shape Gallery sample 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. +Text and symbols, from the TextAndSymbols scene of the Shape Gallery sample + +Text and symbols are drawn with lines too, in 2D or 3D, from glyph packs you can edit in the Glyph Editor: the package comes with a font of the printable ASCII characters and 23 symbols. + Without writing code, add a shape component to a GameObject: it draws there at once, in Edit Mode too, and shows every change to its fields as you make it. Shapes draw the same in Play Mode, Edit Mode and player builds, under the Built-in Render Pipeline, URP and HDRP, from desktops and phones to WebGL, while server builds keep them working without drawing. @@ -13,18 +17,19 @@ Shapes draw the same in Play Mode, Edit Mode and player builds, under the Built- ### Features 1. Shapes follow Transforms on the GPU. -2. 17 shapes, from lines to capsules. +2. 19 shapes, from lines to text and symbols. 3. Shape components, edited live in Edit Mode. -4. Edits upload only what they change. -5. Steady frames allocate nothing. -6. Built-in Render Pipeline, URP and HDRP. -7. Windows, Android, WebGL and more. -8. Play Mode, Edit Mode and builds. -9. Hidden lines hide, show or fade. -10. Shapes hide without being disposed. -11. Statistics and Profiler markers. -12. Destroyed bones leave shapes in place. -13. A camera that draws in wireframe. +4. A Glyph Editor for fonts and symbols. +5. Edits upload only what they change. +6. Steady frames allocate nothing. +7. Built-in Render Pipeline, URP and HDRP. +8. Windows, Android, WebGL and more. +9. Play Mode, Edit Mode and builds. +10. Hidden lines hide, show or fade. +11. Shapes hide without being disposed. +12. Statistics and Profiler markers. +13. Destroyed bones leave shapes in place. +14. A camera that draws in wireframe. Detailed about features - see [FEATURES.md](Documentation/FEATURES.md). @@ -32,9 +37,9 @@ Detailed about features - see [FEATURES.md](Documentation/FEATURES.md). **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. -**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). +The Stress Test sample in Play Mode -The Stress Test sample in Play Mode +**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). @@ -54,10 +59,11 @@ Detailed about performance - see [PERFORMANCE.md](Documentation/PERFORMANCE.md). 6. Shapes are never frustum-culled. 7. Shapes are used from the main thread only. 8. Custom materials skin with the package's HLSL. +9. Text has no kerning, and the default font has ASCII only. ### 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. +**From the git URL:** in **Window > Package Manager**, choose **+ > Install package from git URL** and enter the URL below. Add `#2.1.0` to its end to stay on that version. ``` https://github.com/reromanlee/Wireframes.git?path=/UnityPackage @@ -110,7 +116,20 @@ sphere.Radius = 2f; sphere.IsVisible = false; ``` -**Or add a shape component to a GameObject, with no code at all.** **Add Component > Wireframes** has one for every shape, from **Line** to **Pyramid**. It draws on its GameObject right away, in Edit Mode, Play Mode and builds, follows it like a mesh, and shows each change to its fields in the next render; disabling it hides the shape. +**Text and symbols are shapes too,** drawn with the package's Default Glyphs unless given glyph packs of your own. Changing a text with `SetText` allocates nothing, so it can show a value that changes every frame. + +```csharp +IText label = _wireframes.CreateText(_target, new Vector3(0f, 1.5f, 0f), Quaternion.identity, "Target"); +ISymbol marker = _wireframes.CreateSymbol(_target, new Vector3(0f, 2.5f, 0f), Quaternion.identity, DefaultSymbols.Warning); +``` + +**Or add a shape component to a GameObject, with no code at all.** **Add Component > Wireframes** has one for every shape, from **Line** to **Pyramid**, **Text** and **Symbol**. It draws on its GameObject right away, in Edit Mode, Play Mode and builds, follows it like a mesh, and shows each change to its fields in the next render; disabling it hides the shape. + +The Glyph Editor window with text glyphs + +The Glyph Editor window with symbol glyphs + +**Draw glyphs of your own in the Glyph Editor,** in **Window > Wireframes > Glyph Editor**: a gallery of a pack's characters and symbols, and a canvas that edits their lines with exact coordinates. Detailed about usage - see [USAGE.md](Documentation/USAGE.md). diff --git a/UnityPackage/Editor/Glyphs.meta b/UnityPackage/Editor/Glyphs.meta new file mode 100644 index 0000000..7bfc6e4 --- /dev/null +++ b/UnityPackage/Editor/Glyphs.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 26161c5e4ddc087a0eacfef8c442387f +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/UnityPackage/Editor/Glyphs/GlyphAssets.cs b/UnityPackage/Editor/Glyphs/GlyphAssets.cs new file mode 100644 index 0000000..e729ccb --- /dev/null +++ b/UnityPackage/Editor/Glyphs/GlyphAssets.cs @@ -0,0 +1,72 @@ +using System.IO; +using UnityEditor; +using UnityEngine.UIElements; +using Object = UnityEngine.Object; +using PackageInfo = UnityEditor.PackageManager.PackageInfo; +using PackageSource = UnityEditor.PackageManager.PackageSource; + +namespace reromanlee.Wireframes.Editor +{ + /// The Glyph Editor's dealings with assets: which ones it can edit, copies to edit, and its stylesheet. + internal static class GlyphAssets + { + private const string StyleSheetPath = "Packages/com.reromanlee.wireframes/Editor/Glyphs/GlyphEditor.uss"; + + private static StyleSheet _styleSheet; + + /// The Glyph Editor's stylesheet, or null if it's missing, which leaves the editor unstyled but working. + internal static StyleSheet StyleSheet + { + get + { + if (_styleSheet == null) + { + _styleSheet = AssetDatabase.LoadAssetAtPath(StyleSheetPath); + } + return _styleSheet; + } + } + + /// + /// True when can't be changed: it's in a package installed from a registry, git or a + /// tarball, which Unity keeps read-only, or version control won't let it be edited. + /// + internal static bool IsReadOnly(Object asset) + { + string path = AssetDatabase.GetAssetPath(asset); + if (string.IsNullOrEmpty(path)) + { + return false; + } + PackageInfo package = PackageInfo.FindForAssetPath(path); + if (package != null && package.source != PackageSource.Embedded && package.source != PackageSource.Local) + { + return true; + } + return !AssetDatabase.IsOpenForEdit(path, StatusQueryOptions.UseCachedIfPossible); + } + + /// + /// Copies into the Assets folder as "My" followed by its name, to edit it there. The copy + /// forgets the original's enum settings, so generating its enum never clashes with the original's. + /// + internal static WireframeGlyphPack DuplicateToAssets(WireframeGlyphPack pack) + { + string source = AssetDatabase.GetAssetPath(pack); + string target = AssetDatabase.GenerateUniqueAssetPath($"Assets/My {Path.GetFileName(source)}"); + if (!AssetDatabase.CopyAsset(source, target)) + { + return null; + } + WireframeGlyphPack copy = AssetDatabase.LoadAssetAtPath(target); + SerializedObject serialized = new(copy); + SerializedProperty settings = serialized.FindProperty(WireframeGlyphPack.EnumSettingsField); + settings.FindPropertyRelative(GlyphEnumSettings.NameField).stringValue = string.Empty; + settings.FindPropertyRelative(GlyphEnumSettings.NamespaceField).stringValue = string.Empty; + settings.FindPropertyRelative(GlyphEnumSettings.ScriptGuidField).stringValue = string.Empty; + serialized.ApplyModifiedPropertiesWithoutUndo(); + AssetDatabase.SaveAssetIfDirty(copy); + return copy; + } + } +} diff --git a/UnityPackage/Editor/Glyphs/GlyphAssets.cs.meta b/UnityPackage/Editor/Glyphs/GlyphAssets.cs.meta new file mode 100644 index 0000000..2521a83 --- /dev/null +++ b/UnityPackage/Editor/Glyphs/GlyphAssets.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 490852fd60f323bc172d04b79ee47aed diff --git a/UnityPackage/Editor/Glyphs/GlyphCanvas.cs b/UnityPackage/Editor/Glyphs/GlyphCanvas.cs new file mode 100644 index 0000000..eafc4e9 --- /dev/null +++ b/UnityPackage/Editor/Glyphs/GlyphCanvas.cs @@ -0,0 +1,577 @@ +using System; +using System.Collections.Generic; +using UnityEditor; +using UnityEngine; +using UnityEngine.UIElements; + +namespace reromanlee.Wireframes.Editor +{ + /// + /// Edits a glyph's points with the mouse and keyboard, over the glyph box, its grid and the pack's guides. + /// + /// + /// Click a point to select it, and drag it to move it, snapped to the grid unless Shift is held. Ctrl+click + /// (Cmd+click on macOS) adds a point after the end of the selected stroke, or starts a stroke when none is selected. + /// Double-click a line to insert a point there. Delete removes the selected point, the arrow keys nudge it by a grid + /// step, the wheel zooms at the pointer, the middle button pans, and F frames the box. + /// + internal sealed class GlyphCanvas : VisualElement + { + private const float Margin = 28f; + private const float HitDistance = 7f; + private const float HandleSize = 7f; + private const float MinimumZoom = 0.25f; + private const float MaximumZoom = 12f; + + private static readonly float[] UnitValues = { 0f, 0.25f, 0.5f, 0.75f, 1f }; + + private readonly List