//! Pure diagram edits shared by movement previews and committed transactions. use crate::{Command, Id, MAX_COORDINATE, ModelError, Position, Project, projection}; use std::collections::{BTreeMap, BTreeSet}; /// Translate selected placements and their projected descendants simultaneously. /// /// Membership and geometry are read from one snapshot, including folded children. /// Overlapping roots move each placement once. Route pins follow the combined /// movement of their actual endpoint owners (and State boundaries). Semantic /// ownership and other views are untouched. Apply the returned batch through the /// normal validated command path; its selected identity is not a primary selection. pub fn translate_placements( project: &Project, diagram: Id, selected: &BTreeSet, delta: Position, ) -> Result { let view = project .diagrams .get(&diagram) .ok_or_else(|| ModelError("Movement requires an existing diagram.".into()))?; if selected.is_empty() { return Err(ModelError( "Movement requires at least one placement.".into(), )); } if selected.iter().any(|id| !view.nodes.contains_key(id)) { return Err(ModelError( "Every selected placement must belong to the diagram.".into(), )); } if !delta.x.is_finite() || !delta.y.is_finite() { return Err(ModelError("Movement displacement must be finite.".into())); } if delta.x == 0. && delta.y == 0. { return Ok(Command::Batch(vec![])); } let mut children: BTreeMap> = BTreeMap::new(); for (child, parent) in projection::parents(project, view) { if let Some(parent) = parent { children.entry(parent).or_default().push(child); } } let mut moved = BTreeSet::new(); let mut pending: Vec<_> = selected.iter().copied().collect(); while let Some(id) = pending.pop() { if moved.insert(id) { pending.extend(children.get(&id).into_iter().flatten().copied()); } } let deltas: BTreeMap<_, _> = moved.iter().map(|id| (*id, delta)).collect(); let routes = projection::translated_routes(project, view, &deltas); let bounded = |value: f32| value.is_finite() && value.abs() <= MAX_COORDINATE; let mut commands = Vec::with_capacity(moved.len() + routes.len()); for id in moved { let mut node = view.nodes[&id].clone(); node.position.x += delta.x; node.position.y += delta.y; if ![ node.position.x, node.position.y, node.size.width, node.size.height, ] .into_iter() .all(bounded) || node.size.width <= 0. || node.size.height <= 0. { return Err(ModelError( "Movement produces invalid placement bounds.".into(), )); } commands.push(Command::EditNode { diagram, original: id, node, }); } for route in routes { if route .waypoints .iter() .any(|point| !bounded(point.x) || !bounded(point.y)) { return Err(ModelError("Movement produces invalid route bounds.".into())); } commands.push(Command::SetEdgeRoute { diagram, route }); } Ok(Command::Batch(commands)) }