Skip to main content

geop_ops_model/
geometry.rs

1//! Geometric kinds of element: what an entity can be used as — a
2//! [`Point`], a [`Line`], a [`Plane`], something [`Round`], a [`Circle`],
3//! a [`Curve`] — whatever target it is in. The model has them where its
4//! entities have that shape: a straight edge is a line, a flat face a
5//! plane, a circular edge a circle and something round. Other targets — the
6//! datums, a sketch, a placed part — have them too, each in its own crate,
7//! and an operation that takes several of them holds one of them
8//! ([`geop_core_editor::one_of!`]).
9//!
10//! Each is picked from what an editor draws of the model, if that has the
11//! shape: a line by a straight edge, a plane by a flat face.
12//!
13//! The model also has [`Body`]: a solid by its name, or a sheet — faces
14//! standing on their own, which have no name as a whole — by one of its
15//! faces.
16
17use geop_core_editor::selectable::SelectableElement;
18use geop_core_geometry::{
19    nurb_curve::NurbCurve3D,
20    shape::{Arc, Axis},
21};
22use geop_core_math::{
23    geop_error::{GeopError, GeopResult},
24    primitives::CoordinateSystem,
25    vector::Vector3,
26};
27use geop_core_program::{
28    element::{ElementReference, HasElements, Kind, PickedElement},
29    reader::Reader,
30};
31use geop_core_topology::{Body as TopologyBody, EdgeId, FaceId, Sense};
32
33use crate::{
34    NamedModel, RefId,
35    curves::Chain,
36    elements::{Edge, Face, Vertex, drawn_entity},
37    model, read_model,
38};
39
40/// The name the part's model is a target by.
41const MODEL: &str = "model";
42
43/// A point: a vertex, a datum point, a sketch's point.
44pub struct Point;
45
46/// A line: a straight edge, a datum axis, a sketch's line.
47pub struct Line;
48
49/// A plane, as a frame with `w` its normal and `u`/`v` a sketch's `x`/`y`
50/// on it: a flat face, a datum plane, a sketch's plane.
51pub struct Plane;
52
53/// The axis something turns around: a circular edge, a face of
54/// revolution — a cylinder, a cone — a sketch's circle or arc.
55pub struct Round;
56
57/// A circle or an arc of one: a circular edge, a sketch's circle or arc.
58pub struct Circle;
59
60/// A curve, whatever its shape, as a chain of one curve named after it and
61/// its ends: an edge — of a face or of a wire — a sketch's curve.
62pub struct Curve;
63
64/// Curves to run along, in order, as a chain named after what they are: an
65/// edge of a face, a sketch's one chain or loop, a 3-D sketch's.
66pub struct Path;
67
68/// A body of the model: a solid, or a sheet of faces standing on their
69/// own.
70pub struct Body;
71
72impl Kind for Point {
73    const NAME: &'static str = "point";
74}
75
76impl Kind for Line {
77    const NAME: &'static str = "line";
78}
79
80impl Kind for Plane {
81    const NAME: &'static str = "plane";
82}
83
84impl Kind for Round {
85    const NAME: &'static str = "round";
86}
87
88impl Kind for Circle {
89    const NAME: &'static str = "circle";
90}
91
92impl Kind for Curve {
93    const NAME: &'static str = "curve";
94}
95
96impl Kind for Path {
97    const NAME: &'static str = "path";
98}
99
100impl Kind for Body {
101    const NAME: &'static str = "body";
102}
103
104/// A right-handed orthonormal frame at `origin` with `w` along `normal`,
105/// and `u` the world axis most parallel to the plane normal to it,
106/// projected into that plane (see [`Vector3::across`]) — so frames with
107/// parallel normals line up, and a frame put on an axis has `u` where a
108/// joint measures its turns from: two bodies mated on the same axis of each
109/// are at a turn of zero.
110pub fn frame_along(origin: Vector3, normal: &Vector3) -> GeopResult<CoordinateSystem> {
111    let n = normal.normalize()?;
112    let [u, v] = n.across()?;
113    CoordinateSystem::try_new(origin, u, v, n)
114}
115
116impl NamedModel {
117    /// The edge `name`'s curve.
118    fn edge_curve(&self, name: &str) -> GeopResult<(EdgeId, &NurbCurve3D)> {
119        let id = self.edge_id(name)?;
120        Ok((id, &self.topology().get_edge(id)?.curve))
121    }
122
123    /// The plane of the flat face `face`, as a frame: normal pointing out of
124    /// its solid, origin the world origin's projection onto it and `u` the
125    /// world axis most parallel to it, so sketches on parallel faces line
126    /// up.
127    pub fn frame_of_face(&self, face: FaceId) -> GeopResult<CoordinateSystem> {
128        let plane = self.plane_of_face(face, "only a flat face is a plane")?;
129        frame_along(plane.project(&Vector3::zero()), &plane.normal)
130    }
131
132    /// The one curve `edge` is, as a chain named after it and its
133    /// vertices.
134    pub fn edge_chain(&self, edge: EdgeId) -> GeopResult<Chain<NurbCurve3D>> {
135        let name = self.name(edge)?.to_string();
136        let edge = self.topology().get_edge(edge)?;
137        Ok(Chain {
138            curves: vec![edge.curve.clone()],
139            curve_names: vec![name.clone()],
140            joint_names: vec![
141                self.name(edge.start_vertex)?.to_string(),
142                self.name(edge.end_vertex)?.to_string(),
143            ],
144            name,
145        })
146    }
147
148    /// The body `name` is: a solid's, or the one a face is part of.
149    pub fn body(&self, name: &str) -> GeopResult<TopologyBody> {
150        match self.id_of(name) {
151            Some(RefId::Solid(solid)) => Ok(TopologyBody::Solid(solid)),
152            Some(RefId::Face(face)) => self.topology().body_of_face(face),
153            _ => Err(GeopError::new(format!(
154                "{name:?} is no body: pick a solid or a face standing on its own"
155            ))),
156        }
157    }
158}
159
160impl HasElements<Point> for NamedModel {
161    type Element<'a> = Vector3;
162    const SINGLETON: Option<&'static str> = Some(MODEL);
163
164    fn element(&self, name: &str) -> GeopResult<Vector3> {
165        Ok(self.topology().get_vertex(self.vertex_id(name)?)?.point)
166    }
167}
168
169impl HasElements<Line> for NamedModel {
170    type Element<'a> = Axis;
171    const SINGLETON: Option<&'static str> = Some(MODEL);
172
173    fn element(&self, name: &str) -> GeopResult<Axis> {
174        let (_, curve) = self.edge_curve(name)?;
175        (curve.as_line()?).ok_or_else(|| GeopError::new(format!("edge {name:?} is not straight")))
176    }
177}
178
179impl HasElements<Plane> for NamedModel {
180    type Element<'a> = CoordinateSystem;
181    const SINGLETON: Option<&'static str> = Some(MODEL);
182
183    fn element(&self, name: &str) -> GeopResult<CoordinateSystem> {
184        self.frame_of_face(self.face_id(name)?)
185    }
186}
187
188impl HasElements<Round> for NamedModel {
189    type Element<'a> = Axis;
190    const SINGLETON: Option<&'static str> = Some(MODEL);
191
192    fn element(&self, name: &str) -> GeopResult<Axis> {
193        let axis = match self.id_of(name) {
194            Some(RefId::Edge(_)) => {
195                let arc = HasElements::<Circle>::element(self, name)?;
196                Some(arc.circle.axis())
197            }
198            Some(RefId::Face(face)) => {
199                (self.topology().get_face(face)?.surface).axis_of_revolution()?
200            }
201            _ => None,
202        };
203        axis.ok_or_else(|| {
204            GeopError::new(format!(
205                "{name:?} turns around no axis: pick a circular edge, or a cylindrical or conical face"
206            ))
207        })
208    }
209}
210
211impl HasElements<Circle> for NamedModel {
212    type Element<'a> = Arc;
213    const SINGLETON: Option<&'static str> = Some(MODEL);
214
215    fn element(&self, name: &str) -> GeopResult<Arc> {
216        let (_, curve) = self.edge_curve(name)?;
217        (curve.as_arc()?).ok_or_else(|| GeopError::new(format!("edge {name:?} is not circular")))
218    }
219}
220
221impl HasElements<Curve> for NamedModel {
222    type Element<'a> = Chain<NurbCurve3D>;
223    const SINGLETON: Option<&'static str> = Some(MODEL);
224
225    fn element(&self, name: &str) -> GeopResult<Chain<NurbCurve3D>> {
226        self.edge_chain(self.edge_id(name)?)
227    }
228}
229
230/// An edge of a face — of a solid or of a sheet — as the one curve it is;
231/// an edge of a wire as the run of the wire's edges it is in, from end to
232/// end or round (see [`geop_core_topology::Model::wire_chains`]), named
233/// after its edges and the vertices between them. (A 3-D sketch's wire is
234/// picked as the sketch, whose path is named after its elements.)
235impl HasElements<Path> for NamedModel {
236    type Element<'a> = Chain<NurbCurve3D>;
237    const SINGLETON: Option<&'static str> = Some(MODEL);
238
239    fn element(&self, name: &str) -> GeopResult<Chain<NurbCurve3D>> {
240        let edge = self.edge_id(name)?;
241        let topology = self.topology();
242        let Some(wire) = topology.wire_of_edge(edge) else {
243            return self.edge_chain(edge);
244        };
245        let chains = topology.wire_chains(wire)?;
246        let run = (chains.into_iter())
247            .find(|chain| chain.iter().any(|&(e, _)| e == edge))
248            .ok_or_else(|| GeopError::new(format!("edge {name:?} is in no run of its wire")))?;
249        let mut chain = Chain {
250            name: name.to_string(),
251            curves: Vec::new(),
252            curve_names: Vec::new(),
253            joint_names: Vec::new(),
254        };
255        let mut end = None;
256        for &(e, sense) in &run {
257            let e_name = self.name(e)?.to_string();
258            let e = topology.get_edge(e)?;
259            let (start, stop, curve) = match sense {
260                Sense::Forward => (e.start_vertex, e.end_vertex, e.curve.clone()),
261                Sense::Reversed => (e.end_vertex, e.start_vertex, e.curve.reverse()),
262            };
263            chain.curves.push(curve);
264            chain.curve_names.push(e_name);
265            chain.joint_names.push(self.name(start)?.to_string());
266            end = Some(stop);
267        }
268        let first = topology.get_edge(run[0].0)?;
269        let start = match run[0].1 {
270            Sense::Forward => first.start_vertex,
271            Sense::Reversed => first.end_vertex,
272        };
273        // An open run has a joint at its end too; a closed one ends where
274        // it starts.
275        match end {
276            Some(end) if end != start => chain.joint_names.push(self.name(end)?.to_string()),
277            _ => {}
278        }
279        Ok(chain)
280    }
281}
282
283impl HasElements<Body> for NamedModel {
284    type Element<'a> = TopologyBody;
285    const SINGLETON: Option<&'static str> = Some(MODEL);
286
287    fn element(&self, name: &str) -> GeopResult<TopologyBody> {
288        self.body(name)
289    }
290}
291
292/// Geometric kinds picked from what the editor draws of the model where
293/// that has their shape: the hit, if it resolves as one.
294macro_rules! picked_where_it_has_the_shape {
295    ($($kind:ty => [$($from:ty),+];)*) => {$(
296        impl SelectableElement<NamedModel> for $kind {
297            const PICKED_FROM: &'static [&'static str] = &[$(<$from as Kind>::NAME),+];
298
299            fn select(
300                reader: &Reader,
301                hit: &PickedElement,
302            ) -> Option<ElementReference<NamedModel, Self>> {
303                (hit.target == *model().location()
304                    && <Self as SelectableElement<NamedModel>>::PICKED_FROM
305                        .contains(&hit.kind.as_str()))
306                    .then_some(())?;
307                let reference = ElementReference::new(model(), &hit.name);
308                reader.retrieve_element(&reference).ok()?;
309                Some(reference)
310            }
311
312            fn drawn(
313                reader: &Reader,
314                element: &ElementReference<NamedModel, Self>,
315            ) -> Vec<PickedElement> {
316                (read_model(reader).ok())
317                    .map(|model| drawn_entity(&model, element.name()))
318                    .unwrap_or_default()
319            }
320        }
321    )*};
322}
323
324picked_where_it_has_the_shape! {
325    Point => [Vertex];
326    Line => [Edge];
327    Plane => [Face];
328    Round => [Edge, Face];
329    Circle => [Edge];
330    Curve => [Edge];
331    Path => [Edge];
332}
333
334/// A body is picked by a face of it: a solid's face selects the solid, a
335/// sheet's face itself; it is lit as the solid's faces, or the face.
336impl SelectableElement<NamedModel> for Body {
337    const PICKED_FROM: &'static [&'static str] = &[Face::NAME];
338
339    fn select(reader: &Reader, hit: &PickedElement) -> Option<ElementReference<NamedModel, Self>> {
340        let face = ElementReference::<NamedModel, Face>::from_picked(hit)?;
341        let face = reader.retrieve_element(&face).ok()?;
342        let name = match face.model.topology().body_of_face(face.id).ok()? {
343            TopologyBody::Solid(solid) => face.model.name_of(solid)?,
344            TopologyBody::Sheet(_) => &hit.name,
345        };
346        Some(ElementReference::new(model(), name))
347    }
348
349    fn drawn(reader: &Reader, body: &ElementReference<NamedModel, Self>) -> Vec<PickedElement> {
350        (read_model(reader).ok())
351            .map(|model| drawn_entity(&model, body.name()))
352            .unwrap_or_default()
353    }
354}
355
356/// The bodies `bodies` refer to, each once, in the order picked. None
357/// picked is refused.
358pub fn resolve_bodies(
359    reader: &Reader,
360    bodies: &[ElementReference<NamedModel, Body>],
361) -> GeopResult<Vec<TopologyBody>> {
362    if bodies.is_empty() {
363        return Err(GeopError::new("pick the bodies first"));
364    }
365    let mut resolved = Vec::new();
366    for body in bodies {
367        let body = reader.retrieve_element(body)?;
368        if !resolved.contains(&body) {
369            resolved.push(body);
370        }
371    }
372    Ok(resolved)
373}
374
375geop_core_editor::one_of! {
376    /// Curves a sweep runs along, a rail or a guide follows, or a pattern
377    /// lays its instances along: a sketch's or a 3-D sketch's one chain of
378    /// curves, or one loop, or an edge of a face.
379    pub enum PathRef -> Chain<NurbCurve3D> {
380        Sketch(ElementReference<crate::sketch::PlacedSketch, Path>),
381        Sketch3d(ElementReference<crate::sketch3d::PlacedSketch3d, Path>),
382        Edge(ElementReference<NamedModel, Path>),
383    }
384}
385
386impl PathRef {
387    /// Whether it is the sketch `sketch`, as a path.
388    pub fn is_sketch(&self, sketch: &crate::sketch::SketchRef) -> bool {
389        matches!(self, PathRef::Sketch(path) if path.target() == sketch.target())
390    }
391}
392
393#[cfg(test)]
394mod tests {
395    use geop_core_editor::selectable::{Selectable, selected};
396    use geop_core_math::scalars::Scalar;
397    use geop_core_program::part::Part;
398
399    use super::*;
400    use crate::testing::{given_part, unit_cube};
401
402    fn hit(kind: &str, name: &str) -> PickedElement {
403        PickedElement {
404            target: model().location().clone(),
405            kind: kind.to_string(),
406            name: name.to_string(),
407        }
408    }
409
410    /// What a cube's entities are used as: its vertices points, its edges
411    /// lines and curves, its faces planes; nothing of it is round.
412    #[test]
413    fn a_cube_has_points_lines_and_planes() {
414        let part: Part = given_part(&unit_cube()).unwrap();
415        let reader = part.current();
416        let point = ElementReference::<NamedModel, Point>::new(model(), "v0");
417        let at = reader.retrieve_element(&point).unwrap();
418        assert!(
419            at.to_array()
420                .iter()
421                .all(|c| c.to_f64() == 0.0 || c.to_f64() == 1.0)
422        );
423
424        let line = ElementReference::<NamedModel, Line>::new(model(), "e0");
425        let axis = reader.retrieve_element(&line).unwrap();
426        assert!(axis.direction.norm().definitely_greater(Scalar::ZERO));
427        let plane = ElementReference::<NamedModel, Plane>::new(model(), "f0");
428        let frame = reader.retrieve_element(&plane).unwrap();
429        assert!(frame.w().norm().could_be_equal(Scalar::ONE));
430        let curve = ElementReference::<NamedModel, Curve>::new(model(), "e0");
431        let chain = reader.retrieve_element(&curve).unwrap();
432        assert_eq!(chain.curve_names, ["e0"]);
433        assert_eq!(chain.joint_names.len(), 2);
434
435        let round = ElementReference::<NamedModel, Round>::new(model(), "f0");
436        assert!(reader.retrieve_element(&round).is_err());
437        let wrong = ElementReference::<NamedModel, Line>::new(model(), "f0");
438        assert!(reader.retrieve_element(&wrong).is_err());
439    }
440
441    /// A line is picked by a straight edge, held by the edge's name and lit
442    /// as the edge; a click on what has not the shape selects nothing.
443    #[test]
444    fn a_geometric_kind_is_picked_where_the_model_has_its_shape() {
445        let part: Part = given_part(&unit_cube()).unwrap();
446        let reader = part.current();
447        let lines: Vec<ElementReference<NamedModel, Line>> =
448            selected(&reader, &[hit("edge", "e3"), hit("face", "f0")]);
449        assert_eq!(lines.len(), 1);
450        assert_eq!(lines[0].name(), "e3");
451        assert_eq!(lines[0].drawn(&reader), [hit("edge", "e3")]);
452        let rounds: Vec<ElementReference<NamedModel, Round>> =
453            selected(&reader, &[hit("edge", "e3"), hit("face", "f0")]);
454        assert!(rounds.is_empty());
455        assert_eq!(
456            <ElementReference<NamedModel, Round>>::picked_from(),
457            ["edge", "face"]
458        );
459    }
460
461    /// A face of a solid picks the solid as a body, lit as its faces; the
462    /// bodies picked resolve each once.
463    #[test]
464    fn a_body_is_picked_by_a_face_of_it() {
465        let part: Part = given_part(&unit_cube()).unwrap();
466        let reader = part.current();
467        let bodies: Vec<ElementReference<NamedModel, Body>> =
468            selected(&reader, &[hit("face", "f2")]);
469        assert_eq!(bodies.len(), 1);
470        assert_eq!(bodies[0].name(), "s0");
471        assert_eq!(bodies[0].drawn(&reader).len(), 6);
472        let twice = [bodies[0].clone(), ElementReference::new(model(), "f1")];
473        assert_eq!(resolve_bodies(&reader, &twice).unwrap().len(), 1);
474        assert!(resolve_bodies(&reader, &[]).is_err());
475    }
476}