Skip to main content

geop_ops_model/
datums.rs

1//! A part's datums: reference geometry it is built *with*, not *of* — a
2//! point, an axis, a plane or a frame, by name — in the one target
3//! [`datums`], which the steps that add datums revise ([`Datums`]). Every
4//! part also has [`ORIGIN`], the world's origin and axes, which no step
5//! adds.
6//!
7//! A datum is an element of the target as a whole ([`AnyDatum`]), and as
8//! what it is: a point datum a [`Point`], an axis a [`Line`], a plane a
9//! [`Plane`]. A frame's components are elements of their own, named after
10//! it: `origin:z` its `z` axis, `origin:xy` the plane normal to it (see
11//! [`component_name`]); a frame itself is a point, its origin. A part no
12//! step added a datum to has the origin all the same: the target reads as
13//! it where no step made it ([`HasElements::undefined`]).
14
15use geop_core_program::{
16    element::{ElementReference, HasElements, Kind, PickedElement, whole},
17    reader::Reader,
18    target::{Target, TargetReference},
19};
20use std::{borrow::Cow, collections::BTreeMap, sync::LazyLock};
21
22use crate::{Line, Plane, Point};
23use geop_core_editor::view::{PartView, ViewDatum, drawn};
24use geop_core_editor::{
25    registry::{Drawer, StructureItem},
26    selectable::SelectableElement,
27};
28use geop_core_geometry::shape::Axis;
29use geop_core_math::{
30    geop_error::{GeopError, GeopResult},
31    primitives::{CoordinateSystem, Datum, DatumComponent, DatumKind, FrameAxis},
32    vector::Vector3,
33};
34
35/// The name of the frame datum every part has: the world's origin and
36/// axes, and the three planes between them.
37pub const ORIGIN: &str = "origin";
38
39/// The name the part's datums are a target by: there is one.
40const DATUMS: &str = "datums";
41
42/// What separates a frame's name from its component's in an element's
43/// name: `origin:xy`. No step id has it, so no datum's name does.
44const COMPONENT: char = ':';
45
46/// The datums the steps of a part added, by name.
47#[derive(Clone, Debug, Default, PartialEq)]
48pub struct Datums(BTreeMap<String, Datum>);
49
50/// The part's datums: revised by every step that adds one.
51pub fn datums() -> TargetReference<Datums> {
52    TargetReference::new(DATUMS)
53}
54
55/// The datum `name`, or a component of one (see [`component_name`]), of
56/// the kind `K`: what a step's arguments refer to it by.
57pub fn datum<K>(name: impl Into<String>) -> ElementReference<Datums, K> {
58    ElementReference::new(datums(), name)
59}
60
61/// The datums of a part no step added any to: none but [`ORIGIN`], which
62/// every part has to build on.
63fn origin() -> &'static Datums {
64    static ORIGIN_ONLY: LazyLock<Datums> = LazyLock::new(Datums::default);
65    &ORIGIN_ONLY
66}
67
68/// The datums as `reader` reads them: none but [`ORIGIN`], before any step
69/// defines them.
70pub fn read_datums<'a>(reader: &Reader<'a>) -> GeopResult<Cow<'a, Datums>> {
71    if !reader.defines(&datums()) {
72        return Ok(Cow::Borrowed(origin()));
73    }
74    Ok(Cow::Borrowed(reader.retrieve_element(
75        &ElementReference::<Datums, Datums>::whole(datums()),
76    )?))
77}
78
79/// The name of the component `component` of the frame `frame`: `origin:z`
80/// for an axis, `origin:xy` for the plane normal to `z`.
81pub fn component_name(frame: &str, component: DatumComponent) -> String {
82    let component = match component {
83        DatumComponent::Axis(axis) => axis.letter().to_string(),
84        DatumComponent::Plane(normal) => {
85            let [a, b] = in_plane(normal);
86            format!("{}{}", a.letter(), b.letter())
87        }
88    };
89    format!("{frame}{COMPONENT}{component}")
90}
91
92/// The axes spanning the plane normal to `normal`, in order.
93fn in_plane(normal: FrameAxis) -> [FrameAxis; 2] {
94    match normal {
95        FrameAxis::X => [FrameAxis::Y, FrameAxis::Z],
96        FrameAxis::Y => [FrameAxis::Z, FrameAxis::X],
97        FrameAxis::Z => [FrameAxis::X, FrameAxis::Y],
98    }
99}
100
101/// The component `text` names (see [`component_name`]).
102fn component(text: &str) -> GeopResult<DatumComponent> {
103    for axis in FrameAxis::ALL {
104        if text == axis.letter() {
105            return Ok(DatumComponent::Axis(axis));
106        }
107        let [a, b] = in_plane(axis);
108        if text == format!("{}{}", a.letter(), b.letter()) {
109            return Ok(DatumComponent::Plane(axis));
110        }
111    }
112    Err(GeopError::new(format!(
113        "{text:?} is no component of a frame: name an axis, `x`, or a plane, `xy`"
114    )))
115}
116
117/// The plane of [`ORIGIN`] normal to `normal`.
118pub fn origin_plane(normal: FrameAxis) -> ElementReference<Datums, Plane> {
119    ElementReference::new(
120        datums(),
121        component_name(ORIGIN, DatumComponent::Plane(normal)),
122    )
123}
124
125/// The axis `axis` of [`ORIGIN`].
126pub fn origin_axis(axis: FrameAxis) -> ElementReference<Datums, Line> {
127    ElementReference::new(datums(), component_name(ORIGIN, DatumComponent::Axis(axis)))
128}
129
130impl Datums {
131    /// The datum `name`: one a step added, [`ORIGIN`], or a component of a
132    /// frame (see [`component_name`]) as a datum of its own.
133    pub fn get(&self, name: &str) -> GeopResult<Datum> {
134        let (frame, component) = match name.split_once(COMPONENT) {
135            Some((frame, text)) => (frame, Some(self::component(text)?)),
136            None => (name, None),
137        };
138        let datum = match frame {
139            ORIGIN => Datum {
140                kind: DatumKind::Frame,
141                frame: CoordinateSystem::world_at(Vector3::zero()),
142            },
143            _ => (self.0.get(frame).cloned()).ok_or_else(|| {
144                GeopError::new(format!(
145                    "there is no datum {frame:?} (datums: {:?})",
146                    self.names()
147                ))
148            })?,
149        };
150        match component {
151            Some(component) => datum.component(component),
152            None => Ok(datum),
153        }
154    }
155
156    /// Adds `datum` as `name`: an error for a name taken.
157    pub fn insert(&mut self, name: &str, datum: Datum) -> GeopResult<()> {
158        if name == ORIGIN || name.contains(COMPONENT) || self.0.contains_key(name) {
159            return Err(GeopError::new(format!("there is a datum {name:?} already")));
160        }
161        self.0.insert(name.to_string(), datum);
162        Ok(())
163    }
164
165    /// The names of the datums the steps added, sorted.
166    pub fn names(&self) -> Vec<&str> {
167        self.0.keys().map(String::as_str).collect()
168    }
169
170    /// The datums the steps added, by name, sorted.
171    pub fn iter(&self) -> impl Iterator<Item = (&str, &Datum)> {
172        self.0.iter().map(|(name, datum)| (name.as_str(), datum))
173    }
174}
175
176impl Datums {
177    /// Draws [`ORIGIN`] and every datum into `view`, a frame with its axes
178    /// and planes each picked on its own.
179    pub fn draw(&self, view: &mut PartView) -> GeopResult<()> {
180        view.datum_target = DATUMS.to_string();
181        let origin = self.get(ORIGIN)?;
182        let datums = std::iter::once((ORIGIN, &origin)).chain(self.iter());
183        view.datums = datums
184            .map(|(name, datum)| {
185                let components = match datum.kind {
186                    DatumKind::Frame => (FrameAxis::ALL.into_iter())
187                        .flat_map(|axis| [DatumComponent::Axis(axis), DatumComponent::Plane(axis)])
188                        .map(|c| (c, component_name(name, c)))
189                        .collect(),
190                    _ => Vec::new(),
191                };
192                ViewDatum {
193                    name: name.to_string(),
194                    kind: datum.kind,
195                    frame: datum.frame.clone(),
196                    components,
197                }
198            })
199            .collect();
200        Ok(())
201    }
202}
203
204/// A datum of the part, whatever it is.
205pub struct AnyDatum;
206
207impl Kind for AnyDatum {
208    const NAME: &'static str = drawn::DATUM;
209}
210
211/// Every step that adds a datum adds to them.
212impl Target for Datums {
213    const CHAIN: bool = true;
214}
215
216/// Read whole.
217impl Kind for Datums {
218    const NAME: &'static str = "datums";
219}
220
221impl HasElements<Datums> for Datums {
222    type Element<'a> = &'a Datums;
223    const SINGLETON: Option<&'static str> = Some(DATUMS);
224
225    fn undefined() -> Option<&'static Self> {
226        Some(origin())
227    }
228
229    fn element(&self, name: &str) -> GeopResult<&Datums> {
230        whole(self, name)
231    }
232}
233
234impl HasElements<AnyDatum> for Datums {
235    type Element<'a> = Datum;
236    const SINGLETON: Option<&'static str> = Some(DATUMS);
237
238    fn undefined() -> Option<&'static Self> {
239        Some(origin())
240    }
241
242    fn element(&self, name: &str) -> GeopResult<Datum> {
243        self.get(name)
244    }
245}
246
247/// A point datum, or a frame by its origin.
248impl HasElements<Point> for Datums {
249    type Element<'a> = Vector3;
250    const SINGLETON: Option<&'static str> = Some(DATUMS);
251
252    fn undefined() -> Option<&'static Self> {
253        Some(origin())
254    }
255
256    fn element(&self, name: &str) -> GeopResult<Vector3> {
257        let datum = self.get(name)?;
258        match datum.kind {
259            DatumKind::Point | DatumKind::Frame => Ok(*datum.frame.origin()),
260            kind => Err(GeopError::new(format!(
261                "the datum {name:?} is an {kind:?}, not a point"
262            ))),
263        }
264    }
265}
266
267/// An axis datum, or a frame's axis.
268impl HasElements<Line> for Datums {
269    type Element<'a> = Axis;
270    const SINGLETON: Option<&'static str> = Some(DATUMS);
271
272    fn undefined() -> Option<&'static Self> {
273        Some(origin())
274    }
275
276    fn element(&self, name: &str) -> GeopResult<Axis> {
277        let datum = self.get(name)?;
278        match datum.kind {
279            DatumKind::Axis => Axis::try_new(*datum.frame.origin(), *datum.frame.w()),
280            kind => Err(GeopError::new(format!(
281                "the datum {name:?} is a {kind:?}, not an axis"
282            ))),
283        }
284    }
285}
286
287/// A plane datum, or a frame's plane: its frame, `w` the normal.
288impl HasElements<Plane> for Datums {
289    type Element<'a> = CoordinateSystem;
290    const SINGLETON: Option<&'static str> = Some(DATUMS);
291
292    fn undefined() -> Option<&'static Self> {
293        Some(origin())
294    }
295
296    fn element(&self, name: &str) -> GeopResult<CoordinateSystem> {
297        let datum = self.get(name)?;
298        match datum.kind {
299            DatumKind::Plane => Ok(datum.frame),
300            kind => Err(GeopError::new(format!(
301                "the datum {name:?} is a {kind:?}, not a plane"
302            ))),
303        }
304    }
305}
306
307/// Datums are drawn as themselves, a frame's components each on its own:
308/// a click on one selects it as what it is asked for, if it is that.
309macro_rules! picked_as_drawn {
310    ($($kind:ty),*) => {$(
311        impl SelectableElement<Datums> for $kind {
312            const PICKED_FROM: &'static [&'static str] = &[AnyDatum::NAME];
313
314            fn select(
315                reader: &Reader,
316                hit: &PickedElement,
317            ) -> Option<ElementReference<Datums, Self>> {
318                (hit.target == *datums().location() && hit.kind == AnyDatum::NAME).then_some(())?;
319                let reference = ElementReference::new(datums(), &hit.name);
320                reader.retrieve_element(&reference).ok()?;
321                Some(reference)
322            }
323
324            fn drawn(
325                _: &Reader,
326                element: &ElementReference<Datums, Self>,
327            ) -> Vec<PickedElement> {
328                vec![ElementReference::<Datums, AnyDatum>::new(datums(), element.name()).picked()]
329            }
330        }
331    )*};
332}
333
334picked_as_drawn!(AnyDatum, Point, Line, Plane);
335
336/// Draws a part's datums, the origin first, and lists them.
337pub struct DatumsDrawer;
338
339impl Drawer for DatumsDrawer {
340    fn draw(&self, reader: &Reader, view: &mut PartView) -> GeopResult<()> {
341        read_datums(reader)?.draw(view)
342    }
343
344    fn structure(&self, reader: &Reader) -> GeopResult<Vec<StructureItem>> {
345        let item = |name: &str| StructureItem::drawn("datum", "Datums", name);
346        let mut items = vec![item(ORIGIN)];
347        items.extend(read_datums(reader)?.names().into_iter().map(item));
348        Ok(items)
349    }
350}
351
352#[cfg(test)]
353mod tests {
354    use geop_core_editor::selectable::{Selectable, selected};
355    use geop_core_math::scalars::Scalar;
356    use geop_core_program::{operation_registry::OperationRegistry, part::Part, runner::Runner};
357
358    use super::*;
359
360    /// A part of one step, adding the axis datum `a`, along `x`, to the
361    /// origin's datums.
362    fn part() -> Part {
363        let mut part = Part::default();
364        let runner = Runner::new(OperationRegistry::default());
365        (runner.run_step(&mut part, "a", |ctx| {
366            ctx.produce(&datums(), &(), |reader, _| {
367                let mut datums: Datums = reader.previous()?;
368                let world = CoordinateSystem::world_at(Vector3::zero());
369                let axis = Datum {
370                    kind: DatumKind::Frame,
371                    frame: world,
372                }
373                .component(DatumComponent::Axis(FrameAxis::X))?;
374                datums.insert("a", axis)?;
375                Ok(datums)
376            })
377        }))
378        .unwrap();
379        part
380    }
381
382    /// The origin is drawn as a frame whose components are picked on their
383    /// own, as the datums name them; a datum added as itself.
384    #[test]
385    fn datums_are_drawn_and_picked_by_their_names() {
386        use geop_core_editor::editing::{Pointer, Reach};
387        use geop_core_math::primitives::Ray;
388        let part = part();
389        let reader = part.current();
390        let mut view = PartView::new();
391        read_datums(&reader).unwrap().draw(&mut view).unwrap();
392        let view = view.finish();
393        assert_eq!(view.datums.len(), 2);
394        let v = |p: [f64; 3]| Vector3::from_array(p.map(Scalar::from_f64));
395        // Through the origin's xy square, as drawn at a reach of 0.1.
396        let pointer = Pointer {
397            cycle: 0,
398            ray: Ray::try_new(v([0.45, 0.45, 10.0]), v([0.0, 0.0, -1.0])).unwrap(),
399            reach: Reach::Tube {
400                radius: Scalar::from_f64(0.1),
401            },
402        };
403        let hit = view
404            .pick(&pointer, &[drawn::DATUM.to_string()], &[])
405            .unwrap();
406        assert_eq!(hit.element, self::hit("origin:xy"));
407        let planes: Vec<ElementReference<Datums, Plane>> = selected(&reader, &[hit.element]);
408        assert_eq!(planes, [origin_plane(FrameAxis::Z)]);
409    }
410
411    fn hit(name: &str) -> PickedElement {
412        ElementReference::<Datums, AnyDatum>::new(datums(), name).picked()
413    }
414
415    /// The origin is a frame, its origin a point, its components an axis
416    /// and a plane; a datum added is what it is, and nothing else.
417    #[test]
418    fn datums_are_what_they_are() {
419        let part = part();
420        let reader = part.current();
421        let origin =
422            reader.retrieve_element(&ElementReference::<Datums, Point>::new(datums(), ORIGIN));
423        assert_eq!(origin.unwrap(), Vector3::zero());
424        let xy = reader
425            .retrieve_element(&origin_plane(FrameAxis::Z))
426            .unwrap();
427        assert!(xy.w().to_array()[2].could_be_equal(Scalar::ONE));
428        let z = reader.retrieve_element(&origin_axis(FrameAxis::Z)).unwrap();
429        assert!(z.direction.to_array()[2].could_be_equal(Scalar::ONE));
430        let a = reader.retrieve_element(&ElementReference::<Datums, Line>::new(datums(), "a"));
431        assert!(a.unwrap().direction.to_array()[0].could_be_equal(Scalar::ONE));
432        let not_a_plane = ElementReference::<Datums, Plane>::new(datums(), "a");
433        assert!(reader.retrieve_element(&not_a_plane).is_err());
434        assert!(
435            reader
436                .retrieve_element(&ElementReference::<Datums, Line>::new(datums(), "b"))
437                .is_err()
438        );
439        assert_eq!(read_datums(&reader).unwrap().names(), ["a"]);
440        assert_eq!(
441            component_name(ORIGIN, DatumComponent::Plane(FrameAxis::Y)),
442            "origin:zx"
443        );
444    }
445
446    /// A click on a datum selects it as what a field asks for, if it is
447    /// that; it is lit as the datum. Saved, a reference is the datum's name.
448    #[test]
449    fn a_datum_is_picked_as_what_it_is() {
450        let part = part();
451        let reader = part.current();
452        let lines: Vec<ElementReference<Datums, Line>> =
453            selected(&reader, &[hit("a"), hit("origin:xy"), hit("origin:z")]);
454        let names: Vec<&str> = lines.iter().map(|l| l.name()).collect();
455        assert_eq!(names, ["a", "origin:z"]);
456        assert_eq!(lines[1].drawn(&reader), [hit("origin:z")]);
457        assert_eq!(
458            serde_json::to_value(&lines[0]).unwrap(),
459            serde_json::json!("a")
460        );
461        let given = Part::default();
462        let reader = given.current();
463        assert!(read_datums(&reader).unwrap().names().is_empty());
464        let plane = reader.retrieve_element(&origin_plane(FrameAxis::X));
465        assert!(plane.unwrap().w().to_array()[0].could_be_equal(Scalar::ONE));
466    }
467
468    /// A frame datum — here the origin every part has — is hit before
469    /// anything else: its ball, as the whole frame, else an axis, else a
470    /// plane's square. Drawn over the unit cube, as an editor draws a part.
471    #[test]
472    fn frames() {
473        use crate::{
474            NamedModel, Solid,
475            testing::{given_part, unit_cube},
476        };
477        use geop_core_editor::{registry::Registry, selectable, testing::pointer};
478
479        let part = given_part(&unit_cube()).unwrap();
480        let registry = crate::register(Registry::default()).unwrap();
481        let view = registry
482            .draw(&part.current(), &Default::default(), None)
483            .unwrap();
484        let down = |x: f64, y: f64| pointer([x, y, 5.0], [0.0, 0.0, -1.0]);
485        let frame = [drawn::DATUM, drawn::VERTEX, drawn::EDGE, drawn::FACE].map(String::from);
486        let name = |x: f64, y: f64| view.pick(&down(x, y), &frame, &[]).map(|h| h.element.name);
487        // Ten reaches of 0.009 each: the axes reach 0.09 out.
488        assert_eq!(name(0.0, 0.0), Some(ORIGIN.to_string()));
489        assert_eq!(
490            name(0.06, 0.0),
491            Some(origin_axis(FrameAxis::X).name().to_string())
492        );
493        assert_eq!(
494            name(0.04, 0.04),
495            Some(origin_plane(FrameAxis::Z).name().to_string())
496        );
497        // Looking for what no part of a frame can be, the cube under it.
498        let solid = selectable::pick::<ElementReference<NamedModel, Solid>>(
499            &view,
500            &part.current(),
501            &down(0.04, 0.04),
502        );
503        assert!(solid.is_some());
504    }
505}