Skip to main content

geop_ops/ui/
view.rs

1//! [`PartView`]: a part as the viewport draws it, and picking entities of it
2//! with a [`Pointer`].
3//!
4//! A part is drawn from its rasterization — sampled edges, triangulated
5//! faces, outlined sketches — and a pick tests against exactly that, never
6//! re-deriving geometry of its own, so a pick cannot disagree with what is
7//! on screen. The same goes for what the viewer draws besides the part: the
8//! datums are laid out here, by the sizes the viewer draws them with, and
9//! picked by the same rules.
10
11use geop_core_math::{
12    geop_error::GeopResult,
13    polygon::loops_contain,
14    primitives::{CoordinateSystem, DatumComponent, DatumKind, FrameAxis},
15    scalars::{Scalar, as_f64},
16    vector::{Vector2, Vector3},
17};
18use geop_core_sketch::{CurveId, PointId, point::P2, profile::curve_polyline};
19use geop_core_topology::{FaceId, Model, SolidId};
20use geop_ops_rasterize::rasterize;
21use serde::Serialize;
22
23use super::{Pointer, hit::nearer};
24use crate::{
25    Part,
26    operation::{Aspects, EntityRef, Role},
27};
28
29/// Samples per edge and per parametric direction of a face. A face's grid is
30/// refined further where its own curvature asks for it (see
31/// `geop_ops_rasterize::grid`), so this is the floor, not the ceiling.
32const RESOLUTION: usize = 24;
33
34/// How long a frame datum's axes are, in reaches: it is drawn at a constant
35/// size on screen, with its origin ball, its axes and the squares of its
36/// planes laid out in fractions of that, in [`PartView::pick`] as in the
37/// viewer.
38pub const FRAME: f64 = 10.0;
39
40/// An entity a pointer is over, where, and how far along its ray.
41#[derive(Clone, Debug, PartialEq)]
42pub struct PartHit<S: Scalar> {
43    pub entity: EntityRef,
44    pub point: Vector3<S>,
45    pub t: S,
46}
47
48/// A vertex of the part, as drawn.
49#[derive(Clone, Debug, Serialize)]
50#[serde(bound = "S: Scalar")]
51pub struct ViewVertex<S: Scalar> {
52    pub name: String,
53    pub at: Vector3<S>,
54}
55
56/// The roles `entity` can fill in `part` (see [`Aspects`]): none for one
57/// that does not resolve.
58fn roles_of<S: Scalar>(entity: &EntityRef, part: &Part<S>) -> Vec<Role> {
59    Aspects::of(entity, part)
60        .map(|g| g.roles())
61        .unwrap_or_default()
62}
63
64/// An edge of the part, as drawn: its curve sampled into a polyline.
65#[derive(Clone, Debug, Serialize)]
66#[serde(bound = "S: Scalar")]
67pub struct ViewEdge<S: Scalar> {
68    pub name: String,
69    pub polyline: Vec<Vector3<S>>,
70    /// What it can be picked as: an edge, and a line or a circle if it is
71    /// one.
72    #[serde(skip)]
73    pub roles: Vec<Role>,
74}
75
76/// A face of the part, as drawn: triangulated, with the surface's normal at
77/// each corner.
78#[derive(Clone, Debug, Serialize)]
79#[serde(bound = "S: Scalar")]
80pub struct ViewFace<S: Scalar> {
81    pub name: String,
82    /// The solid the face bounds.
83    pub solid: Option<String>,
84    pub triangles: Vec<[Vector3<S>; 3]>,
85    pub normals: Vec<[Vector3<S>; 3]>,
86    /// What it can be picked as: a plane if it is flat, round if it turns
87    /// around an axis.
88    #[serde(skip)]
89    pub roles: Vec<Role>,
90}
91
92/// A curve of a sketch, as drawn, in the sketch's plane.
93#[derive(Clone, Debug, Serialize)]
94#[serde(bound = "S: Scalar")]
95pub struct ViewCurve<S: Scalar> {
96    pub id: CurveId,
97    pub construction: bool,
98    pub polyline: Vec<Vector2<S>>,
99    /// What it can be picked as: a line, if it is one.
100    #[serde(skip)]
101    pub roles: Vec<Role>,
102}
103
104/// A point of a sketch, as drawn, in the sketch's plane.
105#[derive(Clone, Debug, Serialize)]
106#[serde(bound = "S: Scalar")]
107pub struct ViewSketchPoint<S: Scalar> {
108    pub id: PointId,
109    pub at: Vector2<S>,
110}
111
112/// A sketch of the part, as drawn: its plane, its closed regions (each an
113/// outer loop and its holes), its curves and its points, in the plane's
114/// `u`/`v` coordinates.
115#[derive(Clone, Debug, Serialize)]
116#[serde(bound = "S: Scalar")]
117pub struct ViewSketch<S: Scalar> {
118    pub name: String,
119    pub plane: CoordinateSystem<S>,
120    #[serde(skip)]
121    pub regions: Vec<Vec<Vec<Vector2<S>>>>,
122    pub curves: Vec<ViewCurve<S>>,
123    pub points: Vec<ViewSketchPoint<S>>,
124}
125
126/// A datum of the part.
127#[derive(Clone, Debug, Serialize)]
128#[serde(bound = "S: Scalar")]
129pub struct ViewDatum<S: Scalar> {
130    pub name: String,
131    pub kind: DatumKind,
132    pub frame: CoordinateSystem<S>,
133}
134
135/// Where the drawing is and how big: the center and diagonal (at least 1)
136/// of the box around it. Datum planes and axes, which are endless, are
137/// drawn this big around the point of them nearest the center.
138#[derive(Clone, Copy, Debug, PartialEq, Serialize)]
139#[serde(bound = "S: Scalar")]
140pub struct Extent<S: Scalar> {
141    pub center: Vector3<S>,
142    #[serde(with = "as_f64")]
143    pub size: S,
144}
145
146/// A part as the viewport draws it, every entity by name — and, serialized,
147/// what a viewer draws.
148#[derive(Clone, Debug, Serialize)]
149#[serde(bound = "S: Scalar")]
150pub struct PartView<S: Scalar> {
151    pub vertices: Vec<ViewVertex<S>>,
152    pub edges: Vec<ViewEdge<S>>,
153    pub faces: Vec<ViewFace<S>>,
154    pub sketches: Vec<ViewSketch<S>>,
155    pub datums: Vec<ViewDatum<S>>,
156    /// The part's solids, oldest first.
157    pub solids: Vec<String>,
158    pub extent: Extent<S>,
159}
160
161/// The solid that owns `face`: a face is part of exactly one shell, and a
162/// shell of exactly one solid.
163pub fn solid_of_face<S: Scalar>(model: &Model<S>, face: FaceId) -> Option<SolidId> {
164    model
165        .get_face(face)
166        .ok()
167        .and_then(|f| model.get_shell(f.shell).ok())
168        .map(|s| s.solid)
169}
170
171impl<S: Scalar> PartView<S> {
172    /// `part` as drawn.
173    pub fn of(part: &Part<S>) -> GeopResult<Self> {
174        let model = part.topology();
175        let raster = rasterize(model, RESOLUTION)?;
176        let name = |id: crate::RefId| part.name_of(id).unwrap_or_default().to_string();
177        let uv = |polyline: Vec<P2>| {
178            polyline
179                .into_iter()
180                .map(|p| Vector2::from_array(p.map(S::from_f64)))
181                .collect::<Vec<_>>()
182        };
183
184        let mut vertices: Vec<_> = raster.vertices.iter().collect();
185        vertices.sort_by_key(|(id, _)| id.0);
186        let mut edges: Vec<_> = raster.edges.iter().collect();
187        edges.sort_by_key(|(id, _)| id.0);
188        let mut faces: Vec<_> = raster.faces.iter().collect();
189        faces.sort_by_key(|(id, _)| id.0);
190        let mut solids: Vec<SolidId> = model.solids.keys().copied().collect();
191        solids.sort_by_key(|s| s.0);
192
193        let sketches = part
194            .sketches()
195            .map(|(id, placed)| {
196                let sketch = name(id.into());
197                let s = &placed.sketch;
198                let positions = s.positions();
199                // A sketch whose curves form no region is still a sketch,
200                // hit on its curves.
201                let regions = s
202                    .regions()
203                    .map(|regions| {
204                        regions
205                            .iter()
206                            .map(|r| {
207                                std::iter::once(&r.outer)
208                                    .chain(&r.holes)
209                                    .map(|l| uv(l.polyline(s, &positions)))
210                                    .collect()
211                            })
212                            .collect()
213                    })
214                    .unwrap_or_default();
215                ViewSketch {
216                    name: sketch.clone(),
217                    plane: placed.plane.clone(),
218                    regions,
219                    curves: s
220                        .curves
221                        .iter()
222                        .map(|(&id, c)| ViewCurve {
223                            id,
224                            construction: c.construction,
225                            polyline: uv(curve_polyline(s, &positions, id)),
226                            roles: roles_of(
227                                &EntityRef::SketchCurve {
228                                    sketch: sketch.clone(),
229                                    curve: id,
230                                },
231                                part,
232                            ),
233                        })
234                        .collect(),
235                    points: positions
236                        .iter()
237                        .map(|(&id, &p)| ViewSketchPoint {
238                            id,
239                            at: Vector2::from_array(p.map(S::from_f64)),
240                        })
241                        .collect(),
242                }
243            })
244            .collect();
245
246        let mut view = PartView {
247            vertices: vertices
248                .into_iter()
249                .map(|(&id, p)| ViewVertex {
250                    name: name(id.into()),
251                    at: *p,
252                })
253                .collect(),
254            edges: edges
255                .into_iter()
256                .map(|(&id, polyline)| {
257                    let edge = name(id.into());
258                    ViewEdge {
259                        roles: roles_of(&EntityRef::Edge { name: edge.clone() }, part),
260                        name: edge,
261                        polyline: polyline.clone(),
262                    }
263                })
264                .collect(),
265            faces: faces
266                .into_iter()
267                .map(|(&id, tris)| {
268                    let face = name(id.into());
269                    ViewFace {
270                        roles: roles_of(&EntityRef::Face { name: face.clone() }, part),
271                        name: face,
272                        solid: solid_of_face(model, id).map(|s| name(s.into())),
273                        triangles: tris.iter().map(|t| [t.a, t.b, t.c]).collect(),
274                        normals: tris
275                            .iter()
276                            .map(|t| t.vertex_normals.unwrap_or([t.normal; 3]))
277                            .collect(),
278                    }
279                })
280                .collect(),
281            sketches,
282            datums: part
283                .datums()
284                .map(|(id, datum)| ViewDatum {
285                    name: name(id.into()),
286                    kind: datum.kind,
287                    frame: datum.frame.clone(),
288                })
289                .collect(),
290            solids: solids.into_iter().map(|s| name(s.into())).collect(),
291            extent: Extent {
292                center: Vector3::zero(),
293                size: S::ONE,
294            },
295        };
296        view.extent = view.measure();
297        Ok(view)
298    }
299
300    /// The box around everything drawn: the union of every point of it.
301    fn measure(&self) -> Extent<S> {
302        let sketch_points = self.sketches.iter().flat_map(|sketch| {
303            sketch
304                .curves
305                .iter()
306                .flat_map(|c| &c.polyline)
307                .map(|p| sketch.plane.uv_to_xyz(p))
308        });
309        let hull = self
310            .vertices
311            .iter()
312            .map(|v| v.at)
313            .chain(self.edges.iter().flat_map(|e| e.polyline.iter().copied()))
314            .chain(
315                self.faces
316                    .iter()
317                    .flat_map(|f| f.triangles.iter().flatten().copied()),
318            )
319            .chain(sketch_points)
320            .reduce(|a, b| a.union(&b));
321        let Some(hull) = hull else {
322            return Extent {
323                center: Vector3::zero(),
324                size: S::ONE,
325            };
326        };
327        let size = Vector3::from_array([0, 1, 2].map(|k| hull[k].width())).norm();
328        Extent {
329            center: Vector3::from_array([0, 1, 2].map(|k| hull[k].midpoint())),
330            size: if size.definitely_less(S::ONE) {
331                S::ONE
332            } else {
333                size
334            },
335        }
336    }
337
338    /// Whatever the pointer is over that can fill one of `roles` — and, with
339    /// a `scope`, is part of it: a line of one sketch. Frame datums are drawn
340    /// on top of everything, so they are picked first. Otherwise a vertex or
341    /// a sketch point, else an edge or a sketch curve, near the pointer — and not hidden behind a
342    /// face — wins: the smallest entity under the pointer is the one meant.
343    /// Else the nearest along the ray of the faces, solids, sketches and
344    /// other datums hit.
345    pub fn pick(
346        &self,
347        pointer: &Pointer<S>,
348        roles: &[Role],
349        scope: Option<&EntityRef>,
350    ) -> Option<PartHit<S>> {
351        let accept = |entity: &EntityRef, its: &[Role]| {
352            its.iter().any(|r| roles.contains(r)) && scope.is_none_or(|s| entity.lies_in(s))
353        };
354        if let Some(hit) = self.pick_frame(pointer, &accept) {
355            return Some(hit);
356        }
357        let ray = &pointer.ray;
358        let face = self.pick_face(pointer);
359        // In front of the face hit, give or take the reach: a vertex or an
360        // edge on the face's own boundary lies right at it.
361        let visible = |t: S| {
362            face.as_ref()
363                .is_none_or(|(ft, _)| !t.definitely_greater(ft.add(pointer.reach_at(1.0, *ft))))
364        };
365        let near = |(dist, t): (S, S)| (pointer.within(dist, t, 1.0) && visible(t)).then_some(t);
366        let nearest = |hits: Vec<PartHit<S>>| hits.into_iter().min_by(|a, b| nearer(a.t, b.t));
367
368        let vertices = self.vertices.iter().map(|v| {
369            let entity = EntityRef::Vertex {
370                name: v.name.clone(),
371            };
372            (entity, v.at)
373        });
374        let sketch_points = self.sketches.iter().flat_map(|sketch| {
375            sketch.points.iter().map(|p| {
376                let entity = EntityRef::SketchPoint {
377                    sketch: sketch.name.clone(),
378                    point: p.id,
379                };
380                (entity, sketch.plane.uv_to_xyz(&p.at))
381            })
382        });
383        let points = vertices
384            .chain(sketch_points)
385            .filter(|(entity, _)| accept(entity, &[Role::Point]))
386            .filter_map(|(entity, at)| {
387                near(ray.distance_to_point(&at)).map(|t| PartHit {
388                    entity,
389                    point: at,
390                    t,
391                })
392            })
393            .collect();
394        if let Some(hit) = nearest(points) {
395            return Some(hit);
396        }
397
398        let polyline_hit =
399            |entity: EntityRef, polyline: &mut dyn Iterator<Item = (Vector3<S>, Vector3<S>)>| {
400                polyline
401                    .filter_map(|(a, b)| near(ray.distance_to_segment(&a, &b)))
402                    .min_by(|&a, &b| nearer(a, b))
403                    .map(|t| PartHit {
404                        entity: entity.clone(),
405                        point: ray.at(t),
406                        t,
407                    })
408            };
409        let mut curves = Vec::new();
410        for e in &self.edges {
411            let entity = EntityRef::Edge {
412                name: e.name.clone(),
413            };
414            if accept(&entity, &e.roles) {
415                let mut segments = e.polyline.windows(2).map(|w| (w[0], w[1]));
416                curves.extend(polyline_hit(entity, &mut segments));
417            }
418        }
419        for sketch in &self.sketches {
420            for c in &sketch.curves {
421                let entity = EntityRef::SketchCurve {
422                    sketch: sketch.name.clone(),
423                    curve: c.id,
424                };
425                if accept(&entity, &c.roles) {
426                    let world = |p: &Vector2<S>| sketch.plane.uv_to_xyz(p);
427                    let mut segments = c.polyline.windows(2).map(|w| (world(&w[0]), world(&w[1])));
428                    curves.extend(polyline_hit(entity, &mut segments));
429                }
430            }
431        }
432        if let Some(hit) = nearest(curves) {
433            return Some(hit);
434        }
435
436        let mut hits: Vec<PartHit<S>> = Vec::new();
437        if let Some((t, f)) = face {
438            let face = EntityRef::Face {
439                name: f.name.clone(),
440            };
441            let solid = f.solid.clone().map(|name| EntityRef::Solid { name });
442            let entity = if accept(&face, &f.roles) {
443                Some(face)
444            } else {
445                solid.filter(|solid| accept(solid, &[Role::Solid]))
446            };
447            hits.extend(entity.map(|entity| PartHit {
448                entity,
449                point: ray.at(t),
450                t,
451            }));
452        }
453        hits.extend(self.pick_sketch(pointer, &accept));
454        hits.extend(self.pick_datum(pointer, &accept));
455        nearest(hits)
456    }
457
458    /// Whether `entity` — a sketch or a datum — or a part of it can fill one
459    /// of `roles`: whether a pick for them could take something of it.
460    pub fn can_fill(&self, entity: &EntityRef, roles: &[Role]) -> bool {
461        let fills = |its: &[Role]| its.iter().any(|r| roles.contains(r));
462        match entity {
463            EntityRef::Sketch { name } => self.sketches.iter().any(|s| {
464                s.name == *name
465                    && (fills(&[Role::Sketch])
466                        || (!s.points.is_empty() && fills(&[Role::Point]))
467                        || s.curves.iter().any(|c| fills(&c.roles)))
468            }),
469            EntityRef::Datum { name, .. } => self.datums.iter().any(|d| {
470                d.name == *name
471                    && match d.kind {
472                        DatumKind::Frame => fills(&[Role::Point, Role::Line, Role::Plane]),
473                        kind => fills(&[datum_role(kind)]),
474                    }
475            }),
476            _ => false,
477        }
478    }
479
480    /// The nearest face the ray enters.
481    fn pick_face(&self, pointer: &Pointer<S>) -> Option<(S, &ViewFace<S>)> {
482        self.faces
483            .iter()
484            .flat_map(|f| f.triangles.iter().map(move |tri| (f, tri)))
485            .filter_map(|(f, [a, b, c])| pointer.ray.intersect_triangle(a, b, c).map(|t| (t, f)))
486            .min_by(|a, b| nearer(a.0, b.0))
487    }
488
489    /// The nearest sketch hit inside one of its closed regions — the area a
490    /// viewer shades — or near one of its curves.
491    fn pick_sketch(
492        &self,
493        pointer: &Pointer<S>,
494        accept: &impl Fn(&EntityRef, &[Role]) -> bool,
495    ) -> Option<PartHit<S>> {
496        self.sketches
497            .iter()
498            .filter(|sketch| {
499                let entity = EntityRef::Sketch {
500                    name: sketch.name.clone(),
501                };
502                accept(&entity, &[Role::Sketch])
503            })
504            .filter_map(|sketch| {
505                let (t, p) = pointer.ray.intersect_uv_plane(&sketch.plane)?;
506                let reach = pointer.reach_at(1.0, t);
507                let in_region = sketch.regions.iter().any(|loops| loops_contain(loops, &p));
508                let on_curve = || {
509                    sketch.curves.iter().any(|c| {
510                        c.polyline.windows(2).any(|w| {
511                            !p.distance_to_segment(&w[0], &w[1])
512                                .definitely_greater(reach)
513                        })
514                    })
515                };
516                (in_region || on_curve()).then(|| PartHit {
517                    entity: EntityRef::Sketch {
518                        name: sketch.name.clone(),
519                    },
520                    point: pointer.ray.at(t),
521                    t,
522                })
523            })
524            .min_by(|a, b| nearer(a.t, b.t))
525    }
526
527    /// The nearest datum other than a frame `accept`ed the ray hits, as the
528    /// viewer draws it: a point at its origin, an axis as a line and a plane
529    /// as a square, each [`Extent::size`] long around the point of it
530    /// nearest the extent's center.
531    fn pick_datum(
532        &self,
533        pointer: &Pointer<S>,
534        accept: &impl Fn(&EntityRef, &[Role]) -> bool,
535    ) -> Option<PartHit<S>> {
536        let Extent { center, size } = self.extent;
537        let ray = &pointer.ray;
538        let half = size.div(S::TWO).ok()?;
539        self.datums
540            .iter()
541            .filter(|d| {
542                d.kind != DatumKind::Frame
543                    && accept(&EntityRef::datum(d.name.clone()), &[datum_role(d.kind)])
544            })
545            .filter_map(|d| {
546                let f = &d.frame;
547                let (origin, w) = (f.origin(), f.w());
548                let t = match d.kind {
549                    DatumKind::Point => {
550                        let (dist, t) = ray.distance_to_point(origin);
551                        pointer.within(dist, t, 1.0).then_some(t)?
552                    }
553                    DatumKind::Axis => {
554                        let mid = origin.add(&w.prod_scalar(center.sub(origin).prod_dot(w)));
555                        let reach = w.prod_scalar(size);
556                        let (dist, t) = ray.distance_to_segment(&mid.sub(&reach), &mid.add(&reach));
557                        pointer.within(dist, t, 1.0).then_some(t)?
558                    }
559                    DatumKind::Plane => {
560                        let (t, point) = ray.intersect_plane(origin, w)?;
561                        let d = point.sub(&center);
562                        let inside =
563                            |axis: &Vector3<S>| !d.prod_dot(axis).abs().definitely_greater(half);
564                        (inside(f.u()) && inside(f.v())).then_some(t)?
565                    }
566                    DatumKind::Frame => unreachable!("frames are filtered out above"),
567                };
568                Some(PartHit {
569                    entity: EntityRef::datum(d.name.clone()),
570                    point: ray.at(t),
571                    t,
572                })
573            })
574            .min_by(|a, b| nearer(a.t, b.t))
575    }
576
577    /// The part of a frame datum `accept`ed the ray hits, the frame
578    /// [`FRAME`] reaches tall: its origin ball — the frame as a whole, a
579    /// point — else an axis (from a tenth of its length to its tip), else a
580    /// plane's square (off the origin, between the other two axes). The
581    /// smallest part under the pointer, as for the part's entities, and of
582    /// the nearest frame.
583    fn pick_frame(
584        &self,
585        pointer: &Pointer<S>,
586        accept: &impl Fn(&EntityRef, &[Role]) -> bool,
587    ) -> Option<PartHit<S>> {
588        let ray = &pointer.ray;
589        let frac = |size: S, x: f64| size.mul(S::from_f64(x));
590        let (mut balls, mut axes, mut planes) = (Vec::new(), Vec::new(), Vec::new());
591        for d in self.datums.iter().filter(|d| d.kind == DatumKind::Frame) {
592            let f = &d.frame;
593            let origin = f.origin();
594            let size = pointer.reach_at(FRAME, ray.closest_to_point(origin));
595            if accept(&EntityRef::datum(d.name.clone()), &[Role::Point]) {
596                let (dist, t) = ray.distance_to_point(origin);
597                if !dist.definitely_greater(frac(size, 0.12)) {
598                    balls.push(PartHit {
599                        entity: EntityRef::datum(d.name.clone()),
600                        point: *origin,
601                        t,
602                    });
603                }
604            }
605            for axis in FrameAxis::ALL {
606                let dir = axis.of(f);
607                let component = |c| EntityRef::datum_component(d.name.clone(), c);
608                if accept(&component(DatumComponent::Axis(axis)), &[Role::Line]) {
609                    let (a, b) = (
610                        origin.add(&dir.prod_scalar(frac(size, 0.1))),
611                        origin.add(&dir.prod_scalar(size)),
612                    );
613                    let (dist, t) = ray.distance_to_segment(&a, &b);
614                    if pointer.within(dist, t, 0.6) {
615                        axes.push(PartHit {
616                            entity: EntityRef::datum_component(
617                                d.name.clone(),
618                                DatumComponent::Axis(axis),
619                            ),
620                            point: b,
621                            t,
622                        });
623                    }
624                }
625                if accept(&component(DatumComponent::Plane(axis)), &[Role::Plane])
626                    && let Some((t, point)) = ray.intersect_plane(origin, &dir)
627                {
628                    let local = point.sub(origin);
629                    let in_square =
630                        FrameAxis::ALL
631                            .iter()
632                            .filter(|&&other| other != axis)
633                            .all(|other| {
634                                let x = local
635                                    .prod_dot(&other.of(f))
636                                    .div(size)
637                                    .unwrap_or(S::INFINITY);
638                                !x.sub(S::from_f64(0.45))
639                                    .abs()
640                                    .definitely_greater(S::from_f64(0.15))
641                            });
642                    if in_square {
643                        planes.push(PartHit {
644                            entity: EntityRef::datum_component(
645                                d.name.clone(),
646                                DatumComponent::Plane(axis),
647                            ),
648                            point,
649                            t,
650                        });
651                    }
652                }
653            }
654        }
655        let nearest = |hits: Vec<PartHit<S>>| hits.into_iter().min_by(|a, b| nearer(a.t, b.t));
656        nearest(balls)
657            .or_else(|| nearest(axes))
658            .or_else(|| nearest(planes))
659    }
660}
661
662/// The role a datum of `kind` fills as a whole, other than a frame's.
663fn datum_role(kind: DatumKind) -> Role {
664    match kind {
665        DatumKind::Point | DatumKind::Frame => Role::Point,
666        DatumKind::Axis => Role::Line,
667        DatumKind::Plane => Role::Plane,
668    }
669}