@msb235/blits 0.10.0 → 0.12.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.
- package/CHANGELOG.md +248 -0
- package/README.md +15 -4
- package/dist/arcs.d.ts +40 -0
- package/dist/arcs.js +239 -0
- package/dist/blend.js +1 -5
- package/dist/book.d.ts +2 -0
- package/dist/book.js +27 -3
- package/dist/chain.d.ts +17 -1
- package/dist/chain.js +57 -15
- package/dist/channels.d.ts +2 -0
- package/dist/channels.js +12 -1
- package/dist/clock.d.ts +14 -0
- package/dist/clock.js +9 -1
- package/dist/clone.js +16 -4
- package/dist/columns.d.ts +4 -5
- package/dist/columns.js +11 -5
- package/dist/crowd.d.ts +1 -1
- package/dist/crowd.js +4 -1
- package/dist/cue.js +63 -7
- package/dist/departed.d.ts +8 -3
- package/dist/departed.js +76 -29
- package/dist/due.d.ts +4 -0
- package/dist/due.js +16 -3
- package/dist/easing.d.ts +12 -1
- package/dist/easing.js +7 -1
- package/dist/everyone.d.ts +14 -1
- package/dist/everyone.js +27 -2
- package/dist/fade.d.ts +12 -5
- package/dist/fade.js +57 -19
- package/dist/fill.js +16 -9
- package/dist/fit.d.ts +8 -1
- package/dist/fit.js +7 -3
- package/dist/fold.d.ts +1 -1
- package/dist/fold.js +112 -9
- package/dist/frames.d.ts +2 -0
- package/dist/frames.js +17 -0
- package/dist/gather.js +38 -7
- package/dist/handle.d.ts +7 -2
- package/dist/handle.js +28 -12
- package/dist/held.d.ts +13 -2
- package/dist/held.js +43 -9
- package/dist/history.d.ts +25 -2
- package/dist/history.js +163 -35
- package/dist/hosts.js +45 -17
- package/dist/index.d.ts +5 -2
- package/dist/index.js +3 -1
- package/dist/keyfill.js +3 -1
- package/dist/lane.d.ts +1 -1
- package/dist/lane.js +3 -1
- package/dist/lanes.d.ts +31 -7
- package/dist/lanes.js +57 -6
- package/dist/lean.d.ts +15 -0
- package/dist/lean.js +23 -0
- package/dist/leavings.d.ts +6 -1
- package/dist/leavings.js +16 -2
- package/dist/marks.d.ts +7 -3
- package/dist/marks.js +105 -27
- package/dist/meet.js +1 -0
- package/dist/mixer.d.ts +35 -6
- package/dist/mixer.js +113 -45
- package/dist/motion.d.ts +4 -1
- package/dist/motion.js +16 -2
- package/dist/motions.d.ts +4 -0
- package/dist/motions.js +45 -11
- package/dist/move.js +35 -12
- package/dist/numbers.d.ts +5 -1
- package/dist/numbers.js +4 -0
- package/dist/owner.d.ts +18 -2
- package/dist/owner.js +44 -3
- package/dist/pack.d.ts +1 -0
- package/dist/pack.js +3 -0
- package/dist/place.d.ts +8 -0
- package/dist/place.js +16 -8
- package/dist/placed.d.ts +19 -0
- package/dist/placed.js +54 -0
- package/dist/project.d.ts +5 -2
- package/dist/project.js +238 -34
- package/dist/pull.js +37 -8
- package/dist/qualify.d.ts +10 -0
- package/dist/qualify.js +60 -3
- package/dist/reading.d.ts +8 -1
- package/dist/reading.js +8 -1
- package/dist/record.d.ts +6 -0
- package/dist/record.js +2 -0
- package/dist/restored.d.ts +69 -17
- package/dist/restored.js +163 -50
- package/dist/revive.d.ts +6 -3
- package/dist/revive.js +11 -3
- package/dist/roster.js +2 -1
- package/dist/rows.js +15 -5
- package/dist/seek.d.ts +7 -2
- package/dist/seek.js +152 -43
- package/dist/shared.d.ts +5 -0
- package/dist/shared.js +5 -0
- package/dist/signals.d.ts +10 -0
- package/dist/signals.js +17 -0
- package/dist/spans.d.ts +5 -0
- package/dist/spans.js +75 -5
- package/dist/store.d.ts +13 -0
- package/dist/store.js +23 -0
- package/dist/strays.js +50 -2
- package/dist/tape.d.ts +29 -4
- package/dist/tape.js +49 -11
- package/dist/touch.js +3 -2
- package/dist/transport.d.ts +11 -2
- package/dist/transport.js +26 -10
- package/dist/types/arc.d.ts +55 -0
- package/dist/types/arc.js +1 -0
- package/dist/types/mix.d.ts +57 -14
- package/dist/types/patch.d.ts +5 -1
- package/dist/types/place.d.ts +11 -4
- package/dist/types/transport.d.ts +12 -3
- package/dist/types/voice.d.ts +72 -5
- package/dist/types/watch.d.ts +126 -0
- package/dist/types/watch.js +1 -0
- package/dist/types.d.ts +2 -0
- package/dist/unpage.d.ts +6 -1
- package/dist/unpage.js +34 -2
- package/dist/voice.d.ts +93 -16
- package/dist/voice.js +60 -3
- package/dist/watch.d.ts +26 -0
- package/dist/watch.js +441 -0
- package/dist/weigh.d.ts +13 -2
- package/dist/weigh.js +10 -3
- package/docs/schema.html +106 -38
- package/package.json +10 -4
package/dist/project.js
CHANGED
|
@@ -1,37 +1,52 @@
|
|
|
1
1
|
import { ownBlends } from './blend.js';
|
|
2
|
+
import { headOf, heldAt } from './chain.js';
|
|
3
|
+
import { stepOf } from './clock.js';
|
|
2
4
|
import { open, sighted, weightAt } from './everyone.js';
|
|
5
|
+
import { fadesAt } from './fade.js';
|
|
6
|
+
import { meet, origin } from './held.js';
|
|
3
7
|
import { copyHeld, last, lastWithin } from './history.js';
|
|
8
|
+
import { markOf, voiceMarks } from './marks.js';
|
|
4
9
|
import { Mixer } from './mixer.js';
|
|
5
10
|
import { move } from './move.js';
|
|
6
11
|
import { heldByInput, ownerReading, relink } from './owner.js';
|
|
7
|
-
import { pin } from './place.js';
|
|
12
|
+
import { pin, place } from './place.js';
|
|
8
13
|
import { reading } from './reading.js';
|
|
9
14
|
import { record } from './record.js';
|
|
10
15
|
import { recount } from './roster.js';
|
|
11
16
|
import { copyStanding, stands } from './standing.js';
|
|
12
|
-
import { hostAt } from './tape.js';
|
|
17
|
+
import { hostAhead, hostAt, replay } from './tape.js';
|
|
13
18
|
import { Transport, within } from './transport.js';
|
|
14
|
-
import { cover } from './unpage.js';
|
|
19
|
+
import { cover, coverFrame } from './unpage.js';
|
|
15
20
|
import { unreached } from './unreached.js';
|
|
16
21
|
/**
|
|
17
22
|
* Every mix on a transport read at mix time `t`, through copies of them on a transport of their
|
|
18
|
-
* own, moved together so an anchor across mixes answers as it would live.
|
|
23
|
+
* own, moved together so an anchor across mixes answers as it would live. Ahead, they stop at each
|
|
24
|
+
* known event on the way, and a subject asked about is read at each before `t`.
|
|
19
25
|
*/
|
|
20
|
-
export function projectAll(transport,
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
26
|
+
export function projectAll(transport, at) {
|
|
27
|
+
const kept = transport.history !== undefined;
|
|
28
|
+
let t;
|
|
29
|
+
let numbered;
|
|
30
|
+
if (typeof at === 'number')
|
|
31
|
+
t = at;
|
|
32
|
+
else {
|
|
33
|
+
if (!kept)
|
|
34
|
+
throw new Error('blits: reading a frame back needs a mix made with history');
|
|
35
|
+
numbered = coverFrame(transport, at.frame);
|
|
36
|
+
t = numbered.at;
|
|
37
|
+
}
|
|
38
|
+
// The frame being played reads as the mix's own time does; any other is behind, whatever its time.
|
|
39
|
+
const ahead = numbered !== undefined
|
|
40
|
+
? numbered.seq === transport.seq
|
|
41
|
+
: Number.isNaN(transport.now) || t >= transport.now;
|
|
24
42
|
const pace = transport.pace;
|
|
25
|
-
const next = stamp === undefined || pace === null ? stamp : pace.reading(stamp);
|
|
26
|
-
if (next !== undefined && next <= t)
|
|
27
|
-
throw new Error(`blits: the tape holds calls at ${next} that a read ahead to ${t} cannot play; seek there to see them`);
|
|
28
43
|
// Behind, the last frame at or before `t`, read as it showed when `t` is that frame's own time.
|
|
29
44
|
let cut = { seq: transport.seq, strict: false };
|
|
30
|
-
let u = hostAt(transport, t);
|
|
31
|
-
const kept = transport.history !== undefined;
|
|
45
|
+
let u = ahead ? hostAhead(transport, t) : hostAt(transport, t);
|
|
32
46
|
if (!ahead && kept) {
|
|
33
|
-
|
|
34
|
-
|
|
47
|
+
if (numbered === undefined)
|
|
48
|
+
cover(transport, t);
|
|
49
|
+
const frame = numbered ?? transport.frames.at(t);
|
|
35
50
|
if (frame !== undefined) {
|
|
36
51
|
cut = { seq: frame.seq, strict: frame.at === t };
|
|
37
52
|
if (frame.at === t)
|
|
@@ -57,7 +72,7 @@ export function projectAll(transport, t) {
|
|
|
57
72
|
history: undefined,
|
|
58
73
|
lanes: false,
|
|
59
74
|
transport: c,
|
|
60
|
-
});
|
|
75
|
+
}, null);
|
|
61
76
|
copy.slot = mix.slot;
|
|
62
77
|
copy.projecting = true;
|
|
63
78
|
copies.set(mix, copy);
|
|
@@ -70,28 +85,50 @@ export function projectAll(transport, t) {
|
|
|
70
85
|
}
|
|
71
86
|
// A read at the mix's own time lands nothing, as a probe does not: what the host has done since
|
|
72
87
|
// the last sync shows at the next one.
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
88
|
+
let stood = c;
|
|
89
|
+
let earlier = null;
|
|
90
|
+
if (ahead && t !== transport.now) {
|
|
91
|
+
earlier = new Map();
|
|
92
|
+
stood = playAhead(transport, c, copies, t, earlier);
|
|
93
|
+
}
|
|
76
94
|
const read = (f) => {
|
|
77
95
|
reading.live = false;
|
|
96
|
+
reading.ahead = stood.ahead.size > 0 ? stood.ahead : null;
|
|
78
97
|
try {
|
|
79
98
|
return f();
|
|
80
99
|
}
|
|
81
100
|
finally {
|
|
82
101
|
reading.live = true;
|
|
102
|
+
reading.ahead = null;
|
|
83
103
|
}
|
|
84
104
|
};
|
|
85
105
|
return {
|
|
86
106
|
timestamp: t,
|
|
87
107
|
of(mix) {
|
|
88
|
-
const
|
|
108
|
+
const key = mix;
|
|
109
|
+
const copy = copies.get(key);
|
|
89
110
|
if (copy === undefined)
|
|
90
111
|
throw new Error('blits: that mix is not on this transport');
|
|
112
|
+
const stages = earlier?.get(key);
|
|
113
|
+
// A subject asked about is read at every stop first, in order, so the state it carries
|
|
114
|
+
// crosses each event as a frame there would carry it.
|
|
115
|
+
const warm = stages === undefined
|
|
116
|
+
? null
|
|
117
|
+
: (subject) => {
|
|
118
|
+
if (stages.met.has(subject))
|
|
119
|
+
return;
|
|
120
|
+
stages.met.add(subject);
|
|
121
|
+
for (const stage of stages.at)
|
|
122
|
+
stage.fold(subject);
|
|
123
|
+
};
|
|
91
124
|
return {
|
|
92
125
|
timestamp: t,
|
|
93
|
-
probe: (subject, out) => read(() =>
|
|
126
|
+
probe: (subject, out) => read(() => {
|
|
127
|
+
warm?.(subject);
|
|
128
|
+
return copy.fold(subject, out);
|
|
129
|
+
}),
|
|
94
130
|
assess: (subject) => read(() => {
|
|
131
|
+
warm?.(subject);
|
|
95
132
|
copy.fold(subject);
|
|
96
133
|
return doubts(copy, subject);
|
|
97
134
|
}),
|
|
@@ -99,7 +136,150 @@ export function projectAll(transport, t) {
|
|
|
99
136
|
},
|
|
100
137
|
};
|
|
101
138
|
}
|
|
102
|
-
/**
|
|
139
|
+
/**
|
|
140
|
+
* Moves a read ahead's copies to mix time `t`, making the calls the tape holds on the way as a seek
|
|
141
|
+
* there makes them on the mixes. While a voice could carry state across it, it stops at each known
|
|
142
|
+
* event: a call, and every mark of a voice or announced. At each stop the copies are kept for
|
|
143
|
+
* subjects to be read there and copied on, so `copies` ends holding those at `t`. Answers the
|
|
144
|
+
* transport they stand on, and puts the tape back where it stood.
|
|
145
|
+
*/
|
|
146
|
+
function playAhead(transport, c, copies, t, earlier) {
|
|
147
|
+
const tape = transport.tape;
|
|
148
|
+
const depth = tape?.undoDepth() ?? 0;
|
|
149
|
+
let played = false;
|
|
150
|
+
try {
|
|
151
|
+
for (;;) {
|
|
152
|
+
const e = nextEvent(tape, c, t);
|
|
153
|
+
if (!(e < t))
|
|
154
|
+
break;
|
|
155
|
+
played = playTo(transport, c, copies, e) || played;
|
|
156
|
+
if (!carries(c))
|
|
157
|
+
continue;
|
|
158
|
+
// Placed now, as the next move would place them, while a target that has just left is here.
|
|
159
|
+
for (const m of c.members)
|
|
160
|
+
if (m.anchored > 0)
|
|
161
|
+
place(m);
|
|
162
|
+
const next = restage(c);
|
|
163
|
+
for (const [mix, copy] of copies) {
|
|
164
|
+
const on = new Mixer(copy.kit, { ...copy.opts, transport: next }, null);
|
|
165
|
+
on.slot = copy.slot;
|
|
166
|
+
on.projecting = true;
|
|
167
|
+
copyAhead(copy, on);
|
|
168
|
+
const stages = earlier.get(mix);
|
|
169
|
+
if (stages === undefined)
|
|
170
|
+
earlier.set(mix, { at: [copy], met: new Set() });
|
|
171
|
+
else
|
|
172
|
+
stages.at.push(copy);
|
|
173
|
+
copies.set(mix, on);
|
|
174
|
+
}
|
|
175
|
+
c = next;
|
|
176
|
+
}
|
|
177
|
+
played = playTo(transport, c, copies, t) || played;
|
|
178
|
+
}
|
|
179
|
+
finally {
|
|
180
|
+
if (played)
|
|
181
|
+
tape?.goto(depth);
|
|
182
|
+
}
|
|
183
|
+
return c;
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Whether a voice on a read ahead's copies could carry state from one known event to the next: a
|
|
187
|
+
* patch that steps, or a weight given by a signal, which may keep state. Where none could, a read
|
|
188
|
+
* crosses the events in one go, as fast as one with none.
|
|
189
|
+
*/
|
|
190
|
+
function carries(c) {
|
|
191
|
+
for (const m of c.members)
|
|
192
|
+
for (const v of m.cued)
|
|
193
|
+
if (v.state !== 'done' && (v.patch.step !== undefined || typeof v.spec.weight === 'function'))
|
|
194
|
+
return true;
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* The earliest known event a read ahead's copies have yet to reach before `t`, mix time: the next
|
|
199
|
+
* call the tape holds, and while a voice could carry state across one, a mark of a voice or one
|
|
200
|
+
* announced. Infinity where there is none.
|
|
201
|
+
*/
|
|
202
|
+
function nextEvent(tape, c, t) {
|
|
203
|
+
const now = c.now;
|
|
204
|
+
let e = Number.POSITIVE_INFINITY;
|
|
205
|
+
const call = tape?.timestampAt(tape.undoDepth());
|
|
206
|
+
if (call !== undefined)
|
|
207
|
+
e = c.pace === null ? call : c.pace.reading(call);
|
|
208
|
+
if (!carries(c))
|
|
209
|
+
return e < t ? e : Number.POSITIVE_INFINITY;
|
|
210
|
+
for (const m of c.members)
|
|
211
|
+
for (const v of m.cued) {
|
|
212
|
+
if (v.state === 'done')
|
|
213
|
+
continue;
|
|
214
|
+
for (const mark of voiceMarks) {
|
|
215
|
+
const at = markOf(m, v, mark);
|
|
216
|
+
if (at !== undefined && at > now && at < e)
|
|
217
|
+
e = at;
|
|
218
|
+
}
|
|
219
|
+
const inner = v.spec.marks;
|
|
220
|
+
if (inner !== undefined)
|
|
221
|
+
for (const mark of Object.keys(inner)) {
|
|
222
|
+
const at = markOf(m, v, mark);
|
|
223
|
+
if (at !== undefined && at > now && at < e)
|
|
224
|
+
e = at;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
for (const a of c.announced) {
|
|
228
|
+
const at = c.pace === null ? a.at : c.pace.at(a.at, c.u);
|
|
229
|
+
if (at > now && at < e)
|
|
230
|
+
e = at;
|
|
231
|
+
}
|
|
232
|
+
return e < t ? e : Number.POSITIVE_INFINITY;
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Makes the calls the tape holds up to mix time `to` on a read ahead's copies and moves them
|
|
236
|
+
* there. Answers whether it moved the tape.
|
|
237
|
+
*/
|
|
238
|
+
function playTo(transport, c, copies, to) {
|
|
239
|
+
const tape = transport.tape;
|
|
240
|
+
const due = (at) => (c.pace === null ? at : c.pace.reading(at)) <= to;
|
|
241
|
+
const next = tape?.timestampAt(tape.undoDepth());
|
|
242
|
+
c.u = hostAhead(c, to);
|
|
243
|
+
let played = false;
|
|
244
|
+
if (next !== undefined && due(next)) {
|
|
245
|
+
// A call may write what a copy shares with the voice it was copied from.
|
|
246
|
+
for (const copy of c.members)
|
|
247
|
+
for (const v of copy.cued) {
|
|
248
|
+
if (v.parts !== null)
|
|
249
|
+
v.parts = new Map(v.parts);
|
|
250
|
+
if (v.parted !== null)
|
|
251
|
+
v.parted = new Map(v.parted);
|
|
252
|
+
}
|
|
253
|
+
played = true;
|
|
254
|
+
const last = replay(transport, due, c, {
|
|
255
|
+
transport: () => c,
|
|
256
|
+
mix: (of) => copies.get(of),
|
|
257
|
+
});
|
|
258
|
+
c.u = Math.max(last, hostAhead(c, to));
|
|
259
|
+
}
|
|
260
|
+
// A call played at `to` itself moved the copies there before it, as a sync does.
|
|
261
|
+
if (!(c.now >= to))
|
|
262
|
+
for (const copy of c.members)
|
|
263
|
+
move(copy, to);
|
|
264
|
+
return played;
|
|
265
|
+
}
|
|
266
|
+
/** A transport for a read ahead's copies to go on from where those on `c` stand. */
|
|
267
|
+
function restage(c) {
|
|
268
|
+
const next = new Transport(undefined, true);
|
|
269
|
+
next.offset = c.offset;
|
|
270
|
+
next.u = c.u;
|
|
271
|
+
next.nextId = c.nextId;
|
|
272
|
+
next.now = c.now;
|
|
273
|
+
next.pace = c.pace === null ? null : c.pace.until(Number.POSITIVE_INFINITY);
|
|
274
|
+
next.announced = c.announced.map((a) => ({ ...a }));
|
|
275
|
+
// The retargets and pushes the tape made carry their times, so every stop shares one list.
|
|
276
|
+
next.ahead = c.ahead;
|
|
277
|
+
return next;
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* A mix's copy for a read ahead: its voices as they stand, each subject carried from the live one,
|
|
281
|
+
* or from the copy at the known event before.
|
|
282
|
+
*/
|
|
103
283
|
function copyAhead(mix, c) {
|
|
104
284
|
mirror(mix, c);
|
|
105
285
|
c.cued = mix.cued
|
|
@@ -112,7 +292,8 @@ function copyAhead(mix, c) {
|
|
|
112
292
|
return copy;
|
|
113
293
|
});
|
|
114
294
|
ownBlends(c.cued);
|
|
115
|
-
|
|
295
|
+
// A copy keeps no list of owners, so the one before says only by its voices.
|
|
296
|
+
if (mix.owners !== null || (mix.projecting && c.cued.some((v) => v.holding !== null)))
|
|
116
297
|
relink(c.cued);
|
|
117
298
|
recount(c);
|
|
118
299
|
}
|
|
@@ -136,11 +317,26 @@ function copyBack(mix, c, t, cut) {
|
|
|
136
317
|
const copy = v.copy((subject) => recall(mix, v, subject, t, cut, c, copy), lastWithin(log, cut) ?? log[0]);
|
|
137
318
|
if (then !== undefined)
|
|
138
319
|
copy.setting.host = then;
|
|
320
|
+
// Its own, which a fade the copy plays out writes to.
|
|
321
|
+
if (v.parts !== null)
|
|
322
|
+
copy.parts = new Map(v.parts);
|
|
323
|
+
if (v.parted !== null)
|
|
324
|
+
copy.parted = new Map(v.parted);
|
|
325
|
+
if (v.left !== null)
|
|
326
|
+
fadesAt(copy, v.left.after(cut), cut.seq);
|
|
327
|
+
if (copy.parted !== null)
|
|
328
|
+
for (const [subject, p] of copy.parted) {
|
|
329
|
+
// A ramp that has ended since was still running at `t`.
|
|
330
|
+
if (p.ramp === undefined || !(t < p.at))
|
|
331
|
+
continue;
|
|
332
|
+
copy.parts ??= new Map();
|
|
333
|
+
copy.parts.set(subject, p.ramp);
|
|
334
|
+
}
|
|
139
335
|
return copy;
|
|
140
336
|
});
|
|
141
337
|
ownBlends(c.cued);
|
|
142
338
|
if (mix.owners !== null)
|
|
143
|
-
relink(c.cued);
|
|
339
|
+
relink(c.cued, cut.seq);
|
|
144
340
|
stateAt(c, t);
|
|
145
341
|
recount(c);
|
|
146
342
|
}
|
|
@@ -166,7 +362,7 @@ function recordOf(mix, voice, subject) {
|
|
|
166
362
|
// A voice sharing one record has that for every subject a chain has asked it about, pointed at
|
|
167
363
|
// the subject for the caller to copy from.
|
|
168
364
|
if (voice.sharing) {
|
|
169
|
-
const slot = mix
|
|
365
|
+
const slot = headOf(mix, subject)?.slot ?? -1;
|
|
170
366
|
if (!sighted(voice, slot))
|
|
171
367
|
return undefined;
|
|
172
368
|
const all = voice.everyone;
|
|
@@ -177,7 +373,7 @@ function recordOf(mix, voice, subject) {
|
|
|
177
373
|
const held = voice.subjects.get(subject);
|
|
178
374
|
if (held !== undefined || voice.unreachedBits === null)
|
|
179
375
|
return held;
|
|
180
|
-
return unreached(voice, mix
|
|
376
|
+
return unreached(voice, headOf(mix, subject)?.slot ?? -1)
|
|
181
377
|
? voice.unreached
|
|
182
378
|
: undefined;
|
|
183
379
|
}
|
|
@@ -202,12 +398,16 @@ function carry(mix, voice, subject, c, copy) {
|
|
|
202
398
|
* voice's start with fresh state, which is exact for a voice stepped at a fixed interval.
|
|
203
399
|
*/
|
|
204
400
|
export function recall(mix, voice, subject, t, cut, c, copy) {
|
|
205
|
-
const live = voice.left?.at(subject, cut) ??
|
|
401
|
+
const live = voice.left?.at(subject, cut) ??
|
|
402
|
+
heldAt(voice, subject, recordOf(mix, voice, subject), t, cut.seq);
|
|
206
403
|
if (live === undefined)
|
|
207
404
|
return undefined;
|
|
208
405
|
const snap = live.snaps && last(live.snaps, t, true);
|
|
209
406
|
if (snap) {
|
|
210
407
|
const h = copyHeld(voice, snap.held);
|
|
408
|
+
// A start still ahead when the copy was kept may have moved before `t`, with no copy between.
|
|
409
|
+
if (c !== undefined && copy !== undefined && !(h.since <= snap.at))
|
|
410
|
+
origin(c, copy, h);
|
|
211
411
|
h.from = h.stepped;
|
|
212
412
|
h.replay = live.inputs;
|
|
213
413
|
return h;
|
|
@@ -215,17 +415,16 @@ export function recall(mix, voice, subject, t, cut, c, copy) {
|
|
|
215
415
|
// Its origin by the controls the voice had then: a seek or an anchor has moved a waiting
|
|
216
416
|
// voice's since.
|
|
217
417
|
const since = c === undefined || copy === undefined ? live.since : c.sinceOf(copy, live.delay);
|
|
218
|
-
const stepped = since < t ? since : t;
|
|
219
418
|
const h = record(voice, live.reaches, live.reaches && voice.patch.state
|
|
220
419
|
? voice.patch.state(subject)
|
|
221
420
|
: undefined);
|
|
222
421
|
h.delay = live.delay;
|
|
223
422
|
h.since = since;
|
|
224
423
|
h.shown = c === undefined || copy === undefined ? live.shown : c.shownOf(copy, since);
|
|
225
|
-
|
|
424
|
+
const fresh = meet(mix, voice, h, t);
|
|
226
425
|
h.rebuilt = voice.rebuilds;
|
|
227
|
-
h.from = stepped;
|
|
228
|
-
h.unknown = voice.spec.from === 'current';
|
|
426
|
+
h.from = h.stepped;
|
|
427
|
+
h.unknown = voice.spec.from === 'current' || fresh;
|
|
229
428
|
h.replay = live.inputs;
|
|
230
429
|
return h;
|
|
231
430
|
}
|
|
@@ -286,10 +485,15 @@ function doubtOf(mix, voice, held) {
|
|
|
286
485
|
return 'held';
|
|
287
486
|
}
|
|
288
487
|
if (mix.now > (held.from ?? mix.now)) {
|
|
289
|
-
const
|
|
290
|
-
|
|
291
|
-
|
|
488
|
+
const fixed = stepOf(mix, voice) !== undefined;
|
|
489
|
+
// A read ahead reads a signal's state at every known event, and one step across a stretch
|
|
490
|
+
// with none in it is as good as any number; a read back steps it from a copy in one go.
|
|
491
|
+
const kept = held.kept !== null && held.kept.size > 0 && !replayed && mix.backward;
|
|
492
|
+
if ((voice.patch.step && !fixed) || kept)
|
|
292
493
|
return 'stepped';
|
|
293
494
|
}
|
|
495
|
+
// A seek stepped it from a copy, and what it keeps now goes on from there.
|
|
496
|
+
if (held.copied && held.kept !== null && held.kept.size > 0 && !replayed)
|
|
497
|
+
return 'stepped';
|
|
294
498
|
return 'exact';
|
|
295
499
|
}
|
package/dist/pull.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
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
|
+
/** What `pull` remembers at a position it has read no subject at. */
|
|
5
|
+
const UNREAD = Symbol('unread');
|
|
4
6
|
/** Writes a folded pose's value into subject `n`'s places in a column. */
|
|
5
7
|
function writeValue(c, v, n) {
|
|
6
8
|
const at = n * c.axes;
|
|
@@ -76,19 +78,33 @@ export function pull(mix, subjects, into) {
|
|
|
76
78
|
// all of them at once while nothing has relinked since the last pull, else one by one.
|
|
77
79
|
const was = mix.pulled;
|
|
78
80
|
const heads = mix.pulledHeads;
|
|
79
|
-
was.length = list.length;
|
|
80
|
-
heads.length = list.length;
|
|
81
81
|
if (mix.pulledSlots.length < list.length) {
|
|
82
|
-
const slots = new Int32Array(Math.max(64, list.length));
|
|
82
|
+
const slots = new Int32Array(Math.max(64, list.length)).fill(-1);
|
|
83
83
|
slots.set(mix.pulledSlots);
|
|
84
84
|
mix.pulledSlots = slots;
|
|
85
85
|
}
|
|
86
|
+
// A position the list no longer has, or has anew, holds no head: its slot says so.
|
|
87
|
+
if (list.length < was.length)
|
|
88
|
+
mix.pulledSlots.fill(-1, list.length, was.length);
|
|
89
|
+
// A new position holds a mark no subject equals: a hole would read as `undefined`, a subject.
|
|
90
|
+
for (let n = was.length; n < list.length; n++)
|
|
91
|
+
was.push(UNREAD);
|
|
92
|
+
was.length = list.length;
|
|
93
|
+
heads.length = list.length;
|
|
86
94
|
const version = mix.version;
|
|
87
95
|
const relinks = mix.relinks;
|
|
88
96
|
const live = !Number.isNaN(now);
|
|
89
97
|
const current = live && mix.pulledVersion === version && mix.pulledRelinks === relinks;
|
|
98
|
+
// Which remembered heads `pullRun` takes as current: all while nothing has changed; those
|
|
99
|
+
// `unlink` has not stamped since `pulledRelinks` while the version they were linked at stands;
|
|
100
|
+
// none otherwise.
|
|
101
|
+
const since = current
|
|
102
|
+
? Number.POSITIVE_INFINITY
|
|
103
|
+
: live && mix.pulledVersion === version
|
|
104
|
+
? mix.pulledRelinks
|
|
105
|
+
: -1;
|
|
90
106
|
try {
|
|
91
|
-
readAll(mix, list, columns, live, current, whole);
|
|
107
|
+
readAll(mix, list, columns, live, current, since, whole);
|
|
92
108
|
}
|
|
93
109
|
finally {
|
|
94
110
|
if (lanes !== null)
|
|
@@ -104,7 +120,7 @@ export function pull(mix, subjects, into) {
|
|
|
104
120
|
* compile the loop on the stack does not leave `pull`'s own code compiled before its tail had run:
|
|
105
121
|
* under churn that code deoptimized at the tail on every frame.
|
|
106
122
|
*/
|
|
107
|
-
function readAll(mix, list, columns, live, current, whole) {
|
|
123
|
+
function readAll(mix, list, columns, live, current, since, whole) {
|
|
108
124
|
const lanes = mix.lanes;
|
|
109
125
|
const now = mix.now;
|
|
110
126
|
const scratch = mix.scratch;
|
|
@@ -116,7 +132,7 @@ function readAll(mix, list, columns, live, current, whole) {
|
|
|
116
132
|
const runs = live && whole;
|
|
117
133
|
for (let n = 0; n < list.length; n++) {
|
|
118
134
|
if (runs) {
|
|
119
|
-
n = pullRun(lanes, slots, list, was,
|
|
135
|
+
n = pullRun(lanes, slots, list, was, since, n, columns, now, mix.version);
|
|
120
136
|
if (n === list.length)
|
|
121
137
|
break;
|
|
122
138
|
}
|
|
@@ -149,13 +165,24 @@ function readAll(mix, list, columns, live, current, whole) {
|
|
|
149
165
|
writeValue(c, pose[c.key], n);
|
|
150
166
|
}
|
|
151
167
|
}
|
|
152
|
-
/**
|
|
168
|
+
/**
|
|
169
|
+
* The kit slot, width, rest and bounds of every channel `pull` was handed an array for: one record
|
|
170
|
+
* a channel for the mix's life, pointed at the array this call was handed.
|
|
171
|
+
*/
|
|
153
172
|
function columnsOf(mix, into) {
|
|
154
173
|
const columns = [];
|
|
174
|
+
mix.pullColumns ??= new Map();
|
|
175
|
+
const known = mix.pullColumns;
|
|
155
176
|
for (const key of Object.keys(into)) {
|
|
156
177
|
const out = into[key];
|
|
157
178
|
if (out === undefined)
|
|
158
179
|
continue;
|
|
180
|
+
const had = known.get(key);
|
|
181
|
+
if (had !== undefined) {
|
|
182
|
+
had.out = out;
|
|
183
|
+
columns.push(had);
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
159
186
|
const slot = mix.slotOf.get(key);
|
|
160
187
|
if (slot === undefined)
|
|
161
188
|
throw new Error(`blits: pull was handed ${key}, which the kit lacks`);
|
|
@@ -170,7 +197,9 @@ function columnsOf(mix, into) {
|
|
|
170
197
|
at.set(rest);
|
|
171
198
|
if (bounds !== undefined)
|
|
172
199
|
clampRun(at, 0, axes, bounds);
|
|
173
|
-
|
|
200
|
+
const column = { key, slot, axes, out, rest: at, bounds };
|
|
201
|
+
known.set(key, column);
|
|
202
|
+
columns.push(column);
|
|
174
203
|
}
|
|
175
204
|
return columns;
|
|
176
205
|
}
|
package/dist/qualify.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Lanes } from './lanes.js';
|
|
2
|
+
import type { Channel } from './types.js';
|
|
2
3
|
import type { Voice } from './voice.js';
|
|
3
4
|
/** What qualifying needs of one voice: whether its patch and spec can run on a lane, and its channels. */
|
|
4
5
|
export interface Candidate {
|
|
@@ -16,6 +17,15 @@ export declare function qualify(numeric: readonly boolean[], voices: readonly Ca
|
|
|
16
17
|
voices: Set<number>;
|
|
17
18
|
};
|
|
18
19
|
export declare function requalify<I, O>(lanes: Lanes<I, O>, version: number): void;
|
|
20
|
+
/** Whether `keepCallsOff` applies; settable for bench/frame.mjs. */
|
|
21
|
+
export declare let callsOff: boolean;
|
|
22
|
+
export declare function setCallsOff(on: boolean): void;
|
|
23
|
+
/**
|
|
24
|
+
* A voice whose patch a lane would call, as a fn voice's (no tween, keys or motion), writing only
|
|
25
|
+
* arrays: a crowd writing a number reads 17% to 25% faster on lanes (bench/frame.mjs `named` under
|
|
26
|
+
* CALLS_OFF, msb-uai, 2026-10-21).
|
|
27
|
+
*/
|
|
28
|
+
export declare function calls<I, O>(v: Voice<I, O>, channels: readonly Channel<unknown>[]): boolean;
|
|
19
29
|
/**
|
|
20
30
|
* A voice starts playing on a lane or crowd: its epoch. Every subject meets a voice over all of
|
|
21
31
|
* them on its next probe; a voice naming its subjects marks only those.
|
package/dist/qualify.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { numericOf } from './channels.js';
|
|
2
2
|
import { flush } from './columns.js';
|
|
3
3
|
import { crowdable } from './crowd.js';
|
|
4
|
+
import { byId } from './due.js';
|
|
4
5
|
import { weighAt } from './everyone.js';
|
|
5
6
|
import { wake } from './fill.js';
|
|
6
7
|
import { Lane } from './lane.js';
|
|
@@ -52,6 +53,8 @@ export function requalify(lanes, version) {
|
|
|
52
53
|
const numeric = host.channels.map((c) => numericOf(c) !== undefined);
|
|
53
54
|
// A voice writing nothing has no lane to fill, so it stays where its calls are made.
|
|
54
55
|
const fits = present.map((v) => v.slots.length > 0 && host.fits(v));
|
|
56
|
+
if (callsOff)
|
|
57
|
+
keepCallsOff(present, fits, host.channels);
|
|
55
58
|
const candidates = () => present.map((v, i) => ({ id: v.id, fits: fits[i], slots: v.slots }));
|
|
56
59
|
let { channels, voices } = qualify(numeric, candidates());
|
|
57
60
|
// A locus folds its members together, so they run on lanes all together or not at all.
|
|
@@ -192,6 +195,54 @@ export function requalify(lanes, version) {
|
|
|
192
195
|
}
|
|
193
196
|
regroup(lanes, members);
|
|
194
197
|
}
|
|
198
|
+
/** Whether `keepCallsOff` applies; settable for bench/frame.mjs. */
|
|
199
|
+
export let callsOff = true;
|
|
200
|
+
export function setCallsOff(on) {
|
|
201
|
+
callsOff = on;
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* A voice whose patch a lane would call, as a fn voice's (no tween, keys or motion), writing only
|
|
205
|
+
* arrays: a crowd writing a number reads 17% to 25% faster on lanes (bench/frame.mjs `named` under
|
|
206
|
+
* CALLS_OFF, msb-uai, 2026-10-21).
|
|
207
|
+
*/
|
|
208
|
+
export function calls(v, channels) {
|
|
209
|
+
return (v.built === null &&
|
|
210
|
+
v.motion === undefined &&
|
|
211
|
+
v.slots.every((s) => vector(channels[s])));
|
|
212
|
+
}
|
|
213
|
+
/** A channel folding arrays with a rest, not `last`: what the rule was timed on. */
|
|
214
|
+
function vector(channel) {
|
|
215
|
+
return Array.isArray(channel.rest) && numericOf(channel)?.op !== 'last';
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Leaves off lanes each voice calling its patch where no other fitting voice writes any of its
|
|
219
|
+
* channels, but a crowd's other voices: a lane calls such a patch once a subject as the general
|
|
220
|
+
* path does, and its fill walks the subjects a second time (bench/frame.mjs `fns`, 2.58 ms a frame
|
|
221
|
+
* on lanes against 1.07 off, studio, 2026-10-21). Two voices over many subjects on one channel
|
|
222
|
+
* fold faster on lanes (`tweenfn` with three voices).
|
|
223
|
+
*/
|
|
224
|
+
function keepCallsOff(present, fits, channels) {
|
|
225
|
+
const others = new Array(channels.length).fill(false);
|
|
226
|
+
const wide = new Array(channels.length).fill(false);
|
|
227
|
+
present.forEach((v, i) => {
|
|
228
|
+
if (!fits[i])
|
|
229
|
+
return;
|
|
230
|
+
const crowd = crowdable(v);
|
|
231
|
+
if (!calls(v, channels))
|
|
232
|
+
for (const s of v.slots)
|
|
233
|
+
others[s] = true;
|
|
234
|
+
else if (!crowd)
|
|
235
|
+
for (const s of v.slots) {
|
|
236
|
+
if (wide[s])
|
|
237
|
+
others[s] = true;
|
|
238
|
+
wide[s] = true;
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
present.forEach((v, i) => {
|
|
242
|
+
if (fits[i] && calls(v, channels) && !v.slots.some((s) => others[s]))
|
|
243
|
+
fits[i] = false;
|
|
244
|
+
});
|
|
245
|
+
}
|
|
195
246
|
/** Whether qualifying laned exactly the channels laned now. */
|
|
196
247
|
function sameChannels(lanes, channels) {
|
|
197
248
|
const by = lanes.bySlot;
|
|
@@ -231,8 +282,13 @@ export function open(lanes, v) {
|
|
|
231
282
|
* A crowd a quarter empty rows is compacted.
|
|
232
283
|
*/
|
|
233
284
|
export function retouch(lanes) {
|
|
234
|
-
const touched =
|
|
235
|
-
lanes.touched
|
|
285
|
+
const touched = byId(lanes.touched);
|
|
286
|
+
lanes.touched = [];
|
|
287
|
+
let kept = 0;
|
|
288
|
+
for (let i = 0; i < touched.length; i++)
|
|
289
|
+
if (i === 0 || touched[i] !== touched[i - 1])
|
|
290
|
+
touched[kept++] = touched[i];
|
|
291
|
+
touched.length = kept;
|
|
236
292
|
// The qualify a false answer calls takes up every touched voice, crowd rows starting included.
|
|
237
293
|
const qualify = () => {
|
|
238
294
|
lanes.touched = touched;
|
|
@@ -267,7 +323,8 @@ export function retouch(lanes) {
|
|
|
267
323
|
return qualify();
|
|
268
324
|
const laned = v.slots.some((slot) => lanes.bySlot[slot] !== undefined);
|
|
269
325
|
const fits = v.slots.length > 0 && lanes.host.fits(v);
|
|
270
|
-
|
|
326
|
+
// As `keepCallsOff`: with none of its channels laned, no fitting voice but its like writes them.
|
|
327
|
+
if (!laned && (!fits || (callsOff && crowdable(v) && calls(v, lanes.host.channels)))) {
|
|
271
328
|
lanes.whole = false;
|
|
272
329
|
continue;
|
|
273
330
|
}
|
package/dist/reading.d.ts
CHANGED
|
@@ -7,7 +7,8 @@ export declare const reading: {
|
|
|
7
7
|
live: boolean;
|
|
8
8
|
/**
|
|
9
9
|
* The earliest voice time a read may still ask for; a patch keeping a history may drop what is
|
|
10
|
-
* older. Infinity where the mix keeps none, so only what plays now is kept.
|
|
10
|
+
* older. Infinity where the mix keeps none, so only what plays now is kept. Set before a motion
|
|
11
|
+
* voice's read alone, so another patch finds whatever the last one left.
|
|
11
12
|
*/
|
|
12
13
|
horizon: number;
|
|
13
14
|
/**
|
|
@@ -25,4 +26,10 @@ export declare const reading: {
|
|
|
25
26
|
* since it read it. One count for every mix, as `moved` is.
|
|
26
27
|
*/
|
|
27
28
|
inputs: number;
|
|
29
|
+
/**
|
|
30
|
+
* While a read ahead that played its tape's calls reads: the retargets and pushes those calls
|
|
31
|
+
* made, by motion patch and then subject, in time order. A patch applies them with its own and
|
|
32
|
+
* commits none. Null for every other read.
|
|
33
|
+
*/
|
|
34
|
+
ahead: Map<object, Map<unknown, unknown[]>> | null;
|
|
28
35
|
};
|
package/dist/reading.js
CHANGED
|
@@ -7,7 +7,8 @@ export const reading = {
|
|
|
7
7
|
live: true,
|
|
8
8
|
/**
|
|
9
9
|
* The earliest voice time a read may still ask for; a patch keeping a history may drop what is
|
|
10
|
-
* older. Infinity where the mix keeps none, so only what plays now is kept.
|
|
10
|
+
* older. Infinity where the mix keeps none, so only what plays now is kept. Set before a motion
|
|
11
|
+
* voice's read alone, so another patch finds whatever the last one left.
|
|
11
12
|
*/
|
|
12
13
|
horizon: Number.POSITIVE_INFINITY,
|
|
13
14
|
/**
|
|
@@ -25,4 +26,10 @@ export const reading = {
|
|
|
25
26
|
* since it read it. One count for every mix, as `moved` is.
|
|
26
27
|
*/
|
|
27
28
|
inputs: 0,
|
|
29
|
+
/**
|
|
30
|
+
* While a read ahead that played its tape's calls reads: the retargets and pushes those calls
|
|
31
|
+
* made, by motion patch and then subject, in time order. A patch applies them with its own and
|
|
32
|
+
* commits none. Null for every other read.
|
|
33
|
+
*/
|
|
34
|
+
ahead: null,
|
|
28
35
|
};
|
package/dist/record.d.ts
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import type { Subject } from './voice.js';
|
|
2
|
+
/** A record with every field written out, optional ones too, as every record in a mix is built. */
|
|
3
|
+
export type WholeRecord = {
|
|
4
|
+
[K in Field]: Subject<unknown>[K];
|
|
5
|
+
};
|
|
6
|
+
type Field = keyof Subject<unknown>;
|
|
2
7
|
/**
|
|
3
8
|
* A subject's record for `voice` holding every field one can come to hold, in one order, so every
|
|
4
9
|
* record in a mix shares one hidden class: a record built elsewhere, or given `base`, `snaps` or
|
|
@@ -7,3 +12,4 @@ import type { Subject } from './voice.js';
|
|
|
7
12
|
* does not inline would box.
|
|
8
13
|
*/
|
|
9
14
|
export declare function record(voice: object | null, reaches: boolean, state: unknown): Subject<unknown>;
|
|
15
|
+
export {};
|