@msb235/blits 0.15.0 → 0.16.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 (80) hide show
  1. package/CHANGELOG.md +79 -0
  2. package/README.md +1 -1
  3. package/dist/ahead.js +5 -4
  4. package/dist/bare.js +22 -7
  5. package/dist/chain.js +2 -3
  6. package/dist/color.d.ts +36 -14
  7. package/dist/color.js +186 -58
  8. package/dist/columns.d.ts +3 -12
  9. package/dist/columns.js +56 -41
  10. package/dist/crowd.d.ts +8 -10
  11. package/dist/crowd.js +17 -18
  12. package/dist/cue.js +7 -4
  13. package/dist/delay.d.ts +3 -0
  14. package/dist/delay.js +7 -0
  15. package/dist/due.js +1 -1
  16. package/dist/easing.d.ts +2 -0
  17. package/dist/easing.js +4 -0
  18. package/dist/everyone.d.ts +22 -6
  19. package/dist/everyone.js +47 -8
  20. package/dist/fade.d.ts +11 -0
  21. package/dist/fade.js +44 -8
  22. package/dist/fill.js +1 -1
  23. package/dist/fold.js +4 -7
  24. package/dist/held.js +5 -5
  25. package/dist/hosts.d.ts +7 -0
  26. package/dist/hosts.js +18 -5
  27. package/dist/index.d.ts +2 -2
  28. package/dist/index.js +1 -1
  29. package/dist/lane.d.ts +2 -5
  30. package/dist/lane.js +14 -10
  31. package/dist/lanes.d.ts +33 -5
  32. package/dist/lanes.js +34 -28
  33. package/dist/lch.d.ts +23 -0
  34. package/dist/lch.js +126 -0
  35. package/dist/meet.js +5 -3
  36. package/dist/mixer.d.ts +6 -2
  37. package/dist/mixer.js +7 -0
  38. package/dist/motions.d.ts +4 -1
  39. package/dist/motions.js +16 -2
  40. package/dist/move.js +15 -10
  41. package/dist/named.d.ts +6 -1
  42. package/dist/named.js +29 -1
  43. package/dist/numbers.d.ts +5 -0
  44. package/dist/numbers.js +29 -0
  45. package/dist/owner.d.ts +7 -2
  46. package/dist/owner.js +13 -3
  47. package/dist/project.js +3 -8
  48. package/dist/pull.d.ts +11 -0
  49. package/dist/pull.js +18 -4
  50. package/dist/qualify.js +10 -11
  51. package/dist/renumber.d.ts +10 -0
  52. package/dist/renumber.js +75 -0
  53. package/dist/roles.js +1 -3
  54. package/dist/roster.d.ts +6 -0
  55. package/dist/roster.js +8 -1
  56. package/dist/rows.js +27 -19
  57. package/dist/sample.js +2 -1
  58. package/dist/seat.d.ts +17 -0
  59. package/dist/seat.js +25 -0
  60. package/dist/seek.js +1 -13
  61. package/dist/shared.d.ts +1 -1
  62. package/dist/shared.js +1 -1
  63. package/dist/spans.d.ts +4 -1
  64. package/dist/spans.js +6 -3
  65. package/dist/strays.js +3 -2
  66. package/dist/touch.js +1 -2
  67. package/dist/trace.d.ts +4 -0
  68. package/dist/trace.js +255 -0
  69. package/dist/turns.js +2 -1
  70. package/dist/types/mix.d.ts +15 -0
  71. package/dist/types/trace.d.ts +51 -0
  72. package/dist/types/trace.js +1 -0
  73. package/dist/types/voice.d.ts +13 -7
  74. package/dist/types.d.ts +1 -0
  75. package/dist/unreached.d.ts +2 -0
  76. package/dist/unreached.js +7 -0
  77. package/dist/voice.d.ts +22 -0
  78. package/dist/voice.js +44 -4
  79. package/docs/schema.html +48 -11
  80. package/package.json +1 -1
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';
@@ -234,6 +235,8 @@ export class Mixer {
234
235
  roleDepth = 0;
235
236
  slotOf = new Map();
236
237
  lanes;
238
+ /** The sharing hooks of the lanes this entry was built with, which a projection's copy keeps. */
239
+ share;
237
240
  handles = null;
238
241
  /** What `book` made, still booking; null while there is none. */
239
242
  bookers = null;
@@ -265,6 +268,7 @@ export class Mixer {
265
268
  });
266
269
  if (lanesOf === null && opts.lanes === true)
267
270
  throw new Error("blits: '@msb235/blits/lean' has no lanes; take mix from '@msb235/blits'");
271
+ this.share = lanesOf?.share ?? null;
268
272
  this.lanes =
269
273
  lanesOf !== null && (opts.lanes ?? defaults.lanes) ? new lanesOf(laneHost(this)) : null;
270
274
  this.name = opts.name;
@@ -435,6 +439,9 @@ export class Mixer {
435
439
  project(at) {
436
440
  return projectAll(this.transport, at).of(this);
437
441
  }
442
+ trace(from, to, opts) {
443
+ return traceMix(this, from, to, opts);
444
+ }
438
445
  announce(name, opts = {}) {
439
446
  this.attached();
440
447
  announce(this.transport, name, opts, this.slot, this.name);
package/dist/motions.d.ts CHANGED
@@ -64,11 +64,14 @@ export declare class Motions<I> {
64
64
  /** Whether the channel it was cued on holds a number; undefined until it is cued. */
65
65
  private holdsNumber;
66
66
  private channel;
67
+ /** Whether the channel it was cued on travels round the hue, as `ink` does, and its stretches too. */
68
+ round: boolean;
67
69
  /**
68
70
  * Records what the channel the patch is cued on holds, refusing a value of the other kind: at once
69
71
  * where every subject's value is one kind, else on the first sample of a subject whose value is not.
72
+ * `round` for a channel that travels round the hue, as `ink` does.
70
73
  */
71
- cuedOn(channel: string, holdsNumber: boolean): void;
74
+ cuedOn(channel: string, holdsNumber: boolean, round?: boolean): void;
72
75
  frame(subject: I): number;
73
76
  constructor(shape: Shape<I>);
74
77
  /** The patch's number for `subject`, made with its first stretch on first ask. */
package/dist/motions.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { applied, change, commit, cut, dueTo, insert, peek, queue, rewind } from './changes.js';
2
- import { closed } from './closed.js';
2
+ import { BOUND, closed } from './closed.js';
3
+ import { closedLch } from './lch.js';
3
4
  import { made, pack, prune, reclaim, release, restretch, runAt, runThen, unpack, unrelease, } from './lives.js';
4
5
  import { absent, Numbers } from './numbers.js';
5
6
  import { reading } from './reading.js';
@@ -16,6 +17,7 @@ const PENDING = 2 /* RunFlag.PENDING */;
16
17
  const OLDER = 4 /* RunFlag.OLDER */;
17
18
  const LANDED = 8 /* RunFlag.LANDED */;
18
19
  const wrongKind = (channel, holdsNumber) => new Error(`blits: channel ${channel} holds ${holdsNumber ? 'a number' : 'an array'}, and this motion patch gives a subject ${holdsNumber ? 'an array' : 'a number'}`);
20
+ const notOklab = (channel) => new Error(`blits: channel ${channel} goes round the hue, and a motion on it moves oklab() values, four numbers each`);
19
21
  /**
20
22
  * A motion patch's subjects: the stretch each is playing, in one flat array by a number the patch
21
23
  * gives it, with earlier stretches a read back may reach and changes not yet applied beside them.
@@ -68,11 +70,18 @@ export class Motions {
68
70
  /** Whether the channel it was cued on holds a number; undefined until it is cued. */
69
71
  holdsNumber = undefined;
70
72
  channel = '';
73
+ /** Whether the channel it was cued on travels round the hue, as `ink` does, and its stretches too. */
74
+ round = false;
71
75
  /**
72
76
  * Records what the channel the patch is cued on holds, refusing a value of the other kind: at once
73
77
  * where every subject's value is one kind, else on the first sample of a subject whose value is not.
78
+ * `round` for a channel that travels round the hue, as `ink` does.
74
79
  */
75
- cuedOn(channel, holdsNumber) {
80
+ cuedOn(channel, holdsNumber, round = false) {
81
+ if (round && this.shape.law[0] === BOUND)
82
+ throw new Error(`blits: channel ${channel} goes round the hue, and a glide's bounds are a box on its OKLab axes; glide it without bounds`);
83
+ if (round && this.n >= 0 && this.n !== 4)
84
+ throw notOklab(channel);
76
85
  const every = this.shape.scalarEvery();
77
86
  if (every !== undefined && every !== holdsNumber)
78
87
  throw wrongKind(channel, holdsNumber);
@@ -82,6 +91,7 @@ export class Motions {
82
91
  throw wrongKind(channel, holdsNumber);
83
92
  this.holdsNumber = holdsNumber;
84
93
  this.channel = channel;
94
+ this.round = round;
85
95
  }
86
96
  frame(subject) {
87
97
  return this.owner === null ? Number.NaN : this.owner.frame(this.ownerId, subject);
@@ -127,6 +137,8 @@ export class Motions {
127
137
  }
128
138
  /** Sets the axes every subject moves on, at the first. */
129
139
  size(n) {
140
+ if (this.round && n !== 4)
141
+ throw notOklab(this.channel);
130
142
  this.n = n;
131
143
  this.stride = 3 + 3 * n;
132
144
  if (shared.xs.length < n)
@@ -426,6 +438,8 @@ export class Motions {
426
438
  this.evaluate(seg.at, seg.ms, seg.x0, 0, seg.v0, 0, seg.to, 0, t, xo, vo);
427
439
  }
428
440
  evaluate(at, ms, x0, x, v0, v, to, g, t, xo, vo) {
441
+ if (this.round)
442
+ return closedLch(this.runs, this.shape.ease, at, ms, x0, x, v0, v, to, g, t, xo, vo);
429
443
  return closed(this.runs, this.shape.ease, this.n, at, ms, x0, x, v0, v, to, g, t, xo, vo);
430
444
  }
431
445
  }
package/dist/move.js CHANGED
@@ -2,16 +2,15 @@ import { settleArcs } from './arcs.js';
2
2
  import { changed } from './chain.js';
3
3
  import { elapsedWith } from './clock.js';
4
4
  import { byId, popDue, schedule } from './due.js';
5
- import { detour, outgrow } from './everyone.js';
6
- import { beginFade, part, retire } from './fade.js';
5
+ import { beginFade, part, retire, rouse, stay } from './fade.js';
7
6
  import { expireGone, keepGone } from './gone.js';
8
7
  import { begin } from './held.js';
9
8
  import { noted } from './history.js';
10
9
  import { leave as leaveClock } from './origin.js';
11
- import { ownerReading } from './owner.js';
10
+ import { ownerReading, plays } from './owner.js';
12
11
  import { mixAt, place, repin, startAt, startOf } from './place.js';
13
12
  import { pageVoice } from './revive.js';
14
- import { delist } from './roster.js';
13
+ import { delist, detour } from './roster.js';
15
14
  import { scoredLeft, scoredRemove } from './scored.js';
16
15
  import { lapse } from './spans.js';
17
16
  import { unplay } from './strays.js';
@@ -80,6 +79,8 @@ function moveTo(mix, now) {
80
79
  if (voice.state !== 'pending') {
81
80
  if (voice.turns)
82
81
  turn(mix, voice, now);
82
+ if (voice.state === 'live' && voice.out !== null && !voice.quietAt(now))
83
+ rouse(mix, voice, now);
83
84
  // Its passes run out at its end running forward, and at its start running back. A clock
84
85
  // standing still at or before its start is where it was: waiting, or frozen there.
85
86
  const way = heading(voice, now);
@@ -87,7 +88,7 @@ function moveTo(mix, now) {
87
88
  const back = way < 0;
88
89
  const atStart = !voice.beginless && at <= voice.soonest;
89
90
  // An owner run back to its start stays for what it holds, and leaves with its last child.
90
- const holds = voice.holding !== null && voice.holding.children.length > 0;
91
+ const holds = voice.holding !== null && plays(voice, now);
91
92
  const over = back
92
93
  ? atStart && !holds
93
94
  : Number.isFinite(voice.span) && at >= voice.span + voice.latest;
@@ -95,11 +96,11 @@ function moveTo(mix, now) {
95
96
  voice.play(true, now, mix.transport.seq);
96
97
  // The fade starts when its passes ran out, not at the frame that noticed, so it plays
97
98
  // the same at any frame rate and a read at another time can find it.
98
- if (voice.state === 'live') {
99
+ if (voice.state === 'live' && voice.out === null) {
99
100
  if (back ? voice.freezesBefore : voice.freezesAfter)
100
101
  voice.state = 'frozen';
101
102
  else
102
- beginFade(mix, voice, { back }, Math.max(startOf(voice), Math.min(now, voice.endsAt())));
103
+ beginFade(mix, voice, { back, stays: voice.owner !== null && mix.opts.history !== undefined }, Math.max(startOf(voice), Math.min(now, voice.endsAt())));
103
104
  }
104
105
  }
105
106
  else if (voice.state === 'frozen' && !(atStart && way === 0))
@@ -126,8 +127,12 @@ function moveTo(mix, now) {
126
127
  else if (settled)
127
128
  retire(mix, voice);
128
129
  }
129
- else if (spent >= over)
130
- retire(mix, voice, at + over);
130
+ else if (spent >= over) {
131
+ if (voice.out.stays)
132
+ stay(mix, voice, at + over);
133
+ else
134
+ retire(mix, voice, at + over);
135
+ }
131
136
  }
132
137
  if (!mix.projecting)
133
138
  schedule(mix, voice);
@@ -137,7 +142,7 @@ function moveTo(mix, now) {
137
142
  settleArcs(mix, now);
138
143
  }
139
144
  if (mix.sharers.length > 0 && !mix.projecting)
140
- outgrow(mix);
145
+ mix.share?.outgrow(mix);
141
146
  mix.stirred = false;
142
147
  mix.asleep = false;
143
148
  if (mix.retired.length > 0)
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/owner.d.ts CHANGED
@@ -36,8 +36,13 @@ export declare function mixTime<I, O>(owner: Voice<I, O>, t: number): number;
36
36
  export declare function turnedBack<I, O>(voice: Voice<I, O>): boolean;
37
37
  /** Takes a child into its owner. */
38
38
  export declare function adopt<I, O>(owner: Voice<I, O>, child: Voice<I, O>): void;
39
- /** A child left: its owner, where that was its last child and it should leave too. */
40
- export declare function orphan<I, O>(child: Voice<I, O>): Voice<I, O> | null;
39
+ /**
40
+ * A child left at mix time `now`: its owner, where that was its last child still playing and it
41
+ * should leave too.
42
+ */
43
+ export declare function orphan<I, O>(child: Voice<I, O>, now: number): Voice<I, O> | null;
44
+ /** Whether an owner holds a child at mix time `now` that is not kept quiet past its passes. */
45
+ export declare function plays<I, O>(owner: Voice<I, O>, now: number): boolean;
41
46
  /** A child finished its passes: its owner has played once every child it held has. */
42
47
  export declare function childPlayed<I, O>(owner: Voice<I, O>, at: number, seq: number): void;
43
48
  /** Every voice an owner holds, its children's children included. */
package/dist/owner.js CHANGED
@@ -76,14 +76,24 @@ export function adopt(owner, child) {
76
76
  h.children.push(child);
77
77
  h.kids++;
78
78
  }
79
- /** A child left: its owner, where that was its last child and it should leave too. */
80
- export function orphan(child) {
79
+ /**
80
+ * A child left at mix time `now`: its owner, where that was its last child still playing and it
81
+ * should leave too.
82
+ */
83
+ export function orphan(child, now) {
81
84
  const owner = child.owner;
82
85
  const h = owner.holding;
83
86
  const i = h.children.indexOf(child);
84
87
  if (i >= 0)
85
88
  h.children.splice(i, 1);
86
- return h.children.length === 0 && owner.state !== 'done' ? owner : null;
89
+ return !plays(owner, now) && owner.state !== 'done' ? owner : null;
90
+ }
91
+ /** Whether an owner holds a child at mix time `now` that is not kept quiet past its passes. */
92
+ export function plays(owner, now) {
93
+ for (const child of owner.holding.children)
94
+ if (!child.quietAt(now))
95
+ return true;
96
+ return false;
87
97
  }
88
98
  /** A child finished its passes: its owner has played once every child it held has. */
89
99
  export function childPlayed(owner, at, seq) {
package/dist/project.js CHANGED
@@ -7,7 +7,7 @@ import { fadesAt } from './fade.js';
7
7
  import { meet, origin } from './held.js';
8
8
  import { copyHeld, last, lastWithin } from './history.js';
9
9
  import { Mixer } from './mixer.js';
10
- import { heldByInput, ownerReading, relink } from './owner.js';
10
+ import { heldByInput, relink } from './owner.js';
11
11
  import { reading } from './reading.js';
12
12
  import { record } from './record.js';
13
13
  import { recount } from './roster.js';
@@ -72,6 +72,7 @@ export function projectAll(transport, at) {
72
72
  }, null);
73
73
  copy.slot = mix.slot;
74
74
  copy.projecting = true;
75
+ copy.share = mix.share;
75
76
  copy.places = mix.places;
76
77
  copies.set(mix, copy);
77
78
  if (ahead)
@@ -183,13 +184,7 @@ function copyBack(mix, c, t, cut) {
183
184
  /** Sets each copied voice's state to what its clock and fade make it at `t`. */
184
185
  export function stateAt(c, t) {
185
186
  for (const copy of c.cued)
186
- copy.state = copy.out
187
- ? 'fading'
188
- : (copy.owner === null ? t : ownerReading(copy.owner, t)) < copy.start
189
- ? 'pending'
190
- : copy.freezesAfter && copy.elapsedAt(t) >= copy.span + copy.latest
191
- ? 'frozen'
192
- : 'live';
187
+ copy.state = copy.stateAt(t);
193
188
  }
194
189
  /**
195
190
  * A projection back starts each subject from the latest copy kept at or before `t`, else from its
package/dist/pull.d.ts CHANGED
@@ -1,5 +1,16 @@
1
1
  import type { Mixer } from './mixer.js';
2
2
  import type { Columns } from './types.js';
3
+ /** One channel `pull` writes: where in the kit, how many numbers a subject, and its rest. */
4
+ export interface Column {
5
+ key: string;
6
+ slot: number;
7
+ axes: number;
8
+ out: Float64Array;
9
+ /** The rest, clamped, or NaN for a channel with none. */
10
+ rest: Float64Array;
11
+ bounds: readonly [number, number] | undefined;
12
+ }
13
+ export declare function clampRun(out: Float64Array, at: number, n: number, [lo, hi]: readonly [number, number]): void;
3
14
  /** After a probe, keeps the pose a retarget from `'current'` reads as the subject's last. */
4
15
  export declare function keep<I, O>(this: Mixer<I, O>, subject: I, pose: O, out: O | undefined): void;
5
16
  export declare function pull<I, O>(mix: Mixer<I, O>, subjects: Iterable<I>, into: Columns<O>): void;
package/dist/pull.js CHANGED
@@ -1,8 +1,13 @@
1
1
  import { axesOf } from './channels.js';
2
2
  import { copy } from './clone.js';
3
- import { clampRun, flush, pullRun, writeLater } from './columns.js';
4
3
  /** What `pull` remembers at a position it has read no subject at. */
5
4
  const UNREAD = Symbol('unread');
5
+ export function clampRun(out, at, n, [lo, hi]) {
6
+ for (let i = at; i < at + n; i++) {
7
+ const x = out[i];
8
+ out[i] = x < lo ? lo : x > hi ? hi : x;
9
+ }
10
+ }
6
11
  /** Writes a folded pose's value into subject `n`'s places in a column. */
7
12
  function writeValue(c, v, n) {
8
13
  const at = n * c.axes;
@@ -111,7 +116,7 @@ export function pull(mix, subjects, into) {
111
116
  }
112
117
  finally {
113
118
  if (lanes !== null)
114
- flush(lanes);
119
+ lanes.pulls.flush(lanes);
115
120
  }
116
121
  // Every head is current only if nothing relinked or changed version while they were read.
117
122
  const unchanged = live && mix.version === version && mix.relinks === relinks;
@@ -135,7 +140,7 @@ function readAll(mix, list, columns, live, current, since, whole) {
135
140
  const runs = live && whole;
136
141
  for (let n = 0; n < list.length; n++) {
137
142
  if (runs) {
138
- n = pullRun(lanes, slots, list, was, since, n, columns, now, mix.version);
143
+ n = lanes.pulls.pullRun(lanes, slots, list, was, since, n, columns, now, mix.version);
139
144
  if (n === list.length)
140
145
  break;
141
146
  }
@@ -152,12 +157,21 @@ function readAll(mix, list, columns, live, current, since, whole) {
152
157
  was[n] = subject;
153
158
  heads[n] = head;
154
159
  slot = head.slot;
160
+ // Numbered out of the list's order, as swap-remove leaves a subject: renumbered into it.
161
+ const to = n === 0 ? 0 : slots[n - 1] + 1;
162
+ if (runs && slot >= 0 && slot !== to && to >= 0) {
163
+ if (lanes.pulls.renumber(mix, subject, head, to)) {
164
+ for (let j = was.indexOf(subject); j >= 0; j = was.indexOf(subject, j + 1))
165
+ slots[j] = to;
166
+ slot = to;
167
+ }
168
+ }
155
169
  slots[n] = slot;
156
170
  }
157
171
  }
158
172
  const laned = live && lanes?.prepare(slot, subject, mix.version, heads[n] ?? null) === true;
159
173
  if (laned && whole && lanes.whole && !lanes.owes(slot)) {
160
- writeLater(lanes, slot, columns, n);
174
+ lanes.pulls.writeLater(lanes, slot, columns, n, slots);
161
175
  continue;
162
176
  }
163
177
  const head = live ? heads[n] : null;
package/dist/qualify.js CHANGED
@@ -1,13 +1,14 @@
1
1
  import { numericOf } from './channels.js';
2
2
  import { flush } from './columns.js';
3
- import { crowdable } from './crowd.js';
4
3
  import { byId } from './due.js';
5
4
  import { weighAt } from './everyone.js';
6
5
  import { wake } from './fill.js';
6
+ import { crowdable } from './hosts.js';
7
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/roles.js CHANGED
@@ -1,9 +1,7 @@
1
1
  import { first } from './clone.js';
2
- import { open as openRecord } from './everyone.js';
3
2
  import { merged, unband } from './fold.js';
4
3
  import { roleScratch } from './role.js';
5
4
  // V8 reads a local const as a constant, where it reads an import from its module on every call.
6
- const open = openRecord;
7
5
  /**
8
6
  * Whether a fold's next voice is its chain's next record, `held`, and not the next voice sharing
9
7
  * one record, `sharers[q]`: the two lists are each in voice order, and a fold takes them as one.
@@ -54,7 +52,7 @@ export function foldRoles(subject, pose, head, laned, dry, except, owed) {
54
52
  continue;
55
53
  const held = rec;
56
54
  if (!mine)
57
- open(voice, held, head.slot);
55
+ this.share.open(voice, held, head.slot);
58
56
  const delta = this.read(voice, subject, now, dry, held);
59
57
  if (owed >= 0 && voice.laned)
60
58
  this.lanes.paid(voice.id, owed, held.weight);
package/dist/roster.d.ts CHANGED
@@ -1,5 +1,11 @@
1
1
  import type { Mixer } from './mixer.js';
2
2
  import type { Voice } from './voice.js';
3
+ /**
4
+ * Counts what makes a mix's fold more than a walk of each chain: a role, a voice sharing one
5
+ * record, a voice that stopped sharing this frame. Called wherever one of the three changes, so
6
+ * `foldWith` asks one number.
7
+ */
8
+ export declare function detour<I, O>(mix: Mixer<I, O>): void;
3
9
  /** Counts a voice just put in the list into everything a sync and a fold skip work by. */
4
10
  export declare function enlist<I, O>(mix: Mixer<I, O>, voice: Voice<I, O>): void;
5
11
  /**
package/dist/roster.js CHANGED
@@ -1,6 +1,13 @@
1
1
  import { index, unindex } from './chain.js';
2
- import { detour } from './everyone.js';
3
2
  import { Store } from './store.js';
3
+ /**
4
+ * Counts what makes a mix's fold more than a walk of each chain: a role, a voice sharing one
5
+ * record, a voice that stopped sharing this frame. Called wherever one of the three changes, so
6
+ * `foldWith` asks one number.
7
+ */
8
+ export function detour(mix) {
9
+ mix.detours = mix.roleVoices + mix.sharers.length + (mix.unshared ? 1 : 0);
10
+ }
4
11
  function tally(mix, voice, by) {
5
12
  if (voice.spec.role !== undefined)
6
13
  mix.roleVoices += by;