@laplace.live/persona-sdk 1.14.0 → 1.16.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;
@@ -178,6 +178,47 @@ export declare function contentAssetId(content: ObjectContent): string | null;
178
178
  * would z-fight by construction.
179
179
  */
180
180
  export declare function attachableParentFormat(space: ObjectSpace): ModelFormat;
181
+ /**
182
+ * Every instance that rides `instanceId`, directly or through a chain of pins — the set it can
183
+ * never pin to itself, or the two would ride each other in a circle. Includes `instanceId`.
184
+ */
185
+ export declare function pinRiders(items: readonly {
186
+ instanceId: string;
187
+ attach?: {
188
+ parentInstanceId: string;
189
+ } | null;
190
+ }[], instanceId: string): Set<string>;
191
+ /**
192
+ * Rows for the pin picker: every other Live2D model, each flagged when it already rides
193
+ * `instanceId` — picking one would put the two in a circle, so both apps offer it disabled
194
+ * rather than hidden. One rule, or the console offers a pin the desktop refuses.
195
+ */
196
+ export declare function pinnableParents(models: readonly {
197
+ instanceId: string;
198
+ ref: {
199
+ kind: string;
200
+ name: string;
201
+ };
202
+ }[], items: readonly {
203
+ instanceId: string;
204
+ attach?: {
205
+ parentInstanceId: string;
206
+ } | null;
207
+ }[], instanceId: string): {
208
+ instanceId: string;
209
+ name: string;
210
+ ridesThis: boolean;
211
+ }[];
212
+ /**
213
+ * The depth control's stops, back to front: behind the model, above each of its ArtMeshes in
214
+ * paint order, then in front of it all. One continuous run, so depth is a slider over the host's
215
+ * stack rather than a list of ArtMesh ids nobody can read.
216
+ */
217
+ export declare function depthStops(meshes: readonly string[]): AttachDepth[];
218
+ /** Which stop a depth sits at; an ArtMesh the host no longer has reads as the frontmost. */
219
+ export declare function depthStopIndex(meshes: readonly string[], depth: AttachDepth): number;
220
+ /** A fresh pin onto `parentInstanceId`: root anchor, in front of the model, no response tuning. */
221
+ export declare function defaultAttach(parentInstanceId: string): Attach;
181
222
  export declare const DEFAULT_HEAD_ANGLE: NonNullable<Attach['headAngle']>;
182
223
  export declare const ATTACH_MULTIPLIER_MIN = -2;
183
224
  export declare const ATTACH_MULTIPLIER_MAX = 2;
@@ -304,6 +304,71 @@ export function contentAssetId(content) {
304
304
  export function attachableParentFormat(space) {
305
305
  return space === '2d' ? 'live2d' : 'vrm';
306
306
  }
307
+ /**
308
+ * Every instance that rides `instanceId`, directly or through a chain of pins — the set it can
309
+ * never pin to itself, or the two would ride each other in a circle. Includes `instanceId`.
310
+ */
311
+ export function pinRiders(items, instanceId) {
312
+ const riders = new Set([instanceId]);
313
+ // One pass per item is enough for a chain of any depth: each pass adopts the next level down.
314
+ for (let i = 0; i < items.length; i++) {
315
+ let grew = false;
316
+ for (const item of items) {
317
+ const parent = item.attach?.parentInstanceId;
318
+ if (parent !== undefined && riders.has(parent) && !riders.has(item.instanceId)) {
319
+ riders.add(item.instanceId);
320
+ grew = true;
321
+ }
322
+ }
323
+ if (!grew)
324
+ break;
325
+ }
326
+ return riders;
327
+ }
328
+ /**
329
+ * Rows for the pin picker: every other Live2D model, each flagged when it already rides
330
+ * `instanceId` — picking one would put the two in a circle, so both apps offer it disabled
331
+ * rather than hidden. One rule, or the console offers a pin the desktop refuses.
332
+ */
333
+ export function pinnableParents(models, items, instanceId) {
334
+ const riders = pinRiders(items, instanceId);
335
+ const out = [];
336
+ for (const m of models) {
337
+ if (m.ref.kind !== 'live2d' || m.instanceId === instanceId)
338
+ continue;
339
+ out.push({ instanceId: m.instanceId, name: m.ref.name, ridesThis: riders.has(m.instanceId) });
340
+ }
341
+ return out;
342
+ }
343
+ /**
344
+ * The depth control's stops, back to front: behind the model, above each of its ArtMeshes in
345
+ * paint order, then in front of it all. One continuous run, so depth is a slider over the host's
346
+ * stack rather than a list of ArtMesh ids nobody can read.
347
+ */
348
+ export function depthStops(meshes) {
349
+ return [{ kind: 'behind' }, ...meshes.map((id) => ({ kind: 'artMesh', id })), { kind: 'front' }];
350
+ }
351
+ /** Which stop a depth sits at; an ArtMesh the host no longer has reads as the frontmost. */
352
+ export function depthStopIndex(meshes, depth) {
353
+ if (depth.kind === 'behind')
354
+ return 0;
355
+ if (depth.kind === 'front')
356
+ return meshes.length + 1;
357
+ const at = meshes.indexOf(depth.id);
358
+ return at < 0 ? meshes.length + 1 : at + 1;
359
+ }
360
+ /** A fresh pin onto `parentInstanceId`: root anchor, in front of the model, no response tuning. */
361
+ export function defaultAttach(parentInstanceId) {
362
+ return {
363
+ parentInstanceId,
364
+ anchor: { kind: 'root' },
365
+ followRotation: true,
366
+ depth: { kind: 'front' },
367
+ split: null,
368
+ headAngle: null,
369
+ elasticity: null,
370
+ };
371
+ }
307
372
  export const DEFAULT_HEAD_ANGLE = {
308
373
  multiplier: 1,
309
374
  parallaxX: 0,
@@ -31,10 +31,12 @@ export interface EventMap {
31
31
  active: string[];
32
32
  weights?: Record<string, number>;
33
33
  };
34
+ /** `oneShot`: the motion outranks the idle, so `motion.stop` would end it; hosts with `motion-stop` send it. */
34
35
  'motion.started': {
35
36
  instanceId: string;
36
37
  group: string;
37
38
  index: number;
39
+ oneShot?: boolean;
38
40
  };
39
41
  'motion.ended': {
40
42
  instanceId: string;
@@ -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>;
@@ -292,6 +302,16 @@ export interface MotionPlayingRequest {
292
302
  }
293
303
  export interface MotionPlayingResponse {
294
304
  playing: PlayingMotion | null;
305
+ /** `playing` outranks the idle — what `motion.stop` ends. Absent on hosts without `motion-stop`. */
306
+ oneShot?: boolean;
307
+ }
308
+ /** Fade the running motion out and let the idle resume; the idle itself is left alone. */
309
+ export interface MotionStopRequest {
310
+ instanceId?: string;
311
+ }
312
+ export interface MotionStopResponse {
313
+ /** A motion above the idle was playing and is now fading out. */
314
+ stopped: boolean;
295
315
  }
296
316
  /** Play a clip with lip-sync. One utterance per model: a new play supersedes the current one. */
297
317
  export interface SpeechPlayRequest {
@@ -800,6 +820,10 @@ export interface MethodMap {
800
820
  request: InstanceSetPlacementRequest;
801
821
  response: InstanceSetPlacementResponse;
802
822
  };
823
+ 'instance.attach': {
824
+ request: InstanceAttachRequest;
825
+ response: InstanceAttachResponse;
826
+ };
803
827
  'instance.setMToon': {
804
828
  request: InstanceSetMToonRequest;
805
829
  response: InstanceSetMToonResponse;
@@ -884,6 +908,11 @@ export interface MethodMap {
884
908
  request: MotionPlayingRequest;
885
909
  response: MotionPlayingResponse;
886
910
  };
911
+ /** Needs the `motion-stop` app capability. */
912
+ 'motion.stop': {
913
+ request: MotionStopRequest;
914
+ response: MotionStopResponse;
915
+ };
887
916
  /**
888
917
  * Lip-synced audio on models advertising the `speech` capability. The response means
889
918
  * playback started (else `invalid-state`); `speech.started`/`speech.ended` bracket it.
@@ -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 on the three engine only: elsewhere 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
  }
@@ -1005,7 +1041,7 @@ export interface ScenePatch {
1005
1041
  * App-level features a client gates on (never version-sniff): `hello` and
1006
1042
  * `app.info` report them — the per-app mirror of {@link InstanceRuntime.capabilities}.
1007
1043
  */
1008
- export declare const APP_CAPABILITIES: readonly ["storage", "speech", "automations", "controllers", "model-editing", "asset-inspection", "layer-effects", "scene-transitions", "area-lights", "spot-lights", "camera-follow-lights", "shadow-filters", "environment-map-model", "spawn", "tracking-lost"];
1044
+ export declare const APP_CAPABILITIES: readonly ["storage", "speech", "automations", "controllers", "model-editing", "asset-inspection", "layer-effects", "scene-transitions", "area-lights", "spot-lights", "camera-follow-lights", "shadow-filters", "environment-map-model", "spawn", "tracking-lost", "motion-stop"];
1009
1045
  export type AppCapability = (typeof APP_CAPABILITIES)[number];
1010
1046
  export declare function isAppCapability(v: unknown): v is AppCapability;
1011
1047
  /** What a loaded model instance can do; absent capabilities answer `unsupported-for-format`. */
@@ -1059,6 +1095,11 @@ export interface PlayingMotion {
1059
1095
  export interface AnchorOption {
1060
1096
  anchor: AttachAnchor;
1061
1097
  label: string;
1098
+ /**
1099
+ * Paint order when the list was taken, ascending from the backmost — what the depth control
1100
+ * orders its stops by. Live2D only, and absent from hosts older than depth pinning.
1101
+ */
1102
+ order?: number;
1062
1103
  }
1063
1104
  export type { ModelInfo } from '../values/model-info.ts';
1064
1105
  export interface Hotkey {
@@ -49,6 +49,8 @@ export const APP_CAPABILITIES = [
49
49
  'spawn',
50
50
  /** `instance.setIdle` takes `trackingLostBehavior`, `trackingLostMotion` and `trackingLostDelay`. */
51
51
  'tracking-lost',
52
+ /** `motion.stop`; `motion.playing` and `motion.started` carry `oneShot`. */
53
+ 'motion-stop',
52
54
  ];
53
55
  export function isAppCapability(v) {
54
56
  return isOneOf(v, APP_CAPABILITIES);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@laplace.live/persona-sdk",
3
- "version": "1.14.0",
3
+ "version": "1.16.0",
4
4
  "description": "TypeScript SDK and wire schema for the LAPLACE Persona plugin API",
5
5
  "license": "MIT",
6
6
  "type": "module",