Record ownership transfers in the timeline so earlier Join/Cut membership, world placement, machining data and styles survive component moves, edits, independent copying, undo and reopening. Preserve pending positions in history snapshots and keep constructor transfers ordered during feature redefinition. Retain shallow sheet pockets and exact planar intersection islands through unfold/refold, with depth-aware flat previews and DXF export. Store explicit 3D wire coordinate spaces and migrate known legacy provenance without moving saved points or analytic fits. Infer unique eligible cylindrical Emboss targets, align Revolve manipulators with actual extent geometry, and discard obsolete preview errors after input correction. Certify constructed and immutable imported face styles using exact source metadata while preserving legacy face names. Upgrade designs to format10 with bounded validation and geometry-preserving legacy migration.
154 lines
6.6 KiB
Rust
154 lines
6.6 KiB
Rust
//! Atomic timeline ownership changes, preserving the selected world geometry.
|
|
use super::*;
|
|
use std::collections::{BTreeMap, BTreeSet};
|
|
|
|
type UniqueFrames = BTreeMap<u64, Option<(Vec<u64>, Mat)>>;
|
|
|
|
fn unique_frames(s: &Session, cmd: &str) -> Result<UniqueFrames> {
|
|
let doc = s.placed_document();
|
|
let mut children = BTreeMap::<u64, Vec<&Occurrence>>::new();
|
|
for occurrence in &doc.occurrences {
|
|
children.entry(occurrence.parent).or_default().push(occurrence);
|
|
}
|
|
let mut frames = BTreeMap::from([(0, Some((Vec::new(), solvecraft_doc::IDENTITY)))]);
|
|
let mut stack = vec![(0, Vec::new(), solvecraft_doc::IDENTITY)];
|
|
let mut work = 0usize;
|
|
while let Some((component, path, matrix)) = stack.pop() {
|
|
for occurrence in children.get(&component).into_iter().flatten() {
|
|
work += 1;
|
|
if path.len() >= 64 || work > 100_000 {
|
|
return Err(bad(cmd, "transfer assembly exceeds 64 levels or 100000 placements"));
|
|
}
|
|
let mut child_path = path.clone();
|
|
child_path.push(occurrence.id);
|
|
let frame = solvecraft_doc::mat_mul(&matrix, &occurrence.transform);
|
|
frames.entry(occurrence.component).and_modify(|entry| *entry = None).or_insert_with(|| Some((child_path.clone(), frame)));
|
|
stack.push((occurrence.component, child_path, frame));
|
|
}
|
|
}
|
|
Ok(frames)
|
|
}
|
|
|
|
fn body_names(s: &Session, p: &Value, cmd: &str, allow_boundary: bool) -> Result<Vec<String>> {
|
|
if p.get("bodies").and_then(Value::as_array).is_none_or(|names| names.iter().any(|name| !name.is_string())) {
|
|
return Err(bad(cmd, "bodies must be an array of body names"));
|
|
}
|
|
let names = string_list(p, "bodies");
|
|
if names.is_empty() || names.len() > 1000 || names.iter().collect::<BTreeSet<_>>().len() != names.len() {
|
|
return Err(bad(cmd, "bodies must list 1..1000 distinct existing definition bodies"));
|
|
}
|
|
if s.doc.marker.is_some() && !allow_boundary {
|
|
return Err(bad(cmd, "finish timeline rollback before transferring bodies"));
|
|
}
|
|
let state = s.model.state();
|
|
for name in &names {
|
|
if state.body(name).is_none() {
|
|
return Err(bad(cmd, format!("no body `{name}`")));
|
|
}
|
|
}
|
|
Ok(names)
|
|
}
|
|
|
|
fn transfer(s: &mut Session, name: &str, to: u64, frames: &UniqueFrames, cmd: &str) -> Result<u64> {
|
|
let state = s.model.state();
|
|
let body = state.body(name).ok_or_else(|| bad(cmd, "body is missing"))?;
|
|
let from = s.doc.model_body_component(body);
|
|
let doc = s.placed_document();
|
|
let unique = |component| {
|
|
frames.get(&component).and_then(Option::as_ref).ok_or_else(|| {
|
|
bad(cmd, "body transfer requires one source and destination placement; shared instances cannot preserve a unique world pose")
|
|
})
|
|
};
|
|
let source = unique(from)?;
|
|
let destination = unique(to)?;
|
|
let old = doc.model_body_offset(body).map(|offset| solvecraft_doc::mat_mul(&source.1, offset)).unwrap_or(source.1);
|
|
let inv = solvecraft_doc::mat_inverse(&destination.1).ok_or_else(|| bad(cmd, "destination frame is not invertible"))?;
|
|
let transform = solvecraft_doc::mat_mul(&inv, &old);
|
|
if !solvecraft_doc::valid_transfer_matrix(&transform) {
|
|
return Err(bad(cmd, "body transfer exceeds supported rigid coordinate range"));
|
|
}
|
|
let inherited = (!s.doc.appearances.bodies.contains_key(name)).then(|| s.doc.appearances.components.get(&from).cloned()).flatten();
|
|
let paths: BTreeSet<u64> = source.0.iter().chain(&destination.0).copied().collect();
|
|
let captures: Vec<_> = paths.into_iter().filter_map(|id| s.pending_moves.get(&id).map(|transform| (id, *transform))).collect();
|
|
if captures.iter().any(|(_, transform)| !solvecraft_doc::valid_transfer_matrix(transform)) {
|
|
return Err(bad(cmd, "pending transfer position is not a supported rigid transform"));
|
|
}
|
|
drop(doc);
|
|
for (id, transform) in captures {
|
|
if let Some(occurrence) = s.doc_mut().occurrences.iter_mut().find(|occurrence| occurrence.id == id) {
|
|
occurrence.transform = transform;
|
|
}
|
|
s.pending_moves.remove(&id);
|
|
}
|
|
if let Some(look) = inherited {
|
|
s.doc_mut().appearances.bodies.insert(name.to_string(), look);
|
|
}
|
|
let id = s.doc_mut().add_feature(FeatureKind::BodyTransfer { bodies: vec![name.to_string()], from, to, transform }, None)?;
|
|
if let Some(feature) = s.doc_mut().feature_mut(id) {
|
|
feature.component = to;
|
|
}
|
|
Ok(id)
|
|
}
|
|
|
|
fn finish(s: &mut Session, scratch: &mut Session, features: &[u64], cmd: &str) -> Result<()> {
|
|
scratch.refresh();
|
|
for id in features {
|
|
if let Some(error) = scratch.model.result(*id).and_then(|result| result.error.as_ref()) {
|
|
return Err(bad(cmd, error));
|
|
}
|
|
}
|
|
s.accept_computed(std::mem::take(scratch));
|
|
Ok(())
|
|
}
|
|
|
|
pub(super) fn from_bodies(s: &mut Session, p: &Value) -> Result<Value> {
|
|
let cmd = "component.from_bodies";
|
|
let bodies = body_names(s, p, cmd, false)?;
|
|
let mut scratch = s.scratch();
|
|
let parent = s.active_component;
|
|
let mut made = Vec::new();
|
|
let mut targets = Vec::new();
|
|
for name in &bodies {
|
|
let component = scratch.doc_mut().add_component(Some(name), parent)?;
|
|
targets.push(component);
|
|
}
|
|
let frames = unique_frames(&scratch, cmd)?;
|
|
let mut features = Vec::new();
|
|
for (name, component) in bodies.into_iter().zip(targets) {
|
|
let feature = transfer(&mut scratch, &name, component, &frames, cmd)?;
|
|
features.push(feature);
|
|
made.push(json!({"component":component,"body":name,"feature":feature}));
|
|
}
|
|
finish(s, &mut scratch, &features, cmd)?;
|
|
Ok(json!({"components":made}))
|
|
}
|
|
|
|
pub(super) fn move_bodies(s: &mut Session, p: &Value) -> Result<Value> {
|
|
move_at_boundary(s, p, false)
|
|
}
|
|
|
|
/// A solid constructor can attach its result immediately after the new feature,
|
|
/// including inside the scratch timeline used by Edit Feature.
|
|
pub(super) fn move_result(s: &mut Session, p: &Value) -> Result<Value> {
|
|
move_at_boundary(s, p, true)
|
|
}
|
|
|
|
fn move_at_boundary(s: &mut Session, p: &Value, allow_boundary: bool) -> Result<Value> {
|
|
let cmd = "component.move_bodies";
|
|
let to = super::component(s, p, "component", cmd)?;
|
|
let bodies = body_names(s, p, cmd, allow_boundary)?;
|
|
let mut scratch = s.scratch();
|
|
let frames = unique_frames(&scratch, cmd)?;
|
|
let mut features = Vec::new();
|
|
for name in &bodies {
|
|
features.push(transfer(&mut scratch, name, to, &frames, cmd)?);
|
|
}
|
|
finish(s, &mut scratch, &features, cmd)?;
|
|
Ok(json!({"bodies":bodies,"component":to,"features":features}))
|
|
}
|
|
|
|
#[cfg(test)]
|
|
#[path = "component_transfer_tests.rs"]
|
|
#[allow(clippy::unwrap_used, clippy::expect_used)]
|
|
mod tests;
|