Preserve nested architecture during authoring and distinguish allocation summaries

This commit is contained in:
2026-09-05 17:58:46 +03:00
parent c20a2073d7
commit c7101b970a
13 changed files with 2719 additions and 1653 deletions
+63 -9
View File
@@ -11,6 +11,8 @@ pub struct ProjectedNode {
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 {
@@ -42,6 +44,17 @@ pub(crate) fn minimum_node_size(p: &Project, element: Id, mut size: Size) -> Siz
}
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
@@ -62,12 +75,22 @@ pub(crate) fn parents(p: &Project, d: &Diagram) -> BTreeMap<Id, Option<Id>> {
if d.projection == ProjectionKind::Allocation
&& let Some(performer) = crate::validation::allocation_summary(p, n.element)
{
let keep_parent = semantic.is_some_and(|parent| {
crate::validation::allocation_summary(p, parent) == Some(performer)
});
if !keep_parent {
semantic = Some(performer);
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);
@@ -117,6 +140,10 @@ 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() {
@@ -145,7 +172,18 @@ pub fn project_view(p: &Project, diagram: Id) -> ProjectedView {
current = parents.get(&id).copied().flatten();
}
if !hidden {
let size = minimum_node_size(p, e.id, n.size);
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,
@@ -154,6 +192,12 @@ pub fn project_view(p: &Project, diagram: Id) -> ProjectedView {
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),
});
}
}
@@ -164,11 +208,12 @@ pub fn project_view(p: &Project, diagram: Id) -> ProjectedView {
.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 + 24.);
(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.);
@@ -209,14 +254,23 @@ pub fn project_view(p: &Project, diagram: Id) -> ProjectedView {
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: node.position.y + 64. + (node.size.height - 76.).max(0.) * fraction,
y,
},
PortDirection::Out => Position {
x: node.position.x + node.size.width,
y: node.position.y + 64. + (node.size.height - 76.).max(0.) * fraction,
y,
},
PortDirection::InOut => Position {
x: node.position.x + node.size.width * fraction,