1use core::hash::{Hash, Hasher};
22
23use ogeom_core::{Key, OgeomResult, Tolerances};
24use ogeom_math::Transform;
25
26use crate::entity::NodeData;
27use crate::location::{DatumStore, Location};
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
34pub enum ShapeType {
35 Vertex,
37 Edge,
39 Wire,
41 Face,
43 Shell,
45 Solid,
47 CompSolid,
49 Compound,
51}
52
53impl ShapeType {
54 #[must_use]
59 pub const fn child_type(self) -> Option<Self> {
60 match self {
61 Self::Vertex | Self::Compound => None,
62 Self::Edge => Some(Self::Vertex),
63 Self::Wire => Some(Self::Edge),
64 Self::Face => Some(Self::Wire),
65 Self::Shell => Some(Self::Face),
66 Self::Solid => Some(Self::Shell),
67 Self::CompSolid => Some(Self::Solid),
68 }
69 }
70
71 #[must_use]
75 pub const fn dimension(self) -> Option<u8> {
76 match self {
77 Self::Vertex => Some(0),
78 Self::Edge | Self::Wire => Some(1),
79 Self::Face | Self::Shell => Some(2),
80 Self::Solid | Self::CompSolid => Some(3),
81 Self::Compound => None,
82 }
83 }
84}
85
86#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
90pub enum Orientation {
91 #[default]
93 Forward,
94 Reversed,
96 Internal,
99 External,
102}
103
104impl Orientation {
105 #[must_use]
114 pub const fn compose(self, inner: Self) -> Self {
115 match self {
116 Self::Forward => inner,
117 Self::Reversed => match inner {
118 Self::Forward => Self::Reversed,
119 Self::Reversed => Self::Forward,
120 other => other,
121 },
122 Self::Internal => Self::Internal,
123 Self::External => Self::External,
124 }
125 }
126
127 #[must_use]
132 pub const fn reversed(self) -> Self {
133 match self {
134 Self::Forward => Self::Reversed,
135 Self::Reversed => Self::Forward,
136 other => other,
137 }
138 }
139
140 #[must_use]
142 pub const fn is_boundary(self) -> bool {
143 matches!(self, Self::Forward | Self::Reversed)
144 }
145}
146
147#[derive(Debug, Clone, PartialEq)]
153pub struct TShape {
154 kind: ShapeType,
155 data: NodeData,
156 children: Vec<Shape>,
157}
158
159pub type TShapeId = Key<TShape>;
161
162impl TShape {
163 #[must_use]
165 pub const fn new(kind: ShapeType, data: NodeData, children: Vec<Shape>) -> Self {
166 Self {
167 kind,
168 data,
169 children,
170 }
171 }
172
173 #[must_use]
175 pub const fn container(kind: ShapeType, children: Vec<Shape>) -> Self {
176 Self {
177 kind,
178 data: NodeData::Container,
179 children,
180 }
181 }
182
183 #[must_use]
185 pub const fn leaf(kind: ShapeType, data: NodeData) -> Self {
186 Self {
187 kind,
188 data,
189 children: Vec::new(),
190 }
191 }
192
193 #[must_use]
195 pub const fn kind(&self) -> ShapeType {
196 self.kind
197 }
198
199 #[must_use]
201 pub const fn data(&self) -> &NodeData {
202 &self.data
203 }
204
205 #[must_use]
207 pub const fn data_mut(&mut self) -> &mut NodeData {
208 &mut self.data
209 }
210
211 #[must_use]
213 pub fn children(&self) -> &[Shape] {
214 &self.children
215 }
216
217 pub(crate) const fn children_mut(&mut self) -> &mut Vec<Shape> {
219 &mut self.children
220 }
221
222 #[must_use]
224 pub fn child_count(&self) -> usize {
225 self.children.len()
226 }
227}
228
229#[derive(Debug, Clone)]
234pub struct Shape {
235 node: TShapeId,
236 location: Location,
237 orientation: Orientation,
238}
239
240impl Shape {
241 #[must_use]
243 pub const fn new(node: TShapeId, location: Location, orientation: Orientation) -> Self {
244 Self {
245 node,
246 location,
247 orientation,
248 }
249 }
250
251 #[must_use]
253 pub fn of(node: TShapeId) -> Self {
254 Self::new(node, Location::identity(), Orientation::Forward)
255 }
256
257 #[must_use]
259 pub const fn node(&self) -> TShapeId {
260 self.node
261 }
262
263 #[must_use]
265 pub const fn location(&self) -> &Location {
266 &self.location
267 }
268
269 #[must_use]
271 pub const fn orientation(&self) -> Orientation {
272 self.orientation
273 }
274
275 #[must_use]
277 pub fn located(&self, location: Location) -> Self {
278 Self {
279 location,
280 ..self.clone()
281 }
282 }
283
284 pub(crate) fn rebound(&self, nodes: u32, datums: u32) -> Self {
289 Self {
290 node: self.node.with_scope(nodes),
291 location: self.location.with_datum_scope(datums),
292 orientation: self.orientation,
293 }
294 }
295
296 pub(crate) fn shifted(&self, nodes: u32, datums: u32) -> Self {
303 Self {
304 node: crate::entity::shifted_key(self.node, nodes),
305 location: self.location.with_datum_offset(datums),
306 orientation: self.orientation,
307 }
308 }
309
310 #[must_use]
315 pub fn moved(&self, outer: &Location) -> Self {
316 Self {
317 node: self.node,
318 location: outer.then(&self.location),
319 orientation: self.orientation,
320 }
321 }
322
323 #[must_use]
327 pub fn beneath(&self, outer: &Location, sense: Orientation) -> Self {
328 Self {
329 node: self.node,
330 location: outer.then(&self.location),
331 orientation: sense.compose(self.orientation),
332 }
333 }
334
335 #[must_use]
337 pub fn oriented(&self, orientation: Orientation) -> Self {
338 Self {
339 node: self.node,
340 location: self.location.clone(),
341 orientation,
342 }
343 }
344
345 #[must_use]
347 pub fn reversed(&self) -> Self {
348 self.oriented(self.orientation.reversed())
349 }
350
351 #[must_use]
355 pub fn composed(&self, outer: Orientation) -> Self {
356 self.oriented(outer.compose(self.orientation))
357 }
358
359 pub fn transform(&self, store: &DatumStore) -> OgeomResult<Transform> {
365 self.location.composed(store)
366 }
367
368 #[must_use]
375 pub fn is_partner(&self, other: &Self) -> bool {
376 self.node == other.node
377 }
378
379 #[must_use]
385 pub fn is_same(&self, other: &Self) -> bool {
386 self.node == other.node && self.location == other.location
387 }
388
389 #[must_use]
394 pub fn is_equal(&self, other: &Self) -> bool {
395 self.is_same(other) && self.orientation == other.orientation
396 }
397
398 pub fn is_same_position(
408 &self,
409 other: &Self,
410 store: &DatumStore,
411 tol: Tolerances,
412 ) -> OgeomResult<bool> {
413 Ok(self.node == other.node
414 && self
415 .location
416 .is_same_placement(&other.location, store, tol)?)
417 }
418}
419
420impl PartialEq for Shape {
427 fn eq(&self, other: &Self) -> bool {
428 self.is_equal(other)
429 }
430}
431
432impl Eq for Shape {}
433
434impl Hash for Shape {
435 fn hash<H: Hasher>(&self, state: &mut H) {
436 self.node.hash(state);
437 self.location.hash(state);
438 self.orientation.hash(state);
439 }
440}
441
442#[derive(Debug, Clone)]
451pub struct SameKey(pub Shape);
452
453impl PartialEq for SameKey {
454 fn eq(&self, other: &Self) -> bool {
455 self.0.is_same(&other.0)
456 }
457}
458
459impl Eq for SameKey {}
460
461impl Hash for SameKey {
462 fn hash<H: Hasher>(&self, state: &mut H) {
463 self.0.node.hash(state);
464 self.0.location.hash(state);
465 }
466}
467
468#[derive(Debug, Clone)]
470pub struct PartnerKey(pub Shape);
471
472impl PartialEq for PartnerKey {
473 fn eq(&self, other: &Self) -> bool {
474 self.0.is_partner(&other.0)
475 }
476}
477
478impl Eq for PartnerKey {}
479
480impl Hash for PartnerKey {
481 fn hash<H: Hasher>(&self, state: &mut H) {
482 self.0.node.hash(state);
483 }
484}
485
486#[cfg(test)]
487#[allow(clippy::unwrap_used)]
488mod tests {
489 use super::*;
490 use crate::entity::VertexData;
491 use ogeom_core::Arena;
492 use ogeom_core::FastSet;
493 use ogeom_math::{Point, Vector};
494
495 fn setup() -> (Arena<TShape>, DatumStore, TShapeId, TShapeId, Location) {
496 let mut arena = Arena::new();
497 let a = arena.insert(TShape::leaf(
498 ShapeType::Vertex,
499 NodeData::Vertex(VertexData::new(Point::ORIGIN)),
500 ));
501 let b = arena.insert(TShape::leaf(
502 ShapeType::Vertex,
503 NodeData::Vertex(VertexData::new(Point::ORIGIN)),
504 ));
505 let mut store = DatumStore::new();
506 let datum = store.insert(Transform::translation(Vector::new(1.0, 0.0, 0.0)));
507 (arena, store, a, b, Location::of(datum))
508 }
509
510 #[test]
511 fn orientation_composition_is_a_monoid_with_forward_as_identity() {
512 use Orientation::{External, Forward, Internal, Reversed};
513 for o in [Forward, Reversed, Internal, External] {
514 assert_eq!(Forward.compose(o), o, "forward is a left identity");
515 assert_eq!(o.compose(Forward), o, "and a right identity");
516 }
517 assert_eq!(Reversed.compose(Reversed), Forward);
518 assert_eq!(Reversed.compose(Forward), Reversed);
519 }
520
521 #[test]
522 fn composition_is_associative() {
523 use Orientation::{External, Forward, Internal, Reversed};
524 let all = [Forward, Reversed, Internal, External];
525 for a in all {
526 for b in all {
527 for c in all {
528 assert_eq!(
529 a.compose(b).compose(c),
530 a.compose(b.compose(c)),
531 "{a:?} {b:?} {c:?}"
532 );
533 }
534 }
535 }
536 }
537
538 #[test]
539 fn internal_and_external_absorb_and_do_not_reverse() {
540 use Orientation::{External, Forward, Internal, Reversed};
541 for o in [Forward, Reversed, Internal, External] {
544 assert_eq!(Internal.compose(o), Internal);
545 assert_eq!(External.compose(o), External);
546 }
547 assert_eq!(Internal.reversed(), Internal);
548 assert_eq!(External.reversed(), External);
549 assert!(!Internal.is_boundary());
550 assert!(!External.is_boundary());
551 assert!(Forward.is_boundary() && Reversed.is_boundary());
552 }
553
554 #[test]
555 fn reversal_is_an_involution() {
556 use Orientation::{External, Forward, Internal, Reversed};
557 for o in [Forward, Reversed, Internal, External] {
558 assert_eq!(o.reversed().reversed(), o);
559 }
560 }
561
562 #[test]
563 fn the_identity_trichotomy_distinguishes_three_questions() {
564 let (_, _, a, b, loc) = setup();
565
566 let base = Shape::of(a);
567 let reversed = base.reversed();
568 let moved = base.located(loc.clone());
569 let other_node = Shape::of(b);
570
571 assert!(base.is_partner(&reversed));
573 assert!(base.is_same(&reversed));
574 assert!(
575 !base.is_equal(&reversed),
576 "orientation must distinguish them"
577 );
578
579 assert!(base.is_partner(&moved));
581 assert!(!base.is_same(&moved), "placement must distinguish them");
582 assert!(!base.is_equal(&moved));
583
584 assert!(!base.is_partner(&other_node));
586 assert!(!base.is_same(&other_node));
587 assert!(!base.is_equal(&other_node));
588
589 assert!(base.is_equal(&base) && base.is_same(&base) && base.is_partner(&base));
591 }
592
593 #[test]
594 fn each_key_type_hashes_consistently_with_its_own_equality() {
595 let (_, _, a, _, loc) = setup();
599 let base = Shape::of(a);
600 let reversed = base.reversed();
601 let moved = base.located(loc);
602
603 let equal: FastSet<Shape> = [base.clone(), reversed.clone(), moved.clone()]
604 .into_iter()
605 .collect();
606 assert_eq!(equal.len(), 3, "all three differ under strict equality");
607
608 let same: FastSet<SameKey> = [base.clone(), reversed.clone(), moved.clone()]
609 .into_iter()
610 .map(SameKey)
611 .collect();
612 assert_eq!(same.len(), 2, "orientation is ignored, placement is not");
613
614 let partner: FastSet<PartnerKey> = [base, reversed, moved]
615 .into_iter()
616 .map(PartnerKey)
617 .collect();
618 assert_eq!(partner.len(), 1, "only the node matters");
619 }
620
621 #[test]
622 fn a_shape_placed_and_composed_reports_the_expected_position() {
623 let (_, store, a, _, loc) = setup();
624 let shape = Shape::of(a).located(loc.clone());
625 assert!(
626 shape
627 .transform(&store)
628 .unwrap()
629 .apply(Point::ORIGIN)
630 .is_equal(Point::new(1.0, 0.0, 0.0), Tolerances::millimetres())
631 );
632
633 let nested = shape.moved(&loc);
635 assert!(
636 nested
637 .transform(&store)
638 .unwrap()
639 .apply(Point::ORIGIN)
640 .is_equal(Point::new(2.0, 0.0, 0.0), Tolerances::millimetres())
641 );
642 assert_eq!(nested.location().depth(), 1, "same datum, merged power");
643 }
644
645 #[test]
646 fn positions_reached_by_different_routes_compare_equal() {
647 let mut store = DatumStore::new();
648 let a = store.insert(Transform::translation(Vector::X));
649 let b = store.insert(Transform::translation(Vector::X));
650 let mut arena = Arena::new();
651 let node = arena.insert(TShape::leaf(
652 ShapeType::Vertex,
653 NodeData::Vertex(VertexData::new(Point::ORIGIN)),
654 ));
655
656 let via_a = Shape::of(node).located(Location::of(a));
657 let via_b = Shape::of(node).located(Location::of(b));
658 let tol = Tolerances::millimetres();
659
660 assert!(!via_a.is_same(&via_b), "structurally different chains");
661 assert!(
662 via_a.is_same_position(&via_b, &store, tol).unwrap(),
663 "but the same position"
664 );
665 }
666
667 #[test]
668 fn shape_types_report_their_children_and_dimensions() {
669 use ShapeType::{CompSolid, Compound, Edge, Face, Shell, Solid, Vertex, Wire};
670 assert_eq!(Vertex.child_type(), None);
671 assert_eq!(Edge.child_type(), Some(Vertex));
672 assert_eq!(Face.child_type(), Some(Wire));
673 assert_eq!(Solid.child_type(), Some(Shell));
674 assert_eq!(Compound.child_type(), None, "a compound holds anything");
675
676 assert_eq!(Vertex.dimension(), Some(0));
677 assert_eq!(Wire.dimension(), Some(1));
678 assert_eq!(Shell.dimension(), Some(2));
679 assert_eq!(CompSolid.dimension(), Some(3));
680 assert_eq!(Compound.dimension(), None, "a compound may mix dimensions");
681
682 assert!(Face > Edge && Solid > Face);
684 assert!(Face >= Face);
685 }
686
687 #[test]
688 fn a_node_carries_its_children_in_order() {
689 let (mut arena, _, a, b, _) = setup();
690 let wire = arena.insert(TShape::container(
691 ShapeType::Wire,
692 vec![Shape::of(a), Shape::of(b).reversed()],
693 ));
694 let node = arena.get(wire).unwrap();
695 assert_eq!(node.kind(), ShapeType::Wire);
696 assert_eq!(node.child_count(), 2);
697 assert_eq!(node.children()[1].orientation(), Orientation::Reversed);
698 }
699}