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 world_vertex_ref(solid: &str, position: [f64; 3]) -> String {
45 let trim = |v: f64| {
46 let rounded = (v * 1e9).round() / 1e9;
47 let trimmed = crate::formatting::compact_decimal(rounded, 9);
48 if trimmed == "-0" || trimmed.is_empty() { "0".to_string() } else { trimmed }
49 };
50 format!("{solid}@{},{},{}", trim(position[0]), trim(position[1]), trim(position[2]))
51}
52
53impl EngineState {
54 pub fn pmi_state(&self) -> PmiState {
59 self.history
60 .pmi_block()
61 .and_then(|block| serde_json::from_value(block.clone()).ok())
62 .unwrap_or_default()
63 }
64
65 pub fn pmi_report(&self) -> Option<&PmiReport> {
67 self.pmi_report.as_ref()
68 }
69
70 pub fn pmi_active_view(&self) -> Option<&str> {
71 self.pmi_active_view.as_deref()
72 }
73
74 pub fn pmi_open_annotation(&self) -> Option<&str> {
75 self.pmi_open_annotation.as_deref()
76 }
77
78 pub fn pmi_workbench_entered(&self) -> bool {
80 self.pmi_modeling.is_some()
81 }
82
83 pub fn pmi_state_json(&self) -> String {
86 let state = self.pmi_state();
87 serde_json::json!({
88 "views": state.views,
89 "idCounter": state.id_counter,
90 "activeView": self.pmi_active_view,
91 "openAnnotation": self.pmi_open_annotation,
92 "entered": self.pmi_modeling.is_some(),
93 "datums": state.datum_letters(),
94 "report": self.pmi_report,
95 })
96 .to_string()
97 }
98
99 fn write_pmi_state(&mut self, state: PmiState) -> String {
105 let block = if state.is_empty() { None } else { serde_json::to_value(&state).ok() };
106 self.history.set_pmi_block(block, None);
107 self.rerun_history()
108 }
109
110 fn snapshot_camera(&self) -> PmiCamera {
114 let camera = &self.camera;
115 PmiCamera {
116 eye: camera.eye,
117 target: camera.target,
118 up: camera.up,
119 projection: match camera.projection {
120 Projection::Orthographic { half_height } => PmiProjection::Orthographic { half_height },
121 Projection::Perspective { fov_y_deg } => PmiProjection::Perspective { fov_y_deg },
122 },
123 viewport: [camera.width, camera.height],
124 }
125 }
126
127 fn hidden_solid_names(&self) -> Vec<String> {
129 self.scene
130 .solids()
131 .iter()
132 .filter(|solid| !solid.visible)
133 .map(|solid| solid.name.clone())
134 .collect()
135 }
136
137 pub fn pmi_capture_view(&mut self, name: Option<&str>) -> String {
140 let mut state = self.pmi_state();
141 let id = state.next_id("VIEW");
142 let name = name
143 .map(str::trim)
144 .filter(|n| !n.is_empty())
145 .map(String::from)
146 .unwrap_or_else(|| format!("View {}", state.views.len() + 1));
147 let was_active = self.pmi_active_view.is_some();
151 if was_active {
152 self.pmi_deactivate_view();
153 }
154 self.pmi_remember_modeling();
155 state.views.push(PmiView {
156 id: id.clone(),
157 name,
158 camera: Some(self.snapshot_camera()),
159 display: PmiDisplay {
160 text_size_pt: 12.0,
161 wireframe: self.settings.wireframe,
162 hidden: self.hidden_solid_names(),
163 },
164 annotations: Vec::new(),
165 });
166 self.write_pmi_state(state);
167 let _ = self.pmi_activate_view(&id);
168 id
169 }
170
171 pub fn pmi_rename_view(&mut self, id: &str, name: &str) -> Result<(), String> {
172 let mut state = self.pmi_state();
173 let view = state.find_view_mut(id).ok_or_else(|| format!("no PMI view '{id}'"))?;
174 let name = name.trim();
175 if name.is_empty() {
176 return Err("a view needs a name".into());
177 }
178 view.name = name.to_string();
179 self.write_pmi_state(state);
180 Ok(())
181 }
182
183 pub fn pmi_delete_view(&mut self, id: &str) -> Result<(), String> {
185 let mut state = self.pmi_state();
186 let before = state.views.len();
187 if self.pmi_active_view.as_deref() == Some(id) {
188 self.pmi_deactivate_view();
189 }
190 state.views.retain(|view| view.id != id);
191 if state.views.len() == before {
192 return Err(format!("no PMI view '{id}'"));
193 }
194 if let Some(open) = &self.pmi_open_annotation {
195 if state.find_annotation(open).is_none() {
196 self.pmi_open_annotation = None;
197 }
198 }
199 self.write_pmi_state(state);
200 Ok(())
201 }
202
203 pub fn pmi_update_view_camera(&mut self, id: &str) -> Result<(), String> {
206 let mut state = self.pmi_state();
207 let camera = self.snapshot_camera();
208 let view = state.find_view_mut(id).ok_or_else(|| format!("no PMI view '{id}'"))?;
209 view.camera = Some(camera);
210 self.write_pmi_state(state);
211 Ok(())
212 }
213
214 pub fn pmi_update_view_visibility(&mut self, id: &str) -> Result<(), String> {
217 let hidden = self.hidden_solid_names();
218 self.pmi_set_view_display(id, &PmiViewPatch { hidden: Some(hidden), ..Default::default() })
219 }
220
221 pub fn pmi_set_view_display(&mut self, id: &str, patch: &PmiViewPatch) -> Result<(), String> {
223 let mut state = self.pmi_state();
224 let view = state.find_view_mut(id).ok_or_else(|| format!("no PMI view '{id}'"))?;
225 if let Some(name) = &patch.name {
226 let name = name.trim();
227 if name.is_empty() {
228 return Err("a view needs a name".into());
229 }
230 view.name = name.to_string();
231 }
232 if let Some(size) = patch.text_size_pt {
233 view.display.text_size_pt = brep_kernel::clamp_text_size(size);
234 }
235 if let Some(wireframe) = patch.wireframe {
236 view.display.wireframe = wireframe;
237 }
238 if let Some(hidden) = &patch.hidden {
239 view.display.hidden = hidden.clone();
240 }
241 let display = view.display.clone();
242 self.write_pmi_state(state);
243 if self.pmi_active_view.as_deref() == Some(id) {
244 self.apply_view_display(&display);
245 }
246 Ok(())
247 }
248
249 fn pmi_remember_modeling(&mut self) {
255 if self.pmi_active_view.is_none() || self.pmi_modeling.is_none() {
256 self.pmi_modeling = Some(PmiModelingSnapshot {
257 camera_json: self.camera_state_json(),
258 hidden: self.hidden_solid_names(),
259 wireframe: self.settings.wireframe,
260 });
261 }
262 }
263
264 pub fn pmi_enter_workbench(&mut self) {
267 self.pmi_remember_modeling();
268 }
269
270 pub fn pmi_leave_workbench(&mut self) {
273 self.pmi_deactivate_view();
274 self.pmi_modeling = None;
275 self.pmi_open_annotation = None;
276 self.refresh_pmi_overlay();
277 }
278
279 fn apply_view_display(&mut self, display: &PmiDisplay) {
280 let names: Vec<String> = self.scene.solids().iter().map(|s| s.name.clone()).collect();
281 for name in names {
282 let visible = !display.hidden.contains(&name);
283 self.scene.set_visible(&name, visible);
284 }
285 if self.settings.wireframe != display.wireframe {
286 self.settings.wireframe = display.wireframe;
287 self.settings_generation = self.settings_generation.wrapping_add(1);
288 }
289 self.dirty = true;
290 }
291
292 pub fn pmi_activate_view(&mut self, id: &str) -> Result<(), String> {
296 let state = self.pmi_state();
297 let view = state.find_view(id).ok_or_else(|| format!("no PMI view '{id}'"))?.clone();
298 if self.pmi_active_view.as_deref() != Some(id) {
299 let keep = self.pmi_active_view.is_some();
302 self.pmi_deactivate_view();
303 if !keep {
304 self.pmi_remember_modeling();
305 }
306 }
307 if self.pmi_modeling.is_none() {
308 self.pmi_remember_modeling();
309 }
310 self.pmi_active_view = Some(id.to_string());
311 if let Some(camera) = &view.camera {
312 let (kind, scale) = match camera.projection {
313 PmiProjection::Orthographic { half_height } => ("orthographic", half_height),
314 PmiProjection::Perspective { fov_y_deg } => ("perspective", fov_y_deg),
315 };
316 let json = serde_json::json!({
317 "kind": kind, "eye": camera.eye, "target": camera.target, "up": camera.up, "scale": scale,
318 })
319 .to_string();
320 let _ = self.apply_camera_state_json(&json);
321 self.controls_sync_after_camera_apply();
322 }
323 self.apply_view_display(&view.display);
324 self.pmi_apply_explode();
325 if let Some(open) = &self.pmi_open_annotation {
326 if !view.annotations.iter().any(|a| a.id() == open) {
327 self.pmi_open_annotation = None;
328 }
329 }
330 self.refresh_pmi_overlay();
331 Ok(())
332 }
333
334 pub fn pmi_deactivate_view(&mut self) {
337 if self.pmi_active_view.take().is_none() {
338 return;
339 }
340 self.pmi_restore_explode();
341 if let Some(snapshot) = self.pmi_modeling.clone() {
342 let names: Vec<String> = self.scene.solids().iter().map(|s| s.name.clone()).collect();
343 for name in names {
344 let visible = !snapshot.hidden.contains(&name);
345 self.scene.set_visible(&name, visible);
346 }
347 if self.settings.wireframe != snapshot.wireframe {
348 self.settings.wireframe = snapshot.wireframe;
349 self.settings_generation = self.settings_generation.wrapping_add(1);
350 }
351 let _ = self.apply_camera_state_json(&snapshot.camera_json);
352 self.controls_sync_after_camera_apply();
353 }
354 self.pmi_open_annotation = None;
355 self.dirty = true;
356 self.refresh_pmi_overlay();
357 }
358
359 fn controls_sync_after_camera_apply(&mut self) {
361 self.dirty = true;
365 }
366
367 pub(crate) fn pmi_apply_explode(&mut self) {
372 self.pmi_restore_explode();
373 let Some(active) = self.pmi_active_view.clone() else {
374 return;
375 };
376 let Some(report) = self.pmi_report.as_ref() else {
377 return;
378 };
379 let Some(view) = report.view(&active) else {
380 return;
381 };
382 let poses: Vec<(Vec<String>, [f64; 3], [f64; 3], [f64; 3], [f64; 3])> = view
383 .annotations
384 .iter()
385 .filter(|row| row.enabled && row.status == PmiStatus::Ok)
386 .filter_map(|row| match &row.geometry {
387 PmiGeometry::Explode { solids, translate, rotate_deg, scale, center, .. } => {
388 Some((solids.clone(), *translate, *rotate_deg, *scale, *center))
389 }
390 _ => None,
391 })
392 .collect();
393 for (solids, translate, rotate_deg, scale, center) in poses {
394 for name in solids {
395 let Some(display) = self.scene.solid(&name) else { continue };
396 self.pmi_explode_originals
397 .entry(name.clone())
398 .or_insert_with(|| display.clone());
399 if let Some(display) = self.scene.solid_mut(&name) {
400 transform_display(display, center, translate, rotate_deg, scale);
401 }
402 }
403 }
404 if !self.pmi_explode_originals.is_empty() {
405 self.dirty = true;
406 }
407 }
408
409 pub(crate) fn pmi_restore_explode(&mut self) {
411 let originals = std::mem::take(&mut self.pmi_explode_originals);
412 for (name, original) in originals {
413 if let Some(display) = self.scene.solid_mut(&name) {
414 *display = original;
415 }
416 }
417 self.dirty = true;
418 }
419
420 pub(crate) fn pmi_after_apply(&mut self) {
423 self.pmi_restore_explode();
427 if let Some(active) = self.pmi_active_view.clone() {
428 let exists = self.pmi_state().find_view(&active).is_some();
430 if !exists {
431 self.pmi_active_view = None;
432 self.pmi_open_annotation = None;
433 } else {
434 self.pmi_apply_explode();
435 }
436 }
437 if let Some(open) = &self.pmi_open_annotation {
438 if self.pmi_state().find_annotation(open).is_none() {
439 self.pmi_open_annotation = None;
440 }
441 }
442 self.refresh_pmi_overlay();
443 }
444
445 pub fn pmi_add_annotation(
452 &mut self,
453 view_id: Option<&str>,
454 type_id: &str,
455 params_json: &str,
456 ) -> Result<String, String> {
457 let def = brep_kernel::pmi_type(type_id).ok_or_else(|| format!("unknown PMI annotation type '{type_id}'"))?;
458 let mut state = self.pmi_state();
459 let view_id = view_id
460 .map(String::from)
461 .or_else(|| self.pmi_active_view.clone())
462 .ok_or_else(|| "no active PMI view — capture or activate a view first".to_string())?;
463 let id = state.next_id(def.short_name);
464 let mut params: serde_json::Value = serde_json::from_str(params_json).unwrap_or_else(|_| serde_json::json!({}));
465 if !params.is_object() {
466 params = serde_json::json!({});
467 }
468 let schema = (def.schema)();
470 if let (Some(fields), Some(object)) = (
471 schema.get("inputParamsSchema").and_then(serde_json::Value::as_object),
472 params.as_object_mut(),
473 ) {
474 for (key, spec) in fields {
475 if !object.contains_key(key) {
476 if let Some(default) = spec.get("default_value") {
477 if !default.is_null() {
478 object.insert(key.clone(), default.clone());
479 }
480 }
481 }
482 }
483 object.insert("id".into(), serde_json::Value::String(id.clone()));
484 if type_id == "datum" {
485 let letter = object.get("letter").and_then(serde_json::Value::as_str).unwrap_or("").trim().to_string();
486 if letter.is_empty() {
487 if let Some(next) = state.next_datum_letter() {
488 object.insert("letter".into(), serde_json::Value::String(next));
489 }
490 }
491 }
492 }
493 let view = state.find_view_mut(&view_id).ok_or_else(|| format!("no PMI view '{view_id}'"))?;
494 view.annotations.push(PmiAnnotation {
495 kind: type_id.to_string(),
496 enabled: true,
497 params,
498 label_world: None,
499 });
500 self.pmi_open_annotation = Some(id.clone());
501 self.write_pmi_state(state);
502 Ok(id)
503 }
504
505 pub fn pmi_update_annotation(&mut self, id: &str, params_json: &str) -> Result<(), String> {
507 self.pmi_update_annotation_no_rerun(id, params_json)?;
508 self.rerun_history();
509 Ok(())
510 }
511
512 pub(crate) fn pmi_update_annotation_no_rerun(&mut self, id: &str, params_json: &str) -> Result<(), String> {
516 let mut state = self.pmi_state();
517 let annotation = state.find_annotation_mut(id).ok_or_else(|| format!("no PMI annotation '{id}'"))?;
518 let mut params: serde_json::Value =
519 serde_json::from_str(params_json).map_err(|error| format!("annotation params: {error}"))?;
520 if let Some(object) = params.as_object_mut() {
521 object.insert("id".into(), serde_json::Value::String(id.to_string()));
522 }
523 annotation.params = params;
524 let block = serde_json::to_value(&state).ok();
525 self.history.set_pmi_block(block, Some(&format!("pmi:params:{id}")));
526 Ok(())
527 }
528
529 pub fn pmi_remove_annotation(&mut self, id: &str) -> Result<(), String> {
530 let mut state = self.pmi_state();
531 let Some((view_index, index)) = state.locate_annotation(id) else {
532 return Err(format!("no PMI annotation '{id}'"));
533 };
534 state.views[view_index].annotations.remove(index);
535 if self.pmi_open_annotation.as_deref() == Some(id) {
536 self.pmi_open_annotation = None;
537 }
538 self.write_pmi_state(state);
539 Ok(())
540 }
541
542 pub fn pmi_set_annotation_enabled(&mut self, id: &str, enabled: bool) -> Result<(), String> {
543 let mut state = self.pmi_state();
544 let annotation = state.find_annotation_mut(id).ok_or_else(|| format!("no PMI annotation '{id}'"))?;
545 if annotation.enabled == enabled {
546 return Ok(());
547 }
548 annotation.enabled = enabled;
549 self.write_pmi_state(state);
550 Ok(())
551 }
552
553 pub fn pmi_move_annotation(&mut self, id: &str, index: usize) -> Result<(), String> {
555 let mut state = self.pmi_state();
556 let Some((view_index, from)) = state.locate_annotation(id) else {
557 return Err(format!("no PMI annotation '{id}'"));
558 };
559 let annotations = &mut state.views[view_index].annotations;
560 let annotation = annotations.remove(from);
561 let to = index.min(annotations.len());
562 annotations.insert(to, annotation);
563 self.write_pmi_state(state);
564 Ok(())
565 }
566
567 pub fn pmi_move_annotation_to_view(&mut self, id: &str, view_id: &str) -> Result<(), String> {
569 let mut state = self.pmi_state();
570 let Some((view_index, from)) = state.locate_annotation(id) else {
571 return Err(format!("no PMI annotation '{id}'"));
572 };
573 if state.find_view(view_id).is_none() {
574 return Err(format!("no PMI view '{view_id}'"));
575 }
576 let annotation = state.views[view_index].annotations.remove(from);
577 state.find_view_mut(view_id).expect("checked").annotations.push(annotation);
578 self.write_pmi_state(state);
579 Ok(())
580 }
581
582 pub fn pmi_set_annotation_open(&mut self, id: Option<&str>) {
585 match id {
586 Some(id) => {
587 let state = self.pmi_state();
588 if let Some((view, _)) = state.find_annotation(id) {
589 let view_id = view.id.clone();
590 if self.pmi_active_view.as_deref() != Some(view_id.as_str()) {
591 let _ = self.pmi_activate_view(&view_id);
592 }
593 self.pmi_open_annotation = Some(id.to_string());
594 }
595 }
596 None => self.pmi_open_annotation = None,
597 }
598 self.refresh_pmi_overlay();
599 }
600
601 pub fn pmi_set_label_world(&mut self, id: &str, world: [f64; 3]) -> Result<(), String> {
607 let mut state = self.pmi_state();
608 let annotation = state.find_annotation_mut(id).ok_or_else(|| format!("no PMI annotation '{id}'"))?;
609 annotation.label_world = Some(world);
610 let block = serde_json::to_value(&state).ok();
611 self.history.set_pmi_block(block, Some(&format!("pmi:label:{id}")));
612 if let Some(report) = self.pmi_report.as_mut() {
613 if let Some(row) = report.annotation_mut(id) {
614 row.label_world = world;
615 if let PmiGeometry::Note { position } = &mut row.geometry {
616 *position = world;
617 }
618 }
619 }
620 self.refresh_pmi_overlay();
621 Ok(())
622 }
623
624 pub fn pmi_label_drag_to(&mut self, id: &str, x: f64, y: f64) {
628 let Some((current, plane)) = self
629 .pmi_report
630 .as_ref()
631 .and_then(|r| r.annotation(id))
632 .map(|r| (r.label_world, r.plane))
633 else {
634 return;
635 };
636 let ray = self.camera.pick_ray(x, y);
637 let plane = plane.unwrap_or_else(|| {
641 let (_, _, view) = self.camera.basis();
642 brep_kernel::PmiPlane {
643 origin: current,
644 normal: view,
645 x_axis: [0.0; 3],
646 }
647 });
648 let Some(world) = plane.hit(ray.origin, ray.dir) else {
649 return;
650 };
651 let _ = self.pmi_set_label_world(id, world);
652 }
653
654 pub fn pmi_label_drag_end(&mut self) {
656 self.history.break_coalescing();
657 }
658
659 pub fn pmi_hover(&mut self, id: &str) {
661 self.pmi_label_hover_active = true;
662 if self.pmi_hovered.as_deref() == Some(id) {
663 return;
664 }
665 let references: Vec<String> = self
666 .pmi_report
667 .as_ref()
668 .and_then(|report| report.annotation(id))
669 .map(|row| row.references.clone())
670 .unwrap_or_default();
671 let mut solids: Vec<String> = Vec::new();
672 let mut faces: Vec<String> = Vec::new();
673 let mut edges: Vec<String> = Vec::new();
674 for reference in &references {
675 if let Some(at) = reference.find('@') {
676 solids.push(reference[..at].to_string());
677 } else if self.scene_has_face(reference) {
678 faces.push(reference.clone());
679 } else if self.scene_has_edge(reference) {
680 edges.push(reference.clone());
681 } else if self.scene.solid(reference).is_some() {
682 solids.push(reference.clone());
683 } else {
684 let prefix = format!("{reference}:");
685 solids.extend(self.scene.solids().iter().filter(|s| s.name.starts_with(&prefix)).map(|s| s.name.clone()));
686 }
687 }
688 self.clear_hover();
689 self.emphasis.hovered_solids.extend(solids);
690 self.emphasis.hovered_faces.extend(faces);
691 self.emphasis.hovered_edges.extend(edges);
692 self.emphasis.generation = self.emphasis.generation.wrapping_add(1);
693 self.pmi_hovered = Some(id.to_string());
694 self.dirty = true;
695 }
696
697 pub fn pmi_hover_end(&mut self) {
698 if self.pmi_hovered.take().is_some() {
699 self.clear_hover();
700 self.dirty = true;
701 }
702 }
703
704 pub fn take_pmi_label_hover(&mut self) -> bool {
707 std::mem::take(&mut self.pmi_label_hover_active)
708 }
709
710 pub fn pmi_label_clicked(&mut self, id: &str) {
712 self.pmi_set_annotation_open(Some(id));
713 }
714
715 pub fn begin_ref_select_for_pmi(
719 &mut self,
720 id: &str,
721 path: Vec<String>,
722 label: String,
723 filter: Vec<String>,
724 multiple: bool,
725 seed_names: Vec<String>,
726 ) {
727 let restore_index = self.history.rollback();
728 self.selection_filter = SelectionFilter::from_ref_filter(&filter);
729 self.ref_select = Some(RefSelectState {
730 feature_id: id.to_string(),
731 path,
732 label,
733 filter,
734 multiple,
735 names: seed_names,
736 restore_index,
737 target: RefSelectTarget::Pmi,
738 });
739 self.sync_ref_select_emphasis();
740 }
741
742 pub(crate) fn pmi_commit_refs(&mut self, id: &str, path: &[String], names: &[String], multiple: bool) {
745 let state = self.pmi_state();
746 let Some((_, annotation)) = state.find_annotation(id) else {
747 self.push_notice(format!("unknown PMI annotation '{id}'"));
748 return;
749 };
750 let mut params = annotation.params.clone();
751 let value = if multiple {
752 serde_json::Value::Array(names.iter().cloned().map(serde_json::Value::String).collect())
753 } else {
754 serde_json::Value::String(names.first().cloned().unwrap_or_default())
755 };
756 super::selection_ux::set_json_at(&mut params, path, value);
757 if let Err(error) = self.pmi_update_annotation_no_rerun(id, ¶ms.to_string()) {
758 self.push_notice(format!("PMI update failed: {error}"));
759 }
760 }
761
762 pub(crate) fn pmi_merge_imported(&mut self, lifted: PmiState) {
768 let mut state = self.pmi_state();
769 for view in lifted.views {
770 let id = state.next_id("VIEW");
771 let mut annotations = Vec::with_capacity(view.annotations.len());
772 for mut annotation in view.annotations {
773 let prefix = brep_kernel::pmi_type(&annotation.kind).map(|def| def.short_name).unwrap_or("PMI");
774 let fresh = state.next_id(prefix);
775 if let Some(object) = annotation.params.as_object_mut() {
776 object.insert("id".into(), serde_json::Value::String(fresh));
777 }
778 annotations.push(annotation);
779 }
780 state.views.push(PmiView {
781 id,
782 name: view.name,
783 camera: view.camera,
784 display: view.display,
785 annotations,
786 });
787 }
788 let block = serde_json::to_value(&state).ok();
789 self.history.set_pmi_block_no_undo(block);
790 self.rerun_history();
791 }
792}
793
794fn transform_display(
797 display: &mut crate::scene::SolidDisplay,
798 center: [f64; 3],
799 translate: [f64; 3],
800 rotate_deg: [f64; 3],
801 scale: [f64; 3],
802) {
803 let rotate = |v: [f64; 3]| super::rotate_euler_xyz_f64(v, rotate_deg);
804 let pose = |p: [f64; 3]| -> [f64; 3] {
805 let local = [(p[0] - center[0]) * scale[0], (p[1] - center[1]) * scale[1], (p[2] - center[2]) * scale[2]];
806 let rotated = rotate(local);
807 [rotated[0] + center[0] + translate[0], rotated[1] + center[1] + translate[1], rotated[2] + center[2] + translate[2]]
808 };
809 let mut bbox = crate::camera::Aabb::empty();
810 for position in &mut display.mesh.positions {
811 let posed = pose([position[0] as f64, position[1] as f64, position[2] as f64]);
812 *position = [posed[0] as f32, posed[1] as f32, posed[2] as f32];
813 bbox.expand(posed);
814 }
815 for normal in &mut display.mesh.normals {
816 let rotated = rotate([normal[0] as f64, normal[1] as f64, normal[2] as f64]);
817 *normal = [rotated[0] as f32, rotated[1] as f32, rotated[2] as f32];
818 }
819 for edge in &mut display.edges {
820 for point in &mut edge.polyline {
821 let posed = pose([point[0] as f64, point[1] as f64, point[2] as f64]);
822 *point = [posed[0] as f32, posed[1] as f32, posed[2] as f32];
823 bbox.expand(posed);
824 }
825 }
826 for vertex in &mut display.vertices {
827 vertex.position = pose(vertex.position);
828 bbox.expand(vertex.position);
829 }
830 if !bbox.is_empty() {
831 display.bbox = bbox;
832 }
833}
834
835