Compare commits

...

56 Commits

Author SHA1 Message Date
bherbruck b5728f284b Release 0.24.1 2026-10-27 08:34:27 -04:00
bherbruck dc32cff620 Release 0.24.1: version bump, design format 40 (#7653)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 08:34:21 -04:00
dudedagger42 531fc04db8 Inspect: center of mass measures the world state, as Measure does (#5246) (#5268)
* Inspect: center of mass measures the world state, as Measure does (#5246)

inspect.center_of_mass read the model state, so in an assembly a moved
occurrence was measured where its bodies were made, a second placement
was left out, and a placed copy's world name was "no such bodies". It
now reads the world state, as inspect.measure does, and agrees with
component.properties' mass centre at uniform density.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Center of mass test: unground the placed part before moving it

Upstream now grounds a design's first part when it is made.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 12:04:53 +00:00
Nathan Dane 4907429439 Render workspace: original icon drafts for owner review (#6281) (#6283)
* claim: #6281

* assets: add Render appearance, library and paint icon drafts for owner review

* assets: add Render lighting, environment and light-type icon drafts for owner review

* assets: add Render camera, shot, frame and strip icon drafts for owner review

* assets: add Render output, result and compare icon drafts for owner review

* assets: list the Render icon drafts in the inventory and ATTRIBUTION

* assets: remake the Render light icons as one family with a shared dotted stem

* assets: remake the Render look icons around the lit look sphere

* assets: redraw the Render look icons so each action reads at a glance

* assets: record the owner's acceptance of the Render light icons

* assets: simplify the Render look icons and make Remove Override an alias of From Material

* assets: record the owner's acceptance of Render look icons

* assets: mark the removed look on From Material with a cross instead of an arrow

* assets: record the owner's acceptance of further Render icons

* assets: redraw the Render library, presets, environment, ground and backdrop icons

* assets: restore the first Render library and backdrop drawings and equalise the cube in the studio icons

* assets: record the owner's acceptance of the Render studio icons

* assets: redraw the Render camera and shot icons as one family

* assets: redraw the Render All Shots and Lens icons so they read at a glance

* assets: put the play badge back on the Render All Shots icon

* assets: redraw the Render output and result icons around one picture base

* assets: give Render Image its own play badge and enlarge the Compare handle

* assets: record the owner's acceptance of the Render output icons

* assets: describe the final Render icon drafts in RENDER_DRAFTS.md and ATTRIBUTION.md
2026-10-27 12:02:54 +00:00
Nathan Dane 192ef78ff3 ui_kit: lit sphere thumbnail and look-chain widget (#6281) (#6284)
* claim: #6281

* ui_kit: lit sphere thumbnail, cached and bounded

* ui_kit: look-chain widget (four steps, one active, empty steps dashed)

* ui_kit: add both to the gallery and the UI kit notes
2026-10-27 11:52:55 +00:00
Nathan Dane 44a0be8d94 Render workspace UX: roadmap and claim (#6281) (#6282)
* claim: #6281

* docs: record the Render workspace interaction design and implementation roadmap

* docs: plan the Render panels so each has one purpose

* docs: replace the tabs-and-ribbon plan with the stage-bar design

* docs: drop the note about the superseded draft

* docs: plan every Render panel and where each old control goes

* docs: defer the shared lighting rig, UI steps go first
2026-10-27 11:46:54 +00:00
vandang890615 9cba66c14d i18n: catch up Vietnamese (vi) catalog with latest main (3,317 keys) (#5823) 2026-10-27 08:45:02 +00:00
dudedagger42 4afd93331f Patterns: a body of a moved component is copied where it is shown (#4763) (#4969)
* Patterns: a body of a moved component is copied where it is shown (#4763)

A pattern of bodies made outside the bodies' component (a screw's body patterned from the root)
moved each body's stored geometry, which is in its component's frame, by transforms in the
pattern's frame. The copies landed where the body sits before its component is placed: near the
origin, for every pattern kind. As Mirror does, each copy is now moved as the pattern moves the
body in the world and kept beside its source, in its component.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Pattern test: unground the placed component before moving it

Upstream now grounds a design's first part when it is made.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 08:27:38 +00:00
github-actions[bot] 698621dfdf Regenerate generated files and fold pending files [skip ci] 2026-10-27 08:19:30 +00:00
Lars Jansen 5fc23436ca History: show the origin and bodies at the top (#6490) (#6495)
* claim: #6490

* feat(history): show the origin and bodies at the top (#6490)
2026-10-27 07:46:25 +00:00
Jackson Oursland 76879f8b39 Viewport: write hover edges as one record per line, colours interned once (#7416)
Hovering many bodies re-emits every edge in the highlight's colours. Each edge
interned its three colours into the palette again, each segment was written in
eight small appends, and segments without their faces' normals encoded the
same zero normals again. Lines are now one record, the edge colours are
interned once per highlight, the buffers are reserved per edge, and the shared
width word is worked out once.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-27 07:46:19 +00:00
Jackson Oursland 514b0c84f9 Viewport: hold the blocks the sorted glass stream was taken from (#6970)
The no-OIT glass cache compared the blocks' vertex and run buffers by address
without holding them, so a freed buffer's address could come back for other
contents. It now holds them and compares with Arc::ptr_eq.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-27 06:35:15 +00:00
Jackson Oursland ddca1054ab Viewport: leave bodies out of the view out of their blocks' draws (#7436)
Instanced placements were culled per view, but a block drew every body in it
whatever the camera saw. Blocks now keep each body's vertex span and box; when
the view changes, each class's runs are drawn without the bodies whose boxes
are out of the view. A block whose bodies are all in view draws its runs as
they are.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-27 06:35:11 +00:00
bherbruck 5965439a15 Release 0.24.0 2026-10-26 19:23:21 -04:00
bherbruck 586a358ed4 Release 0.23.1 2026-10-26 07:07:21 -04:00
bherbruck 7770f8982d Release 0.23.0 2026-10-25 18:18:39 -04:00
bherbruck 72ad6383ef Release 0.22.1 2026-10-25 10:38:39 -04:00
bherbruck 0537623a75 Release 0.22.0 2026-10-24 18:53:43 -04:00
bherbruck 65d288227e Release 0.21.0 (rc.2's code, version only)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 20:51:08 -04:00
bherbruck fedb3bcdb1 Release 0.21.0-rc.2 2026-10-23 17:14:38 -04:00
bherbruck 097b8b7550 Release 0.21.0-rc.1
The tree is main at f120285c9 exactly; the previous release commit is kept as a parent so release only moves forward.
2026-10-23 13:48:05 -04:00
bherbruck af7ca3a8a2 Release 0.21.0-rc.1
The tree is main at b31fddb98 exactly; the previous release commit is kept as a parent so release only moves forward.
2026-10-23 13:37:43 -04:00
bherbruck 27ddb725c7 Release 0.21.0-rc.1
The tree is main at 2bf3d4e11 exactly; the previous release commit is kept as a parent so release only moves forward.
2026-10-23 12:10:02 -04:00
bherbruck a06bdf5cfc Release 0.20.0
The tree is main at 030187452 exactly; the previous release commit is kept as a parent so release only moves forward.
2026-10-22 18:34:40 -04:00
bherbruck 33fdcdb3a0 Release 0.19.0
The tree is main at 5899e5b6c exactly; the previous release commit is kept as a parent so release only moves forward.
2026-10-21 21:13:33 -04:00
bherbruck c07ab5f8f9 Release 0.18.4
The tree is main at 728f753b8 exactly; the previous release commit is kept as a parent so release only moves forward.
2026-10-21 12:26:34 -04:00
bherbruck fea32f8aac Release 0.18.3
The tree is main at 034aa60d4 exactly; the previous release commit is kept as a parent so release only moves forward.
2026-10-21 08:46:37 -04:00
bherbruck 4927214976 Release 0.18.2
The tree is main at 57bcc4083 exactly; the previous release commit is kept as a parent so release only moves forward.
2026-10-20 22:09:18 -04:00
bherbruck b621819c0c Release 0.18.1
The tree is main at 522ae7755 exactly; the previous release commit is kept as a parent so release only moves forward.
2026-10-20 17:39:11 -04:00
bherbruck ec552a5300 Release 0.18.0
The tree is main at c3151fb7f exactly; the previous release commit is kept as a parent so release only moves forward.
2026-10-20 16:28:59 -04:00
bherbruck e0cc740cc3 Release 0.17.0
The tree is main at 511faedcf exactly; the 0.16.x release commits are kept as a parent so release only moves forward.
2026-10-20 11:41:48 -04:00
bherbruck 5de48e916d Release 0.16.2 2026-10-19 00:39:30 +00:00
bherbruck 80af7bbe3e Release 0.16.2
Version 0.16.2: 0.16.1 with the rigid orbit fix (#2753). Same design format (27).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 00:38:53 +00:00
bherbruck 191e983423 Fix orbit about the picked point (regression in 0.16.0) (#2753)
Orbiting about the point under the cursor (or the origin) turned the camera
about its target and then panned to put the pivot back. The pan works at the
target's depth with the parallel-view scale, so in a perspective view the
pivot slid off its pixel and the model swung about the camera.

The camera now turns rigidly about the pivot: Camera::orbit_about keeps the
pivot's coordinates in the camera's frame and rebuilds the target from the
new orientation, so the pivot stays exactly on its pixel in both projections.
The Shift snap spring and view.navigate's pivot orbit use the same.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit e88779218)
2026-10-19 00:38:52 +00:00
bherbruck 2b9445b049 Release 0.16.1 2026-10-18 20:16:21 +00:00
bherbruck 7ddf48ca16 Release 0.16.1
Version 0.16.1: 0.16.0 with the orbit fix (reverts #2142). Same design format (27).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 20:12:04 +00:00
bherbruck d2f6848377 Revert #2142: orbit about the picked point like 0.15.0
#2142 made orbiting about the point under the cursor turn the camera in place instead of turning the part about that point. Restore the 0.15.0 behaviour.

This reverts commit cac4bccea8.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 20:12:00 +00:00
bherbruck 0b6d6fecd8 Release 0.16.0 2026-10-18 18:16:11 +00:00
bherbruck fa691707c2 Release 0.15.0 2026-10-17 13:54:24 +00:00
bherbruck b9a55c74e2 Release 0.15.0
Version 0.15.0; the pending design-format changes are folded into format 26.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 13:53:44 +00:00
bherbruck 2bb9c81069 Release 0.14.0 2026-10-16 09:19:14 +00:00
bherbruck cedb51b8ea Release 0.14.0
Version 0.14.0; the pending design-format changes are folded into format 25.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 09:19:10 +00:00
bherbruck 66d13059dd Release 0.13.1 2026-10-15 21:56:43 +00:00
bherbruck 800ea40c0d Release 0.13.1
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 21:56:35 +00:00
bherbruck 20442eecd3 Agent chat on Windows: start npm agents through their .cmd shim
npm puts a Unix shell script with the bare name (codex-acp,
claude-code-acp, gemini) beside the .cmd shim. The lookup tried the bare
name first, so every npm-installed agent failed with "not a valid Win32
application (os error 193)". Windows now only considers .exe/.cmd/.bat,
and a command saved with the bare path runs the shim beside it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 60ea8c69d2)
2026-10-15 21:51:01 +00:00
bherbruck 22d1cd7032 Release 0.13.0 2026-10-15 20:34:13 +00:00
bherbruck 6a0dc18655 Fix the Windows 32-bit and macOS release builds
On 32-bit Windows SetWindowLongPtrW is SetWindowLongW, which takes and
returns i32, so the window-procedure hook didn't compile for i686.
The macOS app bundle now signs the bundled solidcraft-cli before the
bundle itself; codesign refuses a bundle with an unsigned helper.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 94e76cd97e)
2026-10-15 20:34:12 +00:00
bherbruck e006dbf1af Release 0.13.0
The tree is the 0.13.0 version commit (8241a750fa) on main exactly; the previous release is kept as a parent so release only moves forward.
2026-10-15 19:59:50 +00:00
bherbruck 8241a750fa Release 0.13.0
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 19:59:42 +00:00
bherbruck 41779cf7bb Release 0.12.0: release branch follows main again after the 0.11.1 patch
The tree is main at c36a6f9f exactly; the 0.11.1 patch commit is kept as a parent so release only moves forward.
2026-10-15 13:32:01 +00:00
bherbruck b01b9689c8 Release 0.11.1
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 20:41:41 +00:00
bherbruck 4426da6f5d Windows: start Direct3D 12 first, and Vulkan only without a D3D12 GPU (#1190)
* Windows: start Direct3D 12 first, and Vulkan only without a D3D12 GPU

wgpu started Vulkan, D3D12 and OpenGL together and preferred Vulkan, so
every Windows start loaded all installed Vulkan drivers. An old Intel
Vulkan driver (igvk64.dll) crashed 0.10.1 at startup inside the driver.

The app now probes D3D12, then Vulkan, then OpenGL, one backend at a
time, and starts only the first with a hardware adapter (WARP when none
has one). WGPU_BACKEND still wins; graphics.json's "backend" sets the
order by hand; the software renderer (safe mode, the retry) uses D3D12
alone. A new Windows CI job starts the real app under several settings.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* CI: a test filter YAML parses

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* CI: the startup smoke test writes graphics.json where the app reads it

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* CI: run the WARP shader tests one at a time

WARP's shader JIT (d3d10warp.dll) crashed twice in 124 runs of the gpu
tests with four test threads, with a null read inside its own optimizer
while several devices compiled compute pipelines at once. Serial runs
did not crash.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 95ea05e7c8)
2026-10-14 20:41:37 +00:00
bherbruck 5ead82e627 Release 0.11.0: release branch follows main again after the 0.10.1 patch
The tree is main at cccbe125 exactly; c29b0d2f (0.10.1) is kept as a parent so release only moves forward.
2026-10-14 20:07:41 +00:00
bherbruck c29b0d2f13 Release 0.10.1 2026-10-14 14:41:19 +00:00
bherbruck 480fd8d517 Fix the startup crash on D3D12 with FXC: CPU fallback for the tessellation pipeline, FXC-friendly shader (#1098) (#1099)
* GPU tessellation: a pipeline the device cannot build falls back to CPU meshing instead of crashing (#1098)

Build the tessellation compute pipeline inside error scopes; TessGpu::new returns an error,
and the viewport logs it and meshes bodies on the CPU. A Windows CI job builds the pipeline
with D3D12's FXC on the WARP adapter.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Tessellation shader: basis functions in one-dimensional local arrays, so D3D12's FXC compiles it (#1098)

FXC has to unroll loops that write an array inside a struct (Basis.n0/n1) or a 2-D array
(ndu) by a counter, and cannot with run-time trip counts (X3511). A2.2 basis functions in a
local 1-D array returned whole; derivatives from the degree p-1 functions; the pole step
offsets computed rather than indexed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 14:41:16 +00:00
bherbruck 4c09fa5355 Hovered and selected faces light solidly again over GPU-drawn bodies (#1064)
Since the model's mesh went back to the reference mesher (#940), the GPU viewport drew
opaque bodies from their display plans while hover and selection lit faces with the
reference mesh's triangles. Two triangulations of the same face at equal depth fought, so
a hovered face shimmered in wedges instead of lighting whole.

Faces lit over a body drawn from its plan now use the plan's own triangles
(ModelBody::plan_mesh, evaluated once on the CPU), and the highlight pass gets a small
depth bias toward the eye so f32 rounding between the GPU and CPU evaluations of the same
triangles cannot speckle it either.

Test: every face of a box with a pocket and a round boss, hovered and rendered offscreen
through the viewport's real pipelines on the GPU path, lights the same pixels as on the
CPU path (skipped without a GPU adapter).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 12:56:47 +00:00
78 changed files with 3821 additions and 84 deletions
+48
View File
@@ -292,6 +292,53 @@ Editable drawing code, native projects and shipped exports are collected under `
| assets/icons/ui/design/svg/model/colored_components_off.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/combine.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/component.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
### Render workspace icon drafts
Original SolidCraft drawings, Open for owner review; no runtime assignments.
| File | Author | Source | Licence |
|---|---|---|---|
| assets/icons/ui/design/svg/model/render_all_shots.svg | SolidCraft contributors | original Camera icon master (64 grid): stacked camera bodies with a lower-right play badge | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/render_appearance.svg | SolidCraft contributors | original Looks icon master (64 grid): shaded look sphere on the top corner of an accent-shaded cube | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/render_aspect_frame.svg | SolidCraft contributors | original Camera icon master (64 grid): wide rounded frame with two opposite corner brackets | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/render_backdrop.svg | SolidCraft contributors | original Studio icon master (64 grid): grey cube in front of a tall upright accent panel | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/render_cancel.svg | SolidCraft contributors | original Output icon master (64 grid): picture with a lower-right round stop badge | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/render_check.svg | SolidCraft contributors | original Output icon master (64 grid): picture with a lower-right round check badge | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/render_compare.svg | SolidCraft contributors | original Output icon master (64 grid): two grey pictures split by a vertical slider with opposed arrows | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/render_copy_look.svg | SolidCraft contributors | original Looks icon master (64 grid): look sphere in a square frame offset from a second frame outline | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/render_execute.svg | SolidCraft contributors | original Output icon master (64 grid): picture with a lower-right round play badge | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/render_export_image.svg | SolidCraft contributors | original Output icon master (64 grid): picture with a large outward diagonal arrow at the upper right | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/render_exposure.svg | SolidCraft contributors | original Camera icon master (64 grid): split light and accent disc with plus and minus marks | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/render_fit_product.svg | SolidCraft contributors | original Camera icon master (64 grid): grey cube inside four corner brackets | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/render_frame_guides.svg | SolidCraft contributors | original Camera icon master (64 grid): rounded frame with a rule-of-thirds grid and four accent nodes | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/render_from_material.svg | SolidCraft contributors | original Looks icon master (64 grid): look sphere struck through above a plain grey slab | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/render_ground.svg | SolidCraft contributors | original Studio icon master (64 grid): grey cube on a slab with an accent contact patch | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/render_lens.svg | SolidCraft contributors | original Camera icon master (64 grid): concentric lens rings around an accent centre | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/render_library.svg | SolidCraft contributors | original Studio icon master (64 grid): row of three look spheres, middle one accent, on a shelf bar | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/render_light_area.svg | SolidCraft contributors | original Lights icon master (64 grid): slanted flat panel above a dotted accent stem | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/render_light_by_highlight.svg | SolidCraft contributors | original Lights icon master (64 grid): shaded sphere with a ringed highlight target and dotted stem arriving from above | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/render_light_point.svg | SolidCraft contributors | original Lights icon master (64 grid): round source disc with centre dot above a dotted accent stem | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/render_light_spot.svg | SolidCraft contributors | original Lights icon master (64 grid): cone source with curved rim above a dotted accent stem | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/render_lighting_presets.svg | SolidCraft contributors | original Studio icon master (64 grid): two stacked cards, the front one holding a light bulb | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/render_output_size.svg | SolidCraft contributors | original Output icon master (64 grid): picture frame with a diagonal double-headed dimension arrow | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/render_paint.svg | SolidCraft contributors | original Looks icon master (64 grid): flat brush with an accent tip, light ferrule and slate handle | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/render_pick_up.svg | SolidCraft contributors | original Looks icon master (64 grid): eyedropper with a grey bulb, slate crossbar and accent sampling tip | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/render_quality.svg | SolidCraft contributors | original Output icon master (64 grid): three rising bars, the tallest in accent | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/render_replace_all.svg | SolidCraft contributors | original Looks icon master (64 grid): grey sample sphere with an arrow to an accent sphere beside a second one | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/render_results.svg | SolidCraft contributors | original Output icon master (64 grid): stack of two pictures, the front one showing a landscape | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/render_same_look.svg | SolidCraft contributors | original Looks icon master (64 grid): look sphere and three cubes, the two matching cubes ringed | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/render_shot.svg | SolidCraft contributors | original Camera icon master (64 grid): camera body with a lower-right plus badge | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/render_shots_strip.svg | SolidCraft contributors | original Camera icon master (64 grid): strip of three small frames, the middle one in accent | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/render_solo_light.svg | SolidCraft contributors | original Lights icon master (64 grid): point source and dotted stem inside four corner focus brackets | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/render_sun.svg | SolidCraft contributors | original Lights icon master (64 grid): source disc with eight short radial rays | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/render_transparent_background.svg | SolidCraft contributors | original Output icon master (64 grid): grey cube on a checker tile | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/render_update_shot.svg | SolidCraft contributors | original Camera icon master (64 grid): camera body with a lower-right refresh badge | MIT OR Apache-2.0 |
### UI icon masters (continued)
| File | Author | Source | Licence |
|---|---|---|---|
| assets/icons/ui/design/svg/model/compute.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/configuration_table.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/conic.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
@@ -381,6 +428,7 @@ Editable drawing code, native projects and shipped exports are collected under `
| assets/icons/ui/design/svg/flat/shield.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/status_recovery.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/frame_member.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/render_environment.svg | SolidCraft contributors | original Studio icon master (64 grid): dome with a grey cube inside | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/placeholder.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/plane.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/plane_angle.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
Generated
+17 -17
View File
@@ -4208,7 +4208,7 @@ dependencies = [
[[package]]
name = "solidcraft"
version = "0.24.0"
version = "0.24.1"
dependencies = [
"block2 0.6.2",
"eframe",
@@ -4242,11 +4242,11 @@ dependencies = [
[[package]]
name = "solidcraft-3dx-client"
version = "0.24.0"
version = "0.24.1"
[[package]]
name = "solidcraft-cli"
version = "0.24.0"
version = "0.24.1"
dependencies = [
"image",
"log",
@@ -4258,7 +4258,7 @@ dependencies = [
[[package]]
name = "solidcraft-derive"
version = "0.24.0"
version = "0.24.1"
dependencies = [
"proc-macro2",
"quote",
@@ -4267,7 +4267,7 @@ dependencies = [
[[package]]
name = "solidcraft-doc"
version = "0.24.0"
version = "0.24.1"
dependencies = [
"log",
"serde",
@@ -4284,7 +4284,7 @@ dependencies = [
[[package]]
name = "solidcraft-engine"
version = "0.24.0"
version = "0.24.1"
dependencies = [
"image",
"log",
@@ -4305,7 +4305,7 @@ dependencies = [
[[package]]
name = "solidcraft-geom"
version = "0.24.0"
version = "0.24.1"
dependencies = [
"serde",
"serde_json",
@@ -4314,7 +4314,7 @@ dependencies = [
[[package]]
name = "solidcraft-gpu"
version = "0.24.0"
version = "0.24.1"
dependencies = [
"image",
"log",
@@ -4334,7 +4334,7 @@ dependencies = [
[[package]]
name = "solidcraft-io"
version = "0.24.0"
version = "0.24.1"
dependencies = [
"cadmpeg-core",
"cadmpeg-ir",
@@ -4358,7 +4358,7 @@ dependencies = [
[[package]]
name = "solidcraft-kernel"
version = "0.24.0"
version = "0.24.1"
dependencies = [
"log",
"proptest",
@@ -4378,7 +4378,7 @@ dependencies = [
[[package]]
name = "solidcraft-mcp"
version = "0.24.0"
version = "0.24.1"
dependencies = [
"serde_json",
"solidcraft-engine",
@@ -4387,7 +4387,7 @@ dependencies = [
[[package]]
name = "solidcraft-render"
version = "0.24.0"
version = "0.24.1"
dependencies = [
"image",
"moxcms",
@@ -4401,7 +4401,7 @@ dependencies = [
[[package]]
name = "solidcraft-sketch"
version = "0.24.0"
version = "0.24.1"
dependencies = [
"serde",
"serde_json",
@@ -4412,14 +4412,14 @@ dependencies = [
[[package]]
name = "solidcraft-space-mouse"
version = "0.24.0"
version = "0.24.1"
dependencies = [
"serde",
]
[[package]]
name = "solidcraft-ui-egui"
version = "0.24.0"
version = "0.24.1"
dependencies = [
"arboard",
"egui",
@@ -4449,7 +4449,7 @@ dependencies = [
[[package]]
name = "solidcraft-web"
version = "0.24.0"
version = "0.24.1"
dependencies = [
"eframe",
"egui",
@@ -5994,7 +5994,7 @@ checksum = "e450f9b2ed1dff33c94c12589a87338689467b9c4f5d8a5710bd09a847d2c8a7"
[[package]]
name = "xtask"
version = "0.24.0"
version = "0.24.1"
dependencies = [
"proc-macro2",
"serde",
+1 -1
View File
@@ -5,7 +5,7 @@ members = ["crates/*", "apps/*", "xtask"]
exclude = ["vendor"]
[workspace.package]
version = "0.24.0"
version = "0.24.1"
edition = "2024"
license = "MIT OR Apache-2.0"
rust-version = "1.95"
+93
View File
@@ -0,0 +1,93 @@
# Render workspace icon drafts
All ids below are **Open** for owner review, except `render_scene` and `render_image`,
which are existing accepted icons used only as references. No commands, toolbar
entries, browser selectors or runtime request manifests are changed by this draft set.
`render_remove_override` is the one remaining alias (of `render_from_material`);
`render_execute`, `render_environment` and `render_fit_product` now have their own
masters in `design/svg/model/`. Accepted source masters stay unchanged.
Review with `python3 -B assets/icons/ui/design/tools/icon_status.py --serve`,
search `render_`, and select Open. See [STYLE_GUIDE.md](STYLE_GUIDE.md) for
acceptance; drafts do not imply implemented commands.
The finished references in each row were read and previewed in both themes
before authoring. Draft contact sheets (96, 24 and 16 px, both themes) are local
review evidence, not shipped assets. The icons are grouped in five families, each
with one shared design rule.
## Lights
Rule: a slate source glyph with light passive fill, 2.55-unit outlines and a dotted
accent stem (three dots, 4.25 diameter, 7 units apart) showing the direction.
| Meaning | Id | Finished references | Drawing |
|---|---|---|---|
| Light area | `render_light_area` | `box`, `camera` | Slanted flat panel on the canonical axes above the dotted stem. |
| Light spot | `render_light_spot` | `camera`, `material` | Cone with a curved rim above the dotted stem. |
| Light point | `render_light_point` | `camera`, `material` | Round source disc with a centre dot above the dotted stem. |
| Sun | `render_sun` | `sphere`, `camera` | Source disc of the same size with eight short radial rays instead of a stem. |
| Light by highlight | `render_light_by_highlight` | `sphere`, `material` | Shaded sphere with a ringed highlight target; the dotted stem arrives from above. |
| Solo light | `render_solo_light` | `fit`, `camera` | Point source and stem inside four corner focus brackets. |
## Looks
Rule: the lit look sphere (top-left highlight, bottom-right shade) plus one cue that
says what happens to it; receiving bodies stay canonical grey or cube shapes.
| Meaning | Id | Finished references | Drawing |
|---|---|---|---|
| Appearance | `render_appearance` | `material`, `box` | Shaded look sphere on the top corner of an accent-shaded cube: a look assigned to a body. |
| Paint | `render_paint` | `material`, `paste` | Flat brush with an accent tip, light ferrule and slate handle. |
| Pick up | `render_pick_up` | `material`, `copy` | Eyedropper with a grey bulb, slate crossbar and accent sampling tip. |
| Replace all | `render_replace_all` | `copy`, `box` | Grey sample sphere with an arrow to an accent sphere, beside a second accent sphere. |
| Copy look | `render_copy_look` | `material`, `copy` | Look sphere in a square frame, offset from a second frame outline. |
| Remove override | `render_remove_override` | `appearance`, `box` | Alias of `render_from_material`: the custom look is crossed off. |
| Same look | `render_same_look` | `select`, `box` | Look sphere and three cubes; the two matching cubes are ringed. |
| From material | `render_from_material` | `material`, `extrude` | Look sphere struck through above a plain grey slab, so the material shows. |
## Studio
Rule: the standard grey cube stands in the set-up it is lit or placed in.
| Meaning | Id | Finished references | Drawing |
|---|---|---|---|
| Library | `render_library` | `library`, `copy` | Row of three look spheres, the middle one accent, on a shelf bar. |
| Lighting presets | `render_lighting_presets` | `new`, `render_scene` | Two stacked cards, the front one holding a light bulb. |
| Environment | `render_environment` | `environment_map`, `render_scene` | Dome with a grey cube inside. |
| Ground | `render_ground` | `box`, `render_scene` | Grey cube on a slab with an accent contact patch. |
| Backdrop | `render_backdrop` | `render_scene`, `render_image` | Grey cube in front of a tall upright accent panel. |
## Camera and shots
Rule: one chunky camera body with a lower-right round badge for shot actions; the
other camera icons are simple frames.
| Meaning | Id | Finished references | Drawing |
|---|---|---|---|
| Shot | `render_shot` | `camera`, `new` | Camera body with a plus badge: save a shot. |
| Update shot | `render_update_shot` | `camera`, `undo` | Camera body with a refresh badge. |
| Render all shots | `render_all_shots` | `copy`, `render_image` | Stacked camera bodies with a play badge. |
| Shots strip | `render_shots_strip` | `camera`, `copy` | Strip of three small frames, the middle one in accent. |
| Fit product | `render_fit_product` | `fit`, `camera` | Grey cube inside four corner brackets. |
| Lens / field of view | `render_lens` | `camera`, `fit` | Concentric lens rings around an accent centre. |
| Exposure | `render_exposure` | `material`, `environment_map` | Disc split light and accent, with plus and minus marks. |
| Aspect frame | `render_aspect_frame` | `render_image`, `fit` | Wide rounded frame with two opposite corner brackets. |
| Frame guides | `render_frame_guides` | `render_image`, `fit` | Rounded frame with a rule-of-thirds grid and four accent nodes. |
## Output and result
Rule: one picture base (frame with landscape) with a lower-right round badge; the
size, background and quality icons draw their own simple subject.
| Meaning | Id | Finished references | Drawing |
|---|---|---|---|
| Render image | `render_execute` | `render_image`, `camera` | Picture with a play badge: create a rendered image. |
| Render output size | `render_output_size` | `render_image`, `fit` | Picture frame with a diagonal double-headed dimension arrow. |
| Transparent background | `render_transparent_background` | `material`, `render_image` | Grey cube on a checker tile. |
| Quality | `render_quality` | `list`, `library` | Three rising bars, the tallest in accent. |
| Check render | `render_check` | `render_image`, `new` | Picture with a check badge. |
| Cancel render | `render_cancel` | `render_image`, `delete` | Picture with a square stop badge. |
| Export image | `render_export_image` | `render_image`, `copy` | Picture with a large outward diagonal arrow at the upper right. |
| Compare | `render_compare` | `render_image`, `material` | Two grey pictures split by a vertical slider with opposed arrows. |
| Results | `render_results` | `render_image`, `copy` | Stack of two pictures, the front one showing a landscape. |
+4
View File
@@ -45,3 +45,7 @@ sketch_chamfer_equal = { master = "sketch_chamfer", accent = "sketch" }
sketch_chamfer_two = { master = "sketch_chamfer_two_distances", accent = "sketch" }
offset_one_side = { master = "offset", accent = "sketch" }
bom = "list"
# Render workspace drafts: shared meanings; owner review remains Open.
# Removing a look override returns the body to its material look.
render_remove_override = "render_from_material"
+47
View File
@@ -101,6 +101,7 @@
"error": "b7188fcdfadf1512ddd2896805a14618179789f87047b219560a7dab74bd360f",
"explode": "2aecb05d5f05aff1ab629066cb9bea8718ea213ac70948ef71a3ced3321f4cb2",
"export": "c8c2abf007e43e896479d78798e3976ba9eda809507c820bb38538f77db696ac",
"export_tray": "f18606e1f63726c57a0787dced4fb8a221b4a4fc18b88b0bbdee743e546cb489",
"extend": "28c63df352c77d02feffe44cf09fb0fc1d48f7905aaabbfa6dba601dbb70f8e8",
"extrude": "9cc1538971c5d25467aaed23f739899d9d102fd07f86472ac3197c54ca2e1144",
"eye": "2449167c8410826dc1106b687c12a309d5bea8f7817b897943e80f182a163c2d",
@@ -122,6 +123,7 @@
"hole": "f82d47751f826e08682db847a270897474661bfe9f35b6d3678f49a203172563",
"home": "ab5f1c41d377687e4c5a808dfdccf2757b33dd43f934d44df43d6e9118176853",
"import": "e343540eb49fdb1e53c4d54b9e60ae513b3103df6c9133048bed878e58067455",
"import_tray": "c6aec9556dc8ecb6c953d6bf1f384f220c68167a92e444950c24ded81f66a0b6",
"include": "f5b80ad3e06e3b863fcfbe415ec912e4e09653dd519f9ac207819339fe34730f",
"infinite": "53fb781131078d2f8e71be23aa610d9f9d9c8bb79ba5f97fa397475e9ff8c3bd",
"inspect": "d2b4644b2d5f1aa50cb576b9265e275fa91201e296c2bc79865f810067d538e9",
@@ -206,8 +208,45 @@
"redo": "28e925493fa9e5f52e2c8e3beeb288af13fdb4f85f3cd92270096a10561e7a1a",
"refold": "1fa6a8604af7d04c01bc31d0e5875eb2cc91f5fa6bfc5491dd6680d3299eb26f",
"rename": "6887c5467101dca1d213154020d3b03be9b2704ac310de9d0a8ebde62c013bf5",
"render_all_shots": "9c04f49624cc2685472041cb3f3e95da2c5f32deeebb9723f4a8e3c369680a5b",
"render_appearance": "53287b30915a266d8a3858d5bef24b93ed11f8d612c20c445d3032f400a925cc",
"render_aspect_frame": "5d74b263eeb03eb0494b3774817135b8f119caff205a8b31c480d7dd0e5a8554",
"render_backdrop": "d27f7d6da7a31c5944dd0ff67c21838690f000f2d87b0bf5274bef0744de34ce",
"render_cancel": "2f1d8b3039007ccfc224390758e05d6202fa2eabf63724c744217dcab5d58e5e",
"render_check": "65ccf0407d80937c6931cbf87b6d0e628cfb62ca3e841483d02dd9de19c3717c",
"render_compare": "1b2d3f3920782490b96b4d14a7cc9506eff98ff2efae00f86d911fabcabc7cb0",
"render_copy_look": "25e9138a839a7e6b8225b48cd2f36daba42a928eff541fe616f3df39a1bc82b7",
"render_environment": "50cb18380d9310f6e04084c24ac85529599966b1d90c6bef514f4e63f9b30854",
"render_execute": "e4d9a66f4569e00c4b4b2ac05a80a9f3eba4e59802c62d3af5bb951ba9153cbf",
"render_export_image": "2456f55fa3b50ab84ae871963d4a6b6e92f486cf6ed3039f3a2f07f11192a655",
"render_exposure": "60eb5fa040aefe872ab2de66099493513a494ab758a23d443753d033eaf3a05d",
"render_fit_product": "8e58b293a39a06fca4cda0543adc2afef3d0041fe34b0bc0c800afb2d5635123",
"render_frame_guides": "cb1df01051de42047e38de9dd2a96fcb5b67bd5979cb5756412890400b69e5bc",
"render_from_material": "c928daa803a28f90ce24096731b8de711c6cd3215259ac9cd761b6be2e1b9618",
"render_ground": "2b900b433870cbc4bd0282e54c604ec696aa28354c753a13b09806f5e87d99c3",
"render_image": "e3dd622591de4fdc211f9bd3c6dcb54b01a308e8bcebd02bb80dd6b3982ce2a5",
"render_lens": "79bb615d46a05ea5ffc9e2e905cc25f266b3b7e761c160f7d25bbdb3dea0a430",
"render_library": "828af74034a62616c774ab0cbe70b7cf9fd42778b574febfb1812f8aa17fd49b",
"render_light_area": "b1f3ee596ee1d35f6e8444b54e94f36a34c2129697c88e5286dbf38565f50816",
"render_light_by_highlight": "5b37c8c6aa27ac2002dd8c8094e415c64e9eb315a55c4f146e328314e8f639a2",
"render_light_point": "a602cb5fa26f04f1bb9be7c14e5a9a9ce52b37550dfd3211a55e915f941f5803",
"render_light_spot": "a9413de906266bec0165b4cb06ecac8e36dce0b6ddec4bc4cc6ff4f152601483",
"render_lighting_presets": "e0a08433f548a9dd139296107b04b1d4c57ff04303c9bee32d896e2ea06a8d07",
"render_output_size": "f08fe354e4d5a6dd21af29cb077ceed96c413c68f48d9f72548ae1614337f88b",
"render_paint": "e3d3202205a5a90906348cae519340d38f475e42039a7f60c835cf7a21081da1",
"render_pick_up": "df67d10f333a3c9e367897d599a4be510f10ed36c24763c26817f22d24ff35b7",
"render_quality": "28fe44cdd2b2f58b91d91dcf20a6dfbd22cf3db6008e1db179717523ecc4889e",
"render_remove_override": "c928daa803a28f90ce24096731b8de711c6cd3215259ac9cd761b6be2e1b9618",
"render_replace_all": "3bbf830619a91be186b9dc91726b8e47c293556d2ac8c93d87c71014c9f3abcd",
"render_results": "94e78898d88eef415e56ca5ec1e056089a2a5aa709abc69e5b8103d6096a019c",
"render_same_look": "2b5c69c523a1c949c22638c378803e77123095fddd36cfa58e94b2fd91ca61d7",
"render_scene": "1c4d7350f6f17ad637a7d633cb65a64c298af20321b1dbdac01b51d47a0e2822",
"render_shot": "4428080faa94f276c8a1d20eb38d04835ee5ae9d6337402371a5edf09f97ddc5",
"render_shots_strip": "1dcf5cc8e704691985284e0ca06e03b22540ea9d4cd23f4fb8675801ae7e69ec",
"render_solo_light": "abb9f8993be108a79d491bfaca32cde59d07a69db3ead58195e8ca0d8e8d909b",
"render_sun": "d86b51f3d4d83df4e932a22bb20d567513529a2eaaa42b93f744412eed2be901",
"render_transparent_background": "71dab4763516053734d4eec964b0899716d617597fe0d09e3bfa0cc36224dd7a",
"render_update_shot": "22d82ad332f8325395596c4bec4c1ec9bbb72d9fc4cd62ad658a93d7f5f803b9",
"reorder": "c45755c8f23575415397bffb316eed9f2d8c014f451ab6f8c3123aca1a99b675",
"replace_face": "fcee556c684a1138a30a2574ea565c98ff29f9368c2e861d0fb861155a57d28b",
"rest": "eab64961b5340bdbbd92affe0c9b8477dc297a3b49e05e39d5fa71c0f2fda66c",
@@ -224,10 +263,17 @@
"screw": "5e30570785360d6a591ea22862a82f1cf62228385c1e96382536db8a453ac791",
"section": "cdd13f99f15f6d59bfa21c747810608d6afd00cb8207faf0337928c8521e7eae",
"select": "fdf6431270006ca7409c60c37cfed59572de04229f104ec723e6010ffa2b7163",
"send": "f83b6cdab792f9567a81d46d7828ed20795513c57a2c7d00fbbe6bd14d2f5848",
"settings": "9193dd3a1007c43660d0d47b6499cb2571338b67949e1a99df112057b9bf2b8a",
"settings_keyboard": "4968c70788094365af9c6e2b1c15b2aac45cf21212a14b8a256daf1ffe17f41a",
"settings_monitor": "6fcea26e2fc1673c76c6e7fadac4a91812ed43250cfb363c4f40c0cacfbd7aa5",
"settings_mouse": "47754752ea6fb37ec9686c0460740935a4db30fb267f1136a9246aec9a17b3f1",
"settings_palette": "a060015a750ff4367c65c04b83bf43720e13a38d8dee35330e1d8880f89eaa22",
"settings_plug": "79c00b84b2d1d28e3bf6096639c75476b7e1ee0a326c34ad76a79ad866c0102e",
"sheet": "774e30923ef6f37dd9c0b2bc3e45705e83b3a76700bf071cd41155a365d0daa4",
"sheet_rules": "5d3a2f37d3b4a4262b3ddbdae53d61f29e0f8e284dd54006da25a7acebfa1114",
"shell": "a1959ea0d0441a479bf654b6e549e2e44a0bfdc9cda4eb46fe1fbfd884de7e38",
"shield": "5e2d2d157309e0daa72f7c53073dfdb0a474def972a041cd172567f1d1e485ec",
"silhouette": "2a848aed2f247b1c0439f830a34a513c93bc5493c40044c821d493d863ad996e",
"sketch": "b52e5862b1709a9a7e3ca00d7436d126fbfc01b38080006fa4e17dc581dfa103",
"sketch_break": "67a5fab332616e6d6a9aeae3bedc04ee0768179143ba34b795bdd98cea04bb24",
@@ -261,6 +307,7 @@
"status_error": "b7188fcdfadf1512ddd2896805a14618179789f87047b219560a7dab74bd360f",
"status_warning": "1ac4f0ccfac2c01f6f66cb42188f9ae053c4aef9c5bbd249f1fb09611eb4d825",
"stitch": "6c3205d64007b771ed78d6f8055867578439fb25d591563b32dbd3343c98732d",
"stop": "36967eb4d423370d0d6c679b0a71f8d09f4fb2c73eed3e8cc50c604c44b0c32b",
"style": "6c2b04234b82ea4858c80190cc7208c60f549358253005afc35ab4e29506b578",
"suppress": "c54fbc01c12c0175d78cd63f692e8aaf73fc2cac79752b2a3ad821ac32734343",
"surface": "ba0e839ce0ee03e7ff95a2389a5ecb20bd80c713064d9b1bd5f461f4a80d1921",
@@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Render all shots — every saved camera view at once</title>
<path d="M27.1 12L29.26 12L31.42 6.96Q31.85 5.52 33.22 5.52L36.82 5.52Q38.19 5.52 38.62 6.96L40.78 12L51.58 12Q55.18 12 55.18 15.6L55.18 31.44Q55.18 35.04 51.58 35.04L27.1 35.04Q23.5 35.04 23.5 31.44L23.5 15.6Q23.5 12 27.1 12Z" fill="#e2e8ef" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M16.6 21L18.76 21L20.92 15.96Q21.35 14.52 22.72 14.52L26.32 14.52Q27.69 14.52 28.12 15.96L30.28 21L41.08 21Q44.68 21 44.68 24.6L44.68 40.44Q44.68 44.04 41.08 44.04L16.6 44.04Q13 44.04 13 40.44L13 24.6Q13 21 16.6 21Z" fill="#b8c3cf" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M6.1 30L8.26 30L10.42 24.96Q10.85 23.52 12.22 23.52L15.82 23.52Q17.19 23.52 17.62 24.96L19.78 30L30.58 30Q34.18 30 34.18 33.6L34.18 49.44Q34.18 53.04 30.58 53.04L6.1 53.04Q2.5 53.04 2.5 49.44L2.5 33.6Q2.5 30 6.1 30Z" fill="#b8c3cf" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<circle cx="18.34" cy="41.52" r="7.56" fill="#b9ddf8" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<circle cx="47.5" cy="47.5" r="14" fill="#ffffff"/>
<circle cx="47.5" cy="47.5" r="12" fill="#236ead"/>
<path d="M43.9 41.7L53.3 47.5L43.9 53.3Z" fill="#ffffff" stroke="#ffffff" stroke-width="1.6" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

@@ -0,0 +1,14 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Appearance — a body wearing the look sphere</title>
<path d="M17.33 24.8L38.98 37.3L38.98 62.3L17.33 49.8Z" fill="#328bd3"/>
<path d="M60.63 24.8L38.98 37.3L38.98 62.3L60.63 49.8Z" fill="#236ead"/>
<path d="M38.98 12.3L60.63 24.8L38.98 37.3L17.33 24.8Z" fill="#b9ddf8"/>
<path d="M38.98 12.3L60.63 24.8L60.63 49.8L38.98 62.3L17.33 49.8L17.33 24.8Z" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M17.33 24.8L38.98 37.3L60.63 24.8" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M38.98 37.3L38.98 62.3" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<circle cx="18.98" cy="17.3" r="16.88" fill="#ffffff"/>
<circle cx="18.98" cy="17.3" r="14" fill="#328bd3"/>
<path d="M32.14 12.51A14 14 0 0 1 14.19 30.46A18.31 18.31 0 0 0 32.14 12.51Z" fill="#236ead"/>
<circle cx="14.22" cy="12.54" r="3.78" fill="#b9ddf8"/>
<circle cx="18.98" cy="17.3" r="14" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Aspect frame — the shape of the picture</title>
<path d="M13 17L51 17A4 4 0 0 1 55 21L55 43A4 4 0 0 1 51 47L13 47A4 4 0 0 1 9 43L9 21A4 4 0 0 1 13 17Z" fill="#e2e8ef" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M4.5 23.5L4.5 12.5L15.5 12.5" fill="none" stroke="#236ead" stroke-width="4.25" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M59.5 40.5L59.5 51.5L48.5 51.5" fill="none" stroke="#236ead" stroke-width="4.25" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 614 B

@@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Backdrop — the coloured wall behind the product</title>
<path d="M30.69 4L54.25 17.6L54.25 41.6L30.69 28Z" fill="#328bd3" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M18.91 29.2L30 35.6L30 48.4L18.91 42Z" fill="#b8c3cf"/>
<path d="M41.09 29.2L30 35.6L30 48.4L41.09 42Z" fill="#8e9faf"/>
<path d="M30 22.8L41.09 29.2L30 35.6L18.91 29.2Z" fill="#e2e8ef"/>
<path d="M30 22.8L41.09 29.2L41.09 42L30 48.4L18.91 42L18.91 29.2Z" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M18.91 29.2L30 35.6L41.09 29.2" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M30 35.6L30 48.4" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 928 B

@@ -0,0 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Cancel render — stop the running render</title>
<path d="M9 12L45 12A5 5 0 0 1 50 17L50 43A5 5 0 0 1 45 48L9 48A5 5 0 0 1 4 43L4 17A5 5 0 0 1 9 12Z" fill="#e2e8ef"/>
<defs><clipPath id="pic1"><path d="M9 12L45 12A5 5 0 0 1 50 17L50 43A5 5 0 0 1 45 48L9 48A5 5 0 0 1 4 43L4 17A5 5 0 0 1 9 12Z" fill="none"/></clipPath></defs>
<g clip-path="url(#pic1)">
<path d="M4 40.8L17.8 27.12L30.68 38.64L38.04 32.88L50 42.96L50 48L4 48Z" fill="#8e9faf"/>
</g>
<circle cx="36.2" cy="22.8" r="4.2" fill="#8e9faf"/>
<path d="M9 12L45 12A5 5 0 0 1 50 17L50 43A5 5 0 0 1 45 48L9 48A5 5 0 0 1 4 43L4 17A5 5 0 0 1 9 12Z" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<circle cx="47.5" cy="47.5" r="14" fill="#ffffff"/>
<circle cx="47.5" cy="47.5" r="12" fill="#236ead"/>
<path d="M44.1 42.9L50.9 42.9A1.2 1.2 0 0 1 52.1 44.1L52.1 50.9A1.2 1.2 0 0 1 50.9 52.1L44.1 52.1A1.2 1.2 0 0 1 42.9 50.9L42.9 44.1A1.2 1.2 0 0 1 44.1 42.9Z" fill="#ffffff" stroke="#ffffff" stroke-width="1.2" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

@@ -0,0 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Check render — verify the request is ready, without rendering</title>
<path d="M9 12L45 12A5 5 0 0 1 50 17L50 43A5 5 0 0 1 45 48L9 48A5 5 0 0 1 4 43L4 17A5 5 0 0 1 9 12Z" fill="#e2e8ef"/>
<defs><clipPath id="pic1"><path d="M9 12L45 12A5 5 0 0 1 50 17L50 43A5 5 0 0 1 45 48L9 48A5 5 0 0 1 4 43L4 17A5 5 0 0 1 9 12Z" fill="none"/></clipPath></defs>
<g clip-path="url(#pic1)">
<path d="M4 40.8L17.8 27.12L30.68 38.64L38.04 32.88L50 42.96L50 48L4 48Z" fill="#8e9faf"/>
</g>
<circle cx="36.2" cy="22.8" r="4.2" fill="#8e9faf"/>
<path d="M9 12L45 12A5 5 0 0 1 50 17L50 43A5 5 0 0 1 45 48L9 48A5 5 0 0 1 4 43L4 17A5 5 0 0 1 9 12Z" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<circle cx="47.5" cy="47.5" r="14" fill="#ffffff"/>
<circle cx="47.5" cy="47.5" r="12" fill="#236ead"/>
<path d="M41.9 47.7L45.9 51.9L53.3 43.1" fill="none" stroke="#ffffff" stroke-width="3.6" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

@@ -0,0 +1,19 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Compare — live preview and render side by side</title>
<path d="M9 12L55 12A5 5 0 0 1 60 17L60 47A5 5 0 0 1 55 52L9 52A5 5 0 0 1 4 47L4 17A5 5 0 0 1 9 12Z" fill="#e2e8ef"/>
<defs><clipPath id="cmpL"><path d="M4 12L32 12L32 52L4 52Z" fill="none"/></clipPath></defs>
<g clip-path="url(#cmpL)">
<path d="M9 12L55 12A5 5 0 0 1 60 17L60 47A5 5 0 0 1 55 52L9 52A5 5 0 0 1 4 47L4 17A5 5 0 0 1 9 12Z" fill="#b8c3cf"/>
</g>
<defs><clipPath id="cmp"><path d="M9 12L55 12A5 5 0 0 1 60 17L60 47A5 5 0 0 1 55 52L9 52A5 5 0 0 1 4 47L4 17A5 5 0 0 1 9 12Z" fill="none"/></clipPath></defs>
<g clip-path="url(#cmp)">
<path d="M4 44L20.8 28.8L36.48 41.6L45.44 35.2L60 46.4L60 52L4 52Z" fill="#8e9faf"/>
</g>
<path d="M9 12L55 12A5 5 0 0 1 60 17L60 47A5 5 0 0 1 55 52L9 52A5 5 0 0 1 4 47L4 17A5 5 0 0 1 9 12Z" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M32 5L32 59" fill="none" stroke="#ffffff" stroke-width="8.6" stroke-linejoin="round" stroke-linecap="round"/>
<circle cx="32" cy="32" r="14.5" fill="#ffffff"/>
<path d="M32 5L32 59" fill="none" stroke="#236ead" stroke-width="5.6" stroke-linejoin="round" stroke-linecap="round"/>
<circle cx="32" cy="32" r="12.8" fill="#236ead"/>
<path d="M23.4 32L29.8 25.6L29.8 38.4Z" fill="#ffffff" stroke="#ffffff" stroke-width="1.2" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M40.6 32L34.2 25.6L34.2 38.4Z" fill="#ffffff" stroke="#ffffff" stroke-width="1.2" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

@@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Copy Look — the look in front of a copy outline</title>
<path d="M8 40L8 12.5A4.5 4.5 0 0 1 12.5 8L40 8" fill="none" stroke="currentColor" stroke-width="4.25" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M24 19L51 19A5 5 0 0 1 56 24L56 51A5 5 0 0 1 51 56L24 56A5 5 0 0 1 19 51L19 24A5 5 0 0 1 24 19Z" fill="none" stroke="currentColor" stroke-width="4.25" stroke-linejoin="round" stroke-linecap="round"/>
<circle cx="37.5" cy="37.5" r="12" fill="#328bd3"/>
<path d="M48.78 33.4A12 12 0 0 1 33.4 48.78A15.69 15.69 0 0 0 48.78 33.4Z" fill="#236ead"/>
<circle cx="33.42" cy="33.42" r="3.24" fill="#b9ddf8"/>
<circle cx="37.5" cy="37.5" r="12" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 841 B

@@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Environment — a sky dome lighting the product</title>
<path d="M4.04 42A25.96 25.96 0 0 1 55.96 42A25.96 14.99 0 0 1 4.04 42Z" fill="#b9ddf8" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M18.91 29.2L30 35.6L30 48.4L18.91 42Z" fill="#b8c3cf"/>
<path d="M41.09 29.2L30 35.6L30 48.4L41.09 42Z" fill="#8e9faf"/>
<path d="M30 22.8L41.09 29.2L30 35.6L18.91 29.2Z" fill="#e2e8ef"/>
<path d="M30 22.8L41.09 29.2L41.09 42L30 48.4L18.91 42L18.91 29.2Z" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M18.91 29.2L30 35.6L41.09 29.2" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M30 35.6L30 48.4" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 948 B

@@ -0,0 +1,12 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Render image — start rendering the picture</title>
<path d="M9 12L45 12A5 5 0 0 1 50 17L50 43A5 5 0 0 1 45 48L9 48A5 5 0 0 1 4 43L4 17A5 5 0 0 1 9 12Z" fill="#e2e8ef"/>
<defs><clipPath id="pic1"><path d="M9 12L45 12A5 5 0 0 1 50 17L50 43A5 5 0 0 1 45 48L9 48A5 5 0 0 1 4 43L4 17A5 5 0 0 1 9 12Z" fill="none"/></clipPath></defs>
<g clip-path="url(#pic1)">
<path d="M4 40.8L17.8 27.12L30.68 38.64L38.04 32.88L50 42.96L50 48L4 48Z" fill="#328bd3"/>
</g>
<path d="M9 12L45 12A5 5 0 0 1 50 17L50 43A5 5 0 0 1 45 48L9 48A5 5 0 0 1 4 43L4 17A5 5 0 0 1 9 12Z" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<circle cx="47.5" cy="47.5" r="14" fill="#ffffff"/>
<circle cx="47.5" cy="47.5" r="12" fill="#236ead"/>
<path d="M43.9 41.7L53.3 47.5L43.9 53.3Z" fill="#ffffff" stroke="#ffffff" stroke-width="1.6" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 993 B

@@ -0,0 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Export image — save the finished picture as a PNG file</title>
<path d="M9 18L43 18A5 5 0 0 1 48 23L48 49A5 5 0 0 1 43 54L9 54A5 5 0 0 1 4 49L4 23A5 5 0 0 1 9 18Z" fill="#e2e8ef"/>
<defs><clipPath id="pic1"><path d="M9 18L43 18A5 5 0 0 1 48 23L48 49A5 5 0 0 1 43 54L9 54A5 5 0 0 1 4 49L4 23A5 5 0 0 1 9 18Z" fill="none"/></clipPath></defs>
<g clip-path="url(#pic1)">
<path d="M4 46.8L17.2 33.12L29.52 44.64L36.56 38.88L48 48.96L48 54L4 54Z" fill="#8e9faf"/>
</g>
<path d="M9 18L43 18A5 5 0 0 1 48 23L48 49A5 5 0 0 1 43 54L9 54A5 5 0 0 1 4 49L4 23A5 5 0 0 1 9 18Z" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M28 36L47.77 17.55" fill="none" stroke="#ffffff" stroke-width="11.6" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M58 8L53.52 25.18L40.55 11.29Z" fill="#ffffff" stroke="#ffffff" stroke-width="6" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M28 36L47.77 17.55" fill="none" stroke="#236ead" stroke-width="5.6" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M58 8L53.52 25.18L40.55 11.29Z" fill="#236ead" stroke="#236ead" stroke-width="2" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

@@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Exposure — brighten or darken the picture</title>
<circle cx="32" cy="32" r="25" fill="#e2e8ef"/>
<path d="M32 7A25 25 0 0 1 32 57Z" fill="#236ead"/>
<circle cx="32" cy="32" r="25" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M14 32L27 32M20.5 25.5L20.5 38.5" fill="none" stroke="#34465b" stroke-width="4.25" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M37 32L50 32" fill="none" stroke="#ffffff" stroke-width="4.25" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 630 B

@@ -0,0 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Fit product — frame the whole product in the view</title>
<path d="M15.98 22.75L32 32L32 50.5L15.98 41.25Z" fill="#b8c3cf"/>
<path d="M48.02 22.75L32 32L32 50.5L48.02 41.25Z" fill="#8e9faf"/>
<path d="M32 13.5L48.02 22.75L32 32L15.98 22.75Z" fill="#e2e8ef"/>
<path d="M32 13.5L48.02 22.75L48.02 41.25L32 50.5L15.98 41.25L15.98 22.75Z" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M15.98 22.75L32 32L48.02 22.75" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M32 32L32 50.5" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M5.5 18.5L5.5 5.5L18.5 5.5" fill="none" stroke="#236ead" stroke-width="4.25" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M58.5 18.5L58.5 5.5L45.5 5.5" fill="none" stroke="#236ead" stroke-width="4.25" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M58.5 45.5L58.5 58.5L45.5 58.5" fill="none" stroke="#236ead" stroke-width="4.25" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M5.5 45.5L5.5 58.5L18.5 58.5" fill="none" stroke="#236ead" stroke-width="4.25" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

@@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Frame guides — rule of thirds over the picture</title>
<path d="M10 14L54 14A4 4 0 0 1 58 18L58 46A4 4 0 0 1 54 50L10 50A4 4 0 0 1 6 46L6 18A4 4 0 0 1 10 14Z" fill="#e2e8ef" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M23.33 15.3L23.33 48.7M7.3 26L56.7 26M40.67 15.3L40.67 48.7M7.3 38L56.7 38" fill="none" stroke="#6f879c" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<circle cx="23.33" cy="26" r="3.6" fill="#236ead"/>
<circle cx="23.33" cy="38" r="3.6" fill="#236ead"/>
<circle cx="40.67" cy="26" r="3.6" fill="#236ead"/>
<circle cx="40.67" cy="38" r="3.6" fill="#236ead"/>
</svg>

After

Width:  |  Height:  |  Size: 742 B

@@ -0,0 +1,15 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>From Material — custom look crossed off, material shows</title>
<path d="M6 38L32 53.01L32 57.01L6 42Z" fill="#b8c3cf"/>
<path d="M58 38L32 53.01L32 57.01L58 42Z" fill="#8e9faf"/>
<path d="M32 22.99L58 38L32 53.01L6 38Z" fill="#e2e8ef"/>
<path d="M32 22.99L58 38L58 42L32 57.01L6 42L6 38Z" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M6 38L32 53.01L58 38" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M32 53.01L32 57.01" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<circle cx="32" cy="22" r="15" fill="#328bd3"/>
<path d="M46.1 16.87A15 15 0 0 1 26.87 36.1A19.62 19.62 0 0 0 46.1 16.87Z" fill="#236ead"/>
<circle cx="26.9" cy="16.9" r="4.05" fill="#b9ddf8"/>
<circle cx="32" cy="22" r="15" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M21 11L43 33M43 11L21 33" fill="none" stroke="#ffffff" stroke-width="6.65" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M21 11L43 33M43 11L21 33" fill="none" stroke="currentColor" stroke-width="4.25" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

@@ -0,0 +1,16 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Ground — the floor the product stands on, with its contact shadow</title>
<path d="M5.06 42L30 56.4L30 58.8L5.06 44.4Z" fill="#328bd3"/>
<path d="M54.94 42L30 56.4L30 58.8L54.94 44.4Z" fill="#236ead"/>
<path d="M30 27.6L54.94 42L30 56.4L5.06 42Z" fill="#b9ddf8"/>
<path d="M30 27.6L54.94 42L54.94 44.4L30 58.8L5.06 44.4L5.06 42Z" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M5.06 42L30 56.4L54.94 42" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M30 56.4L30 58.8" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M30 35.6L45.93 44.8L34.85 51.2L18.91 42Z" fill="#328bd3"/>
<path d="M18.91 29.2L30 35.6L30 48.4L18.91 42Z" fill="#b8c3cf"/>
<path d="M41.09 29.2L30 35.6L30 48.4L41.09 42Z" fill="#8e9faf"/>
<path d="M30 22.8L41.09 29.2L30 35.6L18.91 29.2Z" fill="#e2e8ef"/>
<path d="M30 22.8L41.09 29.2L41.09 42L30 48.4L18.91 42L18.91 29.2Z" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M18.91 29.2L30 35.6L41.09 29.2" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M30 35.6L30 48.4" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Lens — the camera lens and its field of view</title>
<circle cx="32" cy="32" r="27" fill="#b8c3cf" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<circle cx="32" cy="32" r="19" fill="#b9ddf8" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<circle cx="32" cy="32" r="8.5" fill="none" stroke="#236ead" stroke-width="4.25" stroke-linejoin="round" stroke-linecap="round"/>
<circle cx="21.75" cy="21.75" r="3" fill="#e2e8ef"/>
</svg>

After

Width:  |  Height:  |  Size: 587 B

@@ -0,0 +1,17 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Appearance library — looks on a shelf</title>
<circle cx="13" cy="28.35" r="9.5" fill="#b8c3cf"/>
<path d="M21.93 25.1A9.5 9.5 0 0 1 9.75 37.28A12.42 12.42 0 0 0 21.93 25.1Z" fill="#8e9faf"/>
<circle cx="9.77" cy="25.12" r="2.57" fill="#e2e8ef"/>
<circle cx="13" cy="28.35" r="9.5" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<circle cx="51" cy="28.35" r="9.5" fill="#b8c3cf"/>
<path d="M59.93 25.1A9.5 9.5 0 0 1 47.75 37.28A12.42 12.42 0 0 0 59.93 25.1Z" fill="#8e9faf"/>
<circle cx="47.77" cy="25.12" r="2.57" fill="#e2e8ef"/>
<circle cx="51" cy="28.35" r="9.5" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<circle cx="32" cy="28.35" r="13.97" fill="#ffffff"/>
<circle cx="32" cy="28.35" r="11.5" fill="#328bd3"/>
<path d="M42.81 24.42A11.5 11.5 0 0 1 28.07 39.16A15.04 15.04 0 0 0 42.81 24.42Z" fill="#236ead"/>
<circle cx="28.09" cy="24.44" r="3.11" fill="#b9ddf8"/>
<circle cx="32" cy="28.35" r="11.5" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M7 42.35L57 42.35A2 2 0 0 1 59 44.35L59 46.35A2 2 0 0 1 57 48.35L7 48.35A2 2 0 0 1 5 46.35L5 44.35A2 2 0 0 1 7 42.35Z" fill="#8e9faf" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Area light — angled panel and dotted direction stem</title>
<path d="M23.34 11L54.52 29L40.66 37L9.48 19Z" fill="#e2e8ef" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<circle cx="32" cy="44" r="2.125" fill="#236ead"/>
<circle cx="32" cy="51" r="2.125" fill="#236ead"/>
<circle cx="32" cy="58" r="2.125" fill="#236ead"/>
</svg>

After

Width:  |  Height:  |  Size: 448 B

@@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Light by highlight — dotted aim stem and sphere target</title>
<circle cx="32" cy="9" r="2.125" fill="#236ead"/>
<circle cx="32" cy="16" r="2.125" fill="#236ead"/>
<circle cx="32" cy="23" r="2.125" fill="#236ead"/>
<circle cx="32" cy="42" r="15" fill="#e2e8ef"/>
<path d="M44.29 33.4A15 15 0 0 1 23.4 54.29A19.62 19.62 0 0 0 44.29 33.4Z" fill="#b8c3cf"/>
<circle cx="32" cy="42" r="15" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<circle cx="32" cy="37.5" r="5.5" fill="#e2e8ef" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 712 B

@@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Point light — source disc and dotted direction stem</title>
<circle cx="32" cy="23" r="15" fill="#e2e8ef" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<circle cx="32" cy="23" r="2.125" fill="#34465b"/>
<circle cx="32" cy="44" r="2.125" fill="#236ead"/>
<circle cx="32" cy="51" r="2.125" fill="#236ead"/>
<circle cx="32" cy="58" r="2.125" fill="#236ead"/>
</svg>

After

Width:  |  Height:  |  Size: 485 B

@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Spot light — cone source and dotted direction stem</title>
<path d="M32 8L49 33Q32 43 15 33Z" fill="#e2e8ef" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<circle cx="32" cy="44" r="2.125" fill="#236ead"/>
<circle cx="32" cy="51" r="2.125" fill="#236ead"/>
<circle cx="32" cy="58" r="2.125" fill="#236ead"/>
</svg>

After

Width:  |  Height:  |  Size: 435 B

@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Lighting presets — a stack of ready-made light set-ups</title>
<path d="M24 5L54 5A5 5 0 0 1 59 10L59 40A5 5 0 0 1 54 45L24 45A5 5 0 0 1 19 40L19 10A5 5 0 0 1 24 5Z" fill="#b8c3cf" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M10 17L42 17A5 5 0 0 1 47 22L47 54A5 5 0 0 1 42 59L10 59A5 5 0 0 1 5 54L5 22A5 5 0 0 1 10 17Z" fill="#e2e8ef" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M18.3 41.55A11.5 11.5 0 1 1 33.7 41.55L33.2 45.55L18.8 45.55Z" fill="#b9ddf8" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M21.3 45.55L30.7 45.55A2.5 2.5 0 0 1 33.2 48.05L33.2 50.05A2.5 2.5 0 0 1 30.7 52.55L21.3 52.55A2.5 2.5 0 0 1 18.8 50.05L18.8 48.05A2.5 2.5 0 0 1 21.3 45.55Z" fill="#8e9faf" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1000 B

@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Output size — the picture size in pixels</title>
<path d="M9 12L55 12A5 5 0 0 1 60 17L60 47A5 5 0 0 1 55 52L9 52A5 5 0 0 1 4 47L4 17A5 5 0 0 1 9 12Z" fill="#e2e8ef" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M22.08 38.79L41.92 25.21" fill="none" stroke="#236ead" stroke-width="5.6" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M51 19L45.33 31.97L36.86 19.59Z" fill="#236ead" stroke="#236ead" stroke-width="2" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M13 45L18.67 32.03L27.14 44.41Z" fill="#236ead" stroke="#236ead" stroke-width="2" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 750 B

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Paint — flat brush with look-coloured bristles</title>
<path d="M26.13 59.09L22.24 55.2L22.94 48.83L16.58 49.54L14.46 47.42L15.17 41.06L8.8 41.76L4.91 37.87L19.06 23.73L40.27 44.94Z" fill="#328bd3" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M22.24 20.55L28.25 14.54L37.09 16.31L46.28 7.11A7.5 7.5 0 0 1 56.89 17.72L47.69 26.91L49.46 35.75L43.45 41.76Z" fill="#e2e8ef" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 589 B

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Pick Up — eyedropper with a look-coloured tube</title>
<path d="M8.96 52.22Q15.32 44.44 14.97 37.01L28.4 23.58L40.42 35.6L26.99 49.03Q19.56 48.68 11.78 55.04Z" fill="#328bd3" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M30.52 20.04L39.01 11.56A9.5 9.5 0 0 1 52.44 24.99L43.96 33.48Z" fill="#b8c3cf" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M25.22 18.98L29.46 14.74L49.26 34.54L45.02 38.78Z" fill="#e2e8ef" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 682 B

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Render quality — draft, balanced or high</title>
<path d="M10 38L17 38A3.5 3.5 0 0 1 20.5 41.5L20.5 52.5A3.5 3.5 0 0 1 17 56L10 56A3.5 3.5 0 0 1 6.5 52.5L6.5 41.5A3.5 3.5 0 0 1 10 38Z" fill="#e2e8ef" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M28.5 24L35.5 24A3.5 3.5 0 0 1 39 27.5L39 52.5A3.5 3.5 0 0 1 35.5 56L28.5 56A3.5 3.5 0 0 1 25 52.5L25 27.5A3.5 3.5 0 0 1 28.5 24Z" fill="#b8c3cf" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M47 8L54 8A3.5 3.5 0 0 1 57.5 11.5L57.5 52.5A3.5 3.5 0 0 1 54 56L47 56A3.5 3.5 0 0 1 43.5 52.5L43.5 11.5A3.5 3.5 0 0 1 47 8Z" fill="#328bd3" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 848 B

@@ -0,0 +1,18 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Replace All — every use of a look switched to another</title>
<circle cx="12.5" cy="13.3" r="10" fill="#b8c3cf"/>
<path d="M21.9 9.88A10 10 0 0 1 9.08 22.7A13.08 13.08 0 0 0 21.9 9.88Z" fill="#8e9faf"/>
<circle cx="9.1" cy="9.9" r="2.7" fill="#e2e8ef"/>
<circle cx="12.5" cy="13.3" r="10" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M36 36.8L25.39 34.68L33.88 26.19Z" fill="#236ead"/>
<path d="M21 21.8L30.34 31.14" fill="none" stroke="#236ead" stroke-width="4.25" stroke-linejoin="round" stroke-linecap="round"/>
<circle cx="52.5" cy="35.3" r="9" fill="#328bd3"/>
<path d="M60.96 32.22A9 9 0 0 1 49.42 43.76A11.77 11.77 0 0 0 60.96 32.22Z" fill="#236ead"/>
<circle cx="49.44" cy="32.24" r="2.43" fill="#b9ddf8"/>
<circle cx="52.5" cy="35.3" r="9" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<circle cx="46.5" cy="47.3" r="14.68" fill="#ffffff"/>
<circle cx="46.5" cy="47.3" r="12" fill="#328bd3"/>
<path d="M57.78 43.2A12 12 0 0 1 42.4 58.58A15.69 15.69 0 0 0 57.78 43.2Z" fill="#236ead"/>
<circle cx="42.42" cy="43.22" r="3.24" fill="#b9ddf8"/>
<circle cx="46.5" cy="47.3" r="12" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

@@ -0,0 +1,11 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Render results — open the finished renders</title>
<path d="M21 6L55 6A5 5 0 0 1 60 11L60 35A5 5 0 0 1 55 40L21 40A5 5 0 0 1 16 35L16 11A5 5 0 0 1 21 6Z" fill="#b8c3cf" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M9 17.5L43 17.5A7.5 7.5 0 0 1 50.5 25L50.5 51A7.5 7.5 0 0 1 43 58.5L9 58.5A7.5 7.5 0 0 1 1.5 51L1.5 25A7.5 7.5 0 0 1 9 17.5Z" fill="#ffffff"/>
<path d="M9 20L43 20A5 5 0 0 1 48 25L48 51A5 5 0 0 1 43 56L9 56A5 5 0 0 1 4 51L4 25A5 5 0 0 1 9 20Z" fill="#e2e8ef"/>
<defs><clipPath id="res"><path d="M9 20L43 20A5 5 0 0 1 48 25L48 51A5 5 0 0 1 43 56L9 56A5 5 0 0 1 4 51L4 25A5 5 0 0 1 9 20Z" fill="none"/></clipPath></defs>
<g clip-path="url(#res)">
<path d="M4 48.8L17.2 35.12L29.52 46.64L36.56 40.88L48 50.96L48 56L4 56Z" fill="#328bd3"/>
</g>
<path d="M9 20L43 20A5 5 0 0 1 48 25L48 51A5 5 0 0 1 43 56L9 56A5 5 0 0 1 4 51L4 25A5 5 0 0 1 9 20Z" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

@@ -0,0 +1,27 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Same Look — the bodies wearing this look selected</title>
<path d="M6.3 41.5L15.83 47L15.83 58L6.3 52.5Z" fill="#b8c3cf"/>
<path d="M25.36 41.5L15.83 47L15.83 58L25.36 52.5Z" fill="#8e9faf"/>
<path d="M15.83 36L25.36 41.5L15.83 47L6.3 41.5Z" fill="#e2e8ef"/>
<path d="M15.83 36L25.36 41.5L25.36 52.5L15.83 58L6.3 52.5L6.3 41.5Z" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M6.3 41.5L15.83 47L25.36 41.5" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M15.83 47L15.83 58" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M37.17 12L45.83 17L45.83 27L37.17 22Z" fill="#328bd3"/>
<path d="M54.48 12L45.83 17L45.83 27L54.48 22Z" fill="#236ead"/>
<path d="M45.83 7L54.48 12L45.83 17L37.17 12Z" fill="#b9ddf8"/>
<path d="M45.83 7L54.48 12L54.48 22L45.83 27L37.17 22L37.17 12Z" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M37.17 12L45.83 17L54.48 12" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M45.83 17L45.83 27" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M37.33 4L54.33 4A4.5 4.5 0 0 1 58.83 8.5L58.83 25.5A4.5 4.5 0 0 1 54.33 30L37.33 30A4.5 4.5 0 0 1 32.83 25.5L32.83 8.5A4.5 4.5 0 0 1 37.33 4Z" fill="none" stroke="#236ead" stroke-width="4.25" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M37.17 42L45.83 47L45.83 57L37.17 52Z" fill="#328bd3"/>
<path d="M54.48 42L45.83 47L45.83 57L54.48 52Z" fill="#236ead"/>
<path d="M45.83 37L54.48 42L45.83 47L37.17 42Z" fill="#b9ddf8"/>
<path d="M45.83 37L54.48 42L54.48 52L45.83 57L37.17 52L37.17 42Z" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M37.17 42L45.83 47L54.48 42" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M45.83 47L45.83 57" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M37.33 34L54.33 34A4.5 4.5 0 0 1 58.83 38.5L58.83 55.5A4.5 4.5 0 0 1 54.33 60L37.33 60A4.5 4.5 0 0 1 32.83 55.5L32.83 38.5A4.5 4.5 0 0 1 37.33 34Z" fill="none" stroke="#236ead" stroke-width="4.25" stroke-linejoin="round" stroke-linecap="round"/>
<circle cx="15.83" cy="17" r="11.5" fill="#328bd3"/>
<path d="M26.64 13.07A11.5 11.5 0 0 1 11.9 27.81A15.04 15.04 0 0 0 26.64 13.07Z" fill="#236ead"/>
<circle cx="11.92" cy="13.09" r="3.11" fill="#b9ddf8"/>
<circle cx="15.83" cy="17" r="11.5" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

@@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Shot — save the camera view as a named shot</title>
<path d="M9 19L12 19L15 12Q15.6 10 17.5 10L22.5 10Q24.4 10 25 12L28 19L43 19Q48 19 48 24L48 46Q48 51 43 51L9 51Q4 51 4 46L4 24Q4 19 9 19Z" fill="#b8c3cf" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<circle cx="26" cy="35" r="10.5" fill="#b9ddf8" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<circle cx="47.5" cy="47.5" r="14" fill="#ffffff"/>
<circle cx="47.5" cy="47.5" r="12" fill="#236ead"/>
<path d="M42 47.5L53 47.5M47.5 42L47.5 53" fill="none" stroke="#ffffff" stroke-width="3.4" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 759 B

@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Shots strip — show or hide the strip of shot thumbnails</title>
<path d="M8 19L56 19A5 5 0 0 1 61 24L61 40A5 5 0 0 1 56 45L8 45A5 5 0 0 1 3 40L3 24A5 5 0 0 1 8 19Z" fill="#b8c3cf" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M10.5 24.5L19.5 24.5A2.5 2.5 0 0 1 22 27L22 37A2.5 2.5 0 0 1 19.5 39.5L10.5 39.5A2.5 2.5 0 0 1 8 37L8 27A2.5 2.5 0 0 1 10.5 24.5Z" fill="#e2e8ef" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M27.5 24.5L36.5 24.5A2.5 2.5 0 0 1 39 27L39 37A2.5 2.5 0 0 1 36.5 39.5L27.5 39.5A2.5 2.5 0 0 1 25 37L25 27A2.5 2.5 0 0 1 27.5 24.5Z" fill="#328bd3" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M44.5 24.5L53.5 24.5A2.5 2.5 0 0 1 56 27L56 37A2.5 2.5 0 0 1 53.5 39.5L44.5 39.5A2.5 2.5 0 0 1 42 37L42 27A2.5 2.5 0 0 1 44.5 24.5Z" fill="#e2e8ef" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

@@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Solo light — point source and dotted stem inside focus brackets</title>
<path d="M13.5 5L5.5 5L5.5 13M50.5 5L58.5 5L58.5 13M58.5 51.5L58.5 59.5L50.5 59.5M13.5 59.5L5.5 59.5L5.5 51.5" fill="none" stroke="currentColor" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<circle cx="32" cy="23" r="15" fill="#e2e8ef" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<circle cx="32" cy="23" r="2.125" fill="#34465b"/>
<circle cx="32" cy="44" r="2.125" fill="#236ead"/>
<circle cx="32" cy="51" r="2.125" fill="#236ead"/>
<circle cx="32" cy="58" r="2.125" fill="#236ead"/>
</svg>

After

Width:  |  Height:  |  Size: 713 B

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Sun — source disc with short radial rays</title>
<circle cx="32" cy="32" r="15" fill="#e2e8ef" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M32 5.5L32 10.5M32 53.5L32 58.5M5.5 32L10.5 32M53.5 32L58.5 32M13.26 13.26L16.8 16.8M47.2 47.2L50.74 50.74M13.26 50.74L16.8 47.2M47.2 16.8L50.74 13.26" fill="none" stroke="#236ead" stroke-width="4.25" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 523 B

@@ -0,0 +1,16 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Transparent background — render with an alpha background</title>
<path d="M11 6L53 6A5 5 0 0 1 58 11L58 53A5 5 0 0 1 53 58L11 58A5 5 0 0 1 6 53L6 11A5 5 0 0 1 11 6Z" fill="#e2e8ef"/>
<defs><clipPath id="chk"><path d="M11 6L53 6A5 5 0 0 1 58 11L58 53A5 5 0 0 1 53 58L11 58A5 5 0 0 1 6 53L6 11A5 5 0 0 1 11 6Z" fill="none"/></clipPath></defs>
<g clip-path="url(#chk)">
<path d="M6 19L19 19L19 32L6 32ZM6 45L19 45L19 58L6 58ZM19 6L32 6L32 19L19 19ZM19 32L32 32L32 45L19 45ZM32 19L45 19L45 32L32 32ZM32 45L45 45L45 58L32 58ZM45 6L58 6L58 19L45 19ZM45 32L58 32L58 45L45 45Z" fill="#b9ddf8"/>
</g>
<path d="M11 6L53 6A5 5 0 0 1 58 11L58 53A5 5 0 0 1 53 58L11 58A5 5 0 0 1 6 53L6 11A5 5 0 0 1 11 6Z" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M32 18L44.12 25L44.12 39L32 46L19.88 39L19.88 25Z" fill="#ffffff" stroke="#ffffff" stroke-width="7.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M19.88 25L32 32L32 46L19.88 39Z" fill="#b8c3cf"/>
<path d="M44.12 25L32 32L32 46L44.12 39Z" fill="#8e9faf"/>
<path d="M32 18L44.12 25L32 32L19.88 25Z" fill="#e2e8ef"/>
<path d="M32 18L44.12 25L44.12 39L32 46L19.88 39L19.88 25Z" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M19.88 25L32 32L44.12 25" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M32 32L32 46" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

@@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Update shot — overwrite the active shot with the current camera</title>
<path d="M9 19L12 19L15 12Q15.6 10 17.5 10L22.5 10Q24.4 10 25 12L28 19L43 19Q48 19 48 24L48 46Q48 51 43 51L9 51Q4 51 4 46L4 24Q4 19 9 19Z" fill="#b8c3cf" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<circle cx="26" cy="35" r="10.5" fill="#b9ddf8" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<circle cx="47.5" cy="47.5" r="14" fill="#ffffff"/>
<circle cx="47.5" cy="47.5" r="12" fill="#236ead"/>
<path d="M50.97 43.36A5.4 5.4 0 1 1 43.08 44.4" fill="none" stroke="#ffffff" stroke-width="3" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M46.06 40.14L39.8 42.11L46.35 46.7Z" fill="#ffffff" stroke="#ffffff" stroke-width="0.8" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 930 B

+2
View File
@@ -1478,6 +1478,8 @@
"prefs.graphics_note": "وازن بين السرعة والنعومة. كل خيار يعرض أثره على عيّنة.",
"prefs.graphics_presets_note": "الإعدادات المسبقة تضبط الخيارات أدناه معًا.",
"prefs.history": "السجل",
"prefs.history_origin_bodies": "إظهار الأصل والأجسام في الأعلى",
"prefs.history_origin_bodies_note": "يبدأ السجل على اليسار أو تحت المستعرض بمجلد الأجسام للمكوّن النشط وبالأصل، مع أيقونات الإظهار والقوائم كما في المستعرض. لا يتغير السجل في الأسفل.",
"prefs.history_sketches": "الرسومات",
"prefs.history_sketches_in_order": "بالترتيب",
"prefs.history_sketches_note": "عند ضبط الرسومات على «تحت ميزتها»، يعرض السجل على اليسار كل رسم بمستوى واحد للداخل، تحت أول ميزة بُنيت منه، متى كان عريضًا بما يكفي لإظهار الأسماء. أما الخط الزمني في الأسفل فيحتفظ دائمًا بترتيب الإنشاء.",
+2
View File
@@ -1148,6 +1148,8 @@
"pick a hole to seat it, or OK places it at the origin": "Wählen Sie eine Bohrung zum Einsetzen, oder OK platziert es im Ursprung",
"pick one or two vertices, edges or faces": "Wählen Sie ein oder zwei Ecken, Kanten oder Flächen",
"prefs.cpu": "CPU (Software)",
"prefs.history_origin_bodies": "Ursprung und Körper oben zeigen",
"prefs.history_origin_bodies_note": "Der Verlauf links oder unter dem Browser beginnt mit dem Körper-Ordner der aktiven Komponente und dem Ursprung, mit Augen und Menüs wie im Browser. Der Verlauf unten bleibt unverändert.",
"prefs.minutes_suffix": " min",
"properties.title": "Eigenschaften: {name}",
"recovery.days_ago": "vor {count} Tagen",
+2
View File
@@ -1478,6 +1478,8 @@
"prefs.graphics_note": "Trade speed for smoothness. Each choice shows its effect on a sample.",
"prefs.graphics_presets_note": "Presets set the choices below together.",
"prefs.history": "History",
"prefs.history_origin_bodies": "Show origin and bodies at the top",
"prefs.history_origin_bodies_note": "The History on the left or under the browser starts with the active component's Bodies folder and the Origin, with their eyes and menus as in the browser. The History at the bottom is unchanged.",
"prefs.history_sketches": "Sketches",
"prefs.history_sketches_in_order": "In order",
"prefs.history_sketches_note": "With Sketches set to Under their feature, the History at the left lists each sketch one level in, under the first feature built from it, whenever it is wide enough for names. The timeline at the bottom always keeps the creation order.",
+2
View File
@@ -1478,6 +1478,8 @@
"prefs.graphics_note": "איזון בין מהירות לחלקות. כל בחירה מציגה את השפעתה על דוגמה.",
"prefs.graphics_presets_note": "הגדרות קבועות מראש קובעות יחד את הבחירות שלמטה.",
"prefs.history": "ציר הזמן",
"prefs.history_origin_bodies": "הצגת הראשית והגופים למעלה",
"prefs.history_origin_bodies_note": "ההיסטוריה משמאל או מתחת לדפדפן מתחילה בתיקיית הגופים של הרכיב הפעיל ובראשית, עם סמלי העין והתפריטים כמו בדפדפן. ההיסטוריה למטה לא משתנה.",
"prefs.history_sketches": "סקיצות",
"prefs.history_sketches_in_order": "לפי הסדר",
"prefs.history_sketches_note": "כשהסקיצות מוגדרות ל„מתחת לפיצ'ר שלהן”, ציר הזמן משמאל מציג כל סקיצה רמה אחת פנימה, מתחת לפיצ'ר הראשון שנבנה ממנה, בכל פעם שהוא רחב מספיק להצגת שמות. ציר הזמן בתחתית שומר תמיד על סדר היצירה.",
+2
View File
@@ -1478,6 +1478,8 @@
"prefs.graphics_note": "速度と滑らかさのバランスを調整します。各選択肢の効果をサンプルで確認できます。",
"prefs.graphics_presets_note": "プリセットは以下の設定をまとめて変更します。",
"prefs.history": "履歴",
"prefs.history_origin_bodies": "原点とボディを先頭に表示",
"prefs.history_origin_bodies_note": "左側またはブラウザー下の履歴の先頭に、アクティブなコンポーネントのボディフォルダーと原点を、ブラウザーと同じ表示切り替えとメニュー付きで表示します。下部の履歴は変わりません。",
"prefs.history_sketches": "スケッチ",
"prefs.history_sketches_in_order": "作成順",
"prefs.history_sketches_note": "スケッチ:「フィーチャーの下」では、左側の履歴に名前が表示される幅があるとき、スケッチをそれから作成した最初のフィーチャーの下に 1 段字下げして表示します。下部のタイムラインは常に作成順です。",
+2
View File
@@ -1242,6 +1242,8 @@
"prefs.graphics_note": "속도와 부드러움 중에서 선택합니다. 각 선택의 효과가 샘플에 표시됩니다.",
"prefs.graphics_presets_note": "프리셋은 아래의 선택 항목을 한 번에 설정합니다.",
"prefs.history": "이력",
"prefs.history_origin_bodies": "원점과 바디를 맨 위에 표시",
"prefs.history_origin_bodies_note": "왼쪽 또는 브라우저 아래의 히스토리가 활성 구성 요소의 바디 폴더와 원점으로 시작하며, 브라우저처럼 표시 아이콘과 메뉴가 있습니다. 아래쪽 히스토리는 바뀌지 않습니다.",
"prefs.hold_stop_snapping": "누르고 있으면 스냅 중지",
"prefs.icons_only": "아이콘만",
"prefs.imported_file": "가져온 파일",
+1289 -10
View File
File diff suppressed because it is too large Load Diff
+2
View File
@@ -1478,6 +1478,8 @@
"prefs.graphics_note": "在速度与平滑度之间取舍。每个选项均以示例展示效果。",
"prefs.graphics_presets_note": "预设会同时设置以下选项。",
"prefs.history": "历史",
"prefs.history_origin_bodies": "在顶部显示原点和实体",
"prefs.history_origin_bodies_note": "左侧或浏览器下方的历史以活动组件的实体文件夹和原点开头,并带有与浏览器相同的显示切换和菜单。底部的历史保持不变。",
"prefs.history_sketches": "草图",
"prefs.history_sketches_in_order": "按创建顺序",
"prefs.history_sketches_note": "草图:选择“位于其特征下”后,左侧历史在宽度足以显示名称时,会将草图缩进一级,列在由其创建的第一个特征下。底部时间轴始终保持创建顺序。",
+26 -3
View File
@@ -7487,14 +7487,37 @@ fn eval_feature(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st:
return replay(doc, vals, f, st, features, &mats, warning);
}
// Bodies: each copy is a new body named after its source.
let here = doc.component_transform(f.component);
let mut output = 0;
for n in bodies {
let mb = st.body(n).cloned().ok_or_else(|| DocError::Unknown(format!("body `{n}`")))?;
// A body of another component is held in that component's frame, the pattern's
// transforms in this feature's: each copy is moved as the pattern moves it in the
// world and kept beside its source, in its component (#4763). Taken as they were,
// the copies landed where the source sits before its component is placed.
let owner = (mb.component != f.component).then(|| {
let frame = doc.component_transform(mb.component);
let singular = || DocError::Invalid(format!("the component holding `{n}` has a singular placement"));
Ok::<_, DocError>((frame, crate::mat_inverse(&frame).ok_or_else(singular)?, crate::mat_inverse(&here).ok_or_else(singular)?))
});
let owner = owner.transpose()?;
for (k, m) in mats.iter().enumerate() {
let copy = moved_copy(&mb, kernel::transform_matrix(&mb.body, *m)?, k + 1);
let local = match &owner {
Some((frame, frame_inv, here_inv)) => {
crate::mat_mul(&crate::mat_mul(frame_inv, &here), &crate::mat_mul(&crate::mat_mul(m, here_inv), frame))
}
None => *m,
};
let copy = moved_copy(&mb, kernel::transform_matrix(&mb.body, local)?, k + 1);
let name = pattern_copy_name(st, f, output, &mb.name);
copy_sheet(st, &mb.name, &name, m);
st.bodies.push(ModelBody::traced_new(name, copy, f.id));
copy_sheet(st, &mb.name, &name, &local);
let mut body = ModelBody::traced_new(name, copy, f.id);
if owner.is_some() {
// Kept in that component as a moved body is (and through later features).
body.component = mb.component;
body.transferred = true;
}
st.bodies.push(body);
output += 1;
}
}
+33
View File
@@ -384,6 +384,39 @@ fn patterns_of_components_place_occurrences() {
assert!(s.execute("solid.pattern.circular", &json!({"components": ["Nope"], "axis": "Z", "count": 2})).is_err());
}
/// #4763: a body of a component moved 60 mm along X, patterned from the root. The copy goes
/// where the pattern puts the body as it is shown (beside it), not where the body sits before
/// its component is placed (x 10, near the origin).
#[test]
fn patterns_of_a_body_in_a_moved_component_follow_its_placement() {
for (kind, params, want) in [
("solid.pattern.path", json!({"bodies": ["Pin"], "path_sketch": "Path", "path": ["l1"], "count": 2, "spacing": 10}), [70.0, 0.0]),
("solid.pattern.rectangular", json!({"bodies": ["Pin"], "dir1": [1, 0, 0], "count1": 2, "spacing1": 10}), [70.0, 0.0]),
("solid.pattern.circular", json!({"bodies": ["Pin"], "axis": "Z", "count": 2, "angle": "90 deg"}), [-4.0, 60.0]),
] {
let mut s = Session::default();
run(&mut s, "component.create", json!({"name": "Screw"}));
run(&mut s, "solid.box", json!({"length": 4, "width": 4, "height": 4, "body_name": "Pin"}));
run(&mut s, "sketch.create", json!({"plane": "XY", "name": "Path"}));
run(&mut s, "sketch.line", json!({"points": [[0, 0], [20, 0]]}));
run(&mut s, "sketch.finish", json!({}));
run(&mut s, "component.activate", json!({"component": "root"}));
// The first part is grounded when it is made; this one is moved.
run(&mut s, "occurrence.ground", json!({"occurrence": 2, "grounded": false}));
run(&mut s, "occurrence.move", json!({"occurrence": 2, "translate": [60, 0, 0]}));
run(&mut s, kind, params);
let m = run(&mut s, "inspect.measure", json!({}));
let bodies = m["bodies"].as_array().unwrap();
let at = |name: &str| {
let b = bodies.iter().find(|b| b["name"] == name).unwrap_or_else(|| panic!("{kind}: no `{name}` in {m}"));
[b["bbox"]["min"][0].as_f64().unwrap(), b["bbox"]["min"][1].as_f64().unwrap()]
};
assert!((at("Pin")[0] - 60.0).abs() < 1e-6, "{kind}: the source stays at x 60");
let copy = at("Pin (2)");
assert!((copy[0] - want[0]).abs() < 1e-6 && (copy[1] - want[1]).abs() < 1e-6, "{kind}: the copy is at {copy:?}, want {want:?}");
}
}
#[test]
fn path_pattern_orientation_follows_the_path() {
let width_of_copy = |orient: &str| {
+42 -1
View File
@@ -319,7 +319,8 @@ fn isocurves(s: &mut Session, p: &Value) -> Result<Value> {
fn center_of_mass(s: &mut Session, p: &Value) -> Result<Value> {
let cmd = "inspect.center_of_mass";
let names = string_list(p, "bodies");
let st = s.model.state();
// The world state, as Measure reads: placed occurrences where they are, each placement once.
let st = s.world_state();
let mut out = Vec::new();
let (mut vol, mut moment) = (0.0, Vec3::ZERO);
for b in st.bodies.iter().filter(|b| names.is_empty() || names.contains(&b.name)) {
@@ -412,4 +413,44 @@ mod tests {
assert!(curves.iter().filter(|c| flat(c)).count() >= 4, "rings at constant height");
assert!(s.execute("inspect.isocurve", &json!({"faces": []})).is_err());
}
/// #5246: the centre of mass of an assembly is the world's, like Measure's and Properties':
/// a moved occurrence where it is, a copy counted and found by its world name.
#[test]
fn center_of_mass_measures_occurrences_where_they_are_placed() {
let mut s = Session::default();
for (c, p) in [
("solid.box", json!({"length": 100, "width": 100, "height": 10, "body_name": "Plate"})),
("component.create", json!({"name": "Fixture"})),
("solid.box", json!({"length": 10, "width": 10, "height": 10, "corner": [200, 0, 0], "body_name": "Block"})),
("component.activate", json!({"component": "root"})),
// The first part is grounded when it is made; this one is moved.
("occurrence.ground", json!({"occurrence": "Fixture:1", "grounded": false})),
("occurrence.move", json!({"occurrence": "Fixture:1", "translate": [0, 0, 100], "space": "world"})),
] {
s.execute(c, &p).unwrap_or_else(|e| panic!("{c}: {e}"));
}
let at = |r: &Value, k: usize| r["center"][k].as_f64().unwrap();
let r = s.execute("inspect.center_of_mass", &json!({"bodies": ["Block"]})).unwrap();
assert!((at(&r, 0) - 205.0).abs() < 1e-6 && (at(&r, 2) - 105.0).abs() < 1e-6, "{r}");
s.execute("occurrence.copy", &json!({"component": "Fixture", "translate": [0, 300, 0]})).unwrap();
let r = s.execute("inspect.center_of_mass", &json!({})).unwrap();
assert_eq!(r["bodies"].as_array().unwrap().len(), 3, "{r}");
assert!((r["volume"].as_f64().unwrap() - 102_000.0).abs() < 1e-6, "{r}");
// The same as Properties' mass centre: every body has the same density here.
let props = s.execute("component.properties", &json!({"component": "root"})).unwrap();
for k in 0..3 {
let want = props["total"]["center_of_mass"][k].as_f64().unwrap();
assert!((at(&r, k) - want).abs() < 1e-9, "{k}: {r} vs {props}");
}
let copy = r["bodies"]
.as_array()
.unwrap()
.iter()
.find(|b| b["body"] != "Plate" && b["body"] != "Block")
.expect("the copy, by its world name")["body"]
.clone();
let r = s.execute("inspect.center_of_mass", &json!({"bodies": [copy]})).unwrap();
assert!((at(&r, 1) - 305.0).abs() < 1e-6, "{r}");
}
}
+184 -34
View File
@@ -602,6 +602,9 @@ pub enum TriClass {
#[derive(Default, Clone)]
pub struct BlockBytes {
pub vertices: Arc<Vec<u8>>,
/// Each body's share of `vertices` and the box around it: a body out of the view is left
/// out of the draws ([`Batches::cull`]). Vertices outside every span are always drawn.
pub bodies: Arc<Vec<BodySpan>>,
pub opaque: Arc<Vec<std::ops::Range<u32>>>,
pub glass: Arc<Vec<std::ops::Range<u32>>>,
pub faded: Arc<Vec<std::ops::Range<u32>>>,
@@ -620,6 +623,43 @@ impl BlockBytes {
}
}
/// A body's vertices in a block and the box around them.
#[derive(Clone, Debug, PartialEq)]
pub struct BodySpan {
pub vertices: std::ops::Range<u32>,
pub bounds: [[f32; 3]; 2],
}
/// `runs` without the vertices in `hidden` (both sorted and disjoint).
fn runs_without(runs: &[std::ops::Range<u32>], hidden: &[std::ops::Range<u32>]) -> Vec<std::ops::Range<u32>> {
let mut out = Vec::with_capacity(runs.len());
let mut h = hidden.iter().peekable();
for r in runs {
let mut start = r.start;
while let Some(x) = h.peek() {
if x.end <= start {
h.next();
continue;
}
if x.start >= r.end {
break;
}
if x.start > start {
out.push(start..x.start);
}
start = start.max(x.end);
if x.end >= r.end {
break;
}
h.next();
}
if start < r.end {
out.push(start..r.end);
}
}
out
}
/// Add a triangle starting at vertex `at` to `runs`, extending the last run when it ends there.
pub fn extend_runs(runs: &mut Vec<std::ops::Range<u32>>, at: u32) {
let end = at.saturating_add(3);
@@ -638,9 +678,28 @@ pub struct BlockBuilder {
faded: Vec<std::ops::Range<u32>>,
lines: Vec<u8>,
hidden: Vec<u8>,
bodies: Vec<BodySpan>,
/// The body being written: its first vertex and the box so far.
open: Option<(u32, [[f32; 3]; 2])>,
}
impl BlockBuilder {
/// The triangles from here to [`BlockBuilder::end_body`] are one body's (its box is kept, so
/// it can be left out of a view it is not in).
pub fn begin_body(&mut self) {
let at = u32::try_from(self.vertices.len() / TRI_SIZE).unwrap_or(u32::MAX);
self.open = Some((at, [[f32::INFINITY; 3], [f32::NEG_INFINITY; 3]]));
}
/// End the body [`BlockBuilder::begin_body`] began.
pub fn end_body(&mut self) {
let Some((start, bounds)) = self.open.take() else { return };
let end = u32::try_from(self.vertices.len() / TRI_SIZE).unwrap_or(u32::MAX);
if end > start && bounds.iter().flatten().all(|x| x.is_finite()) {
self.bodies.push(BodySpan { vertices: start..end, bounds });
}
}
/// Room for `triangles` more triangles, so a block of large bodies is written without
/// reallocating (and copying) its vertex stream as it grows.
pub fn reserve_triangles(&mut self, triangles: usize) {
@@ -658,6 +717,14 @@ impl BlockBuilder {
}
}
self.vertices.extend_from_slice(&record);
if let Some((_, [lo, hi])) = &mut self.open {
for v in p {
for k in 0..3 {
lo[k] = lo[k].min(v[k]);
hi[k] = hi[k].max(v[k]);
}
}
}
let runs = match class {
TriClass::Opaque => &mut self.opaque,
TriClass::Glass => &mut self.glass,
@@ -678,21 +745,11 @@ impl BlockBuilder {
/// A depth-tested line, or one only shown where the model hides it.
pub fn line(&mut self, a: [f32; 3], b: [f32; 3], c: PaletteIndex, width: f32) {
let out = &mut self.lines;
for v in a.iter().chain(&b) {
out.extend_from_slice(&v.to_le_bytes());
}
out.extend_from_slice(&c.0.to_le_bytes());
out.extend_from_slice(&width.to_le_bytes());
self.lines.extend_from_slice(&line_record(a, b, c, width.to_bits()));
}
pub fn hidden_line(&mut self, a: [f32; 3], b: [f32; 3], c: PaletteIndex, width: f32) {
let out = &mut self.hidden;
for v in a.iter().chain(&b) {
out.extend_from_slice(&v.to_le_bytes());
}
out.extend_from_slice(&c.0.to_le_bytes());
out.extend_from_slice(&width.to_le_bytes());
self.hidden.extend_from_slice(&line_record(a, b, c, width.to_bits()));
}
/// How many vertices the block holds so far.
@@ -704,6 +761,7 @@ impl BlockBuilder {
pub fn freeze(self) -> Arc<BlockBytes> {
Arc::new(BlockBytes {
vertices: Arc::new(self.vertices),
bodies: Arc::new(self.bodies),
opaque: Arc::new(self.opaque),
glass: Arc::new(self.glass),
faded: Arc::new(self.faded),
@@ -742,11 +800,14 @@ impl GpuScene {
}
/// A line shown only where the model hides it.
pub fn hidden_line(&mut self, a: [f32; 3], b: [f32; 3], c: PaletteIndex, width: f32) {
for v in a.iter().chain(&b) {
self.hidden.extend_from_slice(&v.to_le_bytes());
}
self.hidden.extend_from_slice(&c.0.to_le_bytes());
self.hidden.extend_from_slice(&width.to_le_bytes());
self.hidden.extend_from_slice(&line_record(a, b, c, width.to_bits()));
}
/// Room for `lines` more lines and `hidden` more hidden lines, so a highlight of many edges
/// is written without growing its buffers line by line.
pub fn reserve_lines(&mut self, lines: usize, hidden: usize) {
let _ = self.lines.try_reserve(lines.saturating_mul(LINE_SIZE));
let _ = self.hidden.try_reserve(hidden.saturating_mul(LINE_SIZE));
}
pub fn glass_tri(&mut self, p: [f32; 3], n: [f32; 3], c: PaletteIndex) {
for v in p.iter().chain(&n) {
@@ -795,22 +856,30 @@ impl GpuScene {
/// unknown), so a face seen at a grazing angle no longer buries part of the band (it showed as
/// a thin wedge instead of a line). See [`lifted_width`] for how this travels.
pub fn lifted_line(&mut self, a: [f32; 3], b: [f32; 3], c: PaletteIndex, width: f32, normals: [[f32; 3]; 2]) {
for v in a.iter().chain(&b) {
self.lines.extend_from_slice(&v.to_le_bytes());
}
self.lines.extend_from_slice(&c.0.to_le_bytes());
self.lines.extend_from_slice(&lifted_width(width, normals).to_le_bytes());
self.lifted_line_word(a, b, c, lifted_width(width, normals));
}
/// [`GpuScene::lifted_line`] with its width word already worked out ([`lifted_width`]), for
/// many segments that share it.
pub fn lifted_line_word(&mut self, a: [f32; 3], b: [f32; 3], c: PaletteIndex, word: u32) {
self.lines.extend_from_slice(&line_record(a, b, c, word));
}
pub fn line(&mut self, a: [f32; 3], b: [f32; 3], c: PaletteIndex, width: f32, on_top: bool) {
let out = if on_top { &mut self.overlay } else { &mut self.lines };
for v in a.iter().chain(&b) {
out.extend_from_slice(&v.to_le_bytes());
}
out.extend_from_slice(&c.0.to_le_bytes());
out.extend_from_slice(&width.to_le_bytes());
out.extend_from_slice(&line_record(a, b, c, width.to_bits()));
}
}
/// One line's [`LINE_SIZE`] bytes: its ends, its palette index and its width word (an `f32`'s
/// bits, or [`lifted_width`]'s).
fn line_record(a: [f32; 3], b: [f32; 3], c: PaletteIndex, width: u32) -> [u8; LINE_SIZE] {
let mut out = [0u8; LINE_SIZE];
let words = a.into_iter().chain(b).map(f32::to_bits).chain([c.0, width]);
for (chunk, word) in out.as_chunks_mut::<4>().0.iter_mut().zip(words) {
*chunk = word.to_le_bytes();
}
out
}
pub type SceneSlot = Arc<Mutex<Option<GpuScene>>>;
/// A reference image (canvas or decal): a textured quad, depth-tested against the model.
@@ -1210,6 +1279,9 @@ struct BlockGpu {
faded: Option<ClassGpu>,
lines: Option<Batch>,
hidden: Option<Batch>,
/// The runs of each class (opaque, glass, faded) without the bodies out of the view, while
/// some are ([`Batches::cull`]); `None`: every body is drawn.
culled: Option<[Vec<std::ops::Range<u32>>; 3]>,
}
fn class_gpu(
@@ -1281,7 +1353,7 @@ impl BlockGpu {
};
let lines = keep(old.as_ref(), |b| &b.lines, |b| b.lines.as_ref()).or_else(|| fresh(&bytes.lines, "sc_block_lines"));
let hidden = keep(old.as_ref(), |b| &b.hidden, |b| b.hidden.as_ref()).or_else(|| fresh(&bytes.hidden, "sc_block_hidden"));
BlockGpu { bytes, vertices, opaque, glass, faded, lines, hidden }
BlockGpu { bytes, vertices, opaque, glass, faded, lines, hidden, culled: None }
}
fn class(&self, class: TriClass) -> Option<&ClassGpu> {
@@ -1298,6 +1370,7 @@ impl BlockGpu {
Some(ClassGpu::Runs(runs)) => {
let Some(vertices) = &self.vertices else { return };
pass.set_vertex_buffer(0, vertices.slice(..));
let runs = self.culled.as_ref().map_or(&runs[..], |c| &c[class as usize][..]);
for r in runs.iter() {
pass.draw(r.clone(), 0..1);
tally.triangles(r.len() as u32, 1);
@@ -1470,14 +1543,21 @@ impl PaletteGpu {
const CHUNK_SPLIT: u64 = 32;
const MAX_CHUNK_BODIES: usize = 128;
/// A block's see-through triangles as the global glass stream took them ([`Batches::glass_from`]).
struct GlassSource {
key: u64,
vertices: Arc<Vec<u8>>,
glass: Arc<Vec<std::ops::Range<u32>>>,
}
#[derive(Default)]
struct Batches {
/// What the last scene taken uploaded.
uploads: UploadStats,
/// The blocks' see-through triangles the global glass stream was assembled from (block key,
/// vertices and glass runs, by address), when it holds nothing else: an edit that changes
/// neither leaves the sorted stream as it is.
glass_from: Option<Vec<(u64, usize, usize)>>,
/// vertices and glass runs, held so their addresses cannot be reused), when it holds nothing
/// else: an edit that changes neither leaves the sorted stream as it is.
glass_from: Option<Vec<GlassSource>>,
/// The scene's palette texture and its group 0 bind (uniform + palette).
palette: Option<PaletteGpu>,
/// The scene blocks on the GPU, by key, and their drawing order.
@@ -1492,6 +1572,8 @@ struct Batches {
/// The glass triangles as built, and the view direction they were last sorted for.
glass_cpu: Vec<u8>,
glass_sorted_for: Option<[f32; 3]>,
/// The view the blocks' bodies were last culled for ([`Batches::cull`]).
blocks_culled_for: Option<[[f32; 4]; 4]>,
lines: Option<Batch>,
hidden: Option<Batch>,
overlays: Option<Batch>,
@@ -1594,16 +1676,23 @@ impl Batches {
// so cross-body blend order is kept; the blocks' glass triangles join it here. With
// OIT the blocks draw their own runs and nothing is sorted. A stream made of the same
// blocks' glass as before (a colour or opacity edit, a selection) is kept as sorted.
let from: Vec<(u64, usize, usize)> = if oit_available {
let from: Vec<GlassSource> = if oit_available {
Vec::new()
} else {
sc.blocks
.iter()
.filter(|b| !b.bytes.glass.is_empty())
.map(|b| (b.key, Arc::as_ptr(&b.bytes.vertices) as usize, Arc::as_ptr(&b.bytes.glass) as usize))
.map(|b| GlassSource { key: b.key, vertices: b.bytes.vertices.clone(), glass: b.bytes.glass.clone() })
.collect()
};
let same_glass = sc.glass.is_empty() && self.glass_from.as_ref() == Some(&from);
let same_glass = sc.glass.is_empty()
&& self.glass_from.as_ref().is_some_and(|was| {
was.len() == from.len()
&& was
.iter()
.zip(&from)
.all(|(a, b)| a.key == b.key && Arc::ptr_eq(&a.vertices, &b.vertices) && Arc::ptr_eq(&a.glass, &b.glass))
});
if !same_glass {
self.glass_cpu.clear();
self.glass_cpu.extend_from_slice(&sc.glass);
@@ -1618,6 +1707,7 @@ impl Batches {
self.glass_from = sc.glass.is_empty().then_some(from);
}
self.uploads.geometry += geometry;
self.blocks_culled_for = None;
self.key = Some(key);
}
}
@@ -1664,6 +1754,23 @@ impl Batches {
out
}
/// Leave the blocks' bodies that are out of the view `vp` out of their draws: each class's
/// runs without those bodies' vertices. Worked out again only when the view changes; a block
/// with every body in view draws its runs as they are.
fn cull_blocks(&mut self, vp: &[[f32; 4]; 4]) {
if self.blocks_culled_for.as_ref() == Some(vp) {
return;
}
self.blocks_culled_for = Some(*vp);
const ROWS: [[f32; 4]; 3] = [[1.0, 0.0, 0.0, 0.0], [0.0, 1.0, 0.0, 0.0], [0.0, 0.0, 1.0, 0.0]];
for block in self.blocks.values_mut() {
let hidden: Vec<std::ops::Range<u32>> =
block.bytes.bodies.iter().filter(|b| !placed_box_in_view(&ROWS, &b.bounds, vp)).map(|b| b.vertices.clone()).collect();
block.culled = (!hidden.is_empty())
.then(|| [TriClass::Opaque, TriClass::Glass, TriClass::Faded].map(|class| runs_without(block.bytes.runs(class), &hidden)));
}
}
/// Keep only the placements of the instanced meshes that may be seen in the view `vp` (see
/// [`placed_box_in_view`]): a part placed many times, most of it out of the view, draws
/// only what is in it. The placements are written again only when that set changes.
@@ -1671,6 +1778,7 @@ impl Batches {
/// [`LOD_PX`] where it is drawn ([`placement_scale`], [`level_for`]), in a view of `size`
/// pixels.
fn cull(&mut self, queue: &wgpu::Queue, vp: &[[f32; 4]; 4], size: [f32; 2]) {
self.cull_blocks(vp);
let f = |b: &[u8], i: usize| b.get(i..i + 4).and_then(|x| x.try_into().ok()).map_or(0.0, f32::from_le_bytes);
for d in &mut self.instanced {
let errors: Vec<f32> = d.levels.iter().map(|l| l.error).collect();
@@ -3902,6 +4010,48 @@ mod glass_tests {
assert!(buffer_chunk(TRI_SIZE, 1) >= TRI_SIZE * 6, "a tiny limit still holds one unit");
}
/// Bodies out of the view are cut out of a class's runs, wherever they fall in them: inside
/// a run, over its ends, across two runs, or not at all.
#[test]
fn runs_without_hidden_bodies() {
let r = |v: &[(u32, u32)]| v.iter().map(|&(a, b)| a..b).collect::<Vec<_>>();
let cut = |runs: &[(u32, u32)], hidden: &[(u32, u32)]| runs_without(&r(runs), &r(hidden));
assert_eq!(cut(&[(0, 30)], &[(9, 18)]), r(&[(0, 9), (18, 30)]));
assert_eq!(cut(&[(0, 9), (12, 30)], &[(6, 15)]), r(&[(0, 6), (15, 30)]));
assert_eq!(cut(&[(0, 9), (12, 30)], &[(0, 9), (27, 30)]), r(&[(12, 27)]));
assert_eq!(cut(&[(3, 6)], &[(0, 3), (6, 9)]), r(&[(3, 6)]));
assert_eq!(cut(&[(0, 30)], &[(0, 30)]), r(&[]));
assert_eq!(cut(&[(0, 12), (15, 24)], &[]), r(&[(0, 12), (15, 24)]));
}
/// A line is written as one record: its ends, its palette index and its width word, each
/// four little-endian bytes, as the shader reads them; a lifted line's word is
/// [`lifted_width`]'s, whether worked out per segment or once for many.
#[test]
fn lines_are_written_as_one_record() {
let (a, b, c): ([f32; 3], [f32; 3], _) = ([1.0, -2.0, 3.5], [4.0, 5.0, -6.25], PaletteIndex(7));
let mut expected = Vec::new();
for v in a.iter().chain(&b) {
expected.extend_from_slice(&v.to_le_bytes());
}
expected.extend_from_slice(&7u32.to_le_bytes());
expected.extend_from_slice(&1.5f32.to_le_bytes());
let mut sc = GpuScene::default();
sc.line(a, b, c, 1.5, false);
sc.hidden_line(a, b, c, 1.5);
assert_eq!(sc.lines, expected);
assert_eq!(sc.hidden, expected);
let normals = [[0.0, 0.0, 1.0], [1.0, 0.0, 0.0]];
let (mut per_segment, mut shared) = (GpuScene::default(), GpuScene::default());
per_segment.lifted_line(a, b, c, 2.0, normals);
shared.lifted_line_word(a, b, c, lifted_width(2.0, normals));
assert_eq!(per_segment.lines, shared.lines);
assert_eq!(per_segment.lines.get(28..32), Some(&lifted_width(2.0, normals).to_le_bytes()[..]));
let mut block = BlockBuilder::default();
block.line(a, b, c, 1.5);
assert_eq!(*block.freeze().lines, expected);
}
/// Without order-independent transparency, the sorted glass stream is kept while the blocks'
/// see-through triangles stay the same: a colour edit (a new palette) uploads palette rows
/// and no geometry; a class change rebuilds the stream.
+45 -1
View File
@@ -130,6 +130,9 @@ pub struct TreeState {
pub timeline_reveal: Option<u64>,
/// The Edit Canvas / Calibrate panel.
pub canvas_panel: Option<CanvasPanel>,
/// The bodies a row at the top of the history lights this frame (`history_rows`), for the
/// browser, drawn after it, to keep.
history_hover: Vec<String>,
/// A Section Analysis hidden with its eye in the Analysis folder: its plane (origin,
/// normal), so the eye can bring it back (#822).
pub hidden_section: Option<(solidcraft_engine::geom::Vec3, solidcraft_engine::geom::Vec3)>,
@@ -1218,12 +1221,53 @@ fn rows_scrolling(app: &mut SolveApp, ui: &mut egui::Ui, acts: &mut Actions, shr
app.tree.cache = Some(cache);
}
/// The Bodies folder and the Origin at the top of the vertical History (Preferences › Interface:
/// Show origin and bodies at the top, #6490): the browser's own rows, with their eyes, selection,
/// menus and fold state. The bodies are the active component's, in the placement being edited,
/// as that component's folder in the browser lists them. The rows stay out of the browser's row
/// order, automation list and reveal, which the browser keeps for its own rows.
pub(crate) fn history_rows(app: &mut SolveApp, ui: &mut egui::Ui) {
prepare_model(app);
let mut acts = Actions { lit: Lit::of(app), ..Default::default() };
let reveal = app.tree.reveal.take();
let order = app.tree.order_len;
let drawn = DRAWN.with(|d| d.borrow().len());
let component = app.session.active_component;
let path = (component != 0)
.then(|| app.session.active_occurrence_path.clone().unwrap_or_else(|| app.session.doc.first_path(component)))
.filter(|path| !path.is_empty());
// The Bodies folder as the browser draws it for that component or placement (`browser_cache`).
let mut cache = app.tree.cache.take().unwrap_or_default();
cache.prepare(app);
let bodies = cache.folder(app, component, path.as_deref(), "bodies");
app.tree.cache = Some(cache);
ui.scope(|ui| {
ui.spacing_mut().item_spacing.y = 0.0;
folder_rows_at(app, ui, component, path.as_deref(), "bodies", 0, &mut acts, &bodies);
origin_rows(app, ui, 0, &acts);
});
app.tree.reveal = reveal;
// The browser's own rows reuse these order slots (`TreeState::remember`).
app.tree.order_len = order;
DRAWN.with(|d| d.borrow_mut().truncate(drawn));
app.viewport.hover_bodies = acts.hover_bodies.clone();
app.tree.history_hover = acts.hover_bodies;
if let Some(d) = acts.drop {
apply_drop(app, d);
}
if let Some((at, target)) = acts.menu {
crate::context_menu::open_for(app, at, target);
}
}
fn finish_browser(app: &mut SolveApp, ui: &mut egui::Ui, mut acts: Actions) {
app.tree.order.truncate(app.tree.order_len);
if !acts.hovered {
acts.hover_bodies.clear();
}
app.viewport.hover_bodies = acts.hover_bodies;
// The history's rows, drawn before the browser, light bodies too (`history_rows`).
let history = std::mem::take(&mut app.tree.history_hover);
app.viewport.hover_bodies = if acts.hover_bodies.is_empty() { history } else { acts.hover_bodies };
if let Some(d) = acts.drop {
apply_drop(app, d);
}
+1 -1
View File
@@ -10,7 +10,7 @@ use egui_wgpu::wgpu;
pub use solidcraft_gpu::view::{
AccessMap, BlockBuilder, BlockBytes, BlockTris, DrawStats, DrawStatsHandle, Frame, GpuImage, GpuScene, GpuTarget, INSTANCE_SIZE, InstanceLook,
InstancedMesh, LINE_SIZE, LOD_PX, MAX_PALETTE, PBR_SIZE, Palette, PaletteIndex, PlannedBody, Renderer, SHARED_SIZE, SceneBlock, SceneSlot,
SharedTris, TRI_SIZE, TriClass, extend_runs, level_for, placement_scale, runs_within, supersampled_size,
SharedTris, TRI_SIZE, TriClass, extend_runs, level_for, lifted_width, placement_scale, runs_within, supersampled_size,
};
/// A frame of the viewport as an egui paint callback.
+4 -2
View File
@@ -326,7 +326,9 @@ mod tests {
let pending: BTreeMap<String, Messages> = serde_json::from_str(&std::fs::read_to_string(entry.path()).unwrap()).unwrap();
for (locale, messages) in pending {
for (id, text) in messages {
assert_eq!(Locale::for_language(&locale).text(&id), text, "{locale}: {id}");
// The catalogs hand right-to-left text out in display order (`crate::bidi`);
// pending files hold it in reading order, as the catalogs do (#7527).
assert_eq!(Locale::for_language(&locale).text(&id), crate::bidi::visual_template(&text), "{locale}: {id}");
}
}
}
@@ -436,7 +438,7 @@ mod tests {
/// are kept partial so a new label doesn't need more translations), so their gaps fall back
/// to English and are reported by `cargo xtask i18n` without failing the build, while
/// nothing in the complete catalogs may regress.
const COMPLETE: &[&str] = &["ar", "he", "ja", "zh"];
const COMPLETE: &[&str] = &["ar", "he", "ja", "vi", "zh"];
/// `text()` falls back to English silently, so a message a complete locale never received is
/// shown in English inside an otherwise translated UI, with nothing in the build to say so.
+3
View File
@@ -654,6 +654,9 @@ preference_settings! {
timeline_left_browser_collapsed: bool = false, PAGE_INTERFACE;
/// Whether the History at the left lists a sketch under the feature built from it.
history_sketches: HistorySketches = HistorySketches::default(), PAGE_INTERFACE;
/// The vertical history (left, or under the browser) starts with the active component's Bodies
/// folder and the Origin, as the browser shows them (#6490). The bottom strip is unchanged.
timeline_origin_bodies: bool = false, PAGE_INTERFACE;
/// Width of the browser sidebar, which
/// the user drags wider or narrower at its right edge.
browser_width: f32 = DEFAULT_BROWSER_WIDTH, PAGE_INTERFACE;
+9
View File
@@ -95,6 +95,15 @@ pub(super) fn interface(app: &mut SolveApp, ui: &mut egui::Ui, t: &Tokens) {
}
});
});
// Only the history on the left or under the browser has room for these rows.
ui_kit::row(ui, "", |ui| {
ui.add_enabled_ui(app.preferences.timeline_left, |ui| {
let check = ui_kit::checkbox(ui, app.tr("prefs.history_origin_bodies"), &mut app.preferences.timeline_origin_bodies)
.on_hover_text(app.tr("prefs.history_origin_bodies_note"))
.on_disabled_hover_text(app.tr("prefs.history_origin_bodies_note"));
crate::scenario::publish_target("prefs:history_origin_bodies", check.rect);
});
});
ui_kit::row(ui, app.tr("prefs.browser"), |ui| {
ui.add_enabled_ui(app.ui.layout == crate::shell::LayoutMode::Floating, |ui| {
let mut selected = usize::from(app.preferences.browser_float);
+44
View File
@@ -527,3 +527,47 @@ fn step_file_cost() {
}
println!("{line}");
}
/// Zoomed onto one part of an assembly, as when editing it: the GPU frame and the triangles
/// drawn, against the whole assembly in view. Bodies out of the view are left out of their
/// blocks' draws. Skipped without a GPU adapter. Run with `--ignored --nocapture`.
#[cfg(not(target_arch = "wasm32"))]
#[test]
#[ignore = "manual culling measurement"]
fn zoomed_view_frame_cost() {
const SIZE: u32 = 1024;
let Some(mut frames) = crate::gpu::OffscreenFrames::new(SIZE, false) else {
eprintln!("skipping: no GPU adapter");
return;
};
let mut h = assembly(300, 500);
h.app.viewport.gpu = frames.target();
h.app.fit_view();
let model: crate::gpu::SceneSlot = std::sync::Arc::new(std::sync::Mutex::new(Some(crate::viewport::build_scene(&h.app))));
let none: crate::gpu::SceneSlot = std::sync::Arc::new(std::sync::Mutex::new(None));
let still = |h: &Harness, frames: &mut crate::gpu::OffscreenFrames| {
frames.frame(view_frame(&h.app, SIZE, (7, &model), (0, &none)));
let mut t: Vec<f64> = (0..7).map(|_| frames.frame(view_frame(&h.app, SIZE, (7, &model), (0, &none)))).collect();
t.sort_by(f64::total_cmp);
(t[3], frames.stats().triangles)
};
let (whole, whole_tris) = still(&h, &mut frames);
let st = h.app.session.world_state();
let b = st.bodies.iter().find(|b| b.component == 0).unwrap().mesh().bounds();
drop(st);
h.app.cam.target = b.center();
h.app.cam.distance = b.diagonal() * 1.5;
let (zoomed, zoomed_tris) = still(&h, &mut frames);
// Orbiting: a new view every frame, so the bodies are culled again each time.
let mut t: Vec<f64> = (0..21)
.map(|i| {
h.app.cam.yaw += 0.01 * f64::from(i);
frames.frame(view_frame(&h.app, SIZE, (7, &model), (0, &none)))
})
.collect();
t.sort_by(f64::total_cmp);
let orbit = t[10];
println!(
"ZOOMED whole_ms={whole:.2} whole_triangles={whole_tris} zoomed_ms={zoomed:.2} zoomed_triangles={zoomed_tris} zoomed_orbit_ms={orbit:.2}"
);
}
+108 -1
View File
@@ -1007,6 +1007,17 @@ struct Rows<'a> {
fn vertical_features(app: &mut SolveApp, ui: &mut egui::Ui, names: bool) {
let t = Tokens::get();
// Show origin and bodies at the top (#6490): the browser's Bodies folder and Origin rows lead
// the history when it is wide enough for names. They are not places in the history: nothing
// drags or drops among them, and the features start below them.
let features_top = (names && app.preferences.timeline_origin_bodies).then(|| {
crate::browser::history_rows(app, ui);
ui.add_space(t.space_3);
t.secondary_hline(ui.painter(), ui.max_rect().x_range(), ui.cursor().top(), t.border);
ui.add_space(t.space_3);
ui.cursor().top()
});
let among_features = |y: f32| features_top.is_none_or(|top| y >= top);
ui.spacing_mut().item_spacing.y = if names { t.space_2 } else { t.space_4 };
let features: Vec<_> = app
.session
@@ -1142,6 +1153,7 @@ fn vertical_features(app: &mut SolveApp, ui: &mut egui::Ui, names: bool) {
}
// The marker dropped where it already is needs no bar: the marker shows the spot.
if let (Some(active), Some(pointer)) = (drag.as_ref(), pointer)
&& among_features(pointer.y)
&& let Some(slot) = nearest_vertical_slot(&slots, pointer.y)
&& !(matches!(active, Drag::Marker) && slot.position == marker.min(n))
{
@@ -1170,7 +1182,7 @@ fn vertical_features(app: &mut SolveApp, ui: &mut egui::Ui, names: bool) {
}
if ui.input(|input| input.pointer.any_released())
&& let Some(active) = drag.take()
&& let Some(pointer) = pointer
&& let Some(pointer) = pointer.filter(|pointer| among_features(pointer.y))
&& let Some(slot) = nearest_vertical_slot(&slots, pointer.y)
{
match active {
@@ -1887,6 +1899,101 @@ mod tests {
assert_eq!(harness.app.session.selection, vec![solidcraft_engine::Sel::Feature { id: base }]);
}
/// The handle `name` drawn inside the vertical history (the browser publishes the same names).
fn in_history(name: &str) -> Option<egui::Pos2> {
let card = crate::scenario::rect_of("history:vertical")?;
crate::scenario::handles(name).into_iter().filter(|(n, _)| n == name).map(|(_, at)| at).find(|at| card.contains(*at))
}
/// Show origin and bodies at the top (#6490): off by default; on, the vertical history starts
/// with the active component's Bodies folder and the Origin, the browser's rows with their
/// folds, eyes, selection and menus; they take no drops; too narrow for names, it leaves them out.
#[test]
fn origin_and_bodies_lead_the_vertical_history_when_asked() {
let mut h = Harness::new();
h.app.preferences.timeline_left = true;
h.app.preferences.timeline_left_width = 240.0;
h.app.run("solid.box", json!({"length": 10, "width": 10, "height": 10, "name": "First", "body_name": "Block"})).unwrap();
h.app
.run("solid.box", json!({"length": 10, "width": 10, "height": 10, "corner": [20, 0, 0], "operation": "new_body", "name": "Second", "body_name": "Other"}))
.unwrap();
h.frames(4);
assert!(in_history("row:Origin").is_none() && in_history("row:Bodies").is_none(), "off by default");
h.call("ui.prefs", json!({"timeline_origin_bodies": true}));
h.frames(3);
let bodies = in_history("row:Bodies").expect("the Bodies folder");
let origin = in_history("row:Origin").expect("the Origin");
let first = handle_at("timeline:First").unwrap();
assert!(bodies.y < origin.y && origin.y < first.y, "Bodies, Origin, then the features");
assert!(in_history("row:Block").is_some() && in_history("row:Other").is_some(), "the Bodies folder starts open, as in the browser");
assert!(in_history("row:XY").is_none(), "the Origin starts closed, as in the browser");
// The fold is the browser's: opening it here opens it there.
h.call("ui.click", json!({"x": origin.x, "y": origin.y}));
h.frames(2);
assert!(crate::browser::is_open(&h.app, "origin", false));
let xy = in_history("row:XY").expect("the origin planes");
let eye = in_history("eye:XY").unwrap();
h.call("ui.click", json!({"x": eye.x, "y": eye.y}));
h.frames(2);
assert!(h.app.session.visibility.hidden_origin.iter().any(|p| p == "XY"), "the plane's eye hides it");
let eye = in_history("eye:Block").unwrap();
h.call("ui.click", json!({"x": eye.x, "y": eye.y}));
h.frames(2);
assert!(h.app.session.visibility.hidden_bodies.iter().any(|b| b == "Block"), "the body's eye hides it");
h.call("ui.click", json!({"x": xy.x, "y": xy.y, "button": "right"}));
h.frames(2);
assert_eq!(h.app.menu.target, crate::context_menu::Target::OriginPlane { name: "XY".into() }, "the plane's own menu");
crate::context_menu::close(&mut h.app);
h.frames(2);
let other = in_history("row:Other").unwrap();
h.call("ui.click", json!({"x": other.x, "y": other.y}));
h.frames(2);
assert_eq!(h.app.session.selection, vec![solidcraft_engine::Sel::Body { name: "Other".into() }]);
// Neither the marker nor a feature drops among these rows.
let order = |h: &Harness| h.app.session.doc.features.iter().map(|f| f.name.clone()).collect::<Vec<_>>();
let before = order(&h);
h.step(&json!({"drag": [{"handle": "timeline:rollback"}, [origin.x, origin.y]]})).unwrap();
assert_eq!(h.app.session.doc.marker, None, "the marker stays at the end");
h.step(&json!({"drag": [{"handle": "timeline:Second"}, [bodies.x, bodies.y]]})).unwrap();
assert_eq!(order(&h), before, "the feature keeps its place");
let first = handle_at("timeline:First").unwrap();
h.step(&json!({"drag": [{"handle": "timeline:Second"}, [first.x, first.y - 8.0]]})).unwrap();
assert_eq!(order(&h), vec!["Second".to_string(), "First".to_string()], "the first feature's top is still a place");
// Under the browser too; not in the icons-only width, nor in the bottom strip.
h.app.preferences.timeline_under_browser = true;
h.frames(4);
assert!(in_history("row:Origin").is_some());
h.app.preferences.timeline_under_browser = false;
h.app.preferences.timeline_left_width = crate::ui_kit::history_width(false);
h.frames(4);
assert!(in_history("row:Origin").is_none(), "no room for the rows without names");
h.app.preferences.timeline_left = false;
h.frames(4);
assert_eq!(crate::scenario::handles("row:Origin").into_iter().filter(|(n, _)| n == "row:Origin").count(), 1, "only the browser's");
}
/// In a design with components, the Bodies folder is the active component's, in the
/// placement being edited, as the browser lists it under that placement.
#[test]
fn the_history_lists_the_active_components_bodies() {
let mut h = Harness::new();
h.app.preferences.timeline_left = true;
h.app.preferences.timeline_left_width = 240.0;
h.app.preferences.timeline_origin_bodies = true;
h.app.run("solid.box", json!({"length": 10, "width": 10, "height": 10, "body_name": "RootBlock"})).unwrap();
h.app.run("component.create", json!({"name": "Part"})).unwrap();
h.app.run("solid.box", json!({"length": 10, "width": 10, "height": 10, "corner": [20, 0, 0], "body_name": "Inside"})).unwrap();
h.frames(4);
assert!(in_history("row:Inside").is_some() && in_history("row:RootBlock").is_none(), "the active component's bodies");
assert!(in_history("row:Origin").is_some());
h.app.run("component.activate", json!({"component": "root"})).unwrap();
h.frames(4);
assert!(in_history("row:RootBlock").is_some() && in_history("row:Inside").is_none(), "the root's own bodies");
}
#[test]
fn rollback_bar_above_last_feature_can_be_dragged_to_the_end() {
let mut harness = Harness::new();
+69
View File
@@ -29,6 +29,36 @@ macro_rules! element {
// One entry per element/variant: names, rendering and geometry expectations stay together.
const ELEMENTS: &[Element] = &[
element!("sphere_thumbnail", |ui, _| {
ui.horizontal(|ui| {
for kind in [FinishKind::Matte, FinishKind::Satin, FinishKind::Gloss, FinishKind::Metal, FinishKind::Glass] {
sphere_thumbnail(
ui,
&format!("gallery:sphere:{kind:?}"),
Sphere { color: Tokens::get().render_accent, kind, opacity: None },
Tokens::get().space_24 * 2.0,
);
}
});
}),
element!("look_chain", "gallery:chain", Some("gallery:chain:body"), |ui, _| {
let t = Tokens::get();
look_chain(
ui,
"gallery:chain",
["Face", "Body", "Component", "Material"],
[
None,
Some(Sphere { color: t.render_accent, kind: FinishKind::Satin, opacity: None }),
None,
Some(Sphere { color: t.control_border, kind: FinishKind::Metal, opacity: None }),
],
LookStep::Body,
);
}),
element!("look_chain_empty", |ui, _| {
look_chain(ui, "gallery:empty-chain", ["Face", "Body", "Component", "Material"], [None; 4], LookStep::Face);
}),
element!("settings_form", |ui, state| {
settings_form(ui, Tokens::get().form_label_width, |ui| {
row(ui, "Sample", |ui| {
@@ -1400,10 +1430,12 @@ fn gallery_covers_every_inline_element() {
"disclosure.rs",
"fields.rs",
"layout.rs",
"look_chain.rs",
"menu.rs",
"metrics.rs",
"radial.rs",
"reports.rs",
"sphere.rs",
"status.rs",
"table.rs",
] {
@@ -1956,3 +1988,40 @@ fn file_view_application_evidence_in_both_themes_and_widths() {
std::fs::remove_file(path).unwrap();
std::fs::remove_dir(fixture_dir).unwrap();
}
/// CPU-rendered review board using the actual gallery widgets, without a window.
#[test]
fn material_widgets_board_in_both_themes() {
use super::checks::tests::{raster, textures_update};
use std::collections::HashMap;
let previous = crate::theme::is_dark();
for dark in [false, true] {
crate::theme::set_dark(dark);
let ctx = crate::fonts::test_context();
let bounds = egui::Rect::from_min_size(egui::Pos2::ZERO, vec2(360.0, 240.0));
let mut textures = HashMap::new();
let mut image = None;
for _ in 0..3 {
let mut output = ctx.run_ui(egui::RawInput { screen_rect: Some(bounds), ..Default::default() }, |ui| {
ui.set_width(320.0);
ui.painter().rect_filled(bounds, 0.0, Tokens::get().panel);
scope(ui, |ui| {
for name in ["sphere_thumbnail", "look_chain", "look_chain_empty"] {
example(ui, ELEMENTS.iter().find(|e| e.name == name).unwrap());
ui.add_space(Tokens::get().space_12);
}
});
});
textures_update(&mut textures, &output.textures_delta);
output.textures_delta.clear();
image = Some(raster(&ctx, output.shapes, output.pixels_per_point, bounds, &textures));
}
let image = image.unwrap();
assert!(image.pixels().any(|p| p.0[3] != 0));
if let Some(dir) = std::env::var_os("SOLIDCRAFT_MATERIAL_WIDGET_EVIDENCE") {
std::fs::create_dir_all(&dir).unwrap();
image.save(std::path::Path::new(&dir).join(if dark { "material-widgets-dark.png" } else { "material-widgets-light.png" })).unwrap();
}
}
crate::theme::set_dark(previous);
}
+89
View File
@@ -0,0 +1,89 @@
//! Appearance inheritance selector. The caller owns selection and commands.
use super::{Sphere, sphere::paint_sphere};
use crate::{scenario, theme::Tokens};
use egui::{Rect, Sense, Stroke, Ui, pos2, vec2};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum LookStep {
Face,
Body,
Component,
Material,
}
impl LookStep {
pub const ALL: [Self; 4] = [Self::Face, Self::Body, Self::Component, Self::Material];
fn name(self) -> &'static str {
match self {
Self::Face => "face",
Self::Body => "body",
Self::Component => "component",
Self::Material => "material",
}
}
}
/// Labels are caller supplied for translation. `None` is an empty inheritance slot.
/// An enum active value guarantees exactly one active step, including empty steps.
/// Publishes `{id}:{face|body|component|material}` hit targets and the `{id}` allocation.
pub fn look_chain(ui: &mut Ui, id: &str, labels: [&str; 4], looks: [Option<Sphere>; 4], active: LookStep) -> Option<LookStep> {
let t = Tokens::get();
let width = ui.available_width().clamp(1.0, t.form_card_width * 2.0);
let side = t.space_24.min(width / 4.0);
let ring_width = t.space_2.min(side / 8.0);
let height = side + t.space_8 + t.font_small + t.space_4;
let (rect, _) = ui.allocate_exact_size(vec2(width, height), Sense::hover());
let mut clicked = None;
for (index, ((step, label), look)) in LookStep::ALL.into_iter().zip(labels).zip(looks).enumerate() {
let cell_width = width / 4.0;
let cell = Rect::from_min_size(rect.min + vec2(index as f32 * cell_width, 0.0), vec2(cell_width, height));
let painter = ui.painter().with_clip_rect(ui.clip_rect().intersect(cell));
let center = pos2(cell.center().x, cell.top() + side / 2.0 + ring_width);
let radius = (side / 2.0 - ring_width * 2.0).max(0.0);
if index < 3 {
ui.painter()
.line_segment([center + vec2(side / 2.0, 0.0), center + vec2(cell_width - side / 2.0, 0.0)], Stroke::new(1.0, t.control_border));
}
let key = format!("{id}:{}", step.name());
let response = ui.interact(cell, ui.make_persistent_id(&key), Sense::click());
response.widget_info(|| egui::WidgetInfo::selected(egui::WidgetType::SelectableLabel, ui.is_enabled(), active == step, label));
if response.clicked() {
clicked = Some(step);
}
if let Some(look) = look {
paint_sphere(ui, Rect::from_center_size(center, vec2(radius * 2.0, radius * 2.0)), look);
} else {
// Evenly spaced analytic dashes, independent of any icon asset.
for dash in 0..12 {
let start = dash as f32 * std::f32::consts::TAU / 12.0;
let points = (0..=4)
.map(|n| {
let a = start + n as f32 * std::f32::consts::TAU / 96.0;
center + vec2(a.cos(), a.sin()) * radius
})
.collect();
painter.add(egui::Shape::line(points, Stroke::new(1.0, t.form_dim)));
}
let arm = radius * 0.4;
painter.line_segment([center - vec2(arm, 0.0), center + vec2(arm, 0.0)], Stroke::new(1.0, t.form_dim));
painter.line_segment([center - vec2(0.0, arm), center + vec2(0.0, arm)], Stroke::new(1.0, t.form_dim));
}
if step == active {
painter.circle_stroke(center, radius + ring_width, Stroke::new(ring_width, t.render_accent));
}
let font = egui::FontId::proportional(t.font_small);
let label_width = ui.painter().layout_no_wrap(label.to_owned(), font.clone(), t.form_ink).size().x;
let label_left = (cell.center().x - label_width / 2.0).max(cell.left());
let label_rect = Rect::from_min_max(pos2(label_left, cell.top() + side + t.space_4), cell.max);
super::text::paint_text(ui, label_rect, label, font, t.form_ink, false);
scenario::publish_rect(&key, cell);
scenario::publish_handle(&key, cell.center());
}
scenario::publish_rect(id, rect);
clicked
}
#[cfg(test)]
#[path = "look_chain_tests.rs"]
mod tests;
@@ -0,0 +1,65 @@
use super::*;
use crate::ui_kit::{
FinishKind,
test_support::{frame, pointer},
};
#[test]
fn chain_has_one_active_ring_and_empty_slots_and_returns_each_click() {
let ctx = crate::fonts::test_context();
let t = Tokens::get();
let sphere = Sphere { color: t.render_accent, kind: FinishKind::Satin, opacity: None };
for active in LookStep::ALL {
for filled in [false, true] {
let looks = if filled { [Some(sphere); 4] } else { [None; 4] };
let clicked = std::cell::Cell::new(None);
let mut draw = |ui: &mut Ui| {
clicked.set(look_chain(ui, "chain", ["Face", "Body", "Component", "Material"], looks, active));
};
frame(&ctx, vec![], &mut draw);
assert_eq!(clicked.get(), None);
for step in LookStep::ALL {
let key = format!("chain:{}", step.name());
let pos = scenario::handle_at(&key).unwrap();
assert!(scenario::rect_of("chain").unwrap().contains(pos));
frame(&ctx, pointer(pos, true), &mut draw);
frame(&ctx, pointer(pos, false), &mut draw);
assert_eq!(clicked.get(), Some(step));
}
let output = ctx.run_ui(Default::default(), |ui| {
look_chain(ui, "paint", ["Face", "Body", "Component", "Material"], looks, active);
});
let rings: Vec<_> = output
.shapes
.iter()
.filter_map(|s| match &s.shape {
egui::Shape::Circle(c) if c.stroke.color == t.render_accent => Some(c),
_ => None,
})
.collect();
assert_eq!(rings.len(), 1);
let active_rect = scenario::rect_of(&format!("paint:{}", active.name())).unwrap();
assert_eq!(rings[0].center.x, active_rect.center().x);
let meshes = output.shapes.iter().filter(|s| matches!(s.shape, egui::Shape::Mesh(_))).count();
let dashes = output.shapes.iter().filter(|s| matches!(s.shape, egui::Shape::Path(_))).count();
assert_eq!(meshes, if filled { 4 } else { 0 });
assert_eq!(dashes, if filled { 0 } else { 48 });
}
}
}
#[test]
fn disabled_chain_does_not_return_clicks() {
let ctx = crate::fonts::test_context();
let mut clicked = None;
let mut draw = |ui: &mut Ui| {
ui.add_enabled_ui(false, |ui| {
clicked = look_chain(ui, "disabled", ["Face", "Body", "Component", "Material"], [None; 4], LookStep::Body);
});
};
frame(&ctx, vec![], &mut draw);
let pos = scenario::handle_at("disabled:face").unwrap();
frame(&ctx, pointer(pos, true), &mut draw);
frame(&ctx, pointer(pos, false), &mut draw);
assert_eq!(clicked, None);
}
+4
View File
@@ -14,6 +14,7 @@ mod fields;
mod file_view;
mod history;
mod layout;
mod look_chain;
mod menu;
mod metrics;
pub(crate) mod panel;
@@ -26,6 +27,7 @@ mod radial;
mod reports;
mod search;
mod shortcuts;
mod sphere;
mod status;
mod table;
mod text;
@@ -46,6 +48,7 @@ pub(crate) use fields::*;
pub(crate) use file_view::{FileView, RecentRow, RecoveryMessage, SampleCard};
pub(crate) use history::*;
pub(crate) use layout::*;
pub use look_chain::{LookStep, look_chain};
pub(crate) use menu::{Menu, MenuItem, rename_menu, workspace_menu};
pub(crate) use metrics::{ContentWidth, header_height, row_height, table_row_height};
pub(crate) use panel::{ComputeStatus, FeatureOptions, Panel, Part, WindowPage, panel_header_hover};
@@ -60,6 +63,7 @@ pub(crate) use radial::{RADIAL_RADIUS, radial_menu};
pub(crate) use reports::*;
pub(crate) use search::{CommandSearch, SearchBadge, SearchFieldGeometry};
pub(crate) use shortcuts::*;
pub use sphere::{FinishKind, Sphere, sphere_thumbnail};
pub(crate) use status::*;
pub(crate) use table::*;
pub(crate) use value_focus::*;
+144
View File
@@ -0,0 +1,144 @@
//! Small deterministic material previews; no textures, jobs or graphics device.
use crate::{scenario, theme::Tokens};
use egui::{Color32, Mesh, Rect, Response, Sense, Ui, pos2, vec2};
use std::{collections::VecDeque, sync::Arc};
const CACHE_ENTRIES: usize = 64;
const MAX_SIDE: u16 = 96;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum FinishKind {
Matte,
Satin,
Gloss,
Metal,
Glass,
}
/// Colour is document data. All surrounding chrome and checker colours come from Tokens.
#[derive(Clone, Copy, Debug)]
pub struct Sphere {
pub color: Color32,
pub kind: FinishKind,
pub opacity: Option<f32>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
struct Key {
color: Color32,
kind: FinishKind,
opacity: u8,
side: u16,
}
impl Sphere {
fn key(self, side: f32) -> Key {
let opacity = self.opacity.filter(|v| v.is_finite()).unwrap_or(if self.kind == FinishKind::Glass { 0.35 } else { 1.0 });
Key {
color: self.color,
kind: self.kind,
opacity: (opacity.clamp(0.0, 1.0) * 255.0).round() as u8,
side: if side.is_finite() { side.round().clamp(1.0, f32::from(MAX_SIDE)) as u16 } else { 1 },
}
}
}
#[derive(Clone, Default)]
struct SphereCache {
entries: VecDeque<(Key, Arc<Mesh>)>,
palette: Option<(Color32, Color32)>,
}
impl SphereCache {
fn mesh(&mut self, key: Key, t: &Tokens) -> Arc<Mesh> {
let palette = (t.on_accent, t.tab_on_fill_dark);
if self.palette != Some(palette) {
self.entries.clear();
self.palette = Some(palette);
}
if let Some((_, mesh)) = self.entries.iter().find(|(stored, _)| *stored == key) {
return mesh.clone();
}
let mesh = Arc::new(shade(key, t));
if self.entries.len() >= CACHE_ENTRIES {
self.entries.pop_front();
}
self.entries.push_back((key, mesh.clone()));
mesh
}
}
fn shade(key: Key, t: &Tokens) -> Mesh {
// Fixed topology bounds both work and memory independently of input size.
const RINGS: u32 = 12;
const SECTORS: u32 = 48;
let mut mesh = Mesh::default();
let radius = f32::from(key.side) * 0.5;
for ring in 0..=RINGS {
let r = ring as f32 / RINGS as f32;
for sector in 0..=SECTORS {
let angle = std::f32::consts::TAU * sector as f32 / SECTORS as f32;
let (x, y) = (r * angle.cos(), r * angle.sin());
let z = (1.0 - r * r).max(0.0).sqrt();
let light = (-0.4 * x - 0.5 * y + 0.76 * z).max(0.0);
let specular = (-0.25 * x - 0.35 * y + 0.902 * z).max(0.0);
let (diffuse, shine) = match key.kind {
FinishKind::Matte => (0.3 + 0.7 * light, 0.04 * specular.powi(8)),
FinishKind::Satin => (0.25 + 0.75 * light, 0.25 * specular.powi(16)),
FinishKind::Gloss => (0.2 + 0.8 * light, 0.85 * specular.powi(64)),
FinishKind::Metal => (0.15 + 0.65 * light, 0.8 * specular.powi(24)),
FinishKind::Glass => (0.5 + 0.5 * light, (0.85 * specular.powi(64) + 0.4 * (1.0 - z).powi(3)).min(1.0)),
};
let shaded = key.color.lerp_to_gamma(t.tab_on_fill_dark, 1.0 - diffuse);
let highlight = if key.kind == FinishKind::Metal { key.color.lerp_to_gamma(t.on_accent, 0.65) } else { t.on_accent };
let color = shaded.lerp_to_gamma(highlight, shine).gamma_multiply(f32::from(key.opacity) / 255.0);
mesh.colored_vertex(pos2(radius + x * radius, radius + y * radius), color);
if ring < RINGS && sector < SECTORS {
let a = ring * (SECTORS + 1) + sector;
let b = a + SECTORS + 1;
mesh.add_triangle(a, b, b + 1);
mesh.add_triangle(a, b + 1, a + 1);
}
}
}
mesh
}
pub(super) fn paint_sphere(ui: &Ui, rect: Rect, sphere: Sphere) {
if !ui.is_rect_visible(rect) {
return;
}
let t = Tokens::get();
let key = sphere.key(rect.width());
let mesh = ui.ctx().data_mut(|data| data.get_temp_mut_or_default::<SphereCache>(egui::Id::new("ui_kit:spheres")).mesh(key, &t));
if sphere.kind == FinishKind::Glass {
// A bounded four-by-four checker remains visible through the sphere.
for y in 0..4 {
for x in 0..4 {
let cell = Rect::from_min_size(rect.min + vec2(x as f32, y as f32) * rect.size() / 4.0, rect.size() / 4.0);
ui.painter().rect_filled(cell, 0.0, if (x + y) % 2 == 0 { t.control_fill } else { t.control_border });
}
}
}
let mut mesh = (*mesh).clone();
let scale = rect.width() / f32::from(key.side);
for vertex in &mut mesh.vertices {
vertex.pos = rect.min + vertex.pos.to_vec2() * scale;
}
ui.painter().add(egui::Shape::mesh(mesh));
}
/// Clamps size to 1–96 logical points. Cache: 64 FIFO entries per egui context.
/// The caller owns commands and may attach interaction to the returned response.
pub fn sphere_thumbnail(ui: &mut Ui, id: &str, sphere: Sphere, size: f32) -> Response {
let side = f32::from(sphere.key(size).side);
let (rect, response) = ui.allocate_exact_size(vec2(side, side), Sense::hover());
paint_sphere(ui, rect, sphere);
scenario::publish_rect(id, rect);
scenario::publish_handle(id, rect.center());
response
}
#[cfg(test)]
#[path = "sphere_tests.rs"]
mod tests;
+46
View File
@@ -0,0 +1,46 @@
use super::*;
#[test]
fn cache_bounds_a_thousand_distinct_inputs_and_evicts_oldest() {
let t = Tokens::light();
let mut cache = SphereCache::default();
let first = Sphere { color: t.render_accent, kind: FinishKind::Satin, opacity: None }.key(32.0);
let initial = cache.mesh(first, &t);
assert!(Arc::ptr_eq(&initial, &cache.mesh(first, &t)), "hits reuse shading");
for index in 0..1000_u16 {
let key = Sphere { color: Color32::from_rgb(index as u8, (index / 256) as u8, 100), kind: FinishKind::Gloss, opacity: Some(0.8) }
.key(f32::from(index));
let mesh = cache.mesh(key, &t);
assert!(cache.entries.len() <= CACHE_ENTRIES);
assert!(key.side <= MAX_SIDE);
assert!(mesh.vertices.len() <= 13 * 49);
assert!(mesh.vertices.iter().all(|v| v.pos.is_finite() && v.pos.x <= f32::from(MAX_SIDE) && v.pos.y <= f32::from(MAX_SIDE)));
}
assert_eq!(cache.entries.len(), CACHE_ENTRIES);
assert!(!cache.entries.iter().any(|(key, _)| *key == first));
assert!(!Arc::ptr_eq(&initial, &cache.mesh(first, &t)));
let mut changed_palette = Tokens::dark();
changed_palette.on_accent = t.render_accent;
cache.mesh(first, &changed_palette);
assert_eq!(cache.entries.len(), 1, "theme changes discard shaded colours");
}
#[test]
fn sphere_inputs_are_finite_bounded_and_finishes_differ() {
let t = Tokens::light();
let sphere = Sphere { color: t.render_accent, kind: FinishKind::Glass, opacity: Some(f32::NAN) };
for size in [f32::NAN, f32::INFINITY, -2.0, 0.0, f32::MAX] {
let key = sphere.key(size);
assert!((1..=MAX_SIDE).contains(&key.side));
assert_eq!(key.opacity, 89);
}
let mut previous = None;
for kind in [FinishKind::Matte, FinishKind::Satin, FinishKind::Gloss, FinishKind::Metal, FinishKind::Glass] {
let mesh = shade(Sphere { kind, opacity: Some(1.0), ..sphere }.key(32.0), &t);
let colors: Vec<_> = mesh.vertices.iter().map(|v| v.color).collect();
if let Some(previous) = previous {
assert_ne!(colors, previous);
}
previous = Some(colors);
}
}
+34 -11
View File
@@ -1025,7 +1025,10 @@ fn build_block(draws: &[BodyDraw], shaded: bool, build: &impl Fn(&BodyDraw, &mut
let mut spans = Vec::with_capacity(draws.len());
for d in draws {
let v0 = builder.vertices_len();
// Its box goes with its triangles, so a view without it leaves them out.
builder.begin_body();
build(d, &mut builder);
builder.end_body();
spans.push(at(v0)..at(builder.vertices_len()));
}
let ranges = draws
@@ -1079,6 +1082,7 @@ fn reindex_block(block: CachedBlock, draws: &[BodyDraw]) -> CachedBlock {
let old = block.bytes;
let bytes = std::sync::Arc::new(crate::gpu::BlockBytes {
vertices: old.vertices.clone(),
bodies: old.bodies.clone(),
opaque: std::sync::Arc::new(opaque),
glass: std::sync::Arc::new(glass),
faded: std::sync::Arc::new(faded),
@@ -2813,17 +2817,34 @@ const SKETCH_HALO_WIDTH: f32 = 2.0;
/// A highlighted edge: a halo and a core, and where the model hides it, the core dimmed (so a
/// pick on the far side still shows).
fn edge_lines(sc: &mut GpuScene, e: &[Vec3], core: Color32, halo: Color32, w: f32, normals: &[[[f32; 3]; 2]]) {
for (col, width) in [(halo, w + 3.0), (core, w)] {
let col = sc.color(c4(col));
/// The palette entries of a highlighted edge: its halo, its core, and its dimmed part where the
/// model hides it. Interned once for every edge drawn in them.
#[derive(Clone, Copy)]
struct EdgeColours {
halo: crate::gpu::PaletteIndex,
core: crate::gpu::PaletteIndex,
hidden: crate::gpu::PaletteIndex,
}
impl EdgeColours {
fn new(sc: &mut GpuScene, core: Color32, halo: Color32) -> EdgeColours {
EdgeColours { halo: sc.color(c4(halo)), core: sc.color(c4(core)), hidden: sc.color(c4(core.gamma_multiply(0.45))) }
}
}
fn edge_lines(sc: &mut GpuScene, e: &[Vec3], cols: EdgeColours, w: f32, normals: &[[[f32; 3]; 2]]) {
let segments = e.len().saturating_sub(1);
sc.reserve_lines(2 * segments, segments);
for (col, width) in [(cols.halo, w + 3.0), (cols.core, w)] {
// Segments without their faces' normals share one width word.
let flat = crate::gpu::lifted_width(width, [[0.0; 3]; 2]);
for (i, p) in e.windows(2).enumerate() {
let n = normals.get(i).copied().unwrap_or_default();
sc.lifted_line(p[0].to_f32(), p[1].to_f32(), col, width, n);
let word = normals.get(i).map_or(flat, |n| crate::gpu::lifted_width(width, *n));
sc.lifted_line_word(p[0].to_f32(), p[1].to_f32(), col, word);
}
}
let hidden = sc.color(c4(core.gamma_multiply(0.45)));
for p in e.windows(2) {
sc.hidden_line(p[0].to_f32(), p[1].to_f32(), hidden, (w * 0.7).max(1.0));
sc.hidden_line(p[0].to_f32(), p[1].to_f32(), cols.hidden, (w * 0.7).max(1.0));
}
}
@@ -3303,6 +3324,8 @@ pub(crate) fn build_highlight(app: &SolveApp) -> GpuScene {
return sc;
}
let mut feedback = GpuScene { palette: std::sync::Arc::new((*sc.palette).clone()), ..Default::default() };
let (hover_edge, selected_edge) =
(EdgeColours::new(&mut feedback, t.hover_edge, t.hover_edge_halo), EdgeColours::new(&mut feedback, t.sel_edge, t.sel_edge_rim));
let mut hover_instances = HighlightInstances::default();
let mut selected_instances = HighlightInstances::default();
// Selected things.
@@ -3333,7 +3356,7 @@ pub(crate) fn build_highlight(app: &SolveApp) -> GpuScene {
{
let m = drawn_mesh(app, b);
if let Some(e) = edge_of(&m, *index, *point) {
edge_lines(&mut feedback, e, t.sel_edge, t.sel_edge_rim, 3.0, &edge_normals(&m, e));
edge_lines(&mut feedback, e, selected_edge, 3.0, &edge_normals(&m, e));
}
}
}
@@ -3436,7 +3459,7 @@ pub(crate) fn build_highlight(app: &SolveApp) -> GpuScene {
}
for (ei, e) in m.edges.iter().enumerate() {
if m.edge_pickable(ei) && (whole || m.edge_faces.get(ei).is_some_and(|fs| fs.iter().any(|f| faces.contains(&(*f as usize))))) {
edge_lines(&mut feedback, e, t.hover_edge, t.hover_edge_halo, 1.5, &[]);
edge_lines(&mut feedback, e, hover_edge, 1.5, &[]);
}
}
}
@@ -3473,7 +3496,7 @@ pub(crate) fn build_highlight(app: &SolveApp) -> GpuScene {
if let Some(p) = m.edges.get(e)
&& m.edge_pickable(e)
{
edge_lines(&mut feedback, p, t.hover_edge, t.hover_edge_halo, 1.5, &[]);
edge_lines(&mut feedback, p, hover_edge, 1.5, &[]);
}
}
}
@@ -3485,7 +3508,7 @@ pub(crate) fn build_highlight(app: &SolveApp) -> GpuScene {
for i in m.circular_rim(*index) {
let selected = sel.iter().any(|x| matches!(x, Sel::Edge { body: b, index: j, .. } if b == body && *j == i));
if !selected && let Some(e) = if i == *index { edge_of(&m, i, *mid) } else { m.edges.get(i) } {
edge_lines(&mut feedback, e, t.hover_edge, t.hover_edge_halo, 2.0, &edge_normals(&m, e));
edge_lines(&mut feedback, e, hover_edge, 2.0, &edge_normals(&m, e));
}
}
}
@@ -267,6 +267,41 @@ fn appearance_edits_upload_palette_rows_only() {
assert!(glass.palette > 0, "it writes the palette: {glass:?}");
}
/// A body out of the view is left out of its block's draws: two boxes far apart, drawn with
/// both in view and then with the camera on one of them, draw half the triangles. Skipped
/// without a GPU adapter.
#[cfg(not(target_arch = "wasm32"))]
#[test]
fn bodies_out_of_the_view_are_not_drawn() {
const SIZE: u32 = 64;
let Some(mut frames) = crate::gpu::OffscreenFrames::new(SIZE, false) else {
eprintln!("skipping: no GPU adapter");
return;
};
let mut app = SolveApp::new(Session::default(), Services::default());
app.ui.ground_shadow = false;
app.set_origin_shown(false);
app.viewport.gpu = frames.target();
app.run("solid.box", json!({"length": 10, "width": 10, "height": 10, "corner": [0, 0, 0], "operation": "new"})).unwrap();
app.run("solid.box", json!({"length": 10, "width": 10, "height": 10, "corner": [1000, 0, 0], "operation": "new"})).unwrap();
app.fit_view();
let scene = build_scene(&app);
let spans: usize = scene.blocks.iter().map(|b| b.bytes.bodies.len()).sum();
assert_eq!(spans, 2, "each body's box is kept with its block");
let slot: crate::gpu::SceneSlot = std::sync::Arc::new(std::sync::Mutex::new(Some(scene)));
let none: crate::gpu::SceneSlot = std::sync::Arc::new(std::sync::Mutex::new(None));
frames.frame(crate::scale_tests::view_frame(&app, SIZE, (1, &slot), (0, &none)));
let both = frames.stats().triangles;
assert_eq!(both, 24, "both boxes in view");
app.cam.target = solidcraft_engine::geom::Vec3::new(5.0, 5.0, 5.0);
app.cam.distance = 40.0;
frames.frame(crate::scale_tests::view_frame(&app, SIZE, (1, &slot), (0, &none)));
assert_eq!(frames.stats().triangles, 12, "the far box is not drawn");
app.fit_view();
frames.frame(crate::scale_tests::view_frame(&app, SIZE, (1, &slot), (0, &none)));
assert_eq!(frames.stats().triangles, both, "and drawn again once in view");
}
/// Exercise the live GPU scene slots: a fresh headless render always rebuilds and would hide
/// stale cached geometry. Theme changes must update bytes while retaining the model's mesh.
#[test]
@@ -0,0 +1,22 @@
[
{"note": "#6490: Show origin and bodies at the top. The History on the left starts with the Bodies folder and the Origin, the browser's own rows; their folds and eyes work there. The browser is hidden so every row handle below is the History's."},
{"call": "engine.execute", "params": {"command": "solid.box", "params": {"length": 10, "width": 10, "height": 10, "body_name": "Block"}}},
{"call": "ui.set", "params": {"showBrowser": false}},
{"call": "engine.execute", "params": {"command": "view.history_layout", "params": {"dock": "left"}}},
{"frames": 3},
{"note": "Off by default: the History lists only the features."},
{"expect": {"hidden_handles": ["row:Bodies", "row:Origin"]}},
{"call": "ui.prefs", "params": {"timeline_origin_bodies": true}},
{"frames": 3},
{"expect": {"shown_handles": ["row:Bodies", "row:Block", "row:Origin"], "hidden_handles": ["row:XY"]}},
{"click": {"handle": "row:Origin"}},
{"frames": 2},
{"expect": {"shown_handles": ["row:XY", "row:XZ", "row:YZ", "eye:XY"]}},
{"click": {"handle": "eye:Block"}},
{"frames": 2},
{"expect": {"hidden": ["Block"]}},
{"note": "The History at the bottom is unchanged."},
{"call": "engine.execute", "params": {"command": "view.history_layout", "params": {"dock": "bottom"}}},
{"frames": 3},
{"expect": {"hidden_handles": ["row:Bodies", "row:Origin"]}}
]
+6
View File
@@ -129,6 +129,12 @@ General › **Show text under ribbon icons** is a separate, saved option for eit
layout. Turn it off for shorter icon-only command buttons; tooltips, section names
and command menus remain available.
Interface › Layout › **Show origin and bodies at the top** (off by default) puts the active
component's Bodies folder and the Origin (planes, axes and point) at the top of the History when
it sits on the left or under the browser and is wide enough for names. They are the browser's own
rows: the same eyes, folds and right-click menus, so one list is enough for single-part work. The
History at the bottom is unchanged.
### Colour themes
Interface › **Theme** lists Dark, Light and System (follows the operating system), the built-in
File diff suppressed because one or more lines are too long
+755
View File
@@ -0,0 +1,755 @@
# Render workspace panels
Tracking issue: #6281. Part of [the Render workspace UX roadmap](render-workspace-ui.md).
The old "Render studio" panel (eleven pages, a setup header and a pinned footer) is removed by the end of the Render workspace transition. Every control moves to a panel attached to the thing it edits. A control has one home. A *shortcut* is a promotion of that control to a first-class item (a stage bar chip, a handle in the view, a menu entry); it reads and writes the setting its home owns through the same command, so the two cannot disagree.
Every one of the 546 labels the old panel used and every `render.*` and `appearance.*` command has a home below or is listed under Removed. The panel designs are on the SolidCraft UX Layouts board, artboards `Render_Q0_Plan` to `Render_Q13_Dialogs`.
## Rules
- **One home.** Each control is owned by one editor; anything elsewhere is a shortcut that names its home.
- **Attached to a thing.** A panel edits what is selected in the Browser or the view. With nothing selected the Scene overview shows. There is no panel switcher and no All page.
- **Never hide, fade with a reason.** Controls a method does not support (Raster, CPU ray tracing, GPU path tracing) stay visible, faded, with the reason in words. Opening a panel never changes the method.
- **Apply with consent.** Anything that replaces saved state opens one review dialog first. Everything else writes at once through its command, with undo.
## What each Browser row opens
| Row | Panel | Holds |
| --- | --- | --- |
| Scene (nothing selected) | Scene | Orientation and readiness |
| Environment | Environment | World, backdrop, stage, atmosphere |
| Lights ( + adds a light ) | Lighting | Source, shadows, budget |
| a light | Light | Sun, point or area |
| Shots ( + saves a shot ) | Shot | Camera, lens, exposure, frame |
| Objects › body, face or component | Appearance | Look, glass, coat, maps |
| Objects › Wear | Wear | Layered ageing and contamination |
| Output | Output | Renderer, image, destination, render |
| Output › Sampling | Sampling | Quality and limits |
| Output › Effects | Effects | Tone, looks, lens, finishing, regrade |
| Output › Check | Check | Valid? cost? diagnostics |
## Decisions
Approved by the owner. Each changes something the earlier design said, or a default.
- **D-A.** Sampling, Effects and Check are rows under Output, not sections. The approved rules put Effects and Check inside the Output editor. A long scroll hides expert controls from the person who just wants a size. As child rows the Output editor stays short and each child is one click or one chip away. It changes the approved Output design.
- **D-B.** Wear is its own row under Objects. Wear acts on the whole scene, not one body, and is GPU only. Keeping it out of Appearance stops a per-body editor from silently carrying scene state.
- **D-C.** Dust and the participating medium live in Environment › Atmosphere. They were on the Aging page but they are air, not wear. Both are GPU only and fade with the reason when another method is chosen.
- **D-D.** Every control writes the shot through its command, with undo. Save Setup, Reload Saved Settings and the draft confirmation go away. This changes a default: today edits stay in a draft until Save Setup. The camera keeps Update shot and Revert because it moves by navigation, not by a control.
- **D-E.** Unavailable controls stay visible, faded, with the reason. Raster, CPU ray tracing and GPU path tracing support different things. Hiding a control makes people ask where it went; a faded control with 'needs GPU path tracing' answers it, and opening a panel never changes the method.
- **D-F.** One review dialog for every apply that replaces saved state. Studio, rig, stage, physical plan, quality budget and map sets all show what is replaced and what is kept, and need a tick before they apply.
- **D-G.** Diagnostics become Check › Under the hood. Raw request JSON, job JSON and the capability catalogue stay available for experts and agents, but out of the everyday list.
## Editors (the Inspector, one per selection)
### Scene
- **Attached to:** The scene (nothing selected)
- **Purpose:** Orientation and readiness: where the scene stands in each stage, and whether it can render.
- **Reached by:** Press Esc or click empty canvas; the Scene row at the top of the tree.
- **When:** On arrival, after finishing an edit, and before rendering. Everyday, read-mostly.
**Stages**
- Look: bodies with a look, saved overrides, wear layers · jumps to Objects
- Light: environment name, lights n · jumps to Environment / Lights
- Frame: current shot, lens, aspect · jumps to the shot
- Output: size, format, renderer, quality · jumps to Output
**Ready to render**
- Check result line with the count of notes · opens Check
- Renderer state: preparing, failed, unavailable, offline (explains why a stage control is faded)
- Check now · shortcut of Check
- Render shot · All shots (n) · shortcuts of the stage bar Render
**Reminders**
- Scene preserves the design: geometry, engineering properties and CAD are never changed here
### Appearance
- **Attached to:** A picked body, face or component
- **Purpose:** How one thing looks: the look chain, its values, and the optical overrides that belong to that body.
- **Reached by:** Click a body, face or component in the view or the tree; the quick bar's Edit; right-click Edit look.
- **When:** After choosing a look, when it needs tuning, and for glass, coat or maps. Everyday for the top; expert for the sections below it.
**Look**
- Scope chips: Face · Body · Component · Material (where the look comes from)
- Look card: sphere, name, source, Used by n · Replace everywhere
- Applies to: This face | All n users
**Edit**
- Colour (sRGB) · Finish: Matte, Satin, Gloss, Metal · Metallic · Roughness · Opacity
- Refractive index (IOR) for non-metals
- Opaque only: glass is a separate section, with a pointer when opacity is below 1
**Glass and absorption (GPU)**
- Use physical glass · recipe · closed solid, thin sheet or axis-aligned box
- IOR · thin-sheet thickness · absorption R G B per mm · transmit straight shadow rays
- Interior scattering: collision rate, phase mean cosine (closed dielectric only)
- Cannot share a body with clear coat or maps
**Clear coat (GPU)**
- Enable clear coat · coat weight 0–1 · coat roughness 0.045–1
**Maps (GPU)**
- Enable maps · base colour image · normal image · metallic map · roughness map
- Mapping origin X Y Z · one U repeat · one V repeat · repeat multiplier
- Normal strength · texture V points upward · base map replaces body colour
**Saved with this shot**
- State line: temporary on this tab, or saved · Save these optics to the shot · Replace saved · Remove saved optics
- Saved surface binding (authored map): replace · remove · conflict with temporary overrides
- Revalidate this body when the model changed
**Reflections (CPU)**
- This body: Off · Mirror · Rough (rough uses its own assigned look)
- Confirm this body after the scene changed
**Footer**
- Save to library… · Reset
### Wear
- **Attached to:** The scene's wear stack (the Wear row in the tree)
- **Purpose:** Ageing and contamination layers, in order, each aimed at all eligible bodies or one body.
- **Reached by:** The Wear row in the tree; the Wear chip in the Appearance editor of an affected body.
- **When:** Late in the Look stage, to take the CAD cleanliness off. Occasional; GPU only.
**Layers**
- Add layer menu: deposited dust, grime and staining, oxidised iron, copper-green patina, chipped paint, fine scratches, faded coating
- Ordered list (up to 8): eye, kind, target · move up · move down · remove
**Selected layer**
- Enabled · strength · coverage · pattern scale mm · seed
- Layer colour (sRGB) · layer roughness · layer metallic fraction · upward-facing deposition
- Surface class: all opaque, metallic only, nonmetallic only
**Target**
- All eligible opaque bodies, or one named body (filter, refresh from current bodies)
- Field origin in world mm
### Environment
- **Attached to:** The Environment row (the world around the product)
- **Purpose:** What lights and surrounds the product: the source, its orientation, what the camera sees behind it, the ground, and the air.
- **Reached by:** The Environment row; the ring in the view; Change in the Studios and environments panel.
- **When:** Light stage, right after choosing a studio. Everyday for source, rotation and intensity; occasional for the rest.
**Source**
- Source: None, Constant colour, Artistic sky, HDR file, Built-in HDR · Change opens Studios and environments
- Constant: colour (linear RGB) · Sky: zenith, horizon, ground (linear RGB)
- HDR file: choose, check file, details (size, primaries, digest) · saved inside the design vs file on disk · treat unspecified primaries as linear sRGB
- HDR lighting samples (CPU)
**Orientation and strength**
- Rotate (degrees) · Intensity · the ring in the view is the shortcut
**Backdrop**
- What the camera sees: Environment · Colour · Gradient · Clear
- Colour (sRGB) · gradient mode: vertical, horizontal, radial · first and second colour · centre X Y · radius
- It never lights, shadows or reflects; refused with transparent output
**Ground and stage**
- Fit ground: extent margin · clearance gap mm · diffuse reflectance (linear RGB) · replace existing stage (consent)
- Physical floor and wall · Polygonal cyclorama (both fit to the product)
- Saved panels n · Remove stage
**Atmosphere (GPU)**
- Participating medium: enable · bounds min and max (world mm)
- Extinction · scattering albedo (linear RGB) · phase mean cosine
### Lighting
- **Attached to:** The Lights group row
- **Purpose:** The lighting as a whole: which source of lights, shadows, and how many of each kind remain.
- **Reached by:** The Lights row; the + on it adds a light.
- **When:** When starting a lighting set up or changing method. Occasional.
**Light source**
- Studio preset: Neutral · Key and rim · Warm and cool, or Custom lights (turns a preset into editable lights)
- Rig applied: name · replace (consent)
**Shadows and sampling**
- Cast shadows (CPU) · area-light samples (CPU)
**Budget**
- Used of allowed: GPU 1 sun and 4 area; CPU 8 directional, 8 point, 4 area · Add light menu
### Light
- **Attached to:** One light (Sun or directional, point, area)
- **Purpose:** One light: what it is, how bright and what colour, where it is and where it points.
- **Reached by:** Its row in the tree; its handle in the view; Light tools in the stage bar.
- **When:** Light stage, constantly. Everyday.
**Light**
- Type: Area · Point · Sun (Spot appears with the unified light model)
- Intensity: EV or relative 0–64 · Colour: temperature or linear RGB · Softness / angular radius
**Position**
- Azimuth · Elevation · Distance (× subject size) · Aim at: Product centre, a body, a point
- Or world X Y Z · Direction toward light (X Y Z)
- Place by highlight
**Shape (area)**
- Full side U and V in mm · radiance (linear RGB)
**Falloff (point)**
- Reference distance for intensity · minimum distance for attenuation
**Behaviour**
- Solo · Mute · Duplicate · Delete (solo and mute also live on the tree row)
### Shot
- **Attached to:** One saved shot (a camera with its settings)
- **Purpose:** One camera: where it is, what it sees, its lens and exposure, and the shot's own housekeeping.
- **Reached by:** A shot in the tree or the Shots strip; the Frame tools; Save shot creates one.
- **When:** Frame stage. Everyday for projection, FOV and Update; occasional for the rest.
**Shot**
- Name · rename · duplicate · remove
- Camera changed since this shot was saved: Update shot · Revert
**Camera**
- Projection: Perspective | Orthographic · Field of view · Distance (× subject size or mm)
- Target X Y Z · Yaw · Pitch · Roll · Frame subject · Look at selection
**Lens (GPU)**
- Depth of field: enable · aperture radius · focus distance (needs perspective)
**Exposure**
- Exposure EV (not for raster)
**Frame**
- Aspect: 16:9, 3:2, 1:1, 4:5, 9:16, Custom · Rule of thirds · Safe area · Centre mark
- Output size: read-only, with a link to Output (size lives there)
### Output
- **Attached to:** The Output row (the image to be made)
- **Purpose:** What image comes out: the renderer, its size and format, transparency, and where it goes.
- **Reached by:** The Output row; the size and format chips in the stage bar.
- **When:** Output stage, before rendering. Everyday.
**Renderer**
- Method: CPU raster | CPU ray tracing | GPU path tracing · what each supports
- State: GPU compiling shaders with elapsed time, failed with reason, unavailable, offline setup
**Image**
- Width × Height · lock aspect ratio · swap · use the largest size at this aspect
- Over the limit: which limit, and the way out
- Format: PNG display image · EXR with the raw linear master retained
- Transparent background (conflicts: backdrop, bloom, CPU ray tracing)
**Destination**
- Replace existing file · Export PNG… / EXR… · View image · Copy
**Render**
- Render shot · All shots (n) · Cancel · progress and phase · last result summary
### Sampling
- **Attached to:** Output › Sampling
- **Purpose:** How hard the renderer works: noise, depth, and the limits it admits.
- **Reached by:** Output › Sampling in the tree; the quality chips in the stage bar.
- **When:** When a render is too noisy, too slow or refused for size. Occasional; expert beyond the quality preset.
**Quality**
- Draft · Balanced · High · Custom · Review a quality specification…
**Path tracing (GPU)**
- Samples per pixel (1–4096, powers of two) · maximum path depth · random seed
- Reduce noise in the finished image (denoise)
**Adaptive (GPU)**
- Allocate samples with independent pilots · pilot samples · minimum production samples
- Relative RGB error · absolute floor · coverage error
**Glass transport (GPU)**
- Conditional Fresnel strata 1, 2, 4 · certified nested dielectrics
**CPU ray tracing**
- Resource profile: Compact, Studio, Workstation · limits: memory, triangles, bodies, samples
- Sampling mode: single image | progressive · edge samples per pixel 1, 4, 16 · progressive samples and seed
- Reflection depth 1–8 · reflection samples 1–64
### Effects
- **Attached to:** Output › Effects
- **Purpose:** Everything that happens to the picture after it is traced: tone, looks, lens effects, finishing, regrade.
- **Reached by:** Output › Effects in the tree.
- **When:** After a first render looks right but flat. Occasional; GPU only.
**Tone**
- Tone mapping: Reinhard | ACES fitted
**Creative looks**
- Ordered stack: warm, cool, sepia, monochrome, teal and orange, cyanotype-inspired, faded print, bleach-bypass-inspired
- Enabled · look strength · move up and down · remove
**Lens effects**
- Highlight flare strength, threshold, halo, streak, ghost rings · chromatic edge offset · radial distortion
- Soft focus · detail sharpening · photographic grain · pattern seed · reset lens effects
**Finishing**
- RGB gain · contrast · saturation · vignette · bloom: strength, threshold, radius · reset
**Regrade**
- Regrade the raw master (no denoising, separate PNG) · needs a retained raw master
### Check
- **Attached to:** Output › Check
- **Purpose:** Is this request valid, what will it cost, and what is the machine doing. Also the escape hatch for experts.
- **Reached by:** The Check chip in the stage bar; Output › Check in the tree; a note count on the Output row.
- **When:** Before Render, and whenever something is refused. Everyday for the list; rare for details.
**Check**
- Check now · status line · stale marker when the request changed after the check
- Notes and warnings, each with a jump to the control that owns it
**This request**
- Method · size · samples · logical admission estimate (memory) · HDR digest captured
- Captured scene: bodies, triangles · live-preview payload bytes of the limit
**Renderers**
- Available backends and why one is unavailable · GPU compile progress · scene error
**Under the hood (collapsed)**
- Exact current request · last preflight details · job details and errors · Copy JSON
- Explicit capability boundaries · browse the capability catalogue
## Stage panels (under the Browser)
### Appearances
- **Attached to:** The Look stage · applies to the selection
- **Purpose:** Browse and apply opaque looks.
- **Reached by:** Under the tree in the Look stage.
- **When:** Look stage. Everyday.
**Browse**
- Search · scopes: In Design · Library · Recent · Favourites · categories
- Applicable first · availability badge (native, projection, reference only)
- Click applies to the selection · drag onto the model · click with nothing selected arms Paint
- SELENE opaque appearances and material concepts live in the Library scope
### Surfaces and references
- **Attached to:** The Look stage · applies to the picked body
- **Purpose:** Authored PBR surfaces with maps, and physical reference data.
- **Reached by:** Under the tree in the Look stage: the Surfaces and References scopes of the library switch; Surface in the Appearance editor opens it.
- **When:** When a look needs real textures or a physical reference. Occasional.
**Surfaces**
- Authored PBR surfaces · source map preview · use as uniform appearance · use authored GPU maps
- Target body · plane · physical repeat scale · save maps to this shot
**Physical references**
- Physical profiles and candidates · prepare creation plan (read-only) · copy plan with sources
### Studios and environments
- **Attached to:** The Light stage · applies to the scene
- **Purpose:** Browse and apply complete lighting set-ups and environments.
- **Reached by:** Under the tree in the Light stage.
- **When:** Light stage, first. Everyday.
**Browse**
- Scopes: Studios · Environments · Rigs · Mine · Specifications
- Native studios (review, then apply) · authored light rigs · built-in HDR environments · concepts and scene specifications (read only)
- Apply opens Review and apply; nothing is replaced silently
### Frame presets
- **Attached to:** The Frame stage · applies to the current shot
- **Purpose:** Aspect and composition aids.
- **Reached by:** Under the tree in the Frame stage.
- **When:** Frame stage. Everyday.
**Aspect**
- 16:9 · 3:2 · 1:1 · 4:5 · 9:16 · Custom (choosing one resizes the output, as one undo step)
**Guides**
- Thirds · Safe area · Centre mark (view only, never saved)
### Size and quality
- **Attached to:** The Output stage · applies to the Output
- **Purpose:** Common sizes and fixed sampling budgets.
- **Reached by:** Under the tree in the Output stage.
- **When:** Output stage. Everyday for size.
**Size**
- Preview 1280 × 720 · Full HD · 4K (with the limit shown) · Square · Custom
**Quality specifications**
- Fixed sampling budgets to review and apply (opens Review and apply)
## Stage chrome
### Stage tools
- **Attached to:** The stage bar, row 2 (current stage)
- **Purpose:** The tools and the handful of promoted settings of the current stage.
- **Reached by:** Always visible under the stage chips.
- **When:** Constantly. Everyday.
**Look**
- Pick: Body | Face | Component · Paint · Pick Up · Preview chip (opens the Preview menu)
**Light**
- + Add light · Lights fixed to Camera | World · Place by highlight · Handles on|off
**Frame**
- Perspective | Orthographic · Lens (FOV) · Look at selection · Save shot
**Output**
- Size menu · Format PNG | EXR · Quality Draft | Balanced | High | Custom · Check with note count
### Preview menu
- **Attached to:** The Preview chip in the Look tools
- **Purpose:** What the live preview is, and how to get it back when it fails.
- **Reached by:** The Preview chip.
- **When:** When the preview looks wrong or is unavailable. Rare.
**Preview**
- Integrated preview: raster shading, fixed studio lighting · exposure and image lighting apply to the image only
- Retry live preview · Use integrated preview · the setup asks for an unavailable preview profile
### Quick bar
- **Attached to:** The current selection in the view
- **Purpose:** One-click actions on what is selected.
- **Reached by:** Appears over a selected body, face or component.
- **When:** Look stage. Everyday.
**Actions**
- Name chip · Assign look · Pick up · Copy and paste look · Select same look · Remove look · Edit look
### Result viewer
- **Attached to:** A completed render (or two, to compare)
- **Purpose:** Look at, compare and take away results.
- **Reached by:** Opens when a render finishes; View image; the Live | Render segmented control.
- **When:** After every render. Everyday.
**Toolbar**
- Live | Render 1 | Render 2 · swap · result picker · discard · Fit · 100% · Copy · Export · Retain result · Close
**Status**
- Matches current design | stale · captured revision · job · executor · checkpoint and provisional state
- Progress pill while rendering · display reduced to the device limit
### Shots strip
- **Attached to:** The scene's shots and results
- **Purpose:** Switch shots, see their results, start renders.
- **Reached by:** The bottom strip; its chevron hides it.
- **When:** Constantly. Everyday.
**Strip**
- Shot thumbnails with modified dot · + Save shot · results with state · n running, m waiting
- Size chip · Render Image · All shots (n) · Update shot (shortcut of the Shot editor)
### Workspace menu
- **Attached to:** The workspace card
- **Purpose:** Leave, and the few preferences about the workspace itself.
- **Reached by:** The Render card top left.
- **When:** Rare.
**Menu**
- Return to Design · Focus view (Tab) · Dock side · Show Shots strip
## Dialogs
### Review and apply
- **Attached to:** A resource about to replace something
- **Purpose:** One dialog for every apply that replaces or adds saved state, so nothing is replaced silently.
- **Reached by:** Apply in a stage panel; Replace in an editor.
- **When:** When applying a studio, rig, stage, physical plan, quality budget or map set. Occasional.
**Variants**
- Native studio: replaces lighting, environment, exposure and stage; kept fields; consent
- Rig: centre, extent, use captured scene bounds, include passive cards or emitters only
- Ground and stage: replace existing stage
- Physical plan: read-only, no material created, acknowledge partial data, copy plan
- Quality budget: acknowledge omissions, projected size, samples, depth
- Maps: spatial maps vs uniform approximation, saved vs temporary conflict
### Confirmations
- **Attached to:** A destructive action
- **Purpose:** Confirm removal.
- **Reached by:** Remove shot, remove stage, remove saved optics.
- **When:** Rare.
**Confirm**
- Remove shot (the shot only; geometry and completed images stay) · Remove stage · Remove saved optics
## Shortcuts
| Setting | Home | Shortcuts |
| --- | --- | --- |
| Render, Cancel | Stage bar (R, Esc) | Shots strip, Scene overview, Output › Render |
| Check | Output › Check | Stage bar Check chip, Scene overview, Output |
| Save shot, Update shot | Shots strip +, Shot editor | Stage bar Save shot, tree + |
| Output size | Output › Image | Output size chip in the stage bar, Size presets, strip size chip, aspect presets (ratio only) |
| Format | Output › Image | Stage bar format chips |
| Quality preset | Output › Sampling | Stage bar quality chips, quality specifications panel |
| Projection | Shot › Camera | Stage bar Perspective \| Orthographic |
| Field of view | Shot › Camera | Stage bar lens chip |
| Aspect and guides | Shot › Frame | Frame presets panel, overlay chips in the view |
| Environment rotation | Environment › Orientation | Ring in the view |
| Light position and brightness | Light | Handle and dial in the view |
| Solo and mute | Tree row | Light › Behaviour |
| Add light | Lights group + in the tree | Stage bar + Add light, Lighting › Budget |
| Pick: Body, Face, Component | Look tools | Keys |
| Apply a look | Appearances panel (click, drag) | Quick bar, Appearance › Look |
| Fit | Navigation bar | Shot › Camera Frame subject |
| Change environment source | Studios and environments | Environment › Source Change |
| Export | Result viewer | Output › Destination |
## Removed
- **Page switcher (11 icon pages) and the All controls page.** Replaced by one editor per selection. The dense page has no home: nothing in it is a control that is not elsewhere.
- **Render studio header: setup picker, setup name, Save Setup, Reload Saved Settings, Unsaved preview changes.** A shot is chosen in the tree or the strip and renamed in the Shot editor. Controls write the shot through commands, with undo; Update shot and Revert remain for the camera.
- **Draft confirmation: Preserve edited render settings, Save and continue, Discard.** Gone with the draft.
- **Live Preview | Render Result segmented control.** Becomes the Live | Render control of the result viewer.
- **Model | Library toggle in the Render Browser.** The library is in the stage panels; the tree stays the tree.
- **Pinned footer: Check Render Request, Start Render, Cancel, Export, Replace existing file, Return to Design.** Render and Cancel are the stage bar; Check is the Check chip; Export is the viewer; Replace existing file is in Output; Return to Design is the workspace menu.
## Where every old control went
Role is Home (the one place it lives), Shortcut (also promoted elsewhere) or Removed.
### Header and footer
| Old control | Now in | Section | Role |
| --- | --- | --- | --- |
| Setup picker and setup name | Shot | Shot › Name; switch in the tree or the strip | Home |
| Save Setup, Reload Saved Settings, Unsaved preview changes | — | Every control writes the shot through its command, with undo. Update shot and Revert remain for the camera | Removed |
| Preserve edited render settings (Save, Discard, Keep editing) | — | Gone with the draft | Removed |
| Live Preview \| Render Result | Result viewer | Live \| Render n | Home |
| Check Render Request | Check | Check › Check now | Home |
| Start Render | Stage bar | Render (R) | Home |
| Cancel | Stage bar | Cancel (Esc) | Home |
| Export completed image | Result viewer | Export | Home |
| Replace existing file | Output | Destination | Home |
| Job status, progress bar, captured HDR and error text | Shots strip, Output | Progress pill and Output › Render | Home |
| Return to Design | Workspace menu | Return to Design | Home |
### Page strip
| Old control | Now in | Section | Role |
| --- | --- | --- | --- |
| Eleven page icons and the All controls page | — | One editor per selection. Nothing on All has no other home | Removed |
### Scene
| Old control | Now in | Section | Role |
| --- | --- | --- | --- |
| Scene preserves the design note | Scene | Reminders | Home |
| Image method | Output | Renderer | Home |
| GPU path tracer state (compiling, failed, unavailable) | Output | Renderer (and the Scene overview when it blocks) | Home |
| Browse native studios, resolve and review, apply with consent | Studios and environments, Review and apply | Studios scope; Native studio variant | Home |
| Browse scene specifications, SELENE shot specifications | Studios and environments | Specifications scope (read only) | Home |
| Ground and passive stage: margin, gap, reflectance, replace, fit and save, floor and wall, cyclorama, saved panels, remove | Environment | Ground and stage | Home |
| Saved shots: new name, duplicate, rename, remove | Shot | Shot › Name, duplicate, remove; Confirmations | Home |
### Appearance
| Old control | Now in | Section | Role |
| --- | --- | --- | --- |
| Editable body appearance: filter, refresh, choose body, colour, metallic, roughness, IOR, apply | Appearance | Edit (the selection replaces the body picker) | Home |
| Browse authored surfaces, inspect, source map preview, use uniform, use GPU maps, save maps | Surfaces and references | Surfaces | Home |
| Browse physical references, prepare creation plan, copy plan | Surfaces and references, Review and apply | References; Physical plan variant | Home |
| Browse materials, SELENE appearances | Appearances | Library scope | Home |
| Saved surface bindings: replace, remove, conflict with temporary overrides | Appearance | Saved with this shot | Home |
| GPU image materials: enable, filter, add body, per-body include, revalidate | Appearance | The picked body replaces filter and add; Revalidate stays | Home |
| Glass and optical absorption (recipe, IOR, thickness, absorption, shadows, interior scattering) | Appearance | Glass and absorption | Home |
| Clear coat over the base material | Appearance | Clear coat | Home |
| Planar colour and normal maps, metallic and roughness maps, origin, repeats, strength | Appearance | Maps | Home |
| Save optics to the setup, replace saved, saved optical targets, remove | Appearance | Saved with this shot | Home |
| CPU reflections: filter, choose body, Off, Mirror, Rough, add, depth, samples | Appearance, Sampling | Reflections (this body); depth and samples in Sampling | Home |
### Aging and contamination
| Old control | Now in | Section | Role |
| --- | --- | --- | --- |
| Add layer, layer list, move, remove, enabled | Wear | Layers | Home |
| Strength, coverage, scale, seed, colour, roughness, metallic, upward bias, surface class | Wear | Selected layer | Home |
| Target and coordinates: all bodies or one body, refresh, origin | Wear | Target | Home |
| Airborne dust and participating medium | Environment | Atmosphere | Home |
### Lighting
| Old control | Now in | Section | Role |
| --- | --- | --- | --- |
| Browse authored light rigs, inspect, fit centre and extent, include cards | Studios and environments, Review and apply | Rigs scope; Rig variant | Home |
| GPU: use custom studio lighting, constant environment radiance | Lighting, Environment | Light source; Source › Constant colour | Home |
| GPU: Sun (direction, radiance, angular radius) | Light | Type Sun | Home |
| GPU: area lights up to 4 (centre, half-edges U and V, radiance), add, remove | Light, Lighting | Type Area; Budget | Home |
| CPU: lighting preset (Neutral, Warm and cool, Key and rim), cast shadows | Lighting | Light source; Shadows and sampling | Home |
| CPU: custom directional lights up to 8 | Light, Lighting | Type Sun (directional); Budget | Home |
| CPU: point lights up to 8 (position, colour, intensity, reference and minimum distance) | Light | Type Point; Falloff | Home |
| CPU: rectangular lights up to 4, area samples | Light, Lighting | Type Area; Shadows and sampling | Home |
| Raster: not supported note | Lighting, Light | Faded with the reason | Home |
### Environment
| Old control | Now in | Section | Role |
| --- | --- | --- | --- |
| Scene background colour | Environment | Backdrop › Colour | Home |
| Camera-only gradient backdrop (mode, colours, centre, radius) | Environment | Backdrop › Gradient | Home |
| Browse original HDR environments, embed | Studios and environments | Environments scope | Home |
| GPU environment: none, artistic sky, HDR file; choose, check, details, sRGB primaries | Environment | Source | Home |
| Sky gradient: zenith, horizon, ground | Environment | Source › Sky | Home |
| Environment intensity and rotation | Environment | Orientation and strength | Home |
| CPU environment: none, constant, HDR; colour; HDR lighting samples | Environment | Source | Home |
| Bounded homogeneous volume | Environment | Atmosphere | Home |
### Backgrounds
| Old control | Now in | Section | Role |
| --- | --- | --- | --- |
| Camera background colour and backdrop | Environment | Backdrop | Home |
| Apply to retained raw master | Effects | Regrade | Home |
| Physical floor and wall, cyclorama | Environment | Ground and stage | Home |
| HDR illumination is separate (jump to Environment) | Environment | Source | Home |
### Camera
| Old control | Now in | Section | Role |
| --- | --- | --- | --- |
| Perspective, vertical field of view, eye distance, target, yaw, pitch, roll | Shot | Camera | Home |
| Fit Product | Navigation bar | Fit (shortcut: Shot › Frame subject) | Shortcut |
| Restore Saved Camera | Shot | Revert | Home |
| Depth of field: enable, aperture radius, focus distance | Shot | Lens | Home |
| Fit Image, 100% (result) | Result viewer | Toolbar | Home |
### Lens and finishing effects
| Old control | Now in | Section | Role |
| --- | --- | --- | --- |
| Creative look stack: add, order, remove, enabled, strength | Effects | Creative looks | Home |
| Lens-inspired effects: flare, chromatic offset, distortion, soft focus, sharpening, grain, seed, reset | Effects | Lens effects | Home |
| Presentation filters: RGB gain, contrast, saturation, vignette, bloom, reset | Effects | Finishing | Home |
| Regrade the raw master | Effects | Regrade | Home |
| Retain raw linear EXR | Output | Image › Format | Home |
### Output
| Old control | Now in | Section | Role |
| --- | --- | --- | --- |
| Export format: display PNG or raw linear EXR | Output | Image › Format | Home |
| Width, height, lock aspect, swap, HD, Full HD, 4K, largest at this aspect, limits | Output, Size and quality | Image › Size; the presets in Size and quality | Home |
| GPU samples, path depth, seed | Sampling | Path tracing | Home |
| Nested dielectrics, adaptive sampling, transmission strata | Sampling | Adaptive; Glass transport | Home |
| Tone mapping, denoise | Effects, Sampling | Tone; Path tracing | Home |
| CPU: resource profile, sampling mode, edge samples, progressive samples and seed | Sampling | CPU ray tracing | Home |
| Exposure EV | Shot | Exposure | Home |
| Transparent background | Output | Image | Home |
| Browse and review quality specifications, apply fixed budget | Size and quality, Review and apply | Quality specifications; Quality variant | Home |
| Completed image preview, Open in the main canvas | Result viewer | Opens over the view | Home |
### Diagnostics
| Old control | Now in | Section | Role |
| --- | --- | --- | --- |
| Live-preview payload bytes, scene error | Check | Renderers; This request | Home |
| Refresh captured scene summary, bodies, triangles | Check | This request | Home |
| Last preflight: memory estimate, warnings, details | Check | This request; Under the hood | Home |
| Captured job details, exact current request, Copy JSON | Check | Under the hood | Home |
| Explicit capability boundaries, capability catalogue | Check | Under the hood | Home |
### Stage bar
| Old control | Now in | Section | Role |
| --- | --- | --- | --- |
| Pick Body, Face, Component; Paint; Pick Up | Stage tools, Quick bar | Look | Home |
| Preview note and gear | Preview menu | Preview | Home |
| Lights fixed to Camera or World | Stage tools | Light | Home |
| Perspective \| Orthographic | Stage tools | Frame (shortcut of Shot › Camera) | Shortcut |
| Output size and format chips, Check | Stage tools | Output (shortcuts of Output, Sampling, Check) | Shortcut |
### Browser
| Old control | Now in | Section | Role |
| --- | --- | --- | --- |
| Model \| Library toggle | — | The library is in the stage panels | Removed |
+121
View File
@@ -0,0 +1,121 @@
# Render workspace UX roadmap
Tracking issue: #6281. Related: #5493 (render integration), #6201 (Render usability audit).
The Render workspace on `main` (basic delivery #6050 and the full ribbon, inspector and
libraries from #5494) works but is one ribbon tab over an 11-page inspector. This plan
restructures it, in small PRs, around the order a render is prepared. It reuses the existing
commands and controls; it does not add a second renderer or change the document format.
## Design rationale
Render does not use tools; it configures the variables of a scene. A ribbon suits tool-driven
workspaces, so this one has none. The four stages at the top (Look, Light, Frame, Output) carry
the most common tools and the major settings. Configuration happens in the Inspector on the right
(the user can move it). Presets and assets load from the panel at the bottom left. Anything in
the scene, such as a body, a light or the environment, is edited by selecting it in the Browser
or in the view.
## Interaction design
- **One selection.** The Browser lists what exists: Environment, Lights, Shots, Objects and Output.
Selecting a row, or picking in the view, selects the same thing. The Inspector shows the one
editor for the selection; with nothing selected it shows the scene overview. There is no panel
switcher and no All page.
- **Stage bar.** At the top, as tall as the workspace card, in two rows: stage chips (Look, Light,
Frame, Output), Render, Cancel and command search above; the tools of the current stage below
(Pick and Paint; light placement; lens and Save shot; size, format and Check). Stages choose what
the Browser expands and what the bottom-left panel offers. They never lock a control or force an
order. Opening a stage or an editor never applies, saves or starts anything.
- **Bottom-left panel.** Presets and assets for the current stage: Appearances (applicable
resources first), Studios and Environments, Frame presets and guides, Size presets.
- **Editors** (one per kind of selection):
| Selection | Editor | Holds | In the view |
| --- | --- | --- | --- |
| Body, face, component | Appearance | look chain, Applies to scope, colour, finish, transparency; Advanced: glass, clear coat, maps, saved optics, CPU reflections | outline in the live view only |
| Wear | Wear | aging layers, selected layer, target | none |
| Environment | Environment | source, rotation, intensity, backdrop, blur, ground and stage, atmosphere | rotation ring |
| Lights group | Lighting | light source, shadows and sampling, per-type light budget | none |
| A light | Light | type, intensity, colour, softness, position, aim, Place by highlight, duplicate, delete | handle, intensity dial, distance, orbit |
| A shot | Shot | projection, field of view, distance, exposure, frame and aspect, size, update, revert, duplicate, remove | frame overlay |
| Output | Output | renderer, size, format, transparency, destination, render | result viewer |
| Output › Sampling | Sampling | quality, path tracing, adaptive sampling, glass transport, CPU ray tracing | none |
| Output › Effects | Effects | tone, creative looks, lens effects, finishing, regrade | none |
| Output › Check | Check | request check, this request, renderers, under the hood | none |
| Nothing | Scene overview | one line per stage, Check, Render shot | none |
[Render workspace panels](render-panels.md) lists every panel, what it is attached to, how it is
reached and when, and where each control of the removed Render studio panel went.
- **Docks.** The Browser and the Inspector float and share the same top and bottom edges, starting
below the stage bar and ending above the Shots strip. The compass floats in the canvas beside
the Inspector. The Shots strip (`view.shots_strip`, a UI preference) hides from its chevron.
- **Picking and assigning.** Body, face or component picking with outlines that exist only in the
live view and never in images. Click a library look, drag it onto the model, or paint; the
Inspector shows where a look comes from.
- **Results.** The existing result viewer, then several retained images and a Live | Render compare.
## Where the old ribbon items go
| Ribbon item | Now |
| --- | --- |
| Assign, Paint, Pick Up | Look tools, quick bar on the selection, drag from the library |
| Edit, Replace All, Remove, From Material | Appearance editor and the right-click menu |
| Library, Same Look | Appearances panel; Select same look in the row menu |
| Presets, Environment | Studios and Environments panel; Environment row |
| Add Light, By Highlight, Solo | plus on Lights; Light editor; solo and mute on the row |
| Ground, Background | Environment editor |
| Save Shot, Update, Strip | Frame tools, the Shots strip, the strip chevron |
| Fit, Views | view bar at the bottom of the view |
| Lens, Exposure, Aspect, Guides | Shot editor and Frame presets |
| Size, Transparent, Quality, Check | Output tools, Output editor, Size presets |
| Render, Cancel | stage bar; R and Esc |
| Open, Compare, Export, All Shots | result viewer; Output editor and the strip |
Every command stays registered, so search (S), keys, the radial menu, scripts and the control
channel still reach all of them.
## Order of work
Each step is its own PR. Steps that edit the shared Render files are taken one after another.
"Keep" means the open PR stays as it is, "Modify" that it changes to fit the design above.
1. Roadmap and claim (this document). Modify: rewritten for this design.
2. Original icon drafts for owner review. Keep.
3. ui_kit widgets: lit sphere thumbnail and look chain. Keep.
4. Shell: stage bar replaces the four tabs and the ribbon; aligned floating docks; Shots strip host;
free-canvas navigation. Modify.
5. Picking and outlines in the Render view. Keep; the Pick control sits in the Look tools.
6. Browser scene tree with Output row, plus on groups, solo and mute on rows, stage expansion. Modify.
7. Appearance engine: `appearance.chain`, `appearance.replace`, From Material tests. Keep.
8. Bottom-left stage panel with the Appearances grid; applicable resources first. Modify.
9. Assign gestures: click, drag, paint, Pick Up, copy and paste; Paint and Pick Up in the Look tools. Modify.
10. Appearance editor with the look chain, scope and mixed values; Advanced section. Modify.
11. Shots strip with thumbnails, Update and Revert. Modify: drop the ribbon item.
12. Output editor and result viewer: size presets, Check, Copy, progress pill. Modify.
13. Selection model and Inspector dispatch: one selection, one editor per kind, scene overview. New.
14. Movable Inspector (dock side). New.
15. Environment editor, ground, Studios and Environments panel, rotation ring. New.
16. Lights: named lights, Light editor, solo and mute, handles in the view, Place by highlight. New.
17. Shot editor, frame overlay, aspect presets, guides, Frame presets. New.
18. Output children (Sampling, Effects, Check) and Size presets. New.
19. Several retained results, Live | Render compare, render all shots. New.
20. Focus view, compact and dark layouts, localisation pass. New.
21. Deferred: a lighting rig shared across shots. It needs an owner decision and one
`format/pending` file; no UI step depends on it, so the UI work goes first and every shot
keeps its own lights until it is decided.
22. Wear, Lighting, Sampling and Check editors with every remaining control of the Render studio
panel moved to its home ([panel plan](render-panels.md)); the Render studio panel is deleted. New.
## Rules for every PR
- Panels float and use `ui_kit::Container`, `Panel`, `Menu`, `Confirm` and the shared fields;
colours come from `theme::Tokens`; cameras use `navigation::handle`.
- Everything is a command. New ids must not collide with `render.*` ids already registered;
planned controls are faded with their reason and refuse without changing anything.
- No panics; input-derived numbers are bounded and finite. UI preferences are never saved in
the design. A saved concept is one new `crates/doc/src/format/pending/<name>.rs`.
- Labels are catalog entries in en, ja, zh, de and ko (`cargo xtask lists --fix`).
- Commit messages are titles only. Each PR carries targeted tests and, for visible changes,
before and after screenshots.
+30
View File
@@ -558,3 +558,33 @@ shared renderer snapshots that palette into the icon look and cache identity;
callers continue to request semantic icon IDs and surface-specific monochrome ink.
See [the shipped-theme colour audit](theme-colour-audit.md) for the schema,
workspace assignments, accessibility rules and intentionally monochrome themes.
## Material previews and look chains
`sphere_thumbnail(ui, id, Sphere { color, kind, opacity }, size)` paints a lit
sphere using a fixed CPU mesh and the egui painter, without a texture or GPU.
`FinishKind` covers Matte, Satin, Gloss, Metal and Glass. Colour is document data;
highlights, shadows, checker and UI chrome use `Tokens`. Glass shows a four-by-four
checker and defaults to 35% opacity; other finishes default to opaque. Explicit
opacity is clamped to 0–1; non-finite opacity uses the finish default. Sizes are
rounded and bounded to 1–96 logical points (non-finite sizes become 1).
A shared per-egui-context FIFO cache retains at most 64 meshes, keyed by RGBA,
finish, byte opacity and bounded size. Hits reuse shading without changing eviction
order. Every mesh has 637 vertices and 1,152 triangles, independently of input.
Theme highlight/shadow changes clear the cache. No file assets are added.
`look_chain(ui, id, labels, looks, active)` draws Face → Body → Component → Material
as four connected steps. Pass translated labels, `[Option<Sphere>; 4]` and one
`LookStep`; empty slots use a dashed circle and plus, while filled slots reuse the
sphere cache. Exactly one step has a `Tokens::render_accent` ring. It returns
`Option<LookStep>` on activation; callers own commands and selection. Disabled
controls do not activate. Handles/rects are `{id}:face`, `:body`, `:component`,
`:material`; `{id}` is the full allocation. Long labels clip within their step.
These are content widgets; their enclosing surface still belongs to `Container`.
Gallery entries show all five sphere finishes, a mixed filled/empty look chain
with Body active, and an empty chain with Face active in both themes.
For CPU-rendered review images, run
`SOLIDCRAFT_MATERIAL_WIDGET_EVIDENCE=/tmp/material-widgets cargo test -p solidcraft-ui-egui ui_kit::gallery::material_widgets_board_in_both_themes`.
+1 -1
View File
@@ -24,7 +24,7 @@ const DIR: &str = "assets/locales";
/// Catalogs delivered complete: a missing message here fails `--check`. English is the source
/// catalog and is checked implicitly; keep this in step with `COMPLETE` in `ui-egui::i18n`.
const COMPLETE: &[&str] = &["ja", "zh"];
const COMPLETE: &[&str] = &["ar", "he", "ja", "vi", "zh"];
/// What `run` found.
#[derive(Default)]