scad-gltf 0.5.0 → 0.6.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 CHANGED
@@ -25,7 +25,8 @@ The C++ source code for this custom OpenSCAD version is included directly in thi
25
25
  - **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
26
  - **Godot 4, Rust Bevy & Web Integration:** Native Godot 4 importer addon and Rust Bevy compile-time `build.rs` integration.
27
27
  - **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 game projects in isolated temporary environments.
28
+ - **Project Runner:** Bundled `scad-gltf play` CLI utility to safely unpack, build, and launch generated Markdown projects in isolated temporary environments.
29
+ - **Project Extractor:** Bundled `scad-gltf extract` CLI utility to permanently unpack AI-generated Markdown projects to your local filesystem.
29
30
 
30
31
  ---
31
32
 
@@ -286,7 +287,7 @@ If you installed the package globally, you can configure your MCP client to use
286
287
 
287
288
  ## Automated AI Generation (`scad-gltf gen`)
288
289
 
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.
290
+ 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
291
 
291
292
  **Supported Generation Types:**
292
293
 
@@ -341,25 +342,43 @@ scad-gltf gen godot "A game using the provided player model" '{"scadFiles": ["./
341
342
  _Once the automated process completes for full game projects, you can test it instantly using the `play` command:_
342
343
 
343
344
  ```bash
344
- scad-gltf play generate_godot_project.js
345
+ scad-gltf play generate_godot_project.md
345
346
  ```
346
347
 
347
- _Alternatively, to extract the final project permanently into your current directory, just run the script with Node:_
348
+ _Alternatively, to permanently unpack the project to your local filesystem, use the `extract` command:_
348
349
 
349
350
  ```bash
350
- node generate_godot_project.js
351
+ scad-gltf extract generate_godot_project.md
351
352
  ```
352
353
 
353
354
  ---
354
355
 
355
356
  ## Testing Generated Projects (`scad-gltf play`)
356
357
 
357
- The `scad-gltf play` utility is designed to quickly test the single-file Node.js scripts 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.
358
+ 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.
358
359
 
359
360
  **Usage:**
360
361
 
361
362
  ```bash
362
- scad-gltf play <path_to_generated_script.js>
363
+ scad-gltf play <path_to_generated_project.md>
364
+ ```
365
+
366
+ ---
367
+
368
+ ## Extracting Generated Projects (`scad-gltf extract`)
369
+
370
+ 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.
371
+
372
+ It reads the `### filepath` headers and writes the code blocks to their respective directories.
373
+
374
+ **Usage:**
375
+
376
+ ```bash
377
+ # Extract files into the current working directory
378
+ scad-gltf extract <path_to_generated_project.md>
379
+
380
+ # Extract files into a specific directory
381
+ scad-gltf extract <path_to_generated_project.md> ./my_new_game
363
382
  ```
364
383
 
365
384
  ---
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 { generateScadPreviewUrl } from "../src/cli-utils.js";
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.3.0",
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
- "Runs a specified Godot project for a short duration to detect script errors, missing resources, or runtime crashes. Returns the console output logs and a visual screenshot of the running project.",
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
- project_dir: {
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 self-contained Node.js script that generates the Godot project. If provided, the tool will execute this script in a temporary directory to extract the project before testing it. Use this during the generation phase to test your code before providing the final answer.",
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 (faster). Default is true.",
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
- "Compiles a specified Rust project to detect syntax errors, missing dependencies, or compilation failures. Returns the cargo console output logs.",
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
- project_dir: {
253
+ project_markdown: {
254
254
  type: "string",
255
255
  description:
256
- "The relative or absolute path to the directory containing Cargo.toml. Use this if the project is already extracted on disk.",
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 (args.nodejs_script) {
791
- // Create a unique temporary directory
792
- const tempBase = fs.mkdtempSync(
793
- path.join(os.tmpdir(), "scad-godot-test-"),
794
- );
795
- cleanupDir = tempBase;
796
-
797
- const scriptPath = path.join(tempBase, "generate.js");
798
- fs.writeFileSync(scriptPath, args.nodejs_script, "utf-8");
799
-
800
- try {
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
- // 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
- }
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
- if (!projectDir) {
834
- return {
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: Invalid arguments. You must provide either 'project_dir' or 'nodejs_script'.`,
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
- if (!fs.existsSync(path.join(projectDir, "project.godot"))) {
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 specified directory is not a valid Godot project. 'project.godot' was not found in: ${projectDir}`,
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 (args.nodejs_script) {
1086
- // Create a unique temporary directory
1087
- const tempBase = fs.mkdtempSync(
1088
- path.join(os.tmpdir(), "scad-rust-compile-"),
1089
- );
1090
- cleanupDir = tempBase;
1091
-
1092
- const scriptPath = path.join(tempBase, "generate.js");
1093
- fs.writeFileSync(scriptPath, args.nodejs_script, "utf-8");
1094
-
1095
- try {
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
- // Find the generated folder containing Cargo.toml
1116
- const items = fs.readdirSync(tempBase);
1117
- for (const item of items) {
1118
- const itemPath = path.join(tempBase, item);
1119
- if (
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
- if (!projectDir) {
1129
- return {
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: Invalid arguments. You must provide either 'project_dir' or 'nodejs_script'.`,
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
- if (!fs.existsSync(path.join(projectDir, "Cargo.toml"))) {
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 specified directory is not a valid Rust project. 'Cargo.toml' was not found in: ${projectDir}`,
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 packed Node.js generator script is required.",
18
+ "❌ Error: Path to the generated Markdown project file is required.",
15
19
  );
16
- console.error("Usage: scad-gltf play <path_to_generated_project.js>");
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 script to temporary directory...\n ${tempDir}`);
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
- // 2. Execute the packed Node.js script inside the temp directory
58
- console.log(
59
- `\n⚙️ Running generator script: ${path.basename(absScriptPath)}`,
60
- );
61
- execSync(`node "${absScriptPath}"`, { cwd: tempDir, stdio: "inherit" });
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 script did not produce a folder containing 'project.godot', 'package.json', or 'Cargo.toml'.",
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 generated ${projectType.toUpperCase()} project: ${path.basename(projectDir)}`,
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(" play Test and launch generated Node.js scripts");
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scad-gltf",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
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",
@@ -29,6 +29,7 @@
29
29
  "bin",
30
30
  "src",
31
31
  "godot",
32
+ "rust",
32
33
  "editor/dist"
33
34
  ],
34
35
  "bin": {
@@ -43,9 +44,10 @@
43
44
  "./openscad.wasm": "./src/ext/openscad.wasm"
44
45
  },
45
46
  "scripts": {
47
+ "test": "node --test 'test/**/*.test.js'",
46
48
  "format": "prettier --write .",
47
49
  "mcp": "npx @modelcontextprotocol/inspector node ./bin/mcp.js",
48
- "prepublishOnly": "npm install --prefix editor && npm run build --prefix editor"
50
+ "prepublishOnly": "npm test && npm install --prefix editor && npm run build --prefix editor"
49
51
  },
50
52
  "dependencies": {
51
53
  "@modelcontextprotocol/sdk": "^1.31.0",
package/rust/build.rs ADDED
@@ -0,0 +1,22 @@
1
+ use std::process::Command;
2
+
3
+ fn main() {
4
+ // Tell Cargo to re-run this script only if the 'scad' directory changes
5
+ println!("cargo::rerun-if-changed=scad");
6
+
7
+ // Ensure cross-platform compatibility for npm global binaries
8
+ let cmd = if cfg!(target_os = "windows") {
9
+ "scad-gltf.cmd"
10
+ } else {
11
+ "scad-gltf"
12
+ };
13
+
14
+ let status = Command::new(cmd)
15
+ .args(["convert", "./scad", "./assets/models", "--cache"])
16
+ .status()
17
+ .expect("Failed to execute scad-gltf. Is scad-gltf installed globally?");
18
+
19
+ if !status.success() {
20
+ panic!("scad-gltf convert failed with status: {}", status);
21
+ }
22
+ }
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
- const outputFilename = `generate_${projectType}_project.js`;
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: "system", content: systemPrompt },
246
- { role: "user", content: inputRequest },
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
- ` Generating project files to preview in a temporary folder...`,
563
+ ` Extracting Markdown project files to temporary folder...`,
513
564
  );
514
- console.log(` Temp script: ${tempFilePath}`);
565
+ const files = extractFilesFromMarkdown(finalCode);
515
566
 
516
- execSync(`node "${tempFilePath}"`, {
517
- cwd: tempDir,
518
- stdio: "pipe",
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 script successfully executed, but no valid project directory was found. Make sure your script creates a root folder and generates the correct required files (like ${expectedFile}) inside it.`,
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: node ${extractedFilename}`,
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" : "Node.js script"}.`,
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" : "Node.js script"}.`,
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) {
@@ -1,8 +1,8 @@
1
- import fs from "fs";
2
- import path from "path";
3
- import os from "os";
4
- import { execSync, spawn } from "child_process";
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}"`, { stdio: "ignore" });
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}"`, { stdio: "ignore" });
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
  }
@@ -153,7 +157,7 @@ export function runGodotAsync(args, cwd, timeoutMs) {
153
157
  spawnBin = "xvfb-run";
154
158
  }
155
159
 
156
- const godotProcess = spawn(spawnBin, spawnArgs, { cwd, env });
160
+ const godotProcess = cp.spawn(spawnBin, spawnArgs, { cwd, env });
157
161
 
158
162
  godotProcess.stdout.on("data", (data) => {
159
163
  output += data.toString();
@@ -16,7 +16,7 @@ function readFile(filePath) {
16
16
  }
17
17
 
18
18
  function formatMarkdownFile(displayPath, lang, content) {
19
- return `### ${displayPath}\n---\n\`\`\`${lang}\n${content}\n\`\`\`\n\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
- 1. 3D Assets (.scad):
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
- 3. Delivery Format (Single Node.js Script):
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
- 1. 3D Web Assets (.scad):
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
- 3. Delivery Format (Single Node.js Script):
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
- 1. 3D Assets (.scad):
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
- 3. Delivery Format (Single Node.js Script):
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