1use ogeom_core::{OgeomResult, ogeom_bail};
16use ogeom_math::Transform;
17use ogeom_topo::{Location, Model, Shape, TShapeId};
18use std::collections::HashMap;
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: HashMap<TShapeId, Colour>,
115 names: HashMap<TShapeId, String>,
116 pmi: crate::pmi::Pmi,
117 properties: HashMap<TShapeId, Vec<crate::attributes::Property>>,
118 materials: Vec<crate::attributes::Material>,
119 material_of: HashMap<TShapeId, crate::attributes::MaterialId>,
120 layers: Vec<crate::attributes::Layer>,
121 on_layer: HashMap<TShapeId, Vec<crate::attributes::LayerId>>,
122 validation: HashMap<TShapeId, crate::attributes::ValidationProperties>,
123 textures: Vec<crate::attributes::Texture>,
124 texture_of: HashMap<TShapeId, crate::attributes::TextureId>,
125 views: Vec<crate::view::View>,
126 notes: Vec<crate::view::Note>,
127 history: Vec<State>,
130 undone: usize,
132}
133
134#[derive(Debug, Clone, Default)]
144struct State {
145 products: Vec<Product>,
146 colours: HashMap<TShapeId, Colour>,
147 names: HashMap<TShapeId, String>,
148 pmi: crate::pmi::Pmi,
149 properties: HashMap<TShapeId, Vec<crate::attributes::Property>>,
150 materials: Vec<crate::attributes::Material>,
151 material_of: HashMap<TShapeId, crate::attributes::MaterialId>,
152 layers: Vec<crate::attributes::Layer>,
153 on_layer: HashMap<TShapeId, Vec<crate::attributes::LayerId>>,
154 validation: HashMap<TShapeId, crate::attributes::ValidationProperties>,
155 textures: Vec<crate::attributes::Texture>,
156 texture_of: HashMap<TShapeId, crate::attributes::TextureId>,
157 views: Vec<crate::view::View>,
158 notes: Vec<crate::view::Note>,
159}
160
161impl Document {
162 #[must_use]
164 pub fn new() -> Self {
165 Self::default()
166 }
167
168 #[must_use]
170 pub fn over(model: Model) -> Self {
171 Self {
172 model,
173 ..Self::default()
174 }
175 }
176
177 #[must_use]
179 pub const fn model(&self) -> &Model {
180 &self.model
181 }
182
183 pub const fn model_mut(&mut self) -> &mut Model {
185 &mut self.model
186 }
187
188 pub fn add_part(&mut self, name: impl Into<String>, shape: Shape) -> ProductId {
190 self.push(Product {
191 name: name.into(),
192 colour: None,
193 kind: ProductKind::Part { shape },
194 })
195 }
196
197 pub fn add_assembly(&mut self, name: impl Into<String>) -> ProductId {
199 self.push(Product {
200 name: name.into(),
201 colour: None,
202 kind: ProductKind::Assembly {
203 children: Vec::new(),
204 },
205 })
206 }
207
208 fn push(&mut self, product: Product) -> ProductId {
209 self.products.push(product);
210 #[allow(clippy::cast_possible_truncation)]
211 ProductId(self.products.len() as u32 - 1)
212 }
213
214 pub fn add_instance(
228 &mut self,
229 assembly: ProductId,
230 product: ProductId,
231 at: Transform,
232 name: Option<String>,
233 ) -> OgeomResult<()> {
234 if self.get(product).is_none() {
235 ogeom_bail!(Dangling, "the product to place is not in this document");
236 }
237 if self.contains_product(product, assembly) {
238 ogeom_bail!(
239 Construction,
240 "placing this product here would make it contain itself"
241 );
242 }
243 let location = if at == Transform::IDENTITY {
244 Location::identity()
245 } else {
246 Location::of(self.model.add_datum(at))
247 };
248 let Some(entry) = self.products.get_mut(assembly.0 as usize) else {
249 ogeom_bail!(Dangling, "the assembly is not in this document");
250 };
251 let ProductKind::Assembly { children } = &mut entry.kind else {
252 ogeom_bail!(Construction, "instances go inside assemblies, not parts");
253 };
254 children.push(Instance {
255 product,
256 location,
257 name,
258 });
259 Ok(())
260 }
261
262 pub fn add_instance_at(
272 &mut self,
273 assembly: ProductId,
274 product: ProductId,
275 location: Location,
276 name: Option<String>,
277 ) -> OgeomResult<()> {
278 if self.get(product).is_none() {
279 ogeom_bail!(Dangling, "the product to place is not in this document");
280 }
281 if self.contains_product(product, assembly) {
282 ogeom_bail!(
283 Construction,
284 "placing this product here would make it contain itself"
285 );
286 }
287 let Some(entry) = self.products.get_mut(assembly.0 as usize) else {
288 ogeom_bail!(Dangling, "the assembly is not in this document");
289 };
290 let ProductKind::Assembly { children } = &mut entry.kind else {
291 ogeom_bail!(Construction, "instances go inside assemblies, not parts");
292 };
293 children.push(Instance {
294 product,
295 location,
296 name,
297 });
298 Ok(())
299 }
300
301 fn contains_product(&self, haystack: ProductId, needle: ProductId) -> bool {
303 if haystack == needle {
304 return true;
305 }
306 match self.get(haystack).map(|p| &p.kind) {
307 Some(ProductKind::Assembly { children }) => children
308 .iter()
309 .any(|i| self.contains_product(i.product, needle)),
310 _ => false,
311 }
312 }
313
314 #[must_use]
316 pub fn get(&self, id: ProductId) -> Option<&Product> {
317 self.products.get(id.0 as usize)
318 }
319
320 pub fn products(&self) -> impl Iterator<Item = (ProductId, &Product)> {
322 self.products
323 .iter()
324 .enumerate()
325 .map(|(i, p)| (ProductId(u32::try_from(i).unwrap_or(u32::MAX)), p))
326 }
327
328 #[must_use]
330 pub fn roots(&self) -> Vec<ProductId> {
331 let mut placed = vec![false; self.products.len()];
332 for product in &self.products {
333 if let ProductKind::Assembly { children } = &product.kind {
334 for instance in children {
335 placed[instance.product.0 as usize] = true;
336 }
337 }
338 }
339 placed
340 .iter()
341 .enumerate()
342 .filter(|&(_, &used)| !used)
343 .map(|(i, _)| ProductId(u32::try_from(i).unwrap_or(u32::MAX)))
344 .collect()
345 }
346
347 pub fn occurrences_of(&self, product: ProductId) -> OgeomResult<Vec<Occurrence>> {
358 let Some(root) = self.get(product) else {
359 ogeom_bail!(Dangling, "the product is not in this document");
360 };
361 let mut out = Vec::new();
362 self.flatten(
363 product,
364 root,
365 &Location::identity(),
366 &root.name.clone(),
367 &mut out,
368 );
369 Ok(out)
370 }
371
372 fn flatten(
373 &self,
374 id: ProductId,
375 product: &Product,
376 above: &Location,
377 path: &str,
378 out: &mut Vec<Occurrence>,
379 ) {
380 match &product.kind {
381 ProductKind::Part { shape } => out.push(Occurrence {
382 part: id,
383 shape: shape.moved(above),
384 path: path.to_string(),
385 }),
386 ProductKind::Assembly { children } => {
387 for instance in children {
388 let Some(child) = self.get(instance.product) else {
389 continue;
390 };
391 let below = above.then(&instance.location);
392 let step = instance.name.as_deref().unwrap_or(&child.name);
393 let path = format!("{path}/{step}");
394 self.flatten(instance.product, child, &below, &path, out);
395 }
396 }
397 }
398 }
399
400 pub fn set_product_colour(&mut self, product: ProductId, colour: Colour) -> OgeomResult<()> {
407 let Some(entry) = self.products.get_mut(product.0 as usize) else {
408 ogeom_bail!(Dangling, "the product is not in this document");
409 };
410 entry.colour = Some(colour);
411 Ok(())
412 }
413
414 pub fn add_texture(
416 &mut self,
417 texture: crate::attributes::Texture,
418 ) -> crate::attributes::TextureId {
419 self.textures.push(texture);
420 crate::attributes::TextureId(self.textures.len() - 1)
421 }
422
423 pub fn set_texture(&mut self, shape: &Shape, texture: crate::attributes::TextureId) {
425 self.texture_of.insert(shape.node(), texture);
426 }
427
428 #[must_use]
430 pub fn texture_of(&self, shape: &Shape) -> Option<&crate::attributes::Texture> {
431 let id = self.texture_of.get(&shape.node())?;
432 self.textures.get(id.0)
433 }
434
435 #[must_use]
437 pub fn textures(&self) -> &[crate::attributes::Texture] {
438 &self.textures
439 }
440
441 pub fn checkpoint(&mut self) {
447 if self.undone > 0 {
448 let keep = self.history.len() - self.undone;
449 self.history.truncate(keep);
450 self.undone = 0;
451 }
452 let state = self.state();
453 self.history.push(state);
454 }
455
456 pub fn undo(&mut self) -> bool {
458 if self.history.len() <= self.undone {
459 return false;
460 }
461 let index = self.history.len() - self.undone - 1;
464 let current = self.state();
465 let restored = core::mem::replace(&mut self.history[index], current);
466 self.apply(restored);
467 self.undone += 1;
468 true
469 }
470
471 pub fn redo(&mut self) -> bool {
473 if self.undone == 0 {
474 return false;
475 }
476 let index = self.history.len() - self.undone;
477 let current = self.state();
478 let restored = core::mem::replace(&mut self.history[index], current);
479 self.apply(restored);
480 self.undone -= 1;
481 true
482 }
483
484 #[must_use]
486 pub fn undo_depth(&self) -> (usize, usize) {
487 (self.history.len() - self.undone, self.undone)
488 }
489
490 #[must_use]
493 pub fn product_index(&self, id: ProductId) -> usize {
494 id.index() as usize
495 }
496
497 pub fn add_view(&mut self, view: crate::view::View) -> usize {
500 self.views.push(view);
501 self.views.len() - 1
502 }
503
504 #[must_use]
506 pub fn views(&self) -> &[crate::view::View] {
507 &self.views
508 }
509
510 pub fn add_note(&mut self, note: crate::view::Note) -> usize {
512 self.notes.push(note);
513 self.notes.len() - 1
514 }
515
516 #[must_use]
518 pub fn notes(&self) -> &[crate::view::Note] {
519 &self.notes
520 }
521
522 fn state(&self) -> State {
523 State {
524 products: self.products.clone(),
525 colours: self.colours.clone(),
526 names: self.names.clone(),
527 pmi: self.pmi.clone(),
528 properties: self.properties.clone(),
529 materials: self.materials.clone(),
530 material_of: self.material_of.clone(),
531 layers: self.layers.clone(),
532 on_layer: self.on_layer.clone(),
533 validation: self.validation.clone(),
534 textures: self.textures.clone(),
535 texture_of: self.texture_of.clone(),
536 views: self.views.clone(),
537 notes: self.notes.clone(),
538 }
539 }
540
541 fn apply(&mut self, state: State) {
543 self.products = state.products;
544 self.colours = state.colours;
545 self.names = state.names;
546 self.pmi = state.pmi;
547 self.properties = state.properties;
548 self.materials = state.materials;
549 self.material_of = state.material_of;
550 self.layers = state.layers;
551 self.on_layer = state.on_layer;
552 self.validation = state.validation;
553 self.textures = state.textures;
554 self.texture_of = state.texture_of;
555 self.views = state.views;
556 self.notes = state.notes;
557 }
558
559 pub fn set_colour(&mut self, shape: &Shape, colour: Colour) {
565 self.colours.insert(shape.node(), colour);
566 }
567
568 #[must_use]
570 pub fn colour_of(&self, shape: &Shape) -> Option<Colour> {
571 self.colours.get(&shape.node()).copied()
572 }
573
574 #[must_use]
579 pub fn resolved_colour(&self, part: ProductId, sub: &Shape) -> Option<Colour> {
580 if let Some(own) = self.colour_of(sub) {
581 return Some(own);
582 }
583 let product = self.get(part)?;
584 if let ProductKind::Part { shape } = &product.kind
585 && let Some(whole) = self.colour_of(shape)
586 {
587 return Some(whole);
588 }
589 product.colour
590 }
591
592 pub fn set_name(&mut self, shape: &Shape, name: impl Into<String>) {
594 self.names.insert(shape.node(), name.into());
595 }
596
597 #[must_use]
599 pub fn name_of(&self, shape: &Shape) -> Option<&str> {
600 self.names.get(&shape.node()).map(String::as_str)
601 }
602
603 pub fn colours(&self) -> impl Iterator<Item = (TShapeId, Colour)> + '_ {
605 self.colours.iter().map(|(&node, &colour)| (node, colour))
606 }
607
608 pub fn names(&self) -> impl Iterator<Item = (TShapeId, &str)> {
610 self.names.iter().map(|(&node, name)| (node, name.as_str()))
611 }
612
613 pub fn replace_part_shape(&mut self, product: ProductId, shape: Shape) -> OgeomResult<()> {
626 let Some(entry) = self.products.get_mut(product.0 as usize) else {
627 ogeom_bail!(Dangling, "the product is not in this document");
628 };
629 let ProductKind::Part { shape: slot } = &mut entry.kind else {
630 ogeom_bail!(Construction, "only a part carries a shape to replace");
631 };
632 *slot = shape;
633 Ok(())
634 }
635
636 #[must_use]
638 pub const fn pmi(&self) -> &crate::pmi::Pmi {
639 &self.pmi
640 }
641
642 pub const fn pmi_mut(&mut self) -> &mut crate::pmi::Pmi {
644 &mut self.pmi
645 }
646
647 pub fn set_property(&mut self, shape: &Shape, property: crate::attributes::Property) {
652 let list = self.properties.entry(shape.node()).or_default();
653 if let Some(held) = list.iter_mut().find(|p| p.name == property.name) {
654 *held = property;
655 } else {
656 list.push(property);
657 }
658 }
659
660 #[must_use]
662 pub fn properties_of(&self, shape: &Shape) -> &[crate::attributes::Property] {
663 self.properties
664 .get(&shape.node())
665 .map_or(&[], Vec::as_slice)
666 }
667
668 pub fn properties(&self) -> impl Iterator<Item = (TShapeId, &[crate::attributes::Property])> {
670 self.properties.iter().map(|(k, v)| (*k, v.as_slice()))
671 }
672
673 pub fn add_material(
675 &mut self,
676 material: crate::attributes::Material,
677 ) -> crate::attributes::MaterialId {
678 self.materials.push(material);
679 crate::attributes::MaterialId(self.materials.len() - 1)
680 }
681
682 #[must_use]
684 pub fn material(
685 &self,
686 id: crate::attributes::MaterialId,
687 ) -> Option<&crate::attributes::Material> {
688 self.materials.get(id.0)
689 }
690
691 #[must_use]
693 pub fn materials(&self) -> &[crate::attributes::Material] {
694 &self.materials
695 }
696
697 #[must_use]
699 pub fn material_id(&self, index: usize) -> Option<crate::attributes::MaterialId> {
700 (index < self.materials.len()).then_some(crate::attributes::MaterialId(index))
701 }
702
703 pub fn assign_material(&mut self, shape: &Shape, id: crate::attributes::MaterialId) {
705 self.material_of.insert(shape.node(), id);
706 }
707
708 #[must_use]
710 pub fn material_of(&self, shape: &Shape) -> Option<crate::attributes::MaterialId> {
711 self.material_of.get(&shape.node()).copied()
712 }
713
714 pub fn material_assignments(
716 &self,
717 ) -> impl Iterator<Item = (TShapeId, crate::attributes::MaterialId)> + '_ {
718 self.material_of.iter().map(|(k, v)| (*k, *v))
719 }
720
721 pub fn add_layer(&mut self, name: impl Into<String>) -> crate::attributes::LayerId {
723 self.layers.push(crate::attributes::Layer {
724 name: name.into(),
725 visible: true,
726 });
727 crate::attributes::LayerId(self.layers.len() - 1)
728 }
729
730 #[must_use]
732 pub fn layer(&self, id: crate::attributes::LayerId) -> Option<&crate::attributes::Layer> {
733 self.layers.get(id.0)
734 }
735
736 #[must_use]
738 pub fn layers(&self) -> &[crate::attributes::Layer] {
739 &self.layers
740 }
741
742 #[must_use]
744 pub fn layer_id(&self, index: usize) -> Option<crate::attributes::LayerId> {
745 (index < self.layers.len()).then_some(crate::attributes::LayerId(index))
746 }
747
748 pub fn set_layer_visible(&mut self, id: crate::attributes::LayerId, visible: bool) {
750 if let Some(layer) = self.layers.get_mut(id.0) {
751 layer.visible = visible;
752 }
753 }
754
755 pub fn place_on_layer(&mut self, shape: &Shape, layer: crate::attributes::LayerId) {
757 let list = self.on_layer.entry(shape.node()).or_default();
758 if !list.contains(&layer) {
759 list.push(layer);
760 }
761 }
762
763 #[must_use]
765 pub fn layers_of(&self, shape: &Shape) -> &[crate::attributes::LayerId] {
766 self.on_layer.get(&shape.node()).map_or(&[], Vec::as_slice)
767 }
768
769 pub fn layer_memberships(
771 &self,
772 ) -> impl Iterator<Item = (TShapeId, &[crate::attributes::LayerId])> {
773 self.on_layer.iter().map(|(k, v)| (*k, v.as_slice()))
774 }
775
776 pub fn set_validation(
778 &mut self,
779 shape: &Shape,
780 values: crate::attributes::ValidationProperties,
781 ) {
782 self.validation.insert(shape.node(), values);
783 }
784
785 #[must_use]
787 pub fn validation_of(&self, shape: &Shape) -> Option<crate::attributes::ValidationProperties> {
788 self.validation.get(&shape.node()).copied()
789 }
790
791 pub fn validations(
793 &self,
794 ) -> impl Iterator<Item = (TShapeId, crate::attributes::ValidationProperties)> + '_ {
795 self.validation.iter().map(|(k, v)| (*k, *v))
796 }
797}
798
799#[cfg(test)]
800#[allow(clippy::unwrap_used, clippy::expect_used)]
801mod tests {
802 use super::*;
803 use ogeom_core::Tolerances;
804 use ogeom_math::{Frame, Point, Vector};
805 use ogeom_topo::{Filter, ShapeType, explore};
806
807 const T: Tolerances = Tolerances::millimetres();
808
809 fn box_part(document: &mut Document, name: &str, size: f64) -> (ProductId, Shape) {
810 let shape = ogeom_algo::make_box(document.model_mut(), Frame::WORLD, (size, size, size), T)
811 .unwrap()
812 .shape;
813 (document.add_part(name, shape.clone()), shape)
814 }
815
816 #[test]
817 fn two_instances_of_one_part_share_the_node_and_differ_in_placement() {
818 let mut document = Document::new();
819 let (bolt, shape) = box_part(&mut document, "bolt", 1.0);
820 let assembly = document.add_assembly("plate");
821 document
822 .add_instance(assembly, bolt, Transform::IDENTITY, Some("bolt-1".into()))
823 .unwrap();
824 document
825 .add_instance(
826 assembly,
827 bolt,
828 Transform::translation(Vector::new(10.0, 0.0, 0.0)),
829 Some("bolt-2".into()),
830 )
831 .unwrap();
832
833 let occurrences = document.occurrences_of(assembly).unwrap();
834 assert_eq!(occurrences.len(), 2);
835 assert_eq!(occurrences[0].path, "plate/bolt-1");
836 assert_eq!(occurrences[1].path, "plate/bolt-2");
837 assert_eq!(occurrences[0].shape.node(), shape.node());
839 assert_eq!(occurrences[1].shape.node(), shape.node());
840 let at = |o: &Occurrence| {
841 o.shape
842 .transform(document.model().datums())
843 .unwrap()
844 .apply(Point::new(0.0, 0.0, 0.0))
845 };
846 assert!(at(&occurrences[0]).is_equal(Point::new(0.0, 0.0, 0.0), T));
847 assert!(at(&occurrences[1]).is_equal(Point::new(10.0, 0.0, 0.0), T));
848 }
849
850 #[test]
851 fn nested_placements_compose_outer_then_inner() {
852 let mut document = Document::new();
853 let (part, _) = box_part(&mut document, "washer", 1.0);
854 let sub = document.add_assembly("stack");
855 let top = document.add_assembly("machine");
856 document
857 .add_instance(
858 sub,
859 part,
860 Transform::translation(Vector::new(0.0, 5.0, 0.0)),
861 None,
862 )
863 .unwrap();
864 document
865 .add_instance(
866 top,
867 sub,
868 Transform::translation(Vector::new(100.0, 0.0, 0.0)),
869 None,
870 )
871 .unwrap();
872
873 let occurrences = document.occurrences_of(top).unwrap();
874 assert_eq!(occurrences.len(), 1);
875 assert_eq!(occurrences[0].path, "machine/stack/washer");
876 let world = occurrences[0]
877 .shape
878 .transform(document.model().datums())
879 .unwrap()
880 .apply(Point::new(0.0, 0.0, 0.0));
881 assert!(world.is_equal(Point::new(100.0, 5.0, 0.0), T));
882 }
883
884 #[test]
885 fn a_product_cannot_contain_itself() {
886 let mut document = Document::new();
887 let a = document.add_assembly("a");
888 let b = document.add_assembly("b");
889 document
890 .add_instance(a, b, Transform::IDENTITY, None)
891 .unwrap();
892 let err = document.add_instance(b, a, Transform::IDENTITY, None);
893 assert!(err.is_err(), "a cycle must be refused");
894 let direct = document.add_instance(a, a, Transform::IDENTITY, None);
895 assert!(direct.is_err(), "self-containment must be refused");
896 }
897
898 #[test]
899 fn instances_go_only_inside_assemblies() {
900 let mut document = Document::new();
901 let (part, _) = box_part(&mut document, "block", 1.0);
902 let (other, _) = box_part(&mut document, "pin", 1.0);
903 assert!(
904 document
905 .add_instance(part, other, Transform::IDENTITY, None)
906 .is_err()
907 );
908 }
909
910 #[test]
911 fn roots_are_the_products_nothing_places() {
912 let mut document = Document::new();
913 let (part, _) = box_part(&mut document, "gear", 1.0);
914 let assembly = document.add_assembly("gearbox");
915 document
916 .add_instance(assembly, part, Transform::IDENTITY, None)
917 .unwrap();
918 assert_eq!(document.roots(), vec![assembly]);
919 }
920
921 #[test]
922 fn colour_resolution_walks_sub_shape_then_shape_then_product() {
923 let mut document = Document::new();
924 let (part, shape) = box_part(&mut document, "housing", 2.0);
925 let face = explore(document.model(), &shape, Filter::OfType(ShapeType::Face))
926 .unwrap()
927 .remove(0);
928
929 let red = Colour::rgb(1.0, 0.0, 0.0);
930 let green = Colour::rgb(0.0, 1.0, 0.0);
931 let blue = Colour::rgb(0.0, 0.0, 1.0);
932
933 assert_eq!(document.resolved_colour(part, &face), None);
934 document.set_product_colour(part, blue).unwrap();
935 assert_eq!(document.resolved_colour(part, &face), Some(blue));
936 document.set_colour(&shape, green);
937 assert_eq!(document.resolved_colour(part, &face), Some(green));
938 document.set_colour(&face, red);
939 assert_eq!(document.resolved_colour(part, &face), Some(red));
940 assert_eq!(document.colour_of(&shape), Some(green));
942 }
943
944 #[test]
945 fn names_attach_to_nodes_and_survive_occurrences() {
946 let mut document = Document::new();
947 let (_, shape) = box_part(&mut document, "bracket", 1.0);
948 let face = explore(document.model(), &shape, Filter::OfType(ShapeType::Face))
949 .unwrap()
950 .remove(0);
951 document.set_name(&face, "mounting-face");
952 assert_eq!(document.name_of(&face), Some("mounting-face"));
953 let moved = face.moved(&Location::identity());
955 assert_eq!(document.name_of(&moved), Some("mounting-face"));
956 }
957}