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"], "default_value": "View" },
60 "sectionOffset": { "type": "number", "label": "Section offset (mm)", "default_value": 0.0 },
61 "sectionFlip": { "type": "boolean", "label": "Reverse section", "default_value": false },
62 "updateCamera": { "type": "button", "label": "Update camera from the viewport" },
63 "updateVisibility": { "type": "button", "label": "Update visibility from the scene" }
64 }
65 })
66}
67
68pub fn pmi_view_params(view: &brep_kernel::PmiView) -> serde_json::Value {
70 serde_json::json!({
71 "name": view.name,
72 "textSizePt": view.display.text_size_pt,
73 "wireframe": view.display.wireframe,
74 "sectionEnabled": view.display.section.is_some(),
75 "sectionAxis": view.display.section.as_ref().map(|s| s.axis.as_str()).unwrap_or("View"),
76 "sectionOffset": view.display.section.as_ref().map(|s| s.offset).unwrap_or(0.),
77 "sectionFlip": view.display.section.as_ref().is_some_and(|s| s.flip),
78 })
79}
80
81pub fn world_vertex_ref(solid: &str, position: [f64; 3]) -> String {
83 let trim = |v: f64| {
84 let rounded = (v * 1e9).round() / 1e9;
85 let trimmed = crate::formatting::compact_decimal(rounded, 9);
86 if trimmed == "-0" || trimmed.is_empty() { "0".to_string() } else { trimmed }
87 };
88 format!("{solid}@{},{},{}", trim(position[0]), trim(position[1]), trim(position[2]))
89}
90
91impl EngineState {
92 pub fn pmi_state(&self) -> PmiState {
97 self.history
98 .pmi_block()
99 .and_then(|block| serde_json::from_value(block.clone()).ok())
100 .unwrap_or_default()
101 }
102
103 pub fn pmi_report(&self) -> Option<&PmiReport> {
105 self.pmi_report.as_ref()
106 }
107
108 pub fn pmi_active_view(&self) -> Option<&str> {
109 self.pmi_active_view.as_deref()
110 }
111
112 pub fn pmi_open_annotation(&self) -> Option<&str> {
113 self.pmi_open_annotation.as_deref()
114 }
115
116 pub fn pmi_open_view(&self) -> Option<&str> {
118 self.pmi_open_view.as_deref()
119 }
120
121 pub fn pmi_dialog_opens(&self) -> u64 {
126 self.pmi_dialog_opens
127 }
128
129 fn open_pmi_annotation(&mut self, id: String) {
140 self.pmi_open_view = None;
141 self.pmi_selected_view = None;
142 self.pmi_selected_annotation = Some(id.clone());
143 self.pmi_open_annotation = Some(id);
144 self.pmi_dialog_opens = self.pmi_dialog_opens.wrapping_add(1);
145 }
146
147 fn open_pmi_view(&mut self, id: String) {
150 self.pmi_open_annotation = None;
151 if self.pmi_selected_annotation.take().is_some() {
152 self.refresh_pmi_overlay();
153 }
154 self.pmi_selected_view = Some(id.clone());
155 self.pmi_open_view = Some(id);
156 self.pmi_dialog_opens = self.pmi_dialog_opens.wrapping_add(1);
157 }
158
159 pub fn pmi_selected_annotation(&self) -> Option<&str> {
161 let id = self.pmi_selected_annotation.as_deref()?;
162 self.pmi_state().find_annotation(id).is_some().then_some(id)
163 }
164
165 pub fn pmi_selected_view(&self) -> Option<&str> {
167 let id = self.pmi_selected_view.as_deref()?;
168 self.pmi_state().find_view(id).is_some().then_some(id)
169 }
170
171 pub fn pmi_workbench_entered(&self) -> bool {
173 self.pmi_modeling.is_some()
174 }
175
176 pub fn pmi_state_json(&self) -> String {
179 let state = self.pmi_state();
180 serde_json::json!({
181 "views": state.views,
182 "idCounter": state.id_counter,
183 "activeView": self.pmi_active_view,
184 "openAnnotation": self.pmi_open_annotation,
185 "openView": self.pmi_open_view,
186 "selectedAnnotation": self.pmi_selected_annotation(),
187 "selectedView": self.pmi_selected_view(),
188 "entered": self.pmi_modeling.is_some(),
189 "datums": state.datum_letters(),
190 "report": self.pmi_report,
191 })
192 .to_string()
193 }
194
195 fn write_pmi_state(&mut self, state: PmiState) -> String {
201 let block = if state.is_empty() { None } else { serde_json::to_value(&state).ok() };
202 self.history.set_pmi_block(block, None);
203 self.rerun_history()
204 }
205
206 fn snapshot_camera(&self) -> PmiCamera {
210 let camera = &self.camera;
211 PmiCamera {
212 eye: camera.eye,
213 target: camera.target,
214 up: camera.up,
215 projection: match camera.projection {
216 Projection::Orthographic { half_height } => PmiProjection::Orthographic { half_height },
217 Projection::Perspective { fov_y_deg } => PmiProjection::Perspective { fov_y_deg },
218 },
219 viewport: [camera.width, camera.height],
220 }
221 }
222
223 fn hidden_solid_names(&self) -> Vec<String> {
225 self.scene
226 .solids()
227 .iter()
228 .filter(|solid| !solid.visible)
229 .map(|solid| solid.name.clone())
230 .collect()
231 }
232
233 pub fn pmi_capture_view(&mut self, name: Option<&str>) -> String {
236 let mut state = self.pmi_state();
237 let id = state.next_id("VIEW");
238 let name = name
239 .map(str::trim)
240 .filter(|n| !n.is_empty())
241 .map(String::from)
242 .unwrap_or_else(|| format!("View {}", state.views.len() + 1));
243 let was_active = self.pmi_active_view.is_some();
247 if was_active {
248 self.pmi_deactivate_view();
249 }
250 self.pmi_remember_modeling();
251 state.views.push(PmiView {
252 id: id.clone(),
253 name,
254 camera: Some(self.snapshot_camera()),
255 display: PmiDisplay {
256 text_size_pt: 12.0,
257 wireframe: self.settings.wireframe,
258 hidden: self.hidden_solid_names(),
259 section: None,
260 },
261 annotations: Vec::new(),
262 });
263 self.write_pmi_state(state);
264 let _ = self.pmi_activate_view(&id);
265 id
266 }
267
268 pub fn pmi_rename_view(&mut self, id: &str, name: &str) -> Result<(), String> {
269 let mut state = self.pmi_state();
270 let view = state.find_view_mut(id).ok_or_else(|| format!("no PMI view '{id}'"))?;
271 let name = name.trim();
272 if name.is_empty() {
273 return Err("a view needs a name".into());
274 }
275 view.name = name.to_string();
276 self.write_pmi_state(state);
277 Ok(())
278 }
279
280 pub fn pmi_delete_view(&mut self, id: &str) -> Result<(), String> {
282 let mut state = self.pmi_state();
283 let before = state.views.len();
284 if self.pmi_active_view.as_deref() == Some(id) {
285 self.pmi_deactivate_view();
286 }
287 state.views.retain(|view| view.id != id);
288 if state.views.len() == before {
289 return Err(format!("no PMI view '{id}'"));
290 }
291 if let Some(open) = &self.pmi_open_annotation {
292 if state.find_annotation(open).is_none() {
293 self.pmi_open_annotation = None;
294 }
295 }
296 if self.pmi_open_view.as_deref() == Some(id) {
297 self.pmi_open_view = None;
298 }
299 self.write_pmi_state(state);
300 Ok(())
301 }
302
303 pub fn pmi_update_view_camera(&mut self, id: &str) -> Result<(), String> {
306 let mut state = self.pmi_state();
307 let camera = self.snapshot_camera();
308 let view = state.find_view_mut(id).ok_or_else(|| format!("no PMI view '{id}'"))?;
309 view.camera = Some(camera);
310 self.write_pmi_state(state);
311 Ok(())
312 }
313
314 pub fn pmi_update_view_visibility(&mut self, id: &str) -> Result<(), String> {
317 let hidden = self.hidden_solid_names();
318 self.pmi_set_view_display(id, &PmiViewPatch { hidden: Some(hidden), ..Default::default() })
319 }
320
321 pub fn pmi_set_view_display(&mut self, id: &str, patch: &PmiViewPatch) -> Result<(), String> {
323 self.patch_pmi_view(id, patch, None)
324 }
325
326 pub fn pmi_update_view(&mut self, id: &str, params_json: &str) -> Result<(), String> {
331 let params: serde_json::Value =
332 serde_json::from_str(params_json).map_err(|error| format!("view params: {error}"))?;
333 let state = self.pmi_state();
334 let view = state.find_view(id).ok_or_else(|| format!("no PMI view '{id}'"))?;
335 let name = params
336 .get("name")
337 .and_then(serde_json::Value::as_str)
338 .filter(|name| *name != view.name)
339 .map(String::from);
340 let text_size_pt = params
341 .get("textSizePt")
342 .and_then(serde_json::Value::as_f64)
343 .filter(|size| *size != view.display.text_size_pt);
344 let wireframe = params
345 .get("wireframe")
346 .and_then(serde_json::Value::as_bool)
347 .filter(|on| *on != view.display.wireframe);
348 let section = if params.get("sectionEnabled").is_some() {
349 let section = if params["sectionEnabled"].as_bool().unwrap_or(false) {
350 let axis = params["sectionAxis"].as_str().unwrap_or("View");
351 let offset = params["sectionOffset"].as_f64().unwrap_or(0.);
352 if !["View", "X", "Y", "Z"].contains(&axis) || !offset.is_finite() { return Err("invalid section plane".into()); }
353 Some(brep_kernel::feature_pipeline::pmi::PmiSection { axis: axis.into(), offset, flip: params["sectionFlip"].as_bool().unwrap_or(false) })
354 } else { None };
355 (section != view.display.section).then_some(section)
356 } else { None };
357 if name.is_none() && text_size_pt.is_none() && wireframe.is_none() && section.is_none() {
358 return Ok(());
359 }
360 let patch = PmiViewPatch { name, text_size_pt, wireframe, hidden: None, section };
361 self.patch_pmi_view(id, &patch, Some(&format!("pmi:view:{id}")))
362 }
363
364 pub fn pmi_set_view_open(&mut self, id: Option<&str>) {
368 match id {
369 Some(id) => {
370 if self.pmi_state().find_view(id).is_none() {
371 return;
372 }
373 if self.pmi_active_view.as_deref() != Some(id) {
374 let _ = self.pmi_activate_view(id);
375 }
376 self.open_pmi_view(id.to_string());
377 }
378 None => self.pmi_open_view = None,
379 }
380 self.refresh_pmi_overlay();
381 }
382
383 fn patch_pmi_view(&mut self, id: &str, patch: &PmiViewPatch, coalesce_key: Option<&str>) -> Result<(), String> {
384 let mut state = self.pmi_state();
385 let view = state.find_view_mut(id).ok_or_else(|| format!("no PMI view '{id}'"))?;
386 if let Some(name) = &patch.name {
387 let name = name.trim();
388 if name.is_empty() {
389 return Err("a view needs a name".into());
390 }
391 view.name = name.to_string();
392 }
393 if let Some(size) = patch.text_size_pt {
394 view.display.text_size_pt = brep_kernel::clamp_text_size(size);
395 }
396 if let Some(wireframe) = patch.wireframe {
397 view.display.wireframe = wireframe;
398 }
399 if let Some(hidden) = &patch.hidden {
400 view.display.hidden = hidden.clone();
401 }
402 if let Some(section) = &patch.section { view.display.section = section.clone(); }
403 let display = view.display.clone();
404 let block = if state.is_empty() { None } else { serde_json::to_value(&state).ok() };
405 self.history.set_pmi_block(block, coalesce_key);
406 self.rerun_history();
407 if self.pmi_active_view.as_deref() == Some(id) {
408 self.apply_view_display(&display);
409 }
410 Ok(())
411 }
412
413 fn pmi_remember_modeling(&mut self) {
419 if self.pmi_active_view.is_none() || self.pmi_modeling.is_none() {
420 self.pmi_modeling = Some(PmiModelingSnapshot {
421 camera_json: self.camera_state_json(),
422 hidden: self.hidden_solid_names(),
423 wireframe: self.settings.wireframe,
424 });
425 }
426 }
427
428 pub fn pmi_enter_workbench(&mut self) {
431 self.pmi_remember_modeling();
432 }
433
434 pub fn pmi_leave_workbench(&mut self) {
439 self.pmi_deactivate_view();
440 self.pmi_modeling = None;
441 self.pmi_open_annotation = None;
442 self.pmi_open_view = None;
443 self.refresh_pmi_overlay();
444 }
445
446 fn apply_view_display(&mut self, display: &PmiDisplay) {
447 let names: Vec<String> = self.scene.solids().iter().map(|s| s.name.clone()).collect();
448 for name in names {
449 let visible = !display.hidden.contains(&name);
450 self.scene.set_visible(&name, visible);
451 }
452 if self.settings.wireframe != display.wireframe {
453 self.settings.wireframe = display.wireframe;
454 self.settings_generation = self.settings_generation.wrapping_add(1);
455 }
456 self.dirty = true;
457 }
458
459 pub fn pmi_activate_view(&mut self, id: &str) -> Result<(), String> {
463 let state = self.pmi_state();
464 let view = state.find_view(id).ok_or_else(|| format!("no PMI view '{id}'"))?.clone();
465 if self.pmi_active_view.as_deref() != Some(id) {
466 let keep = self.pmi_active_view.is_some();
469 self.pmi_deactivate_view();
470 if !keep {
471 self.pmi_remember_modeling();
472 }
473 }
474 if self.pmi_modeling.is_none() {
475 self.pmi_remember_modeling();
476 }
477 self.pmi_active_view = Some(id.to_string());
478 if let Some(camera) = &view.camera {
479 let (kind, scale) = match camera.projection {
480 PmiProjection::Orthographic { half_height } => ("orthographic", half_height),
481 PmiProjection::Perspective { fov_y_deg } => ("perspective", fov_y_deg),
482 };
483 let json = serde_json::json!({
484 "kind": kind, "eye": camera.eye, "target": camera.target, "up": camera.up, "scale": scale,
485 })
486 .to_string();
487 let _ = self.apply_camera_state_json(&json);
488 self.controls_sync_after_camera_apply();
489 }
490 self.apply_view_display(&view.display);
491 self.pmi_apply_explode();
492 if let Some(open) = &self.pmi_open_annotation {
493 if !view.annotations.iter().any(|a| a.id() == open) {
494 self.pmi_open_annotation = None;
495 }
496 }
497 self.refresh_pmi_overlay();
498 Ok(())
499 }
500
501 pub fn pmi_deactivate_view(&mut self) {
504 if self.pmi_active_view.take().is_none() {
505 return;
506 }
507 if self.pmi_state().find_annotation(&self.transform_armed_feature()).is_some() { self.disarm_transform(); }
508 self.pmi_restore_explode();
509 if let Some(snapshot) = self.pmi_modeling.clone() {
510 let names: Vec<String> = self.scene.solids().iter().map(|s| s.name.clone()).collect();
511 for name in names {
512 let visible = !snapshot.hidden.contains(&name);
513 self.scene.set_visible(&name, visible);
514 }
515 if self.settings.wireframe != snapshot.wireframe {
516 self.settings.wireframe = snapshot.wireframe;
517 self.settings_generation = self.settings_generation.wrapping_add(1);
518 }
519 let _ = self.apply_camera_state_json(&snapshot.camera_json);
520 self.controls_sync_after_camera_apply();
521 }
522 self.pmi_open_annotation = None;
523 self.dirty = true;
524 self.refresh_pmi_overlay();
525 }
526
527 fn controls_sync_after_camera_apply(&mut self) {
529 self.dirty = true;
533 }
534
535 pub(crate) fn pmi_apply_explode(&mut self) {
540 self.pmi_restore_explode();
541 let Some(active) = self.pmi_active_view.clone() else {
542 return;
543 };
544 let Some(report) = self.pmi_report.as_ref() else {
545 return;
546 };
547 let Some(view) = report.view(&active) else {
548 return;
549 };
550 let poses: Vec<(Vec<String>, [f64; 3], [f64; 3], [f64; 3], [f64; 3])> = view
551 .annotations
552 .iter()
553 .filter(|row| row.enabled && row.status == PmiStatus::Ok)
554 .filter_map(|row| match &row.geometry {
555 PmiGeometry::Explode { solids, translate, rotate_deg, scale, center, .. } => {
556 Some((solids.clone(), *translate, *rotate_deg, *scale, *center))
557 }
558 _ => None,
559 })
560 .collect();
561 for (solids, translate, rotate_deg, scale, center) in poses {
562 for name in solids {
563 let Some(display) = self.scene.solid(&name) else { continue };
564 self.pmi_explode_originals
565 .entry(name.clone())
566 .or_insert_with(|| display.clone());
567 if let Some(display) = self.scene.solid_mut(&name) {
568 transform_display(display, center, translate, rotate_deg, scale);
569 }
570 }
571 }
572 if let Some((point, normal)) = self.pmi_state().find_view(&active).and_then(|v| v.section_plane()) {
573 let names: Vec<String> = self.scene.solids().iter().map(|s| s.name.clone()).collect();
574 for name in names {
575 if let Some(display) = self.scene.solid(&name) { self.pmi_explode_originals.entry(name.clone()).or_insert_with(|| display.clone()); }
576 if let Some(display) = self.scene.solid_mut(&name) { super::pmi_section::clip_display(display, point, normal); }
577 }
578 }
579 if !self.pmi_explode_originals.is_empty() {
580 self.dirty = true;
581 }
582 }
583
584 pub(crate) fn pmi_restore_explode(&mut self) {
586 let originals = std::mem::take(&mut self.pmi_explode_originals);
587 for (name, original) in originals {
588 if let Some(display) = self.scene.solid_mut(&name) {
589 *display = original;
590 }
591 }
592 self.dirty = true;
593 }
594
595 pub(crate) fn pmi_after_apply(&mut self) {
598 self.pmi_balloon_stale = None;
600 self.pmi_restore_explode();
604 if let Some(active) = self.pmi_active_view.clone() {
605 let exists = self.pmi_state().find_view(&active).is_some();
607 if !exists {
608 self.pmi_active_view = None;
609 self.pmi_open_annotation = None;
610 } else {
611 self.pmi_apply_explode();
612 }
613 }
614 if let Some(open) = &self.pmi_open_annotation {
615 if self.pmi_state().find_annotation(open).is_none() {
616 self.pmi_open_annotation = None;
617 }
618 }
619 if let Some(open) = &self.pmi_open_view {
621 if self.pmi_state().find_view(open).is_none() {
622 self.pmi_open_view = None;
623 }
624 }
625 self.refresh_pmi_overlay();
626 }
627
628 pub fn pmi_add_annotation(
635 &mut self,
636 view_id: Option<&str>,
637 type_id: &str,
638 params_json: &str,
639 ) -> Result<String, String> {
640 let def = brep_kernel::pmi_type(type_id).ok_or_else(|| format!("unknown PMI annotation type '{type_id}'"))?;
641 let mut state = self.pmi_state();
642 let view_id = view_id
643 .map(String::from)
644 .or_else(|| self.pmi_active_view.clone())
645 .ok_or_else(|| "no active PMI view — capture or activate a view first".to_string())?;
646 let id = state.next_id(def.short_name);
647 let mut params: serde_json::Value = serde_json::from_str(params_json).unwrap_or_else(|_| serde_json::json!({}));
648 if !params.is_object() {
649 params = serde_json::json!({});
650 }
651 let schema = (def.schema)();
653 if let (Some(fields), Some(object)) = (
654 schema.get("inputParamsSchema").and_then(serde_json::Value::as_object),
655 params.as_object_mut(),
656 ) {
657 for (key, spec) in fields {
658 if !object.contains_key(key) {
659 if let Some(default) = spec.get("default_value") {
660 if !default.is_null() {
661 object.insert(key.clone(), default.clone());
662 }
663 }
664 }
665 }
666 object.insert("id".into(), serde_json::Value::String(id.clone()));
667 if type_id == "datum" {
668 let letter = object.get("letter").and_then(serde_json::Value::as_str).unwrap_or("").trim().to_string();
669 if letter.is_empty() {
670 if let Some(next) = state.next_datum_letter() {
671 object.insert("letter".into(), serde_json::Value::String(next));
672 }
673 }
674 }
675 }
676 let view = state.find_view_mut(&view_id).ok_or_else(|| format!("no PMI view '{view_id}'"))?;
677 view.annotations.push(PmiAnnotation {
678 plugin_replay: None,
679 persistent_data: serde_json::Value::Null,
680 kind: type_id.to_string(),
681 enabled: true,
682 params,
683 label_world: None,
684 });
685 self.open_pmi_annotation(id.clone());
686 self.write_pmi_state(state);
687 Ok(id)
688 }
689
690 pub fn pmi_update_annotation(&mut self, id: &str, params_json: &str) -> Result<(), String> {
692 if self.pmi_state().find_annotation(id).is_some_and(|(_, a)| a.kind.contains('/')) {
693 let params = serde_json::from_str(params_json).map_err(|e| format!("annotation params: {e}"))?;
694 return self.plugin_update_annotation(id, params).map(|_| ());
695 }
696 self.pmi_update_annotation_no_rerun(id, params_json)?;
697 self.rerun_history();
698 Ok(())
699 }
700
701 pub(crate) fn pmi_update_annotation_no_rerun(&mut self, id: &str, params_json: &str) -> Result<(), String> {
705 let mut state = self.pmi_state();
706 let annotation = state.find_annotation_mut(id).ok_or_else(|| format!("no PMI annotation '{id}'"))?;
707 if annotation.kind.contains('/') {
708 return Err("plugin annotation edits require the worker transaction path".into());
709 }
710 let mut params: serde_json::Value =
711 serde_json::from_str(params_json).map_err(|error| format!("annotation params: {error}"))?;
712 if let Some(object) = params.as_object_mut() {
713 object.insert("id".into(), serde_json::Value::String(id.to_string()));
714 }
715 annotation.params = params;
716 let block = serde_json::to_value(&state).ok();
717 self.history.set_pmi_block(block, Some(&format!("pmi:params:{id}")));
718 Ok(())
719 }
720
721 pub fn pmi_remove_annotation(&mut self, id: &str) -> Result<(), String> {
722 if self.pmi_state().find_annotation(id).is_some_and(|(_, a)| a.kind.contains('/')) {
723 return self.plugin_delete_annotation(id).map(|_| ());
724 }
725 let mut state = self.pmi_state();
726 let Some((view_index, index)) = state.locate_annotation(id) else {
727 return Err(format!("no PMI annotation '{id}'"));
728 };
729 state.views[view_index].annotations.remove(index);
730 if self.pmi_open_annotation.as_deref() == Some(id) {
731 self.pmi_open_annotation = None;
732 }
733 self.write_pmi_state(state);
734 Ok(())
735 }
736
737 pub fn pmi_set_annotation_enabled(&mut self, id: &str, enabled: bool) -> Result<(), String> {
738 if self.pmi_state().find_annotation(id).is_some_and(|(_, a)| a.kind.contains('/')) {
739 return self.plugin_set_annotation_enabled(id, enabled).map(|_| ());
740 }
741 let mut state = self.pmi_state();
742 let annotation = state.find_annotation_mut(id).ok_or_else(|| format!("no PMI annotation '{id}'"))?;
743 if annotation.enabled == enabled {
744 return Ok(());
745 }
746 annotation.enabled = enabled;
747 self.write_pmi_state(state);
748 Ok(())
749 }
750
751 pub fn pmi_move_annotation(&mut self, id: &str, index: usize) -> Result<(), String> {
753 let mut state = self.pmi_state();
754 let Some((view_index, from)) = state.locate_annotation(id) else {
755 return Err(format!("no PMI annotation '{id}'"));
756 };
757 let annotations = &mut state.views[view_index].annotations;
758 let annotation = annotations.remove(from);
759 let to = index.min(annotations.len());
760 annotations.insert(to, annotation);
761 self.write_pmi_state(state);
762 Ok(())
763 }
764
765 pub fn pmi_move_annotation_to_view(&mut self, id: &str, view_id: &str) -> Result<(), String> {
767 let mut state = self.pmi_state();
768 let Some((view_index, from)) = state.locate_annotation(id) else {
769 return Err(format!("no PMI annotation '{id}'"));
770 };
771 if state.find_view(view_id).is_none() {
772 return Err(format!("no PMI view '{view_id}'"));
773 }
774 let annotation = state.views[view_index].annotations.remove(from);
775 state.find_view_mut(view_id).expect("checked").annotations.push(annotation);
776 self.write_pmi_state(state);
777 Ok(())
778 }
779
780 pub fn pmi_set_annotation_open(&mut self, id: Option<&str>) {
787 match id {
788 Some(id) => {
789 let state = self.pmi_state();
790 if let Some((view, _)) = state.find_annotation(id) {
791 let view_id = view.id.clone();
792 if self.pmi_active_view.as_deref() != Some(view_id.as_str()) {
793 let _ = self.pmi_activate_view(&view_id);
794 }
795 self.open_pmi_annotation(id.to_string());
796 }
797 }
798 None => {
799 if self.pmi_open_annotation.as_deref() == Some(self.transform_armed_feature().as_str()) { self.disarm_transform(); }
800 self.pmi_open_annotation = None;
801 },
802 }
803 self.refresh_pmi_overlay();
804 }
805
806 pub fn pmi_set_label_world(&mut self, id: &str, world: [f64; 3]) -> Result<(), String> {
813 let mut state = self.pmi_state();
814 let annotation = state.find_annotation_mut(id).ok_or_else(|| format!("no PMI annotation '{id}'"))?;
815 annotation.label_world = Some(world);
816 let block = serde_json::to_value(&state).ok();
817 self.history.set_pmi_block(block, Some(&format!("pmi:label:{id}")));
818 if let Some(report) = self.pmi_report.as_mut() {
819 if let Some(row) = report.annotation_mut(id) {
820 row.label_world = world;
821 if let PmiGeometry::Note { position } = &mut row.geometry {
822 *position = world;
823 }
824 }
825 }
826 self.pmi_reproject_balloon_head(id);
827 self.refresh_pmi_overlay();
828 Ok(())
829 }
830
831 pub(crate) fn pmi_reproject_balloon_head(&mut self, id: &str) {
841 if let Some(missing) = self.pmi_balloon_head_now(id) {
842 self.pmi_balloon_stale = Some(id.to_string());
843 self.request_exact_solids(missing.iter().map(String::as_str));
844 self.pmi_refresh_stale_balloon();
845 }
846 }
847
848 pub(crate) fn pmi_refresh_stale_balloon(&mut self) {
850 let Some(id) = self.pmi_balloon_stale.take() else {
851 return;
852 };
853 if self.pmi_balloon_head_now(&id).is_some() {
854 self.pmi_balloon_stale = Some(id);
856 } else {
857 self.refresh_pmi_overlay();
858 }
859 }
860
861 fn pmi_balloon_head_now(&mut self, id: &str) -> Option<Vec<String>> {
864 let (anchor, bubble) = self
865 .pmi_report
866 .as_ref()
867 .and_then(|report| report.annotation(id))
868 .and_then(|row| match &row.geometry {
869 PmiGeometry::Leader { balloon: true, anchor: Some(anchor), .. } => Some((anchor.clone(), row.label_world)),
870 _ => None,
871 })?;
872 let mut missing = Vec::new();
873 let head = brep_kernel::pmi_balloon_head(&anchor, bubble, |name| self.scene.exact_solid(name), &mut missing);
874 if !missing.is_empty() {
875 return Some(missing);
876 }
877 if let Ok(Some(head)) = head {
880 if let Some(row) = self.pmi_report.as_mut().and_then(|report| report.annotation_mut(id)) {
881 if let PmiGeometry::Leader { targets, .. } = &mut row.geometry {
882 if let Some(first) = targets.first_mut() {
883 *first = head;
884 }
885 }
886 }
887 }
888 None
889 }
890
891 pub fn pmi_label_drag_to(&mut self, id: &str, x: f64, y: f64) {
895 let Some((current, plane)) = self
896 .pmi_report
897 .as_ref()
898 .and_then(|r| r.annotation(id))
899 .map(|r| (r.label_world, r.plane))
900 else {
901 return;
902 };
903 let ray = self.camera.pick_ray(x, y);
904 let plane = plane.unwrap_or_else(|| {
908 let (_, _, view) = self.camera.basis();
909 brep_kernel::PmiPlane {
910 origin: current,
911 normal: view,
912 x_axis: [0.0; 3],
913 }
914 });
915 let Some(world) = plane.hit(ray.origin, ray.dir) else {
916 return;
917 };
918 let _ = self.pmi_set_label_world(id, world);
919 }
920
921 pub fn pmi_label_drag_end(&mut self) {
923 self.history.break_coalescing();
924 }
925
926 pub fn pmi_hover(&mut self, id: &str) {
928 self.pmi_label_hover_active = true;
929 if self.pmi_hovered.as_deref() == Some(id) {
930 return;
931 }
932 let references: Vec<String> = self
933 .pmi_report
934 .as_ref()
935 .and_then(|report| report.annotation(id))
936 .map(|row| row.references.clone())
937 .unwrap_or_default();
938 let mut solids: Vec<String> = Vec::new();
939 let mut faces: Vec<String> = Vec::new();
940 let mut edges: Vec<String> = Vec::new();
941 for reference in &references {
942 if let Some(at) = reference.find('@') {
943 solids.push(reference[..at].to_string());
944 } else if self.scene_has_face(reference) {
945 faces.push(reference.clone());
946 } else if self.scene_has_edge(reference) {
947 edges.push(reference.clone());
948 } else if self.scene.solid(reference).is_some() {
949 solids.push(reference.clone());
950 } else {
951 let prefix = format!("{reference}:");
952 solids.extend(self.scene.solids().iter().filter(|s| s.name.starts_with(&prefix)).map(|s| s.name.clone()));
953 }
954 }
955 self.clear_hover();
956 self.emphasis.hovered_solids.extend(solids);
957 self.emphasis.hovered_faces.extend(faces);
958 self.emphasis.hovered_edges.extend(edges);
959 self.emphasis.generation = self.emphasis.generation.wrapping_add(1);
960 self.pmi_hovered = Some(id.to_string());
961 self.dirty = true;
962 }
963
964 pub fn pmi_hover_end(&mut self) {
965 if self.pmi_hovered.take().is_some() {
966 self.clear_hover();
967 self.dirty = true;
968 }
969 }
970
971 pub fn take_pmi_label_hover(&mut self) -> bool {
974 std::mem::take(&mut self.pmi_label_hover_active)
975 }
976
977 pub fn pmi_label_clicked(&mut self, id: &str) {
980 self.pmi_select_annotation(id);
981 }
982
983 pub fn pmi_label_double_clicked(&mut self, id: &str) {
986 self.pmi_set_annotation_open(Some(id));
987 }
988
989 pub fn pmi_select_annotation(&mut self, id: &str) -> bool {
994 let state = self.pmi_state();
995 let Some((view, _)) = state.find_annotation(id) else {
996 return false;
997 };
998 if self.pmi_active_view.as_deref() != Some(view.id.as_str()) {
999 let _ = self.pmi_activate_view(&view.id);
1000 }
1001 self.pmi_selected_view = None;
1002 self.pmi_selected_annotation = Some(id.to_string());
1003 self.refresh_pmi_overlay();
1004 true
1005 }
1006
1007 pub fn pmi_select_view(&mut self, id: &str) -> bool {
1013 if self.pmi_state().find_view(id).is_none() {
1014 return false;
1015 }
1016 if self.pmi_selected_annotation.take().is_some() {
1017 self.refresh_pmi_overlay();
1018 }
1019 self.pmi_selected_view = Some(id.to_string());
1020 self.dirty = true;
1021 true
1022 }
1023
1024 pub fn begin_ref_select_for_pmi(
1028 &mut self,
1029 id: &str,
1030 path: Vec<String>,
1031 label: String,
1032 filter: Vec<String>,
1033 multiple: bool,
1034 seed_names: Vec<String>,
1035 ) {
1036 let restore_index = self.history.rollback();
1037 self.selection_filter = SelectionFilter::from_ref_filter(&filter);
1038 self.ref_select = Some(RefSelectState {
1039 feature_id: id.to_string(),
1040 path,
1041 label,
1042 filter,
1043 multiple,
1044 names: seed_names,
1045 restore_index,
1046 target: RefSelectTarget::Pmi,
1047 picks: 0,
1048 claimed: false,
1049 finish_requested: false,
1050 });
1051 self.sync_ref_select_emphasis();
1052 }
1053
1054 pub(crate) fn pmi_commit_refs(&mut self, id: &str, path: &[String], names: &[String], multiple: bool) {
1057 let state = self.pmi_state();
1058 let Some((_, annotation)) = state.find_annotation(id) else {
1059 self.push_notice(format!("unknown PMI annotation '{id}'"));
1060 return;
1061 };
1062 let mut params = annotation.params.clone();
1063 let value = if multiple {
1064 serde_json::Value::Array(names.iter().cloned().map(serde_json::Value::String).collect())
1065 } else {
1066 serde_json::Value::String(names.first().cloned().unwrap_or_default())
1067 };
1068 super::selection_ux::set_json_at(&mut params, path, value);
1069 if let Err(error) = self.pmi_update_annotation_no_rerun(id, ¶ms.to_string()) {
1070 self.push_notice(format!("PMI update failed: {error}"));
1071 }
1072 self.history.break_coalescing();
1076 }
1077
1078 pub(crate) fn pmi_merge_imported(&mut self, lifted: PmiState) {
1084 let mut state = self.pmi_state();
1085 for view in lifted.views {
1086 let id = state.next_id("VIEW");
1087 let mut annotations = Vec::with_capacity(view.annotations.len());
1088 for mut annotation in view.annotations {
1089 let prefix = brep_kernel::pmi_type(&annotation.kind).map(|def| def.short_name).unwrap_or("PMI");
1090 let fresh = state.next_id(prefix);
1091 if let Some(object) = annotation.params.as_object_mut() {
1092 object.insert("id".into(), serde_json::Value::String(fresh));
1093 }
1094 annotations.push(annotation);
1095 }
1096 state.views.push(PmiView {
1097 id,
1098 name: view.name,
1099 camera: view.camera,
1100 display: view.display,
1101 annotations,
1102 });
1103 }
1104 let block = serde_json::to_value(&state).ok();
1105 self.history.set_pmi_block_no_undo(block);
1106 self.rerun_history();
1107 }
1108}
1109
1110pub(super) fn transform_display(
1113 display: &mut crate::scene::SolidDisplay,
1114 center: [f64; 3],
1115 translate: [f64; 3],
1116 rotate_deg: [f64; 3],
1117 scale: [f64; 3],
1118) {
1119 display.revision = crate::scene::next_revision();
1120 let rotate = |v: [f64; 3]| super::rotate_euler_xyz_f64(v, rotate_deg);
1121 let pose = |p: [f64; 3]| -> [f64; 3] {
1122 let local = [(p[0] - center[0]) * scale[0], (p[1] - center[1]) * scale[1], (p[2] - center[2]) * scale[2]];
1123 let rotated = rotate(local);
1124 [rotated[0] + center[0] + translate[0], rotated[1] + center[1] + translate[1], rotated[2] + center[2] + translate[2]]
1125 };
1126 let mut bbox = crate::camera::Aabb::empty();
1127 for position in &mut display.mesh.positions {
1128 let posed = pose([position[0] as f64, position[1] as f64, position[2] as f64]);
1129 *position = [posed[0] as f32, posed[1] as f32, posed[2] as f32];
1130 bbox.expand(posed);
1131 }
1132 for normal in &mut display.mesh.normals {
1133 let rotated = rotate(std::array::from_fn(|i| normal[i] as f64 / scale[i]));
1134 let length = rotated.iter().map(|v| v * v).sum::<f64>().sqrt().max(1e-20);
1135 *normal = rotated.map(|v| (v / length) as f32);
1136 }
1137 for edge in &mut display.edges {
1138 for point in &mut edge.polyline {
1139 let posed = pose([point[0] as f64, point[1] as f64, point[2] as f64]);
1140 *point = [posed[0] as f32, posed[1] as f32, posed[2] as f32];
1141 bbox.expand(posed);
1142 }
1143 }
1144 for vertex in &mut display.vertices {
1145 vertex.position = pose(vertex.position);
1146 bbox.expand(vertex.position);
1147 }
1148 if !bbox.is_empty() {
1149 display.bbox = bbox;
1150 }
1151}
1152