Files
ARCADIA-Studio/crates/oa-core/src/projection/state.rs
T

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))
})
}