BREP_render 0.1.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
use super::*;

/// The calm base color of an unselected construction datum/plane (a soft blue).
const DATUM_PLANE_COLOR: &str = "#6b8fd0";
/// The selection accent for a selected datum/plane (matches `faceSelectedColor`).
const DATUM_PLANE_SELECTED_COLOR: &str = "#ffc400";

impl EngineState {
    /// Map every feature id at the CURRENT rollback (`0..=rollback`) to its TYPE
    /// token — the lookup that classifies a frame name's producing feature so only
    /// DATUM (`"D"`) / PLANE (`"P"`) frames display as datum planes (a SKETCH `"S"`
    /// frame renders as curves, not a datum).
    fn feature_type_map(&self) -> HashMap<String, String> {
        let rollback = self.history.rollback();
        let mut map = HashMap::new();
        for index in 0..=rollback {
            if let (Some(id), Some(ty)) =
                (self.history.feature_id(index), self.history.feature_type(index))
            {
                map.insert(id, ty);
            }
        }
        map
    }

    /// Classify a plane-frame NAME against the feature type map: strip a trailing
    /// DATUM sub-plane suffix (`:XY`/`:XZ`/`:YZ`) to the producing feature id, look
    /// up its type, and keep only `"D"`/`"P"` producers. Returns `(producing
    /// feature id, feature type)` for a datum/plane frame, else `None` (a SKETCH
    /// `"S"` frame, or a feature past the rollback / not in the history).
    fn datum_feature_of(
        name: &str,
        type_map: &HashMap<String, String>,
    ) -> Option<(String, String)> {
        let base = [":XY", ":XZ", ":YZ"]
            .iter()
            .find_map(|suffix| name.strip_suffix(suffix))
            .unwrap_or(name);
        let ty = type_map.get(base)?;
        if ty == "D" || ty == "P" {
            Some((base.to_string(), ty.clone()))
        } else {
            None
        }
    }

    /// The construction datum/plane frame NAMES the last run resolved, filtered to
    /// the D/P producing features at the current rollback, in run order. Every
    /// DATUM contributes three (`{id}:XY|XZ|YZ`), every PLANE one (`{id}`); a
    /// SKETCH's own plane frame is excluded (it renders as curves).
    fn construction_datum_names(&self) -> Vec<String> {
        let type_map = self.feature_type_map();
        self.construction_frames
            .iter()
            .filter(|(name, _)| Self::datum_feature_of(name, &type_map).is_some())
            .map(|(name, _)| name.clone())
            .collect()
    }

    /// The producing `(feature id, feature type)` of a datum/plane frame NAME, but
    /// ONLY when the name is an actually-resolved D/P frame at the current rollback
    /// — the provenance the Properties Info tab reports for a selected datum.
    pub fn datum_feature_for_name(&self, name: &str) -> Option<(String, String)> {
        if !self.construction_frames.iter().any(|(n, _)| n == name) {
            return None;
        }
        Self::datum_feature_of(name, &self.feature_type_map())
    }

    /// (Re)build the persistent construction datum/plane overlays. Feeds every D/P
    /// frame the last run resolved (minus [`hidden_datums`]) to the datum-plane
    /// widget channel as a screen-constant NAMED plane in the calm datum color — or
    /// the selection accent when it is in `emphasis.selected_datums`. The feed
    /// REPLACES the widget's datum set wholesale, so a departed/hidden/rolled-back
    /// plane is auto-dropped; `shown_datum_names` mirrors what was fed. Marks dirty.
    pub fn refresh_construction_datums(&mut self) {
        let type_map = self.feature_type_map();
        let mut planes: Vec<serde_json::Value> = Vec::new();
        let mut fed: Vec<String> = Vec::new();
        for (name, frame) in &self.construction_frames {
            if Self::datum_feature_of(name, &type_map).is_none() {
                continue;
            }
            if self.hidden_datums.contains(name) {
                continue;
            }
            let selected = self.emphasis.selected_datums.contains(name);
            let color = if selected {
                DATUM_PLANE_SELECTED_COLOR
            } else {
                DATUM_PLANE_COLOR
            };
            planes.push(serde_json::json!({
                "name": name,
                "origin": [frame.origin.x, frame.origin.y, frame.origin.z],
                "x": [frame.x_axis.x, frame.x_axis.y, frame.x_axis.z],
                "y": [frame.y_axis.x, frame.y_axis.y, frame.y_axis.z],
                "color": color,
                "selected": selected,
            }));
            fed.push(name.clone());
        }
        // `set_datums` replaces its whole datum set, so a full re-feed each call
        // drops any plane no longer present (rolled back / deleted / hidden).
        let payload = serde_json::json!({ "planes": planes }).to_string();
        let _ = self.set_datums_json(&payload);
        self.shown_datum_names = fed;
        self.dirty = true;
    }

    /// Whether the construction datum/plane `name`'s plane is shown (absent from
    /// [`hidden_datums`] = visible).
    pub fn datum_visible(&self, name: &str) -> bool {
        !self.hidden_datums.contains(name)
    }

    /// Show/hide the construction datum/plane `name`'s plane (the Scene-tree
    /// checkbox). Toggles [`hidden_datums`] and re-feeds the datum planes so the
    /// plane appears/disappears immediately.
    pub fn set_datum_visible(&mut self, name: &str, visible: bool) {
        if visible {
            self.hidden_datums.remove(name);
        } else {
            self.hidden_datums.insert(name.to_string());
        }
        self.refresh_construction_datums();
    }

    /// The construction datums/planes to list in the Scene tree: every D/P frame at
    /// the current rollback, each with its live visibility (hidden ones included,
    /// like [`committed_sketches`](Self::committed_sketches)).
    pub fn construction_datums(&self) -> Vec<(String, bool)> {
        self.construction_datum_names()
            .into_iter()
            .map(|name| {
                let visible = !self.hidden_datums.contains(&name);
                (name, visible)
            })
            .collect()
    }

    /// The construction datums/planes as JSON (`[{"name","visible"}]`) — the datum
    /// sibling of [`sketch_entities_json`](Self::sketch_entities_json) the Scene
    /// panel publishes (`__brepDatums`) for the headed verifier.
    pub fn datum_entities_json(&self) -> String {
        let list: Vec<serde_json::Value> = self
            .construction_datums()
            .into_iter()
            .map(|(name, visible)| serde_json::json!({ "name": name, "visible": visible }))
            .collect();
        serde_json::Value::Array(list).to_string()
    }

    /// Select a construction datum/plane by frame NAME (replacing the whole
    /// selection): a Scene-tree row click or a viewport datum pick. Only a name
    /// that is an actually-resolved D/P frame at the current rollback selects;
    /// others return false without changing the selection. Re-feeds the datum
    /// planes so the selected one shows the accent, and bumps the generation.
    pub fn select_datum(&mut self, name: &str) -> bool {
        if name.is_empty() || !self.construction_frames.iter().any(|(n, _)| n == name) {
            return false;
        }
        if Self::datum_feature_of(name, &self.feature_type_map()).is_none() {
            return false;
        }
        self.emphasis.selected_solids.clear();
        self.emphasis.selected_faces.clear();
        self.emphasis.selected_edges.clear();
        self.emphasis.selected_vertices.clear();
        self.emphasis.selected_datums.clear();
        self.emphasis.selected_datums.insert(name.to_string());
        self.emphasis.generation = self.emphasis.generation.wrapping_add(1);
        self.refresh_construction_datums();
        self.dirty = true;
        true
    }
}


// Construction datum/plane persistent-display + Scene-tree-listing + selection
// tests — their OWN module (appended last) so they do not conflict with the
// modules above.
#[cfg(test)]
mod construction_datum_tests {
    use super::*;

    /// A cube (index 0) followed by a DATUM feature `Datum` (index 1). The datum
    /// registers three base-plane frames `Datum:XY|XZ|YZ`.
    fn cube_and_datum_history() -> String {
        serde_json::json!({
            "features": [
                {
                    "type": "P.CU",
                    "inputParams": {
                        "id": "Box",
                        "sizeX": 10.0, "sizeY": 10.0, "sizeZ": 10.0,
                        "transform": {
                            "position": [0.0, 0.0, 0.0],
                            "rotationEuler": [0.0, 0.0, 0.0],
                            "scale": [1.0, 1.0, 1.0]
                        },
                        "boolean": { "targets": [], "operation": "NONE" }
                    },
                    "persistentData": {}
                },
                {
                    "type": "D",
                    "inputParams": { "id": "Datum" },
                    "persistentData": {}
                }
            ]
        })
        .to_string()
    }

    /// A PLANE feature `Pl` (XZ orientation, offset 3) — registers ONE frame `Pl`.
    fn plane_history() -> String {
        serde_json::json!({
            "features": [{
                "type": "P",
                "inputParams": { "id": "Pl", "orientation": "XZ", "offset_distance": 3.0 },
                "persistentData": {}
            }]
        })
        .to_string()
    }

    /// A DATUM `Datum` (index 0) followed by a committed rectangle SKETCH `Sk`
    /// (index 1) on the XY plane. The sketch publishes its OWN plane frame `Sk`,
    /// which must NOT surface as a datum plane (it renders as curves).
    fn datum_and_sketch_history() -> String {
        serde_json::json!({
            "features": [
                {
                    "type": "D",
                    "inputParams": { "id": "Datum" },
                    "persistentData": {}
                },
                {
                    "type": "S",
                    "inputParams": { "id": "Sk" },
                    "persistentData": {
                        "basis": { "origin": [0, 0, 0], "x": [1, 0, 0], "y": [0, 1, 0], "z": [0, 0, 1] },
                        "sketch": {
                            "points": [
                                { "id": 0, "x": 0.0,  "y": 0.0 },
                                { "id": 1, "x": 10.0, "y": 0.0 },
                                { "id": 2, "x": 10.0, "y": 6.0 },
                                { "id": 3, "x": 0.0,  "y": 6.0 }
                            ],
                            "geometries": [
                                { "id": 10, "type": "line", "points": [0, 1] },
                                { "id": 11, "type": "line", "points": [1, 2] },
                                { "id": 12, "type": "line", "points": [2, 3] },
                                { "id": 13, "type": "line", "points": [3, 0] }
                            ],
                            "constraints": []
                        }
                    }
                }
            ]
        })
        .to_string()
    }

    /// The datum plane NAMES currently fed to the widget (sorted for stable
    /// comparison).
    fn fed_datum_names(engine: &EngineState) -> Vec<String> {
        let mut names: Vec<String> = engine
            .widgets
            .datum_plane_names()
            .iter()
            .map(|(n, _)| n.to_string())
            .collect();
        names.sort();
        names
    }

    #[test]
    fn datum_feature_feeds_three_named_planes_and_lists_them() {
        let mut engine = EngineState::new();
        engine.set_history_json(&cube_and_datum_history()).unwrap();

        // The widget received the three base-plane frames as named datum planes.
        assert_eq!(
            fed_datum_names(&engine),
            vec!["Datum:XY".to_string(), "Datum:XZ".to_string(), "Datum:YZ".to_string()]
        );

        // `construction_datums` lists all three (all visible), and the JSON sibling
        // publishes the same.
        let listed = engine.construction_datums();
        assert_eq!(listed.len(), 3);
        assert!(listed.iter().all(|(_, visible)| *visible));
        let json: serde_json::Value =
            serde_json::from_str(&engine.datum_entities_json()).unwrap();
        assert_eq!(json.as_array().unwrap().len(), 3);
        assert!(json.as_array().unwrap().iter().any(|d| d["name"] == "Datum:XY"));

        // The solid listing is unchanged (datums ride a separate sibling method).
        let solids: serde_json::Value =
            serde_json::from_str(&engine.scene_entities_json()).unwrap();
        assert_eq!(solids.as_array().unwrap().len(), 1);
        assert_eq!(solids[0]["name"], "Box");
    }

    #[test]
    fn plane_feature_feeds_and_lists_its_single_frame() {
        let mut engine = EngineState::new();
        engine.set_history_json(&plane_history()).unwrap();
        assert_eq!(fed_datum_names(&engine), vec!["Pl".to_string()]);
        assert_eq!(engine.construction_datums(), vec![("Pl".to_string(), true)]);
    }

    #[test]
    fn sketch_frame_is_not_fed_as_a_datum() {
        let mut engine = EngineState::new();
        engine.set_history_json(&datum_and_sketch_history()).unwrap();
        // Only the DATUM's three planes are datum planes; the sketch's own frame
        // `Sk` is excluded (it renders as curves, not a datum plane).
        assert_eq!(
            fed_datum_names(&engine),
            vec!["Datum:XY".to_string(), "Datum:XZ".to_string(), "Datum:YZ".to_string()]
        );
        assert!(!fed_datum_names(&engine).contains(&"Sk".to_string()));
        assert!(engine.construction_datums().iter().all(|(n, _)| n != "Sk"));
    }

    #[test]
    fn rollback_before_datum_clears_its_planes() {
        let mut engine = EngineState::new();
        engine.set_history_json(&cube_and_datum_history()).unwrap();
        assert_eq!(fed_datum_names(&engine).len(), 3);

        // Roll to the cube (index 0), before the datum: its planes are cleared and it
        // drops out of the listing.
        engine.roll_to(0);
        assert!(fed_datum_names(&engine).is_empty(), "rolled-back datum cleared");
        assert!(engine.construction_datums().is_empty());

        // Rolling forward again re-feeds the three planes.
        engine.roll_to(1);
        assert_eq!(fed_datum_names(&engine).len(), 3, "rolled-forward datum re-shown");
    }

    #[test]
    fn set_datum_visible_hides_and_restores_a_plane() {
        let mut engine = EngineState::new();
        engine.set_history_json(&cube_and_datum_history()).unwrap();

        // Hide one sub-plane: it is dropped from the feed but still LISTED (visible:false).
        engine.set_datum_visible("Datum:XZ", false);
        assert!(!engine.datum_visible("Datum:XZ"));
        assert!(!fed_datum_names(&engine).contains(&"Datum:XZ".to_string()));
        assert_eq!(fed_datum_names(&engine).len(), 2);
        assert!(engine
            .construction_datums()
            .contains(&("Datum:XZ".to_string(), false)));

        // Show it again: re-fed.
        engine.set_datum_visible("Datum:XZ", true);
        assert!(engine.datum_visible("Datum:XZ"));
        assert_eq!(fed_datum_names(&engine).len(), 3);
    }

    #[test]
    fn select_datum_sets_selection_and_emphasizes_it() {
        let mut engine = EngineState::new();
        engine.set_history_json(&cube_and_datum_history()).unwrap();

        assert!(engine.select_datum("Datum:XY"));
        let sel: serde_json::Value = serde_json::from_str(&engine.selection_json()).unwrap();
        assert_eq!(sel["datums"].as_array().unwrap().len(), 1);
        assert_eq!(sel["datums"][0], "Datum:XY");

        // The fed plane carries the selection emphasis (its `hot` flag is set).
        let hot: Vec<(&str, bool)> = engine.widgets.datum_plane_names();
        assert!(
            hot.iter().any(|(n, hot)| *n == "Datum:XY" && *hot),
            "selected datum is emphasized: {hot:?}"
        );

        // A bogus / non-D-P name does not select.
        assert!(!engine.select_datum("NotADatum"));

        // Clearing the selection drops the datum.
        assert!(engine.clear_selection());
        let sel: serde_json::Value = serde_json::from_str(&engine.selection_json()).unwrap();
        assert!(sel["datums"].as_array().unwrap().is_empty());
    }

    #[test]
    fn select_by_name_routes_datum_kind() {
        let mut engine = EngineState::new();
        engine.set_history_json(&cube_and_datum_history()).unwrap();
        assert!(engine.select_by_name("datum", "Datum:YZ"));
        let sel: serde_json::Value = serde_json::from_str(&engine.selection_json()).unwrap();
        assert_eq!(sel["datums"][0], "Datum:YZ");

        // Selecting a solid supersedes the datum (they share the one selection).
        assert!(engine.select_by_name("solid", "Box"));
        let sel: serde_json::Value = serde_json::from_str(&engine.selection_json()).unwrap();
        assert!(sel["datums"].as_array().unwrap().is_empty());
        assert_eq!(sel["solids"][0], "Box");
    }

    #[test]
    fn object_info_json_on_a_datum_returns_a_graceful_record() {
        let mut engine = EngineState::new();
        engine.set_history_json(&cube_and_datum_history()).unwrap();

        // A DATUM base plane: ok, kind "datum", provenance points at the D feature.
        let info: serde_json::Value =
            serde_json::from_str(&engine.object_info_json("Datum:XY")).unwrap();
        assert_eq!(info["ok"], true, "datum info must not error: {info}");
        assert_eq!(info["kind"], "datum");
        assert_eq!(info["creatingFeature"]["id"], "Datum");
        assert_eq!(info["creatingFeature"]["type"], "D");

        // An unrelated name still reports the unknown-object error.
        let unknown: serde_json::Value =
            serde_json::from_str(&engine.object_info_json("Nope")).unwrap();
        assert_eq!(unknown["ok"], false);
    }

    #[test]
    fn object_info_json_on_a_plane_reports_kind_plane() {
        let mut engine = EngineState::new();
        engine.set_history_json(&plane_history()).unwrap();
        let info: serde_json::Value =
            serde_json::from_str(&engine.object_info_json("Pl")).unwrap();
        assert_eq!(info["ok"], true, "plane info must not error: {info}");
        assert_eq!(info["kind"], "plane");
        assert_eq!(info["creatingFeature"]["type"], "P");
    }
}