291 lines
12 KiB
Rust
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();
|
|
}
|
|
}
|