Add persisted state-machine diagram contexts and shared geometry
This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
//! State-specific presentation policies over the shared persisted placements.
|
||||
use super::*;
|
||||
|
||||
/// Membership of a dedicated state view. Other projection kinds may retain
|
||||
/// unrelated hidden placements and must not acquire this additional restriction.
|
||||
pub fn state_machine_contains(p: &Project, machine: Id, element: Id) -> bool {
|
||||
let Some(owner) = p
|
||||
.elements
|
||||
.get(&machine)
|
||||
.filter(|e| e.kind() == Kind::StateMachine)
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
p.elements.get(&element).is_some_and(|e| {
|
||||
e.layer() == owner.layer()
|
||||
&& (matches!(e.kind(), Kind::StateMachine | Kind::Region) || e.kind().is_state())
|
||||
&& validation::ancestor_of_kind(p, e.id, Kind::StateMachine) == Some(machine)
|
||||
})
|
||||
}
|
||||
|
||||
/// Fixed body bounds for UML-style state symbols. Their authored position is
|
||||
/// the top-left of this body; labels and larger pointer targets are derived UI.
|
||||
/// A State or Mode uses its authored rectangular size instead.
|
||||
pub fn state_symbol_size(kind: Kind) -> Option<Size> {
|
||||
let (width, height) = match kind {
|
||||
Kind::Initial => (16., 16.),
|
||||
Kind::Junction => (10., 10.),
|
||||
Kind::Fork | Kind::Join => (8., 36.),
|
||||
Kind::Choice | Kind::ShallowHistory | Kind::DeepHistory => (28., 28.),
|
||||
Kind::Final | Kind::EntryPoint | Kind::ExitPoint | Kind::Terminate => (24., 24.),
|
||||
_ => return None,
|
||||
};
|
||||
Some(Size { width, height })
|
||||
}
|
||||
|
||||
/// Reserved content before nested Regions. The same boundary is used by layout,
|
||||
/// routing and the state editor's action/internal-transition rows.
|
||||
pub fn state_header_height(p: &Project, element: Id) -> f32 {
|
||||
let Some(e) = p.elements.get(&element) else {
|
||||
return 64.;
|
||||
};
|
||||
if !e.kind().is_stable_state() {
|
||||
return 64.;
|
||||
}
|
||||
let actions = matches!(e.shared(), Some(SharedBody::State { entry, exit, do_activity, .. })
|
||||
if !entry.is_empty() || !exit.is_empty() || !do_activity.is_empty());
|
||||
let internal = p
|
||||
.relations
|
||||
.values()
|
||||
.filter(|r| {
|
||||
matches!(&r.body,
|
||||
RelationBody::StateTransition { source, transition_kind: TransitionKind::Internal, .. }
|
||||
if *source == element)
|
||||
})
|
||||
.count();
|
||||
64. + if actions { 28. } else { 0. } + internal as f32 * 56.
|
||||
}
|
||||
|
||||
pub(crate) fn node_size(p: &Project, e: &Element, size: Size, collapsed: bool) -> Size {
|
||||
if let Some(size) = state_symbol_size(e.kind()) {
|
||||
return size;
|
||||
}
|
||||
Size {
|
||||
width: size.width.max(if e.kind().is_stable_state() {
|
||||
184.
|
||||
} else {
|
||||
224.
|
||||
}),
|
||||
height: size.height.max(if collapsed {
|
||||
94.
|
||||
} else {
|
||||
state_header_height(p, e.id) + 20.
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn snap_boundaries(p: &Project, nodes: &mut [ProjectedNode]) {
|
||||
let geometry: BTreeMap<_, _> = nodes
|
||||
.iter()
|
||||
.map(|n| (n.element, (n.position, n.size)))
|
||||
.collect();
|
||||
for node in nodes {
|
||||
let Some(e) = p.elements.get(&node.element) else {
|
||||
continue;
|
||||
};
|
||||
if !matches!(e.kind(), Kind::EntryPoint | Kind::ExitPoint) {
|
||||
continue;
|
||||
}
|
||||
let boundary = e
|
||||
.parent
|
||||
.and_then(|id| p.elements.get(&id))
|
||||
.and_then(|region| region.parent);
|
||||
let Some((position, size)) = boundary.and_then(|id| geometry.get(&id)) else {
|
||||
// Partial views may omit the containing boundary. Do not invent a
|
||||
// placement or silently attach the symbol to a different container.
|
||||
continue;
|
||||
};
|
||||
node.position.x = position.x
|
||||
+ if e.kind() == Kind::ExitPoint {
|
||||
size.width
|
||||
} else {
|
||||
0.
|
||||
}
|
||||
- node.size.width / 2.;
|
||||
let top = position.y + boundary.map_or(64., |id| state_header_height(p, id));
|
||||
let bottom = (position.y + size.height - node.size.height - 12.).max(top);
|
||||
node.position.y = node.position.y.clamp(top, bottom);
|
||||
}
|
||||
}
|
||||
|
||||
// Entry/Exit positions are attached to a containing State/Machine, not the
|
||||
// Region that owns them semantically. Moving only that Region changes the
|
||||
// symbol's authored position but does not translate its snapped boundary.
|
||||
pub(super) fn boundary_moves_with(
|
||||
p: &Project,
|
||||
d: &Diagram,
|
||||
edge: Id,
|
||||
delta: &Position,
|
||||
deltas: &BTreeMap<Id, Position>,
|
||||
) -> bool {
|
||||
let Some(r) = p.relations.get(&edge) else {
|
||||
return true;
|
||||
};
|
||||
[r.source(), r.target()].into_iter().all(|id| {
|
||||
let Some(e) = p.elements.get(&id) else {
|
||||
return true;
|
||||
};
|
||||
if !matches!(e.kind(), Kind::EntryPoint | Kind::ExitPoint) {
|
||||
return true;
|
||||
}
|
||||
let boundary = e
|
||||
.parent
|
||||
.and_then(|id| p.elements.get(&id))
|
||||
.and_then(|region| region.parent);
|
||||
boundary
|
||||
.and_then(|id| d.nodes.values().find(|n| n.element == id))
|
||||
.is_none_or(|node| deltas.get(&node.id) == Some(delta))
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user