BREP_app 0.2.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 (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_appBREP_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 EngineState (the single brain), 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/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; Per-Solid Colors), each drawn as a tree.rs tree, persisted via the Store.
    • 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:

    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, &mut self.state); call in eframe::App::ui, where you want it in the left column.

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 for prerequisites and the full matrix.

Web (wasm)

./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/:

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)

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)" 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:

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.

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. The feature-dialog screenshots embedded in the docs are captured headlessly from the real schema-driven dialogs:

cargo run --example capture_dialogs    # writes docs/features/<name>_dialog.png (change-detected)

License and links