* WIP deep rename (not for push) * WIP2 * WIP3 * WIP4 wxs fix * rustfmt * Rename the remaining identifiers in merged crash-report code Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RjGw4gA5ZvKrU8MQP6sBgT --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
122 lines
6.8 KiB
Rust
122 lines
6.8 KiB
Rust
//! Primitive placement points (`base`, `corner`, `center`) take expressions like their sizes do,
|
|
//! follow parameter changes, and are refused when they cannot be read (user report: a cylinder
|
|
//! with `base: ["basin_width/2", …]` silently landed at the origin).
|
|
|
|
use serde_json::{Value, json};
|
|
|
|
use super::run;
|
|
use crate::*;
|
|
|
|
fn bbox(s: &Session, body: &str) -> ([f64; 3], [f64; 3]) {
|
|
let b = s.model.state().body(body).map(|b| b.body.clone()).unwrap();
|
|
let m = solidcraft_kernel::measure(&b).unwrap();
|
|
([m.bbox.min.x, m.bbox.min.y, m.bbox.min.z], [m.bbox.max.x, m.bbox.max.y, m.bbox.max.z])
|
|
}
|
|
|
|
fn near(a: [f64; 3], b: [f64; 3]) -> bool {
|
|
a.iter().zip(b).all(|(x, y)| (x - y).abs() < 1e-3)
|
|
}
|
|
|
|
#[test]
|
|
fn cylinder_base_expressions_follow_parameters() {
|
|
let mut s = Session::default();
|
|
run(&mut s, "parameters.change", json!({"name": "basin_width", "expression": "100 mm"}));
|
|
run(&mut s, "solid.cylinder", json!({"radius": 5, "height": 10, "base": ["basin_width/2", "10 mm", "-2"], "body_name": "Pin"}));
|
|
let (lo, hi) = bbox(&s, "Pin");
|
|
assert!(near(lo, [45.0, 5.0, -2.0]) && near(hi, [55.0, 15.0, 8.0]), "{lo:?} {hi:?}");
|
|
// A parameter change moves it on recompute.
|
|
run(&mut s, "parameters.change", json!({"name": "basin_width", "expression": "60 mm"}));
|
|
let (lo, _) = bbox(&s, "Pin");
|
|
assert!(near(lo, [25.0, 5.0, -2.0]), "{lo:?}");
|
|
// And it survives a save and reopen.
|
|
let text = s.doc.to_json();
|
|
assert!(text.contains("basin_width/2"), "{text}");
|
|
let s2 = Session::new(solidcraft_doc::Document::from_json(&text).unwrap());
|
|
let (lo, _) = bbox(&s2, "Pin");
|
|
assert!(near(lo, [25.0, 5.0, -2.0]), "{lo:?}");
|
|
}
|
|
|
|
#[test]
|
|
fn primitive_points_take_expressions() {
|
|
let mut s = Session::default();
|
|
run(&mut s, "parameters.change", json!({"name": "w", "expression": "40 mm"}));
|
|
run(&mut s, "solid.box", json!({"length": 10, "width": 10, "height": 10, "corner": ["w", 0, 0], "body_name": "A"}));
|
|
run(&mut s, "solid.box", json!({"length": "w", "width": 10, "height": 10, "center": [0, "w", 0], "body_name": "B"}));
|
|
run(&mut s, "solid.sphere", json!({"radius": 2, "center": ["w * 2", 0, 0], "body_name": "C"}));
|
|
run(&mut s, "solid.torus", json!({"major": 5, "minor": 1, "center": [0, 0, "w"], "body_name": "D"}));
|
|
run(&mut s, "solid.coil", json!({"diameter": 10, "revolutions": 2, "pitch": 5, "section_size": 1, "base": ["-w", 0, 0], "body_name": "E"}));
|
|
assert!(near(bbox(&s, "A").0, [40.0, 0.0, 0.0]));
|
|
assert!(near(bbox(&s, "B").0, [-20.0, 35.0, -5.0]), "{:?}", bbox(&s, "B"));
|
|
assert!(near(bbox(&s, "C").0, [78.0, -2.0, -2.0]));
|
|
assert!(near(bbox(&s, "D").0, [-6.0, -6.0, 39.0]));
|
|
assert!((bbox(&s, "E").0[0] + 45.5).abs() < 0.2, "{:?}", bbox(&s, "E"));
|
|
run(&mut s, "parameters.change", json!({"name": "w", "expression": "20 mm"}));
|
|
assert!(near(bbox(&s, "A").0, [20.0, 0.0, 0.0]));
|
|
assert!(near(bbox(&s, "B").0, [-10.0, 15.0, -5.0]), "{:?}", bbox(&s, "B"));
|
|
assert!(near(bbox(&s, "C").0, [38.0, -2.0, -2.0]));
|
|
assert!(near(bbox(&s, "D").0, [-6.0, -6.0, 19.0]));
|
|
assert!((bbox(&s, "E").0[0] + 25.5).abs() < 0.2, "{:?}", bbox(&s, "E"));
|
|
}
|
|
|
|
/// Points and axes that cannot be read are refused, never silently put at the origin.
|
|
#[test]
|
|
fn unreadable_primitive_points_are_refused() {
|
|
let mut s = Session::default();
|
|
let cases: Vec<(&str, Value)> = vec![
|
|
("solid.cylinder", json!({"radius": 5, "height": 10, "base": ["nope/2", 0, 0]})),
|
|
("solid.cylinder", json!({"radius": 5, "height": 10, "base": [1, 2]})),
|
|
("solid.cylinder", json!({"radius": 5, "height": 10, "base": "XY"})),
|
|
("solid.cylinder", json!({"radius": 5, "height": 10, "base": [1, 2, "5 deg"]})),
|
|
("solid.cylinder", json!({"radius": 5, "height": 10, "axis": ["x", 0, 1]})),
|
|
("solid.cylinder", json!({"radius": 5, "height": 10, "axis": [0, 0, 0]})),
|
|
("solid.box", json!({"length": 1, "width": 1, "height": 1, "corner": [0, null, 0]})),
|
|
("solid.box", json!({"length": 1, "width": 1, "height": 1, "center": {"x": 1}})),
|
|
("solid.sphere", json!({"radius": 1, "center": ["a +", 0, 0]})),
|
|
("solid.torus", json!({"major": 5, "minor": 1, "center": [1e300, 0, 0]})),
|
|
("solid.coil", json!({"diameter": 10, "revolutions": 2, "pitch": 5, "section_size": 1, "base": [true, 0, 0]})),
|
|
];
|
|
for (cmd, p) in cases {
|
|
let before = s.doc.features.len();
|
|
assert!(s.execute(cmd, &p).is_err(), "{cmd} {p} should be refused");
|
|
assert_eq!(s.doc.features.len(), before, "{cmd} {p} added a feature");
|
|
}
|
|
}
|
|
|
|
/// Renaming a parameter follows into placement points; deleting it is refused while a point
|
|
/// uses it. Plain-number points are stored as numbers, as before.
|
|
#[test]
|
|
fn point_parameters_rename_and_store() {
|
|
let mut s = Session::default();
|
|
run(&mut s, "parameters.change", json!({"name": "off", "expression": "30 mm"}));
|
|
run(&mut s, "solid.sphere", json!({"radius": 2, "center": ["off", 0, 0], "body_name": "S"}));
|
|
run(&mut s, "solid.box", json!({"length": 1, "width": 1, "height": 1, "corner": [1.5, -2, 0], "body_name": "B"}));
|
|
assert!(s.execute("parameters.delete", &json!({"name": "off"})).is_err());
|
|
run(&mut s, "parameters.rename", json!({"name": "off", "new_name": "shift"}));
|
|
run(&mut s, "parameters.change", json!({"name": "shift", "expression": "10 mm"}));
|
|
assert!(near(bbox(&s, "S").0, [8.0, -2.0, -2.0]), "{:?}", bbox(&s, "S"));
|
|
let v: Value = serde_json::from_str(&s.doc.to_json()).unwrap();
|
|
let feats = v["features"].as_array().unwrap();
|
|
assert_eq!(feats[0]["center"], json!(["shift", 0.0, 0.0]), "{v}");
|
|
assert_eq!(feats[1]["corner"], json!([1.5, -2.0, 0.0]), "{v}");
|
|
}
|
|
|
|
/// A primitive placed by an expression in a moved component lands in that component's frame
|
|
/// and keeps following its parameter.
|
|
#[test]
|
|
fn point_expressions_in_a_moved_component() {
|
|
let mut s = Session::default();
|
|
run(&mut s, "parameters.change", json!({"name": "x0", "expression": "10 mm"}));
|
|
let c = run(&mut s, "component.create", json!({"name": "Arm"}));
|
|
let (cid, occ) = (c["component"].as_u64().unwrap(), c["occurrence"].as_u64().unwrap());
|
|
run(&mut s, "component.activate", json!({"component": 0}));
|
|
run(&mut s, "occurrence.move", json!({"occurrence": occ, "translate": [0, 50, 0]}));
|
|
run(&mut s, "component.activate", json!({"component": cid}));
|
|
run(&mut s, "solid.cylinder", json!({"radius": 1, "height": 2, "base": ["x0", 60, 0], "body_name": "Pin"}));
|
|
let wlo = |s: &Session| s.world_state().body("Pin").unwrap().mesh().bounds().min;
|
|
let lo = wlo(&s);
|
|
assert!((lo.x - 9.0).abs() < 1e-3 && (lo.y - 59.0).abs() < 1e-3, "{lo:?}");
|
|
run(&mut s, "parameters.change", json!({"name": "x0", "expression": "20 mm"}));
|
|
let lo = wlo(&s);
|
|
assert!((lo.x - 19.0).abs() < 1e-3 && (lo.y - 59.0).abs() < 1e-3, "{lo:?}");
|
|
}
|