Skip to main content

brep_render/
scene.rs

1//! The renderer-agnostic scene store (requirements R8–R9): display objects
2//! keyed by the SAME kernel names the feature pipeline mints, with typed
3//! metadata — no `userData` grab-bag, no renderer objects. Rendering, picking
4//! and feature-reference display all resolve against this one map.
5//!
6//! Slice 1 populates solids (face mesh ranges + names, edge polylines + names,
7//! vertices) from the kernel's native display payload. Datums/sketches/widgets
8//! arrive in later slices as further [`DisplayObject`] kinds.
9
10use crate::camera::Aabb;
11use std::collections::HashMap;
12
13/// The kind of a display face (surface classification rides along when known —
14/// typed replacement for the previous app's untyped `faceKind` tag).
15#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
16pub enum FaceKind {
17    Unknown,
18}
19
20/// One face of a solid: a contiguous triangle range of the solid mesh plus the
21/// kernel face identity.
22#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
23pub struct FaceDisplay {
24    /// Kernel face name (byte-exact pipeline name); empty when unnamed.
25    pub name: String,
26    /// Kernel topology face id.
27    pub topo_id: u64,
28    /// First triangle (not index) of this face in the mesh.
29    pub tri_start: u32,
30    /// Triangle count.
31    pub tri_count: u32,
32    pub kind: FaceKind,
33}
34
35/// One display edge: a world-space polyline plus the kernel edge identity and
36/// the typed flags the previous display layer kept per object.
37#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
38pub struct EdgeDisplay {
39    /// Kernel edge name (`faceA|faceB[n]` convention); empty when unnamed.
40    pub name: String,
41    /// Kernel topology edge id.
42    pub topo_id: u64,
43    /// World-space polyline (chord-tolerance sampled, ≥ 2 points).
44    pub polyline: Vec<[f32; 3]>,
45    /// Auxiliary display edge (not a real BREP boundary).
46    pub aux: bool,
47    /// Centerline flag (hole/revolve axis display).
48    pub centerline: bool,
49}
50
51/// One display vertex (kernel topology vertex).
52#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize)]
53pub struct VertexDisplay {
54    pub topo_id: u64,
55    pub position: [f64; 3],
56}
57
58/// The triangle mesh of one solid, ready for GPU upload (f32; the kernel's f64
59/// buffers are narrowed exactly once, here).
60#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
61pub struct DisplayMesh {
62    /// Interleaved-ready parallel arrays: xyz per vertex.
63    pub positions: Vec<[f32; 3]>,
64    pub normals: Vec<[f32; 3]>,
65    /// Triangle indices (3 per triangle).
66    pub indices: Vec<u32>,
67    /// Per-TRIANGLE face index into `SolidDisplay::faces`.
68    pub face_ids: Vec<u32>,
69}
70
71/// A displayed solid: mesh + named faces/edges/vertices + visibility.
72#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
73pub struct SolidDisplay {
74    /// Kernel solid name — the scene key.
75    pub name: String,
76    /// The resident kernel handle this display was tessellated from (0 = none,
77    /// e.g. a synthesized sheet with no kernel solid). Handles are monotonic and
78    /// never recycled, so this is a stable identity for the geometry: the R10
79    /// display-reuse fast path keeps an existing display across a history rerun
80    /// ONLY when the name's resident handle still equals this — a name re-bound to
81    /// a DIFFERENT handle (a SUBTRACT result inherits its target's name; a
82    /// roll-back replays that name's ORIGINAL producer) must re-tessellate.
83    pub source_handle: u32,
84    pub visible: bool,
85    /// Optional per-solid base color override (R14 — user-set solid color),
86    /// else the name-hashed stable color is used.
87    pub color_override: Option<[f32; 3]>,
88    /// Monotonic content revision (R10). Every freshly built display gets a
89    /// unique value, so the renderer's GPU-buffer cache re-uploads on any
90    /// rebuild — correct-by-default (equivalent to teardown-rebuild). The
91    /// reused-buffer fast path (keep a revision stable across a `reused`
92    /// pipeline result) is a follow-up optimization.
93    pub revision: u64,
94    pub mesh: DisplayMesh,
95    pub faces: Vec<FaceDisplay>,
96    pub edges: Vec<EdgeDisplay>,
97    pub vertices: Vec<VertexDisplay>,
98    /// Per-entity + group hide state (individual faces/edges/vertices, or a
99    /// whole group). Default = everything visible; see [`crate::visibility`].
100    /// Reused solids keep it across history reruns (this whole struct is cloned
101    /// forward); a re-tessellated solid resets to all-visible.
102    pub visibility: crate::visibility::EntityVisibility,
103    /// World bbox over mesh positions (edges lie on the mesh by construction).
104    pub bbox: Aabb,
105    /// This display is a SYNTHESIZED committed-sketch SHEET (planar face + named
106    /// boundary edges + corner vertices), not a kernel solid — it carries no
107    /// resident handle (`source_handle == 0`). The marker lets the UI treat a
108    /// sketch as a sketch: it is listed under "Sketches" (not among solids) yet is
109    /// pickable / selectable / measurable like any scene solid.
110    pub is_sketch: bool,
111    /// This body is SHEET METAL — its resident handle carried a `SheetTree` when
112    /// the display was built. Stamped by the pipeline from
113    /// [`brep_kernel::is_sheet_metal_handle`] on the RUNNER thread (where the
114    /// tree's thread-local is warm), so the UI thread can answer "is this a
115    /// sheet-metal body?" straight off the scene — the gate for the sheet-metal
116    /// edit features (SM Flange / Fillet / Chamfer). Immutable-correct: a tree is
117    /// attached at solid creation and dropped exactly when the handle is freed,
118    /// handles never recycle, and the display-reuse fast path clones this struct
119    /// forward only while the handle is unchanged.
120    pub is_sheet_metal: bool,
121}
122
123/// Source of monotonic [`SolidDisplay::revision`] values.
124fn next_revision() -> u64 {
125    use std::sync::atomic::{AtomicU64, Ordering};
126    static COUNTER: AtomicU64 = AtomicU64::new(1);
127    COUNTER.fetch_add(1, Ordering::Relaxed)
128}
129
130/// The scene: insertion-ordered solids + an exact name index (R8 — the
131/// `getObjectByName` heuristic-scoring lookup is replaced by this map).
132#[derive(Debug, Default)]
133pub struct RenderScene {
134    solids: Vec<SolidDisplay>,
135    index: HashMap<String, usize>,
136}
137
138impl RenderScene {
139    pub fn new() -> Self {
140        Self::default()
141    }
142
143    /// Insert or replace a solid by name (replacement keeps insertion order —
144    /// a boolean result reusing its target's name stays in place).
145    pub fn insert_solid(&mut self, solid: SolidDisplay) {
146        match self.index.get(&solid.name) {
147            Some(&slot) => self.solids[slot] = solid,
148            None => {
149                self.index.insert(solid.name.clone(), self.solids.len());
150                self.solids.push(solid);
151            }
152        }
153    }
154
155    /// Drop every solid (the scene rebuild path clears then repopulates).
156    pub fn clear(&mut self) {
157        self.solids.clear();
158        self.index.clear();
159    }
160
161    /// Empty the scene, RETURNING every solid by value (the name index is cleared
162    /// and the vec is `mem::take`-n out). The history-apply seam MOVES existing
163    /// displays out this way to reinsert the reused ones without cloning their
164    /// meshes — a scene-free [`crate::pipeline::SceneRunner`] delta is applied by
165    /// draining then reinserting in snapshot order.
166    pub fn drain(&mut self) -> Vec<SolidDisplay> {
167        self.index.clear();
168        std::mem::take(&mut self.solids)
169    }
170
171    /// Set (or clear) a solid's metadata color override, bumping its revision
172    /// so the renderer re-derives the base style. Returns false if unknown.
173    pub fn set_color_override(&mut self, name: &str, color: Option<[f32; 3]>) -> bool {
174        let Some(slot) = self.index.get(name).copied() else {
175            return false;
176        };
177        let solid = &mut self.solids[slot];
178        if solid.color_override != color {
179            solid.color_override = color;
180            solid.revision = next_revision();
181        }
182        true
183    }
184
185    /// Set a solid's visibility (R11). Returns false if unknown.
186    pub fn set_visible(&mut self, name: &str, visible: bool) -> bool {
187        match self.solid_mut(name) {
188            Some(solid) => {
189                solid.visible = visible;
190                true
191            }
192            None => false,
193        }
194    }
195
196    /// Scene enumeration for the host scene-tree panel (R11): names, kind,
197    /// visibility, child face/edge/vertex counts — as JSON.
198    pub fn listing_json(&self) -> String {
199        let solids: Vec<serde_json::Value> = self
200            .solids
201            .iter()
202            .map(|solid| {
203                serde_json::json!({
204                    "name": solid.name,
205                    "kind": "SOLID",
206                    "visible": solid.visible,
207                    "faces": solid.faces.len(),
208                    "edges": solid.edges.len(),
209                    "vertices": solid.vertices.len(),
210                })
211            })
212            .collect();
213        serde_json::Value::Array(solids).to_string()
214    }
215
216    /// Remove a solid by exact name.
217    pub fn remove_solid(&mut self, name: &str) -> bool {
218        let Some(slot) = self.index.remove(name) else {
219            return false;
220        };
221        self.solids.remove(slot);
222        for value in self.index.values_mut() {
223            if *value > slot {
224                *value -= 1;
225            }
226        }
227        true
228    }
229
230    pub fn solid(&self, name: &str) -> Option<&SolidDisplay> {
231        self.index.get(name).map(|&slot| &self.solids[slot])
232    }
233
234    pub fn solid_mut(&mut self, name: &str) -> Option<&mut SolidDisplay> {
235        let slot = *self.index.get(name)?;
236        Some(&mut self.solids[slot])
237    }
238
239    /// Insertion-ordered iteration (deterministic — drives draw order).
240    pub fn solids(&self) -> &[SolidDisplay] {
241        &self.solids
242    }
243
244    /// The world-space polyline of the first display edge named `name` across all
245    /// solids (widened to `f64`), or `None` when no edge carries that exact name.
246    /// The engine-native sketch pickEdges tool (S6b-2) uses this to fetch a picked
247    /// scene edge's geometry for projection into the sketch plane. There is no name
248    /// index for edges (only solids), so this is a linear scan — fine for the
249    /// interactive per-click use.
250    pub fn edge_polyline_world(&self, name: &str) -> Option<Vec<[f64; 3]>> {
251        for solid in &self.solids {
252            for edge in &solid.edges {
253                if edge.name == name {
254                    return Some(
255                        edge.polyline
256                            .iter()
257                            .map(|p| [p[0] as f64, p[1] as f64, p[2] as f64])
258                            .collect(),
259                    );
260                }
261            }
262        }
263        None
264    }
265
266    /// The name of the solid owning the first display edge named `name`, or `None`
267    /// (the companion of [`edge_polyline_world`](Self::edge_polyline_world) — the
268    /// pickEdges tool stores it as external-ref metadata).
269    pub fn edge_solid_name(&self, name: &str) -> Option<&str> {
270        for solid in &self.solids {
271            if solid.edges.iter().any(|edge| edge.name == name) {
272                return Some(&solid.name);
273            }
274        }
275        None
276    }
277
278    pub fn is_empty(&self) -> bool {
279        self.solids.is_empty()
280    }
281
282    /// World bbox over every VISIBLE solid.
283    pub fn bbox(&self) -> Aabb {
284        let mut bbox = Aabb::empty();
285        for solid in &self.solids {
286            if solid.visible {
287                bbox.union(&solid.bbox);
288            }
289        }
290        bbox
291    }
292}
293
294/// Build a [`SolidDisplay`] from the kernel's native display payload.
295pub fn solid_display_from_payload(
296    name: &str,
297    payload: brep_kernel::DisplaySolidPayload,
298) -> SolidDisplay {
299    let mesh_in = payload.mesh;
300    let vertex_count = mesh_in.positions.len() / 3;
301    let mut positions = Vec::with_capacity(vertex_count);
302    let mut normals = Vec::with_capacity(vertex_count);
303    let mut bbox = Aabb::empty();
304    for i in 0..vertex_count {
305        let p = [
306            mesh_in.positions[i * 3],
307            mesh_in.positions[i * 3 + 1],
308            mesh_in.positions[i * 3 + 2],
309        ];
310        bbox.expand(p);
311        positions.push([p[0] as f32, p[1] as f32, p[2] as f32]);
312        normals.push([
313            mesh_in.normals[i * 3] as f32,
314            mesh_in.normals[i * 3 + 1] as f32,
315            mesh_in.normals[i * 3 + 2] as f32,
316        ]);
317    }
318
319    // Face ranges: the watertight mesh emits each face's triangles as one
320    // contiguous run of `face_ids`. Group runs; a face with no triangles gets
321    // an empty range.
322    let mut faces: Vec<FaceDisplay> = payload
323        .faces
324        .iter()
325        .map(|(topo_id, name)| FaceDisplay {
326            name: name.clone().unwrap_or_default(),
327            topo_id: *topo_id,
328            tri_start: 0,
329            tri_count: 0,
330            kind: FaceKind::Unknown,
331        })
332        .collect();
333    let mut run_start = 0u32;
334    let mut run_face: Option<u32> = None;
335    for (tri, &face_id) in mesh_in.face_ids.iter().enumerate() {
336        if run_face != Some(face_id) {
337            run_face = Some(face_id);
338            run_start = tri as u32;
339        }
340        if let Some(face) = faces.get_mut(face_id as usize) {
341            if face.tri_count == 0 {
342                face.tri_start = run_start;
343            }
344            face.tri_count += 1;
345        }
346    }
347
348    // Edges and vertices expand the bbox too. For a real solid they lie ON the
349    // meshed boundary, so this changes nothing; for a synthesized OPEN-sketch
350    // display (edges + endpoints, no face) they are the ONLY extent there is, and
351    // an empty bbox would leave the sketch out of zoom-to-fit and out of every
352    // bbox-gated traversal.
353    for (_, _, points) in &payload.edges {
354        for point in points {
355            bbox.expand([point.x, point.y, point.z]);
356        }
357    }
358    for (_, point) in &payload.vertices {
359        bbox.expand([point.x, point.y, point.z]);
360    }
361
362    let edges = payload
363        .edges
364        .into_iter()
365        .map(|(topo_id, name, points)| EdgeDisplay {
366            name: name.unwrap_or_default(),
367            topo_id,
368            polyline: points
369                .iter()
370                .map(|p| [p.x as f32, p.y as f32, p.z as f32])
371                .collect(),
372            aux: false,
373            centerline: false,
374        })
375        .collect();
376
377    let vertices = payload
378        .vertices
379        .into_iter()
380        .map(|(topo_id, p)| VertexDisplay {
381            topo_id,
382            position: [p.x, p.y, p.z],
383        })
384        .collect();
385
386    SolidDisplay {
387        name: name.to_string(),
388        source_handle: 0, // set by the caller that knows the resident handle
389        visible: true,
390        color_override: None,
391        revision: next_revision(),
392        mesh: DisplayMesh {
393            positions,
394            normals,
395            indices: mesh_in.indices,
396            face_ids: mesh_in.face_ids,
397        },
398        faces,
399        edges,
400        vertices,
401        visibility: crate::visibility::EntityVisibility::default(),
402        bbox,
403        is_sketch: false, // set by the sketch-sheet synthesizer for a committed sketch
404        is_sheet_metal: false, // stamped by the pipeline from the resident handle's SheetTree
405    }
406}