@msb235/blits 0.14.0 → 0.15.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 (187) hide show
  1. package/CHANGELOG.md +198 -0
  2. package/README.md +3 -3
  3. package/dist/ahead.d.ts +26 -0
  4. package/dist/ahead.js +264 -0
  5. package/dist/around.d.ts +11 -0
  6. package/dist/around.js +32 -0
  7. package/dist/bare.js +37 -14
  8. package/dist/blank.d.ts +10 -0
  9. package/dist/blank.js +26 -0
  10. package/dist/blend.d.ts +2 -1
  11. package/dist/blend.js +1 -6
  12. package/dist/bound.d.ts +70 -0
  13. package/dist/bound.js +351 -0
  14. package/dist/chain.d.ts +2 -1
  15. package/dist/chain.js +7 -5
  16. package/dist/changes.d.ts +24 -0
  17. package/dist/changes.js +189 -0
  18. package/dist/channels.d.ts +39 -13
  19. package/dist/channels.js +14 -5
  20. package/dist/color.d.ts +12 -10
  21. package/dist/color.js +25 -25
  22. package/dist/columns.d.ts +2 -1
  23. package/dist/columns.js +62 -38
  24. package/dist/contribution.d.ts +30 -0
  25. package/dist/contribution.js +374 -0
  26. package/dist/copyout.d.ts +10 -0
  27. package/dist/copyout.js +69 -24
  28. package/dist/crowd.d.ts +16 -6
  29. package/dist/crowd.js +20 -11
  30. package/dist/cue.d.ts +8 -1
  31. package/dist/cue.js +39 -13
  32. package/dist/cut.d.ts +8 -0
  33. package/dist/cut.js +4 -0
  34. package/dist/delay.d.ts +3 -0
  35. package/dist/delay.js +7 -0
  36. package/dist/drop.d.ts +9 -0
  37. package/dist/drop.js +84 -0
  38. package/dist/due.d.ts +24 -5
  39. package/dist/due.js +114 -34
  40. package/dist/easing.d.ts +6 -4
  41. package/dist/easing.js +7 -3
  42. package/dist/everyone.d.ts +8 -2
  43. package/dist/everyone.js +33 -3
  44. package/dist/fade.d.ts +2 -1
  45. package/dist/fade.js +2 -1
  46. package/dist/fill.d.ts +3 -2
  47. package/dist/fill.js +52 -20
  48. package/dist/fit.d.ts +44 -26
  49. package/dist/fit.js +10 -9
  50. package/dist/fold.d.ts +4 -37
  51. package/dist/fold.js +5 -513
  52. package/dist/gather.d.ts +12 -7
  53. package/dist/gather.js +179 -44
  54. package/dist/gone.js +2 -2
  55. package/dist/grid.d.ts +47 -0
  56. package/dist/grid.js +247 -0
  57. package/dist/handle.d.ts +2 -0
  58. package/dist/handle.js +14 -8
  59. package/dist/held.d.ts +2 -1
  60. package/dist/held.js +2 -1
  61. package/dist/history.d.ts +6 -5
  62. package/dist/history.js +7 -9
  63. package/dist/hosts.js +9 -15
  64. package/dist/index.d.ts +5 -4
  65. package/dist/keyfill.d.ts +1 -1
  66. package/dist/keyfill.js +5 -3
  67. package/dist/keyrows.d.ts +12 -0
  68. package/dist/keyrows.js +99 -12
  69. package/dist/lane.d.ts +38 -11
  70. package/dist/lane.js +46 -11
  71. package/dist/lanes.d.ts +30 -12
  72. package/dist/lanes.js +56 -36
  73. package/dist/leavings.d.ts +2 -2
  74. package/dist/leavings.js +1 -1
  75. package/dist/lives.d.ts +47 -0
  76. package/dist/lives.js +180 -0
  77. package/dist/meet.d.ts +2 -2
  78. package/dist/meet.js +95 -7
  79. package/dist/mixer.d.ts +56 -37
  80. package/dist/mixer.js +57 -171
  81. package/dist/motion.d.ts +14 -118
  82. package/dist/motion.js +16 -8
  83. package/dist/motions.d.ts +62 -155
  84. package/dist/motions.js +56 -370
  85. package/dist/moved.d.ts +1 -1
  86. package/dist/moved.js +16 -7
  87. package/dist/named.d.ts +6 -2
  88. package/dist/named.js +49 -3
  89. package/dist/numbers.d.ts +7 -0
  90. package/dist/numbers.js +33 -0
  91. package/dist/owed.d.ts +2 -0
  92. package/dist/owed.js +4 -0
  93. package/dist/pack.d.ts +2 -1
  94. package/dist/pack.js +2 -3
  95. package/dist/patch.d.ts +7 -3
  96. package/dist/patch.js +6 -2
  97. package/dist/places.d.ts +53 -0
  98. package/dist/places.js +171 -0
  99. package/dist/plot.d.ts +12 -0
  100. package/dist/plot.js +52 -0
  101. package/dist/project.d.ts +4 -4
  102. package/dist/project.js +9 -212
  103. package/dist/pull.js +22 -9
  104. package/dist/qualify.js +93 -31
  105. package/dist/reading.d.ts +6 -0
  106. package/dist/reading.js +5 -0
  107. package/dist/record.d.ts +5 -1
  108. package/dist/record.js +3 -2
  109. package/dist/relink.d.ts +1 -1
  110. package/dist/renumber.d.ts +10 -0
  111. package/dist/renumber.js +75 -0
  112. package/dist/restored.d.ts +2 -2
  113. package/dist/restored.js +1 -1
  114. package/dist/rests.d.ts +9 -0
  115. package/dist/rests.js +76 -0
  116. package/dist/revive.d.ts +3 -2
  117. package/dist/revive.js +2 -2
  118. package/dist/{locus.d.ts → role.d.ts} +10 -9
  119. package/dist/{locus.js → role.js} +3 -3
  120. package/dist/roles.d.ts +10 -0
  121. package/dist/roles.js +158 -0
  122. package/dist/roster.js +3 -3
  123. package/dist/rotation.d.ts +1 -1
  124. package/dist/rotation.js +1 -1
  125. package/dist/rows.js +28 -20
  126. package/dist/sample.d.ts +2 -2
  127. package/dist/sample.js +33 -7
  128. package/dist/seat.d.ts +17 -0
  129. package/dist/seat.js +25 -0
  130. package/dist/seek.js +5 -5
  131. package/dist/setting.d.ts +27 -0
  132. package/dist/setting.js +55 -0
  133. package/dist/shared.d.ts +1 -1
  134. package/dist/shared.js +1 -1
  135. package/dist/signals.d.ts +64 -32
  136. package/dist/signals.js +20 -18
  137. package/dist/stack.d.ts +5 -5
  138. package/dist/stack.js +5 -5
  139. package/dist/standing.d.ts +2 -1
  140. package/dist/standing.js +3 -2
  141. package/dist/store.d.ts +9 -0
  142. package/dist/store.js +24 -0
  143. package/dist/strays.d.ts +2 -1
  144. package/dist/strays.js +23 -15
  145. package/dist/stretch.d.ts +112 -0
  146. package/dist/stretch.js +9 -0
  147. package/dist/subject.d.ts +142 -0
  148. package/dist/subject.js +1 -0
  149. package/dist/tape.d.ts +9 -5
  150. package/dist/tape.js +5 -1
  151. package/dist/then.d.ts +9 -0
  152. package/dist/then.js +57 -0
  153. package/dist/ticker.d.ts +4 -4
  154. package/dist/trace.d.ts +4 -0
  155. package/dist/trace.js +255 -0
  156. package/dist/transport.d.ts +0 -8
  157. package/dist/transport.js +0 -4
  158. package/dist/turns.d.ts +4 -3
  159. package/dist/turns.js +4 -3
  160. package/dist/types/arc.d.ts +12 -11
  161. package/dist/types/channel.d.ts +12 -8
  162. package/dist/types/history.d.ts +35 -19
  163. package/dist/types/mix.d.ts +237 -163
  164. package/dist/types/motion.d.ts +151 -0
  165. package/dist/types/motion.js +1 -0
  166. package/dist/types/patch.d.ts +67 -34
  167. package/dist/types/place.d.ts +51 -36
  168. package/dist/types/trace.d.ts +51 -0
  169. package/dist/types/trace.js +1 -0
  170. package/dist/types/transport.d.ts +20 -18
  171. package/dist/types/voice.d.ts +144 -73
  172. package/dist/types/watch.d.ts +88 -40
  173. package/dist/types.d.ts +2 -0
  174. package/dist/unreached.d.ts +2 -0
  175. package/dist/unreached.js +7 -0
  176. package/dist/voice.d.ts +10 -143
  177. package/dist/voice.js +37 -82
  178. package/dist/warn.d.ts +2 -0
  179. package/dist/warn.js +1 -1
  180. package/dist/watch.d.ts +2 -2
  181. package/dist/watch.js +5 -3
  182. package/dist/wave.d.ts +4 -3
  183. package/dist/wave.js +3 -2
  184. package/dist/weigh.d.ts +7 -1
  185. package/dist/weigh.js +27 -0
  186. package/docs/schema.html +223 -34
  187. package/package.json +2 -2
package/dist/project.js CHANGED
@@ -1,23 +1,20 @@
1
+ import { copyAhead, mirror, playAhead, recordOf } from './ahead.js';
1
2
  import { ownBlends } from './blend.js';
2
- import { headOf, heldAt } from './chain.js';
3
+ import { heldAt } from './chain.js';
3
4
  import { stepOf } from './clock.js';
4
- import { open, sighted, weightAt } from './everyone.js';
5
+ import { within } from './cut.js';
5
6
  import { fadesAt } from './fade.js';
6
7
  import { meet, origin } from './held.js';
7
8
  import { copyHeld, last, lastWithin } from './history.js';
8
- import { markOf, voiceMarks } from './marks.js';
9
9
  import { Mixer } from './mixer.js';
10
- import { move } from './move.js';
11
10
  import { heldByInput, ownerReading, relink } from './owner.js';
12
- import { pin, place } from './place.js';
13
11
  import { reading } from './reading.js';
14
12
  import { record } from './record.js';
15
13
  import { recount } from './roster.js';
16
14
  import { copyStanding, stands } from './standing.js';
17
- import { hostAhead, hostAt, replay } from './tape.js';
18
- import { Transport, within } from './transport.js';
15
+ import { hostAhead, hostAt } from './tape.js';
16
+ import { Transport } from './transport.js';
19
17
  import { cover, coverFrame } from './unpage.js';
20
- import { unreached } from './unreached.js';
21
18
  /**
22
19
  * Every mix on a transport read at mix time `t`, through copies of them on a transport of their
23
20
  * own, moved together so an anchor across mixes answers as it would live. Ahead, they stop at each
@@ -75,6 +72,7 @@ export function projectAll(transport, at) {
75
72
  }, null);
76
73
  copy.slot = mix.slot;
77
74
  copy.projecting = true;
75
+ copy.places = mix.places;
78
76
  copies.set(mix, copy);
79
77
  if (ahead)
80
78
  copyAhead(mix, copy);
@@ -91,15 +89,18 @@ export function projectAll(transport, at) {
91
89
  earlier = new Map();
92
90
  stood = playAhead(transport, c, copies, t, earlier);
93
91
  }
92
+ const back = !ahead && kept ? cut : null;
94
93
  const read = (f) => {
95
94
  reading.live = false;
96
95
  reading.ahead = stood.ahead.size > 0 ? stood.ahead : null;
96
+ reading.back = back;
97
97
  try {
98
98
  return f();
99
99
  }
100
100
  finally {
101
101
  reading.live = true;
102
102
  reading.ahead = null;
103
+ reading.back = null;
103
104
  }
104
105
  };
105
106
  return {
@@ -136,168 +137,6 @@ export function projectAll(transport, at) {
136
137
  },
137
138
  };
138
139
  }
139
- /**
140
- * Moves a read ahead's copies to mix time `t`, making the calls the tape holds on the way as a seek
141
- * there makes them on the mixes. While a voice could carry state across it, it stops at each known
142
- * event: a call, and every mark of a voice or announced. At each stop the copies are kept for
143
- * subjects to be read there and copied on, so `copies` ends holding those at `t`. Answers the
144
- * transport they stand on, and puts the tape back where it stood.
145
- */
146
- function playAhead(transport, c, copies, t, earlier) {
147
- const tape = transport.tape;
148
- const depth = tape?.undoDepth() ?? 0;
149
- let played = false;
150
- try {
151
- for (;;) {
152
- const e = nextEvent(tape, c, t);
153
- if (!(e < t))
154
- break;
155
- played = playTo(transport, c, copies, e) || played;
156
- if (!carries(c))
157
- continue;
158
- // Placed now, as the next move would place them, while a target that has just left is here.
159
- for (const m of c.members)
160
- if (m.anchored > 0)
161
- place(m);
162
- const next = restage(c);
163
- for (const [mix, copy] of copies) {
164
- const on = new Mixer(copy.kit, { ...copy.opts, transport: next }, null);
165
- on.slot = copy.slot;
166
- on.projecting = true;
167
- copyAhead(copy, on);
168
- const stages = earlier.get(mix);
169
- if (stages === undefined)
170
- earlier.set(mix, { at: [copy], met: new Set() });
171
- else
172
- stages.at.push(copy);
173
- copies.set(mix, on);
174
- }
175
- c = next;
176
- }
177
- played = playTo(transport, c, copies, t) || played;
178
- }
179
- finally {
180
- if (played)
181
- tape?.goto(depth);
182
- }
183
- return c;
184
- }
185
- /**
186
- * Whether a voice on a read ahead's copies could carry state from one known event to the next: a
187
- * patch that steps, or a weight given by a signal, which may keep state. Where none could, a read
188
- * crosses the events in one go, as fast as one with none.
189
- */
190
- function carries(c) {
191
- for (const m of c.members)
192
- for (const v of m.cued)
193
- if (v.state !== 'done' && (v.patch.step !== undefined || typeof v.spec.weight === 'function'))
194
- return true;
195
- return false;
196
- }
197
- /**
198
- * The earliest known event a read ahead's copies have yet to reach before `t`, mix time: the next
199
- * call the tape holds, and while a voice could carry state across one, a mark of a voice or one
200
- * announced. Infinity where there is none.
201
- */
202
- function nextEvent(tape, c, t) {
203
- const now = c.now;
204
- let e = Number.POSITIVE_INFINITY;
205
- const call = tape?.timestampAt(tape.undoDepth());
206
- if (call !== undefined)
207
- e = c.pace === null ? call : c.pace.reading(call);
208
- if (!carries(c))
209
- return e < t ? e : Number.POSITIVE_INFINITY;
210
- for (const m of c.members)
211
- for (const v of m.cued) {
212
- if (v.state === 'done')
213
- continue;
214
- for (const mark of voiceMarks) {
215
- const at = markOf(m, v, mark);
216
- if (at !== undefined && at > now && at < e)
217
- e = at;
218
- }
219
- const inner = v.spec.marks;
220
- if (inner !== undefined)
221
- for (const mark of Object.keys(inner)) {
222
- const at = markOf(m, v, mark);
223
- if (at !== undefined && at > now && at < e)
224
- e = at;
225
- }
226
- }
227
- for (const a of c.announced) {
228
- const at = c.pace === null ? a.at : c.pace.at(a.at, c.u);
229
- if (at > now && at < e)
230
- e = at;
231
- }
232
- return e < t ? e : Number.POSITIVE_INFINITY;
233
- }
234
- /**
235
- * Makes the calls the tape holds up to mix time `to` on a read ahead's copies and moves them
236
- * there. Answers whether it moved the tape.
237
- */
238
- function playTo(transport, c, copies, to) {
239
- const tape = transport.tape;
240
- const due = (at) => (c.pace === null ? at : c.pace.reading(at)) <= to;
241
- const next = tape?.timestampAt(tape.undoDepth());
242
- c.u = hostAhead(c, to);
243
- let played = false;
244
- if (next !== undefined && due(next)) {
245
- // A call may write what a copy shares with the voice it was copied from.
246
- for (const copy of c.members)
247
- for (const v of copy.cued) {
248
- if (v.parts !== null)
249
- v.parts = new Map(v.parts);
250
- if (v.parted !== null)
251
- v.parted = new Map(v.parted);
252
- }
253
- played = true;
254
- const last = replay(transport, due, c, {
255
- transport: () => c,
256
- mix: (of) => copies.get(of),
257
- });
258
- c.u = Math.max(last, hostAhead(c, to));
259
- }
260
- // A call played at `to` itself moved the copies there before it, as a sync does.
261
- if (!(c.now >= to))
262
- for (const copy of c.members)
263
- move(copy, to);
264
- return played;
265
- }
266
- /** A transport for a read ahead's copies to go on from where those on `c` stand. */
267
- function restage(c) {
268
- const next = new Transport(undefined, true);
269
- next.offset = c.offset;
270
- next.u = c.u;
271
- next.nextId = c.nextId;
272
- next.now = c.now;
273
- next.pace = c.pace === null ? null : c.pace.until(Number.POSITIVE_INFINITY);
274
- next.announced = c.announced.map((a) => ({ ...a }));
275
- // The retargets and pushes the tape made carry their times, so every stop shares one list.
276
- next.ahead = c.ahead;
277
- return next;
278
- }
279
- /**
280
- * A mix's copy for a read ahead: its voices as they stand, each subject carried from the live one,
281
- * or from the copy at the known event before.
282
- */
283
- function copyAhead(mix, c) {
284
- mirror(mix, c);
285
- c.cued = mix.cued
286
- .filter((v) => v.state !== 'done')
287
- .map((v) => {
288
- const copy = v.copy((subject) => carry(mix, v, subject, c, copy), undefined, true);
289
- const at = mix.pins?.get(v);
290
- if (at !== undefined)
291
- pin(c, copy, at);
292
- return copy;
293
- });
294
- c.waking = mix.waking;
295
- ownBlends(c.cued);
296
- // A copy keeps no list of owners, so the one before says only by its voices.
297
- if (mix.owners !== null || (mix.projecting && c.cued.some((v) => v.holding !== null)))
298
- relink(c.cued);
299
- recount(c);
300
- }
301
140
  /** A mix's copy for a read back to `t`, cut at `cut`: its voices then, from the controls and copies kept. */
302
141
  function copyBack(mix, c, t, cut) {
303
142
  mirror(mix, c);
@@ -352,48 +191,6 @@ export function stateAt(c, t) {
352
191
  ? 'frozen'
353
192
  : 'live';
354
193
  }
355
- /** Gives a projection's copy what it reads of the live mix as it is, before its voices are copied. */
356
- export function mirror(mix, c) {
357
- c.pose = mix.pose;
358
- c.wantsPose = mix.wantsPose;
359
- c.reducedNow = mix.reducedNow;
360
- }
361
- /** A voice's record of a subject, the one it shares among those it does not reach included. */
362
- function recordOf(mix, voice, subject) {
363
- // A voice sharing one record has that for every subject a chain has asked it about, pointed at
364
- // the subject for the caller to copy from.
365
- if (voice.sharing) {
366
- const slot = headOf(mix, subject)?.slot ?? -1;
367
- if (!sighted(voice, slot))
368
- return undefined;
369
- const all = voice.everyone;
370
- open(voice, all, slot);
371
- all.weight = weightAt(voice, slot);
372
- return all;
373
- }
374
- const held = voice.subjects.get(subject);
375
- if (held !== undefined || voice.unreachedBits === null)
376
- return held;
377
- return unreached(voice, headOf(mix, subject)?.slot ?? -1)
378
- ? voice.unreached
379
- : undefined;
380
- }
381
- /**
382
- * A projection ahead starts each subject from where the live mix holds it, its `since` worked out
383
- * again where it was still ahead, since the copy's clock may have moved it.
384
- */
385
- function carry(mix, voice, subject, c, copy) {
386
- const live = recordOf(mix, voice, subject);
387
- if (live === undefined)
388
- return undefined;
389
- const h = copyHeld(voice, live);
390
- if (h.reaches && !(h.since <= mix.now)) {
391
- h.since = c.sinceOf(copy, h.delay);
392
- h.shown = c.shownOf(copy, h.since);
393
- }
394
- h.from = h.stepped;
395
- return h;
396
- }
397
194
  /**
398
195
  * A projection back starts each subject from the latest copy kept at or before `t`, else from its
399
196
  * voice's start with fresh state, which is exact for a voice stepped at a fixed interval.
package/dist/pull.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { axesOf } from './channels.js';
2
2
  import { copy } from './clone.js';
3
3
  import { clampRun, flush, pullRun, writeLater } from './columns.js';
4
+ import { renumber } from './renumber.js';
4
5
  /** What `pull` remembers at a position it has read no subject at. */
5
6
  const UNREAD = Symbol('unread');
6
7
  /** Writes a folded pose's value into subject `n`'s places in a column. */
@@ -18,12 +19,10 @@ function writeValue(c, v, n) {
18
19
  }
19
20
  /** After a probe, keeps the pose a retarget from `'current'` reads as the subject's last. */
20
21
  export function keep(subject, pose, out) {
21
- // Keeping last frame's pose is free when the mix allocated it; a host sampling into its own
22
- // object only pays for the copy once something in the mix has asked to retarget from it.
22
+ // A pose handed back without `out` is the host's to edit, so it is copied too. A host sampling
23
+ // into its own object only pays for the copy once something has asked to retarget from it.
23
24
  let kept;
24
- if (out === undefined)
25
- kept = pose;
26
- else if (this.wantsPose) {
25
+ if (out === undefined || this.wantsPose) {
27
26
  kept = {};
28
27
  for (const key of this.names) {
29
28
  const v = pose[key];
@@ -32,9 +31,14 @@ export function keep(subject, pose, out) {
32
31
  }
33
32
  }
34
33
  if (kept !== undefined) {
35
- const rec = this.pose.get(subject);
34
+ const rec = this.lastPoses.get(subject);
36
35
  if (rec === undefined)
37
- this.pose.set(subject, { pose: kept, at: this.now, prev: undefined, prevAt: Number.NaN });
36
+ this.lastPoses.set(subject, {
37
+ pose: kept,
38
+ at: this.now,
39
+ prev: undefined,
40
+ prevAt: Number.NaN,
41
+ });
38
42
  else if (rec.at === this.now)
39
43
  rec.pose = kept;
40
44
  else {
@@ -50,7 +54,7 @@ export function pull(mix, subjects, into) {
50
54
  const lanes = mix.lanes;
51
55
  const now = mix.now;
52
56
  if (mix.scratch === undefined)
53
- mix.scratch = {};
57
+ mix.scratch = mix.pose();
54
58
  let room = Number.POSITIVE_INFINITY;
55
59
  let tightest = '';
56
60
  for (const c of columns) {
@@ -149,12 +153,21 @@ function readAll(mix, list, columns, live, current, since, whole) {
149
153
  was[n] = subject;
150
154
  heads[n] = head;
151
155
  slot = head.slot;
156
+ // Numbered out of the list's order, as swap-remove leaves a subject: renumbered into it.
157
+ const to = n === 0 ? 0 : slots[n - 1] + 1;
158
+ if (runs && slot >= 0 && slot !== to && to >= 0) {
159
+ if (renumber(mix, subject, head, to)) {
160
+ for (let j = was.indexOf(subject); j >= 0; j = was.indexOf(subject, j + 1))
161
+ slots[j] = to;
162
+ slot = to;
163
+ }
164
+ }
152
165
  slots[n] = slot;
153
166
  }
154
167
  }
155
168
  const laned = live && lanes?.prepare(slot, subject, mix.version, heads[n] ?? null) === true;
156
169
  if (laned && whole && lanes.whole && !lanes.owes(slot)) {
157
- writeLater(lanes, slot, columns, n);
170
+ writeLater(lanes, slot, columns, n, slots);
158
171
  continue;
159
172
  }
160
173
  const head = live ? heads[n] : null;
package/dist/qualify.js CHANGED
@@ -5,8 +5,10 @@ import { byId } from './due.js';
5
5
  import { weighAt } from './everyone.js';
6
6
  import { wake } from './fill.js';
7
7
  import { Lane } from './lane.js';
8
+ import { reach } from './meet.js';
8
9
  import { bury, compactSparse, join, regroup, retouchRow, sameCrowds } from './rows.js';
9
10
  import { settle } from './sample.js';
11
+ import { seated, seatOf } from './seat.js';
10
12
  /**
11
13
  * Which channels run as lanes and which voices run on them: the fixed point where every voice
12
14
  * writing a laned channel fits and writes only laned channels. Only channels some laned voice
@@ -57,21 +59,21 @@ export function requalify(lanes, version) {
57
59
  keepCallsOff(present, fits, host.channels);
58
60
  const candidates = () => present.map((v, i) => ({ id: v.id, fits: fits[i], slots: v.slots }));
59
61
  let { channels, voices } = qualify(numeric, candidates());
60
- // A locus folds its members together, so they run on lanes all together or not at all.
62
+ // A role folds its members together, so they run on lanes all together or not at all.
61
63
  for (let split = true; split;) {
62
64
  split = false;
63
- const loci = new Map();
65
+ const roles = new Map();
64
66
  present.forEach((v, i) => {
65
- const name = v.spec.locus;
67
+ const name = v.spec.role;
66
68
  if (name === undefined)
67
69
  return;
68
- const at = loci.get(name);
70
+ const at = roles.get(name);
69
71
  if (at === undefined)
70
- loci.set(name, [i]);
72
+ roles.set(name, [i]);
71
73
  else
72
74
  at.push(i);
73
75
  });
74
- for (const at of loci.values()) {
76
+ for (const at of roles.values()) {
75
77
  const on = at.filter((i) => voices.has(present[i].id)).length;
76
78
  if (on === 0 || on === at.length)
77
79
  continue;
@@ -97,18 +99,18 @@ export function requalify(lanes, version) {
97
99
  const lane = lanes.byId.get(v.id) ?? new Lane(v);
98
100
  v.laned = true;
99
101
  if (lane.epoch === 0 && v.state !== 'pending')
100
- lane.epoch = open(lanes, v);
102
+ opened(lanes, lane);
101
103
  kept.push(lane);
102
104
  }
103
105
  lanes.lanes = kept;
104
- lanes.loci = [];
105
- const loci = new Map();
106
+ lanes.roles = [];
107
+ const roles = new Map();
106
108
  for (const lane of kept) {
107
109
  lane.group = null;
108
- const name = lane.voice.spec.locus;
110
+ const name = lane.voice.spec.role;
109
111
  if (name === undefined)
110
112
  continue;
111
- let g = loci.get(name);
113
+ let g = roles.get(name);
112
114
  if (g === undefined) {
113
115
  g = {
114
116
  members: [],
@@ -117,13 +119,17 @@ export function requalify(lanes, version) {
117
119
  sum: new Float64Array(lanes.cap),
118
120
  values: [],
119
121
  taken: [],
122
+ contiguous: false,
123
+ inline: false,
120
124
  };
121
- loci.set(name, g);
122
- lanes.loci.push(g);
125
+ roles.set(name, g);
126
+ lanes.roles.push(g);
123
127
  }
124
128
  g.members.push(lane);
125
129
  lane.group = g;
126
130
  }
131
+ for (const [name, g] of roles)
132
+ g.contiguous = contiguous(present, voices, name, g.members);
127
133
  lanes.dense = kept
128
134
  .filter((l) => l.voice.named === null && l.epoch > 0)
129
135
  .sort((a, b) => a.epoch - b.epoch);
@@ -145,22 +151,22 @@ export function requalify(lanes, version) {
145
151
  for (const c of lanes.crowds)
146
152
  for (let p = 0; p < c.size; p++) {
147
153
  const v = c.voices[p];
148
- if (c.rowOf.get(v.id) === p && v.state === 'done')
154
+ if (seated(lanes, v, c, p) && v.state === 'done')
149
155
  bury(lanes, c, p);
150
156
  }
151
157
  // A crowd voice that started or began fading takes it up on its row, as `retouch` does.
152
158
  for (const v of touched) {
153
159
  if (v.state === 'done')
154
160
  continue;
155
- const c = lanes.crowdOf.get(v.id);
156
- const p = c?.rowOf.get(v.id);
157
- if (c !== undefined && p !== undefined)
158
- retouchRow(lanes, c, p, v);
161
+ const s = seatOf(lanes, v);
162
+ if (s !== null)
163
+ retouchRow(lanes, s.crowd, s.row, v);
159
164
  }
160
165
  compactSparse(lanes);
161
166
  return;
162
167
  }
163
168
  lanes.laned = [];
169
+ lanes.copier = null;
164
170
  lanes.bySlot = host.channels.map(() => undefined);
165
171
  channels.forEach((on, slot) => {
166
172
  if (!on)
@@ -170,10 +176,12 @@ export function requalify(lanes, version) {
170
176
  // A rest-less channel's values are NaN until some voice passes the band: absent from the pose.
171
177
  const rest = (channel.rest ?? Number.NaN);
172
178
  const scalar = typeof rest === 'number' && n.axes === 1;
179
+ const op = n.folds ?? n.op;
173
180
  const ch = {
174
181
  name: host.names[slot],
175
- op: n.op,
176
- plain: n.op !== 'last' && n.op !== 'own' && n.turn === undefined,
182
+ op,
183
+ plain: op !== 'last' && op !== 'own' && n.turn === undefined,
184
+ straight: n.folds === undefined,
177
185
  turn: n.turn ?? 0,
178
186
  channel,
179
187
  scalar,
@@ -263,7 +271,7 @@ export function open(lanes, v) {
263
271
  lanes.wide = epoch;
264
272
  return epoch;
265
273
  }
266
- for (const subject of v.named) {
274
+ for (const subject of v.named.list) {
267
275
  const slot = lanes.host.slotOf(subject);
268
276
  if (slot < 0 || slot >= lanes.cap)
269
277
  continue;
@@ -298,14 +306,13 @@ export function retouch(lanes) {
298
306
  const known = v.id <= lanes.known;
299
307
  if (v.id > lanes.known)
300
308
  lanes.known = v.id;
301
- const c = lanes.crowdOf.get(v.id);
302
- const p = c?.rowOf.get(v.id);
303
- if (c !== undefined && p !== undefined) {
309
+ const s = seatOf(lanes, v);
310
+ if (s !== null) {
304
311
  if (v.state === 'done') {
305
- bury(lanes, c, p);
312
+ bury(lanes, s.crowd, s.row);
306
313
  continue;
307
314
  }
308
- retouchRow(lanes, c, p, v);
315
+ retouchRow(lanes, s.crowd, s.row, v);
309
316
  continue;
310
317
  }
311
318
  if (v.state === 'done' && !known)
@@ -318,8 +325,8 @@ export function retouch(lanes) {
318
325
  }
319
326
  if (v.state === 'done')
320
327
  return qualify();
321
- // A locus's members are on lanes together or not at all, so one joining may move the others.
322
- if (v.spec.locus !== undefined)
328
+ // A role's members are on lanes together or not at all, so one joining may move the others.
329
+ if (v.spec.role !== undefined)
323
330
  return qualify();
324
331
  const laned = v.slots.some((slot) => lanes.bySlot[slot] !== undefined);
325
332
  const fits = v.slots.length > 0 && lanes.host.fits(v);
@@ -341,6 +348,46 @@ export function retouch(lanes) {
341
348
  lanes.subjects = [];
342
349
  return true;
343
350
  }
351
+ /** A lane's voice started playing: its epoch, and room up front where it reaches every subject. */
352
+ function opened(lanes, lane) {
353
+ const v = lane.voice;
354
+ lane.epoch = open(lanes, v);
355
+ if (v.named === null && v.spec.target === undefined) {
356
+ lane.reserve(lanes.live, lanes.cap);
357
+ if (placesAll(lanes, lane))
358
+ placeAll(lanes, lane);
359
+ }
360
+ }
361
+ /**
362
+ * Whether a lane just opened can take every numbered subject now, its first call or read for each
363
+ * made by the fill: a stateless fn or keys voice sharing one record, which every subject would meet
364
+ * at its next probe, in a host that probed every subject last frame. One by one, each probe would
365
+ * meet it and make its first call or read there (`foldNow` in `meet.ts`).
366
+ */
367
+ function placesAll(lanes, lane) {
368
+ const v = lane.voice;
369
+ return (v.sharing &&
370
+ v.everyone !== null &&
371
+ lane.motion === undefined &&
372
+ !v.keeping &&
373
+ typeof v.spec.weight !== 'function' &&
374
+ lane.list.length === 0 &&
375
+ lanes.lastDistinct >= lanes.live);
376
+ }
377
+ /** Gives every numbered subject a position on a lane `placesAll` passed. */
378
+ function placeAll(lanes, lane) {
379
+ const rec = lane.voice.everyone;
380
+ const numbers = lanes.numbers;
381
+ const size = Math.min(numbers.size, lanes.cap);
382
+ for (let slot = 0; slot < size; slot++) {
383
+ if (!numbers.has(slot))
384
+ continue;
385
+ lane.add(slot, rec);
386
+ if (lane.idle)
387
+ reach(lanes, slot, 1);
388
+ }
389
+ lane.every = true;
390
+ }
344
391
  /**
345
392
  * A new voice that fits and writes only laned channels, and that no crowd takes, gets a lane at
346
393
  * the end of the order, its id being past every other: what a qualify would give it, since such a
@@ -350,7 +397,7 @@ function enlane(lanes, v) {
350
397
  const lane = new Lane(v);
351
398
  v.laned = true;
352
399
  if (v.state !== 'pending')
353
- lane.epoch = open(lanes, v);
400
+ opened(lanes, lane);
354
401
  lane.chans = v.slots.map((s) => lanes.bySlot[s]);
355
402
  lane.values = lane.chans.map(() => undefined);
356
403
  lanes.lanes.push(lane);
@@ -360,13 +407,13 @@ function enlane(lanes, v) {
360
407
  }
361
408
  /**
362
409
  * A laned voice that started playing or finished, as a qualify would take it: a lane leaving
363
- * cannot lane or unlane another channel. False for a locus member, which a qualify must take.
410
+ * cannot lane or unlane another channel. False for a role member, which a qualify must take.
364
411
  */
365
412
  function relane(lanes, lane) {
366
413
  const v = lane.voice;
367
414
  if (v.state !== 'done') {
368
415
  if (lane.epoch === 0 && v.state !== 'pending') {
369
- lane.epoch = open(lanes, v);
416
+ opened(lanes, lane);
370
417
  if (v.named === null)
371
418
  lanes.dense.push(lane);
372
419
  }
@@ -410,3 +457,18 @@ function leave(lanes, lane) {
410
457
  }
411
458
  lane.voice.laned = false;
412
459
  }
460
+ /**
461
+ * Whether a role's members, in the order lanes run, have no laned or crowded voice of another
462
+ * role or none between them, so its every subject folds at its first member's turn.
463
+ */
464
+ function contiguous(present, voices, name, members) {
465
+ for (let i = 1; i < members.length; i++)
466
+ if (members[i].voice.id <= members[i - 1].voice.id)
467
+ return false;
468
+ const lo = members[0].voice.id;
469
+ const hi = members[members.length - 1].voice.id;
470
+ for (const v of present)
471
+ if (v.id > lo && v.id < hi && voices.has(v.id) && v.spec.role !== name)
472
+ return false;
473
+ return true;
474
+ }
package/dist/reading.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type { Cut } from './cut.js';
1
2
  /**
2
3
  * Internal: what kind of read the mix is making right now, for patches that keep state of their own
3
4
  * outside the mix. Not exported from the package.
@@ -32,4 +33,9 @@ export declare const reading: {
32
33
  * commits none. Null for every other read.
33
34
  */
34
35
  ahead: Map<object, Map<unknown, unknown[]>> | null;
36
+ /**
37
+ * While a read back under history reads: the frame it reads, so a motion patch reads the run a
38
+ * subject had then and only the changes made by then. Null for every other read.
39
+ */
40
+ back: Cut | null;
35
41
  };
package/dist/reading.js CHANGED
@@ -32,4 +32,9 @@ export const reading = {
32
32
  * commits none. Null for every other read.
33
33
  */
34
34
  ahead: null,
35
+ /**
36
+ * While a read back under history reads: the frame it reads, so a motion patch reads the run a
37
+ * subject had then and only the changes made by then. Null for every other read.
38
+ */
39
+ back: null,
35
40
  };
package/dist/record.d.ts CHANGED
@@ -1,4 +1,8 @@
1
- import type { Subject } from './voice.js';
1
+ import type { Subject } from './subject.js';
2
+ /** Whether a record was left at rest during a handover. */
3
+ export declare const isRested: (h: {
4
+ restedAt: number;
5
+ }) => boolean;
2
6
  /** A record with every field written out, optional ones too, as every record in a mix is built. */
3
7
  export type WholeRecord = {
4
8
  [K in Field]: Subject<unknown>[K];
package/dist/record.js CHANGED
@@ -1,3 +1,5 @@
1
+ /** Whether a record was left at rest during a handover. */
2
+ export const isRested = (h) => !Number.isNaN(h.restedAt);
1
3
  /**
2
4
  * A subject's record for `voice` holding every field one can come to hold, in one order, so every
3
5
  * record in a mix shares one hidden class: a record built elsewhere, or given `base`, `snaps` or
@@ -12,7 +14,6 @@ export function record(voice, reaches, state) {
12
14
  since: 0,
13
15
  shown: 0,
14
16
  weight: 0,
15
- rested: false,
16
17
  restedAt: Number.NaN,
17
18
  bands: null,
18
19
  state,
@@ -37,7 +38,7 @@ export function record(voice, reaches, state) {
37
38
  voice,
38
39
  next: null,
39
40
  version: Number.NaN,
40
- loci: null,
41
+ roles: null,
41
42
  slot: -1,
42
43
  run: -1,
43
44
  freed: -1,
package/dist/relink.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { Subject } from './voice.js';
1
+ import type { Subject } from './subject.js';
2
2
  /** What `patch` needs of a voice: its place in voice order. */
3
3
  interface Ordered {
4
4
  readonly id: number;
@@ -0,0 +1,10 @@
1
+ import type { Mixer } from './mixer.js';
2
+ import type { Subject } from './subject.js';
3
+ /**
4
+ * Gives a subject the free number `to` in place of its own, carrying everything kept under the old
5
+ * one, so `pull` can keep a list's numbers in its order: a list kept dense by swap-remove moves its
6
+ * last subject into the place a dropped one left, whose number the drop just freed. Says whether it
7
+ * did; it does not where some store it would have to carry is in use: while a fill runs, under
8
+ * history, in a read ahead, or with roles, a bound or anything owed on the lanes.
9
+ */
10
+ export declare function renumber<I, O>(mix: Mixer<I, O>, subject: I, head: Subject<unknown>, to: number): boolean;