Establish OA/SA prototype baseline and first-release work

This commit is contained in:
2026-09-05 17:04:16 +03:00
commit 93ba83d321
109 changed files with 56512 additions and 0 deletions
+310
View File
@@ -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,
}
}