scad-gltf 0.4.0 → 0.5.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 +15 -14
- package/bin/mcp.js +198 -69
- package/editor/dist/assets/{index-D-FSzMhd.js → index-b4iPmEjR.js} +204 -92
- package/editor/dist/assets/{openscad-DiJCzDqJ.wasm → openscad-DJJQxKJM.wasm} +0 -0
- package/editor/dist/assets/preview-DfaOpP2w.js +1 -0
- package/editor/dist/assets/{prompt-ui-BjZlMzjn.js → prompt-ui-zw7U8lsl.js} +4 -3
- package/editor/dist/assets/shared-CDFEFgsW.js +4315 -0
- package/editor/dist/content.js +1 -1
- package/editor/dist/index.html +7 -3
- package/editor/dist/preview.html +2 -2
- package/editor/dist/sw.js +1 -1
- package/godot/addons/scad_importer/plugin.cfg +0 -1
- package/godot/addons/scad_importer/scad_importer.gd +3 -153
- package/godot/addons/scad_importer/scad_plugin.gd +0 -3
- package/package.json +4 -4
- package/src/cli-utils.js +404 -374
- package/src/ext/openscad.js +1 -1
- package/src/ext/openscad.wasm +0 -0
- package/src/project-prompts.js +64 -73
- package/src/prompt.js +20 -0
- package/editor/dist/assets/OutputPass-C3DZe46t.js +0 -4317
- package/editor/dist/assets/preview-Br3_yFrj.js +0 -1
- package/godot/README.md +0 -64
- package/godot/addons/scad_importer/LICENSE +0 -21
package/README.md
CHANGED
|
@@ -16,6 +16,7 @@ The C++ source code for this custom OpenSCAD version is included directly in thi
|
|
|
16
16
|
- **Extended PBR Material Support:** Native extensions to the OpenSCAD `color()` module supporting `metalness`, `roughness`, `transmission` (glass), `thickness`, `ior`, `attenuationColor`, `attenuationDistance`, `clearcoat`, `sheen`, `emissive`, `specular`, and `iridescence`, plus a `$asa` special variable for auto smooth shading.
|
|
17
17
|
- **Hierarchical Node Animation:** Define articulated, rigid-body hierarchies and keyframe animations using `armature()` and `bone()` modules. Exports proper glTF node transform tracks (rigid parenting rather than vertex-weighted skinning—ideal for robots, mechanical parts, vehicles, and articulated components).
|
|
18
18
|
- **Texture Baking:** Automatically generate UVs and bake high-poly details (colors, normals, ORM) onto low-poly meshes using the new `bake()` module.
|
|
19
|
+
- **Scene Lighting:** Spawn precise light sources within your hierarchy (point, directional, spot) using the `light()` module, exporting directly to glTF.
|
|
19
20
|
- **Web Editor & Real-time Viewer (Scadify):** In-browser IDE with live WebAssembly compilation, GPU path tracing, animation timeline scrubbing, video/image export, URL sharing, and `.scad` / `.hdr` drag-and-drop.
|
|
20
21
|
- **LLM Friendly:** Includes a built-in modular prompt generator (`prompt.js` and Web UI) to help AI models (like Gemini, Claude, or ChatGPT) write compatible OpenSCAD scripts utilizing the new features.
|
|
21
22
|
- **Local API Server & Editor:** Bundled `scad-gltf serve` CLI utility to manage local `.scad` files remotely via REST API with automatic `include`/`use` dependency resolution.
|
|
@@ -260,7 +261,7 @@ Instead of generating code blindly, the AI can compile its script, render the 3D
|
|
|
260
261
|
|
|
261
262
|
**Exposed MCP Tools:**
|
|
262
263
|
|
|
263
|
-
- `
|
|
264
|
+
- `get_project_prompt`: Generates the specialized system prompt and user input request for creating full procedural 3D projects (Godot, Web, Rust, or pure SCAD).
|
|
264
265
|
- `render_scad_model`: Compiles the generated `.scad` code to GLB and returns base64 images from requested camera angles (front, back, left, right, top, bottom, isometric) and specific animation keyframes.
|
|
265
266
|
- `compile_rust_project` / `test_godot_project`: Tests the generated game projects for compilation and runtime errors.
|
|
266
267
|
|
|
@@ -283,18 +284,6 @@ If you installed the package globally, you can configure your MCP client to use
|
|
|
283
284
|
|
|
284
285
|
---
|
|
285
286
|
|
|
286
|
-
## 🎮 Godot Engine Integration
|
|
287
|
-
|
|
288
|
-
This repository includes an official **Godot 4.x Importer Addon** located in the [`/godot`](./godot) directory.
|
|
289
|
-
|
|
290
|
-
The addon allows you to drag-and-drop `.scad` files directly into your Godot project. It uses this WASM compiler under the hood to transform scripts into 3D scenes automatically.
|
|
291
|
-
|
|
292
|
-
- **Features:** Supports PBR Materials and Hierarchical Node Animations inside the Godot Editor.
|
|
293
|
-
- **License:** The Godot Addon is licensed under **MIT**.
|
|
294
|
-
- **Setup:** Simply copy the `addons/scad_importer` folder to your project and enable it in Project Settings.
|
|
295
|
-
|
|
296
|
-
---
|
|
297
|
-
|
|
298
287
|
## Automated AI Generation (`scad-gltf gen`)
|
|
299
288
|
|
|
300
289
|
By default, the `scad-gltf gen` utility copies a heavily engineered system prompt to your clipboard to paste into an LLM. However, if you provide a **Base URL** (and optional **API Key**), the CLI will fully automate this process. It will automatically spin up the `scad-gltf mcp` server in the background, connect it to your LLM, and allow the AI to _visually evaluate and fix_ its code in real-time before saving the final `generate_project.js` script to your disk.
|
|
@@ -467,6 +456,17 @@ bake(uvs=true) {
|
|
|
467
456
|
}
|
|
468
457
|
```
|
|
469
458
|
|
|
459
|
+
### 4. Scene Lighting
|
|
460
|
+
|
|
461
|
+
Add glTF lights directly into your model hierarchy:
|
|
462
|
+
|
|
463
|
+
```openscad
|
|
464
|
+
// A spot light pointing down at an angle
|
|
465
|
+
translate([0, 0, 50]) rotate([0, 30, 0])
|
|
466
|
+
light(type="spot", color=[1, 0.8, 0.8], intensity=5.0, innerConeAngle=15.0, outerConeAngle=30.0)
|
|
467
|
+
color("gray") cylinder(h=2, r=1, center=true); // A visual representation of the light fixture
|
|
468
|
+
```
|
|
469
|
+
|
|
470
470
|
---
|
|
471
471
|
|
|
472
472
|
## AI Integration (`prompt.js`)
|
|
@@ -490,6 +490,7 @@ const promptContext = generatePrompt(description, {
|
|
|
490
490
|
iridescence: true, // Thin-film interference
|
|
491
491
|
autoSmoothAngle: true, // $asa rules
|
|
492
492
|
animation: true, // Armature & bone syntax
|
|
493
|
+
lights: false, // Light rules
|
|
493
494
|
bakeColors: false, // Texture baking flags
|
|
494
495
|
bakeNormals: false,
|
|
495
496
|
bakeOrm: false,
|
|
@@ -505,7 +506,7 @@ console.log(promptContext);
|
|
|
505
506
|
|
|
506
507
|
Once connected, an AI assistant can use the server to execute the following loop:
|
|
507
508
|
|
|
508
|
-
1. **Retrieve Syntax Rules:** The assistant calls the `
|
|
509
|
+
1. **Retrieve Syntax Rules:** The assistant calls the `get_project_prompt` tool to get the extended syntax rules and project structure guidelines.
|
|
509
510
|
2. **Generate Code:** The assistant writes the `.scad` script based on your design request.
|
|
510
511
|
3. **Compile & Visually Inspect:** The assistant calls the `render_scad_model` tool to inspect rendered multi-angle frames and keyframes, fixing any errors before final export.
|
|
511
512
|
|
package/bin/mcp.js
CHANGED
|
@@ -13,7 +13,9 @@ import {
|
|
|
13
13
|
import puppeteer from "puppeteer";
|
|
14
14
|
import { convertScadToGltf } from "../src/convert.js";
|
|
15
15
|
import { generatePrompt } from "../src/prompt.js";
|
|
16
|
+
import { getProjectPrompts } from "../src/project-prompts.js";
|
|
16
17
|
import { runGodotAsync } from "../src/godot-utils.js";
|
|
18
|
+
import { generateScadPreviewUrl } from "../src/cli-utils.js";
|
|
17
19
|
|
|
18
20
|
const __filename = fileURLToPath(import.meta.url);
|
|
19
21
|
const __dirname = path.dirname(__filename);
|
|
@@ -55,15 +57,22 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
55
57
|
return {
|
|
56
58
|
tools: [
|
|
57
59
|
{
|
|
58
|
-
name: "
|
|
60
|
+
name: "get_project_prompt",
|
|
59
61
|
description:
|
|
60
|
-
"Generates the specialized
|
|
62
|
+
"Generates the specialized system prompt and user input request for creating full procedural 3D projects (Godot, Web, Rust, or pure SCAD). This provides the syntax rules and structure required to generate the project.",
|
|
61
63
|
inputSchema: {
|
|
62
64
|
type: "object",
|
|
63
65
|
properties: {
|
|
66
|
+
project_type: {
|
|
67
|
+
type: "string",
|
|
68
|
+
enum: ["scad", "bevy", "wgpu", "web", "godot"],
|
|
69
|
+
description:
|
|
70
|
+
"The target framework or output type for the project.",
|
|
71
|
+
},
|
|
64
72
|
description: {
|
|
65
73
|
type: "string",
|
|
66
|
-
description:
|
|
74
|
+
description:
|
|
75
|
+
"The description of the project or object you want to design.",
|
|
67
76
|
},
|
|
68
77
|
options: {
|
|
69
78
|
type: "object",
|
|
@@ -78,17 +87,17 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
78
87
|
transmission: {
|
|
79
88
|
type: "boolean",
|
|
80
89
|
description:
|
|
81
|
-
"Include rules for transparent/glass volumes
|
|
90
|
+
"Include rules for transparent/glass volumes. (Default: false)",
|
|
82
91
|
},
|
|
83
92
|
clearcoat: {
|
|
84
93
|
type: "boolean",
|
|
85
94
|
description:
|
|
86
|
-
"Include rules for clearcoat and clearcoatRoughness. (Default:
|
|
95
|
+
"Include rules for clearcoat and clearcoatRoughness. (Default: false)",
|
|
87
96
|
},
|
|
88
97
|
sheen: {
|
|
89
98
|
type: "boolean",
|
|
90
99
|
description:
|
|
91
|
-
"Include rules for cloth/velvet sheen
|
|
100
|
+
"Include rules for cloth/velvet sheen. (Default: false)",
|
|
92
101
|
},
|
|
93
102
|
emissive: {
|
|
94
103
|
type: "boolean",
|
|
@@ -98,12 +107,12 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
98
107
|
specular: {
|
|
99
108
|
type: "boolean",
|
|
100
109
|
description:
|
|
101
|
-
"Include rules for specular reflections
|
|
110
|
+
"Include rules for specular reflections. (Default: true)",
|
|
102
111
|
},
|
|
103
112
|
iridescence: {
|
|
104
113
|
type: "boolean",
|
|
105
114
|
description:
|
|
106
|
-
"Include rules for thin-film interference
|
|
115
|
+
"Include rules for thin-film interference. (Default: false)",
|
|
107
116
|
},
|
|
108
117
|
autoSmoothAngle: {
|
|
109
118
|
type: "boolean",
|
|
@@ -140,21 +149,27 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
140
149
|
description:
|
|
141
150
|
"Include rules for the lazy-union compiler optimization. (Default: false)",
|
|
142
151
|
},
|
|
143
|
-
|
|
152
|
+
lights: {
|
|
144
153
|
type: "boolean",
|
|
145
154
|
description:
|
|
146
|
-
"Include
|
|
155
|
+
"Include rules for adding scene lights. (Default: false)",
|
|
156
|
+
},
|
|
157
|
+
scadFiles: {
|
|
158
|
+
type: "array",
|
|
159
|
+
items: { type: "string" },
|
|
160
|
+
description:
|
|
161
|
+
"Array of file paths to existing .scad files to embed as context.",
|
|
147
162
|
},
|
|
148
163
|
},
|
|
149
164
|
},
|
|
150
165
|
},
|
|
151
|
-
required: ["description"],
|
|
166
|
+
required: ["project_type", "description"],
|
|
152
167
|
},
|
|
153
168
|
},
|
|
154
169
|
{
|
|
155
170
|
name: "render_scad_model",
|
|
156
171
|
description:
|
|
157
|
-
"Converts OpenSCAD code to GLTF and uses a 3D renderer to capture images from requested camera angles. Analyze these returned images to verify your design
|
|
172
|
+
"Converts OpenSCAD code to GLTF and uses a 3D renderer to capture images from requested camera angles. Analyze these returned images to verify your design. The tool response will also include a GitHub-hosted Live Editor Preview URL—you MUST share this URL with the user so they can view the model interactively.",
|
|
158
173
|
inputSchema: {
|
|
159
174
|
type: "object",
|
|
160
175
|
properties: {
|
|
@@ -257,23 +272,54 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
257
272
|
const { name, arguments: args } = request.params;
|
|
258
273
|
|
|
259
274
|
// ------------------------------------------
|
|
260
|
-
// TOOL 1:
|
|
275
|
+
// TOOL 1: get_project_prompt
|
|
261
276
|
// ------------------------------------------
|
|
262
|
-
if (name === "
|
|
277
|
+
if (name === "get_project_prompt") {
|
|
263
278
|
try {
|
|
264
|
-
const
|
|
279
|
+
const pType = args.project_type;
|
|
280
|
+
const desc = args.description;
|
|
281
|
+
const opts = {
|
|
282
|
+
transmission: false,
|
|
283
|
+
clearcoat: false,
|
|
284
|
+
sheen: false,
|
|
285
|
+
iridescence: false,
|
|
286
|
+
...(args.options || {}),
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
if (!["scad", "bevy", "wgpu", "web", "godot"].includes(pType)) {
|
|
290
|
+
throw new Error(`Invalid project type: ${pType}`);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (pType !== "scad") {
|
|
294
|
+
opts.modelName = false;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const promptRules = generatePrompt(
|
|
298
|
+
pType === "scad" ? desc : "the 3D assets for the project",
|
|
299
|
+
opts,
|
|
300
|
+
);
|
|
301
|
+
|
|
302
|
+
const prompts = getProjectPrompts(pType);
|
|
303
|
+
const systemPrompt = prompts.buildSystemPrompt(promptRules, opts);
|
|
304
|
+
const userRequest = prompts.buildInputRequest
|
|
305
|
+
? prompts.buildInputRequest(desc, promptRules)
|
|
306
|
+
: promptRules;
|
|
307
|
+
|
|
265
308
|
return {
|
|
266
309
|
content: [
|
|
267
310
|
{
|
|
268
311
|
type: "text",
|
|
269
|
-
text:
|
|
312
|
+
text: `=== SYSTEM PROMPT ===\n${systemPrompt}\n\n=== USER REQUEST ===\n${userRequest}`,
|
|
270
313
|
},
|
|
271
314
|
],
|
|
272
315
|
};
|
|
273
316
|
} catch (error) {
|
|
274
317
|
return {
|
|
275
318
|
content: [
|
|
276
|
-
{
|
|
319
|
+
{
|
|
320
|
+
type: "text",
|
|
321
|
+
text: `Error generating project prompt: ${error.message}`,
|
|
322
|
+
},
|
|
277
323
|
],
|
|
278
324
|
isError: true,
|
|
279
325
|
};
|
|
@@ -295,7 +341,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
295
341
|
const animIndex = args.animation_index || 0;
|
|
296
342
|
const returnImages = args.return_images !== false;
|
|
297
343
|
|
|
298
|
-
// 1.
|
|
344
|
+
// 1. Generate the public GitHub Scadify URL
|
|
345
|
+
const previewUrl =
|
|
346
|
+
generateScadPreviewUrl(
|
|
347
|
+
scadCode,
|
|
348
|
+
"https://iliagrigorevdev.github.io/scad-gltf",
|
|
349
|
+
) || "Failed to generate URL";
|
|
350
|
+
|
|
351
|
+
// 2. Convert SCAD to GLB ArrayBuffer
|
|
299
352
|
const glbDataArray = await convertScadToGltf(scadCode, {
|
|
300
353
|
wasmUrl: `file://${wasmPath}`,
|
|
301
354
|
});
|
|
@@ -305,7 +358,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
305
358
|
content: [
|
|
306
359
|
{
|
|
307
360
|
type: "text",
|
|
308
|
-
text: `Successfully compiled SCAD without syntax errors. (Visual rendering skipped because return_images is false)
|
|
361
|
+
text: `Successfully compiled SCAD without syntax errors. (Visual rendering skipped because return_images is false).\n\nLive Editor Preview URL: ${previewUrl}`,
|
|
309
362
|
},
|
|
310
363
|
],
|
|
311
364
|
};
|
|
@@ -330,8 +383,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
330
383
|
<script type="importmap">
|
|
331
384
|
{
|
|
332
385
|
"imports": {
|
|
333
|
-
"three": "https://unpkg.com/three@0.
|
|
334
|
-
"three/addons/": "https://unpkg.com/three@0.
|
|
386
|
+
"three": "https://unpkg.com/three@0.186.1/build/three.module.js",
|
|
387
|
+
"three/addons/": "https://unpkg.com/three@0.186.1/examples/jsm/"
|
|
335
388
|
}
|
|
336
389
|
}
|
|
337
390
|
</script>
|
|
@@ -416,6 +469,58 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
416
469
|
const gltf = await loader.loadAsync(dataUrl);
|
|
417
470
|
scene.add(gltf.scene);
|
|
418
471
|
|
|
472
|
+
// Detect scene lights to match editor behavior
|
|
473
|
+
let sceneHasLights = false;
|
|
474
|
+
let maxDir = 0;
|
|
475
|
+
let maxPt = 0;
|
|
476
|
+
|
|
477
|
+
gltf.scene.traverse((child) => {
|
|
478
|
+
if (child.isMesh) {
|
|
479
|
+
child.frustumCulled = false;
|
|
480
|
+
}
|
|
481
|
+
if (child.isLight) {
|
|
482
|
+
sceneHasLights = true;
|
|
483
|
+
child.castShadow = true;
|
|
484
|
+
if (child.isDirectionalLight) {
|
|
485
|
+
maxDir = Math.max(maxDir, child.intensity);
|
|
486
|
+
} else {
|
|
487
|
+
maxPt = Math.max(maxPt, child.intensity);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
if (sceneHasLights) {
|
|
493
|
+
renderer.shadowMap.enabled = true;
|
|
494
|
+
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
|
|
495
|
+
|
|
496
|
+
gltf.scene.traverse((child) => {
|
|
497
|
+
if (child.isMesh) {
|
|
498
|
+
child.castShadow = true;
|
|
499
|
+
child.receiveShadow = true;
|
|
500
|
+
}
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
let optimalExposure = 1.0;
|
|
504
|
+
const expDir = maxDir > 0 ? 3.0 / maxDir : 1.0;
|
|
505
|
+
const expPt = maxPt > 0 ? 3000.0 / maxPt : 1.0;
|
|
506
|
+
|
|
507
|
+
if (maxDir > 0 && maxPt > 0)
|
|
508
|
+
optimalExposure = Math.min(expDir, expPt);
|
|
509
|
+
else if (maxDir > 0) optimalExposure = expDir;
|
|
510
|
+
else if (maxPt > 0) optimalExposure = expPt;
|
|
511
|
+
|
|
512
|
+
optimalExposure = Math.max(0.00001, Math.min(optimalExposure, 5.0));
|
|
513
|
+
|
|
514
|
+
renderer.toneMappingExposure = optimalExposure;
|
|
515
|
+
scene.environmentIntensity = 0.0;
|
|
516
|
+
|
|
517
|
+
ambientLight.visible = false;
|
|
518
|
+
hemiLight.visible = false;
|
|
519
|
+
keyLight.visible = false;
|
|
520
|
+
fillLight.visible = false;
|
|
521
|
+
rimLight.visible = false;
|
|
522
|
+
}
|
|
523
|
+
|
|
419
524
|
// --- Apply Animation State if requested ---
|
|
420
525
|
let appliedAnim = false;
|
|
421
526
|
if (gltf.animations && gltf.animations.length > 0) {
|
|
@@ -477,6 +582,35 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
477
582
|
vertices.push(center.x, center.y, center.z);
|
|
478
583
|
}
|
|
479
584
|
|
|
585
|
+
// Dynamically scale shadow biases and camera bounds based on actual model size
|
|
586
|
+
const shadowCamSize = initialDim * 1.5;
|
|
587
|
+
const shadowScale = initialDim / 10.0;
|
|
588
|
+
|
|
589
|
+
if (sceneHasLights) {
|
|
590
|
+
gltf.scene.traverse((child) => {
|
|
591
|
+
if (child.isLight && child.shadow) {
|
|
592
|
+
child.shadow.bias = -0.0005 * shadowScale;
|
|
593
|
+
child.shadow.normalBias = 0.02 * shadowScale;
|
|
594
|
+
|
|
595
|
+
if (child.isDirectionalLight) {
|
|
596
|
+
child.shadow.camera.left = -shadowCamSize;
|
|
597
|
+
child.shadow.camera.right = shadowCamSize;
|
|
598
|
+
child.shadow.camera.top = shadowCamSize;
|
|
599
|
+
child.shadow.camera.bottom = -shadowCamSize;
|
|
600
|
+
child.shadow.camera.near = Math.max(0.01, initialDim * 0.01);
|
|
601
|
+
child.shadow.camera.far = shadowCamSize * 4;
|
|
602
|
+
child.shadow.camera.updateProjectionMatrix();
|
|
603
|
+
child.shadow.mapSize.width = 2048;
|
|
604
|
+
child.shadow.mapSize.height = 2048;
|
|
605
|
+
} else if (child.isPointLight || child.isSpotLight) {
|
|
606
|
+
child.shadow.camera.near = Math.max(0.01, initialDim * 0.01);
|
|
607
|
+
child.shadow.camera.far = shadowCamSize * 4;
|
|
608
|
+
child.shadow.camera.updateProjectionMatrix();
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
});
|
|
612
|
+
}
|
|
613
|
+
|
|
480
614
|
const fovY = (camera.fov * Math.PI) / 180;
|
|
481
615
|
const tanY = Math.tan(fovY / 2);
|
|
482
616
|
const tanX = tanY * camera.aspect;
|
|
@@ -611,7 +745,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
611
745
|
const content = [
|
|
612
746
|
{
|
|
613
747
|
type: "text",
|
|
614
|
-
text: `Successfully compiled SCAD and rendered ${snapshots.length} camera angle(s)${appliedAnimStr}. Please analyze these visual results to determine your next adjustments
|
|
748
|
+
text: `Successfully compiled SCAD and rendered ${snapshots.length} camera angle(s)${appliedAnimStr}. Please analyze these visual results to determine your next adjustments.\n\nLive Editor Preview URL: ${previewUrl}`,
|
|
615
749
|
},
|
|
616
750
|
];
|
|
617
751
|
|
|
@@ -746,16 +880,15 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
746
880
|
const originalProjectGodot = fs.readFileSync(projectGodotPath, "utf8");
|
|
747
881
|
let playResult;
|
|
748
882
|
|
|
883
|
+
// 1.5 Prepare MCP Runner Autoload Script (ALWAYS inject to ensure a clean exit)
|
|
884
|
+
const snapTime = Math.max(0.5, runTime - 0.5);
|
|
885
|
+
|
|
886
|
+
let runnerScript = `extends Node\nfunc _ready():\n\tprint("[MCP] Autoload ready. Waiting ${snapTime} seconds...")\n\tawait get_tree().create_timer(${snapTime}).timeout\n`;
|
|
887
|
+
|
|
888
|
+
// If returning images, inject the screenshot capture logic
|
|
749
889
|
if (returnImages) {
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
const screenshotScript = `extends Node
|
|
754
|
-
func _ready():
|
|
755
|
-
\tprint("[ScreenshotMCP] Autoload ready. Waiting ${snapTime} seconds...")
|
|
756
|
-
\tawait get_tree().create_timer(${snapTime}).timeout
|
|
757
|
-
\tvar ds = DisplayServer.get_name()
|
|
758
|
-
\tprint("[ScreenshotMCP] DisplayServer is: ", ds)
|
|
890
|
+
runnerScript += `\tvar ds = DisplayServer.get_name()
|
|
891
|
+
\tprint("[MCP] DisplayServer is: ", ds)
|
|
759
892
|
\tif ds != "headless":
|
|
760
893
|
\t\tawait RenderingServer.frame_post_draw
|
|
761
894
|
\t\tvar tex = get_viewport().get_texture()
|
|
@@ -770,34 +903,37 @@ func _ready():
|
|
|
770
903
|
\t\t\t\tvar cropped = img.get_region(Rect2i(x, y, size, size))
|
|
771
904
|
\t\t\t\tvar path = ProjectSettings.globalize_path("res://screenshot_mcp.png")
|
|
772
905
|
\t\t\t\tvar err = cropped.save_png(path)
|
|
773
|
-
\t\t\t\tprint("[
|
|
906
|
+
\t\t\t\tprint("[MCP] Save PNG to ", path, " returned error code: ", err)
|
|
774
907
|
\t\t\telse:
|
|
775
|
-
\t\t\t\tprint("[
|
|
908
|
+
\t\t\t\tprint("[MCP] Error: Image is null or empty.")
|
|
776
909
|
\t\telse:
|
|
777
|
-
\t\t\tprint("[
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
910
|
+
\t\t\tprint("[MCP] Error: Viewport texture is null.")\n`;
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
// Always force Godot to quit gracefully from the inside
|
|
914
|
+
runnerScript += `\tget_tree().quit()\n`;
|
|
915
|
+
|
|
916
|
+
fs.writeFileSync(path.join(projectDir, "mcp_runner.gd"), runnerScript);
|
|
917
|
+
|
|
918
|
+
let tempProjectGodot = originalProjectGodot;
|
|
919
|
+
if (tempProjectGodot.includes("[autoload]")) {
|
|
920
|
+
tempProjectGodot = tempProjectGodot.replace(
|
|
921
|
+
"[autoload]",
|
|
922
|
+
'[autoload]\nMCPRunner="*res://mcp_runner.gd"',
|
|
783
923
|
);
|
|
924
|
+
} else {
|
|
925
|
+
tempProjectGodot += `\n[autoload]\nMCPRunner="*res://mcp_runner.gd"\n`;
|
|
926
|
+
}
|
|
927
|
+
fs.writeFileSync(projectGodotPath, tempProjectGodot, "utf8");
|
|
784
928
|
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
"[autoload]",
|
|
789
|
-
'[autoload]\nScreenshotMCP="*res://screenshot_mcp.gd"',
|
|
790
|
-
);
|
|
791
|
-
} else {
|
|
792
|
-
tempProjectGodot += `\n[autoload]\nScreenshotMCP="*res://screenshot_mcp.gd"\n`;
|
|
793
|
-
}
|
|
794
|
-
fs.writeFileSync(projectGodotPath, tempProjectGodot, "utf8");
|
|
929
|
+
// 2. Run Project Step
|
|
930
|
+
// Give the Node wrapper a +5000ms buffer so the internal Godot timer always finishes first
|
|
931
|
+
const safeNodeTimeout = runTime * 1000 + 5000;
|
|
795
932
|
|
|
796
|
-
|
|
797
|
-
//
|
|
798
|
-
// If the environment lacks a display server, we'll catch the error and fallback.
|
|
933
|
+
if (returnImages) {
|
|
934
|
+
// Windowed first to get the screenshot
|
|
799
935
|
let playArgs = [
|
|
800
|
-
"--windowed",
|
|
936
|
+
"--windowed",
|
|
801
937
|
"--resolution",
|
|
802
938
|
"512x512",
|
|
803
939
|
"--audio-driver",
|
|
@@ -806,13 +942,7 @@ func _ready():
|
|
|
806
942
|
projectDir,
|
|
807
943
|
];
|
|
808
944
|
|
|
809
|
-
playResult = await runGodotAsync(
|
|
810
|
-
playArgs,
|
|
811
|
-
projectDir,
|
|
812
|
-
// Give Godot extra buffer time to boot, take the shot, and quit gracefully
|
|
813
|
-
runTime * 1000 + 5000,
|
|
814
|
-
);
|
|
815
|
-
|
|
945
|
+
playResult = await runGodotAsync(playArgs, projectDir, safeNodeTimeout);
|
|
816
946
|
let windowedOutput = playResult.output;
|
|
817
947
|
|
|
818
948
|
// Fallback if windowed mode fails due to display server issues
|
|
@@ -824,30 +954,29 @@ func _ready():
|
|
|
824
954
|
playResult = await runGodotAsync(
|
|
825
955
|
["--headless", "--audio-driver", "Dummy", "--path", projectDir],
|
|
826
956
|
projectDir,
|
|
827
|
-
|
|
957
|
+
safeNodeTimeout,
|
|
828
958
|
);
|
|
829
|
-
// Explicitly inject the display error into the final output so we aren't flying blind!
|
|
830
959
|
playResult.output =
|
|
831
960
|
"--- WINDOWED LAUNCH FAILED (Display Error) ---\n" +
|
|
832
961
|
windowedOutput.trim() +
|
|
833
962
|
"\n\n--- FALLBACK HEADLESS LAUNCH ---\n" +
|
|
834
963
|
playResult.output;
|
|
835
964
|
}
|
|
836
|
-
|
|
837
|
-
// 3. Cleanup Autoloads
|
|
838
|
-
fs.writeFileSync(projectGodotPath, originalProjectGodot, "utf8");
|
|
839
|
-
if (fs.existsSync(path.join(projectDir, "screenshot_mcp.gd"))) {
|
|
840
|
-
fs.unlinkSync(path.join(projectDir, "screenshot_mcp.gd"));
|
|
841
|
-
}
|
|
842
965
|
} else {
|
|
843
966
|
// Text-only mode: Run headless immediately
|
|
844
967
|
playResult = await runGodotAsync(
|
|
845
968
|
["--headless", "--audio-driver", "Dummy", "--path", projectDir],
|
|
846
969
|
projectDir,
|
|
847
|
-
|
|
970
|
+
safeNodeTimeout,
|
|
848
971
|
);
|
|
849
972
|
}
|
|
850
973
|
|
|
974
|
+
// 3. Cleanup Autoloads
|
|
975
|
+
fs.writeFileSync(projectGodotPath, originalProjectGodot, "utf8");
|
|
976
|
+
if (fs.existsSync(path.join(projectDir, "mcp_runner.gd"))) {
|
|
977
|
+
fs.unlinkSync(path.join(projectDir, "mcp_runner.gd"));
|
|
978
|
+
}
|
|
979
|
+
|
|
851
980
|
// 4. Read Screenshot
|
|
852
981
|
let screenshotBase64 = null;
|
|
853
982
|
if (returnImages) {
|