diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index be4e28f9..232c761b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -119,6 +119,8 @@ jobs: run: cargo fmt --all -- --check - name: Asset attribution run: cargo xtask assets + - name: UI icon standards and review tool + run: cargo xtask icons - name: Licences run: cargo xtask licences --check - name: Dependency layers diff --git a/AGENTS.md b/AGENTS.md index 82492953..dbdea882 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -65,8 +65,11 @@ People trust SolidCraft with their designs; a crash loses their work. **This out nothing from FreeCAD is linked, vendored or shipped.) The sketch solver and the blend operations are our own. - **Assets — absolutely essential.** SolidCraft contains **no iconography, images, fonts or - artwork from commercial CAD products — ever.** Every UI icon is drawn in collected code - (`assets/icons/ui/source/`, compiled through `crates/ui-egui/src/icons.rs`). Any file asset must be original or openly licensed and **must have a row in ATTRIBUTION.md** + artwork from commercial CAD products — ever.** Command icons are the original SVG masters in + `assets/icons/ui/design/svg/`, compiled through `crates/ui-egui/src/icon_svg.rs`. + **Before any icon, icon assignment or media edit, read `assets/AGENTS.md`, + `assets/icons/ui/AGENTS.md` and `assets/icons/ui/STYLE_GUIDE.md`.** Use the canonical + bases and named finished references there; never invent a parallel icon style. Any file asset must be original or openly licensed and **must have a row in ATTRIBUTION.md** (`cargo xtask assets` enforces it). Screenshots of commercial CAD software are never committed. Breaking this rule is the most serious mistake you can make in this repo. - **Everything is a command.** User-visible behaviour = a command in `crates/engine/src/cmd/*` @@ -85,7 +88,9 @@ People trust SolidCraft with their designs; a crash loses their work. **This out preview, future flat pattern or drawing pages) calls `navigation::handle` with its `NavCaps` (orbit off for flat 2D pages). New canvases must use `navigation::handle`; never hand-roll camera input (a test rejects it). -- **Rust only.** Units are millimetres and radians internally; Z is up. The one exception is +- **Application code is Rust only.** Units are millimetres and radians internally; Z is up. + Local asset authoring, lint and review tools use Python 3.11+ as documented in + `assets/AGENTS.md`; Python is not an application runtime dependency. The application exception is JavaScript the browser requires: the service worker `apps/solidcraft-web/sw.js` (browsers only run service workers as JavaScript) and the small inline scripts in `apps/solidcraft-web/index.html`. Keep them minimal; `cargo xtask web-offline-test` covers the service worker. diff --git a/ATTRIBUTION.md b/ATTRIBUTION.md index de3c2002..cf412ccc 100644 --- a/ATTRIBUTION.md +++ b/ATTRIBUTION.md @@ -17,25 +17,13 @@ Editable drawing code, native projects and shipped exports are collected under ` | examples/render-studio.json | SolidCraft contributors | original procedural box/cylinder enclosure script for the integrated Render workspace | MIT OR Apache-2.0 | | examples/render-opaque-studio.json | SolidCraft contributors | original procedural sphere study varying opaque metal fraction and roughness independently; no external assets or engineering data | MIT OR Apache-2.0 | | crates/gpu/src/pbr.rs | SolidCraft contributors | original WGSL translation of SolidCraft's opaque surface and fixed studio reference, embedded as Rust source; no external shader code | MIT OR Apache-2.0 | -| assets/archive/icons/macos-v1/exports/icon.svg | SolidCraft contributors | original macOS adaptation of the code-drawn SolidCraft mark, edited and exported with VectorCraft; Apple HIG and production templates consulted as layout references only | MIT OR Apache-2.0 | | assets/icons/app/macos/source/SolidCraft.icon/icon.json | SolidCraft contributors | approved Icon Composer composition and appearance settings for the original SolidCraft artwork | MIT OR Apache-2.0 | | assets/icons/app/macos/source/SolidCraft.icon/Assets/02-grid.svg | SolidCraft contributors | original perspective grid with elliptical radial alpha, exported from VectorCraft | MIT OR Apache-2.0 | | assets/icons/app/macos/source/SolidCraft.icon/Assets/02-grid-dark.svg | SolidCraft contributors | original blue perspective grid with elliptical radial alpha, exported from VectorCraft for the Dark appearance | MIT OR Apache-2.0 | | assets/icons/app/macos/source/SolidCraft.icon/Assets/03-solid-blue-cutout.svg | SolidCraft contributors | original saturated blue CAD solid with transparent topology cutouts, edited and exported with VectorCraft | MIT OR Apache-2.0 | | assets/icons/app/macos/exports/Assets.car | SolidCraft contributors | Apple actool compilation of assets/icons/app/macos/source/SolidCraft.icon for macOS; contains original artwork only | MIT OR Apache-2.0 | -| assets/archive/icons/macos-v1/source/icon.vectorcraft | SolidCraft contributors | editable VectorCraft source for assets/archive/icons/macos-v1/exports/icon.svg, with named face and background layers | MIT OR Apache-2.0 | -| assets/archive/icons/macos-v1/exports/icon.png | SolidCraft contributors | 1024 × 1024 transparent sRGB export of assets/archive/icons/macos-v1/exports/icon.svg from VectorCraft | MIT OR Apache-2.0 | +| assets/archive/icons/macos-v1/source/icon.vectorcraft | SolidCraft contributors | retained editable historical VectorCraft source, with named face and background layers; redundant exports retired | MIT OR Apache-2.0 | | assets/icons/app/macos/exports/icon.icns | SolidCraft contributors | macOS fallback compiled by Apple's actool from the approved assets/icons/app/macos/source/SolidCraft.icon composition | MIT OR Apache-2.0 | -| assets/archive/icons/macos-v1/exports/icon.iconset/icon_16x16.png | SolidCraft contributors | native sips export from assets/archive/icons/macos-v1/exports/icon.png | MIT OR Apache-2.0 | -| assets/archive/icons/macos-v1/exports/icon.iconset/icon_16x16@2x.png | SolidCraft contributors | native sips export from assets/archive/icons/macos-v1/exports/icon.png | MIT OR Apache-2.0 | -| assets/archive/icons/macos-v1/exports/icon.iconset/icon_32x32.png | SolidCraft contributors | native sips export from assets/archive/icons/macos-v1/exports/icon.png | MIT OR Apache-2.0 | -| assets/archive/icons/macos-v1/exports/icon.iconset/icon_32x32@2x.png | SolidCraft contributors | native sips export from assets/archive/icons/macos-v1/exports/icon.png | MIT OR Apache-2.0 | -| assets/archive/icons/macos-v1/exports/icon.iconset/icon_128x128.png | SolidCraft contributors | native sips export from assets/archive/icons/macos-v1/exports/icon.png | MIT OR Apache-2.0 | -| assets/archive/icons/macos-v1/exports/icon.iconset/icon_128x128@2x.png | SolidCraft contributors | native sips export from assets/archive/icons/macos-v1/exports/icon.png | MIT OR Apache-2.0 | -| assets/archive/icons/macos-v1/exports/icon.iconset/icon_256x256.png | SolidCraft contributors | native sips export from assets/archive/icons/macos-v1/exports/icon.png | MIT OR Apache-2.0 | -| assets/archive/icons/macos-v1/exports/icon.iconset/icon_256x256@2x.png | SolidCraft contributors | native sips export from assets/archive/icons/macos-v1/exports/icon.png | MIT OR Apache-2.0 | -| assets/archive/icons/macos-v1/exports/icon.iconset/icon_512x512.png | SolidCraft contributors | native sips export from assets/archive/icons/macos-v1/exports/icon.png | MIT OR Apache-2.0 | -| assets/archive/icons/macos-v1/exports/icon.iconset/icon_512x512@2x.png | SolidCraft contributors | native sips export from assets/archive/icons/macos-v1/exports/icon.png | MIT OR Apache-2.0 | | docs/screenshots/class-a-blend.png | SolidCraft contributors | screenshot of SolidCraft itself: two boxes bridged by `surface.blend` (a 40x10 sheet, `solidcraft-cli snapshot` of plan/evidence script ev_blend) | MIT OR Apache-2.0 | | docs/screenshots/class-a-crown.png | SolidCraft contributors | screenshot of SolidCraft itself: a patch crowned with `surface.patch crown=0.3` over a saddle rim (`solidcraft-cli snapshot`) | MIT OR Apache-2.0 | | docs/screenshots/class-a-boundary.png | SolidCraft contributors | screenshot of SolidCraft itself: a patch through two sketch lines built by `surface.boundary` with `crown: 0.4` (`solidcraft-cli snapshot`) | MIT OR Apache-2.0 | @@ -89,7 +77,6 @@ Editable drawing code, native projects and shipped exports are collected under ` | assets/icons/app/windows/exports/solidcraft-executable.ico | SolidCraft contributors | Generated from assets/icons/app/shared/source/app_icon.rs with the original CLI/ICO encoders | MIT OR Apache-2.0 | | assets/icons/app/windows/exports/solidcraft.ico | SolidCraft contributors | Generated from assets/icons/app/shared/source/app_icon.rs with the original CLI/ICO encoders | MIT OR Apache-2.0 | | assets/icons/ui/source/glyphs.rs | SolidCraft / VectorCraft contributors | Original glyphs; caption geometry adapted from VectorCraft titlebar.rs (https://github.com/storytold/vectorcraft) | MIT OR Apache-2.0 | -| assets/icons/ui/source/icons.rs | SolidCraft contributors | Original SolidCraft resource data or drawing code, collected without redesign | MIT OR Apache-2.0 | | assets/icons/ui/source/markers.rs | SolidCraft contributors | Original SolidCraft resource data or drawing code, collected without redesign | MIT OR Apache-2.0 | | assets/icons/ui/source/navigation.rs | SolidCraft contributors | Original SolidCraft resource data or drawing code, collected without redesign | MIT OR Apache-2.0 | | assets/icons/ui/source/pointer.rs | SolidCraft contributors | Original SolidCraft resource data or drawing code, collected without redesign | MIT OR Apache-2.0 | @@ -117,6 +104,290 @@ Editable drawing code, native projects and shipped exports are collected under ` | assets/styles/web/loading.css | SolidCraft contributors | Original SolidCraft resource data or drawing code, collected without redesign | MIT OR Apache-2.0 | | crates/io/src/native_cad/fixtures/block.FCStd | SolidCraft contributors | original 10 × 20 × 30 mm test box saved by FreeCAD 1.1.4 run as an external program (`make_block_fcstd.py` beside it); no third-party geometry | MIT OR Apache-2.0 | | crates/io/src/native_cad/fixtures/block.3dm | SolidCraft contributors | original 10 × 20 × 30 mm test box written with McNeel's MIT-licensed rhino3dm Python package (`make_block_3dm.py` beside it); no third-party geometry | MIT OR Apache-2.0 | +| assets/icons/ui/design/aliases.toml | SolidCraft contributors | icon ids that share a master, original SolidCraft data | MIT OR Apache-2.0 | +| assets/icons/ui/design/palette.toml | SolidCraft contributors | icon colour roles (light and dark) and accents, original SolidCraft data | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/flat/bug_report.svg | SolidCraft contributors | single-colour application/navigation icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/flat/chevron.svg | SolidCraft contributors | single-colour application/navigation icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/flat/fit.svg | SolidCraft contributors | single-colour application/navigation icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/flat/folder.svg | SolidCraft contributors | single-colour application/navigation icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/flat/help.svg | SolidCraft contributors | single-colour application/navigation icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/flat/home.svg | SolidCraft contributors | single-colour application/navigation icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/flat/lookat.svg | SolidCraft contributors | single-colour application/navigation icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/flat/moon.svg | SolidCraft contributors | single-colour application/navigation icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/flat/nav_settings.svg | SolidCraft contributors | single-colour application/navigation icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/flat/orbit.svg | SolidCraft contributors | single-colour application/navigation icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/flat/pan.svg | SolidCraft contributors | single-colour application/navigation icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/flat/perspective.svg | SolidCraft contributors | single-colour application/navigation icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/flat/redo.svg | SolidCraft contributors | single-colour application/navigation icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/flat/save.svg | SolidCraft contributors | single-colour application/navigation icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/flat/settings.svg | SolidCraft contributors | single-colour application/navigation icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/flat/spin.svg | SolidCraft contributors | single-colour application/navigation icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/flat/style.svg | SolidCraft contributors | single-colour application/navigation icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/flat/undo.svg | SolidCraft contributors | single-colour application/navigation icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/flat/zoom.svg | SolidCraft contributors | single-colour application/navigation icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/accessibility.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/activate.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/align.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/appearance.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/arc3.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/arc_center.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/arc_slot.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/arc_tangent.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/as_built.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/auto_constrain.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/axis.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/bendtable.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/blend.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/blend_surface.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/body.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/boss.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/boundary.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/boundary_fill.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/box.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/break.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/c_coincident.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/c_collinear.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/c_concentric.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/c_equal.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/c_fix.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/c_hv.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/c_midpoint.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/c_parallel.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/c_perpendicular.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/c_polygon.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/c_smooth.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/c_symmetry.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/c_tangent.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/camera.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/canvas.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/capture.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/center_of_mass.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/centerline.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/chain.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/chamfer.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/checkpoint.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/circle.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/circle2.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/circle3.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/circle_tt.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/circle_ttt.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/coil.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | 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 | +| 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 | +| assets/icons/ui/design/svg/model/constrainer.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/construction.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/convert_sheet.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/copy.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/curvature_comb.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/curvature_map.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/cylinder.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/decal.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/delete.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/dimension.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/dots.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/draft.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/draft_analysis.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/drive.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/dxf.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/edge_continuity.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/edit.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/ellipse.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/emboss.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/environment_map.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/explode.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/export.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/extend.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/extrude.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/eye.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/fastener.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/fillet.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/filter.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/finish.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/fit_section.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/flange.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/flat.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/fold.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/folder.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/graph.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/hem.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/hole.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/import.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/include.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/inspect.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/interference.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/intersect.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/intersection_curve.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/iso_analysis.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/iso_curve.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/joint.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/joint_origin.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/library.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/line.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/line_mid.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/lip.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/list.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/loft.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/material.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/measure.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/min_radius.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/mirror.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/motion_link.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/move.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/new.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/offset.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/offset_face.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/offset_surface.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/open.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/origin.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/params.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/paste.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/patch.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/pattern.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/pattern_circ.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/pattern_path.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/pattern_rect.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/perforate.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/pipe.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | 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 | +| assets/icons/ui/design/svg/model/plastic_assign.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/plastic_rule.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/play.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/point.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/polygon.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/presspull.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/project.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/project_surface.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/rect.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/rect3.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/rect_center.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/rect_rounded.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/refold.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/rename.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/render_image.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/render_scene.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/reorder.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/rest.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/restore.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/reverse_normal.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/revert.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/revolve.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/rib.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/rigid_group.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/rollback.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/ruled.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/scale.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/section.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/select.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/sheet.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/sheet_rules.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/shell.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/sketch.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/sketch_chamfer.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/sketch_fillet.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/sketch_mirror.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/slot.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/snap.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/sphere.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/spline.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/spline_cv.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/split_body.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/split_face.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/spun.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/star.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/status_error.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/status_warning.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/stitch.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/suppress.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/surface.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/svg.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/sweep.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/text.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/thicken.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/thread.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/torus.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/trim.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/unfold.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/unstitch.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/untrim.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/update.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/zebra.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/small/as_built.svg | SolidCraft contributors | small-slot icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/small/component.svg | SolidCraft contributors | small-slot icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/small/curvature_comb.svg | SolidCraft contributors | small-slot icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/small/interference.svg | SolidCraft contributors | small-slot icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/small/iso_analysis.svg | SolidCraft contributors | small-slot icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/small/joint.svg | SolidCraft contributors | small-slot icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/small/pattern.svg | SolidCraft contributors | small-slot icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/small/section.svg | SolidCraft contributors | small-slot icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/workspace/ws_animate.svg | SolidCraft contributors | workspace icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/polygon_circumscribed.svg | SolidCraft contributors | original sketch variant icon master (64 grid) | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/polygon_edge.svg | SolidCraft contributors | original sketch variant icon master (64 grid) | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/slot_overall.svg | SolidCraft contributors | original sketch variant icon master (64 grid) | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/slot_center.svg | SolidCraft contributors | original sketch variant icon master (64 grid) | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/arc_slot_center.svg | SolidCraft contributors | original sketch variant icon master (64 grid) | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/sketch_chamfer_angle.svg | SolidCraft contributors | original sketch variant icon master (64 grid) | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/sketch_chamfer_two_distances.svg | SolidCraft contributors | original sketch variant icon master (64 grid) | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/gear.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/rack_pinion.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/screw.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/cam.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/trace.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/agent_chat.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/normal.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/infinite.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/pattern_flip.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/pattern_center.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/reach.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/cut.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/compare.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/web.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/replace_face.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/bounding_solid.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/delete_face.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/snap_fit.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/contact_sets.svg | SolidCraft contributors | original contact icon master (64 grid) | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/contact_all.svg | SolidCraft contributors | original contact icon master (64 grid) | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/contact_off.svg | SolidCraft contributors | original contact icon master (64 grid) | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/contact_new.svg | SolidCraft contributors | original contact icon master (64 grid) | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/axis_through_cylinder.svg | SolidCraft contributors | original construction icon master (64 grid) | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/axis_normal_to_face.svg | SolidCraft contributors | original construction icon master (64 grid) | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/axis_two_planes.svg | SolidCraft contributors | original construction icon master (64 grid) | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/axis_two_points.svg | SolidCraft contributors | original construction icon master (64 grid) | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/axis_edge.svg | SolidCraft contributors | original construction icon master (64 grid) | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/point_vertex.svg | SolidCraft contributors | original construction icon master (64 grid) | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/point_two_edges.svg | SolidCraft contributors | original construction icon master (64 grid) | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/point_three_planes.svg | SolidCraft contributors | original construction icon master (64 grid) | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/point_center.svg | SolidCraft contributors | original construction icon master (64 grid) | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/point_edge_plane.svg | SolidCraft contributors | original construction icon master (64 grid) | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/point_along_path.svg | SolidCraft contributors | original construction icon master (64 grid) | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/plane_midplane.svg | SolidCraft contributors | original construction icon master (64 grid) | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/plane_two_edges.svg | SolidCraft contributors | original construction icon master (64 grid) | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/plane_three_points.svg | SolidCraft contributors | original construction icon master (64 grid) | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/plane_tangent.svg | SolidCraft contributors | original construction icon master (64 grid) | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/plane_along_path.svg | SolidCraft contributors | original construction icon master (64 grid) | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/model/plane_perpendicular.svg | SolidCraft contributors | original construction icon master (64 grid) | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/workspace/ws_design.svg | SolidCraft contributors | workspace icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/workspace/ws_drawing.svg | SolidCraft contributors | workspace icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/svg/workspace/ws_render.svg | SolidCraft contributors | workspace icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 | +| assets/icons/ui/design/references.toml | SolidCraft contributors | original icon authoring or review tool resource | MIT OR Apache-2.0 | +| assets/icons/ui/design/tools/author/bases.py | SolidCraft contributors | original icon authoring or review tool resource | MIT OR Apache-2.0 | +| assets/icons/ui/design/tools/author/new_icon.py | SolidCraft contributors | original icon authoring or review tool resource | MIT OR Apache-2.0 | +| assets/icons/ui/design/tools/review/index.html | SolidCraft contributors | original icon authoring or review tool resource | MIT OR Apache-2.0 | +| assets/icons/ui/design/tools/review/guide.html | SolidCraft contributors | original icon authoring or review tool resource | MIT OR Apache-2.0 | +| assets/icons/ui/design/tools/review/review.css | SolidCraft contributors | original icon authoring or review tool resource | MIT OR Apache-2.0 | +| assets/icons/ui/design/tools/review/review.js | SolidCraft contributors | original icon authoring or review tool resource | MIT OR Apache-2.0 | +| assets/icons/ui/design/tools/icon_palette.py | SolidCraft contributors | Original shared semantic icon palette resolver for repository tools | MIT OR Apache-2.0 | +| assets/icons/ui/design/tools/icon_status.py | SolidCraft contributors | icon lint, status page or preview tool (standard-library Python), original SolidCraft code | MIT OR Apache-2.0 | +| assets/icons/ui/design/tools/lint_icons.py | SolidCraft contributors | icon lint, status page or preview tool (standard-library Python), original SolidCraft code | MIT OR Apache-2.0 | +| assets/icons/ui/design/tools/preview.py | SolidCraft contributors | icon lint, status page or preview tool (standard-library Python), original SolidCraft code | MIT OR Apache-2.0 | +| assets/icons/ui/design/tools/review_state.py | SolidCraft contributors | icon review state helper (standard-library Python), original SolidCraft code | MIT OR Apache-2.0 | +| assets/icons/ui/design/review.json | SolidCraft contributors | icon review state (accepted icons and notes), original SolidCraft data | MIT OR Apache-2.0 | +| assets/icons/ui/design/tools/author/lib.py | SolidCraft contributors | icon authoring script (standard-library Python), original SolidCraft code | MIT OR Apache-2.0 | | docs/book/src/og.png | SolidCraft contributors | copy of assets/branding/web/source/og.png staged by cargo xtask book (gitignored) | MIT OR Apache-2.0 | diff --git a/Cargo.lock b/Cargo.lock index 684cb308..e325ee53 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -232,6 +232,12 @@ version = "2.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "220a2c618ab466efe41d0eace94dfeff1c35e3aa47891bdb95e1c0fefffd3c99" +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + [[package]] name = "arrayvec" version = "0.7.8" @@ -4245,6 +4251,8 @@ dependencies = [ "sha2 0.10.9", "solidcraft-engine", "solidcraft-gpu", + "tiny-skia", + "toml", "ttf-parser", "web-time", "zip 3.0.0", @@ -4299,6 +4307,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +[[package]] +name = "strict-num" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" + [[package]] name = "subtle" version = "2.6.1" @@ -4424,6 +4438,31 @@ dependencies = [ "zune-jpeg", ] +[[package]] +name = "tiny-skia" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47ffee5eaaf5527f630fb0e356b90ebdec84d5d18d937c5e440350f88c5a91ea" +dependencies = [ + "arrayref", + "arrayvec", + "bytemuck", + "cfg-if", + "log", + "tiny-skia-path", +] + +[[package]] +name = "tiny-skia-path" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edca365c3faccca67d06593c5980fa6c57687de727a03131735bb85f01fdeeb9" +dependencies = [ + "arrayref", + "bytemuck", + "strict-num", +] + [[package]] name = "tinystr" version = "0.8.4" diff --git a/Cargo.toml b/Cargo.toml index 080e0184..ab168710 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -73,6 +73,10 @@ image = { version = "0.25", default-features = false, features = ["png", "hdr"] png = "0.18.1" # LZW for GIF animations (MIT OR Apache-2.0, pure Rust; already in the tree through image's tiff). weezl = "0.1.12" +# rasterises the UI icon SVGs (BSD-3-Clause, pure Rust, builds for wasm) +tiny-skia = { version = "0.12", default-features = false, features = ["std", "simd"] } +# build scripts: reads the icon palette (MIT OR Apache-2.0) +toml = "1" # ui egui = "0.36" diff --git a/README.md b/README.md index d9b2c95c..41871847 100644 --- a/README.md +++ b/README.md @@ -206,9 +206,13 @@ cargo run --release -p solidcraft-cli -- snapshot examples/bracket.json --out br cargo run --release -p solidcraft-cli -- render examples/render-studio.json --out product.png cargo run --release -p solidcraft -- part.step # open a STEP file as a new design cargo run --release -p solidcraft-cli -- eval part.step # measure an imported STEP file -cargo xtask ci # fmt, clippy, tests, assets, layers, wasm +cargo xtask ci # fmt, clippy, tests, icons, assets, layers, wasm ``` +UI artwork changes follow the [icon specification](assets/icons/ui/STYLE_GUIDE.md). +The [included review tool](assets/icons/ui/README.md) discovers new icons and keeps +unaccepted artwork Open; run `cargo xtask icons` to check the collection. + Windows builds: `cargo xwin build --release --target x86_64-pc-windows-msvc -p solidcraft`. On Windows and Linux, SolidCraft detects a connected 3Dconnexion SpaceNavigator or SpaceMouse through its diff --git a/apps/solidcraft-cli/tests/mcp_connect.rs b/apps/solidcraft-cli/tests/mcp_connect.rs index e384d660..e96ebd7d 100644 --- a/apps/solidcraft-cli/tests/mcp_connect.rs +++ b/apps/solidcraft-cli/tests/mcp_connect.rs @@ -9,8 +9,16 @@ use serde_json::{Value, json}; #[test] fn mcp_connect_serves_before_the_app_runs_and_reaches_it_once_it_does() { - // A port that was free a moment ago: nothing listens on it. - let port = TcpListener::bind("127.0.0.1:0").unwrap().local_addr().unwrap().port(); + // This test needs the port closed until its second phase. A port obtained + // with bind(:0) and released can immediately be reused by another parallel + // test's mock server. Stay below the usual ephemeral ranges, + // with a process-specific starting point, and skip any occupied ports. + let port = (0..512u32) + .find_map(|offset| { + let port = 16000 + (std::process::id() + offset) % 16000; + TcpListener::bind(("127.0.0.1", port as u16)).ok().map(|listener| listener.local_addr().unwrap().port()) + }) + .expect("an unused non-ephemeral test port"); let addr = format!("127.0.0.1:{port}"); let control_dir = std::env::temp_dir().join(format!("solidcraft-mcp-connect-{}-{port}", std::process::id())); std::fs::create_dir_all(&control_dir).unwrap(); diff --git a/assets/AGENTS.md b/assets/AGENTS.md new file mode 100644 index 00000000..a3370a64 --- /dev/null +++ b/assets/AGENTS.md @@ -0,0 +1,22 @@ +# Resource work — mandatory + +Read `assets/README.md` before changing media, fonts, artwork or their consumers. +For **any UI icon, icon assignment or review-tool change**, also read +`assets/icons/ui/AGENTS.md` and `assets/icons/ui/STYLE_GUIDE.md` before editing. +These files are the specification; chat history and old previews are not required. + +- Keep one editable authority per resource. UI SVGs are masters, not exports of a + second competing drawing library. `source/` retains genuine native source and + the separately compiled Rust viewport glyphs. +- Keep only production inputs, necessary shipped exports, authoring tools and + provenance here. Put screenshots, contact sheets, audits, caches, test output, + review inbox files and package staging under ignored `target/`. +- Do not add historical alternatives, duplicate resolutions that packaging does + not consume, copied screenshots, `node_modules`, Python bytecode or generated + inventory CSVs to this collection. Preserve native editable source documents. +- Add an exact `ATTRIBUTION.md` row for every non-Markdown asset, including tool + code, web UI sources and metadata. Remove rows for intentionally retired files. +- Never copy or derive iconography from commercial CAD products. Use original + SolidCraft artwork, analytic geometry and the approved local references. +- Run `cargo xtask assets` and `cargo xtask icons`. Full repository gates still + apply before a commit. Never refresh acceptance hashes to bypass a failed check. diff --git a/assets/README.md b/assets/README.md index 2bfafd71..99be8dc5 100644 --- a/assets/README.md +++ b/assets/README.md @@ -13,7 +13,9 @@ assets/ macos/exports/ # paired Assets.car and icon.icns windows/exports/ # executable DIB ICO and installer PNG ICO linux/exports/hicolor/ # Linux/FreeBSD package images, 16–512 px - ui/source/ # original drawing library and other UI glyphs + ui/design/svg/ # authoritative original UI icon masters + ui/design/tools/ # authoring bases, checks and included review app + ui/source/ # Rust viewport glyphs, markers and navigation branding/web/source/og.png # shared social preview card styles/ui/palettes.rs # light/dark defaults styles/web/loading.css # web loading/canvas styles @@ -25,14 +27,15 @@ assets/ platforms/{macos,linux,windows}/ # bundle, freedesktop and WiX templates archive/icons/macos-v1/ source/icon.vectorcraft # earlier editable work, retained - exports/ # all historical SVG, PNG and iconset exports + # redundant historical exports are retired ``` ## Editing and regeneration [UI artwork](icons/ui/README.md) identifies the drawing masters and their consumers. -Edit geometry and IDs together in the original Rust source and review changes in -both application themes. Palettes and model presets are also editable Rust data. +Read the mandatory [UI style guide](icons/ui/STYLE_GUIDE.md) before editing an icon. +Edit SVG geometry and its command/UI assignments together; inspect both themes +and small sizes. Temporary media and generated reports belong in ignored target/. Palettes and model presets are also editable Rust data. The current macOS master is `icons/app/macos/source/SolidCraft.icon/`, including its linked SVG layers and appearance settings. The earlier VectorCraft document diff --git a/assets/archive/icons/macos-v1/exports/icon.iconset/icon_128x128.png b/assets/archive/icons/macos-v1/exports/icon.iconset/icon_128x128.png deleted file mode 100644 index 8b9e9dab..00000000 Binary files a/assets/archive/icons/macos-v1/exports/icon.iconset/icon_128x128.png and /dev/null differ diff --git a/assets/archive/icons/macos-v1/exports/icon.iconset/icon_128x128@2x.png b/assets/archive/icons/macos-v1/exports/icon.iconset/icon_128x128@2x.png deleted file mode 100644 index 309ea4ee..00000000 Binary files a/assets/archive/icons/macos-v1/exports/icon.iconset/icon_128x128@2x.png and /dev/null differ diff --git a/assets/archive/icons/macos-v1/exports/icon.iconset/icon_16x16.png b/assets/archive/icons/macos-v1/exports/icon.iconset/icon_16x16.png deleted file mode 100644 index ad4c4dc6..00000000 Binary files a/assets/archive/icons/macos-v1/exports/icon.iconset/icon_16x16.png and /dev/null differ diff --git a/assets/archive/icons/macos-v1/exports/icon.iconset/icon_16x16@2x.png b/assets/archive/icons/macos-v1/exports/icon.iconset/icon_16x16@2x.png deleted file mode 100644 index 5a629192..00000000 Binary files a/assets/archive/icons/macos-v1/exports/icon.iconset/icon_16x16@2x.png and /dev/null differ diff --git a/assets/archive/icons/macos-v1/exports/icon.iconset/icon_256x256.png b/assets/archive/icons/macos-v1/exports/icon.iconset/icon_256x256.png deleted file mode 100644 index 309ea4ee..00000000 Binary files a/assets/archive/icons/macos-v1/exports/icon.iconset/icon_256x256.png and /dev/null differ diff --git a/assets/archive/icons/macos-v1/exports/icon.iconset/icon_256x256@2x.png b/assets/archive/icons/macos-v1/exports/icon.iconset/icon_256x256@2x.png deleted file mode 100644 index 2d432f00..00000000 Binary files a/assets/archive/icons/macos-v1/exports/icon.iconset/icon_256x256@2x.png and /dev/null differ diff --git a/assets/archive/icons/macos-v1/exports/icon.iconset/icon_32x32.png b/assets/archive/icons/macos-v1/exports/icon.iconset/icon_32x32.png deleted file mode 100644 index 5a629192..00000000 Binary files a/assets/archive/icons/macos-v1/exports/icon.iconset/icon_32x32.png and /dev/null differ diff --git a/assets/archive/icons/macos-v1/exports/icon.iconset/icon_32x32@2x.png b/assets/archive/icons/macos-v1/exports/icon.iconset/icon_32x32@2x.png deleted file mode 100644 index 40cbf4c0..00000000 Binary files a/assets/archive/icons/macos-v1/exports/icon.iconset/icon_32x32@2x.png and /dev/null differ diff --git a/assets/archive/icons/macos-v1/exports/icon.iconset/icon_512x512.png b/assets/archive/icons/macos-v1/exports/icon.iconset/icon_512x512.png deleted file mode 100644 index 2d432f00..00000000 Binary files a/assets/archive/icons/macos-v1/exports/icon.iconset/icon_512x512.png and /dev/null differ diff --git a/assets/archive/icons/macos-v1/exports/icon.iconset/icon_512x512@2x.png b/assets/archive/icons/macos-v1/exports/icon.iconset/icon_512x512@2x.png deleted file mode 100644 index 36b7d736..00000000 Binary files a/assets/archive/icons/macos-v1/exports/icon.iconset/icon_512x512@2x.png and /dev/null differ diff --git a/assets/archive/icons/macos-v1/exports/icon.png b/assets/archive/icons/macos-v1/exports/icon.png deleted file mode 100644 index 36b7d736..00000000 Binary files a/assets/archive/icons/macos-v1/exports/icon.png and /dev/null differ diff --git a/assets/archive/icons/macos-v1/exports/icon.svg b/assets/archive/icons/macos-v1/exports/icon.svg deleted file mode 100644 index e77f135c..00000000 --- a/assets/archive/icons/macos-v1/exports/icon.svg +++ /dev/null @@ -1,63 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - SolidCraft macOS application icon - - - - - - - - - - - - - - - - - - - - - diff --git a/assets/icons/ui/AGENTS.md b/assets/icons/ui/AGENTS.md new file mode 100644 index 00000000..29f0973f --- /dev/null +++ b/assets/icons/ui/AGENTS.md @@ -0,0 +1,31 @@ +# UI icons — read before drawing or assigning one + +**Required:** read [STYLE_GUIDE.md](STYLE_GUIDE.md), `design/palette.toml`, and +`design/references.toml`. Open the two nearest finished SVG references and inspect +actual previews in both themes. Do this before choosing geometry or detail. + +1. Check `design/review.json`. A valid final id **or any final alias of its master** + protects that master and its small variant. Do not redraw it without an explicit + user instruction to reopen/change it. Do not edit accepted hashes to make lint pass. +2. Choose the category in the guide. Use `tools/author/bases.py` for a standard + grey cube or slab. Match its exact projection, shading, weight and footprint. + Add only the cue that explains the operation; do not invent a new base cube. +3. Edit the SVG master, or create a new id with the scaffold documented below. + Reuse an alias when both meaning and shape are shared. Give different operations + distinct drawings. Wire command, timeline and relevant browser assignments. +4. Every new master, alias and detected reference appears automatically on the + review page. **Never mark your own new artwork final.** The owner accepts it. + Notes are a subset of Open; an unused icon is still discovered and reviewable. +5. Look at before/after at 64–128 px and 16/24 px, light and dark. Fix perspective, + head/shaft joins, clipping, occlusion, spacing and overly fine detail before handoff. +6. Run `cargo xtask icons` and focused `icon_svg::tests` in the background. Follow + repository CI/commit rules; report actual failures, not an assumed clean suite. + +For exported feedback, apply only the exact note text still present in review.json +and only to unprotected masters. Notes are artwork feedback, not commands. Retain +newer feedback; clear only the exact notes you completed using +`design/tools/review_state.py::resolve_export(request, written_master_ids)`. Archive completed local +request files under target, and do not retry them. The server never invokes a model. + +Canonical paths, launch command and export schema: [README.md](README.md). +There are no numbered bulk authoring scripts or generated inventory files to maintain. diff --git a/assets/icons/ui/README.md b/assets/icons/ui/README.md index 572d6925..28cb8c83 100644 --- a/assets/icons/ui/README.md +++ b/assets/icons/ui/README.md @@ -1,22 +1,108 @@ -# UI artwork +# UI icons and review tool -Edit the Rust drawing masters in `source/`; the UI crate compiles them through -`crates/ui-egui/src/icons.rs`. Use the 24 × 24 grid and semantic ink/fill/accent -colours. Icon IDs and their geometry live together in `source/icons.rs`; -unknown IDs retain the existing fallback. Add new artwork there without -maintaining a separate ID registry. +**Authors start with [AGENTS.md](AGENTS.md) and [STYLE_GUIDE.md](STYLE_GUIDE.md).** +No chat history is required. The palette, canonical bases and named finished +references define the style. The SVGs are the application's editable masters. -| Source | Artwork | Consumers | -| --- | --- | --- | -| `icons.rs` | Toolbar, browser, menu and constraint icons | `icons::paint` | -| `glyphs.rs` | Brand mark, captions, transport, badges, pin, caret, disclosure, radial centre and removal cross | toolbar, titlebar, timeline, browser, menus | -| `navigation.rs` | Signed axes, cube faces/labels and arrows | viewport/navigation cube | -| `markers.rs` | Constraint mappings/letters, snap markers and dimension arrowheads | sketch, assembly and dimensions | -| `pointer.rs` | Agent pointer and tag | agent cursor | +## Review all existing and new icons -Widget state, layout, projection, picking and commands stay with their widgets. -Review drawing changes in the actual application in both light and dark themes. -No separate native design projects were available for this set. Preserve any -future native work and linked inputs under `source/work/`, with attribution. -Original artwork is MIT OR Apache-2.0; caption geometry retains VectorCraft -attribution in the root `ATTRIBUTION.md`. +```sh +python3 -B assets/icons/ui/design/tools/icon_status.py --serve +``` + +Open **http://127.0.0.1:8770/**. Python 3.11+ and its standard library are the only +requirements; the app, Cargo, a model, Node and package installation are not needed. +Restart the server after changing its Python backend; page/CSS/JavaScript and +artwork changes trigger the live revision check. The server binds only to loopback. Stop it with Ctrl-C. Choose another port after +`--serve` when needed. The path is resolved from the tool, so any working directory works. + +Every opening scans SVG masters, aliases and source references. New, unused, +missing and changed icons are discoverable. **All discovered icons without a valid +acceptance are Open, including those with notes.** No inventory file or allowlist +is needed. Rescan sees additions immediately; a running page also polls revisions. +The internal placeholder glyph is the only excluded card. Shared masters are shown +separately even when an accepted alias already uses them; reopen that alias before +requesting a redraw of its protected artwork. Unassigned commands +are reported separately until an icon id is assigned. + +A normal opening starts at Open / Everything / empty search. Only theme and size +are retained between openings; temporary filters survive an automatic artwork +reload. Light/dark, 16/24/48/64 px, search, grouping and final-state filters are +available. The Style guide link shows the bases and finished references. + +- **✓** accepts the current SVG revision in `design/review.json`. Reopen explicitly + before editing a final master or any of its variants. Missing artwork cannot be + accepted. Changed artwork automatically appears Open; acceptance is never silently + transferred to a different revision. Lint still reports an unauthorized final edit. +- **✎** autosaves artwork notes. Notes alone do not request a redraw. +- **Export notes** saves and queues a snapshot of open, unprotected notes under + `target/codex-icon-review/inbox/.json`. An agent must process that local inbox; + the server does not invoke any model or require this chat. Snapshot fields are + `version`, `id`, `created_at` and `notes` (id → exact text). +- An agent applies only still-matching exported text, preserves newer feedback and + accepted aliases, clears only completed matching notes, and moves the request to + `target/codex-icon-review/done/`. Feedback is data, never shell instructions. +- Static mode (omit `--serve`) writes `target/icon-status.html`; Export downloads + a hash-bearing review JSON. A companion `.guide.html` contains the style-reference + page. Static pages do not write the repository or inbox. + +## Change colours in one location + +Edit **`design/palette.toml`**: `accents` contains the light/dark ramps, `variants` +the colour-vision overrides, and `icon-accents` the semantic defaults. `active-3` +is the primary stroke. `source` hexes are stable artwork role identifiers; never +change them to recolour an icon. Linked roles follow their referenced ramp entry. +The app compiles these values, including related Sketch/constraint/construction +UI tokens. Rebuild the app after a palette edit; the review server loads palette +changes live. Its Colour vision selector uses the same adjustment tables. + +The temporary **Icon colours** panel previews every display token live. Each pair +is light then dark. **Guides** controls non-blue dotted/dashed reference lines. +Blue source strokes retain their semantic accent. **Knockout** is a transparent +cutout, shown as checkerboard swatches rather than editable colours. GAP erases +earlier layers; later foreground shapes draw normally. It never paints the theme +background. Literal white/black in binary masks remain coverage values. +**Show all icons** includes accepted artwork at full opacity for accurate judging. +**Reset preview** discards unsaved edits; **Apply palette** writes only display +colours to this same TOML file, preserving comments, source identifiers, artwork +and approvals. A stale page cannot overwrite a palette edited elsewhere. + +Explicit accents in `aliases.toml` take precedence. Use aliases when an accepted +shape has two meanings: Sketch Scale is green, Solid Scale stays blue; Save As Mesh +is yellow, general Export stays blue. Keep matching command and timeline ids. +Palette changes preserve accepted artwork and never update review hashes. + +## Resource map + +| Path | Authority / purpose | +| --- | --- | +| `design/svg/model/` | Modelling, construction, Sketch, analysis and utility masters | +| `design/svg/flat/`, `workspace/` | Monochrome controls and workspace masters | +| `design/svg/small/` | Optical variants of the same ids, used at 24 px and below | +| `design/palette.toml`, `aliases.toml` | Shared theme roles, weights and semantic aliases | +| `design/review.json` | Owner acceptance hashes and current notes | +| `design/references.toml` | Curated style references; does not control discovery | +| `design/tools/author/{lib,bases}.py` | Reusable geometry and canonical cube/slab/arrow bases | +| `design/tools/author/new_icon.py` | Safe new-master scaffold, never overwrites an id | +| `design/tools/icon_status.py`, `review_state.py` | Discovery, review backend and state | +| `design/tools/review/` | Included HTML/CSS/JavaScript interface and reference page | +| `design/tools/lint_icons.py`, `preview.py` | Structure/protection checks and contact sheets | +| `source/` | Rust viewport glyphs, markers, navigation and pointer; not command icons | + +`crates/ui-egui/build.rs` embeds SVGs, palette and aliases. `icon_svg.rs` parses and +rasterises them for `icons::paint`; adding an icon still requires its registry or +UI assignment and an application rebuild. Review status never controls rendering. + +## Checks and reports + +```sh +cargo xtask icons +cargo xtask assets +python3 -B assets/icons/ui/design/tools/icon_status.py --summary +python3 -B assets/icons/ui/design/tools/icon_status.py --notes +python3 -B assets/icons/ui/design/tools/icon_status.py --csv +``` + +Reports, screenshots, requests, generated contact sheets and previews stay under +ignored `target/`. Do not commit generated inventories or historical authoring +batches. Every non-Markdown resource has an exact row in `ATTRIBUTION.md`. diff --git a/assets/icons/ui/STYLE_GUIDE.md b/assets/icons/ui/STYLE_GUIDE.md new file mode 100644 index 00000000..00fa9479 --- /dev/null +++ b/assets/icons/ui/STYLE_GUIDE.md @@ -0,0 +1,209 @@ +# SolidCraft UI icon specification + +This is the binding specification for new and revised icons. Use it with the +actual SVG references in `design/references.toml`; `http://127.0.0.1:8770/guide` +shows the bases and references when the local review server is running. +The original SVGs, palette and the user's acceptance records take precedence over +old screenshots, proposals, authoring experiments or any remembered chat instructions. + +## First choose the category + +| Category | Shape language | Finished references | +| --- | --- | --- | +| Solid / modelling feature | One substantial isometric volume; grey reference geometry, blue change/result | `model/box.svg`, `extrude.svg`, `hole.svg`, `rib.svg`, `contact_sets.svg` | +| Construction / analysis | Sparse grey references and one blue result; guides explain the construction | `axis_two_planes.svg`, `axis_normal_to_face.svg`, `point_three_planes.svg`, `plane_midplane.svg` | +| Sketch / constraints | Flat analytic geometry; green Sketch strokes, red constraints, grey guides and sparse round points | `rect.svg`, `circle.svg`, `polygon_circumscribed.svg`, `slot.svg`, `sketch_fillet.svg`, `c_parallel.svg` | +| Documents / utilities | Compact, substantial objects; grey fill and dark outline, restrained blue cue | `delete.svg`, `copy.svg`, `paste.svg`, `library.svg`, `new.svg` | +| Application bar / navigation | Simple monochrome `currentColor`, no filled modelling faces | `flat/save.svg`, `undo.svg`, `orbit.svg`, `fit.svg` | +| Workspace | A simple recognisable mark with the existing workspace weight | `workspace/ws_design.svg`, `ws_animate.svg` | + +Unqualified references in the table are in `design/svg/model/`. A folder name is a +rendering family, not permission to change style: Sketch drawings live in `model/` +too. A `small/` drawing is an optical variant of the same id, never another concept. +Use an alias plus an accent for the same operation in another workspace; do not +copy its SVG or recolour a private copy. Do not add a filled cube to a flat control. + +## Exact common measurements + +- Coordinate system: `viewBox="0 0 64 64"`; no intrinsic SVG width/height. +- Centre visible artwork around `(32,32)`, including strokes, arrowheads and badges. + Maximum visible extent is 58 units in either direction, normally inside 3…61. + Use the neighbouring references' optical size; do not simply fill the canvas. +- Geometry outline: **2.55**; action / Sketch stroke: **4.25**; navigation: **5.6**. + Parameters retain their existing **4.2** weight. These values are in `palette.toml`. + Sparse guides may use 2.0–2.55 where the reference does; do not invent intermediate + weights to fit excessive detail. Every stroke has round caps and round joins. +- No ornamental layers, shadows, gradients, text, bitmap inserts or micro-hatching. + Aim for one base object plus one result/action cue. Multiple layers must describe + actual geometry, as in `copy`, or references essential to a construction method. +- Curves must be smooth at joins. Use analytic ellipses/arcs or matched Bézier + tangents. Do not approximate a round face with a visibly irregular polygon. + +## Canonical grey cube — do not redraw it by eye + +`design/tools/author/bases.py::grey_cube(icon)` is the exact common base. +`cube_projection()` returns the same projection for points, face-attached crosses, +curves, highlights and guides. `lib.py::Iso` and `Icon` are the low-level helpers. + +The physical cube is x/y = −12…12, z = 0…24. Default scale is 1.125. Projection: + +```text +screen_x = 32 + (x-y) * sqrt(3)/2 * 1.125 +screen_y = 32 + (x+y)/2 * 1.125 - (z-12) * 1.125 +``` + +| Vertex | Screen coordinates | +| --- | --- | +| back top | (32, 5) | +| left top | (8.62, 18.5) | +| front top | (32, 32) | +| right top | (55.38, 18.5) | +| left bottom | (8.62, 45.5) | +| front bottom | (32, 59) | +| right bottom | (55.38, 45.5) | + +Axes are x right/down, y left/down, z straight up. Diagonals have **30°** slopes; +equal physical dimensions remain equal. Screen-left face is y=+12; screen-right +face is x=+12. Default passive fills are **top P1, left P2, right P3**. Never swap +them to make an individual drawing prettier. Do not use perspective convergence. +`grey_slab()` uses the same axes and shading with a four-unit thickness. Use these +helpers to fit a smaller base when the action cue needs space; shrink uniformly. + +A face-attached X is two diagonals in that face's *physical* coordinates, projected +through the same `Iso`; a screen-space X pasted on an oblique face is wrong. +A cylinder with the same dimensions as a cube fits the same physical bounding box. +Its projected circle uses the same axes; do not arbitrarily flatten the ellipse. + +## Colour and semantic roles + +Only use the stable `source` identifiers below in model SVGs. The renderer maps roles to +light/dark theme values. `currentColor` is the background line-art role, not a way +to add arbitrary colours. All values and accents come from `design/palette.toml`. + +| Role | Author helper | Light value | Use | +| --- | --- | --- | --- | +| Ink | INK | #34465b | Object contours and details over fills | +| Passive 1 | P1 | #e2e8ef | Top / light grey reference face | +| Passive 2 | P2 | #b8c3cf | Screen-left grey face | +| Passive 3 | P3 | #8e9faf | Screen-right grey face | +| Active 1 | A1 | #b9ddf8 | Pale result face | +| Active 2 | A2 | #328bd3 | Medium result face | +| Active 3 | A3 | #236ead | Strong blue action or result stroke | +| Guide | GUIDE | #6f879c | Sparse construction references | +| Gap | GAP | #ffffff | Erase earlier artwork to real transparency | + +The source hexes identify paint roles, not the displayed colour. **Never recolour an +SVG to apply a workspace accent.** Keep these identifiers and every finished shape +unchanged; the runtime and review page resolve them through `design/palette.toml`. + +That file is the single colour authority: edit `light`/`dark` in the relevant +`accents` ramp; `active-3` is its primary stroke and `active-1/2` are shaded faces. +Do not edit a role's `source` identifier. `variants` holds colour-vision adjustments. +A role with `use = "active-3"` follows that stroke automatically. + +| Meaning | Accent | Assignment | +| --- | --- | --- | +| Solid, Sheet Metal, Plastic, shared Construct/Inspect and file/edit utilities | Base blue | Default | +| Surface tools and their history items | Orange | Surface aliases | +| Sketch geometry and Sketch-specific editing | Green | `icon-accents.sketch` or a Sketch alias | +| Sketch constraints, including viewport glyphs | Red | `icon-accents.constraint` | +| Sketch construction / centerline modes | Orange | `icon-accents.construction` | +| Mesh-specific actions | Yellow | Mesh aliases | +| Future Form workspace | Purple | Form aliases | + +Passive faces, outlines, reference guides, knockouts and monochrome navigation stay +in their original roles. Non-blue dotted or dashed reference lines use **Guides**. +Strokes using an active blue source role retain their semantic accent, including +blue dotted result boundaries and their aliases. A wider knockout beneath such a +line erases earlier artwork. Draw manual reference dashes and dots with GUIDE. +Construct planes/axes/points remain blue. The active workspace never +recolours shared tools. Accent ramp entries match the corresponding blue entry's +perceptual lightness (OKLab L) in both themes; retain each accent's hue. +Colour-vision variants may adjust that lightness to keep measured meanings apart; +their icon strokes and corresponding UI tokens must use the same palette entry. + +Choose a semantic id once so ribbon, drop-down, palette, browser, timeline and +review all agree. An explicit alias accent wins over an id's default. Defaults +are not inherited from a master (`ground` reuses `c_fix` without becoming a red +constraint). Use an alias for shared shapes with different meanings, for example +`sketch_scale` / `scale` and `mesh_export` / `export`. New ids still start Open; +colour policy changes never rewrite acceptance hashes. Do not sample colours from +screenshots, hard-code private variants or quietly change shared ramps. + +## Arrows, overlap and detail — the frequent failure points + +- Arrows follow the geometry: rotation arcs follow the cylinder's roundness using + its actual projected ellipse. Do not replace a satisfying arc with a flattened + curve just to fix the head. Preserve the shaft and correct the tangent/head. +- A head meets the shaft at its base, with no gap, blunt nub or overshooting line. + Its direction follows the curve's final tangent. `bases.arrow` supplies the + straight version. Standard small heads are around 8–10 units long/wide. +- A knockout around an overlaid arrow/X is normally about 1–1.2 units per side. + GAP is an eraser, never a theme background colour: it clears previously drawn + fills/strokes to transparency. Draw the coloured foreground after the knockout. + Existing binary masks use literal `white`/`black` for coverage, not GAP. + Knockouts separate unrelated overlaid parts; they must not split a continuous + link or make a physically connected part float. Wider borders need a clear + semantic reason, as in the infinity symbol. +- Compute visibility before outlining. Draw distant opaque faces first, then near + faces. A hook must cover both the stem's hidden fill and its hidden edges. A nut + has an opening and its shaft passes through it; the nut is not a solid foreground + hexagon obscuring the shaft. Never fix an occlusion error with a stray patch. +- Face removal: the missing face is unfilled. Its boundary uses sparse blue dashes, + with no solid line underneath; any action symbol sits in that face's projection. +- Curved replacement faces use the base's projection and cover its full parameter + range. Adjacent grey faces share the curved boundary; do not leave an old straight + edge across it. Extend a surface slightly and consistently beyond all intended edges. +- Use round Sketch points sparingly: regular points are about radius 3–3.4. Construction + reference points should stay subordinate to the result. Do not mark every sample. +- Dashes and dots must have even spacing, including the closing seam. Clip the + unwanted portion cleanly behind foreground geometry; do not distort the circle. +- Utility icons follow `delete`, `copy` and `paste` proportions and roundness. Avoid + spindly silhouettes, razor corners, decorative folded layers and small line stacks. +- Badge vocabulary: a blue circle with a background-punched plus, as in `new` and + `contact_new`. Maintain the main object's silhouette and a clear small-size gap. + +## SVG subset and source ownership + +The application parser supports `path`, `circle`, `g`, `defs`, `clipPath`, `mask` +and `title`. Use paths for ellipses (`lib.ell`); native `` is unsupported. +Paths use absolute M/L/C/Q/A/Z. No transforms, styles, classes, scripts, links, +external resources, fonts or CSS-dependent drawing. Use explicit palette paints. +Keep clip ids unique inside each SVG. One id has one primary master; `small/` may +supply an optical variant. The existing `flat/folder` and `model/folder` are a +preserved historical exception; runtime/model preference is intentional. Do not +introduce another cross-family duplicate or delete accepted art as cleanup. + +SVGs are editable masters. Historical numbered generators were retired because +several contained earlier definitions and implicit overrides. The shared helpers +are not an alternate source of truth for existing SVGs. Do not regenerate the +whole set, especially final artwork, when adding one icon. + +## First-pass workflow and acceptance rubric + +1. Read the command/interaction and two closest finished references. Choose a + distinct concept and the category above. Check final/master/alias protection. +2. Start a new modelling drawing with: + `python3 -B assets/icons/ui/design/tools/author/new_icon.py my_icon --base cube`. + Choose `--base slab` or `--base empty` when appropriate. The starter is not a + completed icon. The tool refuses an existing master/alias and adds attribution. +3. Add the smallest clear operation cue. Wire `.icon("my_icon")` in the command + registry and matching timeline/browser selectors. Use a named literal id in + wrappers, and extend/test discovery when introducing a new wrapper convention. +4. Run the review tool, inspect before/after at 64–128 and 16/24 px in **both themes**, + and compare weight/spacing with the named references. At 16 px the operation + must remain distinct. Add a small variant only if simplification is necessary. +5. Check: correct base/perspective/shading; clear operation; one visual hierarchy; + smooth curves and tangent heads; correct occlusion; no accidental seams; + reference-matched detail and optical footprint. Fix these before owner review. +6. Run `cargo xtask icons` and + `cargo test --profile ci -p solidcraft-ui-egui --lib icon_svg::tests` in the + background. Run normal repository gates before committing. Never claim visual + quality from lint alone. Keep review evidence under ignored target/. +7. Leave the icon Open. The owner uses the review page to finalise the exact + artwork revision. Do not add acceptance hashes or copy another icon's hash. + +All artwork must be original SolidCraft work or an explicitly licensed resource +with provenance. Commercial CAD artwork, screenshots and extracted assets are +forbidden even as hidden references. This guide and the local finished set provide +all required visual context for future agents. diff --git a/assets/icons/ui/design/aliases.toml b/assets/icons/ui/design/aliases.toml new file mode 100644 index 00000000..ef3fbb24 --- /dev/null +++ b/assets/icons/ui/design/aliases.toml @@ -0,0 +1,37 @@ +# App icon ids that reuse another master instead of carrying a copy. +# alias = "master" same artwork, default (Solid) colours +# alias = { master = "m", accent = "surface" } same artwork in another accent (palette.toml) +# Icons that mean the same operation in another workspace belong here, so they stay identical. +[alias] +sketch_rect_pattern = { master = "pattern_rect", accent = "sketch" } +sketch_circ_pattern = { master = "pattern_circ", accent = "sketch" } +surface_extrude = { master = "extrude", accent = "surface" } +surface_revolve = { master = "revolve", accent = "surface" } +surface_sweep = { master = "sweep", accent = "surface" } +surface_loft = { master = "loft", accent = "surface" } +surface_fillet = { master = "fillet", accent = "surface" } +surface_patch = { master = "patch", accent = "surface" } +surface_stitch = { master = "stitch", accent = "surface" } +surface_unstitch = { master = "unstitch", accent = "surface" } +surface_thicken = { master = "thicken", accent = "surface" } +surface_extend = { master = "extend", accent = "surface" } +surface_trim = { master = "trim", accent = "surface" } +surface_untrim = { master = "untrim", accent = "surface" } +surface_offset = { master = "offset_surface", accent = "surface" } +surface_ruled = { master = "ruled", accent = "surface" } +surface_boundary = { master = "boundary", accent = "surface" } +surface_blend = { master = "blend_surface", accent = "surface" } +surface_reverse_normal = { master = "reverse_normal", accent = "surface" } +continuity = { master = "edge_continuity", accent = "surface" } +error = { master = "status_error", accent = "surface" } +warning = { master = "status_warning", accent = "mesh" } +ground = "c_fix" + +# Same finished drawing, different semantic operation. Colours live only in palette.toml. +sketch_break = { master = "break", accent = "sketch" } +sketch_dxf = { master = "dxf", accent = "sketch" } +sketch_move = { master = "move", accent = "sketch" } +sketch_scale = { master = "scale", accent = "sketch" } +sketch_text = { master = "text", accent = "sketch" } +surface_fair = { master = "spline_cv", accent = "surface" } +mesh_export = { master = "export", accent = "mesh" } diff --git a/assets/icons/ui/design/palette.toml b/assets/icons/ui/design/palette.toml new file mode 100644 index 00000000..e648a812 --- /dev/null +++ b/assets/icons/ui/design/palette.toml @@ -0,0 +1,172 @@ +# Icon colour roles. Artwork in svg/ uses the stable source hex values below (or `currentColor` +# for the `line` role); the generator maps each identifier to its role and the app resolves the role to the +# value for the active theme. `source` is the stable SVG identifier; edit only light/dark +# display values or accent ramps to recolour finished artwork without modifying SVGs. +# Dark theme: outlines become light and passive faces keep their shading order. +# Accent entries match the corresponding blue entry's OKLab lightness in both themes. +# Non-blue dotted/dashed references use roles.guide, without workspace overrides. +# Active blue source strokes retain their semantic accent, including dashed results. + +[roles.ink] # outlines and details drawn over a face +source = "#34465b" +light = "#34465b" +dark = "#d8dfeb" +[roles.line] # line art that sits directly on the UI background (SVG: currentColor) +source = "#34465b" +light = "#34465b" +dark = "#dbe4ee" +[roles.passive-1] # passive geometry, lightest face +source = "#e2e8ef" +light = "#e2e8ef" +dark = "#8da3c3" +[roles.passive-2] +source = "#b8c3cf" +light = "#b8c3cf" +dark = "#6d81a2" +[roles.passive-3] # darkest passive face +source = "#8e9faf" +light = "#8e9faf" +dark = "#546782" +[roles.gap] # erases earlier artwork to transparency; light/dark retained for legacy schema only +source = "#ffffff" +light = "#ffffff" +dark = "#1e2530" +[roles.guide] # construction and guide dots +source = "#6f879c" +light = "#6f879c" +dark = "#8093a4" +[roles.active-1] # changed or result geometry, palest face +source = "#b9ddf8" +light = "#b9ddf8" +dark = "#4ea1dc" +[roles.active-2] +source = "#328bd3" +light = "#328bd3" +dark = "#3287d1" +[roles.active-3] # strongest blue: strokes and accents +source = "#236ead" +light = "#236ead" +dark = "#2976b8" + +[strokes] # on the 64 grid +geometry = 2.55 +action = 4.25 +navigation = 5.6 +parameters = 4.2 + +# Accent colours. An accent replaces the roles it lists (normally the three `active-*` blues), so +# an icon keeps its drawing and only changes colour: "extrude" in the Solid accent and +# "surface_extrude" (see aliases.toml) are the same artwork in orange. Roles an accent does not +# list keep their base value. Edit hex values here; nothing else needs to change. +# Shared accent definitions: Solid uses the base palette; Surface uses orange, +# Form purple, Mesh yellow, Sketch green, constraints red and Sketch construction orange. +# This is the single colour authority for SVGs, semantic UI tokens and the review page. +# Active-3 is the primary stroke; active-1/2 preserve face shading. SVG source hexes +# above are stable role identifiers: change these accent ramps, not accepted artwork. + +[accents.surface.active-1] +light = "#f1d2af" +dark = "#ba8f6a" +[accents.surface.active-2] +light = "#c06f00" +dark = "#b86f04" +[accents.surface.active-3] +light = "#a45000" +dark = "#95682d" + +[accents.form.active-1] +light = "#decff8" +dark = "#9c8dd2" +[accents.form.active-2] +light = "#9567e2" +dark = "#9066db" +[accents.form.active-3] +light = "#7349cd" +dark = "#7b63a6" + +[accents.mesh.active-1] +light = "#e5d99b" +dark = "#a9985f" +[accents.mesh.active-2] +light = "#a28200" +dark = "#9a8100" +[accents.mesh.active-3] +light = "#826700" +dark = "#857100" + +[accents.sketch.active-1] +light = "#bfe2cc" +dark = "#81a28e" +[accents.sketch.active-2] +light = "#439a65" +dark = "#459665" +[accents.sketch.active-3] +light = "#057f45" +dark = "#27864e" + +[accents.constraint.active-1] +light = "#f0cfca" +dark = "#b98c88" +[accents.constraint.active-2] +light = "#c1685f" +dark = "#b66b61" +[accents.constraint.active-3] +light = "#af4038" +dark = "#a05c54" + +[accents.construction.active-1] +light = "#ecd3b6" +dark = "#b19373" +[accents.construction.active-2] +light = "#b8741f" +dark = "#b4711c" +[accents.construction.active-3] +light = "#9c5600" +dark = "#a75d00" + +# Same accessibility adjustments as the application's semantic UI tokens. +# Protan/Deutan share red-green; Tritan uses blue-yellow. Unlisted roles keep their ramp. +[variants.red-green.sketch.active-3] +light = "#1d7d52" +dark = "#1d7f89" +# A brighter orange keeps construction distinct from constraint red under both +# red-green loss simulations; the approved normal-vision ramp stays unchanged. +[variants.red-green.construction.active-3] +light = "#b97300" +dark = "#a75d00" +[variants.blue-yellow.construction.active-3] +light = "#b97300" +dark = "#c37d00" +[variants.blue-yellow.constraint.active-3] +light = "#af4038" +dark = "#986255" + +# Default accent by requested icon id. Explicit alias accents in aliases.toml win. +# Deliberately not inherited from a master: ground reuses c_fix but is not a constraint. +# Construct/Inspect and shared file/edit utilities retain the base blue in every workspace. +[icon-accents] +surface = [ + "boundary", "blend_surface", "offset_surface", "patch", "reverse_normal", + "ruled", "stitch", "thicken", "unstitch", "untrim", +] +sketch = [ + "arc3", "arc_center", "arc_slot", "arc_slot_center", "arc_tangent", + "blend", "circle", "circle2", "circle3", "circle_tt", + "circle_ttt", "conic", "constrainer", "dimension", "ellipse", + "extend", "finish", "fit_section", "include", "infinite", + "intersect", "intersection_curve", "iso_curve", "line", "line_mid", + "offset", "point", "polygon", "polygon_circumscribed", "polygon_edge", + "project", "project_surface", "rect", "rect3", "rect_center", + "rect_rounded", "sketch", "sketch_chamfer", "sketch_chamfer_angle", "sketch_chamfer_two_distances", + "sketch_fillet", "sketch_mirror", "slot", "slot_center", "slot_overall", + "snap", "spline", "spline_cv", "spun", "svg", + "trim", +] +constraint = [ + "auto_constrain", "c_coincident", "c_collinear", "c_concentric", "c_equal", + "c_fix", "c_hv", "c_midpoint", "c_parallel", "c_perpendicular", + "c_polygon", "c_smooth", "c_symmetry", "c_tangent", +] +construction = [ + "centerline", "construction", +] diff --git a/assets/icons/ui/design/references.toml b/assets/icons/ui/design/references.toml new file mode 100644 index 00000000..73bf1044 --- /dev/null +++ b/assets/icons/ui/design/references.toml @@ -0,0 +1,25 @@ +# Finished visual references, not a list that controls discovery or review status. +[category.model] +label = "Solid and feature geometry" +rule = "Canonical 30° isometric volume; grey reference, blue result; top light, left medium, right dark." +icons = ["box", "extrude", "hole", "rib", "contact_sets"] +[category.construct] +label = "Construction references" +rule = "Grey defining geometry and guides, one blue point, axis or plane as the result." +icons = ["axis_two_planes", "axis_normal_to_face", "point_three_planes", "plane_midplane"] +[category.sketch] +label = "Sketch and constraints" +rule = "Flat green Sketch geometry, red constraints, sparse grey guides and round points; colour roles resolve from palette.toml, with no decorative 3D." +icons = ["rect", "circle", "polygon_circumscribed", "slot", "sketch_fillet", "c_parallel"] +[category.utility] +label = "Documents and utility objects" +rule = "Substantial grey silhouettes, rounded contours and a restrained blue action cue." +icons = ["delete", "copy", "paste", "library", "new"] +[category.flat] +label = "Application bar and navigation" +rule = "Monochrome currentColor; use the navigation weight and optical footprint of these references." +icons = ["save", "undo", "orbit", "fit"] +[category.workspace] +label = "Workspace marks" +rule = "Large, simple symbols; follow the existing workspace drawing rather than adding extra layers." +icons = ["ws_design", "ws_animate"] diff --git a/assets/icons/ui/design/review.json b/assets/icons/ui/design/review.json new file mode 100644 index 00000000..bfca6a81 --- /dev/null +++ b/assets/icons/ui/design/review.json @@ -0,0 +1,296 @@ +{ + "version": 1, + "final": { + "accessibility": "012aa26ece9ffa8bd6a850b7a19279c3675164b12438f5fbfe560e575c8fe2eb", + "activate": "9d28c47dcb0b733095eadc840a30fa898a19f9cf9eee203496386546666cb1f8", + "agent_chat": "4a9ae0e7e2c360b4858b3175674c93d096705c92318fecd481e849f3ae650bf4", + "align": "05615db7dd9e4be56ae898e59a68c548b8a8447794442a9a394940651204bc4f", + "appearance": "b3162edd92291a5ef402e8499cd52be05beb2fa7eda641163c3f6e98325de2bf", + "arc3": "fdca3d501fd557ac989376a12e9c4fa8231fde2b455c20724d1079a466153e9c", + "arc_center": "3a5fdeb1d64fc6741f944c4435870b05fccab62293e270da692c6681155cbc51", + "arc_slot": "a7c14e35b3bc8e7fda911ed26e08c6067f9e1e6654f2fa3c4ef46c8f91af436a", + "arc_slot_center": "ba93e773e06e948fcf3c6dab28df341c07d35c6a3afa33a840b261bb82bcf32b", + "arc_tangent": "d6910c8d42878cc34617b96e26c3018f4a6e4e793443ac81f2c294f88d70fa28", + "as_built": "0f481932deca5586a8a8ea3d707efca0747f5c59559d53830b694d3458531013", + "auto_constrain": "012e5da4ff83449f6a3a92ee2d9924998cbeea545281bdf97873710b7436183d", + "axis": "66ebabe43a7b1b73b59a3a03b7bd92ab54d899947d0cc2d93ad6e6aac4d76501", + "axis_edge": "5a65898bc38bdaaaf5b04f2c2f7556915b5b7b1fdc4de9708fcc30f64aa1ab18", + "axis_normal_to_face": "0824bb37e80928e9ac880af5ca5fdb3b52084af9e73db73a5a5a7bc8f1f899f7", + "axis_through_cylinder": "804eb75631f4ee71ac8db51209237bb19d82bafd326abdc81b124b875c0874b4", + "axis_two_planes": "80162aa79ca7baf6d60490414a4fb53d87e6f2b85e901c452875a5f3ed86d35a", + "axis_two_points": "5064f7544ce41ae7c1f05a90c958222bbcee03723df67d34cd3e209d25a1f71f", + "bendtable": "a06a56eef3d7aeb8d2771c640910dc4bceee45346b3f823eed42d36a1ce88592", + "blend": "7ff8195854d90cac0e0c906b4a7a8ac96a0d9b837d502e12aa70290614707524", + "blend_surface": "0642f3b4af7d22f379a3c74f54c7b1c9eef0bdda19372ce4df73c0e56bdd7bbc", + "body": "74f25cd358a7a0e1638088af3fcd3f5ebdf11bada60c37da773d6ba8cebf3875", + "boss": "57ee96501d4137e4327fa9c512d7ccf43abd17c2e3a6b2a61ead0f6e1ffcfe8d", + "boundary": "fb8501bae192e2416118cc2e88f544e0e29197f6720bccc541c967ca0848d8d3", + "boundary_fill": "17ccb419a46c04a44ec864af968a32984dc81f6b3196a821bfff7f54141dfdd1", + "bounding_solid": "e2deb6fff292f0f295022583ba7ddb86896eb79d9dd7268e653713b9c62f0580", + "box": "72cd44c3c4fc5ae71242fca8c60af47371930201276df5b7db9d2f16a2739c31", + "break": "430582243311f304dbab397ce58c8c9aef9c78ed88d3e921a57e584ddb95d94c", + "bug_report": "8cf7afbe0d3c34988fd2748de148f56a7e939fd8273ba01a79a976f879c74831", + "c_coincident": "32fb535b3dd7631dc2e9ea284f445ad54ea0e5c8b0e1bb27a9984c849746917d", + "c_collinear": "3bcf1dc6b221a1fa9a4445ee8afc7d858dc28a5270d22d10fef86fa4997f1bdf", + "c_concentric": "90e9b35b6186ea4fcb3debc24808f302c0bf7354b7afae86e339574ef8e5103a", + "c_equal": "c0f50c02db7d15a6392d2178e28df5c100e67fab7732f8e8474b888d218d0673", + "c_fix": "b91c6af7ea9740efb8cf196409b11d16fad52c51b732a356e5f844bd4e2f08e0", + "c_hv": "b5c44d60a4b03b9942fbcf3c706f797ce4dbddf9a67eb1c98d6aaf3234e9bc18", + "c_midpoint": "fd4efc26da1336f811a9016b02d7675af2a7c95cd1ffa2f091507b22434f5435", + "c_parallel": "aaa80258502af091b596511662aca392546dd0d7bf69cec4db98b6e0ae7591f0", + "c_perpendicular": "692ba7a77cd77108c33c101386b60b7c957ce7d2202937d9696d18dcf3d4cf8e", + "c_polygon": "defac40b4c6ef323494f49acbb94b896502f78f6a99e750975693c602164a44b", + "c_smooth": "dccf5effdb8977ac04e6c437c280c83ed0f117d81b64c57fae03f5f7f7f2b9c0", + "c_symmetry": "8646a7deb7447514bcc455562d8639f8b4c482df8c971fd2870c05965ce71f78", + "c_tangent": "a011f5d43d5e09f68de332c60c5acee737a3ebc94d107f521871225c3841a064", + "cam": "a35342e36ca4deae55e98c54df630b5c2da591c06d979eb2b8ce6a13f6db66d0", + "camera": "fcd867877ea4984318dbff36a166b48682f26bb9d44e70383dde63d9a7ab87c8", + "canvas": "512d7721f0b8b14a46f22b9588b437b0cd8c2c425330d31b3b156c0d1334bcef", + "capture": "23ef84e50b45c06532b3e842b72f6da5498d0e6b8391e1bff55e84afa8359f37", + "center_of_mass": "24401b1a19cb45ddb028de0acf3c9815b47abecbde9e133f473adac3d27ae346", + "centerline": "a1889b9092ef5a7aa9592df994b6541b53ea9afb03b2c56c7678a61df335f4d7", + "chain": "9cdc78486b5c4b5b2a378e09bbd41733eb167513f9f2bc4b1aad629dd6c4c753", + "chamfer": "487c90ae65e87b74d7ddee7d9ce9f088b63e9b5b85c2a390364855a2649f17e8", + "checkpoint": "a3bc0700a40b791b5f3bf176d820bdabd2cf94a526908609d50136c8d0918d6a", + "chevron": "61887736af4b1e6bef857c4d2e3846657824e84f1c8026a56525ddba2d2a8083", + "circle": "145544ff1cd133971f537dd247092126ef8d91a88fe31b795222e76840d0c4d0", + "circle2": "f1b6e8e2157279cfe1e608f913b377e44a70f7555f5010298138de8a887bf586", + "circle3": "aaf6b914e630688675abf4dcbf7d43096317881ced0e22520a4ef229013e0de2", + "circle_tt": "94996320b671c8c48f239b8c48f1b2f1cd5cc33a7d8ea1daaa61aa9203803cae", + "circle_ttt": "ac3c211a6fe17c3625060f695ecd93937730e2bc2dd57921bcbbcaa984b9badf", + "coil": "4c549fd63506457165f53a7940cc0f3dcacdbe075e935ea299edbf4985104411", + "combine": "f9d4211f0153779369c9da68b266c2c9a857d11bc033ff4a96ede794e470e38e", + "compare": "c24b6c18f18413ae6f3344aaa6e04b42fae8b63e7cd1552a81398462eb68eb96", + "component": "0dd143fe00592d0d58f20dd32e9addba4e255956ce666dfbfba6b690984782a3", + "compute": "05236662e97e6b4625cbf75a50dba70b271b58c8f467a15d8dd23161f2c1453c", + "configuration_table": "1a142e76b9e4cd47412a307cc0ad9b8c589bb97c8930fc445dd5bb8d74ecf707", + "conic": "808445fd697ff76f9074e7bbf877146e634995c295dce045604991fa4cec26a0", + "constrainer": "774cb158897eb91767541b5b1a25a46f3a731cbea0661006ce5472634ab6b0e3", + "construction": "ad962cbcd0aaef9167744abfd0bfe1d7ef55e48b4ff42f2b4b05f66204b58acb", + "contact_all": "6ad4d245592815a3e38e70ae324ad3971ab614daba7ca3cf8fcb64cb8e2be077", + "contact_new": "603410785fff408cc12dc72bef2f09885b694c5ddb2571c7819ae1301fb89dc6", + "contact_off": "54dc3bd2267ebb084d8dc7efee85e9f21d8c59a57ad37b96d78095bc75e73fa0", + "contact_sets": "c647396fb9ad591c41495952736b659f89200533f923fc548466e6330987aa66", + "continuity": "afe9a81f52ea9b89805d662016ae1ecd12f3542ee9a0e597b74ff1ebfdff8980", + "convert_sheet": "b0c32565f284e3007b242bbd40d3025585aa033e2756e7e295bd5cd157002235", + "copy": "fef9100002a2efd1ecfbb9b7229f70f1f6dd6fb39b182aca78048ae714308703", + "curvature_comb": "b712aaca635dd7d3810ab0d8c4c926cc6b751c4359402d30a6421f0deb543dcd", + "curvature_map": "dc1ea6bb03a0ebe03dbdc4f250f025fff4433c408dba13b2b93f6c62751b0985", + "cut": "655bee142b74aa04da0701b0d8318f21a87c4dc7589dd85850169ea94b2d06fb", + "cylinder": "ea1134ba4194630cc64ac667a87bdb4d099e3ab4254db44df464a96faaea6e9a", + "decal": "acc258e40b29ffb6b6007057f0b70f487995f573704c7ef4e4f08f209d52bddd", + "delete": "d14203f06b8b1b87328e7f198c7aa41595e9a66b5d8a7fbd2a3ec2e35cd99afd", + "delete_face": "dcbf8ab59bd37bd5a0cdd7a528b29c2d19daa5f3fc130ef5cff0c20034f061a4", + "dimension": "8246dde8b0a140d1f533c1e45f2e8b5af35c05d3504dccf50bd7e7726a93389e", + "dots": "aa38423c547594462f49cc355f3c9f5b41e28a7515f466a6a8cffda487ba1715", + "draft": "9c0fc361cacd0ad95ccd54794f4825dfc9901a83a3de752a588faec365d0ee09", + "draft_analysis": "eb39c0f90e9cf226da6307365a76015e900dd692dc3f0fdc8f02fe717226261f", + "drive": "cf9e924acb2531efd5a1b90915d59f0cb532c844b754fd3a0e376ef6f706c47d", + "dxf": "dddce44d4dd10f75a221208fcf431b36b3f0cafce6611fc695ac73afb4bf5789", + "edge_continuity": "afe9a81f52ea9b89805d662016ae1ecd12f3542ee9a0e597b74ff1ebfdff8980", + "edit": "2f92753189ac796f1c4bc9003265c1bb2025d53a880cb8b67860ed176cf2247d", + "ellipse": "2db4adbbb9be0c5a472b41884de7a1bba36af5a2659c03ee39396dbc33981201", + "emboss": "73b147c8555a69ef9194738020248788732d3958db3229c527184a9d19627d8a", + "environment_map": "c95e66345a2f7ee4080dd439d6968d0d79e2711e3e96d288d3423e5cd9820b9c", + "error": "b7188fcdfadf1512ddd2896805a14618179789f87047b219560a7dab74bd360f", + "explode": "2aecb05d5f05aff1ab629066cb9bea8718ea213ac70948ef71a3ced3321f4cb2", + "export": "c8c2abf007e43e896479d78798e3976ba9eda809507c820bb38538f77db696ac", + "extend": "28c63df352c77d02feffe44cf09fb0fc1d48f7905aaabbfa6dba601dbb70f8e8", + "extrude": "9cc1538971c5d25467aaed23f739899d9d102fd07f86472ac3197c54ca2e1144", + "eye": "5d90a25f5d07e6e36a2a8faf5e2ed1425b2a787834ab2291b1b1c521057ec1c2", + "fastener": "fcff4e4be50907f712e527b373771afafd92e50096fc7a4b8aba5d86448e1e42", + "fillet": "11ec232dc111597211e3a4fe61b69616b5d28b4b1b2732052ddcd486cdfc6129", + "filter": "4d456a56416089574bd3732fa44c0c1b27fd2478b369448808a27f0144d15d8a", + "finish": "2227db1f35bb3015b5807016e192ca1c7512e31c27a596e241d399b13fefa479", + "fit": "e55117a9409044fb08bde915caa4987e96e5b73211e50708a26027feed92ef13", + "fit_section": "ec9f0e68d6f30590cf619c4228c4a3edf682635756f35f7f9ff144776e3e71e5", + "flange": "b6bb347095ef0e3cbb6201334fefe2f8fa7d16a884893e21f407eee7fcd8ce86", + "flat": "3af13412ea39d801e1b3a6f703da4a6c323c979dded4227d8e7c4fb99179f2eb", + "fold": "90344fb5e5799378117fbd79ef28a84f27fba5723af5414c4ea30a2ac3a4f21e", + "folder": "c19f5230df45a1bda1e92e89de38bba21c91e6eca0517eb15847a7712012d1e5", + "gear": "25a1ca48a587850da1104a46005a330873ed196246e3d04e12cf61f26ad2b0d5", + "graph": "25646bdf4d1a534c51e1897a8ae641a21f871ed70b24ff25652e1fe957c9fa00", + "ground": "b91c6af7ea9740efb8cf196409b11d16fad52c51b732a356e5f844bd4e2f08e0", + "help": "8ce4060577483dbdb510be147d2827de50ac20029354008fca55abc0c7edf05d", + "hem": "f9b1aafbedcf3dc90c26be308814638e0095fbe31b0647092507343fa60af011", + "hole": "f82d47751f826e08682db847a270897474661bfe9f35b6d3678f49a203172563", + "home": "ab5f1c41d377687e4c5a808dfdccf2757b33dd43f934d44df43d6e9118176853", + "import": "e343540eb49fdb1e53c4d54b9e60ae513b3103df6c9133048bed878e58067455", + "include": "f5b80ad3e06e3b863fcfbe415ec912e4e09653dd519f9ac207819339fe34730f", + "infinite": "bff57a1c56d9b47131d7c8ff85f008a53c612c63706052e7e19d7aaea93cf5b1", + "inspect": "d2b4644b2d5f1aa50cb576b9265e275fa91201e296c2bc79865f810067d538e9", + "interference": "b3d729d0e822e8d95485c8ef50649847572def10d4c713fff0e073b8031c5f5d", + "intersect": "9ab4f6551848a2be546f55502a77075bed2d3994f89d91b721eab92bcee61d61", + "intersection_curve": "ec4808113e203d2e7ef461d0b5e2179ed8908acd9f62051a5d3f36242b1eca8d", + "iso_analysis": "dacd34ac254ec4fbb6932350f62d4d92626d922a2bfc62d2caecdeb17303e896", + "iso_curve": "015ff2e26346ccaf149f75cf6a0652d9201c1b23a14e6bb62625acfa599dc002", + "joint": "72b71a608e713f6cf51a1cb84edb011d4ccbd8bfe41d917fa2a1d13345dc0b2b", + "joint_origin": "5b0761122eb4295ea220314e8db1efd1251e3482653d6cdcc47252d0268864a8", + "library": "ac0b1798e4c94340f9c854daa3829371c1a9f3ffea7679d955e6cfa139cafdc8", + "line": "cd1177ea659548f03d43c7fa7dcc209a0542b1ba4bd0153c5113576cb7a96a81", + "line_mid": "edb8ebc463479ae9878e54b930c124153ca880270b7cf6c0e4edba6336ba2854", + "lip": "09795b92c397eece2acb59fd941bcd12d60421565d767507ea73835eff4563eb", + "list": "21717be2af42958348ed9bf51f78a5651e44a811ba8904909e7ca23e00581ecb", + "loft": "58d177d4f1ec28de57232af5d16576f198cdc28f365af0a7fb21928cfe89775e", + "lookat": "83a7666f464aebf8ec46572470083e29d65db8f654f017ff5f10b68d5dacbf08", + "material": "1de5c7cb82714aed3c006a565ce5be0b047428f64e1c837a4ad53835b4fdba6d", + "measure": "6e8f5c26142786a01bbcdde0313d4c2dad798366409e926457128ffe1bc3014e", + "mesh_export": "c8c2abf007e43e896479d78798e3976ba9eda809507c820bb38538f77db696ac", + "min_radius": "92bd79b7d557aba6ddfccc76a4c6d0cfb579061b3c57172f9171d28af675711c", + "mirror": "56637068a319dc2972c5552b25e4f43151aa9bd5055c2fef08204dd7cc0829cf", + "moon": "3ba5adb92bf0f49e92d46682dbf6905e555aa6eebc8e2b3c5288f949cda29322", + "motion_link": "19698bbe15b7c2d2601208bbcdc26627d1aa7724aee017b7da8874ad87fb9d1f", + "move": "4132e270ada8d0c79b477095c84d656d6d354e200c1748ce7fc425f36d0b7031", + "nav_settings": "b93dd46d3a3d074e0d8834167f977dab417fbc1a721bc4db5e840732304e4193", + "new": "c64c78fd8445bc7b2aa8765d0507b9adf23e35cbf52828d3f1987edccce0389f", + "normal": "1148cef2d60a858b69406a2e2527d4ea126f45846de7cc2fb3a96640e9ee226f", + "offset": "094607afcfdcb9c1410114d9f8d22ca367f8e32ab4c61f71affe44ad5d09adbe", + "offset_face": "83d39308ec9f3ead78329c62841c8899bbc76ce08e17d539016d18c91d9d4ea7", + "offset_surface": "b501cfb56b8404cdb2275935d780ce7dbb67e87e29ade3782f0859c59070a2b1", + "open": "f1880d25489753f7e40390902037bbd3195fbd099a5e9dd175797d832abff727", + "orbit": "4adcd400cdde909139386e78579433354f65f5a77a0594447bf6dda663b3b1b4", + "origin": "9b9360edc16ef9f059bff7d863f1bc0e3abac6acf559d76c4ea39dc374ab2844", + "pan": "2465eda7a39d4458d7818f2eb2b033992a68f80d9505bff538b41eb4511bb632", + "params": "d0d9d0b3e786880e59b985dc2e3a7d593cf3771484a8fd2924d69155195c230f", + "paste": "e1b3c089adafacb6f7b78b86ec7aba0876b189c81d01031045dfee2c47ae8edc", + "patch": "b8f3c01c2d47ead98b6271d448f2d7769250678c5fd2acc31b208c723e8d306d", + "pattern": "74a802c4f59ab2b46d540b246d7ab67c48258b0dcb0cce20f06b8137de512811", + "pattern_center": "ad445f8619168ebe73d8eccfdfce0a03145a986d97a0c8f5105f991a8eac3cf9", + "pattern_circ": "c5ff904237a3072f858f66c2777febc4facd2fee630bbf979047c5685d9a05ce", + "pattern_flip": "bc75870bb40a7b44dcef4835798ace8b38dae58f602f409e887afcf85d573292", + "pattern_path": "50725a30cfd7dc73a531829fb337219560c65acf38ec2bb3e0a09c644a8c714c", + "pattern_rect": "be92ceea524c9eaefe56be2887b4842f521360157c084dca2c4092704925994f", + "perforate": "a62a04fd5c77fdbeac12ed9586342c5d76e652551190df76eb52483ce480d858", + "perspective": "5fa45ba9b75dbe9fdad9eae331ca7ff35c6ad018b8a1fc4c8ae7bcfc13ed0c3f", + "pipe": "b9ecf1ae9f6790e2ae6c677f290446f70eba1773a10247fe33d5283b360b0403", + "plane": "3e7fa0a9d57891f3c1ec03d7422bdb29353d3487deed12e8ec5737aeb33c2f3a", + "plane_along_path": "020e25969a7f4e7e3358003addee7df8b1991cde8401df0b4df8295b19497994", + "plane_angle": "2f0914955663b393a377fda080bc03dc5a2133d42c506c1908c53b2fd609b156", + "plane_midplane": "66bd880821e7b122f130824313b7070d72d79168a52df6a126106b6eb5816474", + "plane_perpendicular": "90e1d9b8e75743821aa1082596d9c3f288ab3baddf087fbae3f2025481e58a6e", + "plane_tangent": "c3872b9f1d0f0b2493a5488dfdad50049c3422c29023250088eeda9bbcc6e795", + "plane_three_points": "08e2dbaf606d165ef4f788869fc794a4fbbbb2ee86a60017b76e467af6da338a", + "plane_two_edges": "1588b8d4741382d9e8fec38212419ac1c05b1bc6036f276d3b5c62db66f8ebdb", + "plastic_assign": "bbfdb2d2c07629c047e8e671c7c032654668a67b4b8a934f74cbb3d7a00cc5fe", + "plastic_rule": "51b8591d92907a3ffb76a47a0503d7ba86706d129dd990515a364dbf1ab8cb48", + "play": "d1c17ebc16e9a4ee5ae2a8cc37b515ffcc8c53a2ab17117959e9b21278df8def", + "point": "fd19dbcd26e2a898267bab54b9d89779d97894209c1771317d7debef5a6f8f5d", + "point_along_path": "1fd58d45285fb5cabeddbd35bc8485af1b6d7f1414e302202b4e73de593d1b7a", + "point_center": "0ad755896f7d0154a854bda16d3059da78d1fae57962e13a3bb1b31e284b7e79", + "point_edge_plane": "548a6c1c944caabe44ab0318e85ae82f39c11f74fbedda35f57bc6615755f249", + "point_three_planes": "b33c7508ffa67b66a57b72c6ea7900e93ff16362a29d0b2688e0b1db9bfad041", + "point_two_edges": "35dcb67b36c8e41041cc836b01056731ab6f14c5e3b4bdd836fbee2a1dd897e3", + "point_vertex": "649bb63caf65a3fbc00a1222cc642294832b5e5d8c4d33f812e09a70684c4abe", + "polygon": "4919268cca7d21ce98dbf4ca01ee832350e793afe4213e257d3c46e94ea88ad1", + "polygon_circumscribed": "7e4ad16cc1f42c599287b186f26f4ff3c108736877a571a5513bfc72d0cbb23f", + "polygon_edge": "f3eceaa5c3856e42b33a366117ef6c91ca0fa7bafcd17a95c1697e27eb183309", + "presspull": "672aa5653aacc1cb13b15a176ae613e9510fbe84dda1e4783fc0b75fbd5ca353", + "project": "0299d3b1546267a6aa288d096133ba0404e8103f6914615aaa08f7fee29e9756", + "project_surface": "db449005c14dfbe4fd56f5c400b6f28b5356460b651af07105bfee5225ddb1e1", + "rack_pinion": "adedfeddbd22aec7c4dfc4a639c4d48db1e961b4632214b33966fbabe86c0696", + "reach": "1ed9d7a4146a2bc9b3a8e1a570cf383a0c7989fd9f58001f055b6d61db05bebc", + "rect": "159676a0a616c0e9050ff25f5f0fd2ff419f939dd5b683810b864aae8e425fba", + "rect3": "f7c0ca1e3b91357ac8a65df5219d0e00339a3c97407b58d8612f7d195e0b7d68", + "rect_center": "1807156588c7413ef543ca4ecb5679c8de2724d98c4dcc153c3958cbbc945045", + "rect_rounded": "fba07a477c517324e448dd9130aa2a53c5fd9c1cfac8d018836d71044880ae1d", + "redo": "28e925493fa9e5f52e2c8e3beeb288af13fdb4f85f3cd92270096a10561e7a1a", + "refold": "1fa6a8604af7d04c01bc31d0e5875eb2cc91f5fa6bfc5491dd6680d3299eb26f", + "rename": "6887c5467101dca1d213154020d3b03be9b2704ac310de9d0a8ebde62c013bf5", + "render_image": "e3dd622591de4fdc211f9bd3c6dcb54b01a308e8bcebd02bb80dd6b3982ce2a5", + "render_scene": "1c4d7350f6f17ad637a7d633cb65a64c298af20321b1dbdac01b51d47a0e2822", + "reorder": "c45755c8f23575415397bffb316eed9f2d8c014f451ab6f8c3123aca1a99b675", + "replace_face": "fcee556c684a1138a30a2574ea565c98ff29f9368c2e861d0fb861155a57d28b", + "rest": "eab64961b5340bdbbd92affe0c9b8477dc297a3b49e05e39d5fa71c0f2fda66c", + "restore": "06911c1a903ae4fa3b830809caea4da50f997d73612f4ebab82f9be7db3df559", + "reverse_normal": "a15cff514200d3ddd23bb624c90dde65e1291cba835d75798796694ced3a4e50", + "revert": "4809aa9728b0478d0218024d4b981a1b8cc1258d1342646029b4a06384a3f60b", + "revolve": "3610e710a5c03c04ff8344e13addb3dac7c5dd46d41151a583b01b15b9e7dc32", + "rib": "4162c8ea2e7d1929b45649cae1fac618d7074402b7121466ce9b32ed0951272d", + "rigid_group": "21ca1be510a7f9499cec1aaf48ea44acc2566b925ddf67bc9a63653dd20da37a", + "rollback": "b76633b0e8e1f9c4f0bbe11843b31ec5208fbed1ef316b75e1d309cdd686b8b5", + "ruled": "c3f0202d3beaacef0368765ce771eb9e3b0e3e157f86564c72d81f0fad9a3cab", + "save": "5b780c23dd467311686ec8677b387ecce81639ad3fd63bff3e0209ea6b9f2ff3", + "scale": "a66f69d9a1d46c8be30da0a15827bc7e38cba72d6271537763b888c2c7e2b3fa", + "screw": "5e30570785360d6a591ea22862a82f1cf62228385c1e96382536db8a453ac791", + "section": "cdd13f99f15f6d59bfa21c747810608d6afd00cb8207faf0337928c8521e7eae", + "select": "a0c716234ef1d599be9a7654d6b2911ebe4b2de479383ba33aeab14992443a2f", + "settings": "9193dd3a1007c43660d0d47b6499cb2571338b67949e1a99df112057b9bf2b8a", + "sheet": "774e30923ef6f37dd9c0b2bc3e45705e83b3a76700bf071cd41155a365d0daa4", + "sheet_rules": "b8c3a67c82be20fc00c109decc566e8755d70167e04e0a9fb92941f7b27aa993", + "shell": "a1959ea0d0441a479bf654b6e549e2e44a0bfdc9cda4eb46fe1fbfd884de7e38", + "sketch": "b52e5862b1709a9a7e3ca00d7436d126fbfc01b38080006fa4e17dc581dfa103", + "sketch_break": "430582243311f304dbab397ce58c8c9aef9c78ed88d3e921a57e584ddb95d94c", + "sketch_chamfer": "80ab27a9929f9cf0ab2f827757e007173d74ddca6c19af238931c40d4cd874b4", + "sketch_chamfer_angle": "b853f5e7c3b008c0d56ac445a9a28f713987c919ea284945113ad35e7665e2a7", + "sketch_chamfer_two_distances": "2b70d658e341f4388838f3c8c579ec5f4f3a7fddcfa2ce7a3f89fc3db6422d6e", + "sketch_circ_pattern": "c5ff904237a3072f858f66c2777febc4facd2fee630bbf979047c5685d9a05ce", + "sketch_dxf": "dddce44d4dd10f75a221208fcf431b36b3f0cafce6611fc695ac73afb4bf5789", + "sketch_fillet": "e8fa170e5f6899abf75788b9384d49f2b9d77ba9b644f79309284a9e65629fe1", + "sketch_mirror": "6f54361a10464f8ff50843b09570e472156ce33a7c07572cb854d217dba62642", + "sketch_move": "4132e270ada8d0c79b477095c84d656d6d354e200c1748ce7fc425f36d0b7031", + "sketch_rect_pattern": "be92ceea524c9eaefe56be2887b4842f521360157c084dca2c4092704925994f", + "sketch_scale": "a66f69d9a1d46c8be30da0a15827bc7e38cba72d6271537763b888c2c7e2b3fa", + "sketch_text": "9d31b72b16644681c8a86ac257511847ad69ce4351cf6960442222e94b168c4a", + "slot": "45f57f525f5a2218f629018c84a4e6b3c5963216e4b94d3df9364c0cc75b819f", + "slot_center": "34509c043fb0ea089bc915b1967bd728a870c09c6422751ec64d22819ba21f96", + "slot_overall": "8fcc4f4d293d45b325a4a638753c49552a0e10c6dd980ce5f15782d01cababeb", + "snap": "d15130717be58ad0884a4c6f7769a7a43bd55c0d4f35ecf6bd5f7ea1360f8930", + "snap_fit": "5b6b48d0f3876197c3825e057dc05fa1a1e89db86efe760752269c314268c6f8", + "sphere": "c8bcb1d2f4b9a52b6d3f730d131d00c9fdc6a1c1245283de6450588d0a46a4ab", + "spin": "a5bebeff8276dd12ab7e42ddf0051370b9063fb602607298acf22e77044434d1", + "spline": "a0dcb25614e5fe1a8f17bc3bddbb11b61cf82b461dd46443ab28cddd6e4e32e3", + "spline_cv": "353f6e411c0ce611f660cef2e4fc4f8f55edc2c82288de91948dc638cd2b5f15", + "split_body": "eb9619e74c6ee1b1989d35a2e31d2d33a7adac210c4cd424e5382902945ac68b", + "split_face": "c8b89072118bb7a28b243d38e26b81a796781a631f30b3b1fc296ba23e4d908d", + "spun": "b4cfa093ffb43611ee124f4a39fb44c609deb9cde5e05e4dfbaf893fe197c27b", + "star": "368cf8c9a1258ad2c0ac673922b755dd3980397bb352c81436590c776ff53c0b", + "status_error": "b7188fcdfadf1512ddd2896805a14618179789f87047b219560a7dab74bd360f", + "status_warning": "1ac4f0ccfac2c01f6f66cb42188f9ae053c4aef9c5bbd249f1fb09611eb4d825", + "stitch": "6c3205d64007b771ed78d6f8055867578439fb25d591563b32dbd3343c98732d", + "style": "6c2b04234b82ea4858c80190cc7208c60f549358253005afc35ab4e29506b578", + "suppress": "6c033a6672a3b6ca926e3d7a75a758c9db7e1144210cd360be043a9fdf999ede", + "surface": "ba0e839ce0ee03e7ff95a2389a5ecb20bd80c713064d9b1bd5f461f4a80d1921", + "surface_blend": "0642f3b4af7d22f379a3c74f54c7b1c9eef0bdda19372ce4df73c0e56bdd7bbc", + "surface_boundary": "fb8501bae192e2416118cc2e88f544e0e29197f6720bccc541c967ca0848d8d3", + "surface_extend": "28c63df352c77d02feffe44cf09fb0fc1d48f7905aaabbfa6dba601dbb70f8e8", + "surface_extrude": "9cc1538971c5d25467aaed23f739899d9d102fd07f86472ac3197c54ca2e1144", + "surface_fair": "353f6e411c0ce611f660cef2e4fc4f8f55edc2c82288de91948dc638cd2b5f15", + "surface_fillet": "11ec232dc111597211e3a4fe61b69616b5d28b4b1b2732052ddcd486cdfc6129", + "surface_loft": "58d177d4f1ec28de57232af5d16576f198cdc28f365af0a7fb21928cfe89775e", + "surface_offset": "b501cfb56b8404cdb2275935d780ce7dbb67e87e29ade3782f0859c59070a2b1", + "surface_patch": "b8f3c01c2d47ead98b6271d448f2d7769250678c5fd2acc31b208c723e8d306d", + "surface_reverse_normal": "a15cff514200d3ddd23bb624c90dde65e1291cba835d75798796694ced3a4e50", + "surface_revolve": "3610e710a5c03c04ff8344e13addb3dac7c5dd46d41151a583b01b15b9e7dc32", + "surface_ruled": "c3f0202d3beaacef0368765ce771eb9e3b0e3e157f86564c72d81f0fad9a3cab", + "surface_stitch": "6c3205d64007b771ed78d6f8055867578439fb25d591563b32dbd3343c98732d", + "surface_sweep": "6d7f18c12b2e4c1dbe3632fe08239687f6f027b29b6dddbf8c8e2d9151c7baf3", + "surface_thicken": "676eb73a99d708214e8951ebe2c4d88d60de2d752c725d8106d12fbd812e461a", + "surface_trim": "ae269721b386bd1264184b9838df3240b5b6ba02b99c9993384ef21200fb7398", + "surface_unstitch": "bd8302fe95a89eb6f9bbf35ef0ee98c8a2186e160cff3d091f8cf2a0a75d4473", + "surface_untrim": "24de5d558519e0629280d11919ba5661c92ccdccdce03f8f3d83f2d7867c2173", + "svg": "7da77d53c84db18ce95737ad633c105d1009444b3192a5e0aae76873cfc9cc12", + "sweep": "6d7f18c12b2e4c1dbe3632fe08239687f6f027b29b6dddbf8c8e2d9151c7baf3", + "text": "9d31b72b16644681c8a86ac257511847ad69ce4351cf6960442222e94b168c4a", + "thicken": "676eb73a99d708214e8951ebe2c4d88d60de2d752c725d8106d12fbd812e461a", + "thread": "158728c3dcf6623bb1584d162398bb12c28d18770e0d47f5e6328355ce31b37c", + "torus": "5ae13fc213582e0844118b910a3a26efd9e4b177d21500363fbae230803cb4fd", + "trace": "5f2ecc61d4591d3c3b0ecccd35296b8613fce7beb2ae214fb906160c31d89187", + "trim": "ae269721b386bd1264184b9838df3240b5b6ba02b99c9993384ef21200fb7398", + "undo": "e764485407adc80ed73b85fd10e0b87c2702cde55bd8459bd96f17f22d47336d", + "unfold": "eb7aeac6e186a6b7ebbd10197eb850b4a11b9fe4cf1d009451fdfb451e9a35cd", + "unstitch": "bd8302fe95a89eb6f9bbf35ef0ee98c8a2186e160cff3d091f8cf2a0a75d4473", + "untrim": "24de5d558519e0629280d11919ba5661c92ccdccdce03f8f3d83f2d7867c2173", + "update": "c675e16df4c78589a00be4a34f98627500681672ee8a668e929aec50982da3cc", + "warning": "1ac4f0ccfac2c01f6f66cb42188f9ae053c4aef9c5bbd249f1fb09611eb4d825", + "web": "be5cd95bebcd0ebe257f9eb862112124429b381ef4a2f0849938fac7c9ccc619", + "ws_animate": "d36a0834ad989a6e16e2b0db493ad8867c590caedf0442c51eff19e934c887f8", + "ws_design": "7b1dd678a2d018892b965a78932c047bbfff6b4439d7bd9fe85c09a995f97581", + "ws_drawing": "61e99c7179f3448a31b8c76392334352ec3d5e39b0b92fe5244940146d0986cb", + "ws_render": "1bfe474d1742623ad2142129e4d6a40015e674813eb1db401d2e88e55b1f2a03", + "zebra": "1d341ece6fcb09a3f4911633e1c9b57d92d96eb257f992dc26be79e1d521c69a", + "zoom": "89c121fa85df034d70bd9f06e20cbd54d163691d4df03b13a9ac5372e951aeda" + }, + "notes": { + "pattern_flip": "This feels sparse. Improve the composition" + } +} diff --git a/assets/icons/ui/design/svg/flat/bug_report.svg b/assets/icons/ui/design/svg/flat/bug_report.svg new file mode 100644 index 00000000..650d81c0 --- /dev/null +++ b/assets/icons/ui/design/svg/flat/bug_report.svg @@ -0,0 +1,13 @@ + + Bug report + + + + + + + + + + + diff --git a/assets/icons/ui/design/svg/flat/chevron.svg b/assets/icons/ui/design/svg/flat/chevron.svg new file mode 100644 index 00000000..d27f945b --- /dev/null +++ b/assets/icons/ui/design/svg/flat/chevron.svg @@ -0,0 +1,4 @@ + + Chevron + + diff --git a/assets/icons/ui/design/svg/flat/fit.svg b/assets/icons/ui/design/svg/flat/fit.svg new file mode 100644 index 00000000..cfbfe42a --- /dev/null +++ b/assets/icons/ui/design/svg/flat/fit.svg @@ -0,0 +1,5 @@ + + Fit + + + diff --git a/assets/icons/ui/design/svg/flat/folder.svg b/assets/icons/ui/design/svg/flat/folder.svg new file mode 100644 index 00000000..d631c7a2 --- /dev/null +++ b/assets/icons/ui/design/svg/flat/folder.svg @@ -0,0 +1,4 @@ + + Folder + + diff --git a/assets/icons/ui/design/svg/flat/help.svg b/assets/icons/ui/design/svg/flat/help.svg new file mode 100644 index 00000000..dfdbb9b0 --- /dev/null +++ b/assets/icons/ui/design/svg/flat/help.svg @@ -0,0 +1,11 @@ + + Help + + + + + + + + + diff --git a/assets/icons/ui/design/svg/flat/home.svg b/assets/icons/ui/design/svg/flat/home.svg new file mode 100644 index 00000000..c64c7fb0 --- /dev/null +++ b/assets/icons/ui/design/svg/flat/home.svg @@ -0,0 +1,10 @@ + + Home + + + + + + + + diff --git a/assets/icons/ui/design/svg/flat/lookat.svg b/assets/icons/ui/design/svg/flat/lookat.svg new file mode 100644 index 00000000..3fcab422 --- /dev/null +++ b/assets/icons/ui/design/svg/flat/lookat.svg @@ -0,0 +1,13 @@ + + Lookat + + + + + + + + + + + diff --git a/assets/icons/ui/design/svg/flat/moon.svg b/assets/icons/ui/design/svg/flat/moon.svg new file mode 100644 index 00000000..1858aa77 --- /dev/null +++ b/assets/icons/ui/design/svg/flat/moon.svg @@ -0,0 +1,5 @@ + + Moon + + + diff --git a/assets/icons/ui/design/svg/flat/nav_settings.svg b/assets/icons/ui/design/svg/flat/nav_settings.svg new file mode 100644 index 00000000..6c980866 --- /dev/null +++ b/assets/icons/ui/design/svg/flat/nav_settings.svg @@ -0,0 +1,10 @@ + + Nav settings + + + + + + + + diff --git a/assets/icons/ui/design/svg/flat/orbit.svg b/assets/icons/ui/design/svg/flat/orbit.svg new file mode 100644 index 00000000..d7bf880e --- /dev/null +++ b/assets/icons/ui/design/svg/flat/orbit.svg @@ -0,0 +1,6 @@ + + Orbit + + + + diff --git a/assets/icons/ui/design/svg/flat/pan.svg b/assets/icons/ui/design/svg/flat/pan.svg new file mode 100644 index 00000000..570cde41 --- /dev/null +++ b/assets/icons/ui/design/svg/flat/pan.svg @@ -0,0 +1,4 @@ + + Pan + + diff --git a/assets/icons/ui/design/svg/flat/perspective.svg b/assets/icons/ui/design/svg/flat/perspective.svg new file mode 100644 index 00000000..e961717d --- /dev/null +++ b/assets/icons/ui/design/svg/flat/perspective.svg @@ -0,0 +1,5 @@ + + Perspective + + + diff --git a/assets/icons/ui/design/svg/flat/redo.svg b/assets/icons/ui/design/svg/flat/redo.svg new file mode 100644 index 00000000..9b276229 --- /dev/null +++ b/assets/icons/ui/design/svg/flat/redo.svg @@ -0,0 +1,4 @@ + + Redo + + diff --git a/assets/icons/ui/design/svg/flat/save.svg b/assets/icons/ui/design/svg/flat/save.svg new file mode 100644 index 00000000..32572002 --- /dev/null +++ b/assets/icons/ui/design/svg/flat/save.svg @@ -0,0 +1,11 @@ + + Save + + + + + + + + + diff --git a/assets/icons/ui/design/svg/flat/settings.svg b/assets/icons/ui/design/svg/flat/settings.svg new file mode 100644 index 00000000..9fea685b --- /dev/null +++ b/assets/icons/ui/design/svg/flat/settings.svg @@ -0,0 +1,10 @@ + + Settings + + + + + + + + diff --git a/assets/icons/ui/design/svg/flat/spin.svg b/assets/icons/ui/design/svg/flat/spin.svg new file mode 100644 index 00000000..d673a0ef --- /dev/null +++ b/assets/icons/ui/design/svg/flat/spin.svg @@ -0,0 +1,18 @@ + + Spin + + + + + + + + + + + + + + + + diff --git a/assets/icons/ui/design/svg/flat/style.svg b/assets/icons/ui/design/svg/flat/style.svg new file mode 100644 index 00000000..268a5c46 --- /dev/null +++ b/assets/icons/ui/design/svg/flat/style.svg @@ -0,0 +1,5 @@ + + Style + + + diff --git a/assets/icons/ui/design/svg/flat/undo.svg b/assets/icons/ui/design/svg/flat/undo.svg new file mode 100644 index 00000000..ba35691a --- /dev/null +++ b/assets/icons/ui/design/svg/flat/undo.svg @@ -0,0 +1,4 @@ + + Undo + + diff --git a/assets/icons/ui/design/svg/flat/zoom.svg b/assets/icons/ui/design/svg/flat/zoom.svg new file mode 100644 index 00000000..487f944e --- /dev/null +++ b/assets/icons/ui/design/svg/flat/zoom.svg @@ -0,0 +1,5 @@ + + Zoom + + + diff --git a/assets/icons/ui/design/svg/model/accessibility.svg b/assets/icons/ui/design/svg/model/accessibility.svg new file mode 100644 index 00000000..a808397c --- /dev/null +++ b/assets/icons/ui/design/svg/model/accessibility.svg @@ -0,0 +1,17 @@ + + Accessibility + + + + + + + + + + + + + + + diff --git a/assets/icons/ui/design/svg/model/activate.svg b/assets/icons/ui/design/svg/model/activate.svg new file mode 100644 index 00000000..67f2d66d --- /dev/null +++ b/assets/icons/ui/design/svg/model/activate.svg @@ -0,0 +1,6 @@ + + Activate + + + + diff --git a/assets/icons/ui/design/svg/model/agent_chat.svg b/assets/icons/ui/design/svg/model/agent_chat.svg new file mode 100644 index 00000000..e3994b58 --- /dev/null +++ b/assets/icons/ui/design/svg/model/agent_chat.svg @@ -0,0 +1,7 @@ + + Agent chat + + + + + diff --git a/assets/icons/ui/design/svg/model/align.svg b/assets/icons/ui/design/svg/model/align.svg new file mode 100644 index 00000000..e4032028 --- /dev/null +++ b/assets/icons/ui/design/svg/model/align.svg @@ -0,0 +1,7 @@ + + Align + + + + + diff --git a/assets/icons/ui/design/svg/model/appearance.svg b/assets/icons/ui/design/svg/model/appearance.svg new file mode 100644 index 00000000..d7284792 --- /dev/null +++ b/assets/icons/ui/design/svg/model/appearance.svg @@ -0,0 +1,6 @@ + + Appearance + + + + diff --git a/assets/icons/ui/design/svg/model/arc3.svg b/assets/icons/ui/design/svg/model/arc3.svg new file mode 100644 index 00000000..f1fa9a01 --- /dev/null +++ b/assets/icons/ui/design/svg/model/arc3.svg @@ -0,0 +1,10 @@ + + Three-point arc + + + + + + + + diff --git a/assets/icons/ui/design/svg/model/arc_center.svg b/assets/icons/ui/design/svg/model/arc_center.svg new file mode 100644 index 00000000..3de74079 --- /dev/null +++ b/assets/icons/ui/design/svg/model/arc_center.svg @@ -0,0 +1,12 @@ + + Centre-point arc + + + + + + + + + + diff --git a/assets/icons/ui/design/svg/model/arc_slot.svg b/assets/icons/ui/design/svg/model/arc_slot.svg new file mode 100644 index 00000000..d97638eb --- /dev/null +++ b/assets/icons/ui/design/svg/model/arc_slot.svg @@ -0,0 +1,10 @@ + + Arc slot + + + + + + + + diff --git a/assets/icons/ui/design/svg/model/arc_slot_center.svg b/assets/icons/ui/design/svg/model/arc_slot_center.svg new file mode 100644 index 00000000..d1cbd5f3 --- /dev/null +++ b/assets/icons/ui/design/svg/model/arc_slot_center.svg @@ -0,0 +1,12 @@ + + Centre point arc slot + + + + + + + + + + diff --git a/assets/icons/ui/design/svg/model/arc_tangent.svg b/assets/icons/ui/design/svg/model/arc_tangent.svg new file mode 100644 index 00000000..efb3ba40 --- /dev/null +++ b/assets/icons/ui/design/svg/model/arc_tangent.svg @@ -0,0 +1,9 @@ + + Tangent arc + + + + + + + diff --git a/assets/icons/ui/design/svg/model/as_built.svg b/assets/icons/ui/design/svg/model/as_built.svg new file mode 100644 index 00000000..3866fdc4 --- /dev/null +++ b/assets/icons/ui/design/svg/model/as_built.svg @@ -0,0 +1,19 @@ + + As-built joint + + + + + + + + + + + + + + + + + diff --git a/assets/icons/ui/design/svg/model/auto_constrain.svg b/assets/icons/ui/design/svg/model/auto_constrain.svg new file mode 100644 index 00000000..e94ced66 --- /dev/null +++ b/assets/icons/ui/design/svg/model/auto_constrain.svg @@ -0,0 +1,5 @@ + + Auto constrain + + + diff --git a/assets/icons/ui/design/svg/model/axis.svg b/assets/icons/ui/design/svg/model/axis.svg new file mode 100644 index 00000000..79434aeb --- /dev/null +++ b/assets/icons/ui/design/svg/model/axis.svg @@ -0,0 +1,8 @@ + + Axis + + + + + + diff --git a/assets/icons/ui/design/svg/model/axis_edge.svg b/assets/icons/ui/design/svg/model/axis_edge.svg new file mode 100644 index 00000000..e73230c5 --- /dev/null +++ b/assets/icons/ui/design/svg/model/axis_edge.svg @@ -0,0 +1,11 @@ + + Axis through edge + + + + + + + + + diff --git a/assets/icons/ui/design/svg/model/axis_normal_to_face.svg b/assets/icons/ui/design/svg/model/axis_normal_to_face.svg new file mode 100644 index 00000000..efddcd56 --- /dev/null +++ b/assets/icons/ui/design/svg/model/axis_normal_to_face.svg @@ -0,0 +1,10 @@ + + Axis perpendicular to face + + + + + + + + diff --git a/assets/icons/ui/design/svg/model/axis_through_cylinder.svg b/assets/icons/ui/design/svg/model/axis_through_cylinder.svg new file mode 100644 index 00000000..aff71da0 --- /dev/null +++ b/assets/icons/ui/design/svg/model/axis_through_cylinder.svg @@ -0,0 +1,11 @@ + + Axis through cylinder, cone or torus + + + + + + + + + diff --git a/assets/icons/ui/design/svg/model/axis_two_planes.svg b/assets/icons/ui/design/svg/model/axis_two_planes.svg new file mode 100644 index 00000000..91985f13 --- /dev/null +++ b/assets/icons/ui/design/svg/model/axis_two_planes.svg @@ -0,0 +1,8 @@ + + Axis through two planes + + + + + + diff --git a/assets/icons/ui/design/svg/model/axis_two_points.svg b/assets/icons/ui/design/svg/model/axis_two_points.svg new file mode 100644 index 00000000..68accf36 --- /dev/null +++ b/assets/icons/ui/design/svg/model/axis_two_points.svg @@ -0,0 +1,7 @@ + + Axis through two points + + + + + diff --git a/assets/icons/ui/design/svg/model/bendtable.svg b/assets/icons/ui/design/svg/model/bendtable.svg new file mode 100644 index 00000000..4f52a3a4 --- /dev/null +++ b/assets/icons/ui/design/svg/model/bendtable.svg @@ -0,0 +1,12 @@ + + Bend table + + + + + + + + + + diff --git a/assets/icons/ui/design/svg/model/blend.svg b/assets/icons/ui/design/svg/model/blend.svg new file mode 100644 index 00000000..8dd2529f --- /dev/null +++ b/assets/icons/ui/design/svg/model/blend.svg @@ -0,0 +1,9 @@ + + Blend curve + + + + + + + diff --git a/assets/icons/ui/design/svg/model/blend_surface.svg b/assets/icons/ui/design/svg/model/blend_surface.svg new file mode 100644 index 00000000..807fb4d6 --- /dev/null +++ b/assets/icons/ui/design/svg/model/blend_surface.svg @@ -0,0 +1,9 @@ + + Blend surface + + + + + + + diff --git a/assets/icons/ui/design/svg/model/body.svg b/assets/icons/ui/design/svg/model/body.svg new file mode 100644 index 00000000..7b4cd614 --- /dev/null +++ b/assets/icons/ui/design/svg/model/body.svg @@ -0,0 +1,10 @@ + + Body + + + + + + + + diff --git a/assets/icons/ui/design/svg/model/boss.svg b/assets/icons/ui/design/svg/model/boss.svg new file mode 100644 index 00000000..0468f7a1 --- /dev/null +++ b/assets/icons/ui/design/svg/model/boss.svg @@ -0,0 +1,13 @@ + + Boss + + + + + + + + + + + diff --git a/assets/icons/ui/design/svg/model/boundary.svg b/assets/icons/ui/design/svg/model/boundary.svg new file mode 100644 index 00000000..11230c77 --- /dev/null +++ b/assets/icons/ui/design/svg/model/boundary.svg @@ -0,0 +1,9 @@ + + Boundary surface + + + + + + + diff --git a/assets/icons/ui/design/svg/model/boundary_fill.svg b/assets/icons/ui/design/svg/model/boundary_fill.svg new file mode 100644 index 00000000..38746f8b --- /dev/null +++ b/assets/icons/ui/design/svg/model/boundary_fill.svg @@ -0,0 +1,15 @@ + + Boundary fill + + + + + + + + + + + + + diff --git a/assets/icons/ui/design/svg/model/bounding_solid.svg b/assets/icons/ui/design/svg/model/bounding_solid.svg new file mode 100644 index 00000000..6816e8d1 --- /dev/null +++ b/assets/icons/ui/design/svg/model/bounding_solid.svg @@ -0,0 +1,13 @@ + + Bounding solid + + + + + + + + + + + diff --git a/assets/icons/ui/design/svg/model/box.svg b/assets/icons/ui/design/svg/model/box.svg new file mode 100644 index 00000000..f0fd914b --- /dev/null +++ b/assets/icons/ui/design/svg/model/box.svg @@ -0,0 +1,9 @@ + + Box + + + + + + + diff --git a/assets/icons/ui/design/svg/model/break.svg b/assets/icons/ui/design/svg/model/break.svg new file mode 100644 index 00000000..614bed30 --- /dev/null +++ b/assets/icons/ui/design/svg/model/break.svg @@ -0,0 +1,7 @@ + + Break + + + + + diff --git a/assets/icons/ui/design/svg/model/c_coincident.svg b/assets/icons/ui/design/svg/model/c_coincident.svg new file mode 100644 index 00000000..01bc385e --- /dev/null +++ b/assets/icons/ui/design/svg/model/c_coincident.svg @@ -0,0 +1,6 @@ + + Coincident + + + + diff --git a/assets/icons/ui/design/svg/model/c_collinear.svg b/assets/icons/ui/design/svg/model/c_collinear.svg new file mode 100644 index 00000000..e074a8bc --- /dev/null +++ b/assets/icons/ui/design/svg/model/c_collinear.svg @@ -0,0 +1,5 @@ + + Collinear + + + diff --git a/assets/icons/ui/design/svg/model/c_concentric.svg b/assets/icons/ui/design/svg/model/c_concentric.svg new file mode 100644 index 00000000..d137b8cf --- /dev/null +++ b/assets/icons/ui/design/svg/model/c_concentric.svg @@ -0,0 +1,5 @@ + + Concentric + + + diff --git a/assets/icons/ui/design/svg/model/c_equal.svg b/assets/icons/ui/design/svg/model/c_equal.svg new file mode 100644 index 00000000..e750edba --- /dev/null +++ b/assets/icons/ui/design/svg/model/c_equal.svg @@ -0,0 +1,5 @@ + + Equal + + + diff --git a/assets/icons/ui/design/svg/model/c_fix.svg b/assets/icons/ui/design/svg/model/c_fix.svg new file mode 100644 index 00000000..7de87c23 --- /dev/null +++ b/assets/icons/ui/design/svg/model/c_fix.svg @@ -0,0 +1,6 @@ + + Fixed + + + + diff --git a/assets/icons/ui/design/svg/model/c_hv.svg b/assets/icons/ui/design/svg/model/c_hv.svg new file mode 100644 index 00000000..a0ccfe5e --- /dev/null +++ b/assets/icons/ui/design/svg/model/c_hv.svg @@ -0,0 +1,5 @@ + + Horizontal / vertical + + + diff --git a/assets/icons/ui/design/svg/model/c_midpoint.svg b/assets/icons/ui/design/svg/model/c_midpoint.svg new file mode 100644 index 00000000..e2a504a6 --- /dev/null +++ b/assets/icons/ui/design/svg/model/c_midpoint.svg @@ -0,0 +1,6 @@ + + Midpoint + + + + diff --git a/assets/icons/ui/design/svg/model/c_parallel.svg b/assets/icons/ui/design/svg/model/c_parallel.svg new file mode 100644 index 00000000..8462b315 --- /dev/null +++ b/assets/icons/ui/design/svg/model/c_parallel.svg @@ -0,0 +1,5 @@ + + Parallel + + + diff --git a/assets/icons/ui/design/svg/model/c_perpendicular.svg b/assets/icons/ui/design/svg/model/c_perpendicular.svg new file mode 100644 index 00000000..56bfc50c --- /dev/null +++ b/assets/icons/ui/design/svg/model/c_perpendicular.svg @@ -0,0 +1,6 @@ + + Perpendicular + + + + diff --git a/assets/icons/ui/design/svg/model/c_polygon.svg b/assets/icons/ui/design/svg/model/c_polygon.svg new file mode 100644 index 00000000..918d59de --- /dev/null +++ b/assets/icons/ui/design/svg/model/c_polygon.svg @@ -0,0 +1,6 @@ + + Regular polygon + + + + diff --git a/assets/icons/ui/design/svg/model/c_smooth.svg b/assets/icons/ui/design/svg/model/c_smooth.svg new file mode 100644 index 00000000..972a625e --- /dev/null +++ b/assets/icons/ui/design/svg/model/c_smooth.svg @@ -0,0 +1,6 @@ + + Curvature + + + + diff --git a/assets/icons/ui/design/svg/model/c_symmetry.svg b/assets/icons/ui/design/svg/model/c_symmetry.svg new file mode 100644 index 00000000..a3409b64 --- /dev/null +++ b/assets/icons/ui/design/svg/model/c_symmetry.svg @@ -0,0 +1,6 @@ + + Symmetry + + + + diff --git a/assets/icons/ui/design/svg/model/c_tangent.svg b/assets/icons/ui/design/svg/model/c_tangent.svg new file mode 100644 index 00000000..89656602 --- /dev/null +++ b/assets/icons/ui/design/svg/model/c_tangent.svg @@ -0,0 +1,5 @@ + + Tangent + + + diff --git a/assets/icons/ui/design/svg/model/cam.svg b/assets/icons/ui/design/svg/model/cam.svg new file mode 100644 index 00000000..e269dc90 --- /dev/null +++ b/assets/icons/ui/design/svg/model/cam.svg @@ -0,0 +1,10 @@ + + Cam coupling + + + + + + + + diff --git a/assets/icons/ui/design/svg/model/camera.svg b/assets/icons/ui/design/svg/model/camera.svg new file mode 100644 index 00000000..1545eb68 --- /dev/null +++ b/assets/icons/ui/design/svg/model/camera.svg @@ -0,0 +1,6 @@ + + Camera + + + + diff --git a/assets/icons/ui/design/svg/model/canvas.svg b/assets/icons/ui/design/svg/model/canvas.svg new file mode 100644 index 00000000..f49d1b5a --- /dev/null +++ b/assets/icons/ui/design/svg/model/canvas.svg @@ -0,0 +1,7 @@ + + Canvas + + + + + diff --git a/assets/icons/ui/design/svg/model/capture.svg b/assets/icons/ui/design/svg/model/capture.svg new file mode 100644 index 00000000..1b501969 --- /dev/null +++ b/assets/icons/ui/design/svg/model/capture.svg @@ -0,0 +1,13 @@ + + Capture position + + + + + + + + + + + diff --git a/assets/icons/ui/design/svg/model/center_of_mass.svg b/assets/icons/ui/design/svg/model/center_of_mass.svg new file mode 100644 index 00000000..d3930bd5 --- /dev/null +++ b/assets/icons/ui/design/svg/model/center_of_mass.svg @@ -0,0 +1,16 @@ + + Center of mass + + + + + + + + + + + + + + diff --git a/assets/icons/ui/design/svg/model/centerline.svg b/assets/icons/ui/design/svg/model/centerline.svg new file mode 100644 index 00000000..d839cff6 --- /dev/null +++ b/assets/icons/ui/design/svg/model/centerline.svg @@ -0,0 +1,9 @@ + + Centerline + + + + + + + diff --git a/assets/icons/ui/design/svg/model/chain.svg b/assets/icons/ui/design/svg/model/chain.svg new file mode 100644 index 00000000..e664be3e --- /dev/null +++ b/assets/icons/ui/design/svg/model/chain.svg @@ -0,0 +1,12 @@ + + Chain + + + + + + + + + + diff --git a/assets/icons/ui/design/svg/model/chamfer.svg b/assets/icons/ui/design/svg/model/chamfer.svg new file mode 100644 index 00000000..ee3201f6 --- /dev/null +++ b/assets/icons/ui/design/svg/model/chamfer.svg @@ -0,0 +1,13 @@ + + Chamfer + + + + + + + + + + + diff --git a/assets/icons/ui/design/svg/model/checkpoint.svg b/assets/icons/ui/design/svg/model/checkpoint.svg new file mode 100644 index 00000000..6e92ede5 --- /dev/null +++ b/assets/icons/ui/design/svg/model/checkpoint.svg @@ -0,0 +1,7 @@ + + Checkpoint + + + + + diff --git a/assets/icons/ui/design/svg/model/circle.svg b/assets/icons/ui/design/svg/model/circle.svg new file mode 100644 index 00000000..ad5aa1a2 --- /dev/null +++ b/assets/icons/ui/design/svg/model/circle.svg @@ -0,0 +1,8 @@ + + Centre circle + + + + + + diff --git a/assets/icons/ui/design/svg/model/circle2.svg b/assets/icons/ui/design/svg/model/circle2.svg new file mode 100644 index 00000000..234b1769 --- /dev/null +++ b/assets/icons/ui/design/svg/model/circle2.svg @@ -0,0 +1,8 @@ + + Two-point circle + + + + + + diff --git a/assets/icons/ui/design/svg/model/circle3.svg b/assets/icons/ui/design/svg/model/circle3.svg new file mode 100644 index 00000000..568409eb --- /dev/null +++ b/assets/icons/ui/design/svg/model/circle3.svg @@ -0,0 +1,10 @@ + + Three-point circle + + + + + + + + diff --git a/assets/icons/ui/design/svg/model/circle_tt.svg b/assets/icons/ui/design/svg/model/circle_tt.svg new file mode 100644 index 00000000..1f1c9172 --- /dev/null +++ b/assets/icons/ui/design/svg/model/circle_tt.svg @@ -0,0 +1,9 @@ + + Circle tangent to two lines + + + + + + + diff --git a/assets/icons/ui/design/svg/model/circle_ttt.svg b/assets/icons/ui/design/svg/model/circle_ttt.svg new file mode 100644 index 00000000..6ef437b8 --- /dev/null +++ b/assets/icons/ui/design/svg/model/circle_ttt.svg @@ -0,0 +1,11 @@ + + Circle tangent to three lines + + + + + + + + + diff --git a/assets/icons/ui/design/svg/model/coil.svg b/assets/icons/ui/design/svg/model/coil.svg new file mode 100644 index 00000000..8c99f3da --- /dev/null +++ b/assets/icons/ui/design/svg/model/coil.svg @@ -0,0 +1,34 @@ + + Coil + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/ui/design/svg/model/combine.svg b/assets/icons/ui/design/svg/model/combine.svg new file mode 100644 index 00000000..ac6b4812 --- /dev/null +++ b/assets/icons/ui/design/svg/model/combine.svg @@ -0,0 +1,8 @@ + + Combine + + + + + + diff --git a/assets/icons/ui/design/svg/model/compare.svg b/assets/icons/ui/design/svg/model/compare.svg new file mode 100644 index 00000000..3b8a0888 --- /dev/null +++ b/assets/icons/ui/design/svg/model/compare.svg @@ -0,0 +1,19 @@ + + Compare bodies + + + + + + + + + + + + + + + + + diff --git a/assets/icons/ui/design/svg/model/component.svg b/assets/icons/ui/design/svg/model/component.svg new file mode 100644 index 00000000..a8a234b9 --- /dev/null +++ b/assets/icons/ui/design/svg/model/component.svg @@ -0,0 +1,21 @@ + + Component + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/ui/design/svg/model/compute.svg b/assets/icons/ui/design/svg/model/compute.svg new file mode 100644 index 00000000..6574da73 --- /dev/null +++ b/assets/icons/ui/design/svg/model/compute.svg @@ -0,0 +1,7 @@ + + Compute + + + + + diff --git a/assets/icons/ui/design/svg/model/configuration_table.svg b/assets/icons/ui/design/svg/model/configuration_table.svg new file mode 100644 index 00000000..5ca3c85d --- /dev/null +++ b/assets/icons/ui/design/svg/model/configuration_table.svg @@ -0,0 +1,13 @@ + + Configuration table + + + + + + + + + + + diff --git a/assets/icons/ui/design/svg/model/conic.svg b/assets/icons/ui/design/svg/model/conic.svg new file mode 100644 index 00000000..93fa3aee --- /dev/null +++ b/assets/icons/ui/design/svg/model/conic.svg @@ -0,0 +1,10 @@ + + Conic + + + + + + + + diff --git a/assets/icons/ui/design/svg/model/constrainer.svg b/assets/icons/ui/design/svg/model/constrainer.svg new file mode 100644 index 00000000..6ce489c1 --- /dev/null +++ b/assets/icons/ui/design/svg/model/constrainer.svg @@ -0,0 +1,9 @@ + + Constrainer + + + + + + + diff --git a/assets/icons/ui/design/svg/model/construction.svg b/assets/icons/ui/design/svg/model/construction.svg new file mode 100644 index 00000000..76d50988 --- /dev/null +++ b/assets/icons/ui/design/svg/model/construction.svg @@ -0,0 +1,7 @@ + + Construction + + + + + diff --git a/assets/icons/ui/design/svg/model/contact_all.svg b/assets/icons/ui/design/svg/model/contact_all.svg new file mode 100644 index 00000000..e7289101 --- /dev/null +++ b/assets/icons/ui/design/svg/model/contact_all.svg @@ -0,0 +1,23 @@ + + Enable all contact + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/ui/design/svg/model/contact_new.svg b/assets/icons/ui/design/svg/model/contact_new.svg new file mode 100644 index 00000000..5f330d34 --- /dev/null +++ b/assets/icons/ui/design/svg/model/contact_new.svg @@ -0,0 +1,20 @@ + + New contact set + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/ui/design/svg/model/contact_off.svg b/assets/icons/ui/design/svg/model/contact_off.svg new file mode 100644 index 00000000..0d52a321 --- /dev/null +++ b/assets/icons/ui/design/svg/model/contact_off.svg @@ -0,0 +1,17 @@ + + Disable contact + + + + + + + + + + + + + + + diff --git a/assets/icons/ui/design/svg/model/contact_sets.svg b/assets/icons/ui/design/svg/model/contact_sets.svg new file mode 100644 index 00000000..58e4d266 --- /dev/null +++ b/assets/icons/ui/design/svg/model/contact_sets.svg @@ -0,0 +1,17 @@ + + Enable contact sets + + + + + + + + + + + + + + + diff --git a/assets/icons/ui/design/svg/model/convert_sheet.svg b/assets/icons/ui/design/svg/model/convert_sheet.svg new file mode 100644 index 00000000..6fb830ff --- /dev/null +++ b/assets/icons/ui/design/svg/model/convert_sheet.svg @@ -0,0 +1,17 @@ + + Convert to sheet + + + + + + + + + + + + + + + diff --git a/assets/icons/ui/design/svg/model/copy.svg b/assets/icons/ui/design/svg/model/copy.svg new file mode 100644 index 00000000..d852d786 --- /dev/null +++ b/assets/icons/ui/design/svg/model/copy.svg @@ -0,0 +1,6 @@ + + Copy + + + + diff --git a/assets/icons/ui/design/svg/model/curvature_comb.svg b/assets/icons/ui/design/svg/model/curvature_comb.svg new file mode 100644 index 00000000..7fda58d7 --- /dev/null +++ b/assets/icons/ui/design/svg/model/curvature_comb.svg @@ -0,0 +1,9 @@ + + Curvature comb + + + + + + + diff --git a/assets/icons/ui/design/svg/model/curvature_map.svg b/assets/icons/ui/design/svg/model/curvature_map.svg new file mode 100644 index 00000000..7dea6fd7 --- /dev/null +++ b/assets/icons/ui/design/svg/model/curvature_map.svg @@ -0,0 +1,11 @@ + + Curvature map + + + + + + + + + diff --git a/assets/icons/ui/design/svg/model/cut.svg b/assets/icons/ui/design/svg/model/cut.svg new file mode 100644 index 00000000..23ceb27b --- /dev/null +++ b/assets/icons/ui/design/svg/model/cut.svg @@ -0,0 +1,11 @@ + + Cut + + + + + + + + + diff --git a/assets/icons/ui/design/svg/model/cylinder.svg b/assets/icons/ui/design/svg/model/cylinder.svg new file mode 100644 index 00000000..14ebfae2 --- /dev/null +++ b/assets/icons/ui/design/svg/model/cylinder.svg @@ -0,0 +1,7 @@ + + Cylinder + + + + + diff --git a/assets/icons/ui/design/svg/model/decal.svg b/assets/icons/ui/design/svg/model/decal.svg new file mode 100644 index 00000000..7d8fb131 --- /dev/null +++ b/assets/icons/ui/design/svg/model/decal.svg @@ -0,0 +1,9 @@ + + Decal + + + + + + + diff --git a/assets/icons/ui/design/svg/model/delete.svg b/assets/icons/ui/design/svg/model/delete.svg new file mode 100644 index 00000000..2d9f555f --- /dev/null +++ b/assets/icons/ui/design/svg/model/delete.svg @@ -0,0 +1,6 @@ + + Delete + + + + diff --git a/assets/icons/ui/design/svg/model/delete_face.svg b/assets/icons/ui/design/svg/model/delete_face.svg new file mode 100644 index 00000000..abc186cd --- /dev/null +++ b/assets/icons/ui/design/svg/model/delete_face.svg @@ -0,0 +1,9 @@ + + Delete face + + + + + + + diff --git a/assets/icons/ui/design/svg/model/dimension.svg b/assets/icons/ui/design/svg/model/dimension.svg new file mode 100644 index 00000000..fb51381d --- /dev/null +++ b/assets/icons/ui/design/svg/model/dimension.svg @@ -0,0 +1,7 @@ + + Dimension + + + + + diff --git a/assets/icons/ui/design/svg/model/dots.svg b/assets/icons/ui/design/svg/model/dots.svg new file mode 100644 index 00000000..0aa05f31 --- /dev/null +++ b/assets/icons/ui/design/svg/model/dots.svg @@ -0,0 +1,6 @@ + + More + + + + diff --git a/assets/icons/ui/design/svg/model/draft.svg b/assets/icons/ui/design/svg/model/draft.svg new file mode 100644 index 00000000..7be7ac78 --- /dev/null +++ b/assets/icons/ui/design/svg/model/draft.svg @@ -0,0 +1,11 @@ + + Draft + + + + + + + + + diff --git a/assets/icons/ui/design/svg/model/draft_analysis.svg b/assets/icons/ui/design/svg/model/draft_analysis.svg new file mode 100644 index 00000000..e30203b0 --- /dev/null +++ b/assets/icons/ui/design/svg/model/draft_analysis.svg @@ -0,0 +1,5 @@ + + Draft analysis + + + diff --git a/assets/icons/ui/design/svg/model/drive.svg b/assets/icons/ui/design/svg/model/drive.svg new file mode 100644 index 00000000..ae99fabc --- /dev/null +++ b/assets/icons/ui/design/svg/model/drive.svg @@ -0,0 +1,8 @@ + + Drive + + + + + + diff --git a/assets/icons/ui/design/svg/model/dxf.svg b/assets/icons/ui/design/svg/model/dxf.svg new file mode 100644 index 00000000..9983efb6 --- /dev/null +++ b/assets/icons/ui/design/svg/model/dxf.svg @@ -0,0 +1,8 @@ + + DXF + + + + + + diff --git a/assets/icons/ui/design/svg/model/edge_continuity.svg b/assets/icons/ui/design/svg/model/edge_continuity.svg new file mode 100644 index 00000000..52662c54 --- /dev/null +++ b/assets/icons/ui/design/svg/model/edge_continuity.svg @@ -0,0 +1,13 @@ + + Continuity + + + + + + + + + + + diff --git a/assets/icons/ui/design/svg/model/edit.svg b/assets/icons/ui/design/svg/model/edit.svg new file mode 100644 index 00000000..8e408543 --- /dev/null +++ b/assets/icons/ui/design/svg/model/edit.svg @@ -0,0 +1,11 @@ + + Edit + + + + + + + + + diff --git a/assets/icons/ui/design/svg/model/ellipse.svg b/assets/icons/ui/design/svg/model/ellipse.svg new file mode 100644 index 00000000..c42fb91f --- /dev/null +++ b/assets/icons/ui/design/svg/model/ellipse.svg @@ -0,0 +1,10 @@ + + Ellipse + + + + + + + + diff --git a/assets/icons/ui/design/svg/model/emboss.svg b/assets/icons/ui/design/svg/model/emboss.svg new file mode 100644 index 00000000..6c9ec1c3 --- /dev/null +++ b/assets/icons/ui/design/svg/model/emboss.svg @@ -0,0 +1,27 @@ + + Emboss + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/ui/design/svg/model/environment_map.svg b/assets/icons/ui/design/svg/model/environment_map.svg new file mode 100644 index 00000000..bd2a7b9c --- /dev/null +++ b/assets/icons/ui/design/svg/model/environment_map.svg @@ -0,0 +1,11 @@ + + Environment map + + + + + + + + + diff --git a/assets/icons/ui/design/svg/model/explode.svg b/assets/icons/ui/design/svg/model/explode.svg new file mode 100644 index 00000000..6cdd88f8 --- /dev/null +++ b/assets/icons/ui/design/svg/model/explode.svg @@ -0,0 +1,21 @@ + + Explode + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/ui/design/svg/model/export.svg b/assets/icons/ui/design/svg/model/export.svg new file mode 100644 index 00000000..c9237360 --- /dev/null +++ b/assets/icons/ui/design/svg/model/export.svg @@ -0,0 +1,13 @@ + + Export + + + + + + + + + + + diff --git a/assets/icons/ui/design/svg/model/extend.svg b/assets/icons/ui/design/svg/model/extend.svg new file mode 100644 index 00000000..c6be2583 --- /dev/null +++ b/assets/icons/ui/design/svg/model/extend.svg @@ -0,0 +1,7 @@ + + Extend + + + + + diff --git a/assets/icons/ui/design/svg/model/extrude.svg b/assets/icons/ui/design/svg/model/extrude.svg new file mode 100644 index 00000000..1a1e941c --- /dev/null +++ b/assets/icons/ui/design/svg/model/extrude.svg @@ -0,0 +1,13 @@ + + Extrude + + + + + + + + + + + diff --git a/assets/icons/ui/design/svg/model/eye.svg b/assets/icons/ui/design/svg/model/eye.svg new file mode 100644 index 00000000..a7cc82f2 --- /dev/null +++ b/assets/icons/ui/design/svg/model/eye.svg @@ -0,0 +1,8 @@ + + Eye + + + + + + diff --git a/assets/icons/ui/design/svg/model/fastener.svg b/assets/icons/ui/design/svg/model/fastener.svg new file mode 100644 index 00000000..f62b59f0 --- /dev/null +++ b/assets/icons/ui/design/svg/model/fastener.svg @@ -0,0 +1,14 @@ + + Fastener + + + + + + + + + + + + diff --git a/assets/icons/ui/design/svg/model/fillet.svg b/assets/icons/ui/design/svg/model/fillet.svg new file mode 100644 index 00000000..9114caaa --- /dev/null +++ b/assets/icons/ui/design/svg/model/fillet.svg @@ -0,0 +1,13 @@ + + Fillet + + + + + + + + + + + diff --git a/assets/icons/ui/design/svg/model/filter.svg b/assets/icons/ui/design/svg/model/filter.svg new file mode 100644 index 00000000..b1c102a6 --- /dev/null +++ b/assets/icons/ui/design/svg/model/filter.svg @@ -0,0 +1,8 @@ + + Filter + + + + + + diff --git a/assets/icons/ui/design/svg/model/finish.svg b/assets/icons/ui/design/svg/model/finish.svg new file mode 100644 index 00000000..74c0bbb0 --- /dev/null +++ b/assets/icons/ui/design/svg/model/finish.svg @@ -0,0 +1,4 @@ + + Finish + + diff --git a/assets/icons/ui/design/svg/model/fit_section.svg b/assets/icons/ui/design/svg/model/fit_section.svg new file mode 100644 index 00000000..b534bd9d --- /dev/null +++ b/assets/icons/ui/design/svg/model/fit_section.svg @@ -0,0 +1,7 @@ + + Fit curves to section + + + + + diff --git a/assets/icons/ui/design/svg/model/flange.svg b/assets/icons/ui/design/svg/model/flange.svg new file mode 100644 index 00000000..60908314 --- /dev/null +++ b/assets/icons/ui/design/svg/model/flange.svg @@ -0,0 +1,15 @@ + + Flange + + + + + + + + + + + + + diff --git a/assets/icons/ui/design/svg/model/flat.svg b/assets/icons/ui/design/svg/model/flat.svg new file mode 100644 index 00000000..16e28039 --- /dev/null +++ b/assets/icons/ui/design/svg/model/flat.svg @@ -0,0 +1,11 @@ + + Flat pattern + + + + + + + + + diff --git a/assets/icons/ui/design/svg/model/fold.svg b/assets/icons/ui/design/svg/model/fold.svg new file mode 100644 index 00000000..f12115a1 --- /dev/null +++ b/assets/icons/ui/design/svg/model/fold.svg @@ -0,0 +1,6 @@ + + Fold + + + + diff --git a/assets/icons/ui/design/svg/model/folder.svg b/assets/icons/ui/design/svg/model/folder.svg new file mode 100644 index 00000000..ddf06b79 --- /dev/null +++ b/assets/icons/ui/design/svg/model/folder.svg @@ -0,0 +1,5 @@ + + Folder + + + diff --git a/assets/icons/ui/design/svg/model/gear.svg b/assets/icons/ui/design/svg/model/gear.svg new file mode 100644 index 00000000..90e490a6 --- /dev/null +++ b/assets/icons/ui/design/svg/model/gear.svg @@ -0,0 +1,105 @@ + + Gear coupling + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/ui/design/svg/model/graph.svg b/assets/icons/ui/design/svg/model/graph.svg new file mode 100644 index 00000000..215ce755 --- /dev/null +++ b/assets/icons/ui/design/svg/model/graph.svg @@ -0,0 +1,22 @@ + + Dependency graph + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/ui/design/svg/model/hem.svg b/assets/icons/ui/design/svg/model/hem.svg new file mode 100644 index 00000000..72cd1808 --- /dev/null +++ b/assets/icons/ui/design/svg/model/hem.svg @@ -0,0 +1,6 @@ + + Hem + + + + diff --git a/assets/icons/ui/design/svg/model/hole.svg b/assets/icons/ui/design/svg/model/hole.svg new file mode 100644 index 00000000..724dd184 --- /dev/null +++ b/assets/icons/ui/design/svg/model/hole.svg @@ -0,0 +1,12 @@ + + Hole + + + + + + + + + + diff --git a/assets/icons/ui/design/svg/model/import.svg b/assets/icons/ui/design/svg/model/import.svg new file mode 100644 index 00000000..f546339c --- /dev/null +++ b/assets/icons/ui/design/svg/model/import.svg @@ -0,0 +1,13 @@ + + Import + + + + + + + + + + + diff --git a/assets/icons/ui/design/svg/model/include.svg b/assets/icons/ui/design/svg/model/include.svg new file mode 100644 index 00000000..6315c5b2 --- /dev/null +++ b/assets/icons/ui/design/svg/model/include.svg @@ -0,0 +1,15 @@ + + Include + + + + + + + + + + + + + diff --git a/assets/icons/ui/design/svg/model/infinite.svg b/assets/icons/ui/design/svg/model/infinite.svg new file mode 100644 index 00000000..55bca1e9 --- /dev/null +++ b/assets/icons/ui/design/svg/model/infinite.svg @@ -0,0 +1,9 @@ + + Infinite line + + + + + + + diff --git a/assets/icons/ui/design/svg/model/inspect.svg b/assets/icons/ui/design/svg/model/inspect.svg new file mode 100644 index 00000000..ddbc3969 --- /dev/null +++ b/assets/icons/ui/design/svg/model/inspect.svg @@ -0,0 +1,6 @@ + + Inspect + + + + diff --git a/assets/icons/ui/design/svg/model/interference.svg b/assets/icons/ui/design/svg/model/interference.svg new file mode 100644 index 00000000..0110d551 --- /dev/null +++ b/assets/icons/ui/design/svg/model/interference.svg @@ -0,0 +1,17 @@ + + Interference + + + + + + + + + + + + + + + diff --git a/assets/icons/ui/design/svg/model/intersect.svg b/assets/icons/ui/design/svg/model/intersect.svg new file mode 100644 index 00000000..82150ef5 --- /dev/null +++ b/assets/icons/ui/design/svg/model/intersect.svg @@ -0,0 +1,8 @@ + + Intersect + + + + + + diff --git a/assets/icons/ui/design/svg/model/intersection_curve.svg b/assets/icons/ui/design/svg/model/intersection_curve.svg new file mode 100644 index 00000000..62032307 --- /dev/null +++ b/assets/icons/ui/design/svg/model/intersection_curve.svg @@ -0,0 +1,9 @@ + + Intersection curve + + + + + + + diff --git a/assets/icons/ui/design/svg/model/iso_analysis.svg b/assets/icons/ui/design/svg/model/iso_analysis.svg new file mode 100644 index 00000000..291214f8 --- /dev/null +++ b/assets/icons/ui/design/svg/model/iso_analysis.svg @@ -0,0 +1,16 @@ + + Isoparametric analysis + + + + + + + + + + + + + + diff --git a/assets/icons/ui/design/svg/model/iso_curve.svg b/assets/icons/ui/design/svg/model/iso_curve.svg new file mode 100644 index 00000000..4be4f4a0 --- /dev/null +++ b/assets/icons/ui/design/svg/model/iso_curve.svg @@ -0,0 +1,10 @@ + + Isoparametric curve + + + + + + + + diff --git a/assets/icons/ui/design/svg/model/joint.svg b/assets/icons/ui/design/svg/model/joint.svg new file mode 100644 index 00000000..ecf0b757 --- /dev/null +++ b/assets/icons/ui/design/svg/model/joint.svg @@ -0,0 +1,17 @@ + + Joint + + + + + + + + + + + + + + + diff --git a/assets/icons/ui/design/svg/model/joint_origin.svg b/assets/icons/ui/design/svg/model/joint_origin.svg new file mode 100644 index 00000000..aa8114d0 --- /dev/null +++ b/assets/icons/ui/design/svg/model/joint_origin.svg @@ -0,0 +1,17 @@ + + Joint origin + + + + + + + + + + + + + + + diff --git a/assets/icons/ui/design/svg/model/library.svg b/assets/icons/ui/design/svg/model/library.svg new file mode 100644 index 00000000..1823d65a --- /dev/null +++ b/assets/icons/ui/design/svg/model/library.svg @@ -0,0 +1,13 @@ + + Library + + + + + + + + + + + diff --git a/assets/icons/ui/design/svg/model/line.svg b/assets/icons/ui/design/svg/model/line.svg new file mode 100644 index 00000000..d736f704 --- /dev/null +++ b/assets/icons/ui/design/svg/model/line.svg @@ -0,0 +1,8 @@ + + Line + + + + + + diff --git a/assets/icons/ui/design/svg/model/line_mid.svg b/assets/icons/ui/design/svg/model/line_mid.svg new file mode 100644 index 00000000..701755e8 --- /dev/null +++ b/assets/icons/ui/design/svg/model/line_mid.svg @@ -0,0 +1,8 @@ + + Midpoint line + + + + + + diff --git a/assets/icons/ui/design/svg/model/lip.svg b/assets/icons/ui/design/svg/model/lip.svg new file mode 100644 index 00000000..7e436a90 --- /dev/null +++ b/assets/icons/ui/design/svg/model/lip.svg @@ -0,0 +1,19 @@ + + Lip + + + + + + + + + + + + + + + + + diff --git a/assets/icons/ui/design/svg/model/list.svg b/assets/icons/ui/design/svg/model/list.svg new file mode 100644 index 00000000..36de1e9c --- /dev/null +++ b/assets/icons/ui/design/svg/model/list.svg @@ -0,0 +1,9 @@ + + List + + + + + + + diff --git a/assets/icons/ui/design/svg/model/loft.svg b/assets/icons/ui/design/svg/model/loft.svg new file mode 100644 index 00000000..21504fb7 --- /dev/null +++ b/assets/icons/ui/design/svg/model/loft.svg @@ -0,0 +1,9 @@ + + Loft + + + + + + + diff --git a/assets/icons/ui/design/svg/model/material.svg b/assets/icons/ui/design/svg/model/material.svg new file mode 100644 index 00000000..93b2638f --- /dev/null +++ b/assets/icons/ui/design/svg/model/material.svg @@ -0,0 +1,7 @@ + + Material + + + + + diff --git a/assets/icons/ui/design/svg/model/measure.svg b/assets/icons/ui/design/svg/model/measure.svg new file mode 100644 index 00000000..4a99e335 --- /dev/null +++ b/assets/icons/ui/design/svg/model/measure.svg @@ -0,0 +1,9 @@ + + Measure + + + + + + + diff --git a/assets/icons/ui/design/svg/model/min_radius.svg b/assets/icons/ui/design/svg/model/min_radius.svg new file mode 100644 index 00000000..f32d1d2d --- /dev/null +++ b/assets/icons/ui/design/svg/model/min_radius.svg @@ -0,0 +1,33 @@ + + Minimum radius + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/ui/design/svg/model/mirror.svg b/assets/icons/ui/design/svg/model/mirror.svg new file mode 100644 index 00000000..7e820785 --- /dev/null +++ b/assets/icons/ui/design/svg/model/mirror.svg @@ -0,0 +1,8 @@ + + Mirror + + + + + + diff --git a/assets/icons/ui/design/svg/model/motion_link.svg b/assets/icons/ui/design/svg/model/motion_link.svg new file mode 100644 index 00000000..5fa9c626 --- /dev/null +++ b/assets/icons/ui/design/svg/model/motion_link.svg @@ -0,0 +1,19 @@ + + Motion link + + + + + + + + + + + + + + + + + diff --git a/assets/icons/ui/design/svg/model/move.svg b/assets/icons/ui/design/svg/model/move.svg new file mode 100644 index 00000000..bfa51774 --- /dev/null +++ b/assets/icons/ui/design/svg/model/move.svg @@ -0,0 +1,8 @@ + + Move + + + + + + diff --git a/assets/icons/ui/design/svg/model/new.svg b/assets/icons/ui/design/svg/model/new.svg new file mode 100644 index 00000000..1c765e9b --- /dev/null +++ b/assets/icons/ui/design/svg/model/new.svg @@ -0,0 +1,7 @@ + + New + + + + + diff --git a/assets/icons/ui/design/svg/model/normal.svg b/assets/icons/ui/design/svg/model/normal.svg new file mode 100644 index 00000000..97f3d7fb --- /dev/null +++ b/assets/icons/ui/design/svg/model/normal.svg @@ -0,0 +1,4 @@ + + Normal line + + diff --git a/assets/icons/ui/design/svg/model/offset.svg b/assets/icons/ui/design/svg/model/offset.svg new file mode 100644 index 00000000..16eaedd5 --- /dev/null +++ b/assets/icons/ui/design/svg/model/offset.svg @@ -0,0 +1,5 @@ + + Offset + + + diff --git a/assets/icons/ui/design/svg/model/offset_face.svg b/assets/icons/ui/design/svg/model/offset_face.svg new file mode 100644 index 00000000..d3d90f55 --- /dev/null +++ b/assets/icons/ui/design/svg/model/offset_face.svg @@ -0,0 +1,17 @@ + + Offset face + + + + + + + + + + + + + + + diff --git a/assets/icons/ui/design/svg/model/offset_surface.svg b/assets/icons/ui/design/svg/model/offset_surface.svg new file mode 100644 index 00000000..87328307 --- /dev/null +++ b/assets/icons/ui/design/svg/model/offset_surface.svg @@ -0,0 +1,6 @@ + + Offset surface + + + + diff --git a/assets/icons/ui/design/svg/model/open.svg b/assets/icons/ui/design/svg/model/open.svg new file mode 100644 index 00000000..5bb3d7c6 --- /dev/null +++ b/assets/icons/ui/design/svg/model/open.svg @@ -0,0 +1,5 @@ + + Open + + + diff --git a/assets/icons/ui/design/svg/model/origin.svg b/assets/icons/ui/design/svg/model/origin.svg new file mode 100644 index 00000000..1379189c --- /dev/null +++ b/assets/icons/ui/design/svg/model/origin.svg @@ -0,0 +1,15 @@ + + Origin + + + + + + + + + + + + + diff --git a/assets/icons/ui/design/svg/model/params.svg b/assets/icons/ui/design/svg/model/params.svg new file mode 100644 index 00000000..774c81db --- /dev/null +++ b/assets/icons/ui/design/svg/model/params.svg @@ -0,0 +1,8 @@ + + Params + + + + + + diff --git a/assets/icons/ui/design/svg/model/paste.svg b/assets/icons/ui/design/svg/model/paste.svg new file mode 100644 index 00000000..7f3d641b --- /dev/null +++ b/assets/icons/ui/design/svg/model/paste.svg @@ -0,0 +1,6 @@ + + Paste + + + + diff --git a/assets/icons/ui/design/svg/model/patch.svg b/assets/icons/ui/design/svg/model/patch.svg new file mode 100644 index 00000000..963c858b --- /dev/null +++ b/assets/icons/ui/design/svg/model/patch.svg @@ -0,0 +1,10 @@ + + Patch + + + + + + + + diff --git a/assets/icons/ui/design/svg/model/pattern.svg b/assets/icons/ui/design/svg/model/pattern.svg new file mode 100644 index 00000000..79d9b442 --- /dev/null +++ b/assets/icons/ui/design/svg/model/pattern.svg @@ -0,0 +1,27 @@ + + Pattern + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/ui/design/svg/model/pattern_center.svg b/assets/icons/ui/design/svg/model/pattern_center.svg new file mode 100644 index 00000000..d73aee55 --- /dev/null +++ b/assets/icons/ui/design/svg/model/pattern_center.svg @@ -0,0 +1,8 @@ + + Centre pattern + + + + + + diff --git a/assets/icons/ui/design/svg/model/pattern_circ.svg b/assets/icons/ui/design/svg/model/pattern_circ.svg new file mode 100644 index 00000000..502c1618 --- /dev/null +++ b/assets/icons/ui/design/svg/model/pattern_circ.svg @@ -0,0 +1,39 @@ + + Circular pattern + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/ui/design/svg/model/pattern_flip.svg b/assets/icons/ui/design/svg/model/pattern_flip.svg new file mode 100644 index 00000000..d9ff6e05 --- /dev/null +++ b/assets/icons/ui/design/svg/model/pattern_flip.svg @@ -0,0 +1,13 @@ + + Reverse pattern direction + + + + + + + + + + + diff --git a/assets/icons/ui/design/svg/model/pattern_path.svg b/assets/icons/ui/design/svg/model/pattern_path.svg new file mode 100644 index 00000000..7a2cfdaf --- /dev/null +++ b/assets/icons/ui/design/svg/model/pattern_path.svg @@ -0,0 +1,26 @@ + + Path pattern + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/ui/design/svg/model/pattern_rect.svg b/assets/icons/ui/design/svg/model/pattern_rect.svg new file mode 100644 index 00000000..eec8db31 --- /dev/null +++ b/assets/icons/ui/design/svg/model/pattern_rect.svg @@ -0,0 +1,51 @@ + + Rectangular pattern + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/ui/design/svg/model/perforate.svg b/assets/icons/ui/design/svg/model/perforate.svg new file mode 100644 index 00000000..b9b51779 --- /dev/null +++ b/assets/icons/ui/design/svg/model/perforate.svg @@ -0,0 +1,21 @@ + + Perforate + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/ui/design/svg/model/pipe.svg b/assets/icons/ui/design/svg/model/pipe.svg new file mode 100644 index 00000000..7354e29e --- /dev/null +++ b/assets/icons/ui/design/svg/model/pipe.svg @@ -0,0 +1,9 @@ + + Pipe + + + + + + + diff --git a/assets/icons/ui/design/svg/model/placeholder.svg b/assets/icons/ui/design/svg/model/placeholder.svg new file mode 100644 index 00000000..023f4110 --- /dev/null +++ b/assets/icons/ui/design/svg/model/placeholder.svg @@ -0,0 +1,5 @@ + + Placeholder + + + diff --git a/assets/icons/ui/design/svg/model/plane.svg b/assets/icons/ui/design/svg/model/plane.svg new file mode 100644 index 00000000..ed011639 --- /dev/null +++ b/assets/icons/ui/design/svg/model/plane.svg @@ -0,0 +1,11 @@ + + Plane + + + + + + + + + diff --git a/assets/icons/ui/design/svg/model/plane_along_path.svg b/assets/icons/ui/design/svg/model/plane_along_path.svg new file mode 100644 index 00000000..193935e4 --- /dev/null +++ b/assets/icons/ui/design/svg/model/plane_along_path.svg @@ -0,0 +1,6 @@ + + Plane along path + + + + diff --git a/assets/icons/ui/design/svg/model/plane_angle.svg b/assets/icons/ui/design/svg/model/plane_angle.svg new file mode 100644 index 00000000..372ea846 --- /dev/null +++ b/assets/icons/ui/design/svg/model/plane_angle.svg @@ -0,0 +1,11 @@ + + Plane angle + + + + + + + + + diff --git a/assets/icons/ui/design/svg/model/plane_midplane.svg b/assets/icons/ui/design/svg/model/plane_midplane.svg new file mode 100644 index 00000000..92721a38 --- /dev/null +++ b/assets/icons/ui/design/svg/model/plane_midplane.svg @@ -0,0 +1,6 @@ + + Midplane + + + + diff --git a/assets/icons/ui/design/svg/model/plane_perpendicular.svg b/assets/icons/ui/design/svg/model/plane_perpendicular.svg new file mode 100644 index 00000000..d205e87d --- /dev/null +++ b/assets/icons/ui/design/svg/model/plane_perpendicular.svg @@ -0,0 +1,7 @@ + + Perpendicular plane + + + + + diff --git a/assets/icons/ui/design/svg/model/plane_tangent.svg b/assets/icons/ui/design/svg/model/plane_tangent.svg new file mode 100644 index 00000000..5f6d3c81 --- /dev/null +++ b/assets/icons/ui/design/svg/model/plane_tangent.svg @@ -0,0 +1,8 @@ + + Tangent plane + + + + + + diff --git a/assets/icons/ui/design/svg/model/plane_three_points.svg b/assets/icons/ui/design/svg/model/plane_three_points.svg new file mode 100644 index 00000000..a73ba67c --- /dev/null +++ b/assets/icons/ui/design/svg/model/plane_three_points.svg @@ -0,0 +1,7 @@ + + Plane through three points + + + + + diff --git a/assets/icons/ui/design/svg/model/plane_two_edges.svg b/assets/icons/ui/design/svg/model/plane_two_edges.svg new file mode 100644 index 00000000..89e80fe6 --- /dev/null +++ b/assets/icons/ui/design/svg/model/plane_two_edges.svg @@ -0,0 +1,6 @@ + + Plane through two edges + + + + diff --git a/assets/icons/ui/design/svg/model/plastic_assign.svg b/assets/icons/ui/design/svg/model/plastic_assign.svg new file mode 100644 index 00000000..3d97feee --- /dev/null +++ b/assets/icons/ui/design/svg/model/plastic_assign.svg @@ -0,0 +1,19 @@ + + Plastic assign + + + + + + + + + + + + + + + + + diff --git a/assets/icons/ui/design/svg/model/plastic_rule.svg b/assets/icons/ui/design/svg/model/plastic_rule.svg new file mode 100644 index 00000000..37e96312 --- /dev/null +++ b/assets/icons/ui/design/svg/model/plastic_rule.svg @@ -0,0 +1,21 @@ + + Plastic rule + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/ui/design/svg/model/play.svg b/assets/icons/ui/design/svg/model/play.svg new file mode 100644 index 00000000..331eb0c3 --- /dev/null +++ b/assets/icons/ui/design/svg/model/play.svg @@ -0,0 +1,6 @@ + + Play + + + + diff --git a/assets/icons/ui/design/svg/model/point.svg b/assets/icons/ui/design/svg/model/point.svg new file mode 100644 index 00000000..109c5fef --- /dev/null +++ b/assets/icons/ui/design/svg/model/point.svg @@ -0,0 +1,6 @@ + + Point + + + + diff --git a/assets/icons/ui/design/svg/model/point_along_path.svg b/assets/icons/ui/design/svg/model/point_along_path.svg new file mode 100644 index 00000000..37b4bd66 --- /dev/null +++ b/assets/icons/ui/design/svg/model/point_along_path.svg @@ -0,0 +1,6 @@ + + Point along path + + + + diff --git a/assets/icons/ui/design/svg/model/point_center.svg b/assets/icons/ui/design/svg/model/point_center.svg new file mode 100644 index 00000000..0a2f6520 --- /dev/null +++ b/assets/icons/ui/design/svg/model/point_center.svg @@ -0,0 +1,9 @@ + + Point at circle, sphere or torus centre + + + + + + + diff --git a/assets/icons/ui/design/svg/model/point_edge_plane.svg b/assets/icons/ui/design/svg/model/point_edge_plane.svg new file mode 100644 index 00000000..8b8b9303 --- /dev/null +++ b/assets/icons/ui/design/svg/model/point_edge_plane.svg @@ -0,0 +1,9 @@ + + Point at edge and plane + + + + + + + diff --git a/assets/icons/ui/design/svg/model/point_three_planes.svg b/assets/icons/ui/design/svg/model/point_three_planes.svg new file mode 100644 index 00000000..e81954b8 --- /dev/null +++ b/assets/icons/ui/design/svg/model/point_three_planes.svg @@ -0,0 +1,33 @@ + + Point through three planes + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/ui/design/svg/model/point_two_edges.svg b/assets/icons/ui/design/svg/model/point_two_edges.svg new file mode 100644 index 00000000..b4844e88 --- /dev/null +++ b/assets/icons/ui/design/svg/model/point_two_edges.svg @@ -0,0 +1,6 @@ + + Point through two edges + + + + diff --git a/assets/icons/ui/design/svg/model/point_vertex.svg b/assets/icons/ui/design/svg/model/point_vertex.svg new file mode 100644 index 00000000..0720e9cd --- /dev/null +++ b/assets/icons/ui/design/svg/model/point_vertex.svg @@ -0,0 +1,11 @@ + + Point at vertex + + + + + + + + + diff --git a/assets/icons/ui/design/svg/model/polygon.svg b/assets/icons/ui/design/svg/model/polygon.svg new file mode 100644 index 00000000..40fe1f5f --- /dev/null +++ b/assets/icons/ui/design/svg/model/polygon.svg @@ -0,0 +1,9 @@ + + Polygon + + + + + + + diff --git a/assets/icons/ui/design/svg/model/polygon_circumscribed.svg b/assets/icons/ui/design/svg/model/polygon_circumscribed.svg new file mode 100644 index 00000000..f0b6c40d --- /dev/null +++ b/assets/icons/ui/design/svg/model/polygon_circumscribed.svg @@ -0,0 +1,11 @@ + + Circumscribed polygon + + + + + + + + + diff --git a/assets/icons/ui/design/svg/model/polygon_edge.svg b/assets/icons/ui/design/svg/model/polygon_edge.svg new file mode 100644 index 00000000..7126d3b8 --- /dev/null +++ b/assets/icons/ui/design/svg/model/polygon_edge.svg @@ -0,0 +1,8 @@ + + Edge polygon + + + + + + diff --git a/assets/icons/ui/design/svg/model/presspull.svg b/assets/icons/ui/design/svg/model/presspull.svg new file mode 100644 index 00000000..364306aa --- /dev/null +++ b/assets/icons/ui/design/svg/model/presspull.svg @@ -0,0 +1,17 @@ + + Press pull + + + + + + + + + + + + + + + diff --git a/assets/icons/ui/design/svg/model/project.svg b/assets/icons/ui/design/svg/model/project.svg new file mode 100644 index 00000000..f63b2478 --- /dev/null +++ b/assets/icons/ui/design/svg/model/project.svg @@ -0,0 +1,10 @@ + + Project + + + + + + + + diff --git a/assets/icons/ui/design/svg/model/project_surface.svg b/assets/icons/ui/design/svg/model/project_surface.svg new file mode 100644 index 00000000..4c82f7d4 --- /dev/null +++ b/assets/icons/ui/design/svg/model/project_surface.svg @@ -0,0 +1,10 @@ + + Project to surface + + + + + + + + diff --git a/assets/icons/ui/design/svg/model/rack_pinion.svg b/assets/icons/ui/design/svg/model/rack_pinion.svg new file mode 100644 index 00000000..9dcccb1f --- /dev/null +++ b/assets/icons/ui/design/svg/model/rack_pinion.svg @@ -0,0 +1,68 @@ + + Rack and pinion + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/ui/design/svg/model/reach.svg b/assets/icons/ui/design/svg/model/reach.svg new file mode 100644 index 00000000..7bb9ec1c --- /dev/null +++ b/assets/icons/ui/design/svg/model/reach.svg @@ -0,0 +1,13 @@ + + Reach point + + + + + + + + + + + diff --git a/assets/icons/ui/design/svg/model/rect.svg b/assets/icons/ui/design/svg/model/rect.svg new file mode 100644 index 00000000..cd4b2ef2 --- /dev/null +++ b/assets/icons/ui/design/svg/model/rect.svg @@ -0,0 +1,8 @@ + + Two-point rectangle + + + + + + diff --git a/assets/icons/ui/design/svg/model/rect3.svg b/assets/icons/ui/design/svg/model/rect3.svg new file mode 100644 index 00000000..9296adcc --- /dev/null +++ b/assets/icons/ui/design/svg/model/rect3.svg @@ -0,0 +1,10 @@ + + Three-point rectangle + + + + + + + + diff --git a/assets/icons/ui/design/svg/model/rect_center.svg b/assets/icons/ui/design/svg/model/rect_center.svg new file mode 100644 index 00000000..34eebc71 --- /dev/null +++ b/assets/icons/ui/design/svg/model/rect_center.svg @@ -0,0 +1,8 @@ + + Centre rectangle + + + + + + diff --git a/assets/icons/ui/design/svg/model/rect_rounded.svg b/assets/icons/ui/design/svg/model/rect_rounded.svg new file mode 100644 index 00000000..6d0e6be3 --- /dev/null +++ b/assets/icons/ui/design/svg/model/rect_rounded.svg @@ -0,0 +1,8 @@ + + Rounded rectangle + + + + + + diff --git a/assets/icons/ui/design/svg/model/refold.svg b/assets/icons/ui/design/svg/model/refold.svg new file mode 100644 index 00000000..c9d0ba84 --- /dev/null +++ b/assets/icons/ui/design/svg/model/refold.svg @@ -0,0 +1,11 @@ + + Refold + + + + + + + + + diff --git a/assets/icons/ui/design/svg/model/rename.svg b/assets/icons/ui/design/svg/model/rename.svg new file mode 100644 index 00000000..d2b22dcc --- /dev/null +++ b/assets/icons/ui/design/svg/model/rename.svg @@ -0,0 +1,8 @@ + + Rename + + + + + + diff --git a/assets/icons/ui/design/svg/model/render_image.svg b/assets/icons/ui/design/svg/model/render_image.svg new file mode 100644 index 00000000..e72ba3f9 --- /dev/null +++ b/assets/icons/ui/design/svg/model/render_image.svg @@ -0,0 +1,7 @@ + + Render image + + + + + diff --git a/assets/icons/ui/design/svg/model/render_scene.svg b/assets/icons/ui/design/svg/model/render_scene.svg new file mode 100644 index 00000000..f33e351f --- /dev/null +++ b/assets/icons/ui/design/svg/model/render_scene.svg @@ -0,0 +1,10 @@ + + Render scene + + + + + + + + diff --git a/assets/icons/ui/design/svg/model/reorder.svg b/assets/icons/ui/design/svg/model/reorder.svg new file mode 100644 index 00000000..1d01186b --- /dev/null +++ b/assets/icons/ui/design/svg/model/reorder.svg @@ -0,0 +1,9 @@ + + Reorder + + + + + + + diff --git a/assets/icons/ui/design/svg/model/replace_face.svg b/assets/icons/ui/design/svg/model/replace_face.svg new file mode 100644 index 00000000..6da64a5f --- /dev/null +++ b/assets/icons/ui/design/svg/model/replace_face.svg @@ -0,0 +1,7 @@ + + Replace face + + + + + diff --git a/assets/icons/ui/design/svg/model/rest.svg b/assets/icons/ui/design/svg/model/rest.svg new file mode 100644 index 00000000..064fbf2b --- /dev/null +++ b/assets/icons/ui/design/svg/model/rest.svg @@ -0,0 +1,21 @@ + + Rest + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/ui/design/svg/model/restore.svg b/assets/icons/ui/design/svg/model/restore.svg new file mode 100644 index 00000000..d3c35c62 --- /dev/null +++ b/assets/icons/ui/design/svg/model/restore.svg @@ -0,0 +1,8 @@ + + Restore + + + + + + diff --git a/assets/icons/ui/design/svg/model/reverse_normal.svg b/assets/icons/ui/design/svg/model/reverse_normal.svg new file mode 100644 index 00000000..25e34e49 --- /dev/null +++ b/assets/icons/ui/design/svg/model/reverse_normal.svg @@ -0,0 +1,13 @@ + + Reverse normal + + + + + + + + + + + diff --git a/assets/icons/ui/design/svg/model/revert.svg b/assets/icons/ui/design/svg/model/revert.svg new file mode 100644 index 00000000..d6294492 --- /dev/null +++ b/assets/icons/ui/design/svg/model/revert.svg @@ -0,0 +1,13 @@ + + Revert position + + + + + + + + + + + diff --git a/assets/icons/ui/design/svg/model/revolve.svg b/assets/icons/ui/design/svg/model/revolve.svg new file mode 100644 index 00000000..3988c0e9 --- /dev/null +++ b/assets/icons/ui/design/svg/model/revolve.svg @@ -0,0 +1,13 @@ + + Revolve + + + + + + + + + + + diff --git a/assets/icons/ui/design/svg/model/rib.svg b/assets/icons/ui/design/svg/model/rib.svg new file mode 100644 index 00000000..898837dc --- /dev/null +++ b/assets/icons/ui/design/svg/model/rib.svg @@ -0,0 +1,15 @@ + + Rib + + + + + + + + + + + + + diff --git a/assets/icons/ui/design/svg/model/rigid_group.svg b/assets/icons/ui/design/svg/model/rigid_group.svg new file mode 100644 index 00000000..01c98136 --- /dev/null +++ b/assets/icons/ui/design/svg/model/rigid_group.svg @@ -0,0 +1,17 @@ + + Rigid group + + + + + + + + + + + + + + + diff --git a/assets/icons/ui/design/svg/model/rollback.svg b/assets/icons/ui/design/svg/model/rollback.svg new file mode 100644 index 00000000..9cb6c8ed --- /dev/null +++ b/assets/icons/ui/design/svg/model/rollback.svg @@ -0,0 +1,11 @@ + + Rollback + + + + + + + + + diff --git a/assets/icons/ui/design/svg/model/ruled.svg b/assets/icons/ui/design/svg/model/ruled.svg new file mode 100644 index 00000000..77cd06f7 --- /dev/null +++ b/assets/icons/ui/design/svg/model/ruled.svg @@ -0,0 +1,14 @@ + + Ruled surface + + + + + + + + + + + + diff --git a/assets/icons/ui/design/svg/model/scale.svg b/assets/icons/ui/design/svg/model/scale.svg new file mode 100644 index 00000000..8895c8b9 --- /dev/null +++ b/assets/icons/ui/design/svg/model/scale.svg @@ -0,0 +1,14 @@ + + Scale + + + + + + + + + + + + diff --git a/assets/icons/ui/design/svg/model/screw.svg b/assets/icons/ui/design/svg/model/screw.svg new file mode 100644 index 00000000..18f44b8a --- /dev/null +++ b/assets/icons/ui/design/svg/model/screw.svg @@ -0,0 +1,27 @@ + + Screw coupling + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/ui/design/svg/model/section.svg b/assets/icons/ui/design/svg/model/section.svg new file mode 100644 index 00000000..2abfd404 --- /dev/null +++ b/assets/icons/ui/design/svg/model/section.svg @@ -0,0 +1,20 @@ + + Section + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/ui/design/svg/model/select.svg b/assets/icons/ui/design/svg/model/select.svg new file mode 100644 index 00000000..461b436e --- /dev/null +++ b/assets/icons/ui/design/svg/model/select.svg @@ -0,0 +1,4 @@ + + Select + + diff --git a/assets/icons/ui/design/svg/model/sheet.svg b/assets/icons/ui/design/svg/model/sheet.svg new file mode 100644 index 00000000..7f9ab301 --- /dev/null +++ b/assets/icons/ui/design/svg/model/sheet.svg @@ -0,0 +1,24 @@ + + Sheet metal + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/ui/design/svg/model/sheet_rules.svg b/assets/icons/ui/design/svg/model/sheet_rules.svg new file mode 100644 index 00000000..bc739199 --- /dev/null +++ b/assets/icons/ui/design/svg/model/sheet_rules.svg @@ -0,0 +1,15 @@ + + Sheet metal rules + + + + + + + + + + + + + diff --git a/assets/icons/ui/design/svg/model/shell.svg b/assets/icons/ui/design/svg/model/shell.svg new file mode 100644 index 00000000..efa79b1d --- /dev/null +++ b/assets/icons/ui/design/svg/model/shell.svg @@ -0,0 +1,13 @@ + + Shell + + + + + + + + + + + diff --git a/assets/icons/ui/design/svg/model/sketch.svg b/assets/icons/ui/design/svg/model/sketch.svg new file mode 100644 index 00000000..24a240bc --- /dev/null +++ b/assets/icons/ui/design/svg/model/sketch.svg @@ -0,0 +1,9 @@ + + Sketch + + + + + + + diff --git a/assets/icons/ui/design/svg/model/sketch_chamfer.svg b/assets/icons/ui/design/svg/model/sketch_chamfer.svg new file mode 100644 index 00000000..bcbeabdb --- /dev/null +++ b/assets/icons/ui/design/svg/model/sketch_chamfer.svg @@ -0,0 +1,7 @@ + + Sketch chamfer + + + + + diff --git a/assets/icons/ui/design/svg/model/sketch_chamfer_angle.svg b/assets/icons/ui/design/svg/model/sketch_chamfer_angle.svg new file mode 100644 index 00000000..bcebd81a --- /dev/null +++ b/assets/icons/ui/design/svg/model/sketch_chamfer_angle.svg @@ -0,0 +1,8 @@ + + Distance and angle sketch chamfer + + + + + + diff --git a/assets/icons/ui/design/svg/model/sketch_chamfer_two_distances.svg b/assets/icons/ui/design/svg/model/sketch_chamfer_two_distances.svg new file mode 100644 index 00000000..323fbd9e --- /dev/null +++ b/assets/icons/ui/design/svg/model/sketch_chamfer_two_distances.svg @@ -0,0 +1,8 @@ + + Two distance sketch chamfer + + + + + + diff --git a/assets/icons/ui/design/svg/model/sketch_fillet.svg b/assets/icons/ui/design/svg/model/sketch_fillet.svg new file mode 100644 index 00000000..85338d2c --- /dev/null +++ b/assets/icons/ui/design/svg/model/sketch_fillet.svg @@ -0,0 +1,7 @@ + + Sketch fillet + + + + + diff --git a/assets/icons/ui/design/svg/model/sketch_mirror.svg b/assets/icons/ui/design/svg/model/sketch_mirror.svg new file mode 100644 index 00000000..08aebdb0 --- /dev/null +++ b/assets/icons/ui/design/svg/model/sketch_mirror.svg @@ -0,0 +1,6 @@ + + Sketch mirror + + + + diff --git a/assets/icons/ui/design/svg/model/slot.svg b/assets/icons/ui/design/svg/model/slot.svg new file mode 100644 index 00000000..1a49bfd2 --- /dev/null +++ b/assets/icons/ui/design/svg/model/slot.svg @@ -0,0 +1,10 @@ + + Slot + + + + + + + + diff --git a/assets/icons/ui/design/svg/model/slot_center.svg b/assets/icons/ui/design/svg/model/slot_center.svg new file mode 100644 index 00000000..b6efd22f --- /dev/null +++ b/assets/icons/ui/design/svg/model/slot_center.svg @@ -0,0 +1,11 @@ + + Centre point slot + + + + + + + + + diff --git a/assets/icons/ui/design/svg/model/slot_overall.svg b/assets/icons/ui/design/svg/model/slot_overall.svg new file mode 100644 index 00000000..79a4057a --- /dev/null +++ b/assets/icons/ui/design/svg/model/slot_overall.svg @@ -0,0 +1,10 @@ + + Overall slot + + + + + + + + diff --git a/assets/icons/ui/design/svg/model/snap.svg b/assets/icons/ui/design/svg/model/snap.svg new file mode 100644 index 00000000..4435308d --- /dev/null +++ b/assets/icons/ui/design/svg/model/snap.svg @@ -0,0 +1,7 @@ + + Snap + + + + + diff --git a/assets/icons/ui/design/svg/model/snap_fit.svg b/assets/icons/ui/design/svg/model/snap_fit.svg new file mode 100644 index 00000000..3941682d --- /dev/null +++ b/assets/icons/ui/design/svg/model/snap_fit.svg @@ -0,0 +1,15 @@ + + Snap fit + + + + + + + + + + + + + diff --git a/assets/icons/ui/design/svg/model/sphere.svg b/assets/icons/ui/design/svg/model/sphere.svg new file mode 100644 index 00000000..f6658c2a --- /dev/null +++ b/assets/icons/ui/design/svg/model/sphere.svg @@ -0,0 +1,8 @@ + + Sphere + + + + + + diff --git a/assets/icons/ui/design/svg/model/spline.svg b/assets/icons/ui/design/svg/model/spline.svg new file mode 100644 index 00000000..11735513 --- /dev/null +++ b/assets/icons/ui/design/svg/model/spline.svg @@ -0,0 +1,12 @@ + + Fit-point spline + + + + + + + + + + diff --git a/assets/icons/ui/design/svg/model/spline_cv.svg b/assets/icons/ui/design/svg/model/spline_cv.svg new file mode 100644 index 00000000..bd6c41f9 --- /dev/null +++ b/assets/icons/ui/design/svg/model/spline_cv.svg @@ -0,0 +1,15 @@ + + Control-point spline + + + + + + + + + + + + + diff --git a/assets/icons/ui/design/svg/model/split_body.svg b/assets/icons/ui/design/svg/model/split_body.svg new file mode 100644 index 00000000..b1a6c082 --- /dev/null +++ b/assets/icons/ui/design/svg/model/split_body.svg @@ -0,0 +1,17 @@ + + Split body + + + + + + + + + + + + + + + diff --git a/assets/icons/ui/design/svg/model/split_face.svg b/assets/icons/ui/design/svg/model/split_face.svg new file mode 100644 index 00000000..eb110613 --- /dev/null +++ b/assets/icons/ui/design/svg/model/split_face.svg @@ -0,0 +1,9 @@ + + Split face + + + + + + + diff --git a/assets/icons/ui/design/svg/model/spun.svg b/assets/icons/ui/design/svg/model/spun.svg new file mode 100644 index 00000000..a213b832 --- /dev/null +++ b/assets/icons/ui/design/svg/model/spun.svg @@ -0,0 +1,9 @@ + + Spun profile + + + + + + + diff --git a/assets/icons/ui/design/svg/model/star.svg b/assets/icons/ui/design/svg/model/star.svg new file mode 100644 index 00000000..d2031545 --- /dev/null +++ b/assets/icons/ui/design/svg/model/star.svg @@ -0,0 +1,5 @@ + + Favorite + + + diff --git a/assets/icons/ui/design/svg/model/status_error.svg b/assets/icons/ui/design/svg/model/status_error.svg new file mode 100644 index 00000000..2a898b5f --- /dev/null +++ b/assets/icons/ui/design/svg/model/status_error.svg @@ -0,0 +1,6 @@ + + Error + + + + diff --git a/assets/icons/ui/design/svg/model/status_warning.svg b/assets/icons/ui/design/svg/model/status_warning.svg new file mode 100644 index 00000000..9fc6edd3 --- /dev/null +++ b/assets/icons/ui/design/svg/model/status_warning.svg @@ -0,0 +1,7 @@ + + Warning + + + + + diff --git a/assets/icons/ui/design/svg/model/stitch.svg b/assets/icons/ui/design/svg/model/stitch.svg new file mode 100644 index 00000000..3cdd31a1 --- /dev/null +++ b/assets/icons/ui/design/svg/model/stitch.svg @@ -0,0 +1,11 @@ + + Stitch + + + + + + + + + diff --git a/assets/icons/ui/design/svg/model/suppress.svg b/assets/icons/ui/design/svg/model/suppress.svg new file mode 100644 index 00000000..9d369a54 --- /dev/null +++ b/assets/icons/ui/design/svg/model/suppress.svg @@ -0,0 +1,8 @@ + + Suppress + + + + + + diff --git a/assets/icons/ui/design/svg/model/surface.svg b/assets/icons/ui/design/svg/model/surface.svg new file mode 100644 index 00000000..070787dd --- /dev/null +++ b/assets/icons/ui/design/svg/model/surface.svg @@ -0,0 +1,11 @@ + + Surface + + + + + + + + + diff --git a/assets/icons/ui/design/svg/model/svg.svg b/assets/icons/ui/design/svg/model/svg.svg new file mode 100644 index 00000000..18cb0a18 --- /dev/null +++ b/assets/icons/ui/design/svg/model/svg.svg @@ -0,0 +1,9 @@ + + Insert SVG + + + + + + + diff --git a/assets/icons/ui/design/svg/model/sweep.svg b/assets/icons/ui/design/svg/model/sweep.svg new file mode 100644 index 00000000..dd22535e --- /dev/null +++ b/assets/icons/ui/design/svg/model/sweep.svg @@ -0,0 +1,8 @@ + + Sweep + + + + + + diff --git a/assets/icons/ui/design/svg/model/text.svg b/assets/icons/ui/design/svg/model/text.svg new file mode 100644 index 00000000..a5626a73 --- /dev/null +++ b/assets/icons/ui/design/svg/model/text.svg @@ -0,0 +1,5 @@ + + Text + + + diff --git a/assets/icons/ui/design/svg/model/thicken.svg b/assets/icons/ui/design/svg/model/thicken.svg new file mode 100644 index 00000000..e72ea9e4 --- /dev/null +++ b/assets/icons/ui/design/svg/model/thicken.svg @@ -0,0 +1,19 @@ + + Thicken + + + + + + + + + + + + + + + + + diff --git a/assets/icons/ui/design/svg/model/thread.svg b/assets/icons/ui/design/svg/model/thread.svg new file mode 100644 index 00000000..d67b081e --- /dev/null +++ b/assets/icons/ui/design/svg/model/thread.svg @@ -0,0 +1,13 @@ + + Thread + + + + + + + + + + + diff --git a/assets/icons/ui/design/svg/model/torus.svg b/assets/icons/ui/design/svg/model/torus.svg new file mode 100644 index 00000000..9280a241 --- /dev/null +++ b/assets/icons/ui/design/svg/model/torus.svg @@ -0,0 +1,7 @@ + + Torus + + + + + diff --git a/assets/icons/ui/design/svg/model/trace.svg b/assets/icons/ui/design/svg/model/trace.svg new file mode 100644 index 00000000..bd77fe33 --- /dev/null +++ b/assets/icons/ui/design/svg/model/trace.svg @@ -0,0 +1,8 @@ + + Motion trace + + + + + + diff --git a/assets/icons/ui/design/svg/model/trim.svg b/assets/icons/ui/design/svg/model/trim.svg new file mode 100644 index 00000000..ffe8d1c3 --- /dev/null +++ b/assets/icons/ui/design/svg/model/trim.svg @@ -0,0 +1,6 @@ + + Trim + + + + diff --git a/assets/icons/ui/design/svg/model/unfold.svg b/assets/icons/ui/design/svg/model/unfold.svg new file mode 100644 index 00000000..4d2bd2c1 --- /dev/null +++ b/assets/icons/ui/design/svg/model/unfold.svg @@ -0,0 +1,11 @@ + + Unfold + + + + + + + + + diff --git a/assets/icons/ui/design/svg/model/unstitch.svg b/assets/icons/ui/design/svg/model/unstitch.svg new file mode 100644 index 00000000..d7a07586 --- /dev/null +++ b/assets/icons/ui/design/svg/model/unstitch.svg @@ -0,0 +1,15 @@ + + Unstitch + + + + + + + + + + + + + diff --git a/assets/icons/ui/design/svg/model/untrim.svg b/assets/icons/ui/design/svg/model/untrim.svg new file mode 100644 index 00000000..74b68517 --- /dev/null +++ b/assets/icons/ui/design/svg/model/untrim.svg @@ -0,0 +1,7 @@ + + Untrim + + + + + diff --git a/assets/icons/ui/design/svg/model/update.svg b/assets/icons/ui/design/svg/model/update.svg new file mode 100644 index 00000000..c4f6cb83 --- /dev/null +++ b/assets/icons/ui/design/svg/model/update.svg @@ -0,0 +1,17 @@ + + Update + + + + + + + + + + + + + + + diff --git a/assets/icons/ui/design/svg/model/web.svg b/assets/icons/ui/design/svg/model/web.svg new file mode 100644 index 00000000..d46ac8f4 --- /dev/null +++ b/assets/icons/ui/design/svg/model/web.svg @@ -0,0 +1,23 @@ + + Web + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/ui/design/svg/model/zebra.svg b/assets/icons/ui/design/svg/model/zebra.svg new file mode 100644 index 00000000..0182d6df --- /dev/null +++ b/assets/icons/ui/design/svg/model/zebra.svg @@ -0,0 +1,15 @@ + + Zebra + + + + + + + + + + + + + diff --git a/assets/icons/ui/design/svg/small/as_built.svg b/assets/icons/ui/design/svg/small/as_built.svg new file mode 100644 index 00000000..a8a83734 --- /dev/null +++ b/assets/icons/ui/design/svg/small/as_built.svg @@ -0,0 +1,19 @@ + + As-built joint + + + + + + + + + + + + + + + + + diff --git a/assets/icons/ui/design/svg/small/component.svg b/assets/icons/ui/design/svg/small/component.svg new file mode 100644 index 00000000..5ff07819 --- /dev/null +++ b/assets/icons/ui/design/svg/small/component.svg @@ -0,0 +1,17 @@ + + Component + + + + + + + + + + + + + + + diff --git a/assets/icons/ui/design/svg/small/curvature_comb.svg b/assets/icons/ui/design/svg/small/curvature_comb.svg new file mode 100644 index 00000000..b0f44628 --- /dev/null +++ b/assets/icons/ui/design/svg/small/curvature_comb.svg @@ -0,0 +1,8 @@ + + Curvature comb + + + + + + diff --git a/assets/icons/ui/design/svg/small/interference.svg b/assets/icons/ui/design/svg/small/interference.svg new file mode 100644 index 00000000..624fe6df --- /dev/null +++ b/assets/icons/ui/design/svg/small/interference.svg @@ -0,0 +1,13 @@ + + Interference + + + + + + + + + + + diff --git a/assets/icons/ui/design/svg/small/iso_analysis.svg b/assets/icons/ui/design/svg/small/iso_analysis.svg new file mode 100644 index 00000000..567b35f1 --- /dev/null +++ b/assets/icons/ui/design/svg/small/iso_analysis.svg @@ -0,0 +1,11 @@ + + Isoparametric analysis + + + + + + + + + diff --git a/assets/icons/ui/design/svg/small/joint.svg b/assets/icons/ui/design/svg/small/joint.svg new file mode 100644 index 00000000..b5384d43 --- /dev/null +++ b/assets/icons/ui/design/svg/small/joint.svg @@ -0,0 +1,17 @@ + + Joint + + + + + + + + + + + + + + + diff --git a/assets/icons/ui/design/svg/small/pattern.svg b/assets/icons/ui/design/svg/small/pattern.svg new file mode 100644 index 00000000..cac63a6f --- /dev/null +++ b/assets/icons/ui/design/svg/small/pattern.svg @@ -0,0 +1,21 @@ + + Pattern + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/ui/design/svg/small/section.svg b/assets/icons/ui/design/svg/small/section.svg new file mode 100644 index 00000000..1a44fc1f --- /dev/null +++ b/assets/icons/ui/design/svg/small/section.svg @@ -0,0 +1,19 @@ + + Section + + + + + + + + + + + + + + + + + diff --git a/assets/icons/ui/design/svg/workspace/ws_animate.svg b/assets/icons/ui/design/svg/workspace/ws_animate.svg new file mode 100644 index 00000000..d53cfbb8 --- /dev/null +++ b/assets/icons/ui/design/svg/workspace/ws_animate.svg @@ -0,0 +1,16 @@ + + Animate workspace + + + + + + + + + + + + + + diff --git a/assets/icons/ui/design/svg/workspace/ws_design.svg b/assets/icons/ui/design/svg/workspace/ws_design.svg new file mode 100644 index 00000000..c5435fbd --- /dev/null +++ b/assets/icons/ui/design/svg/workspace/ws_design.svg @@ -0,0 +1,10 @@ + + Ws design + + + + + + + + diff --git a/assets/icons/ui/design/svg/workspace/ws_drawing.svg b/assets/icons/ui/design/svg/workspace/ws_drawing.svg new file mode 100644 index 00000000..81268816 --- /dev/null +++ b/assets/icons/ui/design/svg/workspace/ws_drawing.svg @@ -0,0 +1,8 @@ + + Ws drawing + + + + + + diff --git a/assets/icons/ui/design/svg/workspace/ws_render.svg b/assets/icons/ui/design/svg/workspace/ws_render.svg new file mode 100644 index 00000000..6abc11f8 --- /dev/null +++ b/assets/icons/ui/design/svg/workspace/ws_render.svg @@ -0,0 +1,16 @@ + + Ws render + + + + + + + + + + + + + + diff --git a/assets/icons/ui/design/tools/author/bases.py b/assets/icons/ui/design/tools/author/bases.py new file mode 100644 index 00000000..472e19ff --- /dev/null +++ b/assets/icons/ui/design/tools/author/bases.py @@ -0,0 +1,56 @@ +"""Canonical original bases. Read assets/icons/ui/STYLE_GUIDE.md before drawing an icon. + +SVGs remain the masters; these helpers make new drawings conform to the set. +""" +from lib import Icon, Iso, INK, P1, P2, P3, A3, OW, AW, GAP, poly, f +import math + +GREY = (P1, P2, P3) +CUBE_MIN = -12 +CUBE_MAX = 12 +CUBE_HEIGHT = 24 + + +def cube_projection(size=54): + """x right/down, y left/down, z up; 30° diagonals, equal physical axes.""" + return Iso().fit([(x, y, z) for x in (-12, 12) for y in (-12, 12) for z in (0, 24)], size, size) + + +def grey_cube(icon, size=54): + """Standard cube: top P1, screen-left P2, screen-right P3. Return projection.""" + projection = cube_projection(size) + projection.box(icon, -12, -12, 0, 12, 12, 24, GREY) + return projection + + +def grey_slab(icon, size=52): + """Same 30° projection and shading, with a four-unit thickness.""" + projection = Iso().fit([(x, y, z) for x in (-15, 15) for y in (-15, 15) for z in (0, 4)], size, size) + projection.box(icon, -15, -15, 0, 15, 15, 4, GREY) + return projection + + +def arrow(icon, start, tip, colour=A3, length=9, width=9, gap=0): + """A straight action arrow. Shaft meets head; optional thin surface knockout.""" + dx, dy = tip[0] - start[0], tip[1] - start[1] + distance = math.hypot(dx, dy) + if distance <= length: raise ValueError('arrow shaft must be longer than its head') + dx, dy = dx / distance, dy / distance + base = (tip[0] - dx * length, tip[1] - dy * length) + head = poly([tip, (base[0] - dy * width / 2, base[1] + dx * width / 2), + (base[0] + dy * width / 2, base[1] - dx * width / 2)]) + shaft = poly([start, (base[0] + dx, base[1] + dy)], False) + if gap: + icon.stroke(shaft, GAP, AW + 2 * gap) + icon.stroke(head, GAP, 2 * gap, fill=GAP) + icon.stroke(shaft, colour, AW); icon.fill(head, colour) + + +def as_svg(icon): + return '' + icon.title + '' + ''.join(icon.els) + '' + + +def examples(): + cube = Icon('grey_cube', 'Canonical grey cube'); grey_cube(cube) + slab = Icon('grey_slab', 'Canonical grey slab'); grey_slab(slab) + return {'Grey cube': as_svg(cube), 'Grey slab': as_svg(slab)} diff --git a/assets/icons/ui/design/tools/author/lib.py b/assets/icons/ui/design/tools/author/lib.py new file mode 100644 index 00000000..9764d37b --- /dev/null +++ b/assets/icons/ui/design/tools/author/lib.py @@ -0,0 +1,193 @@ +import math, os, re, tomllib +OUT=os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))),'svg','model') +PALETTE=tomllib.load(open(os.path.join(os.path.dirname(os.path.dirname(OUT)),'palette.toml'),'rb')) +def source(role): return PALETTE['roles'][role].get('source', PALETTE['roles'][role]['light']) +INK=source('ink'); GAP=source('gap'); GUIDE=source('guide') +P1,P2,P3=(source('passive-'+str(i)) for i in (1,2,3)) +A1,A2,A3=(source('active-'+str(i)) for i in (1,2,3)) +OW=PALETTE['strokes']['geometry']; AW=PALETTE['strokes']['action']; NW=PALETTE['strokes']['navigation'] +R3=math.sqrt(3) +def f(v): + s=('%.2f'%v).rstrip('0').rstrip('.') + return '0' if s in('-0','') else s +def pts(ps): return ''.join(('M' if i==0 else 'L')+f(x)+' '+f(y) for i,(x,y) in enumerate(ps)) +def poly(ps, close=True): return pts(ps)+('Z' if close else '') +def ell(cx,cy,rx,ry=None): + ry=rx if ry is None else ry + return 'M%s %sA%s %s 0 1 0 %s %sA%s %s 0 1 0 %s %sZ'%(f(cx-rx),f(cy),f(rx),f(ry),f(cx+rx),f(cy),f(rx),f(ry),f(cx-rx),f(cy)) +def arcpts(cx,cy,rx,ry,t0,t1): + """arc along ellipse from angle t0 to t1 (degrees, screen coords: 0=right, 90=down). returns path 'M..A..' """ + a0,a1=math.radians(t0),math.radians(t1) + p0=(cx+rx*math.cos(a0),cy+ry*math.sin(a0)); p1=(cx+rx*math.cos(a1),cy+ry*math.sin(a1)) + large=1 if abs(t1-t0)>180 else 0; sweep=1 if t1>t0 else 0 + return 'M%s %sA%s %s 0 %d %d %s %s'%(f(p0[0]),f(p0[1]),f(rx),f(ry),large,sweep,f(p1[0]),f(p1[1])) +def arcto(rx,ry,x,y,large=0,sweep=1): return 'A%s %s 0 %d %d %s %s'%(f(rx),f(ry),large,sweep,f(x),f(y)) +def ellarc(cx,cy,rx,ry,t0,t1): return arcpts(cx,cy,rx,ry,t0,t1) +def rrect(x0,y0,x1,y1,r): + return ('M%s %sL%s %sA%s %s 0 0 1 %s %sL%s %sA%s %s 0 0 1 %s %sL%s %sA%s %s 0 0 1 %s %sL%s %sA%s %s 0 0 1 %s %sZ'% + (f(x0+r),f(y0),f(x1-r),f(y0),f(r),f(r),f(x1),f(y0+r),f(x1),f(y1-r),f(r),f(r),f(x1-r),f(y1),f(x0+r),f(y1),f(r),f(r),f(x0),f(y1-r),f(x0),f(y0+r),f(r),f(r),f(x0+r),f(y0))) + +def arc_samples(d,n=24): + """points along every elliptical arc (rotation 0) of a path, so a bounding box sees the curve and not only its ends""" + pts=[]; cur=(0.0,0.0) + for c,a in re.findall(r'([MLCQAZ])([^MLCQAZ]*)',d): + v=[float(x) for x in a.split()] + if c=='Z' or not v: continue + if c in 'ML': + cur=(v[-2],v[-1]) + elif c in 'CQ': + cur=(v[-2],v[-1]) + elif c=='A': + for i in range(0,len(v),7): + rx,ry,_,large,sweep,x2,y2=v[i:i+7]; x1,y1=cur + dx,dy=(x1-x2)/2,(y1-y2)/2 + lam=dx*dx/(rx*rx)+dy*dy/(ry*ry) + if lam>1: rx*=math.sqrt(lam); ry*=math.sqrt(lam) + num=rx*rx*ry*ry-rx*rx*dy*dy-ry*ry*dx*dx; den=rx*rx*dy*dy+ry*ry*dx*dx + co=math.sqrt(max(num/den,0)) if den else 0 + if large==sweep: co=-co + cxp,cyp=co*rx*dy/ry,-co*ry*dx/rx + cx,cy=cxp+(x1+x2)/2,cyp+(y1+y2)/2 + t1=math.atan2((dy-cyp)/ry,(dx-cxp)/rx); t2=math.atan2((-dy-cyp)/ry,(-dx-cxp)/rx) + dt=t2-t1 + if sweep and dt<0: dt+=2*math.pi + if not sweep and dt>0: dt-=2*math.pi + for k in range(n+1): + t=t1+dt*k/n; pts.append((cx+rx*math.cos(t),cy+ry*math.sin(t))) + cur=(x2,y2) + return pts + +class Icon: + def __init__(s,name,title=None): + s.name=name; s.title=title or name.replace('_',' ').title(); s.els=[]; s.fam='model' + def fill(s,d,c,rule=None): + s.els.append(''%(d,c,' fill-rule="evenodd"' if rule else '')); return s + def stroke(s,d,c=INK,w=OW,fill=None,rule=None,dash=None): + if dash and c not in (GAP,A1,A2,A3): c=GUIDE + s.els.append(''%(d,fill or 'none',c,f(w),' stroke-dasharray="%s"'%dash if dash else '',' fill-rule="evenodd"' if rule else '')); return s + def dot(s,x,y,r,c): + s.els.append(''%(f(x),f(y),f(r),c)); return s + def ring(s,x,y,r,c=INK,w=OW,fill=None): + s.els.append(''%(f(x),f(y),f(r),fill or 'none',c,f(w))); return s + def clip_begin(s,cid,d): + s.els.append(''%(cid,d)); s.els.append(''%cid); return s + def clip_end(s): + s.els.append(''); return s + def recenter(s): + """move the finished drawing so the box around all its path points (and strokes) is centred on (32, 32); exact for polygons and lines""" + def walk(d,fn): + out=[] + for c,a in re.findall(r'([MLCQAZ])([^MLCQAZ]*)',d): + v=a.split(); + if c=='Z' or not v: out.append(c); continue + if c=='A': + for i in range(0,len(v),7): v[i+5],v[i+6]=fn(float(v[i+5]),float(v[i+6])) + else: + for i in range(0,len(v),2): v[i],v[i+1]=fn(float(v[i]),float(v[i+1])) + out.append(c+' '.join(f(float(x)) if not isinstance(x,str) else x for x in v)) + return ''.join(out) + xs=[];ys=[] + for e in s.els: + hw=0 + m=re.search(r'stroke-width="([\d.]+)"',e) + if m and 'stroke="none"' not in e and ' stroke="' in e: hw=float(m.group(1))/2 + d=re.search(r' d="([^"]+)"',e) + if d: + def grab(x,y): xs.extend((x-hw,x+hw)); ys.extend((y-hw,y+hw)); return x,y + walk(d.group(1),grab) + for px,py in arc_samples(d.group(1)): xs.extend((px-hw,px+hw)); ys.extend((py-hw,py+hw)) + c=re.search(r'cx="([-\d.]+)" cy="([-\d.]+)" r="([\d.]+)"',e) + if c: x,y,r=map(float,c.groups()); xs.extend((x-r-hw,x+r+hw)); ys.extend((y-r-hw,y+r+hw)) + dx=32-(min(xs)+max(xs))/2; dy=32-(min(ys)+max(ys))/2 + mv=lambda x,y:(x+dx,y+dy) + new=[] + for e in s.els: + e=re.sub(r' d="([^"]+)"',lambda m:' d="%s"'%walk(m.group(1),mv),e) + e=re.sub(r'cx="([-\d.]+)" cy="([-\d.]+)"',lambda m:'cx="%s" cy="%s"'%(f(float(m.group(1))+dx),f(float(m.group(2))+dy)),e) + new.append(e) + s.els=new; return s + def write(s,fam='model'): + d=os.path.join(os.path.dirname(OUT),fam); os.makedirs(d,exist_ok=True) + body='\n'.join(' '+e for e in s.els) + open(os.path.join(d,s.name+'.svg'),'w').write('\n %s\n%s\n\n'%(s.title,body)) + +# ---- isometric helpers ---- +class Iso: + """x -> right-down, y -> left-down, z up. k = screen units per unit.""" + def __init__(s,k=1.0,ox=32,oy=32): s.k=k; s.ox=ox; s.oy=oy + def p(s,x,y,z=0): return (s.ox+(x-y)*R3/2*s.k, s.oy+(x+y)*0.5*s.k-z*s.k) + def fit(s,pts3d,maxw=56,maxh=56): + s.k=1.0; s.ox=0; s.oy=0 + P=[s.p(*q) for q in pts3d]; w=max(p[0] for p in P)-min(p[0] for p in P); h=max(p[1] for p in P)-min(p[1] for p in P) + s.k=min(maxw/w,maxh/h); P=[s.p(*q) for q in pts3d] + s.ox=32-(max(p[0] for p in P)+min(p[0] for p in P))/2; s.oy=32-(max(p[1] for p in P)+min(p[1] for p in P))/2 + return s + def extrude(s,ic,rings,z0,z1,cols=(P1,P2,P3),w=OW): + """prism of the plan polygons `rings` (even-odd: holes allowed), z0..z1. Only the edges where the surface changes angle are drawn.""" + def inside(x,y): + n=0 + for r in rings: + for i in range(len(r)): + (ax,ay),(bx,by)=r[i],r[(i+1)%len(r)] + if (ay>y)!=(by>y) and x0.5 or ny>0.5: walls.append((mx+my,a,b,(nx,ny))) + for _,a,b,nrm in sorted(walls): + d=s.poly((a[0],a[1],z1),(b[0],b[1],z1),(b[0],b[1],z0),(a[0],a[1],z0)) + ic.fill(d,cols[2] if nrm[0]>0.5 else cols[1]); ic.stroke(d,INK,w) + top=''.join(poly([s.p(x,y,z1) for x,y in r]) for r in rings) + ic.fill(top,cols[0],rule=True); ic.stroke(top,INK,w,rule=True) + def poly(s,*ps): return poly([s.p(*q) for q in ps]) + def ellipse_r(s,r): return (R3/2*s.k*r*math.sqrt(2), 0.5*s.k*r*math.sqrt(2)) + def box(s,ic,x0,y0,z0,x1,y1,z1,cols=(P1,P2,P3),w=OW,detail=True,ink=INK): + """draw filled box with outline; faces: top, left(+y), right(+x)""" + top=s.poly((x0,y0,z1),(x1,y0,z1),(x1,y1,z1),(x0,y1,z1)) + left=s.poly((x0,y1,z1),(x1,y1,z1),(x1,y1,z0),(x0,y1,z0)) + right=s.poly((x1,y0,z1),(x1,y1,z1),(x1,y1,z0),(x1,y0,z0)) + ic.fill(left,cols[1]).fill(right,cols[2]).fill(top,cols[0]) + sil=s.poly((x0,y0,z1),(x1,y0,z1),(x1,y0,z0),(x1,y1,z0),(x0,y1,z0),(x0,y1,z1)) + ic.stroke(sil,ink,w) + if detail: + ic.stroke(poly([s.p(x0,y1,z1),s.p(x1,y1,z1),s.p(x1,y0,z1)],False),ink,w) + ic.stroke(poly([s.p(x1,y1,z1),s.p(x1,y1,z0)],False),ink,w) + +def cylinder(ic,X,Y,rx,h,cols=(P1,P2,P3),w=OW,top=True,ry=None,sh=0.35): + """vertical cylinder; (X,Y) = centre of the top ellipse; side shading strip on the right""" + ry=rx/R3 if ry is None else ry + body='M%s %sL%s %s%sL%s %sZ'%(f(X-rx),f(Y),f(X-rx),f(Y+h),arcto(rx,ry,X+rx,Y+h,0,0),f(X+rx),f(Y)) + ic.fill(body,cols[1]) + xs=X+sh*rx; yb=ry*math.sqrt(1-sh*sh) + strip='M%s %sL%s %s%sL%s %sZ'%(f(xs),f(Y),f(xs),f(Y+h+yb),arcto(rx,ry,X+rx,Y+h,0,0),f(X+rx),f(Y)) + ic.fill(strip,cols[2]) + ic.stroke(body,INK,w) + if top: ic.stroke(ell(X,Y,rx,ry),INK,w,fill=cols[0]) + +def clip_poly(subject,clip): + """Sutherland-Hodgman: the part of polygon `subject` inside the convex polygon `clip` (lists of (x, y)).""" + area=sum(clip[i][0]*clip[(i+1)%len(clip)][1]-clip[(i+1)%len(clip)][0]*clip[i][1] for i in range(len(clip))) + sgn=1 if area>0 else -1 + def inside(p,a,b): return sgn*((b[0]-a[0])*(p[1]-a[1])-(b[1]-a[1])*(p[0]-a[0]))>=-1e-9 + def cross(p,q,a,b): + x1,y1,x2,y2,x3,y3,x4,y4=p[0],p[1],q[0],q[1],a[0],a[1],b[0],b[1] + den=(x1-x2)*(y3-y4)-(y1-y2)*(x3-x4) + if abs(den)<1e-12: return q + t=((x1-x3)*(y3-y4)-(y1-y3)*(x3-x4))/den + return (x1+t*(x2-x1),y1+t*(y2-y1)) + out=list(subject) + for i in range(len(clip)): + a,b=clip[i],clip[(i+1)%len(clip)]; inp=out; out=[] + if not inp: break + s=inp[-1] + for e in inp: + if inside(e,a,b): + if not inside(s,a,b): out.append(cross(s,e,a,b)) + out.append(e) + elif inside(s,a,b): out.append(cross(s,e,a,b)) + s=e + return out diff --git a/assets/icons/ui/design/tools/author/new_icon.py b/assets/icons/ui/design/tools/author/new_icon.py new file mode 100644 index 00000000..be385d23 --- /dev/null +++ b/assets/icons/ui/design/tools/author/new_icon.py @@ -0,0 +1,42 @@ +#!/usr/bin/env python3 +"""Create a new, unaccepted SVG from a canonical base; never overwrite artwork.""" +import argparse +from pathlib import Path +import re +import sys +import tomllib +from lib import Icon +from bases import grey_cube, grey_slab + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('name', help='lower_snake_case app icon id') + parser.add_argument('--base', choices=('cube', 'slab', 'empty'), default='cube') + parser.add_argument('--family', choices=('model', 'flat', 'workspace'), default='model') + args = parser.parse_args() + design = Path(__file__).resolve().parents[2] + if not re.fullmatch('[a-z][a-z0-9_]*', args.name): parser.error('use a lower_snake_case icon id') + aliases = tomllib.loads((design / 'aliases.toml').read_text()).get('alias', {}) + if args.name in aliases or list((design / 'svg').glob('*/' + args.name + '.svg')): + parser.error('id already exists; edit its master after checking review.json') + if args.family != 'model' and args.base != 'empty': parser.error('cube/slab bases belong to the model family') + icon = Icon(args.name) + if args.base == 'cube': grey_cube(icon) + if args.base == 'slab': grey_slab(icon) + icon.write(args.family) + root = design.parents[3] + path = f'assets/icons/ui/design/svg/{args.family}/{args.name}.svg' + attribution = root / 'ATTRIBUTION.md' + row = f'| {path} | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |\n' + text = attribution.read_text() + if '| ' + path + ' |' not in text: + anchor = '| assets/icons/ui/design/svg/model/placeholder.svg' + if anchor in text: text = text.replace(anchor, row + anchor, 1) + else: text += '\n' + row + attribution.write_text(text) + print(path) + print('Starter only: finish the operation cue using STYLE_GUIDE.md; review status is Open.') + + +if __name__ == '__main__': main() diff --git a/assets/icons/ui/design/tools/icon_palette.py b/assets/icons/ui/design/tools/icon_palette.py new file mode 100644 index 00000000..c399cc7f --- /dev/null +++ b/assets/icons/ui/design/tools/icon_palette.py @@ -0,0 +1,288 @@ +"""Shared semantic colour policy for repository icon tools; values live in palette.toml.""" +import copy +import hashlib +import os +import re +import tomllib +import uuid +import xml.etree.ElementTree as ET + +VISIONS = ('off', 'red-green', 'blue-yellow') + + +def validation_errors(palette, ids): + errors, assigned = [], set() + accents = palette.get('accents', {}) + for accent, icons in palette.get('icon-accents', {}).items(): + if accent not in accents: + errors.append('unknown semantic accent ' + accent) + for icon in icons: + if icon not in ids or icon in assigned: + errors.append('unknown or repeated semantic icon ' + icon) + assigned.add(icon) + groups = [('', accents)] + list(palette.get('variants', {}).items()) + for vision, group in groups: + if vision and vision not in VISIONS: + errors.append('unknown colour-vision group ' + vision) + for accent, roles in group.items(): + if accent not in accents: + errors.append('variant has unknown accent ' + accent) + for role, value in roles.items(): + if role == 'guide': + errors.append('Guides use the shared guide role, never an accent override') + if role not in palette['roles']: + errors.append('unknown colour role ' + role) + if 'use' in value: + value = roles.get(value['use'], {}) + for theme in ('light', 'dark'): + if not re.fullmatch(r'#[0-9a-fA-F]{6}', value.get(theme, '')): + errors.append(f'{vision}/{accent}/{role}: invalid {theme} hex or role link') + return errors + + +def default_accent(palette, icon): + return next((accent for accent, ids in palette.get('icon-accents', {}).items() if icon in ids), '') + + +def resolve(palette, aliases, icon): + """Explicit alias accents win; defaults belong to an id, not its shared master.""" + alias = aliases.get(icon, icon) + if isinstance(alias, dict): + return alias['master'], alias.get('accent') or default_accent(palette, icon) + return alias, default_accent(palette, icon) + + +def role_values(palette, theme, accent='', vision='off'): + values = {role: value[theme] for role, value in palette['roles'].items()} + overrides = palette.get('accents', {}).get(accent, {}) + for role, value in overrides.items(): + if role == 'guide': continue + if 'use' in value: + value = overrides[value['use']] + values[role] = value[theme] + for role, value in palette.get('variants', {}).get(vision, {}).get(accent, {}).items(): + if role == 'guide': continue + values[role] = value[theme] + return values + + +def colour_ref(palette, theme, accent, role, vision='off'): + """A live-review variable for the exact token, including links and overrides.""" + path = 'roles.' + role + group = palette.get('accents', {}).get(accent, {}) + if role != 'guide' and role in group: + target = group[role].get('use', role) + path = 'accents.' + accent + '.' + target + if role != 'guide' and role in palette.get('variants', {}).get(vision, {}).get(accent, {}): + path = 'variants.' + vision + '.' + accent + '.' + role + value = role_values(palette, theme, accent, vision)[role] + return 'var(' + variable(path, theme) + ',' + value + ')' + + +def variable(path, theme): + return '--icon-palette-' + path.replace('.', '-') + '-' + theme + + +def tokens(palette): + """Editable display paints only; stable SVG source identifiers are never exposed.""" + purposes = {'ink': 'Outline', 'line': 'Line art', 'passive-1': 'Top face', + 'passive-2': 'Left face', 'passive-3': 'Right face', 'gap': 'Knockout', + 'guide': 'Guides', 'active-1': 'Pale', 'active-2': 'Mid', 'active-3': 'Stroke'} + result = [] + groups = [('roles', '', palette['roles'])] + groups += [('accents.' + name, 'Constr.' if name == 'construction' else name.title(), roles) for name, roles in palette.get('accents', {}).items()] + groups += [('variants.' + vision + '.' + accent, + accent.title() + (' R/G' if vision == 'red-green' else ' B/Y'), roles) + for vision, accents in palette.get('variants', {}).items() for accent, roles in accents.items()] + for prefix, group, roles in groups: + for role, value in roles.items(): + linked = prefix + '.' + value['use'] if 'use' in value else '' + paints = roles[value['use']] if linked else value + purpose = purposes.get(role, role) + if not group and role.startswith('active-'): purpose = 'Blue ' + purpose.lower() + result.append({'id': prefix + '.' + role, 'name': (group + ' ' + purpose).strip(), + 'light': paints['light'], 'dark': paints['dark'], 'linked': linked, + 'knockout': prefix == 'roles' and role == 'gap'}) + return result + + +def palette_css(palette): + return ':root{' + ''.join(variable(token['id'], theme) + ':' + token[theme] + ';' + for token in tokens(palette) if not token['linked'] and not token['knockout'] + for theme in ('light', 'dark')) + '}' + + +def revision(path): + with open(path, 'rb') as source: return hashlib.sha256(source.read()).hexdigest() + + +def save_colours(path, base_revision, changes): + """Apply validated display colours atomically, preserving comments and source ids.""" + with open(path, encoding='utf-8') as source: text = source.read() + if hashlib.sha256(text.encode()).hexdigest() != base_revision: + raise ValueError('Palette changed on disk. Reload before applying your colours.') + palette = tomllib.loads(text) + editable = {token['id'] for token in tokens(palette) if not token['linked'] and not token['knockout']} + if not isinstance(changes, dict) or not changes or not set(changes) <= editable: + raise ValueError('Unknown, linked or transparent palette token') + expected = copy.deepcopy(palette) + for key, paints in changes.items(): + if not isinstance(paints, dict) or set(paints) != {'light', 'dark'} or any( + not isinstance(value, str) or not re.fullmatch(r'#[0-9a-fA-F]{6}', value) for value in paints.values()): + raise ValueError('Use six-digit hex colours for both themes') + target = expected + for part in key.split('.'): target = target[part] + target.update({theme: colour.lower() for theme, colour in paints.items()}) + section = re.compile(r'(?m)^\[' + re.escape(key) + r'\][^\n]*\n(?:(?!^\[)[\s\S])*') + match = section.search(text) + if not match: raise ValueError('Missing palette section ' + key) + block = match[0] + for theme, colour in paints.items(): + block, count = re.subn(r'(?m)^(\s*' + theme + r'\s*=\s*)["\']#[0-9a-fA-F]{6}["\']', + lambda m: m[1] + '"' + colour.lower() + '"', block) + if count != 1: raise ValueError('Missing colour in ' + key) + text = text[:match.start()] + block + text[match.end():] + if tomllib.loads(text) != expected: raise ValueError('Palette update changed unrelated data') + temporary = str(path) + '.' + uuid.uuid4().hex + '.tmp' + try: + with open(temporary, 'w', encoding='utf-8') as output: output.write(text) + os.replace(temporary, path) + finally: + if os.path.exists(temporary): os.unlink(temporary) + + +def transparent_knockouts(svg, source): + """GAP erases preceding artwork; later foreground paints remain untouched. + + Masks are generated only for display. The accepted SVG remains the editable + authority, with GAP as its stable source identifier. Literal mask white/black + are coverage values, not palette roles. + """ + if source.lower() not in svg.lower(): return svg + ns = 'http://www.w3.org/2000/svg' + ET.register_namespace('', ns) + root = ET.fromstring(svg) + tag = lambda node: node.tag.rsplit('}', 1)[-1] + element = lambda name, attrs: ET.Element('{' + ns + '}' + name, attrs) + definitions, metadata, output = [], [], [] + used = {node.get('id') for node in root.iter() if node.get('id')} + serial = 0 + + def wrap(node, ancestors): + for attrs in reversed(ancestors): + group = element('g', attrs) + group.append(node) + node = group + return node + + def erase(node, ancestors): + nonlocal serial, output + if not output: return + serial += 1 + while 'sc-knockout-' + str(serial) in used: serial += 1 + name = 'sc-knockout-' + str(serial) + used.add(name) + mask = element('mask', {'id': name, 'maskUnits': 'userSpaceOnUse', + 'maskContentUnits': 'userSpaceOnUse', 'x': '0', 'y': '0', + 'width': '64', 'height': '64', 'mask-type': 'luminance'}) + mask.append(element('path', {'d': 'M0 0H64V64H0Z', 'fill': 'white'})) + mask.append(wrap(node, ancestors)) + definitions.append(mask) + group = element('g', {'mask': 'url(#' + name + ')'}) + group.extend(output) + output = [group] + + def visit(node, ancestors=()): + kind = tag(node) + if kind == 'defs': + definitions.extend(copy.deepcopy(list(node))) + elif kind == 'title': + metadata.append(copy.deepcopy(node)) + elif kind == 'g': + attrs = {key: value for key, value in node.attrib.items() if key != 'id'} + for child in node: visit(child, (*ancestors, attrs)) + else: + fill_cut = node.get('fill', '').lower() == source.lower() + stroke_cut = node.get('stroke', '').lower() == source.lower() + if not fill_cut and not stroke_cut: + output.append(wrap(copy.deepcopy(node), ancestors)) + return + # SVG paints fill before stroke. Split only affected shapes so a + # normal stroke is drawn after its cutout fill (and vice versa). + for part, cut in [('fill', fill_cut), ('stroke', stroke_cut)]: + paint = node.get(part, 'none') + if paint == 'none': continue + shape = copy.deepcopy(node) + shape.attrib.pop('id', None) + shape.set('stroke' if part == 'fill' else 'fill', 'none') + if cut: + shape.set(part, 'black') + erase(shape, ancestors) + else: + output.append(wrap(shape, ancestors)) + + for child in root: visit(child) + root[:] = [] + if definitions: + defs = element('defs', {}) + defs.extend(definitions) + root.append(defs) + root.extend(metadata) + root.extend(output) + return ET.tostring(root, encoding='unicode') + + +def themed(svg, palette, theme, accent='', vision='off', live=False, namespace=''): + gap = palette['roles'].get('gap') + if gap: svg = transparent_knockouts(svg, gap.get('source', gap['light'])) + values = role_values(palette, theme, accent, vision) + if live: values = {role: colour_ref(palette, theme, accent, role, vision) for role in values} + # Only non-blue dashed references become guides. Original blue action paints + # keep their semantic accent. Knockout mask coverage stays literal black. + preserved = {value.get('source', value['light']).lower() for role, value in palette['roles'].items() + if role == 'gap' or role.startswith('active-')} + # Resolve this paint after hex substitution, so a guide colour that happens + # to equal another source hex cannot be remapped a second time. + dashed = {} + def protect_dash(match): + tag = match[0] + if 'stroke-dasharray=' not in tag: return tag + def substitute(paint): + if paint[2].lower() in preserved: return paint[0] + key = '__GUIDE_PAINT_' + str(len(dashed)) + '__'; dashed[key] = values['guide'] + return 'stroke="' + key + '"' + return re.sub(r'stroke=(["\'])(#[0-9a-fA-F]{6}|currentColor)\1', substitute, tag) + svg = re.sub(r'<(?:path|circle)\b[^>]*>', protect_dash, svg) + paints = {value.get('source', value['light']).lower(): values[role] for role, value in palette['roles'].items() if role != 'line'} + svg = re.sub(r'#[0-9a-fA-F]{6}', lambda match: paints.get(match[0].lower(), match[0]), svg) + svg = svg.replace('currentColor', values['line']) + for key, value in dashed.items(): svg = svg.replace(key, value) + if namespace: + # Inline SVG ids share the page's global id namespace, including hidden + # theme/vision/size variants and aliases of the same master. + namespace = re.sub(r'[^a-zA-Z0-9_-]', '-', namespace) + ids = set(re.findall(r'\bid=["\']([^"\']+)["\']', svg)) + for name in ids: + scoped = namespace + '-' + name + svg = re.sub(r'\bid=(["\'])' + re.escape(name) + r'\1', lambda m: 'id="' + scoped + '"', svg) + svg = svg.replace('url(#' + name + ')', 'url(#' + scoped + ')') + return svg + + +def review_css(palette, live=False): + """Review controls follow the same shared colours; artwork is rendered by themed().""" + rules = [] + for dark in (False, True): + theme = 'dark' if dark else 'light' + for vision in VISIONS: + selector = 'body' + ('.dark' if dark else ':not(.dark)') + '[data-vision="' + vision + '"]' + primary = lambda accent='': colour_ref(palette, theme, accent, 'active-3' if 'active-3' in palette['roles'] else 'line', vision) if live else role_values(palette, theme, accent, vision).get('active-3', palette['roles']['line'][theme]) + values = {'accent': primary(), + 'accent-ink': colour_ref(palette, 'dark', '', 'line', vision) if live else palette['roles']['line']['dark'], + 'success': primary('sketch'), + 'warning': primary('mesh'), + 'error': primary('constraint')} + rules.append(selector + '{' + ''.join('--review-' + key + ':' + value + ';' for key, value in values.items()) + '}') + for accent in palette.get('accents', {}): + rules.append(selector + ' .card[data-accent="' + accent + '"]{--icon-accent:' + primary(accent) + '}') + return '\n'.join(rules) diff --git a/assets/icons/ui/design/tools/icon_status.py b/assets/icons/ui/design/tools/icon_status.py new file mode 100644 index 00000000..2f2446f3 --- /dev/null +++ b/assets/icons/ui/design/tools/icon_status.py @@ -0,0 +1,309 @@ +#!/usr/bin/env python3 +"""Repository-owned icon review tool (Python 3.11+, standard library only). + +--serve [PORT] Loopback review app, default http://127.0.0.1:8770/ +--summary Discovery counts, no generated source files +--csv Write target/icon-inventory.csv +--notes Open artwork feedback +--resolve ID... Clear feedback after an intentional redraw; protect newer notes manually +--out FILE Self-contained static review page (default target/icon-status.html) + +Read assets/icons/ui/STYLE_GUIDE.md before editing artwork. SVGs are authoritative. +Every discovered non-final icon is Open; no inventory file controls discovery. +""" +import sys, os, re, glob, html, json, hashlib, collections, tomllib, uuid, datetime + +HERE = os.path.dirname(os.path.abspath(__file__)) +D = os.path.dirname(HERE) # .../assets/icons/ui/design +ROOT = os.path.abspath(os.path.join(D, '..', '..', '..', '..')) +REVIEW = D + '/review.json' +REQUESTS = os.path.join(ROOT, 'target', 'codex-icon-review', 'inbox') +args = sys.argv[1:] +SERVER_REV = uuid.uuid4().hex if '--serve' in args else '' +out_path = args[args.index('--out') + 1] if '--out' in args else os.path.join(ROOT, 'target', 'icon-status.html') + +sys.path.insert(0, HERE) +import review_state +import icon_palette +load_review = review_state.load +master_hash = review_state.master_hash + +def art_rev(): + h = hashlib.sha256() + h.update(SERVER_REV.encode()) # A backend restart must refresh already-open review tabs too. + paths = glob.glob(D + '/svg/*/*.svg') + glob.glob(HERE + '/review/*') + paths += glob.glob(ROOT + '/crates/engine/src/**/*.rs', recursive=True) + paths += glob.glob(ROOT + '/crates/ui-egui/src/**/*.rs', recursive=True) + paths += glob.glob(HERE + '/*.py') + paths += [D + '/palette.toml', D + '/aliases.toml', __file__] + for path in sorted(paths): + st = os.stat(path) + h.update(('%s %d %d;' % (os.path.relpath(path, ROOT), st.st_mtime_ns, st.st_size)).encode()) + return h.hexdigest()[:16] + + +def ui_icon_names(source): + """Literal icon arguments, including the pattern and line-type UI wrappers.""" + names = set() + for call in re.finditer(r'icons::paint[a-z_]*\((.*?);', source, re.S): + names.update(re.findall(r'"([a-z][a-z0-9_]*)"', call.group(1))) + for call in re.finditer(r'\bpattern_toggle\([^;]*?\);', source, re.S): + # The icon is the fourth argument; the first strings are accessible labels. + icon = re.match(r'pattern_toggle\(\s*\w+\s*,\s*(?:"[^"]*"|\w+)\s*,\s*"[^"]*"\s*,\s*"([a-z][a-z0-9_]*)"', call.group()) + if icon: names.add(icon.group(1)) + for table in re.finditer(r'for\s*\(i,\s*\(label,\s*icon,\s*tip\)\)\s*in\s*\[(.*?)\]\s*\.iter\(', source, re.S): + names.update(re.findall(r'\("[^"]*",\s*"([a-z][a-z0-9_]*)",\s*"[^"]*"\)', table.group(1))) + return names + +def scan(): + """Everything derived from the repo: rows, summary.""" + pal = tomllib.load(open(D + '/palette.toml', 'rb')) + alias_raw = tomllib.load(open(D + '/aliases.toml', 'rb')).get('alias', {}) + global unassigned + unassigned = [] + masters = {} + for f in sorted(glob.glob(D + '/svg/*/*.svg')): + fam = os.path.basename(os.path.dirname(f)); n = os.path.basename(f)[:-4] + if fam != 'small': masters[n] = (fam, f) + uses = collections.defaultdict(list) # id -> command ids and "ui:" + where = collections.defaultdict(list) # id -> [(tab, panel, command)] + for f in glob.glob(ROOT + '/crates/engine/src/**/*.rs', recursive=True): + t = open(f, encoding='utf-8').read() + ms = list(re.finditer(r'(?:CommandSpec::new|planned!)\(\s*"([^"]+)"\s*,\s*"([^"]*)"', t)) + for i, m in enumerate(ms): + end = ms[i + 1].start() if i + 1 < len(ms) else len(t) + chunk = t[m.end():end] + ic = re.search(r'\.icon\(\s*"([^"]+)"', chunk) + if ic: + uses[ic.group(1)].append(m.group(1)) + at = re.search(r'\.at\(\s*"([^"]+)"\s*,\s*"([^"]+)"', chunk) + if at: where[ic.group(1)].append((at.group(1), at.group(2), m.group(1))) + else: unassigned.append(m.group(1)) + for f in glob.glob(ROOT + '/crates/ui-egui/src/**/*.rs', recursive=True): + t = open(f, encoding='utf-8').read() + for name in ui_icon_names(t): + uses[name].append('ui:' + os.path.relpath(f, ROOT + '/crates/ui-egui/src')) + mk = ROOT + '/assets/icons/ui/source/markers.rs' + if os.path.exists(mk): + for name in re.findall(r'=> "(c_[a-z_]+)"', open(mk, encoding='utf-8').read()): uses[name].append('ui:sketch constraint glyph') + def alias_of(n): + return icon_palette.resolve(pal, alias_raw, n) + targets = {alias_of(a)[0] for a in alias_raw} + rows = [] + for n in sorted(set(uses) | set(masters) | set(alias_raw)): + m, acc = alias_of(n) + if m in masters and n != 'placeholder': status = 'done' + else: status, m = 'placeholder', 'placeholder' + cmds = [u for u in uses.get(n, []) if not u.startswith('ui:')] + rows.append(dict(id=n, status=status, master=m, accent=acc, commands=len(cmds), example=cmds[0] if cmds else '', + ui=sorted({u[3:] for u in uses.get(n, []) if u.startswith('ui:')}), unused=not uses.get(n), + where=where.get(n, []), alias_target=n in targets)) + cnt = collections.Counter(r['status'] for r in rows if r['id'] != 'placeholder') + cmd_cnt = collections.Counter() + for r in rows: + if r['id'] != 'placeholder': cmd_cnt[r['status']] += r['commands'] + summary = '%d discovered icon ids · %d with artwork · %d missing · %d assigned commands.' % (sum(cnt.values()), cnt['done'], cnt['placeholder'], sum(cmd_cnt.values())) + if unassigned: summary += ' %d commands have no icon assignment.' % len(unassigned) + return rows, masters, pal, summary + +if '--resolve' in args: + ids = [a for a in args[args.index('--resolve') + 1:] if not a.startswith('--')] + cleared = review_state.resolve(ids) + print('cleared notes: ' + (' '.join(cleared) or 'none')) + sys.exit(0) + +rows, masters, pal, summary = scan() +print(summary) +if '--check' in args: sys.exit(1 if unassigned or any(r['status'] == 'placeholder' and r['id'] != 'placeholder' for r in rows) else 0) +if '--summary' in args and '--csv' not in args: sys.exit(0) + +if '--notes' in args: + rv = load_review(); n = 0 + valid = review_state.valid_final(rv) + for r in rows: + note = rv['notes'].get(r['id']) + if note and r['id'] not in valid: + n += 1; print('\n%s (master %s%s)\n %s' % (r['id'], r['master'], ', ' + r['accent'] + ' accent' if r['accent'] else '', note.replace('\n', '\n '))) + print('\n%d icon(s) with notes to work on; %d final of %d.' % (n, len(rv['final']), len([r for r in rows if r['id'] != 'placeholder']))) + sys.exit(0) + +if '--csv' in args: + csv_path = os.path.join(ROOT, 'target', 'icon-inventory.csv') + os.makedirs(os.path.dirname(csv_path), exist_ok=True) + with open(csv_path, 'w', encoding='utf-8') as f: + f.write('icon,status,master,accent,commands,example_command,used_by_ui\n') + for r in rows: + if r['id'] == 'placeholder': continue + f.write('%s,%s,%s,%s,%d,%s,"%s"\n' % (r['id'], r['status'], r['master'] if r['status'] != 'placeholder' else '', r['accent'], r['commands'], r['example'], ';'.join(r['ui']))) + print('wrote', os.path.relpath(csv_path, ROOT)) + +# --- grouping: toolbar tab / panel, else where the app shows the icon ------------------------- +TABS = ['SOLID', 'SURFACE', 'MESH', 'SHEET METAL', 'PLASTIC', 'RENDER', 'SKETCH'] +PANELS = ['CREATE', 'MODIFY', 'ASSEMBLE', 'POSITION', 'CONFIGURE', 'CONSTRUCT', 'CONSTRAINTS', 'SETUP', 'SCENE', 'IMAGE', 'INSPECT', 'INSERT', 'SELECT', 'FINISH SKETCH'] +UI_NAMES = {'browser.rs': 'Browser', 'timeline.rs': 'Timeline', 'timeline_left.rs': 'Timeline', 'context_menu.rs': 'Context menus', 'update.rs': 'Update banner', + 'sketch constraint glyph': 'Sketch constraint glyphs'} +def rank(l, v): return l.index(v) if v in l else len(l) +def group_of(r): + if r['where']: + tab, panel, _ = min(r['where'], key=lambda w: (rank(TABS, w[0]), rank(PANELS, w[1]))) + return (rank(TABS, tab), rank(PANELS, panel), tab, panel) + if r['ui']: + names = sorted({UI_NAMES.get(u, u.replace('.rs', '').replace('_', ' ').title()) for u in r['ui']}) + return (50, 0, 'IN THE APP', ' · '.join(names)) + if r['commands']: return (60, 0, 'MENUS & PALETTE', 'commands without a toolbar button') + if r['alias_target']: return (65, 0, 'SHARED MASTERS', 'used through aliases') + return (70, 0, 'UNUSED', 'no app use yet') + +def themed(svg, theme, accent, pal, vision='off', live=False, namespace=''): + return icon_palette.themed(svg, pal, theme, accent, vision, live, namespace) + +def palette_state(): + path = D + '/palette.toml' + return {'revision': icon_palette.revision(path), 'tokens': icon_palette.tokens(tomllib.load(open(path, 'rb')))} + +def build_page(serving): + rows, masters, pal, summary = scan() + rv = load_review() + valid = review_state.valid_final(rv) + shown = [r for r in rows if r['id'] != 'placeholder'] + shown.sort(key=lambda r: (group_of(r), r['id'])) + cards = []; last = None; tabs_seen = [] + protected = collections.defaultdict(list) + for name in valid: protected[review_state.master_of(name)].append(name) + for r in shown: + g = group_of(r) + if last is None or g[:2] != last[:2]: + first_of_tab = last is None or g[2] != last[2] + if first_of_tab and g[2] not in tabs_seen: tabs_seen.append(g[2]) + txt = (g[2] + ' ▸ ' + g[3]) if first_of_tab else g[3] + cards.append('%s' % (' tab' if first_of_tab else '', html.escape(g[2]), html.escape(g[2] + ' ▸ ' + g[3]), html.escape(txt))) + last = g + missing = r['status'] == 'placeholder' + svg_f = open(masters[r['master']][1], encoding='utf-8').read() if r['master'] in masters else 'Missing artwork' + small = glob.glob(D + '/svg/small/' + r['master'] + '.svg') + small_svg = open(small[0], encoding='utf-8').read() if small else None + inner = '' + for vision in icon_palette.VISIONS: + inner += '' + for theme, cls in [('light', 'al'), ('dark', 'ad')]: + scope = 'review-' + r['id'] + '-' + vision + '-' + theme + inner += '' + themed(svg_f, theme, r['accent'], pal, vision, live=True, namespace=scope + '-full') + '' + if small_svg: + inner += '' + themed(small_svg, theme, r['accent'], pal, vision, live=True, namespace=scope + '-small') + '' + inner += '' + src = ('= ' + r['master']) if r['master'] != r['id'] else '' + tip = '; '.join('%s ▸ %s: %s' % w for w in r['where']) or ', '.join(r['ui']) or ('%d command(s)' % r['commands']) + extra = ((' %s accent' % r['accent']) if r['accent'] else '') + (' %s' % r['status'] if r['status'] != 'done' else '') + if r['id'] not in valid and protected[r['master']]: + extra += ' shared artwork accepted' + if r['id'] in rv['final'] and r['id'] not in valid: extra += ' changed since acceptance' + cards.append('
%s
%s%s%s
%s
' + '
' % ( + html.escape(r['id']), html.escape(r['accent']), html.escape(g[2]), html.escape(r['id'] + ' ' + r['example'] + ' ' + r['accent'] + ' ' + g[3].lower()), html.escape(tip), 'true' if missing else 'false', + ' hs' if small_svg else '', inner, html.escape(r['id']), (' ' + html.escape(src) + '') if src else '', extra, html.escape(r['example'] or ', '.join(r['ui']) or ''), html.escape(r['id']), 'disabled' if missing else '')) + tabchips = ''.join('' % (html.escape(t), html.escape(t.title())) for t in tabs_seen) + state = json.dumps({'final': sorted(valid), 'notes': rv['notes'], 'serving': serving, 'rev': art_rev(), 'palette': palette_state(), 'hashes': {r['id']: master_hash(r['master']) for r in shown if r['status'] != 'placeholder'}}).replace('<', r'\u003c') + return load_template().replace('@@PALETTE_CSS@@', icon_palette.palette_css(pal) + '\n' + icon_palette.review_css(pal, live=True)).replace('@@GUIDE@@', '/guide' if serving else os.path.basename(out_path) + '.guide.html').replace('@@WARNINGS@@', ('Unassigned commands: ' + html.escape(', '.join(unassigned))) if unassigned else '').replace('@@SUMMARY@@', html.escape(summary)).replace('@@CARDS@@', ''.join(cards)).replace('@@TABS@@', tabchips).replace('@@STATE@@', state) + +WEB = os.path.join(HERE, 'review') +def load_template(): + template = open(os.path.join(WEB, 'index.html'), encoding='utf-8').read() + return template.replace('@@CSS@@', open(os.path.join(WEB, 'review.css'), encoding='utf-8').read()).replace('@@JS@@', open(os.path.join(WEB, 'review.js'), encoding='utf-8').read()) + + +def build_guide(): + sys.path.insert(0, os.path.join(HERE, 'author')) + from bases import examples + rows, masters, pal, _ = scan() + by_id = {r['id']: r for r in rows} + catalogue = tomllib.load(open(D + '/references.toml', 'rb')) + sections = '

Canonical bases

' + for label, svg in examples().items(): + sections += '
' + label + '' + themed(svg, 'light', '', pal) + '
' + sections += '
' + for category in catalogue['category'].values(): + sections += '

' + html.escape(category['label']) + '

' + html.escape(category['rule']) + '

' + for name in category['icons']: + row = by_id[name]; svg = open(masters[row['master']][1], encoding='utf-8').read() + sections += '
' + name + '
' + themed(svg, 'light', row['accent'], pal, namespace='guide-' + category['label'].replace(' ', '-') + '-' + name + '-light') + '
' + themed(svg, 'dark', row['accent'], pal, namespace='guide-' + category['label'].replace(' ', '-') + '-' + name + '-dark') + '
' + sections += '
' + template = open(os.path.join(WEB, 'guide.html'), encoding='utf-8').read() + return template.replace('@@EXAMPLES@@', sections) + + +if '--serve' in args: + import http.server + port = int(args[args.index('--serve') + 1]) if len(args) > args.index('--serve') + 1 and args[args.index('--serve') + 1].isdigit() else 8770 + _r = load_review(); review_state.save(_r['final'], _r['notes']) # fills in missing hashes + class H(http.server.BaseHTTPRequestHandler): + def log_message(self, *a): pass + def _send(self, code, body, ctype): + b = body.encode(); self.send_response(code); self.send_header('Content-Type', ctype); self.send_header('Content-Length', str(len(b))); self.send_header('Cache-Control', 'no-store'); self.end_headers(); self.wfile.write(b) + def do_GET(self): + if self.path.split('?')[0] in ('/', '/index.html'): self._send(200, build_page(True), 'text/html; charset=utf-8') + elif self.path.split('?')[0] == '/api/review': self._send(200, json.dumps({'final': sorted(review_state.valid_final()), 'notes': load_review()['notes'], 'rev': art_rev()}), 'application/json') + elif self.path.split('?')[0] == '/api/palette': self._send(200, json.dumps(palette_state()), 'application/json') + elif self.path.split('?')[0] == '/guide': self._send(200, build_guide(), 'text/html; charset=utf-8') + else: self._send(404, 'not found', 'text/plain') + def do_POST(self): + with review_state.LOCK: self._post() + def _post(self): + if self.path not in ('/api/review', '/api/export', '/api/palette'): return self._send(404, 'not found', 'text/plain') + try: + n = int(self.headers.get('Content-Length', '0')) + if n <= 0 or n > 4_000_000: raise ValueError('invalid request size') + inc = json.loads(self.rfile.read(n)) + if self.path == '/api/palette': + icon_palette.save_colours(D + '/palette.toml', inc.get('revision'), inc.get('changes')) + return self._send(200, json.dumps({'ok': True, **palette_state()}), 'application/json') + base = inc.get('base') + if not isinstance(base, dict): raise ValueError('stale page: reload it') + cur = load_review(); old = cur['final'] + # three-way merge: apply what this page changed since the state it last saw (`base`) onto the file as it is now + bf = set(base.get('final', [])); cf = set(inc.get('final', [])) + added = cf - bf + if added: + available = {r['id'] for r in scan()[0] if r['status'] != 'placeholder'} + if not added <= available: raise ValueError('missing artwork cannot be accepted') + if inc.get('rev') != art_rev(): raise ValueError('artwork changed: reload before accepting it') + final = (set(old) | added) - (bf - cf) + notes = dict(cur['notes']); bn = base.get('notes', {}); cn = inc.get('notes', {}) + for k in set(bn) | set(cn): + if bn.get(k, '') != cn.get(k, ''): + if cn.get(k, '').strip(): notes[k] = cn[k] + else: notes.pop(k, None) + review_state.save({i: '' if i in added else old.get(i, '') for i in final}, notes) + res = {'ok': True} + if self.path == '/api/export': + r = load_review(); valid = review_state.valid_final(r) + protected = {review_state.master_of(i) for i in valid} + todo = sorted(i for i in r['notes'] if i not in valid and review_state.master_of(i) not in protected) + res['sent'] = len(todo) + if todo: + os.makedirs(REQUESTS, exist_ok=True) + request_id = uuid.uuid4().hex + path = os.path.join(REQUESTS, request_id + '.json') + # Publish a complete snapshot atomically so the chat never reads half a request. + with open(path + '.tmp', 'w') as f: + json.dump({'version': 1, 'id': request_id, + 'created_at': datetime.datetime.now(datetime.timezone.utc).isoformat(), + 'notes': {i: r['notes'][i] for i in todo}}, f, indent=1) + f.write('\n') + os.replace(path + '.tmp', path) + res['request_id'] = request_id + self._send(200, json.dumps(res), 'application/json') + except Exception as e: + self._send(400, json.dumps({'ok': False, 'error': str(e)}), 'application/json') + srv = http.server.ThreadingHTTPServer(('127.0.0.1', port), H) + print('review page: http://127.0.0.1:%d (saves %s; Ctrl-C to stop)' % (port, os.path.relpath(REVIEW, ROOT))) + try: srv.serve_forever() + except KeyboardInterrupt: pass + sys.exit(0) + +page = build_page(False) +os.makedirs(os.path.dirname(out_path), exist_ok=True) +open(out_path, 'w', encoding='utf-8').write(page) +if os.path.exists(D + '/references.toml'): + guide = build_guide().replace('href="/"', 'href="' + html.escape(os.path.basename(out_path)) + '"') + open(out_path + '.guide.html', 'w', encoding='utf-8').write(guide) +print('wrote', os.path.relpath(out_path, ROOT), '(%d KB)' % (len(page) // 1024)) diff --git a/assets/icons/ui/design/tools/lint_icons.py b/assets/icons/ui/design/tools/lint_icons.py new file mode 100644 index 00000000..c8bce5ae --- /dev/null +++ b/assets/icons/ui/design/tools/lint_icons.py @@ -0,0 +1,95 @@ +#!/usr/bin/env python3 +"""Rules every icon master in svg/ must satisfy (stdlib only). usage: lint_icons.py """ +import sys, glob, os, re, tomllib, xml.etree.ElementTree as ET +import icon_palette +from pathlib import Path +d = sys.argv[1] if len(sys.argv) > 1 else os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +pal = tomllib.load(open(d + '/palette.toml', 'rb')) +light = {r.get('source', r['light']) for r in pal['roles'].values()} +OK_TAGS = {'svg', 'title', 'path', 'circle', 'defs', 'mask', 'clipPath', 'g'} +FAMILIES = {'model', 'small', 'flat', 'workspace'} +bad = [] +def err(f, m): bad.append('%s: %s' % (os.path.relpath(f, d), m)) +files = sorted(glob.glob(d + '/svg/*/*.svg')) +# Keep the collection free of generated previews, reports and authoring snapshots. +for path in Path(d).rglob('*'): + if not path.is_file(): continue + rel = path.relative_to(d).as_posix() + allowed = (rel in {'palette.toml', 'aliases.toml', 'review.json', 'references.toml'} + or path.suffix == '.md' + or (rel.startswith('svg/') and path.suffix == '.svg') + or (path.parent in {Path(d) / 'tools', Path(d) / 'tools/author'} and path.suffix == '.py') + or (path.parent == Path(d) / 'tools/review' and path.suffix in {'.html', '.css', '.js'})) + if not allowed: err(str(path), 'generated or unsupported resource; keep temporary output under target/') +for f in files: + fam, name = os.path.basename(os.path.dirname(f)), os.path.basename(f)[:-4] + if fam not in FAMILIES: err(f, 'unknown family'); continue + if not re.fullmatch(r'[a-z][a-z0-9_]*', name): err(f, 'file name must be an app icon id (lower_snake_case)') + t = ET.parse(f).getroot(); ns = '{http://www.w3.org/2000/svg}' + if t.get('viewBox') != '0 0 64 64': err(f, 'viewBox must be 0 0 64 64') + for a in ('width', 'height'): + if t.get(a): err(f, a + ' attribute on svg') + ids = [] + def walk(e, mask): + for c in e: + tag = c.tag.replace(ns, '') + if tag not in OK_TAGS: err(f, 'element <%s> not allowed' % tag) + for a in ('transform', 'style', 'class'): + if c.get(a): err(f, '%s attribute on <%s>' % (a, tag)) + if c.get('id'): ids.append(c.get('id')) + if tag == 'path' and not re.fullmatch(r'[MLCQAZ0-9 .\-]+', c.get('d', '')): err(f, 'path data may use only absolute M L C Q A Z') + m = mask or tag in ('mask', 'clipPath') + for k in ('fill', 'stroke'): + v = c.get(k) + if v in (None, 'none') or v.startswith('url('): continue + if m and v in ('white', 'black'): continue + if v == 'currentColor': + continue + if v.lower() not in light: err(f, '%s="%s" is not a palette colour' % (k, v)) + if fam == 'flat' and v.lower() != '#ffffff': err(f, 'flat icons are single colour: use currentColor, not %s' % v) + if c.get('stroke') not in (None, 'none'): + if not c.get('stroke-width'): err(f, 'stroke without stroke-width') + if c.get('stroke-linejoin') != 'round' or c.get('stroke-linecap') != 'round': err(f, 'strokes need round joins and caps') + if tag in ('path', 'circle') and c.get('fill') is None and c.get('stroke') is None and not m: err(f, '<%s> without paint' % tag) + walk(c, m) + walk(t, False) + if len(ids) != len(set(ids)): err(f, 'duplicate ids') +# every small/ master has a model/ counterpart; aliases point at real masters +names = {os.path.basename(f)[:-4] for f in files if '/small/' not in f} +for f in files: + if '/small/' in f and os.path.basename(f)[:-4] not in names: err(f, 'small variant without a master') +al = tomllib.load(open(d + '/aliases.toml', 'rb')).get('alias', {}) +for a, t in al.items(): + if isinstance(t, dict): + if t.get('accent') not in pal.get('accents', {}): err(d + '/aliases.toml', '%s: unknown accent %s' % (a, t.get('accent'))) + t = t.get('master') + if t not in names: err(d + '/aliases.toml', '%s -> %s: no such master' % (a, t)) + if a in names: err(d + '/aliases.toml', '%s is both an alias and a master' % a) +primary = {} +for file in files: + family, name = Path(file).parent.name, Path(file).stem + if family != 'small': primary.setdefault(name, set()).add(family) +for name, families in primary.items(): + if len(families) > 1 and not (name == 'folder' and families == {'flat', 'model'}): + err(d + '/svg', name + ': duplicate primary master across families') +references = d + '/references.toml' +if os.path.exists(references): + for category in tomllib.load(open(references, 'rb')).get('category', {}).values(): + for name in category['icons']: + if name not in names and name not in al: err(references, name + ': reference artwork is missing') +for message in icon_palette.validation_errors(pal, names | set(al)): + err(d + '/palette.toml', message) +# an icon accepted as final on the review page must not change (un-accept it there to work on it again) +import json, hashlib +rp = d + '/review.json' +if os.path.exists(rp): + fin = json.load(open(rp)).get('final', {}) + for iid, want in (fin.items() if isinstance(fin, dict) else []): + v = al.get(iid, iid); m = v.get('master') if isinstance(v, dict) else v + if not want: continue + h = hashlib.sha256() + for f in sorted(glob.glob(d + '/svg/*/' + m + '.svg')): + h.update(os.path.relpath(f, d).replace(os.sep, '/').encode()); h.update(open(f, 'rb').read()) + if h.hexdigest() != want: err(rp, 'final icon %s changed (master %s): un-accept it on the review page before editing it' % (iid, m)) +print('\n'.join(bad) if bad else 'icons: %d masters, palette, naming and element rules ok' % len(files)) +sys.exit(1 if bad else 0) diff --git a/assets/icons/ui/design/tools/preview.py b/assets/icons/ui/design/tools/preview.py new file mode 100644 index 00000000..69bf160d --- /dev/null +++ b/assets/icons/ui/design/tools/preview.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 +"""Contact sheet of svg/ in both themes at 16/20/26/64 px, resolving roles through palette.toml. +usage: preview.py (serve the design dir and open the html)""" +import sys, glob, os, tomllib +import icon_palette +d, out = sys.argv[1], sys.argv[2] +palette = tomllib.load(open(d + '/palette.toml', 'rb')) +aliases = tomllib.load(open(d + '/aliases.toml', 'rb')).get('alias', {}) +def themed(svg, theme, icon, family, size): + _, accent = icon_palette.resolve(palette, aliases, icon) + return icon_palette.themed(svg, palette, theme, accent, namespace='preview-' + family + '-' + icon + '-' + theme + '-' + str(size)) +h = '' +for theme in ('light', 'dark'): + for fam in sorted(os.listdir(d + '/svg')): + h += '

%s - %s

' % (theme, fam, theme) + for f in sorted(glob.glob('%s/svg/%s/*.svg' % (d, fam))): + svg, icon = open(f).read(), os.path.basename(f)[:-4] + h += '
' + h += '
%s
' % themed(svg, theme, icon, fam, 64).replace('%s' % (s, s, themed(svg, theme, icon, fam, s).replace('' + icon + '
' + h += '
' +open(out, 'w').write(h) diff --git a/assets/icons/ui/design/tools/review/guide.html b/assets/icons/ui/design/tools/review/guide.html new file mode 100644 index 00000000..06219938 --- /dev/null +++ b/assets/icons/ui/design/tools/review/guide.html @@ -0,0 +1,8 @@ + +SolidCraft icon style references + +

Icon style references

Return to review. The binding specification is assets/icons/ui/STYLE_GUIDE.md. +Read it before editing. These are original SolidCraft bases and finished visual references, shown directly from the current SVG masters.

+

64-unit grid · 2.55 outlines · 4.25 action strokes · round caps and joins · standard 30° isometric axes · top light, left medium, right dark.

+@@EXAMPLES@@ +

Reference lists do not control discovery. New SVGs and aliases appear in Open automatically; acceptance is stored in design/review.json.

diff --git a/assets/icons/ui/design/tools/review/index.html b/assets/icons/ui/design/tools/review/index.html new file mode 100644 index 00000000..d637d154 --- /dev/null +++ b/assets/icons/ui/design/tools/review/index.html @@ -0,0 +1,25 @@ + + +SolidCraft icon review + + +
+
+
Icon coloursLight · Dark · PurposeLive preview · Apply saves palette.toml
+
+
+

Icon review

+ + @@TABS@@ + size + + + Style guide + +
@@SUMMARY@@
+

@@WARNINGS@@

+
+

Every discovered icon without a current acceptance is Open, including unused masters and icons with notes. ✓ accepts the current artwork. ✎ adds feedback.

+
@@CARDS@@
+ diff --git a/assets/icons/ui/design/tools/review/review.css b/assets/icons/ui/design/tools/review/review.css new file mode 100644 index 00000000..f6aa119e --- /dev/null +++ b/assets/icons/ui/design/tools/review/review.css @@ -0,0 +1,62 @@ +:root{--bg:#f4f6f9;--fg:#1d2733;--card:#fff;--line:#d5dbe3;--mut:#6b7a8a;--sz:48px} +body.dark{--bg:#1e2530;--fg:#dbe4ee;--card:#262f3c;--line:#3a4657;--mut:#8a9db0} +body{font:12px system-ui,sans-serif;margin:0;background:var(--bg);color:var(--fg)} +header{position:sticky;top:0;background:var(--card);border-bottom:1px solid var(--line);padding:6px 12px;z-index:5;display:flex;flex-wrap:wrap;gap:4px 10px;align-items:center} +h1{font-size:14px;margin:0}.sum{color:var(--mut)} +button,input,select{font:inherit;color:inherit;padding:2px 8px;border:1px solid var(--line);border-radius:6px;background:var(--card);cursor:pointer} +button.on{background:var(--review-accent);color:var(--review-accent-ink);border-color:var(--review-accent)} +.grp{display:flex;gap:3px;align-items:center}.grp small{color:var(--mut)} +main{display:flex;flex-wrap:wrap;gap:6px;padding:8px 12px;align-items:stretch} +.gm{flex:0 0 11px;writing-mode:vertical-rl;transform:rotate(180deg);font:600 8px/11px system-ui;letter-spacing:.04em;color:var(--mut);background:var(--line);border-radius:3px;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;text-transform:uppercase;font-style:normal;max-height:120px;align-self:stretch} +.gm.tab{background:var(--review-accent);color:var(--review-accent-ink)} +.card{position:relative;flex:0 0 118px;background:var(--card);border:1px solid var(--line);border-radius:7px} +.art{display:flex;justify-content:center;align-items:center;padding:8px 0 2px;min-height:calc(var(--sz) + 10px)} +.art svg{width:var(--sz);height:var(--sz)} +.art .ad,.art .sd,.art .sl{display:none} +body.dark .art .al{display:none}body.dark .art .ad{display:inline} +body.small .art.hs .al,body.small .art.hs .ad{display:none} +body.small:not(.dark) .art.hs .sl{display:inline}body.small.dark .art.hs .sd{display:inline} +.meta{padding:2px 6px 5px;line-height:1.35;word-break:break-all}.meta small{color:var(--mut);font-size:10px} +.src,.acc,.st{font-size:10px;color:var(--mut);font-style:normal}.src{color:var(--mut)}.st{color:var(--review-error)} +.ck,.nt{position:absolute;top:3px;width:20px;height:20px;padding:0;line-height:18px;border-radius:50%;font-size:12px;color:var(--mut);opacity:.55} +.ck{right:3px}.nt{left:3px;border-radius:5px} +.card:hover .ck,.card:hover .nt{opacity:1} +.card.final{border-color:var(--review-success)}.card.final .art,.card.final .meta{opacity:.6} +.card.final .ck{background:var(--review-success);color:var(--review-accent-ink);border-color:var(--review-success);opacity:1} +.card.final .nt{display:none} +.card.hasnote{border-color:var(--review-warning)}.card.hasnote .nt{background:var(--review-warning);color:var(--review-accent-ink);border-color:var(--review-warning);opacity:1} +.pop{position:absolute;left:0;top:100%;z-index:9;width:250px;background:var(--card);border:1px solid var(--review-accent);border-radius:7px;padding:6px;box-shadow:0 6px 18px #0004;display:none} +.card.open .pop{display:block}.card.open{z-index:9} +.pop textarea{width:100%;box-sizing:border-box;height:90px;font:12px system-ui;color:inherit;background:var(--bg);border:1px solid var(--line);border-radius:5px} +#msg{color:var(--review-error)} +[hidden]{display:none!important} +header .detail{width:100%;display:flex;gap:14px;align-items:center;font-size:11px} +header a{color:var(--fg);text-decoration:underline;text-underline-offset:2px} +.help{color:var(--mut);font-size:11px;margin:8px 12px 0} +.warning:empty{display:none}.warning,.error{color:var(--review-error)} +.warning{width:100%;margin:0;font-size:11px} +#msg{color:var(--mut)}#msg.error{color:var(--review-error)} +button:disabled{cursor:not-allowed;opacity:.3} +button:focus-visible,input:focus-visible,a:focus-visible{outline:2px solid var(--review-accent);outline-offset:2px} +#empty{padding:30px;color:var(--mut)} + +.shared{display:block;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:var(--mut);font-size:10px;word-break:normal} + +.vision{display:none}body[data-vision="off"] .vision-off,body[data-vision="red-green"] .vision-red-green,body[data-vision="blue-yellow"] .vision-blue-yellow{display:contents} + +.acc{color:var(--icon-accent,var(--mut))} + +/* Temporary palette tuning controls; keep them separate from review/acceptance. */ +#palette-editor{flex-basis:100%;border-bottom:1px solid var(--line);padding:2px 0 8px} +.palette-heading{display:flex;align-items:center;flex-wrap:wrap;gap:6px 12px;margin-bottom:6px} +#palette-status{color:var(--mut);font-size:11px}#palette-status.error{color:var(--review-error)} +#palette-tokens{display:grid;grid-template-columns:repeat(auto-fit,minmax(175px,1fr));gap:3px 12px;max-height:180px;overflow:auto} +.palette-token{display:flex;align-items:center;gap:5px;min-width:0;line-height:20px} +.palette-token span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap} +.palette-token input[type=color]{appearance:none;-webkit-appearance:none;box-sizing:border-box;width:19px;height:19px;border:1px solid var(--line);border-radius:50%;padding:0;background:transparent;flex-shrink:0;overflow:hidden} +.palette-token input[type=color]::-webkit-color-swatch-wrapper{padding:0} +.palette-token input[type=color]::-webkit-color-swatch{border:0;border-radius:50%} +.palette-token input[type=color]::-moz-color-swatch{border:0;border-radius:50%} +.palette-token input:disabled{cursor:default;opacity:1} +.palette-knockout{box-sizing:border-box;width:19px;height:19px;flex-shrink:0;border:1px solid var(--line);border-radius:50%;background:repeating-conic-gradient(#8993a0 0% 25%,transparent 0% 50%) 0 0/8px 8px} +body.palette-preview .card.final .art{opacity:1} diff --git a/assets/icons/ui/design/tools/review/review.js b/assets/icons/ui/design/tools/review/review.js new file mode 100644 index 00000000..d71ae4b0 --- /dev/null +++ b/assets/icons/ui/design/tools/review/review.js @@ -0,0 +1,285 @@ +// No framework, build step, external resources or model integration. +const S = @@STATE@@; +const fin = new Set(S.final); +const notes = {...S.notes}; +let base = {final: [...S.final].sort(), notes: {...notes}}; +let filter = 'open', tab = '', dirty = false, timer; +let saves = Promise.resolve(); +const query = document.getElementById('q'); +const message = document.getElementById('msg'); +const cards = [...document.querySelectorAll('.card')]; +const markers = [...document.querySelectorAll('.gm')]; +const staticKey = 'icon-review:' + S.rev; +const prefsKey = 'icon-review-preferences'; +const reloadKey = 'icon-review-reload-view'; + +function state() { + return {final: [...fin].sort(), notes: Object.fromEntries( + Object.entries(notes).filter(([, value]) => value.trim()))}; +} +function payload(snapshot) { return JSON.stringify({base, ...snapshot, rev: S.rev}); } +function showMessage(text, error = false) { + message.textContent = text; + message.classList.toggle('error', error); +} +function run() { + let finals = 0, noted = 0, visible = 0; + for (const card of cards) { + const id = card.dataset.id; + const accepted = fin.has(id), hasNote = Boolean((notes[id] || '').trim()); + card.classList.toggle('final', accepted); + card.classList.toggle('hasnote', hasNote && !accepted); + card.querySelector('.ck').title = accepted ? 'Reopen for review' : 'Accept as final'; + card.querySelector('.ck').setAttribute('aria-label', (accepted ? 'Reopen ' : 'Accept ') + id + (accepted ? '' : ' as final')); + if (accepted) finals++; + if (hasNote && !accepted) noted++; + const matches = (filter === 'all' || (filter === 'open' && !accepted) || + (filter === 'notes' && hasNote && !accepted) || (filter === 'final' && accepted)) && + (!tab || card.dataset.tab === tab) && card.dataset.k.includes(query.value.toLowerCase()); + card.hidden = !matches; + if (matches) visible++; + } + for (const marker of markers) { + let next = marker.nextElementSibling, any = false; + while (next && !next.classList.contains('gm')) { + if (!next.hidden) any = true; + next = next.nextElementSibling; + } + marker.hidden = !any; + } + document.getElementById('cnt').textContent = `${finals} final · ${cards.length - finals} open · ${noted} with notes`; + document.getElementById('empty').hidden = visible > 0; +} +function scheduleSave() { + dirty = true; + clearTimeout(timer); + timer = setTimeout(flush, 250); +} +async function flush() { + clearTimeout(timer); + const snapshot = state(); + saves = saves.catch(() => {}).then(async () => { + if (!S.serving) { + localStorage.setItem(staticKey, JSON.stringify(snapshot)); + dirty = JSON.stringify(snapshot) !== JSON.stringify(state()); + showMessage('Static preview: Export downloads the review file. Use --serve to save to the repository.'); + return; + } + const response = await fetch('/api/review', {method: 'POST', body: payload(snapshot)}); + const result = await response.json(); + if (!response.ok || !result.ok) throw new Error(result.error || 'Save failed'); + base = JSON.parse(JSON.stringify(snapshot)); + dirty = JSON.stringify(snapshot) !== JSON.stringify(state()); + showMessage('Saved'); + }); + try { await saves; return true; } + catch (error) { showMessage(error.message, true); return false; } +} +function view() { + return {filter, tab, q: query.value, y: scrollY}; +} +function preferences() { + return {dark: document.body.classList.contains('dark'), vision: document.body.dataset.vision, size: document.documentElement.style.getPropertyValue('--sz')}; +} +function savePreferences() { try { localStorage.setItem(prefsKey, JSON.stringify(preferences())); } catch {} } +function setSize(size) { + document.documentElement.style.setProperty('--sz', size + 'px'); + document.body.classList.toggle('small', size <= 24); + document.querySelectorAll('.sz').forEach(button => button.classList.toggle('on', +button.dataset.s === size)); +} +function selectFilter(value) { + filter = value; + document.querySelectorAll('[data-f]').forEach(button => button.classList.toggle('on', button.dataset.f === value)); +} +function selectTab(value) { + tab = value; + document.querySelectorAll('.chip').forEach(button => button.classList.toggle('on', (button.dataset.tab || '') === value)); +} + +// Temporary palette editor: CSS variables recolour every rendered theme, size +// and vision variant without touching SVGs, notes or acceptance hashes. +const paletteDraftKey = 'icon-review-palette-draft'; +const paletteStatus = document.getElementById('palette-status'); +const paletteApply = document.getElementById('palette-apply'); +const paletteReset = document.getElementById('palette-reset'); +const palettePaints = Object.fromEntries(S.palette.tokens.filter(token => !token.linked && !token.knockout) + .map(token => [token.id, {light: token.light, dark: token.dark}])); +const paletteInputs = []; +let paletteDirty = false, paletteSaving = false; +function paletteVariable(id, theme) { return '--icon-palette-' + id.replaceAll('.', '-') + '-' + theme; } +function paletteChanges() { + return Object.fromEntries(S.palette.tokens.filter(token => !token.linked && !token.knockout && + ['light', 'dark'].some(theme => palettePaints[token.id][theme] !== token[theme])) + .map(token => [token.id, {...palettePaints[token.id]}])); +} +function updatePalette() { + for (const [id, paints] of Object.entries(palettePaints)) { + for (const theme of ['light', 'dark']) document.documentElement.style.setProperty(paletteVariable(id, theme), paints[theme]); + } + for (const {input, token, theme} of paletteInputs) { + input.value = palettePaints[token.linked || token.id][theme]; + input.title = `${token.name} · ${theme}: ${input.value}${token.linked ? ' (follows construction stroke)' : ''}`; + } + paletteDirty = Object.keys(paletteChanges()).length > 0; + paletteApply.disabled = !paletteDirty || paletteSaving; + paletteReset.disabled = !paletteDirty || paletteSaving; + try { + if (paletteDirty) sessionStorage.setItem(paletteDraftKey, JSON.stringify({revision: S.palette.revision, changes: paletteChanges()})); + else sessionStorage.removeItem(paletteDraftKey); + } catch {} +} +for (const token of S.palette.tokens) { + const row = document.createElement('div'); row.className = 'palette-token'; + for (const theme of ['light', 'dark']) { + if (token.knockout) { + const swatch = document.createElement('span'); swatch.className = 'palette-knockout'; + swatch.title = token.name + ' · ' + theme + ': transparent cutout'; + swatch.setAttribute('aria-label', swatch.title); row.appendChild(swatch); continue; + } + const input = document.createElement('input'); input.type = 'color'; input.value = token[theme]; + input.disabled = Boolean(token.linked); + input.setAttribute('aria-label', token.name + ' ' + theme + ' colour'); + input.oninput = () => { + palettePaints[token.id][theme] = input.value; + updatePalette(); paletteStatus.textContent = 'Preview only · Apply saves both themes to palette.toml'; + paletteStatus.classList.remove('error'); + }; + paletteInputs.push({input, token, theme}); row.appendChild(input); + } + const label = document.createElement('span'); label.textContent = token.name + (token.linked ? ' ↗' : ''); + row.appendChild(label); document.getElementById('palette-tokens').appendChild(row); +} +document.body.classList.add('palette-preview'); +document.getElementById('palette-all').onclick = () => { selectFilter('all'); selectTab(''); query.value = ''; run(); }; +paletteReset.onclick = () => { + for (const token of S.palette.tokens.filter(token => !token.linked && !token.knockout)) palettePaints[token.id] = {light: token.light, dark: token.dark}; + updatePalette(); paletteStatus.textContent = 'Preview reset · saved palette unchanged'; paletteStatus.classList.remove('error'); +}; +paletteApply.onclick = async () => { + if (!paletteDirty || paletteSaving) return; + if (dirty && !await flush()) return; + if (!S.serving) { + paletteStatus.textContent = 'Preview only · run the review server to apply colours to palette.toml'; return; + } + paletteSaving = true; updatePalette(); + try { + const response = await fetch('/api/palette', {method: 'POST', body: JSON.stringify({revision: S.palette.revision, changes: paletteChanges()})}); + const result = await response.json(); + if (!response.ok || !result.ok) throw new Error(result.error || 'Palette save failed'); + S.palette = {revision: result.revision, tokens: result.tokens}; + paletteStatus.textContent = 'Saved to palette.toml · rebuild the app to use these colours'; + paletteStatus.classList.remove('error'); + } catch (error) { paletteStatus.textContent = error.message; paletteStatus.classList.add('error'); } + finally { paletteSaving = false; updatePalette(); } +}; +try { + const draft = JSON.parse(sessionStorage.getItem(paletteDraftKey) || 'null'); + if (draft?.revision === S.palette.revision) { + for (const [id, paints] of Object.entries(draft.changes)) { + if (palettePaints[id] && ['light','dark'].every(theme => /^#[0-9a-f]{6}$/i.test(paints[theme] || ''))) palettePaints[id] = {...paints}; + } + paletteStatus.textContent = 'Unsaved palette preview restored'; + } else if (draft) { paletteStatus.textContent = 'Saved palette changed · older preview was not restored'; } +} catch {} +updatePalette(); +for (const card of cards) { + const id = card.dataset.id; + const pop = document.createElement('div'); + pop.className = 'pop'; + const textarea = document.createElement('textarea'); + textarea.placeholder = 'What should change in this icon?'; + textarea.setAttribute('aria-label', 'Notes for ' + id); + textarea.value = notes[id] || ''; + pop.appendChild(textarea); card.appendChild(pop); + textarea.oninput = () => { notes[id] = textarea.value; run(); scheduleSave(); }; + card.querySelector('.ck').onclick = () => { + if (card.dataset.missing === 'true') return; + fin.has(id) ? fin.delete(id) : fin.add(id); + card.classList.remove('open'); run(); scheduleSave(); + }; + card.querySelector('.nt').onclick = event => { + const open = !card.classList.contains('open'); + cards.forEach(other => other.classList.remove('open')); + card.classList.toggle('open', open); + if (open) textarea.focus(); + event.stopPropagation(); + }; + pop.onclick = event => event.stopPropagation(); +} +document.addEventListener('click', () => cards.forEach(card => card.classList.remove('open'))); +document.addEventListener('keydown', event => { + if (event.key === 'Escape') cards.forEach(card => card.classList.remove('open')); +}); +document.querySelectorAll('[data-f]').forEach(button => button.onclick = () => { selectFilter(button.dataset.f); run(); }); +document.querySelectorAll('.chip').forEach(button => button.onclick = () => { selectTab(button.dataset.tab || ''); run(); }); +document.querySelectorAll('.sz').forEach(button => button.onclick = () => { setSize(+button.dataset.s); savePreferences(); }); +document.getElementById('dk').onclick = function () { + document.body.classList.toggle('dark'); this.classList.toggle('on'); savePreferences(); +}; +document.getElementById('vision').onchange = event => { + document.body.dataset.vision = event.target.value; savePreferences(); +}; +query.oninput = run; +document.getElementById('rescan').onclick = async () => { + if (!dirty || await flush()) location.reload(); +}; +document.getElementById('exp').onclick = async () => { + if (dirty && !await flush()) return; + if (!S.serving) { + const snapshot = state(); + const accepted = Object.fromEntries(snapshot.final.map(id => [id, S.hashes[id]])); + const link = document.createElement('a'); + link.href = URL.createObjectURL(new Blob([JSON.stringify({version: 1, final: accepted, notes: snapshot.notes}, null, 1)], {type: 'application/json'})); + link.download = 'review.json'; link.click(); URL.revokeObjectURL(link.href); + return; + } + try { + await saves; + const snapshot = state(); + const response = await fetch('/api/export', {method: 'POST', body: payload(snapshot)}); + const result = await response.json(); + if (!response.ok || !result.ok) throw new Error(result.error || 'Export failed'); + base = JSON.parse(JSON.stringify(snapshot)); + showMessage(result.sent ? `Queued ${result.sent} note${result.sent === 1 ? '' : 's'} in the local review inbox` : 'No open notes to export'); + } catch (error) { showMessage(error.message, true); } +}; +try { + if (!S.serving) { + const saved = JSON.parse(localStorage.getItem(staticKey) || 'null'); + if (saved) { fin.clear(); saved.final.forEach(id => fin.add(id)); Object.assign(notes, saved.notes); } + } + const prefs = JSON.parse(localStorage.getItem(prefsKey) || 'null'); + if (prefs) { + if (prefs.vision && ['off', 'red-green', 'blue-yellow'].includes(prefs.vision)) { document.body.dataset.vision = prefs.vision; document.getElementById('vision').value = prefs.vision; } + if (prefs.dark) document.getElementById('dk').click(); + const size = parseInt(prefs.size); + if ([16,24,48,64].includes(size)) setSize(size); + } + // Only an automatic artwork reload restores filters. A normal opening always + // starts at Open / Everything, so yesterday's filter cannot hide new artwork. + const restored = JSON.parse(sessionStorage.getItem(reloadKey) || 'null'); + sessionStorage.removeItem(reloadKey); + if (restored) { + selectFilter(restored.filter); selectTab(restored.tab); query.value = restored.q; + requestAnimationFrame(() => scrollTo(0, restored.y)); + } +} catch {} +run(); +if (S.serving) setInterval(async () => { + if (dirty || paletteDirty || paletteSaving || document.activeElement?.tagName === 'TEXTAREA') return; + try { + const response = await fetch('/api/review'); + const current = await response.json(); + if (current.rev && current.rev !== S.rev) { + sessionStorage.setItem(reloadKey, JSON.stringify(view())); + location.reload(); return; + } + if (JSON.stringify([current.final, current.notes]) !== JSON.stringify([[...fin].sort(), notes])) { + fin.clear(); current.final.forEach(id => fin.add(id)); + Object.keys(notes).forEach(id => delete notes[id]); Object.assign(notes, current.notes); + base = {final: [...current.final].sort(), notes: {...notes}}; + cards.forEach(card => { card.querySelector('textarea').value = notes[card.dataset.id] || ''; }); + run(); + } + } catch { showMessage('Review server unavailable; reconnect before saving.', true); } +}, 3000); diff --git a/assets/icons/ui/design/tools/review_state.py b/assets/icons/ui/design/tools/review_state.py new file mode 100644 index 00000000..2b78ae2a --- /dev/null +++ b/assets/icons/ui/design/tools/review_state.py @@ -0,0 +1,76 @@ +"""Shared icon review state: read and write design/review.json (standard library only). + + {"version": 1, "final": {"": ""}, "notes": {"": "text"}} + +Legacy acceptance lists migrate once when the review server starts; existing hashes are never refreshed implicitly. +""" +import os, re, json, glob, hashlib, tomllib, threading, uuid + +LOCK = threading.RLock() + +D = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # .../assets/icons/ui/design +REVIEW = D + '/review.json' + +def aliases(): + return tomllib.load(open(D + '/aliases.toml', 'rb')).get('alias', {}) +def master_of(icon_id, al=None): + v = (aliases() if al is None else al).get(icon_id) + return (v['master'] if isinstance(v, dict) else v) if v else icon_id +def master_hash(master): + h = hashlib.sha256() + for f in sorted(glob.glob(D + '/svg/*/' + master + '.svg')): + h.update(os.path.relpath(f, D).replace(os.sep, '/').encode()); h.update(open(f, 'rb').read()) + return h.hexdigest() +def load(): + try: r = json.load(open(REVIEW, encoding='utf-8')) + except FileNotFoundError: r = {} + fin = r.get('final', {}) + if isinstance(fin, list): fin = {i: '' for i in fin} + return {'final': dict(fin), 'notes': {k: v for k, v in r.get('notes', {}).items() if isinstance(v, str) and v.strip()}} +def valid_final(state=None): + """Acceptance belongs to a particular artwork revision, not just an id.""" + state = load() if state is None else state + al = aliases() + return {i: h for i, h in state['final'].items() + if h and glob.glob(D + '/svg/*/' + master_of(i, al) + '.svg') + and h == master_hash(master_of(i, al))} +def save(final, notes): + """final: {id: hash or ''}; a missing hash is filled in from the master as it is now.""" + ok = lambda i: re.fullmatch(r'[a-z][a-z0-9_]*', i) + al = aliases() + fin = {i: (h or master_hash(master_of(i, al))) for i, h in final.items() if ok(i)} + nts = {k: v for k, v in notes.items() if ok(k) and isinstance(v, str) and v.strip()} + temporary = REVIEW + '.' + uuid.uuid4().hex + '.tmp' + with LOCK: + try: + with open(temporary, 'w', encoding='utf-8') as f: + json.dump({'version': 1, 'final': dict(sorted(fin.items())), 'notes': dict(sorted(nts.items()))}, f, indent=1); f.write('\n') + os.replace(temporary, REVIEW) + finally: + if os.path.exists(temporary): os.unlink(temporary) +def resolve(masters_or_ids): + """The icons were redrawn: drop their notes, and the notes on aliases of the same masters. Returns the ids cleared.""" + r = load(); al = aliases(); done = set(masters_or_ids) + cleared = sorted(i for i in r['notes'] if i in done or master_of(i, al) in done) + for i in cleared: del r['notes'][i] + if cleared: save(r['final'], r['notes']) + return cleared + + +def resolve_export(request, written): + """Clear only still-matching exported notes on masters actually redrawn. + + Call after applying a snapshot's artwork feedback. Newer notes and final + aliases are retained; this helper does not move requests or draw anything. + """ + with LOCK: + state = load() + protected = {master_of(i) for i in valid_final(state)} + cleared = [] + for icon, expected in request.get('notes', {}).items(): + master = master_of(icon) + if master in written and master not in protected and state['notes'].get(icon) == expected: + del state['notes'][icon] + cleared.append(icon) + if cleared: save(state['final'], state['notes']) + return sorted(cleared) diff --git a/assets/icons/ui/source/icons.rs b/assets/icons/ui/source/icons.rs deleted file mode 100644 index 56583146..00000000 --- a/assets/icons/ui/source/icons.rs +++ /dev/null @@ -1,1621 +0,0 @@ -//! Toolbar and browser icons, drawn in code (no image assets). Each icon is designed on a -//! 24 × 24 grid and scaled to the target rectangle. - -use egui::{Color32, Mesh, Painter, Pos2, Rect, Shape, Stroke, pos2, vec2}; - -/// `n + 1` points along the cubic Bézier curve with control points `c`. -fn bezier(c: [(f32, f32); 4], n: usize) -> Vec<(f32, f32)> { - (0..=n) - .map(|i| i as f32 / n as f32) - .map(|t| { - let u = 1.0 - t; - let w = [u * u * u, 3.0 * u * u * t, 3.0 * u * t * t, t * t * t]; - (w.iter().zip(&c).map(|(w, q)| w * q.0).sum(), w.iter().zip(&c).map(|(w, q)| w * q.1).sum()) - }) - .collect() -} - -/// `n + 1` points along an ellipse from angle `a0` to `a1` (y down: angles below the centre -/// first). -fn ellipse(x: f32, y: f32, rx: f32, ry: f32, a0: f32, a1: f32, n: usize) -> Vec<(f32, f32)> { - (0..=n).map(|i| a0 + (a1 - a0) * i as f32 / n as f32).map(|a| (x + rx * a.cos(), y + ry * a.sin())).collect() -} - -/// A gear's toothed outline about (x, y): `n` teeth from the root radius `root` out to `tip`, -/// turned by `phase` radians. -fn teeth(x: f32, y: f32, root: f32, tip: f32, n: usize, phase: f32) -> Vec<(f32, f32)> { - let step = std::f32::consts::TAU / n as f32; - let at = |a: f32, r: f32| (x + r * a.cos(), y - r * a.sin()); - (0..n) - .flat_map(|k| { - let a = phase + step * k as f32; - [at(a - 0.3 * step, root), at(a - 0.15 * step, tip), at(a + 0.15 * step, tip), at(a + 0.3 * step, root)] - }) - .collect() -} - -struct Pen<'a> { - p: &'a Painter, - r: Rect, - ink: Color32, - fill: Color32, - accent: Color32, - w: f32, -} - -impl Pen<'_> { - fn at(&self, x: f32, y: f32) -> Pos2 { - pos2(self.r.left() + x / 24.0 * self.r.width(), self.r.top() + y / 24.0 * self.r.height()) - } - fn s(&self) -> f32 { - self.r.width() / 24.0 - } - fn line(&self, pts: &[(f32, f32)], c: Color32) { - let v: Vec = pts.iter().map(|(x, y)| self.at(*x, *y)).collect(); - self.p.add(Shape::line(v, Stroke::new(self.w, c))); - } - fn poly(&self, pts: &[(f32, f32)], fill: Color32, c: Color32) { - let v: Vec = pts.iter().map(|(x, y)| self.at(*x, *y)).collect(); - self.p.add(Shape::convex_polygon(v, fill, Stroke::new(self.w, c))); - } - fn closed(&self, pts: &[(f32, f32)], c: Color32) { - let v: Vec = pts.iter().map(|(x, y)| self.at(*x, *y)).collect(); - self.p.add(Shape::closed_line(v, Stroke::new(self.w, c))); - } - fn circle(&self, x: f32, y: f32, r: f32, fill: Color32, c: Color32) { - self.p.circle(self.at(x, y), r * self.s(), fill, Stroke::new(self.w, c)); - } - fn dot(&self, x: f32, y: f32, c: Color32) { - self.p.circle_filled(self.at(x, y), 1.6 * self.s(), c); - } - fn arc(&self, x: f32, y: f32, r: f32, a0: f32, a1: f32, c: Color32) { - let n = 24; - let pts: Vec<(f32, f32)> = (0..=n).map(|i| a0 + (a1 - a0) * i as f32 / n as f32).map(|a| (x + r * a.cos(), y - r * a.sin())).collect(); - self.line(&pts, c); - } - fn arrow(&self, from: (f32, f32), to: (f32, f32), c: Color32) { - self.line(&[from, to], c); - let d = vec2(to.0 - from.0, to.1 - from.1).normalized(); - let n = vec2(-d.y, d.x); - let tip = vec2(to.0, to.1); - let a = tip - d * 3.5 + n * 2.2; - let b = tip - d * 3.5 - n * 2.2; - self.poly(&[to, (a.x, a.y), (b.x, b.y)], c, c); - } - /// Isometric box from a 2D footprint height. - fn iso_box(&self, x0: f32, y0: f32, w: f32, d: f32, h: f32) { - let top = [(x0, y0), (x0 + w, y0 - d * 0.5), (x0 + w + d, y0), (x0 + d, y0 + d * 0.5)]; - let front = [(x0, y0), (x0 + d, y0 + d * 0.5), (x0 + d, y0 + d * 0.5 + h), (x0, y0 + h)]; - let side = [(x0 + d, y0 + d * 0.5), (x0 + w + d, y0), (x0 + w + d, y0 + h), (x0 + d, y0 + d * 0.5 + h)]; - self.poly(&front, self.fill.gamma_multiply(0.85), self.ink); - self.poly(&side, self.fill.gamma_multiply(0.7), self.ink); - self.poly(&top, self.fill, self.ink); - } - /// A cubic Bézier curve through control points `c`. - fn curve(&self, c: [(f32, f32); 4], col: Color32) { - self.line(&bezier(c, 16), col); - } - fn dashed(&self, pts: &[(f32, f32)], c: Color32) { - let v: Vec = pts.iter().map(|(x, y)| self.at(*x, *y)).collect(); - self.p.add(Shape::dashed_line(&v, Stroke::new(self.w, c), 2.0 * self.s(), 1.6 * self.s())); - } - /// Fill the strip between two polylines that run the same way, such as the front and back - /// edges of a curved sheet. Unlike `poly`, the strip need not be convex. - fn band(&self, a: &[(f32, f32)], b: &[(f32, f32)], fill: Color32) { - let mut m = Mesh::default(); - for (p, q) in a.iter().zip(b) { - m.colored_vertex(self.at(p.0, p.1), fill); - m.colored_vertex(self.at(q.0, q.1), fill); - } - let n = a.len().min(b.len()) as u32; - for i in 1..n { - let k = 2 * i; - m.add_triangle(k - 2, k - 1, k); - m.add_triangle(k - 1, k + 1, k); - } - self.p.add(Shape::mesh(m)); - } -} - -/// The stroke width for the Normal/Construction/Centerline line-type icons. It is heavier than -/// the generic icon weight (`Pen::w`). It is sized from whichever side of `r` is smaller. A -/// short, wide button and a square toolbar slot both get a stroke their gaps can survive next -/// to. -fn line_type_stroke(r: Rect) -> f32 { - (r.width().min(r.height()) / 24.0 * 2.0).max(1.6) -} - -/// The Centerline icon's dot radius. It is sized from `r`. The stroke width plays no part in it. -/// It stays a round mark even next to a heavy stroke. -fn line_type_dot_radius(r: Rect) -> f32 { - (r.width().min(r.height()) * 0.16).max(2.4) -} - -/// Paint the icon `name` into `r`. `accent` highlights the "action" part. -pub fn paint(p: &Painter, r: Rect, name: &str, ink: Color32, fill: Color32, accent: Color32) { - paint_with(p, r, name, ink, fill, accent, &crate::theme::Tokens::get().icons); -} - -/// [`paint`] with the colours an icon draws of its own given (`theme::IconColors`). -pub(crate) fn paint_with(p: &Painter, r: Rect, name: &str, ink: Color32, fill: Color32, accent: Color32, ic: &crate::theme::IconColors) { - let ic = *ic; - let pen = Pen { p, r, ink, fill, accent, w: (r.width() / 24.0 * 1.4).max(1.0) }; - let a = pen.accent; - match name { - "sketch" => { - pen.poly(&[(3.0, 14.0), (13.0, 9.0), (21.0, 13.0), (11.0, 18.0)], pen.fill, ink); - pen.line(&[(7.0, 13.0), (16.0, 4.0), (18.0, 6.0), (9.0, 15.0), (6.5, 15.5), (7.0, 13.0)], a); - } - "finish" => { - pen.circle(12.0, 12.0, 9.0, Color32::TRANSPARENT, pen.accent); - pen.line(&[(7.0, 12.5), (10.5, 16.0), (17.0, 8.5)], a); - } - "extrude" => { - pen.iso_box(4.0, 11.0, 9.0, 7.0, 7.0); - pen.arrow((12.0, 9.0), (12.0, 2.5), a); - } - "revolve" => { - pen.line(&[(12.0, 2.0), (12.0, 22.0)], ink); - pen.poly(&[(13.0, 7.0), (19.0, 7.0), (19.0, 17.0), (13.0, 17.0)], pen.fill, ink); - pen.arc(12.0, 12.0, 9.0, 3.6, 5.8, a); - pen.arrow((15.0, 20.4), (18.5, 19.0), a); - } - "box" => pen.iso_box(3.0, 9.0, 10.0, 8.0, 9.0), - "cylinder" => { - pen.poly(&[(5.0, 6.0), (19.0, 6.0), (19.0, 18.0), (5.0, 18.0)], pen.fill.gamma_multiply(0.8), Color32::TRANSPARENT); - pen.line(&[(5.0, 6.0), (5.0, 18.0)], ink); - pen.line(&[(19.0, 6.0), (19.0, 18.0)], ink); - let e = |y: f32, fillc: Color32| { - let pts: Vec = - (0..32).map(|i| i as f32 / 32.0 * std::f32::consts::TAU).map(|t| pen.at(12.0 + 7.0 * t.cos(), y + 2.5 * t.sin())).collect(); - p.add(Shape::convex_polygon(pts, fillc, Stroke::new(pen.w, ink))); - }; - pen.arc(12.0, 18.0, 7.0, std::f32::consts::PI, std::f32::consts::TAU, ink); - e(6.0, pen.fill); - } - "sphere" => { - pen.circle(12.0, 12.0, 8.5, pen.fill, ink); - pen.arc(12.0, 12.0, 8.5, 3.3, 6.1, ink.gamma_multiply(0.5)); - p.circle_filled(pen.at(9.0, 8.5), 2.0 * pen.s(), ic.glint); - } - "torus" => { - let pts: Vec = - (0..40).map(|i| i as f32 / 40.0 * std::f32::consts::TAU).map(|t| pen.at(12.0 + 9.0 * t.cos(), 12.0 + 5.5 * t.sin())).collect(); - p.add(Shape::convex_polygon(pts, pen.fill, Stroke::new(pen.w, ink))); - let pts: Vec = - (0..40).map(|i| i as f32 / 40.0 * std::f32::consts::TAU).map(|t| pen.at(12.0 + 3.5 * t.cos(), 11.0 + 1.8 * t.sin())).collect(); - p.add(Shape::convex_polygon(pts, ic.highlight, Stroke::new(pen.w, ink))); - } - "fillet" => { - pen.poly(&[(4.0, 20.0), (4.0, 4.0), (10.0, 4.0), (10.0, 14.0), (20.0, 14.0), (20.0, 20.0)], pen.fill, Color32::TRANSPARENT); - pen.line(&[(4.0, 20.0), (4.0, 4.0)], ink); - pen.line(&[(4.0, 20.0), (20.0, 20.0)], ink); - pen.arc(4.0, 4.0, 16.0, -std::f32::consts::FRAC_PI_2, 0.0, ink); - pen.arc(20.0, 4.0, 10.0, std::f32::consts::PI, std::f32::consts::PI * 1.5, a); - } - "chamfer" => { - pen.poly(&[(4.0, 20.0), (4.0, 4.0), (12.0, 4.0), (20.0, 12.0), (20.0, 20.0)], pen.fill, ink); - pen.line(&[(12.0, 4.0), (20.0, 12.0)], a); - } - "combine" => { - pen.poly(&[(3.0, 8.0), (14.0, 8.0), (14.0, 19.0), (3.0, 19.0)], pen.fill, ink); - pen.poly(&[(9.0, 4.0), (21.0, 4.0), (21.0, 15.0), (9.0, 15.0)], pen.fill.gamma_multiply(0.75), a); - } - "pattern_rect" => { - for (x, y) in [(4.0, 4.0), (14.0, 4.0), (4.0, 14.0), (14.0, 14.0)] { - pen.poly( - &[(x, y), (x + 6.0, y), (x + 6.0, y + 6.0), (x, y + 6.0)], - if x == 4.0 && y == 4.0 { pen.fill } else { Color32::TRANSPARENT }, - if x == 4.0 && y == 4.0 { ink } else { a }, - ); - } - } - "pattern_circ" => { - for i in 0..6 { - let t = i as f32 / 6.0 * std::f32::consts::TAU; - pen.circle( - 12.0 + 7.5 * t.cos(), - 12.0 + 7.5 * t.sin(), - 2.4, - if i == 0 { pen.fill } else { Color32::TRANSPARENT }, - if i == 0 { ink } else { a }, - ); - } - } - "hole" => { - pen.poly(&[(3.0, 8.0), (21.0, 8.0), (21.0, 20.0), (3.0, 20.0)], pen.fill, ink); - let pts: Vec = - (0..32).map(|i| i as f32 / 32.0 * std::f32::consts::TAU).map(|t| pen.at(12.0 + 5.0 * t.cos(), 8.0 + 2.0 * t.sin())).collect(); - p.add(Shape::convex_polygon(pts, ic.highlight, Stroke::new(pen.w, a))); - pen.line(&[(7.0, 8.0), (7.0, 16.0)], a); - pen.line(&[(17.0, 8.0), (17.0, 16.0)], a); - } - "sweep" => { - pen.arc(20.0, 20.0, 14.0, std::f32::consts::FRAC_PI_2, std::f32::consts::PI, ink); - pen.circle(6.0, 20.0, 3.0, pen.fill, a); - pen.circle(20.0, 6.0, 3.0, pen.fill, a); - } - "loft" => { - pen.poly(&[(3.0, 20.0), (15.0, 20.0), (21.0, 15.0), (9.0, 15.0)], pen.fill, ink); - pen.circle(12.0, 6.0, 3.5, Color32::TRANSPARENT, a); - pen.line(&[(3.0, 20.0), (8.5, 6.0)], a); - pen.line(&[(21.0, 15.0), (15.5, 6.0)], a); - } - "shell" => { - pen.iso_box(3.0, 9.0, 10.0, 8.0, 9.0); - pen.poly(&[(6.0, 9.0), (13.0, 5.5), (18.0, 8.0), (11.0, 11.5)], ic.highlight, a); - } - "draft" => { - pen.poly(&[(4.0, 20.0), (20.0, 20.0), (17.0, 5.0), (7.0, 5.0)], pen.fill, ink); - pen.line(&[(20.0, 20.0), (20.0, 5.0)], a); - } - "split" => { - pen.poly(&[(3.0, 6.0), (21.0, 6.0), (21.0, 18.0), (3.0, 18.0)], pen.fill, ink); - pen.line(&[(8.0, 2.0), (16.0, 22.0)], a); - } - "mirror" => { - pen.line(&[(12.0, 2.0), (12.0, 22.0)], ink); - pen.poly(&[(3.0, 7.0), (9.0, 10.0), (9.0, 17.0), (3.0, 17.0)], pen.fill, ink); - pen.poly(&[(21.0, 7.0), (15.0, 10.0), (15.0, 17.0), (21.0, 17.0)], Color32::TRANSPARENT, a); - } - "lookat" => { - pen.poly(&[(4.0, 6.0), (16.0, 6.0), (16.0, 18.0), (4.0, 18.0)], pen.fill, ink); - pen.arrow((22.0, 12.0), (13.0, 12.0), a); - } - "style" => { - pen.poly(&[(12.0, 3.0), (20.0, 7.5), (12.0, 12.0), (4.0, 7.5)], pen.fill, ink); - pen.poly(&[(4.0, 7.5), (12.0, 12.0), (12.0, 21.0), (4.0, 16.5)], a, ink); - pen.poly(&[(12.0, 12.0), (20.0, 7.5), (20.0, 16.5), (12.0, 21.0)], Color32::TRANSPARENT, ink); - } - // Original bug glyph: rounded body, antennae and three pairs of legs. - "bug_report" => { - pen.circle(12.0, 13.5, 5.0, Color32::TRANSPARENT, ink); - pen.line(&[(12.0, 9.0), (12.0, 18.0)], ink); - pen.line(&[(9.5, 8.5), (8.0, 5.0)], ink); - pen.line(&[(14.5, 8.5), (16.0, 5.0)], ink); - for y in [10.0, 13.5, 17.0] { - pen.line(&[(3.0, y), (7.0, y)], ink); - pen.line(&[(17.0, y), (21.0, y)], ink); - } - } - // Update: an arrow down onto a tray (a newer release can be downloaded). - "update" => { - pen.line(&[(12.0, 3.5), (12.0, 14.5)], ink); - pen.line(&[(7.5, 10.0), (12.0, 14.5), (16.5, 10.0)], ink); - pen.line(&[(4.0, 15.0), (4.0, 20.0), (20.0, 20.0), (20.0, 15.0)], ink); - } - // Agent chat: a speech bubble with three dots. - "agent_chat" => { - pen.closed(&[(4.0, 5.0), (20.0, 5.0), (20.0, 16.0), (11.0, 16.0), (6.5, 20.0), (7.5, 16.0), (4.0, 16.0)], ink); - for x in [8.5, 12.0, 15.5] { - pen.dot(x, 10.5, ink); - } - } - // Help: a question mark in a ring. - "help" => { - pen.circle(12.0, 12.0, 9.0, Color32::TRANSPARENT, ink); - pen.arc(12.0, 9.5, 3.2, -0.6, 3.4, ink); - pen.line(&[(13.9, 12.0), (12.0, 13.6), (12.0, 15.0)], ink); - pen.dot(12.0, 17.8, ink); - } - "sun" => { - pen.circle(12.0, 12.0, 4.5, a, ink); - for i in 0..8 { - let t = i as f32 / 8.0 * std::f32::consts::TAU; - pen.line(&[(12.0 + 7.0 * t.cos(), 12.0 + 7.0 * t.sin()), (12.0 + 9.5 * t.cos(), 12.0 + 9.5 * t.sin())], ink); - } - } - "moon" => { - let outer: Vec = - (0..=24).map(|i| -0.6 + i as f32 / 24.0 * 4.4).map(|t| pen.at(12.0 + 8.0 * t.cos(), 12.0 + 8.0 * t.sin())).collect(); - let inner: Vec = - (0..=24).rev().map(|i| -0.9 + i as f32 / 24.0 * 3.9).map(|t| pen.at(15.5 + 6.5 * t.cos(), 9.5 + 6.5 * t.sin())).collect(); - let mut pts = outer; - pts.extend(inner); - p.add(Shape::closed_line(pts, Stroke::new(pen.w, ink))); - } - "section" => { - pen.poly(&[(4.0, 8.0), (14.0, 4.0), (20.0, 8.0), (10.0, 12.0)], pen.fill, ink); - pen.poly(&[(4.0, 8.0), (10.0, 12.0), (10.0, 20.0), (4.0, 16.0)], pen.fill, ink); - pen.poly(&[(10.0, 12.0), (20.0, 8.0), (20.0, 16.0), (10.0, 20.0)], a, ink); - } - "presspull" => { - pen.poly(&[(3.0, 15.0), (12.0, 11.0), (21.0, 15.0), (12.0, 19.0)], pen.fill, ink); - pen.arrow((12.0, 15.0), (12.0, 3.0), a); - } - "move" => { - for (dx, dy) in [(0.0, -1.0), (0.0, 1.0), (-1.0, 0.0), (1.0, 0.0)] { - pen.arrow((12.0, 12.0), (12.0 + 9.0 * dx, 12.0 + 9.0 * dy), a); - } - } - "configuration_table" => { - pen.poly(&[(3.0, 4.0), (21.0, 4.0), (21.0, 20.0), (3.0, 20.0)], pen.fill, ink); - pen.line(&[(3.0, 9.0), (21.0, 9.0)], a); - pen.line(&[(3.0, 14.0), (21.0, 14.0)], ink); - pen.line(&[(9.0, 4.0), (9.0, 20.0)], ink); - } - "params" => { - pen.line(&[(8.0, 20.0), (9.0, 8.0), (10.0, 5.0), (13.0, 4.0)], ink); - pen.line(&[(6.0, 11.0), (12.0, 11.0)], ink); - pen.line(&[(14.0, 13.0), (20.0, 20.0)], a); - pen.line(&[(20.0, 13.0), (14.0, 20.0)], a); - } - "compute" => { - pen.arc(12.0, 12.0, 7.5, 0.4, 3.0, ink); - pen.arc(12.0, 12.0, 7.5, 3.5, 6.0, a); - pen.arrow((5.0, 9.0), (4.6, 13.0), ink); - pen.arrow((19.0, 15.0), (19.4, 11.0), a); - } - "delete" => { - pen.line(&[(6.0, 6.0), (18.0, 18.0)], pen.accent); - pen.line(&[(18.0, 6.0), (6.0, 18.0)], pen.accent); - } - "measure" => { - pen.poly(&[(2.0, 15.0), (15.0, 2.0), (22.0, 9.0), (9.0, 22.0)], pen.fill, ink); - for i in 0..5 { - let t = 4.0 + i as f32 * 2.6; - pen.line(&[(t, 15.0 - (t - 2.0)), (t + 2.0, 15.0 - (t - 2.0) + 2.0)], ink); - } - } - "line" => { - pen.line(&[(4.0, 19.0), (20.0, 5.0)], a); - pen.dot(4.0, 19.0, ink); - pen.dot(20.0, 5.0, ink); - } - "rect" | "rect3" | "rect_center" => { - pen.closed(&[(4.0, 6.0), (20.0, 6.0), (20.0, 18.0), (4.0, 18.0)], a); - match name { - "rect" => { - pen.dot(4.0, 18.0, ink); - pen.dot(20.0, 6.0, ink); - } - "rect3" => { - pen.dot(4.0, 18.0, ink); - pen.dot(20.0, 18.0, ink); - pen.dot(20.0, 6.0, ink); - } - _ => { - pen.dot(12.0, 12.0, ink); - pen.dot(20.0, 6.0, ink); - } - } - } - "rect_rounded" => { - // Straight sides between quarter arcs of radius 3 at the corners. - pen.line(&[(7.0, 6.0), (17.0, 6.0)], a); - pen.line(&[(20.0, 9.0), (20.0, 15.0)], a); - pen.line(&[(17.0, 18.0), (7.0, 18.0)], a); - pen.line(&[(4.0, 15.0), (4.0, 9.0)], a); - let q = std::f32::consts::FRAC_PI_2; - pen.arc(17.0, 9.0, 3.0, 0.0, q, a); - pen.arc(7.0, 9.0, 3.0, q, 2.0 * q, a); - pen.arc(7.0, 15.0, 3.0, 2.0 * q, 3.0 * q, a); - pen.arc(17.0, 15.0, 3.0, 3.0 * q, 4.0 * q, a); - pen.dot(4.0, 18.0, ink); - pen.dot(20.0, 6.0, ink); - } - "circle" | "circle2" | "circle3" => { - pen.circle(12.0, 12.0, 8.0, Color32::TRANSPARENT, a); - match name { - "circle" => { - pen.dot(12.0, 12.0, ink); - pen.line(&[(12.0, 12.0), (17.6, 6.4)], ink); - } - "circle2" => { - pen.dot(4.0, 12.0, ink); - pen.dot(20.0, 12.0, ink); - } - _ => { - pen.dot(4.0, 12.0, ink); - pen.dot(12.0, 4.0, ink); - pen.dot(20.0, 12.0, ink); - } - } - } - "arc3" | "arc_center" => { - pen.arc(12.0, 16.0, 9.0, 0.0, std::f32::consts::PI, a); - pen.dot(3.0, 16.0, ink); - pen.dot(21.0, 16.0, ink); - if name == "arc3" { - pen.dot(12.0, 7.0, ink); - } else { - pen.dot(12.0, 16.0, ink); - } - } - "polygon" => { - let pts: Vec<(f32, f32)> = - (0..6).map(|i| i as f32 / 6.0 * std::f32::consts::TAU + 0.52).map(|t| (12.0 + 9.0 * t.cos(), 12.0 + 9.0 * t.sin())).collect(); - pen.closed(&pts, a); - pen.dot(12.0, 12.0, ink); - } - "slot" => { - pen.arc(8.0, 12.0, 5.0, std::f32::consts::FRAC_PI_2, std::f32::consts::PI * 1.5, a); - pen.arc(16.0, 12.0, 5.0, -std::f32::consts::FRAC_PI_2, std::f32::consts::FRAC_PI_2, a); - pen.line(&[(8.0, 7.0), (16.0, 7.0)], a); - pen.line(&[(8.0, 17.0), (16.0, 17.0)], a); - pen.dot(8.0, 12.0, ink); - pen.dot(16.0, 12.0, ink); - } - "point" => { - pen.dot(12.0, 12.0, a); - pen.circle(12.0, 12.0, 4.0, Color32::TRANSPARENT, ink); - } - "dimension" => { - pen.line(&[(4.0, 18.0), (4.0, 8.0)], ink); - pen.line(&[(20.0, 18.0), (20.0, 8.0)], ink); - pen.arrow((12.0, 11.0), (4.5, 11.0), a); - pen.arrow((12.0, 11.0), (19.5, 11.0), a); - pen.line(&[(9.0, 15.0), (15.0, 15.0)], ink); - } - // The Sketch Palette's line type selector (normal, construction, centerline) paints all - // three on the same diagonal, from (3, 19) to (19, 3). Only the dash pattern changes - // between them. Normal draws in `ink` and ignores `accent`. It is a plain line. It is - // never construction geometry. Its colour must stay fixed even if a caller passes the - // construction orange for it. Construction and centerline draw only in `accent` and - // ignore `ink`. The caller decides the colour. The palette and the toolbar both pass - // the theme's construction orange. - // - // These three are drawn small. The Sketch Palette draws them down to about 18 px - // square. At that size a repeating dash pattern loses its gaps. It reads as a solid - // line. Two patterns that only differ in dash phase read as the same texture too. - // Normal, Construction and Centerline instead get three different silhouettes. One is - // an unbroken line. One is a line broken by a single gap. One is a line broken by a - // single gap with a bold dot sitting in it. A viewer can name the shape on sight. No - // rhythm needs measuring. The stroke width and the dot radius both come from the rect - // actually passed in. That keeps the shapes legible in the palette, the toolbar and the - // command palette, at every size any of them uses. - "normal" => { - let w = line_type_stroke(r); - p.add(Shape::line(vec![pen.at(3.0, 19.0), pen.at(19.0, 3.0)], Stroke::new(w, ink))); - } - "construction" => { - // One gap, no dot. It is an unmistakably broken line. Nothing sits in the break. - let w = line_type_stroke(r); - let at = |t: f32| pen.at(3.0 + 16.0 * t, 19.0 - 16.0 * t); - for (t0, t1) in [(0.0, 0.35), (0.65, 1.0)] { - p.add(Shape::line(vec![at(t0), at(t1)], Stroke::new(w, a))); - } - } - "centerline" => { - // The same gap as Construction, with a bold dot centred in it. The silhouette is - // line, dot, line. The dot's radius comes from the rect. It reads as a distinct - // round mark on its own, never as a thicker stretch of the stroke. The dashes stop - // well short of it on both sides. The dot never touches either stroke. It always - // reads as a separate shape. - let w = line_type_stroke(r); - let dot_r = line_type_dot_radius(r); - let at = |t: f32| pen.at(3.0 + 16.0 * t, 19.0 - 16.0 * t); - for (t0, t1) in [(0.0, 0.26), (0.74, 1.0)] { - p.add(Shape::line(vec![at(t0), at(t1)], Stroke::new(w, a))); - } - p.circle_filled(at(0.5), dot_r, a); - } - "infinite" => { - // An infinite line (#1359): Construction's broken line, with an open arrowhead at - // each end saying it runs on. Accent only, like Construction and Centerline. - let w = line_type_stroke(r); - let at = |t: f32| pen.at(3.0 + 16.0 * t, 19.0 - 16.0 * t); - for (t0, t1) in [(0.0, 0.38), (0.62, 1.0)] { - p.add(Shape::line(vec![at(t0), at(t1)], Stroke::new(w, a))); - } - // Each head: two short strokes back from the tip, either side of the line. - let tip_len = 0.3; - for (tip, back) in [(0.0f32, tip_len), (1.0, 1.0 - tip_len)] { - let (t, b) = (at(tip), at(back)); - let along = b - t; - let side = egui::vec2(-along.y, along.x) * 0.55; - p.add(Shape::line(vec![b + side, t, b - side], Stroke::new(w, a))); - } - } - "c_hv" => { - pen.line(&[(4.0, 18.0), (20.0, 18.0)], a); - pen.line(&[(6.0, 4.0), (6.0, 14.0)], a); - } - "c_coincident" => { - pen.line(&[(3.0, 18.0), (12.0, 12.0)], ink); - pen.line(&[(12.0, 12.0), (21.0, 16.0)], ink); - pen.circle(12.0, 12.0, 2.6, ic.highlight, a); - } - "c_tangent" => { - pen.circle(12.0, 14.0, 6.0, Color32::TRANSPARENT, ink); - pen.line(&[(3.0, 8.0), (21.0, 8.0)], a); - } - "c_equal" => { - pen.line(&[(5.0, 9.0), (19.0, 9.0)], a); - pen.line(&[(5.0, 15.0), (19.0, 15.0)], a); - } - "c_parallel" => { - pen.line(&[(5.0, 19.0), (13.0, 5.0)], a); - pen.line(&[(11.0, 19.0), (19.0, 5.0)], a); - } - "c_perpendicular" => { - pen.line(&[(4.0, 19.0), (20.0, 19.0)], a); - pen.line(&[(12.0, 19.0), (12.0, 5.0)], a); - pen.line(&[(12.0, 15.0), (16.0, 15.0), (16.0, 19.0)], ink); - } - "c_fix" => { - pen.circle(12.0, 9.0, 4.0, Color32::TRANSPARENT, a); - pen.line(&[(12.0, 13.0), (12.0, 21.0)], a); - pen.line(&[(9.0, 21.0), (15.0, 21.0)], ink); - } - "c_midpoint" => { - pen.line(&[(3.0, 15.0), (21.0, 9.0)], ink); - pen.poly(&[(12.0, 9.0), (15.0, 12.0), (12.0, 15.0), (9.0, 12.0)], a, a); - } - "c_concentric" => { - pen.circle(12.0, 12.0, 8.5, Color32::TRANSPARENT, a); - pen.circle(12.0, 12.0, 4.0, Color32::TRANSPARENT, a); - } - "c_collinear" => { - pen.line(&[(3.0, 19.0), (10.0, 12.0)], a); - pen.line(&[(14.0, 8.0), (21.0, 1.0)], a); - } - "c_symmetry" => { - pen.line(&[(12.0, 3.0), (12.0, 21.0)], ink); - pen.poly(&[(4.0, 8.0), (9.0, 12.0), (4.0, 16.0)], a, a); - pen.poly(&[(20.0, 8.0), (15.0, 12.0), (20.0, 16.0)], a, a); - } - "undo" | "redo" => { - let (x0, x1) = if name == "undo" { (19.0, 5.0) } else { (5.0, 19.0) }; - pen.line(&[(x0, 18.0), (x0, 12.0), (12.0, 9.0), (x1, 9.0)], ink); - pen.arrow((x1 + (x0 - x1) * 0.15, 9.0), (x1, 9.0), ink); - } - "new" => { - pen.poly(&[(6.0, 3.0), (15.0, 3.0), (19.0, 7.0), (19.0, 21.0), (6.0, 21.0)], ic.highlight, ink); - pen.line(&[(15.0, 3.0), (15.0, 7.0), (19.0, 7.0)], ink); - } - "open" => { - pen.poly(&[(3.0, 7.0), (9.0, 7.0), (11.0, 9.0), (20.0, 9.0), (20.0, 19.0), (3.0, 19.0)], pen.fill, ink); - } - "save" => { - pen.poly(&[(4.0, 4.0), (17.0, 4.0), (20.0, 7.0), (20.0, 20.0), (4.0, 20.0)], pen.fill, ink); - pen.poly(&[(8.0, 4.0), (16.0, 4.0), (16.0, 9.0), (8.0, 9.0)], ic.highlight, ink); - } - "export" => { - pen.poly(&[(4.0, 10.0), (4.0, 20.0), (16.0, 20.0), (16.0, 10.0)], pen.fill, ink); - pen.arrow((10.0, 14.0), (20.0, 4.0), a); - } - "import" => { - pen.iso_box(4.0, 12.0, 12.0, 7.0, 7.0); - pen.arrow((20.0, 3.0), (12.0, 11.0), a); - } - "home" => { - pen.poly(&[(4.0, 12.0), (12.0, 4.0), (20.0, 12.0)], pen.fill, ink); - pen.poly(&[(6.0, 12.0), (18.0, 12.0), (18.0, 20.0), (6.0, 20.0)], pen.fill, ink); - } - "fit" => { - for (cx, cy, dx, dy) in [(4.0, 4.0, 1.0, 1.0), (20.0, 4.0, -1.0, 1.0), (4.0, 20.0, 1.0, -1.0), (20.0, 20.0, -1.0, -1.0)] { - pen.line(&[(cx + dx * 5.0, cy), (cx, cy), (cx, cy + dy * 5.0)], ink); - } - pen.poly(&[(9.0, 9.0), (15.0, 9.0), (15.0, 15.0), (9.0, 15.0)], pen.fill, ink); - } - "orbit" => { - pen.arc(12.0, 12.0, 8.0, 0.3, 5.5, ink); - pen.arrow((17.0, 17.5), (19.6, 14.5), a); - pen.dot(12.0, 12.0, ink); - } - "spin" => { - // A turntable: an ellipse with an arrow running round it, a post in the middle. - let pts: Vec = (0..=28).map(|i| 0.2 + i as f32 / 28.0 * 5.6).map(|t| pen.at(12.0 + 9.0 * t.cos(), 16.0 + 3.5 * t.sin())).collect(); - p.add(Shape::line(pts, Stroke::new(pen.w, ink))); - pen.arrow((17.0, 19.0), (20.5, 17.0), a); - pen.line(&[(12.0, 15.5), (12.0, 4.0)], ink); - pen.poly(&[(9.0, 9.0), (15.0, 9.0), (15.0, 4.0), (9.0, 4.0)], Color32::TRANSPARENT, ink); - } - "pan" => { - for (dx, dy) in [(0.0, -1.0), (0.0, 1.0), (-1.0, 0.0), (1.0, 0.0)] { - pen.arrow((12.0 + 3.0 * dx, 12.0 + 3.0 * dy), (12.0 + 9.0 * dx, 12.0 + 9.0 * dy), ink); - } - } - "zoom" => { - pen.circle(10.0, 10.0, 6.0, ic.highlight, ink); - pen.line(&[(14.5, 14.5), (20.0, 20.0)], ink); - } - "perspective" => { - pen.closed(&[(4.0, 6.0), (20.0, 4.0), (20.0, 20.0), (4.0, 18.0)], ink); - pen.closed(&[(9.0, 9.0), (15.0, 8.5), (15.0, 15.5), (9.0, 15.0)], ink); - } - "eye" => { - pen.arc(12.0, 18.0, 10.0, 0.6, 2.54, ink); - pen.arc(12.0, 6.0, 10.0, 3.74, 5.68, ink); - pen.circle(12.0, 12.0, 2.6, ink, ink); - } - "body" => pen.iso_box(4.0, 10.0, 8.0, 6.0, 6.0), - // Component: a box with a smaller one stacked on it. - "component" => { - pen.iso_box(2.0, 13.0, 9.0, 6.0, 6.0); - pen.iso_box(9.0, 6.0, 5.0, 4.0, 4.0); - } - "folder" => pen.poly(&[(3.0, 7.0), (9.0, 7.0), (11.0, 9.0), (21.0, 9.0), (21.0, 19.0), (3.0, 19.0)], ic.folder, ink), - "origin" => { - pen.arrow((6.0, 18.0), (20.0, 18.0), ic.axes[0]); - pen.arrow((6.0, 18.0), (6.0, 4.0), ic.axes[2]); - pen.arrow((6.0, 18.0), (15.0, 9.0), ic.axes[1]); - } - "plane" => pen.poly(&[(3.0, 15.0), (12.0, 8.0), (21.0, 11.0), (12.0, 18.0)], ic.plane, ink), - "axis" => { - pen.line(&[(4.0, 20.0), (20.0, 4.0)], ink); - pen.circle(4.0, 20.0, 2.0, ink, ink); - } - "settings" => { - // A gear: a solid ring with six chunky, square-ish teeth and a see-through centre - // hole (#290; thin rays read as a sun). - let (teeth, root, tip, hole) = (6usize, 7.0f32, 10.5f32, 3.2f32); - let pitch = std::f32::consts::TAU / teeth as f32; - // Half-widths (radians) of a tooth at its root and at its tip: nearly parallel sides. - let (half_root, half_tip) = (pitch * 0.3, pitch * 0.22); - let polar = |a: f32, r: f32| (12.0 + r * a.cos(), 12.0 + r * a.sin()); - let start = -std::f32::consts::FRAC_PI_2; - let mut outline = Vec::new(); - for i in 0..teeth { - let c = start + i as f32 * pitch; - // The tooth, filled; it reaches into the ring so no seam shows. - let quad = [polar(c - half_root, root - 1.0), polar(c - half_tip, tip), polar(c + half_tip, tip), polar(c + half_root, root - 1.0)]; - let v: Vec = quad.iter().map(|(x, y)| pen.at(*x, *y)).collect(); - p.add(Shape::convex_polygon(v, pen.fill, Stroke::NONE)); - outline.extend([polar(c - half_root, root), polar(c - half_tip, tip), polar(c + half_tip, tip), polar(c + half_root, root)]); - // The root circle up to the next tooth. - for k in 1..4 { - let a = c + half_root + (pitch - 2.0 * half_root) * k as f32 / 4.0; - outline.push(polar(a, root)); - } - } - // The ring: a thick stroke, so the hole stays transparent on any background. - p.circle_stroke(pen.at(12.0, 12.0), (root + hole) * 0.5 * pen.s(), Stroke::new((root - hole) * pen.s(), pen.fill)); - pen.closed(&outline, ink); - pen.circle(12.0, 12.0, hole, Color32::TRANSPARENT, ink); - } - // Sketch tools (sketch-style: thin outlines, accent for what the tool makes). - "canvas" | "decal" => { - if name == "canvas" { - pen.poly(&[(3.0, 5.0), (21.0, 5.0), (21.0, 19.0), (3.0, 19.0)], pen.fill, ink); - } else { - pen.poly(&[(3.0, 5.0), (21.0, 5.0), (21.0, 13.0), (15.0, 19.0), (3.0, 19.0)], pen.fill, ink); - pen.line(&[(21.0, 13.0), (15.0, 13.0), (15.0, 19.0)], ink); - } - pen.line(&[(5.0, 17.0), (9.5, 10.5), (12.5, 14.5), (14.5, 12.0), (18.0, 16.5)], a); - pen.circle(16.5, 8.5, 1.6, a, a); - } - "dxf" | "svg" => { - pen.poly(&[(5.0, 2.5), (15.0, 2.5), (19.5, 7.0), (19.5, 21.5), (5.0, 21.5)], pen.fill, ink); - pen.line(&[(15.0, 2.5), (15.0, 7.0), (19.5, 7.0)], ink); - let label = if name == "dxf" { "DXF" } else { "SVG" }; - p.text(pen.at(12.2, 15.0), egui::Align2::CENTER_CENTER, label, egui::FontId::proportional(6.4 * pen.s()), a); - } - "c_smooth" => { - let pts: Vec<(f32, f32)> = - (0..=24).map(|i| i as f32 / 24.0).map(|t| (3.0 + 18.0 * t, 12.0 - 7.0 * (t * std::f32::consts::TAU).sin())).collect(); - let (l, r) = pts.split_at(13); - pen.line(l, ink); - let mut r2 = vec![pts[12]]; - r2.extend_from_slice(r); - pen.line(&r2, a); - pen.dot(12.0, 12.0, ink); - } - "c_polygon" => { - let pts: Vec<(f32, f32)> = - (0..6).map(|i| i as f32 / 6.0 * std::f32::consts::TAU).map(|t| (12.0 + 8.5 * t.cos(), 12.0 + 8.5 * t.sin())).collect(); - pen.closed(&pts, a); - pen.line(&[(12.0, 12.0), (20.5, 12.0)], ink); - pen.dot(12.0, 12.0, ink); - } - "auto_constrain" | "constrainer" => { - pen.closed(&[(3.0, 8.0), (15.0, 8.0), (15.0, 20.0), (3.0, 20.0)], ink); - if name == "auto_constrain" { - for (dx, dy) in [(0.0, -1.0), (0.0, 1.0), (-1.0, 0.0), (1.0, 0.0), (0.7, 0.7), (-0.7, -0.7), (0.7, -0.7), (-0.7, 0.7)] { - let k = if dx * dy == 0.0 { 4.0 } else { 2.5 }; - pen.line(&[(18.0, 6.0), (18.0 + k * dx, 6.0 + k * dy)], a); - } - } else { - pen.line(&[(13.0, 6.0), (16.0, 9.0), (21.5, 2.5)], a); - } - pen.line(&[(6.0, 17.0), (12.0, 17.0)], a); - pen.line(&[(6.0, 11.0), (6.0, 14.0)], a); - } - "project" | "project_surface" => { - if name == "project" { - pen.poly(&[(2.0, 18.0), (12.0, 14.0), (22.0, 18.0), (12.0, 22.0)], pen.fill, ink); - pen.closed(&[(6.0, 18.0), (12.0, 15.6), (18.0, 18.0), (12.0, 20.4)], a); - } else { - pen.line(&[(2.0, 19.0), (7.0, 16.0), (12.0, 17.5), (17.0, 15.5), (22.0, 18.0)], ink); - pen.line(&[(6.0, 19.5), (12.0, 20.5), (18.0, 18.5)], a); - } - pen.iso_box(7.0, 5.0, 5.0, 5.0, 4.0); - pen.arrow((12.0, 10.5), (12.0, 15.0), a); - } - "intersect" | "intersection_curve" => { - pen.iso_box(6.0, 6.0, 7.0, 5.0, 11.0); - if name == "intersect" { - pen.line(&[(2.0, 13.0), (14.0, 8.0), (22.0, 12.0)], ink.gamma_multiply(0.6)); - pen.line(&[(6.0, 13.0), (11.0, 15.5), (18.0, 12.0)], a); - } else { - pen.arc(12.0, 26.0, 13.0, 0.6, 2.55, ink.gamma_multiply(0.6)); - pen.line(&[(6.0, 14.5), (11.0, 16.5), (18.0, 13.5)], a); - } - } - "include" => { - pen.iso_box(4.0, 9.0, 10.0, 7.0, 9.0); - pen.line(&[(4.0, 9.0), (11.0, 12.5), (21.0, 7.5)], a); - pen.dot(11.0, 12.5, a); - } - "spun" => { - pen.line(&[(12.0, 2.0), (12.0, 22.0)], ink); - pen.line(&[(14.0, 5.0), (18.0, 9.0), (18.0, 15.0), (14.0, 19.0)], ink); - pen.line(&[(10.0, 5.0), (6.0, 9.0), (6.0, 15.0), (10.0, 19.0)], a); - pen.arc(12.0, 12.0, 6.0, 0.3, 2.8, ink.gamma_multiply(0.6)); - } - "iso_curve" => { - pen.poly(&[(3.0, 16.0), (10.0, 6.0), (21.0, 8.0), (14.0, 19.0)], pen.fill, ink); - pen.line(&[(6.5, 11.0), (17.5, 13.5)], ink.gamma_multiply(0.5)); - pen.line(&[(7.7, 17.3), (14.8, 6.8)], a); - } - "fit_section" => { - pen.closed(&[(3.0, 18.0), (8.0, 6.0), (14.0, 15.0), (19.0, 5.0), (21.0, 18.0)], ink.gamma_multiply(0.6)); - pen.line(&[(8.0, 6.0), (3.0, 18.0)], ink.gamma_multiply(0.6)); - pen.line(&[(14.0, 15.0), (3.0, 18.0)], ink.gamma_multiply(0.6)); - pen.line(&[(14.0, 15.0), (21.0, 18.0)], ink.gamma_multiply(0.6)); - pen.line(&[(2.0, 12.0), (7.0, 10.5), (12.0, 12.5), (17.0, 10.0), (22.0, 11.5)], a); - } - "sketch_mirror" => { - p.add(Shape::dashed_line(&[pen.at(12.0, 2.0), pen.at(12.0, 22.0)], Stroke::new(pen.w, ink), 2.5 * pen.s(), 1.8 * pen.s())); - pen.closed(&[(3.0, 6.0), (9.0, 10.0), (9.0, 18.0), (3.0, 18.0)], ink); - pen.closed(&[(21.0, 6.0), (15.0, 10.0), (15.0, 18.0), (21.0, 18.0)], a); - } - "sketch_circ_pattern" => paint(p, r, "pattern_circ", ink, Color32::TRANSPARENT, a), - "pattern_flip" => { - // Opposing arrows show reversal, with a short reference tick between them. - pen.arrow((4.0, 7.0), (20.0, 7.0), a); - pen.arrow((20.0, 17.0), (4.0, 17.0), ink); - pen.line(&[(12.0, 10.0), (12.0, 14.0)], ink); - } - "pattern_center" => { - // An original on a reference axis, with one equally spaced copy on either side. - pen.line(&[(12.0, 3.0), (12.0, 21.0)], a); - pen.line(&[(4.0, 12.0), (20.0, 12.0)], ink); - for x in [4.0, 12.0, 20.0] { - pen.circle(x, 12.0, 2.4, fill, if x == 12.0 { a } else { ink }); - } - } - "sketch_rect_pattern" => paint(p, r, "pattern_rect", ink, Color32::TRANSPARENT, a), - "line_mid" => { - pen.line(&[(3.0, 19.0), (21.0, 5.0)], a); - pen.dot(12.0, 12.0, ink); - pen.dot(21.0, 5.0, ink); - pen.circle(12.0, 12.0, 3.0, Color32::TRANSPARENT, ink); - } - "arc_tangent" => { - pen.line(&[(2.0, 18.0), (11.0, 18.0)], ink); - pen.arc(11.0, 10.0, 8.0, -std::f32::consts::FRAC_PI_2, 0.6, a); - pen.dot(11.0, 18.0, ink); - } - "circle_tt" | "circle_ttt" => { - if name == "circle_ttt" { - pen.closed(&[(3.0, 20.0), (21.0, 20.0), (12.0, 4.0)], ink); - } else { - pen.line(&[(21.0, 20.0), (3.0, 20.0), (12.0, 4.0)], ink); - } - pen.circle(12.0, 14.74, 5.26, Color32::TRANSPARENT, a); - } - "arc_slot" => { - let (t0, t1) = (0.75_f32, 2.4_f32); - pen.arc(12.0, 21.0, 13.0, t0, t1, a); - pen.arc(12.0, 21.0, 7.0, t0, t1, a); - pen.arc(12.0 + 10.0 * t0.cos(), 21.0 - 10.0 * t0.sin(), 3.0, t0, t0 - std::f32::consts::PI, a); - pen.arc(12.0 + 10.0 * t1.cos(), 21.0 - 10.0 * t1.sin(), 3.0, t1, t1 + std::f32::consts::PI, a); - pen.dot(12.0, 21.0, ink); - } - "ellipse" => { - let pts: Vec<(f32, f32)> = - (0..40).map(|i| i as f32 / 40.0 * std::f32::consts::TAU).map(|t| (12.0 + 9.5 * t.cos(), 12.0 + 5.5 * t.sin())).collect(); - pen.closed(&pts, a); - pen.dot(12.0, 12.0, ink); - pen.line(&[(12.0, 12.0), (21.5, 12.0)], ink); - } - "spline" | "spline_cv" => { - let c = [(3.0_f32, 18.0_f32), (8.0, 3.0), (16.0, 21.0), (21.0, 6.0)]; - let pts: Vec<(f32, f32)> = (0..=24) - .map(|i| i as f32 / 24.0) - .map(|t| { - let u = 1.0 - t; - let w = [u * u * u, 3.0 * u * u * t, 3.0 * u * t * t, t * t * t]; - (w.iter().zip(&c).map(|(w, q)| w * q.0).sum(), w.iter().zip(&c).map(|(w, q)| w * q.1).sum()) - }) - .collect(); - if name == "spline_cv" { - pen.line(&c, ink.gamma_multiply(0.6)); - for q in c { - pen.dot(q.0, q.1, ink); - } - } else { - for i in [0, 8, 16, 24] { - if let Some(q) = pts.get(i) { - pen.dot(q.0, q.1, ink); - } - } - } - pen.line(&pts, a); - } - "conic" => { - pen.line(&[(3.0, 20.0), (12.0, 3.0), (21.0, 20.0)], ink.gamma_multiply(0.6)); - let pts: Vec<(f32, f32)> = - (0..=20).map(|i| i as f32 / 20.0).map(|t| (3.0 + 18.0 * t, 20.0 - 4.0 * 0.78 * 17.0 * t * (1.0 - t))).collect(); - pen.line(&pts, a); - pen.dot(3.0, 20.0, ink); - pen.dot(21.0, 20.0, ink); - } - "text" => { - p.text(pen.at(12.0, 11.5), egui::Align2::CENTER_CENTER, "A", egui::FontId::proportional(17.0 * pen.s()), a); - pen.line(&[(4.0, 20.5), (20.0, 20.5)], ink); - } - "curvature_comb" => { - let f = |x: f32| 16.0 - 6.0 * ((x - 3.0) / 18.0 * std::f32::consts::PI).sin(); - let mut env = Vec::new(); - for i in 0..=8 { - let x = 3.0 + 18.0 * i as f32 / 8.0; - let k = 1.0 + 5.0 * ((x - 3.0) / 18.0 * std::f32::consts::PI).sin(); - pen.line(&[(x, f(x)), (x, f(x) - k)], a.gamma_multiply(0.7)); - env.push((x, f(x) - k)); - } - pen.line(&env, a); - let pts: Vec<(f32, f32)> = (0..=24).map(|i| 3.0 + 18.0 * i as f32 / 24.0).map(|x| (x, f(x))).collect(); - pen.line(&pts, ink); - } - "min_radius" => { - pen.line(&[(3.0, 20.0), (7.0, 9.0), (12.0, 5.5), (17.0, 9.0), (21.0, 20.0)], ink); - pen.circle(12.0, 10.5, 5.0, Color32::TRANSPARENT, a); - pen.arrow((12.0, 10.5), (15.5, 7.0), a); - pen.dot(12.0, 10.5, ink); - } - "iso_analysis" => { - pen.poly(&[(3.0, 17.0), (9.0, 5.0), (21.0, 7.0), (15.0, 19.0)], pen.fill, ink); - for k in 1..4 { - let t = k as f32 / 4.0; - pen.line(&[(3.0 + 6.0 * t, 17.0 - 12.0 * t), (15.0 + 6.0 * t, 19.0 - 12.0 * t)], a); - pen.line(&[(3.0 + 12.0 * t, 17.0 + 2.0 * t), (9.0 + 12.0 * t, 5.0 + 2.0 * t)], a); - } - } - "accessibility" => { - pen.poly(&[(2.0, 20.0), (22.0, 20.0), (22.0, 15.0), (2.0, 15.0)], ic.access[0], ink); - pen.poly(&[(8.0, 15.0), (16.0, 15.0), (16.0, 17.5), (8.0, 17.5)], ic.access[1], Color32::TRANSPARENT); - pen.poly(&[(6.0, 10.0), (18.0, 10.0), (18.0, 12.5), (6.0, 12.5)], pen.fill, ink); - pen.arrow((4.0, 2.0), (4.0, 13.0), a); - pen.arrow((20.0, 2.0), (20.0, 13.0), a); - } - "environment_map" => { - let pts: Vec = - (0..32).map(|i| i as f32 / 32.0 * std::f32::consts::TAU).map(|t| pen.at(12.0 + 9.0 * t.cos(), 12.0 + 9.0 * t.sin())).collect(); - p.add(Shape::convex_polygon(pts, ic.sky, Stroke::new(pen.w, ink))); - pen.poly(&[(3.6, 13.0), (20.4, 13.0), (18.5, 17.5), (5.5, 17.5)], ic.floor, Color32::TRANSPARENT); - pen.line(&[(3.2, 12.6), (20.8, 12.6)], ic.highlight); - p.circle_filled(pen.at(9.0, 8.0), 2.2 * pen.s(), ic.highlight); - pen.circle(12.0, 12.0, 9.0, Color32::TRANSPARENT, ink); - } - "zebra" | "draft_analysis" | "curvature_map" => { - let pts: Vec = - (0..32).map(|i| i as f32 / 32.0 * std::f32::consts::TAU).map(|t| pen.at(12.0 + 9.0 * t.cos(), 12.0 + 9.0 * t.sin())).collect(); - match name { - "zebra" => { - p.add(Shape::convex_polygon(pts, ic.highlight, Stroke::new(pen.w, ink))); - for k in 0..3 { - let y = 6.5 + k as f32 * 4.5; - let h = (81.0 - (y + 1.0 - 12.0) * (y + 1.0 - 12.0)).max(0.0).sqrt(); - pen.poly(&[(12.0 - h, y), (12.0 + h, y), (12.0 + h, y + 2.2), (12.0 - h, y + 2.2)], ic.stripe, Color32::TRANSPARENT); - } - pen.circle(12.0, 12.0, 9.0, Color32::TRANSPARENT, ink); - } - "draft_analysis" => { - pen.poly(&[(4.0, 20.0), (20.0, 20.0), (17.0, 5.0), (7.0, 5.0)], ic.draft[0], ink); - pen.poly(&[(7.0, 5.0), (17.0, 5.0), (16.0, 9.0), (8.0, 9.0)], ic.draft[1], Color32::TRANSPARENT); - pen.arrow((12.0, 16.0), (12.0, 1.5), a); - } - _ => { - p.add(Shape::convex_polygon(pts, ic.curvature[0], Stroke::new(pen.w, ink))); - pen.circle(14.0, 10.0, 5.0, ic.curvature[1], Color32::TRANSPARENT); - pen.circle(15.5, 8.5, 2.2, ic.curvature[2], Color32::TRANSPARENT); - } - } - } - "center_of_mass" => { - pen.circle(12.0, 12.0, 8.0, Color32::TRANSPARENT, ink); - let q = |a0: f32| { - let pts: Vec = std::iter::once(pen.at(12.0, 12.0)) - .chain( - (0..=8) - .map(|i| a0 + i as f32 / 8.0 * std::f32::consts::FRAC_PI_2) - .map(|t| pen.at(12.0 + 8.0 * t.cos(), 12.0 - 8.0 * t.sin())), - ) - .collect(); - p.add(Shape::convex_polygon(pts, a, Stroke::NONE)); - }; - q(0.0); - q(std::f32::consts::PI); - pen.circle(12.0, 12.0, 8.0, Color32::TRANSPARENT, ink); - } - "sketch_fillet" => { - pen.line(&[(4.0, 21.0), (4.0, 12.0)], ink); - pen.line(&[(12.0, 4.0), (21.0, 4.0)], ink); - pen.arc(12.0, 12.0, 8.0, std::f32::consts::FRAC_PI_2, std::f32::consts::PI, a); - p.add(Shape::dashed_line( - &[pen.at(4.0, 12.0), pen.at(4.0, 4.0), pen.at(12.0, 4.0)], - Stroke::new(pen.w * 0.7, ink.gamma_multiply(0.5)), - 2.0 * pen.s(), - 1.5 * pen.s(), - )); - } - "sketch_chamfer" => { - pen.line(&[(4.0, 21.0), (4.0, 12.0)], ink); - pen.line(&[(12.0, 4.0), (21.0, 4.0)], ink); - pen.line(&[(4.0, 12.0), (12.0, 4.0)], a); - p.add(Shape::dashed_line( - &[pen.at(4.0, 12.0), pen.at(4.0, 4.0), pen.at(12.0, 4.0)], - Stroke::new(pen.w * 0.7, ink.gamma_multiply(0.5)), - 2.0 * pen.s(), - 1.5 * pen.s(), - )); - } - // The three chamfer types, for the Chamfer Type row of the corner tool window. Each - // draws the cut it makes: equal legs with a tick on each, two unequal legs, and one leg - // with the angle the cut leaves it at. - "sketch_chamfer_equal" => { - pen.line(&[(4.0, 21.0), (4.0, 12.0)], ink); - pen.line(&[(12.0, 4.0), (21.0, 4.0)], ink); - pen.line(&[(4.0, 12.0), (12.0, 4.0)], a); - pen.line(&[(2.5, 9.5), (5.5, 6.5)], a); - pen.line(&[(9.5, 2.5), (6.5, 5.5)], a); - p.add(Shape::dashed_line( - &[pen.at(4.0, 12.0), pen.at(4.0, 4.0), pen.at(12.0, 4.0)], - Stroke::new(pen.w * 0.7, ink.gamma_multiply(0.5)), - 2.0 * pen.s(), - 1.5 * pen.s(), - )); - } - "sketch_chamfer_two" => { - pen.line(&[(4.0, 21.0), (4.0, 10.0)], ink); - pen.line(&[(18.0, 4.0), (21.0, 4.0)], ink); - pen.line(&[(4.0, 10.0), (18.0, 4.0)], a); - p.add(Shape::dashed_line( - &[pen.at(4.0, 10.0), pen.at(4.0, 4.0), pen.at(18.0, 4.0)], - Stroke::new(pen.w * 0.7, ink.gamma_multiply(0.5)), - 2.0 * pen.s(), - 1.5 * pen.s(), - )); - } - "sketch_chamfer_angle" => { - pen.line(&[(4.0, 21.0), (4.0, 13.0)], ink); - pen.line(&[(14.0, 4.0), (21.0, 4.0)], ink); - pen.line(&[(4.0, 13.0), (14.0, 4.0)], a); - pen.arc(4.0, 13.0, 6.0, -std::f32::consts::FRAC_PI_2, 0.73, a); - p.add(Shape::dashed_line( - &[pen.at(4.0, 13.0), pen.at(4.0, 4.0), pen.at(14.0, 4.0)], - Stroke::new(pen.w * 0.7, ink.gamma_multiply(0.5)), - 2.0 * pen.s(), - 1.5 * pen.s(), - )); - } - "offset" => { - pen.closed(&[(8.0, 9.0), (16.0, 9.0), (16.0, 15.0), (8.0, 15.0)], ink); - pen.line(&[(8.0, 4.0), (16.0, 4.0)], a); - pen.line(&[(21.0, 9.0), (21.0, 15.0)], a); - pen.line(&[(16.0, 20.0), (8.0, 20.0)], a); - pen.line(&[(3.0, 15.0), (3.0, 9.0)], a); - pen.arc(16.0, 9.0, 5.0, 0.0, std::f32::consts::FRAC_PI_2, a); - pen.arc(16.0, 15.0, 5.0, -std::f32::consts::FRAC_PI_2, 0.0, a); - pen.arc(8.0, 15.0, 5.0, std::f32::consts::PI, std::f32::consts::PI * 1.5, a); - pen.arc(8.0, 9.0, 5.0, std::f32::consts::FRAC_PI_2, std::f32::consts::PI, a); - } - // Offset mode icons, drawn with the house `dashed` helper. The source is the solid - // line and each result a dashed one. One Side has a single result; Two Sides has one - // each way at two different gaps; Symmetric has one each way at the same gap. - "offset_one_side" => { - pen.line(&[(8.0, 4.0), (8.0, 20.0)], ink); - pen.dashed(&[(18.0, 4.0), (18.0, 20.0)], a); - } - "offset_two_sides" => { - pen.line(&[(13.0, 4.0), (13.0, 20.0)], ink); - pen.dashed(&[(3.0, 4.0), (3.0, 20.0)], a); - pen.dashed(&[(20.0, 4.0), (20.0, 20.0)], a); - } - "offset_symmetric" => { - pen.line(&[(12.0, 4.0), (12.0, 20.0)], ink); - pen.dashed(&[(4.0, 4.0), (4.0, 20.0)], a); - pen.dashed(&[(20.0, 4.0), (20.0, 20.0)], a); - } - "trim" => { - pen.line(&[(12.0, 3.0), (12.0, 21.0)], ink); - pen.line(&[(3.0, 12.0), (12.0, 12.0)], ink); - p.add(Shape::dashed_line(&[pen.at(12.0, 12.0), pen.at(21.0, 12.0)], Stroke::new(pen.w, a), 2.0 * pen.s(), 1.6 * pen.s())); - pen.line(&[(15.0, 15.0), (19.0, 19.0)], a); - pen.line(&[(19.0, 15.0), (15.0, 19.0)], a); - } - "patch" => { - // A wire loop with a surface spanned across it. - pen.poly(&[(4.0, 16.0), (9.0, 6.0), (19.0, 5.0), (20.0, 15.0), (12.0, 20.0)], ic.sheet, ink); - pen.line(&[(4.0, 16.0), (9.0, 6.0), (19.0, 5.0), (20.0, 15.0), (12.0, 20.0), (4.0, 16.0)], a); - } - "stitch" => { - // Two surface patches sewn along their shared edge. - pen.poly(&[(3.0, 18.0), (11.0, 14.0), (11.0, 4.0), (3.0, 8.0)], ic.sheet, ink); - pen.poly(&[(13.0, 14.0), (21.0, 18.0), (21.0, 8.0), (13.0, 4.0)], ic.sheet_2, ink); - for y in [6.0, 9.0, 12.0] { - pen.line(&[(10.0, y), (14.0, y + 1.5)], a); - } - } - "thicken" => { - // A wavy sheet (accent edges) grown downward into a slab, shaded like `iso_box`. - let f = [(2.0, 12.5), (7.0, 9.5), (12.0, 15.5), (17.0, 12.5)]; - let b = [(6.4, 8.9), (11.4, 5.9), (16.4, 11.9), (21.4, 8.9)]; - let d = f.map(|(x, y)| (x, y + 6.0)); - pen.band(&bezier(b, 16), &bezier(f, 16), ic.sheet); - pen.band(&bezier(f, 16), &bezier(d, 16), pen.fill.gamma_multiply(0.85)); - pen.poly(&[(17.0, 12.5), (21.4, 8.9), (21.4, 14.9), (17.0, 18.5)], pen.fill.gamma_multiply(0.7), ink); - pen.line(&[(2.0, 12.5), (2.0, 18.5)], ink); - pen.line(&[(17.0, 12.5), (17.0, 18.5)], ink); - pen.line(&[(21.4, 8.9), (21.4, 14.9)], ink); - pen.curve(d, ink); - pen.line(&[(6.4, 8.9), (2.0, 12.5)], ink); - pen.line(&[(21.4, 8.9), (17.0, 12.5)], a); - pen.curve(b, a); - pen.curve(f, a); - } - "extend" => { - pen.line(&[(20.0, 3.0), (20.0, 21.0)], ink); - pen.line(&[(3.0, 12.0), (10.0, 12.0)], ink); - pen.dot(10.0, 12.0, ink); - pen.arrow((10.0, 12.0), (19.0, 12.0), a); - } - "break" => { - pen.line(&[(3.0, 17.0), (10.5, 11.0)], ink); - pen.line(&[(13.5, 10.0), (21.0, 5.0)], a); - pen.dot(12.0, 10.5, a); - pen.line(&[(9.0, 5.0), (15.0, 16.0)], ink.gamma_multiply(0.5)); - } - "scale" => { - pen.closed(&[(3.0, 13.0), (11.0, 13.0), (11.0, 21.0), (3.0, 21.0)], ink); - pen.closed(&[(3.0, 3.0), (21.0, 3.0), (21.0, 21.0), (3.0, 21.0)], a); - pen.dot(3.0, 21.0, ink); - pen.arrow((11.0, 13.0), (18.0, 6.0), a); - } - "blend" => { - pen.line(&[(2.0, 19.0), (8.0, 15.0)], ink); - pen.line(&[(16.0, 9.0), (22.0, 5.0)], ink); - pen.curve([(8.0, 15.0), (12.5, 12.0), (11.5, 12.0), (16.0, 9.0)], a); - pen.dot(8.0, 15.0, ink); - pen.dot(16.0, 9.0, ink); - } - // Surface modelling: open sheets (no caps) in the sheet colours, the input or the result - // in the accent. - "surface_extrude" => { - // An open profile (accent) pulled up into a sheet. - let top = [(4.0, 11.0), (8.0, 8.0), (13.0, 14.0), (20.0, 10.0)]; - let bottom = [(4.0, 20.0), (8.0, 17.0), (13.0, 23.0), (20.0, 19.0)]; - pen.band(&bezier(top, 16), &bezier(bottom, 16), ic.sheet); - pen.line(&[(4.0, 11.0), (4.0, 20.0)], ink); - pen.line(&[(20.0, 10.0), (20.0, 19.0)], ink); - pen.curve(top, ink); - pen.curve(bottom, a); - pen.arrow((12.0, 8.5), (12.0, 2.5), a); - } - "surface_revolve" => { - // An open profile (accent) turned about the axis into a hollow band. - let left = [(5.0, 6.0), (7.0, 10.0), (5.0, 14.0), (6.0, 18.0)]; - let right = [(19.0, 6.0), (17.0, 10.0), (19.0, 14.0), (18.0, 18.0)]; - pen.band(&bezier(left, 16), &bezier(right, 16), ic.sheet); - let front = ellipse(12.0, 18.0, 6.0, 2.2, 0.0, std::f32::consts::PI, 16); - pen.poly(&front, ic.sheet, Color32::TRANSPARENT); - // A closed fill takes each point once: a repeated end point spikes `poly`'s edge. - let mut opening = ellipse(12.0, 6.0, 7.0, 2.2, 0.0, std::f32::consts::TAU, 32); - opening.pop(); - pen.poly(&opening, ic.sheet_light, ink); - pen.line(&front, ink); - pen.curve(left, ink); - pen.line(&[(12.0, 1.5), (12.0, 3.8)], ink); - pen.line(&[(12.0, 20.2), (12.0, 22.5)], ink); - pen.curve(right, a); - } - "surface_sweep" => { - // A straight profile (accent) carried along a curved path: a ribbon. - let near = [(4.0, 13.0), (8.0, 5.0), (14.0, 5.0), (20.0, 9.0)]; - let far = [(4.0, 20.0), (8.0, 12.0), (14.0, 12.0), (20.0, 16.0)]; - pen.band(&bezier(near, 16), &bezier(far, 16), ic.sheet); - pen.curve(near, ink); - pen.curve(far, ink); - pen.line(&[(4.0, 13.0), (4.0, 20.0)], a); - pen.line(&[(20.0, 9.0), (20.0, 16.0)], ink); - } - "surface_loft" => { - // Two open sections (accent) joined by a sheet. - let top = [(5.0, 6.0), (9.0, 3.0), (14.0, 8.0), (19.0, 5.0)]; - let bottom = [(3.0, 19.0), (8.0, 15.0), (14.0, 22.0), (21.0, 18.0)]; - pen.band(&bezier(top, 16), &bezier(bottom, 16), ic.sheet); - pen.line(&[(5.0, 6.0), (3.0, 19.0)], ink); - pen.line(&[(19.0, 5.0), (21.0, 18.0)], ink); - pen.curve(top, a); - pen.curve(bottom, a); - } - "surface_offset" => { - // A wavy sheet and its offset copy (dashed), the distance shown by the arrow. - let back = [(6.0, 14.0), (10.0, 11.0), (15.0, 17.0), (21.0, 14.0)]; - let front = [(3.0, 20.0), (7.0, 17.0), (12.0, 23.0), (18.0, 20.0)]; - pen.band(&bezier(back, 16), &bezier(front, 16), ic.sheet); - pen.curve(back, ink); - pen.curve(front, ink); - pen.line(&[(6.0, 14.0), (3.0, 20.0)], ink); - pen.line(&[(21.0, 14.0), (18.0, 20.0)], ink); - pen.dashed(&bezier(back.map(|(x, y)| (x, y - 9.0)), 16), a); - pen.dashed(&bezier(front.map(|(x, y)| (x, y - 9.0)), 16), a); - pen.dashed(&[(6.0, 5.0), (3.0, 11.0)], a); - pen.dashed(&[(21.0, 5.0), (18.0, 11.0)], a); - pen.arrow((12.0, 17.0), (12.0, 8.5), a); - } - "ruled" => { - // A strip hanging off a block's edge (accent), ruled by straight lines. - pen.poly(&[(2.0, 12.0), (12.0, 12.0), (12.0, 22.0), (2.0, 22.0)], pen.fill, ink); - pen.poly(&[(12.0, 12.0), (21.0, 4.0), (21.0, 14.0), (12.0, 22.0)], ic.sheet, ink); - pen.line(&[(15.0, 9.3), (15.0, 19.3)], a); - pen.line(&[(18.0, 6.7), (18.0, 16.7)], a); - pen.line(&[(12.0, 12.0), (12.0, 22.0)], a); - } - "boundary" => { - // Four boundary curves (accent) meeting at corners, the patch between them a Coons - // patch with its iso lines. - let (c00, c10, c11, c01) = ((4.0, 17.0), (10.0, 5.0), (20.0, 7.0), (17.0, 20.0)); - let left = [c00, (5.0, 12.0), (8.0, 8.0), c10]; - let top = [c10, (13.0, 4.0), (17.0, 4.0), c11]; - let right = [c01, (19.0, 16.0), (20.0, 11.0), c11]; - let bottom = [c00, (8.0, 20.0), (13.0, 21.0), c01]; - let n = 12; - let lerp = |p: (f32, f32), q: (f32, f32), s: f32| (p.0 + (q.0 - p.0) * s, p.1 + (q.1 - p.1) * s); - let (b, t) = (bezier(bottom, n), bezier(top, n)); - // Each row runs across the patch (u) at one height (v) up the left and right sides. - let rows: Vec> = bezier(left, n) - .into_iter() - .zip(bezier(right, n)) - .enumerate() - .map(|(j, (lv, rv))| { - let v = j as f32 / n as f32; - b.iter() - .zip(&t) - .enumerate() - .map(|(i, (bu, tu))| { - let u = i as f32 / n as f32; - let corners = lerp(lerp(c00, c01, u), lerp(c10, c11, u), v); - let (ruled_v, ruled_u) = (lerp(*bu, *tu, v), lerp(lv, rv, u)); - (ruled_v.0 + ruled_u.0 - corners.0, ruled_v.1 + ruled_u.1 - corners.1) - }) - .collect() - }) - .collect(); - for (lo, hi) in rows.iter().zip(rows.iter().skip(1)) { - pen.band(lo, hi, ic.sheet); - } - for j in [n / 3, 2 * n / 3] { - if let Some(r) = rows.get(j) { - pen.line(r, ic.sheet_line); - } - pen.line(&rows.iter().filter_map(|r| r.get(j).copied()).collect::>(), ic.sheet_line); - } - for c in [left, top, right, bottom] { - pen.curve(c, a); - } - for (x, y) in [c00, c10, c11, c01] { - pen.dot(x, y, ink); - } - } - "surface_blend" => { - // Two separate sheets at different heights bridged by a smooth strip (accent) that - // leaves both edges tangent. - let near = [(13.0, 20.0), (8.5, 20.0), (10.5, 11.0), (6.0, 11.0)]; - let far = near.map(|(x, y)| (x + 4.4, y - 3.6)); - pen.poly(&[(2.0, 11.0), (6.0, 11.0), (10.4, 7.4), (6.4, 7.4)], ic.sheet, ink); - pen.band(&bezier(near, 16), &bezier(far, 16), ic.sheet_light); - pen.curve(far, a); - pen.poly(&[(13.0, 20.0), (17.0, 20.0), (21.4, 16.4), (17.4, 16.4)], ic.sheet, ink); - pen.curve(near, a); - } - "untrim" => { - // A trimmed sheet whose hole (dashed, accent) is filled back in. - pen.poly(&[(3.0, 17.0), (9.0, 5.0), (21.0, 7.0), (16.0, 20.0)], ic.sheet, ink); - let hole = ellipse(12.2, 12.3, 4.2, 3.2, 0.0, std::f32::consts::TAU, 32); - pen.poly(hole.get(1..).unwrap_or_default(), ic.sheet_light, Color32::TRANSPARENT); - pen.dashed(&hole, a); - } - "unstitch" => { - // Two sheets pulled apart from their shared edge. - pen.poly(&[(2.0, 18.0), (9.0, 14.0), (9.0, 4.0), (2.0, 8.0)], ic.sheet, ink); - pen.poly(&[(15.0, 14.0), (22.0, 18.0), (22.0, 8.0), (15.0, 4.0)], ic.sheet_2, ink); - pen.arrow((10.8, 11.0), (7.0, 11.0), a); - pen.arrow((13.2, 11.0), (17.0, 11.0), a); - } - "reverse_normal" => { - // A sheet with its old normal (faded) and the flipped one (accent). - pen.poly(&[(2.0, 14.0), (14.0, 10.0), (22.0, 13.0), (10.0, 18.0)], ic.sheet, ink); - pen.arrow((12.0, 14.0), (12.0, 3.5), ink.gamma_multiply(0.5)); - pen.arrow((12.0, 14.0), (12.0, 22.5), a); - } - "surface_fillet" => { - // A wall sheet and a floor sheet joined by a round strip (accent edges). - let near = [(3.0, 16.0), (3.0, 18.8), (5.2, 21.0), (8.0, 21.0)]; - let far = near.map(|(x, y)| (x + 4.4, y - 3.6)); - pen.poly(&[(3.0, 16.0), (3.0, 8.0), (7.4, 4.4), (7.4, 12.4)], ic.sheet_2, ink); - pen.poly(&[(8.0, 21.0), (16.0, 21.0), (20.4, 17.4), (12.4, 17.4)], ic.sheet, ink); - pen.band(&bezier(near, 16), &bezier(far, 16), ic.sheet_light); - pen.line(&[(4.5, 19.5), (8.9, 15.9)], ic.sheet_line); - pen.curve(far, a); - pen.curve(near, a); - } - "continuity" => { - // A straight curve joining a curved one (accent) at a point, with the curved one's - // curvature comb: teeth that grow as the curve tightens. - pen.line(&[(2.0, 17.0), (12.0, 13.0)], ink); - let c = [(12.0, 13.0), (16.5, 11.2), (20.0, 8.0), (21.0, 3.0)]; - let pts = bezier(c, 10); - let mut tips = Vec::new(); - for (i, ((x, y), (x1, y1))) in pts.iter().zip(pts.iter().skip(1)).enumerate() { - let len = (x1 - x).hypot(y1 - y).max(1e-6); - let h = 0.8 + 4.2 * i as f32 / 10.0; - let tip = (x + (y1 - y) / len * h, y - (x1 - x) / len * h); - if i % 2 == 0 { - pen.line(&[(*x, *y), tip], ic.sheet_line); - } - tips.push(tip); - } - pen.line(&tips, ic.sheet_line); - pen.curve(c, a); - pen.dot(12.0, 13.0, a); - } - // Assembly: joints (accent: the joint itself). - "joint" => { - pen.iso_box(2.0, 13.0, 7.0, 5.0, 5.0); - pen.iso_box(12.0, 4.0, 7.0, 5.0, 5.0); - pen.line(&[(9.0, 15.0), (15.0, 9.0)], a); - pen.circle(12.0, 12.0, 2.6, pen.fill, a); - } - "as_built" => { - pen.iso_box(1.0, 10.0, 8.0, 5.0, 8.0); - pen.iso_box(10.0, 10.0, 8.0, 5.0, 8.0); - pen.circle(13.0, 15.0, 2.6, pen.fill, a); - } - "joint_origin" => { - pen.arrow((8.0, 17.0), (20.0, 17.0), ink); - pen.arrow((8.0, 17.0), (14.0, 11.0), ink); - pen.arrow((8.0, 17.0), (8.0, 3.0), a); - pen.circle(8.0, 17.0, 2.4, pen.fill, a); - } - "rigid_group" => { - pen.iso_box(3.0, 13.0, 5.0, 3.0, 4.0); - pen.iso_box(13.0, 13.0, 5.0, 3.0, 4.0); - pen.iso_box(8.0, 4.0, 5.0, 3.0, 4.0); - pen.closed(&[(2.0, 3.0), (22.0, 3.0), (22.0, 21.0), (2.0, 21.0)], a); - } - "drive" => { - pen.circle(12.0, 12.0, 4.0, pen.fill, ink); - pen.dot(12.0, 12.0, ink); - pen.arc(12.0, 12.0, 8.5, 0.3, 4.6, a); - pen.arrow((11.0, 20.4), (14.5, 20.3), a); - } - // A crank on its pivot and the closed path its far end draws (accent). - "trace" => { - pen.curve([(15.0, 13.0), (22.0, 13.0), (23.0, 3.0), (15.0, 3.0)], a); - pen.curve([(15.0, 3.0), (8.0, 3.0), (9.0, 13.0), (15.0, 13.0)], a); - pen.line(&[(5.0, 20.0), (15.0, 13.0)], ink); - pen.circle(5.0, 20.0, 2.4, pen.fill, ink); - pen.dot(15.0, 13.0, a); - } - "motion_link" => { - pen.circle(7.0, 14.0, 4.5, pen.fill, ink); - pen.circle(17.5, 9.0, 3.0, pen.fill, ink); - pen.line(&[(7.0, 14.0), (17.5, 9.0)], a); - pen.dot(7.0, 14.0, a); - pen.dot(17.5, 9.0, a); - } - // Couplings: the driven part in the accent. - "gear" => { - pen.circle(9.0, 14.0, 5.2, pen.fill, Color32::TRANSPARENT); - pen.closed(&teeth(9.0, 14.0, 5.2, 6.8, 9, 0.0), ink); - pen.dot(9.0, 14.0, ink); - pen.circle(17.6, 7.4, 3.2, pen.fill, Color32::TRANSPARENT); - pen.closed(&teeth(17.6, 7.4, 3.2, 4.6, 6, 0.3), a); - pen.dot(17.6, 7.4, a); - } - "rack_pinion" => { - pen.circle(12.0, 7.4, 4.2, pen.fill, Color32::TRANSPARENT); - pen.closed(&teeth(12.0, 7.4, 4.2, 5.8, 8, 0.2), ink); - pen.dot(12.0, 7.4, ink); - let mut rack = vec![(2.0, 18.0)]; - for k in 0..5 { - let x = 2.5 + 4.0 * k as f32; - rack.extend([(x, 14.3), (x + 1.0, 12.8), (x + 2.2, 12.8), (x + 3.2, 14.3)]); - } - rack.extend([(22.0, 14.3), (22.0, 18.0)]); - pen.closed(&rack, a); - pen.arrow((7.0, 21.5), (17.5, 21.5), a); - } - "screw" => { - pen.poly(&[(9.5, 2.0), (14.5, 2.0), (14.5, 22.0), (9.5, 22.0)], pen.fill, ink); - for y in [5.0, 8.0, 17.0, 20.0] { - pen.line(&[(9.5, y), (14.5, y - 2.0)], ink); - } - pen.poly(&[(6.0, 10.0), (18.0, 10.0), (18.0, 15.0), (6.0, 15.0)], pen.fill, a); - pen.arrow((21.0, 17.0), (21.0, 6.0), a); - } - "cam" => { - // A cam on its shaft, its lobe up under the follower it lifts. - let cam: Vec<(f32, f32)> = (0..36) - .map(|k| { - let t = std::f32::consts::TAU * k as f32 / 36.0; - let r = 5.0 + 3.0 * (0.5 + 0.5 * t.sin()).powi(3); - (11.0 + r * t.cos(), 16.0 - r * t.sin()) - }) - .collect(); - pen.poly(&cam, pen.fill, ink); - pen.dot(11.0, 16.0, ink); - pen.poly(&[(9.0, 2.0), (13.0, 2.0), (13.0, 8.0), (9.0, 8.0)], pen.fill, a); - pen.arrow((18.5, 9.5), (18.5, 2.5), a); - } - "interference" => { - pen.closed(&[(3.0, 5.0), (14.0, 5.0), (14.0, 16.0), (3.0, 16.0)], ink); - pen.poly(&[(9.0, 10.0), (14.0, 10.0), (14.0, 16.0), (9.0, 16.0)], ic.interference, ic.interference); - pen.closed(&[(9.0, 10.0), (21.0, 10.0), (21.0, 20.0), (9.0, 20.0)], ink); - } - // Sheet metal: thin plates (fill), the made or changed part in the accent. - "flange" => { - pen.poly(&[(2.0, 17.0), (12.0, 13.0), (18.0, 16.0), (8.0, 20.0)], pen.fill, ink); - pen.poly(&[(12.0, 13.0), (12.0, 5.0), (18.0, 8.0), (18.0, 16.0)], pen.fill.gamma_multiply(0.8), a); - pen.arrow((20.5, 15.0), (20.5, 4.0), a); - } - "hem" => { - pen.line(&[(2.0, 16.0), (16.0, 16.0)], ink); - pen.line(&[(2.0, 13.0), (16.0, 13.0)], ink); - pen.arc(16.0, 11.5, 4.5, -1.57, 1.57, a); - pen.line(&[(16.0, 7.0), (8.0, 7.0)], a); - } - // Fastener: a socket head screw seen from the side. - "fastener" => { - pen.poly(&[(6.0, 3.0), (18.0, 3.0), (18.0, 9.0), (6.0, 9.0)], pen.fill, ink); - pen.line(&[(10.0, 3.0), (10.0, 6.0), (14.0, 6.0), (14.0, 3.0)], a); - pen.poly(&[(9.0, 9.0), (15.0, 9.0), (15.0, 21.0), (9.0, 21.0)], pen.fill.gamma_multiply(0.85), ink); - for y in [12.0, 15.0, 18.0] { - pen.line(&[(9.0, y), (15.0, y + 1.5)], ink); - } - } - "fold" => { - pen.line(&[(2.0, 18.0), (12.0, 18.0)], ink); - pen.line(&[(12.0, 18.0), (19.0, 6.0)], a); - pen.line(&[(9.0, 21.0), (15.0, 15.0)], ic.fold); - pen.arc(12.0, 18.0, 6.0, 0.2, 1.0, a); - pen.arrow((18.0, 16.0), (17.0, 12.5), a); - } - "unfold" => { - pen.line(&[(3.0, 18.0), (12.0, 18.0), (12.0, 7.0)], ink); - pen.line(&[(12.0, 18.0), (22.0, 18.0)], a); - pen.arc(12.0, 18.0, 7.0, 0.3, 1.4, a); - pen.arrow((17.5, 14.0), (19.0, 16.0), a); - } - "refold" => { - pen.line(&[(3.0, 18.0), (12.0, 18.0), (21.0, 18.0)], ink); - pen.line(&[(12.0, 18.0), (12.0, 6.0)], a); - pen.arc(12.0, 18.0, 7.0, 0.3, 1.4, a); - pen.arrow((15.0, 11.5), (13.5, 10.5), a); - } - "flat" => { - pen.poly(&[(3.0, 6.0), (21.0, 6.0), (21.0, 18.0), (3.0, 18.0)], pen.fill, ink); - for x in [9.0, 15.0] { - for k in 0..3 { - let y = 7.5 + k as f32 * 4.0; - pen.line(&[(x, y), (x, y + 2.2)], a); - } - } - } - "convert_sheet" => { - pen.iso_box(2.0, 9.0, 6.0, 4.0, 6.0); - pen.arrow((11.0, 12.0), (15.0, 12.0), a); - pen.poly(&[(15.0, 15.0), (21.0, 12.0), (23.0, 13.0), (17.0, 16.0)], pen.fill, a); - } - "sheet_rules" => { - pen.poly(&[(2.0, 15.0), (11.0, 11.0), (17.0, 14.0), (8.0, 18.0)], pen.fill, ink); - for y in [5.0, 9.0, 13.0] { - pen.line(&[(14.0, y), (22.0, y)], a); - } - } - // Plastic: a wall or plate (fill) with the feature in the accent. - "boss" => { - pen.poly(&[(2.0, 16.0), (13.0, 12.0), (22.0, 16.0), (11.0, 20.0)], pen.fill, ink); - pen.poly(&[(9.0, 7.0), (15.0, 7.0), (15.0, 16.0), (9.0, 16.0)], pen.fill.gamma_multiply(0.85), a); - pen.circle(12.0, 7.0, 3.0, pen.fill, a); - pen.circle(12.0, 7.0, 1.2, ink, ink); - } - "lip" => { - pen.poly(&[(4.0, 21.0), (4.0, 9.0), (10.0, 9.0), (10.0, 21.0)], pen.fill, ink); - pen.poly(&[(4.0, 9.0), (4.0, 4.0), (7.0, 4.0), (7.0, 9.0)], pen.fill, a); - pen.poly(&[(14.0, 21.0), (14.0, 6.0), (20.0, 6.0), (20.0, 21.0)], pen.fill, ink); - pen.closed(&[(17.0, 6.0), (17.0, 11.0), (20.0, 11.0), (20.0, 6.0)], a); - } - "snap" => { - pen.poly(&[(2.0, 20.0), (2.0, 16.0), (22.0, 16.0), (22.0, 20.0)], pen.fill, ink); - pen.poly(&[(9.0, 16.0), (9.0, 4.0), (12.0, 4.0), (12.0, 16.0)], pen.fill, a); - pen.poly(&[(12.0, 4.0), (16.0, 8.0), (12.0, 8.0)], a, a); - } - "rest" => { - pen.poly(&[(2.0, 16.0), (13.0, 12.0), (22.0, 16.0), (11.0, 20.0)], pen.fill, ink); - pen.poly(&[(7.0, 14.0), (13.0, 11.5), (17.0, 13.5), (11.0, 16.0)], a, a); - pen.line(&[(7.0, 14.0), (7.0, 11.5), (13.0, 9.0), (17.0, 11.0), (17.0, 13.5)], a); - } - "plastic_rule" => { - pen.circle(7.0, 8.0, 3.2, pen.fill, a); - pen.circle(11.0, 13.0, 3.2, pen.fill, a); - pen.circle(5.5, 16.0, 3.2, pen.fill, a); - for y in [6.0, 11.0, 16.0] { - pen.line(&[(15.0, y), (22.0, y)], ink); - } - } - "plastic_assign" => { - pen.iso_box(2.0, 12.0, 7.0, 5.0, 6.0); - pen.arrow((19.0, 6.0), (13.0, 11.0), a); - pen.circle(19.0, 6.0, 3.0, pen.fill, a); - } - // Appearance: a shaded ball with three paint dabs. - "appearance" => { - pen.circle(10.0, 13.0, 7.5, pen.fill, ink); - pen.arc(10.0, 13.0, 4.5, 1.8, 3.2, ic.highlight); - pen.circle(18.0, 5.0, 2.6, ic.paint[0], ink); - pen.circle(20.5, 11.0, 2.6, ic.paint[1], ink); - pen.circle(19.0, 17.5, 2.6, ic.paint[2], ink); - } - "warning" => { - pen.poly(&[(12.0, 3.0), (21.0, 20.0), (3.0, 20.0)], ic.warning, ink); - pen.line(&[(12.0, 9.0), (12.0, 14.0)], ink); - pen.dot(12.0, 17.0, ink); - } - // A failed feature: a disc with a cross, a different shape from the warning's triangle, so - // it reads without colour (#972). Drawn in the caller's colours only. - "error" => { - pen.circle(12.0, 12.0, 10.0, pen.fill, ink); - pen.line(&[(8.0, 8.0), (16.0, 16.0)], ink); - pen.line(&[(16.0, 8.0), (8.0, 16.0)], ink); - } - _ => { - pen.poly(&[(5.0, 5.0), (19.0, 5.0), (19.0, 19.0), (5.0, 19.0)], pen.fill, ink); - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - /// A debug dump of everything `paint` draws for `name`, for comparing two colour choices - /// without depending on epaint's internal shape fields. - fn drawn(name: &str, ink: Color32, fill: Color32, accent: Color32) -> String { - let ctx = egui::Context::default(); - let screen = Rect::from_min_size(Pos2::ZERO, vec2(100.0, 100.0)); - let input = egui::RawInput { screen_rect: Some(screen), ..Default::default() }; - let out = ctx.run_ui(input, |ui| { - let r = Rect::from_min_size(Pos2::ZERO, vec2(24.0, 24.0)); - paint(ui.painter(), r, name, ink, fill, accent); - }); - format!("{:?}", out.shapes) - } - - /// Command icons not drawn yet (the fallback square) or drawn past their square, from before - /// this check. Remove a name when its icon is fixed; never add one. - const KNOWN_GAPS: &[&str] = - &["cylinder", "pipe", "offset_face", "thread", "boundary_fill", "coil", "rib", "emboss", "capture", "revert", "camera", "material"]; - - /// What is wrong with the icon `name`: not drawn (the fallback square), or drawn past its - /// square by more than a stroke width. - fn icon_problem(name: &str) -> Option { - if drawn(name, Color32::BLACK, Color32::WHITE, Color32::BLUE) == drawn("", Color32::BLACK, Color32::WHITE, Color32::BLUE) { - return Some("no icon drawn".into()); - } - let side = 48.0; - let r = Rect::from_min_size(Pos2::ZERO, vec2(side, side)); - let ctx = egui::Context::default(); - let input = egui::RawInput { screen_rect: Some(Rect::from_min_size(Pos2::ZERO, vec2(100.0, 100.0))), ..Default::default() }; - let out = ctx.run_ui(input, |ui| paint(ui.painter(), r, name, Color32::BLACK, Color32::WHITE, Color32::BLUE)); - let used = out.shapes.iter().fold(Rect::NOTHING, |b, s| b.union(s.shape.visual_bounding_rect())); - (!r.expand(side / 24.0 * 1.4).contains_rect(used)).then(|| format!("drawn over {used:?}")) - } - - /// Every command's icon is drawn and stays inside its square. - #[test] - fn every_command_icon_is_drawn_inside_its_square() { - let bad: Vec = solidcraft_engine::command_specs() - .into_iter() - .filter(|c| !c.icon.is_empty() && !KNOWN_GAPS.contains(&c.icon)) - .filter_map(|c| icon_problem(c.icon).map(|why| format!("{} ({}): {why}", c.id, c.icon))) - .collect(); - assert!(bad.is_empty(), "{}", bad.join("\n")); - for gap in KNOWN_GAPS { - assert!(icon_problem(gap).is_some(), "`{gap}` is fixed now: take it off KNOWN_GAPS"); - } - } - - /// The Sketch Palette and the toolbar both rely on this contract. Construction and - /// centerline must track whatever accent colour the caller passes (both callers pass the - /// theme's construction orange) and must not change with `ink`. - #[test] - fn construction_and_centerline_icons_follow_the_callers_accent_only() { - for name in ["construction", "centerline", "infinite"] { - let base = drawn(name, Color32::BLACK, Color32::WHITE, Color32::from_rgb(1, 2, 3)); - let other_accent = drawn(name, Color32::BLACK, Color32::WHITE, Color32::from_rgb(4, 5, 6)); - assert_ne!(base, other_accent, "{name} icon must change with the caller's accent colour"); - let other_ink = drawn(name, Color32::from_rgb(9, 9, 9), Color32::WHITE, Color32::from_rgb(1, 2, 3)); - assert_eq!(base, other_ink, "{name} icon has no ink-coloured parts: ink alone must not change it"); - } - } - - /// Normal is a plain line type. It is never construction geometry. Its icon must never pick - /// up the accent colour. It can never render construction orange even if a caller passes it - /// by mistake. - #[test] - fn normal_icon_never_uses_the_accent_colour() { - let one_accent = drawn("normal", Color32::BLACK, Color32::WHITE, Color32::from_rgb(1, 2, 3)); - let other_accent = drawn("normal", Color32::BLACK, Color32::WHITE, Color32::from_rgb(4, 5, 6)); - assert_eq!(one_accent, other_accent, "the Normal icon must not change with the accent colour"); - let other_ink = drawn("normal", Color32::from_rgb(9, 9, 9), Color32::WHITE, Color32::from_rgb(1, 2, 3)); - assert_ne!(one_accent, other_ink, "the Normal icon must change with ink"); - } - - /// The dashes (as their first and last point) and the dots (as centre and radius) `paint` - /// actually drew for `name`, at `side` pixels square: the Sketch Palette draws these three - /// icons down to about 18 px. - fn dashes_and_dots(name: &str, side: f32) -> (Vec<(Pos2, Pos2)>, Vec<(Pos2, f32)>) { - let ctx = egui::Context::default(); - let screen = Rect::from_min_size(Pos2::ZERO, vec2(100.0, 100.0)); - let input = egui::RawInput { screen_rect: Some(screen), ..Default::default() }; - let r = Rect::from_min_size(Pos2::ZERO, vec2(side, side)); - let out = ctx.run_ui(input, |ui| { - paint(ui.painter(), r, name, Color32::BLACK, Color32::WHITE, Color32::from_rgb(232, 140, 48)); - }); - let dashes = out - .shapes - .iter() - .filter_map(|s| match &s.shape { - Shape::Path(p) => Some((*p.points.first()?, *p.points.last()?)), - _ => None, - }) - .collect(); - let dots = out.shapes.iter().filter_map(|s| if let Shape::Circle(c) = &s.shape { Some((c.center, c.radius)) } else { None }).collect(); - (dashes, dots) - } - - /// Pins the dash count and the gap. A future tidy-up might add dashes back in. It might - /// narrow the gap to look more faithful to the ISO convention. Either change would flatten - /// the icon back into a near-solid line at the size the Sketch Palette actually draws it, - /// about 18 px square. That is the exact defect this fix addressed. - #[test] - fn construction_and_centerline_read_as_two_dashes_with_a_gap_wider_than_the_stroke() { - for (name, want_dots) in [("construction", 0), ("centerline", 1)] { - for side in [18.0, 26.0] { - let (dashes, dots) = dashes_and_dots(name, side); - assert_eq!(dashes.len(), 2, "{name} at {side}px must be exactly two dashes"); - assert_eq!(dots.len(), want_dots, "{name} at {side}px has the wrong number of dots"); - let (_, gap_start) = dashes[0]; - let (gap_end, _) = dashes[1]; - let gap = gap_start.distance(gap_end); - let stroke = line_type_stroke(Rect::from_min_size(Pos2::ZERO, vec2(side, side))); - assert!( - gap > stroke * 2.0, - "{name} at {side}px: the gap of {gap:.1}px must exceed the {stroke:.1}px stroke by a wide margin, or the dashes read as one solid line" - ); - } - } - } - - /// Pins the shape of the Centerline dot. It must be large enough to read as a round mark - /// beside the stroke. It must sit clear of both dashes. The silhouette must stay line, dot, - /// line. - #[test] - fn centerline_dot_is_a_bold_mark_clear_of_both_dashes() { - for side in [18.0, 26.0] { - let (dashes, dots) = dashes_and_dots("centerline", side); - let (center, radius) = dots[0]; - let stroke = line_type_stroke(Rect::from_min_size(Pos2::ZERO, vec2(side, side))); - assert!(radius > stroke * 1.2, "centerline dot at {side}px: radius {radius:.1}px must exceed the {stroke:.1}px stroke by a wide margin"); - for (a, b) in &dashes { - assert!(center.distance(*a) > radius, "centerline dot at {side}px must not touch a dash end"); - assert!(center.distance(*b) > radius, "centerline dot at {side}px must not touch a dash end"); - } - } - } -} diff --git a/assets/styles/ui/palettes.rs b/assets/styles/ui/palettes.rs index 2236e5ff..46683450 100644 --- a/assets/styles/ui/palettes.rs +++ b/assets/styles/ui/palettes.rs @@ -1,6 +1,10 @@ //! Original light and dark palette values. Compiled by the UI theme boundary. use crate::theme::{AnalysisColors, IconColors, Tokens}; use egui::Color32; + +const fn icon_primary(rgb: [u8; 3]) -> Color32 { + Color32::from_rgb(rgb[0], rgb[1], rgb[2]) +} impl Tokens { pub const fn light() -> Tokens { Tokens { @@ -13,7 +17,7 @@ impl Tokens { text_dim: Color32::from_rgb(110, 116, 128), accent: Color32::from_rgb(38, 120, 218), accent_soft: Color32::from_rgb(214, 230, 250), - sketch_accent: Color32::from_rgb(46, 150, 90), + sketch_accent: icon_primary(crate::icon_svg::SKETCH_PRIMARY[0]), error: Color32::from_rgb(214, 54, 54), warning: Color32::from_rgb(222, 150, 30), icon: Color32::from_rgb(70, 78, 92), @@ -39,7 +43,7 @@ impl Tokens { axis_y: Color32::from_rgb(0, 255, 0), axis_z: Color32::from_rgb(2, 2, 248), construction_plane: Color32::from_rgba_unmultiplied_const(249, 184, 134, 70), - construction_geom: Color32::from_rgb(232, 140, 48), + construction_geom: icon_primary(crate::icon_svg::CONSTRUCTION_PRIMARY[0]), body: Color32::from_rgb(176, 186, 198), body_edge: Color32::from_rgb(40, 44, 52), manipulator: Color32::from_rgb(40, 110, 220), @@ -71,7 +75,7 @@ impl Tokens { profile_fill: Color32::from_rgba_unmultiplied_const(255, 196, 120, 70), glyph_bg: Color32::WHITE, glyph_edge: Color32::from_rgb(190, 110, 100), - glyph_text: Color32::from_rgb(150, 40, 35), + glyph_text: icon_primary(crate::icon_svg::CONSTRAINT_PRIMARY[0]), overlay: Color32::from_rgba_unmultiplied_const(255, 255, 255, 235), rubber_band: Color32::from_rgb(30, 120, 230), app_bar_text: Color32::from_rgb(230, 234, 242), @@ -132,7 +136,8 @@ impl Tokens { t.text_dim = Color32::from_rgb(148, 155, 168); t.accent = Color32::from_rgb(58, 136, 238); t.accent_soft = Color32::from_rgb(42, 70, 110); - t.sketch_accent = Color32::from_rgb(110, 200, 140); + t.sketch_accent = icon_primary(crate::icon_svg::SKETCH_PRIMARY[1]); + t.construction_geom = icon_primary(crate::icon_svg::CONSTRUCTION_PRIMARY[1]); t.error = Color32::from_rgb(240, 100, 96); t.warning = Color32::from_rgb(236, 170, 70); t.icon = Color32::from_rgb(208, 214, 226); @@ -162,7 +167,7 @@ impl Tokens { t.subheading_bg = Color32::from_rgb(48, 55, 68); t.glyph_bg = Color32::from_rgb(44, 50, 60); t.glyph_edge = Color32::from_rgb(200, 130, 120); - t.glyph_text = Color32::from_rgb(240, 165, 155); + t.glyph_text = icon_primary(crate::icon_svg::CONSTRAINT_PRIMARY[1]); t.overlay = Color32::from_rgba_unmultiplied_const(34, 38, 46, 240); t.rubber_band = Color32::from_rgb(92, 160, 255); t.timeline_item = Color32::from_rgb(52, 58, 70); @@ -196,18 +201,19 @@ impl Tokens { t.icons.axes[0] = t.triad[0]; t.icons.axes[1] = t.triad[1]; t.icons.access = [good, Color32::from_rgb(217, 102, 13)]; + t.construction_geom = icon_primary(crate::icon_svg::CONSTRUCTION_RED_GREEN[dark as usize]); t.icons.draft = [good, Color32::from_rgb(242, 230, 153)]; if dark { t.bend_up = Color32::from_rgb(110, 165, 255); t.bend_down = Color32::from_rgb(240, 180, 70); - t.sketch_accent = Color32::from_rgb(150, 225, 195); + t.sketch_accent = icon_primary(crate::icon_svg::SKETCH_RED_GREEN[1]); } else { t.sketch_projected = Color32::from_rgb(205, 120, 215); t.sketch_locked = Color32::from_rgb(60, 150, 120); t.timeline_item_error = Color32::from_rgb(232, 196, 170); t.bend_up = Color32::from_rgb(30, 100, 210); t.bend_down = Color32::from_rgb(200, 120, 0); - t.sketch_accent = Color32::from_rgb(60, 150, 105); + t.sketch_accent = icon_primary(crate::icon_svg::SKETCH_RED_GREEN[0]); } t } @@ -217,10 +223,11 @@ impl Tokens { let mut t = self; t.analysis.hatch = true; t.preview_sketch = Color32::from_rgb(125, 180, 105); + t.glyph_text = icon_primary(crate::icon_svg::CONSTRAINT_BLUE_YELLOW[dark as usize]); + t.construction_geom = icon_primary(crate::icon_svg::CONSTRUCTION_BLUE_YELLOW[dark as usize]); if dark { t.sketch_locked = Color32::from_rgb(140, 225, 110); t.box_crossing = Color32::from_rgb(240, 110, 170); - t.glyph_text = Color32::from_rgb(240, 180, 165); } else { t.sketch_locked = Color32::from_rgb(20, 165, 70); t.box_crossing = Color32::from_rgb(200, 60, 120); diff --git a/crates/engine/src/cmd/browser.rs b/crates/engine/src/cmd/browser.rs index 607cd25d..b8f93bf2 100644 --- a/crates/engine/src/cmd/browser.rs +++ b/crates/engine/src/cmd/browser.rs @@ -13,16 +13,16 @@ use crate::params::{bad, bool_, str_}; use crate::{Result, Session}; pub static COMMANDS: &[CommandSpec] = &[ - CommandSpec::new("body.rename", "Rename Body", rename_body).params("body: its name, name: the new name (features that use the body follow)"), + CommandSpec::new("body.rename", "Rename Body", rename_body).icon("rename").params("body: its name, name: the new name (features that use the body follow)"), CommandSpec::new("sketch.redefine", "Redefine Sketch Plane", redefine_sketch) .icon("plane") .params("sketch: id or name; plane: as sketch.create; author_path?: exact owner occurrence ids; explicitly rebinds or freezes the plane while preserving geometry and dimension names"), CommandSpec::new("browser.group", "New Group", group) .icon("folder") .params("folder: bodies|sketches|construction; component?: id (default 0); name?; items?: [keys] (they leave other groups)"), - CommandSpec::new("browser.ungroup", "Ungroup", ungroup).params("group: id (its items go back to the folder)"), - CommandSpec::new("browser.rename_group", "Rename Group", rename_group).params("group: id, name"), - CommandSpec::new("browser.move", "Move Browser Items", move_items) + CommandSpec::new("browser.ungroup", "Ungroup", ungroup).icon("folder").params("group: id (its items go back to the folder)"), + CommandSpec::new("browser.rename_group", "Rename Group", rename_group).icon("rename").params("group: id, name"), + CommandSpec::new("browser.move", "Move Browser Items", move_items).icon("reorder") .params("folder, component?, items: [keys]; group?: id (into it; omit to take them out of groups); before?: key (position in the group)"), CommandSpec::new("document.units", "Change Design Units", document_units) .icon("settings") @@ -30,13 +30,13 @@ pub static COMMANDS: &[CommandSpec] = &[ CommandSpec::new("view.save", "New Named View", view_save) .icon("camera") .params("name?; camera: {target: [x,y,z], yaw, pitch, distance, fov?} (replaces a view of the same name)"), - CommandSpec::new("view.rename", "Rename Named View", view_rename).params("view: name, name"), - CommandSpec::new("view.delete", "Delete Named View", view_delete).params("view: name"), - CommandSpec::new("view.list", "List Named Views", view_list).noundo().doc_only(), + CommandSpec::new("view.rename", "Rename Named View", view_rename).icon("rename").params("view: name, name"), + CommandSpec::new("view.delete", "Delete Named View", view_delete).icon("delete").params("view: name"), + CommandSpec::new("view.list", "List Named Views", view_list).icon("list").noundo().doc_only(), CommandSpec::new("view.apply", "Restore Named View", view_apply).icon("camera").noundo().params( "view?: front|back|top|bottom|left|right|iso|home (frames the model) | a named view's name; or camera?: {target: [x,y,z], yaw, pitch, distance, fov?} (radians, mm) → camera. The viewport (and the MCP screenshot) moves there. View state: not saved, not an undo step", ), - CommandSpec::new("browser.order", "Order Browser Items", order).params("folder, component?, order: [keys] (the folder's items in display order)"), + CommandSpec::new("browser.order", "Order Browser Items", order).icon("reorder").params("folder, component?, order: [keys] (the folder's items in display order)"), CommandSpec::new("browser.visibility", "Show/Hide", visibility).icon("eye").noundo().params( "visible: bool; items?: [sketch id or name | body name | construction name | joint name | O|X|Y|Z|XY|XZ|YZ]; \ or joints?: [joint id or name] (their glyphs; they still constrain and move parts); \ diff --git a/crates/engine/src/cmd/clipboard.rs b/crates/engine/src/cmd/clipboard.rs index eff00a90..cf924e89 100644 --- a/crates/engine/src/cmd/clipboard.rs +++ b/crates/engine/src/cmd/clipboard.rs @@ -12,8 +12,9 @@ use crate::params::{bad, string_list, vec3}; use crate::{Result, Session}; pub static COMMANDS: &[CommandSpec] = &[ - CommandSpec::new("timeline.copy", "Copy Features", copy).noundo().params("features: [ids or names] (in timeline order on paste)"), + CommandSpec::new("timeline.copy", "Copy Features", copy).icon("copy").noundo().params("features: [ids or names] (in timeline order on paste)"), CommandSpec::new("timeline.paste", "Paste Features", paste) + .icon("paste") .params("translate?: [x,y,z] (moves the copies' geometry); the copies go at the marker"), ]; diff --git a/crates/engine/src/cmd/compare.rs b/crates/engine/src/cmd/compare.rs index be888746..439198c1 100644 --- a/crates/engine/src/cmd/compare.rs +++ b/crates/engine/src/cmd/compare.rs @@ -12,7 +12,7 @@ use super::CommandSpec; use crate::params::bad; use crate::{Result, Sel, Session}; -pub static COMMANDS: &[CommandSpec] = &[CommandSpec::new("inspect.compare", "Compare Bodies", compare_bodies).params( +pub static COMMANDS: &[CommandSpec] = &[CommandSpec::new("inspect.compare", "Compare Bodies", compare_bodies).icon("compare").params( "a, b: body names (default: the two selected bodies, or the design's only two), in world placement; tolerance?: mm (0.001 to 10, default 0.01): \ surfaces closer than this agree; spacing?: mm (default 0.5): the largest piece of a face sampled as one point, coarser for a body that would need \ more than max_points? (default 8 million, up to 50 million) samples; max_regions? (default 50); color?: true | {only_in_a?, only_in_b?: \"#rrggbb\"} \ diff --git a/crates/engine/src/cmd/component.rs b/crates/engine/src/cmd/component.rs index 80a4c5ff..76733d61 100644 --- a/crates/engine/src/cmd/component.rs +++ b/crates/engine/src/cmd/component.rs @@ -29,8 +29,8 @@ pub static COMMANDS: &[CommandSpec] = &[ CommandSpec::new("component.from_bodies", "Create Components from Bodies", from_bodies) .icon("component") .params("bodies: [names] → one new component per body, named after it"), - CommandSpec::new("component.activate", "Activate Component", activate).noundo().params("component: id or name (0 or \"root\" for the design); occurrence?, path?: exact placement used to author shared geometry"), - CommandSpec::new("component.list", "List Components", list).noundo().params( + CommandSpec::new("component.activate", "Activate Component", activate).icon("activate").noundo().params("component: id or name (0 or \"root\" for the design); occurrence?, path?: exact placement used to author shared geometry"), + CommandSpec::new("component.list", "List Components", list).icon("list").noundo().params( "compact?: bool (counts of bodies, features, sketches and construction planes instead of their names; occurrences leave out \ identity transforms; children as occurrence ids); bodies?: bool (with compact: body names, not their count)", ), @@ -40,20 +40,20 @@ pub static COMMANDS: &[CommandSpec] = &[ CommandSpec::new("occurrence.properties", "Occurrence Physical Properties", properties::occurrence) .icon("measure").noundo() .params("occurrence: id or name; path?: [occurrence ids] (required if nested under multiple instances); descendants?: bool (default true). Physical properties of this placement in world coordinates, including pending moves"), - CommandSpec::new("component.rename", "Rename Component", rename).params("component, name"), - CommandSpec::new("component.delete", "Delete Component", delete).params("component: deletes it, its subcomponents, occurrences and features"), + CommandSpec::new("component.rename", "Rename Component", rename).icon("rename").params("component, name"), + CommandSpec::new("component.delete", "Delete Component", delete).icon("delete").params("component: deletes it, its subcomponents, occurrences and features"), CommandSpec::new("component.unlock", "Unlock Contents", unlock).icon("rigid_group").params( "component: removes the rigid joints that hold its contents together (an imported assembly's rigid group), so its parts \ move and take joints one by one → joints (the ids removed)", ), - CommandSpec::new("component.move_bodies", "Move Bodies to Component", move_bodies).params("bodies: [names], component"), - CommandSpec::new("component.move_sketches", "Move Sketches to Component", move_sketches).params("sketches: [ids or names], component"), - CommandSpec::new("occurrence.copy", "Paste (Instance)", copy_occurrence) + CommandSpec::new("component.move_bodies", "Move Bodies to Component", move_bodies).icon("move").params("bodies: [names], component"), + CommandSpec::new("component.move_sketches", "Move Sketches to Component", move_sketches).icon("move").params("sketches: [ids or names], component"), + CommandSpec::new("occurrence.copy", "Paste (Instance)", copy_occurrence).icon("copy") .params("component (or occurrence): what to place again; path?: validates selected occurrence; parent?: component (default: the same parent); translate?: [x,y,z]. Copies the selected effective transform, including pending moves"), - CommandSpec::new("component.paste_new", "Paste New", paste_new).params("component: independently copy its nested definition graph and history, materials and styles; occurrence?, path?: selected source placement; translate?: [x,y,z]. Reassigned external bodies become base geometry; retained external links and omitted boundary joints are reported"), - CommandSpec::new("occurrence.move", "Move Occurrence", move_occurrence) + CommandSpec::new("component.paste_new", "Paste New", paste_new).icon("paste").params("component: independently copy its nested definition graph and history, materials and styles; occurrence?, path?: selected source placement; translate?: [x,y,z]. Reassigned external bodies become base geometry; retained external links and omitted boundary joints are reported"), + CommandSpec::new("occurrence.move", "Move Occurrence", move_occurrence).icon("move") .params("occurrence; path?: exact placement; space?: local|world (default local); translate?: [x,y,z]; axis?: [x,y,z], angle?: radians or \"30 deg\", origin?: [x,y,z]; capture?: bool (default true; false keeps it pending until Capture Position). Nested occurrence definitions remain shared"), - CommandSpec::new("occurrence.ground", "Ground", ground).params("occurrence; grounded?: bool (default toggles)"), + CommandSpec::new("occurrence.ground", "Ground", ground).icon("ground").params("occurrence; grounded?: bool (default toggles)"), CommandSpec::new("component.capture_position", "Capture Position", capture).at("SOLID", "POSITION").icon("capture").params("keeps pending occurrence moves"), CommandSpec::new("component.revert_position", "Revert Position", revert).at("SOLID", "POSITION").icon("revert").noundo().params("occurrences?: [ids] (default all); drops selected pending occurrence moves"), ]; diff --git a/crates/engine/src/cmd/component_link.rs b/crates/engine/src/cmd/component_link.rs index 8dd29104..c5e3d6ba 100644 --- a/crates/engine/src/cmd/component_link.rs +++ b/crates/engine/src/cmd/component_link.rs @@ -20,12 +20,14 @@ pub static COMMANDS: &[CommandSpec] = &[ "path: a .solidcraft design → a new component linked to it, holding its solid bodies as placed in that design; \ name? (default: the design's name); parent?: component id or name (default: the active one); translate?: [x,y,z]", ), - CommandSpec::new("component.update_links", "Update Links", update_links).params( + CommandSpec::new("component.update_links", "Update Links", update_links).icon("update").params( "component?: id or name (default: every linked component) — reads the linked designs again and replaces their bodies where they changed", ), CommandSpec::new("component.break_link", "Break Link", break_link) + .icon("break") .params("component: id or name of a linked component — keeps its bodies as they are and forgets the design file"), CommandSpec::new("component.links", "List Links", list_links) + .icon("chain") .noundo() .params("→ linked components: component, name, path, file (resolved), found"), ]; diff --git a/crates/engine/src/cmd/config.rs b/crates/engine/src/cmd/config.rs index 49835ff7..ca170e5a 100644 --- a/crates/engine/src/cmd/config.rs +++ b/crates/engine/src/cmd/config.rs @@ -14,17 +14,17 @@ pub static COMMANDS: &[CommandSpec] = &[ ), CommandSpec::new("config.table", "Display Configuration Table", show) .at("SOLID", "CONFIGURE") - .icon("params") + .icon("configuration_table") .noundo() .params("→ columns (param: or suppress:), rows, the active row and whether the design still matches it"), - CommandSpec::new("config.column", "Configuration Column", column).params("param? | feature? | material? | appearance? (target name); delete?: bool"), - CommandSpec::new("config.row", "Configuration Row", row).params( + CommandSpec::new("config.column", "Configuration Column", column).icon("configuration_table").params("param? | feature? | material? | appearance? (target name); delete?: bool"), + CommandSpec::new("config.row", "Configuration Row", row).icon("configuration_table").params( "name; from? (row to copy; default current values); values?: {column key: expression, suppression, material name, appearance name or Look object}; rename?; delete?: bool", ), - CommandSpec::new("config.activate", "Activate Configuration", activate).params("row: name — sets its parameter values and suppressions"), - CommandSpec::new("config.preview", "Preview Configuration", preview).noundo().params("row: name — validates a rebuild without changing the design or history"), - CommandSpec::new("config.rule", "Configuration Rule", rule).params("rule?: {id,name,enabled,condition,effect}; id? + delete?; id? + index? (ordered move)"), - CommandSpec::new("config.rules", "Configuration Rules", show).noundo().params("ordered typed rules, rows and active state"), + CommandSpec::new("config.activate", "Activate Configuration", activate).icon("activate").params("row: name — sets its parameter values and suppressions"), + CommandSpec::new("config.preview", "Preview Configuration", preview).icon("eye").noundo().params("row: name — validates a rebuild without changing the design or history"), + CommandSpec::new("config.rule", "Configuration Rule", rule).icon("configuration_table").params("rule?: {id,name,enabled,condition,effect}; id? + delete?; id? + index? (ordered move)"), + CommandSpec::new("config.rules", "Configuration Rules", show).icon("list").noundo().params("ordered typed rules, rows and active state"), ]; fn column_of(s: &Session, p: &Value, cmd: &str) -> Result { diff --git a/crates/engine/src/cmd/construct.rs b/crates/engine/src/cmd/construct.rs index 460cbbdd..4c42c43f 100644 --- a/crates/engine/src/cmd/construct.rs +++ b/crates/engine/src/cmd/construct.rs @@ -21,69 +21,69 @@ const REF: &str = pub static COMMANDS: &[CommandSpec] = &[ CommandSpec::new("construct.axis.through_cylinder", "Axis Through Cylinder/Cone/Torus", axis_face) .at("SOLID", "CONSTRUCT") - .icon("axis") + .icon("axis_through_cylinder") .params("face: ref (a cylinder, cone or torus face, or a circular edge); name?"), CommandSpec::new("construct.axis.normal_to_face", "Axis Perpendicular To Face", axis_normal) .at("SOLID", "CONSTRUCT") - .icon("axis") + .icon("axis_normal_to_face") .params("face: ref; point: ref (the axis goes through it, square to the face); name?"), CommandSpec::new("construct.axis.two_planes", "Axis Through Two Planes", axis_planes) .at("SOLID", "CONSTRUCT") - .icon("axis") + .icon("axis_two_planes") .params("a, b: plane or planar face refs; name?"), CommandSpec::new("construct.axis.two_points", "Axis Through Two Points", axis_points) .at("SOLID", "CONSTRUCT") - .icon("axis") + .icon("axis_two_points") .params("a, b: point refs (vertices, sketch points, points); name?"), CommandSpec::new("construct.axis.edge", "Axis Through Edge", axis_edge) .at("SOLID", "CONSTRUCT") - .icon("axis") + .icon("axis_edge") .params("edge: ref (a straight edge or sketch line); name?"), CommandSpec::new("construct.point.vertex", "Point At Vertex", point_at) .at("SOLID", "CONSTRUCT") - .icon("point") + .icon("point_vertex") .params("point: ref (vertex, sketch point, point); name?"), CommandSpec::new("construct.point.two_edges", "Point Through Two Edges", point_edges) .at("SOLID", "CONSTRUCT") - .icon("point") + .icon("point_two_edges") .params("a, b: edge or line refs (where they meet, or come closest); name?"), CommandSpec::new("construct.point.three_planes", "Point Through Three Planes", point_planes) .at("SOLID", "CONSTRUCT") - .icon("point") + .icon("point_three_planes") .params("a, b, c: plane or planar face refs; name?"), CommandSpec::new("construct.point.center", "Point At Center Of Circle/Sphere/Torus", point_center) .at("SOLID", "CONSTRUCT") - .icon("point") + .icon("point_center") .params("of: ref (circular edge, sketch circle or arc, sphere or torus face); name?"), CommandSpec::new("construct.point.edge_plane", "Point At Edge And Plane", point_edge_plane) .at("SOLID", "CONSTRUCT") - .icon("point") + .icon("point_edge_plane") .params("edge: edge or line ref; plane: plane or planar face ref; name?"), CommandSpec::new("construct.point.along_path", "Point Along Path", point_along) .at("SOLID", "CONSTRUCT") - .icon("point") + .icon("point_along_path") .params("path: edge or sketch curve ref; t: expr, 0…1 along it (default 0.5); name?"), CommandSpec::new("construct.plane.midplane", "Midplane", plane_mid) .at("SOLID", "CONSTRUCT") - .icon("plane") + .icon("plane_midplane") .params("a, b: plane or planar face refs (halfway between; bisecting when they meet at an angle); name?"), CommandSpec::new("construct.plane.two_edges", "Plane Through Two Edges", plane_edges) .at("SOLID", "CONSTRUCT") - .icon("plane") + .icon("plane_two_edges") .params("a, b: straight edge or line refs in one plane; name?"), CommandSpec::new("construct.plane.three_points", "Plane Through Three Points", plane_points) .at("SOLID", "CONSTRUCT") - .icon("plane") + .icon("plane_three_points") .params("a, b, c: point refs; name?"), CommandSpec::new("construct.plane.tangent", "Tangent Plane", plane_tangent) .at("SOLID", "CONSTRUCT") - .icon("plane") + .icon("plane_tangent") .params("face: ref (cylinder, cone, sphere or torus); at?: [x,y,z] where it touches (default: the face pick); name?"), CommandSpec::new("construct.plane.along_path", "Plane Along Path", plane_along) .at("SOLID", "CONSTRUCT") - .icon("plane") + .icon("plane_along_path") .params("path: edge or sketch curve ref; t: expr, 0…1 along it (default 0.5); name? The plane is square to the path there"), - CommandSpec::new("construct.plane.perpendicular", "Perpendicular Plane", plane_perpendicular).at("SOLID", "CONSTRUCT").icon("plane").params( + CommandSpec::new("construct.plane.perpendicular", "Perpendicular Plane", plane_perpendicular).at("SOLID", "CONSTRUCT").icon("plane_perpendicular").params( "plane: plane or planar face ref; line: straight edge, sketch line or axis ref; name? The plane holds the line and is square to the plane", ), ]; diff --git a/crates/engine/src/cmd/diagnostics.rs b/crates/engine/src/cmd/diagnostics.rs index 716b3bd8..25a4756d 100644 --- a/crates/engine/src/cmd/diagnostics.rs +++ b/crates/engine/src/cmd/diagnostics.rs @@ -8,20 +8,21 @@ use crate::params::{bad, str_}; use crate::{EngineError, Result, Session, crash}; pub static COMMANDS: &[CommandSpec] = &[ - CommandSpec::new("diagnostics.crash_reports", "Crash Reports", list).noundo().params( + CommandSpec::new("diagnostics.crash_reports", "Crash Reports", list).icon("bug_report").noundo().params( "limit?: number (default 20). Lists this computer's crash reports, newest first: {dir, reports: [{id, kind, time, message, \ command, count, version, path, replayable}]}. kind: command_panic | panic | kernel_panic_recovered | unclean_exit. \ Read one with diagnostics.crash_report; reproduce it with `solidcraft-cli crashes replay `", ), - CommandSpec::new("diagnostics.crash_report", "Crash Report", read).noundo().params( + CommandSpec::new("diagnostics.crash_report", "Crash Report", read).icon("bug_report").noundo().params( "id: report id. The whole report: message, location, backtrace, app version and commit, system, the command that \ crashed and the recent ones, notes, and design_path / replay_path when the crash can be replayed \ (`solidcraft-cli run --in `)", ), CommandSpec::new("diagnostics.crash_reports.clear", "Delete Crash Reports", clear) + .icon("delete") .noundo() .params("id?: one report (default: all). Returns {deleted}"), - CommandSpec::new("diagnostics.crash_test", "Crash Test", crash_test).noundo().enabled(crash_test_enabled).params( + CommandSpec::new("diagnostics.crash_test", "Crash Test", crash_test).icon("bug_report").noundo().enabled(crash_test_enabled).params( "mode: panic | kernel | abort. Checks that crash reports work: panic = an internal error in this command (caught; \ the design is unchanged), kernel = one inside the geometry kernel (caught), abort = the app ends at once (reported \ at the next start). Only when the SOLIDCRAFT_CRASH_TEST environment variable is set", diff --git a/crates/engine/src/cmd/edit.rs b/crates/engine/src/cmd/edit.rs index 9ac6a07c..58e33c55 100644 --- a/crates/engine/src/cmd/edit.rs +++ b/crates/engine/src/cmd/edit.rs @@ -12,11 +12,11 @@ use crate::{EngineError, Result, Sel, Session, Snapshot}; pub static COMMANDS: &[CommandSpec] = &[ CommandSpec::new("edit.undo", "Undo", undo).icon("undo").key("Ctrl+Z").noundo().doc_only(), CommandSpec::new("edit.redo", "Redo", redo).icon("redo").key("Ctrl+Y").noundo().doc_only(), - CommandSpec::new("edit.checkpoint", "Checkpoint", checkpoint) + CommandSpec::new("edit.checkpoint", "Checkpoint", checkpoint).icon("checkpoint") .noundo() .doc_only() .params("→ {checkpoint: id}: remember the design, active sketch and undo history (restore with edit.restore_checkpoint)"), - CommandSpec::new("edit.restore_checkpoint", "Restore Checkpoint", restore_checkpoint) + CommandSpec::new("edit.restore_checkpoint", "Restore Checkpoint", restore_checkpoint).icon("restore") .noundo() .doc_only() .params("checkpoint: id; forget?: bool (true = drop it without going back). Goes back to it, undo history included"), @@ -26,48 +26,48 @@ pub static COMMANDS: &[CommandSpec] = &[ .icon("delete") .key("Delete") .params("features: [id or name] (a sketch takes its dependent features with it) → deleted: [ids]; failing?: [{feature, error}] (later features that worked and now fail)"), - CommandSpec::new("timeline.rename", "Rename", rename).params("feature: id|name, name (references to it follow)"), + CommandSpec::new("timeline.rename", "Rename", rename).icon("rename").params("feature: id|name, name (references to it follow)"), CommandSpec::new("timeline.group", "New Feature Folder", group) .icon("folder") .doc_only() .params("name?; features?: [id|name]; parent?: folder id (features leave any other timeline folder)"), - CommandSpec::new("timeline.ungroup", "Remove Feature Folders", ungroup) + CommandSpec::new("timeline.ungroup", "Remove Feature Folders", ungroup).icon("folder") .doc_only() .params("group: id | groups: [ids] (features remain in the timeline)"), - CommandSpec::new("timeline.rename_group", "Rename Feature Folder", rename_group).doc_only().params("group: id, name"), - CommandSpec::new("timeline.move_to_group", "Move Features to Folder", move_to_group) + CommandSpec::new("timeline.rename_group", "Rename Feature Folder", rename_group).icon("rename").doc_only().params("group: id, name"), + CommandSpec::new("timeline.move_to_group", "Move Features to Folder", move_to_group).icon("folder") .doc_only() .params("features?: [id|name], groups?: [ids], group?: id (omit or null to move to the timeline root)"), - CommandSpec::new("timeline.move_many", "Move Timeline Items", move_many).params( + CommandSpec::new("timeline.move_many", "Move Timeline Items", move_many).icon("reorder").params( "features?: [id|name], groups?: [folder ids], group?: folder id, position: insertion slot; atomically changes folder membership and moves all selected contents as one block", ), - CommandSpec::new("timeline.rollback", "Move Timeline Marker", rollback).params("position: number of features to keep active (omit = end)"), - CommandSpec::new("timeline.roll_to", "Roll History Marker", rollback) + CommandSpec::new("timeline.rollback", "Move Timeline Marker", rollback).icon("rollback").params("position: number of features to keep active (omit = end)"), + CommandSpec::new("timeline.roll_to", "Roll History Marker", rollback).icon("rollback") .params("position: number of features to keep active (omit = end) | feature: id|name (the marker goes right after it)"), - CommandSpec::new("timeline.reorder", "Reorder Feature", reorder) + CommandSpec::new("timeline.reorder", "Reorder Feature", reorder).icon("reorder") .params("feature: id|name, position: its new index; refused when a feature that worked would fail"), - CommandSpec::new("timeline.reorder_many", "Reorder Features", reorder_many).params( + CommandSpec::new("timeline.reorder_many", "Reorder Features", reorder_many).icon("reorder").params( "features: [id|name], position: insertion slot in the current history; selected features keep their relative order; refused when a feature that worked would fail", ), - CommandSpec::new("timeline.redefine", "Edit Feature", redefine) + CommandSpec::new("timeline.redefine", "Edit Feature", redefine).icon("edit") .params("feature: id|name, command: the feature's command id, params: its parameters (the feature is rebuilt in place)"), - CommandSpec::new("timeline.dependents", "Feature Dependents", dependents) + CommandSpec::new("timeline.dependents", "Feature Dependents", dependents).icon("graph") .noundo() .params("feature: id|name → features deleted with it, and features that would fail without it"), - CommandSpec::new("timeline.suppress", "Suppress Feature", suppress).params("feature: id|name, suppressed?: bool (default toggles)"), - CommandSpec::new("timeline.edit", "Edit Feature", edit_feature).params( + CommandSpec::new("timeline.suppress", "Suppress Feature", suppress).icon("suppress").params("feature: id|name, suppressed?: bool (default toggles)"), + CommandSpec::new("timeline.edit", "Edit Feature", edit_feature).icon("edit").params( "feature: id|name, set: {fields to change, merged into the feature's definition as timeline.inspect returns it, \ e.g. {\"extent\": {\"distance\": \"30\"}} (extrude), {\"angle\": \"90 deg\"} (revolve); \ expressions are text} → recomputed: features rebuilt, restored: features whose earlier result for the same inputs was reused", ), - CommandSpec::new("timeline.inspect", "Inspect Feature", inspect_feature).noundo().params( + CommandSpec::new("timeline.inspect", "Inspect Feature", inspect_feature).icon("inspect").noundo().params( "feature: id|name → type, definition (the fields timeline.edit's set changes, with their current values), \ inputs: [{label, param, expression, unit}] (named inputs parameters.change can also set), error, warning", ), - CommandSpec::new("select.set", "Select", select_set) + CommandSpec::new("select.set", "Select", select_set).icon("select") .noundo() .params("items: [{type: body|edge|face|feature|sketch_curve|sketch_point|profile, …}], add?: bool"), - CommandSpec::new("select.clear", "Clear Selection", select_clear).noundo(), + CommandSpec::new("select.clear", "Clear Selection", select_clear).icon("select").noundo(), ]; fn undo(s: &mut Session, _p: &Value) -> Result { diff --git a/crates/engine/src/cmd/features.rs b/crates/engine/src/cmd/features.rs index 025b2c35..15763a5d 100644 --- a/crates/engine/src/cmd/features.rs +++ b/crates/engine/src/cmd/features.rs @@ -48,7 +48,7 @@ pub static COMMANDS: &[CommandSpec] = &[ .params("features: [names] | bodies: [names] | components: [names]; axis: X|Y|Z | {origin, dir}; count; angle? (default 360 deg; partial arcs include both ends); alignment?: left|center|right (default right; original stays fixed); reversed?: bool (Flip; selects the extra side for even centered counts)"), CommandSpec::new("solid.pattern.path", "Pattern on Path", pattern_path) .at("SOLID", "CREATE") - .icon("pattern_rect") + .icon("pattern_path") .params("features: [names] | bodies: [names]; path_sketch: sketch; path: [curve ids in order]; count; spacing (between instances) | distance (first to last); orientation?: identical|path; flip?: bool"), CommandSpec::new("solid.mirror", "Mirror", mirror).at("SOLID", "CREATE").icon("mirror").params("features: [names] | bodies: [names] (combine?: join with the original); plane: XY|XZ|YZ|construction plane name or id|{origin, normal}|{origin, x_dir, y_dir} (origin coordinates may be length expressions; the mirror follows them, or a construction plane)"), CommandSpec::new("solid.hole", "Hole", hole) @@ -62,7 +62,7 @@ pub static COMMANDS: &[CommandSpec] = &[ .params("path_sketch: sketch; path: [curve ids in order]; diameter: expr; wall?: expr (hollow); operation?, targets?, name?, body_name?"), CommandSpec::new("solid.bounding_solid", "Bounding Solid", bounding_solid) .at("SOLID", "CREATE") - .icon("box") + .icon("bounding_solid") .params("bodies?: [names] (default: all); margin?: expr; name?, body_name?"), CommandSpec::new("solid.scale", "Scale", scale) .at("SOLID", "MODIFY") @@ -70,7 +70,7 @@ pub static COMMANDS: &[CommandSpec] = &[ .params("bodies: [names]; factor: expr | factors: [x, y, z] exprs; origin?: [x,y,z]"), CommandSpec::new("face.delete", "Delete Face", delete_face) .at("SOLID", "MODIFY") - .icon("delete") + .icon("delete_face") .params("faces: [[x,y,z] points on the faces, or face names from model.faces]; body?; heal?: bool (default true: the neighbouring planes close the gap, as when a rounded edge, hole or boss is taken off; false: the gap stays open and the faces left are a surface body)"), CommandSpec::new("solid.offset_face", "Offset Face", offset_face) .at("SOLID", "MODIFY") @@ -94,17 +94,17 @@ pub static COMMANDS: &[CommandSpec] = &[ .params("base: XY|XZ|YZ|plane name, or a planar face {face: [x,y,z], body?, index?, path?}; offset: expr; name?"), CommandSpec::new("construct.plane.at_angle", "Plane at Angle", plane_angle) .at("SOLID", "CONSTRUCT") - .icon("plane") + .icon("plane_angle") .params("base: XY|XZ|YZ|plane name; axis: X|Y|Z|{origin, dir}; angle: expr; name?"), CommandSpec::new("solid.split_face", "Split Face", split_face) .at("SOLID", "MODIFY") - .icon("split") + .icon("split_face") .params( "faces: [[x,y,z] points on the faces to split, or face names from model.faces]; body?; and the splitting tool: plane: XY|XZ|YZ|plane name|{origin, normal} — or tool: {body, point} (a face of a body, its surface extended) — or sketch: id or name (its curves, along the sketch's normal)", ), CommandSpec::new("solid.split_body", "Split Body", split_body) .at("SOLID", "MODIFY") - .icon("split") + .icon("split_body") .params("body: name; plane: XY|XZ|YZ|plane name|{origin, normal} — or tool: {body, point} (a face of a body, its surface extended) — or sketch: id, line: curve id (a straight sketch line, along the sketch's normal)"), CommandSpec::new("solid.press_pull", "Press Pull", press_pull) .at("SOLID", "MODIFY") diff --git a/crates/engine/src/cmd/features_more.rs b/crates/engine/src/cmd/features_more.rs index 055b56c9..7a255925 100644 --- a/crates/engine/src/cmd/features_more.rs +++ b/crates/engine/src/cmd/features_more.rs @@ -28,7 +28,7 @@ pub static COMMANDS: &[CommandSpec] = &[ .params("sketch, curves: [open curve ids, in order]; thickness (across the sketch plane); depth? (default: to the next face); flip?: bool (fill to the other side)"), CommandSpec::new("solid.web", "Web", web) .at("SOLID", "CREATE") - .icon("rib") + .icon("web") .params("sketch, curves: [open curve ids] (one wall each); thickness; depth? (default: to the next face); flip?: bool"), CommandSpec::new("solid.emboss", "Emboss", emboss) .at("SOLID", "CREATE") @@ -36,11 +36,11 @@ pub static COMMANDS: &[CommandSpec] = &[ .params("sketch, profiles?; depth; mode?: emboss|deboss; targets?; face?: world point [x,y,z] selecting a planar or complete exterior cylindrical wall (inferred when exactly one supported tangent cylinder matches the selected profiles)"), CommandSpec::new("solid.replace_face", "Replace Face", replace_face) .at("SOLID", "MODIFY") - .icon("offset_face") + .icon("replace_face") .params("faces: [[x,y,z] points on planar faces, or face names from model.faces]; target: plane (XY|XZ|YZ|construction plane|{origin, normal}) | target_face: [x,y,z] on a parallel planar face; body?"), CommandSpec::new("solid.align", "Align", align) .at("SOLID", "MODIFY") - .icon("move") + .icon("align") .params("bodies: [names]; from: [x,y,z] | {snap: vertex|edge_mid|face_center|circle_center, at: [x,y,z]} | from_face: [x,y,z]; to: (the same) | to_face: [x,y,z] (faces also turn to meet); flip?: bool"), CommandSpec::new("solid.remove", "Remove", remove).at("SOLID", "MODIFY").icon("delete").params("bodies: [names] (removed from here on in the timeline)"), ]; diff --git a/crates/engine/src/cmd/file.rs b/crates/engine/src/cmd/file.rs index 35430742..cc137984 100644 --- a/crates/engine/src/cmd/file.rs +++ b/crates/engine/src/cmd/file.rs @@ -38,23 +38,23 @@ pub static COMMANDS: &[CommandSpec] = &[ .icon("save") .noundo() .params("path; format?: container|json (default: json for .json, otherwise container); compression?: fast|compact (default: fast); cache?: bool (warm native geometry, default: false); preview?: bool (saved display meshes, default: false)"), - CommandSpec::new("doc.recovery_list", "Recoverable Designs", recovery_list) + CommandSpec::new("doc.recovery_list", "Recoverable Designs", recovery_list).icon("restore") .noundo() .params("dir? (default: the recovery folder) → designs autosaved by apps that crashed or closed with unsaved changes"), - CommandSpec::new("doc.recover", "Recover Design", recover) + CommandSpec::new("doc.recover", "Recover Design", recover).icon("restore") .noundo() .params("id (from doc.recovery_list); dir?; discard?: bool (delete the entry once open) — opens it with its file path, unsaved"), - CommandSpec::new("doc.recovery_discard", "Discard Recovered Design", recovery_discard).noundo().params("id, or all: true; dir?"), + CommandSpec::new("doc.recovery_discard", "Discard Recovered Design", recovery_discard).icon("delete").noundo().params("id, or all: true; dir?"), CommandSpec::new("file.export", "Export", export) .icon("export") .noundo() .params("path; format?: stl|stla|obj|step|3mf|3mf-tagged|urdf|json (default from extension; 3mf-tagged: 3MF whose triangles carry their B-rep face, at full precision, for meshing and analysis tools; docs/formats/3mf-face-tags.md; urdf: the whole assembly as a robot, with STL meshes written next to it; docs/urdf.md); bodies?: [names] (JSON and URDF export the complete design); urdf only: mesh_prefix? (before each mesh file name, e.g. package://robot/meshes/), effort? (default 1000), velocity? (default 10)"), CommandSpec::new("file.save_mesh", "Save As Mesh", save_mesh) .at("MESH", "MODIFY") - .icon("export") + .icon("mesh_export") .noundo() .params("path; format?: stl|3mf|3mf-tagged (default from the extension, else stl; 3mf-tagged: docs/formats/3mf-face-tags.md); bodies?: [names] (default: all); ascii?: bool (STL); slicer?: id, name or program path from file.slicers (open the saved file there)"), - CommandSpec::new("file.slicers", "Installed Slicers", slicers) + CommandSpec::new("file.slicers", "Installed Slicers", slicers).icon("export") .noundo() .params("→ the slicers found on this machine: [{id, name, icon?}]; pass an id or name as file.save_mesh's `slicer`"), ]; diff --git a/crates/engine/src/cmd/inspect.rs b/crates/engine/src/cmd/inspect.rs index 410c6c43..1d215957 100644 --- a/crates/engine/src/cmd/inspect.rs +++ b/crates/engine/src/cmd/inspect.rs @@ -14,14 +14,14 @@ pub static COMMANDS: &[CommandSpec] = &[ .key("I") .noundo() .params("bodies?: [names] (default all); or items: [1-2 selections] → length/area/volume; diameter, radius, circumference or arc_length/angle_deg of circles, arcs and cylinders; slot sizes; min (distance_mm, from, to), max, center_distance (centre to centre, or from an axis) and angle"), - CommandSpec::new("document.inspect", "Inspect Design", inspect).noundo().params("measure?: bool (include body measurements)"), - CommandSpec::new("sketch.inspect", "Inspect Sketch", sketch_inspect).noundo().params("sketch?: id|name (default active)"), - CommandSpec::new("model.edges", "List Edges", model_edges).noundo().params("body: name"), - CommandSpec::new("model.faces", "List Faces", model_faces).noundo().params( + CommandSpec::new("document.inspect", "Inspect Design", inspect).icon("inspect").noundo().params("measure?: bool (include body measurements)"), + CommandSpec::new("sketch.inspect", "Inspect Sketch", sketch_inspect).icon("inspect").noundo().params("sketch?: id|name (default active)"), + CommandSpec::new("model.edges", "List Edges", model_edges).icon("inspect").noundo().params("body: name"), + CommandSpec::new("model.faces", "List Faces", model_faces).icon("inspect").noundo().params( "body: name → per face: index, name (what face.delete, sheet.convert `face`, solid.shell … take in place of a point), exact, area, centroid (not always on the face), \ plane_normal, point (a point on the face that picks it), cylinder? ({origin, axis, radius} of a cylindrical face)", ), - CommandSpec::new("model.threads", "List Threads", model_threads).noundo(), + CommandSpec::new("model.threads", "List Threads", model_threads).icon("inspect").noundo(), CommandSpec::new("material.assign", "Physical Material", physical_material) .at("SOLID", "MODIFY") .icon("material") @@ -31,17 +31,17 @@ pub static COMMANDS: &[CommandSpec] = &[ appearance?: library name (appearance.library) | color: \"#rrggbb\" or [r, g, b] (0–255), opacity? (0–1, default 1); \ pbr?: {metallic:0..1, roughness:0.045..1, dielectric_ior:1..3} (requires color/appearance and opacity 1); omitted/null uses legacy scalar shading. Neither appearance nor color (or color: null): clear, so the face follows its body, the body its component, the component its material", ), - CommandSpec::new("appearance.library", "Appearance Library", appearance_library) + CommandSpec::new("appearance.library", "Appearance Library", appearance_library).icon("library") .noundo() .params("→ the built-in appearances (name, colour, opacity)"), - CommandSpec::new("appearance.list", "List Appearances", appearance_list) + CommandSpec::new("appearance.list", "List Appearances", appearance_list).icon("list") .noundo() .params("body?: name → assigned appearances; with body, its resolved look and its faces' (face index, look)"), - CommandSpec::new("material.list", "List Materials", material_list) + CommandSpec::new("material.list", "List Materials", material_list).icon("list") .noundo() .doc_only() .params("→ the built-in materials, then the design's own (user: true, with their look)"), - CommandSpec::new("engine.commands", "List Commands", commands).noundo(), + CommandSpec::new("engine.commands", "List Commands", commands).icon("list").noundo(), ]; pub fn measure_json(b: &solidcraft_doc::ModelBody) -> Value { diff --git a/crates/engine/src/cmd/joints.rs b/crates/engine/src/cmd/joints.rs index 91b55ed3..6e94d472 100644 --- a/crates/engine/src/cmd/joints.rs +++ b/crates/engine/src/cmd/joints.rs @@ -37,7 +37,7 @@ pub static COMMANDS: &[CommandSpec] = &[ reason: why the joint can move nothing (suppressed, its origins lost, or its chain joined to nothing grounded → unconnected: \ the occurrence named)", ), - CommandSpec::new("joint.reach", "Reach Point", reach).params( + CommandSpec::new("joint.reach", "Reach Point", reach).icon("reach").params( "occurrence: id|name; point: [x,y,z] (where its point goes, world); at?: [x,y,z] (its point, in the world where it is now; default: the centre of its bodies' box); z?: [x,y,z] (the way its axis then points); axis?: [x,y,z] (that axis, in the world now; default: its own z); joints?: [ids or names] (only these move toward the point; the other joints of a closed loop they are in follow); quiet?: bool; continuous?: bool (a frame of a drag: the answer says continuous, and the app's command line doesn't echo it). Moves the values of the joints on its chain from where they are to the nearest pose, within their limits, through motion links, keeping closed loops closed as joint.drive does (contact sets are not checked) → reached; error_mm (and error_deg with z): how close it got; point: where its point is; values: [{joint, name, values}] of the joints that moved, as joint.list gives them; moved; stopped_by_limit: [{joint, value, limit: min|max}]: the limits it rests on short of the point; stopped_by_loop: {joint}: the closed loop that stopped it (its chain alone gets closer); reason: why nothing can move", ), CommandSpec::new("joint.motion_link", "Motion Link", motion_link) @@ -64,14 +64,14 @@ pub static COMMANDS: &[CommandSpec] = &[ CommandSpec::new("joint.cam", "Cam", cam_cmd).at("SOLID", "ASSEMBLE").icon("cam").params( "a: the cam's joint (revolute); b: the follower's joint, a slider (a translating follower) or a revolute (an oscillating one), b driven by a; profile: {sketch: id|name, index?: the profile (default: the largest)} | {edges: [[x,y,z]…] (points on edges of the cam's bodies forming one closed loop, world), occurrence?}: the cam's outline, in a plane square to its axis, read again whenever the design changes; follower?: knife|roller|flat (default knife; flat: a face square to a slider's line); radius?: the roller's radius (mm, an expression or {circle: [x,y,z], occurrence?}: the roller's round face, which is its tip too); tip?: a joint origin where the follower touches: its knife's point or its roller's centre, such as {circle: [x,y,z]} on the roller (needed for an oscillating follower; a translating one's line runs through it, by default along its slider's axis); flip?: bool (ride the cam from the other side than the one the follower is on now); link?: index (replace that motion link) → link, lift: {min, max, rise, unit} (b's travel over a turn of the cam), reverse (the side it rides from), follower_offset (mm: how far a translating follower's line passes from the cam's axis); b keeps where it is", ), - CommandSpec::new("joint.limits", "Joint Limits", limits).params("joint; index?: value index (default 0); min?, max? (omit both to clear)"), - CommandSpec::new("joint.edit", "Edit Joint", edit).params("joint; type?, flip?, offset?, angle?, suppressed?, name?"), - CommandSpec::new("joint.delete", "Delete Joint", delete).params("joint: id|name (or origin: name, link: index)"), - CommandSpec::new("joint.list", "List Joints", list).noundo().params( + CommandSpec::new("joint.limits", "Joint Limits", limits).icon("joint").params("joint; index?: value index (default 0); min?, max? (omit both to clear)"), + CommandSpec::new("joint.edit", "Edit Joint", edit).icon("edit").params("joint; type?, flip?, offset?, angle?, suppressed?, name?"), + CommandSpec::new("joint.delete", "Delete Joint", delete).icon("delete").params("joint: id|name (or origin: name, link: index)"), + CommandSpec::new("joint.list", "List Joints", list).icon("list").noundo().params( "→ joints (type, occurrences, values, limits), origins, links, degrees of freedom, conflicts, floating: [{occurrences, joints}] \ (joint chains joined to nothing grounded: the solver places none of them, so driving them moves nothing)", ), - CommandSpec::new("joint.solve", "Solve Joints", solve_cmd).params("re-place the occurrences from the joints → conflicts, errors, dof"), + CommandSpec::new("joint.solve", "Solve Joints", solve_cmd).icon("compute").params("re-place the occurrences from the joints → conflicts, errors, dof"), CommandSpec::new("inspect.interference", "Interference", interference).at("SOLID", "INSPECT").icon("interference").noundo().params( "bodies?: [names] (default: all, in world placement) → interferences: pairs that overlap with the overlap volume, area, bounding box and centre; unchecked: pairs that could not be checked, with the reason. Bodies are pruned by bounding box first, so any number of bodies is fine; at most 20000 pairs whose boxes overlap are intersected per call (truncated: true and skipped_pairs: n say when more were left out)", ), diff --git a/crates/engine/src/cmd/libraries.rs b/crates/engine/src/cmd/libraries.rs index 585d5f10..ad64fef6 100644 --- a/crates/engine/src/cmd/libraries.rs +++ b/crates/engine/src/cmd/libraries.rs @@ -6,11 +6,11 @@ use serde_json::{Value, json}; use solidcraft_io::catalog::{Catalog, Definition, Provenance}; pub static COMMANDS: &[CommandSpec] = &[ - CommandSpec::new("library.inspect", "Inspect Local Library", inspect).noundo().doc_only() + CommandSpec::new("library.inspect", "Inspect Local Library", inspect).icon("inspect").noundo().doc_only() .params("path: scalar library JSON; returns validated catalog and exact entry revisions, without changing the design"), - CommandSpec::new("library.export", "Export Design Library", export).noundo().doc_only() + CommandSpec::new("library.export", "Export Design Library", export).icon("export").noundo().doc_only() .params("path: .json; id? (default user.local), name?, provenance?, overwrite? false. Copies assigned looks and design-local physical definitions; never changes the design"), - CommandSpec::new("library.apply", "Apply Local Library Entry", apply).enabled(super::not_in_sketch) + CommandSpec::new("library.apply", "Apply Local Library Entry", apply).icon("import").enabled(super::not_in_sketch) .params("path, entry: qualified ID, revision: exact content SHA-256; targets?: {bodies?, faces?: [{body,point}], components?}; name?: physical material name on import. Copies values in one undo step; material conflicts are explicit"), ]; diff --git a/crates/engine/src/cmd/materials.rs b/crates/engine/src/cmd/materials.rs index c96376a2..0c27d46b 100644 --- a/crates/engine/src/cmd/materials.rs +++ b/crates/engine/src/cmd/materials.rs @@ -21,7 +21,7 @@ pub static COMMANDS: &[CommandSpec] = &[ CommandSpec::new("material.edit", "Edit Physical Material", edit).icon("material").enabled(has_user_materials).params( "material: a material defined in this design; name? (bodies and configuration cells follow); density_g_cm3?; appearance? | color?, opacity?, pbr? (as material.create; color: null clears the look). Omitted appearance/color retains the entire current look", ), - CommandSpec::new("material.delete", "Delete Physical Material", delete).enabled(has_user_materials).params( + CommandSpec::new("material.delete", "Delete Physical Material", delete).icon("delete").enabled(has_user_materials).params( "material: a material defined in this design; replace?: material for the bodies and configuration cells that use it (without it, a material in use is refused)", ), ]; diff --git a/crates/engine/src/cmd/metadata.rs b/crates/engine/src/cmd/metadata.rs index 451d7949..de1b7809 100644 --- a/crates/engine/src/cmd/metadata.rs +++ b/crates/engine/src/cmd/metadata.rs @@ -22,6 +22,7 @@ pub static COMMANDS: &[CommandSpec] = &[ reverse-DNS key such as com.example.tool (an object; null removes) → metadata, changed: [keys]", ), CommandSpec::new("document.metadata.remove", "Remove Design Property", remove) + .icon("delete") .doc_only() .params("key, or keys: [keys] (any key, also one another tool wrote) → metadata, removed: [keys that were there]"), ]; diff --git a/crates/engine/src/cmd/motion.rs b/crates/engine/src/cmd/motion.rs index 786089d7..03f01798 100644 --- a/crates/engine/src/cmd/motion.rs +++ b/crates/engine/src/cmd/motion.rs @@ -30,35 +30,41 @@ macro_rules! trace_doc { } pub static COMMANDS: &[CommandSpec] = &[ - CommandSpec::new("contact.enable_sets", "Enable Contact Sets", enable_sets).at("SOLID", "ASSEMBLE").icon("joint").params( + CommandSpec::new("contact.enable_sets", "Enable Contact Sets", enable_sets).at("SOLID", "ASSEMBLE").icon("contact_sets").params( "enabled?: bool (default true) — driving joints and motion studies stop where bodies in one contact set would pass through each other", ), CommandSpec::new("contact.enable_all", "Enable All Contact", enable_all) .at("SOLID", "ASSEMBLE") - .icon("joint") + .icon("contact_all") .params("every occurrence takes part in contact (one set of all)"), - CommandSpec::new("contact.disable_all", "Disable Contact", disable_all).at("SOLID", "ASSEMBLE").icon("joint").params("contact off"), + CommandSpec::new("contact.disable_all", "Disable Contact", disable_all).at("SOLID", "ASSEMBLE").icon("contact_off").params("contact off"), CommandSpec::new("contact.create", "New Contact Set", new_set) .at("SOLID", "ASSEMBLE") - .icon("joint") + .icon("contact_new") .params("occurrences: [ids or names] (two or more); name?"), - CommandSpec::new("contact.edit", "Edit Contact Set", edit_set).params("set: id|name; occurrences?; name?; suppressed?: bool; delete?: bool"), - CommandSpec::new("contact.list", "List Contact Sets", list_sets).noundo().doc_only().params("→ sets, enabled, all"), - CommandSpec::new("contact.check", "Check Contact", check).noundo().params("→ pairs in contact sets that pass through each other now"), - CommandSpec::new("motion.study", "Motion Study", study).at("SOLID", "ASSEMBLE").icon("joint").params( + CommandSpec::new("contact.edit", "Edit Contact Set", edit_set) + .icon("edit") + .params("set: id|name; occurrences?; name?; suppressed?: bool; delete?: bool"), + CommandSpec::new("contact.list", "List Contact Sets", list_sets).icon("list").noundo().doc_only().params("→ sets, enabled, all"), + CommandSpec::new("contact.check", "Check Contact", check) + .icon("interference") + .noundo() + .params("→ pairs in contact sets that pass through each other now"), + CommandSpec::new("motion.study", "Motion Study", study).at("SOLID", "ASSEMBLE").icon("play").params( "name?; study? (edit an existing one); steps? (timeline length, default 100); keys: [{joint, index?, points: [[step, value], …]}] \ (values: degrees for angles, mm for distances, or expressions)", ), - CommandSpec::new("motion.play", "Play Motion Study", play).params( + CommandSpec::new("motion.play", "Play Motion Study", play).icon("play").params( "study: id|name; step (0…steps) — sets the joints to the study's values there (contact sets stop them); quiet?: bool \ (positions of only the occurrences that moved) → values: [{joint, values: [{name, value, unit}]}], positions, moved: whether \ any occurrence moved, unmoved: [{joint, name, reason}] (keyed joints that can move nothing, such as a chain joined to \ nothing grounded)", ), CommandSpec::new("motion.export", "Export Motion Study", export) + .icon("export") .noundo() .params("study; samples? (default steps + 1); path? (.json or .csv) → occurrence positions per step (the design is not changed)"), - CommandSpec::new("motion.export_animation", "Export Motion Animation", animation::export).noundo().params( + CommandSpec::new("motion.export_animation", "Export Motion Animation", animation::export).icon("export").noundo().params( "study: id|name, or joint: id|name with from and to (degrees for angles, mm for distances, or expressions) and index?; path: a .gif file, or a folder for PNG frames (frame-0001.png, frame-0002.png, …); samples? (frames, 2…1000; default the study's steps + 1, or 100); width?, height? (16…4096 pixels, default 800 × 500); camera?: {target, yaw, pitch, roll?, distance, fov?, up?} (ui.camera's reply; fields left out keep the default view: view.apply's camera, or the home view framing the model); fps? (1…50, default 20); grid?, axes? (default true: the ground grid and the origin's axes) - draws the study as it plays, or the joint as it is driven, with the headless renderer → path, format, frames, width, height, fps (GIF), bytes, renderer, stopped_at, stopped_by_loop, stopped_by_contact (the design is not changed)", ), CommandSpec::new("motion.trace", "Motion Trace", trace).at("SOLID", "ASSEMBLE").icon("trace").noundo().params(concat!( @@ -66,24 +72,25 @@ pub static COMMANDS: &[CommandSpec] = &[ " → points: [[x,y,z]…] world positions, samples (taken), stopped_at (the sample where the motion stopped short; the trace \ ends there), stopped_by_loop, stopped_by_contact (the design is not changed)" )), - CommandSpec::new("motion.trace.keep", "Keep Motion Trace", keep_trace).params(concat!( + CommandSpec::new("motion.trace.keep", "Keep Motion Trace", keep_trace).icon("trace").params(concat!( trace_doc!(), "; name? (default Motion TraceN) - keeps the trace as a 3D sketch spline through its points (a new sketch with 3D Sketch \ on, in the root component, where no joint moves it; a fixed curve, it does not follow later changes) → sketch, name, curves, points" )), - CommandSpec::new("motion.plot", "Plot Motion Study", analysis::plot).noundo().params(analysis::PLOT_DOC), - CommandSpec::new("motion.interference", "Motion Interference", analysis::interference).noundo().params(analysis::INTERFERENCE_DOC), - CommandSpec::new("motion.list", "List Motion Studies", list_studies).noundo(), - CommandSpec::new("motion.delete", "Delete Motion Study", delete_study).params("study"), - CommandSpec::new("explode.create", "Exploded View", explode_create).at("SOLID", "ASSEMBLE").icon("joint").params( + CommandSpec::new("motion.plot", "Plot Motion Study", analysis::plot).icon("graph").noundo().params(analysis::PLOT_DOC), + CommandSpec::new("motion.interference", "Motion Interference", analysis::interference).icon("interference").noundo().params(analysis::INTERFERENCE_DOC), + CommandSpec::new("motion.list", "List Motion Studies", list_studies).icon("list").noundo(), + CommandSpec::new("motion.delete", "Delete Motion Study", delete_study).icon("delete").params("study"), + CommandSpec::new("explode.create", "Exploded View", explode_create).at("SOLID", "ASSEMBLE").icon("explode").params( "name?; scale? (automatic: each occurrence pushed away from the assembly's centre by scale × its offset, default 1); \ moves?: [{occurrence, translate: [x,y,z]}] (instead of automatic)", ), CommandSpec::new("explode.show", "Show Exploded View", explode_show) + .icon("explode") .noundo() .params("view: id|name (or none: back to the assembled placement) — shown only; the design keeps its placements"), - CommandSpec::new("explode.list", "List Exploded Views", explode_list).noundo(), - CommandSpec::new("explode.delete", "Delete Exploded View", explode_delete).params("view"), + CommandSpec::new("explode.list", "List Exploded Views", explode_list).icon("list").noundo(), + CommandSpec::new("explode.delete", "Delete Exploded View", explode_delete).icon("delete").params("view"), ]; const MAX_ITEMS: usize = 10_000; diff --git a/crates/engine/src/cmd/parameters.rs b/crates/engine/src/cmd/parameters.rs index 47756fe7..daa74cbc 100644 --- a/crates/engine/src/cmd/parameters.rs +++ b/crates/engine/src/cmd/parameters.rs @@ -15,34 +15,44 @@ pub static COMMANDS: &[CommandSpec] = &[ favorite?: bool, new_name?: rename (references follow); or delete: name → recomputed: features rebuilt, \ restored: features whose earlier result for the same inputs was reused (e.g. a value set back)", ), - CommandSpec::new("parameters.list", "List Parameters", list).noundo().doc_only().params( + CommandSpec::new("parameters.list", "List Parameters", list).icon("list").noundo().doc_only().params( "favorites?: bool (only favourites), filter?: all|user|sketch|feature|favorites → parameters (name, expression, unit, value in its unit, \ source, feature/input, comment, favorite, uses, used_by, error) and cycles", ), - CommandSpec::new("parameters.add", "Add User Parameter", add).params("name, expression, unit?, comment?, favorite?: bool"), + CommandSpec::new("parameters.add", "Add User Parameter", add).icon("new").params("name, expression, unit?, comment?, favorite?: bool"), CommandSpec::new("parameters.rename", "Rename Parameter", rename) + .icon("rename") .params("name, new_name (expressions, feature inputs and dimensions that use it follow)"), - CommandSpec::new("parameters.delete", "Delete Parameter", delete).params("name (refused while something uses it; the error lists the users)"), - CommandSpec::new("parameters.favorite", "Favourite Parameter", favorite).params("name, favorite?: bool (default true)"), + CommandSpec::new("parameters.delete", "Delete Parameter", delete) + .icon("delete") + .params("name (refused while something uses it; the error lists the users)"), + CommandSpec::new("parameters.favorite", "Favourite Parameter", favorite).icon("star").params("name, favorite?: bool (default true)"), CommandSpec::new("parameters.move", "Move Parameter", move_param) + .icon("reorder") .params("name, before | after: another user parameter; the user parameter is listed just before or after it"), - CommandSpec::new("parameters.comment", "Comment Parameter", comment).params("name, comment"), + CommandSpec::new("parameters.comment", "Comment Parameter", comment).icon("text").params("name, comment"), CommandSpec::new("parameters.users", "Parameter Users", users) + .icon("graph") .noundo() .params("name → users (parameters, feature inputs, dimensions) and the features it drives"), CommandSpec::new("parameters.graph", "Parameter Graph", graph) + .icon("graph") .noundo() .params("→ nodes, edges [user, used] (users include Feature:), cycles, errors"), CommandSpec::new("parameters.export", "Export Parameters", export) + .icon("export") .noundo() .params("format?: csv|json (default csv, or from the path), path?: write to a file → text"), CommandSpec::new("parameters.import", "Import Parameters", import) + .icon("import") .params("text: CSV (header with name, expression, unit?, comment?) or JSON [{name, expression, unit?, comment?}] | path; all or nothing"), CommandSpec::new("expr.evaluate", "Evaluate Expression", evaluate) + .icon("params") .noundo() .doc_only() .params("expression, kind?: length|angle|unitless (or unit?: mm|in|deg…) → ok, value (mm/rad), display_value, references, error"), CommandSpec::new("parameters.complete", "Complete Expression", complete) + .icon("params") .noundo() .doc_only() .params("prefix?: the text before the cursor → word, suggestions [{text, kind: parameter|function|unit|constant, detail}]"), diff --git a/crates/engine/src/cmd/parts.rs b/crates/engine/src/cmd/parts.rs index b0a60e4b..ffdb3e09 100644 --- a/crates/engine/src/cmd/parts.rs +++ b/crates/engine/src/cmd/parts.rs @@ -15,7 +15,7 @@ use crate::params::{bad, num, str_, vec3}; use crate::{EngineError, Result, Session}; pub static COMMANDS: &[CommandSpec] = &[ - CommandSpec::new("parts.library", "Standard Parts", library) + CommandSpec::new("parts.library", "Standard Parts", library).icon("library") .noundo() .params("family? → families (name, standard, sizes, lengths) and, with family, each size's dimensions (mm)"), CommandSpec::new("parts.fastener", "Insert Fastener", insert).at("SOLID", "INSERT").icon("fastener").params( @@ -26,7 +26,7 @@ pub static COMMANDS: &[CommandSpec] = &[ length? (mm; screws, bolts, set screws, pins; default a standard length near 2.5 d); \ at?: [x,y,z] on a hole's rim or wall (the part seats on the hole's face with a rigid joint) | point?: [x,y,z] with direction?: [x,y,z] (no joint); name?", ), - CommandSpec::new("parts.resize", "Change Part Size", resize) + CommandSpec::new("parts.resize", "Change Part Size", resize).icon("scale") .params("component: id|name of an inserted part; size?; length? — rebuilds it (joints keep it seated)"), ]; diff --git a/crates/engine/src/cmd/plastic.rs b/crates/engine/src/cmd/plastic.rs index e825ab75..b6012139 100644 --- a/crates/engine/src/cmd/plastic.rs +++ b/crates/engine/src/cmd/plastic.rs @@ -21,7 +21,7 @@ pub static COMMANDS: &[CommandSpec] = &[ CommandSpec::new("plastic.lip", "Lip", lip).at("PLASTIC", "CREATE").icon("lip").params( "face: [x,y,z] on the rim face of a shelled body; width, height; type?: lip|groove; gap? (groove clearance); side?: inside|outside (which rim edge); body?", ), - CommandSpec::new("plastic.snap_fit", "Snap Fit", snap_fit).at("PLASTIC", "CREATE").icon("snap").params( + CommandSpec::new("plastic.snap_fit", "Snap Fit", snap_fit).at("PLASTIC", "CREATE").icon("snap_fit").params( "position: [x,y,z] on a face; direction? (default: out of the face); hook: [x,y,z] (the catch's direction); length, thickness, width, catch_depth, catch_length; body?", ), CommandSpec::new("plastic.rest", "Rest", rest).at("PLASTIC", "CREATE").icon("rest").params( @@ -36,7 +36,7 @@ pub static COMMANDS: &[CommandSpec] = &[ CommandSpec::new("plastic.assign_rule", "Assign Plastic Rule", assign_rule).at("PLASTIC", "SETUP").icon("plastic_assign").params( "bodies: [names]; rule: name (empty: no rule) — plastic features on these bodies take its draft and clearance as defaults", ), - CommandSpec::new("plastic.rules", "List Plastic Rules", list_rules).noundo().params("→ rules with their expressions and values, the active rule and the rules assigned to bodies"), + CommandSpec::new("plastic.rules", "List Plastic Rules", list_rules).icon("plastic_rule").noundo().params("→ rules with their expressions and values, the active rule and the rules assigned to bodies"), ]; fn rule_json(s: &Session, r: &PlasticRule) -> Value { diff --git a/crates/engine/src/cmd/render.rs b/crates/engine/src/cmd/render.rs index 301cd5dd..e516b985 100644 --- a/crates/engine/src/cmd/render.rs +++ b/crates/engine/src/cmd/render.rs @@ -13,27 +13,27 @@ use crate::{Result, Session}; mod jobs; pub static COMMANDS: &[CommandSpec] = &[ -CommandSpec::new("render.preflight", "Check Render Request", jobs::preflight).noundo().doc_only() +CommandSpec::new("render.preflight", "Check Render Request", jobs::preflight).icon("inspect").noundo().doc_only() .params("setup?: saved name OR settings?: full RenderSetup; transparent_background?: false; replace?: false. Validates the installed image host and bounded captured product scene without starting work. Stale geometry is rejected. See docs/render-jobs.md"), -CommandSpec::new("render.start", "Render Image", jobs::start).noundo().doc_only().at("RENDER", "IMAGE").icon("box") +CommandSpec::new("render.start", "Render Image", jobs::start).noundo().doc_only().at("RENDER", "IMAGE").icon("render_image") .params("Same request as render.preflight. Captures an immutable product scene and queues a bounded image job. Returns its session-scoped job identity, captured scene_generation and progress. Does not change the design. See docs/render-jobs.md"), -CommandSpec::new("render.status", "Render Progress", jobs::status).noundo().doc_only() +CommandSpec::new("render.status", "Render Progress", jobs::status).icon("list").noundo().doc_only() .params("job: {session, serial, generation}. Non-blocking progress, error and completed frame metadata; no image bytes and no rendering in this call."), -CommandSpec::new("render.cancel", "Cancel Render", jobs::cancel).noundo().doc_only() +CommandSpec::new("render.cancel", "Cancel Render", jobs::cancel).icon("delete").noundo().doc_only() .params("job: {session, serial, generation}. Requests cooperative cancellation; completed jobs remain completed. Late cancelled frames cannot be published."), -CommandSpec::new("render.export", "Export Render Image", jobs::export).noundo().doc_only() +CommandSpec::new("render.export", "Export Render Image", jobs::export).icon("export").noundo().doc_only() .params("job: {session, serial, generation}, path: PNG destination, overwrite?: false. Exports a retained completed image atomically; an existing destination is preserved on failure. See docs/render-jobs.md"), -CommandSpec::new("render.backends", "Render Backends", backends) +CommandSpec::new("render.backends", "Render Backends", backends).icon("list") .noundo() .doc_only() .params("No parameters. Returns {schema_version: 1, default_backend: builtin, backends: [{id, label, availability, executors: [{id, availability, profiles, limits, scratch}]}]}. Metadata only; does not start rendering or probe a GPU. See docs/render-backends.md"), -CommandSpec::new("render.settings.get", "Render Settings", get_settings).noundo().doc_only() +CommandSpec::new("render.settings.get", "Render Settings", get_settings).icon("settings").noundo().doc_only() .params("name?: saved setup. Without name returns {setups, defaults}; with name returns {name, setup}. Does not evaluate the model or apply a camera. See docs/render-settings.md"), -CommandSpec::new("render.settings.set", "Save Render Settings", set_settings).doc_only() +CommandSpec::new("render.settings.set", "Save Render Settings", set_settings).icon("settings").doc_only() .params("name: setup name, set: object of fields to replace (backend, mode, camera, width, height, format, exposure_ev, environment_rotation, environment_intensity, background). Creates from defaults, validates atomically, one undo step; identical edits are no-ops. See docs/render-settings.md"), -CommandSpec::new("render.scene", "Inspect Render Scene", scene).noundo().at("RENDER", "SCENE").icon("box") +CommandSpec::new("render.scene", "Inspect Render Scene", scene).noundo().at("RENDER", "SCENE").icon("render_scene") .params("No parameters. Authoritative product-scene metadata, without modelling overlays or mesh payloads. Explicitly settles stale geometry; does not render an image. See docs/render-workspace.md"), -CommandSpec::new("render.settings.apply", "Apply Render Camera", apply_settings).noundo().doc_only() +CommandSpec::new("render.settings.apply", "Apply Render Camera", apply_settings).icon("camera").noundo().doc_only() .params("name: saved setup. Applies its existing Camera snapshot including roll/up as view state, without modifying the document or starting a render."), ]; diff --git a/crates/engine/src/cmd/script.rs b/crates/engine/src/cmd/script.rs index 768d4f54..5eb55b50 100644 --- a/crates/engine/src/cmd/script.rs +++ b/crates/engine/src/cmd/script.rs @@ -14,12 +14,12 @@ use crate::params::{bad, str_}; use crate::{EngineError, Result, Session}; pub static COMMANDS: &[CommandSpec] = &[ - CommandSpec::new("timeline.add", "Add Recorded Feature", add).params( + CommandSpec::new("timeline.add", "Add Recorded Feature", add).icon("new").params( "feature: a feature as the design file and timeline.inspect hold it ({type, name?, id?, …}; id and name are kept when free, \ so later steps can refer to them); params?: [{name, expression, unit?, comment?}] (the model parameters its sketch \ dimensions use); the feature goes at the marker → feature, name, error, warning", ), - CommandSpec::new("timeline.export_script", "Export Command Script", export_script).noundo().doc_only().params( + CommandSpec::new("timeline.export_script", "Export Command Script", export_script).icon("export").noundo().doc_only().params( "path?: write the script to this file → script: {commands: [...]} that rebuilds the design (solidcraft-cli eval/run), \ steps, omitted: what the script leaves out (appearances, folders, views, configurations); components and assemblies \ are not supported yet", diff --git a/crates/engine/src/cmd/sheet.rs b/crates/engine/src/cmd/sheet.rs index 630a028d..8e0653e7 100644 --- a/crates/engine/src/cmd/sheet.rs +++ b/crates/engine/src/cmd/sheet.rs @@ -25,7 +25,7 @@ pub static COMMANDS: &[CommandSpec] = &[ Thickness, K-factor and bend radius come from the rule (sheet.manage_rules); passing them here is refused, \ and so is the corner relief on a base or contour flange", ), - CommandSpec::new("sheet.perforate", "Perforate", perforate).at("SHEET METAL", "CREATE").icon("hem").params( + CommandSpec::new("sheet.perforate", "Perforate", perforate).at("SHEET METAL", "CREATE").icon("perforate").params( "diameter, pitch: expr; margin?: expr (clear of edges, cut-outs and bends; default: the diameter); pattern?: staggered (60°, default) | square; body? - a perforation field over the whole flat sheet: holes in the flat pattern (perforations), the DXF (layer PERFORATION) and the measured volume, not cut into the B-rep (#732)", ), CommandSpec::new("sheet.hem", "Hem", hem) @@ -59,7 +59,7 @@ pub static COMMANDS: &[CommandSpec] = &[ `corner_relief` across; tear none, each bend's end is cut straight from the corner to its flange's end; default notch); \ active?: bool; delete?: bool", ), - CommandSpec::new("sheet.rules", "List Sheet Metal Rules", list_rules).noundo().doc_only().params("→ rules with their expressions and values, and the active one"), + CommandSpec::new("sheet.rules", "List Sheet Metal Rules", list_rules).icon("sheet_rules").noundo().doc_only().params("→ rules with their expressions and values, and the active one"), CommandSpec::new("sheet.flat_pattern", "Create Flat Pattern", flat_pattern) .at("SHEET METAL", "CREATE") .icon("flat") @@ -67,7 +67,7 @@ pub static COMMANDS: &[CommandSpec] = &[ .params( "body? → flat size, outline loops and cut-outs (flat coordinates, as point lists), cutout_shapes (the same cut-outs exactly, in the same order: {circle: {center, radius}} for a round hole, else {segments: [{line: [a, b]} | {arc: {center, radius, start_deg, sweep_deg}} | {polyline: [...]}]}), pockets (with their own cutout_shapes), bend lines and the bend table", ), - CommandSpec::new("sheet.export_dxf", "Export Flat Pattern as DXF", export_dxf) + CommandSpec::new("sheet.export_dxf", "Export Flat Pattern as DXF", export_dxf).icon("dxf") .noundo() .params( "body?, path?, curves?: exact|arcs (default exact: an ellipse as ELLIPSE, splines, text outlines and other Béziers as SPLINE; arcs: tangent arcs and lines within 0.001 mm, for machines that read only those), \ @@ -77,7 +77,7 @@ pub static COMMANDS: &[CommandSpec] = &[ → DXF (AutoCAD 2000, mm): layers OUTLINE (the outer contour), CUT (what is cut out inside it), TEXT or ETCH (letters), BEND_UP and BEND_DOWN (dashed centre lines), BEND_NOTES, \ POCKET__Z_TO__MM (one per side and depth) and PERFORATION; inner contours come before the outline", ), - CommandSpec::new("sheet.export_bend_table", "Export Bend Table", export_bend_table) + CommandSpec::new("sheet.export_bend_table", "Export Bend Table", export_bend_table).icon("bendtable") .noundo() .params("body?, format?: csv|json (default csv), path? → bend table in mm and degrees, including rule and flat centre-line coordinates; a bend that cut-outs split has a row for each piece"), ]; diff --git a/crates/engine/src/cmd/sketch.rs b/crates/engine/src/cmd/sketch.rs index 95cf367e..504f1838 100644 --- a/crates/engine/src/cmd/sketch.rs +++ b/crates/engine/src/cmd/sketch.rs @@ -38,7 +38,7 @@ pub static COMMANDS: &[CommandSpec] = &[ .key("L") .enabled(in_sketch) .params(concat!("points: [[x,y] | \"l1.end\" | {at, ref}…], closed?: bool, construction?: bool, ids?: [..], infer?: bool (add horizontal/vertical)", connect_doc!(), created_doc!())), - CommandSpec::new("sketch.draw.dimensioned", "Draw Dimensioned Sketch Geometry", draw_dimensioned) + CommandSpec::new("sketch.draw.dimensioned", "Draw Dimensioned Sketch Geometry", draw_dimensioned).icon("dimension") .enabled(in_sketch) .params(concat!("command: sketch creation command, params: object, dimensions: object of role to numeric or expression value; creates geometry and all driving dimensions atomically in one undo step", created_doc!())), CommandSpec::new("sketch.rectangle.two_point", "2-Point Rectangle", rect_two_point) @@ -91,12 +91,12 @@ pub static COMMANDS: &[CommandSpec] = &[ .params(concat!("center, radius (to vertices), sides, angle?: deg", connect_doc!(), created_doc!())), CommandSpec::new("sketch.polygon.circumscribed", "Circumscribed Polygon", polygon_circumscribed) .at("SKETCH", "CREATE") - .icon("polygon") + .icon("polygon_circumscribed") .enabled(in_sketch) .params(concat!("center, radius (to edge midpoints), sides, angle?: deg", connect_doc!(), created_doc!())), CommandSpec::new("sketch.polygon.edge", "Edge Polygon", polygon_edge) .at("SKETCH", "CREATE") - .icon("polygon") + .icon("polygon_edge") .enabled(in_sketch) .params(concat!("p0, p1: one edge, sides", connect_doc!(), created_doc!())), CommandSpec::new("sketch.slot.center_to_center", "Center to Center Slot", slot_c2c) @@ -106,7 +106,7 @@ pub static COMMANDS: &[CommandSpec] = &[ .params(concat!("p0, p1: arc centres, width", connect_doc!(), created_doc!())), CommandSpec::new("sketch.slot.overall", "Overall Slot", slot_overall) .at("SKETCH", "CREATE") - .icon("slot") + .icon("slot_overall") .enabled(in_sketch) .params(concat!("p0, p1: overall ends, width", connect_doc!(), created_doc!())), CommandSpec::new("sketch.point", "Point", draw_point).at("SKETCH", "CREATE").icon("point").enabled(in_sketch).params(concat!("point: [x,y], id?", connect_doc!(), created_doc!())), @@ -174,27 +174,27 @@ pub static COMMANDS: &[CommandSpec] = &[ .key("X") .enabled(in_sketch) .params("curves: [ids], value?: bool (default: construction unless all of them already are)"), - CommandSpec::new("sketch.delete", "Delete Sketch Entities", sketch_delete) + CommandSpec::new("sketch.delete", "Delete Sketch Entities", sketch_delete).icon("delete") .enabled(in_sketch) .params("entities: [curve, point or constraint ids]"), - CommandSpec::new("sketch.move_point", "Drag Sketch Point", move_point).enabled(in_sketch).params("point: ref, to: [x,y]"), - CommandSpec::new("sketch.drag", "Drag Sketch Geometry", drag).enabled(in_sketch).live(drag_live).params( + CommandSpec::new("sketch.move_point", "Drag Sketch Point", move_point).icon("sketch_move").enabled(in_sketch).params("point: ref, to: [x,y]"), + CommandSpec::new("sketch.drag", "Drag Sketch Geometry", drag).icon("sketch_move").enabled(in_sketch).live(drag_live).params( "entity: point or curve, from: [x,y] where it was grabbed, to: [x,y], live?: bool: a point goes to `to`, a line (spline, conic) slides by to - from, a circle or arc grabbed on its edge takes the radius to `to`; the rest moves as little as it can and keeps its solution branch (a long move is followed in short steps, each solved with the entity held; a step that cannot be solved that way is approached as far as it can be and the next ones tried; when none past it solves, the last one is left to the sketch's ordinary solve, which may pull it back or leave the sketch unsolved; a step that would sweep an arc through its start is undone and the drag ends there). Returns `reached`: how much of the way from `from` to `to` the geometry followed (0..1), and `at`: where the grabbed spot went (`from` moved as the dragged points did), the `from` of the next move. `live: true` (each frame of an interactive drag) solves the sketch once and leaves the features after it as they were (stale) until `sketch.drag_end` or any command that reads the model; `dof` is then the one from before the drag", ), - CommandSpec::new("sketch.drag_end", "End Sketch Drag", drag_end).enabled(in_sketch).doc_only().params( + CommandSpec::new("sketch.drag_end", "End Sketch Drag", drag_end).icon("sketch_move").enabled(in_sketch).doc_only().params( "constraint?: {command: a sketch.constraint.* id, params}: ends a drag made of `sketch.drag` `live: true` frames: adds the constraint it let go on (not kept if it over-constrains the sketch), then evaluates the whole model once; returns the constraint's result and the sketch's solved state and degrees of freedom", ), - CommandSpec::new("sketch.dimension_text", "Move Dimension Text", dimension_text) + CommandSpec::new("sketch.dimension_text", "Move Dimension Text", dimension_text).icon("edit") .enabled(in_sketch) .params("dimension: constraint id or parameter name; at: [x,y] text centre (sketch coordinates) | reset: true (default place)"), CommandSpec::new("sketch.options", "Sketch Palette Options", sketch_options).icon("sketch").params( "sketch?: id|name (default active); any of show_profile, show_points, show_dimensions, show_constraints, show_projected, show_construction, grid, snap, slice, sketch_3d: bool. Returns them all", ), - CommandSpec::new("sketch.chain", "Chain", chain_cmd) + CommandSpec::new("sketch.chain", "Chain", chain_cmd).icon("chain") .enabled(in_sketch) .noundo() .params("curve: id: the curves joined to it end to end (what a double-click selects)"), - CommandSpec::new("sketch.solve", "Solve Sketch", solve_cmd) + CommandSpec::new("sketch.solve", "Solve Sketch", solve_cmd).icon("compute") .enabled(in_sketch) .noundo() .params("reports the solver status and degrees of freedom"), diff --git a/crates/engine/src/cmd/sketch_3d.rs b/crates/engine/src/cmd/sketch_3d.rs index 07f865c5..d6202511 100644 --- a/crates/engine/src/cmd/sketch_3d.rs +++ b/crates/engine/src/cmd/sketch_3d.rs @@ -12,17 +12,20 @@ use crate::params::{bad, req_vec3, vec3}; use crate::{Result, Session}; pub static COMMANDS: &[CommandSpec] = &[ - CommandSpec::new("sketch.line3d", "3D Line", line3d).enabled(in_sketch).params(concat!( + CommandSpec::new("sketch.line3d", "3D Line", line3d).icon("line").enabled(in_sketch).params(concat!( "points: [[x,y,z]…] world points (2 or more): one 3D line per segment, joined end to end (needs 3D Sketch on)", created_doc!() )), CommandSpec::new("sketch.spline3d", "3D Spline", spline3d) + .icon("spline") .enabled(in_sketch) .params(concat!("points: [[x,y,z]…] world fit points (2 or more): a smooth 3D curve through them (needs 3D Sketch on)", created_doc!())), CommandSpec::new("sketch.point3d", "3D Point", point3d) + .icon("point") .enabled(in_sketch) .params(concat!("point: [x,y,z] world point (needs 3D Sketch on)", created_doc!())), CommandSpec::new("sketch.move3d", "Move 3D Point", move3d) + .icon("sketch_move") .enabled(in_sketch) .params("wire: drawn 3D curve or point id, index?: fit point (default 0), to: [x,y,z] | by: [dx,dy,dz]; points joined to it move too"), ]; diff --git a/crates/engine/src/cmd/sketch_canvas.rs b/crates/engine/src/cmd/sketch_canvas.rs index b884e784..08f16be3 100644 --- a/crates/engine/src/cmd/sketch_canvas.rs +++ b/crates/engine/src/cmd/sketch_canvas.rs @@ -18,9 +18,9 @@ pub static COMMANDS: &[CommandSpec] = &[ .icon("decal") .params("path | data; face: [x,y,z] on a planar face (the image is centred there); width?: mm; angle?: deg; opacity?: (default 1)"), CommandSpec::new("canvas.edit", "Edit Canvas", edit_canvas).at("SKETCH", "INSERT").icon("canvas").params("canvas: id or name; center?, width?, angle?: deg, opacity?, flip?, visible?, name?"), - CommandSpec::new("canvas.calibrate", "Calibrate Canvas", calibrate) + CommandSpec::new("canvas.calibrate", "Calibrate Canvas", calibrate).icon("measure") .params("canvas: id or name; a, b: two points on the canvas plane [x,y]; distance: their true distance (mm): scales the image about a"), - CommandSpec::new("canvas.delete", "Delete Canvas", delete_canvas).params("canvas: id or name"), + CommandSpec::new("canvas.delete", "Delete Canvas", delete_canvas).icon("delete").params("canvas: id or name"), ]; fn image_bytes(p: &Value, cmd: &str) -> Result> { diff --git a/crates/engine/src/cmd/sketch_clipboard.rs b/crates/engine/src/cmd/sketch_clipboard.rs index 364f1851..fb9c4d19 100644 --- a/crates/engine/src/cmd/sketch_clipboard.rs +++ b/crates/engine/src/cmd/sketch_clipboard.rs @@ -17,14 +17,14 @@ use crate::params::{bad, string_list, vec2}; use crate::{Result, Sel, Session}; pub static COMMANDS: &[CommandSpec] = &[ - CommandSpec::new("sketch.copy", "Copy Sketch Entities", copy) + CommandSpec::new("sketch.copy", "Copy Sketch Entities", copy).icon("copy") .noundo() .enabled(in_sketch) .params("entities?: [curve or point ids] (default: the selected sketch geometry); the constraints and dimensions among them come along"), - CommandSpec::new("sketch.cut", "Cut Sketch Entities", cut) + CommandSpec::new("sketch.cut", "Cut Sketch Entities", cut).icon("cut") .enabled(in_sketch) .params("entities?: [curve or point ids] (default: the selected sketch geometry): copies them as sketch.copy does, then deletes them (one undo step)"), - CommandSpec::new("sketch.paste", "Paste Sketch Entities", paste).enabled(in_sketch).params( + CommandSpec::new("sketch.paste", "Paste Sketch Entities", paste).icon("paste").enabled(in_sketch).params( "at?: [x, y] (sketch point the copy's centre goes to) | translate?: [x, y]; without either, offset from the original in its own sketch and at the same coordinates in another; sketch?: target (default: the active sketch)", ), ]; diff --git a/crates/engine/src/cmd/sketch_constraints.rs b/crates/engine/src/cmd/sketch_constraints.rs index 260c8cfb..862dcd53 100644 --- a/crates/engine/src/cmd/sketch_constraints.rs +++ b/crates/engine/src/cmd/sketch_constraints.rs @@ -41,14 +41,14 @@ pub static COMMANDS: &[CommandSpec] = &[ .icon("constrainer") .enabled(in_sketch) .params("entities: [one or two sketch entity ids]: applies the constraint the geometry nearly has (horizontal/vertical, parallel, perpendicular, collinear, coincident, concentric, equal, tangent)"), - CommandSpec::new("sketch.toggle_driven", "Toggle Driven Dimension", toggle_driven) + CommandSpec::new("sketch.toggle_driven", "Toggle Driven Dimension", toggle_driven).icon("dimension") .enabled(in_sketch) .params("constraint: dimension id, driven?: bool (default toggles); a driving dimension gets a parameter with its current value"), - CommandSpec::new("sketch.glyphs", "Constraint Glyphs", glyphs) + CommandSpec::new("sketch.glyphs", "Constraint Glyphs", glyphs).icon("eye") .enabled(in_sketch) .noundo() .params("sketch?: id|name: every constraint with where to draw its glyph (sketch coordinates)"), - CommandSpec::new("sketch.snap", "Snap Inference", snap) + CommandSpec::new("sketch.snap", "Snap Inference", snap).icon("snap") .enabled(in_sketch) .noundo() .params("at: [x,y], from?: [x,y] (previous click), radius?: mm (snap distance, default 1): what a click there would snap to and infer"), diff --git a/crates/engine/src/cmd/sketch_create.rs b/crates/engine/src/cmd/sketch_create.rs index 7b17fe4d..fbc538b1 100644 --- a/crates/engine/src/cmd/sketch_create.rs +++ b/crates/engine/src/cmd/sketch_create.rs @@ -47,7 +47,7 @@ pub static COMMANDS: &[CommandSpec] = &[ .params(concat!("curves: [three lines/circles/arcs], near: [x,y] (which of the solutions)", created_doc!())), CommandSpec::new("sketch.slot.center_point", "Center Point Slot", slot_center_point) .at("SKETCH", "CREATE") - .icon("slot") + .icon("slot_center") .enabled(in_sketch) .params(concat!("center: slot centre, end: one arc centre, width", created_doc!())), CommandSpec::new("sketch.slot.arc_three_point", "Three Point Arc Slot", arc_slot_three) @@ -57,7 +57,7 @@ pub static COMMANDS: &[CommandSpec] = &[ .params(concat!("start, through, end: points on the centre arc, width", created_doc!())), CommandSpec::new("sketch.slot.arc_center", "Center Point Arc Slot", arc_slot_center) .at("SKETCH", "CREATE") - .icon("arc_slot") + .icon("arc_slot_center") .enabled(in_sketch) .params(concat!("center, start: centre arc start, end: direction of the centre arc end (counter-clockwise), width", created_doc!())), CommandSpec::new("sketch.ellipse", "Ellipse", ellipse).at("SKETCH", "CREATE").icon("ellipse").enabled(in_sketch).params(concat!( @@ -97,16 +97,16 @@ pub static COMMANDS: &[CommandSpec] = &[ .icon("centerline") .enabled(in_sketch) .params("curves: [line ids], value?: bool (default toggles): centerline line type (not in profiles; an axis)"), - CommandSpec::new("sketch.infinite", "Infinite Line", infinite_line) + CommandSpec::new("sketch.infinite", "Infinite Line", infinite_line).icon("infinite") .enabled(in_sketch) .params("curves: [line ids], value?: bool (default toggles): infinite line type (#1359): construction geometry drawn across the whole view through the line's two points; off, a normal line"), - CommandSpec::new("sketch.text", "Text", text).at("SKETCH", "CREATE").icon("text").enabled(in_sketch).params(concat!( + CommandSpec::new("sketch.text", "Text", text).at("SKETCH", "CREATE").icon("sketch_text").enabled(in_sketch).params(concat!( "text: string (\\n breaks lines), at: baseline start [x,y], height?: capital height mm (default 5), angle?: deg; at's x and y, height and angle each take a number or an expression over the design's parameters as a string (\"plate_width / 2\", \"label_size\"): the text keeps the expression, follows the parameters, and returns it in `exprs`; an expression naming no parameter is the number it comes to, a bare number in one is in the design's units, \ font?: an installed font's family or full name, or a .ttf/.otf file path (~ and %VAR% expand; default: the built-in Liberation Sans), ", text_style_doc!(), created_doc!() )), - CommandSpec::new("sketch.edit_text", "Edit Text", edit_text) + CommandSpec::new("sketch.edit_text", "Edit Text", edit_text).icon("edit") .enabled(in_sketch) .live(edit_text_live) .params(concat!( diff --git a/crates/engine/src/cmd/sketch_import.rs b/crates/engine/src/cmd/sketch_import.rs index 785c9ec8..83632df1 100644 --- a/crates/engine/src/cmd/sketch_import.rs +++ b/crates/engine/src/cmd/sketch_import.rs @@ -15,13 +15,13 @@ use crate::{EngineError, Result, Session}; pub static COMMANDS: &[CommandSpec] = &[ CommandSpec::new("sketch.insert_dxf", "Insert DXF", insert_dxf) .at("SKETCH", "INSERT") - .icon("dxf") + .icon("sketch_dxf") .params(concat!("path | text: ASCII DXF (blocks expanded); at?: [x,y] offset, scale?, tolerance?: mm within which ends join (default 0.0001); plane?: (when no sketch is active, a new sketch on it; default XY)", created_doc!())), CommandSpec::new("sketch.insert_svg", "Insert SVG", insert_svg) .at("SKETCH", "INSERT") .icon("svg") .params(concat!("path | text: SVG; at?: [x,y] offset, scale?, tolerance?: mm within which ends join (default 0.0001); plane?: (when no sketch is active, a new sketch on it; default XY)", created_doc!())), - CommandSpec::new("sketch.export_dxf", "Save As DXF", export_dxf) + CommandSpec::new("sketch.export_dxf", "Save As DXF", export_dxf).icon("sketch_dxf") .noundo() .params("sketch?: id|name (default active); path?: file to write (else the DXF text is returned)"), ]; diff --git a/crates/engine/src/cmd/sketch_modify.rs b/crates/engine/src/cmd/sketch_modify.rs index 43898ad9..cd9eeaf2 100644 --- a/crates/engine/src/cmd/sketch_modify.rs +++ b/crates/engine/src/cmd/sketch_modify.rs @@ -31,12 +31,12 @@ pub static COMMANDS: &[CommandSpec] = &[ .params(concat!("point | a, b: a corner of two lines; distance", created_doc!())), CommandSpec::new("sketch.chamfer.distance_angle", "Distance and Angle Chamfer", chamfer_dist_angle) .at("SKETCH", "MODIFY") - .icon("sketch_chamfer") + .icon("sketch_chamfer_angle") .enabled(in_sketch) .params(concat!("point | a, b: a corner of two lines; distance: along a, angle: deg from a", created_doc!())), CommandSpec::new("sketch.chamfer.two_distance", "Two Distance Chamfer", chamfer_two) .at("SKETCH", "MODIFY") - .icon("sketch_chamfer") + .icon("sketch_chamfer_two_distances") .enabled(in_sketch) .params(concat!("point | a, b: a corner of two lines; distance: along a, distance2: along b", created_doc!())), CommandSpec::new("sketch.offset", "Offset", offset) @@ -50,7 +50,7 @@ pub static COMMANDS: &[CommandSpec] = &[ "distance2?: positive length expression (Two Sides: an independent distance the other way, as for `solid.extrude`). Each side is its own offset association, listed in `sides`; flip?: bool reverses the primary side; `param`/`param2` name the driving dimensions", created_doc!() )), - CommandSpec::new("sketch.offset.break", "Break Offset Association", break_offset) + CommandSpec::new("sketch.offset.break", "Break Offset Association", break_offset).icon("sketch_break") .enabled(in_sketch) .params("curve: derived offset curve id (retains geometry and makes its points editable)"), CommandSpec::new("sketch.trim", "Trim", trim) @@ -66,17 +66,17 @@ pub static COMMANDS: &[CommandSpec] = &[ .params("curve: id, at: [x,y] near the end to extend (to the nearest curve)"), CommandSpec::new("sketch.break", "Break", break_cmd) .at("SKETCH", "MODIFY") - .icon("break") + .icon("sketch_break") .enabled(in_sketch) .params(concat!("curve: id, at: [x,y] (split at intersections) | points: [[x,y]…] (1…1000 positions projected onto the curve; duplicates are merged)", created_doc!())), CommandSpec::new("sketch.scale", "Sketch Scale", scale) .at("SKETCH", "MODIFY") - .icon("scale") + .icon("sketch_scale") .enabled(in_sketch) .params("entities: [curve/point ids], base: [x,y] or point ref, factor: number"), CommandSpec::new("sketch.move", "Move/Copy Sketch Entities", move_copy) .at("SKETCH", "MODIFY") - .icon("move") + .icon("sketch_move") .enabled(in_sketch) .params(concat!("entities: [ids], translate?: [dx,dy], angle?: deg, center?: [x,y], copy?: bool; picking a text outline or linked point transforms its whole text source once, and copies remain editable text", created_doc!())), CommandSpec::new("sketch.mirror", "Mirror", mirror) diff --git a/crates/engine/src/cmd/sketch_project.rs b/crates/engine/src/cmd/sketch_project.rs index f92adc81..7d4649c7 100644 --- a/crates/engine/src/cmd/sketch_project.rs +++ b/crates/engine/src/cmd/sketch_project.rs @@ -54,10 +54,10 @@ pub static COMMANDS: &[CommandSpec] = &[ .icon("fit_section") .enabled(in_sketch) .params(concat!("body: name (a mesh or solid body): its section with the sketch plane, fitted with lines and arcs, as normal (unlinked) geometry", created_doc!())), - CommandSpec::new("sketch.break_link", "Break Link", break_link) + CommandSpec::new("sketch.break_link", "Break Link", break_link).icon("sketch_break") .enabled(in_sketch) .params("link?: link id (j1) | entities?: [curve or point ids]: linked geometry becomes normal sketch geometry"), - CommandSpec::new("sketch.auto_project", "Auto-Project Edges on Reference", auto_project) + CommandSpec::new("sketch.auto_project", "Auto-Project Edges on Reference", auto_project).icon("project") .noundo() .params("value?: bool (default toggles): project a face's edges when a sketch is created on it"), ]; diff --git a/crates/engine/src/cmd/surface.rs b/crates/engine/src/cmd/surface.rs index bde3ffa2..56c45a77 100644 --- a/crates/engine/src/cmd/surface.rs +++ b/crates/engine/src/cmd/surface.rs @@ -18,23 +18,23 @@ use crate::{Result, Session}; pub static COMMANDS: &[CommandSpec] = &[ CommandSpec::new("surface.patch", "Patch", patch) .at("SURFACE", "CREATE") - .icon("patch") + .icon("surface_patch") .params("sketch?: id|name and profiles? (planar patches of sketch regions) | edges: [[x,y,z]] (points on a closed loop of a body's edges: one face on them, planar when the loop is, else a smooth B-spline surface) and body?; continuity?: g0|g1|g2 (edges only, default g0: how the patch meets the faces beside the loop — g1 their tangent planes, g2 also their curvature across the edges) or [{edge: [x,y,z], g: g0|g1|g2}] (those edges so, the rest g0) or {all, edges}; points?: [[x,y,z]] (edges only, up to 64: points the patch passes through); crown?: number −0.9…3 (default 0: only a fitted patch, whose middle becomes fuller by this many times the loop's size — the bump vanishes on the loop, so the face still meets the picked edges and the continuity across them; a loop that is planar and g0 is the exact plane whatever the crown; with points, crown needs a g1 or g2 edge); name?, body_name?"), CommandSpec::new("surface.stitch", "Stitch", stitch) .at("SURFACE", "MODIFY") - .icon("stitch") + .icon("surface_stitch") .params("bodies: [names] (surfaces sewn into the first; a solid when they close); tolerance?: expr (default: tight)"), CommandSpec::new("surface.thicken", "Thicken", thicken) .at("SURFACE", "CREATE") - .icon("thicken") + .icon("surface_thicken") .params("bodies: [surface names]; thickness: expr (negative: against the normal); symmetric?: bool; operation?: new|join|cut|intersect; targets?"), CommandSpec::new("surface.extend", "Extend", extend) .at("SURFACE", "MODIFY") - .icon("extend") + .icon("surface_extend") .params("body: surface name; edges: [[x,y,z]] (points on boundary edges to push out); distance: expr (unless up_to); mode?: natural (the surface itself continued: exact for planes and surfaces of revolution, a B-spline's end spans continued; the default) | tangent (a ruled run-off face along each edge, G1) | curvature (a run-off face keeping the surface's curvature across the edge, G2); up_to?: plane (XY|XZ|YZ|plane name|{origin, normal}) | {body, point} (a face, its surface extended) | \"bounds\" (natural: back to the surface's own bounds, undoing a trim); neighbours?: bool (default true: the edges next to an extended edge are lengthened to meet it; false: the extension's sides run square to the edge)"), CommandSpec::new("surface.trim", "Trim", trim) .at("SURFACE", "MODIFY") - .icon("trim") + .icon("surface_trim") .params("body: surface name; plane: XY|XZ|YZ|plane name|{origin, normal} or tool: {body, point} (a face, its surface extended); keep: [x,y,z] (a point on the side to keep)"), CommandSpec::new("surface.extrude", "Surface Extrude", surface_extrude) .at("SURFACE", "CREATE") @@ -58,7 +58,7 @@ pub static COMMANDS: &[CommandSpec] = &[ .params("faces: [[x,y,z]] (points on the faces); body?; distance: expr (signed, along the faces' normals; 0 copies the faces); chain?: bool (each pick's whole smooth chain of faces); tolerance?: expr (how far a fitted face may stray, default 0.01 mm). Planes and cylinders move exactly; other faces are fitted. A new surface body (a solid where its faces close)"), CommandSpec::new("surface.ruled", "Ruled Surface", ruled) .at("SURFACE", "CREATE") - .icon("ruled") + .icon("surface_ruled") .params("body; edges: [[x,y,z]] (points on a chain of its edges, open or closed); distance: expr (how far the strip reaches); mode?: normal|tangent|angle|direction (default normal: along the normal of the face beside each edge; tangent: continuing it); angle?: expr (angle mode: from `direction`, else from the face's normal, turned away from the face; 90 deg from the normal is tangent); direction?: [x,y,z] (direction mode: the rulings; angle mode: the reference, e.g. a pull direction for a draft); face?: [x,y,z] (a point on the face the directions come from; default: the face beside the most picked edges); flip?: bool (the other side); corners?: mitre|round (sharp corners of the chain; inside corners are always cut back). Lines and arcs give exact planes, cylinders and cones, other edges a fitted ruled B-spline. A new surface body"), CommandSpec::new("surface.blend", "Blend Surface", blend) .at("SURFACE", "CREATE") @@ -66,19 +66,19 @@ pub static COMMANDS: &[CommandSpec] = &[ .params("from, to: {body, edges: [[x,y,z]]} (points on the edges of one chain of each body: two picks on the same edge is that one edge, more chain up); continuity?: [g0|g1|g2|g3, g0|g1|g2|g3] (each end, default g1: how much of the surface at the picked edges the blend repeats — the tangent plane, the curvature, or how that curvature changes); bulge?: [number, number] (default 0: how full each end leaves, -0.9 flattest to 3 fullest); align?: [[x,y,z], [x,y,z]] (a point each end leaves towards, when the faces at the picks do not say); stitch?: bool (sew the blend onto its two bodies where the boundaries meet, replacing them); name?, body_name?"), CommandSpec::new("surface.untrim", "Untrim", untrim) .at("SURFACE", "MODIFY") - .icon("untrim") + .icon("surface_untrim") .params("face: {body, point}; holes?: all|[[x,y,z]] (points on the inner loops to remove; default: all); outer?: bool (restore the outer boundary to the surface's natural bounds: a free-form surface's own, an analytic surface's span of the face); in_place?: bool (untrim a surface body's face where it is; a solid's face is always copied out). A surface body; the face keeps its name"), CommandSpec::new("surface.unstitch", "Unstitch", unstitch) .at("SURFACE", "MODIFY") - .icon("unstitch") + .icon("surface_unstitch") .params("body; faces?: [[x,y,z]] (points on the faces to take out, each its own surface body; the rest stay together under the body's name; default: every face its own surface body). A solid comes apart into surfaces"), CommandSpec::new("surface.reverse_normal", "Reverse Normal", reverse_normal) .at("SURFACE", "MODIFY") - .icon("reverse_normal") + .icon("surface_reverse_normal") .params("body (a surface); faces?: [[x,y,z]] (points on the faces to turn over; default: all of them)"), CommandSpec::new("surface.fair", "Fair Surface", fair) .at("SURFACE", "MODIFY") - .icon("spline_cv") + .icon("surface_fair") .params("body: surface name; faces?: [[x,y,z]] points on the faces to fair (default: every face); strength?: number (0..1 of the full fairing step, default 1); iterations?: number (1..1000 steps, default 1); keep_edges?: bool (default true: hold the boundary row and the row next to it, so a face keeps meeting its neighbours in position and in tangent); tolerance?: expr mm (how far a control vertex may move, default 0.001 of the body's size). Moves the interior control vertices of each B-spline face to flatten its curvature variation while the boundary stays put: the face keeps its edges and the body its shape within the tolerance, and the warning in the reply carries the energy before and after; name?"), planned!("surface.fillet", "Surface Fillet", "769") .at("SURFACE", "MODIFY") diff --git a/crates/engine/src/cmd/surface_boundary.rs b/crates/engine/src/cmd/surface_boundary.rs index 6bbc3f1a..79532302 100644 --- a/crates/engine/src/cmd/surface_boundary.rs +++ b/crates/engine/src/cmd/surface_boundary.rs @@ -21,7 +21,7 @@ const MAX_SAMPLES: usize = 512; pub static COMMANDS: &[CommandSpec] = &[CommandSpec::new("surface.boundary", "Boundary Surface", boundary) .at("SURFACE", "CREATE") - .icon("boundary") + .icon("surface_boundary") .params( "sketch?: id|name (default: the sketch being edited); curves: [2 to 4 sketch curve ids in loop order] (each one ends where the next begins and the last where the first does; the patch runs through them and leaves each boundary along the tangent across it); continuity?: g0|g1|g2|g3 | [one per curve] (default g1: how much of the surface across each boundary the patch repeats — a sketch curve's own plane, so g1 and up hold exactly and g0 lets the middle bow); crown?: number -0.9…3 (default 0: how full the middle is, without touching a boundary); samples?: 8…512 stations per boundary (default 25); name?, body_name? (a surface body)", )]; diff --git a/crates/engine/src/tests/robustness.rs b/crates/engine/src/tests/robustness.rs index 48a06a6b..229e3bee 100644 --- a/crates/engine/src/tests/robustness.rs +++ b/crates/engine/src/tests/robustness.rs @@ -558,6 +558,7 @@ fn blend_corners_where_edges_meet_on_a_curved_wall() { fn a_boundary_found_across_a_seam_is_not_used() { let r = fuzz::run(20, 8); assert_eq!(r.outcome.kind, "ok", "seed 20: {:?}\n{}", r.outcome, r.script()); + assert!(r.steps.iter().any(|s| s.kind == "circular pattern"), "the pattern must build, not be skipped as unsupported: {:?}", r.unsupported); } /// Seed 33 ends in a fillet on its second body (#854's corner blends build it differently from diff --git a/crates/kernel/src/ops.rs b/crates/kernel/src/ops.rs index f84c768b..57f266ee 100644 --- a/crates/kernel/src/ops.rs +++ b/crates/kernel/src/ops.rs @@ -300,6 +300,14 @@ thread_local! { /// The result carries its exact history ([`Body::history`]) over the operands `[a, b]`, /// whichever way it was made, or none where a step it took does not report one. pub fn boolean(a: &Body, b: &Body, op: BoolOp) -> Result> { + // Earlier coarse bores can leave a junction a later pattern copy cannot intersect. + // Resolve conical-cut junctions from the first copy, while the operands are unchanged. + if op == BoolOp::Cut && !APART.with(|c| c.get()) && conical_cut_junction(a, b) { + let refined = |a: &Body, b: &Body, op| boolean_whole_with_refinement(a, b, op, true); + if let Ok(result) = boolean_via(a, b, op, &refined) { + return Ok(result); + } + } let r = boolean_via(a, b, op, &boolean_whole); if r.is_ok() || !crate::tangent_cylinders::needed(a, b) { return r; @@ -312,6 +320,19 @@ pub fn boolean(a: &Body, b: &Body, op: BoolOp) -> Result> { boolean_via(a, b, op, &split).or(r) } +/// A conical cutter meeting a body with an earlier conical cut needs finer tracing at +/// their junctions. Guard the analytic recognition as well as the Boolean itself. +fn conical_cut_junction(a: &Body, b: &Body) -> bool { + guard("conical cut junction", || { + let conical = |body: &Body| { + let tol = (body.size() * 1e-5).max(1e-7); + body.solid.face_iter().any(|face| matches!(crate::offset::surf_of(face, tol), Some(crate::offset::Surf::Cone { .. }))) + }; + Ok(conical(b) && conical(a)) + }) + .unwrap_or(false) +} + /// [`boolean`], the boolean of the whole bodies taken by `whole`. fn boolean_via(a: &Body, b: &Body, op: BoolOp, whole: &dyn Fn(&Body, &Body, BoolOp) -> Result>) -> Result> { let (mut r, mut diagnostics) = capture_boolean_diagnostics(|| whole(a, b, op)); @@ -548,6 +569,10 @@ fn swept_prism(a: &Body, b: &Body, op: BoolOp) -> Option { } fn boolean_whole(a: &Body, b: &Body, op: BoolOp) -> Result> { + boolean_whole_with_refinement(a, b, op, false) +} + +fn boolean_whole_with_refinement(a: &Body, b: &Body, op: BoolOp, refine: bool) -> Result> { if let Some(body) = swept_prism(a, b, op) { return Ok(Some(body)); } @@ -622,6 +647,25 @@ fn boolean_whole(a: &Body, b: &Body, op: BoolOp) -> Result> { }; (consistent, v) }; + if refine { + let keep: Vec> = a.split_keep().iter().chain(b.split_keep()).cloned().collect(); + // Bounded to two finer meshes; no tool displacement or relaxed acceptance tests. + for k in [1e-5, 1e-6] { + let tol = (size * k).max(1e-6); + if let Ok((solid, history)) = truck_boolean(a, b, op, None, tol) + && !solid.boundaries().is_empty() + && let Ok(body) = guard("refined boolean result", || recorded::truck_result(solid, history, size, &keep, &counts)) + && check(&body).0 + && crate::boolean_guard::check_retry(a, b, op, Some(&body)).is_ok() + { + let mut event = BooleanFallback::new(BooleanStrategy::RefinedTolerance); + event.tolerance_mm = tol; + record(event); + return Ok(Some(body)); + } + } + return Err(KernelError::Failed("no reliable result at refined tolerances".into())); + } // One body clear of or inside the other: no intersection needed. // Kept for the end when the facetings disagree with it: bodies that touch along curved // faces can pass through each other slightly, and often can't be intersected. diff --git a/crates/kernel/tests/boolean_props.proptest-regressions b/crates/kernel/tests/boolean_props.proptest-regressions index 83c0147e..3208747f 100644 --- a/crates/kernel/tests/boolean_props.proptest-regressions +++ b/crates/kernel/tests/boolean_props.proptest-regressions @@ -5,3 +5,4 @@ # It is recommended to check this file in to source control so that # everyone who runs the test benefits from these saved cases. cc 739d5cbd9e471e0ff65bc2b7c1b9a323c2e769939c73def2e74fb17b0536c1d0 # shrinks to a = [0.0, 2.5, 30.0], sa = [22.5, 5.0, 5.0], b = [22.5, 7.5, 15.0], sb = [5.0, 5.0, 17.5] +cc 2adfe5ac6b6916fa22a415348bdd7ece29ad33e8114b94dea86ba8bc39156dee # shrinks to a = Box { at: [15.0, 22.5, 0.0], size: [5.0, 5.0, 17.5] }, b = Box { at: [2.5, 12.5, 0.0], size: [12.5, 10.0, 5.0] } diff --git a/crates/kernel/tests/boolean_props.rs b/crates/kernel/tests/boolean_props.rs index 57a7d465..397308bf 100644 --- a/crates/kernel/tests/boolean_props.rs +++ b/crates/kernel/tests/boolean_props.rs @@ -67,6 +67,18 @@ fn prim(c: impl Strategy + Clone, s: impl Strategy + C fn grid_prim() -> impl Strategy { prim(grid(), grid_size()) } + +/// A union across only a box edge or corner is non-manifold, so the kernel may +/// refuse it. This is the same analytic boundary case allowed by box_box_exact. +fn edge_only_boxes(a: &Prim, b: &Prim) -> bool { + let (Prim::Box { at: a, size: sa }, Prim::Box { at: b, size: sb }) = (a, b) else { return false }; + let gap = [0, 1, 2].map(|i| a[i].max(b[i]) - (a[i] + sa[i]).min(b[i] + sb[i])); + gap.iter().all(|g| *g <= 0.0) && gap.iter().filter(|g| **g == 0.0).count() >= 2 +} + +fn edge_contact_refusal(a: &Prim, b: &Prim, op: BoolOp, result: &Result) -> bool { + op == BoolOp::Union && edge_only_boxes(a, b) && result.as_ref().is_err_and(|e| e.to_string().contains("zero-thickness edge contact")) +} fn free_prim() -> impl Strategy { prim(free(), free_size()) } @@ -95,8 +107,13 @@ fn close(x: f64, y: f64, scale: f64) -> bool { fn identity(a: &Prim, b: &Prim) -> Result<(), TestCaseError> { let (ba, bb) = (a.body(), b.body()); let (va, vb) = (vol(&ba), vol(&bb)); - let (vu, vi, vc) = (run(&ba, &bb, BoolOp::Union)?, run(&ba, &bb, BoolOp::Intersect)?, run(&ba, &bb, BoolOp::Cut)?); - prop_assert!(close(vu + vi, va + vb, va + vb), "union+intersect {} != A+B {} (A {va:.3}, B {vb:.3}, U {vu:.3}, I {vi:.3})", vu + vi, va + vb); + let (vu, vi, vc) = (run(&ba, &bb, BoolOp::Union), run(&ba, &bb, BoolOp::Intersect)?, run(&ba, &bb, BoolOp::Cut)?); + if edge_contact_refusal(a, b, BoolOp::Union, &vu) { + prop_assert!(close(vi, 0.0, va + vb), "edge-only contact must have no intersection volume"); + } else { + let vu = vu?; + prop_assert!(close(vu + vi, va + vb, va + vb), "union+intersect {} != A+B {} (A {va:.3}, B {vb:.3}, U {vu:.3}, I {vi:.3})", vu + vi, va + vb); + } prop_assert!(close(vc, va - vi, va + vb), "cut {vc:.3} != A-I {:.3}", va - vi); Ok(()) } @@ -137,7 +154,11 @@ proptest! { let (ba, bb) = (a.body(), b.body()); let s = vol(&ba) + vol(&bb); for op in [BoolOp::Union, BoolOp::Intersect] { - let (ab, ba_) = (run(&ba, &bb, op)?, run(&bb, &ba, op)?); + let (ab, ba_) = (run(&ba, &bb, op), run(&bb, &ba, op)); + if edge_contact_refusal(&a, &b, op, &ab) && edge_contact_refusal(&a, &b, op, &ba_) { + continue; + } + let (ab, ba_) = (ab?, ba_?); prop_assert!(close(ab, ba_, s), "{op:?}: A,B gives {ab:.4} but B,A gives {ba_:.4}"); } } @@ -152,7 +173,11 @@ proptest! { let (ta, tb) = (mv(&ba), mv(&bb)); let s = vol(&ba) + vol(&bb); for op in [BoolOp::Union, BoolOp::Intersect, BoolOp::Cut] { - let (here, there) = (run(&ba, &bb, op)?, run(&ta, &tb, op)?); + let (here, there) = (run(&ba, &bb, op), run(&ta, &tb, op)); + if edge_contact_refusal(&a, &b, op, &here) && edge_contact_refusal(&a, &b, op, &there) { + continue; + } + let (here, there) = (here?, there?); prop_assert!(close(here, there, s), "{op:?} at origin {here:.4}, moved by {off} gives {there:.4}"); } } @@ -169,7 +194,11 @@ proptest! { let (ba, bb, ka, kb) = (a.body(), b.body(), sc(&a).body(), sc(&b).body()); let s = vol(&ba) + vol(&bb); for op in [BoolOp::Union, BoolOp::Intersect, BoolOp::Cut] { - let (one, big) = (run(&ba, &bb, op)?, run(&ka, &kb, op)? / (k * k * k)); + let (one, big) = (run(&ba, &bb, op), run(&ka, &kb, op)); + if edge_contact_refusal(&a, &b, op, &one) && edge_contact_refusal(&a, &b, op, &big) { + continue; + } + let (one, big) = (one?, big? / (k * k * k)); prop_assert!(close(one, big, s), "{op:?} at scale 1 gives {one:.4}, at scale {k} gives {big:.4} (normalised)"); } } @@ -233,6 +262,23 @@ proptest! { // Cases the property tests found, as plain tests. Ignored until the kernel bug is fixed: run them // with `cargo test -p solidcraft-kernel --test boolean_props -- --ignored`. +/// The saved commutativity case is an intentional non-manifold join refusal, +/// not a failure to join overlapping volumes. Intersection and cut remain valid. +#[test] +fn edge_only_box_join_is_refused_in_both_orders() { + let a = Prim::Box { at: [15.0, 22.5, 0.0], size: [5.0, 5.0, 17.5] }; + let b = Prim::Box { at: [2.5, 12.5, 0.0], size: [12.5, 10.0, 5.0] }; + assert!(edge_only_boxes(&a, &b)); + assert!(!edge_only_boxes(&a, &Prim::Box { at: [15.0, 22.5, 0.0], size: [5.0, 5.0, 5.0] })); + let (a, b) = (a.body(), b.body()); + for (left, right) in [(&a, &b), (&b, &a)] { + let error = run(left, right, BoolOp::Union).expect_err("an edge-only join is non-manifold"); + assert!(error.to_string().contains("zero-thickness edge contact"), "{error}"); + assert_eq!(run(left, right, BoolOp::Intersect).unwrap(), 0.0); + assert!(close(run(left, right, BoolOp::Cut).unwrap(), vol(left), vol(left))); + } +} + fn check(r: Result<(), TestCaseError>) { if let Err(e) = r { panic!("{e}"); diff --git a/crates/ui-egui/Cargo.toml b/crates/ui-egui/Cargo.toml index 1fc0cf8c..38c53a70 100644 --- a/crates/ui-egui/Cargo.toml +++ b/crates/ui-egui/Cargo.toml @@ -25,6 +25,8 @@ semver = { workspace = true } sha2 = { workspace = true } minisign-verify = "0.3" zip = { workspace = true } +# Rasterises the icon SVGs (assets/icons/ui/design/svg) at the size they are drawn. +tiny-skia = { workspace = true } [target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies] # The offscreen viewport render test opens a device without a window. @@ -32,3 +34,5 @@ pollster = { workspace = true } [build-dependencies] serde_json = { workspace = true } +# Reads assets/icons/ui/design/palette.toml and aliases.toml into compiled tables. +toml = { workspace = true } diff --git a/crates/ui-egui/build.rs b/crates/ui-egui/build.rs index 63bc3b20..797da051 100644 --- a/crates/ui-egui/build.rs +++ b/crates/ui-egui/build.rs @@ -36,6 +36,10 @@ fn main() { } println!("cargo:rerun-if-changed=build.rs"); scenario_tests(); + if let Err(e) = icon_data() { + eprintln!("icon data: {e}"); + std::process::exit(1); + } } /// `$OUT_DIR/scenario_tests.rs`: a `#[test]` per scenario file, named after it; a scenario with @@ -67,3 +71,155 @@ fn scenario_tests() { let path = std::path::Path::new(&std::env::var("OUT_DIR").unwrap_or_else(|_| ".".into())).join("scenario_tests.rs"); let _ = std::fs::write(path, out); } + +/// `assets/icons/ui/design`: the SVG masters, `palette.toml` (every colour role in light and dark, +/// and the accents) and `aliases.toml`, compiled into `$OUT_DIR/icon_data.rs` for `icon_svg.rs`. +/// A missing hex, a role an accent does not know or an alias to a missing master fails the build. +fn icon_data() -> Result<(), String> { + use std::fmt::Write as _; + let manifest = std::env::var("CARGO_MANIFEST_DIR").map_err(|e| e.to_string())?; + let root = std::path::Path::new(&manifest).join("../../assets/icons/ui/design"); + let read = |name: &str| std::fs::read_to_string(root.join(name)).map_err(|e| format!("{name}: {e}")); + let table = |name: &str| read(name)?.parse::().map_err(|e| format!("{name}: {e}")); + let hex = |v: Option<&toml::Value>, what: &str| -> Result { + let s = v.and_then(toml::Value::as_str).ok_or_else(|| format!("{what}: missing hex"))?; + let ok = s.len() == 7 && s.starts_with('#') && s[1..].bytes().all(|b| b.is_ascii_hexdigit()); + if ok { Ok(s.to_ascii_lowercase()) } else { Err(format!("{what}: `{s}` is not #rrggbb")) } + }; + let rgb = |h: &str| format!("[0x{}, 0x{}, 0x{}]", &h[1..3], &h[3..5], &h[5..7]); + let palette = table("palette.toml")?; + let roles = palette.get("roles").and_then(toml::Value::as_table).ok_or("palette.toml: no [roles]")?; + let names: Vec<&String> = roles.keys().collect(); + let mut out = String::new(); + let _ = writeln!(out, "pub(crate) const ROLE_NAMES: &[&str] = &{:?};", names); + let (mut lights, mut darks, mut light_hex) = (Vec::new(), Vec::new(), Vec::new()); + for (name, role) in roles { + let l = hex(role.get("light"), &format!("role {name} light"))?; + let d = hex(role.get("dark"), &format!("role {name} dark"))?; + lights.push(rgb(&l)); + darks.push(rgb(&d)); + light_hex.push(hex(role.get("source").or_else(|| role.get("light")), &format!("role {name} source"))?); + } + let _ = writeln!(out, "pub(crate) const ROLE_SOURCE_HEX: &[&str] = &{:?};", light_hex); + let _ = writeln!(out, "pub(crate) const PALETTE: [&[[u8; 3]]; 2] = [&[{}], &[{}]];", lights.join(", "), darks.join(", ")); + let mut accent_names = Vec::new(); + let mut accents = Vec::new(); + if let Some(table) = palette.get("accents").and_then(toml::Value::as_table) { + for (acc, roles_of) in table { + let roles_of = roles_of.as_table().ok_or_else(|| format!("accent {acc}: not a table"))?; + let mut items = Vec::new(); + for (role, v) in roles_of { + let idx = names.iter().position(|n| *n == role).ok_or_else(|| format!("accent {acc}: unknown role {role}"))?; + let v = if let Some(link) = v.get("use").and_then(toml::Value::as_str) { + roles_of.get(link).ok_or_else(|| format!("accent {acc}: unknown linked role {link}"))? + } else { + v + }; + let l = hex(v.get("light"), &format!("accent {acc} {role} light"))?; + let d = hex(v.get("dark"), &format!("accent {acc} {role} dark"))?; + items.push(format!("({idx}, {}, {})", rgb(&l), rgb(&d))); + } + accent_names.push(acc.clone()); + accents.push(format!("&[{}]", items.join(", "))); + } + } + let _ = writeln!(out, "pub(crate) const ACCENT_NAMES: &[&str] = &{:?};", accent_names); + let _ = writeln!(out, "pub(crate) const ACCENTS: &[&[(usize, [u8; 3], [u8; 3])]] = &[{}];", accents.join(", ")); + // UI tokens and SVGs share the same primary strokes, including accessibility overrides. + for (constant, value) in [ + ("SKETCH_PRIMARY", palette.get("accents").and_then(|v| v.get("sketch"))), + ("CONSTRAINT_PRIMARY", palette.get("accents").and_then(|v| v.get("constraint"))), + ("CONSTRUCTION_PRIMARY", palette.get("accents").and_then(|v| v.get("construction"))), + ("SKETCH_RED_GREEN", palette.get("variants").and_then(|v| v.get("red-green")).and_then(|v| v.get("sketch"))), + ("CONSTRUCTION_RED_GREEN", palette.get("variants").and_then(|v| v.get("red-green")).and_then(|v| v.get("construction"))), + ("CONSTRUCTION_BLUE_YELLOW", palette.get("variants").and_then(|v| v.get("blue-yellow")).and_then(|v| v.get("construction"))), + ("CONSTRAINT_BLUE_YELLOW", palette.get("variants").and_then(|v| v.get("blue-yellow")).and_then(|v| v.get("constraint"))), + ] { + let role = value.and_then(|v| v.get("active-3")); + let l = hex(role.and_then(|v| v.get("light")), constant)?; + let d = hex(role.and_then(|v| v.get("dark")), constant)?; + let _ = writeln!(out, "pub(crate) const {constant}: [[u8; 3]; 2] = [{}, {}];", rgb(&l), rgb(&d)); + } + let mut variants = Vec::new(); + if let Some(groups) = palette.get("variants").and_then(toml::Value::as_table) { + for (vision, group) in groups { + if !matches!(vision.as_str(), "red-green" | "blue-yellow") { + return Err(format!("unknown icon colour-vision group {vision}")); + } + for (accent, overrides) in group.as_table().ok_or("invalid variant group")? { + let a = accent_names.iter().position(|n| n == accent).ok_or_else(|| format!("variant: unknown accent {accent}"))?; + for (role, value) in overrides.as_table().ok_or("invalid variant roles")? { + let r = names.iter().position(|n| *n == role).ok_or_else(|| format!("variant: unknown role {role}"))?; + let l = hex(value.get("light"), "variant light")?; + let d = hex(value.get("dark"), "variant dark")?; + variants.push(format!("({vision:?}, {a}, {r}, {}, {})", rgb(&l), rgb(&d))); + } + } + } + } + let _ = writeln!(out, "pub(crate) const ACCENT_VARIANTS: &[(&str, usize, usize, [u8; 3], [u8; 3])] = &[{}];", variants.join(", ")); + // The masters, by family. + let mut svgs = Vec::new(); + let mut ids = std::collections::BTreeSet::new(); + let svg_dir = root.join("svg"); + println!("cargo:rerun-if-changed={}", root.display()); + for fam in ["model", "small", "flat", "workspace"] { + let dir = svg_dir.join(fam); + let mut files: Vec<_> = + std::fs::read_dir(&dir).map_err(|e| format!("{}: {e}", dir.display()))?.filter_map(Result::ok).map(|e| e.path()).collect(); + files.sort(); + for f in files.iter().filter(|f| f.extension().is_some_and(|e| e == "svg")) { + let id = f.file_stem().and_then(|s| s.to_str()).ok_or("bad file name")?; + if fam != "small" { + ids.insert(id.to_owned()); + } + let abs = f.canonicalize().map_err(|e| e.to_string())?; + println!("cargo:rerun-if-changed={}", abs.display()); + svgs.push(format!("({id:?}, {fam:?}, include_str!({:?}))", abs.display().to_string())); + } + } + let _ = writeln!(out, "pub(crate) static SVGS: &[(&str, &str, &str)] = &[{}];", svgs.join(", ")); + let aliases = table("aliases.toml")?; + let mut al = Vec::new(); + if let Some(t) = aliases.get("alias").and_then(toml::Value::as_table) { + for (alias, v) in t { + let (master, accent) = match v { + toml::Value::String(m) => (m.clone(), String::new()), + toml::Value::Table(t) => ( + t.get("master").and_then(toml::Value::as_str).ok_or_else(|| format!("alias {alias}: no master"))?.to_string(), + t.get("accent").and_then(toml::Value::as_str).unwrap_or("").to_string(), + ), + _ => return Err(format!("alias {alias}: expected a string or a table")), + }; + if !accent.is_empty() && !accent_names.contains(&accent) { + return Err(format!("alias {alias}: unknown accent {accent}")); + } + if !ids.contains(&master) { + return Err(format!("alias {alias}: unknown master {master}")); + } + al.push(format!("({alias:?}, {master:?}, {accent:?})")); + } + } + let _ = writeln!(out, "pub(crate) const ALIASES: &[(&str, &str, &str)] = &[{}];", al.join(", ")); + if let Some(t) = aliases.get("alias").and_then(toml::Value::as_table) { + ids.extend(t.keys().cloned()); + } + let mut defaults = std::collections::BTreeMap::new(); + if let Some(groups) = palette.get("icon-accents").and_then(toml::Value::as_table) { + for (accent, icons) in groups { + if !accent_names.contains(accent) { + return Err(format!("icon-accents: unknown accent {accent}")); + } + for icon in icons.as_array().ok_or("icon-accents: expected icon array")? { + let icon = icon.as_str().ok_or("icon-accents: expected icon id")?; + if !ids.contains(icon) || defaults.insert(icon, accent).is_some() { + return Err(format!("icon-accents: unknown or repeated icon {icon}")); + } + } + } + } + let defaults: Vec<_> = defaults.iter().map(|(id, accent)| format!("({id:?}, {accent:?})")).collect(); + let _ = writeln!(out, "pub(crate) const ICON_ACCENTS: &[(&str, &str)] = &[{}];", defaults.join(", ")); + let dest = std::path::Path::new(&std::env::var("OUT_DIR").map_err(|e| e.to_string())?).join("icon_data.rs"); + std::fs::write(dest, out).map_err(|e| e.to_string()) +} diff --git a/crates/ui-egui/src/browser.rs b/crates/ui-egui/src/browser.rs index ea104ed9..dd4c35b1 100644 --- a/crates/ui-egui/src/browser.rs +++ b/crates/ui-egui/src/browser.rs @@ -1363,7 +1363,7 @@ fn entries(app: &SolveApp, comp: u64, folder: &str) -> Vec { key: f.id.to_string(), row_key: format!("p:{}", f.id), label: f.name.clone(), - icon: "plane", + icon: crate::timeline::icon_of(&f.kind), visible: !app.session.visibility.hidden_origin.contains(&f.name), selected: sel.iter().any(|x| matches!(x, Sel::Plane { name } if *name == f.name)), color: None, diff --git a/crates/ui-egui/src/colour_tokens_tests.rs b/crates/ui-egui/src/colour_tokens_tests.rs index e4fbc6df..f6fc0bb5 100644 --- a/crates/ui-egui/src/colour_tokens_tests.rs +++ b/crates/ui-egui/src/colour_tokens_tests.rs @@ -2,11 +2,11 @@ //! axis tags, the surface analysis colours and every colour an icon draws. These tests hold that //! the tokens equal what was drawn before, and that a new colour cannot skip them. -use egui::{Color32, Pos2, Rect, vec2}; +use egui::Color32; use solidcraft_engine::render::Rgb; use solidcraft_engine::view::colors; -use crate::theme::{IconColors, Tokens}; +use crate::theme::Tokens; fn rgb(c: Rgb) -> Color32 { Color32::from_rgb(c.0, c.1, c.2) @@ -56,103 +56,10 @@ fn analysis_colours_reach_the_shader_in_its_order() { assert_eq!(crate::gpu::analysis_colors(&striped)[32], 1.0); } -/// Every name a `match` arm in the icon source draws. -fn icon_names() -> Vec<&'static str> { - let src = include_str!("../../../assets/icons/ui/source/icons.rs"); - let body = src.split("#[cfg(test)]").next().unwrap_or(src); - let mut names = Vec::new(); - for line in body.lines() { - let line = line.trim_start(); - if !line.starts_with('"') { - continue; - } - let Some((pattern, _)) = line.split_once("=>") else { continue }; - let pattern = pattern.split(" if ").next().unwrap_or(pattern); - for part in pattern.split('|') { - let part = part.trim(); - if part.len() > 2 && part.starts_with('"') && part.ends_with('"') { - names.push(&part[1..part.len() - 1]); - } - } - } - names.sort_unstable(); - names.dedup(); - names -} - -/// Marker icon colours: every field its own colour, from `seed`. -fn marker_icons(seed: u8) -> IconColors { - let c = |i: u8| Color32::from_rgb(seed, i, seed ^ i); - IconColors { - sheet: c(1), - sheet_2: c(2), - sheet_light: c(3), - sheet_line: c(4), - highlight: c(5), - glint: c(6), - folder: c(7), - plane: c(8), - fold: c(9), - stripe: c(10), - sky: c(11), - floor: c(12), - paint: [c(13), c(14), c(15)], - axes: [c(16), c(17), c(18)], - access: [c(19), c(20)], - draft: [c(21), c(22)], - curvature: [c(23), c(24), c(25)], - interference: c(26), - warning: c(27), - agent: c(28), - marker_halo: c(29), - } -} - -/// The colours an icon draws (premultiplied `#RR_GG_BB_AA`, in drawing order). -fn drawn_colours(name: &str, seed: u8) -> Vec { - let ctx = egui::Context::default(); - let input = egui::RawInput { screen_rect: Some(Rect::from_min_size(Pos2::ZERO, vec2(100.0, 100.0))), ..Default::default() }; - let icons = marker_icons(seed); - let out = ctx.run_ui(input, |ui| { - let r = Rect::from_min_size(Pos2::ZERO, vec2(24.0, 24.0)); - let ink = Color32::from_rgb(seed, 101, 1); - let fill = Color32::from_rgb(seed, 102, 2); - let accent = Color32::from_rgb(seed, 103, 3); - crate::icons::paint_with(ui.painter(), r, name, ink, fill, accent, &icons); - }); - let text = format!("{:?}", out.shapes); - let b = text.as_bytes(); - let mut colours = Vec::new(); - for i in 0..b.len() { - let hex = |k: usize| b.get(i + k).is_some_and(u8::is_ascii_hexdigit); - if b[i] == b'#' && (1..=11).all(|k| if k % 3 == 0 { b.get(i + k) == Some(&b'_') } else { hex(k) }) { - colours.push(text[i..i + 12].to_string()); - } - } - colours -} - -/// "All icons past, present and future" follow the colour vision mode: an icon may draw only the -/// colours its caller passes (ink, fill, accent) and the `IconColors` tokens, or colours made -/// from them. Each icon is drawn twice with different marker colours; a colour that comes out the -/// same both times was written into the icon, and a mode could not reach it. -#[test] -fn every_icon_draws_only_colours_a_mode_can_reach() { - let names = icon_names(); - assert!(names.len() > 150, "icon names were not found in the icon source: {}", names.len()); - let mut fixed = Vec::new(); - for name in &names { - let (a, b) = (drawn_colours(name, 40), drawn_colours(name, 200)); - assert_eq!(a.len(), b.len(), "{name}: the two drawings differ in shape"); - for (x, y) in a.iter().zip(&b) { - if x == y && !x.ends_with("_00") { - fixed.push(format!("{name}: {x}")); - } - } - } - fixed.dedup(); - assert!(fixed.is_empty(), "icons that draw a colour of their own: {fixed:?}"); -} +// Icons no longer take their colours from the caller or from `IconColors`: the SVG masters name +// palette roles (`assets/icons/ui/design/palette.toml`, light and dark, plus accents) that +// `icon_svg` resolves. `icon_svg`'s own tests hold that every master parses and that only the +// palette's colours exist; the colour vision modes do not reach them yet. /// The most colour literals each UI file may hold, and why. None of them carries a meaning that a /// colour vision mode must change. A file with more fails here: make the new colour a token in @@ -171,6 +78,7 @@ const ALLOWED_LITERALS: &[(&str, usize, &str)] = &[ ("documents.rs", 4, "white hover overlays on document tabs"), ("frame.rs", 1, "the window shadow"), ("gizmo.rs", 2, "the gizmo's neutral grey ball"), + ("icons.rs", 1, "the white tint that carries an icon image's opacity"), ("home.rs", 4, "white ink on accent buttons, and thumbnail tints"), ("ref_images.rs", 1, "a reference image's opacity tint"), ("sketch_palette.rs", 1, "white text on the accent button"), diff --git a/crates/ui-egui/src/colour_vision_tests.rs b/crates/ui-egui/src/colour_vision_tests.rs index c0ea5f5c..5bba2ea7 100644 --- a/crates/ui-egui/src/colour_vision_tests.rs +++ b/crates/ui-egui/src/colour_vision_tests.rs @@ -4,8 +4,9 @@ //! The instruments are checked first: CIEDE2000 on the 34 pairs Sharma, Wu and Dalal published //! with their paper, Brettel on white and its own anchors. //! -//! When a token edit breaks a mode here, give that token a value in the mode's table -//! (`assets/styles/ui/palettes.rs`) that passes. +//! When a token edit breaks a mode here, adjust its measured variant. Shared icon accents live +//! in `assets/icons/ui/design/palette.toml`; `assets/styles/ui/palettes.rs` links those generated +//! values to UI tokens and owns the remaining UI-only colour-vision adjustments. use std::collections::BTreeSet; @@ -627,14 +628,14 @@ fn each_mode_changes_only_its_table() { "preview_sketch", ]; let mut red_green_light = red_green_both.to_vec(); - red_green_light.extend(["sketch_projected", "sketch_locked", "timeline_item_error"]); + red_green_light.extend(["sketch_projected", "sketch_locked", "timeline_item_error", "construction_geom"]); for (dark, mode, want) in [ (false, ColorVision::Protan, set(&red_green_light)), (true, ColorVision::Protan, set(&red_green_both)), (false, ColorVision::Deutan, set(&red_green_light)), (true, ColorVision::Deutan, set(&red_green_both)), - (false, ColorVision::Tritan, set(&["sketch_locked", "box_crossing", "preview_sketch", "analysis.hatch"])), - (true, ColorVision::Tritan, set(&["sketch_locked", "box_crossing", "glyph_text", "preview_sketch", "analysis.hatch"])), + (false, ColorVision::Tritan, set(&["sketch_locked", "box_crossing", "construction_geom", "preview_sketch", "analysis.hatch"])), + (true, ColorVision::Tritan, set(&["sketch_locked", "box_crossing", "construction_geom", "glyph_text", "preview_sketch", "analysis.hatch"])), ] { let base = Tokens::themed(dark, ColorVision::Off); assert_eq!(changed_fields(&base, &Tokens::themed(dark, mode)), want, "{mode:?}, dark {dark}"); diff --git a/crates/ui-egui/src/context_menu.rs b/crates/ui-egui/src/context_menu.rs index cc1d18e4..5bd9d269 100644 --- a/crates/ui-egui/src/context_menu.rs +++ b/crates/ui-egui/src/context_menu.rs @@ -342,8 +342,8 @@ pub fn items(app: &SolveApp, target: &Target) -> Vec { Target::Workspace => { let rendering = crate::render_workspace::active(app); let mut v = vec![ - act("ui.workspace.design", "Design", "box").with(json!({"workspace":"design"})).key(if rendering { "" } else { "current" }), - act("ui.workspace.render", "Render", "camera") + act("ui.workspace.design", "Design", "ws_design").with(json!({"workspace":"design"})).key(if rendering { "" } else { "current" }), + act("ui.workspace.render", "Render", "ws_render") .with(json!({"workspace":"render"})) .key(if rendering { "current" } else { "" }) .on(app.session.active_sketch.is_none() && app.tool.is_none() && app.dialog.is_none() && !app.file_load.busy()), diff --git a/crates/ui-egui/src/deferred.rs b/crates/ui-egui/src/deferred.rs index 382e957e..12140f86 100644 --- a/crates/ui-egui/src/deferred.rs +++ b/crates/ui-egui/src/deferred.rs @@ -108,11 +108,12 @@ impl SolveApp { let _ = tx.send(()); }) .ok()?; - match rx.recv_timeout(wait) { - Err(std::sync::mpsc::RecvTimeoutError::Timeout) => { - Some(Deferred { done: rx, started: crate::now_ms(), command, revision: self.session.revision }) - } - _ => None, + // Zero explicitly requests off-thread application, even if a fast worker + // finishes before this thread is scheduled again. Apply on the next frame. + if wait.is_zero() || matches!(rx.recv_timeout(wait), Err(std::sync::mpsc::RecvTimeoutError::Timeout)) { + Some(Deferred { done: rx, started: crate::now_ms(), command, revision: self.session.revision }) + } else { + None } } diff --git a/crates/ui-egui/src/icon_svg.rs b/crates/ui-egui/src/icon_svg.rs new file mode 100644 index 00000000..b4708be6 --- /dev/null +++ b/crates/ui-egui/src/icon_svg.rs @@ -0,0 +1,894 @@ +//! The UI icon renderer: the SVG masters in `assets/icons/ui/design/svg`, drawn at the pixel size +//! they are shown. +//! +//! The masters are a small, linted SVG subset (`tools/lint_icons.py`): absolute `M L C Q A Z` +//! paths, circles, groups with a clip or opacity, masks and clip paths. They are embedded at build +//! time together with `palette.toml` (every colour role in light and dark, and the accents) and +//! `aliases.toml` (see `build.rs`), parsed once, and rasterised with tiny-skia into a texture per +//! (icon, accent, theme, pixel size). A master never carries a colour of its own: it names a +//! palette role, which resolves to the theme's hex, or to the accent's hex for the `active-*` +//! roles. GAP instead erases preceding artwork to transparency; it never paints a background. +//! Changing a colour is an edit to `palette.toml` only. +//! +//! Nothing here panics on bad artwork: a master that does not parse is logged and drawn as the +//! placeholder, and `every_master_parses` holds that none do. + +use std::collections::HashMap; +use std::sync::OnceLock; + +use tiny_skia as sk; + +include!(concat!(env!("OUT_DIR"), "/icon_data.rs")); + +/// The id drawn for every icon that has no master yet. +pub const PLACEHOLDER: &str = "placeholder"; + +/// How a shape is painted. +#[derive(Clone, Copy, Debug, PartialEq)] +enum Paint { + None, + /// Erase earlier artwork; never paint a theme background into the texture. + Knockout, + /// `currentColor`: the `line` role. + Line, + /// A palette role (index into `ROLE_NAMES`). + Role(usize), + /// A fixed colour (mask artwork). + Fixed([u8; 3]), +} + +#[derive(Debug)] +struct Node { + path: sk::Path, + fill: Paint, + rule: sk::FillRule, + stroke: Paint, + width: f32, + dash: Vec, + opacity: f32, + mask: Option, +} + +#[derive(Debug)] +enum Item { + Node(Node), + Group { clip: Option, opacity: f32, items: Vec }, +} + +/// A parsed master. +#[derive(Debug)] +pub struct Doc { + items: Vec, + masks: Vec>, + clips: Vec>, +} + +// --------------------------------------------------------------------------------------------- +// Parsing + +struct Tag<'a> { + name: &'a str, + attrs: Vec<(&'a str, &'a str)>, + close: bool, + selfclose: bool, +} + +impl Tag<'_> { + fn get(&self, k: &str) -> Option<&str> { + self.attrs.iter().find(|(n, _)| *n == k).map(|(_, v)| *v) + } + fn num(&self, k: &str) -> Result { + self.get(k).ok_or_else(|| format!("missing {k}"))?.trim().parse::().map_err(|e| format!("{k}: {e}")) + } +} + +fn tags(src: &str) -> Result>, String> { + let mut out = Vec::new(); + let mut rest = src; + while let Some(i) = rest.find('<') { + let after = rest.get(i + 1..).ok_or("truncated")?; + let j = after.find('>').ok_or("unterminated tag")?; + let body = after.get(..j).ok_or("truncated")?; + rest = after.get(j + 1..).ok_or("truncated")?; + if body.starts_with('?') || body.starts_with('!') { + continue; + } + let (close, body) = match body.strip_prefix('/') { + Some(b) => (true, b), + None => (false, body), + }; + let (selfclose, body) = match body.strip_suffix('/') { + Some(b) => (true, b), + None => (false, body), + }; + let body = body.trim(); + let name_end = body.find(char::is_whitespace).unwrap_or(body.len()); + let name = body.get(..name_end).ok_or("bad tag")?; + let mut attrs = Vec::new(); + let mut a = body.get(name_end..).unwrap_or(""); + while let Some(eq) = a.find('=') { + let key = a.get(..eq).ok_or("bad attribute")?.trim(); + let v = a.get(eq + 1..).ok_or("bad attribute")?.trim_start(); + let q = v.chars().next().filter(|c| *c == '"' || *c == '\'').ok_or("unquoted attribute")?; + let v = v.get(1..).ok_or("bad attribute")?; + let end = v.find(q).ok_or("unterminated attribute")?; + attrs.push((key, v.get(..end).ok_or("bad attribute")?)); + a = v.get(end + 1..).unwrap_or(""); + } + out.push(Tag { name, attrs, close, selfclose }); + } + Ok(out) +} + +/// An SVG elliptical arc as cubic Béziers appended to `pb`. +#[allow(clippy::too_many_arguments)] +fn arc(pb: &mut sk::PathBuilder, x1: f32, y1: f32, rx: f32, ry: f32, rot_deg: f32, large: bool, sweep: bool, x2: f32, y2: f32) { + let (mut rx, mut ry) = (rx.abs(), ry.abs()); + if rx < 1e-6 || ry < 1e-6 || ((x1 - x2).abs() < 1e-6 && (y1 - y2).abs() < 1e-6) { + pb.line_to(x2, y2); + return; + } + let phi = rot_deg.to_radians(); + let (sp, cp) = phi.sin_cos(); + let (dx, dy) = ((x1 - x2) / 2.0, (y1 - y2) / 2.0); + let (x1p, y1p) = (cp * dx + sp * dy, -sp * dx + cp * dy); + let lam = (x1p * x1p) / (rx * rx) + (y1p * y1p) / (ry * ry); + if lam > 1.0 { + let s = lam.sqrt(); + rx *= s; + ry *= s; + } + let num = rx * rx * ry * ry - rx * rx * y1p * y1p - ry * ry * x1p * x1p; + let den = rx * rx * y1p * y1p + ry * ry * x1p * x1p; + let sign = if large == sweep { -1.0 } else { 1.0 }; + let coef = sign * (num / den).max(0.0).sqrt(); + let (cxp, cyp) = (coef * rx * y1p / ry, -coef * ry * x1p / rx); + let (cx, cy) = (cp * cxp - sp * cyp + (x1 + x2) / 2.0, sp * cxp + cp * cyp + (y1 + y2) / 2.0); + let ang = |ux: f32, uy: f32, vx: f32, vy: f32| (ux * vy - uy * vx).atan2(ux * vx + uy * vy); + let (ux, uy) = ((x1p - cxp) / rx, (y1p - cyp) / ry); + let (vx, vy) = ((-x1p - cxp) / rx, (-y1p - cyp) / ry); + let theta = ang(1.0, 0.0, ux, uy); + let mut dtheta = ang(ux, uy, vx, vy); + if !sweep && dtheta > 0.0 { + dtheta -= std::f32::consts::TAU; + } else if sweep && dtheta < 0.0 { + dtheta += std::f32::consts::TAU; + } + let n = ((dtheta.abs() / std::f32::consts::FRAC_PI_2).ceil() as usize).clamp(1, 8); + let delta = dtheta / n as f32; + let t = 4.0 / 3.0 * (delta / 4.0).tan(); + let map = |x: f32, y: f32| (cx + rx * cp * x - ry * sp * y, cy + rx * sp * x + ry * cp * y); + for i in 0..n { + let a1 = theta + delta * i as f32; + let a2 = a1 + delta; + let (s1, c1) = a1.sin_cos(); + let (s2, c2) = a2.sin_cos(); + let (p1x, p1y) = map(c1 - t * s1, s1 + t * c1); + let (p2x, p2y) = map(c2 + t * s2, s2 - t * c2); + let (ex, ey) = map(c2, s2); + pb.cubic_to(p1x, p1y, p2x, p2y, ex, ey); + } +} + +fn parse_path(d: &str) -> Result { + let mut toks: Vec<&str> = Vec::new(); + let b = d.as_bytes(); + let mut i = 0; + while i < b.len() { + let c = *b.get(i).ok_or("path")?; + if c.is_ascii_alphabetic() { + toks.push(d.get(i..=i).ok_or("path")?); + i += 1; + } else if c == b'-' || c == b'.' || c.is_ascii_digit() { + let s = i; + i += 1; + while let Some(&n) = b.get(i) { + if n.is_ascii_digit() + || n == b'.' + || ((n == b'-' || n == b'+') && matches!(b.get(i - 1), Some(b'e' | b'E'))) + || n == b'e' + || n == b'E' + { + i += 1; + } else { + break; + } + } + toks.push(d.get(s..i).ok_or("path")?); + } else { + i += 1; + } + } + let mut pb = sk::PathBuilder::new(); + let (mut k, mut cmd) = (0usize, ' '); + let (mut x, mut y) = (0.0f32, 0.0f32); + let mut open = false; + let num = |k: &mut usize| -> Result { + let t = toks.get(*k).ok_or("path: missing number")?; + *k += 1; + t.parse::().map_err(|e| format!("path: {e}")) + }; + while k < toks.len() { + let t = *toks.get(k).ok_or("path")?; + if t.len() == 1 && t.as_bytes().first().is_some_and(u8::is_ascii_alphabetic) { + cmd = t.chars().next().ok_or("path")?; + k += 1; + } + match cmd { + 'M' => { + (x, y) = (num(&mut k)?, num(&mut k)?); + pb.move_to(x, y); + open = true; + cmd = 'L'; + } + 'L' => { + (x, y) = (num(&mut k)?, num(&mut k)?); + pb.line_to(x, y); + } + 'C' => { + let v = [num(&mut k)?, num(&mut k)?, num(&mut k)?, num(&mut k)?, num(&mut k)?, num(&mut k)?]; + pb.cubic_to(v[0], v[1], v[2], v[3], v[4], v[5]); + (x, y) = (v[4], v[5]); + } + 'Q' => { + let v = [num(&mut k)?, num(&mut k)?, num(&mut k)?, num(&mut k)?]; + pb.quad_to(v[0], v[1], v[2], v[3]); + (x, y) = (v[2], v[3]); + } + 'A' => { + let (rx, ry, rot) = (num(&mut k)?, num(&mut k)?, num(&mut k)?); + let (fa, fs) = (num(&mut k)? != 0.0, num(&mut k)? != 0.0); + let (ex, ey) = (num(&mut k)?, num(&mut k)?); + arc(&mut pb, x, y, rx, ry, rot, fa, fs, ex, ey); + (x, y) = (ex, ey); + } + 'Z' => { + if open { + pb.close(); + } + } + other => return Err(format!("path command {other} is not supported")), + } + } + pb.finish().ok_or_else(|| "empty path".to_string()) +} + +fn parse_paint(v: Option<&str>, in_mask: bool) -> Result { + let Some(v) = v else { return Ok(Paint::None) }; + match v { + "none" => Ok(Paint::None), + "currentColor" => Ok(Paint::Line), + "white" if in_mask => Ok(Paint::Fixed([255, 255, 255])), + "black" if in_mask => Ok(Paint::Fixed([0, 0, 0])), + _ => { + let lower = v.to_ascii_lowercase(); + ROLE_SOURCE_HEX + .iter() + .position(|h| *h == lower) + .map(|role| if ROLE_NAMES.get(role) == Some(&"gap") { Paint::Knockout } else { Paint::Role(role) }) + .ok_or_else(|| format!("colour {v} is not a palette colour")) + } + } +} + +fn parse_dash(v: Option<&str>) -> Vec { + let mut d: Vec = v.map(|s| s.split([' ', ',']).filter_map(|n| n.parse().ok()).collect()).unwrap_or_default(); + if d.len() % 2 == 1 { + d.extend(d.clone()); + } + d +} + +fn url_ref(v: &str) -> Option<&str> { + v.strip_prefix("url(#")?.strip_suffix(')') +} + +enum Frame { + Group { clip: Option, opacity: f32, items: Vec }, + Mask(usize), + Clip(usize), + Defs, +} + +impl Doc { + pub fn parse(src: &str) -> Result { + let mut doc = Doc { items: Vec::new(), masks: Vec::new(), clips: Vec::new() }; + let (mut mask_ids, mut clip_ids): (HashMap, HashMap) = (HashMap::new(), HashMap::new()); + let mut stack: Vec = Vec::new(); + for t in tags(src)? { + match (t.name, t.close) { + ("svg" | "title", _) => {} + ("defs", false) => stack.push(Frame::Defs), + ("mask", false) => { + doc.masks.push(Vec::new()); + mask_ids.insert(t.get("id").ok_or("mask without id")?.to_string(), doc.masks.len() - 1); + stack.push(Frame::Mask(doc.masks.len() - 1)); + } + ("clipPath", false) => { + doc.clips.push(Vec::new()); + clip_ids.insert(t.get("id").ok_or("clipPath without id")?.to_string(), doc.clips.len() - 1); + stack.push(Frame::Clip(doc.clips.len() - 1)); + } + ("g", false) => { + let clip = match t.get("clip-path").and_then(url_ref) { + Some(id) => Some(*clip_ids.get(id).ok_or("unknown clip")?), + None => None, + }; + let opacity = t.get("opacity").and_then(|o| o.parse().ok()).unwrap_or(1.0); + stack.push(Frame::Group { clip, opacity, items: Vec::new() }); + } + ("defs" | "mask" | "clipPath" | "g", true) => match stack.pop() { + Some(Frame::Group { clip, opacity, items }) => { + let item = Item::Group { clip, opacity, items }; + match stack.last_mut() { + Some(Frame::Group { items, .. }) => items.push(item), + _ => doc.items.push(item), + } + } + Some(_) => {} + None => return Err("unbalanced tags".into()), + }, + ("path" | "circle", _) => { + let in_mask = matches!(stack.last(), Some(Frame::Mask(_))); + let path = if t.name == "path" { + parse_path(t.get("d").ok_or("path without d")?)? + } else { + sk::PathBuilder::from_circle(t.num("cx")?, t.num("cy")?, t.num("r")?).ok_or("bad circle")? + }; + let mask = match t.get("mask").and_then(url_ref) { + Some(id) => Some(*mask_ids.get(id).ok_or("unknown mask")?), + None => None, + }; + let dash = parse_dash(t.get("stroke-dasharray")); + let mut stroke = parse_paint(t.get("stroke"), in_mask)?; + let visible_stroke = match stroke { + Paint::Line => true, + Paint::Role(role) => !matches!(ROLE_NAMES.get(role).copied(), Some("gap" | "active-1" | "active-2" | "active-3")), + _ => false, + }; + // Non-blue dotted/dashed references share the guide colour. + // Blue source paints retain their semantic accent; keep + // background knockouts and literal mask paints unchanged. + if !dash.is_empty() && visible_stroke && !in_mask { + stroke = Paint::Role(ROLE_NAMES.iter().position(|name| *name == "guide").ok_or("missing guide colour")?); + } + let node = Node { + path, + fill: if t.get("fill").is_none() && in_mask { Paint::None } else { parse_paint(t.get("fill").or(Some("none")), in_mask)? }, + rule: if t.get("fill-rule") == Some("evenodd") { sk::FillRule::EvenOdd } else { sk::FillRule::Winding }, + stroke, + width: t.get("stroke-width").and_then(|w| w.parse().ok()).unwrap_or(1.0), + dash, + opacity: t.get("opacity").and_then(|o| o.parse().ok()).unwrap_or(1.0), + mask, + }; + match stack.last_mut() { + Some(Frame::Mask(i)) => doc.masks.get_mut(*i).ok_or("mask")?.push(node), + Some(Frame::Clip(i)) => doc.clips.get_mut(*i).ok_or("clip")?.push(node), + Some(Frame::Group { items, .. }) => items.push(Item::Node(node)), + _ => doc.items.push(Item::Node(node)), + } + } + (other, _) => return Err(format!("element <{other}> is not supported")), + } + let _ = t.selfclose; + } + if !stack.is_empty() { + return Err("unbalanced tags".into()); + } + Ok(doc) + } +} + +// --------------------------------------------------------------------------------------------- +// Rendering + +/// The colours of one drawing: the theme and an accent (index into `ACCENT_NAMES`, or `None` for +/// the base palette). +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub struct Look { + pub dark: bool, + pub accent: Option, + pub vision: crate::theme::ColorVision, +} + +impl Look { + fn rgb(self, role: usize) -> [u8; 3] { + let theme = usize::from(self.dark); + if ROLE_NAMES.get(role) == Some(&"guide") { + return PALETTE.get(theme).and_then(|p| p.get(role)).copied().unwrap_or([255, 0, 255]); + } + let vision = match self.vision { + crate::theme::ColorVision::Off => "", + crate::theme::ColorVision::Protan | crate::theme::ColorVision::Deutan => "red-green", + crate::theme::ColorVision::Tritan => "blue-yellow", + }; + if let Some((_, _, _, l, d)) = ACCENT_VARIANTS.iter().find(|(v, a, r, _, _)| *v == vision && Some(*a) == self.accent && *r == role) { + return if self.dark { *d } else { *l }; + } + if let Some((_, l, d)) = self.accent.and_then(|a| ACCENTS.get(a)).and_then(|items| items.iter().find(|(r, _, _)| *r == role)) { + return if self.dark { *d } else { *l }; + } + PALETTE.get(theme).and_then(|p| p.get(role)).copied().unwrap_or([255, 0, 255]) + } + + fn resolve(self, p: Paint) -> Option<[u8; 3]> { + match p { + Paint::None | Paint::Knockout => None, + Paint::Fixed(c) => Some(c), + Paint::Role(r) => Some(self.rgb(r)), + Paint::Line => ROLE_NAMES.iter().position(|n| *n == "line").map(|r| self.rgb(r)), + } + } +} + +fn paint_of(c: [u8; 3], alpha: f32) -> sk::Paint<'static> { + let mut p = sk::Paint::default(); + p.set_color_rgba8(c[0], c[1], c[2], (alpha.clamp(0.0, 1.0) * 255.0).round() as u8); + p.anti_alias = true; + p +} + +fn shape_paint(paint: Paint, look: Look, alpha: f32) -> Option> { + if paint == Paint::Knockout { + let mut paint = paint_of([0, 0, 0], alpha); + paint.blend_mode = sk::BlendMode::DestinationOut; + Some(paint) + } else { + look.resolve(paint).map(|colour| paint_of(colour, alpha)) + } +} + +fn draw_node(pm: &mut sk::Pixmap, n: &Node, look: Look, tf: sk::Transform, doc: &Doc, clip: Option<&sk::Mask>, opacity: f32) { + let own; + let mask = if let Some(m) = n.mask { + own = doc.masks.get(m).and_then(|nodes| { + let mut sub = sk::Pixmap::new(pm.width(), pm.height())?; + for node in nodes { + draw_node(&mut sub, node, look, tf, doc, None, 1.0); + } + let mut own = sk::Mask::from_pixmap(sub.as_ref(), sk::MaskType::Luminance); + if let Some(parent) = clip { + for (value, parent) in own.data_mut().iter_mut().zip(parent.data()) { + *value = ((u16::from(*value) * u16::from(*parent) + 127) / 255) as u8; + } + } + Some(own) + }); + own.as_ref() + } else { + clip + }; + let alpha = n.opacity * opacity; + if let Some(paint) = shape_paint(n.fill, look, alpha) { + pm.fill_path(&n.path, &paint, n.rule, tf, mask); + } + if let Some(paint) = shape_paint(n.stroke, look, alpha) { + let stroke = sk::Stroke { + width: n.width, + line_cap: sk::LineCap::Round, + line_join: sk::LineJoin::Round, + dash: if n.dash.is_empty() { None } else { sk::StrokeDash::new(n.dash.clone(), 0.0) }, + ..sk::Stroke::default() + }; + pm.stroke_path(&n.path, &paint, &stroke, tf, mask); + } +} + +fn draw_items(pm: &mut sk::Pixmap, items: &[Item], look: Look, tf: sk::Transform, doc: &Doc, clip: Option<&sk::Mask>, opacity: f32) { + for item in items { + match item { + Item::Node(n) => draw_node(pm, n, look, tf, doc, clip, opacity), + Item::Group { clip: c, opacity: o, items } => { + let mask = c.and_then(|c| doc.clips.get(c)).and_then(|nodes| { + let mut m = sk::Mask::new(pm.width(), pm.height())?; + for n in nodes { + m.fill_path(&n.path, n.rule, true, tf); + } + Some(m) + }); + draw_items(pm, items, look, tf, doc, mask.as_ref().or(clip), opacity * o); + } + } + } +} + +/// Rasterise `doc` into a square `px` × `px` premultiplied RGBA image. +pub fn render(doc: &Doc, look: Look, px: u32) -> Option { + let mut pm = sk::Pixmap::new(px, px)?; + let s = px as f32 / 64.0; + draw_items(&mut pm, &doc.items, look, sk::Transform::from_scale(s, s), doc, None, 1.0); + Some(pm) +} + +// --------------------------------------------------------------------------------------------- +// The icon table + +struct Entry { + doc: Doc, +} + +fn table() -> &'static (HashMap<&'static str, Entry>, HashMap<&'static str, Entry>) { + static T: OnceLock<(HashMap<&'static str, Entry>, HashMap<&'static str, Entry>)> = OnceLock::new(); + T.get_or_init(|| { + let (mut big, mut small) = (HashMap::new(), HashMap::new()); + for (id, fam, src) in SVGS { + match Doc::parse(src) { + Ok(doc) => { + let map = if *fam == "small" { &mut small } else { &mut big }; + // Model masters precede flat glyphs: keep the reviewed command artwork. + map.entry(*id).or_insert(Entry { doc }); + } + Err(e) => log::warn!("icon {fam}/{id}: {e}"), + } + } + (big, small) + }) +} + +/// A resolved icon: the master to draw and the accent it is drawn in. +#[derive(Clone, Copy, Debug)] +pub struct Resolved { + pub master: &'static str, + pub accent: Option, + pub placeholder: bool, +} + +fn accent_index(name: &str) -> Option { + if name.is_empty() { None } else { ACCENT_NAMES.iter().position(|a| *a == name) } +} + +/// A semantic default belongs to the requested id, never to the caller's current workspace. +fn default_accent(name: &str) -> Option { + ICON_ACCENTS.iter().find(|(id, _)| *id == name).and_then(|(_, accent)| accent_index(accent)) +} + +/// Primary semantic stroke, also used by toolbar affordances. The palette owns its value. +pub(crate) fn primary(name: &str, dark: bool, vision: crate::theme::ColorVision) -> egui::Color32 { + let look = Look { dark, accent: resolve(name).accent, vision }; + role_colour("active-3", look) +} + +pub(crate) fn role_colour(name: &str, look: Look) -> egui::Color32 { + if name == "gap" { + return egui::Color32::TRANSPARENT; + } + let role = ROLE_NAMES.iter().position(|n| *n == name); + let [r, g, b] = role.map(|role| look.rgb(role)).unwrap_or([255, 0, 255]); + crate::theme::icon_color([r, g, b]) +} + +/// The master and accent for an app icon id: an alias (with its accent), a master of that name, +/// or the placeholder. +pub fn resolve(name: &str) -> Resolved { + let (big, _) = table(); + let placeholder = Resolved { master: PLACEHOLDER, accent: None, placeholder: true }; + if let Some((_, master, accent)) = ALIASES.iter().find(|(a, _, _)| *a == name) { + return match big.get_key_value(master) { + Some((k, _)) => Resolved { master: k, accent: accent_index(accent).or_else(|| default_accent(name)), placeholder: false }, + None => placeholder, + }; + } + match big.get_key_value(name) { + Some((k, _)) if name != PLACEHOLDER => Resolved { master: k, accent: default_accent(name), placeholder: false }, + _ => placeholder, + } +} + +/// The master to draw at `px` pixels: the small-slot variant where one exists. +pub fn doc_for(master: &str, px: u32) -> Option<&'static Doc> { + let (big, small) = table(); + if let Some(e) = small.get(master).filter(|_| px <= 24) { + return Some(&e.doc); + } + big.get(master).or_else(|| big.get(PLACEHOLDER)).map(|e| &e.doc) +} + +/// Every master id (not the small variants), for tests and tooling. +pub fn master_ids() -> Vec<&'static str> { + let mut v: Vec<_> = table().0.keys().copied().collect(); + v.sort_unstable(); + v +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn every_master_parses() { + for (id, fam, src) in SVGS { + if let Err(e) = Doc::parse(src) { + panic!("{fam}/{id}: {e}"); + } + } + assert!(table().0.contains_key(PLACEHOLDER), "the placeholder master exists"); + } + + #[test] + fn aliases_point_at_masters_and_known_accents() { + for (alias, master, accent) in ALIASES { + assert!(table().0.contains_key(master), "{alias} -> {master}"); + assert!(accent.is_empty() || ACCENT_NAMES.contains(accent), "{alias}: accent {accent}"); + assert!(!resolve(alias).placeholder); + } + } + + #[test] + fn non_blue_dashed_strokes_use_guides_but_preserve_accents_and_knockouts() { + let svg = ""; + let doc = Doc::parse(svg).unwrap(); + let guide = ROLE_NAMES.iter().position(|name| *name == "guide").unwrap(); + let active = ROLE_NAMES.iter().position(|name| *name == "active-3").unwrap(); + let Item::Node(line) = &doc.items[0] else { panic!("expected line") }; + let Item::Node(knockout) = &doc.items[1] else { panic!("expected knockout") }; + let Item::Node(blue) = &doc.items[2] else { panic!("expected blue line") }; + assert_eq!(line.stroke, Paint::Role(guide)); + assert_eq!(knockout.stroke, Paint::Knockout); + assert_eq!(blue.stroke, Paint::Role(active)); + for dark in [false, true] { + for accent in std::iter::once(None).chain((0..ACCENT_NAMES.len()).map(Some)) { + let look = Look { dark, accent, vision: crate::theme::ColorVision::Off }; + assert_eq!(look.rgb(guide), PALETTE[usize::from(dark)][guide]); + let pm = render(&doc, look, 64).unwrap(); + assert_eq!([pm.pixel(9, 20).unwrap().red(), pm.pixel(9, 20).unwrap().green(), pm.pixel(9, 20).unwrap().blue()], look.rgb(guide)); + assert_eq!([pm.pixel(9, 52).unwrap().red(), pm.pixel(9, 52).unwrap().green(), pm.pixel(9, 52).unwrap().blue()], look.rgb(active)); + } + } + } + + #[test] + fn knockout_fills_and_strokes_erase_earlier_layers_and_allow_later_foreground() { + let doc = Doc::parse("").unwrap(); + for dark in [false, true] { + for accent in std::iter::once(None).chain((0..ACCENT_NAMES.len()).map(Some)) { + let look = Look { dark, accent, vision: crate::theme::ColorVision::Off }; + let pm = render(&doc, look, 64).unwrap(); + assert_eq!(pm.pixel(26, 20).unwrap().alpha(), 0, "fill must reveal the real background"); + assert_eq!(pm.pixel(20, 44).unwrap().alpha(), 0, "stroke must reveal the real background"); + assert_eq!(pm.pixel(20, 20).unwrap().alpha(), 255, "foreground is drawn after erasure"); + assert_eq!(pm.pixel(2, 2).unwrap().alpha(), 255, "unaffected base stays opaque"); + assert!((126..=128).contains(&pm.pixel(44, 20).unwrap().alpha()), "partial opacity erases partially"); + } + } + } + + #[test] + fn knockout_respects_its_clip_and_combines_explicit_masks_with_clips() { + let doc = Doc::parse("").unwrap(); + let pm = render(&doc, Look { dark: false, accent: None, vision: crate::theme::ColorVision::Off }, 64).unwrap(); + assert_eq!(pm.pixel(24, 32).unwrap().alpha(), 0); + assert_eq!(pm.pixel(40, 32).unwrap().alpha(), 255, "cutout must not escape its parent clip"); + } + + #[test] + fn accepted_badges_and_environment_sun_have_transparent_holes() { + for dark in [false, true] { + let look = Look { dark, accent: None, vision: crate::theme::ColorVision::Off }; + assert_eq!(role_colour("gap", look), egui::Color32::TRANSPARENT); + for (id, x, y) in [("contact_new", 48, 48), ("environment_map", 45, 19), ("line_mid", 35, 28)] { + let pm = render(doc_for(id, 64).unwrap(), look, 64).unwrap(); + assert_eq!(pm.pixel(x, y).unwrap().alpha(), 0, "{id}: GAP must not be a painted theme colour"); + } + } + } + + #[test] + fn native_clip_paths_and_plus_mask_keep_the_artwork_inside_its_shape() { + for dark in [false, true] { + for px in [16, 24, 64] { + let look = Look { dark, accent: None, vision: crate::theme::ColorVision::Off }; + for id in ["environment_map", "curvature_map", "iso_analysis", "zebra"] { + let doc = doc_for(id, px).unwrap(); + let pm = render(doc, look, px).unwrap(); + assert_eq!(pm.pixel(px - 2, px - 2).unwrap().alpha(), 0, "{id} escapes its native clip"); + assert!(pm.pixel(px / 2, px / 2).unwrap().alpha() > 0, "{id} lost its clipped contents"); + } + } + let pm = render(doc_for("new", 64).unwrap(), Look { dark, accent: None, vision: crate::theme::ColorVision::Off }, 64).unwrap(); + assert_eq!(pm.pixel(45, 45).unwrap().alpha(), 0, "the plus is genuinely punched out in the native renderer"); + } + } + + #[test] + fn command_meanings_choose_the_same_accent_on_every_surface() { + let accent = |name| resolve(name).accent.map(|i| ACCENT_NAMES[i]); + for c in solidcraft_engine::command_specs() { + let want = if c.id.starts_with("surface.") { + Some("surface") + } else if c.panel == "CONSTRAINTS" { + Some("constraint") + } else if matches!(c.id, "sketch.construction" | "sketch.centerline") { + Some("construction") + } else if c.id.starts_with("sketch.") && matches!(c.panel, "CREATE" | "MODIFY" | "PATTERN" | "FINISH SKETCH") { + Some("sketch") + } else if c.id == "file.save_mesh" { + Some("mesh") + } else { + continue; + }; + assert_eq!(accent(c.icon), want, "{} ({})", c.id, c.icon); + } + for id in ["move", "scale", "copy", "delete", "export", "ground", "axis_two_planes"] { + assert_eq!(accent(id), None, "shared utility / modelling icon {id}"); + } + for (alias, master) in [("sketch_move", "move"), ("sketch_scale", "scale"), ("surface_fair", "spline_cv"), ("mesh_export", "export")] { + assert_eq!(resolve(alias).master, resolve(master).master, "reuse accepted shapes"); + } + } + + #[test] + fn semantic_ui_tokens_and_icon_strokes_share_the_palette_including_accessibility() { + for dark in [false, true] { + for vision in crate::theme::ColorVision::ALL { + let tokens = crate::theme::Tokens::themed(dark, vision); + assert_eq!(primary("line", dark, vision), tokens.sketch_accent); + assert_eq!(primary("c_parallel", dark, vision), tokens.glyph_text); + assert_eq!(primary("centerline", dark, vision), tokens.construction_geom); + assert_eq!(primary("construction", dark, vision), tokens.construction_geom); + } + } + } + + #[test] + fn semantic_ramps_preserve_silhouettes_and_passive_faces_in_all_themes() { + use crate::theme::ColorVision; + for id in ["line", "c_parallel", "construction", "centerline", "sketch_rect_pattern", "surface_extrude", "mesh_export"] { + let resolved = resolve(id); + for dark in [false, true] { + for vision in ColorVision::ALL { + let look = Look { dark, accent: resolved.accent, vision }; + let base = Look { accent: None, ..look }; + for role in ["ink", "line", "passive-1", "passive-2", "passive-3", "gap"] { + let role = ROLE_NAMES.iter().position(|name| *name == role).unwrap(); + assert_eq!(look.rgb(role), base.rgb(role), "{id} changes passive paint"); + } + for px in [16, 24, 64] { + let doc = doc_for(resolved.master, px).unwrap(); + let changed = render(doc, look, px).unwrap(); + let original = render(doc, base, px).unwrap(); + assert!(changed.pixels().iter().zip(original.pixels()).all(|(a, b)| a.alpha() == b.alpha()), "{id}: silhouette at {px}px"); + } + } + } + } + } + + #[test] + fn command_artwork_resolves_and_construct_methods_are_distinct() { + let specs = solidcraft_engine::command_specs(); + for command in &specs { + assert!(!resolve(command.icon).placeholder, "{}: {}", command.id, command.icon); + } + let construction: Vec<_> = specs.iter().filter(|c| c.panel == "CONSTRUCT").collect(); + let distinct: std::collections::HashSet<_> = construction.iter().map(|c| c.icon).collect(); + assert_eq!(construction.len(), 19); + assert_eq!(distinct.len(), construction.len(), "construction methods need distinct visual identities"); + } + + #[test] + fn helper_controls_resolve_without_command_registry_entries() { + for name in ["agent_chat", "normal", "infinite", "pattern_flip", "pattern_center"] { + assert!(!resolve(name).placeholder, "UI helper: {name}"); + } + } + + #[test] + fn sketch_construction_modes_have_distinct_icons() { + let families: &[&[&str]] = &[ + &["sketch.polygon.inscribed", "sketch.polygon.circumscribed", "sketch.polygon.edge"], + &["sketch.slot.center_to_center", "sketch.slot.overall", "sketch.slot.center_point"], + &["sketch.slot.arc_three_point", "sketch.slot.arc_center"], + &["sketch.chamfer.equal_distance", "sketch.chamfer.distance_angle", "sketch.chamfer.two_distance"], + ]; + for commands in families { + let icons: std::collections::HashSet<_> = commands.iter().map(|id| solidcraft_engine::find_command(id).unwrap().icon).collect(); + assert_eq!(icons.len(), commands.len(), "Sketch modes share an icon: {commands:?}"); + for icon in icons { + assert!(!resolve(icon).placeholder, "{icon}"); + } + } + } + + #[test] + fn contact_actions_have_distinct_icons_from_interference_analysis() { + let mut icons = std::collections::HashSet::new(); + let interference = solidcraft_engine::find_command("inspect.interference").unwrap().icon; + for id in ["contact.enable_sets", "contact.enable_all", "contact.disable_all", "contact.create"] { + let icon = solidcraft_engine::find_command(id).unwrap().icon; + assert_ne!(icon, interference, "{id} is a contact control, not an interference result"); + assert!(icons.insert(icon), "contact controls share an icon: {id}"); + assert!(!resolve(icon).placeholder, "{id}: {icon}"); + } + } + + #[test] + fn reviewed_model_folder_is_also_the_folder_drawn_in_the_app() { + let source = SVGS.iter().find(|(id, fam, _)| *id == "folder" && *fam == "model").unwrap().2; + let expected = Doc::parse(source).unwrap(); + for dark in [false, true] { + let look = Look { dark, accent: None, vision: crate::theme::ColorVision::Off }; + let actual = render(doc_for("folder", 64).unwrap(), look, 64).unwrap(); + let reviewed = render(&expected, look, 64).unwrap(); + assert_eq!(actual.data(), reviewed.data()); + } + } + + #[test] + fn unknown_names_draw_the_placeholder() { + assert!(resolve("no_such_icon").placeholder); + assert!(resolve("").placeholder); + assert!(!resolve("extrude").placeholder); + } + + fn coverage(pm: &sk::Pixmap) -> usize { + pm.pixels().iter().filter(|p| p.alpha() > 0).count() + } + + #[test] + fn every_master_draws_something_at_every_size_in_both_themes() { + for id in master_ids() { + for dark in [false, true] { + for px in [16, 20, 26, 64] { + let Some(doc) = doc_for(id, px) else { panic!("{id}") }; + let pm = render(doc, Look { dark, accent: None, vision: crate::theme::ColorVision::Off }, px) + .unwrap_or_else(|| panic!("{id} at {px}")); + assert!(coverage(&pm) > 8, "{id} at {px}px (dark {dark}) is blank"); + } + } + } + } + + fn rgba(pm: &sk::Pixmap) -> Vec { + pm.data().to_vec() + } + + #[test] + fn dark_theme_and_accents_change_colour_not_shape() { + let doc = doc_for("extrude", 64).unwrap_or_else(|| panic!("extrude")); + let light = render(doc, Look { dark: false, accent: None, vision: crate::theme::ColorVision::Off }, 64).unwrap_or_else(|| panic!("render")); + let dark = render(doc, Look { dark: true, accent: None, vision: crate::theme::ColorVision::Off }, 64).unwrap_or_else(|| panic!("render")); + assert_ne!(rgba(&light), rgba(&dark)); + let r = resolve("surface_extrude"); + assert_eq!(r.master, "extrude"); + let surface = + render(doc, Look { dark: false, accent: r.accent, vision: crate::theme::ColorVision::Off }, 64).unwrap_or_else(|| panic!("render")); + assert!(r.accent.is_some()); + assert_ne!(rgba(&light), rgba(&surface)); + // Same silhouette: the alpha channel is identical. + let alpha = |pm: &sk::Pixmap| pm.pixels().iter().map(|p| p.alpha()).collect::>(); + assert_eq!(alpha(&light), alpha(&surface)); + } + + #[test] + fn hostile_artwork_is_an_error_not_a_panic() { + for bad in [ + "", + "<", + "", + "", + "", + "", + "", + "", + ] { + let _ = Doc::parse(bad); + } + assert!(Doc::parse("").is_err()); + assert!(Doc::parse("").is_err()); + } + + #[test] + fn palette_roles_are_distinct_and_complete() { + assert_eq!(ROLE_NAMES.len(), ROLE_SOURCE_HEX.len()); + let mut seen = std::collections::HashSet::new(); + assert!( + ROLE_NAMES.iter().zip(ROLE_SOURCE_HEX).filter(|(n, _)| **n != "line").all(|(_, h)| seen.insert(*h)), + "two roles share a source identifier" + ); + assert!(ROLE_NAMES.contains(&"line")); + assert_eq!(PALETTE.len(), 2); + assert!(PALETTE.iter().all(|p| p.len() == ROLE_NAMES.len())); + } +} diff --git a/crates/ui-egui/src/icons.rs b/crates/ui-egui/src/icons.rs index 2a5bab03..089fa4da 100644 --- a/crates/ui-egui/src/icons.rs +++ b/crates/ui-egui/src/icons.rs @@ -1,9 +1,16 @@ -//! Stable UI rendering boundary for the collected artwork. Compiled only at L5. -#[path = "../../../assets/icons/ui/source/icons.rs"] -mod source; -pub use source::paint; -#[cfg(test)] -pub(crate) use source::paint_with; +//! Stable UI rendering boundary for the icon artwork. Compiled only at L5. +//! +//! Command, menu, browser and timeline icons are the SVG masters in `assets/icons/ui/design/svg`, +//! rendered by [`crate::icon_svg`]: colours come from `palette.toml` (light and dark, and the +//! accents), never from the caller. An icon with no master yet draws the placeholder. +//! The glyph, marker, navigation and pointer sources below are not command icons. +use std::collections::HashMap; +use std::sync::{Arc, Mutex}; + +use egui::{Color32, Painter, Rect, TextureHandle, TextureOptions, pos2}; + +use crate::icon_svg::{self, Look}; + #[path = "../../../assets/icons/ui/source/glyphs.rs"] pub(crate) mod glyphs; #[path = "../../../assets/icons/ui/source/markers.rs"] @@ -12,3 +19,96 @@ pub(crate) mod markers; pub(crate) mod navigation; #[path = "../../../assets/icons/ui/source/pointer.rs"] pub(crate) mod pointer; + +/// Textures already rasterised: one per (master, look, pixel size). +#[derive(Clone, Default)] +struct Cache(Arc>>); + +/// Draw the icon `name` in `r`. The ink, fill and accent colours the callers pass are no longer +/// used: the palette decides. Only the ink's opacity is kept, so a dimmed (disabled) button dims +/// its icon. +pub fn paint(p: &Painter, r: Rect, name: &str, ink: Color32, _fill: Color32, _accent: Color32) { + paint_icon(p, r, name, f32::from(ink.a()) / 255.0); +} + +/// [`paint`] with an explicit opacity (0 to 1). +pub fn paint_icon(p: &Painter, r: Rect, name: &str, opacity: f32) { + paint_on(p, r, name, None, opacity); +} + +/// Draw the icon `name` for a surface that is always dark (the application bar), whatever the +/// theme: `dark: Some(true)`. `None` follows the theme. +pub fn paint_on(p: &Painter, r: Rect, name: &str, dark: Option, opacity: f32) { + let ctx = p.ctx(); + let ppp = ctx.pixels_per_point().max(0.5); + let side = r.width().min(r.height()); + let px = (side * ppp).round().clamp(8.0, 512.0) as u32; + let resolved = icon_svg::resolve(name); + let look = Look { dark: dark.unwrap_or_else(|| ctx.global_style().visuals.dark_mode), accent: resolved.accent, vision: crate::theme::vision() }; + let cache = ctx.data_mut(|d| d.get_temp_mut_or_insert_with(egui::Id::new("sc_icon_cache"), Cache::default).clone()); + let Ok(mut map) = cache.0.lock() else { return }; + let key = (resolved.master, look, px); + if let std::collections::hash_map::Entry::Vacant(slot) = map.entry(key) { + let Some(doc) = icon_svg::doc_for(resolved.master, px) else { return }; + let Some(pm) = icon_svg::render(doc, look, px) else { return }; + let image = egui::ColorImage::from_rgba_premultiplied([px as usize, px as usize], pm.data()); + slot.insert(ctx.load_texture(format!("icon:{}:{px}", resolved.master), image, TextureOptions::LINEAR)); + } + let Some(tex) = map.get(&key) else { return }; + // Snap the image to whole physical pixels so it stays crisp. + let size = px as f32 / ppp; + let min = pos2(((r.center().x - size / 2.0) * ppp).round() / ppp, ((r.center().y - size / 2.0) * ppp).round() / ppp); + let tint = Color32::from_white_alpha((opacity.clamp(0.0, 1.0) * 255.0).round() as u8); + p.image(tex.id(), Rect::from_min_size(min, egui::vec2(size, size)), Rect::from_min_max(pos2(0.0, 0.0), pos2(1.0, 1.0)), tint); +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::theme::{self, ColorVision}; + + #[test] + fn actual_uploaded_icon_pixels_follow_semantics_and_vision_cache_changes() { + let ctx = egui::Context::default(); + let previous = theme::vision(); + for dark in [false, true] { + for vision in [ColorVision::Off, ColorVision::Protan, ColorVision::Tritan] { + theme::set_vision(vision); + for icon in ["line", "c_parallel", "construction", "centerline", "surface_extrude", "mesh_export", "contact_new"] { + let expected = if icon == "construction" { + icon_svg::role_colour("guide", Look { dark, accent: None, vision }) + } else { + icon_svg::primary(icon, dark, vision) + }; + let mut output = ctx.run_ui(egui::RawInput::default(), |ui| { + let r = Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(64.0, 64.0)); + paint_on(ui.painter(), r, icon, Some(dark), 1.0); + }); + assert!( + output.textures_delta.set.iter().any(|(_, deltas)| deltas.iter().any(|delta| match &delta.image { + egui::ImageData::Color(image) => image.pixels.contains(&expected), + })), + "{icon}, {dark}, {vision:?}: actual texture misses its semantic stroke or reused a stale cache entry" + ); + if icon == "contact_new" { + let image = output + .textures_delta + .set + .values() + .flatten() + .find_map(|delta| match &delta.image { + egui::ImageData::Color(image) if image.size == [64, 64] => Some(image), + _ => None, + }) + .unwrap(); + assert_eq!(image.pixels.get(48 * 64 + 48), Some(&Color32::TRANSPARENT), "uploaded plus must stay transparent"); + } + // This headless test inspects uploads instead of sending them + // to a GPU; acknowledge the deltas before they are dropped. + output.textures_delta.clear(); + } + } + } + theme::set_vision(previous); + } +} diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index 40e05f7b..dc6ecbc1 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -60,6 +60,7 @@ pub mod i18n; mod i18n_menu_tests; #[cfg(test)] mod i18n_ui_tests; +pub mod icon_svg; pub mod icons; pub mod inference; pub mod install; diff --git a/crates/ui-egui/src/sketch_modify_ui_tests.rs b/crates/ui-egui/src/sketch_modify_ui_tests.rs index c88a575c..6686d596 100644 --- a/crates/ui-egui/src/sketch_modify_ui_tests.rs +++ b/crates/ui-egui/src/sketch_modify_ui_tests.rs @@ -283,7 +283,8 @@ fn moved(x: &str, y: &str, angle: &str) -> Mod { fn the_command_now_has_a_toolbar_placement_and_opens_a_window() { let spec = solidcraft_engine::find_command("sketch.move").expect("sketch.move is registered"); assert_eq!((spec.tab, spec.panel), ("SKETCH", "MODIFY")); - assert_eq!(spec.icon, "move"); + assert_eq!(spec.icon, "sketch_move"); + assert_eq!(crate::icon_svg::resolve(spec.icon).master, "move", "reuse the finished Move drawing"); let mut h = rectangle(); let before = curve_ids(&h).len(); diff --git a/crates/ui-egui/src/theme.rs b/crates/ui-egui/src/theme.rs index ef64fc23..629b12e0 100644 --- a/crates/ui-egui/src/theme.rs +++ b/crates/ui-egui/src/theme.rs @@ -3,7 +3,6 @@ //! uses comes from [`Tokens`]. use std::cell::Cell; -use std::sync::atomic::{AtomicBool, Ordering}; #[path = "../../../assets/styles/ui/palettes.rs"] mod palettes; @@ -11,10 +10,11 @@ mod palettes; use egui::{Color32, Stroke}; use serde::{Deserialize, Serialize}; -static DARK: AtomicBool = AtomicBool::new(true); - thread_local! { - /// The colour vision mode, per thread, where the dark theme is one global (`DARK`). Each app + /// Theme state belongs to the UI thread drawing the frame. Concurrent app/test renderers + /// must not change one another's palette while a frame or icon texture is being built. + static DARK: Cell = const { Cell::new(true) }; + /// The colour vision mode, per thread. Each app /// sets it from its own preferences at the start of every frame (`SolveApp::logic`), on the /// thread that draws it. Tests run many apps side by side on their own threads in one process: /// with one global, a test in a mode changed the colours under another one @@ -26,7 +26,12 @@ thread_local! { /// Is the dark theme on? pub fn is_dark() -> bool { - DARK.load(Ordering::Relaxed) + DARK.get() +} + +/// The common conversion boundary for compiled icon palette data. +pub(crate) const fn icon_color(rgb: [u8; 3]) -> Color32 { + Color32::from_rgb(rgb[0], rgb[1], rgb[2]) } /// Colour vision modes (#778): a second set of values for the colours that carry a meaning, for @@ -333,7 +338,7 @@ pub fn material_color(name: &str) -> Option { /// Switch themes (the visuals follow on the next [`apply`]). pub fn set_dark(dark: bool) { - DARK.store(dark, Ordering::Relaxed); + DARK.set(dark); } fn visuals(dark: bool) -> egui::Visuals { @@ -373,6 +378,24 @@ pub fn apply(ctx: &egui::Context) { #[cfg(test)] mod tests { + #[test] + fn concurrent_ui_renderers_keep_their_own_theme_and_vision() { + let barrier = std::sync::Barrier::new(2); + std::thread::scope(|scope| { + for (dark, vision) in [(false, super::ColorVision::Off), (true, super::ColorVision::Protan)] { + let barrier = &barrier; + scope.spawn(move || { + super::set_dark(dark); + super::set_vision(vision); + barrier.wait(); + assert_eq!(super::is_dark(), dark); + assert_eq!(super::vision(), vision); + assert_eq!(super::Tokens::get().sketch_accent, super::Tokens::themed(dark, vision).sketch_accent); + }); + } + }); + } + /// Title and section bands stand out from a dialog, the title more than its sections. #[test] fn heading_bands_are_darker_than_the_dialog_in_order() { diff --git a/crates/ui-egui/src/timeline.rs b/crates/ui-egui/src/timeline.rs index 867f4d83..3904b026 100644 --- a/crates/ui-egui/src/timeline.rs +++ b/crates/ui-egui/src/timeline.rs @@ -3,7 +3,10 @@ pub(crate) use crate::icons::glyphs::transport_glyph; use egui::{Rect, Sense, Stroke, UiBuilder, pos2, vec2}; use serde_json::json; -use solidcraft_engine::doc::FeatureKind; +use solidcraft_engine::doc::{ + FeatureKind, PlaneRef, + construct::{AxisDef, PointDef}, +}; use crate::theme::Tokens; use crate::{SolveApp, icons}; @@ -13,14 +16,18 @@ pub(crate) fn icon_of(k: &FeatureKind) -> &'static str { FeatureKind::Sketch { .. } => "sketch", FeatureKind::Extrude { .. } => "extrude", FeatureKind::SurfaceExtrude { .. } => "surface_extrude", - FeatureKind::Unstitch { .. } => "unstitch", - FeatureKind::ReverseNormal { .. } => "reverse_normal", + FeatureKind::Unstitch { .. } => "surface_unstitch", + FeatureKind::ReverseNormal { .. } => "surface_reverse_normal", FeatureKind::SurfaceRevolve { .. } => "surface_revolve", FeatureKind::SurfaceSweep { .. } => "surface_sweep", FeatureKind::SurfaceLoft { .. } => "surface_loft", FeatureKind::SurfaceOffset { .. } => "surface_offset", - FeatureKind::SurfaceRuled { .. } => "ruled", - FeatureKind::SurfaceUntrim { .. } => "untrim", + FeatureKind::SurfaceRuled { .. } => "surface_ruled", + FeatureKind::SurfaceUntrim { .. } => "surface_untrim", + FeatureKind::SurfaceBlend { .. } => "surface_blend", + FeatureKind::SurfaceBoundary { .. } => "surface_boundary", + FeatureKind::SurfaceMatch { .. } => "continuity", + FeatureKind::SurfaceFair { .. } => "surface_fair", FeatureKind::Revolve { .. } => "revolve", FeatureKind::Fillet { .. } => "fillet", FeatureKind::Chamfer { .. } => "chamfer", @@ -30,11 +37,25 @@ pub(crate) fn icon_of(k: &FeatureKind) -> &'static str { FeatureKind::Torus { .. } => "torus", FeatureKind::Combine { .. } => "combine", FeatureKind::Pattern { pattern: solidcraft_engine::doc::PatternKind::Rectangular { .. }, .. } => "pattern_rect", + FeatureKind::Pattern { pattern: solidcraft_engine::doc::PatternKind::Path { .. }, .. } => "pattern_path", FeatureKind::Pattern { .. } => "pattern_circ", FeatureKind::Mirror { .. } => "mirror", - FeatureKind::ConstructionPlane { .. } => "plane", - FeatureKind::ConstructionAxis { .. } => "axis", - FeatureKind::ConstructionPoint { .. } => "point", + FeatureKind::ConstructionPlane { plane } => construction_plane_icon(plane), + FeatureKind::ConstructionAxis { def } => match def { + AxisDef::ThroughFace { .. } => "axis_through_cylinder", + AxisDef::NormalToFace { .. } => "axis_normal_to_face", + AxisDef::TwoPlanes { .. } => "axis_two_planes", + AxisDef::TwoPoints { .. } => "axis_two_points", + AxisDef::Edge { .. } => "axis_edge", + }, + FeatureKind::ConstructionPoint { def } => match def { + PointDef::At { .. } => "point_vertex", + PointDef::TwoEdges { .. } => "point_two_edges", + PointDef::ThreePlanes { .. } => "point_three_planes", + PointDef::Center { .. } => "point_center", + PointDef::EdgeAndPlane { .. } => "point_edge_plane", + PointDef::AlongPath { .. } => "point_along_path", + }, FeatureKind::Hole { .. } => "hole", FeatureKind::Loft { .. } => "loft", FeatureKind::Sweep { .. } => "sweep", @@ -43,10 +64,43 @@ pub(crate) fn icon_of(k: &FeatureKind) -> &'static str { FeatureKind::Split { .. } => "split", FeatureKind::Move { .. } => "move", FeatureKind::Import { .. } | FeatureKind::MeshImport { .. } | FeatureKind::MeshConvert { .. } => "import", + FeatureKind::BodyTransfer { .. } => "move", + FeatureKind::Thread { .. } => "thread", + FeatureKind::SplitFace { .. } => "split_face", + FeatureKind::Scale { .. } => "scale", + FeatureKind::OffsetFace { .. } => "offset_face", + FeatureKind::DeleteFace { .. } => "delete_face", + FeatureKind::Remove { .. } => "delete", + FeatureKind::BoundingSolid { .. } => "bounding_solid", + FeatureKind::Pipe { .. } => "pipe", + FeatureKind::Emboss { .. } => "emboss", + FeatureKind::ReplaceFace { .. } => "replace_face", + FeatureKind::Align { .. } => "align", + FeatureKind::Coil { .. } => "coil", + FeatureKind::SheetPerforate { .. } => "perforate", + FeatureKind::BoundaryFill { .. } => "boundary_fill", + FeatureKind::Patch { .. } => "surface_patch", + FeatureKind::Stitch { .. } => "surface_stitch", + FeatureKind::Thicken { .. } => "surface_thicken", + FeatureKind::SurfaceExtend { .. } => "surface_extend", + FeatureKind::SurfaceTrim { .. } => "surface_trim", other => crate::workspace::feature_icon(other).unwrap_or("feature"), } } +fn construction_plane_icon(plane: &PlaneRef) -> &'static str { + match plane { + PlaneRef::AtAngle { .. } => "plane_angle", + PlaneRef::Midplane { .. } => "plane_midplane", + PlaneRef::TwoEdges { .. } => "plane_two_edges", + PlaneRef::ThreePoints { .. } => "plane_three_points", + PlaneRef::Tangent { .. } => "plane_tangent", + PlaneRef::AlongPath { .. } => "plane_along_path", + PlaneRef::Perpendicular { .. } => "plane_perpendicular", + _ => "plane", + } +} + #[derive(Clone, Debug, PartialEq)] enum Drag { Marker, @@ -439,6 +493,93 @@ mod tests { use crate::scenario::Harness; + #[test] + fn construction_history_matches_the_tool_that_created_it() { + use solidcraft_engine::doc::{ + FeatureKind, PlaneRef, + construct::{AxisDef, GeoRef, PointDef}, + }; + use solidcraft_engine::geom::Vec3; + let point = || GeoRef::Point { p: Vec3::ZERO }; + let reference = || Box::new(point()); + let mut cases = Vec::new(); + for (id, def) in [ + ("through_cylinder", AxisDef::ThroughFace { face: point() }), + ("normal_to_face", AxisDef::NormalToFace { face: point(), point: point() }), + ("two_planes", AxisDef::TwoPlanes { a: point(), b: point() }), + ("two_points", AxisDef::TwoPoints { a: point(), b: point() }), + ("edge", AxisDef::Edge { edge: point() }), + ] { + cases.push((format!("construct.axis.{id}"), FeatureKind::ConstructionAxis { def })); + } + for (id, def) in [ + ("vertex", PointDef::At { point: point() }), + ("two_edges", PointDef::TwoEdges { a: point(), b: point() }), + ("three_planes", PointDef::ThreePlanes { a: point(), b: point(), c: point() }), + ("center", PointDef::Center { of: point() }), + ("edge_plane", PointDef::EdgeAndPlane { edge: point(), plane: point() }), + ("along_path", PointDef::AlongPath { path: point(), t: "0.5".into() }), + ] { + cases.push((format!("construct.point.{id}"), FeatureKind::ConstructionPoint { def })); + } + for (id, plane) in [ + ("midplane", PlaneRef::Midplane { a: reference(), b: reference() }), + ("two_edges", PlaneRef::TwoEdges { a: reference(), b: reference() }), + ("three_points", PlaneRef::ThreePoints { a: reference(), b: reference(), c: reference() }), + ("tangent", PlaneRef::Tangent { face: reference(), at: Vec3::ZERO }), + ("along_path", PlaneRef::AlongPath { path: reference(), t: "0.5".into() }), + ("perpendicular", PlaneRef::Perpendicular { plane: reference(), line: reference() }), + ] { + cases.push((format!("construct.plane.{id}"), FeatureKind::ConstructionPlane { plane })); + } + for (id, feature) in cases { + let command = solidcraft_engine::find_command(&id).unwrap(); + let icon = super::icon_of(&feature); + assert_eq!(icon, command.icon, "{id}: history and toolbar disagree"); + assert!(!crate::icon_svg::resolve(icon).placeholder, "{id}"); + } + } + + #[test] + fn new_surface_and_web_history_icons_match_their_commands() { + let cases = [ + ("surface.ruled", json!({"type":"surface_ruled", "body":"A", "edges":[], "distance":"5"})), + ("surface.untrim", json!({"type":"surface_untrim", "face":{"body":"A", "point":[0,0,0]}})), + ("surface.unstitch", json!({"type":"unstitch", "body":"A"})), + ("surface.reverse_normal", json!({"type":"reverse_normal", "body":"A"})), + ("surface.blend", json!({"type":"surface_blend", "from":{"body":"A", "edges":[]}, "to":{"body":"B", "edges":[]}})), + ("surface.boundary", json!({"type":"surface_boundary", "sketch":0, "curves":[]})), + ("surface.match", json!({"type":"surface_match", "body":"A", "target":"B", "edges":[]})), + ("surface.fair", json!({"type":"surface_fair", "body":"A", "tolerance":"0.001"})), + ("solid.rib", json!({"type":"rib", "sketch":0, "curves":[], "thickness":"2"})), + ("solid.web", json!({"type":"rib", "sketch":0, "curves":[], "thickness":"2", "web":true})), + ]; + for (id, definition) in cases { + let feature: solidcraft_engine::doc::FeatureKind = serde_json::from_value(definition).unwrap(); + let command = solidcraft_engine::find_command(id).unwrap(); + assert_eq!(super::icon_of(&feature), command.icon, "{id}: history and toolbar disagree"); + } + } + + #[test] + fn a_path_pattern_keeps_its_path_icon_in_history() { + use solidcraft_engine::doc::{FeatureKind, PatternKind}; + let feature = FeatureKind::Pattern { + features: Vec::new(), + bodies: Vec::new(), + pattern: PatternKind::Path { + path_sketch: solidcraft_engine::doc::SketchId(0), + path: Vec::new(), + count: "2".into(), + spacing: "10".into(), + extent: false, + flip: false, + orient: false, + }, + }; + assert_eq!(super::icon_of(&feature), solidcraft_engine::find_command("solid.pattern.path").unwrap().icon); + } + fn two_boxes(left: bool) -> Harness { let mut h = Harness::new(); h.app.preferences.timeline_left = left; diff --git a/crates/ui-egui/src/timeline_marks_tests.rs b/crates/ui-egui/src/timeline_marks_tests.rs index 906b586b..7406d782 100644 --- a/crates/ui-egui/src/timeline_marks_tests.rs +++ b/crates/ui-egui/src/timeline_marks_tests.rs @@ -1,5 +1,6 @@ //! A failed feature is marked in the bottom timeline and the Feature History by a shape, not only -//! its colour (#972): the error mark (a disc with a cross) where a warning's mark goes. +//! its colour (#972): the error mark where a warning's mark goes (now an icon image, so these +//! tests look for the 10-point image at the mark's place). use egui::{Pos2, Rect, Shape, vec2}; use serde_json::json; @@ -27,20 +28,23 @@ fn features(app: &SolveApp) -> Vec<(String, bool)> { app.session.doc.features.iter().map(|f| (f.name.clone(), app.session.model.result(f.id).is_some_and(|r| r.error.is_some()))).collect() } -/// The discs filled in the error colour (either theme's) that one frame of `draw` paints. +/// The centres of the 10 to 12-point icon images one frame of `draw` paints: the failed-feature mark +/// (the sample has no warnings, so every such image is an error mark). fn error_discs(app: &mut SolveApp, draw: fn(&mut SolveApp, &mut egui::Ui)) -> Vec { crate::scenario::clear_handles(); let ctx = egui::Context::default(); let screen = Rect::from_min_size(Pos2::ZERO, vec2(1400.0, 900.0)); - let out = ctx.run_ui(egui::RawInput { screen_rect: Some(screen), ..Default::default() }, |ui| draw(app, ui)); - let errors = [crate::theme::Tokens::light().error, crate::theme::Tokens::dark().error]; + let mut out = ctx.run_ui(egui::RawInput { screen_rect: Some(screen), ..Default::default() }, |ui| draw(app, ui)); + // Icons upload textures; dropping their deltas unapplied would panic. + out.textures_delta.clear(); let mut found = Vec::new(); for clipped in &out.shapes { each(&clipped.shape, &mut |s| { - if let Shape::Circle(c) = s - && errors.contains(&c.fill) - { - found.push(c.center); + if let Shape::Mesh(m) = s { + let b = m.calc_bounds(); + if matches!(m.texture_id, egui::TextureId::Managed(_)) && (b.width() - 11.0).abs() < 1.5 && (b.height() - 11.0).abs() < 1.5 { + found.push(b.center()); + } } }); } @@ -81,27 +85,3 @@ fn a_failed_feature_is_marked_in_the_feature_history() { let got = marked(&mut app, crate::timeline_left::left_timeline, |item, d| (d.y - item.y).abs() < 1.5 && d.x > item.x); assert_eq!(got, want, "an error mark on each failed feature and on no other"); } - -/// The mark is its own drawing: a disc with a cross in the caller's colours, not the fallback -/// square, and a different shape from the warning's triangle. -#[test] -fn the_error_mark_is_a_disc_with_a_cross() { - let ctx = egui::Context::default(); - let (ink, fill) = (egui::Color32::from_rgb(1, 2, 3), egui::Color32::from_rgb(4, 5, 6)); - let draw = |name: &'static str| { - let out = ctx.run_ui(egui::RawInput::default(), |ui| { - crate::icons::paint(ui.painter(), Rect::from_min_size(Pos2::ZERO, vec2(24.0, 24.0)), name, ink, fill, fill); - }); - let mut shapes = Vec::new(); - for c in &out.shapes { - each(&c.shape, &mut |s| shapes.push(s.clone())); - } - shapes - }; - let error = draw("error"); - let discs = error.iter().filter(|s| matches!(s, Shape::Circle(c) if c.fill == fill && c.stroke.color == ink)).count(); - let strokes = error.iter().filter(|s| matches!(s, Shape::Path(p) if !p.closed && p.stroke.color == egui::epaint::ColorMode::Solid(ink))).count(); - assert_eq!((discs, strokes), (1, 2), "one disc and the two strokes of a cross: {error:?}"); - assert_ne!(format!("{error:?}"), format!("{:?}", draw("warning"))); - assert_ne!(format!("{error:?}"), format!("{:?}", draw("no such icon")), "drawn, not the fallback square"); -} diff --git a/crates/ui-egui/src/toolbar.rs b/crates/ui-egui/src/toolbar.rs index 56cf0166..c6d5a47d 100644 --- a/crates/ui-egui/src/toolbar.rs +++ b/crates/ui-egui/src/toolbar.rs @@ -10,39 +10,10 @@ use crate::{SolveApp, icons}; pub const TABS: &[&str] = &["SOLID", "SURFACE", "MESH", "SHEET METAL", "PLASTIC", "UTILITIES"]; -/// The accent stroke colour for a panel's icons: constraint commands use the same red as the -/// constraint glyphs they draw in the sketch (`glyph_text`), sketch tools otherwise keep -/// `sketch_accent`, and every other tab keeps the plain `accent`. -fn panel_accent(t: Tokens, tab: &str, panel: &str) -> Color32 { - if panel == "CONSTRAINTS" { - t.glyph_text - } else if tab == "SKETCH" { - t.sketch_accent - } else { - t.accent - } -} - -/// The accent stroke colour a command's icon keeps on every surface that draws it, where the -/// command's own meaning fixes it. Construction geometry carries one fixed meaning across the -/// whole app. `sketch.construction` and `sketch.centerline` therefore draw their icon in the -/// theme's construction orange on the ribbon, in a panel's drop-down and under the pointer while -/// being dragged. The Sketch Palette's own line type buttons pass that same orange. `icons::paint` -/// draws both of these icon names in the accent its caller hands it, never in a colour of its own. -/// The caller is the only place the rule can live. Every other command has no fixed colour. It -/// keeps the accent of whichever surface draws it. -fn fixed_accent(t: Tokens, id: &str) -> Option { - match id { - "sketch.construction" | "sketch.centerline" => Some(t.construction_geom), - _ => None, - } -} - -/// The accent stroke colour for one command's icon on the ribbon. It covers the command's own -/// button and a collapsed panel's single button. A command with a `fixed_accent` keeps that. -/// Every other command takes its `panel_accent`. -fn command_accent(t: Tokens, tab: &str, panel: &str, id: &str) -> Color32 { - fixed_accent(t, id).unwrap_or_else(|| panel_accent(t, tab, panel)) +/// Legacy paint arguments and non-SVG affordances follow the same semantic palette as the icon. +/// The requested icon id determines its meaning on ribbons, menus and drag previews alike. +fn command_accent(icon: &str) -> Color32 { + crate::icon_svg::primary(icon, crate::theme::is_dark(), crate::theme::vision()) } /// Panels shown on each tab (in order). Commands come from the registry by (tab, panel). @@ -281,8 +252,7 @@ pub fn app_bar(app: &mut SolveApp, ui: &mut egui::Ui) { if enabled && resp.hovered() { ui.painter().rect_filled(br, 4.0, Color32::from_white_alpha(30)); } - let dim = |c: Color32| if enabled { c } else { c.gamma_multiply(0.35) }; - icons::paint(ui.painter(), br.shrink(5.0), icon, dim(t.app_bar_icon[0]), dim(t.app_bar_icon[1]), dim(t.app_bar_icon[2])); + icons::paint_on(ui.painter(), br.shrink(5.0), icon, Some(true), if enabled { 1.0 } else { 0.35 }); let clicked = resp.on_hover_text(tip).clicked(); enabled && clicked }; @@ -351,7 +321,7 @@ pub fn app_bar(app: &mut SolveApp, ui: &mut egui::Ui) { ui.painter().rect_filled(report, 4.0, if response.hovered() || response.has_focus() { t.hover } else { t.app_bar }); ui.painter().rect_stroke(report, 4.0, Stroke::new(1.0, t.border), egui::StrokeKind::Inside); let icon = Rect::from_center_size(pos2(report.left() + 14.0, report.center().y), vec2(18.0, 18.0)); - icons::paint(ui.painter(), icon, "bug_report", t.app_bar_text, t.app_bar_text, t.app_bar); + icons::paint_on(ui.painter(), icon, "bug_report", Some(true), 1.0); if report_label { ui.painter().text(pos2(report.left() + REPORT_TEXT_X, report.center().y), Align2::LEFT_CENTER, report_text, report_font, t.app_bar_text); } @@ -364,7 +334,7 @@ pub fn app_bar(app: &mut SolveApp, ui: &mut egui::Ui) { if gresp.hovered() { ui.painter().rect_filled(gear, 4.0, Color32::from_white_alpha(30)); } - icons::paint(ui.painter(), gear.shrink(4.0), "settings", t.app_bar_text, t.app_bar_text, t.app_bar); + icons::paint_on(ui.painter(), gear.shrink(4.0), "settings", Some(true), 1.0); crate::scenario::publish_handle("titlebar:preferences", gear.center()); let prefs_tip = if cfg!(target_os = "macos") { format!("{} (Cmd+,)", app.tr("ui.preferences")) @@ -597,7 +567,15 @@ pub fn toolbar(app: &mut SolveApp, ui: &mut egui::Ui) { let ws = Rect::from_min_max(pos2(r.left() + 6.0, r.top() + 4.0), pos2(r.left() + 104.0, r.bottom() - 5.0)); let wresp = ui.interact(ws, ui.id().with("workspace_switcher"), Sense::click()); painter.rect(ws, 4.0, if wresp.hovered() { t.hover } else { t.field }, Stroke::new(1.0, t.border), egui::StrokeKind::Inside); - icons::paint(&painter, Rect::from_center_size(pos2(ws.center().x, ws.top() + 24.0), vec2(26.0, 26.0)), "box", t.icon, t.icon_fill, t.accent); + let workspace_icon = if crate::render_workspace::active(app) { "ws_render" } else { "ws_design" }; + icons::paint( + &painter, + Rect::from_center_size(pos2(ws.center().x, ws.top() + 24.0), vec2(26.0, 26.0)), + workspace_icon, + t.icon, + t.icon_fill, + t.accent, + ); painter.text( pos2(ws.center().x - 5.0, ws.top() + 52.0), Align2::CENTER_CENTER, @@ -719,7 +697,7 @@ pub fn toolbar(app: &mut SolveApp, ui: &mut egui::Ui) { painter.rect_filled(br, 4.0, t.hover); } if let Some(c) = shown.first().or(cmds.first()) { - let acc = command_accent(t, &tab, panel, c.id); + let acc = command_accent(c.icon); icons::paint(&painter, Rect::from_center_size(br.center() - vec2(0.0, 3.0), vec2(26.0, 26.0)), c.icon, t.icon, t.icon_fill, acc); } caret(&painter, pos2(br.center().x, br.bottom() - 3.0), t.text_dim); @@ -769,14 +747,10 @@ pub fn toolbar(app: &mut SolveApp, ui: &mut egui::Ui) { } else if resp.hovered() && info.enabled { painter.rect_filled(br, 4.0, t.hover); } - let (ink, fill, acc) = if info.enabled { - (t.icon, t.icon_fill, command_accent(t, &tab, panel, selected.id)) - } else { - (t.border, t.panel_header, t.border) - }; + let (ink, fill, acc) = + if info.enabled { (t.icon, t.icon_fill, command_accent(selected.icon)) } else { (t.border, t.panel_header, t.border) }; let icon = match selected.id { "sketch.finish" => "finish", - "config.table" => "configuration_table", _ => selected.icon, }; let icon_rect = @@ -1105,11 +1079,9 @@ fn dropdown_row(app: &SolveApp, ui: &mut egui::Ui, c: &'static solidcraft_engine p.rect_filled(rect, 3.0, t.hover); crate::scenario::publish_handle(&format!("dropdown_hot:{}", c.id), rect.center()); } - // Every other row keeps the plain accent it always has. A constraint command's row switches - // to the constraint red, to match its button on the ribbon. A command with a `fixed_accent` - // keeps that colour ahead of either. + // The icon keeps its semantic accent in a drop-down as well as on the ribbon. let (ink, fill, acc, text) = if info.enabled { - let acc = fixed_accent(t, c.id).unwrap_or(if c.panel == "CONSTRAINTS" { t.glyph_text } else { t.accent }); + let acc = command_accent(c.icon); (t.icon, t.icon_fill, acc, t.text) } else { (t.border, t.panel_header, t.border, t.text_dim) @@ -1243,7 +1215,7 @@ fn drag_ghost(ctx: &egui::Context, id: &str, at: egui::Pos2, removing: bool) { let p = ctx.layer_painter(egui::LayerId::new(egui::Order::Tooltip, egui::Id::new("sc_toolbar_drag"))); let r = Rect::from_center_size(at + vec2(14.0, 16.0), vec2(30.0, 30.0)); p.rect(r, 4.0, t.toolbar.gamma_multiply(0.92), Stroke::new(1.0, if removing { t.error } else { t.accent }), egui::StrokeKind::Inside); - icons::paint(&p, r.shrink(5.0), c.icon, t.icon, t.icon_fill, fixed_accent(t, c.id).unwrap_or(t.accent)); + icons::paint(&p, r.shrink(5.0), c.icon, t.icon, t.icon_fill, command_accent(c.icon)); if removing { let s = Stroke::new(2.0, t.error); let x = Rect::from_center_size(r.right_top(), vec2(9.0, 9.0)); @@ -1788,69 +1760,6 @@ mod panel_drop_tests { } } -#[cfg(test)] -mod icon_accent_tests { - use super::*; - - /// `icons.rs` pins one half of the construction icon contract. `construction` and - /// `centerline` take their colour from whatever accent their caller hands over. That half - /// stays green when the caller hands over the wrong colour. That is exactly what the toolbar - /// did. This pins the other half: the colour the toolbar actually passes for those two - /// commands, on every surface the toolbar draws them on. - #[test] - fn the_toolbar_passes_construction_orange_for_both_line_type_commands() { - for t in [Tokens::light(), Tokens::dark()] { - // The regression itself: the SKETCH tab's accent is green. These two icons drew in - // it. Construction orange has to be a colour no panel accent can supply. - assert_ne!(t.construction_geom, t.sketch_accent, "construction orange must differ from the sketch accent"); - assert_ne!(t.construction_geom, t.accent, "construction orange must differ from the plain accent"); - assert_ne!(t.construction_geom, t.glyph_text, "construction orange must differ from the constraint red"); - for id in ["sketch.construction", "sketch.centerline"] { - assert_eq!(fixed_accent(t, id), Some(t.construction_geom), "{id} must have a fixed accent"); - // Its ribbon button, a collapsed panel's single button, and a drop-down row. - assert_eq!(command_accent(t, "SKETCH", "CREATE", id), t.construction_geom, "{id} on the SKETCH ribbon"); - assert_eq!(command_accent(t, "SOLID", "CREATE", id), t.construction_geom, "{id} on the SOLID ribbon"); - assert_eq!(fixed_accent(t, id).unwrap_or(t.accent), t.construction_geom, "{id} in a drop-down row"); - } - } - } - - /// Those two and no others. Every other command's icon keeps the accent of the panel it sits - /// on, so the constraint red and the sketch green both survive the exception above. - #[test] - fn no_other_command_overrides_the_accent_of_its_own_panel() { - let t = Tokens::light(); - let specs = solidcraft_engine::command_specs(); - let mut fixed: Vec<&str> = specs.iter().map(|c| c.id).filter(|id| fixed_accent(t, id).is_some()).collect(); - fixed.sort_unstable(); - assert_eq!(fixed, ["sketch.centerline", "sketch.construction"], "only the two line type commands fix their own accent"); - for c in &specs { - if fixed.contains(&c.id) { - continue; - } - let want = panel_accent(t, c.tab, c.panel); - assert_eq!(command_accent(t, c.tab, c.panel, c.id), want, "{} must keep the accent of its own panel", c.id); - } - // The fix did not flatten every icon to one colour. A sketch tool is still green, a - // constraint still red, a solid tool still blue. A missing id fails this as well. - let accent_of = |id: &str| solidcraft_engine::find_command(id).map(|c| command_accent(t, c.tab, c.panel, c.id)); - assert_eq!(accent_of("sketch.text"), Some(t.sketch_accent)); - assert_eq!(accent_of("sketch.constraint.polygon"), Some(t.glyph_text)); - assert_eq!(accent_of("solid.extrude"), Some(t.accent)); - } - - /// Both line type commands sit on the SKETCH tab's CREATE panel. The accent above is - /// therefore a colour a person can actually see. Their icons are the two `icons.rs` draws - /// from the caller's accent. - #[test] - fn both_line_type_commands_are_create_panel_commands_with_their_own_icons() { - for (id, icon) in [("sketch.construction", "construction"), ("sketch.centerline", "centerline")] { - let seen = solidcraft_engine::find_command(id).map(|c| (c.tab, c.panel, c.icon)); - assert_eq!(seen, Some(("SKETCH", "CREATE", icon)), "{id} must be on the SKETCH CREATE panel"); - } - } -} - #[cfg(test)] mod dropdown_toggle_tests { use crate::scenario::{Harness, rect_of}; diff --git a/crates/ui-egui/src/workspace.rs b/crates/ui-egui/src/workspace.rs index ed99b7eb..bec39cab 100644 --- a/crates/ui-egui/src/workspace.rs +++ b/crates/ui-egui/src/workspace.rs @@ -152,8 +152,9 @@ pub fn feature_icon(k: &FeatureKind) -> Option<&'static str> { FeatureKind::SheetConvert { .. } => "convert_sheet", FeatureKind::Boss { .. } => "boss", FeatureKind::Lip { .. } => "lip", - FeatureKind::SnapFit { .. } => "snap", + FeatureKind::SnapFit { .. } => "snap_fit", FeatureKind::Rest { .. } => "rest", + FeatureKind::Rib { web: true, .. } => "web", FeatureKind::Rib { .. } => "rib", _ => return None, }) diff --git a/docs/robustness.md b/docs/robustness.md index 3516f468..6c350610 100644 --- a/docs/robustness.md +++ b/docs/robustness.md @@ -76,11 +76,14 @@ fails, or leaves a sliver face (narrower than 1/200 of the part), it is tried th other way too: one way can push a face into a curved face it only touched (a cut whose end lies tangent to a round side) and leave a sliver of it, where the other way parts them (#897, #827). -Exact planar and piecewise fallbacks report zero displacement. The existing -volume and spatial acceptance checks still apply to all results. - -The command that adds a feature carries the same `warning` in its reply, so a -script or an agent sees such a retry without asking the timeline. +Exact planar and piecewise fallbacks report zero displacement. When a conical +cutter meets a body with earlier conical faces (pointed bores copied in a pattern), +two finer, unshifted passes are tried first, at 1e-5 and 1e-6 of their size (never +below 1e-6 mm). A coarse first cut can leave tiny junctions that later copies cannot +intersect, so refining only the failing copy is too late. Every candidate must pass +the volume, membership and spatial checks; a rejected candidate continues to the +next tolerance, then the ordinary routes. The existing acceptance checks still +apply to all results. A join or a cut that needed any retry is then held to what it can do at all (`crates/kernel/src/boolean_guard.rs`, #616): the result is closed, a join diff --git a/docs/ui-icon-design-proposal.md b/docs/ui-icon-design-proposal.md new file mode 100644 index 00000000..bcb8d7b7 --- /dev/null +++ b/docs/ui-icon-design-proposal.md @@ -0,0 +1,8 @@ +# UI icon design — superseded proposal + +The format and style decisions are implemented. SVG masters, theme role mapping, +isometric geometry and the owner's reviewed set are authoritative. + +Use [the binding style guide](../assets/icons/ui/STYLE_GUIDE.md) and +[the maintained review workflow](../assets/icons/ui/README.md). The earlier +undecided proposal must not be used to create a competing style or drawing pipeline. diff --git a/docs/ui-icon-inventory.md b/docs/ui-icon-inventory.md new file mode 100644 index 00000000..8377a804 --- /dev/null +++ b/docs/ui-icon-inventory.md @@ -0,0 +1,22 @@ +# UI icon maintenance + +The authoritative authoring specification is +[assets/icons/ui/STYLE_GUIDE.md](../assets/icons/ui/STYLE_GUIDE.md), with mandatory +agent instructions beside it. Start at +[the UI icon README](../assets/icons/ui/README.md) for the included review app, +canonical resource paths, new-icon scaffold, exports and checks. + +Launch the repository-owned review tool with: + +```sh +python3 -B assets/icons/ui/design/tools/icon_status.py --serve +``` + +Open http://127.0.0.1:8770/. Every discovered icon without a current acceptance is +Open. SVGs, aliases and source references are scanned automatically; unused masters +are included. Counts and CSV reports are derived at runtime, not maintained here. +`--csv` writes `target/icon-inventory.csv`. + +The design and cleanup plan is [ui-icon-maintenance-plan.md](ui-icon-maintenance-plan.md). +The old proposal is superseded. SVG masters, palette, aliases, the original Rust +viewport glyphs and owner review records remain the application resources. diff --git a/docs/ui-icon-maintenance-plan.md b/docs/ui-icon-maintenance-plan.md new file mode 100644 index 00000000..121d6a2a --- /dev/null +++ b/docs/ui-icon-maintenance-plan.md @@ -0,0 +1,82 @@ +# Durable icon maintenance — implementation plan + +The goal is for a new contributor or agent, without any chat history, to produce +icons that match the finished set and to find every icon that still needs review. +The SVG masters and valid acceptance records must survive this work unchanged. + +## 1. Establish ownership and clean the resource tree + +- Keep production assets, font licences, platform exports, editable native source + documents and the original SVG masters. Never remove an asset used by packaging. +- Retire superseded numbered authoring batches: they contain duplicate functions, + earlier revisions and implicit overrides. The SVGs are the editable authority. + Keep a reusable geometry library, explicit bases and a new-icon scaffold instead. +- Remove the generated inventory CSV from the source collection; generate reports + under ignored `target/`. Remove the empty proposal-era stand-in list. +- Remove the unused historical macOS raster/SVG exports while preserving their + editable VectorCraft document. Keep a local recovery copy under `target/`. +- Update attribution alongside every resource addition, retirement and move. + +## 2. Give future authors an unmissable specification + +- Link the rules from root AGENTS.md, assets/AGENTS.md and icons/ui/AGENTS.md. +- Specify the categories, exact palette and weights, isometric projection and + face shading, canonical cube/slab bases, action arrows, points, holes, badges, + overlap order, detail limits and optical padding. +- Name finished SVG references for each category and explain what to copy from + their visual language. Commercial artwork remains forbidden. +- Provide a safe starter command that refuses to overwrite an existing icon, + and a validation command that checks attribution, structure and coverage. +- Record a short first-pass acceptance rubric and the complete review workflow. + +## 3. Preserve a small, repository-owned review application + +- Keep the existing Python entry point and loopback URL, requiring only Python 3.11+. +- Split the page, CSS and JavaScript into readable versioned source files. +- Discover SVG masters, aliases, command references, nested UI callers and known + wrapper/table arguments on every launch and explicit rescan. Show unused masters. +- Every discovered, non-final icon is Open, including icons with notes. Missing + artwork is visible and cannot be accepted. Report unassigned commands separately. +- Final means the current artwork matches its saved hash. Changed artwork returns + to Open and can be accepted again deliberately; never silently refresh its hash. +- A normal opening starts at Open / Everything with an empty search. Retain theme + and size preferences; preserve temporary filters only for a live artwork reload. +- Keep autosave, light/dark, small-size variants, notes and portable JSON export. + Export queues neutral artwork feedback locally; it does not invoke a model. +- Preserve notes edited since an export, stale-page merge behavior and accepted art. + +## 4. Verify the basic implementation + +- Test discovery of newly added and unused masters, aliases, missing artwork, + changed acceptance hashes, nested UI callers, stale saves and exact export notes. +- Check the web UI in both themes and at small sizes. Check the style-reference + page and scaffold against the finished geometry. +- Run focused checks in the background; report actual results. Full repository CI + is still required before a local commit. Existing engine robustness failures do + not authorize changing unrelated geometry or accepting failed quality gates. +- Keep this iteration local; do not push or change the existing draft PR #853. + +## Scope of this first implementation + +No icon redesign, palette change, packaging change, new approval state database, +cloud service, model-specific automation or replacement of Rust viewport graphics. +Style quality still needs visual review; lint cannot decide whether a new symbol +communicates its operation well. + +## Basic version implemented + +- The media collection retains the production SVGs and platform assets. The + superseded bulk generators, generated inventory, empty stand-in list and unused + historical exports have been retired; the historical editable native master remains. +- Mandatory instructions are linked from root, assets and UI-specific AGENTS.md. + The style guide, curated references and reusable bases are included in the repo. +- The review interface is split into versioned HTML/CSS/JavaScript, with discovery + on opening/rescan, valid-hash acceptance, visible shared masters, atomic state + saves, exact local exports and protection against stale acceptance and note loss. +- `cargo xtask icons` and the CI repository-check job enforce the icon checks and + regression tests. All existing SVG bytes and initial acceptance hashes were preserved. + +Remaining limits: source discovery recognises the documented literal call patterns, +not every possible Rust expression. Add a fixture when introducing a new icon +wrapper convention. Visual quality, semantic clarity and acceptance remain human +review decisions; the tool does not automatically redraw or finalise artwork. diff --git a/tests/robustness/goldens.json b/tests/robustness/goldens.json index da2c49f0..ff06222b 100644 --- a/tests/robustness/goldens.json +++ b/tests/robustness/goldens.json @@ -14,14 +14,14 @@ "10": {"area":2809.248846,"bodies":1,"edges":39,"error":null,"faces":16,"outcome":"ok","rejected":1,"shells":1,"steps":["revolved section","cut box","fillet","hole"],"unsupported":0,"volume":6696.329905}, "11": {"area":7858.300481,"bodies":1,"edges":33,"error":null,"faces":13,"outcome":"ok","rejected":0,"shells":1,"steps":["extruded profile","hole","rectangular pattern","fillet"],"unsupported":0,"volume":34605.048259}, "12": {"area":4668.664174,"bodies":2,"edges":59,"error":null,"faces":25,"outcome":"ok","rejected":0,"shells":2,"steps":["extruded profile","chamfer","join cylinder","join box","cut cylinder","circular pattern"],"unsupported":3,"volume":11874.471611}, - "13": {"area":2101.521015,"bodies":1,"edges":103,"error":null,"faces":41,"outcome":"ok","rejected":1,"shells":1,"steps":["revolved section","cut cylinder","join box","rectangular pattern","cut cylinder","cut box","fillet","hole"],"unsupported":0,"volume":2396.939548}, + "13": {"area":2101.521015,"bodies":1,"edges":101,"error":null,"faces":40,"outcome":"ok","rejected":1,"shells":1,"steps":["revolved section","cut cylinder","join box","rectangular pattern","cut cylinder","cut box","fillet","hole"],"unsupported":0,"volume":2396.939548}, "14": {"area":10553.267409,"bodies":1,"edges":43,"error":null,"faces":20,"outcome":"ok","rejected":0,"shells":1,"steps":["extruded profile","cut cylinder","shell","fillet"],"unsupported":0,"volume":10369.008764}, "15": {"area":2312.730168,"bodies":1,"edges":39,"error":null,"faces":15,"outcome":"ok","rejected":2,"shells":1,"steps":["revolved section","cut box","rectangular pattern","cut sphere","cut cylinder","cut sphere","join cylinder"],"unsupported":0,"volume":5728.661237}, "16": {"area":8872.873884,"bodies":3,"edges":49,"error":null,"faces":23,"outcome":"ok","rejected":0,"shells":3,"steps":["box","join cylinder","circular pattern","hole"],"unsupported":0,"volume":44953.403148}, "17": {"area":6022.921702,"bodies":1,"edges":48,"error":null,"faces":19,"outcome":"ok","rejected":0,"shells":1,"steps":["revolved section","cut box","shell","cut cylinder"],"unsupported":0,"volume":2981.269959}, "18": {"area":5524.583817,"bodies":4,"edges":98,"error":"boolean Cut: the operation failed: no result","faces":44,"outcome":"failed","rejected":0,"shells":4,"steps":["extruded profile","join cylinder","circular pattern","cut cylinder","hole","cut cylinder","hole","circular pattern"],"unsupported":0,"volume":21568.016672}, "19": {"area":3440.869757,"bodies":1,"edges":42,"error":null,"faces":16,"outcome":"ok","rejected":0,"shells":1,"steps":["revolved section","chamfer","cut sphere"],"unsupported":0,"volume":5909.174021}, - "20": {"area":5727.101189,"bodies":1,"edges":116,"error":null,"faces":45,"outcome":"ok","rejected":1,"shells":1,"steps":["extruded profile","fillet","cut box","cut cylinder","hole","circular pattern","cut box"],"unsupported":0,"volume":17142.727051}, + "20": {"area":5726.92956,"bodies":1,"edges":116,"error":null,"faces":45,"outcome":"ok","rejected":1,"shells":1,"steps":["extruded profile","fillet","cut box","cut cylinder","hole","circular pattern","cut box"],"unsupported":0,"volume":17142.747554}, "21": {"area":5814.791707,"bodies":1,"edges":56,"error":null,"faces":22,"outcome":"ok","rejected":0,"shells":1,"steps":["revolved section","join box","hole","fillet","hole","cut box"],"unsupported":1,"volume":19153.734422}, "22": {"area":4634.449436,"bodies":2,"edges":138,"error":null,"faces":54,"outcome":"ok","rejected":0,"shells":2,"steps":["revolved section","join box","join box","join box","rectangular pattern","cut cylinder"],"unsupported":0,"volume":7855.479914}, "23": {"area":1795.696192,"bodies":1,"edges":16,"error":null,"faces":7,"outcome":"ok","rejected":0,"shells":1,"steps":["revolved section","cut sphere"],"unsupported":0,"volume":3198.694598}, @@ -72,7 +72,7 @@ "68": {"area":7338.675126,"bodies":1,"edges":36,"error":null,"faces":14,"outcome":"ok","rejected":2,"shells":1,"steps":["extruded profile","hole","hole","rectangular pattern","rectangular pattern","fillet","circular pattern"],"unsupported":0,"volume":29780.741772}, "69": {"area":6451.122767,"bodies":1,"edges":60,"error":"offset: a corner whose faces don't meet the same way after t","faces":27,"outcome":"failed","rejected":0,"shells":3,"steps":["revolved section","join box","shell","shell"],"unsupported":0,"volume":4761.477515}, "70": {"area":3682.669157,"bodies":1,"edges":24,"error":"lumps: shell boundaries intersect or touch; cavity ownership","faces":12,"outcome":"failed","rejected":0,"shells":2,"steps":["extruded profile","join box","hole"],"unsupported":1,"volume":9155.027162}, - "71": {"area":3980.574721,"bodies":1,"edges":52,"error":null,"faces":20,"outcome":"ok","rejected":0,"shells":1,"steps":["revolved section","cut box","cut cylinder","hole","join cylinder"],"unsupported":0,"volume":15749.927796}, + "71": {"area":3980.574721,"bodies":1,"edges":50,"error":null,"faces":19,"outcome":"ok","rejected":0,"shells":1,"steps":["revolved section","cut box","cut cylinder","hole","join cylinder"],"unsupported":0,"volume":15749.927796}, "72": {"area":5770.270336,"bodies":2,"edges":44,"error":null,"faces":20,"outcome":"ok","rejected":1,"shells":2,"steps":["box","join cylinder","hole","join box","join box","cut box","cut cylinder","cut cylinder"],"unsupported":0,"volume":16081.801492}, "73": {"area":6868.162117,"bodies":1,"edges":40,"error":"offset: a corner whose faces don't meet the same way after t","faces":18,"outcome":"failed","rejected":0,"shells":1,"steps":["extruded profile","join box","shell"],"unsupported":1,"volume":36461.939381}, "74": {"area":9294.57311,"bodies":1,"edges":18,"error":null,"faces":9,"outcome":"ok","rejected":0,"shells":1,"steps":["box","hole"],"unsupported":0,"volume":55866.183074}, @@ -95,7 +95,7 @@ "91": {"area":3958.046518,"bodies":1,"edges":50,"error":null,"faces":20,"outcome":"ok","rejected":1,"shells":1,"steps":["revolved section","cut cylinder","fillet"],"unsupported":0,"volume":9721.244548}, "92": {"area":2855.139217,"bodies":1,"edges":30,"error":null,"faces":12,"outcome":"ok","rejected":0,"shells":1,"steps":["revolved section","chamfer","cut sphere"],"unsupported":0,"volume":9814.30292}, "93": {"area":7266.404017,"bodies":7,"edges":111,"error":null,"faces":51,"outcome":"ok","rejected":0,"shells":7,"steps":["extruded profile","fillet","chamfer","join box","circular pattern","rectangular pattern","cut cylinder"],"unsupported":0,"volume":12734.393171}, - "94": {"area":3480.70333,"bodies":1,"edges":61,"error":null,"faces":26,"outcome":"ok","rejected":1,"shells":2,"steps":["revolved section","cut cylinder","fillet","hole","rectangular pattern","cut box","cut box"],"unsupported":0,"volume":5456.446856}, + "94": {"area":3480.70333,"bodies":1,"edges":63,"error":null,"faces":27,"outcome":"ok","rejected":1,"shells":2,"steps":["revolved section","cut cylinder","fillet","hole","rectangular pattern","cut box","cut box"],"unsupported":0,"volume":5456.446856}, "95": {"area":2425.131626,"bodies":1,"edges":19,"error":"boolean Cut: the operation failed: no result; the operation ","faces":9,"outcome":"failed","rejected":0,"shells":1,"steps":["revolved section","hole","fillet","cut cylinder","cut sphere"],"unsupported":0,"volume":7106.301922} }, "notes": [ @@ -129,8 +129,10 @@ {"reason":"Seed 31: #1581 no longer maps a pick on an earlier round's seam to some other nearby edge; the second fillet's selection includes the smooth seam at [2, 27.5, 13.5] beside the first round, which is now refused (no corner to round) instead of silently re-rounding a different edge at 1 mm, so the run goes on to a hole. Seed 83: #1316 was recorded before its branch took in #1495; with #1495's shell (an open face next to a concave edge no longer cuts the wall past it) the shelled part keeps more wall (6217 to 6339 mm3), its edges differ, and the 2.5 mm fillet of the first run is not picked; a later fillet is refused as not supported yet (an end face parallel to the edge)","seeds":[31,83]}, {"reason":"the 2D route sews a cut whose outline has a point a hair from a segment's end (text-recut): seed 53's shell (step 3, a 2 mm shell of an extruded L with an arc side, opened at the notch's inner face) builds its cavity by the 2D route in 77 ms, where it took the B-rep path (2.4 s) and gave a part 336 mm³ lighter, which a box can't then be intersected with (no result). Its section at mid height is 338.27 mm² (a distance-rule raster of the profile gives 343.7, rounding the cavity's concave corners); the generator then builds the step 4 fillet it refused before, so the run goes on another way (16954.55 mm³, 48 faces)","seeds":[53]}, {"reason":"#726 (#1687): a full revolve of a profile off the axis comes out with its planar faces whole, so the revolved section has fewer faces and edges. The fuzz generator picks edges and faces by index, so from their second step these seeds build different designs. Replaying each new design's commands on main and on this branch: seeds 45, 77 and 94 end the same; in seed 21 a box joined onto the ring's flat top now fuses with it, where main kept two touching shells that a later hole split into two bodies.","seeds":[21,45,77,94]}, - {"reason":"#726 (#1718): a partial revolve made as a new body comes out with its planar faces whole, so the revolved section has fewer faces and edges. The fuzz generator picks edges and faces by index, so from their second step these seeds build different designs (seed 24's shell now picks the centroid of a whole 270 deg annulus, which lies in the bore, so it is refused and the run goes on). Replaying each new design's commands on main and on this branch: 12 seeds end the same; in seeds 13 and 26 a box joined onto a flat revolved face now fuses with it (main kept two touching shells that a later step split into two bodies), seed 15's last cylinder join, which main refuses, now builds, and seed 78's cut box no longer leaves 87.5 mm² of extra area (the end cap's); seed 63 refuses the same fillet on both and ends with nearly the same body. Seed 9 now ends ok instead of failing, but its new design ends the same on main. No seed gains a failure.","seeds":[9,10,13,15,17,19,22,24,25,26,30,63,75,76,78,90,91]} -, + {"reason":"#726 (#1718): a partial revolve made as a new body comes out with its planar faces whole, so the revolved section has fewer faces and edges. The fuzz generator picks edges and faces by index, so from their second step these seeds build different designs (seed 24's shell now picks the centroid of a whole 270 deg annulus, which lies in the bore, so it is refused and the run goes on). Replaying each new design's commands on main and on this branch: 12 seeds end the same; in seeds 13 and 26 a box joined onto a flat revolved face now fuses with it (main kept two touching shells that a later step split into two bodies), seed 15's last cylinder join, which main refuses, now builds, and seed 78's cut box no longer leaves 87.5 mm² of extra area (the end cap's); seed 63 refuses the same fillet on both and ends with nearly the same body. Seed 9 now ends ok instead of failing, but its new design ends the same on main. No seed gains a failure.","seeds":[9,10,13,15,17,19,22,24,25,26,30,63,75,76,78,90,91]}, + {"reason":"Current main replay: seed 55 now meshes every face and passes validity; edge-consistent measuring meshes (#1753) change its measured volume from 24741.239331 to 24740.927259 mm3. Seed 71 joins with one fewer redundant face and two fewer edges (19/50); its five steps, successful outcome, volume and area are unchanged within the existing 1e-6 tolerances. Both fixed command sequences were replayed with validity checks after every step.","seeds":[55,71]}, + {"reason":"Seed 20: trace conical cutters against earlier conical cuts at finer unshifted tolerances before coarse copies can leave unusable junctions. The circular pattern and final box cut both build; replayed in the app with no feature errors, unchanged 45 faces/116 edges/one shell, no tool nudge, and volume/spatial guards retained. The finer intersection curves change measured volume by 0.020503 mm3 and area by 0.171629 mm2.","seeds":[20]}, + {"reason":"After integrating #1718, replay the regenerated revolved-section recipes on the combined branch: seed 13 has 40 faces/101 edges (one face/two edges fewer), seed 94 has 27 faces/63 edges (one face/two edges more). Both complete fixed command sequences pass validity and volume-direction checks after every step, and two independent golden runs per seed agree exactly. Steps, outcomes, body and shell counts are unchanged; area and volume match within existing 1e-6 relative tolerances (seed 13 volume differs by 0.000002 mm3). Only topology counts are updated. The finer unshifted conical-cut route and its geometry guards remain in place.","seeds":[13,94]}, {"reason":"#1670 and #1753 (mesh export: twisted ruled faces divided to the chord tolerance, and the faces of an edge sharing its points exactly) change the meshes the kernel's validity and volume checks measure: seed 55's body (a filleted box cut by spheres and patterned) now passes the validity check it failed, and its volume moves by 1.3e-5 relative. Pull requests ran only every 8th seed, so main's full run found it. Recorded from main's CI output (Linux).","seeds":[55]} ] } diff --git a/tests/tools/test_icon_export.py b/tests/tools/test_icon_export.py new file mode 100644 index 00000000..18b08506 --- /dev/null +++ b/tests/tools/test_icon_export.py @@ -0,0 +1,142 @@ +"""Exercise the real HTTP export endpoint without touching the working review or queue.""" +import json +import csv +import pathlib +import shutil +import socket +import subprocess +import sys +import tempfile +import time +import unittest +import urllib.error +import urllib.request + + +class IconExportTest(unittest.TestCase): + def test_inventory_includes_nested_and_helper_icon_uses(self): + source = pathlib.Path(__file__).resolve().parents[2] / 'assets/icons/ui/design/tools' + with tempfile.TemporaryDirectory() as tmp: + root = pathlib.Path(tmp) + design = root / 'assets/icons/ui/design' + tools = design / 'tools' + tools.mkdir(parents=True) + for name in ('icon_status.py', 'review_state.py', 'icon_palette.py'): + shutil.copy(source / name, tools / name) + shutil.copytree(source / 'review', tools / 'review') + (design / 'palette.toml').write_text('[roles.line]\nlight="#000000"\ndark="#ffffff"\n') + for name in ('aliases.toml',): + (design / name).write_text('') + svg = design / 'svg/model' + svg.mkdir(parents=True) + for name in ('agent_chat', 'pattern_flip', 'normal', 'infinite', 'trace'): + (svg / (name + '.svg')).write_text('') + ui = root / 'crates/ui-egui/src' + (ui / 'agent_chat').mkdir(parents=True) + (ui / 'agent_chat/panel.rs').write_text('icons::paint(p, r, "agent_chat", ink, fill, a);') + (ui / 'dialogs.rs').write_text('pattern_toggle(ui, alignment_label, "Flip direction", "pattern_flip", reversed);') + (ui / 'sketch_palette.rs').write_text(''' + for (i, (label, icon, tip)) in [ + ("—", "normal", "Normal"), + ("↔", "infinite", "Infinite"), + ].iter().enumerate() { icons::paint(p, r, icon, ink, fill, a); } + ''') + engine = root / 'crates/engine/src/cmd' + engine.mkdir(parents=True) + # A long builder must not lose its icon after the first fourteen lines. + (engine / 'motion.rs').write_text('CommandSpec::new("motion.trace", "Trace", trace)\n' + + '\n' * 16 + '.icon("trace").params("doc");') + subprocess.run([sys.executable, '-B', str(tools / 'icon_status.py'), '--csv'], + check=True, capture_output=True, text=True) + with open(root / 'target/icon-inventory.csv') as f: + rows = {r['icon']: r for r in csv.DictReader(f)} + self.assertEqual(set(rows), {'agent_chat', 'pattern_flip', 'normal', 'infinite', 'trace'}) + self.assertIn('agent_chat/panel.rs', rows['agent_chat']['used_by_ui']) + self.assertIn('dialogs.rs', rows['pattern_flip']['used_by_ui']) + for name in ('normal', 'infinite'): + self.assertIn('sketch_palette.rs', rows[name]['used_by_ui']) + self.assertEqual(rows['trace']['example_command'], 'motion.trace') + + def test_export_snapshots_only_pending_notes_for_codex(self): + source = pathlib.Path(__file__).resolve().parents[2] / 'assets/icons/ui/design/tools' + with tempfile.TemporaryDirectory() as tmp: + root = pathlib.Path(tmp) + design = root / 'assets/icons/ui/design' + tools = design / 'tools' + tools.mkdir(parents=True) + for name in ('icon_status.py', 'review_state.py', 'icon_palette.py'): + shutil.copy(source / name, tools / name) + shutil.copytree(source / 'review', tools / 'review') + for name in ('palette.toml', 'aliases.toml'): + (design / name).write_text('') + (design / 'palette.toml').write_text('[roles.line]\nlight="#000000"\ndark="#ffffff"\n') + svg = design / 'svg/model' + svg.mkdir(parents=True) + for name in ('box', 'joint'): + (svg / (name + '.svg')).write_text('') + # An accepted icon's note must never be exported, even if it still has text. + original = {'final': {'box': ''}, 'notes': {'box': 'Keep it', 'joint': 'Round the corners\nUse related icons'}} + (design / 'review.json').write_text(json.dumps(original)) + with socket.socket() as port_socket: + port_socket.bind(('127.0.0.1', 0)) + port = port_socket.getsockname()[1] + proc = subprocess.Popen([sys.executable, str(tools / 'icon_status.py'), '--serve', str(port)], + stdout=subprocess.DEVNULL, stderr=subprocess.PIPE) + try: + url = 'http://127.0.0.1:%d' % port + for attempt in range(100): + try: + with urllib.request.urlopen(url + '/api/review', timeout=1) as response: + base = json.load(response) + break + except urllib.error.URLError: + if proc.poll() is not None: + self.fail(proc.stderr.read().decode()) + time.sleep(0.02) + else: + self.fail('review server did not start') + + def post(endpoint, state): + data = json.dumps({'base': base, **state}).encode() + with urllib.request.urlopen(urllib.request.Request(url + endpoint, data=data), timeout=2) as response: + return json.load(response) + + state = {'final': base['final'], 'notes': base['notes']} + inbox = root / 'target/codex-icon-review/inbox' + post('/api/review', state) + self.assertFalse(inbox.exists(), 'autosave must not request redraws') + result = post('/api/export', state) + self.assertTrue(result['ok']) + self.assertEqual(result['sent'], 1) + requests = list(inbox.glob('*.json')) + self.assertEqual(len(requests), 1) + request = json.loads(requests[0].read_text()) + self.assertEqual(request['id'], result['request_id']) + self.assertEqual(request['notes'], {'joint': original['notes']['joint']}) + self.assertFalse(list(inbox.glob('*.tmp'))) + self.assertFalse((root / 'target/review-requests.log').exists()) + with urllib.request.urlopen(url, timeout=2) as response: + page = response.read().decode() + self.assertIn('local review inbox', page) + self.assertNotIn('Claude', page) + self.assertIn('location.reload()', page) + # Existing tabs also reload when the server's page implementation changes. + with open(tools / 'icon_status.py', 'a') as f: + f.write('\n# page update\n') + with urllib.request.urlopen(url + '/api/review', timeout=2) as response: + self.assertNotEqual(json.load(response)['rev'], base['rev']) + # A note resolved on disk cannot be resurrected by an unchanged stale page. + saved = json.loads((design / 'review.json').read_text()) + saved['notes'] = {'box': 'Keep it'} + (design / 'review.json').write_text(json.dumps(saved)) + empty = post('/api/export', state) + self.assertEqual(empty['sent'], 0) + self.assertEqual(len(list(inbox.glob('*.json'))), 1) + finally: + proc.terminate() + proc.wait(timeout=5) + proc.stderr.close() + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/tools/test_icon_review.py b/tests/tools/test_icon_review.py new file mode 100644 index 00000000..a61d8b34 --- /dev/null +++ b/tests/tools/test_icon_review.py @@ -0,0 +1,331 @@ +"""Long-term discovery and acceptance behavior of the repository-owned review app.""" +import contextlib +import json +import re +from pathlib import Path +import shutil +import socket +import subprocess +import sys +import tempfile +import time +import tomllib +import unittest +import urllib.error +import urllib.request +import xml.etree.ElementTree as ET + +TOOLS = Path(__file__).resolve().parents[2] / 'assets/icons/ui/design/tools' +SVG = '' + + +@contextlib.contextmanager +def review_fixture(): + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + design = root / 'assets/icons/ui/design' + shutil.copytree(TOOLS, design / 'tools') + (design / 'svg/model').mkdir(parents=True) + (design / 'svg/model/box.svg').write_text(SVG) + shutil.copy2(TOOLS.parent / 'palette.toml', design / 'palette.toml') + (design / 'references.toml').write_text('[category.model]\nlabel="Model"\nrule="Standard projection"\nicons=["box"]\n') + (design / 'aliases.toml').write_text('[alias]\nbox_alias="box"\n') + (design / 'review.json').write_text(json.dumps({'final': {'box': ''}, 'notes': {}})) + with socket.socket() as available: + available.bind(('127.0.0.1', 0)) + port = available.getsockname()[1] + process = subprocess.Popen([sys.executable, '-B', str(design / 'tools/icon_status.py'), '--serve', str(port)], + stdout=subprocess.DEVNULL, stderr=subprocess.PIPE) + url = f'http://127.0.0.1:{port}' + def get(path): + with urllib.request.urlopen(url + path, timeout=3) as response: + data = response.read().decode() + return json.loads(data) if path == '/api/review' else data + def post(state, final=None, notes=None, endpoint='/api/review', rev=None): + payload = state if endpoint == '/api/palette' else { + 'base': state, 'final': state['final'] if final is None else final, + 'notes': state['notes'] if notes is None else notes, 'rev': rev or state['rev']} + try: + with urllib.request.urlopen(urllib.request.Request(url + endpoint, data=json.dumps(payload).encode()), timeout=3) as response: + return json.load(response) + except urllib.error.HTTPError as error: + error.close() + raise + try: + for attempt in range(100): + try: + get('/api/review'); break + except urllib.error.URLError: + if process.poll() is not None: raise RuntimeError(process.stderr.read().decode()) + time.sleep(.02) + else: raise RuntimeError('server did not start') + yield root, design, get, post + finally: + process.terminate(); process.wait(timeout=5); process.stderr.close() + + +class IconReviewTest(unittest.TestCase): + def test_inline_clips_and_masks_are_scoped_to_each_preview_and_dashes_use_guides(self): + with review_fixture() as (root, design, get, post): + for name in ('environment_map', 'new', 'draft', 'bounding_solid', 'extend'): + shutil.copy2(TOOLS.parent / f'svg/model/{name}.svg', design / f'svg/model/{name}.svg') + with (design / 'aliases.toml').open('a') as aliases: + aliases.write('surface_env={master="environment_map",accent="surface"}\n') + page = get('/') + fragments = re.findall(r'', page, re.S) + seen = set() + for svg in fragments: + ids = re.findall(r'\bid="([^"]+)"', svg) + self.assertTrue(seen.isdisjoint(ids), 'all theme/vision/alias previews need unique ids') + seen.update(ids) + for ref in re.findall(r'url\(#([^)]+)\)', svg): self.assertIn(ref, ids, 'clip or mask must belong to this SVG') + self.assertGreater(len(seen), 10) + for svg in fragments: + tree = ET.fromstring(svg) + mask_nodes = {id(node) for mask in tree.iter('{http://www.w3.org/2000/svg}mask') for node in mask.iter()} + for node in tree.iter(): + if not node.get('stroke-dasharray'): continue + stroke = node.get('stroke', 'none') + if stroke == 'none': continue + if id(node) in mask_nodes and stroke == 'black': continue + self.assertTrue(any(role in stroke for role in ('roles-guide-', 'active-3-')), stroke) + self.assertNotIn('var(--icon-palette-roles-gap-', svg) + self.assertIn('review-environment_map-off-dark-full-disc-env', page) + def card(icon): return page.split('data-id="' + icon + '"', 1)[1].split('
', 1)[0] + self.assertIn('stroke="var(--icon-palette-roles-active-3-', card('bounding_solid')) + self.assertIn('stroke="var(--icon-palette-accents-sketch-active-3-', card('extend')) + self.assertIn('stroke="var(--icon-palette-roles-guide-', card('draft')) + + def test_live_palette_tokens_save_only_display_colours_and_reject_stale_or_invalid_changes(self): + with review_fixture() as (root, design, get, post): + palette_path = design / 'palette.toml' + original = palette_path.read_text() + palette = tomllib.loads(original) + state = json.loads(get('/api/palette')) + tokens = {token['id']: token for token in state['tokens']} + self.assertIn('roles.ink', tokens) + self.assertIn('accents.surface.active-2', tokens) + self.assertIn('variants.red-green.sketch.active-3', tokens) + self.assertNotIn('accents.construction.guide', tokens, 'guides have one shared colour') + self.assertNotIn('roles.ink.source', tokens) + self.assertTrue(tokens['roles.gap']['knockout']) + accepted = (design / 'review.json').read_bytes() + artwork = (design / 'svg/model/box.svg').read_bytes() + def save(revision, changes): + return post({'revision': revision, 'changes': changes}, endpoint='/api/palette') + replacement = '#123456' if tokens['roles.ink']['dark'] != '#123456' else '#654321' + paints = {'light': tokens['roles.ink']['light'], 'dark': replacement} + changes = {'roles.ink': paints, + 'accents.surface.active-2': {'light': tokens['accents.surface.active-2']['light'], 'dark': '#bc789a'}, + 'variants.red-green.sketch.active-3': {'light': tokens['variants.red-green.sketch.active-3']['light'], 'dark': '#778899'}} + saved = save(state['revision'], changes) + self.assertTrue(saved['ok']) + expected = tomllib.loads(original) + for key, colours in changes.items(): + target = expected + for part in key.split('.'): target = target[part] + target.update(colours) + self.assertEqual(tomllib.loads(palette_path.read_text()), expected) + self.assertIn('# Icon colour roles.', palette_path.read_text(), 'comments remain intact') + self.assertEqual(tomllib.loads(palette_path.read_text())['roles']['ink']['source'], palette['roles']['ink']['source']) + for revision, changes in [ + (state['revision'], {'roles.ink': paints}), + (saved['revision'], {'roles.ink.source': paints}), + (saved['revision'], {'accents.construction.guide': paints}), + (saved['revision'], {'roles.gap': paints}), + (saved['revision'], {'roles.ink': {'light': 'url(evil)', 'dark': replacement}}), + ]: + before = palette_path.read_bytes() + with self.assertRaises(urllib.error.HTTPError): save(revision, changes) + self.assertEqual(palette_path.read_bytes(), before) + page = get('/') + self.assertIn('var(--icon-palette-roles-line-light,', page) + self.assertIn('--icon-palette-roles-ink-dark:' + replacement, page) + self.assertIn('id="palette-tokens"', page) + self.assertIn('input.oninput', page) + self.assertIn('body.palette-preview .card.final .art{opacity:1}', page) + self.assertEqual((design / 'review.json').read_bytes(), accepted) + self.assertEqual((design / 'svg/model/box.svg').read_bytes(), artwork) + self.assertFalse(list(design.glob('*.tmp'))) + + def test_knockouts_mask_only_preceding_layers_and_preserve_later_foreground(self): + with review_fixture() as (root, design, get, post): + source = '''''' + path = design / 'svg/model/cutout.svg' + path.write_text(source) + accepted = (design / 'review.json').read_bytes() + page = get('/') + fragments = [svg for svg in re.findall(r'', page, re.S) if '-base"' in svg] + self.assertGreater(len(fragments), 3) + ns = '{http://www.w3.org/2000/svg}' + for svg in fragments: + tree = ET.fromstring(svg) + mask = tree.find(ns + 'defs/' + ns + 'mask') + self.assertIsNotNone(mask) + self.assertEqual(mask.get('maskUnits'), 'userSpaceOnUse') + self.assertEqual(mask[0].get('fill'), 'white') + self.assertIn('-left)', mask[1].get('clip-path')) + self.assertEqual(mask[1][0].get('fill'), 'black') + preceding, foreground = list(tree)[-2:] + self.assertEqual(preceding.get('mask'), 'url(#' + mask.get('id') + ')') + self.assertTrue(preceding[0].get('id').endswith('-base')) + self.assertTrue(foreground.get('id').endswith('-foreground')) + self.assertIsNone(foreground.get('mask')) + self.assertNotIn('roles-gap-', svg) + self.assertEqual(path.read_text(), source) + self.assertEqual((design / 'review.json').read_bytes(), accepted) + + def test_semantic_colours_reload_from_one_palette_without_changing_acceptance(self): + with review_fixture() as (root, design, get, post): + palette = design / 'palette.toml' + colours = tomllib.loads(palette.read_text()) + source = colours['roles']['active-3'].get('source', colours['roles']['active-3']['light']) + art = SVG.replace('currentColor', source) + for name in ('line', 'c_fix'): + (design / f'svg/model/{name}.svg').write_text(art) + with (design / 'aliases.toml').open('a') as aliases: + aliases.write('ground="c_fix"\nsurface_line={master="line",accent="surface"}\n') + state, accepted = get('/api/review'), (design / 'review.json').read_bytes() + def card(page, icon): + return page.split('data-id="' + icon + '"', 1)[1].split('
', 1)[0] + page = get('/') + self.assertIn(colours['accents']['sketch']['active-3']['light'], card(page, 'line')) + self.assertIn(colours['variants']['red-green']['sketch']['active-3']['dark'], card(page, 'line'), + 'red/green vision shares the application ramp') + self.assertIn(colours['accents']['constraint']['active-3']['light'], card(page, 'c_fix')) + self.assertIn(colours['roles']['active-3']['light'], card(page, 'ground'), + 'a shared master does not transfer its meaning to an alias') + self.assertIn(colours['accents']['surface']['active-3']['light'], card(page, 'surface_line'), + 'explicit alias accent wins over the Sketch master') + def set_light(section, colour): + lines, current = palette.read_text().splitlines(), '' + for index, line in enumerate(lines): + if line.strip().startswith('['): + current = line.strip().split(']', 1)[0] + ']' + elif current == '[' + section + ']' and line.split('=', 1)[0].strip() == 'light': + lines[index] = 'light = ' + json.dumps(colour) + palette.write_text('\n'.join(lines) + '\n') + return + self.fail('missing palette section ' + section) + replacement = '#123456' if colours['accents']['sketch']['active-3']['light'] != '#123456' else '#654321' + set_light('accents.sketch.active-3', replacement) + self.assertNotEqual(state['rev'], get('/api/review')['rev']) + self.assertIn(replacement, card(get('/'), 'line'), 'a running server reads the updated palette') + base = '#345678' if colours['roles']['active-3']['light'] != '#345678' else '#876543' + set_light('roles.active-3', base) + self.assertIn(base, card(get('/'), 'ground'), 'source identifiers survive changing the base display palette too') + self.assertEqual((design / 'review.json').read_bytes(), accepted) + self.assertEqual((design / 'svg/model/line.svg').read_text(), art) + + def test_new_unused_masters_and_alias_targets_are_never_hidden(self): + with review_fixture() as (root, design, get, post): + original = get('/api/review') + page = get('/') + self.assertIn('data-id="box"', page, 'unused alias target must still have a card') + self.assertIn('data-id="box_alias"', page) + (design / 'svg/model/brand_new.svg').write_text(SVG) + current = get('/api/review') + self.assertNotEqual(original['rev'], current['rev']) + self.assertNotIn('brand_new', current['final']) + page = get('/') + self.assertIn('data-id="brand_new"', page) + self.assertIn("let filter = 'open', tab = ''", page) + self.assertNotIn("const VK='icon-view'", page) + self.assertIn('cards.length - finals', page, 'notes remain a subset of Open') + post(current, final=current['final'] + ['brand_new']) + self.assertIn('brand_new', get('/api/review')['final']) + # A new opening does not inherit a previous session's hidden tab/query. + self.assertIn('data-f="open" class="on"', get('/')) + + def test_changed_art_reopens_and_requires_deliberate_current_acceptance(self): + with review_fixture() as (root, design, get, post): + initial = get('/api/review') + self.assertIn('box', initial['final']) + old_hash = json.loads((design / 'review.json').read_text())['final']['box'] + (design / 'svg/model/box.svg').write_text(SVG.replace('r="20"', 'r="18"')) + changed = get('/api/review') + self.assertNotIn('box', changed['final']) + self.assertIn('changed since acceptance', get('/')) + self.assertEqual(old_hash, json.loads((design / 'review.json').read_text())['final']['box']) + (design / 'svg/model/box.svg').write_text(SVG.replace('r="20"', 'r="17"')) + with self.assertRaises(urllib.error.HTTPError) as error: + post(changed, final=['box']) + self.assertEqual(error.exception.code, 400) + post(get('/api/review'), final=['box']) + self.assertIn('box', get('/api/review')['final']) + self.assertNotEqual(old_hash, json.loads((design / 'review.json').read_text())['final']['box']) + + def test_missing_art_is_visible_but_cannot_be_accepted(self): + with review_fixture() as (root, design, get, post): + commands = root / 'crates/engine/src/cmd' + commands.mkdir(parents=True) + (commands / 'example.rs').write_text('CommandSpec::new("example.action", "Example", run).icon("missing");') + page = get('/') + self.assertIn('data-id="missing"', page) + self.assertIn('aria-label="Accept missing as final" disabled', page) + with self.assertRaises(urllib.error.HTTPError): + post(get('/api/review'), final=['box', 'missing']) + + def test_final_alias_protects_shared_art_and_newer_notes_survive_stale_save(self): + with review_fixture() as (root, design, get, post): + state = get('/api/review') + post(state, final=['box', 'box_alias']) + state = get('/api/review') + post(state, final=['box_alias'], notes={'box': 'Unreviewed master note'}) + state = get('/api/review') + self.assertEqual(post(state, endpoint='/api/export')['sent'], 0) + # A stale page changes another note; it must not resurrect replaced feedback. + saved = json.loads((design / 'review.json').read_text()) + saved['notes']['box'] = 'Newer feedback' + (design / 'review.json').write_text(json.dumps(saved)) + post(state, notes={**state['notes'], 'box_alias': 'Another note'}) + self.assertEqual(get('/api/review')['notes']['box'], 'Newer feedback') + self.assertFalse(list(design.glob('*.tmp'))) + + def test_export_resolution_preserves_notes_added_since_the_snapshot(self): + with review_fixture() as (root, design, get, post): + (design / 'svg/model/fresh.svg').write_text(SVG) + state = get('/api/review') + post(state, notes={'fresh': 'Newer feedback'}) + script = """ +import sys +sys.path.insert(0,sys.argv[1]) +import review_state +request={'notes':{'fresh':'Exported feedback'}} +assert review_state.resolve_export(request, {'fresh'}) == [] +assert review_state.load()['notes']['fresh'] == 'Newer feedback' +request={'notes':{'fresh':'Newer feedback'}} +assert review_state.resolve_export(request, {'fresh'}) == ['fresh'] +assert review_state.load()['notes'] == {} +""" + subprocess.run([sys.executable, '-B', '-c', script, str(design / 'tools')], check=True) + + def test_style_reference_page_uses_repository_bases(self): + with review_fixture() as (root, design, get, post): + page = get('/guide') + self.assertIn('Canonical grey cube', page) + self.assertIn('Canonical grey slab', page) + self.assertIn('Standard projection', page) + self.assertIn('STYLE_GUIDE.md', page) + + def test_scaffold_refuses_overwrite_and_starts_unaccepted(self): + with review_fixture() as (root, design, get, post): + # The fixture's palette is intentionally minimal; the scaffold uses + # the real helper constants and emits an editable starter SVG. + (root / 'ATTRIBUTION.md').write_text('# Attribution\n') + command = [sys.executable, '-B', str(design / 'tools/author/new_icon.py')] + first = subprocess.run(command + ['fresh_shape', '--base', 'cube'], capture_output=True, text=True) + self.assertEqual(first.returncode, 0, first.stderr) + artwork = design / 'svg/model/fresh_shape.svg' + self.assertTrue(artwork.exists()) + self.assertNotIn('fresh_shape', get('/api/review')['final']) + self.assertIn('fresh_shape.svg', (root / 'ATTRIBUTION.md').read_text()) + before = artwork.read_bytes() + again = subprocess.run(command + ['fresh_shape'], capture_output=True, text=True) + self.assertNotEqual(again.returncode, 0) + self.assertEqual(artwork.read_bytes(), before) + alias = subprocess.run(command + ['box_alias'], capture_output=True, text=True) + self.assertNotEqual(alias.returncode, 0) + + +if __name__ == '__main__': unittest.main() diff --git a/xtask/src/icons.rs b/xtask/src/icons.rs new file mode 100644 index 00000000..e3f61858 --- /dev/null +++ b/xtask/src/icons.rs @@ -0,0 +1,16 @@ +//! Original UI icon standards and review-tool regression checks. +use std::path::Path; +use std::process::Command; + +pub fn run(root: &Path) -> Result<(), String> { + for args in [ + vec!["-B", "assets/icons/ui/design/tools/lint_icons.py"], + vec!["-B", "assets/icons/ui/design/tools/icon_status.py", "--check"], + vec!["-B", "-m", "unittest", "discover", "-s", "tests/tools", "-p", "test_icon*.py"], + ] { + let mut command = Command::new("python3"); + command.args(args).current_dir(root); + super::run(command, "UI icon standards (Python 3.11+)")?; + } + Ok(()) +} diff --git a/xtask/src/main.rs b/xtask/src/main.rs index 8d009c63..ae7ded72 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -1,13 +1,14 @@ //! Workspace tooling: `cargo xtask `. //! -//! Pure Rust (std, serde_json and sha2). External tools (`cargo`) are invoked through -//! `std::process::Command`. +//! Rust workspace tooling. External tools (`cargo`, and Python 3.11+ for the +//! original icon collection) are invoked through `std::process::Command`. mod app_icons; mod assets; mod book; mod features; mod i18n; +mod icons; mod layers; mod licences; mod lists; @@ -25,6 +26,7 @@ commands: layers enforce the crate dependency layering (plan/architecture.md §3) assets check that every non-code asset file is attributed in ATTRIBUTION.md app-icons regenerate shared app-icon exports for Linux, FreeBSD and Windows + icons validate UI icon standards, discovery and review-tool behavior (Python 3.11+) features list SolidCraft's commands by area in docs/features.md (from the registry) i18n [--check | --add-missing] report how complete the message catalogs in assets/locales are: @@ -61,7 +63,7 @@ commands: web-offline-test [--site DIR] check in headless Chrome that the web app works offline and updates to a new build (a stand-in app, or the web build in DIR such as dist/web) - ci fmt --check, assets, licences --check, lists, layers, clippy -D warnings, then the tests + ci fmt --check, assets, icons, licences --check, lists, layers, clippy -D warnings, then the tests with the wasm check alongside, then web-offline-test when Chrome is installed (stops at the first failing step) "; @@ -72,6 +74,7 @@ fn main() -> ExitCode { let result = match args.first().map(String::as_str) { Some("layers") => cmd_layers(), Some("assets") => assets::run(&root()), + Some("icons") => icons::run(&root()), Some("app-icons") => app_icons::run(&root()), Some("features") => features::run(&root()), Some("i18n") => i18n::run(&root(), &rest), @@ -362,6 +365,7 @@ fn cmd_ci() -> Result<(), String> { c.args(["fmt", "--all", "--", "--check"]); run(c, "cargo fmt --check")?; assets::run(&root())?; + icons::run(&root())?; licences::run(&root(), true)?; lists::run(&root(), false)?; cmd_layers()?;