1use super::*;
20use brep_kernel::{
21 PmiAnnotation, PmiCamera, PmiDisplay, PmiGeometry, PmiProjection, PmiReport, PmiState,
22 PmiStatus, PmiView,
23};
24use crate::view::Projection;
25
26#[derive(Debug, Clone)]
28pub struct PmiModelingSnapshot {
29 pub camera_json: String,
30 pub hidden: Vec<String>,
31 pub wireframe: bool,
32}
33
34#[derive(Debug, Default, Clone, PartialEq)]
36pub struct PmiViewPatch {
37 pub name: Option<String>,
38 pub text_size_pt: Option<f64>,
39 pub wireframe: Option<bool>,
40 pub hidden: Option<Vec<String>>,
41 pub section: Option<Option<brep_kernel::feature_pipeline::pmi::PmiSection>>,
42}
43
44pub fn pmi_view_schema() -> serde_json::Value {
50 serde_json::json!({
51 "type": "pmiView",
52 "shortName": "VIEW",
53 "longName": "PMI view",
54 "inputParamsSchema": {
55 "name": { "type": "string", "label": "Name", "default_value": "View 1" },
56 "textSizePt": { "type": "number", "label": "Text size (pt)", "default_value": brep_kernel::PmiDisplay::default().text_size_pt },
57 "wireframe": { "type": "boolean", "label": "Wireframe", "default_value": false },
58 "sectionEnabled": { "type": "boolean", "label": "Section view", "default_value": false },
59 "sectionAxis": { "type": "options", "label": "Section normal", "options": ["View", "X", "Y", "Z", "Plane"], "default_value": "View" },
60 "sectionPlane": {
61 "type": "reference_selection", "label": "Section plane", "selectionFilter": ["FACE", "PLANE"], "multiple": false,
62 "minSelections": 0, "maxSelections": 1, "default_value": null,
63 "hint": "A planar face, datum plane or sketch plane to cut on (Section normal: Plane); the offset moves the cut along its normal"
64 },
65 "sectionOffset": { "type": "number", "label": "Section offset (mm)", "default_value": 0.0 },
66 "sectionFlip": { "type": "boolean", "label": "Reverse section", "default_value": false },
67 "halfAxis": {
68 "type": "options", "label": "Half section", "options": ["None", "View", "X", "Y", "Z"], "default_value": "None",
69 "hint": "A second plane through the section's origin: material is removed only where both planes remove it (a quarter cut away)"
70 },
71 "halfOffset": { "type": "number", "label": "Half section offset (mm)", "default_value": 0.0 },
72 "halfFlip": { "type": "boolean", "label": "Reverse half section", "default_value": false },
73 "sectionExclude": {
74 "type": "reference_selection", "label": "Not cut", "selectionFilter": ["SOLID", "COMPONENT"], "multiple": true,
75 "minSelections": 0, "maxSelections": 64, "default_value": [],
76 "hint": "Solids or components the section leaves whole, such as shafts and fasteners"
77 },
78 "updateCamera": { "type": "button", "label": "Update camera from the viewport" },
79 "hideSelected": { "type": "button", "label": "Hide the selected bodies in this view" }
80 }
81 })
82}
83
84fn section_planes_from(
89 params: &serde_json::Value,
90 prior: Option<&brep_kernel::feature_pipeline::pmi::PmiSection>,
91) -> Result<Vec<brep_kernel::feature_pipeline::pmi::PmiSectionPlane>, String> {
92 use brep_kernel::feature_pipeline::pmi::PmiSectionPlane;
93 let prior_planes = prior.map(|s| s.planes.clone()).unwrap_or_default();
94 let valid = |plane: &PmiSectionPlane| ["View", "X", "Y", "Z"].contains(&plane.axis.as_str()) && plane.offset.is_finite();
95 let mut planes = match params.get("sectionPlanes") {
96 Some(serde_json::Value::Null) => Vec::new(),
97 Some(list) => {
98 let planes: Vec<PmiSectionPlane> = serde_json::from_value(list.clone()).map_err(|e| format!("invalid section planes: {e}"))?;
99 if !planes.iter().all(valid) {
100 return Err("invalid section plane".into());
101 }
102 planes
103 }
104 None => prior_planes,
105 };
106 if let Some(axis) = params.get("halfAxis").and_then(serde_json::Value::as_str) {
107 if axis == "None" {
108 if !planes.is_empty() {
109 planes.remove(0);
110 }
111 } else {
112 let before = planes.first();
113 let plane = PmiSectionPlane {
114 axis: axis.to_string(),
115 offset: params.get("halfOffset").and_then(serde_json::Value::as_f64).or(before.map(|p| p.offset)).unwrap_or(0.),
116 flip: params.get("halfFlip").and_then(serde_json::Value::as_bool).or(before.map(|p| p.flip)).unwrap_or(false),
117 normal: before.filter(|p| p.axis == axis).and_then(|p| p.normal),
118 };
119 if !valid(&plane) {
120 return Err("invalid section plane".into());
121 }
122 match planes.first_mut() {
123 Some(first) => *first = plane,
124 None => planes.push(plane),
125 }
126 }
127 }
128 Ok(planes)
130}
131
132pub fn pmi_view_params(view: &brep_kernel::PmiView) -> serde_json::Value {
134 serde_json::json!({
135 "name": view.name,
136 "textSizePt": view.display.text_size_pt,
137 "wireframe": view.display.wireframe,
138 "sectionEnabled": view.display.section.is_some(),
139 "sectionAxis": view.display.section.as_ref().map(|s| s.axis.as_str()).unwrap_or("View"),
140 "sectionOffset": view.display.section.as_ref().map(|s| s.offset).unwrap_or(0.),
141 "sectionFlip": view.display.section.as_ref().is_some_and(|s| s.flip),
142 "sectionPlane": view.display.section.as_ref().and_then(|s| s.plane.as_deref()).unwrap_or(""),
143 "sectionExclude": view.display.section.as_ref().map(|s| s.exclude.clone()).unwrap_or_default(),
144 "halfAxis": view.display.section.as_ref().and_then(|s| s.planes.first()).map(|p| p.axis.as_str()).unwrap_or("None"),
145 "halfOffset": view.display.section.as_ref().and_then(|s| s.planes.first()).map(|p| p.offset).unwrap_or(0.),
146 "halfFlip": view.display.section.as_ref().and_then(|s| s.planes.first()).is_some_and(|p| p.flip),
147 })
148}
149
150pub fn world_vertex_ref(solid: &str, position: [f64; 3]) -> String {
152 let trim = |v: f64| {
153 let rounded = (v * 1e9).round() / 1e9;
154 let trimmed = crate::formatting::compact_decimal(rounded, 9);
155 if trimmed == "-0" || trimmed.is_empty() { "0".to_string() } else { trimmed }
156 };
157 format!("{solid}@{},{},{}", trim(position[0]), trim(position[1]), trim(position[2]))
158}
159
160impl EngineState {
161 pub fn pmi_state(&self) -> PmiState {
166 self.history
167 .pmi_block()
168 .and_then(|block| serde_json::from_value(block.clone()).ok())
169 .unwrap_or_default()
170 }
171
172 pub fn pmi_state_shared(&self) -> std::rc::Rc<PmiState> {
177 let revision = self.history.revision();
178 if let Some((at, state)) = self.pmi_state_cache.borrow().as_ref() {
179 if *at == revision {
180 return state.clone();
181 }
182 }
183 let state = std::rc::Rc::new(self.pmi_state());
184 *self.pmi_state_cache.borrow_mut() = Some((revision, state.clone()));
185 state
186 }
187
188 pub fn pmi_report(&self) -> Option<&PmiReport> {
190 self.pmi_report.as_ref()
191 }
192
193 pub fn pmi_active_view(&self) -> Option<&str> {
194 self.pmi_active_view.as_deref()
195 }
196
197 pub fn pmi_open_annotation(&self) -> Option<&str> {
198 self.pmi_open_annotation.as_deref()
199 }
200
201 pub fn pmi_open_view(&self) -> Option<&str> {
203 self.pmi_open_view.as_deref()
204 }
205
206 pub fn pmi_dialog_opens(&self) -> u64 {
211 self.pmi_dialog_opens
212 }
213
214 fn open_pmi_annotation(&mut self, id: String) {
225 self.pmi_open_view = None;
226 self.pmi_selected_view = None;
227 self.pmi_selected_annotation = Some(id.clone());
228 self.pmi_open_annotation = Some(id);
229 self.pmi_dialog_opens = self.pmi_dialog_opens.wrapping_add(1);
230 }
231
232 fn open_pmi_view(&mut self, id: String) {
235 self.pmi_open_annotation = None;
236 if self.pmi_selected_annotation.take().is_some() {
237 self.refresh_pmi_overlay();
238 }
239 self.pmi_selected_view = Some(id.clone());
240 self.pmi_open_view = Some(id);
241 self.pmi_dialog_opens = self.pmi_dialog_opens.wrapping_add(1);
242 }
243
244 pub fn pmi_selected_annotation(&self) -> Option<&str> {
247 let id = self.pmi_selected_annotation.as_deref()?;
248 self.pmi_state_shared().find_annotation(id).is_some().then_some(id)
249 }
250
251 pub fn pmi_selected_view(&self) -> Option<&str> {
253 let id = self.pmi_selected_view.as_deref()?;
254 self.pmi_state_shared().find_view(id).is_some().then_some(id)
255 }
256
257 pub fn pmi_workbench_entered(&self) -> bool {
259 self.pmi_modeling.is_some()
260 }
261
262 pub fn pmi_state_json(&self) -> String {
265 self.pmi_state_value().to_string()
266 }
267
268 pub fn pmi_state_value(&self) -> serde_json::Value {
270 let state = self.pmi_state();
271 serde_json::json!({
272 "views": state.views,
273 "idCounter": state.id_counter,
274 "activeView": self.pmi_active_view,
275 "openAnnotation": self.pmi_open_annotation,
276 "openView": self.pmi_open_view,
277 "selectedAnnotation": self.pmi_selected_annotation(),
278 "selectedView": self.pmi_selected_view(),
279 "entered": self.pmi_modeling.is_some(),
280 "datums": state.datum_letters(),
281 "report": self.pmi_report,
282 })
283 }
284
285 pub(super) fn write_pmi_state(&mut self, state: PmiState) -> String {
291 let block = if state.is_empty() { None } else { serde_json::to_value(&state).ok() };
292 self.history.set_pmi_block(block, None);
293 self.rerun_history()
294 }
295
296 pub(super) fn snapshot_camera(&self) -> PmiCamera {
300 let camera = &self.camera;
301 PmiCamera {
302 eye: camera.eye,
303 target: camera.target,
304 up: camera.up,
305 projection: match camera.projection {
306 Projection::Orthographic { half_height } => PmiProjection::Orthographic { half_height },
307 Projection::Perspective { fov_y_deg } => PmiProjection::Perspective { fov_y_deg },
308 },
309 viewport: [camera.width, camera.height],
310 }
311 }
312
313 fn hidden_solid_names(&self) -> Vec<String> {
315 self.scene
316 .solids()
317 .iter()
318 .filter(|solid| !solid.visible)
319 .map(|solid| solid.name.clone())
320 .collect()
321 }
322
323 pub fn pmi_capture_view(&mut self, name: Option<&str>) -> String {
326 let mut state = self.pmi_state();
327 let id = state.next_id("VIEW");
328 let name = name
329 .map(str::trim)
330 .filter(|n| !n.is_empty())
331 .map(String::from)
332 .unwrap_or_else(|| format!("View {}", state.views.len() + 1));
333 let was_active = self.pmi_active_view.is_some();
337 if was_active {
338 self.pmi_deactivate_view();
339 }
340 self.pmi_remember_modeling();
341 state.views.push(PmiView {
342 id: id.clone(),
343 name,
344 camera: Some(self.snapshot_camera()),
345 display: PmiDisplay {
346 text_size_pt: 12.0,
347 wireframe: self.settings.wireframe,
348 hidden: self.hidden_solid_names(),
349 section: None,
350 },
351 annotations: Vec::new(),
352 });
353 self.write_pmi_state(state);
354 let _ = self.pmi_activate_view(&id);
355 id
356 }
357
358 pub fn pmi_rename_view(&mut self, id: &str, name: &str) -> Result<(), String> {
359 let mut state = self.pmi_state();
360 let view = state.find_view_mut(id).ok_or_else(|| format!("no PMI view '{id}'"))?;
361 let name = name.trim();
362 if name.is_empty() {
363 return Err("a view needs a name".into());
364 }
365 view.name = name.to_string();
366 self.write_pmi_state(state);
367 Ok(())
368 }
369
370 pub fn pmi_delete_view(&mut self, id: &str) -> Result<(), String> {
372 let mut state = self.pmi_state();
373 let before = state.views.len();
374 if self.pmi_active_view.as_deref() == Some(id) {
375 self.pmi_deactivate_view();
376 }
377 state.views.retain(|view| view.id != id);
378 if state.views.len() == before {
379 return Err(format!("no PMI view '{id}'"));
380 }
381 if let Some(open) = &self.pmi_open_annotation {
382 if state.find_annotation(open).is_none() {
383 self.pmi_open_annotation = None;
384 }
385 }
386 if self.pmi_open_view.as_deref() == Some(id) {
387 self.pmi_open_view = None;
388 }
389 self.write_pmi_state(state);
390 Ok(())
391 }
392
393 pub fn pmi_update_view_camera(&mut self, id: &str) -> Result<(), String> {
396 if let Some(step) = self.instructions_step_of_view(id) {
401 let shown = self.instructions_shown_step().as_deref() == Some(step.as_str());
402 self.instructions_capture_camera(&step)?;
403 if shown {
404 self.instructions_show_step(&step)?;
405 }
406 return Ok(());
407 }
408 let mut state = self.pmi_state();
409 let camera = self.snapshot_camera();
410 let view = state.find_view_mut(id).ok_or_else(|| format!("no PMI view '{id}'"))?;
411 if let Some(section) = view.display.section.as_mut() {
414 section.freeze(view.camera.as_ref());
415 }
416 view.camera = Some(camera);
417 self.write_pmi_state(state);
418 Ok(())
419 }
420
421 pub fn pmi_hide_selected(&mut self, view_id: Option<&str>) -> Result<String, String> {
428 let mut targets = self.selected_occurrences();
429 for solid in &self.emphasis.selected_solids {
430 if !targets.iter().any(|target| brep_kernel::entry_names_solid(target, solid)) {
431 targets.push(solid.clone());
432 }
433 }
434 if targets.is_empty() {
435 return Err("select the solids or components to hide".into());
436 }
437 let params = serde_json::json!({ "targets": targets }).to_string();
438 self.pmi_add_annotation(view_id, "hide", ¶ms)
439 }
440
441 pub fn pmi_set_view_display(&mut self, id: &str, patch: &PmiViewPatch) -> Result<(), String> {
443 self.patch_pmi_view(id, patch, None)
444 }
445
446 pub fn pmi_update_view(&mut self, id: &str, params_json: &str) -> Result<(), String> {
451 let params: serde_json::Value =
452 serde_json::from_str(params_json).map_err(|error| format!("view params: {error}"))?;
453 let state = self.pmi_state();
454 let view = state.find_view(id).ok_or_else(|| format!("no PMI view '{id}'"))?;
455 let name = params
456 .get("name")
457 .and_then(serde_json::Value::as_str)
458 .filter(|name| *name != view.name)
459 .map(String::from);
460 let text_size_pt = params
461 .get("textSizePt")
462 .and_then(serde_json::Value::as_f64)
463 .filter(|size| *size != view.display.text_size_pt);
464 let wireframe = params
465 .get("wireframe")
466 .and_then(serde_json::Value::as_bool)
467 .filter(|on| *on != view.display.wireframe);
468 let section = if params.get("sectionEnabled").is_some() {
469 let section = if params["sectionEnabled"].as_bool().unwrap_or(false) {
470 let axis = params["sectionAxis"].as_str().unwrap_or("View");
471 let offset = params["sectionOffset"].as_f64().unwrap_or(0.);
472 if !["View", "X", "Y", "Z", "Plane"].contains(&axis) || !offset.is_finite() { return Err("invalid section plane".into()); }
473 let prior = view.display.section.as_ref();
477 let mut section = brep_kernel::feature_pipeline::pmi::PmiSection {
478 axis: axis.into(),
479 offset,
480 flip: params["sectionFlip"].as_bool().unwrap_or(false),
481 origin: prior.and_then(|s| s.origin),
482 normal: prior.filter(|s| s.axis == axis).and_then(|s| s.normal),
483 plane: match params.get("sectionPlane") {
485 Some(serde_json::Value::String(name)) => Some(name.trim().to_string()).filter(|name| !name.is_empty()),
486 Some(serde_json::Value::Null) => None,
487 _ => prior.and_then(|s| s.plane.clone()),
488 },
489 exclude: match params.get("sectionExclude") {
490 Some(serde_json::Value::Array(names)) => names
491 .iter()
492 .filter_map(serde_json::Value::as_str)
493 .map(|name| name.trim().to_string())
494 .filter(|name| !name.is_empty())
495 .collect(),
496 Some(serde_json::Value::Null) => Vec::new(),
497 _ => prior.map(|s| s.exclude.clone()).unwrap_or_default(),
498 },
499 planes: section_planes_from(¶ms, prior)?,
500 };
501 section.freeze(view.camera.as_ref());
502 Some(section)
503 } else { None };
504 (section != view.display.section).then_some(section)
505 } else { None };
506 if name.is_none() && text_size_pt.is_none() && wireframe.is_none() && section.is_none() {
507 return Ok(());
508 }
509 let patch = PmiViewPatch { name, text_size_pt, wireframe, hidden: None, section };
510 self.patch_pmi_view(id, &patch, Some(&format!("pmi:view:{id}")))
511 }
512
513 #[allow(clippy::too_many_arguments)]
521 pub fn pmi_set_view_section(
522 &mut self,
523 id: &str,
524 enabled: bool,
525 axis: Option<&str>,
526 offset: Option<f64>,
527 flip: Option<bool>,
528 plane: Option<&str>,
529 exclude: Option<Vec<String>>,
530 planes: Option<Vec<brep_kernel::feature_pipeline::pmi::PmiSectionPlane>>,
531 ) -> Result<(), String> {
532 let state = self.pmi_state();
533 let view = state.find_view(id).ok_or_else(|| format!("no PMI view '{id}'"))?;
534 let prior = view.display.section.as_ref();
535 let axis = axis.or_else(|| plane.filter(|plane| !plane.trim().is_empty()).map(|_| "Plane"));
538 let mut params = serde_json::json!({
539 "sectionEnabled": enabled,
540 "sectionAxis": axis.map(str::to_string).or_else(|| prior.map(|s| s.axis.clone())).unwrap_or_else(|| "View".into()),
541 "sectionOffset": offset.or_else(|| prior.map(|s| s.offset)).unwrap_or(0.),
542 "sectionFlip": flip.or_else(|| prior.map(|s| s.flip)).unwrap_or(false),
543 });
544 if let Some(plane) = plane {
545 params["sectionPlane"] = plane.into();
546 }
547 if let Some(exclude) = exclude {
548 params["sectionExclude"] = exclude.into();
549 }
550 if let Some(planes) = planes {
551 params["sectionPlanes"] = serde_json::to_value(planes).map_err(|e| e.to_string())?;
552 }
553 self.history.break_coalescing();
554 let result = self.pmi_update_view(id, ¶ms.to_string());
555 self.history.break_coalescing();
556 result
557 }
558
559 pub fn pmi_set_view_open(&mut self, id: Option<&str>) {
563 match id {
564 Some(id) => {
565 if self.pmi_state().find_view(id).is_none() {
566 return;
567 }
568 if self.pmi_active_view.as_deref() != Some(id) {
569 let _ = self.pmi_activate_view(id);
570 }
571 self.open_pmi_view(id.to_string());
572 }
573 None => self.pmi_open_view = None,
574 }
575 self.refresh_pmi_overlay();
576 }
577
578 fn patch_pmi_view(&mut self, id: &str, patch: &PmiViewPatch, coalesce_key: Option<&str>) -> Result<(), String> {
579 let mut state = self.pmi_state();
580 let view = state.find_view_mut(id).ok_or_else(|| format!("no PMI view '{id}'"))?;
581 if let Some(name) = &patch.name {
582 let name = name.trim();
583 if name.is_empty() {
584 return Err("a view needs a name".into());
585 }
586 view.name = name.to_string();
587 }
588 if let Some(size) = patch.text_size_pt {
589 view.display.text_size_pt = brep_kernel::clamp_text_size(size);
590 }
591 if let Some(wireframe) = patch.wireframe {
592 view.display.wireframe = wireframe;
593 }
594 if let Some(hidden) = &patch.hidden {
595 view.display.hidden = hidden.clone();
596 }
597 if let Some(section) = &patch.section { view.display.section = section.clone(); }
598 let view = view.clone();
599 let block = if state.is_empty() { None } else { serde_json::to_value(&state).ok() };
600 self.history.set_pmi_block(block, coalesce_key);
601 self.rerun_history();
602 if self.pmi_active_view.as_deref() == Some(id) {
603 self.apply_view_display(&view);
604 }
605 Ok(())
606 }
607
608 fn pmi_remember_modeling(&mut self) {
614 if self.pmi_active_view.is_none() || self.pmi_modeling.is_none() {
615 self.pmi_modeling = Some(PmiModelingSnapshot {
616 camera_json: self.camera_state_json(),
617 hidden: self.hidden_solid_names(),
618 wireframe: self.settings.wireframe,
619 });
620 }
621 }
622
623 pub fn pmi_enter_workbench(&mut self) {
626 self.pmi_remember_modeling();
627 }
628
629 pub fn pmi_leave_workbench(&mut self) {
634 self.pmi_deactivate_view();
635 self.pmi_modeling = None;
636 self.pmi_open_annotation = None;
637 self.pmi_open_view = None;
638 self.refresh_pmi_overlay();
639 }
640
641 fn apply_view_display(&mut self, view: &PmiView) {
646 let display = &view.display;
647 let names: Vec<String> = self.scene.solids().iter().map(|s| s.name.clone()).collect();
648 for name in names {
649 let hidden = view.hides(&name);
650 if let Some(original) = self.pmi_explode_originals.get_mut(&name).filter(|original| original.visible == hidden) {
651 original.visible = !hidden;
652 self.pmi_unposed_visibility += 1;
653 }
654 if hidden {
655 self.pmi_section_hidden.retain(|hidden| *hidden != name);
657 self.scene.set_visible(&name, false);
658 } else if !self.pmi_section_hidden.contains(&name) {
659 self.scene.set_visible(&name, true);
661 }
662 }
663 if self.settings.wireframe != display.wireframe {
664 self.settings.wireframe = display.wireframe;
665 self.settings_generation = self.settings_generation.wrapping_add(1);
666 }
667 self.dirty = true;
668 }
669
670 pub fn pmi_activate_view(&mut self, id: &str) -> Result<(), String> {
674 let state = self.pmi_state();
675 let view = state.find_view(id).ok_or_else(|| format!("no PMI view '{id}'"))?.clone();
676 if self.pmi_active_view.as_deref() != Some(id) {
677 let keep = self.pmi_active_view.is_some();
680 self.pmi_deactivate_view();
681 if !keep {
682 self.pmi_remember_modeling();
683 }
684 }
685 if self.pmi_modeling.is_none() {
686 self.pmi_remember_modeling();
687 }
688 if self.pmi_active_view.as_deref() != Some(id) {
689 self.explode_playback = Default::default();
691 }
692 self.pmi_active_view = Some(id.to_string());
693 if let Some(camera) = &view.camera {
694 let (kind, scale) = match camera.projection {
695 PmiProjection::Orthographic { half_height } => ("orthographic", half_height),
696 PmiProjection::Perspective { fov_y_deg } => ("perspective", fov_y_deg),
697 };
698 let json = serde_json::json!({
699 "kind": kind, "eye": camera.eye, "target": camera.target, "up": camera.up, "scale": scale,
700 })
701 .to_string();
702 let _ = self.apply_camera_state_json(&json);
703 self.controls_sync_after_camera_apply();
704 }
705 self.apply_view_display(&view);
706 self.pmi_apply_explode();
707 self.instructions_refresh_ghosts();
708 if let Some(open) = &self.pmi_open_annotation {
709 if !view.annotations.iter().any(|a| a.id() == open) {
710 self.pmi_open_annotation = None;
711 }
712 }
713 self.refresh_pmi_overlay();
714 Ok(())
715 }
716
717 pub fn pmi_deactivate_view(&mut self) {
720 if self.pmi_active_view.take().is_none() {
721 return;
722 }
723 self.instructions_refresh_ghosts();
725 if self.pmi_state().find_annotation(&self.transform_armed_feature()).is_some() { self.disarm_transform(); }
726 self.pmi_restore_explode();
727 if let Some(snapshot) = self.pmi_modeling.clone() {
728 let names: Vec<String> = self.scene.solids().iter().map(|s| s.name.clone()).collect();
729 for name in names {
730 let visible = !snapshot.hidden.contains(&name);
731 self.scene.set_visible(&name, visible);
732 }
733 if self.settings.wireframe != snapshot.wireframe {
734 self.settings.wireframe = snapshot.wireframe;
735 self.settings_generation = self.settings_generation.wrapping_add(1);
736 }
737 let _ = self.apply_camera_state_json(&snapshot.camera_json);
738 self.controls_sync_after_camera_apply();
739 }
740 self.pmi_open_annotation = None;
741 self.dirty = true;
742 self.refresh_pmi_overlay();
743 }
744
745 pub(super) fn controls_sync_after_camera_apply(&mut self) {
747 self.dirty = true;
751 }
752
753 pub(crate) fn pmi_apply_explode(&mut self) {
758 let Some(active) = self.pmi_active_view.clone() else {
759 self.pmi_restore_explode();
760 return;
761 };
762 let report = self.pmi_report.as_ref().and_then(|report| report.view(&active));
763 let state = self.pmi_state_shared();
764 let view = state.find_view(&active);
765 let planes = view.map(|v| v.section_planes_in(report)).unwrap_or_default();
767 let section = (!planes.is_empty()).then_some(());
768 let exclude = view.and_then(|v| v.display.section.clone());
769 let poses: Vec<(Vec<String>, [f64; 3], [f64; 3], [f64; 3], [f64; 3])> = report
770 .map(|view| {
771 view.annotations
772 .iter()
773 .filter(|row| row.enabled && row.status == PmiStatus::Ok)
774 .filter(|row| matches!(row.geometry, PmiGeometry::Explode { .. }))
775 .enumerate()
776 .filter_map(|(step, row)| match super::exploded_ops::explode_at(&row.geometry, self.explode_fraction(step)) {
778 PmiGeometry::Explode { solids, translate, rotate_deg, scale, center, .. } => {
779 Some((solids, translate, rotate_deg, scale, center))
780 }
781 _ => None,
782 })
783 .collect()
784 })
785 .unwrap_or_default();
786 let mut poses = poses;
788 poses.extend(self.instructions_insertion_poses());
789 if section.is_none() && self.pmi_section_hidden.is_empty() && !self.pmi_explode_originals.is_empty() {
790 let wanted: std::collections::HashSet<&String> = poses.iter().flat_map(|pose| pose.0.iter()).collect();
796 let stale: Vec<String> = self.pmi_explode_originals.keys().filter(|name| !wanted.contains(name)).cloned().collect();
797 for name in stale {
798 if let (Some(original), Some(display)) = (self.pmi_explode_originals.remove(&name), self.scene.solid_mut(&name)) {
799 *display = original;
800 }
801 }
802 for name in wanted {
803 match self.pmi_explode_originals.get(name) {
804 Some(original) => {
805 if let Some(display) = self.scene.solid_mut(name) {
806 reset_posed_geometry(display, original);
807 }
808 }
809 None => {
810 if let Some(display) = self.scene.solid(name) {
811 self.pmi_explode_originals.insert(name.clone(), display.clone());
812 }
813 }
814 }
815 }
816 } else {
817 self.pmi_restore_explode();
818 for (solids, ..) in &poses {
819 for name in solids {
820 let Some(display) = self.scene.solid(name) else { continue };
821 self.pmi_explode_originals.entry(name.clone()).or_insert_with(|| display.clone());
822 }
823 }
824 }
825 for (solids, translate, rotate_deg, scale, center) in poses {
826 for name in solids {
827 if let Some(display) = self.scene.solid_mut(&name) {
828 transform_display(display, center, translate, rotate_deg, scale);
829 }
830 }
831 }
832 if section.is_some() {
833 use super::pmi_section::{side_of_all, Side};
834 let names: Vec<(String, Side)> = self
842 .scene
843 .solids()
844 .iter()
845 .map(|s| (s.name.clone(), if exclude.as_ref().is_some_and(|cut| cut.excludes(&s.name)) { Side::Kept } else { side_of_all(s, &planes) }))
846 .collect();
847 for (name, side) in names {
848 match side {
849 Side::Kept => {}
850 Side::Removed => {
851 if let Some(display) = self.scene.solid(&name).filter(|display| display.visible) {
852 self.pmi_explode_originals.entry(name.clone()).or_insert_with(|| display.clone());
853 self.scene.set_visible(&name, false);
854 self.pmi_section_hidden.push(name);
855 }
856 }
857 Side::Cut => {
858 if let Some(display) = self.scene.solid(&name) { self.pmi_explode_originals.entry(name.clone()).or_insert_with(|| display.clone()); }
859 if let Some(display) = self.scene.solid_mut(&name) { super::pmi_section::clip_display_all(display, &planes); }
860 }
861 }
862 }
863 }
864 if !self.pmi_explode_originals.is_empty() || !self.pmi_section_hidden.is_empty() {
865 self.dirty = true;
866 }
867 }
868
869 pub(crate) fn pmi_restore_explode(&mut self) {
871 for name in std::mem::take(&mut self.pmi_section_hidden) {
872 self.scene.set_visible(&name, true);
873 }
874 let originals = std::mem::take(&mut self.pmi_explode_originals);
875 for (name, original) in originals {
876 if let Some(display) = self.scene.solid_mut(&name) {
877 *display = original;
878 }
879 }
880 self.dirty = true;
881 }
882
883 pub(crate) fn pmi_after_apply(&mut self) {
886 self.pmi_balloon_stale = None;
888 self.pmi_restore_explode();
892 if let Some(active) = self.pmi_active_view.clone() {
893 let view = self.pmi_state().find_view(&active).cloned();
895 match view {
896 None => {
897 self.pmi_active_view = None;
898 self.pmi_open_annotation = None;
899 }
900 Some(view) => {
901 self.apply_view_display(&view);
905 self.pmi_apply_explode();
906 }
907 }
908 }
909 self.instructions_refresh_ghosts();
910 if let Some(open) = &self.pmi_open_annotation {
911 if self.pmi_state().find_annotation(open).is_none() {
912 self.pmi_open_annotation = None;
913 }
914 }
915 if let Some(open) = &self.pmi_open_view {
917 if self.pmi_state().find_view(open).is_none() {
918 self.pmi_open_view = None;
919 }
920 }
921 self.refresh_pmi_overlay();
922 }
923
924 pub fn pmi_add_annotation(
931 &mut self,
932 view_id: Option<&str>,
933 type_id: &str,
934 params_json: &str,
935 ) -> Result<String, String> {
936 let def = brep_kernel::pmi_type(type_id).ok_or_else(|| format!("unknown PMI annotation type '{type_id}'"))?;
937 let mut state = self.pmi_state();
938 let view_id = view_id
939 .map(String::from)
940 .or_else(|| self.pmi_active_view.clone())
941 .ok_or_else(|| "no active PMI view — capture or activate a view first".to_string())?;
942 let id = state.next_id(def.short_name);
943 let mut params: serde_json::Value = serde_json::from_str(params_json).unwrap_or_else(|_| serde_json::json!({}));
944 if !params.is_object() {
945 params = serde_json::json!({});
946 }
947 let schema = (def.schema)();
949 if let (Some(fields), Some(object)) = (
950 schema.get("inputParamsSchema").and_then(serde_json::Value::as_object),
951 params.as_object_mut(),
952 ) {
953 for (key, spec) in fields {
954 if !object.contains_key(key) {
955 if let Some(default) = spec.get("default_value") {
956 if !default.is_null() {
957 object.insert(key.clone(), default.clone());
958 }
959 }
960 }
961 }
962 object.insert("id".into(), serde_json::Value::String(id.clone()));
963 if type_id == "datum" {
964 let letter = object.get("letter").and_then(serde_json::Value::as_str).unwrap_or("").trim().to_string();
965 if letter.is_empty() {
966 if let Some(next) = state.next_datum_letter() {
967 object.insert("letter".into(), serde_json::Value::String(next));
968 }
969 }
970 }
971 }
972 let view = state.find_view_mut(&view_id).ok_or_else(|| format!("no PMI view '{view_id}'"))?;
973 view.annotations.push(PmiAnnotation {
974 plugin_replay: None,
975 persistent_data: serde_json::Value::Null,
976 kind: type_id.to_string(),
977 enabled: true,
978 params,
979 label_world: None,
980 });
981 self.open_pmi_annotation(id.clone());
982 self.write_pmi_state(state);
983 Ok(id)
984 }
985
986 pub fn pmi_update_annotation(&mut self, id: &str, params_json: &str) -> Result<(), String> {
988 if self.pmi_state().find_annotation(id).is_some_and(|(_, a)| a.kind.contains('/')) {
989 let params = serde_json::from_str(params_json).map_err(|e| format!("annotation params: {e}"))?;
990 return self.plugin_update_annotation(id, params).map(|_| ());
991 }
992 self.pmi_update_annotation_no_rerun(id, params_json)?;
993 self.rerun_history();
994 Ok(())
995 }
996
997 pub(crate) fn pmi_update_annotation_no_rerun(&mut self, id: &str, params_json: &str) -> Result<(), String> {
1001 let mut state = self.pmi_state();
1002 let annotation = state.find_annotation_mut(id).ok_or_else(|| format!("no PMI annotation '{id}'"))?;
1003 if annotation.kind.contains('/') {
1004 return Err("plugin annotation edits require the worker transaction path".into());
1005 }
1006 let mut params: serde_json::Value =
1007 serde_json::from_str(params_json).map_err(|error| format!("annotation params: {error}"))?;
1008 if let Some(object) = params.as_object_mut() {
1009 object.insert("id".into(), serde_json::Value::String(id.to_string()));
1010 }
1011 annotation.params = params;
1012 let block = serde_json::to_value(&state).ok();
1013 self.history.set_pmi_block(block, Some(&format!("pmi:params:{id}")));
1014 Ok(())
1015 }
1016
1017 pub fn pmi_remove_annotation(&mut self, id: &str) -> Result<(), String> {
1018 if self.pmi_state().find_annotation(id).is_some_and(|(_, a)| a.kind.contains('/')) {
1019 return self.plugin_delete_annotation(id).map(|_| ());
1020 }
1021 let mut state = self.pmi_state();
1022 let Some((view_index, index)) = state.locate_annotation(id) else {
1023 return Err(format!("no PMI annotation '{id}'"));
1024 };
1025 state.views[view_index].annotations.remove(index);
1026 if self.pmi_open_annotation.as_deref() == Some(id) {
1027 self.pmi_open_annotation = None;
1028 }
1029 self.write_pmi_state(state);
1030 Ok(())
1031 }
1032
1033 pub fn pmi_set_annotation_enabled(&mut self, id: &str, enabled: bool) -> Result<(), String> {
1034 if self.pmi_state().find_annotation(id).is_some_and(|(_, a)| a.kind.contains('/')) {
1035 return self.plugin_set_annotation_enabled(id, enabled).map(|_| ());
1036 }
1037 let mut state = self.pmi_state();
1038 let annotation = state.find_annotation_mut(id).ok_or_else(|| format!("no PMI annotation '{id}'"))?;
1039 if annotation.enabled == enabled {
1040 return Ok(());
1041 }
1042 annotation.enabled = enabled;
1043 self.write_pmi_state(state);
1044 Ok(())
1045 }
1046
1047 pub fn pmi_move_annotation(&mut self, id: &str, index: usize) -> Result<(), String> {
1049 let mut state = self.pmi_state();
1050 let Some((view_index, from)) = state.locate_annotation(id) else {
1051 return Err(format!("no PMI annotation '{id}'"));
1052 };
1053 let annotations = &mut state.views[view_index].annotations;
1054 let annotation = annotations.remove(from);
1055 let to = index.min(annotations.len());
1056 annotations.insert(to, annotation);
1057 self.write_pmi_state(state);
1058 Ok(())
1059 }
1060
1061 pub fn pmi_move_annotation_to_view(&mut self, id: &str, view_id: &str) -> Result<(), String> {
1063 let mut state = self.pmi_state();
1064 let Some((view_index, from)) = state.locate_annotation(id) else {
1065 return Err(format!("no PMI annotation '{id}'"));
1066 };
1067 if state.find_view(view_id).is_none() {
1068 return Err(format!("no PMI view '{view_id}'"));
1069 }
1070 let annotation = state.views[view_index].annotations.remove(from);
1071 state.find_view_mut(view_id).expect("checked").annotations.push(annotation);
1072 self.write_pmi_state(state);
1073 Ok(())
1074 }
1075
1076 pub fn pmi_set_annotation_open(&mut self, id: Option<&str>) {
1083 match id {
1084 Some(id) => {
1085 let state = self.pmi_state();
1086 if let Some((view, _)) = state.find_annotation(id) {
1087 let view_id = view.id.clone();
1088 if self.pmi_active_view.as_deref() != Some(view_id.as_str()) {
1089 let _ = self.pmi_activate_view(&view_id);
1090 }
1091 self.open_pmi_annotation(id.to_string());
1092 }
1093 }
1094 None => {
1095 if self.pmi_open_annotation.as_deref() == Some(self.transform_armed_feature().as_str()) { self.disarm_transform(); }
1096 self.pmi_open_annotation = None;
1097 },
1098 }
1099 self.refresh_pmi_overlay();
1100 }
1101
1102 pub fn pmi_set_label_world(&mut self, id: &str, world: [f64; 3]) -> Result<(), String> {
1109 let mut state = self.pmi_state();
1110 let annotation = state.find_annotation_mut(id).ok_or_else(|| format!("no PMI annotation '{id}'"))?;
1111 annotation.label_world = Some(world);
1112 let block = serde_json::to_value(&state).ok();
1113 self.history.set_pmi_block(block, Some(&format!("pmi:label:{id}")));
1114 if let Some(report) = self.pmi_report.as_mut() {
1115 if let Some(row) = report.annotation_mut(id) {
1116 row.label_world = world;
1117 if let PmiGeometry::Note { position } = &mut row.geometry {
1118 *position = world;
1119 }
1120 }
1121 }
1122 self.pmi_reproject_balloon_head(id);
1123 self.refresh_pmi_overlay();
1124 Ok(())
1125 }
1126
1127 pub(crate) fn pmi_reproject_balloon_head(&mut self, id: &str) {
1137 if let Some(missing) = self.pmi_balloon_head_now(id) {
1138 self.pmi_balloon_stale = Some(id.to_string());
1139 self.request_exact_solids(missing.iter().map(String::as_str));
1140 self.pmi_refresh_stale_balloon();
1141 }
1142 }
1143
1144 pub(crate) fn pmi_refresh_stale_balloon(&mut self) {
1146 let Some(id) = self.pmi_balloon_stale.take() else {
1147 return;
1148 };
1149 if self.pmi_balloon_head_now(&id).is_some() {
1150 self.pmi_balloon_stale = Some(id);
1152 } else {
1153 self.refresh_pmi_overlay();
1154 }
1155 }
1156
1157 pub(super) fn pmi_balloon_head_now(&mut self, id: &str) -> Option<Vec<String>> {
1160 let (mut anchor, bubble) = self
1161 .pmi_report
1162 .as_ref()
1163 .and_then(|report| report.annotation(id))
1164 .and_then(|row| match &row.geometry {
1165 PmiGeometry::Leader { balloon: true, anchor: Some(anchor), .. } => Some((anchor.clone(), row.label_world)),
1166 _ => None,
1167 })?;
1168 anchor.camera = Some(self.snapshot_camera());
1169 let mut missing = Vec::new();
1170 let head = brep_kernel::pmi_balloon_head(&anchor, bubble, |name| self.scene.exact_solid(name), &mut missing);
1171 if !missing.is_empty() {
1172 return Some(missing);
1173 }
1174 if let Ok(Some(head)) = head {
1177 if let Some(row) = self.pmi_report.as_mut().and_then(|report| report.annotation_mut(id)) {
1178 if let PmiGeometry::Leader { targets, anchor: recipe, .. } = &mut row.geometry {
1179 *recipe = Some(anchor);
1180 if let Some(first) = targets.first_mut() {
1181 *first = head;
1182 }
1183 }
1184 }
1185 }
1186 None
1187 }
1188
1189 pub fn pmi_label_drag_to(&mut self, id: &str, x: f64, y: f64) {
1193 let playback = self.explode_playback_poses();
1196 let Some((current, plane)) = self
1197 .pmi_report
1198 .as_ref()
1199 .and_then(|r| r.annotation(id))
1200 .map(|r| (playback.as_ref().map_or(r.label_world, |poses| poses.shown(r, r.label_world)), r.plane))
1201 else {
1202 return;
1203 };
1204 let ray = self.camera.pick_ray(x, y);
1205 let plane = plane.unwrap_or_else(|| {
1209 let (_, _, view) = self.camera.basis();
1210 brep_kernel::PmiPlane {
1211 origin: current,
1212 normal: view,
1213 x_axis: [0.0; 3],
1214 }
1215 });
1216 let Some(world) = plane.hit(ray.origin, ray.dir) else {
1217 return;
1218 };
1219 let world = match (&playback, self.pmi_report.as_ref().and_then(|r| r.annotation(id))) {
1220 (Some(poses), Some(row)) => poses.full(row, world),
1221 _ => world,
1222 };
1223 let _ = self.pmi_set_label_world(id, world);
1224 }
1225
1226 pub fn pmi_label_drag_end(&mut self) {
1228 self.history.break_coalescing();
1229 }
1230
1231 pub fn pmi_hover(&mut self, id: &str) {
1233 self.pmi_label_hover_active = true;
1234 if self.pmi_hovered.as_deref() == Some(id) {
1235 return;
1236 }
1237 let references: Vec<String> = self
1238 .pmi_report
1239 .as_ref()
1240 .and_then(|report| report.annotation(id))
1241 .map(|row| row.references.clone())
1242 .unwrap_or_default();
1243 let mut solids: Vec<String> = Vec::new();
1244 let mut faces: Vec<String> = Vec::new();
1245 let mut edges: Vec<String> = Vec::new();
1246 for reference in &references {
1247 if let Some(at) = reference.find('@') {
1248 solids.push(reference[..at].to_string());
1249 } else if self.scene_has_face(reference) {
1250 faces.push(reference.clone());
1251 } else if self.scene_has_edge(reference) {
1252 edges.push(reference.clone());
1253 } else if self.scene.solid(reference).is_some() {
1254 solids.push(reference.clone());
1255 } else {
1256 let prefix = format!("{reference}:");
1257 solids.extend(self.scene.solids().iter().filter(|s| s.name.starts_with(&prefix)).map(|s| s.name.clone()));
1258 }
1259 }
1260 self.clear_hover();
1261 self.emphasis.hovered_solids.extend(solids);
1262 self.emphasis.hovered_faces.extend(faces);
1263 self.emphasis.hovered_edges.extend(edges);
1264 self.emphasis.generation = self.emphasis.generation.wrapping_add(1);
1265 self.pmi_hovered = Some(id.to_string());
1266 self.dirty = true;
1267 }
1268
1269 pub fn pmi_hover_end(&mut self) {
1270 if self.pmi_hovered.take().is_some() {
1271 self.clear_hover();
1272 self.dirty = true;
1273 }
1274 }
1275
1276 pub fn take_pmi_label_hover(&mut self) -> bool {
1279 std::mem::take(&mut self.pmi_label_hover_active)
1280 }
1281
1282 pub fn pmi_label_clicked(&mut self, id: &str) {
1285 self.pmi_select_annotation(id);
1286 }
1287
1288 pub fn pmi_label_double_clicked(&mut self, id: &str) {
1291 self.pmi_set_annotation_open(Some(id));
1292 }
1293
1294 pub fn pmi_select_annotation(&mut self, id: &str) -> bool {
1299 let state = self.pmi_state();
1300 let Some((view, _)) = state.find_annotation(id) else {
1301 return false;
1302 };
1303 if self.pmi_active_view.as_deref() != Some(view.id.as_str()) {
1304 let _ = self.pmi_activate_view(&view.id);
1305 }
1306 self.pmi_selected_view = None;
1307 self.pmi_selected_annotation = Some(id.to_string());
1308 self.refresh_pmi_overlay();
1309 true
1310 }
1311
1312 pub fn pmi_select_view(&mut self, id: &str) -> bool {
1318 if self.pmi_state().find_view(id).is_none() {
1319 return false;
1320 }
1321 if self.pmi_selected_annotation.take().is_some() {
1322 self.refresh_pmi_overlay();
1323 }
1324 self.pmi_selected_view = Some(id.to_string());
1325 self.dirty = true;
1326 true
1327 }
1328
1329 pub fn begin_ref_select_for_pmi(
1333 &mut self,
1334 id: &str,
1335 path: Vec<String>,
1336 label: String,
1337 filter: Vec<String>,
1338 multiple: bool,
1339 seed_names: Vec<String>,
1340 ) {
1341 let restore_index = self.history.rollback();
1342 self.selection_filter = SelectionFilter::from_ref_filter(&filter);
1343 self.ref_select = Some(RefSelectState {
1344 feature_id: id.to_string(),
1345 path,
1346 label,
1347 filter,
1348 multiple,
1349 names: seed_names,
1350 restore_index,
1351 target: RefSelectTarget::Pmi,
1352 picks: 0,
1353 claimed: false,
1354 finish_requested: false,
1355 });
1356 self.sync_ref_select_emphasis();
1357 }
1358
1359 fn explode_target_names(&self, id: &str, annotation: &PmiAnnotation) -> Vec<String> {
1365 if let Some(field) = self.ref_select_field() {
1366 if field.target == RefSelectTarget::Pmi && field.subject == id && field.path == ["targets"] {
1367 return field.names;
1368 }
1369 }
1370 annotation.references("targets")
1371 }
1372
1373 fn scene_names_center(&self, names: &[String]) -> Option<[f64; 3]> {
1377 let mut low = [f64::INFINITY; 3];
1378 let mut high = [f64::NEG_INFINITY; 3];
1379 let mut any = false;
1380 for name in names {
1381 let prefix = format!("{name}:");
1382 for solid in self.scene.solids() {
1383 if solid.name == *name || solid.name.starts_with(&prefix) {
1384 for axis in 0..3 {
1385 low[axis] = low[axis].min(solid.bbox.min[axis]);
1386 high[axis] = high[axis].max(solid.bbox.max[axis]);
1387 }
1388 any = true;
1389 }
1390 }
1391 }
1392 any.then(|| std::array::from_fn(|axis| (low[axis] + high[axis]) * 0.5))
1393 }
1394
1395 pub(super) fn pmi_explode_live_center(&self, id: &str) -> Option<[f64; 3]> {
1405 let state = self.pmi_state_shared();
1406 let (view, annotation) = state.find_annotation(id)?;
1407 if annotation.kind != "explode" || self.pmi_active_view.as_deref() != Some(view.id.as_str()) {
1408 return None;
1409 }
1410 let names = self.explode_target_names(id, annotation);
1411 self.scene_names_center(&names)
1412 }
1413
1414 pub(crate) fn pmi_commit_refs(&mut self, id: &str, path: &[String], names: &[String], multiple: bool) {
1417 if self.pmi_state().find_view(id).is_some() {
1418 match path.first().map(String::as_str) {
1419 Some("sectionExclude") => self.pmi_commit_section_exclude(id, names),
1420 _ => self.pmi_commit_section_plane(id, names.first().map(String::as_str).unwrap_or("")),
1421 }
1422 self.history.break_coalescing();
1423 return;
1424 }
1425 let state = self.pmi_state();
1426 let Some((_, annotation)) = state.find_annotation(id) else {
1427 self.push_notice(format!("unknown PMI annotation '{id}'"));
1428 return;
1429 };
1430 let mut params = annotation.params.clone();
1431 let value = if multiple {
1432 serde_json::Value::Array(names.iter().cloned().map(serde_json::Value::String).collect())
1433 } else {
1434 serde_json::Value::String(names.first().cloned().unwrap_or_default())
1435 };
1436 super::selection_ux::set_json_at(&mut params, path, value);
1437 if let Err(error) = self.pmi_update_annotation_no_rerun(id, ¶ms.to_string()) {
1438 self.push_notice(format!("PMI update failed: {error}"));
1439 }
1440 self.history.break_coalescing();
1444 }
1445
1446 fn pmi_commit_section_plane(&mut self, id: &str, name: &str) {
1450 let mut state = self.pmi_state();
1451 let Some(view) = state.find_view_mut(id) else { return };
1452 let section = view.display.section.get_or_insert_with(|| brep_kernel::feature_pipeline::pmi::PmiSection {
1453 axis: "Plane".into(),
1454 offset: 0.,
1455 flip: false,
1456 origin: None,
1457 normal: None,
1458 plane: None,
1459 exclude: Vec::new(),
1460 planes: Vec::new(),
1461 });
1462 section.axis = "Plane".into();
1463 section.plane = Some(name.trim().to_string()).filter(|name| !name.is_empty());
1464 section.freeze(None);
1465 let block = serde_json::to_value(&state).ok();
1466 self.history.break_coalescing();
1468 self.history.set_pmi_block(block, Some(&format!("pmi:view:{id}")));
1469 }
1470
1471 fn pmi_commit_section_exclude(&mut self, id: &str, names: &[String]) {
1475 let mut state = self.pmi_state();
1476 let Some(section) = state.find_view_mut(id).and_then(|view| view.display.section.as_mut()) else { return };
1477 section.exclude = names.iter().map(|name| name.trim().to_string()).filter(|name| !name.is_empty()).collect();
1478 let block = serde_json::to_value(&state).ok();
1479 self.history.break_coalescing();
1480 self.history.set_pmi_block(block, Some(&format!("pmi:view:{id}")));
1481 }
1482
1483 pub(crate) fn pmi_merge_imported(&mut self, lifted: PmiState) {
1489 let mut state = self.pmi_state();
1490 for view in lifted.views {
1491 let id = state.next_id("VIEW");
1492 let mut annotations = Vec::with_capacity(view.annotations.len());
1493 for mut annotation in view.annotations {
1494 let prefix = brep_kernel::pmi_type(&annotation.kind).map(|def| def.short_name).unwrap_or("PMI");
1495 let fresh = state.next_id(prefix);
1496 if let Some(object) = annotation.params.as_object_mut() {
1497 object.insert("id".into(), serde_json::Value::String(fresh));
1498 }
1499 annotations.push(annotation);
1500 }
1501 state.views.push(PmiView {
1502 id,
1503 name: view.name,
1504 camera: view.camera,
1505 display: view.display,
1506 annotations,
1507 });
1508 }
1509 let block = serde_json::to_value(&state).ok();
1510 self.history.set_pmi_block_no_undo(block);
1511 self.rerun_history();
1512 }
1513}
1514
1515fn pose_matrix(center: [f64; 3], translate: [f64; 3], rotate: &dyn Fn([f64; 3]) -> [f64; 3], scale: [f64; 3]) -> [f32; 16] {
1520 let columns = [rotate([1.0, 0.0, 0.0]), rotate([0.0, 1.0, 0.0]), rotate([0.0, 0.0, 1.0])];
1521 let linear: [[f64; 3]; 3] = std::array::from_fn(|j| columns[j].map(|v| v * scale[j]));
1522 let shifted: [f64; 3] = std::array::from_fn(|i| center[i] + translate[i] - (0..3).map(|j| linear[j][i] * center[j]).sum::<f64>());
1523 let mut m = [0.0f32; 16];
1524 for j in 0..3 {
1525 for i in 0..3 {
1526 m[j * 4 + i] = linear[j][i] as f32;
1527 }
1528 }
1529 m[12] = shifted[0] as f32;
1530 m[13] = shifted[1] as f32;
1531 m[14] = shifted[2] as f32;
1532 m[15] = 1.0;
1533 m
1534}
1535
1536use crate::view::mat4_mul_column_major as mat4_mul;
1538
1539fn reset_posed_geometry(display: &mut crate::scene::SolidDisplay, original: &crate::scene::SolidDisplay) {
1543 display.mesh.positions.clone_from(&original.mesh.positions);
1544 display.mesh.normals.clone_from(&original.mesh.normals);
1545 if display.edges.len() == original.edges.len() {
1546 for (edge, from) in display.edges.iter_mut().zip(&original.edges) {
1547 edge.polyline.clone_from(&from.polyline);
1548 }
1549 } else {
1550 display.edges.clone_from(&original.edges);
1551 }
1552 display.vertices.clone_from(&original.vertices);
1553 display.bbox = original.bbox;
1554 display.revision = original.revision;
1555 display.shape_revision = original.shape_revision;
1556 display.pose = original.pose;
1557}
1558
1559pub(super) fn transform_display(
1560 display: &mut crate::scene::SolidDisplay,
1561 center: [f64; 3],
1562 translate: [f64; 3],
1563 rotate_deg: [f64; 3],
1564 scale: [f64; 3],
1565) {
1566 let rotate_rad = rotate_deg.map(f64::to_radians);
1573 let rotate = |v: [f64; 3]| super::rotate_euler_xyz_f64(v, rotate_rad);
1574 let step = pose_matrix(center, translate, &rotate, scale);
1578 display.pose = if display.shape_revision == 0 {
1579 Some(step)
1580 } else {
1581 display.pose.map(|before| mat4_mul(&step, &before))
1582 };
1583 if display.shape_revision == 0 {
1584 display.shape_revision = display.revision;
1585 }
1586 display.revision = crate::scene::next_revision();
1587 let pose = |p: [f64; 3]| -> [f64; 3] {
1588 let local = [(p[0] - center[0]) * scale[0], (p[1] - center[1]) * scale[1], (p[2] - center[2]) * scale[2]];
1589 let rotated = rotate(local);
1590 [rotated[0] + center[0] + translate[0], rotated[1] + center[1] + translate[1], rotated[2] + center[2] + translate[2]]
1591 };
1592 let mut bbox = crate::camera::Aabb::empty();
1593 for position in &mut display.mesh.positions {
1594 let posed = pose([position[0] as f64, position[1] as f64, position[2] as f64]);
1595 *position = [posed[0] as f32, posed[1] as f32, posed[2] as f32];
1596 bbox.expand(posed);
1597 }
1598 for normal in &mut display.mesh.normals {
1599 let rotated = rotate(std::array::from_fn(|i| normal[i] as f64 / scale[i]));
1600 let length = rotated.iter().map(|v| v * v).sum::<f64>().sqrt().max(1e-20);
1601 *normal = rotated.map(|v| (v / length) as f32);
1602 }
1603 for edge in &mut display.edges {
1604 for point in &mut edge.polyline {
1605 let posed = pose([point[0] as f64, point[1] as f64, point[2] as f64]);
1606 *point = [posed[0] as f32, posed[1] as f32, posed[2] as f32];
1607 bbox.expand(posed);
1608 }
1609 }
1610 for vertex in &mut display.vertices {
1611 vertex.position = pose(vertex.position);
1612 bbox.expand(vertex.position);
1613 }
1614 if !bbox.is_empty() {
1615 display.bbox = bbox;
1616 }
1617}
1618