12 KiB
Fab → nigig-build CAD gap implementation plan
- Date: 2026-08-27
- Baseline: code-verified review of
makepad/libs/fab(gitdab work branch) vscrates/apps/nigig-build/src/.../workspace/cad(current HEAD). - Scope: port user-facing capabilities from the
fabreference CAD into the nigig-build CAD, adapted to our architecture, not wholesale copy. - Adaptation rules (from earlier decisions):
- Port algorithms/logic from fab; keep our
CadNodeflat arena, our batching/instancing renderer, our mobile-first 430x860 DSL, our tool model. - 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.
- fab communicates only through
api.rsShellActions +AppState. We adapt that as: workspace button/action handlers + our existing dirty-flag sync.
- Port algorithms/logic from fab; keep our
- 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 --libstays 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.
- Pure command table
command_palette.rs:Vec<PaletteItem{ id, label, shorcut, run }>. 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. Eachrundispatches to the sameCadWorkspacehandlers our toolbar buttons already call. - Fuzzy subsequence matcher (pure fn, unit-tested) — port fab's scoring (subsequence + prefix/word-start bonus) exactly.
- Palette overlay in the mobile DSL (a
Viewlist + filterTextInput, arrow-keys + Enter), toggled by the existing keymap or a toolbar button. - 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.
CadViewport::solo_selected— isolate to the selection; toggle off on repeat (isolate_selectedalready does exactly this — expose as hotkey + outliner button).CadViewport::unhide_all— alias for existingshow_all; bind Alt+H.- Bind H = isolated-selected (currently
I), keepItoo. Unit testisolate_selectedround-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.
properties.rsor newinfo_card.rs: pureinfo_card_text(&CadNode, tri_count)returning the multi-line card (kind, id, name, pos, size, layer, tris). Unit-tested.- 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):
- 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. section.rs(pure):SectionPlane{ normal, offset },kept(aabb) -> bool,plane_through(p0, normal), offset/with_offset helpers — port from fab, unit-test.- Panel:
SetSectionbuttons (axis, flip, clear) in the outliner/properties panel. - 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).
explode.rs(pure):ExplodeMode{ ByElement },ExplodeState{ amount },element_offset(id_idx, centre, amount)— port fab's radial rule, unit-test.- Apply offsets in
part_model_matrix_cadnode/the draw when explode active (transform-time, so pick/snap reuse the same offset — no LUT needed). ExplodeStatestored onCadViewport; 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.
sun.rs(pure):SunSettings{ latitude, longitude, date, hour }, NOAAlikesolar_position() -> (azimuth_deg, elevation_deg),compass_point(),direction() -> Vec3f— port fromapi::SkyStateandsun_study.rs, unit-test against known noon values.- Toolbar button opens a small sun panel (date/hour/latitude, play scrub) reusing
the drag-number/TextInput style; set
u_light_dirfromdirection()inviewport_render.rs. - 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:
RenderSettings{ width, height, samples }state onCadWorkspace.- "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).
- 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)
- X-ray: add an
xrayoverlay uniform toDrawCadMesh(or reuse display_mode degree), toggled byAlt+Z+ a toolbar button; only affects the shader, tested byparametersnapshot if present. - Drag-number: port fab's pure
header_drag_math(anchor/step/fine/ctrl) as a Rust fn with tests, and wrap our existing numericTextInputs where ergonomic (properties panel X/Y/Z/W/H/D). Keep current inputs working. - Outliner search + type filter: add a
TextInputfilter in the outliner panel; pure filter fnfilter_rows(rows, query) -> Vec<..>unit-tested; funnel dropdown filters byPartKind.
Acceptance: per-item tests; no regression; lib green.
Explicitly NOT porting (with reason)
- fab's
api.rsshell/ShellActiondictionary — our app has a different action model and mobile-first layout; adopting it would be a rewrite. ui/shell.rsdock /area.rsswappable editors /menubar.rs— desktop-chrome that our 430x860 mobile UI does not host; our toolbar + bottom sheet already cover it.render/mod.rscamera-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.rsfull hue-ring picker — nice-to-have; we have a 9-swatch palette; deferred unless requested.nav/gizmo.rsaxis-ball gizmo — we have a nav pad + preset views; low ROI.ui/dragnum.rsdrag-number value field — parity with fab: we have numericTextInputs in the properties panel; full drag+fine-control (anchor/step/ctrl) is a UX polish, not an inspection capability. (cross-ref Phase H.2.)render/mod.rsprogressive 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.