@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.
Files changed (44) hide show
  1. package/README.md +10 -10
  2. package/dist/client/client.js +5 -6
  3. package/dist/client/legacy-names.d.ts +2 -0
  4. package/dist/client/{legacy-kinds.js → legacy-names.js} +12 -3
  5. package/dist/client/node.d.ts +13 -0
  6. package/dist/client/node.js +29 -0
  7. package/dist/client/scene-hotkeys.d.ts +16 -0
  8. package/dist/client/scene-hotkeys.js +17 -0
  9. package/dist/index.d.ts +3 -0
  10. package/dist/index.js +3 -0
  11. package/dist/values/bindings.d.ts +40 -15
  12. package/dist/values/controller.d.ts +9 -6
  13. package/dist/values/edit-history.d.ts +4 -0
  14. package/dist/values/edit-history.js +14 -7
  15. package/dist/values/effect-schema.d.ts +26 -0
  16. package/dist/values/effect-schema.js +76 -91
  17. package/dist/values/hotkey-targets.d.ts +10 -3
  18. package/dist/values/hotkey-targets.js +20 -0
  19. package/dist/values/hotkeys.d.ts +9 -1
  20. package/dist/values/hotkeys.js +11 -0
  21. package/dist/values/labels.js +1 -0
  22. package/dist/values/limits.d.ts +96 -8
  23. package/dist/values/limits.js +109 -1
  24. package/dist/values/lipsync.d.ts +7 -8
  25. package/dist/values/locale.d.ts +4 -1
  26. package/dist/values/locale.js +4 -0
  27. package/dist/values/model-movement.d.ts +19 -4
  28. package/dist/values/model-movement.js +17 -4
  29. package/dist/values/volumetric-lighting.d.ts +1 -1
  30. package/dist/values/volumetric-lighting.js +1 -1
  31. package/dist/wire/envelope.js +6 -3
  32. package/dist/wire/methods.d.ts +16 -17
  33. package/dist/wire/protocol.d.ts +2 -0
  34. package/dist/wire/protocol.js +4 -0
  35. package/dist/wire/schemas/fields.d.ts +8 -0
  36. package/dist/wire/schemas/fields.js +1 -0
  37. package/dist/wire/schemas/requests.d.ts +2 -2
  38. package/dist/wire/schemas/requests.js +70 -20
  39. package/dist/wire/schemas/settings.d.ts +3 -3
  40. package/dist/wire/schemas/settings.js +3 -3
  41. package/dist/wire/types.d.ts +68 -115
  42. package/dist/wire/types.js +68 -9
  43. package/package.json +2 -2
  44. 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 you can use an
38
- `Authorization: Bearer` header instead by supplying a socket that can set headers, e.g. the
39
- [`ws`](https://www.npmjs.com/package/ws) package (install it separately — it is not a
40
- dependency of this SDK):
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 WebSocket from "ws";
43
+ import { createNodeClient } from "@laplace.live/persona-sdk";
44
44
 
45
- const persona = new PersonaClient({
46
- token,
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
@@ -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, SCENE_ACTIVATION_TIMEOUT_MS, } from "../wire/protocol.js";
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 { renameLegacyKinds } from "./legacy-kinds.js";
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 = renameLegacyKinds(msg.event, msg.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(renameLegacyKinds(p.method, msg.result));
374
+ p.resolve(renameLegacyNames(p.method, msg.result));
376
375
  else
377
376
  p.reject(new PersonaApiError(msg.code, msg.message));
378
377
  }
@@ -0,0 +1,2 @@
1
+ /** A method result or event payload with the old names an older host still sends renamed to current ones. */
2
+ export declare function renameLegacyNames(name: string, data: unknown): unknown;
@@ -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
- /** 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) {
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
- /** The rig author's own name for this binding, when their `.vtube.json` carried one. */
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
- display: {
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.key === key)
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
- apply(entry);
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
- apply(entry);
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. */