scad-gltf 0.1.4 → 0.2.2

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.
@@ -0,0 +1,74 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { runCliApp } from "../src/cli-utils.js";
4
+
5
+ async function main() {
6
+ await runCliApp({
7
+ projectType: "bevy",
8
+ buildSystemPrompt: (
9
+ promptRules,
10
+ ) => `You are an expert Rust Bevy engine game developer and procedural 3D technical artist.
11
+
12
+ NAMING CONVENTION REQUIREMENT:
13
+ - All generated files, directories, models, scripts, and root folders MUST strictly use snake_case (lowercase with underscores, e.g. \`player_character.rs\`, \`enemy_walker.scad\`).
14
+ - NEVER use hyphens/minus signs (\`-\`) or spaces in any file or folder names.
15
+
16
+ What to generate:
17
+ 1. 3D Game Assets (.scad):
18
+ - Generate procedural 3D models for the game using OpenSCAD.
19
+ - 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.
20
+ - 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:
21
+
22
+ === OPENSCAD SYNTAX RULES ===
23
+ ${promptRules}
24
+ =============================
25
+
26
+ 2. Rust Bevy Project Files:
27
+ - Create the necessary files for a modern Rust Bevy engine application (e.g., \`Cargo.toml\`, \`build.rs\`, \`src/main.rs\`).
28
+ - In \`Cargo.toml\`, include \`bevy\` as a dependency.
29
+ - You MUST include this EXACT \`build.rs\` script at the root of the project to automatically compile the \`.scad\` files into \`.glb\` format inside the \`assets/models\` folder before running the game via Cargo:
30
+ \`\`\`rust
31
+ use std::process::Command;
32
+
33
+ fn main() {
34
+ // Tell Cargo to re-run this script only if the 'scad' directory changes
35
+ println!("cargo::rerun-if-changed=scad");
36
+
37
+ // Ensure cross-platform compatibility for npm global binaries
38
+ let cmd = if cfg!(target_os = "windows") {
39
+ "scad-convert.cmd"
40
+ } else {
41
+ "scad-convert"
42
+ };
43
+
44
+ let status = Command::new(cmd)
45
+ .args(["./scad", "./assets/models", "--cache"])
46
+ .status()
47
+ .expect("Failed to execute scad-convert. Is scad-gltf installed globally?");
48
+
49
+ if !status.success() {
50
+ panic!("scad-convert failed with status: {}", status);
51
+ }
52
+ }
53
+ \`\`\`
54
+ - Write the core application logic in \`src/main.rs\` to load and display the converted \`.glb\` files interactively. Provide standard Bevy game systems (camera, lights, movement, etc.).
55
+
56
+ 3. Delivery Format (Single Node.js Script):
57
+ - Output exactly ONE self-contained Node.js script. Do not output manual setup instructions.
58
+ - 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.
59
+ - When executed, this script must programmatically create the entire project directory structure and write all the files to disk using the \`fs\` module.
60
+ - The script must embed and write:
61
+ - Your generated \`.scad\` 3D assets.
62
+ - Your generated Rust Bevy project files (\`Cargo.toml\`, \`build.rs\`, \`src/main.rs\`).
63
+ - Ensure all string file contents inside the Node.js script are properly escaped.`,
64
+ buildInputRequest: (task) =>
65
+ `Design and implement a Rust Bevy engine game for the following concept: "${task}"`,
66
+ allowedTools: ["render_scad_model", "compile_bevy_project"],
67
+ });
68
+ }
69
+
70
+ // Execute and handle unhandled runtime errors
71
+ main().catch((err) => {
72
+ console.error("An unexpected error occurred:", err);
73
+ process.exit(1);
74
+ });
@@ -0,0 +1,25 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { runCliApp } from "../src/cli-utils.js";
4
+
5
+ async function main() {
6
+ await runCliApp({
7
+ projectType: "gen",
8
+ buildSystemPrompt: () =>
9
+ `You are an expert procedural 3D technical artist and OpenSCAD developer.
10
+ Your goal is to generate a single OpenSCAD (.scad) file based on the user's request.
11
+
12
+ CRITICAL WORKFLOW:
13
+ 1. Write the OpenSCAD code using the custom syntax rules provided in the request.
14
+ 2. Call the \`render_scad_model\` tool with your code to visually verify your design.
15
+ 3. If the model looks incorrect, adjust your code and re-render. Iterate until perfect.
16
+ 4. Provide your final OpenSCAD code in a standard markdown block (\`\`\`openscad).`,
17
+ allowedTools: ["render_scad_model"],
18
+ });
19
+ }
20
+
21
+ // Execute and handle unhandled runtime errors
22
+ main().catch((err) => {
23
+ console.error("An unexpected error occurred:", err);
24
+ process.exit(1);
25
+ });
package/bin/scad-godot.js CHANGED
@@ -3,147 +3,20 @@
3
3
  import fs from "node:fs";
4
4
  import path from "node:path";
5
5
  import url from "node:url";
6
- import readline from "node:readline";
6
+ import { runCliApp } from "../src/cli-utils.js";
7
7
 
8
- // Safely resolve symlinks to find the actual package directory
9
- const __filename = fs.realpathSync(url.fileURLToPath(import.meta.url));
8
+ const __filename = url.fileURLToPath(import.meta.url);
10
9
  const __dirname = path.dirname(__filename);
11
10
  const DIR = path.resolve(__dirname, "..");
12
11
 
13
- // Safely detect if actual data is being piped into the script via STDIN
14
- function hasStdinData() {
15
- try {
16
- const stat = fs.fstatSync(0); // 0 is the file descriptor for STDIN
17
- // isFIFO means piped (echo "foo" | script)
18
- // isFile means redirected (script < foo.txt)
19
- return stat.isFIFO() || stat.isFile();
20
- } catch (e) {
21
- return false;
22
- }
23
- }
24
-
25
- // Writes text to system clipboard
26
- async function writeToClipboard(text) {
27
- const clipboardy = (await import("clipboardy")).default;
28
- await clipboardy.write(text);
29
- }
30
-
31
- function waitForEnter(message) {
32
- return new Promise((resolve) => {
33
- // If standard input was piped/redirected, we need to bypass it and read from the actual terminal
34
- if (!process.stdin.isTTY) {
35
- try {
36
- const tty = process.platform === "win32" ? "CONIN$" : "/dev/tty";
37
- const fd = fs.openSync(tty, "rs");
38
- process.stdout.write(message);
39
- const buf = Buffer.alloc(1);
40
- fs.readSync(fd, buf, 0, 1, null);
41
- fs.closeSync(fd);
42
- console.log();
43
- resolve();
44
- return;
45
- } catch (e) {
46
- console.log(
47
- message +
48
- " (Auto-continuing due to non-interactive terminal environment)",
49
- );
50
- resolve();
51
- return;
52
- }
53
- }
54
-
55
- // For standard TTY terminals
56
- const rl = readline.createInterface({
57
- input: process.stdin,
58
- output: process.stdout,
59
- });
60
- rl.question(message, () => {
61
- rl.close();
62
- resolve();
63
- });
64
- });
65
- }
66
-
67
12
  async function main() {
68
- let task = "";
69
- let optionsStr = "{}";
70
-
71
- // 1. Read TASK and OPTIONS
72
- if (hasStdinData()) {
73
- try {
74
- task = fs.readFileSync(0, "utf-8").trim();
75
- } catch (e) {
76
- console.error("Error reading from STDIN:", e);
77
- }
78
- if (process.argv[2]) optionsStr = process.argv[2];
79
- } else {
80
- if (process.argv[2]) task = process.argv[2];
81
- if (process.argv[3]) optionsStr = process.argv[3];
82
- }
83
-
84
- if (!task) {
85
- console.error("Error: Task parameter is required.");
86
- console.error(
87
- 'Usage: scad-godot "<description of the game to generate>" [options_json]',
88
- );
89
- console.error(' or: echo "<description>" | scad-godot [options_json]');
90
- console.error("");
91
- console.error("Example with JSON options:");
92
- console.error(
93
- ' scad-godot "Game description" \'{"animation": false, "bakeColors": true, "scadFiles": ["player.scad"]}\'',
94
- );
95
- process.exit(1);
96
- }
97
-
98
- // 2. Parse Options JSON
99
- // Disable heavy PBR features by default
100
- let options = {
101
- transmission: false,
102
- clearcoat: false,
103
- sheen: false,
104
- iridescence: false,
105
- scadFiles: [],
106
- };
107
-
108
- if (optionsStr) {
109
- try {
110
- const parsed = JSON.parse(optionsStr);
111
- options = { ...options, ...parsed }; // User provided options override defaults
112
- } catch (e) {
113
- console.error(`Invalid JSON options: ${optionsStr}`);
114
- process.exit(1);
115
- }
116
- }
117
-
118
- // Disable the modelName instructions specifically for the Godot wrapper context
119
- options.modelName = false;
120
-
121
- // 3. Dynamically import and generate prompt rules from src/prompt.js
122
- let generatePrompt;
123
- try {
124
- const promptJsPath = path.join(DIR, "src", "prompt.js");
125
- const promptModuleUrl = url.pathToFileURL(promptJsPath).href;
126
- const m = await import(promptModuleUrl);
127
- generatePrompt = m.generatePrompt;
128
- } catch (e) {
129
- console.error(`Error loading ${path.join("src", "prompt.js")}:`, e);
130
- process.exit(1);
131
- }
13
+ await runCliApp({
14
+ projectType: "godot",
15
+ buildSystemPrompt: (promptRules, options) => {
16
+ const hasUserScadFiles =
17
+ Array.isArray(options.scadFiles) && options.scadFiles.length > 0;
132
18
 
133
- let promptRules = "";
134
- try {
135
- promptRules = generatePrompt("the 3D assets for the game", options);
136
- } catch (e) {
137
- console.error("Error generating prompt rules from prompt.js:");
138
- console.error(e);
139
- process.exit(1);
140
- }
141
-
142
- const hasUserScadFiles =
143
- Array.isArray(options.scadFiles) && options.scadFiles.length > 0;
144
-
145
- // 4. Construct the Main Godot Prompt System Text (System Instructions)
146
- const systemPrompt = `You are an expert Godot 4 game developer and procedural 3D technical artist.
19
+ const systemPrompt = `You are an expert Godot 4 game developer and procedural 3D technical artist.
147
20
 
148
21
  NAMING CONVENTION REQUIREMENT:
149
22
  - All generated files, directories, models, scripts, scenes, and root folders MUST strictly use snake_case (lowercase with underscores, e.g. \`player_character.gd\`, \`main_scene.tscn\`, \`enemy_walker.scad\`).
@@ -194,100 +67,70 @@ ${promptRules}
194
67
  ? "\n - The exact source code of the provided user `.scad` files, placed in the appropriate project folders."
195
68
  : ""
196
69
  }
197
- - Ensure all string file contents inside the Node.js script are properly escaped.`;
198
-
199
- // 5. Gather Addon Files content
200
- const addonDir = path.join(DIR, "godot", "addons", "scad_importer");
201
- let addonFiles = [];
202
- try {
203
- if (fs.existsSync(addonDir)) {
204
- const files = fs.readdirSync(addonDir);
205
- for (const file of files) {
206
- const fullPath = path.join(addonDir, file);
207
- // Ensure we only read text files to prevent binary/hidden files
208
- // from introducing control characters that crash browser UIs.
209
- if (
210
- fs.statSync(fullPath).isFile() &&
211
- (file.endsWith(".gd") || file.endsWith(".cfg"))
212
- ) {
213
- addonFiles.push(fullPath);
214
- }
215
- }
216
- }
217
- } catch (e) {
218
- console.error("Warning: Could not read addon directory.", e);
219
- }
220
-
221
- // 6. Format the unified system instructions clipboard output
222
- let systemClipboardOutput = `${systemPrompt}\n\n`;
223
-
224
- for (const file of addonFiles) {
225
- try {
226
- // Normalize line endings to avoid mixed line-ending layout loops in web editors
227
- const content = fs.readFileSync(file, "utf-8").replace(/\r\n/g, "\n");
228
- // Format to use relative paths and force forward slashes for LLM clarity
229
- const relativePath = path.relative(DIR, file).replace(/\\/g, "/");
230
-
231
- // Add explicit language tags to prevent catastrophic regex backtracking
232
- // during the Markdown parser's language auto-detection step.
233
- const lang = file.endsWith(".gd") ? "gdscript" : "text";
234
- systemClipboardOutput += `### ${relativePath}\n---\n\`\`\`${lang}\n${content}\n\`\`\`\n\n`;
235
- } catch (e) {
236
- console.error(`Warning: Skipping '${file}'. It is not a readable file.`);
237
- }
238
- }
70
+ - Ensure all string file contents inside the Node.js script are properly escaped.
71
+ - TESTING CAPABILITY: You have access to the \`test_godot_project\` tool. If you want to verify your code before outputting your final response, you can pass your complete generated Node.js script as the \`nodejs_script\` parameter. The server will execute it in a temporary folder and run the Godot tests automatically.`;
239
72
 
240
- if (hasUserScadFiles) {
241
- systemClipboardOutput += `=== USER PROVIDED OPENSCAD FILES ===\n`;
242
- systemClipboardOutput += `The following .scad files are provided as reference or base assets. You MUST embed and write them into the generated project, modifying them if necessary to fit the game logic.\n\n`;
243
- for (const file of options.scadFiles) {
73
+ // Gather Addon Files content
74
+ const addonDir = path.join(DIR, "godot", "addons", "scad_importer");
75
+ let addonFiles = [];
244
76
  try {
245
- const content = fs.readFileSync(file, "utf-8").replace(/\r\n/g, "\n");
246
- const relativePath = path.isAbsolute(file)
247
- ? path.relative(process.cwd(), file).replace(/\\/g, "/")
248
- : file.replace(/\\/g, "/");
249
- systemClipboardOutput += `### ${relativePath}\n---\n\`\`\`openscad\n${content}\n\`\`\`\n\n`;
77
+ if (fs.existsSync(addonDir)) {
78
+ const files = fs.readdirSync(addonDir);
79
+ for (const file of files) {
80
+ const fullPath = path.join(addonDir, file);
81
+ if (
82
+ fs.statSync(fullPath).isFile() &&
83
+ (file.endsWith(".gd") || file.endsWith(".cfg"))
84
+ ) {
85
+ addonFiles.push(fullPath);
86
+ }
87
+ }
88
+ }
250
89
  } catch (e) {
251
- console.error(
252
- `Warning: Skipping user SCAD file '${file}'. It is not a readable file.`,
253
- );
90
+ console.error("Warning: Could not read addon directory.", e);
254
91
  }
255
- }
256
- }
257
-
258
- systemClipboardOutput = systemClipboardOutput.trimEnd() + "\n";
259
92
 
260
- // 7. Format the input request output
261
- const inputRequestOutput = `Design and implement a Godot 4 project for the following game concept: "${task}"`;
262
-
263
- // 8. Write to System Clipboard (Part 1: System Instructions)
264
- try {
265
- await writeToClipboard(systemClipboardOutput);
266
- console.log("✔️ System instructions have been copied to the clipboard.");
267
- } catch (err) {
268
- console.error(
269
- "Error: Failed to copy system instructions to the clipboard.",
270
- );
271
- console.error(err.message);
272
- process.exit(1);
273
- }
93
+ let systemClipboardOutput = `${systemPrompt}\n\n`;
94
+
95
+ for (const file of addonFiles) {
96
+ try {
97
+ const content = fs.readFileSync(file, "utf-8").replace(/\r\n/g, "\n");
98
+ const relativePath = path.relative(DIR, file).replace(/\\/g, "/");
99
+ const lang = file.endsWith(".gd") ? "gdscript" : "text";
100
+ systemClipboardOutput += `### ${relativePath}\n---\n\`\`\`${lang}\n${content}\n\`\`\`\n\n`;
101
+ } catch (e) {
102
+ console.error(
103
+ `Warning: Skipping '${file}'. It is not a readable file.`,
104
+ );
105
+ }
106
+ }
274
107
 
275
- // 9. Await user confirmation
276
- await waitForEnter(
277
- "Please paste the system instructions into your LLM, then press ENTER to copy your input request...",
278
- );
108
+ if (hasUserScadFiles) {
109
+ systemClipboardOutput += `=== USER PROVIDED OPENSCAD FILES ===\n`;
110
+ systemClipboardOutput += `The following .scad files are provided as reference or base assets. You MUST embed and write them into the generated project, modifying them if necessary to fit the game logic.\n\n`;
111
+ for (const file of options.scadFiles) {
112
+ try {
113
+ const content = fs
114
+ .readFileSync(file, "utf-8")
115
+ .replace(/\r\n/g, "\n");
116
+ const relativePath = path.isAbsolute(file)
117
+ ? path.relative(process.cwd(), file).replace(/\\/g, "/")
118
+ : file.replace(/\\/g, "/");
119
+ systemClipboardOutput += `### ${relativePath}\n---\n\`\`\`openscad\n${content}\n\`\`\`\n\n`;
120
+ } catch (e) {
121
+ console.error(
122
+ `Warning: Skipping user SCAD file '${file}'. It is not a readable file.`,
123
+ );
124
+ }
125
+ }
126
+ }
279
127
 
280
- // 10. Write to System Clipboard (Part 2: Input Request)
281
- try {
282
- await writeToClipboard(inputRequestOutput);
283
- console.log(
284
- "✔️ Input request has been copied to the clipboard. You can now paste it into your LLM.",
285
- );
286
- } catch (err) {
287
- console.error("Error: Failed to copy input request to the clipboard.");
288
- console.error(err.message);
289
- process.exit(1);
290
- }
128
+ return systemClipboardOutput.trimEnd() + "\n";
129
+ },
130
+ buildInputRequest: (task) =>
131
+ `Design and implement a Godot 4 project for the following game concept: "${task}"`,
132
+ allowedTools: ["render_scad_model", "test_godot_project"],
133
+ });
291
134
  }
292
135
 
293
136
  // Execute and handle unhandled runtime errors