@laplace.live/persona-sdk 1.23.1 → 1.25.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/README.md +10 -10
- package/dist/client/client.js +5 -6
- package/dist/client/legacy-names.d.ts +2 -0
- package/dist/client/{legacy-kinds.js → legacy-names.js} +12 -3
- package/dist/client/node.d.ts +13 -0
- package/dist/client/node.js +29 -0
- package/dist/client/scene-hotkeys.d.ts +16 -0
- package/dist/client/scene-hotkeys.js +17 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/values/bindings.d.ts +40 -15
- package/dist/values/controller.d.ts +9 -6
- package/dist/values/effect-schema.d.ts +26 -0
- package/dist/values/effect-schema.js +77 -91
- package/dist/values/hotkey-targets.d.ts +10 -3
- package/dist/values/hotkey-targets.js +20 -0
- package/dist/values/hotkeys.d.ts +9 -1
- package/dist/values/hotkeys.js +11 -0
- package/dist/values/labels.js +1 -0
- package/dist/values/limits.d.ts +96 -8
- package/dist/values/limits.js +109 -1
- package/dist/values/lipsync.d.ts +7 -8
- package/dist/values/locale.d.ts +4 -1
- package/dist/values/locale.js +4 -0
- package/dist/values/model-movement.d.ts +19 -4
- package/dist/values/model-movement.js +17 -4
- package/dist/values/volumetric-lighting.d.ts +1 -1
- package/dist/values/volumetric-lighting.js +1 -1
- package/dist/wire/envelope.js +6 -3
- package/dist/wire/methods.d.ts +16 -17
- package/dist/wire/protocol.d.ts +2 -0
- package/dist/wire/protocol.js +4 -0
- package/dist/wire/schemas/fields.d.ts +8 -0
- package/dist/wire/schemas/fields.js +1 -0
- package/dist/wire/schemas/requests.d.ts +2 -2
- package/dist/wire/schemas/requests.js +70 -20
- package/dist/wire/schemas/settings.d.ts +3 -3
- package/dist/wire/schemas/settings.js +3 -3
- package/dist/wire/types.d.ts +71 -116
- package/dist/wire/types.js +68 -9
- package/package.json +2 -2
- package/dist/client/legacy-kinds.d.ts +0 -2
package/README.md
CHANGED
|
@@ -34,21 +34,21 @@ mouth.set(0.3);
|
|
|
34
34
|
mouth.release();
|
|
35
35
|
```
|
|
36
36
|
|
|
37
|
-
By default the token travels as `?token=` (works in browsers). From Node
|
|
38
|
-
`Authorization: Bearer` header instead
|
|
39
|
-
|
|
40
|
-
|
|
37
|
+
By default the token travels as `?token=` (works in browsers). From Node, `createNodeClient`
|
|
38
|
+
sends it as an `Authorization: Bearer` header instead, over Node's global `WebSocket`, and
|
|
39
|
+
logs the close that ends a session for good (the key revoked, or the session disconnected in
|
|
40
|
+
Persona):
|
|
41
41
|
|
|
42
42
|
```ts
|
|
43
|
-
import
|
|
43
|
+
import { createNodeClient } from "@laplace.live/persona-sdk";
|
|
44
44
|
|
|
45
|
-
const persona =
|
|
46
|
-
|
|
47
|
-
auth: "header",
|
|
48
|
-
createWebSocket: (url, headers) => new WebSocket(url, { headers }),
|
|
49
|
-
});
|
|
45
|
+
const persona = createNodeClient({ token, log: console.warn });
|
|
46
|
+
await persona.connect();
|
|
50
47
|
```
|
|
51
48
|
|
|
49
|
+
Any other socket that can set headers, such as the [`ws`](https://www.npmjs.com/package/ws)
|
|
50
|
+
package, plugs into `new PersonaClient({ token, auth: "header", createWebSocket })`.
|
|
51
|
+
|
|
52
52
|
## Identifying your app
|
|
53
53
|
|
|
54
54
|
Pass `clientInfo` so your app shows up by name under **Connected Clients** in Persona's
|
package/dist/client/client.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { parseServerMessage } from "../wire/envelope.js";
|
|
2
2
|
import { PersonaApiError } from "../wire/errors.js";
|
|
3
|
-
import { CLOSE_FORCE_DISCONNECTED, CLOSE_KEY_REVOKED, INJECT_HEARTBEAT_MS, injectTargetKey, PROTOCOL_VERSION,
|
|
3
|
+
import { CLOSE_FORCE_DISCONNECTED, CLOSE_KEY_REVOKED, INJECT_HEARTBEAT_MS, injectTargetKey, PROTOCOL_VERSION, requestTimeoutMs, } from "../wire/protocol.js";
|
|
4
4
|
import { personaWsUrl } from "./address.js";
|
|
5
|
-
import {
|
|
5
|
+
import { renameLegacyNames } from "./legacy-names.js";
|
|
6
6
|
const DEFAULTS = {
|
|
7
7
|
url: personaWsUrl(),
|
|
8
8
|
reconnectDelayMs: 500,
|
|
@@ -108,8 +108,7 @@ export class PersonaClient {
|
|
|
108
108
|
const timer = setTimeout(() => {
|
|
109
109
|
this.pending.delete(id);
|
|
110
110
|
reject(new Error(`request timed out: ${method}`));
|
|
111
|
-
}, this.opts.requestTimeoutMs ??
|
|
112
|
-
(method === 'scene.activate' ? SCENE_ACTIVATION_TIMEOUT_MS : DEFAULTS.requestTimeoutMs));
|
|
111
|
+
}, this.opts.requestTimeoutMs ?? requestTimeoutMs(method, DEFAULTS.requestTimeoutMs));
|
|
113
112
|
// Only the runtime id ties a response back to `M`, and its result is unvalidated wire JSON.
|
|
114
113
|
this.pending.set(id, { method, resolve: resolve, reject, timer });
|
|
115
114
|
ws.send(JSON.stringify({ kind: 'request', id, method, ...(params === undefined ? {} : { params }) }));
|
|
@@ -357,7 +356,7 @@ export class PersonaClient {
|
|
|
357
356
|
const set = this.listeners.get(msg.event);
|
|
358
357
|
if (!set)
|
|
359
358
|
return;
|
|
360
|
-
const data =
|
|
359
|
+
const data = renameLegacyNames(msg.event, msg.data);
|
|
361
360
|
// Only `msg.event` ties these callbacks to their payload type, and the payload is unvalidated wire JSON.
|
|
362
361
|
for (const cb of set)
|
|
363
362
|
cb(data);
|
|
@@ -372,7 +371,7 @@ export class PersonaClient {
|
|
|
372
371
|
this.pending.delete(id);
|
|
373
372
|
clearTimeout(p.timer);
|
|
374
373
|
if (msg.kind === 'response')
|
|
375
|
-
p.resolve(
|
|
374
|
+
p.resolve(renameLegacyNames(p.method, msg.result));
|
|
376
375
|
else
|
|
377
376
|
p.reject(new PersonaApiError(msg.code, msg.message));
|
|
378
377
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { isRecord } from "../values/guards.js";
|
|
2
|
-
import { canonicalActionKind } from "../wire/types.js";
|
|
2
|
+
import { canonicalActionKind, canonicalAppCapability } from "../wire/types.js";
|
|
3
3
|
function renameHotkeyActions(state) {
|
|
4
4
|
if (!isRecord(state) || !Array.isArray(state.hotkeys))
|
|
5
5
|
return state;
|
|
@@ -23,8 +23,15 @@ function renameActionKinds(list) {
|
|
|
23
23
|
: automation),
|
|
24
24
|
};
|
|
25
25
|
}
|
|
26
|
-
/**
|
|
27
|
-
|
|
26
|
+
/** Old capability names folded into current ones, once each; unknown names stay, as `app.info` reports them. */
|
|
27
|
+
function renameCapabilities(info) {
|
|
28
|
+
if (!isRecord(info) || !Array.isArray(info.capabilities))
|
|
29
|
+
return info;
|
|
30
|
+
const names = info.capabilities.map((name) => typeof name === 'string' ? canonicalAppCapability(name) : name);
|
|
31
|
+
return { ...info, capabilities: [...new Set(names)] };
|
|
32
|
+
}
|
|
33
|
+
/** A method result or event payload with the old names an older host still sends renamed to current ones. */
|
|
34
|
+
export function renameLegacyNames(name, data) {
|
|
28
35
|
switch (name) {
|
|
29
36
|
case 'hotkey.list':
|
|
30
37
|
case 'hotkey.set':
|
|
@@ -34,6 +41,8 @@ export function renameLegacyKinds(name, data) {
|
|
|
34
41
|
case 'automation.list':
|
|
35
42
|
case 'automation.state':
|
|
36
43
|
return renameActionKinds(data);
|
|
44
|
+
case 'app.info':
|
|
45
|
+
return renameCapabilities(data);
|
|
37
46
|
default:
|
|
38
47
|
return data;
|
|
39
48
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { PersonaClientOptions } from './client.ts';
|
|
2
|
+
import { PersonaClient } from './client.ts';
|
|
3
|
+
/** A Node app's own identity and log sink; {@link createNodeClient} sets the transport and close handling. */
|
|
4
|
+
export type NodeClientOptions = Omit<PersonaClientOptions, 'auth' | 'connectTimeoutMs' | 'createWebSocket' | 'onWarning' | 'onClose'> & {
|
|
5
|
+
/** Receives SDK warnings and the notice of a terminal close. */
|
|
6
|
+
log: (message: string) => void;
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* A client for a Node process — the Stream Deck plugin, the MCP server. The API key rides an
|
|
10
|
+
* `Authorization: Bearer` header, never the URL, and a terminal close (key revoked, session
|
|
11
|
+
* disconnected in Persona) is logged, since the client stops redialing after one.
|
|
12
|
+
*/
|
|
13
|
+
export declare function createNodeClient({ log, ...options }: NodeClientOptions): PersonaClient;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { PersonaClient } from "./client.js";
|
|
2
|
+
/** A key press or tool call must answer promptly; the attempt keeps going in the background. */
|
|
3
|
+
const CONNECT_TIMEOUT_MS = 5000;
|
|
4
|
+
/** Trusts a global WebSocket to be Node's: the DOM typings the SDK compiles under only know the browser one. */
|
|
5
|
+
function takesHeaders(ctor) {
|
|
6
|
+
return typeof ctor === 'function';
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* A client for a Node process — the Stream Deck plugin, the MCP server. The API key rides an
|
|
10
|
+
* `Authorization: Bearer` header, never the URL, and a terminal close (key revoked, session
|
|
11
|
+
* disconnected in Persona) is logged, since the client stops redialing after one.
|
|
12
|
+
*/
|
|
13
|
+
export function createNodeClient({ log, ...options }) {
|
|
14
|
+
const Socket = globalThis.WebSocket;
|
|
15
|
+
if (!takesHeaders(Socket))
|
|
16
|
+
throw new Error('createNodeClient needs a global WebSocket (Node 22 or later)');
|
|
17
|
+
return new PersonaClient({
|
|
18
|
+
...options,
|
|
19
|
+
connectTimeoutMs: CONNECT_TIMEOUT_MS,
|
|
20
|
+
auth: 'header',
|
|
21
|
+
createWebSocket: (url, headers) => new Socket(url, { headers }),
|
|
22
|
+
onWarning: log,
|
|
23
|
+
onClose: ({ code, reason, terminal }) => {
|
|
24
|
+
if (!terminal)
|
|
25
|
+
return;
|
|
26
|
+
log(`Persona closed the session (${String(code)}${reason === '' ? '' : `: ${reason}`}) — check the API key in Persona's settings`);
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { HotkeyState, Scene, SceneModelItem } from '../wire/types.ts';
|
|
2
|
+
import type { PersonaClient } from './client.ts';
|
|
3
|
+
/** One on-stage model's `hotkey.list` answer. */
|
|
4
|
+
export interface ModelHotkeys {
|
|
5
|
+
model: SceneModelItem;
|
|
6
|
+
/** First in {@link hotkeyModels} order: the scene's primary model. */
|
|
7
|
+
primary: boolean;
|
|
8
|
+
state: HotkeyState;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* `hotkey.list` for each model whose hotkeys are live ({@link hotkeyModels}: every model on stage, the
|
|
12
|
+
* primary first) — one promise per model, so a caller can settle each alone.
|
|
13
|
+
*/
|
|
14
|
+
export declare function listModelHotkeys(client: Pick<PersonaClient, 'call'>, scene: Pick<Scene, 'items' | 'primaryInstanceId'>): Promise<ModelHotkeys>[];
|
|
15
|
+
/** The active scene's live hotkeys, per model and the primary first; a scene without a model lists none. */
|
|
16
|
+
export declare function listSceneHotkeys(client: Pick<PersonaClient, 'call'>): Promise<ModelHotkeys[]>;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { hotkeyModels } from "../values/hotkeys.js";
|
|
2
|
+
/**
|
|
3
|
+
* `hotkey.list` for each model whose hotkeys are live ({@link hotkeyModels}: every model on stage, the
|
|
4
|
+
* primary first) — one promise per model, so a caller can settle each alone.
|
|
5
|
+
*/
|
|
6
|
+
export function listModelHotkeys(client, scene) {
|
|
7
|
+
return hotkeyModels(scene).map(async (model, index) => ({
|
|
8
|
+
model,
|
|
9
|
+
primary: index === 0,
|
|
10
|
+
state: await client.call('hotkey.list', { modelId: model.ref.id }),
|
|
11
|
+
}));
|
|
12
|
+
}
|
|
13
|
+
/** The active scene's live hotkeys, per model and the primary first; a scene without a model lists none. */
|
|
14
|
+
export async function listSceneHotkeys(client) {
|
|
15
|
+
const { scene } = await client.call('scene.get', {});
|
|
16
|
+
return Promise.all(listModelHotkeys(client, scene));
|
|
17
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
export * from './client/address.ts';
|
|
2
2
|
export * from './client/client.ts';
|
|
3
|
+
export * from './client/node.ts';
|
|
4
|
+
export * from './client/scene-hotkeys.ts';
|
|
3
5
|
export * from './values/arkit.ts';
|
|
4
6
|
export * from './values/bindings.ts';
|
|
5
7
|
export * from './values/camera.ts';
|
|
@@ -31,6 +33,7 @@ export * from './wire/errors.ts';
|
|
|
31
33
|
export * from './wire/events.ts';
|
|
32
34
|
export * from './wire/methods.ts';
|
|
33
35
|
export * from './wire/protocol.ts';
|
|
36
|
+
export * from './wire/schemas/fields.ts';
|
|
34
37
|
export * from './wire/schemas/requests.ts';
|
|
35
38
|
export * from './wire/schemas/settings.ts';
|
|
36
39
|
export * from './wire/types.ts';
|
package/dist/index.js
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
// and the values/registries every Persona app must agree on.
|
|
3
3
|
export * from "./client/address.js";
|
|
4
4
|
export * from "./client/client.js";
|
|
5
|
+
export * from "./client/node.js";
|
|
6
|
+
export * from "./client/scene-hotkeys.js";
|
|
5
7
|
export * from "./values/arkit.js";
|
|
6
8
|
export * from "./values/bindings.js";
|
|
7
9
|
export * from "./values/camera.js";
|
|
@@ -33,6 +35,7 @@ export * from "./wire/errors.js";
|
|
|
33
35
|
export * from "./wire/events.js";
|
|
34
36
|
export * from "./wire/methods.js";
|
|
35
37
|
export * from "./wire/protocol.js";
|
|
38
|
+
export * from "./wire/schemas/fields.js";
|
|
36
39
|
export * from "./wire/schemas/requests.js";
|
|
37
40
|
export * from "./wire/schemas/settings.js";
|
|
38
41
|
export * from "./wire/types.js";
|
|
@@ -6,13 +6,25 @@ export interface Binding {
|
|
|
6
6
|
/** Summed at their weights before the range map; never empty. The first is the input the list files it under. */
|
|
7
7
|
terms: BindingTerm[];
|
|
8
8
|
output: string;
|
|
9
|
-
/**
|
|
9
|
+
/**
|
|
10
|
+
* The rig author's own name for this binding, when their `.vtube.json` carried one
|
|
11
|
+
* (`ParameterSettings[].Name`). It outranks `.cdi3.json`, which names only the output parameter.
|
|
12
|
+
*/
|
|
10
13
|
name?: string;
|
|
11
14
|
inRange: [number, number];
|
|
12
15
|
outRange: [number, number];
|
|
13
16
|
clampOutput: boolean;
|
|
17
|
+
/** VTS's smoothing, per parameter as its manual prescribes, on its own 0-and-up scale; 0 is none. */
|
|
14
18
|
smoothing: number;
|
|
19
|
+
/**
|
|
20
|
+
* VTS's auto-blink: the engine's blink closes the output alongside any input. Only on the
|
|
21
|
+
* model's `EyeBlink` parameters, the ones that blink writes; inert elsewhere.
|
|
22
|
+
*/
|
|
15
23
|
autoBlink: boolean;
|
|
24
|
+
/**
|
|
25
|
+
* Response shape over the normalized input, replacing the straight line between the ranges.
|
|
26
|
+
* Absent until something authors one: `.vtube.json` has no curve.
|
|
27
|
+
*/
|
|
16
28
|
curve?: Curve;
|
|
17
29
|
}
|
|
18
30
|
/** A parameter the rig declares, as the picker lists it. */
|
|
@@ -29,23 +41,36 @@ export interface BindingLiveValues {
|
|
|
29
41
|
/** What each bound parameter reads on the model right now, by output id; empty for a host that cannot read it back. */
|
|
30
42
|
outputs: Record<string, number>;
|
|
31
43
|
}
|
|
44
|
+
/** A `.cdi3.json` parameter group. */
|
|
45
|
+
export interface CubismParameterGroup {
|
|
46
|
+
id: string;
|
|
47
|
+
name: string;
|
|
48
|
+
}
|
|
49
|
+
/** The rig author's own naming, from `.cdi3.json`; every map omits ids the file leaves unnamed. */
|
|
50
|
+
export interface CubismDisplayInfo {
|
|
51
|
+
parameterNames: Record<string, string>;
|
|
52
|
+
/** Parameter id → group id. Absent for a parameter the rig left ungrouped. */
|
|
53
|
+
parameterGroups: Record<string, string>;
|
|
54
|
+
/** Groups in file order — the order the rig author arranged them in. */
|
|
55
|
+
groups: CubismParameterGroup[];
|
|
56
|
+
}
|
|
57
|
+
/** What `.physics3.json` says about parameters. */
|
|
58
|
+
export interface CubismPhysicsInfo {
|
|
59
|
+
/** Parameters that drive physics — normal to track. */
|
|
60
|
+
inputs: string[];
|
|
61
|
+
/** Parameters physics writes; tracking one of these fights the simulation. */
|
|
62
|
+
outputs: string[];
|
|
63
|
+
}
|
|
64
|
+
/** What a rig's Cubism sidecars say about its parameters; each side null when absent or unusable. */
|
|
65
|
+
export interface CubismMetadata {
|
|
66
|
+
display: CubismDisplayInfo | null;
|
|
67
|
+
physics: CubismPhysicsInfo | null;
|
|
68
|
+
}
|
|
32
69
|
/** Metadata read from a rig's display and physics sidecars. */
|
|
33
70
|
export interface BindingEditorData {
|
|
34
71
|
connections: Binding[];
|
|
35
72
|
parameters: BindableParameter[];
|
|
36
|
-
metadata:
|
|
37
|
-
|
|
38
|
-
parameterNames: Record<string, string>;
|
|
39
|
-
parameterGroups: Record<string, string>;
|
|
40
|
-
groups: {
|
|
41
|
-
id: string;
|
|
42
|
-
name: string;
|
|
43
|
-
}[];
|
|
44
|
-
} | null;
|
|
45
|
-
physics: {
|
|
46
|
-
inputs: string[];
|
|
47
|
-
outputs: string[];
|
|
48
|
-
} | null;
|
|
49
|
-
};
|
|
73
|
+
metadata: CubismMetadata;
|
|
74
|
+
/** The connections came from Persona's sidecar, not the rig's `.vtube.json`. */
|
|
50
75
|
edited: boolean;
|
|
51
76
|
}
|
|
@@ -92,6 +92,7 @@ export declare const CONTROLLER_DEAD_ZONE_MAX = 0.5;
|
|
|
92
92
|
export interface ControllerProfileInfo {
|
|
93
93
|
slot: ControllerSlot;
|
|
94
94
|
deviceId: string;
|
|
95
|
+
/** User label; absent keeps the numbered controller name. */
|
|
95
96
|
name?: string;
|
|
96
97
|
/** Radial stick dead zone, 0..0.5; absent until tuned, meaning `DEFAULT_CONTROLLER_DEAD_ZONE`. */
|
|
97
98
|
deadZone?: number;
|
|
@@ -100,15 +101,17 @@ export interface ControllerConfig {
|
|
|
100
101
|
enabled: boolean;
|
|
101
102
|
profiles: ControllerProfileInfo[];
|
|
102
103
|
}
|
|
104
|
+
/** A live connection and the saved profile it is identified as, if any. */
|
|
105
|
+
export interface ControllerDeviceInfo {
|
|
106
|
+
id: string;
|
|
107
|
+
index: number;
|
|
108
|
+
mapping: string;
|
|
109
|
+
slot: ControllerSlot | null;
|
|
110
|
+
}
|
|
103
111
|
/** Stage-owned input sample; hardware is never polled by the remote console. */
|
|
104
112
|
export interface ControllerState {
|
|
105
113
|
profiles: ControllerProfileInfo[];
|
|
106
|
-
devices:
|
|
107
|
-
id: string;
|
|
108
|
-
index: number;
|
|
109
|
-
mapping: string;
|
|
110
|
-
slot: ControllerSlot | null;
|
|
111
|
-
}[];
|
|
114
|
+
devices: ControllerDeviceInfo[];
|
|
112
115
|
inputs: Partial<Record<ControllerInputName, number>>;
|
|
113
116
|
pressed: ControllerTriggerName[];
|
|
114
117
|
assigningSlot: ControllerSlot | null;
|
|
@@ -60,6 +60,23 @@ export interface EffectEnumSpec<T extends string = string> {
|
|
|
60
60
|
default: T;
|
|
61
61
|
values: readonly T[];
|
|
62
62
|
}
|
|
63
|
+
/** Levels' color channels, run before the master levels; each suffixes its own copy of the params (`inputBlackR`). */
|
|
64
|
+
export declare const LEVEL_CHANNELS: readonly ["R", "G", "B"];
|
|
65
|
+
/** Color Shift's hue bands, around the wheel from red; each suffixes its own copy of the params (`hueRed`). */
|
|
66
|
+
export declare const COLOR_SHIFT_BANDS: readonly ["Red", "Yellow", "Green", "Cyan", "Blue", "Magenta"];
|
|
67
|
+
/** Select Colors' color slots, `color1` up. */
|
|
68
|
+
export declare const SELECT_COLORS_SLOTS: readonly [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
|
|
69
|
+
/** A Select Colors slot, one of {@link SELECT_COLORS_SLOTS}. */
|
|
70
|
+
export type SelectColorsSlot = (typeof SELECT_COLORS_SLOTS)[number];
|
|
71
|
+
/** The color slots a layer's bloom can limit its glow to, `color1` up. */
|
|
72
|
+
export declare const BLOOM_COLOR_SLOTS: readonly [1, 2, 3, 4, 5];
|
|
73
|
+
/** A layer bloom color slot, one of {@link BLOOM_COLOR_SLOTS}. */
|
|
74
|
+
export type BloomColorSlot = (typeof BLOOM_COLOR_SLOTS)[number];
|
|
75
|
+
/**
|
|
76
|
+
* `table` once per suffix, suffix by suffix, each key suffixed (`{ hue }` over `['Red', 'Blue']` is
|
|
77
|
+
* `{ hueRed, hueBlue }`): how a per-channel parameter set spells its specs, labels and descriptions.
|
|
78
|
+
*/
|
|
79
|
+
export declare function suffixKeys<K extends string, V, S extends string>(table: Readonly<Record<K, V>>, suffixes: readonly S[]): Record<`${K}${S}`, V>;
|
|
63
80
|
export declare const EFFECT_SPECS: Record<EffectKey, Readonly<Record<string, EffectParamSpec>>>;
|
|
64
81
|
/**
|
|
65
82
|
* Hex-color params, keyed like {@link EFFECT_SPECS} — generic walkers (healing,
|
|
@@ -71,6 +88,15 @@ export declare const EFFECT_COLOR_SPECS: Partial<Record<EffectKey, Readonly<Reco
|
|
|
71
88
|
export declare const EFFECT_BOOLEAN_SPECS: Partial<Record<EffectKey, Readonly<Record<string, EffectBooleanSpec>>>>;
|
|
72
89
|
/** Blend-mode order shared by effect editors and shader uniforms. */
|
|
73
90
|
export declare const EFFECT_BLEND_MODES: readonly ["normal", "darken", "multiply", "colorBurn", "linearBurn", "add", "lighten", "screen", "colorDodge", "overlay", "softLight", "hardLight", "vividLight", "linearLight", "pinLight", "hardMix", "difference", "exclusion", "subtract", "divide", "hue", "saturation", "color", "luminosity"];
|
|
91
|
+
export declare const FLARE_MODES: readonly ["add", "screen", "colorDodge"];
|
|
92
|
+
export declare const FLARE_RAYS_MODES: readonly ["sun", "shine", "god"];
|
|
93
|
+
export declare const GRADIENT_TYPES: readonly ["solid", "linear", "circle", "ellipse"];
|
|
94
|
+
export declare const BLUR_MODES: readonly ["gaussian", "bokeh"];
|
|
95
|
+
export declare const BLOOM_MODES: readonly ["normal", "streak", "star"];
|
|
96
|
+
export declare const RIM_MODES: readonly ["single", "double", "sharpenSingle", "sharpenDouble"];
|
|
97
|
+
export declare const COLOR_WHEELS_MODES: readonly ["liftGammaGain", "shadowsMidtonesHighlights"];
|
|
98
|
+
/** Effects quality tiers, lowest first. */
|
|
99
|
+
export declare const EFFECTS_QUALITY_LEVELS: readonly ["low", "medium", "high"];
|
|
74
100
|
/** Enum defaults and allowed values. */
|
|
75
101
|
export declare const EFFECT_ENUM_SPECS: Partial<Record<EffectKey, Readonly<Record<string, EffectEnumSpec>>>>;
|
|
76
102
|
/** Select the layer effects from any value set containing them. */
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Scope and parameter metadata are plain data; labels and rendering stay with their consumers.
|
|
2
|
-
import { finiteOr, isOneOf, isRecord } from "./guards.js";
|
|
2
|
+
import { finiteOr, isOneOf, isRecord, keysOf } from "./guards.js";
|
|
3
3
|
import { clamp, hexColorOr } from "./limits.js";
|
|
4
4
|
/** Supported owners of each toggle effect; scene and layer membership are independent. */
|
|
5
5
|
export const EFFECT_SCOPES = {
|
|
@@ -48,6 +48,57 @@ export const SCENE_EFFECT_KEYS = keysForScope('scene');
|
|
|
48
48
|
/** Scene toggle keys; retained for existing SDK consumers. */
|
|
49
49
|
export const TOGGLE_EFFECT_KEYS = SCENE_EFFECT_KEYS;
|
|
50
50
|
export const LAYER_EFFECT_KEYS = keysForScope('layer');
|
|
51
|
+
/** Levels' color channels, run before the master levels; each suffixes its own copy of the params (`inputBlackR`). */
|
|
52
|
+
export const LEVEL_CHANNELS = ['R', 'G', 'B'];
|
|
53
|
+
/** Color Shift's hue bands, around the wheel from red; each suffixes its own copy of the params (`hueRed`). */
|
|
54
|
+
export const COLOR_SHIFT_BANDS = ['Red', 'Yellow', 'Green', 'Cyan', 'Blue', 'Magenta'];
|
|
55
|
+
/** Select Colors' color slots, `color1` up. */
|
|
56
|
+
export const SELECT_COLORS_SLOTS = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
|
|
57
|
+
/** The color slots a layer's bloom can limit its glow to, `color1` up. */
|
|
58
|
+
export const BLOOM_COLOR_SLOTS = [1, 2, 3, 4, 5];
|
|
59
|
+
/** One channel's levels, shared by the master set and each of {@link LEVEL_CHANNELS}. */
|
|
60
|
+
const LEVEL_SPECS = {
|
|
61
|
+
inputBlack: { default: 0, min: 0, max: 1, step: 0.01 },
|
|
62
|
+
inputWhite: { default: 1, min: 0, max: 1, step: 0.01 },
|
|
63
|
+
inputGamma: { default: 1, min: 0.1, max: 4, step: 0.01 },
|
|
64
|
+
outputBlack: { default: 0, min: 0, max: 1, step: 0.01 },
|
|
65
|
+
outputWhite: { default: 1, min: 0, max: 1, step: 0.01 },
|
|
66
|
+
};
|
|
67
|
+
/** One hue band's offsets, shared by each of {@link COLOR_SHIFT_BANDS}. */
|
|
68
|
+
const COLOR_SHIFT_SPECS = {
|
|
69
|
+
hue: { default: 0, min: -180, max: 180, step: 1, unit: '°' },
|
|
70
|
+
saturation: { default: 0, min: -1, max: 1, step: 0.01 },
|
|
71
|
+
luminance: { default: 0, min: -1, max: 1, step: 0.01 },
|
|
72
|
+
luminanceSaturation: { default: 0, min: -1, max: 1, step: 0.01 },
|
|
73
|
+
};
|
|
74
|
+
const SELECT_COLORS_DEFAULTS = {
|
|
75
|
+
1: '#ff0000',
|
|
76
|
+
2: '#ffff00',
|
|
77
|
+
3: '#00ff00',
|
|
78
|
+
4: '#00ffff',
|
|
79
|
+
5: '#0000ff',
|
|
80
|
+
6: '#ff00ff',
|
|
81
|
+
7: '#ffffff',
|
|
82
|
+
8: '#808080',
|
|
83
|
+
9: '#000000',
|
|
84
|
+
10: '#ff8800',
|
|
85
|
+
};
|
|
86
|
+
/**
|
|
87
|
+
* `table` once per suffix, suffix by suffix, each key suffixed (`{ hue }` over `['Red', 'Blue']` is
|
|
88
|
+
* `{ hueRed, hueBlue }`): how a per-channel parameter set spells its specs, labels and descriptions.
|
|
89
|
+
*/
|
|
90
|
+
export function suffixKeys(table, suffixes) {
|
|
91
|
+
// Object.fromEntries widens its keys to string; the entries are exactly the keys named.
|
|
92
|
+
return Object.fromEntries(suffixes.flatMap(suffix => keysOf(table).map(key => [`${key}${suffix}`, table[key]])));
|
|
93
|
+
}
|
|
94
|
+
/** `spec(slot)` for each color slot, keyed `color${slot}${suffix}` in slot order. */
|
|
95
|
+
function colorSlotSpecs(slots, suffix, spec) {
|
|
96
|
+
// Object.fromEntries widens its keys to string; the entries are exactly the keys named.
|
|
97
|
+
return Object.fromEntries(slots.map(slot => [`color${slot}${suffix}`, spec(slot)]));
|
|
98
|
+
}
|
|
99
|
+
// The first slot starts on: with every pick off the mask is zero, so switching selection on
|
|
100
|
+
// would select nothing rather than narrow.
|
|
101
|
+
const slotEnabled = (slot) => ({ default: slot === 1 });
|
|
51
102
|
// Declared string-indexed so generic call sites (slider loop, healing walk) iterate
|
|
52
103
|
// without erasure casts; `satisfies` still checks exact per-effect param parity here.
|
|
53
104
|
export const EFFECT_SPECS = {
|
|
@@ -60,28 +111,7 @@ export const EFFECT_SPECS = {
|
|
|
60
111
|
temperature: { default: 0, min: -100, max: 100, step: 1 },
|
|
61
112
|
tint: { default: 0, min: -100, max: 100, step: 1 },
|
|
62
113
|
},
|
|
63
|
-
levels:
|
|
64
|
-
inputBlack: { default: 0, min: 0, max: 1, step: 0.01 },
|
|
65
|
-
inputWhite: { default: 1, min: 0, max: 1, step: 0.01 },
|
|
66
|
-
inputGamma: { default: 1, min: 0.1, max: 4, step: 0.01 },
|
|
67
|
-
outputBlack: { default: 0, min: 0, max: 1, step: 0.01 },
|
|
68
|
-
outputWhite: { default: 1, min: 0, max: 1, step: 0.01 },
|
|
69
|
-
inputBlackR: { default: 0, min: 0, max: 1, step: 0.01 },
|
|
70
|
-
inputWhiteR: { default: 1, min: 0, max: 1, step: 0.01 },
|
|
71
|
-
inputGammaR: { default: 1, min: 0.1, max: 4, step: 0.01 },
|
|
72
|
-
outputBlackR: { default: 0, min: 0, max: 1, step: 0.01 },
|
|
73
|
-
outputWhiteR: { default: 1, min: 0, max: 1, step: 0.01 },
|
|
74
|
-
inputBlackG: { default: 0, min: 0, max: 1, step: 0.01 },
|
|
75
|
-
inputWhiteG: { default: 1, min: 0, max: 1, step: 0.01 },
|
|
76
|
-
inputGammaG: { default: 1, min: 0.1, max: 4, step: 0.01 },
|
|
77
|
-
outputBlackG: { default: 0, min: 0, max: 1, step: 0.01 },
|
|
78
|
-
outputWhiteG: { default: 1, min: 0, max: 1, step: 0.01 },
|
|
79
|
-
inputBlackB: { default: 0, min: 0, max: 1, step: 0.01 },
|
|
80
|
-
inputWhiteB: { default: 1, min: 0, max: 1, step: 0.01 },
|
|
81
|
-
inputGammaB: { default: 1, min: 0.1, max: 4, step: 0.01 },
|
|
82
|
-
outputBlackB: { default: 0, min: 0, max: 1, step: 0.01 },
|
|
83
|
-
outputWhiteB: { default: 1, min: 0, max: 1, step: 0.01 },
|
|
84
|
-
},
|
|
114
|
+
levels: suffixKeys(LEVEL_SPECS, ['', ...LEVEL_CHANNELS]),
|
|
85
115
|
colorWheels: {
|
|
86
116
|
lift: { default: 0, min: -1, max: 1, step: 0.01 },
|
|
87
117
|
gamma: { default: 0, min: -1, max: 1, step: 0.01 },
|
|
@@ -92,32 +122,7 @@ export const EFFECT_SPECS = {
|
|
|
92
122
|
shadowLimit: { default: 0.3, min: 0, max: 1, step: 0.01 },
|
|
93
123
|
highlightLimit: { default: 0.7, min: 0, max: 1, step: 0.01 },
|
|
94
124
|
},
|
|
95
|
-
colorShift:
|
|
96
|
-
hueRed: { default: 0, min: -180, max: 180, step: 1, unit: '°' },
|
|
97
|
-
saturationRed: { default: 0, min: -1, max: 1, step: 0.01 },
|
|
98
|
-
luminanceRed: { default: 0, min: -1, max: 1, step: 0.01 },
|
|
99
|
-
luminanceSaturationRed: { default: 0, min: -1, max: 1, step: 0.01 },
|
|
100
|
-
hueYellow: { default: 0, min: -180, max: 180, step: 1, unit: '°' },
|
|
101
|
-
saturationYellow: { default: 0, min: -1, max: 1, step: 0.01 },
|
|
102
|
-
luminanceYellow: { default: 0, min: -1, max: 1, step: 0.01 },
|
|
103
|
-
luminanceSaturationYellow: { default: 0, min: -1, max: 1, step: 0.01 },
|
|
104
|
-
hueGreen: { default: 0, min: -180, max: 180, step: 1, unit: '°' },
|
|
105
|
-
saturationGreen: { default: 0, min: -1, max: 1, step: 0.01 },
|
|
106
|
-
luminanceGreen: { default: 0, min: -1, max: 1, step: 0.01 },
|
|
107
|
-
luminanceSaturationGreen: { default: 0, min: -1, max: 1, step: 0.01 },
|
|
108
|
-
hueCyan: { default: 0, min: -180, max: 180, step: 1, unit: '°' },
|
|
109
|
-
saturationCyan: { default: 0, min: -1, max: 1, step: 0.01 },
|
|
110
|
-
luminanceCyan: { default: 0, min: -1, max: 1, step: 0.01 },
|
|
111
|
-
luminanceSaturationCyan: { default: 0, min: -1, max: 1, step: 0.01 },
|
|
112
|
-
hueBlue: { default: 0, min: -180, max: 180, step: 1, unit: '°' },
|
|
113
|
-
saturationBlue: { default: 0, min: -1, max: 1, step: 0.01 },
|
|
114
|
-
luminanceBlue: { default: 0, min: -1, max: 1, step: 0.01 },
|
|
115
|
-
luminanceSaturationBlue: { default: 0, min: -1, max: 1, step: 0.01 },
|
|
116
|
-
hueMagenta: { default: 0, min: -180, max: 180, step: 1, unit: '°' },
|
|
117
|
-
saturationMagenta: { default: 0, min: -1, max: 1, step: 0.01 },
|
|
118
|
-
luminanceMagenta: { default: 0, min: -1, max: 1, step: 0.01 },
|
|
119
|
-
luminanceSaturationMagenta: { default: 0, min: -1, max: 1, step: 0.01 },
|
|
120
|
-
},
|
|
125
|
+
colorShift: suffixKeys(COLOR_SHIFT_SPECS, COLOR_SHIFT_BANDS),
|
|
121
126
|
selectColors: {
|
|
122
127
|
hueRange: { default: 0.08, min: 0, max: 0.5, step: 0.01 },
|
|
123
128
|
saturationRange: { default: 1, min: 0, max: 1, step: 0.01 },
|
|
@@ -202,6 +207,7 @@ export const EFFECT_SPECS = {
|
|
|
202
207
|
},
|
|
203
208
|
outline: {
|
|
204
209
|
size: { default: 0, min: 0, max: 1, step: 0.01 },
|
|
210
|
+
roundness: { default: 0, min: 0, max: 1, step: 0.01 },
|
|
205
211
|
opacity: { default: 1, min: 0, max: 1, step: 0.01 },
|
|
206
212
|
},
|
|
207
213
|
// VTube Studio's backlight shadow: an offset silhouette copy. VTS hides it at
|
|
@@ -288,25 +294,10 @@ export const EFFECT_COLOR_SPECS = {
|
|
|
288
294
|
midtonesColor: { default: '#ffffff' },
|
|
289
295
|
highlightsColor: { default: '#ffffff' },
|
|
290
296
|
},
|
|
291
|
-
selectColors: {
|
|
292
|
-
color1: { default: '#ff0000' },
|
|
293
|
-
color2: { default: '#ffff00' },
|
|
294
|
-
color3: { default: '#00ff00' },
|
|
295
|
-
color4: { default: '#00ffff' },
|
|
296
|
-
color5: { default: '#0000ff' },
|
|
297
|
-
color6: { default: '#ff00ff' },
|
|
298
|
-
color7: { default: '#ffffff' },
|
|
299
|
-
color8: { default: '#808080' },
|
|
300
|
-
color9: { default: '#000000' },
|
|
301
|
-
color10: { default: '#ff8800' },
|
|
302
|
-
},
|
|
297
|
+
selectColors: colorSlotSpecs(SELECT_COLORS_SLOTS, '', slot => ({ default: SELECT_COLORS_DEFAULTS[slot] })),
|
|
303
298
|
bloom: {
|
|
304
299
|
tint: { default: '#ffffff' },
|
|
305
|
-
|
|
306
|
-
color2: { default: '#ffffff' },
|
|
307
|
-
color3: { default: '#ffffff' },
|
|
308
|
-
color4: { default: '#ffffff' },
|
|
309
|
-
color5: { default: '#ffffff' },
|
|
300
|
+
...colorSlotSpecs(BLOOM_COLOR_SLOTS, '', () => ({ default: '#ffffff' })),
|
|
310
301
|
},
|
|
311
302
|
rim: {
|
|
312
303
|
color: { default: '#ffeb74' },
|
|
@@ -334,26 +325,11 @@ export const EFFECT_BOOLEAN_SPECS = {
|
|
|
334
325
|
bloom: {
|
|
335
326
|
selectColors: { default: false },
|
|
336
327
|
invertColors: { default: false },
|
|
337
|
-
|
|
338
|
-
// selection on would make the glow vanish rather than narrow.
|
|
339
|
-
color1Enabled: { default: true },
|
|
340
|
-
color2Enabled: { default: false },
|
|
341
|
-
color3Enabled: { default: false },
|
|
342
|
-
color4Enabled: { default: false },
|
|
343
|
-
color5Enabled: { default: false },
|
|
328
|
+
...colorSlotSpecs(BLOOM_COLOR_SLOTS, 'Enabled', slotEnabled),
|
|
344
329
|
},
|
|
345
330
|
selectColors: {
|
|
346
331
|
invert: { default: false },
|
|
347
|
-
|
|
348
|
-
color2Enabled: { default: false },
|
|
349
|
-
color3Enabled: { default: false },
|
|
350
|
-
color4Enabled: { default: false },
|
|
351
|
-
color5Enabled: { default: false },
|
|
352
|
-
color6Enabled: { default: false },
|
|
353
|
-
color7Enabled: { default: false },
|
|
354
|
-
color8Enabled: { default: false },
|
|
355
|
-
color9Enabled: { default: false },
|
|
356
|
-
color10Enabled: { default: false },
|
|
332
|
+
...colorSlotSpecs(SELECT_COLORS_SLOTS, 'Enabled', slotEnabled),
|
|
357
333
|
},
|
|
358
334
|
};
|
|
359
335
|
/** Blend-mode order shared by effect editors and shader uniforms. */
|
|
@@ -383,21 +359,31 @@ export const EFFECT_BLEND_MODES = [
|
|
|
383
359
|
'color',
|
|
384
360
|
'luminosity',
|
|
385
361
|
];
|
|
362
|
+
// The wire types derive from these, so every member a type admits also heals and publishes.
|
|
363
|
+
export const FLARE_MODES = ['add', 'screen', 'colorDodge'];
|
|
364
|
+
export const FLARE_RAYS_MODES = ['sun', 'shine', 'god'];
|
|
365
|
+
export const GRADIENT_TYPES = ['solid', 'linear', 'circle', 'ellipse'];
|
|
366
|
+
export const BLUR_MODES = ['gaussian', 'bokeh'];
|
|
367
|
+
export const BLOOM_MODES = ['normal', 'streak', 'star'];
|
|
368
|
+
export const RIM_MODES = ['single', 'double', 'sharpenSingle', 'sharpenDouble'];
|
|
369
|
+
export const COLOR_WHEELS_MODES = ['liftGammaGain', 'shadowsMidtonesHighlights'];
|
|
370
|
+
/** Effects quality tiers, lowest first. */
|
|
371
|
+
export const EFFECTS_QUALITY_LEVELS = ['low', 'medium', 'high'];
|
|
386
372
|
/** Enum defaults and allowed values. */
|
|
387
373
|
export const EFFECT_ENUM_SPECS = {
|
|
388
374
|
flare: {
|
|
389
|
-
mode: { default: 'add', values:
|
|
390
|
-
raysMode: { default: 'sun', values:
|
|
375
|
+
mode: { default: 'add', values: FLARE_MODES },
|
|
376
|
+
raysMode: { default: 'sun', values: FLARE_RAYS_MODES },
|
|
391
377
|
},
|
|
392
378
|
gradient: {
|
|
393
|
-
type: { default: 'circle', values:
|
|
379
|
+
type: { default: 'circle', values: GRADIENT_TYPES },
|
|
394
380
|
blendMode: { default: 'normal', values: EFFECT_BLEND_MODES },
|
|
395
381
|
},
|
|
396
|
-
blur: { mode: { default: 'gaussian', values:
|
|
397
|
-
bloom: { mode: { default: 'normal', values:
|
|
398
|
-
rim: { mode: { default: 'single', values:
|
|
399
|
-
outline: { quality: { default: 'medium', values:
|
|
400
|
-
colorWheels: { mode: { default: 'liftGammaGain', values:
|
|
382
|
+
blur: { mode: { default: 'gaussian', values: BLUR_MODES } },
|
|
383
|
+
bloom: { mode: { default: 'normal', values: BLOOM_MODES } },
|
|
384
|
+
rim: { mode: { default: 'single', values: RIM_MODES } },
|
|
385
|
+
outline: { quality: { default: 'medium', values: EFFECTS_QUALITY_LEVELS } },
|
|
386
|
+
colorWheels: { mode: { default: 'liftGammaGain', values: COLOR_WHEELS_MODES } },
|
|
401
387
|
};
|
|
402
388
|
/** Select the layer effects from any value set containing them. */
|
|
403
389
|
export function pickLayerEffects(effects) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type { Hotkey, MotionGroup } from '../wire/types.ts';
|
|
1
|
+
import type { Hotkey, ModelRef, MotionGroup } from '../wire/types.ts';
|
|
2
2
|
/** A hotkey action Persona can run. */
|
|
3
|
-
type HotkeyAction = NonNullable<Hotkey['action']>;
|
|
3
|
+
export type HotkeyAction = NonNullable<Hotkey['action']>;
|
|
4
4
|
/** What a model offers hotkeys: its expressions (a null `file` on formats with none) and its motion groups. */
|
|
5
5
|
export interface HotkeyListing {
|
|
6
6
|
expressions: readonly {
|
|
@@ -53,4 +53,11 @@ export declare function modelMatchesFile(file: string, model: {
|
|
|
53
53
|
vtubeFile?: string | null;
|
|
54
54
|
entryFile?: string;
|
|
55
55
|
}): boolean;
|
|
56
|
-
|
|
56
|
+
/**
|
|
57
|
+
* Labels a model's hotkeys by what each points at — an expression's Name, a motion's label, a
|
|
58
|
+
* load-model's model name — or undefined for an action with no file, or once the target is gone.
|
|
59
|
+
* Walks the listing once, so a shortcut list builds one per model and asks it per row.
|
|
60
|
+
* @param models `model.list`'s refs; scene item refs carry no `vtubeFile` (see {@link modelMatchesFile}).
|
|
61
|
+
* @param groupLabel Display name for a motion group; VRM groups are clip refs, not names.
|
|
62
|
+
*/
|
|
63
|
+
export declare function hotkeyTargetLabels(listing: HotkeyListing, models: readonly Pick<ModelRef, 'name' | 'vtubeFile' | 'entryFile'>[], groupLabel?: (group: string) => string): (h: Pick<Hotkey, 'action' | 'file'>) => string | undefined;
|