Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AS5Tn3xieewUSg3uxrMVdL
144 lines
7.6 KiB
Rust
144 lines
7.6 KiB
Rust
use std::f64::consts::{FRAC_PI_2, PI};
|
||
|
||
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 volume(s: &mut Session) -> f64 {
|
||
run(s, "MeasureCommand", json!({}))["total"]["volume_mm3"].as_f64().unwrap_or(f64::NAN)
|
||
}
|
||
|
||
fn rel(a: f64, b: f64) -> f64 {
|
||
(a - b).abs() / b.abs().max(1e-12)
|
||
}
|
||
|
||
fn flat(s: &mut Session) -> Value {
|
||
run(s, "FusionSheetMetalFlatPatternCmd", json!({}))
|
||
}
|
||
|
||
/// A w x h base flange on XY.
|
||
fn base(s: &mut Session, w: f64, h: f64) {
|
||
run(s, "SketchCreate", json!({"plane": "XY", "name": "Base"}));
|
||
run(s, "ShapeRectangleTwoPoint", json!({"p0": [0, 0], "p1": [w, h]}));
|
||
run(s, "SketchStop", json!({}));
|
||
run(s, "FusionSheetMetalFlangeCommand", json!({"sketch": "Base"}));
|
||
}
|
||
|
||
#[test]
|
||
fn base_and_edge_flange_follow_the_k_factor() {
|
||
let mut s = Session::default();
|
||
base(&mut s, 100.0, 60.0);
|
||
assert!(rel(volume(&mut s), 15000.0) < 1e-9);
|
||
run(&mut s, "FusionSheetMetalFlangeCommand", json!({"edges": [[50, 0, 2.5]], "height": 20}));
|
||
// Oracle 68: flat = 60 − 5 + (π/2)(2.5 + 0.44·2.5) + 20 − 5.
|
||
let want = 60.0 - 5.0 + FRAC_PI_2 * (2.5 + 0.44 * 2.5) + 15.0;
|
||
let f = flat(&mut s);
|
||
assert!((f["flat_size_mm"][1].as_f64().unwrap_or(0.0) - want).abs() < 1e-6, "{f}");
|
||
assert_eq!(f["bends"][0]["direction"], "up");
|
||
assert!(rel(volume(&mut s), 18972.622) < 1e-4, "{}", volume(&mut s));
|
||
let m = run(&mut s, "MeasureCommand", json!({}));
|
||
assert_eq!(m["total"]["faces"], 14, "{m}");
|
||
// The angle is a parameter: 45° (oracle 70 uses height 15).
|
||
let names = s.doc.features.last().map(|f| f.param_names.clone()).unwrap_or_default();
|
||
run(&mut s, "ChangeParameterCommand", json!({"name": names[0], "expression": "15"}));
|
||
run(&mut s, "ChangeParameterCommand", json!({"name": names[1], "expression": "45 deg"}));
|
||
assert!(rel(volume(&mut s), 18450.777) < 1e-4, "{}", volume(&mut s));
|
||
assert!((flat(&mut s)["flat_size_mm"][1].as_f64().unwrap_or(0.0) - 73.685298).abs() < 1e-5);
|
||
// DXF: outline and an up bend line.
|
||
let d = run(&mut s, "sheet.export_dxf", json!({}));
|
||
let text = d["dxf"].as_str().unwrap_or_default();
|
||
assert!(text.contains("BEND_UP") && text.contains("OUTLINE"), "{text}");
|
||
assert!(s.execute("FusionSheetMetalFlangeCommand", &json!({"edges": [[50, 30, 40]], "height": 20})).is_err(), "not on an edge");
|
||
}
|
||
|
||
#[test]
|
||
fn rules_are_parameters() {
|
||
let mut s = Session::default();
|
||
run(&mut s, "ChangeParameterCommand", json!({"name": "Thick", "expression": "1.5 mm"}));
|
||
run(&mut s, "FusionSheetMetalRulesCommand", json!({"name": "Custom", "thickness": "Thick", "k_factor": 0.33, "bend_radius": "3 mm"}));
|
||
base(&mut s, 80.0, 50.0);
|
||
run(&mut s, "FusionSheetMetalFlangeCommand", json!({"edges": [[40, 50, 1.5], [40, 0, 1.5]], "height": 20}));
|
||
// Oracle 71: 50 − 9 + 2·(π/2)(3 + 0.495) + 2·15.5 = 82.980.
|
||
let want = 50.0 - 2.0 * 4.5 + 2.0 * FRAC_PI_2 * (3.0 + 0.33 * 1.5) + 2.0 * 15.5;
|
||
assert!((flat(&mut s)["flat_size_mm"][1].as_f64().unwrap_or(0.0) - want).abs() < 1e-6);
|
||
assert!(rel(volume(&mut s), 10053.717) < 1e-4, "{}", volume(&mut s));
|
||
// Thicker: everything follows.
|
||
run(&mut s, "ChangeParameterCommand", json!({"name": "Thick", "expression": "2 mm"}));
|
||
let want = 50.0 - 2.0 * 5.0 + 2.0 * FRAC_PI_2 * (3.0 + 0.33 * 2.0) + 2.0 * 15.0;
|
||
assert!((flat(&mut s)["flat_size_mm"][1].as_f64().unwrap_or(0.0) - want).abs() < 1e-6);
|
||
let l = run(&mut s, "sheet.rules", json!({}));
|
||
assert_eq!(l["active"], "Custom", "{l}");
|
||
assert!(s.execute("FusionSheetMetalRulesCommand", &json!({"name": "Bad", "k_factor": 3})).is_err());
|
||
}
|
||
|
||
#[test]
|
||
fn contour_flange_hem_and_tray() {
|
||
// Oracle 72.
|
||
let mut s = Session::default();
|
||
run(&mut s, "SketchCreate", json!({"plane": {"origin": [0, 0, 0], "x_dir": [1, 0, 0], "y_dir": [0, 0, -1]}, "name": "Contour"}));
|
||
run(&mut s, "DrawPolyline", json!({"points": [[0, 0], [40, 0], [40, -30], [70, -30]], "ids": ["l1", "l2", "l3"]}));
|
||
run(&mut s, "SketchStop", json!({}));
|
||
run(&mut s, "FusionSheetMetalFlangeCommand", json!({"sketch": "Contour", "curves": ["l1", "l2", "l3"], "distance": 50}));
|
||
assert!(rel(volume(&mut s), 12097.622) < 1e-4, "{}", volume(&mut s));
|
||
assert!((flat(&mut s)["flat_size_mm"][0].as_f64().unwrap_or(0.0) - 96.309734).abs() < 1e-5);
|
||
// Oracle 73: flange, then a flat hem of 8.
|
||
let mut s = Session::default();
|
||
base(&mut s, 100.0, 60.0);
|
||
run(&mut s, "FusionSheetMetalFlangeCommand", json!({"edges": [[50, 0, 2.5]], "height": 20}));
|
||
run(&mut s, "FusionSheetMetalHemFlangeCommand", json!({"edges": [[50, 0, 20]], "length": 8}));
|
||
assert!(rel(volume(&mut s), 21334.723) < 1e-4, "{}", volume(&mut s));
|
||
let b = flat(&mut s)["bends"].clone();
|
||
assert!(b.as_array().is_some_and(|a| a.iter().any(|x| (x["angle_deg"].as_f64().unwrap_or(0.0) - 180.0).abs() < 1e-9)), "{b}");
|
||
// A tray: four flanges at once stop at each other's bends.
|
||
let mut s = Session::default();
|
||
base(&mut s, 100.0, 60.0);
|
||
run(&mut s, "FusionSheetMetalFlangeCommand", json!({"edges": [[50, 60, 2.5], [100, 30, 2.5], [50, 0, 2.5], [0, 30, 2.5]], "height": 25}));
|
||
let f = flat(&mut s);
|
||
// Oracle 69: 100 − 10 + 2·5.655 + 2·20 = 141.310 across.
|
||
assert!((f["flat_size_mm"][0].as_f64().unwrap_or(0.0) - (90.0 + 2.0 * FRAC_PI_2 * 3.6 + 40.0)).abs() < 1e-6, "{f}");
|
||
let v = volume(&mut s);
|
||
// Each flange runs on past its bend toward the corner until the inner corners are the
|
||
// rule's gap apart (diagonally): 5 − (2.5 + 2.5/√2) = 0.732 at each of 8 ends.
|
||
let section = PI / 4.0 * (25.0 - 6.25) + 2.5 * 20.0;
|
||
let ext = 5.0 - (2.5 + 2.5 / std::f64::consts::SQRT_2);
|
||
let want = 90.0 * 50.0 * 2.5 + section * (2.0 * 90.0 + 2.0 * 50.0) + 8.0 * ext * 2.5 * 20.0;
|
||
assert!(rel(v, want) < 1e-4 && rel(v, 29666.234) < 1e-4, "{v} {want}");
|
||
}
|
||
|
||
#[test]
|
||
fn unfold_cut_refold_and_convert() {
|
||
// Oracle 74: two flanges, unfold, a slot across the bend, refold.
|
||
let mut s = Session::default();
|
||
base(&mut s, 80.0, 50.0);
|
||
run(&mut s, "FusionSheetMetalFlangeCommand", json!({"edges": [[40, 50, 2.5], [40, 0, 2.5]], "height": 20}));
|
||
let folded = volume(&mut s);
|
||
run(&mut s, "FusionSheetmetalUnfoldCommand", json!({}));
|
||
let m = run(&mut s, "MeasureCommand", json!({}));
|
||
assert!((m["bodies"][0]["bbox"]["max"][2].as_f64().unwrap_or(0.0) - 2.5).abs() < 1e-6, "{m}");
|
||
run(&mut s, "SketchCreate", json!({"plane": "XY", "name": "Slot"}));
|
||
run(&mut s, "ShapeRectangleTwoPoint", json!({"p0": [35, -12], "p1": [45, 10]}));
|
||
run(&mut s, "SketchStop", json!({}));
|
||
run(&mut s, "Extrude", json!({"sketch": "Slot", "through_all": true, "operation": "cut", "direction": "symmetric"}));
|
||
run(&mut s, "sheet.refold", json!({}));
|
||
let v = volume(&mut s);
|
||
assert!(v < folded - 300.0, "{v} {folded}");
|
||
assert!(rel(v, 15800.304) < 1e-4, "{v}");
|
||
assert_eq!(flat(&mut s)["cutouts"].as_array().map(Vec::len), Some(1));
|
||
// Convert a plate.
|
||
let mut s = Session::default();
|
||
run(&mut s, "PrimitiveBox", json!({"length": 100, "width": 60, "height": 2.5, "body_name": "Plate"}));
|
||
run(&mut s, "ConvertToSheetMetalCmd", json!({"body": "Plate", "face": [50, 30, 2.5]}));
|
||
run(&mut s, "FusionSheetMetalFlangeCommand", json!({"edges": [[50, 0, 2.5]], "height": 20}));
|
||
assert!(rel(volume(&mut s), 18972.622) < 1e-4, "{}", volume(&mut s));
|
||
let mut t = Session::default();
|
||
run(&mut t, "PrimitiveCylinder", json!({"radius": 10, "height": 30, "body_name": "Rod"}));
|
||
assert!(t.execute("ConvertToSheetMetalCmd", &json!({"body": "Rod", "face": [0, 0, 30]})).is_err());
|
||
}
|