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}
42
43pub fn pmi_view_schema() -> serde_json::Value {
49 serde_json::json!({
50 "type": "pmiView",
51 "shortName": "VIEW",
52 "longName": "PMI view",
53 "inputParamsSchema": {
54 "name": { "type": "string", "label": "Name", "default_value": "View 1" },
55 "textSizePt": { "type": "number", "label": "Text size (pt)", "default_value": brep_kernel::PmiDisplay::default().text_size_pt },
56 "wireframe": { "type": "boolean", "label": "Wireframe", "default_value": false },
57 "updateCamera": { "type": "button", "label": "Update camera from the viewport" },
58 "updateVisibility": { "type": "button", "label": "Update visibility from the scene" }
59 }
60 })
61}
62
63pub fn pmi_view_params(view: &brep_kernel::PmiView) -> serde_json::Value {
65 serde_json::json!({
66 "name": view.name,
67 "textSizePt": view.display.text_size_pt,
68 "wireframe": view.display.wireframe,
69 })
70}
71
72pub fn world_vertex_ref(solid: &str, position: [f64; 3]) -> String {
74 let trim = |v: f64| {
75 let rounded = (v * 1e9).round() / 1e9;
76 let trimmed = crate::formatting::compact_decimal(rounded, 9);
77 if trimmed == "-0" || trimmed.is_empty() { "0".to_string() } else { trimmed }
78 };
79 format!("{solid}@{},{},{}", trim(position[0]), trim(position[1]), trim(position[2]))
80}
81
82impl EngineState {
83 pub fn pmi_state(&self) -> PmiState {
88 self.history
89 .pmi_block()
90 .and_then(|block| serde_json::from_value(block.clone()).ok())
91 .unwrap_or_default()
92 }
93
94 pub fn pmi_report(&self) -> Option<&PmiReport> {
96 self.pmi_report.as_ref()
97 }
98
99 pub fn pmi_active_view(&self) -> Option<&str> {
100 self.pmi_active_view.as_deref()
101 }
102
103 pub fn pmi_open_annotation(&self) -> Option<&str> {
104 self.pmi_open_annotation.as_deref()
105 }
106
107 pub fn pmi_open_view(&self) -> Option<&str> {
109 self.pmi_open_view.as_deref()
110 }
111
112 pub fn pmi_dialog_opens(&self) -> u64 {
117 self.pmi_dialog_opens
118 }
119
120 fn open_pmi_annotation(&mut self, id: String) {
131 self.pmi_open_view = None;
132 self.pmi_selected_view = None;
133 self.pmi_selected_annotation = Some(id.clone());
134 self.pmi_open_annotation = Some(id);
135 self.pmi_dialog_opens = self.pmi_dialog_opens.wrapping_add(1);
136 }
137
138 fn open_pmi_view(&mut self, id: String) {
141 self.pmi_open_annotation = None;
142 if self.pmi_selected_annotation.take().is_some() {
143 self.refresh_pmi_overlay();
144 }
145 self.pmi_selected_view = Some(id.clone());
146 self.pmi_open_view = Some(id);
147 self.pmi_dialog_opens = self.pmi_dialog_opens.wrapping_add(1);
148 }
149
150 pub fn pmi_selected_annotation(&self) -> Option<&str> {
152 let id = self.pmi_selected_annotation.as_deref()?;
153 self.pmi_state().find_annotation(id).is_some().then_some(id)
154 }
155
156 pub fn pmi_selected_view(&self) -> Option<&str> {
158 let id = self.pmi_selected_view.as_deref()?;
159 self.pmi_state().find_view(id).is_some().then_some(id)
160 }
161
162 pub fn pmi_workbench_entered(&self) -> bool {
164 self.pmi_modeling.is_some()
165 }
166
167 pub fn pmi_state_json(&self) -> String {
170 let state = self.pmi_state();
171 serde_json::json!({
172 "views": state.views,
173 "idCounter": state.id_counter,
174 "activeView": self.pmi_active_view,
175 "openAnnotation": self.pmi_open_annotation,
176 "openView": self.pmi_open_view,
177 "selectedAnnotation": self.pmi_selected_annotation(),
178 "selectedView": self.pmi_selected_view(),
179 "entered": self.pmi_modeling.is_some(),
180 "datums": state.datum_letters(),
181 "report": self.pmi_report,
182 })
183 .to_string()
184 }
185
186 fn write_pmi_state(&mut self, state: PmiState) -> String {
192 let block = if state.is_empty() { None } else { serde_json::to_value(&state).ok() };
193 self.history.set_pmi_block(block, None);
194 self.rerun_history()
195 }
196
197 fn snapshot_camera(&self) -> PmiCamera {
201 let camera = &self.camera;
202 PmiCamera {
203 eye: camera.eye,
204 target: camera.target,
205 up: camera.up,
206 projection: match camera.projection {
207 Projection::Orthographic { half_height } => PmiProjection::Orthographic { half_height },
208 Projection::Perspective { fov_y_deg } => PmiProjection::Perspective { fov_y_deg },
209 },
210 viewport: [camera.width, camera.height],
211 }
212 }
213
214 fn hidden_solid_names(&self) -> Vec<String> {
216 self.scene
217 .solids()
218 .iter()
219 .filter(|solid| !solid.visible)
220 .map(|solid| solid.name.clone())
221 .collect()
222 }
223
224 pub fn pmi_capture_view(&mut self, name: Option<&str>) -> String {
227 let mut state = self.pmi_state();
228 let id = state.next_id("VIEW");
229 let name = name
230 .map(str::trim)
231 .filter(|n| !n.is_empty())
232 .map(String::from)
233 .unwrap_or_else(|| format!("View {}", state.views.len() + 1));
234 let was_active = self.pmi_active_view.is_some();
238 if was_active {
239 self.pmi_deactivate_view();
240 }
241 self.pmi_remember_modeling();
242 state.views.push(PmiView {
243 id: id.clone(),
244 name,
245 camera: Some(self.snapshot_camera()),
246 display: PmiDisplay {
247 text_size_pt: 12.0,
248 wireframe: self.settings.wireframe,
249 hidden: self.hidden_solid_names(),
250 },
251 annotations: Vec::new(),
252 });
253 self.write_pmi_state(state);
254 let _ = self.pmi_activate_view(&id);
255 id
256 }
257
258 pub fn pmi_rename_view(&mut self, id: &str, name: &str) -> Result<(), String> {
259 let mut state = self.pmi_state();
260 let view = state.find_view_mut(id).ok_or_else(|| format!("no PMI view '{id}'"))?;
261 let name = name.trim();
262 if name.is_empty() {
263 return Err("a view needs a name".into());
264 }
265 view.name = name.to_string();
266 self.write_pmi_state(state);
267 Ok(())
268 }
269
270 pub fn pmi_delete_view(&mut self, id: &str) -> Result<(), String> {
272 let mut state = self.pmi_state();
273 let before = state.views.len();
274 if self.pmi_active_view.as_deref() == Some(id) {
275 self.pmi_deactivate_view();
276 }
277 state.views.retain(|view| view.id != id);
278 if state.views.len() == before {
279 return Err(format!("no PMI view '{id}'"));
280 }
281 if let Some(open) = &self.pmi_open_annotation {
282 if state.find_annotation(open).is_none() {
283 self.pmi_open_annotation = None;
284 }
285 }
286 if self.pmi_open_view.as_deref() == Some(id) {
287 self.pmi_open_view = None;
288 }
289 self.write_pmi_state(state);
290 Ok(())
291 }
292
293 pub fn pmi_update_view_camera(&mut self, id: &str) -> Result<(), String> {
296 let mut state = self.pmi_state();
297 let camera = self.snapshot_camera();
298 let view = state.find_view_mut(id).ok_or_else(|| format!("no PMI view '{id}'"))?;
299 view.camera = Some(camera);
300 self.write_pmi_state(state);
301 Ok(())
302 }
303
304 pub fn pmi_update_view_visibility(&mut self, id: &str) -> Result<(), String> {
307 let hidden = self.hidden_solid_names();
308 self.pmi_set_view_display(id, &PmiViewPatch { hidden: Some(hidden), ..Default::default() })
309 }
310
311 pub fn pmi_set_view_display(&mut self, id: &str, patch: &PmiViewPatch) -> Result<(), String> {
313 self.patch_pmi_view(id, patch, None)
314 }
315
316 pub fn pmi_update_view(&mut self, id: &str, params_json: &str) -> Result<(), String> {
321 let params: serde_json::Value =
322 serde_json::from_str(params_json).map_err(|error| format!("view params: {error}"))?;
323 let state = self.pmi_state();
324 let view = state.find_view(id).ok_or_else(|| format!("no PMI view '{id}'"))?;
325 let name = params
326 .get("name")
327 .and_then(serde_json::Value::as_str)
328 .filter(|name| *name != view.name)
329 .map(String::from);
330 let text_size_pt = params
331 .get("textSizePt")
332 .and_then(serde_json::Value::as_f64)
333 .filter(|size| *size != view.display.text_size_pt);
334 let wireframe = params
335 .get("wireframe")
336 .and_then(serde_json::Value::as_bool)
337 .filter(|on| *on != view.display.wireframe);
338 if name.is_none() && text_size_pt.is_none() && wireframe.is_none() {
339 return Ok(());
340 }
341 let patch = PmiViewPatch { name, text_size_pt, wireframe, hidden: None };
342 self.patch_pmi_view(id, &patch, Some(&format!("pmi:view:{id}")))
343 }
344
345 pub fn pmi_set_view_open(&mut self, id: Option<&str>) {
349 match id {
350 Some(id) => {
351 if self.pmi_state().find_view(id).is_none() {
352 return;
353 }
354 if self.pmi_active_view.as_deref() != Some(id) {
355 let _ = self.pmi_activate_view(id);
356 }
357 self.open_pmi_view(id.to_string());
358 }
359 None => self.pmi_open_view = None,
360 }
361 self.refresh_pmi_overlay();
362 }
363
364 fn patch_pmi_view(&mut self, id: &str, patch: &PmiViewPatch, coalesce_key: Option<&str>) -> Result<(), String> {
365 let mut state = self.pmi_state();
366 let view = state.find_view_mut(id).ok_or_else(|| format!("no PMI view '{id}'"))?;
367 if let Some(name) = &patch.name {
368 let name = name.trim();
369 if name.is_empty() {
370 return Err("a view needs a name".into());
371 }
372 view.name = name.to_string();
373 }
374 if let Some(size) = patch.text_size_pt {
375 view.display.text_size_pt = brep_kernel::clamp_text_size(size);
376 }
377 if let Some(wireframe) = patch.wireframe {
378 view.display.wireframe = wireframe;
379 }
380 if let Some(hidden) = &patch.hidden {
381 view.display.hidden = hidden.clone();
382 }
383 let display = view.display.clone();
384 let block = if state.is_empty() { None } else { serde_json::to_value(&state).ok() };
385 self.history.set_pmi_block(block, coalesce_key);
386 self.rerun_history();
387 if self.pmi_active_view.as_deref() == Some(id) {
388 self.apply_view_display(&display);
389 }
390 Ok(())
391 }
392
393 fn pmi_remember_modeling(&mut self) {
399 if self.pmi_active_view.is_none() || self.pmi_modeling.is_none() {
400 self.pmi_modeling = Some(PmiModelingSnapshot {
401 camera_json: self.camera_state_json(),
402 hidden: self.hidden_solid_names(),
403 wireframe: self.settings.wireframe,
404 });
405 }
406 }
407
408 pub fn pmi_enter_workbench(&mut self) {
411 self.pmi_remember_modeling();
412 }
413
414 pub fn pmi_leave_workbench(&mut self) {
419 self.pmi_deactivate_view();
420 self.pmi_modeling = None;
421 self.pmi_open_annotation = None;
422 self.pmi_open_view = None;
423 self.refresh_pmi_overlay();
424 }
425
426 fn apply_view_display(&mut self, display: &PmiDisplay) {
427 let names: Vec<String> = self.scene.solids().iter().map(|s| s.name.clone()).collect();
428 for name in names {
429 let visible = !display.hidden.contains(&name);
430 self.scene.set_visible(&name, visible);
431 }
432 if self.settings.wireframe != display.wireframe {
433 self.settings.wireframe = display.wireframe;
434 self.settings_generation = self.settings_generation.wrapping_add(1);
435 }
436 self.dirty = true;
437 }
438
439 pub fn pmi_activate_view(&mut self, id: &str) -> Result<(), String> {
443 let state = self.pmi_state();
444 let view = state.find_view(id).ok_or_else(|| format!("no PMI view '{id}'"))?.clone();
445 if self.pmi_active_view.as_deref() != Some(id) {
446 let keep = self.pmi_active_view.is_some();
449 self.pmi_deactivate_view();
450 if !keep {
451 self.pmi_remember_modeling();
452 }
453 }
454 if self.pmi_modeling.is_none() {
455 self.pmi_remember_modeling();
456 }
457 self.pmi_active_view = Some(id.to_string());
458 if let Some(camera) = &view.camera {
459 let (kind, scale) = match camera.projection {
460 PmiProjection::Orthographic { half_height } => ("orthographic", half_height),
461 PmiProjection::Perspective { fov_y_deg } => ("perspective", fov_y_deg),
462 };
463 let json = serde_json::json!({
464 "kind": kind, "eye": camera.eye, "target": camera.target, "up": camera.up, "scale": scale,
465 })
466 .to_string();
467 let _ = self.apply_camera_state_json(&json);
468 self.controls_sync_after_camera_apply();
469 }
470 self.apply_view_display(&view.display);
471 self.pmi_apply_explode();
472 if let Some(open) = &self.pmi_open_annotation {
473 if !view.annotations.iter().any(|a| a.id() == open) {
474 self.pmi_open_annotation = None;
475 }
476 }
477 self.refresh_pmi_overlay();
478 Ok(())
479 }
480
481 pub fn pmi_deactivate_view(&mut self) {
484 if self.pmi_active_view.take().is_none() {
485 return;
486 }
487 self.pmi_restore_explode();
488 if let Some(snapshot) = self.pmi_modeling.clone() {
489 let names: Vec<String> = self.scene.solids().iter().map(|s| s.name.clone()).collect();
490 for name in names {
491 let visible = !snapshot.hidden.contains(&name);
492 self.scene.set_visible(&name, visible);
493 }
494 if self.settings.wireframe != snapshot.wireframe {
495 self.settings.wireframe = snapshot.wireframe;
496 self.settings_generation = self.settings_generation.wrapping_add(1);
497 }
498 let _ = self.apply_camera_state_json(&snapshot.camera_json);
499 self.controls_sync_after_camera_apply();
500 }
501 self.pmi_open_annotation = None;
502 self.dirty = true;
503 self.refresh_pmi_overlay();
504 }
505
506 fn controls_sync_after_camera_apply(&mut self) {
508 self.dirty = true;
512 }
513
514 pub(crate) fn pmi_apply_explode(&mut self) {
519 self.pmi_restore_explode();
520 let Some(active) = self.pmi_active_view.clone() else {
521 return;
522 };
523 let Some(report) = self.pmi_report.as_ref() else {
524 return;
525 };
526 let Some(view) = report.view(&active) else {
527 return;
528 };
529 let poses: Vec<(Vec<String>, [f64; 3], [f64; 3], [f64; 3], [f64; 3])> = view
530 .annotations
531 .iter()
532 .filter(|row| row.enabled && row.status == PmiStatus::Ok)
533 .filter_map(|row| match &row.geometry {
534 PmiGeometry::Explode { solids, translate, rotate_deg, scale, center, .. } => {
535 Some((solids.clone(), *translate, *rotate_deg, *scale, *center))
536 }
537 _ => None,
538 })
539 .collect();
540 for (solids, translate, rotate_deg, scale, center) in poses {
541 for name in solids {
542 let Some(display) = self.scene.solid(&name) else { continue };
543 self.pmi_explode_originals
544 .entry(name.clone())
545 .or_insert_with(|| display.clone());
546 if let Some(display) = self.scene.solid_mut(&name) {
547 transform_display(display, center, translate, rotate_deg, scale);
548 }
549 }
550 }
551 if !self.pmi_explode_originals.is_empty() {
552 self.dirty = true;
553 }
554 }
555
556 pub(crate) fn pmi_restore_explode(&mut self) {
558 let originals = std::mem::take(&mut self.pmi_explode_originals);
559 for (name, original) in originals {
560 if let Some(display) = self.scene.solid_mut(&name) {
561 *display = original;
562 }
563 }
564 self.dirty = true;
565 }
566
567 pub(crate) fn pmi_after_apply(&mut self) {
570 self.pmi_restore_explode();
574 if let Some(active) = self.pmi_active_view.clone() {
575 let exists = self.pmi_state().find_view(&active).is_some();
577 if !exists {
578 self.pmi_active_view = None;
579 self.pmi_open_annotation = None;
580 } else {
581 self.pmi_apply_explode();
582 }
583 }
584 if let Some(open) = &self.pmi_open_annotation {
585 if self.pmi_state().find_annotation(open).is_none() {
586 self.pmi_open_annotation = None;
587 }
588 }
589 if let Some(open) = &self.pmi_open_view {
591 if self.pmi_state().find_view(open).is_none() {
592 self.pmi_open_view = None;
593 }
594 }
595 self.refresh_pmi_overlay();
596 }
597
598 pub fn pmi_add_annotation(
605 &mut self,
606 view_id: Option<&str>,
607 type_id: &str,
608 params_json: &str,
609 ) -> Result<String, String> {
610 let def = brep_kernel::pmi_type(type_id).ok_or_else(|| format!("unknown PMI annotation type '{type_id}'"))?;
611 let mut state = self.pmi_state();
612 let view_id = view_id
613 .map(String::from)
614 .or_else(|| self.pmi_active_view.clone())
615 .ok_or_else(|| "no active PMI view — capture or activate a view first".to_string())?;
616 let id = state.next_id(def.short_name);
617 let mut params: serde_json::Value = serde_json::from_str(params_json).unwrap_or_else(|_| serde_json::json!({}));
618 if !params.is_object() {
619 params = serde_json::json!({});
620 }
621 let schema = (def.schema)();
623 if let (Some(fields), Some(object)) = (
624 schema.get("inputParamsSchema").and_then(serde_json::Value::as_object),
625 params.as_object_mut(),
626 ) {
627 for (key, spec) in fields {
628 if !object.contains_key(key) {
629 if let Some(default) = spec.get("default_value") {
630 if !default.is_null() {
631 object.insert(key.clone(), default.clone());
632 }
633 }
634 }
635 }
636 object.insert("id".into(), serde_json::Value::String(id.clone()));
637 if type_id == "datum" {
638 let letter = object.get("letter").and_then(serde_json::Value::as_str).unwrap_or("").trim().to_string();
639 if letter.is_empty() {
640 if let Some(next) = state.next_datum_letter() {
641 object.insert("letter".into(), serde_json::Value::String(next));
642 }
643 }
644 }
645 }
646 let view = state.find_view_mut(&view_id).ok_or_else(|| format!("no PMI view '{view_id}'"))?;
647 view.annotations.push(PmiAnnotation {
648 kind: type_id.to_string(),
649 enabled: true,
650 params,
651 label_world: None,
652 });
653 self.open_pmi_annotation(id.clone());
654 self.write_pmi_state(state);
655 Ok(id)
656 }
657
658 pub fn pmi_update_annotation(&mut self, id: &str, params_json: &str) -> Result<(), String> {
660 self.pmi_update_annotation_no_rerun(id, params_json)?;
661 self.rerun_history();
662 Ok(())
663 }
664
665 pub(crate) fn pmi_update_annotation_no_rerun(&mut self, id: &str, params_json: &str) -> Result<(), String> {
669 let mut state = self.pmi_state();
670 let annotation = state.find_annotation_mut(id).ok_or_else(|| format!("no PMI annotation '{id}'"))?;
671 let mut params: serde_json::Value =
672 serde_json::from_str(params_json).map_err(|error| format!("annotation params: {error}"))?;
673 if let Some(object) = params.as_object_mut() {
674 object.insert("id".into(), serde_json::Value::String(id.to_string()));
675 }
676 annotation.params = params;
677 let block = serde_json::to_value(&state).ok();
678 self.history.set_pmi_block(block, Some(&format!("pmi:params:{id}")));
679 Ok(())
680 }
681
682 pub fn pmi_remove_annotation(&mut self, id: &str) -> Result<(), String> {
683 let mut state = self.pmi_state();
684 let Some((view_index, index)) = state.locate_annotation(id) else {
685 return Err(format!("no PMI annotation '{id}'"));
686 };
687 state.views[view_index].annotations.remove(index);
688 if self.pmi_open_annotation.as_deref() == Some(id) {
689 self.pmi_open_annotation = None;
690 }
691 self.write_pmi_state(state);
692 Ok(())
693 }
694
695 pub fn pmi_set_annotation_enabled(&mut self, id: &str, enabled: bool) -> Result<(), String> {
696 let mut state = self.pmi_state();
697 let annotation = state.find_annotation_mut(id).ok_or_else(|| format!("no PMI annotation '{id}'"))?;
698 if annotation.enabled == enabled {
699 return Ok(());
700 }
701 annotation.enabled = enabled;
702 self.write_pmi_state(state);
703 Ok(())
704 }
705
706 pub fn pmi_move_annotation(&mut self, id: &str, index: usize) -> Result<(), String> {
708 let mut state = self.pmi_state();
709 let Some((view_index, from)) = state.locate_annotation(id) else {
710 return Err(format!("no PMI annotation '{id}'"));
711 };
712 let annotations = &mut state.views[view_index].annotations;
713 let annotation = annotations.remove(from);
714 let to = index.min(annotations.len());
715 annotations.insert(to, annotation);
716 self.write_pmi_state(state);
717 Ok(())
718 }
719
720 pub fn pmi_move_annotation_to_view(&mut self, id: &str, view_id: &str) -> Result<(), String> {
722 let mut state = self.pmi_state();
723 let Some((view_index, from)) = state.locate_annotation(id) else {
724 return Err(format!("no PMI annotation '{id}'"));
725 };
726 if state.find_view(view_id).is_none() {
727 return Err(format!("no PMI view '{view_id}'"));
728 }
729 let annotation = state.views[view_index].annotations.remove(from);
730 state.find_view_mut(view_id).expect("checked").annotations.push(annotation);
731 self.write_pmi_state(state);
732 Ok(())
733 }
734
735 pub fn pmi_set_annotation_open(&mut self, id: Option<&str>) {
742 match id {
743 Some(id) => {
744 let state = self.pmi_state();
745 if let Some((view, _)) = state.find_annotation(id) {
746 let view_id = view.id.clone();
747 if self.pmi_active_view.as_deref() != Some(view_id.as_str()) {
748 let _ = self.pmi_activate_view(&view_id);
749 }
750 self.open_pmi_annotation(id.to_string());
751 }
752 }
753 None => self.pmi_open_annotation = None,
754 }
755 self.refresh_pmi_overlay();
756 }
757
758 pub fn pmi_set_label_world(&mut self, id: &str, world: [f64; 3]) -> Result<(), String> {
764 let mut state = self.pmi_state();
765 let annotation = state.find_annotation_mut(id).ok_or_else(|| format!("no PMI annotation '{id}'"))?;
766 annotation.label_world = Some(world);
767 let block = serde_json::to_value(&state).ok();
768 self.history.set_pmi_block(block, Some(&format!("pmi:label:{id}")));
769 if let Some(report) = self.pmi_report.as_mut() {
770 if let Some(row) = report.annotation_mut(id) {
771 row.label_world = world;
772 if let PmiGeometry::Note { position } = &mut row.geometry {
773 *position = world;
774 }
775 }
776 }
777 self.refresh_pmi_overlay();
778 Ok(())
779 }
780
781 pub fn pmi_label_drag_to(&mut self, id: &str, x: f64, y: f64) {
785 let Some((current, plane)) = self
786 .pmi_report
787 .as_ref()
788 .and_then(|r| r.annotation(id))
789 .map(|r| (r.label_world, r.plane))
790 else {
791 return;
792 };
793 let ray = self.camera.pick_ray(x, y);
794 let plane = plane.unwrap_or_else(|| {
798 let (_, _, view) = self.camera.basis();
799 brep_kernel::PmiPlane {
800 origin: current,
801 normal: view,
802 x_axis: [0.0; 3],
803 }
804 });
805 let Some(world) = plane.hit(ray.origin, ray.dir) else {
806 return;
807 };
808 let _ = self.pmi_set_label_world(id, world);
809 }
810
811 pub fn pmi_label_drag_end(&mut self) {
813 self.history.break_coalescing();
814 }
815
816 pub fn pmi_hover(&mut self, id: &str) {
818 self.pmi_label_hover_active = true;
819 if self.pmi_hovered.as_deref() == Some(id) {
820 return;
821 }
822 let references: Vec<String> = self
823 .pmi_report
824 .as_ref()
825 .and_then(|report| report.annotation(id))
826 .map(|row| row.references.clone())
827 .unwrap_or_default();
828 let mut solids: Vec<String> = Vec::new();
829 let mut faces: Vec<String> = Vec::new();
830 let mut edges: Vec<String> = Vec::new();
831 for reference in &references {
832 if let Some(at) = reference.find('@') {
833 solids.push(reference[..at].to_string());
834 } else if self.scene_has_face(reference) {
835 faces.push(reference.clone());
836 } else if self.scene_has_edge(reference) {
837 edges.push(reference.clone());
838 } else if self.scene.solid(reference).is_some() {
839 solids.push(reference.clone());
840 } else {
841 let prefix = format!("{reference}:");
842 solids.extend(self.scene.solids().iter().filter(|s| s.name.starts_with(&prefix)).map(|s| s.name.clone()));
843 }
844 }
845 self.clear_hover();
846 self.emphasis.hovered_solids.extend(solids);
847 self.emphasis.hovered_faces.extend(faces);
848 self.emphasis.hovered_edges.extend(edges);
849 self.emphasis.generation = self.emphasis.generation.wrapping_add(1);
850 self.pmi_hovered = Some(id.to_string());
851 self.dirty = true;
852 }
853
854 pub fn pmi_hover_end(&mut self) {
855 if self.pmi_hovered.take().is_some() {
856 self.clear_hover();
857 self.dirty = true;
858 }
859 }
860
861 pub fn take_pmi_label_hover(&mut self) -> bool {
864 std::mem::take(&mut self.pmi_label_hover_active)
865 }
866
867 pub fn pmi_label_clicked(&mut self, id: &str) {
870 self.pmi_select_annotation(id);
871 }
872
873 pub fn pmi_label_double_clicked(&mut self, id: &str) {
876 self.pmi_set_annotation_open(Some(id));
877 }
878
879 pub fn pmi_select_annotation(&mut self, id: &str) -> bool {
884 let state = self.pmi_state();
885 let Some((view, _)) = state.find_annotation(id) else {
886 return false;
887 };
888 if self.pmi_active_view.as_deref() != Some(view.id.as_str()) {
889 let _ = self.pmi_activate_view(&view.id);
890 }
891 self.pmi_selected_view = None;
892 self.pmi_selected_annotation = Some(id.to_string());
893 self.refresh_pmi_overlay();
894 true
895 }
896
897 pub fn pmi_select_view(&mut self, id: &str) -> bool {
903 if self.pmi_state().find_view(id).is_none() {
904 return false;
905 }
906 if self.pmi_selected_annotation.take().is_some() {
907 self.refresh_pmi_overlay();
908 }
909 self.pmi_selected_view = Some(id.to_string());
910 self.dirty = true;
911 true
912 }
913
914 pub fn begin_ref_select_for_pmi(
918 &mut self,
919 id: &str,
920 path: Vec<String>,
921 label: String,
922 filter: Vec<String>,
923 multiple: bool,
924 seed_names: Vec<String>,
925 ) {
926 let restore_index = self.history.rollback();
927 self.selection_filter = SelectionFilter::from_ref_filter(&filter);
928 self.ref_select = Some(RefSelectState {
929 feature_id: id.to_string(),
930 path,
931 label,
932 filter,
933 multiple,
934 names: seed_names,
935 restore_index,
936 target: RefSelectTarget::Pmi,
937 });
938 self.sync_ref_select_emphasis();
939 }
940
941 pub(crate) fn pmi_commit_refs(&mut self, id: &str, path: &[String], names: &[String], multiple: bool) {
944 let state = self.pmi_state();
945 let Some((_, annotation)) = state.find_annotation(id) else {
946 self.push_notice(format!("unknown PMI annotation '{id}'"));
947 return;
948 };
949 let mut params = annotation.params.clone();
950 let value = if multiple {
951 serde_json::Value::Array(names.iter().cloned().map(serde_json::Value::String).collect())
952 } else {
953 serde_json::Value::String(names.first().cloned().unwrap_or_default())
954 };
955 super::selection_ux::set_json_at(&mut params, path, value);
956 if let Err(error) = self.pmi_update_annotation_no_rerun(id, ¶ms.to_string()) {
957 self.push_notice(format!("PMI update failed: {error}"));
958 }
959 }
960
961 pub(crate) fn pmi_merge_imported(&mut self, lifted: PmiState) {
967 let mut state = self.pmi_state();
968 for view in lifted.views {
969 let id = state.next_id("VIEW");
970 let mut annotations = Vec::with_capacity(view.annotations.len());
971 for mut annotation in view.annotations {
972 let prefix = brep_kernel::pmi_type(&annotation.kind).map(|def| def.short_name).unwrap_or("PMI");
973 let fresh = state.next_id(prefix);
974 if let Some(object) = annotation.params.as_object_mut() {
975 object.insert("id".into(), serde_json::Value::String(fresh));
976 }
977 annotations.push(annotation);
978 }
979 state.views.push(PmiView {
980 id,
981 name: view.name,
982 camera: view.camera,
983 display: view.display,
984 annotations,
985 });
986 }
987 let block = serde_json::to_value(&state).ok();
988 self.history.set_pmi_block_no_undo(block);
989 self.rerun_history();
990 }
991}
992
993fn transform_display(
996 display: &mut crate::scene::SolidDisplay,
997 center: [f64; 3],
998 translate: [f64; 3],
999 rotate_deg: [f64; 3],
1000 scale: [f64; 3],
1001) {
1002 let rotate = |v: [f64; 3]| super::rotate_euler_xyz_f64(v, rotate_deg);
1003 let pose = |p: [f64; 3]| -> [f64; 3] {
1004 let local = [(p[0] - center[0]) * scale[0], (p[1] - center[1]) * scale[1], (p[2] - center[2]) * scale[2]];
1005 let rotated = rotate(local);
1006 [rotated[0] + center[0] + translate[0], rotated[1] + center[1] + translate[1], rotated[2] + center[2] + translate[2]]
1007 };
1008 let mut bbox = crate::camera::Aabb::empty();
1009 for position in &mut display.mesh.positions {
1010 let posed = pose([position[0] as f64, position[1] as f64, position[2] as f64]);
1011 *position = [posed[0] as f32, posed[1] as f32, posed[2] as f32];
1012 bbox.expand(posed);
1013 }
1014 for normal in &mut display.mesh.normals {
1015 let rotated = rotate([normal[0] as f64, normal[1] as f64, normal[2] as f64]);
1016 *normal = [rotated[0] as f32, rotated[1] as f32, rotated[2] as f32];
1017 }
1018 for edge in &mut display.edges {
1019 for point in &mut edge.polyline {
1020 let posed = pose([point[0] as f64, point[1] as f64, point[2] as f64]);
1021 *point = [posed[0] as f32, posed[1] as f32, posed[2] as f32];
1022 bbox.expand(posed);
1023 }
1024 }
1025 for vertex in &mut display.vertices {
1026 vertex.position = pose(vertex.position);
1027 bbox.expand(vertex.position);
1028 }
1029 if !bbox.is_empty() {
1030 display.bbox = bbox;
1031 }
1032}
1033