use serde_json::json; use solvecraft_engine::Session; use super::*; use crate::Services; use crate::dialogs::{apply_commands, face_sel, preview_commands}; /// Components A (grounded) and B: a 20 x 20 x 10 box each, B 30 mm along X. fn two_boxes() -> SolveApp { let mut s = Session::default(); for (name, x) in [("A", 0), ("B", 30)] { s.execute("component.activate", &json!({"component": "root"})).unwrap(); s.execute("FusionCreateNewComponentCommand", &json!({ "name": name })).unwrap(); s.execute("PrimitiveBox", &json!({"length": 20, "width": 20, "height": 10, "corner": [x, 0, 0]})).unwrap(); } s.execute("component.activate", &json!({"component": "root"})).unwrap(); s.execute("occurrence.ground", &json!({"occurrence": "A:1", "grounded": true})).unwrap(); SolveApp::new(s, Services::default()) } fn occ(app: &SolveApp, name: &str) -> u64 { let c = app.session.doc.find_component(name).unwrap(); app.session.doc.occurrence_of(c).unwrap().id } /// Two face picks make a joint between the components that own them; offsets across the face /// turn component 1's origin into a frame; limits follow the type. #[test] fn joint_from_two_snaps() { let mut app = two_boxes(); app.start("JointAssembleCmdNew"); let mut d = app.dialog.clone().unwrap(); let (fa, fb) = (face_sel(&app.session, Vec3::new(10.0, 10.0, 10.0)).unwrap(), face_sel(&app.session, Vec3::new(40.0, 10.0, 0.0)).unwrap()); assert!(apply_commands(&app, &d).is_err(), "no snaps yet"); d.pick(&app.session, fa); assert_eq!(d.active, 1, "component 2 is next"); d.pick(&app.session, fb); if let Kind::Assembly(Asm::Joint(f)) = &mut d.kind { f.kind = 1; f.fit_limits(); f.limits[0] = (true, "-45 deg".into(), "45 deg".into()); } let c = apply_commands(&app, &d).unwrap(); let (id, p) = &c[0]; assert_eq!(id, "JointAssembleCmdNew"); assert_eq!(p["type"], "revolute"); assert_eq!(p["a"]["occurrence"], occ(&app, "A")); assert_eq!(p["b"]["occurrence"], occ(&app, "B")); assert!(p["a"]["face"].is_array() && p["b"]["face"].is_array(), "{p}"); assert_eq!(p["limits"], json!([["-45 deg", "45 deg"]])); // The bottom face of B snaps at its centre, z down. let sb = snap_of(&app.session, &d.inputs[1].items[0]).unwrap(); assert!(sb.at.dist(Vec3::new(40.0, 10.0, 0.0)) < 1e-6 && sb.z.dist(-Vec3::Z) < 1e-9, "{sb:?}"); // Offset X: an explicit frame 5 mm along the face. if let Kind::Assembly(Asm::Joint(f)) = &mut d.kind { f.offset[0] = "5 mm".into(); } let p = apply_commands(&app, &d).unwrap()[0].1.clone(); let o = &p["a"]["frame"]["origin"]; let at = Vec3::new(o[0].as_f64().unwrap(), o[1].as_f64().unwrap(), o[2].as_f64().unwrap()); assert!((at.dist(Vec3::new(10.0, 10.0, 10.0)) - 5.0).abs() < 1e-9, "{p}"); // OK makes it. for (id, p) in apply_commands(&app, &d).unwrap() { app.run(&id, p).unwrap(); } assert_eq!(app.session.doc.assembly.joints.len(), 1); } /// The preview moves B where the joint puts it and plays the motion while animating. #[test] fn joint_and_drive_preview_on_the_placed_model() { let mut app = two_boxes(); app.start("JointAssembleCmdNew"); let mut d = app.dialog.clone().unwrap(); d.pick(&app.session, face_sel(&app.session, Vec3::new(10.0, 10.0, 10.0)).unwrap()); d.pick(&app.session, face_sel(&app.session, Vec3::new(40.0, 10.0, 0.0)).unwrap()); let colors = crate::preview::Colors { body: [1, 1, 1, 255], added: [2, 2, 2, 255], removed: [3, 3, 3, 100], edge: [0, 0, 0, 255] }; let cmds = preview_commands(&app, &d).unwrap(); let b = world_preview(&app.session, &cmds, colors).unwrap().unwrap(); assert_eq!(b.replaced, vec!["Body2".to_string()], "B moves"); assert!(app.session.doc.assembly.joints.is_empty(), "the preview changes nothing"); if let Kind::Assembly(Asm::Joint(f)) = &mut d.kind { f.kind = 1; f.animate = true; f.phase = 0.25; } let cmds = preview_commands(&app, &d).unwrap(); assert_eq!(cmds[0].1["values"], json!(["90 deg"]), "a quarter turn into the animation"); assert!(apply_commands(&app, &d).unwrap()[0].1.get("values").is_none(), "OK makes the joint at rest"); // Drive: the dialog's value turns the joint, the preview shows it. for (id, p) in apply_commands(&app, &d).unwrap() { app.run(&id, p).unwrap(); } app.start("FusionMoveJointsCommand"); let mut d = app.dialog.clone().unwrap(); if let Kind::Assembly(Asm::Drive { values, .. }) = &mut d.kind { assert_eq!(values, &vec!["0 deg".to_string()]); values[0] = "30 deg".into(); } let cmds = apply_commands(&app, &d).unwrap(); assert_eq!(cmds[0].0, "FusionMoveJointsCommand"); let b = world_preview(&app.session, &cmds, colors).unwrap().unwrap(); assert_eq!(b.replaced, vec!["Body2".to_string()]); assert!(world_preview(&app.session, &[("Extrude".into(), json!({}))], colors).is_none(), "features preview as before"); } /// Edit Joint changes type, alignment and limits of the joint in place. #[test] fn edit_joint_runs_edit_and_limits() { let mut app = two_boxes(); let (a, b) = (occ(&app, "A"), occ(&app, "B")); app.run( "JointAssembleCmdNew", json!({"type": "slider", "a": {"occurrence": a, "face": [10, 10, 10]}, "b": {"occurrence": b, "face": [40, 10, 0]}}), ) .unwrap(); let id = app.session.doc.assembly.joints[0].id; let mut d = edit_joint(&app, id).unwrap(); assert!(d.inputs.is_empty()); if let Kind::Assembly(Asm::Joint(f)) = &mut d.kind { assert_eq!(TYPES[f.kind], "slider"); f.limits[0] = (true, "0 mm".into(), "15 mm".into()); f.offset[2] = "2 mm".into(); } let c = apply_commands(&app, &d).unwrap(); assert_eq!(c.iter().map(|x| x.0.as_str()).collect::>(), ["joint.edit", "joint.limits"]); for (id, p) in c { app.run(&id, p).unwrap(); } let j = &app.session.doc.assembly.joints[0]; assert_eq!(j.limits[0], Some((0.0, 15.0))); assert!((j.offset - 2.0).abs() < 1e-12); } /// As-built joints and rigid groups take components from body picks; New Component makes one /// from bodies when bodies are picked. #[test] fn component_picks_become_occurrences() { let mut app = two_boxes(); app.session.selection = vec![Sel::Body { name: "Body1".into() }, Sel::Body { name: "Body2".into() }]; app.start("JointAsBuiltCmd"); let d = app.dialog.clone().unwrap(); let c = apply_commands(&app, &d).unwrap(); assert_eq!(c[0].1["a"], occ(&app, "A")); assert_eq!(c[0].1["b"], occ(&app, "B")); app.session.selection = vec![Sel::Body { name: "Body1".into() }, Sel::Body { name: "Body2".into() }]; app.start("RigidGroupCmd"); let c = apply_commands(&app, app.dialog.as_ref().unwrap()).unwrap(); assert_eq!(c[0].1["occurrences"], json!([occ(&app, "A"), occ(&app, "B")])); app.dialog = None; app.start("FusionCreateNewComponentCommand"); let c = apply_commands(&app, app.dialog.as_ref().unwrap()).unwrap(); assert_eq!(c[0].0, "FusionCreateNewComponentCommand"); } /// Round edges snap to their centre; straight ones are not circles. #[test] fn circles_from_edge_polylines() { let pts: Vec = (0..=24).map(|i| f64::from(i) / 24.0 * std::f64::consts::TAU).map(|t| Vec3::new(5.0 + 3.0 * t.cos(), 2.0 + 3.0 * t.sin(), 7.0)).collect(); let (c, n) = circle_of(&pts).unwrap(); assert!(c.dist(Vec3::new(5.0, 2.0, 7.0)) < 1e-9 && n.cross(Vec3::Z).len() < 1e-9); let line: Vec = (0..10).map(|i| Vec3::new(f64::from(i), 0.0, 0.0)).collect(); assert!(circle_of(&line).is_none()); }