Files
ARCADIA-Studio/crates/oa-core/src/projection.rs
T

574 lines
21 KiB
Rust

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};
#[path = "projection/state.rs"]
mod state;
pub(crate) use state::node_size as state_node_size;
pub use state::{state_header_height, state_machine_contains, state_symbol_size};
#[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,
/// 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 oriented: 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 side: PortAnchorSide,
}
/// 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;
};
match contract {
PortContract::FunctionInput { .. } => Some(PortAnchorSide::Left),
PortContract::FunctionOutput { .. } => Some(PortAnchorSide::Right),
PortContract::Component {
kind: ComponentPortKind::Flow,
orientation: ComponentPortOrientation::In,
} => Some(PortAnchorSide::Left),
PortContract::Component {
kind: ComponentPortKind::Flow,
orientation: ComponentPortOrientation::Out,
} => Some(PortAnchorSide::Right),
PortContract::Legacy => match legacy.as_ref().map(|l| l.direction) {
Some(PortDirection::In) => Some(PortAnchorSide::Left),
Some(PortDirection::Out) => Some(PortAnchorSide::Right),
_ => None,
},
_ => None,
}
}
#[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, size: Size) -> Size {
port_layout::minimum_size(p, element, size)
}
pub(crate) fn port_footer(_p: &Project, _element: Id) -> f32 {
24.
}
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())
&& d.projection
.state_machine()
.is_none_or(|machine| state_machine_contains(p, machine, e.id))
})
})
.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<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()
}
/// Actual endpoint-owner placements before folding promotes a hidden endpoint
/// to an ancestor. Ports belong to their placed function/participant; they do not
/// acquire independent placement identities. Missing placements are not inferred.
pub fn edge_endpoint_placements(p: &Project, d: &Diagram, edge: Id) -> Option<(Id, Id)> {
let (source, target) = if let Some(r) = p.relations.get(&edge) {
(r.source(), r.target())
} else {
let trace = p.traces.get(&edge)?;
(trace.oa, trace.sa)
};
let placement = |id| {
let element = p.elements.get(&id)?;
let owner = if element.kind() == Kind::Port {
element.parent?
} else {
id
};
d.nodes.values().find(|n| n.element == owner).map(|n| n.id)
};
Some((placement(source)?, placement(target)?))
}
/// Route updates for a simultaneous placement movement. `deltas` maps placement
/// identities to diagram-space displacement. Only routes with two actual owners
/// moving by the same nonzero delta translate; one-ended moves keep world pins.
/// Returns changed records only, for the caller to apply with its node edits in
/// one transaction. This pure helper also drives reflow and drag previews.
pub fn translated_routes(
p: &Project,
d: &Diagram,
deltas: &BTreeMap<Id, Position>,
) -> Vec<EdgeRoute> {
d.routes
.iter()
.filter_map(|route| {
let (source, target) = edge_endpoint_placements(p, d, route.edge)?;
let source_delta = deltas.get(&source)?;
let target_delta = deltas.get(&target)?;
if d.projection.state_machine().is_some()
&& !state::boundary_moves_with(p, d, route.edge, source_delta, deltas)
{
return None;
}
if source_delta != target_delta
|| (source_delta.x == 0. && source_delta.y == 0.)
|| route.waypoints.is_empty()
{
return None;
}
let mut translated = route.clone();
for point in &mut translated.waypoints {
point.x += source_delta.x;
point.y += source_delta.y;
}
Some(translated)
})
.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)
}
/// Reuse an index built for this accepted semantic revision during presentation
/// previews. Callers must invalidate it when semantic records change.
pub fn project_view_with_index(
p: &Project,
diagram: Id,
index: &crate::view_options::ViewIndex,
) -> ProjectedView {
p.diagrams
.get(&diagram)
.map_or_else(ProjectedView::default, |d| {
project_diagram_with_index(p, d, index)
})
}
/// Project alternate placement geometry without copying or mutating semantics.
pub fn project_diagram(p: &Project, d: &Diagram) -> ProjectedView {
let index = crate::view_options::ViewIndex::new(p);
project_diagram_with_index(p, d, &index)
}
pub fn project_diagram_with_index(
p: &Project,
d: &Diagram,
index: &crate::view_options::ViewIndex,
) -> ProjectedView {
project_diagram_impl(p, d, None, Some(index))
}
/// Unfiltered eligibility from the same projection/anchor/fold decisions used
/// for drawing. Compute once for a view-options editor, then combine with the
/// prepared filter's reasons; hidden lines remain geometrically eligible here.
pub fn edge_availability(
p: &Project,
d: &Diagram,
) -> BTreeMap<Id, crate::view_options::EdgeAvailability> {
let mut availability = p
.relations
.keys()
.chain(p.traces.keys())
.map(|id| {
(
*id,
crate::view_options::EdgeAvailability::OutsideProjection,
)
})
.collect();
project_diagram_impl(p, d, Some(&mut availability), None);
availability
}
fn project_diagram_impl(
p: &Project,
d: &Diagram,
mut availability: Option<&mut BTreeMap<Id, crate::view_options::EdgeAvailability>>,
index: Option<&crate::view_options::ViewIndex>,
) -> 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())
|| d.projection
.state_machine()
.is_some_and(|machine| !state_machine_contains(p, machine, e.id))
{
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
},
);
let size = if d.projection.state_machine().is_some() {
state::node_size(p, e, size, n.collapsed)
} else {
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));
enclose_with_headers(p, &mut nodes, d.projection.state_machine().is_some());
if d.projection.state_machine().is_some() {
state::snap_boundaries(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| {
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 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() {
// Interface allocations are selectable nested dictionary rows. Their
// independently identified contracts do not add noisy diagram arrows.
if r.kind() == RelationKind::InterfaceItemAllocation {
continue;
}
if d.projection == ProjectionKind::Allocation && r.kind() == RelationKind::Allocation {
continue;
}
let included = match d.projection {
ProjectionKind::StateMachine { machine } => matches!(&r.body,
RelationBody::StateTransition { transition_kind, .. }
if *transition_kind != TransitionKind::Internal
&& state_machine_contains(p, machine, r.source())
&& state_machine_contains(p, machine, r.target())),
ProjectionKind::Allocation => matches!(
r.kind(),
RelationKind::Interaction
| RelationKind::FunctionalExchange
| RelationKind::BoundaryExchange
| RelationKind::CommunicationMean
),
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
| RelationKind::CommunicationMean
),
_ => true,
};
if !included {
continue;
}
let ends = (anchor(r.source()), anchor(r.target()));
let self_loop = (d.projection.state_machine().is_some()
|| r.kind() == RelationKind::DataAssociation)
&& r.source() == r.target()
&& nodes
.iter()
.any(|n| Some(n.id) == ends.0 && n.element == r.source());
if let Some(availability) = availability.as_deref_mut() {
use crate::view_options::EdgeAvailability as A;
availability.insert(
r.id,
match ends {
(Some(a), Some(b)) if a != b || self_loop => A::Visible,
(Some(_), Some(_)) => A::FoldedInternal,
(a, b) => A::EndpointNotShown {
source: a.is_none(),
target: b.is_none(),
},
},
);
}
if let (Some(source), Some(target)) = ends
&& (source != target || self_loop)
{
let (source_port, target_port) = match &r.body {
RelationBody::FunctionalExchange {
source_port,
target_port,
..
} => (Some(*source_port), Some(*target_port)),
RelationBody::BoundaryExchange { source, target, .. }
| RelationBody::PortAllocation { source, target } => (Some(*source), Some(*target)),
_ => (None, None),
};
edges.push(ProjectedEdge {
id: r.id,
source,
target,
trace: false,
oriented: !matches!(
&r.body,
RelationBody::BoundaryExchange {
oriented: false,
..
} | RelationBody::CommunicationMean {
oriented: false,
..
} | RelationBody::DataAssociation { .. }
),
source_port: source_port.filter(|id| visible_ports.contains(id)),
target_port: target_port.filter(|id| visible_ports.contains(id)),
});
}
}
if d.projection == ProjectionKind::Traceability {
for t in p.traces.values() {
let ends = (anchor(t.oa), anchor(t.sa));
if let Some(availability) = availability.as_deref_mut() {
use crate::view_options::EdgeAvailability as A;
availability.insert(
t.id,
match ends {
(Some(a), Some(b)) if a != b => A::Visible,
(Some(_), Some(_)) => A::FoldedInternal,
(a, b) => A::EndpointNotShown {
source: a.is_none(),
target: b.is_none(),
},
},
);
}
if let (Some(source), Some(target)) = ends
&& source != target
{
edges.push(ProjectedEdge {
id: t.id,
source,
target,
trace: true,
oriented: true,
source_port: None,
target_port: None,
});
}
}
}
// Dedicated state views contain state symbols, not semantic component ports.
// Port layout would re-enclose already snapped boundary vertices.
let ports = if d.projection.state_machine().is_some() {
Vec::new()
} else {
port_layout::layout(p, d, &mut nodes, &edges)
};
// Port geometry uses every eligible edge. Presentation filters must never
// move visible ports when another line is hidden or a category is selected.
if let Some(index) = index {
let filter = if d.projection == ProjectionKind::Traceability {
index.prepare_cross_layer(d.layer, &d.view_options)
} else {
index.prepare(d.layer, &d.view_options)
};
edges.retain(|edge| filter.includes(edge.id));
}
ProjectedView {
nodes,
edges,
ports,
}
}
fn enclose(p: &Project, nodes: &mut [ProjectedNode]) {
enclose_with_headers(p, nodes, false);
}
fn enclose_with_headers(p: &Project, nodes: &mut [ProjectedNode], state_view: bool) {
for index in (0..nodes.len()).rev() {
let child = nodes[index].clone();
if state_view
&& p.elements
.get(&child.element)
.is_some_and(|e| matches!(e.kind(), Kind::EntryPoint | Kind::ExitPoint))
{
continue;
}
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 header = if state_view {
state_header_height(p, nodes[parent].element)
} else {
64.
};
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 - header);
p.size.width = right - p.position.x;
p.size.height = bottom - p.position.y;
}
}
}