@laplace.live/persona-sdk 1.24.0 → 1.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) 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/effect-schema.d.ts +26 -0
  14. package/dist/values/effect-schema.js +76 -91
  15. package/dist/values/hotkey-targets.d.ts +10 -3
  16. package/dist/values/hotkey-targets.js +20 -0
  17. package/dist/values/hotkeys.d.ts +9 -1
  18. package/dist/values/hotkeys.js +11 -0
  19. package/dist/values/labels.js +1 -0
  20. package/dist/values/limits.d.ts +96 -8
  21. package/dist/values/limits.js +109 -1
  22. package/dist/values/lipsync.d.ts +7 -8
  23. package/dist/values/locale.d.ts +4 -1
  24. package/dist/values/locale.js +4 -0
  25. package/dist/values/model-movement.d.ts +19 -4
  26. package/dist/values/model-movement.js +17 -4
  27. package/dist/values/volumetric-lighting.d.ts +1 -1
  28. package/dist/values/volumetric-lighting.js +1 -1
  29. package/dist/wire/envelope.js +6 -3
  30. package/dist/wire/methods.d.ts +16 -17
  31. package/dist/wire/protocol.d.ts +2 -0
  32. package/dist/wire/protocol.js +4 -0
  33. package/dist/wire/schemas/fields.d.ts +8 -0
  34. package/dist/wire/schemas/fields.js +1 -0
  35. package/dist/wire/schemas/requests.d.ts +2 -2
  36. package/dist/wire/schemas/requests.js +70 -20
  37. package/dist/wire/schemas/settings.d.ts +3 -3
  38. package/dist/wire/schemas/settings.js +3 -3
  39. package/dist/wire/types.d.ts +68 -115
  40. package/dist/wire/types.js +68 -9
  41. package/package.json +2 -2
  42. package/dist/client/legacy-kinds.d.ts +0 -2
@@ -1,4 +1,4 @@
1
- import type { Hotkey } from '../wire/types.ts';
1
+ import type { Hotkey, HotkeyState } from '../wire/types.ts';
2
2
  /** True when the combo carries a command modifier and so may be registered globally. */
3
3
  export declare function hasCommandModifier(accelerator: string | null): boolean;
4
4
  /**
@@ -9,6 +9,12 @@ export declare function hasCommandModifier(accelerator: string | null): boolean;
9
9
  export declare function canRunInBackground(accelerator: string | null): boolean;
10
10
  /** True when a model hotkey is meant to fire while Persona sits in the background — what main registers on, and what the globe shows. */
11
11
  export declare function hotkeyRunsInBackground(h: Hotkey): boolean;
12
+ /**
13
+ * True when main should hold an OS registration for `h` but doesn't — another app owns its
14
+ * command-modifier combo. A non-command combo (a controller one included) never registers, so its
15
+ * absence is not contention; nor is anything under the model's master switch turned off.
16
+ */
17
+ export declare function hotkeyContended(state: Pick<HotkeyState, 'keyboardEnabled' | 'registered'>, h: Hotkey): boolean;
12
18
  /**
13
19
  * Why a `hold` hotkey toggles instead of holding, or null when it can hold. A combo registered with
14
20
  * the OS reaches Persona as a press alone — Electron reports no key-up for it — and one registration
@@ -16,6 +22,8 @@ export declare function hotkeyRunsInBackground(h: Hotkey): boolean;
16
22
  * `'shared'` when only another one on its combo does, which `registered` (main's list) alone can tell.
17
23
  */
18
24
  export declare function holdBlockedBy(h: Pick<Hotkey, 'hold' | 'global' | 'accelerator'>, registered: boolean): 'own' | 'shared' | null;
25
+ /** A hotkey's playback options, which every writer and healer treats as one group. */
26
+ export type HotkeyOptions = Pick<Hotkey, 'hold' | 'fadeSeconds' | 'deactivateAfterSeconds' | 'stopsOnLastFrame'>;
19
27
  /** VTS's editor limits for a hotkey's fade and auto-off times, in seconds; healing clamps API input to them. */
20
28
  export declare const HOTKEY_FADE_SECONDS_MAX = 2;
21
29
  export declare const HOTKEY_DEACTIVATE_SECONDS_MIN = 0.1;
@@ -25,6 +25,17 @@ export function canRunInBackground(accelerator) {
25
25
  export function hotkeyRunsInBackground(h) {
26
26
  return h.global && h.active && h.action !== null && canRunInBackground(h.accelerator);
27
27
  }
28
+ /**
29
+ * True when main should hold an OS registration for `h` but doesn't — another app owns its
30
+ * command-modifier combo. A non-command combo (a controller one included) never registers, so its
31
+ * absence is not contention; nor is anything under the model's master switch turned off.
32
+ */
33
+ export function hotkeyContended(state, h) {
34
+ return (state.keyboardEnabled &&
35
+ hotkeyRunsInBackground(h) &&
36
+ hasCommandModifier(h.accelerator) &&
37
+ !state.registered.includes(h.id));
38
+ }
28
39
  /**
29
40
  * Why a `hold` hotkey toggles instead of holding, or null when it can hold. A combo registered with
30
41
  * the OS reaches Persona as a press alone — Electron reports no key-up for it — and one registration
@@ -26,6 +26,7 @@ export function motionSlots(files) {
26
26
  return files.flatMap((file, index) => (file === '' ? [] : [{ file, index }]));
27
27
  }
28
28
  // English fallbacks; localizing clients keep their own map and use these for unknown kinds.
29
+ // The panel's `KIND_SPECS` labels must read the same — a desktop test holds them together.
29
30
  const AUTOMATION_ACTION_KIND_LABELS = {
30
31
  'effect-toggle': 'Toggle Effect',
31
32
  'effect-params': 'Apply Effect Settings',
@@ -1,9 +1,11 @@
1
- import type { AssetRef, Attach, AttachDepth, CameraAnchor, CameraFollowDamping, CameraTrackTarget, ChromaKey, EnvironmentLook, KeyableContent, ModelFormat, MToonTuning, ObjectContent, ObjectSpace, SceneCameraAim, SceneCameraAimSettings, SceneCameraFocus, SceneCameraFocusSettings, SceneCameraFollow, SceneCameraFollowSettings, SceneCameraHandheld, SceneEnvironment, SceneLight, SceneLightCameraFollowOptions, SceneLightType, ScreenPlacement, VrmPlacement } from '../wire/types.ts';
1
+ import type { AssetRef, Attach, AttachAnchor, AttachDepth, CameraAnchor, CameraFollowDamping, CameraTrackTarget, ChromaKey, EnvironmentLook, KeyableContent, ModelFormat, MToonTuning, ObjectContent, ObjectSpace, SceneCameraAim, SceneCameraAimSettings, SceneCameraFocus, SceneCameraFocusSettings, SceneCameraFollow, SceneCameraFollowSettings, SceneCameraHandheld, SceneEnvironment, SceneLight, SceneLightCameraFollowOptions, SceneLightType, ScreenPlacement, TextFont, 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;
5
5
  /** Wrap an angle into (-π, π]. */
6
6
  export declare function wrapAngle(a: number): number;
7
+ /** Wrap degrees into (-180°, 180°], {@link wrapAngle}'s range; whole turns keep in-range values exact. */
8
+ export declare function wrapDegrees(v: number): number;
7
9
  export declare const DEFAULT_LIVE2D_PLACEMENT: ScreenPlacement;
8
10
  export declare const DEFAULT_VRM_PLACEMENT: VrmPlacement;
9
11
  /** Scene colors heal to 6-digit hex; both cases are spelled out because JSON Schema patterns carry no flags. */
@@ -27,6 +29,11 @@ export declare const SCENE_LIGHT_SPOT_ANGLE_MIN = 1;
27
29
  export declare const SCENE_LIGHT_SPOT_ANGLE_MAX = 89;
28
30
  export declare const DEFAULT_SPOT_ANGLE_DEG = 30;
29
31
  export declare const DEFAULT_SPOT_PENUMBRA = 0.3;
32
+ /** Light azimuth and roll bounds in degrees, symmetric: a half turn either way. */
33
+ export declare const SCENE_LIGHT_AZIMUTH_MAX = 180;
34
+ export declare const SCENE_LIGHT_ROLL_MAX = 180;
35
+ /** Light elevation bound in degrees, symmetric: straight up or straight down. */
36
+ export declare const SCENE_LIGHT_ELEVATION_MAX = 90;
30
37
  /** Positional lights need falloff headroom; the slider and scene healing share this ceiling. */
31
38
  export declare function sceneLightIntensityMax(type: SceneLightType): number;
32
39
  /** Whether a light of `type` aims with azimuth/elevation. */
@@ -41,6 +48,7 @@ export declare const LIVE2D_SCALE_MIN = 0.1;
41
48
  export declare const LIVE2D_SCALE_MAX = 24;
42
49
  export declare const VRM_SCALE_MIN = 0.05;
43
50
  export declare const VRM_SCALE_MAX = 10;
51
+ export declare const SCENE_LIGHT_RANGE_MIN = 0.1;
44
52
  export declare const SCENE_LIGHT_RANGE_MAX = 20;
45
53
  /** Past this the 5-tap kernel spreads thin enough that its dither reads as noise. */
46
54
  export declare const SCENE_LIGHT_SHADOW_RADIUS_MAX = 16;
@@ -48,6 +56,8 @@ export declare const SCENE_LIGHT_SHADOW_RADIUS_MAX = 16;
48
56
  export declare const SCENE_LIGHT_POINT_SHADOW_RADIUS_MAX = 32;
49
57
  export declare const SCENE_LIGHT_SHADOW_SOURCE_SIZE_MAX = 10;
50
58
  export declare const DEFAULT_LIGHT_SHADOW_SOURCE_SIZE = 0.1;
59
+ /** A light override's key: the baked light's traversal index, in decimal digits. */
60
+ export declare const LIGHT_OVERRIDE_KEY_RE: RegExp;
51
61
  /** Shadow softness (PCF disk radius, in shadow-map texels) ceiling for a light of `type`. */
52
62
  export declare function sceneLightShadowRadiusMax(type: SceneLightType): number;
53
63
  /**
@@ -68,6 +78,8 @@ export declare const CAMERA_DAMPING_MAX = 10;
68
78
  /** Ceiling for {@link SceneCameraAim}'s lookahead time, in seconds, and its smoothing. */
69
79
  export declare const CAMERA_AIM_LOOKAHEAD_MAX = 1;
70
80
  export declare const CAMERA_AIM_SMOOTHING_MAX = 30;
81
+ /** {@link SceneCameraAim}'s bias bound on each axis, symmetric: a share of the slack between its zones. */
82
+ export declare const CAMERA_AIM_BIAS_MAX = 0.5;
71
83
  /** Ceiling for {@link SceneCameraHandheld}'s speed. */
72
84
  export declare const CAMERA_HANDHELD_SPEED_MAX = 2;
73
85
  /** Warudo's Handheld Movement defaults, switched off. */
@@ -105,6 +117,30 @@ export declare function cameraTrackKind(item: {
105
117
  kind: 'object';
106
118
  space: ObjectSpace;
107
119
  }): 'model' | 'object' | null;
120
+ /**
121
+ * The layer `target` tracks in `scene` — the primary model for a primary pick, else the first model — when it
122
+ * is one a camera can track.
123
+ */
124
+ export declare function cameraTrackItem<T extends {
125
+ kind: 'model';
126
+ instanceId: string;
127
+ ref: {
128
+ kind: string;
129
+ };
130
+ } | {
131
+ kind: 'object';
132
+ instanceId: string;
133
+ space: ObjectSpace;
134
+ }>(scene: {
135
+ items: readonly T[];
136
+ primaryInstanceId: string | null;
137
+ }, target: CameraTrackTarget): T | undefined;
138
+ /** A layer the camera's follow, aim and focus pickers offer; `model` tells a model from an object. */
139
+ export interface CameraTrackCandidate {
140
+ instanceId: string;
141
+ name: string;
142
+ model: boolean;
143
+ }
108
144
  /** 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
145
  export declare function cameraTrackCandidates(items: readonly ({
110
146
  kind: 'model';
@@ -118,11 +154,7 @@ export declare function cameraTrackCandidates(items: readonly ({
118
154
  instanceId: string;
119
155
  name: string;
120
156
  space: ObjectSpace;
121
- })[]): {
122
- instanceId: string;
123
- name: string;
124
- model: boolean;
125
- }[];
157
+ })[]): CameraTrackCandidate[];
126
158
  /** Manual navigation stops short of ±90°; authored camera elevations remain unrestricted. */
127
159
  export declare const ELEVATION_LIMIT: number;
128
160
  /** Manual navigation distance bounds; authored cameras retain signed, unrestricted distances. */
@@ -153,6 +185,23 @@ export declare const MTOON_OUTLINE_WIDTH_MAX = 2;
153
185
  /** HDR headroom: past ×1 the point is pushing emissive parts over the bloom threshold. */
154
186
  export declare const MTOON_EMISSIVE_MAX = 4;
155
187
  export declare const MTOON_UV_ANIMATION_MAX = 4;
188
+ /** Each {@link MToonTuning} field's `[min, max]`: offsets −1..1, `shade` 0..1, multipliers 0 to their ceiling. */
189
+ export declare const MTOON_RANGES: {
190
+ readonly shade: readonly [0, 1];
191
+ readonly shadingShift: readonly [-1, 1];
192
+ readonly shadingToony: readonly [-1, 1];
193
+ readonly giEqualization: readonly [-1, 1];
194
+ readonly normalScale: readonly [0, 2];
195
+ readonly rim: readonly [0, 2];
196
+ readonly rimLift: readonly [-1, 1];
197
+ readonly rimFresnelPower: readonly [0, 4];
198
+ readonly rimLightingMix: readonly [-1, 1];
199
+ readonly matcap: readonly [0, 4];
200
+ readonly outlineWidth: readonly [0, 2];
201
+ readonly outlineLightingMix: readonly [-1, 1];
202
+ readonly emissive: readonly [0, 4];
203
+ readonly uvAnimation: readonly [0, 4];
204
+ };
156
205
  export declare function defaultMToonTuning(): MToonTuning;
157
206
  export declare const DEFAULT_LIGHT_AZIMUTH_DEG = 45;
158
207
  export declare const DEFAULT_LIGHT_ELEVATION_DEG: number;
@@ -262,8 +311,26 @@ export declare const TEXT_BEVEL_RESOLUTION_MAX = 8;
262
311
  export declare const TEXT_EMISSION_MAX = 4;
263
312
  /** A PostScript name: printable ASCII, at most 63 chars, minus the quote and backslash that would escape `local("…")`. */
264
313
  export declare const TEXT_POSTSCRIPT_NAME_RE: RegExp;
314
+ /** A text font's CSS `font-weight` bounds. */
315
+ export declare const TEXT_FONT_WEIGHT_MIN = 1;
316
+ export declare const TEXT_FONT_WEIGHT_MAX = 1000;
317
+ /** A text font's CSS `font-stretch` bounds, in percent. */
318
+ export declare const TEXT_FONT_STRETCH_MIN = 50;
319
+ export declare const TEXT_FONT_STRETCH_MAX = 200;
320
+ /** CSS's normal face: what a text font, or a font file, that leaves its weight, slope or width unsaid reads as. */
321
+ export declare const DEFAULT_TEXT_FONT_FACE: Pick<TextFont, 'weight' | 'italic' | 'stretch'>;
265
322
  /** A prop is a mesh, so it only exists in the three.js scene; every other kind renders in both. */
266
323
  export declare function objectSupportsSpace(kind: ObjectContent['kind'], space: ObjectSpace): boolean;
324
+ /** The {@link ObjectContent} kinds that stream from a registered asset; the rest carry their source inline. */
325
+ declare const ASSET_CONTENT_KINDS: readonly ["image", "video", "prop"];
326
+ /** The variants of {@link ObjectContent} that stream from a registered asset. */
327
+ export type AssetContent = Extract<ObjectContent, {
328
+ kind: (typeof ASSET_CONTENT_KINDS)[number];
329
+ }>;
330
+ /** Whether `kind` — a content kind, or an Inventory row's — is one whose asset streams into a layer. */
331
+ export declare function isAssetContentKind(kind: string): kind is AssetContent['kind'];
332
+ /** Whether the content streams from a registered asset: image, video and prop; the rest carry their source inline. */
333
+ export declare function isAssetContent(content: ObjectContent): content is AssetContent;
267
334
  /** The asset an object streams from, or null for kinds that carry their source inline (web, capture, text). */
268
335
  export declare function contentAssetId(content: ObjectContent): string | null;
269
336
  /**
@@ -281,6 +348,12 @@ export declare function pinRiders(items: readonly {
281
348
  parentInstanceId: string;
282
349
  } | null;
283
350
  }[], instanceId: string): Set<string>;
351
+ /** A pin picker row; `ridesThis` flags a model that already rides the item. */
352
+ interface PinnableParent {
353
+ instanceId: string;
354
+ name: string;
355
+ ridesThis: boolean;
356
+ }
284
357
  /**
285
358
  * Rows for the pin picker: every other Live2D model, each flagged when it already rides
286
359
  * `instanceId` — picking one would put the two in a circle, so both apps offer it disabled
@@ -297,10 +370,20 @@ export declare function pinnableParents(models: readonly {
297
370
  attach?: {
298
371
  parentInstanceId: string;
299
372
  } | null;
300
- }[], instanceId: string): {
373
+ }[], instanceId: string): PinnableParent[];
374
+ /**
375
+ * {@link pinnableParents} for an object: every model of the one format its space can ride. Nothing
376
+ * rides an object, so no row is ever flagged.
377
+ */
378
+ export declare function objectPinParents(models: readonly {
379
+ instanceId: string;
380
+ ref: {
381
+ kind: string;
382
+ name: string;
383
+ };
384
+ }[], space: ObjectSpace): {
301
385
  instanceId: string;
302
386
  name: string;
303
- ridesThis: boolean;
304
387
  }[];
305
388
  /**
306
389
  * Whether pinning puts an item inside its Live2D host's ArtMesh stack, where `attach.depth` places
@@ -323,6 +406,8 @@ export declare function pinEntersStack(item: {
323
406
  export declare function depthStops(meshes: readonly string[]): AttachDepth[];
324
407
  /** Which stop a depth sits at; an ArtMesh the host no longer has reads as the frontmost. */
325
408
  export declare function depthStopIndex(meshes: readonly string[], depth: AttachDepth): number;
409
+ /** A stored anchor — a model's root, a VRM bone or a point on a Live2D ArtMesh — as a usable one, or null. */
410
+ export declare function healAttachAnchor(raw: unknown): AttachAnchor | null;
326
411
  export declare const DEFAULT_HEAD_ANGLE: NonNullable<Attach['headAngle']>;
327
412
  /**
328
413
  * A fresh pin onto `parentInstanceId`: root anchor, in front of the model. A screen-space item (a
@@ -343,6 +428,8 @@ export declare const STORAGE_KEY_MAX_LENGTH = 128;
343
428
  export declare const STORAGE_VALUE_MAX_LENGTH: number;
344
429
  /** Keys one API key may hold; a `storage.set` that would exceed it answers `invalid-state`. */
345
430
  export declare const STORAGE_KEYS_MAX = 256;
431
+ /** `session.identify` refuses a longer name, so a client composing one trims it to this. */
432
+ export declare const SESSION_NAME_MAX_LENGTH = 64;
346
433
  /** `speech.play` URL ceiling — sized for a ~40 s WAV as a base64 `data:audio/*` payload. */
347
434
  export declare const SPEECH_URL_MAX_LENGTH = 8000000;
348
435
  /**
@@ -364,3 +451,4 @@ export declare const TWO_COLUMN_WIDTH = 672;
364
451
  export declare const SAVED_COLORS_MAX = 12;
365
452
  /** 3D-stage resolution multipliers offered by both performance pickers. */
366
453
  export declare const RENDER_SCALE_PRESETS: readonly [1, 0.85, 0.75, 0.66, 0.5];
454
+ export {};
@@ -1,4 +1,5 @@
1
1
  import { effectLayerKeys } from "./effect-schema.js";
2
+ import { isFiniteNumber, isOneOf, isRecord } from "./guards.js";
2
3
  export function clamp(v, min, max) {
3
4
  return Math.min(max, Math.max(min, v));
4
5
  }
@@ -16,6 +17,10 @@ export function wrapAngle(a) {
16
17
  r += TAU;
17
18
  return r;
18
19
  }
20
+ /** Wrap degrees into (-180°, 180°], {@link wrapAngle}'s range; whole turns keep in-range values exact. */
21
+ export function wrapDegrees(v) {
22
+ return v - 360 * Math.ceil((v - 180) / 360);
23
+ }
19
24
  export const DEFAULT_LIVE2D_PLACEMENT = { x: 0, y: 0, scale: 1, rotation: 0 };
20
25
  export const DEFAULT_VRM_PLACEMENT = { x: 0, y: 0, z: 0, rotX: 0, rotY: 0, rotZ: 0, scale: 1 };
21
26
  /** Scene colors heal to 6-digit hex; both cases are spelled out because JSON Schema patterns carry no flags. */
@@ -43,6 +48,11 @@ export const SCENE_LIGHT_SPOT_ANGLE_MIN = 1;
43
48
  export const SCENE_LIGHT_SPOT_ANGLE_MAX = 89;
44
49
  export const DEFAULT_SPOT_ANGLE_DEG = 30;
45
50
  export const DEFAULT_SPOT_PENUMBRA = 0.3;
51
+ /** Light azimuth and roll bounds in degrees, symmetric: a half turn either way. */
52
+ export const SCENE_LIGHT_AZIMUTH_MAX = 180;
53
+ export const SCENE_LIGHT_ROLL_MAX = 180;
54
+ /** Light elevation bound in degrees, symmetric: straight up or straight down. */
55
+ export const SCENE_LIGHT_ELEVATION_MAX = 90;
46
56
  /** Positional lights need falloff headroom; the slider and scene healing share this ceiling. */
47
57
  export function sceneLightIntensityMax(type) {
48
58
  return type === 'directional' || type === 'ambient' ? SCENE_LIGHT_INTENSITY_MAX : SCENE_LIGHT_POINT_INTENSITY_MAX;
@@ -68,6 +78,7 @@ export const LIVE2D_SCALE_MIN = 0.1;
68
78
  export const LIVE2D_SCALE_MAX = 24;
69
79
  export const VRM_SCALE_MIN = 0.05;
70
80
  export const VRM_SCALE_MAX = 10;
81
+ export const SCENE_LIGHT_RANGE_MIN = 0.1;
71
82
  // Range only *cuts off* a point light — three's 1/d² decay has extinguished it
72
83
  // by ~18 m even at max intensity, so a slider past 20 changes nothing visible.
73
84
  export const SCENE_LIGHT_RANGE_MAX = 20;
@@ -77,6 +88,8 @@ export const SCENE_LIGHT_SHADOW_RADIUS_MAX = 16;
77
88
  export const SCENE_LIGHT_POINT_SHADOW_RADIUS_MAX = 32;
78
89
  export const SCENE_LIGHT_SHADOW_SOURCE_SIZE_MAX = 10;
79
90
  export const DEFAULT_LIGHT_SHADOW_SOURCE_SIZE = 0.1;
91
+ /** A light override's key: the baked light's traversal index, in decimal digits. */
92
+ export const LIGHT_OVERRIDE_KEY_RE = /^\d+$/;
80
93
  /** Shadow softness (PCF disk radius, in shadow-map texels) ceiling for a light of `type`. */
81
94
  export function sceneLightShadowRadiusMax(type) {
82
95
  return type === 'point' ? SCENE_LIGHT_POINT_SHADOW_RADIUS_MAX : SCENE_LIGHT_SHADOW_RADIUS_MAX;
@@ -99,6 +112,8 @@ export const CAMERA_DAMPING_MAX = 10;
99
112
  /** Ceiling for {@link SceneCameraAim}'s lookahead time, in seconds, and its smoothing. */
100
113
  export const CAMERA_AIM_LOOKAHEAD_MAX = 1;
101
114
  export const CAMERA_AIM_SMOOTHING_MAX = 30;
115
+ /** {@link SceneCameraAim}'s bias bound on each axis, symmetric: a share of the slack between its zones. */
116
+ export const CAMERA_AIM_BIAS_MAX = 0.5;
102
117
  /** Ceiling for {@link SceneCameraHandheld}'s speed. */
103
118
  export const CAMERA_HANDHELD_SPEED_MAX = 2;
104
119
  /** Warudo's Handheld Movement defaults, switched off. */
@@ -158,6 +173,18 @@ export function cameraTrackKind(item) {
158
173
  return item.ref.kind === 'vrm' ? 'model' : null;
159
174
  return item.space === '3d' ? 'object' : null;
160
175
  }
176
+ /**
177
+ * The layer `target` tracks in `scene` — the primary model for a primary pick, else the first model — when it
178
+ * is one a camera can track.
179
+ */
180
+ export function cameraTrackItem(scene, target) {
181
+ // Runs per frame for each camera track, so it searches the list rather than filtering a copy.
182
+ const item = target.target === 'primary'
183
+ ? (scene.items.find(i => i.kind === 'model' && i.instanceId === scene.primaryInstanceId) ??
184
+ scene.items.find(i => i.kind === 'model'))
185
+ : scene.items.find(i => i.instanceId === target.instanceId);
186
+ return item && cameraTrackKind(item) ? item : undefined;
187
+ }
161
188
  /** 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
189
  export function cameraTrackCandidates(items) {
163
190
  return items.flatMap(item => {
@@ -196,6 +223,23 @@ export const MTOON_OUTLINE_WIDTH_MAX = 2;
196
223
  /** HDR headroom: past ×1 the point is pushing emissive parts over the bloom threshold. */
197
224
  export const MTOON_EMISSIVE_MAX = 4;
198
225
  export const MTOON_UV_ANIMATION_MAX = 4;
226
+ /** Each {@link MToonTuning} field's `[min, max]`: offsets −1..1, `shade` 0..1, multipliers 0 to their ceiling. */
227
+ export const MTOON_RANGES = {
228
+ shade: [0, 1],
229
+ shadingShift: [-1, 1],
230
+ shadingToony: [-1, 1],
231
+ giEqualization: [-1, 1],
232
+ normalScale: [0, MTOON_NORMAL_SCALE_MAX],
233
+ rim: [0, MTOON_RIM_MAX],
234
+ rimLift: [-1, 1],
235
+ rimFresnelPower: [0, MTOON_RIM_FRESNEL_POWER_MAX],
236
+ rimLightingMix: [-1, 1],
237
+ matcap: [0, MTOON_MATCAP_MAX],
238
+ outlineWidth: [0, MTOON_OUTLINE_WIDTH_MAX],
239
+ outlineLightingMix: [-1, 1],
240
+ emissive: [0, MTOON_EMISSIVE_MAX],
241
+ uvAnimation: [0, MTOON_UV_ANIMATION_MAX],
242
+ };
199
243
  export function defaultMToonTuning() {
200
244
  return {
201
245
  shade: 1,
@@ -402,13 +446,35 @@ export const TEXT_BEVEL_RESOLUTION_MAX = 8;
402
446
  export const TEXT_EMISSION_MAX = 4;
403
447
  /** A PostScript name: printable ASCII, at most 63 chars, minus the quote and backslash that would escape `local("…")`. */
404
448
  export const TEXT_POSTSCRIPT_NAME_RE = /^[\x21\x23-\x5b\x5d-\x7e]{1,63}$/;
449
+ /** A text font's CSS `font-weight` bounds. */
450
+ export const TEXT_FONT_WEIGHT_MIN = 1;
451
+ export const TEXT_FONT_WEIGHT_MAX = 1000;
452
+ /** A text font's CSS `font-stretch` bounds, in percent. */
453
+ export const TEXT_FONT_STRETCH_MIN = 50;
454
+ export const TEXT_FONT_STRETCH_MAX = 200;
455
+ /** CSS's normal face: what a text font, or a font file, that leaves its weight, slope or width unsaid reads as. */
456
+ export const DEFAULT_TEXT_FONT_FACE = {
457
+ weight: 400,
458
+ italic: false,
459
+ stretch: 100,
460
+ };
405
461
  /** A prop is a mesh, so it only exists in the three.js scene; every other kind renders in both. */
406
462
  export function objectSupportsSpace(kind, space) {
407
463
  return kind === 'prop' ? space === '3d' : true;
408
464
  }
465
+ /** The {@link ObjectContent} kinds that stream from a registered asset; the rest carry their source inline. */
466
+ const ASSET_CONTENT_KINDS = ['image', 'video', 'prop'];
467
+ /** Whether `kind` — a content kind, or an Inventory row's — is one whose asset streams into a layer. */
468
+ export function isAssetContentKind(kind) {
469
+ return isOneOf(kind, ASSET_CONTENT_KINDS);
470
+ }
471
+ /** Whether the content streams from a registered asset: image, video and prop; the rest carry their source inline. */
472
+ export function isAssetContent(content) {
473
+ return isAssetContentKind(content.kind);
474
+ }
409
475
  /** The asset an object streams from, or null for kinds that carry their source inline (web, capture, text). */
410
476
  export function contentAssetId(content) {
411
- return content.kind === 'image' || content.kind === 'video' || content.kind === 'prop' ? content.assetId : null;
477
+ return isAssetContent(content) ? content.assetId : null;
412
478
  }
413
479
  /**
414
480
  * The one model format a space's objects can ride: the screen layer always composites
@@ -453,6 +519,14 @@ export function pinnableParents(models, items, instanceId) {
453
519
  }
454
520
  return out;
455
521
  }
522
+ /**
523
+ * {@link pinnableParents} for an object: every model of the one format its space can ride. Nothing
524
+ * rides an object, so no row is ever flagged.
525
+ */
526
+ export function objectPinParents(models, space) {
527
+ const format = attachableParentFormat(space);
528
+ return models.filter(m => m.ref.kind === format).map(m => ({ instanceId: m.instanceId, name: m.ref.name }));
529
+ }
456
530
  /**
457
531
  * Whether pinning puts an item inside its Live2D host's ArtMesh stack, where `attach.depth` places
458
532
  * it: a Live2D item, or a 2D object as one quad. A 3D object only ever paints in front.
@@ -477,6 +551,38 @@ export function depthStopIndex(meshes, depth) {
477
551
  const at = meshes.indexOf(depth.id);
478
552
  return at < 0 ? meshes.length + 1 : at + 1;
479
553
  }
554
+ /** Three non-negative finite numbers — whole ones when `int` — or null. */
555
+ function healTriple(raw, int) {
556
+ if (!Array.isArray(raw) || raw.length !== 3)
557
+ return null;
558
+ const usable = (v) => isFiniteNumber(v) && v >= 0 && (!int || Number.isInteger(v));
559
+ const [a, b, c] = raw;
560
+ return usable(a) && usable(b) && usable(c) ? [a, b, c] : null;
561
+ }
562
+ /** A stored anchor — a model's root, a VRM bone or a point on a Live2D ArtMesh — as a usable one, or null. */
563
+ export function healAttachAnchor(raw) {
564
+ if (!isRecord(raw))
565
+ return null;
566
+ if (raw.kind === 'root')
567
+ return { kind: 'root' };
568
+ if (raw.kind === 'bone')
569
+ return typeof raw.bone === 'string' && raw.bone !== '' ? { kind: 'bone', bone: raw.bone } : null;
570
+ if (raw.kind !== 'artMesh' || typeof raw.id !== 'string' || raw.id === '')
571
+ return null;
572
+ const verts = healTriple(raw.verts, true);
573
+ const weights = healTriple(raw.weights, false);
574
+ if (!verts || !weights)
575
+ return null;
576
+ const sum = weights[0] + weights[1] + weights[2];
577
+ // Finite weights can still sum to Infinity; dividing by it would zero the point.
578
+ if (!Number.isFinite(sum) || sum <= 0)
579
+ return null;
580
+ // Normalized so the point stays inside its triangle whatever was stored. A normalized triple is kept
581
+ // bit for bit: dividing it again can flip last bits, and hosts key anchor caches on them.
582
+ if (Math.abs(sum - 1) < 1e-9)
583
+ return { kind: 'artMesh', id: raw.id, verts, weights };
584
+ return { kind: 'artMesh', id: raw.id, verts, weights: [weights[0] / sum, weights[1] / sum, weights[2] / sum] };
585
+ }
480
586
  export const DEFAULT_HEAD_ANGLE = {
481
587
  multiplier: 1,
482
588
  parallaxX: 0,
@@ -514,6 +620,8 @@ export const STORAGE_KEY_MAX_LENGTH = 128;
514
620
  export const STORAGE_VALUE_MAX_LENGTH = 64 * 1024;
515
621
  /** Keys one API key may hold; a `storage.set` that would exceed it answers `invalid-state`. */
516
622
  export const STORAGE_KEYS_MAX = 256;
623
+ /** `session.identify` refuses a longer name, so a client composing one trims it to this. */
624
+ export const SESSION_NAME_MAX_LENGTH = 64;
517
625
  /** `speech.play` URL ceiling — sized for a ~40 s WAV as a base64 `data:audio/*` payload. */
518
626
  export const SPEECH_URL_MAX_LENGTH = 8_000_000;
519
627
  /**
@@ -54,19 +54,18 @@ export interface LipSyncSample {
54
54
  /** Smoothed vowel weights sum to at most one; the remainder represents non-vowel audio. */
55
55
  vowels: Record<LipSyncVowel, number>;
56
56
  }
57
+ /** A capture or output device as a device picker lists it. */
58
+ export interface MediaDeviceOption {
59
+ deviceId: string;
60
+ label: string;
61
+ }
57
62
  export interface LipSyncState {
58
63
  config: LipSyncConfig;
59
64
  status: 'off' | 'starting' | 'listening' | 'error';
60
65
  error: 'permission' | 'device' | 'analysis' | null;
61
- devices: {
62
- deviceId: string;
63
- label: string;
64
- }[];
66
+ devices: MediaDeviceOption[];
65
67
  sample: LipSyncSample | null;
66
- microphone: {
67
- deviceId: string;
68
- label: string;
69
- } | null;
68
+ microphone: MediaDeviceOption | null;
70
69
  profile: Omit<LipSyncProfile, 'samples'> | null;
71
70
  calibration: LipSyncCalibrationState | null;
72
71
  }
@@ -2,7 +2,8 @@ export type Locale = keyof typeof LOCALE_LABELS;
2
2
  /** `ui.language` setting: an explicit locale, or follow the OS language. */
3
3
  export type LanguageSetting = 'system' | Locale;
4
4
  /** `ui.theme` setting: an explicit appearance, or follow the OS. */
5
- export type ThemeSetting = 'system' | 'light' | 'dark';
5
+ export declare const THEME_SETTINGS: readonly ["system", "light", "dark"];
6
+ export type ThemeSetting = (typeof THEME_SETTINGS)[number];
6
7
  /** Native-language display names for the language picker (deliberately untranslated). */
7
8
  export declare const LOCALE_LABELS: {
8
9
  en: string;
@@ -12,5 +13,7 @@ export declare const LOCALE_LABELS: {
12
13
  };
13
14
  /** Locales shipped in the compiled catalogs (`pnpm i18n`). */
14
15
  export declare const LOCALES: readonly Locale[];
16
+ /** Every `ui.language` value, `system` first. */
17
+ export declare const LANGUAGE_SETTINGS: readonly LanguageSetting[];
15
18
  /** Best supported locale for a BCP 47-ish tag: exact match first, then fuzzy per language. */
16
19
  export declare function matchLocaleTag(tag: string): Locale;
@@ -1,4 +1,6 @@
1
1
  import { isOneOf, keysOf } from "./guards.js";
2
+ /** `ui.theme` setting: an explicit appearance, or follow the OS. */
3
+ export const THEME_SETTINGS = ['system', 'light', 'dark'];
2
4
  /** Native-language display names for the language picker (deliberately untranslated). */
3
5
  export const LOCALE_LABELS = {
4
6
  en: 'English',
@@ -8,6 +10,8 @@ export const LOCALE_LABELS = {
8
10
  };
9
11
  /** Locales shipped in the compiled catalogs (`pnpm i18n`). */
10
12
  export const LOCALES = keysOf(LOCALE_LABELS);
13
+ /** Every `ui.language` value, `system` first. */
14
+ export const LANGUAGE_SETTINGS = ['system', ...LOCALES];
11
15
  /** Best supported locale for a BCP 47-ish tag: exact match first, then fuzzy per language. */
12
16
  export function matchLocaleTag(tag) {
13
17
  if (isOneOf(tag, LOCALES))
@@ -1,3 +1,11 @@
1
+ import type { AttachAnchor } from '../wire/types.ts';
2
+ /**
3
+ * The point `z` grows and shrinks the model around: its centre (`root`), or a point on one of its
4
+ * ArtMeshes, taken where that mesh sits in the model's rest pose so head motion never moves it.
5
+ */
6
+ export type ModelMovementPivot = Extract<AttachAnchor, {
7
+ kind: 'root' | 'artMesh';
8
+ }>;
1
9
  /**
2
10
  * VTube Studio's Movement Config: how far tracked face position slides (`x`, `y`) and scales (`z`)
3
11
  * a whole Live2D model. It moves the model itself and never writes a parameter, so it stacks with
@@ -9,21 +17,28 @@ export interface ModelMovementConfig {
9
17
  x: number;
10
18
  y: number;
11
19
  z: number;
20
+ /** Persona-only: VTS has no such setting, so it lives in the sidecar even beside a `.vtube.json`. */
21
+ pivot: ModelMovementPivot;
12
22
  /** Smoothing levels 0–100, read once when the model loads; file-only, as in VTS. */
13
23
  smoothingX: number;
14
24
  smoothingY: number;
15
25
  smoothingZ: number;
16
26
  }
17
- /** VTS's slider limit (`MAX_MOVEMENT`): an edited amount is a whole number within ±20. */
18
- export declare const MODEL_MOVEMENT_MAX = 20;
27
+ /** The per-axis smoothing levels, read once when the model loads and never edited. */
28
+ export type ModelMovementSmoothing = Pick<ModelMovementConfig, 'smoothingX' | 'smoothingY' | 'smoothingZ'>;
29
+ /**
30
+ * The editor's limit: an edited amount is a whole number within ±50. VTS's own sliders stop at ±20
31
+ * (`MAX_MOVEMENT`), and releasing one there writes all three axes back clamped to that.
32
+ */
33
+ export declare const MODEL_MOVEMENT_MAX = 50;
19
34
  /** VTS's `ModelPositionMovement` field initializers — a model nobody configured moves by default. */
20
35
  export declare const DEFAULT_MODEL_MOVEMENT: Readonly<ModelMovementConfig>;
21
36
  /**
22
37
  * A stored or relayed configuration, read per field over the defaults. Amounts are taken as
23
- * written: VTS clamps its sliders to ±20, never what a file holds.
38
+ * written: the editors clamp what they write, never what a file holds.
24
39
  */
25
40
  export declare function healModelMovement(raw: unknown): ModelMovementConfig;
26
41
  /** What the Movement Config editor changes; a file's smoothing levels are left as they are. */
27
- export type ModelMovementEdit = Partial<Pick<ModelMovementConfig, 'enabled' | 'x' | 'y' | 'z'>>;
42
+ export type ModelMovementEdit = Partial<Pick<ModelMovementConfig, 'enabled' | 'x' | 'y' | 'z' | 'pivot'>>;
28
43
  /** An edit healed to the editor's limits; an absent or unusable field is dropped, not defaulted. */
29
44
  export declare function healModelMovementEdit(raw: unknown): ModelMovementEdit;
@@ -1,20 +1,29 @@
1
1
  import { finiteOr, isFiniteNumber, isRecord } from "./guards.js";
2
- import { clamp } from "./limits.js";
3
- /** VTS's slider limit (`MAX_MOVEMENT`): an edited amount is a whole number within ±20. */
4
- export const MODEL_MOVEMENT_MAX = 20;
2
+ import { clamp, healAttachAnchor } from "./limits.js";
3
+ /**
4
+ * The editor's limit: an edited amount is a whole number within ±50. VTS's own sliders stop at ±20
5
+ * (`MAX_MOVEMENT`), and releasing one there writes all three axes back clamped to that.
6
+ */
7
+ export const MODEL_MOVEMENT_MAX = 50;
5
8
  /** VTS's `ModelPositionMovement` field initializers — a model nobody configured moves by default. */
6
9
  export const DEFAULT_MODEL_MOVEMENT = {
7
10
  enabled: true,
8
11
  x: 6,
9
12
  y: 8,
10
13
  z: 11,
14
+ pivot: { kind: 'root' },
11
15
  smoothingX: 10,
12
16
  smoothingY: 10,
13
17
  smoothingZ: 10,
14
18
  };
19
+ /** A stored or relayed pivot as a usable one: any anchor but a bone, which no Live2D model has. */
20
+ function healPivot(raw) {
21
+ const anchor = healAttachAnchor(raw);
22
+ return anchor?.kind === 'bone' ? null : anchor;
23
+ }
15
24
  /**
16
25
  * A stored or relayed configuration, read per field over the defaults. Amounts are taken as
17
- * written: VTS clamps its sliders to ±20, never what a file holds.
26
+ * written: the editors clamp what they write, never what a file holds.
18
27
  */
19
28
  export function healModelMovement(raw) {
20
29
  const r = isRecord(raw) ? raw : {};
@@ -24,6 +33,7 @@ export function healModelMovement(raw) {
24
33
  x: finiteOr(r.x, d.x),
25
34
  y: finiteOr(r.y, d.y),
26
35
  z: finiteOr(r.z, d.z),
36
+ pivot: healPivot(r.pivot) ?? d.pivot,
27
37
  smoothingX: finiteOr(r.smoothingX, d.smoothingX),
28
38
  smoothingY: finiteOr(r.smoothingY, d.smoothingY),
29
39
  smoothingZ: finiteOr(r.smoothingZ, d.smoothingZ),
@@ -40,5 +50,8 @@ export function healModelMovementEdit(raw) {
40
50
  if (isFiniteNumber(v))
41
51
  edit[axis] = clamp(Math.round(v), -MODEL_MOVEMENT_MAX, MODEL_MOVEMENT_MAX);
42
52
  }
53
+ const pivot = healPivot(r.pivot);
54
+ if (pivot)
55
+ edit.pivot = pivot;
43
56
  return edit;
44
57
  }
@@ -1,4 +1,4 @@
1
- import { type SceneEnvironment, type SceneVolumetricLighting } from '../wire/types.ts';
1
+ import type { SceneEnvironment, SceneVolumetricLighting } from '../wire/types.ts';
2
2
  /** Slider and healing metadata for the haze's numeric settings. */
3
3
  export declare const SCENE_VOLUMETRIC_LIGHTING_SPECS: {
4
4
  readonly density: {
@@ -1,4 +1,4 @@
1
- import { EFFECTS_QUALITY_LEVELS } from "../wire/types.js";
1
+ import { EFFECTS_QUALITY_LEVELS } from "./effect-schema.js";
2
2
  import { finiteOr, isOneOf, isRecord } from "./guards.js";
3
3
  import { clamp } from "./limits.js";
4
4
  /** Slider and healing metadata for the haze's numeric settings. */
@@ -1,7 +1,7 @@
1
1
  import { isRecord } from "../values/guards.js";
2
2
  import { isApiErrorCode } from "./errors.js";
3
3
  import { isEventName } from "./events.js";
4
- import { isAppCapability } from "./types.js";
4
+ import { canonicalAppCapability, isAppCapability } from "./types.js";
5
5
  /**
6
6
  * Parse one inbound client frame. Returns the request, or an error code telling
7
7
  * the server what to answer: `parse-error` for junk bytes, `invalid-request`
@@ -47,8 +47,11 @@ export function parseServerMessage(raw) {
47
47
  const { name, version, platform } = v.app;
48
48
  if (typeof name !== 'string' || typeof version !== 'string' || typeof platform !== 'string')
49
49
  return null;
50
- // Tolerant both ways: absent on older servers, unknown names from newer ones dropped.
51
- const capabilities = Array.isArray(v.app.capabilities) ? v.app.capabilities.filter(isAppCapability) : [];
50
+ // Tolerant both ways: absent on older servers, unknown names from newer ones dropped. Old names fold
51
+ // into their replacements, which a newer server also lists.
52
+ const names = Array.isArray(v.app.capabilities) ? v.app.capabilities : [];
53
+ const folded = names.map(n => (typeof n === 'string' ? canonicalAppCapability(n) : n));
54
+ const capabilities = [...new Set(folded.filter(isAppCapability))];
52
55
  return { kind: 'hello', protocol: v.protocol, app: { name, version, platform, capabilities } };
53
56
  }
54
57
  case 'response':