summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--public/App.js31
-rw-r--r--public/index.html10
-rw-r--r--public/vendor/addons/libs/draco/draco_decoder.wasmbin0 -> 192420 bytes
-rw-r--r--public/vendor/addons/libs/draco/draco_wasm_wrapper.js116
-rw-r--r--public/vendor/addons/loaders/DRACOLoader.js613
-rw-r--r--public/vendor/three-setup.js2
-rw-r--r--src/SomhairlesDream.Frontend/App.fs32
-rw-r--r--src/SomhairlesDream.Frontend/Bindings.fs9
-rw-r--r--src/SomhairlesDream.Modeling/Definitions.fs102
-rw-r--r--src/SomhairlesDream.Server/Program.fs13
-rw-r--r--stage/blender/ship.py1705
11 files changed, 1861 insertions, 772 deletions
diff --git a/public/App.js b/public/App.js
index a91f298..7e73305 100644
--- a/public/App.js
+++ b/public/App.js
@@ -1,8 +1,8 @@
import { Union, Record } from "./fable_modules/fable-library-js.4.24.0/Types.js";
import { union_type, obj_type, class_type, record_type, bool_type, string_type } from "./fable_modules/fable-library-js.4.24.0/Reflection.js";
-import { comparePrimitives, defaultOf, equals, createAtom } from "./fable_modules/fable-library-js.4.24.0/Util.js";
+import { defaultOf, equals, comparePrimitives, createAtom } from "./fable_modules/fable-library-js.4.24.0/Util.js";
import { Replay_frames } from "./SomhairlesDream.Shared/Replay.js";
-import { sortBy, map, equalsWith, tryFind, iterateIndexed, item } from "./fable_modules/fable-library-js.4.24.0/Array.js";
+import { sortBy, map, equalsWith, tryFind, iterateIndexed, sort, item } from "./fable_modules/fable-library-js.4.24.0/Array.js";
import { join, isNullOrWhiteSpace, printf, toText } from "./fable_modules/fable-library-js.4.24.0/String.js";
import { MeshData_triangleIndices, MeshData_positionData } from "./SomhairlesDream.Shared/Domain.js";
import { min, max } from "./fable_modules/fable-library-js.4.24.0/Double.js";
@@ -253,19 +253,23 @@ export function frameObject(view, node) {
const size = new THREE.Vector3();
box.getCenter(center);
box.getSize(size);
- const radius = 0.5 * size.length();
- if (radius > 1E-06) {
+ const axes = sort(new Float64Array([size.x, size.y, size.z]), {
+ Compare: comparePrimitives,
+ });
+ const longest = item(2, axes);
+ const smallest = item(0, axes);
+ if (longest > 1E-06) {
node.position.set(-center.x, -center.y, -center.z);
const camera = view.Camera;
- const aspect = max(1, view.Canvas.clientWidth) / max(1, view.Canvas.clientHeight);
+ const aspect = max(0.3, max(1, view.Canvas.clientWidth) / max(1, view.Canvas.clientHeight));
camera.aspect = aspect;
const fovY = (camera.fov * 3.141592653589793) / 180;
- const limitingFov = min(fovY, 2 * Math.atan(Math.tan(fovY / 2) * aspect));
- const distance = (1.28 * radius) / Math.sin(limitingFov / 2);
- const norm = Math.sqrt(((0.62 * 0.62) + (0.45 * 0.45)) + (0.7 * 0.7));
- camera.position.set((0.62 / norm) * distance, (0.45 / norm) * distance, (0.7 / norm) * distance);
- camera.near = max(distance * 0.01, distance - (radius * 2.5));
- camera.far = (distance + (radius * 4));
+ const tanHalf = Math.tan(fovY / 2);
+ const distance = 1.25 * max((0.5 * longest) / (tanHalf * aspect), (0.5 * smallest) / tanHalf);
+ const norm = Math.sqrt(((0.3 * 0.3) + (0.44 * 0.44)) + (0.85 * 0.85));
+ camera.position.set((0.3 / norm) * distance, (0.44 / norm) * distance, (0.85 / norm) * distance);
+ camera.near = max(1, distance * 0.05);
+ camera.far = (distance + (longest * 3));
camera.lookAt(0, 0, 0);
camera.updateProjectionMatrix();
const matchValue_3 = view.Controls;
@@ -438,6 +442,9 @@ export function startStepLoad(view, owner, historical) {
loadedStep(undefined);
document.getElementById("viewport-loading").classList.toggle('is-hidden', false);
const loader = new GLTFLoader();
+ const draco = new DRACOLoader();
+ draco.setDecoderPath("/vendor/addons/libs/draco/");
+ loader.setDRACOLoader(draco);
const projectId = encodeURIComponent(owner.ProjectId);
const runId = encodeURIComponent(owner.RunId);
const stamp = ~~(Date.now() % 100000000) | 0;
@@ -884,7 +891,7 @@ export function inputValue(id) {
export const probeSelection = new RunSelection("probe-001", "run-probe-real-2", "probe-base-000", "probe-target-007", false);
-export const shipSelection = new RunSelection("ship-001", "run-ship-001", "ship-base-000", "ship-target-007", false);
+export const shipSelection = new RunSelection("ship-001", "run-ship-002", "ship-base-000", "ship-target-007", false);
export let activeModel = createAtom(probeSelection);
diff --git a/public/index.html b/public/index.html
index b52705c..84dd147 100644
--- a/public/index.html
+++ b/public/index.html
@@ -14,7 +14,7 @@
}
}
</script>
- <script type="module" src="/vendor/three-setup.js?v=orbit1"></script>
+ <script type="module" src="/vendor/three-setup.js?v=draco1"></script>
</head>
<body>
<main class="shell">
@@ -49,9 +49,9 @@
<span class="model-name">YJ-01 盘形探测器</span>
<span class="model-desc">无人星际探测器 · 光帆/RTG · 7 步构建</span>
</button>
- <button type="button" class="model-card" data-project="ship-001" data-run="run-ship-001" data-base="ship-base-000" data-target="ship-target-007">
- <span class="model-name">HNX-01 "Keel" 载人星舰</span>
- <span class="model-desc">旋转 habitat 环 · 水屏蔽 · 聚变火炬 · 7 步构建</span>
+ <button type="button" class="model-card" data-project="ship-001" data-run="run-ship-002" data-base="ship-base-000" data-target="ship-target-007">
+ <span class="model-name">HNX-02 "Copernicus" 载人行星际飞船</span>
+ <span class="model-desc">核热推进 · 旋转离心机 · 水墙屏蔽 · 7 步构建</span>
</button>
</div>
<div class="version-block">
@@ -140,6 +140,6 @@
<span>heritage 参考线保留占位演示 · F# / OPEN CODE</span>
</footer>
</main>
- <script type="module" src="/App.js?v=ui7"></script>
+ <script type="module" src="/App.js?v=ui10"></script>
</body>
</html>
diff --git a/public/vendor/addons/libs/draco/draco_decoder.wasm b/public/vendor/addons/libs/draco/draco_decoder.wasm
new file mode 100644
index 0000000..469904e
--- /dev/null
+++ b/public/vendor/addons/libs/draco/draco_decoder.wasm
Binary files differ
diff --git a/public/vendor/addons/libs/draco/draco_wasm_wrapper.js b/public/vendor/addons/libs/draco/draco_wasm_wrapper.js
new file mode 100644
index 0000000..43f1556
--- /dev/null
+++ b/public/vendor/addons/libs/draco/draco_wasm_wrapper.js
@@ -0,0 +1,116 @@
+var $jscomp=$jscomp||{};$jscomp.scope={};$jscomp.arrayIteratorImpl=function(h){var n=0;return function(){return n<h.length?{done:!1,value:h[n++]}:{done:!0}}};$jscomp.arrayIterator=function(h){return{next:$jscomp.arrayIteratorImpl(h)}};$jscomp.makeIterator=function(h){var n="undefined"!=typeof Symbol&&Symbol.iterator&&h[Symbol.iterator];return n?n.call(h):$jscomp.arrayIterator(h)};$jscomp.ASSUME_ES5=!1;$jscomp.ASSUME_NO_NATIVE_MAP=!1;$jscomp.ASSUME_NO_NATIVE_SET=!1;$jscomp.SIMPLE_FROUND_POLYFILL=!1;
+$jscomp.ISOLATE_POLYFILLS=!1;$jscomp.FORCE_POLYFILL_PROMISE=!1;$jscomp.FORCE_POLYFILL_PROMISE_WHEN_NO_UNHANDLED_REJECTION=!1;$jscomp.getGlobal=function(h){h=["object"==typeof globalThis&&globalThis,h,"object"==typeof window&&window,"object"==typeof self&&self,"object"==typeof global&&global];for(var n=0;n<h.length;++n){var k=h[n];if(k&&k.Math==Math)return k}throw Error("Cannot find global object");};$jscomp.global=$jscomp.getGlobal(this);
+$jscomp.defineProperty=$jscomp.ASSUME_ES5||"function"==typeof Object.defineProperties?Object.defineProperty:function(h,n,k){if(h==Array.prototype||h==Object.prototype)return h;h[n]=k.value;return h};$jscomp.IS_SYMBOL_NATIVE="function"===typeof Symbol&&"symbol"===typeof Symbol("x");$jscomp.TRUST_ES6_POLYFILLS=!$jscomp.ISOLATE_POLYFILLS||$jscomp.IS_SYMBOL_NATIVE;$jscomp.polyfills={};$jscomp.propertyToPolyfillSymbol={};$jscomp.POLYFILL_PREFIX="$jscp$";
+var $jscomp$lookupPolyfilledValue=function(h,n){var k=$jscomp.propertyToPolyfillSymbol[n];if(null==k)return h[n];k=h[k];return void 0!==k?k:h[n]};$jscomp.polyfill=function(h,n,k,p){n&&($jscomp.ISOLATE_POLYFILLS?$jscomp.polyfillIsolated(h,n,k,p):$jscomp.polyfillUnisolated(h,n,k,p))};
+$jscomp.polyfillUnisolated=function(h,n,k,p){k=$jscomp.global;h=h.split(".");for(p=0;p<h.length-1;p++){var l=h[p];if(!(l in k))return;k=k[l]}h=h[h.length-1];p=k[h];n=n(p);n!=p&&null!=n&&$jscomp.defineProperty(k,h,{configurable:!0,writable:!0,value:n})};
+$jscomp.polyfillIsolated=function(h,n,k,p){var l=h.split(".");h=1===l.length;p=l[0];p=!h&&p in $jscomp.polyfills?$jscomp.polyfills:$jscomp.global;for(var y=0;y<l.length-1;y++){var f=l[y];if(!(f in p))return;p=p[f]}l=l[l.length-1];k=$jscomp.IS_SYMBOL_NATIVE&&"es6"===k?p[l]:null;n=n(k);null!=n&&(h?$jscomp.defineProperty($jscomp.polyfills,l,{configurable:!0,writable:!0,value:n}):n!==k&&(void 0===$jscomp.propertyToPolyfillSymbol[l]&&(k=1E9*Math.random()>>>0,$jscomp.propertyToPolyfillSymbol[l]=$jscomp.IS_SYMBOL_NATIVE?
+$jscomp.global.Symbol(l):$jscomp.POLYFILL_PREFIX+k+"$"+l),$jscomp.defineProperty(p,$jscomp.propertyToPolyfillSymbol[l],{configurable:!0,writable:!0,value:n})))};
+$jscomp.polyfill("Promise",function(h){function n(){this.batch_=null}function k(f){return f instanceof l?f:new l(function(q,u){q(f)})}if(h&&(!($jscomp.FORCE_POLYFILL_PROMISE||$jscomp.FORCE_POLYFILL_PROMISE_WHEN_NO_UNHANDLED_REJECTION&&"undefined"===typeof $jscomp.global.PromiseRejectionEvent)||!$jscomp.global.Promise||-1===$jscomp.global.Promise.toString().indexOf("[native code]")))return h;n.prototype.asyncExecute=function(f){if(null==this.batch_){this.batch_=[];var q=this;this.asyncExecuteFunction(function(){q.executeBatch_()})}this.batch_.push(f)};
+var p=$jscomp.global.setTimeout;n.prototype.asyncExecuteFunction=function(f){p(f,0)};n.prototype.executeBatch_=function(){for(;this.batch_&&this.batch_.length;){var f=this.batch_;this.batch_=[];for(var q=0;q<f.length;++q){var u=f[q];f[q]=null;try{u()}catch(A){this.asyncThrow_(A)}}}this.batch_=null};n.prototype.asyncThrow_=function(f){this.asyncExecuteFunction(function(){throw f;})};var l=function(f){this.state_=0;this.result_=void 0;this.onSettledCallbacks_=[];this.isRejectionHandled_=!1;var q=this.createResolveAndReject_();
+try{f(q.resolve,q.reject)}catch(u){q.reject(u)}};l.prototype.createResolveAndReject_=function(){function f(A){return function(F){u||(u=!0,A.call(q,F))}}var q=this,u=!1;return{resolve:f(this.resolveTo_),reject:f(this.reject_)}};l.prototype.resolveTo_=function(f){if(f===this)this.reject_(new TypeError("A Promise cannot resolve to itself"));else if(f instanceof l)this.settleSameAsPromise_(f);else{a:switch(typeof f){case "object":var q=null!=f;break a;case "function":q=!0;break a;default:q=!1}q?this.resolveToNonPromiseObj_(f):
+this.fulfill_(f)}};l.prototype.resolveToNonPromiseObj_=function(f){var q=void 0;try{q=f.then}catch(u){this.reject_(u);return}"function"==typeof q?this.settleSameAsThenable_(q,f):this.fulfill_(f)};l.prototype.reject_=function(f){this.settle_(2,f)};l.prototype.fulfill_=function(f){this.settle_(1,f)};l.prototype.settle_=function(f,q){if(0!=this.state_)throw Error("Cannot settle("+f+", "+q+"): Promise already settled in state"+this.state_);this.state_=f;this.result_=q;2===this.state_&&this.scheduleUnhandledRejectionCheck_();
+this.executeOnSettledCallbacks_()};l.prototype.scheduleUnhandledRejectionCheck_=function(){var f=this;p(function(){if(f.notifyUnhandledRejection_()){var q=$jscomp.global.console;"undefined"!==typeof q&&q.error(f.result_)}},1)};l.prototype.notifyUnhandledRejection_=function(){if(this.isRejectionHandled_)return!1;var f=$jscomp.global.CustomEvent,q=$jscomp.global.Event,u=$jscomp.global.dispatchEvent;if("undefined"===typeof u)return!0;"function"===typeof f?f=new f("unhandledrejection",{cancelable:!0}):
+"function"===typeof q?f=new q("unhandledrejection",{cancelable:!0}):(f=$jscomp.global.document.createEvent("CustomEvent"),f.initCustomEvent("unhandledrejection",!1,!0,f));f.promise=this;f.reason=this.result_;return u(f)};l.prototype.executeOnSettledCallbacks_=function(){if(null!=this.onSettledCallbacks_){for(var f=0;f<this.onSettledCallbacks_.length;++f)y.asyncExecute(this.onSettledCallbacks_[f]);this.onSettledCallbacks_=null}};var y=new n;l.prototype.settleSameAsPromise_=function(f){var q=this.createResolveAndReject_();
+f.callWhenSettled_(q.resolve,q.reject)};l.prototype.settleSameAsThenable_=function(f,q){var u=this.createResolveAndReject_();try{f.call(q,u.resolve,u.reject)}catch(A){u.reject(A)}};l.prototype.then=function(f,q){function u(w,B){return"function"==typeof w?function(R){try{A(w(R))}catch(Z){F(Z)}}:B}var A,F,v=new l(function(w,B){A=w;F=B});this.callWhenSettled_(u(f,A),u(q,F));return v};l.prototype.catch=function(f){return this.then(void 0,f)};l.prototype.callWhenSettled_=function(f,q){function u(){switch(A.state_){case 1:f(A.result_);
+break;case 2:q(A.result_);break;default:throw Error("Unexpected state: "+A.state_);}}var A=this;null==this.onSettledCallbacks_?y.asyncExecute(u):this.onSettledCallbacks_.push(u);this.isRejectionHandled_=!0};l.resolve=k;l.reject=function(f){return new l(function(q,u){u(f)})};l.race=function(f){return new l(function(q,u){for(var A=$jscomp.makeIterator(f),F=A.next();!F.done;F=A.next())k(F.value).callWhenSettled_(q,u)})};l.all=function(f){var q=$jscomp.makeIterator(f),u=q.next();return u.done?k([]):new l(function(A,
+F){function v(R){return function(Z){w[R]=Z;B--;0==B&&A(w)}}var w=[],B=0;do w.push(void 0),B++,k(u.value).callWhenSettled_(v(w.length-1),F),u=q.next();while(!u.done)})};return l},"es6","es3");$jscomp.owns=function(h,n){return Object.prototype.hasOwnProperty.call(h,n)};$jscomp.assign=$jscomp.TRUST_ES6_POLYFILLS&&"function"==typeof Object.assign?Object.assign:function(h,n){for(var k=1;k<arguments.length;k++){var p=arguments[k];if(p)for(var l in p)$jscomp.owns(p,l)&&(h[l]=p[l])}return h};
+$jscomp.polyfill("Object.assign",function(h){return h||$jscomp.assign},"es6","es3");$jscomp.checkStringArgs=function(h,n,k){if(null==h)throw new TypeError("The 'this' value for String.prototype."+k+" must not be null or undefined");if(n instanceof RegExp)throw new TypeError("First argument to String.prototype."+k+" must not be a regular expression");return h+""};
+$jscomp.polyfill("String.prototype.startsWith",function(h){return h?h:function(n,k){var p=$jscomp.checkStringArgs(this,n,"startsWith");n+="";var l=p.length,y=n.length;k=Math.max(0,Math.min(k|0,p.length));for(var f=0;f<y&&k<l;)if(p[k++]!=n[f++])return!1;return f>=y}},"es6","es3");
+$jscomp.polyfill("Array.prototype.copyWithin",function(h){function n(k){k=Number(k);return Infinity===k||-Infinity===k?k:k|0}return h?h:function(k,p,l){var y=this.length;k=n(k);p=n(p);l=void 0===l?y:n(l);k=0>k?Math.max(y+k,0):Math.min(k,y);p=0>p?Math.max(y+p,0):Math.min(p,y);l=0>l?Math.max(y+l,0):Math.min(l,y);if(k<p)for(;p<l;)p in this?this[k++]=this[p++]:(delete this[k++],p++);else for(l=Math.min(l,y+p-k),k+=l-p;l>p;)--l in this?this[--k]=this[l]:delete this[--k];return this}},"es6","es3");
+$jscomp.typedArrayCopyWithin=function(h){return h?h:Array.prototype.copyWithin};$jscomp.polyfill("Int8Array.prototype.copyWithin",$jscomp.typedArrayCopyWithin,"es6","es5");$jscomp.polyfill("Uint8Array.prototype.copyWithin",$jscomp.typedArrayCopyWithin,"es6","es5");$jscomp.polyfill("Uint8ClampedArray.prototype.copyWithin",$jscomp.typedArrayCopyWithin,"es6","es5");$jscomp.polyfill("Int16Array.prototype.copyWithin",$jscomp.typedArrayCopyWithin,"es6","es5");
+$jscomp.polyfill("Uint16Array.prototype.copyWithin",$jscomp.typedArrayCopyWithin,"es6","es5");$jscomp.polyfill("Int32Array.prototype.copyWithin",$jscomp.typedArrayCopyWithin,"es6","es5");$jscomp.polyfill("Uint32Array.prototype.copyWithin",$jscomp.typedArrayCopyWithin,"es6","es5");$jscomp.polyfill("Float32Array.prototype.copyWithin",$jscomp.typedArrayCopyWithin,"es6","es5");$jscomp.polyfill("Float64Array.prototype.copyWithin",$jscomp.typedArrayCopyWithin,"es6","es5");
+var DracoDecoderModule=function(){var h="undefined"!==typeof document&&document.currentScript?document.currentScript.src:void 0;"undefined"!==typeof __filename&&(h=h||__filename);return function(n){function k(e){return a.locateFile?a.locateFile(e,U):U+e}function p(e,b){if(e){var c=ia;var d=e+b;for(b=e;c[b]&&!(b>=d);)++b;if(16<b-e&&c.buffer&&ra)c=ra.decode(c.subarray(e,b));else{for(d="";e<b;){var g=c[e++];if(g&128){var t=c[e++]&63;if(192==(g&224))d+=String.fromCharCode((g&31)<<6|t);else{var aa=c[e++]&
+63;g=224==(g&240)?(g&15)<<12|t<<6|aa:(g&7)<<18|t<<12|aa<<6|c[e++]&63;65536>g?d+=String.fromCharCode(g):(g-=65536,d+=String.fromCharCode(55296|g>>10,56320|g&1023))}}else d+=String.fromCharCode(g)}c=d}}else c="";return c}function l(){var e=ja.buffer;a.HEAP8=W=new Int8Array(e);a.HEAP16=new Int16Array(e);a.HEAP32=ca=new Int32Array(e);a.HEAPU8=ia=new Uint8Array(e);a.HEAPU16=new Uint16Array(e);a.HEAPU32=Y=new Uint32Array(e);a.HEAPF32=new Float32Array(e);a.HEAPF64=new Float64Array(e)}function y(e){if(a.onAbort)a.onAbort(e);
+e="Aborted("+e+")";da(e);sa=!0;e=new WebAssembly.RuntimeError(e+". Build with -sASSERTIONS for more info.");ka(e);throw e;}function f(e){try{if(e==P&&ea)return new Uint8Array(ea);if(ma)return ma(e);throw"both async and sync fetching of the wasm failed";}catch(b){y(b)}}function q(){if(!ea&&(ta||fa)){if("function"==typeof fetch&&!P.startsWith("file://"))return fetch(P,{credentials:"same-origin"}).then(function(e){if(!e.ok)throw"failed to load wasm binary file at '"+P+"'";return e.arrayBuffer()}).catch(function(){return f(P)});
+if(na)return new Promise(function(e,b){na(P,function(c){e(new Uint8Array(c))},b)})}return Promise.resolve().then(function(){return f(P)})}function u(e){for(;0<e.length;)e.shift()(a)}function A(e){this.excPtr=e;this.ptr=e-24;this.set_type=function(b){Y[this.ptr+4>>2]=b};this.get_type=function(){return Y[this.ptr+4>>2]};this.set_destructor=function(b){Y[this.ptr+8>>2]=b};this.get_destructor=function(){return Y[this.ptr+8>>2]};this.set_refcount=function(b){ca[this.ptr>>2]=b};this.set_caught=function(b){W[this.ptr+
+12>>0]=b?1:0};this.get_caught=function(){return 0!=W[this.ptr+12>>0]};this.set_rethrown=function(b){W[this.ptr+13>>0]=b?1:0};this.get_rethrown=function(){return 0!=W[this.ptr+13>>0]};this.init=function(b,c){this.set_adjusted_ptr(0);this.set_type(b);this.set_destructor(c);this.set_refcount(0);this.set_caught(!1);this.set_rethrown(!1)};this.add_ref=function(){ca[this.ptr>>2]+=1};this.release_ref=function(){var b=ca[this.ptr>>2];ca[this.ptr>>2]=b-1;return 1===b};this.set_adjusted_ptr=function(b){Y[this.ptr+
+16>>2]=b};this.get_adjusted_ptr=function(){return Y[this.ptr+16>>2]};this.get_exception_ptr=function(){if(ua(this.get_type()))return Y[this.excPtr>>2];var b=this.get_adjusted_ptr();return 0!==b?b:this.excPtr}}function F(){function e(){if(!la&&(la=!0,a.calledRun=!0,!sa)){va=!0;u(oa);wa(a);if(a.onRuntimeInitialized)a.onRuntimeInitialized();if(a.postRun)for("function"==typeof a.postRun&&(a.postRun=[a.postRun]);a.postRun.length;)xa.unshift(a.postRun.shift());u(xa)}}if(!(0<ba)){if(a.preRun)for("function"==
+typeof a.preRun&&(a.preRun=[a.preRun]);a.preRun.length;)ya.unshift(a.preRun.shift());u(ya);0<ba||(a.setStatus?(a.setStatus("Running..."),setTimeout(function(){setTimeout(function(){a.setStatus("")},1);e()},1)):e())}}function v(){}function w(e){return(e||v).__cache__}function B(e,b){var c=w(b),d=c[e];if(d)return d;d=Object.create((b||v).prototype);d.ptr=e;return c[e]=d}function R(e){if("string"===typeof e){for(var b=0,c=0;c<e.length;++c){var d=e.charCodeAt(c);127>=d?b++:2047>=d?b+=2:55296<=d&&57343>=
+d?(b+=4,++c):b+=3}b=Array(b+1);c=0;d=b.length;if(0<d){d=c+d-1;for(var g=0;g<e.length;++g){var t=e.charCodeAt(g);if(55296<=t&&57343>=t){var aa=e.charCodeAt(++g);t=65536+((t&1023)<<10)|aa&1023}if(127>=t){if(c>=d)break;b[c++]=t}else{if(2047>=t){if(c+1>=d)break;b[c++]=192|t>>6}else{if(65535>=t){if(c+2>=d)break;b[c++]=224|t>>12}else{if(c+3>=d)break;b[c++]=240|t>>18;b[c++]=128|t>>12&63}b[c++]=128|t>>6&63}b[c++]=128|t&63}}b[c]=0}e=r.alloc(b,W);r.copy(b,W,e);return e}return e}function Z(e){if("object"===
+typeof e){var b=r.alloc(e,W);r.copy(e,W,b);return b}return e}function X(){throw"cannot construct a VoidPtr, no constructor in IDL";}function S(){this.ptr=za();w(S)[this.ptr]=this}function Q(){this.ptr=Aa();w(Q)[this.ptr]=this}function V(){this.ptr=Ba();w(V)[this.ptr]=this}function x(){this.ptr=Ca();w(x)[this.ptr]=this}function D(){this.ptr=Da();w(D)[this.ptr]=this}function G(){this.ptr=Ea();w(G)[this.ptr]=this}function H(){this.ptr=Fa();w(H)[this.ptr]=this}function E(){this.ptr=Ga();w(E)[this.ptr]=
+this}function T(){this.ptr=Ha();w(T)[this.ptr]=this}function C(){throw"cannot construct a Status, no constructor in IDL";}function I(){this.ptr=Ia();w(I)[this.ptr]=this}function J(){this.ptr=Ja();w(J)[this.ptr]=this}function K(){this.ptr=Ka();w(K)[this.ptr]=this}function L(){this.ptr=La();w(L)[this.ptr]=this}function M(){this.ptr=Ma();w(M)[this.ptr]=this}function N(){this.ptr=Na();w(N)[this.ptr]=this}function O(){this.ptr=Oa();w(O)[this.ptr]=this}function z(){this.ptr=Pa();w(z)[this.ptr]=this}function m(){this.ptr=
+Qa();w(m)[this.ptr]=this}n=void 0===n?{}:n;var a="undefined"!=typeof n?n:{},wa,ka;a.ready=new Promise(function(e,b){wa=e;ka=b});var Ra=!1,Sa=!1;a.onRuntimeInitialized=function(){Ra=!0;if(Sa&&"function"===typeof a.onModuleLoaded)a.onModuleLoaded(a)};a.onModuleParsed=function(){Sa=!0;if(Ra&&"function"===typeof a.onModuleLoaded)a.onModuleLoaded(a)};a.isVersionSupported=function(e){if("string"!==typeof e)return!1;e=e.split(".");return 2>e.length||3<e.length?!1:1==e[0]&&0<=e[1]&&5>=e[1]?!0:0!=e[0]||10<
+e[1]?!1:!0};var Ta=Object.assign({},a),ta="object"==typeof window,fa="function"==typeof importScripts,Ua="object"==typeof process&&"object"==typeof process.versions&&"string"==typeof process.versions.node,U="";if(Ua){var Va=require("fs"),pa=require("path");U=fa?pa.dirname(U)+"/":__dirname+"/";var Wa=function(e,b){e=e.startsWith("file://")?new URL(e):pa.normalize(e);return Va.readFileSync(e,b?void 0:"utf8")};var ma=function(e){e=Wa(e,!0);e.buffer||(e=new Uint8Array(e));return e};var na=function(e,
+b,c){e=e.startsWith("file://")?new URL(e):pa.normalize(e);Va.readFile(e,function(d,g){d?c(d):b(g.buffer)})};1<process.argv.length&&process.argv[1].replace(/\\/g,"/");process.argv.slice(2);a.inspect=function(){return"[Emscripten Module object]"}}else if(ta||fa)fa?U=self.location.href:"undefined"!=typeof document&&document.currentScript&&(U=document.currentScript.src),h&&(U=h),U=0!==U.indexOf("blob:")?U.substr(0,U.replace(/[?#].*/,"").lastIndexOf("/")+1):"",Wa=function(e){var b=new XMLHttpRequest;b.open("GET",
+e,!1);b.send(null);return b.responseText},fa&&(ma=function(e){var b=new XMLHttpRequest;b.open("GET",e,!1);b.responseType="arraybuffer";b.send(null);return new Uint8Array(b.response)}),na=function(e,b,c){var d=new XMLHttpRequest;d.open("GET",e,!0);d.responseType="arraybuffer";d.onload=function(){200==d.status||0==d.status&&d.response?b(d.response):c()};d.onerror=c;d.send(null)};a.print||console.log.bind(console);var da=a.printErr||console.warn.bind(console);Object.assign(a,Ta);Ta=null;var ea;a.wasmBinary&&
+(ea=a.wasmBinary);"object"!=typeof WebAssembly&&y("no native wasm support detected");var ja,sa=!1,ra="undefined"!=typeof TextDecoder?new TextDecoder("utf8"):void 0,W,ia,ca,Y,ya=[],oa=[],xa=[],va=!1,ba=0,qa=null,ha=null;var P="draco_decoder_gltf.wasm";P.startsWith("data:application/octet-stream;base64,")||(P=k(P));var pd=0,qd={b:function(e,b,c){(new A(e)).init(b,c);pd++;throw e;},a:function(){y("")},d:function(e,b,c){ia.copyWithin(e,b,b+c)},c:function(e){var b=ia.length;e>>>=0;if(2147483648<e)return!1;
+for(var c=1;4>=c;c*=2){var d=b*(1+.2/c);d=Math.min(d,e+100663296);var g=Math;d=Math.max(e,d);g=g.min.call(g,2147483648,d+(65536-d%65536)%65536);a:{d=ja.buffer;try{ja.grow(g-d.byteLength+65535>>>16);l();var t=1;break a}catch(aa){}t=void 0}if(t)return!0}return!1}};(function(){function e(g,t){a.asm=g.exports;ja=a.asm.e;l();oa.unshift(a.asm.f);ba--;a.monitorRunDependencies&&a.monitorRunDependencies(ba);0==ba&&(null!==qa&&(clearInterval(qa),qa=null),ha&&(g=ha,ha=null,g()))}function b(g){e(g.instance)}
+function c(g){return q().then(function(t){return WebAssembly.instantiate(t,d)}).then(function(t){return t}).then(g,function(t){da("failed to asynchronously prepare wasm: "+t);y(t)})}var d={a:qd};ba++;a.monitorRunDependencies&&a.monitorRunDependencies(ba);if(a.instantiateWasm)try{return a.instantiateWasm(d,e)}catch(g){da("Module.instantiateWasm callback failed with error: "+g),ka(g)}(function(){return ea||"function"!=typeof WebAssembly.instantiateStreaming||P.startsWith("data:application/octet-stream;base64,")||
+P.startsWith("file://")||Ua||"function"!=typeof fetch?c(b):fetch(P,{credentials:"same-origin"}).then(function(g){return WebAssembly.instantiateStreaming(g,d).then(b,function(t){da("wasm streaming compile failed: "+t);da("falling back to ArrayBuffer instantiation");return c(b)})})})().catch(ka);return{}})();var Xa=a._emscripten_bind_VoidPtr___destroy___0=function(){return(Xa=a._emscripten_bind_VoidPtr___destroy___0=a.asm.h).apply(null,arguments)},za=a._emscripten_bind_DecoderBuffer_DecoderBuffer_0=
+function(){return(za=a._emscripten_bind_DecoderBuffer_DecoderBuffer_0=a.asm.i).apply(null,arguments)},Ya=a._emscripten_bind_DecoderBuffer_Init_2=function(){return(Ya=a._emscripten_bind_DecoderBuffer_Init_2=a.asm.j).apply(null,arguments)},Za=a._emscripten_bind_DecoderBuffer___destroy___0=function(){return(Za=a._emscripten_bind_DecoderBuffer___destroy___0=a.asm.k).apply(null,arguments)},Aa=a._emscripten_bind_AttributeTransformData_AttributeTransformData_0=function(){return(Aa=a._emscripten_bind_AttributeTransformData_AttributeTransformData_0=
+a.asm.l).apply(null,arguments)},$a=a._emscripten_bind_AttributeTransformData_transform_type_0=function(){return($a=a._emscripten_bind_AttributeTransformData_transform_type_0=a.asm.m).apply(null,arguments)},ab=a._emscripten_bind_AttributeTransformData___destroy___0=function(){return(ab=a._emscripten_bind_AttributeTransformData___destroy___0=a.asm.n).apply(null,arguments)},Ba=a._emscripten_bind_GeometryAttribute_GeometryAttribute_0=function(){return(Ba=a._emscripten_bind_GeometryAttribute_GeometryAttribute_0=
+a.asm.o).apply(null,arguments)},bb=a._emscripten_bind_GeometryAttribute___destroy___0=function(){return(bb=a._emscripten_bind_GeometryAttribute___destroy___0=a.asm.p).apply(null,arguments)},Ca=a._emscripten_bind_PointAttribute_PointAttribute_0=function(){return(Ca=a._emscripten_bind_PointAttribute_PointAttribute_0=a.asm.q).apply(null,arguments)},cb=a._emscripten_bind_PointAttribute_size_0=function(){return(cb=a._emscripten_bind_PointAttribute_size_0=a.asm.r).apply(null,arguments)},db=a._emscripten_bind_PointAttribute_GetAttributeTransformData_0=
+function(){return(db=a._emscripten_bind_PointAttribute_GetAttributeTransformData_0=a.asm.s).apply(null,arguments)},eb=a._emscripten_bind_PointAttribute_attribute_type_0=function(){return(eb=a._emscripten_bind_PointAttribute_attribute_type_0=a.asm.t).apply(null,arguments)},fb=a._emscripten_bind_PointAttribute_data_type_0=function(){return(fb=a._emscripten_bind_PointAttribute_data_type_0=a.asm.u).apply(null,arguments)},gb=a._emscripten_bind_PointAttribute_num_components_0=function(){return(gb=a._emscripten_bind_PointAttribute_num_components_0=
+a.asm.v).apply(null,arguments)},hb=a._emscripten_bind_PointAttribute_normalized_0=function(){return(hb=a._emscripten_bind_PointAttribute_normalized_0=a.asm.w).apply(null,arguments)},ib=a._emscripten_bind_PointAttribute_byte_stride_0=function(){return(ib=a._emscripten_bind_PointAttribute_byte_stride_0=a.asm.x).apply(null,arguments)},jb=a._emscripten_bind_PointAttribute_byte_offset_0=function(){return(jb=a._emscripten_bind_PointAttribute_byte_offset_0=a.asm.y).apply(null,arguments)},kb=a._emscripten_bind_PointAttribute_unique_id_0=
+function(){return(kb=a._emscripten_bind_PointAttribute_unique_id_0=a.asm.z).apply(null,arguments)},lb=a._emscripten_bind_PointAttribute___destroy___0=function(){return(lb=a._emscripten_bind_PointAttribute___destroy___0=a.asm.A).apply(null,arguments)},Da=a._emscripten_bind_AttributeQuantizationTransform_AttributeQuantizationTransform_0=function(){return(Da=a._emscripten_bind_AttributeQuantizationTransform_AttributeQuantizationTransform_0=a.asm.B).apply(null,arguments)},mb=a._emscripten_bind_AttributeQuantizationTransform_InitFromAttribute_1=
+function(){return(mb=a._emscripten_bind_AttributeQuantizationTransform_InitFromAttribute_1=a.asm.C).apply(null,arguments)},nb=a._emscripten_bind_AttributeQuantizationTransform_quantization_bits_0=function(){return(nb=a._emscripten_bind_AttributeQuantizationTransform_quantization_bits_0=a.asm.D).apply(null,arguments)},ob=a._emscripten_bind_AttributeQuantizationTransform_min_value_1=function(){return(ob=a._emscripten_bind_AttributeQuantizationTransform_min_value_1=a.asm.E).apply(null,arguments)},pb=
+a._emscripten_bind_AttributeQuantizationTransform_range_0=function(){return(pb=a._emscripten_bind_AttributeQuantizationTransform_range_0=a.asm.F).apply(null,arguments)},qb=a._emscripten_bind_AttributeQuantizationTransform___destroy___0=function(){return(qb=a._emscripten_bind_AttributeQuantizationTransform___destroy___0=a.asm.G).apply(null,arguments)},Ea=a._emscripten_bind_AttributeOctahedronTransform_AttributeOctahedronTransform_0=function(){return(Ea=a._emscripten_bind_AttributeOctahedronTransform_AttributeOctahedronTransform_0=
+a.asm.H).apply(null,arguments)},rb=a._emscripten_bind_AttributeOctahedronTransform_InitFromAttribute_1=function(){return(rb=a._emscripten_bind_AttributeOctahedronTransform_InitFromAttribute_1=a.asm.I).apply(null,arguments)},sb=a._emscripten_bind_AttributeOctahedronTransform_quantization_bits_0=function(){return(sb=a._emscripten_bind_AttributeOctahedronTransform_quantization_bits_0=a.asm.J).apply(null,arguments)},tb=a._emscripten_bind_AttributeOctahedronTransform___destroy___0=function(){return(tb=
+a._emscripten_bind_AttributeOctahedronTransform___destroy___0=a.asm.K).apply(null,arguments)},Fa=a._emscripten_bind_PointCloud_PointCloud_0=function(){return(Fa=a._emscripten_bind_PointCloud_PointCloud_0=a.asm.L).apply(null,arguments)},ub=a._emscripten_bind_PointCloud_num_attributes_0=function(){return(ub=a._emscripten_bind_PointCloud_num_attributes_0=a.asm.M).apply(null,arguments)},vb=a._emscripten_bind_PointCloud_num_points_0=function(){return(vb=a._emscripten_bind_PointCloud_num_points_0=a.asm.N).apply(null,
+arguments)},wb=a._emscripten_bind_PointCloud___destroy___0=function(){return(wb=a._emscripten_bind_PointCloud___destroy___0=a.asm.O).apply(null,arguments)},Ga=a._emscripten_bind_Mesh_Mesh_0=function(){return(Ga=a._emscripten_bind_Mesh_Mesh_0=a.asm.P).apply(null,arguments)},xb=a._emscripten_bind_Mesh_num_faces_0=function(){return(xb=a._emscripten_bind_Mesh_num_faces_0=a.asm.Q).apply(null,arguments)},yb=a._emscripten_bind_Mesh_num_attributes_0=function(){return(yb=a._emscripten_bind_Mesh_num_attributes_0=
+a.asm.R).apply(null,arguments)},zb=a._emscripten_bind_Mesh_num_points_0=function(){return(zb=a._emscripten_bind_Mesh_num_points_0=a.asm.S).apply(null,arguments)},Ab=a._emscripten_bind_Mesh___destroy___0=function(){return(Ab=a._emscripten_bind_Mesh___destroy___0=a.asm.T).apply(null,arguments)},Ha=a._emscripten_bind_Metadata_Metadata_0=function(){return(Ha=a._emscripten_bind_Metadata_Metadata_0=a.asm.U).apply(null,arguments)},Bb=a._emscripten_bind_Metadata___destroy___0=function(){return(Bb=a._emscripten_bind_Metadata___destroy___0=
+a.asm.V).apply(null,arguments)},Cb=a._emscripten_bind_Status_code_0=function(){return(Cb=a._emscripten_bind_Status_code_0=a.asm.W).apply(null,arguments)},Db=a._emscripten_bind_Status_ok_0=function(){return(Db=a._emscripten_bind_Status_ok_0=a.asm.X).apply(null,arguments)},Eb=a._emscripten_bind_Status_error_msg_0=function(){return(Eb=a._emscripten_bind_Status_error_msg_0=a.asm.Y).apply(null,arguments)},Fb=a._emscripten_bind_Status___destroy___0=function(){return(Fb=a._emscripten_bind_Status___destroy___0=
+a.asm.Z).apply(null,arguments)},Ia=a._emscripten_bind_DracoFloat32Array_DracoFloat32Array_0=function(){return(Ia=a._emscripten_bind_DracoFloat32Array_DracoFloat32Array_0=a.asm._).apply(null,arguments)},Gb=a._emscripten_bind_DracoFloat32Array_GetValue_1=function(){return(Gb=a._emscripten_bind_DracoFloat32Array_GetValue_1=a.asm.$).apply(null,arguments)},Hb=a._emscripten_bind_DracoFloat32Array_size_0=function(){return(Hb=a._emscripten_bind_DracoFloat32Array_size_0=a.asm.aa).apply(null,arguments)},Ib=
+a._emscripten_bind_DracoFloat32Array___destroy___0=function(){return(Ib=a._emscripten_bind_DracoFloat32Array___destroy___0=a.asm.ba).apply(null,arguments)},Ja=a._emscripten_bind_DracoInt8Array_DracoInt8Array_0=function(){return(Ja=a._emscripten_bind_DracoInt8Array_DracoInt8Array_0=a.asm.ca).apply(null,arguments)},Jb=a._emscripten_bind_DracoInt8Array_GetValue_1=function(){return(Jb=a._emscripten_bind_DracoInt8Array_GetValue_1=a.asm.da).apply(null,arguments)},Kb=a._emscripten_bind_DracoInt8Array_size_0=
+function(){return(Kb=a._emscripten_bind_DracoInt8Array_size_0=a.asm.ea).apply(null,arguments)},Lb=a._emscripten_bind_DracoInt8Array___destroy___0=function(){return(Lb=a._emscripten_bind_DracoInt8Array___destroy___0=a.asm.fa).apply(null,arguments)},Ka=a._emscripten_bind_DracoUInt8Array_DracoUInt8Array_0=function(){return(Ka=a._emscripten_bind_DracoUInt8Array_DracoUInt8Array_0=a.asm.ga).apply(null,arguments)},Mb=a._emscripten_bind_DracoUInt8Array_GetValue_1=function(){return(Mb=a._emscripten_bind_DracoUInt8Array_GetValue_1=
+a.asm.ha).apply(null,arguments)},Nb=a._emscripten_bind_DracoUInt8Array_size_0=function(){return(Nb=a._emscripten_bind_DracoUInt8Array_size_0=a.asm.ia).apply(null,arguments)},Ob=a._emscripten_bind_DracoUInt8Array___destroy___0=function(){return(Ob=a._emscripten_bind_DracoUInt8Array___destroy___0=a.asm.ja).apply(null,arguments)},La=a._emscripten_bind_DracoInt16Array_DracoInt16Array_0=function(){return(La=a._emscripten_bind_DracoInt16Array_DracoInt16Array_0=a.asm.ka).apply(null,arguments)},Pb=a._emscripten_bind_DracoInt16Array_GetValue_1=
+function(){return(Pb=a._emscripten_bind_DracoInt16Array_GetValue_1=a.asm.la).apply(null,arguments)},Qb=a._emscripten_bind_DracoInt16Array_size_0=function(){return(Qb=a._emscripten_bind_DracoInt16Array_size_0=a.asm.ma).apply(null,arguments)},Rb=a._emscripten_bind_DracoInt16Array___destroy___0=function(){return(Rb=a._emscripten_bind_DracoInt16Array___destroy___0=a.asm.na).apply(null,arguments)},Ma=a._emscripten_bind_DracoUInt16Array_DracoUInt16Array_0=function(){return(Ma=a._emscripten_bind_DracoUInt16Array_DracoUInt16Array_0=
+a.asm.oa).apply(null,arguments)},Sb=a._emscripten_bind_DracoUInt16Array_GetValue_1=function(){return(Sb=a._emscripten_bind_DracoUInt16Array_GetValue_1=a.asm.pa).apply(null,arguments)},Tb=a._emscripten_bind_DracoUInt16Array_size_0=function(){return(Tb=a._emscripten_bind_DracoUInt16Array_size_0=a.asm.qa).apply(null,arguments)},Ub=a._emscripten_bind_DracoUInt16Array___destroy___0=function(){return(Ub=a._emscripten_bind_DracoUInt16Array___destroy___0=a.asm.ra).apply(null,arguments)},Na=a._emscripten_bind_DracoInt32Array_DracoInt32Array_0=
+function(){return(Na=a._emscripten_bind_DracoInt32Array_DracoInt32Array_0=a.asm.sa).apply(null,arguments)},Vb=a._emscripten_bind_DracoInt32Array_GetValue_1=function(){return(Vb=a._emscripten_bind_DracoInt32Array_GetValue_1=a.asm.ta).apply(null,arguments)},Wb=a._emscripten_bind_DracoInt32Array_size_0=function(){return(Wb=a._emscripten_bind_DracoInt32Array_size_0=a.asm.ua).apply(null,arguments)},Xb=a._emscripten_bind_DracoInt32Array___destroy___0=function(){return(Xb=a._emscripten_bind_DracoInt32Array___destroy___0=
+a.asm.va).apply(null,arguments)},Oa=a._emscripten_bind_DracoUInt32Array_DracoUInt32Array_0=function(){return(Oa=a._emscripten_bind_DracoUInt32Array_DracoUInt32Array_0=a.asm.wa).apply(null,arguments)},Yb=a._emscripten_bind_DracoUInt32Array_GetValue_1=function(){return(Yb=a._emscripten_bind_DracoUInt32Array_GetValue_1=a.asm.xa).apply(null,arguments)},Zb=a._emscripten_bind_DracoUInt32Array_size_0=function(){return(Zb=a._emscripten_bind_DracoUInt32Array_size_0=a.asm.ya).apply(null,arguments)},$b=a._emscripten_bind_DracoUInt32Array___destroy___0=
+function(){return($b=a._emscripten_bind_DracoUInt32Array___destroy___0=a.asm.za).apply(null,arguments)},Pa=a._emscripten_bind_MetadataQuerier_MetadataQuerier_0=function(){return(Pa=a._emscripten_bind_MetadataQuerier_MetadataQuerier_0=a.asm.Aa).apply(null,arguments)},ac=a._emscripten_bind_MetadataQuerier_HasEntry_2=function(){return(ac=a._emscripten_bind_MetadataQuerier_HasEntry_2=a.asm.Ba).apply(null,arguments)},bc=a._emscripten_bind_MetadataQuerier_GetIntEntry_2=function(){return(bc=a._emscripten_bind_MetadataQuerier_GetIntEntry_2=
+a.asm.Ca).apply(null,arguments)},cc=a._emscripten_bind_MetadataQuerier_GetIntEntryArray_3=function(){return(cc=a._emscripten_bind_MetadataQuerier_GetIntEntryArray_3=a.asm.Da).apply(null,arguments)},dc=a._emscripten_bind_MetadataQuerier_GetDoubleEntry_2=function(){return(dc=a._emscripten_bind_MetadataQuerier_GetDoubleEntry_2=a.asm.Ea).apply(null,arguments)},ec=a._emscripten_bind_MetadataQuerier_GetStringEntry_2=function(){return(ec=a._emscripten_bind_MetadataQuerier_GetStringEntry_2=a.asm.Fa).apply(null,
+arguments)},fc=a._emscripten_bind_MetadataQuerier_NumEntries_1=function(){return(fc=a._emscripten_bind_MetadataQuerier_NumEntries_1=a.asm.Ga).apply(null,arguments)},gc=a._emscripten_bind_MetadataQuerier_GetEntryName_2=function(){return(gc=a._emscripten_bind_MetadataQuerier_GetEntryName_2=a.asm.Ha).apply(null,arguments)},hc=a._emscripten_bind_MetadataQuerier___destroy___0=function(){return(hc=a._emscripten_bind_MetadataQuerier___destroy___0=a.asm.Ia).apply(null,arguments)},Qa=a._emscripten_bind_Decoder_Decoder_0=
+function(){return(Qa=a._emscripten_bind_Decoder_Decoder_0=a.asm.Ja).apply(null,arguments)},ic=a._emscripten_bind_Decoder_DecodeArrayToPointCloud_3=function(){return(ic=a._emscripten_bind_Decoder_DecodeArrayToPointCloud_3=a.asm.Ka).apply(null,arguments)},jc=a._emscripten_bind_Decoder_DecodeArrayToMesh_3=function(){return(jc=a._emscripten_bind_Decoder_DecodeArrayToMesh_3=a.asm.La).apply(null,arguments)},kc=a._emscripten_bind_Decoder_GetAttributeId_2=function(){return(kc=a._emscripten_bind_Decoder_GetAttributeId_2=
+a.asm.Ma).apply(null,arguments)},lc=a._emscripten_bind_Decoder_GetAttributeIdByName_2=function(){return(lc=a._emscripten_bind_Decoder_GetAttributeIdByName_2=a.asm.Na).apply(null,arguments)},mc=a._emscripten_bind_Decoder_GetAttributeIdByMetadataEntry_3=function(){return(mc=a._emscripten_bind_Decoder_GetAttributeIdByMetadataEntry_3=a.asm.Oa).apply(null,arguments)},nc=a._emscripten_bind_Decoder_GetAttribute_2=function(){return(nc=a._emscripten_bind_Decoder_GetAttribute_2=a.asm.Pa).apply(null,arguments)},
+oc=a._emscripten_bind_Decoder_GetAttributeByUniqueId_2=function(){return(oc=a._emscripten_bind_Decoder_GetAttributeByUniqueId_2=a.asm.Qa).apply(null,arguments)},pc=a._emscripten_bind_Decoder_GetMetadata_1=function(){return(pc=a._emscripten_bind_Decoder_GetMetadata_1=a.asm.Ra).apply(null,arguments)},qc=a._emscripten_bind_Decoder_GetAttributeMetadata_2=function(){return(qc=a._emscripten_bind_Decoder_GetAttributeMetadata_2=a.asm.Sa).apply(null,arguments)},rc=a._emscripten_bind_Decoder_GetFaceFromMesh_3=
+function(){return(rc=a._emscripten_bind_Decoder_GetFaceFromMesh_3=a.asm.Ta).apply(null,arguments)},sc=a._emscripten_bind_Decoder_GetTriangleStripsFromMesh_2=function(){return(sc=a._emscripten_bind_Decoder_GetTriangleStripsFromMesh_2=a.asm.Ua).apply(null,arguments)},tc=a._emscripten_bind_Decoder_GetTrianglesUInt16Array_3=function(){return(tc=a._emscripten_bind_Decoder_GetTrianglesUInt16Array_3=a.asm.Va).apply(null,arguments)},uc=a._emscripten_bind_Decoder_GetTrianglesUInt32Array_3=function(){return(uc=
+a._emscripten_bind_Decoder_GetTrianglesUInt32Array_3=a.asm.Wa).apply(null,arguments)},vc=a._emscripten_bind_Decoder_GetAttributeFloat_3=function(){return(vc=a._emscripten_bind_Decoder_GetAttributeFloat_3=a.asm.Xa).apply(null,arguments)},wc=a._emscripten_bind_Decoder_GetAttributeFloatForAllPoints_3=function(){return(wc=a._emscripten_bind_Decoder_GetAttributeFloatForAllPoints_3=a.asm.Ya).apply(null,arguments)},xc=a._emscripten_bind_Decoder_GetAttributeIntForAllPoints_3=function(){return(xc=a._emscripten_bind_Decoder_GetAttributeIntForAllPoints_3=
+a.asm.Za).apply(null,arguments)},yc=a._emscripten_bind_Decoder_GetAttributeInt8ForAllPoints_3=function(){return(yc=a._emscripten_bind_Decoder_GetAttributeInt8ForAllPoints_3=a.asm._a).apply(null,arguments)},zc=a._emscripten_bind_Decoder_GetAttributeUInt8ForAllPoints_3=function(){return(zc=a._emscripten_bind_Decoder_GetAttributeUInt8ForAllPoints_3=a.asm.$a).apply(null,arguments)},Ac=a._emscripten_bind_Decoder_GetAttributeInt16ForAllPoints_3=function(){return(Ac=a._emscripten_bind_Decoder_GetAttributeInt16ForAllPoints_3=
+a.asm.ab).apply(null,arguments)},Bc=a._emscripten_bind_Decoder_GetAttributeUInt16ForAllPoints_3=function(){return(Bc=a._emscripten_bind_Decoder_GetAttributeUInt16ForAllPoints_3=a.asm.bb).apply(null,arguments)},Cc=a._emscripten_bind_Decoder_GetAttributeInt32ForAllPoints_3=function(){return(Cc=a._emscripten_bind_Decoder_GetAttributeInt32ForAllPoints_3=a.asm.cb).apply(null,arguments)},Dc=a._emscripten_bind_Decoder_GetAttributeUInt32ForAllPoints_3=function(){return(Dc=a._emscripten_bind_Decoder_GetAttributeUInt32ForAllPoints_3=
+a.asm.db).apply(null,arguments)},Ec=a._emscripten_bind_Decoder_GetAttributeDataArrayForAllPoints_5=function(){return(Ec=a._emscripten_bind_Decoder_GetAttributeDataArrayForAllPoints_5=a.asm.eb).apply(null,arguments)},Fc=a._emscripten_bind_Decoder_SkipAttributeTransform_1=function(){return(Fc=a._emscripten_bind_Decoder_SkipAttributeTransform_1=a.asm.fb).apply(null,arguments)},Gc=a._emscripten_bind_Decoder_GetEncodedGeometryType_Deprecated_1=function(){return(Gc=a._emscripten_bind_Decoder_GetEncodedGeometryType_Deprecated_1=
+a.asm.gb).apply(null,arguments)},Hc=a._emscripten_bind_Decoder_DecodeBufferToPointCloud_2=function(){return(Hc=a._emscripten_bind_Decoder_DecodeBufferToPointCloud_2=a.asm.hb).apply(null,arguments)},Ic=a._emscripten_bind_Decoder_DecodeBufferToMesh_2=function(){return(Ic=a._emscripten_bind_Decoder_DecodeBufferToMesh_2=a.asm.ib).apply(null,arguments)},Jc=a._emscripten_bind_Decoder___destroy___0=function(){return(Jc=a._emscripten_bind_Decoder___destroy___0=a.asm.jb).apply(null,arguments)},Kc=a._emscripten_enum_draco_AttributeTransformType_ATTRIBUTE_INVALID_TRANSFORM=
+function(){return(Kc=a._emscripten_enum_draco_AttributeTransformType_ATTRIBUTE_INVALID_TRANSFORM=a.asm.kb).apply(null,arguments)},Lc=a._emscripten_enum_draco_AttributeTransformType_ATTRIBUTE_NO_TRANSFORM=function(){return(Lc=a._emscripten_enum_draco_AttributeTransformType_ATTRIBUTE_NO_TRANSFORM=a.asm.lb).apply(null,arguments)},Mc=a._emscripten_enum_draco_AttributeTransformType_ATTRIBUTE_QUANTIZATION_TRANSFORM=function(){return(Mc=a._emscripten_enum_draco_AttributeTransformType_ATTRIBUTE_QUANTIZATION_TRANSFORM=
+a.asm.mb).apply(null,arguments)},Nc=a._emscripten_enum_draco_AttributeTransformType_ATTRIBUTE_OCTAHEDRON_TRANSFORM=function(){return(Nc=a._emscripten_enum_draco_AttributeTransformType_ATTRIBUTE_OCTAHEDRON_TRANSFORM=a.asm.nb).apply(null,arguments)},Oc=a._emscripten_enum_draco_GeometryAttribute_Type_INVALID=function(){return(Oc=a._emscripten_enum_draco_GeometryAttribute_Type_INVALID=a.asm.ob).apply(null,arguments)},Pc=a._emscripten_enum_draco_GeometryAttribute_Type_POSITION=function(){return(Pc=a._emscripten_enum_draco_GeometryAttribute_Type_POSITION=
+a.asm.pb).apply(null,arguments)},Qc=a._emscripten_enum_draco_GeometryAttribute_Type_NORMAL=function(){return(Qc=a._emscripten_enum_draco_GeometryAttribute_Type_NORMAL=a.asm.qb).apply(null,arguments)},Rc=a._emscripten_enum_draco_GeometryAttribute_Type_COLOR=function(){return(Rc=a._emscripten_enum_draco_GeometryAttribute_Type_COLOR=a.asm.rb).apply(null,arguments)},Sc=a._emscripten_enum_draco_GeometryAttribute_Type_TEX_COORD=function(){return(Sc=a._emscripten_enum_draco_GeometryAttribute_Type_TEX_COORD=
+a.asm.sb).apply(null,arguments)},Tc=a._emscripten_enum_draco_GeometryAttribute_Type_GENERIC=function(){return(Tc=a._emscripten_enum_draco_GeometryAttribute_Type_GENERIC=a.asm.tb).apply(null,arguments)},Uc=a._emscripten_enum_draco_EncodedGeometryType_INVALID_GEOMETRY_TYPE=function(){return(Uc=a._emscripten_enum_draco_EncodedGeometryType_INVALID_GEOMETRY_TYPE=a.asm.ub).apply(null,arguments)},Vc=a._emscripten_enum_draco_EncodedGeometryType_POINT_CLOUD=function(){return(Vc=a._emscripten_enum_draco_EncodedGeometryType_POINT_CLOUD=
+a.asm.vb).apply(null,arguments)},Wc=a._emscripten_enum_draco_EncodedGeometryType_TRIANGULAR_MESH=function(){return(Wc=a._emscripten_enum_draco_EncodedGeometryType_TRIANGULAR_MESH=a.asm.wb).apply(null,arguments)},Xc=a._emscripten_enum_draco_DataType_DT_INVALID=function(){return(Xc=a._emscripten_enum_draco_DataType_DT_INVALID=a.asm.xb).apply(null,arguments)},Yc=a._emscripten_enum_draco_DataType_DT_INT8=function(){return(Yc=a._emscripten_enum_draco_DataType_DT_INT8=a.asm.yb).apply(null,arguments)},Zc=
+a._emscripten_enum_draco_DataType_DT_UINT8=function(){return(Zc=a._emscripten_enum_draco_DataType_DT_UINT8=a.asm.zb).apply(null,arguments)},$c=a._emscripten_enum_draco_DataType_DT_INT16=function(){return($c=a._emscripten_enum_draco_DataType_DT_INT16=a.asm.Ab).apply(null,arguments)},ad=a._emscripten_enum_draco_DataType_DT_UINT16=function(){return(ad=a._emscripten_enum_draco_DataType_DT_UINT16=a.asm.Bb).apply(null,arguments)},bd=a._emscripten_enum_draco_DataType_DT_INT32=function(){return(bd=a._emscripten_enum_draco_DataType_DT_INT32=
+a.asm.Cb).apply(null,arguments)},cd=a._emscripten_enum_draco_DataType_DT_UINT32=function(){return(cd=a._emscripten_enum_draco_DataType_DT_UINT32=a.asm.Db).apply(null,arguments)},dd=a._emscripten_enum_draco_DataType_DT_INT64=function(){return(dd=a._emscripten_enum_draco_DataType_DT_INT64=a.asm.Eb).apply(null,arguments)},ed=a._emscripten_enum_draco_DataType_DT_UINT64=function(){return(ed=a._emscripten_enum_draco_DataType_DT_UINT64=a.asm.Fb).apply(null,arguments)},fd=a._emscripten_enum_draco_DataType_DT_FLOAT32=
+function(){return(fd=a._emscripten_enum_draco_DataType_DT_FLOAT32=a.asm.Gb).apply(null,arguments)},gd=a._emscripten_enum_draco_DataType_DT_FLOAT64=function(){return(gd=a._emscripten_enum_draco_DataType_DT_FLOAT64=a.asm.Hb).apply(null,arguments)},hd=a._emscripten_enum_draco_DataType_DT_BOOL=function(){return(hd=a._emscripten_enum_draco_DataType_DT_BOOL=a.asm.Ib).apply(null,arguments)},id=a._emscripten_enum_draco_DataType_DT_TYPES_COUNT=function(){return(id=a._emscripten_enum_draco_DataType_DT_TYPES_COUNT=
+a.asm.Jb).apply(null,arguments)},jd=a._emscripten_enum_draco_StatusCode_OK=function(){return(jd=a._emscripten_enum_draco_StatusCode_OK=a.asm.Kb).apply(null,arguments)},kd=a._emscripten_enum_draco_StatusCode_DRACO_ERROR=function(){return(kd=a._emscripten_enum_draco_StatusCode_DRACO_ERROR=a.asm.Lb).apply(null,arguments)},ld=a._emscripten_enum_draco_StatusCode_IO_ERROR=function(){return(ld=a._emscripten_enum_draco_StatusCode_IO_ERROR=a.asm.Mb).apply(null,arguments)},md=a._emscripten_enum_draco_StatusCode_INVALID_PARAMETER=
+function(){return(md=a._emscripten_enum_draco_StatusCode_INVALID_PARAMETER=a.asm.Nb).apply(null,arguments)},nd=a._emscripten_enum_draco_StatusCode_UNSUPPORTED_VERSION=function(){return(nd=a._emscripten_enum_draco_StatusCode_UNSUPPORTED_VERSION=a.asm.Ob).apply(null,arguments)},od=a._emscripten_enum_draco_StatusCode_UNKNOWN_VERSION=function(){return(od=a._emscripten_enum_draco_StatusCode_UNKNOWN_VERSION=a.asm.Pb).apply(null,arguments)};a._malloc=function(){return(a._malloc=a.asm.Qb).apply(null,arguments)};
+a._free=function(){return(a._free=a.asm.Rb).apply(null,arguments)};var ua=function(){return(ua=a.asm.Sb).apply(null,arguments)};a.___start_em_js=11660;a.___stop_em_js=11758;var la;ha=function b(){la||F();la||(ha=b)};if(a.preInit)for("function"==typeof a.preInit&&(a.preInit=[a.preInit]);0<a.preInit.length;)a.preInit.pop()();F();v.prototype=Object.create(v.prototype);v.prototype.constructor=v;v.prototype.__class__=v;v.__cache__={};a.WrapperObject=v;a.getCache=w;a.wrapPointer=B;a.castObject=function(b,
+c){return B(b.ptr,c)};a.NULL=B(0);a.destroy=function(b){if(!b.__destroy__)throw"Error: Cannot destroy object. (Did you create it yourself?)";b.__destroy__();delete w(b.__class__)[b.ptr]};a.compare=function(b,c){return b.ptr===c.ptr};a.getPointer=function(b){return b.ptr};a.getClass=function(b){return b.__class__};var r={buffer:0,size:0,pos:0,temps:[],needed:0,prepare:function(){if(r.needed){for(var b=0;b<r.temps.length;b++)a._free(r.temps[b]);r.temps.length=0;a._free(r.buffer);r.buffer=0;r.size+=
+r.needed;r.needed=0}r.buffer||(r.size+=128,r.buffer=a._malloc(r.size),r.buffer||y(void 0));r.pos=0},alloc:function(b,c){r.buffer||y(void 0);b=b.length*c.BYTES_PER_ELEMENT;b=b+7&-8;r.pos+b>=r.size?(0<b||y(void 0),r.needed+=b,c=a._malloc(b),r.temps.push(c)):(c=r.buffer+r.pos,r.pos+=b);return c},copy:function(b,c,d){d>>>=0;switch(c.BYTES_PER_ELEMENT){case 2:d>>>=1;break;case 4:d>>>=2;break;case 8:d>>>=3}for(var g=0;g<b.length;g++)c[d+g]=b[g]}};X.prototype=Object.create(v.prototype);X.prototype.constructor=
+X;X.prototype.__class__=X;X.__cache__={};a.VoidPtr=X;X.prototype.__destroy__=X.prototype.__destroy__=function(){Xa(this.ptr)};S.prototype=Object.create(v.prototype);S.prototype.constructor=S;S.prototype.__class__=S;S.__cache__={};a.DecoderBuffer=S;S.prototype.Init=S.prototype.Init=function(b,c){var d=this.ptr;r.prepare();"object"==typeof b&&(b=Z(b));c&&"object"===typeof c&&(c=c.ptr);Ya(d,b,c)};S.prototype.__destroy__=S.prototype.__destroy__=function(){Za(this.ptr)};Q.prototype=Object.create(v.prototype);
+Q.prototype.constructor=Q;Q.prototype.__class__=Q;Q.__cache__={};a.AttributeTransformData=Q;Q.prototype.transform_type=Q.prototype.transform_type=function(){return $a(this.ptr)};Q.prototype.__destroy__=Q.prototype.__destroy__=function(){ab(this.ptr)};V.prototype=Object.create(v.prototype);V.prototype.constructor=V;V.prototype.__class__=V;V.__cache__={};a.GeometryAttribute=V;V.prototype.__destroy__=V.prototype.__destroy__=function(){bb(this.ptr)};x.prototype=Object.create(v.prototype);x.prototype.constructor=
+x;x.prototype.__class__=x;x.__cache__={};a.PointAttribute=x;x.prototype.size=x.prototype.size=function(){return cb(this.ptr)};x.prototype.GetAttributeTransformData=x.prototype.GetAttributeTransformData=function(){return B(db(this.ptr),Q)};x.prototype.attribute_type=x.prototype.attribute_type=function(){return eb(this.ptr)};x.prototype.data_type=x.prototype.data_type=function(){return fb(this.ptr)};x.prototype.num_components=x.prototype.num_components=function(){return gb(this.ptr)};x.prototype.normalized=
+x.prototype.normalized=function(){return!!hb(this.ptr)};x.prototype.byte_stride=x.prototype.byte_stride=function(){return ib(this.ptr)};x.prototype.byte_offset=x.prototype.byte_offset=function(){return jb(this.ptr)};x.prototype.unique_id=x.prototype.unique_id=function(){return kb(this.ptr)};x.prototype.__destroy__=x.prototype.__destroy__=function(){lb(this.ptr)};D.prototype=Object.create(v.prototype);D.prototype.constructor=D;D.prototype.__class__=D;D.__cache__={};a.AttributeQuantizationTransform=
+D;D.prototype.InitFromAttribute=D.prototype.InitFromAttribute=function(b){var c=this.ptr;b&&"object"===typeof b&&(b=b.ptr);return!!mb(c,b)};D.prototype.quantization_bits=D.prototype.quantization_bits=function(){return nb(this.ptr)};D.prototype.min_value=D.prototype.min_value=function(b){var c=this.ptr;b&&"object"===typeof b&&(b=b.ptr);return ob(c,b)};D.prototype.range=D.prototype.range=function(){return pb(this.ptr)};D.prototype.__destroy__=D.prototype.__destroy__=function(){qb(this.ptr)};G.prototype=
+Object.create(v.prototype);G.prototype.constructor=G;G.prototype.__class__=G;G.__cache__={};a.AttributeOctahedronTransform=G;G.prototype.InitFromAttribute=G.prototype.InitFromAttribute=function(b){var c=this.ptr;b&&"object"===typeof b&&(b=b.ptr);return!!rb(c,b)};G.prototype.quantization_bits=G.prototype.quantization_bits=function(){return sb(this.ptr)};G.prototype.__destroy__=G.prototype.__destroy__=function(){tb(this.ptr)};H.prototype=Object.create(v.prototype);H.prototype.constructor=H;H.prototype.__class__=
+H;H.__cache__={};a.PointCloud=H;H.prototype.num_attributes=H.prototype.num_attributes=function(){return ub(this.ptr)};H.prototype.num_points=H.prototype.num_points=function(){return vb(this.ptr)};H.prototype.__destroy__=H.prototype.__destroy__=function(){wb(this.ptr)};E.prototype=Object.create(v.prototype);E.prototype.constructor=E;E.prototype.__class__=E;E.__cache__={};a.Mesh=E;E.prototype.num_faces=E.prototype.num_faces=function(){return xb(this.ptr)};E.prototype.num_attributes=E.prototype.num_attributes=
+function(){return yb(this.ptr)};E.prototype.num_points=E.prototype.num_points=function(){return zb(this.ptr)};E.prototype.__destroy__=E.prototype.__destroy__=function(){Ab(this.ptr)};T.prototype=Object.create(v.prototype);T.prototype.constructor=T;T.prototype.__class__=T;T.__cache__={};a.Metadata=T;T.prototype.__destroy__=T.prototype.__destroy__=function(){Bb(this.ptr)};C.prototype=Object.create(v.prototype);C.prototype.constructor=C;C.prototype.__class__=C;C.__cache__={};a.Status=C;C.prototype.code=
+C.prototype.code=function(){return Cb(this.ptr)};C.prototype.ok=C.prototype.ok=function(){return!!Db(this.ptr)};C.prototype.error_msg=C.prototype.error_msg=function(){return p(Eb(this.ptr))};C.prototype.__destroy__=C.prototype.__destroy__=function(){Fb(this.ptr)};I.prototype=Object.create(v.prototype);I.prototype.constructor=I;I.prototype.__class__=I;I.__cache__={};a.DracoFloat32Array=I;I.prototype.GetValue=I.prototype.GetValue=function(b){var c=this.ptr;b&&"object"===typeof b&&(b=b.ptr);return Gb(c,
+b)};I.prototype.size=I.prototype.size=function(){return Hb(this.ptr)};I.prototype.__destroy__=I.prototype.__destroy__=function(){Ib(this.ptr)};J.prototype=Object.create(v.prototype);J.prototype.constructor=J;J.prototype.__class__=J;J.__cache__={};a.DracoInt8Array=J;J.prototype.GetValue=J.prototype.GetValue=function(b){var c=this.ptr;b&&"object"===typeof b&&(b=b.ptr);return Jb(c,b)};J.prototype.size=J.prototype.size=function(){return Kb(this.ptr)};J.prototype.__destroy__=J.prototype.__destroy__=function(){Lb(this.ptr)};
+K.prototype=Object.create(v.prototype);K.prototype.constructor=K;K.prototype.__class__=K;K.__cache__={};a.DracoUInt8Array=K;K.prototype.GetValue=K.prototype.GetValue=function(b){var c=this.ptr;b&&"object"===typeof b&&(b=b.ptr);return Mb(c,b)};K.prototype.size=K.prototype.size=function(){return Nb(this.ptr)};K.prototype.__destroy__=K.prototype.__destroy__=function(){Ob(this.ptr)};L.prototype=Object.create(v.prototype);L.prototype.constructor=L;L.prototype.__class__=L;L.__cache__={};a.DracoInt16Array=
+L;L.prototype.GetValue=L.prototype.GetValue=function(b){var c=this.ptr;b&&"object"===typeof b&&(b=b.ptr);return Pb(c,b)};L.prototype.size=L.prototype.size=function(){return Qb(this.ptr)};L.prototype.__destroy__=L.prototype.__destroy__=function(){Rb(this.ptr)};M.prototype=Object.create(v.prototype);M.prototype.constructor=M;M.prototype.__class__=M;M.__cache__={};a.DracoUInt16Array=M;M.prototype.GetValue=M.prototype.GetValue=function(b){var c=this.ptr;b&&"object"===typeof b&&(b=b.ptr);return Sb(c,b)};
+M.prototype.size=M.prototype.size=function(){return Tb(this.ptr)};M.prototype.__destroy__=M.prototype.__destroy__=function(){Ub(this.ptr)};N.prototype=Object.create(v.prototype);N.prototype.constructor=N;N.prototype.__class__=N;N.__cache__={};a.DracoInt32Array=N;N.prototype.GetValue=N.prototype.GetValue=function(b){var c=this.ptr;b&&"object"===typeof b&&(b=b.ptr);return Vb(c,b)};N.prototype.size=N.prototype.size=function(){return Wb(this.ptr)};N.prototype.__destroy__=N.prototype.__destroy__=function(){Xb(this.ptr)};
+O.prototype=Object.create(v.prototype);O.prototype.constructor=O;O.prototype.__class__=O;O.__cache__={};a.DracoUInt32Array=O;O.prototype.GetValue=O.prototype.GetValue=function(b){var c=this.ptr;b&&"object"===typeof b&&(b=b.ptr);return Yb(c,b)};O.prototype.size=O.prototype.size=function(){return Zb(this.ptr)};O.prototype.__destroy__=O.prototype.__destroy__=function(){$b(this.ptr)};z.prototype=Object.create(v.prototype);z.prototype.constructor=z;z.prototype.__class__=z;z.__cache__={};a.MetadataQuerier=
+z;z.prototype.HasEntry=z.prototype.HasEntry=function(b,c){var d=this.ptr;r.prepare();b&&"object"===typeof b&&(b=b.ptr);c=c&&"object"===typeof c?c.ptr:R(c);return!!ac(d,b,c)};z.prototype.GetIntEntry=z.prototype.GetIntEntry=function(b,c){var d=this.ptr;r.prepare();b&&"object"===typeof b&&(b=b.ptr);c=c&&"object"===typeof c?c.ptr:R(c);return bc(d,b,c)};z.prototype.GetIntEntryArray=z.prototype.GetIntEntryArray=function(b,c,d){var g=this.ptr;r.prepare();b&&"object"===typeof b&&(b=b.ptr);c=c&&"object"===
+typeof c?c.ptr:R(c);d&&"object"===typeof d&&(d=d.ptr);cc(g,b,c,d)};z.prototype.GetDoubleEntry=z.prototype.GetDoubleEntry=function(b,c){var d=this.ptr;r.prepare();b&&"object"===typeof b&&(b=b.ptr);c=c&&"object"===typeof c?c.ptr:R(c);return dc(d,b,c)};z.prototype.GetStringEntry=z.prototype.GetStringEntry=function(b,c){var d=this.ptr;r.prepare();b&&"object"===typeof b&&(b=b.ptr);c=c&&"object"===typeof c?c.ptr:R(c);return p(ec(d,b,c))};z.prototype.NumEntries=z.prototype.NumEntries=function(b){var c=this.ptr;
+b&&"object"===typeof b&&(b=b.ptr);return fc(c,b)};z.prototype.GetEntryName=z.prototype.GetEntryName=function(b,c){var d=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&(c=c.ptr);return p(gc(d,b,c))};z.prototype.__destroy__=z.prototype.__destroy__=function(){hc(this.ptr)};m.prototype=Object.create(v.prototype);m.prototype.constructor=m;m.prototype.__class__=m;m.__cache__={};a.Decoder=m;m.prototype.DecodeArrayToPointCloud=m.prototype.DecodeArrayToPointCloud=function(b,c,d){var g=
+this.ptr;r.prepare();"object"==typeof b&&(b=Z(b));c&&"object"===typeof c&&(c=c.ptr);d&&"object"===typeof d&&(d=d.ptr);return B(ic(g,b,c,d),C)};m.prototype.DecodeArrayToMesh=m.prototype.DecodeArrayToMesh=function(b,c,d){var g=this.ptr;r.prepare();"object"==typeof b&&(b=Z(b));c&&"object"===typeof c&&(c=c.ptr);d&&"object"===typeof d&&(d=d.ptr);return B(jc(g,b,c,d),C)};m.prototype.GetAttributeId=m.prototype.GetAttributeId=function(b,c){var d=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&
+(c=c.ptr);return kc(d,b,c)};m.prototype.GetAttributeIdByName=m.prototype.GetAttributeIdByName=function(b,c){var d=this.ptr;r.prepare();b&&"object"===typeof b&&(b=b.ptr);c=c&&"object"===typeof c?c.ptr:R(c);return lc(d,b,c)};m.prototype.GetAttributeIdByMetadataEntry=m.prototype.GetAttributeIdByMetadataEntry=function(b,c,d){var g=this.ptr;r.prepare();b&&"object"===typeof b&&(b=b.ptr);c=c&&"object"===typeof c?c.ptr:R(c);d=d&&"object"===typeof d?d.ptr:R(d);return mc(g,b,c,d)};m.prototype.GetAttribute=
+m.prototype.GetAttribute=function(b,c){var d=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&(c=c.ptr);return B(nc(d,b,c),x)};m.prototype.GetAttributeByUniqueId=m.prototype.GetAttributeByUniqueId=function(b,c){var d=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&(c=c.ptr);return B(oc(d,b,c),x)};m.prototype.GetMetadata=m.prototype.GetMetadata=function(b){var c=this.ptr;b&&"object"===typeof b&&(b=b.ptr);return B(pc(c,b),T)};m.prototype.GetAttributeMetadata=m.prototype.GetAttributeMetadata=
+function(b,c){var d=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&(c=c.ptr);return B(qc(d,b,c),T)};m.prototype.GetFaceFromMesh=m.prototype.GetFaceFromMesh=function(b,c,d){var g=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&(c=c.ptr);d&&"object"===typeof d&&(d=d.ptr);return!!rc(g,b,c,d)};m.prototype.GetTriangleStripsFromMesh=m.prototype.GetTriangleStripsFromMesh=function(b,c){var d=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&(c=c.ptr);
+return sc(d,b,c)};m.prototype.GetTrianglesUInt16Array=m.prototype.GetTrianglesUInt16Array=function(b,c,d){var g=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&(c=c.ptr);d&&"object"===typeof d&&(d=d.ptr);return!!tc(g,b,c,d)};m.prototype.GetTrianglesUInt32Array=m.prototype.GetTrianglesUInt32Array=function(b,c,d){var g=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&(c=c.ptr);d&&"object"===typeof d&&(d=d.ptr);return!!uc(g,b,c,d)};m.prototype.GetAttributeFloat=m.prototype.GetAttributeFloat=
+function(b,c,d){var g=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&(c=c.ptr);d&&"object"===typeof d&&(d=d.ptr);return!!vc(g,b,c,d)};m.prototype.GetAttributeFloatForAllPoints=m.prototype.GetAttributeFloatForAllPoints=function(b,c,d){var g=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&(c=c.ptr);d&&"object"===typeof d&&(d=d.ptr);return!!wc(g,b,c,d)};m.prototype.GetAttributeIntForAllPoints=m.prototype.GetAttributeIntForAllPoints=function(b,c,d){var g=this.ptr;
+b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&(c=c.ptr);d&&"object"===typeof d&&(d=d.ptr);return!!xc(g,b,c,d)};m.prototype.GetAttributeInt8ForAllPoints=m.prototype.GetAttributeInt8ForAllPoints=function(b,c,d){var g=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&(c=c.ptr);d&&"object"===typeof d&&(d=d.ptr);return!!yc(g,b,c,d)};m.prototype.GetAttributeUInt8ForAllPoints=m.prototype.GetAttributeUInt8ForAllPoints=function(b,c,d){var g=this.ptr;b&&"object"===typeof b&&(b=
+b.ptr);c&&"object"===typeof c&&(c=c.ptr);d&&"object"===typeof d&&(d=d.ptr);return!!zc(g,b,c,d)};m.prototype.GetAttributeInt16ForAllPoints=m.prototype.GetAttributeInt16ForAllPoints=function(b,c,d){var g=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&(c=c.ptr);d&&"object"===typeof d&&(d=d.ptr);return!!Ac(g,b,c,d)};m.prototype.GetAttributeUInt16ForAllPoints=m.prototype.GetAttributeUInt16ForAllPoints=function(b,c,d){var g=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&
+(c=c.ptr);d&&"object"===typeof d&&(d=d.ptr);return!!Bc(g,b,c,d)};m.prototype.GetAttributeInt32ForAllPoints=m.prototype.GetAttributeInt32ForAllPoints=function(b,c,d){var g=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&(c=c.ptr);d&&"object"===typeof d&&(d=d.ptr);return!!Cc(g,b,c,d)};m.prototype.GetAttributeUInt32ForAllPoints=m.prototype.GetAttributeUInt32ForAllPoints=function(b,c,d){var g=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&(c=c.ptr);d&&"object"===
+typeof d&&(d=d.ptr);return!!Dc(g,b,c,d)};m.prototype.GetAttributeDataArrayForAllPoints=m.prototype.GetAttributeDataArrayForAllPoints=function(b,c,d,g,t){var aa=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&(c=c.ptr);d&&"object"===typeof d&&(d=d.ptr);g&&"object"===typeof g&&(g=g.ptr);t&&"object"===typeof t&&(t=t.ptr);return!!Ec(aa,b,c,d,g,t)};m.prototype.SkipAttributeTransform=m.prototype.SkipAttributeTransform=function(b){var c=this.ptr;b&&"object"===typeof b&&(b=b.ptr);Fc(c,
+b)};m.prototype.GetEncodedGeometryType_Deprecated=m.prototype.GetEncodedGeometryType_Deprecated=function(b){var c=this.ptr;b&&"object"===typeof b&&(b=b.ptr);return Gc(c,b)};m.prototype.DecodeBufferToPointCloud=m.prototype.DecodeBufferToPointCloud=function(b,c){var d=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&(c=c.ptr);return B(Hc(d,b,c),C)};m.prototype.DecodeBufferToMesh=m.prototype.DecodeBufferToMesh=function(b,c){var d=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&"object"===
+typeof c&&(c=c.ptr);return B(Ic(d,b,c),C)};m.prototype.__destroy__=m.prototype.__destroy__=function(){Jc(this.ptr)};(function(){function b(){a.ATTRIBUTE_INVALID_TRANSFORM=Kc();a.ATTRIBUTE_NO_TRANSFORM=Lc();a.ATTRIBUTE_QUANTIZATION_TRANSFORM=Mc();a.ATTRIBUTE_OCTAHEDRON_TRANSFORM=Nc();a.INVALID=Oc();a.POSITION=Pc();a.NORMAL=Qc();a.COLOR=Rc();a.TEX_COORD=Sc();a.GENERIC=Tc();a.INVALID_GEOMETRY_TYPE=Uc();a.POINT_CLOUD=Vc();a.TRIANGULAR_MESH=Wc();a.DT_INVALID=Xc();a.DT_INT8=Yc();a.DT_UINT8=Zc();a.DT_INT16=
+$c();a.DT_UINT16=ad();a.DT_INT32=bd();a.DT_UINT32=cd();a.DT_INT64=dd();a.DT_UINT64=ed();a.DT_FLOAT32=fd();a.DT_FLOAT64=gd();a.DT_BOOL=hd();a.DT_TYPES_COUNT=id();a.OK=jd();a.DRACO_ERROR=kd();a.IO_ERROR=ld();a.INVALID_PARAMETER=md();a.UNSUPPORTED_VERSION=nd();a.UNKNOWN_VERSION=od()}va?b():oa.unshift(b)})();if("function"===typeof a.onModuleParsed)a.onModuleParsed();a.Decoder.prototype.GetEncodedGeometryType=function(b){if(b.__class__&&b.__class__===a.DecoderBuffer)return a.Decoder.prototype.GetEncodedGeometryType_Deprecated(b);
+if(8>b.byteLength)return a.INVALID_GEOMETRY_TYPE;switch(b[7]){case 0:return a.POINT_CLOUD;case 1:return a.TRIANGULAR_MESH;default:return a.INVALID_GEOMETRY_TYPE}};return n.ready}}();"object"===typeof exports&&"object"===typeof module?module.exports=DracoDecoderModule:"function"===typeof define&&define.amd?define([],function(){return DracoDecoderModule}):"object"===typeof exports&&(exports.DracoDecoderModule=DracoDecoderModule);
diff --git a/public/vendor/addons/loaders/DRACOLoader.js b/public/vendor/addons/loaders/DRACOLoader.js
new file mode 100644
index 0000000..b176ebf
--- /dev/null
+++ b/public/vendor/addons/loaders/DRACOLoader.js
@@ -0,0 +1,613 @@
+import {
+ BufferAttribute,
+ BufferGeometry,
+ Color,
+ FileLoader,
+ Loader,
+ LinearSRGBColorSpace,
+ SRGBColorSpace
+} from 'three';
+
+const _taskCache = new WeakMap();
+
+class DRACOLoader extends Loader {
+
+ constructor( manager ) {
+
+ super( manager );
+
+ this.decoderPath = '';
+ this.decoderConfig = {};
+ this.decoderBinary = null;
+ this.decoderPending = null;
+
+ this.workerLimit = 4;
+ this.workerPool = [];
+ this.workerNextTaskID = 1;
+ this.workerSourceURL = '';
+
+ this.defaultAttributeIDs = {
+ position: 'POSITION',
+ normal: 'NORMAL',
+ color: 'COLOR',
+ uv: 'TEX_COORD'
+ };
+ this.defaultAttributeTypes = {
+ position: 'Float32Array',
+ normal: 'Float32Array',
+ color: 'Float32Array',
+ uv: 'Float32Array'
+ };
+
+ }
+
+ setDecoderPath( path ) {
+
+ this.decoderPath = path;
+
+ return this;
+
+ }
+
+ setDecoderConfig( config ) {
+
+ this.decoderConfig = config;
+
+ return this;
+
+ }
+
+ setWorkerLimit( workerLimit ) {
+
+ this.workerLimit = workerLimit;
+
+ return this;
+
+ }
+
+ load( url, onLoad, onProgress, onError ) {
+
+ const loader = new FileLoader( this.manager );
+
+ loader.setPath( this.path );
+ loader.setResponseType( 'arraybuffer' );
+ loader.setRequestHeader( this.requestHeader );
+ loader.setWithCredentials( this.withCredentials );
+
+ loader.load( url, ( buffer ) => {
+
+ this.parse( buffer, onLoad, onError );
+
+ }, onProgress, onError );
+
+ }
+
+
+ parse( buffer, onLoad, onError = ()=>{} ) {
+
+ this.decodeDracoFile( buffer, onLoad, null, null, SRGBColorSpace ).catch( onError );
+
+ }
+
+ decodeDracoFile( buffer, callback, attributeIDs, attributeTypes, vertexColorSpace = LinearSRGBColorSpace, onError = () => {} ) {
+
+ const taskConfig = {
+ attributeIDs: attributeIDs || this.defaultAttributeIDs,
+ attributeTypes: attributeTypes || this.defaultAttributeTypes,
+ useUniqueIDs: !! attributeIDs,
+ vertexColorSpace: vertexColorSpace,
+ };
+
+ return this.decodeGeometry( buffer, taskConfig ).then( callback ).catch( onError );
+
+ }
+
+ decodeGeometry( buffer, taskConfig ) {
+
+ const taskKey = JSON.stringify( taskConfig );
+
+ // Check for an existing task using this buffer. A transferred buffer cannot be transferred
+ // again from this thread.
+ if ( _taskCache.has( buffer ) ) {
+
+ const cachedTask = _taskCache.get( buffer );
+
+ if ( cachedTask.key === taskKey ) {
+
+ return cachedTask.promise;
+
+ } else if ( buffer.byteLength === 0 ) {
+
+ // Technically, it would be possible to wait for the previous task to complete,
+ // transfer the buffer back, and decode again with the second configuration. That
+ // is complex, and I don't know of any reason to decode a Draco buffer twice in
+ // different ways, so this is left unimplemented.
+ throw new Error(
+
+ 'THREE.DRACOLoader: Unable to re-decode a buffer with different ' +
+ 'settings. Buffer has already been transferred.'
+
+ );
+
+ }
+
+ }
+
+ //
+
+ let worker;
+ const taskID = this.workerNextTaskID ++;
+ const taskCost = buffer.byteLength;
+
+ // Obtain a worker and assign a task, and construct a geometry instance
+ // when the task completes.
+ const geometryPending = this._getWorker( taskID, taskCost )
+ .then( ( _worker ) => {
+
+ worker = _worker;
+
+ return new Promise( ( resolve, reject ) => {
+
+ worker._callbacks[ taskID ] = { resolve, reject };
+
+ worker.postMessage( { type: 'decode', id: taskID, taskConfig, buffer }, [ buffer ] );
+
+ // this.debug();
+
+ } );
+
+ } )
+ .then( ( message ) => this._createGeometry( message.geometry ) );
+
+ // Remove task from the task list.
+ // Note: replaced '.finally()' with '.catch().then()' block - iOS 11 support (#19416)
+ geometryPending
+ .catch( () => true )
+ .then( () => {
+
+ if ( worker && taskID ) {
+
+ this._releaseTask( worker, taskID );
+
+ // this.debug();
+
+ }
+
+ } );
+
+ // Cache the task result.
+ _taskCache.set( buffer, {
+
+ key: taskKey,
+ promise: geometryPending
+
+ } );
+
+ return geometryPending;
+
+ }
+
+ _createGeometry( geometryData ) {
+
+ const geometry = new BufferGeometry();
+
+ if ( geometryData.index ) {
+
+ geometry.setIndex( new BufferAttribute( geometryData.index.array, 1 ) );
+
+ }
+
+ for ( let i = 0; i < geometryData.attributes.length; i ++ ) {
+
+ const result = geometryData.attributes[ i ];
+ const name = result.name;
+ const array = result.array;
+ const itemSize = result.itemSize;
+
+ const attribute = new BufferAttribute( array, itemSize );
+
+ if ( name === 'color' ) {
+
+ this._assignVertexColorSpace( attribute, result.vertexColorSpace );
+
+ attribute.normalized = ( array instanceof Float32Array ) === false;
+
+ }
+
+ geometry.setAttribute( name, attribute );
+
+ }
+
+ return geometry;
+
+ }
+
+ _assignVertexColorSpace( attribute, inputColorSpace ) {
+
+ // While .drc files do not specify colorspace, the only 'official' tooling
+ // is PLY and OBJ converters, which use sRGB. We'll assume sRGB when a .drc
+ // file is passed into .load() or .parse(). GLTFLoader uses internal APIs
+ // to decode geometry, and vertex colors are already Linear-sRGB in there.
+
+ if ( inputColorSpace !== SRGBColorSpace ) return;
+
+ const _color = new Color();
+
+ for ( let i = 0, il = attribute.count; i < il; i ++ ) {
+
+ _color.fromBufferAttribute( attribute, i ).convertSRGBToLinear();
+ attribute.setXYZ( i, _color.r, _color.g, _color.b );
+
+ }
+
+ }
+
+ _loadLibrary( url, responseType ) {
+
+ const loader = new FileLoader( this.manager );
+ loader.setPath( this.decoderPath );
+ loader.setResponseType( responseType );
+ loader.setWithCredentials( this.withCredentials );
+
+ return new Promise( ( resolve, reject ) => {
+
+ loader.load( url, resolve, undefined, reject );
+
+ } );
+
+ }
+
+ preload() {
+
+ this._initDecoder();
+
+ return this;
+
+ }
+
+ _initDecoder() {
+
+ if ( this.decoderPending ) return this.decoderPending;
+
+ const useJS = typeof WebAssembly !== 'object' || this.decoderConfig.type === 'js';
+ const librariesPending = [];
+
+ if ( useJS ) {
+
+ librariesPending.push( this._loadLibrary( 'draco_decoder.js', 'text' ) );
+
+ } else {
+
+ librariesPending.push( this._loadLibrary( 'draco_wasm_wrapper.js', 'text' ) );
+ librariesPending.push( this._loadLibrary( 'draco_decoder.wasm', 'arraybuffer' ) );
+
+ }
+
+ this.decoderPending = Promise.all( librariesPending )
+ .then( ( libraries ) => {
+
+ const jsContent = libraries[ 0 ];
+
+ if ( ! useJS ) {
+
+ this.decoderConfig.wasmBinary = libraries[ 1 ];
+
+ }
+
+ const fn = DRACOWorker.toString();
+
+ const body = [
+ '/* draco decoder */',
+ jsContent,
+ '',
+ '/* worker */',
+ fn.substring( fn.indexOf( '{' ) + 1, fn.lastIndexOf( '}' ) )
+ ].join( '\n' );
+
+ this.workerSourceURL = URL.createObjectURL( new Blob( [ body ] ) );
+
+ } );
+
+ return this.decoderPending;
+
+ }
+
+ _getWorker( taskID, taskCost ) {
+
+ return this._initDecoder().then( () => {
+
+ if ( this.workerPool.length < this.workerLimit ) {
+
+ const worker = new Worker( this.workerSourceURL );
+
+ worker._callbacks = {};
+ worker._taskCosts = {};
+ worker._taskLoad = 0;
+
+ worker.postMessage( { type: 'init', decoderConfig: this.decoderConfig } );
+
+ worker.onmessage = function ( e ) {
+
+ const message = e.data;
+
+ switch ( message.type ) {
+
+ case 'decode':
+ worker._callbacks[ message.id ].resolve( message );
+ break;
+
+ case 'error':
+ worker._callbacks[ message.id ].reject( message );
+ break;
+
+ default:
+ console.error( 'THREE.DRACOLoader: Unexpected message, "' + message.type + '"' );
+
+ }
+
+ };
+
+ this.workerPool.push( worker );
+
+ } else {
+
+ this.workerPool.sort( function ( a, b ) {
+
+ return a._taskLoad > b._taskLoad ? - 1 : 1;
+
+ } );
+
+ }
+
+ const worker = this.workerPool[ this.workerPool.length - 1 ];
+ worker._taskCosts[ taskID ] = taskCost;
+ worker._taskLoad += taskCost;
+ return worker;
+
+ } );
+
+ }
+
+ _releaseTask( worker, taskID ) {
+
+ worker._taskLoad -= worker._taskCosts[ taskID ];
+ delete worker._callbacks[ taskID ];
+ delete worker._taskCosts[ taskID ];
+
+ }
+
+ debug() {
+
+ console.log( 'Task load: ', this.workerPool.map( ( worker ) => worker._taskLoad ) );
+
+ }
+
+ dispose() {
+
+ for ( let i = 0; i < this.workerPool.length; ++ i ) {
+
+ this.workerPool[ i ].terminate();
+
+ }
+
+ this.workerPool.length = 0;
+
+ if ( this.workerSourceURL !== '' ) {
+
+ URL.revokeObjectURL( this.workerSourceURL );
+
+ }
+
+ return this;
+
+ }
+
+}
+
+/* WEB WORKER */
+
+function DRACOWorker() {
+
+ let decoderConfig;
+ let decoderPending;
+
+ onmessage = function ( e ) {
+
+ const message = e.data;
+
+ switch ( message.type ) {
+
+ case 'init':
+ decoderConfig = message.decoderConfig;
+ decoderPending = new Promise( function ( resolve/*, reject*/ ) {
+
+ decoderConfig.onModuleLoaded = function ( draco ) {
+
+ // Module is Promise-like. Wrap before resolving to avoid loop.
+ resolve( { draco: draco } );
+
+ };
+
+ DracoDecoderModule( decoderConfig ); // eslint-disable-line no-undef
+
+ } );
+ break;
+
+ case 'decode':
+ const buffer = message.buffer;
+ const taskConfig = message.taskConfig;
+ decoderPending.then( ( module ) => {
+
+ const draco = module.draco;
+ const decoder = new draco.Decoder();
+
+ try {
+
+ const geometry = decodeGeometry( draco, decoder, new Int8Array( buffer ), taskConfig );
+
+ const buffers = geometry.attributes.map( ( attr ) => attr.array.buffer );
+
+ if ( geometry.index ) buffers.push( geometry.index.array.buffer );
+
+ self.postMessage( { type: 'decode', id: message.id, geometry }, buffers );
+
+ } catch ( error ) {
+
+ console.error( error );
+
+ self.postMessage( { type: 'error', id: message.id, error: error.message } );
+
+ } finally {
+
+ draco.destroy( decoder );
+
+ }
+
+ } );
+ break;
+
+ }
+
+ };
+
+ function decodeGeometry( draco, decoder, array, taskConfig ) {
+
+ const attributeIDs = taskConfig.attributeIDs;
+ const attributeTypes = taskConfig.attributeTypes;
+
+ let dracoGeometry;
+ let decodingStatus;
+
+ const geometryType = decoder.GetEncodedGeometryType( array );
+
+ if ( geometryType === draco.TRIANGULAR_MESH ) {
+
+ dracoGeometry = new draco.Mesh();
+ decodingStatus = decoder.DecodeArrayToMesh( array, array.byteLength, dracoGeometry );
+
+ } else if ( geometryType === draco.POINT_CLOUD ) {
+
+ dracoGeometry = new draco.PointCloud();
+ decodingStatus = decoder.DecodeArrayToPointCloud( array, array.byteLength, dracoGeometry );
+
+ } else {
+
+ throw new Error( 'THREE.DRACOLoader: Unexpected geometry type.' );
+
+ }
+
+ if ( ! decodingStatus.ok() || dracoGeometry.ptr === 0 ) {
+
+ throw new Error( 'THREE.DRACOLoader: Decoding failed: ' + decodingStatus.error_msg() );
+
+ }
+
+ const geometry = { index: null, attributes: [] };
+
+ // Gather all vertex attributes.
+ for ( const attributeName in attributeIDs ) {
+
+ const attributeType = self[ attributeTypes[ attributeName ] ];
+
+ let attribute;
+ let attributeID;
+
+ // A Draco file may be created with default vertex attributes, whose attribute IDs
+ // are mapped 1:1 from their semantic name (POSITION, NORMAL, ...). Alternatively,
+ // a Draco file may contain a custom set of attributes, identified by known unique
+ // IDs. glTF files always do the latter, and `.drc` files typically do the former.
+ if ( taskConfig.useUniqueIDs ) {
+
+ attributeID = attributeIDs[ attributeName ];
+ attribute = decoder.GetAttributeByUniqueId( dracoGeometry, attributeID );
+
+ } else {
+
+ attributeID = decoder.GetAttributeId( dracoGeometry, draco[ attributeIDs[ attributeName ] ] );
+
+ if ( attributeID === - 1 ) continue;
+
+ attribute = decoder.GetAttribute( dracoGeometry, attributeID );
+
+ }
+
+ const attributeResult = decodeAttribute( draco, decoder, dracoGeometry, attributeName, attributeType, attribute );
+
+ if ( attributeName === 'color' ) {
+
+ attributeResult.vertexColorSpace = taskConfig.vertexColorSpace;
+
+ }
+
+ geometry.attributes.push( attributeResult );
+
+ }
+
+ // Add index.
+ if ( geometryType === draco.TRIANGULAR_MESH ) {
+
+ geometry.index = decodeIndex( draco, decoder, dracoGeometry );
+
+ }
+
+ draco.destroy( dracoGeometry );
+
+ return geometry;
+
+ }
+
+ function decodeIndex( draco, decoder, dracoGeometry ) {
+
+ const numFaces = dracoGeometry.num_faces();
+ const numIndices = numFaces * 3;
+ const byteLength = numIndices * 4;
+
+ const ptr = draco._malloc( byteLength );
+ decoder.GetTrianglesUInt32Array( dracoGeometry, byteLength, ptr );
+ const index = new Uint32Array( draco.HEAPF32.buffer, ptr, numIndices ).slice();
+ draco._free( ptr );
+
+ return { array: index, itemSize: 1 };
+
+ }
+
+ function decodeAttribute( draco, decoder, dracoGeometry, attributeName, attributeType, attribute ) {
+
+ const numComponents = attribute.num_components();
+ const numPoints = dracoGeometry.num_points();
+ const numValues = numPoints * numComponents;
+ const byteLength = numValues * attributeType.BYTES_PER_ELEMENT;
+ const dataType = getDracoDataType( draco, attributeType );
+
+ const ptr = draco._malloc( byteLength );
+ decoder.GetAttributeDataArrayForAllPoints( dracoGeometry, attribute, dataType, byteLength, ptr );
+ const array = new attributeType( draco.HEAPF32.buffer, ptr, numValues ).slice();
+ draco._free( ptr );
+
+ return {
+ name: attributeName,
+ array: array,
+ itemSize: numComponents
+ };
+
+ }
+
+ function getDracoDataType( draco, attributeType ) {
+
+ switch ( attributeType ) {
+
+ case Float32Array: return draco.DT_FLOAT32;
+ case Int8Array: return draco.DT_INT8;
+ case Int16Array: return draco.DT_INT16;
+ case Int32Array: return draco.DT_INT32;
+ case Uint8Array: return draco.DT_UINT8;
+ case Uint16Array: return draco.DT_UINT16;
+ case Uint32Array: return draco.DT_UINT32;
+
+ }
+
+ }
+
+}
+
+export { DRACOLoader };
diff --git a/public/vendor/three-setup.js b/public/vendor/three-setup.js
index 7fe818a..c69cc2e 100644
--- a/public/vendor/three-setup.js
+++ b/public/vendor/three-setup.js
@@ -1,7 +1,9 @@
import * as THREE from 'three';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
+import { DRACOLoader } from 'three/addons/loaders/DRACOLoader.js';
window.THREE = THREE;
window.GLTFLoader = GLTFLoader;
window.OrbitControls = OrbitControls;
+window.DRACOLoader = DRACOLoader;
diff --git a/src/SomhairlesDream.Frontend/App.fs b/src/SomhairlesDream.Frontend/App.fs
index 82bae88..72d1be2 100644
--- a/src/SomhairlesDream.Frontend/App.fs
+++ b/src/SomhairlesDream.Frontend/App.fs
@@ -184,25 +184,33 @@ let frameObject (view: ViewState) (node: ThreeNode) =
let size = createVector3 ()
box.getCenter center |> ignore
box.getSize size |> ignore
- let radius = 0.5 * size.length ()
+ // Fit the longest axis horizontally (it runs across the screen in the
+ // side-on hero camera) and the next-longest vertically.
+ let axes = [| size.x; size.y; size.z |] |> Array.sort
+ let longest = axes.[2]
+ let smallest = axes.[0]
- if radius > 0.000001 then
+ if longest > 0.000001 then
node.position.set(-center.x, -center.y, -center.z) |> ignore
let camera = view.Camera
let width = max 1.0 (float view.Canvas.clientWidth)
let height = max 1.0 (float view.Canvas.clientHeight)
- let aspect = width / height
+ let aspect = max 0.3 (width / height)
camera.aspect <- aspect
let fovY = camera.fov * Math.PI / 180.0
- let fovX = 2.0 * atan (tan (fovY / 2.0) * aspect)
- let limitingFov = min fovY fovX
- let distance = 1.28 * radius / sin (limitingFov / 2.0)
- let dirX, dirY, dirZ = 0.62, 0.45, 0.70
+ let tanHalf = tan (fovY / 2.0)
+ // Longest axis lies across the screen; the smallest axis is the
+ // one stacking vertically in a side-on view of a slender vehicle.
+ let halfW = 0.5 * longest / (tanHalf * aspect)
+ let halfH = 0.5 * smallest / tanHalf
+ let distance = 1.25 * max halfW halfH
+ // Side-on hero camera: look across the hull, not down its nose.
+ let dirX, dirY, dirZ = 0.30, 0.44, 0.85
let norm = sqrt (dirX * dirX + dirY * dirY + dirZ * dirZ)
camera.position.set(dirX / norm * distance, dirY / norm * distance, dirZ / norm * distance)
|> ignore
- camera.near <- max (distance * 0.01) (distance - radius * 2.5)
- camera.far <- distance + radius * 4.0
+ camera.near <- max 1.0 (distance * 0.05)
+ camera.far <- distance + longest * 3.0
camera.lookAt(0.0, 0.0, 0.0)
camera.updateProjectionMatrix ()
@@ -374,6 +382,10 @@ let startStepLoad (view: ViewState) (owner: LoadOwner) (historical: bool) =
loadedStep <- None
toggleHidden ("viewport-loading", false)
let loader = createGLTFLoader ()
+ // Attach the Draco decoder so geometry-compressed GLBs stream in small.
+ let draco = createDRACOLoader ()
+ setDracoDecoderPath draco "/vendor/addons/libs/draco/"
+ attachDracoLoader loader draco
let projectId = JS.encodeURIComponent owner.ProjectId
let runId = JS.encodeURIComponent owner.RunId
// Cache-buster: the GLB is always re-fetched from the server.
@@ -723,7 +735,7 @@ let probeSelection : RunSelection =
let shipSelection : RunSelection =
{ ProjectId = "ship-001"
- RunId = "run-ship-001"
+ RunId = "run-ship-002"
BaseId = "ship-base-000"
TargetId = "ship-target-007"
Render = false }
diff --git a/src/SomhairlesDream.Frontend/Bindings.fs b/src/SomhairlesDream.Frontend/Bindings.fs
index 7f0ce31..05d8e95 100644
--- a/src/SomhairlesDream.Frontend/Bindings.fs
+++ b/src/SomhairlesDream.Frontend/Bindings.fs
@@ -59,6 +59,15 @@ type ServerEvent =
type GLTFScene =
abstract scene: ThreeNode with get
+[<Emit("new DRACOLoader()")>]
+let createDRACOLoader () : obj = jsNative
+
+[<Emit("$0.setDecoderPath($1)")>]
+let setDracoDecoderPath (draco: obj) (path: string) : unit = jsNative
+
+[<Emit("$0.setDRACOLoader($1)")>]
+let attachDracoLoader (loader: obj) (draco: obj) : unit = jsNative
+
[<Emit("new GLTFLoader()")>]
let createGLTFLoader () : obj = jsNative
diff --git a/src/SomhairlesDream.Modeling/Definitions.fs b/src/SomhairlesDream.Modeling/Definitions.fs
index e1d2739..44e1998 100644
--- a/src/SomhairlesDream.Modeling/Definitions.fs
+++ b/src/SomhairlesDream.Modeling/Definitions.fs
@@ -92,87 +92,65 @@ module Design =
ObjectIds = prior (prior (prior (prior (prior (prior bus booms) rtgs) hga) instruments) mast) [||]
Changes = liveryChanges } |]
- // HNX-01 crewed interstellar starship (Concept A "Keel"): fusion-torch
- // study with a spin-gravity habitat torus, water-shadow radiation
- // shield and an axial keel separating crew from the reactor section.
- // Display-scale visual research; no mass/thrust/shielding closure is
- // claimed — the ship.py module header lists the deliberately open
- // engineering problems.
+ // HNX-02 "Copernicus-class" crewed interplanetary transfer vehicle:
+ // nuclear-thermal propulsion stage, spin-gravity centrifuge ring,
+ // water-wall radiation shielding, Whipple MMOD shielding, and an
+ // Orion-class shuttle docked as the scale reference. Built from
+ // procedural engineering structures (trusses, pressure vessels, bell
+ // nozzles), not stacked primitives. Display-scale visual research;
+ // no mass/thrust/shielding closure is claimed.
let shipSteps : DesignStep array =
let keel =
- [| "starship.keel.spine"; "starship.nose.cone"; "starship.dock.ring" |]
+ [| "hnx02.hull.core"; "hnx02.spine.truss"; "hnx02.node.forward" |]
let hab =
- [| "starship.hab.torus"; "starship.hub.core"
- "starship.spoke.00"; "starship.spoke.01"; "starship.spoke.02"
- "starship.spoke.03"; "starship.spoke.04"; "starship.spoke.05" |]
+ [| "hnx02.hab.a"; "hnx02.hab.b"; "hnx02.hab.whipple" |]
let radiation =
- [| "starship.shadow.collar"; "starship.storm.cellar" |]
+ [| "hnx02.rad.waterwall"; "hnx02.centrifuge.ring" |]
let reactor =
- [| "starship.core.pressure"; "starship.core.shield.plug"
- "starship.nozzle.magnetic"
- "starship.radiator.00"; "starship.radiator.01"; "starship.radiator.02"
- "starship.radiator.03"; "starship.radiator.04"; "starship.radiator.05" |]
+ [| "hnx02.prop.tank"; "hnx02.reactor.core"; "hnx02.thrust.cone"
+ "hnx02.nozzle.cluster" |]
let power =
- [| "starship.power.truss"; "starship.capacitor.ring"
- "starship.comm.dish"; "starship.comm.feed" |]
+ [| "hnx02.power.solar"; "hnx02.power.radiator" |]
let cabin =
- [| "starship.window.00"; "starship.window.01"; "starship.window.02"
- "starship.window.03"
- "starship.airlock.ring"; "starship.airlock.tunnel"
- "starship.cargo.pod.00"; "starship.cargo.pod.01" |]
+ [| "hnx02.cabin.airlock"; "hnx02.shuttle.orion" |]
+ let livery =
+ [| "hnx02.detail.greeble"; "hnx02.detail.antenna" |]
let prior (prefix: string array) (current: string array) =
Array.append prefix current
let keelChanges =
- [| { ObjectId = "starship.keel.spine"; Kind = "add"; Summary = "Add axial keel spine from bow to nozzle bay" }
- { ObjectId = "starship.nose.cone"; Kind = "add"; Summary = "Add forward nose cone" }
- { ObjectId = "starship.dock.ring"; Kind = "add"; Summary = "Add forward docking ring" } |]
+ [| { ObjectId = "hnx02.hull.core"; Kind = "add"; Summary = "Add continuous 16 m pressure hull with viewport bands and ring frames" }
+ { ObjectId = "hnx02.spine.truss"; Kind = "add"; Summary = "Add outrigger trusses with utility conduits and equipment rows" }
+ { ObjectId = "hnx02.node.forward"; Kind = "add"; Summary = "Add five-port forward docking node" } |]
let habChanges =
- Array.append
- [| { ObjectId = "starship.hab.torus"; Kind = "add"; Summary = "Add rotating spin-gravity habitat torus" }
- { ObjectId = "starship.hub.core"; Kind = "add"; Summary = "Add zero-g hub with bearing core" } |]
- (Array.init 6 (fun index ->
- { ObjectId = sprintf "starship.spoke.%02d" index; Kind = "add"; Summary = "Add pressurized transfer spoke" }))
+ [| { ObjectId = "hnx02.hab.a"; Kind = "add"; Summary = "Add forward habitat module with viewport rows" }
+ { ObjectId = "hnx02.hab.b"; Kind = "add"; Summary = "Add aft habitat module" }
+ { ObjectId = "hnx02.hab.whipple"; Kind = "add"; Summary = "Add Whipple MMOD shield panels" } |]
let radiationChanges =
- [| { ObjectId = "starship.shadow.collar"; Kind = "add"; Summary = "Add water-shadow radiation collar" }
- { ObjectId = "starship.storm.cellar"; Kind = "add"; Summary = "Add storm cellar shelter volume" } |]
+ [| { ObjectId = "hnx02.rad.waterwall"; Kind = "add"; Summary = "Add water-wall radiation shield tanks" }
+ { ObjectId = "hnx02.centrifuge.ring"; Kind = "add"; Summary = "Add spin-gravity centrifuge ring with crew cars" } |]
let reactorChanges =
- Array.append
- [| { ObjectId = "starship.core.pressure"; Kind = "add"; Summary = "Add fusion core pressure vessel" }
- { ObjectId = "starship.core.shield.plug"; Kind = "add"; Summary = "Add reactor shield plug" }
- { ObjectId = "starship.nozzle.magnetic"; Kind = "add"; Summary = "Add magnetic exhaust nozzle" } |]
- (Array.init 6 (fun index ->
- { ObjectId = sprintf "starship.radiator.%02d" index; Kind = "add"; Summary = "Add waste-heat radiator panel" }))
+ [| { ObjectId = "hnx02.prop.tank"; Kind = "add"; Summary = "Add cryogenic propellant tanks with MLI blankets" }
+ { ObjectId = "hnx02.reactor.core"; Kind = "add"; Summary = "Add reactor core with control drums" }
+ { ObjectId = "hnx02.thrust.cone"; Kind = "add"; Summary = "Add thrust structure and shadow shield" }
+ { ObjectId = "hnx02.nozzle.cluster"; Kind = "add"; Summary = "Add nuclear-thermal engine cluster" } |]
let powerChanges =
- [| { ObjectId = "starship.power.truss"; Kind = "add"; Summary = "Add power conditioning truss" }
- { ObjectId = "starship.capacitor.ring"; Kind = "add"; Summary = "Add pulsed-power capacitor hoop" }
- { ObjectId = "starship.comm.dish"; Kind = "add"; Summary = "Add high-gain communication dish" }
- { ObjectId = "starship.comm.feed"; Kind = "add"; Summary = "Add communication feed horn" } |]
+ [| { ObjectId = "hnx02.power.solar"; Kind = "add"; Summary = "Add deployable solar arrays" }
+ { ObjectId = "hnx02.power.radiator"; Kind = "add"; Summary = "Add finned heat-rejection radiators" } |]
let cabinChanges =
- Array.append
- (Array.init 4 (fun index ->
- { ObjectId = sprintf "starship.window.%02d" index; Kind = "add"; Summary = "Add habitat observation window" }))
- [| { ObjectId = "starship.airlock.ring"; Kind = "add"; Summary = "Add forward airlock ring" }
- { ObjectId = "starship.airlock.tunnel"; Kind = "add"; Summary = "Add pressurized airlock tunnel" }
- { ObjectId = "starship.cargo.pod.00"; Kind = "add"; Summary = "Add port cargo pod" }
- { ObjectId = "starship.cargo.pod.01"; Kind = "add"; Summary = "Add starboard cargo pod" } |]
+ [| { ObjectId = "hnx02.cabin.airlock"; Kind = "add"; Summary = "Add EVA airlock with external hatch" }
+ { ObjectId = "hnx02.shuttle.orion"; Kind = "add"; Summary = "Add docked Orion-class crew shuttle" } |]
let liveryChanges =
- [| { ObjectId = "starship.keel.spine"; Kind = "update"; Summary = "Livery pass: bone-white hull structure" }
- { ObjectId = "starship.hab.torus"; Kind = "update"; Summary = "Livery pass: habitat ring bone finish" }
- { ObjectId = "starship.shadow.collar"; Kind = "update"; Summary = "Livery pass: ceramic shield darkening" }
- { ObjectId = "starship.storm.cellar"; Kind = "update"; Summary = "Livery pass: ceramic cellar finish" }
- { ObjectId = "starship.dock.ring"; Kind = "update"; Summary = "Livery pass: amber accent on dock ring" }
- { ObjectId = "starship.capacitor.ring"; Kind = "update"; Summary = "Livery pass: amber capacitor hoop" }
- { ObjectId = "starship.window.00"; Kind = "update"; Summary = "Livery pass: glass glazing" }
- { ObjectId = "starship.nozzle.magnetic"; Kind = "update"; Summary = "Livery pass: nozzle coil graphite" } |]
+ [| { ObjectId = "hnx02.detail.greeble"; Kind = "add"; Summary = "Add equipment rows, cable looms and navigation lights" }
+ { ObjectId = "hnx02.detail.antenna"; Kind = "add"; Summary = "Add high-gain communication antennas" } |]
[| { StepId = "ship-01-keel"; Index = 0; ObjectIds = keel; Changes = keelChanges }
- { StepId = "ship-02-hab"; Index = 1; ObjectIds = Array.append keel hab; Changes = habChanges }
- { StepId = "ship-03-radiation"; Index = 2; ObjectIds = Array.append (Array.append keel hab) radiation; Changes = radiationChanges }
- { StepId = "ship-04-reactor"; Index = 3; ObjectIds = Array.append (Array.append (Array.append keel hab) radiation) reactor; Changes = reactorChanges }
- { StepId = "ship-05-power"; Index = 4; ObjectIds = Array.append (Array.append (Array.append (Array.append keel hab) radiation) reactor) power; Changes = powerChanges }
- { StepId = "ship-06-cabin"; Index = 5; ObjectIds = Array.append (Array.append (Array.append (Array.append (Array.append keel hab) radiation) reactor) power) cabin; Changes = cabinChanges }
- { StepId = "ship-07-livery"; Index = 6; ObjectIds = Array.append (Array.append (Array.append (Array.append (Array.append (Array.append keel hab) radiation) reactor) power) cabin) [||]; Changes = liveryChanges } |]
+ { StepId = "ship-02-hab"; Index = 1; ObjectIds = prior keel hab; Changes = habChanges }
+ { StepId = "ship-03-radiation"; Index = 2; ObjectIds = prior (prior keel hab) radiation; Changes = radiationChanges }
+ { StepId = "ship-04-reactor"; Index = 3; ObjectIds = prior (prior (prior keel hab) radiation) reactor; Changes = reactorChanges }
+ { StepId = "ship-05-power"; Index = 4; ObjectIds = prior (prior (prior (prior keel hab) radiation) reactor) power; Changes = powerChanges }
+ { StepId = "ship-06-cabin"; Index = 5; ObjectIds = prior (prior (prior (prior (prior keel hab) radiation) reactor) power) cabin; Changes = cabinChanges }
+ { StepId = "ship-07-livery"; Index = 6; ObjectIds = prior (prior (prior (prior (prior (prior keel hab) radiation) reactor) power) cabin) livery; Changes = liveryChanges } |]
let forProject (projectId: string) : DesignStep array =
if projectId.StartsWith("probe-") then probeSteps
diff --git a/src/SomhairlesDream.Server/Program.fs b/src/SomhairlesDream.Server/Program.fs
index 53cfef6..b2ea8a8 100644
--- a/src/SomhairlesDream.Server/Program.fs
+++ b/src/SomhairlesDream.Server/Program.fs
@@ -6,6 +6,7 @@ open System.Threading.Tasks
open Microsoft.AspNetCore.Builder
open Microsoft.AspNetCore.Hosting
open Microsoft.AspNetCore.Http
+open Microsoft.AspNetCore.StaticFiles
open Microsoft.Extensions.FileProviders
open Microsoft.Extensions.Hosting
open SomhairlesDream.Server
@@ -57,7 +58,17 @@ let events (context: HttpContext) : Task =
} :> Task)
app.UseDefaultFiles() |> ignore
-app.UseStaticFiles() |> ignore
+
+// Serve geometry and decoder binaries with correct MIME types: ASP.NET's
+// default map has no entry for .glb/.wasm, so those requests would 404.
+let staticFileOptions =
+ let provider = FileExtensionContentTypeProvider()
+ provider.Mappings.[".glb"] <- "model/gltf-binary"
+ provider.Mappings.[".gltf"] <- "model/gltf+json"
+ provider.Mappings.[".wasm"] <- "application/wasm"
+ StaticFileOptions(ContentTypeProvider = provider)
+
+app.UseStaticFiles(staticFileOptions) |> ignore
app.MapGet("/health", Func<IResult>(fun () -> Results.Ok({| status = "ok" |}))) |> ignore
diff --git a/stage/blender/ship.py b/stage/blender/ship.py
index e1dd30c..0f30d54 100644
--- a/stage/blender/ship.py
+++ b/stage/blender/ship.py
@@ -1,697 +1,1051 @@
-"""HNX-01 crewed interstellar starship dataset for the artifact pipeline.
+"""HNX-02 "Copernicus-class" crewed interplanetary transfer vehicle.
-Concept A "Keel" — a crewed fusion-torch starship study built on the same
-Blender contract as the YJ-01 probe dataset (stage/blender/probe.py):
+Rebuild of the crewed starship with procedural engineering structures
+(space trusses, pressure vessels, habitat modules, a spin-gravity
+centrifuge, an NTP engine cluster, and a docked Orion-class shuttle as
+the scale reference) instead of stacked primitives.
--step-id <id> --output <glb> --result-output <json> [--render-output <png>]
-Design intent (visual research, NOT a closed engineering design):
- - Long axial keel so the crew habitat sits as far as practical from the
- reactor section (inverse-square separation for neutron/gamma dose).
- - Rotating habitat torus for continuous spin gravity; six spokes carry
- the pressurized transfer tunnels; the hub is the zero-g docking core.
- - Heavy water-shadow shield collar and a storm cellar between the
- habitat and the reactor.
- - Aft fusion core with a magnetic nozzle; six droop radiators reject the
- waste heat. These hardware positions are placeholders — the script
- does not claim thrust, burn time, shielding closure, or life-support
- closure.
-
-Object ids: starship.<part>[.<index>], lowercase dotted, stable per step.
+Design intent (display-scale visual research, NOT closed engineering):
+ - Nuclear-thermal propulsion (NERVA-class stainless/epoxy composite
+ fuel element heritage) with a shadow shield between reactor and crew.
+ - Axial layout keeps the crew section as far from the reactor as the
+ truss allows; the water-wall tanks double as radiation shielding.
+ - The centrifuge ring provides spin gravity; cars are the pressurized
+ crew volumes. No mass/thrust/shielding closure is claimed.
+
+Object ids: hnx02.<part>[.<index>], lowercase dotted, stable per step.
Cumulative object lists must stay in lockstep with Design.shipSteps in
src/SomhairlesDream.Modeling/Definitions.fs.
"""
-
import argparse
import json
import math
import os
+import random
import sys
-from datetime import datetime, timezone
-import bpy # noqa: F401 (must be imported before mathutils: registers the bundled mathutils module)
+import bpy # noqa: F401 (must be imported first: registers bundled bmesh/mathutils)
+import bmesh
from mathutils import Vector
+# --------------------------------------------------------------- contract
+
STEP_OBJECTS = {
"ship-01-keel": [
- "starship.keel.spine",
- "starship.nose.cone",
- "starship.dock.ring",
+ "hnx02.hull.core",
+ "hnx02.spine.truss",
+ "hnx02.node.forward",
],
"ship-02-hab": [
- "starship.keel.spine",
- "starship.nose.cone",
- "starship.dock.ring",
- "starship.hab.torus",
- "starship.hub.core",
- "starship.spoke.00",
- "starship.spoke.01",
- "starship.spoke.02",
- "starship.spoke.03",
- "starship.spoke.04",
- "starship.spoke.05",
+ "hnx02.hull.core",
+ "hnx02.spine.truss",
+ "hnx02.node.forward",
+ "hnx02.hab.a",
+ "hnx02.hab.b",
+ "hnx02.hab.whipple",
],
"ship-03-radiation": [
- "starship.keel.spine",
- "starship.nose.cone",
- "starship.dock.ring",
- "starship.hab.torus",
- "starship.hub.core",
- "starship.spoke.00",
- "starship.spoke.01",
- "starship.spoke.02",
- "starship.spoke.03",
- "starship.spoke.04",
- "starship.spoke.05",
- "starship.shadow.collar",
- "starship.storm.cellar",
+ "hnx02.hull.core",
+ "hnx02.spine.truss",
+ "hnx02.node.forward",
+ "hnx02.hab.a",
+ "hnx02.hab.b",
+ "hnx02.hab.whipple",
+ "hnx02.rad.waterwall",
+ "hnx02.centrifuge.ring",
],
"ship-04-reactor": [
- "starship.keel.spine",
- "starship.nose.cone",
- "starship.dock.ring",
- "starship.hab.torus",
- "starship.hub.core",
- "starship.spoke.00",
- "starship.spoke.01",
- "starship.spoke.02",
- "starship.spoke.03",
- "starship.spoke.04",
- "starship.spoke.05",
- "starship.shadow.collar",
- "starship.storm.cellar",
- "starship.core.pressure",
- "starship.core.shield.plug",
- "starship.nozzle.magnetic",
- "starship.radiator.00",
- "starship.radiator.01",
- "starship.radiator.02",
- "starship.radiator.03",
- "starship.radiator.04",
- "starship.radiator.05",
+ "hnx02.hull.core",
+ "hnx02.spine.truss",
+ "hnx02.node.forward",
+ "hnx02.hab.a",
+ "hnx02.hab.b",
+ "hnx02.hab.whipple",
+ "hnx02.rad.waterwall",
+ "hnx02.centrifuge.ring",
+ "hnx02.prop.tank",
+ "hnx02.reactor.core",
+ "hnx02.thrust.cone",
+ "hnx02.nozzle.cluster",
],
"ship-05-power": [
- "starship.keel.spine",
- "starship.nose.cone",
- "starship.dock.ring",
- "starship.hab.torus",
- "starship.hub.core",
- "starship.spoke.00",
- "starship.spoke.01",
- "starship.spoke.02",
- "starship.spoke.03",
- "starship.spoke.04",
- "starship.spoke.05",
- "starship.shadow.collar",
- "starship.storm.cellar",
- "starship.core.pressure",
- "starship.core.shield.plug",
- "starship.nozzle.magnetic",
- "starship.radiator.00",
- "starship.radiator.01",
- "starship.radiator.02",
- "starship.radiator.03",
- "starship.radiator.04",
- "starship.radiator.05",
- "starship.power.truss",
- "starship.capacitor.ring",
- "starship.comm.dish",
- "starship.comm.feed",
+ "hnx02.hull.core",
+ "hnx02.spine.truss",
+ "hnx02.node.forward",
+ "hnx02.hab.a",
+ "hnx02.hab.b",
+ "hnx02.hab.whipple",
+ "hnx02.rad.waterwall",
+ "hnx02.centrifuge.ring",
+ "hnx02.prop.tank",
+ "hnx02.reactor.core",
+ "hnx02.thrust.cone",
+ "hnx02.nozzle.cluster",
+ "hnx02.power.solar",
+ "hnx02.power.radiator",
],
"ship-06-cabin": [
- "starship.keel.spine",
- "starship.nose.cone",
- "starship.dock.ring",
- "starship.hab.torus",
- "starship.hub.core",
- "starship.spoke.00",
- "starship.spoke.01",
- "starship.spoke.02",
- "starship.spoke.03",
- "starship.spoke.04",
- "starship.spoke.05",
- "starship.shadow.collar",
- "starship.storm.cellar",
- "starship.core.pressure",
- "starship.core.shield.plug",
- "starship.nozzle.magnetic",
- "starship.radiator.00",
- "starship.radiator.01",
- "starship.radiator.02",
- "starship.radiator.03",
- "starship.radiator.04",
- "starship.radiator.05",
- "starship.power.truss",
- "starship.capacitor.ring",
- "starship.comm.dish",
- "starship.comm.feed",
- "starship.window.00",
- "starship.window.01",
- "starship.window.02",
- "starship.window.03",
- "starship.airlock.ring",
- "starship.airlock.tunnel",
- "starship.cargo.pod.00",
- "starship.cargo.pod.01",
+ "hnx02.hull.core",
+ "hnx02.spine.truss",
+ "hnx02.node.forward",
+ "hnx02.hab.a",
+ "hnx02.hab.b",
+ "hnx02.hab.whipple",
+ "hnx02.rad.waterwall",
+ "hnx02.centrifuge.ring",
+ "hnx02.prop.tank",
+ "hnx02.reactor.core",
+ "hnx02.thrust.cone",
+ "hnx02.nozzle.cluster",
+ "hnx02.power.solar",
+ "hnx02.power.radiator",
+ "hnx02.cabin.airlock",
+ "hnx02.shuttle.orion",
],
"ship-07-livery": [
- "starship.keel.spine",
- "starship.nose.cone",
- "starship.dock.ring",
- "starship.hab.torus",
- "starship.hub.core",
- "starship.spoke.00",
- "starship.spoke.01",
- "starship.spoke.02",
- "starship.spoke.03",
- "starship.spoke.04",
- "starship.spoke.05",
- "starship.shadow.collar",
- "starship.storm.cellar",
- "starship.core.pressure",
- "starship.core.shield.plug",
- "starship.nozzle.magnetic",
- "starship.radiator.00",
- "starship.radiator.01",
- "starship.radiator.02",
- "starship.radiator.03",
- "starship.radiator.04",
- "starship.radiator.05",
- "starship.power.truss",
- "starship.capacitor.ring",
- "starship.comm.dish",
- "starship.comm.feed",
- "starship.window.00",
- "starship.window.01",
- "starship.window.02",
- "starship.window.03",
- "starship.airlock.ring",
- "starship.airlock.tunnel",
- "starship.cargo.pod.00",
- "starship.cargo.pod.01",
+ "hnx02.hull.core",
+ "hnx02.spine.truss",
+ "hnx02.node.forward",
+ "hnx02.hab.a",
+ "hnx02.hab.b",
+ "hnx02.hab.whipple",
+ "hnx02.rad.waterwall",
+ "hnx02.centrifuge.ring",
+ "hnx02.prop.tank",
+ "hnx02.reactor.core",
+ "hnx02.thrust.cone",
+ "hnx02.nozzle.cluster",
+ "hnx02.power.solar",
+ "hnx02.power.radiator",
+ "hnx02.cabin.airlock",
+ "hnx02.shuttle.orion",
+ "hnx02.detail.greeble",
+ "hnx02.detail.antenna",
],
}
-LIVERY_STEPS = {"ship-07-livery"}
+# Material keys per object id.
+OBJECT_MATERIAL = {
+ "hnx02.hull.core": "white",
+ "hnx02.spine.truss": "hull",
+ "hnx02.node.forward": "white",
+ "hnx02.hab.a": "white",
+ "hnx02.hab.b": "white",
+ "hnx02.hab.whipple": "shield",
+ "hnx02.rad.waterwall": "hull",
+ "hnx02.centrifuge.ring": "hull",
+ "hnx02.reactor.core": "dark",
+ "hnx02.prop.tank": "gold",
+ "hnx02.thrust.cone": "hull",
+ "hnx02.nozzle.cluster": "dark",
+ "hnx02.power.solar": "pv",
+ "hnx02.power.radiator": "radiator",
+ "hnx02.cabin.airlock": "white",
+ "hnx02.shuttle.orion": "white",
+ "hnx02.detail.greeble": "hull",
+ "hnx02.detail.antenna": "hull",
+}
def utc_now():
- return datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
+ from datetime import datetime, timezone
+
+ return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
def fail(message, code):
- print(message, file=sys.stderr)
- raise SystemExit(code)
+ print(json.dumps({"status": "failed", "message": message}))
+ sys.exit(code)
-def rgba(rgb, alpha=1.0):
- return (*rgb, alpha)
+# --------------------------------------------------------------- materials
-def make_material(bpy, name, color, *, metallic=0.0, roughness=0.45, emission=None, emission_strength=0.0):
- material = bpy.data.materials.get(name) or bpy.data.materials.new(name)
+def make_material(bpy, name, color, *, metallic=0.0, roughness=0.45):
+ material = bpy.data.materials.get(name)
+ if material is not None:
+ return material
+ material = bpy.data.materials.new(name)
material.use_nodes = True
- material.diffuse_color = rgba(color)
- shader = material.node_tree.nodes.get("Principled BSDF")
- shader.inputs["Base Color"].default_value = rgba(color)
- shader.inputs["Metallic"].default_value = metallic
- shader.inputs["Roughness"].default_value = roughness
- if emission is not None:
- emission_input = shader.inputs.get("Emission Color") or shader.inputs.get("Emission")
- if emission_input:
- emission_input.default_value = rgba(emission)
- strength_input = shader.inputs.get("Emission Strength")
- if strength_input:
- strength_input.default_value = emission_strength
+ bsdf = material.node_tree.nodes.get("Principled BSDF")
+ bsdf.inputs["Base Color"].default_value = (*color, 1.0)
+ bsdf.inputs["Metallic"].default_value = metallic
+ bsdf.inputs["Roughness"].default_value = roughness
return material
def build_materials(bpy):
return {
- "clay_body": make_material(bpy, "CLAY BODY", (0.38, 0.42, 0.46), metallic=0.20, roughness=0.38),
- "clay_structure": make_material(bpy, "CLAY STRUCTURE", (0.17, 0.21, 0.25), metallic=0.65, roughness=0.32),
- "clay_dark": make_material(bpy, "CLAY DARK", (0.045, 0.055, 0.065), metallic=0.45, roughness=0.28),
- "clay_white": make_material(bpy, "CLAY REFLECTOR", (0.62, 0.65, 0.67), metallic=0.18, roughness=0.34),
- "clay_glass": make_material(bpy, "CLAY GLASS", (0.035, 0.10, 0.13), metallic=0.12, roughness=0.14, emission=(0.01, 0.05, 0.07), emission_strength=0.15),
- "livery_hull": make_material(bpy, "LIVERY HULL BONE", (0.72, 0.70, 0.64), metallic=0.30, roughness=0.30),
- "livery_structure": make_material(bpy, "LIVERY STRUCTURE", (0.13, 0.18, 0.22), metallic=0.78, roughness=0.24),
- "livery_dark": make_material(bpy, "LIVERY GRAPHITE", (0.018, 0.025, 0.032), metallic=0.70, roughness=0.24),
- "livery_ceramic": make_material(bpy, "LIVERY SHADOW CERAMIC", (0.10, 0.115, 0.125), metallic=0.25, roughness=0.42),
- "livery_amber": make_material(bpy, "LIVERY AMBER STRIPE", (0.62, 0.34, 0.05), metallic=0.25, roughness=0.30),
- "livery_white": make_material(bpy, "LIVERY DISH WHITE", (0.78, 0.81, 0.79), metallic=0.15, roughness=0.28),
- "livery_glass": make_material(bpy, "LIVERY GLASS", (0.008, 0.055, 0.085), metallic=0.20, roughness=0.10, emission=(0.005, 0.045, 0.09), emission_strength=0.20),
- "livery_nozzle": make_material(bpy, "LIVERY NOZZLE COIL", (0.16, 0.17, 0.20), metallic=0.85, roughness=0.20),
+ "hull": make_material(bpy, "hnx02.hull", (0.62, 0.64, 0.66), metallic=0.88, roughness=0.38),
+ "white": make_material(bpy, "hnx02.white", (0.86, 0.86, 0.84), metallic=0.18, roughness=0.52),
+ "shield": make_material(bpy, "hnx02.shield", (0.44, 0.46, 0.50), metallic=0.30, roughness=0.72),
+ "dark": make_material(bpy, "hnx02.dark", (0.10, 0.11, 0.13), metallic=0.72, roughness=0.48),
+ "radiator": make_material(bpy, "hnx02.radiator", (0.90, 0.91, 0.92), metallic=0.08, roughness=0.30),
+ "pv": make_material(bpy, "hnx02.pv", (0.07, 0.10, 0.24), metallic=0.35, roughness=0.28),
+ "gold": make_material(bpy, "hnx02.gold", (0.82, 0.58, 0.19), metallic=1.0, roughness=0.30),
+ "copper": make_material(bpy, "hnx02.copper", (0.70, 0.44, 0.19), metallic=1.0, roughness=0.34),
}
-def clear_scene(bpy):
- bpy.ops.wm.read_factory_settings(use_empty=True)
-
+# ------------------------------------------------------------ bmesh kit
+
+
+def basis(axis):
+ z = Vector(axis).normalized()
+ ref = Vector((0.0, 0.0, 1.0)) if abs(z.z) < 0.9 else Vector((1.0, 0.0, 0.0))
+ x = ref.cross(z).normalized()
+ y = z.cross(x).normalized()
+ return x, y, z
+
+
+def cyl(bm, p1, p2, r1, r2=None, seg=12, cap1=True, cap2=True):
+ p1, p2 = Vector(p1), Vector(p2)
+ r2 = r1 if r2 is None else r2
+ d = p2 - p1
+ if d.length < 1e-7:
+ return
+ x, y, _ = basis(d)
+ ring1, ring2 = [], []
+ for i in range(seg):
+ a = 2.0 * math.pi * i / seg
+ off = x * math.cos(a) + y * math.sin(a)
+ ring1.append(bm.verts.new(p1 + off * r1))
+ ring2.append(bm.verts.new(p2 + off * r2))
+ for i in range(seg):
+ j = (i + 1) % seg
+ bm.faces.new((ring1[i], ring1[j], ring2[j], ring2[i]))
+ if cap1:
+ bm.faces.new(tuple(reversed(ring1)))
+ if cap2:
+ bm.faces.new(tuple(ring2))
+
+
+def torus(bm, center, R, r, axis=(0.0, 0.0, 1.0), seg=24, rseg=8):
+ center = Vector(center)
+ x, y, z = basis(axis)
+ rings = []
+ for i in range(seg):
+ a = 2.0 * math.pi * i / seg
+ radial = x * math.cos(a) + y * math.sin(a)
+ c = center + radial * R
+ ring = []
+ for j in range(rseg):
+ b = 2.0 * math.pi * j / rseg
+ ring.append(bm.verts.new(c + radial * (r * math.cos(b)) + z * (r * math.sin(b))))
+ rings.append(ring)
+ for i in range(seg):
+ i2 = (i + 1) % seg
+ for j in range(rseg):
+ j2 = (j + 1) % rseg
+ bm.faces.new((rings[i][j], rings[i][j2], rings[i2][j2], rings[i2][j]))
+
+
+def dome(bm, base, direction, radius, seg=20, layers=5):
+ d = Vector(direction).normalized()
+ x, y, _ = basis(d)
+ prev = None
+ for i in range(layers + 1):
+ t = i / layers
+ phi = t * math.pi / 2.0
+ r = radius * math.cos(phi)
+ h = radius * math.sin(phi)
+ c = Vector(base) + d * h
+ if r < 1e-6:
+ tip = bm.verts.new(c)
+ if prev:
+ for j in range(seg):
+ bm.faces.new((prev[j], prev[(j + 1) % seg], tip))
+ break
+ ring = []
+ for j in range(seg):
+ a = 2.0 * math.pi * j / seg
+ ring.append(bm.verts.new(c + (x * math.cos(a) + y * math.sin(a)) * r))
+ if prev:
+ for j in range(seg):
+ j2 = (j + 1) % seg
+ bm.faces.new((prev[j], prev[j2], ring[j2], ring[j]))
+ prev = ring
+
+
+def bell(bm, throat, direction, length, r_throat, r_exit, seg=32, layers=14, exponent=1.7):
+ d = Vector(direction).normalized()
+ x, y, _ = basis(d)
+ prev = None
+ for i in range(layers + 1):
+ t = i / layers
+ r = r_throat + (r_exit - r_throat) * (t ** exponent)
+ c = Vector(throat) + d * (length * t)
+ ring = []
+ for j in range(seg):
+ a = 2.0 * math.pi * j / seg
+ ring.append(bm.verts.new(c + (x * math.cos(a) + y * math.sin(a)) * r))
+ if prev:
+ for j in range(seg):
+ j2 = (j + 1) % seg
+ bm.faces.new((prev[j], prev[j2], ring[j2], ring[j]))
+ prev = ring
+
+
+def box(bm, center, ex, ey, ez, ax, ay, az):
+ c = Vector(center)
+ ax, ay, az = Vector(ax), Vector(ay), Vector(az)
+ vs = {}
+ for sx in (-1, 1):
+ for sy in (-1, 1):
+ for sz in (-1, 1):
+ vs[(sx, sy, sz)] = bm.verts.new(c + ax * (sx * ex) + ay * (sy * ey) + az * (sz * ez))
+ quads = [
+ (vs[(-1, -1, -1)], vs[(-1, 1, -1)], vs[(-1, 1, 1)], vs[(-1, -1, 1)]),
+ (vs[(1, 1, -1)], vs[(1, -1, -1)], vs[(1, -1, 1)], vs[(1, 1, 1)]),
+ (vs[(-1, 1, -1)], vs[(1, 1, -1)], vs[(1, 1, 1)], vs[(-1, 1, 1)]),
+ (vs[(1, -1, -1)], vs[(-1, -1, -1)], vs[(-1, -1, 1)], vs[(1, -1, 1)]),
+ (vs[(-1, -1, 1)], vs[(-1, 1, 1)], vs[(1, 1, 1)], vs[(1, -1, 1)]),
+ (vs[(1, -1, -1)], vs[(1, 1, -1)], vs[(-1, 1, -1)], vs[(-1, -1, -1)]),
+ ]
+ for q in quads:
+ bm.faces.new(q)
+
+
+# ------------------------------------------------------------ structures
+
+
+def truss(bm, p1, p2, width, bays=6, chord_r=0.09, strut_r=0.05, angles=None):
+ p1, p2 = Vector(p1), Vector(p2)
+ if angles is None:
+ angles = [90.0, 210.0, 330.0]
+ x, y, _ = basis(p2 - p1)
+ nodes = []
+ for deg in angles:
+ a = math.radians(deg)
+ off = (x * math.cos(a) + y * math.sin(a)) * width
+ nodes.append((p1 + off, p2 + off))
+ for a, b in nodes:
+ cyl(bm, a, b, chord_r, seg=8)
+ n = len(nodes)
+ for i in range(bays + 1):
+ t = i / bays
+ pts = [a.lerp(b, t) for a, b in nodes]
+ for k in range(n):
+ cyl(bm, pts[k], pts[(k + 1) % n], strut_r, seg=6)
+ if i < bays:
+ t2 = (i + 1) / bays
+ pts2 = [a.lerp(b, t2) for a, b in nodes]
+ for k in range(n):
+ cyl(bm, pts[k], pts2[(k + 1) % n], strut_r, seg=6)
+
+
+def ring_frame(bm, center, radius, axis, tube=0.07, seg=28, spokes=0, spoke_r=0.05):
+ center = Vector(center)
+ torus(bm, center, radius, tube, axis=axis, seg=seg, rseg=6)
+ if spokes:
+ x, y, _ = basis(axis)
+ for k in range(spokes):
+ a = 2.0 * math.pi * k / spokes
+ radial = x * math.cos(a) + y * math.sin(a)
+ cyl(bm, center + radial * radius * 0.12, center + radial * radius, spoke_r, seg=6)
+
+
+def pressure_vessel(bm, p1, p2, radius, rings=5, seg=24, ports=True):
+ p1, p2 = Vector(p1), Vector(p2)
+ d = (p2 - p1).normalized()
+ cyl(bm, p1, p2, radius, seg=seg, cap1=False, cap2=False)
+ dome(bm, p1, -d, radius, seg=seg)
+ dome(bm, p2, d, radius, seg=seg)
+ for i in range(1, rings + 1):
+ t = i / (rings + 1)
+ torus(bm, p1.lerp(p2, t), radius, 0.06, axis=d, seg=seg, rseg=6)
+ if ports:
+ x, y, _ = basis(d)
+ for t in (0.2, 0.5, 0.8):
+ c = p1.lerp(p2, t)
+ off = x * radius
+ cyl(bm, c + off * 0.88, c + off * 1.45, 0.10, seg=8)
+ torus(bm, c + off * 1.45, 0.13, 0.04, axis=x, seg=10, rseg=5)
+
+
+def hab_module(bm, p1, p2, radius, ribs=8, windows=10, seg=28, hatch=True):
+ p1, p2 = Vector(p1), Vector(p2)
+ d = (p2 - p1).normalized()
+ x, y, _ = basis(d)
+ cyl(bm, p1, p2, radius, seg=seg)
+ for i in range(1, ribs + 1):
+ t = i / (ribs + 1)
+ torus(bm, p1.lerp(p2, t), radius, 0.075, axis=d, seg=seg, rseg=6)
+ length = (p2 - p1).length
+ for row, tt in ((0, 0.34), (1, 0.66)):
+ for k in range(windows):
+ a = 2.0 * math.pi * k / windows + row * math.pi / windows
+ radial = x * math.cos(a) + y * math.sin(a)
+ c = p1.lerp(p2, tt) + radial * radius
+ cyl(bm, c - radial * 0.08, c + radial * 0.14, 0.17, seg=12, cap1=False)
+ torus(bm, c + radial * 0.14, 0.185, 0.04, axis=radial, seg=14, rseg=5)
+ box(bm, c + radial * 0.16, 0.10, 0.30, 0.012, radial, d, d.cross(radial))
+ if hatch:
+ for end, dirv in ((p2, d), (p1, -d)):
+ torus(bm, end + dirv * 0.05, radius * 0.52, 0.11, axis=d, seg=seg, rseg=6)
+ for k in range(8):
+ a = 2.0 * math.pi * k / 8
+ radial = x * math.cos(a) + y * math.sin(a)
+ cyl(bm, end + radial * radius * 0.52 + dirv * 0.05,
+ end + radial * radius * 0.70 + dirv * 0.05, 0.05, seg=6)
+
+
+def centrifuge_ring(bm, center, radius, axis=(0.0, 1.0, 0.0), cars=6, seg=44):
+ """Rotating gravity ring: twin truss hoops, spokes, suspended cars."""
+ center = Vector(center)
+ x, y, z = basis(axis)
+ for off in (-0.55, 0.55):
+ c = center + z * off
+ torus(bm, c, radius, 0.085, axis=z, seg=seg, rseg=6)
+ torus(bm, c, radius * 0.995, 0.045, axis=z, seg=seg, rseg=5)
+ # cross bracing between the two hoops
+ for k in range(0, seg, 2):
+ a = 2.0 * math.pi * k / seg
+ a2 = 2.0 * math.pi * (k + 1) / seg
+ radial = x * math.cos(a) + y * math.sin(a)
+ radial2 = x * math.cos(a2) + y * math.sin(a2)
+ p_a0 = center + radial * radius + z * -0.55
+ p_a1 = center + radial * radius + z * 0.55
+ p_b0 = center + radial2 * radius + z * -0.55
+ p_b1 = center + radial2 * radius + z * 0.55
+ cyl(bm, p_a0, p_b1, 0.035, seg=5)
+ cyl(bm, p_a1, p_b0, 0.035, seg=5)
+ cyl(bm, p_a0, p_a1, 0.035, seg=5)
+ # spokes to hub
+ for k in range(cars):
+ a = 2.0 * math.pi * k / cars
+ radial = x * math.cos(a) + y * math.sin(a)
+ outer = center + radial * radius
+ cyl(bm, center, outer, 0.17, seg=10)
+ for tang in (-1.0, 1.0):
+ tangent = z * tang
+ p0 = center + radial * radius * 0.18 + tangent * 0.7
+ p1 = center + radial * radius * 0.55 + tangent * 0.7
+ cyl(bm, p0, p1, 0.07, seg=6)
+ cyl(bm, center + radial * radius * 0.5 - z * 0.9, center + radial * radius * 0.5 + z * 0.9, 0.09, seg=6)
+ # de-spin bearing rings at the hub
+ for zz in (-0.9, 0.9):
+ torus(bm, center + z * zz, radius * 0.135, 0.11, axis=z, seg=28, rseg=6)
+ # suspended cars
+ for k in range(cars):
+ a = 2.0 * math.pi * k / cars + math.pi / cars
+ radial = x * math.cos(a) + y * math.sin(a)
+ car = center + radial * (radius + 0.62)
+ cyl(bm, car - z * 0.85, car + z * 0.85, 0.62, seg=18)
+ torus(bm, car - z * 0.85, 0.64, 0.07, axis=z, seg=18, rseg=5)
+ torus(bm, car + z * 0.85, 0.64, 0.07, axis=z, seg=18, rseg=5)
+ cyl(bm, center + radial * radius, car, 0.075, seg=7)
+ for kk in range(5):
+ aa = 2.0 * math.pi * kk / 5
+ off = (z * math.cos(aa) + radial * math.sin(aa)) * 0.2
+ cyl(bm, car + off, car + off * 2.6, 0.03, seg=5)
+
+
+def gimbal(bm, pos, direction, radius=0.28, s=1.0):
+ pos = Vector(pos)
+ d = Vector(direction).normalized()
+ x, _, _ = basis(d)
+ torus(bm, pos, radius * s, 0.055 * s, axis=d, seg=16, rseg=5)
+ torus(bm, pos + d * 0.15 * s, radius * s * 0.9, 0.055 * s, axis=x, seg=16, rseg=5)
+ for k in (0, 1):
+ side = 1.0 if k == 0 else -1.0
+ cyl(bm, pos + x * side * radius * s * 1.35 - d * 0.12 * s,
+ pos + x * side * radius * s * 0.8 + d * 0.3 * s, 0.04 * s, seg=6)
+
+
+def regen_channels(bm, throat, direction, length, r_throat, r_exit, turns=6, tube=0.035, seg_pts=15):
+ throat = Vector(throat)
+ d = Vector(direction).normalized()
+ x, y, _ = basis(d)
+ steps = turns * seg_pts
+ pts = []
+ for i in range(steps + 1):
+ t = i / steps
+ r = r_throat + (r_exit - r_throat) * (t ** 1.7) + tube * 1.3
+ a = 2.0 * math.pi * turns * t
+ c = throat + d * (length * t)
+ pts.append(c + (x * math.cos(a) + y * math.sin(a)) * r)
+ for a, b in zip(pts, pts[1:]):
+ cyl(bm, a, b, tube, seg=5)
+
+
+def ntp_engine(bm, mount, direction, scale=1.0):
+ """Nuclear thermal engine: thrust structure, chamber, bell, turbopump."""
+ mount = Vector(mount)
+ d = Vector(direction).normalized()
+ s = scale
+ x, y, _ = basis(d)
+ cyl(bm, mount, mount + d * 0.5 * s, 0.44 * s, seg=14)
+ chamber = mount + d * 0.5 * s
+ cyl(bm, chamber, chamber + d * 0.45 * s, 0.36 * s, 0.31 * s, seg=18)
+ bell(bm, chamber + d * 0.45 * s, d, 2.3 * s, 0.31 * s, 1.05 * s, seg=32, layers=14)
+ for t in (0.3, 0.55, 0.78, 0.95):
+ c = chamber + d * (0.45 * s + 2.3 * s * t)
+ r = 0.31 * s + (1.05 * s - 0.31 * s) * (t ** 1.7)
+ torus(bm, c, r + 0.02 * s, 0.032 * s, axis=d, seg=24, rseg=5)
+ regen_channels(bm, chamber + d * 0.45 * s, d, 2.3 * s, 0.31 * s, 1.05 * s)
+ # turbopump cluster
+ pump = mount + d * 0.75 * s + x * 0.5 * s
+ cyl(bm, pump, pump + d * 0.6 * s, 0.21 * s, seg=12)
+ cyl(bm, pump - x * 0.22 * s, pump + x * 0.6 * s, 0.10 * s, seg=8)
+ cyl(bm, chamber + x * 0.32 * s, pump + d * 0.32 * s + x * 0.27 * s, 0.075 * s, seg=7)
+ cyl(bm, mount + d * 0.18 * s + x * 0.48 * s, chamber + x * 0.32 * s, 0.055 * s, seg=6)
+ # nozzle exit stiffener ring
+ exit_c = chamber + d * (0.45 * s + 2.3 * s)
+ torus(bm, exit_c - d * 0.05 * s, 1.05 * s, 0.05 * s, axis=d, seg=32, rseg=6)
+
+
+def shadow_shield(bm, center, direction, radius, seg=32, ribs=10):
+ center = Vector(center)
+ d = Vector(direction).normalized()
+ x, y, _ = basis(d)
+ cyl(bm, center - d * 0.12, center + d * 0.12, radius, seg=seg)
+ for k in range(ribs):
+ a = 2.0 * math.pi * k / ribs
+ radial = x * math.cos(a) + y * math.sin(a)
+ cyl(bm, center + radial * radius * 0.12 - d * 0.12,
+ center + radial * radius * 0.96 - d * 0.12, 0.05, seg=6)
+ torus(bm, center, radius, 0.06, axis=d, seg=seg, rseg=6)
+ for k in range(6):
+ a = 2.0 * math.pi * k / 6
+ radial = x * math.cos(a) + y * math.sin(a)
+ cyl(bm, center + radial * radius * 0.55, center + radial * radius * 0.55 - d * 1.2, 0.08, seg=7)
+
+
+def thrust_cone(bm, mount, direction, r_base, r_top, length, struts=10):
+ mount = Vector(mount)
+ d = Vector(direction).normalized()
+ x, y, _ = basis(d)
+ top = mount + d * length
+ for i, (c, r) in enumerate(((mount, r_base), (top, r_top))):
+ torus(bm, c, r, 0.06, axis=d, seg=28, rseg=5)
+ for k in range(struts):
+ a = 2.0 * math.pi * k / struts
+ radial = x * math.cos(a) + y * math.sin(a)
+ cyl(bm, mount + radial * r_base, top + radial * r_top, 0.055, seg=6)
+ for k in range(struts):
+ a = 2.0 * math.pi * k / struts
+ b = 2.0 * math.pi * (k + 1) / struts
+ ra = x * math.cos(a) + y * math.sin(a)
+ rb = x * math.cos(b) + y * math.sin(b)
+ mid = mount + d * (length * 0.5)
+ cyl(bm, mount + ra * r_base, mid + rb * ((r_base + r_top) * 0.5), 0.04, seg=5)
+
+
+def mli_bands(bm, p1, p2, radius, bands=10, seg=24, wobble=0.035):
+ p1, p2 = Vector(p1), Vector(p2)
+ d = (p2 - p1).normalized()
+ for i in range(1, bands + 1):
+ t = i / (bands + 1)
+ c = p1.lerp(p2, t)
+ w = 1.0 + wobble * math.sin(i * 2.3)
+ torus(bm, c, radius * w, 0.085, axis=d, seg=seg, rseg=7)
+
+
+def solar_array(bm, root, direction, span, width, cells=72, rows=14, face_normal=(0.0, 0.0, 1.0)):
+ """Deployable PV blanket: cell grid, frame rails, torque tube, hinge."""
+ root = Vector(root)
+ d = Vector(direction).normalized()
+ x, _y, _z = basis(d)
+ fn = Vector(face_normal).normalized()
+ if abs(fn.dot(d)) > 0.9:
+ fn = _z
+ x = d.cross(fn).normalized()
+ cw = span / cells
+ ch = width / rows
+ for i in range(cells):
+ for j in range(rows):
+ c = root + d * (cw * (i + 0.5)) + x * ((j - rows * 0.5 + 0.5) * ch)
+ box(bm, c, cw * 0.40, ch * 0.38, 0.02, d, x, fn)
+ for side in (-1.0, 1.0):
+ off = x * side * width * 0.5
+ box(bm, root + d * (span * 0.5) + off, span * 0.5, 0.05, 0.05, d, x, fn)
+ cyl(bm, root - x * width * 0.5, root + x * width * 0.5, 0.10, seg=10)
+ cyl(bm, root, root - d * 0.6, 0.13, seg=10)
+ torus(bm, root, 0.24, 0.06, axis=x, seg=16, rseg=5)
+
+
+def fin_radiator(bm, p1, p2, width, fins=48, fin_h=0.42, coolant=True):
+ p1, p2 = Vector(p1), Vector(p2)
+ d = (p2 - p1).normalized()
+ x, y, _ = basis(d)
+ cyl(bm, p1, p2, width * 0.5, seg=4)
+ for i in range(fins):
+ t = (i + 0.5) / fins
+ c = p1.lerp(p2, t)
+ for side in (0.5, -0.5):
+ off = y * side * 0.05
+ box(bm, c + off + y * side * fin_h * 0.5, width * 0.46, fin_h * 0.5, 0.014, x, y, d)
+ if coolant:
+ for side in (-0.30, 0.0, 0.30):
+ off = x * (width * side)
+ cyl(bm, p1 + off, p2 + off, 0.035, seg=6)
+
+
+def pipe_run(bm, points, r=0.06, seg=8):
+ for a, b in zip(points, points[1:]):
+ cyl(bm, a, b, r, seg=seg)
+
+
+def rcs_cluster(bm, pos, direction, s=1.0):
+ pos = Vector(pos)
+ d = Vector(direction).normalized()
+ x, y, _ = basis(d)
+ cyl(bm, pos, pos + d * 0.22 * s, 0.17 * s, seg=12)
+ for k in range(4):
+ a = 2.0 * math.pi * k / 4 + math.pi / 4
+ off = (x * math.cos(a) + y * math.sin(a)) * 0.14 * s
+ cyl(bm, pos + d * 0.22 * s + off, pos + d * 0.40 * s + off * 1.35, 0.055 * s, 0.032 * s, seg=6)
+
+
+def antenna_dish(bm, base, direction, radius, depth=0.4, ribs=8, s=1.0):
+ base = Vector(base)
+ d = Vector(direction).normalized()
+ x, y, _ = basis(d)
+ cyl(bm, base - d * 0.1 * s, base + d * 0.28 * s, 0.16 * s, seg=10)
+ gimbal(bm, base + d * 0.28 * s, d, radius=0.2 * s, s=s)
+ dish_center = base + d * 0.45 * s
+ layers = 5
+ prev = None
+ for i in range(layers + 1):
+ t = i / layers
+ r = radius * t
+ h = depth * (1.0 - t * t)
+ ring = []
+ for j in range(24):
+ a = 2.0 * math.pi * j / 24
+ ring.append(bm.verts.new(dish_center + (x * math.cos(a) + y * math.sin(a)) * r + d * h))
+ if prev:
+ for j in range(24):
+ j2 = (j + 1) % 24
+ bm.faces.new((prev[j], prev[j2], ring[j2], ring[j]))
+ prev = ring
+ for k in range(ribs):
+ a = 2.0 * math.pi * k / ribs
+ radial = x * math.cos(a) + y * math.sin(a)
+ cyl(bm, dish_center + radial * radius * 0.06, dish_center + radial * radius * 0.92 + d * 0.02, 0.03 * s, seg=5)
+ feed = dish_center + d * (depth + 1.15 * s * 0.6)
+ cyl(bm, dish_center + d * 0.06, feed, 0.035 * s, seg=6)
+ cyl(bm, feed - d * 0.06 * s, feed + d * 0.10 * s, 0.09 * s, seg=8)
+ for k in range(3):
+ a = 2.0 * math.pi * k / 3
+ radial = x * math.cos(a) + y * math.sin(a)
+ cyl(bm, dish_center + radial * radius * 0.8 + d * depth * 0.9, feed, 0.022 * s, seg=5)
+
+
+def whipple_panels(bm, p1, p2, radius, rows=6, cols=14, gap=0.04):
+ """MMOD shield: arc panels in a regular grid over a cylinder."""
+ p1, p2 = Vector(p1), Vector(p2)
+ d = (p2 - p1).normalized()
+ x, y, _ = basis(d)
+ length = (p2 - p1).length
+ for i in range(cols):
+ t = (i + 0.5) / cols
+ for j in range(rows):
+ a = 2.0 * math.pi * (j + 0.5) / rows
+ radial = x * math.cos(a) + y * math.sin(a)
+ tangent = d.cross(radial)
+ c = p1 + d * (length * t) + radial * (radius + 0.09)
+ box(bm, c, length / cols * 0.44, radius * math.pi / rows * 0.42, 0.035,
+ d, tangent, radial)
+
+
+def equipment_row(bm, start, direction, lateral, count, spacing, s_lo, s_hi, seed, lateral_axis=None):
+ """A deliberately arranged row of machinery boxes (not noise)."""
+ rng = random.Random(seed)
+ start = Vector(start)
+ direction = Vector(direction).normalized()
+ lateral = Vector(lateral).normalized()
+ ax = lateral_axis if lateral_axis is not None else direction.cross(lateral).normalized()
+ for i in range(count):
+ t = (i - (count - 1) * 0.5) * spacing
+ base = start + direction * t
+ w = rng.uniform(s_lo, s_hi)
+ h = rng.uniform(s_lo, s_hi)
+ dep = rng.uniform(s_lo, s_hi * 1.3)
+ box(bm, base + lateral * (dep * 0.5), w, h, dep * 0.5, direction, ax, lateral)
+ if rng.random() < 0.4:
+ cyl(bm, base + lateral * dep * 0.3 - ax * w * 0.6,
+ base + lateral * (dep * 0.5) - ax * w * 0.6, w * 0.18, seg=6)
+
+
+def panel_bank(bm, p1, p2, radius, cols=16, rows=8, depth=0.05):
+ """Regular rectangular panel grid over a cylinder (machined surface look)."""
+ p1, p2 = Vector(p1), Vector(p2)
+ d = (p2 - p1).normalized()
+ x, y, _ = basis(d)
+ length = (p2 - p1).length
+ for i in range(cols):
+ t = (i + 0.5) / cols
+ for j in range(rows):
+ a = 2.0 * math.pi * (j + 0.5) / rows
+ radial = x * math.cos(a) + y * math.sin(a)
+ tangent = d.cross(radial)
+ c = p1 + d * (length * t) + radial * radius
+ box(bm, c, length / cols * 0.42, radius * math.pi / rows * 0.40, depth,
+ d, tangent, radial)
+
+
+# --------------------------------------------------------------- assembly
-def assign_variants(obj, clay_material, livery_material):
- obj["object_id"] = obj.name
- obj["clay_material"] = clay_material.name
- obj["livery_material"] = livery_material.name
- obj.data.materials.clear()
- obj.data.materials.append(clay_material)
- return obj
-
-def apply_material_mode(bpy, materials, mode):
- if mode not in {"clay", "livery"}:
- raise ValueError(f"unknown material mode: {mode}")
- for obj in bpy.context.scene.objects:
- material_name = obj.get(f"{mode}_material")
- if not material_name or not obj.data or not hasattr(obj.data, "materials"):
- continue
- material = bpy.data.materials.get(material_name)
- if material is None:
- raise RuntimeError(f"missing material {material_name!r} for {obj.name}")
- obj.data.materials.clear()
- obj.data.materials.append(material)
-
-
-def add_beveled_cube(bpy, name, location, dimensions, clay_material, livery_material, bevel=0.04, rotation=None):
- bpy.ops.mesh.primitive_cube_add(location=location, rotation=rotation or (0.0, 0.0, 0.0))
- obj = bpy.context.object
- obj.name = name
- obj.data.name = name + ".mesh"
- obj.dimensions = dimensions
- bpy.ops.object.transform_apply(location=False, rotation=False, scale=True)
- if bevel:
- modifier = obj.modifiers.new("Structural edge softening", "BEVEL")
- modifier.width = bevel
- modifier.segments = 3
- return assign_variants(obj, clay_material, livery_material)
-
-
-def add_cylinder(bpy, name, location, radius, depth, clay_material, livery_material, *, rotation=(0.0, 0.0, 0.0), vertices=32):
- bpy.ops.mesh.primitive_cylinder_add(
- vertices=vertices,
- radius=radius,
- depth=depth,
- location=location,
- rotation=rotation,
- )
- obj = bpy.context.object
- obj.name = name
- obj.data.name = name + ".mesh"
- return assign_variants(obj, clay_material, livery_material)
-
-
-def add_cone(bpy, name, location, radius1, radius2, depth, clay_material, livery_material, *, rotation=(0.0, 0.0, 0.0)):
- bpy.ops.mesh.primitive_cone_add(
- vertices=24,
- radius1=radius1,
- radius2=radius2,
- depth=depth,
- location=location,
- rotation=rotation,
- )
- obj = bpy.context.object
- obj.name = name
- obj.data.name = name + ".mesh"
- return assign_variants(obj, clay_material, livery_material)
-
-
-def add_torus(bpy, name, location, major_radius, minor_radius, clay_material, livery_material, *, rotation=(0.0, 0.0, 0.0), minor_segments=12):
- bpy.ops.mesh.primitive_torus_add(
- major_radius=major_radius,
- minor_radius=minor_radius,
- major_segments=64,
- minor_segments=minor_segments,
- location=location,
- rotation=rotation,
- )
- obj = bpy.context.object
- obj.name = name
- obj.data.name = name + ".mesh"
- return assign_variants(obj, clay_material, livery_material)
-
-
-def add_between(bpy, name, start, end, radius, clay_material, livery_material, vertices=20):
- start_vec = Vector(start)
- end_vec = Vector(end)
- delta = end_vec - start_vec
- obj = add_cylinder(
- bpy,
- name,
- (start_vec + end_vec) / 2.0,
- radius,
- delta.length,
- clay_material,
- livery_material,
- vertices=vertices,
- )
- obj.rotation_euler = delta.to_track_quat("Z", "Y").to_euler()
- return obj
+def clear_scene(bpy):
+ bpy.ops.wm.read_factory_settings(use_empty=True)
-def add_dish(bpy, name, base_x, center_y, center_z, radius, depth, clay_material, livery_material):
- """Create a shallow open paraboloid whose boresight points along +X."""
- segments = 64
- rings = 9
- vertices = [(base_x, center_y, center_z)]
- for ring in range(1, rings + 1):
- fraction = ring / rings
- ring_radius = radius * fraction
- ring_x = base_x + depth * fraction * fraction
- for segment in range(segments):
- angle = 2.0 * math.pi * segment / segments
- vertices.append(
- (
- ring_x,
- center_y + ring_radius * math.cos(angle),
- center_z + ring_radius * math.sin(angle),
- )
- )
- faces = []
- for segment in range(segments):
- faces.append((0, 1 + segment, 1 + (segment + 1) % segments))
- for ring in range(1, rings):
- first = 1 + (ring - 1) * segments
- next_first = first + segments
- for segment in range(segments):
- current = first + segment
- following = first + (segment + 1) % segments
- next_current = next_first + segment
- next_following = next_first + (segment + 1) % segments
- faces.append((current, next_current, next_following, following))
- mesh = bpy.data.meshes.new(name + ".mesh")
- mesh.from_pydata(vertices, [], faces)
- mesh.update()
+def build_object(bpy, name, material, builder):
+ bm = bmesh.new()
+ builder(bm)
+ bmesh.ops.recalc_face_normals(bm, faces=bm.faces[:])
+ mesh = bpy.data.meshes.new(name)
+ bm.to_mesh(mesh)
+ bm.free()
obj = bpy.data.objects.new(name, mesh)
bpy.context.collection.objects.link(obj)
- assign_variants(obj, clay_material, livery_material)
- solidify = obj.modifiers.new("Reflector thickness", "SOLIDIFY")
- solidify.thickness = 0.012
- return obj
-
-
-
-
-def merge_details(bpy, master, extras):
- """Join detail meshes into the master object so the exported GLB keeps
- exactly the contract object names while carrying real secondary detail."""
- if not extras:
- return master
- for obj in bpy.context.selected_objects:
- obj.select_set(False)
- for extra in extras:
- extra.select_set(True)
- master.select_set(True)
- bpy.context.view_layer.objects.active = master
- bpy.ops.object.join()
- return master
-
-
-def detail_torus(bpy, name, location, major_radius, minor_radius, rotation=(0.0, 0.0, 0.0), minor_segments=10):
- bpy.ops.mesh.primitive_torus_add(
- major_radius=major_radius,
- minor_radius=minor_radius,
- major_segments=48,
- minor_segments=minor_segments,
- location=location,
- rotation=rotation,
- )
- obj = bpy.context.object
- obj.name = name
- return obj
-
-
-def detail_cube(bpy, name, location, dimensions, rotation=(0.0, 0.0, 0.0)):
- bpy.ops.mesh.primitive_cube_add(location=location, rotation=rotation)
- obj = bpy.context.object
- obj.name = name
- obj.dimensions = dimensions
- bpy.ops.object.transform_apply(location=False, rotation=False, scale=True)
- return obj
-
-
-def detail_cylinder(bpy, name, location, radius, depth, rotation=(0.0, 0.0, 0.0), vertices=20):
- bpy.ops.mesh.primitive_cylinder_add(
- vertices=vertices,
- radius=radius,
- depth=depth,
- location=location,
- rotation=rotation,
- )
- obj = bpy.context.object
- obj.name = name
+ obj.data.materials.append(material)
return obj
-def build_details(bpy, object_id, master, materials):
- """Secondary engineering detail per contract object (joined back into the
- master mesh before export). Names below never reach the GLB as objects."""
- clay = None
- details = []
-
- if object_id == "starship.keel.spine":
- # Reinforcement rings along the spine + twin conduit lines + equipment
- # blocks riding the top ridge.
- for index in range(14):
- x = -5.2 + index * 0.72
- details.append(detail_torus(
- bpy,
- f"{master.name}.detail.ring.{index:02d}",
- (x, 0.0, 0.0),
- 0.40,
- 0.028,
- rotation=(0.0, math.pi / 2.0, 0.0),
- ))
- details.append(detail_cylinder(bpy, master.name + ".detail.conduit.0", (-0.8, 0.30, 0.10), 0.035, 9.4, rotation=(0.0, math.pi / 2.0, 0.0)))
- details.append(detail_cylinder(bpy, master.name + ".detail.conduit.1", (-0.8, -0.30, 0.10), 0.035, 9.4, rotation=(0.0, math.pi / 2.0, 0.0)))
- for index, x in enumerate((-3.9, -1.9, 0.6, 2.9)):
- details.append(detail_cube(bpy, f"{master.name}.detail.box.{index:02d}", (x, 0.0, 0.30), (0.5, 0.30, 0.16)))
- elif object_id == "starship.hab.torus":
- # Outer panel ribs every 15 degrees, inner twin conduits, and two
- # comm masts riding the ring.
- for index in range(24):
- angle = 2.0 * math.pi * index / 24.0
- y = HAB_RADIUS * math.cos(angle)
- z = HAB_RADIUS * math.sin(angle)
- rotation = (angle, 0.0, 0.0)
- details.append(detail_cube(
- bpy,
- f"{master.name}.detail.rib.{index:02d}",
- (HAB_X + 0.06, y, z),
- (0.10, 0.08, 0.09),
- rotation=rotation,
- ))
- for offset in (-0.10, 0.10):
- details.append(detail_torus(
- bpy,
- f"{master.name}.detail.conduit.{offset:+.2f}",
- (HAB_X + offset, 0.0, 0.0),
- HAB_RADIUS - HAB_MINOR - 0.04,
- 0.020,
- rotation=(0.0, math.pi / 2.0, 0.0),
- minor_segments=8,
- ))
- details.append(detail_cylinder(bpy, master.name + ".detail.mast.0", (HAB_X - 0.02, HAB_RADIUS + HAB_MINOR, 0.0), 0.02, 0.5, rotation=(0.0, math.pi / 2.0, 0.0)))
- details.append(detail_cylinder(bpy, master.name + ".detail.mast.1", (HAB_X + 0.02, 0.0, -(HAB_RADIUS + HAB_MINOR)), 0.02, 0.5))
- elif object_id == "starship.hub.core":
- for offset, radius in ((-0.74, 0.40), (0.74, 0.40)):
- details.append(detail_cylinder(bpy, f"{master.name}.detail.flange.{offset:+.2f}", (HAB_X + offset, 0.0, 0.0), radius, 0.06, rotation=(0.0, math.pi / 2.0, 0.0)))
- details.append(detail_torus(bpy, master.name + ".detail.bearing", (HAB_X, 0.0, 0.0), 0.34, 0.035, rotation=(0.0, math.pi / 2.0, 0.0), minor_segments=10))
- elif object_id == "starship.core.pressure":
- for index in range(4):
- x = -4.05 + index * 0.42
- details.append(detail_torus(bpy, f"{master.name}.detail.band.{index:02d}", (x, 0.0, 0.0), 0.58, 0.030, rotation=(0.0, math.pi / 2.0, 0.0), minor_segments=10))
- for offset in (-0.92, 0.92):
- details.append(detail_cylinder(bpy, f"{master.name}.detail.flange.{offset:+.2f}", (-3.4 + offset, 0.0, 0.0), 0.60, 0.07, rotation=(0.0, math.pi / 2.0, 0.0)))
- elif object_id == "starship.shadow.collar":
- for index in range(8):
- angle = 2.0 * math.pi * index / 8.0
- y = 0.86 * math.cos(angle)
- z = 0.86 * math.sin(angle)
- details.append(detail_cube(bpy, f"{master.name}.detail.rib.{index:02d}", (-1.35, y, z), (0.34, 0.10, 0.10), rotation=(angle, 0.0, 0.0)))
- elif object_id == "starship.storm.cellar":
- details.append(detail_cube(bpy, master.name + ".detail.doorframe", (-0.55, 0.0, 0.30), (0.66, 0.52, 0.05)))
- details.append(detail_cylinder(bpy, master.name + ".detail.hinge", (-0.55, -0.32, 0.0), 0.035, 0.5))
- elif object_id == "starship.nozzle.magnetic":
- # Coil grooves down the bell + coolant lines hugging the cone.
- for index in range(5):
- fraction = 0.25 + index * 0.14
- x = -6.35 + 2.2 * fraction
- radius = 1.15 - 0.55 * fraction
- details.append(detail_torus(bpy, f"{master.name}.detail.coil.{index:02d}", (x, 0.0, 0.0), radius, 0.034, rotation=(0.0, math.pi / 2.0, 0.0), minor_segments=10))
- for index in range(4):
- angle = 2.0 * math.pi * index / 4.0 + math.pi / 4.0
- y = 0.95 * math.cos(angle)
- z = 0.95 * math.sin(angle)
- details.append(detail_cylinder(bpy, f"{master.name}.detail.coolant.{index:02d}", (-5.35, y, z), 0.028, 2.5, rotation=(0.0, math.pi / 2.0, 0.0)))
- elif object_id == "starship.power.truss":
- for index in range(6):
- t = index / 5.0
- x = -1.2 + t * 5.1
- details.append(detail_cylinder(bpy, f"{master.name}.detail.diagonal.{index:02d}", (x, 0.0, 0.34), 0.022, 0.34, rotation=(0.0, math.pi / 2.0, 0.0), vertices=8))
- details.append(detail_torus(bpy, master.name + ".detail.hoop", (1.6, 0.0, 0.34), 0.40, 0.028, rotation=(math.pi / 2.0, 0.0, 0.0), minor_segments=10))
- elif object_id.startswith("starship.radiator."):
- index = int(object_id.rsplit(".", 1)[-1])
- angle = 2.0 * math.pi * index / 6.0 + math.pi / 6.0
- y = 0.95 * math.cos(angle)
- z = 0.95 * math.sin(angle)
- details.append(detail_cube(bpy, master.name + ".detail.backbone", (-3.6, y, z), (1.5, 0.05, 0.06)))
- for rib in (-0.45, 0.0, 0.45):
- details.append(detail_cube(bpy, f"{master.name}.detail.stiff.{rib:+.2f}", (-3.6 + rib, y, z), (0.05, 0.06, 0.40)))
- elif object_id.startswith("starship.cargo.pod."):
- index = int(object_id.rsplit(".", 1)[-1])
- y = (-0.62, 0.62)[index]
- for offset in (-0.5, 0.0, 0.5):
- details.append(detail_cube(bpy, f"{master.name}.detail.rib.{offset:+.2f}", (0.35 + offset, y, 0.0), (0.05, 0.26, 0.32)))
- details.append(detail_cylinder(bpy, master.name + ".detail.endcap", (0.97, y, 0.0), 0.10, 0.05, rotation=(0.0, math.pi / 2.0, 0.0)))
- elif object_id == "starship.nose.cone":
- details.append(detail_torus(bpy, master.name + ".detail.seam.0", (4.45, 0.0, 0.0), 0.26, 0.02, rotation=(0.0, math.pi / 2.0, 0.0), minor_segments=8))
- details.append(detail_torus(bpy, master.name + ".detail.seam.1", (4.75, 0.0, 0.0), 0.16, 0.018, rotation=(0.0, math.pi / 2.0, 0.0), minor_segments=8))
- elif object_id == "starship.comm.dish":
- # Mounting mast rising from the keel deck + gimbal block under the
- # dish base, so the dish reads as bolted hardware instead of floating.
- details.append(detail_cylinder(bpy, master.name + ".detail.post", (3.40, 0.0, 0.37), 0.045, 0.30, vertices=12))
- details.append(detail_cube(bpy, master.name + ".detail.gimbal", (3.40, 0.0, 0.46), (0.16, 0.16, 0.10)))
- details.append(detail_cube(bpy, master.name + ".detail.baseplate", (3.40, 0.0, 0.24), (0.26, 0.26, 0.05)))
- elif object_id.startswith("starship.spoke."):
- index = int(object_id.rsplit(".", 1)[-1])
- angle = 2.0 * math.pi * index / SPOKE_COUNT
- end_y = HAB_RADIUS * math.cos(angle)
- end_z = HAB_RADIUS * math.sin(angle)
- # Collar at the ring end and a hinge ring at the hub end.
- details.append(detail_torus(bpy, master.name + ".detail.cuff", (HAB_X, end_y * 0.92, end_z * 0.92), 0.16, 0.045, rotation=(angle, 0.0, 0.0), minor_segments=12))
- details.append(detail_torus(bpy, master.name + ".detail.root", (HAB_X + 0.06, 0.0, 0.0), 0.40, 0.045, rotation=(0.0, math.pi / 2.0, 0.0), minor_segments=12))
- else:
- return []
-
- _ = clay
- return details
-
-
-# ---------------------------------------------------------------------------
-# Starship layout (units are display metres, unscaled示意 dimensions).
-#
-# nose +X : forward docking / sensor section
-# -X : reactor + nozzle (the ship accelerates toward -X)
-#
-# habitat : torus in the Y-Z plane at x=+1.2 (far from reactor at x<-3)
-# shadow : water-shadow collar at x=-1.2, storm cellar hugs the keel
-# reactor : fusion pressure vessel x in [-4.2, -2.6], nozzle to x=-5.6
-# ---------------------------------------------------------------------------
-
-HAB_X = 1.2
-HAB_RADIUS = 1.7
-HAB_MINOR = 0.30
-SPOKE_COUNT = 6
+# Geometry constants shared across steps (units: 1 = 2 m).
+# Scale philosophy: few large elements, not many small ones. The hull core
+# is a continuous 16 m-diameter pressure body; everything else hangs off it.
+HULL_P1 = (0.0, -4.0, 0.0)
+HULL_P2 = (0.0, 42.0, 0.0)
+HULL_R = 4.0 # 16 m diameter
+NODE_C = (0.0, 44.6, 0.0)
+CENTRIFUGE_CENTER = (0.0, 6.0, 0.0)
+CENTRIFUGE_R = 14.0 # 56 m ring
+SPINE_P1 = (0.0, -34.0, 0.0)
+SPINE_P2 = (0.0, 20.0, 0.0)
+SPINE_R = 1.2
+HAB_A = ((5.6, 12.0, 0.0), (5.6, 30.0, 0.0), 2.6) # side-mounted lab module
+HAB_B = ((-5.6, 12.0, 0.0), (-5.6, 30.0, 0.0), 2.6) # side-mounted hab module
+TANK_X = 7.6
+TANK_R = 2.5 # 10 m diameter tanks
+REACTOR_C = (0.0, -30.5, 0.0)
+NOZZLE_MOUNT_Y = -40.5
+
+
+# --------------------------------------------------------------- builders
+
+
+def b_hull_core(bm):
+ """Continuous 16 m-diameter pressure hull — the ship's body."""
+ p1, p2 = Vector(HULL_P1), Vector(HULL_P2)
+ r = HULL_R
+ seg = 56
+ cyl(bm, p1, p2, r, seg=seg, cap1=False, cap2=False)
+ dome(bm, p1, (0.0, -1.0, 0.0), r, seg=seg)
+ dome(bm, p2, (0.0, 1.0, 0.0), r, seg=seg)
+ frames = 24
+ for i in range(1, frames):
+ t = i / frames
+ torus(bm, p1.lerp(p2, t), r, 0.115, axis=(0, 1, 0), seg=seg, rseg=7)
+ for k in range(8):
+ a = 2.0 * math.pi * k / 8 + math.pi / 8
+ radial = Vector((math.cos(a), 0.0, math.sin(a)))
+ cyl(bm, p1 + radial * r, p2 + radial * r, 0.10, seg=7)
+ for band_t, count in ((0.18, 22), (0.46, 26), (0.74, 22)):
+ c0 = p1.lerp(p2, band_t)
+ for k in range(count):
+ a = 2.0 * math.pi * k / count
+ radial = Vector((math.cos(a), 0.0, math.sin(a)))
+ wc = c0 + radial * r
+ cyl(bm, wc - radial * 0.12, wc + radial * 0.18, 0.22, seg=12, cap1=False)
+ torus(bm, wc + radial * 0.18, 0.245, 0.05, axis=radial, seg=14, rseg=5)
+ panel_bank(bm, p1 + Vector((0.0, 3.5, 0.0)), p2 - Vector((0.0, 3.5, 0.0)),
+ r + 0.035, cols=72, rows=18, depth=0.05)
+ for (t0, count, seed) in ((0.10, 26, 601), (0.60, 30, 602), (0.90, 22, 603)):
+ base = p1.lerp(p2, t0)
+ equipment_row(bm, base + Vector((0.0, 0.0, r + 0.12)), (0, 1, 0), (0, 0, 1), count, 1.35, 0.14, 0.34, seed=seed)
+ equipment_row(bm, base + Vector((0.0, 0.0, -r - 0.12)), (0, 1, 0), (0, 0, -1), count, 1.35, 0.14, 0.34, seed=seed + 50)
+
+
+def b_spine(bm):
+ """Outrigger trusses carrying tanks, arrays and radiators."""
+ for sx in (-1.0, 1.0):
+ x0 = sx * (HULL_R + 1.6)
+ truss(bm, (x0, -34.0, 0.0), (x0, 20.0, 0.0), 0.85, bays=36, chord_r=0.085, strut_r=0.05,
+ angles=[0.0, 120.0, 240.0] if sx > 0 else [180.0, 60.0, 300.0])
+ for st in (-30.0, -22.0, -14.0, -6.0, 2.0, 10.0):
+ cyl(bm, Vector((sx * HULL_R, st, 0.0)), Vector((x0, st, 0.0)), 0.10, seg=7)
+
+
+def b_node(bm):
+ c = Vector(NODE_C)
+ cyl(bm, c - Vector((0, 1.3, 0)), c + Vector((0, 1.3, 0)), 2.6, seg=24)
+ torus(bm, c - Vector((0, 1.3, 0)), 2.63, 0.09, axis=(0, 1, 0), seg=28, rseg=6)
+ torus(bm, c + Vector((0, 1.3, 0)), 2.63, 0.09, axis=(0, 1, 0), seg=28, rseg=6)
+ for dirv in ((0, 1, 0), (1, 0, 0), (-1, 0, 0), (0, 0, 1), (0, 0, -1)):
+ d = Vector(dirv)
+ x, y, _ = basis(d)
+ base = c + d * 1.25
+ cyl(bm, base, base + d * 0.65, 1.05, seg=18)
+ torus(bm, base + d * 0.65, 1.12, 0.085, axis=d, seg=20, rseg=6)
+ for k in range(10):
+ a = 2.0 * math.pi * k / 10
+ radial = x * math.cos(a) + y * math.sin(a)
+ cyl(bm, base + d * 0.65 + radial * 1.12, base + d * 0.80 + radial * 1.22, 0.055, seg=6)
+
+
+def b_hab_a(bm):
+ p1, p2, r = HAB_A
+ hab_module(bm, p1, p2, r, ribs=9, windows=12, seg=32)
+ panel_bank(bm, p1, p2, r + 0.02, cols=30, rows=18, depth=0.04)
+ equipment_row(bm, (6.6 + r + 0.12, 14.0, 0.0), (0, 1, 0), (1, 0, 0), 12, 1.25, 0.12, 0.28, seed=201)
+ equipment_row(bm, (6.6, 14.5, r + 0.12), (0, 1, 0), (0, 0, 1), 10, 1.4, 0.10, 0.26, seed=203)
+
+
+def b_hab_b(bm):
+ p1, p2, r = HAB_B
+ hab_module(bm, p1, p2, r, ribs=9, windows=12, seg=32)
+ panel_bank(bm, p1, p2, r + 0.02, cols=30, rows=18, depth=0.04)
+ equipment_row(bm, (-6.6 - r - 0.12, 15.0, 0.0), (0, 1, 0), (-1, 0, 0), 12, 1.25, 0.12, 0.28, seed=211)
+ equipment_row(bm, (-6.6, 15.5, r + 0.12), (0, 1, 0), (0, 0, 1), 10, 1.4, 0.10, 0.26, seed=213)
+
+
+def b_whipple(bm):
+ p1, p2 = Vector(HULL_P1), Vector(HULL_P2)
+ whipple_panels(bm, p1 + Vector((0, 3.0, 0)), p2 - Vector((0, 3.0, 0)), HULL_R, rows=14, cols=30)
+
+
+def b_waterwall(bm):
+ """Water tanks ringed around the forward hull as radiation shielding."""
+ for k in range(14):
+ a = 2.0 * math.pi * k / 14
+ radial = Vector((math.cos(a), 0.0, math.sin(a)))
+ p1 = Vector((0.0, 22.0, 0.0)) + radial * (HULL_R + 1.15)
+ p2 = Vector((0.0, 38.0, 0.0)) + radial * (HULL_R + 1.15)
+ pressure_vessel(bm, p1, p2, 1.15, rings=3, seg=16, ports=False)
+ mli_bands(bm, p1, p2, 1.18, bands=7, seg=18, wobble=0.02)
+ for t in (0.25, 0.75):
+ cco = p1.lerp(p2, t)
+ cyl(bm, cco - radial * 1.1, cco - radial * 0.4, 0.08, seg=6)
+
+
+def b_centrifuge(bm):
+ centrifuge_ring(bm, CENTRIFUGE_CENTER, CENTRIFUGE_R, axis=(0, 1, 0), cars=8, seg=72)
+ hub = Vector(CENTRIFUGE_CENTER)
+ cyl(bm, hub - Vector((0, 2.2, 0)), hub + Vector((0, 2.2, 0)), 1.9, seg=24)
+ torus(bm, hub + Vector((0, 2.2, 0)), 1.93, 0.09, axis=(0, 1, 0), seg=28, rseg=6)
+ torus(bm, hub - Vector((0, 2.2, 0)), 1.93, 0.09, axis=(0, 1, 0), seg=28, rseg=6)
+ for k in range(8):
+ a = 2.0 * math.pi * k / 8
+ radial = Vector((math.cos(a), 0.0, math.sin(a)))
+ cyl(bm, hub + radial * 1.9, hub + radial * 3.0, 0.11, seg=7)
+ for k in range(32):
+ a = 2.0 * math.pi * k / 32
+ radial = Vector((math.cos(a), 0.0, math.sin(a)))
+ c = Vector(CENTRIFUGE_CENTER) + radial * (CENTRIFUGE_R - 1.9)
+ cyl(bm, c - Vector((0, 1.1, 0)), c + Vector((0, 1.1, 0)), 0.62, seg=16)
+ torus(bm, c - Vector((0, 1.1, 0)), 0.64, 0.055, axis=(0, 1, 0), seg=16, rseg=5)
+ torus(bm, c + Vector((0, 1.1, 0)), 0.64, 0.055, axis=(0, 1, 0), seg=16, rseg=5)
+ for k in range(28):
+ a = 2.0 * math.pi * k / 28 + 0.22
+ radial = Vector((math.cos(a), 0.0, math.sin(a)))
+ c = Vector(CENTRIFUGE_CENTER) + radial * (CENTRIFUGE_R + 0.4)
+ box(bm, c, 0.38, 0.62, 0.38, radial, Vector((0, 1, 0)), Vector((-math.sin(a), 0.0, math.cos(a))))
+
+
+def b_prop_tank(bm):
+ for sx in (-1.0, 1.0):
+ for yc in (-14.0, -23.0):
+ x0 = sx * TANK_X
+ p1 = (x0, yc - 4.5, 0.0)
+ p2 = (x0, yc + 4.5, 0.0)
+ pressure_vessel(bm, p1, p2, TANK_R, rings=5, seg=30)
+ mli_bands(bm, p1, p2, TANK_R + 0.05, bands=13, seg=30, wobble=0.03)
+ panel_bank(bm, Vector(p1) + Vector((0, 1.4, 0)), Vector(p2) - Vector((0, 1.4, 0)),
+ TANK_R + 0.05, cols=22, rows=16, depth=0.035)
+ for st in (yc - 3.0, yc, yc + 3.0):
+ cyl(bm, Vector((sx * (HULL_R + 1.6), st, 0.0)), Vector((x0, st, 0.0)), 0.16, seg=8)
+ cyl(bm, Vector((sx * (HULL_R + 1.6), st - 0.5, 0.0)), Vector((x0, st, 0.6)), 0.07, seg=6)
+ torus(bm, Vector((x0, st, 0.0)), TANK_R + 0.07, 0.06, axis=(0, 1, 0), seg=24, rseg=5)
+
+
+def b_reactor(bm):
+ base = Vector(REACTOR_C)
+ cyl(bm, base - Vector((0, 1.8, 0)), base + Vector((0, 1.8, 0)), 2.1, seg=26)
+ for yy in (-1.8, 1.8):
+ torus(bm, base + Vector((0, yy, 0)), 2.13, 0.09, axis=(0, 1, 0), seg=28, rseg=6)
+ for k in range(14):
+ a = 2.0 * math.pi * k / 14
+ radial = Vector((math.cos(a), 0.0, math.sin(a)))
+ cyl(bm, base + radial * 2.0 - Vector((0, 1.5, 0)), base + radial * 2.0 + Vector((0, 1.5, 0)), 0.11, seg=6)
+ for k in range(8):
+ a = 2.0 * math.pi * k / 8
+ radial = Vector((math.cos(a), 0.0, math.sin(a)))
+ cyl(bm, base + radial * 2.1, base + radial * 3.1 - Vector((0, 3.4, 0)), 0.10, seg=7)
+
+
+def b_thrust_cone(bm):
+ thrust_cone(bm, (0.0, -33.5, 0.0), (0, -1, 0), 3.6, 2.9, 1.8, struts=14)
+ shadow_shield(bm, (0.0, -35.6, 0.0), (0, -1, 0), 4.4, seg=36, ribs=12)
+
+
+def b_nozzle_cluster(bm):
+ for ex in (-2.4, 2.4):
+ mount = Vector((ex, NOZZLE_MOUNT_Y, 0.0))
+ gimbal(bm, mount, (0, -1, 0), radius=0.55, s=2.0)
+ ntp_engine(bm, mount, (0, -1, 0), scale=2.0)
+
+
+def b_solar(bm):
+ for sx in (-1.0, 1.0):
+ for yy in (24.0, -18.0):
+ root = Vector((sx * (HULL_R + 2.4), yy, 0.0))
+ solar_array(bm, root, (sx, 0, 0), 30.0, 13.0, cells=84, rows=18)
+ d = Vector((sx, 0, 0))
+ x, _, _ = basis(d)
+ for k in range(16):
+ t = (k + 0.5) / 16
+ c = root + d * (30.0 * t)
+ cyl(bm, c - x * 6.5, c + x * 6.5, 0.05, seg=6)
+ for side in (-1.0, 1.0):
+ off = x * 6.5 * side
+ cyl(bm, root + off, root + d * 30.0 + off, 0.07, seg=6)
+ cyl(bm, root, root + Vector((sx * 2.4, 0, 0)), 0.20, seg=10)
+ for k in range(4):
+ a = 2.0 * math.pi * k / 4
+ cyl(bm, Vector((sx * (HULL_R + 0.4), yy, 0.0)),
+ root + Vector((sx * 1.2, 0, 0)) + x * 0.8 * math.cos(a), 0.06, seg=6)
+
+
+def b_radiator(bm):
+ for sx in (-1.0, 1.0):
+ for yy in (-32.0, -12.0, 32.0):
+ p1 = Vector((sx * (HULL_R + 3.4), yy - 6.0, 0.0))
+ p2 = Vector((sx * (HULL_R + 3.4), yy + 6.0, 0.0))
+ fin_radiator(bm, p1, p2, 3.4, fins=64, fin_h=0.55)
+ mid = p1.lerp(p2, 0.5)
+ cyl(bm, Vector((sx * (HULL_R + 0.6), mid.y, 0.0)), mid, 0.11, seg=8)
+ torus(bm, mid, 1.7, 0.06, axis=(1, 0, 0), seg=20, rseg=5)
+
+
+def b_airlock(bm):
+ p1 = Vector((0.0, 16.0, HULL_R - 0.5))
+ p2 = Vector((0.0, 16.0, HULL_R + 4.6))
+ cyl(bm, p1, p2, 1.6, seg=22)
+ for zz in (HULL_R - 0.5, HULL_R + 4.6):
+ torus(bm, Vector((0.0, 16.0, zz)), 1.63, 0.08, axis=(0, 0, 1), seg=24, rseg=6)
+ hatch = Vector((0.0, 16.0, HULL_R + 4.6 + 0.4))
+ cyl(bm, hatch - Vector((0, 0, 0.4)), hatch + Vector((0, 0, 0.25)), 1.0, seg=18)
+ torus(bm, hatch + Vector((0, 0, 0.25)), 1.06, 0.075, axis=(0, 0, 1), seg=20, rseg=6)
+ for k in range(8):
+ a = 2.0 * math.pi * k / 8
+ off = Vector((math.cos(a), math.sin(a), 0.0)) * 1.06
+ cyl(bm, hatch + off + Vector((0, 0, 0.25)), hatch + off + Vector((0, 0, 0.42)), 0.055, seg=6)
+ equipment_row(bm, (1.9, 16.0, HULL_R + 2.4), (0, 0, 1), (1, 0, 0), 8, 0.5, 0.10, 0.22, seed=401)
+
+
+def b_orion(bm):
+ """Orion-class shuttle docked forward — the human-scale reference."""
+ base = Vector((0.0, 47.6, 0.0))
+ cyl(bm, base, base + Vector((0, 5.4, 0)), 1.5, seg=20)
+ for t in (0.3, 0.62):
+ torus(bm, base + Vector((0, 5.4 * t, 0)), 1.53, 0.065, axis=(0, 1, 0), seg=22, rseg=5)
+ cyl(bm, base + Vector((0, 5.4, 0)), base + Vector((0, 7.8, 0)), 1.5, 0.55, seg=20)
+ torus(bm, base + Vector((0, 5.4, 0)), 1.53, 0.08, axis=(0, 1, 0), seg=22, rseg=6)
+ cyl(bm, base + Vector((0, 7.8, 0)), base + Vector((0, 8.5, 0)), 0.55, seg=14)
+ torus(bm, base + Vector((0, 8.5, 0)), 0.65, 0.07, axis=(0, 1, 0), seg=16, rseg=6)
+ for k in range(8):
+ a = 2.0 * math.pi * k / 8
+ radial = Vector((math.cos(a), 0.0, math.sin(a)))
+ c = base + Vector((0, 6.9, 0)) + radial * 1.06
+ cyl(bm, c - radial * 0.05, c + radial * 0.11, 0.13, seg=10, cap1=False)
+ for k in range(4):
+ a = 2.0 * math.pi * k / 4 + math.pi / 4
+ radial = Vector((math.cos(a), 0.0, math.sin(a)))
+ root = base + Vector((0, 5.4, 0)) + radial * 1.5
+ solar_array(bm, root, radial, 4.6, 2.0, cells=22, rows=8)
+ bell(bm, base - Vector((0, 0.6, 0)), (0, -1, 0), 1.5, 0.22, 0.68, seg=22, layers=9)
+
+
+def b_greeble(bm):
+ """Organised machinery, cable looms, nav lights, hatch plates."""
+ p1, p2 = Vector(HULL_P1), Vector(HULL_P2)
+ r = HULL_R
+ for rx, (yy, ang) in ((0, (28.0, 0.9)), (0, (36.0, 2.5)), (0, (10.0, 4.2))):
+ radial = Vector((math.cos(ang), 0.0, math.sin(ang)))
+ base = Vector((0.0, yy, 0.0)) + radial * r
+ cyl(bm, base - radial * 0.05, base + radial * 0.13, 0.55, seg=14)
+ torus(bm, base + radial * 0.13, 0.60, 0.055, axis=radial, seg=16, rseg=5)
+ for sx in (-1.0, 1.0):
+ x0 = sx * (HULL_R + 1.6)
+ for dz in (0.95, -0.95):
+ pipe_run(bm, [(x0, -34.0, dz), (x0, 20.0, dz)], 0.055)
+ for (pos, dirv, rad) in (((-4.0, 40.0, 0.0), (-1, 0, 0), 0.20),
+ ((4.0, 40.0, 0.0), (1, 0, 0), 0.20),
+ ((0.0, 40.0, 4.0), (0, 0, 1), 0.20),
+ ((-4.0, -2.0, 0.0), (-1, 0, 0), 0.16),
+ ((4.0, -2.0, 0.0), (1, 0, 0), 0.16)):
+ d = Vector(dirv)
+ cyl(bm, Vector(pos), Vector(pos) + d * 0.15, rad, seg=10)
+ cyl(bm, Vector(pos) + d * 0.15, Vector(pos) + d * 0.34, rad * 0.7, seg=10)
+ equipment_row(bm, (0.0, 34.0, r + 0.15), (0, 1, 0), (0, 0, 1), 12, 1.0, 0.10, 0.24, seed=501)
+ equipment_row(bm, (0.0, 6.0, -r - 0.15), (0, 1, 0), (0, 0, -1), 14, 1.0, 0.10, 0.24, seed=502)
+
+
+def b_antenna(bm):
+ antenna_dish(bm, (0.0, 30.0, HULL_R + 1.4), (0.0, 0.3, 1.0), 2.6, depth=0.7, ribs=12)
+ antenna_dish(bm, (HULL_R + 1.2, 38.0, 0.0), (1.0, 0.25, 0.1), 1.4, depth=0.4, ribs=9)
+ for (bx, by, bz, ll) in ((0.0, 42.5, 3.0, 2.2), (0.0, 8.0, -3.0, 2.0), (3.0, 41.0, 0.0, 1.6)):
+ cyl(bm, Vector((bx, by, bz)), Vector((bx, by + 0.2, bz + ll)), 0.055, seg=6)
+ torus(bm, Vector((bx, by + 0.2, bz + ll)), 0.4, 0.045, axis=(0, 0, 1), seg=16, rseg=5)
+
+
+BUILDERS = {
+ "hnx02.hull.core": b_hull_core,
+ "hnx02.spine.truss": b_spine,
+ "hnx02.node.forward": b_node,
+ "hnx02.hab.a": b_hab_a,
+ "hnx02.hab.b": b_hab_b,
+ "hnx02.hab.whipple": b_whipple,
+ "hnx02.rad.waterwall": b_waterwall,
+ "hnx02.centrifuge.ring": b_centrifuge,
+ "hnx02.prop.tank": b_prop_tank,
+ "hnx02.reactor.core": b_reactor,
+ "hnx02.thrust.cone": b_thrust_cone,
+ "hnx02.nozzle.cluster": b_nozzle_cluster,
+ "hnx02.power.solar": b_solar,
+ "hnx02.power.radiator": b_radiator,
+ "hnx02.cabin.airlock": b_airlock,
+ "hnx02.shuttle.orion": b_orion,
+ "hnx02.detail.greeble": b_greeble,
+ "hnx02.detail.antenna": b_antenna,
+}
def build_geometry(bpy, materials, step_id):
- """Build every object of the cumulative step, in STEP_OBJECTS order."""
- clay_body = materials["clay_body"]
- clay_structure = materials["clay_structure"]
- clay_dark = materials["clay_dark"]
- clay_white = materials["clay_white"]
- clay_glass = materials["clay_glass"]
- livery_hull = materials["livery_hull"]
- livery_structure = materials["livery_structure"]
- livery_dark = materials["livery_dark"]
- livery_ceramic = materials["livery_ceramic"]
- livery_amber = materials["livery_amber"]
- livery_white = materials["livery_white"]
- livery_glass = materials["livery_glass"]
- livery_nozzle = materials["livery_nozzle"]
-
- def spoke_end(index):
- angle = 2.0 * math.pi * index / SPOKE_COUNT
- return (HAB_X, HAB_RADIUS * math.cos(angle), HAB_RADIUS * math.sin(angle))
-
- objects = {}
+ objects = []
for object_id in STEP_OBJECTS[step_id]:
- if object_id == "starship.keel.spine":
- objects[object_id] = add_beveled_cube(bpy, object_id, (-0.8, 0.0, 0.0), (10.0, 0.46, 0.42), clay_structure, livery_structure, bevel=0.05)
- elif object_id == "starship.nose.cone":
- objects[object_id] = add_cone(bpy, object_id, (4.6, 0.0, 0.0), 0.42, 0.08, 1.0, clay_body, livery_hull, rotation=(0.0, -math.pi / 2.0, 0.0))
- elif object_id == "starship.dock.ring":
- objects[object_id] = add_torus(bpy, object_id, (4.1, 0.0, 0.0), 0.44, 0.06, clay_structure, livery_amber, rotation=(0.0, math.pi / 2.0, 0.0))
- elif object_id == "starship.hab.torus":
- objects[object_id] = add_torus(bpy, object_id, (HAB_X, 0.0, 0.0), HAB_RADIUS, HAB_MINOR, clay_body, livery_hull, rotation=(0.0, math.pi / 2.0, 0.0), minor_segments=18)
- elif object_id == "starship.hub.core":
- objects[object_id] = add_cylinder(bpy, object_id, (HAB_X, 0.0, 0.0), 0.34, 1.4, clay_structure, livery_structure, rotation=(0.0, math.pi / 2.0, 0.0))
- elif object_id.startswith("starship.spoke."):
- index = int(object_id.rsplit(".", 1)[-1])
- end = spoke_end(index)
- objects[object_id] = add_between(bpy, object_id, (HAB_X, 0.0, 0.0), end, 0.11, clay_structure, livery_structure, vertices=16)
- elif object_id == "starship.shadow.collar":
- objects[object_id] = add_torus(bpy, object_id, (-1.35, 0.0, 0.0), 0.78, 0.24, clay_dark, livery_ceramic, rotation=(0.0, math.pi / 2.0, 0.0), minor_segments=16)
- elif object_id == "starship.storm.cellar":
- objects[object_id] = add_beveled_cube(bpy, object_id, (-0.55, 0.0, 0.0), (1.1, 0.62, 0.58), clay_dark, livery_ceramic, bevel=0.06)
- elif object_id == "starship.core.pressure":
- objects[object_id] = add_cylinder(bpy, object_id, (-3.4, 0.0, 0.0), 0.56, 1.7, clay_structure, livery_nozzle, rotation=(0.0, math.pi / 2.0, 0.0))
- elif object_id == "starship.core.shield.plug":
- objects[object_id] = add_cylinder(bpy, object_id, (-2.45, 0.0, 0.0), 0.60, 0.5, clay_dark, livery_ceramic, rotation=(0.0, math.pi / 2.0, 0.0))
- elif object_id == "starship.nozzle.magnetic":
- objects[object_id] = add_cone(bpy, object_id, (-5.35, 0.0, 0.0), 1.15, 0.60, 2.4, clay_structure, livery_nozzle, rotation=(0.0, math.pi / 2.0, 0.0))
- elif object_id.startswith("starship.radiator."):
- index = int(object_id.rsplit(".", 1)[-1])
- angle = 2.0 * math.pi * index / 6.0 + math.pi / 6.0
- y = 0.95 * math.cos(angle)
- z = 0.95 * math.sin(angle)
- objects[object_id] = add_beveled_cube(bpy, object_id, (-3.6, y, z), (1.5, 0.04, 0.42), clay_white, livery_white, bevel=0.02, rotation=(angle, 0.0, 0.0))
- elif object_id == "starship.power.truss":
- objects[object_id] = add_between(bpy, object_id, (-1.2, 0.0, 0.34), (3.9, 0.0, 0.34), 0.09, clay_structure, livery_structure, vertices=14)
- elif object_id == "starship.capacitor.ring":
- objects[object_id] = add_torus(bpy, object_id, (2.6, 0.0, 0.34), 0.42, 0.08, clay_structure, livery_amber, rotation=(math.pi / 2.0, 0.0, 0.0))
- elif object_id == "starship.comm.dish":
- objects[object_id] = add_dish(bpy, object_id, 3.4, 0.0, 0.50, 0.55, 0.10, clay_white, livery_white)
- elif object_id == "starship.comm.feed":
- objects[object_id] = add_cylinder(bpy, object_id, (3.72, 0.0, 0.50), 0.05, 0.09, clay_glass, livery_glass, rotation=(0.0, math.pi / 2.0, 0.0))
- elif object_id.startswith("starship.window."):
- index = int(object_id.rsplit(".", 1)[-1])
- angle = 2.0 * math.pi * index / SPOKE_COUNT + math.pi / 6.0
- y = (HAB_RADIUS + HAB_MINOR * 0.55) * math.cos(angle)
- z = (HAB_RADIUS + HAB_MINOR * 0.55) * math.sin(angle)
- objects[object_id] = add_cylinder(bpy, object_id, (HAB_X + 0.16, y, z), 0.14, 0.10, clay_glass, livery_glass, rotation=(angle, 0.0, 0.0), vertices=18)
- elif object_id == "starship.airlock.ring":
- objects[object_id] = add_torus(bpy, object_id, (4.1, 0.0, 0.0), 0.30, 0.05, clay_structure, livery_amber, rotation=(0.0, math.pi / 2.0, 0.0))
- elif object_id == "starship.airlock.tunnel":
- objects[object_id] = add_cylinder(bpy, object_id, (4.55, 0.0, 0.0), 0.26, 0.9, clay_body, livery_hull, rotation=(0.0, math.pi / 2.0, 0.0))
- elif object_id.startswith("starship.cargo.pod."):
- index = int(object_id.rsplit(".", 1)[-1])
- y = (-0.62, 0.62)[index]
- objects[object_id] = add_beveled_cube(bpy, object_id, (0.35, y, 0.0), (1.2, 0.24, 0.30), clay_body, livery_hull, bevel=0.03)
- else:
- fail("unknown object id: " + object_id, 4)
-
- # Secondary engineering detail is generated per contract object and
- # merged into the master mesh, keeping the GLB object contract intact.
- for object_id in list(STEP_OBJECTS[step_id]):
- master = objects[object_id]
- merge_details(bpy, master, build_details(bpy, object_id, master, materials))
-
- ordered = [objects[object_id] for object_id in STEP_OBJECTS[step_id]]
- for obj, object_id in zip(ordered, STEP_OBJECTS[step_id]):
- if obj.get("object_id") != object_id:
- fail("source object custom property verification failed", 4)
- return ordered
+ material = materials[OBJECT_MATERIAL[object_id]]
+ objects.append(build_object(bpy, object_id, material, BUILDERS[object_id]))
+ return objects
-def add_render_setup(bpy):
- from mathutils import Vector
+# ------------------------------------------------------------ render / io
+
+def add_render_setup(bpy):
scene = bpy.context.scene
- scene.render.resolution_x = 512
- scene.render.resolution_y = 512
+ scene.render.resolution_x = 1280
+ scene.render.resolution_y = 860
scene.render.resolution_percentage = 100
scene.render.image_settings.file_format = "PNG"
if scene.world is None:
- scene.world = bpy.data.worlds.new("ship.world")
- scene.world.color = (0.025, 0.035, 0.045)
+ scene.world = bpy.data.worlds.new("hnx02.world")
+ scene.world.color = (0.02, 0.03, 0.04)
- camera_data = bpy.data.cameras.new("ship.camera")
+ camera_data = bpy.data.cameras.new("hnx02.camera")
camera_data.type = "ORTHO"
- camera_data.ortho_scale = 13.5
- camera = bpy.data.objects.new("ship.camera", camera_data)
+ camera_data.ortho_scale = 118.0
+ camera = bpy.data.objects.new("hnx02.camera", camera_data)
bpy.context.collection.objects.link(camera)
- camera.location = (7.0, -13.5, 7.5)
- camera.rotation_euler = (Vector((-0.5, 0.0, 0.0)) - camera.location).to_track_quat("-Z", "Y").to_euler()
+ camera.location = (52.0, -66.0, 40.0)
+ camera.rotation_euler = (Vector((0.0, 2.0, 0.0)) - camera.location).to_track_quat("-Z", "Y").to_euler()
scene.camera = camera
def add_light(name, location, energy, size, color=(1.0, 1.0, 1.0)):
@@ -703,12 +1057,12 @@ def add_render_setup(bpy):
light = bpy.data.objects.new(name, light_data)
bpy.context.collection.objects.link(light)
light.location = location
- light.rotation_euler = (Vector((-0.5, 0.0, 0.0)) - Vector(location)).to_track_quat("-Z", "Y").to_euler()
+ light.rotation_euler = (Vector((0.0, 2.0, 0.0)) - Vector(location)).to_track_quat("-Z", "Y").to_euler()
return light
- add_light("ship.key", (5.0, -6.0, 9.0), 1400, 6.0, (1.0, 0.98, 0.94))
- add_light("ship.fill", (-4.0, -9.0, 2.0), 420, 8.0, (0.72, 0.82, 1.0))
- add_light("ship.rim", (-7.5, 5.0, -3.0), 900, 3.0, (1.0, 0.86, 0.62))
+ add_light("hnx02.key", (34.0, -30.0, 40.0), 16000, 26.0, (1.0, 0.97, 0.92))
+ add_light("hnx02.fill", (-30.0, -42.0, 10.0), 5200, 30.0, (0.68, 0.79, 1.0))
+ add_light("hnx02.rim", (-36.0, 26.0, -16.0), 9000, 16.0, (1.0, 0.85, 0.58))
def export_glb(bpy, objects, output):
@@ -717,7 +1071,17 @@ def export_glb(bpy, objects, output):
for obj in objects:
obj.select_set(True)
bpy.context.view_layer.objects.active = objects[-1]
- result = bpy.ops.export_scene.gltf(filepath=output, export_format="GLB", use_selection=True, export_apply=True)
+ result = bpy.ops.export_scene.gltf(
+ filepath=output,
+ export_format="GLB",
+ use_selection=True,
+ export_apply=True,
+ export_draco_mesh_compression_enable=True,
+ export_draco_mesh_compression_level=6,
+ export_draco_position_quantization=14,
+ export_draco_normal_quantization=10,
+ export_draco_texcoord_quantization=12,
+ )
if "FINISHED" not in result or not os.path.isfile(output) or os.path.getsize(output) <= 0:
fail("GLB export failed", 4)
@@ -738,11 +1102,9 @@ def verify_reimport(bpy, output, expected):
result = bpy.ops.import_scene.gltf(filepath=output)
if "FINISHED" not in result:
fail("GLB reimport failed", 4)
-
names = [obj.name for obj in bpy.context.scene.objects if obj.type == "MESH"]
if len(names) != len(expected) or set(names) != set(expected):
fail("GLB reimport object IDs did not match", 4)
-
return [object_id for object_id in expected if object_id in names]
@@ -754,61 +1116,40 @@ def main():
parser.add_argument("--result-output", required=True)
args = parser.parse_args()
- if args.step_id not in STEP_OBJECTS:
- fail("unknown step id", 2)
- if bool(args.render_output) and os.path.abspath(args.render_output) == os.path.abspath(args.output):
- fail("render output must differ from GLB output", 2)
-
- try:
- import bpy
- except Exception as exc:
- fail("bpy is unavailable: " + str(exc), 3)
-
- if tuple(bpy.app.version) < (3, 0, 0):
- fail("bpy version is unsupported", 3)
-
- output = os.path.abspath(args.output)
- result_output = os.path.abspath(args.result_output)
- os.makedirs(os.path.dirname(output), exist_ok=True)
-
- try:
- clear_scene(bpy)
- materials = build_materials(bpy)
- objects = build_geometry(bpy, materials, args.step_id)
- apply_material_mode(bpy, materials, "livery" if args.step_id in LIVERY_STEPS else "clay")
- export_glb(bpy, objects, output)
- exported_at = utc_now()
- glb_bytes = os.path.getsize(output)
-
- rendered_at = None
- if args.render_output:
- render_output = os.path.abspath(args.render_output)
- os.makedirs(os.path.dirname(render_output), exist_ok=True)
- add_render_setup(bpy)
- bpy.context.scene.render.filepath = render_output
- result = bpy.ops.render.render(write_still=True)
- if "FINISHED" not in result or not os.path.isfile(render_output) or os.path.getsize(render_output) <= 0:
- fail("PNG render failed", 4)
- rendered_at = utc_now()
-
- verified = verify_reimport(bpy, output, STEP_OBJECTS[args.step_id])
- response = {
- "ok": True,
- "blenderVersion": ".".join(str(part) for part in bpy.app.version),
- "exportedAt": exported_at,
- "objectIds": STEP_OBJECTS[args.step_id],
- "verifiedObjectIds": verified,
- "glbBytes": glb_bytes,
- }
- if args.render_output:
- response["renderPath"] = os.path.abspath(args.render_output)
- response["renderedAt"] = rendered_at
- write_result(result_output, response)
- print("starship result written: " + result_output)
- except SystemExit:
- raise
- except Exception as exc:
- fail("starship failed: " + str(exc), 4)
+ step_id = args.step_id
+ if step_id not in STEP_OBJECTS:
+ fail("unknown step id: " + step_id, 3)
+
+ clear_scene(bpy)
+ materials = build_materials(bpy)
+ objects = build_geometry(bpy, materials, step_id)
+
+ render_path = None
+ if args.render_output:
+ add_render_setup(bpy)
+ scene = bpy.context.scene
+ scene.render.filepath = args.render_output
+ bpy.ops.render.render(write_still=True)
+ if os.path.isfile(args.render_output):
+ render_path = os.path.abspath(args.render_output)
+
+ exported_at = utc_now()
+ export_glb(bpy, objects, args.output)
+ reimported = verify_reimport(bpy, args.output, STEP_OBJECTS[step_id])
+
+ response = {
+ "ok": True,
+ "blenderVersion": ".".join(str(part) for part in bpy.app.version),
+ "exportedAt": exported_at,
+ "objectIds": STEP_OBJECTS[step_id],
+ "verifiedObjectIds": reimported,
+ "glbBytes": os.path.getsize(args.output),
+ }
+ if render_path is not None:
+ response["renderPath"] = render_path
+ response["renderedAt"] = utc_now()
+ write_result(args.result_output, response)
+ print("hnx02 result written: " + args.result_output)
if __name__ == "__main__":