export const abortError = () => new DOMException("Playback stopped", "AbortError"); // Promises advance only when physics advances. No wall-clock callbacks are // used to guess when a robot, posture transition, or recorded motion finishes. export function createSimulationTimeline() { const jobs = new Set(); function run({ duration, update = () => {}, until, maxDuration = Infinity, signal }) { if (signal?.aborted) return Promise.reject(abortError()); return new Promise((resolve, reject) => { const job = { elapsed: 0, duration, update, until, maxDuration }; const finish = (error) => { jobs.delete(job); signal?.removeEventListener("abort", abort); error ? reject(error) : resolve(); }; const abort = () => finish(abortError()); job.finish = finish; signal?.addEventListener("abort", abort, { once: true }); jobs.add(job); }); } return { run, wait: (duration, signal) => run({ duration, signal }), until: (until, maxDuration, signal) => run({ until, maxDuration, signal }), tick(dt) { for (const job of [...jobs]) { try { job.elapsed += dt; const t = job.duration === undefined ? job.elapsed : Math.min(job.elapsed, job.duration); job.update(t); if (job.until ? job.until(job.elapsed) : job.elapsed + 1e-9 >= job.duration) job.finish(); else if (job.elapsed >= job.maxDuration) job.finish(new Error("Robot did not finish its movement")); } catch (e) { job.finish(e); } } }, cancel() { for (const job of [...jobs]) job.finish(abortError()); }, }; }