Establish OA/SA prototype baseline and first-release work
This commit is contained in:
@@ -0,0 +1,792 @@
|
||||
use crate::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
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,
|
||||
},
|
||||
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,
|
||||
}
|
||||
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 mut candidate = self.project.clone();
|
||||
let selected = apply_command(&mut candidate, &command, 0)?;
|
||||
validate_candidate(&candidate)?;
|
||||
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();
|
||||
for command in commands {
|
||||
apply_command(&mut candidate, command, 0)?;
|
||||
}
|
||||
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) -> 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();
|
||||
None
|
||||
}
|
||||
Command::Batch(commands) => {
|
||||
let mut selected = None;
|
||||
for command in commands {
|
||||
let id = apply_command(p, command, depth + 1)?;
|
||||
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(p, *old, value.id)?;
|
||||
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(p, *old, 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);
|
||||
reidentify(p, *old, value.id)?;
|
||||
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(p, *old, value.id)?;
|
||||
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(p, *old, value.id)?;
|
||||
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(p, *original, node.id)?;
|
||||
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(p, p.id, *id)?;
|
||||
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(p, *old, *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) => {
|
||||
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(p, *id);
|
||||
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(p, id);
|
||||
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::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_mut(diagram).ok_or_else(missing)?;
|
||||
let n = d.nodes.get_mut(node).ok_or_else(missing)?;
|
||||
let delta = Position {
|
||||
x: position.x - n.position.x,
|
||||
y: position.y - n.position.y,
|
||||
};
|
||||
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;
|
||||
}
|
||||
}
|
||||
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 own = crate::projection::minimum_node_size(p, d.nodes[&id].element, d.nodes[&id].size);
|
||||
let mut width = own.width;
|
||||
let mut height = 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 += 8.;
|
||||
} 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>,
|
||||
) {
|
||||
positions.insert(id, position);
|
||||
let mut y = position.y + 64.;
|
||||
if let Some(kids) = children.get(&Some(id)) {
|
||||
for child in kids {
|
||||
place(
|
||||
*child,
|
||||
Position {
|
||||
x: position.x + 24.,
|
||||
y,
|
||||
},
|
||||
children,
|
||||
sizes,
|
||||
positions,
|
||||
);
|
||||
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 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);
|
||||
x += sizes[id].width + 60.;
|
||||
row_height = row_height.max(sizes[id].height);
|
||||
}
|
||||
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];
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user