brep-app — the BREP CAD application
The BREP CAD application (on crates.io as BREP_app, live at 0.1.0, this
tree 0.2.0; lib
brep_app): an eframe (egui + wgpu) app
hosting the brep-render engine on the BREP kernel — one codebase for
web and desktop, no DOM, the 3D drawn by the engine and the UI drawn by
egui, sharing one wgpu frame. Build the wasm app with ./build.sh app at the
repository root; the generated docs site lands in web/help/.
Dependency chain: BREP_app → BREP_render → { BREP_kernel,
BREP_gizmos, BREP_reconstruction }.
Architecture
EngineState(brep-render) stays the windowing-agnostic brain — scene, camera, controls, settings, selection, sketch mode, history, widgets, pointer/wheel/ViewCube/pick. Not forked; panels borrow&mut EngineState.RenderCore::render_to_viewdraws the 3D into an app-owned offscreen texture (its own 4× MSAA + full-target clear + submit) on eframe's shared wgpu device/queue — the render core is built viaRenderCore::new(device, queue, format)from eframe'sRenderState.- That offscreen texture is composited into egui's own frame by an
egui_wgpupaint callback — a fullscreen-triangle blit into egui's render pass at the viewport rect egui scissors for the callback. The engine's nativeRgba8Unormoutput is linearized in the blit when egui's target is sRGB so colors round-trip exactly. - The toolbar + 3D ViewCube drive
EngineStatelive (zoom-to-fit, projection toggle, standard views);camera_state_json()is published for the verifiers. Pointer/wheel/ViewCube over the viewport route intoEngineState(mirroring the engine's own desktop shell).
Module layout (one module per panel)
src/app.rs is a thin shell: it owns Documents (every open model — see
below), the Viewport, the Store, and one small state value per panel, and its
eframe::App::ui just lays the panels out. The heavy code lives in focused
modules:
src/document.rs— the OPEN MODELS. EachDocumentis oneEngineState(still the single brain, per model) plus its file identity;Documentsis the open list + the active index, anddocs.engine_mut()is how every panel reaches the active brain. Panels are shared, not per-document: the shell resets their transient state when the active document changes.src/panels/document_tabs.rs— the seam the document tabs report through (click / close / verifier rects). The tabs themselves areegui_tiles' own: each open model is aPaneKind::Documentpane and they share oneTabscontainer, so the 3D pane's tab bar IS the model switcher.dock.rskeeps that bar exclusive (nothing else may be dropped in, no model tab may be dragged out).src/viewport.rs+src/viewport/— the 3D viewport: the engine render core, the app-owned offscreen texture, theegui_wgpublit callback + shader, viewport input routing, and in-viewport labels.src/panels/— one file per UI surface, all following one pattern:toolbar.rs— the top action strip (undo/redo, wireframe, zoom-to-fit, standard views, the file-action seam);toolbar_button.rsis the single source of truth for toolbar-button styling, shared by every toolbar.history.rs— the history feature tree + the schema-driven feature dialogs.scene.rs— the Scene tree;tree.rsis the reusable custom-painted tree-node widget both trees are built on.settings.rs— the Settings window: one tab per section (Display, the schema-driven display settings;Assemblies, the BOM columns), each drawn as atree.rstree, persisted via theStore. Model colours are NOT set here — they are a durablecolormetadata attribute edited in the Info window;Display ▸ Faces ▸ Override model colorsis only the display switch that makes the viewport ignore them.selection.rs— the selection filter (which entity kinds a click picks).sketch.rs— the 2D sketcher's side-panel entry point + in-sketch bars.expressions.rs— the variable sheet (name = expr;) feature params resolve against.file.rs— the model-document modal: New / Open / Save / Save As, plus Import, Export, flat-pattern export, and Insert-Component (oneFileActionper mode).file_explorer.rs— the reusable, embeddable file explorer shared by Open, Save As, Import, and Insert Component. A single click SELECTS a file (highlight + footer preview); a double-click, Enter, or the footer confirm button opens it; ↑/↓ move the selection; folders enter on a single click. Everything is driven through theModelStoreseam, identical on web and desktop.info_windows.rs— pinned per-entity inspector windows.context_bar.rs/action_rail.rs/mode_bar.rs— the selection-driven context actions, the shared right-side action rail that renders them, and the pinned Finish/Cancel controls for special modes.toasts.rs— transient notices queued by the engine.
src/form.rs— the generic, schema-driven form engine: onefield_inputrenderer per field kind (Color,Bool,Enum,Number/Range,Text,Vec3, references, and the expression-capableScalar), used by both the settings tree and the feature dialogs — the structure of the schema drives the UI; no hand-coded widget per field.src/store.rs— the unified persistence seam, the one platform exception:ModelStorehandles settings, dock layout, and model documents through the sameread/writeAPI, with a native filesystem implementation and a wasm in-memory mirror backed asynchronously by IndexedDB.src/palette.rs— a generic searchable command-palette modal (used for "pick one of N named things", e.g. add-feature).src/worker.rs— the wasm history runner: executes the history in a dedicated web worker behind the sameHistoryRunnertrait as the engine's native thread runner, so the UI stays responsive during a run.src/fonts.rs— the bundled UI fonts (assets/, embedded at compile time).
Adding a panel
EngineState (brep-render) is the single brain; a panel is just a small state
struct + a show method that borrows &mut EngineState. To add one — nothing
else changes:
-
Add
src/panels/<name>.rswith a state struct and ashowmethod:use EngineState; use egui; -
Register it: add
pub mod <name>;tosrc/panels/mod.rs. -
Add one field to
BrepAppinsrc/app.rs(<name>: MyPanel) and construct it inBrepApp::new(<name>: MyPanel::new()). -
Add one
self.<name>.show(ui, self.docs.engine_mut());call ineframe::App::ui(or aPaneKindarm inpanels/dock.rs), where you want it. Useself.docs.engine_mut()rather than an accessor onBrepApp: it borrows one FIELD, so it still composes with the disjoint&mut self.<panel>borrow beside it.
Keep the model state in EngineState — panels own only transient editing
buffers. That keeps parallel panel work conflict-free (different files, one field
- one call each in the shell).
Schema-driven UI
There is deliberately no hand-written dialog code per feature or setting:
- Display settings come from the engine:
brep_render::style::settings_schema()returns the ordered field list andRenderSettings::to_json()/apply_json()are a round-trip identity, so adding a field to the engine schema grows a widget with zero UI code changes. Edits are applied toEngineState(which marks the GPU state dirty) and persisted through theStore. - Feature dialogs come from the kernel:
brep_render::features::feature_form_fieldsmaps each feature'sinputParamsSchema(the kernel'sfeature_schema_catalogue) to the sameFormFieldshape, rendered by the samesrc/form.rsengine. Editing a param callsEngineState::update_feature_params→ the engine mutates its history and re-runs → the viewport updates live. Numeric fields accept expressions resolved against the expressions panel's variable sheet. - History is engine-owned (
brep_render::history::History, held byEngineState): the ordered features + rollback index are the single source of truth with undo/redo; the UI keeps no copy and mutates only throughEngineStatemethods.
Version alignment (the crux)
brep-render is on wgpu 29. eframe 0.35 → egui 0.35 → egui-wgpu 0.35 → wgpu ^29.0, so all three unify on wgpu 29 — the paint callback can hand
eframe's device/queue/renderpass straight to the wgpu-29 RenderCore. No wgpu
bump to brep-render.
eframe 0.35 requires Rust ≥ 1.92. The repo's root rust-toolchain.toml
pins the whole crate family — kernel, render engine, and this crate — to 1.92,
so plain cargo and wasm-pack already use it; no +toolchain needed.
Build & run
./build.sh at the repository root is the front door — see the root
README for prerequisites and the full matrix.
Web (wasm)
or manually, from BREP_app/:
# open http://127.0.0.1:8099/web/index.html
web/index.html loads pkg/brep_app.js and calls the exported
start("brep_canvas"). eframe uses WebGPU when available and falls back to
WebGL2.
Desktop (native)
WGPU_POWER_PREF=high
Open, Save As, import, and export use the same in-app file explorer on desktop
and web; no OS-native file-dialog dependency is used. On desktop the explorer
can move to parent, home, and filesystem-root directories and browse the real
filesystem. In the browser it navigates the application's persistent virtual
folder tree under /models (stored in IndexedDB), because browser sandboxing
does not expose the host filesystem. Both implementations support creating and
navigating folders through the same explorer API.
The explorer follows a conventional file-dialog interaction model: a single
click SELECTS a file (highlight + a footer selection preview), and a
double-click, the Enter key, or the footer confirm button (Open / Import /
Insert) commits it — so the destructive "open" gesture is deliberate, which
matters most for Save As (pick a name to overwrite without immediately
committing). Folders enter on a single click. ↑/↓ move the selection whenever a
confirm action is offered; Save As leaves Enter to its own name field.
A left sidebar offers quick-access places (Home / Documents / Downloads /
Models / root on desktop; the /models root on web) plus user-pinned folders
(persisted under the reserved @pinned key); the top bar is a clickable
breadcrumb with an editable path field and back/forward history; and the whole
browse area is a drag-resizable region capped to the viewport. Files list in a
sortable Name / Type / Size / Date table (the primary layout) whose body
scrolls; hidden dotfiles are off by default with a toggle to reveal them —
which never blocks typing a hidden path directly. Size/date come from the
filesystem on desktop; the browser reports size from stored length and leaves
date blank (the IndexedDB records store no timestamps).
WGPU_POWER_PREF=high asks wgpu for the discrete GPU. cargo build is the
compile-only gate on headless boxes.
Windows (cross-compiled from Linux)
See the root README's "Windows (cross-compiled from
Linux)" section — the
toolchain setup and linker config live there; the build itself is
cargo build --release --target x86_64-pc-windows-gnu
from this directory.
Verification
Browser-driven UI verification scripts live in web/verify_*.mjs
(Playwright + system Chrome, resolved from the repo-root node_modules;
override with PLAYWRIGHT_DIR). After building the wasm app:
plus the focused scripts (verify_settings.mjs, verify_history.mjs,
verify_selection.mjs, verify_ref_select.mjs, verify_scene.mjs, …), each
driving the real egui widgets and asserting on screenshots + the published
camera/hit state.
node web/verify_recovery.mjs http://127.0.0.1:8099 proves an unsaved edit survives
a reload as the boot-time recovery offer, restores as a dirty tab, and is gone
after Discard. node web/verify_cancel_run.mjs http://127.0.0.1:8098 needs the
REPO ROOT served (the slow fixture is loaded through ?loadModel=): it names the
feature the worker is executing, cancels it mid-boolean, and rebuilds through the
respawned worker.
node web/verify_mesh_import_worker.mjs http://127.0.0.1:8099 checks STL and OBJ
reconstruction through the actual WASM worker, including invalid input and a
subsequent successful import. Serve BREP_app/ at the supplied URL. This check
does not require WebGPU or a rendered canvas.
node web/verify_stl_import_preview.mjs http://127.0.0.1:8099 drives the real
STL picker, tolerance controls, preview camera, Cancel, Accept, and Undo.
Run with a display (or xvfb-run -a). cargo run --example capture_stl_import_preview
captures the same panel and 3D viewport for the file-format documentation.
Documentation
User docs live in the repo-root docs/ tree (docs/features/,
docs/panels/). The brep-docs crate generates the in-app help site from
those user-facing docs into BREP_app/web/help/ (gitignored, wiped each run);
docs/developer/ is not included. ./build.sh app regenerates the help site
when the generator crate is present.
Every feature page leads its heading with assets/glyphs/icon_<hex>.svg — the
app's own artwork, referenced as an ordinary markdown image so it shows in any
reader — and docs/features/index.md repeats it beside each link. The generator
INLINES those files (and any catalogued glyph character in the prose) as <svg>,
lifts a heading's image off the title so the sidebar can draw the icon beside the
name, and rewrites monochrome ink to currentColor so it reads in both themes.
tests/docs_feature_icons.rs holds each page to the icon features::feature_icon
gives for the feature it documents.
The feature-dialog screenshots embedded in the docs are captured headlessly from the real schema-driven dialogs:
License and links
- License: the repository's Autodrop3d
LICENSE.md(license-filein the manifest). - Repository: https://github.com/mmiscool/NURBS_BREP_kernel (this crate
lives in
BREP_app/). - Live on crates.io:
BREP_app0.1.0; this tree is 0.2.0. See the repository-root publishing guide for the crate-family order and validation.