BREP_render 0.1.0

BREP Rust rendering engine: kernel-fed scene store + wgpu renderer (headless artifact, desktop window, and wasm canvas shells).
Documentation
use super::*;

impl EngineState {
    /// Frame the whole scene (used right after the first history feed).
    pub fn zoom_to_fit(&mut self) {
        self.camera.zoom_to_fit(&self.scene.bbox(), 1.15);
        self.dirty = true;
    }

    // --- Sizing -----------------------------------------------------------

    /// Update the CSS viewport size (used by all camera math). The physical
    /// framebuffer size + DPR are the presentation shell's concern.
    pub fn resize(&mut self, css_width: f64, css_height: f64) {
        self.camera.width = css_width.max(1.0);
        self.camera.height = css_height.max(1.0);
        self.dirty = true;
    }

    // --- Pointer / wheel ingestion (R22) ----------------------------------

    pub fn pointer_down(&mut self, x: f64, y: f64, button: i32) -> bool {
        // Sketch camera lock: while locked, the view is held flat-on to the sketch
        // plane, so a LEFT press must NOT drive the camera at all — neither orbit
        // (which would tilt off the plane) NOR pan. Suppressing it keeps left-drag
        // free for sketch interaction and leaves pan on right/middle. Modeling mode
        // and the UNLOCKED sketch view (where left orbits) are unaffected.
        if self.sketch_mode()
            && self.sketch_camera_locked
            && button == crate::controls::BUTTON_LEFT
        {
            return false;
        }
        self.controls.pointer_down(x, y, button)
    }

    pub fn pointer_move(&mut self, x: f64, y: f64) -> bool {
        let changed = self.controls.pointer_move(&mut self.camera, x, y);
        if changed {
            self.dirty = true;
        }
        changed
    }

    pub fn pointer_up(&mut self) -> bool {
        self.controls.pointer_up()
    }

    pub fn wheel(&mut self, delta_y: f64, cursor: Option<[f64; 2]>) -> bool {
        let changed = self.controls.wheel(&mut self.camera, delta_y, cursor);
        if changed {
            self.dirty = true;
        }
        changed
    }

    pub fn set_controls_enabled(&mut self, enabled: bool) {
        self.controls.enabled = enabled;
    }

    // --- Camera commands (R21) --------------------------------------------

    pub fn toggle_projection(&mut self) -> &'static str {
        let kind = self.camera.toggle_projection();
        self.dirty = true;
        kind
    }

    pub fn set_projection(&mut self, kind: &str) {
        let is_persp = matches!(self.camera.projection, crate::view::Projection::Perspective { .. });
        let want_persp = kind.to_ascii_lowercase().starts_with("pers");
        if is_persp != want_persp {
            self.camera.toggle_projection();
            self.dirty = true;
        }
    }

    pub fn standard_view(&mut self, name: &str) -> bool {
        let ok = self.camera.standard_view(name);
        if ok {
            self.camera.zoom_to_fit(&self.scene.bbox(), 1.15);
            self.dirty = true;
        }
        ok
    }

    pub fn camera_state_json(&self) -> String {
        self.camera.state_json()
    }

    pub fn apply_camera_state_json(&mut self, json: &str) -> Result<(), String> {
        self.camera.apply_state_json(json)?;
        self.dirty = true;
        Ok(())
    }

    pub fn world_per_pixel(&self) -> f64 {
        self.camera.world_per_pixel()
    }

    // --- World → screen (R25) ---------------------------------------------

    /// Project world points to CSS-pixel screen coords for host anchoring. Input
    /// is `[[x,y,z], …]`; output `[[sx, sy, depth, inFront], …]` where inFront
    /// is 1 when the point is in front of the eye plane.
    pub fn world_to_screen_json(&self, points_json: &str) -> Result<String, String> {
        let points: Vec<[f64; 3]> = serde_json::from_str(points_json)
            .map_err(|error| format!("world_to_screen points parse: {error}"))?;
        let out: Vec<[f64; 4]> = points
            .into_iter()
            .map(|p| {
                let (sx, sy, depth) = self.camera.project(p);
                [sx, sy, depth, if depth > 0.0 { 1.0 } else { 0.0 }]
            })
            .collect();
        Ok(serde_json::to_string(&out).unwrap_or_else(|_| "[]".to_string()))
    }

    /// The camera matrices for the host overlays' per-frame world→screen /
    /// screen→world hot path: `{ viewProj:[16], viewProjInverse:[16],
    /// viewport:[w,h] }`. Both matrices are column-major (index =
    /// `col*4 + row`); `viewProj` maps world → wgpu clip
    /// (x,y in −1..1, z in 0..1) and `viewport` is the CSS-pixel size. This lets
    /// dimensions + sketch drop the compat mirror camera and read the engine's
    /// own view-projection directly (see `world_to_screen_json` for one-shots).
    pub fn camera_matrices_json(&self) -> String {
        let view_proj = self.camera.view_proj_flat();
        let view_proj_inverse = self.camera.view_proj_inverse_flat();
        serde_json::json!({
            "viewProj": view_proj,
            "viewProjInverse": view_proj_inverse,
            "viewport": [self.camera.width, self.camera.height],
        })
        .to_string()
    }

    // --- Picking (R23/R24) ------------------------------------------------

}

impl EngineState {
    /// Build this frame's overlay-widget geometry, or None when nothing is
    /// enabled (skips the overlay passes entirely).
    pub fn build_widget_overlay(&self) -> Option<WidgetOverlay> {
        if !self.widgets.any_visible() {
            return None;
        }
        Some(self.widgets.build_overlay(&gizmo_camera(&self.camera)))
    }

    pub fn set_datums_json(&mut self, json: &str) -> Result<(), String> {
        self.widgets.set_datums_json(json)?;
        self.dirty = true;
        Ok(())
    }

    /// Feed the general overlay geometry channel (`set_overlay`): arbitrary named
    /// tri/line/point groups (feature-dialog previews and other display-only
    /// geometry), drawn in the widget overlay pass.
    pub fn set_overlay_json(&mut self, json: &str) -> Result<(), String> {
        self.widgets.set_overlay_json(json)?;
        self.dirty = true;
        Ok(())
    }

    pub fn set_dimensions_json(&mut self, json: &str) -> Result<(), String> {
        self.widgets.set_dimensions_json(json)?;
        self.dirty = true;
        Ok(())
    }

    pub fn set_transform_json(&mut self, json: &str) -> Result<(), String> {
        self.widgets.set_transform_json(json)?;
        self.dirty = true;
        Ok(())
    }

    pub fn set_viewcube_enabled(&mut self, enabled: bool) {
        self.widgets.set_viewcube_enabled(enabled);
        self.dirty = true;
    }

    /// The ViewCube corner rect `{x,y,w,h}` (CSS px) so the host can decide
    /// whether to forward a pointer event.
    pub fn viewcube_rect_json(&self) -> String {
        let r = self.widgets.viewcube_rect(&gizmo_camera(&self.camera));
        serde_json::json!({ "x": r[0], "y": r[1], "w": r[2], "h": r[3] }).to_string()
    }

    /// Update the ViewCube hover from cube-local pixels; returns whether it
    /// changed (a hover-out is `(None)` with local coords outside).
    pub fn viewcube_hover(&mut self, local_x: f64, local_y: f64) -> bool {
        let cam = gizmo_camera(&self.camera);
        let handle = self.widgets.viewcube_hit(&cam, local_x as f32, local_y as f32);
        let changed = self.widgets.set_viewcube_hover(handle);
        if changed {
            self.dirty = true;
        }
        changed
    }

    pub fn viewcube_clear_hover(&mut self) -> bool {
        let changed = self.widgets.set_viewcube_hover(None);
        if changed {
            self.dirty = true;
        }
        changed
    }

    /// Click the ViewCube at cube-local pixels: snap the shared camera to the
    /// region's standard view (keeping the current pivot distance). Returns
    /// true if a region was hit.
    pub fn viewcube_click(&mut self, local_x: f64, local_y: f64) -> bool {
        let cam = gizmo_camera(&self.camera);
        let Some(handle) = self.widgets.viewcube_hit(&cam, local_x as f32, local_y as f32) else {
            return false;
        };
        // Navigation arrows apply a RELATIVE camera rotation (orbit / roll) to
        // the current view instead of snapping to an absolute standard view.
        if brep_gizmos::view_cube::ViewCube::is_arrow(handle) {
            self.apply_viewcube_arrow(handle);
            self.dirty = true;
            return true;
        }
        let (dir, fallback_up) = self.widgets.viewcube_target(handle);
        // Minimal-rotation snap: keep the current roll by projecting the current
        // up onto the plane perpendicular to the new view direction, so the
        // camera reorients by the smallest angle instead of snapping to a fixed
        // world up (which could spin/flip the model).
        let dirf = [dir[0] as f64, dir[1] as f64, dir[2] as f64];
        let cu = self.camera.up;
        let d = cu[0] * dirf[0] + cu[1] * dirf[1] + cu[2] * dirf[2];
        let proj = [
            cu[0] - dirf[0] * d,
            cu[1] - dirf[1] * d,
            cu[2] - dirf[2] * d,
        ];
        let len = (proj[0] * proj[0] + proj[1] * proj[1] + proj[2] * proj[2]).sqrt();
        let up = if len > 1e-4 {
            [
                (proj[0] / len) as f32,
                (proj[1] / len) as f32,
                (proj[2] / len) as f32,
            ]
        } else {
            fallback_up
        };
        self.apply_look_direction(dir, up);
        self.dirty = true;
        true
    }

    /// Reorient the camera to look along `dir` (world eye→target) with `up`,
    /// preserving the current pivot distance.
    pub(super) fn apply_look_direction(&mut self, dir: [f32; 3], up: [f32; 3]) {
        let dir = [dir[0] as f64, dir[1] as f64, dir[2] as f64];
        let dist = self.camera.distance();
        self.camera.eye = [
            self.camera.target[0] - dir[0] * dist,
            self.camera.target[1] - dir[1] * dist,
            self.camera.target[2] - dir[2] * dist,
        ];
        self.camera.up = [up[0] as f64, up[1] as f64, up[2] as f64];
    }

    /// Apply a ViewCube navigation-arrow rotation to the CURRENT camera (a
    /// relative 90° orbit / roll), keeping the pivot (`target`) fixed. The
    /// rotation axes are the camera's own screen axes so the motion follows the
    /// on-screen arrow direction: the eye moves toward the pan arrow it points
    /// at, and the roll arcs spin the up vector about the view direction.
    fn apply_viewcube_arrow(&mut self, handle: u32) {
        use crate::view::{add3, rotate3, sub3};
        use brep_gizmos::view_cube::ViewCube;
        // World-space screen axes of the current view: right, up, forward(eye→target).
        let (right, up_axis, fwd) = self.camera.basis();
        let target = self.camera.target;
        let rel = sub3(self.camera.eye, target); // eye relative to pivot
        let q = std::f64::consts::FRAC_PI_2; // 90° per click
        match handle {
            // Orbit about the screen-up axis; eye moves toward the arrow side.
            ViewCube::ARROW_RIGHT => {
                self.camera.eye = add3(target, rotate3(rel, up_axis, q));
            }
            ViewCube::ARROW_LEFT => {
                self.camera.eye = add3(target, rotate3(rel, up_axis, -q));
            }
            // Orbit about the screen-right axis; carry the up vector along so the
            // view stays upright (eye moves toward the arrow side).
            ViewCube::ARROW_UP => {
                self.camera.eye = add3(target, rotate3(rel, right, -q));
                self.camera.up = rotate3(self.camera.up, right, -q);
            }
            ViewCube::ARROW_DOWN => {
                self.camera.eye = add3(target, rotate3(rel, right, q));
                self.camera.up = rotate3(self.camera.up, right, q);
            }
            // Roll about the view direction; only the up vector changes.
            ViewCube::ROLL_CCW => {
                self.camera.up = rotate3(self.camera.up, fwd, q);
            }
            ViewCube::ROLL_CW => {
                self.camera.up = rotate3(self.camera.up, fwd, -q);
            }
            _ => {}
        }
    }

    /// Pick the datum plane/axis under a screen pixel; returns its name (empty
    /// when none). The host merges this with solid picking into SelectionFilter.
    pub fn datum_pick(&self, x: f64, y: f64) -> String {
        self.widgets
            .datum_pick(&gizmo_camera(&self.camera), x as f32, y as f32)
            .unwrap_or_default()
    }

    /// Update the transform-gizmo hover from a screen pixel; returns the handle
    /// under the pointer (0 = none). Marks dirty when the highlight changed.
    pub fn transform_hover(&mut self, x: f64, y: f64) -> u32 {
        let cam = gizmo_camera(&self.camera);
        let handle = self.widgets.transform_hit(&cam, x as f32, y as f32);
        if self.widgets.set_transform_hover(handle) {
            self.dirty = true;
        }
        handle
    }

    /// The transform-gizmo handle under a screen pixel (0 = none) — the host
    /// echoes it back to start a drag.
    pub fn transform_pick(&self, x: f64, y: f64) -> u32 {
        self.widgets.transform_hit(&gizmo_camera(&self.camera), x as f32, y as f32)
    }

    /// Compute a transform drag (frame-space + world delta) as JSON for the
    /// feature-edit commit. Marks the handle active for the highlight.
    pub fn transform_drag(
        &mut self,
        handle: u32,
        sx: f64,
        sy: f64,
        cx: f64,
        cy: f64,
    ) -> String {
        let cam = gizmo_camera(&self.camera);
        self.widgets.set_transform_active(handle);
        self.dirty = true;
        self.widgets
            .transform_drag_json(&cam, handle, sx as f32, sy as f32, cx as f32, cy as f32)
    }

    pub fn transform_drag_end(&mut self) {
        self.widgets.set_transform_active(0);
        self.dirty = true;
    }

    /// Per-dimension label placement: `[{id, anchor:[x,y,z],
    /// screen:[sx,sy,inFront]}]` — the host pins each text label at `screen`.
    pub fn dimension_anchors_json(&self) -> String {
        let anchors = self.widgets.dimension_anchors(&gizmo_camera(&self.camera));
        let out: Vec<serde_json::Value> = anchors
            .into_iter()
            .map(|(id, p)| {
                let (sx, sy, depth) = self.camera.project([p[0] as f64, p[1] as f64, p[2] as f64]);
                serde_json::json!({
                    "id": id,
                    "anchor": p,
                    "screen": [sx, sy, if depth > 0.0 { 1.0 } else { 0.0 }],
                })
            })
            .collect();
        serde_json::to_string(&out).unwrap_or_else(|_| "[]".to_string())
    }

    // --- Undo / redo (engine-owned) ---------------------------------------
    //
    // The model is engine-owned, so its undo history lives in the engine core
    // too: `History` holds the stacks and snapshots itself BEFORE each model
    // mutation (edit / add / delete / reorder), while roll-to-step is view state
    // and is NOT snapshotted. The UI only TRIGGERS these; it never holds a stack.

}