brep_render/scene.rs
1//! The renderer-agnostic scene store (requirements R8–R9): display objects
2//! keyed by the SAME kernel names the feature pipeline mints, with typed
3//! metadata — no `userData` grab-bag, no renderer objects. Rendering, picking
4//! and feature-reference display all resolve against this one map.
5//!
6//! Slice 1 populates solids (face mesh ranges + names, edge polylines + names,
7//! vertices) from the kernel's native display payload. Datums/sketches/widgets
8//! arrive in later slices as further [`DisplayObject`] kinds.
9
10use crate::camera::Aabb;
11use std::collections::HashMap;
12
13/// The kind of a display face (surface classification rides along when known —
14/// typed replacement for the previous app's untyped `faceKind` tag).
15#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
16pub enum FaceKind {
17 Unknown,
18}
19
20/// One face of a solid: a contiguous triangle range of the solid mesh plus the
21/// kernel face identity.
22#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
23pub struct FaceDisplay {
24 /// Kernel face name (byte-exact pipeline name); empty when unnamed.
25 pub name: String,
26 /// Kernel topology face id.
27 pub topo_id: u64,
28 /// First triangle (not index) of this face in the mesh.
29 pub tri_start: u32,
30 /// Triangle count.
31 pub tri_count: u32,
32 pub kind: FaceKind,
33}
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}
112
113/// Source of monotonic [`SolidDisplay::revision`] values.
114fn next_revision() -> u64 {
115 use std::sync::atomic::{AtomicU64, Ordering};
116 static COUNTER: AtomicU64 = AtomicU64::new(1);
117 COUNTER.fetch_add(1, Ordering::Relaxed)
118}
119
120/// The scene: insertion-ordered solids + an exact name index (R8 — the
121/// `getObjectByName` heuristic-scoring lookup is replaced by this map).
122#[derive(Debug, Default)]
123pub struct RenderScene {
124 solids: Vec<SolidDisplay>,
125 index: HashMap<String, usize>,
126}
127
128impl RenderScene {
129 pub fn new() -> Self {
130 Self::default()
131 }
132
133 /// Insert or replace a solid by name (replacement keeps insertion order —
134 /// a boolean result reusing its target's name stays in place).
135 pub fn insert_solid(&mut self, solid: SolidDisplay) {
136 match self.index.get(&solid.name) {
137 Some(&slot) => self.solids[slot] = solid,
138 None => {
139 self.index.insert(solid.name.clone(), self.solids.len());
140 self.solids.push(solid);
141 }
142 }
143 }
144
145 /// Drop every solid (the scene rebuild path clears then repopulates).
146 pub fn clear(&mut self) {
147 self.solids.clear();
148 self.index.clear();
149 }
150
151 /// Empty the scene, RETURNING every solid by value (the name index is cleared
152 /// and the vec is `mem::take`-n out). The history-apply seam MOVES existing
153 /// displays out this way to reinsert the reused ones without cloning their
154 /// meshes — a scene-free [`crate::pipeline::SceneRunner`] delta is applied by
155 /// draining then reinserting in snapshot order.
156 pub fn drain(&mut self) -> Vec<SolidDisplay> {
157 self.index.clear();
158 std::mem::take(&mut self.solids)
159 }
160
161 /// Set (or clear) a solid's metadata color override, bumping its revision
162 /// so the renderer re-derives the base style. Returns false if unknown.
163 pub fn set_color_override(&mut self, name: &str, color: Option<[f32; 3]>) -> bool {
164 let Some(slot) = self.index.get(name).copied() else {
165 return false;
166 };
167 let solid = &mut self.solids[slot];
168 if solid.color_override != color {
169 solid.color_override = color;
170 solid.revision = next_revision();
171 }
172 true
173 }
174
175 /// Set a solid's visibility (R11). Returns false if unknown.
176 pub fn set_visible(&mut self, name: &str, visible: bool) -> bool {
177 match self.solid_mut(name) {
178 Some(solid) => {
179 solid.visible = visible;
180 true
181 }
182 None => false,
183 }
184 }
185
186 /// Scene enumeration for the host scene-tree panel (R11): names, kind,
187 /// visibility, child face/edge/vertex counts — as JSON.
188 pub fn listing_json(&self) -> String {
189 let solids: Vec<serde_json::Value> = self
190 .solids
191 .iter()
192 .map(|solid| {
193 serde_json::json!({
194 "name": solid.name,
195 "kind": "SOLID",
196 "visible": solid.visible,
197 "faces": solid.faces.len(),
198 "edges": solid.edges.len(),
199 "vertices": solid.vertices.len(),
200 })
201 })
202 .collect();
203 serde_json::Value::Array(solids).to_string()
204 }
205
206 /// Remove a solid by exact name.
207 pub fn remove_solid(&mut self, name: &str) -> bool {
208 let Some(slot) = self.index.remove(name) else {
209 return false;
210 };
211 self.solids.remove(slot);
212 for value in self.index.values_mut() {
213 if *value > slot {
214 *value -= 1;
215 }
216 }
217 true
218 }
219
220 pub fn solid(&self, name: &str) -> Option<&SolidDisplay> {
221 self.index.get(name).map(|&slot| &self.solids[slot])
222 }
223
224 pub fn solid_mut(&mut self, name: &str) -> Option<&mut SolidDisplay> {
225 let slot = *self.index.get(name)?;
226 Some(&mut self.solids[slot])
227 }
228
229 /// Insertion-ordered iteration (deterministic — drives draw order).
230 pub fn solids(&self) -> &[SolidDisplay] {
231 &self.solids
232 }
233
234 /// The world-space polyline of the first display edge named `name` across all
235 /// solids (widened to `f64`), or `None` when no edge carries that exact name.
236 /// The engine-native sketch pickEdges tool (S6b-2) uses this to fetch a picked
237 /// scene edge's geometry for projection into the sketch plane. There is no name
238 /// index for edges (only solids), so this is a linear scan — fine for the
239 /// interactive per-click use.
240 pub fn edge_polyline_world(&self, name: &str) -> Option<Vec<[f64; 3]>> {
241 for solid in &self.solids {
242 for edge in &solid.edges {
243 if edge.name == name {
244 return Some(
245 edge.polyline
246 .iter()
247 .map(|p| [p[0] as f64, p[1] as f64, p[2] as f64])
248 .collect(),
249 );
250 }
251 }
252 }
253 None
254 }
255
256 /// The name of the solid owning the first display edge named `name`, or `None`
257 /// (the companion of [`edge_polyline_world`](Self::edge_polyline_world) — the
258 /// pickEdges tool stores it as external-ref metadata).
259 pub fn edge_solid_name(&self, name: &str) -> Option<&str> {
260 for solid in &self.solids {
261 if solid.edges.iter().any(|edge| edge.name == name) {
262 return Some(&solid.name);
263 }
264 }
265 None
266 }
267
268 pub fn is_empty(&self) -> bool {
269 self.solids.is_empty()
270 }
271
272 /// World bbox over every VISIBLE solid.
273 pub fn bbox(&self) -> Aabb {
274 let mut bbox = Aabb::empty();
275 for solid in &self.solids {
276 if solid.visible {
277 bbox.union(&solid.bbox);
278 }
279 }
280 bbox
281 }
282}
283
284/// Build a [`SolidDisplay`] from the kernel's native display payload.
285pub fn solid_display_from_payload(
286 name: &str,
287 payload: brep_kernel::DisplaySolidPayload,
288) -> SolidDisplay {
289 let mesh_in = payload.mesh;
290 let vertex_count = mesh_in.positions.len() / 3;
291 let mut positions = Vec::with_capacity(vertex_count);
292 let mut normals = Vec::with_capacity(vertex_count);
293 let mut bbox = Aabb::empty();
294 for i in 0..vertex_count {
295 let p = [
296 mesh_in.positions[i * 3],
297 mesh_in.positions[i * 3 + 1],
298 mesh_in.positions[i * 3 + 2],
299 ];
300 bbox.expand(p);
301 positions.push([p[0] as f32, p[1] as f32, p[2] as f32]);
302 normals.push([
303 mesh_in.normals[i * 3] as f32,
304 mesh_in.normals[i * 3 + 1] as f32,
305 mesh_in.normals[i * 3 + 2] as f32,
306 ]);
307 }
308
309 // Face ranges: the watertight mesh emits each face's triangles as one
310 // contiguous run of `face_ids`. Group runs; a face with no triangles gets
311 // an empty range.
312 let mut faces: Vec<FaceDisplay> = payload
313 .faces
314 .iter()
315 .map(|(topo_id, name)| FaceDisplay {
316 name: name.clone().unwrap_or_default(),
317 topo_id: *topo_id,
318 tri_start: 0,
319 tri_count: 0,
320 kind: FaceKind::Unknown,
321 })
322 .collect();
323 let mut run_start = 0u32;
324 let mut run_face: Option<u32> = None;
325 for (tri, &face_id) in mesh_in.face_ids.iter().enumerate() {
326 if run_face != Some(face_id) {
327 run_face = Some(face_id);
328 run_start = tri as u32;
329 }
330 if let Some(face) = faces.get_mut(face_id as usize) {
331 if face.tri_count == 0 {
332 face.tri_start = run_start;
333 }
334 face.tri_count += 1;
335 }
336 }
337
338 let edges = payload
339 .edges
340 .into_iter()
341 .map(|(topo_id, name, points)| EdgeDisplay {
342 name: name.unwrap_or_default(),
343 topo_id,
344 polyline: points
345 .iter()
346 .map(|p| [p.x as f32, p.y as f32, p.z as f32])
347 .collect(),
348 aux: false,
349 centerline: false,
350 })
351 .collect();
352
353 let vertices = payload
354 .vertices
355 .into_iter()
356 .map(|(topo_id, p)| VertexDisplay {
357 topo_id,
358 position: [p.x, p.y, p.z],
359 })
360 .collect();
361
362 SolidDisplay {
363 name: name.to_string(),
364 source_handle: 0, // set by the caller that knows the resident handle
365 visible: true,
366 color_override: None,
367 revision: next_revision(),
368 mesh: DisplayMesh {
369 positions,
370 normals,
371 indices: mesh_in.indices,
372 face_ids: mesh_in.face_ids,
373 },
374 faces,
375 edges,
376 vertices,
377 visibility: crate::visibility::EntityVisibility::default(),
378 bbox,
379 is_sketch: false, // set by the sketch-sheet synthesizer for a committed sketch
380 }
381}