use serde_json::{Value, json}; use crate::Session; fn run(s: &mut Session, id: &str, p: Value) -> Value { match s.execute(id, &p) { Ok(v) => v, Err(e) => panic!("{id} {p}: {e}"), } } fn measure(s: &mut Session) -> Value { run(s, "MeasureCommand", json!({})) } fn volume(s: &mut Session) -> f64 { measure(s)["total"]["volume_mm3"].as_f64().unwrap_or(f64::NAN) } fn rel(a: f64, b: f64) -> f64 { (a - b).abs() / b.abs().max(1e-12) } /// An L bracket: a 40 x 20 x 5 base and a 5 x 20 x 40 wall at x = 0..5. fn bracket() -> Session { let mut s = Session::default(); run(&mut s, "PrimitiveBox", json!({"length": 40, "width": 20, "height": 5, "corner": [0, 0, 0], "body_name": "L"})); run(&mut s, "PrimitiveBox", json!({"length": 5, "width": 20, "height": 40, "corner": [0, 0, 0], "operation": "join"})); assert!(rel(volume(&mut s), 4000.0 + 3500.0) < 1e-9); // Mid-plane of the bracket (y = 10): sketch x = X, sketch y = Z. run(&mut s, "SketchCreate", json!({"plane": {"origin": [0, 10, 0], "x_dir": [1, 0, 0], "y_dir": [0, 0, 1]}, "name": "RibSketch"})); run(&mut s, "DrawPolyline", json!({"points": [[5, 25], [25, 5]], "ids": ["l1"]})); run(&mut s, "SketchStop", json!({})); s } #[test] fn rib_to_the_next_face_fills_the_corner() { let mut s = bracket(); run(&mut s, "FusionRibCommand", json!({"sketch": "RibSketch", "curves": ["l1"], "thickness": 4})); // The triangle (5,5)-(5,25)-(25,5) of area 200, 4 thick. assert!(rel(volume(&mut s), 7500.0 + 800.0) < 1e-6, "{}", volume(&mut s)); assert_eq!(measure(&mut s)["body_count"], 1); // The thickness is a parameter. let t = s.doc.features.last().and_then(|f| f.param_names.first().cloned()).unwrap_or_default(); run(&mut s, "ChangeParameterCommand", json!({"name": t, "expression": "2"})); assert!(rel(volume(&mut s), 7500.0 + 400.0) < 1e-6); } #[test] fn rib_with_a_depth_and_web() { let mut s = bracket(); run(&mut s, "FusionRibCommand", json!({"sketch": "RibSketch", "curves": ["l1"], "thickness": 4, "depth": 2})); let v = volume(&mut s); assert!(v > 7500.0 + 100.0 && v < 7500.0 + 800.0, "{v}"); let mut s = bracket(); run(&mut s, "FusionWebCommand", json!({"sketch": "RibSketch", "curves": ["l1"], "thickness": 4})); assert!(rel(volume(&mut s), 8300.0) < 1e-6); // Curves that face no body. let mut s = Session::default(); run(&mut s, "PrimitiveBox", json!({"length": 10, "width": 10, "height": 10})); run(&mut s, "SketchCreate", json!({"plane": "XY", "name": "Far"})); run(&mut s, "DrawPolyline", json!({"points": [[50, 50], [60, 60]], "ids": ["l1"]})); run(&mut s, "SketchStop", json!({})); let e = s.execute("FusionRibCommand", &json!({"sketch": "Far", "curves": ["l1"], "thickness": 1})); assert!(e.is_err_and(|e| e.to_string().contains("do not face a body"))); } #[test] fn emboss_and_deboss_on_a_face() { let mut s = Session::default(); run(&mut s, "PrimitiveBox", json!({"length": 40, "width": 30, "height": 10})); run(&mut s, "SketchCreate", json!({"plane": {"face": [20, 15, 10]}, "name": "Logo"})); run(&mut s, "ShapeRectangleTwoPoint", json!({"p0": [5, 5], "p1": [15, 15]})); run(&mut s, "SketchStop", json!({})); run(&mut s, "EmbossCmd", json!({"sketch": "Logo", "depth": 2})); assert!(rel(volume(&mut s), 12000.0 + 200.0) < 1e-6, "{}", volume(&mut s)); run(&mut s, "UndoCommand", json!({})); run(&mut s, "EmbossCmd", json!({"sketch": "Logo", "depth": 3, "mode": "deboss"})); assert!(rel(volume(&mut s), 12000.0 - 300.0) < 1e-6); assert!(s.execute("EmbossCmd", &json!({"sketch": "Logo", "depth": 1, "mode": "sideways"})).is_err()); } #[test] fn replace_face_align_remove() { let mut s = Session::default(); run(&mut s, "PrimitiveBox", json!({"length": 10, "width": 10, "height": 10, "body_name": "A"})); run(&mut s, "FusionReplaceFaceCommand", json!({"faces": [[5, 5, 10]], "target": {"origin": [0, 0, 15], "normal": [0, 0, 1]}})); assert!(rel(volume(&mut s), 1500.0) < 1e-6); let e = s.execute("FusionReplaceFaceCommand", &json!({"faces": [[5, 5, 15]], "target": {"origin": [0, 0, 15], "normal": [1, 0, 1]}})); assert!(e.is_err() || s.model.results.last().is_some_and(|r| r.error.as_deref().unwrap_or("").contains("parallel"))); let mut s = Session::default(); run(&mut s, "PrimitiveBox", json!({"length": 10, "width": 10, "height": 10, "body_name": "A"})); run(&mut s, "PrimitiveBox", json!({"length": 10, "width": 10, "height": 10, "corner": [50, 0, 0], "body_name": "B"})); // B's top face onto A's top face: B turns over and sits on A. run(&mut s, "AlignCmd", json!({"bodies": ["B"], "from_face": [55, 5, 10], "to_face": [5, 5, 10]})); let m = measure(&mut s); let b = m["bodies"].as_array().unwrap().iter().find(|b| b["name"] == "B").unwrap().clone(); let (lo, hi) = (b["bbox"]["min"].clone(), b["bbox"]["max"].clone()); for (got, want) in [(&lo[0], 0.0), (&lo[2], 10.0), (&hi[0], 10.0), (&hi[2], 20.0)] { assert!((got.as_f64().unwrap_or(f64::NAN) - want).abs() < 1e-6, "{b}"); } // Points only: a plain move. run(&mut s, "AlignCmd", json!({"bodies": ["B"], "from": [0, 0, 10], "to": [0, 0, 30]})); let m = measure(&mut s); let b = m["bodies"].as_array().unwrap().iter().find(|b| b["name"] == "B").unwrap().clone(); assert!((b["bbox"]["min"][2].as_f64().unwrap_or(0.0) - 30.0).abs() < 1e-6, "{b}"); run(&mut s, "SoftDeleteCommand", json!({"bodies": ["B"]})); assert_eq!(measure(&mut s)["body_count"], 1); } #[test] fn coil_and_loft_to_a_point() { use std::f64::consts::{PI, TAU}; let mut s = Session::default(); run(&mut s, "PrimitiveCoil", json!({"diameter": 40, "revolutions": 2, "pitch": 8, "section_size": 4})); assert!(rel(volume(&mut s), PI * 4.0 * TAU * 20.0 * 2.0) < 1e-3, "{}", volume(&mut s)); // Height and revolutions give the pitch; a square section outside the diameter. let mut s = Session::default(); run( &mut s, "PrimitiveCoil", json!({"diameter": 40, "revolutions": 1.5, "height": 15, "section": "square", "section_size": 2, "section_position": "outside", "axis": "X"}), ); assert!(rel(volume(&mut s), 4.0 * TAU * 21.0 * 1.5) < 1e-3, "{}", volume(&mut s)); assert!(s.execute("PrimitiveCoil", &json!({"diameter": 40, "pitch": 1, "revolutions": 3, "section_size": 4})).is_err(), "runs into itself"); assert!(s.execute("PrimitiveCoil", &json!({"diameter": 40, "section_size": 4, "pitch": 8})).is_err(), "two of three"); // Pyramid: a square lofted to a point. let mut s = Session::default(); run(&mut s, "SketchCreate", json!({"plane": "XY", "name": "Base"})); run(&mut s, "ShapeRectangleTwoPoint", json!({"p0": [-10, -10], "p1": [10, 10]})); run(&mut s, "SketchStop", json!({})); run(&mut s, "SketchCreate", json!({"plane": {"origin": [0, 0, 30], "x_dir": [1, 0, 0], "y_dir": [0, 1, 0]}, "name": "Top"})); run(&mut s, "DrawPoint", json!({"point": [0, 0], "id": "apex"})); run(&mut s, "SketchStop", json!({})); run(&mut s, "SolidLoft", json!({"sections": [{"sketch": "Base"}, {"sketch": "Top", "point": "apex"}]})); assert!(rel(volume(&mut s), 400.0 * 30.0 / 3.0) < 1e-6); assert!(s.execute("SolidLoft", &json!({"sections": [{"sketch": "Base"}, {"sketch": "Top", "point": "nope"}]})).is_err()); }