brep_kernel/feature_pipeline/pmi/annotations/
angle.rs1use super::{boolean_field, decimals_of, dimension_fields, id_field, options_field, params, reference_field, schema_entry, PmiTypeDef};
5use crate::feature_pipeline::pmi::resolve::{a3, class_of, direction_of, resolve_reference};
6use crate::feature_pipeline::pmi::{format_dimension, PmiAnnotation, PmiContext, PmiGeometry, Resolved, ToleranceBlock};
7use crate::feature_pipeline::SelectionProbe;
8use crate::{SelectionGeometry, Vec3};
9
10pub const DEF: PmiTypeDef = PmiTypeDef {
11 type_id: "angle",
12 short_name: "ANG",
13 icon: "\u{2220}",
14 long_name: "\u{2220} Angle dimension",
15 label: "Angle dimension",
16 applicable,
17 schema,
18 resolve,
19};
20
21fn applicable(probe: &SelectionProbe) -> bool {
23 probe.faces + probe.edges + probe.planes == 2
24 && probe.vertices == 0
25 && probe.solids == 0
26 && probe.sketches == 0
27}
28
29fn schema() -> serde_json::Value {
30 let mut fields = params(vec![
31 ("id", id_field()),
32 (
33 "targets",
34 reference_field(
35 "Targets",
36 &["FACE", "EDGE", "PLANE"],
37 true,
38 2,
39 2,
40 "Two planar faces, planes or straight edges",
41 ),
42 ),
43 (
44 "angleType",
45 options_field(
46 "Angle",
47 &["acute", "obtuse", "reflex"],
48 "acute",
49 "Which of the four angles between the elements to dimension",
50 ),
51 ),
52 (
53 "reverseElementOrder",
54 boolean_field("Reverse order", false, "Sweep the arc from the second element to the first"),
55 ),
56 ]);
57 dimension_fields(&mut fields, 1);
58 schema_entry(&DEF, fields)
59}
60
61fn arc_direction(geometry: &SelectionGeometry, axis: Vec3) -> Vec3 {
65 match geometry {
66 SelectionGeometry::Plane { normal, .. } => normal.cross(axis),
67 other => direction_of(other).unwrap_or(Vec3::new(1.0, 0.0, 0.0)),
68 }
69}
70
71fn vertex_of(first: &SelectionGeometry, second: &SelectionGeometry, axis: Vec3) -> Vec3 {
75 let origin = |g: &SelectionGeometry| g.representative_point();
76 match (first, second) {
77 (
78 SelectionGeometry::Plane {
79 origin: oa,
80 normal: na,
81 },
82 SelectionGeometry::Plane {
83 origin: ob,
84 normal: nb,
85 },
86 ) => {
87 let mid = oa.add(*ob).scale(0.5);
89 let na = *na;
90 let nb = *nb;
91 let da = oa.sub(mid).dot(na);
92 let db = ob.sub(mid).dot(nb);
93 let nn = na.dot(nb);
94 let det = 1.0 - nn * nn;
95 if det.abs() < 1e-12 {
96 return mid;
97 }
98 let ka = (da - db * nn) / det;
99 let kb = (db - da * nn) / det;
100 let point = mid.add(na.scale(ka)).add(nb.scale(kb));
101 let t = mid.sub(point).dot(axis);
103 point.add(axis.scale(t))
104 }
105 (SelectionGeometry::Plane { origin: po, normal }, line) | (line, SelectionGeometry::Plane { origin: po, normal }) => {
106 let lo = origin(line);
107 let ld = direction_of(line).unwrap_or(Vec3::new(1.0, 0.0, 0.0));
108 let denom = ld.dot(*normal);
109 if denom.abs() > 1e-9 {
110 let t = po.sub(lo).dot(*normal) / denom;
111 lo.add(ld.scale(t))
112 } else {
113 lo
114 }
115 }
116 (a, b) => {
117 let (oa, da) = (origin(a), direction_of(a).unwrap_or(Vec3::new(1.0, 0.0, 0.0)));
118 let (ob, db) = (origin(b), direction_of(b).unwrap_or(Vec3::new(0.0, 1.0, 0.0)));
119 let w = oa.sub(ob);
120 let aa = da.dot(da);
121 let bb = da.dot(db);
122 let cc = db.dot(db);
123 let dd = da.dot(w);
124 let ee = db.dot(w);
125 let denom = aa * cc - bb * bb;
126 if denom.abs() < 1e-12 {
127 return oa.add(ob).scale(0.5);
128 }
129 let s = (bb * ee - cc * dd) / denom;
130 let t = (aa * ee - bb * dd) / denom;
131 oa.add(da.scale(s)).add(ob.add(db.scale(t))).scale(0.5)
132 }
133 }
134}
135
136fn resolve(annotation: &PmiAnnotation, context: &PmiContext<'_>) -> Result<Resolved, String> {
137 let targets = annotation.references("targets");
138 let [first, second] = targets.as_slice() else {
139 return Err("select two planar faces, planes or straight edges".into());
140 };
141 let mut ga = resolve_reference(context.scene, first)?;
142 let mut gb = resolve_reference(context.scene, second)?;
143 if annotation.flag("reverseElementOrder") {
144 std::mem::swap(&mut ga, &mut gb);
145 }
146 for (name, geometry) in [(first, &ga), (second, &gb)] {
147 if direction_of(geometry).is_none() || matches!(geometry, SelectionGeometry::Circle { .. } | SelectionGeometry::Axis { .. }) {
148 return Err(format!(
149 "'{name}' is a {} — an angle needs planar faces, planes or straight edges",
150 class_of(geometry)
151 ));
152 }
153 }
154 let na = direction_of(&ga).expect("checked");
155 let nb = direction_of(&gb).expect("checked");
156 let axis = na.cross(nb);
157 if axis.length() < 1e-7 {
158 return Err("the two elements are parallel — there is no angle to dimension".into());
159 }
160 let axis = axis.normalized().map_err(|e| e.to_string())?;
161 let vertex = vertex_of(&ga, &gb, axis);
162 let u = arc_direction(&ga, axis).normalized().map_err(|e| e.to_string())?;
163 let v = arc_direction(&gb, axis).normalized().map_err(|e| e.to_string())?;
164 let mut sweep = u.cross(v).dot(axis).atan2(u.dot(v)).to_degrees();
166 if sweep < 0.0 {
167 sweep += 360.0;
168 }
169 let interior = if sweep > 180.0 { 360.0 - sweep } else { sweep };
170 let (dir_a, dir_b, degrees) = match annotation.text("angleType").to_ascii_lowercase().as_str() {
171 "obtuse" => {
172 if interior >= 90.0 {
173 (u, v, sweep.min(360.0 - sweep))
174 } else {
175 (u, v.scale(-1.0), 180.0 - interior)
176 }
177 }
178 "reflex" => {
179 let acute = if interior <= 90.0 { (u, v) } else { (u, v.scale(-1.0)) };
180 let inner = if interior <= 90.0 { interior } else { 180.0 - interior };
181 (acute.0, acute.1, 360.0 - (180.0 - inner))
182 }
183 _ => {
184 if interior <= 90.0 {
185 (u, v, interior)
186 } else {
187 (u, v.scale(-1.0), 180.0 - interior)
188 }
189 }
190 };
191 let signed = dir_a.cross(dir_b).dot(axis).atan2(dir_a.dot(dir_b)).to_degrees();
193 let axis = if signed < 0.0 { axis.scale(-1.0) } else { axis };
194 let decimals = decimals_of(annotation, context, 1.0);
195 let tolerance = ToleranceBlock::read(annotation, context.env)?;
196 let text = format_dimension(degrees, decimals, &tolerance, annotation.flag("isReference"), "", "\u{00B0}");
197 let half = (degrees * 0.5).to_radians();
199 let bisector = rotate(dir_a, axis, half);
200 let scale = ga.representative_point().sub(vertex).length().max(gb.representative_point().sub(vertex).length()).max(1.0);
201 let default_label = vertex.add(bisector.scale(scale * 0.6));
202 Ok(Resolved {
203 text,
204 value: Some(degrees),
205 unit: "deg",
206 references: targets,
207 geometry: PmiGeometry::Angular {
208 vertex: a3(vertex),
209 dir_a: a3(dir_a),
210 dir_b: a3(dir_b),
211 axis: a3(axis),
212 degrees,
213 },
214 default_label: a3(default_label),
215 })
216}
217
218pub fn rotate(v: Vec3, axis: Vec3, angle: f64) -> Vec3 {
220 v.rotate_about(axis, angle)
221}