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)]
93pub struct PmiState {
94 #[serde(default)]
95 pub views: Vec<PmiView>,
96 #[serde(default, rename = "idCounter")]
99 pub id_counter: u64,
100}
101
102#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
104pub struct PmiView {
105 pub id: String,
106 #[serde(default)]
107 pub name: String,
108 #[serde(default, skip_serializing_if = "Option::is_none")]
109 pub camera: Option<PmiCamera>,
110 #[serde(default)]
111 pub display: PmiDisplay,
112 #[serde(default)]
113 pub annotations: Vec<PmiAnnotation>,
114}
115
116#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
121pub struct PmiCamera {
122 pub eye: [f64; 3],
123 pub target: [f64; 3],
124 pub up: [f64; 3],
125 pub projection: PmiProjection,
126 #[serde(default = "default_viewport")]
128 pub viewport: [f64; 2],
129}
130
131fn default_viewport() -> [f64; 2] {
132 [1280.0, 800.0]
133}
134
135impl PmiCamera {
136 pub fn view_direction(&self) -> [f64; 3] {
138 let d = [
139 self.target[0] - self.eye[0],
140 self.target[1] - self.eye[1],
141 self.target[2] - self.eye[2],
142 ];
143 let len = (d[0] * d[0] + d[1] * d[1] + d[2] * d[2]).sqrt();
144 if len < 1e-12 {
145 [0.0, 0.0, -1.0]
146 } else {
147 [d[0] / len, d[1] / len, d[2] / len]
148 }
149 }
150}
151
152#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
153#[serde(tag = "kind", rename_all = "camelCase")]
154pub enum PmiProjection {
155 Orthographic {
156 #[serde(rename = "halfHeight")]
157 half_height: f64,
158 },
159 Perspective {
160 #[serde(rename = "fovYDeg")]
161 fov_y_deg: f64,
162 },
163}
164
165#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
167pub struct PmiDisplay {
168 #[serde(default = "default_text_size", rename = "textSizePt")]
170 pub text_size_pt: f64,
171 #[serde(default)]
172 pub wireframe: bool,
173 #[serde(default)]
178 pub hidden: Vec<String>,
179 #[serde(default, skip_serializing_if = "Option::is_none")]
180 pub section: Option<PmiSection>,
181}
182
183#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
191pub struct PmiSection {
192 pub axis: String,
193 pub offset: f64,
194 #[serde(default)]
195 pub flip: bool,
196 #[serde(default, skip_serializing_if = "Option::is_none")]
197 pub origin: Option<[f64; 3]>,
198 #[serde(default, skip_serializing_if = "Option::is_none")]
199 pub normal: Option<[f64; 3]>,
200 #[serde(default, skip_serializing_if = "Option::is_none")]
201 pub plane: Option<String>,
202 #[serde(default, skip_serializing_if = "Vec::is_empty")]
205 pub exclude: Vec<String>,
206 #[serde(default, skip_serializing_if = "Vec::is_empty")]
211 pub planes: Vec<PmiSectionPlane>,
212}
213
214#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
218pub struct PmiSectionPlane {
219 pub axis: String,
220 #[serde(default)]
221 pub offset: f64,
222 #[serde(default)]
223 pub flip: bool,
224 #[serde(default, skip_serializing_if = "Option::is_none")]
225 pub normal: Option<[f64; 3]>,
226}
227
228impl PmiSectionPlane {
229 pub fn axis_normal(&self, camera: Option<&PmiCamera>) -> Option<[f64; 3]> {
232 match self.axis.as_str() {
233 "X" => Some([1., 0., 0.]),
234 "Y" => Some([0., 1., 0.]),
235 "Z" => Some([0., 0., 1.]),
236 "View" => self.normal.or_else(|| camera.map(PmiCamera::view_direction)),
237 _ => None,
238 }
239 }
240}
241pub fn entry_names_solid(entry: &str, solid: &str) -> bool {
247 let entry = entry.trim();
248 !entry.is_empty() && (solid == entry || solid.strip_prefix(entry).is_some_and(|rest| rest.starts_with(':')))
249}
250
251impl PmiSection {
252 pub fn excludes(&self, solid: &str) -> bool {
256 self.exclude.iter().any(|entry| entry_names_solid(entry, solid))
257 }
258
259 pub fn axis_normal(&self, camera: Option<&PmiCamera>) -> Option<[f64; 3]> {
262 match self.axis.as_str() {
263 "X" => Some([1., 0., 0.]),
264 "Y" => Some([0., 1., 0.]),
265 "Z" => Some([0., 0., 1.]),
266 "Plane" => self.normal,
267 _ => self.normal.or_else(|| camera.map(PmiCamera::view_direction)),
268 }
269 }
270
271 pub fn freeze(&mut self, camera: Option<&PmiCamera>) {
274 if self.axis == "Plane" {
275 self.origin = None;
277 self.normal = None;
278 return;
279 }
280 self.plane = None;
281 let Some(camera) = camera else { return };
282 for plane in &mut self.planes {
283 if plane.axis == "View" {
284 plane.normal.get_or_insert(camera.view_direction());
285 } else {
286 plane.normal = None;
287 }
288 }
289 self.origin.get_or_insert(camera.target);
290 if self.axis == "View" {
291 self.normal.get_or_insert(camera.view_direction());
292 } else {
293 self.normal = None;
294 }
295 }
296}
297impl PmiView {
298 pub fn section_plane(&self) -> Option<([f64; 3], [f64; 3])> {
299 let section = self.display.section.as_ref()?;
300 let camera = self.camera.as_ref();
301 let mut normal = section.axis_normal(camera)?;
302 let origin = section.origin.or_else(|| camera.filter(|_| section.axis != "Plane").map(|c| c.target))?;
303 let point = std::array::from_fn(|i| origin[i] + normal[i] * section.offset);
304 if section.flip {
305 normal = normal.map(|v| -v);
306 }
307 Some((point, normal))
308 }
309
310 pub fn section_plane_in(&self, report: Option<&PmiViewReport>) -> Option<([f64; 3], [f64; 3])> {
313 match self.display.section.as_ref() {
314 Some(section) if section.axis == "Plane" => report.and_then(|report| report.section_plane),
315 _ => self.section_plane(),
316 }
317 }
318
319 pub fn section_planes_in(&self, report: Option<&PmiViewReport>) -> Vec<([f64; 3], [f64; 3])> {
325 self.section_plane_in(report).map_or_else(Vec::new, |first| self.with_further_planes(first))
326 }
327
328 pub fn section_planes(&self) -> Vec<([f64; 3], [f64; 3])> {
331 self.section_plane().map_or_else(Vec::new, |first| self.with_further_planes(first))
332 }
333
334 fn with_further_planes(&self, first: ([f64; 3], [f64; 3])) -> Vec<([f64; 3], [f64; 3])> {
335 let mut planes = vec![first];
336 let Some(section) = self.display.section.as_ref() else { return planes };
337 let camera = self.camera.as_ref();
338 let origin = section.origin.or_else(|| camera.filter(|_| section.axis != "Plane").map(|c| c.target)).unwrap_or(first.0);
339 for plane in §ion.planes {
340 let Some(mut normal) = plane.axis_normal(camera) else { continue };
341 let point = std::array::from_fn(|i| origin[i] + normal[i] * plane.offset);
342 if plane.flip {
343 normal = normal.map(|v| -v);
344 }
345 planes.push((point, normal));
346 }
347 planes
348 }
349}
350
351impl PmiView {
352 pub fn hides(&self, solid: &str) -> bool {
359 self.display.hidden.iter().any(|name| name == solid)
360 || self.hide_targets().any(|entry| entry_names_solid(entry, solid))
361 }
362
363 pub fn hide_targets(&self) -> impl Iterator<Item = &str> {
365 self.annotations
366 .iter()
367 .filter(|annotation| annotation.enabled && annotation.kind == annotations::hide::DEF.type_id)
368 .flat_map(|annotation| match annotation.params.get("targets") {
369 Some(Value::String(name)) => std::slice::from_ref(name).iter().map(String::as_str).collect::<Vec<_>>(),
370 Some(Value::Array(items)) => items.iter().filter_map(Value::as_str).collect(),
371 _ => Vec::new(),
372 })
373 .map(str::trim)
374 .filter(|name| !name.is_empty())
375 }
376}
377
378fn default_text_size() -> f64 {
379 12.0
380}
381
382impl Default for PmiDisplay {
383 fn default() -> Self {
384 Self {
385 text_size_pt: default_text_size(),
386 wireframe: false,
387 hidden: Vec::new(),
388 section: None,
389 }
390 }
391}
392
393pub fn clamp_text_size(size: f64) -> f64 {
395 if !size.is_finite() {
396 return default_text_size();
397 }
398 size.clamp(1.0, 288.0)
399}
400
401#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
403pub struct PmiAnnotation {
404 #[serde(rename = "type")]
405 pub kind: String,
406 #[serde(default = "default_true")]
407 pub enabled: bool,
408 #[serde(default, rename = "inputParams")]
409 pub params: Value,
410 #[serde(default, rename = "persistentData", skip_serializing_if = "Value::is_null")]
411 pub persistent_data: Value,
412 #[serde(default, rename = "pluginReplay", skip_serializing_if = "Option::is_none")]
414 pub plugin_replay: Option<PluginAnnotationReplay>,
415 #[serde(default, rename = "labelWorld", skip_serializing_if = "Option::is_none")]
418 pub label_world: Option<[f64; 3]>,
419}
420
421fn is_false(value: &bool) -> bool { !value }
422
423fn default_true() -> bool {
424 true
425}
426
427impl PmiAnnotation {
428 pub fn id(&self) -> &str {
430 self.params
431 .get("id")
432 .and_then(Value::as_str)
433 .unwrap_or("")
434 }
435
436 pub fn text(&self, key: &str) -> &str {
438 self.params
439 .get(key)
440 .and_then(Value::as_str)
441 .map(str::trim)
442 .unwrap_or("")
443 }
444
445 pub fn flag(&self, key: &str) -> bool {
447 self.params
448 .get(key)
449 .and_then(Value::as_bool)
450 .unwrap_or(false)
451 }
452
453 pub fn number(&self, key: &str, env: &Env, default: f64) -> Result<f64, String> {
456 match self.params.get(key) {
457 None | Some(Value::Null) => Ok(default),
458 Some(Value::Number(number)) => Ok(number.as_f64().unwrap_or(default)),
459 Some(Value::Bool(flag)) => Ok(if *flag { 1.0 } else { 0.0 }),
460 Some(Value::String(source)) => {
461 let source = source.trim();
462 if source.is_empty() {
463 return Ok(default);
464 }
465 env.eval(source)
466 .map_err(|error| format!("{key}: {error}"))
467 }
468 Some(other) => Err(format!("{key}: expected a number, got {other}")),
469 }
470 }
471
472 pub fn plane_ref(&self) -> Option<&str> {
475 let name = self.text("plane").trim();
476 (!name.is_empty()).then_some(name)
477 }
478
479 pub fn references(&self, key: &str) -> Vec<String> {
482 match self.params.get(key) {
483 Some(Value::String(name)) => {
484 let name = name.trim();
485 if name.is_empty() {
486 Vec::new()
487 } else {
488 vec![name.to_string()]
489 }
490 }
491 Some(Value::Array(items)) => items
492 .iter()
493 .filter_map(Value::as_str)
494 .map(str::trim)
495 .filter(|name| !name.is_empty())
496 .map(String::from)
497 .collect(),
498 _ => Vec::new(),
499 }
500 }
501}
502
503impl PmiState {
504 pub fn next_id(&mut self, prefix: &str) -> String {
508 let seen = self.max_numeric_suffix();
509 if seen > self.id_counter {
510 self.id_counter = seen;
511 }
512 loop {
513 self.id_counter += 1;
514 let candidate = format!("{prefix}{}", self.id_counter);
515 if self.find_view(&candidate).is_none() && self.find_annotation(&candidate).is_none() {
516 return candidate;
517 }
518 }
519 }
520
521 fn max_numeric_suffix(&self) -> u64 {
522 let mut best = 0u64;
523 let mut consider = |id: &str| {
524 let digits = id
525 .bytes()
526 .rev()
527 .take_while(u8::is_ascii_digit)
528 .count();
529 if digits > 0 {
530 if let Ok(value) = id[id.len() - digits..].parse::<u64>() {
531 best = best.max(value);
532 }
533 }
534 };
535 for view in &self.views {
536 consider(&view.id);
537 for annotation in &view.annotations {
538 consider(annotation.id());
539 }
540 }
541 best
542 }
543
544 pub fn find_view(&self, id: &str) -> Option<&PmiView> {
545 self.views.iter().find(|view| view.id == id)
546 }
547
548 pub fn find_view_mut(&mut self, id: &str) -> Option<&mut PmiView> {
549 self.views.iter_mut().find(|view| view.id == id)
550 }
551
552 pub fn find_annotation(&self, id: &str) -> Option<(&PmiView, &PmiAnnotation)> {
554 self.views.iter().find_map(|view| {
555 view.annotations
556 .iter()
557 .find(|annotation| annotation.id() == id)
558 .map(|annotation| (view, annotation))
559 })
560 }
561
562 pub fn locate_annotation(&self, id: &str) -> Option<(usize, usize)> {
564 self.views.iter().enumerate().find_map(|(view_index, view)| {
565 view.annotations
566 .iter()
567 .position(|annotation| annotation.id() == id)
568 .map(|index| (view_index, index))
569 })
570 }
571
572 pub fn find_annotation_mut(&mut self, id: &str) -> Option<&mut PmiAnnotation> {
573 self.views.iter_mut().find_map(|view| {
574 view.annotations
575 .iter_mut()
576 .find(|annotation| annotation.id() == id)
577 })
578 }
579
580 pub fn datum_letters(&self) -> Vec<(String, String)> {
584 let mut out = Vec::new();
585 for view in &self.views {
586 for annotation in &view.annotations {
587 if annotation.kind == annotations::datum::DEF.type_id {
588 let letter = annotation.text("letter").to_uppercase();
589 if !letter.is_empty() {
590 out.push((letter, annotation.id().to_string()));
591 }
592 }
593 }
594 }
595 out
596 }
597
598 pub fn next_datum_letter(&self) -> Option<String> {
601 let used: Vec<String> = self.datum_letters().into_iter().map(|(l, _)| l).collect();
602 let alphabet: Vec<char> = ('A'..='Z').filter(|c| !matches!(c, 'I' | 'O' | 'Q')).collect();
603 for letter in &alphabet {
604 let candidate = letter.to_string();
605 if !used.contains(&candidate) {
606 return Some(candidate);
607 }
608 }
609 for first in &alphabet {
610 for second in &alphabet {
611 let candidate = format!("{first}{second}");
612 if !used.contains(&candidate) {
613 return Some(candidate);
614 }
615 }
616 }
617 None
618 }
619
620 pub fn is_empty(&self) -> bool {
622 self.views.is_empty() && self.id_counter == 0
623 }
624}
625
626#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
632pub struct PmiReport {
633 pub views: Vec<PmiViewReport>,
634}
635
636impl PmiReport {
637 pub fn view(&self, id: &str) -> Option<&PmiViewReport> {
638 self.views.iter().find(|view| view.id == id)
639 }
640
641 pub fn annotation(&self, id: &str) -> Option<&PmiAnnotationReport> {
642 self.views
643 .iter()
644 .find_map(|view| view.annotations.iter().find(|a| a.id == id))
645 }
646
647 pub fn annotation_mut(&mut self, id: &str) -> Option<&mut PmiAnnotationReport> {
648 self.views
649 .iter_mut()
650 .find_map(|view| view.annotations.iter_mut().find(|a| a.id == id))
651 }
652}
653
654#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
655pub struct PmiViewReport {
656 pub id: String,
657 pub annotations: Vec<PmiAnnotationReport>,
658 #[serde(default, rename = "sectionPlane", skip_serializing_if = "Option::is_none")]
661 pub section_plane: Option<([f64; 3], [f64; 3])>,
662 #[serde(default, rename = "sectionError", skip_serializing_if = "String::is_empty")]
664 pub section_error: String,
665 #[serde(default, skip_serializing_if = "Vec::is_empty")]
669 pub hidden: Vec<String>,
670}
671
672#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
673#[serde(rename_all = "kebab-case")]
674pub enum PmiStatus {
675 Ok,
677 Error,
680}
681
682#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
684pub struct PmiAnnotationReport {
685 #[serde(default, rename = "persistentData", skip_serializing_if = "Option::is_none")]
687 pub persistent_data: Option<Value>,
688 #[serde(default, rename = "pluginReplay", skip_serializing_if = "Option::is_none")]
689 pub plugin_replay: Option<PluginAnnotationReplay>,
690 pub id: String,
691 #[serde(rename = "type")]
692 pub kind: String,
693 pub enabled: bool,
694 pub status: PmiStatus,
695 #[serde(default)]
696 pub message: String,
697 #[serde(default)]
700 pub text: String,
701 #[serde(default, skip_serializing_if = "Option::is_none")]
703 pub value: Option<f64>,
704 #[serde(default)]
706 pub unit: String,
707 #[serde(default)]
710 pub references: Vec<String>,
711 #[serde(rename = "labelWorld")]
714 pub label_world: [f64; 3],
715 #[serde(default, skip_serializing_if = "Option::is_none")]
717 pub plane: Option<PmiPlane>,
718 pub geometry: PmiGeometry,
719}
720
721#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
728pub struct PmiPlane {
729 pub origin: [f64; 3],
730 pub normal: [f64; 3],
731 #[serde(rename = "xAxis")]
732 pub x_axis: [f64; 3],
733}
734
735impl PmiPlane {
736 pub fn project(&self, point: [f64; 3]) -> [f64; 3] {
738 let n = self.normal;
739 let d = (point[0] - self.origin[0]) * n[0] + (point[1] - self.origin[1]) * n[1] + (point[2] - self.origin[2]) * n[2];
740 [point[0] - n[0] * d, point[1] - n[1] * d, point[2] - n[2] * d]
741 }
742
743 pub fn y_axis(&self) -> [f64; 3] {
745 let n = self.normal;
746 let x = self.x_axis;
747 [n[1] * x[2] - n[2] * x[1], n[2] * x[0] - n[0] * x[2], n[0] * x[1] - n[1] * x[0]]
748 }
749
750 pub fn hit(&self, origin: [f64; 3], dir: [f64; 3]) -> Option<[f64; 3]> {
752 let n = self.normal;
753 let denominator = dir[0] * n[0] + dir[1] * n[1] + dir[2] * n[2];
754 if denominator.abs() < 1e-12 {
755 return None;
756 }
757 let diff = [self.origin[0] - origin[0], self.origin[1] - origin[1], self.origin[2] - origin[2]];
758 let t = (diff[0] * n[0] + diff[1] * n[1] + diff[2] * n[2]) / denominator;
759 Some([origin[0] + dir[0] * t, origin[1] + dir[1] * t, origin[2] + dir[2] * t])
760 }
761}
762
763#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
766#[serde(tag = "kind", rename_all = "camelCase")]
767pub enum PmiGeometry {
768 None,
769 Linear {
772 a: [f64; 3],
773 b: [f64; 3],
774 #[serde(default, skip_serializing_if = "Option::is_none")]
775 component: Option<char>,
776 },
777 Radial {
778 center: [f64; 3],
779 axis: [f64; 3],
780 radius: f64,
781 diameter: bool,
782 sphere: bool,
783 },
784 Angular {
787 vertex: [f64; 3],
788 #[serde(rename = "dirA")]
789 dir_a: [f64; 3],
790 #[serde(rename = "dirB")]
791 dir_b: [f64; 3],
792 axis: [f64; 3],
793 degrees: f64,
794 },
795 Leader {
796 targets: Vec<[f64; 3]>,
797 dot: bool,
798 #[serde(default, skip_serializing_if = "is_false")]
799 balloon: bool,
800 #[serde(default, skip_serializing_if = "Option::is_none")]
803 anchor: Option<BalloonAnchor>,
804 },
805 Note {
806 position: [f64; 3],
807 },
808 Hole {
809 anchor: [f64; 3],
810 normal: [f64; 3],
811 },
812 Hide {
815 solids: Vec<String>,
816 },
817 Explode {
819 solids: Vec<String>,
820 translate: [f64; 3],
821 #[serde(rename = "rotateDeg")]
822 rotate_deg: [f64; 3],
823 scale: [f64; 3],
824 center: [f64; 3],
826 trace: bool,
827 },
828 Datum {
829 anchor: [f64; 3],
830 normal: [f64; 3],
831 letter: String,
832 },
833 Fcf {
834 anchor: [f64; 3],
835 normal: [f64; 3],
836 frame: FcfFrame,
837 },
838}
839
840#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
842pub struct FcfFrame {
843 pub characteristic: String,
845 pub symbol: String,
847 pub zone: String,
849 pub datums: Vec<String>,
851}
852
853#[derive(Debug, Clone, Copy, PartialEq)]
860pub struct ToleranceBlock {
861 pub mode: ToleranceMode,
862 pub upper: f64,
863 pub lower: f64,
864}
865
866#[derive(Debug, Clone, Copy, PartialEq, Eq)]
867pub enum ToleranceMode {
868 None,
869 Symmetric,
870 Deviation,
871 Limits,
872}
873
874impl ToleranceMode {
875 pub fn parse(text: &str) -> Self {
876 match text.trim().to_ascii_lowercase().as_str() {
877 "symmetric" => ToleranceMode::Symmetric,
878 "deviation" => ToleranceMode::Deviation,
879 "limits" => ToleranceMode::Limits,
880 _ => ToleranceMode::None,
881 }
882 }
883
884 pub fn as_str(self) -> &'static str {
885 match self {
886 ToleranceMode::None => "none",
887 ToleranceMode::Symmetric => "symmetric",
888 ToleranceMode::Deviation => "deviation",
889 ToleranceMode::Limits => "limits",
890 }
891 }
892}
893
894impl ToleranceBlock {
895 pub fn read(annotation: &PmiAnnotation, env: &Env) -> Result<Self, String> {
897 Ok(Self {
898 mode: ToleranceMode::parse(annotation.text("tolMode")),
899 upper: annotation.number("tolUpper", env, 0.0)?.abs(),
900 lower: annotation.number("tolLower", env, 0.0)?.abs(),
901 })
902 }
903
904 pub fn bounds(&self) -> Option<(f64, f64)> {
907 match self.mode {
908 ToleranceMode::None => None,
909 ToleranceMode::Symmetric => Some((-self.upper, self.upper)),
910 ToleranceMode::Deviation | ToleranceMode::Limits => Some((-self.lower, self.upper)),
911 }
912 }
913}
914
915pub fn format_number(value: f64, decimals: usize) -> String {
917 let decimals = decimals.min(8);
918 let text = format!("{:.*}", decimals, value);
919 if text.starts_with('-') && text[1..].bytes().all(|b| b == b'0' || b == b'.') {
921 text[1..].to_string()
922 } else {
923 text
924 }
925}
926
927pub fn format_dimension(
931 value: f64,
932 decimals: usize,
933 tolerance: &ToleranceBlock,
934 is_reference: bool,
935 prefix: &str,
936 suffix: &str,
937) -> String {
938 let nominal = format!("{prefix}{}{suffix}", format_number(value, decimals));
939 if is_reference {
940 return format!("({nominal})");
941 }
942 match tolerance.mode {
943 ToleranceMode::None => nominal,
944 ToleranceMode::Symmetric => format!(
945 "{nominal} \u{00B1}{}{suffix}",
946 format_number(tolerance.upper, decimals)
947 ),
948 ToleranceMode::Deviation => format!(
949 "{nominal} +{}{suffix}/\u{2212}{}{suffix}",
950 format_number(tolerance.upper, decimals),
951 format_number(tolerance.lower, decimals)
952 ),
953 ToleranceMode::Limits => format!(
954 "{prefix}{}{suffix} / {prefix}{}{suffix}",
955 format_number(value + tolerance.upper, decimals),
956 format_number(value - tolerance.lower, decimals)
957 ),
958 }
959}
960
961pub struct PmiContext<'a> {
968 pub scene: &'a SceneMap,
969 pub request: &'a HistoryRequest,
970 pub env: &'a Env,
971 pub state: &'a PmiState,
973 pub datums: BTreeMap<String, String>,
975}
976
977pub struct Resolved {
979 pub text: String,
980 pub value: Option<f64>,
981 pub unit: &'static str,
982 pub references: Vec<String>,
983 pub geometry: PmiGeometry,
984 pub default_label: [f64; 3],
986}
987
988pub(crate) fn finish_history_run(
991 request: &HistoryRequest,
992 scene: &SceneMap,
993 env: &Env,
994) -> Option<PmiReport> {
995 let state = request.pmi.as_ref()?;
996 Some(resolve_state(state, scene, request, env))
997}
998
999pub fn resolve_state(
1001 state: &PmiState,
1002 scene: &SceneMap,
1003 request: &HistoryRequest,
1004 env: &Env,
1005) -> PmiReport {
1006 let mut datums: BTreeMap<String, String> = BTreeMap::new();
1009 for (letter, id) in state.datum_letters() {
1010 datums.entry(letter).or_insert(id);
1011 }
1012 let mut state = state.clone();
1015 let mut section_errors: BTreeMap<String, String> = BTreeMap::new();
1016 for view in &mut state.views {
1017 if let Err(error) = resolve_section_plane(view, scene) {
1018 section_errors.insert(view.id.clone(), error);
1019 }
1020 }
1021 let state = &state;
1022 let context = PmiContext {
1023 scene,
1024 request,
1025 env,
1026 state,
1027 datums,
1028 };
1029 PmiReport {
1030 views: state
1031 .views
1032 .iter()
1033 .map(|view| {
1034 let section = section_half_space(view);
1035 let exempt = |reference: &str| {
1036 let section = view.display.section.as_ref();
1037 section.is_some_and(|section| reference_solid(scene, reference).is_some_and(|solid| section.excludes(&solid)))
1038 };
1039 let mut hidden: Vec<String> = scene.solids.keys().filter(|name| view.hides(name)).cloned().collect();
1040 hidden.sort();
1041 PmiViewReport {
1042 section_plane: view.display.section.as_ref().filter(|s| s.axis == "Plane").and_then(|_| view.section_plane()),
1043 section_error: section_errors.remove(&view.id).unwrap_or_default(),
1044 hidden,
1045 id: view.id.clone(),
1046 annotations: view
1047 .annotations
1048 .iter()
1049 .map(|annotation| {
1050 let report = resolve_annotation(annotation, &context, view.camera.as_ref());
1051 match §ion {
1052 Some(section) => sectioned_away(report, section, &exempt),
1053 None => report,
1054 }
1055 })
1056 .collect(),
1057 }
1058 })
1059 .collect(),
1060 }
1061}
1062
1063fn resolve_section_plane(view: &mut PmiView, scene: &SceneMap) -> Result<(), String> {
1066 let Some(section) = view.display.section.as_mut().filter(|s| s.axis == "Plane") else {
1067 return Ok(());
1068 };
1069 let name = section.plane.clone().unwrap_or_default();
1070 if name.trim().is_empty() {
1071 return Err("pick a planar face or a datum plane to cut on".into());
1072 }
1073 match resolve::resolve_reference(scene, &name) {
1074 Ok(crate::SelectionGeometry::Plane { origin, normal }) => {
1075 section.origin = Some(resolve::a3(origin));
1076 section.normal = Some(resolve::a3(normal));
1077 Ok(())
1078 }
1079 Ok(_) => Err(format!("section plane '{name}' is not planar: pick a planar face or a datum plane")),
1080 Err(error) => Err(format!("section plane '{name}': {error}")),
1081 }
1082}
1083
1084fn section_half_space(view: &PmiView) -> Option<anchor::HalfSpace> {
1089 if view.annotations.iter().any(|a| a.enabled && a.kind == "explode") {
1090 return None;
1091 }
1092 anchor::HalfSpace::of(&view.section_planes())
1093}
1094
1095fn sectioned_away(mut report: PmiAnnotationReport, section: &anchor::HalfSpace, exempt: &dyn Fn(&str) -> bool) -> PmiAnnotationReport {
1103 if report.status != PmiStatus::Ok {
1104 return report;
1105 }
1106 let points: Vec<[f64; 3]> = match &report.geometry {
1107 PmiGeometry::Linear { a, b, component: None } => vec![*a, *b],
1108 PmiGeometry::Linear { a, component: Some(_), .. } => vec![*a],
1110 PmiGeometry::Leader { targets, balloon: false, .. } => targets.clone(),
1111 PmiGeometry::Hole { anchor, .. } | PmiGeometry::Datum { anchor, .. } | PmiGeometry::Fcf { anchor, .. } => vec![*anchor],
1112 _ => return report,
1113 };
1114 let references = &report.references;
1117 let point_exempt = |index: usize| match references.len() == points.len() {
1118 true => exempt(&references[index]),
1119 false => !references.is_empty() && references.iter().all(|reference| exempt(reference)),
1120 };
1121 if points.iter().enumerate().all(|(index, point)| point_exempt(index) || section.keeps(*point, 1e-6)) {
1122 return report;
1123 }
1124 report.status = PmiStatus::Error;
1125 report.message = "it is on geometry the section removes in this view: move the section, Reverse it, or re-attach the annotation".into();
1126 report.text = String::new();
1127 report.value = None;
1128 report.geometry = PmiGeometry::None;
1129 report.plane = None;
1130 report
1131}
1132
1133fn reference_solid(scene: &SceneMap, reference: &str) -> Option<String> {
1136 let reference = reference.trim();
1137 if let Some((solid, _)) = reference.split_once('@') {
1138 return Some(solid.to_string());
1139 }
1140 let handle = scene
1141 .resolve_face(reference)
1142 .map(|face| face.handle)
1143 .or_else(|| scene.resolve_edge(reference).map(|edge| edge.handle))
1144 .or_else(|| scene.resolve_solid(reference))?;
1145 scene.solids.iter().find(|(_, h)| **h == handle).map(|(name, _)| name.clone())
1146}
1147
1148pub fn resolve_annotation(
1152 annotation: &PmiAnnotation,
1153 context: &PmiContext<'_>,
1154 camera: Option<&PmiCamera>,
1155) -> PmiAnnotationReport {
1156 let mut persistent_data = None;
1157 let mut plugin_replay = None;
1158 let outcome = match pmi_type(&annotation.kind) {
1159 None if annotation.kind.contains('/') && !annotation.enabled => Ok(Resolved {
1160 text: String::new(), value: None, unit: "", references: Vec::new(),
1161 geometry: PmiGeometry::None, default_label: annotation.label_world.unwrap_or([0.; 3]),
1162 }),
1163 Some(def) => (def.resolve)(annotation, context),
1164 None if annotation.kind.contains('/') => plugin::resolve(annotation, context).map(|(resolved, data, replay)| {
1165 persistent_data = Some(data);
1166 plugin_replay = Some(replay);
1167 resolved
1168 }),
1169 None => Err(format!("unknown PMI annotation type '{}'", annotation.kind)),
1170 }
1171 .and_then(|resolved| {
1172 let plane = annotation_plane(annotation, context, camera, &resolved.geometry)?;
1173 Ok((resolved, plane))
1174 });
1175 match outcome {
1176 Ok((resolved, plane)) => {
1177 let label = annotation.label_world.unwrap_or(resolved.default_label);
1178 PmiAnnotationReport {
1179 persistent_data,
1180 plugin_replay,
1181 id: annotation.id().to_string(),
1182 kind: annotation.kind.clone(),
1183 enabled: annotation.enabled,
1184 status: PmiStatus::Ok,
1185 message: String::new(),
1186 text: resolved.text,
1187 value: resolved.value,
1188 unit: resolved.unit.to_string(),
1189 references: resolved.references,
1190 label_world: plane.map_or(label, |plane| plane.project(label)),
1191 plane,
1192 geometry: resolved.geometry,
1193 }
1194 }
1195 Err(message) => PmiAnnotationReport {
1196 persistent_data: None,
1197 plugin_replay: None,
1198 id: annotation.id().to_string(),
1199 kind: annotation.kind.clone(),
1200 enabled: annotation.enabled,
1201 status: PmiStatus::Error,
1202 message,
1203 text: String::new(),
1204 value: None,
1205 unit: String::new(),
1206 references: Vec::new(),
1207 label_world: annotation
1210 .label_world
1211 .or_else(|| (annotation.kind == "balloon").then(|| annotations::balloon::error_bubble(annotation, context)).flatten())
1212 .unwrap_or([0.0; 3]),
1213 plane: None,
1214 geometry: PmiGeometry::None,
1215 },
1216 }
1217}
1218
1219fn annotation_plane(
1227 annotation: &PmiAnnotation,
1228 context: &PmiContext<'_>,
1229 camera: Option<&PmiCamera>,
1230 geometry: &PmiGeometry,
1231) -> Result<Option<PmiPlane>, String> {
1232 use crate::SelectionGeometry;
1233 use resolve::{a3, perpendicular_in_plane, v3};
1234 let Some(name) = annotation.plane_ref() else {
1235 return Ok(None);
1236 };
1237 let (origin, normal) = match resolve::resolve_reference(context.scene, name)? {
1238 SelectionGeometry::Plane { origin, normal } => (origin, normal),
1239 _ => return Err(format!("annotation plane '{name}' must be a planar face or a reference plane")),
1240 };
1241 let mut normal = normal
1242 .normalized()
1243 .map_err(|_| format!("annotation plane '{name}' has no normal"))?;
1244 const PARALLEL: f64 = 1e-3;
1246 match geometry {
1247 PmiGeometry::Linear { a, b, .. } => {
1248 let span = v3(*b).sub(v3(*a));
1249 if span.length() > 1e-9 && span.normalized().map(|d| d.dot(normal).abs()).unwrap_or(0.0) > PARALLEL {
1250 return Err(format!("annotation plane '{name}' is not parallel to the measured direction"));
1251 }
1252 }
1253 PmiGeometry::Angular { axis, .. } => {
1254 if v3(*axis).cross(normal).length() > PARALLEL {
1255 return Err(format!("annotation plane '{name}' is not parallel to the angle's plane"));
1256 }
1257 }
1258 PmiGeometry::Radial { axis, sphere: false, .. } => {
1259 if v3(*axis).cross(normal).length() > PARALLEL {
1260 return Err(format!("annotation plane '{name}' is not parallel to the circle's plane"));
1261 }
1262 }
1263 _ => {}
1264 }
1265 let x_axis = match camera {
1266 Some(camera) => {
1267 let view = v3(camera.view_direction());
1268 if normal.dot(view) > 0.0 {
1269 normal = normal.scale(-1.0);
1270 }
1271 perpendicular_in_plane(normal, view.cross(v3(camera.up)))
1272 }
1273 None => perpendicular_in_plane(normal, crate::Vec3::new(1.0, 0.0, 0.0)),
1274 };
1275 Ok(Some(PmiPlane {
1276 origin: a3(origin),
1277 normal: a3(normal),
1278 x_axis: a3(x_axis),
1279 }))
1280}
1281