@threenative/core 0.2.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +55 -0
- package/capabilities.json +5292 -0
- package/dist/assets-kyoF7JlJ.d.ts +103 -0
- package/dist/audio-BFiGneTL.d.ts +156 -0
- package/dist/canvas-layer-BLVijiUJ.d.ts +62 -0
- package/dist/game-XGrTzapq.d.ts +1166 -0
- package/dist/gpu-readback-D2iRvoe9.d.ts +112 -0
- package/dist/hot.d.ts +20 -2
- package/dist/hot.js +14 -2
- package/dist/index.d.ts +2108 -55
- package/dist/index.js +15645 -2222
- package/dist/net.d.ts +65 -0
- package/dist/net.js +643 -0
- package/dist/playtest.d.ts +37 -4
- package/dist/playtest.js +246 -546
- package/dist/react.d.ts +177 -0
- package/dist/react.js +635 -0
- package/dist/renderer-C6hqZpoG.d.ts +770 -0
- package/dist/ui-layer.d.ts +306 -0
- package/dist/ui-layer.js +425 -0
- package/dist/world.d.ts +254 -0
- package/dist/world.js +2686 -0
- package/gpl/LICENSE.GPL +117 -0
- package/gpl/convert.py +192 -0
- package/gpl/recipes/_common.py +169 -0
- package/gpl/recipes/bake_ao.py +111 -0
- package/gpl/recipes/decimate.py +64 -0
- package/gpl/recipes/retarget.py +131 -0
- package/gpl/recipes/unwrap.py +71 -0
- package/mcp/assets.mjs +5 -0
- package/mcp/blender-server.mjs +632 -0
- package/mcp/blender.mjs +27 -0
- package/mcp/engine-server.mjs +501 -0
- package/mcp/engine.mjs +31 -0
- package/mcp/install.d.mts +37 -0
- package/mcp/install.mjs +145 -0
- package/mcp/launch.mjs +72 -0
- package/mcp/sculpt.mjs +5 -0
- package/mcp/servers.d.mts +34 -0
- package/mcp/servers.mjs +160 -0
- package/package.json +76 -6
- package/patches/three@0.185.1.patch +522 -0
- package/scripts/apply-three-patch.mjs +297 -0
- package/scripts/ensure-mcp.mjs +43 -0
- package/scripts/postinstall.mjs +6 -0
- package/dist/audio-CEAw0w5y.d.ts +0 -35
- package/dist/game-DRt1Qhq3.d.ts +0 -429
package/dist/ui-layer.js
ADDED
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
// src/ui-bridge.ts
|
|
2
|
+
var HIT_REGIONS_MESSAGE = "tn:hit-regions";
|
|
3
|
+
var GAME_STATE_MESSAGE = "tn:state";
|
|
4
|
+
var UI_INTENT_MESSAGE = "tn:intent";
|
|
5
|
+
var UI_READY_INTENT = "tn:ready";
|
|
6
|
+
var UI_BRIDGE_GLOBALS = {
|
|
7
|
+
/** UI end, inbound: the host calls this with one JSON string. */
|
|
8
|
+
uiReceive: "__tnUiReceive",
|
|
9
|
+
/** UI end, outbound: an injected object with `postMessage(string)`. */
|
|
10
|
+
uiHost: "tnHost",
|
|
11
|
+
/** Game end, inbound: the runtime calls this with one JSON string. */
|
|
12
|
+
gameReceive: "__tnUiGameReceive",
|
|
13
|
+
/** Game end, outbound: the runtime installs this and forwards to the web view. */
|
|
14
|
+
gamePost: "__tnUiPost"
|
|
15
|
+
};
|
|
16
|
+
function assertSendable(message) {
|
|
17
|
+
if (typeof message !== "object" || message === null || Array.isArray(message)) {
|
|
18
|
+
throw new Error("TN_UI_BRIDGE_MESSAGE_INVALID: a bridge message must be a JSON object.");
|
|
19
|
+
}
|
|
20
|
+
if (typeof message.type !== "string" || message.type.length === 0) {
|
|
21
|
+
throw new Error("TN_UI_BRIDGE_MESSAGE_INVALID: a bridge message must carry a string 'type'.");
|
|
22
|
+
}
|
|
23
|
+
let frame;
|
|
24
|
+
try {
|
|
25
|
+
frame = JSON.stringify(message);
|
|
26
|
+
} catch (error) {
|
|
27
|
+
throw new Error(
|
|
28
|
+
`TN_UI_BRIDGE_MESSAGE_INVALID: '${message.type}' is not JSON-serialisable: ${error instanceof Error ? error.message : String(error)}`
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
if (frame === void 0) {
|
|
32
|
+
throw new Error(`TN_UI_BRIDGE_MESSAGE_INVALID: '${message.type}' serialised to nothing.`);
|
|
33
|
+
}
|
|
34
|
+
return frame;
|
|
35
|
+
}
|
|
36
|
+
function parseFrame(frame) {
|
|
37
|
+
const text = typeof frame === "string" ? frame : frame?.data;
|
|
38
|
+
if (typeof text !== "string") {
|
|
39
|
+
throw new Error("TN_UI_BRIDGE_FRAME_INVALID: expected a JSON string from the host.");
|
|
40
|
+
}
|
|
41
|
+
let parsed;
|
|
42
|
+
try {
|
|
43
|
+
parsed = JSON.parse(text);
|
|
44
|
+
} catch (error) {
|
|
45
|
+
throw new Error(
|
|
46
|
+
`TN_UI_BRIDGE_FRAME_INVALID: not JSON: ${error instanceof Error ? error.message : String(error)}`
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
50
|
+
throw new Error("TN_UI_BRIDGE_FRAME_INVALID: a bridge frame must be a JSON object.");
|
|
51
|
+
}
|
|
52
|
+
if (typeof parsed.type !== "string") {
|
|
53
|
+
throw new Error("TN_UI_BRIDGE_FRAME_INVALID: a bridge frame must carry a string 'type'.");
|
|
54
|
+
}
|
|
55
|
+
return parsed;
|
|
56
|
+
}
|
|
57
|
+
function findOutbound(scope, end) {
|
|
58
|
+
if (end === "game") {
|
|
59
|
+
const post = scope[UI_BRIDGE_GLOBALS.gamePost];
|
|
60
|
+
return typeof post === "function" ? (frame) => post(frame) : void 0;
|
|
61
|
+
}
|
|
62
|
+
const injected = scope[UI_BRIDGE_GLOBALS.uiHost];
|
|
63
|
+
if (typeof injected?.postMessage === "function") {
|
|
64
|
+
return (frame) => injected.postMessage(frame);
|
|
65
|
+
}
|
|
66
|
+
const webkit = scope.webkit;
|
|
67
|
+
const handler = webkit?.messageHandlers?.[UI_BRIDGE_GLOBALS.uiHost];
|
|
68
|
+
if (typeof handler?.postMessage === "function") {
|
|
69
|
+
return (frame) => handler.postMessage(frame);
|
|
70
|
+
}
|
|
71
|
+
const ipc = scope.ipc;
|
|
72
|
+
if (typeof ipc?.postMessage === "function") {
|
|
73
|
+
return (frame) => ipc.postMessage(frame);
|
|
74
|
+
}
|
|
75
|
+
return void 0;
|
|
76
|
+
}
|
|
77
|
+
var BROKER_KEY = "__tnUiBridgeBroker";
|
|
78
|
+
function broker(scope) {
|
|
79
|
+
const existing = scope[BROKER_KEY];
|
|
80
|
+
if (existing !== void 0) return existing;
|
|
81
|
+
const created = { game: /* @__PURE__ */ new Set(), ui: /* @__PURE__ */ new Set() };
|
|
82
|
+
scope[BROKER_KEY] = created;
|
|
83
|
+
return created;
|
|
84
|
+
}
|
|
85
|
+
function connectUiBridge(options) {
|
|
86
|
+
const { end } = options;
|
|
87
|
+
if (end !== "game" && end !== "ui") {
|
|
88
|
+
throw new Error(`TN_UI_BRIDGE_END_INVALID: expected 'game' or 'ui', got '${String(end)}'.`);
|
|
89
|
+
}
|
|
90
|
+
const scope = options.scope ?? globalThis;
|
|
91
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
92
|
+
const deliver = (frame) => {
|
|
93
|
+
const message = parseFrame(frame);
|
|
94
|
+
for (const listener of [...listeners]) listener(message);
|
|
95
|
+
};
|
|
96
|
+
const outbound = findOutbound(scope, end);
|
|
97
|
+
const inboundKey = end === "ui" ? UI_BRIDGE_GLOBALS.uiReceive : UI_BRIDGE_GLOBALS.gameReceive;
|
|
98
|
+
let closed = false;
|
|
99
|
+
if (outbound === void 0) {
|
|
100
|
+
const bus = broker(scope);
|
|
101
|
+
const peer = end === "ui" ? "game" : "ui";
|
|
102
|
+
const own = (message) => {
|
|
103
|
+
for (const listener of [...listeners]) listener(message);
|
|
104
|
+
};
|
|
105
|
+
bus[end].add(own);
|
|
106
|
+
return {
|
|
107
|
+
end,
|
|
108
|
+
transport: "in-process",
|
|
109
|
+
hasPeer: () => bus[peer].size > 0,
|
|
110
|
+
post(message) {
|
|
111
|
+
const frame = assertSendable(message);
|
|
112
|
+
if (closed) throw new Error("TN_UI_BRIDGE_CLOSED: post on a closed bridge.");
|
|
113
|
+
const copy = JSON.parse(frame);
|
|
114
|
+
for (const listener of [...bus[peer]]) listener(copy);
|
|
115
|
+
},
|
|
116
|
+
onMessage(listener) {
|
|
117
|
+
listeners.add(listener);
|
|
118
|
+
return () => listeners.delete(listener);
|
|
119
|
+
},
|
|
120
|
+
close() {
|
|
121
|
+
closed = true;
|
|
122
|
+
listeners.clear();
|
|
123
|
+
bus[end].delete(own);
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
scope[inboundKey] = deliver;
|
|
128
|
+
const injected = scope[UI_BRIDGE_GLOBALS.uiHost];
|
|
129
|
+
if (end === "ui" && injected !== void 0 && typeof injected === "object") {
|
|
130
|
+
injected.onmessage = (event) => deliver(event);
|
|
131
|
+
}
|
|
132
|
+
return {
|
|
133
|
+
end,
|
|
134
|
+
transport: "host",
|
|
135
|
+
hasPeer() {
|
|
136
|
+
const attached = scope.__tnUiOverlayAttached;
|
|
137
|
+
if (end === "ui") return true;
|
|
138
|
+
return typeof attached === "function" ? attached() === true : false;
|
|
139
|
+
},
|
|
140
|
+
post(message) {
|
|
141
|
+
const frame = assertSendable(message);
|
|
142
|
+
if (closed) throw new Error("TN_UI_BRIDGE_CLOSED: post on a closed bridge.");
|
|
143
|
+
outbound(frame);
|
|
144
|
+
},
|
|
145
|
+
onMessage(listener) {
|
|
146
|
+
listeners.add(listener);
|
|
147
|
+
return () => listeners.delete(listener);
|
|
148
|
+
},
|
|
149
|
+
close() {
|
|
150
|
+
closed = true;
|
|
151
|
+
listeners.clear();
|
|
152
|
+
if (scope[inboundKey] === deliver) scope[inboundKey] = void 0;
|
|
153
|
+
if (end === "ui" && injected !== void 0 && typeof injected === "object") {
|
|
154
|
+
injected.onmessage = void 0;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// src/ui-state.ts
|
|
161
|
+
var microtask = (flush) => {
|
|
162
|
+
void Promise.resolve().then(flush);
|
|
163
|
+
};
|
|
164
|
+
function publishUiState(bridge, store, options = {}) {
|
|
165
|
+
if (bridge.end !== "game") {
|
|
166
|
+
throw new Error(
|
|
167
|
+
`TN_UI_STATE_WRONG_END: state is published by the 'game' end, not '${bridge.end}'.`
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
const schedule = options.schedule ?? microtask;
|
|
171
|
+
const read = () => store.getPublishedState === void 0 ? store.getState() : store.getPublishedState();
|
|
172
|
+
let stopped = false;
|
|
173
|
+
let queued = false;
|
|
174
|
+
let lastFrame = "";
|
|
175
|
+
const publish = () => {
|
|
176
|
+
if (stopped) return;
|
|
177
|
+
const state = read();
|
|
178
|
+
lastFrame = JSON.stringify(state);
|
|
179
|
+
bridge.post({ type: GAME_STATE_MESSAGE, state });
|
|
180
|
+
};
|
|
181
|
+
const flush = () => {
|
|
182
|
+
queued = false;
|
|
183
|
+
if (stopped) return;
|
|
184
|
+
if (!bridge.hasPeer()) return;
|
|
185
|
+
if (JSON.stringify(read()) === lastFrame) return;
|
|
186
|
+
publish();
|
|
187
|
+
};
|
|
188
|
+
const unsubscribe = store.subscribe(() => {
|
|
189
|
+
if (stopped || queued) return;
|
|
190
|
+
queued = true;
|
|
191
|
+
schedule(flush);
|
|
192
|
+
});
|
|
193
|
+
if (bridge.hasPeer()) publish();
|
|
194
|
+
return {
|
|
195
|
+
publish,
|
|
196
|
+
stop() {
|
|
197
|
+
stopped = true;
|
|
198
|
+
unsubscribe();
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
function subscribeUiState(bridge) {
|
|
203
|
+
if (bridge.end !== "ui") {
|
|
204
|
+
throw new Error(
|
|
205
|
+
`TN_UI_STATE_WRONG_END: state is mirrored by the 'ui' end, not '${bridge.end}'.`
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
209
|
+
let current;
|
|
210
|
+
const off = bridge.onMessage((message) => {
|
|
211
|
+
if (message.type !== GAME_STATE_MESSAGE) return;
|
|
212
|
+
const state = message.state;
|
|
213
|
+
if (typeof state !== "object" || state === null || Array.isArray(state)) {
|
|
214
|
+
throw new Error("TN_UI_STATE_FRAME_INVALID: a published state frame must carry an object.");
|
|
215
|
+
}
|
|
216
|
+
current = state;
|
|
217
|
+
for (const listener of [...listeners]) listener();
|
|
218
|
+
});
|
|
219
|
+
return {
|
|
220
|
+
get: () => current,
|
|
221
|
+
subscribe(listener) {
|
|
222
|
+
listeners.add(listener);
|
|
223
|
+
return () => listeners.delete(listener);
|
|
224
|
+
},
|
|
225
|
+
stop() {
|
|
226
|
+
off();
|
|
227
|
+
listeners.clear();
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
function sendUiIntent(bridge, intent, payload) {
|
|
232
|
+
if (bridge.end !== "ui") {
|
|
233
|
+
throw new Error(
|
|
234
|
+
`TN_UI_INTENT_WRONG_END: intents are sent by the 'ui' end, not '${bridge.end}'.`
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
if (typeof intent !== "string" || intent.length === 0) {
|
|
238
|
+
throw new Error("TN_UI_INTENT_INVALID: an intent needs a non-empty name.");
|
|
239
|
+
}
|
|
240
|
+
bridge.post({
|
|
241
|
+
type: UI_INTENT_MESSAGE,
|
|
242
|
+
intent,
|
|
243
|
+
...payload === void 0 ? {} : { payload }
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
function onUiIntent(bridge, listener) {
|
|
247
|
+
if (bridge.end !== "game") {
|
|
248
|
+
throw new Error(
|
|
249
|
+
`TN_UI_INTENT_WRONG_END: intents are received by the 'game' end, not '${bridge.end}'.`
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
return bridge.onMessage((message) => {
|
|
253
|
+
if (message.type !== UI_INTENT_MESSAGE) return;
|
|
254
|
+
if (typeof message.intent !== "string" || message.intent.length === 0) {
|
|
255
|
+
throw new Error("TN_UI_INTENT_INVALID: an intent frame must carry a non-empty name.");
|
|
256
|
+
}
|
|
257
|
+
listener(message.intent, message.payload);
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// src/ui-hit-regions.ts
|
|
262
|
+
var INTERACTIVE_ATTRIBUTE = "data-tn-interactive";
|
|
263
|
+
function quantize(value) {
|
|
264
|
+
return Math.round(value * 1e5) / 1e5;
|
|
265
|
+
}
|
|
266
|
+
var TRANSITION_START = ["transitionrun", "transitionstart", "animationstart"];
|
|
267
|
+
var TRANSITION_END = [
|
|
268
|
+
"transitionend",
|
|
269
|
+
"transitioncancel",
|
|
270
|
+
"animationend",
|
|
271
|
+
"animationcancel"
|
|
272
|
+
];
|
|
273
|
+
var LAYOUT_EVENTS = ["resize", "orientationchange", "scroll"];
|
|
274
|
+
function publishHitRegions(options) {
|
|
275
|
+
const scope = options.scope ?? globalThis;
|
|
276
|
+
const document = scope.document;
|
|
277
|
+
if (document === void 0 || typeof document.querySelectorAll !== "function") {
|
|
278
|
+
throw new Error(
|
|
279
|
+
"TN_UI_HIT_REGIONS_NO_DOCUMENT: the interactive-rect registry runs in the UI page and found no document."
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
const selector = `[${options.attribute ?? INTERACTIVE_ATTRIBUTE}]`;
|
|
283
|
+
const bridge = options.bridge;
|
|
284
|
+
if (bridge.end !== "ui") {
|
|
285
|
+
throw new Error(
|
|
286
|
+
`TN_UI_HIT_REGIONS_WRONG_END: hit regions are published by the 'ui' end, not '${bridge.end}'.`
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
const unmarkedWarning = { warned: false };
|
|
290
|
+
let published = [];
|
|
291
|
+
let publishedFrame = "";
|
|
292
|
+
let stopped = false;
|
|
293
|
+
let liveTransitions = 0;
|
|
294
|
+
let frameHandle;
|
|
295
|
+
const viewport = () => ({
|
|
296
|
+
width: scope.innerWidth ?? document.documentElement?.clientWidth ?? 0,
|
|
297
|
+
height: scope.innerHeight ?? document.documentElement?.clientHeight ?? 0
|
|
298
|
+
});
|
|
299
|
+
const boxesOf = (element) => {
|
|
300
|
+
const list = element.getClientRects?.();
|
|
301
|
+
if (list === void 0 || list.length === 0) return [element.getBoundingClientRect()];
|
|
302
|
+
const boxes = [];
|
|
303
|
+
for (let index = 0; index < list.length; index += 1) {
|
|
304
|
+
const rect = list[index];
|
|
305
|
+
if (rect !== void 0) boxes.push(rect);
|
|
306
|
+
}
|
|
307
|
+
return boxes;
|
|
308
|
+
};
|
|
309
|
+
const normalize = (boxes, width, height) => boxes.filter((rect) => rect.width > 0 && rect.height > 0).map((rect) => ({
|
|
310
|
+
x: quantize(rect.left / width),
|
|
311
|
+
y: quantize(rect.top / height),
|
|
312
|
+
width: quantize(rect.width / width),
|
|
313
|
+
height: quantize(rect.height / height)
|
|
314
|
+
}));
|
|
315
|
+
const measure = () => {
|
|
316
|
+
const { width, height } = viewport();
|
|
317
|
+
if (width <= 0 || height <= 0) return [];
|
|
318
|
+
const regions = [];
|
|
319
|
+
const elements = document.querySelectorAll(selector);
|
|
320
|
+
for (let index = 0; index < elements.length; index += 1) {
|
|
321
|
+
const element = elements[index];
|
|
322
|
+
if (element !== void 0) regions.push(...normalize(boxesOf(element), width, height));
|
|
323
|
+
}
|
|
324
|
+
warnAboutUnmarkedControls(document, selector, scope, unmarkedWarning);
|
|
325
|
+
return regions;
|
|
326
|
+
};
|
|
327
|
+
const publish = (regions) => {
|
|
328
|
+
const frame = JSON.stringify(regions);
|
|
329
|
+
if (frame === publishedFrame) return;
|
|
330
|
+
publishedFrame = frame;
|
|
331
|
+
published = regions;
|
|
332
|
+
bridge.post({ type: HIT_REGIONS_MESSAGE, regions });
|
|
333
|
+
};
|
|
334
|
+
const refresh = () => {
|
|
335
|
+
if (stopped) return;
|
|
336
|
+
publish(measure());
|
|
337
|
+
};
|
|
338
|
+
const requestFrame = scope.requestAnimationFrame;
|
|
339
|
+
const cancelFrame = scope.cancelAnimationFrame;
|
|
340
|
+
const pump = () => {
|
|
341
|
+
frameHandle = void 0;
|
|
342
|
+
if (stopped || liveTransitions <= 0) return;
|
|
343
|
+
refresh();
|
|
344
|
+
if (requestFrame !== void 0) frameHandle = requestFrame(pump);
|
|
345
|
+
};
|
|
346
|
+
const onTransitionStart = () => {
|
|
347
|
+
liveTransitions += 1;
|
|
348
|
+
if (frameHandle === void 0 && requestFrame !== void 0) frameHandle = requestFrame(pump);
|
|
349
|
+
};
|
|
350
|
+
const onTransitionEnd = () => {
|
|
351
|
+
liveTransitions = Math.max(0, liveTransitions - 1);
|
|
352
|
+
refresh();
|
|
353
|
+
};
|
|
354
|
+
const listen = (on, host, types, listener) => {
|
|
355
|
+
const method = host[on];
|
|
356
|
+
if (typeof method !== "function") return;
|
|
357
|
+
for (const type of types) method.call(host, type, listener, true);
|
|
358
|
+
};
|
|
359
|
+
listen("addEventListener", document, TRANSITION_START, onTransitionStart);
|
|
360
|
+
listen("addEventListener", document, TRANSITION_END, onTransitionEnd);
|
|
361
|
+
listen("addEventListener", scope, LAYOUT_EVENTS, refresh);
|
|
362
|
+
const MutationObserverCtor = scope.MutationObserver;
|
|
363
|
+
const mutations = MutationObserverCtor === void 0 ? void 0 : new MutationObserverCtor(refresh);
|
|
364
|
+
mutations?.observe(document, {
|
|
365
|
+
attributes: true,
|
|
366
|
+
childList: true,
|
|
367
|
+
subtree: true,
|
|
368
|
+
characterData: true
|
|
369
|
+
});
|
|
370
|
+
const ResizeObserverCtor = scope.ResizeObserver;
|
|
371
|
+
const resizes = ResizeObserverCtor === void 0 ? void 0 : new ResizeObserverCtor(refresh);
|
|
372
|
+
if (resizes !== void 0 && document.documentElement !== void 0) {
|
|
373
|
+
resizes.observe(document.documentElement);
|
|
374
|
+
}
|
|
375
|
+
refresh();
|
|
376
|
+
return {
|
|
377
|
+
refresh,
|
|
378
|
+
regions: () => published,
|
|
379
|
+
stop() {
|
|
380
|
+
if (stopped) return;
|
|
381
|
+
stopped = true;
|
|
382
|
+
if (frameHandle !== void 0 && cancelFrame !== void 0) cancelFrame(frameHandle);
|
|
383
|
+
frameHandle = void 0;
|
|
384
|
+
listen("removeEventListener", document, TRANSITION_START, onTransitionStart);
|
|
385
|
+
listen("removeEventListener", document, TRANSITION_END, onTransitionEnd);
|
|
386
|
+
listen("removeEventListener", scope, LAYOUT_EVENTS, refresh);
|
|
387
|
+
mutations?.disconnect();
|
|
388
|
+
resizes?.disconnect();
|
|
389
|
+
published = [];
|
|
390
|
+
publishedFrame = "[]";
|
|
391
|
+
bridge.post({ type: HIT_REGIONS_MESSAGE, regions: [] });
|
|
392
|
+
}
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
var UNMARKED_CONTROL_SELECTOR = 'button, [role="button"], a[href], input, select, textarea';
|
|
396
|
+
function warnAboutUnmarkedControls(document, selector, scope, once) {
|
|
397
|
+
if (once.warned || !isDevelopment()) return;
|
|
398
|
+
if (typeof document.querySelectorAll !== "function") return;
|
|
399
|
+
const candidates = document.querySelectorAll(UNMARKED_CONTROL_SELECTOR);
|
|
400
|
+
const unmarked = [];
|
|
401
|
+
for (let index = 0; index < candidates.length; index += 1) {
|
|
402
|
+
const element = candidates[index];
|
|
403
|
+
if (element === void 0) continue;
|
|
404
|
+
if (element.closest?.(selector) != null) continue;
|
|
405
|
+
unmarked.push(describe(element));
|
|
406
|
+
}
|
|
407
|
+
if (unmarked.length === 0) return;
|
|
408
|
+
once.warned = true;
|
|
409
|
+
const names = unmarked.slice(0, 5).join(", ");
|
|
410
|
+
const rest = unmarked.length > 5 ? ` and ${unmarked.length - 5} more` : "";
|
|
411
|
+
scope.console?.warn?.(
|
|
412
|
+
`TN_UI_UNMARKED_CONTROLS: ${unmarked.length} control(s) publish no hit region, so on native the press reaches the game instead: ${names}${rest}. Mark each with ${INTERACTIVE_ATTRIBUTE}, or wrap them in an element that carries it.`
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
function describe(element) {
|
|
416
|
+
const tag = (element.tagName ?? "element").toLowerCase();
|
|
417
|
+
const id = element.id === void 0 || element.id === "" ? "" : `#${element.id}`;
|
|
418
|
+
const first = (element.className ?? "").split(/\s+/u).filter(Boolean)[0];
|
|
419
|
+
return `<${tag}${id}${first === void 0 ? "" : `.${first}`}>`;
|
|
420
|
+
}
|
|
421
|
+
function isDevelopment() {
|
|
422
|
+
return import.meta.env?.DEV === true;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
export { GAME_STATE_MESSAGE, HIT_REGIONS_MESSAGE, INTERACTIVE_ATTRIBUTE, UI_BRIDGE_GLOBALS, UI_INTENT_MESSAGE, UI_READY_INTENT, connectUiBridge, onUiIntent, publishHitRegions, publishUiState, sendUiIntent, subscribeUiState };
|
package/dist/world.d.ts
ADDED
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import { Object3D, Mesh, LOD, Vector3, Group, BufferGeometry } from 'three';
|
|
2
|
+
import { I as IComputeDriven, a as IGPUReadbackSample } from './gpu-readback-D2iRvoe9.js';
|
|
3
|
+
import { I as IRendererLike } from './renderer-C6hqZpoG.js';
|
|
4
|
+
import { I as IAssetLoader } from './assets-kyoF7JlJ.js';
|
|
5
|
+
import 'three/webgpu';
|
|
6
|
+
|
|
7
|
+
interface IWorldErosionOptions {
|
|
8
|
+
readonly depositionRate: number;
|
|
9
|
+
readonly erosionRate: number;
|
|
10
|
+
readonly evaporation: number;
|
|
11
|
+
readonly iterations: number;
|
|
12
|
+
readonly rainfall: number;
|
|
13
|
+
readonly sedimentCapacity: number;
|
|
14
|
+
readonly timeStep: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
type MeshSurface = NonNullable<ConstructorParameters<typeof Mesh>[1]>;
|
|
18
|
+
interface IWorldTileCollider {
|
|
19
|
+
dispose(): void;
|
|
20
|
+
}
|
|
21
|
+
interface IWorldTileColliderInput {
|
|
22
|
+
readonly field: Heightfield;
|
|
23
|
+
readonly key: string;
|
|
24
|
+
readonly object: Object3D;
|
|
25
|
+
readonly tileX: number;
|
|
26
|
+
readonly tileZ: number;
|
|
27
|
+
}
|
|
28
|
+
interface IWorldTile {
|
|
29
|
+
readonly bytes: number;
|
|
30
|
+
readonly collider: IWorldTileCollider;
|
|
31
|
+
readonly field: Heightfield;
|
|
32
|
+
readonly key: string;
|
|
33
|
+
readonly lod: LOD;
|
|
34
|
+
readonly lodLevel: number;
|
|
35
|
+
readonly object: Object3D;
|
|
36
|
+
readonly skirtVertexCount: number;
|
|
37
|
+
readonly tileX: number;
|
|
38
|
+
readonly tileZ: number;
|
|
39
|
+
}
|
|
40
|
+
interface IWorldTilesFollowPosition {
|
|
41
|
+
readonly x: number;
|
|
42
|
+
readonly z: number;
|
|
43
|
+
}
|
|
44
|
+
interface IWorldTilesTopologyObservation {
|
|
45
|
+
readonly columns: number;
|
|
46
|
+
readonly depth: number;
|
|
47
|
+
readonly origin: IHeightfieldOrigin;
|
|
48
|
+
readonly rows: number;
|
|
49
|
+
readonly width: number;
|
|
50
|
+
}
|
|
51
|
+
interface IWorldTilesOptions {
|
|
52
|
+
/** Releases a caller-supplied model key when its last tile reference is evicted. */
|
|
53
|
+
readonly assets?: Pick<IAssetLoader, "release">;
|
|
54
|
+
/** A game-owned, already-loaded logical model key, or a key resolver per tile. */
|
|
55
|
+
readonly assetKey?: string | ((tileX: number, tileZ: number) => string);
|
|
56
|
+
/** Creates the physics body from the field's explicit collider-order copy. */
|
|
57
|
+
readonly createCollider?: (input: IWorldTileColliderInput) => IWorldTileCollider;
|
|
58
|
+
/** TSL pass options are game supplied and are forwarded to each resident field. */
|
|
59
|
+
readonly worldPasses?: IHeightfieldWorldPassOptions;
|
|
60
|
+
/** Game-owned surface; this class never creates or mutates it. */
|
|
61
|
+
readonly surface: MeshSurface;
|
|
62
|
+
readonly residentByteBudget: number;
|
|
63
|
+
readonly residentTileBudget: number;
|
|
64
|
+
readonly sampleHeight: IHeightfieldSamplerOptions["sampleHeight"];
|
|
65
|
+
/** How deep each edge skirt extends below its surface. Defaults to one tile width. */
|
|
66
|
+
readonly skirtDepth?: number;
|
|
67
|
+
/** Square tile neighborhood to consider around the followed point. Defaults to 1. */
|
|
68
|
+
readonly streamRadius?: number;
|
|
69
|
+
readonly tileResolution: number;
|
|
70
|
+
readonly tileSize: number;
|
|
71
|
+
/** Vertex decimation factors. Defaults to 1, 2, and 4 over the same field. */
|
|
72
|
+
readonly lodFactors?: readonly number[];
|
|
73
|
+
/** Distances in world units at which the next LOD becomes active. */
|
|
74
|
+
readonly lodDistances?: readonly number[];
|
|
75
|
+
/** Explicit game-owned measurement region used by the topology evaluator. */
|
|
76
|
+
readonly topologyObservation?: IWorldTilesTopologyObservation;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Stream a bounded square of game-authored heightfields and keep their render and physics units
|
|
80
|
+
* together. The class composes ordinary THREE.LOD objects and leaves frustum/projection culling
|
|
81
|
+
* to the renderer's existing scene path.
|
|
82
|
+
*
|
|
83
|
+
* @situation stream terrain without cracks
|
|
84
|
+
* @situation keep generated terrain resident around a moving player
|
|
85
|
+
* @situation put a generated terrain tile into a game-owned physics world
|
|
86
|
+
* @alias stream terrain across chunks
|
|
87
|
+
* @constraint sampleHeight and surface are required game choices; no landform or surface preset is installed
|
|
88
|
+
* @constraint residentTileBudget and residentByteBudget are hard caps; a tile that cannot fit throws
|
|
89
|
+
* @override tileSize, tileResolution, lodFactors, lodDistances, skirtDepth, streamRadius, and budgets
|
|
90
|
+
* @example const tiles = new TerrainTiles({ sampleHeight, surface: gameSurface(), tileSize: 256, tileResolution: 129, residentTileBudget: 25, residentByteBudget: 32_000_000 });
|
|
91
|
+
*/
|
|
92
|
+
declare class TerrainTiles extends Object3D implements IComputeDriven {
|
|
93
|
+
#private;
|
|
94
|
+
readonly residentTileBudget: number;
|
|
95
|
+
readonly residentByteBudget: number;
|
|
96
|
+
readonly skirtDepth: number;
|
|
97
|
+
readonly tileResolution: number;
|
|
98
|
+
readonly tileSize: number;
|
|
99
|
+
readonly processCadence: "render";
|
|
100
|
+
constructor(options: IWorldTilesOptions);
|
|
101
|
+
get released(): boolean;
|
|
102
|
+
get residentTileCount(): number;
|
|
103
|
+
get residentBytes(): number;
|
|
104
|
+
get peakResidentTileCount(): number;
|
|
105
|
+
get peakResidentBytes(): number;
|
|
106
|
+
get residentKeys(): readonly string[];
|
|
107
|
+
get residentColliderKeys(): readonly string[];
|
|
108
|
+
get lodLevelCount(): number;
|
|
109
|
+
get lodTransitions(): number;
|
|
110
|
+
/** Maximum per-render-frame displacement of the visible LOD surface during transitions. */
|
|
111
|
+
get maxLodPop(): number;
|
|
112
|
+
/** Maximum number of rendered frames during which an LOD transition remained observable. */
|
|
113
|
+
get maxLodTransitionFrames(): number;
|
|
114
|
+
/** Maximum visible edge gap observed across follow/process calls for this residency owner. */
|
|
115
|
+
get maxSeamGap(): number;
|
|
116
|
+
/** Maximum remaining visible gap after skirt or bridge coverage observed across follow/process calls. */
|
|
117
|
+
get maxVisualSeamGap(): number;
|
|
118
|
+
/** Number of mixed-LOD edge reconciliations observed during this residency lifetime. */
|
|
119
|
+
get stitchedEdgeCount(): number;
|
|
120
|
+
get warmupNodes(): readonly unknown[];
|
|
121
|
+
getTile(key: string): IWorldTile | undefined;
|
|
122
|
+
follow(position: IWorldTilesFollowPosition | Pick<Vector3, "x" | "z">): void;
|
|
123
|
+
heightAt(x: number, z: number): number;
|
|
124
|
+
normalAt(x: number, z: number, target?: Vector3): Vector3;
|
|
125
|
+
sample(channel: string, x: number, z: number): number;
|
|
126
|
+
attachRenderer(renderer: IRendererLike): void;
|
|
127
|
+
process(renderer?: IRendererLike | undefined): void;
|
|
128
|
+
debug(): Record<string, unknown>;
|
|
129
|
+
detach(): void;
|
|
130
|
+
dispose(): void;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
type WorldGenerationPath = "gpu" | "cpu-fallback" | "unsupported";
|
|
134
|
+
interface IWorldComputeLimits {
|
|
135
|
+
readonly maxComputeInvocationsPerWorkgroup: number;
|
|
136
|
+
readonly maxComputeWorkgroupsPerDimension: number;
|
|
137
|
+
readonly maxStorageBufferBindingSize: number;
|
|
138
|
+
}
|
|
139
|
+
interface IWorldCapabilities {
|
|
140
|
+
readonly compute: boolean;
|
|
141
|
+
/** Whether the host exposed a GPU adapter, even when its limits cannot run the world pass. */
|
|
142
|
+
readonly gpu: boolean;
|
|
143
|
+
readonly generation: WorldGenerationPath;
|
|
144
|
+
readonly limits: IWorldComputeLimits;
|
|
145
|
+
readonly cpuFallbackIterations: number;
|
|
146
|
+
readonly reason?: string;
|
|
147
|
+
}
|
|
148
|
+
interface IWorldCapabilitiesOptions {
|
|
149
|
+
/** Explicitly supplied by a host that already probed its adapter. */
|
|
150
|
+
readonly gpuAvailable?: boolean;
|
|
151
|
+
readonly limits?: Partial<IWorldComputeLimits>;
|
|
152
|
+
readonly minimumWorkgroupsPerDimension?: number;
|
|
153
|
+
readonly minimumStorageBufferBindingSize?: number;
|
|
154
|
+
/** A positive value makes CPU generation an explicit, reduced fallback. */
|
|
155
|
+
readonly cpuFallbackIterations?: number;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Resolve the active world-generation path from host capability facts.
|
|
159
|
+
*
|
|
160
|
+
* The function accepts the adapter facts instead of reaching through a renderer-specific global,
|
|
161
|
+
* so browser and native hosts can report the same object. Missing limits are not treated as
|
|
162
|
+
* infinite: a host must either provide a valid GPU limit report or explicitly choose CPU fallback.
|
|
163
|
+
* GPU generation remains unavailable until a GPU readback can own the canonical field; a host
|
|
164
|
+
* adapter report therefore never upgrades a CPU fallback into a GPU generation claim.
|
|
165
|
+
*
|
|
166
|
+
* @situation decide whether generated terrain can use GPU compute
|
|
167
|
+
* @situation report why terrain generation is using a reduced CPU fallback
|
|
168
|
+
* @constraint unsupported is returned when compute limits are unknown or below the requirement; callers must not silently continue
|
|
169
|
+
* @override minimumWorkgroupsPerDimension, minimumStorageBufferBindingSize, and cpuFallbackIterations
|
|
170
|
+
* @example const capabilities = getWorldCapabilities({ limits: adapter.limits, cpuFallbackIterations: 8 });
|
|
171
|
+
*/
|
|
172
|
+
declare function getWorldCapabilities(options?: IWorldCapabilitiesOptions): IWorldCapabilities;
|
|
173
|
+
|
|
174
|
+
interface IHeightfieldOrigin {
|
|
175
|
+
readonly x: number;
|
|
176
|
+
readonly z: number;
|
|
177
|
+
}
|
|
178
|
+
interface IHeightfieldOptions {
|
|
179
|
+
readonly columns: number;
|
|
180
|
+
readonly depth: number;
|
|
181
|
+
readonly heights: Float32Array;
|
|
182
|
+
readonly origin: IHeightfieldOrigin;
|
|
183
|
+
readonly rows: number;
|
|
184
|
+
readonly width: number;
|
|
185
|
+
readonly worldPasses?: IHeightfieldWorldPassOptions;
|
|
186
|
+
}
|
|
187
|
+
interface IHeightfieldWorldPassOptions {
|
|
188
|
+
/** Maximum TSL compute dispatches submitted by one rendered frame. */
|
|
189
|
+
readonly dispatchBudget: number;
|
|
190
|
+
/** Reserved GPU path; true is rejected until GPU readback owns the canonical field. */
|
|
191
|
+
readonly gpu?: boolean;
|
|
192
|
+
/** Every physical coefficient is game supplied; core owns no terrain preset. */
|
|
193
|
+
readonly erosion: IWorldErosionOptions;
|
|
194
|
+
}
|
|
195
|
+
interface IHeightfieldSamplerOptions extends Omit<IHeightfieldOptions, "heights"> {
|
|
196
|
+
/** Game-owned terrain function. It is evaluated once and never retained. */
|
|
197
|
+
readonly sampleHeight: (x: number, z: number) => number;
|
|
198
|
+
}
|
|
199
|
+
interface IStoredHeightfieldChannels {
|
|
200
|
+
readonly flow?: Float32Array;
|
|
201
|
+
readonly moisture?: Float32Array;
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* One height buffer shared by world queries, rendered geometry, and a physics heightfield.
|
|
205
|
+
*
|
|
206
|
+
* The game supplies every value, so changing the terrain's shape never requires a package edit.
|
|
207
|
+
* `fromSampler` evaluates that game function exactly once at each vertex and retains only the
|
|
208
|
+
* resulting numbers. Queries interpolate those same numbers instead of evaluating the function
|
|
209
|
+
* again.
|
|
210
|
+
*
|
|
211
|
+
* @situation build terrain geometry and collision from one game-authored height function
|
|
212
|
+
* @situation generate a terrain a player can walk across
|
|
213
|
+
* @situation query the same ground height or normal that a player sees and collides with
|
|
214
|
+
* @situation ask how high the ground is here
|
|
215
|
+
* @situation build islands and coastlines from terrain
|
|
216
|
+
* @constraint sampleHeight owns the terrain shape and stays in game source; the framework stores and interpolates its output
|
|
217
|
+
* @constraint rows and columns are vertex counts; geometry is row-major z-then-x and collider export transposes once into Rapier's column-major matrix order
|
|
218
|
+
* @override rows, columns, width, depth, origin, and sampleHeight are explicit on every field
|
|
219
|
+
* @example const field = Heightfield.fromSampler({ rows: 65, columns: 65, width: 64, depth: 64, origin: { x: 0, z: 0 }, sampleHeight: terrainHeight });
|
|
220
|
+
*/
|
|
221
|
+
declare class Heightfield extends Group implements IComputeDriven {
|
|
222
|
+
#private;
|
|
223
|
+
readonly columns: number;
|
|
224
|
+
readonly depth: number;
|
|
225
|
+
readonly origin: IHeightfieldOrigin;
|
|
226
|
+
readonly rows: number;
|
|
227
|
+
readonly width: number;
|
|
228
|
+
readonly processCadence: "render";
|
|
229
|
+
readonly warmupNodes: readonly unknown[];
|
|
230
|
+
constructor(options: IHeightfieldOptions & IStoredHeightfieldChannels);
|
|
231
|
+
static fromSampler(options: IHeightfieldSamplerOptions): Heightfield;
|
|
232
|
+
sample(channel: string, x: number, z: number): number;
|
|
233
|
+
get released(): boolean;
|
|
234
|
+
get generationComplete(): boolean;
|
|
235
|
+
get gpuHeightSample(): IGPUReadbackSample | undefined;
|
|
236
|
+
attachRenderer(renderer: IRendererLike): void;
|
|
237
|
+
process(renderer?: IRendererLike | undefined): void;
|
|
238
|
+
debug(): Record<string, unknown>;
|
|
239
|
+
detach(): void;
|
|
240
|
+
/** A copy of the canonical row-major samples, safe for game-side analysis. */
|
|
241
|
+
get heights(): Float32Array;
|
|
242
|
+
/** A copy of the normalized routed-flow channel, when world passes were requested. */
|
|
243
|
+
get flow(): Float32Array | undefined;
|
|
244
|
+
/** Bytes retained by this field's CPU channels and its conservative GPU allowance. */
|
|
245
|
+
get memoryBytes(): number;
|
|
246
|
+
heightAt(x: number, z: number): number;
|
|
247
|
+
normalAt(x: number, z: number, target?: Vector3): Vector3;
|
|
248
|
+
/** The same values transposed once into Rapier's column-major height-matrix order. */
|
|
249
|
+
toColliderHeights(): Float32Array;
|
|
250
|
+
/** Builds an ordinary Three.js geometry without choosing its surface. */
|
|
251
|
+
toGeometry(): BufferGeometry;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export { Heightfield, type IHeightfieldOptions, type IHeightfieldOrigin, type IHeightfieldSamplerOptions, type IHeightfieldWorldPassOptions, type IWorldCapabilities, type IWorldTileColliderInput, type IWorldTilesTopologyObservation, TerrainTiles, getWorldCapabilities };
|