brep_render/engine_state/
committed_sketches.rs1use super::*;
2
3const SKETCH_SHEET_COLOR: [f32; 3] = [
6 0x67 as f32 / 255.0,
7 0xc7 as f32 / 255.0,
8 0xd4 as f32 / 255.0,
9];
10
11impl EngineState {
12
13 fn committed_sketch_ids(&self) -> Vec<String> {
18 let editing = self.sketch_edit.as_ref().map(|edit| edit.feature_id.as_str());
19 let rollback = self.history.rollback();
20 let mut ids = Vec::new();
21 for index in 0..=rollback {
22 if self.history.feature_type(index).as_deref() != Some("S") {
23 continue;
24 }
25 let Some(id) = self.history.feature_id(index) else {
26 continue;
27 };
28 if Some(id.as_str()) == editing {
29 continue;
30 }
31 ids.push(id);
32 }
33 ids
34 }
35
36 pub fn refresh_committed_sketches(&mut self) {
45 let visible_ids: Vec<String> = self
46 .committed_sketch_ids()
47 .into_iter()
48 .filter(|id| !self.hidden_sketches.contains(id))
49 .collect();
50
51 let payloads: Vec<(String, brep_kernel::DisplaySolidPayload)> = visible_ids
55 .iter()
56 .filter_map(|id| {
57 let profile = self
58 .sketch_profiles
59 .iter()
60 .find(|(name, _)| name == id)
61 .map(|(_, profile)| profile)?;
62 let payload = brep_kernel::sketch_profile_display_payload(profile);
63 if payload.mesh.indices.is_empty() && payload.edges.is_empty() {
64 return None;
65 }
66 Some((id.clone(), payload))
67 })
68 .collect();
69
70 let mut fed: Vec<String> = Vec::with_capacity(payloads.len());
72 for (id, payload) in payloads {
73 let mut solid = crate::scene::solid_display_from_payload(&id, payload);
74 solid.is_sketch = true;
75 solid.color_override = Some(SKETCH_SHEET_COLOR);
76 self.scene.insert_solid(solid);
77 fed.push(id);
78 }
79
80 let fed_set: std::collections::HashSet<&str> = fed.iter().map(String::as_str).collect();
82 let stale: Vec<String> = self
83 .shown_sketch_ids
84 .iter()
85 .filter(|id| !fed_set.contains(id.as_str()))
86 .cloned()
87 .collect();
88 for id in stale {
89 self.scene.remove_solid(&id);
90 }
91
92 self.shown_sketch_ids = fed;
93 self.dirty = true;
94 }
95
96 pub fn sketch_visible(&self, id: &str) -> bool {
99 !self.hidden_sketches.contains(id)
100 }
101
102 pub fn set_sketch_visible(&mut self, id: &str, visible: bool) {
106 if visible {
107 self.hidden_sketches.remove(id);
108 } else {
109 self.hidden_sketches.insert(id.to_string());
110 }
111 self.refresh_committed_sketches();
112 }
113
114 pub fn committed_sketches(&self) -> Vec<(String, bool)> {
118 self.committed_sketch_ids()
119 .into_iter()
120 .map(|id| {
121 let visible = !self.hidden_sketches.contains(&id);
122 (id, visible)
123 })
124 .collect()
125 }
126
127 pub fn sketch_entities_json(&self) -> String {
132 let list: Vec<serde_json::Value> = self
133 .committed_sketches()
134 .into_iter()
135 .map(|(id, visible)| serde_json::json!({ "name": id, "visible": visible }))
136 .collect();
137 serde_json::Value::Array(list).to_string()
138 }
139}
140
141#[cfg(test)]
158mod committed_sketch_tests {
159 use super::*;
160
161 fn cube_and_sketch_history() -> String {
164 serde_json::json!({
165 "features": [
166 {
167 "type": "P.CU",
168 "inputParams": {
169 "id": "Box",
170 "sizeX": 10.0, "sizeY": 10.0, "sizeZ": 10.0,
171 "transform": {
172 "position": [0.0, 0.0, 0.0],
173 "rotationEuler": [0.0, 0.0, 0.0],
174 "scale": [1.0, 1.0, 1.0]
175 },
176 "boolean": { "targets": [], "operation": "NONE" }
177 },
178 "persistentData": {}
179 },
180 {
181 "type": "S",
182 "inputParams": { "id": "Sk" },
183 "persistentData": {
184 "basis": { "origin": [0, 0, 0], "x": [1, 0, 0], "y": [0, 1, 0], "z": [0, 0, 1] },
185 "sketch": {
186 "points": [
187 { "id": 0, "x": 0.0, "y": 0.0 },
188 { "id": 1, "x": 10.0, "y": 0.0 },
189 { "id": 2, "x": 10.0, "y": 6.0 },
190 { "id": 3, "x": 0.0, "y": 6.0 }
191 ],
192 "geometries": [
193 { "id": 10, "type": "line", "points": [0, 1] },
194 { "id": 11, "type": "line", "points": [1, 2] },
195 { "id": 12, "type": "line", "points": [2, 3] },
196 { "id": 13, "type": "line", "points": [3, 0] }
197 ],
198 "constraints": []
199 }
200 }
201 }
202 ]
203 })
204 .to_string()
205 }
206
207 fn has_group(engine: &EngineState, name: &str) -> bool {
208 engine.widgets.overlay_group_names().contains(&name)
209 }
210
211 fn sketch_sheet<'a>(
213 engine: &'a EngineState,
214 id: &str,
215 ) -> Option<&'a crate::scene::SolidDisplay> {
216 engine.scene.solid(id).filter(|solid| solid.is_sketch)
217 }
218
219 fn has_sketch_sheet(engine: &EngineState, id: &str) -> bool {
220 sketch_sheet(engine, id).is_some()
221 }
222
223 #[test]
224 fn committed_sketch_renders_and_lists_after_rerun() {
225 let mut engine = EngineState::new();
226 engine.set_history_json(&cube_and_sketch_history()).unwrap();
227
228 let sheet = sketch_sheet(&engine, "Sk").expect("committed sketch sheet solid");
232 assert!(sheet.is_sketch, "flagged as a sketch");
233 assert_eq!(sheet.faces.len(), 1, "one sheet face");
234 assert_eq!(sheet.edges.len(), 4, "four boundary edges");
235 assert_eq!(sheet.vertices.len(), 4, "four corner vertices");
236 assert!(!sheet.mesh.positions.is_empty(), "filled face mesh");
237
238 let solids: serde_json::Value =
240 serde_json::from_str(&engine.scene_entities_json()).unwrap();
241 assert_eq!(solids.as_array().unwrap().len(), 1);
242 assert_eq!(solids[0]["name"], "Box");
243
244 assert_eq!(engine.committed_sketches(), vec![("Sk".to_string(), true)]);
246 let sketches: serde_json::Value =
247 serde_json::from_str(&engine.sketch_entities_json()).unwrap();
248 assert_eq!(sketches[0]["name"], "Sk");
249 assert_eq!(sketches[0]["visible"], true);
250 assert!(engine.sketch_visible("Sk"));
251 }
252
253 #[test]
254 fn rolling_back_before_sketch_clears_committed_sheet() {
255 let mut engine = EngineState::new();
256 engine.set_history_json(&cube_and_sketch_history()).unwrap();
257 assert!(has_sketch_sheet(&engine, "Sk"));
258
259 engine.roll_to(0);
262 assert!(!has_sketch_sheet(&engine, "Sk"), "rolled-back sketch sheet removed");
263 assert!(engine.committed_sketches().is_empty());
264
265 engine.roll_to(1);
267 assert!(has_sketch_sheet(&engine, "Sk"), "rolled-forward sketch re-shown");
268 assert_eq!(engine.committed_sketches(), vec![("Sk".to_string(), true)]);
269 }
270
271 #[test]
272 fn set_sketch_visible_hides_and_restores() {
273 let mut engine = EngineState::new();
274 engine.set_history_json(&cube_and_sketch_history()).unwrap();
275 assert!(has_sketch_sheet(&engine, "Sk"));
276
277 engine.set_sketch_visible("Sk", false);
279 assert!(!has_sketch_sheet(&engine, "Sk"), "hidden sketch sheet removed");
280 assert!(!engine.sketch_visible("Sk"));
281 assert_eq!(engine.committed_sketches(), vec![("Sk".to_string(), false)]);
282 let sketches: serde_json::Value =
283 serde_json::from_str(&engine.sketch_entities_json()).unwrap();
284 assert_eq!(sketches[0]["visible"], false);
285
286 engine.set_sketch_visible("Sk", true);
288 assert!(has_sketch_sheet(&engine, "Sk"), "re-shown sketch sheet inserted");
289 assert!(engine.sketch_visible("Sk"));
290 }
291
292 #[test]
293 fn entering_sketch_mode_removes_committed_sheet_then_commit_readds() {
294 let mut engine = EngineState::new();
295 engine.set_history_json(&cube_and_sketch_history()).unwrap();
296 assert!(has_sketch_sheet(&engine, "Sk"));
297
298 engine.enter_sketch_mode("Sk").expect("enter");
301 assert!(engine.sketch_mode());
302 assert!(
303 !has_sketch_sheet(&engine, "Sk"),
304 "active sketch's committed sheet removed"
305 );
306 assert!(has_group(&engine, "sketch-geometry"), "live editing overlay fed");
307 assert!(engine.committed_sketches().is_empty());
309
310 engine.exit_sketch_mode(true);
313 assert!(!engine.sketch_mode());
314 assert!(
315 has_sketch_sheet(&engine, "Sk"),
316 "committed sketch reappears as a sheet after commit"
317 );
318 assert!(!has_group(&engine, "sketch-geometry"), "live editing overlay cleared");
319 assert_eq!(engine.committed_sketches(), vec![("Sk".to_string(), true)]);
320 }
321
322 #[test]
327 fn sketch_sheet_info_reports_area_and_edge_length() {
328 let mut engine = EngineState::new();
329 engine.set_history_json(&cube_and_sketch_history()).unwrap();
330 let info: serde_json::Value =
331 serde_json::from_str(&engine.object_info_json("Sk")).unwrap();
332 assert_eq!(info["ok"], true, "sketch info must not error: {info}");
333 assert_eq!(info["kind"], "sketch");
334 assert!(info.get("volume").is_none(), "a sheet has no volume");
335 assert!((info["area"].as_f64().unwrap() - 60.0).abs() < 1e-3, "area {}", info["area"]);
336 assert!(
337 (info["edgeLengthTotal"].as_f64().unwrap() - 32.0).abs() < 1e-3,
338 "perimeter {}",
339 info["edgeLengthTotal"]
340 );
341 assert_eq!(info["creatingFeature"]["id"], "Sk");
342 assert_eq!(info["creatingFeature"]["type"], "S");
343 }
344
345 #[test]
348 fn sketch_sheet_edge_is_measurable() {
349 let mut engine = EngineState::new();
350 engine.set_history_json(&cube_and_sketch_history()).unwrap();
351 let edge_name = sketch_sheet(&engine, "Sk").unwrap().edges[0].name.clone();
352 assert!(!edge_name.is_empty(), "boundary edge is named");
353 let info: serde_json::Value =
354 serde_json::from_str(&engine.object_info_json(&edge_name)).unwrap();
355 assert_eq!(info["ok"], true, "edge info: {info}");
356 assert_eq!(info["kind"], "edge");
357 assert_eq!(info["solid"], "Sk");
358 let length = info["length"].as_f64().unwrap();
359 assert!(
360 (length - 10.0).abs() < 1e-3 || (length - 6.0).abs() < 1e-3,
361 "rectangle side length {length}"
362 );
363 }
364
365 #[test]
369 fn sketch_sheet_is_selectable_by_name() {
370 let mut engine = EngineState::new();
371 engine.set_history_json(&cube_and_sketch_history()).unwrap();
372 assert!(engine.select_by_name("solid", "Sk"));
373 let sel: serde_json::Value = serde_json::from_str(&engine.selection_json()).unwrap();
374 assert_eq!(sel["solids"][0], "Sk");
375 }
376}
377