Add persisted per-view port anchors and connection-aware border placement
This commit is contained in:
@@ -1,6 +1,9 @@
|
||||
pub use crate::model::PortAnchorSide;
|
||||
use crate::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
mod port_layout;
|
||||
pub use port_layout::{port_anchor_offset, port_anchor_position};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct ProjectedNode {
|
||||
@@ -32,34 +35,31 @@ pub struct ProjectedPort {
|
||||
pub position: Position,
|
||||
pub side: PortAnchorSide,
|
||||
}
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum PortAnchorSide {
|
||||
Left,
|
||||
Right,
|
||||
Bottom,
|
||||
}
|
||||
/// Semantic fallback for an unconnected port. Neutral ports have no preferred
|
||||
/// side; the projected view resolves their placement using peers and capacity.
|
||||
/// A preferred side never constrains an authored anchor.
|
||||
pub fn port_anchor_side(e: &Element) -> Option<PortAnchorSide> {
|
||||
let Some(SharedBody::Port { contract, legacy }) = e.shared() else {
|
||||
return None;
|
||||
};
|
||||
Some(match contract {
|
||||
PortContract::FunctionInput { .. } => PortAnchorSide::Left,
|
||||
PortContract::FunctionOutput { .. } => PortAnchorSide::Right,
|
||||
match contract {
|
||||
PortContract::FunctionInput { .. } => Some(PortAnchorSide::Left),
|
||||
PortContract::FunctionOutput { .. } => Some(PortAnchorSide::Right),
|
||||
PortContract::Component {
|
||||
kind: ComponentPortKind::Flow,
|
||||
orientation: ComponentPortOrientation::In,
|
||||
} => PortAnchorSide::Left,
|
||||
} => Some(PortAnchorSide::Left),
|
||||
PortContract::Component {
|
||||
kind: ComponentPortKind::Flow,
|
||||
orientation: ComponentPortOrientation::Out,
|
||||
} => PortAnchorSide::Right,
|
||||
} => Some(PortAnchorSide::Right),
|
||||
PortContract::Legacy => match legacy.as_ref().map(|l| l.direction) {
|
||||
Some(PortDirection::In) => PortAnchorSide::Left,
|
||||
Some(PortDirection::Out) => PortAnchorSide::Right,
|
||||
_ => PortAnchorSide::Bottom,
|
||||
Some(PortDirection::In) => Some(PortAnchorSide::Left),
|
||||
Some(PortDirection::Out) => Some(PortAnchorSide::Right),
|
||||
_ => None,
|
||||
},
|
||||
_ => PortAnchorSide::Bottom,
|
||||
})
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
|
||||
pub struct ProjectedView {
|
||||
@@ -67,38 +67,11 @@ pub struct ProjectedView {
|
||||
pub edges: Vec<ProjectedEdge>,
|
||||
pub ports: Vec<ProjectedPort>,
|
||||
}
|
||||
pub(crate) fn minimum_node_size(p: &Project, element: Id, mut size: Size) -> Size {
|
||||
let count = [PortAnchorSide::Left, PortAnchorSide::Right]
|
||||
.into_iter()
|
||||
.map(|side| {
|
||||
p.children(element)
|
||||
.filter(|port| port_anchor_side(port) == Some(side))
|
||||
.count()
|
||||
})
|
||||
.max()
|
||||
.unwrap_or(0);
|
||||
if count > 0 {
|
||||
size.height = size.height.max(76. + 28. * (count + 1) as f32);
|
||||
}
|
||||
let bottom = p
|
||||
.children(element)
|
||||
.filter(|port| port_anchor_side(port) == Some(PortAnchorSide::Bottom))
|
||||
.count();
|
||||
if bottom > 0 {
|
||||
size.width = size.width.max(28. * (bottom + 1) as f32);
|
||||
}
|
||||
size
|
||||
pub(crate) fn minimum_node_size(p: &Project, element: Id, size: Size) -> Size {
|
||||
port_layout::minimum_size(p, element, 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 port_footer(_p: &Project, _element: Id) -> f32 {
|
||||
24.
|
||||
}
|
||||
|
||||
pub(crate) fn parents(p: &Project, d: &Diagram) -> BTreeMap<Id, Option<Id>> {
|
||||
@@ -300,25 +273,7 @@ pub fn project_diagram(p: &Project, d: &Diagram) -> ProjectedView {
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
enclose(p, &mut nodes);
|
||||
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| {
|
||||
@@ -341,56 +296,17 @@ pub fn project_diagram(p: &Project, d: &Diagram) -> ProjectedView {
|
||||
}
|
||||
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 anchor_side in [
|
||||
PortAnchorSide::Left,
|
||||
PortAnchorSide::Right,
|
||||
PortAnchorSide::Bottom,
|
||||
] {
|
||||
let side: Vec<_> = children
|
||||
.iter()
|
||||
.filter(|e| port_anchor_side(e) == Some(anchor_side))
|
||||
.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 anchor_side {
|
||||
PortAnchorSide::Left => Position {
|
||||
x: node.position.x,
|
||||
y,
|
||||
},
|
||||
PortAnchorSide::Right => Position {
|
||||
x: node.position.x + node.size.width,
|
||||
y,
|
||||
},
|
||||
PortAnchorSide::Bottom => 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,
|
||||
side: anchor_side,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
let visible_ports: BTreeSet<_> = p
|
||||
.elements
|
||||
.values()
|
||||
.filter(|e| {
|
||||
e.kind() == Kind::Port
|
||||
&& e.parent
|
||||
.and_then(|id| placed.get(&id))
|
||||
.is_some_and(|id| visible.contains(id))
|
||||
})
|
||||
.map(|e| e.id)
|
||||
.collect();
|
||||
let mut edges = Vec::new();
|
||||
for r in p.relations.values() {
|
||||
if d.projection == ProjectionKind::Allocation && r.kind() == RelationKind::Allocation {
|
||||
@@ -446,8 +362,8 @@ pub fn project_diagram(p: &Project, d: &Diagram) -> ProjectedView {
|
||||
..
|
||||
}
|
||||
),
|
||||
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)),
|
||||
source_port: source_port.filter(|id| visible_ports.contains(id)),
|
||||
target_port: target_port.filter(|id| visible_ports.contains(id)),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -468,9 +384,32 @@ pub fn project_diagram(p: &Project, d: &Diagram) -> ProjectedView {
|
||||
}
|
||||
}
|
||||
}
|
||||
let ports = port_layout::layout(p, d, &mut nodes, &edges);
|
||||
ProjectedView {
|
||||
nodes,
|
||||
edges,
|
||||
ports,
|
||||
}
|
||||
}
|
||||
|
||||
fn enclose(p: &Project, nodes: &mut [ProjectedNode]) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user