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 <[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 a283e424d9
commit 10a67ef259
8 files changed
+929 -8

No files matched your search

+20 -1
View File
@@ -148,11 +148,30 @@ pub struct Assembly {
pub origins: Vec<NamedOrigin>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub links: Vec<MotionLink>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub contact_sets: Vec<crate::motion::ContactSet>,
/// Contact sets take effect.
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub contact_enabled: bool,
/// Every occurrence is in contact with every other (Enable All Contact).
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub contact_all: bool,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub studies: Vec<crate::motion::MotionStudy>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub exploded: Vec<crate::motion::ExplodedView>,
}
impl Assembly {
pub fn is_empty(&self) -> bool {
self.joints.is_empty() && self.origins.is_empty() && self.links.is_empty()
self.joints.is_empty()
&& self.origins.is_empty()
&& self.links.is_empty()
&& self.contact_sets.is_empty()
&& !self.contact_enabled
&& !self.contact_all
&& self.studies.is_empty()
&& self.exploded.is_empty()
}
}
+1
View File
@@ -14,6 +14,7 @@ mod eval;
pub mod expr;
pub mod format;
pub mod joints;
pub mod motion;
mod params;
pub mod plastic;
pub mod project;
+76
View File
@@ -0,0 +1,76 @@
//! Assembly extras: contact sets (occurrences that stop each other when joints are driven),
//! motion studies (joint values keyed along a timeline) and exploded views (occurrence moves
//! shown without changing the design).
use serde::{Deserialize, Serialize};
use solvecraft_geom::Vec3;
/// Occurrences whose bodies may touch but not pass through each other while joints move.
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct ContactSet {
pub id: u64,
pub name: String,
pub occurrences: Vec<u64>,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub suppressed: bool,
}
/// One joint value along a motion study: (step, value) points, values in internal units
/// (radians, mm), linear in between and held before the first and after the last.
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct StudyKey {
pub joint: u64,
#[serde(default)]
pub index: usize,
pub points: Vec<(f64, f64)>,
}
impl StudyKey {
pub fn value_at(&self, step: f64) -> Option<f64> {
let first = self.points.first()?;
let last = self.points.last()?;
if step <= first.0 {
return Some(first.1);
}
if step >= last.0 {
return Some(last.1);
}
self.points.windows(2).find_map(|w| {
let (a, b) = (w.first()?, w.get(1)?);
(step >= a.0 && step <= b.0).then(|| if b.0 - a.0 < 1e-12 { b.1 } else { a.1 + (b.1 - a.1) * (step - a.0) / (b.0 - a.0) })
})
}
}
/// A motion study: a timeline of `steps` steps along which joints follow their keys.
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct MotionStudy {
pub id: u64,
pub name: String,
pub steps: u32,
pub keys: Vec<StudyKey>,
}
/// An exploded view: occurrences moved apart (shown only; the design keeps its placements).
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct ExplodedView {
pub id: u64,
pub name: String,
/// (occurrence, world translation)
pub moves: Vec<(u64, Vec3)>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn keys_interpolate_and_hold() {
let k = StudyKey { joint: 1, index: 0, points: vec![(0.0, 0.0), (10.0, 1.0), (20.0, -1.0)] };
assert_eq!(k.value_at(-5.0), Some(0.0));
assert_eq!(k.value_at(5.0), Some(0.5));
assert_eq!(k.value_at(15.0), Some(0.0));
assert_eq!(k.value_at(99.0), Some(-1.0));
assert_eq!(StudyKey { joint: 1, index: 0, points: vec![] }.value_at(1.0), None);
}
}