1use ogeom_core::{Arena, Key, OgeomResult, Tolerances, ogeom_bail};
24use ogeom_math::{Transform, TransformKind};
25use smallvec::SmallVec;
26
27pub type Datum = Transform;
32
33pub type DatumId = Key<Datum>;
35
36#[derive(Debug, Clone, Default)]
50pub struct DatumStore {
51 arena: Arena<Datum>,
52}
53
54impl DatumStore {
55 #[must_use]
57 pub const fn new() -> Self {
58 Self {
59 arena: Arena::new(),
60 }
61 }
62
63 pub fn insert(&mut self, transform: Datum) -> DatumId {
70 self.arena.insert(transform)
71 }
72
73 #[must_use]
75 pub fn get(&self, id: DatumId) -> Option<Datum> {
76 self.arena.get(id).copied()
77 }
78
79 #[must_use]
81 pub fn len(&self) -> usize {
82 self.arena.len()
83 }
84
85 #[must_use]
87 pub fn is_empty(&self) -> bool {
88 self.arena.is_empty()
89 }
90
91 pub(crate) const fn scope(&self) -> u32 {
93 self.arena.scope()
94 }
95
96 pub(crate) fn is_dense(&self) -> bool {
101 self.arena.is_dense()
102 }
103
104 pub fn iter(&self) -> impl Iterator<Item = (DatumId, Datum)> {
106 self.arena.iter().map(|(id, t)| (id, *t))
107 }
108}
109
110#[derive(Debug, Clone, Default, PartialEq, Eq, Hash)]
124pub struct Location {
125 chain: SmallVec<[(DatumId, i32); 2]>,
126}
127
128impl Location {
129 #[must_use]
131 pub fn identity() -> Self {
132 Self::default()
133 }
134
135 #[must_use]
137 pub fn of(datum: DatumId) -> Self {
138 let mut chain = SmallVec::new();
139 chain.push((datum, 1));
140 Self { chain }
141 }
142
143 #[must_use]
149 pub fn powered(datum: DatumId, power: i32) -> Self {
150 if power == 0 {
151 return Self::identity();
152 }
153 let mut chain = SmallVec::new();
154 if power == i32::MIN {
155 chain.push((datum, -i32::MAX));
156 chain.push((datum, -1));
157 } else {
158 chain.push((datum, power));
159 }
160 Self { chain }
161 }
162
163 #[must_use]
165 pub fn is_identity(&self) -> bool {
166 self.chain.is_empty()
167 }
168
169 #[must_use]
172 pub fn ends_with(&self, inner: &Self) -> bool {
173 self.chain.ends_with(&inner.chain)
174 }
175
176 #[must_use]
178 pub fn chain(&self) -> &[(DatumId, i32)] {
179 &self.chain
180 }
181
182 #[must_use]
184 pub fn depth(&self) -> usize {
185 self.chain.len()
186 }
187
188 #[must_use]
201 pub fn then(&self, inner: &Self) -> Self {
202 if inner.chain.is_empty() {
204 return self.clone();
205 }
206 if self.chain.is_empty() {
207 return inner.clone();
208 }
209 let mut chain = self.chain.clone();
210 for &(datum, power) in &inner.chain {
211 match chain.last_mut() {
212 Some((last, last_power))
213 if *last == datum
214 && last_power
215 .checked_add(power)
216 .is_some_and(|sum| sum != i32::MIN) =>
217 {
218 *last_power += power;
219 if *last_power == 0 {
220 chain.pop();
221 }
222 }
223 _ => chain.push((datum, power)),
224 }
225 }
226 Self { chain }
227 }
228
229 pub(crate) fn with_datum_scope(&self, scope: u32) -> Self {
234 Self {
235 chain: self
236 .chain
237 .iter()
238 .map(|(datum, power)| (datum.with_scope(scope), *power))
239 .collect(),
240 }
241 }
242
243 pub(crate) fn with_datum_offset(&self, offset: u32) -> Self {
250 Self {
251 chain: self
252 .chain
253 .iter()
254 .map(|(datum, power)| (crate::entity::shifted_key(*datum, offset), *power))
255 .collect(),
256 }
257 }
258
259 #[must_use]
265 pub fn inverted(&self) -> Self {
266 Self {
267 chain: self.chain.iter().rev().map(|&(d, p)| (d, -p)).collect(),
268 }
269 }
270
271 pub fn composed(&self, store: &DatumStore) -> OgeomResult<Transform> {
280 let mut result = Transform::IDENTITY;
281 for &(id, power) in &self.chain {
282 let Some(datum) = store.get(id) else {
283 ogeom_bail!(Dangling, "location refers to a datum not in this store");
284 };
285 let step = if power >= 0 { datum } else { datum.inverse()? };
286 result = result * raised(step, power.unsigned_abs());
287 }
288 Ok(result)
289 }
290
291 pub fn preserves_handedness(&self, store: &DatumStore) -> OgeomResult<bool> {
301 Ok(self.composed(store)?.preserves_handedness())
302 }
303
304 pub fn is_same_placement(
314 &self,
315 other: &Self,
316 store: &DatumStore,
317 tol: Tolerances,
318 ) -> OgeomResult<bool> {
319 if self == other {
320 return Ok(true);
321 }
322 Ok(self.composed(store)?.is_equal(&other.composed(store)?, tol))
323 }
324
325 pub fn kind(&self, store: &DatumStore) -> OgeomResult<TransformKind> {
331 if self.is_identity() {
332 return Ok(TransformKind::Identity);
333 }
334 Ok(self.composed(store)?.kind())
335 }
336}
337
338fn raised(step: Transform, times: u32) -> Transform {
341 let mut result = Transform::IDENTITY;
342 let mut base = step;
343 let mut left = times;
344 while left > 0 {
345 if left & 1 == 1 {
346 result = result * base;
347 }
348 left >>= 1;
349 if left > 0 {
350 base = base * base;
351 }
352 }
353 result
354}
355
356#[cfg(test)]
357#[allow(clippy::unwrap_used)]
358mod tests {
359 use super::*;
360 use ogeom_math::{Axis, Point, Vector};
361
362 const T: Tolerances = Tolerances::millimetres();
363
364 fn store() -> (DatumStore, DatumId, DatumId) {
365 let mut s = DatumStore::new();
366 let a = s.insert(Transform::translation(Vector::new(1.0, 0.0, 0.0)));
367 let b = s.insert(Transform::rotation(Axis::Z, core::f64::consts::FRAC_PI_2));
368 (s, a, b)
369 }
370
371 #[test]
372 fn the_identity_is_empty_and_composes_to_nothing() {
373 let (s, _, _) = store();
374 let id = Location::identity();
375 assert!(id.is_identity());
376 assert_eq!(id.depth(), 0);
377 assert_eq!(id.composed(&s).unwrap().kind(), TransformKind::Identity);
378 }
379
380 #[test]
381 fn composition_applies_the_inner_placement_first() {
382 let (s, a, b) = store();
383 let outer = Location::of(a);
384 let inner = Location::of(b);
385 let combined = outer.then(&inner);
386
387 let expected = outer.composed(&s).unwrap() * inner.composed(&s).unwrap();
388 assert!(combined.composed(&s).unwrap().is_equal(&expected, T));
389
390 assert!(
392 !combined
393 .composed(&s)
394 .unwrap()
395 .is_equal(&inner.then(&outer).composed(&s).unwrap(), T)
396 );
397 }
398
399 #[test]
400 fn inversion_is_exact_rather_than_approximate() {
401 let (_, a, b) = store();
405 let l = Location::of(a)
406 .then(&Location::of(b))
407 .then(&Location::of(a));
408 let round_trip = l.then(&l.inverted());
409 assert!(round_trip.is_identity(), "chain: {:?}", round_trip.chain());
410 assert_eq!(round_trip, Location::identity());
411 }
412
413 #[test]
414 fn repeated_composition_does_not_grow_the_chain() {
415 let (s, a, _) = store();
419 let mut l = Location::identity();
420 for _ in 0..100 {
421 l = l.then(&Location::of(a));
422 }
423 assert_eq!(l.depth(), 1);
424 assert_eq!(l.chain()[0].1, 100);
425 assert!(
426 l.composed(&s)
427 .unwrap()
428 .apply(Point::ORIGIN)
429 .is_equal(Point::new(100.0, 0.0, 0.0), T)
430 );
431 }
432
433 #[test]
434 fn powers_cancel_exactly() {
435 let (s, a, _) = store();
436 let forward = Location::powered(a, 5);
437 let back = Location::powered(a, -5);
438 assert!(forward.then(&back).is_identity());
439 assert_eq!(Location::powered(a, 0), Location::identity());
440 assert!(
441 Location::powered(a, -2)
442 .composed(&s)
443 .unwrap()
444 .apply(Point::ORIGIN)
445 .is_equal(Point::new(-2.0, 0.0, 0.0), T)
446 );
447 }
448
449 #[test]
450 fn equality_is_structural_not_numerical() {
451 let mut s = DatumStore::new();
456 let a = s.insert(Transform::translation(Vector::new(1.0, 0.0, 0.0)));
457 let b = s.insert(Transform::translation(Vector::new(1.0, 0.0, 0.0)));
458
459 let via_a = Location::of(a);
460 let via_b = Location::of(b);
461 assert_ne!(via_a, via_b, "different datums are different placements");
462 assert!(via_a.is_same_placement(&via_b, &s, T).unwrap());
463
464 assert_eq!(via_a, Location::of(a));
465 assert!(via_a.is_same_placement(&Location::of(a), &s, T).unwrap());
466 }
467
468 #[test]
469 fn locations_hash_consistently_with_equality() {
470 use ogeom_core::FastSet;
471 let (_, a, b) = store();
472 let mut set = FastSet::default();
473 set.insert(Location::of(a));
474 set.insert(Location::of(a));
475 set.insert(Location::of(b));
476 set.insert(Location::identity());
477 assert_eq!(set.len(), 3);
478 assert!(set.contains(&Location::of(a)));
479 }
480
481 #[test]
482 fn evaluating_a_location_does_not_change_its_identity() {
483 let (s, a, _) = store();
487 let x = Location::of(a);
488 let y = Location::of(a);
489 let _ = x.composed(&s).unwrap();
490 assert_eq!(x, y);
491
492 use std::collections::hash_map::DefaultHasher;
493 use std::hash::{Hash, Hasher};
494 let hash = |l: &Location| {
495 let mut h = DefaultHasher::new();
496 l.hash(&mut h);
497 h.finish()
498 };
499 assert_eq!(hash(&x), hash(&y));
500 }
501
502 #[test]
503 fn a_location_is_send_and_sync() {
504 const fn assert_send_sync<T: Send + Sync>() {}
507 assert_send_sync::<Location>();
508 assert_send_sync::<DatumStore>();
509 }
510
511 #[test]
512 fn a_handle_the_store_does_not_hold_is_reported_rather_than_ignored() {
513 let (s, _, _) = store();
514 let mut other = DatumStore::new();
515 let mut beyond = other.insert(Transform::translation(Vector::Y));
517 for _ in 0..10 {
518 beyond = other.insert(Transform::translation(Vector::Y));
519 }
520 assert!(Location::of(beyond).composed(&s).is_err());
521 }
522
523 #[test]
524 fn a_handle_from_another_store_is_refused_rather_than_resolved() {
525 let (s, first, _) = store();
530 let mut other = DatumStore::new();
531 let foreign = other.insert(Transform::translation(Vector::new(0.0, 99.0, 0.0)));
532
533 assert_eq!(
534 foreign.index(),
535 first.index(),
536 "the same slot in both stores, so only the store tells the two apart"
537 );
538 assert!(s.get(foreign).is_none());
539 assert!(Location::of(foreign).composed(&s).is_err());
540 assert!(Location::of(first).composed(&s).is_ok());
541 }
542
543 #[test]
544 fn handedness_follows_the_composed_transform() {
545 let mut s = DatumStore::new();
546 let mirror = s.insert(Transform::plane_mirror(
547 Point::ORIGIN,
548 ogeom_math::Direction::Z,
549 ));
550 let once = Location::of(mirror);
551 assert!(!once.preserves_handedness(&s).unwrap());
552 assert!(
554 Location::powered(mirror, 2)
555 .preserves_handedness(&s)
556 .unwrap()
557 );
558 assert!(Location::identity().preserves_handedness(&s).unwrap());
559 }
560
561 #[test]
562 fn composing_different_datums_keeps_both_entries() {
563 let (_, a, b) = store();
564 let l = Location::of(a).then(&Location::of(b));
565 assert_eq!(l.depth(), 2);
566 assert_eq!(l.chain(), &[(a, 1), (b, 1)]);
567 }
568
569 #[test]
572 fn a_huge_power_composes_at_once() {
573 let mut s = DatumStore::new();
574 let step = s.insert(Transform::translation(Vector::new(1e-9, 0.0, 0.0)));
575 let far = Location::powered(step, i32::MAX).composed(&s).unwrap();
576 let x = far.apply(Point::ORIGIN).x;
577 let want = 1e-9 * f64::from(i32::MAX);
578 assert!((x - want).abs() < want * 1e-9, "{x} against {want}");
579 let turn = s.insert(Transform::rotation(
580 ogeom_math::Axis::new(Point::ORIGIN, ogeom_math::Direction::Z),
581 core::f64::consts::FRAC_PI_2,
582 ));
583 let four = Location::powered(turn, 4).composed(&s).unwrap();
584 assert!(
585 four.apply(Point::new(1.0, 0.0, 0.0))
586 .distance(Point::new(1.0, 0.0, 0.0))
587 < 1e-12
588 );
589 }
590
591 #[test]
593 fn merging_powers_never_overflows() {
594 let (_, a, _) = store();
595 let l = Location::powered(a, i32::MAX).then(&Location::powered(a, 1));
596 assert_eq!(l.chain(), &[(a, i32::MAX), (a, 1)]);
597 }
598
599 #[test]
602 fn the_most_negative_power_inverts_exactly() {
603 let (s, a, _) = store();
604 let built = Location::powered(a, i32::MIN);
605 let merged = Location::powered(a, i32::MIN + 1).then(&Location::powered(a, -1));
606 for l in [built, merged] {
607 assert!(
608 l.chain().iter().all(|&(_, p)| p != i32::MIN),
609 "{:?}",
610 l.chain()
611 );
612 let x = |l: &Location| l.composed(&s).unwrap().apply(Point::ORIGIN).x;
613 assert_eq!(x(&l), -2_147_483_648.0);
614 let inverse = l.inverted();
615 assert_eq!(x(&inverse), 2_147_483_648.0);
616 assert_eq!(inverse.inverted(), l);
617 assert!(l.then(&inverse).is_identity());
618 assert!(inverse.then(&l).is_identity());
619 }
620 }
621
622 #[test]
623 fn the_datum_store_does_not_deduplicate() {
624 let mut s = DatumStore::new();
627 let t = Transform::translation(Vector::X);
628 let a = s.insert(t);
629 let b = s.insert(t);
630 assert_ne!(a, b);
631 assert_eq!(s.len(), 2);
632 assert!(!s.is_empty());
633 }
634}