@laplace.live/persona-sdk 1.15.0 → 1.17.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.
@@ -1,4 +1,4 @@
1
- import type { AssetRef, Attach, EnvironmentLook, ModelFormat, MToonTuning, ObjectContent, ObjectSpace, SceneEnvironment, SceneLight, SceneLightCameraFollowOptions, SceneLightType, ScreenPlacement, VrmPlacement } from '../wire/types.ts';
1
+ import type { AssetRef, Attach, AttachDepth, EnvironmentLook, ModelFormat, MToonTuning, ObjectContent, ObjectSpace, 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;
@@ -173,11 +173,51 @@ export declare function objectSupportsSpace(kind: ObjectContent['kind'], space:
173
173
  /** The asset an object streams from, or null for kinds that carry their source inline (web, capture). */
174
174
  export declare function contentAssetId(content: ObjectContent): string | null;
175
175
  /**
176
- * The one model format a space's objects can ride: the renderers never mix, and
177
- * the three canvas always composites over the Pixi one, so a cross-space pin
178
- * would z-fight by construction.
176
+ * The one model format a space's objects can ride: the screen layer always composites
177
+ * over the world scene, so a cross-space pin would z-fight by construction.
179
178
  */
180
179
  export declare function attachableParentFormat(space: ObjectSpace): ModelFormat;
180
+ /**
181
+ * Every instance that rides `instanceId`, directly or through a chain of pins — the set it can
182
+ * never pin to itself, or the two would ride each other in a circle. Includes `instanceId`.
183
+ */
184
+ export declare function pinRiders(items: readonly {
185
+ instanceId: string;
186
+ attach?: {
187
+ parentInstanceId: string;
188
+ } | null;
189
+ }[], instanceId: string): Set<string>;
190
+ /**
191
+ * Rows for the pin picker: every other Live2D model, each flagged when it already rides
192
+ * `instanceId` — picking one would put the two in a circle, so both apps offer it disabled
193
+ * rather than hidden. One rule, or the console offers a pin the desktop refuses.
194
+ */
195
+ export declare function pinnableParents(models: readonly {
196
+ instanceId: string;
197
+ ref: {
198
+ kind: string;
199
+ name: string;
200
+ };
201
+ }[], items: readonly {
202
+ instanceId: string;
203
+ attach?: {
204
+ parentInstanceId: string;
205
+ } | null;
206
+ }[], instanceId: string): {
207
+ instanceId: string;
208
+ name: string;
209
+ ridesThis: boolean;
210
+ }[];
211
+ /**
212
+ * The depth control's stops, back to front: behind the model, above each of its ArtMeshes in
213
+ * paint order, then in front of it all. One continuous run, so depth is a slider over the host's
214
+ * stack rather than a list of ArtMesh ids nobody can read.
215
+ */
216
+ export declare function depthStops(meshes: readonly string[]): AttachDepth[];
217
+ /** Which stop a depth sits at; an ArtMesh the host no longer has reads as the frontmost. */
218
+ export declare function depthStopIndex(meshes: readonly string[], depth: AttachDepth): number;
219
+ /** A fresh pin onto `parentInstanceId`: root anchor, in front of the model, no response tuning. */
220
+ export declare function defaultAttach(parentInstanceId: string): Attach;
181
221
  export declare const DEFAULT_HEAD_ANGLE: NonNullable<Attach['headAngle']>;
182
222
  export declare const ATTACH_MULTIPLIER_MIN = -2;
183
223
  export declare const ATTACH_MULTIPLIER_MAX = 2;
@@ -297,13 +297,77 @@ export function contentAssetId(content) {
297
297
  return content.kind === 'image' || content.kind === 'video' || content.kind === 'prop' ? content.assetId : null;
298
298
  }
299
299
  /**
300
- * The one model format a space's objects can ride: the renderers never mix, and
301
- * the three canvas always composites over the Pixi one, so a cross-space pin
302
- * would z-fight by construction.
300
+ * The one model format a space's objects can ride: the screen layer always composites
301
+ * over the world scene, so a cross-space pin would z-fight by construction.
303
302
  */
304
303
  export function attachableParentFormat(space) {
305
304
  return space === '2d' ? 'live2d' : 'vrm';
306
305
  }
306
+ /**
307
+ * Every instance that rides `instanceId`, directly or through a chain of pins — the set it can
308
+ * never pin to itself, or the two would ride each other in a circle. Includes `instanceId`.
309
+ */
310
+ export function pinRiders(items, instanceId) {
311
+ const riders = new Set([instanceId]);
312
+ // One pass per item is enough for a chain of any depth: each pass adopts the next level down.
313
+ for (let i = 0; i < items.length; i++) {
314
+ let grew = false;
315
+ for (const item of items) {
316
+ const parent = item.attach?.parentInstanceId;
317
+ if (parent !== undefined && riders.has(parent) && !riders.has(item.instanceId)) {
318
+ riders.add(item.instanceId);
319
+ grew = true;
320
+ }
321
+ }
322
+ if (!grew)
323
+ break;
324
+ }
325
+ return riders;
326
+ }
327
+ /**
328
+ * Rows for the pin picker: every other Live2D model, each flagged when it already rides
329
+ * `instanceId` — picking one would put the two in a circle, so both apps offer it disabled
330
+ * rather than hidden. One rule, or the console offers a pin the desktop refuses.
331
+ */
332
+ export function pinnableParents(models, items, instanceId) {
333
+ const riders = pinRiders(items, instanceId);
334
+ const out = [];
335
+ for (const m of models) {
336
+ if (m.ref.kind !== 'live2d' || m.instanceId === instanceId)
337
+ continue;
338
+ out.push({ instanceId: m.instanceId, name: m.ref.name, ridesThis: riders.has(m.instanceId) });
339
+ }
340
+ return out;
341
+ }
342
+ /**
343
+ * The depth control's stops, back to front: behind the model, above each of its ArtMeshes in
344
+ * paint order, then in front of it all. One continuous run, so depth is a slider over the host's
345
+ * stack rather than a list of ArtMesh ids nobody can read.
346
+ */
347
+ export function depthStops(meshes) {
348
+ return [{ kind: 'behind' }, ...meshes.map((id) => ({ kind: 'artMesh', id })), { kind: 'front' }];
349
+ }
350
+ /** Which stop a depth sits at; an ArtMesh the host no longer has reads as the frontmost. */
351
+ export function depthStopIndex(meshes, depth) {
352
+ if (depth.kind === 'behind')
353
+ return 0;
354
+ if (depth.kind === 'front')
355
+ return meshes.length + 1;
356
+ const at = meshes.indexOf(depth.id);
357
+ return at < 0 ? meshes.length + 1 : at + 1;
358
+ }
359
+ /** A fresh pin onto `parentInstanceId`: root anchor, in front of the model, no response tuning. */
360
+ export function defaultAttach(parentInstanceId) {
361
+ return {
362
+ parentInstanceId,
363
+ anchor: { kind: 'root' },
364
+ followRotation: true,
365
+ depth: { kind: 'front' },
366
+ split: null,
367
+ headAngle: null,
368
+ elasticity: null,
369
+ };
370
+ }
307
371
  export const DEFAULT_HEAD_ANGLE = {
308
372
  multiplier: 1,
309
373
  parallaxX: 0,
@@ -149,6 +149,16 @@ export interface InstanceSetPlacementRequest {
149
149
  vrm?: Partial<VrmPlacement>;
150
150
  }
151
151
  export type InstanceSetPlacementResponse = EmptyResponse;
152
+ /**
153
+ * Pin a Live2D instance to another one as an item (VTS's Live2D Items): its placement becomes an
154
+ * offset from the anchor, and `attach.depth` says where it paints in the host's ArtMesh stack.
155
+ */
156
+ export interface InstanceAttachRequest {
157
+ instanceId?: string;
158
+ /** Null detaches, leaving the instance where it stands. */
159
+ attach: AttachInput | null;
160
+ }
161
+ export type InstanceAttachResponse = EmptyResponse;
152
162
  export interface InstanceSetMToonRequest {
153
163
  instanceId?: string;
154
164
  tuning: Partial<MToonTuning>;
@@ -810,6 +820,10 @@ export interface MethodMap {
810
820
  request: InstanceSetPlacementRequest;
811
821
  response: InstanceSetPlacementResponse;
812
822
  };
823
+ 'instance.attach': {
824
+ request: InstanceAttachRequest;
825
+ response: InstanceAttachResponse;
826
+ };
813
827
  'instance.setMToon': {
814
828
  request: InstanceSetMToonRequest;
815
829
  response: InstanceSetMToonResponse;
@@ -18,9 +18,10 @@ export declare const INJECT_LEASE_TTL_MS = 1000;
18
18
  /** How often {@link PersonaClient.driveParameter} re-sends held leases to keep them alive. */
19
19
  export declare const INJECT_HEARTBEAT_MS = 100;
20
20
  /**
21
- * How often the binding editor's live indicators sample the tracking inputs (~20/s) — both the
22
- * stage's push and the editor's poll of it, which must agree. Fast enough to read as live on a
23
- * level bar, slow enough that neither the worker port nor the panel relay carries per-frame traffic.
21
+ * Least gap between the stage's refreshes of the tracking inputs behind the binding editor's live
22
+ * indicators (~20/s at most). The refresh rides the stage's frames, so a capped frame rate
23
+ * stretches it. Fast enough to read as live, slow enough that the worker port carries no
24
+ * per-frame traffic.
24
25
  */
25
26
  export declare const TRACKING_INPUTS_MS = 50;
26
27
  /**
@@ -17,9 +17,10 @@ export const INJECT_LEASE_TTL_MS = 1000;
17
17
  /** How often {@link PersonaClient.driveParameter} re-sends held leases to keep them alive. */
18
18
  export const INJECT_HEARTBEAT_MS = 100;
19
19
  /**
20
- * How often the binding editor's live indicators sample the tracking inputs (~20/s) — both the
21
- * stage's push and the editor's poll of it, which must agree. Fast enough to read as live on a
22
- * level bar, slow enough that neither the worker port nor the panel relay carries per-frame traffic.
20
+ * Least gap between the stage's refreshes of the tracking inputs behind the binding editor's live
21
+ * indicators (~20/s at most). The refresh rides the stage's frames, so a capped frame rate
22
+ * stretches it. Fast enough to read as live, slow enough that the worker port carries no
23
+ * per-frame traffic.
23
24
  */
24
25
  export const TRACKING_INPUTS_MS = 50;
25
26
  /**
@@ -255,10 +255,6 @@ export declare const requestSchemas: {
255
255
  medium: "medium";
256
256
  }>>;
257
257
  renderScale: z.ZodOptional<z.ZodNumber>;
258
- live2dEngine: z.ZodOptional<z.ZodEnum<{
259
- pixi: "pixi";
260
- three: "three";
261
- }>>;
262
258
  }, z.core.$strip>>;
263
259
  }, z.core.$strip>;
264
260
  }, z.core.$strip>;
@@ -1,10 +1,6 @@
1
1
  import * as z from 'zod';
2
2
  /** Shared field definitions; parsing validates values without adding defaults or healing them. */
3
3
  export declare const PortSchema: z.ZodNumber;
4
- export declare const Live2DEngineSchema: z.ZodEnum<{
5
- pixi: "pixi";
6
- three: "three";
7
- }>;
8
4
  export declare const EffectsQualitySchema: z.ZodEnum<{
9
5
  high: "high";
10
6
  low: "low";
@@ -110,10 +106,6 @@ export declare const PerformanceSettingsSchema: z.ZodObject<{
110
106
  medium: "medium";
111
107
  }>;
112
108
  renderScale: z.ZodNumber;
113
- live2dEngine: z.ZodEnum<{
114
- pixi: "pixi";
115
- three: "three";
116
- }>;
117
109
  }, z.core.$strip>;
118
110
  /** Stage content dimensions in device-independent pixels. */
119
111
  export declare const StageSizeSchema: z.ZodObject<{
@@ -201,10 +193,6 @@ export declare const SettingsSchema: z.ZodObject<{
201
193
  medium: "medium";
202
194
  }>;
203
195
  renderScale: z.ZodOptional<z.ZodNumber>;
204
- live2dEngine: z.ZodOptional<z.ZodEnum<{
205
- pixi: "pixi";
206
- three: "three";
207
- }>>;
208
196
  }, z.core.$strip>;
209
197
  tracking: z.ZodObject<{
210
198
  enabled: z.ZodBoolean;
@@ -276,9 +264,5 @@ export declare const SettingsPatchSchema: z.ZodObject<{
276
264
  medium: "medium";
277
265
  }>>;
278
266
  renderScale: z.ZodOptional<z.ZodNumber>;
279
- live2dEngine: z.ZodOptional<z.ZodEnum<{
280
- pixi: "pixi";
281
- three: "three";
282
- }>>;
283
267
  }, z.core.$strip>>;
284
268
  }, z.core.$strip>;
@@ -2,10 +2,9 @@ import * as z from 'zod';
2
2
  import { CONTROLLER_DEAD_ZONE_MAX } from "../../values/controller.js";
3
3
  import { LIP_SYNC_GAIN_MAX, LIP_SYNC_MAX_SAMPLES, LIP_SYNC_MFCC_COEFFICIENTS, LIP_SYNC_MIN_SAMPLES, LIP_SYNC_NOISE_GATE_MIN, LIP_SYNC_PHONEMES, LIP_SYNC_SMOOTHING_MAX, } from "../../values/lipsync.js";
4
4
  import { LOCALES } from "../../values/locale.js";
5
- import { EFFECTS_QUALITY_LEVELS, LIVE2D_ENGINES, MEDIAPIPE_DELEGATES, POSE_SOURCE_IDS, TRACKING_SOURCE_IDS, TRACKING_SOURCE_KINDS, } from "../types.js";
5
+ import { EFFECTS_QUALITY_LEVELS, MEDIAPIPE_DELEGATES, POSE_SOURCE_IDS, TRACKING_SOURCE_IDS, TRACKING_SOURCE_KINDS, } from "../types.js";
6
6
  /** Shared field definitions; parsing validates values without adding defaults or healing them. */
7
7
  export const PortSchema = z.number().int().min(1).max(65535).describe('Network port, from 1 through 65535.');
8
- export const Live2DEngineSchema = z.enum(LIVE2D_ENGINES).describe('Engine used to render Live2D models.');
9
8
  export const EffectsQualitySchema = z.enum(EFFECTS_QUALITY_LEVELS).describe('Scene and layer effects quality tier.');
10
9
  export const LanguageSettingSchema = z
11
10
  .enum(['system', ...LOCALES])
@@ -75,7 +74,6 @@ export const PerformanceSettingsSchema = z.object({
75
74
  fpsLimit: z.number().describe('Frame-rate cap; zero is unlimited. The desktop snaps changes to supported presets.'),
76
75
  effectsQuality: EffectsQualitySchema,
77
76
  renderScale: z.number().describe('Render resolution multiplier. The desktop snaps changes to supported presets.'),
78
- live2dEngine: Live2DEngineSchema,
79
77
  });
80
78
  /** Stage content dimensions in device-independent pixels. */
81
79
  export const StageSizeSchema = z.object({
@@ -107,7 +105,7 @@ export const SettingsSchema = z.object({
107
105
  ui: z
108
106
  .object({ trayVisible: z.boolean().describe('Show the tray or menu bar icon.') })
109
107
  .describe('Public interface preferences.'),
110
- performance: PerformanceSettingsSchema.partial({ renderScale: true, live2dEngine: true }).describe('Renderer settings; renderScale and live2dEngine are absent on older hosts.'),
108
+ performance: PerformanceSettingsSchema.partial({ renderScale: true }).describe('Renderer settings; renderScale is absent on older hosts.'),
111
109
  tracking: TrackingSettingsSchema.describe('Network face master switch and all configured tracking sources.'),
112
110
  pose: PoseSettingsSchema.describe('Network body master switch and legacy source mirrors.'),
113
111
  });
@@ -198,6 +198,12 @@ export interface SceneModelItem {
198
198
  breathDepth: number;
199
199
  /** MToon material fine-tuning, VRM only. */
200
200
  mtoon: MToonTuning;
201
+ /**
202
+ * Live2D only: this model rides another one as an item (VTS's Live2D Items) — its placement
203
+ * reads as an offset from the anchor, and `depth` says where it paints in the parent's stack.
204
+ * Null stands alone; a VRM instance ignores it, keeping it across a format switch.
205
+ */
206
+ attach: Attach | null;
201
207
  }
202
208
  export declare const OBJECT_SPACES: readonly ["2d", "3d"];
203
209
  export type ObjectSpace = (typeof OBJECT_SPACES)[number];
@@ -275,11 +281,41 @@ export interface AttachElasticity {
275
281
  damping: number;
276
282
  maxSpeed: number;
277
283
  }
284
+ /**
285
+ * How deep a pinned item paints in its parent — VTube Studio's Between-Layer Item Pinning, as one
286
+ * value: the two ends clear the parent's whole stack, and `artMesh` slots the item in directly
287
+ * above that ArtMesh, so every parent mesh over it paints on top. Ids rather than positions,
288
+ * because Cubism re-sorts the stack as motions play, and an id carries to any rig that has it.
289
+ * Live2D parents only: on a VRM an item always paints in front.
290
+ */
291
+ export type AttachDepth = {
292
+ kind: 'front';
293
+ } | {
294
+ kind: 'behind';
295
+ } | {
296
+ kind: 'artMesh';
297
+ id: string;
298
+ };
299
+ /**
300
+ * VTube Studio's front/back splitting: the item is cut at one of its **own** ArtMeshes, and
301
+ * everything it paints below that goes to a second depth in the parent — so the parent's meshes
302
+ * in between wrap over it (a scarf in front of the chest and behind the hair). Live2D items only.
303
+ */
304
+ export interface AttachSplit {
305
+ /** ArtMesh of the item it is cut at; the item paints everything under this as its back half. */
306
+ itemArtMesh: string;
307
+ /** How deep the back half paints — the same control as the item's own depth. */
308
+ depth: AttachDepth;
309
+ }
278
310
  export interface Attach {
279
311
  /** Always a model instance in the same space; anything else is cleared on load. */
280
312
  parentInstanceId: string;
281
313
  anchor: AttachAnchor;
282
314
  followRotation: boolean;
315
+ /** Where it paints in the parent's ArtMesh stack; `front` is the plain "on top of the model". */
316
+ depth: AttachDepth;
317
+ /** Front/back splitting, independent of `depth`; null keeps the item in one piece. */
318
+ split: AttachSplit | null;
283
319
  headAngle: AttachHeadAngle | null;
284
320
  elasticity: AttachElasticity | null;
285
321
  }
@@ -1016,12 +1052,15 @@ export type InstanceCapability = 'motions' | 'expressions' | 'placement-2d' | 'p
1016
1052
  | 'breath'
1017
1053
  /** `expression.setWeight`; Cubism expressions carry no user-settable weight, so Live2D lacks it. */
1018
1054
  | 'expression-weights'
1019
- /** `speech.play` lip-sync; engine-dependent, so gate on the runtime, not the format. */
1055
+ /**
1056
+ * `speech.play` lip-sync. Loaded Live2D models only; 0.53 and earlier hosts also omit it under
1057
+ * the Pixi engine, so gate on the runtime, not the format.
1058
+ */
1020
1059
  | 'speech'
1021
1060
  /**
1022
1061
  * Draws through the 3D post chain, so the scene's look rows (tone mapping, exposure, LUT)
1023
- * reach it. Always on VRM; on Live2D only under the `three` renderer, so read it off the
1024
- * runtime rather than the format.
1062
+ * reach it. Every model; 0.53 and earlier hosts omit it on Pixi-engine Live2D, so read it off
1063
+ * the runtime rather than the format.
1025
1064
  */
1026
1065
  | 'post-fx';
1027
1066
  export interface InstanceRuntime {
@@ -1059,6 +1098,11 @@ export interface PlayingMotion {
1059
1098
  export interface AnchorOption {
1060
1099
  anchor: AttachAnchor;
1061
1100
  label: string;
1101
+ /**
1102
+ * Paint order when the list was taken, ascending from the backmost — what the depth control
1103
+ * orders its stops by. Live2D only, and absent from hosts older than depth pinning.
1104
+ */
1105
+ order?: number;
1062
1106
  }
1063
1107
  export type { ModelInfo } from '../values/model-info.ts';
1064
1108
  export interface Hotkey {
@@ -1116,8 +1160,6 @@ export interface ExpressionPersistence {
1116
1160
  export type JsonValue = string | number | boolean | null | JsonValue[] | {
1117
1161
  [key: string]: JsonValue;
1118
1162
  };
1119
- export declare const LIVE2D_ENGINES: readonly ["pixi", "three"];
1120
- export type Live2DEngine = (typeof LIVE2D_ENGINES)[number];
1121
1163
  export declare const TRACKING_SOURCE_IDS: readonly ["persona-ios", "ifacialmocap", "vts-ios"];
1122
1164
  export type TrackingSourceId = (typeof TRACKING_SOURCE_IDS)[number];
1123
1165
  /** Body-pose protocols. Every one so far is UDP with a configurable port. */
@@ -82,7 +82,6 @@ export const AUTOMATION_BOUNDARY_KINDS = [
82
82
  'load-model',
83
83
  ];
84
84
  // ---- Settings ------------------------------------------------------------------
85
- export const LIVE2D_ENGINES = ['pixi', 'three'];
86
85
  export const TRACKING_SOURCE_IDS = ['persona-ios', 'ifacialmocap', 'vts-ios'];
87
86
  /** Body-pose protocols. Every one so far is UDP with a configurable port. */
88
87
  export const POSE_SOURCE_IDS = ['vmc', 'mocopi'];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@laplace.live/persona-sdk",
3
- "version": "1.15.0",
3
+ "version": "1.17.0",
4
4
  "description": "TypeScript SDK and wire schema for the LAPLACE Persona plugin API",
5
5
  "license": "MIT",
6
6
  "type": "module",