Files
OneCAD/crates/engine/src/occurrences.rs
T

291 lines
12 KiB
Rust

//! 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<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>,
}
/// [`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<String>,
}
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<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.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<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)
}
/// 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<String> {
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();
}
}