@msb235/blits 0.15.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 (51) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/README.md +1 -1
  3. package/dist/bare.js +22 -7
  4. package/dist/columns.d.ts +2 -1
  5. package/dist/columns.js +55 -35
  6. package/dist/crowd.d.ts +8 -4
  7. package/dist/crowd.js +17 -7
  8. package/dist/delay.d.ts +3 -0
  9. package/dist/delay.js +7 -0
  10. package/dist/due.js +1 -1
  11. package/dist/easing.d.ts +2 -0
  12. package/dist/easing.js +4 -0
  13. package/dist/everyone.d.ts +5 -0
  14. package/dist/everyone.js +30 -0
  15. package/dist/fill.js +1 -1
  16. package/dist/held.js +2 -1
  17. package/dist/index.d.ts +1 -1
  18. package/dist/lane.d.ts +2 -5
  19. package/dist/lane.js +14 -10
  20. package/dist/lanes.d.ts +7 -4
  21. package/dist/lanes.js +27 -28
  22. package/dist/meet.js +5 -3
  23. package/dist/mixer.d.ts +2 -1
  24. package/dist/mixer.js +4 -0
  25. package/dist/named.d.ts +6 -1
  26. package/dist/named.js +29 -1
  27. package/dist/numbers.d.ts +5 -0
  28. package/dist/numbers.js +29 -0
  29. package/dist/pull.js +11 -1
  30. package/dist/qualify.js +9 -10
  31. package/dist/renumber.d.ts +10 -0
  32. package/dist/renumber.js +75 -0
  33. package/dist/rows.js +27 -19
  34. package/dist/seat.d.ts +17 -0
  35. package/dist/seat.js +25 -0
  36. package/dist/shared.d.ts +1 -1
  37. package/dist/shared.js +1 -1
  38. package/dist/strays.js +3 -2
  39. package/dist/trace.d.ts +4 -0
  40. package/dist/trace.js +255 -0
  41. package/dist/types/mix.d.ts +15 -0
  42. package/dist/types/trace.d.ts +51 -0
  43. package/dist/types/trace.js +1 -0
  44. package/dist/types/voice.d.ts +10 -5
  45. package/dist/types.d.ts +1 -0
  46. package/dist/unreached.d.ts +2 -0
  47. package/dist/unreached.js +7 -0
  48. package/dist/voice.d.ts +3 -0
  49. package/dist/voice.js +9 -3
  50. package/docs/schema.html +11 -0
  51. package/package.json +1 -1
package/dist/lanes.js CHANGED
@@ -10,6 +10,7 @@ import { Placed } from './placed.js';
10
10
  import { requalify, retouch } from './qualify.js';
11
11
  import { reading } from './reading.js';
12
12
  import { reportedRow, unplace } from './rows.js';
13
+ import { seatOf } from './seat.js';
13
14
  /**
14
15
  * The lanes of one mix: subject numbers, which channels and voices run on lanes, and the values a
15
16
  * frame's fill leaves for probes to copy.
@@ -24,7 +25,7 @@ export class Lanes {
24
25
  byId = new Map();
25
26
  /** The crowds of single-subject voices, one per list of channels, and which holds each voice. */
26
27
  crowds = [];
27
- crowdOf = new Map();
28
+ seats = new Map();
28
29
  /** Which crowd voices' rows are placed on each subject. */
29
30
  placed = new Placed();
30
31
  /**
@@ -115,14 +116,16 @@ export class Lanes {
115
116
  */
116
117
  late = [];
117
118
  /**
118
- * What `writeLater` and `pullRun` have queued, for one set of columns, as runs: each run's first
119
- * lane slot, the row it goes to, and how many subjects follow both one apart.
119
+ * What `writeLater` and `pullRun` have queued, for one set of columns, as runs of rows: each
120
+ * run's first row and how many follow, and its first lane slot where its slots follow one apart
121
+ * too, else -1, when each row's slot is read from `queueFrom` by row.
120
122
  */
121
123
  queueSlots = new Int32Array(0);
122
124
  queueRows = new Int32Array(0);
123
125
  queueCounts = new Int32Array(0);
124
126
  queued = 0;
125
127
  queuedFor = null;
128
+ queueFrom = null;
126
129
  keeps = false;
127
130
  now = Number.NaN;
128
131
  filledAt = Number.NaN;
@@ -233,10 +236,9 @@ export class Lanes {
233
236
  lane.remove(slot);
234
237
  return;
235
238
  }
236
- const c = this.crowdOf.get(id);
237
- const p = c?.rowOf.get(id);
238
- if (c !== undefined && p !== undefined && c.list[p] === slot)
239
- unplace(this, c, p);
239
+ const s = this.seats.get(id);
240
+ if (s !== undefined && s.crowd.list[s.row] === slot)
241
+ unplace(this, s.crowd, s.row);
240
242
  }
241
243
  /** A voice brought a subject back: its next probe meets every lane again, as it met them first. */
242
244
  rejoin(slot) {
@@ -254,9 +256,8 @@ export class Lanes {
254
256
  const lane = this.byId.get(id);
255
257
  if (lane !== undefined)
256
258
  return this.reported(lane, slot);
257
- const c = this.crowdOf.get(id);
258
- const p = c?.rowOf.get(id);
259
- return c === undefined || p === undefined ? undefined : reportedRow(this, c, p, slot);
259
+ const s = this.seats.get(id);
260
+ return s === undefined ? undefined : reportedRow(this, s.crowd, s.row, slot);
260
261
  }
261
262
  reported(lane, slot) {
262
263
  const o = slot * 6 /* Per.SLOT */;
@@ -439,10 +440,9 @@ export class Lanes {
439
440
  lane.remove(slot);
440
441
  const placed = this.placed;
441
442
  for (let id = placed.any(slot); id !== 0; id = placed.any(slot)) {
442
- const c = this.crowdOf.get(id);
443
- const p = c?.rowOf.get(id);
444
- if (c !== undefined && p !== undefined && c.list[p] === slot)
445
- unplace(this, c, p);
443
+ const s = this.seats.get(id);
444
+ if (s !== undefined && s.crowd.list[s.row] === slot)
445
+ unplace(this, s.crowd, s.row);
446
446
  else
447
447
  placed.remove(slot, id);
448
448
  }
@@ -474,10 +474,10 @@ export class Lanes {
474
474
  * next fill, and a lane keeps the sample the old one gave.
475
475
  */
476
476
  stretchChanged(id, s) {
477
- const c = this.crowdOf.get(id);
478
- const p = c?.rowOf.get(id);
479
- if (c !== undefined && p !== undefined) {
480
- c.restale(p, c.hot[p * c.stride + 0 /* Hot.FLAGS */]);
477
+ const seat = this.seats.get(id);
478
+ if (seat !== undefined) {
479
+ const c = seat.crowd;
480
+ c.restale(seat.row, c.hot[seat.row * c.stride + 0 /* Hot.FLAGS */]);
481
481
  return;
482
482
  }
483
483
  const lane = this.byId.get(id);
@@ -502,12 +502,11 @@ export class Lanes {
502
502
  }
503
503
  /** A voice's clock moved the `shown` of records it had already placed: copies them again. */
504
504
  reshown(id) {
505
- const c = this.crowdOf.get(id);
506
- const p = c?.rowOf.get(id);
507
- if (c !== undefined && p !== undefined) {
508
- const held = c.records[p];
505
+ const s = this.seats.get(id);
506
+ if (s !== undefined) {
507
+ const held = s.crowd.records[s.row];
509
508
  if (held !== undefined)
510
- c.data[p * 8 /* Row.STRIDE */ + 1 /* Row.SINCE */] = held.shown;
509
+ s.crowd.data[s.row * 8 /* Row.STRIDE */ + 1 /* Row.SINCE */] = held.shown;
511
510
  }
512
511
  const lane = this.byId.get(id);
513
512
  if (lane === undefined)
@@ -519,12 +518,12 @@ export class Lanes {
519
518
  }
520
519
  }
521
520
  /** Something on a voice changed: a crowd copies it again at the next fill. */
522
- voiceChanged(id) {
523
- const c = this.crowdOf.get(id);
524
- const p = c?.rowOf.get(id);
525
- if (c === undefined || p === undefined)
521
+ voiceChanged(voice) {
522
+ const s = seatOf(this, voice);
523
+ if (s === null)
526
524
  return;
527
- const f = p * c.stride + 0 /* Hot.FLAGS */;
525
+ const c = s.crowd;
526
+ const f = s.row * c.stride + 0 /* Hot.FLAGS */;
528
527
  c.hot[f] = c.hot[f] | 64 /* Flag.VOICE */;
529
528
  }
530
529
  }
package/dist/meet.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { open, shut } from './everyone.js';
2
+ import { seatOf } from './seat.js';
2
3
  /**
3
4
  * Whether the lanes a probe of the subject at `slot` just met (`met`) can be folded on the general
4
5
  * path after the lanes' values: none is in a role or a crowd, the subject owes none already this
@@ -183,10 +184,11 @@ function foldNow(lanes, slot, subject, again) {
183
184
  }
184
185
  /** Places a crowd voice's row on the subject a probe met, as `Lane.add` adds a position. */
185
186
  function place(lanes, voice, slot, subject, from, head) {
186
- const c = lanes.crowdOf.get(voice.id);
187
- const p = c?.rowOf.get(voice.id);
188
- if (c === undefined || p === undefined)
187
+ const s = seatOf(lanes, voice);
188
+ if (s === null)
189
189
  return false;
190
+ const c = s.crowd;
191
+ const p = s.row;
190
192
  const h = p * c.stride;
191
193
  const flags = c.hot[h + 0 /* Hot.FLAGS */];
192
194
  if (c.hot[h + 6 /* Hot.EPOCH */] <= from || (flags & 16 /* Flag.PLACED */) !== 0)
package/dist/mixer.d.ts CHANGED
@@ -32,7 +32,7 @@ import { Store } from './store.js';
32
32
  import type { MotionOwner } from './stretch.js';
33
33
  import type { Subject } from './subject.js';
34
34
  import { type Announced, Transport } from './transport.js';
35
- import type { ArcHandle, ArcSpec, BlendMember, Booker, BookOptions, Channel, Columns, Engine, Handle, Kit, Marked, Mix, MixOptions, OwnerSpec, Patch, Projection, Sent, Signal, SpanHandle, SpanSpec, Tape, VoiceSpec } from './types.js';
35
+ import type { ArcHandle, ArcSpec, BlendMember, Booker, BookOptions, Channel, Columns, Engine, Handle, Kit, Marked, Mix, MixOptions, OwnerSpec, Patch, Projection, Sent, Signal, SpanHandle, SpanSpec, Tape, Trace, TraceOptions, VoiceSpec } from './types.js';
36
36
  import { type Voice } from './voice.js';
37
37
  import { base, capped, fadeOf, horizonFor, ownedBy, ownerBase, prime, primeLaned, weigh, weighOwned } from './weigh.js';
38
38
  /**
@@ -266,6 +266,7 @@ export declare class Mixer<I, O> implements Mix<I, O> {
266
266
  project(at: number | {
267
267
  readonly frame: number;
268
268
  }): Projection<I, O>;
269
+ trace(from: number, to: number, opts?: TraceOptions): Trace<I, O>;
269
270
  announce(name: string, opts?: {
270
271
  at?: number;
271
272
  score?: string;
package/dist/mixer.js CHANGED
@@ -33,6 +33,7 @@ import { Fitting, refit } from './spans.js';
33
33
  import { Store } from './store.js';
34
34
  import { record } from './tape.js';
35
35
  import { touch } from './touch.js';
36
+ import { traceMix } from './trace.js';
36
37
  import { announce, Transport } from './transport.js';
37
38
  import { none } from './voice.js';
38
39
  import { gave, historyFor, register, unregister } from './watch.js';
@@ -435,6 +436,9 @@ export class Mixer {
435
436
  project(at) {
436
437
  return projectAll(this.transport, at).of(this);
437
438
  }
439
+ trace(from, to, opts) {
440
+ return traceMix(this, from, to, opts);
441
+ }
438
442
  announce(name, opts = {}) {
439
443
  this.attached();
440
444
  announce(this.transport, name, opts, this.slot, this.name);
package/dist/named.d.ts CHANGED
@@ -5,7 +5,12 @@
5
5
  export declare class Named<I> {
6
6
  readonly list: readonly I[];
7
7
  private readonly set;
8
- constructor(subjects: Iterable<I>);
8
+ /** Each subject's `stagger` answer, asked at cue; null for a voice with no stagger. */
9
+ readonly delays: Map<I, number> | null;
10
+ /** The longest of `delays`, 0 at least, and the least, 0 at most: a voice's bounds at cue. */
11
+ readonly latest: number;
12
+ readonly soonest: number;
13
+ constructor(subjects: Iterable<I>, stagger?: (subject: I) => number);
9
14
  get size(): number;
10
15
  has(subject: I): boolean;
11
16
  }
package/dist/named.js CHANGED
@@ -7,11 +7,19 @@ const FEW = 8;
7
7
  export class Named {
8
8
  list;
9
9
  set;
10
- constructor(subjects) {
10
+ /** Each subject's `stagger` answer, asked at cue; null for a voice with no stagger. */
11
+ delays = null;
12
+ /** The longest of `delays`, 0 at least, and the least, 0 at most: a voice's bounds at cue. */
13
+ latest = 0;
14
+ soonest = 0;
15
+ constructor(subjects, stagger) {
11
16
  // An array of distinct subjects, as nearly every voice names, is copied without iterating it.
12
17
  if (Array.isArray(subjects) && distinct(subjects)) {
13
18
  this.list = subjects.slice();
14
19
  this.set = null;
20
+ if (stagger)
21
+ this.delays = delaysOf(this.list, stagger);
22
+ [this.latest, this.soonest] = boundsOf(this.delays);
15
23
  return;
16
24
  }
17
25
  const list = [];
@@ -32,6 +40,9 @@ export class Named {
32
40
  // Exactly as long as it is: a list grown by push keeps room for 17.
33
41
  this.list = list.slice();
34
42
  this.set = set;
43
+ if (stagger)
44
+ this.delays = delaysOf(this.list, stagger);
45
+ [this.latest, this.soonest] = boundsOf(this.delays);
35
46
  }
36
47
  get size() {
37
48
  return this.list.length;
@@ -40,6 +51,23 @@ export class Named {
40
51
  return this.set === null ? this.list.includes(subject) : this.set.has(subject);
41
52
  }
42
53
  }
54
+ function delaysOf(list, stagger) {
55
+ const delays = new Map();
56
+ for (const s of list)
57
+ delays.set(s, stagger(s));
58
+ return delays;
59
+ }
60
+ function boundsOf(delays) {
61
+ let latest = 0;
62
+ let soonest = 0;
63
+ for (const delay of delays?.values() ?? []) {
64
+ if (delay > latest)
65
+ latest = delay;
66
+ if (delay < soonest)
67
+ soonest = delay;
68
+ }
69
+ return [latest, soonest];
70
+ }
43
71
  /** Whether a short array names no subject twice, as `includes` compares; a long one says no. */
44
72
  function distinct(subjects) {
45
73
  const n = subjects.length;
package/dist/numbers.d.ts CHANGED
@@ -36,5 +36,10 @@ export declare class Numbers<I> {
36
36
  /** Whether `slot` is a number in use, its subject perhaps collected and not yet released. */
37
37
  has(slot: number): boolean;
38
38
  subject(slot: number): I | typeof absent;
39
+ /**
40
+ * Hands the subject numbered `from` the free number `to`, and frees `from` without telling the
41
+ * owner, whose stores the caller moves; false, changing nothing, where either does not hold.
42
+ */
43
+ move(from: number, to: number): boolean;
39
44
  release(slot: number): void;
40
45
  }
package/dist/numbers.js CHANGED
@@ -82,6 +82,35 @@ export class Numbers {
82
82
  }
83
83
  return ref;
84
84
  }
85
+ /**
86
+ * Hands the subject numbered `from` the free number `to`, and frees `from` without telling the
87
+ * owner, whose stores the caller moves; false, changing nothing, where either does not hold.
88
+ */
89
+ move(from, to) {
90
+ const ref = this.refs[from];
91
+ const free = this.free;
92
+ if (ref === undefined || to >= this.next || this.refs[to] !== undefined || free === null)
93
+ return false;
94
+ const at = free.lastIndexOf(to);
95
+ if (at < 0)
96
+ return false;
97
+ if (ref instanceof WeakRef) {
98
+ const subject = ref.deref();
99
+ if (subject === undefined)
100
+ return false;
101
+ if (ref === this.lone)
102
+ this.loneSlot = to;
103
+ else if (this.registry !== null) {
104
+ this.registry.unregister(ref);
105
+ this.registry.register(subject, to, ref);
106
+ }
107
+ }
108
+ free.splice(at, 1);
109
+ free.push(from);
110
+ this.refs[to] = ref;
111
+ this.refs[from] = undefined;
112
+ return true;
113
+ }
85
114
  release(slot) {
86
115
  const ref = this.refs[slot];
87
116
  if (ref === undefined)
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. */
@@ -152,12 +153,21 @@ function readAll(mix, list, columns, live, current, since, whole) {
152
153
  was[n] = subject;
153
154
  heads[n] = head;
154
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
+ }
155
165
  slots[n] = slot;
156
166
  }
157
167
  }
158
168
  const laned = live && lanes?.prepare(slot, subject, mix.version, heads[n] ?? null) === true;
159
169
  if (laned && whole && lanes.whole && !lanes.owes(slot)) {
160
- writeLater(lanes, slot, columns, n);
170
+ writeLater(lanes, slot, columns, n, slots);
161
171
  continue;
162
172
  }
163
173
  const head = live ? heads[n] : null;
package/dist/qualify.js CHANGED
@@ -8,6 +8,7 @@ import { Lane } from './lane.js';
8
8
  import { reach } from './meet.js';
9
9
  import { bury, compactSparse, join, regroup, retouchRow, sameCrowds } from './rows.js';
10
10
  import { settle } from './sample.js';
11
+ import { seated, seatOf } from './seat.js';
11
12
  /**
12
13
  * Which channels run as lanes and which voices run on them: the fixed point where every voice
13
14
  * writing a laned channel fits and writes only laned channels. Only channels some laned voice
@@ -150,17 +151,16 @@ export function requalify(lanes, version) {
150
151
  for (const c of lanes.crowds)
151
152
  for (let p = 0; p < c.size; p++) {
152
153
  const v = c.voices[p];
153
- if (c.rowOf.get(v.id) === p && v.state === 'done')
154
+ if (seated(lanes, v, c, p) && v.state === 'done')
154
155
  bury(lanes, c, p);
155
156
  }
156
157
  // A crowd voice that started or began fading takes it up on its row, as `retouch` does.
157
158
  for (const v of touched) {
158
159
  if (v.state === 'done')
159
160
  continue;
160
- const c = lanes.crowdOf.get(v.id);
161
- const p = c?.rowOf.get(v.id);
162
- if (c !== undefined && p !== undefined)
163
- retouchRow(lanes, c, p, v);
161
+ const s = seatOf(lanes, v);
162
+ if (s !== null)
163
+ retouchRow(lanes, s.crowd, s.row, v);
164
164
  }
165
165
  compactSparse(lanes);
166
166
  return;
@@ -306,14 +306,13 @@ export function retouch(lanes) {
306
306
  const known = v.id <= lanes.known;
307
307
  if (v.id > lanes.known)
308
308
  lanes.known = v.id;
309
- const c = lanes.crowdOf.get(v.id);
310
- const p = c?.rowOf.get(v.id);
311
- if (c !== undefined && p !== undefined) {
309
+ const s = seatOf(lanes, v);
310
+ if (s !== null) {
312
311
  if (v.state === 'done') {
313
- bury(lanes, c, p);
312
+ bury(lanes, s.crowd, s.row);
314
313
  continue;
315
314
  }
316
- retouchRow(lanes, c, p, v);
315
+ retouchRow(lanes, s.crowd, s.row, v);
317
316
  continue;
318
317
  }
319
318
  if (v.state === 'done' && !known)
@@ -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;
@@ -0,0 +1,75 @@
1
+ import { resight } from './everyone.js';
2
+ import { goneIndex } from './gone.js';
3
+ import { reunreach } from './unreached.js';
4
+ /**
5
+ * Gives a subject the free number `to` in place of its own, carrying everything kept under the old
6
+ * one, so `pull` can keep a list's numbers in its order: a list kept dense by swap-remove moves its
7
+ * last subject into the place a dropped one left, whose number the drop just freed. Says whether it
8
+ * did; it does not where some store it would have to carry is in use: while a fill runs, under
9
+ * history, in a read ahead, or with roles, a bound or anything owed on the lanes.
10
+ */
11
+ export function renumber(mix, subject, head, to) {
12
+ const lanes = mix.lanes;
13
+ const from = head.slot;
14
+ if (from < 0 ||
15
+ lanes.filling ||
16
+ lanes.host.keeps ||
17
+ mix.projecting ||
18
+ lanes.roles.length > 0 ||
19
+ lanes.owed.any() ||
20
+ lanes.lanes.some((lane) => lane.voice.bound !== null) ||
21
+ !lanes.numbers.move(from, to))
22
+ return false;
23
+ for (const lane of lanes.lanes)
24
+ lane.renumber(from, to);
25
+ const placed = lanes.placed;
26
+ for (const id of placed.all(from)) {
27
+ placed.remove(from, id);
28
+ const s = lanes.seats.get(id);
29
+ if (s === undefined || s.crowd.list[s.row] !== from)
30
+ continue;
31
+ s.crowd.list[s.row] = to;
32
+ placed.add(to, id);
33
+ }
34
+ const per = lanes.per;
35
+ per.copyWithin(to * 6 /* Per.SLOT */, from * 6 /* Per.SLOT */, (from + 1) * 6 /* Per.SLOT */);
36
+ per[from * 6 /* Per.SLOT */ + 0 /* Per.FILLED */] = lanes.fills;
37
+ lanes.unlinked[to] = lanes.unlinked[from];
38
+ lanes.subjects[to] = lanes.subjects[from];
39
+ lanes.subjects[from] = undefined;
40
+ for (const ch of lanes.laned)
41
+ ch.values.copyWithin(to * ch.axes, from * ch.axes, (from + 1) * ch.axes);
42
+ mix.places?.forgot(from);
43
+ mix.places?.number(subject, to);
44
+ const cap = lanes.cap;
45
+ for (const voice of mix.general) {
46
+ reunreach(voice, from, to);
47
+ resight(voice, from, to, cap);
48
+ reblend(mix, voice, from, to);
49
+ }
50
+ for (const voice of goneIndex(mix).general)
51
+ reunreach(voice, from, to);
52
+ for (let held = head; held !== null; held = held.next)
53
+ if (held.voice !== null)
54
+ reblend(mix, held.voice, from, to);
55
+ head.slot = to;
56
+ return true;
57
+ }
58
+ /** Moves a blend's read for the subject this frame, if it has made one, to its new number. */
59
+ function reblend(mix, voice, from, to) {
60
+ const of = voice.blend?.of;
61
+ if (of === undefined || from >= of.frames.length || of.frames[from] !== mix.frame)
62
+ return;
63
+ if (to >= of.frames.length) {
64
+ const size = Math.max(to + 1, of.frames.length * 2, 64);
65
+ const frames = new Float64Array(size);
66
+ const values = new Float64Array(size);
67
+ frames.set(of.frames);
68
+ values.set(of.values);
69
+ of.frames = frames;
70
+ of.values = values;
71
+ }
72
+ of.frames[to] = mix.frame;
73
+ of.values[to] = of.values[from];
74
+ of.frames[from] = Number.NaN;
75
+ }
package/dist/rows.js CHANGED
@@ -4,6 +4,7 @@ import { reach } from './meet.js';
4
4
  import { motionOf } from './motion.js';
5
5
  import { open } from './qualify.js';
6
6
  import { settle } from './sample.js';
7
+ import { Seat, seatOf } from './seat.js';
7
8
  /** Whether two crowds' channels are the same list. */
8
9
  function same(a, b) {
9
10
  if (a.length !== b.length)
@@ -38,14 +39,13 @@ export function unplace(lanes, c, p) {
38
39
  export function sameCrowds(lanes, members) {
39
40
  let live = 0;
40
41
  for (const v of members) {
41
- if (!lanes.crowdOf.has(v.id))
42
+ if (!lanes.seats.has(v.id))
42
43
  return false;
43
44
  live++;
44
45
  }
45
46
  let rows = 0;
46
- for (const [id, c] of lanes.crowdOf) {
47
- const p = c.rowOf.get(id);
48
- if (c.voices[p].state !== 'done')
47
+ for (const s of lanes.seats.values()) {
48
+ if (s.crowd.voices[s.row].state !== 'done')
49
49
  rows++;
50
50
  }
51
51
  return rows === live;
@@ -64,13 +64,15 @@ export function regroup(lanes, members) {
64
64
  leaveCrowd(lanes, c, p);
65
65
  }
66
66
  const old = lanes.crowds;
67
- const was = new Map(lanes.crowdOf);
67
+ const was = new Map(lanes.seats);
68
+ for (const s of was.values())
69
+ s.lanes = null;
68
70
  lanes.crowds = [];
69
- lanes.crowdOf.clear();
71
+ lanes.seats.clear();
70
72
  lanes.overlap = false;
71
73
  for (const v of members) {
72
74
  const from = was.get(v.id);
73
- join(lanes, v, from, from?.rowOf.get(v.id), old);
75
+ join(lanes, v, from?.crowd, from?.row, old);
74
76
  }
75
77
  if (lanes.crowds.length === 0) {
76
78
  lanes.lawsByKey.clear();
@@ -104,8 +106,9 @@ export function join(lanes, v, from, at, old) {
104
106
  newRow(lanes, c, p, v);
105
107
  if (c.hot[p * c.stride + 6 /* Hot.EPOCH */] === 0 && v.state !== 'pending')
106
108
  c.hot[p * c.stride + 6 /* Hot.EPOCH */] = open(lanes, v);
107
- c.rowOf.set(v.id, p);
108
- lanes.crowdOf.set(v.id, c);
109
+ const seat = new Seat(lanes, c, p);
110
+ lanes.seats.set(v.id, seat);
111
+ v.seat = seat;
109
112
  v.laned = true;
110
113
  }
111
114
  /** A crowd row takes up its voice as it now stands: opened once it starts, its fields copied again. */
@@ -122,27 +125,28 @@ export function retouchRow(lanes, c, p, v) {
122
125
  export function compactSparse(lanes) {
123
126
  for (const c of lanes.crowds)
124
127
  if (c.dead > 64 && c.dead * 4 >= c.size)
125
- compact(c);
128
+ compact(lanes, c);
126
129
  }
127
130
  /**
128
131
  * Slides a crowd's rows down over the empty ones its departed voices left, in order, so its rows
129
132
  * stay in voice order; a qualify did this by rebuilding every crowd, a dropped frame at 10k rows.
130
133
  */
131
- function compact(c) {
134
+ function compact(lanes, c) {
132
135
  // The rows' stops are copied into a fresh pool, leaving the departed rows' behind.
133
136
  const keys = c.keys;
134
137
  c.keys = new KeyRows();
135
138
  let q = 0;
136
139
  for (let p = 0; p < c.size; p++) {
137
140
  const v = c.voices[p];
138
- if (c.rowOf.get(v.id) !== p) {
141
+ const s = seatOf(lanes, v);
142
+ if (s === null || s.crowd !== c || s.row !== p) {
139
143
  c.odd?.delete(p);
140
144
  continue;
141
145
  }
142
146
  if (q !== p) {
143
147
  copyRow(c, p, c, q, keys);
144
148
  c.odd?.delete(p);
145
- c.rowOf.set(v.id, q);
149
+ s.row = q;
146
150
  }
147
151
  else
148
152
  copyKeys(keys, c, q);
@@ -166,8 +170,12 @@ export function bury(lanes, c, p) {
166
170
  c.deltas[p] = null;
167
171
  c.motions[p] = undefined;
168
172
  c.hot[p * c.stride + 0 /* Hot.FLAGS */] = 0;
169
- c.rowOf.delete(v.id);
170
- lanes.crowdOf.delete(v.id);
173
+ lanes.seats.delete(v.id);
174
+ const s = v.seat;
175
+ if (s !== null && s.lanes === lanes) {
176
+ s.lanes = null;
177
+ v.seat = null;
178
+ }
171
179
  c.dead++;
172
180
  }
173
181
  /** Copies `n` numbers of a row; a view of them to hand `set` was three objects a row moved. */
@@ -202,7 +210,7 @@ function copyRow(from, p, to, q, keys = from.keys) {
202
210
  function copyKeys(keys, c, q) {
203
211
  const h = q * c.stride;
204
212
  if ((c.hot[h + 0 /* Hot.FLAGS */] & 512 /* Flag.FLAT */) !== 0)
205
- c.hot[h + 8 /* Hot.AT */] = c.keys.copy(keys, c.hot[h + 8 /* Hot.AT */]);
213
+ c.hot[h + 9 /* Hot.AT */] = c.keys.copy(keys, c.hot[h + 9 /* Hot.AT */]);
206
214
  }
207
215
  function newRow(lanes, c, p, v) {
208
216
  const run = motionOf(v.patch);
@@ -210,9 +218,9 @@ function newRow(lanes, c, p, v) {
210
218
  c.hot.fill(0, h, h + c.stride);
211
219
  const at = v.built === null ? -1 : c.keys.add(v.built, c.chans);
212
220
  if (at >= 0) {
213
- c.hot[h + 8 /* Hot.AT */] = at;
214
- c.hot[h + 10 /* Hot.X0 */] = v.duration;
215
- c.hot[h + 9 /* Hot.MS */] = v.passes;
221
+ c.hot[h + 9 /* Hot.AT */] = at;
222
+ c.hot[h + 11 /* Hot.X0 */] = v.duration;
223
+ c.hot[h + 10 /* Hot.MS */] = v.passes;
216
224
  }
217
225
  c.restale(p, 64 /* Flag.VOICE */ |
218
226
  (run === undefined ? 0 : 128 /* Flag.MOTION */) |
package/dist/seat.d.ts ADDED
@@ -0,0 +1,17 @@
1
+ import type { Crowd } from './crowd.js';
2
+ import type { Voice } from './voice.js';
3
+ /**
4
+ * A crowd voice's row, held on the voice and by its id in its lanes' `seats`: one place to read
5
+ * both its crowd and its row, where two maps by id were two cache misses a stop at 40,000 voices.
6
+ * `lanes` is the lanes it was given in, null once they let it go.
7
+ */
8
+ export declare class Seat<I, O> {
9
+ lanes: object | null;
10
+ readonly crowd: Crowd<I, O>;
11
+ row: number;
12
+ constructor(lanes: object | null, crowd: Crowd<I, O>, row: number);
13
+ }
14
+ /** `voice`'s seat in `lanes`, null where it has none there. */
15
+ export declare function seatOf<I, O>(lanes: object, voice: Voice<I, O>): Seat<I, O> | null;
16
+ /** Whether row `p` of `c` is `voice`'s seat in `lanes`, not a row it has left. */
17
+ export declare function seated<I, O>(lanes: object, voice: Voice<I, O>, c: Crowd<I, O>, p: number): boolean;
package/dist/seat.js ADDED
@@ -0,0 +1,25 @@
1
+ /**
2
+ * A crowd voice's row, held on the voice and by its id in its lanes' `seats`: one place to read
3
+ * both its crowd and its row, where two maps by id were two cache misses a stop at 40,000 voices.
4
+ * `lanes` is the lanes it was given in, null once they let it go.
5
+ */
6
+ export class Seat {
7
+ lanes;
8
+ crowd;
9
+ row;
10
+ constructor(lanes, crowd, row) {
11
+ this.lanes = lanes;
12
+ this.crowd = crowd;
13
+ this.row = row;
14
+ }
15
+ }
16
+ /** `voice`'s seat in `lanes`, null where it has none there. */
17
+ export function seatOf(lanes, voice) {
18
+ const s = voice.seat;
19
+ return s !== null && s.lanes === lanes ? s : null;
20
+ }
21
+ /** Whether row `p` of `c` is `voice`'s seat in `lanes`, not a row it has left. */
22
+ export function seated(lanes, voice, c, p) {
23
+ const s = seatOf(lanes, voice);
24
+ return s !== null && s.crowd === c && s.row === p;
25
+ }
package/dist/shared.d.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  * The package version, for a fact whose shape follows the code too closely to version by hand:
3
3
  * copies of different releases keep apart. `test/copies.test.ts` holds it to `package.json`.
4
4
  */
5
- export declare const release = "0.15.0";
5
+ export declare const release = "0.15.1";
6
6
  /**
7
7
  * A WeakMap every copy of blits loaded in one realm shares, so a fact one copy records about a
8
8
  * patch or channel it made is read by another copy's mix. The name carries a version, bumped when
package/dist/shared.js CHANGED
@@ -2,7 +2,7 @@
2
2
  * The package version, for a fact whose shape follows the code too closely to version by hand:
3
3
  * copies of different releases keep apart. `test/copies.test.ts` holds it to `package.json`.
4
4
  */
5
- export const release = '0.15.0';
5
+ export const release = '0.15.1';
6
6
  /**
7
7
  * A WeakMap every copy of blits loaded in one realm shares, so a fact one copy records about a
8
8
  * patch or channel it made is read by another copy's mix. The name carries a version, bumped when