use crate::*; use serde::{Deserialize, Serialize}; use std::collections::{BTreeMap, BTreeSet}; use thiserror::Error; #[derive(Debug, Clone, Error, Serialize, Deserialize)] #[error("{0}")] pub struct ModelError(pub String); #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub enum Command { Replace(Project), Add(Kind), AddElement { kind: Kind, layer: Layer, name: String, parent: Option, }, CreateElement(Element), EditElement(Id, Element), CreateRelation(Relation), EditRelation(Id, Relation), CreateDiagram(Diagram), EditDiagram(Id, Diagram), CreateArtifact(ArtifactLink), EditArtifact(Id, ArtifactLink), CreateTrace(Trace), EditTrace(Id, Trace), CreateNode { diagram: Id, node: NodePlacement, }, EditNode { diagram: Id, original: Id, node: NodePlacement, }, SetEdgeRoute { diagram: Id, route: EdgeRoute, }, RemoveEdgeRoute { diagram: Id, edge: Id, }, SetPortAnchor { diagram: Id, node: Id, anchor: PortAnchor, }, RemovePortAnchor { diagram: Id, node: Id, port: Id, }, RelocateNode { node: Id, diagram: Id, }, EditProject { id: Id, alias: String, name: String, description: String, }, Reparent { element: Id, parent: Option, }, Reidentify(Id, Id), Rename(Id, String), Describe(Id, String), SetAlias(Id, String), Connect(RelationKind, String, Id, Id), Delete(Id), Batch(Vec), AddStep(Id, Id), RemoveStep(Id, usize), MoveStep(Id, usize, usize), SetSteps(Id, Vec), MoveNode { diagram: Id, node: Id, position: Position, with_descendants: bool, }, Layout(Id), } pub struct Session { project: Project, past: Vec, future: Vec, saved: Project, revision: u64, } /// One validated command result, ready for an owning editor or service to adopt. /// Preparation never mutates the input and allocates creation identities once. /// A source editor can validate its corresponding document edits before either /// representation becomes visible. pub struct PreparedCommand { pub project: Project, pub selected: Option, /// Complete correspondence from surviving original identities to candidate /// identities, including unchanged objects. Newly created objects have no /// origin. Replacement establishes correspondence by common original UUIDs. pub identity_map: BTreeMap, } /// Track actual identity operations, not aliases or structural similarity. /// The reverse map composes renames without scanning every original object. struct IdentityProvenance { original: BTreeSet, current_to_original: BTreeMap, explicit_replacement: bool, } impl IdentityProvenance { fn new(project: &Project) -> Self { let original: BTreeSet<_> = project.identities().into_iter().map(|(id, _)| id).collect(); let current_to_original = original.iter().map(|id| (*id, *id)).collect(); Self { original, current_to_original, explicit_replacement: false, } } fn replace(&mut self, project: &Project) { self.explicit_replacement = true; let present: BTreeSet<_> = project.identities().into_iter().map(|(id, _)| id).collect(); self.current_to_original = self .original .intersection(&present) .map(|id| (*id, *id)) .collect(); } fn retain_present(&mut self, project: &Project) { if !self.current_to_original.is_empty() { let present: BTreeSet<_> = project.identities().into_iter().map(|(id, _)| id).collect(); self.current_to_original .retain(|id, _| present.contains(id)); } } fn into_map(self) -> BTreeMap { self.current_to_original .into_iter() .map(|(current, original)| (original, current)) .collect() } } fn reidentify_tracked( project: &mut Project, old: Id, new: Id, provenance: &mut IdentityProvenance, ) -> Result<(), ModelError> { reidentify(project, old, new)?; if let Some(original) = provenance.current_to_original.remove(&old) { provenance.current_to_original.insert(new, original); } Ok(()) } pub fn prepare_command( project: &Project, command: &Command, ) -> Result { let mut candidate = project.clone(); let mut pending_deletions = BTreeSet::new(); let mut provenance = IdentityProvenance::new(project); let selected = apply_command( &mut candidate, command, 0, &mut pending_deletions, &mut provenance, )?; crate::references::finish_deletions(&mut candidate, &pending_deletions); provenance.retain_present(&candidate); if !provenance.explicit_replacement { crate::data::prune_removed_inheritance( project, &mut candidate, &provenance.current_to_original, ); } validate_candidate(&candidate)?; Ok(PreparedCommand { project: candidate, selected, identity_map: provenance.into_map(), }) } impl Session { pub fn try_new(project: Project) -> Result { validate_candidate(&project)?; Ok(Self { saved: project.clone(), project, past: vec![], future: vec![], revision: 0, }) } pub fn new(project: Project) -> Self { Self::try_new(project).expect("Session requires a valid project") } pub fn project(&self) -> &Project { &self.project } pub fn revision(&self) -> u64 { self.revision } pub fn dirty(&self) -> bool { self.project != self.saved } pub fn mark_saved(&mut self, snapshot: Project) { self.saved = snapshot; } pub fn can_undo(&self) -> bool { !self.past.is_empty() } pub fn can_redo(&self) -> bool { !self.future.is_empty() } pub fn undo(&mut self) { if let Some(p) = self.past.pop() { self.future.push(std::mem::replace(&mut self.project, p)); self.revision = self.revision.saturating_add(1); } } pub fn redo(&mut self) { if let Some(p) = self.future.pop() { self.past.push(std::mem::replace(&mut self.project, p)); self.revision = self.revision.saturating_add(1); } } pub fn execute_many(&mut self, commands: &[Command]) -> Result<(), ModelError> { let candidate = execute_batch(&self.project, commands)?; if candidate != self.project { self.past .push(std::mem::replace(&mut self.project, candidate)); if self.past.len() > 100 { self.past.remove(0); } self.future.clear(); self.revision = self.revision.saturating_add(1); } Ok(()) } pub fn execute(&mut self, command: Command) -> Result, ModelError> { let PreparedCommand { project: candidate, selected, .. } = prepare_command(&self.project, &command)?; if candidate != self.project { self.past .push(std::mem::replace(&mut self.project, candidate)); if self.past.len() > 100 { self.past.remove(0); } self.future.clear(); self.revision = self.revision.saturating_add(1); } Ok(selected) } } pub fn validate_candidate(p: &Project) -> Result<(), ModelError> { if let Some(i) = validation::validate(p) .into_iter() .find(|i| i.severity == validation::Severity::Error) { Err(ModelError(format!("{}: {}", i.code, i.message))) } else { Ok(()) } } pub fn execute_batch(project: &Project, commands: &[Command]) -> Result { let mut candidate = project.clone(); let mut pending_deletions = BTreeSet::new(); let mut provenance = IdentityProvenance::new(project); for command in commands { apply_command( &mut candidate, command, 0, &mut pending_deletions, &mut provenance, )?; } crate::references::finish_deletions(&mut candidate, &pending_deletions); if !provenance.explicit_replacement { crate::data::prune_removed_inheritance( project, &mut candidate, &provenance.current_to_original, ); } validate_candidate(&candidate)?; Ok(candidate) } fn vacant(p: &Project, id: Id) -> Result<(), ModelError> { if p.contains(id) { Err(ModelError( "This ID already belongs to another object.".into(), )) } else { Ok(()) } } fn missing() -> ModelError { ModelError("Object no longer exists.".into()) } fn detach(p: &mut Project, id: Id) { for e in p.elements.values_mut() { if let Some(steps) = e.steps_mut() { steps.retain(|step| *step != id); } if let Some(SharedBody::Occurrence { body: OccurrenceBody::Fragment { branches, .. }, .. }) = e.shared_mut() { branches.retain(|branch| *branch != id); } } } fn attach(p: &mut Project, id: Id) { let Some(e) = p.elements.get(&id) else { return; }; if e.kind() != Kind::Occurrence { return; } let parent = e.parent; let branch = matches!( e.shared(), Some(SharedBody::Occurrence { body: OccurrenceBody::Branch { .. }, .. }) ); if let Some(owner) = parent.and_then(|id| p.elements.get_mut(&id)) { if let Some(steps) = owner.steps_mut() { if !steps.contains(&id) { steps.push(id); } } else if branch && let Some(SharedBody::Occurrence { body: OccurrenceBody::Fragment { branches, .. }, .. }) = owner.shared_mut() && !branches.contains(&id) { branches.push(id); } } } fn apply_command( p: &mut Project, c: &Command, depth: usize, pending_deletions: &mut BTreeSet, provenance: &mut IdentityProvenance, ) -> Result, ModelError> { if depth > 64 { return Err(ModelError( "Command batch nesting exceeds 64 levels.".into(), )); } let selected = match c { Command::Replace(value) => { *p = value.clone(); pending_deletions.clear(); provenance.replace(p); None } Command::Batch(commands) => { let mut selected = None; for command in commands { let id = apply_command(p, command, depth + 1, pending_deletions, provenance)?; if id.is_some() { selected = id; } } selected } Command::Add(kind) => { let layer = if kind.supports(Layer::Oa) { Layer::Oa } else { Layer::Sa }; let name = format!("New {kind}"); let e = p .make_element(*kind, layer, &name, None) .ok_or_else(|| ModelError("Invalid layer for kind.".into()))?; let id = e.id; p.elements.insert(id, e); let d = p.ensure_diagram(layer); let slot = p.diagrams[&d].nodes.len(); p.place( d, id, Position { x: 70. + (slot % 4) as f32 * 270., y: 70. + (slot / 4) as f32 * 150., }, ); Some(id) } Command::AddElement { kind, layer, name, parent, } => { let e = p .make_element(*kind, *layer, name, *parent) .ok_or_else(|| ModelError("Invalid layer for kind.".into()))?; let id = e.id; p.elements.insert(id, e); attach(p, id); Some(id) } Command::CreateElement(value) => { vacant(p, value.id)?; p.elements.insert(value.id, value.clone()); attach(p, value.id); Some(value.id) } Command::EditElement(old, value) => { let previous = p.elements.get(old).ok_or_else(missing)?; let ownership_change = previous.parent != value.parent || previous.kind() != value.kind(); if ownership_change { detach(p, *old); } let mut e = value.clone(); e.id = *old; p.elements.insert(*old, e); reidentify_tracked(p, *old, value.id, provenance)?; if ownership_change { attach(p, value.id); } Some(value.id) } Command::CreateRelation(value) => { vacant(p, value.id)?; p.relations.insert(value.id, value.clone()); Some(value.id) } Command::EditRelation(old, value) => { if !p.relations.contains_key(old) { return Err(missing()); } let mut v = value.clone(); v.id = *old; p.relations.insert(*old, v); reidentify_tracked(p, *old, value.id, provenance)?; if pending_deletions.remove(old) { pending_deletions.insert(value.id); } Some(value.id) } Command::CreateDiagram(value) => { vacant(p, value.id)?; p.diagrams.insert(value.id, value.clone()); Some(value.id) } Command::EditDiagram(old, value) => { if !p.diagrams.contains_key(old) { return Err(missing()); } let mut v = value.clone(); v.id = *old; p.diagrams.insert(*old, v); // Replacing a diagram can remove embedded placement identities. provenance.retain_present(p); reidentify_tracked(p, *old, value.id, provenance)?; Some(value.id) } Command::CreateArtifact(value) => { vacant(p, value.id)?; p.artifact_links.insert(value.id, value.clone()); Some(value.id) } Command::EditArtifact(old, value) => { if !p.artifact_links.contains_key(old) { return Err(missing()); } let mut v = value.clone(); v.id = *old; p.artifact_links.insert(*old, v); reidentify_tracked(p, *old, value.id, provenance)?; Some(value.id) } Command::CreateTrace(value) => { vacant(p, value.id)?; p.traces.insert(value.id, value.clone()); Some(value.id) } Command::EditTrace(old, value) => { if !p.traces.contains_key(old) { return Err(missing()); } let mut v = value.clone(); v.id = *old; p.traces.insert(*old, v); reidentify_tracked(p, *old, value.id, provenance)?; Some(value.id) } Command::CreateNode { diagram, node } => { vacant(p, node.id)?; p.diagrams .get_mut(diagram) .ok_or_else(missing)? .nodes .insert(node.id, node.clone()); Some(node.id) } Command::EditNode { diagram, original, node, } => { let d = p.diagrams.get_mut(diagram).ok_or_else(missing)?; if !d.nodes.contains_key(original) { return Err(missing()); } let mut n = node.clone(); n.id = *original; d.nodes.insert(*original, n); reidentify_tracked(p, *original, node.id, provenance)?; Some(node.id) } Command::RelocateNode { node, diagram } => { if !p.diagrams.contains_key(diagram) { return Err(missing()); } let source = p .diagrams .values() .find(|d| d.nodes.contains_key(node)) .map(|d| d.id) .ok_or_else(missing)?; if source != *diagram { let placement = p .diagrams .get_mut(&source) .unwrap() .nodes .remove(node) .unwrap(); p.diagrams .get_mut(diagram) .unwrap() .nodes .insert(*node, placement); } Some(*node) } Command::EditProject { id, alias, name, description, } => { reidentify_tracked(p, p.id, *id, provenance)?; p.alias = alias.clone(); p.name = name.clone(); p.description = description.clone(); None } Command::Reparent { element, parent } => { let current_parent = p.elements.get(element).ok_or_else(missing)?.parent; if current_parent != *parent { detach(p, *element); p.elements.get_mut(element).unwrap().parent = *parent; attach(p, *element); } Some(*element) } Command::Reidentify(old, new) => { reidentify_tracked(p, *old, *new, provenance)?; if pending_deletions.remove(old) { pending_deletions.insert(*new); } Some(*new) } Command::Rename(id, value) => { edit_header(p, *id, Some(value), None, None)?; Some(*id) } Command::Describe(id, value) => { edit_header(p, *id, None, Some(value), None)?; Some(*id) } Command::SetAlias(id, value) => { edit_header(p, *id, None, None, Some(value))?; Some(*id) } Command::Connect(kind, name, source, target) => { if *kind == RelationKind::FunctionalExchange { let id = Id::new_v4(); let commands = crate::ports::functional_exchange_commands( p, &FunctionalExchangeRequest { id, alias: p.fresh_alias(name, "relation", id), name: name.clone(), description: String::new(), source: *source, target: *target, source_port: PortChoice::Auto, target_port: PortChoice::Auto, items: vec![], }, )?; return apply_command( p, &Command::Batch(commands), depth + 1, pending_deletions, provenance, ); } Some(p.relate(*kind, name, *source, *target)) } Command::Delete(id) => { if *id == p.id { return Err(ModelError("Use New/Replace to delete a project.".into())); } if !p.contains(*id) { return Err(missing()); } crate::references::delete_cascade_deferred(p, *id, pending_deletions); provenance.retain_present(p); None } Command::AddStep(owner, exchange) => { let e = p.elements.get(owner).ok_or_else(missing)?; if e.steps().is_none() { return Err(ModelError( "Selected element does not own an ordered sequence.".into(), )); } let layer = e.layer(); let name = format!("{} occurrence", p.named(*exchange)); let mut occ = p .make_element(Kind::Occurrence, layer, &name, Some(*owner)) .unwrap(); *occ.shared_mut().unwrap() = SharedBody::Occurrence { body: OccurrenceBody::Exchange { operation: None, exchange: Some(*exchange), sender: None, receiver: None, message_kind: MessageKind::Asynchronous, reply_to: None, values: vec![], }, precondition: String::new(), postcondition: String::new(), }; let id = occ.id; p.elements.insert(id, occ); attach(p, id); Some(*owner) } Command::RemoveStep(owner, index) => { let id = *p .elements .get(owner) .and_then(Element::steps) .and_then(|s| s.get(*index)) .ok_or_else(|| ModelError("Step index is out of range.".into()))?; crate::references::delete_cascade_deferred(p, id, pending_deletions); provenance.retain_present(p); Some(*owner) } Command::MoveStep(owner, from, to) => { let steps = p .elements .get_mut(owner) .and_then(Element::steps_mut) .ok_or_else(missing)?; if *from >= steps.len() || *to >= steps.len() { return Err(ModelError("Step index is out of range.".into())); } let step = steps.remove(*from); steps.insert(*to, step); Some(*owner) } Command::SetSteps(owner, value) => { *p.elements .get_mut(owner) .and_then(Element::steps_mut) .ok_or_else(missing)? = value.clone(); Some(*owner) } Command::SetPortAnchor { diagram, node, anchor, } => { let n = p .diagrams .get_mut(diagram) .and_then(|d| d.nodes.get_mut(node)) .ok_or_else(missing)?; if let Some(existing) = n.port_anchors.iter_mut().find(|a| a.port == anchor.port) { *existing = anchor.clone(); } else { n.port_anchors.push(anchor.clone()); } Some(anchor.port) } Command::RemovePortAnchor { diagram, node, port, } => { let n = p .diagrams .get_mut(diagram) .and_then(|d| d.nodes.get_mut(node)) .ok_or_else(missing)?; if !p .elements .get(port) .is_some_and(|e| e.kind() == Kind::Port && e.parent == Some(n.element)) { return Err(ModelError( "Port positioning requires a direct child port of the owner placement.".into(), )); } n.port_anchors.retain(|anchor| anchor.port != *port); Some(*port) } Command::SetEdgeRoute { diagram, route } => { let d = p.diagrams.get_mut(diagram).ok_or_else(missing)?; if let Some(existing) = d.routes.iter_mut().find(|r| r.edge == route.edge) { *existing = route.clone(); } else { d.routes.push(route.clone()); } Some(route.edge) } Command::RemoveEdgeRoute { diagram, edge } => { if !p.relations.contains_key(edge) && !p.traces.contains_key(edge) { return Err(ModelError( "Routing requires an existing relation or trace.".into(), )); } p.diagrams .get_mut(diagram) .ok_or_else(missing)? .routes .retain(|route| route.edge != *edge); Some(*edge) } Command::MoveNode { diagram, node, position, with_descendants, } => { let descendants = if *with_descendants { crate::projection::descendant_nodes(p, *diagram, *node) } else { vec![] }; let d = p.diagrams.get(diagram).ok_or_else(missing)?; let n = d.nodes.get(node).ok_or_else(missing)?; let delta = Position { x: position.x - n.position.x, y: position.y - n.position.y, }; let deltas = std::iter::once(*node) .chain(descendants.iter().copied()) .map(|id| (id, delta)) .collect(); let routes = crate::projection::translated_routes(p, d, &deltas); let d = p.diagrams.get_mut(diagram).unwrap(); let n = d.nodes.get_mut(node).unwrap(); n.position = *position; for id in descendants { if let Some(child) = d.nodes.get_mut(&id) { child.position.x += delta.x; child.position.y += delta.y; } } for route in routes { if let Some(existing) = d.routes.iter_mut().find(|r| r.edge == route.edge) { *existing = route; } } Some(*node) } Command::Layout(diagram) => { layout(p, *diagram)?; Some(*diagram) } }; Ok(selected) } fn edit_header( p: &mut Project, id: Id, name: Option<&String>, description: Option<&String>, alias: Option<&String>, ) -> Result<(), ModelError> { let assign = |n: &mut String, d: &mut String, a: &mut String| { if let Some(v) = name { *n = v.clone(); } if let Some(v) = description { *d = v.clone(); } if let Some(v) = alias { *a = v.clone(); } }; if p.id == id { assign(&mut p.name, &mut p.description, &mut p.alias); return Ok(()); } if let Some(v) = p.elements.get_mut(&id) { assign(&mut v.name, &mut v.description, &mut v.alias); return Ok(()); } if let Some(v) = p.relations.get_mut(&id) { assign(&mut v.name, &mut v.description, &mut v.alias); return Ok(()); } if let Some(v) = p.diagrams.get_mut(&id) { assign(&mut v.name, &mut v.description, &mut v.alias); return Ok(()); } if let Some(v) = p.artifact_links.get_mut(&id) { assign(&mut v.name, &mut v.description, &mut v.alias); return Ok(()); } if let Some(v) = p.traces.get_mut(&id) { assign(&mut v.name, &mut v.description, &mut v.alias); return Ok(()); } for d in p.diagrams.values_mut() { if let Some(n) = d.nodes.get_mut(&id) { if name.is_some() || description.is_some() { return Err(ModelError( "Diagram nodes have no independent name/description.".into(), )); } if let Some(a) = alias { n.alias = a.clone(); } return Ok(()); } } Err(missing()) } pub fn reidentify(p: &mut Project, old: Id, new: Id) -> Result<(), ModelError> { if !p.contains(old) { return Err(missing()); } if old == new { return Ok(()); } vacant(p, new)?; if p.id == old { p.id = new; } else if let Some(mut v) = p.elements.remove(&old) { v.id = new; p.elements.insert(new, v); } else if let Some(mut v) = p.relations.remove(&old) { v.id = new; p.relations.insert(new, v); } else if let Some(mut v) = p.diagrams.remove(&old) { v.id = new; p.diagrams.insert(new, v); } else if let Some(mut v) = p.artifact_links.remove(&old) { v.id = new; p.artifact_links.insert(new, v); } else if let Some(mut v) = p.traces.remove(&old) { v.id = new; p.traces.insert(new, v); } else { for d in p.diagrams.values_mut() { if let Some(mut n) = d.nodes.remove(&old) { n.id = new; d.nodes.insert(new, n); break; } } } crate::references::remap_references(p, old, new); Ok(()) } fn layout(p: &mut Project, diagram: Id) -> Result<(), ModelError> { use std::collections::{BTreeMap, BTreeSet}; let d = p.diagrams.get(&diagram).ok_or_else(missing)?; let parents = crate::projection::parents(p, d); let mut children: BTreeMap, Vec> = BTreeMap::new(); for (id, parent) in &parents { children.entry(*parent).or_default().push(*id); } fn measure( p: &Project, id: Id, d: &Diagram, children: &BTreeMap, Vec>, sizes: &mut BTreeMap, seen: &mut BTreeSet, ) -> Result { if !seen.insert(id) { return Err(ModelError("Diagram containment cycle.".into())); } let node = &d.nodes[&id]; let own = crate::projection::minimum_node_size(p, node.element, node.size); let own = if d.projection.state_machine().is_some() { crate::projection::state_node_size(p, &p.elements[&node.element], own, false) } else { own }; let mut width = own.width; let mut height = if d.projection.state_machine().is_some() { crate::projection::state_header_height(p, node.element) } else { 64. }; let kids = children.get(&Some(id)); if let Some(kids) = kids { for child in kids { let size = measure(p, *child, d, children, sizes, seen)?; width = width.max(size.width + 48.); height += size.height + 16.; } height += crate::projection::port_footer(p, d.nodes[&id].element) - 16.; } else { height = own.height; } let size = Size { width, height: height.max(own.height), }; sizes.insert(id, size); Ok(size) } fn place( id: Id, position: Position, children: &BTreeMap, Vec>, sizes: &BTreeMap, positions: &mut BTreeMap, headers: &BTreeMap, ) { positions.insert(id, position); let mut y = position.y + headers[&id]; if let Some(kids) = children.get(&Some(id)) { for child in kids { place( *child, Position { x: position.x + 24., y, }, children, sizes, positions, headers, ); y += sizes[child].height + 16.; } } } // Orient containers by their actual exchanges. UUID ordering made otherwise // identical models swap source/sink sides and sent boundary ports backwards. let mut remaining: BTreeSet<_> = children .get(&None) .cloned() .unwrap_or_default() .into_iter() .collect(); let root_of = |mut node| { let mut seen = BTreeSet::new(); while let Some(parent) = parents.get(&node).copied().flatten() { if !seen.insert(node) { break; } node = parent; } node }; let edges: BTreeSet<_> = crate::projection::project_view(p, diagram) .edges .iter() .filter_map(|edge| { let source = root_of(edge.source); let target = root_of(edge.target); (source != target).then_some((source, target)) }) .collect(); let mut roots = Vec::new(); while !remaining.is_empty() { let next = remaining .iter() .filter(|id| { !edges .iter() .any(|(source, target)| target == *id && remaining.contains(source)) }) .min_by_key(|id| (&p.elements[&d.nodes[id].element].alias, **id)) .or_else(|| { remaining .iter() .min_by_key(|id| (&p.elements[&d.nodes[id].element].alias, **id)) }) .copied() .unwrap(); remaining.remove(&next); roots.push(next); } let mut sizes = BTreeMap::new(); let mut seen = BTreeSet::new(); for id in &roots { measure(p, *id, d, &children, &mut sizes, &mut seen)?; } let headers = d .nodes .values() .map(|n| { ( n.id, if d.projection.state_machine().is_some() { crate::projection::state_header_height(p, n.element) } else { 64. }, ) }) .collect(); let mut positions = BTreeMap::new(); let mut x = 60.; let mut y = 60.; let mut row_height = 0f32; for (index, id) in roots.iter().enumerate() { if index > 0 && index % 4 == 0 { x = 60.; y += row_height + 60.; row_height = 0.; } place( *id, Position { x, y }, &children, &sizes, &mut positions, &headers, ); x += sizes[id].width + 240.; row_height = row_height.max(sizes[id].height); } let deltas = positions .iter() .map(|(id, position)| { let original = d.nodes[id].position; ( *id, Position { x: position.x - original.x, y: position.y - original.y, }, ) }) .collect(); let routes = crate::projection::translated_routes(p, d, &deltas); let d = p.diagrams.get_mut(&diagram).unwrap(); for (id, position) in positions { let n = d.nodes.get_mut(&id).unwrap(); n.position = position; n.size = sizes[&id]; } for route in routes { if let Some(existing) = d.routes.iter_mut().find(|r| r.edge == route.edge) { *existing = route; } } Ok(()) }