Add persisted state-machine diagram contexts and shared geometry
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user