@scenography/engine 0.2.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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,8 +30,19 @@ 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;
40
+ /**
41
+ * Whether a piece is framed and still selected (the framing watch is active).
42
+ * Usually paired with {@link isAnimating} to gate picking: while a piece is
43
+ * selected, hovering must not re-highlight it (its info panel is open).
44
+ */
45
+ isFocused(): boolean;
28
46
  }
29
47
  //#endregion
30
48
  //#region src/api/index.d.ts
@@ -93,6 +111,97 @@ interface SkyApi {
93
111
  getAzimuth(): number;
94
112
  }
95
113
  //#endregion
114
+ //#region src/input/InputBus.d.ts
115
+ /**
116
+ * The shared analog-input channel: the current movement and look intent, each
117
+ * axis in `-1..1` and held until a producer changes it. Producers (on-screen
118
+ * joysticks in the overlay, a gamepad…) write to it; whatever camera system is
119
+ * active reads it and interprets it its own way — free-roam walks with it, a
120
+ * fixed camera might only use the look axes to glance around. This is what makes
121
+ * the joysticks reusable across camera types. Pure state — no three.js.
122
+ */
123
+ declare class InputBus {
124
+ /** Forward (`+1`) / backward (`-1`). */
125
+ moveForward: number;
126
+ /** Strafe right (`+1`) / left (`-1`). */
127
+ moveStrafe: number;
128
+ /** Look right (`+1`) / left (`-1`). */
129
+ lookX: number;
130
+ /** Look up (`+1`) / down (`-1`). */
131
+ lookY: number;
132
+ /** Set the movement intent (forward, strafe). */
133
+ setMove(forward: number, strafe?: number): void;
134
+ /** Set the look intent (turn, pitch). */
135
+ setLook(x: number, y?: number): void;
136
+ }
137
+ //#endregion
138
+ //#region src/input/controlSettings.d.ts
139
+ /**
140
+ * The visitor's camera-control preferences: a set of orthogonal capability flags
141
+ * plus a derived, friendly "mode". Pure state — no three.js, no DOM. The engine
142
+ * wires the flags to effects (gyroscope head-look, stereo split); consumers read
143
+ * them (the analog look-invert axes); the overlay drives it from a mode picker
144
+ * (the `ScanEye` button) and the settings panel. Persistence and device
145
+ * feature-detection live in those callers, not here, so this stays trivially
146
+ * unit-testable.
147
+ */
148
+ /**
149
+ * Orthogonal control capabilities. `gyro` / `splitScreen` / `joysticks` are what a
150
+ * mode fixes; the two `invertLook*` axes are independent look-feel preferences
151
+ * (some people feel the right stick inverted), kept separate from the mode.
152
+ */
153
+ interface ControlFlags {
154
+ /** Orient the camera from the device gyroscope (head-look). */
155
+ gyro: boolean;
156
+ /** Split the view into a stereo left/right eye pair (phone-in-a-headset). */
157
+ splitScreen: boolean;
158
+ /** Show the on-screen virtual joysticks. */
159
+ joysticks: boolean;
160
+ /** Invert the horizontal (turn) axis of analog look. */
161
+ invertLookX: boolean;
162
+ /** Invert the vertical (pitch) axis of analog look. */
163
+ invertLookY: boolean;
164
+ }
165
+ /** A named preset over the mode-defining flags; `custom` when none matches. */
166
+ type ControlMode = "touch" | "vr" | "cardboard" | "custom";
167
+ /** The three flags a mode fixes (the look-invert axes stay a separate preference). */
168
+ type ModeFlags = Pick<ControlFlags, "gyro" | "splitScreen" | "joysticks">;
169
+ /**
170
+ * Mode presets. Touch = two joysticks, no gyro. VR = the gyro aims and a single
171
+ * move joystick walks you, no split. Cardboard = gyro aims, stereo split, gaze
172
+ * select (no joysticks). The move-only single stick in VR is derived downstream
173
+ * from `gyro && !splitScreen`, so it needs no flag of its own.
174
+ */
175
+ declare const MODE_PRESETS: Record<Exclude<ControlMode, "custom">, ModeFlags>;
176
+ /** Default flags: plain touch, no look inversion. */
177
+ declare const DEFAULT_CONTROL_FLAGS: ControlFlags;
178
+ /** The mode a flag set represents, or `custom` when no preset matches. */
179
+ declare function modeOf(flags: ControlFlags): ControlMode;
180
+ /** Apply a mode's preset onto `flags`, leaving the look-invert axes untouched. */
181
+ declare function applyMode(flags: ControlFlags, mode: Exclude<ControlMode, "custom">): ControlFlags;
182
+ /**
183
+ * Live control-preferences state: holds the flags, derives the mode, and notifies
184
+ * subscribers on every real change. Callers apply the effects — the engine wires
185
+ * gyro and split, the overlay redraws — so this object is just the shared source
186
+ * of truth they all read and write.
187
+ */
188
+ declare class ControlSettings {
189
+ private flags;
190
+ private readonly subscribers;
191
+ constructor(initial?: Partial<ControlFlags>);
192
+ /** The current flags (a copy — mutate through `setMode` / `setFlag`). */
193
+ get(): ControlFlags;
194
+ /** The mode the current flags represent (`custom` when hand-tuned). */
195
+ getMode(): ControlMode;
196
+ /** Switch to a mode's preset (keeps the look-invert axes). No-op if unchanged. */
197
+ setMode(mode: Exclude<ControlMode, "custom">): void;
198
+ /** Set one flag. Flipping a mode-defining flag can drop you into `custom`. */
199
+ setFlag<K extends keyof ControlFlags>(key: K, value: ControlFlags[K]): void;
200
+ /** Subscribe to changes; returns an unsubscribe. Fires on every real change. */
201
+ subscribe(fn: (flags: ControlFlags) => void): () => void;
202
+ private replace;
203
+ }
204
+ //#endregion
96
205
  //#region src/plugin/CapabilityRegistry.d.ts
97
206
  /**
98
207
  * Shared runtime contracts between plugins, keyed by name. A plugin (or the
@@ -182,30 +291,6 @@ interface EnginePlugin<T = unknown> {
182
291
  controller?(elements: () => ReadonlyMap<string, Object3D>): ElementController;
183
292
  }
184
293
  //#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
294
  //#region src/plugin/systemPlugin.d.ts
210
295
  /**
211
296
  * What the kernel hands a system on setup. A *system* plugin — unlike an element
@@ -227,6 +312,8 @@ interface SystemContext {
227
312
  renderer: WebGLRenderer;
228
313
  /** Shared analog input (joysticks feed it); systems interpret it. */
229
314
  input: InputBus;
315
+ /** The visitor's current control flags (e.g. look-invert axes), read live. */
316
+ controlFlags: () => ControlFlags;
230
317
  /** Capability registry for cross-plugin contracts (terrain, colliders…). */
231
318
  capabilities: CapabilityRegistry;
232
319
  /** Emit a gallery event. */
@@ -413,6 +500,25 @@ interface ControlsApi {
413
500
  /** Turn right (`+1`) / left (`-1`); optional look up (`+1`) / down (`-1`). */
414
501
  setLookInput(x: number, y?: number): void;
415
502
  }
503
+ /**
504
+ * The visitor's camera-control preferences (view mode + capability flags). The
505
+ * engine applies the effects that touch rendering/input — gyroscope head-look and
506
+ * the stereo split — as the flags change; the overlay drives it from the mode
507
+ * picker and settings, and reads it to show the right joysticks. See
508
+ * `ControlSettings`.
509
+ */
510
+ interface ViewControlsApi {
511
+ /** Current capability flags (a copy). */
512
+ get(): ControlFlags;
513
+ /** The mode the flags represent (`custom` when hand-tuned in settings). */
514
+ getMode(): ControlMode;
515
+ /** Switch to a mode's preset (keeps the look-invert axes). */
516
+ setMode(mode: Exclude<ControlMode, "custom">): void;
517
+ /** Set one capability flag (may drop the mode into `custom`). */
518
+ setFlag<K extends keyof ControlFlags>(key: K, value: ControlFlags[K]): void;
519
+ /** Subscribe to changes; returns an unsubscribe. */
520
+ subscribe(fn: (flags: ControlFlags) => void): () => void;
521
+ }
416
522
  /** Control the HDRI sky / environment at runtime, for behavior code. */
417
523
  interface EnvironmentApi {
418
524
  /** Set the visible sky background brightness (`0` = black). */
@@ -431,6 +537,8 @@ interface GalleryContext {
431
537
  readonly media: MediaController;
432
538
  /** Drive the camera externally (on-screen touch controls, etc.). */
433
539
  readonly controls: ControlsApi;
540
+ /** The visitor's camera-control preferences (view mode + capability flags). */
541
+ readonly viewControls: ViewControlsApi;
434
542
  /** Control the HDRI sky / environment at runtime. */
435
543
  readonly environment: EnvironmentApi;
436
544
  /** Drive the procedural sky's time of day. */
@@ -559,5 +667,5 @@ declare class InteractionController {
559
667
  private buildHighlight;
560
668
  }
561
669
  //#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 };
670
+ 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
671
  //# 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,8 +30,19 @@ 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;
40
+ /**
41
+ * Whether a piece is framed and still selected (the framing watch is active).
42
+ * Usually paired with {@link isAnimating} to gate picking: while a piece is
43
+ * selected, hovering must not re-highlight it (its info panel is open).
44
+ */
45
+ isFocused(): boolean;
28
46
  }
29
47
  //#endregion
30
48
  //#region src/api/index.d.ts
@@ -93,6 +111,97 @@ interface SkyApi {
93
111
  getAzimuth(): number;
94
112
  }
95
113
  //#endregion
114
+ //#region src/input/InputBus.d.ts
115
+ /**
116
+ * The shared analog-input channel: the current movement and look intent, each
117
+ * axis in `-1..1` and held until a producer changes it. Producers (on-screen
118
+ * joysticks in the overlay, a gamepad…) write to it; whatever camera system is
119
+ * active reads it and interprets it its own way — free-roam walks with it, a
120
+ * fixed camera might only use the look axes to glance around. This is what makes
121
+ * the joysticks reusable across camera types. Pure state — no three.js.
122
+ */
123
+ declare class InputBus {
124
+ /** Forward (`+1`) / backward (`-1`). */
125
+ moveForward: number;
126
+ /** Strafe right (`+1`) / left (`-1`). */
127
+ moveStrafe: number;
128
+ /** Look right (`+1`) / left (`-1`). */
129
+ lookX: number;
130
+ /** Look up (`+1`) / down (`-1`). */
131
+ lookY: number;
132
+ /** Set the movement intent (forward, strafe). */
133
+ setMove(forward: number, strafe?: number): void;
134
+ /** Set the look intent (turn, pitch). */
135
+ setLook(x: number, y?: number): void;
136
+ }
137
+ //#endregion
138
+ //#region src/input/controlSettings.d.ts
139
+ /**
140
+ * The visitor's camera-control preferences: a set of orthogonal capability flags
141
+ * plus a derived, friendly "mode". Pure state — no three.js, no DOM. The engine
142
+ * wires the flags to effects (gyroscope head-look, stereo split); consumers read
143
+ * them (the analog look-invert axes); the overlay drives it from a mode picker
144
+ * (the `ScanEye` button) and the settings panel. Persistence and device
145
+ * feature-detection live in those callers, not here, so this stays trivially
146
+ * unit-testable.
147
+ */
148
+ /**
149
+ * Orthogonal control capabilities. `gyro` / `splitScreen` / `joysticks` are what a
150
+ * mode fixes; the two `invertLook*` axes are independent look-feel preferences
151
+ * (some people feel the right stick inverted), kept separate from the mode.
152
+ */
153
+ interface ControlFlags {
154
+ /** Orient the camera from the device gyroscope (head-look). */
155
+ gyro: boolean;
156
+ /** Split the view into a stereo left/right eye pair (phone-in-a-headset). */
157
+ splitScreen: boolean;
158
+ /** Show the on-screen virtual joysticks. */
159
+ joysticks: boolean;
160
+ /** Invert the horizontal (turn) axis of analog look. */
161
+ invertLookX: boolean;
162
+ /** Invert the vertical (pitch) axis of analog look. */
163
+ invertLookY: boolean;
164
+ }
165
+ /** A named preset over the mode-defining flags; `custom` when none matches. */
166
+ type ControlMode = "touch" | "vr" | "cardboard" | "custom";
167
+ /** The three flags a mode fixes (the look-invert axes stay a separate preference). */
168
+ type ModeFlags = Pick<ControlFlags, "gyro" | "splitScreen" | "joysticks">;
169
+ /**
170
+ * Mode presets. Touch = two joysticks, no gyro. VR = the gyro aims and a single
171
+ * move joystick walks you, no split. Cardboard = gyro aims, stereo split, gaze
172
+ * select (no joysticks). The move-only single stick in VR is derived downstream
173
+ * from `gyro && !splitScreen`, so it needs no flag of its own.
174
+ */
175
+ declare const MODE_PRESETS: Record<Exclude<ControlMode, "custom">, ModeFlags>;
176
+ /** Default flags: plain touch, no look inversion. */
177
+ declare const DEFAULT_CONTROL_FLAGS: ControlFlags;
178
+ /** The mode a flag set represents, or `custom` when no preset matches. */
179
+ declare function modeOf(flags: ControlFlags): ControlMode;
180
+ /** Apply a mode's preset onto `flags`, leaving the look-invert axes untouched. */
181
+ declare function applyMode(flags: ControlFlags, mode: Exclude<ControlMode, "custom">): ControlFlags;
182
+ /**
183
+ * Live control-preferences state: holds the flags, derives the mode, and notifies
184
+ * subscribers on every real change. Callers apply the effects — the engine wires
185
+ * gyro and split, the overlay redraws — so this object is just the shared source
186
+ * of truth they all read and write.
187
+ */
188
+ declare class ControlSettings {
189
+ private flags;
190
+ private readonly subscribers;
191
+ constructor(initial?: Partial<ControlFlags>);
192
+ /** The current flags (a copy — mutate through `setMode` / `setFlag`). */
193
+ get(): ControlFlags;
194
+ /** The mode the current flags represent (`custom` when hand-tuned). */
195
+ getMode(): ControlMode;
196
+ /** Switch to a mode's preset (keeps the look-invert axes). No-op if unchanged. */
197
+ setMode(mode: Exclude<ControlMode, "custom">): void;
198
+ /** Set one flag. Flipping a mode-defining flag can drop you into `custom`. */
199
+ setFlag<K extends keyof ControlFlags>(key: K, value: ControlFlags[K]): void;
200
+ /** Subscribe to changes; returns an unsubscribe. Fires on every real change. */
201
+ subscribe(fn: (flags: ControlFlags) => void): () => void;
202
+ private replace;
203
+ }
204
+ //#endregion
96
205
  //#region src/plugin/CapabilityRegistry.d.ts
97
206
  /**
98
207
  * Shared runtime contracts between plugins, keyed by name. A plugin (or the
@@ -182,30 +291,6 @@ interface EnginePlugin<T = unknown> {
182
291
  controller?(elements: () => ReadonlyMap<string, Object3D>): ElementController;
183
292
  }
184
293
  //#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
294
  //#region src/plugin/systemPlugin.d.ts
210
295
  /**
211
296
  * What the kernel hands a system on setup. A *system* plugin — unlike an element
@@ -227,6 +312,8 @@ interface SystemContext {
227
312
  renderer: WebGLRenderer;
228
313
  /** Shared analog input (joysticks feed it); systems interpret it. */
229
314
  input: InputBus;
315
+ /** The visitor's current control flags (e.g. look-invert axes), read live. */
316
+ controlFlags: () => ControlFlags;
230
317
  /** Capability registry for cross-plugin contracts (terrain, colliders…). */
231
318
  capabilities: CapabilityRegistry;
232
319
  /** Emit a gallery event. */
@@ -413,6 +500,25 @@ interface ControlsApi {
413
500
  /** Turn right (`+1`) / left (`-1`); optional look up (`+1`) / down (`-1`). */
414
501
  setLookInput(x: number, y?: number): void;
415
502
  }
503
+ /**
504
+ * The visitor's camera-control preferences (view mode + capability flags). The
505
+ * engine applies the effects that touch rendering/input — gyroscope head-look and
506
+ * the stereo split — as the flags change; the overlay drives it from the mode
507
+ * picker and settings, and reads it to show the right joysticks. See
508
+ * `ControlSettings`.
509
+ */
510
+ interface ViewControlsApi {
511
+ /** Current capability flags (a copy). */
512
+ get(): ControlFlags;
513
+ /** The mode the flags represent (`custom` when hand-tuned in settings). */
514
+ getMode(): ControlMode;
515
+ /** Switch to a mode's preset (keeps the look-invert axes). */
516
+ setMode(mode: Exclude<ControlMode, "custom">): void;
517
+ /** Set one capability flag (may drop the mode into `custom`). */
518
+ setFlag<K extends keyof ControlFlags>(key: K, value: ControlFlags[K]): void;
519
+ /** Subscribe to changes; returns an unsubscribe. */
520
+ subscribe(fn: (flags: ControlFlags) => void): () => void;
521
+ }
416
522
  /** Control the HDRI sky / environment at runtime, for behavior code. */
417
523
  interface EnvironmentApi {
418
524
  /** Set the visible sky background brightness (`0` = black). */
@@ -431,6 +537,8 @@ interface GalleryContext {
431
537
  readonly media: MediaController;
432
538
  /** Drive the camera externally (on-screen touch controls, etc.). */
433
539
  readonly controls: ControlsApi;
540
+ /** The visitor's camera-control preferences (view mode + capability flags). */
541
+ readonly viewControls: ViewControlsApi;
434
542
  /** Control the HDRI sky / environment at runtime. */
435
543
  readonly environment: EnvironmentApi;
436
544
  /** Drive the procedural sky's time of day. */
@@ -559,5 +667,5 @@ declare class InteractionController {
559
667
  private buildHighlight;
560
668
  }
561
669
  //#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 };
670
+ 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
671
  //# 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,25 @@ function createGallery(scene, options) {
557
692
  focusArtwork: () => {},
558
693
  centerArtwork: () => {},
559
694
  release: () => {},
560
- isAnimating: () => false
695
+ reset: () => {},
696
+ isAnimating: () => false,
697
+ isFocused: () => false
561
698
  };
562
699
  const headTracking = apis.get(CARDBOARD_SYSTEM);
563
- if (options.cardboard) headTracking?.setEnabled(true);
700
+ const initialFlags = controlSettings.get();
701
+ render.cardboard = initialFlags.splitScreen;
702
+ if (initialFlags.gyro) headTracking?.setEnabled(true);
703
+ let prevFlags = controlSettings.get();
704
+ controlSettings.subscribe((flags) => {
705
+ if (flags.gyro !== prevFlags.gyro) headTracking?.setEnabled(flags.gyro);
706
+ if (flags.splitScreen !== prevFlags.splitScreen) render.cardboard = flags.splitScreen;
707
+ prevFlags = flags;
708
+ saveControlFlags(flags);
709
+ emit("controls:change", {
710
+ flags,
711
+ mode: controlSettings.getMode()
712
+ });
713
+ });
564
714
  const lightsApi = apis.get(CTX_SLOT.lights) ?? {
565
715
  set: () => {},
566
716
  dimAll: () => {},
@@ -590,7 +740,7 @@ function createGallery(scene, options) {
590
740
  const gated = s.artworks ?? [];
591
741
  return new Map(gated.filter((a) => a.infoRadius !== void 0).map((a) => [a.id, a.infoRadius]));
592
742
  };
593
- const interaction = new InteractionController(camera, canvas, threeScene, emit, () => !cameraApi.isAnimating());
743
+ const interaction = new InteractionController(camera, canvas, threeScene, emit, () => !cameraApi.isAnimating() && !cameraApi.isFocused());
594
744
  interaction.setRadii(radiiOf(scene));
595
745
  interaction.attach();
596
746
  const clock = new Clock();
@@ -705,6 +855,13 @@ function createGallery(scene, options) {
705
855
  setMoveInput: (forward, strafe) => input.setMove(forward, strafe),
706
856
  setLookInput: (x, y) => input.setLook(x, y)
707
857
  },
858
+ viewControls: {
859
+ get: () => controlSettings.get(),
860
+ getMode: () => controlSettings.getMode(),
861
+ setMode: (mode) => controlSettings.setMode(mode),
862
+ setFlag: (key, value) => controlSettings.setFlag(key, value),
863
+ subscribe: (fn) => controlSettings.subscribe(fn)
864
+ },
708
865
  environment: { setBackgroundIntensity: (v) => environment.setBackgroundIntensity(v) },
709
866
  sky: skyApi,
710
867
  camera: cameraApi,
@@ -780,10 +937,7 @@ function createGallery(scene, options) {
780
937
  if (next.bloom !== void 0) render.bloom = next.bloom;
781
938
  const aaChanged = next.antialias !== void 0 && next.antialias !== render.antialias;
782
939
  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
- }
940
+ if (next.cardboard !== void 0 && next.cardboard !== render.cardboard) controlSettings.setMode(next.cardboard ? "cardboard" : "touch");
787
941
  if (next.eyeSeparation !== void 0) {
788
942
  render.eyeSeparation = next.eyeSeparation;
789
943
  stereo.eyeSep = next.eyeSeparation;
@@ -843,6 +997,6 @@ const applyTexture = (material, texture) => {
843
997
  return texture.ao !== void 0;
844
998
  };
845
999
  //#endregion
846
- export { COLLIDERS, CapabilityRegistry, EDGES, EYE_HEIGHT, InputBus, InteractionController, ROAM_CONTROL, SceneManager, TERRAIN, applyTexture, createGallery, isSystemPlugin };
1000
+ export { COLLIDERS, CapabilityRegistry, ControlSettings, DEFAULT_CONTROL_FLAGS, EDGES, EYE_HEIGHT, InputBus, InteractionController, MODE_PRESETS, ROAM_CONTROL, SceneManager, TERRAIN, applyMode, applyTexture, createGallery, isSystemPlugin, modeOf };
847
1001
 
848
1002
  //# sourceMappingURL=index.mjs.map