brep_kernel/feature_pipeline/pmi/annotations/
linear.rs1use 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
21fn 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 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}