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 "updateVisibility": { "type": "button", "label": "Update visibility from the scene" }
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 let state = self.pmi_state();
266 serde_json::json!({
267 "views": state.views,
268 "idCounter": state.id_counter,
269 "activeView": self.pmi_active_view,
270 "openAnnotation": self.pmi_open_annotation,
271 "openView": self.pmi_open_view,
272 "selectedAnnotation": self.pmi_selected_annotation(),
273 "selectedView": self.pmi_selected_view(),
274 "entered": self.pmi_modeling.is_some(),
275 "datums": state.datum_letters(),
276 "report": self.pmi_report,
277 })
278 .to_string()
279 }
280
281 pub(super) fn write_pmi_state(&mut self, state: PmiState) -> String {
287 let block = if state.is_empty() { None } else { serde_json::to_value(&state).ok() };
288 self.history.set_pmi_block(block, None);
289 self.rerun_history()
290 }
291
292 pub(super) fn snapshot_camera(&self) -> PmiCamera {
296 let camera = &self.camera;
297 PmiCamera {
298 eye: camera.eye,
299 target: camera.target,
300 up: camera.up,
301 projection: match camera.projection {
302 Projection::Orthographic { half_height } => PmiProjection::Orthographic { half_height },
303 Projection::Perspective { fov_y_deg } => PmiProjection::Perspective { fov_y_deg },
304 },
305 viewport: [camera.width, camera.height],
306 }
307 }
308
309 fn hidden_solid_names(&self) -> Vec<String> {
311 self.scene
312 .solids()
313 .iter()
314 .filter(|solid| !solid.visible)
315 .map(|solid| solid.name.clone())
316 .collect()
317 }
318
319 pub fn pmi_capture_view(&mut self, name: Option<&str>) -> String {
322 let mut state = self.pmi_state();
323 let id = state.next_id("VIEW");
324 let name = name
325 .map(str::trim)
326 .filter(|n| !n.is_empty())
327 .map(String::from)
328 .unwrap_or_else(|| format!("View {}", state.views.len() + 1));
329 let was_active = self.pmi_active_view.is_some();
333 if was_active {
334 self.pmi_deactivate_view();
335 }
336 self.pmi_remember_modeling();
337 state.views.push(PmiView {
338 id: id.clone(),
339 name,
340 camera: Some(self.snapshot_camera()),
341 display: PmiDisplay {
342 text_size_pt: 12.0,
343 wireframe: self.settings.wireframe,
344 hidden: self.hidden_solid_names(),
345 section: None,
346 },
347 annotations: Vec::new(),
348 });
349 self.write_pmi_state(state);
350 let _ = self.pmi_activate_view(&id);
351 id
352 }
353
354 pub fn pmi_rename_view(&mut self, id: &str, name: &str) -> Result<(), String> {
355 let mut state = self.pmi_state();
356 let view = state.find_view_mut(id).ok_or_else(|| format!("no PMI view '{id}'"))?;
357 let name = name.trim();
358 if name.is_empty() {
359 return Err("a view needs a name".into());
360 }
361 view.name = name.to_string();
362 self.write_pmi_state(state);
363 Ok(())
364 }
365
366 pub fn pmi_delete_view(&mut self, id: &str) -> Result<(), String> {
368 let mut state = self.pmi_state();
369 let before = state.views.len();
370 if self.pmi_active_view.as_deref() == Some(id) {
371 self.pmi_deactivate_view();
372 }
373 state.views.retain(|view| view.id != id);
374 if state.views.len() == before {
375 return Err(format!("no PMI view '{id}'"));
376 }
377 if let Some(open) = &self.pmi_open_annotation {
378 if state.find_annotation(open).is_none() {
379 self.pmi_open_annotation = None;
380 }
381 }
382 if self.pmi_open_view.as_deref() == Some(id) {
383 self.pmi_open_view = None;
384 }
385 self.write_pmi_state(state);
386 Ok(())
387 }
388
389 pub fn pmi_update_view_camera(&mut self, id: &str) -> Result<(), String> {
392 if let Some(step) = self.instructions_step_of_view(id) {
397 let shown = self.instructions_shown_step().as_deref() == Some(step.as_str());
398 self.instructions_capture_camera(&step)?;
399 if shown {
400 self.instructions_show_step(&step)?;
401 }
402 return Ok(());
403 }
404 let mut state = self.pmi_state();
405 let camera = self.snapshot_camera();
406 let view = state.find_view_mut(id).ok_or_else(|| format!("no PMI view '{id}'"))?;
407 if let Some(section) = view.display.section.as_mut() {
410 section.freeze(view.camera.as_ref());
411 }
412 view.camera = Some(camera);
413 self.write_pmi_state(state);
414 Ok(())
415 }
416
417 pub fn pmi_update_view_visibility(&mut self, id: &str) -> Result<(), String> {
420 let hidden = self.hidden_solid_names();
421 self.pmi_set_view_display(id, &PmiViewPatch { hidden: Some(hidden), ..Default::default() })
422 }
423
424 pub fn pmi_set_view_display(&mut self, id: &str, patch: &PmiViewPatch) -> Result<(), String> {
426 self.patch_pmi_view(id, patch, None)
427 }
428
429 pub fn pmi_update_view(&mut self, id: &str, params_json: &str) -> Result<(), String> {
434 let params: serde_json::Value =
435 serde_json::from_str(params_json).map_err(|error| format!("view params: {error}"))?;
436 let state = self.pmi_state();
437 let view = state.find_view(id).ok_or_else(|| format!("no PMI view '{id}'"))?;
438 let name = params
439 .get("name")
440 .and_then(serde_json::Value::as_str)
441 .filter(|name| *name != view.name)
442 .map(String::from);
443 let text_size_pt = params
444 .get("textSizePt")
445 .and_then(serde_json::Value::as_f64)
446 .filter(|size| *size != view.display.text_size_pt);
447 let wireframe = params
448 .get("wireframe")
449 .and_then(serde_json::Value::as_bool)
450 .filter(|on| *on != view.display.wireframe);
451 let section = if params.get("sectionEnabled").is_some() {
452 let section = if params["sectionEnabled"].as_bool().unwrap_or(false) {
453 let axis = params["sectionAxis"].as_str().unwrap_or("View");
454 let offset = params["sectionOffset"].as_f64().unwrap_or(0.);
455 if !["View", "X", "Y", "Z", "Plane"].contains(&axis) || !offset.is_finite() { return Err("invalid section plane".into()); }
456 let prior = view.display.section.as_ref();
460 let mut section = brep_kernel::feature_pipeline::pmi::PmiSection {
461 axis: axis.into(),
462 offset,
463 flip: params["sectionFlip"].as_bool().unwrap_or(false),
464 origin: prior.and_then(|s| s.origin),
465 normal: prior.filter(|s| s.axis == axis).and_then(|s| s.normal),
466 plane: match params.get("sectionPlane") {
468 Some(serde_json::Value::String(name)) => Some(name.trim().to_string()).filter(|name| !name.is_empty()),
469 Some(serde_json::Value::Null) => None,
470 _ => prior.and_then(|s| s.plane.clone()),
471 },
472 exclude: match params.get("sectionExclude") {
473 Some(serde_json::Value::Array(names)) => names
474 .iter()
475 .filter_map(serde_json::Value::as_str)
476 .map(|name| name.trim().to_string())
477 .filter(|name| !name.is_empty())
478 .collect(),
479 Some(serde_json::Value::Null) => Vec::new(),
480 _ => prior.map(|s| s.exclude.clone()).unwrap_or_default(),
481 },
482 planes: section_planes_from(¶ms, prior)?,
483 };
484 section.freeze(view.camera.as_ref());
485 Some(section)
486 } else { None };
487 (section != view.display.section).then_some(section)
488 } else { None };
489 if name.is_none() && text_size_pt.is_none() && wireframe.is_none() && section.is_none() {
490 return Ok(());
491 }
492 let patch = PmiViewPatch { name, text_size_pt, wireframe, hidden: None, section };
493 self.patch_pmi_view(id, &patch, Some(&format!("pmi:view:{id}")))
494 }
495
496 #[allow(clippy::too_many_arguments)]
504 pub fn pmi_set_view_section(
505 &mut self,
506 id: &str,
507 enabled: bool,
508 axis: Option<&str>,
509 offset: Option<f64>,
510 flip: Option<bool>,
511 plane: Option<&str>,
512 exclude: Option<Vec<String>>,
513 planes: Option<Vec<brep_kernel::feature_pipeline::pmi::PmiSectionPlane>>,
514 ) -> Result<(), String> {
515 let state = self.pmi_state();
516 let view = state.find_view(id).ok_or_else(|| format!("no PMI view '{id}'"))?;
517 let prior = view.display.section.as_ref();
518 let axis = axis.or_else(|| plane.filter(|plane| !plane.trim().is_empty()).map(|_| "Plane"));
521 let mut params = serde_json::json!({
522 "sectionEnabled": enabled,
523 "sectionAxis": axis.map(str::to_string).or_else(|| prior.map(|s| s.axis.clone())).unwrap_or_else(|| "View".into()),
524 "sectionOffset": offset.or_else(|| prior.map(|s| s.offset)).unwrap_or(0.),
525 "sectionFlip": flip.or_else(|| prior.map(|s| s.flip)).unwrap_or(false),
526 });
527 if let Some(plane) = plane {
528 params["sectionPlane"] = plane.into();
529 }
530 if let Some(exclude) = exclude {
531 params["sectionExclude"] = exclude.into();
532 }
533 if let Some(planes) = planes {
534 params["sectionPlanes"] = serde_json::to_value(planes).map_err(|e| e.to_string())?;
535 }
536 self.history.break_coalescing();
537 let result = self.pmi_update_view(id, ¶ms.to_string());
538 self.history.break_coalescing();
539 result
540 }
541
542 pub fn pmi_set_view_open(&mut self, id: Option<&str>) {
546 match id {
547 Some(id) => {
548 if self.pmi_state().find_view(id).is_none() {
549 return;
550 }
551 if self.pmi_active_view.as_deref() != Some(id) {
552 let _ = self.pmi_activate_view(id);
553 }
554 self.open_pmi_view(id.to_string());
555 }
556 None => self.pmi_open_view = None,
557 }
558 self.refresh_pmi_overlay();
559 }
560
561 fn patch_pmi_view(&mut self, id: &str, patch: &PmiViewPatch, coalesce_key: Option<&str>) -> Result<(), String> {
562 let mut state = self.pmi_state();
563 let view = state.find_view_mut(id).ok_or_else(|| format!("no PMI view '{id}'"))?;
564 if let Some(name) = &patch.name {
565 let name = name.trim();
566 if name.is_empty() {
567 return Err("a view needs a name".into());
568 }
569 view.name = name.to_string();
570 }
571 if let Some(size) = patch.text_size_pt {
572 view.display.text_size_pt = brep_kernel::clamp_text_size(size);
573 }
574 if let Some(wireframe) = patch.wireframe {
575 view.display.wireframe = wireframe;
576 }
577 if let Some(hidden) = &patch.hidden {
578 view.display.hidden = hidden.clone();
579 }
580 if let Some(section) = &patch.section { view.display.section = section.clone(); }
581 let display = view.display.clone();
582 let block = if state.is_empty() { None } else { serde_json::to_value(&state).ok() };
583 self.history.set_pmi_block(block, coalesce_key);
584 self.rerun_history();
585 if self.pmi_active_view.as_deref() == Some(id) {
586 self.apply_view_display(&display);
587 }
588 Ok(())
589 }
590
591 fn pmi_remember_modeling(&mut self) {
597 if self.pmi_active_view.is_none() || self.pmi_modeling.is_none() {
598 self.pmi_modeling = Some(PmiModelingSnapshot {
599 camera_json: self.camera_state_json(),
600 hidden: self.hidden_solid_names(),
601 wireframe: self.settings.wireframe,
602 });
603 }
604 }
605
606 pub fn pmi_enter_workbench(&mut self) {
609 self.pmi_remember_modeling();
610 }
611
612 pub fn pmi_leave_workbench(&mut self) {
617 self.pmi_deactivate_view();
618 self.pmi_modeling = None;
619 self.pmi_open_annotation = None;
620 self.pmi_open_view = None;
621 self.refresh_pmi_overlay();
622 }
623
624 fn apply_view_display(&mut self, display: &PmiDisplay) {
625 let names: Vec<String> = self.scene.solids().iter().map(|s| s.name.clone()).collect();
626 for name in names {
627 let hidden = display.hidden.contains(&name);
628 if let Some(original) = self.pmi_explode_originals.get_mut(&name).filter(|original| original.visible == hidden) {
629 original.visible = !hidden;
630 self.pmi_unposed_visibility += 1;
631 }
632 if hidden {
633 self.pmi_section_hidden.retain(|hidden| *hidden != name);
635 self.scene.set_visible(&name, false);
636 } else if !self.pmi_section_hidden.contains(&name) {
637 self.scene.set_visible(&name, true);
639 }
640 }
641 if self.settings.wireframe != display.wireframe {
642 self.settings.wireframe = display.wireframe;
643 self.settings_generation = self.settings_generation.wrapping_add(1);
644 }
645 self.dirty = true;
646 }
647
648 pub fn pmi_activate_view(&mut self, id: &str) -> Result<(), String> {
652 let state = self.pmi_state();
653 let view = state.find_view(id).ok_or_else(|| format!("no PMI view '{id}'"))?.clone();
654 if self.pmi_active_view.as_deref() != Some(id) {
655 let keep = self.pmi_active_view.is_some();
658 self.pmi_deactivate_view();
659 if !keep {
660 self.pmi_remember_modeling();
661 }
662 }
663 if self.pmi_modeling.is_none() {
664 self.pmi_remember_modeling();
665 }
666 if self.pmi_active_view.as_deref() != Some(id) {
667 self.explode_playback = Default::default();
669 }
670 self.pmi_active_view = Some(id.to_string());
671 if let Some(camera) = &view.camera {
672 let (kind, scale) = match camera.projection {
673 PmiProjection::Orthographic { half_height } => ("orthographic", half_height),
674 PmiProjection::Perspective { fov_y_deg } => ("perspective", fov_y_deg),
675 };
676 let json = serde_json::json!({
677 "kind": kind, "eye": camera.eye, "target": camera.target, "up": camera.up, "scale": scale,
678 })
679 .to_string();
680 let _ = self.apply_camera_state_json(&json);
681 self.controls_sync_after_camera_apply();
682 }
683 self.apply_view_display(&view.display);
684 self.pmi_apply_explode();
685 self.instructions_refresh_ghosts();
686 if let Some(open) = &self.pmi_open_annotation {
687 if !view.annotations.iter().any(|a| a.id() == open) {
688 self.pmi_open_annotation = None;
689 }
690 }
691 self.refresh_pmi_overlay();
692 Ok(())
693 }
694
695 pub fn pmi_deactivate_view(&mut self) {
698 if self.pmi_active_view.take().is_none() {
699 return;
700 }
701 self.instructions_refresh_ghosts();
703 if self.pmi_state().find_annotation(&self.transform_armed_feature()).is_some() { self.disarm_transform(); }
704 self.pmi_restore_explode();
705 if let Some(snapshot) = self.pmi_modeling.clone() {
706 let names: Vec<String> = self.scene.solids().iter().map(|s| s.name.clone()).collect();
707 for name in names {
708 let visible = !snapshot.hidden.contains(&name);
709 self.scene.set_visible(&name, visible);
710 }
711 if self.settings.wireframe != snapshot.wireframe {
712 self.settings.wireframe = snapshot.wireframe;
713 self.settings_generation = self.settings_generation.wrapping_add(1);
714 }
715 let _ = self.apply_camera_state_json(&snapshot.camera_json);
716 self.controls_sync_after_camera_apply();
717 }
718 self.pmi_open_annotation = None;
719 self.dirty = true;
720 self.refresh_pmi_overlay();
721 }
722
723 pub(super) fn controls_sync_after_camera_apply(&mut self) {
725 self.dirty = true;
729 }
730
731 pub(crate) fn pmi_apply_explode(&mut self) {
736 let Some(active) = self.pmi_active_view.clone() else {
737 self.pmi_restore_explode();
738 return;
739 };
740 let report = self.pmi_report.as_ref().and_then(|report| report.view(&active));
741 let state = self.pmi_state_shared();
742 let view = state.find_view(&active);
743 let planes = view.map(|v| v.section_planes_in(report)).unwrap_or_default();
745 let section = (!planes.is_empty()).then_some(());
746 let exclude = view.and_then(|v| v.display.section.clone());
747 let poses: Vec<(Vec<String>, [f64; 3], [f64; 3], [f64; 3], [f64; 3])> = report
748 .map(|view| {
749 view.annotations
750 .iter()
751 .filter(|row| row.enabled && row.status == PmiStatus::Ok)
752 .filter(|row| matches!(row.geometry, PmiGeometry::Explode { .. }))
753 .enumerate()
754 .filter_map(|(step, row)| match super::exploded_ops::explode_at(&row.geometry, self.explode_fraction(step)) {
756 PmiGeometry::Explode { solids, translate, rotate_deg, scale, center, .. } => {
757 Some((solids, translate, rotate_deg, scale, center))
758 }
759 _ => None,
760 })
761 .collect()
762 })
763 .unwrap_or_default();
764 let mut poses = poses;
766 poses.extend(self.instructions_insertion_poses());
767 if section.is_none() && self.pmi_section_hidden.is_empty() && !self.pmi_explode_originals.is_empty() {
768 let wanted: std::collections::HashSet<&String> = poses.iter().flat_map(|pose| pose.0.iter()).collect();
774 let stale: Vec<String> = self.pmi_explode_originals.keys().filter(|name| !wanted.contains(name)).cloned().collect();
775 for name in stale {
776 if let (Some(original), Some(display)) = (self.pmi_explode_originals.remove(&name), self.scene.solid_mut(&name)) {
777 *display = original;
778 }
779 }
780 for name in wanted {
781 match self.pmi_explode_originals.get(name) {
782 Some(original) => {
783 if let Some(display) = self.scene.solid_mut(name) {
784 reset_posed_geometry(display, original);
785 }
786 }
787 None => {
788 if let Some(display) = self.scene.solid(name) {
789 self.pmi_explode_originals.insert(name.clone(), display.clone());
790 }
791 }
792 }
793 }
794 } else {
795 self.pmi_restore_explode();
796 for (solids, ..) in &poses {
797 for name in solids {
798 let Some(display) = self.scene.solid(name) else { continue };
799 self.pmi_explode_originals.entry(name.clone()).or_insert_with(|| display.clone());
800 }
801 }
802 }
803 for (solids, translate, rotate_deg, scale, center) in poses {
804 for name in solids {
805 if let Some(display) = self.scene.solid_mut(&name) {
806 transform_display(display, center, translate, rotate_deg, scale);
807 }
808 }
809 }
810 if section.is_some() {
811 use super::pmi_section::{side_of_all, Side};
812 let names: Vec<(String, Side)> = self
820 .scene
821 .solids()
822 .iter()
823 .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) }))
824 .collect();
825 for (name, side) in names {
826 match side {
827 Side::Kept => {}
828 Side::Removed => {
829 if let Some(display) = self.scene.solid(&name).filter(|display| display.visible) {
830 self.pmi_explode_originals.entry(name.clone()).or_insert_with(|| display.clone());
831 self.scene.set_visible(&name, false);
832 self.pmi_section_hidden.push(name);
833 }
834 }
835 Side::Cut => {
836 if let Some(display) = self.scene.solid(&name) { self.pmi_explode_originals.entry(name.clone()).or_insert_with(|| display.clone()); }
837 if let Some(display) = self.scene.solid_mut(&name) { super::pmi_section::clip_display_all(display, &planes); }
838 }
839 }
840 }
841 }
842 if !self.pmi_explode_originals.is_empty() || !self.pmi_section_hidden.is_empty() {
843 self.dirty = true;
844 }
845 }
846
847 pub(crate) fn pmi_restore_explode(&mut self) {
849 for name in std::mem::take(&mut self.pmi_section_hidden) {
850 self.scene.set_visible(&name, true);
851 }
852 let originals = std::mem::take(&mut self.pmi_explode_originals);
853 for (name, original) in originals {
854 if let Some(display) = self.scene.solid_mut(&name) {
855 *display = original;
856 }
857 }
858 self.dirty = true;
859 }
860
861 pub(crate) fn pmi_after_apply(&mut self) {
864 self.pmi_balloon_stale = None;
866 self.pmi_restore_explode();
870 if let Some(active) = self.pmi_active_view.clone() {
871 let exists = self.pmi_state().find_view(&active).is_some();
873 if !exists {
874 self.pmi_active_view = None;
875 self.pmi_open_annotation = None;
876 } else {
877 self.pmi_apply_explode();
878 }
879 }
880 self.instructions_refresh_ghosts();
881 if let Some(open) = &self.pmi_open_annotation {
882 if self.pmi_state().find_annotation(open).is_none() {
883 self.pmi_open_annotation = None;
884 }
885 }
886 if let Some(open) = &self.pmi_open_view {
888 if self.pmi_state().find_view(open).is_none() {
889 self.pmi_open_view = None;
890 }
891 }
892 self.refresh_pmi_overlay();
893 }
894
895 pub fn pmi_add_annotation(
902 &mut self,
903 view_id: Option<&str>,
904 type_id: &str,
905 params_json: &str,
906 ) -> Result<String, String> {
907 let def = brep_kernel::pmi_type(type_id).ok_or_else(|| format!("unknown PMI annotation type '{type_id}'"))?;
908 let mut state = self.pmi_state();
909 let view_id = view_id
910 .map(String::from)
911 .or_else(|| self.pmi_active_view.clone())
912 .ok_or_else(|| "no active PMI view — capture or activate a view first".to_string())?;
913 let id = state.next_id(def.short_name);
914 let mut params: serde_json::Value = serde_json::from_str(params_json).unwrap_or_else(|_| serde_json::json!({}));
915 if !params.is_object() {
916 params = serde_json::json!({});
917 }
918 let schema = (def.schema)();
920 if let (Some(fields), Some(object)) = (
921 schema.get("inputParamsSchema").and_then(serde_json::Value::as_object),
922 params.as_object_mut(),
923 ) {
924 for (key, spec) in fields {
925 if !object.contains_key(key) {
926 if let Some(default) = spec.get("default_value") {
927 if !default.is_null() {
928 object.insert(key.clone(), default.clone());
929 }
930 }
931 }
932 }
933 object.insert("id".into(), serde_json::Value::String(id.clone()));
934 if type_id == "datum" {
935 let letter = object.get("letter").and_then(serde_json::Value::as_str).unwrap_or("").trim().to_string();
936 if letter.is_empty() {
937 if let Some(next) = state.next_datum_letter() {
938 object.insert("letter".into(), serde_json::Value::String(next));
939 }
940 }
941 }
942 }
943 let view = state.find_view_mut(&view_id).ok_or_else(|| format!("no PMI view '{view_id}'"))?;
944 view.annotations.push(PmiAnnotation {
945 plugin_replay: None,
946 persistent_data: serde_json::Value::Null,
947 kind: type_id.to_string(),
948 enabled: true,
949 params,
950 label_world: None,
951 });
952 self.open_pmi_annotation(id.clone());
953 self.write_pmi_state(state);
954 Ok(id)
955 }
956
957 pub fn pmi_update_annotation(&mut self, id: &str, params_json: &str) -> Result<(), String> {
959 if self.pmi_state().find_annotation(id).is_some_and(|(_, a)| a.kind.contains('/')) {
960 let params = serde_json::from_str(params_json).map_err(|e| format!("annotation params: {e}"))?;
961 return self.plugin_update_annotation(id, params).map(|_| ());
962 }
963 self.pmi_update_annotation_no_rerun(id, params_json)?;
964 self.rerun_history();
965 Ok(())
966 }
967
968 pub(crate) fn pmi_update_annotation_no_rerun(&mut self, id: &str, params_json: &str) -> Result<(), String> {
972 let mut state = self.pmi_state();
973 let annotation = state.find_annotation_mut(id).ok_or_else(|| format!("no PMI annotation '{id}'"))?;
974 if annotation.kind.contains('/') {
975 return Err("plugin annotation edits require the worker transaction path".into());
976 }
977 let mut params: serde_json::Value =
978 serde_json::from_str(params_json).map_err(|error| format!("annotation params: {error}"))?;
979 if let Some(object) = params.as_object_mut() {
980 object.insert("id".into(), serde_json::Value::String(id.to_string()));
981 }
982 annotation.params = params;
983 let block = serde_json::to_value(&state).ok();
984 self.history.set_pmi_block(block, Some(&format!("pmi:params:{id}")));
985 Ok(())
986 }
987
988 pub fn pmi_remove_annotation(&mut self, id: &str) -> Result<(), String> {
989 if self.pmi_state().find_annotation(id).is_some_and(|(_, a)| a.kind.contains('/')) {
990 return self.plugin_delete_annotation(id).map(|_| ());
991 }
992 let mut state = self.pmi_state();
993 let Some((view_index, index)) = state.locate_annotation(id) else {
994 return Err(format!("no PMI annotation '{id}'"));
995 };
996 state.views[view_index].annotations.remove(index);
997 if self.pmi_open_annotation.as_deref() == Some(id) {
998 self.pmi_open_annotation = None;
999 }
1000 self.write_pmi_state(state);
1001 Ok(())
1002 }
1003
1004 pub fn pmi_set_annotation_enabled(&mut self, id: &str, enabled: bool) -> Result<(), String> {
1005 if self.pmi_state().find_annotation(id).is_some_and(|(_, a)| a.kind.contains('/')) {
1006 return self.plugin_set_annotation_enabled(id, enabled).map(|_| ());
1007 }
1008 let mut state = self.pmi_state();
1009 let annotation = state.find_annotation_mut(id).ok_or_else(|| format!("no PMI annotation '{id}'"))?;
1010 if annotation.enabled == enabled {
1011 return Ok(());
1012 }
1013 annotation.enabled = enabled;
1014 self.write_pmi_state(state);
1015 Ok(())
1016 }
1017
1018 pub fn pmi_move_annotation(&mut self, id: &str, index: usize) -> Result<(), String> {
1020 let mut state = self.pmi_state();
1021 let Some((view_index, from)) = state.locate_annotation(id) else {
1022 return Err(format!("no PMI annotation '{id}'"));
1023 };
1024 let annotations = &mut state.views[view_index].annotations;
1025 let annotation = annotations.remove(from);
1026 let to = index.min(annotations.len());
1027 annotations.insert(to, annotation);
1028 self.write_pmi_state(state);
1029 Ok(())
1030 }
1031
1032 pub fn pmi_move_annotation_to_view(&mut self, id: &str, view_id: &str) -> Result<(), String> {
1034 let mut state = self.pmi_state();
1035 let Some((view_index, from)) = state.locate_annotation(id) else {
1036 return Err(format!("no PMI annotation '{id}'"));
1037 };
1038 if state.find_view(view_id).is_none() {
1039 return Err(format!("no PMI view '{view_id}'"));
1040 }
1041 let annotation = state.views[view_index].annotations.remove(from);
1042 state.find_view_mut(view_id).expect("checked").annotations.push(annotation);
1043 self.write_pmi_state(state);
1044 Ok(())
1045 }
1046
1047 pub fn pmi_set_annotation_open(&mut self, id: Option<&str>) {
1054 match id {
1055 Some(id) => {
1056 let state = self.pmi_state();
1057 if let Some((view, _)) = state.find_annotation(id) {
1058 let view_id = view.id.clone();
1059 if self.pmi_active_view.as_deref() != Some(view_id.as_str()) {
1060 let _ = self.pmi_activate_view(&view_id);
1061 }
1062 self.open_pmi_annotation(id.to_string());
1063 }
1064 }
1065 None => {
1066 if self.pmi_open_annotation.as_deref() == Some(self.transform_armed_feature().as_str()) { self.disarm_transform(); }
1067 self.pmi_open_annotation = None;
1068 },
1069 }
1070 self.refresh_pmi_overlay();
1071 }
1072
1073 pub fn pmi_set_label_world(&mut self, id: &str, world: [f64; 3]) -> Result<(), String> {
1080 let mut state = self.pmi_state();
1081 let annotation = state.find_annotation_mut(id).ok_or_else(|| format!("no PMI annotation '{id}'"))?;
1082 annotation.label_world = Some(world);
1083 let block = serde_json::to_value(&state).ok();
1084 self.history.set_pmi_block(block, Some(&format!("pmi:label:{id}")));
1085 if let Some(report) = self.pmi_report.as_mut() {
1086 if let Some(row) = report.annotation_mut(id) {
1087 row.label_world = world;
1088 if let PmiGeometry::Note { position } = &mut row.geometry {
1089 *position = world;
1090 }
1091 }
1092 }
1093 self.pmi_reproject_balloon_head(id);
1094 self.refresh_pmi_overlay();
1095 Ok(())
1096 }
1097
1098 pub(crate) fn pmi_reproject_balloon_head(&mut self, id: &str) {
1108 if let Some(missing) = self.pmi_balloon_head_now(id) {
1109 self.pmi_balloon_stale = Some(id.to_string());
1110 self.request_exact_solids(missing.iter().map(String::as_str));
1111 self.pmi_refresh_stale_balloon();
1112 }
1113 }
1114
1115 pub(crate) fn pmi_refresh_stale_balloon(&mut self) {
1117 let Some(id) = self.pmi_balloon_stale.take() else {
1118 return;
1119 };
1120 if self.pmi_balloon_head_now(&id).is_some() {
1121 self.pmi_balloon_stale = Some(id);
1123 } else {
1124 self.refresh_pmi_overlay();
1125 }
1126 }
1127
1128 pub(super) fn pmi_balloon_head_now(&mut self, id: &str) -> Option<Vec<String>> {
1131 let (mut anchor, bubble) = self
1132 .pmi_report
1133 .as_ref()
1134 .and_then(|report| report.annotation(id))
1135 .and_then(|row| match &row.geometry {
1136 PmiGeometry::Leader { balloon: true, anchor: Some(anchor), .. } => Some((anchor.clone(), row.label_world)),
1137 _ => None,
1138 })?;
1139 anchor.camera = Some(self.snapshot_camera());
1140 let mut missing = Vec::new();
1141 let head = brep_kernel::pmi_balloon_head(&anchor, bubble, |name| self.scene.exact_solid(name), &mut missing);
1142 if !missing.is_empty() {
1143 return Some(missing);
1144 }
1145 if let Ok(Some(head)) = head {
1148 if let Some(row) = self.pmi_report.as_mut().and_then(|report| report.annotation_mut(id)) {
1149 if let PmiGeometry::Leader { targets, anchor: recipe, .. } = &mut row.geometry {
1150 *recipe = Some(anchor);
1151 if let Some(first) = targets.first_mut() {
1152 *first = head;
1153 }
1154 }
1155 }
1156 }
1157 None
1158 }
1159
1160 pub fn pmi_label_drag_to(&mut self, id: &str, x: f64, y: f64) {
1164 let playback = self.explode_playback_poses();
1167 let Some((current, plane)) = self
1168 .pmi_report
1169 .as_ref()
1170 .and_then(|r| r.annotation(id))
1171 .map(|r| (playback.as_ref().map_or(r.label_world, |poses| poses.shown(r, r.label_world)), r.plane))
1172 else {
1173 return;
1174 };
1175 let ray = self.camera.pick_ray(x, y);
1176 let plane = plane.unwrap_or_else(|| {
1180 let (_, _, view) = self.camera.basis();
1181 brep_kernel::PmiPlane {
1182 origin: current,
1183 normal: view,
1184 x_axis: [0.0; 3],
1185 }
1186 });
1187 let Some(world) = plane.hit(ray.origin, ray.dir) else {
1188 return;
1189 };
1190 let world = match (&playback, self.pmi_report.as_ref().and_then(|r| r.annotation(id))) {
1191 (Some(poses), Some(row)) => poses.full(row, world),
1192 _ => world,
1193 };
1194 let _ = self.pmi_set_label_world(id, world);
1195 }
1196
1197 pub fn pmi_label_drag_end(&mut self) {
1199 self.history.break_coalescing();
1200 }
1201
1202 pub fn pmi_hover(&mut self, id: &str) {
1204 self.pmi_label_hover_active = true;
1205 if self.pmi_hovered.as_deref() == Some(id) {
1206 return;
1207 }
1208 let references: Vec<String> = self
1209 .pmi_report
1210 .as_ref()
1211 .and_then(|report| report.annotation(id))
1212 .map(|row| row.references.clone())
1213 .unwrap_or_default();
1214 let mut solids: Vec<String> = Vec::new();
1215 let mut faces: Vec<String> = Vec::new();
1216 let mut edges: Vec<String> = Vec::new();
1217 for reference in &references {
1218 if let Some(at) = reference.find('@') {
1219 solids.push(reference[..at].to_string());
1220 } else if self.scene_has_face(reference) {
1221 faces.push(reference.clone());
1222 } else if self.scene_has_edge(reference) {
1223 edges.push(reference.clone());
1224 } else if self.scene.solid(reference).is_some() {
1225 solids.push(reference.clone());
1226 } else {
1227 let prefix = format!("{reference}:");
1228 solids.extend(self.scene.solids().iter().filter(|s| s.name.starts_with(&prefix)).map(|s| s.name.clone()));
1229 }
1230 }
1231 self.clear_hover();
1232 self.emphasis.hovered_solids.extend(solids);
1233 self.emphasis.hovered_faces.extend(faces);
1234 self.emphasis.hovered_edges.extend(edges);
1235 self.emphasis.generation = self.emphasis.generation.wrapping_add(1);
1236 self.pmi_hovered = Some(id.to_string());
1237 self.dirty = true;
1238 }
1239
1240 pub fn pmi_hover_end(&mut self) {
1241 if self.pmi_hovered.take().is_some() {
1242 self.clear_hover();
1243 self.dirty = true;
1244 }
1245 }
1246
1247 pub fn take_pmi_label_hover(&mut self) -> bool {
1250 std::mem::take(&mut self.pmi_label_hover_active)
1251 }
1252
1253 pub fn pmi_label_clicked(&mut self, id: &str) {
1256 self.pmi_select_annotation(id);
1257 }
1258
1259 pub fn pmi_label_double_clicked(&mut self, id: &str) {
1262 self.pmi_set_annotation_open(Some(id));
1263 }
1264
1265 pub fn pmi_select_annotation(&mut self, id: &str) -> bool {
1270 let state = self.pmi_state();
1271 let Some((view, _)) = state.find_annotation(id) else {
1272 return false;
1273 };
1274 if self.pmi_active_view.as_deref() != Some(view.id.as_str()) {
1275 let _ = self.pmi_activate_view(&view.id);
1276 }
1277 self.pmi_selected_view = None;
1278 self.pmi_selected_annotation = Some(id.to_string());
1279 self.refresh_pmi_overlay();
1280 true
1281 }
1282
1283 pub fn pmi_select_view(&mut self, id: &str) -> bool {
1289 if self.pmi_state().find_view(id).is_none() {
1290 return false;
1291 }
1292 if self.pmi_selected_annotation.take().is_some() {
1293 self.refresh_pmi_overlay();
1294 }
1295 self.pmi_selected_view = Some(id.to_string());
1296 self.dirty = true;
1297 true
1298 }
1299
1300 pub fn begin_ref_select_for_pmi(
1304 &mut self,
1305 id: &str,
1306 path: Vec<String>,
1307 label: String,
1308 filter: Vec<String>,
1309 multiple: bool,
1310 seed_names: Vec<String>,
1311 ) {
1312 let restore_index = self.history.rollback();
1313 self.selection_filter = SelectionFilter::from_ref_filter(&filter);
1314 self.ref_select = Some(RefSelectState {
1315 feature_id: id.to_string(),
1316 path,
1317 label,
1318 filter,
1319 multiple,
1320 names: seed_names,
1321 restore_index,
1322 target: RefSelectTarget::Pmi,
1323 picks: 0,
1324 claimed: false,
1325 finish_requested: false,
1326 });
1327 self.sync_ref_select_emphasis();
1328 }
1329
1330 fn explode_target_names(&self, id: &str, annotation: &PmiAnnotation) -> Vec<String> {
1336 if let Some(field) = self.ref_select_field() {
1337 if field.target == RefSelectTarget::Pmi && field.subject == id && field.path == ["targets"] {
1338 return field.names;
1339 }
1340 }
1341 annotation.references("targets")
1342 }
1343
1344 fn scene_names_center(&self, names: &[String]) -> Option<[f64; 3]> {
1348 let mut low = [f64::INFINITY; 3];
1349 let mut high = [f64::NEG_INFINITY; 3];
1350 let mut any = false;
1351 for name in names {
1352 let prefix = format!("{name}:");
1353 for solid in self.scene.solids() {
1354 if solid.name == *name || solid.name.starts_with(&prefix) {
1355 for axis in 0..3 {
1356 low[axis] = low[axis].min(solid.bbox.min[axis]);
1357 high[axis] = high[axis].max(solid.bbox.max[axis]);
1358 }
1359 any = true;
1360 }
1361 }
1362 }
1363 any.then(|| std::array::from_fn(|axis| (low[axis] + high[axis]) * 0.5))
1364 }
1365
1366 pub(super) fn pmi_explode_live_center(&self, id: &str) -> Option<[f64; 3]> {
1376 let state = self.pmi_state_shared();
1377 let (view, annotation) = state.find_annotation(id)?;
1378 if annotation.kind != "explode" || self.pmi_active_view.as_deref() != Some(view.id.as_str()) {
1379 return None;
1380 }
1381 let names = self.explode_target_names(id, annotation);
1382 self.scene_names_center(&names)
1383 }
1384
1385 pub(crate) fn pmi_commit_refs(&mut self, id: &str, path: &[String], names: &[String], multiple: bool) {
1388 if self.pmi_state().find_view(id).is_some() {
1389 match path.first().map(String::as_str) {
1390 Some("sectionExclude") => self.pmi_commit_section_exclude(id, names),
1391 _ => self.pmi_commit_section_plane(id, names.first().map(String::as_str).unwrap_or("")),
1392 }
1393 self.history.break_coalescing();
1394 return;
1395 }
1396 let state = self.pmi_state();
1397 let Some((_, annotation)) = state.find_annotation(id) else {
1398 self.push_notice(format!("unknown PMI annotation '{id}'"));
1399 return;
1400 };
1401 let mut params = annotation.params.clone();
1402 let value = if multiple {
1403 serde_json::Value::Array(names.iter().cloned().map(serde_json::Value::String).collect())
1404 } else {
1405 serde_json::Value::String(names.first().cloned().unwrap_or_default())
1406 };
1407 super::selection_ux::set_json_at(&mut params, path, value);
1408 if let Err(error) = self.pmi_update_annotation_no_rerun(id, ¶ms.to_string()) {
1409 self.push_notice(format!("PMI update failed: {error}"));
1410 }
1411 self.history.break_coalescing();
1415 }
1416
1417 fn pmi_commit_section_plane(&mut self, id: &str, name: &str) {
1421 let mut state = self.pmi_state();
1422 let Some(view) = state.find_view_mut(id) else { return };
1423 let section = view.display.section.get_or_insert_with(|| brep_kernel::feature_pipeline::pmi::PmiSection {
1424 axis: "Plane".into(),
1425 offset: 0.,
1426 flip: false,
1427 origin: None,
1428 normal: None,
1429 plane: None,
1430 exclude: Vec::new(),
1431 planes: Vec::new(),
1432 });
1433 section.axis = "Plane".into();
1434 section.plane = Some(name.trim().to_string()).filter(|name| !name.is_empty());
1435 section.freeze(None);
1436 let block = serde_json::to_value(&state).ok();
1437 self.history.break_coalescing();
1439 self.history.set_pmi_block(block, Some(&format!("pmi:view:{id}")));
1440 }
1441
1442 fn pmi_commit_section_exclude(&mut self, id: &str, names: &[String]) {
1446 let mut state = self.pmi_state();
1447 let Some(section) = state.find_view_mut(id).and_then(|view| view.display.section.as_mut()) else { return };
1448 section.exclude = names.iter().map(|name| name.trim().to_string()).filter(|name| !name.is_empty()).collect();
1449 let block = serde_json::to_value(&state).ok();
1450 self.history.break_coalescing();
1451 self.history.set_pmi_block(block, Some(&format!("pmi:view:{id}")));
1452 }
1453
1454 pub(crate) fn pmi_merge_imported(&mut self, lifted: PmiState) {
1460 let mut state = self.pmi_state();
1461 for view in lifted.views {
1462 let id = state.next_id("VIEW");
1463 let mut annotations = Vec::with_capacity(view.annotations.len());
1464 for mut annotation in view.annotations {
1465 let prefix = brep_kernel::pmi_type(&annotation.kind).map(|def| def.short_name).unwrap_or("PMI");
1466 let fresh = state.next_id(prefix);
1467 if let Some(object) = annotation.params.as_object_mut() {
1468 object.insert("id".into(), serde_json::Value::String(fresh));
1469 }
1470 annotations.push(annotation);
1471 }
1472 state.views.push(PmiView {
1473 id,
1474 name: view.name,
1475 camera: view.camera,
1476 display: view.display,
1477 annotations,
1478 });
1479 }
1480 let block = serde_json::to_value(&state).ok();
1481 self.history.set_pmi_block_no_undo(block);
1482 self.rerun_history();
1483 }
1484}
1485
1486fn pose_matrix(center: [f64; 3], translate: [f64; 3], rotate: &dyn Fn([f64; 3]) -> [f64; 3], scale: [f64; 3]) -> [f32; 16] {
1491 let columns = [rotate([1.0, 0.0, 0.0]), rotate([0.0, 1.0, 0.0]), rotate([0.0, 0.0, 1.0])];
1492 let linear: [[f64; 3]; 3] = std::array::from_fn(|j| columns[j].map(|v| v * scale[j]));
1493 let shifted: [f64; 3] = std::array::from_fn(|i| center[i] + translate[i] - (0..3).map(|j| linear[j][i] * center[j]).sum::<f64>());
1494 let mut m = [0.0f32; 16];
1495 for j in 0..3 {
1496 for i in 0..3 {
1497 m[j * 4 + i] = linear[j][i] as f32;
1498 }
1499 }
1500 m[12] = shifted[0] as f32;
1501 m[13] = shifted[1] as f32;
1502 m[14] = shifted[2] as f32;
1503 m[15] = 1.0;
1504 m
1505}
1506
1507fn mat4_mul(a: &[f32; 16], b: &[f32; 16]) -> [f32; 16] {
1509 std::array::from_fn(|k| {
1510 let (col, row) = (k / 4, k % 4);
1511 (0..4).map(|n| a[n * 4 + row] * b[col * 4 + n]).sum()
1512 })
1513}
1514
1515fn reset_posed_geometry(display: &mut crate::scene::SolidDisplay, original: &crate::scene::SolidDisplay) {
1519 display.mesh.positions.clone_from(&original.mesh.positions);
1520 display.mesh.normals.clone_from(&original.mesh.normals);
1521 if display.edges.len() == original.edges.len() {
1522 for (edge, from) in display.edges.iter_mut().zip(&original.edges) {
1523 edge.polyline.clone_from(&from.polyline);
1524 }
1525 } else {
1526 display.edges.clone_from(&original.edges);
1527 }
1528 display.vertices.clone_from(&original.vertices);
1529 display.bbox = original.bbox;
1530 display.revision = original.revision;
1531 display.shape_revision = original.shape_revision;
1532 display.pose = original.pose;
1533}
1534
1535pub(super) fn transform_display(
1536 display: &mut crate::scene::SolidDisplay,
1537 center: [f64; 3],
1538 translate: [f64; 3],
1539 rotate_deg: [f64; 3],
1540 scale: [f64; 3],
1541) {
1542 let rotate_rad = rotate_deg.map(f64::to_radians);
1549 let rotate = |v: [f64; 3]| super::rotate_euler_xyz_f64(v, rotate_rad);
1550 let step = pose_matrix(center, translate, &rotate, scale);
1554 display.pose = if display.shape_revision == 0 {
1555 Some(step)
1556 } else {
1557 display.pose.map(|before| mat4_mul(&step, &before))
1558 };
1559 if display.shape_revision == 0 {
1560 display.shape_revision = display.revision;
1561 }
1562 display.revision = crate::scene::next_revision();
1563 let pose = |p: [f64; 3]| -> [f64; 3] {
1564 let local = [(p[0] - center[0]) * scale[0], (p[1] - center[1]) * scale[1], (p[2] - center[2]) * scale[2]];
1565 let rotated = rotate(local);
1566 [rotated[0] + center[0] + translate[0], rotated[1] + center[1] + translate[1], rotated[2] + center[2] + translate[2]]
1567 };
1568 let mut bbox = crate::camera::Aabb::empty();
1569 for position in &mut display.mesh.positions {
1570 let posed = pose([position[0] as f64, position[1] as f64, position[2] as f64]);
1571 *position = [posed[0] as f32, posed[1] as f32, posed[2] as f32];
1572 bbox.expand(posed);
1573 }
1574 for normal in &mut display.mesh.normals {
1575 let rotated = rotate(std::array::from_fn(|i| normal[i] as f64 / scale[i]));
1576 let length = rotated.iter().map(|v| v * v).sum::<f64>().sqrt().max(1e-20);
1577 *normal = rotated.map(|v| (v / length) as f32);
1578 }
1579 for edge in &mut display.edges {
1580 for point in &mut edge.polyline {
1581 let posed = pose([point[0] as f64, point[1] as f64, point[2] as f64]);
1582 *point = [posed[0] as f32, posed[1] as f32, posed[2] as f32];
1583 bbox.expand(posed);
1584 }
1585 }
1586 for vertex in &mut display.vertices {
1587 vertex.position = pose(vertex.position);
1588 bbox.expand(vertex.position);
1589 }
1590 if !bbox.is_empty() {
1591 display.bbox = bbox;
1592 }
1593}
1594