Skip to main content

brep_kernel/feature_pipeline/pmi/annotations/
datum.rs

1//! Datum (`DTM`): a datum feature letter on a face or edge. Letters are
2//! unique per part (the kernel registry); a duplicate is an error on the
3//! later annotation.
4
5use super::{id_field, params, reference_field, schema_entry, string_field, PmiTypeDef};
6use crate::feature_pipeline::pmi::resolve::{a3, direction_of, resolve_reference};
7use crate::feature_pipeline::pmi::{PmiAnnotation, PmiContext, PmiGeometry, Resolved};
8use crate::feature_pipeline::SelectionProbe;
9use crate::Vec3;
10
11pub const DEF: PmiTypeDef = PmiTypeDef {
12    type_id: "datum",
13    short_name: "DTM",
14    icon: "\u{25B3}",
15    long_name: "\u{25B3} Datum",
16    label: "Datum",
17    applicable,
18    schema,
19    resolve,
20};
21
22/// Exactly one face or edge.
23fn applicable(probe: &SelectionProbe) -> bool {
24    probe.faces + probe.edges == 1
25        && probe.vertices == 0
26        && probe.planes == 0
27        && probe.solids == 0
28        && probe.sketches == 0
29}
30
31fn schema() -> serde_json::Value {
32    schema_entry(
33        &DEF,
34        params(vec![
35            ("id", id_field()),
36            ("target", reference_field("Target", &["FACE", "EDGE"], false, 1, 1, "The datum feature")),
37            ("letter", string_field("Letter", "", "The datum identifier (A–Z, skipping I, O, Q); assigned automatically when left empty at creation")),
38        ]),
39    )
40}
41
42/// Validate a datum letter: 1–2 letters A–Z, never I / O / Q.
43pub fn valid_letter(letter: &str) -> bool {
44    !letter.is_empty()
45        && letter.len() <= 2
46        && letter
47            .chars()
48            .all(|c| c.is_ascii_uppercase() && !matches!(c, 'I' | 'O' | 'Q'))
49}
50
51fn resolve(annotation: &PmiAnnotation, context: &PmiContext<'_>) -> Result<Resolved, String> {
52    let targets = annotation.references("target");
53    let Some(target) = targets.first() else {
54        return Err("select the datum feature (a face or an edge)".into());
55    };
56    let letter = annotation.text("letter").to_ascii_uppercase();
57    if !valid_letter(&letter) {
58        return Err(format!("'{letter}' is not a datum letter (A–Z, skipping I, O and Q)"));
59    }
60    if let Some(owner) = context.datums.get(&letter) {
61        if owner != annotation.id() {
62            return Err(format!("datum letter {letter} is already used by {owner}"));
63        }
64    }
65    let geometry = resolve_reference(context.scene, target)?;
66    let anchor = geometry.representative_point();
67    let normal = direction_of(&geometry).unwrap_or(Vec3::new(0.0, 0.0, 1.0));
68    let default_label = anchor.add(normal.scale(4.0)).add(Vec3::new(2.0, 2.0, 0.0));
69    Ok(Resolved {
70        text: letter.clone(),
71        value: None,
72        unit: "",
73        references: targets,
74        geometry: PmiGeometry::Datum {
75            anchor: a3(anchor),
76            normal: a3(normal),
77            letter,
78        },
79        default_label: a3(default_label),
80    })
81}