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}
190
191impl RenderScene {
192 pub fn new() -> Self {
193 Self::default()
194 }
195
196 /// Insert or replace a solid by name (replacement keeps insertion order —
197 /// a boolean result reusing its target's name stays in place).
198 pub fn insert_solid(&mut self, solid: SolidDisplay) {
199 self.pick_trees.get_mut().remove(&solid.name);
200 match self.index.get(&solid.name) {
201 Some(&slot) => self.solids[slot] = solid,
202 None => {
203 self.index.insert(solid.name.clone(), self.solids.len());
204 self.solids.push(solid);
205 }
206 }
207 }
208
209 /// Drop every solid (the scene rebuild path clears then repopulates).
210 pub fn clear(&mut self) {
211 self.pick_trees.get_mut().clear();
212 self.solids.clear();
213 self.index.clear();
214 self.highlight.clear();
215 }
216
217 /// Empty the scene, RETURNING every solid by value (the name index is cleared
218 /// and the vec is `mem::take`-n out). The history-apply seam MOVES existing
219 /// displays out this way to reinsert the reused ones without cloning their
220 /// meshes — a scene-free [`crate::pipeline::SceneRunner`] delta is applied by
221 /// draining then reinserting in snapshot order.
222 pub fn drain(&mut self) -> Vec<SolidDisplay> {
223 self.pick_trees.get_mut().clear();
224 self.index.clear();
225 std::mem::take(&mut self.solids)
226 }
227
228 /// Set (or clear) a solid's metadata color override, bumping its revision
229 /// so the renderer re-derives the base style. Returns false if unknown.
230 pub fn set_color_override(&mut self, name: &str, color: Option<[f32; 3]>) -> bool {
231 let Some(slot) = self.index.get(name).copied() else {
232 return false;
233 };
234 let solid = &mut self.solids[slot];
235 if solid.color_override != color {
236 solid.color_override = color;
237 solid.revision = next_revision();
238 solid.shape_revision = 0;
239 solid.pose = None;
240 }
241 true
242 }
243
244 /// Set the per-FACE colour overrides of solid `name`, face `i` taking
245 /// `colors[i]` (faces past the slice are left alone), bumping the solid's
246 /// revision only when one changed. Returns false if the solid is unknown.
247 /// A display-only body (the mesh import's region view) colours its faces
248 /// through this.
249 pub fn set_face_colors(&mut self, name: &str, colors: &[Option<[f32; 3]>]) -> bool {
250 let Some(slot) = self.index.get(name).copied() else {
251 return false;
252 };
253 let solid = &mut self.solids[slot];
254 let mut changed = false;
255 for (face, &color) in solid.faces.iter_mut().zip(colors) {
256 if face.color_override != color {
257 face.color_override = color;
258 changed = true;
259 }
260 }
261 if changed {
262 solid.revision = next_revision();
263 solid.shape_revision = 0;
264 solid.pose = None;
265 }
266 true
267 }
268
269 /// Re-derive every solid's and face's base colour from the name-keyed
270 /// metadata store — the ONE seam through which the durable `color`
271 /// attribute reaches the display.
272 ///
273 /// `lookup` maps an object NAME (a solid's or a face's) to its resolved
274 /// colour. It returns `None` both for "no colour recorded" and for "the
275 /// display setting is overriding model colours", so this method needs to
276 /// know about neither.
277 ///
278 /// A SKETCH sheet's own `color_override` is the synthesized
279 /// `crate::engine_state::SKETCH_SHEET_COLOR`, not a metadata colour, and
280 /// re-deriving it from a store that has no record for the sheet would blank
281 /// it back to the global face colour — so it is left alone. The sketch's
282 /// `color` attribute (keyed by the sketch id, which is the sheet's name)
283 /// lands on the sheet's FACES instead: a face override wins over the
284 /// solid's colour, and clearing the attribute clears the face overrides,
285 /// which restores the synthesized sheet colour underneath.
286 ///
287 /// A changed solid's `revision` is bumped so the renderer re-uploads it —
288 /// and ONLY when something actually changed. That no-op guarantee is
289 /// load-bearing, not a nicety: this runs after EVERY history apply, and the
290 /// R10 GPU-buffer reuse fast path keys off a stable revision.
291 pub fn apply_metadata_colors(&mut self, lookup: impl Fn(&str) -> Option<[f32; 3]>) -> bool {
292 let mut any = false;
293 for solid in &mut self.solids {
294 let mut changed = false;
295 // A sketch sheet keeps its synthesized base colour; its metadata
296 // colour reaches the display through the faces below.
297 let sheet_color = if solid.is_sketch {
298 lookup(&solid.name)
299 } else {
300 let want = lookup(&solid.name);
301 if solid.color_override != want {
302 solid.color_override = want;
303 changed = true;
304 }
305 None
306 };
307 for face in &mut solid.faces {
308 // A section cap keeps the section colour its view gave it.
309 if face.kind == FaceKind::SectionCap {
310 continue;
311 }
312 // An unnamed face can carry no metadata record, so it always
313 // inherits the solid rather than costing a store lookup.
314 let want = if face.name.is_empty() {
315 None
316 } else {
317 lookup(&face.name)
318 }
319 .or(sheet_color);
320 if face.color_override != want {
321 face.color_override = want;
322 changed = true;
323 }
324 }
325 if changed {
326 solid.revision = next_revision();
327 solid.shape_revision = 0;
328 solid.pose = None;
329 any = true;
330 }
331 }
332 any
333 }
334
335 /// Set a solid's visibility (R11). Returns false if unknown.
336 pub fn set_visible(&mut self, name: &str, visible: bool) -> bool {
337 let Some(solid) = self.solid_mut(name) else {
338 return false;
339 };
340 let changed = solid.visible != visible;
341 solid.visible = visible;
342 if changed {
343 self.visibility_revision = self.visibility_revision.wrapping_add(1);
344 }
345 true
346 }
347
348 /// Monotonic counter of VISIBILITY changes — the cache key for anything
349 /// derived from which bodies are shown.
350 ///
351 /// Hiding a body is SESSION state: it writes no history, runs nothing, and
352 /// lands no exact solid, so it moves none of the counters a derived
353 /// drawing would otherwise key on. A drawing sheet draws "the solids
354 /// visible in the scene" (`sheets::project::sheet_solid_names`), so its
355 /// cached projection depends on this and nothing else could tell it.
356 ///
357 /// Only a REAL change counts. `EngineState::apply_view_display` sets every
358 /// solid's visibility from the active PMI view's hidden set on every
359 /// activation, and almost all of those calls are no-ops; counting them
360 /// would re-project the open sheet for nothing.
361 pub fn visibility_revision(&self) -> u64 {
362 self.visibility_revision
363 }
364
365 /// Scene enumeration for the host scene-tree panel (R11): names, kind,
366 /// visibility, child face/edge/vertex counts — as JSON.
367 pub fn listing_json(&self) -> String {
368 let solids: Vec<serde_json::Value> = self
369 .solids
370 .iter()
371 .map(|solid| {
372 serde_json::json!({
373 "name": solid.name,
374 "kind": "SOLID",
375 "visible": solid.visible,
376 "faces": solid.faces.len(),
377 "edges": solid.edges.iter().filter(|e| !e.aux).count(),
378 "vertices": solid.vertices.len(),
379 })
380 })
381 .collect();
382 serde_json::Value::Array(solids).to_string()
383 }
384
385 /// Remove a solid by exact name.
386 pub fn remove_solid(&mut self, name: &str) -> bool {
387 let Some(slot) = self.index.remove(name) else {
388 return false;
389 };
390 self.solids.remove(slot);
391 self.pick_trees.get_mut().remove(name);
392 for value in self.index.values_mut() {
393 if *value > slot {
394 *value -= 1;
395 }
396 }
397 true
398 }
399
400 pub fn solid(&self, name: &str) -> Option<&SolidDisplay> {
401 self.index.get(name).map(|&slot| &self.solids[slot])
402 }
403
404 /// Drop every kept B-rep whose display is gone or now comes from a
405 /// different handle. Called after a run's displays are reconciled, so "the
406 /// display's handle" is that run's.
407 pub fn prune_exact_solids(&mut self) {
408 let (solids, index) = (&self.solids, &self.index);
409 let before = self.exact.len();
410 self.exact.retain(|name, (handle, _, _)| {
411 index.get(name).is_some_and(|&slot| solids[slot].source_handle == *handle)
412 });
413 if self.exact.len() != before {
414 self.exact_revision += 1;
415 }
416 }
417
418 /// Record the B-rep the runner sent for `name`, when `handle` is still the
419 /// one its display was tessellated from; `false` (and nothing kept) when
420 /// the display has moved on since the request went out.
421 pub fn set_exact_solid(&mut self, name: &str, handle: u32, solid: brep_kernel::BrepSolid) -> bool {
422 if handle == 0 || self.solid(name).map(|display| display.source_handle) != Some(handle) {
423 return false;
424 }
425 self.exact.insert(name.to_string(), (handle, brep_fingerprint(&solid), solid));
426 self.exact_revision += 1;
427 true
428 }
429
430 /// Forget the B-rep of `name`: its geometry changed under the SAME handle
431 /// (an assembly solve re-poses a resident solid in place), so the next
432 /// projection must ask again.
433 pub fn drop_exact_solid(&mut self, name: &str) {
434 if self.exact.remove(name).is_some() {
435 self.exact_revision += 1;
436 }
437 }
438
439 /// Forget every B-rep: the handles they were keyed by belong to a runner
440 /// that is gone (a cancel restarts its registry, which counts from one
441 /// again) or to a document that was replaced.
442 pub fn clear_exact_solids(&mut self) {
443 if !self.exact.is_empty() {
444 self.exact.clear();
445 self.exact_revision += 1;
446 }
447 }
448
449 /// See `Self::exact`'s revision counter.
450 pub fn exact_revision(&self) -> u64 {
451 self.exact_revision
452 }
453
454 /// The B-rep the displayed solid `name` was tessellated from, or `None`
455 /// when the scene holds none for its CURRENT handle — a synthesized sketch
456 /// sheet, or a solid whose topology did not reach this thread.
457 pub fn exact_solid(&self, name: &str) -> Option<&brep_kernel::BrepSolid> {
458 self.exact_solid_fingerprinted(name).map(|(solid, _)| solid)
459 }
460
461 /// [`Self::exact_solid`] with its content fingerprint.
462 pub fn exact_solid_fingerprinted(&self, name: &str) -> Option<(&brep_kernel::BrepSolid, u64)> {
463 let display = self.solid(name)?;
464 self.exact
465 .get(name)
466 .filter(|(handle, _, _)| *handle == display.source_handle && *handle != 0)
467 .map(|(_, fingerprint, solid)| (solid, *fingerprint))
468 }
469
470 pub fn solid_mut(&mut self, name: &str) -> Option<&mut SolidDisplay> {
471 let slot = *self.index.get(name)?;
472 // A caller can edit positions, indices or face ranges through this
473 // reference without changing the display revision.
474 self.pick_trees.get_mut().remove(name);
475 Some(&mut self.solids[slot])
476 }
477
478 pub(crate) fn pick_triangles(&self, solid: &SolidDisplay, ray: &crate::view::Ray, out: &mut Vec<usize>) {
479 let mut trees = self.pick_trees.borrow_mut();
480 let tree = trees.entry(solid.name.clone()).or_insert_with(|| {
481 let boxes: Vec<_> = solid.mesh.indices.chunks_exact(3).map(|indices| {
482 let mut bounds = brep_kernel::Aabb::empty();
483 for &index in indices {
484 let p = solid.mesh.positions[index as usize];
485 bounds.include_point(brep_kernel::Vec3::new(p[0] as f64, p[1] as f64, p[2] as f64));
486 }
487 bounds
488 }).collect();
489 brep_kernel::Bvh::build(&boxes)
490 });
491 let v = |p: [f64; 3]| brep_kernel::Vec3::new(p[0], p[1], p[2]);
492 tree.intersecting_ray(v(ray.origin), v(ray.dir), 1e-9, out);
493 // Preserve face/triangle traversal order, including tied hits.
494 out.sort_unstable();
495 }
496
497 /// Insertion-ordered iteration (deterministic — drives draw order).
498 pub fn solids(&self) -> &[SolidDisplay] {
499 &self.solids
500 }
501
502 /// The world-space polyline of the first display edge named `name` across all
503 /// solids (widened to `f64`), or `None` when no edge carries that exact name.
504 /// The engine-native sketch pickEdges tool (S6b-2) uses this to fetch a picked
505 /// scene edge's geometry for projection into the sketch plane. There is no name
506 /// index for edges (only solids), so this is a linear scan — fine for the
507 /// interactive per-click use.
508 pub fn edge_polyline_world(&self, name: &str) -> Option<Vec<[f64; 3]>> {
509 for solid in &self.solids {
510 for edge in &solid.edges {
511 if edge.name == name {
512 return Some(
513 edge.polyline
514 .iter()
515 .map(|p| [p[0] as f64, p[1] as f64, p[2] as f64])
516 .collect(),
517 );
518 }
519 }
520 }
521 None
522 }
523
524 /// The name of the solid owning the first display edge named `name`, or `None`
525 /// (the companion of [`edge_polyline_world`](Self::edge_polyline_world) — the
526 /// pickEdges tool stores it as external-ref metadata).
527 pub fn edge_solid_name(&self, name: &str) -> Option<&str> {
528 for solid in &self.solids {
529 if solid.edges.iter().any(|edge| edge.name == name) {
530 return Some(&solid.name);
531 }
532 }
533 None
534 }
535
536 /// The world plane of the first display face named `name` across all solids,
537 /// as `(centroid, unit outward normal)`: the area-weighted centroid of the
538 /// face's mesh triangles and the normalized sum of their cross products. The
539 /// watertight tessellation winds every face's triangles by `same_sense`, so
540 /// the cross-product sum IS the outward normal (the stored per-vertex normals
541 /// are shading normals and can be blended at shared boundary vertices). This
542 /// is what lets an extrude/revolve whose `profile` is a resident solid FACE
543 /// (not a sketch) anchor its dimension gizmo — the engine's dimension refs
544 /// fall back to it (`EngineState::lookup_profile_plane`). Hidden solids count
545 /// too: a hidden source solid still anchors the gizmo. `None` when no face
546 /// carries that exact name, it has no triangles, or the triangles are
547 /// degenerate (zero area). Linear scan like [`edge_polyline_world`](Self::edge_polyline_world).
548 pub fn face_plane_world(&self, name: &str) -> Option<([f64; 3], [f64; 3])> {
549 for solid in &self.solids {
550 let Some(face) = solid.faces.iter().find(|face| face.name == name) else {
551 continue;
552 };
553 let positions = &solid.mesh.positions;
554 let indices = &solid.mesh.indices;
555 let start = face.tri_start as usize;
556 let end = (start + face.tri_count as usize).min(indices.len() / 3);
557 let mut weighted = [0.0f64; 3];
558 let mut normal = [0.0f64; 3];
559 let mut total_area = 0.0f64;
560 for tri in start..end {
561 let a = f64_point(positions[indices[tri * 3] as usize]);
562 let b = f64_point(positions[indices[tri * 3 + 1] as usize]);
563 let c = f64_point(positions[indices[tri * 3 + 2] as usize]);
564 let ab = [b[0] - a[0], b[1] - a[1], b[2] - a[2]];
565 let ac = [c[0] - a[0], c[1] - a[1], c[2] - a[2]];
566 // Twice the signed-area vector; its length is 2·area.
567 let cross = [
568 ab[1] * ac[2] - ab[2] * ac[1],
569 ab[2] * ac[0] - ab[0] * ac[2],
570 ab[0] * ac[1] - ab[1] * ac[0],
571 ];
572 let area =
573 0.5 * (cross[0] * cross[0] + cross[1] * cross[1] + cross[2] * cross[2]).sqrt();
574 for k in 0..3 {
575 weighted[k] += area * (a[k] + b[k] + c[k]) / 3.0;
576 normal[k] += cross[k];
577 }
578 total_area += area;
579 }
580 let normal_len =
581 (normal[0] * normal[0] + normal[1] * normal[1] + normal[2] * normal[2]).sqrt();
582 if total_area <= 1e-18 || normal_len <= 1e-18 {
583 return None;
584 }
585 return Some((
586 [
587 weighted[0] / total_area,
588 weighted[1] / total_area,
589 weighted[2] / total_area,
590 ],
591 [normal[0] / normal_len, normal[1] / normal_len, normal[2] / normal_len],
592 ));
593 }
594 None
595 }
596
597 /// Whether the display face named `name` is PLANAR: every one of its mesh
598 /// triangles' normals is parallel to the face's summed normal (within
599 /// `1e-4` in the sine). A cylinder, cone or freeform face fails this; a
600 /// face with no triangles is not planar either. Linear scan like
601 /// [`face_plane_world`](Self::face_plane_world) — the 3D sketch editor asks
602 /// this once per reference pick, never per frame.
603 pub fn face_is_planar(&self, name: &str) -> bool {
604 for solid in &self.solids {
605 let Some(face) = solid.faces.iter().find(|face| face.name == name) else {
606 continue;
607 };
608 let positions = &solid.mesh.positions;
609 let indices = &solid.mesh.indices;
610 let start = face.tri_start as usize;
611 let end = (start + face.tri_count as usize).min(indices.len() / 3);
612 if start >= end {
613 return false;
614 }
615 let cross_of = |tri: usize| -> [f64; 3] {
616 let a = f64_point(positions[indices[tri * 3] as usize]);
617 let b = f64_point(positions[indices[tri * 3 + 1] as usize]);
618 let c = f64_point(positions[indices[tri * 3 + 2] as usize]);
619 let ab = [b[0] - a[0], b[1] - a[1], b[2] - a[2]];
620 let ac = [c[0] - a[0], c[1] - a[1], c[2] - a[2]];
621 [
622 ab[1] * ac[2] - ab[2] * ac[1],
623 ab[2] * ac[0] - ab[0] * ac[2],
624 ab[0] * ac[1] - ab[1] * ac[0],
625 ]
626 };
627 let mut sum = [0.0f64; 3];
628 for tri in start..end {
629 let n = cross_of(tri);
630 for k in 0..3 {
631 sum[k] += n[k];
632 }
633 }
634 let sum_len = (sum[0] * sum[0] + sum[1] * sum[1] + sum[2] * sum[2]).sqrt();
635 if sum_len <= 1e-18 {
636 return false;
637 }
638 let unit = [sum[0] / sum_len, sum[1] / sum_len, sum[2] / sum_len];
639 for tri in start..end {
640 let n = cross_of(tri);
641 let len = (n[0] * n[0] + n[1] * n[1] + n[2] * n[2]).sqrt();
642 if len <= 1e-18 {
643 continue; // A sliver triangle says nothing about the plane.
644 }
645 let cross = [
646 n[1] * unit[2] - n[2] * unit[1],
647 n[2] * unit[0] - n[0] * unit[2],
648 n[0] * unit[1] - n[1] * unit[0],
649 ];
650 let sine = (cross[0] * cross[0] + cross[1] * cross[1] + cross[2] * cross[2]).sqrt() / len;
651 if sine > 1e-4 {
652 return false;
653 }
654 }
655 return true;
656 }
657 false
658 }
659
660 pub fn is_empty(&self) -> bool {
661 self.solids.is_empty()
662 }
663
664 /// World bbox over every VISIBLE solid.
665 pub fn bbox(&self) -> Aabb {
666 let mut bbox = Aabb::empty();
667 for solid in &self.solids {
668 if solid.visible {
669 bbox.union(&solid.bbox);
670 }
671 }
672 bbox
673 }
674}
675
676/// A hash of everything in a B-rep the drawing sheet's hidden-line pass reads: every vertex,
677/// curve, carrier and pcurve number to the bit, and the topology that ties
678/// them together.
679pub fn brep_fingerprint(solid: &brep_kernel::BrepSolid) -> u64 {
680 use std::hash::{Hash, Hasher};
681 let mut hasher = std::collections::hash_map::DefaultHasher::new();
682 let mut numbers = |values: &mut dyn Iterator<Item = f64>| {
683 for value in values {
684 value.to_bits().hash(&mut hasher);
685 }
686 };
687 let curve = |curve: &brep_kernel::NurbsCurve| -> Vec<f64> {
688 let mut out = vec![curve.degree as f64];
689 out.extend(curve.knots.iter().copied());
690 out.extend(curve.control_points.iter().flat_map(|cp| [cp.x, cp.y, cp.z, cp.w]));
691 out
692 };
693 for vertex in &solid.vertices {
694 numbers(&mut [vertex.id as f64, vertex.point.x, vertex.point.y, vertex.point.z].into_iter());
695 }
696 for edge in &solid.edges {
697 numbers(&mut [edge.id as f64, edge.t0, edge.t1, edge.start_vertex_id as f64, edge.end_vertex_id as f64].into_iter());
698 numbers(&mut [edge.degenerate as u8 as f64].into_iter());
699 numbers(&mut curve(&edge.curve).into_iter());
700 }
701 for shell in &solid.shells {
702 for face in &shell.faces {
703 let surface = &face.surface;
704 numbers(&mut [face.same_sense as u8 as f64, surface.degree_u as f64, surface.degree_v as f64].into_iter());
705 numbers(&mut surface.knots_u.iter().chain(&surface.knots_v).copied());
706 numbers(&mut surface.control_points.iter().flatten().flat_map(|cp| [cp.x, cp.y, cp.z, cp.w]));
707 for lp in &face.loops {
708 numbers(&mut [f64::NAN].into_iter());
709 for coedge in &lp.coedges {
710 numbers(&mut [coedge.edge_id as f64, coedge.forward as u8 as f64].into_iter());
711 numbers(&mut curve(&coedge.pcurve).into_iter());
712 }
713 }
714 }
715 }
716 hasher.finish()
717}
718
719/// Widen a display-mesh vertex to `f64` (the mesh stores `f32`).
720fn f64_point(p: [f32; 3]) -> [f64; 3] {
721 [p[0] as f64, p[1] as f64, p[2] as f64]
722}
723
724/// Build a [`SolidDisplay`] from the kernel's native display payload.
725pub fn solid_display_from_payload(
726 name: &str,
727 payload: brep_kernel::DisplaySolidPayload,
728) -> SolidDisplay {
729 let mesh_in = payload.mesh;
730 let vertex_count = mesh_in.positions.len() / 3;
731 let mut positions = Vec::with_capacity(vertex_count);
732 let mut normals = Vec::with_capacity(vertex_count);
733 let mut bbox = Aabb::empty();
734 for i in 0..vertex_count {
735 let p = [
736 mesh_in.positions[i * 3],
737 mesh_in.positions[i * 3 + 1],
738 mesh_in.positions[i * 3 + 2],
739 ];
740 bbox.expand(p);
741 positions.push([p[0] as f32, p[1] as f32, p[2] as f32]);
742 normals.push([
743 mesh_in.normals[i * 3] as f32,
744 mesh_in.normals[i * 3 + 1] as f32,
745 mesh_in.normals[i * 3 + 2] as f32,
746 ]);
747 }
748
749 // Face ranges: the watertight mesh emits each face's triangles as one
750 // contiguous run of `face_ids`. Group runs; a face with no triangles gets
751 // an empty range.
752 let mut faces: Vec<FaceDisplay> = payload
753 .faces
754 .iter()
755 .map(|(topo_id, name)| FaceDisplay {
756 name: name.clone().unwrap_or_default(),
757 topo_id: *topo_id,
758 tri_start: 0,
759 tri_count: 0,
760 kind: FaceKind::Unknown,
761 color_override: None,
762 })
763 .collect();
764 let mut run_start = 0u32;
765 let mut run_face: Option<u32> = None;
766 for (tri, &face_id) in mesh_in.face_ids.iter().enumerate() {
767 if run_face != Some(face_id) {
768 run_face = Some(face_id);
769 run_start = tri as u32;
770 }
771 if let Some(face) = faces.get_mut(face_id as usize) {
772 if face.tri_count == 0 {
773 face.tri_start = run_start;
774 }
775 face.tri_count += 1;
776 }
777 }
778
779 // Edges and vertices expand the bbox too. For a real solid they lie ON the
780 // meshed boundary, so this changes nothing; for a synthesized OPEN-sketch
781 // display (edges + endpoints, no face) they are the ONLY extent there is, and
782 // an empty bbox would leave the sketch out of zoom-to-fit and out of every
783 // bbox-gated traversal.
784 for (_, _, points) in &payload.edges {
785 for point in points {
786 bbox.expand([point.x, point.y, point.z]);
787 }
788 }
789 for (_, point) in &payload.vertices {
790 bbox.expand([point.x, point.y, point.z]);
791 }
792
793 let edges = payload
794 .edges
795 .into_iter()
796 .map(|(topo_id, name, points)| EdgeDisplay {
797 name: name.unwrap_or_default(),
798 topo_id,
799 polyline: points
800 .iter()
801 .map(|p| [p.x as f32, p.y as f32, p.z as f32])
802 .collect(),
803 aux: false,
804 centerline: false,
805 })
806 .collect();
807
808 let vertices = payload
809 .vertices
810 .into_iter()
811 .map(|(topo_id, p)| VertexDisplay {
812 topo_id,
813 position: [p.x, p.y, p.z],
814 })
815 .collect();
816
817 SolidDisplay {
818 name: name.to_string(),
819 source_handle: 0, // set by the caller that knows the resident handle
820 visible: true,
821 color_override: None,
822 opacity: None,
823 revision: next_revision(),
824 shape_revision: 0,
825 pose: None,
826 mesh: DisplayMesh {
827 positions,
828 normals,
829 indices: mesh_in.indices,
830 face_ids: mesh_in.face_ids,
831 },
832 faces,
833 edges,
834 vertices,
835 visibility: crate::visibility::EntityVisibility::default(),
836 bbox,
837 is_sketch: false, // set by the sketch-sheet synthesizer for a committed sketch
838 is_sheet_metal: false, // stamped by the pipeline from the resident handle's SheetTree
839 }
840}