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)]
179pub struct PmiSection {
180 pub axis: String,
181 pub offset: f64,
182 #[serde(default)]
183 pub flip: bool,
184}
185impl PmiView {
186 pub fn section_plane(&self) -> Option<([f64; 3], [f64; 3])> {
187 let section = self.display.section.as_ref()?;
188 let camera = self.camera.as_ref()?;
189 let mut normal = match section.axis.as_str() {
190 "X" => [1., 0., 0.],
191 "Y" => [0., 1., 0.],
192 "Z" => [0., 0., 1.],
193 _ => camera.view_direction(),
194 };
195 let point = std::array::from_fn(|i| camera.target[i] + normal[i] * section.offset);
196 if section.flip {
197 normal = normal.map(|v| -v);
198 }
199 Some((point, normal))
200 }
201}
202
203fn default_text_size() -> f64 {
204 12.0
205}
206
207impl Default for PmiDisplay {
208 fn default() -> Self {
209 Self {
210 text_size_pt: default_text_size(),
211 wireframe: false,
212 hidden: Vec::new(),
213 section: None,
214 }
215 }
216}
217
218pub fn clamp_text_size(size: f64) -> f64 {
220 if !size.is_finite() {
221 return default_text_size();
222 }
223 size.clamp(1.0, 288.0)
224}
225
226#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
228pub struct PmiAnnotation {
229 #[serde(rename = "type")]
230 pub kind: String,
231 #[serde(default = "default_true")]
232 pub enabled: bool,
233 #[serde(default, rename = "inputParams")]
234 pub params: Value,
235 #[serde(default, rename = "persistentData", skip_serializing_if = "Value::is_null")]
236 pub persistent_data: Value,
237 #[serde(default, rename = "pluginReplay", skip_serializing_if = "Option::is_none")]
239 pub plugin_replay: Option<PluginAnnotationReplay>,
240 #[serde(default, rename = "labelWorld", skip_serializing_if = "Option::is_none")]
243 pub label_world: Option<[f64; 3]>,
244}
245
246fn is_false(value: &bool) -> bool { !value }
247
248fn default_true() -> bool {
249 true
250}
251
252impl PmiAnnotation {
253 pub fn id(&self) -> &str {
255 self.params
256 .get("id")
257 .and_then(Value::as_str)
258 .unwrap_or("")
259 }
260
261 pub fn text(&self, key: &str) -> &str {
263 self.params
264 .get(key)
265 .and_then(Value::as_str)
266 .map(str::trim)
267 .unwrap_or("")
268 }
269
270 pub fn flag(&self, key: &str) -> bool {
272 self.params
273 .get(key)
274 .and_then(Value::as_bool)
275 .unwrap_or(false)
276 }
277
278 pub fn number(&self, key: &str, env: &Env, default: f64) -> Result<f64, String> {
281 match self.params.get(key) {
282 None | Some(Value::Null) => Ok(default),
283 Some(Value::Number(number)) => Ok(number.as_f64().unwrap_or(default)),
284 Some(Value::Bool(flag)) => Ok(if *flag { 1.0 } else { 0.0 }),
285 Some(Value::String(source)) => {
286 let source = source.trim();
287 if source.is_empty() {
288 return Ok(default);
289 }
290 env.eval(source)
291 .map_err(|error| format!("{key}: {error}"))
292 }
293 Some(other) => Err(format!("{key}: expected a number, got {other}")),
294 }
295 }
296
297 pub fn plane_ref(&self) -> Option<&str> {
300 let name = self.text("plane").trim();
301 (!name.is_empty()).then_some(name)
302 }
303
304 pub fn references(&self, key: &str) -> Vec<String> {
307 match self.params.get(key) {
308 Some(Value::String(name)) => {
309 let name = name.trim();
310 if name.is_empty() {
311 Vec::new()
312 } else {
313 vec![name.to_string()]
314 }
315 }
316 Some(Value::Array(items)) => items
317 .iter()
318 .filter_map(Value::as_str)
319 .map(str::trim)
320 .filter(|name| !name.is_empty())
321 .map(String::from)
322 .collect(),
323 _ => Vec::new(),
324 }
325 }
326}
327
328impl PmiState {
329 pub fn next_id(&mut self, prefix: &str) -> String {
333 let seen = self.max_numeric_suffix();
334 if seen > self.id_counter {
335 self.id_counter = seen;
336 }
337 loop {
338 self.id_counter += 1;
339 let candidate = format!("{prefix}{}", self.id_counter);
340 if self.find_view(&candidate).is_none() && self.find_annotation(&candidate).is_none() {
341 return candidate;
342 }
343 }
344 }
345
346 fn max_numeric_suffix(&self) -> u64 {
347 let mut best = 0u64;
348 let mut consider = |id: &str| {
349 let digits = id
350 .bytes()
351 .rev()
352 .take_while(u8::is_ascii_digit)
353 .count();
354 if digits > 0 {
355 if let Ok(value) = id[id.len() - digits..].parse::<u64>() {
356 best = best.max(value);
357 }
358 }
359 };
360 for view in &self.views {
361 consider(&view.id);
362 for annotation in &view.annotations {
363 consider(annotation.id());
364 }
365 }
366 best
367 }
368
369 pub fn find_view(&self, id: &str) -> Option<&PmiView> {
370 self.views.iter().find(|view| view.id == id)
371 }
372
373 pub fn find_view_mut(&mut self, id: &str) -> Option<&mut PmiView> {
374 self.views.iter_mut().find(|view| view.id == id)
375 }
376
377 pub fn find_annotation(&self, id: &str) -> Option<(&PmiView, &PmiAnnotation)> {
379 self.views.iter().find_map(|view| {
380 view.annotations
381 .iter()
382 .find(|annotation| annotation.id() == id)
383 .map(|annotation| (view, annotation))
384 })
385 }
386
387 pub fn locate_annotation(&self, id: &str) -> Option<(usize, usize)> {
389 self.views.iter().enumerate().find_map(|(view_index, view)| {
390 view.annotations
391 .iter()
392 .position(|annotation| annotation.id() == id)
393 .map(|index| (view_index, index))
394 })
395 }
396
397 pub fn find_annotation_mut(&mut self, id: &str) -> Option<&mut PmiAnnotation> {
398 self.views.iter_mut().find_map(|view| {
399 view.annotations
400 .iter_mut()
401 .find(|annotation| annotation.id() == id)
402 })
403 }
404
405 pub fn datum_letters(&self) -> Vec<(String, String)> {
409 let mut out = Vec::new();
410 for view in &self.views {
411 for annotation in &view.annotations {
412 if annotation.kind == annotations::datum::DEF.type_id {
413 let letter = annotation.text("letter").to_uppercase();
414 if !letter.is_empty() {
415 out.push((letter, annotation.id().to_string()));
416 }
417 }
418 }
419 }
420 out
421 }
422
423 pub fn next_datum_letter(&self) -> Option<String> {
426 let used: Vec<String> = self.datum_letters().into_iter().map(|(l, _)| l).collect();
427 let alphabet: Vec<char> = ('A'..='Z').filter(|c| !matches!(c, 'I' | 'O' | 'Q')).collect();
428 for letter in &alphabet {
429 let candidate = letter.to_string();
430 if !used.contains(&candidate) {
431 return Some(candidate);
432 }
433 }
434 for first in &alphabet {
435 for second in &alphabet {
436 let candidate = format!("{first}{second}");
437 if !used.contains(&candidate) {
438 return Some(candidate);
439 }
440 }
441 }
442 None
443 }
444
445 pub fn is_empty(&self) -> bool {
447 self.views.is_empty() && self.id_counter == 0
448 }
449}
450
451#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
457pub struct PmiReport {
458 pub views: Vec<PmiViewReport>,
459}
460
461impl PmiReport {
462 pub fn view(&self, id: &str) -> Option<&PmiViewReport> {
463 self.views.iter().find(|view| view.id == id)
464 }
465
466 pub fn annotation(&self, id: &str) -> Option<&PmiAnnotationReport> {
467 self.views
468 .iter()
469 .find_map(|view| view.annotations.iter().find(|a| a.id == id))
470 }
471
472 pub fn annotation_mut(&mut self, id: &str) -> Option<&mut PmiAnnotationReport> {
473 self.views
474 .iter_mut()
475 .find_map(|view| view.annotations.iter_mut().find(|a| a.id == id))
476 }
477}
478
479#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
480pub struct PmiViewReport {
481 pub id: String,
482 pub annotations: Vec<PmiAnnotationReport>,
483}
484
485#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
486#[serde(rename_all = "kebab-case")]
487pub enum PmiStatus {
488 Ok,
490 Error,
493}
494
495#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
497pub struct PmiAnnotationReport {
498 #[serde(default, rename = "persistentData", skip_serializing_if = "Option::is_none")]
500 pub persistent_data: Option<Value>,
501 #[serde(default, rename = "pluginReplay", skip_serializing_if = "Option::is_none")]
502 pub plugin_replay: Option<PluginAnnotationReplay>,
503 pub id: String,
504 #[serde(rename = "type")]
505 pub kind: String,
506 pub enabled: bool,
507 pub status: PmiStatus,
508 #[serde(default)]
509 pub message: String,
510 #[serde(default)]
513 pub text: String,
514 #[serde(default, skip_serializing_if = "Option::is_none")]
516 pub value: Option<f64>,
517 #[serde(default)]
519 pub unit: String,
520 #[serde(default)]
523 pub references: Vec<String>,
524 #[serde(rename = "labelWorld")]
527 pub label_world: [f64; 3],
528 #[serde(default, skip_serializing_if = "Option::is_none")]
530 pub plane: Option<PmiPlane>,
531 pub geometry: PmiGeometry,
532}
533
534#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
541pub struct PmiPlane {
542 pub origin: [f64; 3],
543 pub normal: [f64; 3],
544 #[serde(rename = "xAxis")]
545 pub x_axis: [f64; 3],
546}
547
548impl PmiPlane {
549 pub fn project(&self, point: [f64; 3]) -> [f64; 3] {
551 let n = self.normal;
552 let d = (point[0] - self.origin[0]) * n[0] + (point[1] - self.origin[1]) * n[1] + (point[2] - self.origin[2]) * n[2];
553 [point[0] - n[0] * d, point[1] - n[1] * d, point[2] - n[2] * d]
554 }
555
556 pub fn y_axis(&self) -> [f64; 3] {
558 let n = self.normal;
559 let x = self.x_axis;
560 [n[1] * x[2] - n[2] * x[1], n[2] * x[0] - n[0] * x[2], n[0] * x[1] - n[1] * x[0]]
561 }
562
563 pub fn hit(&self, origin: [f64; 3], dir: [f64; 3]) -> Option<[f64; 3]> {
565 let n = self.normal;
566 let denominator = dir[0] * n[0] + dir[1] * n[1] + dir[2] * n[2];
567 if denominator.abs() < 1e-12 {
568 return None;
569 }
570 let diff = [self.origin[0] - origin[0], self.origin[1] - origin[1], self.origin[2] - origin[2]];
571 let t = (diff[0] * n[0] + diff[1] * n[1] + diff[2] * n[2]) / denominator;
572 Some([origin[0] + dir[0] * t, origin[1] + dir[1] * t, origin[2] + dir[2] * t])
573 }
574}
575
576#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
579#[serde(tag = "kind", rename_all = "camelCase")]
580pub enum PmiGeometry {
581 None,
582 Linear {
585 a: [f64; 3],
586 b: [f64; 3],
587 #[serde(default, skip_serializing_if = "Option::is_none")]
588 component: Option<char>,
589 },
590 Radial {
591 center: [f64; 3],
592 axis: [f64; 3],
593 radius: f64,
594 diameter: bool,
595 sphere: bool,
596 },
597 Angular {
600 vertex: [f64; 3],
601 #[serde(rename = "dirA")]
602 dir_a: [f64; 3],
603 #[serde(rename = "dirB")]
604 dir_b: [f64; 3],
605 axis: [f64; 3],
606 degrees: f64,
607 },
608 Leader {
609 targets: Vec<[f64; 3]>,
610 dot: bool,
611 #[serde(default, skip_serializing_if = "is_false")]
612 balloon: bool,
613 #[serde(default, skip_serializing_if = "Option::is_none")]
616 anchor: Option<BalloonAnchor>,
617 },
618 Note {
619 position: [f64; 3],
620 },
621 Hole {
622 anchor: [f64; 3],
623 normal: [f64; 3],
624 },
625 Explode {
627 solids: Vec<String>,
628 translate: [f64; 3],
629 #[serde(rename = "rotateDeg")]
630 rotate_deg: [f64; 3],
631 scale: [f64; 3],
632 center: [f64; 3],
634 trace: bool,
635 },
636 Datum {
637 anchor: [f64; 3],
638 normal: [f64; 3],
639 letter: String,
640 },
641 Fcf {
642 anchor: [f64; 3],
643 normal: [f64; 3],
644 frame: FcfFrame,
645 },
646}
647
648#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
650pub struct FcfFrame {
651 pub characteristic: String,
653 pub symbol: String,
655 pub zone: String,
657 pub datums: Vec<String>,
659}
660
661#[derive(Debug, Clone, Copy, PartialEq)]
668pub struct ToleranceBlock {
669 pub mode: ToleranceMode,
670 pub upper: f64,
671 pub lower: f64,
672}
673
674#[derive(Debug, Clone, Copy, PartialEq, Eq)]
675pub enum ToleranceMode {
676 None,
677 Symmetric,
678 Deviation,
679 Limits,
680}
681
682impl ToleranceMode {
683 pub fn parse(text: &str) -> Self {
684 match text.trim().to_ascii_lowercase().as_str() {
685 "symmetric" => ToleranceMode::Symmetric,
686 "deviation" => ToleranceMode::Deviation,
687 "limits" => ToleranceMode::Limits,
688 _ => ToleranceMode::None,
689 }
690 }
691
692 pub fn as_str(self) -> &'static str {
693 match self {
694 ToleranceMode::None => "none",
695 ToleranceMode::Symmetric => "symmetric",
696 ToleranceMode::Deviation => "deviation",
697 ToleranceMode::Limits => "limits",
698 }
699 }
700}
701
702impl ToleranceBlock {
703 pub fn read(annotation: &PmiAnnotation, env: &Env) -> Result<Self, String> {
705 Ok(Self {
706 mode: ToleranceMode::parse(annotation.text("tolMode")),
707 upper: annotation.number("tolUpper", env, 0.0)?.abs(),
708 lower: annotation.number("tolLower", env, 0.0)?.abs(),
709 })
710 }
711
712 pub fn bounds(&self) -> Option<(f64, f64)> {
715 match self.mode {
716 ToleranceMode::None => None,
717 ToleranceMode::Symmetric => Some((-self.upper, self.upper)),
718 ToleranceMode::Deviation | ToleranceMode::Limits => Some((-self.lower, self.upper)),
719 }
720 }
721}
722
723pub fn format_number(value: f64, decimals: usize) -> String {
725 let decimals = decimals.min(8);
726 let text = format!("{:.*}", decimals, value);
727 if text.starts_with('-') && text[1..].bytes().all(|b| b == b'0' || b == b'.') {
729 text[1..].to_string()
730 } else {
731 text
732 }
733}
734
735pub fn format_dimension(
739 value: f64,
740 decimals: usize,
741 tolerance: &ToleranceBlock,
742 is_reference: bool,
743 prefix: &str,
744 suffix: &str,
745) -> String {
746 let nominal = format!("{prefix}{}{suffix}", format_number(value, decimals));
747 if is_reference {
748 return format!("({nominal})");
749 }
750 match tolerance.mode {
751 ToleranceMode::None => nominal,
752 ToleranceMode::Symmetric => format!(
753 "{nominal} \u{00B1}{}{suffix}",
754 format_number(tolerance.upper, decimals)
755 ),
756 ToleranceMode::Deviation => format!(
757 "{nominal} +{}{suffix}/\u{2212}{}{suffix}",
758 format_number(tolerance.upper, decimals),
759 format_number(tolerance.lower, decimals)
760 ),
761 ToleranceMode::Limits => format!(
762 "{prefix}{}{suffix} / {prefix}{}{suffix}",
763 format_number(value + tolerance.upper, decimals),
764 format_number(value - tolerance.lower, decimals)
765 ),
766 }
767}
768
769pub struct PmiContext<'a> {
776 pub scene: &'a SceneMap,
777 pub request: &'a HistoryRequest,
778 pub env: &'a Env,
779 pub datums: BTreeMap<String, String>,
781}
782
783pub struct Resolved {
785 pub text: String,
786 pub value: Option<f64>,
787 pub unit: &'static str,
788 pub references: Vec<String>,
789 pub geometry: PmiGeometry,
790 pub default_label: [f64; 3],
792}
793
794pub(crate) fn finish_history_run(
797 request: &HistoryRequest,
798 scene: &SceneMap,
799 env: &Env,
800) -> Option<PmiReport> {
801 let state = request.pmi.as_ref()?;
802 Some(resolve_state(state, scene, request, env))
803}
804
805pub fn resolve_state(
807 state: &PmiState,
808 scene: &SceneMap,
809 request: &HistoryRequest,
810 env: &Env,
811) -> PmiReport {
812 let mut datums: BTreeMap<String, String> = BTreeMap::new();
815 for (letter, id) in state.datum_letters() {
816 datums.entry(letter).or_insert(id);
817 }
818 let context = PmiContext {
819 scene,
820 request,
821 env,
822 datums,
823 };
824 PmiReport {
825 views: state
826 .views
827 .iter()
828 .map(|view| PmiViewReport {
829 id: view.id.clone(),
830 annotations: view
831 .annotations
832 .iter()
833 .map(|annotation| resolve_annotation(annotation, &context, view.camera.as_ref()))
834 .collect(),
835 })
836 .collect(),
837 }
838}
839
840pub fn resolve_annotation(
844 annotation: &PmiAnnotation,
845 context: &PmiContext<'_>,
846 camera: Option<&PmiCamera>,
847) -> PmiAnnotationReport {
848 let mut persistent_data = None;
849 let mut plugin_replay = None;
850 let outcome = match pmi_type(&annotation.kind) {
851 None if annotation.kind.contains('/') && !annotation.enabled => Ok(Resolved {
852 text: String::new(), value: None, unit: "", references: Vec::new(),
853 geometry: PmiGeometry::None, default_label: annotation.label_world.unwrap_or([0.; 3]),
854 }),
855 Some(def) => (def.resolve)(annotation, context),
856 None if annotation.kind.contains('/') => plugin::resolve(annotation, context).map(|(resolved, data, replay)| {
857 persistent_data = Some(data);
858 plugin_replay = Some(replay);
859 resolved
860 }),
861 None => Err(format!("unknown PMI annotation type '{}'", annotation.kind)),
862 }
863 .and_then(|resolved| {
864 let plane = annotation_plane(annotation, context, camera, &resolved.geometry)?;
865 Ok((resolved, plane))
866 });
867 match outcome {
868 Ok((resolved, plane)) => {
869 let label = annotation.label_world.unwrap_or(resolved.default_label);
870 PmiAnnotationReport {
871 persistent_data,
872 plugin_replay,
873 id: annotation.id().to_string(),
874 kind: annotation.kind.clone(),
875 enabled: annotation.enabled,
876 status: PmiStatus::Ok,
877 message: String::new(),
878 text: resolved.text,
879 value: resolved.value,
880 unit: resolved.unit.to_string(),
881 references: resolved.references,
882 label_world: plane.map_or(label, |plane| plane.project(label)),
883 plane,
884 geometry: resolved.geometry,
885 }
886 }
887 Err(message) => PmiAnnotationReport {
888 persistent_data: None,
889 plugin_replay: None,
890 id: annotation.id().to_string(),
891 kind: annotation.kind.clone(),
892 enabled: annotation.enabled,
893 status: PmiStatus::Error,
894 message,
895 text: String::new(),
896 value: None,
897 unit: String::new(),
898 references: Vec::new(),
899 label_world: annotation
902 .label_world
903 .or_else(|| (annotation.kind == "balloon").then(|| annotations::balloon::error_bubble(annotation, context)).flatten())
904 .unwrap_or([0.0; 3]),
905 plane: None,
906 geometry: PmiGeometry::None,
907 },
908 }
909}
910
911fn annotation_plane(
919 annotation: &PmiAnnotation,
920 context: &PmiContext<'_>,
921 camera: Option<&PmiCamera>,
922 geometry: &PmiGeometry,
923) -> Result<Option<PmiPlane>, String> {
924 use crate::SelectionGeometry;
925 use resolve::{a3, perpendicular_in_plane, v3};
926 let Some(name) = annotation.plane_ref() else {
927 return Ok(None);
928 };
929 let (origin, normal) = match resolve::resolve_reference(context.scene, name)? {
930 SelectionGeometry::Plane { origin, normal } => (origin, normal),
931 _ => return Err(format!("annotation plane '{name}' must be a planar face or a reference plane")),
932 };
933 let mut normal = normal
934 .normalized()
935 .map_err(|_| format!("annotation plane '{name}' has no normal"))?;
936 const PARALLEL: f64 = 1e-3;
938 match geometry {
939 PmiGeometry::Linear { a, b, .. } => {
940 let span = v3(*b).sub(v3(*a));
941 if span.length() > 1e-9 && span.normalized().map(|d| d.dot(normal).abs()).unwrap_or(0.0) > PARALLEL {
942 return Err(format!("annotation plane '{name}' is not parallel to the measured direction"));
943 }
944 }
945 PmiGeometry::Angular { axis, .. } => {
946 if v3(*axis).cross(normal).length() > PARALLEL {
947 return Err(format!("annotation plane '{name}' is not parallel to the angle's plane"));
948 }
949 }
950 PmiGeometry::Radial { axis, sphere: false, .. } => {
951 if v3(*axis).cross(normal).length() > PARALLEL {
952 return Err(format!("annotation plane '{name}' is not parallel to the circle's plane"));
953 }
954 }
955 _ => {}
956 }
957 let x_axis = match camera {
958 Some(camera) => {
959 let view = v3(camera.view_direction());
960 if normal.dot(view) > 0.0 {
961 normal = normal.scale(-1.0);
962 }
963 perpendicular_in_plane(normal, view.cross(v3(camera.up)))
964 }
965 None => perpendicular_in_plane(normal, crate::Vec3::new(1.0, 0.0, 0.0)),
966 };
967 Ok(Some(PmiPlane {
968 origin: a3(origin),
969 normal: a3(normal),
970 x_axis: a3(x_axis),
971 }))
972}
973