Establish OA/SA prototype baseline and first-release work
This commit is contained in:
@@ -0,0 +1,310 @@
|
||||
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,
|
||||
}
|
||||
#[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 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 keep_parent = semantic.is_some_and(|parent| {
|
||||
crate::validation::allocation_summary(p, parent) == Some(performer)
|
||||
});
|
||||
if !keep_parent {
|
||||
semantic = 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();
|
||||
};
|
||||
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, 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,
|
||||
});
|
||||
}
|
||||
}
|
||||
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 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.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 position = match direction {
|
||||
PortDirection::In => Position {
|
||||
x: node.position.x,
|
||||
y: node.position.y + 64. + (node.size.height - 76.).max(0.) * fraction,
|
||||
},
|
||||
PortDirection::Out => Position {
|
||||
x: node.position.x + node.size.width,
|
||||
y: node.position.y + 64. + (node.size.height - 76.).max(0.) * fraction,
|
||||
},
|
||||
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,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user