@msb235/blits 0.16.0 → 0.16.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/CHANGELOG.md +149 -0
  2. package/README.md +1 -1
  3. package/dist/ahead.js +6 -9
  4. package/dist/clock.d.ts +11 -1
  5. package/dist/clock.js +28 -9
  6. package/dist/contribution.js +11 -4
  7. package/dist/cue.js +12 -3
  8. package/dist/due.d.ts +18 -0
  9. package/dist/due.js +101 -2
  10. package/dist/fade.d.ts +12 -2
  11. package/dist/fade.js +41 -8
  12. package/dist/fill.js +2 -1
  13. package/dist/fit.js +8 -0
  14. package/dist/gone.js +1 -0
  15. package/dist/handle.d.ts +1 -1
  16. package/dist/handle.js +3 -1
  17. package/dist/held.d.ts +2 -1
  18. package/dist/held.js +14 -6
  19. package/dist/hosts.js +5 -1
  20. package/dist/lane.d.ts +2 -0
  21. package/dist/lane.js +2 -0
  22. package/dist/lanes.js +2 -1
  23. package/dist/meet.js +9 -0
  24. package/dist/mixer.d.ts +2 -0
  25. package/dist/mixer.js +2 -0
  26. package/dist/motion.js +6 -6
  27. package/dist/move.js +309 -82
  28. package/dist/origin.d.ts +2 -0
  29. package/dist/origin.js +4 -0
  30. package/dist/owner.js +4 -2
  31. package/dist/paging.d.ts +5 -1
  32. package/dist/paging.js +13 -3
  33. package/dist/patch.js +24 -4
  34. package/dist/place.js +67 -31
  35. package/dist/project.d.ts +5 -2
  36. package/dist/project.js +7 -4
  37. package/dist/qualify.js +6 -1
  38. package/dist/rests.js +9 -1
  39. package/dist/revive.d.ts +11 -1
  40. package/dist/revive.js +7 -0
  41. package/dist/roles.js +5 -2
  42. package/dist/rows.js +4 -1
  43. package/dist/seat.d.ts +5 -1
  44. package/dist/seat.js +5 -1
  45. package/dist/seek.js +6 -4
  46. package/dist/shared.d.ts +1 -1
  47. package/dist/shared.js +1 -1
  48. package/dist/spans.d.ts +10 -4
  49. package/dist/spans.js +121 -31
  50. package/dist/trace.js +7 -2
  51. package/dist/types/transport.d.ts +2 -1
  52. package/dist/voice.d.ts +42 -4
  53. package/dist/voice.js +45 -5
  54. package/dist/weigh.js +1 -1
  55. package/docs/schema.html +1 -1
  56. package/package.json +3 -1
package/dist/project.d.ts CHANGED
@@ -12,8 +12,11 @@ import type { Voice } from './voice.js';
12
12
  export declare function projectAll(transport: Transport, at: number | {
13
13
  readonly frame: number;
14
14
  }): TransportProjection;
15
- /** Sets each copied voice's state to what its clock and fade make it at `t`. */
16
- export declare function stateAt<I, O>(c: Mixer<I, O>, t: number): void;
15
+ /**
16
+ * Sets each copied voice's state to what its clock and fade make it at `t`; with `cut`, the frames
17
+ * up to which history says which had started.
18
+ */
19
+ export declare function stateAt<I, O>(c: Mixer<I, O>, t: number, cut?: Cut): void;
17
20
  /**
18
21
  * A projection back starts each subject from the latest copy kept at or before `t`, else from its
19
22
  * voice's start with fresh state, which is exact for a voice stepped at a fixed interval.
package/dist/project.js CHANGED
@@ -178,13 +178,16 @@ function copyBack(mix, c, t, cut) {
178
178
  ownBlends(c.cued);
179
179
  if (mix.owners !== null)
180
180
  relink(c.cued, cut.seq);
181
- stateAt(c, t);
181
+ stateAt(c, t, cut);
182
182
  recount(c);
183
183
  }
184
- /** Sets each copied voice's state to what its clock and fade make it at `t`. */
185
- export function stateAt(c, t) {
184
+ /**
185
+ * Sets each copied voice's state to what its clock and fade make it at `t`; with `cut`, the frames
186
+ * up to which history says which had started.
187
+ */
188
+ export function stateAt(c, t, cut) {
186
189
  for (const copy of c.cued)
187
- copy.state = copy.stateAt(t);
190
+ copy.state = copy.stateAt(t, cut);
188
191
  }
189
192
  /**
190
193
  * A projection back starts each subject from the latest copy kept at or before `t`, else from its
package/dist/qualify.js CHANGED
@@ -96,7 +96,11 @@ export function requalify(lanes, version) {
96
96
  crowded++;
97
97
  continue;
98
98
  }
99
- const lane = lanes.byId.get(v.id) ?? new Lane(v);
99
+ let lane = lanes.byId.get(v.id);
100
+ if (lane === undefined) {
101
+ lane = new Lane(v);
102
+ lane.made = lanes.probes;
103
+ }
100
104
  v.laned = true;
101
105
  if (lane.epoch === 0 && v.state !== 'pending')
102
106
  opened(lanes, lane);
@@ -395,6 +399,7 @@ function placeAll(lanes, lane) {
395
399
  */
396
400
  function enlane(lanes, v) {
397
401
  const lane = new Lane(v);
402
+ lane.made = lanes.probes;
398
403
  v.laned = true;
399
404
  if (v.state !== 'pending')
400
405
  opened(lanes, lane);
package/dist/rests.js CHANGED
@@ -56,7 +56,15 @@ export function still(voice, now) {
56
56
  if (voice.state === 'frozen' || voice.holding !== null)
57
57
  return true;
58
58
  const motion = voice.motion;
59
- return motion !== undefined && voice.elapsedAt(now) >= voice.latest && motion.landed();
59
+ if (motion === undefined || !(voice.elapsedAt(now) >= voice.latest))
60
+ return false;
61
+ // A subject met inside its stagger and not read since has no run yet, and moves once it is.
62
+ const early = voice.early;
63
+ if (early !== null)
64
+ for (const h of early)
65
+ if (h.since <= now && !(h.probed >= h.since))
66
+ return false;
67
+ return motion.landed();
60
68
  }
61
69
  export function restsLaned() {
62
70
  return this.lanes.whole && this.bounded.length === 0;
package/dist/revive.d.ts CHANGED
@@ -5,7 +5,7 @@ import { type Key } from './paging.js';
5
5
  import type { Run } from './stretch.js';
6
6
  import type { Parted, Parting } from './subject.js';
7
7
  import type { Described } from './types.js';
8
- import { type Controls, type Ramp, type Rise, Voice } from './voice.js';
8
+ import { type Controls, type Earlier, type Ramp, type Rise, Voice } from './voice.js';
9
9
  /** A voice that has left, as plain data: the descriptor it was cued with, its controls and its records. */
10
10
  interface PackedVoice {
11
11
  as: Described;
@@ -21,6 +21,8 @@ interface PackedVoice {
21
21
  weight: number;
22
22
  out: Ramp | null;
23
23
  back: Rise | null;
24
+ /** Absent from a store written before a voice kept them. */
25
+ stalls?: readonly number[] | null;
24
26
  outAt: number;
25
27
  outOver: number;
26
28
  outSet: boolean;
@@ -28,12 +30,20 @@ interface PackedVoice {
28
30
  hold: number;
29
31
  past: number;
30
32
  clocks: Past[] | null;
33
+ /** Absent from a store written before a voice kept it. */
34
+ earlier?: Earlier | null;
35
+ /** Absent from a store written before a voice kept it. */
36
+ later?: Earlier | null;
31
37
  cuedAt: number;
32
38
  cuedSeq: number;
39
+ /** Absent from a store written before a voice kept it. */
40
+ begunSeq?: number;
33
41
  doneAt: number;
34
42
  doneSeq: number;
35
43
  opened: number;
36
44
  pinned: number;
45
+ /** Absent from a store written before a voice kept it. */
46
+ offset?: number;
37
47
  latest: number;
38
48
  /** Absent from a store written before a voice kept it. */
39
49
  soonest?: number;
package/dist/revive.js CHANGED
@@ -79,19 +79,24 @@ function packVoice(voice, keys) {
79
79
  weight: voice.weight,
80
80
  out: voice.out,
81
81
  back: voice.back,
82
+ stalls: voice.stalls,
82
83
  outAt: voice.outAt,
83
84
  outOver: voice.outOver,
84
85
  outSet: voice.outSet,
85
86
  rebuilds: voice.rebuilds,
86
87
  hold: voice.hold,
87
88
  past: voice.clocks?.length ?? 0,
89
+ earlier: voice.earlier,
90
+ later: voice.later,
88
91
  clocks: voice.clocks,
89
92
  cuedAt: voice.cuedAt,
90
93
  cuedSeq: voice.cuedSeq,
94
+ begunSeq: voice.begunSeq,
91
95
  doneAt: voice.doneAt,
92
96
  doneSeq: voice.doneSeq,
93
97
  opened: voice.opened,
94
98
  pinned: voice.pinned,
99
+ offset: voice.offset,
95
100
  latest: voice.latest,
96
101
  soonest: voice.soonest,
97
102
  keeping: voice.keeping,
@@ -127,10 +132,12 @@ export function reviveVoice(mix, id, d) {
127
132
  voice.take(d);
128
133
  voice.cuedAt = d.cuedAt;
129
134
  voice.cuedSeq = d.cuedSeq;
135
+ voice.begunSeq = d.begunSeq ?? Number.POSITIVE_INFINITY;
130
136
  voice.doneAt = d.doneAt;
131
137
  voice.doneSeq = d.doneSeq;
132
138
  voice.opened = d.opened;
133
139
  voice.pinned = d.pinned;
140
+ voice.offset = d.offset ?? 0;
134
141
  voice.latest = d.latest;
135
142
  voice.soonest = d.soonest ?? 0;
136
143
  voice.keeping = d.keeping;
package/dist/roles.js CHANGED
@@ -58,9 +58,12 @@ export function foldRoles(subject, pose, head, laned, dry, except, owed) {
58
58
  this.lanes.paid(voice.id, owed, held.weight);
59
59
  if (!mine)
60
60
  q = this.shared(voice, head.slot, held, q);
61
- if (delta === null)
62
- continue;
63
61
  const role = voice.spec.role;
62
+ if (delta === null) {
63
+ if (role === undefined && !(held.weight > 0))
64
+ unband(held);
65
+ continue;
66
+ }
64
67
  let group = -1;
65
68
  if (role !== undefined) {
66
69
  group = k.names.indexOf(role);
package/dist/rows.js CHANGED
@@ -106,7 +106,7 @@ export function join(lanes, v, from, at, old) {
106
106
  newRow(lanes, c, p, v);
107
107
  if (c.hot[p * c.stride + 6 /* Hot.EPOCH */] === 0 && v.state !== 'pending')
108
108
  c.hot[p * c.stride + 6 /* Hot.EPOCH */] = open(lanes, v);
109
- const seat = new Seat(lanes, c, p);
109
+ const seat = new Seat(lanes, c, p, from === undefined ? lanes.probes : 0);
110
110
  lanes.seats.set(v.id, seat);
111
111
  v.seat = seat;
112
112
  v.laned = true;
@@ -298,5 +298,8 @@ export function reportedRow(lanes, c, p, slot) {
298
298
  return undefined;
299
299
  if (c.list[p] !== slot)
300
300
  return undefined;
301
+ const made = seatOf(lanes, c.voices[p])?.made ?? 0;
302
+ if (!(lanes.per[o + 3 /* Per.LANE_PROBE */] > made))
303
+ return undefined;
301
304
  return c.data[p * 8 /* Row.STRIDE */ + (lanes.per[o + 5 /* Per.LANE_FILL */] === lanes.fills ? 2 /* Row.WEIGHT */ : 3 /* Row.PROBED */)];
302
305
  }
package/dist/seat.d.ts CHANGED
@@ -9,7 +9,11 @@ export declare class Seat<I, O> {
9
9
  lanes: object | null;
10
10
  readonly crowd: Crowd<I, O>;
11
11
  row: number;
12
- constructor(lanes: object | null, crowd: Crowd<I, O>, row: number);
12
+ /** The lanes' probe count when its row was made new, 0 for a row copied from another crowd. */
13
+ readonly made: number;
14
+ constructor(lanes: object | null, crowd: Crowd<I, O>, row: number,
15
+ /** The lanes' probe count when its row was made new, 0 for a row copied from another crowd. */
16
+ made?: number);
13
17
  }
14
18
  /** `voice`'s seat in `lanes`, null where it has none there. */
15
19
  export declare function seatOf<I, O>(lanes: object, voice: Voice<I, O>): Seat<I, O> | null;
package/dist/seat.js CHANGED
@@ -7,10 +7,14 @@ export class Seat {
7
7
  lanes;
8
8
  crowd;
9
9
  row;
10
- constructor(lanes, crowd, row) {
10
+ made;
11
+ constructor(lanes, crowd, row,
12
+ /** The lanes' probe count when its row was made new, 0 for a row copied from another crowd. */
13
+ made = 0) {
11
14
  this.lanes = lanes;
12
15
  this.crowd = crowd;
13
16
  this.row = row;
17
+ this.made = made;
14
18
  }
15
19
  }
16
20
  /** `voice`'s seat in `lanes`, null where it has none there. */
package/dist/seek.js CHANGED
@@ -11,7 +11,6 @@ import { unpull } from './pull.js';
11
11
  import { isRested } from './record.js';
12
12
  import { Restored } from './restored.js';
13
13
  import { recount } from './roster.js';
14
- import { refitAll } from './spans.js';
15
14
  import { Store } from './store.js';
16
15
  import { hostAhead, hostAt, replay } from './tape.js';
17
16
  import { cover, coverFrame, cut } from './unpage.js';
@@ -321,8 +320,12 @@ function back(mix, t, seq, on) {
321
320
  kept.push(voice);
322
321
  for (const voice of kept)
323
322
  restoreVoice(mix, voice, t, seq);
324
- for (const voice of kept)
325
- voice.state = voice.stateAt(t);
323
+ const cut = { seq, strict: false };
324
+ for (const voice of kept) {
325
+ voice.state = voice.stateAt(t, cut);
326
+ if (voice.begunSeq > seq)
327
+ voice.begunSeq = Number.POSITIVE_INFINITY;
328
+ }
326
329
  mix.cued = kept;
327
330
  mix.gone = gone;
328
331
  mix.retired = [];
@@ -388,7 +391,6 @@ function back(mix, t, seq, on) {
388
391
  mix.pins ??= new Map();
389
392
  mix.pins.set(voice, voice.pinned);
390
393
  }
391
- refitAll(mix);
392
394
  mix.due.clear();
393
395
  for (const voice of kept)
394
396
  schedule(mix, voice);
package/dist/shared.d.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  * The package version, for a fact whose shape follows the code too closely to version by hand:
3
3
  * copies of different releases keep apart. `test/copies.test.ts` holds it to `package.json`.
4
4
  */
5
- export declare const release = "0.16.0";
5
+ export declare const release = "0.16.1";
6
6
  /**
7
7
  * A WeakMap every copy of blits loaded in one realm shares, so a fact one copy records about a
8
8
  * patch or channel it made is read by another copy's mix. The name carries a version, bumped when
package/dist/shared.js CHANGED
@@ -2,7 +2,7 @@
2
2
  * The package version, for a fact whose shape follows the code too closely to version by hand:
3
3
  * copies of different releases keep apart. `test/copies.test.ts` holds it to `package.json`.
4
4
  */
5
- export const release = '0.16.0';
5
+ export const release = '0.16.1';
6
6
  /**
7
7
  * A WeakMap every copy of blits loaded in one realm shares, so a fact one copy records about a
8
8
  * patch or channel it made is read by another copy's mix. The name carries a version, bumped when
package/dist/spans.d.ts CHANGED
@@ -15,11 +15,17 @@ export declare class Fitting {
15
15
  }
16
16
  /** Refuses a voice a span cannot fit: one it would not own the timing of, or one with no end. */
17
17
  export declare function checkChild<I, O>(spec: VoiceSpec<I, O>, child: Voice<I, O>): void;
18
+ /**
19
+ * Puts a child whose owner's clock ran back to before a fit changed its rate, or skipped it, back
20
+ * on the clock it ran on before, so it reads, run back over, what it read going forward; and once
21
+ * the owner's clock reaches that change again, takes the change again, as the layout stands.
22
+ */
23
+ export declare function unfit<I, O>(mix: Mixer<I, O>, child: Voice<I, O>, now: number): void;
18
24
  /**
19
25
  * Lays out a span's children and fits them into what is left of its budget, from its clock at mix
20
26
  * time `from`, default now. Only what is still to come moves: a playing child keeps its place and
21
27
  * may change rate, a pending one is placed. A span held by a span is fitted again by its own span
22
- * after.
28
+ * after. A span its clock ran back over keeps the layout it made going forward.
23
29
  */
24
30
  export declare function refit<I, O>(mix: Mixer<I, O>, span: Voice<I, O>, from?: number): void;
25
31
  /**
@@ -31,8 +37,8 @@ export declare function refloor<I, O>(mix: Mixer<I, O>): void;
31
37
  export declare function lingers<I, O>(span: Voice<I, O>, at: number): boolean;
32
38
  /**
33
39
  * Lets the spans out of budget that hold no child still playing leave, at the moment their budget
34
- * ran out.
40
+ * ran out, or, under history, stay quiet with an owner holding them.
35
41
  */
36
42
  export declare function lapse<I, O>(mix: Mixer<I, O>, now: number): void;
37
- /** Fits every span again, as after a seek put its children back. */
38
- export declare function refitAll<I, O>(mix: Mixer<I, O>): void;
43
+ /** Whether a span's budget is out by `now` with no child of it still playing. */
44
+ export declare function lapses<I, O>(span: Voice<I, O>, now: number): boolean;
package/dist/spans.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { arcEnd } from './arcs.js';
2
2
  import { elapsedWith, retime } from './clock.js';
3
- import { retire } from './fade.js';
3
+ import { release } from './fade.js';
4
4
  import { defaultFit, layout, settle } from './fit.js';
5
5
  import { reorigin } from './held.js';
6
6
  import { noted } from './history.js';
@@ -52,40 +52,115 @@ export function checkChild(spec, child) {
52
52
  else if (!Number.isFinite(child.span))
53
53
  throw new Error('blits: a span cannot fit a voice that never ends');
54
54
  }
55
+ /**
56
+ * Whether a span's clock went back over a child at span ms `local`: it holds one kept quiet that
57
+ * ran back to its start, or that starts after `local`, which only a clock gone back leaves quiet.
58
+ * Its layout up to there was made going forward, and stands until the clock passes it again.
59
+ */
60
+ function ranBackOver(span, local) {
61
+ for (const child of span.holding.children)
62
+ if (child.out?.stays === true && (child.out.back === true || child.start > local))
63
+ return true;
64
+ return false;
65
+ }
66
+ /**
67
+ * Puts a child whose owner's clock ran back to before a fit changed its rate, or skipped it, back
68
+ * on the clock it ran on before, so it reads, run back over, what it read going forward; and once
69
+ * the owner's clock reaches that change again, takes the change again, as the layout stands.
70
+ */
71
+ export function unfit(mix, child, now) {
72
+ const t = ownerReading(child.owner, now);
73
+ let moved = false;
74
+ for (let e = child.earlier; e !== null && t < child.anchorNow; e = child.earlier) {
75
+ child.later = { ...standing(child), prev: child.later };
76
+ take(child, e);
77
+ child.earlier = e.prev;
78
+ moved = true;
79
+ }
80
+ for (let e = child.later; e !== null && t >= e.anchorNow; e = child.later) {
81
+ child.earlier = { ...standing(child), prev: child.earlier };
82
+ take(child, e);
83
+ child.later = e.prev;
84
+ moved = true;
85
+ }
86
+ if (!moved)
87
+ return;
88
+ mix.frame = nextFrame();
89
+ reorigin(mix, child, now);
90
+ noted(mix, child);
91
+ mix.lanes?.refill();
92
+ }
93
+ /** A child's clock as it stands, and whether a fit skipped it. */
94
+ function standing(c) {
95
+ const { anchorNow, anchorElapsed, rate, ramp, skipped } = c;
96
+ return { anchorNow, anchorElapsed, rate, ramp, skipped };
97
+ }
98
+ /** Puts a child on a clock it had, which jumps where a fit skipped it on one and not the other. */
99
+ function take(c, e) {
100
+ c.anchorNow = e.anchorNow;
101
+ c.anchorElapsed = e.anchorElapsed;
102
+ c.rate = e.rate;
103
+ c.ramp = e.ramp;
104
+ if (c.skipped !== e.skipped)
105
+ c.seeks++;
106
+ c.skipped = e.skipped;
107
+ }
55
108
  /** A span's own clock at mix time `at`, from 0 while it has not started. */
56
109
  function clockOf(span, at) {
57
110
  if (Number.isNaN(at))
58
111
  return 0;
59
- const t = ownerReading(span, at);
112
+ const t = readingAt(span, at);
60
113
  return Number.isFinite(t) ? Math.max(0, t) : 0;
61
114
  }
115
+ /**
116
+ * What a voice's clock reads at mix time `at` through its owners' clocks as they stand, inverting
117
+ * `mixTime`: -Infinity where one yet to start has not reached its start. Unlike `ownerReading`, an
118
+ * owner that has started reads on its clock before its start, where a seek of the owner put a time
119
+ * its children left at.
120
+ */
121
+ function readingAt(voice, at) {
122
+ const t = voice.owner === null ? at : readingAt(voice.owner, at);
123
+ if (t === Number.NEGATIVE_INFINITY || (voice.state === 'pending' && !(t >= voice.start)))
124
+ return Number.NEGATIVE_INFINITY;
125
+ return elapsedWith(voice, t);
126
+ }
127
+ /**
128
+ * Whether a child has started, or would have by span ms `by` had a frame come between: a fit from a
129
+ * moment between frames keeps a child whose start that moment had passed where it is.
130
+ */
131
+ function begun(child, by) {
132
+ return child.state !== 'pending' || child.start < by;
133
+ }
62
134
  /** What a child's own clock reads at its span's `local`, 0 while it waits. */
63
- function elapsedIn(child, local) {
64
- return child.state === 'pending' ? 0 : Math.max(0, elapsedWith(child, local));
135
+ function elapsedIn(child, local, by) {
136
+ return begun(child, by) ? Math.max(0, elapsedWith(child, local)) : 0;
65
137
  }
66
138
  /** How much of a child is left, voice ms at its own clock: what its span still has to fit. */
67
- function leftOf(child, local) {
139
+ function leftOf(child, local, by) {
68
140
  const length = child.fitting !== null ? child.fitting.end : child.arc !== null ? arcEnd(child) : child.span;
69
- return Math.max(0, length - elapsedIn(child, local));
141
+ return Math.max(0, length - elapsedIn(child, local, by));
70
142
  }
71
143
  /**
72
144
  * Lays out a span's children and fits them into what is left of its budget, from its clock at mix
73
145
  * time `from`, default now. Only what is still to come moves: a playing child keeps its place and
74
146
  * may change rate, a pending one is placed. A span held by a span is fitted again by its own span
75
- * after.
147
+ * after. A span its clock ran back over keeps the layout it made going forward.
76
148
  */
77
149
  export function refit(mix, span, from = mix.now) {
78
150
  const fitting = span.fitting;
79
151
  if (mix.projecting || span.state === 'done')
80
152
  return;
81
153
  const local = clockOf(span, from);
82
- const { kids, floors, waiting } = lineUp(mix, span, local);
154
+ if (ranBackOver(span, local))
155
+ return;
156
+ const by = from < mix.now ? local : Number.NEGATIVE_INFINITY;
157
+ const { kids, floors, waiting } = lineUp(mix, span, local, by);
83
158
  const claims = kids.map((c, i) => {
84
159
  const s = c.spec;
85
160
  return {
86
161
  floor: floors[i],
87
- natural: leftOf(c, local) / (s.rate ?? 1),
88
- state: c.state === 'pending' ? 'pending' : 'playing',
162
+ natural: leftOf(c, local, by) / (s.rate ?? 1),
163
+ state: begun(c, by) ? 'playing' : 'pending',
89
164
  faster: Math.max(1, s.faster ?? 1),
90
165
  slower: Math.max(1, s.slower ?? 1),
91
166
  overlap: s.overlap === true,
@@ -99,9 +174,10 @@ export function refit(mix, span, from = mix.now) {
99
174
  kids.forEach((c, i) => {
100
175
  const rate = (c.spec.rate ?? 1) * plan.rate[i];
101
176
  const skipped = plan.skip[i] && c.holding === null;
177
+ const wasSkipped = c.skipped;
102
178
  c.skipped = skipped;
103
179
  let moved = false;
104
- if (c.state === 'pending') {
180
+ if (!begun(c, by)) {
105
181
  const start = local + at[i];
106
182
  const elapsed = skipped ? c.span : 0;
107
183
  if (c.start !== start || c.rate !== rate || c.anchorElapsed !== elapsed || c.ramp !== null) {
@@ -111,10 +187,16 @@ export function refit(mix, span, from = mix.now) {
111
187
  c.rate = rate;
112
188
  c.ramp = null;
113
189
  c.clocks = null;
190
+ c.earlier = null;
191
+ c.later = null;
114
192
  moved = true;
115
193
  }
116
194
  }
117
195
  else {
196
+ if (skipped || c.rate !== rate || c.ramp !== null) {
197
+ c.earlier = { ...standing(c), skipped: wasSkipped, prev: c.earlier };
198
+ c.later = null;
199
+ }
118
200
  if (skipped) {
119
201
  leave(c, local);
120
202
  c.rebase(local);
@@ -141,6 +223,8 @@ export function refit(mix, span, from = mix.now) {
141
223
  c.anchorNow = Number.POSITIVE_INFINITY;
142
224
  c.anchorElapsed = 0;
143
225
  c.clocks = null;
226
+ c.earlier = null;
227
+ c.later = null;
144
228
  mix.frame = nextFrame();
145
229
  reorigin(mix, c, mix.now);
146
230
  noted(mix, c);
@@ -151,19 +235,26 @@ export function refit(mix, span, from = mix.now) {
151
235
  const over = Number.isFinite(fitting.budget) ? Math.max(0, reach - fitting.budget) : 0;
152
236
  const within = over < 1e-6;
153
237
  const end = !Number.isFinite(fitting.budget) ? reach : within ? fitting.budget : reach;
154
- fitting.report = {
238
+ const was = fitting.report;
239
+ const report = {
155
240
  budget: fitting.budget,
156
241
  length: reach,
157
242
  over: within ? 0 : over,
158
243
  skipped: plan.skip.filter((s, i) => s && kids[i]?.holding === null).length,
159
244
  fell,
160
245
  };
246
+ fitting.report = report;
161
247
  if (end !== fitting.end) {
162
248
  fitting.end = end;
163
249
  noted(mix, span);
164
250
  if (span.owner?.fitting)
165
251
  refit(mix, span.owner, from);
166
252
  }
253
+ else if (report.length !== was.length ||
254
+ report.over !== was.over ||
255
+ report.skipped !== was.skipped ||
256
+ report.fell !== was.fell)
257
+ noted(mix, span);
167
258
  }
168
259
  /**
169
260
  * A span's children still to fit, in the order it lays them out: those playing first, as cued, then
@@ -171,13 +262,14 @@ export function refit(mix, span, from = mix.now) {
171
262
  * from `local`, and before `local` only in the frame it was cued. One whose anchor has no answer yet waits unplaced, and under `queue` or `stagger`
172
263
  * so does every child after it, since each is laid out from the one before.
173
264
  */
174
- function lineUp(mix, span, local) {
175
- const live = (span.holding?.children ?? []).filter((c) => c.state !== 'done' && c.state !== 'frozen' && c.state !== 'fading' && leftOf(c, local) > 0);
176
- const kids = live.filter((c) => c.state !== 'pending');
265
+ function lineUp(mix, span, local, by) {
266
+ // A child fading, or kept quiet past its passes, has nothing left to fit.
267
+ const live = (span.holding?.children ?? []).filter((c) => c.state !== 'done' && c.state !== 'frozen' && c.out === null && leftOf(c, local, by) > 0);
268
+ const kids = live.filter((c) => begun(c, by));
177
269
  const floors = kids.map(() => undefined);
178
270
  const waiting = [];
179
271
  const pending = live
180
- .filter((c) => c.state === 'pending')
272
+ .filter((c) => !begun(c, by))
181
273
  .sort((a, b) => (a.spec.rank ?? 0) - (b.spec.rank ?? 0));
182
274
  const chained = span.fitting.claim.order !== 'together';
183
275
  for (const c of pending) {
@@ -224,23 +316,21 @@ export function lingers(span, at) {
224
316
  }
225
317
  /**
226
318
  * Lets the spans out of budget that hold no child still playing leave, at the moment their budget
227
- * ran out.
319
+ * ran out, or, under history, stay quiet with an owner holding them.
228
320
  */
229
321
  export function lapse(mix, now) {
230
- for (const span of mix.owners ?? []) {
231
- const f = span.fitting;
232
- if (f === null || span.state === 'done' || !Number.isFinite(f.budget))
233
- continue;
234
- if (plays(span, now))
235
- continue;
236
- if (ownerReading(span, now) >= f.budget)
237
- retire(mix, span, span.timeAt(f.budget));
322
+ const owners = mix.owners ?? [];
323
+ // Those held first, cued after what holds them, so a span whose last span lapses goes with it.
324
+ for (let i = owners.length - 1; i >= 0; i--) {
325
+ const span = owners[i];
326
+ if (span.out?.stays !== true && lapses(span, now))
327
+ release(mix, span, span.timeAt(span.fitting.budget));
238
328
  }
239
329
  }
240
- /** Fits every span again, as after a seek put its children back. */
241
- export function refitAll(mix) {
242
- const spans = (mix.owners ?? []).filter((v) => v.fitting !== null);
243
- // Later cued first, so a span held by a span is fitted before the one holding it.
244
- for (let i = spans.length - 1; i >= 0; i--)
245
- refit(mix, spans[i]);
330
+ /** Whether a span's budget is out by `now` with no child of it still playing. */
331
+ export function lapses(span, now) {
332
+ const f = span.fitting;
333
+ if (f === null || span.state === 'done' || !Number.isFinite(f.budget))
334
+ return false;
335
+ return !plays(span, now) && ownerReading(span, now) >= f.budget;
246
336
  }
package/dist/trace.js CHANGED
@@ -151,6 +151,8 @@ function shapeOf(mix, voice, subject, from, to, plan) {
151
151
  return false;
152
152
  if (voice.ramp !== null || !(voice.rate > 0) || voice.out !== null || voice.back !== null)
153
153
  return false;
154
+ if (voice.stalls !== null)
155
+ return false;
154
156
  if (voice.parts !== null || voice.parted !== null || voice.left !== null)
155
157
  return false;
156
158
  if (Number.isFinite(voice.outAt) || voice.state === 'fading')
@@ -190,9 +192,12 @@ function shapeOf(mix, voice, subject, from, to, plan) {
190
192
  at(ends);
191
193
  if (!voice.freezesAfter && Number.isFinite(ends)) {
192
194
  const over = reduced ? 0 : Number.isNaN(voice.outOver) ? (voice.fade.out ?? 0) : voice.outOver;
195
+ // Ended before its start, as a seek past its end leaves it, it fades from its start.
196
+ const fades = Math.max(voice.start, ends);
197
+ at(fades);
193
198
  if (over > 0) {
194
- bent.push([ends, ends + over]);
195
- at(ends + over);
199
+ bent.push([fades, fades + over]);
200
+ at(fades + over);
196
201
  }
197
202
  // It leaves when its latest subject ends, which a subject first met later can move.
198
203
  if (spec.stagger !== undefined)
@@ -45,7 +45,8 @@ export interface Transport {
45
45
  /**
46
46
  * Loads from the history `store` what a seek or projection to `time` will need. That is, what
47
47
  * `seek(time)` and `project(time)` need where it reaches past memory. Without a store, or within
48
- * memory, it loads nothing.
48
+ * memory, it loads nothing. What it loads is kept until a seek or read takes it, or until
49
+ * `history.ms` of mix time passes, after which the time needs `prepare` again.
49
50
  */
50
51
  prepare(time: number): Promise<void>;
51
52
  readonly now: number;