1pub mod anchor;
64pub mod annotations;
65pub mod font;
66pub mod layout;
67pub mod resolve;
68mod plugin;
69pub use plugin::{PluginAnnotationOutput, PluginAnnotationReplay};
70
71use serde::{Deserialize, Serialize};
72use serde_json::Value;
73use std::collections::BTreeMap;
74
75use crate::feature_pipeline::{Env, HistoryRequest, SceneMap};
76
77pub use anchor::{
78 balloon_head, nearest_displayed_point, nearest_displayed_point_on_edge, nearest_point_on_edge, nearest_point_on_solid,
79 pose_apply, pose_unapply, BalloonAnchor, BalloonAnchorMode, HalfSpace, HeadScope, PmiPose, SolidPoses,
80};
81pub use annotations::{pmi_schema_catalogue, pmi_type, PmiTypeDef, PMI_TYPES};
82
83#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
89pub struct PmiState {
90 #[serde(default)]
91 pub views: Vec<PmiView>,
92 #[serde(default, rename = "idCounter")]
95 pub id_counter: u64,
96}
97
98#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
100pub struct PmiView {
101 pub id: String,
102 #[serde(default)]
103 pub name: String,
104 #[serde(default, skip_serializing_if = "Option::is_none")]
105 pub camera: Option<PmiCamera>,
106 #[serde(default)]
107 pub display: PmiDisplay,
108 #[serde(default)]
109 pub annotations: Vec<PmiAnnotation>,
110}
111
112#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
117pub struct PmiCamera {
118 pub eye: [f64; 3],
119 pub target: [f64; 3],
120 pub up: [f64; 3],
121 pub projection: PmiProjection,
122 #[serde(default = "default_viewport")]
124 pub viewport: [f64; 2],
125}
126
127fn default_viewport() -> [f64; 2] {
128 [1280.0, 800.0]
129}
130
131impl PmiCamera {
132 pub fn view_direction(&self) -> [f64; 3] {
134 let d = [
135 self.target[0] - self.eye[0],
136 self.target[1] - self.eye[1],
137 self.target[2] - self.eye[2],
138 ];
139 let len = (d[0] * d[0] + d[1] * d[1] + d[2] * d[2]).sqrt();
140 if len < 1e-12 {
141 [0.0, 0.0, -1.0]
142 } else {
143 [d[0] / len, d[1] / len, d[2] / len]
144 }
145 }
146}
147
148#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
149#[serde(tag = "kind", rename_all = "camelCase")]
150pub enum PmiProjection {
151 Orthographic {
152 #[serde(rename = "halfHeight")]
153 half_height: f64,
154 },
155 Perspective {
156 #[serde(rename = "fovYDeg")]
157 fov_y_deg: f64,
158 },
159}
160
161#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
163pub struct PmiDisplay {
164 #[serde(default = "default_text_size", rename = "textSizePt")]
166 pub text_size_pt: f64,
167 #[serde(default)]
168 pub wireframe: bool,
169 #[serde(default)]
172 pub hidden: Vec<String>,
173 #[serde(default, skip_serializing_if = "Option::is_none")]
174 pub section: Option<PmiSection>,
175}
176
177#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
185pub struct PmiSection {
186 pub axis: String,
187 pub offset: f64,
188 #[serde(default)]
189 pub flip: bool,
190 #[serde(default, skip_serializing_if = "Option::is_none")]
191 pub origin: Option<[f64; 3]>,
192 #[serde(default, skip_serializing_if = "Option::is_none")]
193 pub normal: Option<[f64; 3]>,
194 #[serde(default, skip_serializing_if = "Option::is_none")]
195 pub plane: Option<String>,
196 #[serde(default, skip_serializing_if = "Vec::is_empty")]
199 pub exclude: Vec<String>,
200 #[serde(default, skip_serializing_if = "Vec::is_empty")]
205 pub planes: Vec<PmiSectionPlane>,
206}
207
208#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
212pub struct PmiSectionPlane {
213 pub axis: String,
214 #[serde(default)]
215 pub offset: f64,
216 #[serde(default)]
217 pub flip: bool,
218 #[serde(default, skip_serializing_if = "Option::is_none")]
219 pub normal: Option<[f64; 3]>,
220}
221
222impl PmiSectionPlane {
223 pub fn axis_normal(&self, camera: Option<&PmiCamera>) -> Option<[f64; 3]> {
226 match self.axis.as_str() {
227 "X" => Some([1., 0., 0.]),
228 "Y" => Some([0., 1., 0.]),
229 "Z" => Some([0., 0., 1.]),
230 "View" => self.normal.or_else(|| camera.map(PmiCamera::view_direction)),
231 _ => None,
232 }
233 }
234}
235impl PmiSection {
236 pub fn excludes(&self, solid: &str) -> bool {
240 self.exclude.iter().any(|entry| {
241 let entry = entry.trim();
242 !entry.is_empty() && (solid == entry || solid.strip_prefix(entry).is_some_and(|rest| rest.starts_with(':')))
243 })
244 }
245
246 pub fn axis_normal(&self, camera: Option<&PmiCamera>) -> Option<[f64; 3]> {
249 match self.axis.as_str() {
250 "X" => Some([1., 0., 0.]),
251 "Y" => Some([0., 1., 0.]),
252 "Z" => Some([0., 0., 1.]),
253 "Plane" => self.normal,
254 _ => self.normal.or_else(|| camera.map(PmiCamera::view_direction)),
255 }
256 }
257
258 pub fn freeze(&mut self, camera: Option<&PmiCamera>) {
261 if self.axis == "Plane" {
262 self.origin = None;
264 self.normal = None;
265 return;
266 }
267 self.plane = None;
268 let Some(camera) = camera else { return };
269 for plane in &mut self.planes {
270 if plane.axis == "View" {
271 plane.normal.get_or_insert(camera.view_direction());
272 } else {
273 plane.normal = None;
274 }
275 }
276 self.origin.get_or_insert(camera.target);
277 if self.axis == "View" {
278 self.normal.get_or_insert(camera.view_direction());
279 } else {
280 self.normal = None;
281 }
282 }
283}
284impl PmiView {
285 pub fn section_plane(&self) -> Option<([f64; 3], [f64; 3])> {
286 let section = self.display.section.as_ref()?;
287 let camera = self.camera.as_ref();
288 let mut normal = section.axis_normal(camera)?;
289 let origin = section.origin.or_else(|| camera.filter(|_| section.axis != "Plane").map(|c| c.target))?;
290 let point = std::array::from_fn(|i| origin[i] + normal[i] * section.offset);
291 if section.flip {
292 normal = normal.map(|v| -v);
293 }
294 Some((point, normal))
295 }
296
297 pub fn section_plane_in(&self, report: Option<&PmiViewReport>) -> Option<([f64; 3], [f64; 3])> {
300 match self.display.section.as_ref() {
301 Some(section) if section.axis == "Plane" => report.and_then(|report| report.section_plane),
302 _ => self.section_plane(),
303 }
304 }
305
306 pub fn section_planes_in(&self, report: Option<&PmiViewReport>) -> Vec<([f64; 3], [f64; 3])> {
312 self.section_plane_in(report).map_or_else(Vec::new, |first| self.with_further_planes(first))
313 }
314
315 pub fn section_planes(&self) -> Vec<([f64; 3], [f64; 3])> {
318 self.section_plane().map_or_else(Vec::new, |first| self.with_further_planes(first))
319 }
320
321 fn with_further_planes(&self, first: ([f64; 3], [f64; 3])) -> Vec<([f64; 3], [f64; 3])> {
322 let mut planes = vec![first];
323 let Some(section) = self.display.section.as_ref() else { return planes };
324 let camera = self.camera.as_ref();
325 let origin = section.origin.or_else(|| camera.filter(|_| section.axis != "Plane").map(|c| c.target)).unwrap_or(first.0);
326 for plane in §ion.planes {
327 let Some(mut normal) = plane.axis_normal(camera) else { continue };
328 let point = std::array::from_fn(|i| origin[i] + normal[i] * plane.offset);
329 if plane.flip {
330 normal = normal.map(|v| -v);
331 }
332 planes.push((point, normal));
333 }
334 planes
335 }
336}
337
338fn default_text_size() -> f64 {
339 12.0
340}
341
342impl Default for PmiDisplay {
343 fn default() -> Self {
344 Self {
345 text_size_pt: default_text_size(),
346 wireframe: false,
347 hidden: Vec::new(),
348 section: None,
349 }
350 }
351}
352
353pub fn clamp_text_size(size: f64) -> f64 {
355 if !size.is_finite() {
356 return default_text_size();
357 }
358 size.clamp(1.0, 288.0)
359}
360
361#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
363pub struct PmiAnnotation {
364 #[serde(rename = "type")]
365 pub kind: String,
366 #[serde(default = "default_true")]
367 pub enabled: bool,
368 #[serde(default, rename = "inputParams")]
369 pub params: Value,
370 #[serde(default, rename = "persistentData", skip_serializing_if = "Value::is_null")]
371 pub persistent_data: Value,
372 #[serde(default, rename = "pluginReplay", skip_serializing_if = "Option::is_none")]
374 pub plugin_replay: Option<PluginAnnotationReplay>,
375 #[serde(default, rename = "labelWorld", skip_serializing_if = "Option::is_none")]
378 pub label_world: Option<[f64; 3]>,
379}
380
381fn is_false(value: &bool) -> bool { !value }
382
383fn default_true() -> bool {
384 true
385}
386
387impl PmiAnnotation {
388 pub fn id(&self) -> &str {
390 self.params
391 .get("id")
392 .and_then(Value::as_str)
393 .unwrap_or("")
394 }
395
396 pub fn text(&self, key: &str) -> &str {
398 self.params
399 .get(key)
400 .and_then(Value::as_str)
401 .map(str::trim)
402 .unwrap_or("")
403 }
404
405 pub fn flag(&self, key: &str) -> bool {
407 self.params
408 .get(key)
409 .and_then(Value::as_bool)
410 .unwrap_or(false)
411 }
412
413 pub fn number(&self, key: &str, env: &Env, default: f64) -> Result<f64, String> {
416 match self.params.get(key) {
417 None | Some(Value::Null) => Ok(default),
418 Some(Value::Number(number)) => Ok(number.as_f64().unwrap_or(default)),
419 Some(Value::Bool(flag)) => Ok(if *flag { 1.0 } else { 0.0 }),
420 Some(Value::String(source)) => {
421 let source = source.trim();
422 if source.is_empty() {
423 return Ok(default);
424 }
425 env.eval(source)
426 .map_err(|error| format!("{key}: {error}"))
427 }
428 Some(other) => Err(format!("{key}: expected a number, got {other}")),
429 }
430 }
431
432 pub fn plane_ref(&self) -> Option<&str> {
435 let name = self.text("plane").trim();
436 (!name.is_empty()).then_some(name)
437 }
438
439 pub fn references(&self, key: &str) -> Vec<String> {
442 match self.params.get(key) {
443 Some(Value::String(name)) => {
444 let name = name.trim();
445 if name.is_empty() {
446 Vec::new()
447 } else {
448 vec![name.to_string()]
449 }
450 }
451 Some(Value::Array(items)) => items
452 .iter()
453 .filter_map(Value::as_str)
454 .map(str::trim)
455 .filter(|name| !name.is_empty())
456 .map(String::from)
457 .collect(),
458 _ => Vec::new(),
459 }
460 }
461}
462
463impl PmiState {
464 pub fn next_id(&mut self, prefix: &str) -> String {
468 let seen = self.max_numeric_suffix();
469 if seen > self.id_counter {
470 self.id_counter = seen;
471 }
472 loop {
473 self.id_counter += 1;
474 let candidate = format!("{prefix}{}", self.id_counter);
475 if self.find_view(&candidate).is_none() && self.find_annotation(&candidate).is_none() {
476 return candidate;
477 }
478 }
479 }
480
481 fn max_numeric_suffix(&self) -> u64 {
482 let mut best = 0u64;
483 let mut consider = |id: &str| {
484 let digits = id
485 .bytes()
486 .rev()
487 .take_while(u8::is_ascii_digit)
488 .count();
489 if digits > 0 {
490 if let Ok(value) = id[id.len() - digits..].parse::<u64>() {
491 best = best.max(value);
492 }
493 }
494 };
495 for view in &self.views {
496 consider(&view.id);
497 for annotation in &view.annotations {
498 consider(annotation.id());
499 }
500 }
501 best
502 }
503
504 pub fn find_view(&self, id: &str) -> Option<&PmiView> {
505 self.views.iter().find(|view| view.id == id)
506 }
507
508 pub fn find_view_mut(&mut self, id: &str) -> Option<&mut PmiView> {
509 self.views.iter_mut().find(|view| view.id == id)
510 }
511
512 pub fn find_annotation(&self, id: &str) -> Option<(&PmiView, &PmiAnnotation)> {
514 self.views.iter().find_map(|view| {
515 view.annotations
516 .iter()
517 .find(|annotation| annotation.id() == id)
518 .map(|annotation| (view, annotation))
519 })
520 }
521
522 pub fn locate_annotation(&self, id: &str) -> Option<(usize, usize)> {
524 self.views.iter().enumerate().find_map(|(view_index, view)| {
525 view.annotations
526 .iter()
527 .position(|annotation| annotation.id() == id)
528 .map(|index| (view_index, index))
529 })
530 }
531
532 pub fn find_annotation_mut(&mut self, id: &str) -> Option<&mut PmiAnnotation> {
533 self.views.iter_mut().find_map(|view| {
534 view.annotations
535 .iter_mut()
536 .find(|annotation| annotation.id() == id)
537 })
538 }
539
540 pub fn datum_letters(&self) -> Vec<(String, String)> {
544 let mut out = Vec::new();
545 for view in &self.views {
546 for annotation in &view.annotations {
547 if annotation.kind == annotations::datum::DEF.type_id {
548 let letter = annotation.text("letter").to_uppercase();
549 if !letter.is_empty() {
550 out.push((letter, annotation.id().to_string()));
551 }
552 }
553 }
554 }
555 out
556 }
557
558 pub fn next_datum_letter(&self) -> Option<String> {
561 let used: Vec<String> = self.datum_letters().into_iter().map(|(l, _)| l).collect();
562 let alphabet: Vec<char> = ('A'..='Z').filter(|c| !matches!(c, 'I' | 'O' | 'Q')).collect();
563 for letter in &alphabet {
564 let candidate = letter.to_string();
565 if !used.contains(&candidate) {
566 return Some(candidate);
567 }
568 }
569 for first in &alphabet {
570 for second in &alphabet {
571 let candidate = format!("{first}{second}");
572 if !used.contains(&candidate) {
573 return Some(candidate);
574 }
575 }
576 }
577 None
578 }
579
580 pub fn is_empty(&self) -> bool {
582 self.views.is_empty() && self.id_counter == 0
583 }
584}
585
586#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
592pub struct PmiReport {
593 pub views: Vec<PmiViewReport>,
594}
595
596impl PmiReport {
597 pub fn view(&self, id: &str) -> Option<&PmiViewReport> {
598 self.views.iter().find(|view| view.id == id)
599 }
600
601 pub fn annotation(&self, id: &str) -> Option<&PmiAnnotationReport> {
602 self.views
603 .iter()
604 .find_map(|view| view.annotations.iter().find(|a| a.id == id))
605 }
606
607 pub fn annotation_mut(&mut self, id: &str) -> Option<&mut PmiAnnotationReport> {
608 self.views
609 .iter_mut()
610 .find_map(|view| view.annotations.iter_mut().find(|a| a.id == id))
611 }
612}
613
614#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
615pub struct PmiViewReport {
616 pub id: String,
617 pub annotations: Vec<PmiAnnotationReport>,
618 #[serde(default, rename = "sectionPlane", skip_serializing_if = "Option::is_none")]
621 pub section_plane: Option<([f64; 3], [f64; 3])>,
622 #[serde(default, rename = "sectionError", skip_serializing_if = "String::is_empty")]
624 pub section_error: String,
625}
626
627#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
628#[serde(rename_all = "kebab-case")]
629pub enum PmiStatus {
630 Ok,
632 Error,
635}
636
637#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
639pub struct PmiAnnotationReport {
640 #[serde(default, rename = "persistentData", skip_serializing_if = "Option::is_none")]
642 pub persistent_data: Option<Value>,
643 #[serde(default, rename = "pluginReplay", skip_serializing_if = "Option::is_none")]
644 pub plugin_replay: Option<PluginAnnotationReplay>,
645 pub id: String,
646 #[serde(rename = "type")]
647 pub kind: String,
648 pub enabled: bool,
649 pub status: PmiStatus,
650 #[serde(default)]
651 pub message: String,
652 #[serde(default)]
655 pub text: String,
656 #[serde(default, skip_serializing_if = "Option::is_none")]
658 pub value: Option<f64>,
659 #[serde(default)]
661 pub unit: String,
662 #[serde(default)]
665 pub references: Vec<String>,
666 #[serde(rename = "labelWorld")]
669 pub label_world: [f64; 3],
670 #[serde(default, skip_serializing_if = "Option::is_none")]
672 pub plane: Option<PmiPlane>,
673 pub geometry: PmiGeometry,
674}
675
676#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
683pub struct PmiPlane {
684 pub origin: [f64; 3],
685 pub normal: [f64; 3],
686 #[serde(rename = "xAxis")]
687 pub x_axis: [f64; 3],
688}
689
690impl PmiPlane {
691 pub fn project(&self, point: [f64; 3]) -> [f64; 3] {
693 let n = self.normal;
694 let d = (point[0] - self.origin[0]) * n[0] + (point[1] - self.origin[1]) * n[1] + (point[2] - self.origin[2]) * n[2];
695 [point[0] - n[0] * d, point[1] - n[1] * d, point[2] - n[2] * d]
696 }
697
698 pub fn y_axis(&self) -> [f64; 3] {
700 let n = self.normal;
701 let x = self.x_axis;
702 [n[1] * x[2] - n[2] * x[1], n[2] * x[0] - n[0] * x[2], n[0] * x[1] - n[1] * x[0]]
703 }
704
705 pub fn hit(&self, origin: [f64; 3], dir: [f64; 3]) -> Option<[f64; 3]> {
707 let n = self.normal;
708 let denominator = dir[0] * n[0] + dir[1] * n[1] + dir[2] * n[2];
709 if denominator.abs() < 1e-12 {
710 return None;
711 }
712 let diff = [self.origin[0] - origin[0], self.origin[1] - origin[1], self.origin[2] - origin[2]];
713 let t = (diff[0] * n[0] + diff[1] * n[1] + diff[2] * n[2]) / denominator;
714 Some([origin[0] + dir[0] * t, origin[1] + dir[1] * t, origin[2] + dir[2] * t])
715 }
716}
717
718#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
721#[serde(tag = "kind", rename_all = "camelCase")]
722pub enum PmiGeometry {
723 None,
724 Linear {
727 a: [f64; 3],
728 b: [f64; 3],
729 #[serde(default, skip_serializing_if = "Option::is_none")]
730 component: Option<char>,
731 },
732 Radial {
733 center: [f64; 3],
734 axis: [f64; 3],
735 radius: f64,
736 diameter: bool,
737 sphere: bool,
738 },
739 Angular {
742 vertex: [f64; 3],
743 #[serde(rename = "dirA")]
744 dir_a: [f64; 3],
745 #[serde(rename = "dirB")]
746 dir_b: [f64; 3],
747 axis: [f64; 3],
748 degrees: f64,
749 },
750 Leader {
751 targets: Vec<[f64; 3]>,
752 dot: bool,
753 #[serde(default, skip_serializing_if = "is_false")]
754 balloon: bool,
755 #[serde(default, skip_serializing_if = "Option::is_none")]
758 anchor: Option<BalloonAnchor>,
759 },
760 Note {
761 position: [f64; 3],
762 },
763 Hole {
764 anchor: [f64; 3],
765 normal: [f64; 3],
766 },
767 Explode {
769 solids: Vec<String>,
770 translate: [f64; 3],
771 #[serde(rename = "rotateDeg")]
772 rotate_deg: [f64; 3],
773 scale: [f64; 3],
774 center: [f64; 3],
776 trace: bool,
777 },
778 Datum {
779 anchor: [f64; 3],
780 normal: [f64; 3],
781 letter: String,
782 },
783 Fcf {
784 anchor: [f64; 3],
785 normal: [f64; 3],
786 frame: FcfFrame,
787 },
788}
789
790#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
792pub struct FcfFrame {
793 pub characteristic: String,
795 pub symbol: String,
797 pub zone: String,
799 pub datums: Vec<String>,
801}
802
803#[derive(Debug, Clone, Copy, PartialEq)]
810pub struct ToleranceBlock {
811 pub mode: ToleranceMode,
812 pub upper: f64,
813 pub lower: f64,
814}
815
816#[derive(Debug, Clone, Copy, PartialEq, Eq)]
817pub enum ToleranceMode {
818 None,
819 Symmetric,
820 Deviation,
821 Limits,
822}
823
824impl ToleranceMode {
825 pub fn parse(text: &str) -> Self {
826 match text.trim().to_ascii_lowercase().as_str() {
827 "symmetric" => ToleranceMode::Symmetric,
828 "deviation" => ToleranceMode::Deviation,
829 "limits" => ToleranceMode::Limits,
830 _ => ToleranceMode::None,
831 }
832 }
833
834 pub fn as_str(self) -> &'static str {
835 match self {
836 ToleranceMode::None => "none",
837 ToleranceMode::Symmetric => "symmetric",
838 ToleranceMode::Deviation => "deviation",
839 ToleranceMode::Limits => "limits",
840 }
841 }
842}
843
844impl ToleranceBlock {
845 pub fn read(annotation: &PmiAnnotation, env: &Env) -> Result<Self, String> {
847 Ok(Self {
848 mode: ToleranceMode::parse(annotation.text("tolMode")),
849 upper: annotation.number("tolUpper", env, 0.0)?.abs(),
850 lower: annotation.number("tolLower", env, 0.0)?.abs(),
851 })
852 }
853
854 pub fn bounds(&self) -> Option<(f64, f64)> {
857 match self.mode {
858 ToleranceMode::None => None,
859 ToleranceMode::Symmetric => Some((-self.upper, self.upper)),
860 ToleranceMode::Deviation | ToleranceMode::Limits => Some((-self.lower, self.upper)),
861 }
862 }
863}
864
865pub fn format_number(value: f64, decimals: usize) -> String {
867 let decimals = decimals.min(8);
868 let text = format!("{:.*}", decimals, value);
869 if text.starts_with('-') && text[1..].bytes().all(|b| b == b'0' || b == b'.') {
871 text[1..].to_string()
872 } else {
873 text
874 }
875}
876
877pub fn format_dimension(
881 value: f64,
882 decimals: usize,
883 tolerance: &ToleranceBlock,
884 is_reference: bool,
885 prefix: &str,
886 suffix: &str,
887) -> String {
888 let nominal = format!("{prefix}{}{suffix}", format_number(value, decimals));
889 if is_reference {
890 return format!("({nominal})");
891 }
892 match tolerance.mode {
893 ToleranceMode::None => nominal,
894 ToleranceMode::Symmetric => format!(
895 "{nominal} \u{00B1}{}{suffix}",
896 format_number(tolerance.upper, decimals)
897 ),
898 ToleranceMode::Deviation => format!(
899 "{nominal} +{}{suffix}/\u{2212}{}{suffix}",
900 format_number(tolerance.upper, decimals),
901 format_number(tolerance.lower, decimals)
902 ),
903 ToleranceMode::Limits => format!(
904 "{prefix}{}{suffix} / {prefix}{}{suffix}",
905 format_number(value + tolerance.upper, decimals),
906 format_number(value - tolerance.lower, decimals)
907 ),
908 }
909}
910
911pub struct PmiContext<'a> {
918 pub scene: &'a SceneMap,
919 pub request: &'a HistoryRequest,
920 pub env: &'a Env,
921 pub state: &'a PmiState,
923 pub datums: BTreeMap<String, String>,
925}
926
927pub struct Resolved {
929 pub text: String,
930 pub value: Option<f64>,
931 pub unit: &'static str,
932 pub references: Vec<String>,
933 pub geometry: PmiGeometry,
934 pub default_label: [f64; 3],
936}
937
938pub(crate) fn finish_history_run(
941 request: &HistoryRequest,
942 scene: &SceneMap,
943 env: &Env,
944) -> Option<PmiReport> {
945 let state = request.pmi.as_ref()?;
946 Some(resolve_state(state, scene, request, env))
947}
948
949pub fn resolve_state(
951 state: &PmiState,
952 scene: &SceneMap,
953 request: &HistoryRequest,
954 env: &Env,
955) -> PmiReport {
956 let mut datums: BTreeMap<String, String> = BTreeMap::new();
959 for (letter, id) in state.datum_letters() {
960 datums.entry(letter).or_insert(id);
961 }
962 let mut state = state.clone();
965 let mut section_errors: BTreeMap<String, String> = BTreeMap::new();
966 for view in &mut state.views {
967 if let Err(error) = resolve_section_plane(view, scene) {
968 section_errors.insert(view.id.clone(), error);
969 }
970 }
971 let state = &state;
972 let context = PmiContext {
973 scene,
974 request,
975 env,
976 state,
977 datums,
978 };
979 PmiReport {
980 views: state
981 .views
982 .iter()
983 .map(|view| {
984 let section = section_half_space(view);
985 let exempt = |reference: &str| {
986 let section = view.display.section.as_ref();
987 section.is_some_and(|section| reference_solid(scene, reference).is_some_and(|solid| section.excludes(&solid)))
988 };
989 PmiViewReport {
990 section_plane: view.display.section.as_ref().filter(|s| s.axis == "Plane").and_then(|_| view.section_plane()),
991 section_error: section_errors.remove(&view.id).unwrap_or_default(),
992 id: view.id.clone(),
993 annotations: view
994 .annotations
995 .iter()
996 .map(|annotation| {
997 let report = resolve_annotation(annotation, &context, view.camera.as_ref());
998 match §ion {
999 Some(section) => sectioned_away(report, section, &exempt),
1000 None => report,
1001 }
1002 })
1003 .collect(),
1004 }
1005 })
1006 .collect(),
1007 }
1008}
1009
1010fn resolve_section_plane(view: &mut PmiView, scene: &SceneMap) -> Result<(), String> {
1013 let Some(section) = view.display.section.as_mut().filter(|s| s.axis == "Plane") else {
1014 return Ok(());
1015 };
1016 let name = section.plane.clone().unwrap_or_default();
1017 if name.trim().is_empty() {
1018 return Err("pick a planar face or a datum plane to cut on".into());
1019 }
1020 match resolve::resolve_reference(scene, &name) {
1021 Ok(crate::SelectionGeometry::Plane { origin, normal }) => {
1022 section.origin = Some(resolve::a3(origin));
1023 section.normal = Some(resolve::a3(normal));
1024 Ok(())
1025 }
1026 Ok(_) => Err(format!("section plane '{name}' is not planar: pick a planar face or a datum plane")),
1027 Err(error) => Err(format!("section plane '{name}': {error}")),
1028 }
1029}
1030
1031fn section_half_space(view: &PmiView) -> Option<anchor::HalfSpace> {
1036 if view.annotations.iter().any(|a| a.enabled && a.kind == "explode") {
1037 return None;
1038 }
1039 anchor::HalfSpace::of(&view.section_planes())
1040}
1041
1042fn sectioned_away(mut report: PmiAnnotationReport, section: &anchor::HalfSpace, exempt: &dyn Fn(&str) -> bool) -> PmiAnnotationReport {
1050 if report.status != PmiStatus::Ok {
1051 return report;
1052 }
1053 let points: Vec<[f64; 3]> = match &report.geometry {
1054 PmiGeometry::Linear { a, b, component: None } => vec![*a, *b],
1055 PmiGeometry::Linear { a, component: Some(_), .. } => vec![*a],
1057 PmiGeometry::Leader { targets, balloon: false, .. } => targets.clone(),
1058 PmiGeometry::Hole { anchor, .. } | PmiGeometry::Datum { anchor, .. } | PmiGeometry::Fcf { anchor, .. } => vec![*anchor],
1059 _ => return report,
1060 };
1061 let references = &report.references;
1064 let point_exempt = |index: usize| match references.len() == points.len() {
1065 true => exempt(&references[index]),
1066 false => !references.is_empty() && references.iter().all(|reference| exempt(reference)),
1067 };
1068 if points.iter().enumerate().all(|(index, point)| point_exempt(index) || section.keeps(*point, 1e-6)) {
1069 return report;
1070 }
1071 report.status = PmiStatus::Error;
1072 report.message = "it is on geometry the section removes in this view: move the section, Reverse it, or re-attach the annotation".into();
1073 report.text = String::new();
1074 report.value = None;
1075 report.geometry = PmiGeometry::None;
1076 report.plane = None;
1077 report
1078}
1079
1080fn reference_solid(scene: &SceneMap, reference: &str) -> Option<String> {
1083 let reference = reference.trim();
1084 if let Some((solid, _)) = reference.split_once('@') {
1085 return Some(solid.to_string());
1086 }
1087 let handle = scene
1088 .resolve_face(reference)
1089 .map(|face| face.handle)
1090 .or_else(|| scene.resolve_edge(reference).map(|edge| edge.handle))
1091 .or_else(|| scene.resolve_solid(reference))?;
1092 scene.solids.iter().find(|(_, h)| **h == handle).map(|(name, _)| name.clone())
1093}
1094
1095pub fn resolve_annotation(
1099 annotation: &PmiAnnotation,
1100 context: &PmiContext<'_>,
1101 camera: Option<&PmiCamera>,
1102) -> PmiAnnotationReport {
1103 let mut persistent_data = None;
1104 let mut plugin_replay = None;
1105 let outcome = match pmi_type(&annotation.kind) {
1106 None if annotation.kind.contains('/') && !annotation.enabled => Ok(Resolved {
1107 text: String::new(), value: None, unit: "", references: Vec::new(),
1108 geometry: PmiGeometry::None, default_label: annotation.label_world.unwrap_or([0.; 3]),
1109 }),
1110 Some(def) => (def.resolve)(annotation, context),
1111 None if annotation.kind.contains('/') => plugin::resolve(annotation, context).map(|(resolved, data, replay)| {
1112 persistent_data = Some(data);
1113 plugin_replay = Some(replay);
1114 resolved
1115 }),
1116 None => Err(format!("unknown PMI annotation type '{}'", annotation.kind)),
1117 }
1118 .and_then(|resolved| {
1119 let plane = annotation_plane(annotation, context, camera, &resolved.geometry)?;
1120 Ok((resolved, plane))
1121 });
1122 match outcome {
1123 Ok((resolved, plane)) => {
1124 let label = annotation.label_world.unwrap_or(resolved.default_label);
1125 PmiAnnotationReport {
1126 persistent_data,
1127 plugin_replay,
1128 id: annotation.id().to_string(),
1129 kind: annotation.kind.clone(),
1130 enabled: annotation.enabled,
1131 status: PmiStatus::Ok,
1132 message: String::new(),
1133 text: resolved.text,
1134 value: resolved.value,
1135 unit: resolved.unit.to_string(),
1136 references: resolved.references,
1137 label_world: plane.map_or(label, |plane| plane.project(label)),
1138 plane,
1139 geometry: resolved.geometry,
1140 }
1141 }
1142 Err(message) => PmiAnnotationReport {
1143 persistent_data: None,
1144 plugin_replay: None,
1145 id: annotation.id().to_string(),
1146 kind: annotation.kind.clone(),
1147 enabled: annotation.enabled,
1148 status: PmiStatus::Error,
1149 message,
1150 text: String::new(),
1151 value: None,
1152 unit: String::new(),
1153 references: Vec::new(),
1154 label_world: annotation
1157 .label_world
1158 .or_else(|| (annotation.kind == "balloon").then(|| annotations::balloon::error_bubble(annotation, context)).flatten())
1159 .unwrap_or([0.0; 3]),
1160 plane: None,
1161 geometry: PmiGeometry::None,
1162 },
1163 }
1164}
1165
1166fn annotation_plane(
1174 annotation: &PmiAnnotation,
1175 context: &PmiContext<'_>,
1176 camera: Option<&PmiCamera>,
1177 geometry: &PmiGeometry,
1178) -> Result<Option<PmiPlane>, String> {
1179 use crate::SelectionGeometry;
1180 use resolve::{a3, perpendicular_in_plane, v3};
1181 let Some(name) = annotation.plane_ref() else {
1182 return Ok(None);
1183 };
1184 let (origin, normal) = match resolve::resolve_reference(context.scene, name)? {
1185 SelectionGeometry::Plane { origin, normal } => (origin, normal),
1186 _ => return Err(format!("annotation plane '{name}' must be a planar face or a reference plane")),
1187 };
1188 let mut normal = normal
1189 .normalized()
1190 .map_err(|_| format!("annotation plane '{name}' has no normal"))?;
1191 const PARALLEL: f64 = 1e-3;
1193 match geometry {
1194 PmiGeometry::Linear { a, b, .. } => {
1195 let span = v3(*b).sub(v3(*a));
1196 if span.length() > 1e-9 && span.normalized().map(|d| d.dot(normal).abs()).unwrap_or(0.0) > PARALLEL {
1197 return Err(format!("annotation plane '{name}' is not parallel to the measured direction"));
1198 }
1199 }
1200 PmiGeometry::Angular { axis, .. } => {
1201 if v3(*axis).cross(normal).length() > PARALLEL {
1202 return Err(format!("annotation plane '{name}' is not parallel to the angle's plane"));
1203 }
1204 }
1205 PmiGeometry::Radial { axis, sphere: false, .. } => {
1206 if v3(*axis).cross(normal).length() > PARALLEL {
1207 return Err(format!("annotation plane '{name}' is not parallel to the circle's plane"));
1208 }
1209 }
1210 _ => {}
1211 }
1212 let x_axis = match camera {
1213 Some(camera) => {
1214 let view = v3(camera.view_direction());
1215 if normal.dot(view) > 0.0 {
1216 normal = normal.scale(-1.0);
1217 }
1218 perpendicular_in_plane(normal, view.cross(v3(camera.up)))
1219 }
1220 None => perpendicular_in_plane(normal, crate::Vec3::new(1.0, 0.0, 0.0)),
1221 };
1222 Ok(Some(PmiPlane {
1223 origin: a3(origin),
1224 normal: a3(normal),
1225 x_axis: a3(x_axis),
1226 }))
1227}
1228