* 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 <[email protected]> Claude-Session: https://claude.ai/code/session_01RjGw4gA5ZvKrU8MQP6sBgT --------- Co-authored-by: Claude Opus 5.5 <[email protected]>
167 lines
9.3 KiB
Rust
167 lines
9.3 KiB
Rust
use serde_json::{Value, json};
|
||
use solidcraft_geom::Vec3;
|
||
|
||
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 occ(s: &Session, component: &str) -> u64 {
|
||
let c = s.doc.find_component(component).unwrap_or(0);
|
||
s.doc.occurrence_of(c).map(|o| o.id).unwrap_or(0)
|
||
}
|
||
|
||
/// Where B's corner (20, 0, 0) is now.
|
||
fn b_x(s: &Session) -> f64 {
|
||
super::world_point(s, occ(s, "B"), Vec3::new(20.0, 0.0, 0.0)).x
|
||
}
|
||
|
||
/// Cubes A (grounded, x 0…10) and B (x 20…30) on a slider along x: B's −x face on A's +x face
|
||
/// at value 0, B 10 mm away to start.
|
||
fn rail() -> Session {
|
||
let mut s = Session::default();
|
||
for (name, x) in [("A", 0), ("B", 20)] {
|
||
run(&mut s, "component.activate", json!({"component": "root"}));
|
||
run(&mut s, "component.create", json!({"name": name}));
|
||
run(&mut s, "solid.box", json!({"length": 10, "width": 10, "height": 10, "corner": [x, 0, 0]}));
|
||
}
|
||
run(&mut s, "component.activate", json!({"component": "root"}));
|
||
let a = occ(&s, "A");
|
||
run(&mut s, "occurrence.ground", json!({"occurrence": a, "grounded": true}));
|
||
run(&mut s, "joint.create", json!({"type": "slider", "a": {"face": [10, 5, 5]}, "b": {"face": [20, 5, 5]}, "name": "Rail"}));
|
||
run(&mut s, "joint.drive", json!({"joint": "Rail", "value": 10}));
|
||
assert!((b_x(&s) - 20.0).abs() < 1e-6, "{}", b_x(&s));
|
||
s
|
||
}
|
||
|
||
#[test]
|
||
fn contact_sets_stop_a_driven_joint_at_touching() {
|
||
let mut s = rail();
|
||
// No contact: B passes into A.
|
||
run(&mut s, "joint.drive", json!({"joint": "Rail", "value": -5}));
|
||
assert!((b_x(&s) - 5.0).abs() < 1e-6);
|
||
run(&mut s, "joint.drive", json!({"joint": "Rail", "value": 10}));
|
||
run(&mut s, "contact.create", json!({"occurrences": ["A", "B"]}));
|
||
let r = run(&mut s, "joint.drive", json!({"joint": "Rail", "value": -5}));
|
||
assert!(r["stopped_by_contact"].is_object(), "{r}");
|
||
// Stopped where the faces touch (to the search's resolution, 15 mm / 2^14).
|
||
assert!((b_x(&s) - 10.0).abs() < 2e-3, "{}", b_x(&s));
|
||
assert!(b_x(&s) >= 10.0 - 2e-3, "a touch, not a pass");
|
||
assert_eq!(run(&mut s, "contact.check", json!({}))["collisions"].as_array().map(Vec::len), Some(0));
|
||
// Away is free; contact off lets it through again.
|
||
let r = run(&mut s, "joint.drive", json!({"joint": "Rail", "value": 3}));
|
||
assert!(r["stopped_by_contact"].is_null() && (b_x(&s) - 13.0).abs() < 1e-6, "{r}");
|
||
run(&mut s, "contact.disable_all", json!({}));
|
||
run(&mut s, "joint.drive", json!({"joint": "Rail", "value": -5}));
|
||
assert!((b_x(&s) - 5.0).abs() < 1e-6);
|
||
// Suppressed sets don't count; Enable All Contact puts every occurrence in one.
|
||
run(&mut s, "joint.drive", json!({"joint": "Rail", "value": 10}));
|
||
run(&mut s, "contact.enable_sets", json!({}));
|
||
run(&mut s, "contact.edit", json!({"set": "Contact Set1", "suppressed": true}));
|
||
run(&mut s, "joint.drive", json!({"joint": "Rail", "value": -5}));
|
||
assert!((b_x(&s) - 5.0).abs() < 1e-6);
|
||
run(&mut s, "joint.drive", json!({"joint": "Rail", "value": 10}));
|
||
run(&mut s, "contact.enable_all", json!({}));
|
||
run(&mut s, "joint.drive", json!({"joint": "Rail", "value": -5}));
|
||
assert!((b_x(&s) - 10.0).abs() < 2e-3);
|
||
assert!(s.execute("contact.create", &json!({"occurrences": ["A"]})).is_err());
|
||
}
|
||
|
||
#[test]
|
||
fn contact_uses_preserved_body_placements_after_component_reassignment() {
|
||
let mut s = Session::default();
|
||
run(&mut s, "solid.box", json!({"length": 10, "width": 10, "height": 10, "body_name": "Transferred"}));
|
||
let component = run(&mut s, "component.create", json!({"name": "Carrier"}));
|
||
run(&mut s, "occurrence.move", json!({"occurrence": component["occurrence"], "translate": [100, 0, 0]}));
|
||
run(&mut s, "component.move_bodies", json!({"bodies": ["Transferred"], "component": component["component"]}));
|
||
run(&mut s, "component.activate", json!({"component": "root"}));
|
||
let probe = run(&mut s, "component.create", json!({"name": "Probe"}));
|
||
run(&mut s, "solid.box", json!({"length": 10, "width": 10, "height": 10, "corner": [5, 0, 0]}));
|
||
run(&mut s, "contact.create", json!({"occurrences": [component["occurrence"], probe["occurrence"]]}));
|
||
let collisions = run(&mut s, "contact.check", json!({}));
|
||
assert_eq!(collisions["collisions"].as_array().map(Vec::len), Some(1), "body stayed at x=0: {collisions}");
|
||
run(&mut s, "occurrence.move", json!({"occurrence": probe["occurrence"], "translate": [100, 0, 0]}));
|
||
let collisions = run(&mut s, "contact.check", json!({}));
|
||
assert_eq!(collisions["collisions"].as_array().map(Vec::len), Some(0), "probe moved away: {collisions}");
|
||
}
|
||
|
||
#[test]
|
||
fn automatic_explode_centres_follow_reassigned_bodies() {
|
||
let mut s = Session::default();
|
||
run(&mut s, "solid.box", json!({"length": 10, "width": 10, "height": 10, "body_name": "Transferred"}));
|
||
let component = run(&mut s, "component.create", json!({"name": "Carrier"}));
|
||
run(&mut s, "occurrence.move", json!({"occurrence": component["occurrence"], "translate": [100, 0, 0]}));
|
||
run(&mut s, "component.move_bodies", json!({"bodies": ["Transferred"], "component": component["component"]}));
|
||
run(&mut s, "component.activate", json!({"component": "root"}));
|
||
run(&mut s, "component.create", json!({"name": "Neighbour"}));
|
||
run(&mut s, "solid.box", json!({"length": 10, "width": 10, "height": 10, "corner": [20, 0, 0]}));
|
||
let exploded = run(&mut s, "explode.create", json!({"scale": 1}));
|
||
let moves = exploded["moves"].as_array().unwrap();
|
||
let carrier = moves.iter().find(|m| m["occurrence"] == component["occurrence"]).unwrap();
|
||
assert!((carrier["translate"][0].as_f64().unwrap() + 10.0).abs() < 1e-6, "{exploded}");
|
||
}
|
||
|
||
#[test]
|
||
fn motion_study_plays_and_exports() {
|
||
let mut s = rail();
|
||
let r = run(&mut s, "motion.study", json!({"name": "Push", "steps": 100, "keys": [{"joint": "Rail", "points": [[0, 10], [100, -5]]}]}));
|
||
assert_eq!(r["keys"], 1);
|
||
run(&mut s, "motion.play", json!({"study": "Push", "step": 50}));
|
||
assert!((b_x(&s) - 12.5).abs() < 1e-6, "{}", b_x(&s));
|
||
// With contact, the end of the study stops at touching.
|
||
run(&mut s, "contact.create", json!({"occurrences": ["A", "B"]}));
|
||
let r = run(&mut s, "motion.play", json!({"study": "Push", "step": 100}));
|
||
assert!(r["stopped_by_contact"].is_object() && (b_x(&s) - 10.0).abs() < 2e-3, "{r}");
|
||
// Export: positions per sample; the design keeps its joint value.
|
||
run(&mut s, "motion.play", json!({"study": "Push", "step": 0}));
|
||
let before = s.doc.clone();
|
||
let e = run(&mut s, "motion.export", json!({"study": "Push", "samples": 5}));
|
||
assert_eq!(*s.doc, *before);
|
||
let rows = e["samples"].as_array().cloned().unwrap_or_default();
|
||
assert_eq!(rows.len(), 5);
|
||
let bx = |row: &Value| {
|
||
let p = row["positions"].as_array().and_then(|a| a.iter().find(|p| p["occurrence"] == "B:1")).cloned().unwrap_or_default();
|
||
p["pose"]["origin"][0].as_f64().unwrap_or(f64::NAN)
|
||
};
|
||
// The B occurrence's origin moves by the slider value (it starts at 0 offset with value 0 at 10 mm… so 10 → −5).
|
||
assert!((bx(&rows[1]) - bx(&rows[0]) + 3.75).abs() < 1e-6, "{e}");
|
||
assert!(rows[4]["stopped_by_contact"].is_object());
|
||
let dir = std::env::temp_dir().join(format!("solidcraft-motion-{}", std::process::id()));
|
||
std::fs::create_dir_all(&dir).unwrap();
|
||
let csv = dir.join("push.csv");
|
||
run(&mut s, "motion.export", json!({"study": "Push", "samples": 3, "path": csv.to_string_lossy()}));
|
||
let text = std::fs::read_to_string(&csv).unwrap();
|
||
assert!(text.starts_with("step,occurrence,x,y,z") && text.lines().count() == 1 + 3 * 2, "{text}");
|
||
let _ = std::fs::remove_dir_all(&dir);
|
||
assert!(s.execute("motion.study", &json!({"keys": [{"joint": "Rail", "index": 3, "points": [[0, 1]]}]})).is_err());
|
||
run(&mut s, "motion.delete", json!({"study": "Push"}));
|
||
assert_eq!(run(&mut s, "motion.list", json!({}))["studies"].as_array().map(Vec::len), Some(0));
|
||
}
|
||
|
||
#[test]
|
||
fn exploded_views_show_without_changing_the_design() {
|
||
let mut s = rail();
|
||
let before = s.doc.occurrences.clone();
|
||
let r = run(&mut s, "explode.create", json!({"name": "Apart", "scale": 1}));
|
||
assert_eq!(r["moves"].as_array().map(Vec::len), Some(2), "{r}");
|
||
run(&mut s, "explode.show", json!({"view": "Apart"}));
|
||
// Centres at x 5 and 25 → middle 15: each moves 10 mm further out.
|
||
let bb = |s: &mut Session, b: &str| run(s, "inspect.measure", json!({"bodies": [b]}))["bodies"][0]["bbox"]["min"][0].as_f64().unwrap_or(f64::NAN);
|
||
let names: Vec<String> = s.world_state().bodies.iter().map(|b| b.name.clone()).collect();
|
||
assert!((bb(&mut s, &names[0]) + 10.0).abs() < 1e-6, "{names:?}");
|
||
assert!((bb(&mut s, &names[1]) - 30.0).abs() < 1e-6);
|
||
assert_eq!(s.doc.occurrences, before, "shown only");
|
||
run(&mut s, "explode.show", json!({"view": null}));
|
||
assert!(bb(&mut s, &names[0]).abs() < 1e-6);
|
||
run(&mut s, "explode.create", json!({"name": "Hand", "moves": [{"occurrence": "B", "translate": [0, 0, 50]}]}));
|
||
run(&mut s, "explode.show", json!({"view": "Hand"}));
|
||
assert!(
|
||
(run(&mut s, "inspect.measure", json!({"bodies": [names[1]]}))["bodies"][0]["bbox"]["min"][2].as_f64().unwrap_or(0.0) - 50.0).abs() < 1e-6
|
||
);
|
||
assert_eq!(run(&mut s, "explode.list", json!({}))["views"].as_array().map(Vec::len), Some(2));
|
||
}
|