@laplace.live/persona-sdk 1.24.0 → 1.25.1
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/edit-history.d.ts +4 -0
- package/dist/values/edit-history.js +14 -7
- package/dist/values/effect-schema.d.ts +26 -0
- package/dist/values/effect-schema.js +76 -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 +68 -115
- 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;
|
|
@@ -14,10 +14,14 @@ export interface EditHistory<T> {
|
|
|
14
14
|
redo(): T | null;
|
|
15
15
|
canUndo(): boolean;
|
|
16
16
|
canRedo(): boolean;
|
|
17
|
+
/** The cursor entry itself, uncloned — read it, never mutate it; null before the first reset. */
|
|
18
|
+
peek(): T | null;
|
|
17
19
|
/**
|
|
18
20
|
* Rewrite the run of entries around the cursor that share its `runKey`, outward until the key
|
|
19
21
|
* changes on either side — a fold, never a step, so undo and redo across the run all carry it.
|
|
20
22
|
*/
|
|
21
23
|
amend(runKey: (value: T) => string, rewrite: (value: T) => void): void;
|
|
24
|
+
/** Rewrite every entry, the redo tail included — never a step; for an identity every entry must follow. */
|
|
25
|
+
rewriteAll(rewrite: (value: T) => void): void;
|
|
22
26
|
}
|
|
23
27
|
export declare function createEditHistory<T>(limit?: number): EditHistory<T>;
|
|
@@ -3,6 +3,12 @@ export function createEditHistory(limit = EDIT_HISTORY_LIMIT) {
|
|
|
3
3
|
let stack = [];
|
|
4
4
|
let cursor = -1;
|
|
5
5
|
const entryOf = (value, key = JSON.stringify(value)) => ({ value: structuredClone(value), key });
|
|
6
|
+
// A fold rewrites entries per frame while the camera eases; only the cursor's key is ever compared.
|
|
7
|
+
const keyOf = (entry) => (entry.key ??= JSON.stringify(entry.value));
|
|
8
|
+
const rewriteEntry = (entry, rewrite) => {
|
|
9
|
+
rewrite(entry.value);
|
|
10
|
+
entry.key = null;
|
|
11
|
+
};
|
|
6
12
|
return {
|
|
7
13
|
reset(value) {
|
|
8
14
|
stack = [entryOf(value)];
|
|
@@ -14,7 +20,7 @@ export function createEditHistory(limit = EDIT_HISTORY_LIMIT) {
|
|
|
14
20
|
// the redo tail alive.
|
|
15
21
|
const key = JSON.stringify(value);
|
|
16
22
|
const cur = stack[cursor];
|
|
17
|
-
if (cur && cur
|
|
23
|
+
if (cur && keyOf(cur) === key)
|
|
18
24
|
return;
|
|
19
25
|
stack.length = cursor + 1;
|
|
20
26
|
stack.push(entryOf(value, key));
|
|
@@ -38,27 +44,28 @@ export function createEditHistory(limit = EDIT_HISTORY_LIMIT) {
|
|
|
38
44
|
},
|
|
39
45
|
canUndo: () => cursor > 0,
|
|
40
46
|
canRedo: () => cursor < stack.length - 1,
|
|
47
|
+
peek: () => stack[cursor]?.value ?? null,
|
|
41
48
|
amend(runKey, rewrite) {
|
|
42
49
|
const cur = stack[cursor];
|
|
43
50
|
if (!cur)
|
|
44
51
|
return;
|
|
45
52
|
const from = runKey(cur.value);
|
|
46
|
-
const apply = (e) => {
|
|
47
|
-
rewrite(e.value);
|
|
48
|
-
e.key = JSON.stringify(e.value);
|
|
49
|
-
};
|
|
50
53
|
for (let i = cursor; i >= 0; i--) {
|
|
51
54
|
const entry = stack[i];
|
|
52
55
|
if (runKey(entry.value) !== from)
|
|
53
56
|
break;
|
|
54
|
-
|
|
57
|
+
rewriteEntry(entry, rewrite);
|
|
55
58
|
}
|
|
56
59
|
for (let i = cursor + 1; i < stack.length; i++) {
|
|
57
60
|
const entry = stack[i];
|
|
58
61
|
if (runKey(entry.value) !== from)
|
|
59
62
|
break;
|
|
60
|
-
|
|
63
|
+
rewriteEntry(entry, rewrite);
|
|
61
64
|
}
|
|
62
65
|
},
|
|
66
|
+
rewriteAll(rewrite) {
|
|
67
|
+
for (const entry of stack)
|
|
68
|
+
rewriteEntry(entry, rewrite);
|
|
69
|
+
},
|
|
63
70
|
};
|
|
64
71
|
}
|
|
@@ -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. */
|