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, 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, pub target_port: Option, } #[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, pub edges: Vec, pub ports: Vec, } 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> { 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 { 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, } }