@msb235/blits 0.4.0 → 0.5.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 +262 -0
- package/README.md +2 -1
- package/dist/bare.d.ts +29 -0
- package/dist/bare.js +105 -0
- package/dist/blend.d.ts +34 -0
- package/dist/blend.js +102 -0
- package/dist/book.d.ts +53 -0
- package/dist/book.js +218 -0
- package/dist/chain.d.ts +22 -0
- package/dist/chain.js +150 -0
- package/dist/channels.d.ts +2 -0
- package/dist/channels.js +3 -1
- package/dist/clock.d.ts +50 -3
- package/dist/clock.js +102 -9
- package/dist/clone.d.ts +7 -0
- package/dist/clone.js +57 -0
- package/dist/columns.d.ts +29 -0
- package/dist/columns.js +183 -0
- package/dist/crowd.d.ts +128 -0
- package/dist/crowd.js +354 -0
- package/dist/cue.d.ts +3 -0
- package/dist/cue.js +115 -0
- package/dist/due.d.ts +12 -0
- package/dist/due.js +93 -0
- package/dist/fade.d.ts +19 -0
- package/dist/fade.js +133 -0
- package/dist/fill.d.ts +42 -0
- package/dist/fill.js +381 -0
- package/dist/fold.d.ts +51 -0
- package/dist/fold.js +475 -0
- package/dist/gather.d.ts +24 -0
- package/dist/gather.js +186 -0
- package/dist/handle.d.ts +31 -0
- package/dist/handle.js +56 -0
- package/dist/held.d.ts +14 -0
- package/dist/held.js +117 -0
- package/dist/history.d.ts +22 -0
- package/dist/history.js +149 -0
- package/dist/hosts.d.ts +16 -0
- package/dist/hosts.js +178 -0
- package/dist/index.d.ts +1 -1
- package/dist/keyrows.d.ts +36 -0
- package/dist/keyrows.js +138 -0
- package/dist/lane.d.ts +210 -0
- package/dist/lane.js +187 -0
- package/dist/lanes.d.ts +121 -246
- package/dist/lanes.js +150 -1578
- package/dist/locus.d.ts +40 -0
- package/dist/locus.js +89 -0
- package/dist/marks.d.ts +12 -0
- package/dist/marks.js +80 -0
- package/dist/meet.d.ts +16 -0
- package/dist/meet.js +113 -0
- package/dist/mixer.d.ts +244 -196
- package/dist/mixer.js +242 -2220
- package/dist/motion.d.ts +66 -21
- package/dist/motion.js +325 -165
- package/dist/move.d.ts +9 -0
- package/dist/move.js +190 -0
- package/dist/named.d.ts +12 -0
- package/dist/named.js +39 -0
- package/dist/numbers.d.ts +18 -6
- package/dist/numbers.js +59 -31
- package/dist/owed.d.ts +24 -0
- package/dist/owed.js +51 -0
- package/dist/owner.d.ts +43 -0
- package/dist/owner.js +117 -0
- package/dist/pace.d.ts +40 -0
- package/dist/pace.js +113 -0
- package/dist/patch.d.ts +51 -10
- package/dist/patch.js +140 -45
- package/dist/place.d.ts +20 -0
- package/dist/place.js +196 -0
- package/dist/project.d.ts +4 -0
- package/dist/project.js +233 -0
- package/dist/pull.d.ts +5 -0
- package/dist/pull.js +175 -0
- package/dist/qualify.d.ts +31 -0
- package/dist/qualify.js +345 -0
- package/dist/relink.d.ts +25 -0
- package/dist/relink.js +59 -0
- package/dist/rows.d.ts +33 -0
- package/dist/rows.js +282 -0
- package/dist/sample.d.ts +21 -0
- package/dist/sample.js +232 -0
- package/dist/signals.d.ts +12 -9
- package/dist/signals.js +8 -5
- package/dist/store.d.ts +10 -3
- package/dist/store.js +44 -9
- package/dist/strays.d.ts +21 -0
- package/dist/strays.js +84 -0
- package/dist/tweened.d.ts +11 -0
- package/dist/tweened.js +48 -0
- package/dist/types.d.ts +217 -34
- package/dist/unreached.d.ts +5 -0
- package/dist/unreached.js +24 -0
- package/dist/voice.d.ts +240 -0
- package/dist/voice.js +322 -0
- package/dist/weigh.d.ts +26 -0
- package/dist/weigh.js +89 -0
- package/docs/schema.html +118 -40
- package/package.json +5 -2
package/dist/place.js
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { noted } from './history.js';
|
|
2
|
+
import { markOf } from './marks.js';
|
|
3
|
+
import { mixTime, ownerReading } from './owner.js';
|
|
4
|
+
import { none } from './voice.js';
|
|
5
|
+
/** What a voice's own owner's clock reads now: the mix clock for a voice no owner holds. */
|
|
6
|
+
export function localNow(mix, voice) {
|
|
7
|
+
return voice.owner === null ? mix.now : ownerReading(voice.owner, mix.now);
|
|
8
|
+
}
|
|
9
|
+
/** A voice's start as mix time. */
|
|
10
|
+
export function startOf(voice) {
|
|
11
|
+
return voice.owner === null ? voice.start : mixTime(voice.owner, voice.start);
|
|
12
|
+
}
|
|
13
|
+
/** Mix time at host time `u`, for something pinned there; Infinity while the mix may never get there. */
|
|
14
|
+
export function mixAt(mix, u) {
|
|
15
|
+
return mix.pace === null ? u : mix.pace.at(u, mix.u);
|
|
16
|
+
}
|
|
17
|
+
export function pin(mix, voice, u) {
|
|
18
|
+
mix.pins ??= new Map();
|
|
19
|
+
mix.pins.set(voice, u);
|
|
20
|
+
}
|
|
21
|
+
/** Puts each pending voice's start, given in host time, where the mix's rate now puts it. */
|
|
22
|
+
export function repin(mix) {
|
|
23
|
+
const pins = mix.pins;
|
|
24
|
+
for (const [voice, u] of pins) {
|
|
25
|
+
if (voice.state !== 'pending')
|
|
26
|
+
pins.delete(voice);
|
|
27
|
+
else
|
|
28
|
+
startAt(mix, voice, mixAt(mix, u));
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/** Moves a pending voice's start, its clock with it; false where it is already there. */
|
|
32
|
+
function startAt(mix, voice, start) {
|
|
33
|
+
if (start === voice.start)
|
|
34
|
+
return false;
|
|
35
|
+
voice.start = start;
|
|
36
|
+
voice.anchorNow = start;
|
|
37
|
+
voice.anchorElapsed = 0;
|
|
38
|
+
noted(mix, voice);
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
/** Refuses a placement that names a mark twice on one side, or that waits on itself. */
|
|
42
|
+
export function checkPlacement(mix, spec, anchor, owner) {
|
|
43
|
+
if (anchor.start !== undefined && anchor.in !== undefined)
|
|
44
|
+
throw new Error('blits: a placement anchors start or in, not both');
|
|
45
|
+
if (anchor.out !== undefined && anchor.end !== undefined)
|
|
46
|
+
throw new Error('blits: a placement anchors out or end, not both');
|
|
47
|
+
if (spec.start !== undefined && (anchor.start !== undefined || anchor.in !== undefined))
|
|
48
|
+
throw new Error('blits: a voice takes start or an anchored start, not both');
|
|
49
|
+
const name = spec.name;
|
|
50
|
+
if (name === undefined)
|
|
51
|
+
return;
|
|
52
|
+
// A voice is known by its owner, its score and its name; a bare name in an anchor is in the
|
|
53
|
+
// asker's score under the asker's owner.
|
|
54
|
+
const key = (o, score, n) => `${o?.id ?? 0}\u0000${score ?? ''}\u0000${n}`;
|
|
55
|
+
const names = (p, score, o) => [p.start, p.in, p.out, p.end].flatMap((a) => {
|
|
56
|
+
if (a === undefined || typeof a === 'number')
|
|
57
|
+
return [];
|
|
58
|
+
const q = 'of' in a ? a.of : 'after' in a ? a.after : 'with' in a ? a.with : a.before;
|
|
59
|
+
if (typeof q === 'string')
|
|
60
|
+
return [key(o, score, q)];
|
|
61
|
+
return q.name === undefined ? [] : [key(o, q.score ?? score, q.name)];
|
|
62
|
+
});
|
|
63
|
+
const self = key(owner, spec.score, name);
|
|
64
|
+
const seen = new Set();
|
|
65
|
+
const waits = names(anchor, spec.score, owner);
|
|
66
|
+
while (waits.length > 0) {
|
|
67
|
+
const n = waits.pop();
|
|
68
|
+
if (n === self)
|
|
69
|
+
throw new Error(`blits: ${name}'s placement waits on itself`);
|
|
70
|
+
if (seen.has(n))
|
|
71
|
+
continue;
|
|
72
|
+
seen.add(n);
|
|
73
|
+
for (const v of mix.cued)
|
|
74
|
+
if (v.spec.name !== undefined &&
|
|
75
|
+
key(v.owner, v.spec.score, v.spec.name) === n &&
|
|
76
|
+
v.spec.anchor)
|
|
77
|
+
waits.push(...names(v.spec.anchor, v.spec.score, v.owner));
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Fixes every anchored voice's start and out from what its anchors answer now. A start is fixed
|
|
82
|
+
* while the voice waits and an out until its fade begins; a target that has left keeps the time it
|
|
83
|
+
* last gave. Repeated so a chain of anchors settles in one sync.
|
|
84
|
+
*/
|
|
85
|
+
export function place(mix) {
|
|
86
|
+
for (let round = 0; round <= mix.cued.length; round++) {
|
|
87
|
+
let moved = false;
|
|
88
|
+
for (const voice of mix.cued) {
|
|
89
|
+
const anchor = voice.spec.anchor;
|
|
90
|
+
if (anchor === undefined || voice.state === 'done')
|
|
91
|
+
continue;
|
|
92
|
+
if (voice.placing && voice.state === 'pending') {
|
|
93
|
+
const by = anchor.start ?? anchor.in;
|
|
94
|
+
const t = by === undefined ? undefined : resolve(mix, by, voice);
|
|
95
|
+
if (t !== undefined) {
|
|
96
|
+
const o = voice.owner;
|
|
97
|
+
const fadeIn = voice.fade.in ?? 0;
|
|
98
|
+
const start = anchor.start !== undefined
|
|
99
|
+
? t
|
|
100
|
+
: o === null
|
|
101
|
+
? t - fadeIn
|
|
102
|
+
: ownerReading(o, mixTime(o, t) - fadeIn);
|
|
103
|
+
if (Number.isFinite(start) && startAt(mix, voice, start))
|
|
104
|
+
moved = true;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
if (voice.state !== 'fading') {
|
|
108
|
+
const by = anchor.out ?? anchor.end;
|
|
109
|
+
const t = by === undefined ? undefined : resolve(mix, by, voice);
|
|
110
|
+
if (t !== undefined) {
|
|
111
|
+
const m = voice.owner === null ? t : mixTime(voice.owner, t);
|
|
112
|
+
const at = anchor.out !== undefined ? m : m - (voice.fade.out ?? 0);
|
|
113
|
+
if (at !== voice.outAt) {
|
|
114
|
+
voice.outAt = at;
|
|
115
|
+
noted(mix, voice);
|
|
116
|
+
moved = true;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
if (!moved)
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
/** The time an anchor answers on its voice's owner's clock, or undefined while its target has none. */
|
|
126
|
+
function resolve(mix, a, self) {
|
|
127
|
+
const o = self.owner;
|
|
128
|
+
if (typeof a === 'number')
|
|
129
|
+
return o === null ? mixAt(mix, a - mix.offset) : a;
|
|
130
|
+
let query;
|
|
131
|
+
let mark;
|
|
132
|
+
let by = a.by ?? 0;
|
|
133
|
+
if ('of' in a) {
|
|
134
|
+
query = a.of;
|
|
135
|
+
mark = a.mark;
|
|
136
|
+
}
|
|
137
|
+
else if ('after' in a) {
|
|
138
|
+
query = a.after;
|
|
139
|
+
mark = 'end';
|
|
140
|
+
}
|
|
141
|
+
else if ('with' in a) {
|
|
142
|
+
query = a.with;
|
|
143
|
+
mark = 'start';
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
query = a.before;
|
|
147
|
+
mark = 'start';
|
|
148
|
+
by = -by;
|
|
149
|
+
}
|
|
150
|
+
const t = timeOf(mix, typeof query === 'string' ? { name: query } : query, mark, self);
|
|
151
|
+
if (t === undefined)
|
|
152
|
+
return undefined;
|
|
153
|
+
if (o === null)
|
|
154
|
+
return t + by;
|
|
155
|
+
const local = ownerReading(o, t);
|
|
156
|
+
return Number.isFinite(local) ? local + by : undefined;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* The mix time a query answers: `mark` of the voice it picks, among those cued and those that have
|
|
160
|
+
* left, never the asker; or a mark the host announced on the score, which is the same time for
|
|
161
|
+
* any of the four. Undefined while what it picks has no such time. A query naming no score looks
|
|
162
|
+
* among the asker's siblings, the voices its owner holds.
|
|
163
|
+
*/
|
|
164
|
+
function timeOf(mix, q, mark, self) {
|
|
165
|
+
const score = q.score ?? self.spec.score;
|
|
166
|
+
const anywhere = q.score !== undefined;
|
|
167
|
+
const found = [];
|
|
168
|
+
for (const v of [...mix.gone, ...mix.cued])
|
|
169
|
+
if (v !== self &&
|
|
170
|
+
v.spec.score === score &&
|
|
171
|
+
(anywhere || v.owner === self.owner) &&
|
|
172
|
+
(q.name === undefined || v.spec.name === q.name) &&
|
|
173
|
+
(q.tag === undefined || (v.spec.tags ?? none).includes(q.tag)) &&
|
|
174
|
+
(q.writes === undefined || v.patch.writes.includes(q.writes)))
|
|
175
|
+
found.push({ order: v.id, t: markOf(mix, v, mark) });
|
|
176
|
+
if (q.writes === undefined && (anywhere || self.owner === null))
|
|
177
|
+
for (const a of mix.announced)
|
|
178
|
+
if (a.score === score &&
|
|
179
|
+
(q.name === undefined || a.name === q.name) &&
|
|
180
|
+
(q.tag === undefined || a.tags.includes(q.tag)))
|
|
181
|
+
found.push({ order: a.order, t: Number.isNaN(a.at) ? undefined : mixAt(mix, a.at) });
|
|
182
|
+
if (found.length === 0)
|
|
183
|
+
return undefined;
|
|
184
|
+
found.sort((x, y) => x.order - y.order);
|
|
185
|
+
const resolver = q.resolver ?? 'last';
|
|
186
|
+
if (resolver === 'first')
|
|
187
|
+
return found[0]?.t;
|
|
188
|
+
if (resolver === 'last')
|
|
189
|
+
return found[found.length - 1]?.t;
|
|
190
|
+
const times = found.flatMap((e) => (e.t === undefined ? [] : [e.t])).sort((x, y) => x - y);
|
|
191
|
+
if (resolver === 'next') {
|
|
192
|
+
const now = Number.isNaN(mix.now) ? Number.NEGATIVE_INFINITY : mix.now;
|
|
193
|
+
return times.find((t) => t >= now);
|
|
194
|
+
}
|
|
195
|
+
return resolver === 'earliest' ? times[0] : times[times.length - 1];
|
|
196
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { Mixer } from './mixer.js';
|
|
2
|
+
import type { Projection } from './types.js';
|
|
3
|
+
/** A projection to `timestamp` through `c`, a mixer of its own the mix has just made. */
|
|
4
|
+
export declare function project<I, O>(mix: Mixer<I, O>, c: Mixer<I, O>, timestamp: number): Projection<I, O>;
|
package/dist/project.js
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import { ownBlends } from './blend.js';
|
|
2
|
+
import { index } from './chain.js';
|
|
3
|
+
import { copyHeld, last } from './history.js';
|
|
4
|
+
import { readingAt } from './marks.js';
|
|
5
|
+
import { move } from './move.js';
|
|
6
|
+
import { heldByInput, ownerReading, relink } from './owner.js';
|
|
7
|
+
import { pin } from './place.js';
|
|
8
|
+
import { reading } from './reading.js';
|
|
9
|
+
import { Store } from './store.js';
|
|
10
|
+
import { unreached } from './unreached.js';
|
|
11
|
+
/** A projection to `timestamp` through `c`, a mixer of its own the mix has just made. */
|
|
12
|
+
export function project(mix, c, timestamp) {
|
|
13
|
+
const u = timestamp - mix.offset;
|
|
14
|
+
const pace = mix.pace;
|
|
15
|
+
const t = readingAt(mix, u);
|
|
16
|
+
c.projecting = true;
|
|
17
|
+
c.pose = mix.pose;
|
|
18
|
+
c.offset = mix.offset;
|
|
19
|
+
c.u = u;
|
|
20
|
+
c.wantsPose = mix.wantsPose;
|
|
21
|
+
c.nextId = mix.nextId;
|
|
22
|
+
c.reducedNow = mix.reducedNow;
|
|
23
|
+
if (Number.isNaN(mix.now) || t >= mix.now) {
|
|
24
|
+
c.now = mix.now;
|
|
25
|
+
c.pace = pace === null ? null : pace.until(Number.POSITIVE_INFINITY);
|
|
26
|
+
c.cued = mix.cued
|
|
27
|
+
.filter((v) => v.state !== 'done')
|
|
28
|
+
.map((v) => {
|
|
29
|
+
const copy = v.copy((subject) => carry(mix, v, subject));
|
|
30
|
+
const at = mix.pins?.get(v);
|
|
31
|
+
if (at !== undefined)
|
|
32
|
+
pin(c, copy, at);
|
|
33
|
+
return copy;
|
|
34
|
+
});
|
|
35
|
+
ownBlends(c.cued);
|
|
36
|
+
if (mix.owners !== null)
|
|
37
|
+
relink(c.cued);
|
|
38
|
+
c.announced = mix.announced.map((a) => ({ ...a }));
|
|
39
|
+
count(c);
|
|
40
|
+
move(c, t);
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
const history = mix.opts.history;
|
|
44
|
+
if (!history)
|
|
45
|
+
throw new Error('blits: reading back needs a mix made with history');
|
|
46
|
+
if (t < mix.now - history.ms)
|
|
47
|
+
throw new Error(`blits: ${timestamp} is older than this mix's history reaches`);
|
|
48
|
+
c.now = t;
|
|
49
|
+
c.pace = pace === null ? null : pace.until(u);
|
|
50
|
+
c.backward = true;
|
|
51
|
+
const was = last(mix.hostLog, t, true);
|
|
52
|
+
const host = mix.opts.host;
|
|
53
|
+
const then = was && typeof host === 'object' && host !== null
|
|
54
|
+
? Object.assign(Object.create(host), was.fields)
|
|
55
|
+
: undefined;
|
|
56
|
+
if (was)
|
|
57
|
+
c.hostThen = was;
|
|
58
|
+
c.announced = mix.announced.filter((a) => a.made < t).map((a) => ({ ...a }));
|
|
59
|
+
c.cued = [...mix.cued, ...mix.gone]
|
|
60
|
+
.filter((v) => v.cuedAt <= t && v.doneAt > t)
|
|
61
|
+
.sort((a, b) => a.id - b.id)
|
|
62
|
+
.map((v) => {
|
|
63
|
+
const log = v.log;
|
|
64
|
+
const copy = v.copy((subject) => recall(mix, v, subject, t), last(log, t, (e) => e.sync) ?? log[0]);
|
|
65
|
+
if (then !== undefined)
|
|
66
|
+
copy.setting.host = then;
|
|
67
|
+
return copy;
|
|
68
|
+
});
|
|
69
|
+
ownBlends(c.cued);
|
|
70
|
+
if (mix.owners !== null)
|
|
71
|
+
relink(c.cued);
|
|
72
|
+
for (const copy of c.cued)
|
|
73
|
+
copy.state =
|
|
74
|
+
(copy.owner === null ? t : ownerReading(copy.owner, t)) < copy.start
|
|
75
|
+
? 'pending'
|
|
76
|
+
: copy.out && copy.out.at <= t
|
|
77
|
+
? 'fading'
|
|
78
|
+
: copy.holdsAfter && copy.elapsedAt(t) >= copy.span + copy.latest
|
|
79
|
+
? 'held'
|
|
80
|
+
: 'live';
|
|
81
|
+
count(c);
|
|
82
|
+
}
|
|
83
|
+
const read = (f) => {
|
|
84
|
+
reading.live = false;
|
|
85
|
+
try {
|
|
86
|
+
return f();
|
|
87
|
+
}
|
|
88
|
+
finally {
|
|
89
|
+
reading.live = true;
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
return {
|
|
93
|
+
timestamp,
|
|
94
|
+
probe: (subject, out) => read(() => c.fold(subject, out)),
|
|
95
|
+
assess: (subject) => read(() => {
|
|
96
|
+
c.fold(subject);
|
|
97
|
+
return doubts(c, subject);
|
|
98
|
+
}),
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
/** Recounts what the fold's shortcuts depend on, for a projection's freshly copied voices. */
|
|
102
|
+
function count(mix) {
|
|
103
|
+
mix.named = new Store();
|
|
104
|
+
mix.naming = 0;
|
|
105
|
+
mix.general = [];
|
|
106
|
+
for (const voice of mix.cued)
|
|
107
|
+
index(mix, voice);
|
|
108
|
+
mix.loci = mix.cued.filter((v) => v.spec.locus !== undefined).length;
|
|
109
|
+
mix.anchored = mix.cued.filter((v) => v.spec.anchor !== undefined).length;
|
|
110
|
+
mix.steps.push(null, ++mix.version);
|
|
111
|
+
}
|
|
112
|
+
/** A voice's record of a subject, the one it shares among those it does not reach included. */
|
|
113
|
+
function recordOf(mix, voice, subject) {
|
|
114
|
+
const held = voice.subjects.get(subject);
|
|
115
|
+
if (held !== undefined || voice.unreachedBits === null)
|
|
116
|
+
return held;
|
|
117
|
+
return unreached(voice, mix.chains.get(subject)?.slot ?? -1)
|
|
118
|
+
? voice.unreached
|
|
119
|
+
: undefined;
|
|
120
|
+
}
|
|
121
|
+
/** A projection ahead starts each subject from where the live mix holds it. */
|
|
122
|
+
function carry(mix, voice, subject) {
|
|
123
|
+
const live = recordOf(mix, voice, subject);
|
|
124
|
+
if (live === undefined)
|
|
125
|
+
return undefined;
|
|
126
|
+
const h = copyHeld(voice, live);
|
|
127
|
+
h.from = h.stepped;
|
|
128
|
+
return h;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* A projection back starts each subject from the latest copy kept at or before `t`, else from its
|
|
132
|
+
* voice's start with fresh state, which is exact for a voice stepped at a fixed interval.
|
|
133
|
+
*/
|
|
134
|
+
function recall(mix, voice, subject, t) {
|
|
135
|
+
const live = recordOf(mix, voice, subject);
|
|
136
|
+
if (live === undefined)
|
|
137
|
+
return undefined;
|
|
138
|
+
const snap = live.snaps && last(live.snaps, t, true);
|
|
139
|
+
if (snap) {
|
|
140
|
+
const h = copyHeld(voice, snap.held);
|
|
141
|
+
h.from = h.stepped;
|
|
142
|
+
h.replay = live.inputs;
|
|
143
|
+
return h;
|
|
144
|
+
}
|
|
145
|
+
const stepped = live.since < t ? live.since : t;
|
|
146
|
+
return {
|
|
147
|
+
reaches: live.reaches,
|
|
148
|
+
delay: live.delay,
|
|
149
|
+
since: live.since,
|
|
150
|
+
shown: live.shown,
|
|
151
|
+
weight: 0,
|
|
152
|
+
rested: false,
|
|
153
|
+
bands: null,
|
|
154
|
+
state: live.reaches && voice.patch.state
|
|
155
|
+
? voice.patch.state(subject)
|
|
156
|
+
: undefined,
|
|
157
|
+
stepped,
|
|
158
|
+
ticks: 0,
|
|
159
|
+
probed: Number.NaN,
|
|
160
|
+
delta: null,
|
|
161
|
+
phase: 0,
|
|
162
|
+
seeks: 0,
|
|
163
|
+
kept: null,
|
|
164
|
+
voice,
|
|
165
|
+
next: null,
|
|
166
|
+
version: Number.NaN,
|
|
167
|
+
loci: null,
|
|
168
|
+
slot: -1,
|
|
169
|
+
from: stepped,
|
|
170
|
+
unknown: voice.spec.from === 'current',
|
|
171
|
+
replay: live.inputs,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
/** Per channel, the least sure voice that fed it this frame. */
|
|
175
|
+
function doubts(mix, subject) {
|
|
176
|
+
const out = {};
|
|
177
|
+
for (const name of mix.names)
|
|
178
|
+
out[name] = 'exact';
|
|
179
|
+
const rank = { exact: 0, stepped: 1, held: 2 };
|
|
180
|
+
// A voice still waiting on an anchor nothing has answered may yet play, or stop, by now.
|
|
181
|
+
for (const voice of mix.cued) {
|
|
182
|
+
const anchor = voice.spec.anchor;
|
|
183
|
+
if (anchor === undefined || voice.state === 'done')
|
|
184
|
+
continue;
|
|
185
|
+
const waiting = (voice.state === 'pending' && !Number.isFinite(voice.start)) ||
|
|
186
|
+
(voice.state !== 'pending' &&
|
|
187
|
+
voice.state !== 'fading' &&
|
|
188
|
+
(anchor.out !== undefined || anchor.end !== undefined) &&
|
|
189
|
+
!Number.isFinite(voice.outAt));
|
|
190
|
+
if (!waiting || !mix.aims(voice, subject))
|
|
191
|
+
continue;
|
|
192
|
+
for (const slot of voice.slots)
|
|
193
|
+
out[mix.names[slot]] = 'held';
|
|
194
|
+
}
|
|
195
|
+
for (let held = mix.chain(subject, mix.now); held !== null; held = held.next) {
|
|
196
|
+
const voice = held.voice;
|
|
197
|
+
if (voice === null || voice.state === 'done')
|
|
198
|
+
continue;
|
|
199
|
+
if (held.weight <= 0)
|
|
200
|
+
continue;
|
|
201
|
+
const d = doubtOf(mix, voice, held);
|
|
202
|
+
for (const slot of voice.slots) {
|
|
203
|
+
const name = mix.names[slot];
|
|
204
|
+
if (rank[d] > rank[out[name]])
|
|
205
|
+
out[name] = d;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return out;
|
|
209
|
+
}
|
|
210
|
+
function doubtOf(mix, voice, held) {
|
|
211
|
+
const weight = voice.spec.weight;
|
|
212
|
+
if (held.unknown)
|
|
213
|
+
return 'held';
|
|
214
|
+
// A weight read back from a recording is what the mix used then, so its signal's state is moot.
|
|
215
|
+
const replayed = held.replay !== undefined && last(held.replay, mix.now, true) !== undefined;
|
|
216
|
+
if (typeof weight === 'function' && weight.input && !replayed)
|
|
217
|
+
return 'held';
|
|
218
|
+
if (voice.owner !== null && heldByInput(voice))
|
|
219
|
+
return 'held';
|
|
220
|
+
const reads = voice.patch.reads;
|
|
221
|
+
if (reads !== undefined && reads.length > 0) {
|
|
222
|
+
const then = mix.hostThen?.fields;
|
|
223
|
+
if (then === undefined || !reads.every((f) => f in then))
|
|
224
|
+
return 'held';
|
|
225
|
+
}
|
|
226
|
+
if (mix.now > (held.from ?? mix.now)) {
|
|
227
|
+
const tick = mix.opts.stepMs;
|
|
228
|
+
const fixed = tick !== undefined && tick > 0 && !mix.reducedNow;
|
|
229
|
+
if ((voice.patch.step && !fixed) || (held.kept !== null && held.kept.size > 0 && !replayed))
|
|
230
|
+
return 'stepped';
|
|
231
|
+
}
|
|
232
|
+
return 'exact';
|
|
233
|
+
}
|
package/dist/pull.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { Mixer } from './mixer.js';
|
|
2
|
+
import type { Columns } from './types.js';
|
|
3
|
+
/** After a probe, keeps the pose a retarget from `'current'` reads as the subject's last. */
|
|
4
|
+
export declare function keep<I, O>(this: Mixer<I, O>, subject: I, pose: O, out: O | undefined): void;
|
|
5
|
+
export declare function pull<I, O>(mix: Mixer<I, O>, subjects: Iterable<I>, into: Columns<O>): void;
|
package/dist/pull.js
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { copy } from './clone.js';
|
|
2
|
+
import { clampRun, flush, pullRun, writeLater } from './columns.js';
|
|
3
|
+
/** Writes a folded pose's value into subject `n`'s places in a column. */
|
|
4
|
+
function writeValue(c, v, n) {
|
|
5
|
+
const at = n * c.axes;
|
|
6
|
+
if (typeof v === 'number' && c.axes === 1)
|
|
7
|
+
c.out[at] = v;
|
|
8
|
+
else if (Array.isArray(v) && v.length === c.axes)
|
|
9
|
+
for (let a = 0; a < c.axes; a++)
|
|
10
|
+
c.out[at + a] = v[a];
|
|
11
|
+
else if (v === undefined)
|
|
12
|
+
c.out.fill(Number.NaN, at, at + c.axes);
|
|
13
|
+
else
|
|
14
|
+
throw new TypeError(`blits: pull reads ${c.key} as ${c.axes === 1 ? 'a number' : `${c.axes} numbers`} a subject, and the pose has ${String(v)}`);
|
|
15
|
+
}
|
|
16
|
+
/** After a probe, keeps the pose a retarget from `'current'` reads as the subject's last. */
|
|
17
|
+
export function keep(subject, pose, out) {
|
|
18
|
+
// Keeping last frame's pose is free when the mix allocated it; a host sampling into its own
|
|
19
|
+
// object only pays for the copy once something in the mix has asked to retarget from it.
|
|
20
|
+
let kept;
|
|
21
|
+
if (out === undefined)
|
|
22
|
+
kept = pose;
|
|
23
|
+
else if (this.wantsPose) {
|
|
24
|
+
kept = {};
|
|
25
|
+
for (const key of this.names) {
|
|
26
|
+
const v = pose[key];
|
|
27
|
+
if (v !== undefined)
|
|
28
|
+
kept[key] = copy(v);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
if (kept !== undefined) {
|
|
32
|
+
const rec = this.pose.get(subject);
|
|
33
|
+
if (rec === undefined)
|
|
34
|
+
this.pose.set(subject, { pose: kept, at: this.now, prev: undefined, prevAt: Number.NaN });
|
|
35
|
+
else if (rec.at === this.now)
|
|
36
|
+
rec.pose = kept;
|
|
37
|
+
else {
|
|
38
|
+
rec.prev = rec.pose;
|
|
39
|
+
rec.prevAt = rec.at;
|
|
40
|
+
rec.pose = kept;
|
|
41
|
+
rec.at = this.now;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
export function pull(mix, subjects, into) {
|
|
46
|
+
const columns = columnsOf(mix, into);
|
|
47
|
+
const lanes = mix.lanes;
|
|
48
|
+
const now = mix.now;
|
|
49
|
+
if (mix.scratch === undefined)
|
|
50
|
+
mix.scratch = {};
|
|
51
|
+
let room = Number.POSITIVE_INFINITY;
|
|
52
|
+
let tightest = '';
|
|
53
|
+
for (const c of columns) {
|
|
54
|
+
const fits = Math.floor(c.out.length / c.axes);
|
|
55
|
+
if (fits < room) {
|
|
56
|
+
room = fits;
|
|
57
|
+
tightest = c.key;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
// Any other iterable is read into an array first, no further than one past the room there is.
|
|
61
|
+
let list;
|
|
62
|
+
if (Array.isArray(subjects))
|
|
63
|
+
list = subjects;
|
|
64
|
+
else {
|
|
65
|
+
const taken = [];
|
|
66
|
+
for (const subject of subjects)
|
|
67
|
+
if (taken.push(subject) > room)
|
|
68
|
+
break;
|
|
69
|
+
list = taken;
|
|
70
|
+
}
|
|
71
|
+
if (list.length > room)
|
|
72
|
+
throw new RangeError(`blits: pull's array for ${tightest} has room for ${room} subjects, and was given more`);
|
|
73
|
+
const whole = lanes !== null && !mix.wantsPose;
|
|
74
|
+
// An array read again in the same order reuses each position's chain head while it is current:
|
|
75
|
+
// all of them at once while nothing has relinked since the last pull, else one by one.
|
|
76
|
+
const was = mix.pulled;
|
|
77
|
+
const heads = mix.pulledHeads;
|
|
78
|
+
was.length = list.length;
|
|
79
|
+
heads.length = list.length;
|
|
80
|
+
if (mix.pulledSlots.length < list.length) {
|
|
81
|
+
const slots = new Int32Array(Math.max(64, list.length));
|
|
82
|
+
slots.set(mix.pulledSlots);
|
|
83
|
+
mix.pulledSlots = slots;
|
|
84
|
+
}
|
|
85
|
+
const version = mix.version;
|
|
86
|
+
const relinks = mix.relinks;
|
|
87
|
+
const live = !Number.isNaN(now);
|
|
88
|
+
const current = live && mix.pulledVersion === version && mix.pulledRelinks === relinks;
|
|
89
|
+
try {
|
|
90
|
+
readAll(mix, list, columns, live, current, whole);
|
|
91
|
+
}
|
|
92
|
+
finally {
|
|
93
|
+
if (lanes !== null)
|
|
94
|
+
flush(lanes);
|
|
95
|
+
}
|
|
96
|
+
// Every head is current only if nothing relinked or changed version while they were read.
|
|
97
|
+
const unchanged = live && mix.version === version && mix.relinks === relinks;
|
|
98
|
+
mix.pulledVersion = unchanged ? version : Number.NaN;
|
|
99
|
+
mix.pulledRelinks = relinks;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Reads every subject in `list` into the columns, kept apart from `pull` so a list long enough to
|
|
103
|
+
* compile the loop on the stack does not leave `pull`'s own code compiled before its tail had run:
|
|
104
|
+
* under churn that code deoptimized at the tail on every frame.
|
|
105
|
+
*/
|
|
106
|
+
function readAll(mix, list, columns, live, current, whole) {
|
|
107
|
+
const lanes = mix.lanes;
|
|
108
|
+
const now = mix.now;
|
|
109
|
+
const scratch = mix.scratch;
|
|
110
|
+
const was = mix.pulled;
|
|
111
|
+
const heads = mix.pulledHeads;
|
|
112
|
+
const slots = mix.pulledSlots;
|
|
113
|
+
const version = mix.version;
|
|
114
|
+
// Every subject read from the lanes, at the positions it had last time, goes in one run.
|
|
115
|
+
const runs = live && whole;
|
|
116
|
+
for (let n = 0; n < list.length; n++) {
|
|
117
|
+
if (runs) {
|
|
118
|
+
n = pullRun(lanes, slots, list, was, current ? null : heads, n, columns, now, mix.version);
|
|
119
|
+
if (n === list.length)
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
const subject = list[n];
|
|
123
|
+
let slot = -1;
|
|
124
|
+
if (live) {
|
|
125
|
+
if (current && was[n] === subject)
|
|
126
|
+
slot = slots[n];
|
|
127
|
+
else {
|
|
128
|
+
const kept = heads[n];
|
|
129
|
+
const head = was[n] === subject && kept !== undefined && kept.version === version
|
|
130
|
+
? kept
|
|
131
|
+
: mix.chain(subject, now);
|
|
132
|
+
was[n] = subject;
|
|
133
|
+
heads[n] = head;
|
|
134
|
+
slot = head.slot;
|
|
135
|
+
slots[n] = slot;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
const laned = live && lanes?.prepare(slot, subject, now, mix.version, heads[n] ?? null) === true;
|
|
139
|
+
if (laned && whole && lanes.whole && !lanes.owes(slot)) {
|
|
140
|
+
writeLater(lanes, slot, columns, n);
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
const head = live ? heads[n] : null;
|
|
144
|
+
const folded = mix.foldWith(subject, scratch, head, laned, false);
|
|
145
|
+
const pose = (mix.bounded.length === 0 ? folded : mix.clamp(folded));
|
|
146
|
+
mix.keep(subject, pose, scratch);
|
|
147
|
+
for (const c of columns)
|
|
148
|
+
writeValue(c, pose[c.key], n);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
/** The kit slot, width, rest and bounds of every channel `pull` was handed an array for. */
|
|
152
|
+
function columnsOf(mix, into) {
|
|
153
|
+
const columns = [];
|
|
154
|
+
for (const key of Object.keys(into)) {
|
|
155
|
+
const out = into[key];
|
|
156
|
+
if (out === undefined)
|
|
157
|
+
continue;
|
|
158
|
+
const slot = mix.slotOf.get(key);
|
|
159
|
+
if (slot === undefined)
|
|
160
|
+
throw new Error(`blits: pull was handed ${key}, which the kit lacks`);
|
|
161
|
+
const channel = mix.channels[slot];
|
|
162
|
+
const rest = channel.rest;
|
|
163
|
+
const axes = Array.isArray(rest) ? rest.length : 1;
|
|
164
|
+
const bounds = channel.bounds;
|
|
165
|
+
const at = new Float64Array(axes).fill(Number.NaN);
|
|
166
|
+
if (typeof rest === 'number')
|
|
167
|
+
at[0] = rest;
|
|
168
|
+
else if (Array.isArray(rest))
|
|
169
|
+
at.set(rest);
|
|
170
|
+
if (bounds !== undefined)
|
|
171
|
+
clampRun(at, 0, axes, bounds);
|
|
172
|
+
columns.push({ key, slot, axes, out, rest: at, bounds });
|
|
173
|
+
}
|
|
174
|
+
return columns;
|
|
175
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { Lanes } from './lanes.js';
|
|
2
|
+
import type { Voice } from './voice.js';
|
|
3
|
+
/** What qualifying needs of one voice: whether its patch and spec can run on a lane, and its channels. */
|
|
4
|
+
export interface Candidate {
|
|
5
|
+
readonly id: number;
|
|
6
|
+
readonly fits: boolean;
|
|
7
|
+
readonly slots: readonly number[];
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Which channels run as lanes and which voices run on them: the fixed point where every voice
|
|
11
|
+
* writing a laned channel fits and writes only laned channels. Only channels some laned voice
|
|
12
|
+
* writes are laned.
|
|
13
|
+
*/
|
|
14
|
+
export declare function qualify(numeric: readonly boolean[], voices: readonly Candidate[]): {
|
|
15
|
+
channels: boolean[];
|
|
16
|
+
voices: Set<number>;
|
|
17
|
+
};
|
|
18
|
+
export declare function requalify<I, O>(lanes: Lanes<I, O>, version: number): void;
|
|
19
|
+
/**
|
|
20
|
+
* A voice starts playing on a lane or crowd: its epoch. Every subject meets a voice over all of
|
|
21
|
+
* them on its next probe; a voice naming its subjects marks only those.
|
|
22
|
+
*/
|
|
23
|
+
export declare function open<I, O>(lanes: Lanes<I, O>, v: Voice<I, O>): number;
|
|
24
|
+
/**
|
|
25
|
+
* Takes the voices `touch` named on or off their crowds, each as it now stands; false at the
|
|
26
|
+
* first that cannot be, which needs a qualify: one that leaves a lane or the general path, which
|
|
27
|
+
* may open a channel, or one that joins anything but a crowd on a laned channel. Neither a voice
|
|
28
|
+
* that fits joining laned channels nor a laned one leaving moves the fixed point `qualify` finds.
|
|
29
|
+
* A crowd a quarter empty rows is compacted.
|
|
30
|
+
*/
|
|
31
|
+
export declare function retouch<I, O>(lanes: Lanes<I, O>): boolean;
|