scad-gltf 0.3.3 → 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,7 +44,8 @@ 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
  }
@@ -28,9 +56,12 @@ CRITICAL WORKFLOW:
28
56
  const depName = isBevy ? "bevy" : "wgpu";
29
57
 
30
58
  return {
31
- buildSystemPrompt: (
32
- promptRules,
33
- ) => `You are an expert Rust ${frameworkName} 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.
34
65
 
35
66
  NAMING CONVENTION REQUIREMENT:
36
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\`).
@@ -83,11 +114,19 @@ ${promptRules}
83
114
  3. Delivery Format (Single Node.js Script):
84
115
  - Output exactly ONE self-contained Node.js script. Do not output manual setup instructions.
85
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.
86
118
  - When executed, this script must programmatically create the entire project directory structure and write all the files to disk using the \`fs\` module.
87
119
  - The script must embed and write:
88
120
  - 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.`,
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
+ },
91
130
  buildInputRequest: (task) =>
92
131
  `Design and implement a Rust ${frameworkName} project for the following concept: "${task}"`,
93
132
  allowedTools: ["render_scad_model", "compile_rust_project"],
@@ -96,9 +135,12 @@ ${promptRules}
96
135
 
97
136
  if (projectType === "web") {
98
137
  return {
99
- buildSystemPrompt: (
100
- promptRules,
101
- ) => `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.
102
144
 
103
145
  What to generate:
104
146
  1. 3D Web Assets (.scad):
@@ -124,11 +166,19 @@ ${promptRules}
124
166
  3. Delivery Format (Single Node.js Script):
125
167
  - Output exactly ONE self-contained Node.js script. Do not output manual setup instructions.
126
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.
127
170
  - When executed, this script must programmatically create the entire project directory structure and write all the files to disk using the \`fs\` module.
128
171
  - The script must embed and write:
129
172
  - 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.`,
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
+ },
132
182
  buildInputRequest: (task) =>
133
183
  `Design and implement a web-based 3D glTF project using Vite for the following concept: "${task}"`,
134
184
  allowedTools: ["render_scad_model"],
@@ -183,13 +233,14 @@ ${promptRules}
183
233
  3. Delivery Format (Single Node.js Script):
184
234
  - Output exactly ONE self-contained Node.js script. Do not output manual setup instructions.
185
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.
186
237
  - When executed, this script must programmatically create the entire project directory structure and write all the files to disk using the \`fs\` module.
187
238
  - The script must embed and write:
188
239
  - Your generated \`.scad\` assets.
189
240
  - Your generated Godot project files.
190
241
  - The exact source code of the provided \`addons/scad_importer/*\` files, placed in their correct respective paths.${
191
242
  hasUserScadFiles
192
- ? "\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."
193
244
  : ""
194
245
  }
195
246
  - Ensure all string file contents inside the Node.js script are properly escaped.
@@ -232,25 +283,7 @@ ${promptRules}
232
283
  }
233
284
  }
234
285
 
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
- }
286
+ systemClipboardOutput += appendUserScadFiles(options);
254
287
 
255
288
  return systemClipboardOutput.trimEnd() + "\n";
256
289
  },
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}});