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 location: outer.then(&self.location),
318 ..self.clone()
319 }
320 }
321
322 #[must_use]
324 pub fn oriented(&self, orientation: Orientation) -> Self {
325 Self {
326 orientation,
327 ..self.clone()
328 }
329 }
330
331 #[must_use]
333 pub fn reversed(&self) -> Self {
334 self.oriented(self.orientation.reversed())
335 }
336
337 #[must_use]
341 pub fn composed(&self, outer: Orientation) -> Self {
342 self.oriented(outer.compose(self.orientation))
343 }
344
345 pub fn transform(&self, store: &DatumStore) -> OgeomResult<Transform> {
351 self.location.composed(store)
352 }
353
354 #[must_use]
361 pub fn is_partner(&self, other: &Self) -> bool {
362 self.node == other.node
363 }
364
365 #[must_use]
371 pub fn is_same(&self, other: &Self) -> bool {
372 self.node == other.node && self.location == other.location
373 }
374
375 #[must_use]
380 pub fn is_equal(&self, other: &Self) -> bool {
381 self.is_same(other) && self.orientation == other.orientation
382 }
383
384 pub fn is_same_position(
394 &self,
395 other: &Self,
396 store: &DatumStore,
397 tol: Tolerances,
398 ) -> OgeomResult<bool> {
399 Ok(self.node == other.node
400 && self
401 .location
402 .is_same_placement(&other.location, store, tol)?)
403 }
404}
405
406impl PartialEq for Shape {
413 fn eq(&self, other: &Self) -> bool {
414 self.is_equal(other)
415 }
416}
417
418impl Eq for Shape {}
419
420impl Hash for Shape {
421 fn hash<H: Hasher>(&self, state: &mut H) {
422 self.node.hash(state);
423 self.location.hash(state);
424 self.orientation.hash(state);
425 }
426}
427
428#[derive(Debug, Clone)]
437pub struct SameKey(pub Shape);
438
439impl PartialEq for SameKey {
440 fn eq(&self, other: &Self) -> bool {
441 self.0.is_same(&other.0)
442 }
443}
444
445impl Eq for SameKey {}
446
447impl Hash for SameKey {
448 fn hash<H: Hasher>(&self, state: &mut H) {
449 self.0.node.hash(state);
450 self.0.location.hash(state);
451 }
452}
453
454#[derive(Debug, Clone)]
456pub struct PartnerKey(pub Shape);
457
458impl PartialEq for PartnerKey {
459 fn eq(&self, other: &Self) -> bool {
460 self.0.is_partner(&other.0)
461 }
462}
463
464impl Eq for PartnerKey {}
465
466impl Hash for PartnerKey {
467 fn hash<H: Hasher>(&self, state: &mut H) {
468 self.0.node.hash(state);
469 }
470}
471
472#[cfg(test)]
473#[allow(clippy::unwrap_used)]
474mod tests {
475 use super::*;
476 use crate::entity::VertexData;
477 use ogeom_core::Arena;
478 use ogeom_math::{Point, Vector};
479 use std::collections::HashSet;
480
481 fn setup() -> (Arena<TShape>, DatumStore, TShapeId, TShapeId, Location) {
482 let mut arena = Arena::new();
483 let a = arena.insert(TShape::leaf(
484 ShapeType::Vertex,
485 NodeData::Vertex(VertexData::new(Point::ORIGIN)),
486 ));
487 let b = arena.insert(TShape::leaf(
488 ShapeType::Vertex,
489 NodeData::Vertex(VertexData::new(Point::ORIGIN)),
490 ));
491 let mut store = DatumStore::new();
492 let datum = store.insert(Transform::translation(Vector::new(1.0, 0.0, 0.0)));
493 (arena, store, a, b, Location::of(datum))
494 }
495
496 #[test]
497 fn orientation_composition_is_a_monoid_with_forward_as_identity() {
498 use Orientation::{External, Forward, Internal, Reversed};
499 for o in [Forward, Reversed, Internal, External] {
500 assert_eq!(Forward.compose(o), o, "forward is a left identity");
501 assert_eq!(o.compose(Forward), o, "and a right identity");
502 }
503 assert_eq!(Reversed.compose(Reversed), Forward);
504 assert_eq!(Reversed.compose(Forward), Reversed);
505 }
506
507 #[test]
508 fn composition_is_associative() {
509 use Orientation::{External, Forward, Internal, Reversed};
510 let all = [Forward, Reversed, Internal, External];
511 for a in all {
512 for b in all {
513 for c in all {
514 assert_eq!(
515 a.compose(b).compose(c),
516 a.compose(b.compose(c)),
517 "{a:?} {b:?} {c:?}"
518 );
519 }
520 }
521 }
522 }
523
524 #[test]
525 fn internal_and_external_absorb_and_do_not_reverse() {
526 use Orientation::{External, Forward, Internal, Reversed};
527 for o in [Forward, Reversed, Internal, External] {
530 assert_eq!(Internal.compose(o), Internal);
531 assert_eq!(External.compose(o), External);
532 }
533 assert_eq!(Internal.reversed(), Internal);
534 assert_eq!(External.reversed(), External);
535 assert!(!Internal.is_boundary());
536 assert!(!External.is_boundary());
537 assert!(Forward.is_boundary() && Reversed.is_boundary());
538 }
539
540 #[test]
541 fn reversal_is_an_involution() {
542 use Orientation::{External, Forward, Internal, Reversed};
543 for o in [Forward, Reversed, Internal, External] {
544 assert_eq!(o.reversed().reversed(), o);
545 }
546 }
547
548 #[test]
549 fn the_identity_trichotomy_distinguishes_three_questions() {
550 let (_, _, a, b, loc) = setup();
551
552 let base = Shape::of(a);
553 let reversed = base.reversed();
554 let moved = base.located(loc.clone());
555 let other_node = Shape::of(b);
556
557 assert!(base.is_partner(&reversed));
559 assert!(base.is_same(&reversed));
560 assert!(
561 !base.is_equal(&reversed),
562 "orientation must distinguish them"
563 );
564
565 assert!(base.is_partner(&moved));
567 assert!(!base.is_same(&moved), "placement must distinguish them");
568 assert!(!base.is_equal(&moved));
569
570 assert!(!base.is_partner(&other_node));
572 assert!(!base.is_same(&other_node));
573 assert!(!base.is_equal(&other_node));
574
575 assert!(base.is_equal(&base) && base.is_same(&base) && base.is_partner(&base));
577 }
578
579 #[test]
580 fn each_key_type_hashes_consistently_with_its_own_equality() {
581 let (_, _, a, _, loc) = setup();
585 let base = Shape::of(a);
586 let reversed = base.reversed();
587 let moved = base.located(loc);
588
589 let equal: HashSet<Shape> = [base.clone(), reversed.clone(), moved.clone()]
590 .into_iter()
591 .collect();
592 assert_eq!(equal.len(), 3, "all three differ under strict equality");
593
594 let same: HashSet<SameKey> = [base.clone(), reversed.clone(), moved.clone()]
595 .into_iter()
596 .map(SameKey)
597 .collect();
598 assert_eq!(same.len(), 2, "orientation is ignored, placement is not");
599
600 let partner: HashSet<PartnerKey> = [base, reversed, moved]
601 .into_iter()
602 .map(PartnerKey)
603 .collect();
604 assert_eq!(partner.len(), 1, "only the node matters");
605 }
606
607 #[test]
608 fn a_shape_placed_and_composed_reports_the_expected_position() {
609 let (_, store, a, _, loc) = setup();
610 let shape = Shape::of(a).located(loc.clone());
611 assert!(
612 shape
613 .transform(&store)
614 .unwrap()
615 .apply(Point::ORIGIN)
616 .is_equal(Point::new(1.0, 0.0, 0.0), Tolerances::millimetres())
617 );
618
619 let nested = shape.moved(&loc);
621 assert!(
622 nested
623 .transform(&store)
624 .unwrap()
625 .apply(Point::ORIGIN)
626 .is_equal(Point::new(2.0, 0.0, 0.0), Tolerances::millimetres())
627 );
628 assert_eq!(nested.location().depth(), 1, "same datum, merged power");
629 }
630
631 #[test]
632 fn positions_reached_by_different_routes_compare_equal() {
633 let mut store = DatumStore::new();
634 let a = store.insert(Transform::translation(Vector::X));
635 let b = store.insert(Transform::translation(Vector::X));
636 let mut arena = Arena::new();
637 let node = arena.insert(TShape::leaf(
638 ShapeType::Vertex,
639 NodeData::Vertex(VertexData::new(Point::ORIGIN)),
640 ));
641
642 let via_a = Shape::of(node).located(Location::of(a));
643 let via_b = Shape::of(node).located(Location::of(b));
644 let tol = Tolerances::millimetres();
645
646 assert!(!via_a.is_same(&via_b), "structurally different chains");
647 assert!(
648 via_a.is_same_position(&via_b, &store, tol).unwrap(),
649 "but the same position"
650 );
651 }
652
653 #[test]
654 fn shape_types_report_their_children_and_dimensions() {
655 use ShapeType::{CompSolid, Compound, Edge, Face, Shell, Solid, Vertex, Wire};
656 assert_eq!(Vertex.child_type(), None);
657 assert_eq!(Edge.child_type(), Some(Vertex));
658 assert_eq!(Face.child_type(), Some(Wire));
659 assert_eq!(Solid.child_type(), Some(Shell));
660 assert_eq!(Compound.child_type(), None, "a compound holds anything");
661
662 assert_eq!(Vertex.dimension(), Some(0));
663 assert_eq!(Wire.dimension(), Some(1));
664 assert_eq!(Shell.dimension(), Some(2));
665 assert_eq!(CompSolid.dimension(), Some(3));
666 assert_eq!(Compound.dimension(), None, "a compound may mix dimensions");
667
668 assert!(Face > Edge && Solid > Face);
670 assert!(Face >= Face);
671 }
672
673 #[test]
674 fn a_node_carries_its_children_in_order() {
675 let (mut arena, _, a, b, _) = setup();
676 let wire = arena.insert(TShape::container(
677 ShapeType::Wire,
678 vec![Shape::of(a), Shape::of(b).reversed()],
679 ));
680 let node = arena.get(wire).unwrap();
681 assert_eq!(node.kind(), ShapeType::Wire);
682 assert_eq!(node.child_count(), 2);
683 assert_eq!(node.children()[1].orientation(), Orientation::Reversed);
684 }
685}