scad-gltf 0.5.1 ā 0.6.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/README.md +28 -7
- package/bin/extract.js +54 -0
- package/bin/mcp.js +89 -135
- package/bin/play.js +41 -10
- package/bin/scad-gltf.js +7 -1
- package/editor/dist/content.js +1 -1
- package/editor/dist/sw.js +1 -1
- package/godot/addons/scad_importer/plugin.cfg +1 -1
- package/package.json +3 -2
- package/src/cli-utils.js +75 -22
- package/src/godot-utils.js +43 -25
- package/src/project-prompts.js +91 -75
package/README.md
CHANGED
|
@@ -8,6 +8,8 @@ The C++ source code for this custom OpenSCAD version is included directly in thi
|
|
|
8
8
|
|
|
9
9
|
**⨠Launch Scadify:** Open the web editor and real-time 3D viewer: [https://iliagrigorevdev.github.io/scad-gltf/](https://iliagrigorevdev.github.io/scad-gltf/)
|
|
10
10
|
|
|
11
|
+
**š® Godot AI Generated Examples:** Play AI-generated Godot examples in your browser: [https://iliagrigorevdev.github.io/scad-godot/](https://iliagrigorevdev.github.io/scad-godot/)
|
|
12
|
+
|
|
11
13
|

|
|
12
14
|
|
|
13
15
|
## Features
|
|
@@ -25,7 +27,8 @@ The C++ source code for this custom OpenSCAD version is included directly in thi
|
|
|
25
27
|
- **AI Studio Extension:** Chrome extension to natively preview, prompt, take chat snapshots, open in Scadify, and locally save AI-generated 3D models directly inside Google AI Studio.
|
|
26
28
|
- **Godot 4, Rust Bevy & Web Integration:** Native Godot 4 importer addon and Rust Bevy compile-time `build.rs` integration.
|
|
27
29
|
- **Automated AI Generation:** Pass an OpenAI-compatible base URL (and optional API key) to the `scad-gltf gen` CLI tool to automatically spawn a local MCP server, query the LLM via standard endpoints, allow it to visually iterate, and output a ready-to-run game project or raw 3D asset script.
|
|
28
|
-
- **Project Runner:** Bundled `scad-gltf play` CLI utility to safely unpack, build, and launch generated
|
|
30
|
+
- **Project Runner:** Bundled `scad-gltf play` CLI utility to safely unpack, build, and launch generated Markdown projects in isolated temporary environments.
|
|
31
|
+
- **Project Extractor:** Bundled `scad-gltf extract` CLI utility to permanently unpack AI-generated Markdown projects to your local filesystem.
|
|
29
32
|
|
|
30
33
|
---
|
|
31
34
|
|
|
@@ -286,7 +289,7 @@ If you installed the package globally, you can configure your MCP client to use
|
|
|
286
289
|
|
|
287
290
|
## Automated AI Generation (`scad-gltf gen`)
|
|
288
291
|
|
|
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.
|
|
292
|
+
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.md` file to your disk.
|
|
290
293
|
|
|
291
294
|
**Supported Generation Types:**
|
|
292
295
|
|
|
@@ -341,25 +344,43 @@ scad-gltf gen godot "A game using the provided player model" '{"scadFiles": ["./
|
|
|
341
344
|
_Once the automated process completes for full game projects, you can test it instantly using the `play` command:_
|
|
342
345
|
|
|
343
346
|
```bash
|
|
344
|
-
scad-gltf play generate_godot_project.
|
|
347
|
+
scad-gltf play generate_godot_project.md
|
|
345
348
|
```
|
|
346
349
|
|
|
347
|
-
_Alternatively, to
|
|
350
|
+
_Alternatively, to permanently unpack the project to your local filesystem, use the `extract` command:_
|
|
348
351
|
|
|
349
352
|
```bash
|
|
350
|
-
|
|
353
|
+
scad-gltf extract generate_godot_project.md
|
|
351
354
|
```
|
|
352
355
|
|
|
353
356
|
---
|
|
354
357
|
|
|
355
358
|
## Testing Generated Projects (`scad-gltf play`)
|
|
356
359
|
|
|
357
|
-
The `scad-gltf play` utility is designed to quickly test the single-file
|
|
360
|
+
The `scad-gltf play` utility is designed to quickly test the single-file Markdown projects produced by the `gen` command. Instead of cluttering your working directory, it unpacks the project into a temporary folder, automatically installs dependencies or builds the project (Godot, Rust Bevy, or Vite), and launches it. Once you close the application, the temporary folder is automatically cleaned up.
|
|
361
|
+
|
|
362
|
+
**Usage:**
|
|
363
|
+
|
|
364
|
+
```bash
|
|
365
|
+
scad-gltf play <path_to_generated_project.md>
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
---
|
|
369
|
+
|
|
370
|
+
## Extracting Generated Projects (`scad-gltf extract`)
|
|
371
|
+
|
|
372
|
+
When you are satisfied with a generated project and want to continue developing it yourself, use the `scad-gltf extract` command to permanently unpack the Markdown file to your disk.
|
|
373
|
+
|
|
374
|
+
It reads the `### filepath` headers and writes the code blocks to their respective directories.
|
|
358
375
|
|
|
359
376
|
**Usage:**
|
|
360
377
|
|
|
361
378
|
```bash
|
|
362
|
-
|
|
379
|
+
# Extract files into the current working directory
|
|
380
|
+
scad-gltf extract <path_to_generated_project.md>
|
|
381
|
+
|
|
382
|
+
# Extract files into a specific directory
|
|
383
|
+
scad-gltf extract <path_to_generated_project.md> ./my_new_game
|
|
363
384
|
```
|
|
364
385
|
|
|
365
386
|
---
|
package/bin/extract.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import {
|
|
6
|
+
extractFilesFromMarkdown,
|
|
7
|
+
writeExtractedFiles,
|
|
8
|
+
} from "../src/cli-utils.js";
|
|
9
|
+
|
|
10
|
+
const args = process.argv.slice(2);
|
|
11
|
+
const mdPath = args[0];
|
|
12
|
+
const outDir = args[1] || process.cwd();
|
|
13
|
+
|
|
14
|
+
if (!mdPath) {
|
|
15
|
+
console.error(
|
|
16
|
+
"ā Error: Path to the generated Markdown project file is required.",
|
|
17
|
+
);
|
|
18
|
+
console.error("Usage: scad-gltf extract <project.md> [output_directory]");
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const absMdPath = path.resolve(process.cwd(), mdPath);
|
|
23
|
+
if (!fs.existsSync(absMdPath)) {
|
|
24
|
+
console.error(`ā Error: File not found: ${absMdPath}`);
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
console.log(
|
|
30
|
+
`š¦ Extracting project files from ${path.basename(absMdPath)}...`,
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
const mdContent = fs.readFileSync(absMdPath, "utf-8");
|
|
34
|
+
const files = extractFilesFromMarkdown(mdContent);
|
|
35
|
+
const fileCount = Object.keys(files).length;
|
|
36
|
+
|
|
37
|
+
if (fileCount === 0) {
|
|
38
|
+
console.error("\nā Error: No files could be extracted.");
|
|
39
|
+
console.error(
|
|
40
|
+
"Make sure the Markdown uses '### filepath' headers followed by code blocks.",
|
|
41
|
+
);
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const absOutDir = path.resolve(process.cwd(), outDir);
|
|
46
|
+
writeExtractedFiles(files, absOutDir);
|
|
47
|
+
|
|
48
|
+
console.log(
|
|
49
|
+
`\nā
Successfully extracted ${fileCount} file(s) to:\n ${absOutDir}`,
|
|
50
|
+
);
|
|
51
|
+
} catch (err) {
|
|
52
|
+
console.error(`\nā Extraction failed: ${err.message}`);
|
|
53
|
+
process.exit(1);
|
|
54
|
+
}
|
package/bin/mcp.js
CHANGED
|
@@ -15,7 +15,11 @@ import { convertScadToGltf } from "../src/convert.js";
|
|
|
15
15
|
import { generatePrompt } from "../src/prompt.js";
|
|
16
16
|
import { getProjectPrompts } from "../src/project-prompts.js";
|
|
17
17
|
import { runGodotAsync } from "../src/godot-utils.js";
|
|
18
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
generateScadPreviewUrl,
|
|
20
|
+
extractFilesFromMarkdown,
|
|
21
|
+
writeExtractedFiles,
|
|
22
|
+
} from "../src/cli-utils.js";
|
|
19
23
|
|
|
20
24
|
const __filename = fileURLToPath(import.meta.url);
|
|
21
25
|
const __dirname = path.dirname(__filename);
|
|
@@ -43,7 +47,7 @@ global.fetch = async (url, options) => {
|
|
|
43
47
|
const server = new Server(
|
|
44
48
|
{
|
|
45
49
|
name: "scad-mcp-server",
|
|
46
|
-
version: "1.
|
|
50
|
+
version: "1.4.0",
|
|
47
51
|
},
|
|
48
52
|
{
|
|
49
53
|
capabilities: {
|
|
@@ -216,19 +220,14 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
216
220
|
{
|
|
217
221
|
name: "test_godot_project",
|
|
218
222
|
description:
|
|
219
|
-
"
|
|
223
|
+
"Extracts a Godot project from a Markdown format and runs it for a short duration to detect script errors, missing resources, or runtime crashes. Returns the console output logs and a visual screenshot.",
|
|
220
224
|
inputSchema: {
|
|
221
225
|
type: "object",
|
|
222
226
|
properties: {
|
|
223
|
-
|
|
224
|
-
type: "string",
|
|
225
|
-
description:
|
|
226
|
-
"The relative or absolute path to the directory containing project.godot. Use this if the project is already extracted on disk.",
|
|
227
|
-
},
|
|
228
|
-
nodejs_script: {
|
|
227
|
+
project_markdown: {
|
|
229
228
|
type: "string",
|
|
230
229
|
description:
|
|
231
|
-
"The complete
|
|
230
|
+
"The complete markdown string containing all project files formatted with '### filepath' headers and code blocks.",
|
|
232
231
|
},
|
|
233
232
|
run_time: {
|
|
234
233
|
type: "number",
|
|
@@ -238,29 +237,26 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
238
237
|
return_images: {
|
|
239
238
|
type: "boolean",
|
|
240
239
|
description:
|
|
241
|
-
"Capture a screenshot of the project. Set to false for text-only logs
|
|
240
|
+
"Capture a screenshot of the project. Set to false for text-only logs. Default is true.",
|
|
242
241
|
},
|
|
243
242
|
},
|
|
243
|
+
required: ["project_markdown"],
|
|
244
244
|
},
|
|
245
245
|
},
|
|
246
246
|
{
|
|
247
247
|
name: "compile_rust_project",
|
|
248
248
|
description:
|
|
249
|
-
"
|
|
249
|
+
"Extracts a Rust project from a Markdown format and compiles it to detect syntax errors, missing dependencies, or compilation failures. Returns the cargo console output logs.",
|
|
250
250
|
inputSchema: {
|
|
251
251
|
type: "object",
|
|
252
252
|
properties: {
|
|
253
|
-
|
|
253
|
+
project_markdown: {
|
|
254
254
|
type: "string",
|
|
255
255
|
description:
|
|
256
|
-
"The
|
|
257
|
-
},
|
|
258
|
-
nodejs_script: {
|
|
259
|
-
type: "string",
|
|
260
|
-
description:
|
|
261
|
-
"The complete self-contained Node.js script that generates the Rust project. If provided, the tool will execute this script in a temporary directory to extract the project before compiling it. Use this during the generation phase to test your code before providing the final answer.",
|
|
256
|
+
"The complete markdown string containing all project files formatted with '### filepath' headers and code blocks.",
|
|
262
257
|
},
|
|
263
258
|
},
|
|
259
|
+
required: ["project_markdown"],
|
|
264
260
|
},
|
|
265
261
|
},
|
|
266
262
|
],
|
|
@@ -786,81 +782,60 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
786
782
|
try {
|
|
787
783
|
let projectDir = null;
|
|
788
784
|
const runTime = args.run_time || 5.0;
|
|
785
|
+
const markdown = args.project_markdown;
|
|
789
786
|
|
|
790
|
-
if (
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
execSync(`node generate.js`, {
|
|
802
|
-
cwd: tempBase,
|
|
803
|
-
stdio: "pipe",
|
|
804
|
-
timeout: 15000, // Time out after 15 seconds if it hangs
|
|
805
|
-
});
|
|
806
|
-
} catch (err) {
|
|
807
|
-
const stderr = err.stderr ? err.stderr.toString() : "";
|
|
808
|
-
const stdout = err.stdout ? err.stdout.toString() : "";
|
|
809
|
-
return {
|
|
810
|
-
content: [
|
|
811
|
-
{
|
|
812
|
-
type: "text",
|
|
813
|
-
text: `Error: The Node.js script failed to execute properly.\n\nSTDERR:\n${stderr}\n\nSTDOUT:\n${stdout}\n\nException: ${err.message}`,
|
|
814
|
-
},
|
|
815
|
-
],
|
|
816
|
-
isError: true,
|
|
817
|
-
};
|
|
818
|
-
}
|
|
787
|
+
if (!markdown) {
|
|
788
|
+
return {
|
|
789
|
+
content: [
|
|
790
|
+
{
|
|
791
|
+
type: "text",
|
|
792
|
+
text: `Error: 'project_markdown' is required.`,
|
|
793
|
+
},
|
|
794
|
+
],
|
|
795
|
+
isError: true,
|
|
796
|
+
};
|
|
797
|
+
}
|
|
819
798
|
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
fs.statSync(itemPath).isDirectory() &&
|
|
826
|
-
fs.existsSync(path.join(itemPath, "project.godot"))
|
|
827
|
-
) {
|
|
828
|
-
projectDir = itemPath;
|
|
829
|
-
break;
|
|
830
|
-
}
|
|
831
|
-
}
|
|
799
|
+
// Create a unique temporary directory
|
|
800
|
+
const tempBase = fs.mkdtempSync(
|
|
801
|
+
path.join(os.tmpdir(), "scad-godot-test-"),
|
|
802
|
+
);
|
|
803
|
+
cleanupDir = tempBase;
|
|
832
804
|
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
content: [
|
|
836
|
-
{
|
|
837
|
-
type: "text",
|
|
838
|
-
text: `Error: The Node.js script finished running, but NO Godot project was found. A valid Godot project requires a 'project.godot' file. Ensure your script creates a root project directory and places 'project.godot' inside it.`,
|
|
839
|
-
},
|
|
840
|
-
],
|
|
841
|
-
isError: true,
|
|
842
|
-
};
|
|
843
|
-
}
|
|
844
|
-
} else if (args.project_dir) {
|
|
845
|
-
projectDir = path.resolve(process.cwd(), args.project_dir);
|
|
846
|
-
} else {
|
|
805
|
+
const files = extractFilesFromMarkdown(markdown);
|
|
806
|
+
if (Object.keys(files).length === 0) {
|
|
847
807
|
return {
|
|
848
808
|
content: [
|
|
849
809
|
{
|
|
850
810
|
type: "text",
|
|
851
|
-
text: `Error:
|
|
811
|
+
text: `Error: No files could be extracted from the Markdown text. Make sure you use '### filepath' headers followed by code blocks.`,
|
|
852
812
|
},
|
|
853
813
|
],
|
|
854
814
|
isError: true,
|
|
855
815
|
};
|
|
856
816
|
}
|
|
857
817
|
|
|
858
|
-
|
|
818
|
+
writeExtractedFiles(files, tempBase);
|
|
819
|
+
|
|
820
|
+
// Find the generated folder containing project.godot
|
|
821
|
+
const items = fs.readdirSync(tempBase);
|
|
822
|
+
for (const item of items) {
|
|
823
|
+
const itemPath = path.join(tempBase, item);
|
|
824
|
+
if (
|
|
825
|
+
fs.statSync(itemPath).isDirectory() &&
|
|
826
|
+
fs.existsSync(path.join(itemPath, "project.godot"))
|
|
827
|
+
) {
|
|
828
|
+
projectDir = itemPath;
|
|
829
|
+
break;
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
if (!projectDir) {
|
|
859
834
|
return {
|
|
860
835
|
content: [
|
|
861
836
|
{
|
|
862
837
|
type: "text",
|
|
863
|
-
text: `Error: The
|
|
838
|
+
text: `Error: The extracted Markdown output did not contain a valid Godot project. A valid Godot project requires a 'project.godot' file placed inside a root directory block.`,
|
|
864
839
|
},
|
|
865
840
|
],
|
|
866
841
|
isError: true,
|
|
@@ -1081,81 +1056,60 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1081
1056
|
let cleanupDir = null;
|
|
1082
1057
|
try {
|
|
1083
1058
|
let projectDir = null;
|
|
1059
|
+
const markdown = args.project_markdown;
|
|
1084
1060
|
|
|
1085
|
-
if (
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
execSync(`node generate.js`, {
|
|
1097
|
-
cwd: tempBase,
|
|
1098
|
-
stdio: "pipe",
|
|
1099
|
-
timeout: 15000, // Time out after 15 seconds if it hangs
|
|
1100
|
-
});
|
|
1101
|
-
} catch (err) {
|
|
1102
|
-
const stderr = err.stderr ? err.stderr.toString() : "";
|
|
1103
|
-
const stdout = err.stdout ? err.stdout.toString() : "";
|
|
1104
|
-
return {
|
|
1105
|
-
content: [
|
|
1106
|
-
{
|
|
1107
|
-
type: "text",
|
|
1108
|
-
text: `Error: The Node.js script failed to execute properly.\n\nSTDERR:\n${stderr}\n\nSTDOUT:\n${stdout}\n\nException: ${err.message}`,
|
|
1109
|
-
},
|
|
1110
|
-
],
|
|
1111
|
-
isError: true,
|
|
1112
|
-
};
|
|
1113
|
-
}
|
|
1061
|
+
if (!markdown) {
|
|
1062
|
+
return {
|
|
1063
|
+
content: [
|
|
1064
|
+
{
|
|
1065
|
+
type: "text",
|
|
1066
|
+
text: `Error: 'project_markdown' is required.`,
|
|
1067
|
+
},
|
|
1068
|
+
],
|
|
1069
|
+
isError: true,
|
|
1070
|
+
};
|
|
1071
|
+
}
|
|
1114
1072
|
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
fs.statSync(itemPath).isDirectory() &&
|
|
1121
|
-
fs.existsSync(path.join(itemPath, "Cargo.toml"))
|
|
1122
|
-
) {
|
|
1123
|
-
projectDir = itemPath;
|
|
1124
|
-
break;
|
|
1125
|
-
}
|
|
1126
|
-
}
|
|
1073
|
+
// Create a unique temporary directory
|
|
1074
|
+
const tempBase = fs.mkdtempSync(
|
|
1075
|
+
path.join(os.tmpdir(), "scad-rust-compile-"),
|
|
1076
|
+
);
|
|
1077
|
+
cleanupDir = tempBase;
|
|
1127
1078
|
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
content: [
|
|
1131
|
-
{
|
|
1132
|
-
type: "text",
|
|
1133
|
-
text: `Error: The Node.js script finished running, but NO Rust project was found. A valid Rust project requires a 'Cargo.toml' file. Ensure your script creates a root project directory and places 'Cargo.toml' inside it.`,
|
|
1134
|
-
},
|
|
1135
|
-
],
|
|
1136
|
-
isError: true,
|
|
1137
|
-
};
|
|
1138
|
-
}
|
|
1139
|
-
} else if (args.project_dir) {
|
|
1140
|
-
projectDir = path.resolve(process.cwd(), args.project_dir);
|
|
1141
|
-
} else {
|
|
1079
|
+
const files = extractFilesFromMarkdown(markdown);
|
|
1080
|
+
if (Object.keys(files).length === 0) {
|
|
1142
1081
|
return {
|
|
1143
1082
|
content: [
|
|
1144
1083
|
{
|
|
1145
1084
|
type: "text",
|
|
1146
|
-
text: `Error:
|
|
1085
|
+
text: `Error: No files could be extracted from the Markdown text. Make sure you use '### filepath' headers followed by code blocks.`,
|
|
1147
1086
|
},
|
|
1148
1087
|
],
|
|
1149
1088
|
isError: true,
|
|
1150
1089
|
};
|
|
1151
1090
|
}
|
|
1152
1091
|
|
|
1153
|
-
|
|
1092
|
+
writeExtractedFiles(files, tempBase);
|
|
1093
|
+
|
|
1094
|
+
// Find the generated folder containing Cargo.toml
|
|
1095
|
+
const items = fs.readdirSync(tempBase);
|
|
1096
|
+
for (const item of items) {
|
|
1097
|
+
const itemPath = path.join(tempBase, item);
|
|
1098
|
+
if (
|
|
1099
|
+
fs.statSync(itemPath).isDirectory() &&
|
|
1100
|
+
fs.existsSync(path.join(itemPath, "Cargo.toml"))
|
|
1101
|
+
) {
|
|
1102
|
+
projectDir = itemPath;
|
|
1103
|
+
break;
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
if (!projectDir) {
|
|
1154
1108
|
return {
|
|
1155
1109
|
content: [
|
|
1156
1110
|
{
|
|
1157
1111
|
type: "text",
|
|
1158
|
-
text: `Error: The
|
|
1112
|
+
text: `Error: The extracted Markdown output did not contain a valid Rust project. A valid Rust project requires a 'Cargo.toml' file placed inside a root directory block.`,
|
|
1159
1113
|
},
|
|
1160
1114
|
],
|
|
1161
1115
|
isError: true,
|
package/bin/play.js
CHANGED
|
@@ -5,15 +5,19 @@ import path from "node:path";
|
|
|
5
5
|
import os from "node:os";
|
|
6
6
|
import { execSync, spawn } from "node:child_process";
|
|
7
7
|
import { getGodotBin } from "../src/godot-utils.js";
|
|
8
|
+
import {
|
|
9
|
+
extractFilesFromMarkdown,
|
|
10
|
+
writeExtractedFiles,
|
|
11
|
+
} from "../src/cli-utils.js";
|
|
8
12
|
|
|
9
13
|
function main() {
|
|
10
14
|
const packedScriptPath = process.argv[2];
|
|
11
15
|
|
|
12
16
|
if (!packedScriptPath) {
|
|
13
17
|
console.error(
|
|
14
|
-
"ā Error: Path to the
|
|
18
|
+
"ā Error: Path to the generated Markdown project file is required.",
|
|
15
19
|
);
|
|
16
|
-
console.error("Usage: scad-gltf play <path_to_generated_project.
|
|
20
|
+
console.error("Usage: scad-gltf play <path_to_generated_project.md>");
|
|
17
21
|
process.exit(1);
|
|
18
22
|
}
|
|
19
23
|
|
|
@@ -25,7 +29,7 @@ function main() {
|
|
|
25
29
|
|
|
26
30
|
// 1. Create a unique temporary directory
|
|
27
31
|
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "scad-play-"));
|
|
28
|
-
console.log(`š¦ Unpacking
|
|
32
|
+
console.log(`š¦ Unpacking project to temporary directory...\n ${tempDir}`);
|
|
29
33
|
|
|
30
34
|
let appProcess = null;
|
|
31
35
|
|
|
@@ -54,11 +58,38 @@ function main() {
|
|
|
54
58
|
});
|
|
55
59
|
|
|
56
60
|
try {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
)
|
|
61
|
-
|
|
61
|
+
const ext = path.extname(absScriptPath).toLowerCase();
|
|
62
|
+
|
|
63
|
+
// 2. Extract files based on extension
|
|
64
|
+
if (ext === ".js") {
|
|
65
|
+
// Legacy support for older node.js script generations
|
|
66
|
+
console.log(
|
|
67
|
+
`\nāļø Running legacy generator script: ${path.basename(absScriptPath)}`,
|
|
68
|
+
);
|
|
69
|
+
execSync(`node "${absScriptPath}"`, { cwd: tempDir, stdio: "inherit" });
|
|
70
|
+
} else if (ext === ".md") {
|
|
71
|
+
console.log(
|
|
72
|
+
`\nāļø Extracting Markdown project: ${path.basename(absScriptPath)}`,
|
|
73
|
+
);
|
|
74
|
+
const mdContent = fs.readFileSync(absScriptPath, "utf-8");
|
|
75
|
+
const files = extractFilesFromMarkdown(mdContent);
|
|
76
|
+
|
|
77
|
+
if (Object.keys(files).length === 0) {
|
|
78
|
+
console.error(
|
|
79
|
+
"\nā Error: No files could be extracted from the Markdown. Make sure it uses '### filepath' headers followed by code blocks.",
|
|
80
|
+
);
|
|
81
|
+
cleanup();
|
|
82
|
+
process.exit(1);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
writeExtractedFiles(files, tempDir);
|
|
86
|
+
} else {
|
|
87
|
+
console.error(
|
|
88
|
+
"\nā Error: Unsupported file type. Please provide a .md or .js generated project file.",
|
|
89
|
+
);
|
|
90
|
+
cleanup();
|
|
91
|
+
process.exit(1);
|
|
92
|
+
}
|
|
62
93
|
|
|
63
94
|
// 3. Find the resulting project folder and determine its type
|
|
64
95
|
let projectDir = null;
|
|
@@ -87,14 +118,14 @@ function main() {
|
|
|
87
118
|
if (!projectDir || !projectType) {
|
|
88
119
|
console.error("\nā Error: Could not find a valid generated project.");
|
|
89
120
|
console.error(
|
|
90
|
-
"The
|
|
121
|
+
"The project did not contain a root folder holding 'project.godot', 'package.json', or 'Cargo.toml'.",
|
|
91
122
|
);
|
|
92
123
|
cleanup();
|
|
93
124
|
process.exit(1);
|
|
94
125
|
}
|
|
95
126
|
|
|
96
127
|
console.log(
|
|
97
|
-
`\nāļø Found
|
|
128
|
+
`\nāļø Found extracted ${projectType.toUpperCase()} project: ${path.basename(projectDir)}`,
|
|
98
129
|
);
|
|
99
130
|
|
|
100
131
|
// 4. Boot the corresponding environment
|
package/bin/scad-gltf.js
CHANGED
|
@@ -8,6 +8,7 @@ const commands = {
|
|
|
8
8
|
mcp: "./mcp.js",
|
|
9
9
|
gen: "./gen.js",
|
|
10
10
|
play: "./play.js",
|
|
11
|
+
extract: "./extract.js",
|
|
11
12
|
};
|
|
12
13
|
|
|
13
14
|
if (!command || !commands[command]) {
|
|
@@ -19,7 +20,12 @@ if (!command || !commands[command]) {
|
|
|
19
20
|
console.error(
|
|
20
21
|
" gen Automated AI generation of 3D models, simulations, and interactive projects",
|
|
21
22
|
);
|
|
22
|
-
console.error(
|
|
23
|
+
console.error(
|
|
24
|
+
" play Test and launch generated projects in a temporary folder",
|
|
25
|
+
);
|
|
26
|
+
console.error(
|
|
27
|
+
" extract Extract a generated Markdown project to your local filesystem",
|
|
28
|
+
);
|
|
23
29
|
process.exit(1);
|
|
24
30
|
}
|
|
25
31
|
|
package/editor/dist/content.js
CHANGED
|
@@ -5,7 +5,7 @@ import{i as e,n as t,r as n,t as r}from"./assets/prompt-ui-zw7U8lsl.js";console.
|
|
|
5
5
|
<line x1="12" y1="22.08" x2="12" y2="12"></line>
|
|
6
6
|
</svg>
|
|
7
7
|
<span>Preview 3D</span>
|
|
8
|
-
`,t.onpointerdown=e=>e.stopPropagation(),t.onmousedown=e=>e.stopPropagation(),t.onclick=t=>{t.preventDefault(),t.stopPropagation();let n=e.querySelector(`code`);n&&s(n.
|
|
8
|
+
`,t.onpointerdown=e=>e.stopPropagation(),t.onmousedown=e=>e.stopPropagation(),t.onclick=t=>{t.preventDefault(),t.stopPropagation();let n=e.querySelector(`code`);n&&s(n.textContent)};let n=e.querySelector(`button[aria-label*="copy" i], button[aria-label*="download" i], button[aria-label*="collapse" i], button[mattooltip*="copy" i], button[mattooltip*="download" i], button[mattooltip*="collapse" i], button[title*="Copy" i], button[title*="Download" i]`),r=n?.parentElement||e.querySelector(`.actions`)||e.querySelector(`.header-actions`)||e.querySelector(`.action-buttons`)||e.querySelector(`.buttons`)||e.querySelector(`.header`)||e.querySelector(`.code-header`);if(r)n?r.insertBefore(t,n):r.prepend(t);else{let n=e.querySelector(`pre`);n?n.parentNode.insertBefore(t,n):e.appendChild(t)}})}).observe(document.body,{childList:!0,subtree:!0});function a(){if(document.getElementById(`scad-prompt-btn`))return;let e=document.createElement(`button`);e.id=`scad-prompt-btn`,e.innerText=`⨠SCAD`,e.className=`scad-prompt-btn`,e.onclick=async e=>{e.preventDefault(),e.stopPropagation();let t=document.getElementById(`scad-prompt-modal`);if(t){let e=document.getElementById(`scad-prompt-desc`);e&&(e.value=``),t.style.display=`flex`,e&&e.focus()}},document.body.appendChild(e)}function o(){if(document.getElementById(`scad-prompt-modal`))return;let i=document.createElement(`div`);i.id=`scad-prompt-modal`,i.className=`scad-modal-overlay`,i.style.display=`none`,i.innerHTML=`
|
|
9
9
|
<div class="scad-modal-content">
|
|
10
10
|
<div class="scad-modal-header">
|
|
11
11
|
<h3>⨠SCAD Prompt Generator</h3>
|
package/editor/dist/sw.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
if(!self.define){let e,s={};const i=(i,r)=>(i=new URL(i+".js",r).href,s[i]||new Promise(s=>{if("document"in self){const e=document.createElement("script");e.src=i,e.onload=s,document.head.appendChild(e)}else e=i,importScripts(i),s()}).then(()=>{let e=s[i];if(!e)throw new Error(`Module ${i} didnāt register its module`);return e}));self.define=(r,n)=>{const l=e||("document"in self?document.currentScript.src:"")||location.href;if(s[l])return;let o={};const t=e=>i(e,l),u={module:{uri:l},exports:o,require:t};s[l]=Promise.all(r.map(e=>u[e]||t(e))).then(e=>(n(...e),o))}}define(["./workbox-9c191d2f"],function(e){"use strict";self.skipWaiting(),e.clientsClaim(),e.precacheAndRoute([{url:"registerSW.js",revision:"402b66900e731ca748771b6fc5e7a068"},{url:"preview.html",revision:"16df0b9b2256a3d9f64f70190660f416"},{url:"index.html",revision:"62b35f003a048e944e6221f0778116bb"},{url:"icon.png",revision:"a22007c1fb99682b77960b0d210e273e"},{url:"content.js",revision:"
|
|
1
|
+
if(!self.define){let e,s={};const i=(i,r)=>(i=new URL(i+".js",r).href,s[i]||new Promise(s=>{if("document"in self){const e=document.createElement("script");e.src=i,e.onload=s,document.head.appendChild(e)}else e=i,importScripts(i),s()}).then(()=>{let e=s[i];if(!e)throw new Error(`Module ${i} didnāt register its module`);return e}));self.define=(r,n)=>{const l=e||("document"in self?document.currentScript.src:"")||location.href;if(s[l])return;let o={};const t=e=>i(e,l),u={module:{uri:l},exports:o,require:t};s[l]=Promise.all(r.map(e=>u[e]||t(e))).then(e=>(n(...e),o))}}define(["./workbox-9c191d2f"],function(e){"use strict";self.skipWaiting(),e.clientsClaim(),e.precacheAndRoute([{url:"registerSW.js",revision:"402b66900e731ca748771b6fc5e7a068"},{url:"preview.html",revision:"16df0b9b2256a3d9f64f70190660f416"},{url:"index.html",revision:"62b35f003a048e944e6221f0778116bb"},{url:"icon.png",revision:"a22007c1fb99682b77960b0d210e273e"},{url:"content.js",revision:"fb8d70942d66c5ed73f0714306a214c0"},{url:"content.css",revision:"4ea93a2d03164b49c80a48138305e564"},{url:"content-loader.js",revision:"d94f1755552d49c1fe3a6da123e8a346"},{url:"aristea_wreck_puresky_2k.hdr",revision:"e764c66f871ab0987f3fac422edc841d"},{url:"assets/shared-CDFEFgsW.js",revision:null},{url:"assets/prompt-ui-zw7U8lsl.js",revision:null},{url:"assets/preview-DfaOpP2w.js",revision:null},{url:"assets/preview-Cx3FHqKG.css",revision:null},{url:"assets/openscad-DJJQxKJM.wasm",revision:null},{url:"assets/index-b4iPmEjR.js",revision:null},{url:"assets/index-D8In1wnN.css",revision:null},{url:"icon.png",revision:"a22007c1fb99682b77960b0d210e273e"},{url:"manifest.webmanifest",revision:"705049c691faa532cc20c736d56d54df"}],{}),e.cleanupOutdatedCaches(),e.registerRoute(new e.NavigationRoute(e.createHandlerBoundToURL("index.html")))});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "scad-gltf",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.1",
|
|
4
4
|
"description": "WASM build of a custom OpenSCAD fork that compiles .scad scripts directly to glTF/GLB with PBR and animation support",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/ext/openscad.js",
|
|
@@ -44,9 +44,10 @@
|
|
|
44
44
|
"./openscad.wasm": "./src/ext/openscad.wasm"
|
|
45
45
|
},
|
|
46
46
|
"scripts": {
|
|
47
|
+
"test": "node --test 'test/**/*.test.js'",
|
|
47
48
|
"format": "prettier --write .",
|
|
48
49
|
"mcp": "npx @modelcontextprotocol/inspector node ./bin/mcp.js",
|
|
49
|
-
"prepublishOnly": "npm install --prefix editor && npm run build --prefix editor"
|
|
50
|
+
"prepublishOnly": "npm test && npm install --prefix editor && npm run build --prefix editor"
|
|
50
51
|
},
|
|
51
52
|
"dependencies": {
|
|
52
53
|
"@modelcontextprotocol/sdk": "^1.31.0",
|
package/src/cli-utils.js
CHANGED
|
@@ -157,6 +157,59 @@ export function generateScadPreviewUrl(
|
|
|
157
157
|
}
|
|
158
158
|
}
|
|
159
159
|
|
|
160
|
+
export function extractFilesFromMarkdown(mdContent) {
|
|
161
|
+
const files = {};
|
|
162
|
+
const lines = mdContent.split("\n");
|
|
163
|
+
let currentFile = null;
|
|
164
|
+
let inCodeBlock = false;
|
|
165
|
+
let currentContent = [];
|
|
166
|
+
|
|
167
|
+
// \S+ ensures the header is a single continuous path without spaces,
|
|
168
|
+
// preventing regular English markdown headers from being parsed as files.
|
|
169
|
+
const headerMatchRegex = /^###\s+(\S+)\s*$/;
|
|
170
|
+
|
|
171
|
+
for (const line of lines) {
|
|
172
|
+
const headerMatch = line.match(headerMatchRegex);
|
|
173
|
+
if (headerMatch) {
|
|
174
|
+
currentFile = headerMatch[1];
|
|
175
|
+
inCodeBlock = false;
|
|
176
|
+
currentContent = [];
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (currentFile) {
|
|
181
|
+
if (line.startsWith("```")) {
|
|
182
|
+
if (inCodeBlock) {
|
|
183
|
+
// Close block and save
|
|
184
|
+
files[currentFile] = currentContent.join("\n");
|
|
185
|
+
currentFile = null;
|
|
186
|
+
inCodeBlock = false;
|
|
187
|
+
} else {
|
|
188
|
+
// Open block
|
|
189
|
+
inCodeBlock = true;
|
|
190
|
+
currentContent = [];
|
|
191
|
+
}
|
|
192
|
+
} else if (inCodeBlock) {
|
|
193
|
+
currentContent.push(line);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return files;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export function writeExtractedFiles(files, targetDir) {
|
|
202
|
+
for (const [filepath, content] of Object.entries(files)) {
|
|
203
|
+
// Prevent directory traversal attacks
|
|
204
|
+
const safePath = path.normalize(filepath).replace(/^(\.\.(\/|\\|$))+/, "");
|
|
205
|
+
const absPath = path.resolve(targetDir, safePath);
|
|
206
|
+
if (!absPath.startsWith(path.resolve(targetDir))) continue;
|
|
207
|
+
|
|
208
|
+
fs.mkdirSync(path.dirname(absPath), { recursive: true });
|
|
209
|
+
fs.writeFileSync(absPath, content, "utf-8");
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
160
213
|
export async function runAutomatedAIFlow(
|
|
161
214
|
apiKey,
|
|
162
215
|
baseUrl,
|
|
@@ -177,8 +230,10 @@ export async function runAutomatedAIFlow(
|
|
|
177
230
|
process.argv.includes("-v") ||
|
|
178
231
|
process.env.VERBOSE,
|
|
179
232
|
);
|
|
180
|
-
|
|
233
|
+
|
|
234
|
+
// Output a Markdown file for non-raw scad projects instead of a JS script
|
|
181
235
|
const isRawScad = projectType === "scad";
|
|
236
|
+
const outputFilename = `generate_${projectType}_project.${isRawScad ? "scad" : "md"}`;
|
|
182
237
|
const modelTag = modelName ? ` (${modelName})` : "";
|
|
183
238
|
|
|
184
239
|
console.log(`\nš Starting automated AI generation${modelTag}...`);
|
|
@@ -241,10 +296,12 @@ export async function runAutomatedAIFlow(
|
|
|
241
296
|
);
|
|
242
297
|
}
|
|
243
298
|
|
|
244
|
-
let messages =
|
|
245
|
-
{ role: "
|
|
246
|
-
|
|
247
|
-
|
|
299
|
+
let messages = isRawScad
|
|
300
|
+
? [{ role: "user", content: systemPrompt }]
|
|
301
|
+
: [
|
|
302
|
+
{ role: "system", content: systemPrompt },
|
|
303
|
+
{ role: "user", content: inputRequest },
|
|
304
|
+
];
|
|
248
305
|
|
|
249
306
|
let iterations = 0;
|
|
250
307
|
const maxIterations = 30;
|
|
@@ -462,10 +519,9 @@ export async function runAutomatedAIFlow(
|
|
|
462
519
|
const finalText = message.content || "";
|
|
463
520
|
console.log("\nā
AI proposed a final code version.");
|
|
464
521
|
|
|
465
|
-
let finalCode =
|
|
522
|
+
let finalCode = finalText.trim();
|
|
466
523
|
let extractedFilename = outputFilename;
|
|
467
524
|
|
|
468
|
-
// Extract the code payload based on the project type target
|
|
469
525
|
if (isRawScad) {
|
|
470
526
|
const match = finalText.match(
|
|
471
527
|
/```(?:openscad|scad)?\n([\s\S]*?)```/i,
|
|
@@ -484,11 +540,6 @@ export async function runAutomatedAIFlow(
|
|
|
484
540
|
if (!safeName.endsWith(".scad")) safeName += ".scad";
|
|
485
541
|
extractedFilename = safeName;
|
|
486
542
|
}
|
|
487
|
-
} else {
|
|
488
|
-
const match = finalText.match(
|
|
489
|
-
/```(?:javascript|js|node)?\n([\s\S]*?)```/i,
|
|
490
|
-
);
|
|
491
|
-
finalCode = match ? match[1].trim() : finalText.trim();
|
|
492
543
|
}
|
|
493
544
|
|
|
494
545
|
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "scad-preview-"));
|
|
@@ -509,14 +560,16 @@ export async function runAutomatedAIFlow(
|
|
|
509
560
|
}
|
|
510
561
|
} else {
|
|
511
562
|
console.log(
|
|
512
|
-
`
|
|
563
|
+
` Extracting Markdown project files to temporary folder...`,
|
|
513
564
|
);
|
|
514
|
-
|
|
565
|
+
const files = extractFilesFromMarkdown(finalCode);
|
|
515
566
|
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
567
|
+
if (Object.keys(files).length === 0) {
|
|
568
|
+
throw new Error(
|
|
569
|
+
"No files could be extracted from the AI's Markdown output. Ensure it uses the '### filepath' format.",
|
|
570
|
+
);
|
|
571
|
+
}
|
|
572
|
+
writeExtractedFiles(files, tempDir);
|
|
520
573
|
|
|
521
574
|
let projectDir = null;
|
|
522
575
|
let latestTime = 0;
|
|
@@ -558,7 +611,7 @@ export async function runAutomatedAIFlow(
|
|
|
558
611
|
: "package.json";
|
|
559
612
|
messages.push({
|
|
560
613
|
role: "user",
|
|
561
|
-
content: `Your
|
|
614
|
+
content: `Your generated output successfully parsed, but no valid project directory was found. Make sure you place the required files (like ${expectedFile}) inside a root project folder block.`,
|
|
562
615
|
});
|
|
563
616
|
continue;
|
|
564
617
|
}
|
|
@@ -632,7 +685,7 @@ export async function runAutomatedAIFlow(
|
|
|
632
685
|
|
|
633
686
|
if (!isRawScad) {
|
|
634
687
|
console.log(
|
|
635
|
-
`ā¶ļø Run it again later to unpack the final project with:
|
|
688
|
+
`ā¶ļø Run it again later to unpack and play the final project with: scad-gltf play ${extractedFilename}`,
|
|
636
689
|
);
|
|
637
690
|
}
|
|
638
691
|
|
|
@@ -641,7 +694,7 @@ export async function runAutomatedAIFlow(
|
|
|
641
694
|
console.log("\nš Sending your feedback back to the LLM...");
|
|
642
695
|
messages.push({
|
|
643
696
|
role: "user",
|
|
644
|
-
content: `I reviewed the current version. Here is my feedback to improve it:\n\n${feedback}\n\nPlease implement these fixes and output an updated ${isRawScad ? "OpenSCAD block" : "
|
|
697
|
+
content: `I reviewed the current version. Here is my feedback to improve it:\n\n${feedback}\n\nPlease implement these fixes and output an updated ${isRawScad ? "OpenSCAD block" : "Markdown project file"}.`,
|
|
645
698
|
});
|
|
646
699
|
}
|
|
647
700
|
} catch (err) {
|
|
@@ -651,7 +704,7 @@ export async function runAutomatedAIFlow(
|
|
|
651
704
|
);
|
|
652
705
|
messages.push({
|
|
653
706
|
role: "user",
|
|
654
|
-
content: `Your generated code threw an error when I tried to run/preview it locally:\n${stderr}\n\nPlease fix the ${isRawScad ? "OpenSCAD code" : "
|
|
707
|
+
content: `Your generated code threw an error when I tried to run/preview it locally:\n${stderr}\n\nPlease fix the ${isRawScad ? "OpenSCAD code" : "Markdown output format"}.`,
|
|
655
708
|
});
|
|
656
709
|
} finally {
|
|
657
710
|
if (runProc) {
|
package/src/godot-utils.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import fs from "fs";
|
|
2
|
-
import path from "path";
|
|
3
|
-
import os from "os";
|
|
4
|
-
import
|
|
5
|
-
import { fileURLToPath } from "url";
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import cp from "node:child_process";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
6
|
|
|
7
7
|
const __filename = fileURLToPath(import.meta.url);
|
|
8
8
|
const __dirname = path.dirname(__filename);
|
|
@@ -37,7 +37,7 @@ export function ensureExportTemplates() {
|
|
|
37
37
|
fs.mkdirSync(BIN_DIR, { recursive: true });
|
|
38
38
|
|
|
39
39
|
const tpzPath = path.join(BIN_DIR, "templates.tpz");
|
|
40
|
-
execSync(
|
|
40
|
+
cp.execSync(
|
|
41
41
|
`curl -fL "https://github.com/godotengine/godot/releases/download/${GODOT_VERSION}/Godot_v${GODOT_VERSION}_export_templates.tpz" -o "${tpzPath}"`,
|
|
42
42
|
{ stdio: "inherit" },
|
|
43
43
|
);
|
|
@@ -46,9 +46,11 @@ export function ensureExportTemplates() {
|
|
|
46
46
|
fs.mkdirSync(extractTemp, { recursive: true });
|
|
47
47
|
|
|
48
48
|
if (os.platform() === "win32") {
|
|
49
|
-
execSync(`tar -xf "${tpzPath}" -C "${extractTemp}"`, {
|
|
49
|
+
cp.execSync(`tar -xf "${tpzPath}" -C "${extractTemp}"`, {
|
|
50
|
+
stdio: "ignore",
|
|
51
|
+
});
|
|
50
52
|
} else {
|
|
51
|
-
execSync(`unzip -q -o "${tpzPath}" -d "${extractTemp}"`, {
|
|
53
|
+
cp.execSync(`unzip -q -o "${tpzPath}" -d "${extractTemp}"`, {
|
|
52
54
|
stdio: "ignore",
|
|
53
55
|
});
|
|
54
56
|
}
|
|
@@ -67,7 +69,7 @@ export function ensureExportTemplates() {
|
|
|
67
69
|
|
|
68
70
|
export function getGodotBin(withTemplates = false) {
|
|
69
71
|
try {
|
|
70
|
-
execSync("godot --version", { stdio: "ignore" });
|
|
72
|
+
cp.execSync("godot --version", { stdio: "ignore" });
|
|
71
73
|
if (withTemplates) ensureExportTemplates();
|
|
72
74
|
return "godot";
|
|
73
75
|
} catch {
|
|
@@ -101,12 +103,14 @@ export function getGodotBin(withTemplates = false) {
|
|
|
101
103
|
}
|
|
102
104
|
|
|
103
105
|
const zipPath = path.join(BIN_DIR, "godot.zip");
|
|
104
|
-
execSync(`curl -fL "${zipUrl}" -o "${zipPath}"`, { stdio: "ignore" });
|
|
106
|
+
cp.execSync(`curl -fL "${zipUrl}" -o "${zipPath}"`, { stdio: "ignore" });
|
|
105
107
|
|
|
106
108
|
if (isWin) {
|
|
107
|
-
execSync(`tar -xf "${zipPath}" -C "${BIN_DIR}"`, {
|
|
109
|
+
cp.execSync(`tar -xf "${zipPath}" -C "${BIN_DIR}"`, {
|
|
110
|
+
stdio: "ignore",
|
|
111
|
+
});
|
|
108
112
|
} else {
|
|
109
|
-
execSync(`unzip -q -o "${zipPath}" -d "${BIN_DIR}"`, {
|
|
113
|
+
cp.execSync(`unzip -q -o "${zipPath}" -d "${BIN_DIR}"`, {
|
|
110
114
|
stdio: "ignore",
|
|
111
115
|
});
|
|
112
116
|
}
|
|
@@ -144,36 +148,50 @@ export function runGodotAsync(args, cwd, timeoutMs) {
|
|
|
144
148
|
let spawnBin = godotBin;
|
|
145
149
|
let spawnArgs = args;
|
|
146
150
|
|
|
147
|
-
// ---------------------------------------------------------
|
|
148
|
-
// CRITICAL: Do not use real display on Linux. Run inside Xvfb
|
|
149
|
-
// to avoid popping up windows or failing in headless server environments.
|
|
150
|
-
// ---------------------------------------------------------
|
|
151
151
|
if (process.platform === "linux") {
|
|
152
152
|
spawnArgs = ["-a", "-s", "-screen 0 1024x768x24", spawnBin, ...args];
|
|
153
153
|
spawnBin = "xvfb-run";
|
|
154
154
|
}
|
|
155
155
|
|
|
156
|
-
const godotProcess = spawn(spawnBin, spawnArgs, { cwd, env });
|
|
156
|
+
const godotProcess = cp.spawn(spawnBin, spawnArgs, { cwd, env });
|
|
157
157
|
|
|
158
|
-
godotProcess.stdout.on("data", (data) =>
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
});
|
|
158
|
+
godotProcess.stdout.on("data", (data) => (output += data.toString()));
|
|
159
|
+
godotProcess.stderr.on("data", (data) => (output += data.toString()));
|
|
160
|
+
|
|
161
|
+
// 1. Add a flag to prevent resolving the promise multiple times
|
|
162
|
+
let isResolved = false;
|
|
164
163
|
|
|
165
164
|
const timer = setTimeout(() => {
|
|
166
|
-
|
|
165
|
+
if (isResolved) return;
|
|
166
|
+
isResolved = true;
|
|
167
|
+
try {
|
|
168
|
+
godotProcess.kill("SIGKILL"); // 2. Attempt a harder kill
|
|
169
|
+
} catch (e) {}
|
|
170
|
+
|
|
171
|
+
// 3. EXPLICITLY resolve the promise here so the MCP server doesn't hang
|
|
172
|
+
resolve({ code: 124, output: output + "\n--- KILLED BY TIMEOUT ---" });
|
|
167
173
|
}, timeoutMs);
|
|
168
174
|
|
|
169
175
|
godotProcess.on("close", (code) => {
|
|
176
|
+
if (isResolved) return;
|
|
177
|
+
isResolved = true;
|
|
170
178
|
clearTimeout(timer);
|
|
171
179
|
resolve({ code, output });
|
|
172
180
|
});
|
|
173
181
|
|
|
174
182
|
godotProcess.on("error", (err) => {
|
|
183
|
+
if (isResolved) return;
|
|
184
|
+
isResolved = true;
|
|
175
185
|
clearTimeout(timer);
|
|
176
|
-
|
|
186
|
+
|
|
187
|
+
let errorMsg = `Process error: ${err.message}`;
|
|
188
|
+
|
|
189
|
+
// Notify Linux users that xvfb is required
|
|
190
|
+
if (err.code === "ENOENT" && spawnBin === "xvfb-run") {
|
|
191
|
+
errorMsg = `Error: 'xvfb-run' is missing. To test Godot projects on headless Linux, you must install Xvfb.\n\nPlease run: sudo apt-get install xvfb`;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
resolve({ code: -1, output: errorMsg });
|
|
177
195
|
});
|
|
178
196
|
});
|
|
179
197
|
}
|
package/src/project-prompts.js
CHANGED
|
@@ -16,7 +16,7 @@ function readFile(filePath) {
|
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
function formatMarkdownFile(displayPath, lang, content) {
|
|
19
|
-
return `### ${displayPath}\n
|
|
19
|
+
return `### ${displayPath}\n\`\`\`${lang}\n${content}\n\`\`\`\n\n`;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
function appendUserScadFiles(options = {}, isRawScad = false) {
|
|
@@ -46,6 +46,90 @@ function appendUserScadFiles(options = {}, isRawScad = false) {
|
|
|
46
46
|
return output;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
function getScadAssetsInstructions(promptRules, projectType) {
|
|
50
|
+
let specificRules = "";
|
|
51
|
+
if (projectType === "godot") {
|
|
52
|
+
specificRules = ` - Scale & Units: 1 OpenSCAD unit = 1 Godot meter. Design your models using realistic meter-based scales (e.g., a character should be ~1.8 units tall). DO NOT use millimeter-based scaling.
|
|
53
|
+
- Coordinate System & Forward Convention: Write standard OpenSCAD Z-up code (+Z is UP, XY plane is ground). Build objects standing upright and facing Front (Positive Y-axis).
|
|
54
|
+
- Left/Right Convention: Always name and position "left" and "right" components (e.g., LeftArm, RightEye) based on the object's anatomical point of view (facing Forward towards +Y), NOT the camera/viewer's screen perspective. Because the object faces +Y, the object's Left side is along the -X axis, and the object's Right side is along the +X axis.
|
|
55
|
+
- CRITICAL Coordinate Mapping: The SCAD to glTF converter used by the Godot importer automatically converts OpenSCAD's Z-up coordinate system to Godot's Y-up coordinate system. Design your models naturally in OpenSCAD using this exact mapping:
|
|
56
|
+
* OpenSCAD +X (Right) -> Godot +X (Right)
|
|
57
|
+
* OpenSCAD -X (Left) -> Godot -X (Left)
|
|
58
|
+
* OpenSCAD +Y (Forward) -> Godot -Z (Forward)
|
|
59
|
+
* OpenSCAD -Y (Back) -> Godot +Z (Back)
|
|
60
|
+
* OpenSCAD +Z (Up) -> Godot +Y (Up)
|
|
61
|
+
DO NOT manually apply root rotations (e.g., \`rotate([90, 0, 0])\`) to compensate for Godot.`;
|
|
62
|
+
} else {
|
|
63
|
+
specificRules = ` - CRITICAL: The SCAD to glTF converter automatically converts OpenSCAD's Z-up coordinate system to the standard glTF Y-up coordinate system. Design your models naturally in OpenSCAD.`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return `1. 3D Assets (.scad):
|
|
67
|
+
- Generate procedural 3D models for the project using OpenSCAD.
|
|
68
|
+
${specificRules}
|
|
69
|
+
- CRITICAL: You must use the custom OpenSCAD glTF extensions for PBR materials (e.g., \`roughness\`, \`metalness\`, \`emissive\`) and Hierarchical Node Animations (\`armature()\`, \`bone()\`). The rules and syntax for these features are provided below:
|
|
70
|
+
|
|
71
|
+
=== OPENSCAD SYNTAX RULES ===
|
|
72
|
+
${promptRules}
|
|
73
|
+
=============================`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function getDeliveryFormat(projectType, frameworkName, hasUserScadFiles) {
|
|
77
|
+
let exampleCode = "";
|
|
78
|
+
let includes = "";
|
|
79
|
+
|
|
80
|
+
if (projectType === "bevy" || projectType === "wgpu") {
|
|
81
|
+
exampleCode = ` ### my_project/Cargo.toml
|
|
82
|
+
\`\`\`toml
|
|
83
|
+
[package]
|
|
84
|
+
name = "my_project"
|
|
85
|
+
...
|
|
86
|
+
\`\`\`
|
|
87
|
+
### my_project/src/main.rs
|
|
88
|
+
\`\`\`rust
|
|
89
|
+
fn main() {}
|
|
90
|
+
\`\`\``;
|
|
91
|
+
includes = ` - Your generated Rust ${frameworkName} project files (\`Cargo.toml\`, \`src/main.rs\`).
|
|
92
|
+
- The exact source code of the provided \`build.rs\` file, placed in the project root.`;
|
|
93
|
+
} else if (projectType === "web") {
|
|
94
|
+
exampleCode = ` ### my_project/package.json
|
|
95
|
+
\`\`\`json
|
|
96
|
+
{
|
|
97
|
+
"name": "my_project"
|
|
98
|
+
}
|
|
99
|
+
\`\`\`
|
|
100
|
+
### my_project/main.js
|
|
101
|
+
\`\`\`javascript
|
|
102
|
+
console.log("Started");
|
|
103
|
+
\`\`\``;
|
|
104
|
+
includes = ` - Your generated Vite web project files.`;
|
|
105
|
+
} else if (projectType === "godot") {
|
|
106
|
+
exampleCode = ` ### my_project/project.godot
|
|
107
|
+
\`\`\`ini
|
|
108
|
+
config_version=5
|
|
109
|
+
\`\`\`
|
|
110
|
+
### my_project/main.gd
|
|
111
|
+
\`\`\`gdscript
|
|
112
|
+
extends Node
|
|
113
|
+
\`\`\``;
|
|
114
|
+
includes = ` - Your generated Godot project files.
|
|
115
|
+
- The exact source code of the provided \`addons/scad_importer/*\` files, placed in their correct respective paths.`;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return `3. Delivery Format (Single Markdown File):
|
|
119
|
+
- Output exactly ONE Markdown response containing all project files.
|
|
120
|
+
- Format EACH file with a Markdown header specifying the relative file path (using snake_case directories), followed immediately by a code block containing the file's contents.
|
|
121
|
+
- Example Format:
|
|
122
|
+
${exampleCode}
|
|
123
|
+
- The project files must be placed inside a root project folder (e.g., \`my_project/\`).
|
|
124
|
+
- You must include and write:
|
|
125
|
+
- Your generated \`.scad\` 3D assets.
|
|
126
|
+
${includes}${
|
|
127
|
+
hasUserScadFiles
|
|
128
|
+
? "\n - The provided user `.scad` files (modified if necessary), placed in the appropriate project folders."
|
|
129
|
+
: ""
|
|
130
|
+
}`;
|
|
131
|
+
}
|
|
132
|
+
|
|
49
133
|
export function getProjectPrompts(projectType) {
|
|
50
134
|
if (projectType === "scad") {
|
|
51
135
|
return {
|
|
@@ -78,14 +162,7 @@ NAMING CONVENTION REQUIREMENT:
|
|
|
78
162
|
- NEVER use hyphens/minus signs (\`-\`) or spaces in any file or folder names.
|
|
79
163
|
|
|
80
164
|
What to generate:
|
|
81
|
-
|
|
82
|
-
- Generate procedural 3D models for the project using OpenSCAD.
|
|
83
|
-
- CRITICAL: The SCAD to glTF converter automatically converts OpenSCAD's Z-up coordinate system to the standard glTF Y-up coordinate system. Design your models naturally in OpenSCAD.
|
|
84
|
-
- CRITICAL: You must use the custom OpenSCAD glTF extensions for PBR materials (e.g., \`roughness\`, \`metalness\`, \`emissive\`) and Hierarchical Node Animations (\`armature()\`, \`bone()\`). The rules and syntax for these features are provided below:
|
|
85
|
-
|
|
86
|
-
=== OPENSCAD SYNTAX RULES ===
|
|
87
|
-
${promptRules}
|
|
88
|
-
=============================
|
|
165
|
+
${getScadAssetsInstructions(promptRules, projectType)}
|
|
89
166
|
|
|
90
167
|
2. Rust ${frameworkName} Project Files:
|
|
91
168
|
- Create the necessary files for a modern Rust ${frameworkName} application (e.g., \`Cargo.toml\`, \`build.rs\`, \`src/main.rs\`).
|
|
@@ -97,20 +174,7 @@ ${promptRules}
|
|
|
97
174
|
: ""
|
|
98
175
|
}
|
|
99
176
|
|
|
100
|
-
|
|
101
|
-
- Output exactly ONE self-contained Node.js script. Do not output manual setup instructions.
|
|
102
|
-
- CRITICAL: The generated Node.js script MUST first create a root project folder (named using a slugified version of the project name) and output all files and folders inside this newly created project folder.
|
|
103
|
-
- CRITICAL: You MUST use \`process.cwd()\` (and NOT \`__dirname\`) when defining the target directory path (e.g. \`const rootDir = path.join(process.cwd(), "my_project");\`). This ensures the script extracts correctly into whatever temporary folder it is executed from.
|
|
104
|
-
- When executed, this script must programmatically create the entire project directory structure and write all the files to disk using the \`fs\` module.
|
|
105
|
-
- The script must embed and write:
|
|
106
|
-
- Your generated \`.scad\` 3D assets.
|
|
107
|
-
- Your generated Rust ${frameworkName} project files (\`Cargo.toml\`, \`src/main.rs\`).
|
|
108
|
-
- The exact source code of the provided \`build.rs\` file, placed in the project root.${
|
|
109
|
-
hasUserScadFiles
|
|
110
|
-
? "\n - The provided user `.scad` files (modified if necessary), placed in the appropriate project folders."
|
|
111
|
-
: ""
|
|
112
|
-
}
|
|
113
|
-
- Ensure all string file contents inside the Node.js script are properly escaped.`;
|
|
177
|
+
${getDeliveryFormat(projectType, frameworkName, hasUserScadFiles)}`;
|
|
114
178
|
|
|
115
179
|
let systemClipboardOutput = `${systemPrompt}\n\n`;
|
|
116
180
|
|
|
@@ -144,14 +208,7 @@ ${promptRules}
|
|
|
144
208
|
`You are an expert Web 3D developer and procedural 3D technical artist.
|
|
145
209
|
|
|
146
210
|
What to generate:
|
|
147
|
-
|
|
148
|
-
- Generate procedural 3D models for the project using OpenSCAD.
|
|
149
|
-
- CRITICAL: The SCAD to glTF converter automatically converts OpenSCAD's Z-up coordinate system to the standard glTF Y-up coordinate system. Design your models naturally in OpenSCAD.
|
|
150
|
-
- CRITICAL: You must use the custom OpenSCAD glTF extensions for PBR materials (e.g., \`roughness\`, \`metalness\`, \`emissive\`) and Hierarchical Node Animations (\`armature()\`, \`bone()\`). The rules and syntax for these features are provided below:
|
|
151
|
-
|
|
152
|
-
=== OPENSCAD SYNTAX RULES ===
|
|
153
|
-
${promptRules}
|
|
154
|
-
=============================
|
|
211
|
+
${getScadAssetsInstructions(promptRules, projectType)}
|
|
155
212
|
|
|
156
213
|
2. Vite Web Project Files (npm based):
|
|
157
214
|
- Create the necessary files for a modern web application (e.g., \`package.json\`, \`index.html\`, \`main.js\`).
|
|
@@ -164,19 +221,7 @@ ${promptRules}
|
|
|
164
221
|
\`"prebuild": "scad-gltf convert ./scad ./public/models --cache"\`
|
|
165
222
|
- Write the core application logic to load and display the converted \`.glb\` files interactively.
|
|
166
223
|
|
|
167
|
-
|
|
168
|
-
- Output exactly ONE self-contained Node.js script. Do not output manual setup instructions.
|
|
169
|
-
- CRITICAL: The generated Node.js script MUST first create a root project folder (named using a slugified version of the project name) and output all files and folders inside this newly created project folder.
|
|
170
|
-
- CRITICAL: You MUST use \`process.cwd()\` (and NOT \`__dirname\`) when defining the target directory path (e.g. \`const rootDir = path.join(process.cwd(), "my_project");\`). This ensures the script extracts correctly into whatever temporary folder it is executed from.
|
|
171
|
-
- When executed, this script must programmatically create the entire project directory structure and write all the files to disk using the \`fs\` module.
|
|
172
|
-
- The script must embed and write:
|
|
173
|
-
- Your generated \`.scad\` 3D assets.
|
|
174
|
-
- Your generated Vite web project files.${
|
|
175
|
-
hasUserScadFiles
|
|
176
|
-
? "\n - The provided user `.scad` files (modified if necessary), placed in the appropriate project folders."
|
|
177
|
-
: ""
|
|
178
|
-
}
|
|
179
|
-
- Ensure all string file contents inside the Node.js script are properly escaped.` +
|
|
224
|
+
${getDeliveryFormat(projectType, null, hasUserScadFiles)}` +
|
|
180
225
|
appendUserScadFiles(options)
|
|
181
226
|
);
|
|
182
227
|
},
|
|
@@ -199,23 +244,7 @@ NAMING CONVENTION REQUIREMENT:
|
|
|
199
244
|
- NEVER use hyphens/minus signs (\`-\`) or spaces in any file or folder names.
|
|
200
245
|
|
|
201
246
|
What to generate:
|
|
202
|
-
|
|
203
|
-
- Generate procedural 3D models for the project using OpenSCAD.
|
|
204
|
-
- Scale & Units: 1 OpenSCAD unit = 1 Godot meter. Design your models using realistic meter-based scales (e.g., a character should be ~1.8 units tall). DO NOT use millimeter-based scaling.
|
|
205
|
-
- Coordinate System & Forward Convention: Write standard OpenSCAD Z-up code (+Z is UP, XY plane is ground). Build objects standing upright and facing Front (Positive Y-axis).
|
|
206
|
-
- Left/Right Convention: Always name and position "left" and "right" components (e.g., LeftArm, RightEye) based on the object's anatomical point of view (facing Forward towards +Y), NOT the camera/viewer's screen perspective. Because the object faces +Y, the object's Left side is along the -X axis, and the object's Right side is along the +X axis.
|
|
207
|
-
- CRITICAL Coordinate Mapping: The SCAD to glTF converter used by the Godot importer automatically converts OpenSCAD's Z-up coordinate system to Godot's Y-up coordinate system. Design your models naturally in OpenSCAD using this exact mapping:
|
|
208
|
-
* OpenSCAD +X (Right) -> Godot +X (Right)
|
|
209
|
-
* OpenSCAD -X (Left) -> Godot -X (Left)
|
|
210
|
-
* OpenSCAD +Y (Forward) -> Godot -Z (Forward)
|
|
211
|
-
* OpenSCAD -Y (Back) -> Godot +Z (Back)
|
|
212
|
-
* OpenSCAD +Z (Up) -> Godot +Y (Up)
|
|
213
|
-
DO NOT manually apply root rotations (e.g., \`rotate([90, 0, 0])\`) to compensate for Godot.
|
|
214
|
-
- CRITICAL: You must use the custom OpenSCAD glTF extensions for PBR materials (e.g., \`roughness\`, \`metalness\`, \`emissive\`) and Hierarchical Node Animations (\`armature()\`, \`bone()\`). The rules and syntax for these features are provided below:
|
|
215
|
-
|
|
216
|
-
=== OPENSCAD SYNTAX RULES ===
|
|
217
|
-
${promptRules}
|
|
218
|
-
=============================
|
|
247
|
+
${getScadAssetsInstructions(promptRules, projectType)}
|
|
219
248
|
|
|
220
249
|
2. Godot 4 Project Files:
|
|
221
250
|
- Create the necessary GDScript (\`.gd\`) and scene (\`.tscn\`) files to implement the project logic, responsive user input controls, and a core interaction loop.
|
|
@@ -229,20 +258,7 @@ ${promptRules}
|
|
|
229
258
|
* Natural Model Alignment: Because OpenSCAD models face +Y (Forward), they automatically import facing Godot's Forward direction (-Z). Built-in Godot methods like \`look_at()\` orient the node's -Z axis toward the target, which perfectly aligns with the model's front. Do NOT apply compensation rotations (e.g., \`rotate_y(PI)\`) in GDScript to compensate for model orientation.
|
|
230
259
|
- Generate a \`project.godot\` file. It must configure the project and automatically enable the \`scad_importer\` plugin.
|
|
231
260
|
|
|
232
|
-
|
|
233
|
-
- Output exactly ONE self-contained Node.js script. Do not output manual setup instructions.
|
|
234
|
-
- CRITICAL: The generated Node.js script MUST first create a root project folder (named using snake_case with underscores, e.g., \`my_project\`) and output all files and folders inside this newly created project folder.
|
|
235
|
-
- CRITICAL: You MUST use \`process.cwd()\` (and NOT \`__dirname\`) when defining the target directory path (e.g. \`const rootDir = path.join(process.cwd(), "my_project");\`). This ensures the script extracts correctly into whatever temporary folder it is executed from.
|
|
236
|
-
- When executed, this script must programmatically create the entire project directory structure and write all the files to disk using the \`fs\` module.
|
|
237
|
-
- The script must embed and write:
|
|
238
|
-
- Your generated \`.scad\` assets.
|
|
239
|
-
- Your generated Godot project files.
|
|
240
|
-
- The exact source code of the provided \`addons/scad_importer/*\` files, placed in their correct respective paths.${
|
|
241
|
-
hasUserScadFiles
|
|
242
|
-
? "\n - The provided user `.scad` files (modified if necessary), placed in the appropriate project folders."
|
|
243
|
-
: ""
|
|
244
|
-
}
|
|
245
|
-
- Ensure all string file contents inside the Node.js script are properly escaped.`;
|
|
261
|
+
${getDeliveryFormat(projectType, null, hasUserScadFiles)}`;
|
|
246
262
|
|
|
247
263
|
let systemClipboardOutput = `${systemPrompt}\n\n`;
|
|
248
264
|
|