Add persisted state-machine diagram contexts and shared geometry

This commit is contained in:
2026-09-06 02:23:31 +03:00
parent fef2e0dbce
commit e7a81637dd
9 changed files with 1143 additions and 15 deletions
+65 -8
View File
@@ -4,6 +4,10 @@ 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 {
@@ -79,9 +83,13 @@ pub(crate) fn parents(p: &Project, d: &Diagram) -> BTreeMap<Id, Option<Id>> {
.nodes
.values()
.filter(|n| {
p.elements
.get(&n.element)
.is_some_and(|e| e.kind() != Kind::Port && d.projection.includes(e.kind()))
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();
@@ -191,6 +199,11 @@ pub fn translated_routes(
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()
@@ -221,7 +234,12 @@ pub fn project_diagram(p: &Project, d: &Diagram) -> ProjectedView {
let Some(e) = p.elements.get(&n.element) else {
continue;
};
if e.kind() == Kind::Port || !d.projection.includes(e.kind()) {
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 {
@@ -255,6 +273,11 @@ pub fn project_diagram(p: &Project, d: &Diagram) -> ProjectedView {
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,
@@ -273,7 +296,10 @@ pub fn project_diagram(p: &Project, d: &Diagram) -> ProjectedView {
}
}
nodes.sort_by_key(|n| (n.depth, n.id));
enclose(p, &mut nodes);
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| {
@@ -313,6 +339,11 @@ pub fn project_diagram(p: &Project, d: &Diagram) -> ProjectedView {
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
@@ -338,7 +369,12 @@ pub fn project_diagram(p: &Project, d: &Diagram) -> ProjectedView {
continue;
}
if let (Some(source), Some(target)) = (anchor(r.source()), anchor(r.target()))
&& source != target
&& (source != target
|| (d.projection.state_machine().is_some()
&& r.source() == r.target()
&& nodes
.iter()
.any(|n| n.id == source && n.element == r.source())))
{
let (source_port, target_port) = match &r.body {
RelationBody::FunctionalExchange {
@@ -384,7 +420,13 @@ pub fn project_diagram(p: &Project, d: &Diagram) -> ProjectedView {
}
}
}
let ports = port_layout::layout(p, d, &mut nodes, &edges);
// 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)
};
ProjectedView {
nodes,
edges,
@@ -393,13 +435,28 @@ pub fn project_diagram(p: &Project, d: &Diagram) -> ProjectedView {
}
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.);
@@ -407,7 +464,7 @@ fn enclose(p: &Project, nodes: &mut [ProjectedNode]) {
(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.position.y = p.position.y.min(child.position.y - header);
p.size.width = right - p.position.x;
p.size.height = bottom - p.position.y;
}