92 lines
3.3 KiB
Rust
92 lines
3.3 KiB
Rust
//! 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<Id>,
|
|
delta: Position,
|
|
) -> Result<Command, ModelError> {
|
|
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<Id, Vec<Id>> = 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))
|
|
}
|