//! `solid.mirror` of bodies in a placed component (a moved occurrence, a further instance, an //! inserted design) mirrors each body where it is shown. Before, it mirrored the component's own //! geometry and put the copy in the active component, so the copy lost the placement. use serde_json::{Value, json}; use solidcraft_geom::Vec3; use super::run; use crate::*; fn world_box(s: &Session, body: &str) -> (Vec3, Vec3) { let b = s.world_state().body(body).unwrap_or_else(|| panic!("no body `{body}`")).mesh().bounds(); (b.min, b.max) } /// The box of `body` mirrored in the world YZ plane. fn mirrored_yz(s: &Session, body: &str) -> (Vec3, Vec3) { let (lo, hi) = world_box(s, body); (Vec3::new(-hi.x, lo.y, lo.z), Vec3::new(-lo.x, hi.y, hi.z)) } fn assert_box(got: (Vec3, Vec3), want: (Vec3, Vec3)) { assert!(got.0.dist(want.0) < 1e-6 && got.1.dist(want.1) < 1e-6, "got {got:?}, want {want:?}"); } /// A component "Arm" holding "ArmBody", its occurrence turned and moved; the root is active. fn moved_arm(s: &mut Session) -> (u64, u64) { let c = run(s, "component.create", json!({"name": "Arm"})); let (component, occurrence) = (c["component"].as_u64().unwrap(), c["occurrence"].as_u64().unwrap()); run(s, "solid.box", json!({"corner": [10, 0, 0], "length": 20, "width": 4, "height": 6, "body_name": "ArmBody"})); run(s, "component.activate", json!({"component": 0})); run(s, "occurrence.move", json!({"occurrence": occurrence, "axis": [1, 0, 0], "angle": "90 deg", "translate": [30, 50, 0]})); (component, occurrence) } fn created(r: &Value) -> String { r["created_bodies"][0].as_str().unwrap().to_string() } #[test] fn a_moved_components_body_is_mirrored_where_it_is_shown() { let mut s = Session::default(); let (_, occurrence) = moved_arm(&mut s); let copy = created(&run(&mut s, "solid.mirror", json!({"bodies": ["ArmBody"], "plane": "YZ"}))); assert_box(world_box(&s, ©), mirrored_yz(&s, "ArmBody")); // The copy follows its source when the occurrence moves again. run(&mut s, "occurrence.move", json!({"occurrence": occurrence, "space": "world", "translate": [0, 200, 0]})); assert_box(world_box(&s, ©), mirrored_yz(&s, "ArmBody")); // It survives a save and reopen. let t = Session::new(solidcraft_doc::Document::from_json(&s.doc.to_json()).unwrap()); assert_box(world_box(&t, ©), mirrored_yz(&t, "ArmBody")); } #[test] fn a_further_instance_is_mirrored_at_its_own_placement() { let mut s = Session::default(); let (component, _) = moved_arm(&mut s); run(&mut s, "occurrence.copy", json!({"component": component, "translate": [0, 0, 40]})); let names: Vec = s.world_state().bodies.iter().map(|b| b.name.clone()).collect(); let instance = names.iter().find(|n| n.starts_with("ArmBody (")).cloned().unwrap(); let r = run(&mut s, "solid.mirror", json!({"bodies": ["ArmBody", instance], "plane": "YZ"})); let copies: Vec = r["created_bodies"].as_array().unwrap().iter().map(|n| n.as_str().unwrap().to_string()).collect(); assert_eq!(copies.len(), 2, "{r}"); assert_box(world_box(&s, &copies[0]), mirrored_yz(&s, "ArmBody")); assert_box(world_box(&s, &copies[1]), mirrored_yz(&s, &instance)); } #[test] fn a_plane_picked_on_a_moved_body_is_a_plane_of_the_active_component() { let mut s = Session::default(); moved_arm(&mut s); // A world plane x = 30 through the moved body: the copy is mirrored in it, as shown. let copy = created(&run(&mut s, "solid.mirror", json!({"bodies": ["ArmBody"], "plane": {"origin": [30, 50, 0], "normal": [1, 0, 0]}}))); let (lo, hi) = world_box(&s, "ArmBody"); assert_box(world_box(&s, ©), (Vec3::new(60.0 - hi.x, lo.y, lo.z), Vec3::new(60.0 - lo.x, hi.y, hi.z))); } #[test] fn a_joined_mirror_of_a_moved_body_stays_in_its_component() { let mut s = Session::default(); moved_arm(&mut s); // A world plane through the body's far end: the joined body doubles along x where it is. let (lo, hi) = world_box(&s, "ArmBody"); run(&mut s, "solid.mirror", json!({"bodies": ["ArmBody"], "plane": {"origin": [hi.x, 0, 0], "normal": [1, 0, 0]}, "combine": true})); let len = hi.x - lo.x; assert_box(world_box(&s, "ArmBody"), (lo, Vec3::new(hi.x + len, hi.y, hi.z))); } #[test] fn bodies_in_place_keep_no_placement() { let mut s = Session::default(); run(&mut s, "solid.box", json!({"length": 10, "width": 10, "height": 10, "body_name": "Base"})); run(&mut s, "solid.mirror", json!({"bodies": ["Base"], "plane": "YZ"})); let f = s.doc.features.last().unwrap(); assert!(matches!(&f.kind, solidcraft_doc::FeatureKind::Mirror { placements, .. } if placements.is_empty()), "{:?}", f.kind); } #[test] fn a_position_not_captured_yet_is_refused() { let mut s = Session::default(); let (_, occurrence) = moved_arm(&mut s); run(&mut s, "occurrence.move", json!({"occurrence": occurrence, "translate": [5, 0, 0], "capture": false})); let e = s.execute("solid.mirror", &json!({"bodies": ["ArmBody"], "plane": "YZ"})).unwrap_err().to_string(); assert!(e.contains("Capture Position"), "{e}"); }