scad-gltf 0.1.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.
Files changed (37) hide show
  1. package/README.md +489 -0
  2. package/bin/scad-convert.js +221 -0
  3. package/bin/scad-godot.js +247 -0
  4. package/bin/scad-mcp.js +508 -0
  5. package/bin/scad-serve.js +225 -0
  6. package/bin/scad-web.js +206 -0
  7. package/editor/dist/aristea_wreck_puresky_2k.hdr +0 -0
  8. package/editor/dist/assets/OutputPass-Bvl6NigM.js +4317 -0
  9. package/editor/dist/assets/index-V9cEH4KX.js +4318 -0
  10. package/editor/dist/assets/index-t9MYrExo.css +1 -0
  11. package/editor/dist/assets/openscad-CdBCY4mx.wasm +0 -0
  12. package/editor/dist/assets/preview-C1zc24MJ.js +1 -0
  13. package/editor/dist/assets/preview-fQfL_FJ-.css +1 -0
  14. package/editor/dist/assets/prompt-ui-8EFRz0ju.js +126 -0
  15. package/editor/dist/content-loader.js +4 -0
  16. package/editor/dist/content.css +255 -0
  17. package/editor/dist/content.js +24 -0
  18. package/editor/dist/icon.png +0 -0
  19. package/editor/dist/index.html +187 -0
  20. package/editor/dist/manifest.json +23 -0
  21. package/editor/dist/manifest.webmanifest +1 -0
  22. package/editor/dist/preview.html +27 -0
  23. package/editor/dist/registerSW.js +1 -0
  24. package/editor/dist/sw.js +1 -0
  25. package/editor/dist/workbox-9c191d2f.js +1 -0
  26. package/godot/README.md +64 -0
  27. package/godot/addons/scad_importer/plugin.cfg +7 -0
  28. package/godot/addons/scad_importer/scad_importer.gd +197 -0
  29. package/godot/addons/scad_importer/scad_plugin.gd +12 -0
  30. package/godot/examples/README.md +82 -0
  31. package/godot/examples/fruit_fusion_3d.js +1574 -0
  32. package/godot/examples/package.json +5 -0
  33. package/package.json +63 -0
  34. package/src/convert.js +94 -0
  35. package/src/ext/openscad.js +14 -0
  36. package/src/ext/openscad.wasm +0 -0
  37. package/src/prompt.js +229 -0
@@ -0,0 +1,23 @@
1
+ {
2
+ "manifest_version": 3,
3
+ "name": "AI Studio SCAD Preview",
4
+ "version": "0.1",
5
+ "description": "Preview OpenSCAD natively in Google AI Studio",
6
+ "permissions": ["activeTab"],
7
+ "content_security_policy": {
8
+ "extension_pages": "script-src 'self' 'wasm-unsafe-eval'; object-src 'self';"
9
+ },
10
+ "content_scripts": [
11
+ {
12
+ "matches": ["https://aistudio.google.com/*"],
13
+ "js": ["content-loader.js"],
14
+ "css": ["content.css"]
15
+ }
16
+ ],
17
+ "web_accessible_resources": [
18
+ {
19
+ "resources": ["preview.html", "*.js", "*.css", "*.wasm", "assets/*"],
20
+ "matches": ["https://aistudio.google.com/*"]
21
+ }
22
+ ]
23
+ }
@@ -0,0 +1 @@
1
+ {"name":"Scadify","short_name":"Scadify","description":"A modern, web-based editor and 3D viewer for OpenSCAD supporting WebAssembly compilation, PBR materials, skeletal animations, and texture baking.","start_url":".","display":"standalone","background_color":"#222222","theme_color":"#222222","lang":"en","scope":".","id":".","icons":[{"src":"icon.png","sizes":"192x192","type":"image/png","purpose":"any"},{"src":"icon.png","sizes":"512x512","type":"image/png","purpose":"maskable"}]}
@@ -0,0 +1,27 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <script type="module" crossorigin src="./assets/preview-C1zc24MJ.js"></script>
6
+ <link rel="modulepreload" crossorigin href="./assets/OutputPass-Bvl6NigM.js">
7
+ <link rel="stylesheet" crossorigin href="./assets/preview-fQfL_FJ-.css">
8
+ <link rel="manifest" href="./manifest.webmanifest"><script id="vite-plugin-pwa:register-sw" src="./registerSW.js"></script></head>
9
+ <body>
10
+ <div id="viewer-container">
11
+ <div id="viewer-controls">
12
+ <label><input type="checkbox" id="show-grid-cb" checked /> Grid</label>
13
+ <label><input type="checkbox" id="wireframe-cb" /> Wireframe</label>
14
+ <button id="screenshot-btn" title="Take Screenshot & Add to Chat">
15
+ 📷
16
+ </button>
17
+ <button id="fullscreen-btn" title="Toggle Full Screen">⛶</button>
18
+ </div>
19
+ <div id="save-ui">
20
+ <input type="text" id="filename-input" placeholder="Name" />
21
+ <button id="save-btn">Save</button>
22
+ <button id="open-editor-btn">Edit</button>
23
+ </div>
24
+ <div id="viewer"></div>
25
+ </div>
26
+ </body>
27
+ </html>
@@ -0,0 +1 @@
1
+ if('serviceWorker' in navigator) {window.addEventListener('load', () => {navigator.serviceWorker.register('./sw.js', { scope: './' })})}
@@ -0,0 +1 @@
1
+ if(!self.define){let e,s={};const i=(i,r)=>(i=new URL(i+".js",r).href,s[i]||new Promise(s=>{if("document"in self){const e=document.createElement("script");e.src=i,e.onload=s,document.head.appendChild(e)}else e=i,importScripts(i),s()}).then(()=>{let e=s[i];if(!e)throw new Error(`Module ${i} didn’t register its module`);return e}));self.define=(r,n)=>{const t=e||("document"in self?document.currentScript.src:"")||location.href;if(s[t])return;let l={};const o=e=>i(e,t),a={module:{uri:t},exports:l,require:o};s[t]=Promise.all(r.map(e=>a[e]||o(e))).then(e=>(n(...e),l))}}define(["./workbox-9c191d2f"],function(e){"use strict";self.skipWaiting(),e.clientsClaim(),e.precacheAndRoute([{url:"registerSW.js",revision:"402b66900e731ca748771b6fc5e7a068"},{url:"preview.html",revision:"c90d9c25847a398e770ccae0a6dad222"},{url:"index.html",revision:"ee849062f778f8aea825f9b4cbc7608c"},{url:"icon.png",revision:"20a5bd64b0ab560837e6333f5afd2bf2"},{url:"content.js",revision:"ae49bf7afa56af0f414761aa9e644801"},{url:"content.css",revision:"8a04232febb2faba7b10edc60e2974dd"},{url:"content-loader.js",revision:"d94f1755552d49c1fe3a6da123e8a346"},{url:"aristea_wreck_puresky_2k.hdr",revision:"e764c66f871ab0987f3fac422edc841d"},{url:"assets/prompt-ui-8EFRz0ju.js",revision:null},{url:"assets/preview-fQfL_FJ-.css",revision:null},{url:"assets/preview-C1zc24MJ.js",revision:null},{url:"assets/openscad-CdBCY4mx.wasm",revision:null},{url:"assets/index-t9MYrExo.css",revision:null},{url:"assets/index-V9cEH4KX.js",revision:null},{url:"assets/OutputPass-Bvl6NigM.js",revision:null},{url:"icon.png",revision:"20a5bd64b0ab560837e6333f5afd2bf2"},{url:"manifest.webmanifest",revision:"81dc3433323859b43ec568f4f95cd344"}],{}),e.cleanupOutdatedCaches(),e.registerRoute(new e.NavigationRoute(e.createHandlerBoundToURL("index.html")))});
@@ -0,0 +1 @@
1
+ define(["exports"],function(t){"use strict";try{self["workbox:core:7.4.0"]&&_()}catch(t){}const e=(t,...e)=>{let s=t;return e.length>0&&(s+=` :: ${JSON.stringify(e)}`),s};class s extends Error{constructor(t,s){super(e(t,s)),this.name=t,this.details=s}}try{self["workbox:routing:7.4.0"]&&_()}catch(t){}const n=t=>t&&"object"==typeof t?t:{handle:t};class i{constructor(t,e,s="GET"){this.handler=n(e),this.match=t,this.method=s}setCatchHandler(t){this.catchHandler=n(t)}}class r extends i{constructor(t,e,s){super(({url:e})=>{const s=t.exec(e.href);if(s&&(e.origin===location.origin||0===s.index))return s.slice(1)},e,s)}}class o{constructor(){this.t=new Map,this.i=new Map}get routes(){return this.t}addFetchListener(){self.addEventListener("fetch",t=>{const{request:e}=t,s=this.handleRequest({request:e,event:t});s&&t.respondWith(s)})}addCacheListener(){self.addEventListener("message",t=>{if(t.data&&"CACHE_URLS"===t.data.type){const{payload:e}=t.data,s=Promise.all(e.urlsToCache.map(e=>{"string"==typeof e&&(e=[e]);const s=new Request(...e);return this.handleRequest({request:s,event:t})}));t.waitUntil(s),t.ports&&t.ports[0]&&s.then(()=>t.ports[0].postMessage(!0))}})}handleRequest({request:t,event:e}){const s=new URL(t.url,location.href);if(!s.protocol.startsWith("http"))return;const n=s.origin===location.origin,{params:i,route:r}=this.findMatchingRoute({event:e,request:t,sameOrigin:n,url:s});let o=r&&r.handler;const c=t.method;if(!o&&this.i.has(c)&&(o=this.i.get(c)),!o)return;let a;try{a=o.handle({url:s,request:t,event:e,params:i})}catch(t){a=Promise.reject(t)}const h=r&&r.catchHandler;return a instanceof Promise&&(this.o||h)&&(a=a.catch(async n=>{if(h)try{return await h.handle({url:s,request:t,event:e,params:i})}catch(t){t instanceof Error&&(n=t)}if(this.o)return this.o.handle({url:s,request:t,event:e});throw n})),a}findMatchingRoute({url:t,sameOrigin:e,request:s,event:n}){const i=this.t.get(s.method)||[];for(const r of i){let i;const o=r.match({url:t,sameOrigin:e,request:s,event:n});if(o)return i=o,(Array.isArray(i)&&0===i.length||o.constructor===Object&&0===Object.keys(o).length||"boolean"==typeof o)&&(i=void 0),{route:r,params:i}}return{}}setDefaultHandler(t,e="GET"){this.i.set(e,n(t))}setCatchHandler(t){this.o=n(t)}registerRoute(t){this.t.has(t.method)||this.t.set(t.method,[]),this.t.get(t.method).push(t)}unregisterRoute(t){if(!this.t.has(t.method))throw new s("unregister-route-but-not-found-with-method",{method:t.method});const e=this.t.get(t.method).indexOf(t);if(!(e>-1))throw new s("unregister-route-route-not-registered");this.t.get(t.method).splice(e,1)}}let c;const a=()=>(c||(c=new o,c.addFetchListener(),c.addCacheListener()),c);function h(t,e,n){let o;if("string"==typeof t){const s=new URL(t,location.href);o=new i(({url:t})=>t.href===s.href,e,n)}else if(t instanceof RegExp)o=new r(t,e,n);else if("function"==typeof t)o=new i(t,e,n);else{if(!(t instanceof i))throw new s("unsupported-route-type",{moduleName:"workbox-routing",funcName:"registerRoute",paramName:"capture"});o=t}return a().registerRoute(o),o}const u={googleAnalytics:"googleAnalytics",precache:"precache-v2",prefix:"workbox",runtime:"runtime",suffix:"undefined"!=typeof registration?registration.scope:""},l=t=>[u.prefix,t,u.suffix].filter(t=>t&&t.length>0).join("-"),f=t=>t||l(u.precache),w=t=>t||l(u.runtime);function d(t,e){const s=e();return t.waitUntil(s),s}try{self["workbox:precaching:7.4.0"]&&_()}catch(t){}function p(t){if(!t)throw new s("add-to-cache-list-unexpected-type",{entry:t});if("string"==typeof t){const e=new URL(t,location.href);return{cacheKey:e.href,url:e.href}}const{revision:e,url:n}=t;if(!n)throw new s("add-to-cache-list-unexpected-type",{entry:t});if(!e){const t=new URL(n,location.href);return{cacheKey:t.href,url:t.href}}const i=new URL(n,location.href),r=new URL(n,location.href);return i.searchParams.set("__WB_REVISION__",e),{cacheKey:i.href,url:r.href}}class y{constructor(){this.updatedURLs=[],this.notUpdatedURLs=[],this.handlerWillStart=async({request:t,state:e})=>{e&&(e.originalRequest=t)},this.cachedResponseWillBeUsed=async({event:t,state:e,cachedResponse:s})=>{if("install"===t.type&&e&&e.originalRequest&&e.originalRequest instanceof Request){const t=e.originalRequest.url;s?this.notUpdatedURLs.push(t):this.updatedURLs.push(t)}return s}}}class g{constructor({precacheController:t}){this.cacheKeyWillBeUsed=async({request:t,params:e})=>{const s=(null==e?void 0:e.cacheKey)||this.h.getCacheKeyForURL(t.url);return s?new Request(s,{headers:t.headers}):t},this.h=t}}let R;async function m(t,e){let n=null;if(t.url){n=new URL(t.url).origin}if(n!==self.location.origin)throw new s("cross-origin-copy-response",{origin:n});const i=t.clone(),r={headers:new Headers(i.headers),status:i.status,statusText:i.statusText},o=function(){if(void 0===R){const t=new Response("");if("body"in t)try{new Response(t.body),R=!0}catch(t){R=!1}R=!1}return R}()?i.body:await i.blob();return new Response(o,r)}function v(t,e){const s=new URL(t);for(const t of e)s.searchParams.delete(t);return s.href}class q{constructor(){this.promise=new Promise((t,e)=>{this.resolve=t,this.reject=e})}}const U=new Set;try{self["workbox:strategies:7.4.0"]&&_()}catch(t){}function L(t){return"string"==typeof t?new Request(t):t}class b{constructor(t,e){this.u={},Object.assign(this,e),this.event=e.event,this.l=t,this.p=new q,this.R=[],this.m=[...t.plugins],this.v=new Map;for(const t of this.m)this.v.set(t,{});this.event.waitUntil(this.p.promise)}async fetch(t){const{event:e}=this;let n=L(t);if("navigate"===n.mode&&e instanceof FetchEvent&&e.preloadResponse){const t=await e.preloadResponse;if(t)return t}const i=this.hasCallback("fetchDidFail")?n.clone():null;try{for(const t of this.iterateCallbacks("requestWillFetch"))n=await t({request:n.clone(),event:e})}catch(t){if(t instanceof Error)throw new s("plugin-error-request-will-fetch",{thrownErrorMessage:t.message})}const r=n.clone();try{let t;t=await fetch(n,"navigate"===n.mode?void 0:this.l.fetchOptions);for(const s of this.iterateCallbacks("fetchDidSucceed"))t=await s({event:e,request:r,response:t});return t}catch(t){throw i&&await this.runCallbacks("fetchDidFail",{error:t,event:e,originalRequest:i.clone(),request:r.clone()}),t}}async fetchAndCachePut(t){const e=await this.fetch(t),s=e.clone();return this.waitUntil(this.cachePut(t,s)),e}async cacheMatch(t){const e=L(t);let s;const{cacheName:n,matchOptions:i}=this.l,r=await this.getCacheKey(e,"read"),o=Object.assign(Object.assign({},i),{cacheName:n});s=await caches.match(r,o);for(const t of this.iterateCallbacks("cachedResponseWillBeUsed"))s=await t({cacheName:n,matchOptions:i,cachedResponse:s,request:r,event:this.event})||void 0;return s}async cachePut(t,e){const n=L(t);var i;await(i=0,new Promise(t=>setTimeout(t,i)));const r=await this.getCacheKey(n,"write");if(!e)throw new s("cache-put-with-no-response",{url:(o=r.url,new URL(String(o),location.href).href.replace(new RegExp(`^${location.origin}`),""))});var o;const c=await this.q(e);if(!c)return!1;const{cacheName:a,matchOptions:h}=this.l,u=await self.caches.open(a),l=this.hasCallback("cacheDidUpdate"),f=l?await async function(t,e,s,n){const i=v(e.url,s);if(e.url===i)return t.match(e,n);const r=Object.assign(Object.assign({},n),{ignoreSearch:!0}),o=await t.keys(e,r);for(const e of o)if(i===v(e.url,s))return t.match(e,n)}(u,r.clone(),["__WB_REVISION__"],h):null;try{await u.put(r,l?c.clone():c)}catch(t){if(t instanceof Error)throw"QuotaExceededError"===t.name&&await async function(){for(const t of U)await t()}(),t}for(const t of this.iterateCallbacks("cacheDidUpdate"))await t({cacheName:a,oldResponse:f,newResponse:c.clone(),request:r,event:this.event});return!0}async getCacheKey(t,e){const s=`${t.url} | ${e}`;if(!this.u[s]){let n=t;for(const t of this.iterateCallbacks("cacheKeyWillBeUsed"))n=L(await t({mode:e,request:n,event:this.event,params:this.params}));this.u[s]=n}return this.u[s]}hasCallback(t){for(const e of this.l.plugins)if(t in e)return!0;return!1}async runCallbacks(t,e){for(const s of this.iterateCallbacks(t))await s(e)}*iterateCallbacks(t){for(const e of this.l.plugins)if("function"==typeof e[t]){const s=this.v.get(e),n=n=>{const i=Object.assign(Object.assign({},n),{state:s});return e[t](i)};yield n}}waitUntil(t){return this.R.push(t),t}async doneWaiting(){for(;this.R.length;){const t=this.R.splice(0),e=(await Promise.allSettled(t)).find(t=>"rejected"===t.status);if(e)throw e.reason}}destroy(){this.p.resolve(null)}async q(t){let e=t,s=!1;for(const t of this.iterateCallbacks("cacheWillUpdate"))if(e=await t({request:this.request,response:e,event:this.event})||void 0,s=!0,!e)break;return s||e&&200!==e.status&&(e=void 0),e}}class C{constructor(t={}){this.cacheName=w(t.cacheName),this.plugins=t.plugins||[],this.fetchOptions=t.fetchOptions,this.matchOptions=t.matchOptions}handle(t){const[e]=this.handleAll(t);return e}handleAll(t){t instanceof FetchEvent&&(t={event:t,request:t.request});const e=t.event,s="string"==typeof t.request?new Request(t.request):t.request,n="params"in t?t.params:void 0,i=new b(this,{event:e,request:s,params:n}),r=this.U(i,s,e);return[r,this.L(r,i,s,e)]}async U(t,e,n){let i;await t.runCallbacks("handlerWillStart",{event:n,request:e});try{if(i=await this._(e,t),!i||"error"===i.type)throw new s("no-response",{url:e.url})}catch(s){if(s instanceof Error)for(const r of t.iterateCallbacks("handlerDidError"))if(i=await r({error:s,event:n,request:e}),i)break;if(!i)throw s}for(const s of t.iterateCallbacks("handlerWillRespond"))i=await s({event:n,request:e,response:i});return i}async L(t,e,s,n){let i,r;try{i=await t}catch(r){}try{await e.runCallbacks("handlerDidRespond",{event:n,request:s,response:i}),await e.doneWaiting()}catch(t){t instanceof Error&&(r=t)}if(await e.runCallbacks("handlerDidComplete",{event:n,request:s,response:i,error:r}),e.destroy(),r)throw r}}class E extends C{constructor(t={}){t.cacheName=f(t.cacheName),super(t),this.C=!1!==t.fallbackToNetwork,this.plugins.push(E.copyRedirectedCacheableResponsesPlugin)}async _(t,e){const s=await e.cacheMatch(t);return s||(e.event&&"install"===e.event.type?await this.O(t,e):await this.N(t,e))}async N(t,e){let n;const i=e.params||{};if(!this.C)throw new s("missing-precache-entry",{cacheName:this.cacheName,url:t.url});{const s=i.integrity,r=t.integrity,o=!r||r===s;n=await e.fetch(new Request(t,{integrity:"no-cors"!==t.mode?r||s:void 0})),s&&o&&"no-cors"!==t.mode&&(this.P(),await e.cachePut(t,n.clone()))}return n}async O(t,e){this.P();const n=await e.fetch(t);if(!await e.cachePut(t,n.clone()))throw new s("bad-precaching-response",{url:t.url,status:n.status});return n}P(){let t=null,e=0;for(const[s,n]of this.plugins.entries())n!==E.copyRedirectedCacheableResponsesPlugin&&(n===E.defaultPrecacheCacheabilityPlugin&&(t=s),n.cacheWillUpdate&&e++);0===e?this.plugins.push(E.defaultPrecacheCacheabilityPlugin):e>1&&null!==t&&this.plugins.splice(t,1)}}E.defaultPrecacheCacheabilityPlugin={cacheWillUpdate:async({response:t})=>!t||t.status>=400?null:t},E.copyRedirectedCacheableResponsesPlugin={cacheWillUpdate:async({response:t})=>t.redirected?await m(t):t};class O{constructor({cacheName:t,plugins:e=[],fallbackToNetwork:s=!0}={}){this.j=new Map,this.k=new Map,this.K=new Map,this.l=new E({cacheName:f(t),plugins:[...e,new g({precacheController:this})],fallbackToNetwork:s}),this.install=this.install.bind(this),this.activate=this.activate.bind(this)}get strategy(){return this.l}precache(t){this.addToCacheList(t),this.T||(self.addEventListener("install",this.install),self.addEventListener("activate",this.activate),this.T=!0)}addToCacheList(t){const e=[];for(const n of t){"string"==typeof n?e.push(n):n&&void 0===n.revision&&e.push(n.url);const{cacheKey:t,url:i}=p(n),r="string"!=typeof n&&n.revision?"reload":"default";if(this.j.has(i)&&this.j.get(i)!==t)throw new s("add-to-cache-list-conflicting-entries",{firstEntry:this.j.get(i),secondEntry:t});if("string"!=typeof n&&n.integrity){if(this.K.has(t)&&this.K.get(t)!==n.integrity)throw new s("add-to-cache-list-conflicting-integrities",{url:i});this.K.set(t,n.integrity)}if(this.j.set(i,t),this.k.set(i,r),e.length>0){const t=`Workbox is precaching URLs without revision info: ${e.join(", ")}\nThis is generally NOT safe. Learn more at https://bit.ly/wb-precache`;console.warn(t)}}}install(t){return d(t,async()=>{const e=new y;this.strategy.plugins.push(e);for(const[e,s]of this.j){const n=this.K.get(s),i=this.k.get(e),r=new Request(e,{integrity:n,cache:i,credentials:"same-origin"});await Promise.all(this.strategy.handleAll({params:{cacheKey:s},request:r,event:t}))}const{updatedURLs:s,notUpdatedURLs:n}=e;return{updatedURLs:s,notUpdatedURLs:n}})}activate(t){return d(t,async()=>{const t=await self.caches.open(this.strategy.cacheName),e=await t.keys(),s=new Set(this.j.values()),n=[];for(const i of e)s.has(i.url)||(await t.delete(i),n.push(i.url));return{deletedURLs:n}})}getURLsToCacheKeys(){return this.j}getCachedURLs(){return[...this.j.keys()]}getCacheKeyForURL(t){const e=new URL(t,location.href);return this.j.get(e.href)}getIntegrityForCacheKey(t){return this.K.get(t)}async matchPrecache(t){const e=t instanceof Request?t.url:t,s=this.getCacheKeyForURL(e);if(s){return(await self.caches.open(this.strategy.cacheName)).match(s)}}createHandlerBoundToURL(t){const e=this.getCacheKeyForURL(t);if(!e)throw new s("non-precached-url",{url:t});return s=>(s.request=new Request(t),s.params=Object.assign({cacheKey:e},s.params),this.strategy.handle(s))}}let x;const N=()=>(x||(x=new O),x);class P extends i{constructor(t,e){super(({request:s})=>{const n=t.getURLsToCacheKeys();for(const i of function*(t,{ignoreURLParametersMatching:e=[/^utm_/,/^fbclid$/],directoryIndex:s="index.html",cleanURLs:n=!0,urlManipulation:i}={}){const r=new URL(t,location.href);r.hash="",yield r.href;const o=function(t,e=[]){for(const s of[...t.searchParams.keys()])e.some(t=>t.test(s))&&t.searchParams.delete(s);return t}(r,e);if(yield o.href,s&&o.pathname.endsWith("/")){const t=new URL(o.href);t.pathname+=s,yield t.href}if(n){const t=new URL(o.href);t.pathname+=".html",yield t.href}if(i){const t=i({url:r});for(const e of t)yield e.href}}(s.url,e)){const e=n.get(i);if(e){return{cacheKey:e,integrity:t.getIntegrityForCacheKey(e)}}}},t.strategy)}}t.NavigationRoute=class extends i{constructor(t,{allowlist:e=[/./],denylist:s=[]}={}){super(t=>this.W(t),t),this.M=e,this.S=s}W({url:t,request:e}){if(e&&"navigate"!==e.mode)return!1;const s=t.pathname+t.search;for(const t of this.S)if(t.test(s))return!1;return!!this.M.some(t=>t.test(s))}},t.cleanupOutdatedCaches=function(){self.addEventListener("activate",t=>{const e=f();t.waitUntil((async(t,e="-precache-")=>{const s=(await self.caches.keys()).filter(s=>s.includes(e)&&s.includes(self.registration.scope)&&s!==t);return await Promise.all(s.map(t=>self.caches.delete(t))),s})(e).then(t=>{}))})},t.clientsClaim=function(){self.addEventListener("activate",()=>self.clients.claim())},t.createHandlerBoundToURL=function(t){return N().createHandlerBoundToURL(t)},t.precacheAndRoute=function(t,e){!function(t){N().precache(t)}(t),function(t){const e=N();h(new P(e,t))}(e)},t.registerRoute=h});
@@ -0,0 +1,64 @@
1
+ # OpenSCAD GLTF Importer for Godot 4
2
+
3
+ This folder contains the official Godot 4.x Editor plugin for natively importing OpenSCAD (`.scad`) files as 3D scenes.
4
+
5
+ By leveraging the `scad-gltf` compiler under the hood, this addon allows you to drag and drop procedural CAD files directly into your Godot project. It automatically compiles them into binary glTF (`.glb`) meshes, supporting advanced features like **PBR materials**, **skeletal animations**, and **texture baking**—all natively within the Godot editor.
6
+
7
+ ## ✨ Features
8
+
9
+ - **Seamless Integration**: Drag and drop `.scad` files directly into the Godot FileSystem dock.
10
+ - **Advanced Materials**: Supports standard OpenSCAD plus custom extensions for roughness, metalness, transmission, emission, and more.
11
+ - **Skeletal Animations**: Automatically parses `armature()` and `bone()` modules into Godot `AnimationPlayer` and `Skeleton3D` nodes.
12
+ - **Dependency Tracking**: Smart resolution of local `include` and `use` OpenSCAD dependencies.
13
+ - **Auto-Fallback Engine**: Attempts to use the high-performance CLI compiler (`scad-convert`) and seamlessly falls back to the HTTP backend (`scad-serve`) if running in a restricted environment.
14
+
15
+ ---
16
+
17
+ ## ⚙️ Prerequisites
18
+
19
+ Because OpenSCAD compilation is handled by our WebAssembly engine running in Node.js, your system must have the compiler installed.
20
+
21
+ 1. **Install Node.js** on your system.
22
+ 2. **Install the compiler tools globally:**
23
+ ```bash
24
+ npm install -g scad-gltf
25
+ ```
26
+
27
+ ---
28
+
29
+ ## 📥 Installation
30
+
31
+ 1. Copy the `addons/scad_importer` folder from this repository into your Godot project's `res://addons/` directory.
32
+ _(If your project doesn't have an `addons` folder, create one)._
33
+ 2. Open your Godot project.
34
+ 3. Go to **Project > Project Settings > Plugins**.
35
+ 4. Check the **Enable** box next to **OpenSCAD GLTF Importer**.
36
+
37
+ ---
38
+
39
+ ## 🛠️ How It Works (Two Modes)
40
+
41
+ When you import or reimport a `.scad` file, the plugin will attempt to compile it using one of two methods:
42
+
43
+ ### 1. CLI Mode (Default)
44
+
45
+ The plugin will try to execute the `scad-convert` CLI command natively through your operating system. This requires the `scad-gltf` package to be installed globally (as shown in Prerequisites). This is the fastest and recommended method for Windows, macOS, and Linux desktops.
46
+
47
+ ### 2. Server Fallback Mode (Termux / Android / Portable)
48
+
49
+ If the CLI command fails (e.g., Godot doesn't have permission to run shell commands, or you are running Godot on an Android device via Termux), the plugin will automatically fallback to **Server Mode**.
50
+
51
+ It will look for a local backend server running on `127.0.0.1:3000`. To use this mode:
52
+
53
+ 1. Open your terminal in your project directory.
54
+ 2. Run the server using `npx`:
55
+ ```bash
56
+ npx -p scad-gltf scad-serve
57
+ ```
58
+ 3. Leave the server running in the background. Godot will now send your `.scad` code to this local server, compile it in memory, and import the resulting 3D mesh.
59
+
60
+ ---
61
+
62
+ ## 📜 License
63
+
64
+ This Godot addon is licensed under the **MIT License**.
@@ -0,0 +1,7 @@
1
+ [plugin]
2
+
3
+ name="OpenSCAD GLTF Importer"
4
+ description="Imports .scad files directly as 3D scenes using scad-gltf"
5
+ author="Ilia Grigorev"
6
+ version="0.1"
7
+ script="scad_plugin.gd"
@@ -0,0 +1,197 @@
1
+ @tool
2
+ extends EditorSceneFormatImporter
3
+
4
+ func _get_extensions():
5
+ return PackedStringArray(["scad"])
6
+
7
+ func _get_import_flags():
8
+ return EditorSceneFormatImporter.IMPORT_SCENE
9
+
10
+ func _import_scene(path: String, flags: int, options: Dictionary) -> Object:
11
+ var global_source = ProjectSettings.globalize_path(path)
12
+ var unique_id = str(hash(path))
13
+ var temp_glb_path = ProjectSettings.globalize_path("user://scad_cache_" + unique_id + ".glb")
14
+
15
+ var args = PackedStringArray()
16
+ args.append(global_source)
17
+ args.append(temp_glb_path)
18
+
19
+ var output = []
20
+ print("Importing %s via scad-convert... (This might take a few seconds on the first run)" % path.get_file())
21
+
22
+ var exit_code = -1
23
+ if OS.get_name() == "Windows":
24
+ var win_args = PackedStringArray(["/c", "scad-convert"])
25
+ win_args.append_array(args)
26
+ exit_code = OS.execute("cmd.exe", win_args, output, true)
27
+ else:
28
+ exit_code = OS.execute("scad-convert", args, output, true)
29
+
30
+ if exit_code != 0:
31
+ print("scad-convert conversion failed for %s. Attempting fallback to local scad-serve..." % path.get_file())
32
+ var fallback_success = _try_scad_serve_fallback(global_source, temp_glb_path)
33
+
34
+ if not fallback_success:
35
+ push_error("Failed to compile SCAD file: %s. Ensure Node.js is installed or scad-serve is running." % path.get_file())
36
+ push_error("scad-convert output: ", "\n".join(output))
37
+ return null
38
+
39
+ var gltf_doc = GLTFDocument.new()
40
+ var gltf_state = GLTFState.new()
41
+ var err = gltf_doc.append_from_file(temp_glb_path, gltf_state)
42
+
43
+ if FileAccess.file_exists(temp_glb_path):
44
+ DirAccess.remove_absolute(temp_glb_path)
45
+
46
+ if err != OK:
47
+ push_error("Failed to parse the generated GLB for %s." % path.get_file())
48
+ return null
49
+
50
+ var generated_scene = gltf_doc.generate_scene(gltf_state)
51
+ if generated_scene:
52
+ generated_scene.name = path.get_file().get_basename()
53
+
54
+ # Godot's scene import pipeline takes ownership over the generated node!
55
+ # It will automatically extract ImporterMeshInstance3D nodes and hook it into Advanced Scene Import.
56
+ return generated_scene
57
+
58
+ func _get_relative_path(base: String, target: String) -> String:
59
+ var base_parts = base.replace("\\", "/").split("/", false)
60
+ var target_parts = target.replace("\\", "/").split("/", false)
61
+
62
+ # If on Windows and drive letters are different, relative paths cannot be resolved
63
+ if OS.get_name() == "Windows":
64
+ if base_parts.size() > 0 and target_parts.size() > 0:
65
+ if base_parts[0].nocasecmp_to(target_parts[0]) != 0:
66
+ return target
67
+
68
+ var common_count = 0
69
+ var min_len = min(base_parts.size(), target_parts.size())
70
+ for i in range(min_len):
71
+ # Case-insensitive comparison for cross-platform compatibility
72
+ if base_parts[i].nocasecmp_to(target_parts[i]) == 0:
73
+ common_count += 1
74
+ else:
75
+ break
76
+
77
+ var rel_parts = PackedStringArray()
78
+ for i in range(common_count, base_parts.size()):
79
+ rel_parts.append("..")
80
+
81
+ for i in range(common_count, target_parts.size()):
82
+ rel_parts.append(target_parts[i])
83
+
84
+ return "/".join(rel_parts)
85
+
86
+ func _get_dependencies_recursive(file_path: String, visited: Dictionary) -> void:
87
+ if visited.has(file_path):
88
+ return
89
+
90
+ visited[file_path] = ""
91
+
92
+ if not FileAccess.file_exists(file_path):
93
+ return
94
+
95
+ var file = FileAccess.open(file_path, FileAccess.READ)
96
+ if not file:
97
+ return
98
+
99
+ var content = file.get_as_text()
100
+ file.close()
101
+
102
+ visited[file_path] = content
103
+
104
+ var regex = RegEx.new()
105
+ regex.compile("(?:include|use)\\s*[<\"]([^>\"]+)[>\"]")
106
+
107
+ var base_dir = file_path.get_base_dir()
108
+ for result in regex.search_all(content):
109
+ var dep_rel_path = result.get_string(1)
110
+ var dep_abs_path = base_dir.path_join(dep_rel_path).simplify_path()
111
+ _get_dependencies_recursive(dep_abs_path, visited)
112
+
113
+ func _get_dependencies(file_path: String) -> Dictionary:
114
+ var visited = {}
115
+ _get_dependencies_recursive(file_path, visited)
116
+ return visited
117
+
118
+ func _try_scad_serve_fallback(source_path: String, out_glb_path: String) -> bool:
119
+ var deps = _get_dependencies(source_path)
120
+ var content = deps.get(source_path, "")
121
+ deps.erase(source_path)
122
+
123
+ if content == "":
124
+ return false
125
+
126
+ var additional_files = {}
127
+ var base_dir = source_path.get_base_dir()
128
+ for dep_path in deps.keys():
129
+ var rel_path = _get_relative_path(base_dir, dep_path)
130
+ additional_files[rel_path] = deps[dep_path]
131
+
132
+ var http = HTTPClient.new()
133
+ var err = http.connect_to_host("127.0.0.1", 3000)
134
+ if err != OK:
135
+ return false
136
+
137
+ # Wait for connection (up to 5 seconds)
138
+ var max_wait = 500
139
+ var wait = 0
140
+ while http.get_status() in [HTTPClient.STATUS_CONNECTING, HTTPClient.STATUS_RESOLVING]:
141
+ http.poll()
142
+ OS.delay_msec(10)
143
+ wait += 1
144
+ if wait > max_wait:
145
+ return false
146
+
147
+ if http.get_status() != HTTPClient.STATUS_CONNECTED:
148
+ return false
149
+
150
+ var headers = PackedStringArray(["Content-Type: application/json"])
151
+
152
+ # Pack the content and dependencies inside options object
153
+ var payload = {
154
+ "content": content,
155
+ "options": {
156
+ "additionalFiles": additional_files
157
+ }
158
+ }
159
+
160
+ var body = JSON.stringify(payload)
161
+ err = http.request(HTTPClient.METHOD_POST, "/api/convert", headers, body)
162
+ if err != OK:
163
+ return false
164
+
165
+ # Wait for request to process (up to 60 seconds)
166
+ max_wait = 6000
167
+ wait = 0
168
+ while http.get_status() == HTTPClient.STATUS_REQUESTING:
169
+ http.poll()
170
+ OS.delay_msec(10)
171
+ wait += 1
172
+ if wait > max_wait:
173
+ return false
174
+
175
+ if http.has_response() and http.get_response_code() == 200:
176
+ var rb = PackedByteArray()
177
+ while http.get_status() == HTTPClient.STATUS_BODY:
178
+ http.poll()
179
+ var chunk = http.read_response_body_chunk()
180
+ if chunk.size() == 0:
181
+ OS.delay_msec(10)
182
+ else:
183
+ rb.append_array(chunk)
184
+
185
+ if rb.is_empty():
186
+ return false
187
+
188
+ var out_file = FileAccess.open(out_glb_path, FileAccess.WRITE)
189
+ if not out_file:
190
+ return false
191
+ out_file.store_buffer(rb)
192
+ out_file.close()
193
+
194
+ print("Successfully compiled %s using scad-serve fallback." % source_path.get_file())
195
+ return true
196
+
197
+ return false
@@ -0,0 +1,12 @@
1
+ @tool
2
+ extends EditorPlugin
3
+
4
+ var import_plugin
5
+
6
+ func _enter_tree():
7
+ import_plugin = preload("res://addons/scad_importer/scad_importer.gd").new()
8
+ add_scene_format_importer_plugin(import_plugin)
9
+
10
+ func _exit_tree():
11
+ remove_scene_format_importer_plugin(import_plugin)
12
+ import_plugin = null
@@ -0,0 +1,82 @@
1
+ # AI-Generated Godot Examples
2
+
3
+ This folder contains a collection of **all-in-one Node.js scripts**, each representing a complete, playable 3D Godot 4 game generated entirely by AI using the `scad-godot` CLI tool.
4
+
5
+ Instead of committing messy project folders, each example is bundled into a single, self-extracting JavaScript file. These scripts contain the generated OpenSCAD (`.scad`) 3D assets, Godot scripts (`.gd`), Godot scenes (`.tscn`), and the required `scad_importer` addon needed to parse the SCAD files directly in the engine.
6
+
7
+ ## 🚀 How to Play the Examples
8
+
9
+ To test out an example, you need to extract it and open it in Godot 4. The Godot importer relies on the `scad-gltf` tools to compile the 3D OpenSCAD scripts into binary meshes.
10
+
11
+ Depending on your operating system and environment, there are two ways to handle this.
12
+
13
+ ### Prerequisites
14
+
15
+ - **Node.js** installed on your system.
16
+ - **Godot 4.x** installed on your system.
17
+
18
+ ### Step 1: Extract the Project
19
+
20
+ Open your terminal in this folder and execute the desired example script using Node.js:
21
+
22
+ ```bash
23
+ node my_example_game.js
24
+ ```
25
+
26
+ _The script will automatically create a new folder (e.g., `./my-example-game`) containing the full Godot project directory structure, saving all the assets and addon files to disk._
27
+
28
+ ---
29
+
30
+ ### Step 2: Compile Models & Open Godot
31
+
32
+ Choose the method that best fits your environment:
33
+
34
+ #### Option A: Standard Desktop (Global Install)
35
+
36
+ If you are on a standard Windows, macOS, or Linux desktop, the easiest way is to have the CLI globally installed. The Godot plugin will automatically find and use the `scad-convert` command.
37
+
38
+ 1. Install the tool globally:
39
+ ```bash
40
+ npm install -g scad-gltf
41
+ ```
42
+ 2. Launch **Godot 4**.
43
+ 3. Click **Import** in the Project Manager, browse to the generated folder, and select the `project.godot` file.
44
+ 4. Click **Import & Edit**. The addon will automatically compile the `.scad` files in the background.
45
+
46
+ #### Option B: Termux / Android / Restricted Environments (Backend Server)
47
+
48
+ In some environments (like running Godot directly on an Android device via Termux), the Godot engine cannot easily execute globally installed NPM CLI binaries. To solve this, you can run `scad-serve` as a local backend server. The Godot importer will automatically detect it and use its REST API to convert the 3D models.
49
+
50
+ 1. Open your terminal and navigate into the newly extracted project folder:
51
+ ```bash
52
+ cd my-example-game
53
+ ```
54
+ 2. Start the local server:
55
+ ```bash
56
+ npx -p scad-gltf scad-serve
57
+ ```
58
+ _(This starts a local backend server on port 3000)._
59
+ 3. Leave the server running in the background.
60
+ 4. Launch **Godot 4**, import the project, and open it. The `scad_importer` addon will ping your local `scad-serve` backend to compile the `.scad` files over HTTP.
61
+
62
+ ---
63
+
64
+ ### Step 3: Play the Game
65
+
66
+ Once the Godot Editor opens and finishes importing the 3D scenes:
67
+
68
+ - Press **F5** (or click the Play button in the top right) to run the game!
69
+
70
+ ---
71
+
72
+ ## 🛠️ How were these generated?
73
+
74
+ These files were created using this repository's built-in AI prompt pipeline. By running the `scad-godot` CLI command, the system instructs an LLM (like Claude, Gemini, or ChatGPT) to generate procedural OpenSCAD geometry, gameplay logic, and scene configurations, outputting them as a single JS extractor script.
75
+
76
+ If you want to generate your own games, run:
77
+
78
+ ```bash
79
+ scad-godot "A simple 3D platformer where you control a rolling ball collecting coins"
80
+ ```
81
+
82
+ _(See the main repository documentation for full details on generating your own AI projects)._