@laplace.live/persona-sdk 0.18.1 → 1.0.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 (41) hide show
  1. package/README.md +1 -1
  2. package/dist/client/client.d.ts +1 -1
  3. package/dist/index.d.ts +7 -1
  4. package/dist/index.js +7 -1
  5. package/dist/values/bindings.d.ts +43 -0
  6. package/dist/values/bindings.js +1 -0
  7. package/dist/values/controller.d.ts +109 -0
  8. package/dist/values/controller.js +167 -0
  9. package/dist/values/curve.d.ts +5 -8
  10. package/dist/values/curve.js +16 -7
  11. package/dist/values/edit-history.d.ts +21 -0
  12. package/dist/values/edit-history.js +62 -0
  13. package/dist/values/effect-schema.d.ts +2 -2
  14. package/dist/values/effect-schema.js +1 -2
  15. package/dist/values/gltf-extensions.d.ts +20 -0
  16. package/dist/values/gltf-extensions.js +68 -0
  17. package/dist/values/guards.d.ts +2 -0
  18. package/dist/values/guards.js +5 -0
  19. package/dist/values/hotkeys.d.ts +6 -0
  20. package/dist/values/hotkeys.js +11 -0
  21. package/dist/values/labels.d.ts +2 -0
  22. package/dist/values/labels.js +5 -1
  23. package/dist/values/limits.d.ts +19 -0
  24. package/dist/values/limits.js +27 -0
  25. package/dist/values/locale.d.ts +2 -0
  26. package/dist/values/model-info.d.ts +66 -0
  27. package/dist/values/model-info.js +1 -0
  28. package/dist/values/stage-info.d.ts +27 -0
  29. package/dist/values/stage-info.js +1 -0
  30. package/dist/values/vrm-bindings.d.ts +14 -0
  31. package/dist/values/vrm-bindings.js +20 -0
  32. package/dist/wire/methods.d.ts +151 -5
  33. package/dist/wire/schemas.d.ts +46 -5
  34. package/dist/wire/schemas.js +12 -0
  35. package/dist/wire/types.d.ts +59 -58
  36. package/dist/wire/types.js +39 -11
  37. package/package.json +2 -16
  38. package/dist/effects.d.ts +0 -79
  39. package/dist/effects.js +0 -6
  40. package/dist/values/custom-effect.d.ts +0 -44
  41. package/dist/values/custom-effect.js +0 -136
package/README.md CHANGED
@@ -4,7 +4,7 @@ TypeScript SDK and wire schema for the [LAPLACE Persona](https://github.com/lapl
4
4
  plugin API — a token-authenticated WebSocket API for driving the avatar, scenes, expressions,
5
5
  motions, hotkeys, and tracking parameters from outside the app.
6
6
 
7
- Isomorphic: runs in Node ≥22, Bun, browsers, and OBS browser sources on the global `WebSocket`.
7
+ Isomorphic: runs in Node ≥24, Bun, browsers, and OBS browser sources on the global `WebSocket`.
8
8
  One runtime dependency ([`zod`](https://zod.dev), backing the wire schemas).
9
9
 
10
10
  ## Setup
@@ -5,7 +5,7 @@ import type { InjectTarget } from '../wire/types.ts';
5
5
  export type PersonaClientState = 'closed' | 'connecting' | 'open' | 'reconnecting';
6
6
  /**
7
7
  * The subset of the WebSocket API the client uses. The global `WebSocket`
8
- * (browsers, Node ≥22, Bun) and the `ws` package's client both satisfy it
8
+ * (browsers, Node ≥24, Bun) and the `ws` package's client both satisfy it
9
9
  * structurally, so `createWebSocket` implementations need no casts.
10
10
  */
11
11
  export interface WebSocketLike {
package/dist/index.d.ts CHANGED
@@ -1,15 +1,21 @@
1
1
  export * from './client/address.ts';
2
2
  export * from './client/client.ts';
3
3
  export * from './values/arkit.ts';
4
+ export * from './values/bindings.ts';
5
+ export * from './values/controller.ts';
4
6
  export * from './values/curve.ts';
5
- export * from './values/custom-effect.ts';
7
+ export * from './values/edit-history.ts';
6
8
  export * from './values/effect-schema.ts';
9
+ export * from './values/gltf-extensions.ts';
7
10
  export * from './values/guards.ts';
8
11
  export * from './values/hotkeys.ts';
9
12
  export * from './values/labels.ts';
10
13
  export * from './values/limits.ts';
11
14
  export * from './values/locale.ts';
15
+ export * from './values/model-info.ts';
16
+ export * from './values/stage-info.ts';
12
17
  export * from './values/terms.ts';
18
+ export * from './values/vrm-bindings.ts';
13
19
  export * from './wire/envelope.ts';
14
20
  export * from './wire/errors.ts';
15
21
  export * from './wire/events.ts';
package/dist/index.js CHANGED
@@ -3,15 +3,21 @@
3
3
  export * from "./client/address.js";
4
4
  export * from "./client/client.js";
5
5
  export * from "./values/arkit.js";
6
+ export * from "./values/bindings.js";
7
+ export * from "./values/controller.js";
6
8
  export * from "./values/curve.js";
7
- export * from "./values/custom-effect.js";
9
+ export * from "./values/edit-history.js";
8
10
  export * from "./values/effect-schema.js";
11
+ export * from "./values/gltf-extensions.js";
9
12
  export * from "./values/guards.js";
10
13
  export * from "./values/hotkeys.js";
11
14
  export * from "./values/labels.js";
12
15
  export * from "./values/limits.js";
13
16
  export * from "./values/locale.js";
17
+ export * from "./values/model-info.js";
18
+ export * from "./values/stage-info.js";
14
19
  export * from "./values/terms.js";
20
+ export * from "./values/vrm-bindings.js";
15
21
  export * from "./wire/envelope.js";
16
22
  export * from "./wire/errors.js";
17
23
  export * from "./wire/events.js";
@@ -0,0 +1,43 @@
1
+ import type { Curve } from './curve.ts';
2
+ import type { BindingTerm } from './terms.ts';
3
+ /** A saved input-to-parameter rule shared by the rig editor and plugin API. */
4
+ export interface Binding {
5
+ /** Summed at their weights before the range map; never empty. The first is the input the list files it under. */
6
+ terms: BindingTerm[];
7
+ output: string;
8
+ /** The rig author's own name for this binding, when their `.vtube.json` carried one. */
9
+ name?: string;
10
+ inRange: [number, number];
11
+ outRange: [number, number];
12
+ clampOutput: boolean;
13
+ smoothing: number;
14
+ autoBlink: boolean;
15
+ curve?: Curve;
16
+ }
17
+ /** A parameter the rig declares, as the picker lists it. */
18
+ export interface BindableParameter {
19
+ id: string;
20
+ min: number;
21
+ max: number;
22
+ default: number;
23
+ }
24
+ /** Metadata read from a rig's display and physics sidecars. */
25
+ export interface BindingEditorData {
26
+ connections: Binding[];
27
+ parameters: BindableParameter[];
28
+ metadata: {
29
+ display: {
30
+ parameterNames: Record<string, string>;
31
+ parameterGroups: Record<string, string>;
32
+ groups: {
33
+ id: string;
34
+ name: string;
35
+ }[];
36
+ } | null;
37
+ physics: {
38
+ inputs: string[];
39
+ outputs: string[];
40
+ } | null;
41
+ };
42
+ edited: boolean;
43
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,109 @@
1
+ /** Positive safe-integer controller profile id, independent of the current browser index. */
2
+ export type ControllerSlot = number;
3
+ /** Whether a value can identify a persistent controller profile. */
4
+ export declare function isControllerSlot(value: unknown): value is ControllerSlot;
5
+ /** Standard gamepad controls, using VTS's parameter names for the first controller. */
6
+ export declare const BASE_CONTROLLER_INPUT_NAMES: readonly ["ControllerStickLeftX", "ControllerStickLeftY", "ControllerStickRightX", "ControllerStickRightY", "ControllerStickPressLeft", "ControllerStickPressRight", "ControllerDPadX", "ControllerDPadY", "ControllerCross", "ControllerCircle", "ControllerSquare", "ControllerTriangle", "ControllerShoulderLeft", "ControllerShoulderRight", "ControllerTriggerLeft", "ControllerTriggerRight", "ControllerOptionLeft", "ControllerOptionRight", "ControllerHome"];
7
+ export type BaseControllerInputName = (typeof BASE_CONTROLLER_INPUT_NAMES)[number];
8
+ type NumberedControllerName<T extends string> = T extends `Controller${infer Control}` ? `Controller${number}${Control}` : never;
9
+ export type ControllerInputName = BaseControllerInputName | NumberedControllerName<BaseControllerInputName>;
10
+ /** Address a parameter on one controller; slot 1 retains its original VTS name. */
11
+ export declare function controllerInputName(base: BaseControllerInputName, slot: ControllerSlot): ControllerInputName;
12
+ /** Remove a controller parameter's slot prefix for shared labels and ranges. */
13
+ export declare function baseControllerInputName(full: ControllerInputName): BaseControllerInputName;
14
+ /** Whether an input names a supported controller parameter. */
15
+ export declare function isControllerInputName(v: string): v is ControllerInputName;
16
+ /** Stick and D-pad axes are signed, with positive Y pointing up; other controls are 0..1. */
17
+ export declare const BASE_CONTROLLER_INPUT_RANGES: {
18
+ readonly ControllerStickLeftX: readonly [-1, 1];
19
+ readonly ControllerStickLeftY: readonly [-1, 1];
20
+ readonly ControllerStickRightX: readonly [-1, 1];
21
+ readonly ControllerStickRightY: readonly [-1, 1];
22
+ readonly ControllerStickPressLeft: readonly [0, 1];
23
+ readonly ControllerStickPressRight: readonly [0, 1];
24
+ readonly ControllerDPadX: readonly [-1, 1];
25
+ readonly ControllerDPadY: readonly [-1, 1];
26
+ readonly ControllerCross: readonly [0, 1];
27
+ readonly ControllerCircle: readonly [0, 1];
28
+ readonly ControllerSquare: readonly [0, 1];
29
+ readonly ControllerTriangle: readonly [0, 1];
30
+ readonly ControllerShoulderLeft: readonly [0, 1];
31
+ readonly ControllerShoulderRight: readonly [0, 1];
32
+ readonly ControllerTriggerLeft: readonly [0, 1];
33
+ readonly ControllerTriggerRight: readonly [0, 1];
34
+ readonly ControllerOptionLeft: readonly [0, 1];
35
+ readonly ControllerOptionRight: readonly [0, 1];
36
+ readonly ControllerHome: readonly [0, 1];
37
+ };
38
+ /** Natural parameter range, shared by every controller profile. */
39
+ export declare function controllerInputRange(name: ControllerInputName): readonly [number, number];
40
+ /** VTS shortcut names differ from parameter names, especially shoulders, clicks and D-pad. */
41
+ export declare const BASE_CONTROLLER_TRIGGER_NAMES: readonly ["ControllerCross", "ControllerCircle", "ControllerSquare", "ControllerTriangle", "ControllerDpadUp", "ControllerDpadDown", "ControllerDpadLeft", "ControllerDpadRight", "ControllerLeftShoulder", "ControllerRightShoulder", "ControllerLeftTrigger", "ControllerRightTrigger", "ControllerLeftStickClick", "ControllerRightStickClick", "ControllerLeftStickUp", "ControllerLeftStickDown", "ControllerLeftStickLeft", "ControllerLeftStickRight", "ControllerRightStickUp", "ControllerRightStickDown", "ControllerRightStickLeft", "ControllerRightStickRight", "ControllerOptionLeft", "ControllerOptionRight", "ControllerHome"];
42
+ export type BaseControllerTriggerName = (typeof BASE_CONTROLLER_TRIGGER_NAMES)[number];
43
+ export type ControllerTriggerName = BaseControllerTriggerName | NumberedControllerName<BaseControllerTriggerName>;
44
+ /** Address a shortcut trigger on one controller; slot 1 retains its original VTS name. */
45
+ export declare function controllerTriggerName(base: BaseControllerTriggerName, slot: ControllerSlot): ControllerTriggerName;
46
+ /** Remove a controller trigger's slot prefix for its shared control label. */
47
+ export declare function baseControllerTriggerName(full: ControllerTriggerName): BaseControllerTriggerName;
48
+ /** Whether a shortcut trigger names a supported controller control. */
49
+ export declare function isControllerTriggerName(v: string): v is ControllerTriggerName;
50
+ /**
51
+ * The slot addressed by a controller parameter or shortcut trigger; null for any other string.
52
+ * The typed overload trusts a name this module built or validated — the template types alone
53
+ * also admit `Controller0Cross`, which would answer null.
54
+ */
55
+ export declare function controllerSlot(full: ControllerInputName | ControllerTriggerName): ControllerSlot;
56
+ export declare function controllerSlot(full: string): ControllerSlot | null;
57
+ /** Optional per-instance stage movement; posing remains in the model's input bindings. */
58
+ export interface ControllerMovementConfig {
59
+ enabled: boolean;
60
+ slot: ControllerSlot;
61
+ /** Metres/second for VRM; stage heights/second for Live2D. */
62
+ moveSpeed: number;
63
+ /** Degrees/second. */
64
+ turnSpeed: number;
65
+ /** Velocity response time in seconds; zero responds immediately. */
66
+ smoothing: number;
67
+ /** Play a VRM walking cycle while moving across the stage. */
68
+ walkEnabled: boolean;
69
+ /** Registered animation clip reference, or null until one is selected. */
70
+ walkClip: string | null;
71
+ /** Nominal metres/second of the walking cycle at normal playback speed. */
72
+ walkSpeed: number;
73
+ /** Turn a VRM avatar toward its movement direction. */
74
+ faceMovement: boolean;
75
+ }
76
+ export declare const DEFAULT_CONTROLLER_MOVEMENT: Readonly<ControllerMovementConfig>;
77
+ /** Heal saved movement or a merged API patch using the same limits as the controls. */
78
+ export declare function healControllerMovement(raw: unknown): ControllerMovementConfig;
79
+ /** Analog buttons (triggers, pressure-sensitive faces) count as pressed from here. */
80
+ export declare const BUTTON_PRESS_THRESHOLD = 0.5;
81
+ /** Radial stick dead zone of a profile that has never been tuned. */
82
+ export declare const DEFAULT_CONTROLLER_DEAD_ZONE = 0.15;
83
+ export declare const CONTROLLER_DEAD_ZONE_MAX = 0.5;
84
+ /** Saved controller profile names and per-device tuning, exposed to remote editors. */
85
+ export interface ControllerProfileInfo {
86
+ slot: ControllerSlot;
87
+ deviceId: string;
88
+ name?: string;
89
+ /** Radial stick dead zone, 0..0.5; absent until tuned, meaning `DEFAULT_CONTROLLER_DEAD_ZONE`. */
90
+ deadZone?: number;
91
+ }
92
+ export interface ControllerConfig {
93
+ enabled: boolean;
94
+ profiles: ControllerProfileInfo[];
95
+ }
96
+ /** Stage-owned input sample; hardware is never polled by the remote console. */
97
+ export interface ControllerState {
98
+ profiles: ControllerProfileInfo[];
99
+ devices: {
100
+ id: string;
101
+ index: number;
102
+ mapping: string;
103
+ slot: ControllerSlot | null;
104
+ }[];
105
+ inputs: Partial<Record<ControllerInputName, number>>;
106
+ pressed: ControllerTriggerName[];
107
+ assigningSlot: ControllerSlot | null;
108
+ }
109
+ export {};
@@ -0,0 +1,167 @@
1
+ import { finiteOr, isRecord, nonEmptyString } from "./guards.js";
2
+ import { clamp } from "./limits.js";
3
+ /** Whether a value can identify a persistent controller profile. */
4
+ export function isControllerSlot(value) {
5
+ return typeof value === 'number' && Number.isSafeInteger(value) && value > 0;
6
+ }
7
+ /** Standard gamepad controls, using VTS's parameter names for the first controller. */
8
+ export const BASE_CONTROLLER_INPUT_NAMES = [
9
+ 'ControllerStickLeftX',
10
+ 'ControllerStickLeftY',
11
+ 'ControllerStickRightX',
12
+ 'ControllerStickRightY',
13
+ 'ControllerStickPressLeft',
14
+ 'ControllerStickPressRight',
15
+ 'ControllerDPadX',
16
+ 'ControllerDPadY',
17
+ 'ControllerCross',
18
+ 'ControllerCircle',
19
+ 'ControllerSquare',
20
+ 'ControllerTriangle',
21
+ 'ControllerShoulderLeft',
22
+ 'ControllerShoulderRight',
23
+ 'ControllerTriggerLeft',
24
+ 'ControllerTriggerRight',
25
+ 'ControllerOptionLeft',
26
+ 'ControllerOptionRight',
27
+ 'ControllerHome',
28
+ ];
29
+ /** Slot 1 keeps the bare VTS name; every other slot inserts its number after `Controller`. */
30
+ function slotPrefixed(base, slot) {
31
+ if (!isControllerSlot(slot))
32
+ throw new RangeError('controller profile id must be a positive safe integer');
33
+ return slot === 1 ? base : `Controller${String(slot)}${base.slice('Controller'.length)}`;
34
+ }
35
+ const SLOT_PREFIX = /^Controller\d+/;
36
+ /** Address a parameter on one controller; slot 1 retains its original VTS name. */
37
+ export function controllerInputName(base, slot) {
38
+ return slotPrefixed(base, slot);
39
+ }
40
+ /** Remove a controller parameter's slot prefix for shared labels and ranges. */
41
+ export function baseControllerInputName(full) {
42
+ return full.replace(SLOT_PREFIX, 'Controller');
43
+ }
44
+ const CONTROLLER_INPUT_SET = new Set(BASE_CONTROLLER_INPUT_NAMES);
45
+ function controllerParts(full) {
46
+ const match = /^Controller([1-9]\d*)?([A-Za-z]+)$/.exec(full);
47
+ if (match === null)
48
+ return null;
49
+ const slot = match[1] === undefined ? 1 : Number(match[1]);
50
+ if (!isControllerSlot(slot) || (match[1] !== undefined && slot === 1))
51
+ return null;
52
+ return { slot, base: `Controller${match[2] ?? ''}` };
53
+ }
54
+ /** Whether an input names a supported controller parameter. */
55
+ export function isControllerInputName(v) {
56
+ const parts = controllerParts(v);
57
+ return parts !== null && CONTROLLER_INPUT_SET.has(parts.base);
58
+ }
59
+ /** Stick and D-pad axes are signed, with positive Y pointing up; other controls are 0..1. */
60
+ export const BASE_CONTROLLER_INPUT_RANGES = {
61
+ ControllerStickLeftX: [-1, 1],
62
+ ControllerStickLeftY: [-1, 1],
63
+ ControllerStickRightX: [-1, 1],
64
+ ControllerStickRightY: [-1, 1],
65
+ ControllerStickPressLeft: [0, 1],
66
+ ControllerStickPressRight: [0, 1],
67
+ ControllerDPadX: [-1, 1],
68
+ ControllerDPadY: [-1, 1],
69
+ ControllerCross: [0, 1],
70
+ ControllerCircle: [0, 1],
71
+ ControllerSquare: [0, 1],
72
+ ControllerTriangle: [0, 1],
73
+ ControllerShoulderLeft: [0, 1],
74
+ ControllerShoulderRight: [0, 1],
75
+ ControllerTriggerLeft: [0, 1],
76
+ ControllerTriggerRight: [0, 1],
77
+ ControllerOptionLeft: [0, 1],
78
+ ControllerOptionRight: [0, 1],
79
+ ControllerHome: [0, 1],
80
+ };
81
+ /** Natural parameter range, shared by every controller profile. */
82
+ export function controllerInputRange(name) {
83
+ return BASE_CONTROLLER_INPUT_RANGES[baseControllerInputName(name)];
84
+ }
85
+ /** VTS shortcut names differ from parameter names, especially shoulders, clicks and D-pad. */
86
+ export const BASE_CONTROLLER_TRIGGER_NAMES = [
87
+ 'ControllerCross',
88
+ 'ControllerCircle',
89
+ 'ControllerSquare',
90
+ 'ControllerTriangle',
91
+ 'ControllerDpadUp',
92
+ 'ControllerDpadDown',
93
+ 'ControllerDpadLeft',
94
+ 'ControllerDpadRight',
95
+ 'ControllerLeftShoulder',
96
+ 'ControllerRightShoulder',
97
+ 'ControllerLeftTrigger',
98
+ 'ControllerRightTrigger',
99
+ 'ControllerLeftStickClick',
100
+ 'ControllerRightStickClick',
101
+ 'ControllerLeftStickUp',
102
+ 'ControllerLeftStickDown',
103
+ 'ControllerLeftStickLeft',
104
+ 'ControllerLeftStickRight',
105
+ 'ControllerRightStickUp',
106
+ 'ControllerRightStickDown',
107
+ 'ControllerRightStickLeft',
108
+ 'ControllerRightStickRight',
109
+ 'ControllerOptionLeft',
110
+ 'ControllerOptionRight',
111
+ 'ControllerHome',
112
+ ];
113
+ /** Address a shortcut trigger on one controller; slot 1 retains its original VTS name. */
114
+ export function controllerTriggerName(base, slot) {
115
+ return slotPrefixed(base, slot);
116
+ }
117
+ /** Remove a controller trigger's slot prefix for its shared control label. */
118
+ export function baseControllerTriggerName(full) {
119
+ return full.replace(SLOT_PREFIX, 'Controller');
120
+ }
121
+ const CONTROLLER_TRIGGER_SET = new Set(BASE_CONTROLLER_TRIGGER_NAMES);
122
+ /** Whether a shortcut trigger names a supported controller control. */
123
+ export function isControllerTriggerName(v) {
124
+ const parts = controllerParts(v);
125
+ return parts !== null && CONTROLLER_TRIGGER_SET.has(parts.base);
126
+ }
127
+ export function controllerSlot(full) {
128
+ const parts = controllerParts(full);
129
+ return parts !== null && (CONTROLLER_INPUT_SET.has(parts.base) || CONTROLLER_TRIGGER_SET.has(parts.base))
130
+ ? parts.slot
131
+ : null;
132
+ }
133
+ const CONTROLLER_MOVE_SPEED_MAX = 10;
134
+ const CONTROLLER_TURN_SPEED_MAX = 720;
135
+ const CONTROLLER_MOVEMENT_SMOOTHING_MAX = 0.5;
136
+ export const DEFAULT_CONTROLLER_MOVEMENT = {
137
+ enabled: false,
138
+ slot: 1,
139
+ moveSpeed: 0.5,
140
+ turnSpeed: 90,
141
+ smoothing: 0.12,
142
+ walkEnabled: false,
143
+ walkClip: null,
144
+ walkSpeed: 1,
145
+ faceMovement: false,
146
+ };
147
+ /** Heal saved movement or a merged API patch using the same limits as the controls. */
148
+ export function healControllerMovement(raw) {
149
+ const r = isRecord(raw) ? raw : {};
150
+ const d = DEFAULT_CONTROLLER_MOVEMENT;
151
+ return {
152
+ enabled: typeof r.enabled === 'boolean' ? r.enabled : d.enabled,
153
+ slot: isControllerSlot(r.slot) ? r.slot : d.slot,
154
+ moveSpeed: clamp(finiteOr(r.moveSpeed, d.moveSpeed), 0, CONTROLLER_MOVE_SPEED_MAX),
155
+ turnSpeed: clamp(finiteOr(r.turnSpeed, d.turnSpeed), 0, CONTROLLER_TURN_SPEED_MAX),
156
+ smoothing: clamp(finiteOr(r.smoothing, d.smoothing), 0, CONTROLLER_MOVEMENT_SMOOTHING_MAX),
157
+ walkEnabled: typeof r.walkEnabled === 'boolean' ? r.walkEnabled : d.walkEnabled,
158
+ walkClip: nonEmptyString(r.walkClip),
159
+ walkSpeed: clamp(finiteOr(r.walkSpeed, d.walkSpeed), 0.1, 10),
160
+ faceMovement: typeof r.faceMovement === 'boolean' ? r.faceMovement : d.faceMovement,
161
+ };
162
+ }
163
+ /** Analog buttons (triggers, pressure-sensitive faces) count as pressed from here. */
164
+ export const BUTTON_PRESS_THRESHOLD = 0.5;
165
+ /** Radial stick dead zone of a profile that has never been tuned. */
166
+ export const DEFAULT_CONTROLLER_DEAD_ZONE = 0.15;
167
+ export const CONTROLLER_DEAD_ZONE_MAX = 0.5;
@@ -104,12 +104,8 @@ export declare function withBezierHandles(keyframes: Keyframe[]): Keyframe[];
104
104
  * The segment leaving `index` shaped as an ease-in-out: control points at the thirds, each held
105
105
  * at its own anchor's height, so the curve leaves flat and arrives flat.
106
106
  *
107
- * What an editor's Bezier button applies, and the one place seeding is *meant* to change the
108
- * curve pressing the button is an authoring action, and a mode switch that visibly did nothing
109
- * reads as a broken button. `withBezierHandles` is the opposite: it repairs, so it stays inert.
110
- *
111
- * A segment already carrying both handles is returned untouched, so linear → bezier gives back
112
- * the shape that was there rather than flattening it into a fresh ease.
107
+ * An authoring seed; `withBezierHandles` repairs without changing the shape. A segment already
108
+ * carrying both handles is returned untouched.
113
109
  */
114
110
  export declare function easeBezierSegment(keyframes: Keyframe[], index: number): Keyframe[];
115
111
  /**
@@ -158,10 +154,11 @@ export declare function sanitizeCurve(v: unknown): Curve | null;
158
154
  */
159
155
  export declare function identityCurve(): Curve;
160
156
  /** A ready-made response shape that replaces the whole curve; `linear` is the identity. */
161
- export type CurvePresetId = 'linear' | 'easeIn' | 'easeOut' | 'sCurve' | 'threshold' | 'steps';
157
+ export type CurvePresetId = 'linear' | 'step' | 'invertStep' | 'bezier' | 'easeIn' | 'easeOut' | 'sCurve' | 'threshold' | 'steps';
162
158
  export declare const CURVE_PRESET_IDS: readonly CurvePresetId[];
163
159
  /**
164
- * The shapes VBridger's shipped presets reach for, transcribed from their Unity curves
160
+ * Fresh whole-curve shapes: endpoint holds, a Bezier ease-in-out, and shapes from VBridger's presets.
161
+ * The remaining shapes are transcribed from its Unity curves
165
162
  * (references/vbridger-desktop/saves/, `store[].curve.keys`). A Hermite key pair converts to a
166
163
  * Bezier segment exactly — `P1 = A + Δx/3·(1, m₀)`, `P2 = B − Δx/3·(1, m₁)` — so these evaluate as
167
164
  * the originals do:
@@ -171,12 +171,8 @@ export function withBezierHandles(keyframes) {
171
171
  * The segment leaving `index` shaped as an ease-in-out: control points at the thirds, each held
172
172
  * at its own anchor's height, so the curve leaves flat and arrives flat.
173
173
  *
174
- * What an editor's Bezier button applies, and the one place seeding is *meant* to change the
175
- * curve pressing the button is an authoring action, and a mode switch that visibly did nothing
176
- * reads as a broken button. `withBezierHandles` is the opposite: it repairs, so it stays inert.
177
- *
178
- * A segment already carrying both handles is returned untouched, so linear → bezier gives back
179
- * the shape that was there rather than flattening it into a fresh ease.
174
+ * An authoring seed; `withBezierHandles` repairs without changing the shape. A segment already
175
+ * carrying both handles is returned untouched.
180
176
  */
181
177
  export function easeBezierSegment(keyframes, index) {
182
178
  const left = keyframes[index];
@@ -328,6 +324,9 @@ export function identityCurve() {
328
324
  }
329
325
  export const CURVE_PRESET_IDS = [
330
326
  'linear',
327
+ 'step',
328
+ 'invertStep',
329
+ 'bezier',
331
330
  'easeIn',
332
331
  'easeOut',
333
332
  'sCurve',
@@ -335,7 +334,8 @@ export const CURVE_PRESET_IDS = [
335
334
  'steps',
336
335
  ];
337
336
  /**
338
- * The shapes VBridger's shipped presets reach for, transcribed from their Unity curves
337
+ * Fresh whole-curve shapes: endpoint holds, a Bezier ease-in-out, and shapes from VBridger's presets.
338
+ * The remaining shapes are transcribed from its Unity curves
339
339
  * (references/vbridger-desktop/saves/, `store[].curve.keys`). A Hermite key pair converts to a
340
340
  * Bezier segment exactly — `P1 = A + Δx/3·(1, m₀)`, `P2 = B − Δx/3·(1, m₁)` — so these evaluate as
341
341
  * the originals do:
@@ -351,6 +351,15 @@ export function curvePreset(id) {
351
351
  switch (id) {
352
352
  case 'linear':
353
353
  return identityCurve();
354
+ case 'step':
355
+ case 'invertStep':
356
+ case 'bezier': {
357
+ const keyframes = [
358
+ { anchor: { x: 0, y: 0 }, interpolation: id },
359
+ { anchor: { x: 1, y: 1 }, interpolation: 'linear' },
360
+ ];
361
+ return { keyframes: id === 'bezier' ? easeBezierSegment(keyframes, 0) : keyframes };
362
+ }
354
363
  case 'easeIn':
355
364
  return {
356
365
  keyframes: [
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Bounded undo/redo over whole-value snapshots, keyed by content: a push equal to the cursor
3
+ * entry is dropped, and a push after an undo truncates the redo tail. Entries are cloned on
4
+ * capture and on restore, so a caller mutating what it holds can never rewrite history.
5
+ */
6
+ export interface EditHistory<T> {
7
+ /** Drop everything and seed a single baseline entry. */
8
+ reset(value: T): void;
9
+ /** Record `value` if its content differs from the cursor entry; truncates the redo tail. */
10
+ push(value: T): void;
11
+ /** Step back; a fresh clone, or null at the floor. */
12
+ undo(): T | null;
13
+ /** Step forward; mirror of {@link EditHistory.undo}. */
14
+ redo(): T | null;
15
+ /**
16
+ * Rewrite the run of entries around the cursor that share its `runKey`, outward until the key
17
+ * changes on either side — a fold, never a step, so undo and redo across the run all carry it.
18
+ */
19
+ amend(runKey: (value: T) => string, rewrite: (value: T) => void): void;
20
+ }
21
+ export declare function createEditHistory<T>(limit?: number): EditHistory<T>;
@@ -0,0 +1,62 @@
1
+ const EDIT_HISTORY_LIMIT = 50;
2
+ export function createEditHistory(limit = EDIT_HISTORY_LIMIT) {
3
+ let stack = [];
4
+ let cursor = -1;
5
+ const entryOf = (value, key = JSON.stringify(value)) => ({ value: structuredClone(value), key });
6
+ return {
7
+ reset(value) {
8
+ stack = [entryOf(value)];
9
+ cursor = 0;
10
+ },
11
+ push(value) {
12
+ // Keyed before it is cloned: a commit that changed nothing — a field blurred on the value it
13
+ // already held, or a restore committed back — is not a step, and the second is what keeps
14
+ // the redo tail alive.
15
+ const key = JSON.stringify(value);
16
+ const cur = stack[cursor];
17
+ if (cur && cur.key === key)
18
+ return;
19
+ stack.length = cursor + 1;
20
+ stack.push(entryOf(value, key));
21
+ cursor++;
22
+ if (stack.length > limit) {
23
+ stack.shift();
24
+ cursor--;
25
+ }
26
+ },
27
+ undo() {
28
+ if (cursor <= 0)
29
+ return null;
30
+ cursor--;
31
+ return structuredClone(stack[cursor].value);
32
+ },
33
+ redo() {
34
+ if (cursor >= stack.length - 1)
35
+ return null;
36
+ cursor++;
37
+ return structuredClone(stack[cursor].value);
38
+ },
39
+ amend(runKey, rewrite) {
40
+ const cur = stack[cursor];
41
+ if (!cur)
42
+ return;
43
+ const from = runKey(cur.value);
44
+ const apply = (e) => {
45
+ rewrite(e.value);
46
+ e.key = JSON.stringify(e.value);
47
+ };
48
+ for (let i = cursor; i >= 0; i--) {
49
+ const entry = stack[i];
50
+ if (runKey(entry.value) !== from)
51
+ break;
52
+ apply(entry);
53
+ }
54
+ for (let i = cursor + 1; i < stack.length; i++) {
55
+ const entry = stack[i];
56
+ if (runKey(entry.value) !== from)
57
+ break;
58
+ apply(entry);
59
+ }
60
+ },
61
+ };
62
+ }
@@ -33,9 +33,9 @@ export declare const POST_EFFECT_KEYS: readonly ToggleEffectKey[];
33
33
  /** The canonical `effectLayers`: registry order, junk dropped, every switched-on effect included. */
34
34
  export declare function effectLayerKeys(listed: readonly unknown[], effects: Pick<SceneEffects, ToggleEffectKey>): ToggleEffectKey[];
35
35
  /**
36
- * Composition order, bottom to top, with user-authored effects as one `'custom'` group.
36
+ * Composition order, bottom to top.
37
37
  * Display-only: the desktop chain's `rebuild()` is the order that runs — a stage moved there moves here too.
38
38
  */
39
- export declare const EFFECT_STACK_ORDER: readonly (ToggleEffectKey | 'custom')[];
39
+ export declare const EFFECT_STACK_ORDER: readonly ToggleEffectKey[];
40
40
  /** Every registry effect at its defaults — the derived half of a fresh {@link SceneEffects}. */
41
41
  export declare function defaultToggleEffects(): Pick<SceneEffects, ToggleEffectKey>;
@@ -185,7 +185,7 @@ export function effectLayerKeys(listed, effects) {
185
185
  return TOGGLE_EFFECT_KEYS.filter(key => listed.includes(key) || effects[key].enabled);
186
186
  }
187
187
  /**
188
- * Composition order, bottom to top, with user-authored effects as one `'custom'` group.
188
+ * Composition order, bottom to top.
189
189
  * Display-only: the desktop chain's `rebuild()` is the order that runs — a stage moved there moves here too.
190
190
  */
191
191
  export const EFFECT_STACK_ORDER = [
@@ -202,7 +202,6 @@ export const EFFECT_STACK_ORDER = [
202
202
  'pixelate',
203
203
  'glitch',
204
204
  'droplets',
205
- 'custom',
206
205
  'grain',
207
206
  'vignette',
208
207
  ];
@@ -0,0 +1,20 @@
1
+ /** Which loader read the file: a VRM avatar, or a plain glb — a prop or an environment set. */
2
+ export type GltfLoadPath = 'vrm' | 'gltf';
3
+ /** The app's wardrobe extension, read on avatars — `renderer/vrm/gltf-outfit.ts`. */
4
+ export declare const LAPLACE_OUTFIT = "LAPLACE_outfit";
5
+ /** The app's room extension, read on props and sets — `renderer/vrm/gltf-extension.ts`. */
6
+ export declare const LAPLACE_ENVIRONMENT = "LAPLACE_environment";
7
+ /** `GLTFLoader` extensions that throw at parse without a decoder — and the app sets none. */
8
+ export declare const DECODER_EXTENSIONS: string[];
9
+ /** Per loader; the app's own extensions ride with the loader that reads them. */
10
+ export declare const SUPPORTED_EXTENSIONS: Record<GltfLoadPath, ReadonlySet<string>>;
11
+ /** The extensions a glTF root declares (`extensionsUsed`) or carries itself, deduped in file order. */
12
+ export declare function declaredExtensions(json: {
13
+ extensionsUsed?: unknown;
14
+ extensions?: unknown;
15
+ }): string[];
16
+ /** A file's declared extensions split by what its loader honours, each side in file order. */
17
+ export declare function partitionExtensions(declared: readonly string[], path: GltfLoadPath): {
18
+ supported: string[];
19
+ unsupported: string[];
20
+ };