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    /// Per-FACE base colour, resolved from this face's `color` metadata
34    /// attribute by [`RenderScene::apply_metadata_colors`]. Takes precedence
35    /// over the owning solid's colour (and over `faceColorMode`), but selection
36    /// / hover emphasis still wins over it. `None` = inherit the solid.
37    #[serde(default)]
38    pub color_override: Option<[f32; 3]>,
39}
40
41/// One display edge: a world-space polyline plus the kernel edge identity and
42/// the typed flags the previous display layer kept per object.
43#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
44pub struct EdgeDisplay {
45    /// Kernel edge name (`faceA|faceB[n]` convention); empty when unnamed.
46    pub name: String,
47    /// Kernel topology edge id.
48    pub topo_id: u64,
49    /// World-space polyline (chord-tolerance sampled, ≥ 2 points).
50    pub polyline: Vec<[f32; 3]>,
51    /// Auxiliary display edge (not a real BREP boundary).
52    pub aux: bool,
53    /// Centerline flag (hole/revolve axis display).
54    pub centerline: bool,
55}
56
57/// One display vertex (kernel topology vertex).
58#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize)]
59pub struct VertexDisplay {
60    pub topo_id: u64,
61    pub position: [f64; 3],
62}
63
64/// The triangle mesh of one solid, ready for GPU upload (f32; the kernel's f64
65/// buffers are narrowed exactly once, here).
66#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
67pub struct DisplayMesh {
68    /// Interleaved-ready parallel arrays: xyz per vertex.
69    pub positions: Vec<[f32; 3]>,
70    pub normals: Vec<[f32; 3]>,
71    /// Triangle indices (3 per triangle).
72    pub indices: Vec<u32>,
73    /// Per-TRIANGLE face index into `SolidDisplay::faces`.
74    pub face_ids: Vec<u32>,
75}
76
77/// A displayed solid: mesh + named faces/edges/vertices + visibility.
78#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
79pub struct SolidDisplay {
80    /// Kernel solid name — the scene key.
81    pub name: String,
82    /// The resident kernel handle this display was tessellated from (0 = none,
83    /// e.g. a synthesized sheet with no kernel solid). Handles are monotonic and
84    /// never recycled, so this is a stable identity for the geometry: the R10
85    /// display-reuse fast path keeps an existing display across a history rerun
86    /// ONLY when the name's resident handle still equals this — a name re-bound to
87    /// a DIFFERENT handle (a SUBTRACT result inherits its target's name; a
88    /// roll-back replays that name's ORIGINAL producer) must re-tessellate.
89    pub source_handle: u32,
90    pub visible: bool,
91    /// Optional per-solid base color override (R14 — user-set solid color),
92    /// else the name-hashed stable color is used.
93    pub color_override: Option<[f32; 3]>,
94    /// Monotonic content revision (R10). Every freshly built display gets a
95    /// unique value, so the renderer's GPU-buffer cache re-uploads on any
96    /// rebuild — correct-by-default (equivalent to teardown-rebuild). The
97    /// reused-buffer fast path (keep a revision stable across a `reused`
98    /// pipeline result) is a follow-up optimization.
99    pub revision: u64,
100    pub mesh: DisplayMesh,
101    pub faces: Vec<FaceDisplay>,
102    pub edges: Vec<EdgeDisplay>,
103    pub vertices: Vec<VertexDisplay>,
104    /// Per-entity + group hide state (individual faces/edges/vertices, or a
105    /// whole group). Default = everything visible; see [`crate::visibility`].
106    /// Reused solids keep it across history reruns (this whole struct is cloned
107    /// forward); a re-tessellated solid resets to all-visible.
108    pub visibility: crate::visibility::EntityVisibility,
109    /// World bbox over mesh positions (edges lie on the mesh by construction).
110    pub bbox: Aabb,
111    /// This display is a SYNTHESIZED committed-sketch SHEET (planar face + named
112    /// boundary edges + corner vertices), not a kernel solid — it carries no
113    /// resident handle (`source_handle == 0`). The marker lets the UI treat a
114    /// sketch as a sketch: it is listed under "Sketches" (not among solids) yet is
115    /// pickable / selectable / measurable like any scene solid.
116    pub is_sketch: bool,
117    /// This body is SHEET METAL — its resident handle carried a `SheetTree` when
118    /// the display was built. Stamped by the pipeline from
119    /// [`brep_kernel::is_sheet_metal_handle`] on the RUNNER thread (where the
120    /// tree's thread-local is warm), so the UI thread can answer "is this a
121    /// sheet-metal body?" straight off the scene — the gate for the sheet-metal
122    /// edit features (SM Flange / Fillet / Chamfer). Immutable-correct: a tree is
123    /// attached at solid creation and dropped exactly when the handle is freed,
124    /// handles never recycle, and the display-reuse fast path clones this struct
125    /// forward only while the handle is unchanged.
126    pub is_sheet_metal: bool,
127}
128
129/// Source of monotonic [`SolidDisplay::revision`] values.
130fn next_revision() -> u64 {
131    use std::sync::atomic::{AtomicU64, Ordering};
132    static COUNTER: AtomicU64 = AtomicU64::new(1);
133    COUNTER.fetch_add(1, Ordering::Relaxed)
134}
135
136/// The scene: insertion-ordered solids + an exact name index (R8 — the
137/// `getObjectByName` heuristic-scoring lookup is replaced by this map).
138#[derive(Debug, Default)]
139pub struct RenderScene {
140    solids: Vec<SolidDisplay>,
141    index: HashMap<String, usize>,
142}
143
144impl RenderScene {
145    pub fn new() -> Self {
146        Self::default()
147    }
148
149    /// Insert or replace a solid by name (replacement keeps insertion order —
150    /// a boolean result reusing its target's name stays in place).
151    pub fn insert_solid(&mut self, solid: SolidDisplay) {
152        match self.index.get(&solid.name) {
153            Some(&slot) => self.solids[slot] = solid,
154            None => {
155                self.index.insert(solid.name.clone(), self.solids.len());
156                self.solids.push(solid);
157            }
158        }
159    }
160
161    /// Drop every solid (the scene rebuild path clears then repopulates).
162    pub fn clear(&mut self) {
163        self.solids.clear();
164        self.index.clear();
165    }
166
167    /// Empty the scene, RETURNING every solid by value (the name index is cleared
168    /// and the vec is `mem::take`-n out). The history-apply seam MOVES existing
169    /// displays out this way to reinsert the reused ones without cloning their
170    /// meshes — a scene-free [`crate::pipeline::SceneRunner`] delta is applied by
171    /// draining then reinserting in snapshot order.
172    pub fn drain(&mut self) -> Vec<SolidDisplay> {
173        self.index.clear();
174        std::mem::take(&mut self.solids)
175    }
176
177    /// Set (or clear) a solid's metadata color override, bumping its revision
178    /// so the renderer re-derives the base style. Returns false if unknown.
179    pub fn set_color_override(&mut self, name: &str, color: Option<[f32; 3]>) -> bool {
180        let Some(slot) = self.index.get(name).copied() else {
181            return false;
182        };
183        let solid = &mut self.solids[slot];
184        if solid.color_override != color {
185            solid.color_override = color;
186            solid.revision = next_revision();
187        }
188        true
189    }
190
191    /// Re-derive every solid's and face's base colour from the name-keyed
192    /// metadata store — the ONE seam through which the durable `color`
193    /// attribute reaches the display.
194    ///
195    /// `lookup` maps an object NAME (a solid's or a face's) to its resolved
196    /// colour. It returns `None` both for "no colour recorded" and for "the
197    /// display setting is overriding model colours", so this method needs to
198    /// know about neither.
199    ///
200    /// SKETCH sheets are skipped: their `color_override` is the synthesized
201    /// [`crate::engine_state::SKETCH_SHEET_COLOR`], not a metadata colour, and
202    /// re-deriving it from a store that has no record for the sheet would blank
203    /// it back to the global face colour.
204    ///
205    /// A changed solid's `revision` is bumped so the renderer re-uploads it —
206    /// and ONLY when something actually changed. That no-op guarantee is
207    /// load-bearing, not a nicety: this runs after EVERY history apply, and the
208    /// R10 GPU-buffer reuse fast path keys off a stable revision.
209    pub fn apply_metadata_colors(&mut self, lookup: impl Fn(&str) -> Option<[f32; 3]>) -> bool {
210        let mut any = false;
211        for solid in &mut self.solids {
212            if solid.is_sketch {
213                continue;
214            }
215            let mut changed = false;
216            let want = lookup(&solid.name);
217            if solid.color_override != want {
218                solid.color_override = want;
219                changed = true;
220            }
221            for face in &mut solid.faces {
222                // An unnamed face can carry no metadata record, so it always
223                // inherits the solid rather than costing a store lookup.
224                let want = if face.name.is_empty() {
225                    None
226                } else {
227                    lookup(&face.name)
228                };
229                if face.color_override != want {
230                    face.color_override = want;
231                    changed = true;
232                }
233            }
234            if changed {
235                solid.revision = next_revision();
236                any = true;
237            }
238        }
239        any
240    }
241
242    /// Set a solid's visibility (R11). Returns false if unknown.
243    pub fn set_visible(&mut self, name: &str, visible: bool) -> bool {
244        match self.solid_mut(name) {
245            Some(solid) => {
246                solid.visible = visible;
247                true
248            }
249            None => false,
250        }
251    }
252
253    /// Scene enumeration for the host scene-tree panel (R11): names, kind,
254    /// visibility, child face/edge/vertex counts — as JSON.
255    pub fn listing_json(&self) -> String {
256        let solids: Vec<serde_json::Value> = self
257            .solids
258            .iter()
259            .map(|solid| {
260                serde_json::json!({
261                    "name": solid.name,
262                    "kind": "SOLID",
263                    "visible": solid.visible,
264                    "faces": solid.faces.len(),
265                    "edges": solid.edges.len(),
266                    "vertices": solid.vertices.len(),
267                })
268            })
269            .collect();
270        serde_json::Value::Array(solids).to_string()
271    }
272
273    /// Remove a solid by exact name.
274    pub fn remove_solid(&mut self, name: &str) -> bool {
275        let Some(slot) = self.index.remove(name) else {
276            return false;
277        };
278        self.solids.remove(slot);
279        for value in self.index.values_mut() {
280            if *value > slot {
281                *value -= 1;
282            }
283        }
284        true
285    }
286
287    pub fn solid(&self, name: &str) -> Option<&SolidDisplay> {
288        self.index.get(name).map(|&slot| &self.solids[slot])
289    }
290
291    pub fn solid_mut(&mut self, name: &str) -> Option<&mut SolidDisplay> {
292        let slot = *self.index.get(name)?;
293        Some(&mut self.solids[slot])
294    }
295
296    /// Insertion-ordered iteration (deterministic — drives draw order).
297    pub fn solids(&self) -> &[SolidDisplay] {
298        &self.solids
299    }
300
301    /// The world-space polyline of the first display edge named `name` across all
302    /// solids (widened to `f64`), or `None` when no edge carries that exact name.
303    /// The engine-native sketch pickEdges tool (S6b-2) uses this to fetch a picked
304    /// scene edge's geometry for projection into the sketch plane. There is no name
305    /// index for edges (only solids), so this is a linear scan — fine for the
306    /// interactive per-click use.
307    pub fn edge_polyline_world(&self, name: &str) -> Option<Vec<[f64; 3]>> {
308        for solid in &self.solids {
309            for edge in &solid.edges {
310                if edge.name == name {
311                    return Some(
312                        edge.polyline
313                            .iter()
314                            .map(|p| [p[0] as f64, p[1] as f64, p[2] as f64])
315                            .collect(),
316                    );
317                }
318            }
319        }
320        None
321    }
322
323    /// The name of the solid owning the first display edge named `name`, or `None`
324    /// (the companion of [`edge_polyline_world`](Self::edge_polyline_world) — the
325    /// pickEdges tool stores it as external-ref metadata).
326    pub fn edge_solid_name(&self, name: &str) -> Option<&str> {
327        for solid in &self.solids {
328            if solid.edges.iter().any(|edge| edge.name == name) {
329                return Some(&solid.name);
330            }
331        }
332        None
333    }
334
335    /// The world plane of the first display face named `name` across all solids,
336    /// as `(centroid, unit outward normal)`: the area-weighted centroid of the
337    /// face's mesh triangles and the normalized sum of their cross products. The
338    /// watertight tessellation winds every face's triangles by `same_sense`, so
339    /// the cross-product sum IS the outward normal (the stored per-vertex normals
340    /// are shading normals and can be blended at shared boundary vertices). This
341    /// is what lets an extrude/revolve whose `profile` is a resident solid FACE
342    /// (not a sketch) anchor its dimension gizmo — the engine's dimension refs
343    /// fall back to it (`EngineState::lookup_profile_plane`). Hidden solids count
344    /// too: a hidden source solid still anchors the gizmo. `None` when no face
345    /// carries that exact name, it has no triangles, or the triangles are
346    /// degenerate (zero area). Linear scan like [`edge_polyline_world`](Self::edge_polyline_world).
347    pub fn face_plane_world(&self, name: &str) -> Option<([f64; 3], [f64; 3])> {
348        for solid in &self.solids {
349            let Some(face) = solid.faces.iter().find(|face| face.name == name) else {
350                continue;
351            };
352            let positions = &solid.mesh.positions;
353            let indices = &solid.mesh.indices;
354            let start = face.tri_start as usize;
355            let end = (start + face.tri_count as usize).min(indices.len() / 3);
356            let mut weighted = [0.0f64; 3];
357            let mut normal = [0.0f64; 3];
358            let mut total_area = 0.0f64;
359            for tri in start..end {
360                let a = f64_point(positions[indices[tri * 3] as usize]);
361                let b = f64_point(positions[indices[tri * 3 + 1] as usize]);
362                let c = f64_point(positions[indices[tri * 3 + 2] as usize]);
363                let ab = [b[0] - a[0], b[1] - a[1], b[2] - a[2]];
364                let ac = [c[0] - a[0], c[1] - a[1], c[2] - a[2]];
365                // Twice the signed-area vector; its length is 2·area.
366                let cross = [
367                    ab[1] * ac[2] - ab[2] * ac[1],
368                    ab[2] * ac[0] - ab[0] * ac[2],
369                    ab[0] * ac[1] - ab[1] * ac[0],
370                ];
371                let area =
372                    0.5 * (cross[0] * cross[0] + cross[1] * cross[1] + cross[2] * cross[2]).sqrt();
373                for k in 0..3 {
374                    weighted[k] += area * (a[k] + b[k] + c[k]) / 3.0;
375                    normal[k] += cross[k];
376                }
377                total_area += area;
378            }
379            let normal_len =
380                (normal[0] * normal[0] + normal[1] * normal[1] + normal[2] * normal[2]).sqrt();
381            if total_area <= 1e-18 || normal_len <= 1e-18 {
382                return None;
383            }
384            return Some((
385                [
386                    weighted[0] / total_area,
387                    weighted[1] / total_area,
388                    weighted[2] / total_area,
389                ],
390                [normal[0] / normal_len, normal[1] / normal_len, normal[2] / normal_len],
391            ));
392        }
393        None
394    }
395
396    pub fn is_empty(&self) -> bool {
397        self.solids.is_empty()
398    }
399
400    /// World bbox over every VISIBLE solid.
401    pub fn bbox(&self) -> Aabb {
402        let mut bbox = Aabb::empty();
403        for solid in &self.solids {
404            if solid.visible {
405                bbox.union(&solid.bbox);
406            }
407        }
408        bbox
409    }
410}
411
412/// Widen a display-mesh vertex to `f64` (the mesh stores `f32`).
413fn f64_point(p: [f32; 3]) -> [f64; 3] {
414    [p[0] as f64, p[1] as f64, p[2] as f64]
415}
416
417/// Build a [`SolidDisplay`] from the kernel's native display payload.
418pub fn solid_display_from_payload(
419    name: &str,
420    payload: brep_kernel::DisplaySolidPayload,
421) -> SolidDisplay {
422    let mesh_in = payload.mesh;
423    let vertex_count = mesh_in.positions.len() / 3;
424    let mut positions = Vec::with_capacity(vertex_count);
425    let mut normals = Vec::with_capacity(vertex_count);
426    let mut bbox = Aabb::empty();
427    for i in 0..vertex_count {
428        let p = [
429            mesh_in.positions[i * 3],
430            mesh_in.positions[i * 3 + 1],
431            mesh_in.positions[i * 3 + 2],
432        ];
433        bbox.expand(p);
434        positions.push([p[0] as f32, p[1] as f32, p[2] as f32]);
435        normals.push([
436            mesh_in.normals[i * 3] as f32,
437            mesh_in.normals[i * 3 + 1] as f32,
438            mesh_in.normals[i * 3 + 2] as f32,
439        ]);
440    }
441
442    // Face ranges: the watertight mesh emits each face's triangles as one
443    // contiguous run of `face_ids`. Group runs; a face with no triangles gets
444    // an empty range.
445    let mut faces: Vec<FaceDisplay> = payload
446        .faces
447        .iter()
448        .map(|(topo_id, name)| FaceDisplay {
449            name: name.clone().unwrap_or_default(),
450            topo_id: *topo_id,
451            tri_start: 0,
452            tri_count: 0,
453            kind: FaceKind::Unknown,
454            color_override: None,
455        })
456        .collect();
457    let mut run_start = 0u32;
458    let mut run_face: Option<u32> = None;
459    for (tri, &face_id) in mesh_in.face_ids.iter().enumerate() {
460        if run_face != Some(face_id) {
461            run_face = Some(face_id);
462            run_start = tri as u32;
463        }
464        if let Some(face) = faces.get_mut(face_id as usize) {
465            if face.tri_count == 0 {
466                face.tri_start = run_start;
467            }
468            face.tri_count += 1;
469        }
470    }
471
472    // Edges and vertices expand the bbox too. For a real solid they lie ON the
473    // meshed boundary, so this changes nothing; for a synthesized OPEN-sketch
474    // display (edges + endpoints, no face) they are the ONLY extent there is, and
475    // an empty bbox would leave the sketch out of zoom-to-fit and out of every
476    // bbox-gated traversal.
477    for (_, _, points) in &payload.edges {
478        for point in points {
479            bbox.expand([point.x, point.y, point.z]);
480        }
481    }
482    for (_, point) in &payload.vertices {
483        bbox.expand([point.x, point.y, point.z]);
484    }
485
486    let edges = payload
487        .edges
488        .into_iter()
489        .map(|(topo_id, name, points)| EdgeDisplay {
490            name: name.unwrap_or_default(),
491            topo_id,
492            polyline: points
493                .iter()
494                .map(|p| [p.x as f32, p.y as f32, p.z as f32])
495                .collect(),
496            aux: false,
497            centerline: false,
498        })
499        .collect();
500
501    let vertices = payload
502        .vertices
503        .into_iter()
504        .map(|(topo_id, p)| VertexDisplay {
505            topo_id,
506            position: [p.x, p.y, p.z],
507        })
508        .collect();
509
510    SolidDisplay {
511        name: name.to_string(),
512        source_handle: 0, // set by the caller that knows the resident handle
513        visible: true,
514        color_override: None,
515        revision: next_revision(),
516        mesh: DisplayMesh {
517            positions,
518            normals,
519            indices: mesh_in.indices,
520            face_ids: mesh_in.face_ids,
521        },
522        faces,
523        edges,
524        vertices,
525        visibility: crate::visibility::EntityVisibility::default(),
526        bbox,
527        is_sketch: false, // set by the sketch-sheet synthesizer for a committed sketch
528        is_sheet_metal: false, // stamped by the pipeline from the resident handle's SheetTree
529    }
530}