@laplace.live/persona-sdk 1.21.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.
@@ -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
- this.pending.set(id, { resolve: resolve, reject, timer });
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(msg.data);
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,2 @@
1
+ /** A method result or event payload with the action kinds an older host still sends renamed to current ones. */
2
+ export declare function renameLegacyKinds(name: string, data: unknown): unknown;
@@ -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';
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";
@@ -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
+ }
@@ -1,4 +1,4 @@
1
- import type { AutomationBoundaryKind, AutomationInfo } from '../wire/types.ts';
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 isAutomationBoundary(kind: string | undefined): kind is AutomationBoundaryKind;
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. */
@@ -1,4 +1,4 @@
1
- import { AUTOMATION_BOUNDARY_KINDS } from "../wire/types.js";
1
+ import { AUTOMATION_SETUP_KINDS } from "../wire/types.js";
2
2
  import { isOneOf } from "./guards.js";
3
3
  /** Last path segment, tolerating both `/` and `\` separators. */
4
4
  export function fileBasename(path) {
@@ -28,21 +28,27 @@ export function motionSlots(files) {
28
28
  // English fallbacks; localizing clients keep their own map and use these for unknown kinds.
29
29
  const AUTOMATION_ACTION_KIND_LABELS = {
30
30
  'effect-toggle': 'Toggle Effect',
31
- 'effect-params': 'Effect Settings',
32
- 'effect-clip': 'Effect Clip',
31
+ 'effect-params': 'Apply Effect Settings',
32
+ 'effect-clip': 'Temporary Effect',
33
33
  'camera-pose': 'Camera Position',
34
- 'reset-camera': 'Reset Camera',
34
+ 'camera-reset': 'Reset Camera',
35
35
  'layer-visibility': 'Layer Visibility',
36
+ 'text-set': 'Set Text',
37
+ 'lyrics-play': 'Lyrics',
36
38
  'stream-mode': 'Stream Mode',
37
- 'switch-scene': 'Switch Scene',
38
- 'toggle-expression': 'Toggle Expression',
39
- 'play-motion': 'Play Motion',
40
- 'play-audio': 'Play Audio',
39
+ 'scene-switch': 'Switch Scene',
40
+ 'expression-toggle': 'Toggle Expression',
41
+ 'motion-play': 'Play Motion',
42
+ 'audio-play': 'Play Audio',
41
43
  'audio-control': 'Control Audio',
42
- 'play-camera-motion': 'Play Camera Motion',
43
- 'stop-camera-motion': 'Stop Camera Motion',
44
- 'remove-all-expressions': 'Clear All Expressions',
45
- 'load-model': 'Swap Model',
44
+ 'camera-motion-play': 'Play Camera Motion',
45
+ 'camera-motion-stop': 'Stop Camera Motion',
46
+ 'camera-follow': 'Camera Follow',
47
+ 'camera-aim': 'Camera Aim',
48
+ 'camera-focus': 'Camera Focus',
49
+ 'camera-handheld': 'Handheld Movement',
50
+ 'expression-clear': 'Clear All Expressions',
51
+ 'model-load': 'Swap Model',
46
52
  'model-position': 'Model Position',
47
53
  };
48
54
  /** English label for one automation action kind; kinds newer than this SDK read `Action`. */
@@ -50,9 +56,11 @@ export function automationActionLabel(kind) {
50
56
  return AUTOMATION_ACTION_KIND_LABELS[kind] ?? 'Action';
51
57
  }
52
58
  /** True for an action that prepares the scene before playback. */
53
- export function isAutomationBoundary(kind) {
54
- return isOneOf(kind, AUTOMATION_BOUNDARY_KINDS);
59
+ export function isAutomationSetup(kind) {
60
+ return isOneOf(kind, AUTOMATION_SETUP_KINDS);
55
61
  }
62
+ /** @deprecated Use `isAutomationSetup`. */
63
+ export const isAutomationBoundary = isAutomationSetup;
56
64
  /** Join action labels without implying timing that the client summary does not carry. */
57
65
  export function joinAutomationActionLabels(labels, kinds) {
58
66
  if (labels.length !== kinds.length)
@@ -1,4 +1,4 @@
1
- import type { AssetRef, Attach, AttachDepth, EnvironmentLook, ModelFormat, MToonTuning, ObjectContent, ObjectSpace, SceneEnvironment, SceneLight, SceneLightCameraFollowOptions, SceneLightType, ScreenPlacement, VrmPlacement } from '../wire/types.ts';
1
+ import type { AssetRef, Attach, AttachDepth, CameraAnchor, CameraFollowDamping, CameraTrackTarget, EnvironmentLook, ModelFormat, MToonTuning, ObjectContent, ObjectSpace, SceneCameraAim, SceneCameraAimSettings, SceneCameraFocus, SceneCameraFocusSettings, SceneCameraFollow, SceneCameraFollowSettings, SceneCameraHandheld, SceneEnvironment, SceneLight, SceneLightCameraFollowOptions, SceneLightType, ScreenPlacement, VrmPlacement } from '../wire/types.ts';
2
2
  export declare function clamp(v: number, min: number, max: number): number;
3
3
  /** Clamp to the unit interval. */
4
4
  export declare function clamp01(v: number): number;
@@ -63,6 +63,66 @@ export declare const SCENE_FOV_MAX = 90;
63
63
  /** Nonsingular authored field of view in degrees, including every valid whole-degree VMD key. */
64
64
  export declare const CAMERA_FOV_MIN = 1;
65
65
  export declare const CAMERA_FOV_MAX = 179;
66
+ /** Ceiling for each follow and aim damping, in seconds. */
67
+ export declare const CAMERA_DAMPING_MAX = 10;
68
+ /** Ceiling for {@link SceneCameraAim}'s lookahead time, in seconds, and its smoothing. */
69
+ export declare const CAMERA_AIM_LOOKAHEAD_MAX = 1;
70
+ export declare const CAMERA_AIM_SMOOTHING_MAX = 30;
71
+ /** Ceiling for {@link SceneCameraHandheld}'s speed. */
72
+ export declare const CAMERA_HANDHELD_SPEED_MAX = 2;
73
+ /** Warudo's Handheld Movement defaults, switched off. */
74
+ export declare const DEFAULT_CAMERA_HANDHELD: SceneCameraHandheld;
75
+ /**
76
+ * A fresh follow's damping: Warudo's second of position lag, and half a second on each turn so a
77
+ * tracked head's jitter never shakes the view.
78
+ */
79
+ export declare const DEFAULT_CAMERA_FOLLOW_DAMPING: CameraFollowDamping;
80
+ /** Where a fresh follow, aim or focus tracks a model; an object is always tracked by its root. */
81
+ export declare const DEFAULT_CAMERA_ANCHOR: CameraAnchor;
82
+ /** A fresh follow's settings: position only. */
83
+ export declare function defaultCameraFollowSettings(): SceneCameraFollowSettings;
84
+ /** A fresh follow of `target`, position only. */
85
+ export declare function defaultCameraFollow(target: CameraTrackTarget): SceneCameraFollow;
86
+ /** A fresh aim's settings, Warudo's Composer defaults: centred, no dead zone, 0.8 soft zone. */
87
+ export declare function defaultCameraAimSettings(): SceneCameraAimSettings;
88
+ /** A fresh aim at `target`. */
89
+ export declare function defaultCameraAim(target: CameraTrackTarget): SceneCameraAim;
90
+ /** A fresh focus's settings, refocusing about as fast as Warudo's default focusing speed. */
91
+ export declare function defaultCameraFocusSettings(): SceneCameraFocusSettings;
92
+ /** A fresh focus on `target`. */
93
+ export declare function defaultCameraFocus(target: CameraTrackTarget): SceneCameraFocus;
94
+ /** A follow's, aim's or focus's settings alone, without the layer it names. */
95
+ export declare function cameraTrackSettingsOf<T extends CameraTrackTarget>(track: T): Omit<T, 'instanceId' | 'target'>;
96
+ /** The instance `target` tracks in a scene whose primary model is `primaryInstanceId`. */
97
+ export declare function cameraTrackInstanceId(target: CameraTrackTarget, primaryInstanceId: string | null): string | null;
98
+ /** How a camera can track `item`: by any anchor on a VRM model, by the root of a 3D object, or not at all. */
99
+ export declare function cameraTrackKind(item: {
100
+ kind: 'model';
101
+ ref: {
102
+ kind: string;
103
+ };
104
+ } | {
105
+ kind: 'object';
106
+ space: ObjectSpace;
107
+ }): 'model' | 'object' | null;
108
+ /** Rows for the camera's follow, aim and focus pickers, in layer order. One rule, or the console offers a layer the desktop drops. */
109
+ export declare function cameraTrackCandidates(items: readonly ({
110
+ kind: 'model';
111
+ instanceId: string;
112
+ ref: {
113
+ kind: string;
114
+ name: string;
115
+ };
116
+ } | {
117
+ kind: 'object';
118
+ instanceId: string;
119
+ name: string;
120
+ space: ObjectSpace;
121
+ })[]): {
122
+ instanceId: string;
123
+ name: string;
124
+ model: boolean;
125
+ }[];
66
126
  /** Manual navigation stops short of ±90°; authored camera elevations remain unrestricted. */
67
127
  export declare const ELEVATION_LIMIT: number;
68
128
  /** Manual navigation distance bounds; authored cameras retain signed, unrestricted distances. */
@@ -172,9 +232,32 @@ export declare const WEB_SIZE_MAX = 7680;
172
232
  /** Web object paint-rate bounds, frames per second. */
173
233
  export declare const WEB_FPS_MIN = 1;
174
234
  export declare const WEB_FPS_MAX = 60;
235
+ /** Text object bounds: characters, em size in px, and the box and paint extents in px. */
236
+ export declare const TEXT_LENGTH_MAX = 10000;
237
+ export declare const TEXT_SIZE_MIN = 4;
238
+ export declare const TEXT_SIZE_MAX = 1000;
239
+ export declare const TEXT_SHEAR_MAX = 1;
240
+ /** Letter and word spacing, em. */
241
+ export declare const TEXT_SPACING_MIN = -0.5;
242
+ export declare const TEXT_SPACING_MAX = 2;
243
+ export declare const TEXT_LINE_HEIGHT_MIN = 0.5;
244
+ export declare const TEXT_LINE_HEIGHT_MAX = 4;
245
+ export declare const TEXT_BOX_MAX = 7680;
246
+ export declare const TEXT_STROKE_MAX = 100;
247
+ export declare const TEXT_SHADOW_OFFSET_MAX = 200;
248
+ export declare const TEXT_SHADOW_BLUR_MAX = 100;
249
+ export declare const TEXT_PLATE_PADDING_MAX = 500;
250
+ export declare const TEXT_PLATE_RADIUS_MAX = 500;
251
+ /** 3D text: extrusion and bevel in em, bevel segments, and emission strength. */
252
+ export declare const TEXT_EXTRUDE_MAX = 2;
253
+ export declare const TEXT_BEVEL_MAX = 0.2;
254
+ export declare const TEXT_BEVEL_RESOLUTION_MAX = 8;
255
+ export declare const TEXT_EMISSION_MAX = 4;
256
+ /** A PostScript name: printable ASCII, at most 63 chars, minus the quote and backslash that would escape `local("…")`. */
257
+ export declare const TEXT_POSTSCRIPT_NAME_RE: RegExp;
175
258
  /** A prop is a mesh, so it only exists in the three.js scene; every other kind renders in both. */
176
259
  export declare function objectSupportsSpace(kind: ObjectContent['kind'], space: ObjectSpace): boolean;
177
- /** The asset an object streams from, or null for kinds that carry their source inline (web, capture). */
260
+ /** The asset an object streams from, or null for kinds that carry their source inline (web, capture, text). */
178
261
  export declare function contentAssetId(content: ObjectContent): string | null;
179
262
  /**
180
263
  * The one model format a space's objects can ride: the screen layer always composites
@@ -233,9 +316,12 @@ export declare function pinEntersStack(item: {
233
316
  export declare function depthStops(meshes: readonly string[]): AttachDepth[];
234
317
  /** Which stop a depth sits at; an ArtMesh the host no longer has reads as the frontmost. */
235
318
  export declare function depthStopIndex(meshes: readonly string[], depth: AttachDepth): number;
236
- /** A fresh pin onto `parentInstanceId`: root anchor, in front of the model, no response tuning. */
237
- export declare function defaultAttach(parentInstanceId: string): Attach;
238
319
  export declare const DEFAULT_HEAD_ANGLE: NonNullable<Attach['headAngle']>;
320
+ /**
321
+ * A fresh pin onto `parentInstanceId`: root anchor, in front of the model. A screen-space item (a
322
+ * Live2D model, a 2D object) follows the host's head at the default response; others take none.
323
+ */
324
+ export declare function defaultAttach(parentInstanceId: string, screenSpace?: boolean): Attach;
239
325
  export declare const ATTACH_MULTIPLIER_MIN = -2;
240
326
  export declare const ATTACH_MULTIPLIER_MAX = 2;
241
327
  /** Parallax slider ceiling, symmetric: stage px at a full head turn. */
@@ -94,6 +94,78 @@ export const SCENE_FOV_MAX = 90;
94
94
  /** Nonsingular authored field of view in degrees, including every valid whole-degree VMD key. */
95
95
  export const CAMERA_FOV_MIN = 1;
96
96
  export const CAMERA_FOV_MAX = 179;
97
+ /** Ceiling for each follow and aim damping, in seconds. */
98
+ export const CAMERA_DAMPING_MAX = 10;
99
+ /** Ceiling for {@link SceneCameraAim}'s lookahead time, in seconds, and its smoothing. */
100
+ export const CAMERA_AIM_LOOKAHEAD_MAX = 1;
101
+ export const CAMERA_AIM_SMOOTHING_MAX = 30;
102
+ /** Ceiling for {@link SceneCameraHandheld}'s speed. */
103
+ export const CAMERA_HANDHELD_SPEED_MAX = 2;
104
+ /** Warudo's Handheld Movement defaults, switched off. */
105
+ export const DEFAULT_CAMERA_HANDHELD = { enabled: false, intensity: 0.5, speed: 1 };
106
+ /**
107
+ * A fresh follow's damping: Warudo's second of position lag, and half a second on each turn so a
108
+ * tracked head's jitter never shakes the view.
109
+ */
110
+ export const DEFAULT_CAMERA_FOLLOW_DAMPING = { x: 1, y: 1, z: 1, yaw: 0.5, pitch: 0.5, roll: 0.5 };
111
+ /** Where a fresh follow, aim or focus tracks a model; an object is always tracked by its root. */
112
+ export const DEFAULT_CAMERA_ANCHOR = { kind: 'bone', bone: 'head' };
113
+ /** A fresh follow's settings: position only. */
114
+ export function defaultCameraFollowSettings() {
115
+ return { anchor: DEFAULT_CAMERA_ANCHOR, binding: 'world', damping: { ...DEFAULT_CAMERA_FOLLOW_DAMPING } };
116
+ }
117
+ /** A fresh follow of `target`, position only. */
118
+ export function defaultCameraFollow(target) {
119
+ return { ...target, ...defaultCameraFollowSettings() };
120
+ }
121
+ /** A fresh aim's settings, Warudo's Composer defaults: centred, no dead zone, 0.8 soft zone. */
122
+ export function defaultCameraAimSettings() {
123
+ return {
124
+ anchor: DEFAULT_CAMERA_ANCHOR,
125
+ screenX: 0.5,
126
+ screenY: 0.5,
127
+ deadZone: { width: 0, height: 0 },
128
+ softZone: { width: 0.8, height: 0.8 },
129
+ bias: { x: 0, y: 0 },
130
+ damping: { horizontal: 0.5, vertical: 0.5 },
131
+ lookahead: { time: 0, smoothing: 0.5, ignoreY: false },
132
+ };
133
+ }
134
+ /** A fresh aim at `target`. */
135
+ export function defaultCameraAim(target) {
136
+ return { ...target, ...defaultCameraAimSettings() };
137
+ }
138
+ /** A fresh focus's settings, refocusing about as fast as Warudo's default focusing speed. */
139
+ export function defaultCameraFocusSettings() {
140
+ return { anchor: DEFAULT_CAMERA_ANCHOR, damping: 0.2 };
141
+ }
142
+ /** A fresh focus on `target`. */
143
+ export function defaultCameraFocus(target) {
144
+ return { ...target, ...defaultCameraFocusSettings() };
145
+ }
146
+ /** A follow's, aim's or focus's settings alone, without the layer it names. */
147
+ export function cameraTrackSettingsOf(track) {
148
+ const { instanceId: _instanceId, target: _target, ...settings } = track;
149
+ return settings;
150
+ }
151
+ /** The instance `target` tracks in a scene whose primary model is `primaryInstanceId`. */
152
+ export function cameraTrackInstanceId(target, primaryInstanceId) {
153
+ return target.target === 'primary' ? primaryInstanceId : target.instanceId;
154
+ }
155
+ /** How a camera can track `item`: by any anchor on a VRM model, by the root of a 3D object, or not at all. */
156
+ export function cameraTrackKind(item) {
157
+ if (item.kind === 'model')
158
+ return item.ref.kind === 'vrm' ? 'model' : null;
159
+ return item.space === '3d' ? 'object' : null;
160
+ }
161
+ /** Rows for the camera's follow, aim and focus pickers, in layer order. One rule, or the console offers a layer the desktop drops. */
162
+ export function cameraTrackCandidates(items) {
163
+ return items.flatMap(item => {
164
+ const kind = cameraTrackKind(item);
165
+ const name = item.kind === 'model' ? item.ref.name : item.name;
166
+ return kind ? [{ instanceId: item.instanceId, name, model: kind === 'model' }] : [];
167
+ });
168
+ }
97
169
  /** Manual navigation stops short of ±90°; authored camera elevations remain unrestricted. */
98
170
  export const ELEVATION_LIMIT = (80 * Math.PI) / 180;
99
171
  /** Manual navigation distance bounds; authored cameras retain signed, unrestricted distances. */
@@ -292,11 +364,34 @@ export const WEB_SIZE_MAX = 7680;
292
364
  /** Web object paint-rate bounds, frames per second. */
293
365
  export const WEB_FPS_MIN = 1;
294
366
  export const WEB_FPS_MAX = 60;
367
+ /** Text object bounds: characters, em size in px, and the box and paint extents in px. */
368
+ export const TEXT_LENGTH_MAX = 10_000;
369
+ export const TEXT_SIZE_MIN = 4;
370
+ export const TEXT_SIZE_MAX = 1000;
371
+ export const TEXT_SHEAR_MAX = 1;
372
+ /** Letter and word spacing, em. */
373
+ export const TEXT_SPACING_MIN = -0.5;
374
+ export const TEXT_SPACING_MAX = 2;
375
+ export const TEXT_LINE_HEIGHT_MIN = 0.5;
376
+ export const TEXT_LINE_HEIGHT_MAX = 4;
377
+ export const TEXT_BOX_MAX = 7680;
378
+ export const TEXT_STROKE_MAX = 100;
379
+ export const TEXT_SHADOW_OFFSET_MAX = 200;
380
+ export const TEXT_SHADOW_BLUR_MAX = 100;
381
+ export const TEXT_PLATE_PADDING_MAX = 500;
382
+ export const TEXT_PLATE_RADIUS_MAX = 500;
383
+ /** 3D text: extrusion and bevel in em, bevel segments, and emission strength. */
384
+ export const TEXT_EXTRUDE_MAX = 2;
385
+ export const TEXT_BEVEL_MAX = 0.2;
386
+ export const TEXT_BEVEL_RESOLUTION_MAX = 8;
387
+ export const TEXT_EMISSION_MAX = 4;
388
+ /** A PostScript name: printable ASCII, at most 63 chars, minus the quote and backslash that would escape `local("…")`. */
389
+ export const TEXT_POSTSCRIPT_NAME_RE = /^[\x21\x23-\x5b\x5d-\x7e]{1,63}$/;
295
390
  /** A prop is a mesh, so it only exists in the three.js scene; every other kind renders in both. */
296
391
  export function objectSupportsSpace(kind, space) {
297
392
  return kind === 'prop' ? space === '3d' : true;
298
393
  }
299
- /** The asset an object streams from, or null for kinds that carry their source inline (web, capture). */
394
+ /** The asset an object streams from, or null for kinds that carry their source inline (web, capture, text). */
300
395
  export function contentAssetId(content) {
301
396
  return content.kind === 'image' || content.kind === 'video' || content.kind === 'prop' ? content.assetId : null;
302
397
  }
@@ -367,24 +462,27 @@ export function depthStopIndex(meshes, depth) {
367
462
  const at = meshes.indexOf(depth.id);
368
463
  return at < 0 ? meshes.length + 1 : at + 1;
369
464
  }
370
- /** A fresh pin onto `parentInstanceId`: root anchor, in front of the model, no response tuning. */
371
- export function defaultAttach(parentInstanceId) {
465
+ export const DEFAULT_HEAD_ANGLE = {
466
+ multiplier: 1,
467
+ parallaxX: 0,
468
+ parallaxY: 0,
469
+ smoothing: 15,
470
+ };
471
+ /**
472
+ * A fresh pin onto `parentInstanceId`: root anchor, in front of the model. A screen-space item (a
473
+ * Live2D model, a 2D object) follows the host's head at the default response; others take none.
474
+ */
475
+ export function defaultAttach(parentInstanceId, screenSpace = false) {
372
476
  return {
373
477
  parentInstanceId,
374
478
  anchor: { kind: 'root' },
375
479
  followRotation: true,
376
480
  depth: { kind: 'front' },
377
481
  split: null,
378
- headAngle: null,
482
+ headAngle: screenSpace ? { ...DEFAULT_HEAD_ANGLE } : null,
379
483
  elasticity: null,
380
484
  };
381
485
  }
382
- export const DEFAULT_HEAD_ANGLE = {
383
- multiplier: 1,
384
- parallaxX: 0,
385
- parallaxY: 0,
386
- smoothing: 15,
387
- };
388
486
  export const ATTACH_MULTIPLIER_MIN = -2;
389
487
  export const ATTACH_MULTIPLIER_MAX = 2;
390
488
  /** Parallax slider ceiling, symmetric: stage px at a full head turn. */
@@ -19,6 +19,11 @@ export interface SceneAssetInfo {
19
19
  extensions: string[] | null;
20
20
  mediaSize: ObjectMediaSize | null;
21
21
  }
22
+ /** One scene object's loaded metadata. */
23
+ export interface SceneObjectInfo extends SceneAssetInfo {
24
+ /** A text object's transient text, null while it shows the saved one; absent on other kinds and older hosts. */
25
+ liveText?: string | null;
26
+ }
22
27
  /** Metadata for the active scene, tagged so a response from a previous scene can be discarded. */
23
28
  export interface SceneInspection {
24
29
  sceneId: string;
@@ -26,5 +31,5 @@ export interface SceneInspection {
26
31
  /** The set's own bounding radius before placement scale, for `cameraDistanceLimits`; omitted by older hosts. */
27
32
  radius?: number | null;
28
33
  };
29
- objects: Record<string, SceneAssetInfo>;
34
+ objects: Record<string, SceneObjectInfo>;
30
35
  }
@@ -30,6 +30,14 @@ export interface EventMap {
30
30
  instanceId: string;
31
31
  format: ModelFormat;
32
32
  };
33
+ /**
34
+ * An instance's motion or expression list changed without a reload — a recorded motion, an edited
35
+ * expression file, or a registered clip a VRM lists: re-run `motion.list` or `expression.list`.
36
+ */
37
+ 'instance.listing': {
38
+ instanceId: string;
39
+ listing: 'motions' | 'expressions';
40
+ };
33
41
  /** `weights` rides along on instances with `expression-weights`; a drag is not echoed (see `expression.setWeight`). */
34
42
  'expression.changed': {
35
43
  instanceId: string;
@@ -8,6 +8,7 @@ export const EVENT_NAMES = Object.keys({
8
8
  'automation.state': true,
9
9
  'tracking.status': true,
10
10
  'instance.loaded': true,
11
+ 'instance.listing': true,
11
12
  'expression.changed': true,
12
13
  'motion.started': true,
13
14
  'motion.ended': true,
@@ -5,7 +5,7 @@ import type { LipSyncCalibrationCommand, LipSyncConfig, LipSyncMode, LipSyncStat
5
5
  import type { ModelMovementConfig, ModelMovementEdit } from '../values/model-movement.ts';
6
6
  import type { SceneInspection } from '../values/stage-info.ts';
7
7
  import type { EventName } from './events.ts';
8
- import type { AnchorOption, AppCapability, AssetKind, AssetRef, Attach, AttachHeadAngle, AutomationInfo, BindingInput, Expression, ExpressionPersistence, HandTrackingMode, HotkeyConfig, HotkeyState, InjectEntry, InjectTarget, InstanceRuntime, JsonValue, LayerEffectKey, LayerEffects, LayerEffectsPatch, MediaPipeConfig, ModelInfo, ModelRef, MotionGroup, MToonTuning, ObjectContent, ObjectLightOverride, ObjectSpace, Place2D, Place3D, PlayingMotion, PoseSourceId, PoseStatus, Scene, SceneItem, SceneLight, SceneLightCameraFollowOptions, ScenePatch, SceneState, ScreenPlacement, Settings, SettingsPatch, TrackingLostBehavior, TrackingSourceConfig, TrackingSourceId, TrackingSourceKind, TrackingStatus, VrmPlacement } from './types.ts';
8
+ import type { AnchorOption, AppCapability, AssetKind, AssetRef, Attach, AttachHeadAngle, AutomationInfo, BindingInput, Expression, ExpressionPersistence, FontFamilyInfo, HandTrackingMode, HotkeyConfig, HotkeyState, InjectEntry, InjectTarget, InstanceRuntime, JsonValue, LayerEffectKey, LayerEffects, LayerEffectsPatch, MediaPipeConfig, ModelInfo, ModelRef, MotionGroup, MToonTuning, ObjectContent, ObjectLightOverride, ObjectSpace, Place2D, Place3D, PlayingMotion, PoseSourceId, PoseStatus, Scene, SceneItem, SceneLight, SceneLightCameraFollowOptions, ScenePatch, SceneState, ScreenPlacement, Settings, SettingsPatch, TrackingLostBehavior, TrackingSourceConfig, TrackingSourceId, TrackingSourceKind, TrackingStatus, VrmPlacement } from './types.ts';
9
9
  /** Marker for methods that take no parameters; the client lets you omit the argument. */
10
10
  export type EmptyRequest = Record<never, never>;
11
11
  /** Marker for methods whose success response carries no data. */
@@ -215,6 +215,15 @@ export interface ObjectSetContentRequest {
215
215
  content: ObjectContent;
216
216
  }
217
217
  export type ObjectSetContentResponse = EmptyResponse;
218
+ /** A `text` object's text alone, under `text-objects`. */
219
+ export interface ObjectSetTextRequest {
220
+ instanceId: string;
221
+ /** Null drops a transient text, showing the saved one again. */
222
+ text: string | null;
223
+ /** Show it without saving: no `scenes.json` write and no undo step. */
224
+ transient?: boolean;
225
+ }
226
+ export type ObjectSetTextResponse = EmptyResponse;
218
227
  export interface ObjectSetSpaceRequest {
219
228
  instanceId: string;
220
229
  space: ObjectSpace;
@@ -361,6 +370,11 @@ export type AssetListRequest = EmptyRequest;
361
370
  export interface AssetListResponse {
362
371
  assets: AssetRef[];
363
372
  }
373
+ export type FontsListRequest = EmptyRequest;
374
+ /** The host machine's installed families — what a text object's `font` can name. */
375
+ export interface FontsListResponse {
376
+ families: FontFamilyInfo[];
377
+ }
364
378
  /** Register a model entry file under a user-configured plugin folder. */
365
379
  export interface ModelRegisterRequest {
366
380
  path: string;
@@ -887,6 +901,10 @@ export interface MethodMap {
887
901
  request: ObjectSetContentRequest;
888
902
  response: ObjectSetContentResponse;
889
903
  };
904
+ 'object.setText': {
905
+ request: ObjectSetTextRequest;
906
+ response: ObjectSetTextResponse;
907
+ };
890
908
  'object.setSpace': {
891
909
  request: ObjectSetSpaceRequest;
892
910
  response: ObjectSetSpaceResponse;
@@ -988,6 +1006,10 @@ export interface MethodMap {
988
1006
  request: AssetListRequest;
989
1007
  response: AssetListResponse;
990
1008
  };
1009
+ 'fonts.list': {
1010
+ request: FontsListRequest;
1011
+ response: FontsListResponse;
1012
+ };
991
1013
  'model.register': {
992
1014
  request: ModelRegisterRequest;
993
1015
  response: ModelRegisterResponse;
@@ -35,6 +35,7 @@ export const METHOD_NAMES = Object.keys({
35
35
  'object.addMany': true,
36
36
  'object.rename': true,
37
37
  'object.setContent': true,
38
+ 'object.setText': true,
38
39
  'object.setSpace': true,
39
40
  'object.setPlacement': true,
40
41
  'object.attach': true,
@@ -63,6 +64,7 @@ export const METHOD_NAMES = Object.keys({
63
64
  'model.getMovement': true,
64
65
  'model.setMovement': true,
65
66
  'asset.list': true,
67
+ 'fonts.list': true,
66
68
  'asset.register': true,
67
69
  'registry.thumbnail': true,
68
70
  'settings.get': true,
@@ -234,6 +234,7 @@ export declare const requestSchemas: {
234
234
  lut: "lut";
235
235
  animation: "animation";
236
236
  cameraMotion: "cameraMotion";
237
+ lyrics: "lyrics";
237
238
  }>>;
238
239
  }, z.core.$strip>;
239
240
  'settings.patch': z.ZodObject<{
@@ -329,9 +330,9 @@ export declare const requestSchemas: {
329
330
  ttlMs: z.ZodOptional<z.ZodNumber>;
330
331
  transition: z.ZodOptional<z.ZodObject<{
331
332
  style: z.ZodOptional<z.ZodEnum<{
333
+ pop: "pop";
332
334
  glitch: "glitch";
333
335
  dither: "dither";
334
- pop: "pop";
335
336
  cut: "cut";
336
337
  }>>;
337
338
  inMs: z.ZodOptional<z.ZodNumber>;
@@ -17,7 +17,7 @@ export type ContentOrigin = 'bundled' | 'user';
17
17
  * ever an environment map, and a `.vmd` splits by content — `cameraMotion` for one that
18
18
  * frames a shot, `animation` for one that drives a rig.
19
19
  */
20
- export declare const ASSET_KINDS: readonly ["image", "video", "audio", "prop", "ibl", "lut", "animation", "cameraMotion"];
20
+ export declare const ASSET_KINDS: readonly ["image", "video", "audio", "prop", "ibl", "lut", "animation", "cameraMotion", "lyrics"];
21
21
  export type AssetKind = (typeof ASSET_KINDS)[number];
22
22
  /** Narrow a kind string to the set the asset registry owns — models and effects have their own. */
23
23
  export declare function isAssetKind(v: unknown): v is AssetKind;
@@ -48,6 +48,13 @@ export interface ContentRef {
48
48
  /** A model as the registry lists it. */
49
49
  export interface ModelRef extends ContentRef {
50
50
  kind: ModelFormat;
51
+ /** Entry file basename (`.model3.json`, `.vrm`), with no directory. Absent on older hosts. */
52
+ entryFile?: string;
53
+ /**
54
+ * `.vtube.json` basename, the name a VTS model-load hotkey stores (see `modelMatchesFile`); null
55
+ * without one. Only `model.list` fills it: absent on scene item refs, `model.register`'s answer, and older hosts.
56
+ */
57
+ vtubeFile?: string | null;
51
58
  }
52
59
  /** A registered object-source file. `exists` is false once the file is gone from disk. */
53
60
  export interface AssetRef extends ContentRef {
@@ -209,6 +216,29 @@ export interface SceneModelItem {
209
216
  export declare const OBJECT_SPACES: readonly ["2d", "3d"];
210
217
  export type ObjectSpace = (typeof OBJECT_SPACES)[number];
211
218
  export type CaptureKind = 'display' | 'window';
219
+ export declare const TEXT_ALIGNS: readonly ["left", "center", "right", "justify"];
220
+ export type TextAlign = (typeof TEXT_ALIGNS)[number];
221
+ /** `baseline` is the first line's baseline — Blender's Top Base-Line. */
222
+ export declare const TEXT_VERTICAL_ALIGNS: readonly ["top", "middle", "baseline", "bottom"];
223
+ export type TextVerticalAlign = (typeof TEXT_VERTICAL_ALIGNS)[number];
224
+ /** Blender's text-box overflow: spill past the box, shrink to fit it, or drop the lines that don't fit. */
225
+ export declare const TEXT_OVERFLOWS: readonly ["overflow", "scale", "truncate"];
226
+ export type TextOverflow = (typeof TEXT_OVERFLOWS)[number];
227
+ export declare const TEXT_STROKE_JOINS: readonly ["round", "miter", "bevel"];
228
+ export type TextStrokeJoin = (typeof TEXT_STROKE_JOINS)[number];
229
+ /**
230
+ * A system font face, named exactly: its PostScript name is the identity, since a family's
231
+ * nearest weight is not the face the user picked. The rest lets a machine without that face
232
+ * fall back to the closest one in the same family.
233
+ */
234
+ export interface TextFont {
235
+ postscriptName: string;
236
+ family: string;
237
+ weight: number;
238
+ italic: boolean;
239
+ /** CSS `font-stretch` percent; 100 is normal. */
240
+ stretch: number;
241
+ }
212
242
  /** What an object renders. Mirrors VTube Studio's items and Warudo's screen/prop assets. */
213
243
  export type ObjectContent = {
214
244
  kind: 'image';
@@ -219,6 +249,8 @@ export type ObjectContent = {
219
249
  loop: boolean;
220
250
  muted: boolean;
221
251
  volume: number;
252
+ /** Held rather than played while shown, through a restart too; the playback position is not saved. */
253
+ paused: boolean;
222
254
  } | {
223
255
  kind: 'prop';
224
256
  assetId: string;
@@ -242,11 +274,95 @@ export type ObjectContent = {
242
274
  source: CaptureKind;
243
275
  sourceId: string;
244
276
  label: string;
277
+ } | {
278
+ kind: 'text';
279
+ text: string;
280
+ /** null draws in the platform's default sans-serif. */
281
+ font: TextFont | null;
282
+ /** Em size, px; 3D reads px at the image quads' 1000 px per metre. */
283
+ size: number;
284
+ color: string;
285
+ /** Blender's shear: the slant as a fraction of height, -1..1. */
286
+ shear: number;
287
+ /** Em fractions added between characters and to each space. */
288
+ letterSpacing: number;
289
+ wordSpacing: number;
290
+ /** Line box as a multiple of `size`. */
291
+ lineHeight: number;
292
+ align: TextAlign;
293
+ verticalAlign: TextVerticalAlign;
294
+ /** Text box, px; 0 sizes that side to the text, and a set width wraps. */
295
+ boxWidth: number;
296
+ boxHeight: number;
297
+ overflow: TextOverflow;
298
+ /** Outside the glyphs, px; width 0 draws none. */
299
+ stroke: {
300
+ width: number;
301
+ color: string;
302
+ join: TextStrokeJoin;
303
+ };
304
+ /** px; opacity 0 draws none. */
305
+ shadow: {
306
+ color: string;
307
+ opacity: number;
308
+ offsetX: number;
309
+ offsetY: number;
310
+ blur: number;
311
+ };
312
+ /** A backing box around the text, px; opacity 0 draws none. */
313
+ plate: {
314
+ color: string;
315
+ opacity: number;
316
+ padding: number;
317
+ radius: number;
318
+ };
319
+ /** Karaoke: a lyrics clip paints what is sung in `color`, outlined in `strokeColor` (2D only). */
320
+ sung: {
321
+ color: string;
322
+ strokeColor: string;
323
+ };
324
+ /** 3D: extrusion depth and bevel as fractions of `size`; bevel segments. */
325
+ extrude: number;
326
+ bevel: number;
327
+ bevelResolution: number;
328
+ /** 3D surface: PBR roughness and metalness 0..1, and how strongly the text glows in its own colour. */
329
+ roughness: number;
330
+ metalness: number;
331
+ emission: number;
332
+ /** 3D: turn the text to face the camera every frame, ignoring the placement's rotation. */
333
+ faceCamera: boolean;
245
334
  };
335
+ /** The video variant of {@link ObjectContent}. */
336
+ export type VideoContent = Extract<ObjectContent, {
337
+ kind: 'video';
338
+ }>;
246
339
  /** The webpage variant of {@link ObjectContent}. */
247
340
  export type WebContent = Extract<ObjectContent, {
248
341
  kind: 'web';
249
342
  }>;
343
+ /** The text variant of {@link ObjectContent}. */
344
+ export type TextContent = Extract<ObjectContent, {
345
+ kind: 'text';
346
+ }>;
347
+ /** One face of a {@link FontFamilyInfo}. */
348
+ export interface FontFaceInfo {
349
+ postscriptName: string;
350
+ /** The subfamily as the font names it: `Semibold`, `W6`, `Bold Italic`. */
351
+ style: string;
352
+ weight: number;
353
+ italic: boolean;
354
+ /** CSS `font-stretch` percent. */
355
+ stretch: number;
356
+ /** A variable font's named instance. */
357
+ variable: boolean;
358
+ }
359
+ /** An installed font family and its faces. File paths never cross the wire. */
360
+ export interface FontFamilyInfo {
361
+ family: string;
362
+ /** Every name the family goes by, localized ones included, canonical first. */
363
+ names: string[];
364
+ faces: FontFaceInfo[];
365
+ }
250
366
  /** Where on a parent model an object rides. `root` is the model's own transform. */
251
367
  export type AttachAnchor = {
252
368
  kind: 'root';
@@ -365,6 +481,108 @@ export interface OrbitTransform {
365
481
  targetY: number;
366
482
  targetZ: number;
367
483
  }
484
+ /**
485
+ * How a following camera turns with its anchor — Warudo's Transposer binding modes. `world` follows
486
+ * position only; `yaw`, `yaw-pitch` and `full` also turn with it (Lock To Target With World Up, No Roll,
487
+ * and Lock To Target); `lazy` trails it, turning only as it passes (Simple Follow With World Up).
488
+ */
489
+ export declare const CAMERA_FOLLOW_BINDINGS: readonly ["world", "yaw", "yaw-pitch", "full", "lazy"];
490
+ export type CameraFollowBinding = (typeof CAMERA_FOLLOW_BINDINGS)[number];
491
+ /** Seconds a following camera takes to settle, 0 locking it: position along the view's own axes, then each turn. */
492
+ export interface CameraFollowDamping {
493
+ x: number;
494
+ y: number;
495
+ z: number;
496
+ yaw: number;
497
+ pitch: number;
498
+ roll: number;
499
+ }
500
+ /** The point a camera follows or aims at on a model: a humanoid bone, or its root. An object is tracked by its own origin. */
501
+ export type CameraAnchor = Extract<AttachAnchor, {
502
+ kind: 'root' | 'bone';
503
+ }>;
504
+ /**
505
+ * The layer a scene camera follows, aims at or focuses on: a VRM model or 3D object by id, or whichever model
506
+ * is primary as the scene changes. An id naming anything else heals to no tracking; a primary model that
507
+ * cannot be tracked holds the view.
508
+ */
509
+ export type CameraTrackTarget = {
510
+ target: 'primary';
511
+ instanceId?: never;
512
+ } | {
513
+ target?: never;
514
+ instanceId: string;
515
+ };
516
+ /** How a following camera tracks its layer, whichever layer that is. */
517
+ export interface SceneCameraFollowSettings {
518
+ /** Always the root on an object. */
519
+ anchor: CameraAnchor;
520
+ binding: CameraFollowBinding;
521
+ damping: CameraFollowDamping;
522
+ }
523
+ /**
524
+ * A scene camera following a layer. While set, `orbit` frames the layer as if its anchor rested at the stage
525
+ * origin, and the view moves with the anchor — placement, motion and tracking alike — without writing
526
+ * `orbit` back.
527
+ */
528
+ export type SceneCameraFollow = CameraTrackTarget & SceneCameraFollowSettings;
529
+ /**
530
+ * A scene camera turning to keep a layer at a spot on screen — Warudo's Composer. Screen values are
531
+ * fractions of the view: x from the left, y from the top.
532
+ */
533
+ export interface SceneCameraAimSettings {
534
+ /** Always the root on an object. */
535
+ anchor: CameraAnchor;
536
+ /** Where the anchor sits on screen, 0–1; 0.5 centres it. */
537
+ screenX: number;
538
+ screenY: number;
539
+ /** The camera holds still while the anchor stays inside this zone around its spot. */
540
+ deadZone: {
541
+ width: number;
542
+ height: number;
543
+ };
544
+ /** Past the dead zone the camera turns gradually; past this zone, at once. Never narrower than the dead zone. */
545
+ softZone: {
546
+ width: number;
547
+ height: number;
548
+ };
549
+ /** Shifts the soft zone off the spot, −0.5–0.5 of the slack between the zones. */
550
+ bias: {
551
+ x: number;
552
+ y: number;
553
+ };
554
+ /** Seconds to turn the anchor back into the dead zone, 0 turning at once. */
555
+ damping: {
556
+ horizontal: number;
557
+ vertical: number;
558
+ };
559
+ /**
560
+ * Aims `time` seconds (0–1, 0 off) ahead of a moving anchor, its velocity smoothed over `smoothing` (0–30);
561
+ * `ignoreY` leaves vertical motion out.
562
+ */
563
+ lookahead: {
564
+ time: number;
565
+ smoothing: number;
566
+ ignoreY: boolean;
567
+ };
568
+ }
569
+ export type SceneCameraAim = CameraTrackTarget & SceneCameraAimSettings;
570
+ /** A scene camera keeping a layer sharp under Depth of Field — Warudo's Focus Character. */
571
+ export interface SceneCameraFocusSettings {
572
+ /** Always the root on an object. */
573
+ anchor: CameraAnchor;
574
+ /** Seconds for the focus plane to catch up with the anchor, 0 at once. */
575
+ damping: number;
576
+ }
577
+ export type SceneCameraFocus = CameraTrackTarget & SceneCameraFocusSettings;
578
+ /** Noise-driven sway over the scene camera's own framing — Warudo's Handheld Movement. */
579
+ export interface SceneCameraHandheld {
580
+ enabled: boolean;
581
+ /** 0–1; the default 0.5 is a mild handheld. */
582
+ intensity: number;
583
+ /** 0–2; 1 sways at the authored pace. */
584
+ speed: number;
585
+ }
368
586
  /** Scene-level VRM camera. `orbit: null` = never framed — the first VRM load frames it from model height. */
369
587
  export interface SceneCamera {
370
588
  orbit: OrbitTransform | null;
@@ -379,6 +597,14 @@ export interface SceneCamera {
379
597
  * is set it overrides the framing every frame without writing it back.
380
598
  */
381
599
  clipAssetId: string | null;
600
+ /** The layer the view follows, or null. A camera motion overrides it. Absent on older hosts; gate on `camera-follow`. */
601
+ follow?: SceneCameraFollow | null;
602
+ /** The layer the view turns to keep on screen, or null. A camera motion overrides it. Absent on older hosts; gate on `camera-aim`. */
603
+ aim?: SceneCameraAim | null;
604
+ /** The layer Depth of Field keeps sharp, or null for the look-at point. Absent on older hosts; gate on `camera-focus`. */
605
+ focus?: SceneCameraFocus | null;
606
+ /** Handheld sway; a camera motion overrides it. Absent on older hosts; gate on `camera-handheld`. */
607
+ handheld?: SceneCameraHandheld;
382
608
  }
383
609
  /**
384
610
  * The slice of {@link SceneCamera} a saved pose snapshots and applies — a framing, never
@@ -1061,7 +1287,7 @@ export interface ScenePatch {
1061
1287
  * App-level features a client gates on (never version-sniff): `hello` and
1062
1288
  * `app.info` report them — the per-app mirror of {@link InstanceRuntime.capabilities}.
1063
1289
  */
1064
- export declare const APP_CAPABILITIES: readonly ["storage", "speech", "automations", "controllers", "model-editing", "asset-inspection", "layer-effects", "scene-transitions", "area-lights", "spot-lights", "camera-follow-lights", "shadow-filters", "environment-map-model", "spawn", "tracking-lost", "motion-stop", "stage-capture", "model-movement", "object-pin-depth"];
1290
+ export declare const APP_CAPABILITIES: readonly ["storage", "speech", "automations", "controllers", "model-editing", "asset-inspection", "layer-effects", "scene-transitions", "area-lights", "spot-lights", "camera-follow-lights", "shadow-filters", "environment-map-model", "spawn", "tracking-lost", "motion-stop", "stage-capture", "model-movement", "object-pin-depth", "text-objects", "camera-follow", "camera-aim", "camera-focus", "camera-handheld", "video-sound"];
1065
1291
  export type AppCapability = (typeof APP_CAPABILITIES)[number];
1066
1292
  export declare function isAppCapability(v: unknown): v is AppCapability;
1067
1293
  /** What a loaded model instance can do; absent capabilities answer `unsupported-for-format`. */
@@ -1130,7 +1356,7 @@ export interface Hotkey {
1130
1356
  name: string;
1131
1357
  /** The VTS `Action` string verbatim; `action` is null when Persona cannot run it. */
1132
1358
  vtsAction: string;
1133
- action: 'toggle-expression' | 'play-motion' | 'remove-all-expressions' | 'load-model' | null;
1359
+ action: 'expression-toggle' | 'motion-play' | 'expression-clear' | 'model-load' | null;
1134
1360
  file: string;
1135
1361
  accelerator: string | null;
1136
1362
  global: boolean;
@@ -1153,11 +1379,22 @@ export interface HotkeyState extends HotkeyConfig {
1153
1379
  registered: string[];
1154
1380
  }
1155
1381
  /** Action kinds an app automation can carry today; servers may send kinds newer than this list. */
1156
- export declare const AUTOMATION_ACTION_KINDS: readonly ["effect-toggle", "effect-params", "effect-clip", "camera-pose", "reset-camera", "play-camera-motion", "stop-camera-motion", "layer-visibility", "stream-mode", "switch-scene", "toggle-expression", "play-motion", "play-audio", "audio-control", "remove-all-expressions", "load-model", "model-position"];
1382
+ export declare const AUTOMATION_ACTION_KINDS: readonly ["effect-toggle", "effect-params", "effect-clip", "camera-pose", "camera-reset", "camera-motion-play", "camera-motion-stop", "camera-follow", "camera-aim", "camera-focus", "camera-handheld", "layer-visibility", "text-set", "lyrics-play", "stream-mode", "scene-switch", "expression-toggle", "motion-play", "audio-play", "audio-control", "expression-clear", "model-load", "model-position"];
1157
1383
  export type AutomationActionKind = (typeof AUTOMATION_ACTION_KINDS)[number];
1384
+ /**
1385
+ * Deprecated action kind names, each mapped to its category-first replacement. Older hosts send them in
1386
+ * `actionKinds` and `Hotkey.action`, and older files store them; the client renames them on receipt.
1387
+ */
1388
+ export declare const LEGACY_AUTOMATION_ACTION_KINDS: ReadonlyMap<string, AutomationActionKind>;
1389
+ /** The current name of an action kind; kinds that were never renamed, known or not, pass through. */
1390
+ export declare function canonicalActionKind(kind: string): string;
1158
1391
  /** Setup kinds that finish loading before the sequence clock starts. */
1159
- export declare const AUTOMATION_BOUNDARY_KINDS: readonly ["switch-scene", "load-model"];
1160
- export type AutomationBoundaryKind = (typeof AUTOMATION_BOUNDARY_KINDS)[number];
1392
+ export declare const AUTOMATION_SETUP_KINDS: readonly ["scene-switch", "model-load"];
1393
+ export type AutomationSetupKind = (typeof AUTOMATION_SETUP_KINDS)[number];
1394
+ /** @deprecated Use `AUTOMATION_SETUP_KINDS`. */
1395
+ export declare const AUTOMATION_BOUNDARY_KINDS: readonly ["scene-switch", "model-load"];
1396
+ /** @deprecated Use `AutomationSetupKind`. */
1397
+ export type AutomationBoundaryKind = AutomationSetupKind;
1161
1398
  /**
1162
1399
  * One app automation as clients see it. Action kinds and scene targets drive derived
1163
1400
  * labels; other action payloads stay app-side.
@@ -13,13 +13,35 @@ import { TIME_INPUT_NAMES, TIME_INPUT_RANGES } from "../values/time.js";
13
13
  * ever an environment map, and a `.vmd` splits by content — `cameraMotion` for one that
14
14
  * frames a shot, `animation` for one that drives a rig.
15
15
  */
16
- export const ASSET_KINDS = ['image', 'video', 'audio', 'prop', 'ibl', 'lut', 'animation', 'cameraMotion'];
16
+ export const ASSET_KINDS = [
17
+ 'image',
18
+ 'video',
19
+ 'audio',
20
+ 'prop',
21
+ 'ibl',
22
+ 'lut',
23
+ 'animation',
24
+ 'cameraMotion',
25
+ 'lyrics',
26
+ ];
17
27
  /** Narrow a kind string to the set the asset registry owns — models and effects have their own. */
18
28
  export function isAssetKind(v) {
19
29
  return isOneOf(v, ASSET_KINDS);
20
30
  }
21
31
  // ---- Objects -------------------------------------------------------------------
22
32
  export const OBJECT_SPACES = ['2d', '3d'];
33
+ export const TEXT_ALIGNS = ['left', 'center', 'right', 'justify'];
34
+ /** `baseline` is the first line's baseline — Blender's Top Base-Line. */
35
+ export const TEXT_VERTICAL_ALIGNS = ['top', 'middle', 'baseline', 'bottom'];
36
+ /** Blender's text-box overflow: spill past the box, shrink to fit it, or drop the lines that don't fit. */
37
+ export const TEXT_OVERFLOWS = ['overflow', 'scale', 'truncate'];
38
+ export const TEXT_STROKE_JOINS = ['round', 'miter', 'bevel'];
39
+ /**
40
+ * How a following camera turns with its anchor — Warudo's Transposer binding modes. `world` follows
41
+ * position only; `yaw`, `yaw-pitch` and `full` also turn with it (Lock To Target With World Up, No Roll,
42
+ * and Lock To Target); `lazy` trails it, turning only as it passes (Simple Follow With World Up).
43
+ */
44
+ export const CAMERA_FOLLOW_BINDINGS = ['world', 'yaw', 'yaw-pitch', 'full', 'lazy'];
23
45
  export const SCENE_LIGHT_TYPES = ['directional', 'point', 'ambient', 'area', 'spot'];
24
46
  /**
25
47
  * Per-light shadow tier. `off` is the old `castShadow: false`; the rest raise
@@ -61,6 +83,18 @@ export const APP_CAPABILITIES = [
61
83
  * one pinned to a Live2D model takes `attach.depth` in that model's stack, as a Live2D item does.
62
84
  */
63
85
  'object-pin-depth',
86
+ /** `text` object content and `fonts.list`. */
87
+ 'text-objects',
88
+ /** `SceneCamera.follow`. */
89
+ 'camera-follow',
90
+ /** `SceneCamera.aim`. */
91
+ 'camera-aim',
92
+ /** `SceneCamera.focus`. */
93
+ 'camera-focus',
94
+ /** `SceneCamera.handheld`. */
95
+ 'camera-handheld',
96
+ /** `object.setContent` edits a video's `loop`, `muted` and `volume` in place; its sound follows the output device. */
97
+ 'video-sound',
64
98
  ];
65
99
  export function isAppCapability(v) {
66
100
  return isOneOf(v, APP_CAPABILITIES);
@@ -72,25 +106,51 @@ export const AUTOMATION_ACTION_KINDS = [
72
106
  'effect-params',
73
107
  'effect-clip',
74
108
  'camera-pose',
75
- 'reset-camera',
76
- 'play-camera-motion',
77
- 'stop-camera-motion',
109
+ 'camera-reset',
110
+ 'camera-motion-play',
111
+ 'camera-motion-stop',
112
+ 'camera-follow',
113
+ 'camera-aim',
114
+ 'camera-focus',
115
+ 'camera-handheld',
78
116
  'layer-visibility',
117
+ 'text-set',
118
+ 'lyrics-play',
79
119
  'stream-mode',
80
- 'switch-scene',
81
- 'toggle-expression',
82
- 'play-motion',
83
- 'play-audio',
120
+ 'scene-switch',
121
+ 'expression-toggle',
122
+ 'motion-play',
123
+ 'audio-play',
84
124
  'audio-control',
85
- 'remove-all-expressions',
86
- 'load-model',
125
+ 'expression-clear',
126
+ 'model-load',
87
127
  'model-position',
88
128
  ];
129
+ /**
130
+ * Deprecated action kind names, each mapped to its category-first replacement. Older hosts send them in
131
+ * `actionKinds` and `Hotkey.action`, and older files store them; the client renames them on receipt.
132
+ */
133
+ export const LEGACY_AUTOMATION_ACTION_KINDS = new Map([
134
+ ['reset-camera', 'camera-reset'],
135
+ ['play-camera-motion', 'camera-motion-play'],
136
+ ['stop-camera-motion', 'camera-motion-stop'],
137
+ ['set-text', 'text-set'],
138
+ ['lyrics', 'lyrics-play'],
139
+ ['switch-scene', 'scene-switch'],
140
+ ['toggle-expression', 'expression-toggle'],
141
+ ['play-motion', 'motion-play'],
142
+ ['play-audio', 'audio-play'],
143
+ ['remove-all-expressions', 'expression-clear'],
144
+ ['load-model', 'model-load'],
145
+ ]);
146
+ /** The current name of an action kind; kinds that were never renamed, known or not, pass through. */
147
+ export function canonicalActionKind(kind) {
148
+ return LEGACY_AUTOMATION_ACTION_KINDS.get(kind) ?? kind;
149
+ }
89
150
  /** Setup kinds that finish loading before the sequence clock starts. */
90
- export const AUTOMATION_BOUNDARY_KINDS = [
91
- 'switch-scene',
92
- 'load-model',
93
- ];
151
+ export const AUTOMATION_SETUP_KINDS = ['scene-switch', 'model-load'];
152
+ /** @deprecated Use `AUTOMATION_SETUP_KINDS`. */
153
+ export const AUTOMATION_BOUNDARY_KINDS = AUTOMATION_SETUP_KINDS;
94
154
  // ---- Settings ------------------------------------------------------------------
95
155
  export const TRACKING_SOURCE_IDS = ['persona-ios', 'ifacialmocap', 'vts-ios'];
96
156
  /** Body-pose protocols. Every one so far is UDP with a configurable port. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@laplace.live/persona-sdk",
3
- "version": "1.21.0",
3
+ "version": "1.22.0",
4
4
  "description": "TypeScript SDK and wire schema for the LAPLACE Persona plugin API",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -28,7 +28,7 @@
28
28
  "devDependencies": {
29
29
  "rimraf": "^6.1.3",
30
30
  "typescript": "~6.0.3",
31
- "vitest": "^5.0.1"
31
+ "vitest": "^5.0.2"
32
32
  },
33
33
  "engines": {
34
34
  "node": ">=24"