145 lines
5.1 KiB
Rust
145 lines
5.1 KiB
Rust
//! 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();
|
|
let typed_actions = p
|
|
.relations
|
|
.values()
|
|
.filter(|r| r.kind() == RelationKind::StateAction && r.source() == element)
|
|
.count();
|
|
64. + if actions { 28. } else { 0. } + typed_actions as f32 * 28. + 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))
|
|
})
|
|
}
|