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.
- package/README.md +489 -0
- package/bin/scad-convert.js +221 -0
- package/bin/scad-godot.js +247 -0
- package/bin/scad-mcp.js +508 -0
- package/bin/scad-serve.js +225 -0
- package/bin/scad-web.js +206 -0
- package/editor/dist/aristea_wreck_puresky_2k.hdr +0 -0
- package/editor/dist/assets/OutputPass-Bvl6NigM.js +4317 -0
- package/editor/dist/assets/index-V9cEH4KX.js +4318 -0
- package/editor/dist/assets/index-t9MYrExo.css +1 -0
- package/editor/dist/assets/openscad-CdBCY4mx.wasm +0 -0
- package/editor/dist/assets/preview-C1zc24MJ.js +1 -0
- package/editor/dist/assets/preview-fQfL_FJ-.css +1 -0
- package/editor/dist/assets/prompt-ui-8EFRz0ju.js +126 -0
- package/editor/dist/content-loader.js +4 -0
- package/editor/dist/content.css +255 -0
- package/editor/dist/content.js +24 -0
- package/editor/dist/icon.png +0 -0
- package/editor/dist/index.html +187 -0
- package/editor/dist/manifest.json +23 -0
- package/editor/dist/manifest.webmanifest +1 -0
- package/editor/dist/preview.html +27 -0
- package/editor/dist/registerSW.js +1 -0
- package/editor/dist/sw.js +1 -0
- package/editor/dist/workbox-9c191d2f.js +1 -0
- package/godot/README.md +64 -0
- package/godot/addons/scad_importer/plugin.cfg +7 -0
- package/godot/addons/scad_importer/scad_importer.gd +197 -0
- package/godot/addons/scad_importer/scad_plugin.gd +12 -0
- package/godot/examples/README.md +82 -0
- package/godot/examples/fruit_fusion_3d.js +1574 -0
- package/godot/examples/package.json +5 -0
- package/package.json +63 -0
- package/src/convert.js +94 -0
- package/src/ext/openscad.js +14 -0
- package/src/ext/openscad.wasm +0 -0
- 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});
|
package/godot/README.md
ADDED
|
@@ -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,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)._
|