Skip to main content

brep_render/
scene.rs

1//! Renderer-independent display objects keyed by kernel feature names.
2//! Rendering, picking, and feature-reference display share this scene map.
3
4use crate::camera::Aabb;
5use std::cell::RefCell;
6use std::collections::HashMap;
7
8/// The kind of a display face (surface classification rides along when known —
9/// typed replacement for the previous app's untyped `faceKind` tag).
10#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
11pub enum FaceKind {
12    Unknown,
13}
14
15/// One face of a solid: a contiguous triangle range of the solid mesh plus the
16/// kernel face identity.
17#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
18pub struct FaceDisplay {
19    /// Kernel face name (byte-exact pipeline name); empty when unnamed.
20    pub name: String,
21    /// Kernel topology face id.
22    pub topo_id: u64,
23    /// First triangle (not index) of this face in the mesh.
24    pub tri_start: u32,
25    /// Triangle count.
26    pub tri_count: u32,
27    pub kind: FaceKind,
28    /// Per-FACE base colour, resolved from this face's `color` metadata
29    /// attribute by [`RenderScene::apply_metadata_colors`]. Takes precedence
30    /// over the owning solid's colour (and over `faceColorMode`), but selection
31    /// / hover emphasis still wins over it. `None` = inherit the solid.
32    #[serde(default)]
33    pub color_override: Option<[f32; 3]>,
34}
35
36/// One display edge: a world-space polyline plus the kernel edge identity and
37/// the typed flags the previous display layer kept per object.
38#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
39pub struct EdgeDisplay {
40    /// Kernel edge name (`faceA|faceB[n]` convention); empty when unnamed.
41    pub name: String,
42    /// Kernel topology edge id.
43    pub topo_id: u64,
44    /// World-space polyline (chord-tolerance sampled, ≥ 2 points).
45    pub polyline: Vec<[f32; 3]>,
46    /// Auxiliary display edge (not a real BREP boundary): drawn, but never
47    /// picked, listed or named — a board's copper outline is one.
48    pub aux: bool,
49    /// Centerline flag (hole/revolve axis display).
50    pub centerline: bool,
51}
52
53/// One display vertex (kernel topology vertex).
54#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize)]
55pub struct VertexDisplay {
56    pub topo_id: u64,
57    pub position: [f64; 3],
58}
59
60/// The triangle mesh of one solid, ready for GPU upload (f32; the kernel's f64
61/// buffers are narrowed exactly once, here).
62#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
63pub struct DisplayMesh {
64    /// Interleaved-ready parallel arrays: xyz per vertex.
65    pub positions: Vec<[f32; 3]>,
66    pub normals: Vec<[f32; 3]>,
67    /// Triangle indices (3 per triangle).
68    pub indices: Vec<u32>,
69    /// Per-TRIANGLE face index into `SolidDisplay::faces`.
70    pub face_ids: Vec<u32>,
71}
72
73/// A displayed solid: mesh + named faces/edges/vertices + visibility.
74#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
75pub struct SolidDisplay {
76    /// Kernel solid name — the scene key.
77    pub name: String,
78    /// The resident kernel handle this display was tessellated from (0 = none,
79    /// e.g. a synthesized sheet with no kernel solid). Handles are monotonic and
80    /// never recycled, so this is a stable identity for the geometry: the R10
81    /// display-reuse fast path keeps an existing display across a history rerun
82    /// ONLY when the name's resident handle still equals this — a name re-bound to
83    /// a DIFFERENT handle (a SUBTRACT result inherits its target's name; a
84    /// roll-back replays that name's ORIGINAL producer) must re-tessellate.
85    pub source_handle: u32,
86    pub visible: bool,
87    /// Optional per-solid base color override (R14 — user-set solid color),
88    /// else the name-hashed stable color is used.
89    pub color_override: Option<[f32; 3]>,
90    /// Monotonic content revision (R10). Every freshly built display gets a
91    /// unique value, so the renderer's GPU-buffer cache re-uploads on any
92    /// rebuild — correct-by-default (equivalent to teardown-rebuild). The
93    /// reused-buffer fast path (keep a revision stable across a `reused`
94    /// pipeline result) is a follow-up optimization.
95    pub revision: u64,
96    pub mesh: DisplayMesh,
97    pub faces: Vec<FaceDisplay>,
98    pub edges: Vec<EdgeDisplay>,
99    pub vertices: Vec<VertexDisplay>,
100    /// Per-entity + group hide state (individual faces/edges/vertices, or a
101    /// whole group). Default = everything visible; see [`crate::visibility`].
102    /// Reused solids keep it across history reruns (this whole struct is cloned
103    /// forward); a re-tessellated solid resets to all-visible.
104    pub visibility: crate::visibility::EntityVisibility,
105    /// World bbox over mesh positions (edges lie on the mesh by construction).
106    pub bbox: Aabb,
107    /// This display is a SYNTHESIZED committed-sketch SHEET (planar face + named
108    /// boundary edges + corner vertices), not a kernel solid — it carries no
109    /// resident handle (`source_handle == 0`). The marker lets the UI treat a
110    /// sketch as a sketch: it is listed under "Sketches" (not among solids) yet is
111    /// pickable / selectable / measurable like any scene solid.
112    pub is_sketch: bool,
113    /// This body is SHEET METAL — its resident handle carried a `SheetTree` when
114    /// the display was built. Stamped by the pipeline from
115    /// [`brep_kernel::is_sheet_metal_handle`] on the RUNNER thread (where the
116    /// tree's thread-local is warm), so the UI thread can answer "is this a
117    /// sheet-metal body?" straight off the scene — the gate for the sheet-metal
118    /// edit features (SM Flange / Fillet / Chamfer). Immutable-correct: a tree is
119    /// attached at solid creation and dropped exactly when the handle is freed,
120    /// handles never recycle, and the display-reuse fast path clones this struct
121    /// forward only while the handle is unchanged.
122    pub is_sheet_metal: bool,
123}
124
125/// Source of monotonic [`SolidDisplay::revision`] values.
126pub(crate) fn next_revision() -> u64 {
127    use std::sync::atomic::{AtomicU64, Ordering};
128    static COUNTER: AtomicU64 = AtomicU64::new(1);
129    COUNTER.fetch_add(1, Ordering::Relaxed)
130}
131
132/// The scene: insertion-ordered solids + an exact name index (R8 — the
133/// `getObjectByName` heuristic-scoring lookup is replaced by this map).
134#[derive(Debug, Default)]
135pub struct RenderScene {
136    solids: Vec<SolidDisplay>,
137    index: HashMap<String, usize>,
138    /// Triangle BVHs for dense displays, built once on first pick. Scene
139    /// mutations invalidate these independently of camera and visibility.
140    pick_trees: RefCell<HashMap<String, brep_kernel::Bvh>>,
141    /// The B-rep of the kernel displays a drawing sheet has ASKED for, keyed by
142    /// solid name with the resident handle it came from: `name -> (handle,
143    /// fingerprint, solid)`. Only a sheet reads topology, and the runner sends
144    /// it only when asked (`EngineState::request_exact_solids`), so a document
145    /// with no sheet holds none. Beside the displays rather than inside them,
146    /// because a display is cloned (the PMI explode keeps originals) and
147    /// rebuilt on the UI thread (committed sketches), and neither wants a copy
148    /// of the topology. Read through [`Self::exact_solid`], which answers only
149    /// while the handle still matches the display's. The `u64` is the solid's
150    /// [`brep_fingerprint`], taken once here rather than per projection.
151    exact: HashMap<String, (u32, u64, brep_kernel::BrepSolid)>,
152    /// Bumped whenever [`Self::exact`] gains or loses a solid, so a projection
153    /// cached without a topology is redrawn when it arrives.
154    exact_revision: u64,
155    /// Bumped whenever [`Self::set_visible`] actually CHANGES a solid's
156    /// visibility — see [`Self::visibility_revision`].
157    visibility_revision: u64,
158}
159
160impl RenderScene {
161    pub fn new() -> Self {
162        Self::default()
163    }
164
165    /// Insert or replace a solid by name (replacement keeps insertion order —
166    /// a boolean result reusing its target's name stays in place).
167    pub fn insert_solid(&mut self, solid: SolidDisplay) {
168        self.pick_trees.get_mut().remove(&solid.name);
169        match self.index.get(&solid.name) {
170            Some(&slot) => self.solids[slot] = solid,
171            None => {
172                self.index.insert(solid.name.clone(), self.solids.len());
173                self.solids.push(solid);
174            }
175        }
176    }
177
178    /// Drop every solid (the scene rebuild path clears then repopulates).
179    pub fn clear(&mut self) {
180        self.pick_trees.get_mut().clear();
181        self.solids.clear();
182        self.index.clear();
183    }
184
185    /// Empty the scene, RETURNING every solid by value (the name index is cleared
186    /// and the vec is `mem::take`-n out). The history-apply seam MOVES existing
187    /// displays out this way to reinsert the reused ones without cloning their
188    /// meshes — a scene-free [`crate::pipeline::SceneRunner`] delta is applied by
189    /// draining then reinserting in snapshot order.
190    pub fn drain(&mut self) -> Vec<SolidDisplay> {
191        self.pick_trees.get_mut().clear();
192        self.index.clear();
193        std::mem::take(&mut self.solids)
194    }
195
196    /// Set (or clear) a solid's metadata color override, bumping its revision
197    /// so the renderer re-derives the base style. Returns false if unknown.
198    pub fn set_color_override(&mut self, name: &str, color: Option<[f32; 3]>) -> bool {
199        let Some(slot) = self.index.get(name).copied() else {
200            return false;
201        };
202        let solid = &mut self.solids[slot];
203        if solid.color_override != color {
204            solid.color_override = color;
205            solid.revision = next_revision();
206        }
207        true
208    }
209
210    /// Re-derive every solid's and face's base colour from the name-keyed
211    /// metadata store — the ONE seam through which the durable `color`
212    /// attribute reaches the display.
213    ///
214    /// `lookup` maps an object NAME (a solid's or a face's) to its resolved
215    /// colour. It returns `None` both for "no colour recorded" and for "the
216    /// display setting is overriding model colours", so this method needs to
217    /// know about neither.
218    ///
219    /// SKETCH sheets are skipped: their `color_override` is the synthesized
220    /// [`crate::engine_state::SKETCH_SHEET_COLOR`], not a metadata colour, and
221    /// re-deriving it from a store that has no record for the sheet would blank
222    /// it back to the global face colour.
223    ///
224    /// A changed solid's `revision` is bumped so the renderer re-uploads it —
225    /// and ONLY when something actually changed. That no-op guarantee is
226    /// load-bearing, not a nicety: this runs after EVERY history apply, and the
227    /// R10 GPU-buffer reuse fast path keys off a stable revision.
228    pub fn apply_metadata_colors(&mut self, lookup: impl Fn(&str) -> Option<[f32; 3]>) -> bool {
229        let mut any = false;
230        for solid in &mut self.solids {
231            if solid.is_sketch {
232                continue;
233            }
234            let mut changed = false;
235            let want = lookup(&solid.name);
236            if solid.color_override != want {
237                solid.color_override = want;
238                changed = true;
239            }
240            for face in &mut solid.faces {
241                // An unnamed face can carry no metadata record, so it always
242                // inherits the solid rather than costing a store lookup.
243                let want = if face.name.is_empty() {
244                    None
245                } else {
246                    lookup(&face.name)
247                };
248                if face.color_override != want {
249                    face.color_override = want;
250                    changed = true;
251                }
252            }
253            if changed {
254                solid.revision = next_revision();
255                any = true;
256            }
257        }
258        any
259    }
260
261    /// Set a solid's visibility (R11). Returns false if unknown.
262    pub fn set_visible(&mut self, name: &str, visible: bool) -> bool {
263        let Some(solid) = self.solid_mut(name) else {
264            return false;
265        };
266        let changed = solid.visible != visible;
267        solid.visible = visible;
268        if changed {
269            self.visibility_revision = self.visibility_revision.wrapping_add(1);
270        }
271        true
272    }
273
274    /// Monotonic counter of VISIBILITY changes — the cache key for anything
275    /// derived from which bodies are shown.
276    ///
277    /// Hiding a body is SESSION state: it writes no history, runs nothing, and
278    /// lands no exact solid, so it moves none of the counters a derived
279    /// drawing would otherwise key on. A drawing sheet draws "the solids
280    /// visible in the scene" (`sheets::project::sheet_solid_names`), so its
281    /// cached projection depends on this and nothing else could tell it.
282    ///
283    /// Only a REAL change counts. `EngineState::apply_view_display` sets every
284    /// solid's visibility from the active PMI view's hidden set on every
285    /// activation, and almost all of those calls are no-ops; counting them
286    /// would re-project the open sheet for nothing.
287    pub fn visibility_revision(&self) -> u64 {
288        self.visibility_revision
289    }
290
291    /// Scene enumeration for the host scene-tree panel (R11): names, kind,
292    /// visibility, child face/edge/vertex counts — as JSON.
293    pub fn listing_json(&self) -> String {
294        let solids: Vec<serde_json::Value> = self
295            .solids
296            .iter()
297            .map(|solid| {
298                serde_json::json!({
299                    "name": solid.name,
300                    "kind": "SOLID",
301                    "visible": solid.visible,
302                    "faces": solid.faces.len(),
303                    "edges": solid.edges.iter().filter(|e| !e.aux).count(),
304                    "vertices": solid.vertices.len(),
305                })
306            })
307            .collect();
308        serde_json::Value::Array(solids).to_string()
309    }
310
311    /// Remove a solid by exact name.
312    pub fn remove_solid(&mut self, name: &str) -> bool {
313        let Some(slot) = self.index.remove(name) else {
314            return false;
315        };
316        self.solids.remove(slot);
317        self.pick_trees.get_mut().remove(name);
318        for value in self.index.values_mut() {
319            if *value > slot {
320                *value -= 1;
321            }
322        }
323        true
324    }
325
326    pub fn solid(&self, name: &str) -> Option<&SolidDisplay> {
327        self.index.get(name).map(|&slot| &self.solids[slot])
328    }
329
330    /// Drop every kept B-rep whose display is gone or now comes from a
331    /// different handle. Called after a run's displays are reconciled, so "the
332    /// display's handle" is that run's.
333    pub fn prune_exact_solids(&mut self) {
334        let (solids, index) = (&self.solids, &self.index);
335        let before = self.exact.len();
336        self.exact.retain(|name, (handle, _, _)| {
337            index.get(name).is_some_and(|&slot| solids[slot].source_handle == *handle)
338        });
339        if self.exact.len() != before {
340            self.exact_revision += 1;
341        }
342    }
343
344    /// Record the B-rep the runner sent for `name`, when `handle` is still the
345    /// one its display was tessellated from; `false` (and nothing kept) when
346    /// the display has moved on since the request went out.
347    pub fn set_exact_solid(&mut self, name: &str, handle: u32, solid: brep_kernel::BrepSolid) -> bool {
348        if handle == 0 || self.solid(name).map(|display| display.source_handle) != Some(handle) {
349            return false;
350        }
351        self.exact.insert(name.to_string(), (handle, brep_fingerprint(&solid), solid));
352        self.exact_revision += 1;
353        true
354    }
355
356    /// Forget the B-rep of `name`: its geometry changed under the SAME handle
357    /// (an assembly solve re-poses a resident solid in place), so the next
358    /// projection must ask again.
359    pub fn drop_exact_solid(&mut self, name: &str) {
360        if self.exact.remove(name).is_some() {
361            self.exact_revision += 1;
362        }
363    }
364
365    /// Forget every B-rep: the handles they were keyed by belong to a runner
366    /// that is gone (a cancel restarts its registry, which counts from one
367    /// again) or to a document that was replaced.
368    pub fn clear_exact_solids(&mut self) {
369        if !self.exact.is_empty() {
370            self.exact.clear();
371            self.exact_revision += 1;
372        }
373    }
374
375    /// See [`Self::exact`]'s revision counter.
376    pub fn exact_revision(&self) -> u64 {
377        self.exact_revision
378    }
379
380    /// The B-rep the displayed solid `name` was tessellated from, or `None`
381    /// when the scene holds none for its CURRENT handle — a synthesized sketch
382    /// sheet, or a solid whose topology did not reach this thread.
383    pub fn exact_solid(&self, name: &str) -> Option<&brep_kernel::BrepSolid> {
384        self.exact_solid_fingerprinted(name).map(|(solid, _)| solid)
385    }
386
387    /// [`Self::exact_solid`] with its content fingerprint.
388    pub fn exact_solid_fingerprinted(&self, name: &str) -> Option<(&brep_kernel::BrepSolid, u64)> {
389        let display = self.solid(name)?;
390        self.exact
391            .get(name)
392            .filter(|(handle, _, _)| *handle == display.source_handle && *handle != 0)
393            .map(|(_, fingerprint, solid)| (solid, *fingerprint))
394    }
395
396    pub fn solid_mut(&mut self, name: &str) -> Option<&mut SolidDisplay> {
397        let slot = *self.index.get(name)?;
398        // A caller can edit positions, indices or face ranges through this
399        // reference without changing the display revision.
400        self.pick_trees.get_mut().remove(name);
401        Some(&mut self.solids[slot])
402    }
403
404    pub(crate) fn pick_triangles(&self, solid: &SolidDisplay, ray: &crate::view::Ray, out: &mut Vec<usize>) {
405        let mut trees = self.pick_trees.borrow_mut();
406        let tree = trees.entry(solid.name.clone()).or_insert_with(|| {
407            let boxes: Vec<_> = solid.mesh.indices.chunks_exact(3).map(|indices| {
408                let mut bounds = brep_kernel::Aabb::empty();
409                for &index in indices {
410                    let p = solid.mesh.positions[index as usize];
411                    bounds.include_point(brep_kernel::Vec3::new(p[0] as f64, p[1] as f64, p[2] as f64));
412                }
413                bounds
414            }).collect();
415            brep_kernel::Bvh::build(&boxes)
416        });
417        let v = |p: [f64; 3]| brep_kernel::Vec3::new(p[0], p[1], p[2]);
418        tree.intersecting_ray(v(ray.origin), v(ray.dir), 1e-9, out);
419        // Preserve face/triangle traversal order, including tied hits.
420        out.sort_unstable();
421    }
422
423    /// Insertion-ordered iteration (deterministic — drives draw order).
424    pub fn solids(&self) -> &[SolidDisplay] {
425        &self.solids
426    }
427
428    /// The world-space polyline of the first display edge named `name` across all
429    /// solids (widened to `f64`), or `None` when no edge carries that exact name.
430    /// The engine-native sketch pickEdges tool (S6b-2) uses this to fetch a picked
431    /// scene edge's geometry for projection into the sketch plane. There is no name
432    /// index for edges (only solids), so this is a linear scan — fine for the
433    /// interactive per-click use.
434    pub fn edge_polyline_world(&self, name: &str) -> Option<Vec<[f64; 3]>> {
435        for solid in &self.solids {
436            for edge in &solid.edges {
437                if edge.name == name {
438                    return Some(
439                        edge.polyline
440                            .iter()
441                            .map(|p| [p[0] as f64, p[1] as f64, p[2] as f64])
442                            .collect(),
443                    );
444                }
445            }
446        }
447        None
448    }
449
450    /// The name of the solid owning the first display edge named `name`, or `None`
451    /// (the companion of [`edge_polyline_world`](Self::edge_polyline_world) — the
452    /// pickEdges tool stores it as external-ref metadata).
453    pub fn edge_solid_name(&self, name: &str) -> Option<&str> {
454        for solid in &self.solids {
455            if solid.edges.iter().any(|edge| edge.name == name) {
456                return Some(&solid.name);
457            }
458        }
459        None
460    }
461
462    /// The world plane of the first display face named `name` across all solids,
463    /// as `(centroid, unit outward normal)`: the area-weighted centroid of the
464    /// face's mesh triangles and the normalized sum of their cross products. The
465    /// watertight tessellation winds every face's triangles by `same_sense`, so
466    /// the cross-product sum IS the outward normal (the stored per-vertex normals
467    /// are shading normals and can be blended at shared boundary vertices). This
468    /// is what lets an extrude/revolve whose `profile` is a resident solid FACE
469    /// (not a sketch) anchor its dimension gizmo — the engine's dimension refs
470    /// fall back to it (`EngineState::lookup_profile_plane`). Hidden solids count
471    /// too: a hidden source solid still anchors the gizmo. `None` when no face
472    /// carries that exact name, it has no triangles, or the triangles are
473    /// degenerate (zero area). Linear scan like [`edge_polyline_world`](Self::edge_polyline_world).
474    pub fn face_plane_world(&self, name: &str) -> Option<([f64; 3], [f64; 3])> {
475        for solid in &self.solids {
476            let Some(face) = solid.faces.iter().find(|face| face.name == name) else {
477                continue;
478            };
479            let positions = &solid.mesh.positions;
480            let indices = &solid.mesh.indices;
481            let start = face.tri_start as usize;
482            let end = (start + face.tri_count as usize).min(indices.len() / 3);
483            let mut weighted = [0.0f64; 3];
484            let mut normal = [0.0f64; 3];
485            let mut total_area = 0.0f64;
486            for tri in start..end {
487                let a = f64_point(positions[indices[tri * 3] as usize]);
488                let b = f64_point(positions[indices[tri * 3 + 1] as usize]);
489                let c = f64_point(positions[indices[tri * 3 + 2] as usize]);
490                let ab = [b[0] - a[0], b[1] - a[1], b[2] - a[2]];
491                let ac = [c[0] - a[0], c[1] - a[1], c[2] - a[2]];
492                // Twice the signed-area vector; its length is 2·area.
493                let cross = [
494                    ab[1] * ac[2] - ab[2] * ac[1],
495                    ab[2] * ac[0] - ab[0] * ac[2],
496                    ab[0] * ac[1] - ab[1] * ac[0],
497                ];
498                let area =
499                    0.5 * (cross[0] * cross[0] + cross[1] * cross[1] + cross[2] * cross[2]).sqrt();
500                for k in 0..3 {
501                    weighted[k] += area * (a[k] + b[k] + c[k]) / 3.0;
502                    normal[k] += cross[k];
503                }
504                total_area += area;
505            }
506            let normal_len =
507                (normal[0] * normal[0] + normal[1] * normal[1] + normal[2] * normal[2]).sqrt();
508            if total_area <= 1e-18 || normal_len <= 1e-18 {
509                return None;
510            }
511            return Some((
512                [
513                    weighted[0] / total_area,
514                    weighted[1] / total_area,
515                    weighted[2] / total_area,
516                ],
517                [normal[0] / normal_len, normal[1] / normal_len, normal[2] / normal_len],
518            ));
519        }
520        None
521    }
522
523    /// Whether the display face named `name` is PLANAR: every one of its mesh
524    /// triangles' normals is parallel to the face's summed normal (within
525    /// `1e-4` in the sine). A cylinder, cone or freeform face fails this; a
526    /// face with no triangles is not planar either. Linear scan like
527    /// [`face_plane_world`](Self::face_plane_world) — the 3D sketch editor asks
528    /// this once per reference pick, never per frame.
529    pub fn face_is_planar(&self, name: &str) -> bool {
530        for solid in &self.solids {
531            let Some(face) = solid.faces.iter().find(|face| face.name == name) else {
532                continue;
533            };
534            let positions = &solid.mesh.positions;
535            let indices = &solid.mesh.indices;
536            let start = face.tri_start as usize;
537            let end = (start + face.tri_count as usize).min(indices.len() / 3);
538            if start >= end {
539                return false;
540            }
541            let cross_of = |tri: usize| -> [f64; 3] {
542                let a = f64_point(positions[indices[tri * 3] as usize]);
543                let b = f64_point(positions[indices[tri * 3 + 1] as usize]);
544                let c = f64_point(positions[indices[tri * 3 + 2] as usize]);
545                let ab = [b[0] - a[0], b[1] - a[1], b[2] - a[2]];
546                let ac = [c[0] - a[0], c[1] - a[1], c[2] - a[2]];
547                [
548                    ab[1] * ac[2] - ab[2] * ac[1],
549                    ab[2] * ac[0] - ab[0] * ac[2],
550                    ab[0] * ac[1] - ab[1] * ac[0],
551                ]
552            };
553            let mut sum = [0.0f64; 3];
554            for tri in start..end {
555                let n = cross_of(tri);
556                for k in 0..3 {
557                    sum[k] += n[k];
558                }
559            }
560            let sum_len = (sum[0] * sum[0] + sum[1] * sum[1] + sum[2] * sum[2]).sqrt();
561            if sum_len <= 1e-18 {
562                return false;
563            }
564            let unit = [sum[0] / sum_len, sum[1] / sum_len, sum[2] / sum_len];
565            for tri in start..end {
566                let n = cross_of(tri);
567                let len = (n[0] * n[0] + n[1] * n[1] + n[2] * n[2]).sqrt();
568                if len <= 1e-18 {
569                    continue; // A sliver triangle says nothing about the plane.
570                }
571                let cross = [
572                    n[1] * unit[2] - n[2] * unit[1],
573                    n[2] * unit[0] - n[0] * unit[2],
574                    n[0] * unit[1] - n[1] * unit[0],
575                ];
576                let sine = (cross[0] * cross[0] + cross[1] * cross[1] + cross[2] * cross[2]).sqrt() / len;
577                if sine > 1e-4 {
578                    return false;
579                }
580            }
581            return true;
582        }
583        false
584    }
585
586    pub fn is_empty(&self) -> bool {
587        self.solids.is_empty()
588    }
589
590    /// World bbox over every VISIBLE solid.
591    pub fn bbox(&self) -> Aabb {
592        let mut bbox = Aabb::empty();
593        for solid in &self.solids {
594            if solid.visible {
595                bbox.union(&solid.bbox);
596            }
597        }
598        bbox
599    }
600}
601
602/// A hash of everything in a B-rep the drawing sheet's hidden-line pass reads: every vertex,
603/// curve, carrier and pcurve number to the bit, and the topology that ties
604/// them together.
605pub fn brep_fingerprint(solid: &brep_kernel::BrepSolid) -> u64 {
606    use std::hash::{Hash, Hasher};
607    let mut hasher = std::collections::hash_map::DefaultHasher::new();
608    let mut numbers = |values: &mut dyn Iterator<Item = f64>| {
609        for value in values {
610            value.to_bits().hash(&mut hasher);
611        }
612    };
613    let curve = |curve: &brep_kernel::NurbsCurve| -> Vec<f64> {
614        let mut out = vec![curve.degree as f64];
615        out.extend(curve.knots.iter().copied());
616        out.extend(curve.control_points.iter().flat_map(|cp| [cp.x, cp.y, cp.z, cp.w]));
617        out
618    };
619    for vertex in &solid.vertices {
620        numbers(&mut [vertex.id as f64, vertex.point.x, vertex.point.y, vertex.point.z].into_iter());
621    }
622    for edge in &solid.edges {
623        numbers(&mut [edge.id as f64, edge.t0, edge.t1, edge.start_vertex_id as f64, edge.end_vertex_id as f64].into_iter());
624        numbers(&mut [edge.degenerate as u8 as f64].into_iter());
625        numbers(&mut curve(&edge.curve).into_iter());
626    }
627    for shell in &solid.shells {
628        for face in &shell.faces {
629            let surface = &face.surface;
630            numbers(&mut [face.same_sense as u8 as f64, surface.degree_u as f64, surface.degree_v as f64].into_iter());
631            numbers(&mut surface.knots_u.iter().chain(&surface.knots_v).copied());
632            numbers(&mut surface.control_points.iter().flatten().flat_map(|cp| [cp.x, cp.y, cp.z, cp.w]));
633            for lp in &face.loops {
634                numbers(&mut [f64::NAN].into_iter());
635                for coedge in &lp.coedges {
636                    numbers(&mut [coedge.edge_id as f64, coedge.forward as u8 as f64].into_iter());
637                    numbers(&mut curve(&coedge.pcurve).into_iter());
638                }
639            }
640        }
641    }
642    hasher.finish()
643}
644
645/// Widen a display-mesh vertex to `f64` (the mesh stores `f32`).
646fn f64_point(p: [f32; 3]) -> [f64; 3] {
647    [p[0] as f64, p[1] as f64, p[2] as f64]
648}
649
650/// Build a [`SolidDisplay`] from the kernel's native display payload.
651pub fn solid_display_from_payload(
652    name: &str,
653    payload: brep_kernel::DisplaySolidPayload,
654) -> SolidDisplay {
655    let mesh_in = payload.mesh;
656    let vertex_count = mesh_in.positions.len() / 3;
657    let mut positions = Vec::with_capacity(vertex_count);
658    let mut normals = Vec::with_capacity(vertex_count);
659    let mut bbox = Aabb::empty();
660    for i in 0..vertex_count {
661        let p = [
662            mesh_in.positions[i * 3],
663            mesh_in.positions[i * 3 + 1],
664            mesh_in.positions[i * 3 + 2],
665        ];
666        bbox.expand(p);
667        positions.push([p[0] as f32, p[1] as f32, p[2] as f32]);
668        normals.push([
669            mesh_in.normals[i * 3] as f32,
670            mesh_in.normals[i * 3 + 1] as f32,
671            mesh_in.normals[i * 3 + 2] as f32,
672        ]);
673    }
674
675    // Face ranges: the watertight mesh emits each face's triangles as one
676    // contiguous run of `face_ids`. Group runs; a face with no triangles gets
677    // an empty range.
678    let mut faces: Vec<FaceDisplay> = payload
679        .faces
680        .iter()
681        .map(|(topo_id, name)| FaceDisplay {
682            name: name.clone().unwrap_or_default(),
683            topo_id: *topo_id,
684            tri_start: 0,
685            tri_count: 0,
686            kind: FaceKind::Unknown,
687            color_override: None,
688        })
689        .collect();
690    let mut run_start = 0u32;
691    let mut run_face: Option<u32> = None;
692    for (tri, &face_id) in mesh_in.face_ids.iter().enumerate() {
693        if run_face != Some(face_id) {
694            run_face = Some(face_id);
695            run_start = tri as u32;
696        }
697        if let Some(face) = faces.get_mut(face_id as usize) {
698            if face.tri_count == 0 {
699                face.tri_start = run_start;
700            }
701            face.tri_count += 1;
702        }
703    }
704
705    // Edges and vertices expand the bbox too. For a real solid they lie ON the
706    // meshed boundary, so this changes nothing; for a synthesized OPEN-sketch
707    // display (edges + endpoints, no face) they are the ONLY extent there is, and
708    // an empty bbox would leave the sketch out of zoom-to-fit and out of every
709    // bbox-gated traversal.
710    for (_, _, points) in &payload.edges {
711        for point in points {
712            bbox.expand([point.x, point.y, point.z]);
713        }
714    }
715    for (_, point) in &payload.vertices {
716        bbox.expand([point.x, point.y, point.z]);
717    }
718
719    let edges = payload
720        .edges
721        .into_iter()
722        .map(|(topo_id, name, points)| EdgeDisplay {
723            name: name.unwrap_or_default(),
724            topo_id,
725            polyline: points
726                .iter()
727                .map(|p| [p.x as f32, p.y as f32, p.z as f32])
728                .collect(),
729            aux: false,
730            centerline: false,
731        })
732        .collect();
733
734    let vertices = payload
735        .vertices
736        .into_iter()
737        .map(|(topo_id, p)| VertexDisplay {
738            topo_id,
739            position: [p.x, p.y, p.z],
740        })
741        .collect();
742
743    SolidDisplay {
744        name: name.to_string(),
745        source_handle: 0, // set by the caller that knows the resident handle
746        visible: true,
747        color_override: None,
748        revision: next_revision(),
749        mesh: DisplayMesh {
750            positions,
751            normals,
752            indices: mesh_in.indices,
753            face_ids: mesh_in.face_ids,
754        },
755        faces,
756        edges,
757        vertices,
758        visibility: crate::visibility::EntityVisibility::default(),
759        bbox,
760        is_sketch: false, // set by the sketch-sheet synthesizer for a committed sketch
761        is_sheet_metal: false, // stamped by the pipeline from the resident handle's SheetTree
762    }
763}