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
@@ -0,0 +1,3 @@
1
+ export { MoonsManager } from './moonsManager.js';
2
+ export type { MoonAmountOptions } from './moonsManager.js';
3
+ export { generateMoons, DEFAULT_MOON_TEX_KTX2, DEFAULT_MOON_TEX_PNG, MOON_BASE_RADIUS, MOON_SCALE_MIN, MOON_SCALE_SPAN, MOON_MAX_RADIUS, MOON_ORBIT_SCATTER, MOON_HEIGHT_SCATTER, } from './moonsGeneration.js';
@@ -0,0 +1,3 @@
1
+ // Moons system exports
2
+ export { MoonsManager } from './moonsManager.js';
3
+ export { generateMoons, DEFAULT_MOON_TEX_KTX2, DEFAULT_MOON_TEX_PNG, MOON_BASE_RADIUS, MOON_SCALE_MIN, MOON_SCALE_SPAN, MOON_MAX_RADIUS, MOON_ORBIT_SCATTER, MOON_HEIGHT_SCATTER, } from './moonsGeneration.js';
@@ -0,0 +1,64 @@
1
+ import * as THREE from 'three';
2
+ export declare const DEFAULT_MOON_TEX_KTX2: string;
3
+ export declare const DEFAULT_MOON_TEX_PNG: string;
4
+ /** Radius of the shared moon sphere before per-moon scale. */
5
+ export declare const MOON_BASE_RADIUS = 0.1;
6
+ /** Per-moon scale range: MIN + random * SPAN. */
7
+ export declare const MOON_SCALE_MIN = 0.07;
8
+ export declare const MOON_SCALE_SPAN = 0.15;
9
+ /** Largest moon radius in scene units (base radius × largest scale). */
10
+ export declare const MOON_MAX_RADIUS: number;
11
+ /** Each moon's orbit radius varies by ± half of this around the requested orbit. */
12
+ export declare const MOON_ORBIT_SCATTER = 0.22;
13
+ /** Each moon sits up to ± half of this above or below the orbit plane. */
14
+ export declare const MOON_HEIGHT_SCATTER = 0.12;
15
+ /**
16
+ * - `count` (0): 0 or less resolves to null. `orbitRadius` 1.5.
17
+ * - `textureFallback`: used instead of `texturePNG` when set.
18
+ * - `seed`: deterministic positions when set.
19
+ * - `textureKey`: replaces `textureKTX2` when set (the fallback stays `textureFallback` or
20
+ * `texturePNG`).
21
+ * - `emissiveLift`: night-side self-glow 0..1; omitted = `emissiveIntensity`.
22
+ * - `renderer`: binds the KTX2 moon texture.
23
+ * - Defaults: `moonColor` '#ffffff', `brightness` 1.15, `emissiveColor` 0x333333,
24
+ * `emissiveIntensity` 0.18, `roughness` 0.85, `metalness` 0, `debug` true, `smooth` true,
25
+ * `geometryWidthSegments` 16, `geometryHeightSegments` 12, `useBumpFromColor` true, `bumpScale`
26
+ * 0.15, `debugShowUvGrid` false, `debugShowReferenceMoon` false, `referenceMoonScale` 0.25,
27
+ * `useEmissiveMapFromTexture` true.
28
+ */
29
+ export declare function generateMoons({ count, orbitRadius, textureKTX2, texturePNG, textureFallback, useTexture, seed, // if provided, ensures consistent moon positions
30
+ textureKey, // if provided, overrides textureKTX2/texturePNG
31
+ moonColor, // NEW: moon color
32
+ brightness, emissiveColor, emissiveIntensity, emissiveLift, roughness, metalness, debug, smooth, // set false to keep flatShading
33
+ geometryWidthSegments, // Reduced from 24 for better performance
34
+ geometryHeightSegments, // Reduced from 24 for better performance
35
+ useBumpFromColor, bumpScale, debugShowUvGrid, debugShowReferenceMoon, // changed from true to prevent extra moon
36
+ referenceMoonScale, useEmissiveMapFromTexture, // reuse color texture as emissiveMap to lift dark sides
37
+ renderer, }?: {
38
+ count?: number | undefined;
39
+ orbitRadius?: number | undefined;
40
+ textureKTX2?: string | undefined;
41
+ texturePNG?: string | undefined;
42
+ textureFallback?: string | null | undefined;
43
+ useTexture?: boolean | undefined;
44
+ seed?: string | number | null | undefined;
45
+ textureKey?: string | null | undefined;
46
+ moonColor?: string | undefined;
47
+ brightness?: number | undefined;
48
+ emissiveColor?: number | undefined;
49
+ emissiveIntensity?: number | undefined;
50
+ emissiveLift?: number | undefined;
51
+ roughness?: number | undefined;
52
+ metalness?: number | undefined;
53
+ debug?: boolean | undefined;
54
+ smooth?: boolean | undefined;
55
+ geometryWidthSegments?: number | undefined;
56
+ geometryHeightSegments?: number | undefined;
57
+ useBumpFromColor?: boolean | undefined;
58
+ bumpScale?: number | undefined;
59
+ debugShowUvGrid?: boolean | undefined;
60
+ debugShowReferenceMoon?: boolean | undefined;
61
+ referenceMoonScale?: number | undefined;
62
+ useEmissiveMapFromTexture?: boolean | undefined;
63
+ renderer?: THREE.WebGLRenderer | null | undefined;
64
+ }): Promise<THREE.Group<THREE.Object3DEventMap> | null>;
@@ -0,0 +1,238 @@
1
+ import * as THREE from 'three';
2
+ import { loadTexture } from '../loaders/bodyTextureLoader.js';
3
+ import { resolvePublicUrl } from '../config/env.js';
4
+ function moonTexPath(file) {
5
+ return resolvePublicUrl(`default/textures/moons/${file}`);
6
+ }
7
+ export const DEFAULT_MOON_TEX_KTX2 = moonTexPath('moon-equirectangular.ktx2');
8
+ export const DEFAULT_MOON_TEX_PNG = moonTexPath('moon-equirectangular.png');
9
+ // Moon layout sizes, exported so hosts can keep other bodies clear of the moons without copying numbers.
10
+ /** Radius of the shared moon sphere before per-moon scale. */
11
+ export const MOON_BASE_RADIUS = 0.1;
12
+ /** Per-moon scale range: MIN + random * SPAN. */
13
+ export const MOON_SCALE_MIN = 0.07;
14
+ export const MOON_SCALE_SPAN = 0.15;
15
+ /** Largest moon radius in scene units (base radius × largest scale). */
16
+ export const MOON_MAX_RADIUS = MOON_BASE_RADIUS * (MOON_SCALE_MIN + MOON_SCALE_SPAN);
17
+ /** Each moon's orbit radius varies by ± half of this around the requested orbit. */
18
+ export const MOON_ORBIT_SCATTER = 0.22;
19
+ /** Each moon sits up to ± half of this above or below the orbit plane. */
20
+ export const MOON_HEIGHT_SCATTER = 0.12;
21
+ /**
22
+ * - `count` (0): 0 or less resolves to null. `orbitRadius` 1.5.
23
+ * - `textureFallback`: used instead of `texturePNG` when set.
24
+ * - `seed`: deterministic positions when set.
25
+ * - `textureKey`: replaces `textureKTX2` when set (the fallback stays `textureFallback` or
26
+ * `texturePNG`).
27
+ * - `emissiveLift`: night-side self-glow 0..1; omitted = `emissiveIntensity`.
28
+ * - `renderer`: binds the KTX2 moon texture.
29
+ * - Defaults: `moonColor` '#ffffff', `brightness` 1.15, `emissiveColor` 0x333333,
30
+ * `emissiveIntensity` 0.18, `roughness` 0.85, `metalness` 0, `debug` true, `smooth` true,
31
+ * `geometryWidthSegments` 16, `geometryHeightSegments` 12, `useBumpFromColor` true, `bumpScale`
32
+ * 0.15, `debugShowUvGrid` false, `debugShowReferenceMoon` false, `referenceMoonScale` 0.25,
33
+ * `useEmissiveMapFromTexture` true.
34
+ */
35
+ export async function generateMoons({ count = 0, orbitRadius = 1.5, textureKTX2 = DEFAULT_MOON_TEX_KTX2, texturePNG = DEFAULT_MOON_TEX_PNG, textureFallback = null, useTexture = true,
36
+ // NEW: seed for deterministic positioning
37
+ seed = null, // if provided, ensures consistent moon positions
38
+ // NEW: specific texture key/path
39
+ textureKey = null, // if provided, overrides textureKTX2/texturePNG
40
+ moonColor = '#ffffff', // NEW: moon color
41
+ // lighting tweaks
42
+ brightness = 1.15, emissiveColor = 0x333333, emissiveIntensity = 0.18,
43
+ // Self-glow that lifts the night side. Defaults to `emissiveIntensity` (today's look, for the old
44
+ // flat lighting). With a directional star light, pass something low (≈0.05) so night sides read.
45
+ emissiveLift = undefined, roughness = 0.85, metalness = 0.0,
46
+ // debug & appearance controls
47
+ debug = true, smooth = true, // set false to keep flatShading
48
+ // debug / appearance params
49
+ geometryWidthSegments = 16, // Reduced from 24 for better performance
50
+ geometryHeightSegments = 12, // Reduced from 24 for better performance
51
+ useBumpFromColor = true, bumpScale = 0.15, debugShowUvGrid = false, debugShowReferenceMoon = false, // changed from true to prevent extra moon
52
+ referenceMoonScale = 0.25,
53
+ // behavior flags
54
+ useEmissiveMapFromTexture = true, // reuse color texture as emissiveMap to lift dark sides
55
+ // The renderer whose KTX2 transcoder decodes the moon texture. REQUIRED for the KTX2 texture (3.0:
56
+ // the loader has no global fallback).
57
+ renderer, } = {}) {
58
+ if (count <= 0)
59
+ return null;
60
+ // Seeded random number generator for deterministic positioning
61
+ let seedValue = seed;
62
+ if (typeof seed === 'string') {
63
+ // Convert string seed to number using simple hash
64
+ seedValue = 0;
65
+ for (let i = 0; i < seed.length; i++) {
66
+ const char = seed.charCodeAt(i);
67
+ seedValue = ((seedValue << 5) - seedValue) + char;
68
+ seedValue = seedValue & seedValue; // Convert to 32-bit integer
69
+ }
70
+ }
71
+ // Seeded random function (Linear Congruential Generator)
72
+ // A string seed was hashed to a number above.
73
+ let currentSeed = seedValue !== null ? Math.abs(seedValue) : Date.now();
74
+ const seededRandom = () => {
75
+ if (seedValue !== null) {
76
+ currentSeed = (currentSeed * 1664525 + 1013904223) % 4294967296;
77
+ return currentSeed / 4294967296;
78
+ }
79
+ return Math.random(); // Use regular random if no seed
80
+ };
81
+ const moonGeometry = new THREE.SphereGeometry(MOON_BASE_RADIUS, geometryWidthSegments, geometryHeightSegments);
82
+ // Optional UV grid override
83
+ let sharedTexture = null;
84
+ let usedUvGrid = false;
85
+ async function buildUvGridTexture() {
86
+ const w = 512, h = 256; // 2:1 aspect
87
+ const canvas = document.createElement('canvas');
88
+ canvas.width = w;
89
+ canvas.height = h;
90
+ // A missing 2D context throws on first use; the caller catches it, as before.
91
+ const ctx = canvas.getContext('2d');
92
+ ctx.fillStyle = '#101018';
93
+ ctx.fillRect(0, 0, w, h);
94
+ for (let lon = 0; lon <= 12; lon++) {
95
+ const x = (lon / 12) * w;
96
+ ctx.strokeStyle = lon % 3 === 0 ? '#ff4d4d' : '#444';
97
+ ctx.lineWidth = lon % 3 === 0 ? 2 : 1;
98
+ ctx.beginPath();
99
+ ctx.moveTo(x, 0);
100
+ ctx.lineTo(x, h);
101
+ ctx.stroke();
102
+ }
103
+ for (let lat = 0; lat <= 6; lat++) {
104
+ const y = (lat / 6) * h;
105
+ ctx.strokeStyle = lat % 3 === 0 ? '#4dff7a' : '#444';
106
+ ctx.lineWidth = lat % 3 === 0 ? 2 : 1;
107
+ ctx.beginPath();
108
+ ctx.moveTo(0, y);
109
+ ctx.lineTo(w, y);
110
+ ctx.stroke();
111
+ }
112
+ ctx.fillStyle = 'white';
113
+ ctx.font = '20px monospace';
114
+ ctx.fillText('UV GRID', 10, 24);
115
+ const tex = new THREE.CanvasTexture(canvas);
116
+ tex.colorSpace = THREE.SRGBColorSpace;
117
+ tex.anisotropy = 4;
118
+ return tex;
119
+ }
120
+ if (debugShowUvGrid) {
121
+ try {
122
+ sharedTexture = await buildUvGridTexture();
123
+ usedUvGrid = true;
124
+ }
125
+ catch (_) { }
126
+ }
127
+ else if (useTexture) {
128
+ // Use textureKey if provided, otherwise fall back to default textures
129
+ const primaryTexture = textureKey ? resolvePublicUrl(textureKey) : textureKTX2;
130
+ const fallbackTexture = textureFallback || texturePNG;
131
+ try {
132
+ sharedTexture = await loadTexture(primaryTexture, { renderer });
133
+ }
134
+ catch (eKTX) {
135
+ if (debug)
136
+ console.warn('[Moons] Primary texture failed, trying fallback:', eKTX);
137
+ try {
138
+ sharedTexture = await loadTexture(fallbackTexture, { renderer });
139
+ }
140
+ catch (ePNG) {
141
+ if (debug)
142
+ console.warn('[Moons] Both primary and fallback texture loading failed');
143
+ }
144
+ }
145
+ }
146
+ if (sharedTexture) {
147
+ try {
148
+ sharedTexture.colorSpace = THREE.SRGBColorSpace;
149
+ sharedTexture.anisotropy = 2; // Reduced from 4 for better performance
150
+ }
151
+ catch (e) {
152
+ if (debug)
153
+ console.warn('[Moons] Texture processing failed:', e);
154
+ }
155
+ }
156
+ // `vertexColors` is deliberately NOT set, and setting it here was a bug worth spelling out.
157
+ //
158
+ // The per-moon tint rides on `instanceColor` (set by `setColorAt` below). Three defines
159
+ // USE_INSTANCING_COLOR for that on its own and multiplies it into `vColor` — no material flag
160
+ // needed. `vertexColors` is a DIFFERENT feature: it multiplies by the geometry's `color`
161
+ // ATTRIBUTE. This geometry is a plain SphereGeometry and has no such attribute, so enabling it
162
+ // made the shader multiply by an absent attribute — zero — and every moon rendered BLACK, with the
163
+ // tint annihilated along with it.
164
+ //
165
+ // It only bit the untextured path (`!sharedTexture`), which is the one the performance look uses:
166
+ // moons there carry no texture, so their colour IS the moon, and it was the one case where the
167
+ // colour silently could not work.
168
+ // Diffuse-only (Lambert): the star's key light costs no specular pass, so
169
+ // `roughness` / `metalness` are accepted for older callers but no longer change the look.
170
+ const moonMaterial = new THREE.MeshLambertMaterial({
171
+ map: sharedTexture || null,
172
+ emissive: new THREE.Color(emissiveColor),
173
+ emissiveIntensity: typeof emissiveLift === 'number' && Number.isFinite(emissiveLift) ? Math.min(1, Math.max(0, emissiveLift)) : emissiveIntensity,
174
+ flatShading: !smooth,
175
+ transparent: false,
176
+ depthWrite: true,
177
+ depthTest: true,
178
+ });
179
+ if (sharedTexture && useEmissiveMapFromTexture) {
180
+ moonMaterial.emissiveMap = sharedTexture; // boosts subtle texture detail
181
+ }
182
+ if (!sharedTexture && debug && !debugShowUvGrid) {
183
+ console.warn('[Moons] No texture assigned (using pure color instancing).');
184
+ }
185
+ if (sharedTexture) {
186
+ moonMaterial.map = sharedTexture;
187
+ if (useBumpFromColor) {
188
+ moonMaterial.bumpMap = sharedTexture;
189
+ moonMaterial.bumpScale = bumpScale;
190
+ }
191
+ }
192
+ // Pre-computed color palette for variation (white to dark gray)
193
+ const colorPalette = [
194
+ 0xffffff, 0xf5f5f5, 0xeeeeee, 0xe8e8e8, 0xdddddd, 0xd3d3d3,
195
+ 0xcccccc, 0xc0c0c0, 0xb8b8b8, 0xaaaaaa, 0x999999, 0x888888
196
+ ].map(hex => new THREE.Color(hex));
197
+ const instanced = new THREE.InstancedMesh(moonGeometry, moonMaterial, count + (debugShowReferenceMoon ? 1 : 0));
198
+ // Set render order to ensure proper rendering above glow effects
199
+ instanced.renderOrder = 2000;
200
+ const dummy = new THREE.Object3D();
201
+ let instanceIndex = 0;
202
+ for (let i = 0; i < count; i++) {
203
+ const angle = (i / Math.max(1, count)) * Math.PI * 2;
204
+ const radiusVar = orbitRadius + (seededRandom() - 0.5) * MOON_ORBIT_SCATTER;
205
+ const yOffset = (seededRandom() - 0.5) * MOON_HEIGHT_SCATTER;
206
+ dummy.position.set(radiusVar * Math.cos(angle), yOffset, radiusVar * Math.sin(angle));
207
+ const s = MOON_SCALE_MIN + seededRandom() * MOON_SCALE_SPAN;
208
+ dummy.scale.set(s, s, s);
209
+ dummy.rotation.y = seededRandom() * Math.PI * 2;
210
+ dummy.rotation.x = seededRandom() * 0.3 - 0.15;
211
+ dummy.updateMatrix();
212
+ instanced.setMatrixAt(instanceIndex, dummy.matrix);
213
+ // Always set instance color as tint, even if texture is present
214
+ const base = new THREE.Color(moonColor || '#ffffff');
215
+ instanced.setColorAt(instanceIndex, base);
216
+ instanceIndex++;
217
+ }
218
+ if (debugShowReferenceMoon) {
219
+ dummy.position.set(orbitRadius * 0.6, 0.05, orbitRadius * 0.2);
220
+ dummy.scale.set(referenceMoonScale, referenceMoonScale, referenceMoonScale);
221
+ dummy.rotation.y = seededRandom() * Math.PI * 2;
222
+ dummy.rotation.x = seededRandom() * 0.3 - 0.15;
223
+ dummy.updateMatrix();
224
+ instanced.setMatrixAt(instanceIndex, dummy.matrix);
225
+ const refColor = new THREE.Color('#ffffff');
226
+ instanced.setColorAt(instanceIndex, refColor);
227
+ }
228
+ instanced.instanceMatrix.needsUpdate = true;
229
+ if (instanced.instanceColor)
230
+ instanced.instanceColor.needsUpdate = true;
231
+ const group = new THREE.Group();
232
+ group.rotation.x = THREE.MathUtils.degToRad(15);
233
+ group.add(instanced);
234
+ group.name = 'moonsGroup';
235
+ // Teardown goes through `disposeObject3D` (MoonsManager.dispose / setAmount), which frees the
236
+ // geometry, the material's textures and the instance buffers.
237
+ return group;
238
+ }
@@ -0,0 +1,43 @@
1
+ import * as THREE from 'three';
2
+ /** The `setAmount` options: what the moons look like and where they orbit. */
3
+ export interface MoonAmountOptions {
4
+ /** 1.5 by default. */
5
+ orbitRadius?: number;
6
+ textureKTX2?: string;
7
+ texturePNG?: string;
8
+ /** true by default. */
9
+ useTexture?: boolean;
10
+ /** Deterministic positions when set. */
11
+ seed?: string | number | null;
12
+ /** Replaces `textureKTX2` when set. */
13
+ textureKey?: string | null;
14
+ /** '#ffffff' by default. */
15
+ moonColor?: string;
16
+ /** Used instead of `texturePNG` when set. */
17
+ textureFallback?: string | null;
18
+ /** Night-side self-glow 0..1, kept for later rebuilds; anything but a number is ignored. */
19
+ emissiveLift?: number;
20
+ }
21
+ export declare class MoonsManager {
22
+ scene: THREE.Scene;
23
+ renderer: THREE.WebGLRenderer | null | undefined;
24
+ group: THREE.Group | null;
25
+ _opId: number;
26
+ _emissiveLift: number | undefined;
27
+ /** `renderer`: the renderer the moon texture binds to (KTX2). */
28
+ constructor(scene: THREE.Scene, { renderer }?: {
29
+ renderer?: THREE.WebGLRenderer | null | undefined;
30
+ });
31
+ /** Replace the moons with `count` new ones; 0 or less (or none) just removes them. */
32
+ setAmount(count: number | null | undefined, opts?: MoonAmountOptions): Promise<void>;
33
+ /**
34
+ * Night-side self-glow 0..1, applied in place to the live moons and kept for later rebuilds.
35
+ * Low values (≈0.05) let a star-lit scene show the moons' dark sides.
36
+ */
37
+ setEmissiveLift(v: number): void;
38
+ /**
39
+ * Remove the moons from the scene and free their GPU resources. `scene.remove(group)` alone leaks
40
+ * the instanced geometry, materials and the shared moon texture on every view teardown.
41
+ */
42
+ dispose(): void;
43
+ }
@@ -0,0 +1,93 @@
1
+ import { disposeObject3D } from '../utils/dispose.js';
2
+ import * as THREE from 'three';
3
+ import { generateMoons } from './moonsGeneration.js';
4
+ export class MoonsManager {
5
+ /** `renderer`: the renderer the moon texture binds to (KTX2). */
6
+ constructor(scene, { renderer } = {}) {
7
+ this.scene = scene;
8
+ this.renderer = renderer;
9
+ this.group = null;
10
+ this._opId = 0; // concurrency guard
11
+ this._emissiveLift = undefined; // night-side self-glow; undefined = generator default
12
+ }
13
+ /** Replace the moons with `count` new ones; 0 or less (or none) just removes them. */
14
+ async setAmount(count, opts = {}) {
15
+ // Increment operation id to invalidate older async calls
16
+ const opId = ++this._opId;
17
+ // remove existing
18
+ if (this.group) {
19
+ disposeObject3D(this.group);
20
+ this.group = null;
21
+ }
22
+ if (!count || count <= 0)
23
+ return;
24
+ // Gather options for texture sharing / orbit params
25
+ const { orbitRadius = 1.5, textureKTX2, texturePNG, useTexture = true, seed = null, // seed for deterministic positioning
26
+ textureKey = null, // NEW: specific texture key/path to use
27
+ moonColor = '#ffffff', // NEW: moon color
28
+ textureFallback = null, emissiveLift, // night-side self-glow 0..1 (undefined = today's look)
29
+ } = opts;
30
+ if (typeof emissiveLift === 'number')
31
+ this._emissiveLift = emissiveLift;
32
+ let newGroup = null;
33
+ try {
34
+ newGroup = await generateMoons({
35
+ count,
36
+ orbitRadius,
37
+ textureKTX2,
38
+ texturePNG,
39
+ useTexture,
40
+ seed,
41
+ textureKey, // Pass texture key for custom moon textures
42
+ textureFallback,
43
+ moonColor, // Pass moon color to generator
44
+ emissiveLift: this._emissiveLift,
45
+ renderer: this.renderer,
46
+ });
47
+ }
48
+ catch (e) {
49
+ console.warn('[MoonsManager] Failed to generate moons:', e);
50
+ return;
51
+ }
52
+ // If another call started meanwhile, abandon this result
53
+ if (opId !== this._opId) {
54
+ if (newGroup)
55
+ disposeObject3D(newGroup);
56
+ return;
57
+ }
58
+ if (newGroup instanceof THREE.Object3D) {
59
+ this.group = newGroup;
60
+ this.scene.add(this.group);
61
+ }
62
+ else if (newGroup) {
63
+ console.warn('[MoonsManager] generateMoons did not return an Object3D');
64
+ }
65
+ }
66
+ /**
67
+ * Night-side self-glow 0..1, applied in place to the live moons and kept for later rebuilds.
68
+ * Low values (≈0.05) let a star-lit scene show the moons' dark sides.
69
+ */
70
+ setEmissiveLift(v) {
71
+ if (typeof v !== 'number' || !Number.isFinite(v))
72
+ return;
73
+ this._emissiveLift = Math.min(1, Math.max(0, v));
74
+ this.group?.traverse((object) => {
75
+ // Only a mesh whose material has `emissiveIntensity` is changed; the value was just set above.
76
+ const child = object;
77
+ if (child.isMesh && child.material && 'emissiveIntensity' in child.material) {
78
+ child.material.emissiveIntensity = this._emissiveLift;
79
+ }
80
+ });
81
+ }
82
+ /**
83
+ * Remove the moons from the scene and free their GPU resources. `scene.remove(group)` alone leaks
84
+ * the instanced geometry, materials and the shared moon texture on every view teardown.
85
+ */
86
+ dispose() {
87
+ this._opId++; // abandon any in-flight setAmount
88
+ if (this.group) {
89
+ disposeObject3D(this.group);
90
+ this.group = null;
91
+ }
92
+ }
93
+ }
@@ -1,19 +1,5 @@
1
1
  /**
2
- * A frame gate. Not a clock and not a scheduler: it answers one question and remembers the last time
3
- * the answer was yes.
4
- *
5
- * @param {{ fps?: number, toleranceMs?: number }} [options]
6
- * @returns {{ shouldRender(nowMs: number): boolean, reset(): void }}
7
- */
8
- export function createFrameBudget({ fps, toleranceMs }?: {
9
- fps?: number;
10
- toleranceMs?: number;
11
- }): {
12
- shouldRender(nowMs: number): boolean;
13
- reset(): void;
14
- };
15
- /**
16
- * @file src/render/frameBudget.js
2
+ * @file src/render/frameBudget.ts
17
3
  * @description "Has enough time passed to draw again?" — the whole of a frame cap, kept apart from
18
4
  * whatever is being capped so it can be reasoned about and tested on its own.
19
5
  *
@@ -24,6 +10,21 @@ export function createFrameBudget({ fps, toleranceMs }?: {
24
10
  * near enough through, and the cap lands where it was aimed.
25
11
  */
26
12
  /** Frames a capped loop is allowed per second. */
27
- export const CAPPED_FPS: 30;
13
+ export declare const CAPPED_FPS = 30;
28
14
  /** Slack for rAF landing on display ticks — see the file note. */
29
- export const FRAME_TOLERANCE_MS: 4;
15
+ export declare const FRAME_TOLERANCE_MS = 4;
16
+ /** A frame gate (`createFrameBudget`). */
17
+ export type FrameBudget = {
18
+ /** True when `nowMs` is far enough past the last accepted frame; true without a finite time. */
19
+ shouldRender(nowMs?: number): boolean;
20
+ /** Forget the last frame. */
21
+ reset(): void;
22
+ };
23
+ /**
24
+ * A frame gate. Not a clock and not a scheduler: it answers one question and remembers the last time
25
+ * the answer was yes.
26
+ */
27
+ export declare function createFrameBudget({ fps, toleranceMs }?: {
28
+ fps?: number;
29
+ toleranceMs?: number;
30
+ }): FrameBudget;
@@ -0,0 +1,42 @@
1
+ /**
2
+ * @file src/render/frameBudget.ts
3
+ * @description "Has enough time passed to draw again?" — the whole of a frame cap, kept apart from
4
+ * whatever is being capped so it can be reasoned about and tested on its own.
5
+ *
6
+ * The tolerance is the part that is easy to get wrong. rAF does not deliver frames at arbitrary
7
+ * times; it delivers them on the display's ticks. On a 60Hz display those arrive every ~16.7ms, so a
8
+ * strict `elapsed >= 33.3` test rejects the frame at 33.3ms by a hair and waits for the next one at
9
+ * 50ms — 20fps, not the 30 that was asked for. A few milliseconds of slack lets the frame that is
10
+ * near enough through, and the cap lands where it was aimed.
11
+ */
12
+ /** Frames a capped loop is allowed per second. */
13
+ export const CAPPED_FPS = 30;
14
+ /** Slack for rAF landing on display ticks — see the file note. */
15
+ export const FRAME_TOLERANCE_MS = 4;
16
+ /**
17
+ * A frame gate. Not a clock and not a scheduler: it answers one question and remembers the last time
18
+ * the answer was yes.
19
+ */
20
+ export function createFrameBudget({ fps = CAPPED_FPS, toleranceMs = FRAME_TOLERANCE_MS } = {}) {
21
+ const intervalMs = 1000 / (Number.isFinite(fps) && fps > 0 ? fps : CAPPED_FPS);
22
+ const minElapsedMs = Math.max(0, intervalMs - toleranceMs);
23
+ let lastAtMs = null;
24
+ return {
25
+ /**
26
+ * True when `nowMs` is far enough past the last accepted frame. Accepting records the time, so
27
+ * the caller must only ask when it intends to act on the answer.
28
+ */
29
+ shouldRender(nowMs) {
30
+ if (nowMs === undefined || !Number.isFinite(nowMs))
31
+ return true;
32
+ if (lastAtMs !== null && nowMs - lastAtMs < minElapsedMs)
33
+ return false;
34
+ lastAtMs = nowMs;
35
+ return true;
36
+ },
37
+ /** Forget the last frame, so the next question is answered as if none had been drawn. */
38
+ reset() {
39
+ lastAtMs = null;
40
+ },
41
+ };
42
+ }
@@ -0,0 +1,4 @@
1
+ export { createViewportCompositor } from './viewportCompositor.js';
2
+ export type { ViewportCompositor, ViewportCompositorOptions, RendererProfile, VoiceController, CompositorVoice, CellSource, FrameProbe, ViewportRect, } from './viewportCompositor.js';
3
+ export { createFrameBudget, CAPPED_FPS, FRAME_TOLERANCE_MS } from './frameBudget.js';
4
+ export type { FrameBudget } from './frameBudget.js';
@@ -0,0 +1,124 @@
1
+ /**
2
+ * @file src/render/viewportCompositor.ts
3
+ * @description A multi-view render host: ONE `WebGLRenderer`, ONE rAF loop, many cells.
4
+ * Each frame it iterates the registered voices and renders each voice's scene into its own cell via
5
+ * `setViewport`/`setScissor` (three's "multiple views, one renderer"). This replaces the N-canvas
6
+ * spike, which hit a hard wall: N canvases = N WebGL contexts, and KTX2/compressed textures only
7
+ * upload to the FIRST context, so only voice-0 ever textured. One context → every voice textures.
8
+ *
9
+ * Each voice's controller runs in `sharedRenderer` mode — it contributes a scene+camera and per-frame
10
+ * callbacks but owns no renderer and no loop; this host drives it through `renderViewport(rect)`.
11
+ *
12
+ * Cell geometry has two sources:
13
+ * - default: the compositor builds its own `⌈√n⌉` square grid (`ensureGrid`).
14
+ * - external: a `cellSource` supplies the cells — real DOM the layout owns, so the compositor
15
+ * renders each voice into its cell's rect and never builds or removes grid DOM.
16
+ *
17
+ * ## Presentation modes
18
+ * How rendered pixels reach the screen is a per-host choice (`presentation` option):
19
+ *
20
+ * - `'fixed-overlay'` (default): the WebGL canvas itself is displayed — appended to `document.body`,
21
+ * `position:fixed;inset:0`, each voice scissor-rendered at its cell's on-screen rect. Right for
22
+ * full-screen stage layouts the compositor was built for. Its structural limit: cell positions are
23
+ * re-read on the main thread each frame, while the page scrolls on the compositor thread — so
24
+ * inside a scrolling document the painted rect trails the DOM by a frame or more (the "viewport
25
+ * detaches from the page" symptom). Don't use it under scrolling content.
26
+ *
27
+ * - `'per-cell'`: the WebGL canvas stays detached and is only a render source. Every voice gets a
28
+ * compositor-owned 2D canvas inside its cell (`position:absolute;inset:0`), and each frame the
29
+ * voice is rendered at the buffer's origin and blitted into that canvas (`drawImage`, a GPU-side
30
+ * copy). The DOM owns the pixels' position, so cells scroll glued to the page — scroll can never
31
+ * detach them, no matter how the rAF loop is scheduled. One WebGL context still serves
32
+ * every voice, so the KTX2 single-context constraint holds. Requires each cell to be a positioned
33
+ * element (the compositor makes a `static` cell `relative`); browser-fullscreen needs no canvas
34
+ * reparenting in this mode — the cell brings its own canvas into the top layer.
35
+ *
36
+ * ## Host contract (this module is app-agnostic, but NOT assumption-free)
37
+ * A host adopting it must accept all of the following, or wrap it:
38
+ * - `'fixed-overlay'` only: the canvas is appended to `document.body`, `position:fixed;inset:0`,
39
+ * `z-index:40`, opaque black CSS background; browser-fullscreen on a registered CELL reparents the
40
+ * shared canvas into that cell. The host must not move the canvas itself.
41
+ * - The default grid it may build is `#multi-orbiter-grid` at `z-index:41`.
42
+ * - A voice is `{ voiceId, cell, controller }`, where `controller` exposes `renderActive`,
43
+ * `renderViewport(rect)` and `setViewportSize(w, h)`. Rects are CSS px, GL bottom-left origin.
44
+ * - It owns the realm clear: every frame it re-asserts a fully transparent clear colour.
45
+ *
46
+ * @see `devHandleKey` — the one window global it can write, opt-in.
47
+ */
48
+ import * as THREE from 'three';
49
+ /** The renderer settings a host can override (`rendererProfile`, any subset). */
50
+ export type RendererProfile = {
51
+ antialias: boolean;
52
+ alpha: boolean;
53
+ preserveDrawingBuffer: boolean;
54
+ powerPreference: WebGLPowerPreference;
55
+ maxDevicePixelRatio: number;
56
+ };
57
+ /** CSS px. GL bottom-left origin in fixed-overlay, viewport top-left in per-cell. */
58
+ export type ViewportRect = {
59
+ x: number;
60
+ y: number;
61
+ width: number;
62
+ height: number;
63
+ };
64
+ /** What the compositor calls on a voice's controller (see the host contract in the module doc). */
65
+ export type VoiceController = {
66
+ renderActive: boolean;
67
+ renderViewport: (rect: ViewportRect) => void;
68
+ setViewportSize: (width: number, height: number) => void;
69
+ };
70
+ /** One voice as `addVoice` takes it. */
71
+ export type CompositorVoice = {
72
+ voiceId: string;
73
+ cell: HTMLElement;
74
+ controller: VoiceController;
75
+ };
76
+ /** An external owner of the cell DOM (`cellSource`). */
77
+ export type CellSource = {
78
+ acquireCell: (index: number, total: number) => HTMLElement;
79
+ releaseCell?: (index: number) => void;
80
+ };
81
+ /** A load meter's watch on the realm's frames (`setFrameProbe`). */
82
+ export type FrameProbe = {
83
+ everyMs: number;
84
+ onFrame?: (nowMs: number) => void;
85
+ onDraw: (workMs: number, calls: number, triangles: number) => void;
86
+ };
87
+ /** The `createViewportCompositor` options (all optional; see the function). */
88
+ export type ViewportCompositorOptions = {
89
+ cellSource?: CellSource | null;
90
+ presentation?: 'fixed-overlay' | 'per-cell';
91
+ rendererProfile?: Partial<RendererProfile> | null;
92
+ performanceMode?: boolean;
93
+ devHandleKey?: string | null;
94
+ };
95
+ /** The compositor `createViewportCompositor` returns. */
96
+ export type ViewportCompositor = {
97
+ renderer: THREE.WebGLRenderer;
98
+ canvas: HTMLCanvasElement;
99
+ createCell: (index: number, total: number) => HTMLElement;
100
+ addVoice: (voice: CompositorVoice) => void;
101
+ removeVoice: (voiceId: string) => void;
102
+ renderOnce: () => void;
103
+ setPerformanceMode: (enabled: boolean) => boolean;
104
+ /** Watch the realm's frames for a load meter; `null` stops it. */
105
+ setFrameProbe: (probe: FrameProbe | null) => void;
106
+ dispose: () => void;
107
+ };
108
+ /**
109
+ * Options, all optional:
110
+ * - `cellSource`: external owner of the cell DOM. When present, `createCell` delegates to it and the
111
+ * compositor builds no grid of its own; when absent, the default `⌈√n⌉` grid is used.
112
+ * - `presentation` (default `'fixed-overlay'`): how pixels reach the screen — the displayed fixed
113
+ * canvas (full-screen stage layouts) or a per-cell blit canvas the DOM positions (cells inside
114
+ * scrolling content). See the module doc.
115
+ * - `rendererProfile`: renderer construction + pixel-ratio cap. Omit for the historical defaults.
116
+ * - `performanceMode` (default false): start with the realm's renderer savings on — pixel ratio
117
+ * clamped to 1, shadow map off, this loop capped at 30fps. Off by default, and off means the
118
+ * renderer and the cadence are exactly what they were before this option existed. Toggle it live
119
+ * with `setPerformanceMode`.
120
+ * - `devHandleKey` (default null): when set, publish the returned api on `window[devHandleKey]` so
121
+ * tooling can force a composited frame while rAF is throttled (a hidden tab during automated
122
+ * checks). Off by default — a library does not name globals for its host.
123
+ */
124
+ export declare function createViewportCompositor({ cellSource, presentation, rendererProfile, devHandleKey, performanceMode, }?: ViewportCompositorOptions): ViewportCompositor;