@@ -420,3 +420,113 @@ the third `bear_ring` event pushes `ring_power` past the threshold.
420420``` {seealso}
421421See {ref}`eventless-transitions` for chains, compound interactions, and `In()` guards.
422422```
423+
424+ (cross-boundary-transitions)=
425+
426+ ### Cross-boundary transitions
427+
428+ ``` {versionadded} 3.0.0
429+ ```
430+
431+ In statecharts, transitions can cross compound state boundaries — going from a
432+ state inside one compound to a state outside, or into a different compound. The
433+ engine automatically determines which states to exit and enter by computing the
434+ ** transition domain** : the smallest compound ancestor that contains both the
435+ source and all target states.
436+
437+ ``` py
438+ >> > from statemachine import State, StateChart
439+
440+ >> > class MiddleEarthJourney (StateChart ):
441+ ... validate_disconnected_states = False
442+ ... class rivendell (State .Compound ):
443+ ... council = State(initial = True )
444+ ... preparing = State()
445+ ... get_ready = council.to(preparing)
446+ ... class moria (State .Compound ):
447+ ... gates = State(initial = True )
448+ ... bridge = State(final = True )
449+ ... cross = gates.to(bridge)
450+ ... march = rivendell.to(moria)
451+
452+ >> > sm = MiddleEarthJourney()
453+ >> > set (sm.configuration_values) == {" rivendell" , " council" }
454+ True
455+
456+ >> > sm.send(" march" )
457+ >> > set (sm.configuration_values) == {" moria" , " gates" }
458+ True
459+
460+ ```
461+
462+ When ` march ` fires, the engine:
463+ 1 . Computes the transition domain (the root, since ` rivendell ` and ` moria ` are siblings)
464+ 2 . Exits ` council ` and ` rivendell ` (running their exit actions)
465+ 3 . Enters ` moria ` and its initial child ` gates ` (running their entry actions)
466+
467+ A transition can also go from a deeply nested child to an outer state:
468+
469+ ``` py
470+ >> > from statemachine import State, StateChart
471+
472+ >> > class MoriaEscape (StateChart ):
473+ ... class moria (State .Compound ):
474+ ... class halls (State .Compound ):
475+ ... entrance = State(initial = True )
476+ ... bridge = State(final = True )
477+ ... cross = entrance.to(bridge)
478+ ... assert isinstance (halls, State)
479+ ... depths = State(final = True )
480+ ... descend = halls.to(depths)
481+ ... daylight = State(final = True )
482+ ... escape = moria.to(daylight)
483+
484+ >> > sm = MoriaEscape()
485+ >> > set (sm.configuration_values) == {" moria" , " halls" , " entrance" }
486+ True
487+
488+ >> > sm.send(" escape" )
489+ >> > set (sm.configuration_values) == {" daylight" }
490+ True
491+
492+ ```
493+
494+ (transition-priority)=
495+
496+ ### Transition priority in compound states
497+
498+ ``` {versionadded} 3.0.0
499+ ```
500+
501+ When an event could match transitions at multiple levels of the state hierarchy,
502+ transitions from ** descendant states take priority** over transitions from
503+ ancestor states. This follows the SCXML specification: the most specific
504+ (deepest) matching transition wins.
505+
506+ ``` py
507+ >> > from statemachine import State, StateChart
508+
509+ >> > class PriorityExample (StateChart ):
510+ ... log = []
511+ ... class outer (State .Compound ):
512+ ... class inner (State .Compound ):
513+ ... s1 = State(initial = True )
514+ ... s2 = State(final = True )
515+ ... go = s1.to(s2, on = " log_inner" )
516+ ... assert isinstance (inner, State)
517+ ... after_inner = State(final = True )
518+ ... done_state_inner = inner.to(after_inner)
519+ ... after_outer = State(final = True )
520+ ... done_state_outer = outer.to(after_outer)
521+ ... def log_inner (self ):
522+ ... self .log.append(" inner won" )
523+
524+ >> > sm = PriorityExample()
525+ >> > sm.send(" go" )
526+ >> > sm.log
527+ [' inner won' ]
528+
529+ ```
530+
531+ If two transitions at the same level would exit overlapping states (a conflict),
532+ the one selected first in document order wins.
0 commit comments