1use ogeom_core::FastMap;
16use ogeom_core::{OgeomResult, ogeom_bail};
17use ogeom_math::Transform;
18use ogeom_topo::{Location, Model, Shape, TShapeId};
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
22pub struct ProductId(u32);
23
24impl ProductId {
25 #[must_use]
28 pub const fn index(self) -> u32 {
29 self.0
30 }
31}
32
33#[derive(Debug, Clone, Copy, PartialEq)]
35pub struct Colour {
36 pub r: f64,
38 pub g: f64,
40 pub b: f64,
42 pub a: f64,
44}
45
46impl Colour {
47 #[must_use]
49 pub const fn rgb(r: f64, g: f64, b: f64) -> Self {
50 Self { r, g, b, a: 1.0 }
51 }
52}
53
54#[derive(Debug, Clone)]
57pub struct Instance {
58 pub product: ProductId,
60 pub location: Location,
62 pub name: Option<String>,
64}
65
66#[derive(Debug, Clone)]
68pub enum ProductKind {
69 Part {
71 shape: Shape,
73 },
74 Assembly {
76 children: Vec<Instance>,
78 },
79}
80
81#[derive(Debug, Clone)]
83pub struct Product {
84 pub name: String,
86 pub colour: Option<Colour>,
88 pub kind: ProductKind,
90}
91
92#[derive(Debug, Clone)]
94pub struct Occurrence {
95 pub part: ProductId,
97 pub shape: Shape,
99 pub path: String,
102}
103
104#[derive(Debug, Default)]
111pub struct Document {
112 model: Model,
113 products: Vec<Product>,
114 colours: FastMap<TShapeId, Colour>,
115 names: FastMap<TShapeId, String>,
116 pmi: crate::pmi::Pmi,
117 properties: FastMap<TShapeId, Vec<crate::attributes::Property>>,
118 materials: Vec<crate::attributes::Material>,
119 material_of: FastMap<TShapeId, crate::attributes::MaterialId>,
120 layers: Vec<crate::attributes::Layer>,
121 on_layer: FastMap<TShapeId, Vec<crate::attributes::LayerId>>,
122 validation: FastMap<TShapeId, crate::attributes::ValidationProperties>,
123 textures: Vec<crate::attributes::Texture>,
124 texture_of: FastMap<TShapeId, crate::attributes::TextureId>,
125 views: Vec<crate::view::View>,
126 notes: Vec<crate::view::Note>,
127 construction: Vec<crate::construction::Construction>,
128 history: Vec<State>,
131 undone: usize,
133 undo_limit: Option<usize>,
136}
137
138#[derive(Debug, Clone, Default)]
148struct State {
149 products: Vec<Product>,
150 colours: FastMap<TShapeId, Colour>,
151 names: FastMap<TShapeId, String>,
152 pmi: crate::pmi::Pmi,
153 properties: FastMap<TShapeId, Vec<crate::attributes::Property>>,
154 materials: Vec<crate::attributes::Material>,
155 material_of: FastMap<TShapeId, crate::attributes::MaterialId>,
156 layers: Vec<crate::attributes::Layer>,
157 on_layer: FastMap<TShapeId, Vec<crate::attributes::LayerId>>,
158 validation: FastMap<TShapeId, crate::attributes::ValidationProperties>,
159 textures: Vec<crate::attributes::Texture>,
160 texture_of: FastMap<TShapeId, crate::attributes::TextureId>,
161 views: Vec<crate::view::View>,
162 notes: Vec<crate::view::Note>,
163 construction: Vec<crate::construction::Construction>,
164}
165
166impl Document {
167 #[must_use]
169 pub fn new() -> Self {
170 Self::default()
171 }
172
173 #[must_use]
175 pub fn over(model: Model) -> Self {
176 Self {
177 model,
178 ..Self::default()
179 }
180 }
181
182 #[must_use]
184 pub const fn model(&self) -> &Model {
185 &self.model
186 }
187
188 pub const fn model_mut(&mut self) -> &mut Model {
190 &mut self.model
191 }
192
193 pub fn add_part(&mut self, name: impl Into<String>, shape: Shape) -> ProductId {
195 self.push(Product {
196 name: name.into(),
197 colour: None,
198 kind: ProductKind::Part { shape },
199 })
200 }
201
202 pub fn add_assembly(&mut self, name: impl Into<String>) -> ProductId {
204 self.push(Product {
205 name: name.into(),
206 colour: None,
207 kind: ProductKind::Assembly {
208 children: Vec::new(),
209 },
210 })
211 }
212
213 fn push(&mut self, product: Product) -> ProductId {
214 self.products.push(product);
215 #[allow(clippy::cast_possible_truncation)]
216 ProductId(self.products.len() as u32 - 1)
217 }
218
219 pub fn add_instance(
233 &mut self,
234 assembly: ProductId,
235 product: ProductId,
236 at: Transform,
237 name: Option<String>,
238 ) -> OgeomResult<()> {
239 if self.get(product).is_none() {
240 ogeom_bail!(Dangling, "the product to place is not in this document");
241 }
242 if self.contains_product(product, assembly) {
243 ogeom_bail!(
244 Construction,
245 "placing this product here would make it contain itself"
246 );
247 }
248 let location = if at == Transform::IDENTITY {
249 Location::identity()
250 } else {
251 Location::of(self.model.add_datum(at))
252 };
253 let Some(entry) = self.products.get_mut(assembly.0 as usize) else {
254 ogeom_bail!(Dangling, "the assembly is not in this document");
255 };
256 let ProductKind::Assembly { children } = &mut entry.kind else {
257 ogeom_bail!(Construction, "instances go inside assemblies, not parts");
258 };
259 children.push(Instance {
260 product,
261 location,
262 name,
263 });
264 Ok(())
265 }
266
267 pub fn add_instance_at(
277 &mut self,
278 assembly: ProductId,
279 product: ProductId,
280 location: Location,
281 name: Option<String>,
282 ) -> OgeomResult<()> {
283 if self.get(product).is_none() {
284 ogeom_bail!(Dangling, "the product to place is not in this document");
285 }
286 if self.contains_product(product, assembly) {
287 ogeom_bail!(
288 Construction,
289 "placing this product here would make it contain itself"
290 );
291 }
292 let Some(entry) = self.products.get_mut(assembly.0 as usize) else {
293 ogeom_bail!(Dangling, "the assembly is not in this document");
294 };
295 let ProductKind::Assembly { children } = &mut entry.kind else {
296 ogeom_bail!(Construction, "instances go inside assemblies, not parts");
297 };
298 children.push(Instance {
299 product,
300 location,
301 name,
302 });
303 Ok(())
304 }
305
306 fn contains_product(&self, haystack: ProductId, needle: ProductId) -> bool {
312 let mut seen = ogeom_core::FastSet::default();
313 let mut stack = vec![haystack];
314 while let Some(product) = stack.pop() {
315 if product == needle {
316 return true;
317 }
318 if !seen.insert(product) {
319 continue;
320 }
321 if let Some(ProductKind::Assembly { children }) = self.get(product).map(|p| &p.kind) {
322 stack.extend(children.iter().map(|i| i.product));
323 }
324 }
325 false
326 }
327
328 #[must_use]
330 pub fn get(&self, id: ProductId) -> Option<&Product> {
331 self.products.get(id.0 as usize)
332 }
333
334 pub fn products(&self) -> impl Iterator<Item = (ProductId, &Product)> {
336 self.products
337 .iter()
338 .enumerate()
339 .map(|(i, p)| (ProductId(u32::try_from(i).unwrap_or(u32::MAX)), p))
340 }
341
342 #[must_use]
344 pub fn roots(&self) -> Vec<ProductId> {
345 let mut placed = vec![false; self.products.len()];
346 for product in &self.products {
347 if let ProductKind::Assembly { children } = &product.kind {
348 for instance in children {
349 placed[instance.product.0 as usize] = true;
350 }
351 }
352 }
353 placed
354 .iter()
355 .enumerate()
356 .filter(|&(_, &used)| !used)
357 .map(|(i, _)| ProductId(u32::try_from(i).unwrap_or(u32::MAX)))
358 .collect()
359 }
360
361 pub fn occurrences_of(&self, product: ProductId) -> OgeomResult<Vec<Occurrence>> {
378 let Some(root) = self.get(product) else {
379 ogeom_bail!(Dangling, "the product is not in this document");
380 };
381 let mut out = Vec::new();
382 self.flatten(
383 product,
384 root,
385 &Location::identity(),
386 &root.name.clone(),
387 &mut out,
388 );
389 Ok(out)
390 }
391
392 fn flatten(
393 &self,
394 id: ProductId,
395 product: &Product,
396 above: &Location,
397 path: &str,
398 out: &mut Vec<Occurrence>,
399 ) {
400 match &product.kind {
401 ProductKind::Part { shape } => out.push(Occurrence {
402 part: id,
403 shape: shape.moved(above),
404 path: path.to_string(),
405 }),
406 ProductKind::Assembly { children } => {
407 for instance in children {
408 let Some(child) = self.get(instance.product) else {
409 continue;
410 };
411 let below = above.then(&instance.location);
412 let step = instance.name.as_deref().unwrap_or(&child.name);
413 let path = format!("{path}/{step}");
414 self.flatten(instance.product, child, &below, &path, out);
415 }
416 }
417 }
418 }
419
420 pub fn set_product_colour(&mut self, product: ProductId, colour: Colour) -> OgeomResult<()> {
427 let Some(entry) = self.products.get_mut(product.0 as usize) else {
428 ogeom_bail!(Dangling, "the product is not in this document");
429 };
430 entry.colour = Some(colour);
431 Ok(())
432 }
433
434 pub fn add_texture(
436 &mut self,
437 texture: crate::attributes::Texture,
438 ) -> crate::attributes::TextureId {
439 self.textures.push(texture);
440 crate::attributes::TextureId(self.textures.len() - 1)
441 }
442
443 pub fn set_texture(&mut self, shape: &Shape, texture: crate::attributes::TextureId) {
445 self.texture_of.insert(shape.node(), texture);
446 }
447
448 #[must_use]
450 pub fn texture_of(&self, shape: &Shape) -> Option<&crate::attributes::Texture> {
451 let id = self.texture_of.get(&shape.node())?;
452 self.textures.get(id.0)
453 }
454
455 #[must_use]
457 pub fn textures(&self) -> &[crate::attributes::Texture] {
458 &self.textures
459 }
460
461 pub fn checkpoint(&mut self) {
467 if self.undone > 0 {
468 let keep = self.history.len() - self.undone;
469 self.history.truncate(keep);
470 self.undone = 0;
471 }
472 let state = self.state();
473 self.history.push(state);
474 self.keep_to_limit();
475 }
476
477 pub const DEFAULT_UNDO_LIMIT: usize = 256;
481
482 pub fn set_undo_limit(&mut self, limit: usize) {
484 self.undo_limit = Some(limit.max(1));
485 self.keep_to_limit();
486 }
487
488 fn keep_to_limit(&mut self) {
489 let limit = self.undo_limit.unwrap_or(Self::DEFAULT_UNDO_LIMIT);
490 let behind = self.history.len() - self.undone;
493 if behind > limit {
494 self.history.drain(..behind - limit);
495 }
496 }
497
498 pub fn undo(&mut self) -> bool {
500 if self.history.len() <= self.undone {
501 return false;
502 }
503 let index = self.history.len() - self.undone - 1;
506 let current = self.state();
507 let restored = core::mem::replace(&mut self.history[index], current);
508 self.apply(restored);
509 self.undone += 1;
510 true
511 }
512
513 pub fn redo(&mut self) -> bool {
515 if self.undone == 0 {
516 return false;
517 }
518 let index = self.history.len() - self.undone;
519 let current = self.state();
520 let restored = core::mem::replace(&mut self.history[index], current);
521 self.apply(restored);
522 self.undone -= 1;
523 true
524 }
525
526 #[must_use]
528 pub fn undo_depth(&self) -> (usize, usize) {
529 (self.history.len() - self.undone, self.undone)
530 }
531
532 #[must_use]
535 pub fn product_index(&self, id: ProductId) -> usize {
536 id.index() as usize
537 }
538
539 pub fn add_view(&mut self, view: crate::view::View) -> usize {
542 self.views.push(view);
543 self.views.len() - 1
544 }
545
546 #[must_use]
548 pub fn views(&self) -> &[crate::view::View] {
549 &self.views
550 }
551
552 pub fn add_note(&mut self, note: crate::view::Note) -> usize {
554 self.notes.push(note);
555 self.notes.len() - 1
556 }
557
558 #[must_use]
560 pub fn notes(&self) -> &[crate::view::Note] {
561 &self.notes
562 }
563
564 pub fn add_construction(
575 &mut self,
576 product: ProductId,
577 name: impl Into<String>,
578 geometry: crate::construction::ConstructionGeometry,
579 ) -> OgeomResult<usize> {
580 use crate::construction::ConstructionGeometry as G;
581 if self.get(product).is_none() {
582 ogeom_bail!(Dangling, "the product is not in this document");
583 }
584 let stretch = |(lo, hi): (f64, f64)| lo.is_finite() && hi.is_finite() && lo < hi;
585 let finite = |p: ogeom_math::Point| p.x.is_finite() && p.y.is_finite() && p.z.is_finite();
586 let sound = match &geometry {
587 G::Point(p) => finite(*p),
588 G::Axis(frame) => finite(frame.origin()),
589 G::Curve { range, .. } => stretch(*range),
590 G::Surface { u, v, .. } => stretch(*u) && stretch(*v),
591 };
592 if !sound {
593 ogeom_bail!(
594 Construction,
595 "a construction item's coordinates, range or window are not finite and increasing"
596 );
597 }
598 self.construction.push(crate::construction::Construction {
599 name: name.into(),
600 geometry,
601 product,
602 });
603 Ok(self.construction.len() - 1)
604 }
605
606 #[must_use]
608 pub fn construction(&self) -> &[crate::construction::Construction] {
609 &self.construction
610 }
611
612 pub fn construction_of(
614 &self,
615 product: ProductId,
616 ) -> impl Iterator<Item = &crate::construction::Construction> {
617 self.construction
618 .iter()
619 .filter(move |item| item.product == product)
620 }
621
622 fn state(&self) -> State {
623 State {
624 products: self.products.clone(),
625 colours: self.colours.clone(),
626 names: self.names.clone(),
627 pmi: self.pmi.clone(),
628 properties: self.properties.clone(),
629 materials: self.materials.clone(),
630 material_of: self.material_of.clone(),
631 layers: self.layers.clone(),
632 on_layer: self.on_layer.clone(),
633 validation: self.validation.clone(),
634 textures: self.textures.clone(),
635 texture_of: self.texture_of.clone(),
636 views: self.views.clone(),
637 notes: self.notes.clone(),
638 construction: self.construction.clone(),
639 }
640 }
641
642 fn apply(&mut self, state: State) {
644 self.products = state.products;
645 self.colours = state.colours;
646 self.names = state.names;
647 self.pmi = state.pmi;
648 self.properties = state.properties;
649 self.materials = state.materials;
650 self.material_of = state.material_of;
651 self.layers = state.layers;
652 self.on_layer = state.on_layer;
653 self.validation = state.validation;
654 self.textures = state.textures;
655 self.texture_of = state.texture_of;
656 self.views = state.views;
657 self.notes = state.notes;
658 self.construction = state.construction;
659 }
660
661 pub fn set_colour(&mut self, shape: &Shape, colour: Colour) {
667 self.colours.insert(shape.node(), colour);
668 }
669
670 #[must_use]
672 pub fn colour_of(&self, shape: &Shape) -> Option<Colour> {
673 self.colours.get(&shape.node()).copied()
674 }
675
676 #[must_use]
681 pub fn resolved_colour(&self, part: ProductId, sub: &Shape) -> Option<Colour> {
682 if let Some(own) = self.colour_of(sub) {
683 return Some(own);
684 }
685 let product = self.get(part)?;
686 if let ProductKind::Part { shape } = &product.kind
687 && let Some(whole) = self.colour_of(shape)
688 {
689 return Some(whole);
690 }
691 product.colour
692 }
693
694 pub fn set_name(&mut self, shape: &Shape, name: impl Into<String>) {
696 self.names.insert(shape.node(), name.into());
697 }
698
699 #[must_use]
701 pub fn name_of(&self, shape: &Shape) -> Option<&str> {
702 self.names.get(&shape.node()).map(String::as_str)
703 }
704
705 pub fn colours(&self) -> impl Iterator<Item = (TShapeId, Colour)> + '_ {
707 self.colours.iter().map(|(&node, &colour)| (node, colour))
708 }
709
710 pub fn names(&self) -> impl Iterator<Item = (TShapeId, &str)> {
712 self.names.iter().map(|(&node, name)| (node, name.as_str()))
713 }
714
715 pub fn replace_part_shape(&mut self, product: ProductId, shape: Shape) -> OgeomResult<()> {
728 let Some(entry) = self.products.get_mut(product.0 as usize) else {
729 ogeom_bail!(Dangling, "the product is not in this document");
730 };
731 let ProductKind::Part { shape: slot } = &mut entry.kind else {
732 ogeom_bail!(Construction, "only a part carries a shape to replace");
733 };
734 *slot = shape;
735 Ok(())
736 }
737
738 #[must_use]
740 pub const fn pmi(&self) -> &crate::pmi::Pmi {
741 &self.pmi
742 }
743
744 pub const fn pmi_mut(&mut self) -> &mut crate::pmi::Pmi {
746 &mut self.pmi
747 }
748
749 pub fn set_property(&mut self, shape: &Shape, property: crate::attributes::Property) {
754 let list = self.properties.entry(shape.node()).or_default();
755 if let Some(held) = list.iter_mut().find(|p| p.name == property.name) {
756 *held = property;
757 } else {
758 list.push(property);
759 }
760 }
761
762 #[must_use]
764 pub fn properties_of(&self, shape: &Shape) -> &[crate::attributes::Property] {
765 self.properties
766 .get(&shape.node())
767 .map_or(&[], Vec::as_slice)
768 }
769
770 pub fn properties(&self) -> impl Iterator<Item = (TShapeId, &[crate::attributes::Property])> {
772 self.properties.iter().map(|(k, v)| (*k, v.as_slice()))
773 }
774
775 pub fn add_material(
777 &mut self,
778 material: crate::attributes::Material,
779 ) -> crate::attributes::MaterialId {
780 self.materials.push(material);
781 crate::attributes::MaterialId(self.materials.len() - 1)
782 }
783
784 #[must_use]
786 pub fn material(
787 &self,
788 id: crate::attributes::MaterialId,
789 ) -> Option<&crate::attributes::Material> {
790 self.materials.get(id.0)
791 }
792
793 #[must_use]
795 pub fn materials(&self) -> &[crate::attributes::Material] {
796 &self.materials
797 }
798
799 #[must_use]
801 pub fn material_id(&self, index: usize) -> Option<crate::attributes::MaterialId> {
802 (index < self.materials.len()).then_some(crate::attributes::MaterialId(index))
803 }
804
805 pub fn assign_material(&mut self, shape: &Shape, id: crate::attributes::MaterialId) {
807 self.material_of.insert(shape.node(), id);
808 }
809
810 #[must_use]
812 pub fn material_of(&self, shape: &Shape) -> Option<crate::attributes::MaterialId> {
813 self.material_of.get(&shape.node()).copied()
814 }
815
816 pub fn material_assignments(
818 &self,
819 ) -> impl Iterator<Item = (TShapeId, crate::attributes::MaterialId)> + '_ {
820 self.material_of.iter().map(|(k, v)| (*k, *v))
821 }
822
823 pub fn add_layer(&mut self, name: impl Into<String>) -> crate::attributes::LayerId {
825 self.layers.push(crate::attributes::Layer {
826 name: name.into(),
827 visible: true,
828 });
829 crate::attributes::LayerId(self.layers.len() - 1)
830 }
831
832 #[must_use]
834 pub fn layer(&self, id: crate::attributes::LayerId) -> Option<&crate::attributes::Layer> {
835 self.layers.get(id.0)
836 }
837
838 #[must_use]
840 pub fn layers(&self) -> &[crate::attributes::Layer] {
841 &self.layers
842 }
843
844 #[must_use]
846 pub fn layer_id(&self, index: usize) -> Option<crate::attributes::LayerId> {
847 (index < self.layers.len()).then_some(crate::attributes::LayerId(index))
848 }
849
850 pub fn set_layer_visible(&mut self, id: crate::attributes::LayerId, visible: bool) {
852 if let Some(layer) = self.layers.get_mut(id.0) {
853 layer.visible = visible;
854 }
855 }
856
857 pub fn place_on_layer(&mut self, shape: &Shape, layer: crate::attributes::LayerId) {
859 let list = self.on_layer.entry(shape.node()).or_default();
860 if !list.contains(&layer) {
861 list.push(layer);
862 }
863 }
864
865 #[must_use]
867 pub fn layers_of(&self, shape: &Shape) -> &[crate::attributes::LayerId] {
868 self.on_layer.get(&shape.node()).map_or(&[], Vec::as_slice)
869 }
870
871 pub fn layer_memberships(
873 &self,
874 ) -> impl Iterator<Item = (TShapeId, &[crate::attributes::LayerId])> {
875 self.on_layer.iter().map(|(k, v)| (*k, v.as_slice()))
876 }
877
878 pub fn set_validation(
880 &mut self,
881 shape: &Shape,
882 values: crate::attributes::ValidationProperties,
883 ) {
884 self.validation.insert(shape.node(), values);
885 }
886
887 #[must_use]
889 pub fn validation_of(&self, shape: &Shape) -> Option<crate::attributes::ValidationProperties> {
890 self.validation.get(&shape.node()).copied()
891 }
892
893 pub fn validations(
895 &self,
896 ) -> impl Iterator<Item = (TShapeId, crate::attributes::ValidationProperties)> + '_ {
897 self.validation.iter().map(|(k, v)| (*k, *v))
898 }
899}
900
901#[cfg(test)]
902#[allow(clippy::unwrap_used, clippy::expect_used)]
903mod tests {
904 use super::*;
905 use ogeom_core::Tolerances;
906 use ogeom_math::{Frame, Point, Vector};
907 use ogeom_topo::{Filter, ShapeType, explore};
908
909 const T: Tolerances = Tolerances::millimetres();
910
911 fn box_part(document: &mut Document, name: &str, size: f64) -> (ProductId, Shape) {
912 let shape = ogeom_algo::make_box(document.model_mut(), Frame::WORLD, (size, size, size), T)
913 .unwrap()
914 .shape;
915 (document.add_part(name, shape.clone()), shape)
916 }
917
918 #[test]
922 fn undo_keeps_only_its_most_recent_checkpoints() {
923 let mut doc = Document::new();
924 doc.set_undo_limit(3);
925 for k in 0..10 {
926 doc.checkpoint();
927 doc.names
928 .insert(ogeom_topo::TShapeId::from_parts(k, 0), format!("state {k}"));
929 }
930 assert_eq!(doc.undo_depth(), (3, 0));
931 let mut steps = 0;
932 while doc.undo() {
933 steps += 1;
934 }
935 assert_eq!(steps, 3);
936 }
937
938 #[test]
939 fn two_instances_of_one_part_share_the_node_and_differ_in_placement() {
940 let mut document = Document::new();
941 let (bolt, shape) = box_part(&mut document, "bolt", 1.0);
942 let assembly = document.add_assembly("plate");
943 document
944 .add_instance(assembly, bolt, Transform::IDENTITY, Some("bolt-1".into()))
945 .unwrap();
946 document
947 .add_instance(
948 assembly,
949 bolt,
950 Transform::translation(Vector::new(10.0, 0.0, 0.0)),
951 Some("bolt-2".into()),
952 )
953 .unwrap();
954
955 let occurrences = document.occurrences_of(assembly).unwrap();
956 assert_eq!(occurrences.len(), 2);
957 assert_eq!(occurrences[0].path, "plate/bolt-1");
958 assert_eq!(occurrences[1].path, "plate/bolt-2");
959 assert_eq!(occurrences[0].shape.node(), shape.node());
961 assert_eq!(occurrences[1].shape.node(), shape.node());
962 let at = |o: &Occurrence| {
963 o.shape
964 .transform(document.model().datums())
965 .unwrap()
966 .apply(Point::new(0.0, 0.0, 0.0))
967 };
968 assert!(at(&occurrences[0]).is_equal(Point::new(0.0, 0.0, 0.0), T));
969 assert!(at(&occurrences[1]).is_equal(Point::new(10.0, 0.0, 0.0), T));
970 }
971
972 #[test]
973 fn nested_placements_compose_outer_then_inner() {
974 let mut document = Document::new();
975 let (part, _) = box_part(&mut document, "washer", 1.0);
976 let sub = document.add_assembly("stack");
977 let top = document.add_assembly("machine");
978 document
979 .add_instance(
980 sub,
981 part,
982 Transform::translation(Vector::new(0.0, 5.0, 0.0)),
983 None,
984 )
985 .unwrap();
986 document
987 .add_instance(
988 top,
989 sub,
990 Transform::translation(Vector::new(100.0, 0.0, 0.0)),
991 None,
992 )
993 .unwrap();
994
995 let occurrences = document.occurrences_of(top).unwrap();
996 assert_eq!(occurrences.len(), 1);
997 assert_eq!(occurrences[0].path, "machine/stack/washer");
998 let world = occurrences[0]
999 .shape
1000 .transform(document.model().datums())
1001 .unwrap()
1002 .apply(Point::new(0.0, 0.0, 0.0));
1003 assert!(world.is_equal(Point::new(100.0, 5.0, 0.0), T));
1004 }
1005
1006 #[test]
1011 fn a_part_sub_shape_moved_by_the_placement_above_is_in_each_occurrence() {
1012 let mut document = Document::new();
1013 let (_, shape) = box_part(&mut document, "washer", 1.0);
1014 let shape = document
1015 .model_mut()
1016 .placed(&shape, Transform::translation(Vector::new(0.0, 0.0, 3.0)));
1017 let part = document.add_part("lifted", shape.clone());
1018 let sub = document.add_assembly("stack");
1019 let top = document.add_assembly("machine");
1020 for y in [0.0, 5.0] {
1021 document
1022 .add_instance(
1023 sub,
1024 part,
1025 Transform::translation(Vector::new(0.0, y, 0.0)),
1026 None,
1027 )
1028 .unwrap();
1029 }
1030 document
1031 .add_instance(
1032 top,
1033 sub,
1034 Transform::translation(Vector::new(100.0, 0.0, 0.0)),
1035 None,
1036 )
1037 .unwrap();
1038
1039 let occurrences = document.occurrences_of(top).unwrap();
1040 assert_eq!(occurrences.len(), 2);
1041 let model = document.model();
1042 for occurrence in &occurrences {
1043 let by = occurrence
1044 .shape
1045 .location()
1046 .then(&shape.location().inverted());
1047 for kind in [ShapeType::Face, ShapeType::Edge, ShapeType::Vertex] {
1048 let there = ogeom_topo::explore_unique(model, &occurrence.shape, kind).unwrap();
1049 for s in ogeom_topo::explore_unique(model, &shape, kind).unwrap() {
1050 let moved = s.moved(&by);
1051 assert_eq!(there.iter().filter(|t| t.is_same(&moved)).count(), 1);
1052 }
1053 }
1054 }
1055 }
1056
1057 #[test]
1058 fn a_product_cannot_contain_itself() {
1059 let mut document = Document::new();
1060 let a = document.add_assembly("a");
1061 let b = document.add_assembly("b");
1062 document
1063 .add_instance(a, b, Transform::IDENTITY, None)
1064 .unwrap();
1065 let err = document.add_instance(b, a, Transform::IDENTITY, None);
1066 assert!(err.is_err(), "a cycle must be refused");
1067 let direct = document.add_instance(a, a, Transform::IDENTITY, None);
1068 assert!(direct.is_err(), "self-containment must be refused");
1069 }
1070
1071 #[test]
1072 fn instances_go_only_inside_assemblies() {
1073 let mut document = Document::new();
1074 let (part, _) = box_part(&mut document, "block", 1.0);
1075 let (other, _) = box_part(&mut document, "pin", 1.0);
1076 assert!(
1077 document
1078 .add_instance(part, other, Transform::IDENTITY, None)
1079 .is_err()
1080 );
1081 }
1082
1083 #[test]
1084 fn roots_are_the_products_nothing_places() {
1085 let mut document = Document::new();
1086 let (part, _) = box_part(&mut document, "gear", 1.0);
1087 let assembly = document.add_assembly("gearbox");
1088 document
1089 .add_instance(assembly, part, Transform::IDENTITY, None)
1090 .unwrap();
1091 assert_eq!(document.roots(), vec![assembly]);
1092 }
1093
1094 #[test]
1095 fn colour_resolution_walks_sub_shape_then_shape_then_product() {
1096 let mut document = Document::new();
1097 let (part, shape) = box_part(&mut document, "housing", 2.0);
1098 let face = explore(document.model(), &shape, Filter::OfType(ShapeType::Face))
1099 .unwrap()
1100 .remove(0);
1101
1102 let red = Colour::rgb(1.0, 0.0, 0.0);
1103 let green = Colour::rgb(0.0, 1.0, 0.0);
1104 let blue = Colour::rgb(0.0, 0.0, 1.0);
1105
1106 assert_eq!(document.resolved_colour(part, &face), None);
1107 document.set_product_colour(part, blue).unwrap();
1108 assert_eq!(document.resolved_colour(part, &face), Some(blue));
1109 document.set_colour(&shape, green);
1110 assert_eq!(document.resolved_colour(part, &face), Some(green));
1111 document.set_colour(&face, red);
1112 assert_eq!(document.resolved_colour(part, &face), Some(red));
1113 assert_eq!(document.colour_of(&shape), Some(green));
1115 }
1116
1117 #[test]
1118 fn names_attach_to_nodes_and_survive_occurrences() {
1119 let mut document = Document::new();
1120 let (_, shape) = box_part(&mut document, "bracket", 1.0);
1121 let face = explore(document.model(), &shape, Filter::OfType(ShapeType::Face))
1122 .unwrap()
1123 .remove(0);
1124 document.set_name(&face, "mounting-face");
1125 assert_eq!(document.name_of(&face), Some("mounting-face"));
1126 let moved = face.moved(&Location::identity());
1128 assert_eq!(document.name_of(&moved), Some("mounting-face"));
1129 }
1130}