@threenative/core 0.3.2 → 0.3.4
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/capabilities.json +1891 -101
- package/dist/{assets-kyoF7JlJ.d.ts → assets-CqvE429w.d.ts} +52 -3
- package/dist/{audio-BFiGneTL.d.ts → audio-7i3Xl0l3.d.ts} +32 -1
- package/dist/{canvas-layer-BLVijiUJ.d.ts → canvas-layer-DDmC_VVF.d.ts} +1 -1
- package/dist/{game-XGrTzapq.d.ts → game-CljaDv4D.d.ts} +503 -11
- package/dist/{gpu-readback-D2iRvoe9.d.ts → gpu-readback-CqJEfWNQ.d.ts} +16 -6
- package/dist/hot.d.ts +5 -5
- package/dist/hot.js +20 -3
- package/dist/index.d.ts +1496 -43
- package/dist/index.js +8345 -753
- package/dist/playtest.d.ts +11 -5
- package/dist/playtest.js +96 -11
- package/dist/react.d.ts +2 -2
- package/dist/{renderer-C6hqZpoG.d.ts → renderer-CfsS2hxi.d.ts} +436 -14
- package/dist/ui-layer.d.ts +15 -7
- package/dist/ui-layer.js +2 -1
- package/dist/world.d.ts +900 -14
- package/dist/world.js +10876 -126
- package/gpl/fixtures/make_world_fixture.py +184 -0
- package/gpl/recipes/_common.py +11 -0
- package/gpl/recipes/decimate.py +11 -5
- package/gpl/recipes/export_world.py +682 -0
- package/mcp/blender-server.mjs +55 -1
- package/mcp/engine-server.mjs +84 -22
- package/mcp/servers.mjs +3 -3
- package/package.json +6 -6
- package/patches/three@0.185.1.patch +2228 -105
- package/scripts/apply-three-patch.mjs +61 -37
package/mcp/blender-server.mjs
CHANGED
|
@@ -379,6 +379,21 @@ var RECIPES = Object.freeze([
|
|
|
379
379
|
}
|
|
380
380
|
]),
|
|
381
381
|
script: "retarget.py"
|
|
382
|
+
}),
|
|
383
|
+
Object.freeze({
|
|
384
|
+
description: "Export a Blender-authored world as a ThreeNative world package v1: world.json, a raw uint16 heightmap, one GLB plus a decimated LOD1 per referenced scatter asset, per-cell chunk GLBs and placements.bin. Reads render-density instances, so viewport share tricks never leak in.",
|
|
385
|
+
name: "export_world",
|
|
386
|
+
parameters: Object.freeze([
|
|
387
|
+
{ description: "Path to the .blend world to read.", name: "source", required: true },
|
|
388
|
+
{ description: "Directory to write the package into.", name: "out", required: true },
|
|
389
|
+
{ description: "Cell edge length in metres.", name: "cell", required: true },
|
|
390
|
+
{
|
|
391
|
+
description: "Heightmap sample spacing in metres. Default 2.",
|
|
392
|
+
name: "spacing",
|
|
393
|
+
required: false
|
|
394
|
+
}
|
|
395
|
+
]),
|
|
396
|
+
script: "export_world.py"
|
|
382
397
|
})
|
|
383
398
|
]);
|
|
384
399
|
function recipeNames() {
|
|
@@ -424,7 +439,7 @@ async function runRecipe(name, request, options = {}) {
|
|
|
424
439
|
|
|
425
440
|
// src/index.ts
|
|
426
441
|
var SERVER_NAME = "threenative-blender-mcp";
|
|
427
|
-
var SERVER_VERSION = "0.1.
|
|
442
|
+
var SERVER_VERSION = "0.1.4";
|
|
428
443
|
var AUTHORING_INSTRUCTIONS = "Call blender_status before anything else: it answers whether this machine can convert models at all, and when it cannot it names the install command rather than failing. Blender is never installed for the user \u2014 ask first, then let them run the command. An .fbx, .blend, .obj or .dae placed in a game's assets directory is converted by the build itself; these tools are for inspecting a source before committing it and for operations the build does not perform. For anything no named tool covers, read a shipped recipe with blender_recipes and adapt it, then run it with blender_run_python.";
|
|
429
444
|
var TOOL_DEFINITIONS = [
|
|
430
445
|
{
|
|
@@ -476,6 +491,25 @@ var TOOL_DEFINITIONS = [
|
|
|
476
491
|
type: "object"
|
|
477
492
|
}
|
|
478
493
|
},
|
|
494
|
+
{
|
|
495
|
+
annotations: { destructiveHint: true, openWorldHint: false, readOnlyHint: false },
|
|
496
|
+
name: "blender_export_world",
|
|
497
|
+
description: "Export a Blender-authored world as a ThreeNative world package v1: world.json, a raw uint16 heightmap, one GLB plus a decimated LOD1 per referenced scatter asset, per-cell chunk GLBs and placements.bin. Reads render-density instances so viewport share tricks never leak in; marks the terrain with tn_world_terrain, scatter sources with tn_asset_id and chunk collections with tn_world_chunk.",
|
|
498
|
+
inputSchema: {
|
|
499
|
+
additionalProperties: false,
|
|
500
|
+
properties: {
|
|
501
|
+
cell: { description: "Cell edge length in metres.", type: "number" },
|
|
502
|
+
out: { description: "Directory to write the package into.", type: "string" },
|
|
503
|
+
source: { description: "Path to the .blend world to read.", type: "string" },
|
|
504
|
+
spacing: {
|
|
505
|
+
description: "Heightmap sample spacing in metres. Default 2.",
|
|
506
|
+
type: "number"
|
|
507
|
+
}
|
|
508
|
+
},
|
|
509
|
+
required: ["source", "out", "cell"],
|
|
510
|
+
type: "object"
|
|
511
|
+
}
|
|
512
|
+
},
|
|
479
513
|
{
|
|
480
514
|
annotations: { destructiveHint: true, openWorldHint: false, readOnlyHint: false },
|
|
481
515
|
name: "blender_run_python",
|
|
@@ -570,6 +604,26 @@ async function handleToolCall(params) {
|
|
|
570
604
|
return toolText(await convertModel(source, out));
|
|
571
605
|
}
|
|
572
606
|
if (name === "blender_recipes") return toolText(await handleRecipes(argumentsValue));
|
|
607
|
+
if (name === "blender_export_world") {
|
|
608
|
+
const source = requiredString(argumentsValue, "source", "blender_export_world");
|
|
609
|
+
const out = requiredString(argumentsValue, "out", "blender_export_world");
|
|
610
|
+
const cell = argumentsValue.cell;
|
|
611
|
+
if (typeof cell !== "number" || !Number.isFinite(cell) || cell <= 0) {
|
|
612
|
+
throw new Error("blender_export_world 'cell' must be a number greater than zero.");
|
|
613
|
+
}
|
|
614
|
+
const spacing = argumentsValue.spacing;
|
|
615
|
+
if (spacing !== void 0 && (typeof spacing !== "number" || !Number.isFinite(spacing) || spacing <= 0)) {
|
|
616
|
+
throw new Error("blender_export_world 'spacing' must be a number greater than zero.");
|
|
617
|
+
}
|
|
618
|
+
return toolText(
|
|
619
|
+
await runRecipe("export_world", {
|
|
620
|
+
cell,
|
|
621
|
+
out,
|
|
622
|
+
source,
|
|
623
|
+
...spacing === void 0 ? {} : { spacing }
|
|
624
|
+
})
|
|
625
|
+
);
|
|
626
|
+
}
|
|
573
627
|
if (name === "blender_run_python") {
|
|
574
628
|
const script = requiredString(argumentsValue, "script", "blender_run_python");
|
|
575
629
|
const request = isRecord(argumentsValue.arguments) ? argumentsValue.arguments : {};
|
package/mcp/engine-server.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { realpathSync, readFileSync, existsSync } from 'fs';
|
|
2
|
+
import { realpathSync, readFileSync, existsSync, mkdirSync, appendFileSync } from 'fs';
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import { createInterface } from 'readline';
|
|
5
5
|
import { fileURLToPath } from 'url';
|
|
@@ -131,14 +131,13 @@ function defaultManifestPath(cwd = process.cwd()) {
|
|
|
131
131
|
const override = process.env.THREENATIVE_CAPABILITIES_MANIFEST;
|
|
132
132
|
if (override !== void 0 && override.trim().length > 0) return path.resolve(cwd, override);
|
|
133
133
|
for (let directory = path.resolve(cwd); ; directory = path.dirname(directory)) {
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
"
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
if (existsSync(installed)) return installed;
|
|
134
|
+
const nodeModules = path.join(directory, "node_modules");
|
|
135
|
+
const installed = path.join(nodeModules, "@threenative", "core", "capabilities.json");
|
|
136
|
+
if (existsSync(installed)) {
|
|
137
|
+
const relative = path.relative(realpathSync(directory), realpathSync(nodeModules));
|
|
138
|
+
if (relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative))
|
|
139
|
+
return installed;
|
|
140
|
+
}
|
|
142
141
|
const parent = path.dirname(directory);
|
|
143
142
|
if (parent === directory) break;
|
|
144
143
|
}
|
|
@@ -156,6 +155,9 @@ function manifestError(file, reason) {
|
|
|
156
155
|
function isRecord(value) {
|
|
157
156
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
158
157
|
}
|
|
158
|
+
function isStringArray(value) {
|
|
159
|
+
return Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
160
|
+
}
|
|
159
161
|
function validateManifest(value, file) {
|
|
160
162
|
if (!isRecord(value) || value.version !== 2 || !Array.isArray(value.entries) || !Array.isArray(value.notOwned)) {
|
|
161
163
|
throw manifestError(
|
|
@@ -164,7 +166,7 @@ function validateManifest(value, file) {
|
|
|
164
166
|
);
|
|
165
167
|
}
|
|
166
168
|
for (const [index, raw] of value.entries.entries()) {
|
|
167
|
-
if (!isRecord(raw) || typeof raw.symbol !== "string" || typeof raw.package !== "string" || typeof raw.importPath !== "string" || typeof raw.kind !== "string" || typeof raw.signature !== "string" || typeof raw.summary !== "string" || typeof raw.example !== "string" || !Array.isArray(raw.situations) || !Array.isArray(raw.aliases) || !Array.isArray(raw.constraints) || raw.requires !== void 0 && !
|
|
169
|
+
if (!isRecord(raw) || typeof raw.symbol !== "string" || typeof raw.package !== "string" || typeof raw.importPath !== "string" || typeof raw.kind !== "string" || typeof raw.signature !== "string" || typeof raw.summary !== "string" || typeof raw.example !== "string" || !Array.isArray(raw.situations) || !Array.isArray(raw.aliases) || !Array.isArray(raw.constraints) || raw.requires !== void 0 && !isStringArray(raw.requires) || raw.deprecated !== void 0 && !isStringArray(raw.deprecated) || !raw.situations.every((situation) => typeof situation === "string") || !raw.aliases.every((alias) => typeof alias === "string") || !raw.constraints.every((constraint) => typeof constraint === "string")) {
|
|
168
170
|
throw manifestError(file, `entry ${index} is malformed`);
|
|
169
171
|
}
|
|
170
172
|
}
|
|
@@ -375,13 +377,25 @@ function searchCapabilities(situation, manifestFile = defaultManifestPath(), sco
|
|
|
375
377
|
verdict: "none"
|
|
376
378
|
};
|
|
377
379
|
}
|
|
378
|
-
function
|
|
380
|
+
function capabilityDetails(symbol, manifestFile = defaultManifestPath(), importPath) {
|
|
379
381
|
if (typeof symbol !== "string" || symbol.trim().length === 0)
|
|
380
382
|
throw new Error("engine_capability_detail requires a non-empty symbol string.");
|
|
381
|
-
const
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
383
|
+
const matches = loadCapabilityManifest(manifestFile).entries.filter(
|
|
384
|
+
(candidate) => candidate.symbol === symbol && (importPath === void 0 || candidate.importPath === importPath)
|
|
385
|
+
);
|
|
386
|
+
if (matches.length === 0)
|
|
387
|
+
throw new Error(
|
|
388
|
+
`Unknown engine capability '${symbol}'${importPath === void 0 ? "" : ` in '${importPath}'`}.`
|
|
389
|
+
);
|
|
390
|
+
return matches;
|
|
391
|
+
}
|
|
392
|
+
function capabilityDetail(symbol, manifestFile = defaultManifestPath(), importPath) {
|
|
393
|
+
const matches = capabilityDetails(symbol, manifestFile, importPath);
|
|
394
|
+
if (matches.length > 1)
|
|
395
|
+
throw new Error(
|
|
396
|
+
`Ambiguous engine capability '${symbol}': pass importPath, one of ${matches.map((match) => `'${match.importPath}'`).join(", ")}.`
|
|
397
|
+
);
|
|
398
|
+
return matches[0];
|
|
385
399
|
}
|
|
386
400
|
var TOOL_DEFINITIONS = [
|
|
387
401
|
{
|
|
@@ -405,10 +419,16 @@ var TOOL_DEFINITIONS = [
|
|
|
405
419
|
{
|
|
406
420
|
annotations: { destructiveHint: false, openWorldHint: false, readOnlyHint: true },
|
|
407
421
|
name: "engine_capability_detail",
|
|
408
|
-
description: "Inspect one engine capability's import, signature, example, constraints, and overrides.",
|
|
422
|
+
description: "Inspect one engine capability's import, signature, example, constraints, and overrides. When several packages export the symbol, every match is returned under `matches`.",
|
|
409
423
|
inputSchema: {
|
|
410
424
|
additionalProperties: false,
|
|
411
|
-
properties: {
|
|
425
|
+
properties: {
|
|
426
|
+
importPath: {
|
|
427
|
+
description: "The search result's importPath; required when two packages export the same symbol.",
|
|
428
|
+
type: "string"
|
|
429
|
+
},
|
|
430
|
+
symbol: { type: "string" }
|
|
431
|
+
},
|
|
412
432
|
required: ["symbol"],
|
|
413
433
|
type: "object"
|
|
414
434
|
}
|
|
@@ -417,6 +437,22 @@ var TOOL_DEFINITIONS = [
|
|
|
417
437
|
function toolDefinitions() {
|
|
418
438
|
return TOOL_DEFINITIONS;
|
|
419
439
|
}
|
|
440
|
+
var DEFAULT_LOG_FILE = path.join(".threenative", "engine-mcp.log");
|
|
441
|
+
var logBroken = false;
|
|
442
|
+
function logToolCall(entry) {
|
|
443
|
+
if (logBroken) return;
|
|
444
|
+
const target = process.env.THREENATIVE_ENGINE_MCP_LOG ?? DEFAULT_LOG_FILE;
|
|
445
|
+
if (target === "off") return;
|
|
446
|
+
try {
|
|
447
|
+
mkdirSync(path.dirname(path.resolve(target)), { recursive: true });
|
|
448
|
+
appendFileSync(target, `${JSON.stringify({ time: (/* @__PURE__ */ new Date()).toISOString(), ...entry })}
|
|
449
|
+
`);
|
|
450
|
+
} catch (error) {
|
|
451
|
+
logBroken = true;
|
|
452
|
+
process.stderr.write(`engine-mcp log disabled (${target}): ${String(error)}
|
|
453
|
+
`);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
420
456
|
function jsonRpcError(id, code, message) {
|
|
421
457
|
return JSON.stringify({ id, jsonrpc: "2.0", error: { code, message } });
|
|
422
458
|
}
|
|
@@ -439,12 +475,36 @@ function handleToolCall(params, manifestFile) {
|
|
|
439
475
|
const scope = argumentsValue.scope ?? "mechanic";
|
|
440
476
|
if (scope !== "request" && scope !== "mechanic")
|
|
441
477
|
throw new Error("engine_search_capabilities scope must be 'request' or 'mechanic'.");
|
|
442
|
-
|
|
478
|
+
const response = searchCapabilities(argumentsValue.situation, manifestFile, scope);
|
|
479
|
+
logToolCall({
|
|
480
|
+
results: response.results.map((result) => result.symbol),
|
|
481
|
+
scope,
|
|
482
|
+
situation: argumentsValue.situation,
|
|
483
|
+
tool: name,
|
|
484
|
+
verdict: response.verdict
|
|
485
|
+
});
|
|
486
|
+
return toolText(response);
|
|
443
487
|
}
|
|
444
488
|
if (name === "engine_capability_detail") {
|
|
445
489
|
if (typeof argumentsValue.symbol !== "string")
|
|
446
490
|
throw new Error("engine_capability_detail requires a string 'symbol' argument.");
|
|
447
|
-
|
|
491
|
+
if (argumentsValue.importPath !== void 0 && typeof argumentsValue.importPath !== "string")
|
|
492
|
+
throw new Error("engine_capability_detail 'importPath' must be a string when given.");
|
|
493
|
+
const matches = capabilityDetails(
|
|
494
|
+
argumentsValue.symbol,
|
|
495
|
+
manifestFile,
|
|
496
|
+
argumentsValue.importPath
|
|
497
|
+
);
|
|
498
|
+
const detail = matches.length === 1 ? matches[0] : {
|
|
499
|
+
guidance: `${matches.length} packages export '${argumentsValue.symbol}'. Import the match whose summary fits; pass importPath to get one.`,
|
|
500
|
+
matches
|
|
501
|
+
};
|
|
502
|
+
logToolCall({
|
|
503
|
+
importPath: argumentsValue.importPath,
|
|
504
|
+
symbol: argumentsValue.symbol,
|
|
505
|
+
tool: name
|
|
506
|
+
});
|
|
507
|
+
return toolText(detail);
|
|
448
508
|
}
|
|
449
509
|
throw new Error(`Unknown engine MCP tool '${String(name)}'.`);
|
|
450
510
|
}
|
|
@@ -464,7 +524,7 @@ function handleLine(line, manifestFile) {
|
|
|
464
524
|
capabilities: { tools: { listChanged: false } },
|
|
465
525
|
instructions: AUTHORING_INSTRUCTIONS,
|
|
466
526
|
protocolVersion: "2025-06-18",
|
|
467
|
-
serverInfo: { name: "threenative-engine-mcp", version: "0.2.
|
|
527
|
+
serverInfo: { name: "threenative-engine-mcp", version: "0.2.4" }
|
|
468
528
|
});
|
|
469
529
|
}
|
|
470
530
|
if (request.method === "tools/list") {
|
|
@@ -476,7 +536,9 @@ function handleLine(line, manifestFile) {
|
|
|
476
536
|
}
|
|
477
537
|
return jsonRpcError(request.id, -32601, `Method not found: ${request.method}`);
|
|
478
538
|
} catch (error) {
|
|
479
|
-
|
|
539
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
540
|
+
if (request.method === "tools/call") logToolCall({ error: message, request: request.params });
|
|
541
|
+
return jsonRpcError(request.id, -32e3, message);
|
|
480
542
|
}
|
|
481
543
|
}
|
|
482
544
|
function runServer(manifestFile = defaultManifestPath()) {
|
|
@@ -498,4 +560,4 @@ if (entryPath !== void 0 && realpathSync(path.resolve(entryPath)) === realpathSy
|
|
|
498
560
|
}
|
|
499
561
|
}
|
|
500
562
|
|
|
501
|
-
export { ENGINE_MCP_TOOL_NAMES, RELEVANCE_FLOOR, capabilityDetail, capabilitySituationTokens, defaultManifestPath, handleLine, loadCapabilityManifest, runServer, searchCapabilities, toolDefinitions };
|
|
563
|
+
export { ENGINE_MCP_TOOL_NAMES, RELEVANCE_FLOOR, capabilityDetail, capabilityDetails, capabilitySituationTokens, defaultManifestPath, handleLine, loadCapabilityManifest, runServer, searchCapabilities, toolDefinitions };
|
package/mcp/servers.mjs
CHANGED
|
@@ -38,10 +38,10 @@ export const MCP_SERVERS = Object.freeze({
|
|
|
38
38
|
* separately. The pinned engine version remains an emergency fallback for a legacy development
|
|
39
39
|
* checkout whose bundle has not been built. */
|
|
40
40
|
export const MCP_PACKAGES = Object.freeze({
|
|
41
|
-
assets: Object.freeze({ name: "threenative-asset-mcp", version: "0.
|
|
41
|
+
assets: Object.freeze({ name: "threenative-asset-mcp", version: "0.9.5" }),
|
|
42
42
|
sculpt: Object.freeze({ name: "threenative-sculpt-mcp", version: "0.1.1" }),
|
|
43
|
-
engine: Object.freeze({ name: "threenative-engine-mcp", version: "0.2.
|
|
44
|
-
blender: Object.freeze({ name: "threenative-blender-mcp", version: "0.1.
|
|
43
|
+
engine: Object.freeze({ name: "threenative-engine-mcp", version: "0.2.4" }),
|
|
44
|
+
blender: Object.freeze({ name: "threenative-blender-mcp", version: "0.1.4" }),
|
|
45
45
|
});
|
|
46
46
|
|
|
47
47
|
/** How one host spells a stdio MCP server. Hosts agree on what to run and disagree on where to
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@threenative/core",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.4",
|
|
4
4
|
"license": "MIT AND GPL-2.0-or-later",
|
|
5
5
|
"description": "The small vanilla Three.js game runtime foundation.",
|
|
6
6
|
"keywords": [
|
|
@@ -76,7 +76,7 @@
|
|
|
76
76
|
"dependencies": {
|
|
77
77
|
"three": "0.185.1",
|
|
78
78
|
"three-mesh-bvh": "0.9.14",
|
|
79
|
-
"threenative-asset-mcp": "0.
|
|
79
|
+
"threenative-asset-mcp": "0.9.5",
|
|
80
80
|
"threenative-sculpt-mcp": "0.1.1",
|
|
81
81
|
"zustand": "5.0.14"
|
|
82
82
|
},
|
|
@@ -89,12 +89,12 @@
|
|
|
89
89
|
"react-reconciler": "0.33.0",
|
|
90
90
|
"tsup": "8.5.1",
|
|
91
91
|
"typescript": "5.9.3",
|
|
92
|
-
"threenative
|
|
93
|
-
"
|
|
94
|
-
"threenative-
|
|
92
|
+
"@threenative/playtest": "0.3.4",
|
|
93
|
+
"threenative-engine-mcp": "0.2.4",
|
|
94
|
+
"threenative-blender-mcp": "0.1.4"
|
|
95
95
|
},
|
|
96
96
|
"peerDependencies": {
|
|
97
|
-
"@threenative/playtest": ">=0.3.
|
|
97
|
+
"@threenative/playtest": ">=0.3.4 <0.4.0",
|
|
98
98
|
"react": ">=19.2.0 <20",
|
|
99
99
|
"react-reconciler": ">=0.33.0 <0.34"
|
|
100
100
|
},
|