# 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`](https://github.com/emilk/egui) (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_view` draws 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 via `RenderCore::new(device,
queue, format)` from eframe's `RenderState`.
* That offscreen texture is composited into egui's own frame by an
**`egui_wgpu` paint callback** — a fullscreen-triangle blit into egui's render
pass at the viewport rect egui scissors for the callback. The engine's native
`Rgba8Unorm` output is linearized in the blit when egui's target is sRGB so
colors round-trip exactly.
* The toolbar + 3D ViewCube drive `EngineState` live (zoom-to-fit, projection
toggle, standard views); `camera_state_json()` is published for the
verifiers. Pointer/wheel/ViewCube over the viewport route into `EngineState`
(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. Each `Document` is one `EngineState`
(still the single brain, per model) plus its file identity; `Documents` is the
open list + the active index, and `docs.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 are `egui_tiles`' own:
each open model is a `PaneKind::Document` pane and they share one `Tabs`
container, so the 3D pane's tab bar IS the model switcher. `dock.rs` keeps 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, the `egui_wgpu` blit 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.rs` is 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.rs` is 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 a `tree.rs` tree, persisted via the `Store`. Model colours are NOT set
here — they are a durable `color` metadata attribute edited in the Info
window; `Display ▸ Faces ▸ Override model colors` is 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 (one `FileAction`
per 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 the `ModelStore` seam, 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: one `field_input`
renderer per field kind (`Color`, `Bool`, `Enum`, `Number`/`Range`, `Text`,
`Vec3`, references, and the expression-capable `Scalar`), 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:
`ModelStore` handles settings, dock layout, and model documents through the
same `read` / `write` API, 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 same `HistoryRunner` trait 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**:
1. Add `src/panels/<name>.rs` with a state struct and a `show` method:
```rust
use brep_render::engine_state::EngineState;
use eframe::egui;
#[derive(Default)]
pub struct MyPanel { }
impl MyPanel {
pub fn new() -> Self { Self::default() }
pub fn show(&mut self, ui: &mut egui::Ui, state: &mut EngineState) {
egui::CollapsingHeader::new("My panel")
.id_salt("my-panel")
.default_open(false)
.show(ui, |ui| { });
}
}
```
2. Register it: add `pub mod <name>;` to `src/panels/mod.rs`.
3. Add one field to `BrepApp` in `src/app.rs` (`<name>: MyPanel`) and construct it
in `BrepApp::new` (`<name>: MyPanel::new()`).
4. Add one `self.<name>.show(ui, self.docs.engine_mut());` call in
`eframe::App::ui` (or a `PaneKind` arm in `panels/dock.rs`), where you want it.
Use `self.docs.engine_mut()` rather than an accessor on `BrepApp`: 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 and
`RenderSettings::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 to `EngineState` (which marks the GPU state
dirty) and persisted through the `Store`.
* **Feature dialogs** come from the kernel:
`brep_render::features::feature_form_fields` maps each feature's
`inputParamsSchema` (the kernel's `feature_schema_catalogue`) to the same
`FormField` shape, rendered by the same `src/form.rs` engine. Editing a
param calls `EngineState::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 by
`EngineState`): the ordered features + rollback index are the single source
of truth with undo/redo; the UI keeps no copy and mutates only through
`EngineState` methods.
## 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](../README.md) for prerequisites and the full matrix.
### Web (wasm)
```sh
./build.sh # wasm-pack build BREP_app --target web --out-dir pkg --release
./build.sh serve # serve; open http://localhost:8080/web/
```
or manually, from `BREP_app/`:
```sh
wasm-pack build --target web --out-dir pkg --release # add --no-opt for a faster dev build
python3 -m http.server 8099 --bind 127.0.0.1 # serve the crate root
# 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)
```sh
cargo build --release
WGPU_POWER_PREF=high ./target/release/brep-app
```
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)"](../README.md#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:
```sh
node web/verify.mjs # baseline: shaded render, camera controls, 0 console errors
```
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:
```sh
cargo run --example capture_dialogs # writes docs/features/<name>_dialog.png (change-detected)
```
## License and links
- License: the repository's Autodrop3d [`LICENSE.md`](LICENSE.md)
(`license-file` in the manifest).
- Repository: <https://github.com/mmiscool/NURBS_BREP_kernel> (this crate
lives in `BREP_app/`).
- Live on crates.io: `BREP_app` 0.1.0; this tree is 0.2.0. See the repository-root
[publishing guide](../PUBLISHING.md) for the crate-family order and validation.