1pub mod anchor;
68pub mod annotations;
69pub mod font;
70pub mod layout;
71pub mod resolve;
72mod plugin;
73pub use plugin::{PluginAnnotationOutput, PluginAnnotationReplay};
74
75use serde::{Deserialize, Serialize};
76use serde_json::Value;
77use std::collections::BTreeMap;
78
79use crate::feature_pipeline::{Env, HistoryRequest, SceneMap};
80
81pub use anchor::{
82 balloon_head, nearest_displayed_point, nearest_displayed_point_on_edge, nearest_point_on_edge, nearest_point_on_solid,
83 pose_apply, pose_unapply, BalloonAnchor, BalloonAnchorMode, HalfSpace, HeadScope, PmiPose, SolidPoses,
84};
85pub use annotations::{pmi_schema_catalogue, pmi_type, PmiTypeDef, PMI_TYPES};
86
87#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
96#[serde(from = "PmiStateWire")]
97pub struct PmiState {
98 #[serde(default)]
99 pub views: Vec<PmiView>,
100 #[serde(default, rename = "idCounter")]
103 pub id_counter: u64,
104}
105
106#[derive(Deserialize)]
108struct PmiStateWire {
109 #[serde(default)]
110 views: Vec<PmiView>,
111 #[serde(default, rename = "idCounter")]
112 id_counter: u64,
113}
114
115impl From<PmiStateWire> for PmiState {
116 fn from(wire: PmiStateWire) -> Self {
117 let mut state = PmiState { views: wire.views, id_counter: wire.id_counter };
118 for index in 0..state.views.len() {
119 let legacy = std::mem::take(&mut state.views[index].display.legacy_hidden);
120 if legacy.is_empty() {
121 continue;
122 }
123 let mut names = state.views[index].baseline_hidden();
124 for name in legacy {
125 if !names.contains(&name) {
126 names.push(name);
127 }
128 }
129 let id = state.views[index].id.clone();
130 state.set_view_baseline(&id, &names);
131 }
132 state
133 }
134}
135
136#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
138pub struct PmiView {
139 pub id: String,
140 #[serde(default)]
141 pub name: String,
142 #[serde(default, skip_serializing_if = "Option::is_none")]
143 pub camera: Option<PmiCamera>,
144 #[serde(default)]
145 pub display: PmiDisplay,
146 #[serde(default)]
147 pub annotations: Vec<PmiAnnotation>,
148}
149
150#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
155pub struct PmiCamera {
156 pub eye: [f64; 3],
157 pub target: [f64; 3],
158 pub up: [f64; 3],
159 pub projection: PmiProjection,
160 #[serde(default = "default_viewport")]
162 pub viewport: [f64; 2],
163}
164
165fn default_viewport() -> [f64; 2] {
166 [1280.0, 800.0]
167}
168
169impl PmiCamera {
170 pub fn view_direction(&self) -> [f64; 3] {
172 let d = [
173 self.target[0] - self.eye[0],
174 self.target[1] - self.eye[1],
175 self.target[2] - self.eye[2],
176 ];
177 let len = (d[0] * d[0] + d[1] * d[1] + d[2] * d[2]).sqrt();
178 if len < 1e-12 {
179 [0.0, 0.0, -1.0]
180 } else {
181 [d[0] / len, d[1] / len, d[2] / len]
182 }
183 }
184}
185
186#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
187#[serde(tag = "kind", rename_all = "camelCase")]
188pub enum PmiProjection {
189 Orthographic {
190 #[serde(rename = "halfHeight")]
191 half_height: f64,
192 },
193 Perspective {
194 #[serde(rename = "fovYDeg")]
195 fov_y_deg: f64,
196 },
197}
198
199#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
201pub struct PmiDisplay {
202 #[serde(default = "default_text_size", rename = "textSizePt")]
204 pub text_size_pt: f64,
205 #[serde(default)]
206 pub wireframe: bool,
207 #[serde(default, rename = "hidden", skip_serializing)]
212 legacy_hidden: Vec<String>,
213 #[serde(default, skip_serializing_if = "Option::is_none")]
214 pub section: Option<PmiSection>,
215}
216
217impl PmiDisplay {
218 pub fn new(text_size_pt: f64, wireframe: bool, section: Option<PmiSection>) -> Self {
220 Self { text_size_pt, wireframe, legacy_hidden: Vec::new(), section }
221 }
222}
223
224#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
232pub struct PmiSection {
233 pub axis: String,
234 pub offset: f64,
235 #[serde(default)]
236 pub flip: bool,
237 #[serde(default, skip_serializing_if = "Option::is_none")]
238 pub origin: Option<[f64; 3]>,
239 #[serde(default, skip_serializing_if = "Option::is_none")]
240 pub normal: Option<[f64; 3]>,
241 #[serde(default, skip_serializing_if = "Option::is_none")]
242 pub plane: Option<String>,
243 #[serde(default, skip_serializing_if = "Vec::is_empty")]
246 pub exclude: Vec<String>,
247 #[serde(default, skip_serializing_if = "Vec::is_empty")]
252 pub planes: Vec<PmiSectionPlane>,
253}
254
255#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
259pub struct PmiSectionPlane {
260 pub axis: String,
261 #[serde(default)]
262 pub offset: f64,
263 #[serde(default)]
264 pub flip: bool,
265 #[serde(default, skip_serializing_if = "Option::is_none")]
266 pub normal: Option<[f64; 3]>,
267}
268
269impl PmiSectionPlane {
270 pub fn axis_normal(&self, camera: Option<&PmiCamera>) -> Option<[f64; 3]> {
273 match self.axis.as_str() {
274 "X" => Some([1., 0., 0.]),
275 "Y" => Some([0., 1., 0.]),
276 "Z" => Some([0., 0., 1.]),
277 "View" => self.normal.or_else(|| camera.map(PmiCamera::view_direction)),
278 _ => None,
279 }
280 }
281}
282pub fn entry_names_solid(entry: &str, solid: &str) -> bool {
288 let entry = entry.trim();
289 !entry.is_empty() && (solid == entry || solid.strip_prefix(entry).is_some_and(|rest| rest.starts_with(':')))
290}
291
292impl PmiSection {
293 pub fn excludes(&self, solid: &str) -> bool {
297 self.exclude.iter().any(|entry| entry_names_solid(entry, solid))
298 }
299
300 pub fn axis_normal(&self, camera: Option<&PmiCamera>) -> Option<[f64; 3]> {
303 match self.axis.as_str() {
304 "X" => Some([1., 0., 0.]),
305 "Y" => Some([0., 1., 0.]),
306 "Z" => Some([0., 0., 1.]),
307 "Plane" => self.normal,
308 _ => self.normal.or_else(|| camera.map(PmiCamera::view_direction)),
309 }
310 }
311
312 pub fn freeze(&mut self, camera: Option<&PmiCamera>) {
315 if self.axis == "Plane" {
316 self.origin = None;
318 self.normal = None;
319 return;
320 }
321 self.plane = None;
322 let Some(camera) = camera else { return };
323 for plane in &mut self.planes {
324 if plane.axis == "View" {
325 plane.normal.get_or_insert(camera.view_direction());
326 } else {
327 plane.normal = None;
328 }
329 }
330 self.origin.get_or_insert(camera.target);
331 if self.axis == "View" {
332 self.normal.get_or_insert(camera.view_direction());
333 } else {
334 self.normal = None;
335 }
336 }
337}
338impl PmiView {
339 pub fn section_plane(&self) -> Option<([f64; 3], [f64; 3])> {
340 let section = self.display.section.as_ref()?;
341 let camera = self.camera.as_ref();
342 let mut normal = section.axis_normal(camera)?;
343 let origin = section.origin.or_else(|| camera.filter(|_| section.axis != "Plane").map(|c| c.target))?;
344 let point = std::array::from_fn(|i| origin[i] + normal[i] * section.offset);
345 if section.flip {
346 normal = normal.map(|v| -v);
347 }
348 Some((point, normal))
349 }
350
351 pub fn section_plane_in(&self, report: Option<&PmiViewReport>) -> Option<([f64; 3], [f64; 3])> {
354 match self.display.section.as_ref() {
355 Some(section) if section.axis == "Plane" => report.and_then(|report| report.section_plane),
356 _ => self.section_plane(),
357 }
358 }
359
360 pub fn section_planes_in(&self, report: Option<&PmiViewReport>) -> Vec<([f64; 3], [f64; 3])> {
366 self.section_plane_in(report).map_or_else(Vec::new, |first| self.with_further_planes(first))
367 }
368
369 pub fn section_planes(&self) -> Vec<([f64; 3], [f64; 3])> {
372 self.section_plane().map_or_else(Vec::new, |first| self.with_further_planes(first))
373 }
374
375 fn with_further_planes(&self, first: ([f64; 3], [f64; 3])) -> Vec<([f64; 3], [f64; 3])> {
376 let mut planes = vec![first];
377 let Some(section) = self.display.section.as_ref() else { return planes };
378 let camera = self.camera.as_ref();
379 let origin = section.origin.or_else(|| camera.filter(|_| section.axis != "Plane").map(|c| c.target)).unwrap_or(first.0);
380 for plane in §ion.planes {
381 let Some(mut normal) = plane.axis_normal(camera) else { continue };
382 let point = std::array::from_fn(|i| origin[i] + normal[i] * plane.offset);
383 if plane.flip {
384 normal = normal.map(|v| -v);
385 }
386 planes.push((point, normal));
387 }
388 planes
389 }
390}
391
392pub const HIDE_BASELINE_PARAM: &str = "baseline";
397
398pub fn baseline_hide_id(view_id: &str) -> String {
402 format!("{}-{view_id}", annotations::hide::DEF.short_name)
403}
404
405impl PmiAnnotation {
406 pub fn hide_step(id: &str, targets: &[String], baseline: bool) -> Self {
409 let mut params = serde_json::json!({ "id": id, "targets": targets });
410 if baseline {
411 params[HIDE_BASELINE_PARAM] = Value::Bool(true);
412 }
413 Self {
414 kind: annotations::hide::DEF.type_id.to_string(),
415 enabled: true,
416 params,
417 persistent_data: Value::Null,
418 plugin_replay: None,
419 label_world: None,
420 }
421 }
422
423 pub fn is_baseline_hide(&self) -> bool {
425 self.kind == annotations::hide::DEF.type_id && self.flag(HIDE_BASELINE_PARAM)
426 }
427}
428
429impl PmiView {
430 pub fn hides(&self, solid: &str) -> bool {
437 self.hide_targets().any(|entry| entry_names_solid(entry, solid))
438 }
439
440 pub fn baseline_hidden(&self) -> Vec<String> {
442 self.annotations.iter().filter(|a| a.is_baseline_hide()).flat_map(|a| a.references("targets")).collect()
443 }
444
445 pub fn hide_targets(&self) -> impl Iterator<Item = &str> {
447 self.annotations
448 .iter()
449 .filter(|annotation| annotation.enabled && annotation.kind == annotations::hide::DEF.type_id)
450 .flat_map(|annotation| match annotation.params.get("targets") {
451 Some(Value::String(name)) => std::slice::from_ref(name).iter().map(String::as_str).collect::<Vec<_>>(),
452 Some(Value::Array(items)) => items.iter().filter_map(Value::as_str).collect(),
453 _ => Vec::new(),
454 })
455 .map(str::trim)
456 .filter(|name| !name.is_empty())
457 }
458}
459
460fn default_text_size() -> f64 {
461 12.0
462}
463
464impl Default for PmiDisplay {
465 fn default() -> Self {
466 Self {
467 text_size_pt: default_text_size(),
468 wireframe: false,
469 legacy_hidden: Vec::new(),
470 section: None,
471 }
472 }
473}
474
475pub fn clamp_text_size(size: f64) -> f64 {
477 if !size.is_finite() {
478 return default_text_size();
479 }
480 size.clamp(1.0, 288.0)
481}
482
483#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
485pub struct PmiAnnotation {
486 #[serde(rename = "type")]
487 pub kind: String,
488 #[serde(default = "default_true")]
489 pub enabled: bool,
490 #[serde(default, rename = "inputParams")]
491 pub params: Value,
492 #[serde(default, rename = "persistentData", skip_serializing_if = "Value::is_null")]
493 pub persistent_data: Value,
494 #[serde(default, rename = "pluginReplay", skip_serializing_if = "Option::is_none")]
496 pub plugin_replay: Option<PluginAnnotationReplay>,
497 #[serde(default, rename = "labelWorld", skip_serializing_if = "Option::is_none")]
500 pub label_world: Option<[f64; 3]>,
501}
502
503fn is_false(value: &bool) -> bool { !value }
504
505fn default_true() -> bool {
506 true
507}
508
509impl PmiAnnotation {
510 pub fn id(&self) -> &str {
512 self.params
513 .get("id")
514 .and_then(Value::as_str)
515 .unwrap_or("")
516 }
517
518 pub fn text(&self, key: &str) -> &str {
520 self.params
521 .get(key)
522 .and_then(Value::as_str)
523 .map(str::trim)
524 .unwrap_or("")
525 }
526
527 pub fn flag(&self, key: &str) -> bool {
529 self.params
530 .get(key)
531 .and_then(Value::as_bool)
532 .unwrap_or(false)
533 }
534
535 pub fn number(&self, key: &str, env: &Env, default: f64) -> Result<f64, String> {
538 match self.params.get(key) {
539 None | Some(Value::Null) => Ok(default),
540 Some(Value::Number(number)) => Ok(number.as_f64().unwrap_or(default)),
541 Some(Value::Bool(flag)) => Ok(if *flag { 1.0 } else { 0.0 }),
542 Some(Value::String(source)) => {
543 let source = source.trim();
544 if source.is_empty() {
545 return Ok(default);
546 }
547 env.eval(source)
548 .map_err(|error| format!("{key}: {error}"))
549 }
550 Some(other) => Err(format!("{key}: expected a number, got {other}")),
551 }
552 }
553
554 pub fn plane_ref(&self) -> Option<&str> {
557 let name = self.text("plane").trim();
558 (!name.is_empty()).then_some(name)
559 }
560
561 pub fn references(&self, key: &str) -> Vec<String> {
564 match self.params.get(key) {
565 Some(Value::String(name)) => {
566 let name = name.trim();
567 if name.is_empty() {
568 Vec::new()
569 } else {
570 vec![name.to_string()]
571 }
572 }
573 Some(Value::Array(items)) => items
574 .iter()
575 .filter_map(Value::as_str)
576 .map(str::trim)
577 .filter(|name| !name.is_empty())
578 .map(String::from)
579 .collect(),
580 _ => Vec::new(),
581 }
582 }
583}
584
585impl PmiState {
586 pub fn set_view_baseline(&mut self, view_id: &str, names: &[String]) -> bool {
590 let Some(view) = self.find_view(view_id) else { return false };
591 if view.baseline_hidden() == names && view.annotations.iter().filter(|a| a.is_baseline_hide()).count() <= 1 {
592 return false;
593 }
594 let at = view.annotations.iter().position(PmiAnnotation::is_baseline_hide);
595 let kept_id = at.map(|at| view.annotations[at].id().to_string()).filter(|id| !id.is_empty());
596 let id = kept_id.unwrap_or_else(|| baseline_hide_id(view_id));
597 let view = self.find_view_mut(view_id).expect("found above");
598 view.annotations.retain(|a| !a.is_baseline_hide());
599 if !names.is_empty() {
600 let step = PmiAnnotation::hide_step(&id, names, true);
601 let at = at.unwrap_or(view.annotations.len()).min(view.annotations.len());
602 view.annotations.insert(at, step);
603 }
604 true
605 }
606
607 pub fn hide_in_view(&mut self, view_id: &str, names: &[String]) -> Option<String> {
611 let view = self.find_view(view_id)?;
612 let mut targets: Vec<String> = Vec::new();
613 for name in names {
614 if !view.hides(name) && !targets.contains(name) {
615 targets.push(name.clone());
616 }
617 }
618 if targets.is_empty() {
619 return None;
620 }
621 let id = self.next_id(annotations::hide::DEF.short_name);
622 self.find_view_mut(view_id)?.annotations.push(PmiAnnotation::hide_step(&id, &targets, false));
623 Some(id)
624 }
625
626 pub fn show_in_view(&mut self, view_id: &str, names: &[String], solids: &[String]) -> bool {
632 let Some(view) = self.find_view_mut(view_id) else { return false };
633 let shown = |solid: &str| names.iter().any(|name| name == solid);
634 let mut changed = false;
635 let mut emptied: Vec<usize> = Vec::new();
636 for (index, annotation) in view.annotations.iter_mut().enumerate() {
637 if !annotation.enabled || annotation.kind != annotations::hide::DEF.type_id {
638 continue;
639 }
640 let targets = annotation.references("targets");
641 if !targets.iter().any(|entry| names.iter().any(|name| entry_names_solid(entry, name))) {
642 continue;
643 }
644 let mut kept: Vec<String> = Vec::new();
645 for entry in targets {
646 if !names.iter().any(|name| entry_names_solid(&entry, name)) {
647 kept.push(entry);
648 continue;
649 }
650 for solid in solids.iter().filter(|solid| entry_names_solid(&entry, solid) && !shown(solid)) {
651 if !kept.contains(solid) {
652 kept.push(solid.clone());
653 }
654 }
655 }
656 changed = true;
657 if kept.is_empty() {
658 emptied.push(index);
659 } else {
660 annotation.params["targets"] = serde_json::json!(kept);
661 }
662 }
663 for index in emptied.into_iter().rev() {
664 view.annotations.remove(index);
665 }
666 changed
667 }
668
669 pub fn set_view_hidden(&mut self, view_id: &str, names: &[String], solids: &[String]) -> bool {
674 let Some(view) = self.find_view(view_id) else { return false };
675 let extra: Vec<String> = solids.iter().filter(|solid| view.hides(solid) && !names.contains(solid)).cloned().collect();
676 let mut changed = !extra.is_empty() && self.show_in_view(view_id, &extra, solids);
677 let view = self.find_view(view_id).expect("found above");
678 let mut baseline = view.baseline_hidden();
679 let missing: Vec<String> = names.iter().filter(|name| !view.hides(name)).cloned().collect();
680 if !missing.is_empty() {
681 if view.annotations.iter().any(|step| step.is_baseline_hide() && !step.enabled) {
682 changed |= self.hide_in_view(view_id, &missing).is_some();
685 } else {
686 baseline.extend(missing);
687 changed |= self.set_view_baseline(view_id, &baseline);
688 }
689 }
690 changed
691 }
692
693 pub fn next_id(&mut self, prefix: &str) -> String {
697 let seen = self.max_numeric_suffix();
698 if seen > self.id_counter {
699 self.id_counter = seen;
700 }
701 loop {
702 self.id_counter += 1;
703 let candidate = format!("{prefix}{}", self.id_counter);
704 if self.find_view(&candidate).is_none() && self.find_annotation(&candidate).is_none() {
705 return candidate;
706 }
707 }
708 }
709
710 fn max_numeric_suffix(&self) -> u64 {
711 let mut best = 0u64;
712 let mut consider = |id: &str| {
713 let digits = id
714 .bytes()
715 .rev()
716 .take_while(u8::is_ascii_digit)
717 .count();
718 if digits > 0 {
719 if let Ok(value) = id[id.len() - digits..].parse::<u64>() {
720 best = best.max(value);
721 }
722 }
723 };
724 for view in &self.views {
725 consider(&view.id);
726 for annotation in &view.annotations {
727 consider(annotation.id());
728 }
729 }
730 best
731 }
732
733 pub fn find_view(&self, id: &str) -> Option<&PmiView> {
734 self.views.iter().find(|view| view.id == id)
735 }
736
737 pub fn find_view_mut(&mut self, id: &str) -> Option<&mut PmiView> {
738 self.views.iter_mut().find(|view| view.id == id)
739 }
740
741 pub fn find_annotation(&self, id: &str) -> Option<(&PmiView, &PmiAnnotation)> {
743 self.views.iter().find_map(|view| {
744 view.annotations
745 .iter()
746 .find(|annotation| annotation.id() == id)
747 .map(|annotation| (view, annotation))
748 })
749 }
750
751 pub fn locate_annotation(&self, id: &str) -> Option<(usize, usize)> {
753 self.views.iter().enumerate().find_map(|(view_index, view)| {
754 view.annotations
755 .iter()
756 .position(|annotation| annotation.id() == id)
757 .map(|index| (view_index, index))
758 })
759 }
760
761 pub fn find_annotation_mut(&mut self, id: &str) -> Option<&mut PmiAnnotation> {
762 self.views.iter_mut().find_map(|view| {
763 view.annotations
764 .iter_mut()
765 .find(|annotation| annotation.id() == id)
766 })
767 }
768
769 pub fn datum_letters(&self) -> Vec<(String, String)> {
773 let mut out = Vec::new();
774 for view in &self.views {
775 for annotation in &view.annotations {
776 if annotation.kind == annotations::datum::DEF.type_id {
777 let letter = annotation.text("letter").to_uppercase();
778 if !letter.is_empty() {
779 out.push((letter, annotation.id().to_string()));
780 }
781 }
782 }
783 }
784 out
785 }
786
787 pub fn next_datum_letter(&self) -> Option<String> {
790 let used: Vec<String> = self.datum_letters().into_iter().map(|(l, _)| l).collect();
791 let alphabet: Vec<char> = ('A'..='Z').filter(|c| !matches!(c, 'I' | 'O' | 'Q')).collect();
792 for letter in &alphabet {
793 let candidate = letter.to_string();
794 if !used.contains(&candidate) {
795 return Some(candidate);
796 }
797 }
798 for first in &alphabet {
799 for second in &alphabet {
800 let candidate = format!("{first}{second}");
801 if !used.contains(&candidate) {
802 return Some(candidate);
803 }
804 }
805 }
806 None
807 }
808
809 pub fn is_empty(&self) -> bool {
811 self.views.is_empty() && self.id_counter == 0
812 }
813}
814
815#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
821pub struct PmiReport {
822 pub views: Vec<PmiViewReport>,
823}
824
825impl PmiReport {
826 pub fn view(&self, id: &str) -> Option<&PmiViewReport> {
827 self.views.iter().find(|view| view.id == id)
828 }
829
830 pub fn annotation(&self, id: &str) -> Option<&PmiAnnotationReport> {
831 self.views
832 .iter()
833 .find_map(|view| view.annotations.iter().find(|a| a.id == id))
834 }
835
836 pub fn annotation_mut(&mut self, id: &str) -> Option<&mut PmiAnnotationReport> {
837 self.views
838 .iter_mut()
839 .find_map(|view| view.annotations.iter_mut().find(|a| a.id == id))
840 }
841}
842
843#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
844pub struct PmiViewReport {
845 pub id: String,
846 pub annotations: Vec<PmiAnnotationReport>,
847 #[serde(default, rename = "sectionPlane", skip_serializing_if = "Option::is_none")]
850 pub section_plane: Option<([f64; 3], [f64; 3])>,
851 #[serde(default, rename = "sectionError", skip_serializing_if = "String::is_empty")]
853 pub section_error: String,
854 #[serde(default, skip_serializing_if = "Vec::is_empty")]
858 pub hidden: Vec<String>,
859}
860
861#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
862#[serde(rename_all = "kebab-case")]
863pub enum PmiStatus {
864 Ok,
866 Error,
869}
870
871#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
873pub struct PmiAnnotationReport {
874 #[serde(default, rename = "persistentData", skip_serializing_if = "Option::is_none")]
876 pub persistent_data: Option<Value>,
877 #[serde(default, rename = "pluginReplay", skip_serializing_if = "Option::is_none")]
878 pub plugin_replay: Option<PluginAnnotationReplay>,
879 pub id: String,
880 #[serde(rename = "type")]
881 pub kind: String,
882 pub enabled: bool,
883 pub status: PmiStatus,
884 #[serde(default)]
885 pub message: String,
886 #[serde(default)]
889 pub text: String,
890 #[serde(default, skip_serializing_if = "Option::is_none")]
892 pub value: Option<f64>,
893 #[serde(default)]
895 pub unit: String,
896 #[serde(default)]
899 pub references: Vec<String>,
900 #[serde(rename = "labelWorld")]
903 pub label_world: [f64; 3],
904 #[serde(default, skip_serializing_if = "Option::is_none")]
906 pub plane: Option<PmiPlane>,
907 pub geometry: PmiGeometry,
908}
909
910#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
917pub struct PmiPlane {
918 pub origin: [f64; 3],
919 pub normal: [f64; 3],
920 #[serde(rename = "xAxis")]
921 pub x_axis: [f64; 3],
922}
923
924impl PmiPlane {
925 pub fn project(&self, point: [f64; 3]) -> [f64; 3] {
927 let n = self.normal;
928 let d = (point[0] - self.origin[0]) * n[0] + (point[1] - self.origin[1]) * n[1] + (point[2] - self.origin[2]) * n[2];
929 [point[0] - n[0] * d, point[1] - n[1] * d, point[2] - n[2] * d]
930 }
931
932 pub fn y_axis(&self) -> [f64; 3] {
934 let n = self.normal;
935 let x = self.x_axis;
936 [n[1] * x[2] - n[2] * x[1], n[2] * x[0] - n[0] * x[2], n[0] * x[1] - n[1] * x[0]]
937 }
938
939 pub fn hit(&self, origin: [f64; 3], dir: [f64; 3]) -> Option<[f64; 3]> {
941 let n = self.normal;
942 let denominator = dir[0] * n[0] + dir[1] * n[1] + dir[2] * n[2];
943 if denominator.abs() < 1e-12 {
944 return None;
945 }
946 let diff = [self.origin[0] - origin[0], self.origin[1] - origin[1], self.origin[2] - origin[2]];
947 let t = (diff[0] * n[0] + diff[1] * n[1] + diff[2] * n[2]) / denominator;
948 Some([origin[0] + dir[0] * t, origin[1] + dir[1] * t, origin[2] + dir[2] * t])
949 }
950}
951
952#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
955#[serde(tag = "kind", rename_all = "camelCase")]
956pub enum PmiGeometry {
957 None,
958 Linear {
961 a: [f64; 3],
962 b: [f64; 3],
963 #[serde(default, skip_serializing_if = "Option::is_none")]
964 component: Option<char>,
965 },
966 Radial {
967 center: [f64; 3],
968 axis: [f64; 3],
969 radius: f64,
970 diameter: bool,
971 sphere: bool,
972 },
973 Angular {
976 vertex: [f64; 3],
977 #[serde(rename = "dirA")]
978 dir_a: [f64; 3],
979 #[serde(rename = "dirB")]
980 dir_b: [f64; 3],
981 axis: [f64; 3],
982 degrees: f64,
983 },
984 Leader {
985 targets: Vec<[f64; 3]>,
986 dot: bool,
987 #[serde(default, skip_serializing_if = "is_false")]
988 balloon: bool,
989 #[serde(default, skip_serializing_if = "Option::is_none")]
992 anchor: Option<BalloonAnchor>,
993 },
994 Note {
995 position: [f64; 3],
996 },
997 Hole {
998 anchor: [f64; 3],
999 normal: [f64; 3],
1000 },
1001 Hide {
1004 solids: Vec<String>,
1005 },
1006 Explode {
1008 solids: Vec<String>,
1009 translate: [f64; 3],
1010 #[serde(rename = "rotateDeg")]
1011 rotate_deg: [f64; 3],
1012 scale: [f64; 3],
1013 center: [f64; 3],
1015 trace: bool,
1016 },
1017 Datum {
1018 anchor: [f64; 3],
1019 normal: [f64; 3],
1020 letter: String,
1021 },
1022 Fcf {
1023 anchor: [f64; 3],
1024 normal: [f64; 3],
1025 frame: FcfFrame,
1026 },
1027}
1028
1029#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
1031pub struct FcfFrame {
1032 pub characteristic: String,
1034 pub symbol: String,
1036 pub zone: String,
1038 pub datums: Vec<String>,
1040}
1041
1042#[derive(Debug, Clone, Copy, PartialEq)]
1049pub struct ToleranceBlock {
1050 pub mode: ToleranceMode,
1051 pub upper: f64,
1052 pub lower: f64,
1053}
1054
1055#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1056pub enum ToleranceMode {
1057 None,
1058 Symmetric,
1059 Deviation,
1060 Limits,
1061}
1062
1063impl ToleranceMode {
1064 pub fn parse(text: &str) -> Self {
1065 match text.trim().to_ascii_lowercase().as_str() {
1066 "symmetric" => ToleranceMode::Symmetric,
1067 "deviation" => ToleranceMode::Deviation,
1068 "limits" => ToleranceMode::Limits,
1069 _ => ToleranceMode::None,
1070 }
1071 }
1072
1073 pub fn as_str(self) -> &'static str {
1074 match self {
1075 ToleranceMode::None => "none",
1076 ToleranceMode::Symmetric => "symmetric",
1077 ToleranceMode::Deviation => "deviation",
1078 ToleranceMode::Limits => "limits",
1079 }
1080 }
1081}
1082
1083impl ToleranceBlock {
1084 pub fn read(annotation: &PmiAnnotation, env: &Env) -> Result<Self, String> {
1086 Ok(Self {
1087 mode: ToleranceMode::parse(annotation.text("tolMode")),
1088 upper: annotation.number("tolUpper", env, 0.0)?.abs(),
1089 lower: annotation.number("tolLower", env, 0.0)?.abs(),
1090 })
1091 }
1092
1093 pub fn bounds(&self) -> Option<(f64, f64)> {
1096 match self.mode {
1097 ToleranceMode::None => None,
1098 ToleranceMode::Symmetric => Some((-self.upper, self.upper)),
1099 ToleranceMode::Deviation | ToleranceMode::Limits => Some((-self.lower, self.upper)),
1100 }
1101 }
1102}
1103
1104pub fn format_number(value: f64, decimals: usize) -> String {
1106 let decimals = decimals.min(8);
1107 let text = format!("{:.*}", decimals, value);
1108 if text.starts_with('-') && text[1..].bytes().all(|b| b == b'0' || b == b'.') {
1110 text[1..].to_string()
1111 } else {
1112 text
1113 }
1114}
1115
1116pub fn format_dimension(
1120 value: f64,
1121 decimals: usize,
1122 tolerance: &ToleranceBlock,
1123 is_reference: bool,
1124 prefix: &str,
1125 suffix: &str,
1126) -> String {
1127 let nominal = format!("{prefix}{}{suffix}", format_number(value, decimals));
1128 if is_reference {
1129 return format!("({nominal})");
1130 }
1131 match tolerance.mode {
1132 ToleranceMode::None => nominal,
1133 ToleranceMode::Symmetric => format!(
1134 "{nominal} \u{00B1}{}{suffix}",
1135 format_number(tolerance.upper, decimals)
1136 ),
1137 ToleranceMode::Deviation => format!(
1138 "{nominal} +{}{suffix}/\u{2212}{}{suffix}",
1139 format_number(tolerance.upper, decimals),
1140 format_number(tolerance.lower, decimals)
1141 ),
1142 ToleranceMode::Limits => format!(
1143 "{prefix}{}{suffix} / {prefix}{}{suffix}",
1144 format_number(value + tolerance.upper, decimals),
1145 format_number(value - tolerance.lower, decimals)
1146 ),
1147 }
1148}
1149
1150pub struct PmiContext<'a> {
1157 pub scene: &'a SceneMap,
1158 pub request: &'a HistoryRequest,
1159 pub env: &'a Env,
1160 pub state: &'a PmiState,
1162 pub datums: BTreeMap<String, String>,
1164}
1165
1166pub struct Resolved {
1168 pub text: String,
1169 pub value: Option<f64>,
1170 pub unit: &'static str,
1171 pub references: Vec<String>,
1172 pub geometry: PmiGeometry,
1173 pub default_label: [f64; 3],
1175}
1176
1177pub(crate) fn finish_history_run(
1180 request: &HistoryRequest,
1181 scene: &SceneMap,
1182 env: &Env,
1183) -> Option<PmiReport> {
1184 let state = request.pmi.as_ref()?;
1185 Some(resolve_state(state, scene, request, env))
1186}
1187
1188pub fn resolve_state(
1190 state: &PmiState,
1191 scene: &SceneMap,
1192 request: &HistoryRequest,
1193 env: &Env,
1194) -> PmiReport {
1195 let mut datums: BTreeMap<String, String> = BTreeMap::new();
1198 for (letter, id) in state.datum_letters() {
1199 datums.entry(letter).or_insert(id);
1200 }
1201 let mut state = state.clone();
1204 let mut section_errors: BTreeMap<String, String> = BTreeMap::new();
1205 for view in &mut state.views {
1206 if let Err(error) = resolve_section_plane(view, scene) {
1207 section_errors.insert(view.id.clone(), error);
1208 }
1209 }
1210 let state = &state;
1211 let context = PmiContext {
1212 scene,
1213 request,
1214 env,
1215 state,
1216 datums,
1217 };
1218 PmiReport {
1219 views: state
1220 .views
1221 .iter()
1222 .map(|view| {
1223 let section = section_half_space(view);
1224 let exempt = |reference: &str| {
1225 let section = view.display.section.as_ref();
1226 section.is_some_and(|section| reference_solid(scene, reference).is_some_and(|solid| section.excludes(&solid)))
1227 };
1228 let mut hidden: Vec<String> = scene.solids.keys().filter(|name| view.hides(name)).cloned().collect();
1229 hidden.sort();
1230 PmiViewReport {
1231 section_plane: view.display.section.as_ref().filter(|s| s.axis == "Plane").and_then(|_| view.section_plane()),
1232 section_error: section_errors.remove(&view.id).unwrap_or_default(),
1233 hidden,
1234 id: view.id.clone(),
1235 annotations: view
1236 .annotations
1237 .iter()
1238 .map(|annotation| {
1239 let report = resolve_annotation(annotation, &context, view.camera.as_ref());
1240 match §ion {
1241 Some(section) => sectioned_away(report, section, &exempt),
1242 None => report,
1243 }
1244 })
1245 .collect(),
1246 }
1247 })
1248 .collect(),
1249 }
1250}
1251
1252fn resolve_section_plane(view: &mut PmiView, scene: &SceneMap) -> Result<(), String> {
1255 let Some(section) = view.display.section.as_mut().filter(|s| s.axis == "Plane") else {
1256 return Ok(());
1257 };
1258 let name = section.plane.clone().unwrap_or_default();
1259 if name.trim().is_empty() {
1260 return Err("pick a planar face or a datum plane to cut on".into());
1261 }
1262 match resolve::resolve_reference(scene, &name) {
1263 Ok(crate::SelectionGeometry::Plane { origin, normal }) => {
1264 section.origin = Some(resolve::a3(origin));
1265 section.normal = Some(resolve::a3(normal));
1266 Ok(())
1267 }
1268 Ok(_) => Err(format!("section plane '{name}' is not planar: pick a planar face or a datum plane")),
1269 Err(error) => Err(format!("section plane '{name}': {error}")),
1270 }
1271}
1272
1273fn section_half_space(view: &PmiView) -> Option<anchor::HalfSpace> {
1278 if view.annotations.iter().any(|a| a.enabled && a.kind == "explode") {
1279 return None;
1280 }
1281 anchor::HalfSpace::of(&view.section_planes())
1282}
1283
1284fn sectioned_away(mut report: PmiAnnotationReport, section: &anchor::HalfSpace, exempt: &dyn Fn(&str) -> bool) -> PmiAnnotationReport {
1292 if report.status != PmiStatus::Ok {
1293 return report;
1294 }
1295 let points: Vec<[f64; 3]> = match &report.geometry {
1296 PmiGeometry::Linear { a, b, component: None } => vec![*a, *b],
1297 PmiGeometry::Linear { a, component: Some(_), .. } => vec![*a],
1299 PmiGeometry::Leader { targets, balloon: false, .. } => targets.clone(),
1300 PmiGeometry::Hole { anchor, .. } | PmiGeometry::Datum { anchor, .. } | PmiGeometry::Fcf { anchor, .. } => vec![*anchor],
1301 _ => return report,
1302 };
1303 let references = &report.references;
1306 let point_exempt = |index: usize| match references.len() == points.len() {
1307 true => exempt(&references[index]),
1308 false => !references.is_empty() && references.iter().all(|reference| exempt(reference)),
1309 };
1310 if points.iter().enumerate().all(|(index, point)| point_exempt(index) || section.keeps(*point, 1e-6)) {
1311 return report;
1312 }
1313 report.status = PmiStatus::Error;
1314 report.message = "it is on geometry the section removes in this view: move the section, Reverse it, or re-attach the annotation".into();
1315 report.text = String::new();
1316 report.value = None;
1317 report.geometry = PmiGeometry::None;
1318 report.plane = None;
1319 report
1320}
1321
1322fn reference_solid(scene: &SceneMap, reference: &str) -> Option<String> {
1325 let reference = reference.trim();
1326 if let Some((solid, _)) = reference.split_once('@') {
1327 return Some(solid.to_string());
1328 }
1329 let handle = scene
1330 .resolve_face(reference)
1331 .map(|face| face.handle)
1332 .or_else(|| scene.resolve_edge(reference).map(|edge| edge.handle))
1333 .or_else(|| scene.resolve_solid(reference))?;
1334 scene.solids.iter().find(|(_, h)| **h == handle).map(|(name, _)| name.clone())
1335}
1336
1337pub fn resolve_annotation(
1341 annotation: &PmiAnnotation,
1342 context: &PmiContext<'_>,
1343 camera: Option<&PmiCamera>,
1344) -> PmiAnnotationReport {
1345 let mut persistent_data = None;
1346 let mut plugin_replay = None;
1347 let outcome = match pmi_type(&annotation.kind) {
1348 None if annotation.kind.contains('/') && !annotation.enabled => Ok(Resolved {
1349 text: String::new(), value: None, unit: "", references: Vec::new(),
1350 geometry: PmiGeometry::None, default_label: annotation.label_world.unwrap_or([0.; 3]),
1351 }),
1352 Some(def) => (def.resolve)(annotation, context),
1353 None if annotation.kind.contains('/') => plugin::resolve(annotation, context).map(|(resolved, data, replay)| {
1354 persistent_data = Some(data);
1355 plugin_replay = Some(replay);
1356 resolved
1357 }),
1358 None => Err(format!("unknown PMI annotation type '{}'", annotation.kind)),
1359 }
1360 .and_then(|resolved| {
1361 let plane = annotation_plane(annotation, context, camera, &resolved.geometry)?;
1362 Ok((resolved, plane))
1363 });
1364 match outcome {
1365 Ok((resolved, plane)) => {
1366 let label = annotation.label_world.unwrap_or(resolved.default_label);
1367 PmiAnnotationReport {
1368 persistent_data,
1369 plugin_replay,
1370 id: annotation.id().to_string(),
1371 kind: annotation.kind.clone(),
1372 enabled: annotation.enabled,
1373 status: PmiStatus::Ok,
1374 message: String::new(),
1375 text: resolved.text,
1376 value: resolved.value,
1377 unit: resolved.unit.to_string(),
1378 references: resolved.references,
1379 label_world: plane.map_or(label, |plane| plane.project(label)),
1380 plane,
1381 geometry: resolved.geometry,
1382 }
1383 }
1384 Err(message) => PmiAnnotationReport {
1385 persistent_data: None,
1386 plugin_replay: None,
1387 id: annotation.id().to_string(),
1388 kind: annotation.kind.clone(),
1389 enabled: annotation.enabled,
1390 status: PmiStatus::Error,
1391 message,
1392 text: String::new(),
1393 value: None,
1394 unit: String::new(),
1395 references: Vec::new(),
1396 label_world: annotation
1399 .label_world
1400 .or_else(|| (annotation.kind == "balloon").then(|| annotations::balloon::error_bubble(annotation, context)).flatten())
1401 .unwrap_or([0.0; 3]),
1402 plane: None,
1403 geometry: PmiGeometry::None,
1404 },
1405 }
1406}
1407
1408fn annotation_plane(
1416 annotation: &PmiAnnotation,
1417 context: &PmiContext<'_>,
1418 camera: Option<&PmiCamera>,
1419 geometry: &PmiGeometry,
1420) -> Result<Option<PmiPlane>, String> {
1421 use crate::SelectionGeometry;
1422 use resolve::{a3, perpendicular_in_plane, v3};
1423 let Some(name) = annotation.plane_ref() else {
1424 return Ok(None);
1425 };
1426 let (origin, normal) = match resolve::resolve_reference(context.scene, name)? {
1427 SelectionGeometry::Plane { origin, normal } => (origin, normal),
1428 _ => return Err(format!("annotation plane '{name}' must be a planar face or a reference plane")),
1429 };
1430 let mut normal = normal
1431 .normalized()
1432 .map_err(|_| format!("annotation plane '{name}' has no normal"))?;
1433 const PARALLEL: f64 = 1e-3;
1435 match geometry {
1436 PmiGeometry::Linear { a, b, .. } => {
1437 let span = v3(*b).sub(v3(*a));
1438 if span.length() > 1e-9 && span.normalized().map(|d| d.dot(normal).abs()).unwrap_or(0.0) > PARALLEL {
1439 return Err(format!("annotation plane '{name}' is not parallel to the measured direction"));
1440 }
1441 }
1442 PmiGeometry::Angular { axis, .. } => {
1443 if v3(*axis).cross(normal).length() > PARALLEL {
1444 return Err(format!("annotation plane '{name}' is not parallel to the angle's plane"));
1445 }
1446 }
1447 PmiGeometry::Radial { axis, sphere: false, .. } => {
1448 if v3(*axis).cross(normal).length() > PARALLEL {
1449 return Err(format!("annotation plane '{name}' is not parallel to the circle's plane"));
1450 }
1451 }
1452 _ => {}
1453 }
1454 let x_axis = match camera {
1455 Some(camera) => {
1456 let view = v3(camera.view_direction());
1457 if normal.dot(view) > 0.0 {
1458 normal = normal.scale(-1.0);
1459 }
1460 perpendicular_in_plane(normal, view.cross(v3(camera.up)))
1461 }
1462 None => perpendicular_in_plane(normal, crate::Vec3::new(1.0, 0.0, 0.0)),
1463 };
1464 Ok(Some(PmiPlane {
1465 origin: a3(origin),
1466 normal: a3(normal),
1467 x_axis: a3(x_axis),
1468 }))
1469}
1470