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
318 .solids()
319 .iter()
320 .filter(|solid| !solid.visible && !solid.is_sketch)
321 .map(|solid| solid.name.clone())
322 .collect()
323 }
324
325 pub fn pmi_capture_view(&mut self, name: Option<&str>) -> String {
329 let mut state = self.pmi_state();
330 let id = state.next_id("VIEW");
331 let name = name
332 .map(str::trim)
333 .filter(|n| !n.is_empty())
334 .map(String::from)
335 .unwrap_or_else(|| format!("View {}", state.views.len() + 1));
336 let was_active = self.pmi_active_view.is_some();
340 if was_active {
341 self.pmi_deactivate_view();
342 }
343 self.pmi_remember_modeling();
344 state.views.push(PmiView {
345 id: id.clone(),
346 name,
347 camera: Some(self.snapshot_camera()),
348 display: PmiDisplay::new(12.0, self.settings.wireframe, None),
349 annotations: Vec::new(),
350 });
351 state.set_view_baseline(&id, &self.hidden_solid_names());
352 self.write_pmi_state(state);
353 let _ = self.pmi_activate_view(&id);
354 id
355 }
356
357 pub fn pmi_rename_view(&mut self, id: &str, name: &str) -> Result<(), String> {
358 let mut state = self.pmi_state();
359 let view = state.find_view_mut(id).ok_or_else(|| format!("no PMI view '{id}'"))?;
360 let name = name.trim();
361 if name.is_empty() {
362 return Err("a view needs a name".into());
363 }
364 view.name = name.to_string();
365 self.write_pmi_state(state);
366 Ok(())
367 }
368
369 pub fn pmi_delete_view(&mut self, id: &str) -> Result<(), String> {
371 let mut state = self.pmi_state();
372 let before = state.views.len();
373 if self.pmi_active_view.as_deref() == Some(id) {
374 self.pmi_deactivate_view();
375 }
376 state.views.retain(|view| view.id != id);
377 if state.views.len() == before {
378 return Err(format!("no PMI view '{id}'"));
379 }
380 if let Some(open) = &self.pmi_open_annotation {
381 if state.find_annotation(open).is_none() {
382 self.pmi_open_annotation = None;
383 }
384 }
385 if self.pmi_open_view.as_deref() == Some(id) {
386 self.pmi_open_view = None;
387 }
388 self.write_pmi_state(state);
389 Ok(())
390 }
391
392 pub fn pmi_update_view_camera(&mut self, id: &str) -> Result<(), String> {
395 if let Some(step) = self.instructions_step_of_view(id) {
400 let shown = self.instructions_shown_step().as_deref() == Some(step.as_str());
401 self.instructions_capture_camera(&step)?;
402 if shown {
403 self.instructions_show_step(&step)?;
404 }
405 return Ok(());
406 }
407 let mut state = self.pmi_state();
408 let camera = self.snapshot_camera();
409 let view = state.find_view_mut(id).ok_or_else(|| format!("no PMI view '{id}'"))?;
410 if let Some(section) = view.display.section.as_mut() {
413 section.freeze(view.camera.as_ref());
414 }
415 view.camera = Some(camera);
416 self.write_pmi_state(state);
417 Ok(())
418 }
419
420 pub fn pmi_hide_selected(&mut self, view_id: Option<&str>) -> Result<String, String> {
427 let mut targets = self.selected_occurrences();
428 for solid in &self.emphasis.selected_solids {
429 if !targets.iter().any(|target| brep_kernel::entry_names_solid(target, solid)) {
430 targets.push(solid.clone());
431 }
432 }
433 if targets.is_empty() {
434 return Err("select the solids or components to hide".into());
435 }
436 let params = serde_json::json!({ "targets": targets }).to_string();
437 self.pmi_add_annotation(view_id, "hide", ¶ms)
438 }
439
440 pub fn pmi_set_view_display(&mut self, id: &str, patch: &PmiViewPatch) -> Result<(), String> {
442 self.patch_pmi_view(id, patch, None)
443 }
444
445 pub fn pmi_update_view(&mut self, id: &str, params_json: &str) -> Result<(), String> {
450 let params: serde_json::Value =
451 serde_json::from_str(params_json).map_err(|error| format!("view params: {error}"))?;
452 let state = self.pmi_state();
453 let view = state.find_view(id).ok_or_else(|| format!("no PMI view '{id}'"))?;
454 let name = params
455 .get("name")
456 .and_then(serde_json::Value::as_str)
457 .filter(|name| *name != view.name)
458 .map(String::from);
459 let text_size_pt = params
460 .get("textSizePt")
461 .and_then(serde_json::Value::as_f64)
462 .filter(|size| *size != view.display.text_size_pt);
463 let wireframe = params
464 .get("wireframe")
465 .and_then(serde_json::Value::as_bool)
466 .filter(|on| *on != view.display.wireframe);
467 let section = if params.get("sectionEnabled").is_some() {
468 let section = if params["sectionEnabled"].as_bool().unwrap_or(false) {
469 let axis = params["sectionAxis"].as_str().unwrap_or("View");
470 let offset = params["sectionOffset"].as_f64().unwrap_or(0.);
471 if !["View", "X", "Y", "Z", "Plane"].contains(&axis) || !offset.is_finite() { return Err("invalid section plane".into()); }
472 let prior = view.display.section.as_ref();
476 let mut section = brep_kernel::feature_pipeline::pmi::PmiSection {
477 axis: axis.into(),
478 offset,
479 flip: params["sectionFlip"].as_bool().unwrap_or(false),
480 origin: prior.and_then(|s| s.origin),
481 normal: prior.filter(|s| s.axis == axis).and_then(|s| s.normal),
482 plane: match params.get("sectionPlane") {
484 Some(serde_json::Value::String(name)) => Some(name.trim().to_string()).filter(|name| !name.is_empty()),
485 Some(serde_json::Value::Null) => None,
486 _ => prior.and_then(|s| s.plane.clone()),
487 },
488 exclude: match params.get("sectionExclude") {
489 Some(serde_json::Value::Array(names)) => names
490 .iter()
491 .filter_map(serde_json::Value::as_str)
492 .map(|name| name.trim().to_string())
493 .filter(|name| !name.is_empty())
494 .collect(),
495 Some(serde_json::Value::Null) => Vec::new(),
496 _ => prior.map(|s| s.exclude.clone()).unwrap_or_default(),
497 },
498 planes: section_planes_from(¶ms, prior)?,
499 };
500 section.freeze(view.camera.as_ref());
501 Some(section)
502 } else { None };
503 (section != view.display.section).then_some(section)
504 } else { None };
505 if name.is_none() && text_size_pt.is_none() && wireframe.is_none() && section.is_none() {
506 return Ok(());
507 }
508 let patch = PmiViewPatch { name, text_size_pt, wireframe, hidden: None, section };
509 self.patch_pmi_view(id, &patch, Some(&format!("pmi:view:{id}")))
510 }
511
512 #[allow(clippy::too_many_arguments)]
520 pub fn pmi_set_view_section(
521 &mut self,
522 id: &str,
523 enabled: bool,
524 axis: Option<&str>,
525 offset: Option<f64>,
526 flip: Option<bool>,
527 plane: Option<&str>,
528 exclude: Option<Vec<String>>,
529 planes: Option<Vec<brep_kernel::feature_pipeline::pmi::PmiSectionPlane>>,
530 ) -> Result<(), String> {
531 let state = self.pmi_state();
532 let view = state.find_view(id).ok_or_else(|| format!("no PMI view '{id}'"))?;
533 let prior = view.display.section.as_ref();
534 let axis = axis.or_else(|| plane.filter(|plane| !plane.trim().is_empty()).map(|_| "Plane"));
537 let mut params = serde_json::json!({
538 "sectionEnabled": enabled,
539 "sectionAxis": axis.map(str::to_string).or_else(|| prior.map(|s| s.axis.clone())).unwrap_or_else(|| "View".into()),
540 "sectionOffset": offset.or_else(|| prior.map(|s| s.offset)).unwrap_or(0.),
541 "sectionFlip": flip.or_else(|| prior.map(|s| s.flip)).unwrap_or(false),
542 });
543 if let Some(plane) = plane {
544 params["sectionPlane"] = plane.into();
545 }
546 if let Some(exclude) = exclude {
547 params["sectionExclude"] = exclude.into();
548 }
549 if let Some(planes) = planes {
550 params["sectionPlanes"] = serde_json::to_value(planes).map_err(|e| e.to_string())?;
551 }
552 self.history.break_coalescing();
553 let result = self.pmi_update_view(id, ¶ms.to_string());
554 self.history.break_coalescing();
555 result
556 }
557
558 pub fn pmi_set_view_open(&mut self, id: Option<&str>) {
562 match id {
563 Some(id) => {
564 if self.pmi_state().find_view(id).is_none() {
565 return;
566 }
567 if self.pmi_active_view.as_deref() != Some(id) {
568 let _ = self.pmi_activate_view(id);
569 }
570 self.open_pmi_view(id.to_string());
571 }
572 None => self.pmi_open_view = None,
573 }
574 self.refresh_pmi_overlay();
575 }
576
577 fn patch_pmi_view(&mut self, id: &str, patch: &PmiViewPatch, coalesce_key: Option<&str>) -> Result<(), String> {
578 let mut state = self.pmi_state();
579 let view = state.find_view_mut(id).ok_or_else(|| format!("no PMI view '{id}'"))?;
580 if let Some(name) = &patch.name {
581 let name = name.trim();
582 if name.is_empty() {
583 return Err("a view needs a name".into());
584 }
585 view.name = name.to_string();
586 }
587 if let Some(size) = patch.text_size_pt {
588 view.display.text_size_pt = brep_kernel::clamp_text_size(size);
589 }
590 if let Some(wireframe) = patch.wireframe {
591 view.display.wireframe = wireframe;
592 }
593 if let Some(section) = &patch.section { view.display.section = section.clone(); }
594 if let Some(hidden) = &patch.hidden {
595 let solids = self.view_hideable_names();
596 state.set_view_hidden(id, hidden, &solids);
597 }
598 let view = state.find_view(id).expect("patched above").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 view_hideable_names(&self) -> Vec<String> {
611 self.scene.solids().iter().filter(|solid| !solid.is_sketch).map(|solid| solid.name.clone()).collect()
612 }
613
614 pub(crate) fn pmi_record_visibility(&mut self, view_id: &str, names: &[String], visible: bool) -> bool {
622 let solids = self.view_hideable_names();
623 let names: Vec<String> = names.iter().filter(|name| solids.contains(name)).cloned().collect();
624 if names.is_empty() {
625 return false;
626 }
627 let mut state = self.pmi_state();
628 let changed = if visible {
629 state.show_in_view(view_id, &names, &solids)
630 } else {
631 state.hide_in_view(view_id, &names).is_some()
632 };
633 if !changed {
634 return false;
635 }
636 let Some(view) = state.find_view(view_id).cloned() else { return false };
637 self.write_pmi_state(state);
638 if self.pmi_active_view.as_deref() == Some(view_id) {
639 self.apply_view_display(&view);
640 self.refresh_pmi_overlay();
641 }
642 true
643 }
644
645 fn pmi_remember_modeling(&mut self) {
651 if self.pmi_active_view.is_none() || self.pmi_modeling.is_none() {
652 self.pmi_modeling = Some(PmiModelingSnapshot {
653 camera_json: self.camera_state_json(),
654 hidden: self.hidden_solid_names(),
655 wireframe: self.settings.wireframe,
656 });
657 }
658 }
659
660 pub fn pmi_enter_workbench(&mut self) {
663 self.pmi_remember_modeling();
664 }
665
666 pub fn pmi_leave_workbench(&mut self) {
671 self.pmi_deactivate_view();
672 self.pmi_modeling = None;
673 self.pmi_open_annotation = None;
674 self.pmi_open_view = None;
675 self.refresh_pmi_overlay();
676 }
677
678 fn apply_view_display(&mut self, view: &PmiView) {
683 let display = &view.display;
684 let names: Vec<String> = self.scene.solids().iter().map(|s| s.name.clone()).collect();
685 for name in names {
686 let hidden = view.hides(&name);
687 if let Some(original) = self.pmi_explode_originals.get_mut(&name).filter(|original| original.visible == hidden) {
688 original.visible = !hidden;
689 self.pmi_unposed_visibility += 1;
690 }
691 if hidden {
692 self.pmi_section_hidden.retain(|hidden| *hidden != name);
694 self.scene.set_visible(&name, false);
695 } else if !self.pmi_section_hidden.contains(&name) {
696 self.scene.set_visible(&name, true);
698 }
699 }
700 if self.settings.wireframe != display.wireframe {
701 self.settings.wireframe = display.wireframe;
702 self.settings_generation = self.settings_generation.wrapping_add(1);
703 }
704 self.dirty = true;
705 }
706
707 pub fn pmi_activate_view(&mut self, id: &str) -> Result<(), String> {
711 let state = self.pmi_state();
712 let view = state.find_view(id).ok_or_else(|| format!("no PMI view '{id}'"))?.clone();
713 if self.pmi_active_view.as_deref() != Some(id) {
714 let keep = self.pmi_active_view.is_some();
717 self.pmi_deactivate_view();
718 if !keep {
719 self.pmi_remember_modeling();
720 }
721 }
722 if self.pmi_modeling.is_none() {
723 self.pmi_remember_modeling();
724 }
725 if self.pmi_active_view.as_deref() != Some(id) {
726 self.explode_playback = Default::default();
728 }
729 self.pmi_active_view = Some(id.to_string());
730 if let Some(camera) = &view.camera {
731 let (kind, scale) = match camera.projection {
732 PmiProjection::Orthographic { half_height } => ("orthographic", half_height),
733 PmiProjection::Perspective { fov_y_deg } => ("perspective", fov_y_deg),
734 };
735 let json = serde_json::json!({
736 "kind": kind, "eye": camera.eye, "target": camera.target, "up": camera.up, "scale": scale,
737 })
738 .to_string();
739 let _ = self.apply_camera_state_json(&json);
740 self.controls_sync_after_camera_apply();
741 }
742 self.apply_view_display(&view);
743 self.pmi_apply_explode();
744 self.instructions_refresh_ghosts();
745 if let Some(open) = &self.pmi_open_annotation {
746 if !view.annotations.iter().any(|a| a.id() == open) {
747 self.pmi_open_annotation = None;
748 }
749 }
750 self.refresh_pmi_overlay();
751 Ok(())
752 }
753
754 pub fn pmi_deactivate_view(&mut self) {
757 if self.pmi_active_view.take().is_none() {
758 return;
759 }
760 self.instructions_refresh_ghosts();
762 if self.pmi_state().find_annotation(&self.transform_armed_feature()).is_some() { self.disarm_transform(); }
763 self.pmi_restore_explode();
764 if let Some(snapshot) = self.pmi_modeling.clone() {
765 let names: Vec<String> = self.scene.solids().iter().map(|s| s.name.clone()).collect();
766 for name in names {
767 let visible = !snapshot.hidden.contains(&name);
768 self.scene.set_visible(&name, visible);
769 }
770 if self.settings.wireframe != snapshot.wireframe {
771 self.settings.wireframe = snapshot.wireframe;
772 self.settings_generation = self.settings_generation.wrapping_add(1);
773 }
774 let _ = self.apply_camera_state_json(&snapshot.camera_json);
775 self.controls_sync_after_camera_apply();
776 }
777 self.pmi_open_annotation = None;
778 self.dirty = true;
779 self.refresh_pmi_overlay();
780 }
781
782 pub(super) fn controls_sync_after_camera_apply(&mut self) {
784 self.dirty = true;
788 }
789
790 pub(crate) fn pmi_apply_explode(&mut self) {
795 let Some(active) = self.pmi_active_view.clone() else {
796 self.pmi_restore_explode();
797 return;
798 };
799 let report = self.pmi_report.as_ref().and_then(|report| report.view(&active));
800 let state = self.pmi_state_shared();
801 let view = state.find_view(&active);
802 let planes = view.map(|v| v.section_planes_in(report)).unwrap_or_default();
804 let section = (!planes.is_empty()).then_some(());
805 let exclude = view.and_then(|v| v.display.section.clone());
806 let poses: Vec<(Vec<String>, [f64; 3], [f64; 3], [f64; 3], [f64; 3])> = report
807 .map(|view| {
808 view.annotations
809 .iter()
810 .filter(|row| row.enabled && row.status == PmiStatus::Ok)
811 .filter(|row| matches!(row.geometry, PmiGeometry::Explode { .. }))
812 .enumerate()
813 .filter_map(|(step, row)| match super::exploded_ops::explode_at(&row.geometry, self.explode_fraction(step)) {
815 PmiGeometry::Explode { solids, translate, rotate_deg, scale, center, .. } => {
816 Some((solids, translate, rotate_deg, scale, center))
817 }
818 _ => None,
819 })
820 .collect()
821 })
822 .unwrap_or_default();
823 let mut poses = poses;
825 poses.extend(self.instructions_insertion_poses());
826 if section.is_none() && self.pmi_section_hidden.is_empty() && !self.pmi_explode_originals.is_empty() {
827 let wanted: std::collections::HashSet<&String> = poses.iter().flat_map(|pose| pose.0.iter()).collect();
833 let stale: Vec<String> = self.pmi_explode_originals.keys().filter(|name| !wanted.contains(name)).cloned().collect();
834 for name in stale {
835 if let (Some(original), Some(display)) = (self.pmi_explode_originals.remove(&name), self.scene.solid_mut(&name)) {
836 *display = original;
837 }
838 }
839 for name in wanted {
840 match self.pmi_explode_originals.get(name) {
841 Some(original) => {
842 if let Some(display) = self.scene.solid_mut(name) {
843 reset_posed_geometry(display, original);
844 }
845 }
846 None => {
847 if let Some(display) = self.scene.solid(name) {
848 self.pmi_explode_originals.insert(name.clone(), display.clone());
849 }
850 }
851 }
852 }
853 } else {
854 self.pmi_restore_explode();
855 for (solids, ..) in &poses {
856 for name in solids {
857 let Some(display) = self.scene.solid(name) else { continue };
858 self.pmi_explode_originals.entry(name.clone()).or_insert_with(|| display.clone());
859 }
860 }
861 }
862 for (solids, translate, rotate_deg, scale, center) in poses {
863 for name in solids {
864 if let Some(display) = self.scene.solid_mut(&name) {
865 transform_display(display, center, translate, rotate_deg, scale);
866 }
867 }
868 }
869 if section.is_some() {
870 use super::pmi_section::{side_of_all, Side};
871 let names: Vec<(String, Side)> = self
879 .scene
880 .solids()
881 .iter()
882 .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) }))
883 .collect();
884 for (name, side) in names {
885 match side {
886 Side::Kept => {}
887 Side::Removed => {
888 if let Some(display) = self.scene.solid(&name).filter(|display| display.visible) {
889 self.pmi_explode_originals.entry(name.clone()).or_insert_with(|| display.clone());
890 self.scene.set_visible(&name, false);
891 self.pmi_section_hidden.push(name);
892 }
893 }
894 Side::Cut => {
895 if let Some(display) = self.scene.solid(&name) { self.pmi_explode_originals.entry(name.clone()).or_insert_with(|| display.clone()); }
896 if let Some(display) = self.scene.solid_mut(&name) { super::pmi_section::clip_display_all(display, &planes); }
897 }
898 }
899 }
900 }
901 if !self.pmi_explode_originals.is_empty() || !self.pmi_section_hidden.is_empty() {
902 self.dirty = true;
903 }
904 }
905
906 pub(crate) fn pmi_restore_explode(&mut self) {
908 for name in std::mem::take(&mut self.pmi_section_hidden) {
909 self.scene.set_visible(&name, true);
910 }
911 let originals = std::mem::take(&mut self.pmi_explode_originals);
912 for (name, original) in originals {
913 if let Some(display) = self.scene.solid_mut(&name) {
914 *display = original;
915 }
916 }
917 self.dirty = true;
918 }
919
920 pub(crate) fn pmi_after_apply(&mut self) {
923 self.pmi_balloon_stale = None;
925 self.pmi_restore_explode();
929 if let Some(active) = self.pmi_active_view.clone() {
930 let view = self.pmi_state().find_view(&active).cloned();
932 match view {
933 None => {
934 self.pmi_active_view = None;
935 self.pmi_open_annotation = None;
936 }
937 Some(view) => {
938 self.apply_view_display(&view);
942 self.pmi_apply_explode();
943 }
944 }
945 }
946 self.instructions_refresh_ghosts();
947 if let Some(open) = &self.pmi_open_annotation {
948 if self.pmi_state().find_annotation(open).is_none() {
949 self.pmi_open_annotation = None;
950 }
951 }
952 if let Some(open) = &self.pmi_open_view {
954 if self.pmi_state().find_view(open).is_none() {
955 self.pmi_open_view = None;
956 }
957 }
958 self.refresh_pmi_overlay();
959 }
960
961 pub fn pmi_add_annotation(
968 &mut self,
969 view_id: Option<&str>,
970 type_id: &str,
971 params_json: &str,
972 ) -> Result<String, String> {
973 let def = brep_kernel::pmi_type(type_id).ok_or_else(|| format!("unknown PMI annotation type '{type_id}'"))?;
974 let mut state = self.pmi_state();
975 let view_id = view_id
976 .map(String::from)
977 .or_else(|| self.pmi_active_view.clone())
978 .ok_or_else(|| "no active PMI view — capture or activate a view first".to_string())?;
979 let id = state.next_id(def.short_name);
980 let mut params: serde_json::Value = serde_json::from_str(params_json).unwrap_or_else(|_| serde_json::json!({}));
981 if !params.is_object() {
982 params = serde_json::json!({});
983 }
984 let schema = (def.schema)();
986 if let (Some(fields), Some(object)) = (
987 schema.get("inputParamsSchema").and_then(serde_json::Value::as_object),
988 params.as_object_mut(),
989 ) {
990 for (key, spec) in fields {
991 if !object.contains_key(key) {
992 if let Some(default) = spec.get("default_value") {
993 if !default.is_null() {
994 object.insert(key.clone(), default.clone());
995 }
996 }
997 }
998 }
999 object.insert("id".into(), serde_json::Value::String(id.clone()));
1000 if type_id == "datum" {
1001 let letter = object.get("letter").and_then(serde_json::Value::as_str).unwrap_or("").trim().to_string();
1002 if letter.is_empty() {
1003 if let Some(next) = state.next_datum_letter() {
1004 object.insert("letter".into(), serde_json::Value::String(next));
1005 }
1006 }
1007 }
1008 }
1009 let view = state.find_view_mut(&view_id).ok_or_else(|| format!("no PMI view '{view_id}'"))?;
1010 view.annotations.push(PmiAnnotation {
1011 plugin_replay: None,
1012 persistent_data: serde_json::Value::Null,
1013 kind: type_id.to_string(),
1014 enabled: true,
1015 params,
1016 label_world: None,
1017 });
1018 self.open_pmi_annotation(id.clone());
1019 self.write_pmi_state(state);
1020 Ok(id)
1021 }
1022
1023 pub fn pmi_update_annotation(&mut self, id: &str, params_json: &str) -> Result<(), String> {
1025 if self.pmi_state().find_annotation(id).is_some_and(|(_, a)| a.kind.contains('/')) {
1026 let params = serde_json::from_str(params_json).map_err(|e| format!("annotation params: {e}"))?;
1027 return self.plugin_update_annotation(id, params).map(|_| ());
1028 }
1029 self.pmi_update_annotation_no_rerun(id, params_json)?;
1030 self.rerun_history();
1031 Ok(())
1032 }
1033
1034 pub(crate) fn pmi_update_annotation_no_rerun(&mut self, id: &str, params_json: &str) -> Result<(), String> {
1038 let mut state = self.pmi_state();
1039 let annotation = state.find_annotation_mut(id).ok_or_else(|| format!("no PMI annotation '{id}'"))?;
1040 if annotation.kind.contains('/') {
1041 return Err("plugin annotation edits require the worker transaction path".into());
1042 }
1043 let mut params: serde_json::Value =
1044 serde_json::from_str(params_json).map_err(|error| format!("annotation params: {error}"))?;
1045 if let Some(object) = params.as_object_mut() {
1046 object.insert("id".into(), serde_json::Value::String(id.to_string()));
1047 }
1048 annotation.params = params;
1049 let block = serde_json::to_value(&state).ok();
1050 self.history.set_pmi_block(block, Some(&format!("pmi:params:{id}")));
1051 Ok(())
1052 }
1053
1054 pub fn pmi_remove_annotation(&mut self, id: &str) -> Result<(), String> {
1055 if self.pmi_state().find_annotation(id).is_some_and(|(_, a)| a.kind.contains('/')) {
1056 return self.plugin_delete_annotation(id).map(|_| ());
1057 }
1058 let mut state = self.pmi_state();
1059 let Some((view_index, index)) = state.locate_annotation(id) else {
1060 return Err(format!("no PMI annotation '{id}'"));
1061 };
1062 state.views[view_index].annotations.remove(index);
1063 if self.pmi_open_annotation.as_deref() == Some(id) {
1064 self.pmi_open_annotation = None;
1065 }
1066 self.write_pmi_state(state);
1067 Ok(())
1068 }
1069
1070 pub fn pmi_set_annotation_enabled(&mut self, id: &str, enabled: bool) -> Result<(), String> {
1071 if self.pmi_state().find_annotation(id).is_some_and(|(_, a)| a.kind.contains('/')) {
1072 return self.plugin_set_annotation_enabled(id, enabled).map(|_| ());
1073 }
1074 let mut state = self.pmi_state();
1075 let annotation = state.find_annotation_mut(id).ok_or_else(|| format!("no PMI annotation '{id}'"))?;
1076 if annotation.enabled == enabled {
1077 return Ok(());
1078 }
1079 annotation.enabled = enabled;
1080 self.write_pmi_state(state);
1081 Ok(())
1082 }
1083
1084 pub fn pmi_move_annotation(&mut self, id: &str, index: usize) -> Result<(), String> {
1086 let mut state = self.pmi_state();
1087 let Some((view_index, from)) = state.locate_annotation(id) else {
1088 return Err(format!("no PMI annotation '{id}'"));
1089 };
1090 let annotations = &mut state.views[view_index].annotations;
1091 let annotation = annotations.remove(from);
1092 let to = index.min(annotations.len());
1093 annotations.insert(to, annotation);
1094 self.write_pmi_state(state);
1095 Ok(())
1096 }
1097
1098 pub fn pmi_move_annotation_to_view(&mut self, id: &str, view_id: &str) -> Result<(), String> {
1100 let mut state = self.pmi_state();
1101 let Some((view_index, from)) = state.locate_annotation(id) else {
1102 return Err(format!("no PMI annotation '{id}'"));
1103 };
1104 if state.find_view(view_id).is_none() {
1105 return Err(format!("no PMI view '{view_id}'"));
1106 }
1107 let annotation = state.views[view_index].annotations.remove(from);
1108 state.find_view_mut(view_id).expect("checked").annotations.push(annotation);
1109 self.write_pmi_state(state);
1110 Ok(())
1111 }
1112
1113 pub fn pmi_set_annotation_open(&mut self, id: Option<&str>) {
1120 match id {
1121 Some(id) => {
1122 let state = self.pmi_state();
1123 if let Some((view, _)) = state.find_annotation(id) {
1124 let view_id = view.id.clone();
1125 if self.pmi_active_view.as_deref() != Some(view_id.as_str()) {
1126 let _ = self.pmi_activate_view(&view_id);
1127 }
1128 self.open_pmi_annotation(id.to_string());
1129 }
1130 }
1131 None => {
1132 if self.pmi_open_annotation.as_deref() == Some(self.transform_armed_feature().as_str()) { self.disarm_transform(); }
1133 self.pmi_open_annotation = None;
1134 },
1135 }
1136 self.refresh_pmi_overlay();
1137 }
1138
1139 pub fn pmi_set_label_world(&mut self, id: &str, world: [f64; 3]) -> Result<(), String> {
1146 let mut state = self.pmi_state();
1147 let annotation = state.find_annotation_mut(id).ok_or_else(|| format!("no PMI annotation '{id}'"))?;
1148 annotation.label_world = Some(world);
1149 let block = serde_json::to_value(&state).ok();
1150 self.history.set_pmi_block(block, Some(&format!("pmi:label:{id}")));
1151 if let Some(report) = self.pmi_report.as_mut() {
1152 if let Some(row) = report.annotation_mut(id) {
1153 row.label_world = world;
1154 if let PmiGeometry::Note { position } = &mut row.geometry {
1155 *position = world;
1156 }
1157 }
1158 }
1159 self.pmi_reproject_balloon_head(id);
1160 self.refresh_pmi_overlay();
1161 Ok(())
1162 }
1163
1164 pub(crate) fn pmi_reproject_balloon_head(&mut self, id: &str) {
1174 if let Some(missing) = self.pmi_balloon_head_now(id) {
1175 self.pmi_balloon_stale = Some(id.to_string());
1176 self.request_exact_solids(missing.iter().map(String::as_str));
1177 self.pmi_refresh_stale_balloon();
1178 }
1179 }
1180
1181 pub(crate) fn pmi_refresh_stale_balloon(&mut self) {
1183 let Some(id) = self.pmi_balloon_stale.take() else {
1184 return;
1185 };
1186 if self.pmi_balloon_head_now(&id).is_some() {
1187 self.pmi_balloon_stale = Some(id);
1189 } else {
1190 self.refresh_pmi_overlay();
1191 }
1192 }
1193
1194 pub(super) fn pmi_balloon_head_now(&mut self, id: &str) -> Option<Vec<String>> {
1197 let (mut anchor, bubble) = self
1198 .pmi_report
1199 .as_ref()
1200 .and_then(|report| report.annotation(id))
1201 .and_then(|row| match &row.geometry {
1202 PmiGeometry::Leader { balloon: true, anchor: Some(anchor), .. } => Some((anchor.clone(), row.label_world)),
1203 _ => None,
1204 })?;
1205 anchor.camera = Some(self.snapshot_camera());
1206 let mut missing = Vec::new();
1207 let head = brep_kernel::pmi_balloon_head(&anchor, bubble, |name| self.scene.exact_solid(name), &mut missing);
1208 if !missing.is_empty() {
1209 return Some(missing);
1210 }
1211 if let Ok(Some(head)) = head {
1214 if let Some(row) = self.pmi_report.as_mut().and_then(|report| report.annotation_mut(id)) {
1215 if let PmiGeometry::Leader { targets, anchor: recipe, .. } = &mut row.geometry {
1216 *recipe = Some(anchor);
1217 if let Some(first) = targets.first_mut() {
1218 *first = head;
1219 }
1220 }
1221 }
1222 }
1223 None
1224 }
1225
1226 pub fn pmi_label_drag_to(&mut self, id: &str, x: f64, y: f64) {
1230 let playback = self.explode_playback_poses();
1233 let Some((current, plane)) = self
1234 .pmi_report
1235 .as_ref()
1236 .and_then(|r| r.annotation(id))
1237 .map(|r| (playback.as_ref().map_or(r.label_world, |poses| poses.shown(r, r.label_world)), r.plane))
1238 else {
1239 return;
1240 };
1241 let ray = self.camera.pick_ray(x, y);
1242 let plane = plane.unwrap_or_else(|| {
1246 let (_, _, view) = self.camera.basis();
1247 brep_kernel::PmiPlane {
1248 origin: current,
1249 normal: view,
1250 x_axis: [0.0; 3],
1251 }
1252 });
1253 let Some(world) = plane.hit(ray.origin, ray.dir) else {
1254 return;
1255 };
1256 let world = match (&playback, self.pmi_report.as_ref().and_then(|r| r.annotation(id))) {
1257 (Some(poses), Some(row)) => poses.full(row, world),
1258 _ => world,
1259 };
1260 let _ = self.pmi_set_label_world(id, world);
1261 }
1262
1263 pub fn pmi_label_drag_end(&mut self) {
1265 self.history.break_coalescing();
1266 }
1267
1268 pub fn pmi_hover(&mut self, id: &str) {
1270 self.pmi_label_hover_active = true;
1271 if self.pmi_hovered.as_deref() == Some(id) {
1272 return;
1273 }
1274 let references: Vec<String> = self
1275 .pmi_report
1276 .as_ref()
1277 .and_then(|report| report.annotation(id))
1278 .map(|row| row.references.clone())
1279 .unwrap_or_default();
1280 let mut solids: Vec<String> = Vec::new();
1281 let mut faces: Vec<String> = Vec::new();
1282 let mut edges: Vec<String> = Vec::new();
1283 for reference in &references {
1284 if let Some(at) = reference.find('@') {
1285 solids.push(reference[..at].to_string());
1286 } else if self.scene_has_face(reference) {
1287 faces.push(reference.clone());
1288 } else if self.scene_has_edge(reference) {
1289 edges.push(reference.clone());
1290 } else if self.scene.solid(reference).is_some() {
1291 solids.push(reference.clone());
1292 } else {
1293 let prefix = format!("{reference}:");
1294 solids.extend(self.scene.solids().iter().filter(|s| s.name.starts_with(&prefix)).map(|s| s.name.clone()));
1295 }
1296 }
1297 self.clear_hover();
1298 self.emphasis.hovered_solids.extend(solids);
1299 self.emphasis.hovered_faces.extend(faces);
1300 self.emphasis.hovered_edges.extend(edges);
1301 self.emphasis.generation = self.emphasis.generation.wrapping_add(1);
1302 self.pmi_hovered = Some(id.to_string());
1303 self.dirty = true;
1304 }
1305
1306 pub fn pmi_hover_end(&mut self) {
1307 if self.pmi_hovered.take().is_some() {
1308 self.clear_hover();
1309 self.dirty = true;
1310 }
1311 }
1312
1313 pub fn take_pmi_label_hover(&mut self) -> bool {
1316 std::mem::take(&mut self.pmi_label_hover_active)
1317 }
1318
1319 pub fn pmi_label_clicked(&mut self, id: &str) {
1322 self.pmi_select_annotation(id);
1323 }
1324
1325 pub fn pmi_label_double_clicked(&mut self, id: &str) {
1328 self.pmi_set_annotation_open(Some(id));
1329 }
1330
1331 pub fn pmi_select_annotation(&mut self, id: &str) -> bool {
1336 let state = self.pmi_state();
1337 let Some((view, _)) = state.find_annotation(id) else {
1338 return false;
1339 };
1340 if self.pmi_active_view.as_deref() != Some(view.id.as_str()) {
1341 let _ = self.pmi_activate_view(&view.id);
1342 }
1343 self.pmi_selected_view = None;
1344 self.pmi_selected_annotation = Some(id.to_string());
1345 self.refresh_pmi_overlay();
1346 true
1347 }
1348
1349 pub fn pmi_select_view(&mut self, id: &str) -> bool {
1355 if self.pmi_state().find_view(id).is_none() {
1356 return false;
1357 }
1358 if self.pmi_selected_annotation.take().is_some() {
1359 self.refresh_pmi_overlay();
1360 }
1361 self.pmi_selected_view = Some(id.to_string());
1362 self.dirty = true;
1363 true
1364 }
1365
1366 pub fn begin_ref_select_for_pmi(
1370 &mut self,
1371 id: &str,
1372 path: Vec<String>,
1373 label: String,
1374 filter: Vec<String>,
1375 multiple: bool,
1376 seed_names: Vec<String>,
1377 ) {
1378 let restore_index = self.history.rollback();
1379 self.selection_filter = SelectionFilter::from_ref_filter(&filter);
1380 self.ref_select = Some(RefSelectState {
1381 feature_id: id.to_string(),
1382 path,
1383 label,
1384 filter,
1385 multiple,
1386 names: seed_names,
1387 restore_index,
1388 target: RefSelectTarget::Pmi,
1389 picks: 0,
1390 claimed: false,
1391 finish_requested: false,
1392 });
1393 self.sync_ref_select_emphasis();
1394 }
1395
1396 fn explode_target_names(&self, id: &str, annotation: &PmiAnnotation) -> Vec<String> {
1402 if let Some(field) = self.ref_select_field() {
1403 if field.target == RefSelectTarget::Pmi && field.subject == id && field.path == ["targets"] {
1404 return field.names;
1405 }
1406 }
1407 annotation.references("targets")
1408 }
1409
1410 fn scene_names_center(&self, names: &[String]) -> Option<[f64; 3]> {
1414 let mut low = [f64::INFINITY; 3];
1415 let mut high = [f64::NEG_INFINITY; 3];
1416 let mut any = false;
1417 for name in names {
1418 let prefix = format!("{name}:");
1419 for solid in self.scene.solids() {
1420 if solid.name == *name || solid.name.starts_with(&prefix) {
1421 for axis in 0..3 {
1422 low[axis] = low[axis].min(solid.bbox.min[axis]);
1423 high[axis] = high[axis].max(solid.bbox.max[axis]);
1424 }
1425 any = true;
1426 }
1427 }
1428 }
1429 any.then(|| std::array::from_fn(|axis| (low[axis] + high[axis]) * 0.5))
1430 }
1431
1432 pub(super) fn pmi_explode_live_center(&self, id: &str) -> Option<[f64; 3]> {
1442 let state = self.pmi_state_shared();
1443 let (view, annotation) = state.find_annotation(id)?;
1444 if annotation.kind != "explode" || self.pmi_active_view.as_deref() != Some(view.id.as_str()) {
1445 return None;
1446 }
1447 let names = self.explode_target_names(id, annotation);
1448 self.scene_names_center(&names)
1449 }
1450
1451 pub(crate) fn pmi_commit_refs(&mut self, id: &str, path: &[String], names: &[String], multiple: bool) {
1454 if self.pmi_state().find_view(id).is_some() {
1455 match path.first().map(String::as_str) {
1456 Some("sectionExclude") => self.pmi_commit_section_exclude(id, names),
1457 _ => self.pmi_commit_section_plane(id, names.first().map(String::as_str).unwrap_or("")),
1458 }
1459 self.history.break_coalescing();
1460 return;
1461 }
1462 let state = self.pmi_state();
1463 let Some((_, annotation)) = state.find_annotation(id) else {
1464 self.push_notice(format!("unknown PMI annotation '{id}'"));
1465 return;
1466 };
1467 let mut params = annotation.params.clone();
1468 let value = if multiple {
1469 serde_json::Value::Array(names.iter().cloned().map(serde_json::Value::String).collect())
1470 } else {
1471 serde_json::Value::String(names.first().cloned().unwrap_or_default())
1472 };
1473 super::selection_ux::set_json_at(&mut params, path, value);
1474 if let Err(error) = self.pmi_update_annotation_no_rerun(id, ¶ms.to_string()) {
1475 self.push_notice(format!("PMI update failed: {error}"));
1476 }
1477 self.history.break_coalescing();
1481 }
1482
1483 fn pmi_commit_section_plane(&mut self, id: &str, name: &str) {
1487 let mut state = self.pmi_state();
1488 let Some(view) = state.find_view_mut(id) else { return };
1489 let section = view.display.section.get_or_insert_with(|| brep_kernel::feature_pipeline::pmi::PmiSection {
1490 axis: "Plane".into(),
1491 offset: 0.,
1492 flip: false,
1493 origin: None,
1494 normal: None,
1495 plane: None,
1496 exclude: Vec::new(),
1497 planes: Vec::new(),
1498 });
1499 section.axis = "Plane".into();
1500 section.plane = Some(name.trim().to_string()).filter(|name| !name.is_empty());
1501 section.freeze(None);
1502 let block = serde_json::to_value(&state).ok();
1503 self.history.break_coalescing();
1505 self.history.set_pmi_block(block, Some(&format!("pmi:view:{id}")));
1506 }
1507
1508 fn pmi_commit_section_exclude(&mut self, id: &str, names: &[String]) {
1512 let mut state = self.pmi_state();
1513 let Some(section) = state.find_view_mut(id).and_then(|view| view.display.section.as_mut()) else { return };
1514 section.exclude = names.iter().map(|name| name.trim().to_string()).filter(|name| !name.is_empty()).collect();
1515 let block = serde_json::to_value(&state).ok();
1516 self.history.break_coalescing();
1517 self.history.set_pmi_block(block, Some(&format!("pmi:view:{id}")));
1518 }
1519
1520 pub(crate) fn pmi_merge_imported(&mut self, lifted: PmiState) {
1526 let mut state = self.pmi_state();
1527 for view in lifted.views {
1528 let id = state.next_id("VIEW");
1529 let mut annotations = Vec::with_capacity(view.annotations.len());
1530 for mut annotation in view.annotations {
1531 let prefix = brep_kernel::pmi_type(&annotation.kind).map(|def| def.short_name).unwrap_or("PMI");
1532 let fresh = state.next_id(prefix);
1533 if let Some(object) = annotation.params.as_object_mut() {
1534 object.insert("id".into(), serde_json::Value::String(fresh));
1535 }
1536 annotations.push(annotation);
1537 }
1538 state.views.push(PmiView {
1539 id,
1540 name: view.name,
1541 camera: view.camera,
1542 display: view.display,
1543 annotations,
1544 });
1545 }
1546 let block = serde_json::to_value(&state).ok();
1547 self.history.set_pmi_block_no_undo(block);
1548 self.rerun_history();
1549 }
1550}
1551
1552fn pose_matrix(center: [f64; 3], translate: [f64; 3], rotate: &dyn Fn([f64; 3]) -> [f64; 3], scale: [f64; 3]) -> [f32; 16] {
1557 let columns = [rotate([1.0, 0.0, 0.0]), rotate([0.0, 1.0, 0.0]), rotate([0.0, 0.0, 1.0])];
1558 let linear: [[f64; 3]; 3] = std::array::from_fn(|j| columns[j].map(|v| v * scale[j]));
1559 let shifted: [f64; 3] = std::array::from_fn(|i| center[i] + translate[i] - (0..3).map(|j| linear[j][i] * center[j]).sum::<f64>());
1560 let mut m = [0.0f32; 16];
1561 for j in 0..3 {
1562 for i in 0..3 {
1563 m[j * 4 + i] = linear[j][i] as f32;
1564 }
1565 }
1566 m[12] = shifted[0] as f32;
1567 m[13] = shifted[1] as f32;
1568 m[14] = shifted[2] as f32;
1569 m[15] = 1.0;
1570 m
1571}
1572
1573use crate::view::mat4_mul_column_major as mat4_mul;
1575
1576fn reset_posed_geometry(display: &mut crate::scene::SolidDisplay, original: &crate::scene::SolidDisplay) {
1580 display.mesh.positions.clone_from(&original.mesh.positions);
1581 display.mesh.normals.clone_from(&original.mesh.normals);
1582 if display.edges.len() == original.edges.len() {
1583 for (edge, from) in display.edges.iter_mut().zip(&original.edges) {
1584 edge.polyline.clone_from(&from.polyline);
1585 }
1586 } else {
1587 display.edges.clone_from(&original.edges);
1588 }
1589 display.vertices.clone_from(&original.vertices);
1590 display.bbox = original.bbox;
1591 display.revision = original.revision;
1592 display.shape_revision = original.shape_revision;
1593 display.pose = original.pose;
1594}
1595
1596pub(super) fn transform_display(
1597 display: &mut crate::scene::SolidDisplay,
1598 center: [f64; 3],
1599 translate: [f64; 3],
1600 rotate_deg: [f64; 3],
1601 scale: [f64; 3],
1602) {
1603 let rotate_rad = rotate_deg.map(f64::to_radians);
1610 let rotate = |v: [f64; 3]| super::rotate_euler_xyz_f64(v, rotate_rad);
1611 let step = pose_matrix(center, translate, &rotate, scale);
1615 display.pose = if display.shape_revision == 0 {
1616 Some(step)
1617 } else {
1618 display.pose.map(|before| mat4_mul(&step, &before))
1619 };
1620 if display.shape_revision == 0 {
1621 display.shape_revision = display.revision;
1622 }
1623 display.revision = crate::scene::next_revision();
1624 let pose = |p: [f64; 3]| -> [f64; 3] {
1625 let local = [(p[0] - center[0]) * scale[0], (p[1] - center[1]) * scale[1], (p[2] - center[2]) * scale[2]];
1626 let rotated = rotate(local);
1627 [rotated[0] + center[0] + translate[0], rotated[1] + center[1] + translate[1], rotated[2] + center[2] + translate[2]]
1628 };
1629 let mut bbox = crate::camera::Aabb::empty();
1630 for position in &mut display.mesh.positions {
1631 let posed = pose([position[0] as f64, position[1] as f64, position[2] as f64]);
1632 *position = [posed[0] as f32, posed[1] as f32, posed[2] as f32];
1633 bbox.expand(posed);
1634 }
1635 for normal in &mut display.mesh.normals {
1636 let rotated = rotate(std::array::from_fn(|i| normal[i] as f64 / scale[i]));
1637 let length = rotated.iter().map(|v| v * v).sum::<f64>().sqrt().max(1e-20);
1638 *normal = rotated.map(|v| (v / length) as f32);
1639 }
1640 for edge in &mut display.edges {
1641 for point in &mut edge.polyline {
1642 let posed = pose([point[0] as f64, point[1] as f64, point[2] as f64]);
1643 *point = [posed[0] as f32, posed[1] as f32, posed[2] as f32];
1644 bbox.expand(posed);
1645 }
1646 }
1647 for vertex in &mut display.vertices {
1648 vertex.position = pose(vertex.position);
1649 bbox.expand(vertex.position);
1650 }
1651 if !bbox.is_empty() {
1652 display.bbox = bbox;
1653 }
1654}
1655