Skip to main content

brep_kernel/feature_pipeline/pmi/annotations/
linear.rs

1//! Linear dimension (`DIM`): the distance between two elements, or the
2//! length of one straight edge, with an optional X / Y / Z component.
3
4use super::{decimals_of, dimension_fields, id_field, options_field, params, reference_field, schema_entry, PmiTypeDef};
5use crate::feature_pipeline::pmi::resolve::{a3, edge_endpoints, is_straight_edge, linear_between, perpendicular_in_plane, resolve_reference};
6use crate::feature_pipeline::pmi::{format_dimension, PmiAnnotation, PmiContext, PmiGeometry, Resolved, ToleranceBlock};
7use crate::feature_pipeline::SelectionProbe;
8use crate::Vec3;
9
10pub const DEF: PmiTypeDef = PmiTypeDef {
11    type_id: "linear",
12    short_name: "DIM",
13    icon: "\u{2194}",
14    long_name: "\u{2194} Linear dimension",
15    label: "Linear dimension",
16    applicable,
17    schema,
18    resolve,
19};
20
21/// One or two of faces / edges / vertices / planes, nothing else.
22fn applicable(probe: &SelectionProbe) -> bool {
23    let named = probe.faces + probe.edges + probe.vertices + probe.planes;
24    (1..=2).contains(&named) && probe.solids == 0 && probe.sketches == 0
25}
26
27fn schema() -> serde_json::Value {
28    let mut fields = params(vec![
29        ("id", id_field()),
30        (
31            "targets",
32            reference_field(
33                "Targets",
34                &["VERTEX", "EDGE", "FACE", "PLANE"],
35                true,
36                1,
37                2,
38                "Two elements to measure between, or one straight edge for its length",
39            ),
40        ),
41        (
42            "alignment",
43            options_field(
44                "Alignment",
45                &["free", "X", "Y", "Z"],
46                "free",
47                "free = the true distance; X / Y / Z = the distance component along that world axis",
48            ),
49        ),
50    ]);
51    dimension_fields(&mut fields, 3);
52    schema_entry(&DEF, fields)
53}
54
55fn resolve(annotation: &PmiAnnotation, context: &PmiContext<'_>) -> Result<Resolved, String> {
56    let targets = annotation.references("targets");
57    let (a, b) = match targets.as_slice() {
58        [] => return Err("select one straight edge or two elements".into()),
59        [single] => {
60            let geometry = resolve_reference(context.scene, single)?;
61            if !is_straight_edge(&geometry) {
62                return Err(format!(
63                    "'{single}' is not a straight edge — a single target must be one (pick two elements to measure between)"
64                ));
65            }
66            edge_endpoints(context.scene, single)?
67        }
68        [first, second] => {
69            let ga = resolve_reference(context.scene, first)?;
70            let gb = resolve_reference(context.scene, second)?;
71            linear_between(&ga, &gb)?
72        }
73        _ => return Err("a linear dimension takes at most two targets".into()),
74    };
75    let alignment = annotation.text("alignment").to_ascii_uppercase();
76    let (b, component) = match alignment.as_str() {
77        "X" => (Vec3::new(b.x, a.y, a.z), Some('X')),
78        "Y" => (Vec3::new(a.x, b.y, a.z), Some('Y')),
79        "Z" => (Vec3::new(a.x, a.y, b.z), Some('Z')),
80        _ => (b, None),
81    };
82    let value = b.sub(a).length();
83    let decimals = decimals_of(annotation, context, 3.0);
84    let tolerance = ToleranceBlock::read(annotation, context.env)?;
85    let text = format_dimension(value, decimals, &tolerance, annotation.flag("isReference"), "", "");
86    // Default label: beside the measured span, offset a fraction of its length.
87    let span = b.sub(a);
88    let offset_dir = if span.length() > 1e-9 {
89        perpendicular_in_plane(span, Vec3::new(0.0, 1.0, 0.0))
90    } else {
91        Vec3::new(0.0, 1.0, 0.0)
92    };
93    let midpoint = a.add(b).scale(0.5);
94    let default_label = midpoint.add(offset_dir.scale(0.25 * value.max(1.0)));
95    Ok(Resolved {
96        text,
97        value: Some(value),
98        unit: "mm",
99        references: targets,
100        geometry: PmiGeometry::Linear {
101            a: a3(a),
102            b: a3(b),
103            component,
104        },
105        default_label: a3(default_label),
106    })
107}