Booleans stay in their component: a join, cut or intersect without explicit targets changes only its own component's bodies (and what it made), auto join/cut looks only at the active component; any feature takes participants: [bodies] to reach other components ("*" = every body); file format 5 gives older features in designs with components participants ["*"] so they keep their shape
Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01AS5Tn3xieewUSg3uxrMVdL
This commit is contained in:
1 parent
2886399215
commit
7c611aec0c
5 files changed
+84
-5
No files matched your search
@@ -1045,6 +1045,10 @@ pub struct Feature {
|
||||
/// Persistent names of the faces at the feature's face points (`FeatureKind::face_points`).
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub face_names: Vec<String>,
|
||||
/// Bodies a join/cut/intersect may change (any component); empty = the feature's own
|
||||
/// component's bodies.
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub participants: Vec<String>,
|
||||
#[serde(flatten)]
|
||||
pub kind: FeatureKind,
|
||||
}
|
||||
@@ -1324,6 +1328,7 @@ impl Document {
|
||||
edge_refs: Vec::new(),
|
||||
edge_names: Vec::new(),
|
||||
face_names: Vec::new(),
|
||||
participants: Vec::new(),
|
||||
kind,
|
||||
};
|
||||
match self.marker {
|
||||
|
||||
+13
-2
@@ -25,11 +25,13 @@ pub struct ModelBody {
|
||||
key: u64,
|
||||
/// Persistent face names (worked out on first use).
|
||||
pub(crate) names: crate::naming::NameCell,
|
||||
/// The component the body belongs to (set after each feature).
|
||||
pub component: u64,
|
||||
}
|
||||
|
||||
impl ModelBody {
|
||||
pub fn new(name: String, body: Body, feature: u64) -> Self {
|
||||
ModelBody { name, body, feature, mesh: OnceLock::new(), key: 0, names: Default::default() }
|
||||
ModelBody { name, body, feature, mesh: OnceLock::new(), key: 0, names: Default::default(), component: 0 }
|
||||
}
|
||||
/// Display mesh (chord tolerance 1/1000 of the body size), computed once.
|
||||
pub fn mesh(&self) -> Arc<Mesh> {
|
||||
@@ -564,6 +566,9 @@ impl Model {
|
||||
_ => Vec::new(),
|
||||
};
|
||||
next.stamp(&state, f, key, mats);
|
||||
for b in &mut next.bodies {
|
||||
b.component = doc.body_component(&b.name, b.feature);
|
||||
}
|
||||
state = Arc::new(next);
|
||||
None
|
||||
}
|
||||
@@ -652,8 +657,14 @@ fn new_name(state: &mut ModelState, f: &Feature, k: usize) -> String {
|
||||
|
||||
/// Add tool bodies to the model according to the operation.
|
||||
fn apply_op(state: &mut ModelState, f: &Feature, tools: Vec<Body>, op: Operation, targets: &[String]) -> Result<()> {
|
||||
let target_idx: Vec<usize> = if targets.is_empty() {
|
||||
// Participants: the feature's own targets, else its explicit participants, else the bodies
|
||||
// of its component (and what it made itself): other components are left alone.
|
||||
let targets: &[String] = if targets.is_empty() { &f.participants } else { targets };
|
||||
let all = targets.len() == 1 && targets.first().is_some_and(|t| t == "*");
|
||||
let target_idx: Vec<usize> = if all {
|
||||
(0..state.bodies.len()).collect()
|
||||
} else if targets.is_empty() {
|
||||
(0..state.bodies.len()).filter(|i| state.bodies.get(*i).is_some_and(|b| b.component == f.component || b.feature == f.id)).collect()
|
||||
} else {
|
||||
let mut v = Vec::new();
|
||||
for t in targets {
|
||||
|
||||
@@ -13,6 +13,9 @@
|
||||
//! - 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.
|
||||
//! - 5: joins, cuts and intersects change only their own component's bodies unless bodies are
|
||||
//! named as `participants`; older features in designs with components get `participants:
|
||||
//! ["*"]` (every body), which is what they did, so those designs keep their shape.
|
||||
//! - 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.
|
||||
|
||||
@@ -21,7 +24,7 @@ use serde_json::Value;
|
||||
use crate::{DocError, Document, Result};
|
||||
|
||||
/// The format this build writes.
|
||||
pub const FORMAT: u32 = 4;
|
||||
pub const FORMAT: u32 = 5;
|
||||
|
||||
pub fn format_string(n: u32) -> String {
|
||||
format!("solvecraft/{n}")
|
||||
@@ -34,7 +37,26 @@ 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; 3] = [|_| Ok(()), v2_to_v3, v3_to_v4];
|
||||
const JSON_STEPS: [JsonStep; 4] = [|_| Ok(()), v2_to_v3, v3_to_v4, v4_to_v5];
|
||||
|
||||
/// Features in designs with components keep reaching every body.
|
||||
fn v4_to_v5(v: &mut Value) -> Result<()> {
|
||||
let has_components = v.get("components").and_then(Value::as_array).is_some_and(|c| !c.is_empty());
|
||||
if !has_components {
|
||||
return Ok(());
|
||||
}
|
||||
if let Some(Value::Array(fs)) = v.get_mut("features") {
|
||||
for f in fs.iter_mut() {
|
||||
if let Some(o) = f.as_object_mut()
|
||||
&& o.get("type").and_then(Value::as_str) != Some("sketch")
|
||||
&& o.get("participants").is_none()
|
||||
{
|
||||
o.insert("participants".into(), serde_json::json!(["*"]));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Joints placed with the old convention (aligned unless flipped) keep their placement.
|
||||
fn v3_to_v4(v: &mut Value) -> Result<()> {
|
||||
|
||||
Reference in new issue
Block a user