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::*;

/// The committed-sketch SHEET base color (dim cyan, matching the retired overlay
/// `COMMITTED_COLOR` 0x67c7d4) — a sketch sheet reads distinctly from a real solid.
const SKETCH_SHEET_COLOR: [f32; 3] = [
    0x67 as f32 / 255.0,
    0xc7 as f32 / 255.0,
    0xd4 as f32 / 255.0,
];

impl EngineState {

    /// The committed sketches present at the CURRENT rollback: every `"S"` feature at
    /// index `0..=rollback`, EXCEPT the one being edited (it displays via the live
    /// editing overlay). Hidden ids are still returned (the Scene tree lists them);
    /// visibility is filtered by the caller against [`hidden_sketches`].
    fn committed_sketch_ids(&self) -> Vec<String> {
        let editing = self.sketch_edit.as_ref().map(|edit| edit.feature_id.as_str());
        let rollback = self.history.rollback();
        let mut ids = Vec::new();
        for index in 0..=rollback {
            if self.history.feature_type(index).as_deref() != Some("S") {
                continue;
            }
            let Some(id) = self.history.feature_id(index) else {
                continue;
            };
            if Some(id.as_str()) == editing {
                continue;
            }
            ids.push(id);
        }
        ids
    }

    /// (Re)build the persistent committed-sketch SHEET SOLIDS. For every committed
    /// sketch that should show — [`committed_sketch_ids`](Self::committed_sketch_ids)
    /// minus [`hidden_sketches`] — synthesize its planar sheet from the run's solved
    /// profile and insert it as a scene solid (keyed by the sketch id, dim-cyan,
    /// flagged `is_sketch`); remove any sheet inserted on the PREVIOUS refresh but
    /// not this one (rolled back, deleted, hidden, or became the active edit). A
    /// sketch with no closed profile (open / underconstrained) yields no sheet and
    /// is skipped. Marks dirty.
    pub fn refresh_committed_sketches(&mut self) {
        let visible_ids: Vec<String> = self
            .committed_sketch_ids()
            .into_iter()
            .filter(|id| !self.hidden_sketches.contains(id))
            .collect();

        // Phase 1 (immutable borrow of the surfaced profiles): build a display
        // payload per visible sketch that HAS a closed profile. A sketch with no
        // sheet (no face + no edges) is skipped.
        let payloads: Vec<(String, brep_kernel::DisplaySolidPayload)> = visible_ids
            .iter()
            .filter_map(|id| {
                let profile = self
                    .sketch_profiles
                    .iter()
                    .find(|(name, _)| name == id)
                    .map(|(_, profile)| profile)?;
                let payload = brep_kernel::sketch_profile_display_payload(profile);
                if payload.mesh.indices.is_empty() && payload.edges.is_empty() {
                    return None;
                }
                Some((id.clone(), payload))
            })
            .collect();

        // Phase 2 (mutable borrow of the scene): insert each sheet as a scene solid.
        let mut fed: Vec<String> = Vec::with_capacity(payloads.len());
        for (id, payload) in payloads {
            let mut solid = crate::scene::solid_display_from_payload(&id, payload);
            solid.is_sketch = true;
            solid.color_override = Some(SKETCH_SHEET_COLOR);
            self.scene.insert_solid(solid);
            fed.push(id);
        }

        // Remove any sketch sheet inserted last time but not now.
        let fed_set: std::collections::HashSet<&str> = fed.iter().map(String::as_str).collect();
        let stale: Vec<String> = self
            .shown_sketch_ids
            .iter()
            .filter(|id| !fed_set.contains(id.as_str()))
            .cloned()
            .collect();
        for id in stale {
            self.scene.remove_solid(&id);
        }

        self.shown_sketch_ids = fed;
        self.dirty = true;
    }

    /// Whether the committed sketch `id`'s persistent overlay is shown (absent from
    /// [`hidden_sketches`] = visible).
    pub fn sketch_visible(&self, id: &str) -> bool {
        !self.hidden_sketches.contains(id)
    }

    /// Show/hide the committed sketch `id`'s persistent overlay (the Scene-tree
    /// checkbox). Toggles [`hidden_sketches`], rebuilds the committed overlays (so the
    /// group is fed or cleared immediately), and marks dirty.
    pub fn set_sketch_visible(&mut self, id: &str, visible: bool) {
        if visible {
            self.hidden_sketches.remove(id);
        } else {
            self.hidden_sketches.insert(id.to_string());
        }
        self.refresh_committed_sketches();
    }

    /// The committed sketches to list in the Scene tree: every `"S"` feature at the
    /// current rollback (minus the active edit), each with its live visibility — the
    /// ordered `(id, visible)` list the Scene panel snapshots.
    pub fn committed_sketches(&self) -> Vec<(String, bool)> {
        self.committed_sketch_ids()
            .into_iter()
            .map(|id| {
                let visible = !self.hidden_sketches.contains(&id);
                (id, visible)
            })
            .collect()
    }

    /// The committed sketches as JSON (`[{"name":<id>,"visible":<bool>}]`) — the
    /// sibling of [`scene_entities_json`](Self::scene_entities_json) the Scene panel
    /// publishes for the headed verifier (kept a SEPARATE method so the solids array's
    /// shape is unchanged).
    pub fn sketch_entities_json(&self) -> String {
        let list: Vec<serde_json::Value> = self
            .committed_sketches()
            .into_iter()
            .map(|(id, visible)| serde_json::json!({ "name": id, "visible": visible }))
            .collect();
        serde_json::Value::Array(list).to_string()
    }
}

// ===========================================================================
// Construction datums — DATUM + PLANE feature frames rendered as first-class,
// SELECTABLE scene citizens (the committed-sketch parallel). The frames the last
// history run resolved ride `construction_frames`; this block filters them to the
// D/P producing features (a frame name → its feature TYPE via the history), feeds
// the kept ones to the datum-plane widget channel (`set_datums`, which REPLACES
// its set wholesale each call — so re-feeding the full current set auto-drops
// departed/hidden/rolled-back planes), lists them in the Scene tree, and selects
// one BY NAME (`emphasis.selected_datums`, re-colored with the accent).
// `hidden_datums` drives per-plane visibility. Kept in ONE appended block so
// concurrent edits to the primary impl land clean.
// ===========================================================================


// Committed-sketch persistent-display + Scene-tree-listing tests — their OWN module
// (appended last) so they do not conflict with the modules above.
#[cfg(test)]
mod committed_sketch_tests {
    use super::*;

    /// A two-feature history: a cube (solid) followed by a committed closed-rectangle
    /// sketch on the XY plane (the sketch is the LAST feature).
    fn cube_and_sketch_history() -> String {
        serde_json::json!({
            "features": [
                {
                    "type": "P.CU",
                    "inputParams": {
                        "id": "Box",
                        "sizeX": 10.0, "sizeY": 10.0, "sizeZ": 10.0,
                        "transform": {
                            "position": [0.0, 0.0, 0.0],
                            "rotationEuler": [0.0, 0.0, 0.0],
                            "scale": [1.0, 1.0, 1.0]
                        },
                        "boolean": { "targets": [], "operation": "NONE" }
                    },
                    "persistentData": {}
                },
                {
                    "type": "S",
                    "inputParams": { "id": "Sk" },
                    "persistentData": {
                        "basis": { "origin": [0, 0, 0], "x": [1, 0, 0], "y": [0, 1, 0], "z": [0, 0, 1] },
                        "sketch": {
                            "points": [
                                { "id": 0, "x": 0.0,  "y": 0.0 },
                                { "id": 1, "x": 10.0, "y": 0.0 },
                                { "id": 2, "x": 10.0, "y": 6.0 },
                                { "id": 3, "x": 0.0,  "y": 6.0 }
                            ],
                            "geometries": [
                                { "id": 10, "type": "line", "points": [0, 1] },
                                { "id": 11, "type": "line", "points": [1, 2] },
                                { "id": 12, "type": "line", "points": [2, 3] },
                                { "id": 13, "type": "line", "points": [3, 0] }
                            ],
                            "constraints": []
                        }
                    }
                }
            ]
        })
        .to_string()
    }

    fn has_group(engine: &EngineState, name: &str) -> bool {
        engine.widgets.overlay_group_names().contains(&name)
    }

    /// The synthesized committed-sketch SHEET solid for `id`, if present.
    fn sketch_sheet<'a>(
        engine: &'a EngineState,
        id: &str,
    ) -> Option<&'a crate::scene::SolidDisplay> {
        engine.scene.solid(id).filter(|solid| solid.is_sketch)
    }

    fn has_sketch_sheet(engine: &EngineState, id: &str) -> bool {
        sketch_sheet(engine, id).is_some()
    }

    #[test]
    fn committed_sketch_renders_and_lists_after_rerun() {
        let mut engine = EngineState::new();
        engine.set_history_json(&cube_and_sketch_history()).unwrap();

        // The committed sketch is now a SHEET SOLID keyed by its id: a planar face,
        // its four named boundary edges, and four corner vertices — pickable via the
        // solid infrastructure.
        let sheet = sketch_sheet(&engine, "Sk").expect("committed sketch sheet solid");
        assert!(sheet.is_sketch, "flagged as a sketch");
        assert_eq!(sheet.faces.len(), 1, "one sheet face");
        assert_eq!(sheet.edges.len(), 4, "four boundary edges");
        assert_eq!(sheet.vertices.len(), 4, "four corner vertices");
        assert!(!sheet.mesh.positions.is_empty(), "filled face mesh");

        // The sketch sheet is NOT in the solids listing (it lists under "Sketches").
        let solids: serde_json::Value =
            serde_json::from_str(&engine.scene_entities_json()).unwrap();
        assert_eq!(solids.as_array().unwrap().len(), 1);
        assert_eq!(solids[0]["name"], "Box");

        // The committed sketch is listed by the sibling methods.
        assert_eq!(engine.committed_sketches(), vec![("Sk".to_string(), true)]);
        let sketches: serde_json::Value =
            serde_json::from_str(&engine.sketch_entities_json()).unwrap();
        assert_eq!(sketches[0]["name"], "Sk");
        assert_eq!(sketches[0]["visible"], true);
        assert!(engine.sketch_visible("Sk"));
    }

    #[test]
    fn rolling_back_before_sketch_clears_committed_sheet() {
        let mut engine = EngineState::new();
        engine.set_history_json(&cube_and_sketch_history()).unwrap();
        assert!(has_sketch_sheet(&engine, "Sk"));

        // Roll to the cube (index 0), before the sketch: its sheet is removed and it
        // drops out of the listing.
        engine.roll_to(0);
        assert!(!has_sketch_sheet(&engine, "Sk"), "rolled-back sketch sheet removed");
        assert!(engine.committed_sketches().is_empty());

        // Rolling forward again re-adds it.
        engine.roll_to(1);
        assert!(has_sketch_sheet(&engine, "Sk"), "rolled-forward sketch re-shown");
        assert_eq!(engine.committed_sketches(), vec![("Sk".to_string(), true)]);
    }

    #[test]
    fn set_sketch_visible_hides_and_restores() {
        let mut engine = EngineState::new();
        engine.set_history_json(&cube_and_sketch_history()).unwrap();
        assert!(has_sketch_sheet(&engine, "Sk"));

        // Hide: sheet removed from the scene, still LISTED but visible:false.
        engine.set_sketch_visible("Sk", false);
        assert!(!has_sketch_sheet(&engine, "Sk"), "hidden sketch sheet removed");
        assert!(!engine.sketch_visible("Sk"));
        assert_eq!(engine.committed_sketches(), vec![("Sk".to_string(), false)]);
        let sketches: serde_json::Value =
            serde_json::from_str(&engine.sketch_entities_json()).unwrap();
        assert_eq!(sketches[0]["visible"], false);

        // Show: sheet re-inserted.
        engine.set_sketch_visible("Sk", true);
        assert!(has_sketch_sheet(&engine, "Sk"), "re-shown sketch sheet inserted");
        assert!(engine.sketch_visible("Sk"));
    }

    #[test]
    fn entering_sketch_mode_removes_committed_sheet_then_commit_readds() {
        let mut engine = EngineState::new();
        engine.set_history_json(&cube_and_sketch_history()).unwrap();
        assert!(has_sketch_sheet(&engine, "Sk"));

        // Enter: the committed sheet is removed (no double display); the LIVE editing
        // overlay group is fed instead (unchanged active-edit path).
        engine.enter_sketch_mode("Sk").expect("enter");
        assert!(engine.sketch_mode());
        assert!(
            !has_sketch_sheet(&engine, "Sk"),
            "active sketch's committed sheet removed"
        );
        assert!(has_group(&engine, "sketch-geometry"), "live editing overlay fed");
        // While editing, the sketch is not in the committed listing.
        assert!(engine.committed_sketches().is_empty());

        // Commit exit: the just-committed sketch reappears as a sheet solid and the
        // live editing group is cleared.
        engine.exit_sketch_mode(true);
        assert!(!engine.sketch_mode());
        assert!(
            has_sketch_sheet(&engine, "Sk"),
            "committed sketch reappears as a sheet after commit"
        );
        assert!(!has_group(&engine, "sketch-geometry"), "live editing overlay cleared");
        assert_eq!(engine.committed_sketches(), vec![("Sk".to_string(), true)]);
    }

    /// A committed sketch sheet is MEASURABLE through the handle-less metadata path:
    /// its Info reports `kind:"sketch"` with the profile area (10x6 = 60 mm²) and
    /// total boundary length (perimeter 2*(10+6) = 32 mm), no volume, and its
    /// creating feature is the sketch.
    #[test]
    fn sketch_sheet_info_reports_area_and_edge_length() {
        let mut engine = EngineState::new();
        engine.set_history_json(&cube_and_sketch_history()).unwrap();
        let info: serde_json::Value =
            serde_json::from_str(&engine.object_info_json("Sk")).unwrap();
        assert_eq!(info["ok"], true, "sketch info must not error: {info}");
        assert_eq!(info["kind"], "sketch");
        assert!(info.get("volume").is_none(), "a sheet has no volume");
        assert!((info["area"].as_f64().unwrap() - 60.0).abs() < 1e-3, "area {}", info["area"]);
        assert!(
            (info["edgeLengthTotal"].as_f64().unwrap() - 32.0).abs() < 1e-3,
            "perimeter {}",
            info["edgeLengthTotal"]
        );
        assert_eq!(info["creatingFeature"]["id"], "Sk");
        assert_eq!(info["creatingFeature"]["type"], "S");
    }

    /// A boundary edge of the sheet is a normal selectable/measurable edge: its Info
    /// reports `kind:"edge"` and the segment length (a 10 or 6 mm rectangle side).
    #[test]
    fn sketch_sheet_edge_is_measurable() {
        let mut engine = EngineState::new();
        engine.set_history_json(&cube_and_sketch_history()).unwrap();
        let edge_name = sketch_sheet(&engine, "Sk").unwrap().edges[0].name.clone();
        assert!(!edge_name.is_empty(), "boundary edge is named");
        let info: serde_json::Value =
            serde_json::from_str(&engine.object_info_json(&edge_name)).unwrap();
        assert_eq!(info["ok"], true, "edge info: {info}");
        assert_eq!(info["kind"], "edge");
        assert_eq!(info["solid"], "Sk");
        let length = info["length"].as_f64().unwrap();
        assert!(
            (length - 10.0).abs() < 1e-3 || (length - 6.0).abs() < 1e-3,
            "rectangle side length {length}"
        );
    }

    /// The sheet is selectable BY NAME through the shared solid selection (the
    /// viewport pick and the Scene tree both route here), so a sketch highlights
    /// like any solid.
    #[test]
    fn sketch_sheet_is_selectable_by_name() {
        let mut engine = EngineState::new();
        engine.set_history_json(&cube_and_sketch_history()).unwrap();
        assert!(engine.select_by_name("solid", "Sk"));
        let sel: serde_json::Value = serde_json::from_str(&engine.selection_json()).unwrap();
        assert_eq!(sel["solids"][0], "Sk");
    }
}