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:
bherbruckandClaude Opus 5.5 committed 2026-10-08 11:37:33 +00:00
1 parent d12c955220
commit 2c383460e9
5 files changed
+46 -14

No files matched your search

+23 -2
View File
@@ -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);
+4 -2
View File
@@ -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> {