@smarterplan/ngx-smarterplan-core 1.4.21 → 1.4.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -24
- package/esm2022/lib/components/csv-export/csv-export.component.mjs +3 -3
- package/esm2022/lib/components/loader/loader.component.mjs +3 -3
- package/esm2022/lib/components/menu-bar/avatar/avatar.component.mjs +1 -1
- package/esm2022/lib/components/menu-bar/menu-bar.component.mjs +3 -3
- package/esm2022/lib/components/menu-bar/navigation-bar/navigation-bar.component.mjs +3 -3
- package/esm2022/lib/components/modal-switch-visit/modal-switch-visit.component.mjs +3 -3
- package/esm2022/lib/components/search-bar/search-bar.component.mjs +3 -3
- package/esm2022/lib/components/splat-viewer/splat-viewer.component.mjs +361 -0
- package/esm2022/lib/components/support-modal/support-modal.component.mjs +3 -3
- package/esm2022/lib/config.mjs +1 -1
- package/esm2022/lib/helpers.service.mjs +1 -1
- package/esm2022/lib/matterport-extensions/nest-thermostat/CanvasImage.mjs +1 -1
- package/esm2022/lib/matterport-extensions/nest-thermostat/CanvasRenderer.mjs +1 -1
- package/esm2022/lib/matterport-extensions/nest-thermostat/NestThermostat.mjs +1 -1
- package/esm2022/lib/matterport-extensions/nest-thermostat/PlaneRenderer.mjs +1 -1
- package/esm2022/lib/matterport-extensions/scene-component/SceneComponent.mjs +1 -1
- package/esm2022/lib/matterport-extensions/security-camera/SecurityCamera.mjs +22 -22
- package/esm2022/lib/matterport-extensions/tv-player/TvPlayer.mjs +1 -1
- package/esm2022/lib/matterport-extensions/view-frustum-mesh/ViewFrustumMesh.mjs +22 -22
- package/esm2022/lib/mattertagData.mjs +1 -1
- package/esm2022/lib/ngx-smarterplan-core.module.mjs +9 -4
- package/esm2022/lib/ngx-smarterplan-core.service.mjs +1 -1
- package/esm2022/lib/pipes/duration-to-string.pipe.mjs +1 -1
- package/esm2022/lib/pipes/format-date-number-to-digits.pipe.mjs +1 -1
- package/esm2022/lib/pipes/hashtag-from-id.pipe.mjs +1 -1
- package/esm2022/lib/pipes/safe-url.pipe.mjs +1 -1
- package/esm2022/lib/pipes/time-date-to-local-string.pipe.mjs +1 -1
- package/esm2022/lib/pipes/username-from-id.pipe.mjs +1 -1
- package/esm2022/lib/services/amplify-cache.service.mjs +1 -1
- package/esm2022/lib/services/base-tab.service.mjs +1 -1
- package/esm2022/lib/services/baseVisibility.service.mjs +1 -1
- package/esm2022/lib/services/content.service.mjs +1 -1
- package/esm2022/lib/services/filter.service.mjs +1 -1
- package/esm2022/lib/services/intervention.service.mjs +1 -1
- package/esm2022/lib/services/locale.service.mjs +1 -1
- package/esm2022/lib/services/matterport-import.service.mjs +1 -1
- package/esm2022/lib/services/matterport-measurement.service.mjs +1 -1
- package/esm2022/lib/services/matterport-navigation.service.mjs +1 -1
- package/esm2022/lib/services/matterport-pointer.service.mjs +6 -6
- package/esm2022/lib/services/matterport-tag-copy.service.mjs +1 -1
- package/esm2022/lib/services/matterport-tag.service.mjs +12 -7
- package/esm2022/lib/services/matterport.service.mjs +17 -17
- package/esm2022/lib/services/models/affectation.service.mjs +1 -1
- package/esm2022/lib/services/models/base-object.service.mjs +1 -1
- package/esm2022/lib/services/models/capture.service.mjs +1 -1
- package/esm2022/lib/services/models/comment.service.mjs +1 -1
- package/esm2022/lib/services/models/domain.service.mjs +1 -1
- package/esm2022/lib/services/models/equipment.service.mjs +1 -1
- package/esm2022/lib/services/models/event.service.mjs +1 -1
- package/esm2022/lib/services/models/feature.service.mjs +1 -1
- package/esm2022/lib/services/models/hashtag.service.mjs +1 -1
- package/esm2022/lib/services/models/layer.service.mjs +1 -1
- package/esm2022/lib/services/models/measurement.service.mjs +1 -1
- package/esm2022/lib/services/models/mission.service.mjs +1 -1
- package/esm2022/lib/services/models/navigation.service.mjs +1 -1
- package/esm2022/lib/services/models/node.service.mjs +1 -1
- package/esm2022/lib/services/models/object3D.service.mjs +1 -1
- package/esm2022/lib/services/models/operation.service.mjs +1 -1
- package/esm2022/lib/services/models/organisation.service.mjs +1 -1
- package/esm2022/lib/services/models/plan.service.mjs +1 -1
- package/esm2022/lib/services/models/poi.service.mjs +27 -27
- package/esm2022/lib/services/models/profile.service.mjs +1 -1
- package/esm2022/lib/services/models/property.service.mjs +1 -1
- package/esm2022/lib/services/models/space.service.mjs +1 -1
- package/esm2022/lib/services/models/template.service.mjs +1 -1
- package/esm2022/lib/services/models/ticket.service.mjs +1 -1
- package/esm2022/lib/services/models/visit.service.mjs +1 -1
- package/esm2022/lib/services/models/zone.service.mjs +1 -1
- package/esm2022/lib/services/navigator.service.mjs +1 -1
- package/esm2022/lib/services/obj-to-glb.mjs +649 -0
- package/esm2022/lib/services/s3.service.mjs +43 -3
- package/esm2022/lib/services/search.service.mjs +1 -1
- package/esm2022/lib/services/support.service.mjs +1 -1
- package/esm2022/lib/services/sweep-locator.service.mjs +1 -1
- package/esm2022/lib/services/tag.service.mjs +16 -16
- package/esm2022/lib/services/threejs-import.service.mjs +6 -4
- package/esm2022/lib/services/threejs.service.mjs +867 -95
- package/esm2022/lib/services/user.service.mjs +1 -1
- package/esm2022/lib/services/utils/matterport-transform.util.mjs +1 -1
- package/esm2022/lib/services/validators.service.mjs +1 -1
- package/esm2022/lib/services/viewer.service.mjs +189 -7
- package/esm2022/lib/services/zone-drawer.service.mjs +1 -1
- package/esm2022/lib/services/zoneChange.service.mjs +1 -1
- package/esm2022/lib/tokens/matterport-sdk-key.token.mjs +1 -1
- package/esm2022/lib/validators/email.directive.mjs +1 -1
- package/esm2022/lib/validators/no-empty.directive.mjs +1 -1
- package/esm2022/lib/validators/number.directive.mjs +1 -1
- package/esm2022/lib/validators/text.directive.mjs +1 -1
- package/esm2022/public-api.mjs +3 -1
- package/fesm2022/smarterplan-ngx-smarterplan-core.mjs +2239 -233
- package/fesm2022/smarterplan-ngx-smarterplan-core.mjs.map +1 -1
- package/lib/components/splat-viewer/splat-viewer.component.d.ts +98 -0
- package/lib/ngx-smarterplan-core.module.d.ts +7 -6
- package/lib/services/matterport-tag.service.d.ts +5 -5
- package/lib/services/obj-to-glb.d.ts +66 -0
- package/lib/services/s3.service.d.ts +7 -0
- package/lib/services/threejs.service.d.ts +336 -16
- package/lib/services/viewer.service.d.ts +110 -8
- package/package.json +1 -1
- package/public-api.d.ts +2 -0
- package/src/assets/viewers/splat/chunk-BYLI4YBI.js +17037 -0
- package/src/assets/viewers/splat/chunk-LDV6YPMG.js +1 -0
- package/src/assets/viewers/splat/chunk-WZ5Z4TU3.js +105719 -0
- package/src/assets/viewers/splat/convert-4DRLH77X.js +1 -0
- package/src/assets/viewers/splat/convert.html +11 -0
- package/src/assets/viewers/splat/converter-SLBB5MHG.js +1 -0
- package/src/assets/viewers/splat/index.html +90 -0
- package/src/assets/viewers/splat/spz-DEYRea_v-QE6YSGIB.js +5292 -0
- package/src/assets/viewers/splat/viewer-W4X3FWQJ.js +2704 -0
- package/src/assets/viewers/splat/viewer.css +1096 -0
- package/src/assets/viewers/splat/webp.wasm +0 -0
- package/src/assets/viewers/splat/worker.mjs +1383 -0
- package/src/assets/vs-app.json +67 -67
|
@@ -0,0 +1,1383 @@
|
|
|
1
|
+
async function Module(moduleArg={}){var Module=moduleArg;var ENVIRONMENT_IS_WEB=!!globalThis.window;var ENVIRONMENT_IS_WORKER=!!globalThis.WorkerGlobalScope;var ENVIRONMENT_IS_NODE=globalThis.process?.versions?.node&&globalThis.process?.type!="renderer";if(ENVIRONMENT_IS_NODE){const{createRequire}=await import('node:module');var require=createRequire(import.meta.url);}var _scriptName=import.meta.url;var scriptDirectory="";function locateFile(path){if(Module["locateFile"]){return Module["locateFile"](path,scriptDirectory)}return scriptDirectory+path}var readAsync,readBinary;if(ENVIRONMENT_IS_NODE){var fs=require("node:fs");if(_scriptName.startsWith("file:")){scriptDirectory=require("node:path").dirname(require("node:url").fileURLToPath(_scriptName))+"/";}readBinary=filename=>{filename=isFileURI(filename)?new URL(filename):filename;var ret=fs.readFileSync(filename);return ret};readAsync=async(filename,binary=true)=>{filename=isFileURI(filename)?new URL(filename):filename;var ret=fs.readFileSync(filename,binary?undefined:"utf8");return ret};if(process.argv.length>1){process.argv[1].replace(/\\/g,"/");}process.argv.slice(2);}else if(ENVIRONMENT_IS_WEB||ENVIRONMENT_IS_WORKER){try{scriptDirectory=new URL(".",_scriptName).href;}catch{}{if(ENVIRONMENT_IS_WORKER){readBinary=url=>{var xhr=new XMLHttpRequest;xhr.open("GET",url,false);xhr.responseType="arraybuffer";xhr.send(null);return new Uint8Array(xhr.response)};}readAsync=async url=>{var response=await fetch(url,{credentials:"same-origin"});if(response.ok){return response.arrayBuffer()}throw new Error(response.status+" : "+response.url)};}}else;console.log.bind(console);var err=console.error.bind(console);var wasmBinary;var ABORT=false;var isFileURI=filename=>filename.startsWith("file://");function updateMemoryViews(){var b=wasmMemory.buffer;HEAP8=new Int8Array(b);Module["HEAPU8"]=HEAPU8=new Uint8Array(b);Module["HEAPU32"]=new Uint32Array(b);new BigInt64Array(b);new BigUint64Array(b);}function preRun(){if(Module["preRun"]){if(typeof Module["preRun"]=="function")Module["preRun"]=[Module["preRun"]];while(Module["preRun"].length){addOnPreRun(Module["preRun"].shift());}}callRuntimeCallbacks(onPreRuns);}function initRuntime(){wasmExports["c"]();}function postRun(){if(Module["postRun"]){if(typeof Module["postRun"]=="function")Module["postRun"]=[Module["postRun"]];while(Module["postRun"].length){addOnPostRun(Module["postRun"].shift());}}callRuntimeCallbacks(onPostRuns);}function abort(what){Module["onAbort"]?.(what);what=`Aborted(${what})`;err(what);ABORT=true;what+=". Build with -sASSERTIONS for more info.";var e=new WebAssembly.RuntimeError(what);throw e}var wasmBinaryFile;function findWasmBinary(){if(Module["locateFile"]){return locateFile("webp.wasm")}return new URL("webp.wasm",import.meta.url).href}function getBinarySync(file){if(file==wasmBinaryFile&&wasmBinary){return new Uint8Array(wasmBinary)}if(readBinary){return readBinary(file)}throw "both async and sync fetching of the wasm failed"}async function getWasmBinary(binaryFile){if(!wasmBinary){try{var response=await readAsync(binaryFile);return new Uint8Array(response)}catch{}}return getBinarySync(binaryFile)}async function instantiateArrayBuffer(binaryFile,imports){try{var binary=await getWasmBinary(binaryFile);var instance=await WebAssembly.instantiate(binary,imports);return instance}catch(reason){err(`failed to asynchronously prepare wasm: ${reason}`);abort(reason);}}async function instantiateAsync(binary,binaryFile,imports){if(!binary&&!ENVIRONMENT_IS_NODE){try{var response=fetch(binaryFile,{credentials:"same-origin"});var instantiationResult=await WebAssembly.instantiateStreaming(response,imports);return instantiationResult}catch(reason){err(`wasm streaming compile failed: ${reason}`);err("falling back to ArrayBuffer instantiation");}}return instantiateArrayBuffer(binaryFile,imports)}function getWasmImports(){var imports={a:wasmImports};return imports}async function createWasm(){function receiveInstance(instance,module){wasmExports=instance.exports;assignWasmExports(wasmExports);updateMemoryViews();return wasmExports}function receiveInstantiationResult(result){return receiveInstance(result["instance"])}var info=getWasmImports();if(Module["instantiateWasm"]){return new Promise((resolve,reject)=>{Module["instantiateWasm"](info,(inst,mod)=>{resolve(receiveInstance(inst));});})}wasmBinaryFile??=findWasmBinary();var result=await instantiateAsync(wasmBinary,wasmBinaryFile,info);var exports=receiveInstantiationResult(result);return exports}var HEAP8;var HEAPU8;var callRuntimeCallbacks=callbacks=>{while(callbacks.length>0){callbacks.shift()(Module);}};var onPostRuns=[];var addOnPostRun=cb=>onPostRuns.push(cb);var onPreRuns=[];var addOnPreRun=cb=>onPreRuns.push(cb);var stackRestore=val=>__emscripten_stack_restore(val);var stackSave=()=>_emscripten_stack_get_current();var getHeapMax=()=>2147483648;var alignMemory=(size,alignment)=>Math.ceil(size/alignment)*alignment;var growMemory=size=>{var oldHeapSize=wasmMemory.buffer.byteLength;var pages=(size-oldHeapSize+65535)/65536|0;try{wasmMemory.grow(pages);updateMemoryViews();return 1}catch(e){}};var _emscripten_resize_heap=requestedSize=>{var oldSize=HEAPU8.length;requestedSize>>>=0;var maxHeapSize=getHeapMax();if(requestedSize>maxHeapSize){return false}for(var cutDown=1;cutDown<=4;cutDown*=2){var overGrownHeapSize=oldSize*(1+.2/cutDown);overGrownHeapSize=Math.min(overGrownHeapSize,requestedSize+100663296);var newSize=Math.min(maxHeapSize,alignMemory(Math.max(requestedSize,overGrownHeapSize),65536));var replacement=growMemory(newSize);if(replacement){return true}}return false};var getCFunc=ident=>{var func=Module["_"+ident];return func};var writeArrayToMemory=(array,buffer)=>{HEAP8.set(array,buffer);};var lengthBytesUTF8=str=>{var len=0;for(var i=0;i<str.length;++i){var c=str.charCodeAt(i);if(c<=127){len++;}else if(c<=2047){len+=2;}else if(c>=55296&&c<=57343){len+=4;++i;}else {len+=3;}}return len};var stringToUTF8Array=(str,heap,outIdx,maxBytesToWrite)=>{if(!(maxBytesToWrite>0))return 0;var startIdx=outIdx;var endIdx=outIdx+maxBytesToWrite-1;for(var i=0;i<str.length;++i){var u=str.codePointAt(i);if(u<=127){if(outIdx>=endIdx)break;heap[outIdx++]=u;}else if(u<=2047){if(outIdx+1>=endIdx)break;heap[outIdx++]=192|u>>6;heap[outIdx++]=128|u&63;}else if(u<=65535){if(outIdx+2>=endIdx)break;heap[outIdx++]=224|u>>12;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63;}else {if(outIdx+3>=endIdx)break;heap[outIdx++]=240|u>>18;heap[outIdx++]=128|u>>12&63;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63;i++;}}heap[outIdx]=0;return outIdx-startIdx};var stringToUTF8=(str,outPtr,maxBytesToWrite)=>stringToUTF8Array(str,HEAPU8,outPtr,maxBytesToWrite);var stackAlloc=sz=>__emscripten_stack_alloc(sz);var stringToUTF8OnStack=str=>{var size=lengthBytesUTF8(str)+1;var ret=stackAlloc(size);stringToUTF8(str,ret,size);return ret};var UTF8Decoder=globalThis.TextDecoder&&new TextDecoder;var findStringEnd=(heapOrArray,idx,maxBytesToRead,ignoreNul)=>{var maxIdx=idx+maxBytesToRead;while(heapOrArray[idx]&&!(idx>=maxIdx))++idx;return idx};var UTF8ArrayToString=(heapOrArray,idx=0,maxBytesToRead,ignoreNul)=>{var endPtr=findStringEnd(heapOrArray,idx,maxBytesToRead);if(endPtr-idx>16&&heapOrArray.buffer&&UTF8Decoder){return UTF8Decoder.decode(heapOrArray.subarray(idx,endPtr))}var str="";while(idx<endPtr){var u0=heapOrArray[idx++];if(!(u0&128)){str+=String.fromCharCode(u0);continue}var u1=heapOrArray[idx++]&63;if((u0&224)==192){str+=String.fromCharCode((u0&31)<<6|u1);continue}var u2=heapOrArray[idx++]&63;if((u0&240)==224){u0=(u0&15)<<12|u1<<6|u2;}else {u0=(u0&7)<<18|u1<<12|u2<<6|heapOrArray[idx++]&63;}if(u0<65536){str+=String.fromCharCode(u0);}else {var ch=u0-65536;str+=String.fromCharCode(55296|ch>>10,56320|ch&1023);}}return str};var UTF8ToString=(ptr,maxBytesToRead,ignoreNul)=>ptr?UTF8ArrayToString(HEAPU8,ptr,maxBytesToRead):"";var ccall=(ident,returnType,argTypes,args,opts)=>{var toC={string:str=>{var ret=0;if(str!==null&&str!==undefined&&str!==0){ret=stringToUTF8OnStack(str);}return ret},array:arr=>{var ret=stackAlloc(arr.length);writeArrayToMemory(arr,ret);return ret}};function convertReturnValue(ret){if(returnType==="string"){return UTF8ToString(ret)}if(returnType==="boolean")return Boolean(ret);return ret}var func=getCFunc(ident);var cArgs=[];var stack=0;if(args){for(var i=0;i<args.length;i++){var converter=toC[argTypes[i]];if(converter){if(stack===0)stack=stackSave();cArgs[i]=converter(args[i]);}else {cArgs[i]=args[i];}}}var ret=func(...cArgs);function onDone(ret){if(stack!==0)stackRestore(stack);return convertReturnValue(ret)}ret=onDone(ret);return ret};var cwrap=(ident,returnType,argTypes,opts)=>{var numericArgs=!argTypes||argTypes.every(type=>type==="number"||type==="boolean");var numericRet=returnType!=="string";if(numericRet&&numericArgs&&!opts){return getCFunc(ident)}return (...args)=>ccall(ident,returnType,argTypes,args)};{if(Module["noExitRuntime"])Module["noExitRuntime"];if(Module["print"])Module["print"];if(Module["printErr"])err=Module["printErr"];if(Module["wasmBinary"])wasmBinary=Module["wasmBinary"];if(Module["arguments"])Module["arguments"];if(Module["thisProgram"])Module["thisProgram"];if(Module["preInit"]){if(typeof Module["preInit"]=="function")Module["preInit"]=[Module["preInit"]];while(Module["preInit"].length>0){Module["preInit"].shift()();}}}Module["cwrap"]=cwrap;var __emscripten_stack_restore,__emscripten_stack_alloc,_emscripten_stack_get_current,wasmMemory;function assignWasmExports(wasmExports){Module["_webp_encode_rgba"]=wasmExports["d"];Module["_webp_encode_lossless_rgba"]=wasmExports["e"];Module["_webp_encode_lossless_rgba_level"]=wasmExports["f"];Module["_webp_decode_rgba"]=wasmExports["g"];Module["_webp_free"]=wasmExports["h"];Module["_malloc"]=wasmExports["i"];Module["_free"]=wasmExports["j"];__emscripten_stack_restore=wasmExports["k"];__emscripten_stack_alloc=wasmExports["l"];_emscripten_stack_get_current=wasmExports["m"];wasmMemory=wasmExports["b"];wasmExports["__indirect_function_table"];}var wasmImports={a:_emscripten_resize_heap};async function run(){preRun();var setStatus=Module["setStatus"];if(setStatus){setStatus("Running...");await new Promise(resolve=>setTimeout(resolve,1));setTimeout(setStatus,1,"");}if(ABORT)return;initRuntime();Module["onRuntimeInitialized"]?.();postRun();}var wasmExports;wasmExports=await createWasm();await run();
|
|
2
|
+
return Module}
|
|
3
|
+
|
|
4
|
+
class WebPCodec {
|
|
5
|
+
/**
|
|
6
|
+
* URL to the webp.wasm file. Set this before any SOG read/write operations
|
|
7
|
+
* in browser environments where the default path resolution doesn't work.
|
|
8
|
+
* Must be set before the first `create()` call: the compiled module is
|
|
9
|
+
* cached, so later changes have no effect.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* import { WebPCodec } from '@playcanvas/splat-transform';
|
|
13
|
+
* import wasmUrl from '@playcanvas/splat-transform/lib/webp.wasm?url';
|
|
14
|
+
* WebPCodec.wasmUrl = wasmUrl;
|
|
15
|
+
*/
|
|
16
|
+
static wasmUrl = null;
|
|
17
|
+
static modulePromise = null;
|
|
18
|
+
Module;
|
|
19
|
+
/**
|
|
20
|
+
* The effective webp.wasm location to hand to worker threads (which can't
|
|
21
|
+
* resolve it from their own module URL). Returns `wasmUrl` verbatim when
|
|
22
|
+
* set - exactly the value `locateFile` uses, so a Windows file path or a
|
|
23
|
+
* URL both pass through unchanged - otherwise the default resolution
|
|
24
|
+
* relative to this module.
|
|
25
|
+
*
|
|
26
|
+
* @returns The configured `wasmUrl`, or the default webp.wasm URL.
|
|
27
|
+
* @ignore
|
|
28
|
+
*/
|
|
29
|
+
static resolveWasmUrl() {
|
|
30
|
+
return WebPCodec.wasmUrl ?? new URL('../lib/webp.wasm', import.meta.url).toString();
|
|
31
|
+
}
|
|
32
|
+
static async create() {
|
|
33
|
+
// Compile/instantiate the wasm module once and share it across all
|
|
34
|
+
// instances; per-call instantiation pays a fresh Emscripten heap each
|
|
35
|
+
// time (readers like readLcc2 call create() once per chunk). Memoize
|
|
36
|
+
// the promise so concurrent first calls share a single instantiation,
|
|
37
|
+
// but reset on rejection so a failed load (e.g. wasmUrl set late in a
|
|
38
|
+
// browser) can be retried.
|
|
39
|
+
if (!WebPCodec.modulePromise) {
|
|
40
|
+
const promise = Module({
|
|
41
|
+
locateFile: (path) => {
|
|
42
|
+
if (path.endsWith('.wasm') && WebPCodec.wasmUrl) {
|
|
43
|
+
return WebPCodec.wasmUrl;
|
|
44
|
+
}
|
|
45
|
+
return new URL(`../lib/${path}`, import.meta.url).toString();
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
promise.catch(() => {
|
|
49
|
+
if (WebPCodec.modulePromise === promise) {
|
|
50
|
+
WebPCodec.modulePromise = null;
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
WebPCodec.modulePromise = promise;
|
|
54
|
+
}
|
|
55
|
+
const instance = new WebPCodec();
|
|
56
|
+
instance.Module = await WebPCodec.modulePromise;
|
|
57
|
+
return instance;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Encode RGBA pixels as a lossless WebP.
|
|
61
|
+
*
|
|
62
|
+
* @param rgba - Pixels, `height` rows of `stride` bytes.
|
|
63
|
+
* @param width - Image width.
|
|
64
|
+
* @param height - Image height.
|
|
65
|
+
* @param stride - Row stride in bytes. Default: `width * 4`.
|
|
66
|
+
* @param effort - Compression effort 0–9. Higher levels spend more time
|
|
67
|
+
* trying to reduce file size. Omit to use the default libwebp lossless encoder.
|
|
68
|
+
* Explicit levels also preserve RGB values in fully transparent pixels;
|
|
69
|
+
* the default WebP encoder may discard those invisible values.
|
|
70
|
+
* @returns The encoded WebP bytes.
|
|
71
|
+
*/
|
|
72
|
+
encodeLosslessRGBA(rgba, width, height, stride = width * 4, effort) {
|
|
73
|
+
const { Module } = this;
|
|
74
|
+
if (effort !== undefined && (!Number.isInteger(effort) || effort < 0 || effort > 9)) {
|
|
75
|
+
throw new Error(`WebP lossless effort must be an integer in [0, 9], got ${effort}`);
|
|
76
|
+
}
|
|
77
|
+
const inPtr = Module._malloc(rgba.length);
|
|
78
|
+
const outPtrPtr = Module._malloc(4);
|
|
79
|
+
const outSizePtr = Module._malloc(4);
|
|
80
|
+
Module.HEAPU8.set(rgba, inPtr);
|
|
81
|
+
const ok = effort === undefined
|
|
82
|
+
? Module._webp_encode_lossless_rgba(inPtr, width, height, stride, outPtrPtr, outSizePtr)
|
|
83
|
+
: Module._webp_encode_lossless_rgba_level(inPtr, width, height, stride, effort, outPtrPtr, outSizePtr);
|
|
84
|
+
if (!ok) {
|
|
85
|
+
throw new Error('WebP lossless encode failed');
|
|
86
|
+
}
|
|
87
|
+
const outPtr = Module.HEAPU32[outPtrPtr >> 2];
|
|
88
|
+
const outSize = Module.HEAPU32[outSizePtr >> 2];
|
|
89
|
+
const bytes = Module.HEAPU8.slice(outPtr, outPtr + outSize);
|
|
90
|
+
Module._webp_free(outPtr);
|
|
91
|
+
Module._free(inPtr);
|
|
92
|
+
Module._free(outPtrPtr);
|
|
93
|
+
Module._free(outSizePtr);
|
|
94
|
+
return bytes;
|
|
95
|
+
}
|
|
96
|
+
decodeRGBA(webp) {
|
|
97
|
+
const { Module } = this;
|
|
98
|
+
const input = webp;
|
|
99
|
+
const inPtr = Module._malloc(input.length);
|
|
100
|
+
const outPtrPtr = Module._malloc(4);
|
|
101
|
+
const widthPtr = Module._malloc(4);
|
|
102
|
+
const heightPtr = Module._malloc(4);
|
|
103
|
+
Module.HEAPU8.set(input, inPtr);
|
|
104
|
+
const ok = Module._webp_decode_rgba(inPtr, input.length, outPtrPtr, widthPtr, heightPtr);
|
|
105
|
+
if (!ok) {
|
|
106
|
+
Module._free(inPtr);
|
|
107
|
+
Module._free(outPtrPtr);
|
|
108
|
+
Module._free(widthPtr);
|
|
109
|
+
Module._free(heightPtr);
|
|
110
|
+
throw new Error('WebP decode failed');
|
|
111
|
+
}
|
|
112
|
+
const outPtr = Module.HEAPU32[outPtrPtr >> 2];
|
|
113
|
+
const width = Module.HEAPU32[widthPtr >> 2];
|
|
114
|
+
const height = Module.HEAPU32[heightPtr >> 2];
|
|
115
|
+
const size = width * height * 4;
|
|
116
|
+
const bytes = Module.HEAPU8.slice(outPtr, outPtr + size);
|
|
117
|
+
Module._webp_free(outPtr);
|
|
118
|
+
Module._free(inPtr);
|
|
119
|
+
Module._free(outPtrPtr);
|
|
120
|
+
Module._free(widthPtr);
|
|
121
|
+
Module._free(heightPtr);
|
|
122
|
+
return { rgba: bytes, width, height };
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Marks an unfilled neighbour slot (fewer than k non-self points available). */
|
|
127
|
+
const KNN_SENTINEL$1 = 0xffffffff;
|
|
128
|
+
/**
|
|
129
|
+
* Exact k-nearest-neighbours of query points against a forest of flat KD
|
|
130
|
+
* subtrees (each part's `nodeSplatIdx` holds GLOBAL splat ids; together the
|
|
131
|
+
* parts cover the whole scene).
|
|
132
|
+
*
|
|
133
|
+
* The top-K state carries across parts: a part whose AABB is farther than
|
|
134
|
+
* the carried worst is skipped outright (the near-path descent is otherwise
|
|
135
|
+
* unconditional, so without this cull every part costs a full descent per
|
|
136
|
+
* query); within a traversed part the standard "skip the far subtree" DFS
|
|
137
|
+
* runs with the accumulated bound. The final top-K equals a single tree's
|
|
138
|
+
* over the union — exact by construction, no halos, no verification.
|
|
139
|
+
*
|
|
140
|
+
* Engine-free (imported by worker tasks); mirrors the GpuKnn WGSL traversal.
|
|
141
|
+
*
|
|
142
|
+
* @param parts - The forest (global splat ids in `nodeSplatIdx`).
|
|
143
|
+
* @param queryPos - Interleaved xyz query positions.
|
|
144
|
+
* @param queryIds - Global splat id per query (self-exclusion).
|
|
145
|
+
* @param count - Query count.
|
|
146
|
+
* @param k - Neighbours per query.
|
|
147
|
+
* @param out - Destination, `count * k` global ids (UNSORTED within a row),
|
|
148
|
+
* {@link KNN_SENTINEL} filling surplus slots.
|
|
149
|
+
*/
|
|
150
|
+
const knnForestQuery = (parts, queryPos, queryIds, count, k, out) => {
|
|
151
|
+
const topDist = new Float32Array(k);
|
|
152
|
+
const topIdx = new Uint32Array(k);
|
|
153
|
+
const stack = new Uint32Array(48);
|
|
154
|
+
for (let q = 0; q < count; q++) {
|
|
155
|
+
const qx = queryPos[q * 3];
|
|
156
|
+
const qy = queryPos[q * 3 + 1];
|
|
157
|
+
const qz = queryPos[q * 3 + 2];
|
|
158
|
+
const qid = queryIds[q];
|
|
159
|
+
topDist.fill(Infinity);
|
|
160
|
+
topIdx.fill(KNN_SENTINEL$1);
|
|
161
|
+
let worst = Infinity;
|
|
162
|
+
let worstIdx = 0;
|
|
163
|
+
for (const part of parts) {
|
|
164
|
+
const { nodeSplatIdx, nodePositions, nodeChildren, aabb } = part;
|
|
165
|
+
// Part-entry cull: skip when the AABB cannot improve the top-K.
|
|
166
|
+
const ddx = Math.max(aabb[0] - qx, qx - aabb[3], 0);
|
|
167
|
+
const ddy = Math.max(aabb[1] - qy, qy - aabb[4], 0);
|
|
168
|
+
const ddz = Math.max(aabb[2] - qz, qz - aabb[5], 0);
|
|
169
|
+
if (ddx * ddx + ddy * ddy + ddz * ddz >= worst)
|
|
170
|
+
continue;
|
|
171
|
+
// Stack packs axis (top 2 bits) with the node index, mirroring
|
|
172
|
+
// the GPU kernel: axis cycles x→y→z with depth.
|
|
173
|
+
let sp = 0;
|
|
174
|
+
stack[sp++] = part.rootIdx;
|
|
175
|
+
while (sp > 0) {
|
|
176
|
+
const packed = stack[--sp];
|
|
177
|
+
const nodeIdx = packed & 0x3fffffff;
|
|
178
|
+
const axis = packed >>> 30;
|
|
179
|
+
const np = nodeIdx * 3;
|
|
180
|
+
const nx = nodePositions[np];
|
|
181
|
+
const ny = nodePositions[np + 1];
|
|
182
|
+
const nz = nodePositions[np + 2];
|
|
183
|
+
const splatId = nodeSplatIdx[nodeIdx];
|
|
184
|
+
if (splatId !== qid) {
|
|
185
|
+
const dx = nx - qx, dy = ny - qy, dz = nz - qz;
|
|
186
|
+
const d2 = dx * dx + dy * dy + dz * dz;
|
|
187
|
+
if (d2 < worst) {
|
|
188
|
+
topDist[worstIdx] = d2;
|
|
189
|
+
topIdx[worstIdx] = splatId;
|
|
190
|
+
let w = topDist[0];
|
|
191
|
+
let wi = 0;
|
|
192
|
+
for (let i = 1; i < k; i++) {
|
|
193
|
+
if (topDist[i] > w) {
|
|
194
|
+
w = topDist[i];
|
|
195
|
+
wi = i;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
worst = w;
|
|
199
|
+
worstIdx = wi;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
const qAxisVal = axis === 0 ? qx : axis === 1 ? qy : qz;
|
|
203
|
+
const nAxisVal = axis === 0 ? nx : axis === 1 ? ny : nz;
|
|
204
|
+
const delta = qAxisVal - nAxisVal;
|
|
205
|
+
const nextAxis = axis + 1 >= 3 ? 0 : axis + 1;
|
|
206
|
+
const nextAxisPacked = nextAxis << 30;
|
|
207
|
+
const nc = nodeIdx * 2;
|
|
208
|
+
const left = nodeChildren[nc];
|
|
209
|
+
const right = nodeChildren[nc + 1];
|
|
210
|
+
const near = delta < 0 ? left : right;
|
|
211
|
+
const far = delta < 0 ? right : left;
|
|
212
|
+
if (far !== KNN_SENTINEL$1 && delta * delta < worst) {
|
|
213
|
+
stack[sp++] = far | nextAxisPacked;
|
|
214
|
+
}
|
|
215
|
+
if (near !== KNN_SENTINEL$1) {
|
|
216
|
+
stack[sp++] = near | nextAxisPacked;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
out.set(topIdx, q * k);
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Merge/cost math for chunk-native decimation, generalized to n-ary groups.
|
|
226
|
+
*
|
|
227
|
+
* Engine-free: this module is imported by worker tasks, so it must not import
|
|
228
|
+
* DataTable or playcanvas (see the note at the top of workers/tasks.ts).
|
|
229
|
+
*
|
|
230
|
+
* The n = 2 path of {@link mergeGroup} is arithmetic-identical to the legacy
|
|
231
|
+
* `momentMatch` in the pre-3.0 `data-table/decimate.ts` (area·α weighted,
|
|
232
|
+
* law-of-total-variance covariance, mass-conserving opacity capped at 1) —
|
|
233
|
+
* enforced by test/moment-match.test.mjs against a verbatim reference copy.
|
|
234
|
+
*/
|
|
235
|
+
/** Covariance diagonal regularizer, matching legacy EPS_COV. */
|
|
236
|
+
const EPS_COV = 1e-8;
|
|
237
|
+
// ---------- sigmoid / logit ----------
|
|
238
|
+
const sigmoid = (x) => 1 / (1 + Math.exp(-x));
|
|
239
|
+
const logit = (p) => {
|
|
240
|
+
p = Math.max(1e-7, Math.min(1 - 1e-7, p));
|
|
241
|
+
return Math.log(p / (1 - p));
|
|
242
|
+
};
|
|
243
|
+
const quatToRotmat = (qw, qx, qy, qz, out, o) => {
|
|
244
|
+
const xx = qx * qx, yy = qy * qy, zz = qz * qz;
|
|
245
|
+
const wx = qw * qx, wy = qw * qy, wz = qw * qz;
|
|
246
|
+
const xy = qx * qy, xz = qx * qz, yz = qy * qz;
|
|
247
|
+
out[o] = 1 - 2 * (yy + zz);
|
|
248
|
+
out[o + 1] = 2 * (xy - wz);
|
|
249
|
+
out[o + 2] = 2 * (xz + wy);
|
|
250
|
+
out[o + 3] = 2 * (xy + wz);
|
|
251
|
+
out[o + 4] = 1 - 2 * (xx + zz);
|
|
252
|
+
out[o + 5] = 2 * (yz - wx);
|
|
253
|
+
out[o + 6] = 2 * (xz - wy);
|
|
254
|
+
out[o + 7] = 2 * (yz + wx);
|
|
255
|
+
out[o + 8] = 1 - 2 * (xx + yy);
|
|
256
|
+
};
|
|
257
|
+
const sigmaFromRotVar = (R, r, vx, vy, vz, out, o) => {
|
|
258
|
+
const r00 = R[r], r01 = R[r + 1], r02 = R[r + 2];
|
|
259
|
+
const r10 = R[r + 3], r11 = R[r + 4], r12 = R[r + 5];
|
|
260
|
+
const r20 = R[r + 6], r21 = R[r + 7], r22 = R[r + 8];
|
|
261
|
+
out[o] = r00 * r00 * vx + r01 * r01 * vy + r02 * r02 * vz;
|
|
262
|
+
out[o + 1] = r00 * r10 * vx + r01 * r11 * vy + r02 * r12 * vz;
|
|
263
|
+
out[o + 2] = r00 * r20 * vx + r01 * r21 * vy + r02 * r22 * vz;
|
|
264
|
+
out[o + 3] = out[o + 1];
|
|
265
|
+
out[o + 4] = r10 * r10 * vx + r11 * r11 * vy + r12 * r12 * vz;
|
|
266
|
+
out[o + 5] = r10 * r20 * vx + r11 * r21 * vy + r12 * r22 * vz;
|
|
267
|
+
out[o + 6] = out[o + 2];
|
|
268
|
+
out[o + 7] = out[o + 5];
|
|
269
|
+
out[o + 8] = r20 * r20 * vx + r21 * r21 * vy + r22 * r22 * vz;
|
|
270
|
+
};
|
|
271
|
+
const det3 = (A, o) => {
|
|
272
|
+
return (A[o] * (A[o + 4] * A[o + 8] - A[o + 5] * A[o + 7]) -
|
|
273
|
+
A[o + 1] * (A[o + 3] * A[o + 8] - A[o + 5] * A[o + 6]) +
|
|
274
|
+
A[o + 2] * (A[o + 3] * A[o + 7] - A[o + 4] * A[o + 6]));
|
|
275
|
+
};
|
|
276
|
+
// Jacobi eigendecomposition for 3x3 symmetric matrix; caller-provided scratch,
|
|
277
|
+
// eigenvalues land on A's diagonal, eigenvectors in V's columns.
|
|
278
|
+
const eigenSymmetric3x3 = (Ain, A, V) => {
|
|
279
|
+
A.set(Ain);
|
|
280
|
+
V[0] = 1;
|
|
281
|
+
V[1] = 0;
|
|
282
|
+
V[2] = 0;
|
|
283
|
+
V[3] = 0;
|
|
284
|
+
V[4] = 1;
|
|
285
|
+
V[5] = 0;
|
|
286
|
+
V[6] = 0;
|
|
287
|
+
V[7] = 0;
|
|
288
|
+
V[8] = 1;
|
|
289
|
+
for (let iter = 0; iter < 24; iter++) {
|
|
290
|
+
let p = 0, q = 1;
|
|
291
|
+
let maxAbs = Math.abs(A[1]);
|
|
292
|
+
if (Math.abs(A[2]) > maxAbs) {
|
|
293
|
+
p = 0;
|
|
294
|
+
q = 2;
|
|
295
|
+
maxAbs = Math.abs(A[2]);
|
|
296
|
+
}
|
|
297
|
+
if (Math.abs(A[5]) > maxAbs) {
|
|
298
|
+
p = 1;
|
|
299
|
+
q = 2;
|
|
300
|
+
maxAbs = Math.abs(A[5]);
|
|
301
|
+
}
|
|
302
|
+
if (maxAbs < 1e-12)
|
|
303
|
+
break;
|
|
304
|
+
const pp = 3 * p + p, qq = 3 * q + q, pq = 3 * p + q;
|
|
305
|
+
const app = A[pp], aqq = A[qq], apq = A[pq];
|
|
306
|
+
const tau = (aqq - app) / (2 * apq);
|
|
307
|
+
const t = Math.sign(tau) / (Math.abs(tau) + Math.sqrt(1 + tau * tau));
|
|
308
|
+
const c = 1 / Math.sqrt(1 + t * t);
|
|
309
|
+
const s = t * c;
|
|
310
|
+
for (let k = 0; k < 3; k++) {
|
|
311
|
+
if (k === p || k === q)
|
|
312
|
+
continue;
|
|
313
|
+
const kp = 3 * k + p, kq = 3 * k + q;
|
|
314
|
+
const pk = 3 * p + k, qk = 3 * q + k;
|
|
315
|
+
const akp = A[kp], akq = A[kq];
|
|
316
|
+
A[kp] = c * akp - s * akq;
|
|
317
|
+
A[pk] = A[kp];
|
|
318
|
+
A[kq] = s * akp + c * akq;
|
|
319
|
+
A[qk] = A[kq];
|
|
320
|
+
}
|
|
321
|
+
A[pp] = c * c * app - 2 * s * c * apq + s * s * aqq;
|
|
322
|
+
A[qq] = s * s * app + 2 * s * c * apq + c * c * aqq;
|
|
323
|
+
A[pq] = 0;
|
|
324
|
+
A[3 * q + p] = 0;
|
|
325
|
+
for (let k = 0; k < 3; k++) {
|
|
326
|
+
const kp = 3 * k + p, kq = 3 * k + q;
|
|
327
|
+
const vkp = V[kp], vkq = V[kq];
|
|
328
|
+
V[kp] = c * vkp - s * vkq;
|
|
329
|
+
V[kq] = s * vkp + c * vkq;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
};
|
|
333
|
+
const rotmatToQuat = (R, o, out, oo) => {
|
|
334
|
+
const m00 = R[o], m11 = R[o + 4], m22 = R[o + 8];
|
|
335
|
+
const tr = m00 + m11 + m22;
|
|
336
|
+
let qw, qx, qy, qz;
|
|
337
|
+
if (tr > 0) {
|
|
338
|
+
const S = Math.sqrt(tr + 1) * 2;
|
|
339
|
+
qw = 0.25 * S;
|
|
340
|
+
qx = (R[o + 7] - R[o + 5]) / S;
|
|
341
|
+
qy = (R[o + 2] - R[o + 6]) / S;
|
|
342
|
+
qz = (R[o + 3] - R[o + 1]) / S;
|
|
343
|
+
}
|
|
344
|
+
else if (R[o] > R[o + 4] && R[o] > R[o + 8]) {
|
|
345
|
+
const S = Math.sqrt(1 + R[o] - R[o + 4] - R[o + 8]) * 2;
|
|
346
|
+
qw = (R[o + 7] - R[o + 5]) / S;
|
|
347
|
+
qx = 0.25 * S;
|
|
348
|
+
qy = (R[o + 1] + R[o + 3]) / S;
|
|
349
|
+
qz = (R[o + 2] + R[o + 6]) / S;
|
|
350
|
+
}
|
|
351
|
+
else if (R[o + 4] > R[o + 8]) {
|
|
352
|
+
const S = Math.sqrt(1 + R[o + 4] - R[o] - R[o + 8]) * 2;
|
|
353
|
+
qw = (R[o + 2] - R[o + 6]) / S;
|
|
354
|
+
qx = (R[o + 1] + R[o + 3]) / S;
|
|
355
|
+
qy = 0.25 * S;
|
|
356
|
+
qz = (R[o + 5] + R[o + 7]) / S;
|
|
357
|
+
}
|
|
358
|
+
else {
|
|
359
|
+
const S = Math.sqrt(1 + R[o + 8] - R[o] - R[o + 4]) * 2;
|
|
360
|
+
qw = (R[o + 3] - R[o + 1]) / S;
|
|
361
|
+
qx = (R[o + 2] + R[o + 6]) / S;
|
|
362
|
+
qy = (R[o + 5] + R[o + 7]) / S;
|
|
363
|
+
qz = 0.25 * S;
|
|
364
|
+
}
|
|
365
|
+
const n = Math.hypot(qw, qx, qy, qz);
|
|
366
|
+
const inv = 1 / Math.max(n, 1e-12);
|
|
367
|
+
out[oo] = qw * inv;
|
|
368
|
+
out[oo + 1] = qx * inv;
|
|
369
|
+
out[oo + 2] = qy * inv;
|
|
370
|
+
out[oo + 3] = qz * inv;
|
|
371
|
+
};
|
|
372
|
+
// ---------- ellipsoid area (Knud Thomsen p=1.6075) ----------
|
|
373
|
+
const ELLIPSOID_P = 1.6075;
|
|
374
|
+
const ellipsoidArea = (sx, sy, sz) => {
|
|
375
|
+
const a = Math.pow(sx * sy, ELLIPSOID_P);
|
|
376
|
+
const b = Math.pow(sx * sz, ELLIPSOID_P);
|
|
377
|
+
const c = Math.pow(sy * sz, ELLIPSOID_P);
|
|
378
|
+
return 4 * Math.PI * Math.pow((a + b + c) / 3, 1 / ELLIPSOID_P);
|
|
379
|
+
};
|
|
380
|
+
const createMergeScratch = () => ({
|
|
381
|
+
sigm: new Float64Array(9),
|
|
382
|
+
sigI: new Float64Array(9),
|
|
383
|
+
sigJ: new Float64Array(9),
|
|
384
|
+
rI: new Float64Array(9),
|
|
385
|
+
rJ: new Float64Array(9),
|
|
386
|
+
sig: new Float64Array(9),
|
|
387
|
+
rM: new Float64Array(9),
|
|
388
|
+
eigA: new Float64Array(9),
|
|
389
|
+
eigV: new Float64Array(9),
|
|
390
|
+
weights: new Float64Array(8)
|
|
391
|
+
});
|
|
392
|
+
/**
|
|
393
|
+
* Merge weight of one splat: area·α "ink" mass (+1e-30, the merge-path
|
|
394
|
+
* epsilon; the cost-path cache uses +1e-12 — both match their legacy
|
|
395
|
+
* counterparts exactly).
|
|
396
|
+
* @param geo - Geometric-layer view (8 f32/splat).
|
|
397
|
+
* @param i - Splat index.
|
|
398
|
+
* @returns The merge weight.
|
|
399
|
+
*/
|
|
400
|
+
const splatMass = (geo, i) => {
|
|
401
|
+
const i8 = i * 8;
|
|
402
|
+
const sx = Math.max(Math.exp(geo[i8 + 4]), 1e-12);
|
|
403
|
+
const sy = Math.max(Math.exp(geo[i8 + 5]), 1e-12);
|
|
404
|
+
const sz = Math.max(Math.exp(geo[i8 + 6]), 1e-12);
|
|
405
|
+
return sigmoid(geo[i8 + 7]) * ellipsoidArea(sx, sy, sz) + 1e-30;
|
|
406
|
+
};
|
|
407
|
+
/**
|
|
408
|
+
* n-ary moment match: merge `count` splats into one Gaussian. Weights are
|
|
409
|
+
* area·α; merged covariance is the weighted sum of (δδᵀ + Σₖ) (law of total
|
|
410
|
+
* variance); opacity is mass-conserving capped at 1; color/SH is the weighted
|
|
411
|
+
* average. For n = 2 this is arithmetic-identical to the legacy pairwise
|
|
412
|
+
* `momentMatch`.
|
|
413
|
+
*
|
|
414
|
+
* @param view - Splat columns.
|
|
415
|
+
* @param members - Indices of the splats to merge.
|
|
416
|
+
* @param count - Number of members.
|
|
417
|
+
* @param out - Output splat (geo encoded ready-to-store: quat, log scales, logit opacity).
|
|
418
|
+
* @param scratch - Reusable scratch from {@link createMergeScratch}.
|
|
419
|
+
*/
|
|
420
|
+
const mergeGroup = (view, members, count, out, scratch) => {
|
|
421
|
+
const { pos, geo, color, colorDim } = view;
|
|
422
|
+
// Per-member normalized weights, computed once — splatMass is
|
|
423
|
+
// transcendental-heavy and was previously re-evaluated per loop (and per
|
|
424
|
+
// coefficient in the color loop); caching the identical values is
|
|
425
|
+
// bit-exact.
|
|
426
|
+
let weights = scratch.weights;
|
|
427
|
+
if (count > weights.length) {
|
|
428
|
+
weights = scratch.weights = new Float64Array(count);
|
|
429
|
+
}
|
|
430
|
+
let W = 0;
|
|
431
|
+
for (let m = 0; m < count; m++) {
|
|
432
|
+
const mass = splatMass(geo, members[m]);
|
|
433
|
+
weights[m] = mass;
|
|
434
|
+
W += mass;
|
|
435
|
+
}
|
|
436
|
+
for (let m = 0; m < count; m++)
|
|
437
|
+
weights[m] /= W;
|
|
438
|
+
// Merged mean (weighted).
|
|
439
|
+
let mux = 0, muy = 0, muz = 0;
|
|
440
|
+
for (let m = 0; m < count; m++) {
|
|
441
|
+
const i = members[m];
|
|
442
|
+
const p = weights[m];
|
|
443
|
+
mux += p * pos[i * 3];
|
|
444
|
+
muy += p * pos[i * 3 + 1];
|
|
445
|
+
muz += p * pos[i * 3 + 2];
|
|
446
|
+
}
|
|
447
|
+
// Merged covariance: Σ pₖ (δₖδₖᵀ + Σₖ), accumulated member-by-member.
|
|
448
|
+
const Sig = scratch.sig;
|
|
449
|
+
Sig.fill(0);
|
|
450
|
+
const SigI = scratch.sigI;
|
|
451
|
+
const Ri = scratch.rI;
|
|
452
|
+
for (let m = 0; m < count; m++) {
|
|
453
|
+
const i = members[m];
|
|
454
|
+
const i8 = i * 8;
|
|
455
|
+
const p = weights[m];
|
|
456
|
+
let qw = geo[i8], qx = geo[i8 + 1], qy = geo[i8 + 2], qz = geo[i8 + 3];
|
|
457
|
+
const qn = 1 / Math.max(Math.hypot(qw, qx, qy, qz), 1e-12);
|
|
458
|
+
qw *= qn;
|
|
459
|
+
qx *= qn;
|
|
460
|
+
qy *= qn;
|
|
461
|
+
qz *= qn;
|
|
462
|
+
const sx = Math.max(Math.exp(geo[i8 + 4]), 1e-12);
|
|
463
|
+
const sy = Math.max(Math.exp(geo[i8 + 5]), 1e-12);
|
|
464
|
+
const sz = Math.max(Math.exp(geo[i8 + 6]), 1e-12);
|
|
465
|
+
quatToRotmat(qw, qx, qy, qz, Ri, 0);
|
|
466
|
+
sigmaFromRotVar(Ri, 0, sx * sx, sy * sy, sz * sz, SigI, 0);
|
|
467
|
+
const dx = pos[i * 3] - mux, dy = pos[i * 3 + 1] - muy, dz = pos[i * 3 + 2] - muz;
|
|
468
|
+
Sig[0] += p * (dx * dx + SigI[0]);
|
|
469
|
+
Sig[1] += p * (dx * dy + SigI[1]);
|
|
470
|
+
Sig[2] += p * (dx * dz + SigI[2]);
|
|
471
|
+
Sig[4] += p * (dy * dy + SigI[4]);
|
|
472
|
+
Sig[5] += p * (dy * dz + SigI[5]);
|
|
473
|
+
Sig[8] += p * (dz * dz + SigI[8]);
|
|
474
|
+
}
|
|
475
|
+
Sig[3] = Sig[1];
|
|
476
|
+
Sig[6] = Sig[2];
|
|
477
|
+
Sig[7] = Sig[5];
|
|
478
|
+
Sig[0] += EPS_COV;
|
|
479
|
+
Sig[4] += EPS_COV;
|
|
480
|
+
Sig[8] += EPS_COV;
|
|
481
|
+
// Eigendecompose → scales (√λ, descending) + right-handed rotation → quat.
|
|
482
|
+
const eigA = scratch.eigA;
|
|
483
|
+
const eigV = scratch.eigV;
|
|
484
|
+
eigenSymmetric3x3(Sig, eigA, eigV);
|
|
485
|
+
const vecs = eigV;
|
|
486
|
+
const v0 = eigA[0], v1 = eigA[4], v2 = eigA[8];
|
|
487
|
+
let o0, o1, o2;
|
|
488
|
+
if (v0 >= v1) {
|
|
489
|
+
if (v1 >= v2) {
|
|
490
|
+
o0 = 0;
|
|
491
|
+
o1 = 1;
|
|
492
|
+
o2 = 2;
|
|
493
|
+
}
|
|
494
|
+
else if (v0 >= v2) {
|
|
495
|
+
o0 = 0;
|
|
496
|
+
o1 = 2;
|
|
497
|
+
o2 = 1;
|
|
498
|
+
}
|
|
499
|
+
else {
|
|
500
|
+
o0 = 2;
|
|
501
|
+
o1 = 0;
|
|
502
|
+
o2 = 1;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
else {
|
|
506
|
+
if (v0 >= v2) {
|
|
507
|
+
o0 = 1;
|
|
508
|
+
o1 = 0;
|
|
509
|
+
o2 = 2;
|
|
510
|
+
}
|
|
511
|
+
else if (v1 >= v2) {
|
|
512
|
+
o0 = 1;
|
|
513
|
+
o1 = 2;
|
|
514
|
+
o2 = 0;
|
|
515
|
+
}
|
|
516
|
+
else {
|
|
517
|
+
o0 = 2;
|
|
518
|
+
o1 = 1;
|
|
519
|
+
o2 = 0;
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
const ev0 = Math.max(eigA[3 * o0 + o0], 1e-18);
|
|
523
|
+
const ev1 = Math.max(eigA[3 * o1 + o1], 1e-18);
|
|
524
|
+
const ev2 = Math.max(eigA[3 * o2 + o2], 1e-18);
|
|
525
|
+
const s0 = Math.sqrt(ev0);
|
|
526
|
+
const s1 = Math.sqrt(ev1);
|
|
527
|
+
const s2 = Math.sqrt(ev2);
|
|
528
|
+
// Mass-conserving opacity, capped at 1 (no scale inflation).
|
|
529
|
+
const alphaM = Math.min(1, W / Math.max(ellipsoidArea(s0, s1, s2), 1e-30));
|
|
530
|
+
const Rm = scratch.rM;
|
|
531
|
+
Rm[0] = vecs[o0];
|
|
532
|
+
Rm[1] = vecs[o1];
|
|
533
|
+
Rm[2] = vecs[o2];
|
|
534
|
+
Rm[3] = vecs[3 + o0];
|
|
535
|
+
Rm[4] = vecs[3 + o1];
|
|
536
|
+
Rm[5] = vecs[3 + o2];
|
|
537
|
+
Rm[6] = vecs[6 + o0];
|
|
538
|
+
Rm[7] = vecs[6 + o1];
|
|
539
|
+
Rm[8] = vecs[6 + o2];
|
|
540
|
+
if (det3(Rm, 0) < 0) {
|
|
541
|
+
Rm[2] *= -1;
|
|
542
|
+
Rm[5] *= -1;
|
|
543
|
+
Rm[8] *= -1;
|
|
544
|
+
}
|
|
545
|
+
rotmatToQuat(Rm, 0, out.geo, 0);
|
|
546
|
+
out.pos[0] = mux;
|
|
547
|
+
out.pos[1] = muy;
|
|
548
|
+
out.pos[2] = muz;
|
|
549
|
+
out.geo[4] = Math.log(s0);
|
|
550
|
+
out.geo[5] = Math.log(s1);
|
|
551
|
+
out.geo[6] = Math.log(s2);
|
|
552
|
+
out.geo[7] = logit(Math.max(0, Math.min(1, alphaM)));
|
|
553
|
+
// Color: weight-normalized (area·α weighted) average.
|
|
554
|
+
for (let c = 0; c < colorDim; c++) {
|
|
555
|
+
let acc = 0;
|
|
556
|
+
for (let m = 0; m < count; m++) {
|
|
557
|
+
acc += weights[m] * color[members[m] * colorDim + c];
|
|
558
|
+
}
|
|
559
|
+
out.color[c] = acc;
|
|
560
|
+
}
|
|
561
|
+
};
|
|
562
|
+
|
|
563
|
+
/**
|
|
564
|
+
* KD-tree over raw column arrays: a pointer-node tree for CPU queries
|
|
565
|
+
* (`KdTree`), and a direct-to-flat builder (`buildFlatKdTree`) emitting the
|
|
566
|
+
* GPU traversal layout without intermediate node objects.
|
|
567
|
+
*
|
|
568
|
+
* Engine-free by contract: worker tasks build trees off-thread, and the
|
|
569
|
+
* worker bundle inlines its whole import graph — an engine import here would
|
|
570
|
+
* embed playcanvas into dist/worker.mjs (see the note atop workers/tasks.ts).
|
|
571
|
+
*
|
|
572
|
+
* `KdTree` dimensionality is the number of columns: 3 for spatial consumers,
|
|
573
|
+
* arbitrary for k-means centroid assignment. `buildFlatKdTree` is 3D only.
|
|
574
|
+
*/
|
|
575
|
+
const nthElement = (arr, lo, hi, k, values) => {
|
|
576
|
+
while (lo < hi) {
|
|
577
|
+
const mid = (lo + hi) >> 1;
|
|
578
|
+
const va = values[arr[lo]], vb = values[arr[mid]], vc = values[arr[hi]];
|
|
579
|
+
let pivotIdx;
|
|
580
|
+
if ((vb - va) * (vc - vb) >= 0)
|
|
581
|
+
pivotIdx = mid;
|
|
582
|
+
else if ((va - vb) * (vc - va) >= 0)
|
|
583
|
+
pivotIdx = lo;
|
|
584
|
+
else
|
|
585
|
+
pivotIdx = hi;
|
|
586
|
+
const pivotVal = values[arr[pivotIdx]];
|
|
587
|
+
// 3-way (Dutch National Flag) partition around pivotVal:
|
|
588
|
+
// [lo..lt-1] < pivot, [lt..gt] == pivot, [gt+1..hi] > pivot.
|
|
589
|
+
// The 2-way Lomuto partition this replaces moved only strictly-less
|
|
590
|
+
// elements, so an all-equal range shrank by one per pass and degenerated
|
|
591
|
+
// to O(N^2) — fatal for inputs where many points share a coordinate
|
|
592
|
+
// (e.g. a splat with every gaussian at the origin).
|
|
593
|
+
let lt = lo, gt = hi, i = lo;
|
|
594
|
+
let tmp;
|
|
595
|
+
while (i <= gt) {
|
|
596
|
+
const v = values[arr[i]];
|
|
597
|
+
if (v < pivotVal) {
|
|
598
|
+
tmp = arr[i];
|
|
599
|
+
arr[i] = arr[lt];
|
|
600
|
+
arr[lt] = tmp;
|
|
601
|
+
lt++;
|
|
602
|
+
i++;
|
|
603
|
+
}
|
|
604
|
+
else if (v > pivotVal) {
|
|
605
|
+
tmp = arr[i];
|
|
606
|
+
arr[i] = arr[gt];
|
|
607
|
+
arr[gt] = tmp;
|
|
608
|
+
gt--;
|
|
609
|
+
}
|
|
610
|
+
else {
|
|
611
|
+
i++;
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
if (k < lt)
|
|
615
|
+
hi = lt - 1;
|
|
616
|
+
else if (k > gt)
|
|
617
|
+
lo = gt + 1;
|
|
618
|
+
else
|
|
619
|
+
return; // k within the equal block; arr[k] is the order statistic
|
|
620
|
+
}
|
|
621
|
+
};
|
|
622
|
+
/**
|
|
623
|
+
* Build a 3D kd-tree directly into the flat GPU layout — no intermediate
|
|
624
|
+
* pointer-node graph (which costs ~50-80 B/node of JS objects and GC
|
|
625
|
+
* pressure, prohibitive at part-scale point counts).
|
|
626
|
+
*
|
|
627
|
+
* Same structure as `KdTree`'s build: median split via `nthElement`, axis
|
|
628
|
+
* cycling x→y→z by depth, two-point ranges ordered so the smaller value is
|
|
629
|
+
* the node and the larger its right child. Nodes are emitted in pre-order
|
|
630
|
+
* DFS (left subtree before right), root at index 0. Recursion depth is
|
|
631
|
+
* bounded by the median split (≈ log2 n).
|
|
632
|
+
*
|
|
633
|
+
* @param x - Point x column.
|
|
634
|
+
* @param y - Point y column.
|
|
635
|
+
* @param z - Point z column.
|
|
636
|
+
* @returns The flat tree over all points.
|
|
637
|
+
*/
|
|
638
|
+
const buildFlatKdTree = (x, y, z) => {
|
|
639
|
+
const n = x.length;
|
|
640
|
+
const cols = [x, y, z];
|
|
641
|
+
const indices = new Uint32Array(n);
|
|
642
|
+
for (let i = 0; i < n; i++)
|
|
643
|
+
indices[i] = i;
|
|
644
|
+
const nodeSplatIdx = new Uint32Array(n);
|
|
645
|
+
const nodePositions = new Float32Array(n * 3);
|
|
646
|
+
const nodeChildren = new Uint32Array(n * 2).fill(0xffffffff);
|
|
647
|
+
let cursor = 0;
|
|
648
|
+
const emit = (splat) => {
|
|
649
|
+
const t = cursor++;
|
|
650
|
+
nodeSplatIdx[t] = splat;
|
|
651
|
+
const t3 = t * 3;
|
|
652
|
+
nodePositions[t3] = x[splat];
|
|
653
|
+
nodePositions[t3 + 1] = y[splat];
|
|
654
|
+
nodePositions[t3 + 2] = z[splat];
|
|
655
|
+
return t;
|
|
656
|
+
};
|
|
657
|
+
const build = (lo, hi, depth) => {
|
|
658
|
+
const count = hi - lo + 1;
|
|
659
|
+
if (count === 1)
|
|
660
|
+
return emit(indices[lo]);
|
|
661
|
+
const values = cols[depth % 3];
|
|
662
|
+
if (count === 2) {
|
|
663
|
+
if (values[indices[lo]] > values[indices[hi]]) {
|
|
664
|
+
const tmp = indices[lo];
|
|
665
|
+
indices[lo] = indices[hi];
|
|
666
|
+
indices[hi] = tmp;
|
|
667
|
+
}
|
|
668
|
+
const t = emit(indices[lo]);
|
|
669
|
+
nodeChildren[t * 2 + 1] = emit(indices[hi]);
|
|
670
|
+
return t;
|
|
671
|
+
}
|
|
672
|
+
const mid = lo + (count >> 1);
|
|
673
|
+
nthElement(indices, lo, hi, mid, values);
|
|
674
|
+
const t = emit(indices[mid]);
|
|
675
|
+
nodeChildren[t * 2] = build(lo, mid - 1, depth + 1);
|
|
676
|
+
nodeChildren[t * 2 + 1] = build(mid + 1, hi, depth + 1);
|
|
677
|
+
return t;
|
|
678
|
+
};
|
|
679
|
+
if (n > 0)
|
|
680
|
+
build(0, n - 1, 0);
|
|
681
|
+
return { nodeSplatIdx, nodePositions, nodeChildren, rootIdx: 0 };
|
|
682
|
+
};
|
|
683
|
+
class KdTree {
|
|
684
|
+
root;
|
|
685
|
+
colData;
|
|
686
|
+
numRows;
|
|
687
|
+
constructor(colData) {
|
|
688
|
+
const numCols = colData.length;
|
|
689
|
+
const numRows = colData[0].length;
|
|
690
|
+
const indices = new Uint32Array(numRows);
|
|
691
|
+
for (let i = 0; i < indices.length; ++i) {
|
|
692
|
+
indices[i] = i;
|
|
693
|
+
}
|
|
694
|
+
const build = (lo, hi, depth) => {
|
|
695
|
+
const count = hi - lo + 1;
|
|
696
|
+
if (count === 1) {
|
|
697
|
+
return { index: indices[lo], count: 1 };
|
|
698
|
+
}
|
|
699
|
+
const values = colData[depth % numCols];
|
|
700
|
+
if (count === 2) {
|
|
701
|
+
if (values[indices[lo]] > values[indices[hi]]) {
|
|
702
|
+
const tmp = indices[lo];
|
|
703
|
+
indices[lo] = indices[hi];
|
|
704
|
+
indices[hi] = tmp;
|
|
705
|
+
}
|
|
706
|
+
return {
|
|
707
|
+
index: indices[lo],
|
|
708
|
+
count: 2,
|
|
709
|
+
right: { index: indices[hi], count: 1 }
|
|
710
|
+
};
|
|
711
|
+
}
|
|
712
|
+
const mid = lo + (count >> 1);
|
|
713
|
+
nthElement(indices, lo, hi, mid, values);
|
|
714
|
+
const left = build(lo, mid - 1, depth + 1);
|
|
715
|
+
const right = build(mid + 1, hi, depth + 1);
|
|
716
|
+
return {
|
|
717
|
+
index: indices[mid],
|
|
718
|
+
count: 1 + left.count + right.count,
|
|
719
|
+
left,
|
|
720
|
+
right
|
|
721
|
+
};
|
|
722
|
+
};
|
|
723
|
+
this.colData = colData;
|
|
724
|
+
this.numRows = numRows;
|
|
725
|
+
this.root = build(0, indices.length - 1, 0);
|
|
726
|
+
}
|
|
727
|
+
findNearest(point, filterFunc) {
|
|
728
|
+
const colData = this.colData;
|
|
729
|
+
const numCols = colData.length;
|
|
730
|
+
let mind = Infinity;
|
|
731
|
+
let mini = -1;
|
|
732
|
+
let cnt = 0;
|
|
733
|
+
const recurse = (node, axis) => {
|
|
734
|
+
const distance = point[axis] - colData[axis][node.index];
|
|
735
|
+
const next = distance > 0 ? node.right : node.left;
|
|
736
|
+
const nextAxis = axis + 1 < numCols ? axis + 1 : 0;
|
|
737
|
+
cnt++;
|
|
738
|
+
if (next) {
|
|
739
|
+
recurse(next, nextAxis);
|
|
740
|
+
}
|
|
741
|
+
if (!filterFunc || filterFunc(node.index)) {
|
|
742
|
+
let thisd = 0;
|
|
743
|
+
for (let c = 0; c < numCols; c++) {
|
|
744
|
+
const v = colData[c][node.index] - point[c];
|
|
745
|
+
thisd += v * v;
|
|
746
|
+
}
|
|
747
|
+
if (thisd < mind) {
|
|
748
|
+
mind = thisd;
|
|
749
|
+
mini = node.index;
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
if (distance * distance < mind) {
|
|
753
|
+
const other = next === node.right ? node.left : node.right;
|
|
754
|
+
if (other) {
|
|
755
|
+
recurse(other, nextAxis);
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
};
|
|
759
|
+
recurse(this.root, 0);
|
|
760
|
+
return { index: mini, distanceSqr: mind, cnt };
|
|
761
|
+
}
|
|
762
|
+
findKNearest(point, k, filterFunc) {
|
|
763
|
+
if (k <= 0) {
|
|
764
|
+
return { indices: new Int32Array(0), distances: new Float32Array(0) };
|
|
765
|
+
}
|
|
766
|
+
k = Math.min(k, this.numRows);
|
|
767
|
+
const colData = this.colData;
|
|
768
|
+
const numCols = colData.length;
|
|
769
|
+
// Bounded max-heap: stores (distance, index) pairs sorted so the
|
|
770
|
+
// farthest element is at position 0, enabling O(1) pruning bound.
|
|
771
|
+
const heapDist = new Float32Array(k).fill(Infinity);
|
|
772
|
+
const heapIdx = new Int32Array(k).fill(-1);
|
|
773
|
+
let heapSize = 0;
|
|
774
|
+
const heapPush = (dist, idx) => {
|
|
775
|
+
if (heapSize < k) {
|
|
776
|
+
let pos = heapSize++;
|
|
777
|
+
heapDist[pos] = dist;
|
|
778
|
+
heapIdx[pos] = idx;
|
|
779
|
+
while (pos > 0) {
|
|
780
|
+
const parent = (pos - 1) >> 1;
|
|
781
|
+
if (heapDist[parent] < heapDist[pos]) {
|
|
782
|
+
const td = heapDist[parent];
|
|
783
|
+
heapDist[parent] = heapDist[pos];
|
|
784
|
+
heapDist[pos] = td;
|
|
785
|
+
const ti = heapIdx[parent];
|
|
786
|
+
heapIdx[parent] = heapIdx[pos];
|
|
787
|
+
heapIdx[pos] = ti;
|
|
788
|
+
pos = parent;
|
|
789
|
+
}
|
|
790
|
+
else {
|
|
791
|
+
break;
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
else if (dist < heapDist[0]) {
|
|
796
|
+
heapDist[0] = dist;
|
|
797
|
+
heapIdx[0] = idx;
|
|
798
|
+
let pos = 0;
|
|
799
|
+
for (;;) {
|
|
800
|
+
const left = 2 * pos + 1;
|
|
801
|
+
const right = 2 * pos + 2;
|
|
802
|
+
let largest = pos;
|
|
803
|
+
if (left < k && heapDist[left] > heapDist[largest])
|
|
804
|
+
largest = left;
|
|
805
|
+
if (right < k && heapDist[right] > heapDist[largest])
|
|
806
|
+
largest = right;
|
|
807
|
+
if (largest === pos)
|
|
808
|
+
break;
|
|
809
|
+
const td = heapDist[pos];
|
|
810
|
+
heapDist[pos] = heapDist[largest];
|
|
811
|
+
heapDist[largest] = td;
|
|
812
|
+
const ti = heapIdx[pos];
|
|
813
|
+
heapIdx[pos] = heapIdx[largest];
|
|
814
|
+
heapIdx[largest] = ti;
|
|
815
|
+
pos = largest;
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
};
|
|
819
|
+
const recurse = (node, axis) => {
|
|
820
|
+
const distance = point[axis] - colData[axis][node.index];
|
|
821
|
+
const next = distance > 0 ? node.right : node.left;
|
|
822
|
+
const nextAxis = axis + 1 < numCols ? axis + 1 : 0;
|
|
823
|
+
if (next) {
|
|
824
|
+
recurse(next, nextAxis);
|
|
825
|
+
}
|
|
826
|
+
if (!filterFunc || filterFunc(node.index)) {
|
|
827
|
+
let thisd = 0;
|
|
828
|
+
for (let c = 0; c < numCols; c++) {
|
|
829
|
+
const v = colData[c][node.index] - point[c];
|
|
830
|
+
thisd += v * v;
|
|
831
|
+
}
|
|
832
|
+
heapPush(thisd, node.index);
|
|
833
|
+
}
|
|
834
|
+
const bound = heapSize < k ? Infinity : heapDist[0];
|
|
835
|
+
if (distance * distance < bound) {
|
|
836
|
+
const other = next === node.right ? node.left : node.right;
|
|
837
|
+
if (other) {
|
|
838
|
+
recurse(other, nextAxis);
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
};
|
|
842
|
+
recurse(this.root, 0);
|
|
843
|
+
// Extract results sorted by distance (ascending)
|
|
844
|
+
const resultIndices = new Int32Array(heapSize);
|
|
845
|
+
const resultDist = new Float32Array(heapSize);
|
|
846
|
+
for (let i = 0; i < heapSize; i++) {
|
|
847
|
+
resultIndices[i] = heapIdx[i];
|
|
848
|
+
resultDist[i] = heapDist[i];
|
|
849
|
+
}
|
|
850
|
+
// Simple insertion sort by distance (k is small)
|
|
851
|
+
for (let i = 1; i < heapSize; i++) {
|
|
852
|
+
const d = resultDist[i];
|
|
853
|
+
const idx = resultIndices[i];
|
|
854
|
+
let j = i - 1;
|
|
855
|
+
while (j >= 0 && resultDist[j] > d) {
|
|
856
|
+
resultDist[j + 1] = resultDist[j];
|
|
857
|
+
resultIndices[j + 1] = resultIndices[j];
|
|
858
|
+
j--;
|
|
859
|
+
}
|
|
860
|
+
resultDist[j + 1] = d;
|
|
861
|
+
resultIndices[j + 1] = idx;
|
|
862
|
+
}
|
|
863
|
+
return { indices: resultIndices, distances: resultDist };
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
/** Marks an unfilled neighbour slot (fewer than k non-self points available). */
|
|
868
|
+
const KNN_SENTINEL = 0xffffffff;
|
|
869
|
+
/**
|
|
870
|
+
* Exact k-nearest-neighbours for the owned prefix of a local point set.
|
|
871
|
+
*
|
|
872
|
+
* Engine-free (imported by worker tasks). Builds a {@link KdTree} over
|
|
873
|
+
* all `n` local points (owned first, then halo) and queries the first
|
|
874
|
+
* `ownedCount`. Output `out[q * k + s]` is a LOCAL index into `positions`,
|
|
875
|
+
* sorted ascending by distance, excluding the query itself, with
|
|
876
|
+
* {@link KNN_SENTINEL} filling surplus slots — the same contract as the
|
|
877
|
+
* legacy CPU KNN loop.
|
|
878
|
+
*
|
|
879
|
+
* @param positions - Interleaved xyz for all local points (owned + halo).
|
|
880
|
+
* @param ownedCount - Number of owned points at the front; only these are queried.
|
|
881
|
+
* @param k - Neighbours per query.
|
|
882
|
+
* @returns Local neighbour indices, `ownedCount * k` long.
|
|
883
|
+
*/
|
|
884
|
+
const knnQueryBlock = (positions, ownedCount, k) => {
|
|
885
|
+
const n = positions.length / 3;
|
|
886
|
+
const x = new Float32Array(n);
|
|
887
|
+
const y = new Float32Array(n);
|
|
888
|
+
const z = new Float32Array(n);
|
|
889
|
+
for (let i = 0; i < n; i++) {
|
|
890
|
+
x[i] = positions[i * 3];
|
|
891
|
+
y[i] = positions[i * 3 + 1];
|
|
892
|
+
z[i] = positions[i * 3 + 2];
|
|
893
|
+
}
|
|
894
|
+
const tree = new KdTree([x, y, z]);
|
|
895
|
+
const out = new Uint32Array(ownedCount * k).fill(KNN_SENTINEL);
|
|
896
|
+
const q = new Float32Array(3);
|
|
897
|
+
for (let i = 0; i < ownedCount; i++) {
|
|
898
|
+
q[0] = x[i];
|
|
899
|
+
q[1] = y[i];
|
|
900
|
+
q[2] = z[i];
|
|
901
|
+
// Request k+1 because the tree returns the query itself (distance 0).
|
|
902
|
+
const res = tree.findKNearest(q, k + 1);
|
|
903
|
+
let outPos = 0;
|
|
904
|
+
for (let m = 0; m < res.indices.length && outPos < k; m++) {
|
|
905
|
+
const j = res.indices[m];
|
|
906
|
+
if (j === i)
|
|
907
|
+
continue;
|
|
908
|
+
out[i * k + outPos] = j;
|
|
909
|
+
outPos++;
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
return out;
|
|
913
|
+
};
|
|
914
|
+
|
|
915
|
+
// The histogram + DP core over sorted finite values: returns up to kTarget
|
|
916
|
+
// centroids, sorted ascending.
|
|
917
|
+
const quantizeFinite = (sortedData, N, kTarget, alpha) => {
|
|
918
|
+
const vMin = sortedData[0];
|
|
919
|
+
const vMax = sortedData[N - 1];
|
|
920
|
+
// build histogram using blended uniform/quantile bin positions
|
|
921
|
+
const H = Math.min(1024, N);
|
|
922
|
+
const vRange = vMax - vMin;
|
|
923
|
+
// adaptive blend ratio: when outliers are extreme (IQR << range), lean
|
|
924
|
+
// strongly toward quantile to give the dense center adequate bins; when
|
|
925
|
+
// the distribution has moderate tails (IQR ~ range), reduce quantile
|
|
926
|
+
// bias somewhat, but keep at least 50% quantile to preserve density
|
|
927
|
+
const iqr = sortedData[Math.floor(N * 0.75)] - sortedData[Math.floor(N * 0.25)];
|
|
928
|
+
const beta = Math.max(0.5, Math.min(0.999, 1 - iqr / vRange));
|
|
929
|
+
const counts = new Float64Array(H);
|
|
930
|
+
const sums = new Float64Array(H);
|
|
931
|
+
for (let i = 0; i < N; ++i) {
|
|
932
|
+
const uniformPos = (sortedData[i] - vMin) / vRange;
|
|
933
|
+
const quantilePos = i / N;
|
|
934
|
+
const bin = Math.min(H - 1, Math.floor(H * (beta * quantilePos + (1 - beta) * uniformPos)));
|
|
935
|
+
counts[bin]++;
|
|
936
|
+
sums[bin] += sortedData[i];
|
|
937
|
+
}
|
|
938
|
+
const centers = new Float64Array(H);
|
|
939
|
+
for (let i = 0; i < H; ++i) {
|
|
940
|
+
centers[i] = counts[i] > 0 ? sums[i] / counts[i] : vMin + ((i + 0.5) / H) * vRange;
|
|
941
|
+
}
|
|
942
|
+
// compute weights: w = count^alpha (sub-linear density weighting)
|
|
943
|
+
const weights = new Float64Array(H);
|
|
944
|
+
for (let i = 0; i < H; ++i) {
|
|
945
|
+
weights[i] = counts[i] > 0 ? Math.pow(counts[i], alpha) : 0;
|
|
946
|
+
}
|
|
947
|
+
// prefix sums for O(1) range cost queries
|
|
948
|
+
// cost(a,b) = sum_wxx - sum_wx^2 / sum_w
|
|
949
|
+
// centroid(a,b) = sum_wx / sum_w
|
|
950
|
+
const prefW = new Float64Array(H + 1);
|
|
951
|
+
const prefWX = new Float64Array(H + 1);
|
|
952
|
+
const prefWXX = new Float64Array(H + 1);
|
|
953
|
+
for (let i = 0; i < H; ++i) {
|
|
954
|
+
prefW[i + 1] = prefW[i] + weights[i];
|
|
955
|
+
prefWX[i + 1] = prefWX[i] + weights[i] * centers[i];
|
|
956
|
+
prefWXX[i + 1] = prefWXX[i] + weights[i] * centers[i] * centers[i];
|
|
957
|
+
}
|
|
958
|
+
const rangeCost = (a, b) => {
|
|
959
|
+
const w = prefW[b + 1] - prefW[a];
|
|
960
|
+
if (w <= 0)
|
|
961
|
+
return 0;
|
|
962
|
+
const wx = prefWX[b + 1] - prefWX[a];
|
|
963
|
+
const wxx = prefWXX[b + 1] - prefWXX[a];
|
|
964
|
+
return wxx - (wx * wx) / w;
|
|
965
|
+
};
|
|
966
|
+
const rangeMean = (a, b) => {
|
|
967
|
+
const w = prefW[b + 1] - prefW[a];
|
|
968
|
+
if (w <= 0)
|
|
969
|
+
return (centers[a] + centers[b]) * 0.5;
|
|
970
|
+
return (prefWX[b + 1] - prefWX[a]) / w;
|
|
971
|
+
};
|
|
972
|
+
const nonEmpty = counts.reduce((n, c) => n + (c > 0 ? 1 : 0), 0);
|
|
973
|
+
const effectiveK = Math.min(kTarget, nonEmpty);
|
|
974
|
+
// DP: dp[m][j] = min weighted SSE of quantizing bins 0..j into m centroids
|
|
975
|
+
// Use two rows to save memory (only need previous row)
|
|
976
|
+
const INF = 1e30;
|
|
977
|
+
let dpPrev = new Float64Array(H).fill(INF);
|
|
978
|
+
let dpCurr = new Float64Array(H).fill(INF);
|
|
979
|
+
const splitTable = new Array(effectiveK + 1);
|
|
980
|
+
// base case: m = 1
|
|
981
|
+
const split1 = new Int32Array(H);
|
|
982
|
+
for (let j = 0; j < H; ++j) {
|
|
983
|
+
dpPrev[j] = rangeCost(0, j);
|
|
984
|
+
split1[j] = -1;
|
|
985
|
+
}
|
|
986
|
+
splitTable[1] = split1;
|
|
987
|
+
// fill DP for m = 2..effectiveK
|
|
988
|
+
for (let m = 2; m <= effectiveK; ++m) {
|
|
989
|
+
dpCurr.fill(INF);
|
|
990
|
+
const splitM = new Int32Array(H);
|
|
991
|
+
for (let j = m - 1; j < H; ++j) {
|
|
992
|
+
let bestCost = INF;
|
|
993
|
+
let bestS = m - 2;
|
|
994
|
+
for (let s = m - 2; s < j; ++s) {
|
|
995
|
+
const cost = dpPrev[s] + rangeCost(s + 1, j);
|
|
996
|
+
if (cost < bestCost) {
|
|
997
|
+
bestCost = cost;
|
|
998
|
+
bestS = s;
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
dpCurr[j] = bestCost;
|
|
1002
|
+
splitM[j] = bestS;
|
|
1003
|
+
}
|
|
1004
|
+
splitTable[m] = splitM;
|
|
1005
|
+
// swap rows
|
|
1006
|
+
const tmp = dpPrev;
|
|
1007
|
+
dpPrev = dpCurr;
|
|
1008
|
+
dpCurr = tmp;
|
|
1009
|
+
}
|
|
1010
|
+
// backtrack to find centroid values
|
|
1011
|
+
const centroidValues = new Float32Array(effectiveK);
|
|
1012
|
+
let j = H - 1;
|
|
1013
|
+
for (let m = effectiveK; m >= 1; --m) {
|
|
1014
|
+
const s = m > 1 ? splitTable[m][j] : -1;
|
|
1015
|
+
centroidValues[m - 1] = rangeMean(s + 1, j);
|
|
1016
|
+
j = s;
|
|
1017
|
+
}
|
|
1018
|
+
// sort centroids (should already be sorted, but ensure)
|
|
1019
|
+
centroidValues.sort();
|
|
1020
|
+
return centroidValues;
|
|
1021
|
+
};
|
|
1022
|
+
/**
|
|
1023
|
+
* Optimal 1D quantization using dynamic programming on a histogram.
|
|
1024
|
+
*
|
|
1025
|
+
* Pools all columns into a single 1D dataset, sorts the values, bins them
|
|
1026
|
+
* using a blend of uniform and quantile positioning, then uses DP to find k
|
|
1027
|
+
* centroids that minimize weighted sum-of-squared-errors (SSE).
|
|
1028
|
+
*
|
|
1029
|
+
* Bin positions are an adaptive blend of uniform (value-space) and
|
|
1030
|
+
* quantile (rank-space) positioning. The blend ratio is computed from
|
|
1031
|
+
* the data's IQR-to-range ratio: extreme outlier distributions (small
|
|
1032
|
+
* IQR relative to range) use near-pure quantile to give the dense
|
|
1033
|
+
* center adequate bins, while moderate-tail distributions reduce
|
|
1034
|
+
* quantile bias (but keep at least 50% quantile weighting).
|
|
1035
|
+
*
|
|
1036
|
+
* Bin weights use sub-linear density weighting: weight = count^alpha.
|
|
1037
|
+
* With alpha < 1, sparse tail regions earn meaningful influence on
|
|
1038
|
+
* centroid placement.
|
|
1039
|
+
*
|
|
1040
|
+
* Non-finite values: `±Infinity` is a valid pipeline value (`scale_*` may be
|
|
1041
|
+
* `-Infinity` for flat splats, `opacity` `+Infinity` — see `filterNaN`), but a
|
|
1042
|
+
* single one pooled into the histogram poisons vMin/vRange and NaN-cascades
|
|
1043
|
+
* into an all-NaN codebook (serialized as JSON nulls). So the histogram/DP
|
|
1044
|
+
* runs over the finite values only, and each infinity present earns a
|
|
1045
|
+
* dedicated end centroid at a finite sentinel 20 units outside the finite
|
|
1046
|
+
* range — JSON-safe, and far enough that log-scale consumers decode
|
|
1047
|
+
* exp(-20) ≈ 2e-9x the nearest finite value, i.e. effectively flat. NaN
|
|
1048
|
+
* contributes nothing to the codebook and labels arbitrarily (in range).
|
|
1049
|
+
*
|
|
1050
|
+
* @param columns - Named columns pooled into 1D.
|
|
1051
|
+
* @param k - Number of codebook entries (default 256).
|
|
1052
|
+
* @param alpha - Density weight exponent. 0 = uniform (each bin equal),
|
|
1053
|
+
* 0.5 = sqrt (balanced), 1.0 = standard MSE (dense regions dominate).
|
|
1054
|
+
* Default 0.5.
|
|
1055
|
+
* @returns Object with `centroids` (k Float32 values, sorted ascending) and
|
|
1056
|
+
* `labels` (same column layout as input, each holding Uint8Array indices
|
|
1057
|
+
* into the codebook).
|
|
1058
|
+
*/
|
|
1059
|
+
const quantize1dColumns = (columns, k = 256, alpha = 0.5) => {
|
|
1060
|
+
const numColumns = columns.length;
|
|
1061
|
+
const numRows = numColumns > 0 ? columns[0].data.length : 0;
|
|
1062
|
+
// pool all columns into a flat 1D array
|
|
1063
|
+
const N = numRows * numColumns;
|
|
1064
|
+
if (N === 0) {
|
|
1065
|
+
return {
|
|
1066
|
+
centroids: new Float32Array(k),
|
|
1067
|
+
labels: columns.map((c) => ({ name: c.name, data: new Uint8Array(numRows) }))
|
|
1068
|
+
};
|
|
1069
|
+
}
|
|
1070
|
+
const data = new Float32Array(N);
|
|
1071
|
+
for (let i = 0; i < numColumns; ++i) {
|
|
1072
|
+
data.set(columns[i].data, i * numRows);
|
|
1073
|
+
}
|
|
1074
|
+
// gather the finite values for histogram binning (keep original for label
|
|
1075
|
+
// assignment), noting any infinities for the reserved end slots
|
|
1076
|
+
const sortedData = new Float32Array(N);
|
|
1077
|
+
let numFinite = 0;
|
|
1078
|
+
let hasNegInf = false;
|
|
1079
|
+
let hasPosInf = false;
|
|
1080
|
+
for (let i = 0; i < N; ++i) {
|
|
1081
|
+
const v = data[i];
|
|
1082
|
+
if (isFinite(v)) {
|
|
1083
|
+
sortedData[numFinite++] = v;
|
|
1084
|
+
}
|
|
1085
|
+
else if (v === -Infinity) {
|
|
1086
|
+
hasNegInf = true;
|
|
1087
|
+
}
|
|
1088
|
+
else if (v === Infinity) {
|
|
1089
|
+
hasPosInf = true;
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
const finite = sortedData.subarray(0, numFinite);
|
|
1093
|
+
finite.sort();
|
|
1094
|
+
const INF_MARGIN = 20;
|
|
1095
|
+
const loSlots = hasNegInf ? 1 : 0;
|
|
1096
|
+
const hiSlots = hasPosInf ? 1 : 0;
|
|
1097
|
+
const negInfCentroid = (numFinite > 0 ? finite[0] : 0) - INF_MARGIN;
|
|
1098
|
+
const posInfCentroid = (numFinite > 0 ? finite[numFinite - 1] : 0) + INF_MARGIN;
|
|
1099
|
+
const vMin = finite[0];
|
|
1100
|
+
const vMax = finite[numFinite - 1];
|
|
1101
|
+
// handle degenerate case where all values are identical
|
|
1102
|
+
if (loSlots === 0 && hiSlots === 0 && vMax - vMin < 1e-20) {
|
|
1103
|
+
const centroids = new Float32Array(k);
|
|
1104
|
+
centroids.fill(vMin);
|
|
1105
|
+
return {
|
|
1106
|
+
centroids,
|
|
1107
|
+
labels: columns.map((c) => ({ name: c.name, data: new Uint8Array(numRows) }))
|
|
1108
|
+
};
|
|
1109
|
+
}
|
|
1110
|
+
// centroid budget for the finite values (end slots reserved for infinities)
|
|
1111
|
+
const kFinite = Math.max(0, k - loSlots - hiSlots);
|
|
1112
|
+
let centroidValues;
|
|
1113
|
+
if (numFinite === 0 || kFinite === 0) {
|
|
1114
|
+
// no finite values at all (±Infinity/NaN-only input): sentinels only
|
|
1115
|
+
centroidValues = new Float32Array(0);
|
|
1116
|
+
}
|
|
1117
|
+
else if (vMax - vMin < 1e-20) {
|
|
1118
|
+
// all finite values identical, with infinities alongside (the all-finite
|
|
1119
|
+
// case returned above): a single centroid represents them
|
|
1120
|
+
centroidValues = Float32Array.of(vMin);
|
|
1121
|
+
}
|
|
1122
|
+
else {
|
|
1123
|
+
centroidValues = quantizeFinite(finite, numFinite, kFinite, alpha);
|
|
1124
|
+
}
|
|
1125
|
+
const effectiveK = centroidValues.length;
|
|
1126
|
+
// compose the final codebook, ascending: [-Inf sentinel][finite centroids,
|
|
1127
|
+
// padded with the last][+Inf sentinel]
|
|
1128
|
+
const finalCentroids = new Float32Array(k);
|
|
1129
|
+
finalCentroids.set(centroidValues, loSlots);
|
|
1130
|
+
const padValue = effectiveK > 0 ? centroidValues[effectiveK - 1] : hasNegInf ? negInfCentroid : posInfCentroid;
|
|
1131
|
+
for (let i = loSlots + effectiveK; i < k - hiSlots; ++i) {
|
|
1132
|
+
finalCentroids[i] = padValue;
|
|
1133
|
+
}
|
|
1134
|
+
if (loSlots) {
|
|
1135
|
+
finalCentroids[0] = negInfCentroid;
|
|
1136
|
+
}
|
|
1137
|
+
if (hiSlots) {
|
|
1138
|
+
finalCentroids[k - 1] = posInfCentroid;
|
|
1139
|
+
}
|
|
1140
|
+
// assign each data point to nearest centroid via binary search
|
|
1141
|
+
const labels = new Uint8Array(N);
|
|
1142
|
+
for (let i = 0; i < N; ++i) {
|
|
1143
|
+
const v = data[i];
|
|
1144
|
+
// binary search for nearest centroid
|
|
1145
|
+
let lo = 0;
|
|
1146
|
+
let hi = k - 1;
|
|
1147
|
+
while (lo < hi) {
|
|
1148
|
+
const mid = (lo + hi) >> 1;
|
|
1149
|
+
// compare against midpoint between centroids mid and mid+1
|
|
1150
|
+
if (v < (finalCentroids[mid] + finalCentroids[mid + 1]) * 0.5) {
|
|
1151
|
+
hi = mid;
|
|
1152
|
+
}
|
|
1153
|
+
else {
|
|
1154
|
+
lo = mid + 1;
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
labels[i] = lo;
|
|
1158
|
+
}
|
|
1159
|
+
return {
|
|
1160
|
+
centroids: finalCentroids,
|
|
1161
|
+
labels: columns.map((c, i) => ({
|
|
1162
|
+
name: c.name,
|
|
1163
|
+
data: labels.slice(i * numRows, (i + 1) * numRows)
|
|
1164
|
+
}))
|
|
1165
|
+
};
|
|
1166
|
+
};
|
|
1167
|
+
|
|
1168
|
+
const taskHandlers = {
|
|
1169
|
+
quantize1d: (args) => {
|
|
1170
|
+
const result = quantize1dColumns(args.columns, args.k, args.alpha);
|
|
1171
|
+
return {
|
|
1172
|
+
result,
|
|
1173
|
+
transfer: [
|
|
1174
|
+
result.centroids.buffer,
|
|
1175
|
+
...result.labels.map((c) => c.data.buffer)
|
|
1176
|
+
]
|
|
1177
|
+
};
|
|
1178
|
+
},
|
|
1179
|
+
encodeWebp: async (args) => {
|
|
1180
|
+
// create() memoizes the wasm module per realm (each worker compiles
|
|
1181
|
+
// its own copy on first use)
|
|
1182
|
+
const codec = await WebPCodec.create();
|
|
1183
|
+
const webp = codec.encodeLosslessRGBA(args.rgba, args.width, args.height, args.width * 4, args.effort);
|
|
1184
|
+
return { result: webp, transfer: [webp.buffer] };
|
|
1185
|
+
},
|
|
1186
|
+
// Build one forest part: a flat KD-tree over the part's position columns
|
|
1187
|
+
// whose node splat ids are remapped to GLOBAL ids, plus the part's point
|
|
1188
|
+
// AABB (the query-side part-entry cull). With `shared`, the flat arrays
|
|
1189
|
+
// move to SharedArrayBuffers so knnForest query tasks read them without
|
|
1190
|
+
// copies (structured clone shares SABs).
|
|
1191
|
+
buildKdForestPart: (args) => {
|
|
1192
|
+
const { x, y, z, ids } = args;
|
|
1193
|
+
const flat = buildFlatKdTree(x, y, z);
|
|
1194
|
+
const splatIdx = flat.nodeSplatIdx;
|
|
1195
|
+
for (let t = 0; t < splatIdx.length; t++)
|
|
1196
|
+
splatIdx[t] = ids[splatIdx[t]];
|
|
1197
|
+
const aabb = new Float32Array([Infinity, Infinity, Infinity, -Infinity, -Infinity, -Infinity]);
|
|
1198
|
+
for (let i = 0; i < x.length; i++) {
|
|
1199
|
+
if (x[i] < aabb[0])
|
|
1200
|
+
aabb[0] = x[i];
|
|
1201
|
+
if (y[i] < aabb[1])
|
|
1202
|
+
aabb[1] = y[i];
|
|
1203
|
+
if (z[i] < aabb[2])
|
|
1204
|
+
aabb[2] = z[i];
|
|
1205
|
+
if (x[i] > aabb[3])
|
|
1206
|
+
aabb[3] = x[i];
|
|
1207
|
+
if (y[i] > aabb[4])
|
|
1208
|
+
aabb[4] = y[i];
|
|
1209
|
+
if (z[i] > aabb[5])
|
|
1210
|
+
aabb[5] = z[i];
|
|
1211
|
+
}
|
|
1212
|
+
if (args.shared && typeof SharedArrayBuffer !== 'undefined') {
|
|
1213
|
+
const shareU32 = (a) => {
|
|
1214
|
+
const s = new Uint32Array(new SharedArrayBuffer(a.byteLength));
|
|
1215
|
+
s.set(a);
|
|
1216
|
+
return s;
|
|
1217
|
+
};
|
|
1218
|
+
const shareF32 = (a) => {
|
|
1219
|
+
const s = new Float32Array(new SharedArrayBuffer(a.byteLength));
|
|
1220
|
+
s.set(a);
|
|
1221
|
+
return s;
|
|
1222
|
+
};
|
|
1223
|
+
return {
|
|
1224
|
+
result: {
|
|
1225
|
+
nodeSplatIdx: shareU32(flat.nodeSplatIdx),
|
|
1226
|
+
nodePositions: shareF32(flat.nodePositions),
|
|
1227
|
+
nodeChildren: shareU32(flat.nodeChildren),
|
|
1228
|
+
rootIdx: flat.rootIdx,
|
|
1229
|
+
aabb
|
|
1230
|
+
},
|
|
1231
|
+
transfer: []
|
|
1232
|
+
};
|
|
1233
|
+
}
|
|
1234
|
+
return {
|
|
1235
|
+
result: { ...flat, aabb },
|
|
1236
|
+
transfer: [
|
|
1237
|
+
flat.nodeSplatIdx.buffer,
|
|
1238
|
+
flat.nodePositions.buffer,
|
|
1239
|
+
flat.nodeChildren.buffer,
|
|
1240
|
+
aabb.buffer
|
|
1241
|
+
]
|
|
1242
|
+
};
|
|
1243
|
+
},
|
|
1244
|
+
// Decimation CPU-fallback KNN: exact forest k-NN for a slice of queries,
|
|
1245
|
+
// as global ids. Part arrays are SAB-backed (shared, no copies).
|
|
1246
|
+
knnForest: (args) => {
|
|
1247
|
+
const count = args.queryIds.length;
|
|
1248
|
+
const out = new Uint32Array(count * args.k);
|
|
1249
|
+
knnForestQuery(args.parts, args.queryPos, args.queryIds, count, args.k, out);
|
|
1250
|
+
return { result: out, transfer: [out.buffer] };
|
|
1251
|
+
},
|
|
1252
|
+
// Build + flatten a KD-tree over interleaved LOCAL positions (the
|
|
1253
|
+
// `--decimate` GPU path: node splat ids stay local and the
|
|
1254
|
+
// flattened arrays upload straight into that path's GpuKnn). Serves
|
|
1255
|
+
// decimate-uniform/ — see its README before changing either handler.
|
|
1256
|
+
flattenKdTree: (args) => {
|
|
1257
|
+
const n = args.positions.length / 3;
|
|
1258
|
+
const x = new Float32Array(n);
|
|
1259
|
+
const y = new Float32Array(n);
|
|
1260
|
+
const z = new Float32Array(n);
|
|
1261
|
+
for (let i = 0; i < n; i++) {
|
|
1262
|
+
x[i] = args.positions[i * 3];
|
|
1263
|
+
y[i] = args.positions[i * 3 + 1];
|
|
1264
|
+
z[i] = args.positions[i * 3 + 2];
|
|
1265
|
+
}
|
|
1266
|
+
const flat = buildFlatKdTree(x, y, z);
|
|
1267
|
+
return {
|
|
1268
|
+
result: flat,
|
|
1269
|
+
transfer: [flat.nodeSplatIdx.buffer, flat.nodePositions.buffer, flat.nodeChildren.buffer]
|
|
1270
|
+
};
|
|
1271
|
+
},
|
|
1272
|
+
// `--decimate` CPU-fallback block KNN: exact k-NN of the owned
|
|
1273
|
+
// prefix within the local point set, as local indices. Frozen.
|
|
1274
|
+
knnBlock: (args) => {
|
|
1275
|
+
const result = knnQueryBlock(args.positions, args.ownedCount, args.k);
|
|
1276
|
+
return { result, transfer: [result.buffer] };
|
|
1277
|
+
},
|
|
1278
|
+
// Decimation merge stream: n-ary moment match of packed member-major
|
|
1279
|
+
// groups. Inputs are member-major (pos 3 / geo 8 / color colorDim floats
|
|
1280
|
+
// per member, groups back to back per `sizes`); outputs are group-major.
|
|
1281
|
+
// `other` columns (when present) copy from the dominant-mass member.
|
|
1282
|
+
mergeGroups: (args) => {
|
|
1283
|
+
const { sizes, colorDim } = args;
|
|
1284
|
+
const g = sizes.length;
|
|
1285
|
+
const otherDim = args.otherDim ?? 0;
|
|
1286
|
+
const view = { pos: args.pos, geo: args.geo, color: args.color, colorDim };
|
|
1287
|
+
const outPos = new Float32Array(g * 3);
|
|
1288
|
+
const outGeo = new Float32Array(g * 8);
|
|
1289
|
+
const outColor = new Float32Array(g * colorDim);
|
|
1290
|
+
const outOther = args.other && otherDim > 0 ? new Uint32Array(g * otherDim) : undefined;
|
|
1291
|
+
const merged = {
|
|
1292
|
+
pos: new Float64Array(3),
|
|
1293
|
+
geo: new Float64Array(8),
|
|
1294
|
+
color: new Float64Array(colorDim)
|
|
1295
|
+
};
|
|
1296
|
+
const scratch = createMergeScratch();
|
|
1297
|
+
const members = [];
|
|
1298
|
+
let base = 0;
|
|
1299
|
+
for (let gi = 0; gi < g; gi++) {
|
|
1300
|
+
const size = sizes[gi];
|
|
1301
|
+
members.length = size;
|
|
1302
|
+
for (let m = 0; m < size; m++)
|
|
1303
|
+
members[m] = base + m;
|
|
1304
|
+
mergeGroup(view, members, size, merged, scratch);
|
|
1305
|
+
outPos.set(merged.pos, gi * 3);
|
|
1306
|
+
outGeo.set(merged.geo, gi * 8);
|
|
1307
|
+
outColor.set(merged.color, gi * colorDim);
|
|
1308
|
+
if (outOther) {
|
|
1309
|
+
let dominant = base, best = -Infinity;
|
|
1310
|
+
for (let m = 0; m < size; m++) {
|
|
1311
|
+
const mass = splatMass(args.geo, base + m);
|
|
1312
|
+
if (mass > best) {
|
|
1313
|
+
best = mass;
|
|
1314
|
+
dominant = base + m;
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
for (let c = 0; c < otherDim; c++) {
|
|
1318
|
+
outOther[gi * otherDim + c] = args.other[dominant * otherDim + c];
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
base += size;
|
|
1322
|
+
}
|
|
1323
|
+
const transfer = [
|
|
1324
|
+
outPos.buffer,
|
|
1325
|
+
outGeo.buffer,
|
|
1326
|
+
outColor.buffer
|
|
1327
|
+
];
|
|
1328
|
+
if (outOther)
|
|
1329
|
+
transfer.push(outOther.buffer);
|
|
1330
|
+
return { result: { pos: outPos, geo: outGeo, color: outColor, other: outOther }, transfer };
|
|
1331
|
+
}
|
|
1332
|
+
};
|
|
1333
|
+
|
|
1334
|
+
/**
|
|
1335
|
+
* Worker-side entry point, built and shipped as `dist/worker.mjs` (see
|
|
1336
|
+
* rollup.config.mjs) and spawned by WorkerQueue from a URL. Runs one task at a
|
|
1337
|
+
* time and posts the result back with its buffers transferred.
|
|
1338
|
+
*/
|
|
1339
|
+
const bind = (post, listen) => {
|
|
1340
|
+
listen(async (message) => {
|
|
1341
|
+
if (message.type === 'init') {
|
|
1342
|
+
// resolved host-side and handed in, so the worker uses the same
|
|
1343
|
+
// wasm location as the host regardless of its own module URL
|
|
1344
|
+
WebPCodec.wasmUrl = message.wasmUrl;
|
|
1345
|
+
return;
|
|
1346
|
+
}
|
|
1347
|
+
try {
|
|
1348
|
+
const { result, transfer } = await taskHandlers[message.task](message.args);
|
|
1349
|
+
post({ type: 'result', result }, transfer);
|
|
1350
|
+
}
|
|
1351
|
+
catch (err) {
|
|
1352
|
+
post({
|
|
1353
|
+
type: 'error',
|
|
1354
|
+
message: err instanceof Error ? err.message : String(err),
|
|
1355
|
+
stack: err instanceof Error ? err.stack : undefined
|
|
1356
|
+
}, []);
|
|
1357
|
+
}
|
|
1358
|
+
});
|
|
1359
|
+
// unprompted readiness signal: lets the host distinguish "environment
|
|
1360
|
+
// cannot run workers" (no ready, fall back inline) from a task crashing
|
|
1361
|
+
// a live worker
|
|
1362
|
+
post({ type: 'ready' }, []);
|
|
1363
|
+
};
|
|
1364
|
+
// same guard as WorkerQueue's isNode: a real worker_threads worker, not an
|
|
1365
|
+
// Electron renderer (where process.versions.node is present but messaging goes
|
|
1366
|
+
// through the Web Worker scope)
|
|
1367
|
+
if (typeof process !== 'undefined' &&
|
|
1368
|
+
!!process.versions?.node &&
|
|
1369
|
+
process.type !== 'renderer') {
|
|
1370
|
+
// node MessagePorts buffer messages until a listener attaches, so the
|
|
1371
|
+
// host's init message survives this async import
|
|
1372
|
+
import('node:worker_threads').then(({ parentPort }) => {
|
|
1373
|
+
bind((message, transfer) => parentPort.postMessage(message, transfer), (handler) => parentPort.on('message', handler));
|
|
1374
|
+
});
|
|
1375
|
+
}
|
|
1376
|
+
else {
|
|
1377
|
+
// dom lib types globalThis as Window; the options form of postMessage is
|
|
1378
|
+
// valid on both Window and the dedicated worker scope
|
|
1379
|
+
bind((message, transfer) => globalThis.postMessage(message, { transfer }), (handler) => {
|
|
1380
|
+
globalThis.onmessage = (event) => handler(event.data);
|
|
1381
|
+
});
|
|
1382
|
+
}
|
|
1383
|
+
//# sourceMappingURL=worker.mjs.map
|