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