# Fab → nigig-build CAD gap implementation plan - **Date:** 2026-08-27 - **Baseline:** code-verified review of `makepad/libs/fab` (gitdab work branch) vs `crates/apps/nigig-build/src/.../workspace/cad` (current HEAD). - **Scope:** port user-facing capabilities from the `fab` reference CAD into the nigig-build CAD, **adapted to our architecture**, not wholesale copy. - **Adaptation rules (from earlier decisions):** 1. Port **algorithms/logic** from fab; keep our `CadNode` flat arena, our batching/instancing renderer, our mobile-first 430x860 DSL, our tool model. 2. fab is an **architecture viewer/inspection** app (measure, section, isolate, explode, sun study, walk, render) — it has **no geometry-authoring tools**. Our CAD already has authoring (18 tools). So every port target is a *reader/inspection/rendering* feature we lack, layered on our existing model. 3. fab communicates only through `api.rs` `ShellAction`s + `AppState`. We adapt that as: workspace button/action handlers + our existing dirty-flag sync. - **Completion standard (echoes repo convention):** a phase is done only when its numbered items all land with unit tests, compile clean, and `cargo test -p nigig-build --lib` stays green. --- ## Inventory: what fab has that we lack (verified) | fab capability | file | status in our CAD | |---|---|---| | Section planes (drag handle, caps, GPU discard) | `tools/section.rs`, `viewport/dsl.rs` | **missing** — big gap | | Explode view (by-storey / by-element) | `tools/explode.rs` | **missing** | | Sun study (NOAA solar, day/hour scrub, compass) | `tools/sun_study.rs`, `tools/overlay.rs` | **missing** | | Full object snap incl. midpoints/face + glyph preview | `tools/snap.rs` (we have most already) | **partial** — we lack glyph/ghost preview + normal | | Element info card (I) + reveal in outliner | `tools/info.rs` | **missing** | | Command palette (F3 fuzzy) + keymap help (F1) | `ui/command_palette.rs`, `ui/keymap.rs` | **missing** — high value, low risk | | Isolate/solo/hide/unhide (H/Shift+H/Alt+H, `/`) | `tools/isolate.rs` | **partial** — we have isolate (I) + per-part hide via outliner; no solo, no unhide-all hotkey | | F12 high-res render + Save PNG / track-to-mp4 | `render/mod.rs` | **missing** (we have ray mode, no export render) | | Progressive path-traced preview | `viewport/mod.rs` | **missing** (we have Realistic/Ray view modes) | | Drag-number / value field; colour picker | `ui/dragnum.rs`, `ui/colorpick.rs` | **partial** — we have numeric TextInputs; no drag+fine control | | X-ray toggle; 6 shading modes | `api.rs`, `viewport/dsl.rs` | **partial** — we have 6 modes via display_mode; no x-ray | | `●`/`○` outliner (done), gets search + type filter | `ui/outliner.rs` | **partial** — done base; no search/filter/funnel | | Predefined camera views (front/right/top/iso) | `nav/`, `api.rs`, `keymap.rs` | **partial** — we have Alt+1-8 presets already | | Frustum culling + BVH (element-level) | `model/bvh.rs` | **done** — we already ported BVH + frustum culling | | Instancing/batching by shared shape | — | **done** — we have ShapeHash instancing | | Measure distance/angle/area | `tools/measure.rs` | **done** — ported | | Per-part visibility honored in render/pick/snap | `viewport/elements.rs` | **done** (new in this session) | | Properties panel readout | `ui/properties.rs` | **partial** — we have X/Y/Z inputs + kind; no IFC-ish grouped props | --- ## Phase A — Command palette + keyboard map (highest value, lowest risk) **Why first:** delivers broad discoverability and requires no new GPU/scene work; reuses our existing workspace action handlers and already-mapped hotkeys. 1. **Pure command table** `command_palette.rs`: `Vec`. Commands = existing actions we already support: frame all (fit), frame selected, preset views (F5/Alt+1-8), ortho toggle, shading modes, isolate, hide/show all, toggle outliner, undo/redo, open/save, exit. Each `run` dispatches to the same `CadWorkspace` handlers our toolbar buttons already call. 2. **Fuzzy subsequence matcher** (pure fn, unit-tested) — port fab's scoring (subsequence + prefix/word-start bonus) exactly. 3. **Palette overlay** in the mobile DSL (a `View` list + filter `TextInput`, arrow-keys + Enter), toggled by the existing keymap or a toolbar button. 4. **Keymap table** `keymap.rs` — single source of truth for our hotkeys; render an **F1 help** panel from it (like fab). Unit test that every key maps to a real action. **Acceptance:** palette filters and runs ≥6 commands with tests; F1 help renders from the table; `parameter.palette` tests green; full lib suite green. --- ## Phase B — Isolate/solo/hide/unhide parity (small, our mechanism) **Adapt:** fab uses an *isolation set* / solo mode; we use `__hidden__` name prefix (made real this session). Extend, do not rewrite. 1. `CadViewport::solo_selected` — isolate to the selection; toggle off on repeat (`isolate_selected` already does exactly this — expose as hotkey + outliner button). 2. `CadViewport::unhide_all` — alias for existing `show_all`; bind **Alt+H**. 3. Bind **H** = isolated-selected (currently `I`), keep `I` too. Unit test `isolate_selected` round-trips (hide then restore) — add a test now that the visibility mechanism is honored. **Acceptance:** hotkeys + 2 unit tests (isolate round-trip, solo toggle); lib green. --- ## Phase C — Element info card + reveal in outliner **Adapt:** fab's `I` tool card shows type/storey/layer/GUID/size/tri-count/quantities. We have no storey/layer UI per part but have `CadNode` fields (name, kind, pos, size, color, layer) + mesh tri-count via `scene_cache`. 1. `properties.rs` or new `info_card.rs`: pure `info_card_text(&CadNode, tri_count)` returning the multi-line card (kind, id, name, pos, size, layer, tris). Unit-tested. 2. Draw the card as a small label overlay near the hovered part in `viewport_render.rs` (2D + 3D), or reuse the status bar when parked. Follow fab's "click focuses and reveals in outliner" by opening the outliner and selecting the part. **Acceptance:** `info_card_text` tests; overlay/status wiring compiles; lib green. --- ## Phase D — Section planes (largest rendering gap) **Scope honestly:** fab's section = GPU half-space discard + caps in `dsl.rs`. We use a different renderer (`DrawCadMesh` shader, display_mode uniform). A faithful port is large: add half-space uniforms to the shader + caps pass + drag handle + panel. **Adapted approach (bounded):** 1. **CPU clip** in `viewport_render.rs`: when a section plane is active, keep only parts whose AABB is entirely inside the kept half-spaces; draw a plane outline + normal arrow overlay (reuse our existing overlay drawing). This gives the *editor UX* (see the cut live, drag to move) without touching the shader. 2. `section.rs` (pure): `SectionPlane{ normal, offset }`, `kept(aabb) -> bool`, `plane_through(p0, normal)`, offset/with_offset helpers — port from fab, unit-test. 3. Panel: `SetSection` buttons (axis, flip, clear) in the outliner/properties panel. 4. **Shader caps (stretch, gate):** add a CLIP uniform + cap fill only if CPU clip is judged insufficient after a measurement of real scenes. Keep out of the first cut. **Acceptance:** `section.rs` unit tests; CPU-clip + overlay compiles and draws; no regression in lib suite. **Phase marked done even without GPU caps**, which are an explicitly-gated stretch (named as external-effort, consistent with the completion standard). --- ## Phase E — Explode view **Adapt:** our parts have no "storey" grouping by default; support **by-element** radial explode first, include **by-storey** only if a grouping exists (outliner could group by `layer`). 1. `explode.rs` (pure): `ExplodeMode{ ByElement }`, `ExplodeState{ amount }`, `element_offset(id_idx, centre, amount)` — port fab's radial rule, unit-test. 2. Apply offsets in `part_model_matrix_cadnode`/the draw when explode active (transform-time, so pick/snap reuse the same offset — no LUT needed). 3. `ExplodeState` stored on `CadViewport`; slider in the outliner panel actions. **Acceptance:** `explode.rs` tests (element 0 offset = 0; radial sign/direction); transform application compiles; lib green. --- ## Phase F — Sun study **Adapt:** pure NOAA solar model (azimuth/elevation from lat/lon/date/time) + a day scrub. Our CAD has a real `u_light_dir` uniform (per `DrawCadMesh`), so the sun can drive the existing key light + a cast-shadow plane fill. 1. `sun.rs` (pure): `SunSettings{ latitude, longitude, date, hour }`, NOAAlike `solar_position() -> (azimuth_deg, elevation_deg)`, `compass_point()`, `direction() -> Vec3f` — port from `api::SkyState` and `sun_study.rs`, unit-test against known noon values. 2. Toolbar button opens a small sun panel (date/hour/latitude, play scrub) reusing the drag-number/TextInput style; set `u_light_dir` from `direction()` in `viewport_render.rs`. 3. Overlay sun-compass (arc + disc + readout) drawn in the viewport — port the math, keep our draw style. **Acceptance:** `sun.rs` tests (elevation sign at noon, compass names); light-dir wiring compiles; overlay compiles; lib green. --- ## Phase G — F12 high-res render + Save PNG **Adapt:** fab uses a progressive path-traced preview + `FabRenderView`. Our CAD has a **Ray** shading mode via `display_mode` but no standalone capture. Minimal: 1. `RenderSettings{ width, height, samples }` state on `CadWorkspace`. 2. "Render" action captures the current scene at render resolution using our existing DrawCadMesh into an offscreen target, accumulates, and **writes a PNG** (we already export PNG from the arch_pdf path, so the encoder exists — reuse it). 3. Command-palette entry `render-image` (F12). **Acceptance:** a `render settings` pure struct + tests; the PNG write path is wired through an existing tested encoder; no new dependency; lib green. --- ## Phase H — X-ray + shading parity + value-field polish (fill-in gaps) 1. **X-ray:** add an `xray` overlay uniform to `DrawCadMesh` (or reuse display_mode degree), toggled by `Alt+Z` + a toolbar button; only affects the shader, tested by `parameter` snapshot if present. 2. **Drag-number:** port fab's pure `header_drag_math` (anchor/step/fine/ctrl) as a Rust fn with tests, and wrap our existing numeric `TextInput`s where ergonomic (properties panel X/Y/Z/W/H/D). Keep current inputs working. 3. **Outliner search + type filter:** add a `TextInput` filter in the outliner panel; pure filter fn `filter_rows(rows, query) -> Vec<..>` unit-tested; funnel dropdown filters by `PartKind`. **Acceptance:** per-item tests; no regression; lib green. --- ## Explicitly NOT porting (with reason) - **fab's `api.rs` shell/`ShellAction` dictionary** — our app has a different action model and mobile-first layout; adopting it would be a rewrite. - **`ui/shell.rs` dock / `area.rs` swappable editors / `menubar.rs`** — desktop-chrome that our 430x860 mobile UI does not host; our toolbar + bottom sheet already cover it. - **`render/mod.rs` camera-track to mp4** — needs movie encoding we don't ship. - **`file_browser.rs` / in-app open dialog** — platform has no file picker; gated on a platform capability, not effort (matches the completion-standard exception). - **`ui/colorpick.rs` full hue-ring picker** — nice-to-have; we have a 9-swatch palette; deferred unless requested. - **`nav/gizmo.rs` axis-ball gizmo** — we have a nav pad + preset views; low ROI. - **`ui/dragnum.rs` drag-number value field** — parity with fab: we have numeric `TextInput`s in the properties panel; full drag+fine-control (anchor/step/ctrl) is a UX polish, not an inspection capability. ([cross-ref Phase H.2](deferred).) - **`render/mod.rs` progressive path-traced preview (live noise-accumulating view)** — we ship Quality/Realistic/Ray shading modes already; porting fab's live progressive preview to our GPU path is large and gated. See Phase G for the bounded capture/export path we *do* ship. --- ## Recommended order & effort | Phase | Effort | Risk | Do first? | |---|---|---|---| | A Command palette + keymap | S | low | ✅ yes | | B Isolate/solo/outliner parity | XS | low | ✅ yes | | C Info card + reveal | S | low | ✅ yes | | D Section planes (CPU clip) | M | med | next | | E Explode | S | low | next | | F Sun study | M | med | later | | G F12 render + PNG | M | med | later | | H X-ray/dragnum/outliner search | M | med | last | S = small, M = medium. Each phase ends with unit tests + green `--lib` suite, and the GPU-heavy items (D caps, F shadows) are gated as explicit named work rather than silently dropped.