@laplace.live/persona-sdk 1.20.0 → 1.22.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/dist/client/client.js +7 -3
- package/dist/client/legacy-kinds.d.ts +2 -0
- package/dist/client/legacy-kinds.js +40 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/values/curve.d.ts +38 -5
- package/dist/values/curve.js +162 -4
- package/dist/values/effect-schema.d.ts +1 -0
- package/dist/values/effect-schema.js +16 -0
- package/dist/values/hotkey-targets.d.ts +56 -0
- package/dist/values/hotkey-targets.js +88 -0
- package/dist/values/labels.d.ts +4 -2
- package/dist/values/labels.js +22 -14
- package/dist/values/limits.d.ts +105 -4
- package/dist/values/limits.js +117 -10
- package/dist/values/stage-info.d.ts +6 -1
- package/dist/values/time.d.ts +9 -0
- package/dist/values/time.js +11 -0
- package/dist/wire/events.d.ts +8 -0
- package/dist/wire/events.js +1 -0
- package/dist/wire/methods.d.ts +23 -1
- package/dist/wire/methods.js +2 -0
- package/dist/wire/schemas/requests.d.ts +2 -1
- package/dist/wire/schemas/settings.d.ts +5 -0
- package/dist/wire/schemas/settings.js +11 -3
- package/dist/wire/types.d.ts +272 -15
- package/dist/wire/types.js +83 -15
- package/package.json +2 -2
package/dist/client/client.js
CHANGED
|
@@ -2,6 +2,7 @@ import { parseServerMessage } from "../wire/envelope.js";
|
|
|
2
2
|
import { PersonaApiError } from "../wire/errors.js";
|
|
3
3
|
import { CLOSE_FORCE_DISCONNECTED, CLOSE_KEY_REVOKED, INJECT_HEARTBEAT_MS, injectTargetKey, PROTOCOL_VERSION, SCENE_ACTIVATION_TIMEOUT_MS, } from "../wire/protocol.js";
|
|
4
4
|
import { personaWsUrl } from "./address.js";
|
|
5
|
+
import { renameLegacyKinds } from "./legacy-kinds.js";
|
|
5
6
|
const DEFAULTS = {
|
|
6
7
|
url: personaWsUrl(),
|
|
7
8
|
reconnectDelayMs: 500,
|
|
@@ -109,7 +110,8 @@ export class PersonaClient {
|
|
|
109
110
|
reject(new Error(`request timed out: ${method}`));
|
|
110
111
|
}, this.opts.requestTimeoutMs ??
|
|
111
112
|
(method === 'scene.activate' ? SCENE_ACTIVATION_TIMEOUT_MS : DEFAULTS.requestTimeoutMs));
|
|
112
|
-
|
|
113
|
+
// Only the runtime id ties a response back to `M`, and its result is unvalidated wire JSON.
|
|
114
|
+
this.pending.set(id, { method, resolve: resolve, reject, timer });
|
|
113
115
|
ws.send(JSON.stringify({ kind: 'request', id, method, ...(params === undefined ? {} : { params }) }));
|
|
114
116
|
});
|
|
115
117
|
}
|
|
@@ -355,8 +357,10 @@ export class PersonaClient {
|
|
|
355
357
|
const set = this.listeners.get(msg.event);
|
|
356
358
|
if (!set)
|
|
357
359
|
return;
|
|
360
|
+
const data = renameLegacyKinds(msg.event, msg.data);
|
|
361
|
+
// Only `msg.event` ties these callbacks to their payload type, and the payload is unvalidated wire JSON.
|
|
358
362
|
for (const cb of set)
|
|
359
|
-
cb(
|
|
363
|
+
cb(data);
|
|
360
364
|
return;
|
|
361
365
|
}
|
|
362
366
|
const { id } = msg;
|
|
@@ -368,7 +372,7 @@ export class PersonaClient {
|
|
|
368
372
|
this.pending.delete(id);
|
|
369
373
|
clearTimeout(p.timer);
|
|
370
374
|
if (msg.kind === 'response')
|
|
371
|
-
p.resolve(msg.result);
|
|
375
|
+
p.resolve(renameLegacyKinds(p.method, msg.result));
|
|
372
376
|
else
|
|
373
377
|
p.reject(new PersonaApiError(msg.code, msg.message));
|
|
374
378
|
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { isRecord } from "../values/guards.js";
|
|
2
|
+
import { canonicalActionKind } from "../wire/types.js";
|
|
3
|
+
function renameHotkeyActions(state) {
|
|
4
|
+
if (!isRecord(state) || !Array.isArray(state.hotkeys))
|
|
5
|
+
return state;
|
|
6
|
+
return {
|
|
7
|
+
...state,
|
|
8
|
+
hotkeys: state.hotkeys.map((hotkey) => isRecord(hotkey) && typeof hotkey.action === 'string'
|
|
9
|
+
? { ...hotkey, action: canonicalActionKind(hotkey.action) }
|
|
10
|
+
: hotkey),
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
function renameActionKinds(list) {
|
|
14
|
+
if (!isRecord(list) || !Array.isArray(list.automations))
|
|
15
|
+
return list;
|
|
16
|
+
return {
|
|
17
|
+
...list,
|
|
18
|
+
automations: list.automations.map((automation) => isRecord(automation) && Array.isArray(automation.actionKinds)
|
|
19
|
+
? {
|
|
20
|
+
...automation,
|
|
21
|
+
actionKinds: automation.actionKinds.map((kind) => typeof kind === 'string' ? canonicalActionKind(kind) : kind),
|
|
22
|
+
}
|
|
23
|
+
: automation),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
/** A method result or event payload with the action kinds an older host still sends renamed to current ones. */
|
|
27
|
+
export function renameLegacyKinds(name, data) {
|
|
28
|
+
switch (name) {
|
|
29
|
+
case 'hotkey.list':
|
|
30
|
+
case 'hotkey.set':
|
|
31
|
+
return renameHotkeyActions(data);
|
|
32
|
+
case 'hotkey.state':
|
|
33
|
+
return isRecord(data) ? { ...data, config: renameHotkeyActions(data.config) } : data;
|
|
34
|
+
case 'automation.list':
|
|
35
|
+
case 'automation.state':
|
|
36
|
+
return renameActionKinds(data);
|
|
37
|
+
default:
|
|
38
|
+
return data;
|
|
39
|
+
}
|
|
40
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -10,6 +10,7 @@ export * from './values/effect-schema.ts';
|
|
|
10
10
|
export * from './values/gltf-extensions.ts';
|
|
11
11
|
export * from './values/guards.ts';
|
|
12
12
|
export * from './values/hands.ts';
|
|
13
|
+
export * from './values/hotkey-targets.ts';
|
|
13
14
|
export * from './values/hotkeys.ts';
|
|
14
15
|
export * from './values/item-transition.ts';
|
|
15
16
|
export * from './values/labels.ts';
|
|
@@ -22,6 +23,7 @@ export * from './values/mouse.ts';
|
|
|
22
23
|
export * from './values/scene-transition.ts';
|
|
23
24
|
export * from './values/stage-info.ts';
|
|
24
25
|
export * from './values/terms.ts';
|
|
26
|
+
export * from './values/time.ts';
|
|
25
27
|
export * from './values/volumetric-lighting.ts';
|
|
26
28
|
export * from './values/vrm-bindings.ts';
|
|
27
29
|
export * from './wire/envelope.ts';
|
package/dist/index.js
CHANGED
|
@@ -12,6 +12,7 @@ export * from "./values/effect-schema.js";
|
|
|
12
12
|
export * from "./values/gltf-extensions.js";
|
|
13
13
|
export * from "./values/guards.js";
|
|
14
14
|
export * from "./values/hands.js";
|
|
15
|
+
export * from "./values/hotkey-targets.js";
|
|
15
16
|
export * from "./values/hotkeys.js";
|
|
16
17
|
export * from "./values/item-transition.js";
|
|
17
18
|
export * from "./values/labels.js";
|
|
@@ -24,6 +25,7 @@ export * from "./values/mouse.js";
|
|
|
24
25
|
export * from "./values/scene-transition.js";
|
|
25
26
|
export * from "./values/stage-info.js";
|
|
26
27
|
export * from "./values/terms.js";
|
|
28
|
+
export * from "./values/time.js";
|
|
27
29
|
export * from "./values/volumetric-lighting.js";
|
|
28
30
|
export * from "./values/vrm-bindings.js";
|
|
29
31
|
export * from "./wire/envelope.js";
|
package/dist/values/curve.d.ts
CHANGED
|
@@ -18,8 +18,8 @@ export interface Keyframe {
|
|
|
18
18
|
anchor: Point;
|
|
19
19
|
/**
|
|
20
20
|
* Bezier handles. `x` is a **fraction of the segment's x-span** (0 = this segment's left
|
|
21
|
-
* anchor, 1 = its right), `y` is absolute — nizima's
|
|
22
|
-
* anchor
|
|
21
|
+
* anchor, 1 = its right), `y` is absolute — nizima's format. Because `y` stays put when its
|
|
22
|
+
* anchor moves, an editor drag has to carry the handles itself (`dragAnchor`), or the point kinks.
|
|
23
23
|
*/
|
|
24
24
|
next?: Point;
|
|
25
25
|
previous?: Point;
|
|
@@ -61,8 +61,9 @@ export interface CurveHandle extends Point {
|
|
|
61
61
|
span: readonly [number, number];
|
|
62
62
|
}
|
|
63
63
|
/**
|
|
64
|
-
* The keyframe's
|
|
65
|
-
*
|
|
64
|
+
* The keyframe's handles, in curve coordinates — its outgoing one when it governs a Bezier
|
|
65
|
+
* segment, its incoming one when the keyframe before it does. A sharp point's are retracted onto
|
|
66
|
+
* it and still reported; `extendedHandles` is what an editor draws.
|
|
66
67
|
*
|
|
67
68
|
* `handleX` rather than a second clamp-and-lerp: a handle has to read back where the evaluator
|
|
68
69
|
* looks for it. `y` is absolute and deliberately unclamped — `sanitizeCurve` keeps a control point
|
|
@@ -70,6 +71,8 @@ export interface CurveHandle extends Point {
|
|
|
70
71
|
* legitimate overshoot at the wrong height.
|
|
71
72
|
*/
|
|
72
73
|
export declare function handlesFor(keyframes: Keyframe[], index: number): CurveHandle[];
|
|
74
|
+
/** The keyframe's handles an editor draws and lets you grab: a retracted one has nothing to grab. */
|
|
75
|
+
export declare function extendedHandles(keyframes: Keyframe[], index: number): CurveHandle[];
|
|
73
76
|
/**
|
|
74
77
|
* How far an anchor may travel in x, as `[lo, hi]`.
|
|
75
78
|
*
|
|
@@ -81,6 +84,31 @@ export declare function handlesFor(keyframes: Keyframe[], index: number): CurveH
|
|
|
81
84
|
* One statement of the rule, so an editor's drag and its typed field cannot drift apart.
|
|
82
85
|
*/
|
|
83
86
|
export declare function anchorBounds(keyframes: Keyframe[], index: number): [number, number];
|
|
87
|
+
/**
|
|
88
|
+
* The anchor at `index` moved to `to`, held within `anchorBounds` and the plot, carrying its own
|
|
89
|
+
* handles as a pen tool does, so the curve leaves and arrives at it in the same directions: a
|
|
90
|
+
* smooth point stays smooth, a corner keeps its angle. The neighbours' handles keep their
|
|
91
|
+
* fractions, so the segments either side stretch rather than re-aim.
|
|
92
|
+
*/
|
|
93
|
+
export declare function dragAnchor(keyframes: Keyframe[], index: number, to: Point): Keyframe[];
|
|
94
|
+
/**
|
|
95
|
+
* The `side` handle of the anchor at `index` moved to `to`. On a smooth point the other handle
|
|
96
|
+
* turns to stay in line, keeping its length — a pen tool's default — unless `independent`, which
|
|
97
|
+
* moves this one alone and leaves a corner. `to` is held within the segment the handle shapes,
|
|
98
|
+
* where `handleX` reads it, and on the plot, where it can be found again; a stored overshoot still
|
|
99
|
+
* reads back. The turned handle is shortened to fit its own.
|
|
100
|
+
*/
|
|
101
|
+
export declare function dragHandle(keyframes: Keyframe[], index: number, side: HandleSide, to: Point, independent: boolean): Keyframe[];
|
|
102
|
+
/**
|
|
103
|
+
* A pen tool's convert-point click on the interior anchor at `index`: a smooth point turns sharp,
|
|
104
|
+
* anything else turns smooth. Beside a step there is nothing to be in line with, so a point there is
|
|
105
|
+
* smooth while its one handle is out. Sharp retracts the handles onto the anchor, so no curve bends
|
|
106
|
+
* at it. Smooth aims them through the neighbours — flat at a peak or valley, so the curve cannot
|
|
107
|
+
* overshoot the neighbour it turns at — a third of the way along each segment, and a linear side
|
|
108
|
+
* becomes Bezier with its far handle on the old line, so the neighbour's end keeps its direction.
|
|
109
|
+
* Step sides are left as they are. Returns **the input array itself** when it declines.
|
|
110
|
+
*/
|
|
111
|
+
export declare function toggleSmooth(keyframes: Keyframe[], index: number): Keyframe[];
|
|
84
112
|
/**
|
|
85
113
|
* Every Bezier segment carries both handles, seeded collinear unless it already has both.
|
|
86
114
|
*
|
|
@@ -146,13 +174,18 @@ export declare const INTERPOLATIONS: readonly ["linear", "step", "invertStep", "
|
|
|
146
174
|
*/
|
|
147
175
|
export declare function sanitizeCurve(v: unknown): Curve | null;
|
|
148
176
|
/**
|
|
149
|
-
* The straight 0..1 line — what a binding without a curve already does
|
|
177
|
+
* The straight 0..1 line — what a binding without a curve already does — as the Linear preset.
|
|
150
178
|
*
|
|
151
179
|
* No handles: a `linear` keyframe's are never read, and one that outlives the segment it was cut
|
|
152
180
|
* for is what bends a curve on a mode switch. `withBezierHandles` cuts fresh ones against whatever
|
|
153
181
|
* the anchors are by then, so Bezier leaves the line where it is and only a drag shapes it.
|
|
154
182
|
*/
|
|
155
183
|
export declare function identityCurve(): Curve;
|
|
184
|
+
/**
|
|
185
|
+
* The same straight line as one Bezier segment, handles on the line at the thirds — what a new
|
|
186
|
+
* curve starts as, so a point clicked into it splits smooth and a drag bends it rather than kinking.
|
|
187
|
+
*/
|
|
188
|
+
export declare function bezierIdentityCurve(): Curve;
|
|
156
189
|
/** A ready-made response shape that replaces the whole curve; `linear` is the identity. */
|
|
157
190
|
export type CurvePresetId = (typeof CURVE_PRESET_IDS)[number];
|
|
158
191
|
export declare const CURVE_PRESET_IDS: readonly ["linear", "step", "invertStep", "bezier", "easeIn", "easeOut", "sCurve", "threshold", "steps"];
|
package/dist/values/curve.js
CHANGED
|
@@ -92,8 +92,9 @@ export function evaluateCurve(curve, x) {
|
|
|
92
92
|
return left === undefined || right === undefined ? first.anchor.y : segmentAt(left, right, x);
|
|
93
93
|
}
|
|
94
94
|
/**
|
|
95
|
-
* The keyframe's
|
|
96
|
-
*
|
|
95
|
+
* The keyframe's handles, in curve coordinates — its outgoing one when it governs a Bezier
|
|
96
|
+
* segment, its incoming one when the keyframe before it does. A sharp point's are retracted onto
|
|
97
|
+
* it and still reported; `extendedHandles` is what an editor draws.
|
|
97
98
|
*
|
|
98
99
|
* `handleX` rather than a second clamp-and-lerp: a handle has to read back where the evaluator
|
|
99
100
|
* looks for it. `y` is absolute and deliberately unclamped — `sanitizeCurve` keeps a control point
|
|
@@ -117,6 +118,15 @@ export function handlesFor(keyframes, index) {
|
|
|
117
118
|
}
|
|
118
119
|
return out;
|
|
119
120
|
}
|
|
121
|
+
/** Below this length a handle is retracted onto its anchor, as a sharp point's are. */
|
|
122
|
+
const RETRACTED = 1e-9;
|
|
123
|
+
/** The keyframe's handles an editor draws and lets you grab: a retracted one has nothing to grab. */
|
|
124
|
+
export function extendedHandles(keyframes, index) {
|
|
125
|
+
const anchor = keyframes[index]?.anchor;
|
|
126
|
+
if (anchor === undefined)
|
|
127
|
+
return [];
|
|
128
|
+
return handlesFor(keyframes, index).filter(h => Math.hypot(h.x - anchor.x, h.y - anchor.y) > RETRACTED);
|
|
129
|
+
}
|
|
120
130
|
/**
|
|
121
131
|
* How far an anchor may travel in x, as `[lo, hi]`.
|
|
122
132
|
*
|
|
@@ -133,7 +143,143 @@ export function anchorBounds(keyframes, index) {
|
|
|
133
143
|
return [0, 1];
|
|
134
144
|
if (index === 0 || index === keyframes.length - 1)
|
|
135
145
|
return [held.anchor.x, held.anchor.x];
|
|
136
|
-
|
|
146
|
+
const lo = keyframes[index - 1]?.anchor.x ?? 0;
|
|
147
|
+
const hi = keyframes[index + 1]?.anchor.x ?? 1;
|
|
148
|
+
// Clicks in one column stack anchors on the curve; neighbours too close for the margin meet halfway.
|
|
149
|
+
const edge = Math.min(EDGE_EPSILON, (hi - lo) / 2);
|
|
150
|
+
return [lo + edge, hi - edge];
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* The handle at `anchor + s·offset` for the largest `s` in 0..1 inside the box, stored against
|
|
154
|
+
* `xs`, its segment's span: a handle out of room is shortened along its own direction, so a
|
|
155
|
+
* smooth point stays smooth.
|
|
156
|
+
*/
|
|
157
|
+
function fitHandle(anchor, offset, xs, ys) {
|
|
158
|
+
let s = 1;
|
|
159
|
+
if (offset.x > 0)
|
|
160
|
+
s = Math.min(s, (xs[1] - anchor.x) / offset.x);
|
|
161
|
+
if (offset.x < 0)
|
|
162
|
+
s = Math.min(s, (xs[0] - anchor.x) / offset.x);
|
|
163
|
+
if (offset.y > 0)
|
|
164
|
+
s = Math.min(s, (ys[1] - anchor.y) / offset.y);
|
|
165
|
+
if (offset.y < 0)
|
|
166
|
+
s = Math.min(s, (ys[0] - anchor.y) / offset.y);
|
|
167
|
+
s = Math.max(0, s);
|
|
168
|
+
return { x: handleFraction(xs[0], xs[1], anchor.x + offset.x * s), y: anchor.y + offset.y * s };
|
|
169
|
+
}
|
|
170
|
+
/** Where an authored handle may sit in y: on the plot, or no further out than a stored overshoot. */
|
|
171
|
+
function heightsFor(y) {
|
|
172
|
+
return [Math.min(0, y), Math.max(1, y)];
|
|
173
|
+
}
|
|
174
|
+
/** Two handles in line through their anchor, on opposite sides of it: a smooth point. */
|
|
175
|
+
function inLine(anchor, a, b) {
|
|
176
|
+
const u = { x: a.x - anchor.x, y: a.y - anchor.y };
|
|
177
|
+
const v = { x: b.x - anchor.x, y: b.y - anchor.y };
|
|
178
|
+
const lu = Math.hypot(u.x, u.y);
|
|
179
|
+
const lv = Math.hypot(v.x, v.y);
|
|
180
|
+
if (lu <= RETRACTED || lv <= RETRACTED)
|
|
181
|
+
return false;
|
|
182
|
+
return u.x * v.x + u.y * v.y < 0 && Math.abs(u.x * v.y - u.y * v.x) <= 1e-3 * lu * lv;
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* The anchor at `index` moved to `to`, held within `anchorBounds` and the plot, carrying its own
|
|
186
|
+
* handles as a pen tool does, so the curve leaves and arrives at it in the same directions: a
|
|
187
|
+
* smooth point stays smooth, a corner keeps its angle. The neighbours' handles keep their
|
|
188
|
+
* fractions, so the segments either side stretch rather than re-aim.
|
|
189
|
+
*/
|
|
190
|
+
export function dragAnchor(keyframes, index, to) {
|
|
191
|
+
const held = keyframes[index];
|
|
192
|
+
if (held === undefined)
|
|
193
|
+
return keyframes;
|
|
194
|
+
const [lo, hi] = anchorBounds(keyframes, index);
|
|
195
|
+
const at = { x: clamp(to.x, lo, hi), y: clamp(to.y, 0, 1) };
|
|
196
|
+
const moved = { ...held, anchor: at };
|
|
197
|
+
for (const h of handlesFor(keyframes, index)) {
|
|
198
|
+
const span = h.side === 'next' ? [at.x, h.span[1]] : [h.span[0], at.x];
|
|
199
|
+
moved[h.side] = fitHandle(at, { x: h.x - held.anchor.x, y: h.y - held.anchor.y }, span, heightsFor(h.y));
|
|
200
|
+
}
|
|
201
|
+
return keyframes.map((k, i) => (i === index ? moved : k));
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* The `side` handle of the anchor at `index` moved to `to`. On a smooth point the other handle
|
|
205
|
+
* turns to stay in line, keeping its length — a pen tool's default — unless `independent`, which
|
|
206
|
+
* moves this one alone and leaves a corner. `to` is held within the segment the handle shapes,
|
|
207
|
+
* where `handleX` reads it, and on the plot, where it can be found again; a stored overshoot still
|
|
208
|
+
* reads back. The turned handle is shortened to fit its own.
|
|
209
|
+
*/
|
|
210
|
+
export function dragHandle(keyframes, index, side, to, independent) {
|
|
211
|
+
const held = keyframes[index];
|
|
212
|
+
const handles = handlesFor(keyframes, index);
|
|
213
|
+
const dragged = handles.find(h => h.side === side);
|
|
214
|
+
if (held === undefined || dragged === undefined)
|
|
215
|
+
return keyframes;
|
|
216
|
+
const at = { x: clamp(to.x, dragged.span[0], dragged.span[1]), y: clamp(to.y, 0, 1) };
|
|
217
|
+
const moved = { ...held };
|
|
218
|
+
moved[side] = { x: handleFraction(dragged.span[0], dragged.span[1], at.x), y: at.y };
|
|
219
|
+
const other = handles.find(h => h.side !== side);
|
|
220
|
+
const a = held.anchor;
|
|
221
|
+
const reach = Math.hypot(at.x - a.x, at.y - a.y);
|
|
222
|
+
if (!independent && other !== undefined && reach > 0 && inLine(a, dragged, other)) {
|
|
223
|
+
const keep = Math.hypot(other.x - a.x, other.y - a.y) / reach;
|
|
224
|
+
const away = { x: (a.x - at.x) * keep, y: (a.y - at.y) * keep };
|
|
225
|
+
moved[other.side] = fitHandle(a, away, other.span, heightsFor(other.y));
|
|
226
|
+
}
|
|
227
|
+
return keyframes.map((k, i) => (i === index ? moved : k));
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* A pen tool's convert-point click on the interior anchor at `index`: a smooth point turns sharp,
|
|
231
|
+
* anything else turns smooth. Beside a step there is nothing to be in line with, so a point there is
|
|
232
|
+
* smooth while its one handle is out. Sharp retracts the handles onto the anchor, so no curve bends
|
|
233
|
+
* at it. Smooth aims them through the neighbours — flat at a peak or valley, so the curve cannot
|
|
234
|
+
* overshoot the neighbour it turns at — a third of the way along each segment, and a linear side
|
|
235
|
+
* becomes Bezier with its far handle on the old line, so the neighbour's end keeps its direction.
|
|
236
|
+
* Step sides are left as they are. Returns **the input array itself** when it declines.
|
|
237
|
+
*/
|
|
238
|
+
export function toggleSmooth(keyframes, index) {
|
|
239
|
+
const before = keyframes[index - 1];
|
|
240
|
+
const held = keyframes[index];
|
|
241
|
+
const after = keyframes[index + 1];
|
|
242
|
+
if (before === undefined || held === undefined || after === undefined)
|
|
243
|
+
return keyframes;
|
|
244
|
+
// A step side has no curve to aim; a linear one turns Bezier below.
|
|
245
|
+
const smoothIn = before.interpolation === 'linear' || before.interpolation === 'bezier';
|
|
246
|
+
const smoothOut = held.interpolation === 'linear' || held.interpolation === 'bezier';
|
|
247
|
+
if (!smoothIn && !smoothOut)
|
|
248
|
+
return keyframes;
|
|
249
|
+
const a = held.anchor;
|
|
250
|
+
const handles = handlesFor(keyframes, index);
|
|
251
|
+
const incoming = handles.find(h => h.side === 'previous');
|
|
252
|
+
const outgoing = handles.find(h => h.side === 'next');
|
|
253
|
+
const out = [...keyframes];
|
|
254
|
+
const isSmooth = smoothIn && smoothOut
|
|
255
|
+
? incoming !== undefined && outgoing !== undefined && inLine(a, incoming, outgoing)
|
|
256
|
+
: extendedHandles(keyframes, index).length > 0;
|
|
257
|
+
if (isSmooth) {
|
|
258
|
+
const sharp = { ...held };
|
|
259
|
+
if (incoming !== undefined)
|
|
260
|
+
sharp.previous = { x: 1, y: a.y };
|
|
261
|
+
if (outgoing !== undefined)
|
|
262
|
+
sharp.next = { x: 0, y: a.y };
|
|
263
|
+
out[index] = sharp;
|
|
264
|
+
return out;
|
|
265
|
+
}
|
|
266
|
+
const l = before.anchor;
|
|
267
|
+
const r = after.anchor;
|
|
268
|
+
const slope = (a.y - l.y) * (r.y - a.y) > 0 ? (r.y - l.y) / (r.x - l.x) : 0;
|
|
269
|
+
const smooth = { ...held };
|
|
270
|
+
if (before.interpolation === 'linear') {
|
|
271
|
+
out[index - 1] = { ...before, interpolation: 'bezier', next: { x: 1 / 3, y: lerp(l.y, a.y, 1 / 3) } };
|
|
272
|
+
}
|
|
273
|
+
if (smoothIn)
|
|
274
|
+
smooth.previous = fitHandle(a, { x: (l.x - a.x) / 3, y: (slope * (l.x - a.x)) / 3 }, [l.x, a.x], [0, 1]);
|
|
275
|
+
if (held.interpolation === 'linear') {
|
|
276
|
+
smooth.interpolation = 'bezier';
|
|
277
|
+
out[index + 1] = { ...after, previous: { x: 2 / 3, y: lerp(a.y, r.y, 2 / 3) } };
|
|
278
|
+
}
|
|
279
|
+
if (smoothOut)
|
|
280
|
+
smooth.next = fitHandle(a, { x: (r.x - a.x) / 3, y: (slope * (r.x - a.x)) / 3 }, [a.x, r.x], [0, 1]);
|
|
281
|
+
out[index] = smooth;
|
|
282
|
+
return out;
|
|
137
283
|
}
|
|
138
284
|
/**
|
|
139
285
|
* Every Bezier segment carries both handles, seeded collinear unless it already has both.
|
|
@@ -308,7 +454,7 @@ export function sanitizeCurve(v) {
|
|
|
308
454
|
return { keyframes: withBezierHandles(keyframes) };
|
|
309
455
|
}
|
|
310
456
|
/**
|
|
311
|
-
* The straight 0..1 line — what a binding without a curve already does
|
|
457
|
+
* The straight 0..1 line — what a binding without a curve already does — as the Linear preset.
|
|
312
458
|
*
|
|
313
459
|
* No handles: a `linear` keyframe's are never read, and one that outlives the segment it was cut
|
|
314
460
|
* for is what bends a curve on a mode switch. `withBezierHandles` cuts fresh ones against whatever
|
|
@@ -322,6 +468,18 @@ export function identityCurve() {
|
|
|
322
468
|
],
|
|
323
469
|
};
|
|
324
470
|
}
|
|
471
|
+
/**
|
|
472
|
+
* The same straight line as one Bezier segment, handles on the line at the thirds — what a new
|
|
473
|
+
* curve starts as, so a point clicked into it splits smooth and a drag bends it rather than kinking.
|
|
474
|
+
*/
|
|
475
|
+
export function bezierIdentityCurve() {
|
|
476
|
+
return {
|
|
477
|
+
keyframes: withBezierHandles([
|
|
478
|
+
{ anchor: { x: 0, y: 0 }, interpolation: 'bezier' },
|
|
479
|
+
{ anchor: { x: 1, y: 1 }, interpolation: 'linear' },
|
|
480
|
+
]),
|
|
481
|
+
};
|
|
482
|
+
}
|
|
325
483
|
export const CURVE_PRESET_IDS = [
|
|
326
484
|
'linear',
|
|
327
485
|
'step',
|
|
@@ -11,6 +11,7 @@ export declare const EFFECT_SCOPES: {
|
|
|
11
11
|
readonly blur: readonly ["scene", "layer"];
|
|
12
12
|
readonly bloom: readonly ["scene", "layer"];
|
|
13
13
|
readonly diffusion: readonly ["scene", "layer"];
|
|
14
|
+
readonly sketch: readonly ["scene", "layer"];
|
|
14
15
|
readonly dof: readonly ["scene"];
|
|
15
16
|
readonly chromaticAberration: readonly ["scene"];
|
|
16
17
|
readonly grain: readonly ["scene"];
|
|
@@ -13,6 +13,7 @@ export const EFFECT_SCOPES = {
|
|
|
13
13
|
blur: ['scene', 'layer'],
|
|
14
14
|
bloom: ['scene', 'layer'],
|
|
15
15
|
diffusion: ['scene', 'layer'],
|
|
16
|
+
sketch: ['scene', 'layer'],
|
|
16
17
|
dof: ['scene'],
|
|
17
18
|
chromaticAberration: ['scene'],
|
|
18
19
|
grain: ['scene'],
|
|
@@ -165,6 +166,15 @@ export const EFFECT_SPECS = {
|
|
|
165
166
|
radius: { default: 0.85, min: 0, max: 1, step: 0.01 },
|
|
166
167
|
threshold: { default: 0.35, min: 0, max: 1, step: 0.01 },
|
|
167
168
|
},
|
|
169
|
+
sketch: {
|
|
170
|
+
lines: { default: 1, min: 0, max: 2, step: 0.01 },
|
|
171
|
+
lineWidth: { default: 1, min: 0.5, max: 4, step: 0.1, digits: 1, unit: 'px' },
|
|
172
|
+
// 1 is the source's density; above it the grain packs toward solid graphite.
|
|
173
|
+
shading: { default: 1, min: 0, max: 2, step: 0.01 },
|
|
174
|
+
softness: { default: 1, min: 0, max: 1, step: 0.01 },
|
|
175
|
+
saturation: { default: 0, min: 0, max: 1, step: 0.01 },
|
|
176
|
+
opacity: { default: 1, min: 0, max: 1, step: 0.01 },
|
|
177
|
+
},
|
|
168
178
|
dof: {
|
|
169
179
|
bokehScale: { default: 2, min: 0, max: 8, step: 0.01 },
|
|
170
180
|
/** World metres of acceptably-sharp depth around the focus plane. */
|
|
@@ -303,6 +313,10 @@ export const EFFECT_COLOR_SPECS = {
|
|
|
303
313
|
outline: {
|
|
304
314
|
color: { default: '#000000' },
|
|
305
315
|
},
|
|
316
|
+
// The mean of the source demo's kraft paper photo.
|
|
317
|
+
sketch: {
|
|
318
|
+
paperColor: { default: '#b69b83' },
|
|
319
|
+
},
|
|
306
320
|
dropShadow: {
|
|
307
321
|
color: { default: '#000000' },
|
|
308
322
|
},
|
|
@@ -315,6 +329,7 @@ export const EFFECT_BOOLEAN_SPECS = {
|
|
|
315
329
|
transparentBackground: { default: false },
|
|
316
330
|
},
|
|
317
331
|
blur: { highQuality: { default: false } },
|
|
332
|
+
sketch: { paperBackground: { default: false } },
|
|
318
333
|
bloom: {
|
|
319
334
|
selectColors: { default: false },
|
|
320
335
|
invertColors: { default: false },
|
|
@@ -435,6 +450,7 @@ export const ALL_EFFECT_STACK_ORDER = [
|
|
|
435
450
|
'chromaticAberration',
|
|
436
451
|
'bloom',
|
|
437
452
|
'diffusion',
|
|
453
|
+
'sketch',
|
|
438
454
|
'color',
|
|
439
455
|
'levels',
|
|
440
456
|
'colorWheels',
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { Hotkey, MotionGroup } from '../wire/types.ts';
|
|
2
|
+
/** A hotkey action Persona can run. */
|
|
3
|
+
type HotkeyAction = NonNullable<Hotkey['action']>;
|
|
4
|
+
/** What a model offers hotkeys: its expressions (a null `file` on formats with none) and its motion groups. */
|
|
5
|
+
export interface HotkeyListing {
|
|
6
|
+
expressions: readonly {
|
|
7
|
+
name: string;
|
|
8
|
+
file: string | null;
|
|
9
|
+
}[];
|
|
10
|
+
motions: readonly MotionGroup[];
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* The id an expression is stored and bound by: its exp3 basename on Live2D (VTS parity),
|
|
14
|
+
* its name on VRM, which declares no per-expression file. Every persisted or hotkey-bound
|
|
15
|
+
* reference to an expression goes through this, so the two can never disagree.
|
|
16
|
+
*/
|
|
17
|
+
export declare function expressionKey(def: {
|
|
18
|
+
name: string;
|
|
19
|
+
file: string | null;
|
|
20
|
+
}): string;
|
|
21
|
+
/** Saved basenames → definition Names, in saved order; unmatched entries dropped. */
|
|
22
|
+
export declare function matchSavedToDefinitions(saved: readonly string[], defs: readonly {
|
|
23
|
+
Name: string;
|
|
24
|
+
File: string;
|
|
25
|
+
}[]): string[];
|
|
26
|
+
/** Expression Name for a stored expression key, or null when the model no longer declares it. */
|
|
27
|
+
export declare function resolveExpressionName(listing: HotkeyListing, file: string): string | null;
|
|
28
|
+
/** Motion group and index for a stored motion3 basename, or null when it's gone. */
|
|
29
|
+
export declare function resolveMotion(listing: {
|
|
30
|
+
motions: readonly MotionGroup[];
|
|
31
|
+
}, file: string): {
|
|
32
|
+
group: string;
|
|
33
|
+
index: number;
|
|
34
|
+
} | null;
|
|
35
|
+
/** `file` is a basename throughout — the form hotkeys are stored in. */
|
|
36
|
+
export interface HotkeyTarget {
|
|
37
|
+
file: string;
|
|
38
|
+
label: string;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* The files an action's hotkey can point at, with display labels. Deduped by basename,
|
|
42
|
+
* first occurrence wins — the same rule the resolvers above apply, so the picker always
|
|
43
|
+
* lists exactly what a hotkey would run.
|
|
44
|
+
* @param groupLabel Display name for a motion group; VRM groups are clip refs, not names.
|
|
45
|
+
*/
|
|
46
|
+
export declare function hotkeyTargets(listing: HotkeyListing, action: HotkeyAction, groupLabel?: (group: string) => string): HotkeyTarget[];
|
|
47
|
+
/**
|
|
48
|
+
* Whether a model-load hotkey's stored `File` names this model: its `.vtube.json` name (what VTS
|
|
49
|
+
* stores) or its entry file, case-insensitively — VTS's own file-name resolution. A `ModelRef`
|
|
50
|
+
* from an older host lists neither, and matches nothing.
|
|
51
|
+
*/
|
|
52
|
+
export declare function modelMatchesFile(file: string, model: {
|
|
53
|
+
vtubeFile?: string | null;
|
|
54
|
+
entryFile?: string;
|
|
55
|
+
}): boolean;
|
|
56
|
+
export {};
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { fileBasename, motionLabel, motionSlots } from "./labels.js";
|
|
2
|
+
/**
|
|
3
|
+
* The id an expression is stored and bound by: its exp3 basename on Live2D (VTS parity),
|
|
4
|
+
* its name on VRM, which declares no per-expression file. Every persisted or hotkey-bound
|
|
5
|
+
* reference to an expression goes through this, so the two can never disagree.
|
|
6
|
+
*/
|
|
7
|
+
export function expressionKey(def) {
|
|
8
|
+
return def.file === null ? def.name : fileBasename(def.file);
|
|
9
|
+
}
|
|
10
|
+
/** Saved basenames → definition Names, in saved order; unmatched entries dropped. */
|
|
11
|
+
export function matchSavedToDefinitions(saved, defs) {
|
|
12
|
+
const byBase = new Map();
|
|
13
|
+
for (const d of defs) {
|
|
14
|
+
const base = fileBasename(d.File);
|
|
15
|
+
if (!byBase.has(base))
|
|
16
|
+
byBase.set(base, d.Name);
|
|
17
|
+
}
|
|
18
|
+
return saved.flatMap(s => {
|
|
19
|
+
const name = byBase.get(fileBasename(s));
|
|
20
|
+
return name === undefined ? [] : [name];
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
// A hotkey stores the same id expression persistence does, so a VRM entry binds by name.
|
|
24
|
+
// A keyless entry (a model3 declaring an empty File) is dropped — nothing could bind it.
|
|
25
|
+
function expressionDefs(listing) {
|
|
26
|
+
return listing.expressions.flatMap(d => {
|
|
27
|
+
const key = expressionKey(d);
|
|
28
|
+
return key === '' ? [] : [{ Name: d.name, File: key }];
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
/** Expression Name for a stored expression key, or null when the model no longer declares it. */
|
|
32
|
+
export function resolveExpressionName(listing, file) {
|
|
33
|
+
return matchSavedToDefinitions([file], expressionDefs(listing))[0] ?? null;
|
|
34
|
+
}
|
|
35
|
+
/** Motion group and index for a stored motion3 basename, or null when it's gone. */
|
|
36
|
+
export function resolveMotion(listing, file) {
|
|
37
|
+
const base = fileBasename(file);
|
|
38
|
+
for (const { group, files } of listing.motions) {
|
|
39
|
+
const slot = motionSlots(files).find(s => fileBasename(s.file) === base);
|
|
40
|
+
if (slot)
|
|
41
|
+
return { group, index: slot.index };
|
|
42
|
+
}
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* The files an action's hotkey can point at, with display labels. Deduped by basename,
|
|
47
|
+
* first occurrence wins — the same rule the resolvers above apply, so the picker always
|
|
48
|
+
* lists exactly what a hotkey would run.
|
|
49
|
+
* @param groupLabel Display name for a motion group; VRM groups are clip refs, not names.
|
|
50
|
+
*/
|
|
51
|
+
export function hotkeyTargets(listing, action, groupLabel = g => g) {
|
|
52
|
+
const seen = new Set();
|
|
53
|
+
if (action === 'expression-toggle') {
|
|
54
|
+
return expressionDefs(listing).flatMap(d => {
|
|
55
|
+
const file = fileBasename(d.File);
|
|
56
|
+
if (seen.has(file))
|
|
57
|
+
return [];
|
|
58
|
+
seen.add(file);
|
|
59
|
+
return [{ file, label: d.Name }];
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
if (action === 'motion-play') {
|
|
63
|
+
return listing.motions.flatMap(g => motionSlots(g.files).flatMap(({ file: f }) => {
|
|
64
|
+
const file = fileBasename(f);
|
|
65
|
+
if (seen.has(file))
|
|
66
|
+
return [];
|
|
67
|
+
seen.add(file);
|
|
68
|
+
// A VRM group is a single clip named for its own file, so the ` · file` half
|
|
69
|
+
// would just repeat the group.
|
|
70
|
+
const label = g.group === f ? groupLabel(g.group) : `${groupLabel(g.group)} · ${motionLabel(f)}`;
|
|
71
|
+
return [{ file, label }];
|
|
72
|
+
}));
|
|
73
|
+
}
|
|
74
|
+
return [];
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Whether a model-load hotkey's stored `File` names this model: its `.vtube.json` name (what VTS
|
|
78
|
+
* stores) or its entry file, case-insensitively — VTS's own file-name resolution. A `ModelRef`
|
|
79
|
+
* from an older host lists neither, and matches nothing.
|
|
80
|
+
*/
|
|
81
|
+
export function modelMatchesFile(file, model) {
|
|
82
|
+
const base = fileBasename(file).toLowerCase();
|
|
83
|
+
if (base === '')
|
|
84
|
+
return false;
|
|
85
|
+
if (model.vtubeFile?.toLowerCase() === base)
|
|
86
|
+
return true;
|
|
87
|
+
return model.entryFile !== undefined && fileBasename(model.entryFile).toLowerCase() === base;
|
|
88
|
+
}
|
package/dist/values/labels.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { AutomationInfo, AutomationSetupKind } from '../wire/types.ts';
|
|
2
2
|
/** Last path segment, tolerating both `/` and `\` separators. */
|
|
3
3
|
export declare function fileBasename(path: string): string;
|
|
4
4
|
/**
|
|
@@ -20,7 +20,9 @@ export declare function motionSlots(files: readonly string[]): {
|
|
|
20
20
|
/** English label for one automation action kind; kinds newer than this SDK read `Action`. */
|
|
21
21
|
export declare function automationActionLabel(kind: string): string;
|
|
22
22
|
/** True for an action that prepares the scene before playback. */
|
|
23
|
-
export declare function
|
|
23
|
+
export declare function isAutomationSetup(kind: string | undefined): kind is AutomationSetupKind;
|
|
24
|
+
/** @deprecated Use `isAutomationSetup`. */
|
|
25
|
+
export declare const isAutomationBoundary: typeof isAutomationSetup;
|
|
24
26
|
/** Join action labels without implying timing that the client summary does not carry. */
|
|
25
27
|
export declare function joinAutomationActionLabels(labels: readonly string[], kinds: readonly string[]): string;
|
|
26
28
|
/** Display label for an app automation: its title, else its joined action-kind labels. */
|