{% import "partials/condition_node.html" as pred %} {% import "partials/fields.html" as f %} {% import "partials/icons.html" as ic %} {# Actor x causal-progression swimlanes, drawn as a DAW arrangement view. The horizontal axis is *scenario progression*, not time: a card further right happens later in the story, but the distance between two cards means nothing. Borrowing a sequencer's furniture -- track headers down the left, a numbered ruler across the top and a bar line before every slot -- is what makes that direction readable: cards snap to numbered steps, so "further right" is a step number you can name rather than a gap you have to eyeball. The ruler counts steps and not seconds precisely because the axis is not time. Triggers hang under the action they fire, so there is no separate event lane to correlate by eye. #} {# Track colours come from `fields.entity_hue`, the same helper the condition cards tint their actor names with -- two copies of the palette would drift the moment an actor was reordered. #} {# Action categories only -- an unlisted one falls back to the generic accent. #} {%- set CATEGORY_HUES = { 'Vehicle / Motion': 'var(--actor-1)', 'Environment': 'var(--env)' } -%} {% macro add_action(draft, actor, phase='') %}
{{ ic.icon('plus', 12) }} action
{% if phase %}{% endif %}
{% endmacro %} {# One card. The column around it is the caller's, because a step holds a set of actions: everything is armed from the first tick, so actions that nothing sequences run alongside each other and are stacked to say so. #} {% macro action_card(action, draft, selected, document) %} {%- set spec = get_action_spec(action.type) -%} {%- set hue = CATEGORY_HUES.get(spec.category, 'var(--accent)') if spec else 'var(--fail)' -%}
{{ ic.icon('bolt' if spec and spec.visual_kind == 'instant' else 'bar', 13) }} {{ action.title or (spec.title if spec else action.type) }}
{% if spec %}{{ spec.title }}{% else %}unknown action{% endif %} {{ action.phase.replace('_', ' ') }} {% if not action.once %}repeats{% endif %} {# Waypoint conditions are edited in the inspector; the card says it has some, since they change when the action ends. #} {% if action.advance_conditions %} {%- set n = action.advance_conditions|length -%} {{ n }} waypoint condition{{ '' if n == 1 else 's' }} {% endif %}
{# No trigger area in the init phase: it runs once and begins immediately, so there is no later moment to wait for, and offering the control would invite writing a condition the phase cannot honour. #} {% if action.takes_trigger %}
{% if action.trigger %} {{ pred.render(action.trigger, draft, selected, document) }} {% else %} fires immediately {% endif %}
{{ pred.add(draft, 'trigger:' ~ action.id) }}
{% endif %}
{% endmacro %} {# The init cell. Every lane carries one -- including the ones that can hold nothing in it -- because a column only means the same thing on every row if every row has it. Init has no steps of its own: the phase runs once, so its cards are a set drawn in one cell rather than a sequence. #} {% macro init_cell(actions, draft, selected, document, actor, addable) %}
{% for action in actions %} {{ action_card(action, draft, selected, document) }} {% endfor %} {% if addable %} {{ add_action(draft, actor, 'init') }} {% elif not actions %}
—
{% endif %}
{% endmacro %} {# A whole step: every card in it, or an empty slot holding the grid open. #} {% macro step_column(slot, draft, selected, document) %}
{% for action in slot %} {{ action_card(action, draft, selected, document) }} {% endfor %}
{% endmacro %} {% macro verdict_lane(kind, label, hint, glyph, conditions, draft, selected, document) %}
{{ ic.icon(glyph, 15) }}{{ label }}
{{ hint }}
{{ init_cell([], draft, selected, document, '', false) }}
{% for node in conditions %}
{{ pred.render(node, draft, selected, document) }}
{% endfor %}
{{ pred.add(draft, kind) }}
{% endmacro %}
Arrangement
One track per actor. The head column is the state at t=0; right of the seam is step order, not elapsed time
{# Two lines that mean different things need saying once, here, rather than being inferred from a dash pattern. #}
{# The head column is not a step -- it is the state every track starts in. Naming it, and marking the seam, is what says where setup ends and the run begins; without that the two read as one continuous strip. #}
Actor
Initialization run{{ ic.icon('right', 11) }}
{% for step in range(document.step_count()) %}
{{ step + 1 }}
{% endfor %}
{% for entity in document.ordered_entities() %} {%- set hue = f.entity_hue(entity.id, document)|trim -%}
{{ ic.icon('user' if entity.kind == 'ego' else 'car', 13) }} {{ entity.display_name }} {{ 'Ego' if entity.kind == 'ego' else entity.id }}
{% if entity.spawn.mode == 'fixed' %} lanelet {{ entity.spawn.lanelet_id }} at {{ entity.spawn.s.value }} m {% elif entity.spawn.deriving %} {{ entity.spawn.binding_title or 'derived' }} at {{ entity.spawn.s.value }} m {% else %} {{ entity.spawn.constraints|length }} constraint{{ '' if entity.spawn.constraints|length == 1 else 's' }} searched {%- if entity.spawn.s.mode == 'derived' %}
offset derived{% endif %} {% endif %} {# Which stack drives the ego changes what the run is, so it reads beside the spawn rather than only in the inspector. Only the ego is driven by anything the document chooses. #} {% if entity.kind == 'ego' and entity.driven_by != 'autopilot' %}
driven by {{ entity.driven_by }} {% endif %} {# The goal reads as part of the same line as the spawn because it is the other end of it: where this vehicle starts, where it is sent. Only the ego can carry one. #} {% if entity.goal %}
goal {{ 'searched' if entity.goal.searching else ('derived' if entity.goal.deriving else entity.goal.lanelet_id) }} at {{ entity.goal.s }} m {% endif %}
{{ init_cell(document.init_actions(entity.id), draft, selected, document, entity.id, true) }}
{% for slot in document.action_slots(entity.id) %} {{ step_column(slot, draft, selected, document) }} {% endfor %} {{ add_action(draft, entity.id) }}
{% endfor %} {# The environment gets a track of its own, under the actors. Traffic lights and the like are performed by no vehicle, so drawing them in a vehicle's lane claimed a relationship the run does not have; and a track that is always here is where you go to add one. #}
{# Selectable like an actor's head, and for the same reason: this column is the state at t=0, and the environment has one -- its junctions -- even though nothing is placed on the map for it. #} {%- set junctions = document.map.traffic_signal_controllers -%} {%- set movements = document.map.signal_groups -%}
{{ ic.icon('globe', 13) }} Environment the world
{% if junctions %}
{{ junctions|length }} junction{{ '' if junctions|length == 1 else 's' }} {{ movements|length }} movement{{ '' if movements|length == 1 else 's' }}
{% else %}
no junctions yet
{% endif %}
{{ init_cell(document.init_actions(none), draft, selected, document, '', true) }}
{% for slot in document.action_slots(none) %} {{ step_column(slot, draft, selected, document) }} {% endfor %} {{ add_action(draft, '') }}
{% if not document.entities %}
No actors yet

Add one to start building the scenario.

{% endif %} {{ verdict_lane('pass', 'PASS', 'the scenario succeeds when any of these holds', 'check', document.assertions.pass_conditions, draft, selected, document) }} {{ verdict_lane('fail', 'FAIL', 'the scenario fails when any of these holds', 'flag', document.assertions.fail_conditions, draft, selected, document) }}