365 lines
13 KiB
Rust
365 lines
13 KiB
Rust
use crate::*;
|
|
use serde::{Deserialize, Serialize};
|
|
use std::collections::{BTreeMap, BTreeSet};
|
|
|
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
pub struct ProjectedNode {
|
|
pub id: Id,
|
|
pub element: Id,
|
|
pub position: Position,
|
|
pub size: Size,
|
|
pub visual_parent: Option<Id>,
|
|
pub depth: usize,
|
|
pub collapsed: bool,
|
|
/// The work is grouped by its allocated leaves, not by a direct allocation.
|
|
pub allocation_summary: bool,
|
|
}
|
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
pub struct ProjectedEdge {
|
|
pub id: Id,
|
|
pub source: Id,
|
|
pub target: Id,
|
|
pub trace: bool,
|
|
pub source_port: Option<Id>,
|
|
pub target_port: Option<Id>,
|
|
}
|
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
pub struct ProjectedPort {
|
|
pub element: Id,
|
|
/// Placement identity of the owning visible function/participant.
|
|
pub owner: Id,
|
|
pub position: Position,
|
|
pub direction: PortDirection,
|
|
}
|
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
|
|
pub struct ProjectedView {
|
|
pub nodes: Vec<ProjectedNode>,
|
|
pub edges: Vec<ProjectedEdge>,
|
|
pub ports: Vec<ProjectedPort>,
|
|
}
|
|
pub(crate) fn minimum_node_size(p: &Project, element: Id, mut size: Size) -> Size {
|
|
let count = [PortDirection::In, PortDirection::Out].into_iter().map(|direction| p.children(element).filter(|port| matches!(port.shared(), Some(SharedBody::Port {direction:d,..}) if *d==direction)).count()).max().unwrap_or(0);
|
|
if count > 0 {
|
|
size.height = size.height.max(76. + 28. * (count + 1) as f32);
|
|
}
|
|
size
|
|
}
|
|
pub(crate) fn port_footer(p: &Project, element: Id) -> f32 {
|
|
let count = p
|
|
.children(element)
|
|
.filter(|e| e.kind() == Kind::Port)
|
|
.count();
|
|
if count == 0 {
|
|
24.
|
|
} else {
|
|
28. * count as f32 + 24.
|
|
}
|
|
}
|
|
|
|
pub(crate) fn parents(p: &Project, d: &Diagram) -> BTreeMap<Id, Option<Id>> {
|
|
let placed: BTreeMap<_, _> = d
|
|
.nodes
|
|
.values()
|
|
.filter(|n| {
|
|
p.elements
|
|
.get(&n.element)
|
|
.is_some_and(|e| e.kind() != Kind::Port && d.projection.includes(e.kind()))
|
|
})
|
|
.map(|n| (n.element, n.id))
|
|
.collect();
|
|
d.nodes
|
|
.values()
|
|
.filter(|n| placed.contains_key(&n.element))
|
|
.map(|n| {
|
|
let mut semantic = p.elements.get(&n.element).and_then(|e| e.parent);
|
|
if d.projection == ProjectionKind::Allocation
|
|
&& let Some(performer) = crate::validation::allocation_summary(p, n.element)
|
|
{
|
|
let mut candidate = semantic;
|
|
let mut visible_work_parent = None;
|
|
let mut seen = BTreeSet::new();
|
|
while let Some(id) = candidate {
|
|
if !seen.insert(id)
|
|
|| crate::validation::allocation_summary(p, id) != Some(performer)
|
|
{
|
|
break;
|
|
}
|
|
if placed.contains_key(&id) {
|
|
visible_work_parent = Some(id);
|
|
break;
|
|
}
|
|
candidate = p.elements.get(&id).and_then(|e| e.parent);
|
|
}
|
|
semantic = visible_work_parent.or(Some(performer));
|
|
}
|
|
if d.projection == ProjectionKind::Traceability {
|
|
return (n.id, None);
|
|
}
|
|
let mut seen = BTreeSet::new();
|
|
let mut parent = None;
|
|
while let Some(id) = semantic {
|
|
if !seen.insert(id) {
|
|
break;
|
|
}
|
|
if let Some(node) = placed.get(&id) {
|
|
parent = Some(*node);
|
|
break;
|
|
}
|
|
semantic = p.elements.get(&id).and_then(|e| e.parent);
|
|
}
|
|
(n.id, parent)
|
|
})
|
|
.collect()
|
|
}
|
|
pub fn descendant_nodes(p: &Project, diagram: Id, node: Id) -> Vec<Id> {
|
|
let Some(d) = p.diagrams.get(&diagram) else {
|
|
return vec![];
|
|
};
|
|
let parents = parents(p, d);
|
|
parents
|
|
.keys()
|
|
.copied()
|
|
.filter(|candidate| {
|
|
let mut current = parents.get(candidate).copied().flatten();
|
|
let mut seen = BTreeSet::new();
|
|
while let Some(id) = current {
|
|
if id == node {
|
|
return true;
|
|
}
|
|
if !seen.insert(id) {
|
|
break;
|
|
}
|
|
current = parents.get(&id).copied().flatten();
|
|
}
|
|
false
|
|
})
|
|
.collect()
|
|
}
|
|
/// One projection drives drawing, hit testing, Fit, SVG, and grouped movement.
|
|
pub fn project_view(p: &Project, diagram: Id) -> ProjectedView {
|
|
let Some(d) = p.diagrams.get(&diagram) else {
|
|
return ProjectedView::default();
|
|
};
|
|
project_diagram(p, d)
|
|
}
|
|
/// Project alternate placement geometry without copying or mutating semantics.
|
|
pub fn project_diagram(p: &Project, d: &Diagram) -> ProjectedView {
|
|
let parents = parents(p, d);
|
|
let mut nodes = Vec::new();
|
|
for n in d.nodes.values() {
|
|
let Some(e) = p.elements.get(&n.element) else {
|
|
continue;
|
|
};
|
|
if e.kind() == Kind::Port || !d.projection.includes(e.kind()) {
|
|
continue;
|
|
}
|
|
if d.projection != ProjectionKind::Traceability && e.layer() != d.layer {
|
|
continue;
|
|
}
|
|
let mut current = parents.get(&n.id).copied().flatten();
|
|
let mut seen = BTreeSet::new();
|
|
let mut depth = 0;
|
|
let mut hidden = false;
|
|
while let Some(id) = current {
|
|
if !seen.insert(id) {
|
|
hidden = true;
|
|
break;
|
|
}
|
|
depth += 1;
|
|
if d.nodes.get(&id).is_some_and(|n| n.collapsed) {
|
|
hidden = true;
|
|
}
|
|
current = parents.get(&id).copied().flatten();
|
|
}
|
|
if !hidden {
|
|
let size = minimum_node_size(
|
|
p,
|
|
e.id,
|
|
if n.collapsed {
|
|
Size {
|
|
width: n.size.width,
|
|
height: 94.,
|
|
}
|
|
} else {
|
|
n.size
|
|
},
|
|
);
|
|
nodes.push(ProjectedNode {
|
|
id: n.id,
|
|
element: n.element,
|
|
position: n.position,
|
|
size,
|
|
visual_parent: parents.get(&n.id).copied().flatten(),
|
|
depth,
|
|
collapsed: n.collapsed,
|
|
allocation_summary: d.projection == ProjectionKind::Allocation
|
|
&& crate::validation::allocation_summary(p, e.id).is_some()
|
|
&& !p
|
|
.relations
|
|
.values()
|
|
.any(|r| r.kind() == RelationKind::Allocation && r.target() == e.id),
|
|
});
|
|
}
|
|
}
|
|
nodes.sort_by_key(|n| (n.depth, n.id));
|
|
for index in (0..nodes.len()).rev() {
|
|
let child = nodes[index].clone();
|
|
if let Some(parent) = child
|
|
.visual_parent
|
|
.and_then(|id| nodes.iter().position(|n| n.id == id))
|
|
{
|
|
let footer = port_footer(p, nodes[parent].element);
|
|
let p = &mut nodes[parent];
|
|
let right =
|
|
(p.position.x + p.size.width).max(child.position.x + child.size.width + 24.);
|
|
let bottom =
|
|
(p.position.y + p.size.height).max(child.position.y + child.size.height + footer);
|
|
p.position.x = p.position.x.min(child.position.x - 24.);
|
|
// Reserve two readable header lines above the first nested card.
|
|
p.position.y = p.position.y.min(child.position.y - 64.);
|
|
p.size.width = right - p.position.x;
|
|
p.size.height = bottom - p.position.y;
|
|
}
|
|
}
|
|
let visible: BTreeSet<_> = nodes.iter().map(|n| n.id).collect();
|
|
let placed: BTreeMap<_, _> = d.nodes.values().map(|n| (n.element, n.id)).collect();
|
|
let anchor = |element: Id| {
|
|
let owner = p
|
|
.elements
|
|
.get(&element)
|
|
.filter(|e| e.kind() == Kind::Port)
|
|
.and_then(|e| e.parent)
|
|
.unwrap_or(element);
|
|
let mut id = placed.get(&owner).copied();
|
|
let mut seen = BTreeSet::new();
|
|
while let Some(n) = id {
|
|
if visible.contains(&n) {
|
|
return Some(n);
|
|
}
|
|
if !seen.insert(n) {
|
|
break;
|
|
}
|
|
id = parents.get(&n).copied().flatten();
|
|
}
|
|
None
|
|
};
|
|
let mut ports = Vec::new();
|
|
for node in &nodes {
|
|
let mut children: Vec<_> = p
|
|
.children(node.element)
|
|
.filter(|e| e.kind() == Kind::Port)
|
|
.collect();
|
|
children.sort_by_key(|e| (&e.alias, e.id));
|
|
for direction in [PortDirection::In, PortDirection::Out, PortDirection::InOut] {
|
|
let side: Vec<_> = children.iter().filter(|e| matches!(e.shared(), Some(SharedBody::Port {direction: d, ..}) if *d == direction)).collect();
|
|
for (index, port) in side.iter().enumerate() {
|
|
let fraction = (index + 1) as f32 / (side.len() + 1) as f32;
|
|
let y = if p.elements[&node.element].kind().is_performer()
|
|
|| nodes
|
|
.iter()
|
|
.any(|child| child.visual_parent == Some(node.id))
|
|
{
|
|
node.position.y + node.size.height - 24. - index as f32 * 28.
|
|
} else {
|
|
node.position.y + 64. + (node.size.height - 76.).max(0.) * fraction
|
|
};
|
|
let position = match direction {
|
|
PortDirection::In => Position {
|
|
x: node.position.x,
|
|
y,
|
|
},
|
|
PortDirection::Out => Position {
|
|
x: node.position.x + node.size.width,
|
|
y,
|
|
},
|
|
PortDirection::InOut => Position {
|
|
x: node.position.x + node.size.width * fraction,
|
|
y: node.position.y + node.size.height,
|
|
},
|
|
};
|
|
ports.push(ProjectedPort {
|
|
element: port.id,
|
|
owner: node.id,
|
|
position,
|
|
direction,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
let mut edges = Vec::new();
|
|
for r in p.relations.values() {
|
|
if d.projection == ProjectionKind::Allocation && r.kind() == RelationKind::Allocation {
|
|
continue;
|
|
}
|
|
let included = match d.projection {
|
|
ProjectionKind::Allocation => matches!(
|
|
r.kind(),
|
|
RelationKind::Interaction
|
|
| RelationKind::FunctionalExchange
|
|
| RelationKind::BoundaryExchange
|
|
),
|
|
ProjectionKind::FunctionalDataflow => matches!(
|
|
r.kind(),
|
|
RelationKind::Interaction | RelationKind::FunctionalExchange
|
|
),
|
|
ProjectionKind::Capabilities => matches!(
|
|
r.kind(),
|
|
RelationKind::CapabilityInclude
|
|
| RelationKind::CapabilityExtend
|
|
| RelationKind::CapabilityGeneralization
|
|
| RelationKind::MissionCapability
|
|
| RelationKind::MissionInvolvement
|
|
| RelationKind::CapabilityInvolvement
|
|
),
|
|
_ => true,
|
|
};
|
|
if !included {
|
|
continue;
|
|
}
|
|
if let (Some(source), Some(target)) = (anchor(r.source()), anchor(r.target()))
|
|
&& source != target
|
|
{
|
|
let (source_port, target_port) = match &r.body {
|
|
RelationBody::FunctionalExchange {
|
|
source_port,
|
|
target_port,
|
|
..
|
|
} => (*source_port, *target_port),
|
|
RelationBody::BoundaryExchange { source, target, .. } => {
|
|
(Some(*source), Some(*target))
|
|
}
|
|
_ => (None, None),
|
|
};
|
|
edges.push(ProjectedEdge {
|
|
id: r.id,
|
|
source,
|
|
target,
|
|
trace: false,
|
|
source_port: source_port.filter(|id| ports.iter().any(|p| p.element == *id)),
|
|
target_port: target_port.filter(|id| ports.iter().any(|p| p.element == *id)),
|
|
});
|
|
}
|
|
}
|
|
if d.projection == ProjectionKind::Traceability {
|
|
for t in p.traces.values() {
|
|
if let (Some(source), Some(target)) = (anchor(t.oa), anchor(t.sa))
|
|
&& source != target
|
|
{
|
|
edges.push(ProjectedEdge {
|
|
id: t.id,
|
|
source,
|
|
target,
|
|
trace: true,
|
|
source_port: None,
|
|
target_port: None,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
ProjectedView {
|
|
nodes,
|
|
edges,
|
|
ports,
|
|
}
|
|
}
|