@msb235/blits 0.7.1 → 0.8.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 (156) hide show
  1. package/CHANGELOG.md +193 -0
  2. package/README.md +22 -4
  3. package/dist/book.js +4 -4
  4. package/dist/chain.d.ts +1 -1
  5. package/dist/chain.js +57 -54
  6. package/dist/channels.d.ts +26 -5
  7. package/dist/channels.js +12 -8
  8. package/dist/clock.d.ts +8 -1
  9. package/dist/clock.js +14 -3
  10. package/dist/clone.d.ts +8 -0
  11. package/dist/clone.js +22 -0
  12. package/dist/closed.d.ts +1 -1
  13. package/dist/closed.js +3 -2
  14. package/dist/color.d.ts +0 -11
  15. package/dist/color.js +4 -24
  16. package/dist/columns.js +0 -1
  17. package/dist/copyout.d.ts +6 -0
  18. package/dist/copyout.js +38 -0
  19. package/dist/crowd.d.ts +7 -5
  20. package/dist/crowd.js +32 -18
  21. package/dist/cue.d.ts +3 -1
  22. package/dist/cue.js +31 -20
  23. package/dist/departed.d.ts +34 -0
  24. package/dist/departed.js +73 -0
  25. package/dist/easing.d.ts +5 -0
  26. package/dist/easing.js +31 -2
  27. package/dist/everyone.d.ts +56 -0
  28. package/dist/everyone.js +221 -0
  29. package/dist/fade.d.ts +8 -1
  30. package/dist/fade.js +34 -6
  31. package/dist/fill.d.ts +22 -13
  32. package/dist/fill.js +104 -41
  33. package/dist/fit.js +2 -1
  34. package/dist/fold.d.ts +37 -0
  35. package/dist/fold.js +216 -31
  36. package/dist/frames.d.ts +21 -0
  37. package/dist/frames.js +59 -0
  38. package/dist/gather.d.ts +15 -9
  39. package/dist/gather.js +60 -25
  40. package/dist/gone.js +10 -0
  41. package/dist/handle.d.ts +16 -5
  42. package/dist/handle.js +56 -6
  43. package/dist/held.d.ts +8 -5
  44. package/dist/held.js +56 -58
  45. package/dist/history.d.ts +17 -13
  46. package/dist/history.js +47 -28
  47. package/dist/hosts.js +55 -8
  48. package/dist/index.d.ts +9 -4
  49. package/dist/index.js +5 -2
  50. package/dist/keyfill.d.ts +7 -0
  51. package/dist/keyfill.js +124 -0
  52. package/dist/keyrows.d.ts +1 -0
  53. package/dist/keyrows.js +1 -1
  54. package/dist/lane.d.ts +32 -2
  55. package/dist/lane.js +5 -0
  56. package/dist/lanes.d.ts +32 -8
  57. package/dist/lanes.js +69 -37
  58. package/dist/leavings.d.ts +26 -0
  59. package/dist/leavings.js +70 -0
  60. package/dist/locus.js +2 -2
  61. package/dist/meet.js +14 -8
  62. package/dist/mixer.d.ts +39 -7
  63. package/dist/mixer.js +50 -10
  64. package/dist/motion.d.ts +2 -0
  65. package/dist/motion.js +4 -1
  66. package/dist/motions.d.ts +22 -15
  67. package/dist/motions.js +40 -34
  68. package/dist/move.js +23 -16
  69. package/dist/moved.d.ts +11 -0
  70. package/dist/moved.js +29 -0
  71. package/dist/origin.d.ts +19 -0
  72. package/dist/origin.js +68 -0
  73. package/dist/owed.d.ts +4 -4
  74. package/dist/owed.js +8 -8
  75. package/dist/owner.d.ts +1 -1
  76. package/dist/owner.js +2 -2
  77. package/dist/pace.d.ts +13 -5
  78. package/dist/pace.js +21 -8
  79. package/dist/pack.d.ts +3 -1
  80. package/dist/pack.js +15 -24
  81. package/dist/paging.d.ts +8 -2
  82. package/dist/paging.js +39 -4
  83. package/dist/patch.d.ts +14 -9
  84. package/dist/patch.js +83 -21
  85. package/dist/place.js +18 -3
  86. package/dist/project.d.ts +12 -1
  87. package/dist/project.js +101 -83
  88. package/dist/pull.d.ts +2 -0
  89. package/dist/pull.js +8 -0
  90. package/dist/qualify.js +10 -2
  91. package/dist/reading.d.ts +5 -0
  92. package/dist/reading.js +5 -0
  93. package/dist/record.d.ts +9 -0
  94. package/dist/record.js +42 -0
  95. package/dist/relink.d.ts +11 -5
  96. package/dist/relink.js +7 -5
  97. package/dist/restored.d.ts +41 -0
  98. package/dist/restored.js +91 -0
  99. package/dist/revive.d.ts +10 -3
  100. package/dist/revive.js +31 -23
  101. package/dist/roster.d.ts +11 -0
  102. package/dist/roster.js +46 -0
  103. package/dist/rotation.d.ts +29 -0
  104. package/dist/rotation.js +91 -0
  105. package/dist/rows.d.ts +5 -5
  106. package/dist/rows.js +5 -5
  107. package/dist/sample.d.ts +3 -2
  108. package/dist/sample.js +32 -15
  109. package/dist/seek.js +97 -114
  110. package/dist/shared.d.ts +6 -0
  111. package/dist/shared.js +15 -0
  112. package/dist/signals.d.ts +24 -3
  113. package/dist/signals.js +50 -11
  114. package/dist/spans.js +7 -0
  115. package/dist/stack.d.ts +11 -0
  116. package/dist/stack.js +47 -0
  117. package/dist/standing.d.ts +10 -0
  118. package/dist/standing.js +79 -0
  119. package/dist/store.d.ts +3 -3
  120. package/dist/store.js +3 -3
  121. package/dist/strays.js +3 -0
  122. package/dist/tape.d.ts +8 -5
  123. package/dist/tape.js +21 -13
  124. package/dist/testing.d.ts +35 -0
  125. package/dist/testing.js +116 -0
  126. package/dist/touch.d.ts +6 -0
  127. package/dist/touch.js +28 -0
  128. package/dist/transport.d.ts +45 -6
  129. package/dist/transport.js +64 -19
  130. package/dist/turns.d.ts +18 -0
  131. package/dist/turns.js +72 -0
  132. package/dist/tweened.js +1 -1
  133. package/dist/types/channel.d.ts +46 -0
  134. package/dist/types/channel.js +1 -0
  135. package/dist/types/history.d.ts +139 -0
  136. package/dist/types/history.js +1 -0
  137. package/dist/types/mix.d.ts +336 -0
  138. package/dist/types/mix.js +1 -0
  139. package/dist/types/patch.d.ts +187 -0
  140. package/dist/types/patch.js +1 -0
  141. package/dist/types/place.d.ts +157 -0
  142. package/dist/types/place.js +1 -0
  143. package/dist/types/transport.d.ts +71 -0
  144. package/dist/types/transport.js +1 -0
  145. package/dist/types/voice.d.ts +323 -0
  146. package/dist/types/voice.js +1 -0
  147. package/dist/types.d.ts +7 -1162
  148. package/dist/unpage.d.ts +2 -2
  149. package/dist/unpage.js +53 -25
  150. package/dist/voice.d.ts +90 -22
  151. package/dist/voice.js +105 -43
  152. package/dist/wave.d.ts +42 -0
  153. package/dist/wave.js +62 -0
  154. package/dist/weigh.js +1 -1
  155. package/docs/schema.html +42 -36
  156. package/package.json +8 -3
package/dist/patch.js CHANGED
@@ -1,5 +1,5 @@
1
- import { lerpInto } from './channels.js';
2
- import { curve } from './easing.js';
1
+ import { lerpInto, lerpNumber } from './channels.js';
2
+ import { curve, startSlope } from './easing.js';
3
3
  /**
4
4
  * The procedural form. Flicker, roving, a hash walk, a starter strike.
5
5
  *
@@ -12,7 +12,6 @@ export function patch(duration, at, opts) {
12
12
  return {
13
13
  form: 'fn',
14
14
  duration,
15
- period: duration,
16
15
  writes,
17
16
  kit: opts.kit,
18
17
  reads: opts.reads,
@@ -23,11 +22,6 @@ export function patch(duration, at, opts) {
23
22
  unpack: opts.unpack,
24
23
  };
25
24
  }
26
- const options = new WeakMap();
27
- /** The options a `keys` patch was built with, for an engine reading its stops rather than calling it. */
28
- export function keysOptionsOf(p) {
29
- return options.get(p);
30
- }
31
25
  const linear = (a, b, u) => a + (b - a) * u;
32
26
  function interpolate(a, b, u) {
33
27
  if (typeof a === 'number' && typeof b === 'number')
@@ -64,6 +58,10 @@ function build(stops, writes, duration, opts) {
64
58
  }
65
59
  all.sort((x, y) => x.at - y.at);
66
60
  const tail = all.filter((pt) => pt.at !== 0);
61
+ const lerp = opts.lerpBy?.(channel) ?? opts.kit?.[channel]?.lerp;
62
+ const plain = all.length > 0 &&
63
+ (lerp === undefined || lerp === lerpNumber) &&
64
+ all.every((pt) => typeof pt.value === 'number');
67
65
  tracks.push({
68
66
  channel: channel,
69
67
  all,
@@ -71,7 +69,14 @@ function build(stops, writes, duration, opts) {
71
69
  ats: all.map((pt) => pt.at),
72
70
  tailAts: null,
73
71
  delay: opts.delayBy?.(channel) ?? 0,
74
- lerp: opts.lerpBy?.(channel) ?? opts.kit?.[channel]?.lerp,
72
+ lerp,
73
+ numbers: plain
74
+ ? {
75
+ ats: Float64Array.from(all, (pt) => pt.at),
76
+ values: Float64Array.from(all, (pt) => pt.value),
77
+ eases: all.map((pt) => pt.ease),
78
+ }
79
+ : null,
75
80
  });
76
81
  }
77
82
  return { tracks, duration };
@@ -100,12 +105,17 @@ export function shifted(track, phase, duration) {
100
105
  }
101
106
  /** A phase once a channel's delay of `delay` ms in a duration of `duration` applies. */
102
107
  export function shift(delay, phase, duration) {
103
- return delay === 0 || duration === 0 ? phase : Math.max(0, (phase * duration - delay) / duration);
108
+ if (delay === 0 || duration === 0)
109
+ return phase;
110
+ // The channel travels in what is left after its wait, so it lands with the rest at phase 1.
111
+ if (delay >= duration)
112
+ return phase >= 1 ? 1 : 0;
113
+ return Math.max(0, (phase * duration - delay) / (duration - delay));
104
114
  }
105
115
  /**
106
116
  * Where `phase` falls among `n` stop phases `ats[off + k * stride]`, in order: -2 for no stops, -1
107
117
  * at or before the first, `n` at or past the last, and otherwise the first stop past it, where the
108
- * segment from the one before ends. With `segment`'s, the one copy of the search.
118
+ * segment from the one before ends. `segment` and `readNumber` make the same search.
109
119
  */
110
120
  export function locate(ats, off, stride, n, phase) {
111
121
  if (n === 0)
@@ -132,8 +142,8 @@ export function fraction(phase, a, b) {
132
142
  return (phase - a) / (b - a);
133
143
  }
134
144
  /**
135
- * Finds where one track is at a phase, into `seg`: the one copy of the search, which `read` and a
136
- * lane folding stops straight into a subject's values both make. `base` stands in for the value at
145
+ * Finds where one track is at a phase, into `seg`: the search `read` and a lane folding stops
146
+ * straight into a subject's values both make. `base` stands in for the value at
137
147
  * phase 0, which is how `from: 'current'` starts a voice wherever the subject already is.
138
148
  */
139
149
  export function segment(track, phase, base) {
@@ -179,10 +189,44 @@ export function segment(track, phase, base) {
179
189
  seg.ease = b.ease;
180
190
  return BETWEEN;
181
191
  }
192
+ /**
193
+ * A plain-number track at a phase, in exactly `segment`'s and `read`'s arithmetic: the stop at or
194
+ * past the phase by binary search, the fraction before easing, then a straight lerp.
195
+ */
196
+ function readNumber(t, phase) {
197
+ const ats = t.ats;
198
+ const values = t.values;
199
+ const n = ats.length;
200
+ if (phase <= ats[0])
201
+ return values[0];
202
+ if (phase >= ats[n - 1])
203
+ return values[n - 1];
204
+ let lo = 1;
205
+ let hi = n - 1;
206
+ while (lo < hi) {
207
+ const mid = (lo + hi) >> 1;
208
+ if (ats[mid] >= phase)
209
+ hi = mid;
210
+ else
211
+ lo = mid + 1;
212
+ }
213
+ const a = ats[lo - 1];
214
+ const u = (phase - a) / (ats[lo] - a);
215
+ const ease = t.eases[lo];
216
+ const eased = ease ? ease(u) : u;
217
+ const from = values[lo - 1];
218
+ return from + (values[lo] - from) * eased;
219
+ }
182
220
  /** Reads one track at a phase; `base` as `segment` takes it. */
183
221
  function read(track, phase, base, lerp,
184
222
  /** Units per ms, for a retarget's first segment. */
185
223
  slope, duration, into, reuse) {
224
+ const numbers = track.numbers;
225
+ if (numbers !== null &&
226
+ base === undefined &&
227
+ slope === undefined &&
228
+ (lerp === undefined || lerp === lerpNumber || track.lerp !== undefined))
229
+ return readNumber(numbers, phase);
186
230
  const found = segment(track, phase, base);
187
231
  if (found === NOTHING)
188
232
  return undefined;
@@ -208,7 +252,9 @@ slope, duration, into, reuse) {
208
252
  // add c·u(1−u)², which is 0 at both ends, flat at the end, and fixes the slope at the start.
209
253
  if (slope === undefined || !first)
210
254
  return value;
211
- const e0 = ease ? ease(1e-6) / 1e-6 : 1;
255
+ const e0 = ease ? startSlope(ease) : 1;
256
+ if (e0 === Number.POSITIVE_INFINITY)
257
+ return value;
212
258
  const bend = (s, a, z) => (s * duration * len - (z - a) * e0) * u * (1 - u) * (1 - u);
213
259
  if (typeof value === 'number' && typeof slope === 'number')
214
260
  return value + bend(slope, aValue, bValue);
@@ -262,14 +308,15 @@ slopes, intos, scratch) {
262
308
  }
263
309
  return out;
264
310
  }
265
- /** A patch's `duration`, or the deprecated `period` where only that is set. */
266
- export const durationOf = (p) => p.duration ?? p.period ?? 0;
267
311
  const cache = new WeakMap();
268
- /** The built form of a patch's stops, made on first ask for a patch `keys()` did not build. */
312
+ /**
313
+ * The built form of a patch's stops, made on first ask for a patch `keys()` did not build: a copy
314
+ * of one, or one another copy of blits made, each read by the fields it carries.
315
+ */
269
316
  export function builtOf(p) {
270
317
  let held = cache.get(p);
271
318
  if (held === undefined) {
272
- held = build(p.keys, p.writes, durationOf(p), keysOptionsOf(p) ?? {});
319
+ held = build(p.keys, p.writes, p.duration, p);
273
320
  cache.set(p, held);
274
321
  }
275
322
  return held;
@@ -289,13 +336,28 @@ export function keys(duration, stops, opts = {}) {
289
336
  const p = {
290
337
  form: 'keys',
291
338
  duration,
292
- period: duration,
293
339
  writes,
294
340
  kit: opts.kit,
295
341
  keys: stops,
296
- at: (phase) => readKeys(made, phase, {}),
342
+ ...given(opts),
343
+ // A copy of the patch carries this `at` with fields of its own, which it is read by.
344
+ at(phase) {
345
+ return readKeys(this === p ? made : builtOf(this), phase, {});
346
+ },
297
347
  };
298
- options.set(p, opts);
299
348
  cache.set(p, made);
300
349
  return p;
301
350
  }
351
+ /** The `keys` options a patch carries as fields, those given only: an absent one adds no key. */
352
+ function given(opts) {
353
+ const out = {};
354
+ if (opts.ease !== undefined)
355
+ out.ease = opts.ease;
356
+ if (opts.easeBy !== undefined)
357
+ out.easeBy = opts.easeBy;
358
+ if (opts.delayBy !== undefined)
359
+ out.delayBy = opts.delayBy;
360
+ if (opts.lerpBy !== undefined)
361
+ out.lerpBy = opts.lerpBy;
362
+ return out;
363
+ }
package/dist/place.js CHANGED
@@ -1,3 +1,5 @@
1
+ import { departedMatches } from './departed.js';
2
+ import { reorigin } from './held.js';
1
3
  import { noted } from './history.js';
2
4
  import { markOf } from './marks.js';
3
5
  import { mixTime, ownerReading } from './owner.js';
@@ -37,6 +39,10 @@ function startAt(mix, voice, start) {
37
39
  voice.start = start;
38
40
  voice.anchorNow = start;
39
41
  voice.anchorElapsed = 0;
42
+ voice.clocks = null;
43
+ // A clock moved outright, as a seek moves it: what a record computed at this mix time is stale.
44
+ voice.seeks++;
45
+ reorigin(mix, voice, mix.now);
40
46
  noted(mix, voice);
41
47
  return true;
42
48
  }
@@ -266,7 +272,7 @@ class Pick {
266
272
  /**
267
273
  * The mix time a query answers: `mark` of the voice it picks, among those cued and those that have
268
274
  * left, never the asker; or a mark the host announced on the score, which is the same time for
269
- * any of the four. Undefined while what it picks has no such time. A query naming no score looks
275
+ * any mark. Undefined while what it picks has no such time. A query naming no score looks
270
276
  * among the asker's siblings, the voices its owner holds.
271
277
  */
272
278
  function timeOf(mix, key, mark, self) {
@@ -290,8 +296,17 @@ function timeOf(mix, key, mark, self) {
290
296
  : listed
291
297
  ? holders(mix, score)
292
298
  : mix.transport.members;
299
+ // Voices forgotten on leaving answer only where no voice the mix still knows does.
300
+ const owner = self.owner?.id ?? -1;
301
+ const forgotten = new Pick(resolver, now);
293
302
  for (let i = 0; i < mixes.length; i++) {
294
303
  const m = mixes[i];
304
+ let departed = false;
305
+ for (const d of m.departed.all)
306
+ if (departedMatches(d, q, score, anywhere, owner)) {
307
+ forgotten.take(d.id, d.marks[mark]);
308
+ departed = true;
309
+ }
295
310
  if (score === undefined || m.projecting || mix.projecting) {
296
311
  for (const v of [...m.gone, ...m.cued])
297
312
  if (matches(v))
@@ -300,7 +315,7 @@ function timeOf(mix, key, mark, self) {
300
315
  }
301
316
  const cued = scored(m, score);
302
317
  const list = scoredGone(m, score);
303
- if (cued.size === 0 && list.length === 0) {
318
+ if (cued.size === 0 && list.length === 0 && !departed) {
304
319
  if (listed) {
305
320
  unhold(m, score);
306
321
  i--;
@@ -359,7 +374,7 @@ function timeOf(mix, key, mark, self) {
359
374
  (q.name === undefined || a.name === q.name) &&
360
375
  (q.tag === undefined || a.tags.includes(q.tag)))
361
376
  picked.take(a.order, Number.isNaN(a.at) ? undefined : mixAt(mix, a.at));
362
- return picked.answer();
377
+ return (picked.any ? picked : forgotten).answer();
363
378
  }
364
379
  /** `self`'s read of `m`'s gone voices for one query, started over where the list has changed under it. */
365
380
  function foldOf(m, list, key, mark, self, resolver) {
package/dist/project.d.ts CHANGED
@@ -1,7 +1,18 @@
1
- import { Transport } from './transport.js';
1
+ import { Mixer } from './mixer.js';
2
+ import { type Cut, Transport } from './transport.js';
2
3
  import type { TransportProjection } from './types.js';
4
+ import type { Subject, Voice } from './voice.js';
3
5
  /**
4
6
  * Every mix on a transport read at mix time `t`, through copies of them on a transport of their
5
7
  * own, moved together so an anchor across mixes answers as it would live.
6
8
  */
7
9
  export declare function projectAll(transport: Transport, t: number): TransportProjection;
10
+ /** Sets each copied voice's state to what its clock and fade make it at `t`. */
11
+ export declare function stateAt<I, O>(c: Mixer<I, O>, t: number): void;
12
+ /** Gives a projection's copy what it reads of the live mix as it is, before its voices are copied. */
13
+ export declare function mirror<I, O>(mix: Mixer<I, O>, c: Mixer<I, O>): void;
14
+ /**
15
+ * A projection back starts each subject from the latest copy kept at or before `t`, else from its
16
+ * voice's start with fresh state, which is exact for a voice stepped at a fixed interval.
17
+ */
18
+ export declare function recall<I, O>(mix: Mixer<I, O>, voice: Voice<I, O>, subject: I, t: number, cut: Cut): Subject<unknown> | undefined;
package/dist/project.js CHANGED
@@ -1,14 +1,16 @@
1
1
  import { ownBlends } from './blend.js';
2
- import { index } from './chain.js';
3
- import { copyHeld, last, leftAt } from './history.js';
2
+ import { open, sighted, weightAt } from './everyone.js';
3
+ import { copyHeld, last, lastWithin } from './history.js';
4
4
  import { Mixer } from './mixer.js';
5
5
  import { move } from './move.js';
6
6
  import { heldByInput, ownerReading, relink } from './owner.js';
7
7
  import { pin } from './place.js';
8
8
  import { reading } from './reading.js';
9
- import { Store } from './store.js';
9
+ import { record } from './record.js';
10
+ import { recount } from './roster.js';
11
+ import { copyStanding, stands } from './standing.js';
10
12
  import { hostAt } from './tape.js';
11
- import { Transport } from './transport.js';
13
+ import { Transport, within } from './transport.js';
12
14
  import { cover } from './unpage.js';
13
15
  import { unreached } from './unreached.js';
14
16
  /**
@@ -18,24 +20,36 @@ import { unreached } from './unreached.js';
18
20
  export function projectAll(transport, t) {
19
21
  const ahead = Number.isNaN(transport.now) || t >= transport.now;
20
22
  const tape = transport.tape;
21
- const next = ahead && tape !== undefined ? tape.timestampAt(tape.undoDepth()) : undefined;
23
+ const stamp = ahead && tape !== undefined ? tape.timestampAt(tape.undoDepth()) : undefined;
24
+ const pace = transport.pace;
25
+ const next = stamp === undefined || pace === null ? stamp : pace.reading(stamp);
22
26
  if (next !== undefined && next <= t)
23
27
  throw new Error(`blits: the tape holds calls at ${next} that a read ahead to ${t} cannot play; seek there to see them`);
24
- if (!ahead) {
25
- const history = transport.history;
26
- if (!history)
27
- throw new Error('blits: reading back needs a mix made with history');
28
+ // Behind, the last frame at or before `t`, read as it showed when `t` is that frame's own time.
29
+ let cut = { seq: transport.seq, strict: false };
30
+ let u = hostAt(transport, t);
31
+ const kept = transport.history !== undefined;
32
+ if (!ahead && kept) {
28
33
  cover(transport, t);
34
+ const frame = transport.frames.at(t);
35
+ if (frame !== undefined) {
36
+ cut = { seq: frame.seq, strict: frame.at === t };
37
+ if (frame.at === t)
38
+ u = frame.u;
39
+ }
29
40
  }
30
- const u = hostAt(transport, t);
31
- const pace = transport.pace;
41
+ else if (!ahead)
42
+ stands(transport, t);
32
43
  const c = new Transport(undefined, true);
33
44
  c.offset = transport.offset;
34
45
  c.u = u;
35
46
  c.nextId = transport.nextId;
36
47
  c.now = ahead ? transport.now : t;
37
- c.pace = pace === null ? null : pace.until(ahead ? Number.POSITIVE_INFINITY : u);
38
- c.announced = (ahead ? transport.announced : transport.announced.filter((a) => a.made < t)).map((a) => ({ ...a }));
48
+ c.pace =
49
+ pace === null || !within(cut, transport.pacedSeq)
50
+ ? null
51
+ : pace.until(ahead ? Number.POSITIVE_INFINITY : u);
52
+ c.announced = (ahead ? transport.announced : transport.announced.filter((a) => within(cut, a.seq))).map((a) => ({ ...a }));
39
53
  const copies = new Map();
40
54
  for (const mix of transport.members) {
41
55
  const copy = new Mixer(mix.kit, {
@@ -49,8 +63,10 @@ export function projectAll(transport, t) {
49
63
  copies.set(mix, copy);
50
64
  if (ahead)
51
65
  copyAhead(mix, copy);
66
+ else if (kept)
67
+ copyBack(mix, copy, t, cut);
52
68
  else
53
- copyBack(mix, copy, t);
69
+ copyStanding(mix, copy, t, cut);
54
70
  }
55
71
  if (ahead)
56
72
  for (const copy of copies.values())
@@ -83,13 +99,11 @@ export function projectAll(transport, t) {
83
99
  }
84
100
  /** A mix's copy for a read ahead: its voices as they stand, each subject carried from the live one. */
85
101
  function copyAhead(mix, c) {
86
- c.pose = mix.pose;
87
- c.wantsPose = mix.wantsPose;
88
- c.reducedNow = mix.reducedNow;
102
+ mirror(mix, c);
89
103
  c.cued = mix.cued
90
104
  .filter((v) => v.state !== 'done')
91
105
  .map((v) => {
92
- const copy = v.copy((subject) => carry(mix, v, subject));
106
+ const copy = v.copy((subject) => carry(mix, v, subject, c, copy), undefined, true);
93
107
  const at = mix.pins?.get(v);
94
108
  if (at !== undefined)
95
109
  pin(c, copy, at);
@@ -98,15 +112,14 @@ function copyAhead(mix, c) {
98
112
  ownBlends(c.cued);
99
113
  if (mix.owners !== null)
100
114
  relink(c.cued);
101
- count(c);
115
+ recount(c);
102
116
  }
103
- /** A mix's copy for a read back to `t`: its voices then, from the controls and copies kept. */
104
- function copyBack(mix, c, t) {
105
- c.pose = mix.pose;
106
- c.wantsPose = mix.wantsPose;
107
- c.reducedNow = mix.reducedNow;
117
+ /** A mix's copy for a read back to `t`, cut at `cut`: its voices then, from the controls and copies kept. */
118
+ function copyBack(mix, c, t, cut) {
119
+ mirror(mix, c);
108
120
  c.backward = true;
109
- const was = last(mix.hostLog, t, true);
121
+ // Copied as the frame's first probe began, so its own frame shows it.
122
+ const was = lastWithin(mix.hostLog, { seq: cut.seq, strict: false });
110
123
  const host = mix.opts.host;
111
124
  const then = was && typeof host === 'object' && host !== null
112
125
  ? Object.assign(Object.create(host), was.fields)
@@ -114,11 +127,11 @@ function copyBack(mix, c, t) {
114
127
  if (was)
115
128
  c.hostThen = was;
116
129
  c.cued = [...mix.cued, ...mix.gone]
117
- .filter((v) => v.cuedAt <= t && v.doneAt > t)
130
+ .filter((v) => v.cuedSeq <= cut.seq && !within(cut, v.doneSeq, true))
118
131
  .sort((a, b) => a.id - b.id)
119
132
  .map((v) => {
120
133
  const log = v.log;
121
- const copy = v.copy((subject) => recall(mix, v, subject, t), last(log, t, (e) => e.sync) ?? log[0]);
134
+ const copy = v.copy((subject) => recall(mix, v, subject, t, cut), lastWithin(log, cut) ?? log[0]);
122
135
  if (then !== undefined)
123
136
  copy.setting.host = then;
124
137
  return copy;
@@ -126,30 +139,39 @@ function copyBack(mix, c, t) {
126
139
  ownBlends(c.cued);
127
140
  if (mix.owners !== null)
128
141
  relink(c.cued);
142
+ stateAt(c, t);
143
+ recount(c);
144
+ }
145
+ /** Sets each copied voice's state to what its clock and fade make it at `t`. */
146
+ export function stateAt(c, t) {
129
147
  for (const copy of c.cued)
130
- copy.state =
131
- (copy.owner === null ? t : ownerReading(copy.owner, t)) < copy.start
148
+ copy.state = copy.out
149
+ ? 'fading'
150
+ : (copy.owner === null ? t : ownerReading(copy.owner, t)) < copy.start
132
151
  ? 'pending'
133
- : copy.out && copy.out.at <= t
134
- ? 'fading'
135
- : copy.freezesAfter && copy.elapsedAt(t) >= copy.span + copy.latest
136
- ? 'frozen'
137
- : 'live';
138
- count(c);
152
+ : copy.freezesAfter && copy.elapsedAt(t) >= copy.span + copy.latest
153
+ ? 'frozen'
154
+ : 'live';
139
155
  }
140
- /** Recounts what the fold's shortcuts depend on, for a projection's freshly copied voices. */
141
- function count(mix) {
142
- mix.named = new Store();
143
- mix.naming = 0;
144
- mix.general = [];
145
- for (const voice of mix.cued)
146
- index(mix, voice);
147
- mix.loci = mix.cued.filter((v) => v.spec.locus !== undefined).length;
148
- mix.anchored = mix.cued.filter((v) => v.spec.anchor !== undefined).length;
149
- mix.steps.push(null, ++mix.version);
156
+ /** Gives a projection's copy what it reads of the live mix as it is, before its voices are copied. */
157
+ export function mirror(mix, c) {
158
+ c.pose = mix.pose;
159
+ c.wantsPose = mix.wantsPose;
160
+ c.reducedNow = mix.reducedNow;
150
161
  }
151
162
  /** A voice's record of a subject, the one it shares among those it does not reach included. */
152
163
  function recordOf(mix, voice, subject) {
164
+ // A voice sharing one record has that for every subject a chain has asked it about, pointed at
165
+ // the subject for the caller to copy from.
166
+ if (voice.sharing) {
167
+ const slot = mix.chains.get(subject)?.slot ?? -1;
168
+ if (!sighted(voice, slot))
169
+ return undefined;
170
+ const all = voice.everyone;
171
+ open(voice, all, slot);
172
+ all.weight = weightAt(voice, slot);
173
+ return all;
174
+ }
153
175
  const held = voice.subjects.get(subject);
154
176
  if (held !== undefined || voice.unreachedBits === null)
155
177
  return held;
@@ -157,12 +179,19 @@ function recordOf(mix, voice, subject) {
157
179
  ? voice.unreached
158
180
  : undefined;
159
181
  }
160
- /** A projection ahead starts each subject from where the live mix holds it. */
161
- function carry(mix, voice, subject) {
182
+ /**
183
+ * A projection ahead starts each subject from where the live mix holds it, its `since` worked out
184
+ * again where it was still ahead, since the copy's clock may have moved it.
185
+ */
186
+ function carry(mix, voice, subject, c, copy) {
162
187
  const live = recordOf(mix, voice, subject);
163
188
  if (live === undefined)
164
189
  return undefined;
165
190
  const h = copyHeld(voice, live);
191
+ if (h.reaches && !(h.since <= mix.now)) {
192
+ h.since = c.sinceOf(copy, h.delay);
193
+ h.shown = c.shownOf(copy, h.since);
194
+ }
166
195
  h.from = h.stepped;
167
196
  return h;
168
197
  }
@@ -170,8 +199,8 @@ function carry(mix, voice, subject) {
170
199
  * A projection back starts each subject from the latest copy kept at or before `t`, else from its
171
200
  * voice's start with fresh state, which is exact for a voice stepped at a fixed interval.
172
201
  */
173
- function recall(mix, voice, subject, t) {
174
- const live = leftAt(voice.left, subject, t) ?? recordOf(mix, voice, subject);
202
+ export function recall(mix, voice, subject, t, cut) {
203
+ const live = voice.left?.at(subject, cut) ?? recordOf(mix, voice, subject);
175
204
  if (live === undefined)
176
205
  return undefined;
177
206
  const snap = live.snaps && last(live.snaps, t, true);
@@ -182,33 +211,18 @@ function recall(mix, voice, subject, t) {
182
211
  return h;
183
212
  }
184
213
  const stepped = live.since < t ? live.since : t;
185
- return {
186
- reaches: live.reaches,
187
- delay: live.delay,
188
- since: live.since,
189
- shown: live.shown,
190
- weight: 0,
191
- rested: false,
192
- bands: null,
193
- state: live.reaches && voice.patch.state
194
- ? voice.patch.state(subject)
195
- : undefined,
196
- stepped,
197
- ticks: 0,
198
- probed: Number.NaN,
199
- delta: null,
200
- phase: 0,
201
- seeks: 0,
202
- kept: null,
203
- voice,
204
- next: null,
205
- version: Number.NaN,
206
- loci: null,
207
- slot: -1,
208
- from: stepped,
209
- unknown: voice.spec.from === 'current',
210
- replay: live.inputs,
211
- };
214
+ const h = record(voice, live.reaches, live.reaches && voice.patch.state
215
+ ? voice.patch.state(subject)
216
+ : undefined);
217
+ h.delay = live.delay;
218
+ h.since = live.since;
219
+ h.shown = live.shown;
220
+ h.stepped = stepped;
221
+ h.rebuilt = voice.rebuilds;
222
+ h.from = stepped;
223
+ h.unknown = voice.spec.from === 'current';
224
+ h.replay = live.inputs;
225
+ return h;
212
226
  }
213
227
  /** Per channel, the least sure voice that fed it this frame. */
214
228
  function doubts(mix, subject) {
@@ -231,19 +245,23 @@ function doubts(mix, subject) {
231
245
  for (const slot of voice.slots)
232
246
  out[mix.names[slot]] = 'held';
233
247
  }
234
- for (let held = mix.chain(subject); held !== null; held = held.next) {
235
- const voice = held.voice;
236
- if (voice === null || voice.state === 'done')
237
- continue;
238
- if (held.weight <= 0)
239
- continue;
248
+ const feed = (voice, held) => {
249
+ if (voice.state === 'done' || held.weight <= 0)
250
+ return;
240
251
  const d = doubtOf(mix, voice, held);
241
252
  for (const slot of voice.slots) {
242
253
  const name = mix.names[slot];
243
254
  if (rank[d] > rank[out[name]])
244
255
  out[name] = d;
245
256
  }
246
- }
257
+ };
258
+ for (let held = mix.chain(subject); held !== null; held = held.next)
259
+ if (held.voice !== null)
260
+ feed(held.voice, held);
261
+ // A voice sharing one record is in no chain; its record holds the weight the fold just gave.
262
+ for (const voice of mix.sharers)
263
+ if (voice.everyone !== null && voice.state !== 'pending')
264
+ feed(voice, voice.everyone);
247
265
  return out;
248
266
  }
249
267
  function doubtOf(mix, voice, held) {
package/dist/pull.d.ts CHANGED
@@ -3,3 +3,5 @@ import type { Columns } from './types.js';
3
3
  /** After a probe, keeps the pose a retarget from `'current'` reads as the subject's last. */
4
4
  export declare function keep<I, O>(this: Mixer<I, O>, subject: I, pose: O, out: O | undefined): void;
5
5
  export declare function pull<I, O>(mix: Mixer<I, O>, subjects: Iterable<I>, into: Columns<O>): void;
6
+ /** Forgets the array `pull` last read, whose heads a seek has made stale. */
7
+ export declare function unpull<I, O>(mix: Mixer<I, O>): void;
package/dist/pull.js CHANGED
@@ -174,3 +174,11 @@ function columnsOf(mix, into) {
174
174
  }
175
175
  return columns;
176
176
  }
177
+ /** Forgets the array `pull` last read, whose heads a seek has made stale. */
178
+ export function unpull(mix) {
179
+ mix.pulled = [];
180
+ mix.pulledHeads = [];
181
+ mix.pulledSlots = new Int32Array(0);
182
+ mix.pulledVersion = Number.NaN;
183
+ mix.pulledRelinks = -1;
184
+ }
package/dist/qualify.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { numericOf } from './channels.js';
2
2
  import { flush } from './columns.js';
3
3
  import { crowdable } from './crowd.js';
4
+ import { weighAt } from './everyone.js';
4
5
  import { wake } from './fill.js';
5
6
  import { Lane } from './lane.js';
6
7
  import { bury, compactSparse, join, regroup, retouchRow, sameCrowds } from './rows.js';
@@ -169,6 +170,9 @@ export function requalify(lanes, version) {
169
170
  const ch = {
170
171
  name: host.names[slot],
171
172
  op: n.op,
173
+ plain: n.op !== 'last' && n.op !== 'own' && n.turn === undefined,
174
+ turn: n.turn ?? 0,
175
+ channel,
172
176
  scalar,
173
177
  rest: typeof rest === 'number' ? rest : rest[0],
174
178
  axes: n.axes,
@@ -332,8 +336,12 @@ function leave(lanes, lane) {
332
336
  const rec = lane.records[p];
333
337
  const slot = lane.list[p];
334
338
  const w = done ? undefined : lanes.reported(lane, slot);
335
- if (rec !== undefined && w !== undefined)
336
- rec.weight = w;
339
+ if (rec !== undefined && w !== undefined) {
340
+ if (lane.voice.sharing)
341
+ weighAt(lane.voice, slot, w, lanes.cap);
342
+ else
343
+ rec.weight = w;
344
+ }
337
345
  if (rec !== undefined && lane.motion !== undefined)
338
346
  settle(lanes, lane, p, slot, rec);
339
347
  }
package/dist/reading.d.ts CHANGED
@@ -20,4 +20,9 @@ export declare const reading: {
20
20
  * One count for every mix: a change to a spring in one mix refills the lanes of all of them.
21
21
  */
22
22
  moved: number;
23
+ /**
24
+ * How many changes an `input` signal has reported, in any mix, so a fill can tell an input moved
25
+ * since it read it. One count for every mix, as `moved` is.
26
+ */
27
+ inputs: number;
23
28
  };
package/dist/reading.js CHANGED
@@ -20,4 +20,9 @@ export const reading = {
20
20
  * One count for every mix: a change to a spring in one mix refills the lanes of all of them.
21
21
  */
22
22
  moved: 0,
23
+ /**
24
+ * How many changes an `input` signal has reported, in any mix, so a fill can tell an input moved
25
+ * since it read it. One count for every mix, as `moved` is.
26
+ */
27
+ inputs: 0,
23
28
  };
@@ -0,0 +1,9 @@
1
+ import type { Subject } from './voice.js';
2
+ /**
3
+ * A subject's record for `voice` holding every field one can come to hold, in one order, so every
4
+ * record in a mix shares one hidden class: a record built elsewhere, or given `base`, `snaps` or
5
+ * `from` later, took a class of its own, and a fold walking a chain of several kinds reads each
6
+ * field the slow way. The caller sets what it knows; nothing here takes a number, which a call V8
7
+ * does not inline would box.
8
+ */
9
+ export declare function record(voice: object | null, reaches: boolean, state: unknown): Subject<unknown>;