@laplace.live/persona-sdk 1.24.0 → 1.25.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/README.md +10 -10
  2. package/dist/client/client.js +5 -6
  3. package/dist/client/legacy-names.d.ts +2 -0
  4. package/dist/client/{legacy-kinds.js → legacy-names.js} +12 -3
  5. package/dist/client/node.d.ts +13 -0
  6. package/dist/client/node.js +29 -0
  7. package/dist/client/scene-hotkeys.d.ts +16 -0
  8. package/dist/client/scene-hotkeys.js +17 -0
  9. package/dist/index.d.ts +3 -0
  10. package/dist/index.js +3 -0
  11. package/dist/values/bindings.d.ts +40 -15
  12. package/dist/values/controller.d.ts +9 -6
  13. package/dist/values/edit-history.d.ts +4 -0
  14. package/dist/values/edit-history.js +14 -7
  15. package/dist/values/effect-schema.d.ts +26 -0
  16. package/dist/values/effect-schema.js +76 -91
  17. package/dist/values/hotkey-targets.d.ts +10 -3
  18. package/dist/values/hotkey-targets.js +20 -0
  19. package/dist/values/hotkeys.d.ts +9 -1
  20. package/dist/values/hotkeys.js +11 -0
  21. package/dist/values/labels.js +1 -0
  22. package/dist/values/limits.d.ts +96 -8
  23. package/dist/values/limits.js +109 -1
  24. package/dist/values/lipsync.d.ts +7 -8
  25. package/dist/values/locale.d.ts +4 -1
  26. package/dist/values/locale.js +4 -0
  27. package/dist/values/model-movement.d.ts +19 -4
  28. package/dist/values/model-movement.js +17 -4
  29. package/dist/values/volumetric-lighting.d.ts +1 -1
  30. package/dist/values/volumetric-lighting.js +1 -1
  31. package/dist/wire/envelope.js +6 -3
  32. package/dist/wire/methods.d.ts +16 -17
  33. package/dist/wire/protocol.d.ts +2 -0
  34. package/dist/wire/protocol.js +4 -0
  35. package/dist/wire/schemas/fields.d.ts +8 -0
  36. package/dist/wire/schemas/fields.js +1 -0
  37. package/dist/wire/schemas/requests.d.ts +2 -2
  38. package/dist/wire/schemas/requests.js +70 -20
  39. package/dist/wire/schemas/settings.d.ts +3 -3
  40. package/dist/wire/schemas/settings.js +3 -3
  41. package/dist/wire/types.d.ts +68 -115
  42. package/dist/wire/types.js +68 -9
  43. package/package.json +2 -2
  44. package/dist/client/legacy-kinds.d.ts +0 -2
@@ -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':
@@ -4,8 +4,9 @@ import type { SpawnTransition } from '../values/item-transition.ts';
4
4
  import type { LipSyncCalibrationCommand, LipSyncConfig, LipSyncMode, LipSyncState } from '../values/lipsync.ts';
5
5
  import type { ModelMovementConfig, ModelMovementEdit } from '../values/model-movement.ts';
6
6
  import type { SceneInspection } from '../values/stage-info.ts';
7
+ import type { AppInfo } from './envelope.ts';
7
8
  import type { EventName } from './events.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
+ import type { AnchorOption, AssetKind, AssetRef, Attach, AttachHeadAngle, AutomationInfo, BindingInput, ChannelBindings, Expression, ExpressionPersistence, FontFamilyInfo, 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, SceneModelItem, ScenePatch, SceneState, ScreenPlacement, Settings, SettingsPatch, TrackingLostBehavior, TrackingSourceConfig, TrackingSourceId, TrackingSourceKind, TrackingStatus, VrmPlacement } from './types.ts';
9
10
  /** Marker for methods that take no parameters; the client lets you omit the argument. */
10
11
  export type EmptyRequest = Record<never, never>;
11
12
  /** Marker for methods whose success response carries no data. */
@@ -85,7 +86,10 @@ export interface InstanceSetVisibleRequest {
85
86
  visible: boolean;
86
87
  }
87
88
  export type InstanceSetVisibleResponse = EmptyResponse;
88
- /** Edits one model or object's source effects; gate on the `layer-effects` app capability. */
89
+ /**
90
+ * Edits one model or object's source effects; gate on the `layer-effects` app capability, and a `2d`
91
+ * object on `layer-effects-2d`.
92
+ */
89
93
  export interface InstanceSetEffectsRequest {
90
94
  instanceId: string;
91
95
  effects: LayerEffectsPatch;
@@ -96,13 +100,12 @@ export interface InstanceSetEffectsResponse {
96
100
  effects: LayerEffects;
97
101
  effectLayers: LayerEffectKey[];
98
102
  }
99
- /** Omitted fields keep their binding; null stops tracking that channel for this instance. */
100
- export interface InstanceSetTrackingSourcesRequest {
103
+ /**
104
+ * The channels' source bindings (`ChannelBindings`). Omitted fields keep their binding;
105
+ * null stops tracking that channel for this instance.
106
+ */
107
+ export interface InstanceSetTrackingSourcesRequest extends Partial<ChannelBindings>, Partial<Pick<SceneModelItem, 'handTrackingMode'>> {
101
108
  instanceId: string;
102
- faceSourceId?: string | null;
103
- poseSourceId?: string | null;
104
- handSourceId?: string | null;
105
- handTrackingMode?: HandTrackingMode;
106
109
  }
107
110
  export type InstanceSetTrackingSourcesResponse = EmptyResponse;
108
111
  export interface InstanceSetControllerMovementRequest {
@@ -173,7 +176,7 @@ export interface InstanceSetIdleRequest {
173
176
  idleClip?: string;
174
177
  /** Live2D only: a motion file from `motion.list`, or null for the model's `Idle` group. */
175
178
  idleMotion?: string | null;
176
- /** The `tracking-lost` app capability carries these three. */
179
+ /** The `idle-tracking-lost` app capability carries these three. */
177
180
  trackingLostBehavior?: TrackingLostBehavior;
178
181
  /** Live2D only: a motion file from `motion.list`, or null for none. */
179
182
  trackingLostMotion?: string | null;
@@ -215,7 +218,7 @@ export interface ObjectSetContentRequest {
215
218
  content: ObjectContent;
216
219
  }
217
220
  export type ObjectSetContentResponse = EmptyResponse;
218
- /** A `text` object's text alone, under `text-objects`. */
221
+ /** A `text` object's text alone, under `object-text`. */
219
222
  export interface ObjectSetTextRequest {
220
223
  instanceId: string;
221
224
  /** Null drops a transient text, showing the saved one again. */
@@ -526,13 +529,9 @@ export interface StageCaptureResponse {
526
529
  height: number;
527
530
  }
528
531
  export type AppInfoRequest = EmptyRequest;
529
- export interface AppInfoResponse {
530
- name: string;
531
- version: string;
532
- platform: string;
532
+ /** What `hello` carries, the app identity plus its protocol version. */
533
+ export interface AppInfoResponse extends AppInfo {
533
534
  protocol: number;
534
- /** Same list `hello` carries; gate features on it, never on `version`. */
535
- capabilities: AppCapability[];
536
535
  }
537
536
  export type AppLocalAddressesRequest = EmptyRequest;
538
537
  export interface AppLocalAddressesResponse {
@@ -662,7 +661,7 @@ export interface ModelSetMovementRequest {
662
661
  export interface ModelSetMovementResponse {
663
662
  /** The configuration now moving the model, whether or not it reached the file. */
664
663
  movement: ModelMovementConfig;
665
- /** False when nothing reached the file: a refused write (read-only, unreadable), or no usable field. */
664
+ /** False when any part missed its file — a refused write (read-only, unreadable) — or no field was usable. */
666
665
  saved: boolean;
667
666
  }
668
667
  export interface InstanceSetBreathRequest {
@@ -7,6 +7,8 @@ export declare const DEFAULT_API_PORT = 25034;
7
7
  export declare const DEFAULT_API_HOST = "127.0.0.1";
8
8
  /** Scene activation waits for asset preparation before committing the new scene. */
9
9
  export declare const SCENE_ACTIVATION_TIMEOUT_MS = 120000;
10
+ /** How long a caller waits for `method`'s answer: scene activation prepares assets first, the rest get `fallbackMs`. */
11
+ export declare function requestTimeoutMs(method: string, fallbackMs: number): number;
10
12
  /** Server close code: the session's API key was revoked. Terminal — the client must not redial. */
11
13
  export declare const CLOSE_KEY_REVOKED = 4001;
12
14
  /** Server close code: the user disconnected this session from Persona's settings. Terminal — the client must not redial. */
@@ -6,6 +6,10 @@ export const DEFAULT_API_PORT = 25034;
6
6
  export const DEFAULT_API_HOST = '127.0.0.1';
7
7
  /** Scene activation waits for asset preparation before committing the new scene. */
8
8
  export const SCENE_ACTIVATION_TIMEOUT_MS = 120_000;
9
+ /** How long a caller waits for `method`'s answer: scene activation prepares assets first, the rest get `fallbackMs`. */
10
+ export function requestTimeoutMs(method, fallbackMs) {
11
+ return method === 'scene.activate' ? SCENE_ACTIVATION_TIMEOUT_MS : fallbackMs;
12
+ }
9
13
  /** Server close code: the session's API key was revoked. Terminal — the client must not redial. */
10
14
  export const CLOSE_KEY_REVOKED = 4001;
11
15
  /** Server close code: the user disconnected this session from Persona's settings. Terminal — the client must not redial. */
@@ -0,0 +1,8 @@
1
+ import type * as z from 'zod';
2
+ /**
3
+ * A `z.object` shape naming every key of `T`, optional ones included. `satisfies` it so a field added
4
+ * to `T` fails to compile until the schema takes it — `z.object` strips the keys it does not name.
5
+ */
6
+ export type Fields<T> = {
7
+ [K in keyof T]-?: z.ZodType;
8
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -170,12 +170,12 @@ export declare const requestSchemas: {
170
170
  }, z.core.$strip>;
171
171
  'tracking.addSource': z.ZodObject<{
172
172
  kind: z.ZodEnum<{
173
+ mediapipe: "mediapipe";
173
174
  "persona-ios": "persona-ios";
174
175
  ifacialmocap: "ifacialmocap";
175
176
  "vts-ios": "vts-ios";
176
177
  vmc: "vmc";
177
178
  mocopi: "mocopi";
178
- mediapipe: "mediapipe";
179
179
  }>;
180
180
  name: z.ZodOptional<z.ZodString>;
181
181
  phoneIp: z.ZodOptional<z.ZodNullable<z.ZodString>>;
@@ -221,7 +221,7 @@ export declare const requestSchemas: {
221
221
  }, z.core.$strip>;
222
222
  'model.setMovement': z.ZodObject<{
223
223
  modelId: z.ZodOptional<z.ZodString>;
224
- movement: z.ZodCustom<Partial<Pick<import("../../index.ts").ModelMovementConfig, "enabled" | "x" | "y" | "z">>, Partial<Pick<import("../../index.ts").ModelMovementConfig, "enabled" | "x" | "y" | "z">>>;
224
+ movement: z.ZodCustom<Partial<Pick<import("../../index.ts").ModelMovementConfig, "enabled" | "x" | "y" | "z" | "pivot">>, Partial<Pick<import("../../index.ts").ModelMovementConfig, "enabled" | "x" | "y" | "z" | "pivot">>>;
225
225
  }, z.core.$strip>;
226
226
  'asset.register': z.ZodObject<{
227
227
  path: z.ZodString;