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.
- package/README.md +50 -3
- package/bevy/README.md +46 -0
- package/bevy/examples/LICENSE +21 -0
- package/bevy/examples/README.md +58 -0
- package/bevy/examples/package.json +5 -0
- package/bevy/examples/packman_3d.js +2415 -0
- package/bin/scad-bevy.js +74 -0
- package/bin/scad-gen.js +25 -0
- package/bin/scad-godot.js +65 -222
- package/bin/scad-mcp.js +641 -2
- package/bin/scad-play.js +172 -0
- package/bin/scad-web.js +14 -166
- package/package.json +7 -2
- package/src/cli-utils.js +792 -0
- package/src/godot-utils.js +179 -0
package/bin/scad-play.js
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import os from "node:os";
|
|
6
|
+
import { execSync, spawn } from "node:child_process";
|
|
7
|
+
import { getGodotBin } from "../src/godot-utils.js";
|
|
8
|
+
|
|
9
|
+
function main() {
|
|
10
|
+
const packedScriptPath = process.argv[2];
|
|
11
|
+
|
|
12
|
+
if (!packedScriptPath) {
|
|
13
|
+
console.error(
|
|
14
|
+
"❌ Error: Path to the packed Node.js generator script is required.",
|
|
15
|
+
);
|
|
16
|
+
console.error(
|
|
17
|
+
"Usage: node bin/scad-play.js <path_to_generated_project.js>",
|
|
18
|
+
);
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const absScriptPath = path.resolve(process.cwd(), packedScriptPath);
|
|
23
|
+
if (!fs.existsSync(absScriptPath)) {
|
|
24
|
+
console.error(`❌ Error: File not found: ${absScriptPath}`);
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// 1. Create a unique temporary directory
|
|
29
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "scad-play-"));
|
|
30
|
+
console.log(`📦 Unpacking script to temporary directory...\n ${tempDir}`);
|
|
31
|
+
|
|
32
|
+
let appProcess = null;
|
|
33
|
+
|
|
34
|
+
// Ensure cleanup happens on exit
|
|
35
|
+
const cleanup = () => {
|
|
36
|
+
if (fs.existsSync(tempDir)) {
|
|
37
|
+
console.log(`\n🧹 Cleaning up temporary directory...`);
|
|
38
|
+
try {
|
|
39
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
40
|
+
} catch (e) {
|
|
41
|
+
console.error(`⚠️ Failed to fully clean up temp dir: ${e.message}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
process.on("SIGINT", () => {
|
|
47
|
+
if (appProcess) appProcess.kill("SIGINT");
|
|
48
|
+
cleanup();
|
|
49
|
+
process.exit(0);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
process.on("SIGTERM", () => {
|
|
53
|
+
if (appProcess) appProcess.kill("SIGTERM");
|
|
54
|
+
cleanup();
|
|
55
|
+
process.exit(0);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
// 2. Execute the packed Node.js script inside the temp directory
|
|
60
|
+
console.log(
|
|
61
|
+
`\n⚙️ Running generator script: ${path.basename(absScriptPath)}`,
|
|
62
|
+
);
|
|
63
|
+
execSync(`node "${absScriptPath}"`, { cwd: tempDir, stdio: "inherit" });
|
|
64
|
+
|
|
65
|
+
// 3. Find the resulting project folder and determine its type
|
|
66
|
+
let projectDir = null;
|
|
67
|
+
let projectType = null; // 'godot' | 'web'
|
|
68
|
+
let latestTime = 0;
|
|
69
|
+
|
|
70
|
+
const items = fs.readdirSync(tempDir);
|
|
71
|
+
for (const item of items) {
|
|
72
|
+
const itemPath = path.join(tempDir, item);
|
|
73
|
+
if (fs.statSync(itemPath).isDirectory()) {
|
|
74
|
+
const isGodot = fs.existsSync(path.join(itemPath, "project.godot"));
|
|
75
|
+
const isWeb = fs.existsSync(path.join(itemPath, "package.json"));
|
|
76
|
+
const isBevy = fs.existsSync(path.join(itemPath, "Cargo.toml"));
|
|
77
|
+
|
|
78
|
+
if (isGodot || isWeb || isBevy) {
|
|
79
|
+
const mtime = fs.statSync(itemPath).mtimeMs;
|
|
80
|
+
if (mtime > latestTime) {
|
|
81
|
+
latestTime = mtime;
|
|
82
|
+
projectDir = itemPath;
|
|
83
|
+
projectType = isGodot ? "godot" : isBevy ? "bevy" : "web";
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (!projectDir || !projectType) {
|
|
90
|
+
console.error("\n❌ Error: Could not find a valid generated project.");
|
|
91
|
+
console.error(
|
|
92
|
+
"The script did not produce a folder containing 'project.godot' or 'package.json'.",
|
|
93
|
+
);
|
|
94
|
+
cleanup();
|
|
95
|
+
process.exit(1);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
console.log(
|
|
99
|
+
`\n✔️ Found generated ${projectType.toUpperCase()} project: ${path.basename(projectDir)}`,
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
// 4. Boot the corresponding environment
|
|
103
|
+
if (projectType === "godot") {
|
|
104
|
+
const godotBin = getGodotBin();
|
|
105
|
+
|
|
106
|
+
console.log(`\n⚙️ Importing Godot assets (this may take a moment)...`);
|
|
107
|
+
try {
|
|
108
|
+
// Force Godot to build the .godot/imported cache before attempting to run the game
|
|
109
|
+
execSync(`"${godotBin}" --headless --editor --quit`, {
|
|
110
|
+
cwd: projectDir,
|
|
111
|
+
stdio: "inherit",
|
|
112
|
+
});
|
|
113
|
+
} catch (e) {
|
|
114
|
+
console.log(
|
|
115
|
+
"⚠️ Asset import finished with non-zero exit code (continuing...)",
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
console.log(`\n🎮 Launching Godot Engine...`);
|
|
120
|
+
appProcess = spawn(godotBin, ["--path", projectDir], {
|
|
121
|
+
stdio: "inherit",
|
|
122
|
+
});
|
|
123
|
+
} else if (projectType === "web") {
|
|
124
|
+
console.log(
|
|
125
|
+
`\n🌐 Installing NPM dependencies (this may take a moment)...`,
|
|
126
|
+
);
|
|
127
|
+
try {
|
|
128
|
+
execSync("npm install", { cwd: projectDir, stdio: "inherit" });
|
|
129
|
+
} catch (e) {
|
|
130
|
+
console.log(
|
|
131
|
+
"⚠️ npm install failed or showed errors, attempting to continue anyway...",
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
console.log(`\n🌐 Starting Vite dev server...`);
|
|
136
|
+
const npmCmd = process.platform === "win32" ? "npm.cmd" : "npm";
|
|
137
|
+
|
|
138
|
+
appProcess = spawn(npmCmd, ["run", "dev"], {
|
|
139
|
+
cwd: projectDir,
|
|
140
|
+
stdio: "inherit",
|
|
141
|
+
});
|
|
142
|
+
} else if (projectType === "bevy") {
|
|
143
|
+
console.log(
|
|
144
|
+
`\n🦀 Compiling and starting Rust Bevy game (this may take a moment)...`,
|
|
145
|
+
);
|
|
146
|
+
const cargoCmd = process.platform === "win32" ? "cargo.exe" : "cargo";
|
|
147
|
+
appProcess = spawn(cargoCmd, ["run"], {
|
|
148
|
+
cwd: projectDir,
|
|
149
|
+
stdio: "inherit",
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// 5. Handle lifecycle events
|
|
154
|
+
appProcess.on("close", (code) => {
|
|
155
|
+
console.log(`\n🏁 Process exited with code ${code}.`);
|
|
156
|
+
cleanup();
|
|
157
|
+
process.exit(code || 0);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
appProcess.on("error", (err) => {
|
|
161
|
+
console.error(`\n❌ Failed to start the application: ${err.message}`);
|
|
162
|
+
cleanup();
|
|
163
|
+
process.exit(1);
|
|
164
|
+
});
|
|
165
|
+
} catch (err) {
|
|
166
|
+
console.error(`\n❌ An error occurred during execution:\n`, err.message);
|
|
167
|
+
cleanup();
|
|
168
|
+
process.exit(1);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
main();
|
package/bin/scad-web.js
CHANGED
|
@@ -1,141 +1,17 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import
|
|
4
|
-
import path from "node:path";
|
|
5
|
-
import url from "node:url";
|
|
6
|
-
import readline from "node:readline";
|
|
7
|
-
|
|
8
|
-
// Safely resolve symlinks to find the actual package directory
|
|
9
|
-
const __filename = fs.realpathSync(url.fileURLToPath(import.meta.url));
|
|
10
|
-
const __dirname = path.dirname(__filename);
|
|
11
|
-
const DIR = path.resolve(__dirname, "..");
|
|
12
|
-
|
|
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
|
-
}
|
|
3
|
+
import { runCliApp } from "../src/cli-utils.js";
|
|
66
4
|
|
|
67
5
|
async function main() {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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-web "<description of the web app to generate>" [options_json]',
|
|
88
|
-
);
|
|
89
|
-
console.error(' or: echo "<description>" | scad-web [options_json]');
|
|
90
|
-
console.error("");
|
|
91
|
-
console.error("Example with JSON options:");
|
|
92
|
-
console.error(
|
|
93
|
-
' scad-web "3D Car Configurator Web App" \'{"animation": false}\'',
|
|
94
|
-
);
|
|
95
|
-
process.exit(1);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
// 2. Parse Options JSON
|
|
99
|
-
let options = {};
|
|
100
|
-
if (optionsStr) {
|
|
101
|
-
try {
|
|
102
|
-
options = JSON.parse(optionsStr);
|
|
103
|
-
} catch (e) {
|
|
104
|
-
console.error(`Invalid JSON options: ${optionsStr}`);
|
|
105
|
-
process.exit(1);
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// Disable the modelName instructions specifically for this wrapper context
|
|
110
|
-
options.modelName = false;
|
|
111
|
-
|
|
112
|
-
// 3. Dynamically import and generate prompt rules from src/prompt.js
|
|
113
|
-
let generatePrompt;
|
|
114
|
-
try {
|
|
115
|
-
const promptJsPath = path.join(DIR, "src", "prompt.js");
|
|
116
|
-
const promptModuleUrl = url.pathToFileURL(promptJsPath).href;
|
|
117
|
-
const m = await import(promptModuleUrl);
|
|
118
|
-
generatePrompt = m.generatePrompt;
|
|
119
|
-
} catch (e) {
|
|
120
|
-
console.error(`Error loading ${path.join("src", "prompt.js")}:`, e);
|
|
121
|
-
process.exit(1);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
let promptRules = "";
|
|
125
|
-
try {
|
|
126
|
-
promptRules = generatePrompt("the 3D assets for the web app", options);
|
|
127
|
-
} catch (e) {
|
|
128
|
-
console.error("Error generating prompt rules from prompt.js:");
|
|
129
|
-
console.error(e);
|
|
130
|
-
process.exit(1);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// 4. Construct the Main Web Prompt System Text (System Instructions)
|
|
134
|
-
const systemPrompt = `You are an expert Web 3D developer and procedural 3D technical artist.
|
|
6
|
+
await runCliApp({
|
|
7
|
+
projectType: "web",
|
|
8
|
+
buildSystemPrompt: (
|
|
9
|
+
promptRules,
|
|
10
|
+
) => `You are an expert Web 3D developer and procedural 3D technical artist.
|
|
135
11
|
|
|
136
12
|
What to generate:
|
|
137
13
|
1. 3D Web Assets (.scad):
|
|
138
|
-
- Generate procedural 3D models for the
|
|
14
|
+
- Generate procedural 3D models for the game using OpenSCAD.
|
|
139
15
|
- 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.
|
|
140
16
|
- 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:
|
|
141
17
|
|
|
@@ -145,14 +21,14 @@ ${promptRules}
|
|
|
145
21
|
|
|
146
22
|
2. Vite Web Project Files (npm based):
|
|
147
23
|
- Create the necessary files for a modern web application (e.g., \`package.json\`, \`index.html\`, \`main.js\`).
|
|
148
|
-
- You can use ANY web 3D library with glTF support (e.g., Three.js, Babylon.js, @google/model-viewer, PlayCanvas, A-Frame, etc.) that fits the
|
|
24
|
+
- You can use ANY web 3D library with glTF support (e.g., Three.js, Babylon.js, @google/model-viewer, PlayCanvas, A-Frame, etc.) that fits the game's requirements.
|
|
149
25
|
- In \`package.json\`, you MUST include the scad to gltf converter tool as a dev dependency:
|
|
150
26
|
\`"scad-gltf": "^0.1.0"\`
|
|
151
27
|
- In \`package.json\`, add npm scripts to automatically compile the \`.scad\` files into \`.glb\` format inside the \`public/\` folder before Vite runs its dev or build steps.
|
|
152
28
|
For example:
|
|
153
29
|
\`"predev": "scad-convert ./scad ./public/models --cache"\`
|
|
154
30
|
\`"prebuild": "scad-convert ./scad ./public/models --cache"\`
|
|
155
|
-
- Write the core
|
|
31
|
+
- Write the core game logic to load and display the converted \`.glb\` files interactively.
|
|
156
32
|
|
|
157
33
|
3. Delivery Format (Single Node.js Script):
|
|
158
34
|
- Output exactly ONE self-contained Node.js script. Do not output manual setup instructions.
|
|
@@ -161,39 +37,11 @@ ${promptRules}
|
|
|
161
37
|
- The script must embed and write:
|
|
162
38
|
- Your generated \`.scad\` 3D assets.
|
|
163
39
|
- Your generated Vite web project files.
|
|
164
|
-
- Ensure all string file contents inside the Node.js script are properly escaped
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
// 6. Write to System Clipboard (Part 1: System Instructions)
|
|
170
|
-
try {
|
|
171
|
-
await writeToClipboard(systemPrompt);
|
|
172
|
-
console.log("✔️ System instructions have been copied to the clipboard.");
|
|
173
|
-
} catch (err) {
|
|
174
|
-
console.error(
|
|
175
|
-
"Error: Failed to copy system instructions to the clipboard.",
|
|
176
|
-
);
|
|
177
|
-
console.error(err.message);
|
|
178
|
-
process.exit(1);
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
// 7. Await user confirmation
|
|
182
|
-
await waitForEnter(
|
|
183
|
-
"Please paste the system instructions into your LLM, then press ENTER to copy your input request...",
|
|
184
|
-
);
|
|
185
|
-
|
|
186
|
-
// 8. Write to System Clipboard (Part 2: Input Request)
|
|
187
|
-
try {
|
|
188
|
-
await writeToClipboard(inputRequestOutput);
|
|
189
|
-
console.log(
|
|
190
|
-
"✔️ Input request has been copied to the clipboard. You can now paste it into your LLM.",
|
|
191
|
-
);
|
|
192
|
-
} catch (err) {
|
|
193
|
-
console.error("Error: Failed to copy input request to the clipboard.");
|
|
194
|
-
console.error(err.message);
|
|
195
|
-
process.exit(1);
|
|
196
|
-
}
|
|
40
|
+
- Ensure all string file contents inside the Node.js script are properly escaped.`,
|
|
41
|
+
buildInputRequest: (task) =>
|
|
42
|
+
`Design and implement a web-based 3D glTF game using Vite for the following concept: "${task}"`,
|
|
43
|
+
allowedTools: ["render_scad_model"],
|
|
44
|
+
});
|
|
197
45
|
}
|
|
198
46
|
|
|
199
47
|
// Execute and handle unhandled runtime errors
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "scad-gltf",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.2",
|
|
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",
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
"3d",
|
|
23
23
|
"cad",
|
|
24
24
|
"godot",
|
|
25
|
+
"bevy",
|
|
25
26
|
"mcp",
|
|
26
27
|
"ai"
|
|
27
28
|
],
|
|
@@ -29,6 +30,7 @@
|
|
|
29
30
|
"bin",
|
|
30
31
|
"src",
|
|
31
32
|
"godot",
|
|
33
|
+
"bevy",
|
|
32
34
|
"editor/dist"
|
|
33
35
|
],
|
|
34
36
|
"bin": {
|
|
@@ -36,7 +38,10 @@
|
|
|
36
38
|
"scad-serve": "./bin/scad-serve.js",
|
|
37
39
|
"scad-mcp": "./bin/scad-mcp.js",
|
|
38
40
|
"scad-godot": "./bin/scad-godot.js",
|
|
39
|
-
"scad-web": "./bin/scad-web.js"
|
|
41
|
+
"scad-web": "./bin/scad-web.js",
|
|
42
|
+
"scad-bevy": "./bin/scad-bevy.js",
|
|
43
|
+
"scad-gen": "./bin/scad-gen.js",
|
|
44
|
+
"scad-play": "./bin/scad-play.js"
|
|
40
45
|
},
|
|
41
46
|
"exports": {
|
|
42
47
|
".": "./src/ext/openscad.js",
|