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 /// The PMI label chip under the pointer (its referenced geometry is
139 /// highlighted; ended exactly once when the pointer leaves).
140 pmi_label_hovered: Option<String>,
141 /// The PMI label chip being dragged (its world anchor follows the pointer
142 /// on the label's depth plane; release ends the coalesced undo step).
143 pmi_label_dragging: Option<String>,
144 /// The PMI chips drawn LAST frame, `(annotation id, screen rect)` — the
145 /// `__brepPmiLabelHit` verifier global (drag / click a chip by rect).
146 pmi_label_hits: Vec<(String, egui::Rect)>,
147}
148
149impl Viewport {
150 /// Drop everything this viewport is holding ABOUT ONE DOCUMENT — called when
151 /// the shell switches the active document (see `crate::document`).
152 ///
153 /// The GPU cache is the load-bearing half: [`GpuScene`] keys its retained
154 /// buffers by SOLID NAME and reuses one while the solid's `revision` is
155 /// unchanged, but revisions are per-`EngineState` and every document counts
156 /// from the same place — so document B's "Box" rev 1 would be served
157 /// document A's uploaded mesh. Only the active document renders, so a full
158 /// re-upload on the switch is the right price. The transients after it
159 /// (pick popup, open dimension editors, in-flight drags) all name entities
160 /// of the document that is going away.
161 pub fn forget_document(&mut self) {
162 self.gpu_scene = GpuScene::default();
163 self.close_candidate_popup();
164 self.candidate_hits.clear();
165 self.editing_dim = None;
166 self.editing_feature_dim = None;
167 self.constraint_label_hovered = None;
168 self.pmi_label_hovered = None;
169 self.pmi_label_dragging = None;
170 self.pmi_label_hits.clear();
171 self.dragging = false;
172 self.gizmo_dragging = false;
173 self.component_gizmo_dragging = false;
174 self.dim_dragging = None;
175 self.sketch_dragging = false;
176 self.sketch_handdrawing = false;
177 self.constraint_dragging = false;
178 }
179
180 /// Close the pick-list popup if open; returns whether one WAS open. The app
181 /// shell's global Escape routes here FIRST so dismissing the list never
182 /// clears a selection built through it.
183 pub fn close_candidate_popup(&mut self) -> bool {
184 let was_open = self.candidate_popup.is_some();
185 self.candidate_popup = None;
186 self.candidate_popup_rect = None;
187 was_open
188 }
189}
190
191mod blit;
192mod interaction;
193mod labels;
194
195/// The hit keys the viewport publishes (see `automation::hit_keys`).
196pub static HIT_KEYS: &[crate::automation::hit_keys::HitKeyDoc] = &[
197 crate::automation::hit_keys::HitKeyDoc { panel: "candidate", prefix: "", meaning: "one rect per entry of the open pick-candidate popup, keyed by index", command: None },
198 crate::automation::hit_keys::HitKeyDoc { panel: "pmilabel", prefix: "", meaning: "one rect per PMI label chip, keyed by annotation id", command: None },
199];