feat: preserve modeling intent across sketches, assemblies, and fabrication

Persist typed sketch dimensions and bounded ordered configuration rules in format 8. Use exact occurrence paths and source identity for editing, movement, appearance, and mass. Preserve sheet material regions through cutting, folding, and exact DXF export; retain nested cavities and material islands.

Validation: cargo xtask ci passed, including 368 engine tests, 178 UI tests, 68 UI scenarios, 136 kernel tests, and the 10-crate wasm check. Actual preview workflows and legacy format-7 origin-corner reopening were inspected.
This commit is contained in:
bherbruck committed 2026-10-11 01:28:01 -04:00
1 parent d71bdb78ec
commit 07932d0f42
65 files changed
+6145 -484

No files matched your search

+216
View File
@@ -0,0 +1,216 @@
//! Exact placement identity and session view/authoring state.
use crate::{EngineError, Result, Sel, Session};
use solvecraft_doc::{IDENTITY, Mat, mat_mul};
#[derive(Clone, Debug)]
pub struct OccurrenceBody {
pub source: String,
pub world: String,
}
#[derive(Clone, Debug)]
pub struct OccurrencePlacement {
pub occurrence: u64,
pub component: u64,
pub path: Vec<u64>,
pub name: String,
pub world: Mat,
pub local: Mat,
pub parent_world: Mat,
pub grounded: bool,
pub bodies: Vec<OccurrenceBody>,
}
fn invalid(message: &str) -> EngineError {
EngineError::Other(message.to_string())
}
pub(crate) struct VisibilityCache {
revision: u64,
paths: Vec<Vec<u64>>,
bodies: std::collections::BTreeSet<String>,
}
impl Session {
/// Document with uncaptured occurrence positions applied, without changing saved geometry.
pub fn placed_document(&self) -> std::borrow::Cow<'_, solvecraft_doc::Document> {
if self.pending_moves.is_empty() {
return std::borrow::Cow::Borrowed(&self.doc);
}
let mut doc = (*self.doc).clone();
for o in &mut doc.occurrences {
if let Some(transform) = self.pending_moves.get(&o.id) {
o.transform = *transform;
}
}
std::borrow::Cow::Owned(doc)
}
/// Resolve only the supplied path, never an exponentially expanded assembly.
pub fn occurrence_placement(&self, path: &[u64]) -> Result<OccurrencePlacement> {
if path.is_empty() || path.len() > 64 {
return Err(invalid("occurrence path must contain 1..64 ids"));
}
let mut component = 0;
let mut world = IDENTITY;
let mut parent_world = IDENTITY;
let mut seen = std::collections::BTreeSet::new();
for id in path {
if !seen.insert(*id) {
return Err(invalid("occurrence path repeats an id"));
}
let o = self
.doc
.occurrences
.iter()
.find(|o| o.id == *id && o.parent == component)
.ok_or_else(|| invalid("occurrence path has a missing or mismatched parent"))?;
if !self.doc.components.iter().any(|c| c.id == o.component) {
return Err(invalid("occurrence path has a missing component"));
}
parent_world = world;
world = mat_mul(&world, self.pending_moves.get(id).unwrap_or(&o.transform));
if world.iter().flatten().any(|x| !x.is_finite()) {
return Err(invalid("occurrence path has a non-finite transform"));
}
component = o.component;
}
let leaf = self.doc.occurrences.iter().find(|o| Some(&o.id) == path.last()).ok_or_else(|| invalid("occurrence path has no leaf"))?;
let state = self.model.state();
let bodies = state
.bodies
.iter()
.filter(|b| self.doc.body_component(&b.name, b.feature) == component)
.map(|b| OccurrenceBody { source: b.name.clone(), world: self.doc.world_body_name(&state, &b.name, component, path) })
.collect();
Ok(OccurrencePlacement {
occurrence: leaf.id,
component,
path: path.to_vec(),
name: leaf.name.clone(),
world,
local: self.pending_moves.get(&leaf.id).copied().unwrap_or(leaf.transform),
parent_world,
grounded: leaf.grounded,
bodies,
})
}
/// Children are definitions shared by every placement of the parent component.
pub fn occurrence_children(&self, path: &[u64]) -> Result<Vec<OccurrencePlacement>> {
let component = if path.is_empty() { 0 } else { self.occurrence_placement(path)?.component };
self.doc
.occurrences
.iter()
.filter(|o| o.parent == component)
.map(|o| {
let mut child = path.to_vec();
child.push(o.id);
self.occurrence_placement(&child)
})
.collect()
}
/// Bounded descendant expansion. Never return an incomplete selection or bounds list.
pub fn occurrence_bodies(&self, path: &[u64], descendants: bool) -> Result<Vec<String>> {
let mut stack = vec![self.occurrence_placement(path)?];
let mut out = Vec::new();
let mut count = 0;
while let Some(placement) = stack.pop() {
count += 1;
if count > 100_000 || out.len() + placement.bodies.len() > 100_000 {
return Err(invalid("occurrence expansion exceeds 100000 placements or bodies"));
}
out.extend(placement.bodies.into_iter().map(|b| b.world));
if descendants {
stack.extend(self.occurrence_children(&placement.path)?);
}
}
Ok(out)
}
/// Selected authoring placement, or the compatible first-placement frame.
pub fn active_component_frame(&self) -> Mat {
if let Some(path) = &self.active_occurrence_path
&& let Ok(p) = self.occurrence_placement(path)
&& p.component == self.active_component
{
return p.world;
}
let path = self.doc.first_path(self.active_component);
self.occurrence_placement(&path).map(|p| p.world).unwrap_or(IDENTITY)
}
pub fn occurrence_visible(&self, path: &[u64]) -> bool {
!self.visibility.hidden_occurrences.iter().any(|hidden| path.starts_with(hidden))
}
pub fn occurrence_hidden(&self, path: &[u64]) -> bool {
self.visibility.hidden_occurrences.iter().any(|hidden| hidden == path)
}
/// Number of placements affected by editing this shared occurrence definition.
pub fn occurrence_scope_count(&self, path: &[u64]) -> Result<usize> {
let id = self.occurrence_placement(path)?.occurrence;
self.occurrence_id_scope_count(id)
}
pub fn occurrence_id_scope_count(&self, id: u64) -> Result<usize> {
if !self.doc.occurrences.iter().any(|o| o.id == id) {
return Err(invalid("no occurrence"));
}
let mut stack = vec![(0, 0usize)];
let mut visited = 0;
let mut count = 0;
while let Some((component, depth)) = stack.pop() {
for o in self.doc.occurrences.iter().filter(|o| o.parent == component) {
visited += 1;
if depth >= 64 || visited > 100_000 {
return Err(invalid("occurrence scope exceeds 64 levels or 100000 placements"));
}
if o.id == id {
count += 1;
}
stack.push((o.component, depth + 1));
}
}
Ok(count)
}
pub fn body_visible(&self, name: &str) -> bool {
if self.visibility.hidden_bodies.iter().any(|n| n == name) {
return false;
}
if self.visibility.hidden_occurrences.is_empty() {
return true;
}
if let Ok(mut cache) = self.occurrence_visibility_cache.lock() {
if cache.as_ref().is_none_or(|c| c.revision != self.revision || c.paths != self.visibility.hidden_occurrences) {
let bodies = self
.doc
.body_frames(&self.model.state())
.into_iter()
.filter(|(_, _, path, _)| !self.occurrence_visible(path))
.map(|(name, _, _, _)| name)
.collect();
*cache = Some(VisibilityCache { revision: self.revision, paths: self.visibility.hidden_occurrences.clone(), bodies });
}
return cache.as_ref().is_none_or(|c| !c.bodies.contains(name));
}
self.doc.body_frames(&self.model.state()).iter().find(|(n, _, _, _)| n == name).is_none_or(|(_, _, path, _)| self.occurrence_visible(path))
}
pub(crate) fn prune_occurrence_state(&mut self) {
self.pending_moves.retain(|id, _| self.doc.occurrences.iter().any(|o| o.id == *id));
if self.active_component != 0 && !self.doc.components.iter().any(|c| c.id == self.active_component) {
self.active_component = 0;
self.active_occurrence_path = None;
}
if self.active_occurrence_path.as_ref().is_some_and(|p| self.occurrence_placement(p).map_or(true, |p| p.component != self.active_component)) {
self.active_occurrence_path = None;
}
let selection = std::mem::take(&mut self.selection);
self.selection = selection.into_iter().filter(|s| !matches!(s, Sel::Occurrence {path} if self.occurrence_placement(path).is_err())).collect();
let hidden = std::mem::take(&mut self.visibility.hidden_occurrences);
self.visibility.hidden_occurrences = hidden.into_iter().filter(|p| self.occurrence_placement(p).is_ok()).collect();
}
}