@scenography/engine 0.2.0 → 0.3.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.
package/dist/index.d.cts CHANGED
@@ -10,6 +10,13 @@ interface FocusOptions {
10
10
  widthFraction?: number;
11
11
  /** Tween duration in ms. Default `900`. */
12
12
  duration?: number;
13
+ /**
14
+ * Minimum camera distance from the piece, in world units. When the framing that
15
+ * fits the piece would sit closer, the camera pulls back to this instead (the
16
+ * piece stays centered, just smaller). Default `0` (no minimum). Useful on close
17
+ * so a small piece doesn't leave the visitor glued to the wall.
18
+ */
19
+ minDistance?: number;
13
20
  }
14
21
  /**
15
22
  * Public camera-framing API exposed on the gallery context as `ctx.camera`.
@@ -23,6 +30,11 @@ interface CameraApi {
23
30
  centerArtwork(id: string, options?: FocusOptions): void;
24
31
  /** Drop any active framing watch and hand control fully back to the user. */
25
32
  release(): void;
33
+ /**
34
+ * Animate the camera back to the scene's start position and orientation — a
35
+ * "home" for visitors who lose their way and want to return to the entrance.
36
+ */
37
+ reset(): void;
26
38
  /** Whether a framing tween is in progress (e.g. to suppress click-to-select). */
27
39
  isAnimating(): boolean;
28
40
  }
@@ -93,6 +105,97 @@ interface SkyApi {
93
105
  getAzimuth(): number;
94
106
  }
95
107
  //#endregion
108
+ //#region src/input/InputBus.d.ts
109
+ /**
110
+ * The shared analog-input channel: the current movement and look intent, each
111
+ * axis in `-1..1` and held until a producer changes it. Producers (on-screen
112
+ * joysticks in the overlay, a gamepad…) write to it; whatever camera system is
113
+ * active reads it and interprets it its own way — free-roam walks with it, a
114
+ * fixed camera might only use the look axes to glance around. This is what makes
115
+ * the joysticks reusable across camera types. Pure state — no three.js.
116
+ */
117
+ declare class InputBus {
118
+ /** Forward (`+1`) / backward (`-1`). */
119
+ moveForward: number;
120
+ /** Strafe right (`+1`) / left (`-1`). */
121
+ moveStrafe: number;
122
+ /** Look right (`+1`) / left (`-1`). */
123
+ lookX: number;
124
+ /** Look up (`+1`) / down (`-1`). */
125
+ lookY: number;
126
+ /** Set the movement intent (forward, strafe). */
127
+ setMove(forward: number, strafe?: number): void;
128
+ /** Set the look intent (turn, pitch). */
129
+ setLook(x: number, y?: number): void;
130
+ }
131
+ //#endregion
132
+ //#region src/input/controlSettings.d.ts
133
+ /**
134
+ * The visitor's camera-control preferences: a set of orthogonal capability flags
135
+ * plus a derived, friendly "mode". Pure state — no three.js, no DOM. The engine
136
+ * wires the flags to effects (gyroscope head-look, stereo split); consumers read
137
+ * them (the analog look-invert axes); the overlay drives it from a mode picker
138
+ * (the `ScanEye` button) and the settings panel. Persistence and device
139
+ * feature-detection live in those callers, not here, so this stays trivially
140
+ * unit-testable.
141
+ */
142
+ /**
143
+ * Orthogonal control capabilities. `gyro` / `splitScreen` / `joysticks` are what a
144
+ * mode fixes; the two `invertLook*` axes are independent look-feel preferences
145
+ * (some people feel the right stick inverted), kept separate from the mode.
146
+ */
147
+ interface ControlFlags {
148
+ /** Orient the camera from the device gyroscope (head-look). */
149
+ gyro: boolean;
150
+ /** Split the view into a stereo left/right eye pair (phone-in-a-headset). */
151
+ splitScreen: boolean;
152
+ /** Show the on-screen virtual joysticks. */
153
+ joysticks: boolean;
154
+ /** Invert the horizontal (turn) axis of analog look. */
155
+ invertLookX: boolean;
156
+ /** Invert the vertical (pitch) axis of analog look. */
157
+ invertLookY: boolean;
158
+ }
159
+ /** A named preset over the mode-defining flags; `custom` when none matches. */
160
+ type ControlMode = "touch" | "vr" | "cardboard" | "custom";
161
+ /** The three flags a mode fixes (the look-invert axes stay a separate preference). */
162
+ type ModeFlags = Pick<ControlFlags, "gyro" | "splitScreen" | "joysticks">;
163
+ /**
164
+ * Mode presets. Touch = two joysticks, no gyro. VR = the gyro aims and a single
165
+ * move joystick walks you, no split. Cardboard = gyro aims, stereo split, gaze
166
+ * select (no joysticks). The move-only single stick in VR is derived downstream
167
+ * from `gyro && !splitScreen`, so it needs no flag of its own.
168
+ */
169
+ declare const MODE_PRESETS: Record<Exclude<ControlMode, "custom">, ModeFlags>;
170
+ /** Default flags: plain touch, no look inversion. */
171
+ declare const DEFAULT_CONTROL_FLAGS: ControlFlags;
172
+ /** The mode a flag set represents, or `custom` when no preset matches. */
173
+ declare function modeOf(flags: ControlFlags): ControlMode;
174
+ /** Apply a mode's preset onto `flags`, leaving the look-invert axes untouched. */
175
+ declare function applyMode(flags: ControlFlags, mode: Exclude<ControlMode, "custom">): ControlFlags;
176
+ /**
177
+ * Live control-preferences state: holds the flags, derives the mode, and notifies
178
+ * subscribers on every real change. Callers apply the effects — the engine wires
179
+ * gyro and split, the overlay redraws — so this object is just the shared source
180
+ * of truth they all read and write.
181
+ */
182
+ declare class ControlSettings {
183
+ private flags;
184
+ private readonly subscribers;
185
+ constructor(initial?: Partial<ControlFlags>);
186
+ /** The current flags (a copy — mutate through `setMode` / `setFlag`). */
187
+ get(): ControlFlags;
188
+ /** The mode the current flags represent (`custom` when hand-tuned). */
189
+ getMode(): ControlMode;
190
+ /** Switch to a mode's preset (keeps the look-invert axes). No-op if unchanged. */
191
+ setMode(mode: Exclude<ControlMode, "custom">): void;
192
+ /** Set one flag. Flipping a mode-defining flag can drop you into `custom`. */
193
+ setFlag<K extends keyof ControlFlags>(key: K, value: ControlFlags[K]): void;
194
+ /** Subscribe to changes; returns an unsubscribe. Fires on every real change. */
195
+ subscribe(fn: (flags: ControlFlags) => void): () => void;
196
+ private replace;
197
+ }
198
+ //#endregion
96
199
  //#region src/plugin/CapabilityRegistry.d.ts
97
200
  /**
98
201
  * Shared runtime contracts between plugins, keyed by name. A plugin (or the
@@ -182,30 +285,6 @@ interface EnginePlugin<T = unknown> {
182
285
  controller?(elements: () => ReadonlyMap<string, Object3D>): ElementController;
183
286
  }
184
287
  //#endregion
185
- //#region src/input/InputBus.d.ts
186
- /**
187
- * The shared analog-input channel: the current movement and look intent, each
188
- * axis in `-1..1` and held until a producer changes it. Producers (on-screen
189
- * joysticks in the overlay, a gamepad…) write to it; whatever camera system is
190
- * active reads it and interprets it its own way — free-roam walks with it, a
191
- * fixed camera might only use the look axes to glance around. This is what makes
192
- * the joysticks reusable across camera types. Pure state — no three.js.
193
- */
194
- declare class InputBus {
195
- /** Forward (`+1`) / backward (`-1`). */
196
- moveForward: number;
197
- /** Strafe right (`+1`) / left (`-1`). */
198
- moveStrafe: number;
199
- /** Look right (`+1`) / left (`-1`). */
200
- lookX: number;
201
- /** Look up (`+1`) / down (`-1`). */
202
- lookY: number;
203
- /** Set the movement intent (forward, strafe). */
204
- setMove(forward: number, strafe?: number): void;
205
- /** Set the look intent (turn, pitch). */
206
- setLook(x: number, y?: number): void;
207
- }
208
- //#endregion
209
288
  //#region src/plugin/systemPlugin.d.ts
210
289
  /**
211
290
  * What the kernel hands a system on setup. A *system* plugin — unlike an element
@@ -227,6 +306,8 @@ interface SystemContext {
227
306
  renderer: WebGLRenderer;
228
307
  /** Shared analog input (joysticks feed it); systems interpret it. */
229
308
  input: InputBus;
309
+ /** The visitor's current control flags (e.g. look-invert axes), read live. */
310
+ controlFlags: () => ControlFlags;
230
311
  /** Capability registry for cross-plugin contracts (terrain, colliders…). */
231
312
  capabilities: CapabilityRegistry;
232
313
  /** Emit a gallery event. */
@@ -413,6 +494,25 @@ interface ControlsApi {
413
494
  /** Turn right (`+1`) / left (`-1`); optional look up (`+1`) / down (`-1`). */
414
495
  setLookInput(x: number, y?: number): void;
415
496
  }
497
+ /**
498
+ * The visitor's camera-control preferences (view mode + capability flags). The
499
+ * engine applies the effects that touch rendering/input — gyroscope head-look and
500
+ * the stereo split — as the flags change; the overlay drives it from the mode
501
+ * picker and settings, and reads it to show the right joysticks. See
502
+ * `ControlSettings`.
503
+ */
504
+ interface ViewControlsApi {
505
+ /** Current capability flags (a copy). */
506
+ get(): ControlFlags;
507
+ /** The mode the flags represent (`custom` when hand-tuned in settings). */
508
+ getMode(): ControlMode;
509
+ /** Switch to a mode's preset (keeps the look-invert axes). */
510
+ setMode(mode: Exclude<ControlMode, "custom">): void;
511
+ /** Set one capability flag (may drop the mode into `custom`). */
512
+ setFlag<K extends keyof ControlFlags>(key: K, value: ControlFlags[K]): void;
513
+ /** Subscribe to changes; returns an unsubscribe. */
514
+ subscribe(fn: (flags: ControlFlags) => void): () => void;
515
+ }
416
516
  /** Control the HDRI sky / environment at runtime, for behavior code. */
417
517
  interface EnvironmentApi {
418
518
  /** Set the visible sky background brightness (`0` = black). */
@@ -431,6 +531,8 @@ interface GalleryContext {
431
531
  readonly media: MediaController;
432
532
  /** Drive the camera externally (on-screen touch controls, etc.). */
433
533
  readonly controls: ControlsApi;
534
+ /** The visitor's camera-control preferences (view mode + capability flags). */
535
+ readonly viewControls: ViewControlsApi;
434
536
  /** Control the HDRI sky / environment at runtime. */
435
537
  readonly environment: EnvironmentApi;
436
538
  /** Drive the procedural sky's time of day. */
@@ -559,5 +661,5 @@ declare class InteractionController {
559
661
  private buildHighlight;
560
662
  }
561
663
  //#endregion
562
- export { type Antialias, type BuiltElement, COLLIDERS, type CameraApi, CapabilityRegistry, type Collider, type ControlsApi, EDGES, EYE_HEIGHT, type EdgeAnchor, type EdgeIndex, type ElementController, type EngineContext, type EnginePlugin, type EnvironmentApi, type FocusOptions, type Gallery, type GalleryContext, type GalleryOptions, InputBus, InteractionController, type LightSet, type LightSetOptions, type LightsApi, type MediaController, ROAM_CONTROL, type RenderOptions, type RoamControl, SceneManager, type SkyApi, type SystemContext, type SystemInstance, type SystemPlugin, TERRAIN, type TerrainCapability, type TerrainDefinition, type VideoApi, applyTexture, createGallery, isSystemPlugin };
664
+ export { type Antialias, type BuiltElement, COLLIDERS, type CameraApi, CapabilityRegistry, type Collider, type ControlFlags, type ControlMode, ControlSettings, type ControlsApi, DEFAULT_CONTROL_FLAGS, EDGES, EYE_HEIGHT, type EdgeAnchor, type EdgeIndex, type ElementController, type EngineContext, type EnginePlugin, type EnvironmentApi, type FocusOptions, type Gallery, type GalleryContext, type GalleryOptions, InputBus, InteractionController, type LightSet, type LightSetOptions, type LightsApi, MODE_PRESETS, type MediaController, ROAM_CONTROL, type RenderOptions, type RoamControl, SceneManager, type SkyApi, type SystemContext, type SystemInstance, type SystemPlugin, TERRAIN, type TerrainCapability, type TerrainDefinition, type VideoApi, applyMode, applyTexture, createGallery, isSystemPlugin, modeOf };
563
665
  //# sourceMappingURL=index.d.cts.map
package/dist/index.d.mts CHANGED
@@ -10,6 +10,13 @@ interface FocusOptions {
10
10
  widthFraction?: number;
11
11
  /** Tween duration in ms. Default `900`. */
12
12
  duration?: number;
13
+ /**
14
+ * Minimum camera distance from the piece, in world units. When the framing that
15
+ * fits the piece would sit closer, the camera pulls back to this instead (the
16
+ * piece stays centered, just smaller). Default `0` (no minimum). Useful on close
17
+ * so a small piece doesn't leave the visitor glued to the wall.
18
+ */
19
+ minDistance?: number;
13
20
  }
14
21
  /**
15
22
  * Public camera-framing API exposed on the gallery context as `ctx.camera`.
@@ -23,6 +30,11 @@ interface CameraApi {
23
30
  centerArtwork(id: string, options?: FocusOptions): void;
24
31
  /** Drop any active framing watch and hand control fully back to the user. */
25
32
  release(): void;
33
+ /**
34
+ * Animate the camera back to the scene's start position and orientation — a
35
+ * "home" for visitors who lose their way and want to return to the entrance.
36
+ */
37
+ reset(): void;
26
38
  /** Whether a framing tween is in progress (e.g. to suppress click-to-select). */
27
39
  isAnimating(): boolean;
28
40
  }
@@ -93,6 +105,97 @@ interface SkyApi {
93
105
  getAzimuth(): number;
94
106
  }
95
107
  //#endregion
108
+ //#region src/input/InputBus.d.ts
109
+ /**
110
+ * The shared analog-input channel: the current movement and look intent, each
111
+ * axis in `-1..1` and held until a producer changes it. Producers (on-screen
112
+ * joysticks in the overlay, a gamepad…) write to it; whatever camera system is
113
+ * active reads it and interprets it its own way — free-roam walks with it, a
114
+ * fixed camera might only use the look axes to glance around. This is what makes
115
+ * the joysticks reusable across camera types. Pure state — no three.js.
116
+ */
117
+ declare class InputBus {
118
+ /** Forward (`+1`) / backward (`-1`). */
119
+ moveForward: number;
120
+ /** Strafe right (`+1`) / left (`-1`). */
121
+ moveStrafe: number;
122
+ /** Look right (`+1`) / left (`-1`). */
123
+ lookX: number;
124
+ /** Look up (`+1`) / down (`-1`). */
125
+ lookY: number;
126
+ /** Set the movement intent (forward, strafe). */
127
+ setMove(forward: number, strafe?: number): void;
128
+ /** Set the look intent (turn, pitch). */
129
+ setLook(x: number, y?: number): void;
130
+ }
131
+ //#endregion
132
+ //#region src/input/controlSettings.d.ts
133
+ /**
134
+ * The visitor's camera-control preferences: a set of orthogonal capability flags
135
+ * plus a derived, friendly "mode". Pure state — no three.js, no DOM. The engine
136
+ * wires the flags to effects (gyroscope head-look, stereo split); consumers read
137
+ * them (the analog look-invert axes); the overlay drives it from a mode picker
138
+ * (the `ScanEye` button) and the settings panel. Persistence and device
139
+ * feature-detection live in those callers, not here, so this stays trivially
140
+ * unit-testable.
141
+ */
142
+ /**
143
+ * Orthogonal control capabilities. `gyro` / `splitScreen` / `joysticks` are what a
144
+ * mode fixes; the two `invertLook*` axes are independent look-feel preferences
145
+ * (some people feel the right stick inverted), kept separate from the mode.
146
+ */
147
+ interface ControlFlags {
148
+ /** Orient the camera from the device gyroscope (head-look). */
149
+ gyro: boolean;
150
+ /** Split the view into a stereo left/right eye pair (phone-in-a-headset). */
151
+ splitScreen: boolean;
152
+ /** Show the on-screen virtual joysticks. */
153
+ joysticks: boolean;
154
+ /** Invert the horizontal (turn) axis of analog look. */
155
+ invertLookX: boolean;
156
+ /** Invert the vertical (pitch) axis of analog look. */
157
+ invertLookY: boolean;
158
+ }
159
+ /** A named preset over the mode-defining flags; `custom` when none matches. */
160
+ type ControlMode = "touch" | "vr" | "cardboard" | "custom";
161
+ /** The three flags a mode fixes (the look-invert axes stay a separate preference). */
162
+ type ModeFlags = Pick<ControlFlags, "gyro" | "splitScreen" | "joysticks">;
163
+ /**
164
+ * Mode presets. Touch = two joysticks, no gyro. VR = the gyro aims and a single
165
+ * move joystick walks you, no split. Cardboard = gyro aims, stereo split, gaze
166
+ * select (no joysticks). The move-only single stick in VR is derived downstream
167
+ * from `gyro && !splitScreen`, so it needs no flag of its own.
168
+ */
169
+ declare const MODE_PRESETS: Record<Exclude<ControlMode, "custom">, ModeFlags>;
170
+ /** Default flags: plain touch, no look inversion. */
171
+ declare const DEFAULT_CONTROL_FLAGS: ControlFlags;
172
+ /** The mode a flag set represents, or `custom` when no preset matches. */
173
+ declare function modeOf(flags: ControlFlags): ControlMode;
174
+ /** Apply a mode's preset onto `flags`, leaving the look-invert axes untouched. */
175
+ declare function applyMode(flags: ControlFlags, mode: Exclude<ControlMode, "custom">): ControlFlags;
176
+ /**
177
+ * Live control-preferences state: holds the flags, derives the mode, and notifies
178
+ * subscribers on every real change. Callers apply the effects — the engine wires
179
+ * gyro and split, the overlay redraws — so this object is just the shared source
180
+ * of truth they all read and write.
181
+ */
182
+ declare class ControlSettings {
183
+ private flags;
184
+ private readonly subscribers;
185
+ constructor(initial?: Partial<ControlFlags>);
186
+ /** The current flags (a copy — mutate through `setMode` / `setFlag`). */
187
+ get(): ControlFlags;
188
+ /** The mode the current flags represent (`custom` when hand-tuned). */
189
+ getMode(): ControlMode;
190
+ /** Switch to a mode's preset (keeps the look-invert axes). No-op if unchanged. */
191
+ setMode(mode: Exclude<ControlMode, "custom">): void;
192
+ /** Set one flag. Flipping a mode-defining flag can drop you into `custom`. */
193
+ setFlag<K extends keyof ControlFlags>(key: K, value: ControlFlags[K]): void;
194
+ /** Subscribe to changes; returns an unsubscribe. Fires on every real change. */
195
+ subscribe(fn: (flags: ControlFlags) => void): () => void;
196
+ private replace;
197
+ }
198
+ //#endregion
96
199
  //#region src/plugin/CapabilityRegistry.d.ts
97
200
  /**
98
201
  * Shared runtime contracts between plugins, keyed by name. A plugin (or the
@@ -182,30 +285,6 @@ interface EnginePlugin<T = unknown> {
182
285
  controller?(elements: () => ReadonlyMap<string, Object3D>): ElementController;
183
286
  }
184
287
  //#endregion
185
- //#region src/input/InputBus.d.ts
186
- /**
187
- * The shared analog-input channel: the current movement and look intent, each
188
- * axis in `-1..1` and held until a producer changes it. Producers (on-screen
189
- * joysticks in the overlay, a gamepad…) write to it; whatever camera system is
190
- * active reads it and interprets it its own way — free-roam walks with it, a
191
- * fixed camera might only use the look axes to glance around. This is what makes
192
- * the joysticks reusable across camera types. Pure state — no three.js.
193
- */
194
- declare class InputBus {
195
- /** Forward (`+1`) / backward (`-1`). */
196
- moveForward: number;
197
- /** Strafe right (`+1`) / left (`-1`). */
198
- moveStrafe: number;
199
- /** Look right (`+1`) / left (`-1`). */
200
- lookX: number;
201
- /** Look up (`+1`) / down (`-1`). */
202
- lookY: number;
203
- /** Set the movement intent (forward, strafe). */
204
- setMove(forward: number, strafe?: number): void;
205
- /** Set the look intent (turn, pitch). */
206
- setLook(x: number, y?: number): void;
207
- }
208
- //#endregion
209
288
  //#region src/plugin/systemPlugin.d.ts
210
289
  /**
211
290
  * What the kernel hands a system on setup. A *system* plugin — unlike an element
@@ -227,6 +306,8 @@ interface SystemContext {
227
306
  renderer: WebGLRenderer;
228
307
  /** Shared analog input (joysticks feed it); systems interpret it. */
229
308
  input: InputBus;
309
+ /** The visitor's current control flags (e.g. look-invert axes), read live. */
310
+ controlFlags: () => ControlFlags;
230
311
  /** Capability registry for cross-plugin contracts (terrain, colliders…). */
231
312
  capabilities: CapabilityRegistry;
232
313
  /** Emit a gallery event. */
@@ -413,6 +494,25 @@ interface ControlsApi {
413
494
  /** Turn right (`+1`) / left (`-1`); optional look up (`+1`) / down (`-1`). */
414
495
  setLookInput(x: number, y?: number): void;
415
496
  }
497
+ /**
498
+ * The visitor's camera-control preferences (view mode + capability flags). The
499
+ * engine applies the effects that touch rendering/input — gyroscope head-look and
500
+ * the stereo split — as the flags change; the overlay drives it from the mode
501
+ * picker and settings, and reads it to show the right joysticks. See
502
+ * `ControlSettings`.
503
+ */
504
+ interface ViewControlsApi {
505
+ /** Current capability flags (a copy). */
506
+ get(): ControlFlags;
507
+ /** The mode the flags represent (`custom` when hand-tuned in settings). */
508
+ getMode(): ControlMode;
509
+ /** Switch to a mode's preset (keeps the look-invert axes). */
510
+ setMode(mode: Exclude<ControlMode, "custom">): void;
511
+ /** Set one capability flag (may drop the mode into `custom`). */
512
+ setFlag<K extends keyof ControlFlags>(key: K, value: ControlFlags[K]): void;
513
+ /** Subscribe to changes; returns an unsubscribe. */
514
+ subscribe(fn: (flags: ControlFlags) => void): () => void;
515
+ }
416
516
  /** Control the HDRI sky / environment at runtime, for behavior code. */
417
517
  interface EnvironmentApi {
418
518
  /** Set the visible sky background brightness (`0` = black). */
@@ -431,6 +531,8 @@ interface GalleryContext {
431
531
  readonly media: MediaController;
432
532
  /** Drive the camera externally (on-screen touch controls, etc.). */
433
533
  readonly controls: ControlsApi;
534
+ /** The visitor's camera-control preferences (view mode + capability flags). */
535
+ readonly viewControls: ViewControlsApi;
434
536
  /** Control the HDRI sky / environment at runtime. */
435
537
  readonly environment: EnvironmentApi;
436
538
  /** Drive the procedural sky's time of day. */
@@ -559,5 +661,5 @@ declare class InteractionController {
559
661
  private buildHighlight;
560
662
  }
561
663
  //#endregion
562
- export { type Antialias, type BuiltElement, COLLIDERS, type CameraApi, CapabilityRegistry, type Collider, type ControlsApi, EDGES, EYE_HEIGHT, type EdgeAnchor, type EdgeIndex, type ElementController, type EngineContext, type EnginePlugin, type EnvironmentApi, type FocusOptions, type Gallery, type GalleryContext, type GalleryOptions, InputBus, InteractionController, type LightSet, type LightSetOptions, type LightsApi, type MediaController, ROAM_CONTROL, type RenderOptions, type RoamControl, SceneManager, type SkyApi, type SystemContext, type SystemInstance, type SystemPlugin, TERRAIN, type TerrainCapability, type TerrainDefinition, type VideoApi, applyTexture, createGallery, isSystemPlugin };
664
+ export { type Antialias, type BuiltElement, COLLIDERS, type CameraApi, CapabilityRegistry, type Collider, type ControlFlags, type ControlMode, ControlSettings, type ControlsApi, DEFAULT_CONTROL_FLAGS, EDGES, EYE_HEIGHT, type EdgeAnchor, type EdgeIndex, type ElementController, type EngineContext, type EnginePlugin, type EnvironmentApi, type FocusOptions, type Gallery, type GalleryContext, type GalleryOptions, InputBus, InteractionController, type LightSet, type LightSetOptions, type LightsApi, MODE_PRESETS, type MediaController, ROAM_CONTROL, type RenderOptions, type RoamControl, SceneManager, type SkyApi, type SystemContext, type SystemInstance, type SystemPlugin, TERRAIN, type TerrainCapability, type TerrainDefinition, type VideoApi, applyMode, applyTexture, createGallery, isSystemPlugin, modeOf };
563
665
  //# sourceMappingURL=index.d.mts.map
package/dist/index.mjs CHANGED
@@ -272,6 +272,139 @@ var InputBus = class {
272
272
  }
273
273
  };
274
274
  //#endregion
275
+ //#region src/input/controlSettings.ts
276
+ /**
277
+ * Mode presets. Touch = two joysticks, no gyro. VR = the gyro aims and a single
278
+ * move joystick walks you, no split. Cardboard = gyro aims, stereo split, gaze
279
+ * select (no joysticks). The move-only single stick in VR is derived downstream
280
+ * from `gyro && !splitScreen`, so it needs no flag of its own.
281
+ */
282
+ const MODE_PRESETS = {
283
+ touch: {
284
+ gyro: false,
285
+ splitScreen: false,
286
+ joysticks: true
287
+ },
288
+ vr: {
289
+ gyro: true,
290
+ splitScreen: false,
291
+ joysticks: true
292
+ },
293
+ cardboard: {
294
+ gyro: true,
295
+ splitScreen: true,
296
+ joysticks: false
297
+ }
298
+ };
299
+ /** Default flags: plain touch, no look inversion. */
300
+ const DEFAULT_CONTROL_FLAGS = {
301
+ ...MODE_PRESETS.touch,
302
+ invertLookX: false,
303
+ invertLookY: false
304
+ };
305
+ const FLAG_KEYS$1 = [
306
+ "gyro",
307
+ "splitScreen",
308
+ "joysticks",
309
+ "invertLookX",
310
+ "invertLookY"
311
+ ];
312
+ const equalFlags = (a, b) => FLAG_KEYS$1.every((key) => a[key] === b[key]);
313
+ /** The mode a flag set represents, or `custom` when no preset matches. */
314
+ function modeOf(flags) {
315
+ for (const mode of [
316
+ "touch",
317
+ "vr",
318
+ "cardboard"
319
+ ]) {
320
+ const preset = MODE_PRESETS[mode];
321
+ if (flags.gyro === preset.gyro && flags.splitScreen === preset.splitScreen && flags.joysticks === preset.joysticks) return mode;
322
+ }
323
+ return "custom";
324
+ }
325
+ /** Apply a mode's preset onto `flags`, leaving the look-invert axes untouched. */
326
+ function applyMode(flags, mode) {
327
+ return {
328
+ ...flags,
329
+ ...MODE_PRESETS[mode]
330
+ };
331
+ }
332
+ /**
333
+ * Live control-preferences state: holds the flags, derives the mode, and notifies
334
+ * subscribers on every real change. Callers apply the effects — the engine wires
335
+ * gyro and split, the overlay redraws — so this object is just the shared source
336
+ * of truth they all read and write.
337
+ */
338
+ var ControlSettings = class {
339
+ flags;
340
+ subscribers = /* @__PURE__ */ new Set();
341
+ constructor(initial = {}) {
342
+ this.flags = {
343
+ ...DEFAULT_CONTROL_FLAGS,
344
+ ...initial
345
+ };
346
+ }
347
+ /** The current flags (a copy — mutate through `setMode` / `setFlag`). */
348
+ get() {
349
+ return { ...this.flags };
350
+ }
351
+ /** The mode the current flags represent (`custom` when hand-tuned). */
352
+ getMode() {
353
+ return modeOf(this.flags);
354
+ }
355
+ /** Switch to a mode's preset (keeps the look-invert axes). No-op if unchanged. */
356
+ setMode(mode) {
357
+ this.replace(applyMode(this.flags, mode));
358
+ }
359
+ /** Set one flag. Flipping a mode-defining flag can drop you into `custom`. */
360
+ setFlag(key, value) {
361
+ this.replace({
362
+ ...this.flags,
363
+ [key]: value
364
+ });
365
+ }
366
+ /** Subscribe to changes; returns an unsubscribe. Fires on every real change. */
367
+ subscribe(fn) {
368
+ this.subscribers.add(fn);
369
+ return () => this.subscribers.delete(fn);
370
+ }
371
+ replace(next) {
372
+ if (equalFlags(this.flags, next)) return;
373
+ this.flags = next;
374
+ const snapshot = this.get();
375
+ for (const fn of this.subscribers) fn(snapshot);
376
+ }
377
+ };
378
+ //#endregion
379
+ //#region src/input/controlStorage.ts
380
+ /**
381
+ * localStorage persistence for the control flags, kept out of the pure
382
+ * `ControlSettings` so that stays DOM-free and unit-testable. Both functions are
383
+ * best-effort and guarded: no `localStorage` (SSR, privacy mode) or malformed
384
+ * data just falls back to defaults, never throws.
385
+ */
386
+ const STORAGE_KEY = "scenography:controls";
387
+ const FLAG_KEYS = Object.keys(DEFAULT_CONTROL_FLAGS);
388
+ /** Read persisted control flags; `null` when absent, unavailable or malformed. */
389
+ function loadControlFlags() {
390
+ try {
391
+ const raw = globalThis.localStorage?.getItem(STORAGE_KEY);
392
+ if (!raw) return null;
393
+ const parsed = JSON.parse(raw);
394
+ const flags = {};
395
+ for (const key of FLAG_KEYS) if (typeof parsed[key] === "boolean") flags[key] = parsed[key];
396
+ return flags;
397
+ } catch {
398
+ return null;
399
+ }
400
+ }
401
+ /** Persist control flags (best-effort; ignores a full/blocked store). */
402
+ function saveControlFlags(flags) {
403
+ try {
404
+ globalThis.localStorage?.setItem(STORAGE_KEY, JSON.stringify(flags));
405
+ } catch {}
406
+ }
407
+ //#endregion
275
408
  //#region src/plugin/CapabilityRegistry.ts
276
409
  /**
277
410
  * Shared runtime contracts between plugins, keyed by name. A plugin (or the
@@ -523,6 +656,7 @@ function createGallery(scene, options) {
523
656
  const manager = new SceneManager(threeScene, elementPlugins, capabilities);
524
657
  manager.reconcile(scene);
525
658
  const input = new InputBus();
659
+ const controlSettings = new ControlSettings(loadControlFlags() ?? (options.cardboard ? MODE_PRESETS.cardboard : {}));
526
660
  const environment = new EnvironmentController(threeScene);
527
661
  environment.reconcile(scene.environment);
528
662
  const listeners = /* @__PURE__ */ new Map();
@@ -546,6 +680,7 @@ function createGallery(scene, options) {
546
680
  scene3d: threeScene,
547
681
  renderer,
548
682
  input,
683
+ controlFlags: () => controlSettings.get(),
549
684
  capabilities,
550
685
  emit,
551
686
  resolve: (id) => manager.get(id)
@@ -557,10 +692,24 @@ function createGallery(scene, options) {
557
692
  focusArtwork: () => {},
558
693
  centerArtwork: () => {},
559
694
  release: () => {},
695
+ reset: () => {},
560
696
  isAnimating: () => false
561
697
  };
562
698
  const headTracking = apis.get(CARDBOARD_SYSTEM);
563
- if (options.cardboard) headTracking?.setEnabled(true);
699
+ const initialFlags = controlSettings.get();
700
+ render.cardboard = initialFlags.splitScreen;
701
+ if (initialFlags.gyro) headTracking?.setEnabled(true);
702
+ let prevFlags = controlSettings.get();
703
+ controlSettings.subscribe((flags) => {
704
+ if (flags.gyro !== prevFlags.gyro) headTracking?.setEnabled(flags.gyro);
705
+ if (flags.splitScreen !== prevFlags.splitScreen) render.cardboard = flags.splitScreen;
706
+ prevFlags = flags;
707
+ saveControlFlags(flags);
708
+ emit("controls:change", {
709
+ flags,
710
+ mode: controlSettings.getMode()
711
+ });
712
+ });
564
713
  const lightsApi = apis.get(CTX_SLOT.lights) ?? {
565
714
  set: () => {},
566
715
  dimAll: () => {},
@@ -705,6 +854,13 @@ function createGallery(scene, options) {
705
854
  setMoveInput: (forward, strafe) => input.setMove(forward, strafe),
706
855
  setLookInput: (x, y) => input.setLook(x, y)
707
856
  },
857
+ viewControls: {
858
+ get: () => controlSettings.get(),
859
+ getMode: () => controlSettings.getMode(),
860
+ setMode: (mode) => controlSettings.setMode(mode),
861
+ setFlag: (key, value) => controlSettings.setFlag(key, value),
862
+ subscribe: (fn) => controlSettings.subscribe(fn)
863
+ },
708
864
  environment: { setBackgroundIntensity: (v) => environment.setBackgroundIntensity(v) },
709
865
  sky: skyApi,
710
866
  camera: cameraApi,
@@ -780,10 +936,7 @@ function createGallery(scene, options) {
780
936
  if (next.bloom !== void 0) render.bloom = next.bloom;
781
937
  const aaChanged = next.antialias !== void 0 && next.antialias !== render.antialias;
782
938
  if (next.antialias !== void 0) render.antialias = next.antialias;
783
- if (next.cardboard !== void 0 && next.cardboard !== render.cardboard) {
784
- render.cardboard = next.cardboard;
785
- headTracking?.setEnabled(next.cardboard);
786
- }
939
+ if (next.cardboard !== void 0 && next.cardboard !== render.cardboard) controlSettings.setMode(next.cardboard ? "cardboard" : "touch");
787
940
  if (next.eyeSeparation !== void 0) {
788
941
  render.eyeSeparation = next.eyeSeparation;
789
942
  stereo.eyeSep = next.eyeSeparation;
@@ -843,6 +996,6 @@ const applyTexture = (material, texture) => {
843
996
  return texture.ao !== void 0;
844
997
  };
845
998
  //#endregion
846
- export { COLLIDERS, CapabilityRegistry, EDGES, EYE_HEIGHT, InputBus, InteractionController, ROAM_CONTROL, SceneManager, TERRAIN, applyTexture, createGallery, isSystemPlugin };
999
+ export { COLLIDERS, CapabilityRegistry, ControlSettings, DEFAULT_CONTROL_FLAGS, EDGES, EYE_HEIGHT, InputBus, InteractionController, MODE_PRESETS, ROAM_CONTROL, SceneManager, TERRAIN, applyMode, applyTexture, createGallery, isSystemPlugin, modeOf };
847
1000
 
848
1001
  //# sourceMappingURL=index.mjs.map