entangled-worlds-orbiters-shared 3.3.0 → 4.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 (118) hide show
  1. package/README.md +3 -2
  2. package/dist/config/env.d.ts +8 -0
  3. package/dist/config/env.js +36 -0
  4. package/dist/config/index.d.ts +3 -0
  5. package/dist/config/index.js +4 -0
  6. package/dist/config/textureConfig.d.ts +140 -0
  7. package/dist/config/textureConfig.js +318 -0
  8. package/dist/core/index.d.ts +2 -0
  9. package/dist/core/index.js +3 -0
  10. package/dist/core/lightFresnel.d.ts +6 -0
  11. package/dist/core/lightFresnel.js +63 -0
  12. package/dist/core/lights.d.ts +48 -0
  13. package/dist/core/lights.js +79 -0
  14. package/dist/index.d.ts +6 -0
  15. package/dist/index.js +7 -0
  16. package/dist/loaders/bodyTextureLoader.d.ts +10 -0
  17. package/dist/loaders/bodyTextureLoader.js +28 -0
  18. package/dist/loaders/cubeMapLoader.d.ts +58 -0
  19. package/dist/loaders/cubeMapLoader.js +291 -0
  20. package/dist/loaders/glbLoader.d.ts +24 -0
  21. package/dist/loaders/glbLoader.js +72 -0
  22. package/dist/loaders/index.d.ts +5 -0
  23. package/{src → dist}/loaders/index.js +0 -2
  24. package/dist/{types/loaders → loaders}/ktx2.d.ts +4 -6
  25. package/dist/loaders/ktx2.js +48 -0
  26. package/dist/moons/index.d.ts +3 -0
  27. package/dist/moons/index.js +3 -0
  28. package/dist/moons/moonsGeneration.d.ts +64 -0
  29. package/dist/moons/moonsGeneration.js +238 -0
  30. package/dist/moons/moonsManager.d.ts +43 -0
  31. package/dist/moons/moonsManager.js +93 -0
  32. package/dist/{types/render → render}/frameBudget.d.ts +18 -17
  33. package/dist/render/frameBudget.js +42 -0
  34. package/dist/render/index.d.ts +4 -0
  35. package/dist/render/viewportCompositor.d.ts +124 -0
  36. package/dist/render/viewportCompositor.js +643 -0
  37. package/dist/studio/headerBroadcast.d.ts +10 -1
  38. package/dist/studio/hostTransport.d.ts +2 -2
  39. package/dist/studio/index.d.ts +1 -1
  40. package/dist/utils/dispose.d.ts +5 -0
  41. package/dist/utils/dispose.js +50 -0
  42. package/dist/utils/index.d.ts +3 -0
  43. package/{src → dist}/utils/index.js +1 -1
  44. package/dist/utils/materials.d.ts +10 -0
  45. package/dist/utils/materials.js +36 -0
  46. package/dist/world/cloudShell.d.ts +16 -0
  47. package/dist/world/cloudShell.js +73 -0
  48. package/dist/world/index.d.ts +6 -0
  49. package/{src → dist}/world/index.js +0 -2
  50. package/dist/world/worldGLB.d.ts +17 -0
  51. package/dist/world/worldGLB.js +22 -0
  52. package/dist/world/worldManager.d.ts +57 -0
  53. package/dist/world/worldManager.js +125 -0
  54. package/dist/world/worldNormal.d.ts +30 -0
  55. package/dist/world/worldNormal.js +58 -0
  56. package/dist/world/worldTexture.d.ts +56 -0
  57. package/dist/world/worldTexture.js +299 -0
  58. package/package.json +29 -39
  59. package/src/config/env.ts +48 -0
  60. package/src/config/{index.js → index.ts} +7 -0
  61. package/src/config/{textureConfig.js → textureConfig.ts} +126 -60
  62. package/src/core/{lightFresnel.js → lightFresnel.ts} +1 -1
  63. package/src/core/{lights.js → lights.ts} +21 -21
  64. package/src/loaders/{bodyTextureLoader.js → bodyTextureLoader.ts} +10 -7
  65. package/src/loaders/{cubeMapLoader.js → cubeMapLoader.ts} +83 -49
  66. package/src/loaders/{glbLoader.js → glbLoader.ts} +41 -18
  67. package/src/loaders/index.ts +6 -0
  68. package/src/loaders/{ktx2.js → ktx2.ts} +5 -7
  69. package/src/moons/{index.js → index.ts} +1 -0
  70. package/src/moons/{moonsGeneration.js → moonsGeneration.ts} +45 -32
  71. package/src/moons/{moonsManager.js → moonsManager.ts} +37 -11
  72. package/src/render/{frameBudget.js → frameBudget.ts} +15 -8
  73. package/src/render/index.ts +14 -0
  74. package/src/render/{viewportCompositor.js → viewportCompositor.ts} +121 -59
  75. package/src/studio/headerBroadcast.ts +14 -1
  76. package/src/studio/hostTransport.ts +2 -1
  77. package/src/studio/index.ts +1 -1
  78. package/src/utils/dispose.ts +47 -0
  79. package/src/utils/index.ts +4 -0
  80. package/src/utils/materials.ts +44 -0
  81. package/src/world/{cloudShell.js → cloudShell.ts} +19 -12
  82. package/src/world/index.ts +7 -0
  83. package/src/world/worldGLB.ts +41 -0
  84. package/src/world/{worldManager.js → worldManager.ts} +67 -32
  85. package/src/world/{worldNormal.js → worldNormal.ts} +25 -20
  86. package/src/world/{worldTexture.js → worldTexture.ts} +70 -51
  87. package/dist/types/config/env.d.ts +0 -12
  88. package/dist/types/config/index.d.ts +0 -2
  89. package/dist/types/config/textureConfig.d.ts +0 -121
  90. package/dist/types/core/index.d.ts +0 -2
  91. package/dist/types/core/lightFresnel.d.ts +0 -5
  92. package/dist/types/core/lights.d.ts +0 -58
  93. package/dist/types/index.d.ts +0 -6
  94. package/dist/types/loaders/bodyTextureLoader.d.ts +0 -12
  95. package/dist/types/loaders/cubeMapLoader.d.ts +0 -70
  96. package/dist/types/loaders/glbLoader.d.ts +0 -24
  97. package/dist/types/loaders/index.d.ts +0 -5
  98. package/dist/types/moons/index.d.ts +0 -2
  99. package/dist/types/moons/moonsGeneration.d.ts +0 -72
  100. package/dist/types/moons/moonsManager.d.ts +0 -27
  101. package/dist/types/render/index.d.ts +0 -2
  102. package/dist/types/render/viewportCompositor.d.ts +0 -58
  103. package/dist/types/utils/dispose.d.ts +0 -2
  104. package/dist/types/utils/index.d.ts +0 -2
  105. package/dist/types/utils/materials.d.ts +0 -2
  106. package/dist/types/world/cloudShell.d.ts +0 -25
  107. package/dist/types/world/index.d.ts +0 -6
  108. package/dist/types/world/worldGLB.d.ts +0 -20
  109. package/dist/types/world/worldManager.d.ts +0 -63
  110. package/dist/types/world/worldNormal.d.ts +0 -43
  111. package/dist/types/world/worldTexture.d.ts +0 -80
  112. package/src/config/env.js +0 -38
  113. package/src/utils/dispose.js +0 -39
  114. package/src/utils/materials.js +0 -31
  115. package/src/world/worldGLB.js +0 -19
  116. /package/{src → dist}/render/index.js +0 -0
  117. /package/src/core/{index.js → index.ts} +0 -0
  118. /package/src/{index.js → index.ts} +0 -0
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @file src/render/frameBudget.js
2
+ * @file src/render/frameBudget.ts
3
3
  * @description "Has enough time passed to draw again?" — the whole of a frame cap, kept apart from
4
4
  * whatever is being capped so it can be reasoned about and tested on its own.
5
5
  *
@@ -15,25 +15,32 @@ export const CAPPED_FPS = 30;
15
15
  /** Slack for rAF landing on display ticks — see the file note. */
16
16
  export const FRAME_TOLERANCE_MS = 4;
17
17
 
18
+ /** A frame gate (`createFrameBudget`). */
19
+ export type FrameBudget = {
20
+ /** True when `nowMs` is far enough past the last accepted frame; true without a finite time. */
21
+ shouldRender(nowMs?: number): boolean;
22
+ /** Forget the last frame. */
23
+ reset(): void;
24
+ };
25
+
18
26
  /**
19
27
  * A frame gate. Not a clock and not a scheduler: it answers one question and remembers the last time
20
28
  * the answer was yes.
21
- *
22
- * @param {{ fps?: number, toleranceMs?: number }} [options]
23
- * @returns {{ shouldRender(nowMs: number): boolean, reset(): void }}
24
29
  */
25
- export function createFrameBudget({ fps = CAPPED_FPS, toleranceMs = FRAME_TOLERANCE_MS } = {}) {
30
+ export function createFrameBudget(
31
+ { fps = CAPPED_FPS, toleranceMs = FRAME_TOLERANCE_MS }: { fps?: number; toleranceMs?: number } = {},
32
+ ): FrameBudget {
26
33
  const intervalMs = 1000 / (Number.isFinite(fps) && fps > 0 ? fps : CAPPED_FPS);
27
34
  const minElapsedMs = Math.max(0, intervalMs - toleranceMs);
28
- let lastAtMs = null;
35
+ let lastAtMs: number | null = null;
29
36
 
30
37
  return {
31
38
  /**
32
39
  * True when `nowMs` is far enough past the last accepted frame. Accepting records the time, so
33
40
  * the caller must only ask when it intends to act on the answer.
34
41
  */
35
- shouldRender(nowMs) {
36
- if (!Number.isFinite(nowMs)) return true;
42
+ shouldRender(nowMs?: number) {
43
+ if (nowMs === undefined || !Number.isFinite(nowMs)) return true;
37
44
  if (lastAtMs !== null && nowMs - lastAtMs < minElapsedMs) return false;
38
45
  lastAtMs = nowMs;
39
46
  return true;
@@ -0,0 +1,14 @@
1
+ // Render-host exports
2
+ export { createViewportCompositor } from './viewportCompositor.js';
3
+ export type {
4
+ ViewportCompositor,
5
+ ViewportCompositorOptions,
6
+ RendererProfile,
7
+ VoiceController,
8
+ CompositorVoice,
9
+ CellSource,
10
+ FrameProbe,
11
+ ViewportRect,
12
+ } from './viewportCompositor.js';
13
+ export { createFrameBudget, CAPPED_FPS, FRAME_TOLERANCE_MS } from './frameBudget.js';
14
+ export type { FrameBudget } from './frameBudget.js';
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @file src/render/viewportCompositor.js
2
+ * @file src/render/viewportCompositor.ts
3
3
  * @description A multi-view render host: ONE `WebGLRenderer`, ONE rAF loop, many cells.
4
4
  * Each frame it iterates the registered voices and renders each voice's scene into its own cell via
5
5
  * `setViewport`/`setScissor` (three's "multiple views, one renderer"). This replaces the N-canvas
@@ -50,8 +50,60 @@ import { createFrameBudget } from './frameBudget.js';
50
50
 
51
51
  const GRID_ID = 'multi-orbiter-grid';
52
52
 
53
+ /** The renderer settings a host can override (`rendererProfile`, any subset). */
54
+ export type RendererProfile = {
55
+ antialias: boolean;
56
+ alpha: boolean;
57
+ preserveDrawingBuffer: boolean;
58
+ powerPreference: WebGLPowerPreference;
59
+ maxDevicePixelRatio: number;
60
+ };
61
+
62
+ /** CSS px. GL bottom-left origin in fixed-overlay, viewport top-left in per-cell. */
63
+ export type ViewportRect = { x: number; y: number; width: number; height: number };
64
+
65
+ /** What the compositor calls on a voice's controller (see the host contract in the module doc). */
66
+ export type VoiceController = {
67
+ renderActive: boolean;
68
+ renderViewport: (rect: ViewportRect) => void;
69
+ setViewportSize: (width: number, height: number) => void;
70
+ };
71
+
72
+ /** One voice as `addVoice` takes it. */
73
+ export type CompositorVoice = { voiceId: string; cell: HTMLElement; controller: VoiceController };
74
+
75
+ /** An external owner of the cell DOM (`cellSource`). */
76
+ export type CellSource = {
77
+ acquireCell: (index: number, total: number) => HTMLElement;
78
+ releaseCell?: (index: number) => void;
79
+ };
80
+
81
+ /** One registered voice: its cell, its controller, the last size it was told, its cached rect and,
82
+ * per-cell only, its presentation canvas. */
83
+ type VoiceState = {
84
+ cell: HTMLElement;
85
+ controller: VoiceController;
86
+ lastW: number;
87
+ lastH: number;
88
+ rect: ViewportRect | null;
89
+ blit: { el: HTMLCanvasElement; ctx: CanvasRenderingContext2D | null } | null;
90
+ };
91
+
92
+ /** A load meter's watch on the realm's frames (`setFrameProbe`). */
93
+ export type FrameProbe = {
94
+ everyMs: number;
95
+ onFrame?: (nowMs: number) => void;
96
+ onDraw: (workMs: number, calls: number, triangles: number) => void;
97
+ };
98
+
99
+ /** The vendor-prefixed fullscreen element older Safari still reports; not in the DOM lib. */
100
+ type WebkitDocument = Document & { webkitFullscreenElement?: Element | null };
101
+
102
+ /** `window` read as a bag of host-named slots, for the opt-in dev handle. */
103
+ type DevHandleWindow = Record<string, { renderer?: unknown } | undefined>;
104
+
53
105
  /** Renderer defaults. Overridable via `rendererProfile`; these values are the historical constants. */
54
- const DEFAULT_RENDERER_PROFILE = {
106
+ const DEFAULT_RENDERER_PROFILE: RendererProfile = {
55
107
  antialias: true,
56
108
  alpha: true,
57
109
  preserveDrawingBuffer: false,
@@ -64,33 +116,44 @@ const DEFAULT_RENDERER_PROFILE = {
64
116
  * from flashing empty on that frame. */
65
117
  const CULL_SLOP_PX = 256;
66
118
 
119
+ /** The `createViewportCompositor` options (all optional; see the function). */
120
+ export type ViewportCompositorOptions = {
121
+ cellSource?: CellSource | null;
122
+ presentation?: 'fixed-overlay' | 'per-cell';
123
+ rendererProfile?: Partial<RendererProfile> | null;
124
+ performanceMode?: boolean;
125
+ devHandleKey?: string | null;
126
+ };
127
+
128
+ /** The compositor `createViewportCompositor` returns. */
129
+ export type ViewportCompositor = {
130
+ renderer: THREE.WebGLRenderer;
131
+ canvas: HTMLCanvasElement;
132
+ createCell: (index: number, total: number) => HTMLElement;
133
+ addVoice: (voice: CompositorVoice) => void;
134
+ removeVoice: (voiceId: string) => void;
135
+ renderOnce: () => void;
136
+ setPerformanceMode: (enabled: boolean) => boolean;
137
+ /** Watch the realm's frames for a load meter; `null` stops it. */
138
+ setFrameProbe: (probe: FrameProbe | null) => void;
139
+ dispose: () => void;
140
+ };
141
+
67
142
  /**
68
- * @param {object} [opts]
69
- * @param {{ acquireCell: (index:number, total:number) => HTMLElement, releaseCell?: (index:number) => void } | null} [opts.cellSource]
70
- * external owner of the cell DOM. When present, `createCell` delegates to it and the compositor
71
- * builds no grid of its own; when absent, the default `⌈√n⌉` grid is used.
72
- * @param {'fixed-overlay'|'per-cell'} [opts.presentation='fixed-overlay'] how pixels reach the
73
- * screen — the displayed fixed canvas (full-screen stage layouts) or a per-cell blit canvas the
74
- * DOM positions (cells inside scrolling content). See the module doc.
75
- * @param {Partial<typeof DEFAULT_RENDERER_PROFILE>} [opts.rendererProfile] renderer construction +
76
- * pixel-ratio cap. Omit for the historical defaults.
77
- * @param {boolean} [opts.performanceMode=false] start with the realm's renderer savings on — pixel
78
- * ratio clamped to 1, shadow map off, this loop capped at 30fps. Off by default, and off means the
143
+ * Options, all optional:
144
+ * - `cellSource`: external owner of the cell DOM. When present, `createCell` delegates to it and the
145
+ * compositor builds no grid of its own; when absent, the default `⌈√n⌉` grid is used.
146
+ * - `presentation` (default `'fixed-overlay'`): how pixels reach the screen — the displayed fixed
147
+ * canvas (full-screen stage layouts) or a per-cell blit canvas the DOM positions (cells inside
148
+ * scrolling content). See the module doc.
149
+ * - `rendererProfile`: renderer construction + pixel-ratio cap. Omit for the historical defaults.
150
+ * - `performanceMode` (default false): start with the realm's renderer savings on — pixel ratio
151
+ * clamped to 1, shadow map off, this loop capped at 30fps. Off by default, and off means the
79
152
  * renderer and the cadence are exactly what they were before this option existed. Toggle it live
80
153
  * with `setPerformanceMode`.
81
- * @param {string|null} [opts.devHandleKey=null] when set, publish the returned api on
82
- * `window[devHandleKey]` so tooling can force a composited frame while rAF is throttled (a hidden
83
- * tab during automated checks). Off by default — a library does not name globals for its host.
84
- * @returns {{
85
- * renderer: THREE.WebGLRenderer,
86
- * canvas: HTMLCanvasElement,
87
- * createCell: (index: number, total: number) => HTMLElement,
88
- * addVoice: (voice: { voiceId: string, cell: HTMLElement, controller: object }) => void,
89
- * removeVoice: (voiceId: string) => void,
90
- * renderOnce: () => void,
91
- * setPerformanceMode: (enabled: boolean) => boolean,
92
- * dispose: () => void,
93
- * }}
154
+ * - `devHandleKey` (default null): when set, publish the returned api on `window[devHandleKey]` so
155
+ * tooling can force a composited frame while rAF is throttled (a hidden tab during automated
156
+ * checks). Off by default — a library does not name globals for its host.
94
157
  */
95
158
  export function createViewportCompositor({
96
159
  cellSource = null,
@@ -98,7 +161,7 @@ export function createViewportCompositor({
98
161
  rendererProfile = null,
99
162
  devHandleKey = null,
100
163
  performanceMode = false,
101
- } = {}) {
164
+ }: ViewportCompositorOptions = {}): ViewportCompositor {
102
165
  const profile = { ...DEFAULT_RENDERER_PROFILE, ...(rendererProfile || {}) };
103
166
  // The realm's renderer and the realm's loop are this module's, and they are the only things a
104
167
  // single voice cannot change for itself: pixel ratio, shadow map and frame rate are one per realm,
@@ -110,7 +173,7 @@ export function createViewportCompositor({
110
173
  let performanceOn = Boolean(performanceMode);
111
174
  // What the realm's shadow map was before performance mode touched it, so turning the mode off puts
112
175
  // the host's own choice back rather than asserting a default over it.
113
- let baseShadowMapEnabled = null;
176
+ let baseShadowMapEnabled: boolean | null = null;
114
177
  const frameBudget = createFrameBudget();
115
178
  const perCell = presentation === 'per-cell';
116
179
  // The one WebGL canvas. Fixed-overlay: full-bleed behind the grid, it owns the pixels for ALL
@@ -142,7 +205,8 @@ export function createViewportCompositor({
142
205
  /** Re-assert the realm-wide knobs. Called at construction and whenever the mode changes. */
143
206
  function applyRealmRendererProfile() {
144
207
  applyPixelRatio(renderer, effectiveProfile());
145
- renderer.shadowMap.enabled = performanceOn ? false : baseShadowMapEnabled;
208
+ // Only ever called after construction has read the base value, so it is a boolean here.
209
+ renderer.shadowMap.enabled = performanceOn ? false : (baseShadowMapEnabled as boolean);
146
210
  }
147
211
 
148
212
  const renderer = createRenderer(canvas, effectiveProfile());
@@ -154,14 +218,14 @@ export function createViewportCompositor({
154
218
  /** voiceId → { cell, controller, lastW, lastH, rect, blit } — `rect` is the GL draw rect
155
219
  * (fixed-overlay) or the viewport-relative cull/size box (per-cell); `blit` is the per-cell
156
220
  * presentation canvas + its 2d context (per-cell only). */
157
- const voices = new Map();
158
- let grid = null;
159
- let raf = null;
221
+ const voices = new Map<string, VoiceState>();
222
+ let grid: HTMLDivElement | null = null;
223
+ let raf: number | null = null;
160
224
  let disposed = false;
161
225
  // A load meter's watch on the realm's frames (`setFrameProbe`): null when nobody is watching.
162
- let frameProbe = null;
226
+ let frameProbe: FrameProbe | null = null;
163
227
  let frameProbeAt = 0;
164
- let probePixel = null;
228
+ let probePixel: Uint8Array | null = null;
165
229
  let lastCanvasW = 0;
166
230
  let lastCanvasH = 0;
167
231
  let viewportH = 0; // per-cell cull band (CSS px), refreshed with the rects
@@ -200,13 +264,13 @@ export function createViewportCompositor({
200
264
  // compositor the single owner of its canvas. Per-cell: the cell's own canvas rides into the top
201
265
  // layer with it — no reparenting, only the solo-render (and the buffer grows to the cell's new
202
266
  // size through the normal resize path).
203
- let fullscreenVoiceId = null; // voiceId rendered solo full-bleed while fullscreen, else null
204
- let canvasHome = null; // { parent, next, cssText } to restore the canvas to its realm home on exit
267
+ let fullscreenVoiceId: string | null = null; // voiceId rendered solo full-bleed while fullscreen, else null
268
+ let canvasHome: { parent: ParentNode | null; next: ChildNode | null; cssText: string } | null = null; // { parent, next, cssText } to restore the canvas to its realm home on exit
205
269
  const fullscreenElement = () =>
206
270
  (typeof document !== 'undefined'
207
- ? document.fullscreenElement || document.webkitFullscreenElement || null
271
+ ? document.fullscreenElement || (document as WebkitDocument).webkitFullscreenElement || null
208
272
  : null);
209
- const voiceIdForCell = (el) => {
273
+ const voiceIdForCell = (el: Element | null) => {
210
274
  if (!el) return null;
211
275
  for (const [id, voice] of voices) if (voice.cell === el) return id;
212
276
  return null;
@@ -237,7 +301,8 @@ export function createViewportCompositor({
237
301
  // Direct switch from another fullscreen cell (no intervening exit): restore FIRST so canvasHome
238
302
  // captures the true realm home, not the previous cell / already-mutated style.
239
303
  if (fullscreenVoiceId) restoreCanvasHome();
240
- const cell = voices.get(enteringId)?.cell; // enteringId came from a cell match, so this is set
304
+ // enteringId came from a cell match, so this is set.
305
+ const cell = voices.get(enteringId)?.cell as HTMLElement;
241
306
  // Remember where the canvas belongs, then move it into the fullscreened cell. z-index:0 keeps
242
307
  // it BEHIND the cell's pointer-events:none chrome host (a positioned, z-auto later sibling), so
243
308
  // the buttons still show; it stays position:fixed;inset:0 to fill the now-full-viewport cell.
@@ -301,7 +366,7 @@ export function createViewportCompositor({
301
366
  }
302
367
 
303
368
  /** Create-or-get the CSS grid that lays the cells out (square-ish: ⌈√n⌉ columns). */
304
- function ensureGrid(total) {
369
+ function ensureGrid(total: number) {
305
370
  if (grid) return grid;
306
371
  const cols = Math.ceil(Math.sqrt(total));
307
372
  const rows = Math.ceil(total / cols);
@@ -323,7 +388,7 @@ export function createViewportCompositor({
323
388
  return grid;
324
389
  }
325
390
 
326
- function createCell(index, total) {
391
+ function createCell(index: number, total: number) {
327
392
  // Collection mode: the slot layout owns the cell DOM (a resizable slot). Borrow its element as this
328
393
  // voice's viewport rect + GUI mount — the compositor builds no grid of its own.
329
394
  if (cellSource) {
@@ -344,7 +409,7 @@ export function createViewportCompositor({
344
409
  * positions it (absolute, inset:0), so it scrolls with the page on the compositor thread — the
345
410
  * whole point of the mode. First child + z-index:0 keeps it under the cell's chrome; pointer
346
411
  * events pass through to the cell, which owns the voice's camera input binding. */
347
- function createBlitCanvas(cell) {
412
+ function createBlitCanvas(cell: HTMLElement) {
348
413
  // An absolutely-positioned child anchors to the nearest positioned ancestor — make sure that's
349
414
  // the cell itself, not something above it.
350
415
  if (typeof getComputedStyle === 'function' && getComputedStyle(cell).position === 'static') {
@@ -359,7 +424,7 @@ export function createViewportCompositor({
359
424
  return { el, ctx };
360
425
  }
361
426
 
362
- function addVoice({ voiceId, cell, controller }) {
427
+ function addVoice({ voiceId, cell, controller }: CompositorVoice) {
363
428
  if (!voiceId || !cell || !controller) return;
364
429
  const blit = perCell ? createBlitCanvas(cell) : null;
365
430
  voices.set(voiceId, { cell, controller, lastW: 0, lastH: 0, rect: null, blit });
@@ -367,7 +432,7 @@ export function createViewportCompositor({
367
432
  markRectsDirty();
368
433
  }
369
434
 
370
- function removeVoice(voiceId) {
435
+ function removeVoice(voiceId: string) {
371
436
  const voice = voices.get(voiceId);
372
437
  if (!voice) return;
373
438
  // If this voice is fullscreen its cell hosts the shared canvas — restore the canvas to its
@@ -509,7 +574,7 @@ export function createViewportCompositor({
509
574
  }
510
575
  }
511
576
 
512
- function tick(nowMs) {
577
+ function tick(nowMs: number) {
513
578
  if (disposed) return;
514
579
  raf = requestAnimationFrame(tick);
515
580
  // The cap belongs HERE, to the realm's own loop — never to a voice declining to draw inside
@@ -566,22 +631,18 @@ export function createViewportCompositor({
566
631
  * every `everyMs` timed to the end of its drawing (`onDraw(workMs, calls, triangles)`, every cell
567
632
  * together). Only the timed frame waits for the graphics chip; every other frame is untouched.
568
633
  * `null` stops it.
569
- *
570
- * @param {{ everyMs: number, onFrame?: (nowMs: number) => void, onDraw: (workMs: number, calls: number, triangles: number) => void } | null} probe
571
634
  */
572
- function setFrameProbe(probe) {
635
+ function setFrameProbe(probe: FrameProbe | null) {
573
636
  frameProbe = probe ?? null;
574
637
  frameProbeAt = 0;
575
638
  }
576
639
 
577
640
  /**
578
641
  * Turn the realm's performance mode on or off, live. Clamps the realm pixel ratio to 1, turns the
579
- * shadow map off and caps this loop at 30fps while on; puts all three back when off.
580
- *
581
- * @param {boolean} enabled
582
- * @returns {boolean} whether the mode actually changed
642
+ * shadow map off and caps this loop at 30fps while on; puts all three back when off. Returns
643
+ * whether the mode actually changed.
583
644
  */
584
- function setPerformanceMode(enabled) {
645
+ function setPerformanceMode(enabled: boolean) {
585
646
  const next = Boolean(enabled);
586
647
  if (disposed || next === performanceOn) return false;
587
648
  performanceOn = next;
@@ -610,8 +671,9 @@ export function createViewportCompositor({
610
671
  restoreCanvasHome();
611
672
  for (const voice of voices.values()) voice.blit?.el.remove();
612
673
  voices.clear();
613
- if (typeof window !== 'undefined' && devHandleKey && window[devHandleKey]?.renderer === renderer) {
614
- delete window[devHandleKey];
674
+ if (typeof window !== 'undefined' && devHandleKey) {
675
+ const devWindow = window as unknown as DevHandleWindow;
676
+ if (devWindow[devHandleKey]?.renderer === renderer) delete devWindow[devHandleKey];
615
677
  }
616
678
  renderer.dispose();
617
679
  grid?.remove();
@@ -621,7 +683,7 @@ export function createViewportCompositor({
621
683
 
622
684
  raf = requestAnimationFrame(tick);
623
685
 
624
- const api = {
686
+ const api: ViewportCompositor = {
625
687
  renderer,
626
688
  canvas,
627
689
  createCell,
@@ -635,12 +697,12 @@ export function createViewportCompositor({
635
697
  // Dev handle: lets tooling force a composited frame when rAF is throttled (e.g. a hidden/background
636
698
  // tab during automated checks). The HOST names it — this module invents no global of its own.
637
699
  if (devHandleKey && typeof window !== 'undefined') {
638
- window[devHandleKey] = api;
700
+ (window as unknown as DevHandleWindow)[devHandleKey] = api;
639
701
  }
640
702
  return api;
641
703
  }
642
704
 
643
- function createRenderer(canvas, profile) {
705
+ function createRenderer(canvas: HTMLCanvasElement, profile: RendererProfile) {
644
706
  const renderer = new THREE.WebGLRenderer({
645
707
  canvas,
646
708
  antialias: profile.antialias,
@@ -654,7 +716,7 @@ function createRenderer(canvas, profile) {
654
716
  return renderer;
655
717
  }
656
718
 
657
- function applyPixelRatio(renderer, profile) {
719
+ function applyPixelRatio(renderer: THREE.WebGLRenderer, profile: RendererProfile) {
658
720
  const dpr =
659
721
  typeof window !== 'undefined' && Number.isFinite(window.devicePixelRatio) ? window.devicePixelRatio : 1;
660
722
  renderer.setPixelRatio(Math.min(dpr, profile.maxDevicePixelRatio));
@@ -15,7 +15,20 @@ type Setter = (arg: never) => void
15
15
  /** A header sink implements any subset of the setters. */
16
16
  export type HeaderSinkOf<K extends string> = Partial<Record<K, Setter>>
17
17
 
18
- export type HeaderBroadcast<Shell extends object, K extends string> = Shell & {
18
+ /** The setter the wrapper puts on `P`: the shell's own one-argument signature, or any argument. */
19
+ type ForwardingSetter<Shell, P extends string> = P extends keyof Shell
20
+ ? Exclude<Shell[P], undefined> extends (arg: infer A) => unknown
21
+ ? (arg: A) => void
22
+ : (arg: unknown) => void
23
+ : (arg: unknown) => void
24
+
25
+ /**
26
+ * The wrapped shell: the shell's own members, a forwarding setter (always there) for every header
27
+ * key, including keys the shell lacks (they reach the sink only), and `replayInto`. Header keys
28
+ * typed only as `string` name no setter.
29
+ */
30
+ export type HeaderBroadcast<Shell extends object, K extends string> = Shell &
31
+ (string extends K ? unknown : { [P in K]-?: ForwardingSetter<Shell, P> }) & {
19
32
  /** Push the last value of every header setter into a freshly registered sink. */
20
33
  replayInto(sink: HeaderSinkOf<K> | null | undefined): void
21
34
  }
@@ -8,7 +8,8 @@
8
8
  // The app-specific half — what a draft looks like, which API saves it, which message types exist
9
9
  // — stays in each app and drives an AutosaveSession.
10
10
 
11
- interface WindowLike {
11
+ /** The part of a window the host transport reads: its origin, its parent and `postMessage`. */
12
+ export interface WindowLike {
12
13
  location: { origin: string }
13
14
  parent: WindowLike | null
14
15
  postMessage(message: unknown, targetOrigin: string, transfer?: Transferable[]): void
@@ -27,4 +27,4 @@ export {
27
27
  type StatusSink,
28
28
  type StatusShowOptions,
29
29
  } from './statusChannel.js'
30
- export { isTrustedHostOrigin, isEmbedded, postToHost } from './hostTransport.js'
30
+ export { isTrustedHostOrigin, isEmbedded, postToHost, type WindowLike } from './hostTransport.js'
@@ -0,0 +1,47 @@
1
+ // Three.js GPU resources are not garbage-collected — geometries, materials, and
2
+ // the textures hanging off materials must be disposed explicitly on teardown, or
3
+ // the consuming app leaks VRAM on every world swap / moon rebuild.
4
+ import type * as THREE from 'three';
5
+
6
+ /** Anything with a `dispose()` method, as a material's fields are probed for. */
7
+ type MaybeDisposable = { dispose?: unknown } | null | undefined;
8
+
9
+ /** Disposes one material or an array of them, and every disposable value hanging off each. */
10
+ export function disposeMaterial(material: THREE.Material | readonly (THREE.Material | null | undefined)[] | null | undefined): void {
11
+ if (!material) return;
12
+ // A mesh can carry a single material or an array (multi-material meshes, some GLB).
13
+ const materials: readonly (THREE.Material | null | undefined)[] = Array.isArray(material) ? material : [material];
14
+ for (const mat of materials) {
15
+ if (!mat) continue;
16
+ // Free the textures (and any other disposable values) hanging off the material…
17
+ for (const key in mat) {
18
+ const val = (mat as unknown as Record<string, MaybeDisposable>)[key];
19
+ if (val && typeof val.dispose === 'function') {
20
+ try { (val as { dispose(): void }).dispose(); } catch {}
21
+ }
22
+ }
23
+ // …then free the material's own GPU program. Without this the shader program
24
+ // leaks even though its textures were released. `dispose` is a prototype method,
25
+ // so the loop above never reaches it — no double-dispose.
26
+ if (typeof mat.dispose === 'function') {
27
+ try { mat.dispose(); } catch {}
28
+ }
29
+ }
30
+ }
31
+
32
+ /** Disposes the geometry and materials of every mesh under `obj`, then detaches it from its parent. */
33
+ export function disposeObject3D(obj: THREE.Object3D | null | undefined): void {
34
+ if (!obj) return;
35
+ const root = obj;
36
+ root.traverse((child) => {
37
+ if ((child as THREE.Mesh).isMesh) {
38
+ const mesh = child as THREE.Mesh;
39
+ if (mesh.geometry) mesh.geometry.dispose();
40
+ disposeMaterial(mesh.material);
41
+ // An InstancedMesh also owns `instanceMatrix` / `instanceColor` GPU buffers, which hang off the
42
+ // mesh rather than the (shared) geometry — only its own dispose() frees them.
43
+ if ((child as THREE.InstancedMesh).isInstancedMesh) (child as THREE.InstancedMesh).dispose();
44
+ }
45
+ });
46
+ if (root.parent) root.parent.remove(root);
47
+ }
@@ -0,0 +1,4 @@
1
+ // Utilities exports
2
+ export { ensureStandardMaterial, applyFlagsToMaterial } from './materials.js';
3
+ export type { MaterialFlags } from './materials.js';
4
+ export { disposeObject3D } from './dispose.js';
@@ -0,0 +1,44 @@
1
+ import * as THREE from 'three';
2
+
3
+ /** The material fields `applyFlagsToMaterial` touches; `transmission` exists on physical materials only. */
4
+ type FlaggableMaterial = THREE.Material & { wireframe?: boolean; transmission?: number };
5
+
6
+ /** The flags `applyFlagsToMaterial` reads; other fields are ignored. */
7
+ export interface MaterialFlags {
8
+ forceOpaque?: boolean;
9
+ wireframe?: boolean;
10
+ }
11
+
12
+ /** Gives a mesh a MeshStandardMaterial (keeping its map) unless it has one; anything that is not a mesh is left alone. */
13
+ export function ensureStandardMaterial(mesh: THREE.Object3D | null | undefined): void {
14
+ if (!mesh || !(mesh as THREE.Mesh).isMesh) return;
15
+ const m = mesh as THREE.Mesh;
16
+ const mat = m.material as THREE.Material & { map?: THREE.Texture | null };
17
+ if (mat && !(mat instanceof THREE.MeshStandardMaterial)) {
18
+ m.material = new THREE.MeshStandardMaterial({
19
+ map: mat.map || null,
20
+ metalness: 0.3,
21
+ roughness: 0.7,
22
+ });
23
+ }
24
+ }
25
+
26
+ /** Applies `flags` to one material (not an array); nothing happens without a material. */
27
+ export function applyFlagsToMaterial(material: THREE.Material | null | undefined, flags: MaterialFlags = {}): void {
28
+ if (!material) return;
29
+ const mat = material as FlaggableMaterial;
30
+ const { forceOpaque, wireframe } = flags;
31
+ if (forceOpaque) {
32
+ if (mat.transparent || (mat.transmission && mat.transmission > 0)) {
33
+ mat.transparent = false;
34
+ mat.opacity = 1;
35
+ if ('transmission' in mat) mat.transmission = 0;
36
+ mat.alphaTest = 0;
37
+ mat.needsUpdate = true;
38
+ }
39
+ }
40
+ if (wireframe) {
41
+ mat.wireframe = true;
42
+ mat.needsUpdate = true;
43
+ }
44
+ }
@@ -8,6 +8,12 @@ async function makeTextureMaterial({
8
8
  textureUrl,
9
9
  speed = 0.015,
10
10
  renderer = null,
11
+ }: {
12
+ color?: number;
13
+ opacity?: number;
14
+ textureUrl?: string | undefined;
15
+ speed?: number;
16
+ renderer?: THREE.WebGLRenderer | null;
11
17
  } = {}) {
12
18
  // Diffuse-only (Lambert): the star's key light costs no specular pass.
13
19
  const mat = new THREE.MeshLambertMaterial({
@@ -41,7 +47,7 @@ async function makeTextureMaterial({
41
47
  // Randomize wind direction per instance (0..360)
42
48
  const dirRad = (Math.random() * 360 * Math.PI) / 180;
43
49
  const wind = new THREE.Vector2(Math.cos(dirRad), Math.sin(dirRad)).multiplyScalar(speed);
44
- mat.userData.update = (timeSec) => {
50
+ mat.userData.update = (timeSec: number) => {
45
51
  const t = mat.alphaMap;
46
52
  if (t) {
47
53
  t.offset.x = wind.x * timeSec;
@@ -54,17 +60,8 @@ async function makeTextureMaterial({
54
60
  }
55
61
 
56
62
  /**
57
- * @param {object} [opts]
58
- * @param {import('three').BufferGeometry} [opts.baseGeometry] cloned for the shell; when absent a
59
- * sphere is built from `baseRadius` and the segment counts.
60
- * @param {number} [opts.baseRadius=0.5]
61
- * @param {number} [opts.widthSegments=64]
62
- * @param {number} [opts.heightSegments=64]
63
- * @param {string} [opts.textureUrl]
64
- * @param {number} [opts.color=0xffffff]
65
- * @param {number} [opts.opacity=0.6]
66
- * @param {number} [opts.speed=0.015]
67
- * @param {import('three').WebGLRenderer | null} [opts.renderer]
63
+ * `baseGeometry` is cloned for the shell; when absent a sphere is built from `baseRadius` (0.5) and
64
+ * the segment counts (64 × 64). Defaults: `color` 0xffffff, `opacity` 0.6, `speed` 0.015.
68
65
  */
69
66
  export async function createCloudShell({
70
67
  baseGeometry,
@@ -76,6 +73,16 @@ export async function createCloudShell({
76
73
  opacity = 0.6,
77
74
  speed = 0.015,
78
75
  renderer = null,
76
+ }: {
77
+ baseGeometry?: THREE.BufferGeometry | undefined;
78
+ baseRadius?: number | undefined;
79
+ widthSegments?: number | undefined;
80
+ heightSegments?: number | undefined;
81
+ textureUrl?: string | undefined;
82
+ color?: number | undefined;
83
+ opacity?: number | undefined;
84
+ speed?: number | undefined;
85
+ renderer?: THREE.WebGLRenderer | null | undefined;
79
86
  } = {}) {
80
87
  const geom = baseGeometry
81
88
  ? baseGeometry.clone()
@@ -0,0 +1,7 @@
1
+ // World system exports
2
+ export { WorldManager } from './worldManager.js';
3
+ export { makeSphere, EARTH_RADIUS_UNITS } from './worldTexture.js';
4
+ export { makeNormalSphere } from './worldNormal.js';
5
+ export { makeGLB } from './worldGLB.js';
6
+ export { createCloudShell } from './cloudShell.js';
7
+ export type { WorldCreator, WorldModeArgs } from './worldManager.js';
@@ -0,0 +1,41 @@
1
+ import type * as THREE from 'three';
2
+ import { loadGLB } from '../loaders/glbLoader.js';
3
+
4
+ /**
5
+ * `emissiveIntensity`: textured self-glow (1 = old flat-lit look; ≈0.15 under a star light).
6
+ * - `modelUrl`: REQUIRED; throws without it.
7
+ * - `flags`: material flags (`forceOpaque`, `wireframe`).
8
+ * - `retries` (3), `withCredentials` (false).
9
+ * - `renderer`: REQUIRED at runtime (`loadGLB` throws without one); nullable here because
10
+ * `WorldManager` threads its own, which may be null.
11
+ */
12
+ export async function makeGLB(
13
+ {
14
+ modelUrl,
15
+ flags,
16
+ retries = 3,
17
+ withCredentials = false,
18
+ renderer,
19
+ emissiveIntensity,
20
+ }: {
21
+ modelUrl: string;
22
+ flags?: object | undefined;
23
+ retries?: number | undefined;
24
+ withCredentials?: boolean | undefined;
25
+ renderer?: THREE.WebGLRenderer | null | undefined;
26
+ emissiveIntensity?: number | undefined;
27
+ // Optional as a whole, as the JSDoc had it: without them the `modelUrl` guard below throws.
28
+ } = {} as { modelUrl: string },
29
+ // What `loadGLB` resolves to: the loaded scene (`undefined` only when `retries` is 0 or less).
30
+ ): Promise<THREE.Group | undefined> {
31
+ if (!modelUrl) throw new Error('modelUrl is required');
32
+ // A null or missing renderer makes `loadGLB` throw, as it always did (see above).
33
+ const obj = await loadGLB(modelUrl, {
34
+ retries,
35
+ applyFlags: flags,
36
+ withCredentials,
37
+ renderer: renderer as THREE.WebGLRenderer,
38
+ emissiveIntensity,
39
+ });
40
+ return obj;
41
+ }