BREP_render 0.4.0

BREP Rust rendering engine: kernel-fed scene store + wgpu renderer (headless artifact, desktop window, and wasm canvas shells).
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
//! Renderer-independent display objects keyed by kernel feature names.
//! Rendering, picking, and feature-reference display share this scene map.

use crate::camera::Aabb;
use std::collections::HashMap;

/// The kind of a display face (surface classification rides along when known —
/// typed replacement for the previous app's untyped `faceKind` tag).
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum FaceKind {
    Unknown,
}

/// One face of a solid: a contiguous triangle range of the solid mesh plus the
/// kernel face identity.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct FaceDisplay {
    /// Kernel face name (byte-exact pipeline name); empty when unnamed.
    pub name: String,
    /// Kernel topology face id.
    pub topo_id: u64,
    /// First triangle (not index) of this face in the mesh.
    pub tri_start: u32,
    /// Triangle count.
    pub tri_count: u32,
    pub kind: FaceKind,
    /// Per-FACE base colour, resolved from this face's `color` metadata
    /// attribute by [`RenderScene::apply_metadata_colors`]. Takes precedence
    /// over the owning solid's colour (and over `faceColorMode`), but selection
    /// / hover emphasis still wins over it. `None` = inherit the solid.
    #[serde(default)]
    pub color_override: Option<[f32; 3]>,
}

/// One display edge: a world-space polyline plus the kernel edge identity and
/// the typed flags the previous display layer kept per object.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct EdgeDisplay {
    /// Kernel edge name (`faceA|faceB[n]` convention); empty when unnamed.
    pub name: String,
    /// Kernel topology edge id.
    pub topo_id: u64,
    /// World-space polyline (chord-tolerance sampled, ≥ 2 points).
    pub polyline: Vec<[f32; 3]>,
    /// Auxiliary display edge (not a real BREP boundary).
    pub aux: bool,
    /// Centerline flag (hole/revolve axis display).
    pub centerline: bool,
}

/// One display vertex (kernel topology vertex).
#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize)]
pub struct VertexDisplay {
    pub topo_id: u64,
    pub position: [f64; 3],
}

/// The triangle mesh of one solid, ready for GPU upload (f32; the kernel's f64
/// buffers are narrowed exactly once, here).
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct DisplayMesh {
    /// Interleaved-ready parallel arrays: xyz per vertex.
    pub positions: Vec<[f32; 3]>,
    pub normals: Vec<[f32; 3]>,
    /// Triangle indices (3 per triangle).
    pub indices: Vec<u32>,
    /// Per-TRIANGLE face index into `SolidDisplay::faces`.
    pub face_ids: Vec<u32>,
}

/// A displayed solid: mesh + named faces/edges/vertices + visibility.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct SolidDisplay {
    /// Kernel solid name — the scene key.
    pub name: String,
    /// The resident kernel handle this display was tessellated from (0 = none,
    /// e.g. a synthesized sheet with no kernel solid). Handles are monotonic and
    /// never recycled, so this is a stable identity for the geometry: the R10
    /// display-reuse fast path keeps an existing display across a history rerun
    /// ONLY when the name's resident handle still equals this — a name re-bound to
    /// a DIFFERENT handle (a SUBTRACT result inherits its target's name; a
    /// roll-back replays that name's ORIGINAL producer) must re-tessellate.
    pub source_handle: u32,
    pub visible: bool,
    /// Optional per-solid base color override (R14 — user-set solid color),
    /// else the name-hashed stable color is used.
    pub color_override: Option<[f32; 3]>,
    /// Monotonic content revision (R10). Every freshly built display gets a
    /// unique value, so the renderer's GPU-buffer cache re-uploads on any
    /// rebuild — correct-by-default (equivalent to teardown-rebuild). The
    /// reused-buffer fast path (keep a revision stable across a `reused`
    /// pipeline result) is a follow-up optimization.
    pub revision: u64,
    pub mesh: DisplayMesh,
    pub faces: Vec<FaceDisplay>,
    pub edges: Vec<EdgeDisplay>,
    pub vertices: Vec<VertexDisplay>,
    /// Per-entity + group hide state (individual faces/edges/vertices, or a
    /// whole group). Default = everything visible; see [`crate::visibility`].
    /// Reused solids keep it across history reruns (this whole struct is cloned
    /// forward); a re-tessellated solid resets to all-visible.
    pub visibility: crate::visibility::EntityVisibility,
    /// World bbox over mesh positions (edges lie on the mesh by construction).
    pub bbox: Aabb,
    /// This display is a SYNTHESIZED committed-sketch SHEET (planar face + named
    /// boundary edges + corner vertices), not a kernel solid — it carries no
    /// resident handle (`source_handle == 0`). The marker lets the UI treat a
    /// sketch as a sketch: it is listed under "Sketches" (not among solids) yet is
    /// pickable / selectable / measurable like any scene solid.
    pub is_sketch: bool,
    /// This body is SHEET METAL — its resident handle carried a `SheetTree` when
    /// the display was built. Stamped by the pipeline from
    /// [`brep_kernel::is_sheet_metal_handle`] on the RUNNER thread (where the
    /// tree's thread-local is warm), so the UI thread can answer "is this a
    /// sheet-metal body?" straight off the scene — the gate for the sheet-metal
    /// edit features (SM Flange / Fillet / Chamfer). Immutable-correct: a tree is
    /// attached at solid creation and dropped exactly when the handle is freed,
    /// handles never recycle, and the display-reuse fast path clones this struct
    /// forward only while the handle is unchanged.
    pub is_sheet_metal: bool,
}

/// Source of monotonic [`SolidDisplay::revision`] values.
fn next_revision() -> u64 {
    use std::sync::atomic::{AtomicU64, Ordering};
    static COUNTER: AtomicU64 = AtomicU64::new(1);
    COUNTER.fetch_add(1, Ordering::Relaxed)
}

/// The scene: insertion-ordered solids + an exact name index (R8 — the
/// `getObjectByName` heuristic-scoring lookup is replaced by this map).
#[derive(Debug, Default)]
pub struct RenderScene {
    solids: Vec<SolidDisplay>,
    index: HashMap<String, usize>,
}

impl RenderScene {
    pub fn new() -> Self {
        Self::default()
    }

    /// Insert or replace a solid by name (replacement keeps insertion order —
    /// a boolean result reusing its target's name stays in place).
    pub fn insert_solid(&mut self, solid: SolidDisplay) {
        match self.index.get(&solid.name) {
            Some(&slot) => self.solids[slot] = solid,
            None => {
                self.index.insert(solid.name.clone(), self.solids.len());
                self.solids.push(solid);
            }
        }
    }

    /// Drop every solid (the scene rebuild path clears then repopulates).
    pub fn clear(&mut self) {
        self.solids.clear();
        self.index.clear();
    }

    /// Empty the scene, RETURNING every solid by value (the name index is cleared
    /// and the vec is `mem::take`-n out). The history-apply seam MOVES existing
    /// displays out this way to reinsert the reused ones without cloning their
    /// meshes — a scene-free [`crate::pipeline::SceneRunner`] delta is applied by
    /// draining then reinserting in snapshot order.
    pub fn drain(&mut self) -> Vec<SolidDisplay> {
        self.index.clear();
        std::mem::take(&mut self.solids)
    }

    /// Set (or clear) a solid's metadata color override, bumping its revision
    /// so the renderer re-derives the base style. Returns false if unknown.
    pub fn set_color_override(&mut self, name: &str, color: Option<[f32; 3]>) -> bool {
        let Some(slot) = self.index.get(name).copied() else {
            return false;
        };
        let solid = &mut self.solids[slot];
        if solid.color_override != color {
            solid.color_override = color;
            solid.revision = next_revision();
        }
        true
    }

    /// Re-derive every solid's and face's base colour from the name-keyed
    /// metadata store — the ONE seam through which the durable `color`
    /// attribute reaches the display.
    ///
    /// `lookup` maps an object NAME (a solid's or a face's) to its resolved
    /// colour. It returns `None` both for "no colour recorded" and for "the
    /// display setting is overriding model colours", so this method needs to
    /// know about neither.
    ///
    /// SKETCH sheets are skipped: their `color_override` is the synthesized
    /// [`crate::engine_state::SKETCH_SHEET_COLOR`], not a metadata colour, and
    /// re-deriving it from a store that has no record for the sheet would blank
    /// it back to the global face colour.
    ///
    /// A changed solid's `revision` is bumped so the renderer re-uploads it —
    /// and ONLY when something actually changed. That no-op guarantee is
    /// load-bearing, not a nicety: this runs after EVERY history apply, and the
    /// R10 GPU-buffer reuse fast path keys off a stable revision.
    pub fn apply_metadata_colors(&mut self, lookup: impl Fn(&str) -> Option<[f32; 3]>) -> bool {
        let mut any = false;
        for solid in &mut self.solids {
            if solid.is_sketch {
                continue;
            }
            let mut changed = false;
            let want = lookup(&solid.name);
            if solid.color_override != want {
                solid.color_override = want;
                changed = true;
            }
            for face in &mut solid.faces {
                // An unnamed face can carry no metadata record, so it always
                // inherits the solid rather than costing a store lookup.
                let want = if face.name.is_empty() {
                    None
                } else {
                    lookup(&face.name)
                };
                if face.color_override != want {
                    face.color_override = want;
                    changed = true;
                }
            }
            if changed {
                solid.revision = next_revision();
                any = true;
            }
        }
        any
    }

    /// Set a solid's visibility (R11). Returns false if unknown.
    pub fn set_visible(&mut self, name: &str, visible: bool) -> bool {
        match self.solid_mut(name) {
            Some(solid) => {
                solid.visible = visible;
                true
            }
            None => false,
        }
    }

    /// Scene enumeration for the host scene-tree panel (R11): names, kind,
    /// visibility, child face/edge/vertex counts — as JSON.
    pub fn listing_json(&self) -> String {
        let solids: Vec<serde_json::Value> = self
            .solids
            .iter()
            .map(|solid| {
                serde_json::json!({
                    "name": solid.name,
                    "kind": "SOLID",
                    "visible": solid.visible,
                    "faces": solid.faces.len(),
                    "edges": solid.edges.len(),
                    "vertices": solid.vertices.len(),
                })
            })
            .collect();
        serde_json::Value::Array(solids).to_string()
    }

    /// Remove a solid by exact name.
    pub fn remove_solid(&mut self, name: &str) -> bool {
        let Some(slot) = self.index.remove(name) else {
            return false;
        };
        self.solids.remove(slot);
        for value in self.index.values_mut() {
            if *value > slot {
                *value -= 1;
            }
        }
        true
    }

    pub fn solid(&self, name: &str) -> Option<&SolidDisplay> {
        self.index.get(name).map(|&slot| &self.solids[slot])
    }

    pub fn solid_mut(&mut self, name: &str) -> Option<&mut SolidDisplay> {
        let slot = *self.index.get(name)?;
        Some(&mut self.solids[slot])
    }

    /// Insertion-ordered iteration (deterministic — drives draw order).
    pub fn solids(&self) -> &[SolidDisplay] {
        &self.solids
    }

    /// The world-space polyline of the first display edge named `name` across all
    /// solids (widened to `f64`), or `None` when no edge carries that exact name.
    /// The engine-native sketch pickEdges tool (S6b-2) uses this to fetch a picked
    /// scene edge's geometry for projection into the sketch plane. There is no name
    /// index for edges (only solids), so this is a linear scan — fine for the
    /// interactive per-click use.
    pub fn edge_polyline_world(&self, name: &str) -> Option<Vec<[f64; 3]>> {
        for solid in &self.solids {
            for edge in &solid.edges {
                if edge.name == name {
                    return Some(
                        edge.polyline
                            .iter()
                            .map(|p| [p[0] as f64, p[1] as f64, p[2] as f64])
                            .collect(),
                    );
                }
            }
        }
        None
    }

    /// The name of the solid owning the first display edge named `name`, or `None`
    /// (the companion of [`edge_polyline_world`](Self::edge_polyline_world) — the
    /// pickEdges tool stores it as external-ref metadata).
    pub fn edge_solid_name(&self, name: &str) -> Option<&str> {
        for solid in &self.solids {
            if solid.edges.iter().any(|edge| edge.name == name) {
                return Some(&solid.name);
            }
        }
        None
    }

    /// The world plane of the first display face named `name` across all solids,
    /// as `(centroid, unit outward normal)`: the area-weighted centroid of the
    /// face's mesh triangles and the normalized sum of their cross products. The
    /// watertight tessellation winds every face's triangles by `same_sense`, so
    /// the cross-product sum IS the outward normal (the stored per-vertex normals
    /// are shading normals and can be blended at shared boundary vertices). This
    /// is what lets an extrude/revolve whose `profile` is a resident solid FACE
    /// (not a sketch) anchor its dimension gizmo — the engine's dimension refs
    /// fall back to it (`EngineState::lookup_profile_plane`). Hidden solids count
    /// too: a hidden source solid still anchors the gizmo. `None` when no face
    /// carries that exact name, it has no triangles, or the triangles are
    /// degenerate (zero area). Linear scan like [`edge_polyline_world`](Self::edge_polyline_world).
    pub fn face_plane_world(&self, name: &str) -> Option<([f64; 3], [f64; 3])> {
        for solid in &self.solids {
            let Some(face) = solid.faces.iter().find(|face| face.name == name) else {
                continue;
            };
            let positions = &solid.mesh.positions;
            let indices = &solid.mesh.indices;
            let start = face.tri_start as usize;
            let end = (start + face.tri_count as usize).min(indices.len() / 3);
            let mut weighted = [0.0f64; 3];
            let mut normal = [0.0f64; 3];
            let mut total_area = 0.0f64;
            for tri in start..end {
                let a = f64_point(positions[indices[tri * 3] as usize]);
                let b = f64_point(positions[indices[tri * 3 + 1] as usize]);
                let c = f64_point(positions[indices[tri * 3 + 2] as usize]);
                let ab = [b[0] - a[0], b[1] - a[1], b[2] - a[2]];
                let ac = [c[0] - a[0], c[1] - a[1], c[2] - a[2]];
                // Twice the signed-area vector; its length is 2·area.
                let cross = [
                    ab[1] * ac[2] - ab[2] * ac[1],
                    ab[2] * ac[0] - ab[0] * ac[2],
                    ab[0] * ac[1] - ab[1] * ac[0],
                ];
                let area =
                    0.5 * (cross[0] * cross[0] + cross[1] * cross[1] + cross[2] * cross[2]).sqrt();
                for k in 0..3 {
                    weighted[k] += area * (a[k] + b[k] + c[k]) / 3.0;
                    normal[k] += cross[k];
                }
                total_area += area;
            }
            let normal_len =
                (normal[0] * normal[0] + normal[1] * normal[1] + normal[2] * normal[2]).sqrt();
            if total_area <= 1e-18 || normal_len <= 1e-18 {
                return None;
            }
            return Some((
                [
                    weighted[0] / total_area,
                    weighted[1] / total_area,
                    weighted[2] / total_area,
                ],
                [normal[0] / normal_len, normal[1] / normal_len, normal[2] / normal_len],
            ));
        }
        None
    }

    pub fn is_empty(&self) -> bool {
        self.solids.is_empty()
    }

    /// World bbox over every VISIBLE solid.
    pub fn bbox(&self) -> Aabb {
        let mut bbox = Aabb::empty();
        for solid in &self.solids {
            if solid.visible {
                bbox.union(&solid.bbox);
            }
        }
        bbox
    }
}

/// Widen a display-mesh vertex to `f64` (the mesh stores `f32`).
fn f64_point(p: [f32; 3]) -> [f64; 3] {
    [p[0] as f64, p[1] as f64, p[2] as f64]
}

/// Build a [`SolidDisplay`] from the kernel's native display payload.
pub fn solid_display_from_payload(
    name: &str,
    payload: brep_kernel::DisplaySolidPayload,
) -> SolidDisplay {
    let mesh_in = payload.mesh;
    let vertex_count = mesh_in.positions.len() / 3;
    let mut positions = Vec::with_capacity(vertex_count);
    let mut normals = Vec::with_capacity(vertex_count);
    let mut bbox = Aabb::empty();
    for i in 0..vertex_count {
        let p = [
            mesh_in.positions[i * 3],
            mesh_in.positions[i * 3 + 1],
            mesh_in.positions[i * 3 + 2],
        ];
        bbox.expand(p);
        positions.push([p[0] as f32, p[1] as f32, p[2] as f32]);
        normals.push([
            mesh_in.normals[i * 3] as f32,
            mesh_in.normals[i * 3 + 1] as f32,
            mesh_in.normals[i * 3 + 2] as f32,
        ]);
    }

    // Face ranges: the watertight mesh emits each face's triangles as one
    // contiguous run of `face_ids`. Group runs; a face with no triangles gets
    // an empty range.
    let mut faces: Vec<FaceDisplay> = payload
        .faces
        .iter()
        .map(|(topo_id, name)| FaceDisplay {
            name: name.clone().unwrap_or_default(),
            topo_id: *topo_id,
            tri_start: 0,
            tri_count: 0,
            kind: FaceKind::Unknown,
            color_override: None,
        })
        .collect();
    let mut run_start = 0u32;
    let mut run_face: Option<u32> = None;
    for (tri, &face_id) in mesh_in.face_ids.iter().enumerate() {
        if run_face != Some(face_id) {
            run_face = Some(face_id);
            run_start = tri as u32;
        }
        if let Some(face) = faces.get_mut(face_id as usize) {
            if face.tri_count == 0 {
                face.tri_start = run_start;
            }
            face.tri_count += 1;
        }
    }

    // Edges and vertices expand the bbox too. For a real solid they lie ON the
    // meshed boundary, so this changes nothing; for a synthesized OPEN-sketch
    // display (edges + endpoints, no face) they are the ONLY extent there is, and
    // an empty bbox would leave the sketch out of zoom-to-fit and out of every
    // bbox-gated traversal.
    for (_, _, points) in &payload.edges {
        for point in points {
            bbox.expand([point.x, point.y, point.z]);
        }
    }
    for (_, point) in &payload.vertices {
        bbox.expand([point.x, point.y, point.z]);
    }

    let edges = payload
        .edges
        .into_iter()
        .map(|(topo_id, name, points)| EdgeDisplay {
            name: name.unwrap_or_default(),
            topo_id,
            polyline: points
                .iter()
                .map(|p| [p.x as f32, p.y as f32, p.z as f32])
                .collect(),
            aux: false,
            centerline: false,
        })
        .collect();

    let vertices = payload
        .vertices
        .into_iter()
        .map(|(topo_id, p)| VertexDisplay {
            topo_id,
            position: [p.x, p.y, p.z],
        })
        .collect();

    SolidDisplay {
        name: name.to_string(),
        source_handle: 0, // set by the caller that knows the resident handle
        visible: true,
        color_override: None,
        revision: next_revision(),
        mesh: DisplayMesh {
            positions,
            normals,
            indices: mesh_in.indices,
            face_ids: mesh_in.face_ids,
        },
        faces,
        edges,
        vertices,
        visibility: crate::visibility::EntityVisibility::default(),
        bbox,
        is_sketch: false, // set by the sketch-sheet synthesizer for a committed sketch
        is_sheet_metal: false, // stamped by the pipeline from the resident handle's SheetTree
    }
}