scad-gltf 0.3.2 → 0.4.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,10 +6,37 @@ 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 appendUserScadFiles(options = {}) {
10
+ if (
11
+ !options.scadFiles ||
12
+ !Array.isArray(options.scadFiles) ||
13
+ options.scadFiles.length === 0
14
+ ) {
15
+ return "";
16
+ }
17
+
18
+ let output = `\n\n=== USER PROVIDED OPENSCAD FILES ===\n`;
19
+ 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`;
20
+ for (const file of options.scadFiles) {
21
+ try {
22
+ const content = fs.readFileSync(file, "utf-8").replace(/\r\n/g, "\n");
23
+ const relativePath = path.isAbsolute(file)
24
+ ? path.relative(process.cwd(), file).replace(/\\/g, "/")
25
+ : file.replace(/\\/g, "/");
26
+ output += `### ${relativePath}\n---\n\`\`\`openscad\n${content}\n\`\`\`\n\n`;
27
+ } catch (e) {
28
+ console.error(
29
+ `Warning: Skipping user SCAD file '${file}'. It is not a readable file.`,
30
+ );
31
+ }
32
+ }
33
+ return output;
34
+ }
35
+
9
36
  export function getProjectPrompts(projectType) {
10
37
  if (projectType === "scad") {
11
38
  return {
12
- buildSystemPrompt: () =>
39
+ buildSystemPrompt: (promptRules, options = {}) =>
13
40
  `You are an expert procedural 3D technical artist and OpenSCAD developer.
14
41
  Your goal is to generate a single OpenSCAD (.scad) file based on the user's request.
15
42
 
@@ -17,24 +44,32 @@ CRITICAL WORKFLOW:
17
44
  1. Write the OpenSCAD code using the custom syntax rules provided in the request.
18
45
  2. Call the \`render_scad_model\` tool with your code to visually verify your design.
19
46
  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).`,
47
+ 4. Provide your final OpenSCAD code in a standard markdown block (\`\`\`openscad).` +
48
+ appendUserScadFiles(options),
21
49
  allowedTools: ["render_scad_model"],
22
50
  };
23
51
  }
24
52
 
25
- if (projectType === "bevy") {
53
+ if (projectType === "bevy" || projectType === "wgpu") {
54
+ const isBevy = projectType === "bevy";
55
+ const frameworkName = isBevy ? "Bevy" : "wgpu";
56
+ const depName = isBevy ? "bevy" : "wgpu";
57
+
26
58
  return {
27
- buildSystemPrompt: (
28
- promptRules,
29
- ) => `You are an expert Rust Bevy engine game developer and procedural 3D technical artist.
59
+ buildSystemPrompt: (promptRules, options = {}) => {
60
+ const hasUserScadFiles =
61
+ Array.isArray(options.scadFiles) && options.scadFiles.length > 0;
62
+
63
+ return (
64
+ `You are an expert Rust ${frameworkName} developer and procedural 3D technical artist.
30
65
 
31
66
  NAMING CONVENTION REQUIREMENT:
32
67
  - 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\`).
33
68
  - NEVER use hyphens/minus signs (\`-\`) or spaces in any file or folder names.
34
69
 
35
70
  What to generate:
36
- 1. 3D Game Assets (.scad):
37
- - Generate procedural 3D models for the game using OpenSCAD.
71
+ 1. 3D Assets (.scad):
72
+ - Generate procedural 3D models for the project using OpenSCAD.
38
73
  - 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.
39
74
  - 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:
40
75
 
@@ -42,10 +77,10 @@ What to generate:
42
77
  ${promptRules}
43
78
  =============================
44
79
 
45
- 2. Rust Bevy Project Files:
46
- - Create the necessary files for a modern Rust Bevy engine application (e.g., \`Cargo.toml\`, \`build.rs\`, \`src/main.rs\`).
47
- - In \`Cargo.toml\`, include \`bevy\` as a dependency.
48
- - 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:
80
+ 2. Rust ${frameworkName} Project Files:
81
+ - Create the necessary files for a modern Rust ${frameworkName} application (e.g., \`Cargo.toml\`, \`build.rs\`, \`src/main.rs\`).
82
+ - In \`Cargo.toml\`, include \`${depName}\` as a dependency.
83
+ - 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:
49
84
  \`\`\`rust
50
85
  use std::process::Command;
51
86
 
@@ -70,31 +105,46 @@ ${promptRules}
70
105
  }
71
106
  }
72
107
  \`\`\`
73
- - 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.).
108
+ - Write the core application logic in \`src/main.rs\` to load and display the converted \`.glb\` files interactively.${
109
+ isBevy
110
+ ? " Provide standard Bevy systems (camera, lights, movement, etc.)."
111
+ : ""
112
+ }
74
113
 
75
114
  3. Delivery Format (Single Node.js Script):
76
115
  - Output exactly ONE self-contained Node.js script. Do not output manual setup instructions.
77
116
  - 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.
117
+ - 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.
78
118
  - When executed, this script must programmatically create the entire project directory structure and write all the files to disk using the \`fs\` module.
79
119
  - The script must embed and write:
80
120
  - Your generated \`.scad\` 3D assets.
81
- - Your generated Rust Bevy project files (\`Cargo.toml\`, \`build.rs\`, \`src/main.rs\`).
82
- - Ensure all string file contents inside the Node.js script are properly escaped.`,
121
+ - Your generated Rust ${frameworkName} project files (\`Cargo.toml\`, \`build.rs\`, \`src/main.rs\`).${
122
+ hasUserScadFiles
123
+ ? "\n - The provided user `.scad` files (modified if necessary), placed in the appropriate project folders."
124
+ : ""
125
+ }
126
+ - Ensure all string file contents inside the Node.js script are properly escaped.` +
127
+ appendUserScadFiles(options)
128
+ );
129
+ },
83
130
  buildInputRequest: (task) =>
84
- `Design and implement a Rust Bevy engine game for the following concept: "${task}"`,
131
+ `Design and implement a Rust ${frameworkName} project for the following concept: "${task}"`,
85
132
  allowedTools: ["render_scad_model", "compile_rust_project"],
86
133
  };
87
134
  }
88
135
 
89
136
  if (projectType === "web") {
90
137
  return {
91
- buildSystemPrompt: (
92
- promptRules,
93
- ) => `You are an expert Web 3D developer and procedural 3D technical artist.
138
+ buildSystemPrompt: (promptRules, options = {}) => {
139
+ const hasUserScadFiles =
140
+ Array.isArray(options.scadFiles) && options.scadFiles.length > 0;
141
+
142
+ return (
143
+ `You are an expert Web 3D developer and procedural 3D technical artist.
94
144
 
95
145
  What to generate:
96
146
  1. 3D Web Assets (.scad):
97
- - Generate procedural 3D models for the game using OpenSCAD.
147
+ - Generate procedural 3D models for the project using OpenSCAD.
98
148
  - 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.
99
149
  - 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:
100
150
 
@@ -104,25 +154,33 @@ ${promptRules}
104
154
 
105
155
  2. Vite Web Project Files (npm based):
106
156
  - Create the necessary files for a modern web application (e.g., \`package.json\`, \`index.html\`, \`main.js\`).
107
- - 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.
157
+ - 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 project's requirements.
108
158
  - In \`package.json\`, you MUST include the scad to gltf converter tool as a dev dependency:
109
159
  \`"scad-gltf": "^0.3.0"\`
110
160
  - 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.
111
161
  For example:
112
162
  \`"predev": "scad-gltf convert ./scad ./public/models --cache"\`
113
163
  \`"prebuild": "scad-gltf convert ./scad ./public/models --cache"\`
114
- - Write the core game logic to load and display the converted \`.glb\` files interactively.
164
+ - Write the core application logic to load and display the converted \`.glb\` files interactively.
115
165
 
116
166
  3. Delivery Format (Single Node.js Script):
117
167
  - Output exactly ONE self-contained Node.js script. Do not output manual setup instructions.
118
168
  - 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.
169
+ - 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.
119
170
  - When executed, this script must programmatically create the entire project directory structure and write all the files to disk using the \`fs\` module.
120
171
  - The script must embed and write:
121
172
  - Your generated \`.scad\` 3D assets.
122
- - Your generated Vite web project files.
123
- - Ensure all string file contents inside the Node.js script are properly escaped.`,
173
+ - Your generated Vite web project files.${
174
+ hasUserScadFiles
175
+ ? "\n - The provided user `.scad` files (modified if necessary), placed in the appropriate project folders."
176
+ : ""
177
+ }
178
+ - Ensure all string file contents inside the Node.js script are properly escaped.` +
179
+ appendUserScadFiles(options)
180
+ );
181
+ },
124
182
  buildInputRequest: (task) =>
125
- `Design and implement a web-based 3D glTF game using Vite for the following concept: "${task}"`,
183
+ `Design and implement a web-based 3D glTF project using Vite for the following concept: "${task}"`,
126
184
  allowedTools: ["render_scad_model"],
127
185
  };
128
186
  }
@@ -133,15 +191,15 @@ ${promptRules}
133
191
  const hasUserScadFiles =
134
192
  Array.isArray(options.scadFiles) && options.scadFiles.length > 0;
135
193
 
136
- const systemPrompt = `You are an expert Godot 4 game developer and procedural 3D technical artist.
194
+ const systemPrompt = `You are an expert Godot 4 developer and procedural 3D technical artist.
137
195
 
138
196
  NAMING CONVENTION REQUIREMENT:
139
197
  - 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\`).
140
198
  - NEVER use hyphens/minus signs (\`-\`) or spaces in any file or folder names.
141
199
 
142
200
  What to generate:
143
- 1. 3D Game Assets (.scad):
144
- - Generate procedural 3D models for the game using OpenSCAD.
201
+ 1. 3D Assets (.scad):
202
+ - Generate procedural 3D models for the project using OpenSCAD.
145
203
  - 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.
146
204
  - 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).
147
205
  - 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.
@@ -159,7 +217,7 @@ ${promptRules}
159
217
  =============================
160
218
 
161
219
  2. Godot 4 Project Files:
162
- - Create the necessary GDScript (\`.gd\`) and scene (\`.tscn\`) files to implement the game logic, responsive player input controls, and a core gameplay loop.
220
+ - Create the necessary GDScript (\`.gd\`) and scene (\`.tscn\`) files to implement the project logic, responsive user input controls, and a core interaction loop.
163
221
  - The scenes should directly instance the generated \`.scad\` files (the provided addon will handle importing them as 3D scenes).
164
222
  - GDScript Coordinate, Forward, and Left/Right Conventions:
165
223
  * Forward is -Z: In Godot, \`Vector3.FORWARD\` is \`Vector3(0, 0, -1)\`. A 3D node's local forward direction is \`-transform.basis.z\` (or \`-global_transform.basis.z\`). In character movement, forward input (e.g., W or ui_up) must translate along \`-transform.basis.z\`. Never treat +Z as forward.
@@ -170,18 +228,19 @@ ${promptRules}
170
228
  * 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.
171
229
  - Generate a \`project.godot\` file. It must configure the project and automatically enable the \`scad_importer\` plugin.
172
230
  - Generate a \`.gitignore\` file that ignores the \`.godot/\` folder.
173
- - Generate a \`README.md\` file that documents the project, gameplay mechanics, and controls.
231
+ - Generate a \`README.md\` file that documents the project, mechanics/features, and controls.
174
232
 
175
233
  3. Delivery Format (Single Node.js Script):
176
234
  - Output exactly ONE self-contained Node.js script. Do not output manual setup instructions.
177
- - CRITICAL: The generated Node.js script MUST first create a root project folder (named using snake_case with underscores, e.g., \`my_game_project\`) and output all files and folders inside this newly created project folder.
235
+ - 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.
236
+ - 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.
178
237
  - When executed, this script must programmatically create the entire project directory structure and write all the files to disk using the \`fs\` module.
179
238
  - The script must embed and write:
180
- - Your generated \`.scad\` game assets.
239
+ - Your generated \`.scad\` assets.
181
240
  - Your generated Godot project files.
182
241
  - The exact source code of the provided \`addons/scad_importer/*\` files, placed in their correct respective paths.${
183
242
  hasUserScadFiles
184
- ? "\n - The exact source code of the provided user `.scad` files, placed in the appropriate project folders."
243
+ ? "\n - The provided user `.scad` files (modified if necessary), placed in the appropriate project folders."
185
244
  : ""
186
245
  }
187
246
  - Ensure all string file contents inside the Node.js script are properly escaped.
@@ -224,30 +283,12 @@ ${promptRules}
224
283
  }
225
284
  }
226
285
 
227
- if (hasUserScadFiles) {
228
- systemClipboardOutput += `=== USER PROVIDED OPENSCAD FILES ===\n`;
229
- 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`;
230
- for (const file of options.scadFiles) {
231
- try {
232
- const content = fs
233
- .readFileSync(file, "utf-8")
234
- .replace(/\r\n/g, "\n");
235
- const relativePath = path.isAbsolute(file)
236
- ? path.relative(process.cwd(), file).replace(/\\/g, "/")
237
- : file.replace(/\\/g, "/");
238
- systemClipboardOutput += `### ${relativePath}\n---\n\`\`\`openscad\n${content}\n\`\`\`\n\n`;
239
- } catch (e) {
240
- console.error(
241
- `Warning: Skipping user SCAD file '${file}'. It is not a readable file.`,
242
- );
243
- }
244
- }
245
- }
286
+ systemClipboardOutput += appendUserScadFiles(options);
246
287
 
247
288
  return systemClipboardOutput.trimEnd() + "\n";
248
289
  },
249
290
  buildInputRequest: (task) =>
250
- `Design and implement a Godot 4 project for the following game concept: "${task}"`,
291
+ `Design and implement a Godot 4 project for the following concept: "${task}"`,
251
292
  allowedTools: ["render_scad_model", "test_godot_project"],
252
293
  };
253
294
  }
package/src/prompt.js CHANGED
@@ -117,6 +117,7 @@ export function generatePrompt(description, options = {}) {
117
117
  prompt += `\n\nImportant Animation rules:
118
118
  - Wrapping: Use the 'armature(animations=...)' module at the root to wrap all animated components.
119
119
  - Hierarchies: Use the 'bone(name="BoneName", t=[x,y,z], r=[x,y,z])' module to define hierarchical animated parts.
120
+ - 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
121
  - 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
122
  - 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
123
  animations = [
@@ -1 +0,0 @@
1
- body{background-color:#333;width:100vw;height:100vh;margin:0;font-family:sans-serif;display:flex;overflow:hidden}#viewer-container{background:#1e1e1e;flex-grow:1;width:100%;height:100vh;position:relative;overflow:hidden}#viewer{width:100%;height:100%}#viewer-controls{z-index:10;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);background:#0009;border-radius:6px;align-items:center;gap:8px;padding:6px;display:flex;position:absolute;top:15px;right:40px}#viewer-controls label{color:#fff;cursor:pointer;-webkit-user-select:none;user-select:none;align-items:center;gap:4px;margin:0;font-size:13px;display:flex}#fullscreen-btn,#screenshot-btn{color:#fff;cursor:pointer;background:0 0;border:none;justify-content:center;align-items:center;height:auto;font-size:16px;display:flex}#fullscreen-btn{padding:0 4px}#screenshot-btn{padding:0 4px 6px}#fullscreen-btn:hover,#screenshot-btn:hover{color:#0af;background:0 0}#save-ui{z-index:10;background:#0009;border-radius:6px;gap:8px;padding:6px;display:flex;position:absolute;top:15px;left:15px}#filename-input{color:#fff;background:#333;border:1px solid #555;border-radius:4px;outline:none;width:160px;padding:4px 8px;font-family:monospace}#filename-input:focus{border-color:#0af}#save-btn{color:#fff;cursor:pointer;background:#0af;border:none;border-radius:4px;padding:4px 12px;font-weight:700}#save-btn:hover:not(:disabled){background:#08c}#save-btn:disabled{opacity:.7;cursor:not-allowed}#open-editor-btn{color:#fff;cursor:pointer;background:#6f42c1;border:none;border-radius:4px;padding:4px 12px;font-weight:700}#open-editor-btn:hover:not(:disabled){background:#5a32a3}#open-editor-btn:disabled{opacity:.7;cursor:not-allowed}#anim-controls{z-index:10;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);background:#0009;border-radius:8px;align-items:center;gap:10px;width:80%;max-width:400px;padding:8px 16px;display:flex;position:absolute;bottom:20px;left:50%;transform:translate(-50%)}#anim-controls select{color:#fff;background:#333;border:1px solid #555;border-radius:4px;outline:none;padding:4px;font-size:13px}#anim-controls button{color:#fff;cursor:pointer;white-space:nowrap;background:#444;border:none;border-radius:4px;padding:4px 12px;font-size:13px}#anim-controls button:hover{background:#555}#anim-slider{cursor:pointer;-webkit-appearance:none;background:0 0;flex-grow:1;margin:0}#anim-slider:focus{outline:none}#anim-slider::-webkit-slider-runnable-track{cursor:pointer;background:#444;border-radius:3px;width:100%;height:6px}#anim-slider::-webkit-slider-thumb{cursor:pointer;-webkit-appearance:none;background:#0af;border-radius:50%;width:14px;height:14px;margin-top:-4px}
@@ -1 +0,0 @@
1
- import{A as e,B as t,D as n,E as r,H as i,O as a,R as o,W as s,a as ee,d as c,et as te,f as ne,ht as re,i as ie,l,lt as u,mt as ae,n as oe,o as se,p as d,r as f,s as p,t as ce,u as le,ut as m,w as ue,y as de}from"./OutputPass-B6MrFnGE.js";var h=document.getElementById(`viewer-container`),fe=document.getElementById(`viewer`),g=document.getElementById(`filename-input`),_=document.getElementById(`save-btn`),v=document.getElementById(`open-editor-btn`),y=document.getElementById(`show-grid-cb`),b=document.getElementById(`wireframe-cb`),x=document.getElementById(`fullscreen-btn`),S=document.getElementById(`screenshot-btn`),C=document.getElementById(`anim-controls`),w=document.getElementById(`anim-play-btn`),T=document.getElementById(`anim-select`),E=document.getElementById(`anim-slider`),D=null,O=null,k=!0,A=!1,j=[],M=``,N=!1,P=null,F=null,I=!1,L=new te;L.background=new de(2236962);var R=new i(60,window.innerWidth/window.innerHeight,.1,2e3);R.position.set(50,50,-50);var z=new le({antialias:!0});z.setSize(window.innerWidth,window.innerHeight),z.setPixelRatio(Math.min(window.devicePixelRatio,2)),z.shadowMap.enabled=!0,z.shadowMap.type=2,z.toneMapping=4,z.toneMappingExposure=1,fe.appendChild(z.domElement);var B=new ie(z),pe=new f(L,R);B.addPass(pe);var me=new oe(new u(window.innerWidth,window.innerHeight),.1,.1,2);B.addPass(me);var V=new ce;B.addPass(V);var H=new p(R,z.domElement);H.enableDamping=!0,H.dampingFactor=.1,L.environment=new l(z).fromScene(new ee,.04).texture,L.environmentIntensity=.8;var U=new a;L.add(U);var he=new s(2e3,2e3),ge=new t({color:2236962,roughness:.8,metalness:.1}),W=new o(he,ge);W.rotation.x=-Math.PI/2,W.receiveShadow=!0,L.add(W);var G=new n(2e3,100,5592405,3355443);G.material.transparent=!0,G.material.opacity=.5,L.add(G);var K=new ne(100);L.add(K);var q=new ue(16777215,1.5);q.castShadow=!0,q.shadow.mapSize.width=2048,q.shadow.mapSize.height=2048,q.shadow.bias=-5e-4,U.add(q),U.add(q.target);var J=new e(16777215,4473924,.6);J.position.set(0,20,0),U.add(J),b&&b.addEventListener(`change`,()=>{let e=b.checked;D&&D.traverse(t=>{t.isMesh&&t.material&&(Array.isArray(t.material)?t.material.forEach(t=>{t.wireframe=e}):t.material.wireframe=e)})}),x&&h&&(x.addEventListener(`click`,()=>{document.fullscreenElement?document.exitFullscreen().catch(e=>{console.warn(`Error attempting to exit fullscreen: ${e.message}`)}):h.requestFullscreen().catch(e=>{console.warn(`Error attempting to enable fullscreen: ${e.message}`)})}),document.addEventListener(`fullscreenchange`,()=>{setTimeout(()=>window.dispatchEvent(new Event(`resize`)),100)})),S&&S.addEventListener(`click`,()=>{I=!0;let e=S.innerText;S.innerText=`✅`,setTimeout(()=>{S.innerText=e},1e3)});function Y(){O&&(k=!1,O.paused=!0,w&&(w.innerText=`▶ Play`))}function X(e){if(!F||!j[e])return;O&&O.stop();let t=j[e];O=F.clipAction(t),O.play(),O.time=0,F.update(0),k=!0,O.paused=!1,w&&(w.innerText=`⏸ Pause`),E&&(E.value=0)}T&&T.addEventListener(`change`,e=>{X(parseInt(e.target.value))}),w&&w.addEventListener(`click`,()=>{O&&(k=!k,O.paused=!k,w.innerText=k?`⏸ Pause`:`▶ Play`)}),E&&(E.addEventListener(`mousedown`,()=>{A=!0,Y()}),E.addEventListener(`mouseup`,()=>{A=!1}),E.addEventListener(`touchstart`,()=>{A=!0,Y()},{passive:!0}),E.addEventListener(`touchend`,()=>{A=!1},{passive:!0}),E.addEventListener(`input`,e=>{if(Y(),O){let t=O.getClip().duration;O.time=parseFloat(e.target.value)*t,F&&F.update(0)}}));var Z=performance.now();function Q(){requestAnimationFrame(Q);let e=performance.now(),t=(e-Z)/1e3;if(Z=e,F&&(F.update(t),O&&k&&!A)){let e=O.getClip().duration;if(e>0){let t=O.time%e;t<0&&(t+=e),E&&(E.value=t/e)}}H.update();let n=!y||y.checked;if(G.visible=n,K.visible=n,B.render(),I){I=!1;let e=z.domElement.toDataURL(`image/png`);window.parent.postMessage({type:`ADD_IMAGE_TO_CHAT`,dataUrl:e},`*`)}}Q(),window.addEventListener(`resize`,()=>{let e=h?h.clientWidth:window.innerWidth,t=h?h.clientHeight:window.innerHeight;R.aspect=e/t,R.updateProjectionMatrix(),z.setSize(e,t),B.setSize(e,t)});function _e(e,t){if(!e)return new d;e.updateMatrixWorld(!0);let n=new d().setFromObject(e);if(!t||t.length===0)return n;let r=new Map;e.traverse(e=>{r.set(e,{position:e.position.clone(),quaternion:e.quaternion.clone(),scale:e.scale.clone()})});let i=new c(e);for(let r of t){let t=i.clipAction(r);t.play();let a=new Set([0,r.duration]);if(r.tracks){for(let e of r.tracks)if(e.times)for(let t=0;t<e.times.length;t++)a.add(e.times[t])}if(r.duration>0)for(let e=0;e<=20;e++)a.add(e/20*r.duration);let o=Array.from(a).sort((e,t)=>e-t);if(o.length>60){let e=[],t=(o.length-1)/59;for(let n=0;n<60;n++)e.push(o[Math.round(n*t)]);o=e}for(let r of o)t.time=r,i.update(0),e.updateMatrixWorld(!0),n.expandByObject(e);t.stop()}return i.stopAllAction(),i.uncacheRoot(e),r.forEach((e,t)=>{t.position.copy(e.position),t.quaternion.copy(e.quaternion),t.scale.copy(e.scale)}),e.updateMatrixWorld(!0),n}function ve(e){return new Promise((t,n)=>{D&&=(F&&=(F.stopAllAction(),F.uncacheRoot(F.getRoot()),null),O=null,j=[],L.remove(D),D.traverse(e=>{e.isMesh&&(e.geometry.dispose(),Array.isArray(e.material)?e.material.forEach(e=>e.dispose()):e.material.dispose())}),null);let r=e.buffer.slice(e.byteOffset,e.byteOffset+e.byteLength);new se().parse(r,``,e=>{D=e.scene,j=e.animations||[];let n=b?b.checked:!1;j.length>0?(F=new c(D),C?(C.style.display=`flex`,T.innerHTML=``,j.forEach((e,t)=>{let n=document.createElement(`option`);n.value=t,n.innerText=e.name||`Animation ${t+1}`,T.appendChild(n)}),X(0)):(j.forEach(e=>{let t=F.clipAction(e);t.play(),t.time=0}),F.update(0))):(C&&(C.style.display=`none`),O=null),D.traverse(e=>{e.isMesh&&(e.castShadow=!0,e.receiveShadow=!0,e.frustumCulled=!1,e.material&&(Array.isArray(e.material)?e.material.forEach(e=>{e.wireframe=n}):e.material.wireframe=n))}),L.add(D),ye(),t()},e=>{console.error(e),n(e)})})}function ye(){if(!D)return;let e=_e(D,j);if(e.isEmpty())return;let t=e.getCenter(new m),n=e.getSize(new m),i=Math.max(n.x,n.y,n.z)||10;W.position.y=e.min.y-.01,G.position.y=W.position.y+.001,K.position.y=W.position.y+.002;let a=R.fov*(Math.PI/180),o=i/(2*Math.tan(a/2));R.aspect<1&&(o/=R.aspect),o*=1.5,R.near=Math.max(.01,i*.01),R.far=Math.max(2e3,o*10),R.updateProjectionMatrix(),H.maxDistance=R.far,R.position.set(t.x+o*.8,t.y+o*.8,t.z-o*.8),R.lookAt(t),H.target.copy(t),H.update(),q.position.set(t.x+i,t.y+i*1.5,t.z-i),q.target.position.copy(t),q.target.updateMatrixWorld();let s=i*1.5;q.shadow.camera.left=-s,q.shadow.camera.right=s,q.shadow.camera.top=s,q.shadow.camera.bottom=-s,q.shadow.camera.near=.1,q.shadow.camera.far=i*5,q.shadow.camera.updateProjectionMatrix(),L.fog=new r(2236962,o*1.5,o*5)}async function be(e){let t={},n=`http://localhost:3000`,r=[];try{let e=await fetch(`${n}/api/scads`);e.ok&&(r=(await e.json()).files||[])}catch{return t}let i=new Set;async function a(e){let o=/(?:include|use)\s*([<"])([^>"]+)([>"])/g,s;for(;(s=o.exec(e))!==null;){let e=s[2],o=e.split(/[/\\]/).pop();if(!i.has(e)&&r.includes(o)){i.add(e);try{let r=await fetch(`${n}/api/scads/${encodeURIComponent(o)}`);if(r.ok){let n=await r.json();t[e]=n.content,await a(n.content)}}catch{console.warn(`Failed to load dependency: ${e}`)}}}}return await a(e),t}async function $(e){if(N){P=e;return}N=!0;try{let t=await be(e);await ve(await ae(e,{wasmUrl:re,additionalFiles:t}))}catch(e){console.error(e)}finally{if(N=!1,P!==null){let e=P;P=null,$(e)}}}window.addEventListener(`message`,async e=>{if(e.data.type===`RENDER_SCAD`){M=e.data.code;let t=M.match(/\/\*\s*Model Name:\s*(.*?)\s*\*\//i);if(t&&t[1]){let e=t[1].trim().replace(/\s+/g,`_`).replace(/[^a-zA-Z0-9_-]/g,``);e&&(g.value=e)}await $(e.data.code)}}),_.addEventListener(`click`,async()=>{let e=g.value.trim();if(!e){alert(`Please enter a filename.`),g.focus();return}let t=`http://localhost:3000`;try{_.innerText=`Checking...`,_.disabled=!0;let n=await fetch(`${t}/api/scads`);if(!n.ok)throw Error("Could not reach `scad-gltf serve`.");let r=await n.json(),i=e.toLowerCase().endsWith(`.scad`)?e:`${e}.scad`;if(r.files&&r.files.includes(i)&&!confirm(`File "${i}" already exists. Overwrite?`)){_.innerText=`Save`,_.disabled=!1;return}_.innerText=`Saving...`;let a=await fetch(`${t}/api/scads`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({filename:e,content:M||``})});if(!a.ok){let e=await a.json();throw Error(e.error||`Save failed.`)}_.innerText=`✅ Saved!`,setTimeout(()=>{_.innerText=`Save`,_.disabled=!1},2e3)}catch(e){console.error(e),alert(`Error saving file: `+e.message),_.innerText=`Save`,_.disabled=!1}});async function xe(e){try{if(typeof CompressionStream<`u`){let t=new Blob([e]).stream().pipeThrough(new CompressionStream(`deflate-raw`)),n=await new Response(t).arrayBuffer(),r=new Uint8Array(n),i=``;for(let e=0;e<r.length;e++)i+=String.fromCharCode(r[e]);return`c`+btoa(i).replace(/\+/g,`-`).replace(/\//g,`_`).replace(/=+$/,``)}}catch(e){console.warn(`CompressionStream failed, falling back`,e)}return`u`+btoa(unescape(encodeURIComponent(e))).replace(/\+/g,`-`).replace(/\//g,`_`).replace(/=+$/,``)}v.addEventListener(`click`,async()=>{if(!M){alert(`No model loaded to open.`);return}try{v.disabled=!0,v.innerText=`Preparing...`;let e=`https://iliagrigorevdev.github.io/scad-gltf/#${await xe(M)}`;window.open(e,`_blank`),window.parent.postMessage({type:`CLOSE_PREVIEW`},`*`),v.innerText=`✅ Opened!`,setTimeout(()=>{v.innerText=`Edit`,v.disabled=!1},2e3)}catch(e){console.error(`Error creating editor link`,e),alert(`Error creating editor link: `+e.message),v.innerText=`Edit`,v.disabled=!1}});