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.
- package/README.md +3 -2
- package/dist/config/env.d.ts +8 -0
- package/dist/config/env.js +36 -0
- package/dist/config/index.d.ts +3 -0
- package/dist/config/index.js +4 -0
- package/dist/config/textureConfig.d.ts +140 -0
- package/dist/config/textureConfig.js +318 -0
- package/dist/core/index.d.ts +2 -0
- package/dist/core/index.js +3 -0
- package/dist/core/lightFresnel.d.ts +6 -0
- package/dist/core/lightFresnel.js +63 -0
- package/dist/core/lights.d.ts +48 -0
- package/dist/core/lights.js +79 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +7 -0
- package/dist/loaders/bodyTextureLoader.d.ts +10 -0
- package/dist/loaders/bodyTextureLoader.js +28 -0
- package/dist/loaders/cubeMapLoader.d.ts +58 -0
- package/dist/loaders/cubeMapLoader.js +291 -0
- package/dist/loaders/glbLoader.d.ts +24 -0
- package/dist/loaders/glbLoader.js +72 -0
- package/dist/loaders/index.d.ts +5 -0
- package/{src → dist}/loaders/index.js +0 -2
- package/dist/{types/loaders → loaders}/ktx2.d.ts +4 -6
- package/dist/loaders/ktx2.js +48 -0
- package/dist/moons/index.d.ts +3 -0
- package/dist/moons/index.js +3 -0
- package/dist/moons/moonsGeneration.d.ts +64 -0
- package/dist/moons/moonsGeneration.js +238 -0
- package/dist/moons/moonsManager.d.ts +43 -0
- package/dist/moons/moonsManager.js +93 -0
- package/dist/{types/render → render}/frameBudget.d.ts +18 -17
- package/dist/render/frameBudget.js +42 -0
- package/dist/render/index.d.ts +4 -0
- package/dist/render/viewportCompositor.d.ts +124 -0
- package/dist/render/viewportCompositor.js +643 -0
- package/dist/studio/headerBroadcast.d.ts +10 -1
- package/dist/studio/hostTransport.d.ts +2 -2
- package/dist/studio/index.d.ts +1 -1
- package/dist/utils/dispose.d.ts +5 -0
- package/dist/utils/dispose.js +50 -0
- package/dist/utils/index.d.ts +3 -0
- package/{src → dist}/utils/index.js +1 -1
- package/dist/utils/materials.d.ts +10 -0
- package/dist/utils/materials.js +36 -0
- package/dist/world/cloudShell.d.ts +16 -0
- package/dist/world/cloudShell.js +73 -0
- package/dist/world/index.d.ts +6 -0
- package/{src → dist}/world/index.js +0 -2
- package/dist/world/worldGLB.d.ts +17 -0
- package/dist/world/worldGLB.js +22 -0
- package/dist/world/worldManager.d.ts +57 -0
- package/dist/world/worldManager.js +125 -0
- package/dist/world/worldNormal.d.ts +30 -0
- package/dist/world/worldNormal.js +58 -0
- package/dist/world/worldTexture.d.ts +56 -0
- package/dist/world/worldTexture.js +299 -0
- package/package.json +29 -39
- package/src/config/env.ts +48 -0
- package/src/config/{index.js → index.ts} +7 -0
- package/src/config/{textureConfig.js → textureConfig.ts} +126 -60
- package/src/core/{lightFresnel.js → lightFresnel.ts} +1 -1
- package/src/core/{lights.js → lights.ts} +21 -21
- package/src/loaders/{bodyTextureLoader.js → bodyTextureLoader.ts} +10 -7
- package/src/loaders/{cubeMapLoader.js → cubeMapLoader.ts} +83 -49
- package/src/loaders/{glbLoader.js → glbLoader.ts} +41 -18
- package/src/loaders/index.ts +6 -0
- package/src/loaders/{ktx2.js → ktx2.ts} +5 -7
- package/src/moons/{index.js → index.ts} +1 -0
- package/src/moons/{moonsGeneration.js → moonsGeneration.ts} +45 -32
- package/src/moons/{moonsManager.js → moonsManager.ts} +37 -11
- package/src/render/{frameBudget.js → frameBudget.ts} +15 -8
- package/src/render/index.ts +14 -0
- package/src/render/{viewportCompositor.js → viewportCompositor.ts} +121 -59
- package/src/studio/headerBroadcast.ts +14 -1
- package/src/studio/hostTransport.ts +2 -1
- package/src/studio/index.ts +1 -1
- package/src/utils/dispose.ts +47 -0
- package/src/utils/index.ts +4 -0
- package/src/utils/materials.ts +44 -0
- package/src/world/{cloudShell.js → cloudShell.ts} +19 -12
- package/src/world/index.ts +7 -0
- package/src/world/worldGLB.ts +41 -0
- package/src/world/{worldManager.js → worldManager.ts} +67 -32
- package/src/world/{worldNormal.js → worldNormal.ts} +25 -20
- package/src/world/{worldTexture.js → worldTexture.ts} +70 -51
- package/dist/types/config/env.d.ts +0 -12
- package/dist/types/config/index.d.ts +0 -2
- package/dist/types/config/textureConfig.d.ts +0 -121
- package/dist/types/core/index.d.ts +0 -2
- package/dist/types/core/lightFresnel.d.ts +0 -5
- package/dist/types/core/lights.d.ts +0 -58
- package/dist/types/index.d.ts +0 -6
- package/dist/types/loaders/bodyTextureLoader.d.ts +0 -12
- package/dist/types/loaders/cubeMapLoader.d.ts +0 -70
- package/dist/types/loaders/glbLoader.d.ts +0 -24
- package/dist/types/loaders/index.d.ts +0 -5
- package/dist/types/moons/index.d.ts +0 -2
- package/dist/types/moons/moonsGeneration.d.ts +0 -72
- package/dist/types/moons/moonsManager.d.ts +0 -27
- package/dist/types/render/index.d.ts +0 -2
- package/dist/types/render/viewportCompositor.d.ts +0 -58
- package/dist/types/utils/dispose.d.ts +0 -2
- package/dist/types/utils/index.d.ts +0 -2
- package/dist/types/utils/materials.d.ts +0 -2
- package/dist/types/world/cloudShell.d.ts +0 -25
- package/dist/types/world/index.d.ts +0 -6
- package/dist/types/world/worldGLB.d.ts +0 -20
- package/dist/types/world/worldManager.d.ts +0 -63
- package/dist/types/world/worldNormal.d.ts +0 -43
- package/dist/types/world/worldTexture.d.ts +0 -80
- package/src/config/env.js +0 -38
- package/src/utils/dispose.js +0 -39
- package/src/utils/materials.js +0 -31
- package/src/world/worldGLB.js +0 -19
- /package/{src → dist}/render/index.js +0 -0
- /package/src/core/{index.js → index.ts} +0 -0
- /package/src/{index.js → index.ts} +0 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @file src/render/frameBudget.
|
|
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(
|
|
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.
|
|
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
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
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
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
614
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
package/src/studio/index.ts
CHANGED
|
@@ -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,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
|
-
*
|
|
58
|
-
*
|
|
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
|
+
}
|