Merge ordinary and State multiselect into the desktop candidate

This commit is contained in:
2026-09-23 11:17:21 -07:00
139 changed files with 21151 additions and 72 deletions
+91
View File
@@ -0,0 +1,91 @@
//! 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))
}
+1
View File
@@ -1,6 +1,7 @@
//! Framework-independent OA/SA model, transactional commands and editing services.
pub mod analysis_numeric;
pub mod data;
pub mod diagram_edit;
pub mod language;
pub mod model;
pub mod operations;