@threenative/core 0.3.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.
Files changed (39) hide show
  1. package/README.md +10 -0
  2. package/capabilities.json +1881 -131
  3. package/dist/assets-kyoF7JlJ.d.ts +103 -0
  4. package/dist/{audio-Dp2mXpD3.d.ts → audio-BFiGneTL.d.ts} +62 -0
  5. package/dist/canvas-layer-BLVijiUJ.d.ts +62 -0
  6. package/dist/{game-CYIaKhgl.d.ts → game-XGrTzapq.d.ts} +350 -164
  7. package/dist/gpu-readback-D2iRvoe9.d.ts +112 -0
  8. package/dist/hot.d.ts +5 -3
  9. package/dist/hot.js +5 -1
  10. package/dist/index.d.ts +813 -143
  11. package/dist/index.js +4548 -941
  12. package/dist/net.d.ts +65 -0
  13. package/dist/net.js +643 -0
  14. package/dist/playtest.d.ts +29 -5
  15. package/dist/playtest.js +181 -35
  16. package/dist/react.d.ts +4 -2
  17. package/dist/{canvas-layer-CtrZHgIh.d.ts → renderer-C6hqZpoG.d.ts} +237 -75
  18. package/dist/world.d.ts +203 -4
  19. package/dist/world.js +2536 -25
  20. package/gpl/LICENSE.GPL +117 -0
  21. package/gpl/convert.py +192 -0
  22. package/gpl/recipes/_common.py +169 -0
  23. package/gpl/recipes/bake_ao.py +111 -0
  24. package/gpl/recipes/decimate.py +64 -0
  25. package/gpl/recipes/retarget.py +131 -0
  26. package/gpl/recipes/unwrap.py +71 -0
  27. package/mcp/blender-server.mjs +632 -0
  28. package/mcp/blender.mjs +27 -0
  29. package/mcp/engine-server.mjs +271 -23
  30. package/mcp/engine.mjs +15 -7
  31. package/mcp/install.d.mts +37 -0
  32. package/mcp/install.mjs +94 -26
  33. package/mcp/servers.d.mts +34 -0
  34. package/mcp/servers.mjs +110 -9
  35. package/package.json +36 -8
  36. package/patches/three@0.185.1.patch +249 -14
  37. package/scripts/ensure-mcp.mjs +20 -13
  38. package/scripts/bundle-engine-mcp.mjs +0 -15
  39. package/scripts/generate-version.mjs +0 -13
package/dist/playtest.js CHANGED
@@ -8,14 +8,24 @@ function audioRuntimeSnapshot() {
8
8
  let queued = 0;
9
9
  let voices = 0;
10
10
  let pooled = 0;
11
+ let paused = 0;
12
+ const unsupported = /* @__PURE__ */ new Set();
11
13
  for (const bus of buses) {
12
14
  queued += bus.queued;
13
15
  voices += bus.voices;
14
16
  pooled += bus.pooled;
17
+ paused += bus.pausedVoices;
18
+ for (const option of bus.unsupported) unsupported.add(option);
15
19
  }
16
- return { pooled, queued, voices };
20
+ return { paused, pooled, queued, unsupported: [...unsupported].sort(), voices };
17
21
  }
18
22
 
23
+ // src/version.ts
24
+ var CORE_VERSION = "0.3.1";
25
+
26
+ // src/pipeline-census.ts
27
+ var PIPELINE_CENSUS_CAPABILITY = "runtime.pipelineCensus";
28
+
19
29
  // src/render/chain.ts
20
30
  var REPORT_KEY = /* @__PURE__ */ Symbol.for("threenative.renderChain.report");
21
31
  function storedReport(target) {
@@ -36,17 +46,29 @@ function isObject(value) {
36
46
  return typeof value === "object" && value !== null;
37
47
  }
38
48
 
39
- // src/version.ts
40
- var CORE_VERSION = "0.3.0";
41
-
42
49
  // src/playtest.ts
43
50
  var currentAgent = typeof navigator === "undefined" ? "node" : navigator.userAgent;
44
51
  function playtest(options = {}) {
45
52
  let dispose;
46
53
  let attached;
54
+ let startSceneEntered;
55
+ let disposePipelineCensus;
47
56
  let contactHistory = [];
57
+ const watcher = createTransitionWatcher();
58
+ let readTick;
48
59
  return {
60
+ sceneExit: () => {
61
+ contactHistory = [];
62
+ watcher.reset();
63
+ },
64
+ update: (ctx) => {
65
+ const tick = readTick?.();
66
+ if (tick === void 0) return;
67
+ drainContacts(ctx, contactHistory, tick);
68
+ watcher.observe(ctx, tick);
69
+ },
49
70
  setup: async (ctx, runtime) => {
71
+ readTick = runtime?.tick;
50
72
  const seed = runtime?.seed ?? null;
51
73
  const replayRuntime = runtime?.seed === null || runtime?.seed === void 0 ? void 0 : {
52
74
  agent: currentAgent,
@@ -62,13 +84,16 @@ function playtest(options = {}) {
62
84
  ...runtime === void 0 ? {} : { fixedStep: runtime.fixedStep, tick: runtime.tick },
63
85
  ...runtime?.runtimeDiagnosticsSeries === void 0 ? {} : { runtimeDiagnosticsSeries: runtime.runtimeDiagnosticsSeries },
64
86
  ...options.events === void 0 ? {} : { events: options.events },
65
- gameplay: () => gameplayObservations(ctx, contactHistory, seed, replayRuntime),
87
+ gameplay: () => gameplayObservations(ctx, contactHistory, seed, replayRuntime, watcher),
66
88
  gameplayChannels: () => gameplayChannels(),
67
89
  renderer: ctx.renderer.raw,
68
90
  renderChain: () => {
69
91
  const observation = readRenderChainObservation(ctx.renderer);
70
92
  return observation === void 0 ? void 0 : {
71
93
  ...observation,
94
+ contributions: observation.contributions.map((contribution) => ({
95
+ ...contribution
96
+ })),
72
97
  dropped: observation.dropped.map((stage) => ({ ...stage })),
73
98
  requested: [...observation.requested],
74
99
  stages: [...observation.stages],
@@ -83,19 +108,35 @@ function playtest(options = {}) {
83
108
  startup: () => ({
84
109
  phase: ctx.startup.phase,
85
110
  progress: ctx.startup.progress,
86
- ...runtime?.startupCompileSettled === void 0 ? {} : { compileSettled: runtime.startupCompileSettled() }
111
+ ...ctx.startup.warmup === void 0 ? {} : { warmup: ctx.startup.warmup },
112
+ ...runtime?.startupCompileSettled === void 0 ? {} : { compileSettled: runtime.startupCompileSettled() },
113
+ // Wall-clock milestones, so a scenario asserts startup time as an observation instead
114
+ // of reading it off a console marker after the fact.
115
+ ...runtime?.startupTimeline === void 0 ? {} : { timeline: runtime.startupTimeline() }
87
116
  })
88
117
  });
118
+ const pipelineCensus = runtime?.pipelineCensus;
119
+ if (runtime !== void 0 && pipelineCensus !== void 0) {
120
+ disposePipelineCensus = runtime.observations.contribute({
121
+ capabilities: [PIPELINE_CENSUS_CAPABILITY],
122
+ sample: () => ({ pipelineCensus: pipelineCensus() })
123
+ });
124
+ }
89
125
  installRuntimeChannels(installation.bridge, runtime);
90
126
  if (globalThis[PLAYTEST_RUNNER_EXPECTED_GLOBAL] === true)
91
127
  runtime?.enableRuntimeDiagnostics?.();
92
128
  dispose = installation.dispose;
93
- attached = holdUntilAttached(installation.bridge, options);
129
+ attached = holdUntilAttached(installation.bridge, options, () => startSceneEntered);
94
130
  const cleanup = () => {
131
+ disposePipelineCensus?.();
132
+ disposePipelineCensus = void 0;
95
133
  dispose?.();
96
134
  dispose = void 0;
97
135
  attached = void 0;
136
+ startSceneEntered = void 0;
98
137
  contactHistory = [];
138
+ readTick = void 0;
139
+ watcher.reset();
99
140
  };
100
141
  if (attached !== void 0) {
101
142
  const gate = attached.catch((error) => {
@@ -103,7 +144,10 @@ function playtest(options = {}) {
103
144
  throw error;
104
145
  });
105
146
  if (runtime?.holdStart === void 0) await gate;
106
- else runtime.holdStart(gate);
147
+ else {
148
+ startSceneEntered = runtime.holdStart(gate);
149
+ void startSceneEntered.catch(() => void 0);
150
+ }
107
151
  }
108
152
  return cleanup;
109
153
  }
@@ -113,27 +157,54 @@ var PLAYTEST_ATTACH_TIMEOUT_MS = 3e4;
113
157
  var PLAYTEST_RUNNER_EXPECTED_GLOBAL = "__THREENATIVE_PLAYTEST_RUNNER_EXPECTED__";
114
158
  function shouldHoldUntilAttached(options) {
115
159
  if (options.holdUntilAttached !== void 0) return options.holdUntilAttached;
116
- return globalThis[PLAYTEST_RUNNER_EXPECTED_GLOBAL] === true;
160
+ const host = globalThis;
161
+ return host[PLAYTEST_RUNNER_EXPECTED_GLOBAL] === true || host.TN_PLAYTEST_ENDPOINT !== void 0;
117
162
  }
118
- function holdUntilAttached(bridge, options) {
163
+ function holdUntilAttached(bridge, options, startSceneEntered) {
119
164
  if (!shouldHoldUntilAttached(options)) return void 0;
120
165
  const timeoutMs = options.attachTimeoutMs ?? PLAYTEST_ATTACH_TIMEOUT_MS;
121
166
  if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
122
167
  throw new Error(`TN_PLAYTEST_ATTACH_TIMEOUT_INVALID: ${String(options.attachTimeoutMs)}`);
123
168
  }
124
169
  const describe = bridge.describe;
170
+ const applySetup = bridge.applySetup;
125
171
  return new Promise((resolve, reject) => {
172
+ let settled = false;
126
173
  const timer = setTimeout(() => {
174
+ settled = true;
127
175
  reject(
128
176
  new Error(
129
- `TN_PLAYTEST_ATTACH_TIMEOUT: no playtest runner called describe() within ${timeoutMs}ms.`
177
+ `TN_PLAYTEST_ATTACH_TIMEOUT: no playtest runner applied setup or called describe() within ${timeoutMs}ms.`
130
178
  )
131
179
  );
132
180
  }, timeoutMs);
133
181
  timer.unref?.();
134
- bridge.describe = () => {
182
+ const release = () => {
183
+ if (settled) return;
184
+ settled = true;
135
185
  clearTimeout(timer);
136
186
  resolve();
187
+ };
188
+ const fail = (error) => {
189
+ if (settled) return;
190
+ settled = true;
191
+ clearTimeout(timer);
192
+ reject(error);
193
+ };
194
+ if (applySetup !== void 0) {
195
+ bridge.applySetup = async (request) => {
196
+ try {
197
+ await applySetup(request);
198
+ release();
199
+ } catch (error) {
200
+ fail(error);
201
+ throw error;
202
+ }
203
+ };
204
+ }
205
+ bridge.describe = async () => {
206
+ release();
207
+ await startSceneEntered();
137
208
  return describe();
138
209
  };
139
210
  });
@@ -198,7 +269,7 @@ function entityObject(entity) {
198
269
  const candidate = entity?.mesh ?? entity?.object;
199
270
  return candidate instanceof Object3D ? candidate : void 0;
200
271
  }
201
- function gameplayObservations(ctx, contactHistory, seed, replayRuntime) {
272
+ function gameplayObservations(ctx, contactHistory, seed, replayRuntime, watcher) {
202
273
  const animation = {};
203
274
  const states = {};
204
275
  const snapshot = ctx.entities.snapshot();
@@ -206,61 +277,136 @@ function gameplayObservations(ctx, contactHistory, seed, replayRuntime) {
206
277
  const entity = ctx.entities.get(id);
207
278
  const finished = typeof entity?.animation?.finished === "boolean" ? entity.animation.finished : void 0;
208
279
  if (typeof entity?.animation?.current === "string" && typeof entity.animation.advancedFrames === "number") {
280
+ const stride = strideObservation(entity.animation.stride);
209
281
  animation[id] = {
210
282
  advancedFrames: entity.animation.advancedFrames,
211
283
  clip: entity.animation.current,
212
- ...finished === void 0 ? {} : { finished }
284
+ ...finished === void 0 ? {} : { finished },
285
+ ...stride === void 0 ? {} : { stride }
213
286
  };
214
287
  }
215
288
  if (typeof entity?.state === "string") states[id] = entity.state;
216
289
  }
217
290
  const channels = gameplayChannels();
218
- const contacts = channels.includes("runtime.contacts") ? drainContacts(ctx, contactHistory) : void 0;
291
+ const contacts = channels.includes("runtime.contacts") ? drainContacts(ctx, contactHistory, watcher.lastTick()) : void 0;
219
292
  const tags = channels.includes("runtime.tags") ? tagCounts(snapshot) : void 0;
293
+ const transitions = watcher.read();
220
294
  return {
221
295
  animation,
222
296
  ...contacts === void 0 ? {} : { contacts },
223
297
  states,
224
298
  ...tags === void 0 ? {} : { tags },
299
+ ...transitions === void 0 ? {} : { transitions },
225
300
  ...runtimeObservation(seed, replayRuntime)
226
301
  };
227
302
  }
303
+ function strideObservation(value) {
304
+ if (typeof value !== "object" || value === null) return void 0;
305
+ const report = value;
306
+ const numbers = ["clipGroundSpeed", "groundSpeed", "rate"];
307
+ const flags = ["overridden", "synced"];
308
+ if (!numbers.every((key) => typeof report[key] === "number" && Number.isFinite(report[key])))
309
+ return void 0;
310
+ if (!flags.every((key) => typeof report[key] === "boolean")) return void 0;
311
+ return {
312
+ clipGroundSpeed: report.clipGroundSpeed,
313
+ groundSpeed: report.groundSpeed,
314
+ overridden: report.overridden,
315
+ rate: report.rate,
316
+ synced: report.synced
317
+ };
318
+ }
228
319
  function gameplayChannels(_ctx) {
229
- return ["runtime.contacts", "runtime.tags"];
320
+ return ["runtime.contacts", "runtime.tags", "runtime.transitions"];
230
321
  }
231
- function drainContacts(ctx, history) {
232
- const snapshot = ctx.entities.snapshot();
322
+ function drainContacts(ctx, history, tick) {
233
323
  const idsByEntity = /* @__PURE__ */ new Map();
234
- for (const id of Object.keys(snapshot)) {
235
- const registered = ctx.entities.get(id);
236
- if (registered === void 0) continue;
237
- for (const value of Object.values(registered)) {
238
- if (value !== null && typeof value === "object" && !idsByEntity.has(value))
239
- idsByEntity.set(value, id);
240
- }
241
- }
242
- for (const id of Object.keys(snapshot)) {
243
- for (const source of entitySources(ctx.entities.get(id))) {
324
+ ctx.entities.forEach((id, registered) => {
325
+ for (const value of objectGraphValues(registered))
326
+ if (!idsByEntity.has(value)) idsByEntity.set(value, id);
327
+ });
328
+ ctx.entities.forEach((id, registered) => {
329
+ for (const source of entitySources(registered)) {
244
330
  for (const event of source.drainContacts()) {
245
331
  const entity = idsByEntity.get(event.body);
246
- if (entity === void 0 || event.entity === void 0) continue;
332
+ const other = event.entity ?? (event.area === void 0 ? void 0 : idsByEntity.get(event.area)) ?? idsByEntity.get(source);
333
+ if (entity === void 0 || other === void 0) continue;
247
334
  history.push({
248
335
  entity,
249
336
  kind: event.started ? "trigger" : "trigger.exit",
250
- with: event.entity
337
+ ...tick === void 0 ? {} : { tick },
338
+ with: other
251
339
  });
252
340
  if (history.length > PLAYTEST_PROTOCOL_LIMITS.maxEventsPerDrain) history.shift();
253
341
  }
254
342
  }
255
- }
343
+ });
256
344
  return [...history];
257
345
  }
346
+ var PLAYTEST_TRANSITION_LOG_LIMIT = 512;
347
+ function primitiveOrUndefined(value) {
348
+ if (value === null) return null;
349
+ if (typeof value === "string" || typeof value === "boolean") return value;
350
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
351
+ }
352
+ function createTransitionWatcher() {
353
+ let previous = /* @__PURE__ */ new Map();
354
+ let log = [];
355
+ let seen = false;
356
+ let tickSeen;
357
+ const record = (path, to, tick) => {
358
+ const had = previous.has(path);
359
+ const from = previous.get(path);
360
+ previous.set(path, to);
361
+ if (!had || from === to) return;
362
+ log.push({ from, path, tick, to });
363
+ if (log.length > PLAYTEST_TRANSITION_LOG_LIMIT) log.shift();
364
+ };
365
+ return {
366
+ lastTick: () => tickSeen,
367
+ observe: (ctx, tick) => {
368
+ tickSeen = tick;
369
+ seen = true;
370
+ ctx.entities.forEach((id, entity) => {
371
+ const candidate = entity;
372
+ if (typeof candidate.state === "string") record(`states.${id}`, candidate.state, tick);
373
+ });
374
+ const state = ctx.state.getState();
375
+ for (const key of Object.keys(state)) {
376
+ const value = primitiveOrUndefined(state[key]);
377
+ if (value !== void 0) record(`state.${key}`, value, tick);
378
+ }
379
+ },
380
+ // Absent until the loop has ticked at least once. A run that never ticked has not observed
381
+ // that nothing changed; it has observed nothing, and an assertion against it must say so.
382
+ read: () => seen ? log.map((entry) => ({ ...entry })) : void 0,
383
+ reset: () => {
384
+ previous = /* @__PURE__ */ new Map();
385
+ log = [];
386
+ seen = false;
387
+ tickSeen = void 0;
388
+ }
389
+ };
390
+ }
258
391
  function entitySources(entity) {
259
- if (entity === void 0) return [];
260
- return Object.values(entity).filter(
261
- (value) => typeof value === "object" && value !== null && "drainContacts" in value && typeof value.drainContacts === "function"
392
+ return objectGraphValues(entity).filter(
393
+ (value) => "drainContacts" in value && typeof value.drainContacts === "function"
262
394
  );
263
395
  }
396
+ function objectGraphValues(root) {
397
+ if (root === void 0) return [];
398
+ const values = [];
399
+ const visited = /* @__PURE__ */ new Set();
400
+ const visit = (value, depth) => {
401
+ if (value === null || typeof value !== "object" || visited.has(value)) return;
402
+ visited.add(value);
403
+ values.push(value);
404
+ if (depth >= 4 || value instanceof Object3D) return;
405
+ for (const child of Object.values(value)) visit(child, depth + 1);
406
+ };
407
+ visit(root, 0);
408
+ return values;
409
+ }
264
410
  function tagCounts(snapshot) {
265
411
  const counts = {};
266
412
  for (const fields of Object.values(snapshot)) {
@@ -303,4 +449,4 @@ function cloneJsonValue(value) {
303
449
  return value;
304
450
  }
305
451
 
306
- export { PLAYTEST_ATTACH_TIMEOUT_MS, PLAYTEST_RUNNER_EXPECTED_GLOBAL, playtest };
452
+ export { PLAYTEST_ATTACH_TIMEOUT_MS, PLAYTEST_RUNNER_EXPECTED_GLOBAL, PLAYTEST_TRANSITION_LOG_LIMIT, playtest };
package/dist/react.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { ReactNode } from 'react';
2
- import { C as CanvasLayer } from './canvas-layer-CtrZHgIh.js';
2
+ import { C as CanvasLayer } from './canvas-layer-BLVijiUJ.js';
3
3
  import 'three';
4
+ import './renderer-C6hqZpoG.js';
4
5
  import 'three/webgpu';
5
6
 
6
7
  /**
@@ -162,12 +163,13 @@ interface ITextProps {
162
163
  /**
163
164
  * A rectangle. Paints when its style has a `background`; otherwise it only positions children.
164
165
  * @situation group and position native React HUD elements
165
- * @example <View style={{ centerX: 0, top: 24 }}><Text>READY</Text></View>
166
+ * @example <View style={{ centerX: true, top: 24 }}><Text>READY</Text></View>
166
167
  */
167
168
  declare function View(props: IViewProps): ReactNode;
168
169
  /**
169
170
  * A run of bitmap glyphs, drawn as one instanced quad per lit pixel.
170
171
  * @situation show text in a native React HUD without a DOM
172
+ * @alias objective panel journal
171
173
  * @example <Text style={{ color: "#ffffff", fontSize: 24 }}>SCORE 10</Text>
172
174
  */
173
175
  declare function Text(props: ITextProps): ReactNode;