BREP_app 0.4.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
//! The 3D viewport — everything that draws + drives the central `brep-render`
//! scene, split out of the thin app shell.
//!
//! [`Viewport`] owns the GPU seam between the engine and egui:
//!
//! * The engine's [`RenderCore`] renders the 3D into an app-owned OFFSCREEN
//!   texture (its own MSAA + full-target clear), on eframe's SHARED wgpu
//!   device/queue.
//! * That offscreen texture is composited into egui's own frame via an
//!   `egui_wgpu` paint callback ([`ViewportCallback`]/[`ViewportPaint`]) — a
//!   fullscreen-triangle blit into egui's render pass, at the viewport rect egui
//!   scissors for us — so the 3D and the egui UI share one wgpu frame.
//! * Pointer / wheel / ViewCube over the viewport route into `EngineState`
//!   (mirrors `desktop.rs`).
//!
//! [`Viewport::show`] is the single clean entry the shell calls to draw +
//! interact with the central viewport; [`EngineState`] stays the brain and is
//! borrowed in, never owned here.

use brep_render::controls::{BUTTON_LEFT, BUTTON_MIDDLE, BUTTON_RIGHT};
use brep_render::engine_state::EngineState;
use brep_render::pick::PickCandidate;
use brep_render::style::MultiSelectMode;
use brep_render::render::{FrameParams, GpuScene, RenderCore, COLOR_FORMAT};
use eframe::egui;
use eframe::egui_wgpu;
use std::sync::Arc;

/// GPU-side resources the callback needs, stored in egui's `callback_resources`
/// type-map. Refreshed whenever the offscreen texture is (re)created.
struct ViewportPaint {
    pipeline: wgpu::RenderPipeline,
    bind_group: wgpu::BindGroup,
}

/// The paint callback: a zero-sized, `Send + Sync` marker. All the heavy GPU
/// resources live in `callback_resources`; the actual 3D render already happened
/// (into the offscreen texture) during `App::ui`, so `paint` only blits.
struct ViewportCallback;

/// The app-owned offscreen color target the engine resolves into.
struct Offscreen {
    view: wgpu::TextureView,
    w: u32,
    h: u32,
}

/// The open PICK LIST — the "candidates under the cursor" popup that opens on a
/// plain click with MULTIPLE filter-admitted items under the pointer (and on
/// Alt+click explicitly): the ranked, filter-respecting candidates captured when
/// it opened + the screen anchor (cursor position) to draw the list at.
struct CandidatePopup {
    anchor: egui::Pos2,
    candidates: Vec<PickCandidate>,
}

/// The central 3D viewport: the engine's render core + the app-owned offscreen
/// texture + the egui blit that composites it into egui's frame, plus the
/// pointer/wheel/ViewCube routing. Borrows `&mut EngineState` to draw + drive;
/// never owns the engine brain.
pub struct Viewport {
    /// The engine's wgpu render core, built from eframe's device/queue/format.
    core: RenderCore,
    gpu_scene: GpuScene,
    /// egui's renderer, so we can push `ViewportPaint` into its callback map.
    egui_renderer: Arc<egui::mutex::RwLock<egui_wgpu::Renderer>>,
    blit_layout: wgpu::BindGroupLayout,
    blit_sampler: wgpu::Sampler,
    blit_pipeline: wgpu::RenderPipeline,
    offscreen: Option<Offscreen>,
    /// True while a camera drag (orbit/pan) started over the viewport is live.
    dragging: bool,
    /// True while a transform-gizmo HANDLE drag (armed via the in-viewport center
    /// sphere toggle) is live — the pointer press landed on a handle, so the drag
    /// drives the gizmo (edits the feature's transform) instead of orbiting the camera.
    gizmo_dragging: bool,
    /// True while a COMPONENT Move gizmo handle drag is live (assemblies §8.5):
    /// the press landed on the armed component gizmo, so the drag free-moves the
    /// GIZMO ([`EngineState::component_drag_to`]) and the release COMMITS the
    /// composed pose + re-solves ([`EngineState::component_release`]) instead of
    /// orbiting the camera.
    component_gizmo_dragging: bool,
    /// `Some(field_key)` while a DIMENSION-ARROW drag is live (Fix 4) — the press
    /// landed on an arrowhead handle in ◎ dimension mode, so the drag edits that
    /// param via [`EngineState::feature_dimension_drag`] instead of orbiting the
    /// camera. `None` when no arrow drag is in flight.
    dim_dragging: Option<String>,
    /// True while a sketch POINT drag started over the viewport is live (S2) — the
    /// press grabbed a movable sketch point, so the drag moves it instead of
    /// orbiting the camera.
    sketch_dragging: bool,
    /// True while a freehand handdraw STROKE started over the viewport is live
    /// (S6b-3) — the press began a stroke, so the drag captures it as a polyline
    /// instead of orbiting the camera.
    sketch_handdrawing: bool,
    /// The viewport rect (egui points) from the last `show`, so the shell can
    /// publish the viewport origin for the headed verifier (which needs to turn
    /// the engine's viewport-local pick coords into page pixels).
    last_rect: Option<egui::Rect>,
    /// The open pick-list popup (a plain multi-candidate click / Alt+click), or
    /// None. Holds the ranked candidates snapshot + the cursor anchor.
    candidate_popup: Option<CandidatePopup>,
    /// True on the frame the popup opens, so the OPENING Alt+click isn't misread
    /// as a click-outside that would close it immediately.
    candidate_popup_fresh: bool,
    /// The OPEN popup's bounding rect (egui points) from the last draw, so the
    /// viewport click handler can tell a click ON the popup from one BEHIND it
    /// (an entry click belongs to the popup, not a scene pick).
    candidate_popup_rect: Option<egui::Rect>,
    /// Per-entry screen rects of the OPEN popup (index-aligned to its
    /// candidates), published for the headed verifier to click a specific entry.
    candidate_hits: Vec<egui::Rect>,
    /// The dimension label currently being edited (S5): its constraint id + the live
    /// text buffer, or `None` when no field is open. Clicking a label opens it,
    /// Enter applies, Esc cancels.
    editing_dim: Option<(serde_json::Value, String)>,
    /// True on the frame a dim edit field opens, so its seeded text gets focus
    /// before the click-outside logic can close it.
    dim_edit_fresh: bool,
    /// The FEATURE-dimension label currently being edited (FD-1): `(feature_id,
    /// field_key, live text buffer)`, or `None`. Mirrors `editing_dim` for the ◎
    /// dimension-gizmo mode. Clicking a dim label opens it, Enter applies via
    /// [`EngineState::feature_dimension_set_value`], Esc cancels; dragging drives
    /// [`EngineState::feature_dimension_drag`].
    editing_feature_dim: Option<(String, String, String)>,
    /// True on the frame a feature-dim edit field opens (focus seeding).
    feature_dim_edit_fresh: bool,
    /// True while an ASSEMBLY-CONSTRAINT handle drag is live (build-spec §8.4
    /// grabbable arrows) — the press landed on a distance leader / angle-arc
    /// handle, so the drag previews that constraint's value via
    /// [`EngineState::constraint_drag_to`] (release commits + auto-solves)
    /// instead of orbiting the camera.
    constraint_dragging: bool,
    /// The constraint id whose LABEL the pointer hovered LAST frame (the
    /// element-highlight lane) — cleared through
    /// [`EngineState::constraint_hover_end`] when the pointer leaves the labels.
    constraint_label_hovered: Option<String>,
    /// The PMI label chip under the pointer (its referenced geometry is
    /// highlighted; ended exactly once when the pointer leaves).
    pmi_label_hovered: Option<String>,
    /// The PMI label chip being dragged (its world anchor follows the pointer
    /// on the label's depth plane; release ends the coalesced undo step).
    pmi_label_dragging: Option<String>,
    /// The PMI chips drawn LAST frame, `(annotation id, screen rect)` — the
    /// `__brepPmiLabelHit` verifier global (drag / click a chip by rect).
    pmi_label_hits: Vec<(String, egui::Rect)>,
}

impl Viewport {
    /// Drop everything this viewport is holding ABOUT ONE DOCUMENT — called when
    /// the shell switches the active document (see `crate::document`).
    ///
    /// The GPU cache is the load-bearing half: [`GpuScene`] keys its retained
    /// buffers by SOLID NAME and reuses one while the solid's `revision` is
    /// unchanged, but revisions are per-`EngineState` and every document counts
    /// from the same place — so document B's "Box" rev 1 would be served
    /// document A's uploaded mesh. Only the active document renders, so a full
    /// re-upload on the switch is the right price. The transients after it
    /// (pick popup, open dimension editors, in-flight drags) all name entities
    /// of the document that is going away.
    pub fn forget_document(&mut self) {
        self.gpu_scene = GpuScene::default();
        self.close_candidate_popup();
        self.candidate_hits.clear();
        self.editing_dim = None;
        self.editing_feature_dim = None;
        self.constraint_label_hovered = None;
        self.pmi_label_hovered = None;
        self.pmi_label_dragging = None;
        self.pmi_label_hits.clear();
        self.dragging = false;
        self.gizmo_dragging = false;
        self.component_gizmo_dragging = false;
        self.dim_dragging = None;
        self.sketch_dragging = false;
        self.sketch_handdrawing = false;
        self.constraint_dragging = false;
    }

    /// Close the pick-list popup if open; returns whether one WAS open. The app
    /// shell's global Escape routes here FIRST so dismissing the list never
    /// clears a selection built through it.
    pub fn close_candidate_popup(&mut self) -> bool {
        let was_open = self.candidate_popup.is_some();
        self.candidate_popup = None;
        self.candidate_popup_rect = None;
        was_open
    }
}

mod blit;
mod interaction;
mod labels;

/// The hit keys the viewport publishes (see `automation::hit_keys`).
pub static HIT_KEYS: &[crate::automation::hit_keys::HitKeyDoc] = &[
    crate::automation::hit_keys::HitKeyDoc { panel: "candidate", prefix: "", meaning: "one rect per entry of the open pick-candidate popup, keyed by index", command: None },
    crate::automation::hit_keys::HitKeyDoc { panel: "pmilabel", prefix: "", meaning: "one rect per PMI label chip, keyed by annotation id", command: None },
];