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
use super::*;
/// The committed-sketch SHEET base color (dim cyan, matching the retired overlay
/// `COMMITTED_COLOR` 0x67c7d4) — a sketch sheet reads distinctly from a real solid.
const SKETCH_SHEET_COLOR: [f32; 3] = [
0x67 as f32 / 255.0,
0xc7 as f32 / 255.0,
0xd4 as f32 / 255.0,
];
impl EngineState {
/// The committed sketches present at the CURRENT rollback: every `"S"` feature at
/// index `0..=rollback`, EXCEPT the one being edited (it displays via the live
/// editing overlay). Hidden ids are still returned (the Scene tree lists them);
/// visibility is filtered by the caller against [`hidden_sketches`].
fn committed_sketch_ids(&self) -> Vec<String> {
let editing = self.sketch_edit.as_ref().map(|edit| edit.feature_id.as_str());
let rollback = self.history.rollback();
let mut ids = Vec::new();
for index in 0..=rollback {
if self.history.feature_type(index).as_deref() != Some("S") {
continue;
}
let Some(id) = self.history.feature_id(index) else {
continue;
};
if Some(id.as_str()) == editing {
continue;
}
ids.push(id);
}
ids
}
/// (Re)build the persistent committed-sketch SHEET SOLIDS. For every committed
/// sketch that should show — [`committed_sketch_ids`](Self::committed_sketch_ids)
/// minus [`hidden_sketches`] — synthesize its planar sheet from the run's solved
/// profile and insert it as a scene solid (keyed by the sketch id, dim-cyan,
/// flagged `is_sketch`); remove any sheet inserted on the PREVIOUS refresh but
/// not this one (rolled back, deleted, hidden, or became the active edit). A
/// sketch with no closed profile (open / underconstrained) yields no sheet and
/// is skipped. Marks dirty.
pub fn refresh_committed_sketches(&mut self) {
let visible_ids: Vec<String> = self
.committed_sketch_ids()
.into_iter()
.filter(|id| !self.hidden_sketches.contains(id))
.collect();
// Phase 1 (immutable borrow of the surfaced profiles): build a display
// payload per visible sketch that HAS a closed profile. A sketch with no
// sheet (no face + no edges) is skipped.
let payloads: Vec<(String, brep_kernel::DisplaySolidPayload)> = visible_ids
.iter()
.filter_map(|id| {
let profile = self
.sketch_profiles
.iter()
.find(|(name, _)| name == id)
.map(|(_, profile)| profile)?;
let payload = brep_kernel::sketch_profile_display_payload(profile);
if payload.mesh.indices.is_empty() && payload.edges.is_empty() {
return None;
}
Some((id.clone(), payload))
})
.collect();
// Phase 2 (mutable borrow of the scene): insert each sheet as a scene solid.
let mut fed: Vec<String> = Vec::with_capacity(payloads.len());
for (id, payload) in payloads {
let mut solid = crate::scene::solid_display_from_payload(&id, payload);
solid.is_sketch = true;
solid.color_override = Some(SKETCH_SHEET_COLOR);
self.scene.insert_solid(solid);
fed.push(id);
}
// Remove any sketch sheet inserted last time but not now.
let fed_set: std::collections::HashSet<&str> = fed.iter().map(String::as_str).collect();
let stale: Vec<String> = self
.shown_sketch_ids
.iter()
.filter(|id| !fed_set.contains(id.as_str()))
.cloned()
.collect();
for id in stale {
self.scene.remove_solid(&id);
}
self.shown_sketch_ids = fed;
self.dirty = true;
}
/// Whether the committed sketch `id`'s persistent overlay is shown (absent from
/// [`hidden_sketches`] = visible).
pub fn sketch_visible(&self, id: &str) -> bool {
!self.hidden_sketches.contains(id)
}
/// Show/hide the committed sketch `id`'s persistent overlay (the Scene-tree
/// checkbox). Toggles [`hidden_sketches`], rebuilds the committed overlays (so the
/// group is fed or cleared immediately), and marks dirty.
pub fn set_sketch_visible(&mut self, id: &str, visible: bool) {
if visible {
self.hidden_sketches.remove(id);
} else {
self.hidden_sketches.insert(id.to_string());
}
self.refresh_committed_sketches();
}
/// The committed sketches to list in the Scene tree: every `"S"` feature at the
/// current rollback (minus the active edit), each with its live visibility — the
/// ordered `(id, visible)` list the Scene panel snapshots.
pub fn committed_sketches(&self) -> Vec<(String, bool)> {
self.committed_sketch_ids()
.into_iter()
.map(|id| {
let visible = !self.hidden_sketches.contains(&id);
(id, visible)
})
.collect()
}
/// The committed sketches as JSON (`[{"name":<id>,"visible":<bool>}]`) — the
/// sibling of [`scene_entities_json`](Self::scene_entities_json) the Scene panel
/// publishes for the headed verifier (kept a SEPARATE method so the solids array's
/// shape is unchanged).
pub fn sketch_entities_json(&self) -> String {
let list: Vec<serde_json::Value> = self
.committed_sketches()
.into_iter()
.map(|(id, visible)| serde_json::json!({ "name": id, "visible": visible }))
.collect();
serde_json::Value::Array(list).to_string()
}
}
// ===========================================================================
// Construction datums — DATUM + PLANE feature frames rendered as first-class,
// SELECTABLE scene citizens (the committed-sketch parallel). The frames the last
// history run resolved ride `construction_frames`; this block filters them to the
// D/P producing features (a frame name → its feature TYPE via the history), feeds
// the kept ones to the datum-plane widget channel (`set_datums`, which REPLACES
// its set wholesale each call — so re-feeding the full current set auto-drops
// departed/hidden/rolled-back planes), lists them in the Scene tree, and selects
// one BY NAME (`emphasis.selected_datums`, re-colored with the accent).
// `hidden_datums` drives per-plane visibility. Kept in ONE appended block so
// concurrent edits to the primary impl land clean.
// ===========================================================================
// Committed-sketch persistent-display + Scene-tree-listing tests — their OWN module
// (appended last) so they do not conflict with the modules above.
#[cfg(test)]
mod committed_sketch_tests {
use super::*;
/// A two-feature history: a cube (solid) followed by a committed closed-rectangle
/// sketch on the XY plane (the sketch is the LAST feature).
fn cube_and_sketch_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": "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()
}
fn has_group(engine: &EngineState, name: &str) -> bool {
engine.widgets.overlay_group_names().contains(&name)
}
/// The synthesized committed-sketch SHEET solid for `id`, if present.
fn sketch_sheet<'a>(
engine: &'a EngineState,
id: &str,
) -> Option<&'a crate::scene::SolidDisplay> {
engine.scene.solid(id).filter(|solid| solid.is_sketch)
}
fn has_sketch_sheet(engine: &EngineState, id: &str) -> bool {
sketch_sheet(engine, id).is_some()
}
#[test]
fn committed_sketch_renders_and_lists_after_rerun() {
let mut engine = EngineState::new();
engine.set_history_json(&cube_and_sketch_history()).unwrap();
// The committed sketch is now a SHEET SOLID keyed by its id: a planar face,
// its four named boundary edges, and four corner vertices — pickable via the
// solid infrastructure.
let sheet = sketch_sheet(&engine, "Sk").expect("committed sketch sheet solid");
assert!(sheet.is_sketch, "flagged as a sketch");
assert_eq!(sheet.faces.len(), 1, "one sheet face");
assert_eq!(sheet.edges.len(), 4, "four boundary edges");
assert_eq!(sheet.vertices.len(), 4, "four corner vertices");
assert!(!sheet.mesh.positions.is_empty(), "filled face mesh");
// The sketch sheet is NOT in the solids listing (it lists under "Sketches").
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");
// The committed sketch is listed by the sibling methods.
assert_eq!(engine.committed_sketches(), vec![("Sk".to_string(), true)]);
let sketches: serde_json::Value =
serde_json::from_str(&engine.sketch_entities_json()).unwrap();
assert_eq!(sketches[0]["name"], "Sk");
assert_eq!(sketches[0]["visible"], true);
assert!(engine.sketch_visible("Sk"));
}
#[test]
fn rolling_back_before_sketch_clears_committed_sheet() {
let mut engine = EngineState::new();
engine.set_history_json(&cube_and_sketch_history()).unwrap();
assert!(has_sketch_sheet(&engine, "Sk"));
// Roll to the cube (index 0), before the sketch: its sheet is removed and it
// drops out of the listing.
engine.roll_to(0);
assert!(!has_sketch_sheet(&engine, "Sk"), "rolled-back sketch sheet removed");
assert!(engine.committed_sketches().is_empty());
// Rolling forward again re-adds it.
engine.roll_to(1);
assert!(has_sketch_sheet(&engine, "Sk"), "rolled-forward sketch re-shown");
assert_eq!(engine.committed_sketches(), vec![("Sk".to_string(), true)]);
}
#[test]
fn set_sketch_visible_hides_and_restores() {
let mut engine = EngineState::new();
engine.set_history_json(&cube_and_sketch_history()).unwrap();
assert!(has_sketch_sheet(&engine, "Sk"));
// Hide: sheet removed from the scene, still LISTED but visible:false.
engine.set_sketch_visible("Sk", false);
assert!(!has_sketch_sheet(&engine, "Sk"), "hidden sketch sheet removed");
assert!(!engine.sketch_visible("Sk"));
assert_eq!(engine.committed_sketches(), vec![("Sk".to_string(), false)]);
let sketches: serde_json::Value =
serde_json::from_str(&engine.sketch_entities_json()).unwrap();
assert_eq!(sketches[0]["visible"], false);
// Show: sheet re-inserted.
engine.set_sketch_visible("Sk", true);
assert!(has_sketch_sheet(&engine, "Sk"), "re-shown sketch sheet inserted");
assert!(engine.sketch_visible("Sk"));
}
#[test]
fn entering_sketch_mode_removes_committed_sheet_then_commit_readds() {
let mut engine = EngineState::new();
engine.set_history_json(&cube_and_sketch_history()).unwrap();
assert!(has_sketch_sheet(&engine, "Sk"));
// Enter: the committed sheet is removed (no double display); the LIVE editing
// overlay group is fed instead (unchanged active-edit path).
engine.enter_sketch_mode("Sk").expect("enter");
assert!(engine.sketch_mode());
assert!(
!has_sketch_sheet(&engine, "Sk"),
"active sketch's committed sheet removed"
);
assert!(has_group(&engine, "sketch-geometry"), "live editing overlay fed");
// While editing, the sketch is not in the committed listing.
assert!(engine.committed_sketches().is_empty());
// Commit exit: the just-committed sketch reappears as a sheet solid and the
// live editing group is cleared.
engine.exit_sketch_mode(true);
assert!(!engine.sketch_mode());
assert!(
has_sketch_sheet(&engine, "Sk"),
"committed sketch reappears as a sheet after commit"
);
assert!(!has_group(&engine, "sketch-geometry"), "live editing overlay cleared");
assert_eq!(engine.committed_sketches(), vec![("Sk".to_string(), true)]);
}
/// A committed sketch sheet is MEASURABLE through the handle-less metadata path:
/// its Info reports `kind:"sketch"` with the profile area (10x6 = 60 mm²) and
/// total boundary length (perimeter 2*(10+6) = 32 mm), no volume, and its
/// creating feature is the sketch.
#[test]
fn sketch_sheet_info_reports_area_and_edge_length() {
let mut engine = EngineState::new();
engine.set_history_json(&cube_and_sketch_history()).unwrap();
let info: serde_json::Value =
serde_json::from_str(&engine.object_info_json("Sk")).unwrap();
assert_eq!(info["ok"], true, "sketch info must not error: {info}");
assert_eq!(info["kind"], "sketch");
assert!(info.get("volume").is_none(), "a sheet has no volume");
assert!((info["area"].as_f64().unwrap() - 60.0).abs() < 1e-3, "area {}", info["area"]);
assert!(
(info["edgeLengthTotal"].as_f64().unwrap() - 32.0).abs() < 1e-3,
"perimeter {}",
info["edgeLengthTotal"]
);
assert_eq!(info["creatingFeature"]["id"], "Sk");
assert_eq!(info["creatingFeature"]["type"], "S");
}
/// A boundary edge of the sheet is a normal selectable/measurable edge: its Info
/// reports `kind:"edge"` and the segment length (a 10 or 6 mm rectangle side).
#[test]
fn sketch_sheet_edge_is_measurable() {
let mut engine = EngineState::new();
engine.set_history_json(&cube_and_sketch_history()).unwrap();
let edge_name = sketch_sheet(&engine, "Sk").unwrap().edges[0].name.clone();
assert!(!edge_name.is_empty(), "boundary edge is named");
let info: serde_json::Value =
serde_json::from_str(&engine.object_info_json(&edge_name)).unwrap();
assert_eq!(info["ok"], true, "edge info: {info}");
assert_eq!(info["kind"], "edge");
assert_eq!(info["solid"], "Sk");
let length = info["length"].as_f64().unwrap();
assert!(
(length - 10.0).abs() < 1e-3 || (length - 6.0).abs() < 1e-3,
"rectangle side length {length}"
);
}
/// The sheet is selectable BY NAME through the shared solid selection (the
/// viewport pick and the Scene tree both route here), so a sketch highlights
/// like any solid.
#[test]
fn sketch_sheet_is_selectable_by_name() {
let mut engine = EngineState::new();
engine.set_history_json(&cube_and_sketch_history()).unwrap();
assert!(engine.select_by_name("solid", "Sk"));
let sel: serde_json::Value = serde_json::from_str(&engine.selection_json()).unwrap();
assert_eq!(sel["solids"][0], "Sk");
}
}