Skip to main content

brep_render/engine_state/
camera_widgets.rs

1use super::*;
2
3impl EngineState {
4    /// Frame the whole scene (used right after the first history feed).
5    pub fn zoom_to_fit(&mut self) {
6        self.camera.zoom_to_fit(&self.scene.bbox(), 1.15);
7        self.dirty = true;
8    }
9
10    // --- Sizing -----------------------------------------------------------
11
12    /// Update the CSS viewport size (used by all camera math). The physical
13    /// framebuffer size + DPR are the presentation shell's concern.
14    pub fn resize(&mut self, css_width: f64, css_height: f64) {
15        self.camera.width = css_width.max(1.0);
16        self.camera.height = css_height.max(1.0);
17        self.dirty = true;
18    }
19
20    // --- Pointer / wheel ingestion (R22) ----------------------------------
21
22    pub fn pointer_down(&mut self, x: f64, y: f64, button: i32) -> bool {
23        // Sketch camera lock: while locked, the view is held flat-on to the sketch
24        // plane, so a LEFT press must NOT drive the camera at all — neither orbit
25        // (which would tilt off the plane) NOR pan. Suppressing it keeps left-drag
26        // free for sketch interaction and leaves pan on right/middle. Modeling mode
27        // and the UNLOCKED sketch view (where left orbits) are unaffected.
28        if self.sketch_mode()
29            && self.sketch_camera_locked
30            && button == crate::controls::BUTTON_LEFT
31        {
32            return false;
33        }
34        self.controls.pointer_down(x, y, button)
35    }
36
37    pub fn pointer_move(&mut self, x: f64, y: f64) -> bool {
38        let changed = self.controls.pointer_move(&mut self.camera, x, y);
39        if changed {
40            self.dirty = true;
41        }
42        changed
43    }
44
45    pub fn pointer_up(&mut self) -> bool {
46        self.controls.pointer_up()
47    }
48
49    pub fn wheel(&mut self, delta_y: f64, cursor: Option<[f64; 2]>) -> bool {
50        let changed = self.controls.wheel(&mut self.camera, delta_y, cursor);
51        if changed {
52            self.dirty = true;
53        }
54        changed
55    }
56
57    pub fn set_controls_enabled(&mut self, enabled: bool) {
58        self.controls.enabled = enabled;
59    }
60
61    // --- Camera commands (R21) --------------------------------------------
62
63    pub fn toggle_projection(&mut self) -> &'static str {
64        let kind = self.camera.toggle_projection();
65        self.dirty = true;
66        kind
67    }
68
69    pub fn set_projection(&mut self, kind: &str) {
70        let is_persp = matches!(self.camera.projection, crate::view::Projection::Perspective { .. });
71        let want_persp = kind.to_ascii_lowercase().starts_with("pers");
72        if is_persp != want_persp {
73            self.camera.toggle_projection();
74            self.dirty = true;
75        }
76    }
77
78    pub fn standard_view(&mut self, name: &str) -> bool {
79        let ok = self.camera.standard_view(name);
80        if ok {
81            self.camera.zoom_to_fit(&self.scene.bbox(), 1.15);
82            self.dirty = true;
83        }
84        ok
85    }
86
87    pub fn camera_state_json(&self) -> String {
88        self.camera.state_json()
89    }
90
91    pub fn apply_camera_state_json(&mut self, json: &str) -> Result<(), String> {
92        self.camera.apply_state_json(json)?;
93        self.dirty = true;
94        Ok(())
95    }
96
97    pub fn world_per_pixel(&self) -> f64 {
98        self.camera.world_per_pixel()
99    }
100
101    // --- World → screen (R25) ---------------------------------------------
102
103    /// Project world points to CSS-pixel screen coords for host anchoring. Input
104    /// is `[[x,y,z], …]`; output `[[sx, sy, depth, inFront], …]` where inFront
105    /// is 1 when the point is in front of the eye plane.
106    pub fn world_to_screen_json(&self, points_json: &str) -> Result<String, String> {
107        let points: Vec<[f64; 3]> = serde_json::from_str(points_json)
108            .map_err(|error| format!("world_to_screen points parse: {error}"))?;
109        let out: Vec<[f64; 4]> = points
110            .into_iter()
111            .map(|p| {
112                let (sx, sy, depth) = self.camera.project(p);
113                [sx, sy, depth, if depth > 0.0 { 1.0 } else { 0.0 }]
114            })
115            .collect();
116        Ok(serde_json::to_string(&out).unwrap_or_else(|_| "[]".to_string()))
117    }
118
119    /// The camera matrices for the host overlays' per-frame world→screen /
120    /// screen→world hot path: `{ viewProj:[16], viewProjInverse:[16],
121    /// viewport:[w,h] }`. Both matrices are column-major (index =
122    /// `col*4 + row`); `viewProj` maps world → wgpu clip
123    /// (x,y in −1..1, z in 0..1) and `viewport` is the CSS-pixel size. This lets
124    /// dimensions + sketch drop the compat mirror camera and read the engine's
125    /// own view-projection directly (see `world_to_screen_json` for one-shots).
126    pub fn camera_matrices_json(&self) -> String {
127        let view_proj = self.camera.view_proj_flat();
128        let view_proj_inverse = self.camera.view_proj_inverse_flat();
129        serde_json::json!({
130            "viewProj": view_proj,
131            "viewProjInverse": view_proj_inverse,
132            "viewport": [self.camera.width, self.camera.height],
133        })
134        .to_string()
135    }
136
137    // --- Picking (R23/R24) ------------------------------------------------
138
139}
140
141impl EngineState {
142    /// Build this frame's overlay-widget geometry, or None when nothing is
143    /// enabled (skips the overlay passes entirely).
144    pub fn build_widget_overlay(&self) -> Option<WidgetOverlay> {
145        if !self.widgets.any_visible() {
146            return None;
147        }
148        Some(self.widgets.build_overlay(&gizmo_camera(&self.camera)))
149    }
150
151    pub fn set_datums_json(&mut self, json: &str) -> Result<(), String> {
152        self.widgets.set_datums_json(json)?;
153        self.dirty = true;
154        Ok(())
155    }
156
157    /// Feed the general overlay geometry channel (`set_overlay`): arbitrary named
158    /// tri/line/point groups (feature-dialog previews and other display-only
159    /// geometry), drawn in the widget overlay pass.
160    pub fn set_overlay_json(&mut self, json: &str) -> Result<(), String> {
161        self.widgets.set_overlay_json(json)?;
162        self.dirty = true;
163        Ok(())
164    }
165
166    pub fn set_dimensions_json(&mut self, json: &str) -> Result<(), String> {
167        self.widgets.set_dimensions_json(json)?;
168        self.dirty = true;
169        Ok(())
170    }
171
172    pub fn set_transform_json(&mut self, json: &str) -> Result<(), String> {
173        self.widgets.set_transform_json(json)?;
174        self.dirty = true;
175        Ok(())
176    }
177
178    pub fn set_viewcube_enabled(&mut self, enabled: bool) {
179        self.widgets.set_viewcube_enabled(enabled);
180        self.dirty = true;
181    }
182
183    /// The ViewCube corner rect `{x,y,w,h}` (CSS px) so the host can decide
184    /// whether to forward a pointer event.
185    pub fn viewcube_rect_json(&self) -> String {
186        let r = self.widgets.viewcube_rect(&gizmo_camera(&self.camera));
187        serde_json::json!({ "x": r[0], "y": r[1], "w": r[2], "h": r[3] }).to_string()
188    }
189
190    /// Update the ViewCube hover from cube-local pixels; returns whether it
191    /// changed (a hover-out is `(None)` with local coords outside).
192    pub fn viewcube_hover(&mut self, local_x: f64, local_y: f64) -> bool {
193        let cam = gizmo_camera(&self.camera);
194        let handle = self.widgets.viewcube_hit(&cam, local_x as f32, local_y as f32);
195        let changed = self.widgets.set_viewcube_hover(handle);
196        if changed {
197            self.dirty = true;
198        }
199        changed
200    }
201
202    pub fn viewcube_clear_hover(&mut self) -> bool {
203        let changed = self.widgets.set_viewcube_hover(None);
204        if changed {
205            self.dirty = true;
206        }
207        changed
208    }
209
210    /// Click the ViewCube at cube-local pixels: snap the shared camera to the
211    /// region's standard view (keeping the current pivot distance). Returns
212    /// true if a region was hit.
213    pub fn viewcube_click(&mut self, local_x: f64, local_y: f64) -> bool {
214        let cam = gizmo_camera(&self.camera);
215        let Some(handle) = self.widgets.viewcube_hit(&cam, local_x as f32, local_y as f32) else {
216            return false;
217        };
218        // Navigation arrows apply a RELATIVE camera rotation (orbit / roll) to
219        // the current view instead of snapping to an absolute standard view.
220        if brep_gizmos::view_cube::ViewCube::is_arrow(handle) {
221            self.apply_viewcube_arrow(handle);
222            self.dirty = true;
223            return true;
224        }
225        let (dir, fallback_up) = self.widgets.viewcube_target(handle);
226        // Minimal-rotation snap: keep the current roll by projecting the current
227        // up onto the plane perpendicular to the new view direction, so the
228        // camera reorients by the smallest angle instead of snapping to a fixed
229        // world up (which could spin/flip the model).
230        let dirf = [dir[0] as f64, dir[1] as f64, dir[2] as f64];
231        let cu = self.camera.up;
232        let d = cu[0] * dirf[0] + cu[1] * dirf[1] + cu[2] * dirf[2];
233        let proj = [
234            cu[0] - dirf[0] * d,
235            cu[1] - dirf[1] * d,
236            cu[2] - dirf[2] * d,
237        ];
238        let len = (proj[0] * proj[0] + proj[1] * proj[1] + proj[2] * proj[2]).sqrt();
239        let up = if len > 1e-4 {
240            [
241                (proj[0] / len) as f32,
242                (proj[1] / len) as f32,
243                (proj[2] / len) as f32,
244            ]
245        } else {
246            fallback_up
247        };
248        self.apply_look_direction(dir, up);
249        self.dirty = true;
250        true
251    }
252
253    /// Reorient the camera to look along `dir` (world eye→target) with `up`,
254    /// preserving the current pivot distance.
255    pub(super) fn apply_look_direction(&mut self, dir: [f32; 3], up: [f32; 3]) {
256        let dir = [dir[0] as f64, dir[1] as f64, dir[2] as f64];
257        let dist = self.camera.distance();
258        self.camera.eye = [
259            self.camera.target[0] - dir[0] * dist,
260            self.camera.target[1] - dir[1] * dist,
261            self.camera.target[2] - dir[2] * dist,
262        ];
263        self.camera.up = [up[0] as f64, up[1] as f64, up[2] as f64];
264    }
265
266    /// Apply a ViewCube navigation-arrow rotation to the CURRENT camera (a
267    /// relative 90° orbit / roll), keeping the pivot (`target`) fixed. The
268    /// rotation axes are the camera's own screen axes so the motion follows the
269    /// on-screen arrow direction: the eye moves toward the pan arrow it points
270    /// at, and the roll arcs spin the up vector about the view direction.
271    fn apply_viewcube_arrow(&mut self, handle: u32) {
272        use crate::view::{add3, rotate3, sub3};
273        use brep_gizmos::view_cube::ViewCube;
274        // World-space screen axes of the current view: right, up, forward(eye→target).
275        let (right, up_axis, fwd) = self.camera.basis();
276        let target = self.camera.target;
277        let rel = sub3(self.camera.eye, target); // eye relative to pivot
278        let q = std::f64::consts::FRAC_PI_2; // 90° per click
279        match handle {
280            // Orbit about the screen-up axis; eye moves toward the arrow side.
281            ViewCube::ARROW_RIGHT => {
282                self.camera.eye = add3(target, rotate3(rel, up_axis, q));
283            }
284            ViewCube::ARROW_LEFT => {
285                self.camera.eye = add3(target, rotate3(rel, up_axis, -q));
286            }
287            // Orbit about the screen-right axis; carry the up vector along so the
288            // view stays upright (eye moves toward the arrow side).
289            ViewCube::ARROW_UP => {
290                self.camera.eye = add3(target, rotate3(rel, right, -q));
291                self.camera.up = rotate3(self.camera.up, right, -q);
292            }
293            ViewCube::ARROW_DOWN => {
294                self.camera.eye = add3(target, rotate3(rel, right, q));
295                self.camera.up = rotate3(self.camera.up, right, q);
296            }
297            // Roll about the view direction; only the up vector changes.
298            ViewCube::ROLL_CCW => {
299                self.camera.up = rotate3(self.camera.up, fwd, q);
300            }
301            ViewCube::ROLL_CW => {
302                self.camera.up = rotate3(self.camera.up, fwd, -q);
303            }
304            _ => {}
305        }
306    }
307
308    /// Pick the datum plane/axis under a screen pixel; returns its name (empty
309    /// when none). The host merges this with solid picking into SelectionFilter.
310    pub fn datum_pick(&self, x: f64, y: f64) -> String {
311        self.widgets
312            .datum_pick(&gizmo_camera(&self.camera), x as f32, y as f32)
313            .unwrap_or_default()
314    }
315
316    /// Update the transform-gizmo hover from a screen pixel; returns the handle
317    /// under the pointer (0 = none). Marks dirty when the highlight changed.
318    pub fn transform_hover(&mut self, x: f64, y: f64) -> u32 {
319        let cam = gizmo_camera(&self.camera);
320        let handle = self.widgets.transform_hit(&cam, x as f32, y as f32);
321        if self.widgets.set_transform_hover(handle) {
322            self.dirty = true;
323        }
324        handle
325    }
326
327    /// The transform-gizmo handle under a screen pixel (0 = none) — the host
328    /// echoes it back to start a drag.
329    pub fn transform_pick(&self, x: f64, y: f64) -> u32 {
330        self.widgets.transform_hit(&gizmo_camera(&self.camera), x as f32, y as f32)
331    }
332
333    /// Compute a transform drag (frame-space + world delta) as JSON for the
334    /// feature-edit commit. Marks the handle active for the highlight.
335    pub fn transform_drag(
336        &mut self,
337        handle: u32,
338        sx: f64,
339        sy: f64,
340        cx: f64,
341        cy: f64,
342    ) -> String {
343        let cam = gizmo_camera(&self.camera);
344        self.widgets.set_transform_active(handle);
345        self.dirty = true;
346        self.widgets
347            .transform_drag_json(&cam, handle, sx as f32, sy as f32, cx as f32, cy as f32)
348    }
349
350    pub fn transform_drag_end(&mut self) {
351        self.widgets.set_transform_active(0);
352        self.dirty = true;
353    }
354
355    /// Per-dimension label placement: `[{id, anchor:[x,y,z],
356    /// screen:[sx,sy,inFront]}]` — the host pins each text label at `screen`.
357    pub fn dimension_anchors_json(&self) -> String {
358        let anchors = self.widgets.dimension_anchors(&gizmo_camera(&self.camera));
359        let out: Vec<serde_json::Value> = anchors
360            .into_iter()
361            .map(|(id, p)| {
362                let (sx, sy, depth) = self.camera.project([p[0] as f64, p[1] as f64, p[2] as f64]);
363                serde_json::json!({
364                    "id": id,
365                    "anchor": p,
366                    "screen": [sx, sy, if depth > 0.0 { 1.0 } else { 0.0 }],
367                })
368            })
369            .collect();
370        serde_json::to_string(&out).unwrap_or_else(|_| "[]".to_string())
371    }
372
373    // --- Undo / redo (engine-owned) ---------------------------------------
374    //
375    // The model is engine-owned, so its undo history lives in the engine core
376    // too: `History` holds the stacks and snapshots itself BEFORE each model
377    // mutation (edit / add / delete / reorder), while roll-to-step is view state
378    // and is NOT snapshotted. The UI only TRIGGERS these; it never holds a stack.
379
380}