Files
ARCADIA-Studio/crates/oa-core/src/session.rs
T

1100 lines
34 KiB
Rust

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<Id>,
},
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<Id>,
},
Reidentify(Id, Id),
Rename(Id, String),
Describe(Id, String),
SetAlias(Id, String),
Connect(RelationKind, String, Id, Id),
Delete(Id),
Batch(Vec<Command>),
AddStep(Id, Id),
RemoveStep(Id, usize),
MoveStep(Id, usize, usize),
SetSteps(Id, Vec<Id>),
MoveNode {
diagram: Id,
node: Id,
position: Position,
with_descendants: bool,
},
Layout(Id),
}
pub struct Session {
project: Project,
past: Vec<Project>,
future: Vec<Project>,
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<Id>,
/// 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<Id, Id>,
}
/// Track actual identity operations, not aliases or structural similarity.
/// The reverse map composes renames without scanning every original object.
struct IdentityProvenance {
original: BTreeSet<Id>,
current_to_original: BTreeMap<Id, Id>,
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<Id, Id> {
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<PreparedCommand, ModelError> {
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<Self, ModelError> {
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<Option<Id>, 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<Project, ModelError> {
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<Id>,
provenance: &mut IdentityProvenance,
) -> Result<Option<Id>, 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 {
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<Option<Id>, Vec<Id>> = BTreeMap::new();
for (id, parent) in &parents {
children.entry(*parent).or_default().push(*id);
}
fn measure(
p: &Project,
id: Id,
d: &Diagram,
children: &BTreeMap<Option<Id>, Vec<Id>>,
sizes: &mut BTreeMap<Id, Size>,
seen: &mut BTreeSet<Id>,
) -> Result<Size, ModelError> {
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<Option<Id>, Vec<Id>>,
sizes: &BTreeMap<Id, Size>,
positions: &mut BTreeMap<Id, Position>,
headers: &BTreeMap<Id, f32>,
) {
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(())
}