scad-gltf 0.3.3 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,18 +6,57 @@ const __filename = url.fileURLToPath(import.meta.url);
6
6
  const __dirname = path.dirname(__filename);
7
7
  const DIR = path.resolve(__dirname, "..");
8
8
 
9
+ function readFile(filePath) {
10
+ try {
11
+ return fs.readFileSync(filePath, "utf-8").replace(/\r\n/g, "\n");
12
+ } catch (e) {
13
+ console.error(`Warning: Skipping file '${filePath}'. It is not readable.`);
14
+ return null;
15
+ }
16
+ }
17
+
18
+ function formatMarkdownFile(displayPath, lang, content) {
19
+ return `### ${displayPath}\n---\n\`\`\`${lang}\n${content}\n\`\`\`\n\n`;
20
+ }
21
+
22
+ function appendUserScadFiles(options = {}, isRawScad = false) {
23
+ if (
24
+ !options.scadFiles ||
25
+ !Array.isArray(options.scadFiles) ||
26
+ options.scadFiles.length === 0
27
+ ) {
28
+ return "";
29
+ }
30
+
31
+ let output = `\n\n=== USER PROVIDED OPENSCAD FILES ===\n`;
32
+ if (isRawScad) {
33
+ output += `The following .scad files are provided as reference or base assets. You MUST embed their contents directly into your final single OpenSCAD file (or use them as context), modifying them if necessary to fit the project logic.\n\n`;
34
+ } else {
35
+ output += `The following .scad files are provided as reference or base assets. You MUST embed and write them into the generated project (or use them as context), modifying them if necessary to fit the project logic.\n\n`;
36
+ }
37
+ for (const file of options.scadFiles) {
38
+ const content = readFile(file);
39
+ if (content !== null) {
40
+ const relativePath = path.isAbsolute(file)
41
+ ? path.relative(process.cwd(), file).replace(/\\/g, "/")
42
+ : file.replace(/\\/g, "/");
43
+ output += formatMarkdownFile(relativePath, "openscad", content);
44
+ }
45
+ }
46
+ return output;
47
+ }
48
+
9
49
  export function getProjectPrompts(projectType) {
10
50
  if (projectType === "scad") {
11
51
  return {
12
- buildSystemPrompt: () =>
13
- `You are an expert procedural 3D technical artist and OpenSCAD developer.
14
- Your goal is to generate a single OpenSCAD (.scad) file based on the user's request.
15
-
16
- CRITICAL WORKFLOW:
17
- 1. Write the OpenSCAD code using the custom syntax rules provided in the request.
18
- 2. Call the \`render_scad_model\` tool with your code to visually verify your design.
19
- 3. If the model looks incorrect, adjust your code and re-render. Iterate until perfect.
20
- 4. Provide your final OpenSCAD code in a standard markdown block (\`\`\`openscad).`,
52
+ buildSystemPrompt: (promptRules, options = {}) => {
53
+ let output = promptRules;
54
+ if (Array.isArray(options.scadFiles) && options.scadFiles.length > 0) {
55
+ output += appendUserScadFiles(options, true);
56
+ }
57
+ return output;
58
+ },
59
+ buildInputRequest: (task) => task,
21
60
  allowedTools: ["render_scad_model"],
22
61
  };
23
62
  }
@@ -28,9 +67,11 @@ CRITICAL WORKFLOW:
28
67
  const depName = isBevy ? "bevy" : "wgpu";
29
68
 
30
69
  return {
31
- buildSystemPrompt: (
32
- promptRules,
33
- ) => `You are an expert Rust ${frameworkName} developer and procedural 3D technical artist.
70
+ buildSystemPrompt: (promptRules, options = {}) => {
71
+ const hasUserScadFiles =
72
+ Array.isArray(options.scadFiles) && options.scadFiles.length > 0;
73
+
74
+ const systemPrompt = `You are an expert Rust ${frameworkName} developer and procedural 3D technical artist.
34
75
 
35
76
  NAMING CONVENTION REQUIREMENT:
36
77
  - 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\`).
@@ -49,31 +90,7 @@ ${promptRules}
49
90
  2. Rust ${frameworkName} Project Files:
50
91
  - Create the necessary files for a modern Rust ${frameworkName} application (e.g., \`Cargo.toml\`, \`build.rs\`, \`src/main.rs\`).
51
92
  - In \`Cargo.toml\`, include \`${depName}\` as a dependency.
52
- - 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 application via Cargo:
53
- \`\`\`rust
54
- use std::process::Command;
55
-
56
- fn main() {
57
- // Tell Cargo to re-run this script only if the 'scad' directory changes
58
- println!("cargo::rerun-if-changed=scad");
59
-
60
- // Ensure cross-platform compatibility for npm global binaries
61
- let cmd = if cfg!(target_os = "windows") {
62
- "scad-gltf.cmd"
63
- } else {
64
- "scad-gltf"
65
- };
66
-
67
- let status = Command::new(cmd)
68
- .args(["convert", "./scad", "./assets/models", "--cache"])
69
- .status()
70
- .expect("Failed to execute scad-gltf. Is scad-gltf installed globally?");
71
-
72
- if !status.success() {
73
- panic!("scad-gltf convert failed with status: {}", status);
74
- }
75
- }
76
- \`\`\`
93
+ - You MUST include the provided \`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 application via Cargo.
77
94
  - Write the core application logic in \`src/main.rs\` to load and display the converted \`.glb\` files interactively.${
78
95
  isBevy
79
96
  ? " Provide standard Bevy systems (camera, lights, movement, etc.)."
@@ -83,11 +100,34 @@ ${promptRules}
83
100
  3. Delivery Format (Single Node.js Script):
84
101
  - Output exactly ONE self-contained Node.js script. Do not output manual setup instructions.
85
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.
86
104
  - When executed, this script must programmatically create the entire project directory structure and write all the files to disk using the \`fs\` module.
87
105
  - The script must embed and write:
88
106
  - Your generated \`.scad\` 3D assets.
89
- - Your generated Rust ${frameworkName} project files (\`Cargo.toml\`, \`build.rs\`, \`src/main.rs\`).
90
- - Ensure all string file contents inside the Node.js script are properly escaped.`,
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.`;
114
+
115
+ let systemClipboardOutput = `${systemPrompt}\n\n`;
116
+
117
+ const buildRsPath = path.join(DIR, "rust", "build.rs");
118
+ const buildRsContent = readFile(buildRsPath);
119
+ if (buildRsContent !== null) {
120
+ systemClipboardOutput += formatMarkdownFile(
121
+ "build.rs",
122
+ "rust",
123
+ buildRsContent,
124
+ );
125
+ }
126
+
127
+ systemClipboardOutput += appendUserScadFiles(options);
128
+
129
+ return systemClipboardOutput.trimEnd() + "\n";
130
+ },
91
131
  buildInputRequest: (task) =>
92
132
  `Design and implement a Rust ${frameworkName} project for the following concept: "${task}"`,
93
133
  allowedTools: ["render_scad_model", "compile_rust_project"],
@@ -96,9 +136,12 @@ ${promptRules}
96
136
 
97
137
  if (projectType === "web") {
98
138
  return {
99
- buildSystemPrompt: (
100
- promptRules,
101
- ) => `You are an expert Web 3D developer and procedural 3D technical artist.
139
+ buildSystemPrompt: (promptRules, options = {}) => {
140
+ const hasUserScadFiles =
141
+ Array.isArray(options.scadFiles) && options.scadFiles.length > 0;
142
+
143
+ return (
144
+ `You are an expert Web 3D developer and procedural 3D technical artist.
102
145
 
103
146
  What to generate:
104
147
  1. 3D Web Assets (.scad):
@@ -124,11 +167,19 @@ ${promptRules}
124
167
  3. Delivery Format (Single Node.js Script):
125
168
  - Output exactly ONE self-contained Node.js script. Do not output manual setup instructions.
126
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.
127
171
  - When executed, this script must programmatically create the entire project directory structure and write all the files to disk using the \`fs\` module.
128
172
  - The script must embed and write:
129
173
  - Your generated \`.scad\` 3D assets.
130
- - Your generated Vite web project files.
131
- - Ensure all string file contents inside the Node.js script are properly escaped.`,
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.` +
180
+ appendUserScadFiles(options)
181
+ );
182
+ },
132
183
  buildInputRequest: (task) =>
133
184
  `Design and implement a web-based 3D glTF project using Vite for the following concept: "${task}"`,
134
185
  allowedTools: ["render_scad_model"],
@@ -177,27 +228,26 @@ ${promptRules}
177
228
  * Left/Right Convention in Godot Script: Maintain anatomical consistency in scripts—character Right is along +X (\`transform.basis.x\`) and character Left is along -X (\`-transform.basis.x\`).
178
229
  * 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.
179
230
  - Generate a \`project.godot\` file. It must configure the project and automatically enable the \`scad_importer\` plugin.
180
- - Generate a \`.gitignore\` file that ignores the \`.godot/\` folder.
181
- - Generate a \`README.md\` file that documents the project, mechanics/features, and controls.
182
231
 
183
232
  3. Delivery Format (Single Node.js Script):
184
233
  - Output exactly ONE self-contained Node.js script. Do not output manual setup instructions.
185
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.
186
236
  - When executed, this script must programmatically create the entire project directory structure and write all the files to disk using the \`fs\` module.
187
237
  - The script must embed and write:
188
238
  - Your generated \`.scad\` assets.
189
239
  - Your generated Godot project files.
190
240
  - The exact source code of the provided \`addons/scad_importer/*\` files, placed in their correct respective paths.${
191
241
  hasUserScadFiles
192
- ? "\n - The exact source code of the provided user `.scad` files, placed in the appropriate project folders."
242
+ ? "\n - The provided user `.scad` files (modified if necessary), placed in the appropriate project folders."
193
243
  : ""
194
244
  }
195
- - Ensure all string file contents inside the Node.js script are properly escaped.
196
- - 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.`;
245
+ - Ensure all string file contents inside the Node.js script are properly escaped.`;
246
+
247
+ let systemClipboardOutput = `${systemPrompt}\n\n`;
197
248
 
198
249
  // Gather Addon Files content
199
250
  const addonDir = path.join(DIR, "godot", "addons", "scad_importer");
200
- let addonFiles = [];
201
251
  try {
202
252
  if (fs.existsSync(addonDir)) {
203
253
  const files = fs.readdirSync(addonDir);
@@ -207,7 +257,16 @@ ${promptRules}
207
257
  fs.statSync(fullPath).isFile() &&
208
258
  (file.endsWith(".gd") || file.endsWith(".cfg"))
209
259
  ) {
210
- addonFiles.push(fullPath);
260
+ const content = readFile(fullPath);
261
+ if (content !== null) {
262
+ const relativePath = `addons/scad_importer/${file}`;
263
+ const lang = file.endsWith(".gd") ? "gdscript" : "text";
264
+ systemClipboardOutput += formatMarkdownFile(
265
+ relativePath,
266
+ lang,
267
+ content,
268
+ );
269
+ }
211
270
  }
212
271
  }
213
272
  }
@@ -215,42 +274,7 @@ ${promptRules}
215
274
  console.error("Warning: Could not read addon directory.", e);
216
275
  }
217
276
 
218
- let systemClipboardOutput = `${systemPrompt}\n\n`;
219
-
220
- for (const file of addonFiles) {
221
- try {
222
- const content = fs
223
- .readFileSync(file, "utf-8")
224
- .replace(/\r\n/g, "\n");
225
- const relativePath = path.relative(DIR, file).replace(/\\/g, "/");
226
- const lang = file.endsWith(".gd") ? "gdscript" : "text";
227
- systemClipboardOutput += `### ${relativePath}\n---\n\`\`\`${lang}\n${content}\n\`\`\`\n\n`;
228
- } catch (e) {
229
- console.error(
230
- `Warning: Skipping '${file}'. It is not a readable file.`,
231
- );
232
- }
233
- }
234
-
235
- if (hasUserScadFiles) {
236
- systemClipboardOutput += `=== USER PROVIDED OPENSCAD FILES ===\n`;
237
- 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 project logic.\n\n`;
238
- for (const file of options.scadFiles) {
239
- try {
240
- const content = fs
241
- .readFileSync(file, "utf-8")
242
- .replace(/\r\n/g, "\n");
243
- const relativePath = path.isAbsolute(file)
244
- ? path.relative(process.cwd(), file).replace(/\\/g, "/")
245
- : file.replace(/\\/g, "/");
246
- systemClipboardOutput += `### ${relativePath}\n---\n\`\`\`openscad\n${content}\n\`\`\`\n\n`;
247
- } catch (e) {
248
- console.error(
249
- `Warning: Skipping user SCAD file '${file}'. It is not a readable file.`,
250
- );
251
- }
252
- }
253
- }
277
+ systemClipboardOutput += appendUserScadFiles(options);
254
278
 
255
279
  return systemClipboardOutput.trimEnd() + "\n";
256
280
  },
package/src/prompt.js CHANGED
@@ -27,6 +27,7 @@ export function generatePrompt(description, options = {}) {
27
27
  animation: options.animation ?? true,
28
28
  lazyUnion: options.lazyUnion ?? false,
29
29
  modelName: options.modelName ?? true,
30
+ lights: options.lights ?? false,
30
31
  };
31
32
 
32
33
  let prompt = `Generate an OpenSCAD script to design the following: ${description}.`;
@@ -117,6 +118,7 @@ export function generatePrompt(description, options = {}) {
117
118
  prompt += `\n\nImportant Animation rules:
118
119
  - Wrapping: Use the 'armature(animations=...)' module at the root to wrap all animated components.
119
120
  - Hierarchies: Use the 'bone(name="BoneName", t=[x,y,z], r=[x,y,z])' module to define hierarchical animated parts.
121
+ - Camera Animation: You can create an animated glTF camera by naming a bone "Camera" or "camera". You can append numbers or separators to the name (e.g., "Camera1", "Camera_fov60" for Perspective 60 deg FOV, or "Camera_ortho_mag10" for Orthographic, scale 10). Physical parts like "CameraMount" are safely ignored. It will animate just like any other bone! Note: The camera looks forward along the +Y axis, with +Z as its UP direction.
120
122
  - Auto-Unioning: Any child meshes (e.g., cube, cylinder, imported objects) placed directly inside an 'armature()' or 'bone()' node are automatically unioned together by the engine and rigidly attached to that bone's transform node. Child bones remain separate nodes in the hierarchy.
121
123
  - Animation Data: The 'animations' property is an array of named animation sequences. Each sequence contains an array of tracks defining keyframes for each bone. Format:
122
124
  animations = [
@@ -229,5 +231,24 @@ bake(uvs=true) {
229
231
  \`\`\``;
230
232
  }
231
233
 
234
+ if (opts.lights) {
235
+ prompt += `\n\nImportant Lighting rules:
236
+ - Lights: Use the 'light()' module to add lights to the scene.
237
+ - Syntax: \`light(type="point", color=[1,1,1], intensity=2000.0, range=0.0, innerConeAngle=0.0, outerConeAngle=45.0) { ... }\`
238
+ - Supported light types: "directional", "point", "spot".
239
+ - Intensity (Physically Correct): Directional lights use Lux (e.g., 300-500 for indoor lighting, 100000 for the sun). Point and Spot lights use Candelas and decay over distance via the inverse-square law. They require VERY HIGH values (e.g., 1000.0 to 10000.0+) to illuminate objects!
240
+ - Orientation: A directional or spot light shines in the -Z direction by default. Use \`translate()\` and \`rotate()\` around the \`light()\` module to position and orient it.
241
+ - Spot lights accept \`innerConeAngle\` and \`outerConeAngle\` (in degrees). Point/Spot lights accept \`range\` (0.0 means infinite).
242
+ - Children inside a \`light()\` block are rendered normally in OpenSCAD and exported. The light transform automatically follows its enclosing transformations.
243
+
244
+ Example Lighting Usage:
245
+ \`\`\`openscad
246
+ // A spot light pointing down at an angle (Note the high intensity of 5000)
247
+ translate([0, 0, 50]) rotate([0, 30, 0])
248
+ light(type="spot", color=[1, 0.8, 0.8], intensity=5000.0, innerConeAngle=15.0, outerConeAngle=30.0)
249
+ color("gray") cylinder(h=2, r=1, center=true); // A visual representation of the light fixture
250
+ \`\`\``;
251
+ }
252
+
232
253
  return prompt;
233
254
  }