Contact Sets (EnableContactSetsCmd, EnableAllContactCmd, DisableAllContactCmd, ContactSetCmd, contact.edit/list/check): driving a joint stops where bodies of one set would pass through each other (bisection to the touch; penetration found by ray parity and depth on the tessellations); Motion Study (FusionMotionStudyCommand: joint values keyed along a timeline, motion.play, motion.export of occurrence poses per step as JSON or CSV without changing the design, contact-aware); Exploded views (explode.create automatic or by hand, explode.show without changing placements)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AS5Tn3xieewUSg3uxrMVdL
This commit is contained in:
1 parent
a283e424d9
commit
10a67ef259
8 files changed
+929
-8
No files matched your search
@@ -0,0 +1,133 @@
|
||||
use serde_json::{Value, json};
|
||||
use solvecraft_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, "FusionCreateNewComponentCommand", json!({"name": name}));
|
||||
run(&mut s, "PrimitiveBox", 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, "JointAssembleCmdNew", json!({"type": "slider", "a": {"face": [10, 5, 5]}, "b": {"face": [20, 5, 5]}, "flip": true, "name": "Rail"}));
|
||||
run(&mut s, "FusionMoveJointsCommand", 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, "FusionMoveJointsCommand", json!({"joint": "Rail", "value": -5}));
|
||||
assert!((b_x(&s) - 5.0).abs() < 1e-6);
|
||||
run(&mut s, "FusionMoveJointsCommand", json!({"joint": "Rail", "value": 10}));
|
||||
run(&mut s, "ContactSetCmd", json!({"occurrences": ["A", "B"]}));
|
||||
let r = run(&mut s, "FusionMoveJointsCommand", 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, "FusionMoveJointsCommand", json!({"joint": "Rail", "value": 3}));
|
||||
assert!(r["stopped_by_contact"].is_null() && (b_x(&s) - 13.0).abs() < 1e-6, "{r}");
|
||||
run(&mut s, "DisableAllContactCmd", json!({}));
|
||||
run(&mut s, "FusionMoveJointsCommand", 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, "FusionMoveJointsCommand", json!({"joint": "Rail", "value": 10}));
|
||||
run(&mut s, "EnableContactSetsCmd", json!({}));
|
||||
run(&mut s, "contact.edit", json!({"set": "Contact Set1", "suppressed": true}));
|
||||
run(&mut s, "FusionMoveJointsCommand", json!({"joint": "Rail", "value": -5}));
|
||||
assert!((b_x(&s) - 5.0).abs() < 1e-6);
|
||||
run(&mut s, "FusionMoveJointsCommand", json!({"joint": "Rail", "value": 10}));
|
||||
run(&mut s, "EnableAllContactCmd", json!({}));
|
||||
run(&mut s, "FusionMoveJointsCommand", json!({"joint": "Rail", "value": -5}));
|
||||
assert!((b_x(&s) - 10.0).abs() < 2e-3);
|
||||
assert!(s.execute("ContactSetCmd", &json!({"occurrences": ["A"]})).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn motion_study_plays_and_exports() {
|
||||
let mut s = rail();
|
||||
let r =
|
||||
run(&mut s, "FusionMotionStudyCommand", 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, "ContactSetCmd", 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!("solvecraft-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("FusionMotionStudyCommand", &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, "MeasureCommand", 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, "MeasureCommand", 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));
|
||||
}
|
||||
Reference in new issue
Block a user