///|
priv enum Expansion {
Series(ReadOnlyArray[Control], Int32)
Sections(ReadOnlyArray[SectionInfo], Int32)
One(Control, Int32)
Combine(Bool, Int32)
}
///|
/// Choose conditional sections and finite loop counts explicitly. Frame labels
/// remain descriptions; the planner never evaluates their text.
pub fn PlaybackPlan::sequence(
catalog : Catalog,
selections~ : ArrayView[Selection] = [],
duration_ms~ : Int64 = 1000L,
limits~ : Limits = Limits::new(),
) -> PlanResult {
let budget = Budget::{ limits, diagnostics: [], work: 0, failed: false, }
fn fail() -> PlanResult {
{ plan: None, diagnostics: ReadOnlyArray::from_array(budget.diagnostics), }
}
if !limits.valid() {
budget.error("animation/plan-limit", "Invalid animation limits")
return fail()
}
if !(catalog.source is Sequence(_)) {
budget.error(
"animation/plan-mismatch", "Sequence choices require a sequence catalog",
)
return fail()
}
let frames = Map([])
for frame in catalog.frames {
frames[frame.key] = frame
}
let choices = Map([])
for selection in selections {
if !budget.charge(selection.frame_key.length() + 1) {
return fail()
}
if choices.contains(selection.frame_key) {
budget.error(
"animation/invalid-selection", "A frame has more than one choice",
)
return fail()
}
guard frames.get(selection.frame_key) is Some(frame) else {
budget.error(
"animation/invalid-selection", "Choice names an unknown frame",
)
return fail()
}
let valid = match (frame.kind, selection.choice) {
(Alt, Alternative(i)) => i >= 0 && i < frame.sections.length()
(Opt, Optional(_)) => true
(Loop, Repetitions(n)) => n >= 0 && n <= limits.steps
_ => false
}
if !valid {
budget.error(
"animation/invalid-selection", "Choice does not match its frame or is out of bounds",
)
return fail()
}
choices[selection.frame_key] = selection.choice
}
let jobs = [Expansion::Series(catalog.controls, 0)]
let values : Array[Motion] = []
let mut leaves = 0
while jobs.pop() is Some(job) {
if !budget.charge(1) {
return fail()
}
match job {
Combine(parallel, count) => {
let reversed = Array::makei(count, _ => values.pop().unwrap())
let children = Array::makei(count, i => reversed[count - 1 - i])
values.push(
if parallel {
Motion::parallel(children)
} else {
Motion::serial(children)
},
)
}
Series(controls, depth) => {
if depth > limits.depth {
budget.error(
"animation/plan-limit", "Selected scenario depth limit exceeded",
)
return fail()
}
if !budget.charge(controls.length()) {
return fail()
}
jobs.push(Combine(false, controls.length()))
for i in 0.. {
if !budget.charge(sections.length()) {
return fail()
}
jobs.push(Combine(false, sections.length()))
for i in 0..
match control {
Message(index) => {
if leaves >= limits.steps {
budget.error(
"animation/plan-limit", "Selected scenario step limit exceeded",
)
return fail()
}
leaves += 1
values.push(
Motion::Step(catalog.occurrences[index].key, duration_ms),
)
}
Frame(index) => {
let frame = catalog.frames[index]
match frame.kind {
Alt =>
match choices.get(frame.key) {
Some(Alternative(section)) =>
jobs.push(
Series(frame.sections[section].controls, depth + 1),
)
_ => {
budget.error(
"animation/selection-required", "Choose one alternative section explicitly",
)
return fail()
}
}
Opt =>
match choices.get(frame.key) {
Some(Optional(true)) =>
jobs.push(Sections(frame.sections, depth + 1))
Some(Optional(false)) => values.push(Motion::serial([]))
_ => {
budget.error(
"animation/selection-required", "Choose whether to include an optional frame explicitly",
)
return fail()
}
}
Loop =>
match choices.get(frame.key) {
Some(Repetitions(count)) => {
if !budget.charge(count) {
return fail()
}
jobs.push(Combine(false, count))
for _ in 0.. {
budget.error(
"animation/selection-required", "Choose a finite loop repetition count explicitly",
)
return fail()
}
}
Par | ParOver => {
if !budget.charge(frame.sections.length()) {
return fail()
}
jobs.push(Combine(true, frame.sections.length()))
for i in 0.. jobs.push(Sections(frame.sections, depth + 1))
}
}
}
}
}
PlaybackPlan::scenario(catalog, values.pop().unwrap(), limits~)
}