Skip to main content

brep_app/
viewport.rs

1//! The 3D viewport — everything that draws + drives the central `brep-render`
2//! scene, split out of the thin app shell.
3//!
4//! [`Viewport`] owns the GPU seam between the engine and egui:
5//!
6//! * The engine's [`RenderCore`] renders the 3D into an app-owned OFFSCREEN
7//!   texture (its own MSAA + full-target clear), on eframe's SHARED wgpu
8//!   device/queue.
9//! * That offscreen texture is composited into egui's own frame via an
10//!   `egui_wgpu` paint callback ([`ViewportCallback`]/[`ViewportPaint`]) — a
11//!   fullscreen-triangle blit into egui's render pass, at the viewport rect egui
12//!   scissors for us — so the 3D and the egui UI share one wgpu frame.
13//! * Pointer / wheel / ViewCube over the viewport route into `EngineState`
14//!   (mirrors `desktop.rs`).
15//!
16//! [`Viewport::show`] is the single clean entry the shell calls to draw +
17//! interact with the central viewport; [`EngineState`] stays the brain and is
18//! borrowed in, never owned here.
19
20use brep_render::controls::{BUTTON_LEFT, BUTTON_MIDDLE, BUTTON_RIGHT};
21use brep_render::engine_state::EngineState;
22use brep_render::pick::PickCandidate;
23use brep_render::style::MultiSelectMode;
24use brep_render::render::{FrameParams, GpuScene, RenderCore, COLOR_FORMAT};
25use eframe::egui;
26use eframe::egui_wgpu;
27use std::sync::Arc;
28
29/// GPU-side resources the callback needs, stored in egui's `callback_resources`
30/// type-map. Refreshed whenever the offscreen texture is (re)created.
31struct ViewportPaint {
32    pipeline: wgpu::RenderPipeline,
33    bind_group: wgpu::BindGroup,
34}
35
36/// The paint callback: a zero-sized, `Send + Sync` marker. All the heavy GPU
37/// resources live in `callback_resources`; the actual 3D render already happened
38/// (into the offscreen texture) during `App::ui`, so `paint` only blits.
39struct ViewportCallback;
40
41/// The app-owned offscreen color target the engine resolves into.
42struct Offscreen {
43    view: wgpu::TextureView,
44    w: u32,
45    h: u32,
46}
47
48/// The open PICK LIST — the "candidates under the cursor" popup that opens on a
49/// plain click with MULTIPLE filter-admitted items under the pointer (and on
50/// Alt+click explicitly): the ranked, filter-respecting candidates captured when
51/// it opened + the screen anchor (cursor position) to draw the list at.
52struct CandidatePopup {
53    anchor: egui::Pos2,
54    candidates: Vec<PickCandidate>,
55}
56
57/// The central 3D viewport: the engine's render core + the app-owned offscreen
58/// texture + the egui blit that composites it into egui's frame, plus the
59/// pointer/wheel/ViewCube routing. Borrows `&mut EngineState` to draw + drive;
60/// never owns the engine brain.
61pub struct Viewport {
62    /// The engine's wgpu render core, built from eframe's device/queue/format.
63    core: RenderCore,
64    gpu_scene: GpuScene,
65    /// egui's renderer, so we can push `ViewportPaint` into its callback map.
66    egui_renderer: Arc<egui::mutex::RwLock<egui_wgpu::Renderer>>,
67    blit_layout: wgpu::BindGroupLayout,
68    blit_sampler: wgpu::Sampler,
69    blit_pipeline: wgpu::RenderPipeline,
70    offscreen: Option<Offscreen>,
71    /// True while a camera drag (orbit/pan) started over the viewport is live.
72    dragging: bool,
73    /// True while a transform-gizmo HANDLE drag (armed via the in-viewport center
74    /// sphere toggle) is live — the pointer press landed on a handle, so the drag
75    /// drives the gizmo (edits the feature's transform) instead of orbiting the camera.
76    gizmo_dragging: bool,
77    /// True while a COMPONENT Move gizmo handle drag is live (assemblies §8.5):
78    /// the press landed on the armed component gizmo, so the drag free-moves the
79    /// GIZMO ([`EngineState::component_drag_to`]) and the release COMMITS the
80    /// composed pose + re-solves ([`EngineState::component_release`]) instead of
81    /// orbiting the camera.
82    component_gizmo_dragging: bool,
83    /// `Some(field_key)` while a DIMENSION-ARROW drag is live (Fix 4) — the press
84    /// landed on an arrowhead handle in ◎ dimension mode, so the drag edits that
85    /// param via [`EngineState::feature_dimension_drag`] instead of orbiting the
86    /// camera. `None` when no arrow drag is in flight.
87    dim_dragging: Option<String>,
88    /// True while a sketch POINT drag started over the viewport is live (S2) — the
89    /// press grabbed a movable sketch point, so the drag moves it instead of
90    /// orbiting the camera.
91    sketch_dragging: bool,
92    /// True while a freehand handdraw STROKE started over the viewport is live
93    /// (S6b-3) — the press began a stroke, so the drag captures it as a polyline
94    /// instead of orbiting the camera.
95    sketch_handdrawing: bool,
96    /// The viewport rect (egui points) from the last `show`, so the shell can
97    /// publish the viewport origin for the headed verifier (which needs to turn
98    /// the engine's viewport-local pick coords into page pixels).
99    last_rect: Option<egui::Rect>,
100    /// The open pick-list popup (a plain multi-candidate click / Alt+click), or
101    /// None. Holds the ranked candidates snapshot + the cursor anchor.
102    candidate_popup: Option<CandidatePopup>,
103    /// True on the frame the popup opens, so the OPENING Alt+click isn't misread
104    /// as a click-outside that would close it immediately.
105    candidate_popup_fresh: bool,
106    /// The OPEN popup's bounding rect (egui points) from the last draw, so the
107    /// viewport click handler can tell a click ON the popup from one BEHIND it
108    /// (an entry click belongs to the popup, not a scene pick).
109    candidate_popup_rect: Option<egui::Rect>,
110    /// Per-entry screen rects of the OPEN popup (index-aligned to its
111    /// candidates), published for the headed verifier to click a specific entry.
112    candidate_hits: Vec<egui::Rect>,
113    /// The dimension label currently being edited (S5): its constraint id + the live
114    /// text buffer, or `None` when no field is open. Clicking a label opens it,
115    /// Enter applies, Esc cancels.
116    editing_dim: Option<(serde_json::Value, String)>,
117    /// True on the frame a dim edit field opens, so its seeded text gets focus
118    /// before the click-outside logic can close it.
119    dim_edit_fresh: bool,
120    /// The FEATURE-dimension label currently being edited (FD-1): `(feature_id,
121    /// field_key, live text buffer)`, or `None`. Mirrors `editing_dim` for the ◎
122    /// dimension-gizmo mode. Clicking a dim label opens it, Enter applies via
123    /// [`EngineState::feature_dimension_set_value`], Esc cancels; dragging drives
124    /// [`EngineState::feature_dimension_drag`].
125    editing_feature_dim: Option<(String, String, String)>,
126    /// True on the frame a feature-dim edit field opens (focus seeding).
127    feature_dim_edit_fresh: bool,
128    /// True while an ASSEMBLY-CONSTRAINT handle drag is live (build-spec §8.4
129    /// grabbable arrows) — the press landed on a distance leader / angle-arc
130    /// handle, so the drag previews that constraint's value via
131    /// [`EngineState::constraint_drag_to`] (release commits + auto-solves)
132    /// instead of orbiting the camera.
133    constraint_dragging: bool,
134    /// The constraint id whose LABEL the pointer hovered LAST frame (the
135    /// element-highlight lane) — cleared through
136    /// [`EngineState::constraint_hover_end`] when the pointer leaves the labels.
137    constraint_label_hovered: Option<String>,
138}
139
140impl Viewport {
141    /// Drop everything this viewport is holding ABOUT ONE DOCUMENT — called when
142    /// the shell switches the active document (see `crate::document`).
143    ///
144    /// The GPU cache is the load-bearing half: [`GpuScene`] keys its retained
145    /// buffers by SOLID NAME and reuses one while the solid's `revision` is
146    /// unchanged, but revisions are per-`EngineState` and every document counts
147    /// from the same place — so document B's "Box" rev 1 would be served
148    /// document A's uploaded mesh. Only the active document renders, so a full
149    /// re-upload on the switch is the right price. The transients after it
150    /// (pick popup, open dimension editors, in-flight drags) all name entities
151    /// of the document that is going away.
152    pub fn forget_document(&mut self) {
153        self.gpu_scene = GpuScene::default();
154        self.close_candidate_popup();
155        self.candidate_hits.clear();
156        self.editing_dim = None;
157        self.editing_feature_dim = None;
158        self.constraint_label_hovered = None;
159        self.dragging = false;
160        self.gizmo_dragging = false;
161        self.component_gizmo_dragging = false;
162        self.dim_dragging = None;
163        self.sketch_dragging = false;
164        self.sketch_handdrawing = false;
165        self.constraint_dragging = false;
166    }
167
168    /// Close the pick-list popup if open; returns whether one WAS open. The app
169    /// shell's global Escape routes here FIRST so dismissing the list never
170    /// clears a selection built through it.
171    pub fn close_candidate_popup(&mut self) -> bool {
172        let was_open = self.candidate_popup.is_some();
173        self.candidate_popup = None;
174        self.candidate_popup_rect = None;
175        was_open
176    }
177}
178
179mod blit;
180mod interaction;
181mod labels;