BREP_app 0.3.0

The BREP CAD application: an eframe (egui + wgpu) host that draws the brep-render 3D engine into an egui frame — native + wasm from one codebase.
Documentation
# 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 { /* the panel's OWN editing buffers only */ }

   impl MyPanel {
       pub fn new() -> Self { Self::default() }

       // Pass the unified store too if the panel persists:
       // (…, store: &dyn ModelStore).
       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| { /* widgets; read/write via `state.*` */ });
       }
   }
   ```

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.