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