//! Exact placement identity and session view/authoring state. use crate::{EngineError, Result, Sel, Session}; use solidcraft_doc::{FeatureKind, IDENTITY, Mat, PlaneRef, 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, pub name: String, pub world: Mat, pub local: Mat, pub parent_world: Mat, pub grounded: bool, pub bodies: Vec, } fn invalid(message: &str) -> EngineError { EngineError::Other(message.to_string()) } pub(crate) struct VisibilityCache { revision: u64, paths: Vec>, bodies: std::collections::BTreeSet, } /// [`Session::body_removed_later`] for one revision, sketch and history marker. pub(crate) struct RemovedLaterCache { revision: u64, sketch: u64, marker: usize, bodies: std::collections::BTreeSet, } impl Session { /// Document with uncaptured occurrence positions applied, without changing saved geometry. pub fn placed_document(&self) -> std::borrow::Cow<'_, solidcraft_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 { 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.model_body_component(b) == 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> { 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> { 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 { let id = self.occurrence_placement(path)?.occurrence; self.occurrence_id_scope_count(id) } pub fn occurrence_id_scope_count(&self, id: u64) -> Result { 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) } /// Whether a joint's glyph is drawn (`browser.visibility`). Hiding a joint only hides its /// drawing: it still constrains and drives the parts. pub fn joint_visible(&self, id: u64) -> bool { !self.visibility.hidden_joints.contains(&id) } pub fn body_visible(&self, name: &str) -> bool { if self.visibility.hidden_bodies.iter().any(|n| n == name) || self.body_removed_later(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)) } /// Is this body deleted by a feature the history marker has rolled off, while a sketch is /// being edited? Edit Sketch rolls the timeline back to the sketch, so a body made before the /// sketch and removed after it is part of the shown model again; it stays out of the view, /// as it does once the timeline runs to the end. Nothing else changes: the model, its state /// and every command still see the history as it was rolled to. A body the sketch is /// built on (the face it sits on, an edge it projects) stays. pub fn body_removed_later(&self, name: &str) -> bool { let (Some(sketch), Some(marker)) = (self.active_sketch, self.doc.marker) else { return false }; if marker >= self.doc.features.len() { return false; } let Ok(mut cache) = self.removed_later_cache.lock() else { return false }; if cache.as_ref().is_none_or(|c| c.revision != self.revision || c.sketch != sketch || c.marker != marker) { *cache = Some(RemovedLaterCache { revision: self.revision, sketch, marker, bodies: self.removed_later_bodies(sketch, marker) }); } cache.as_ref().is_some_and(|c| c.bodies.contains(name)) } /// The names (as shown in the world) of the bodies that features from `marker` on remove, /// leaving out the bodies `sketch` is built on. fn removed_later_bodies(&self, sketch: u64, marker: usize) -> std::collections::BTreeSet { let none = std::collections::BTreeSet::new(); let Some(FeatureKind::Sketch { plane, sketch: made }) = self.doc.feature(sketch).map(|f| &f.kind) else { return none }; let mut kept: std::collections::BTreeSet<&str> = std::collections::BTreeSet::new(); let mut plane = plane; loop { match plane { PlaneRef::Offset { base, .. } | PlaneRef::AtAngle { base, .. } | PlaneRef::Flipped { base, .. } => plane = base, // A face attachment that names no exact source: which body it sits on is unknown, // so none is taken from the view. PlaneRef::Face { anchor: None, .. } => return none, PlaneRef::Face { anchor: Some(anchor), .. } => { kept.insert(anchor.target.body.as_str()); break; } _ => break, } } for link in &made.links { kept.extend(link.source.bodies()); } let shown = self.model.state(); let mut removed = std::collections::BTreeSet::new(); for feature in self.doc.features.iter().skip(marker).filter(|f| !f.suppressed) { if let FeatureKind::Remove { bodies } = &feature.kind { removed.extend(bodies.iter().filter(|b| !kept.contains(b.0.as_str()) && shown.body(&b.0).is_some()).map(|b| b.0.clone())); } } if removed.is_empty() || self.doc.occurrences.is_empty() { return removed; } // A component's bodies are shown once per placement, each under its own name. self.doc .body_frames_with_source(&shown) .into_iter() .filter(|(_, source, _, _, _)| removed.contains(source)) .map(|(world, ..)| world) .collect() } 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(); } }