@msb235/blits 0.14.0 → 0.15.0

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 (173) hide show
  1. package/CHANGELOG.md +154 -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 +15 -7
  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.js +7 -3
  23. package/dist/contribution.d.ts +30 -0
  24. package/dist/contribution.js +374 -0
  25. package/dist/copyout.d.ts +10 -0
  26. package/dist/copyout.js +69 -24
  27. package/dist/crowd.d.ts +8 -2
  28. package/dist/crowd.js +4 -5
  29. package/dist/cue.d.ts +8 -1
  30. package/dist/cue.js +39 -13
  31. package/dist/cut.d.ts +8 -0
  32. package/dist/cut.js +4 -0
  33. package/dist/drop.d.ts +9 -0
  34. package/dist/drop.js +84 -0
  35. package/dist/due.d.ts +24 -5
  36. package/dist/due.js +113 -33
  37. package/dist/easing.d.ts +4 -4
  38. package/dist/easing.js +3 -3
  39. package/dist/everyone.d.ts +3 -2
  40. package/dist/everyone.js +3 -3
  41. package/dist/fade.d.ts +2 -1
  42. package/dist/fade.js +2 -1
  43. package/dist/fill.d.ts +3 -2
  44. package/dist/fill.js +51 -19
  45. package/dist/fit.d.ts +44 -26
  46. package/dist/fit.js +10 -9
  47. package/dist/fold.d.ts +4 -37
  48. package/dist/fold.js +5 -513
  49. package/dist/gather.d.ts +12 -7
  50. package/dist/gather.js +179 -44
  51. package/dist/gone.js +2 -2
  52. package/dist/grid.d.ts +47 -0
  53. package/dist/grid.js +247 -0
  54. package/dist/handle.d.ts +2 -0
  55. package/dist/handle.js +14 -8
  56. package/dist/held.d.ts +2 -1
  57. package/dist/history.d.ts +6 -5
  58. package/dist/history.js +7 -9
  59. package/dist/hosts.js +9 -15
  60. package/dist/index.d.ts +5 -4
  61. package/dist/keyfill.d.ts +1 -1
  62. package/dist/keyfill.js +5 -3
  63. package/dist/keyrows.d.ts +12 -0
  64. package/dist/keyrows.js +99 -12
  65. package/dist/lane.d.ts +36 -6
  66. package/dist/lane.js +32 -1
  67. package/dist/lanes.d.ts +23 -8
  68. package/dist/lanes.js +29 -8
  69. package/dist/leavings.d.ts +2 -2
  70. package/dist/leavings.js +1 -1
  71. package/dist/lives.d.ts +47 -0
  72. package/dist/lives.js +180 -0
  73. package/dist/meet.d.ts +2 -2
  74. package/dist/meet.js +90 -4
  75. package/dist/mixer.d.ts +54 -36
  76. package/dist/mixer.js +53 -171
  77. package/dist/motion.d.ts +14 -118
  78. package/dist/motion.js +16 -8
  79. package/dist/motions.d.ts +62 -155
  80. package/dist/motions.js +56 -370
  81. package/dist/moved.d.ts +1 -1
  82. package/dist/moved.js +16 -7
  83. package/dist/named.d.ts +0 -1
  84. package/dist/named.js +20 -2
  85. package/dist/numbers.d.ts +2 -0
  86. package/dist/numbers.js +4 -0
  87. package/dist/owed.d.ts +2 -0
  88. package/dist/owed.js +4 -0
  89. package/dist/pack.d.ts +2 -1
  90. package/dist/pack.js +2 -3
  91. package/dist/patch.d.ts +7 -3
  92. package/dist/patch.js +6 -2
  93. package/dist/places.d.ts +53 -0
  94. package/dist/places.js +171 -0
  95. package/dist/plot.d.ts +12 -0
  96. package/dist/plot.js +52 -0
  97. package/dist/project.d.ts +4 -4
  98. package/dist/project.js +9 -212
  99. package/dist/pull.js +11 -8
  100. package/dist/qualify.js +84 -21
  101. package/dist/reading.d.ts +6 -0
  102. package/dist/reading.js +5 -0
  103. package/dist/record.d.ts +5 -1
  104. package/dist/record.js +3 -2
  105. package/dist/relink.d.ts +1 -1
  106. package/dist/restored.d.ts +2 -2
  107. package/dist/restored.js +1 -1
  108. package/dist/rests.d.ts +9 -0
  109. package/dist/rests.js +76 -0
  110. package/dist/revive.d.ts +3 -2
  111. package/dist/revive.js +2 -2
  112. package/dist/{locus.d.ts → role.d.ts} +10 -9
  113. package/dist/{locus.js → role.js} +3 -3
  114. package/dist/roles.d.ts +10 -0
  115. package/dist/roles.js +158 -0
  116. package/dist/roster.js +3 -3
  117. package/dist/rotation.d.ts +1 -1
  118. package/dist/rotation.js +1 -1
  119. package/dist/rows.js +1 -1
  120. package/dist/sample.d.ts +2 -2
  121. package/dist/sample.js +33 -7
  122. package/dist/seek.js +5 -5
  123. package/dist/setting.d.ts +27 -0
  124. package/dist/setting.js +55 -0
  125. package/dist/shared.d.ts +1 -1
  126. package/dist/shared.js +1 -1
  127. package/dist/signals.d.ts +64 -32
  128. package/dist/signals.js +20 -18
  129. package/dist/stack.d.ts +5 -5
  130. package/dist/stack.js +5 -5
  131. package/dist/standing.d.ts +2 -1
  132. package/dist/standing.js +3 -2
  133. package/dist/store.d.ts +9 -0
  134. package/dist/store.js +24 -0
  135. package/dist/strays.d.ts +2 -1
  136. package/dist/strays.js +20 -13
  137. package/dist/stretch.d.ts +112 -0
  138. package/dist/stretch.js +9 -0
  139. package/dist/subject.d.ts +142 -0
  140. package/dist/subject.js +1 -0
  141. package/dist/tape.d.ts +9 -5
  142. package/dist/tape.js +5 -1
  143. package/dist/then.d.ts +9 -0
  144. package/dist/then.js +57 -0
  145. package/dist/ticker.d.ts +4 -4
  146. package/dist/transport.d.ts +0 -8
  147. package/dist/transport.js +0 -4
  148. package/dist/turns.d.ts +4 -3
  149. package/dist/turns.js +4 -3
  150. package/dist/types/arc.d.ts +12 -11
  151. package/dist/types/channel.d.ts +12 -8
  152. package/dist/types/history.d.ts +35 -19
  153. package/dist/types/mix.d.ts +222 -163
  154. package/dist/types/motion.d.ts +151 -0
  155. package/dist/types/motion.js +1 -0
  156. package/dist/types/patch.d.ts +67 -34
  157. package/dist/types/place.d.ts +51 -36
  158. package/dist/types/transport.d.ts +20 -18
  159. package/dist/types/voice.d.ts +136 -70
  160. package/dist/types/watch.d.ts +88 -40
  161. package/dist/types.d.ts +1 -0
  162. package/dist/voice.d.ts +7 -143
  163. package/dist/voice.js +28 -79
  164. package/dist/warn.d.ts +2 -0
  165. package/dist/warn.js +1 -1
  166. package/dist/watch.d.ts +2 -2
  167. package/dist/watch.js +5 -3
  168. package/dist/wave.d.ts +4 -3
  169. package/dist/wave.js +3 -2
  170. package/dist/weigh.d.ts +7 -1
  171. package/dist/weigh.js +27 -0
  172. package/docs/schema.html +212 -34
  173. package/package.json +2 -2
package/dist/qualify.js CHANGED
@@ -5,6 +5,7 @@ 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';
10
11
  /**
@@ -57,21 +58,21 @@ export function requalify(lanes, version) {
57
58
  keepCallsOff(present, fits, host.channels);
58
59
  const candidates = () => present.map((v, i) => ({ id: v.id, fits: fits[i], slots: v.slots }));
59
60
  let { channels, voices } = qualify(numeric, candidates());
60
- // A locus folds its members together, so they run on lanes all together or not at all.
61
+ // A role folds its members together, so they run on lanes all together or not at all.
61
62
  for (let split = true; split;) {
62
63
  split = false;
63
- const loci = new Map();
64
+ const roles = new Map();
64
65
  present.forEach((v, i) => {
65
- const name = v.spec.locus;
66
+ const name = v.spec.role;
66
67
  if (name === undefined)
67
68
  return;
68
- const at = loci.get(name);
69
+ const at = roles.get(name);
69
70
  if (at === undefined)
70
- loci.set(name, [i]);
71
+ roles.set(name, [i]);
71
72
  else
72
73
  at.push(i);
73
74
  });
74
- for (const at of loci.values()) {
75
+ for (const at of roles.values()) {
75
76
  const on = at.filter((i) => voices.has(present[i].id)).length;
76
77
  if (on === 0 || on === at.length)
77
78
  continue;
@@ -97,18 +98,18 @@ export function requalify(lanes, version) {
97
98
  const lane = lanes.byId.get(v.id) ?? new Lane(v);
98
99
  v.laned = true;
99
100
  if (lane.epoch === 0 && v.state !== 'pending')
100
- lane.epoch = open(lanes, v);
101
+ opened(lanes, lane);
101
102
  kept.push(lane);
102
103
  }
103
104
  lanes.lanes = kept;
104
- lanes.loci = [];
105
- const loci = new Map();
105
+ lanes.roles = [];
106
+ const roles = new Map();
106
107
  for (const lane of kept) {
107
108
  lane.group = null;
108
- const name = lane.voice.spec.locus;
109
+ const name = lane.voice.spec.role;
109
110
  if (name === undefined)
110
111
  continue;
111
- let g = loci.get(name);
112
+ let g = roles.get(name);
112
113
  if (g === undefined) {
113
114
  g = {
114
115
  members: [],
@@ -117,13 +118,17 @@ export function requalify(lanes, version) {
117
118
  sum: new Float64Array(lanes.cap),
118
119
  values: [],
119
120
  taken: [],
121
+ contiguous: false,
122
+ inline: false,
120
123
  };
121
- loci.set(name, g);
122
- lanes.loci.push(g);
124
+ roles.set(name, g);
125
+ lanes.roles.push(g);
123
126
  }
124
127
  g.members.push(lane);
125
128
  lane.group = g;
126
129
  }
130
+ for (const [name, g] of roles)
131
+ g.contiguous = contiguous(present, voices, name, g.members);
127
132
  lanes.dense = kept
128
133
  .filter((l) => l.voice.named === null && l.epoch > 0)
129
134
  .sort((a, b) => a.epoch - b.epoch);
@@ -161,6 +166,7 @@ export function requalify(lanes, version) {
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;
@@ -318,8 +326,8 @@ export function retouch(lanes) {
318
326
  }
319
327
  if (v.state === 'done')
320
328
  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)
329
+ // A role's members are on lanes together or not at all, so one joining may move the others.
330
+ if (v.spec.role !== undefined)
323
331
  return qualify();
324
332
  const laned = v.slots.some((slot) => lanes.bySlot[slot] !== undefined);
325
333
  const fits = v.slots.length > 0 && lanes.host.fits(v);
@@ -341,6 +349,46 @@ export function retouch(lanes) {
341
349
  lanes.subjects = [];
342
350
  return true;
343
351
  }
352
+ /** A lane's voice started playing: its epoch, and room up front where it reaches every subject. */
353
+ function opened(lanes, lane) {
354
+ const v = lane.voice;
355
+ lane.epoch = open(lanes, v);
356
+ if (v.named === null && v.spec.target === undefined) {
357
+ lane.reserve(lanes.live, lanes.cap);
358
+ if (placesAll(lanes, lane))
359
+ placeAll(lanes, lane);
360
+ }
361
+ }
362
+ /**
363
+ * Whether a lane just opened can take every numbered subject now, its first call or read for each
364
+ * made by the fill: a stateless fn or keys voice sharing one record, which every subject would meet
365
+ * at its next probe, in a host that probed every subject last frame. One by one, each probe would
366
+ * meet it and make its first call or read there (`foldNow` in `meet.ts`).
367
+ */
368
+ function placesAll(lanes, lane) {
369
+ const v = lane.voice;
370
+ return (v.sharing &&
371
+ v.everyone !== null &&
372
+ lane.motion === undefined &&
373
+ !v.keeping &&
374
+ typeof v.spec.weight !== 'function' &&
375
+ lane.list.length === 0 &&
376
+ lanes.lastDistinct >= lanes.live);
377
+ }
378
+ /** Gives every numbered subject a position on a lane `placesAll` passed. */
379
+ function placeAll(lanes, lane) {
380
+ const rec = lane.voice.everyone;
381
+ const numbers = lanes.numbers;
382
+ const size = Math.min(numbers.size, lanes.cap);
383
+ for (let slot = 0; slot < size; slot++) {
384
+ if (!numbers.has(slot))
385
+ continue;
386
+ lane.add(slot, rec);
387
+ if (lane.idle)
388
+ reach(lanes, slot, 1);
389
+ }
390
+ lane.every = true;
391
+ }
344
392
  /**
345
393
  * A new voice that fits and writes only laned channels, and that no crowd takes, gets a lane at
346
394
  * the end of the order, its id being past every other: what a qualify would give it, since such a
@@ -350,7 +398,7 @@ function enlane(lanes, v) {
350
398
  const lane = new Lane(v);
351
399
  v.laned = true;
352
400
  if (v.state !== 'pending')
353
- lane.epoch = open(lanes, v);
401
+ opened(lanes, lane);
354
402
  lane.chans = v.slots.map((s) => lanes.bySlot[s]);
355
403
  lane.values = lane.chans.map(() => undefined);
356
404
  lanes.lanes.push(lane);
@@ -360,13 +408,13 @@ function enlane(lanes, v) {
360
408
  }
361
409
  /**
362
410
  * 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.
411
+ * cannot lane or unlane another channel. False for a role member, which a qualify must take.
364
412
  */
365
413
  function relane(lanes, lane) {
366
414
  const v = lane.voice;
367
415
  if (v.state !== 'done') {
368
416
  if (lane.epoch === 0 && v.state !== 'pending') {
369
- lane.epoch = open(lanes, v);
417
+ opened(lanes, lane);
370
418
  if (v.named === null)
371
419
  lanes.dense.push(lane);
372
420
  }
@@ -410,3 +458,18 @@ function leave(lanes, lane) {
410
458
  }
411
459
  lane.voice.laned = false;
412
460
  }
461
+ /**
462
+ * Whether a role's members, in the order lanes run, have no laned or crowded voice of another
463
+ * role or none between them, so its every subject folds at its first member's turn.
464
+ */
465
+ function contiguous(present, voices, name, members) {
466
+ for (let i = 1; i < members.length; i++)
467
+ if (members[i].voice.id <= members[i - 1].voice.id)
468
+ return false;
469
+ const lo = members[0].voice.id;
470
+ const hi = members[members.length - 1].voice.id;
471
+ for (const v of present)
472
+ if (v.id > lo && v.id < hi && voices.has(v.id) && v.spec.role !== name)
473
+ return false;
474
+ return true;
475
+ }
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;
@@ -1,7 +1,7 @@
1
+ import type { Cut } from './cut.js';
1
2
  import type { Leavings } from './leavings.js';
2
3
  import { Store } from './store.js';
3
- import type { Cut } from './transport.js';
4
- import type { Subject } from './voice.js';
4
+ import type { Subject } from './subject.js';
5
5
  /**
6
6
  * The seeks back after the one a record was last put back, written or deleted at, which every
7
7
  * record stamped then shares. The next seek fills it; a later seek to as early a moment joins it
package/dist/restored.js CHANGED
@@ -138,7 +138,7 @@ export class Restored extends Store {
138
138
  }
139
139
  /** `get`, given what `own` holds for `key`. */
140
140
  from(key, own) {
141
- const mark = own !== undefined && own.reaches;
141
+ const mark = own?.reaches;
142
142
  const at = (mark ? own.restored : this.at.get(key)) ?? this.first;
143
143
  const now = this.stamp;
144
144
  if (at === now)
@@ -0,0 +1,9 @@
1
+ import type { Mixer } from './mixer.js';
2
+ import type { Voice } from './voice.js';
3
+ export declare function atRest<I, O>(this: Mixer<I, O>, subject: I): boolean;
4
+ export declare function rests<I, O>(this: Mixer<I, O>, pose: O): boolean;
5
+ export declare function restful<I, O>(this: Mixer<I, O>): boolean;
6
+ /** Whether a voice will change no pose from `now` on, short of a change made to it. */
7
+ export declare function still<I, O>(this: Mixer<I, O>, voice: Voice<I, O>, now: number): boolean;
8
+ export declare function restsLaned<I, O>(this: Mixer<I, O>): boolean;
9
+ export declare function restsKey<I, O>(this: Mixer<I, O>): number;
package/dist/rests.js ADDED
@@ -0,0 +1,76 @@
1
+ import { near } from './fold.js';
2
+ import { Store } from './store.js';
3
+ export function atRest(subject) {
4
+ this.restStamps ??= new Store();
5
+ const head = this.linked(subject);
6
+ const laned = this.linkedLaned;
7
+ const lanes = this.lanes;
8
+ // Every voice on lanes and nothing to clamp: the pose a fold would make is the lanes' values.
9
+ if (laned && this.restsLaned()) {
10
+ const slot = head.slot;
11
+ if (!lanes.owes(slot))
12
+ return lanes.rests(slot);
13
+ }
14
+ // A probe this frame answers for the pose it gave the host.
15
+ const since = (this.restStamps.get(subject) ?? Number.NaN) - 2 * this.restsKey();
16
+ if (since >= 0)
17
+ return since === 1;
18
+ this.restScratch ??= this.pose();
19
+ const pose = this.foldWith(subject, this.restScratch, head, laned, true);
20
+ return this.rests((this.bounded.length === 0 ? pose : this.clamp(pose)));
21
+ }
22
+ export function rests(pose) {
23
+ for (let i = 0; i < this.names.length; i++) {
24
+ const channel = this.channels[i];
25
+ const value = pose[this.names[i]];
26
+ if (channel.rest === undefined) {
27
+ if (value !== undefined)
28
+ return false;
29
+ }
30
+ else if (!near(value, channel.rest))
31
+ return false;
32
+ }
33
+ return true;
34
+ }
35
+ export function restful() {
36
+ // Standing still, a voice changes no pose but by its weight signal, or by an anchor or start
37
+ // that host time may yet reach.
38
+ if (this.pace?.stopped(this.u))
39
+ return this.cued.every((v) => v.state === 'done' ||
40
+ (v.state !== 'pending' &&
41
+ typeof v.spec.weight !== 'function' &&
42
+ v.spec.anchor === undefined));
43
+ return this.cued.every((v) => this.still(v, this.now));
44
+ }
45
+ /** Whether a voice will change no pose from `now` on, short of a change made to it. */
46
+ export function still(voice, now) {
47
+ if (voice.state === 'done')
48
+ return true;
49
+ if (voice.state !== 'frozen' && voice.state !== 'live')
50
+ return false;
51
+ if (typeof voice.spec.weight === 'function' || voice.parts !== null)
52
+ return false;
53
+ const fadeIn = this.reducedNow ? 0 : (voice.fade.in ?? 0);
54
+ if (fadeIn > 0 && now - this.sinceOf(voice, voice.latest) < fadeIn)
55
+ return false;
56
+ if (voice.state === 'frozen' || voice.holding !== null)
57
+ return true;
58
+ const motion = voice.motion;
59
+ return motion !== undefined && voice.elapsedAt(now) >= voice.latest && motion.landed();
60
+ }
61
+ export function restsLaned() {
62
+ return this.lanes.whole && this.bounded.length === 0;
63
+ }
64
+ export function restsKey() {
65
+ if (this.frame !== this.restsFrame ||
66
+ this.version !== this.restsVersion ||
67
+ this.relinks !== this.restsRelinks ||
68
+ this.stirs !== this.restsStirs) {
69
+ this.restsEpoch++;
70
+ this.restsFrame = this.frame;
71
+ this.restsVersion = this.version;
72
+ this.restsRelinks = this.relinks;
73
+ this.restsStirs = this.stirs;
74
+ }
75
+ return this.restsEpoch;
76
+ }
package/dist/revive.d.ts CHANGED
@@ -1,10 +1,11 @@
1
1
  import type { Mixer } from './mixer.js';
2
- import type { Run } from './motions.js';
3
2
  import type { Past } from './origin.js';
4
3
  import { type PackedRecord } from './pack.js';
5
4
  import { type Key } from './paging.js';
5
+ import type { Run } from './stretch.js';
6
+ import type { Parted, Parting } from './subject.js';
6
7
  import type { Described } from './types.js';
7
- import { type Controls, type Parted, type Parting, type Ramp, type Rise, Voice } from './voice.js';
8
+ import { type Controls, type Ramp, type Rise, Voice } from './voice.js';
8
9
  /** A voice that has left, as plain data: the descriptor it was cued with, its controls and its records. */
9
10
  interface PackedVoice {
10
11
  as: Described;
package/dist/revive.js CHANGED
@@ -1,4 +1,4 @@
1
- import { playable } from './cue.js';
1
+ import { playable, roleOf } from './cue.js';
2
2
  import { VoiceHandle } from './handle.js';
3
3
  import { handle } from './hosts.js';
4
4
  import { Leavings } from './leavings.js';
@@ -120,7 +120,7 @@ export function reviveVoice(mix, id, d) {
120
120
  const revive = mix.opts.history?.revive;
121
121
  if (revive === undefined)
122
122
  throw new Error('blits: a paged voice needs history.revive');
123
- const spec = { ...revive(d.as), as: d.as };
123
+ const spec = roleOf(mix, { ...revive(d.as), as: d.as });
124
124
  playable(mix, spec);
125
125
  const voice = new Voice(id, spec, spec.patch, spec.fade ?? {}, Number.NEGATIVE_INFINITY, d.start, mix.slotOf, mix.channels, mix.opts.host, mix.send, null);
126
126
  voice.clocks = d.clocks;
@@ -1,11 +1,12 @@
1
1
  import type { Mixer } from './mixer.js';
2
- import type { Subject, Voice } from './voice.js';
2
+ import type { Subject } from './subject.js';
3
+ import type { Voice } from './voice.js';
3
4
  /**
4
- * What a fold with a locus in play gathers, kept from one fold to the next: the contributions in
5
- * voice order, each a voice's own or a locus's first member; every locus member; and per channel
6
- * slot, a locus's folding value, the weight taken into it and the array it owns.
5
+ * What a fold with a role in play gathers, kept from one fold to the next: the contributions in
6
+ * voice order, each a voice's own or a role's first member; every role member; and per channel
7
+ * slot, a role's folding value, the weight taken into it and the array it owns.
7
8
  */
8
- export interface LocusScratch<I, O> {
9
+ export interface RoleScratch<I, O> {
9
10
  n: number;
10
11
  voices: (Voice<I, O> | null)[];
11
12
  helds: (Subject<unknown> | null)[];
@@ -27,17 +28,17 @@ export interface LocusScratch<I, O> {
27
28
  touched: number[];
28
29
  owned: (number[] | undefined)[];
29
30
  }
30
- export declare function locusScratch<I, O>(channels: number): LocusScratch<I, O>;
31
+ export declare function roleScratch<I, O>(channels: number): RoleScratch<I, O>;
31
32
  /**
32
33
  * `value` copied into the array `k` owns for the slot, as long as the channel's rest, where a
33
34
  * shorter value leaves the rest's own fill: what the channel's fold and lerp read past its end.
34
35
  */
35
- export declare function own<I, O>(k: LocusScratch<I, O>, slot: number, value: number[], rest: number[]): number[];
36
+ export declare function own<I, O>(k: RoleScratch<I, O>, slot: number, value: number[], rest: number[]): number[];
36
37
  /**
37
- * Folds one locus's members into one contribution through each channel's own `lerp`: its value per
38
+ * Folds one role's members into one contribution through each channel's own `lerp`: its value per
38
39
  * channel goes in `k.values`, the channels in `k.touched` in the order first met, and its weight
39
40
  * is returned. A stock `last` channel has nothing to lerp, so it takes its heaviest member's value,
40
41
  * the later cued of two as heavy. A channel folding by scale takes its value in an array of `k`'s own, lerped in
41
42
  * place, since the fold only reads it.
42
43
  */
43
- export declare function foldLocus<I, O>(this: Mixer<I, O>, k: LocusScratch<I, O>, group: number): number;
44
+ export declare function foldRole<I, O>(this: Mixer<I, O>, k: RoleScratch<I, O>, group: number): number;
@@ -1,5 +1,5 @@
1
1
  import { first } from './clone.js';
2
- export function locusScratch(channels) {
2
+ export function roleScratch(channels) {
3
3
  return {
4
4
  n: 0,
5
5
  voices: [],
@@ -36,13 +36,13 @@ export function own(k, slot, value, rest) {
36
36
  return out;
37
37
  }
38
38
  /**
39
- * Folds one locus's members into one contribution through each channel's own `lerp`: its value per
39
+ * Folds one role's members into one contribution through each channel's own `lerp`: its value per
40
40
  * channel goes in `k.values`, the channels in `k.touched` in the order first met, and its weight
41
41
  * is returned. A stock `last` channel has nothing to lerp, so it takes its heaviest member's value,
42
42
  * the later cued of two as heavy. A channel folding by scale takes its value in an array of `k`'s own, lerped in
43
43
  * place, since the fold only reads it.
44
44
  */
45
- export function foldLocus(k, group) {
45
+ export function foldRole(k, group) {
46
46
  k.touched.length = 0;
47
47
  let sum = 0;
48
48
  for (let m = 0; m < k.m; m++)
@@ -0,0 +1,10 @@
1
+ import type { Mixer } from './mixer.js';
2
+ import type { Subject } from './subject.js';
3
+ import type { Voice } from './voice.js';
4
+ /**
5
+ * Whether a fold's next voice is its chain's next record, `held`, and not the next voice sharing
6
+ * one record, `sharers[q]`: the two lists are each in voice order, and a fold takes them as one.
7
+ */
8
+ export declare function chainFirst<I, O>(held: Subject<unknown> | null, sharers: readonly Voice<I, O>[], q: number): boolean;
9
+ /** `foldWith`'s voices with a role in play: each role folds at its first member's place. */
10
+ export declare function foldRoles<I, O>(this: Mixer<I, O>, subject: I, pose: Record<string, unknown>, head: Subject<unknown>, laned: boolean, dry: boolean, except: number | undefined, owed: number): O;
package/dist/roles.js ADDED
@@ -0,0 +1,158 @@
1
+ import { first } from './clone.js';
2
+ import { open as openRecord } from './everyone.js';
3
+ import { merged, unband } from './fold.js';
4
+ import { roleScratch } from './role.js';
5
+ // V8 reads a local const as a constant, where it reads an import from its module on every call.
6
+ const open = openRecord;
7
+ /**
8
+ * Whether a fold's next voice is its chain's next record, `held`, and not the next voice sharing
9
+ * one record, `sharers[q]`: the two lists are each in voice order, and a fold takes them as one.
10
+ */
11
+ export function chainFirst(held, sharers, q) {
12
+ if (held === null)
13
+ return false;
14
+ if (q >= sharers.length || held.voice === null)
15
+ return true;
16
+ const next = sharers[q];
17
+ return next.id > held.voice.id;
18
+ }
19
+ /** `foldWith`'s voices with a role in play: each role folds at its first member's place. */
20
+ export function foldRoles(subject, pose, head, laned, dry, except, owed) {
21
+ const now = this.now;
22
+ // A patch reading the pose can fold again from inside this one, so each depth has its own.
23
+ let k = this.roleScratch[this.roleDepth];
24
+ if (k === undefined) {
25
+ k = roleScratch(this.channels.length);
26
+ this.roleScratch[this.roleDepth] = k;
27
+ }
28
+ this.roleDepth++;
29
+ try {
30
+ k.n = 0;
31
+ k.m = 0;
32
+ k.names.length = 0;
33
+ const sharers = this.sharers;
34
+ let q = 0;
35
+ let at = head;
36
+ while (at !== null || q < sharers.length) {
37
+ const mine = chainFirst(at, sharers, q);
38
+ let voice;
39
+ let rec;
40
+ if (mine) {
41
+ rec = at;
42
+ voice = rec.voice;
43
+ at = rec.next;
44
+ }
45
+ else {
46
+ voice = sharers[q++];
47
+ rec = voice.state === 'pending' ? null : voice.everyone;
48
+ }
49
+ if (voice === null || rec === null)
50
+ continue;
51
+ if (laned && voice.laned && !this.owedBy(owed, voice))
52
+ continue;
53
+ if (voice.id === except || voice.state === 'done')
54
+ continue;
55
+ const held = rec;
56
+ if (!mine)
57
+ open(voice, held, head.slot);
58
+ const delta = this.read(voice, subject, now, dry, held);
59
+ if (owed >= 0 && voice.laned)
60
+ this.lanes.paid(voice.id, owed, held.weight);
61
+ if (!mine)
62
+ q = this.shared(voice, head.slot, held, q);
63
+ if (delta === null)
64
+ continue;
65
+ const role = voice.spec.role;
66
+ let group = -1;
67
+ if (role !== undefined) {
68
+ group = k.names.indexOf(role);
69
+ const m = k.m++;
70
+ k.mVoices[m] = voice;
71
+ k.mDeltas[m] = delta;
72
+ k.mWeights[m] = this.w;
73
+ if (group >= 0) {
74
+ k.mGroups[m] = group;
75
+ continue;
76
+ }
77
+ group = k.names.length;
78
+ k.names.push(role);
79
+ k.mGroups[m] = group;
80
+ }
81
+ const n = k.n++;
82
+ k.voices[n] = voice;
83
+ k.helds[n] = held;
84
+ k.deltas[n] = delta;
85
+ k.weights[n] = role === undefined ? this.w : 0;
86
+ k.groups[n] = group;
87
+ }
88
+ let bands = head.roles;
89
+ if (bands === null) {
90
+ bands = new Map();
91
+ head.roles = bands;
92
+ }
93
+ for (let n = 0; n < k.n; n++) {
94
+ const group = k.groups[n];
95
+ if (group < 0) {
96
+ const weight = k.weights[n];
97
+ if (weight > 0)
98
+ this.apply(pose, k.voices[n], k.helds[n], k.deltas[n], weight);
99
+ else
100
+ unband(k.helds[n]);
101
+ continue;
102
+ }
103
+ const weight = this.foldRole(k, group);
104
+ const name = k.names[group];
105
+ if (!(weight > 0)) {
106
+ bands.get(name)?.fill(2);
107
+ continue;
108
+ }
109
+ let on = bands.get(name);
110
+ if (on === undefined) {
111
+ on = new Uint8Array(this.channels.length);
112
+ bands.set(name, on);
113
+ }
114
+ for (let t = 0; t < k.touched.length; t++) {
115
+ const slot = k.touched[t];
116
+ const value = k.values[slot];
117
+ k.values[slot] = undefined;
118
+ const key = this.names[slot];
119
+ const channel = this.channels[slot];
120
+ // A channel folds at the weight of the members that wrote it, not the whole role.
121
+ const taken = k.taken[slot];
122
+ const w = taken > 1 ? 1 : taken;
123
+ if (channel.rest !== undefined && channel.scale) {
124
+ // pose[key] is the copy of rest this fold made, so it is ours to write into.
125
+ pose[key] = channel.fold
126
+ ? channel.fold(pose[key], value, w)
127
+ : merged(channel, pose[key], channel.scale(value, w));
128
+ continue;
129
+ }
130
+ const band = on[slot];
131
+ const passes = this.passes(band === 0 ? undefined : band === 1, w);
132
+ on[slot] = passes ? 1 : 2;
133
+ if (passes)
134
+ pose[key] = pose[key] === undefined ? first(value) : merged(channel, pose[key], value);
135
+ }
136
+ }
137
+ return pose;
138
+ }
139
+ finally {
140
+ for (let n = 0; n < k.n; n++) {
141
+ k.voices[n] = null;
142
+ k.helds[n] = null;
143
+ k.deltas[n] = null;
144
+ }
145
+ for (let m = 0; m < k.m; m++) {
146
+ k.mVoices[m] = null;
147
+ k.mDeltas[m] = null;
148
+ }
149
+ // Left set only where a channel's lerp threw partway, which the next fold must not inherit.
150
+ for (let t = 0; t < k.touched.length; t++) {
151
+ const slot = k.touched[t];
152
+ k.met[slot] = 0;
153
+ k.values[slot] = undefined;
154
+ }
155
+ k.touched.length = 0;
156
+ this.roleDepth--;
157
+ }
158
+ }
package/dist/roster.js CHANGED
@@ -2,8 +2,8 @@ import { index, unindex } from './chain.js';
2
2
  import { detour } from './everyone.js';
3
3
  import { Store } from './store.js';
4
4
  function tally(mix, voice, by) {
5
- if (voice.spec.locus !== undefined)
6
- mix.loci += by;
5
+ if (voice.spec.role !== undefined)
6
+ mix.roleVoices += by;
7
7
  const e = voice.spec.earliest;
8
8
  if (voice.spec.anchor !== undefined || (e !== undefined && typeof e !== 'number'))
9
9
  mix.anchored += by;
@@ -36,7 +36,7 @@ export function recount(mix) {
36
36
  mix.naming = 0;
37
37
  mix.general = [];
38
38
  mix.sharers = [];
39
- mix.loci = 0;
39
+ mix.roleVoices = 0;
40
40
  mix.anchored = 0;
41
41
  for (const voice of mix.cued) {
42
42
  index(mix, voice);