BREP_render 0.1.0

BREP Rust rendering engine: kernel-fed scene store + wgpu renderer (headless artifact, desktop window, and wasm canvas shells).
Documentation
//! The renderer-agnostic scene store (requirements R8–R9): display objects
//! keyed by the SAME kernel names the feature pipeline mints, with typed
//! metadata — no `userData` grab-bag, no renderer objects. Rendering, picking
//! and feature-reference display all resolve against this one map.
//!
//! Slice 1 populates solids (face mesh ranges + names, edge polylines + names,
//! vertices) from the kernel's native display payload. Datums/sketches/widgets
//! arrive in later slices as further [`DisplayObject`] kinds.

use crate::camera::Aabb;
use std::collections::HashMap;

/// The kind of a display face (surface classification rides along when known —
/// typed replacement for the previous app's untyped `faceKind` tag).
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum FaceKind {
    Unknown,
}

/// One face of a solid: a contiguous triangle range of the solid mesh plus the
/// kernel face identity.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct FaceDisplay {
    /// Kernel face name (byte-exact pipeline name); empty when unnamed.
    pub name: String,
    /// Kernel topology face id.
    pub topo_id: u64,
    /// First triangle (not index) of this face in the mesh.
    pub tri_start: u32,
    /// Triangle count.
    pub tri_count: u32,
    pub kind: FaceKind,
}

/// One display edge: a world-space polyline plus the kernel edge identity and
/// the typed flags the previous display layer kept per object.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct EdgeDisplay {
    /// Kernel edge name (`faceA|faceB[n]` convention); empty when unnamed.
    pub name: String,
    /// Kernel topology edge id.
    pub topo_id: u64,
    /// World-space polyline (chord-tolerance sampled, ≥ 2 points).
    pub polyline: Vec<[f32; 3]>,
    /// Auxiliary display edge (not a real BREP boundary).
    pub aux: bool,
    /// Centerline flag (hole/revolve axis display).
    pub centerline: bool,
}

/// One display vertex (kernel topology vertex).
#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize)]
pub struct VertexDisplay {
    pub topo_id: u64,
    pub position: [f64; 3],
}

/// The triangle mesh of one solid, ready for GPU upload (f32; the kernel's f64
/// buffers are narrowed exactly once, here).
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct DisplayMesh {
    /// Interleaved-ready parallel arrays: xyz per vertex.
    pub positions: Vec<[f32; 3]>,
    pub normals: Vec<[f32; 3]>,
    /// Triangle indices (3 per triangle).
    pub indices: Vec<u32>,
    /// Per-TRIANGLE face index into `SolidDisplay::faces`.
    pub face_ids: Vec<u32>,
}

/// A displayed solid: mesh + named faces/edges/vertices + visibility.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct SolidDisplay {
    /// Kernel solid name — the scene key.
    pub name: String,
    /// The resident kernel handle this display was tessellated from (0 = none,
    /// e.g. a synthesized sheet with no kernel solid). Handles are monotonic and
    /// never recycled, so this is a stable identity for the geometry: the R10
    /// display-reuse fast path keeps an existing display across a history rerun
    /// ONLY when the name's resident handle still equals this — a name re-bound to
    /// a DIFFERENT handle (a SUBTRACT result inherits its target's name; a
    /// roll-back replays that name's ORIGINAL producer) must re-tessellate.
    pub source_handle: u32,
    pub visible: bool,
    /// Optional per-solid base color override (R14 — user-set solid color),
    /// else the name-hashed stable color is used.
    pub color_override: Option<[f32; 3]>,
    /// Monotonic content revision (R10). Every freshly built display gets a
    /// unique value, so the renderer's GPU-buffer cache re-uploads on any
    /// rebuild — correct-by-default (equivalent to teardown-rebuild). The
    /// reused-buffer fast path (keep a revision stable across a `reused`
    /// pipeline result) is a follow-up optimization.
    pub revision: u64,
    pub mesh: DisplayMesh,
    pub faces: Vec<FaceDisplay>,
    pub edges: Vec<EdgeDisplay>,
    pub vertices: Vec<VertexDisplay>,
    /// Per-entity + group hide state (individual faces/edges/vertices, or a
    /// whole group). Default = everything visible; see [`crate::visibility`].
    /// Reused solids keep it across history reruns (this whole struct is cloned
    /// forward); a re-tessellated solid resets to all-visible.
    pub visibility: crate::visibility::EntityVisibility,
    /// World bbox over mesh positions (edges lie on the mesh by construction).
    pub bbox: Aabb,
    /// This display is a SYNTHESIZED committed-sketch SHEET (planar face + named
    /// boundary edges + corner vertices), not a kernel solid — it carries no
    /// resident handle (`source_handle == 0`). The marker lets the UI treat a
    /// sketch as a sketch: it is listed under "Sketches" (not among solids) yet is
    /// pickable / selectable / measurable like any scene solid.
    pub is_sketch: bool,
}

/// Source of monotonic [`SolidDisplay::revision`] values.
fn next_revision() -> u64 {
    use std::sync::atomic::{AtomicU64, Ordering};
    static COUNTER: AtomicU64 = AtomicU64::new(1);
    COUNTER.fetch_add(1, Ordering::Relaxed)
}

/// The scene: insertion-ordered solids + an exact name index (R8 — the
/// `getObjectByName` heuristic-scoring lookup is replaced by this map).
#[derive(Debug, Default)]
pub struct RenderScene {
    solids: Vec<SolidDisplay>,
    index: HashMap<String, usize>,
}

impl RenderScene {
    pub fn new() -> Self {
        Self::default()
    }

    /// Insert or replace a solid by name (replacement keeps insertion order —
    /// a boolean result reusing its target's name stays in place).
    pub fn insert_solid(&mut self, solid: SolidDisplay) {
        match self.index.get(&solid.name) {
            Some(&slot) => self.solids[slot] = solid,
            None => {
                self.index.insert(solid.name.clone(), self.solids.len());
                self.solids.push(solid);
            }
        }
    }

    /// Drop every solid (the scene rebuild path clears then repopulates).
    pub fn clear(&mut self) {
        self.solids.clear();
        self.index.clear();
    }

    /// Empty the scene, RETURNING every solid by value (the name index is cleared
    /// and the vec is `mem::take`-n out). The history-apply seam MOVES existing
    /// displays out this way to reinsert the reused ones without cloning their
    /// meshes — a scene-free [`crate::pipeline::SceneRunner`] delta is applied by
    /// draining then reinserting in snapshot order.
    pub fn drain(&mut self) -> Vec<SolidDisplay> {
        self.index.clear();
        std::mem::take(&mut self.solids)
    }

    /// Set (or clear) a solid's metadata color override, bumping its revision
    /// so the renderer re-derives the base style. Returns false if unknown.
    pub fn set_color_override(&mut self, name: &str, color: Option<[f32; 3]>) -> bool {
        let Some(slot) = self.index.get(name).copied() else {
            return false;
        };
        let solid = &mut self.solids[slot];
        if solid.color_override != color {
            solid.color_override = color;
            solid.revision = next_revision();
        }
        true
    }

    /// Set a solid's visibility (R11). Returns false if unknown.
    pub fn set_visible(&mut self, name: &str, visible: bool) -> bool {
        match self.solid_mut(name) {
            Some(solid) => {
                solid.visible = visible;
                true
            }
            None => false,
        }
    }

    /// Scene enumeration for the host scene-tree panel (R11): names, kind,
    /// visibility, child face/edge/vertex counts — as JSON.
    pub fn listing_json(&self) -> String {
        let solids: Vec<serde_json::Value> = self
            .solids
            .iter()
            .map(|solid| {
                serde_json::json!({
                    "name": solid.name,
                    "kind": "SOLID",
                    "visible": solid.visible,
                    "faces": solid.faces.len(),
                    "edges": solid.edges.len(),
                    "vertices": solid.vertices.len(),
                })
            })
            .collect();
        serde_json::Value::Array(solids).to_string()
    }

    /// Remove a solid by exact name.
    pub fn remove_solid(&mut self, name: &str) -> bool {
        let Some(slot) = self.index.remove(name) else {
            return false;
        };
        self.solids.remove(slot);
        for value in self.index.values_mut() {
            if *value > slot {
                *value -= 1;
            }
        }
        true
    }

    pub fn solid(&self, name: &str) -> Option<&SolidDisplay> {
        self.index.get(name).map(|&slot| &self.solids[slot])
    }

    pub fn solid_mut(&mut self, name: &str) -> Option<&mut SolidDisplay> {
        let slot = *self.index.get(name)?;
        Some(&mut self.solids[slot])
    }

    /// Insertion-ordered iteration (deterministic — drives draw order).
    pub fn solids(&self) -> &[SolidDisplay] {
        &self.solids
    }

    /// The world-space polyline of the first display edge named `name` across all
    /// solids (widened to `f64`), or `None` when no edge carries that exact name.
    /// The engine-native sketch pickEdges tool (S6b-2) uses this to fetch a picked
    /// scene edge's geometry for projection into the sketch plane. There is no name
    /// index for edges (only solids), so this is a linear scan — fine for the
    /// interactive per-click use.
    pub fn edge_polyline_world(&self, name: &str) -> Option<Vec<[f64; 3]>> {
        for solid in &self.solids {
            for edge in &solid.edges {
                if edge.name == name {
                    return Some(
                        edge.polyline
                            .iter()
                            .map(|p| [p[0] as f64, p[1] as f64, p[2] as f64])
                            .collect(),
                    );
                }
            }
        }
        None
    }

    /// The name of the solid owning the first display edge named `name`, or `None`
    /// (the companion of [`edge_polyline_world`](Self::edge_polyline_world) — the
    /// pickEdges tool stores it as external-ref metadata).
    pub fn edge_solid_name(&self, name: &str) -> Option<&str> {
        for solid in &self.solids {
            if solid.edges.iter().any(|edge| edge.name == name) {
                return Some(&solid.name);
            }
        }
        None
    }

    pub fn is_empty(&self) -> bool {
        self.solids.is_empty()
    }

    /// World bbox over every VISIBLE solid.
    pub fn bbox(&self) -> Aabb {
        let mut bbox = Aabb::empty();
        for solid in &self.solids {
            if solid.visible {
                bbox.union(&solid.bbox);
            }
        }
        bbox
    }
}

/// Build a [`SolidDisplay`] from the kernel's native display payload.
pub fn solid_display_from_payload(
    name: &str,
    payload: brep_kernel::DisplaySolidPayload,
) -> SolidDisplay {
    let mesh_in = payload.mesh;
    let vertex_count = mesh_in.positions.len() / 3;
    let mut positions = Vec::with_capacity(vertex_count);
    let mut normals = Vec::with_capacity(vertex_count);
    let mut bbox = Aabb::empty();
    for i in 0..vertex_count {
        let p = [
            mesh_in.positions[i * 3],
            mesh_in.positions[i * 3 + 1],
            mesh_in.positions[i * 3 + 2],
        ];
        bbox.expand(p);
        positions.push([p[0] as f32, p[1] as f32, p[2] as f32]);
        normals.push([
            mesh_in.normals[i * 3] as f32,
            mesh_in.normals[i * 3 + 1] as f32,
            mesh_in.normals[i * 3 + 2] as f32,
        ]);
    }

    // Face ranges: the watertight mesh emits each face's triangles as one
    // contiguous run of `face_ids`. Group runs; a face with no triangles gets
    // an empty range.
    let mut faces: Vec<FaceDisplay> = payload
        .faces
        .iter()
        .map(|(topo_id, name)| FaceDisplay {
            name: name.clone().unwrap_or_default(),
            topo_id: *topo_id,
            tri_start: 0,
            tri_count: 0,
            kind: FaceKind::Unknown,
        })
        .collect();
    let mut run_start = 0u32;
    let mut run_face: Option<u32> = None;
    for (tri, &face_id) in mesh_in.face_ids.iter().enumerate() {
        if run_face != Some(face_id) {
            run_face = Some(face_id);
            run_start = tri as u32;
        }
        if let Some(face) = faces.get_mut(face_id as usize) {
            if face.tri_count == 0 {
                face.tri_start = run_start;
            }
            face.tri_count += 1;
        }
    }

    let edges = payload
        .edges
        .into_iter()
        .map(|(topo_id, name, points)| EdgeDisplay {
            name: name.unwrap_or_default(),
            topo_id,
            polyline: points
                .iter()
                .map(|p| [p.x as f32, p.y as f32, p.z as f32])
                .collect(),
            aux: false,
            centerline: false,
        })
        .collect();

    let vertices = payload
        .vertices
        .into_iter()
        .map(|(topo_id, p)| VertexDisplay {
            topo_id,
            position: [p.x, p.y, p.z],
        })
        .collect();

    SolidDisplay {
        name: name.to_string(),
        source_handle: 0, // set by the caller that knows the resident handle
        visible: true,
        color_override: None,
        revision: next_revision(),
        mesh: DisplayMesh {
            positions,
            normals,
            indices: mesh_in.indices,
            face_ids: mesh_in.face_ids,
        },
        faces,
        edges,
        vertices,
        visibility: crate::visibility::EntityVisibility::default(),
        bbox,
        is_sketch: false, // set by the sketch-sheet synthesizer for a committed sketch
    }
}