diff --git a/crates/doc/src/format.rs b/crates/doc/src/format.rs index 2ee161ad..1e638d9c 100644 --- a/crates/doc/src/format.rs +++ b/crates/doc/src/format.rs @@ -13,13 +13,15 @@ //! - 3: appearances per body, face and component with names and opacity //! (`"appearances": {"bodies": {name: {name, color, opacity}}, "faces": […], "components": {…}}`); //! before, `"appearances"` mapped body names to bare `[r, g, b]` colours. +//! - 4: joint origins mate by default (B's z against A's, as Fusion does); `flip` now means +//! "don't mate". Older joints keep their placement: their `flip` is inverted. use serde_json::Value; use crate::{DocError, Document, Result}; /// The format this build writes. -pub const FORMAT: u32 = 3; +pub const FORMAT: u32 = 4; pub fn format_string(n: u32) -> String { format!("solvecraft/{n}") @@ -32,7 +34,20 @@ pub fn version_of(s: &str) -> Option { /// JSON-level upgrades: entry `i` turns format `i + 1` into `i + 2`. type JsonStep = fn(&mut Value) -> Result<()>; -const JSON_STEPS: [JsonStep; 2] = [|_| Ok(()), v2_to_v3]; +const JSON_STEPS: [JsonStep; 3] = [|_| Ok(()), v2_to_v3, v3_to_v4]; + +/// Joints placed with the old convention (aligned unless flipped) keep their placement. +fn v3_to_v4(v: &mut Value) -> Result<()> { + if let Some(Value::Array(js)) = v.get_mut("assembly").and_then(|a| a.get_mut("joints")) { + for j in js.iter_mut() { + if let Some(o) = j.as_object_mut() { + let f = o.get("flip").and_then(Value::as_bool).unwrap_or(false); + o.insert("flip".into(), Value::Bool(!f)); + } + } + } + Ok(()) +} /// Body colours `{name: [r, g, b]}` become named, opaque body appearances. fn v2_to_v3(v: &mut Value) -> Result<()> { @@ -182,6 +197,12 @@ mod tests { old["appearances"] = serde_json::json!({"Body1": [200, 10, 20]}); let d = read(&old.to_string()).unwrap(); let l = d.appearances.bodies.get("Body1").unwrap(); + // Format 3 joints (aligned unless flipped) keep their placement. + let mut j: Value = serde_json::from_str(&Document::new("A").to_json()).unwrap(); + j["format"] = Value::from("solvecraft/3"); + j["assembly"] = serde_json::json!({"joints": [{"id": 1, "name": "J", "kind": "rigid", "a": {"occurrence": 0, "snap": {"type": "point", "point": [0, 0, 0]}}, "b": {"occurrence": 0, "snap": {"type": "point", "point": [0, 0, 0]}}}]}); + let dj = read(&j.to_string()).unwrap(); + assert!(dj.assembly.joints[0].flip, "aligned before = not mated now"); assert_eq!((l.color, l.opacity, l.name.as_str()), ([200, 10, 20], 1.0, "Custom")); let mut v: Value = serde_json::from_str(&Document::new("A").to_json()).unwrap(); v["huge"] = Value::from(1e300); diff --git a/crates/doc/src/joints.rs b/crates/doc/src/joints.rs index 5c4a6eee..c3bfa8a6 100644 --- a/crates/doc/src/joints.rs +++ b/crates/doc/src/joints.rs @@ -122,7 +122,8 @@ pub struct Joint { /// Per value: (min, max) limits. #[serde(default)] pub limits: Vec>, - /// Turn B over (its z opposite to A's). + /// Don't mate: B's z along A's instead of against it (by default the origins mate, as two + /// faces meeting). #[serde(default, skip_serializing_if = "std::ops::Not::not")] pub flip: bool, /// Distance from A's frame to B's along z, and a turn about it (radians). @@ -404,7 +405,8 @@ pub fn solve(doc: &Document, st: &ModelState) -> Solution { let flip_m = |j: &Joint| { let mut m = rot_z(j.angle); m[3][2] = j.offset; - if j.flip { mat_mul(&m, &[[1.0, 0.0, 0.0, 0.0], [0.0, -1.0, 0.0, 0.0], [0.0, 0.0, -1.0, 0.0], [0.0, 0.0, 0.0, 1.0]]) } else { m } + // Origins mate (B's z against A's, as faces meet) unless flipped. + if !j.flip { mat_mul(&m, &[[1.0, 0.0, 0.0, 0.0], [0.0, -1.0, 0.0, 0.0], [0.0, 0.0, -1.0, 0.0], [0.0, 0.0, 0.0, 1.0]]) } else { m } }; // B's world transform from A's (or A's from B's, going the other way). let through = |j: &Joint, from_a: bool, t: &Mat| -> Option { diff --git a/crates/engine/src/cmd/joints.rs b/crates/engine/src/cmd/joints.rs index 292091fa..34c35427 100644 --- a/crates/engine/src/cmd/joints.rs +++ b/crates/engine/src/cmd/joints.rs @@ -303,7 +303,7 @@ fn as_built(s: &mut Session, p: &Value) -> Result { b: JointOrigin { occurrence: ob, snap: fb }, values: vec![0.0; n], limits: limits_param(s, p, kind, cmd)?, - flip: false, + flip: true, // as built: the origins coincide as they are, not mated offset: 0.0, angle: 0.0, suppressed: false, diff --git a/crates/engine/src/cmd/joints_tests.rs b/crates/engine/src/cmd/joints_tests.rs index 2c6f3e10..7b091ed1 100644 --- a/crates/engine/src/cmd/joints_tests.rs +++ b/crates/engine/src/cmd/joints_tests.rs @@ -45,7 +45,7 @@ fn revolute_hinge_turns_about_the_knuckle() { &mut s, "JointAssembleCmdNew", json!({ - "type": "revolute", "a": {"occurrence": "LeafA", "circle": [1, 37, 4]}, "b": {"occurrence": "LeafB", "circle": [1, 37, 4]}, "name": "Hinge"}), + "type": "revolute", "a": {"occurrence": "LeafA", "circle": [1, 37, 4]}, "b": {"occurrence": "LeafB", "circle": [1, 37, 4]}, "flip": true, "name": "Hinge"}), ); assert_eq!(r["conflicts"].as_array().map(Vec::len), Some(0), "{r}"); // Same frames: B stays put at angle 0. @@ -89,7 +89,7 @@ fn slider_limits_clamp() { let r = run( &mut s, "JointAssembleCmdNew", - json!({"type": "slider", "a": {"face": [5, 5, 10]}, "b": {"face": [25, 5, 0]}, "flip": true, "limits": [[0, 10]], "name": "Rail"}), + json!({"type": "slider", "a": {"face": [5, 5, 10]}, "b": {"face": [25, 5, 0]}, "limits": [[0, 10]], "name": "Rail"}), ); assert_eq!(r["conflicts"].as_array().map(Vec::len), Some(0), "{r}"); // B now sits on A. @@ -154,11 +154,7 @@ fn rigid_cycles_solve_and_conflicts_report() { #[test] fn motion_links_and_interference() { let mut s = two_cubes(); - run( - &mut s, - "JointAssembleCmdNew", - json!({"type": "cylindrical", "a": {"face": [5, 5, 10]}, "b": {"face": [25, 5, 0]}, "flip": true, "name": "Screw"}), - ); + run(&mut s, "JointAssembleCmdNew", json!({"type": "cylindrical", "a": {"face": [5, 5, 10]}, "b": {"face": [25, 5, 0]}, "name": "Screw"})); run(&mut s, "FusionMoveJointsCommand", json!({"joint": "Screw", "values": [0, 0]})); // No overlap while B sits on A; then push B 4 mm into A. let i = run(&mut s, "InterferenceCheckCommand", json!({})); @@ -174,7 +170,7 @@ fn motion_links_and_interference() { run( &mut s, "JointAssembleCmdNew", - json!({"type": "slider", "a": {"occurrence": "A", "face": [5, 5, 10]}, "b": {"occurrence": "D", "face": [65, 5, 0]}, "flip": true, "name": "Follower"}), + json!({"type": "slider", "a": {"occurrence": "A", "face": [5, 5, 10]}, "b": {"occurrence": "D", "face": [65, 5, 0]}, "name": "Follower"}), ); run(&mut s, "FusionMotionRelationshipCommand", json!({"a": "Screw", "ia": 1, "b": "Follower", "ratio": 2})); run(&mut s, "FusionMoveJointsCommand", json!({"joint": "Screw", "values": [0, 3]})); @@ -186,3 +182,16 @@ fn motion_links_and_interference() { let l = run(&mut t, "joint.list", json!({})); assert_eq!(l["joints"].as_array().map(Vec::len), Some(2)); } + +/// Two faces joined mate: B's bottom face lands on A's top face, B on top of A (not inside +/// it); Flip turns B over instead. +#[test] +fn faces_mate_by_default() { + let mut s = two_cubes(); + run(&mut s, "JointAssembleCmdNew", json!({"type": "rigid", "a": {"face": [5, 5, 10]}, "b": {"face": [25, 5, 0]}, "name": "Stack"})); + // B's bottom centre (25, 5, 0) at A's top centre, B above it. + assert!(near(place(&s, "B", Vec3::new(25.0, 5.0, 0.0)), Vec3::new(5.0, 5.0, 10.0))); + assert!(near(place(&s, "B", Vec3::new(25.0, 5.0, 10.0)), Vec3::new(5.0, 5.0, 20.0)), "{:?}", place(&s, "B", Vec3::new(25.0, 5.0, 10.0))); + run(&mut s, "joint.edit", json!({"joint": "Stack", "flip": true})); + assert!(near(place(&s, "B", Vec3::new(25.0, 5.0, 10.0)), Vec3::new(5.0, 5.0, 0.0)), "{:?}", place(&s, "B", Vec3::new(25.0, 5.0, 10.0))); +} diff --git a/crates/engine/src/cmd/motion_tests.rs b/crates/engine/src/cmd/motion_tests.rs index a133632d..030ae964 100644 --- a/crates/engine/src/cmd/motion_tests.rs +++ b/crates/engine/src/cmd/motion_tests.rs @@ -32,7 +32,7 @@ fn rail() -> Session { 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, "JointAssembleCmdNew", json!({"type": "slider", "a": {"face": [10, 5, 5]}, "b": {"face": [20, 5, 5]}, "name": "Rail"})); run(&mut s, "FusionMoveJointsCommand", json!({"joint": "Rail", "value": 10})); assert!((b_x(&s) - 20.0).abs() < 1e-6, "{}", b_x(&s)); s