Skip to main content

geop_ops_model/
mate_entity.rs

1//! [`MateEntity`]: what a mate holds by — an element of the part itself, or
2//! of a part placed in it, however deep — and the reference frame it gives
3//! where it is put ([`MateEntity::frame`]): where it sits, and which way
4//! its `z` axis runs.
5
6use crate::{
7    NamedModel, frame_along,
8    placed::locate,
9    selection::{Selected, Shape},
10};
11use geop_core_editor::editing::{Style, Visual};
12use geop_core_editor::selectable::Selectable;
13use geop_core_editor::view::{FramePick, drawn};
14use geop_core_geometry::{
15    nurb_curve::NurbCurve3D,
16    nurb_surface::NurbSurface3D,
17    search::{MAX_NODES, min_subdivision_size},
18    shape::Axis,
19};
20use geop_core_math::{
21    geop_error::{GeopError, GeopResult, WithContext},
22    primitives::CoordinateSystem,
23    scalars::Scalar,
24    vector::Vector3,
25    with_context,
26};
27use geop_core_program::{element::PickedElement, reader::Reader};
28use geop_core_topology::{CoedgeGeometry, EdgeId, FaceId};
29use serde::{Deserialize, Serialize};
30
31/// What a mate holds by: the element `on`, of the part itself or of a part
32/// placed in it, however deep (see [`geop_core_program::placed`]) — and,
33/// for a joint's frame, the element of the same part the frame sits at: a
34/// corner or a side of a face, an end of an edge (see
35/// [`MateEntity::frame`]). Saved as `{"on": {"face": {"target":
36/// "/bolt:model", "name": "extrude(head,end)"}}}`.
37#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
38pub struct MateEntity {
39    pub on: Selected,
40    #[serde(default, skip_serializing_if = "Option::is_none")]
41    pub at: Option<Selected>,
42}
43
44impl MateEntity {
45    /// A frame on `on`, sitting where nothing else says.
46    pub fn on(on: Selected) -> Self {
47        Self { on, at: None }
48    }
49
50    /// A frame on `on`, of the part placed at `part`, as that part names it.
51    pub fn of(part: &str, on: Selected) -> Self {
52        Self::on(on.behind(part))
53    }
54
55    /// It with its frame put at `at`, of the same part as what it is on.
56    pub fn at(self, at: Selected) -> Self {
57        let at = at.local().behind(self.part());
58        Self {
59            at: Some(at),
60            ..self
61        }
62    }
63
64    /// The path of the placed part it is of: empty for the part itself.
65    pub fn part(&self) -> &str {
66        self.on.instance()
67    }
68
69    /// How it reads in a list: `bolt: face extrude(head,end)`.
70    pub fn label(&self) -> String {
71        let on = self.on.local().picked().label();
72        match self.part() {
73            "" => on,
74            part => format!("{part}: {on}"),
75        }
76    }
77
78    /// What it is in its own part — the part `reader` reads, or the one
79    /// placed in it that it is of. Reads no state: where that part is
80    /// placed, [`locate`](crate::placed::locate) says.
81    pub fn shape(&self, reader: &Reader) -> GeopResult<Shape> {
82        Ok(self.on.shape_in_instance(reader)?.element)
83    }
84
85    /// The surface of the wall the face it is on is of, in its own part —
86    /// the part `reader` reads, or the one placed in it that it is of — its
87    /// normal out of its solid: the face's, and those of the faces it runs
88    /// on into smoothly, joined (see [`wall`]). A cam's side swept along a
89    /// profile is a face per piece of the profile, and a follower rolls
90    /// round all of them. None for any element but a face. Reads no state,
91    /// as [`MateEntity::shape`] does not.
92    pub fn surface(&self, reader: &Reader) -> GeopResult<Option<NurbSurface3D>> {
93        let Selected::Face(face) = self.on.local() else {
94            return Ok(None);
95        };
96        let ctx = with_context!("the surface of {}", self.label());
97        let inside = match self.part() {
98            "" => reader.clone(),
99            part => reader.placed_part(part).with_context(ctx)?,
100        };
101        let found = inside.retrieve_element(&face).with_context(ctx)?;
102        wall(found.model, found.id).map(Some).with_context(ctx)
103    }
104
105    /// The curve it runs along, in its own part — the part `reader` reads,
106    /// or the one placed in it that it is of: of an edge, the edge's and
107    /// those of the edges it runs on into smoothly at its ends, joined (see
108    /// [`run`]): a slot's rim is an edge per piece of its outline, and a pin
109    /// runs round all of them. A sketch's curve as it is; none for what is
110    /// no curve. Reads no state.
111    pub fn curve(&self, reader: &Reader) -> GeopResult<Option<NurbCurve3D>> {
112        let ctx = with_context!("the curve of {}", self.label());
113        let Selected::Edge(edge) = self.on.local() else {
114            return Ok(self.shape(reader).with_context(ctx)?.curve);
115        };
116        let inside = match self.part() {
117            "" => reader.clone(),
118            part => reader.placed_part(part).with_context(ctx)?,
119        };
120        let found = inside.retrieve_element(&edge).with_context(ctx)?;
121        run(found.model, found.id).map(Some).with_context(ctx)
122    }
123
124    /// The frame it gives in its own part — the part `reader` reads, or
125    /// the one placed in it that it is of. Reads no state: where that part
126    /// is placed, [`locate`](crate::placed::locate) says.
127    ///
128    /// Its `z` axis is the element's own, and what is turned about it, or
129    /// slid along:
130    ///
131    /// - A planar face: its normal, out of its solid. `at` is where it sits
132    ///   in the plane, else the middle of the face.
133    /// - A round face — a cylinder, a cone: its axis. `at` is where along
134    ///   it, as near as the axis is to it, else half way along the face.
135    /// - A circular edge: the axis of its circle, at its center.
136    /// - A straight edge: its direction. `at` is where along it — a vertex
137    ///   at one end, say — else the middle.
138    /// - A point: the world's axes, there.
139    /// - A datum — a frame, an axis, a plane: its own.
140    ///
141    /// Where the frame sits is a free choice, so it is sharp.
142    pub fn frame(&self, reader: &Reader) -> GeopResult<CoordinateSystem> {
143        let ctx = with_context!("the frame on {}", self.label());
144        let inside = match self.part() {
145            "" => reader.clone(),
146            part => reader.placed_part(part).with_context(ctx)?,
147        };
148        let at = self.at.as_ref().map(Selected::local);
149        frame_on(&inside, &self.on.local(), at.as_ref()).with_context(ctx)
150    }
151
152    /// The frame it gives, where the part it is of is placed in the part
153    /// `reader` reads — in the world, as the part is drawn.
154    pub fn frame_in_world(&self, reader: &Reader) -> GeopResult<CoordinateSystem> {
155        let frame = self.frame(reader)?;
156        match self.part() {
157            "" => Ok(frame),
158            part => (locate(reader, part)?.world.motion())
159                .apply_frame(&frame)
160                .with_context(with_context!("the frame on {}", self.label())),
161        }
162    }
163}
164
165/// The surface of `face` of `named`, and of every face it runs on into
166/// across an edge — and they on — where one surface carries on from the
167/// other's side, tangent to it: joined into one along the way they run
168/// (see [`NurbSurface3D::joined`]). The faces are taken in the order of
169/// their ids, which a model gives in creation order.
170fn wall(named: &NamedModel, face: FaceId) -> GeopResult<NurbSurface3D> {
171    let model = named.topology();
172    let mut wall = model.get_face(face)?.surface.clone();
173    let mut used = vec![face];
174    let mut along = None;
175    loop {
176        let mut next: Vec<FaceId> = (used.iter())
177            .flat_map(|&f| model.face_edges(f).collect::<Vec<_>>())
178            .filter_map(|edge| model.faces_of_edge(edge))
179            .flatten()
180            .filter(|f| !used.contains(f))
181            .collect();
182        next.sort();
183        next.dedup();
184        let joined = next.iter().find_map(|&f| {
185            let surface = &model.get_face(f).ok()?.surface;
186            let ways = along.map_or(vec![true, false], |along| vec![along]);
187            ways.into_iter().find_map(|way| {
188                let after = (runs_on(&wall, surface, way))
189                    .then(|| wall.joined(surface, way).ok())
190                    .flatten();
191                let before = || {
192                    (runs_on(surface, &wall, way))
193                        .then(|| surface.joined(&wall, way).ok())
194                        .flatten()
195                };
196                Some((f, way, after.or_else(before)?))
197            })
198        });
199        let Some((f, way, joined)) = joined else {
200            return Ok(wall);
201        };
202        (wall, along) = (joined, Some(way));
203        used.push(f);
204    }
205}
206
207/// The curve of `edge` of `named`, and of every edge it runs on into at
208/// either end — and they on — tangent to it: joined into one (see
209/// [`NurbCurve3D::joined`]), each turned to run on the way the run goes.
210/// Edges are taken in the order of their ids.
211fn run(named: &NamedModel, edge: EdgeId) -> GeopResult<NurbCurve3D> {
212    let model = named.topology();
213    let first = model.get_edge(edge)?;
214    let mut run = first.curve.clone();
215    let mut ends = [first.start_vertex, first.end_vertex];
216    let mut used = vec![edge];
217    loop {
218        let mut grown = false;
219        for at_end in [true, false] {
220            let vertex = ends[usize::from(at_end)];
221            let mut next = model.edges_at_vertex(vertex);
222            next.retain(|e| !used.contains(e));
223            next.sort();
224            for e in next {
225                let found = model.get_edge(e)?;
226                // Turned to leave the vertex, or to arrive there.
227                let leaves = found.start_vertex == vertex;
228                let curve = match leaves == at_end {
229                    true => found.curve.clone(),
230                    false => found.curve.reverse(),
231                };
232                let far = if leaves {
233                    found.end_vertex
234                } else {
235                    found.start_vertex
236                };
237                let joined = match at_end {
238                    true => tangent(&run, &curve).then(|| run.joined(&curve).ok()),
239                    false => tangent(&curve, &run).then(|| curve.joined(&run).ok()),
240                };
241                if let Some(Some(joined)) = joined {
242                    run = joined;
243                    ends[usize::from(at_end)] = far;
244                    used.push(e);
245                    grown = true;
246                    break;
247                }
248            }
249        }
250        // Closed round: back where it started.
251        if !grown || ends[0] == ends[1] {
252            return Ok(run);
253        }
254    }
255}
256
257/// Whether `next` carries on from where `curve` ends, tangent to it: the
258/// step from its first control point to its second along the step from
259/// `curve`'s last but one to its last, as far as their enclosures can tell.
260fn tangent(curve: &NurbCurve3D, next: &NurbCurve3D) -> bool {
261    let point = |p: &geop_core_math::vector::Vector<4>| {
262        Vector3::from_array([0, 1, 2].map(|k| p[k].div(p[3]).unwrap_or(Scalar::ZERO)))
263    };
264    let (a, b) = (&curve.control_points, &next.control_points);
265    if a.len() < 2 || b.len() < 2 {
266        return false;
267    }
268    let (end, before) = (point(&a[a.len() - 1]), point(&a[a.len() - 2]));
269    let (start, after) = (point(&b[0]), point(&b[1]));
270    let (into, out) = (end.sub(&before), after.sub(&start));
271    end.could_be_equal(&start)
272        && into.prod_cross(&out).norm_sq().could_be_equal(Scalar::ZERO)
273        && into.prod_dot(&out).definitely_greater(Scalar::ZERO)
274}
275
276/// Whether `next` carries on from where `surface` ends along `u` — along
277/// `v`, if not `along_u` — tangent to it: at every control point of the row
278/// they share, the step to the next one on is along the step from the one
279/// before, as far as the points' enclosures can tell.
280fn runs_on(surface: &NurbSurface3D, next: &NurbSurface3D, along_u: bool) -> bool {
281    let point = |s: &NurbSurface3D, i: usize, j: usize| {
282        let p = match along_u {
283            true => s.control_points[i * s.num_v + j],
284            false => s.control_points[j * s.num_v + i],
285        };
286        let w = p[3];
287        Vector3::from_array([0, 1, 2].map(|k| p[k].div(w).unwrap_or(Scalar::ZERO)))
288    };
289    let count = |s: &NurbSurface3D| match along_u {
290        true => (s.num_u, s.num_v),
291        false => (s.num_v, s.num_u),
292    };
293    let ((n, m), (n2, m2)) = (count(surface), count(next));
294    if m != m2 || n < 2 || n2 < 2 {
295        return false;
296    }
297    (0..m).all(|j| {
298        let (end, before) = (point(surface, n - 1, j), point(surface, n - 2, j));
299        let (start, after) = (point(next, 0, j), point(next, 1, j));
300        let (into, out) = (end.sub(&before), after.sub(&start));
301        end.could_be_equal(&start)
302            && into.prod_cross(&out).norm_sq().could_be_equal(Scalar::ZERO)
303            && into.prod_dot(&out).definitely_greater(Scalar::ZERO)
304    })
305}
306
307/// The frame `on` gives in the part `reader` reads, put at `at` (see
308/// [`MateEntity::frame`]).
309fn frame_on(reader: &Reader, on: &Selected, at: Option<&Selected>) -> GeopResult<CoordinateSystem> {
310    let shape = on.shape(reader)?;
311    if let Some(frame) = shape.frame {
312        return Ok(frame);
313    }
314    if let Some(arc) = &shape.arc {
315        return frame_along(arc.circle.center.sharpen(), &arc.circle.normal);
316    }
317    let anchor = at.map(|at| middle_of(reader, at)).transpose()?;
318    // Where on `on` the frame sits, unless `at` says: only worked out where
319    // needed, since not every element has a middle.
320    let near = || match anchor {
321        Some(anchor) => Ok(anchor),
322        None => middle_of(reader, on),
323    };
324    let along = |axis: &Axis| -> GeopResult<CoordinateSystem> {
325        frame_along(axis.project(&near()?).sharpen(), &axis.direction)
326    };
327    if let Some(plane) = &shape.plane {
328        let near = match (anchor, on) {
329            (Some(anchor), _) => anchor,
330            (None, Selected::Face(_)) => middle_of(reader, on)?,
331            (None, _) => *plane.origin(),
332        };
333        let off = near.sub(plane.origin()).prod_dot(plane.w());
334        let origin = near.sub(&plane.w().prod_scalar(off)).sharpen();
335        return CoordinateSystem::try_new(origin, *plane.u(), *plane.v(), *plane.w());
336    }
337    if let Some(axis) = &shape.round {
338        return along(axis);
339    }
340    if let Some(axis) = &shape.line {
341        return along(axis);
342    }
343    if let Some(point) = shape.point {
344        return Ok(CoordinateSystem::world_at(point));
345    }
346    Err(GeopError::new(
347        "it gives no frame: pick a face, an edge, a point or a datum",
348    ))
349}
350
351/// Where an element is in the middle of itself, in the part `reader`
352/// reads: a vertex's point, the center of a circular edge, the middle of
353/// any other edge — of its parameter, not its length — and of a face the
354/// middle of the box round its edges, in its own surface. Where a frame
355/// sits when it sits nowhere else: a free choice, so sharp.
356fn middle_of(reader: &Reader, element: &Selected) -> GeopResult<Vector3> {
357    let ctx = with_context!("the middle of {}", element.picked().label());
358    if let Selected::Face(face) = element {
359        let found = reader.retrieve_element(face).with_context(ctx)?;
360        return face_middle(found.model, found.id).with_context(ctx);
361    }
362    let shape = element.shape(reader).with_context(ctx)?;
363    if let Some(arc) = &shape.arc {
364        return Ok(arc.circle.center.sharpen());
365    }
366    if let Some(curve) = &shape.curve {
367        let t = curve.mid_parameter().with_context(ctx)?;
368        return Ok(curve.evaluate(t).with_context(ctx)?.sharpen());
369    }
370    (shape.point.map(|p| p.sharpen()))
371        .ok_or_else(|| GeopError::new("it has no middle"))
372        .with_context(ctx)
373}
374
375/// The middle of the box round the edges of `face` — each edge's own
376/// extremes, not its control points' — or the point of a bare vertex face.
377/// In the part's own frame.
378fn face_middle(named: &NamedModel, face: FaceId) -> GeopResult<Vector3> {
379    let model = named.topology();
380    let mut bounds: Option<[Scalar; 3]> = None;
381    for coedge in model.iterate_face_coedges(face) {
382        let spans = match model.get_coedge(coedge)?.geometry {
383            CoedgeGeometry::Edge(edge) => model
384                .get_edge(edge)?
385                .curve
386                .bounds(MAX_NODES, min_subdivision_size())?,
387            CoedgeGeometry::Vertex(vertex) => model.get_vertex(vertex)?.point.to_array(),
388        };
389        bounds = Some(match bounds {
390            Some(b) => std::array::from_fn(|k| b[k].union(spans[k])),
391            None => spans,
392        });
393    }
394    let bounds =
395        bounds.ok_or_else(|| GeopError::new(format!("face {face} has no edge to measure")))?;
396    Ok(Vector3::from_array(bounds.map(|axis| axis.midpoint())))
397}
398
399/// A mate's element is picked from what the editor draws of the part and
400/// the parts placed in it; a frame, with where it sits (see
401/// [`drawn::FRAME`]). Its element is held as the pick names it, of the
402/// placed part behind its path.
403impl Selectable for MateEntity {
404    fn picked_from() -> Vec<&'static str> {
405        let mut kinds = Selected::picked_from();
406        kinds.push(drawn::FRAME);
407        kinds
408    }
409
410    fn from_picked(picked: &PickedElement) -> Option<Self> {
411        if let Some(frame) = FramePick::of(picked) {
412            let at = match &frame.at {
413                Some(at) => Some(Selected::from_picked(at)?),
414                None => None,
415            };
416            return Some(MateEntity {
417                on: Selected::from_picked(&frame.on)?,
418                at,
419            });
420        }
421        Selected::from_picked(picked).map(MateEntity::on)
422    }
423
424    fn picked(&self) -> PickedElement {
425        let on = self.on.picked();
426        match &self.at {
427            None => on,
428            Some(at) => FramePick {
429                on,
430                at: Some(at.picked()),
431            }
432            .picked(),
433        }
434    }
435
436    fn select(reader: &Reader, hit: &PickedElement) -> Option<Self> {
437        let _ = reader;
438        // What a click hits is held as it is named: whether it is what a
439        // mate needs is said by the mate, against the part it is of.
440        Self::from_picked(hit)
441    }
442
443    fn drawn(&self, reader: &Reader) -> Vec<PickedElement> {
444        let _ = reader;
445        let mut drawn = vec![drawn_of(&self.on)];
446        drawn.extend(self.at.iter().map(drawn_of));
447        drawn
448    }
449
450    /// The frame it gives, as a triad where it is drawn: nothing for an
451    /// element that gives none, which the mate says of it when it is held.
452    fn visuals(&self, reader: &Reader) -> Vec<Visual> {
453        let Ok(frame) = self.frame_in_world(reader) else {
454            return Vec::new();
455        };
456        let shape = geop_core_editor::editing::Shape::Triad {
457            at: *frame.origin(),
458            axes: [*frame.u(), *frame.v(), *frame.w()],
459        };
460        vec![Visual::new("frame", shape, Style::Selected)]
461    }
462}
463
464/// What the editor draws of `element`, by the kind it is drawn as.
465fn drawn_of(element: &Selected) -> PickedElement {
466    let picked = element.picked();
467    let kind = match element {
468        Selected::Vertex(_) => drawn::VERTEX,
469        Selected::Edge(_) => drawn::EDGE,
470        Selected::Face(_) => drawn::FACE,
471        Selected::Datum(_) => drawn::DATUM,
472        Selected::SketchPoint(_) => drawn::SKETCH_POINT,
473        Selected::SketchCurve(_) => drawn::SKETCH_CURVE,
474    };
475    PickedElement {
476        kind: kind.to_string(),
477        ..picked
478    }
479}