Joints mate by default (Fusion's convention): origin B's z is turned against origin A's, so two picked faces meet face to face and B sits on A instead of inside it; Flip now means don't mate. File format 4 inverts older joints' flip so they keep their placement; as-built joints and rigid groups stay where they are
Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01AS5Tn3xieewUSg3uxrMVdL
This commit is contained in:
1 parent
d12c955220
commit
2c383460e9
5 files changed
+46
-14
No files matched your search
@@ -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<u32> {
|
||||
|
||||
/// 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);
|
||||
|
||||
@@ -122,7 +122,8 @@ pub struct Joint {
|
||||
/// Per value: (min, max) limits.
|
||||
#[serde(default)]
|
||||
pub limits: Vec<Option<(f64, f64)>>,
|
||||
/// 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<Mat> {
|
||||
|
||||
@@ -303,7 +303,7 @@ fn as_built(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
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,
|
||||
|
||||
@@ -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)));
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user