Sign in

    #Pure motion and structural exploration

    moonrockz/diagrams/animation builds immutable catalogs and finite or illustrative plans over BPMN, sequence and swimlane models. It reads no clock, DOM, environment or network. A sample takes explicit integer milliseconds and returns active occurrence keys, playback ordinals and integer progress fractions.

    PlaybackPlan is the full API. The earlier MotionPlan constructor and its single-flow sample remain available for the initial HTML tracer. moonrockz/diagrams-svg scenes and moonrockz/diagrams-animated HTML consume PlaybackPlan with the same source checks.

    #Explicit scenarios

    A Catalog binds occurrence keys to an immutable source diagram. BPMN keys name render connections while retaining their original source identity and flow value. Sequence keys name containment paths through events, frame sections and nested events. Repeated playback of one source occurrence has distinct step ordinals.

    Motion::serial orders children. Motion::parallel starts children together and waits for the longest branch before the next serial child. Parallel branches can contain their own serial order; they do not wait at artificial per-message barriers. Constructors copy input arrays. A completed finite plan has no active occurrences.

    ///|
    test "sample an explicit concurrent scenario" {
    let view = @bpmn.Bpmn::new(
    "P",
    [@bpmn.Node::new("A", Task), @bpmn.Node::new("B", Task)],
    [
    @bpmn.Flow::new("AB", "A", "B", condition="amount > 0"),
    @bpmn.Flow::new("BA", "B", "A"),
    ],
    )
    let catalog = @animation.Catalog::bpmn(view).catalog.unwrap()
    let a = catalog.flow("AB").unwrap().key
    let b = catalog.flow("BA").unwrap().key
    let motion = @animation.Motion::serial([
    @animation.Motion::parallel([Step(a, 1000L), Step(b, 500L)]),
    Step(a, 1000L),
    ])
    let plan = @animation.PlaybackPlan::scenario(catalog, motion).plan.unwrap()
    assert_eq(plan.duration_ms, 2000L)
    assert_eq(plan.sample_at(250L).active.length(), 2)
    assert_eq(plan.sample_at(1000L).active[0].ordinal, 2)
    assert_true(plan.sample_at(2000L).finished)
    assert_eq(
    catalog.flow("AB").unwrap().flow.unwrap().condition,
    Some("amount > 0"),
    )
    }

    PlaybackPlan::illustrative visits selected authored connections independently and repeats its period. It includes sequence messages and, by default, BPMN sequence/message flows. Other BPMN connection kinds can be selected explicitly. Alternative sections shown in this mode are separate illustrations, not one asserted execution. Sampling wraps at the period; negative time starts at zero. Finite scenarios clamp at their end. Conditions and event settings stay text. No plan evaluates conditions, schedules timers or reports execution outcomes.

    matches(catalog) compares the view identity and immutable source diagram. A route supplied to another source produces animation/plan-mismatch, even if local element IDs happen to match. The HTML renderer makes the same check before emitting output.

    #Sequence frames and choices

    The catalog retains the complete frame tree, section labels and message paths. PlaybackPlan::sequence requires explicit Selection values for encountered alt, opt and loop frames. Choose one alt section, include or skip an opt frame, and provide a finite loop count. Zero repetitions are valid. Unselected branches remain part of the source model, but contribute no scheduled steps. Choices for nested frames are required only when that branch is selected.

    Par and par-over sections become concurrent branches with their own order. Other frame kinds retain grouping and authored order. Frame labels remain descriptions; the planner does not interpret break conditions or model runtime cancellation or locking.

    ///|
    test "choose a bounded sequence loop" {
    let message = @sequence.Event::Message(
    @sequence.Message::new("A", "A", "retry"),
    )
    let sequence = @sequence.Sequence::new([@sequence.Participant::new("A")], [
    Frame(
    @sequence.Frame::new(
    Loop,
    @sequence.Section::new([message], label="retry condition"),
    ),
    ),
    ])
    let catalog = @animation.Catalog::sequence(sequence, view_id="Retry").catalog.unwrap()
    let selection = @animation.Selection::new(
    catalog.frames[0].key,
    Repetitions(2),
    )
    let plan = @animation.PlaybackPlan::sequence(catalog, selections=[selection]).plan.unwrap()
    assert_eq(plan.steps.length(), 2)
    assert_eq(plan.steps[0].occurrence.path, [0, 0, 0])
    assert_eq(plan.steps[0].occurrence.key, plan.steps[1].occurrence.key)
    assert_eq(plan.steps[1].ordinal, 1)
    }

    #Structural queries

    catalog.graph exposes immutable nodes and edges. route returns one directed shortest route with authored edge order as the tie-breaker. reachable returns breadth-first discovery including the start once. Both accept explicit kinds, forward/reverse/both direction and limits. They distinguish invalid endpoints, endpoints outside the selected view, exhausted bounds and unreachable targets.

    BPMN defaults to sequence flows. Message flows are opt-in; associations and data associations never become control transitions. Graph endpoints match actual semantic source identities within their process instance. A pool, collapsed subprocess or activity used as a display anchor never becomes a replacement semantic endpoint. Hidden actual endpoints stay outside the graph, while a drawn connection can still be a motion occurrence. Diagram queries are tested against the standalone document graph, including imports, shared process instances, cycles and equal-length ties.

    Sequence graph vertices are message occurrences and frame controls. They preserve alternative/parallel branches and one authored structural loop iteration. Finite repetitions come from scenario choices. A -> B -> A does not create a participant cycle. PlaybackPlan::route can illustrate a selected structural route. Reverse queries retain authored connection direction; they never reverse an arrow. A structural path makes no claim that gateway conditions or synchronization permit an execution.

    #Bounds and findings

    Durations use Int64 milliseconds. Each step must be positive; the total must be at most 9007199254740991, JavaScript's largest exact integer. Plans contain at most 100,000 steps. Defaults also bound catalogs/queries to 10,000 nodes, 50,000 edges, 10,000 results, one million work units and depth 256. Work can exhaust before a size bound; increase Limits explicitly when needed. Exhaustion returns no partial catalog or plan. Empty finite scenarios finish immediately.

    Planning diagnostics include animation/invalid-identity, ambiguous-endpoint, invalid-duration, invalid-selection, selection-required, plan-mismatch, plan-limit and no-flow, all under the animation/ prefix. BPMN scope validation also retains bpmn/invalid-reference and bpmn/layout-limit. Query failures are typed values rather than unreachable results.

    BPMN visibility comes from moonrockz/diagrams/bpmn.Bpmn::scope, shared with SVG geometry. Ownership, collapsed/black-box flags and attachments determine it; category groups never hide their members. A boundary follows its attached activity's visibility, while a boundary on a visible collapsed subprocess stays visible outside that subprocess. The standalone XML module has no dependency on this package or SVG.

    #Swimlane connections and traversal orientation

    Catalog::swimlane retains typed node and connection sources without BPMN substitutes. Lanes and visible groups are selectable endpoints, but containment creates no graph arcs. Collapsed descendants report OutsideView; a visible handoff to their display anchor can still be played independently.

    Directed connections use SwimlaneFlow, included in default route/reachability selection. Bidirectional connections provide both oriented arcs. Headless connections require explicit Undirected route/reachability selection. The illustrative plan can show every visible connection independently, including a headless line, without asserting a directed route. Invisible layout constraints provide no occurrence or arc. Route.reversed records whether each traversal runs backward along the rendered connection, independently of global RL or BT layout. Motion::OrientedStep(key,duration,reversed) retains this value in scheduled steps and samples. Existing Motion::Step remains forward.

    ///|
    test "play a bidirectional swimlane return" {
    let view = @swimlane.Swimlane::new(
    "Review",
    [@swimlane.Node::new("A"), @swimlane.Node::new("B")],
    [@swimlane.Connection::new("AB", "A", "B", source_marker=Arrow)],
    )
    let catalog = @animation.Catalog::swimlane(view).catalog.unwrap()
    guard catalog.graph.route("B", "A") is Found(route) else {
    fail("missing return")
    }
    assert_eq(route.reversed, [true])
    let plan = @animation.PlaybackPlan::route(catalog, route).plan.unwrap()
    assert_true(plan.sample_at(250L).active[0].reversed)
    }

    Active

    pub struct Active {
    ordinal : Int
    key : String
    numerator : Int64
    denominator : Int64
    concurrent_groups : ReadOnlyArray[Int]
    reversed : Bool
    } derive(Eq,
    Debug
    )

    Active::equal

    fn Active::equal(Active, Active) -> Bool

    Active::not_equal

    fn Active::not_equal(x : Active, y : Active) -> Bool

    Active::to_repr

    Catalog

    pub struct Catalog {
    view_id : String
    source :
    Diagram

    occurrences : ReadOnlyArray[Occurrence]
    frames : ReadOnlyArray[FrameInfo]
    controls : ReadOnlyArray[Control]
    graph : Graph
    // private fields
    }

    Catalog::bpmn

    Catalog::connection

    fn Catalog::connection(self : Catalog, id : String) -> Occurrence?

    Catalog::flow

    fn Catalog::flow(self : Catalog, id : String) -> Occurrence?

    Catalog::occurrence

    fn Catalog::occurrence(self : Catalog, key : String) -> Occurrence?

    Catalog::sequence

    fn Catalog::sequence(sequence :
    Sequence
    , view_id? : String, limits? : Limits) -> CatalogResult

    Message vertices are occurrences, not participants. Frame branches remain explicit; a loop contributes one authored structural iteration, never a participant cycle or an inferred unbounded execution.

    Catalog::swimlane

    Swimlane paths are structural connections. Containment never adds an arc.

    CatalogResult

    pub struct CatalogResult {
    catalog : Catalog?
    diagnostics : ReadOnlyArray[
    Diagnostic
    ]
    }

    Choice

    pub(all) enum Choice {
    Alternative(Int)
    Repetitions(Int)
    Optional(Bool)
    } derive(Eq,
    Debug
    )

    Choice::equal

    fn Choice::equal(Choice, Choice) -> Bool

    Choice::not_equal

    fn Choice::not_equal(x : Choice, y : Choice) -> Bool

    Choice::to_repr

    Control

    pub(all) enum Control {
    Message(Int)
    Frame(Int)
    } derive(Eq,
    Debug
    )

    Control::equal

    fn Control::equal(Control, Control) -> Bool

    Control::not_equal

    fn Control::not_equal(x : Control, y : Control) -> Bool

    Control::to_repr

    Direction

    pub(all) enum Direction {
    Forward
    Reverse
    Both
    } derive(Eq,
    Debug
    )

    Direction::equal

    fn Direction::equal(Direction, Direction) -> Bool

    Direction::not_equal

    fn Direction::not_equal(x : Direction, y : Direction) -> Bool

    EdgeKind

    pub(all) enum EdgeKind {
    SequenceFlow
    MessageFlow
    Order
    SwimlaneFlow
    Undirected
    } derive(Eq,
    Debug
    )

    EdgeKind::equal

    fn EdgeKind::equal(EdgeKind, EdgeKind) -> Bool

    EdgeKind::not_equal

    fn EdgeKind::not_equal(x : EdgeKind, y : EdgeKind) -> Bool

    EdgeKind::to_repr

    FrameInfo

    pub struct FrameInfo {
    key : String
    path : ReadOnlyArray[Int]
    kind :
    FrameKind

    sections : ReadOnlyArray[SectionInfo]
    } derive(Eq,
    Debug
    )

    FrameInfo::equal

    fn FrameInfo::equal(FrameInfo, FrameInfo) -> Bool

    FrameInfo::not_equal

    fn FrameInfo::not_equal(x : FrameInfo, y : FrameInfo) -> Bool

    Graph

    pub struct Graph {
    view_id : String
    nodes : ReadOnlyArray[GraphNode]
    edges : ReadOnlyArray[GraphEdge]
    // private fields
    }

    Graph::lookup

    fn Graph::lookup(self : Graph, id : String, limits? : Limits) -> Result[GraphNode, QueryFailure]

    Graph::reachable

    fn Graph::reachable(self : Graph, start : String, kinds? : ArrayView[EdgeKind], direction? : Direction, limits? : Limits) -> ReachabilityResult

    Breadth-first discovery includes the selected start once. Associations never become control-flow edges; MessageFlow is explicit opt-in.

    Graph::route

    fn Graph::route(self : Graph, start : String, target : String, kinds? : ArrayView[EdgeKind], direction? : Direction, limits? : Limits) -> RouteResult

    One shortest structural route; authored edge order breaks equal-length ties. Reverse queries keep the original connection direction and identities.

    GraphEdge

    pub struct GraphEdge {
    kind : EdgeKind
    source : String
    target : String
    occurrence : String?
    reversed : Bool
    } derive(Eq,
    Debug
    )

    GraphEdge::equal

    fn GraphEdge::equal(GraphEdge, GraphEdge) -> Bool

    GraphEdge::not_equal

    fn GraphEdge::not_equal(x : GraphEdge, y : GraphEdge) -> Bool

    GraphNode

    pub struct GraphNode {
    id : String
    label : String
    kind : NodeKind
    occurrence : String?
    source_node :
    Node
    ?
    swimlane_node :
    Node
    ?
    } derive(Eq,
    Debug
    )

    GraphNode::equal

    fn GraphNode::equal(GraphNode, GraphNode) -> Bool

    GraphNode::not_equal

    fn GraphNode::not_equal(x : GraphNode, y : GraphNode) -> Bool

    LimitKind

    pub(all) enum LimitKind {
    Nodes
    Edges
    Results
    Work
    Steps
    Depth
    } derive(Eq,
    Debug
    )

    LimitKind::equal

    fn LimitKind::equal(LimitKind, LimitKind) -> Bool

    LimitKind::not_equal

    fn LimitKind::not_equal(x : LimitKind, y : LimitKind) -> Bool

    Limits

    pub struct Limits {
    nodes : Int
    edges : Int
    steps : Int
    results : Int
    work : Int
    depth : Int
    } derive(Eq,
    Debug
    )

    Limits::equal

    fn Limits::equal(Limits, Limits) -> Bool

    Limits::new

    fn Limits::new(nodes? : Int, edges? : Int, steps? : Int, results? : Int, work? : Int, depth? : Int) -> Limits

    Limits::not_equal

    fn Limits::not_equal(x : Limits, y : Limits) -> Bool

    Limits::to_repr

    Limits::valid

    fn Limits::valid(self : Limits) -> Bool

    Motion

    pub(all) enum Motion {
    Step(String, Int64)
    OrientedStep(String, Int64, Bool)
    Serial(ReadOnlyArray[Motion])
    Parallel(ReadOnlyArray[Motion])
    } derive(Eq,
    Debug
    )

    Motion::equal

    fn Motion::equal(Motion, Motion) -> Bool

    Motion::not_equal

    fn Motion::not_equal(x : Motion, y : Motion) -> Bool

    Motion::parallel

    fn Motion::parallel(children : ArrayView[Motion]) -> Motion

    Motion::serial

    fn Motion::serial(children : ArrayView[Motion]) -> Motion

    Motion::to_repr

    MotionPlan

    pub struct MotionPlan {
    view_id : String
    flow_id : String
    duration_ms : Int
    } derive(Eq,
    Debug
    )

    The first-slice pure plan names one authored connection and a duration.

    MotionPlan::equal

    fn MotionPlan::equal(MotionPlan, MotionPlan) -> Bool

    MotionPlan::new

    fn MotionPlan::new(view_id : String, flow_id : String, duration_ms? : Int) -> MotionPlan?

    MotionPlan::not_equal

    fn MotionPlan::not_equal(x : MotionPlan, y : MotionPlan) -> Bool

    MotionPlan::sample

    fn MotionPlan::sample(self : MotionPlan, elapsed_ms : Int) -> (Int, Int)

    Exact bounded integer progress; no clock or browser state is read.

    NodeKind

    pub(all) enum NodeKind {
    Bpmn(
    NodeKind
    )
    Message
    FrameEnter
    FrameExit
    Start
    End
    SwimlaneNode
    SwimlaneGroup
    SwimlaneLane
    } derive(Eq,
    Debug
    )

    NodeKind::equal

    fn NodeKind::equal(NodeKind, NodeKind) -> Bool

    NodeKind::not_equal

    fn NodeKind::not_equal(x : NodeKind, y : NodeKind) -> Bool

    NodeKind::to_repr

    Occurrence

    Occurrence::equal

    fn Occurrence::equal(Occurrence, Occurrence) -> Bool

    Occurrence::not_equal

    fn Occurrence::not_equal(x : Occurrence, y : Occurrence) -> Bool

    OccurrenceKind

    OccurrenceKind::equal

    OccurrenceKind::not_equal

    fn OccurrenceKind::not_equal(x : OccurrenceKind, y : OccurrenceKind) -> Bool

    PlanResult

    pub struct PlanResult {
    plan : PlaybackPlan?
    diagnostics : ReadOnlyArray[
    Diagnostic
    ]
    }

    PlaybackMode

    pub(all) enum PlaybackMode {
    Finite
    Illustrative
    } derive(Eq,
    Debug
    )

    PlaybackMode::equal

    PlaybackMode::not_equal

    fn PlaybackMode::not_equal(x : PlaybackMode, y : PlaybackMode) -> Bool

    PlaybackPlan

    pub struct PlaybackPlan {
    view_id : String
    mode : PlaybackMode
    duration_ms : Int64
    steps : ReadOnlyArray[ScheduledStep]
    // private fields
    } derive(Eq,
    Debug
    )

    PlaybackPlan::equal

    PlaybackPlan::illustrative

    fn PlaybackPlan::illustrative(catalog : Catalog, duration_ms? : Int64, kinds? : ArrayView[
    ConnectionKind
    ], limits? : Limits) -> PlanResult

    The authored catalog is an illustration, including separate alternatives. Repeating it never asserts that these connections form a feasible execution.

    PlaybackPlan::matches

    fn PlaybackPlan::matches(self : PlaybackPlan, catalog : Catalog) -> Bool

    PlaybackPlan::not_equal

    fn PlaybackPlan::not_equal(x : PlaybackPlan, y : PlaybackPlan) -> Bool

    PlaybackPlan::repeating

    fn PlaybackPlan::repeating(self : PlaybackPlan) -> PlanResult

    Repeat a validated nonempty plan without changing its source or timing.

    PlaybackPlan::route

    fn PlaybackPlan::route(catalog : Catalog, route : Route, duration_ms? : Int64, limits? : Limits) -> PlanResult

    PlaybackPlan::sample_at

    fn PlaybackPlan::sample_at(self : PlaybackPlan, elapsed_ms : Int64) -> Sample

    PlaybackPlan::scenario

    fn PlaybackPlan::scenario(catalog : Catalog, motion : Motion, limits? : Limits) -> PlanResult

    Validate a finite, explicit scenario. Parallel children share a start and the following serial item waits for the longest branch, without stage barriers.

    PlaybackPlan::sequence

    fn PlaybackPlan::sequence(catalog : Catalog, selections? : ArrayView[Selection], duration_ms? : Int64, limits? : Limits) -> PlanResult

    Choose conditional sections and finite loop counts explicitly. Frame labels remain descriptions; the planner never evaluates their text.

    QueryFailure

    pub(all) enum QueryFailure {
    InvalidEndpoint(String)
    OutsideView(String)
    InvalidLimits
    Limit(LimitKind)
    } derive(Eq,
    Debug
    )

    QueryFailure::equal

    QueryFailure::not_equal

    fn QueryFailure::not_equal(x : QueryFailure, y : QueryFailure) -> Bool

    ReachabilityResult

    pub(all) enum ReachabilityResult {
    Complete(ReadOnlyArray[GraphNode])
    Failed(QueryFailure)
    } derive(Eq,
    Debug
    )

    ReachabilityResult::equal

    ReachabilityResult::not_equal

    Route

    pub struct Route {
    view_id : String
    direction : Direction
    nodes : ReadOnlyArray[GraphNode]
    edges : ReadOnlyArray[GraphEdge]
    reversed : ReadOnlyArray[Bool]
    // private fields
    } derive(Eq,
    Debug
    )

    Route::equal

    fn Route::equal(Route, Route) -> Bool

    Route::not_equal

    fn Route::not_equal(x : Route, y : Route) -> Bool

    Route::to_repr

    RouteResult

    pub(all) enum RouteResult {
    Found(Route)
    Unreachable
    Failed(QueryFailure)
    } derive(Eq,
    Debug
    )

    RouteResult::equal

    fn RouteResult::equal(RouteResult, RouteResult) -> Bool

    RouteResult::not_equal

    fn RouteResult::not_equal(x : RouteResult, y : RouteResult) -> Bool

    Sample

    pub struct Sample {
    elapsed_ms : Int64
    duration_ms : Int64
    finished : Bool
    active : ReadOnlyArray[Active]
    } derive(Eq,
    Debug
    )

    Sample::equal

    fn Sample::equal(Sample, Sample) -> Bool

    Sample::not_equal

    fn Sample::not_equal(x : Sample, y : Sample) -> Bool

    Sample::to_repr

    ScheduledStep

    pub struct ScheduledStep {
    ordinal : Int
    occurrence : Occurrence
    start_ms : Int64
    duration_ms : Int64
    concurrent_groups : ReadOnlyArray[Int]
    reversed : Bool
    } derive(Eq,
    Debug
    )

    ScheduledStep::equal

    ScheduledStep::not_equal

    fn ScheduledStep::not_equal(x : ScheduledStep, y : ScheduledStep) -> Bool

    SectionInfo

    pub struct SectionInfo {
    label : String?
    controls : ReadOnlyArray[Control]
    } derive(Eq,
    Debug
    )

    SectionInfo::equal

    fn SectionInfo::equal(SectionInfo, SectionInfo) -> Bool

    SectionInfo::not_equal

    fn SectionInfo::not_equal(x : SectionInfo, y : SectionInfo) -> Bool

    Selection

    pub struct Selection {
    frame_key : String
    choice : Choice
    } derive(Eq,
    Debug
    )

    Selection::equal

    fn Selection::equal(Selection, Selection) -> Bool

    Selection::new

    fn Selection::new(frame_key : String, choice : Choice) -> Selection

    Selection::not_equal

    fn Selection::not_equal(x : Selection, y : Selection) -> Bool

    max_duration_ms

    let max_duration_ms : Int64