Why: every reference a user makes to a face or edge (fillet and chamfer edges, sketches on faces, shell/draft/offset faces, joints, appearances) is stored as a name and found again after any upstream edit. Those names were re-derived by sampling points of each face and voting, and pieces of a split face were numbered by where their centres lay. After an upstream edit that splits, merges, mirrors or shrinks a face, a fillet or sketch could silently re-attach to the wrong face, and nothing told the user. The kernel now reports exact history for its operations, so names follow it and fail loudly instead of guessing. - crates/doc/src/lineage.rs: a feature's kernel histories composed into one lineage over nameable operands (bodies before the feature, moved copies, shell cavities, tools named by role: sketch curve, cap, box side, imported face). Instrumented: extrude/revolve/sweep/loft tools, join/cut/intersect, lumps after a cut, fillet/chamfer, shell, draft, split face/body, pattern/mirror (bodies and features), combine, boundary fill, move/scale, offset/delete face, thread, import. - crates/doc/src/history_names.rs: names from lineage; split pieces told apart by the faces only they touch (F2:top#[F2:side:l4,F4:side:l4]), merged faces answer to every name (aliases), lump bodies ordered by their faces' names instead of shell order. - crates/doc/src/name_lookup.rs, piece_names.rs: references resolve by exact name, alias or bounding faces, else nearest piece with a warning; names worked out from geometry (operations without history) are reported when only geometry decided. - Format 14: names stored by older formats get a legacy mark and are found as the build that saved them found them (some geometric names name a face history names differently: a tube's outer wall was named after its inner circle). New corpus file saved by 9a979cf. - kernel: Body::lumps carries each piece's history over the whole. - Adversarial tests: split, merge, mirrored twins, shrink to a sliver, moving cut with body pieces, history-less bodies, certified sketch. - docs/book/src/dev/naming.md describes the real mechanism and why. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01J53sR6gEaVt56soFDpTck8
81 lines
3.6 KiB
Rust
81 lines
3.6 KiB
Rust
//! Recorded ownership changes are evaluated at their timeline position.
|
|
use crate::{DocError, Document, Mat, ModelBody, ModelState, Result};
|
|
use solvecraft_geom::Vec3;
|
|
use std::collections::BTreeSet;
|
|
|
|
pub fn valid_transfer_matrix(m: &Mat) -> bool {
|
|
let axis = |i: usize| Vec3::new(m[i][0], m[i][1], m[i][2]);
|
|
let (x, y, z) = (axis(0), axis(1), axis(2));
|
|
m.iter().flatten().all(|v| v.is_finite())
|
|
&& m[3][..3].iter().all(|v| v.abs() <= 1e9)
|
|
&& [x, y, z].iter().all(|v| (v.dot(*v) - 1.0).abs() < 1e-6)
|
|
&& x.dot(y).abs() < 1e-6
|
|
&& x.dot(z).abs() < 1e-6
|
|
&& y.dot(z).abs() < 1e-6
|
|
&& x.cross(y).dot(z) > 0.0
|
|
&& [m[0][3], m[1][3], m[2][3]].iter().all(|v| v.abs() < 1e-9)
|
|
&& (m[3][3] - 1.0).abs() < 1e-9
|
|
}
|
|
|
|
/// Validate the stored boundary before allocation, loading, or evaluation.
|
|
pub fn validate_body_transfer(doc: &Document, bodies: &[String], from: u64, to: u64, transform: &Mat) -> Result<()> {
|
|
if bodies.is_empty()
|
|
|| bodies.len() > 1000
|
|
|| bodies.iter().any(|name| name.trim().is_empty() || name.len() > 512 || name.chars().any(char::is_control))
|
|
|| bodies.iter().collect::<BTreeSet<_>>().len() != bodies.len()
|
|
|| !valid_transfer_matrix(transform)
|
|
|| [from, to].iter().any(|id| *id != 0 && !doc.components.iter().any(|c| c.id == *id))
|
|
{
|
|
return Err(DocError::Invalid("invalid body transfer boundary".into()));
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
pub(crate) fn evaluate(doc: &Document, st: &mut ModelState, bodies: &[String], from: u64, to: u64, transform: Mat) -> Result<()> {
|
|
validate_body_transfer(doc, bodies, from, to, &transform)?;
|
|
let mut next = st.clone();
|
|
for name in bodies {
|
|
let old = st.body(name).ok_or_else(|| DocError::Unknown(format!("transferred body `{name}`")))?;
|
|
if doc.model_body_component(old) != from {
|
|
return Err(DocError::Invalid(format!("body `{name}` no longer belongs to transfer source component {from}")));
|
|
}
|
|
let shape = if crate::is_identity(&transform) { old.body.clone() } else { solvecraft_kernel::transform_matrix(&old.body, transform)? };
|
|
let mut body = ModelBody::new(old.name.clone(), shape, old.feature);
|
|
body.component = to;
|
|
body.transferred = true;
|
|
body.source_name = old.source_name.clone();
|
|
body.source_transform = crate::mat_mul(&transform, &old.source_transform);
|
|
body.imported = old.imported.clone();
|
|
body.names = crate::naming::NameCell::same(old);
|
|
let slot = next.bodies.iter_mut().find(|b| b.name == *name).ok_or_else(|| DocError::Unknown(format!("transferred body `{name}`")))?;
|
|
*slot = body;
|
|
}
|
|
crate::body_transfer_sheet::remap_sheet_metadata(&mut next, bodies, &transform)?;
|
|
*st = next;
|
|
Ok(())
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn invalid_transfer_constructor_preserves_document_and_allocator() {
|
|
let mut doc = Document::default();
|
|
let before = doc.to_json();
|
|
let mut scaled = crate::IDENTITY;
|
|
scaled[0][0] = 2.0;
|
|
for (bodies, from, transform) in [
|
|
(vec!["Block".into()], 42, crate::IDENTITY),
|
|
(vec!["Block".into(), "Block".into()], 0, crate::IDENTITY),
|
|
(vec![String::new()], 0, crate::IDENTITY),
|
|
(vec![" ".into()], 0, crate::IDENTITY),
|
|
(vec!["bad\nname".into()], 0, crate::IDENTITY),
|
|
(vec!["Block".into()], 0, scaled),
|
|
] {
|
|
assert!(doc.add_feature(crate::FeatureKind::BodyTransfer { bodies, from, to: 0, transform }, None).is_err());
|
|
assert_eq!(doc.to_json(), before);
|
|
}
|
|
}
|
|
}
|