1use geop_core_editor::output::{Answer, Block, Bounded, Row};
7use geop_core_editor::view::drawn;
8use geop_core_editor::{
9 editing::{Shape as Drawn, Style, Tone, Visual},
10 selectable::Selectable,
11};
12use geop_core_geometry::shape::{Arc, Axis, Circle};
13use geop_core_math::{
14 geop_error::{GeopError, GeopResult, WithContext},
15 primitives::{CoordinateSystem, Pose},
16 scalars::Scalar,
17 vector::Vector3,
18};
19use geop_core_program::{element::PickedElement, reader::Reader};
20use geop_core_topology::distance::{Feature, PlacedFace, closest_points};
21use geop_ops_model::parameters::located;
22use geop_ops_model::selection::{Selected, Shape};
23use serde::Serialize;
24
25pub const PICKS: &[&str] = &[
28 drawn::VERTEX,
29 drawn::EDGE,
30 drawn::FACE,
31 drawn::DATUM,
32 drawn::SKETCH_POINT,
33 drawn::SKETCH_CURVE,
34];
35
36#[derive(Clone, Debug, PartialEq, Serialize)]
38pub struct Measured {
39 pub label: String,
40 #[serde(flatten)]
41 pub value: Bounded,
42 pub unit: &'static str,
44}
45
46#[derive(Clone, Debug, Serialize)]
48pub struct Measurement {
49 pub entities: Vec<PickedElement>,
51 pub values: Vec<Measured>,
52 pub witness: Option<[Vector3; 2]>,
55 pub plane: Option<CoordinateSystem>,
58 pub error: Option<String>,
60}
61
62impl Measurement {
63 pub fn shown(&self) -> Answer {
67 let mut blocks = Vec::new();
68 if self.entities.is_empty() {
69 blocks.push(Block::Text {
70 text: "Click a vertex, edge, face or datum; a second one to measure between them."
71 .into(),
72 tone: Tone::Hint,
73 });
74 } else {
75 blocks.push(Block::List {
76 items: (self.entities.iter())
77 .map(|e| Row {
78 light: vec![e.clone()],
79 ..Row::of(vec![label(e)])
80 })
81 .collect(),
82 });
83 }
84 blocks.extend(self.error.iter().map(|error| Block::Text {
85 text: error.clone(),
86 tone: Tone::Error,
87 }));
88 blocks.push(Block::Table {
89 head: Vec::new(),
90 rows: (self.values.iter())
91 .map(|v| Row::labelled(&v.label, vec![format!("{} {}", v.value.show(3), v.unit)]))
92 .collect(),
93 foot: Vec::new(),
94 });
95 Answer {
96 blocks,
97 plane: self.plane.clone(),
98 }
99 }
100
101 pub fn visuals(&self) -> Vec<Visual> {
104 let Some([a, b]) = self.witness else {
105 return Vec::new();
106 };
107 let mut visuals = vec![Visual::new(
108 "distance",
109 Drawn::Polyline { points: vec![a, b] },
110 Style::Selected,
111 )];
112 for (key, at) in [("from", a), ("to", b)] {
113 visuals.push(Visual::new(key, Drawn::Point { at }, Style::Selected));
114 }
115 if let Some(distance) = self.values.iter().find(|m| m.label == "Distance") {
116 visuals.push(Visual::new(
117 "distance-label",
118 Drawn::Label {
119 at: a.add(&b).prod_scalar(Scalar::from_f64(0.5)),
120 text: format!("{:.3} mm", distance.value.value),
121 offset: Vector3::zero(),
122 },
123 Style::Selected,
124 ));
125 }
126 visuals
127 }
128}
129
130fn measured(label: &str, value: Scalar, unit: &'static str) -> Measured {
131 Measured {
132 label: label.into(),
133 value: Bounded::of(value),
134 unit,
135 }
136}
137
138fn label(picked: &PickedElement) -> String {
141 match picked.instance() {
142 "" => picked.label(),
143 path => format!("{path}: {}", picked.label()),
144 }
145}
146
147struct Picked<'a> {
149 part: Reader<'a>,
151 pose: Option<Pose>,
153 element: Selected,
154}
155
156impl<'a> Picked<'a> {
157 fn of(reader: &Reader<'a>, picked: &PickedElement) -> GeopResult<Self> {
160 let (part, pose) = match picked.instance() {
161 "" => (reader.clone(), None),
162 path => (reader.placed_part(path)?, Some(located(reader, path)?)),
163 };
164 let inner = picked.local();
165 let element = (Selected::from_picked(&inner))
166 .or_else(|| Selected::select(&part, &inner))
167 .ok_or_else(|| {
168 GeopError::new(format!(
169 "{} cannot be measured: pick a vertex, an edge, a face, a datum, or a sketch's point or curve",
170 label(picked)
171 ))
172 })?;
173 Ok(Self {
174 part,
175 pose,
176 element,
177 })
178 }
179
180 fn shape(&self) -> GeopResult<Shape> {
182 let shape = self.element.shape(&self.part)?;
183 match &self.pose {
184 Some(pose) => placed(shape, pose),
185 None => Ok(shape),
186 }
187 }
188}
189
190fn placed(shape: Shape, pose: &Pose) -> GeopResult<Shape> {
193 let motion = pose.motion();
194 let axis = |a: Axis| Axis {
195 point: motion.apply(&a.point),
196 direction: motion.rotate(&a.direction),
197 };
198 let frame = |f: Option<CoordinateSystem>| f.map(|f| motion.apply_frame(&f)).transpose();
199 Ok(Shape {
200 point: shape.point.map(|p| motion.apply(&p)),
201 line: shape.line.map(axis),
202 plane: frame(shape.plane)?,
203 arc: shape.arc.map(|arc| Arc {
204 circle: Circle {
205 center: motion.apply(&arc.circle.center),
206 normal: motion.rotate(&arc.circle.normal),
207 radius: arc.circle.radius,
208 },
209 start: motion.apply(&arc.start),
210 end: motion.apply(&arc.end),
211 }),
212 round: shape.round.map(axis),
213 curve: shape.curve.map(|c| c.transform(&motion)),
214 frame: frame(shape.frame)?,
215 })
216}
217
218fn direction(shape: &Shape) -> Option<(Vector3, bool)> {
221 match (&shape.line, &shape.plane) {
222 (Some(line), _) => Some((line.direction, false)),
223 (None, Some(plane)) => Some((*plane.w(), true)),
224 _ => None,
225 }
226}
227
228pub fn measure(reader: &Reader, picked: &[PickedElement]) -> Measurement {
232 let mut out = Measurement {
233 entities: picked.to_vec(),
234 values: Vec::new(),
235 witness: None,
236 plane: None,
237 error: None,
238 };
239 let result = match picked {
240 [] => Ok(()),
241 [one] => alone(reader, one, &mut out),
242 [a, b] => together(reader, a, b, &mut out),
243 _ => Err(GeopError::new(format!(
244 "{} entities are picked: a measurement takes one or two",
245 picked.len()
246 ))),
247 };
248 if let Err(e) = result {
249 out.error = Some(e.to_string());
250 }
251 out
252}
253
254fn alone(reader: &Reader, picked: &PickedElement, out: &mut Measurement) -> GeopResult<()> {
258 let ctx = |e: GeopError| e.with_context(format!("measuring {}", label(picked)));
259 let entity = Picked::of(reader, picked).with_context(&ctx)?;
260 let shape = entity.shape().with_context(&ctx)?;
261 out.plane = shape.plane.clone();
262 if let Some(curve) = &shape.curve {
263 out.values.push(measured(
264 "Length",
265 curve.length().with_context(&ctx)?.0,
266 "mm",
267 ));
268 }
269 if let Selected::Face(face) = &entity.element {
270 let face = entity.part.retrieve_element(face).with_context(&ctx)?;
271 let topology = face.model.topology();
272 let (area, _) = topology.face_area(face.id).with_context(&ctx)?;
273 out.values.push(measured("Area", area, "mm²"));
274 let surface = &topology.get_face(face.id)?.surface;
275 if let Some(radius) = (surface.as_round_wall().with_context(&ctx)?)
276 .filter(|w| w.taper.could_be_equal(Scalar::ZERO))
277 .map(|w| w.radius)
278 {
279 out.values.push(measured("Radius", radius, "mm"));
280 out.values
281 .push(measured("Diameter", radius.mul(Scalar::TWO), "mm"));
282 }
283 }
284 if let Some(arc) = &shape.arc {
285 out.values.push(measured("Radius", arc.circle.radius, "mm"));
286 out.values.push(measured(
287 "Diameter",
288 arc.circle.radius.mul(Scalar::TWO),
289 "mm",
290 ));
291 }
292 if let Some(p) = shape.point {
293 for (k, axis) in ["X", "Y", "Z"].into_iter().enumerate() {
294 out.values.push(measured(axis, p[k], "mm"));
295 }
296 }
297 Ok(())
298}
299
300fn together(
304 reader: &Reader,
305 a: &PickedElement,
306 b: &PickedElement,
307 out: &mut Measurement,
308) -> GeopResult<()> {
309 let ctx = |e: GeopError| e.with_context(format!("measuring {} and {}", label(a), label(b)));
310 let (pa, pb) = (
311 Picked::of(reader, a).with_context(&ctx)?,
312 Picked::of(reader, b).with_context(&ctx)?,
313 );
314 let (sa, sb) = (
315 pa.shape().with_context(&ctx)?,
316 pb.shape().with_context(&ctx)?,
317 );
318 let (fa, fb) = (feature(&pa, &sa), feature(&pb, &sb));
319 match (fa, fb) {
320 (Ok(fa), Ok(fb)) => {
321 let closest = closest_points(&fa, &fb).with_context(&ctx)?;
322 out.values
323 .push(measured("Distance", closest.distance, "mm"));
324 let delta = closest.b.sub(&closest.a);
325 for (k, axis) in ["dX", "dY", "dZ"].into_iter().enumerate() {
326 out.values.push(measured(axis, delta[k].abs(), "mm"));
327 }
328 out.witness = Some([closest.a, closest.b]);
329 }
330 (Err(e), _) | (_, Err(e)) if direction(&sa).is_none() || direction(&sb).is_none() => {
331 return Err(ctx(e));
332 }
333 _ => {}
334 }
335 if let (Some(da), Some(db)) = (direction(&sa), direction(&sb)) {
336 out.values.push(measured("Angle", angle(&da, &db)?, "°"));
337 }
338 if let (Some(ca), Some(cb)) = (&sa.arc, &sb.arc) {
339 let between = cb.circle.center.sub(&ca.circle.center).norm();
340 out.values.push(measured("Centre distance", between, "mm"));
341 }
342 Ok(())
343}
344
345fn feature<'m>(entity: &Picked<'m>, shape: &Shape) -> GeopResult<Feature<'m>> {
349 let ctx = |e: GeopError| {
350 e.with_context(format!(
351 "measuring a distance to {}",
352 entity.element.picked().label()
353 ))
354 };
355 if let Selected::Face(face) = &entity.element {
356 let face = entity.part.retrieve_element(face).with_context(&ctx)?;
357 return PlacedFace::new(face.model.topology(), face.id, entity.pose)
358 .map(Feature::Face)
359 .with_context(&ctx);
360 }
361 match (&shape.curve, shape.point) {
362 (Some(curve), _) => Ok(Feature::Curve(curve.clone())),
363 (None, Some(point)) => Ok(Feature::Point(point)),
364 (None, None) => Err(ctx(GeopError::new(
365 "a distance is measured between vertices, points, curves and faces",
366 ))),
367 }
368}
369
370fn angle((a, a_normal): &(Vector3, bool), (b, b_normal): &(Vector3, bool)) -> GeopResult<Scalar> {
375 let (a, b) = (a.normalize()?, b.normalize()?);
376 let cos = a.prod_dot(&b).abs();
377 let sin = a.prod_cross(&b).norm();
378 let between = sin.atan2(cos).mul(Scalar::from_f64(180.0).div(Scalar::PI)?);
381 Ok(if a_normal == b_normal {
382 between
383 } else {
384 Scalar::from_f64(90.0).sub(between)
385 })
386}