//! 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 { 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, ) -> 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)) }) }