Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AS5Tn3xieewUSg3uxrMVdL
140 lines
7.2 KiB
Rust
140 lines
7.2 KiB
Rust
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());
|
|
}
|