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
+93 -3
View File
@@ -338,6 +338,19 @@ fn route_diagram_impl(
let nodes: BTreeMap<_, _> = view.nodes.iter().map(|n| (n.id, n)).collect();
let rectangles: Vec<_> = nodes.values().map(|n| Rect::node(n)).collect();
let authored: BTreeMap<_, _> = diagram.routes.iter().map(|r| (r.edge, r)).collect();
let headers: BTreeMap<_, _> = nodes
.values()
.map(|node| {
(
node.id,
if diagram.projection.state_machine().is_some() && !node.collapsed {
projection::state_header_height(project, node.element)
} else {
HEADER
},
)
})
.collect();
let mut ordered: Vec<_> = view.edges.iter().collect();
ordered.sort_by_key(|e| e.id);
let mut edges = BTreeMap::new();
@@ -379,7 +392,8 @@ fn route_diagram_impl(
let custom_suspended = promoted;
let route = if promoted { None } else { stored };
let pins = route.map_or_else(Vec::new, |r| r.waypoints.clone());
let obstacles = edge_obstacles(edge, &nodes, &rectangles);
let obstacles = edge_obstacles(edge, &nodes, &rectangles, &headers);
let cached = preview.and_then(|(baseline, changed)| {
(edge.id == changed)
.then(|| baseline.edges.get(&edge.id))
@@ -459,7 +473,7 @@ fn route_diagram_impl(
if preview.is_none_or(|(baseline, changed)| {
edge.id == changed || !baseline.edges.contains_key(&edge.id)
}) {
let obstacles = edge_obstacles(edge, &nodes, &rectangles);
let obstacles = edge_obstacles(edge, &nodes, &rectangles, &headers);
let authored_label = (!routed.custom_suspended)
.then(|| {
authored
@@ -492,6 +506,7 @@ fn edge_obstacles(
edge: &ProjectedEdge,
nodes: &BTreeMap<Id, &ProjectedNode>,
rectangles: &[Rect],
headers: &BTreeMap<Id, f32>,
) -> Vec<Rect> {
let mut traversable = BTreeSet::new();
ancestors(edge.source, nodes, &mut traversable);
@@ -501,7 +516,7 @@ fn edge_obstacles(
.map(|r| {
let mut r = *r;
if traversable.contains(&r.id) {
r.bottom = r.bottom.min(r.top + HEADER);
r.bottom = r.bottom.min(r.top + headers[&r.id]);
}
r.inflate(CLEARANCE)
})
@@ -564,6 +579,15 @@ fn route_edge(
let mut candidates = Vec::new();
for a in endpoints(source, edge.source_port, view) {
for b in endpoints(target, edge.target_port, view) {
// A real self-transition must leave and return at distinct anchors.
// Reusing one anchor makes the cheapest path a degenerate stub.
if edge.source == edge.target
&& edge.source_port.is_none()
&& edge.target_port.is_none()
&& a.point == b.point
{
continue;
}
let estimate = manhattan(a.stub, pins.first().copied().unwrap_or(b.stub))
+ pins.windows(2).map(|p| manhattan(p[0], p[1])).sum::<f32>()
+ pins.last().map_or(0.0, |&p| manhattan(p, b.stub));
@@ -1010,10 +1034,76 @@ fn grid_path(
(None, false)
}
/// Full semantic label shared by state transition routes and internal rows.
/// Expressions remain descriptive; no state-machine execution is implied.
pub fn state_transition_summary(p: &Project, r: &crate::Relation) -> String {
let crate::RelationBody::StateTransition {
transition_kind,
triggers,
trigger,
guard,
effects,
effect,
..
} = &r.body
else {
return r.name.clone();
};
let activation = if !triggers.is_empty() {
triggers
.iter()
.map(|id| p.named(*id))
.collect::<Vec<_>>()
.join(" OR ")
} else if !trigger.trim().is_empty() {
format!("Unresolved trigger: {trigger}")
} else if p
.elements
.get(&r.source())
.is_some_and(|e| e.kind().is_stable_state())
{
"Completion".into()
} else {
"Transient routing".into()
};
let mut result = format!("{transition_kind:?} \u{b7} {activation}");
if !triggers.is_empty() && !trigger.is_empty() {
result.push_str(&format!(" \u{b7} {trigger}"));
}
if !guard.is_empty() {
result.push_str(&format!(" [{guard}]"));
}
if !effects.is_empty() {
result.push_str(&format!(
" / {}",
effects
.iter()
.map(|id| p.named(*id))
.collect::<Vec<_>>()
.join(", ")
));
}
if !effect.is_empty() {
result.push_str(&format!(" / {effect}"));
}
result
}
fn label_text(project: &Project, edge: Id) -> String {
let Some(relation) = project.relations.get(&edge) else {
return String::new();
};
if relation.kind() == RelationKind::StateTransition {
let text = state_transition_summary(project, relation);
return if text.chars().count() > 48 {
text.chars()
.take(47)
.chain(std::iter::once('\u{2026}'))
.collect()
} else {
text
};
}
let text = match relation.kind() {
RelationKind::Interaction
| RelationKind::FunctionalExchange