From e049a1236afe0bb5d24f743f596a1a9fb3b4bd08 Mon Sep 17 00:00:00 2001 From: KPal Date: Wed, 5 Aug 2026 13:41:54 +0100 Subject: [PATCH 1/2] chore: migrate to ESLint config v3 --- .github/workflows/ci.yml | 12 + eslint.config.mjs | 20 +- package-lock.json | 1913 ++++++++++++++++- package.json | 11 +- prettier.config.mjs | 1 + src/cli/index.ts | 370 ++-- src/cli/node-device.ts | 5 +- src/cli/node-file-system.ts | 40 +- src/lib/chunk/data.ts | 3 +- src/lib/chunk/in-memory.ts | 55 +- src/lib/chunk/layout.ts | 8 +- src/lib/chunk/pool.ts | 11 +- src/lib/chunk/source.ts | 16 +- src/lib/compat/data-table.ts | 231 +- src/lib/data-table/combine.ts | 15 +- src/lib/data-table/data-table.ts | 75 +- src/lib/data-table/gaussian-aabb.ts | 15 +- src/lib/data-table/gaussian-eval.ts | 35 +- src/lib/data-table/morton-order.ts | 3 +- src/lib/data-table/sh-bands.ts | 2 +- src/lib/data-table/transform.ts | 44 +- src/lib/decimate-uniform/README.md | 21 +- src/lib/decimate-uniform/block-producer.ts | 6 +- src/lib/decimate-uniform/decimate-source.ts | 88 +- src/lib/decimate-uniform/edge-cost-cpu.ts | 116 +- src/lib/decimate-uniform/gpu-edge-cost.ts | 32 +- src/lib/decimate-uniform/gpu-knn.ts | 20 +- src/lib/decimate-uniform/knn-blocks.ts | 51 +- src/lib/decimate-uniform/knn-core.ts | 2 +- src/lib/decimate-uniform/merge-stream.ts | 42 +- src/lib/decimate-uniform/partition.ts | 30 +- src/lib/decimate-uniform/priority.ts | 72 +- src/lib/decimate-uniform/select.ts | 7 +- src/lib/decimate/block-allocation.ts | 42 +- src/lib/decimate/block-merge-stream.ts | 43 +- src/lib/decimate/block-plan.ts | 106 +- src/lib/decimate/block-prepare.ts | 14 +- src/lib/decimate/block-producer.ts | 8 +- src/lib/decimate/decimate-source.ts | 222 +- src/lib/decimate/edge-cost-cpu.ts | 180 +- src/lib/decimate/index.ts | 10 +- src/lib/decimate/knn-core.ts | 17 +- src/lib/decimate/merge-stream.ts | 50 +- src/lib/decimate/moment-match.ts | 171 +- src/lib/decimate/partition.ts | 89 +- src/lib/decimate/priority.ts | 67 +- src/lib/decimate/recost-core.ts | 226 +- src/lib/decimate/select-recost.ts | 89 +- src/lib/decimate/select.ts | 17 +- src/lib/global.d.ts | 4 +- src/lib/gpu/compute-kernel.ts | 7 +- src/lib/gpu/gpu-dilation.ts | 128 +- src/lib/gpu/gpu-edge-cost.ts | 22 +- src/lib/gpu/gpu-kmeans.ts | 170 +- src/lib/gpu/gpu-knn.ts | 82 +- src/lib/gpu/gpu-recost.ts | 94 +- src/lib/gpu/gpu-splat-rasterizer.ts | 183 +- src/lib/gpu/gpu-voxelization.ts | 50 +- src/lib/gpu/shaders/chunks/constants.ts | 2 +- src/lib/gpu/shaders/chunks/covariance-3d.ts | 2 +- src/lib/gpu/shaders/chunks/gaussian-l2.ts | 2 +- .../gpu/shaders/chunks/jacobian-equirect.ts | 2 +- .../gpu/shaders/chunks/jacobian-pinhole.ts | 2 +- .../gpu/shaders/chunks/projection-equirect.ts | 2 +- .../gpu/shaders/chunks/projection-pinhole.ts | 2 +- src/lib/gpu/shaders/chunks/quat-rotation.ts | 2 +- src/lib/gpu/shaders/chunks/sh-band-1.ts | 2 +- src/lib/gpu/shaders/chunks/sh-band-2.ts | 2 +- src/lib/gpu/shaders/chunks/sh-band-3.ts | 2 +- .../gpu/shaders/chunks/tile-aabb-equirect.ts | 2 +- .../gpu/shaders/chunks/tile-aabb-pinhole.ts | 2 +- .../gpu/shaders/chunks/tile-walk-equirect.ts | 2 +- .../gpu/shaders/chunks/tile-walk-pinhole.ts | 2 +- src/lib/gpu/shaders/dilation.ts | 12 +- src/lib/gpu/shaders/finalize.ts | 2 +- src/lib/gpu/shaders/find-boundaries.ts | 2 +- src/lib/gpu/shaders/init-tile-offsets.ts | 2 +- src/lib/gpu/shaders/prefix-sum.ts | 2 +- src/lib/gpu/shaders/prepare-indirect.ts | 2 +- src/lib/gpu/shaders/project.ts | 2 +- src/lib/gpu/shaders/rasterize-binned.ts | 2 +- src/lib/gpu/shaders/tile-bin-emit-pairs.ts | 2 +- src/lib/gpu/shaders/uniforms.ts | 8 +- src/lib/index.ts | 32 +- src/lib/io/read/buffered-read-stream.ts | 2 +- src/lib/io/read/file-system.ts | 4 +- src/lib/io/read/memory-file-system.ts | 9 +- src/lib/io/read/url-file-system.ts | 15 +- src/lib/io/read/zip-file-system.ts | 33 +- src/lib/io/write/crc.ts | 6 +- src/lib/io/write/file-system.ts | 2 + src/lib/io/write/memory-file-system.ts | 4 +- src/lib/io/write/write-helpers.ts | 2 +- src/lib/io/write/zip-file-system.ts | 12 +- src/lib/mesh/coplanar-merge.ts | 63 +- src/lib/mesh/marching-cubes.ts | 297 +-- src/lib/mesh/voxel-faces.ts | 79 +- src/lib/ops/bake-transform.ts | 44 +- src/lib/ops/concat-source.ts | 44 +- src/lib/ops/filter-mask.ts | 50 +- src/lib/ops/filter-source.ts | 11 +- src/lib/ops/map-source.ts | 6 +- src/lib/ops/morton-order.ts | 43 +- src/lib/ops/permute-source.ts | 6 +- src/lib/ops/reduce-bands-source.ts | 25 +- src/lib/ops/select-lod.ts | 10 +- src/lib/ops/stack-lods.ts | 21 +- src/lib/ops/stats.ts | 64 +- src/lib/process-source.ts | 28 +- src/lib/process.ts | 85 +- src/lib/read.ts | 14 +- src/lib/readers/container-source.ts | 23 +- src/lib/readers/decompress-ply.ts | 33 +- src/lib/readers/read-ksplat.ts | 50 +- src/lib/readers/read-lcc.ts | 340 +-- src/lib/readers/read-lcc2.ts | 156 +- src/lib/readers/read-lod.ts | 67 +- src/lib/readers/read-mjs.ts | 2 +- src/lib/readers/read-ply.ts | 318 ++- src/lib/readers/read-sog-v1.ts | 42 +- src/lib/readers/read-sog.ts | 83 +- src/lib/readers/read-splat.ts | 114 +- src/lib/readers/read-spz.ts | 80 +- src/lib/readers/reader-utils.ts | 8 +- src/lib/render/camera.ts | 6 +- src/lib/render/config.ts | 1 - src/lib/render/preprocess.ts | 20 +- src/lib/render/raster-pass.ts | 91 +- src/lib/source-info.ts | 43 +- src/lib/spatial/b-tree.ts | 8 +- src/lib/spatial/gaussian-bvh.ts | 49 +- src/lib/spatial/k-means.ts | 16 +- src/lib/spatial/kd-tree.ts | 55 +- src/lib/spatial/quantize-1d-core.ts | 12 +- src/lib/spatial/radix-sort.ts | 18 +- src/lib/splat-model.ts | 4 +- src/lib/spz-module.ts | 14 +- src/lib/stats.ts | 6 +- src/lib/types.ts | 3 +- src/lib/utils/logger.ts | 76 +- src/lib/utils/math.ts | 3 +- src/lib/utils/quickselect.ts | 2 +- src/lib/utils/rotate-sh.ts | 334 ++- src/lib/utils/text-renderer.ts | 24 +- src/lib/utils/webp-codec.ts | 16 +- src/lib/value-transforms.ts | 55 +- src/lib/version.ts | 4 +- src/lib/voxel/block-cleanup.ts | 37 +- src/lib/voxel/block-index-map.ts | 4 +- src/lib/voxel/block-mask-map.ts | 4 +- src/lib/voxel/carve.ts | 29 +- src/lib/voxel/dilation.ts | 77 +- src/lib/voxel/fill-exterior.ts | 22 +- src/lib/voxel/fill-floor.ts | 31 +- src/lib/voxel/filter-cluster.ts | 77 +- src/lib/voxel/filter-floaters.ts | 45 +- src/lib/voxel/filter-pipeline.ts | 39 +- src/lib/voxel/flood-fill.ts | 20 +- src/lib/voxel/grid-ops.ts | 2 +- src/lib/voxel/morton.ts | 15 +- src/lib/voxel/sort-key-mask.ts | 23 +- src/lib/voxel/sparse-voxel-grid.ts | 142 +- src/lib/voxel/voxel-query.ts | 55 +- src/lib/voxel/voxelize.ts | 42 +- src/lib/workers/index.ts | 20 +- src/lib/workers/tasks.ts | 84 +- src/lib/workers/worker-entry.ts | 19 +- src/lib/workers/worker-queue.ts | 28 +- src/lib/write.ts | 227 +- src/lib/writers/collision-glb.ts | 57 +- src/lib/writers/compressed-chunk.ts | 57 +- src/lib/writers/sparse-octree.ts | 172 +- src/lib/writers/utils.ts | 2 +- src/lib/writers/write-compressed-ply.ts | 65 +- src/lib/writers/write-csv.ts | 9 +- src/lib/writers/write-glb.ts | 58 +- src/lib/writers/write-html.ts | 83 +- src/lib/writers/write-image.ts | 43 +- src/lib/writers/write-lod.ts | 110 +- src/lib/writers/write-ply-streaming.ts | 31 +- src/lib/writers/write-ply.ts | 41 +- src/lib/writers/write-sog.ts | 111 +- src/lib/writers/write-splat-streaming.ts | 14 +- src/lib/writers/write-spz.ts | 11 +- src/lib/writers/write-voxel.ts | 188 +- test/render-golden.test.mjs | 10 +- 186 files changed, 7197 insertions(+), 3623 deletions(-) create mode 100644 prettier.config.mjs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 73436a91..5081ec31 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,6 +27,12 @@ jobs: - name: Build Splat Transform run: npm run build + - name: Run Tests + run: npm test + + - name: Run Publint + run: npm run publint + lint: name: Lint runs-on: ubuntu-latest @@ -44,9 +50,15 @@ jobs: - name: Install Dependencies run: npm ci + - name: Run Prettier + run: npm run fmt + - name: Run ESLint run: npm run lint + - name: Run TypeScript + run: npm run typecheck + docs: name: Docs runs-on: ubuntu-latest diff --git a/eslint.config.mjs b/eslint.config.mjs index e7c4edb7..5e46c602 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -1,34 +1,20 @@ -import playcanvasConfig from '@playcanvas/eslint-config'; -import tsPlugin from '@typescript-eslint/eslint-plugin'; -import tsParser from '@typescript-eslint/parser'; +import typescriptConfig from '@playcanvas/eslint-config/typescript'; import globals from 'globals'; export default [ - ...playcanvasConfig, + ...typescriptConfig, { files: ['**/*.ts'], languageOptions: { - parser: tsParser, globals: { ...globals.node, ...globals.browser } }, - plugins: { - '@typescript-eslint': tsPlugin - }, - settings: { - 'import/resolver': { - typescript: {} - } - }, rules: { - ...tsPlugin.configs.recommended.rules, '@typescript-eslint/ban-ts-comment': 'off', '@typescript-eslint/no-explicit-any': 'off', '@typescript-eslint/no-unused-vars': 'off', - 'jsdoc/require-param-type': 'off', - 'jsdoc/require-returns-type': 'off', 'lines-between-class-members': 'off', 'no-await-in-loop': 'off', 'require-atomic-updates': 'off' @@ -41,7 +27,7 @@ export default [ } }, rules: { - 'import/no-unresolved': 'off' + 'import-x/no-unresolved': 'off' } } ]; diff --git a/package-lock.json b/package-lock.json index 15c52ad6..ce5ade5d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,20 +16,18 @@ "splat-transform": "bin/cli.mjs" }, "devDependencies": { - "@playcanvas/eslint-config": "2.1.0", + "@playcanvas/eslint-config": "3.0.0-beta.8", "@playcanvas/supersplat-viewer": "1.28.0", "@rollup/plugin-json": "6.1.0", "@rollup/plugin-node-resolve": "16.0.3", "@rollup/plugin-replace": "6.0.3", "@rollup/plugin-typescript": "12.3.0", "@types/node": "24.13.3", - "@typescript-eslint/eslint-plugin": "8.65.0", - "@typescript-eslint/parser": "8.65.0", "eslint": "9.39.5", - "eslint-import-resolver-typescript": "4.4.5", "fzstd": "0.1.1", "pathe": "2.0.3", "playcanvas": "2.21.1", + "prettier": "3.9.6", "publint": "0.3.22", "rollup": "4.62.2", "tslib": "2.8.1", @@ -51,6 +49,286 @@ "integrity": "sha512-KUCw5R52rCBnfyvOOIa9nUQGfR6TEcHjYzuWDYRGb1fpkpnkci6VXFqCx/xFJ1yEN7V5gc9YxU2zIY6sEjIbhw==", "license": "ISC" }, + "node_modules/@altano/repository-tools": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@altano/repository-tools/-/repository-tools-2.0.5.tgz", + "integrity": "sha512-dG0CH69cWUKaXOrfIdhldDt8zB3Tp4OQNVvnOWIS0wL+MiIkjyJC3WLFZGrO2r5anW00DxbDnEhpNHIkGCoNuw==", + "dev": true, + "license": "ISC" + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz", + "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.8", + "@babel/types": "^7.29.8", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz", + "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.8.tgz", + "integrity": "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.8", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.8", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.8", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@emnapi/core": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.8.1.tgz", @@ -86,18 +364,40 @@ } }, "node_modules/@es-joy/jsdoccomment": { - "version": "0.50.2", + "version": "0.78.0", + "resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.78.0.tgz", + "integrity": "sha512-rQkU5u8hNAq2NVRzHnIUUvR6arbO0b6AOlvpTNS48CkiKSn/xtNfOzBK23JE4SiW89DgvU7GtxLVgV4Vn2HBAw==", "dev": true, "license": "MIT", "dependencies": { - "@types/estree": "^1.0.6", - "@typescript-eslint/types": "^8.11.0", + "@types/estree": "^1.0.8", + "@typescript-eslint/types": "^8.46.4", "comment-parser": "1.4.1", "esquery": "^1.6.0", - "jsdoc-type-pratt-parser": "~4.1.0" + "jsdoc-type-pratt-parser": "~7.0.0" }, "engines": { - "node": ">=18" + "node": ">=20.11.0" + } + }, + "node_modules/@es-joy/jsdoccomment/node_modules/jsdoc-type-pratt-parser": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/jsdoc-type-pratt-parser/-/jsdoc-type-pratt-parser-7.0.0.tgz", + "integrity": "sha512-c7YbokssPOSHmqTbSAmTtnVgAVa/7lumWNYqomgd5KOMyPrRve2anx6lonfOsXEQacqF9FKVUj7bLg4vRSvdYA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@es-joy/resolve.exports": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@es-joy/resolve.exports/-/resolve.exports-1.2.0.tgz", + "integrity": "sha512-Q9hjxWI5xBM+qW2enxfe8wDKdFWMfd0Z29k5ZJnuBqD/CasY5Zryj09aCA6owbGATWz+39p5uIdaHXpopOcG8g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" } }, "node_modules/@esbuild/aix-ppc64": { @@ -777,6 +1077,38 @@ "url": "https://github.com/sponsors/nzakas" } }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.5", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", @@ -784,6 +1116,17 @@ "dev": true, "license": "MIT" }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, "node_modules/@napi-rs/wasm-runtime": { "version": "0.2.12", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", @@ -798,16 +1141,42 @@ } }, "node_modules/@playcanvas/eslint-config": { - "version": "2.1.0", + "version": "3.0.0-beta.8", + "resolved": "https://registry.npmjs.org/@playcanvas/eslint-config/-/eslint-config-3.0.0-beta.8.tgz", + "integrity": "sha512-E9GavHekFgxciZxcf0FHfn79uUT7aKRgxcFz/Pz7l5TszB1EqqNWI3Df0qcqFUSjnq6V1fvkgS1JraNXY0pK9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint/js": "^9.39.0", + "eslint-config-prettier": "^10.1.8", + "eslint-import-resolver-typescript": "^4.4.4", + "eslint-plugin-import-x": "^4.16.2", + "eslint-plugin-jsdoc": "^61.1.11", + "eslint-plugin-jsx-a11y": "^6.10.2", + "eslint-plugin-package-json": "^0.59.1", + "eslint-plugin-react": "^7.37.5", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-regexp": "^2.10.0", + "globals": "^16.5.0", + "jsonc-eslint-parser": "^2.4.1", + "typescript-eslint": "^8.60.1" + }, + "peerDependencies": { + "eslint": ">= 9", + "typescript": ">= 4" + } + }, + "node_modules/@playcanvas/eslint-config/node_modules/globals": { + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.5.0.tgz", + "integrity": "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==", "dev": true, "license": "MIT", - "dependencies": { - "eslint-plugin-import": "^2.31.0", - "eslint-plugin-jsdoc": "^50.6.11", - "eslint-plugin-regexp": "^2.7.0" + "engines": { + "node": ">=18" }, - "peerDependencies": { - "eslint": ">= 8" + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/@playcanvas/supersplat-viewer": { @@ -1301,7 +1670,9 @@ "node_modules/@rtsao/scc": { "version": "1.1.0", "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "peer": true }, "node_modules/@shikijs/engine-oniguruma": { "version": "3.23.0", @@ -1352,6 +1723,19 @@ "dev": true, "license": "MIT" }, + "node_modules/@sindresorhus/base62": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/base62/-/base62-1.0.0.tgz", + "integrity": "sha512-TeheYy0ILzBEI/CO55CP6zJCSdSWeRtGnHy8U8dWSUH4I68iqTsy7HkMktR4xakThc9jotkPQUXT4ITdbV7cHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@tybys/wasm-util": { "version": "0.10.1", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", @@ -1388,7 +1772,9 @@ "node_modules/@types/json5": { "version": "0.0.29", "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "peer": true }, "node_modules/@types/node": { "version": "24.13.3", @@ -1418,17 +1804,17 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.65.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.65.0.tgz", - "integrity": "sha512-IEgob78X12rHpUmtcwFsXhZdVGJtwTVP8FiCLZkR6GlYVrl2PcuB+KhCE5BlVC/eQpQnu8WXRtkHZuPar+gCRA==", + "version": "8.66.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.66.0.tgz", + "integrity": "sha512-p088eaGrzYz1s+7cov0aMOCkNGTJlVxF4jgubf28c8L0Cv9Rloj8YBHnv4hXLq6IIEE1AsjNWavO+k+8kP2Y0A==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.65.0", - "@typescript-eslint/type-utils": "8.65.0", - "@typescript-eslint/utils": "8.65.0", - "@typescript-eslint/visitor-keys": "8.65.0", + "@typescript-eslint/scope-manager": "8.66.0", + "@typescript-eslint/type-utils": "8.66.0", + "@typescript-eslint/utils": "8.66.0", + "@typescript-eslint/visitor-keys": "8.66.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" @@ -1441,22 +1827,22 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.65.0", + "@typescript-eslint/parser": "^8.66.0", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/parser": { - "version": "8.65.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.65.0.tgz", - "integrity": "sha512-CZ4nMxWwgu1HEEFNkeaCptra9QCtkmKdgf3sWh1rl1trIhmxLilgTV4cwcbQ4wemnT4sWQN8CaKOmdYx+g2gMA==", + "version": "8.66.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.66.0.tgz", + "integrity": "sha512-X6ypGChaWYk6PBtUg2BwuTZEFFcHJAtGTVJ9/lCTOufhZ4i9fNolQNnktq+kkMCwMj7V8Svsq7+TxSDslmhE0g==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.65.0", - "@typescript-eslint/types": "8.65.0", - "@typescript-eslint/typescript-estree": "8.65.0", - "@typescript-eslint/visitor-keys": "8.65.0", + "@typescript-eslint/scope-manager": "8.66.0", + "@typescript-eslint/types": "8.66.0", + "@typescript-eslint/typescript-estree": "8.66.0", + "@typescript-eslint/visitor-keys": "8.66.0", "debug": "^4.4.3" }, "engines": { @@ -1472,14 +1858,14 @@ } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.65.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.65.0.tgz", - "integrity": "sha512-SxnPhbTsGahizDgbu7oqFH/xVtzIqMd/s+WtnSxNxJZJpLbdT5IPdzg8EZxO3+PoKahXmwJLeNQOpKJb3/bi7Q==", + "version": "8.66.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.66.0.tgz", + "integrity": "sha512-7MthGPTt4BP69lSryqpqq8HQqxuzynssckL/jyDyk3+TNMQ3y2jFWkptCrktWvBrP+EH787Nl5N5Qpw7WZg+5g==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.65.0", - "@typescript-eslint/types": "^8.65.0", + "@typescript-eslint/tsconfig-utils": "^8.66.0", + "@typescript-eslint/types": "^8.66.0", "debug": "^4.4.3" }, "engines": { @@ -1494,14 +1880,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.65.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.65.0.tgz", - "integrity": "sha512-Esbl8OSYiVxBokYgWPf7VVWg/BE798wXhimnn9ML9Pt5qoDf8bfQlgjlKXR/k98+AcNzlLKYrpCcrcuZ9DZLgg==", + "version": "8.66.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.66.0.tgz", + "integrity": "sha512-8TGcH25j9zqJ/IULB/ppyhRvxA8QYfFEZ7nfbg6/BN9spDgb8fPWQXlE5l8TWBL50EtUx007uZ1o9VOwrq2/9g==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.65.0", - "@typescript-eslint/visitor-keys": "8.65.0" + "@typescript-eslint/types": "8.66.0", + "@typescript-eslint/visitor-keys": "8.66.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1512,9 +1898,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.65.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.65.0.tgz", - "integrity": "sha512-j6GzGqCiRdA7Qhur2VVmKZAkBLfnHFQfx4TaJGL9RMveZqCo48jSHHO0DTgizEnGhtWnqmbtCUSrqSkdiY/0Hg==", + "version": "8.66.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.66.0.tgz", + "integrity": "sha512-9D5gLYZG4rOjcoag8MQ/fWI8WqA9wcPDyOGyWtWFhvM1lHRbliqUSPIY5J3zqCU1tvSwzXxnnjhQhz5Ne7mJ4g==", "dev": true, "license": "MIT", "engines": { @@ -1529,15 +1915,15 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.65.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.65.0.tgz", - "integrity": "sha512-YjaZ7PRI5qY7ax2L3PbvX0rRyGtipAReCWs0mhhDBHjH/vl0g0BonaGXrKdKpMbIIsMIwDgbk/xzkBTyAltS5g==", + "version": "8.66.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.66.0.tgz", + "integrity": "sha512-LG2dWfjZQQp0ADtAu/EWJVayefGL2UEZ3CDeI44D9v3rXB/WYUqE/jpO28KrEKul5AySrmI+Zh1v6v+xW2U9+g==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.65.0", - "@typescript-eslint/typescript-estree": "8.65.0", - "@typescript-eslint/utils": "8.65.0", + "@typescript-eslint/types": "8.66.0", + "@typescript-eslint/typescript-estree": "8.66.0", + "@typescript-eslint/utils": "8.66.0", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, @@ -1554,9 +1940,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.65.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.65.0.tgz", - "integrity": "sha512-JSSwWNy+H0E/01jJEM+hrX6N0OFDzFzeIhHFSAS01tlVaevpG8cFyYRPhS5yjGOvBUx3sqQHVMjCL1CAZZMxBg==", + "version": "8.66.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.66.0.tgz", + "integrity": "sha512-H6gcYaSDOyvL3AD/jHUtUFo2jqGgn/F6nuyuZSu0QTesxL+cP4dQoIMrODRofuJC09g64+WgZ6tE19Y1N2YIFQ==", "dev": true, "license": "MIT", "engines": { @@ -1568,16 +1954,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.65.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.65.0.tgz", - "integrity": "sha512-JboAE2swaYt4tb1fHhHTABE2K+OLy09XfcTbhnk4Pw96f9dd2e9iYsJ28gBggHlo5z5x1rkyWvcPoTuNTd4oGg==", + "version": "8.66.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.66.0.tgz", + "integrity": "sha512-8/x4INiiQb10jGgXYD7116/zQ+OL84ZIFn0za68wwFHCanT/VLbBEroWht8RV8fn0/ZCAoazHLQgwUC0UQcDfg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.65.0", - "@typescript-eslint/tsconfig-utils": "8.65.0", - "@typescript-eslint/types": "8.65.0", - "@typescript-eslint/visitor-keys": "8.65.0", + "@typescript-eslint/project-service": "8.66.0", + "@typescript-eslint/tsconfig-utils": "8.66.0", + "@typescript-eslint/types": "8.66.0", + "@typescript-eslint/visitor-keys": "8.66.0", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", @@ -1596,16 +1982,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.65.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.65.0.tgz", - "integrity": "sha512-gXiwIHsYreboxeJucHKPvgwl7dXt50mF8s1/c00cP/WoVTyWKFdtfhRWwZiXYFU5H2O8vVoSLNrexFZjYS/SGA==", + "version": "8.66.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.66.0.tgz", + "integrity": "sha512-jasearZPolBw5NJNYGMwxzHMF83niVWmMU1VdHzG1CyfI2VS7f7nZltnKtHcg20hW+7Uo5GfK4MeDPoU3qI8EA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.65.0", - "@typescript-eslint/types": "8.65.0", - "@typescript-eslint/typescript-estree": "8.65.0" + "@typescript-eslint/scope-manager": "8.66.0", + "@typescript-eslint/types": "8.66.0", + "@typescript-eslint/typescript-estree": "8.66.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1620,13 +2006,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.65.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.65.0.tgz", - "integrity": "sha512-8C71BQkGjiMmXtop7pHVJu1l2NNShFdkCyD6a2ezzs5vU/L3LRtb69EtcteFwz0mYMPzIgOw0n6OV4VBUWZd7A==", + "version": "8.66.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.66.0.tgz", + "integrity": "sha512-dkKR8q+lKciskj1Y3vthHktl+3cMLWGyVUP23bRiPZ5O9BRT++4EqDDV+TVeIKBL1VXVEqrJlz8MYbcnvJcAlg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.65.0", + "@typescript-eslint/types": "8.66.0", "eslint-visitor-keys": "^5.0.0" }, "engines": { @@ -1924,7 +2310,9 @@ "license": "BSD-3-Clause" }, "node_modules/acorn": { - "version": "8.15.0", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", + "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", "dev": true, "license": "MIT", "bin": { @@ -1959,6 +2347,19 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, "node_modules/ansi-styles": { "version": "4.3.0", "dev": true, @@ -1975,6 +2376,8 @@ }, "node_modules/are-docs-informative": { "version": "0.0.2", + "resolved": "https://registry.npmjs.org/are-docs-informative/-/are-docs-informative-0.0.2.tgz", + "integrity": "sha512-ixiS0nLNNG5jNQzgZJNoUpBKdo9yTYZMGJ+QgT2jmjR7G7+QHRCc4v6LQ3NgE7EBJq+o0ams3waJwkrlBom8Ig==", "dev": true, "license": "MIT", "engines": { @@ -1986,6 +2389,16 @@ "dev": true, "license": "Python-2.0" }, + "node_modules/aria-query": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/array-buffer-byte-length": { "version": "1.0.2", "dev": true, @@ -2022,10 +2435,33 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/array.prototype.findlast": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", + "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/array.prototype.findlastindex": { "version": "1.2.6", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", @@ -2076,6 +2512,23 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/array.prototype.tosorted": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", + "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3", + "es-errors": "^1.3.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/arraybuffer.prototype.slice": { "version": "1.0.4", "dev": true, @@ -2096,6 +2549,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/ast-types-flow": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz", + "integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==", + "dev": true, + "license": "MIT" + }, "node_modules/async-function": { "version": "1.0.0", "dev": true, @@ -2118,13 +2578,46 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/balanced-match": { - "version": "1.0.2", + "node_modules/axe-core": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.12.1.tgz", + "integrity": "sha512-s7iGf5GaVMxEG0ENN9x+xTr7GFZCb1ZP/1uATUpCEK2X78nDB3RwbtFCo9pGAf9ru+VwoQ464DkaLEeRM08wJA==", "dev": true, - "license": "MIT" + "license": "MPL-2.0", + "engines": { + "node": ">=4" + } }, - "node_modules/brace-expansion": { - "version": "5.0.5", + "node_modules/axobject-query": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "dev": true, + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.11.12", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.12.tgz", + "integrity": "sha512-r7WnVImvVCeFpf2DOXfy41aPWzeNg3H/A2X4dKmy1QL0MSyyk/e7z8ihJ3N6Nn2PsdhkVlqnEfnUE4a05P2aTA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.5", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", "dev": true, @@ -2146,14 +2639,50 @@ "node": "18 || 20 || >=22" } }, + "node_modules/browserslist": { + "version": "4.28.7", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.7.tgz", + "integrity": "sha512-JxV13hNrFxqjOc8alRbq9dK1MM79NEXYpma2B2J4wAtpWS5zIEIKqWPGCl7N4o7Uc7B7itylh7SuDujATRyyTw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.44", + "caniuse-lite": "^1.0.30001806", + "electron-to-chromium": "^1.5.393", + "node-releases": "^2.0.51", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, "node_modules/call-bind": { - "version": "1.0.8", + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", + "integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==", "dev": true, "license": "MIT", "dependencies": { - "call-bind-apply-helpers": "^1.0.0", - "es-define-property": "^1.0.0", - "get-intrinsic": "^1.2.4", + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "get-intrinsic": "^1.3.0", "set-function-length": "^1.2.2" }, "engines": { @@ -2198,6 +2727,27 @@ "node": ">=6" } }, + "node_modules/caniuse-lite": { + "version": "1.0.30001806", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001806.tgz", + "integrity": "sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, "node_modules/chalk": { "version": "4.1.2", "dev": true, @@ -2213,6 +2763,28 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/change-case": { + "version": "5.4.4", + "resolved": "https://registry.npmjs.org/change-case/-/change-case-5.4.4.tgz", + "integrity": "sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz", + "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/color-convert": { "version": "2.0.1", "dev": true, @@ -2242,6 +2814,13 @@ "dev": true, "license": "MIT" }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, "node_modules/cross-spawn": { "version": "7.0.6", "dev": true, @@ -2255,6 +2834,13 @@ "node": ">= 8" } }, + "node_modules/damerau-levenshtein": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", + "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", + "dev": true, + "license": "BSD-2-Clause" + }, "node_modules/data-view-buffer": { "version": "1.0.2", "dev": true, @@ -2363,6 +2949,32 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/detect-indent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-7.0.2.tgz", + "integrity": "sha512-y+8xyqdGLL+6sh0tVeHcfP/QDd8gUgbasolJJpY7NgeQGSZ739bDtSiaiDgtoicy+mtYB81dKLxO9xRhCyIB3A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/detect-newline": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-4.0.1.tgz", + "integrity": "sha512-qE3Veg1YXzGHQhlA6jzebZN2qVf6NX+A7m7qlhCGG30dJixrAQhYOsJjsnBjJkCSmuOPpCk30145fr8FV0bzog==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/doctrine": { "version": "2.1.0", "dev": true, @@ -2387,6 +2999,20 @@ "node": ">= 0.4" } }, + "node_modules/electron-to-chromium": { + "version": "1.5.401", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.401.tgz", + "integrity": "sha512-H6ViHN68nGYlChEvlIU67fn8O2/tpbWQPwck98yaJmh+08LSvHiydzDQ6oXNccLU3kNRVIRS9A4mA7CG+i6fLQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, "node_modules/entities": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", @@ -2401,7 +3027,9 @@ } }, "node_modules/es-abstract": { - "version": "1.24.0", + "version": "1.24.2", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.2.tgz", + "integrity": "sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg==", "dev": true, "license": "MIT", "dependencies": { @@ -2483,6 +3111,34 @@ "node": ">= 0.4" } }, + "node_modules/es-iterator-helpers": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.4.0.tgz", + "integrity": "sha512-c/A0P0oxkACDc+cKWw8evLXK83oBKgn0qPOqCYT4x9uolpCIJAcYvJC9QYKNDRPsTeGyCrQ326jrvgZWdCdK5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.2", + "es-errors": "^1.3.0", + "es-set-tostringtag": "^2.1.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.3.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "iterator.prototype": "^1.1.5", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/es-object-atoms": { "version": "1.1.1", "dev": true, @@ -2577,6 +3233,16 @@ "@esbuild/win32-x64": "0.28.0" } }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/escape-string-regexp": { "version": "4.0.0", "dev": true, @@ -2648,6 +3314,41 @@ } } }, + "node_modules/eslint-config-prettier": { + "version": "10.1.8", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz", + "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", + "dev": true, + "license": "MIT", + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "funding": { + "url": "https://opencollective.com/eslint-config-prettier" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-fix-utils": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/eslint-fix-utils/-/eslint-fix-utils-0.4.3.tgz", + "integrity": "sha512-EKzNhsNavV5DdkHVltHBM9TqfsonCmiUhOm1Ra97zo03M9IAx3wtM1eC27eWGMj/D3LrALmHHNe1QlQH1P7Nxw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@types/estree": ">=1", + "eslint": ">=8" + }, + "peerDependenciesMeta": { + "@types/estree": { + "optional": true + } + } + }, "node_modules/eslint-import-context": { "version": "0.1.9", "dev": true, @@ -2675,6 +3376,8 @@ "version": "0.3.9", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "debug": "^3.2.7", "is-core-module": "^2.13.0", @@ -2685,6 +3388,8 @@ "version": "3.2.7", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "ms": "^2.1.1" } @@ -2728,6 +3433,8 @@ "version": "2.12.1", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "debug": "^3.2.7" }, @@ -2744,6 +3451,8 @@ "version": "3.2.7", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "ms": "^2.1.1" } @@ -2752,6 +3461,8 @@ "version": "2.32.0", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -2780,10 +3491,49 @@ "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" } }, + "node_modules/eslint-plugin-import-x": { + "version": "4.17.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-import-x/-/eslint-plugin-import-x-4.17.1.tgz", + "integrity": "sha512-4cdstYkKCyjumM2Q9NSI03K8D2a9F4Ssz33K2lv2hQa4KmR9jPLwk3uWGtNvclfqBrPGfGuMBwsGMbe6dMRbfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "^8.56.0", + "comment-parser": "^1.4.1", + "debug": "^4.4.1", + "eslint-import-context": "^0.1.9", + "is-glob": "^4.0.3", + "minimatch": "^9.0.3 || ^10.1.2", + "semver": "^7.7.2", + "stable-hash-x": "^0.2.0", + "unrs-resolver": "^1.9.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-plugin-import-x" + }, + "peerDependencies": { + "@typescript-eslint/utils": "^8.56.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "eslint-import-resolver-node": "*" + }, + "peerDependenciesMeta": { + "@typescript-eslint/utils": { + "optional": true + }, + "eslint-import-resolver-node": { + "optional": true + } + } + }, "node_modules/eslint-plugin-import/node_modules/brace-expansion": { "version": "1.1.12", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -2793,6 +3543,8 @@ "version": "3.2.7", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "ms": "^2.1.1" } @@ -2801,6 +3553,8 @@ "version": "3.1.2", "dev": true, "license": "ISC", + "optional": true, + "peer": true, "dependencies": { "brace-expansion": "^1.1.7" }, @@ -2812,33 +3566,263 @@ "version": "6.3.1", "dev": true, "license": "ISC", + "optional": true, + "peer": true, "bin": { "semver": "bin/semver.js" } }, "node_modules/eslint-plugin-jsdoc": { - "version": "50.8.0", + "version": "61.7.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-61.7.1.tgz", + "integrity": "sha512-36DpldF95MlTX//n3/naULFVt8d1cV4jmSkx7ZKrE9ikkKHAgMLesuWp1SmwpVwAs5ndIM6abKd6PeOYZUgdWg==", "dev": true, "license": "BSD-3-Clause", "dependencies": { - "@es-joy/jsdoccomment": "~0.50.2", + "@es-joy/jsdoccomment": "~0.78.0", + "@es-joy/resolve.exports": "1.2.0", "are-docs-informative": "^0.0.2", "comment-parser": "1.4.1", - "debug": "^4.4.1", + "debug": "^4.4.3", "escape-string-regexp": "^4.0.0", - "espree": "^10.3.0", - "esquery": "^1.6.0", + "espree": "^11.0.0", + "esquery": "^1.7.0", + "html-entities": "^2.6.0", + "object-deep-merge": "^2.0.0", "parse-imports-exports": "^0.2.4", - "semver": "^7.7.2", - "spdx-expression-parse": "^4.0.0" + "semver": "^7.7.3", + "spdx-expression-parse": "^4.0.0", + "to-valid-identifier": "^1.0.0" }, "engines": { - "node": ">=18" + "node": ">=20.11.0" }, "peerDependencies": { "eslint": "^7.0.0 || ^8.0.0 || ^9.0.0" } }, + "node_modules/eslint-plugin-jsdoc/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-plugin-jsdoc/node_modules/espree": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", + "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.16.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^5.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-plugin-jsx-a11y": { + "version": "6.10.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.2.tgz", + "integrity": "sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "aria-query": "^5.3.2", + "array-includes": "^3.1.8", + "array.prototype.flatmap": "^1.3.2", + "ast-types-flow": "^0.0.8", + "axe-core": "^4.10.0", + "axobject-query": "^4.1.0", + "damerau-levenshtein": "^1.0.8", + "emoji-regex": "^9.2.2", + "hasown": "^2.0.2", + "jsx-ast-utils": "^3.3.5", + "language-tags": "^1.0.9", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.8", + "safe-regex-test": "^1.0.3", + "string.prototype.includes": "^2.0.1" + }, + "engines": { + "node": ">=4.0" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9" + } + }, + "node_modules/eslint-plugin-jsx-a11y/node_modules/brace-expansion": { + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint-plugin-jsx-a11y/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/eslint-plugin-package-json": { + "version": "0.59.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-package-json/-/eslint-plugin-package-json-0.59.1.tgz", + "integrity": "sha512-dalsU3Zsz8RYLkV6FnNBJh408WlTuqQaDfxV/ae29yK7pTzjNNJN+kqU9YhHfHkbJ5XBax4xHWvidvAk4xE31w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@altano/repository-tools": "^2.0.1", + "change-case": "^5.4.4", + "detect-indent": "^7.0.2", + "detect-newline": "^4.0.1", + "eslint-fix-utils": "~0.4.0", + "package-json-validator": "~0.31.0", + "semver": "^7.7.3", + "sort-object-keys": "^2.0.0", + "sort-package-json": "^3.4.0", + "validate-npm-package-name": "^7.0.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "eslint": ">=8.0.0", + "jsonc-eslint-parser": "^2.0.0" + } + }, + "node_modules/eslint-plugin-react": { + "version": "7.37.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", + "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.8", + "array.prototype.findlast": "^1.2.5", + "array.prototype.flatmap": "^1.3.3", + "array.prototype.tosorted": "^1.1.4", + "doctrine": "^2.1.0", + "es-iterator-helpers": "^1.2.1", + "estraverse": "^5.3.0", + "hasown": "^2.0.2", + "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "minimatch": "^3.1.2", + "object.entries": "^1.1.9", + "object.fromentries": "^2.0.8", + "object.values": "^1.2.1", + "prop-types": "^15.8.1", + "resolve": "^2.0.0-next.5", + "semver": "^6.3.1", + "string.prototype.matchall": "^4.0.12", + "string.prototype.repeat": "^1.0.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.1.1.tgz", + "integrity": "sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "hermes-parser": "^0.25.1", + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0" + } + }, + "node_modules/eslint-plugin-react/node_modules/brace-expansion": { + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint-plugin-react/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/eslint-plugin-react/node_modules/resolve": { + "version": "2.0.0-next.7", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.7.tgz", + "integrity": "sha512-tqt+NBWwyaMgw3zDsnygx4CByWjQEJHOPMdslYhppaQSJUtL/D4JO9CcBBlhPoI8lz9oJIDXkwXfhF4aWqP8xQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.2", + "node-exports-info": "^1.6.0", + "object-keys": "^1.1.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/eslint-plugin-react/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/eslint-plugin-regexp": { "version": "2.10.0", "dev": true, @@ -2960,7 +3944,9 @@ } }, "node_modules/esquery": { - "version": "1.6.0", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -3154,6 +4140,39 @@ "node": ">= 0.4" } }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz", + "integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-intrinsic": { "version": "1.3.0", "dev": true, @@ -3216,6 +4235,16 @@ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, + "node_modules/git-hooks-list": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/git-hooks-list/-/git-hooks-list-4.2.1.tgz", + "integrity": "sha512-WNvqJjOxxs/8ZP9+DWdwWJ7cDsd60NHf39XnD82pDVrKO5q7xfPqpkK6hwEAmBa/ZSEE4IOoR75EzbbIuwGlMw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/fisker/git-hooks-list?sponsor=1" + } + }, "node_modules/glob-parent": { "version": "6.0.2", "dev": true, @@ -3334,7 +4363,9 @@ } }, "node_modules/hasown": { - "version": "2.0.2", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", "dev": true, "license": "MIT", "dependencies": { @@ -3344,6 +4375,40 @@ "node": ">= 0.4" } }, + "node_modules/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "dev": true, + "license": "MIT" + }, + "node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, + "node_modules/html-entities": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.6.0.tgz", + "integrity": "sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/mdevils" + }, + { + "type": "patreon", + "url": "https://patreon.com/mdevils" + } + ], + "license": "MIT" + }, "node_modules/ignore": { "version": "7.0.5", "dev": true, @@ -3471,11 +4536,13 @@ } }, "node_modules/is-core-module": { - "version": "2.16.1", + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", + "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", "dev": true, "license": "MIT", "dependencies": { - "hasown": "^2.0.2" + "hasown": "^2.0.3" }, "engines": { "node": ">= 0.4" @@ -3608,6 +4675,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-regex": { "version": "1.2.1", "dev": true, @@ -3745,6 +4825,31 @@ "dev": true, "license": "ISC" }, + "node_modules/iterator.prototype": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", + "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "get-proto": "^1.0.0", + "has-symbols": "^1.1.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, "node_modules/js-yaml": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz", @@ -3776,6 +4881,19 @@ "node": ">=12.0.0" } }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/json-buffer": { "version": "3.0.1", "dev": true, @@ -3797,6 +4915,8 @@ "version": "1.0.2", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "minimist": "^1.2.0" }, @@ -3804,6 +4924,59 @@ "json5": "lib/cli.js" } }, + "node_modules/jsonc-eslint-parser": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/jsonc-eslint-parser/-/jsonc-eslint-parser-2.4.2.tgz", + "integrity": "sha512-1e4qoRgnn448pRuMvKGsFFymUCquZV0mpGgOyIKNgD3JVDTsVJyRBGH/Fm0tBb8WsWGgmB1mDe6/yJMQM37DUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.5.0", + "eslint-visitor-keys": "^3.0.0", + "espree": "^9.0.0", + "semver": "^7.3.5" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ota-meshi" + } + }, + "node_modules/jsonc-eslint-parser/node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/jsx-ast-utils": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", + "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.6", + "array.prototype.flat": "^1.3.1", + "object.assign": "^4.1.4", + "object.values": "^1.1.6" + }, + "engines": { + "node": ">=4.0" + } + }, "node_modules/keyv": { "version": "4.5.4", "dev": true, @@ -3812,6 +4985,26 @@ "json-buffer": "3.0.1" } }, + "node_modules/language-subtag-registry": { + "version": "0.3.23", + "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz", + "integrity": "sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/language-tags": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.9.tgz", + "integrity": "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==", + "dev": true, + "license": "MIT", + "dependencies": { + "language-subtag-registry": "^0.3.20" + }, + "engines": { + "node": ">=0.10" + } + }, "node_modules/levn": { "version": "0.4.1", "dev": true, @@ -3863,6 +5056,29 @@ "dev": true, "license": "MIT" }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, "node_modules/lunr": { "version": "2.3.9", "resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.9.tgz", @@ -3943,6 +5159,8 @@ "version": "1.2.8", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -3980,6 +5198,62 @@ "dev": true, "license": "MIT" }, + "node_modules/node-exports-info": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/node-exports-info/-/node-exports-info-1.6.2.tgz", + "integrity": "sha512-kXs9Go0cah0qHVV2v389IXQLdLCeE1xfFtjOAF+iobu0OIoG1pje8At2vMHyaPMiPMnG/LWP50twML21eMcAag==", + "dev": true, + "license": "MIT", + "dependencies": { + "array.prototype.flatmap": "^1.3.3", + "es-errors": "^1.3.0", + "object.entries": "^1.1.9", + "semver": "^6.3.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/node-exports-info/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/node-releases": { + "version": "2.0.52", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.52.tgz", + "integrity": "sha512-MRlTqhAfoMx/4mhEbPo3Hi02g9LJZaJkka69V6h67Cb1gjrAG0jsTE4CZX1eptNx+VCAwJmfpnDIF4P0Nh1A7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-deep-merge": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/object-deep-merge/-/object-deep-merge-2.0.1.tgz", + "integrity": "sha512-aKttDKcU3pyZqKcCkDhsMn70WmZFG2JGDQLP9EcLyTSIFQRCPWLAmBZRLJnrVUrhPG1jETEEbfdgbNtJf1LyMg==", + "dev": true, + "license": "MIT" + }, "node_modules/object-inspect": { "version": "1.13.4", "dev": true, @@ -4018,6 +5292,22 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/object.entries": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz", + "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/object.fromentries": { "version": "2.0.8", "dev": true, @@ -4039,6 +5329,8 @@ "version": "1.0.3", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", @@ -4125,6 +5417,24 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/package-json-validator": { + "version": "0.31.0", + "resolved": "https://registry.npmjs.org/package-json-validator/-/package-json-validator-0.31.0.tgz", + "integrity": "sha512-kAVO0fNFWI2xpmthogYHnHjCtg0nJvwm9yjd9nnrR5OKIts5fmNMK2OhhjnLD1/ohJNodhCa5tZm8AolOgkfMg==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.7.2", + "validate-npm-package-license": "^3.0.4", + "yargs": "~18.0.0" + }, + "bin": { + "pjv": "lib/bin/pjv.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, "node_modules/package-manager-detector": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.8.0.tgz", @@ -4145,6 +5455,8 @@ }, "node_modules/parse-imports-exports": { "version": "0.2.4", + "resolved": "https://registry.npmjs.org/parse-imports-exports/-/parse-imports-exports-0.2.4.tgz", + "integrity": "sha512-4s6vd6dx1AotCx/RCI2m7t7GCh5bDRUtGNvRfHSP2wbBQdMi67pPe7mtzmgwcaQ8VKK/6IB7Glfyu3qdZJPybQ==", "dev": true, "license": "MIT", "dependencies": { @@ -4153,6 +5465,8 @@ }, "node_modules/parse-statements": { "version": "1.0.11", + "resolved": "https://registry.npmjs.org/parse-statements/-/parse-statements-1.0.11.tgz", + "integrity": "sha512-HlsyYdMBnbPQ9Jr/VgJ1YF4scnldvJpJxCVx6KgqPL4dxppsWrJHCIIxQXMJrqGnsRkNPATbeMJ8Yxu7JMsYcA==", "dev": true, "license": "MIT" }, @@ -4232,6 +5546,34 @@ "node": ">= 0.8.0" } }, + "node_modules/prettier": { + "version": "3.9.6", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.6.tgz", + "integrity": "sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dev": true, + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, "node_modules/publint": { "version": "0.3.22", "resolved": "https://registry.npmjs.org/publint/-/publint-0.3.22.tgz", @@ -4274,6 +5616,13 @@ "node": ">=6" } }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true, + "license": "MIT" + }, "node_modules/refa": { "version": "0.12.1", "dev": true, @@ -4337,6 +5686,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/reserved-identifiers": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/reserved-identifiers/-/reserved-identifiers-1.2.0.tgz", + "integrity": "sha512-yE7KUfFvaBFzGPs5H3Ops1RevfUEsDc5Iz65rOwWg4lE8HJSYtle77uul3+573457oHvBKuHYDl/xqUkKpEEdw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/resolve": { "version": "1.22.11", "dev": true, @@ -4633,13 +5995,68 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/sort-object-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/sort-object-keys/-/sort-object-keys-2.1.0.tgz", + "integrity": "sha512-SOiEnthkJKPv2L6ec6HMwhUcN0/lppkeYuN1x63PbyPRrgSPIuBJCiYxYyvWRTtjMlOi14vQUCGUJqS6PLVm8g==", + "dev": true, + "license": "MIT" + }, + "node_modules/sort-package-json": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/sort-package-json/-/sort-package-json-3.7.1.tgz", + "integrity": "sha512-ssk1HG7whF8N/T1IsNAQrtHG5Cbdi0rAgRJZXYBr9hF5xaHnBNzUx/W6LcthEW7FhOwvZssbESZuO+GxssqAyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-indent": "^7.0.2", + "detect-newline": "^4.0.1", + "git-hooks-list": "^4.1.1", + "is-plain-obj": "^4.1.0", + "semver": "^7.7.3", + "sort-object-keys": "^2.0.1", + "tinyglobby": "^0.2.15" + }, + "bin": { + "sort-package-json": "cli.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/spdx-correct": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-correct/node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, "node_modules/spdx-exceptions": { "version": "2.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", "dev": true, "license": "CC-BY-3.0" }, "node_modules/spdx-expression-parse": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-4.0.0.tgz", + "integrity": "sha512-Clya5JIij/7C6bRR22+tnGXbc4VKlibKSVj2iHvVeX5iMW7s1SIQlqu699JkODJJIhh/pUu8L0/VLh8xflD+LQ==", "dev": true, "license": "MIT", "dependencies": { @@ -4648,7 +6065,9 @@ } }, "node_modules/spdx-license-ids": { - "version": "3.0.22", + "version": "3.0.23", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.23.tgz", + "integrity": "sha512-CWLcCCH7VLu13TgOH+r8p1O/Znwhqv/dbb6lqWy67G+pT1kHmeD/+V36AVb/vq8QMIQwVShJ6Ssl5FPh0fuSdw==", "dev": true, "license": "CC0-1.0" }, @@ -4672,6 +6091,85 @@ "node": ">= 0.4" } }, + "node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width/node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, + "license": "MIT" + }, + "node_modules/string.prototype.includes": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz", + "integrity": "sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/string.prototype.matchall": { + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", + "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.6", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "regexp.prototype.flags": "^1.5.3", + "set-function-name": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.repeat": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", + "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, "node_modules/string.prototype.trim": { "version": "1.2.10", "dev": true, @@ -4725,10 +6223,28 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, "node_modules/strip-bom": { "version": "3.0.0", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "engines": { "node": ">=4" } @@ -4791,6 +6307,23 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "node_modules/to-valid-identifier": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/to-valid-identifier/-/to-valid-identifier-1.0.0.tgz", + "integrity": "sha512-41wJyvKep3yT2tyPqX/4blcfybknGB4D+oETKLs7Q76UiPqRpUJK3hr1nxelyYO0PHKVzJwlu0aCeEAsGI6rpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/base62": "^1.0.0", + "reserved-identifiers": "^1.0.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/ts-api-utils": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", @@ -4808,6 +6341,8 @@ "version": "3.15.0", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "@types/json5": "^0.0.29", "json5": "^1.0.2", @@ -4968,6 +6503,30 @@ "node": ">=14.17" } }, + "node_modules/typescript-eslint": { + "version": "8.66.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.66.0.tgz", + "integrity": "sha512-QlEbBPz/RuJ1XUHj29nm3t0F/O/cSlEnntozqPOYHnnTGAXFamnMBu5i9Vn6vhUPHGAjR+Vl+5J8vPN/BMUrJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.66.0", + "@typescript-eslint/parser": "8.66.0", + "@typescript-eslint/typescript-estree": "8.66.0", + "@typescript-eslint/utils": "8.66.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, "node_modules/uc.micro": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", @@ -5032,6 +6591,37 @@ "@unrs/resolver-binding-win32-x64-msvc": "1.11.1" } }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", @@ -5042,6 +6632,38 @@ "punycode": "^2.1.0" } }, + "node_modules/validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "node_modules/validate-npm-package-license/node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/validate-npm-package-name": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-7.0.2.tgz", + "integrity": "sha512-hVDIBwsRruT73PbK7uP5ebUt+ezEtCmzZz3F59BSr2F6OVFnJ/6h8liuvdLrQ88Xmnk6/+xGGuq+pG9WwTuy3A==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, "node_modules/webgpu": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/webgpu/-/webgpu-0.4.0.tgz", @@ -5156,6 +6778,54 @@ "node": ">=0.10.0" } }, + "node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, "node_modules/yaml": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", @@ -5172,6 +6842,34 @@ "url": "https://github.com/sponsors/eemeli" } }, + "node_modules/yargs": { + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.0.0.tgz", + "integrity": "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^9.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "string-width": "^7.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^22.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, + "node_modules/yargs-parser": { + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", + "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "dev": true, @@ -5182,6 +6880,29 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zod": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-validation-error": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", + "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } } } } diff --git a/package.json b/package.json index b48dfc5e..1f7cae4d 100644 --- a/package.json +++ b/package.json @@ -50,20 +50,18 @@ "webgpu": "0.4.0" }, "devDependencies": { - "@playcanvas/eslint-config": "2.1.0", + "@playcanvas/eslint-config": "3.0.0-beta.8", "@playcanvas/supersplat-viewer": "1.28.0", "@rollup/plugin-json": "6.1.0", "@rollup/plugin-node-resolve": "16.0.3", "@rollup/plugin-replace": "6.0.3", "@rollup/plugin-typescript": "12.3.0", "@types/node": "24.13.3", - "@typescript-eslint/eslint-plugin": "8.65.0", - "@typescript-eslint/parser": "8.65.0", "eslint": "9.39.5", - "eslint-import-resolver-typescript": "4.4.5", "fzstd": "0.1.1", "pathe": "2.0.3", "playcanvas": "2.21.1", + "prettier": "3.9.6", "publint": "0.3.22", "rollup": "4.62.2", "tslib": "2.8.1", @@ -78,13 +76,16 @@ "scripts": { "build": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\" && rollup -c && node -e \"fs=require('fs');fs.copyFileSync('dist/lib/index.d.ts','dist/lib/index.d.cts')\"", "docs": "typedoc", + "fmt": "prettier --check src", + "fmt:fix": "prettier --write src", "lint": "eslint src", "lint:fix": "eslint src --fix", "publint": "publint", "watch": "rollup -c -w", "pretest": "npm run build", "test": "node --import tsx --test --test-force-exit test/*.test.mjs", - "test:fixtures": "node --import tsx test/fixtures/create-fixtures.mjs" + "test:fixtures": "node --import tsx test/fixtures/create-fixtures.mjs", + "typecheck": "tsc --noEmit" }, "engines": { "node": ">=22.0.0" diff --git a/prettier.config.mjs b/prettier.config.mjs new file mode 100644 index 00000000..1f5ea4f3 --- /dev/null +++ b/prettier.config.mjs @@ -0,0 +1 @@ +export { default } from '@playcanvas/eslint-config/prettier'; diff --git a/src/cli/index.ts b/src/cli/index.ts index 7658f28a..1b7a2a8b 100644 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -4,16 +4,14 @@ import { basename, dirname, join, resolve } from 'node:path'; import process, { exit } from 'node:process'; import { parseArgs } from 'node:util'; -import { GraphicsDevice, Vec3 } from 'playcanvas'; +import type { GraphicsDevice } from 'playcanvas'; +import { Vec3 } from 'playcanvas'; -import { createDevice, enumerateAdapters, getPeakGpuMemory } from './node-device'; -import { NodeFileSystem, NodeReadFileSystem } from './node-file-system'; import { bakeTransform, combine, concatSource, createChunkDataPool, - DataTable, dataTableToChunkSource, decimateSource, decimateSourceUniform, @@ -37,16 +35,19 @@ import { WorkerQueue, writeLodSource, writeSource, - type ChunkSource, - type ChunkSourceMetadata, - type ProcessAction, - type FilterFloaters, - type FilterCluster, - type Options as LibOptions, - type CollisionMeshShape, - type ReadFileSystem, logger } from '../lib'; +import type { + DataTable, + ChunkSource, + ChunkSourceMetadata, + ProcessAction, + FilterFloaters, + FilterCluster, + Options as LibOptions, + CollisionMeshShape, + ReadFileSystem +} from '../lib'; // CLI-only internals (deliberately off the public lib surface): the LOD-path // level resolver and the container source readers the LOD writer drives // directly (single-scene callers get these via readFile). @@ -55,10 +56,13 @@ import { readLccSource, readLccEnvironmentSource } from '../lib/readers/read-lcc import { readLcc2Source, readLcc2EnvironmentSource } from '../lib/readers/read-lcc2'; import { readLodSource, readLodEnvironmentSource } from '../lib/readers/read-lod'; +import { createDevice, enumerateAdapters, getPeakGpuMemory } from './node-device'; +import { NodeFileSystem, NodeReadFileSystem } from './node-file-system'; + /** * CLI-specific options extending library options. */ -interface CliOptions extends LibOptions { +type CliOptions = { overwrite: boolean; help: boolean; version: boolean; @@ -67,10 +71,10 @@ interface CliOptions extends LibOptions { mem: boolean; noTty: boolean | undefined; listGpus: boolean; - deviceIdx: number; // -1 = auto, -2 = CPU, 0+ = GPU index - scratchDir: string | undefined; // decimation spill location (default: output directory) - memoryBudgetBytes: number; // decimation residency policy ceiling (not an allocation, not user-facing) -} + deviceIdx: number; // -1 = auto, -2 = CPU, 0+ = GPU index + scratchDir: string | undefined; // decimation spill location (default: output directory) + memoryBudgetBytes: number; // decimation residency policy ceiling (not an allocation, not user-facing) +} & LibOptions; const fileExists = async (filename: string) => { try { @@ -170,21 +174,21 @@ const cliOptionsConfig = { 'voxel-carve': { type: 'string' }, 'seed-pos': { type: 'string', default: '' }, 'collision-mesh': { type: 'string' }, - 'projection': { type: 'string' }, + projection: { type: 'string' }, 'camera-pos': { type: 'string' }, 'camera-target': { type: 'string' }, 'camera-up': { type: 'string' }, 'camera-fov': { type: 'string' }, - 'resolution': { type: 'string' }, + resolution: { type: 'string' }, 'camera-near': { type: 'string' }, - 'background': { type: 'string' }, + background: { type: 'string' }, 'f-stop': { type: 'string' }, 'focus-distance': { type: 'string' }, 'sensor-size': { type: 'string' }, 'camera-pos-end': { type: 'string' }, 'camera-target-end': { type: 'string' }, 'camera-up-end': { type: 'string' }, - 'shutter': { type: 'string' }, + shutter: { type: 'string' }, 'motion-samples': { type: 'string' }, 'scratch-dir': { type: 'string' }, @@ -198,7 +202,7 @@ const cliOptionsConfig = { 'filter-harmonics': { type: 'string', short: 'H', multiple: true }, 'filter-box': { type: 'string', short: 'B', multiple: true }, 'filter-sphere': { type: 'string', short: 'S', multiple: true }, - 'decimate': { type: 'string', short: 'd', multiple: true }, + decimate: { type: 'string', short: 'd', multiple: true }, 'decimate-uniform': { type: 'string', multiple: true }, 'filter-cluster': { type: 'string', short: 'C', multiple: true }, 'filter-floaters': { type: 'string', short: 'F', multiple: true }, @@ -209,9 +213,10 @@ const cliOptionsConfig = { 'morton-order': { type: 'boolean', short: 'm', multiple: true } } as const; -const stringOptionNames = new Set(Object.entries(cliOptionsConfig) -.filter(([, v]) => v.type === 'string') -.flatMap(([name, v]) => [`--${name}`, ...('short' in v ? [`-${v.short}`] : [])]) +const stringOptionNames = new Set( + Object.entries(cliOptionsConfig) + .filter(([, v]) => v.type === 'string') + .flatMap(([name, v]) => [`--${name}`, ...('short' in v ? [`-${v.short}`] : [])]) ); const isNumericValue = (s: string) => /^-?\d[\d.,e+-]*$/.test(s); @@ -222,7 +227,7 @@ const isTextJsonFormat = (s: string) => /^(?:text|json)$/i.test(s); // next argv token is consumed as the value; when omitted (or rejected) the // option is normalized to an empty `--option=` form. type OptionalValueValidator = (next: string) => boolean; -const optionalValueOptions: Map = new Map([ +const optionalValueOptions = new Map([ ['--filter-cluster', isNumericValue], ['-C', isNumericValue], ['--filter-floaters', isNumericValue], @@ -237,8 +242,8 @@ const optionalValueOptions: Map = new Map([ const shortToLong = new Map( Object.entries(cliOptionsConfig) - .filter(([, v]) => 'short' in v) - .map(([name, v]) => [`-${(v as { short: string }).short}`, `--${name}`]) + .filter(([, v]) => 'short' in v) + .map(([name, v]) => [`-${(v as { short: string }).short}`, `--${name}`]) ); /** @@ -306,7 +311,7 @@ const parseArguments = async () => { }; const parseVec = (value: string, count: number): number[] => { - const parts = value.split(',').map(p => parseNumber(p)); + const parts = value.split(',').map((p) => parseNumber(p)); if (parts.length !== count) { throw new Error(`Expected ${count} comma-separated values, got ${parts.length}: ${value}`); } @@ -331,12 +336,18 @@ const parseArguments = async () => { const parseComparator = (value: string): 'lt' | 'lte' | 'gt' | 'gte' | 'eq' | 'neq' => { switch (value) { - case 'lt': return 'lt'; - case 'lte': return 'lte'; - case 'gt': return 'gt'; - case 'gte': return 'gte'; - case 'eq': return 'eq'; - case 'neq': return 'neq'; + case 'lt': + return 'lt'; + case 'lte': + return 'lte'; + case 'gt': + return 'gt'; + case 'gte': + return 'gte'; + case 'eq': + return 'eq'; + case 'neq': + return 'neq'; default: throw new Error(`Invalid comparator value: ${value}`); } @@ -348,7 +359,7 @@ const parseArguments = async () => { let deviceIdx: number; const gpuValue = v.gpu.toLowerCase(); if (gpuValue === 'cpu') { - deviceIdx = -2; // -2 indicates CPU mode + deviceIdx = -2; // -2 indicates CPU mode } else { deviceIdx = parseInteger(v.gpu); if (deviceIdx < -1) { @@ -533,8 +544,11 @@ const parseArguments = async () => { // Half the machine's RAM, capped at 48 GiB — derived here because the // library is node-free and cannot read os.totalmem() itself. memoryBudgetBytes: Math.min(48 * 2 ** 30, Math.floor(totalmem() / 2)), - lodSelect: v['select-lod'].split(',').filter(v => !!v).map(parseInteger), - viewerSettingsJson: viewerSettingsPath && await readJsonFile(viewerSettingsPath), + lodSelect: v['select-lod'] + .split(',') + .filter((v) => !!v) + .map(parseInteger), + viewerSettingsJson: viewerSettingsPath && (await readJsonFile(viewerSettingsPath)), unbundled: v.unbundled, lodChunkCount: parseInteger(v['lod-chunk-count']), lodChunkExtent: parseInteger(v['lod-chunk-extent']), @@ -976,7 +990,7 @@ const main = async () => { const renderer = new TextRenderer({ write, - output: chunk => process.stdout.write(chunk), + output: (chunk) => process.stdout.write(chunk), getPeakCpuMemory: peakCpuMemoryBytes, getPeakGpuMemory }); @@ -1110,30 +1124,33 @@ const main = async () => { // ops (decimate / voxel filters) and the GPU writers (image / voxel). // deviceIdx: -1 = auto, -2 = CPU, 0+ = specific GPU index let cachedDevice: GraphicsDevice | undefined; - const deviceCreator = options.deviceIdx === -2 ? undefined : async () => { - if (cachedDevice) { - return cachedDevice; - } - - let adapterName: string | undefined; - if (options.deviceIdx >= 0) { - const adapters = await enumerateAdapters(); - const adapter = adapters[options.deviceIdx]; - if (adapter) { - adapterName = adapter.name; - } else { - logger.warn(`GPU adapter index ${options.deviceIdx} not found, using default`); - } - } - - cachedDevice = await createDevice(adapterName); - return cachedDevice; - }; + const deviceCreator = + options.deviceIdx === -2 + ? undefined + : async () => { + if (cachedDevice) { + return cachedDevice; + } + + let adapterName: string | undefined; + if (options.deviceIdx >= 0) { + const adapters = await enumerateAdapters(); + const adapter = adapters[options.deviceIdx]; + if (adapter) { + adapterName = adapter.name; + } else { + logger.warn(`GPU adapter index ${options.deviceIdx} not found, using default`); + } + } + + cachedDevice = await createDevice(adapterName); + return cachedDevice; + }; // A single input has an unambiguous format for --info/--stats to report; // with multiple (combined) inputs the format is omitted. - const soleInputFormat = inputArgs.length === 1 ? - getInputFormat(resolveInput(inputArgs[0].filename).classifyName) : undefined; + const soleInputFormat = + inputArgs.length === 1 ? getInputFormat(resolveInput(inputArgs[0].filename).classifyName) : undefined; const processOptions = { createDevice: deviceCreator, sourceFormat: soleInputFormat }; // declare phase total: one Read phase per input + one Write phase @@ -1147,7 +1164,9 @@ const main = async () => { // writer, so it keeps the full multi-LOD source — `--info`/`--stats` // there report every level. lod-meta output keeps every level (path below). if (outputFormat !== null && outputFormat !== 'lod' && options.lodSelect.length > 1) { - throw new Error('Cannot write multiple LOD levels (--select-lod) to a single-scene output; select one level, or output lod-meta.json.'); + throw new Error( + 'Cannot write multiple LOD levels (--select-lod) to a single-scene output; select one level, or output lod-meta.json.' + ); } // Single-scene pipeline (one chunk-native path for every non-lod output). @@ -1162,34 +1181,30 @@ const main = async () => { // voxel/spz). LOD output has its own structural path below; this pipeline // also handles null output (processing for side-effects, skipping the // write). A --tag-lod tag on single-scene output is rejected after the LOD path. - const singleSceneActions = [...inputArgs.flatMap(a => a.processActions), ...outputArg.processActions]; + const singleSceneActions = [...inputArgs.flatMap((a) => a.processActions), ...outputArg.processActions]; // v1 decimation is terminal: the merge stream writes straight into the // destination, so decimate must be the last action and the output must // be plain PLY. Anything else needs two invocations (decimate to PLY, // then convert). - const decimateIdx = singleSceneActions.map((a, i) => (a.kind === 'decimate' ? i : -1)).filter(i => i >= 0); + const decimateIdx = singleSceneActions.map((a, i) => (a.kind === 'decimate' ? i : -1)).filter((i) => i >= 0); if (decimateIdx.length > 0) { - const ok = decimateIdx.length === 1 && + const ok = + decimateIdx.length === 1 && decimateIdx[0] === singleSceneActions.length - 1 && !isNullOutput && outputFormat === 'ply'; if (!ok) { failExit( '--decimate must be the final action and the output must be .ply ' + - `(got ${isNullOutput ? 'no output' : `.${outputFormat}`}${decimateIdx[0] !== singleSceneActions.length - 1 || decimateIdx.length > 1 ? ', with actions after decimate' : ''}). ` + - 'Write a decimated PLY first, then convert in a second invocation.' + `(got ${isNullOutput ? 'no output' : `.${outputFormat}`}${decimateIdx[0] !== singleSceneActions.length - 1 || decimateIdx.length > 1 ? ', with actions after decimate' : ''}). ` + + 'Write a decimated PLY first, then convert in a second invocation.' ); } } - const decimateAction = decimateIdx.length === 1 ? - singleSceneActions[decimateIdx[0]] as CliDecimate : - null; - - if ( - isNullOutput || - (outputFormat !== 'lod' && singleSceneActions.every(a => a.kind !== 'lod')) - ) { + const decimateAction = decimateIdx.length === 1 ? (singleSceneActions[decimateIdx[0]] as CliDecimate) : null; + + if (isNullOutput || (outputFormat !== 'lod' && singleSceneActions.every((a) => a.kind !== 'lod'))) { const pool = createChunkDataPool(); // Open one input as a full (all-LOD) ChunkSource via readFile — native @@ -1197,17 +1212,25 @@ const main = async () => { // inputs. LOD selection is a selectLod node below (real single-LOD // writers only), so readFile always reads every level. mjs generators // need their params + a file:// URL. - const openInput = async (inputArg: typeof inputArgs[number]): Promise => { + const openInput = async (inputArg: (typeof inputArgs)[number]): Promise => { const { filename: inFile, fileSystem, classifyName } = resolveInput(inputArg.filename); const fmt = getInputFormat(classifyName); if (fmt === 'mjs' && isHttpUrl(inputArg.filename)) { throw new Error(`.mjs generator inputs cannot be loaded from a URL: ${inputArg.filename}`); } - const params = inputArg.processActions.filter(a => a.kind === 'param').map((p) => { - return { name: p.name, value: p.value }; - }); + const params = inputArg.processActions + .filter((a) => a.kind === 'param') + .map((p) => { + return { name: p.name, value: p.value }; + }); const readFilename = fmt === 'mjs' ? `file://${inFile}` : inFile; - const srcs = await readFile({ filename: readFilename, inputFormat: fmt, options: { ...options, lodSelect: [] }, params, fileSystem }); + const srcs = await readFile({ + filename: readFilename, + inputFormat: fmt, + options: { ...options, lodSelect: [] }, + params, + fileSystem + }); return srcs.length === 1 ? srcs[0] : concatSource(srcs, pool); }; @@ -1215,20 +1238,21 @@ const main = async () => { // combine() does); mixed layout -> bridge through the DataTable combine(). const combineSources = async (sources: ChunkSource[]): Promise => { if (sources.length === 1) return sources[0]; - const sig = (m: ChunkSourceMetadata) => `${m.shBands}|${[...m.availableLayers].sort().join(',')}|${m.extraColumns.map(e => `${e.name}:${e.type}`).join(',')}`; - if (sources.every(s => sig(s.meta) === sig(sources[0].meta))) { + const sig = (m: ChunkSourceMetadata) => + `${m.shBands}|${[...m.availableLayers].sort().join(',')}|${m.extraColumns.map((e) => `${e.name}:${e.type}`).join(',')}`; + if (sources.every((s) => sig(s.meta) === sig(sources[0].meta))) { const ref = sources[0].meta.transform; - const unified = sources.every(s => s.meta.transform.equals(ref)) ? - sources : - sources.map(s => bakeTransform(s, Transform.IDENTITY)); + const unified = sources.every((s) => s.meta.transform.equals(ref)) + ? sources + : sources.map((s) => bakeTransform(s, Transform.IDENTITY)); return concatSource(unified, pool); } // Mismatched layouts: combine() can union them, concatSource can't. // A DataTable carries no model tag, so resolve it here as // concatSource would (mixed -> 'default', with a warning). - const model = resolveSplatModel(sources.map(s => s.meta.model)); - if (sources.some(s => s.meta.model !== model)) { - const seen = [...new Set(sources.map(s => s.meta.model))].join(', '); + const model = resolveSplatModel(sources.map((s) => s.meta.model)); + if (sources.some((s) => s.meta.model !== model)) { + const seen = [...new Set(sources.map((s) => s.meta.model))].join(', '); logger.warn(`mixed splat models (${seen}); writing the result as '${model}'`); } const dts: DataTable[] = []; @@ -1254,12 +1278,17 @@ const main = async () => { const level = resolveLodLevels(options.lodSelect, src.meta.numLods)[0] ?? 0; src = selectLod(src, level); } - const actions = stripLodTags(inputArg.processActions).filter(a => a.kind !== 'decimate'); + const actions = stripLodTags(inputArg.processActions).filter((a) => a.kind !== 'decimate'); processed.push(await processSourceBridged(src, actions, pool, processOptions)); } let combined = await combineSources(processed); - combined = await processSourceBridged(combined, stripLodTags(outputArg.processActions).filter(a => a.kind !== 'decimate'), pool, processOptions); + combined = await processSourceBridged( + combined, + stripLodTags(outputArg.processActions).filter((a) => a.kind !== 'decimate'), + pool, + processOptions + ); if (combined.meta.numGaussians === 0) { throw new Error('No Gaussians to write'); @@ -1267,9 +1296,10 @@ const main = async () => { if (decimateAction) { const n = combined.meta.numGaussians; - const keepCount = decimateAction.count !== null ? - Math.min(decimateAction.count, n) : - Math.round(n * (decimateAction.percent ?? 100) / 100); + const keepCount = + decimateAction.count !== null + ? Math.min(decimateAction.count, n) + : Math.round((n * (decimateAction.percent ?? 100)) / 100); if (keepCount < 1) { failExit(`--decimate target resolves to ${keepCount} gaussians; must keep at least 1`); } @@ -1279,31 +1309,35 @@ const main = async () => { scratchDir: options.scratchDir ?? dirname(outputFilename), remove: (path: string) => unlink(path) }; - combined = decimateAction.uniform ? - await decimateSourceUniform(combined, pool, { - targetCount: keepCount, - createDevice: deviceCreator, - memoryBudgetBytes: options.memoryBudgetBytes, - spill - }) : - await decimateSource(combined, pool, { - targetCount: keepCount, - createDevice: deviceCreator, - memoryBudgetBytes: options.memoryBudgetBytes, - spill - }); + combined = decimateAction.uniform + ? await decimateSourceUniform(combined, pool, { + targetCount: keepCount, + createDevice: deviceCreator, + memoryBudgetBytes: options.memoryBudgetBytes, + spill + }) + : await decimateSource(combined, pool, { + targetCount: keepCount, + createDevice: deviceCreator, + memoryBudgetBytes: options.memoryBudgetBytes, + spill + }); } logger.info(`${fmtCount(combined.meta.numGaussians)} gaussians · ${combined.meta.shBands} SH bands`); - if (outputFormat !== null) { // null output: process for side-effects (e.g. --stats), skip the write - await writeSource({ - filename: outputFilename, - outputFormat, - source: combined, - pool, - options, - createDevice: deviceCreator - }, new NodeFileSystem()); + if (outputFormat !== null) { + // null output: process for side-effects (e.g. --stats), skip the write + await writeSource( + { + filename: outputFilename, + outputFormat, + source: combined, + pool, + options, + createDevice: deviceCreator + }, + new NodeFileSystem() + ); } await combined.close(); @@ -1321,8 +1355,10 @@ const main = async () => { // output byte-for-byte. if (!isNullOutput && outputFormat === 'lod') { const pool = createChunkDataPool(); - const single = inputArgs.length === 1 && !isHttpUrl(inputArgs[0].filename) ? - getInputFormat(resolveInput(inputArgs[0].filename).classifyName) : null; + const single = + inputArgs.length === 1 && !isHttpUrl(inputArgs[0].filename) + ? getInputFormat(resolveInput(inputArgs[0].filename).classifyName) + : null; let perLevel: ChunkSource[] = []; let envSource: ChunkSource | null = null; @@ -1333,59 +1369,70 @@ const main = async () => { // Intrinsic multi-LOD: view each level with selectLod (shared parent); // env fetched separately. The input's own actions apply per level. const { filename: inFile, fileSystem } = resolveInput(inputArgs[0].filename); - const multi = single === 'lcc2' ? - await readLcc2Source(fileSystem, inFile, { ...options, lodSelect: [] }, pool) : - single === 'lod' ? - await readLodSource(fileSystem, inFile, { ...options, lodSelect: [] }, pool) : - await readLccSource(fileSystem, inFile, { ...options, lodSelect: [] }, pool); + const multi = + single === 'lcc2' + ? await readLcc2Source(fileSystem, inFile, { ...options, lodSelect: [] }, pool) + : single === 'lod' + ? await readLodSource(fileSystem, inFile, { ...options, lodSelect: [] }, pool) + : await readLccSource(fileSystem, inFile, { ...options, lodSelect: [] }, pool); container = multi; - envSource = single === 'lcc2' ? - await readLcc2EnvironmentSource(fileSystem, inFile, pool) : - single === 'lod' ? - await readLodEnvironmentSource(fileSystem, inFile, pool) : - await readLccEnvironmentSource(fileSystem, inFile, pool); + envSource = + single === 'lcc2' + ? await readLcc2EnvironmentSource(fileSystem, inFile, pool) + : single === 'lod' + ? await readLodEnvironmentSource(fileSystem, inFile, pool) + : await readLccEnvironmentSource(fileSystem, inFile, pool); // --select-lod picks which levels go into the lod-meta (default all). - perLevel = resolveLodLevels(options.lodSelect, multi.meta.numLods).map(lvl => selectLod(multi, lvl)); + perLevel = resolveLodLevels(options.lodSelect, multi.meta.numLods).map((lvl) => selectLod(multi, lvl)); inputActions = inputArgs[0].processActions; } else { // PLY inputs grouped by --tag-lod tag (env = -1, untagged = level 0); // each input's own actions applied before grouping. const tagged = inputArgs.map((a) => { - const ply = !isHttpUrl(a.filename) && getInputFormat(resolveInput(a.filename).classifyName) === 'ply'; - const lods = a.processActions.filter(act => act.kind === 'lod'); + const ply = + !isHttpUrl(a.filename) && getInputFormat(resolveInput(a.filename).classifyName) === 'ply'; + const lods = a.processActions.filter((act) => act.kind === 'lod'); const tag = lods.length > 0 ? (lods[lods.length - 1] as { value: number }).value : 0; const rest = stripLodTags(a.processActions); return { arg: a, ply, tag, rest }; }); - if (!tagged.every(t => t.ply)) { - throw new Error('lod-meta.json output requires a single streamed-SOG/LCC/LCC2 input, or local PLY input(s) (optionally --tag-lod tagged).'); - } - const opened = await Promise.all(tagged.map(async (t) => { - const { filename: inFile, fileSystem } = resolveInput(t.arg.filename); - const src = await processSourceBridged( - await readPly(await fileSystem.createSource(inFile), pool), - t.rest, pool, processOptions + if (!tagged.every((t) => t.ply)) { + throw new Error( + 'lod-meta.json output requires a single streamed-SOG/LCC/LCC2 input, or local PLY input(s) (optionally --tag-lod tagged).' ); - return { src, tag: t.tag }; - })); - const mains = opened.filter(o => o.tag >= 0); + } + const opened = await Promise.all( + tagged.map(async (t) => { + const { filename: inFile, fileSystem } = resolveInput(t.arg.filename); + const src = await processSourceBridged( + await readPly(await fileSystem.createSource(inFile), pool), + t.rest, + pool, + processOptions + ); + return { src, tag: t.tag }; + }) + ); + const mains = opened.filter((o) => o.tag >= 0); if (mains.length === 0) { throw new Error('No Gaussians to write'); } - const mainTags = [...new Set(mains.map(m => m.tag))].sort((a, b) => a - b); + const mainTags = [...new Set(mains.map((m) => m.tag))].sort((a, b) => a - b); perLevel = mainTags.map((tag) => { - const group = mains.filter(m => m.tag === tag).map(m => m.src); + const group = mains.filter((m) => m.tag === tag).map((m) => m.src); return group.length === 1 ? group[0] : concatSource(group, pool); }); - const envs = opened.filter(o => o.tag === -1).map(o => o.src); - envSource = envs.length === 0 ? null : (envs.length === 1 ? envs[0] : concatSource(envs, pool)); + const envs = opened.filter((o) => o.tag === -1).map((o) => o.src); + envSource = envs.length === 0 ? null : envs.length === 1 ? envs[0] : concatSource(envs, pool); } // Output (and single-input) actions apply PER LEVEL and to the env — // never across levels. const perLevelActions = stripLodTags([...inputActions, ...outputArg.processActions]); if (perLevelActions.length > 0) { - perLevel = await Promise.all(perLevel.map(s => processSourceBridged(s, perLevelActions, pool, processOptions))); + perLevel = await Promise.all( + perLevel.map((s) => processSourceBridged(s, perLevelActions, pool, processOptions)) + ); if (envSource) envSource = await processSourceBridged(envSource, perLevelActions, pool, processOptions); } @@ -1394,8 +1441,8 @@ const main = async () => { // bake to identity when per-input actions left transforms diverged. if (perLevel.length > 1) { const refTransform = perLevel[0].meta.transform; - if (!perLevel.every(s => s.meta.transform.equals(refTransform))) { - perLevel = perLevel.map(s => bakeTransform(s, Transform.IDENTITY)); + if (!perLevel.every((s) => s.meta.transform.equals(refTransform))) { + perLevel = perLevel.map((s) => bakeTransform(s, Transform.IDENTITY)); } } const mainSource = perLevel.length === 1 ? perLevel[0] : stackLods(perLevel); @@ -1405,16 +1452,21 @@ const main = async () => { } const phase = logger.group(`Output ${outputArg.filename}`, { index: phaseTotal, total: phaseTotal }); - logger.info(`${fmtCount(total)} gaussians · ${mainSource.meta.shBands} SH bands · ${mainSource.meta.numLods} LODs (streaming LOD)`); - await writeLodSource({ - filename: outputFilename, - mainSource, - envSource, - iterations: options.iterations, - createDevice: deviceCreator, - chunkCount: options.lodChunkCount, - chunkExtent: options.lodChunkExtent - }, new NodeFileSystem()); + logger.info( + `${fmtCount(total)} gaussians · ${mainSource.meta.shBands} SH bands · ${mainSource.meta.numLods} LODs (streaming LOD)` + ); + await writeLodSource( + { + filename: outputFilename, + mainSource, + envSource, + iterations: options.iterations, + createDevice: deviceCreator, + chunkCount: options.lodChunkCount, + chunkExtent: options.lodChunkExtent + }, + new NodeFileSystem() + ); await mainSource.close(); if (container) await container.close(); @@ -1428,7 +1480,9 @@ const main = async () => { // *tags*: tags build lod-meta.json levels and don't apply to single-scene // output. (Tag-free non-lod conversions and null output ran the single-scene // pipeline above; lod-meta output ran the LOD path.) - throw new Error('--tag-lod tags apply to lod-meta.json output; for single-scene output choose a level with --select-lod (-L).'); + throw new Error( + '--tag-lod tags apply to lod-meta.json output; for single-scene output choose a level with --select-lod (-L).' + ); } catch (err) { failExit(err); } diff --git a/src/cli/node-device.ts b/src/cli/node-device.ts index 7ae2350d..b7930ecb 100644 --- a/src/cli/node-device.ts +++ b/src/cli/node-device.ts @@ -1,4 +1,5 @@ -import { GraphicsDevice, WebgpuGraphicsDevice } from 'playcanvas'; +import type { GraphicsDevice } from 'playcanvas'; +import { WebgpuGraphicsDevice } from 'playcanvas'; import { create, globals } from 'webgpu'; import { logger } from '../lib'; @@ -76,7 +77,7 @@ let peakGpuBytes = 0; const getPeakGpuMemory = (): number => peakGpuBytes; // Cache enumerated adapters so we don't query Dawn multiple times -let cachedAdapters: Array<{ index: number; name: string }> | null = null; +let cachedAdapters: { index: number; name: string }[] | null = null; const enumerateAdapters = async () => { if (cachedAdapters) { diff --git a/src/cli/node-file-system.ts b/src/cli/node-file-system.ts index 3008260f..f5f88f67 100644 --- a/src/cli/node-file-system.ts +++ b/src/cli/node-file-system.ts @@ -1,16 +1,10 @@ import { randomBytes } from 'crypto'; -import { FileHandle, mkdir, open, rename, stat, unlink } from 'node:fs/promises'; +import type { FileHandle } from 'node:fs/promises'; +import { mkdir, open, rename, stat, unlink } from 'node:fs/promises'; import { basename, dirname, join } from 'node:path'; -import { - BufferedReadStream, - ReadStream, - type FileSystem, - type ProgressCallback, - type ReadFileSystem, - type ReadSource, - type Writer -} from '../lib'; +import { BufferedReadStream, ReadStream } from '../lib'; +import type { FileSystem, ProgressCallback, ReadFileSystem, ReadSource, Writer } from '../lib'; // ============================================================================ // Read implementations @@ -23,17 +17,11 @@ class NodeReadStream extends ReadStream { private fileHandle: FileHandle; private position: number; private end: number; - private closed: boolean = false; + private closed = false; private progress: ProgressCallback | undefined; private totalSize: number | undefined; - constructor( - fileHandle: FileHandle, - start: number, - end: number, - progress?: ProgressCallback, - totalSize?: number - ) { + constructor(fileHandle: FileHandle, start: number, end: number, progress?: ProgressCallback, totalSize?: number) { super(end - start); this.fileHandle = fileHandle; this.position = start; @@ -80,7 +68,7 @@ class NodeReadSource implements ReadSource { readonly seekable: boolean = true; private fileHandle: FileHandle; - private closed: boolean = false; + private closed = false; private progress: ProgressCallback | undefined; constructor(fileHandle: FileHandle, size: number, progress?: ProgressCallback) { @@ -89,7 +77,7 @@ class NodeReadSource implements ReadSource { this.progress = progress; } - read(start: number = 0, end: number = this.size): ReadStream { + read(start = 0, end: number = this.size): ReadStream { if (this.closed) { throw new Error('Source has been closed'); } @@ -99,14 +87,8 @@ class NodeReadSource implements ReadSource { const clampedEnd = Math.max(clampedStart, Math.min(end, this.size)); // Wrap with BufferedReadStream to reduce async overhead from file reads - const raw = new NodeReadStream( - this.fileHandle, - clampedStart, - clampedEnd, - this.progress, - this.size - ); - return new BufferedReadStream(raw, 4 * 1024 * 1024); // 4MB chunks + const raw = new NodeReadStream(this.fileHandle, clampedStart, clampedEnd, this.progress, this.size); + return new BufferedReadStream(raw, 4 * 1024 * 1024); // 4MB chunks } close(): void { @@ -165,7 +147,9 @@ class FileWriter implements Writer { this.abort = async () => { // discard: close and remove the temp file, never touching the // target filename (best-effort — this runs on already-failing paths) + // eslint-disable-next-line @typescript-eslint/no-empty-function -- preserve rejection-only control flow await fileHandle.close().catch(() => {}); + // eslint-disable-next-line @typescript-eslint/no-empty-function -- preserve rejection-only control flow await unlink(tmpFilename).catch(() => {}); }; } diff --git a/src/lib/chunk/data.ts b/src/lib/chunk/data.ts index 7ed42a55..7052cdbc 100644 --- a/src/lib/chunk/data.ts +++ b/src/lib/chunk/data.ts @@ -1,4 +1,4 @@ -import { type ChunkFieldMap, type ChunkLayer } from './layout'; +import type { ChunkFieldMap, ChunkLayer } from './layout'; /** * A CPU-resident buffer holding one layer's data for one chunk of gaussians. @@ -19,6 +19,7 @@ import { type ChunkFieldMap, type ChunkLayer } from './layout'; * `stride` is the bytes per gaussian, dictated by the layer (and, for `color` * and `other`, by the SH band count or extras schema). */ +// eslint-disable-next-line @typescript-eslint/consistent-type-definitions -- preserve public declaration merging interface ChunkData { /** Which layer this buffer holds. */ readonly layer: ChunkLayer; diff --git a/src/lib/chunk/in-memory.ts b/src/lib/chunk/in-memory.ts index 807dd39d..88184ae9 100644 --- a/src/lib/chunk/in-memory.ts +++ b/src/lib/chunk/in-memory.ts @@ -1,21 +1,19 @@ -import { type SplatModel } from '../splat-model'; -import { type Transform } from '../utils'; -import { type ChunkData } from './data'; +import type { SplatModel } from '../splat-model'; +import type { Transform } from '../utils'; + +import type { ChunkData } from './data'; import { colorFields, colorStride, GEOMETRIC_STRIDE, geometricFields, - type ExtraColumn, - type ChunkLayer, - type LayerLayout, otherLayout, positionFields, - POSITION_STRIDE, - type SHBands + POSITION_STRIDE } from './layout'; -import { type ChunkDataPool } from './pool'; -import { type ChunkSource, type ReadRequest, type ChunkSourceMetadata } from './source'; +import type { ExtraColumn, ChunkLayer, LayerLayout, SHBands } from './layout'; +import type { ChunkDataPool } from './pool'; +import type { ChunkSource, ReadRequest, ChunkSourceMetadata } from './source'; /** * Per-layer, per-LOD, per-chunk CPU-resident byte storage. @@ -23,9 +21,7 @@ import { type ChunkSource, type ReadRequest, type ChunkSourceMetadata } from './ * Indexed as `buffers[layer][lod][chunkIndex]`. `undefined` for layers the * source doesn't carry. Each stored buffer is exactly `count * stride` bytes. */ -type LayerChunkBuffers = { - [L in ChunkLayer]?: ReadonlyArray>; -}; +type LayerChunkBuffers = Partial>; /** * Lightweight CPU-resident source. Constructed directly from a set of @@ -43,7 +39,7 @@ class InMemoryChunkSource implements ChunkSource { private buffers: LayerChunkBuffers | null; // One u32 view per chunk buffer, per (layer, lod), built on first use and // reused — a gather pass touches every chunk, so views are made once. - private viewCache: Map = new Map(); + private viewCache = new Map(); constructor(meta: ChunkSourceMetadata, buffers: LayerChunkBuffers) { this.meta = meta; @@ -112,7 +108,7 @@ class InMemoryChunkSource implements ChunkSource { if (!layerBuffers) { throw new Error(`InMemoryChunkSource.gatherRows: layer '${layer}' not available`); } - views = layerBuffers[lod].map(b => new Uint32Array(b)); + views = layerBuffers[lod].map((b) => new Uint32Array(b)); this.viewCache.set(key, views); } return views; @@ -166,7 +162,7 @@ class InMemoryChunkSource implements ChunkSource { } } -type LayerBuffers = ReadonlyArray>; +type LayerBuffers = readonly (readonly ArrayBuffer[])[]; /** * Convenience constructor for an `InMemoryChunkSource` built from raw layer buffers. @@ -192,22 +188,19 @@ const createInMemoryChunkSource = (params: { numGaussians: number; chunkSize: number; shBands: SHBands; - extraColumns?: ReadonlyArray; + extraColumns?: readonly ExtraColumn[]; /** How the scene was trained; untagged sources are `default`. */ model?: SplatModel; transform: Transform; /** Gaussians per LOD. `lodCounts[0]` must equal `numGaussians`. */ - lodCounts: ReadonlyArray; + lodCounts: readonly number[]; /** Per-LOD per-chunk layer buffers, or `undefined` if the source lacks the layer. */ position?: LayerBuffers; geometric?: LayerBuffers; color?: LayerBuffers; other?: LayerBuffers; }): InMemoryChunkSource => { - const { - numGaussians, chunkSize, shBands, transform, lodCounts, - position, geometric, color, other - } = params; + const { numGaussians, chunkSize, shBands, transform, lodCounts, position, geometric, color, other } = params; const extras = params.extraColumns ?? []; if (lodCounts.length === 0) { @@ -220,7 +213,7 @@ const createInMemoryChunkSource = (params: { } const numLods = lodCounts.length; - const numChunks = lodCounts.map(c => Math.ceil(c / chunkSize)); + const numChunks = lodCounts.map((c) => Math.ceil(c / chunkSize)); const availableLayers = new Set(); const layouts: Partial> = {}; @@ -228,9 +221,7 @@ const createInMemoryChunkSource = (params: { const register = (layer: ChunkLayer, data: LayerBuffers, layout: LayerLayout): void => { if (data.length !== numLods) { - throw new Error( - `createInMemoryChunkSource: layer '${layer}' has ${data.length} LODs, expected ${numLods}` - ); + throw new Error(`createInMemoryChunkSource: layer '${layer}' has ${data.length} LODs, expected ${numLods}`); } for (let l = 0; l < numLods; l++) { if (data[l].length !== numChunks[l]) { @@ -288,15 +279,13 @@ const createInMemoryChunkSource = (params: { * @param pool - The `ChunkData` pool used for the temporary read buffers. * @returns A fresh `InMemoryChunkSource` holding the materialized data. */ -const compact = async ( - src: ChunkSource, - pool: ChunkDataPool -): Promise => { +const compact = async (src: ChunkSource, pool: ChunkDataPool): Promise => { const { meta } = src; - const layers: ChunkLayer[] = (['position', 'geometric', 'color', 'other'] as ChunkLayer[]) - .filter(l => meta.availableLayers.has(l)); + const layers: ChunkLayer[] = (['position', 'geometric', 'color', 'other'] as ChunkLayer[]).filter((l) => + meta.availableLayers.has(l) + ); - const out: { [L in ChunkLayer]?: ArrayBuffer[][] } = {}; + const out: Partial> = {}; for (const l of layers) { out[l] = meta.lodCounts.map((): ArrayBuffer[] => []); } diff --git a/src/lib/chunk/layout.ts b/src/lib/chunk/layout.ts index 01594212..7fa2d816 100644 --- a/src/lib/chunk/layout.ts +++ b/src/lib/chunk/layout.ts @@ -95,14 +95,14 @@ const colorFields = (shBands: SHBands): ChunkFieldMap => { }; /** Canonical layer order for reporting. */ -const LAYER_ORDER: ReadonlyArray = ['position', 'geometric', 'color', 'other']; +const LAYER_ORDER: readonly ChunkLayer[] = ['position', 'geometric', 'color', 'other']; /** * List a source's available layers in canonical ({@link LAYER_ORDER}) order. * @param layers - The source's available layers. * @returns The layers as an ordered array. */ -const orderedLayers = (layers: ReadonlySet): ChunkLayer[] => LAYER_ORDER.filter(l => layers.has(l)); +const orderedLayers = (layers: ReadonlySet): ChunkLayer[] => LAYER_ORDER.filter((l) => layers.has(l)); /** * Whether a source's available layers constitute gaussian splat data: @@ -129,9 +129,7 @@ type ExtraColumn = { * @param extras - The extra columns, in storage order. * @returns The byte stride and field map for the `other` layer. */ -const otherLayout = ( - extras: ReadonlyArray -): { stride: number; fields: ChunkFieldMap } => { +const otherLayout = (extras: readonly ExtraColumn[]): { stride: number; fields: ChunkFieldMap } => { const fields: Record = {}; let offset = 0; for (const e of extras) { diff --git a/src/lib/chunk/pool.ts b/src/lib/chunk/pool.ts index e01abcf9..070dd798 100644 --- a/src/lib/chunk/pool.ts +++ b/src/lib/chunk/pool.ts @@ -1,5 +1,7 @@ -import { type ChunkData, ChunkDataImpl } from './data'; -import { type ChunkLayer, type LayerLayout, DEFAULT_CHUNK_SIZE } from './layout'; +import { ChunkDataImpl } from './data'; +import type { ChunkData } from './data'; +import { DEFAULT_CHUNK_SIZE } from './layout'; +import type { ChunkLayer, LayerLayout } from './layout'; /** * A pool-backed allocator for {@link ChunkData} buffers. @@ -19,6 +21,7 @@ import { type ChunkLayer, type LayerLayout, DEFAULT_CHUNK_SIZE } from './layout' * collector) instead. Call {@link ChunkDataPool.trim} to free pooled buffers * down to a target. */ +// eslint-disable-next-line @typescript-eslint/consistent-type-definitions -- preserve public declaration merging interface ChunkDataPool { /** Gaussians-per-chunk granularity this pool allocates for. */ readonly chunkSize: number; @@ -53,9 +56,7 @@ interface ChunkDataPool { * @param options.maxPooledBytes - Cap on bytes held in the free list (default 2 GB). * @returns A new {@link ChunkDataPool}. */ -const createChunkDataPool = ( - options?: { chunkSize?: number; maxPooledBytes?: number } -): ChunkDataPool => { +const createChunkDataPool = (options?: { chunkSize?: number; maxPooledBytes?: number }): ChunkDataPool => { const chunkSize = options?.chunkSize ?? DEFAULT_CHUNK_SIZE; const maxPooledBytes = options?.maxPooledBytes ?? 2 * 1024 * 1024 * 1024; diff --git a/src/lib/chunk/source.ts b/src/lib/chunk/source.ts index 5497ae64..2e94fafa 100644 --- a/src/lib/chunk/source.ts +++ b/src/lib/chunk/source.ts @@ -1,7 +1,8 @@ -import { type SplatModel } from '../splat-model'; -import { type Transform } from '../utils'; -import { type ChunkData } from './data'; -import { type ExtraColumn, type ChunkLayer, type SHBands, type LayerLayout } from './layout'; +import type { SplatModel } from '../splat-model'; +import type { Transform } from '../utils'; + +import type { ChunkData } from './data'; +import type { ExtraColumn, ChunkLayer, SHBands, LayerLayout } from './layout'; /** * Static description of a {@link ChunkSource}'s contents — what's in it and @@ -21,17 +22,17 @@ type ChunkSourceMetadata = { readonly numGaussians: number; readonly numLods: number; /** Gaussian counts per LOD. `lodCounts[0]` matches `numGaussians`. */ - readonly lodCounts: ReadonlyArray; + readonly lodCounts: readonly number[]; /** Gaussians per chunk (all chunks are this size except the last per LOD). */ readonly chunkSize: number; /** Number of chunks per LOD: `Math.ceil(lodCounts[lod] / chunkSize)`. */ - readonly numChunks: ReadonlyArray; + readonly numChunks: readonly number[]; /** SH band count present in the source. */ readonly shBands: SHBands; /** How the scene was trained, and so how it must be evaluated. */ readonly model: SplatModel; /** Extra non-standard columns mapped to the `other` layer. */ - readonly extraColumns: ReadonlyArray; + readonly extraColumns: readonly ExtraColumn[]; /** Coordinate-space transform; applied lazily when consumed. */ readonly transform: Transform; /** Which layers the source can serve. */ @@ -92,6 +93,7 @@ type ReadRequest = * `ChunkDataPool`, filled by `read`, used, and released back to the pool. The * source itself never holds long-lived buffer memory on the caller's behalf. */ +// eslint-disable-next-line @typescript-eslint/consistent-type-definitions -- preserve public declaration merging interface ChunkSource { readonly meta: ChunkSourceMetadata; diff --git a/src/lib/compat/data-table.ts b/src/lib/compat/data-table.ts index d81e5db9..07af1e6b 100644 --- a/src/lib/compat/data-table.ts +++ b/src/lib/compat/data-table.ts @@ -1,19 +1,17 @@ -import { - createInMemoryChunkSource, +import { createInMemoryChunkSource, SH_REST_COUNTS, DEFAULT_CHUNK_SIZE } from '../chunk'; +import type { InMemoryChunkSource, - SH_REST_COUNTS, - DEFAULT_CHUNK_SIZE, - type ExtraColumn, - type ChunkData, - type ChunkDataPool, - type ChunkSource, - type ChunkSourceMetadata, - type ChunkLayer, - type SHBands + ExtraColumn, + ChunkData, + ChunkDataPool, + ChunkSource, + ChunkSourceMetadata, + ChunkLayer, + SHBands } from '../chunk'; import { Column, DataTable } from '../data-table'; -import { type SplatModel } from '../splat-model'; -import { type Transform } from '../utils'; +import type { SplatModel } from '../splat-model'; +import type { Transform } from '../utils'; /** * The legacy `DataTable` <-> `ChunkSource` compatibility bridge. @@ -33,18 +31,10 @@ import { type Transform } from '../utils'; /** Standard column names that map directly to the canonical layers. */ const POSITION_COLS = ['x', 'y', 'z'] as const; -const GEOMETRIC_COLS = [ - 'rot_0', 'rot_1', 'rot_2', 'rot_3', - 'scale_0', 'scale_1', 'scale_2', - 'opacity' -] as const; +const GEOMETRIC_COLS = ['rot_0', 'rot_1', 'rot_2', 'rot_3', 'scale_0', 'scale_1', 'scale_2', 'opacity'] as const; const COLOR_DC_COLS = ['f_dc_0', 'f_dc_1', 'f_dc_2'] as const; -const standardColumnSet = new Set([ - ...POSITION_COLS, - ...GEOMETRIC_COLS, - ...COLOR_DC_COLS -]); +const standardColumnSet = new Set([...POSITION_COLS, ...GEOMETRIC_COLS, ...COLOR_DC_COLS]); /** * Enumerate the canonical column names a source exposes, in the same order @@ -97,9 +87,7 @@ const detectExtras = (dataTable: DataTable): ExtraColumn[] => { for (const c of dataTable.columns) { if (standardColumnSet.has(c.name)) continue; if (/^f_rest_\d+$/.test(c.name)) continue; - const type: 'float32' | 'uint32' = ( - c.dataType === 'float32' || c.dataType === 'float64' - ) ? 'float32' : 'uint32'; + const type: 'float32' | 'uint32' = c.dataType === 'float32' || c.dataType === 'float64' ? 'float32' : 'uint32'; extras.push({ name: c.name, type }); } return extras; @@ -126,10 +114,7 @@ const splitToChunks = ( let rowOffset = 0; while (rowsRemaining > 0) { const rows = Math.min(chunkSize, rowsRemaining); - const slice = interleaved.subarray( - rowOffset * elemsPerRow, - (rowOffset + rows) * elemsPerRow - ); + const slice = interleaved.subarray(rowOffset * elemsPerRow, (rowOffset + rows) * elemsPerRow); // Copy into a fresh ArrayBuffer so each chunk owns its bytes. const ab = new ArrayBuffer(rows * elemsPerRow * 4); if (isFloat) { @@ -177,81 +162,98 @@ const dataTableToChunkSource = ( const extras = detectExtras(dataTable); const transform: Transform = dataTable.transform; - const hasPosition = POSITION_COLS.every(c => dataTable.hasColumn(c)); - const hasGeometric = GEOMETRIC_COLS.every(c => dataTable.hasColumn(c)); - const hasColor = COLOR_DC_COLS.every(c => dataTable.hasColumn(c)); + const hasPosition = POSITION_COLS.every((c) => dataTable.hasColumn(c)); + const hasGeometric = GEOMETRIC_COLS.every((c) => dataTable.hasColumn(c)); + const hasColor = COLOR_DC_COLS.every((c) => dataTable.hasColumn(c)); const hasOther = extras.length > 0; const col = (name: string): Float32Array => dataTable.getColumnByName(name)!.data as Float32Array; const srcRow = (i: number): number => (indices ? indices[i] : i); - const positionChunks: ArrayBuffer[] | undefined = hasPosition ? (() => { - const arr = new Float32Array(count * 3); - const x = col('x'), y = col('y'), z = col('z'); - for (let i = 0; i < count; i++) { - const s = srcRow(i); - arr[i * 3 + 0] = x[s]; - arr[i * 3 + 1] = y[s]; - arr[i * 3 + 2] = z[s]; - } - return splitToChunks(arr, count, 3, chunkSize); - })() : undefined; - - const geometricChunks: ArrayBuffer[] | undefined = hasGeometric ? (() => { - const arr = new Float32Array(count * 8); - const r0 = col('rot_0'), r1 = col('rot_1'), r2 = col('rot_2'), r3 = col('rot_3'); - const s0 = col('scale_0'), s1 = col('scale_1'), s2 = col('scale_2'); - const op = col('opacity'); - for (let i = 0; i < count; i++) { - const s = srcRow(i); - const o = i * 8; - arr[o + 0] = r0[s]; - arr[o + 1] = r1[s]; - arr[o + 2] = r2[s]; - arr[o + 3] = r3[s]; - arr[o + 4] = s0[s]; - arr[o + 5] = s1[s]; - arr[o + 6] = s2[s]; - arr[o + 7] = op[s]; - } - return splitToChunks(arr, count, 8, chunkSize); - })() : undefined; - - const colorChunks: ArrayBuffer[] | undefined = hasColor ? (() => { - const elemsPerRow = 3 + numRest; - const arr = new Float32Array(count * elemsPerRow); - const dc0 = col('f_dc_0'), dc1 = col('f_dc_1'), dc2 = col('f_dc_2'); - const restCols: Float32Array[] = []; - for (let r = 0; r < numRest; r++) restCols.push(col(`f_rest_${r}`)); - for (let i = 0; i < count; i++) { - const s = srcRow(i); - const o = i * elemsPerRow; - arr[o + 0] = dc0[s]; - arr[o + 1] = dc1[s]; - arr[o + 2] = dc2[s]; - for (let r = 0; r < numRest; r++) arr[o + 3 + r] = restCols[r][s]; - } - return splitToChunks(arr, count, elemsPerRow, chunkSize); - })() : undefined; - - const otherChunks: ArrayBuffer[] | undefined = hasOther ? (() => { - const elemsPerRow = extras.length; - const arr = new Uint32Array(count * elemsPerRow); - const f32View = new Float32Array(arr.buffer); - const cols = extras.map(e => dataTable.getColumnByName(e.name)!.data); - for (let i = 0; i < count; i++) { - const s = srcRow(i); - const o = i * elemsPerRow; - for (let e = 0; e < elemsPerRow; e++) { - if (extras[e].type === 'float32') { - f32View[o + e] = cols[e][s] as number; - } else { - arr[o + e] = cols[e][s] as number; - } - } - } - return splitToChunks(arr, count, elemsPerRow, chunkSize); - })() : undefined; + const positionChunks: ArrayBuffer[] | undefined = hasPosition + ? (() => { + const arr = new Float32Array(count * 3); + const x = col('x'), + y = col('y'), + z = col('z'); + for (let i = 0; i < count; i++) { + const s = srcRow(i); + arr[i * 3 + 0] = x[s]; + arr[i * 3 + 1] = y[s]; + arr[i * 3 + 2] = z[s]; + } + return splitToChunks(arr, count, 3, chunkSize); + })() + : undefined; + + const geometricChunks: ArrayBuffer[] | undefined = hasGeometric + ? (() => { + const arr = new Float32Array(count * 8); + const r0 = col('rot_0'), + r1 = col('rot_1'), + r2 = col('rot_2'), + r3 = col('rot_3'); + const s0 = col('scale_0'), + s1 = col('scale_1'), + s2 = col('scale_2'); + const op = col('opacity'); + for (let i = 0; i < count; i++) { + const s = srcRow(i); + const o = i * 8; + arr[o + 0] = r0[s]; + arr[o + 1] = r1[s]; + arr[o + 2] = r2[s]; + arr[o + 3] = r3[s]; + arr[o + 4] = s0[s]; + arr[o + 5] = s1[s]; + arr[o + 6] = s2[s]; + arr[o + 7] = op[s]; + } + return splitToChunks(arr, count, 8, chunkSize); + })() + : undefined; + + const colorChunks: ArrayBuffer[] | undefined = hasColor + ? (() => { + const elemsPerRow = 3 + numRest; + const arr = new Float32Array(count * elemsPerRow); + const dc0 = col('f_dc_0'), + dc1 = col('f_dc_1'), + dc2 = col('f_dc_2'); + const restCols: Float32Array[] = []; + for (let r = 0; r < numRest; r++) restCols.push(col(`f_rest_${r}`)); + for (let i = 0; i < count; i++) { + const s = srcRow(i); + const o = i * elemsPerRow; + arr[o + 0] = dc0[s]; + arr[o + 1] = dc1[s]; + arr[o + 2] = dc2[s]; + for (let r = 0; r < numRest; r++) arr[o + 3 + r] = restCols[r][s]; + } + return splitToChunks(arr, count, elemsPerRow, chunkSize); + })() + : undefined; + + const otherChunks: ArrayBuffer[] | undefined = hasOther + ? (() => { + const elemsPerRow = extras.length; + const arr = new Uint32Array(count * elemsPerRow); + const f32View = new Float32Array(arr.buffer); + const cols = extras.map((e) => dataTable.getColumnByName(e.name)!.data); + for (let i = 0; i < count; i++) { + const s = srcRow(i); + const o = i * elemsPerRow; + for (let e = 0; e < elemsPerRow; e++) { + if (extras[e].type === 'float32') { + f32View[o + e] = cols[e][s] as number; + } else { + arr[o + e] = cols[e][s] as number; + } + } + } + return splitToChunks(arr, count, elemsPerRow, chunkSize); + })() + : undefined; return createInMemoryChunkSource({ numGaussians: count, @@ -322,11 +324,13 @@ const materializeToDataTable = async ( const numRest = SH_REST_COUNTS[meta.shBands]; const restArrays: Float32Array[] = wantsColor ? Array.from({ length: numRest }, () => new Float32Array(N)) : []; - const extraArrays = wantsOther ? meta.extraColumns.map(e => ({ - name: e.name, - type: e.type, - data: e.type === 'float32' ? new Float32Array(N) : new Uint32Array(N) - })) : []; + const extraArrays = wantsOther + ? meta.extraColumns.map((e) => ({ + name: e.name, + type: e.type, + data: e.type === 'float32' ? new Float32Array(N) : new Uint32Array(N) + })) + : []; const chunkSize = meta.chunkSize; @@ -350,8 +354,12 @@ const materializeToDataTable = async ( const layouts = meta.layouts; const acquired: { layer: ChunkLayer; chunkData: ChunkData }[] = []; const req: { - chunkIndex: number; lod: number; - position?: ChunkData; geometric?: ChunkData; color?: ChunkData; other?: ChunkData; + chunkIndex: number; + lod: number; + position?: ChunkData; + geometric?: ChunkData; + color?: ChunkData; + other?: ChunkData; } = { chunkIndex: k, lod }; if (wantsPosition) { @@ -418,7 +426,8 @@ const materializeToDataTable = async ( restArrays[r][di] = f32[si + 3 + r]; } } - } else { // 'other' + } else { + // 'other' const f32 = new Float32Array(chunkData.data, 0, count * elemsPerRow); const u32 = new Uint32Array(chunkData.data, 0, count * elemsPerRow); const cols = extraArrays.length; @@ -455,11 +464,7 @@ const materializeToDataTable = async ( ); } if (wantsColor) { - columns.push( - new Column('f_dc_0', dc0!), - new Column('f_dc_1', dc1!), - new Column('f_dc_2', dc2!) - ); + columns.push(new Column('f_dc_0', dc0!), new Column('f_dc_1', dc1!), new Column('f_dc_2', dc2!)); for (let r = 0; r < numRest; r++) { columns.push(new Column(`f_rest_${r}`, restArrays[r])); } diff --git a/src/lib/data-table/combine.ts b/src/lib/data-table/combine.ts index 9721139c..10a697a2 100644 --- a/src/lib/data-table/combine.ts +++ b/src/lib/data-table/combine.ts @@ -1,7 +1,9 @@ -import { Column, DataTable, TypedArray } from './data-table'; -import { convertToSpace } from './transform'; import { logger, Transform } from '../utils'; +import type { TypedArray } from './data-table'; +import { Column, DataTable } from './data-table'; +import { convertToSpace } from './transform'; + /** * Combines multiple DataTables into a single DataTable. * @@ -21,28 +23,27 @@ import { logger, Transform } from '../utils'; * console.log(combined.numRows); // tableA.numRows + tableB.numRows + tableC.numRows * ``` */ -const combine = (dataTables: DataTable[]) : DataTable => { +const combine = (dataTables: DataTable[]): DataTable => { if (dataTables.length === 1) { return dataTables[0]; } // Check if all transforms match the first table's transform const refTransform = dataTables[0].transform; - const allMatch = dataTables.every(dt => dt.transform.equals(refTransform)); + const allMatch = dataTables.every((dt) => dt.transform.equals(refTransform)); let tables = dataTables; let resultTransform = refTransform; if (!allMatch) { logger.warn('Combining DataTables with different source transforms; converting to engine space.'); - tables = dataTables.map(dt => convertToSpace(dt, Transform.IDENTITY)); + tables = dataTables.map((dt) => convertToSpace(dt, Transform.IDENTITY)); resultTransform = Transform.IDENTITY; } const findMatchingColumn = (columns: Column[], column: Column) => { for (let i = 0; i < columns.length; ++i) { - if (columns[i].name === column.name && - columns[i].dataType === column.dataType) { + if (columns[i].name === column.name && columns[i].dataType === column.dataType) { return columns[i]; } } diff --git a/src/lib/data-table/data-table.ts b/src/lib/data-table/data-table.ts index f4562641..f76beacc 100644 --- a/src/lib/data-table/data-table.ts +++ b/src/lib/data-table/data-table.ts @@ -3,7 +3,8 @@ import { Transform } from '../utils'; /** * Union of all typed array types supported for column data. */ -type TypedArray = Int8Array | Uint8Array | Int16Array | Uint16Array | Int32Array | Uint32Array | Float32Array | Float64Array; +type TypedArray = + Int8Array | Uint8Array | Int16Array | Uint16Array | Int32Array | Uint32Array | Float32Array | Float64Array; /** * String identifiers for typed array element types. @@ -33,14 +34,22 @@ class Column { get dataType(): ColumnType | null { switch (this.data.constructor) { - case Int8Array: return 'int8'; - case Uint8Array: return 'uint8'; - case Int16Array: return 'int16'; - case Uint16Array: return 'uint16'; - case Int32Array: return 'int32'; - case Uint32Array: return 'uint32'; - case Float32Array: return 'float32'; - case Float64Array: return 'float64'; + case Int8Array: + return 'int8'; + case Uint8Array: + return 'uint8'; + case Int16Array: + return 'int16'; + case Uint16Array: + return 'uint16'; + case Int32Array: + return 'int32'; + case Uint32Array: + return 'uint32'; + case Float32Array: + return 'float32'; + case Float64Array: + return 'float64'; } return null; } @@ -54,9 +63,7 @@ class Column { * A row object mapping column names to numeric values. * @ignore */ -type Row = { - [colName: string]: number; -}; +type Row = Record; /** * A table of columnar data representing Gaussian splat properties. @@ -96,7 +103,9 @@ class DataTable { // check all columns have the same lengths for (let i = 1; i < columns.length; i++) { if (columns[i].data.length !== columns[0].data.length) { - throw new Error(`Column '${columns[i].name}' has inconsistent number of rows: expected ${columns[0].data.length}, got ${columns[i].data.length}`); + throw new Error( + `Column '${columns[i].name}' has inconsistent number of rows: expected ${columns[0].data.length}, got ${columns[i].data.length}` + ); } } @@ -119,7 +128,7 @@ class DataTable { setRow(index: number, row: Row, columns = this.columns) { for (const column of columns) { - if (row.hasOwnProperty(column.name)) { + if (Object.hasOwn(row, column.name)) { column.data[index] = row[column.name]; } } @@ -140,15 +149,15 @@ class DataTable { } get columnNames() { - return this.columns.map(column => column.name); + return this.columns.map((column) => column.name); } get columnData() { - return this.columns.map(column => column.data); + return this.columns.map((column) => column.data); } get columnTypes() { - return this.columns.map(column => column.dataType); + return this.columns.map((column) => column.dataType); } getColumn(index: number): Column { @@ -156,26 +165,28 @@ class DataTable { } getColumnIndex(name: string): number { - return this.columns.findIndex(column => column.name === name); + return this.columns.findIndex((column) => column.name === name); } getColumnByName(name: string): Column | null { - return this.columns.find(column => column.name === name); + return this.columns.find((column) => column.name === name); } hasColumn(name: string): boolean { - return this.columns.some(column => column.name === name); + return this.columns.some((column) => column.name === name); } addColumn(column: Column) { if (column.data.length !== this.numRows) { - throw new Error(`Column '${column.name}' has inconsistent number of rows: expected ${this.numRows}, got ${column.data.length}`); + throw new Error( + `Column '${column.name}' has inconsistent number of rows: expected ${this.numRows}, got ${column.data.length}` + ); } this.columns.push(column); } removeColumn(name: string) { - const index = this.columns.findIndex(column => column.name === name); + const index = this.columns.findIndex((column) => column.name === name); if (index === -1) { return false; } @@ -216,24 +227,30 @@ class DataTable { throw new Error(`DataTable.clone: duplicate column name(s): ${[...new Set(dupes)].join(', ')}`); } - const missingColumns = columnNames.filter(name => !this.getColumnByName(name)); + const missingColumns = columnNames.filter((name) => !this.getColumnByName(name)); if (missingColumns.length > 0) { throw new Error(`DataTable.clone: unknown column name(s): ${missingColumns.join(', ')}`); } - srcColumns = columnNames.map(name => this.getColumnByName(name)!); + srcColumns = columnNames.map((name) => this.getColumnByName(name)!); } else { srcColumns = this.columns; } if (!rows) { - return new DataTable(srcColumns.map(c => c.clone()), this.transform); + return new DataTable( + srcColumns.map((c) => c.clone()), + this.transform + ); } - const result = new DataTable(srcColumns.map((c) => { - const constructor = c.data.constructor as new (length: number) => TypedArray; - return new Column(c.name, new constructor(rows.length)); - }), this.transform); + const result = new DataTable( + srcColumns.map((c) => { + const constructor = c.data.constructor as new (length: number) => TypedArray; + return new Column(c.name, new constructor(rows.length)); + }), + this.transform + ); for (let i = 0; i < result.numColumns; ++i) { const src = srcColumns[i].data; diff --git a/src/lib/data-table/gaussian-aabb.ts b/src/lib/data-table/gaussian-aabb.ts index 28dc9615..20450827 100644 --- a/src/lib/data-table/gaussian-aabb.ts +++ b/src/lib/data-table/gaussian-aabb.ts @@ -1,21 +1,22 @@ import { BoundingBox, Mat4, Quat, Vec3 } from 'playcanvas'; -import { Column, DataTable } from './data-table'; import { SIGMA_CUTOFF } from '../render/config'; import { fmtCount, logger } from '../utils'; +import { Column, DataTable } from './data-table'; + /** * Bounds specification with min/max Vec3. */ -interface Bounds { +type Bounds = { min: Vec3; max: Vec3; -} +}; /** * Result of computing Gaussian extents. */ -interface GaussianExtentsResult { +type GaussianExtentsResult = { /** * DataTable containing extent_x, extent_y, extent_z columns. * To compute AABB for Gaussian i: @@ -30,7 +31,7 @@ interface GaussianExtentsResult { /** Number of Gaussians skipped due to invalid values */ invalidCount: number; -} +}; /** * Compute axis-aligned bounding box half-extents for all Gaussians in a DataTable. @@ -154,8 +155,6 @@ const computeGaussianExtents = (dataTable: DataTable): GaussianExtentsResult => }; }; -export { - computeGaussianExtents -}; +export { computeGaussianExtents }; export type { Bounds, GaussianExtentsResult }; diff --git a/src/lib/data-table/gaussian-eval.ts b/src/lib/data-table/gaussian-eval.ts index d88c5ffc..5376d7ac 100644 --- a/src/lib/data-table/gaussian-eval.ts +++ b/src/lib/data-table/gaussian-eval.ts @@ -1,11 +1,12 @@ -import { TypedArray } from './data-table'; import { sigmoid } from '../utils'; +import type { TypedArray } from './data-table'; + /** * Pre-computed per-Gaussian inverse transform data for evaluating * Gaussian contribution at arbitrary 3D points. */ -interface GaussianInverseTransform { +type GaussianInverseTransform = { /** Inverse rotation quaternion (w, x, y, z) with xyz negated */ qw: number; qx: number; @@ -17,7 +18,7 @@ interface GaussianInverseTransform { isz: number; /** Linear opacity (sigmoid of raw logit) */ alpha: number; -} +}; /** * Compute the inverse transform for a single Gaussian. @@ -33,8 +34,13 @@ interface GaussianInverseTransform { * @returns Inverse transform parameters. */ const computeGaussianInverse = ( - rotW: number, rotX: number, rotY: number, rotZ: number, - logScaleX: number, logScaleY: number, logScaleZ: number, + rotW: number, + rotX: number, + rotY: number, + rotZ: number, + logScaleX: number, + logScaleY: number, + logScaleZ: number, opacityLogit: number ): GaussianInverseTransform => { const qlen = Math.sqrt(rotW * rotW + rotX * rotX + rotY * rotY + rotZ * rotZ); @@ -68,8 +74,12 @@ const computeGaussianInverse = ( */ const evaluateGaussianAt = ( g: GaussianInverseTransform, - px: number, py: number, pz: number, - vx: number, vy: number, vz: number + px: number, + py: number, + pz: number, + vx: number, + vy: number, + vz: number ): number => { const dx = vx - px; const dy = vy - py; @@ -94,7 +104,7 @@ const evaluateGaussianAt = ( /** * Column arrays needed for Gaussian contribution evaluation. */ -interface GaussianColumns { +type GaussianColumns = { posX: TypedArray; posY: TypedArray; posZ: TypedArray; @@ -109,11 +119,6 @@ interface GaussianColumns { extentX: TypedArray; extentY: TypedArray; extentZ: TypedArray; -} - -export { - evaluateGaussianAt, - computeGaussianInverse, - type GaussianColumns, - type GaussianInverseTransform }; + +export { evaluateGaussianAt, computeGaussianInverse, type GaussianColumns, type GaussianInverseTransform }; diff --git a/src/lib/data-table/morton-order.ts b/src/lib/data-table/morton-order.ts index 57f9cf1e..5707e8b5 100644 --- a/src/lib/data-table/morton-order.ts +++ b/src/lib/data-table/morton-order.ts @@ -1,7 +1,8 @@ -import { DataTable } from './data-table'; import { sortMortonColumns } from '../ops/morton-order'; import { logger } from '../utils'; +import type { DataTable } from './data-table'; + // Sort the provided indices into morton order using the table's x/y/z columns. // A thin DataTable adapter over the shared columnar core in ops/morton-order. const sortMortonOrder = (dataTable: DataTable, indices: Uint32Array): void => { diff --git a/src/lib/data-table/sh-bands.ts b/src/lib/data-table/sh-bands.ts index 53800f95..ebc6a56f 100644 --- a/src/lib/data-table/sh-bands.ts +++ b/src/lib/data-table/sh-bands.ts @@ -17,7 +17,7 @@ const shRestNames = new Array(45).fill('').map((_, i) => `f_rest_${i}`); * @returns The number of SH bands (0-3). */ const getSHBands = (dataTable: DataTable): number => { - const idx = shRestNames.findIndex(v => !dataTable.hasColumn(v)); + const idx = shRestNames.findIndex((v) => !dataTable.hasColumn(v)); return ({ '9': 1, '24': 2, '-1': 3 } as Record)[String(idx)] ?? 0; }; diff --git a/src/lib/data-table/transform.ts b/src/lib/data-table/transform.ts index 28241f2b..08244dd7 100644 --- a/src/lib/data-table/transform.ts +++ b/src/lib/data-table/transform.ts @@ -1,7 +1,10 @@ import { Mat3, Quat, Vec3 } from 'playcanvas'; -import { Column, DataTable, TypedArray } from './data-table'; -import { Transform, RotateSH } from '../utils'; +import type { Transform } from '../utils'; +import { RotateSH } from '../utils'; + +import type { TypedArray } from './data-table'; +import { Column, DataTable } from './data-table'; const shNames = new Array(45).fill('').map((_, i) => `f_rest_${i}`); @@ -28,7 +31,11 @@ const computeWriteTransform = (transform: Transform, outputFormatTransform: Tran * @ignore */ const detectSHBands = (dataTable: DataTable): number => { - return ({ '9': 1, '24': 2, '-1': 3 } as Record)[String(shNames.findIndex(n => !dataTable.hasColumn(n)))] ?? 0; + return ( + ({ '9': 1, '24': 2, '-1': 3 } as Record)[ + String(shNames.findIndex((n) => !dataTable.hasColumn(n))) + ] ?? 0 + ); }; /** @@ -42,7 +49,12 @@ const detectSHBands = (dataTable: DataTable): number => { * @param inPlace - If true, mutate the DataTable's existing column arrays instead of allocating new ones. * @returns A map of column name to typed array. */ -const transformColumns = (dataTable: DataTable, columnNames: string[], delta: Transform | null, inPlace = false): Map => { +const transformColumns = ( + dataTable: DataTable, + columnNames: string[], + delta: Transform | null, + inPlace = false +): Map => { const result = new Map(); if (!delta || delta.isIdentity()) { @@ -62,16 +74,17 @@ const transformColumns = (dataTable: DataTable, columnNames: string[], delta: Tr const rotNames = ['rot_0', 'rot_1', 'rot_2', 'rot_3']; const scaleNames = ['scale_0', 'scale_1', 'scale_2']; - const hasPos = posNames.every(n => dataTable.hasColumn(n)); - const needPos = hasPos && posNames.some(n => columnNames.includes(n)); - const hasRot = rotNames.every(n => dataTable.hasColumn(n)); - const needRot = hasRot && rotNames.some(n => columnNames.includes(n)); - const needScale = scaleNames.some(n => columnNames.includes(n) && dataTable.hasColumn(n)) && s !== 1; + const hasPos = posNames.every((n) => dataTable.hasColumn(n)); + const needPos = hasPos && posNames.some((n) => columnNames.includes(n)); + const hasRot = rotNames.every((n) => dataTable.hasColumn(n)); + const needRot = hasRot && rotNames.some((n) => columnNames.includes(n)); + const needScale = scaleNames.some((n) => columnNames.includes(n) && dataTable.hasColumn(n)) && s !== 1; const shBands = detectSHBands(dataTable); const shCoeffsPerChannel = [0, 3, 8, 15][shBands]; const rotIsIdentity = Math.abs(Math.abs(r.w) - 1) < 1e-6; - const requestedSH = shBands > 0 && !rotIsIdentity && shNames.slice(0, shCoeffsPerChannel * 3).some(n => columnNames.includes(n)); + const requestedSH = + shBands > 0 && !rotIsIdentity && shNames.slice(0, shCoeffsPerChannel * 3).some((n) => columnNames.includes(n)); // Position columns if (needPos) { @@ -212,11 +225,10 @@ const convertToSpace = (dataTable: DataTable, targetTransform: Transform, inPlac const allNames = dataTable.columnNames; const cols = transformColumns(dataTable, allNames, delta); - return new DataTable(allNames.map(name => new Column(name, cols.get(name)!)), targetTransform); + return new DataTable( + allNames.map((name) => new Column(name, cols.get(name)!)), + targetTransform + ); }; -export { - transformColumns, - computeWriteTransform, - convertToSpace -}; +export { transformColumns, computeWriteTransform, convertToSpace }; diff --git a/src/lib/decimate-uniform/README.md b/src/lib/decimate-uniform/README.md index f671cc3b..4849b6b6 100644 --- a/src/lib/decimate-uniform/README.md +++ b/src/lib/decimate-uniform/README.md @@ -68,16 +68,17 @@ to be deliberate rather than incidental. Before landing one: reference binary's `--decimate` and this tree's `--decimate-uniform`, compares byte for byte, reports PSNR for both, and exits non-zero on any mismatch: - ```bash - node tools/decimate-parity.mjs sky --ref splat-transform - node tools/decimate-parity.mjs snow --ref splat-transform - ``` - - Equivalence was last verified against 3.1.6 on both study scenes, `fr-sky` - (5.81M, 3 SH bands, multi-block) and `fr-snow` (26.1M, DC only, 13 blocks), - six chained halvings each: every level byte-identical, PSNR matching the - published `old` columns exactly. -- If output *should* change, re-baseline `scenes/DECIMATION-RESULTS.md` — the + ```bash + node tools/decimate-parity.mjs sky --ref splat-transform + node tools/decimate-parity.mjs snow --ref splat-transform + ``` + + Equivalence was last verified against 3.1.6 on both study scenes, `fr-sky` + (5.81M, 3 SH bands, multi-block) and `fr-snow` (26.1M, DC only, 13 blocks), + six chained halvings each: every level byte-identical, PSNR matching the + published `old` columns exactly. + +- If output _should_ change, re-baseline `scenes/DECIMATION-RESULTS.md` — the `old` column is this path, and the study's conclusions are stated relative to it. That document is local-only (`scenes/` is gitignored). - Repin the digest in `test/decimate-uniform-parity.test.mjs`, which is the diff --git a/src/lib/decimate-uniform/block-producer.ts b/src/lib/decimate-uniform/block-producer.ts index b45ba9df..e5b3ced2 100644 --- a/src/lib/decimate-uniform/block-producer.ts +++ b/src/lib/decimate-uniform/block-producer.ts @@ -1,4 +1,4 @@ -import { type ChunkData, type ChunkLayer, type ChunkSource, type ChunkSourceMetadata, type ReadRequest } from '../chunk'; +import type { ChunkData, ChunkLayer, ChunkSource, ChunkSourceMetadata, ReadRequest } from '../chunk'; /** * One output chunk of the merge stream. The views hold exactly `count` @@ -61,7 +61,9 @@ const createBlockProducerSource = ( const payload = value; const expected = Math.min(meta.chunkSize, meta.numGaussians - request.chunkIndex * meta.chunkSize); if (payload.count !== expected) { - throw new Error(`decimate output chunk ${request.chunkIndex}: expected ${expected} rows, produced ${payload.count}`); + throw new Error( + `decimate output chunk ${request.chunkIndex}: expected ${expected} rows, produced ${payload.count}` + ); } const fill = (cd: ChunkData | undefined, layer: ChunkLayer): void => { diff --git a/src/lib/decimate-uniform/decimate-source.ts b/src/lib/decimate-uniform/decimate-source.ts index 5e99503b..8b044c2b 100644 --- a/src/lib/decimate-uniform/decimate-source.ts +++ b/src/lib/decimate-uniform/decimate-source.ts @@ -1,26 +1,25 @@ import { join } from 'pathe'; -import { type GraphicsDevice } from 'playcanvas'; +import type { GraphicsDevice } from 'playcanvas'; -import { createBlockProducerSource } from './block-producer'; -import { mergeStream } from './merge-stream'; -import { kdPartition, coherenceRuns, type ResidentPositions } from './partition'; -import { runPriorityPass, HALO_CAP, type CandidateArrays } from './priority'; -import { selectMerges } from './select'; -import { - compact, - type ChunkDataPool, - type ChunkSource, - type ChunkSourceMetadata -} from '../chunk'; -import { APP_CHUNK } from './gpu-edge-cost'; -import { type ReadFileSystem } from '../io/read'; -import { type FileSystem } from '../io/write'; +import { compact } from '../chunk'; +import type { ChunkDataPool, ChunkSource, ChunkSourceMetadata } from '../chunk'; +import type { ReadFileSystem } from '../io/read'; +import type { FileSystem } from '../io/write'; import { bakeTransform } from '../ops'; import { readPly } from '../readers/read-ply'; -import { type DeviceCreator } from '../types'; +import type { DeviceCreator } from '../types'; import { fmtBytes, fmtCount, logger, Transform } from '../utils'; import { writePlyStreaming } from '../writers/write-ply-streaming'; +import { createBlockProducerSource } from './block-producer'; +import { APP_CHUNK } from './gpu-edge-cost'; +import { mergeStream } from './merge-stream'; +import { kdPartition, coherenceRuns } from './partition'; +import type { ResidentPositions } from './partition'; +import { runPriorityPass, HALO_CAP } from './priority'; +import type { CandidateArrays } from './priority'; +import { selectMerges } from './select'; + /** Neighbours per query — unchanged from legacy. */ const KNN_K = 16; @@ -175,14 +174,17 @@ const decimateSource = async ( // fits the adapter's storage-binding limit. let blockSize = BLOCK_SIZE; const bindingLimit = (device as unknown as { limits?: { maxStorageBufferBindingSize?: number } } | undefined) - ?.limits?.maxStorageBufferBindingSize; + ?.limits?.maxStorageBufferBindingSize; if (typeof bindingLimit === 'number') { - const largestBinding = (bs: number) => bs * (1 + HALO_CAP) * Math.max(Math.min(APP_CHUNK, colorDim) * 4, 36); - while (blockSize > (1 << 16) && largestBinding(blockSize) > bindingLimit) { + const largestBinding = (bs: number) => + bs * (1 + HALO_CAP) * Math.max(Math.min(APP_CHUNK, colorDim) * 4, 36); + while (blockSize > 1 << 16 && largestBinding(blockSize) > bindingLimit) { blockSize >>= 1; } if (blockSize !== BLOCK_SIZE) { - logger.warn(`reducing decimate block size to ${fmtCount(blockSize)} to fit GPU binding limit ${fmtBytes(bindingLimit)}`); + logger.warn( + `reducing decimate block size to ${fmtCount(blockSize)} to fit GPU binding limit ${fmtBytes(bindingLimit)}` + ); } } @@ -191,7 +193,9 @@ const decimateSource = async ( partSub.end(); if (generation === 1 && N >= COHERENCE_MIN_N) { - const runs = blocks.map(b => coherenceRuns(order, b.start, b.end, COHERENCE_GAP_ROWS)).sort((a, b) => a - b); + const runs = blocks + .map((b) => coherenceRuns(order, b.start, b.end, COHERENCE_GAP_ROWS)) + .sort((a, b) => a - b); const median = runs[runs.length >> 1] ?? 0; if (median > INCOHERENT_RUNS_PER_BLOCK) { logger.warn( @@ -202,7 +206,7 @@ const decimateSource = async ( const K = chooseK(N, budget); const cand: CandidateArrays = { - idx: new Uint32Array(N * K).fill(0xFFFFFFFF), + idx: new Uint32Array(N * K).fill(0xffffffff), cost: new Float32Array(N * K).fill(Infinity) }; @@ -210,7 +214,7 @@ const decimateSource = async ( await runPriorityPass( { source: src, pool, pos: positions, order, blocks, device, K, k: Math.min(KNN_K, Math.max(1, N - 1)) }, cand, - n => priorityBar.tick(n) + (n) => priorityBar.tick(n) ); priorityBar.end(); @@ -222,12 +226,12 @@ const decimateSource = async ( if (selection.removed === 0) { gen.end(); - const cause = device ? - 'the GPU step likely failed (e.g. out-of-memory) or produced non-finite costs' : - 'cost computation produced no finite merge candidates (e.g. non-finite inputs)'; + const cause = device + ? 'the GPU step likely failed (e.g. out-of-memory) or produced non-finite costs' + : 'cost computation produced no finite merge candidates (e.g. non-finite inputs)'; throw new Error( `decimation found no valid merges at ${N} splats (target ${targetCount}) — ${cause}. ` + - 'Refusing to return an incompletely-decimated scene.' + 'Refusing to return an incompletely-decimated scene.' ); } const removedFraction = selection.removed / N; @@ -235,9 +239,9 @@ const decimateSource = async ( gen.end(); throw new Error( `decimation stalled at ${N} splats (target ${targetCount}): a generation removed only ` + - `${selection.removed} splat${selection.removed === 1 ? '' : 's'} (${(removedFraction * 100).toFixed(3)}% of ${N}) — ` + - 'the nearest-neighbour graph is too degenerate to merge further (e.g. many coincident splats). ' + - 'Refusing to grind toward the target.' + `${selection.removed} splat${selection.removed === 1 ? '' : 's'} (${(removedFraction * 100).toFixed(3)}% of ${N}) — ` + + 'the nearest-neighbour graph is too degenerate to merge further (e.g. many coincident splats). ' + + 'Refusing to grind toward the target.' ); } @@ -257,11 +261,13 @@ const decimateSource = async ( }; const isFinal = outCount <= targetCount; - const nextPositions: ResidentPositions | undefined = isFinal ? undefined : { - x: new Float32Array(outCount), - y: new Float32Array(outCount), - z: new Float32Array(outCount) - }; + const nextPositions: ResidentPositions | undefined = isFinal + ? undefined + : { + x: new Float32Array(outCount), + y: new Float32Array(outCount), + z: new Float32Array(outCount) + }; // `src` is reassigned each generation; capture this generation's // values for the deferred producer closures. @@ -276,12 +282,14 @@ const decimateSource = async ( // streaming happens after this function returns. gen.end(); const mergeBar = logger.bar('merging', N); - const producer = createBlockProducerSource(outMeta, () => mergeStream(streamCtx, genChunkSize, n => mergeBar.tick(n))); + const producer = createBlockProducerSource(outMeta, () => + mergeStream(streamCtx, genChunkSize, (n) => mergeBar.tick(n)) + ); const disposeSpill = disposeCurrentInput; let closed = false; return { meta: producer.meta, - read: request => producer.read(request), + read: (request) => producer.read(request), close: async () => { if (closed) return; closed = true; @@ -294,7 +302,9 @@ const decimateSource = async ( } const mergeBar = logger.bar('merging', N); - const producer = createBlockProducerSource(outMeta, () => mergeStream(streamCtx, genChunkSize, n => mergeBar.tick(n))); + const producer = createBlockProducerSource(outMeta, () => + mergeStream(streamCtx, genChunkSize, (n) => mergeBar.tick(n)) + ); // Intermediate generation: materialize (RAM when comfortably within // budget, else temp PLY spill), then advance the loop. @@ -308,7 +318,7 @@ const decimateSource = async ( if (!opts.spill) { throw new Error( `decimation intermediate generation needs ${fmtBytes(estBytes)}, over the in-memory budget — ` + - 'a spill location is required (opts.spill / --scratch-dir)' + 'a spill location is required (opts.spill / --scratch-dir)' ); } const spill = opts.spill; diff --git a/src/lib/decimate-uniform/edge-cost-cpu.ts b/src/lib/decimate-uniform/edge-cost-cpu.ts index 8b46b900..ad1fd01c 100644 --- a/src/lib/decimate-uniform/edge-cost-cpu.ts +++ b/src/lib/decimate-uniform/edge-cost-cpu.ts @@ -17,10 +17,9 @@ import { quatToRotmat, sigmaFromRotVar, det3, - gaussLogpdfDiagrot, - type SplatView, - type MergeScratch + gaussLogpdfDiagrot } from '../decimate/moment-match'; +import type { SplatView, MergeScratch } from '../decimate/moment-match'; /** * Per-splat derived quantities for the cost function (legacy @@ -60,16 +59,24 @@ const buildCostCache = (view: SplatView): CostCache => { const vy = sy * sy + EPS_COV; const vz = sz * sz + EPS_COV; - v[i3] = vx; v[i3 + 1] = vy; v[i3 + 2] = vz; + v[i3] = vx; + v[i3 + 1] = vy; + v[i3 + 2] = vz; invdiag[i3] = 1 / Math.max(vx, 1e-30); invdiag[i3 + 1] = 1 / Math.max(vy, 1e-30); invdiag[i3 + 2] = 1 / Math.max(vz, 1e-30); logdet[i] = Math.log(Math.max(vx, 1e-30)) + Math.log(Math.max(vy, 1e-30)) + Math.log(Math.max(vz, 1e-30)); - let qw = geo[i8], qx = geo[i8 + 1], qy = geo[i8 + 2], qz = geo[i8 + 3]; + let qw = geo[i8], + qx = geo[i8 + 1], + qy = geo[i8 + 2], + qz = geo[i8 + 3]; const qn = Math.hypot(qw, qx, qy, qz); const invq = 1 / Math.max(qn, 1e-12); - qw *= invq; qx *= invq; qy *= invq; qz *= invq; + qw *= invq; + qx *= invq; + qy *= invq; + qz *= invq; quatToRotmat(qw, qx, qy, qz, R, i9); sigmaFromRotVar(R, i9, vx, vy, vz, sigma, i9); @@ -102,13 +109,20 @@ const computeEdgeCostView = ( scratch: MergeScratch ): number => { const { pos, color, colorDim } = view; - const i3 = 3 * i, j3 = 3 * j; - const i9 = 9 * i, j9 = 9 * j; - - const mux = pos[i3], muy = pos[i3 + 1], muz = pos[i3 + 2]; - const mvx = pos[j3], mvy = pos[j3 + 1], mvz = pos[j3 + 2]; - - const wi = cache.mass[i], wj = cache.mass[j]; + const i3 = 3 * i, + j3 = 3 * j; + const i9 = 9 * i, + j9 = 9 * j; + + const mux = pos[i3], + muy = pos[i3 + 1], + muz = pos[i3 + 2]; + const mvx = pos[j3], + mvy = pos[j3 + 1], + mvz = pos[j3 + 2]; + + const wi = cache.mass[i], + wj = cache.mass[j]; const W = wi + wj; const Wsafe = W > 0 ? W : 1; @@ -122,8 +136,12 @@ const computeEdgeCostView = ( const mmy = pi * muy + pj * mvy; const mmz = pi * muz + pj * mvz; - const dix = mux - mmx, diy = muy - mmy, diz = muz - mmz; - const djx = mvx - mmx, djy = mvy - mmy, djz = mvz - mmz; + const dix = mux - mmx, + diy = muy - mmy, + diz = muz - mmz; + const djx = mvx - mmx, + djy = mvy - mmy, + djz = mvz - mmz; const sigm = scratch.sigm; for (let a = 0; a < 9; a++) { @@ -162,7 +180,9 @@ const computeEdgeCostView = ( let sumLogpOnJ = 0; for (let s = 0; s < Z.length; s++) { - const z0 = Z[s][0], z1 = Z[s][1], z2 = Z[s][2]; + const z0 = Z[s][0], + z1 = Z[s][1], + z2 = Z[s][2]; const xix = mux + z0 * stdix * cache.R[i9 + 0] + z1 * stdiy * cache.R[i9 + 1] + z2 * stdiz * cache.R[i9 + 2]; const xiy = muy + z0 * stdix * cache.R[i9 + 3] + z1 * stdiy * cache.R[i9 + 4] + z2 * stdiz * cache.R[i9 + 5]; @@ -172,16 +192,64 @@ const computeEdgeCostView = ( const xjy = mvy + z0 * stdjx * cache.R[j9 + 3] + z1 * stdjy * cache.R[j9 + 4] + z2 * stdjz * cache.R[j9 + 5]; const xjz = mvz + z0 * stdjx * cache.R[j9 + 6] + z1 * stdjy * cache.R[j9 + 7] + z2 * stdjz * cache.R[j9 + 8]; - const logNiOnI = gaussLogpdfDiagrot(xix, xiy, xiz, mux, muy, muz, - cache.R, i9, cache.invdiag[i3], cache.invdiag[i3 + 1], cache.invdiag[i3 + 2], cache.logdet[i]); - const logNjOnI = gaussLogpdfDiagrot(xix, xiy, xiz, mvx, mvy, mvz, - cache.R, j9, cache.invdiag[j3], cache.invdiag[j3 + 1], cache.invdiag[j3 + 2], cache.logdet[j]); + const logNiOnI = gaussLogpdfDiagrot( + xix, + xiy, + xiz, + mux, + muy, + muz, + cache.R, + i9, + cache.invdiag[i3], + cache.invdiag[i3 + 1], + cache.invdiag[i3 + 2], + cache.logdet[i] + ); + const logNjOnI = gaussLogpdfDiagrot( + xix, + xiy, + xiz, + mvx, + mvy, + mvz, + cache.R, + j9, + cache.invdiag[j3], + cache.invdiag[j3 + 1], + cache.invdiag[j3 + 2], + cache.logdet[j] + ); sumLogpOnI += logAddExp(logPi + logNiOnI, logPj + logNjOnI); - const logNiOnJ = gaussLogpdfDiagrot(xjx, xjy, xjz, mux, muy, muz, - cache.R, i9, cache.invdiag[i3], cache.invdiag[i3 + 1], cache.invdiag[i3 + 2], cache.logdet[i]); - const logNjOnJ = gaussLogpdfDiagrot(xjx, xjy, xjz, mvx, mvy, mvz, - cache.R, j9, cache.invdiag[j3], cache.invdiag[j3 + 1], cache.invdiag[j3 + 2], cache.logdet[j]); + const logNiOnJ = gaussLogpdfDiagrot( + xjx, + xjy, + xjz, + mux, + muy, + muz, + cache.R, + i9, + cache.invdiag[i3], + cache.invdiag[i3 + 1], + cache.invdiag[i3 + 2], + cache.logdet[i] + ); + const logNjOnJ = gaussLogpdfDiagrot( + xjx, + xjy, + xjz, + mvx, + mvy, + mvz, + cache.R, + j9, + cache.invdiag[j3], + cache.invdiag[j3 + 1], + cache.invdiag[j3 + 2], + cache.logdet[j] + ); sumLogpOnJ += logAddExp(logPi + logNiOnJ, logPj + logNjOnJ); } diff --git a/src/lib/decimate-uniform/gpu-edge-cost.ts b/src/lib/decimate-uniform/gpu-edge-cost.ts index 46928781..f49d6853 100644 --- a/src/lib/decimate-uniform/gpu-edge-cost.ts +++ b/src/lib/decimate-uniform/gpu-edge-cost.ts @@ -1,3 +1,4 @@ +import type { GraphicsDevice } from 'playcanvas'; import { BUFFERUSAGE_COPY_DST, BUFFERUSAGE_COPY_SRC, @@ -9,7 +10,6 @@ import { BindStorageBufferFormat, BindUniformBufferFormat, Compute, - GraphicsDevice, Shader, StorageBuffer, UniformBufferFormat, @@ -40,7 +40,7 @@ export const APP_CHUNK = 16; * @param strideC - Live column count of appearance chunk C (0 if unused). * @returns WGSL source. */ -const edgeCostWgsl = (strideA: number, strideB: number, strideC: number) => /* wgsl */` +const edgeCostWgsl = (strideA: number, strideB: number, strideC: number) => /* wgsl */ ` struct Uniforms { edgeCount: u32, z0: f32, @@ -257,7 +257,7 @@ fn main(@builtin(global_invocation_id) gid: vec3u) { * WebGPU per-stage storage-buffer count limit (8) and the per-binding size * limit (~2 GB) — appearance is split into 16-column chunks for the latter. */ -interface EdgeCostCache { +type EdgeCostCache = { /** Per-splat geometry interleaved 8-wide: (x, y, z, mass, logdet, vx, vy, vz). */ posScalars: Float32Array; /** Row-major 3×3 rotation per splat (length 9N). */ @@ -271,7 +271,7 @@ interface EdgeCostCache { numAppCols: number; /** Number of splats. */ numSplats: number; -} +}; /** * GPU edge-cost evaluator. @@ -309,7 +309,7 @@ class GpuEdgeCost { */ constructor(device: GraphicsDevice, maxN: number, maxE: number, maxAppCols: number) { const workgroupSize = 64; - const edgesPerBatch = 1024 * workgroupSize; // 65,536 + const edgesPerBatch = 1024 * workgroupSize; // 65,536 // Appearance is split at fixed APP_CHUNK-column boundaries, but each // chunk's *stride* is its live column count — only the last non-empty // chunk is ever partial, so partial chunks neither allocate nor upload @@ -324,7 +324,7 @@ class GpuEdgeCost { // supplies exactly this many: a short count would leave a hoisted (reused // across iterations) appearance buffer holding the previous iteration's // data, which the kernel would then read as this iteration's appearance. - const numAppChunks = appStrides.filter(stride => stride > 0).length; + const numAppChunks = appStrides.filter((stride) => stride > 0).length; const bindGroupFormat = new BindGroupFormat(device, [ new BindUniformBufferFormat('uniforms', SHADERSTAGE_COMPUTE), @@ -367,7 +367,7 @@ class GpuEdgeCost { if (bytes > maxStorage) { throw new Error( `GpuEdgeCost: ${label} buffer (${bytes} bytes) exceeds device ` + - `maxStorageBufferBindingSize (${maxStorage})` + `maxStorageBufferBindingSize (${maxStorage})` ); } }; @@ -385,9 +385,7 @@ class GpuEdgeCost { // stays fixed regardless of band count. const appDummy = new StorageBuffer(device, 16, BUFFERUSAGE_COPY_DST); const appBufs: StorageBuffer[] = appStrides.map((width) => { - return width > 0 ? - new StorageBuffer(device, maxN * width * 4, BUFFERUSAGE_COPY_DST) : - appDummy; + return width > 0 ? new StorageBuffer(device, maxN * width * 4, BUFFERUSAGE_COPY_DST) : appDummy; }); // Two parallel u32 buffers, sized to a single dispatch batch (not the @@ -398,11 +396,7 @@ class GpuEdgeCost { const edgesIBuf = new StorageBuffer(device, edgesPerBatch * 4, BUFFERUSAGE_COPY_DST); const edgesJBuf = new StorageBuffer(device, edgesPerBatch * 4, BUFFERUSAGE_COPY_DST); - const outBuf = new StorageBuffer( - device, - edgesPerBatch * 4, - BUFFERUSAGE_COPY_SRC | BUFFERUSAGE_COPY_DST - ); + const outBuf = new StorageBuffer(device, edgesPerBatch * 4, BUFFERUSAGE_COPY_SRC | BUFFERUSAGE_COPY_DST); const outScratch = new Float32Array(edgesPerBatch); const compute = new Compute(device, shader, 'compute-edge-cost'); @@ -428,10 +422,14 @@ class GpuEdgeCost { if (n > maxN) throw new Error(`GpuEdgeCost: N=${n} exceeds maxN=${maxN}`); if (e > maxE) throw new Error(`GpuEdgeCost: E=${e} exceeds maxE=${maxE}`); if (cache.numAppCols !== maxAppCols) { - throw new Error(`GpuEdgeCost: numAppCols=${cache.numAppCols} must equal maxAppCols=${maxAppCols} (baked into the kernel)`); + throw new Error( + `GpuEdgeCost: numAppCols=${cache.numAppCols} must equal maxAppCols=${maxAppCols} (baked into the kernel)` + ); } if (cache.appChunks.length !== numAppChunks) { - throw new Error(`GpuEdgeCost: cache supplies ${cache.appChunks.length} appearance chunks but the kernel layout expects ${numAppChunks}`); + throw new Error( + `GpuEdgeCost: cache supplies ${cache.appChunks.length} appearance chunks but the kernel layout expects ${numAppChunks}` + ); } if (edgeJ.length !== e || outCosts.length !== e) { throw new Error('GpuEdgeCost: edgeI / edgeJ / outCosts must have same length'); diff --git a/src/lib/decimate-uniform/gpu-knn.ts b/src/lib/decimate-uniform/gpu-knn.ts index 11f49881..0dedd4bb 100644 --- a/src/lib/decimate-uniform/gpu-knn.ts +++ b/src/lib/decimate-uniform/gpu-knn.ts @@ -1,3 +1,4 @@ +import type { GraphicsDevice } from 'playcanvas'; import { BUFFERUSAGE_COPY_DST, BUFFERUSAGE_COPY_SRC, @@ -8,14 +9,13 @@ import { BindStorageBufferFormat, BindUniformBufferFormat, Compute, - GraphicsDevice, Shader, StorageBuffer, UniformBufferFormat, UniformFormat } from 'playcanvas'; -import { type FlatKdTree } from '../spatial/kd-tree'; +import type { FlatKdTree } from '../spatial/kd-tree'; /** * Block-local GPU KNN for `--decimate-uniform`. @@ -45,7 +45,7 @@ import { type FlatKdTree } from '../spatial/kd-tree'; * @param stackSize - Compile-time per-thread DFS stack depth. * @returns WGSL source. */ -const knnWgsl = (k: number, stackSize: number) => /* wgsl */` +const knnWgsl = (k: number, stackSize: number) => /* wgsl */ ` struct Uniforms { queryOffset: u32, queryCount: u32, @@ -225,11 +225,11 @@ class GpuKnn { */ constructor(device: GraphicsDevice, maxN: number, k: number) { const workgroupSize = 64; - const queriesPerBatch = 1024 * workgroupSize; // 65,536 + const queriesPerBatch = 1024 * workgroupSize; // 65,536 // Per-thread DFS stack depth: tree depth = log2(maxN) + slack. 48 is // safe for any N within the 30-bit nodeIdx packing limit checked below. const stackSize = 48; - if (maxN > 0x3FFFFFFF) { + if (maxN > 0x3fffffff) { throw new Error(`GpuKnn: maxN=${maxN} exceeds 30-bit nodeIdx packing limit (~1B nodes)`); } @@ -267,11 +267,7 @@ class GpuKnn { const nChildrenBuf = new StorageBuffer(device, maxN * 2 * 4, BUFFERUSAGE_COPY_DST); const outBatchBytes = queriesPerBatch * k * 4; - const outBuf = new StorageBuffer( - device, - outBatchBytes, - BUFFERUSAGE_COPY_SRC | BUFFERUSAGE_COPY_DST - ); + const outBuf = new StorageBuffer(device, outBatchBytes, BUFFERUSAGE_COPY_SRC | BUFFERUSAGE_COPY_DST); const outScratch = new Uint32Array(queriesPerBatch * k); const compute = new Compute(device, shader, 'compute-knn-kdtree'); @@ -298,7 +294,9 @@ class GpuKnn { throw new Error(`GpuKnn: queryCount=${queryCount} exceeds N=${n}`); } if (outNeighbours.length !== queryCount * k) { - throw new Error(`GpuKnn: outNeighbours length ${outNeighbours.length} must be queryCount*k = ${queryCount * k}`); + throw new Error( + `GpuKnn: outNeighbours length ${outNeighbours.length} must be queryCount*k = ${queryCount * k}` + ); } // `FlatKdTree` already carries the interleaved layout this kernel diff --git a/src/lib/decimate-uniform/knn-blocks.ts b/src/lib/decimate-uniform/knn-blocks.ts index c8874333..7e5519f8 100644 --- a/src/lib/decimate-uniform/knn-blocks.ts +++ b/src/lib/decimate-uniform/knn-blocks.ts @@ -1,5 +1,5 @@ import { knnQueryBlock, KNN_SENTINEL } from './knn-core'; -import { type BlockRange, type ResidentPositions } from './partition'; +import type { BlockRange, ResidentPositions } from './partition'; /** * A block's local point set: owned gaussians first (in the block's sorted @@ -16,7 +16,7 @@ type BlockLocals = { }; /** Local-slot marker: neighbour was fixed by verification; resolve via its global id. */ -const KNN_FIXED = 0xFFFFFFFE; +const KNN_FIXED = 0xfffffffe; // Density-based halo radius: haloFactor × the Poisson estimate of the k-NN // radius from the block's AABB volume and count. Degenerate blocks (planar / @@ -196,7 +196,14 @@ const toGlobalNeighbors = (locals: BlockLocals, nbLocal: Uint32Array): Uint32Arr }; // Insert (g, d2) into the k-best arrays (ascending by distance). -const kBestInsert = (bestIds: Uint32Array, bestD2: Float64Array, size: number, k: number, g: number, d2: number): number => { +const kBestInsert = ( + bestIds: Uint32Array, + bestD2: Float64Array, + size: number, + k: number, + g: number, + d2: number +): number => { if (size === k && d2 >= bestD2[k - 1]) return size; let at = size < k ? size : k - 1; while (at > 0 && bestD2[at - 1] > d2) { @@ -254,7 +261,9 @@ const verifyAndFixKnn = ( for (let qi = 0; qi < locals.ownedCount; qi++) { const g = locals.ids[qi]; - const qx = pos.x[g], qy = pos.y[g], qz = pos.z[g]; + const qx = pos.x[g], + qy = pos.y[g], + qz = pos.z[g]; let dkSq = 0; let sentinels = false; @@ -264,17 +273,25 @@ const verifyAndFixKnn = ( sentinels = true; continue; } - const dx = pos.x[nb] - qx, dy = pos.y[nb] - qy, dz = pos.z[nb] - qz; + const dx = pos.x[nb] - qx, + dy = pos.y[nb] - qy, + dz = pos.z[nb] - qz; const d2 = dx * dx + dy * dy + dz * dz; if (d2 > dkSq) dkSq = d2; } // Depth of q inside the block AABB (0 at/outside the boundary). - const depth = Math.max(0, Math.min( - qx - block.aabb[0], block.aabb[3] - qx, - qy - block.aabb[1], block.aabb[4] - qy, - qz - block.aabb[2], block.aabb[5] - qz - )); + const depth = Math.max( + 0, + Math.min( + qx - block.aabb[0], + block.aabb[3] - qx, + qy - block.aabb[1], + block.aabb[4] - qy, + qz - block.aabb[2], + block.aabb[5] - qz + ) + ); const needFix = (sentinels && N - 1 >= k) || Math.sqrt(dkSq) > depth + h; if (!needFix) continue; @@ -305,7 +322,9 @@ const verifyAndFixKnn = ( for (let i = other.start; i < other.end; i++) { const cand = order[i]; if (cand === g) continue; - const dx = pos.x[cand] - qx, dy = pos.y[cand] - qy, dz = pos.z[cand] - qz; + const dx = pos.x[cand] - qx, + dy = pos.y[cand] - qy, + dz = pos.z[cand] - qz; const d2 = dx * dx + dy * dy + dz * dz; if (size < k || d2 < bestD2[size - 1]) { size = kBestInsert(bestIds, bestD2, size, k, cand, d2); @@ -321,12 +340,4 @@ const verifyAndFixKnn = ( return fixed; }; -export { - collectBlock, - knnBlockCpu, - toGlobalNeighbors, - verifyAndFixKnn, - haloRadius, - KNN_FIXED, - type BlockLocals -}; +export { collectBlock, knnBlockCpu, toGlobalNeighbors, verifyAndFixKnn, haloRadius, KNN_FIXED, type BlockLocals }; diff --git a/src/lib/decimate-uniform/knn-core.ts b/src/lib/decimate-uniform/knn-core.ts index 7afb94bf..0ed3b3d0 100644 --- a/src/lib/decimate-uniform/knn-core.ts +++ b/src/lib/decimate-uniform/knn-core.ts @@ -1,7 +1,7 @@ import { KdTree } from '../spatial/kd-tree'; /** Marks an unfilled neighbour slot (fewer than k non-self points available). */ -const KNN_SENTINEL = 0xFFFFFFFF; +const KNN_SENTINEL = 0xffffffff; /** * Exact k-nearest-neighbours for the owned prefix of a local point set. diff --git a/src/lib/decimate-uniform/merge-stream.ts b/src/lib/decimate-uniform/merge-stream.ts index 12543c40..76fb773d 100644 --- a/src/lib/decimate-uniform/merge-stream.ts +++ b/src/lib/decimate-uniform/merge-stream.ts @@ -1,9 +1,11 @@ -import { type ChunkPayload } from './block-producer'; -import { type ResidentPositions } from './partition'; -import { gatherBlockView, indexOfSorted, type PriorityContext } from './priority'; -import { type SelectionResult } from './select'; import { WorkerQueue } from '../workers'; +import type { ChunkPayload } from './block-producer'; +import type { ResidentPositions } from './partition'; +import { gatherBlockView, indexOfSorted } from './priority'; +import type { PriorityContext } from './priority'; +import type { SelectionResult } from './select'; + /** Context for the merge stream: the priority context plus the selection. */ type MergeStreamContext = Pick & { selection: SelectionResult; @@ -26,7 +28,7 @@ type MergeStreamContext = Pick void @@ -113,17 +115,25 @@ async function *mergeStream( mi++; } } - const transfer: ArrayBuffer[] = [mPos.buffer as ArrayBuffer, mGeo.buffer as ArrayBuffer, mColor.buffer as ArrayBuffer]; + const transfer: ArrayBuffer[] = [ + mPos.buffer as ArrayBuffer, + mGeo.buffer as ArrayBuffer, + mColor.buffer as ArrayBuffer + ]; if (mOther) transfer.push(mOther.buffer as ArrayBuffer); - const merged = await WorkerQueue.run('mergeGroups', { - pos: mPos, - geo: mGeo, - color: mColor, - sizes, - colorDim, - other: mOther, - otherDim - }, transfer); + const merged = await WorkerQueue.run( + 'mergeGroups', + { + pos: mPos, + geo: mGeo, + color: mColor, + sizes, + colorDim, + other: mOther, + otherDim + }, + transfer + ); mergedPos = merged.pos; mergedGeo = merged.geo; mergedColor = merged.color; @@ -135,7 +145,7 @@ async function *mergeStream( for (let i = 0; i < nOwned; i++) { const g = owned[i]; const mg = memberGroup[g]; - if (mg !== -1 && groupMin[mg] !== g) continue; // consumed member + if (mg !== -1 && groupMin[mg] !== g) continue; // consumed member if (mg === -1) { // Survivor pass-through: position from resident arrays, diff --git a/src/lib/decimate-uniform/partition.ts b/src/lib/decimate-uniform/partition.ts index 07dfe5f3..a3b551a6 100644 --- a/src/lib/decimate-uniform/partition.ts +++ b/src/lib/decimate-uniform/partition.ts @@ -97,7 +97,8 @@ const kdPartition = (pos: ResidentPositions, blockSize: number): { order: Uint32 blocks.push({ start, end, aabb }); return; } - let axis = 0, ext = -Infinity; + let axis = 0, + ext = -Infinity; for (let c = 0; c < 3; c++) { const e = aabb[3 + c] - aabb[c]; if (e > ext) { @@ -119,17 +120,30 @@ const kdPartition = (pos: ResidentPositions, blockSize: number): { order: Uint32 const { lo, hi } = outlierFence(pos); let out = 0; for (let i = 0; i < n; i++) { - if (cols[0][i] < lo[0] || cols[0][i] > hi[0] || - cols[1][i] < lo[1] || cols[1][i] > hi[1] || - cols[2][i] < lo[2] || cols[2][i] > hi[2]) out++; + if ( + cols[0][i] < lo[0] || + cols[0][i] > hi[0] || + cols[1][i] < lo[1] || + cols[1][i] > hi[1] || + cols[2][i] < lo[2] || + cols[2][i] > hi[2] + ) + out++; } if (out > 0 && out <= n * OUTLIER_MAX_FRACTION) { coreEnd = n - out; - let c = 0, o = coreEnd; + let c = 0, + o = coreEnd; for (let i = 0; i < n; i++) { - if (cols[0][i] < lo[0] || cols[0][i] > hi[0] || - cols[1][i] < lo[1] || cols[1][i] > hi[1] || - cols[2][i] < lo[2] || cols[2][i] > hi[2]) order[o++] = i; + if ( + cols[0][i] < lo[0] || + cols[0][i] > hi[0] || + cols[1][i] < lo[1] || + cols[1][i] > hi[1] || + cols[2][i] < lo[2] || + cols[2][i] > hi[2] + ) + order[o++] = i; else order[c++] = i; } } diff --git a/src/lib/decimate-uniform/priority.ts b/src/lib/decimate-uniform/priority.ts index f65e49f7..2feae6ce 100644 --- a/src/lib/decimate-uniform/priority.ts +++ b/src/lib/decimate-uniform/priority.ts @@ -1,14 +1,18 @@ -import { type GraphicsDevice } from 'playcanvas'; +import type { GraphicsDevice } from 'playcanvas'; + +import type { ChunkData, ChunkDataPool, ChunkSource } from '../chunk'; +import { createMergeScratch, makeGaussianSamples, sigmoid, ellipsoidArea } from '../decimate/moment-match'; +import type { SplatView } from '../decimate/moment-match'; +import { WorkerQueue } from '../workers'; import { buildCostCache, computeEdgeCostView } from './edge-cost-cpu'; -import { APP_CHUNK, GpuEdgeCost, type EdgeCostCache } from './gpu-edge-cost'; +import { APP_CHUNK, GpuEdgeCost } from './gpu-edge-cost'; +import type { EdgeCostCache } from './gpu-edge-cost'; import { GpuKnn } from './gpu-knn'; -import { collectBlock, verifyAndFixKnn, toGlobalNeighbors, KNN_FIXED, type BlockLocals } from './knn-blocks'; +import { collectBlock, verifyAndFixKnn, toGlobalNeighbors, KNN_FIXED } from './knn-blocks'; +import type { BlockLocals } from './knn-blocks'; import { KNN_SENTINEL } from './knn-core'; -import { type BlockRange, type ResidentPositions } from './partition'; -import { type ChunkData, type ChunkDataPool, type ChunkSource } from '../chunk'; -import { createMergeScratch, makeGaussianSamples, sigmoid, ellipsoidArea, type SplatView } from '../decimate/moment-match'; -import { WorkerQueue } from '../workers'; +import type { BlockRange, ResidentPositions } from './partition'; /** Halo radius multiplier on the density-estimated k-NN radius. */ const HALO_FACTOR = 2.5; @@ -137,7 +141,8 @@ const gatherBlockView = async ( // Binary search `g` in the sorted array; -1 when absent. const indexOfSorted = (sorted: Uint32Array, g: number): number => { - let lo = 0, hi = sorted.length - 1; + let lo = 0, + hi = sorted.length - 1; while (lo <= hi) { const mid = (lo + hi) >> 1; const v = sorted[mid]; @@ -172,20 +177,39 @@ const packGpuCache = (view: SplatView): EdgeCostCache => { posScalars[o + 1] = pos[i * 3 + 1]; posScalars[o + 2] = pos[i * 3 + 2]; posScalars[o + 3] = linAlpha * ellipsoidArea(sx, sy, sz) + 1e-12; - posScalars[o + 4] = Math.log(Math.max(vx, 1e-30)) + Math.log(Math.max(vy, 1e-30)) + Math.log(Math.max(vz, 1e-30)); + posScalars[o + 4] = + Math.log(Math.max(vx, 1e-30)) + Math.log(Math.max(vy, 1e-30)) + Math.log(Math.max(vz, 1e-30)); posScalars[o + 5] = vx; posScalars[o + 6] = vy; posScalars[o + 7] = vz; - let qw = geo[i8], qx = geo[i8 + 1], qy = geo[i8 + 2], qz = geo[i8 + 3]; + let qw = geo[i8], + qx = geo[i8 + 1], + qy = geo[i8 + 2], + qz = geo[i8 + 3]; const invq = 1 / Math.max(Math.hypot(qw, qx, qy, qz), 1e-12); - qw *= invq; qx *= invq; qy *= invq; qz *= invq; - const xx = qx * qx, yy = qy * qy, zz = qz * qz; - const wx = qw * qx, wy = qw * qy, wz = qw * qz; - const xy = qx * qy, xz = qx * qz, yz = qy * qz; - rot[0] = 1 - 2 * (yy + zz); rot[1] = 2 * (xy - wz); rot[2] = 2 * (xz + wy); - rot[3] = 2 * (xy + wz); rot[4] = 1 - 2 * (xx + zz); rot[5] = 2 * (yz - wx); - rot[6] = 2 * (xz - wy); rot[7] = 2 * (yz + wx); rot[8] = 1 - 2 * (xx + yy); + qw *= invq; + qx *= invq; + qy *= invq; + qz *= invq; + const xx = qx * qx, + yy = qy * qy, + zz = qz * qz; + const wx = qw * qx, + wy = qw * qy, + wz = qw * qz; + const xy = qx * qy, + xz = qx * qz, + yz = qy * qz; + rot[0] = 1 - 2 * (yy + zz); + rot[1] = 2 * (xy - wz); + rot[2] = 2 * (xz + wy); + rot[3] = 2 * (xy + wz); + rot[4] = 1 - 2 * (xx + zz); + rot[5] = 2 * (yz - wx); + rot[6] = 2 * (xz - wy); + rot[7] = 2 * (yz + wx); + rot[8] = 1 - 2 * (xx + yy); rotR.set(rot, i * 9); } @@ -248,10 +272,14 @@ const runPriorityPass = async ( const run = Promise.all([treePromise, gpuKnnQueue]).then(([flat]) => { return gpuKnn!.execute(flat, locals.positions, locals.ids.length, locals.ownedCount, out); }); - gpuKnnQueue = run.catch(() => { /* surfaced by the awaiting block */ }); + gpuKnnQueue = run.catch(() => { + /* surfaced by the awaiting block */ + }); return { locals, nb: run.then(() => out) }; } - const nb = WorkerQueue.run('knnBlock', { positions: copy, ownedCount: locals.ownedCount, k }, [copy.buffer as ArrayBuffer]); + const nb = WorkerQueue.run('knnBlock', { positions: copy, ownedCount: locals.ownedCount, k }, [ + copy.buffer as ArrayBuffer + ]); return { locals, nb }; }; @@ -305,8 +333,8 @@ const runPriorityPass = async ( // Edge lists in owned-major order (view-local endpoints). const edgeI = new Uint32Array(nOwned * k); const edgeJ = new Uint32Array(nOwned * k); - const edgeNb = new Uint32Array(nOwned * k); // global neighbour per edge - const edgeOf = new Uint32Array(nOwned + 1); // CSR into the edge list per owned row + const edgeNb = new Uint32Array(nOwned * k); // global neighbour per edge + const edgeOf = new Uint32Array(nOwned + 1); // CSR into the edge list per owned row let e = 0; for (let qi = 0; qi < nOwned; qi++) { edgeOf[qi] = e; @@ -361,7 +389,7 @@ const runPriorityPass = async ( } const g = owned[qi]; for (let s = 0; s < K; s++) { - cand.idx[g * K + s] = s < size ? bestIdx[s] : 0xFFFFFFFF; + cand.idx[g * K + s] = s < size ? bestIdx[s] : 0xffffffff; cand.cost[g * K + s] = s < size ? bestCost[s] : Infinity; } } diff --git a/src/lib/decimate-uniform/select.ts b/src/lib/decimate-uniform/select.ts index f7cfc72e..eef0a681 100644 --- a/src/lib/decimate-uniform/select.ts +++ b/src/lib/decimate-uniform/select.ts @@ -10,12 +10,12 @@ * Engine-free; pure resident-array computation, no IO. */ -import { type CandidateArrays } from './priority'; +import type { CandidateArrays } from './priority'; /** Cost-histogram buckets for the ordered greedy walk. */ const SELECT_BUCKETS = 1024; -const NO_CANDIDATE = 0xFFFFFFFF; +const NO_CANDIDATE = 0xffffffff; /** * The selected merge groups. @@ -50,7 +50,8 @@ const selectMerges = (cand: CandidateArrays, N: number, K: number, mergesNeeded: const E = N * K; // Pass 1: finite cost range. - let lo = Infinity, hi = -Infinity; + let lo = Infinity, + hi = -Infinity; for (let e = 0; e < E; e++) { const c = cand.cost[e]; if (Number.isFinite(c)) { diff --git a/src/lib/decimate/block-allocation.ts b/src/lib/decimate/block-allocation.ts index 7c8009b8..fcc7427a 100644 --- a/src/lib/decimate/block-allocation.ts +++ b/src/lib/decimate/block-allocation.ts @@ -1,8 +1,9 @@ import { join } from 'pathe'; -import { type BlockPlan } from './block-plan'; -import { type ReadSource, type ReadStream } from '../io/read'; -import { type FileSystem } from '../io/write'; +import type { ReadSource, ReadStream } from '../io/read'; +import type { FileSystem } from '../io/write'; + +import type { BlockPlan } from './block-plan'; const RECORD_BYTES = 12; const CURSOR_RECORDS = 4096; @@ -88,7 +89,10 @@ class PlanCursor { private offset = 0; private consumed = 0; - constructor(source: ReadSource, readonly plan: StoredBlockPlan) { + constructor( + source: ReadSource, + readonly plan: StoredBlockPlan + ) { this.source = source; this.stream = source.read(); } @@ -97,7 +101,10 @@ class PlanCursor { if (this.consumed === this.plan.count) return null; if (this.offset === this.buffered) { const remaining = (this.plan.count - this.consumed) * RECORD_BYTES; - this.buffered = await pullExact(this.stream, this.buffer.subarray(0, Math.min(this.buffer.length, remaining))); + this.buffered = await pullExact( + this.stream, + this.buffer.subarray(0, Math.min(this.buffer.length, remaining)) + ); this.offset = 0; if (this.buffered < RECORD_BYTES) throw new Error(`truncated decimation plan '${this.plan.path}'`); } @@ -142,9 +149,11 @@ const allocatePlanPrefixes = async ( const prefixes = new Uint32Array(plans.length); const heap: HeapEntry[] = []; const cursors: PlanCursor[] = []; - const less = (a: HeapEntry, b: HeapEntry): boolean => a.cost < b.cost || - (a.cost === b.cost && (a.cursor.plan.blockIndex < b.cursor.plan.blockIndex || - (a.cursor.plan.blockIndex === b.cursor.plan.blockIndex && (a.a < b.a || (a.a === b.a && a.b < b.b))))); + const less = (a: HeapEntry, b: HeapEntry): boolean => + a.cost < b.cost || + (a.cost === b.cost && + (a.cursor.plan.blockIndex < b.cursor.plan.blockIndex || + (a.cursor.plan.blockIndex === b.cursor.plan.blockIndex && (a.a < b.a || (a.a === b.a && a.b < b.b))))); const push = (entry: HeapEntry): void => { let i = heap.length; heap.push(entry); @@ -188,11 +197,7 @@ const allocatePlanPrefixes = async ( let removed = 0; while (removed < needed && heap.length > 0) { const entry = pop(); - onSelect?.( - entry.cursor.plan.blockIndex, - prefixes[entry.cursor.plan.blockIndex], - entry - ); + onSelect?.(entry.cursor.plan.blockIndex, prefixes[entry.cursor.plan.blockIndex], entry); prefixes[entry.cursor.plan.blockIndex]++; removed++; const next = await entry.cursor.next(); @@ -225,7 +230,8 @@ const readBlockPlanPrefix = async ( const stream = source.read(0, count * RECORD_BYTES); try { const bytes = new Uint8Array(count * RECORD_BYTES); - if (await pullExact(stream, bytes) !== bytes.length) throw new Error(`truncated decimation plan '${stored.path}'`); + if ((await pullExact(stream, bytes)) !== bytes.length) + throw new Error(`truncated decimation plan '${stored.path}'`); const view = new DataView(bytes.buffer); for (let i = 0; i < count; i++) { pairs[i * 2] = view.getUint32(i * RECORD_BYTES, true); @@ -239,10 +245,4 @@ const readBlockPlanPrefix = async ( } }; -export { - allocatePlanPrefixes, - readBlockPlanPrefix, - storeBlockPlan, - type PlanScratch, - type StoredBlockPlan -}; +export { allocatePlanPrefixes, readBlockPlanPrefix, storeBlockPlan, type PlanScratch, type StoredBlockPlan }; diff --git a/src/lib/decimate/block-merge-stream.ts b/src/lib/decimate/block-merge-stream.ts index ac45ee55..6fc8f822 100644 --- a/src/lib/decimate/block-merge-stream.ts +++ b/src/lib/decimate/block-merge-stream.ts @@ -1,10 +1,13 @@ -import { readBlockPlanPrefix, type PlanScratch, type StoredBlockPlan } from './block-allocation'; -import { replayBlockPlan } from './block-plan'; -import { type DestBuffers } from './block-producer'; -import { type ResidentPositions } from './partition'; -import { gatherBlockView, type PriorityContext } from './priority'; import { WorkerQueue } from '../workers'; +import { readBlockPlanPrefix } from './block-allocation'; +import type { PlanScratch, StoredBlockPlan } from './block-allocation'; +import { replayBlockPlan } from './block-plan'; +import type { DestBuffers } from './block-producer'; +import type { ResidentPositions } from './partition'; +import { gatherBlockView } from './priority'; +import type { PriorityContext } from './priority'; + type BlockMergeStreamContext = Pick & { plans: StoredBlockPlan[]; prefixes: Uint32Array; @@ -21,7 +24,7 @@ type BlockMergeStreamContext = Pick void @@ -72,15 +75,19 @@ async function *blockPlanMergeStream( mColor.buffer as ArrayBuffer ]; if (mOther) transfer.push(mOther.buffer as ArrayBuffer); - const merged = await WorkerQueue.run('mergeGroups', { - pos: mPos, - geo: mGeo, - color: mColor, - sizes, - colorDim, - other: mOther, - otherDim - }, transfer); + const merged = await WorkerQueue.run( + 'mergeGroups', + { + pos: mPos, + geo: mGeo, + color: mColor, + sizes, + colorDim, + other: mOther, + otherDim + }, + transfer + ); mergedPos = merged.pos; mergedGeo = merged.geo; mergedColor = merged.color; @@ -106,8 +113,10 @@ async function *blockPlanMergeStream( py = mergedPos![mi * 3 + 1]; pz = mergedPos![mi * 3 + 2]; if (dest.geometric) dest.geometric.set(mergedGeo!.subarray(mi * 8, mi * 8 + 8), rows * 8); - if (dest.color) dest.color.set(mergedColor!.subarray(mi * colorDim, (mi + 1) * colorDim), rows * colorDim); - if (dest.other) dest.other.set(mergedOther!.subarray(mi * otherDim, (mi + 1) * otherDim), rows * otherDim); + if (dest.color) + dest.color.set(mergedColor!.subarray(mi * colorDim, (mi + 1) * colorDim), rows * colorDim); + if (dest.other) + dest.other.set(mergedOther!.subarray(mi * otherDim, (mi + 1) * otherDim), rows * otherDim); } if (dest.position) { dest.position[rows * 3] = px; diff --git a/src/lib/decimate/block-plan.ts b/src/lib/decimate/block-plan.ts index 135a68cd..44d81f5f 100644 --- a/src/lib/decimate/block-plan.ts +++ b/src/lib/decimate/block-plan.ts @@ -1,14 +1,13 @@ -import { type GraphicsDevice } from 'playcanvas'; +import type { GraphicsDevice } from 'playcanvas'; -import { - bestEdgesForPartition, - partitionBestOut, - type RecostState -} from './recost-core'; -import { MAX_GROUP, type SelectionResult } from './select'; import { GpuRecost, COMMIT_LOG_STRIDE } from '../gpu/gpu-recost'; -const NIL = 0xFFFFFFFF; +import { bestEdgesForPartition, partitionBestOut } from './recost-core'; +import type { RecostState } from './recost-core'; +import { MAX_GROUP } from './select'; +import type { SelectionResult } from './select'; + +const NIL = 0xffffffff; /** Quality-critical maximum validated commits between refreshes. */ const WAVE = 4096; @@ -126,22 +125,35 @@ const planBlockMerges = async (inputs: BlockPlanInputs): Promise => { let seqCounter = 0; const swap = (i: number, j: number): void => { - let t = hCost[i]; hCost[i] = hCost[j]; hCost[j] = t; - t = hA[i]; hA[i] = hA[j]; hA[j] = t; - t = hB[i]; hB[i] = hB[j]; hB[j] = t; - t = hSeq[i]; hSeq[i] = hSeq[j]; hSeq[j] = t; - t = hVb[i]; hVb[i] = hVb[j]; hVb[j] = t; + let t = hCost[i]; + hCost[i] = hCost[j]; + hCost[j] = t; + t = hA[i]; + hA[i] = hA[j]; + hA[j] = t; + t = hB[i]; + hB[i] = hB[j]; + hB[j] = t; + t = hSeq[i]; + hSeq[i] = hSeq[j]; + hSeq[j] = t; + t = hVb[i]; + hVb[i] = hVb[j]; + hVb[j] = t; heapIndex[hA[i]] = i; heapIndex[hA[j]] = j; }; - const less = (i: number, j: number): boolean => hCost[i] < hCost[j] || - (hCost[i] === hCost[j] && (hA[i] < hA[j] || (hA[i] === hA[j] && hB[i] < hB[j]))); + const less = (i: number, j: number): boolean => + hCost[i] < hCost[j] || (hCost[i] === hCost[j] && (hA[i] < hA[j] || (hA[i] === hA[j] && hB[i] < hB[j]))); const heapRemoveAt = (at: number): void => { heapIndex[hA[at]] = -1; heapSize--; if (at === heapSize) return; - hCost[at] = hCost[heapSize]; hA[at] = hA[heapSize]; hB[at] = hB[heapSize]; - hSeq[at] = hSeq[heapSize]; hVb[at] = hVb[heapSize]; + hCost[at] = hCost[heapSize]; + hA[at] = hA[heapSize]; + hB[at] = hB[heapSize]; + hSeq[at] = hSeq[heapSize]; + hVb[at] = hVb[heapSize]; heapIndex[hA[at]] = at; let i = at; if (i > 0 && less(i, (i - 1) >> 1)) { @@ -169,15 +181,29 @@ const planBlockMerges = async (inputs: BlockPlanInputs): Promise => { if (existing >= 0) heapRemoveAt(existing); if (heapSize === heapCap) { const next = heapCap * 2; - const nextCost = new Float32Array(next); nextCost.set(hCost); hCost = nextCost; - const nextA = new Uint32Array(next); nextA.set(hA); hA = nextA; - const nextB = new Uint32Array(next); nextB.set(hB); hB = nextB; - const nextSeq = new Uint32Array(next); nextSeq.set(hSeq); hSeq = nextSeq; - const nextVb = new Uint32Array(next); nextVb.set(hVb); hVb = nextVb; + const nextCost = new Float32Array(next); + nextCost.set(hCost); + hCost = nextCost; + const nextA = new Uint32Array(next); + nextA.set(hA); + hA = nextA; + const nextB = new Uint32Array(next); + nextB.set(hB); + hB = nextB; + const nextSeq = new Uint32Array(next); + nextSeq.set(hSeq); + hSeq = nextSeq; + const nextVb = new Uint32Array(next); + nextVb.set(hVb); + hVb = nextVb; heapCap = next; } let i = heapSize++; - hCost[i] = cost; hA[i] = a; hB[i] = b; hSeq[i] = seq; hVb[i] = vb; + hCost[i] = cost; + hA[i] = a; + hB[i] = b; + hSeq[i] = seq; + hVb[i] = vb; heapIndex[a] = i; while (i > 0) { const p = (i - 1) >> 1; @@ -189,7 +215,11 @@ const planBlockMerges = async (inputs: BlockPlanInputs): Promise => { const popped = { cost: 0, a: 0, b: 0, seq: 0, vb: 0 }; const heapPop = (): boolean => { if (heapSize === 0) return false; - popped.cost = hCost[0]; popped.a = hA[0]; popped.b = hB[0]; popped.seq = hSeq[0]; popped.vb = hVb[0]; + popped.cost = hCost[0]; + popped.a = hA[0]; + popped.b = hB[0]; + popped.seq = hSeq[0]; + popped.vb = hVb[0]; heapRemoveAt(0); return true; }; @@ -218,7 +248,7 @@ const planBlockMerges = async (inputs: BlockPlanInputs): Promise => { if (requireGpu && !gpuFits) { throw new Error( 'multi-block adaptive decimation requires a WebGPU block working set that fits the adapter limits ' + - `(core ${coreCount}, halo ${N - coreCount})` + `(core ${coreCount}, halo ${N - coreCount})` ); } const gpu = gpuFits ? new GpuRecost(device!, N, D, MAX_GROUP, WAVE, coreCount) : undefined; @@ -236,7 +266,13 @@ const planBlockMerges = async (inputs: BlockPlanInputs): Promise => { let heapPops = 0; let staleHeapPops = 0; - const refreshResult = (root: number, corePartner: number, coreCost: number, haloPartner: number, haloCost: number): void => { + const refreshResult = ( + root: number, + corePartner: number, + coreCost: number, + haloPartner: number, + haloCost: number + ): void => { const isFrozen = haloPartner !== NIL && haloCost <= coreCost; if (isFrozen) { if (frozenState[root] === 0) frozen++; @@ -262,8 +298,13 @@ const planBlockMerges = async (inputs: BlockPlanInputs): Promise => { continue; } const b = popped.b; - if (b === a || b >= coreCount || parent[b] !== b || - version[b] !== popped.vb || size[a] + size[b] > MAX_GROUP) { + if ( + b === a || + b >= coreCount || + parent[b] !== b || + version[b] !== popped.vb || + size[a] + size[b] > MAX_GROUP + ) { staleHeapPops++; queueRefresh(a); continue; @@ -420,11 +461,4 @@ const replayBlockPlan = (coreCount: number, plan: BlockPlan): SelectionResult => }; }; -export { - planBlockMerges, - replayBlockPlan, - WAVE, - type BlockPlan, - type BlockPlanDiagnostics, - type BlockPlanInputs -}; +export { planBlockMerges, replayBlockPlan, WAVE, type BlockPlan, type BlockPlanDiagnostics, type BlockPlanInputs }; diff --git a/src/lib/decimate/block-prepare.ts b/src/lib/decimate/block-prepare.ts index 9fe31c58..e8f25b40 100644 --- a/src/lib/decimate/block-prepare.ts +++ b/src/lib/decimate/block-prepare.ts @@ -1,11 +1,13 @@ -import { type GraphicsDevice } from 'playcanvas'; +import type { GraphicsDevice } from 'playcanvas'; -import { buildSplatCache, CACHE_STRIDE } from './edge-cost-cpu'; -import { KNN_SENTINEL } from './knn-core'; -import { type BlockView, gatherBlockView, type PriorityContext } from './priority'; import { GpuKnn } from '../gpu/gpu-knn'; import { WorkerQueue } from '../workers'; +import { buildSplatCache, CACHE_STRIDE } from './edge-cost-cpu'; +import { KNN_SENTINEL } from './knn-core'; +import { gatherBlockView } from './priority'; +import type { BlockView, PriorityContext } from './priority'; + type PreparedBlock = BlockView & { cache: Float32Array; /** Local row ids; rows `[0, ownedCount)` are core queries. */ @@ -68,7 +70,9 @@ const prepareGpuBlock = async ( const cand = new Uint32Array(k); for (let q = 0; q < ownedCount; q++) { let count = 0; - const qx = view.pos[q * 3], qy = view.pos[q * 3 + 1], qz = view.pos[q * 3 + 2]; + const qx = view.pos[q * 3], + qy = view.pos[q * 3 + 1], + qz = view.pos[q * 3 + 2]; for (let s = 0; s < k; s++) { const id = coreNeighbors[q * k + s]; if (id === KNN_SENTINEL) continue; diff --git a/src/lib/decimate/block-producer.ts b/src/lib/decimate/block-producer.ts index d310072e..cba9cfce 100644 --- a/src/lib/decimate/block-producer.ts +++ b/src/lib/decimate/block-producer.ts @@ -1,4 +1,4 @@ -import { type ChunkData, type ChunkSource, type ChunkSourceMetadata, type ReadRequest } from '../chunk'; +import type { ChunkData, ChunkSource, ChunkSourceMetadata, ReadRequest } from '../chunk'; /** * One output chunk's destination views for the merge stream — typed windows @@ -52,7 +52,7 @@ const createBlockProducerSource = ( } if (!generator) { generator = produce(); - await generator.next(); // prime: run to the first dest handshake + await generator.next(); // prime: run to the first dest handshake } const expected = Math.min(meta.chunkSize, meta.numGaussians - request.chunkIndex * meta.chunkSize); @@ -74,7 +74,9 @@ const createBlockProducerSource = ( throw new Error(`decimate output ended early at chunk ${request.chunkIndex}`); } if (value !== expected) { - throw new Error(`decimate output chunk ${request.chunkIndex}: expected ${expected} rows, produced ${value}`); + throw new Error( + `decimate output chunk ${request.chunkIndex}: expected ${expected} rows, produced ${value}` + ); } nextChunk++; diff --git a/src/lib/decimate/decimate-source.ts b/src/lib/decimate/decimate-source.ts index ed9603fa..d21f0479 100644 --- a/src/lib/decimate/decimate-source.ts +++ b/src/lib/decimate/decimate-source.ts @@ -1,34 +1,33 @@ import { join } from 'pathe'; -import { type GraphicsDevice } from 'playcanvas'; +import type { GraphicsDevice } from 'playcanvas'; -import { - allocatePlanPrefixes, - storeBlockPlan, - type StoredBlockPlan -} from './block-allocation'; -import { blockPlanMergeStream } from './block-merge-stream'; -import { planBlockMerges } from './block-plan'; -import { prepareGpuBlock, type PreparedBlock } from './block-prepare'; -import { createBlockProducerSource, type DestBuffers } from './block-producer'; -import { mergeStream } from './merge-stream'; -import { buildBlockHalo, kdPartition, coherenceRuns, type ResidentPositions } from './partition'; -import { runPriorityPass, type CandidateArrays } from './priority'; -import { selectMerges, type SelectionResult } from './select'; -import { selectMergesRecosted, CACHE_STRIDE } from './select-recost'; -import { - compact, - type ChunkDataPool, - type ChunkSource, - type ChunkSourceMetadata -} from '../chunk'; -import { type ReadFileSystem } from '../io/read'; -import { type FileSystem } from '../io/write'; +import { compact } from '../chunk'; +import type { ChunkDataPool, ChunkSource, ChunkSourceMetadata } from '../chunk'; +import type { ReadFileSystem } from '../io/read'; +import type { FileSystem } from '../io/write'; import { bakeTransform, permuteSource } from '../ops'; import { readPly } from '../readers/read-ply'; -import { type DeviceCreator } from '../types'; +import type { DeviceCreator } from '../types'; import { fmtBytes, fmtCount, logger, Transform } from '../utils'; import { writePlyStreaming } from '../writers/write-ply-streaming'; +import { allocatePlanPrefixes, storeBlockPlan } from './block-allocation'; +import type { StoredBlockPlan } from './block-allocation'; +import { blockPlanMergeStream } from './block-merge-stream'; +import { planBlockMerges } from './block-plan'; +import { prepareGpuBlock } from './block-prepare'; +import type { PreparedBlock } from './block-prepare'; +import { createBlockProducerSource } from './block-producer'; +import type { DestBuffers } from './block-producer'; +import { mergeStream } from './merge-stream'; +import { buildBlockHalo, kdPartition, coherenceRuns } from './partition'; +import type { ResidentPositions } from './partition'; +import { runPriorityPass } from './priority'; +import type { CandidateArrays } from './priority'; +import { selectMerges } from './select'; +import type { SelectionResult } from './select'; +import { selectMergesRecosted, CACHE_STRIDE } from './select-recost'; + /** Neighbours per query — unchanged from legacy. */ const KNN_K = 16; @@ -99,16 +98,17 @@ const chooseBlockSize = ( const residentIndex = n * 16; const available = Math.max(0, budget - residentInputBytes - residentIndex - outputPositionBytes); let blockSize = Math.min(BLOCK_SIZE, Math.max(1 << 16, Math.floor(available / MULTI_BLOCK_BYTES_PER_CORE))); - const limits = (device as unknown as { - limits?: { - maxStorageBufferBindingSize?: number; - maxBufferSize?: number; - }; - } | undefined)?.limits; - const bindingLimit = Math.min( - limits?.maxStorageBufferBindingSize ?? Infinity, - limits?.maxBufferSize ?? Infinity - ); + const limits = ( + device as unknown as + | { + limits?: { + maxStorageBufferBindingSize?: number; + maxBufferSize?: number; + }; + } + | undefined + )?.limits; + const bindingLimit = Math.min(limits?.maxStorageBufferBindingSize ?? Infinity, limits?.maxBufferSize ?? Infinity); if (Number.isFinite(bindingLimit)) { // The largest local binding is one half of the core+halo cache: // approximately coreCount × CACHE_STRIDE × sizeof(f32). @@ -236,26 +236,25 @@ const decimateSource = async ( partSub.end(); if (generation === 1 && N >= COHERENCE_MIN_N) { - const runs = blocks.map(b => coherenceRuns(order, b.start, b.end, COHERENCE_GAP_ROWS)).sort((a, b) => a - b); + const runs = blocks + .map((b) => coherenceRuns(order, b.start, b.end, COHERENCE_GAP_ROWS)) + .sort((a, b) => a - b); const median = runs[runs.length >> 1] ?? 0; if (median > INCOHERENT_RUNS_PER_BLOCK) { if (blocks.length > 1) { if (!opts.spill) { throw new Error( 'multi-block adaptive decimation needs scratch storage to stage spatially incoherent input; ' + - 'provide opts.spill / --scratch-dir' + 'provide opts.spill / --scratch-dir' ); } const rowBytes = 12 + 32 + colorDim * 4 + otherStride; logger.info( 'spatially incoherent input: staging one KD-ordered PLY ' + - `(estimated ${fmtBytes(N * rowBytes)})` + `(estimated ${fmtBytes(N * rowBytes)})` ); const spill = opts.spill; - const filename = join( - spill.scratchDir, - `.decimate-stage-${Date.now().toString(36)}.tmp.ply` - ); + const filename = join(spill.scratchDir, `.decimate-stage-${Date.now().toString(36)}.tmp.ply`); const stagedView = permuteSource(src, order); let plySrc: ChunkSource; try { @@ -266,7 +265,7 @@ const decimateSource = async ( try { await spill.remove?.(filename); } catch { - // Preserve the staging failure. + // Preserve the staging failure. } throw err; } @@ -318,20 +317,20 @@ const decimateSource = async ( if (!device) { throw new Error( `multi-block adaptive decimation requires WebGPU (${fmtCount(N)} splats, ` + - `${fmtCount(blockSize)}-splat cores); provide a device, or use --decimate-uniform` + `${fmtCount(blockSize)}-splat cores); provide a device, or use --decimate-uniform` ); } if (!opts.spill) { throw new Error( 'multi-block adaptive decimation needs scratch storage for merge plans ' + - `(approximately ${fmtBytes(N * 12)} this generation); provide opts.spill / --scratch-dir` + `(approximately ${fmtBytes(N * 12)} this generation); provide opts.spill / --scratch-dir` ); } if (generation === 1) { const rowBytes = 12 + 32 + colorDim * 4 + otherStride; logger.info( `decimate scratch estimate: staging up to ${fmtBytes(N * rowBytes)}; ` + - `merge plans up to ${fmtBytes(N * 12)} per generation` + `merge plans up to ${fmtBytes(N * 12)} per generation` ); } @@ -351,7 +350,6 @@ const decimateSource = async ( let allocationMs = 0; let preparedNext: Promise | null = null; - // eslint-disable-next-line no-loop-func const prepare = (bi: number): Promise => { const coreCount = blocks[bi].end - blocks[bi].start; const halo = buildBlockHalo(positions!, partition, bi, coreCount); @@ -405,11 +403,11 @@ const decimateSource = async ( try { await preparedNext; } catch { - // Preserve the active planning failure. + // Preserve the active planning failure. } } try { - await Promise.all(storedPlans.filter(Boolean).map(plan => opts.spill!.remove?.(plan.path))); + await Promise.all(storedPlans.filter(Boolean).map((plan) => opts.spill!.remove?.(plan.path))); } catch { // Preserve the planning failure. } @@ -419,16 +417,16 @@ const decimateSource = async ( } logger.info( `local merge stats: ${fmtCount(cappedHalos)} capped halo${cappedHalos === 1 ? '' : 's'}, ` + - `${fmtCount(frozen)} freezes, ${fmtCount(unfrozen)} unfreezes, ${fmtCount(removed!)} removals` + `${fmtCount(frozen)} freezes, ${fmtCount(unfrozen)} unfreezes, ${fmtCount(removed!)} removals` ); logger.info( `local planner work: ${fmtCount(planned)} planned, ${fmtCount(waves)} waves, ` + - `${fmtCount(refreshes)} refreshes, ${fmtCount(reverseInvalidations)} reverse invalidations, ` + - `${fmtCount(heapPops)} heap pops (${fmtCount(staleHeapPops)} stale)` + `${fmtCount(refreshes)} refreshes, ${fmtCount(reverseInvalidations)} reverse invalidations, ` + + `${fmtCount(heapPops)} heap pops (${fmtCount(staleHeapPops)} stale)` ); logger.info( `local timings: KNN/gather ${(knnMs / 1000).toFixed(2)}s, ` + - `refresh/plan ${(refreshMs / 1000).toFixed(2)}s, allocation ${(allocationMs / 1000).toFixed(2)}s` + `refresh/plan ${(refreshMs / 1000).toFixed(2)}s, allocation ${(allocationMs / 1000).toFixed(2)}s` ); } else { const baseBytes = residentInputBytes + N * (12 + K * 8 + K * 4 + 4) + 3 * 2 ** 30; @@ -436,26 +434,33 @@ const decimateSource = async ( // One block deliberately follows the pre-existing path with no // staging, halos, plan files, or k-way coordination. - const cand: CandidateArrays | undefined = recost ? - undefined : - { - idx: new Uint32Array(N * K).fill(0xFFFFFFFF), - cost: new Float32Array(N * K).fill(Infinity) - }; + const cand: CandidateArrays | undefined = recost + ? undefined + : { + idx: new Uint32Array(N * K).fill(0xffffffff), + cost: new Float32Array(N * K).fill(Infinity) + }; const cacheOut = recost ? new Float32Array(N * CACHE_STRIDE) : undefined; const neighborsOut = recost ? new Uint32Array(N * k) : undefined; const priorityBar = logger.bar('computing merge priorities', N); await runPriorityPass( { source: src, pool, pos: positions, order, blocks, device, K, k, cacheOut, neighborsOut }, cand, - n => priorityBar.tick(n) + (n) => priorityBar.tick(n) ); priorityBar.end(); const selectSub = logger.group(recost ? 'Selecting merges (re-costed)' : 'Selecting merges'); - selection = cacheOut ? - await selectMergesRecosted({ splatCache: cacheOut, neighbors: neighborsOut!, D: k, N, mergesNeeded: needed, device }) : - selectMerges(cand!, N, K, needed); + selection = cacheOut + ? await selectMergesRecosted({ + splatCache: cacheOut, + neighbors: neighborsOut!, + D: k, + N, + mergesNeeded: needed, + device + }) + : selectMerges(cand!, N, K, needed); selectSub.end(); removed = selection.removed; } @@ -464,7 +469,7 @@ const decimateSource = async ( const disposePlans = async (): Promise => { if (plansDisposed || !storedPlans) return; plansDisposed = true; - await Promise.all(storedPlans.map(plan => opts.spill?.remove?.(plan.path))); + await Promise.all(storedPlans.map((plan) => opts.spill?.remove?.(plan.path))); }; if (removed === 0) { @@ -473,18 +478,18 @@ const decimateSource = async ( boundaryRetries++; logger.warn( `no productive local cores at jitter ${generation}; repartitioning unchanged input ` + - `(${boundaryRetries}/8 boundary retries)` + `(${boundaryRetries}/8 boundary retries)` ); gen.end(); continue; } gen.end(); - const cause = device ? - 'the GPU step likely failed (e.g. out-of-memory) or produced non-finite costs' : - 'cost computation produced no finite merge candidates (e.g. non-finite inputs)'; + const cause = device + ? 'the GPU step likely failed (e.g. out-of-memory) or produced non-finite costs' + : 'cost computation produced no finite merge candidates (e.g. non-finite inputs)'; throw new Error( `decimation found no valid merges at ${N} splats (target ${targetCount}) — ${cause}. ` + - 'Refusing to return an incompletely-decimated scene.' + 'Refusing to return an incompletely-decimated scene.' ); } boundaryRetries = 0; @@ -494,9 +499,9 @@ const decimateSource = async ( gen.end(); throw new Error( `decimation stalled at ${N} splats (target ${targetCount}): a generation removed only ` + - `${removed} splat${removed === 1 ? '' : 's'} (${(removedFraction * 100).toFixed(3)}% of ${N}) — ` + - 'the nearest-neighbour graph is too degenerate to merge further (e.g. many coincident splats). ' + - 'Refusing to grind toward the target.' + `${removed} splat${removed === 1 ? '' : 's'} (${(removedFraction * 100).toFixed(3)}% of ${N}) — ` + + 'the nearest-neighbour graph is too degenerate to merge further (e.g. many coincident splats). ' + + 'Refusing to grind toward the target.' ); } @@ -516,57 +521,65 @@ const decimateSource = async ( }; const isFinal = outCount <= targetCount; - const nextPositions: ResidentPositions | undefined = isFinal ? undefined : { - x: new Float32Array(outCount), - y: new Float32Array(outCount), - z: new Float32Array(outCount) - }; + const nextPositions: ResidentPositions | undefined = isFinal + ? undefined + : { + x: new Float32Array(outCount), + y: new Float32Array(outCount), + z: new Float32Array(outCount) + }; // `src` is reassigned each generation; capture this generation's // values for the deferred producer closures. const genSrc = src; const genChunkSize = genSrc.meta.chunkSize; const genPositions = positions; - const createStream = ( - tick: (n: number) => void - ): AsyncGenerator => { + const createStream = (tick: (n: number) => void): AsyncGenerator => { if (storedPlans) { - return blockPlanMergeStream({ + return blockPlanMergeStream( + { + source: genSrc, + pool, + pos: genPositions, + order, + blocks, + plans: storedPlans, + prefixes: planPrefixes!, + scratch: opts.spill!, + nextPositions + }, + genChunkSize, + tick + ); + } + return mergeStream( + { source: genSrc, pool, pos: genPositions, order, blocks, - plans: storedPlans, - prefixes: planPrefixes!, - scratch: opts.spill!, + selection: selection!, nextPositions - }, genChunkSize, tick); - } - return mergeStream({ - source: genSrc, - pool, - pos: genPositions, - order, - blocks, - selection: selection!, - nextPositions - }, genChunkSize, tick); + }, + genChunkSize, + tick + ); }; if (isFinal) { - // The producer reads the input lazily while the consumer pulls - // chunks: the input chain (and any pending spill) is released on - // close. The merge bar lives outside the generation group since - // streaming happens after this function returns. + // The producer reads the input lazily while the consumer pulls + // chunks: the input chain (and any pending spill) is released on + // close. The merge bar lives outside the generation group since + // streaming happens after this function returns. gen.end(); const mergeBar = logger.bar('merging', N); - const producer = createBlockProducerSource(outMeta, () => createStream(n => mergeBar.tick(n))); + const producer = createBlockProducerSource(outMeta, () => createStream((n) => mergeBar.tick(n))); const disposeSpill = disposeCurrentInput; let closed = false; return { meta: producer.meta, - read: request => producer.read(request), + read: (request) => producer.read(request), close: async () => { if (closed) return; closed = true; @@ -589,7 +602,7 @@ const decimateSource = async ( } const mergeBar = logger.bar('merging', N); - const producer = createBlockProducerSource(outMeta, () => createStream(n => mergeBar.tick(n))); + const producer = createBlockProducerSource(outMeta, () => createStream((n) => mergeBar.tick(n))); // Intermediate generation: materialize (RAM when comfortably within // budget, else temp PLY spill), then advance the loop. @@ -606,11 +619,14 @@ const decimateSource = async ( if (!opts.spill) { throw new Error( `decimation intermediate generation needs ${fmtBytes(estBytes)}, over the in-memory budget — ` + - 'a spill location is required (opts.spill / --scratch-dir)' + 'a spill location is required (opts.spill / --scratch-dir)' ); } const spill = opts.spill; - const filename = join(spill.scratchDir, `.decimate-gen${generation}.${Date.now().toString(36)}.tmp.ply`); + const filename = join( + spill.scratchDir, + `.decimate-gen${generation}.${Date.now().toString(36)}.tmp.ply` + ); let plySrc: ChunkSource; try { await writePlyStreaming(producer, pool, { filename }, spill.writeFs); diff --git a/src/lib/decimate/edge-cost-cpu.ts b/src/lib/decimate/edge-cost-cpu.ts index cfb43af4..82befe43 100644 --- a/src/lib/decimate/edge-cost-cpu.ts +++ b/src/lib/decimate/edge-cost-cpu.ts @@ -26,14 +26,8 @@ * Engine-free. */ -import { - EPS_COV, - sigmoid, - ellipsoidArea, - quatToRotmat, - sigmaFromRotVar, - type SplatView -} from './moment-match'; +import { EPS_COV, sigmoid, ellipsoidArea, quatToRotmat, sigmaFromRotVar } from './moment-match'; +import type { SplatView } from './moment-match'; /** SH band-0 constant (f_dc → base colour: 0.5 + C0·f_dc). */ const C0 = 0.28209479177387814; @@ -96,18 +90,30 @@ const buildSplatCache = (view: SplatView, out: Float32Array): number => { const sy = Math.max(Math.exp(geo[i8 + 5]), 1e-12); const sz = Math.max(Math.exp(geo[i8 + 6]), 1e-12); - let qw = geo[i8], qx = geo[i8 + 1], qy = geo[i8 + 2], qz = geo[i8 + 3]; + let qw = geo[i8], + qx = geo[i8 + 1], + qy = geo[i8 + 2], + qz = geo[i8 + 3]; const invq = 1 / Math.max(Math.hypot(qw, qx, qy, qz), 1e-12); - qw *= invq; qx *= invq; qy *= invq; qz *= invq; + qw *= invq; + qx *= invq; + qy *= invq; + qz *= invq; quatToRotmat(qw, qx, qy, qz, R, 0); sigmaFromRotVar(R, 0, sx * sx, sy * sy, sz * sz, S, 0); const o = i * CACHE_STRIDE; const i3 = 3 * i; - out[o] = pos[i3]; out[o + 1] = pos[i3 + 1]; out[o + 2] = pos[i3 + 2]; - out[o + 3] = S[0]; out[o + 4] = S[1]; out[o + 5] = S[2]; - out[o + 6] = S[4]; out[o + 7] = S[5]; out[o + 8] = S[8]; + out[o] = pos[i3]; + out[o + 1] = pos[i3 + 1]; + out[o + 2] = pos[i3 + 2]; + out[o + 3] = S[0]; + out[o + 4] = S[1]; + out[o + 5] = S[2]; + out[o + 6] = S[4]; + out[o + 7] = S[5]; + out[o + 8] = S[8]; out[o + 9] = sx * sy * sz; out[o + 10] = a; out[o + 11] = a * ellipsoidArea(sx, sy, sz) + 1e-30; @@ -115,7 +121,9 @@ const buildSplatCache = (view: SplatView, out: Float32Array): number => { const br = 0.5 + C0 * color[i * colorDim]; const bg = 0.5 + C0 * color[i * colorDim + 1]; const bb = 0.5 + C0 * color[i * colorDim + 2]; - out[o + 12] = br; out[o + 13] = bg; out[o + 14] = bb; + out[o + 12] = br; + out[o + 13] = bg; + out[o + 14] = bb; out[o + 15] = br * br + bg * bg + bb * bb; } @@ -125,9 +133,17 @@ const buildSplatCache = (view: SplatView, out: Float32Array): number => { // dᵀ(A)⁻¹d and √|A| for a symmetric 3×3 A = [xx,xy,xz,yy,yz,zz]; returns the // Gaussian cross-product ⟨G_a,G_b⟩ scaled by the caller's √|Σ_a|·√|Σ_b|. const crossG = ( - sdA: number, sdB: number, - xx: number, xy: number, xz: number, yy: number, yz: number, zz: number, - dx: number, dy: number, dz: number + sdA: number, + sdB: number, + xx: number, + xy: number, + xz: number, + yy: number, + yz: number, + zz: number, + dx: number, + dy: number, + dz: number ): number => { // Signed cofactors (adj is symmetric); det via row-0 expansion. const c00 = yy * zz - yz * yz; @@ -137,10 +153,9 @@ const crossG = ( const c12 = xy * xz - xx * yz; const c22 = xx * yy - xy * xy; const det = Math.max(xx * c00 + xy * c01 + xz * c02, 1e-30); - const quadAdj = c00 * dx * dx + c11 * dy * dy + c22 * dz * dz + - 2 * (c01 * dx * dy + c02 * dx * dz + c12 * dy * dz); + const quadAdj = c00 * dx * dx + c11 * dy * dy + c22 * dz * dz + 2 * (c01 * dx * dy + c02 * dx * dz + c12 * dy * dz); const quad = quadAdj / det; - return TWO_PI_1_5 * sdA * sdB / Math.sqrt(det) * Math.exp(-0.5 * quad); + return ((TWO_PI_1_5 * sdA * sdB) / Math.sqrt(det)) * Math.exp(-0.5 * quad); }; /** @@ -151,17 +166,19 @@ const crossG = ( * @param j - Second splat (cache row). * @returns The edge cost (≥ 0). */ -const computeEdgeCost = ( - cache: Float32Array, - i: number, - j: number -): number => { - const io = i * CACHE_STRIDE, jo = j * CACHE_STRIDE; +const computeEdgeCost = (cache: Float32Array, i: number, j: number): number => { + const io = i * CACHE_STRIDE, + jo = j * CACHE_STRIDE; - const mux = cache[io], muy = cache[io + 1], muz = cache[io + 2]; - const mvx = cache[jo], mvy = cache[jo + 1], mvz = cache[jo + 2]; + const mux = cache[io], + muy = cache[io + 1], + muz = cache[io + 2]; + const mvx = cache[jo], + mvy = cache[jo + 1], + mvz = cache[jo + 2]; - const mi = cache[io + 11], mj = cache[jo + 11]; + const mi = cache[io + 11], + mj = cache[jo + 11]; const W = mi + mj; const pi = W > 0 ? mi / W : 0.5; const pj = 1 - pi; @@ -171,8 +188,12 @@ const computeEdgeCost = ( const mmy = pi * muy + pj * mvy; const mmz = pi * muz + pj * mvz; - const dix = mux - mmx, diy = muy - mmy, diz = muz - mmz; - const djx = mvx - mmx, djy = mvy - mmy, djz = mvz - mmz; + const dix = mux - mmx, + diy = muy - mmy, + diz = muz - mmz; + const djx = mvx - mmx, + djy = mvy - mmy, + djz = mvz - mmz; // Merged covariance: Σ pₖ(δₖδₖᵀ + Σₖ) + EPS_COV·I (law of total variance). const sxx = pi * (dix * dix + cache[io + 3]) + pj * (djx * djx + cache[jo + 3]) + EPS_COV; @@ -194,19 +215,25 @@ const computeEdgeCost = ( const p1 = sxy * sxy + sxz * sxz + syz * syz; let e0: number, e1: number, e2: number; if (p1 <= 1e-30) { - e0 = sxx; e1 = syy; e2 = szz; + e0 = sxx; + e1 = syy; + e2 = szz; } else { const p2 = (sxx - q) * (sxx - q) + (syy - q) * (syy - q) + (szz - q) * (szz - q) + 2 * p1; const p = Math.sqrt(p2 / 6); const ip = 1 / p; - const b00 = (sxx - q) * ip, b11 = (syy - q) * ip, b22 = (szz - q) * ip; - const b01 = sxy * ip, b02 = sxz * ip, b12 = syz * ip; + const b00 = (sxx - q) * ip, + b11 = (syy - q) * ip, + b22 = (szz - q) * ip; + const b01 = sxy * ip, + b02 = sxz * ip, + b12 = syz * ip; const detB = b00 * (b11 * b22 - b12 * b12) - b01 * (b01 * b22 - b12 * b02) + b02 * (b01 * b12 - b11 * b02); let r = detB / 2; - r = r < -1 ? -1 : (r > 1 ? 1 : r); + r = r < -1 ? -1 : r > 1 ? 1 : r; const phi = Math.acos(r) / 3; e0 = q + 2 * p * Math.cos(phi); - e2 = q + 2 * p * Math.cos(phi + 2 * Math.PI / 3); + e2 = q + 2 * p * Math.cos(phi + (2 * Math.PI) / 3); e1 = 3 * q - e0 - e2; } const s0 = Math.sqrt(Math.max(e0, 1e-18)); @@ -215,17 +242,24 @@ const computeEdgeCost = ( const alphaM = Math.min(1, W / Math.max(ellipsoidArea(s0, s1, s2), 1e-30)); // Base colours and their dots (merged colour = mass-weighted average). - const bir = cache[io + 12], big = cache[io + 13], bib = cache[io + 14]; - const bjr = cache[jo + 12], bjg = cache[jo + 13], bjb = cache[jo + 14]; - const bij = bir * bjr + big * bjg + bib * bjb; // base_i · base_j - const bni = cache[io + 15]; // base_i · base_i - const bnj = cache[jo + 15]; // base_j · base_j - const bim = pi * bni + pj * bij; // base_i · base_m - const bjm = pi * bij + pj * bnj; // base_j · base_m + const bir = cache[io + 12], + big = cache[io + 13], + bib = cache[io + 14]; + const bjr = cache[jo + 12], + bjg = cache[jo + 13], + bjb = cache[jo + 14]; + const bij = bir * bjr + big * bjg + bib * bjb; // base_i · base_j + const bni = cache[io + 15]; // base_i · base_i + const bnj = cache[jo + 15]; // base_j · base_j + const bim = pi * bni + pj * bij; // base_i · base_m + const bjm = pi * bij + pj * bnj; // base_j · base_m const bnm = pi * pi * bni + 2 * pi * pj * bij + pj * pj * bnj; // base_m · base_m - const ai = cache[io + 10], aj = cache[jo + 10], am = alphaM; - const sdi = cache[io + 9], sdj = cache[jo + 9]; + const ai = cache[io + 10], + aj = cache[jo + 10], + am = alphaM; + const sdi = cache[io + 9], + sdj = cache[jo + 9]; // Self terms ⟨G,G⟩ = π^{3/2}·√|Σ|. const selfI = ai * ai * bni * PI_1_5 * sdi; @@ -233,23 +267,47 @@ const computeEdgeCost = ( const selfM = am * am * bnm * PI_1_5 * sqrtDetM; // Cross terms (amplitude dot × Gaussian overlap). - const cIJ = crossG(sdi, sdj, - cache[io + 3] + cache[jo + 3], cache[io + 4] + cache[jo + 4], cache[io + 5] + cache[jo + 5], - cache[io + 6] + cache[jo + 6], cache[io + 7] + cache[jo + 7], cache[io + 8] + cache[jo + 8], - mux - mvx, muy - mvy, muz - mvz); - const cIM = crossG(sdi, sqrtDetM, - cache[io + 3] + sxx, cache[io + 4] + sxy, cache[io + 5] + sxz, - cache[io + 6] + syy, cache[io + 7] + syz, cache[io + 8] + szz, - dix, diy, diz); - const cJM = crossG(sdj, sqrtDetM, - cache[jo + 3] + sxx, cache[jo + 4] + sxy, cache[jo + 5] + sxz, - cache[jo + 6] + syy, cache[jo + 7] + syz, cache[jo + 8] + szz, - djx, djy, djz); + const cIJ = crossG( + sdi, + sdj, + cache[io + 3] + cache[jo + 3], + cache[io + 4] + cache[jo + 4], + cache[io + 5] + cache[jo + 5], + cache[io + 6] + cache[jo + 6], + cache[io + 7] + cache[jo + 7], + cache[io + 8] + cache[jo + 8], + mux - mvx, + muy - mvy, + muz - mvz + ); + const cIM = crossG( + sdi, + sqrtDetM, + cache[io + 3] + sxx, + cache[io + 4] + sxy, + cache[io + 5] + sxz, + cache[io + 6] + syy, + cache[io + 7] + syz, + cache[io + 8] + szz, + dix, + diy, + diz + ); + const cJM = crossG( + sdj, + sqrtDetM, + cache[jo + 3] + sxx, + cache[jo + 4] + sxy, + cache[jo + 5] + sxz, + cache[jo + 6] + syy, + cache[jo + 7] + syz, + cache[jo + 8] + szz, + djx, + djy, + djz + ); - const E = selfI + selfJ + selfM + - 2 * ai * aj * bij * cIJ - - 2 * ai * am * bim * cIM - - 2 * aj * am * bjm * cJM; + const E = selfI + selfJ + selfM + 2 * ai * aj * bij * cIJ - 2 * ai * am * bim * cIM - 2 * aj * am * bjm * cJM; // Clamp float-noise negatives (both terms are squared norms ≥ 0; the // colour distance uses separately-rounded f32 norms so it too can round diff --git a/src/lib/decimate/index.ts b/src/lib/decimate/index.ts index 246fe701..d13fb21e 100644 --- a/src/lib/decimate/index.ts +++ b/src/lib/decimate/index.ts @@ -1,3 +1,11 @@ export { decimateSource, type DecimateOptions, type DecimateSpill } from './decimate-source'; -export { mergeGroup, createMergeScratch, splatMass, makeGaussianSamples, type SplatView, type MergedOut, type MergeScratch } from './moment-match'; +export { + mergeGroup, + createMergeScratch, + splatMass, + makeGaussianSamples, + type SplatView, + type MergedOut, + type MergeScratch +} from './moment-match'; export { kdPartition, coherenceRuns, type BlockRange, type ResidentPositions } from './partition'; diff --git a/src/lib/decimate/knn-core.ts b/src/lib/decimate/knn-core.ts index ddc08d80..19dd7f3e 100644 --- a/src/lib/decimate/knn-core.ts +++ b/src/lib/decimate/knn-core.ts @@ -1,7 +1,7 @@ -import { type FlatKdTree } from '../spatial/kd-tree'; +import type { FlatKdTree } from '../spatial/kd-tree'; /** Marks an unfilled neighbour slot (fewer than k non-self points available). */ -const KNN_SENTINEL = 0xFFFFFFFF; +const KNN_SENTINEL = 0xffffffff; /** A forest part: flat KD subtree (GLOBAL splat ids) plus its point AABB. */ type ForestPart = FlatKdTree & { @@ -68,7 +68,7 @@ const knnForestQuery = ( stack[sp++] = part.rootIdx; while (sp > 0) { const packed = stack[--sp]; - const nodeIdx = packed & 0x3FFFFFFF; + const nodeIdx = packed & 0x3fffffff; const axis = packed >>> 30; const np = nodeIdx * 3; @@ -78,7 +78,9 @@ const knnForestQuery = ( const splatId = nodeSplatIdx[nodeIdx]; if (splatId !== qid) { - const dx = nx - qx, dy = ny - qy, dz = nz - qz; + const dx = nx - qx, + dy = ny - qy, + dz = nz - qz; const d2 = dx * dx + dy * dy + dz * dz; if (d2 < worst) { topDist[worstIdx] = d2; @@ -87,7 +89,8 @@ const knnForestQuery = ( let wi = 0; for (let i = 1; i < k; i++) { if (topDist[i] > w) { - w = topDist[i]; wi = i; + w = topDist[i]; + wi = i; } } worst = w; @@ -95,8 +98,8 @@ const knnForestQuery = ( } } - const qAxisVal = axis === 0 ? qx : (axis === 1 ? qy : qz); - const nAxisVal = axis === 0 ? nx : (axis === 1 ? ny : nz); + const qAxisVal = axis === 0 ? qx : axis === 1 ? qy : qz; + const nAxisVal = axis === 0 ? nx : axis === 1 ? ny : nz; const delta = qAxisVal - nAxisVal; const nextAxis = axis + 1 >= 3 ? 0 : axis + 1; const nextAxisPacked = nextAxis << 30; diff --git a/src/lib/decimate/merge-stream.ts b/src/lib/decimate/merge-stream.ts index 15c163db..13445ecd 100644 --- a/src/lib/decimate/merge-stream.ts +++ b/src/lib/decimate/merge-stream.ts @@ -1,9 +1,11 @@ -import { type DestBuffers } from './block-producer'; -import { type ResidentPositions } from './partition'; -import { gatherBlockView, indexOfSorted, type PriorityContext } from './priority'; -import { type SelectionResult } from './select'; import { WorkerQueue } from '../workers'; +import type { DestBuffers } from './block-producer'; +import type { ResidentPositions } from './partition'; +import { gatherBlockView, indexOfSorted } from './priority'; +import type { PriorityContext } from './priority'; +import type { SelectionResult } from './select'; + /** Context for the merge stream: the priority context plus the selection. */ type MergeStreamContext = Pick & { selection: SelectionResult; @@ -32,7 +34,7 @@ type MergeStreamContext = Pick void @@ -47,7 +49,7 @@ async function *mergeStream( let rows = 0; let emitted = 0; - let dest = yield 0; // priming handshake: the first real chunk's views + let dest = yield 0; // priming handshake: the first real chunk's views for (let bi = 0; bi < blocks.length; bi++) { const block = blocks[bi]; @@ -110,17 +112,25 @@ async function *mergeStream( mi++; } } - const transfer: ArrayBuffer[] = [mPos.buffer as ArrayBuffer, mGeo.buffer as ArrayBuffer, mColor.buffer as ArrayBuffer]; + const transfer: ArrayBuffer[] = [ + mPos.buffer as ArrayBuffer, + mGeo.buffer as ArrayBuffer, + mColor.buffer as ArrayBuffer + ]; if (mOther) transfer.push(mOther.buffer as ArrayBuffer); - const merged = await WorkerQueue.run('mergeGroups', { - pos: mPos, - geo: mGeo, - color: mColor, - sizes, - colorDim, - other: mOther, - otherDim - }, transfer); + const merged = await WorkerQueue.run( + 'mergeGroups', + { + pos: mPos, + geo: mGeo, + color: mColor, + sizes, + colorDim, + other: mOther, + otherDim + }, + transfer + ); mergedPos = merged.pos; mergedGeo = merged.geo; mergedColor = merged.color; @@ -132,7 +142,7 @@ async function *mergeStream( for (let i = 0; i < nOwned; i++) { const g = owned[i]; const mg = memberGroup[g]; - if (mg !== -1 && groupMin[mg] !== g) continue; // consumed member + if (mg !== -1 && groupMin[mg] !== g) continue; // consumed member let px: number, py: number, pz: number; if (mg === -1) { @@ -150,8 +160,10 @@ async function *mergeStream( py = mergedPos![mi * 3 + 1]; pz = mergedPos![mi * 3 + 2]; if (dest.geometric) dest.geometric.set(mergedGeo!.subarray(mi * 8, mi * 8 + 8), rows * 8); - if (dest.color) dest.color.set(mergedColor!.subarray(mi * colorDim, (mi + 1) * colorDim), rows * colorDim); - if (dest.other) dest.other.set(mergedOther!.subarray(mi * otherDim, (mi + 1) * otherDim), rows * otherDim); + if (dest.color) + dest.color.set(mergedColor!.subarray(mi * colorDim, (mi + 1) * colorDim), rows * colorDim); + if (dest.other) + dest.other.set(mergedOther!.subarray(mi * otherDim, (mi + 1) * otherDim), rows * otherDim); } if (dest.position) { dest.position[rows * 3] = px; diff --git a/src/lib/decimate/moment-match.ts b/src/lib/decimate/moment-match.ts index 2a67400d..50b621c0 100644 --- a/src/lib/decimate/moment-match.ts +++ b/src/lib/decimate/moment-match.ts @@ -64,9 +64,15 @@ const makeGaussianSamples = (n: number, seed: number): Float64Array[] => { type Mat9 = Float32Array | Float64Array; const quatToRotmat = (qw: number, qx: number, qy: number, qz: number, out: Mat9, o: number) => { - const xx = qx * qx, yy = qy * qy, zz = qz * qz; - const wx = qw * qx, wy = qw * qy, wz = qw * qz; - const xy = qx * qy, xz = qx * qz, yz = qy * qz; + const xx = qx * qx, + yy = qy * qy, + zz = qz * qz; + const wx = qw * qx, + wy = qw * qy, + wz = qw * qz; + const xy = qx * qy, + xz = qx * qz, + yz = qy * qz; out[o] = 1 - 2 * (yy + zz); out[o + 1] = 2 * (xy - wz); out[o + 2] = 2 * (xz + wy); @@ -79,9 +85,15 @@ const quatToRotmat = (qw: number, qx: number, qy: number, qz: number, out: Mat9, }; const sigmaFromRotVar = (R: Mat9, r: number, vx: number, vy: number, vz: number, out: Mat9, o: number) => { - const r00 = R[r], r01 = R[r + 1], r02 = R[r + 2]; - const r10 = R[r + 3], r11 = R[r + 4], r12 = R[r + 5]; - const r20 = R[r + 6], r21 = R[r + 7], r22 = R[r + 8]; + const r00 = R[r], + r01 = R[r + 1], + r02 = R[r + 2]; + const r10 = R[r + 3], + r11 = R[r + 4], + r12 = R[r + 5]; + const r20 = R[r + 6], + r21 = R[r + 7], + r22 = R[r + 8]; out[o] = r00 * r00 * vx + r01 * r01 * vy + r02 * r02 * vz; out[o + 1] = r00 * r10 * vx + r01 * r11 * vy + r02 * r12 * vz; out[o + 2] = r00 * r20 * vx + r01 * r21 * vy + r02 * r22 * vz; @@ -102,12 +114,22 @@ const det3 = (A: Mat9, o: number) => { }; const gaussLogpdfDiagrot = ( - x: number, y: number, z: number, - mx: number, my: number, mz: number, - R: Mat9, ro: number, - invx: number, invy: number, invz: number, logdet: number + x: number, + y: number, + z: number, + mx: number, + my: number, + mz: number, + R: Mat9, + ro: number, + invx: number, + invy: number, + invz: number, + logdet: number ) => { - const dx = x - mx, dy = y - my, dz = z - mz; + const dx = x - mx, + dy = y - my, + dz = z - mz; const y0 = dx * R[ro] + dy * R[ro + 3] + dz * R[ro + 6]; const y1 = dx * R[ro + 1] + dy * R[ro + 4] + dz * R[ro + 7]; const y2 = dx * R[ro + 2] + dy * R[ro + 5] + dz * R[ro + 8]; @@ -119,23 +141,38 @@ const gaussLogpdfDiagrot = ( // eigenvalues land on A's diagonal, eigenvectors in V's columns. const eigenSymmetric3x3 = (Ain: Float64Array, A: Float64Array, V: Float64Array) => { A.set(Ain); - V[0] = 1; V[1] = 0; V[2] = 0; - V[3] = 0; V[4] = 1; V[5] = 0; - V[6] = 0; V[7] = 0; V[8] = 1; + V[0] = 1; + V[1] = 0; + V[2] = 0; + V[3] = 0; + V[4] = 1; + V[5] = 0; + V[6] = 0; + V[7] = 0; + V[8] = 1; for (let iter = 0; iter < 24; iter++) { - let p = 0, q = 1; + let p = 0, + q = 1; let maxAbs = Math.abs(A[1]); if (Math.abs(A[2]) > maxAbs) { - p = 0; q = 2; maxAbs = Math.abs(A[2]); + p = 0; + q = 2; + maxAbs = Math.abs(A[2]); } if (Math.abs(A[5]) > maxAbs) { - p = 1; q = 2; maxAbs = Math.abs(A[5]); + p = 1; + q = 2; + maxAbs = Math.abs(A[5]); } if (maxAbs < 1e-12) break; - const pp = 3 * p + p, qq = 3 * q + q, pq = 3 * p + q; - const app = A[pp], aqq = A[qq], apq = A[pq]; + const pp = 3 * p + p, + qq = 3 * q + q, + pq = 3 * p + q; + const app = A[pp], + aqq = A[qq], + apq = A[pq]; const tau = (aqq - app) / (2 * apq); const t = Math.sign(tau) / (Math.abs(tau) + Math.sqrt(1 + tau * tau)); const c = 1 / Math.sqrt(1 + t * t); @@ -143,9 +180,12 @@ const eigenSymmetric3x3 = (Ain: Float64Array, A: Float64Array, V: Float64Array) for (let k = 0; k < 3; k++) { if (k === p || k === q) continue; - const kp = 3 * k + p, kq = 3 * k + q; - const pk = 3 * p + k, qk = 3 * q + k; - const akp = A[kp], akq = A[kq]; + const kp = 3 * k + p, + kq = 3 * k + q; + const pk = 3 * p + k, + qk = 3 * q + k; + const akp = A[kp], + akq = A[kq]; A[kp] = c * akp - s * akq; A[pk] = A[kp]; A[kq] = s * akp + c * akq; @@ -153,11 +193,14 @@ const eigenSymmetric3x3 = (Ain: Float64Array, A: Float64Array, V: Float64Array) } A[pp] = c * c * app - 2 * s * c * apq + s * s * aqq; A[qq] = s * s * app + 2 * s * c * apq + c * c * aqq; - A[pq] = 0; A[3 * q + p] = 0; + A[pq] = 0; + A[3 * q + p] = 0; for (let k = 0; k < 3; k++) { - const kp = 3 * k + p, kq = 3 * k + q; - const vkp = V[kp], vkq = V[kq]; + const kp = 3 * k + p, + kq = 3 * k + q; + const vkp = V[kp], + vkq = V[kq]; V[kp] = c * vkp - s * vkq; V[kq] = s * vkp + c * vkq; } @@ -165,7 +208,9 @@ const eigenSymmetric3x3 = (Ain: Float64Array, A: Float64Array, V: Float64Array) }; const rotmatToQuat = (R: Float64Array, o: number, out: Float64Array, oo: number) => { - const m00 = R[o], m11 = R[o + 4], m22 = R[o + 8]; + const m00 = R[o], + m11 = R[o + 4], + m22 = R[o + 8]; const tr = m00 + m11 + m22; let qw: number, qx: number, qy: number, qz: number; @@ -322,7 +367,9 @@ const mergeGroup = ( for (let m = 0; m < count; m++) weights[m] /= W; // Merged mean (weighted). - let mux = 0, muy = 0, muz = 0; + let mux = 0, + muy = 0, + muz = 0; for (let m = 0; m < count; m++) { const i = members[m]; const p = weights[m]; @@ -341,9 +388,15 @@ const mergeGroup = ( const i8 = i * 8; const p = weights[m]; - let qw = geo[i8], qx = geo[i8 + 1], qy = geo[i8 + 2], qz = geo[i8 + 3]; + let qw = geo[i8], + qx = geo[i8 + 1], + qy = geo[i8 + 2], + qz = geo[i8 + 3]; const qn = 1 / Math.max(Math.hypot(qw, qx, qy, qz), 1e-12); - qw *= qn; qx *= qn; qy *= qn; qz *= qn; + qw *= qn; + qx *= qn; + qy *= qn; + qz *= qn; const sx = Math.max(Math.exp(geo[i8 + 4]), 1e-12); const sy = Math.max(Math.exp(geo[i8 + 5]), 1e-12); const sz = Math.max(Math.exp(geo[i8 + 6]), 1e-12); @@ -351,7 +404,9 @@ const mergeGroup = ( quatToRotmat(qw, qx, qy, qz, Ri, 0); sigmaFromRotVar(Ri, 0, sx * sx, sy * sy, sz * sz, SigI, 0); - const dx = pos[i * 3] - mux, dy = pos[i * 3 + 1] - muy, dz = pos[i * 3 + 2] - muz; + const dx = pos[i * 3] - mux, + dy = pos[i * 3 + 1] - muy, + dz = pos[i * 3 + 2] - muz; Sig[0] += p * (dx * dx + SigI[0]); Sig[1] += p * (dx * dy + SigI[1]); Sig[2] += p * (dx * dz + SigI[2]); @@ -372,23 +427,37 @@ const mergeGroup = ( eigenSymmetric3x3(Sig, eigA, eigV); const vecs = eigV; - const v0 = eigA[0], v1 = eigA[4], v2 = eigA[8]; + const v0 = eigA[0], + v1 = eigA[4], + v2 = eigA[8]; let o0: number, o1: number, o2: number; if (v0 >= v1) { - if (v1 >= v2) { - o0 = 0; o1 = 1; o2 = 2; + if (v1 >= v2) { + o0 = 0; + o1 = 1; + o2 = 2; } else if (v0 >= v2) { - o0 = 0; o1 = 2; o2 = 1; - } else { - o0 = 2; o1 = 0; o2 = 1; + o0 = 0; + o1 = 2; + o2 = 1; + } else { + o0 = 2; + o1 = 0; + o2 = 1; } } else { - if (v0 >= v2) { - o0 = 1; o1 = 0; o2 = 2; + if (v0 >= v2) { + o0 = 1; + o1 = 0; + o2 = 2; } else if (v1 >= v2) { - o0 = 1; o1 = 2; o2 = 0; - } else { - o0 = 2; o1 = 1; o2 = 0; + o0 = 1; + o1 = 2; + o2 = 0; + } else { + o0 = 2; + o1 = 1; + o2 = 0; } } const ev0 = Math.max(eigA[3 * o0 + o0], 1e-18); @@ -403,15 +472,25 @@ const mergeGroup = ( const alphaM = Math.min(1, W / Math.max(ellipsoidArea(s0, s1, s2), 1e-30)); const Rm = scratch.rM; - Rm[0] = vecs[o0]; Rm[1] = vecs[o1]; Rm[2] = vecs[o2]; - Rm[3] = vecs[3 + o0]; Rm[4] = vecs[3 + o1]; Rm[5] = vecs[3 + o2]; - Rm[6] = vecs[6 + o0]; Rm[7] = vecs[6 + o1]; Rm[8] = vecs[6 + o2]; + Rm[0] = vecs[o0]; + Rm[1] = vecs[o1]; + Rm[2] = vecs[o2]; + Rm[3] = vecs[3 + o0]; + Rm[4] = vecs[3 + o1]; + Rm[5] = vecs[3 + o2]; + Rm[6] = vecs[6 + o0]; + Rm[7] = vecs[6 + o1]; + Rm[8] = vecs[6 + o2]; if (det3(Rm, 0) < 0) { - Rm[2] *= -1; Rm[5] *= -1; Rm[8] *= -1; + Rm[2] *= -1; + Rm[5] *= -1; + Rm[8] *= -1; } rotmatToQuat(Rm, 0, out.geo, 0); - out.pos[0] = mux; out.pos[1] = muy; out.pos[2] = muz; + out.pos[0] = mux; + out.pos[1] = muy; + out.pos[2] = muz; out.geo[4] = Math.log(s0); out.geo[5] = Math.log(s1); out.geo[6] = Math.log(s2); diff --git a/src/lib/decimate/partition.ts b/src/lib/decimate/partition.ts index 5c8fc4c1..1046a517 100644 --- a/src/lib/decimate/partition.ts +++ b/src/lib/decimate/partition.ts @@ -111,7 +111,8 @@ const kdPartition = (pos: ResidentPositions, blockSize: number, generation: numb blocks.push({ start, end, aabb, residual }); return; } - let axis = 0, ext = -Infinity; + let axis = 0, + ext = -Infinity; for (let c = 0; c < 3; c++) { const e = aabb[3 + c] - aabb[c]; if (e > ext) { @@ -125,7 +126,7 @@ const kdPartition = (pos: ResidentPositions, blockSize: number, generation: numb // deterministic. const count = end - start; const upper = generation !== null && ((generation + depth + branch) & 1) !== 0; - const fraction = generation === null ? 1 / 2 : (upper ? 5 / 8 : 3 / 8); + const fraction = generation === null ? 1 / 2 : upper ? 5 / 8 : 3 / 8; const offset = Math.max(1, Math.min(count - 1, Math.floor(count * fraction))); const mid = start + offset; quickselect(cols[axis], order.subarray(start, end), mid - start); @@ -142,17 +143,30 @@ const kdPartition = (pos: ResidentPositions, blockSize: number, generation: numb const { lo, hi } = fence; let out = 0; for (let i = 0; i < n; i++) { - if (cols[0][i] < lo[0] || cols[0][i] > hi[0] || - cols[1][i] < lo[1] || cols[1][i] > hi[1] || - cols[2][i] < lo[2] || cols[2][i] > hi[2]) out++; + if ( + cols[0][i] < lo[0] || + cols[0][i] > hi[0] || + cols[1][i] < lo[1] || + cols[1][i] > hi[1] || + cols[2][i] < lo[2] || + cols[2][i] > hi[2] + ) + out++; } if (out > 0 && out <= n * OUTLIER_MAX_FRACTION) { coreEnd = n - out; - let c = 0, o = coreEnd; + let c = 0, + o = coreEnd; for (let i = 0; i < n; i++) { - if (cols[0][i] < lo[0] || cols[0][i] > hi[0] || - cols[1][i] < lo[1] || cols[1][i] > hi[1] || - cols[2][i] < lo[2] || cols[2][i] > hi[2]) order[o++] = i; + if ( + cols[0][i] < lo[0] || + cols[0][i] > hi[0] || + cols[1][i] < lo[1] || + cols[1][i] > hi[1] || + cols[2][i] < lo[2] || + cols[2][i] > hi[2] + ) + order[o++] = i; else order[c++] = i; } } @@ -165,16 +179,16 @@ const kdPartition = (pos: ResidentPositions, blockSize: number, generation: numb const aabbDistanceSquared = (a: Float32Array, b: Float32Array): number => { let d2 = 0; for (let c = 0; c < 3; c++) { - const d = a[3 + c] < b[c] ? b[c] - a[3 + c] : (b[3 + c] < a[c] ? a[c] - b[3 + c] : 0); + const d = a[3 + c] < b[c] ? b[c] - a[3 + c] : b[3 + c] < a[c] ? a[c] - b[3 + c] : 0; d2 += d * d; } return d2; }; const pointAabbDistanceSquared = (x: number, y: number, z: number, a: Float32Array): number => { - const dx = x < a[0] ? a[0] - x : (x > a[3] ? x - a[3] : 0); - const dy = y < a[1] ? a[1] - y : (y > a[4] ? y - a[4] : 0); - const dz = z < a[2] ? a[2] - z : (z > a[5] ? z - a[5] : 0); + const dx = x < a[0] ? a[0] - x : x > a[3] ? x - a[3] : 0; + const dy = y < a[1] ? a[1] - y : y > a[4] ? y - a[4] : 0; + const dz = z < a[2] ? a[2] - z : z > a[5] ? z - a[5] : 0; return dx * dx + dy * dy + dz * dz; }; @@ -204,9 +218,13 @@ const buildBlockHalo = ( if (cap === 0 || blocks.length === 1) return { rows: new Uint32Array(0), capped: false, radius: 0 }; const neighbours = blocks - .map((block, index) => ({ block, index, d2: index === blockIndex ? Infinity : aabbDistanceSquared(core.aabb, block.aabb) })) - .filter(v => v.index !== blockIndex) - .sort((a, b) => a.d2 - b.d2 || a.index - b.index); + .map((block, index) => ({ + block, + index, + d2: index === blockIndex ? Infinity : aabbDistanceSquared(core.aabb, block.aabb) + })) + .filter((v) => v.index !== blockIndex) + .sort((a, b) => a.d2 - b.d2 || a.index - b.index); const selected: number[] = []; let capped = false; @@ -228,26 +246,39 @@ const buildBlockHalo = ( const a = new Float64Array([Infinity, Infinity, Infinity, -Infinity, -Infinity, -Infinity]); for (let i = core.start; i < core.end; i++) { const g = order[i]; - const x = pos.x[g], y = pos.y[g], z = pos.z[g]; - if (x < fence.lo[0] || x > fence.hi[0] || - y < fence.lo[1] || y > fence.hi[1] || - z < fence.lo[2] || z > fence.hi[2]) continue; + const x = pos.x[g], + y = pos.y[g], + z = pos.z[g]; + if ( + x < fence.lo[0] || + x > fence.hi[0] || + y < fence.lo[1] || + y > fence.hi[1] || + z < fence.lo[2] || + z > fence.hi[2] + ) + continue; inFence++; - a[0] = Math.min(a[0], x); a[1] = Math.min(a[1], y); a[2] = Math.min(a[2], z); - a[3] = Math.max(a[3], x); a[4] = Math.max(a[4], y); a[5] = Math.max(a[5], z); + a[0] = Math.min(a[0], x); + a[1] = Math.min(a[1], y); + a[2] = Math.min(a[2], z); + a[3] = Math.max(a[3], x); + a[4] = Math.max(a[4], y); + a[5] = Math.max(a[5], z); } if (inFence > 0) { const ex = Math.max(0, a[3] - a[0]); const ey = Math.max(0, a[4] - a[1]); const ez = Math.max(0, a[5] - a[2]); const extents = [ex, ey, ez].sort((u, v) => v - u); - const scale = extents[0] > 0 ? - (extents[2] > extents[0] * 1e-6 ? - Math.cbrt(extents[0] * extents[1] * extents[2] / inFence) : - (extents[1] > extents[0] * 1e-6 ? - Math.sqrt(extents[0] * extents[1] / inFence) : - extents[0] / inFence)) : - 0; + const scale = + extents[0] > 0 + ? extents[2] > extents[0] * 1e-6 + ? Math.cbrt((extents[0] * extents[1] * extents[2]) / inFence) + : extents[1] > extents[0] * 1e-6 + ? Math.sqrt((extents[0] * extents[1]) / inFence) + : extents[0] / inFence + : 0; radius = 2.5 * scale; } const r2 = radius * radius; diff --git a/src/lib/decimate/priority.ts b/src/lib/decimate/priority.ts index fcd9be2e..dff23144 100644 --- a/src/lib/decimate/priority.ts +++ b/src/lib/decimate/priority.ts @@ -1,14 +1,16 @@ -import { type GraphicsDevice } from 'playcanvas'; +import type { GraphicsDevice } from 'playcanvas'; -import { buildSplatCache, computeEdgeCost, CACHE_STRIDE } from './edge-cost-cpu'; -import { KNN_SENTINEL, type ForestPart } from './knn-core'; -import { type SplatView } from './moment-match'; -import { type BlockRange, type ResidentPositions } from './partition'; -import { type ChunkData, type ChunkDataPool, type ChunkSource } from '../chunk'; +import type { ChunkData, ChunkDataPool, ChunkSource } from '../chunk'; import { GpuEdgeCost } from '../gpu/gpu-edge-cost'; import { GpuKnn } from '../gpu/gpu-knn'; import { WorkerQueue } from '../workers'; +import { buildSplatCache, computeEdgeCost, CACHE_STRIDE } from './edge-cost-cpu'; +import { KNN_SENTINEL } from './knn-core'; +import type { ForestPart } from './knn-core'; +import type { SplatView } from './moment-match'; +import type { BlockRange, ResidentPositions } from './partition'; + /** * Max points per forest part. Parts build in parallel on the worker pool * (the build is each generation's serial prefix — smaller parts spread it @@ -139,7 +141,8 @@ const gatherBlockView = async ( view.color.set(colSrc, (rowBase + off) * colorDim); } else { for (let r = 0; r < count; r++) { - const src = r * colorDim, dst = (rowBase + off + r) * viewColorDim; + const src = r * colorDim, + dst = (rowBase + off + r) * viewColorDim; for (let c = 0; c < viewColorDim; c++) view.color[dst + c] = colSrc[src + c]; } } @@ -172,7 +175,8 @@ const gatherBlockView = async ( // Binary search `g` in the sorted array; -1 when absent. const indexOfSorted = (sorted: Uint32Array, g: number): number => { - let lo = 0, hi = sorted.length - 1; + let lo = 0, + hi = sorted.length - 1; while (lo <= hi) { const mid = (lo + hi) >> 1; const v = sorted[mid]; @@ -202,7 +206,7 @@ const buildForest = async ( order: Uint32Array, blocks: BlockRange[], shared: boolean -): Promise<{ parts: ForestPart[], blockPart: Uint32Array }> => { +): Promise<{ parts: ForestPart[]; blockPart: Uint32Array }> => { const jobs: Promise[] = []; const blockPart = new Uint32Array(blocks.length); let bi = 0; @@ -228,9 +232,14 @@ const buildForest = async ( y[i] = pos.y[g]; z[i] = pos.z[g]; } - jobs.push(WorkerQueue.run('buildKdForestPart', { x, y, z, ids, shared }, [ - x.buffer as ArrayBuffer, y.buffer as ArrayBuffer, z.buffer as ArrayBuffer, ids.buffer as ArrayBuffer - ])); + jobs.push( + WorkerQueue.run('buildKdForestPart', { x, y, z, ids, shared }, [ + x.buffer as ArrayBuffer, + y.buffer as ArrayBuffer, + z.buffer as ArrayBuffer, + ids.buffer as ArrayBuffer + ]) + ); } return { parts: await Promise.all(jobs), blockPart }; }; @@ -245,23 +254,22 @@ const buildForest = async ( * @param nb - Neighbour rows (`owned.length * k` global ids), sorted in place. * @param k - Neighbours per row. */ -const sortNeighborRows = ( - pos: ResidentPositions, - owned: Uint32Array, - nb: Uint32Array, - k: number -): void => { +const sortNeighborRows = (pos: ResidentPositions, owned: Uint32Array, nb: Uint32Array, k: number): void => { const d = new Float64Array(k); const id = new Uint32Array(k); for (let q = 0; q < owned.length; q++) { const g = owned[q]; - const qx = pos.x[g], qy = pos.y[g], qz = pos.z[g]; + const qx = pos.x[g], + qy = pos.y[g], + qz = pos.z[g]; const base = q * k; let m = 0; for (let s = 0; s < k; s++) { const j = nb[base + s]; if (j === KNN_SENTINEL) continue; - const dx = pos.x[j] - qx, dy = pos.y[j] - qy, dz = pos.z[j] - qz; + const dx = pos.x[j] - qx, + dy = pos.y[j] - qy, + dz = pos.z[j] - qz; const dist = dx * dx + dy * dy + dz * dz; let at = m; while (at > 0 && (d[at - 1] > dist || (d[at - 1] === dist && id[at - 1] > j))) { @@ -330,7 +338,9 @@ const runPriorityPass = async ( if (device) { const out = new Uint32Array(nOwned * k); const run = gpuKnnQueue.then(() => gpuKnn!.execute(queryPos, owned, nOwned, out, home)); - gpuKnnQueue = run.catch(() => { /* surfaced by the awaiting block */ }); + gpuKnnQueue = run.catch(() => { + /* surfaced by the awaiting block */ + }); return run.then(() => out); } // CPU path: split the block's queries across the worker pool (extra @@ -343,9 +353,12 @@ const runPriorityPass = async ( const cnt = Math.min(per, nOwned - off); const qp = queryPos.slice(off * 3, (off + cnt) * 3); const qi = owned.slice(off, off + cnt); - jobs.push(WorkerQueue.run('knnForest', { parts: ordered, queryPos: qp, queryIds: qi, k }, [ - qp.buffer as ArrayBuffer, qi.buffer as ArrayBuffer - ])); + jobs.push( + WorkerQueue.run('knnForest', { parts: ordered, queryPos: qp, queryIds: qi, k }, [ + qp.buffer as ArrayBuffer, + qi.buffer as ArrayBuffer + ]) + ); } return Promise.all(jobs).then((outs) => { const out = new Uint32Array(nOwned * k); @@ -445,9 +458,7 @@ const runPriorityPass = async ( } else { for (let s = 0; s < slots; s++) { const row = nbRow[s]; - blockCosts[s] = row === KNN_SENTINEL ? - 0 : - computeEdgeCost(cache, (s / k) | 0, row); + blockCosts[s] = row === KNN_SENTINEL ? 0 : computeEdgeCost(cache, (s / k) | 0, row); } } @@ -476,7 +487,7 @@ const runPriorityPass = async ( } const g = owned[qi]; for (let s = 0; s < K; s++) { - cand.idx[g * K + s] = s < size ? bestIdx[s] : 0xFFFFFFFF; + cand.idx[g * K + s] = s < size ? bestIdx[s] : 0xffffffff; cand.cost[g * K + s] = s < size ? bestCost[s] : Infinity; } } diff --git a/src/lib/decimate/recost-core.ts b/src/lib/decimate/recost-core.ts index f7b041fe..033b2f0c 100644 --- a/src/lib/decimate/recost-core.ts +++ b/src/lib/decimate/recost-core.ts @@ -30,8 +30,8 @@ import { EPS_COV, ellipsoidArea } from './moment-match'; export { CACHE_STRIDE }; -const NO_CANDIDATE = 0xFFFFFFFF; -const NIL = 0xFFFFFFFF; +const NO_CANDIDATE = 0xffffffff; +const NIL = 0xffffffff; /** Skip Gaussian products whose exponent bound exceeds this (e^-60 ≈ 9e-27). */ const CULL_QUAD = 120; @@ -78,8 +78,15 @@ const findRO = (parent: Uint32Array, x: number): number => { // ⟨G_a,G_b⟩ scaled by √|Σa|·√|Σb| for M = Σa+Σb (6 comps) and offset d. const crossG = ( sdAB: number, - m0: number, m1: number, m2: number, m3: number, m4: number, m5: number, - dx: number, dy: number, dz: number + m0: number, + m1: number, + m2: number, + m3: number, + m4: number, + m5: number, + dx: number, + dy: number, + dz: number ): number => { const c00 = m3 * m5 - m4 * m4; const c01 = m2 * m4 - m1 * m5; @@ -88,10 +95,10 @@ const crossG = ( const c12 = m1 * m2 - m0 * m4; const c22 = m0 * m3 - m1 * m1; const det = Math.max(m0 * c00 + m1 * c01 + m2 * c02, 1e-60); - const quad = (c00 * dx * dx + c11 * dy * dy + c22 * dz * dz + - 2 * (c01 * dx * dy + c02 * dx * dz + c12 * dy * dz)) / det; + const quad = + (c00 * dx * dx + c11 * dy * dy + c22 * dz * dz + 2 * (c01 * dx * dy + c02 * dx * dz + c12 * dy * dz)) / det; if (!(quad < CULL_QUAD)) return 0; - return TWO_PI_1_5 * sdAB / Math.sqrt(det) * Math.exp(-0.5 * quad); + return ((TWO_PI_1_5 * sdAB) / Math.sqrt(det)) * Math.exp(-0.5 * quad); }; // Smith closed-form eigenvalues of a symmetric 3×3 (6 comps), descending. @@ -108,15 +115,21 @@ const eig3 = (m0: number, m1: number, m2: number, m3: number, m4: number, m5: nu const p2 = (m0 - q) * (m0 - q) + (m3 - q) * (m3 - q) + (m5 - q) * (m5 - q) + 2 * p1; const p = Math.sqrt(p2 / 6); const ip = 1 / p; - const b00 = (m0 - q) * ip, b11 = (m3 - q) * ip, b22 = (m5 - q) * ip; - const b01 = m1 * ip, b02 = m2 * ip, b12 = m4 * ip; + const b00 = (m0 - q) * ip, + b11 = (m3 - q) * ip, + b22 = (m5 - q) * ip; + const b01 = m1 * ip, + b02 = m2 * ip, + b12 = m4 * ip; const detB = b00 * (b11 * b22 - b12 * b12) - b01 * (b01 * b22 - b12 * b02) + b02 * (b01 * b12 - b11 * b02); let r = detB / 2; - r = r < -1 ? -1 : (r > 1 ? 1 : r); + r = r < -1 ? -1 : r > 1 ? 1 : r; const phi = Math.acos(r) / 3; const e0 = q + 2 * p * Math.cos(phi); const e2 = q + 2 * p * Math.cos(phi + (2 * Math.PI) / 3); - eigOut[0] = e0; eigOut[1] = 3 * q - e0 - e2; eigOut[2] = e2; + eigOut[0] = e0; + eigOut[1] = 3 * q - e0 - e2; + eigOut[2] = e2; }; /** @@ -127,16 +140,22 @@ const eig3 = (m0: number, m1: number, m2: number, m3: number, m4: number, m5: nu type Comp = { // Raw aggregates. W: number; - mx: number; my: number; mz: number; // mass-weighted mean - M2: Float64Array; // Σ mass·(Σₖ + δδᵀ), 6 comps - bw0: number; bw1: number; bw2: number; // Σ mass·base + mx: number; + my: number; + mz: number; // mass-weighted mean + M2: Float64Array; // Σ mass·(Σₖ + δδᵀ), 6 comps + bw0: number; + bw1: number; + bw2: number; // Σ mass·base // Finished merged Gaussian (valid after finishComp). - sm: Float64Array; // M2/W + EPS_COV·I, 6 comps - sd: number; // √|Σ_C| - alpha: number; // min(1, W / area) - bc0: number; bc1: number; bc2: number; // mean base colour (bw/W) - bn2: number; // |mean base|² - selfM: number; // ⟨f_C, f_C⟩ + sm: Float64Array; // M2/W + EPS_COV·I, 6 comps + sd: number; // √|Σ_C| + alpha: number; // min(1, W / area) + bc0: number; + bc1: number; + bc2: number; // mean base colour (bw/W) + bn2: number; // |mean base|² + selfM: number; // ⟨f_C, f_C⟩ }; const makeComp = (): Comp => ({ @@ -167,22 +186,36 @@ const compAB = makeComp(); // central second moments — algebraically identical to the parallel-axis // accumulation the formation tree would produce). const composeRaw = (SC: Float32Array, members: Uint32Array, count: number, out: Comp): void => { - let W = 0, sx = 0, sy = 0, sz = 0, b0 = 0, b1 = 0, b2 = 0; + let W = 0, + sx = 0, + sy = 0, + sz = 0, + b0 = 0, + b1 = 0, + b2 = 0; for (let t = 0; t < count; t++) { const o = members[t] * CACHE_STRIDE; const m = SC[o + 11]; W += m; - sx += m * SC[o]; sy += m * SC[o + 1]; sz += m * SC[o + 2]; - b0 += m * SC[o + 12]; b1 += m * SC[o + 13]; b2 += m * SC[o + 14]; + sx += m * SC[o]; + sy += m * SC[o + 1]; + sz += m * SC[o + 2]; + b0 += m * SC[o + 12]; + b1 += m * SC[o + 13]; + b2 += m * SC[o + 14]; } const iw = 1 / W; - const mx = sx * iw, my = sy * iw, mz = sz * iw; + const mx = sx * iw, + my = sy * iw, + mz = sz * iw; const M2 = out.M2; M2.fill(0); for (let t = 0; t < count; t++) { const o = members[t] * CACHE_STRIDE; const m = SC[o + 11]; - const dx = SC[o] - mx, dy = SC[o + 1] - my, dz = SC[o + 2] - mz; + const dx = SC[o] - mx, + dy = SC[o + 1] - my, + dz = SC[o + 2] - mz; M2[0] += m * (SC[o + 3] + dx * dx); M2[1] += m * (SC[o + 4] + dx * dy); M2[2] += m * (SC[o + 5] + dx * dz); @@ -190,29 +223,47 @@ const composeRaw = (SC: Float32Array, members: Uint32Array, count: number, out: M2[4] += m * (SC[o + 7] + dy * dz); M2[5] += m * (SC[o + 8] + dz * dz); } - out.W = W; out.mx = mx; out.my = my; out.mz = mz; - out.bw0 = b0; out.bw1 = b1; out.bw2 = b2; + out.W = W; + out.mx = mx; + out.my = my; + out.mz = mz; + out.bw0 = b0; + out.bw1 = b1; + out.bw2 = b2; }; // Compose A∪B's raw aggregates from A's and B's (parallel-axis identity — // the exact arithmetic the merged-covariance step has always used). const composeUnion = (a: Comp, b: Comp, out: Comp): void => { - const WA = a.W, WB = b.W, WC = WA + WB; + const WA = a.W, + WB = b.W, + WC = WA + WB; const iw = 1 / WC; const mcx = (WA * a.mx + WB * b.mx) * iw; const mcy = (WA * a.my + WB * b.my) * iw; const mcz = (WA * a.mz + WB * b.mz) * iw; - const dax = a.mx - mcx, day = a.my - mcy, daz = a.mz - mcz; - const dbx = b.mx - mcx, dby = b.my - mcy, dbz = b.mz - mcz; - const MA = a.M2, MB = b.M2, MC = out.M2; + const dax = a.mx - mcx, + day = a.my - mcy, + daz = a.mz - mcz; + const dbx = b.mx - mcx, + dby = b.my - mcy, + dbz = b.mz - mcz; + const MA = a.M2, + MB = b.M2, + MC = out.M2; MC[0] = MA[0] + MB[0] + WA * dax * dax + WB * dbx * dbx; MC[1] = MA[1] + MB[1] + WA * dax * day + WB * dbx * dby; MC[2] = MA[2] + MB[2] + WA * dax * daz + WB * dbx * dbz; MC[3] = MA[3] + MB[3] + WA * day * day + WB * dby * dby; MC[4] = MA[4] + MB[4] + WA * day * daz + WB * dby * dbz; MC[5] = MA[5] + MB[5] + WA * daz * daz + WB * dbz * dbz; - out.W = WC; out.mx = mcx; out.my = mcy; out.mz = mcz; - out.bw0 = a.bw0 + b.bw0; out.bw1 = a.bw1 + b.bw1; out.bw2 = a.bw2 + b.bw2; + out.W = WC; + out.mx = mcx; + out.my = mcy; + out.mz = mcz; + out.bw0 = a.bw0 + b.bw0; + out.bw1 = a.bw1 + b.bw1; + out.bw2 = a.bw2 + b.bw2; }; // Derive the merged Gaussian's field quantities from the raw aggregates. @@ -227,7 +278,9 @@ const finishComp = (c: Comp): void => { sm[5] = c.M2[5] * iw + EPS_COV; const detm = Math.max( - sm[0] * (sm[3] * sm[5] - sm[4] * sm[4]) - sm[1] * (sm[1] * sm[5] - sm[4] * sm[2]) + sm[2] * (sm[1] * sm[4] - sm[3] * sm[2]), + sm[0] * (sm[3] * sm[5] - sm[4] * sm[4]) - + sm[1] * (sm[1] * sm[5] - sm[4] * sm[2]) + + sm[2] * (sm[1] * sm[4] - sm[3] * sm[2]), 1e-60 ); c.sd = Math.sqrt(detm); @@ -238,7 +291,9 @@ const finishComp = (c: Comp): void => { const s2 = Math.sqrt(Math.max(eigOut[2], 1e-18)); c.alpha = Math.min(1, c.W / Math.max(ellipsoidArea(s0, s1, s2), 1e-30)); - c.bc0 = c.bw0 * iw; c.bc1 = c.bw1 * iw; c.bc2 = c.bw2 * iw; + c.bc0 = c.bw0 * iw; + c.bc1 = c.bw1 * iw; + c.bc2 = c.bw2 * iw; c.bn2 = c.bc0 * c.bc0 + c.bc1 * c.bc1 + c.bc2 * c.bc2; c.selfM = c.alpha * c.alpha * c.bn2 * PI_1_5 * c.sd; }; @@ -249,13 +304,22 @@ const memfm = (SC: Float32Array, members: Uint32Array, count: number, c: Comp): let acc = 0; for (let t = 0; t < count; t++) { const o = members[t] * CACHE_STRIDE; - const wgt = SC[o + 10] * c.alpha * - (SC[o + 12] * c.bc0 + SC[o + 13] * c.bc1 + SC[o + 14] * c.bc2); + const wgt = SC[o + 10] * c.alpha * (SC[o + 12] * c.bc0 + SC[o + 13] * c.bc1 + SC[o + 14] * c.bc2); if (wgt === 0) continue; - acc += wgt * crossG(SC[o + 9] * c.sd, - SC[o + 3] + sm[0], SC[o + 4] + sm[1], SC[o + 5] + sm[2], - SC[o + 6] + sm[3], SC[o + 7] + sm[4], SC[o + 8] + sm[5], - SC[o] - c.mx, SC[o + 1] - c.my, SC[o + 2] - c.mz); + acc += + wgt * + crossG( + SC[o + 9] * c.sd, + SC[o + 3] + sm[0], + SC[o + 4] + sm[1], + SC[o + 5] + sm[2], + SC[o + 6] + sm[3], + SC[o + 7] + sm[4], + SC[o + 8] + sm[5], + SC[o] - c.mx, + SC[o + 1] - c.my, + SC[o + 2] - c.mz + ); } return acc; }; @@ -269,14 +333,15 @@ const selfRow = (SC: Float32Array, row: number): number => { // Member buffers (grow-only; production maxGroup is 4). let aBuf: Uint32Array = new Uint32Array(1 << 12); let bBuf: Uint32Array = new Uint32Array(1 << 12); -const gatherChain = (st: RecostState, root: number, into: Uint32Array): { buf: Uint32Array, count: number } => { +const gatherChain = (st: RecostState, root: number, into: Uint32Array): { buf: Uint32Array; count: number } => { const { mHead, mNext } = st; let buf = into; let cnt = 0; for (let m = mHead[root]; m !== NIL; m = mNext[m]) { if (cnt === buf.length) { const g = new Uint32Array(buf.length * 2); - g.set(buf); buf = g; + g.set(buf); + buf = g; } buf[cnt++] = m; } @@ -294,28 +359,46 @@ const sideTerm = (SC: Float32Array, members: Uint32Array, count: number, c: Comp // scross(A,B) = Σ_{a∈A,b∈B} ⟨f_a, f_b⟩ with distance culling. const scrossPairs = ( SC: Float32Array, - aMembers: Uint32Array, na: number, - bMembers: Uint32Array, nb: number + aMembers: Uint32Array, + na: number, + bMembers: Uint32Array, + nb: number ): number => { let acc = 0; for (let u = 0; u < nb; u++) { const ob = bMembers[u] * CACHE_STRIDE; - const bxp = SC[ob], byp = SC[ob + 1], bzp = SC[ob + 2]; + const bxp = SC[ob], + byp = SC[ob + 1], + bzp = SC[ob + 2]; const trb = SC[ob + 3] + SC[ob + 6] + SC[ob + 8]; - const alb = SC[ob + 10], sdb = SC[ob + 9]; - const cb0 = SC[ob + 12], cb1 = SC[ob + 13], cb2 = SC[ob + 14]; + const alb = SC[ob + 10], + sdb = SC[ob + 9]; + const cb0 = SC[ob + 12], + cb1 = SC[ob + 13], + cb2 = SC[ob + 14]; for (let t = 0; t < na; t++) { const oa = aMembers[t] * CACHE_STRIDE; - const dx = SC[oa] - bxp, dy = SC[oa + 1] - byp, dz = SC[oa + 2] - bzp; + const dx = SC[oa] - bxp, + dy = SC[oa + 1] - byp, + dz = SC[oa + 2] - bzp; const d2 = dx * dx + dy * dy + dz * dz; if (d2 > CULL_QUAD * (SC[oa + 3] + SC[oa + 6] + SC[oa + 8] + trb)) continue; - const wgt = SC[oa + 10] * alb * - (SC[oa + 12] * cb0 + SC[oa + 13] * cb1 + SC[oa + 14] * cb2); + const wgt = SC[oa + 10] * alb * (SC[oa + 12] * cb0 + SC[oa + 13] * cb1 + SC[oa + 14] * cb2); if (wgt === 0) continue; - acc += wgt * crossG(SC[oa + 9] * sdb, - SC[oa + 3] + SC[ob + 3], SC[oa + 4] + SC[ob + 4], SC[oa + 5] + SC[ob + 5], - SC[oa + 6] + SC[ob + 6], SC[oa + 7] + SC[ob + 7], SC[oa + 8] + SC[ob + 8], - dx, dy, dz); + acc += + wgt * + crossG( + SC[oa + 9] * sdb, + SC[oa + 3] + SC[ob + 3], + SC[oa + 4] + SC[ob + 4], + SC[oa + 5] + SC[ob + 5], + SC[oa + 6] + SC[ob + 6], + SC[oa + 7] + SC[ob + 7], + SC[oa + 8] + SC[ob + 8], + dx, + dy, + dz + ); } } return acc; @@ -338,13 +421,13 @@ const evalAgainst = (st: RecostState, na: number, aTerm: number, B: number): num const scross = scrossPairs(SC, aBuf, na, bBuf, nb); // Scale-free DC colour term between the clusters' mean base colours. - const iwA = 1 / compA.W, iwB = 1 / compB.W; + const iwA = 1 / compA.W, + iwB = 1 / compB.W; const d0 = compA.bw0 * iwA - compB.bw0 * iwB; const d1 = compA.bw1 * iwA - compB.bw1 * iwB; const d2c = compA.bw2 * iwA - compB.bw2 * iwB; - return (2 * scross - 2 * memfmAB + compAB.selfM + aTerm + bTerm) + - COLOR_WEIGHT * (d0 * d0 + d1 * d1 + d2c * d2c); + return 2 * scross - 2 * memfmAB + compAB.selfM + aTerm + bTerm + COLOR_WEIGHT * (d0 * d0 + d1 * d1 + d2c * d2c); }; /** @@ -418,7 +501,8 @@ const bestEdgesForPartition = (st: RecostState, root: number, coreCount: number) let seen = false; for (let u = 0; u < cnt; u++) { if (cbuf[u] === r) { - seen = true; break; + seen = true; + break; } } if (!seen) cbuf[cnt++] = r; @@ -429,18 +513,24 @@ const bestEdgesForPartition = (st: RecostState, root: number, coreCount: number) composeRaw(SC, aBuf, na, compA); const aTerm = sideTerm(SC, aBuf, na, compA); - let bc = Infinity, bp = -1, bv = 0; - let hc = Infinity, hp = -1; + let bc = Infinity, + bp = -1, + bv = 0; + let hc = Infinity, + hp = -1; for (let t = 0; t < cnt; t++) { const c = cbuf[t]; if (sz + size[c] > maxGroup) continue; const d = evalAgainst(st, na, aTerm, c); if (c < coreCount) { if (d < bc || (d === bc && c < bp)) { - bc = d; bp = c; bv = version[c]; + bc = d; + bp = c; + bv = version[c]; } } else if (d < hc || (d === hc && c < hp)) { - hc = d; hp = c; + hc = d; + hp = c; } } partitionBestOut.corePartner = bp; @@ -459,12 +549,4 @@ const bestEdgeFor = (st: RecostState, root: number): boolean => { return true; }; -export { - evalMergeCore, - bestEdgeFor, - bestEdgesForPartition, - bestOut, - partitionBestOut, - NO_CANDIDATE, - type RecostState -}; +export { evalMergeCore, bestEdgeFor, bestEdgesForPartition, bestOut, partitionBestOut, NO_CANDIDATE, type RecostState }; diff --git a/src/lib/decimate/select-recost.ts b/src/lib/decimate/select-recost.ts index c2c0daf3..dfdd37f7 100644 --- a/src/lib/decimate/select-recost.ts +++ b/src/lib/decimate/select-recost.ts @@ -39,13 +39,16 @@ * {@link selectMerges} when over. */ -import { type GraphicsDevice } from 'playcanvas'; +import type { GraphicsDevice } from 'playcanvas'; -import { bestEdgeFor, bestOut, CACHE_STRIDE, type RecostState } from './recost-core'; -import { MAX_GROUP, type SelectionResult } from './select'; import { GpuRecost, COMMIT_LOG_STRIDE } from '../gpu/gpu-recost'; -const NIL = 0xFFFFFFFF; +import { bestEdgeFor, bestOut, CACHE_STRIDE } from './recost-core'; +import type { RecostState } from './recost-core'; +import { MAX_GROUP } from './select'; +import type { SelectionResult } from './select'; + +const NIL = 0xffffffff; /** Inputs for {@link selectMergesRecosted}. */ type RecostInputs = { @@ -111,7 +114,9 @@ const selectMergesRecosted = async (inputs: RecostInputs): Promise { @@ -127,58 +132,91 @@ const selectMergesRecosted = async (inputs: RecostInputs): Promise { let t; - t = hCost[i]; hCost[i] = hCost[j]; hCost[j] = t; - t = hA[i]; hA[i] = hA[j]; hA[j] = t; - t = hB[i]; hB[i] = hB[j]; hB[j] = t; - t = hSeq[i]; hSeq[i] = hSeq[j]; hSeq[j] = t; - t = hVb[i]; hVb[i] = hVb[j]; hVb[j] = t; + t = hCost[i]; + hCost[i] = hCost[j]; + hCost[j] = t; + t = hA[i]; + hA[i] = hA[j]; + hA[j] = t; + t = hB[i]; + hB[i] = hB[j]; + hB[j] = t; + t = hSeq[i]; + hSeq[i] = hSeq[j]; + hSeq[j] = t; + t = hVb[i]; + hVb[i] = hVb[j]; + hVb[j] = t; }; const heapPush = (cost: number, a: number, b: number, seq: number, vb: number): void => { if (heapSize === heapCap) { const nc = heapCap * 2; const gf = (old: Float32Array) => { - const x = new Float32Array(nc); x.set(old); return x; + const x = new Float32Array(nc); + x.set(old); + return x; }; const g = (old: Uint32Array) => { - const x = new Uint32Array(nc); x.set(old); return x; + const x = new Uint32Array(nc); + x.set(old); + return x; }; hCost = gf(hCost); - hA = g(hA); hB = g(hB); hSeq = g(hSeq); hVb = g(hVb); + hA = g(hA); + hB = g(hB); + hSeq = g(hSeq); + hVb = g(hVb); heapCap = nc; } let i = heapSize++; - hCost[i] = cost; hA[i] = a; hB[i] = b; hSeq[i] = seq; hVb[i] = vb; + hCost[i] = cost; + hA[i] = a; + hB[i] = b; + hSeq[i] = seq; + hVb[i] = vb; while (i > 0) { const p = (i - 1) >> 1; if (hCost[p] <= hCost[i]) break; - swap(i, p); i = p; + swap(i, p); + i = p; } }; const popOut = { cost: 0, a: 0, b: 0, seq: 0, vb: 0 }; const heapPop = (): boolean => { if (heapSize === 0) return false; - popOut.cost = hCost[0]; popOut.a = hA[0]; popOut.b = hB[0]; popOut.seq = hSeq[0]; popOut.vb = hVb[0]; + popOut.cost = hCost[0]; + popOut.a = hA[0]; + popOut.b = hB[0]; + popOut.seq = hSeq[0]; + popOut.vb = hVb[0]; heapSize--; if (heapSize > 0) { - hCost[0] = hCost[heapSize]; hA[0] = hA[heapSize]; hB[0] = hB[heapSize]; - hSeq[0] = hSeq[heapSize]; hVb[0] = hVb[heapSize]; + hCost[0] = hCost[heapSize]; + hA[0] = hA[heapSize]; + hB[0] = hB[heapSize]; + hSeq[0] = hSeq[heapSize]; + hVb[0] = hVb[heapSize]; let i = 0; for (;;) { - const l = 2 * i + 1, r = l + 1; + const l = 2 * i + 1, + r = l + 1; let m = i; if (l < heapSize && hCost[l] < hCost[m]) m = l; if (r < heapSize && hCost[r] < hCost[m]) m = r; if (m === i) break; - swap(i, m); i = m; + swap(i, m); + i = m; } } return true; @@ -211,9 +249,10 @@ const selectMergesRecosted = async (inputs: RecostInputs): Promise { if (!(c > 0) || !useLog) return 0; const b = 1 + Math.floor(((Math.log(c) - logLo) / logSpan) * (COST_BUCKETS - 2)); - return b < 0 ? 0 : (b >= COST_BUCKETS ? COST_BUCKETS - 1 : b); + return b < 0 ? 0 : b >= COST_BUCKETS ? COST_BUCKETS - 1 : b; }; // Counting sort of finite candidate edges by bucket (cheapest first). @@ -114,11 +116,14 @@ const selectMerges = (cand: CandidateArrays, N: number, K: number, mergesNeeded: const j = cand.idx[e]; if (j === NO_CANDIDATE) continue; const i = (e / K) | 0; - let ri = find(i), rj = find(j); + let ri = find(i), + rj = find(j); if (ri === rj) continue; if (size[ri] + size[rj] > MAX_GROUP) continue; if (size[ri] < size[rj]) { - const tmp = ri; ri = rj; rj = tmp; + const tmp = ri; + ri = rj; + rj = tmp; } parent[rj] = ri; size[ri] += size[rj]; diff --git a/src/lib/global.d.ts b/src/lib/global.d.ts index 10959bc6..c5a44a94 100644 --- a/src/lib/global.d.ts +++ b/src/lib/global.d.ts @@ -1,4 +1,4 @@ declare module '*.mjs' { - const mod: any; - export default mod; + const mod: any; + export default mod; } diff --git a/src/lib/gpu/compute-kernel.ts b/src/lib/gpu/compute-kernel.ts index 7026298b..2fbc91e0 100644 --- a/src/lib/gpu/compute-kernel.ts +++ b/src/lib/gpu/compute-kernel.ts @@ -1,3 +1,4 @@ +import type { GraphicsDevice } from 'playcanvas'; import { SHADERLANGUAGE_WGSL, SHADERSTAGE_COMPUTE, @@ -6,7 +7,6 @@ import { BindStorageBufferFormat, BindUniformBufferFormat, Compute, - GraphicsDevice, Shader, UniformBufferFormat, UniformFormat @@ -48,7 +48,10 @@ const makeKernel = ( cshader: source, // @ts-ignore computeUniformBufferFormats: { - uniforms: new UniformBufferFormat(device, uniformNames.map(u => new UniformFormat(u, UNIFORMTYPE_UINT))) + uniforms: new UniformBufferFormat( + device, + uniformNames.map((u) => new UniformFormat(u, UNIFORMTYPE_UINT)) + ) }, // @ts-ignore computeBindGroupFormat: bindGroupFormat diff --git a/src/lib/gpu/gpu-dilation.ts b/src/lib/gpu/gpu-dilation.ts index 86e300e9..357264cb 100644 --- a/src/lib/gpu/gpu-dilation.ts +++ b/src/lib/gpu/gpu-dilation.ts @@ -1,3 +1,4 @@ +import type { GraphicsDevice } from 'playcanvas'; import { BUFFERUSAGE_COPY_DST, BUFFERUSAGE_COPY_SRC, @@ -8,29 +9,23 @@ import { BindStorageBufferFormat, BindUniformBufferFormat, Compute, - GraphicsDevice, Shader, StorageBuffer, UniformBufferFormat, UniformFormat } from 'playcanvas'; -import { - clearWgsl, - compactWgsl, - dilateXWgsl, - dilateYZWgsl, - extractWgsl -} from './shaders/dilation'; import type { SparseVoxelGrid } from '../voxel/sparse-voxel-grid'; +import { clearWgsl, compactWgsl, dilateXWgsl, dilateYZWgsl, extractWgsl } from './shaders/dilation'; + /** * One double-buffered slot — the four `Compute` instances (X, Y, Z, clear) * each own a uniform buffer that mustn't be overwritten by a sibling * dispatch on the same submit, plus the ping-pong storage buffers. Two * slots let the CPU prepare chunk N+1 while the GPU is busy with chunk N. */ -interface DilationSlot { +type DilationSlot = { bufferA: StorageBuffer; bufferB: StorageBuffer; capacity: number; @@ -45,7 +40,7 @@ interface DilationSlot { masksOutBuffer: StorageBuffer; typesOutCapacity: number; masksOutCapacity: number; -} +}; /** * Separable 3D dilation on the GPU using a row-aligned dense bit grid @@ -176,7 +171,7 @@ class GpuDilation { // @ts-ignore computeUniformBufferFormats: { uniforms: new UniformBufferFormat(device, [ - new UniformFormat('minBx', UNIFORMTYPE_UINT), // signed reinterpret + new UniformFormat('minBx', UNIFORMTYPE_UINT), // signed reinterpret new UniformFormat('minBy', UNIFORMTYPE_UINT), new UniformFormat('minBz', UNIFORMTYPE_UINT), new UniformFormat('outerBx', UNIFORMTYPE_UINT), @@ -218,8 +213,8 @@ class GpuDilation { this.slots = []; for (let i = 0; i < GpuDilation.NUM_SLOTS; i++) { const initialCapacity = 1024 * 1024 * 4; - const initialTypesOut = 64 * 1024; // 16K blocks worth packed - const initialMasksOut = 1024 * 1024; // 128K blocks worth (lo, hi) + const initialTypesOut = 64 * 1024; // 16K blocks worth packed + const initialMasksOut = 1024 * 1024; // 128K blocks worth (lo, hi) this.slots.push({ bufferA: new StorageBuffer(device, initialCapacity, BUFFERUSAGE_COPY_DST | BUFFERUSAGE_COPY_SRC), bufferB: new StorageBuffer(device, initialCapacity, BUFFERUSAGE_COPY_DST | BUFFERUSAGE_COPY_SRC), @@ -339,17 +334,25 @@ class GpuDilation { private ensureSlotOutputBuffers(slot: DilationSlot, innerBlocks: number): void { // typesOut: 2 bits per inner block, packed into u32 words. - const typesBytes = (((innerBlocks + 15) >>> 4) * 4); + const typesBytes = ((innerBlocks + 15) >>> 4) * 4; if (slot.typesOutCapacity < typesBytes) { slot.typesOutBuffer.destroy(); - slot.typesOutBuffer = new StorageBuffer(this.device, typesBytes, BUFFERUSAGE_COPY_DST | BUFFERUSAGE_COPY_SRC); + slot.typesOutBuffer = new StorageBuffer( + this.device, + typesBytes, + BUFFERUSAGE_COPY_DST | BUFFERUSAGE_COPY_SRC + ); slot.typesOutCapacity = typesBytes; } // masksOut: (lo, hi) per inner block. const masksBytes = innerBlocks * 8; if (slot.masksOutCapacity < masksBytes) { slot.masksOutBuffer.destroy(); - slot.masksOutBuffer = new StorageBuffer(this.device, masksBytes, BUFFERUSAGE_COPY_DST | BUFFERUSAGE_COPY_SRC); + slot.masksOutBuffer = new StorageBuffer( + this.device, + masksBytes, + BUFFERUSAGE_COPY_DST | BUFFERUSAGE_COPY_SRC + ); slot.masksOutCapacity = masksBytes; } } @@ -379,15 +382,23 @@ class GpuDilation { submitChunkSparse( slotIdx: number, // outer chunk in block coords (each is voxel/4) - minBx: number, minBy: number, minBz: number, - outerBx: number, outerBy: number, outerBz: number, + minBx: number, + minBy: number, + minBz: number, + outerBx: number, + outerBy: number, + outerBz: number, // halo in blocks - haloBx: number, haloBy: number, haloBz: number, + haloBx: number, + haloBy: number, + haloBz: number, // inner chunk in block coords - innerBx: number, innerBy: number, innerBz: number, + innerBx: number, + innerBy: number, + innerBz: number, halfExtentXZ: number, halfExtentY: number - ): { types: Promise, masks: Promise } { + ): { types: Promise; masks: Promise } { if (this.srcTypesBuffer === null) { throw new Error('GpuDilation: must call uploadSrc() before submitChunkSparse()'); } @@ -425,30 +436,54 @@ class GpuDilation { this.dispatchX(slot, slot.bufferA, slot.bufferB, numXWords, outerNy, outerNz, halfExtentXZ); // Z-pass: B → A - this.dispatchYZ(slot.dilateZCompute, slot.bufferB, slot.bufferA, - numXWords, outerNy, outerNz, halfExtentXZ, numXWords * outerNy, outerNz); + this.dispatchYZ( + slot.dilateZCompute, + slot.bufferB, + slot.bufferA, + numXWords, + outerNy, + outerNz, + halfExtentXZ, + numXWords * outerNy, + outerNz + ); // Y-pass: A → B - this.dispatchYZ(slot.dilateYCompute, slot.bufferA, slot.bufferB, - numXWords, outerNy, outerNz, halfExtentY, numXWords, outerNy); + this.dispatchYZ( + slot.dilateYCompute, + slot.bufferA, + slot.bufferB, + numXWords, + outerNy, + outerNz, + halfExtentY, + numXWords, + outerNy + ); // Compact: clear typesOut (atomicOr accumulates), dispatch compact. // masksOut is written non-atomically — no clear needed. this.dispatchClear(slot, slot.typesOutBuffer, typesOutWords); this.dispatchCompact(slot, haloBx, haloBy, haloBz, innerBx, innerBy, innerBz, numXWords, outerBy); - const typesPromise = slot.typesOutBuffer.read(0, typesOutWords * 4, null, true) - .then((readData: Uint8Array) => new Uint32Array(readData.buffer, readData.byteOffset, typesOutWords)); - const masksPromise = slot.masksOutBuffer.read(0, innerBlocks * 8, null, true) - .then((readData: Uint8Array) => new Uint32Array(readData.buffer, readData.byteOffset, innerBlocks * 2)); + const typesPromise = slot.typesOutBuffer + .read(0, typesOutWords * 4, null, true) + .then((readData: Uint8Array) => new Uint32Array(readData.buffer, readData.byteOffset, typesOutWords)); + const masksPromise = slot.masksOutBuffer + .read(0, innerBlocks * 8, null, true) + .then((readData: Uint8Array) => new Uint32Array(readData.buffer, readData.byteOffset, innerBlocks * 2)); return { types: typesPromise, masks: masksPromise }; } private dispatchExtract( slot: DilationSlot, - minBx: number, minBy: number, minBz: number, - outerBx: number, outerBy: number, outerBz: number, + minBx: number, + minBy: number, + minBz: number, + outerBx: number, + outerBy: number, + outerBz: number, numXWords: number ): void { const c = slot.extractCompute; @@ -460,9 +495,9 @@ class GpuDilation { // Reinterpret signed minB* as u32 bits via the i32 parameter slot — // PlayCanvas treats UNIFORMTYPE_UINT as raw u32, and the WGSL struct // declares these as i32 so they read back signed. - c.setParameter('minBx', (minBx >>> 0)); - c.setParameter('minBy', (minBy >>> 0)); - c.setParameter('minBz', (minBz >>> 0)); + c.setParameter('minBx', minBx >>> 0); + c.setParameter('minBy', minBy >>> 0); + c.setParameter('minBz', minBz >>> 0); c.setParameter('outerBx', outerBx); c.setParameter('outerBy', outerBy); c.setParameter('outerBz', outerBz); @@ -482,8 +517,12 @@ class GpuDilation { private dispatchCompact( slot: DilationSlot, - haloBx: number, haloBy: number, haloBz: number, - innerBx: number, innerBy: number, innerBz: number, + haloBx: number, + haloBy: number, + haloBz: number, + innerBx: number, + innerBy: number, + innerBz: number, numXWords: number, outerBy: number ): void { @@ -509,8 +548,11 @@ class GpuDilation { private dispatchX( slot: DilationSlot, - src: StorageBuffer, dst: StorageBuffer, - numXWords: number, ny: number, nz: number, + src: StorageBuffer, + dst: StorageBuffer, + numXWords: number, + ny: number, + nz: number, halfExtent: number ): void { const c = slot.dilateXCompute; @@ -530,10 +572,14 @@ class GpuDilation { private dispatchYZ( compute: Compute, - src: StorageBuffer, dst: StorageBuffer, - numXWords: number, ny: number, nz: number, + src: StorageBuffer, + dst: StorageBuffer, + numXWords: number, + ny: number, + nz: number, halfExtent: number, - stride: number, axisLen: number + stride: number, + axisLen: number ): void { compute.setParameter('src', src); compute.setParameter('dst', dst); diff --git a/src/lib/gpu/gpu-edge-cost.ts b/src/lib/gpu/gpu-edge-cost.ts index 0bc8d3e5..d4558c36 100644 --- a/src/lib/gpu/gpu-edge-cost.ts +++ b/src/lib/gpu/gpu-edge-cost.ts @@ -1,3 +1,4 @@ +import type { GraphicsDevice } from 'playcanvas'; import { BUFFERUSAGE_COPY_DST, BUFFERUSAGE_COPY_SRC, @@ -8,7 +9,6 @@ import { BindStorageBufferFormat, BindUniformBufferFormat, Compute, - GraphicsDevice, Shader, StorageBuffer, UniformBufferFormat, @@ -16,6 +16,7 @@ import { } from 'playcanvas'; import { CACHE_STRIDE } from '../decimate/edge-cost-cpu'; + import { gaussianL2Wgsl } from './shaders/chunks/gaussian-l2'; /** Per-splat interleaved stride in the `splat` storage buffer (= the CPU cost-cache layout). */ @@ -39,7 +40,7 @@ export const SPLAT_STRIDE = CACHE_STRIDE; * @param k - Compile-time K, neighbour slots per owned row. * @returns WGSL source. */ -const edgeCostWgsl = (k: number) => /* wgsl */` +const edgeCostWgsl = (k: number) => /* wgsl */ ` struct Uniforms { slotBase: u32, slotCount: u32, @@ -179,12 +180,7 @@ fn main(@builtin(global_invocation_id) gid: vec3u) { * cost +Inf. Output is `costs[s] = cost for slot s`. */ class GpuEdgeCost { - execute: ( - splatData: Float32Array, - numSplats: number, - nbRows: Uint32Array, - outCosts: Float32Array - ) => Promise; + execute: (splatData: Float32Array, numSplats: number, nbRows: Uint32Array, outCosts: Float32Array) => Promise; destroy: () => void; /** @@ -197,7 +193,7 @@ class GpuEdgeCost { // Slots per dispatch: bounded by the 65,535 workgroups-per-dimension // limit. One blocking readback per ~4.2M slots (a 2M-row block costs // 8 round trips, not 512 as with 65,536-edge batches). - const slotsPerBatch = 65535 * workgroupSize; // 4,194,240 + const slotsPerBatch = 65535 * workgroupSize; // 4,194,240 const bindGroupFormat = new BindGroupFormat(device, [ new BindUniformBufferFormat('uniforms', SHADERSTAGE_COMPUTE), @@ -229,7 +225,7 @@ class GpuEdgeCost { if (bytes > maxStorage) { throw new Error( `GpuEdgeCost: splat buffer (${maxN} splats × ${SPLAT_STRIDE} floats = ${bytes} bytes) ` + - `exceeds device maxStorageBufferBindingSize (${maxStorage})` + `exceeds device maxStorageBufferBindingSize (${maxStorage})` ); } } @@ -241,11 +237,7 @@ class GpuEdgeCost { // dispatch, keeping it off the ~2 GB per-binding limit. const nbRowBuf = new StorageBuffer(device, slotsPerBatch * 4, BUFFERUSAGE_COPY_DST); - const outBuf = new StorageBuffer( - device, - slotsPerBatch * 4, - BUFFERUSAGE_COPY_SRC | BUFFERUSAGE_COPY_DST - ); + const outBuf = new StorageBuffer(device, slotsPerBatch * 4, BUFFERUSAGE_COPY_SRC | BUFFERUSAGE_COPY_DST); const outScratch = new Float32Array(slotsPerBatch); const compute = new Compute(device, shader, 'compute-edge-cost'); diff --git a/src/lib/gpu/gpu-kmeans.ts b/src/lib/gpu/gpu-kmeans.ts index df3e8536..711f0f77 100644 --- a/src/lib/gpu/gpu-kmeans.ts +++ b/src/lib/gpu/gpu-kmeans.ts @@ -1,12 +1,8 @@ -import { - BUFFERUSAGE_COPY_DST, - BUFFERUSAGE_COPY_SRC, - FloatPacking, - StorageBuffer, - GraphicsDevice -} from 'playcanvas'; +import type { GraphicsDevice } from 'playcanvas'; +import { BUFFERUSAGE_COPY_DST, BUFFERUSAGE_COPY_SRC, FloatPacking, StorageBuffer } from 'playcanvas'; -import { makeKernel, type Kernel } from './compute-kernel'; +import { makeKernel } from './compute-kernel'; +import type { Kernel } from './compute-kernel'; /** * Flash-kmeans (arXiv 2603.09229) adapted to WebGPU: a fully GPU-resident @@ -38,7 +34,7 @@ const roundUp = (value: number, multiple: number) => { // contiguous in memory (coalesced loads). The distance loop is scalar with a // compile-time trip count — explicit vec4 widening benched slower (the kernel // is ALU-bound; the compiler already vectorizes this loop). -const assignWgsl = (floatType: string, numColumns: number, tileSize: number, ppt: number) => /* wgsl */` +const assignWgsl = (floatType: string, numColumns: number, tileSize: number, ppt: number) => /* wgsl */ ` ${floatType === 'f16' ? 'enable f16;' : ''} struct Uniforms { @@ -138,7 +134,7 @@ fn main( // per-cluster population count. Plain u32 atomics — the paper's contention // concern applies to d-wide float scatters, not a 1-word histogram. -const histogramWgsl = () => /* wgsl */` +const histogramWgsl = () => /* wgsl */ ` struct Uniforms { globalOffset: u32, count: u32 @@ -158,7 +154,7 @@ fn main(@builtin(global_invocation_id) gid: vec3u) { // single-workgroup exclusive scan of counts → segment offsets, plus a working // copy for the scatter cursors (same 3-phase pattern as shaders/prefix-sum.ts). -const scanWgsl = () => /* wgsl */` +const scanWgsl = () => /* wgsl */ ` struct Uniforms { numCentroids: u32 }; @@ -208,7 +204,7 @@ fn main(@builtin(local_invocation_index) tid: u32) { // counting-sort placement — the paper's sort-inverse mapping. Within-segment // order is nondeterministic (atomic race), which only permutes float // summation order in the reduce. -const scatterWgsl = () => /* wgsl */` +const scatterWgsl = () => /* wgsl */ ` struct Uniforms { globalOffset: u32, count: u32 @@ -233,7 +229,7 @@ fn main(@builtin(global_invocation_id) gid: vec3u) { // accumulate in shared memory and land in sums[] with plain (non-atomic) // adds — dispatch ordering serializes the per-chunk accumulation. Points // outside the bound chunk are skipped and picked up by that chunk's dispatch. -const reduceWgsl = (floatType: string, numColumns: number) => /* wgsl */` +const reduceWgsl = (floatType: string, numColumns: number) => /* wgsl */ ` ${floatType === 'f16' ? 'enable f16;' : ''} struct Uniforms { @@ -296,7 +292,7 @@ fn main( `; // centroid = sum / count for non-empty clusters (thread per element) -const divideWgsl = (numColumns: number) => /* wgsl */` +const divideWgsl = (numColumns: number) => /* wgsl */ ` struct Uniforms { numCentroids: u32 }; @@ -323,7 +319,7 @@ fn main(@builtin(global_invocation_id) gid: vec3u) { // a per-iteration seed), preserving the CPU path's semantics. The dispatch for // chunk c only handles clusters whose hashed point lands in chunk c; disjoint // from divide (count == 0 vs count > 0), so ordering between them is free. -const reseedWgsl = (floatType: string, numColumns: number) => /* wgsl */` +const reseedWgsl = (floatType: string, numColumns: number) => /* wgsl */ ` ${floatType === 'f16' ? 'enable f16;' : ''} struct Uniforms { @@ -364,7 +360,7 @@ fn main(@builtin(global_invocation_id) gid: vec3u) { `; // mirror the canonical f32 centroids into the f16 copy read by assign -const convertWgsl = () => /* wgsl */` +const convertWgsl = () => /* wgsl */ ` enable f16; struct Uniforms { @@ -427,37 +423,103 @@ class GpuKmeans { const ppt = pptForColumns(numColumns); const kernels = { - assign: makeKernel(device, 'kmeans-assign', assignWgsl(floatType, numColumns, tileSize, ppt), + assign: makeKernel( + device, + 'kmeans-assign', + assignWgsl(floatType, numColumns, tileSize, ppt), ['localOffset', 'globalOffset', 'count', 'numCentroids'], - [['points', true], ['centroids', true], ['labels', false]]), - histogram: makeKernel(device, 'kmeans-histogram', histogramWgsl(), + [ + ['points', true], + ['centroids', true], + ['labels', false] + ] + ), + histogram: makeKernel( + device, + 'kmeans-histogram', + histogramWgsl(), ['globalOffset', 'count'], - [['labels', true], ['counts', false]]), - scan: makeKernel(device, 'kmeans-scan', scanWgsl(), + [ + ['labels', true], + ['counts', false] + ] + ), + scan: makeKernel( + device, + 'kmeans-scan', + scanWgsl(), ['numCentroids'], - [['counts', true], ['offsets', false], ['cursors', false]]), - scatter: makeKernel(device, 'kmeans-scatter', scatterWgsl(), + [ + ['counts', true], + ['offsets', false], + ['cursors', false] + ] + ), + scatter: makeKernel( + device, + 'kmeans-scatter', + scatterWgsl(), ['globalOffset', 'count'], - [['labels', true], ['cursors', false], ['sortedIdx', false]]), - reduce: makeKernel(device, 'kmeans-reduce', reduceWgsl(floatType, numColumns), + [ + ['labels', true], + ['cursors', false], + ['sortedIdx', false] + ] + ), + reduce: makeKernel( + device, + 'kmeans-reduce', + reduceWgsl(floatType, numColumns), ['numCentroids', 'chunkStart', 'chunkEnd'], - [['points', true], ['sortedIdx', true], ['offsets', true], ['counts', true], ['sums', false]]), - divide: makeKernel(device, 'kmeans-divide', divideWgsl(numColumns), + [ + ['points', true], + ['sortedIdx', true], + ['offsets', true], + ['counts', true], + ['sums', false] + ] + ), + divide: makeKernel( + device, + 'kmeans-divide', + divideWgsl(numColumns), ['numCentroids'], - [['sums', true], ['counts', true], ['centroids', false]]), - reseed: makeKernel(device, 'kmeans-reseed', reseedWgsl(floatType, numColumns), + [ + ['sums', true], + ['counts', true], + ['centroids', false] + ] + ), + reseed: makeKernel( + device, + 'kmeans-reseed', + reseedWgsl(floatType, numColumns), ['numCentroids', 'numPoints', 'seed', 'chunkStart', 'chunkEnd'], - [['counts', true], ['points', true], ['centroids', false]]), - convert: useF16 ? makeKernel(device, 'kmeans-convert', convertWgsl(), - ['totalElems'], - [['centroids', true], ['centroidsF16', false]]) : null + [ + ['counts', true], + ['points', true], + ['centroids', false] + ] + ), + convert: useF16 + ? makeKernel( + device, + 'kmeans-convert', + convertWgsl(), + ['totalElems'], + [ + ['centroids', true], + ['centroidsF16', false] + ] + ) + : null }; this.run = async (points, numPoints, centroids, labels, iterations, onIteration) => { // ---- chunk plan: each point chunk must fit a storage binding and // keep assign a 1D dispatch (65535 workgroups × 64 threads) const rowBytes = numColumns * bytesPerElem; - const bindingLimit: number = wgpuLimits?.maxStorageBufferBindingSize ?? (128 * 1024 * 1024); + const bindingLimit: number = wgpuLimits?.maxStorageBufferBindingSize ?? 128 * 1024 * 1024; const chunkCap = Math.min(4_000_000, Math.floor(bindingLimit / rowBytes)) & ~1; const numChunks = Math.ceil(numPoints / chunkCap); @@ -470,8 +532,8 @@ class GpuKmeans { if (numPoints * 4 > bindingLimit) { throw new Error( `GpuKmeans: labels/sortedIdx buffers (${numPoints * 4} bytes for ${numPoints} points) exceed ` + - `device maxStorageBufferBindingSize (${bindingLimit}) — reduce the input (e.g. write streamed ` + - 'SOG / LOD units) or use a device with larger limits' + `device maxStorageBufferBindingSize (${bindingLimit}) — reduce the input (e.g. write streamed ` + + 'SOG / LOD units) or use a device with larger limits' ); } // resident when all chunks fit a 2GB budget, else stream through @@ -490,11 +552,33 @@ class GpuKmeans { } // ---- buffers - const pointBufs = resident ? - Array.from({ length: numChunks }, (_, c) => new StorageBuffer(device, roundUp(chunkRows(c) * numColumns, 2) * bytesPerElem, BUFFERUSAGE_COPY_DST)) : - Array.from({ length: 2 }, () => new StorageBuffer(device, roundUp(chunkCap * numColumns, 2) * bytesPerElem, BUFFERUSAGE_COPY_DST)); - const centroidsBuf = new StorageBuffer(device, numCentroids * numColumns * 4, BUFFERUSAGE_COPY_DST | BUFFERUSAGE_COPY_SRC); - const centroidsF16Buf = useF16 ? new StorageBuffer(device, roundUp(numCentroids * numColumns, 2) * 2, 0) : null; + const pointBufs = resident + ? Array.from( + { length: numChunks }, + (_, c) => + new StorageBuffer( + device, + roundUp(chunkRows(c) * numColumns, 2) * bytesPerElem, + BUFFERUSAGE_COPY_DST + ) + ) + : Array.from( + { length: 2 }, + () => + new StorageBuffer( + device, + roundUp(chunkCap * numColumns, 2) * bytesPerElem, + BUFFERUSAGE_COPY_DST + ) + ); + const centroidsBuf = new StorageBuffer( + device, + numCentroids * numColumns * 4, + BUFFERUSAGE_COPY_DST | BUFFERUSAGE_COPY_SRC + ); + const centroidsF16Buf = useF16 + ? new StorageBuffer(device, roundUp(numCentroids * numColumns, 2) * 2, 0) + : null; const labelsBuf = new StorageBuffer(device, numPoints * 4, BUFFERUSAGE_COPY_SRC); const sortedIdxBuf = new StorageBuffer(device, numPoints * 4, 0); const countsBuf = new StorageBuffer(device, numCentroids * 4, BUFFERUSAGE_COPY_DST); @@ -658,7 +742,7 @@ class GpuKmeans { await labelsBuf.read(0, numPoints * 4, labels, true); await centroidsBuf.read(0, numCentroids * numColumns * 4, centroids, true); } finally { - pointBufs.forEach(b => b.destroy()); + pointBufs.forEach((b) => b.destroy()); centroidsBuf.destroy(); centroidsF16Buf?.destroy(); labelsBuf.destroy(); @@ -671,7 +755,7 @@ class GpuKmeans { }; this.destroy = () => { - Object.values(kernels).forEach(k => k?.destroy()); + Object.values(kernels).forEach((k) => k?.destroy()); }; } } diff --git a/src/lib/gpu/gpu-knn.ts b/src/lib/gpu/gpu-knn.ts index ede26a0a..6fd80a96 100644 --- a/src/lib/gpu/gpu-knn.ts +++ b/src/lib/gpu/gpu-knn.ts @@ -1,12 +1,10 @@ -import { - BUFFERUSAGE_COPY_DST, - BUFFERUSAGE_COPY_SRC, - GraphicsDevice, - StorageBuffer -} from 'playcanvas'; +import type { GraphicsDevice } from 'playcanvas'; +import { BUFFERUSAGE_COPY_DST, BUFFERUSAGE_COPY_SRC, StorageBuffer } from 'playcanvas'; -import { makeKernel, type Kernel } from './compute-kernel'; -import { type ForestPart } from '../decimate/knn-core'; +import type { ForestPart } from '../decimate/knn-core'; + +import { makeKernel } from './compute-kernel'; +import type { Kernel } from './compute-kernel'; /** * WGSL kernel: iterative KD-tree K-nearest-neighbours over one forest part. @@ -32,7 +30,7 @@ import { type ForestPart } from '../decimate/knn-core'; * @param aabb - The part's point AABB [minx, miny, minz, maxx, maxy, maxz]. * @returns WGSL source. */ -const knnWgsl = (k: number, stackSize: number, rootIdx: number, aabb: Float32Array) => /* wgsl */` +const knnWgsl = (k: number, stackSize: number, rootIdx: number, aabb: Float32Array) => /* wgsl */ ` struct Uniforms { queryCount: u32, } @@ -166,7 +164,7 @@ fn main(@builtin(global_invocation_id) gid: vec3u) { `; // Reset the batch's carried top-K rows (runs before the batch's parts). -const knnResetWgsl = () => /* wgsl */` +const knnResetWgsl = () => /* wgsl */ ` struct Uniforms { slotCount: u32, } @@ -223,26 +221,32 @@ class GpuKnn { */ constructor(device: GraphicsDevice, parts: ForestPart[], k: number) { const workgroupSize = 64; - const queriesPerBatch = 1024 * workgroupSize; // 65,536 + const queriesPerBatch = 1024 * workgroupSize; // 65,536 const stackSize = 48; // Group parts under the per-binding ceiling (positions, 12 B/node, // is the largest array). - const limits = (device as unknown as { limits?: { maxStorageBufferBindingSize?: number, maxBufferSize?: number } }).limits; + const limits = ( + device as unknown as { limits?: { maxStorageBufferBindingSize?: number; maxBufferSize?: number } } + ).limits; const maxBinding = Math.min( - typeof limits?.maxStorageBufferBindingSize === 'number' ? limits.maxStorageBufferBindingSize : 128 * 2 ** 20, + typeof limits?.maxStorageBufferBindingSize === 'number' + ? limits.maxStorageBufferBindingSize + : 128 * 2 ** 20, typeof limits?.maxBufferSize === 'number' ? limits.maxBufferSize : 256 * 2 ** 20 ); const maxNodesPerGroup = Math.floor(maxBinding / 12); - type Group = { parts: { part: ForestPart, base: number }[], nodes: number }; + type Group = { parts: { part: ForestPart; base: number }[]; nodes: number }; const groups: Group[] = []; for (const part of parts) { const n = part.nodeSplatIdx.length; if (n > maxNodesPerGroup) { - throw new Error(`GpuKnn: a forest part (${n} nodes) exceeds the device binding limit — reduce the part size`); + throw new Error( + `GpuKnn: a forest part (${n} nodes) exceeds the device binding limit — reduce the part size` + ); } - if (n > 0x3FFFFFFF) { + if (n > 0x3fffffff) { throw new Error(`GpuKnn: part exceeds the 30-bit node packing limit (${n} nodes)`); } let g = groups[groups.length - 1]; @@ -266,10 +270,16 @@ class GpuKnn { const outScratch = new Uint32Array(queriesPerBatch * k); // Per-batch top-K reset (frees the part dispatch order per batch). - const resetKernel = makeKernel(device, 'compute-knn-reset', knnResetWgsl(), ['slotCount'], [ - ['topDist', false], - ['topIdx', false] - ]); + const resetKernel = makeKernel( + device, + 'compute-knn-reset', + knnResetWgsl(), + ['slotCount'], + [ + ['topDist', false], + ['topIdx', false] + ] + ); resetKernel.compute.setParameter('topDist', topDistBuf); resetKernel.compute.setParameter('topIdx', topIdxBuf); @@ -294,20 +304,26 @@ class GpuKnn { if (remapped.length < n * 2) remapped = new Uint32Array(n * 2); for (let i = 0; i < n * 2; i++) { const c = part.nodeChildren[i]; - remapped[i] = c === 0xFFFFFFFF ? c : c + base; + remapped[i] = c === 0xffffffff ? c : c + base; } childrenBuf.write(base * 2 * 4, remapped, 0, n * 2); const source = knnWgsl(k, stackSize, base + part.rootIdx, part.aabb); - const kernel = makeKernel(device, 'compute-knn-forest', source, ['queryCount'], [ - ['queryPos', true], - ['queryIds', true], - ['nodeSplatIdx', true], - ['nodePositions', true], - ['nodeChildren', true], - ['topDist', false], - ['topIdx', false] - ]); + const kernel = makeKernel( + device, + 'compute-knn-forest', + source, + ['queryCount'], + [ + ['queryPos', true], + ['queryIds', true], + ['nodeSplatIdx', true], + ['nodePositions', true], + ['nodeChildren', true], + ['topDist', false], + ['topIdx', false] + ] + ); kernel.compute.setParameter('queryPos', queryPosBuf); kernel.compute.setParameter('queryIds', queryIdBuf); kernel.compute.setParameter('nodeSplatIdx', splatIdxBuf); @@ -327,7 +343,9 @@ class GpuKnn { homePart: number ) => { if (outNeighbours.length !== queryCount * k) { - throw new Error(`GpuKnn: outNeighbours length ${outNeighbours.length} must be queryCount*k = ${queryCount * k}`); + throw new Error( + `GpuKnn: outNeighbours length ${outNeighbours.length} must be queryCount*k = ${queryCount * k}` + ); } // Home part first: it fills the top-K with true near neighbours, @@ -349,7 +367,7 @@ class GpuKnn { kernel.compute.setParameter('queryCount', batchCount); kernel.compute.setupDispatch(dispatchGroups); } - device.computeDispatch([resetKernel.compute, ...order.map(kn => kn.compute)], `knn-batch-${batch}`); + device.computeDispatch([resetKernel.compute, ...order.map((kn) => kn.compute)], `knn-batch-${batch}`); const readBytes = batchCount * k * 4; await topIdxBuf.read(0, readBytes, outScratch, true); diff --git a/src/lib/gpu/gpu-recost.ts b/src/lib/gpu/gpu-recost.ts index 51ec4b79..dfb7d7e9 100644 --- a/src/lib/gpu/gpu-recost.ts +++ b/src/lib/gpu/gpu-recost.ts @@ -1,11 +1,8 @@ -import { - BUFFERUSAGE_COPY_DST, - BUFFERUSAGE_COPY_SRC, - GraphicsDevice, - StorageBuffer -} from 'playcanvas'; - -import { makeKernel, type Kernel } from './compute-kernel'; +import type { GraphicsDevice } from 'playcanvas'; +import { BUFFERUSAGE_COPY_DST, BUFFERUSAGE_COPY_SRC, StorageBuffer } from 'playcanvas'; + +import { makeKernel } from './compute-kernel'; +import type { Kernel } from './compute-kernel'; import { gaussianL2Wgsl } from './shaders/chunks/gaussian-l2'; /** @@ -33,7 +30,7 @@ import { gaussianL2Wgsl } from './shaders/chunks/gaussian-l2'; */ const WG = 64; -const NONE = 0xFFFFFFFF; +const NONE = 0xffffffff; const MAX_DIM = 65535; // Shared structure access for the replay/refresh kernels: parentMeta[i] = @@ -44,7 +41,7 @@ const refreshWgsl = ( splitN: number, coreCount: number, partitioned: boolean -) => /* wgsl */` +) => /* wgsl */ ` struct Uniforms { pendingCount: u32, } @@ -286,9 +283,11 @@ fn main(@builtin(workgroup_id) wgid: vec3u, @builtin(local_invocation_id) lid3: let root = pending[pIdx]; if (parentMeta[root].x != root) { // Stale queued root (absorbed since queuing) — no result. - outBest[pIdx] = ${partitioned ? - 'vec4u(NONE, bitcast(F32_MAX), NONE, bitcast(F32_MAX))' : - 'vec2u(NONE, bitcast(F32_MAX))'}; + outBest[pIdx] = ${ + partitioned + ? 'vec4u(NONE, bitcast(F32_MAX), NONE, bitcast(F32_MAX))' + : 'vec2u(NONE, bitcast(F32_MAX))' + }; wgAbort = 1u; } else { wgAbort = 0u; @@ -397,9 +396,11 @@ fn main(@builtin(workgroup_id) wgid: vec3u, @builtin(local_invocation_id) lid3: if (redCost[0] == F32_MAX) { p = NONE; } var hp = redHaloPartner[0]; if (redHaloCost[0] == F32_MAX) { hp = NONE; } - outBest[pIdx] = ${partitioned ? - 'vec4u(p, bitcast(redCost[0]), hp, bitcast(redHaloCost[0]))' : - 'vec2u(p, bitcast(redCost[0]))'}; + outBest[pIdx] = ${ + partitioned + ? 'vec4u(p, bitcast(redCost[0]), hp, bitcast(redHaloCost[0]))' + : 'vec2u(p, bitcast(redCost[0]))' + }; } } `; @@ -409,7 +410,7 @@ fn main(@builtin(workgroup_id) wgid: vec3u, @builtin(local_invocation_id) lid3: // guarantees each root commits at most once per wave (entries touching a // committed root fail their version/seq/parent checks), so writes across // entries target disjoint words. -const replayWgsl = () => /* wgsl */` +const replayWgsl = () => /* wgsl */ ` struct Uniforms { commitCount: u32, } @@ -436,7 +437,7 @@ fn main(@builtin(global_invocation_id) gid: vec3u) { `; // Initialize the structure buffers: every splat a singleton root. -const initWgsl = () => /* wgsl */` +const initWgsl = () => /* wgsl */ ` struct Uniforms { count: u32, } @@ -501,18 +502,22 @@ class GpuRecost { coreCount = n, allowSmall = false ): boolean { - if (n < 1024 && !allowSmall) return false; // inline is instant below this on the global path + if (n < 1024 && !allowSmall) return false; // inline is instant below this on the global path const limits = (device as any).limits; const maxBinding = Math.min( - typeof limits?.maxStorageBufferBindingSize === 'number' ? limits.maxStorageBufferBindingSize : 128 * 2 ** 20, + typeof limits?.maxStorageBufferBindingSize === 'number' + ? limits.maxStorageBufferBindingSize + : 128 * 2 ** 20, typeof limits?.maxBufferSize === 'number' ? limits.maxBufferSize : 256 * 2 ** 20 ); const splitN = Math.ceil(n / 2); - return splitN * 16 * 4 <= maxBinding && // cacheA/B - splitN * k * 4 <= maxBinding && // nbA/B + return ( + splitN * 16 * 4 <= maxBinding && // cacheA/B + splitN * k * 4 <= maxBinding && // nbA/B n * (coreCount < n ? 16 : 8) <= maxBinding && // outBest - n * 8 <= maxBinding && // parentMeta/chain - wave * COMMIT_LOG_STRIDE * 4 <= maxBinding; + n * 8 <= maxBinding && // parentMeta/chain + wave * COMMIT_LOG_STRIDE * 4 <= maxBinding + ); } /** @@ -539,21 +544,37 @@ class GpuRecost { const parentMetaBuf = new StorageBuffer(device, n * 8, BUFFERUSAGE_COPY_DST); const chainBuf = new StorageBuffer(device, n * 8, BUFFERUSAGE_COPY_DST); const pendingBuf = new StorageBuffer(device, n * 4, BUFFERUSAGE_COPY_DST); - const outBestBuf = new StorageBuffer(device, n * outputStrideBytes, BUFFERUSAGE_COPY_SRC | BUFFERUSAGE_COPY_DST); + const outBestBuf = new StorageBuffer( + device, + n * outputStrideBytes, + BUFFERUSAGE_COPY_SRC | BUFFERUSAGE_COPY_DST + ); const commitLogBuf = new StorageBuffer(device, wave * COMMIT_LOG_STRIDE * 4, BUFFERUSAGE_COPY_DST); - const initKernel = makeKernel(device, 'recost-init', initWgsl(), ['count'], [ - ['parentMeta', false], - ['chain', false] - ]); + const initKernel = makeKernel( + device, + 'recost-init', + initWgsl(), + ['count'], + [ + ['parentMeta', false], + ['chain', false] + ] + ); initKernel.compute.setParameter('parentMeta', parentMetaBuf); initKernel.compute.setParameter('chain', chainBuf); - const replayKernel = makeKernel(device, 'recost-replay', replayWgsl(), ['commitCount'], [ - ['commitLog', true], - ['parentMeta', false], - ['chain', false] - ]); + const replayKernel = makeKernel( + device, + 'recost-replay', + replayWgsl(), + ['commitCount'], + [ + ['commitLog', true], + ['parentMeta', false], + ['chain', false] + ] + ); replayKernel.compute.setParameter('commitLog', commitLogBuf); replayKernel.compute.setParameter('parentMeta', parentMetaBuf); replayKernel.compute.setParameter('chain', chainBuf); @@ -620,10 +641,7 @@ class GpuRecost { } pendingBuf.write(0, pending, 0, pendingCount); refreshKernel.compute.setParameter('pendingCount', pendingCount); - refreshKernel.compute.setupDispatch( - Math.min(pendingCount, MAX_DIM), - Math.ceil(pendingCount / MAX_DIM) - ); + refreshKernel.compute.setupDispatch(Math.min(pendingCount, MAX_DIM), Math.ceil(pendingCount / MAX_DIM)); computes.push(refreshKernel.compute); device.computeDispatch(computes, 'recost-wave'); diff --git a/src/lib/gpu/gpu-splat-rasterizer.ts b/src/lib/gpu/gpu-splat-rasterizer.ts index 726fbfa6..64d9d17a 100644 --- a/src/lib/gpu/gpu-splat-rasterizer.ts +++ b/src/lib/gpu/gpu-splat-rasterizer.ts @@ -1,3 +1,4 @@ +import type { GraphicsDevice } from 'playcanvas'; import { BUFFERUSAGE_COPY_DST, BUFFERUSAGE_COPY_SRC, @@ -9,13 +10,15 @@ import { BindUniformBufferFormat, Compute, ComputeRadixSort, - GraphicsDevice, Shader, StorageBuffer, UniformBufferFormat, UniformFormat } from 'playcanvas'; +import type { Projection } from '../render/camera'; +import { TILE_SIZE } from '../render/config'; + import { constantsChunk } from './shaders/chunks/constants'; import { covariance3D } from './shaders/chunks/covariance-3d'; import { jacobianEquirect } from './shaders/chunks/jacobian-equirect'; @@ -39,8 +42,6 @@ import { projectWgsl } from './shaders/project'; import { rasterizeBinnedWgsl } from './shaders/rasterize-binned'; import { tileBinEmitPairsWgsl } from './shaders/tile-bin-emit-pairs'; import { uniformsStruct, uniformFormatEntries } from './shaders/uniforms'; -import { type Projection } from '../render/camera'; -import { TILE_SIZE } from '../render/config'; /** 12 floats per projected splat: vec4 × 3. */ const PROJECTION_STRIDE_F32 = 12; @@ -64,7 +65,7 @@ const OUTPUT_STRIDE_U32 = 1; * shader's group-AABB cull and group-pixel-origin uniforms still * exercise this code path. */ -interface SplatRasterizerOptions { +type SplatRasterizerOptions = { /** Number of SH bands above DC (0–3). Determines input stride. */ numSHBands: 0 | 1 | 2 | 3; /** @@ -104,13 +105,22 @@ interface SplatRasterizerOptions { /** Near plane distance in world units. */ near: number; /** Camera basis: rows are (right, down, forward) of the world→camera rotation. */ - rightX: number; rightY: number; rightZ: number; - downX: number; downY: number; downZ: number; - forwardX: number; forwardY: number; forwardZ: number; + rightX: number; + rightY: number; + rightZ: number; + downX: number; + downY: number; + downZ: number; + forwardX: number; + forwardY: number; + forwardZ: number; /** Camera eye position in world space. */ - eyeX: number; eyeY: number; eyeZ: number; + eyeX: number; + eyeY: number; + eyeZ: number; /** Focal lengths in pixel units. */ - focalX: number; focalY: number; + focalX: number; + focalY: number; /** * Camera-space Z of the focus plane, world units. Pinhole-only; * unused when `projection === 'equirect'`. @@ -124,15 +134,17 @@ interface SplatRasterizerOptions { */ apertureScale: number; /** RGBA background, each channel in [0, 1]. */ - bgR: number; bgG: number; bgB: number; bgA: number; -} + bgR: number; + bgG: number; + bgB: number; + bgA: number; +}; const numSHCoeffsPerChannel = (bands: number): number => { return bands === 0 ? 0 : bands === 1 ? 3 : bands === 2 ? 8 : 15; }; - -interface PipelineBuffers { +type PipelineBuffers = { inputBuffer: StorageBuffer; projBuffer: StorageBuffer; runningStateBuffer: StorageBuffer; @@ -176,8 +188,7 @@ interface PipelineBuffers { findBoundariesCompute: Compute; rasterizeBinnedCompute: Compute; finalizeCompute: Compute; -} - +}; /** * GPU-accelerated splat rasterizer. @@ -232,8 +243,8 @@ class GpuSplatRasterizer { private sortKeyBits: number; private clearStatePattern: Float32Array; /** Active group's tile dimensions, set by `beginGroup`. */ - private activeTilesX: number = 0; - private activeTilesY: number = 0; + private activeTilesX = 0; + private activeTilesY = 0; /** Floats per gaussian in the input buffer (depends on SH band count). */ readonly inputStride: number; @@ -364,21 +375,22 @@ class GpuSplatRasterizer { uniformEntries: UniformFormat[] = uniformFormatEntries(), cincludes: Map = sharedCincludes, cdefines: Map = sharedCdefines - ) => new Shader(device, { - name, - shaderLanguage: SHADERLANGUAGE_WGSL, - cshader: source, - // @ts-ignore - computeUniformBufferFormats / computeBindGroupFormat / cincludes / cdefines are not in public Shader types. - computeUniformBufferFormats: { - uniforms: new UniformBufferFormat(device, uniformEntries) - }, - // @ts-ignore - computeBindGroupFormat: bgFormat, - // @ts-ignore - cincludes, - // @ts-ignore - cdefines - }); + ) => + new Shader(device, { + name, + shaderLanguage: SHADERLANGUAGE_WGSL, + cshader: source, + // @ts-ignore - computeUniformBufferFormats / computeBindGroupFormat / cincludes / cdefines are not in public Shader types. + computeUniformBufferFormats: { + uniforms: new UniformBufferFormat(device, uniformEntries) + }, + // @ts-ignore + computeBindGroupFormat: bgFormat, + // @ts-ignore + cincludes, + // @ts-ignore + cdefines + }); // The prefix-sum kernel processes the chunk in 256-thread blocks // of `scanPerThread` elements; the constant is compile-time @@ -395,12 +407,33 @@ class GpuSplatRasterizer { ]; this.projectShader = mkShader('splat-project', projectWgsl(coeffs), this.projectBgFormat); - this.prefixSumShader = mkShader('splat-tilebin-prefix-sum', prefixSumWgsl(scanPerThread), this.prefixSumBgFormat); + this.prefixSumShader = mkShader( + 'splat-tilebin-prefix-sum', + prefixSumWgsl(scanPerThread), + this.prefixSumBgFormat + ); this.emitPairsShader = mkShader('splat-tilebin-emit-pairs', tileBinEmitPairsWgsl(), this.emitPairsBgFormat); - this.prepareIndirectShader = mkShader('splat-tilebin-prepare-indirect', prepareIndirectWgsl(), this.prepareIndirectBgFormat, prepareIndirectUniforms); - this.initTileOffsetsShader = mkShader('splat-tilebin-init-tile-offsets', initTileOffsetsWgsl(), this.initTileOffsetsBgFormat); - this.findBoundariesShader = mkShader('splat-tilebin-find-boundaries', findBoundariesWgsl(), this.findBoundariesBgFormat); - this.rasterizeBinnedShader = mkShader('splat-rasterize-binned', rasterizeBinnedWgsl(), this.rasterizeBinnedBgFormat); + this.prepareIndirectShader = mkShader( + 'splat-tilebin-prepare-indirect', + prepareIndirectWgsl(), + this.prepareIndirectBgFormat, + prepareIndirectUniforms + ); + this.initTileOffsetsShader = mkShader( + 'splat-tilebin-init-tile-offsets', + initTileOffsetsWgsl(), + this.initTileOffsetsBgFormat + ); + this.findBoundariesShader = mkShader( + 'splat-tilebin-find-boundaries', + findBoundariesWgsl(), + this.findBoundariesBgFormat + ); + this.rasterizeBinnedShader = mkShader( + 'splat-rasterize-binned', + rasterizeBinnedWgsl(), + this.rasterizeBinnedBgFormat + ); this.finalizeShader = mkShader('splat-finalize-pack', finalizeWgsl(), this.finalizeBgFormat); // Buffer sizing. runningState/output cover exactly the group's @@ -452,12 +485,20 @@ class GpuSplatRasterizer { emitPairsCompute.setParameter('tileKeys', tileKeysBuffer); emitPairsCompute.setParameter('splatValues', splatValuesBuffer); - const prepareIndirectCompute = new Compute(device, this.prepareIndirectShader, 'splat-tilebin-prepare-indirect'); + const prepareIndirectCompute = new Compute( + device, + this.prepareIndirectShader, + 'splat-tilebin-prepare-indirect' + ); prepareIndirectCompute.setParameter('totalPairs', totalPairsBuffer); // `indirectBuffer` is bound per-chunk after the device has // allocated its indirect-dispatch buffer. - const initTileOffsetsCompute = new Compute(device, this.initTileOffsetsShader, 'splat-tilebin-init-tile-offsets'); + const initTileOffsetsCompute = new Compute( + device, + this.initTileOffsetsShader, + 'splat-tilebin-init-tile-offsets' + ); initTileOffsetsCompute.setParameter('totalPairs', totalPairsBuffer); initTileOffsetsCompute.setParameter('tileOffsets', tileOffsetsBuffer); @@ -544,12 +585,7 @@ class GpuSplatRasterizer { * @param groupTilesX - Number of tiles in this group along X. * @param groupTilesY - Number of tiles in this group along Y. */ - private setUniforms( - groupX: number, - groupY: number, - groupTilesX: number, - groupTilesY: number - ): void { + private setUniforms(groupX: number, groupY: number, groupTilesX: number, groupTilesY: number): void { const o = this.options; const originX = groupX * this.groupPixelW; const originY = groupY * this.groupPixelH; @@ -566,20 +602,32 @@ class GpuSplatRasterizer { b.rasterizeBinnedCompute, b.finalizeCompute ]) { - c.setParameter('rightX', o.rightX); c.setParameter('rightY', o.rightY); c.setParameter('rightZ', o.rightZ); + c.setParameter('rightX', o.rightX); + c.setParameter('rightY', o.rightY); + c.setParameter('rightZ', o.rightZ); c.setParameter('_p0', 0); - c.setParameter('downX', o.downX); c.setParameter('downY', o.downY); c.setParameter('downZ', o.downZ); + c.setParameter('downX', o.downX); + c.setParameter('downY', o.downY); + c.setParameter('downZ', o.downZ); c.setParameter('_p1', 0); - c.setParameter('forwardX', o.forwardX); c.setParameter('forwardY', o.forwardY); c.setParameter('forwardZ', o.forwardZ); + c.setParameter('forwardX', o.forwardX); + c.setParameter('forwardY', o.forwardY); + c.setParameter('forwardZ', o.forwardZ); c.setParameter('_p2', 0); - c.setParameter('eyeX', o.eyeX); c.setParameter('eyeY', o.eyeY); c.setParameter('eyeZ', o.eyeZ); + c.setParameter('eyeX', o.eyeX); + c.setParameter('eyeY', o.eyeY); + c.setParameter('eyeZ', o.eyeZ); c.setParameter('_p3', 0); - c.setParameter('focalX', o.focalX); c.setParameter('focalY', o.focalY); - c.setParameter('near', o.near); c.setParameter('_p4', 0); + c.setParameter('focalX', o.focalX); + c.setParameter('focalY', o.focalY); + c.setParameter('near', o.near); + c.setParameter('_p4', 0); c.setParameter('focusDistance', o.focusDistance); c.setParameter('apertureScale', o.apertureScale); - c.setParameter('_p5', 0); c.setParameter('_p6', 0); - c.setParameter('imageWidth', o.imageWidth); c.setParameter('imageHeight', o.imageHeight); + c.setParameter('_p5', 0); + c.setParameter('_p6', 0); + c.setParameter('imageWidth', o.imageWidth); + c.setParameter('imageHeight', o.imageHeight); c.setParameter('splatStride', this.inputStride); // chunkSize set per-dispatch c.setParameter('groupPixelMinX', originX); @@ -590,8 +638,10 @@ class GpuSplatRasterizer { c.setParameter('groupTilesY', groupTilesY); c.setParameter('groupPixelOriginX', originX); c.setParameter('groupPixelOriginY', originY); - c.setParameter('bgR', o.bgR); c.setParameter('bgG', o.bgG); - c.setParameter('bgB', o.bgB); c.setParameter('bgA', o.bgA); + c.setParameter('bgR', o.bgR); + c.setParameter('bgG', o.bgG); + c.setParameter('bgB', o.bgB); + c.setParameter('bgA', o.bgA); } } @@ -603,19 +653,12 @@ class GpuSplatRasterizer { * @param groupTilesX - Number of tiles in this group along X. * @param groupTilesY - Number of tiles in this group along Y. */ - beginGroup( - groupX: number, - groupY: number, - groupTilesX: number, - groupTilesY: number - ): void { + beginGroup(groupX: number, groupY: number, groupTilesX: number, groupTilesY: number): void { this.setUniforms(groupX, groupY, groupTilesX, groupTilesY); this.activeTilesX = groupTilesX; this.activeTilesY = groupTilesY; const groupPixels = groupTilesX * groupTilesY * TILE_SIZE * TILE_SIZE; - this.buffers.runningStateBuffer.write( - 0, this.clearStatePattern, 0, groupPixels * RUNNING_STATE_STRIDE_F32 - ); + this.buffers.runningStateBuffer.write(0, this.clearStatePattern, 0, groupPixels * RUNNING_STATE_STRIDE_F32); } /** @@ -649,7 +692,9 @@ class GpuSplatRasterizer { // @ts-ignore - getIndirectDispatchSlot exists on WebgpuGraphicsDevice. const get = (this.device as { getIndirectDispatchSlot?: (count?: number) => number }).getIndirectDispatchSlot; if (!get) { - throw new Error('GpuSplatRasterizer requires a GraphicsDevice with getIndirectDispatchSlot() (WebGPU backend).'); + throw new Error( + 'GpuSplatRasterizer requires a GraphicsDevice with getIndirectDispatchSlot() (WebGPU backend).' + ); } const sortSlot = get.call(this.device, 1); const boundariesSlot = get.call(this.device, 1); @@ -727,8 +772,12 @@ class GpuSplatRasterizer { // ceil(log2(numTiles))) — only the minimum required passes run. const pairsCap = this.chunkCap * this.options.maxCoveragePerSplat; this.radixSort.sortIndirect( - b.tileKeysBuffer, pairsCap, this.sortKeyBits, sortSlot, - b.totalPairsBuffer, b.splatValuesBuffer + b.tileKeysBuffer, + pairsCap, + this.sortKeyBits, + sortSlot, + b.totalPairsBuffer, + b.splatValuesBuffer ); const sortedTileKeysBuf = this.radixSort.sortedKeys; const sortedSplatIndicesBuf = this.radixSort.sortedIndices; diff --git a/src/lib/gpu/gpu-voxelization.ts b/src/lib/gpu/gpu-voxelization.ts index 6b06e0e9..a1f24157 100644 --- a/src/lib/gpu/gpu-voxelization.ts +++ b/src/lib/gpu/gpu-voxelization.ts @@ -1,3 +1,4 @@ +import type { GraphicsDevice } from 'playcanvas'; import { BUFFERUSAGE_COPY_DST, BUFFERUSAGE_COPY_SRC, @@ -9,14 +10,13 @@ import { BindStorageBufferFormat, BindUniformBufferFormat, Compute, - GraphicsDevice, Shader, StorageBuffer, UniformBufferFormat, UniformFormat } from 'playcanvas'; -import { DataTable } from '../data-table'; +import type { DataTable } from '../data-table'; /** * WGSL shader for multi-batch voxelization of 4x4x4 blocks. @@ -213,7 +213,7 @@ fn main( /** * Specification for a single batch in a multi-batch dispatch. */ -interface BatchSpec { +type BatchSpec = { /** Offset into the concatenated index array */ indexOffset: number; @@ -231,12 +231,12 @@ interface BatchSpec { /** Number of blocks in Z direction */ numBlocksZ: number; -} +}; /** * Result of a multi-batch voxelization dispatch. */ -interface MultiBatchResult { +type MultiBatchResult = { /** * Raw u32 masks for all batches. * For batch i, block j: masks[(i * maxBlocksPerBatch + j) * 2] = low, [+1] = high @@ -245,14 +245,14 @@ interface MultiBatchResult { /** Maximum blocks per batch, used for offset calculation */ maxBlocksPerBatch: number; -} +}; /** * A set of GPU buffers and compute instance for one dispatch slot. * Two slots allow double-buffered pipelining: while the GPU executes * a dispatch on slot A, the CPU can prepare data for slot B. */ -interface DispatchSlot { +type DispatchSlot = { indexBuffer: StorageBuffer; resultsBuffer: StorageBuffer; batchInfoBuffer: StorageBuffer; @@ -260,7 +260,7 @@ interface DispatchSlot { indexBufferSize: number; resultsBufferSize: number; batchInfoBufferSize: number; -} +}; /** * GPU-accelerated voxelization of Gaussian splat data. @@ -282,7 +282,7 @@ class GpuVoxelization { // Double-buffered dispatch slots private slots: DispatchSlot[]; - private totalGaussians: number = 0; + private totalGaussians = 0; /** Floats per Gaussian in the interleaved buffer (16 for alignment) */ private static readonly FLOATS_PER_GAUSSIAN = 16; @@ -337,7 +337,7 @@ class GpuVoxelization { // Create double-buffered dispatch slots this.slots = []; for (let i = 0; i < GpuVoxelization.NUM_SLOTS; i++) { - const indexBufferSize = 1024 * 1024 * 4; // 1M indices = 4 MB + const indexBufferSize = 1024 * 1024 * 4; // 1M indices = 4 MB const indexBuffer = new StorageBuffer(device, indexBufferSize, BUFFERUSAGE_COPY_DST); // Initial capacity: 64 batches × 4096 blocks × 2 u32 × 4 bytes = 2 MB @@ -363,7 +363,6 @@ class GpuVoxelization { batchInfoBufferSize }); } - } /** @@ -419,7 +418,10 @@ class GpuVoxelization { // Normalize quaternion — the Rodrigues rotation in the shader // assumes unit quaternions; non-normalized ones (common in MCMC // training) would produce incorrect Mahalanobis distances. - const qw = rotW[i], qx = rotX[i], qy = rotY[i], qz = rotZ[i]; + const qw = rotW[i], + qx = rotX[i], + qy = rotY[i], + qz = rotZ[i]; const qlen = Math.sqrt(qw * qw + qx * qx + qy * qy + qz * qz); const invLen = qlen > 0 ? 1 / qlen : 0; interleavedData[offset + 4] = qw * invLen; @@ -511,20 +513,32 @@ class GpuVoxelization { // Ensure slot buffers are large enough this.ensureSlotBuffer( - slot, 'indexBuffer', 'indexBufferSize', - totalIndices * 4, BUFFERUSAGE_COPY_DST, 'indices' + slot, + 'indexBuffer', + 'indexBufferSize', + totalIndices * 4, + BUFFERUSAGE_COPY_DST, + 'indices' ); const resultsU32Count = numBatches * maxBlocks * 2; this.ensureSlotBuffer( - slot, 'resultsBuffer', 'resultsBufferSize', - resultsU32Count * 4, BUFFERUSAGE_COPY_SRC, 'results' + slot, + 'resultsBuffer', + 'resultsBufferSize', + resultsU32Count * 4, + BUFFERUSAGE_COPY_SRC, + 'results' ); const batchInfoU32Count = numBatches * GpuVoxelization.BATCH_INFO_U32S; this.ensureSlotBuffer( - slot, 'batchInfoBuffer', 'batchInfoBufferSize', - batchInfoU32Count * 4, BUFFERUSAGE_COPY_DST, 'batchInfos' + slot, + 'batchInfoBuffer', + 'batchInfoBufferSize', + batchInfoU32Count * 4, + BUFFERUSAGE_COPY_DST, + 'batchInfos' ); // Upload concatenated indices diff --git a/src/lib/gpu/shaders/chunks/constants.ts b/src/lib/gpu/shaders/chunks/constants.ts index 966a27d0..0b353fc2 100644 --- a/src/lib/gpu/shaders/chunks/constants.ts +++ b/src/lib/gpu/shaders/chunks/constants.ts @@ -40,7 +40,7 @@ const wgslF32 = (n: number): string => { * Included via `#include "constants"` (see `sharedCincludes` in * `gpu-splat-rasterizer.ts`). */ -const constantsChunk = /* wgsl */` +const constantsChunk = /* wgsl */ ` const TILE_SIZE: u32 = ${TILE_SIZE}u; const SIGMA_CUTOFF: f32 = ${wgslF32(SIGMA_CUTOFF)}; const GAUSSIAN_FLOOR: f32 = ${wgslF32(GAUSSIAN_FLOOR)}; diff --git a/src/lib/gpu/shaders/chunks/covariance-3d.ts b/src/lib/gpu/shaders/chunks/covariance-3d.ts index 75a76961..302c79b4 100644 --- a/src/lib/gpu/shaders/chunks/covariance-3d.ts +++ b/src/lib/gpu/shaders/chunks/covariance-3d.ts @@ -11,7 +11,7 @@ * The output covariance feeds the Jacobian chunks (pinhole / equirect) * to derive the 2D screen-space covariance via cov2D = J · cov3D · Jᵀ. */ -const covariance3D = /* wgsl */` +const covariance3D = /* wgsl */ ` let sx = exp(lsX); let sy = exp(lsY); let sz = exp(lsZ); diff --git a/src/lib/gpu/shaders/chunks/gaussian-l2.ts b/src/lib/gpu/shaders/chunks/gaussian-l2.ts index 73a8dac2..417d58ed 100644 --- a/src/lib/gpu/shaders/chunks/gaussian-l2.ts +++ b/src/lib/gpu/shaders/chunks/gaussian-l2.ts @@ -7,7 +7,7 @@ * Mirrors the CPU implementations in decimate/edge-cost-cpu.ts and * decimate/recost-core.ts — keep them in lockstep. */ -const gaussianL2Wgsl = /* wgsl */` +const gaussianL2Wgsl = /* wgsl */ ` const EPS_COV: f32 = 1e-8; const PI_1_5: f32 = 5.5683279968317084; // π^{3/2} const TWO_PI_1_5: f32 = 15.749609945722419; // (2π)^{3/2} diff --git a/src/lib/gpu/shaders/chunks/jacobian-equirect.ts b/src/lib/gpu/shaders/chunks/jacobian-equirect.ts index ac961ebd..f89e7d4a 100644 --- a/src/lib/gpu/shaders/chunks/jacobian-equirect.ts +++ b/src/lib/gpu/shaders/chunks/jacobian-equirect.ts @@ -18,7 +18,7 @@ * j[1][0] != 0, so cov = J·Σ·Jᵀ carries the extra u00·jy0 / u10·jy0 / * u11·jy0 terms that the pinhole simplification dropped. */ -const jacobianEquirect = /* wgsl */` +const jacobianEquirect = /* wgsl */ ` let kx = imgWf * invTwoPi; let ky = imgHf * invPi; let invRxzC2 = 1.0 / (rxzClamped * rxzClamped); diff --git a/src/lib/gpu/shaders/chunks/jacobian-pinhole.ts b/src/lib/gpu/shaders/chunks/jacobian-pinhole.ts index 65a218ad..9b97f29c 100644 --- a/src/lib/gpu/shaders/chunks/jacobian-pinhole.ts +++ b/src/lib/gpu/shaders/chunks/jacobian-pinhole.ts @@ -15,7 +15,7 @@ * entries (j[0][1] = 0, j[1][0] = 0) let us drop the u01·jy0 / u10 / * u11·jy0 terms in cov = J·Σ·Jᵀ that the equirect path retains. */ -const jacobianPinhole = /* wgsl */` +const jacobianPinhole = /* wgsl */ ` let limX = JACOBIAN_LIMIT_FACTOR * (f32(uniforms.imageWidth) * 0.5) / uniforms.focalX; let limY = JACOBIAN_LIMIT_FACTOR * (f32(uniforms.imageHeight) * 0.5) / uniforms.focalY; let txtz = clamp(cx * invZ, -limX, limX); diff --git a/src/lib/gpu/shaders/chunks/projection-equirect.ts b/src/lib/gpu/shaders/chunks/projection-equirect.ts index 628f69ea..e79e4983 100644 --- a/src/lib/gpu/shaders/chunks/projection-equirect.ts +++ b/src/lib/gpu/shaders/chunks/projection-equirect.ts @@ -17,7 +17,7 @@ * lat > 0 → screenY in the bottom half. screenY = 0 maps to the zenith * (above the camera). */ -const projectionEquirect = /* wgsl */` +const projectionEquirect = /* wgsl */ ` let r2 = cx * cx + cy * cy + cz * cz; if (r2 <= uniforms.near * uniforms.near) { writeInvalid(i); return; } let r = sqrt(r2); diff --git a/src/lib/gpu/shaders/chunks/projection-pinhole.ts b/src/lib/gpu/shaders/chunks/projection-pinhole.ts index 1178eb71..f6d5dd67 100644 --- a/src/lib/gpu/shaders/chunks/projection-pinhole.ts +++ b/src/lib/gpu/shaders/chunks/projection-pinhole.ts @@ -7,7 +7,7 @@ * * Splats with cz <= near are written invalid and the shader returns. */ -const projectionPinhole = /* wgsl */` +const projectionPinhole = /* wgsl */ ` if (cz <= uniforms.near) { writeInvalid(i); return; } let invZ = 1.0 / cz; diff --git a/src/lib/gpu/shaders/chunks/quat-rotation.ts b/src/lib/gpu/shaders/chunks/quat-rotation.ts index 1bacd0f6..452791aa 100644 --- a/src/lib/gpu/shaders/chunks/quat-rotation.ts +++ b/src/lib/gpu/shaders/chunks/quat-rotation.ts @@ -10,7 +10,7 @@ * Normalises the quaternion first so the rotation matrix is orthonormal * even if the upstream data stored an unnormalised quat. */ -const quatRotation = /* wgsl */` +const quatRotation = /* wgsl */ ` let qlen2 = rotW * rotW + rotX * rotX + rotY * rotY + rotZ * rotZ; if (qlen2 == 0.0) { writeInvalid(i); return; } let invQ = inverseSqrt(qlen2); diff --git a/src/lib/gpu/shaders/chunks/sh-band-1.ts b/src/lib/gpu/shaders/chunks/sh-band-1.ts index 5def15af..90bf8d6c 100644 --- a/src/lib/gpu/shaders/chunks/sh-band-1.ts +++ b/src/lib/gpu/shaders/chunks/sh-band-1.ts @@ -9,7 +9,7 @@ * radiance. Channel-major SH layout: `f_rest_0..N-1` red, then green, * then blue. */ -const shBand1 = /* wgsl */` +const shBand1 = /* wgsl */ ` { let n = COEFFS_PER_CHANNEL; let shBase = base + 14u; diff --git a/src/lib/gpu/shaders/chunks/sh-band-2.ts b/src/lib/gpu/shaders/chunks/sh-band-2.ts index b5bf9540..80f4b0b4 100644 --- a/src/lib/gpu/shaders/chunks/sh-band-2.ts +++ b/src/lib/gpu/shaders/chunks/sh-band-2.ts @@ -6,7 +6,7 @@ * SH_C2_0, SH_C2_1, SH_C2_2, SH_C2_3, SH_C2_4 * Defines: (mutates) cR, cG, cB */ -const shBand2 = /* wgsl */` +const shBand2 = /* wgsl */ ` { let n = COEFFS_PER_CHANNEL; let shBase = base + 14u; diff --git a/src/lib/gpu/shaders/chunks/sh-band-3.ts b/src/lib/gpu/shaders/chunks/sh-band-3.ts index 3f66008d..f2e54a67 100644 --- a/src/lib/gpu/shaders/chunks/sh-band-3.ts +++ b/src/lib/gpu/shaders/chunks/sh-band-3.ts @@ -6,7 +6,7 @@ * SH_C3_0, SH_C3_1, SH_C3_2, SH_C3_3, SH_C3_4, SH_C3_5, SH_C3_6 * Defines: (mutates) cR, cG, cB */ -const shBand3 = /* wgsl */` +const shBand3 = /* wgsl */ ` { let n = COEFFS_PER_CHANNEL; let shBase = base + 14u; diff --git a/src/lib/gpu/shaders/chunks/tile-aabb-equirect.ts b/src/lib/gpu/shaders/chunks/tile-aabb-equirect.ts index 434fc504..cd8dd541 100644 --- a/src/lib/gpu/shaders/chunks/tile-aabb-equirect.ts +++ b/src/lib/gpu/shaders/chunks/tile-aabb-equirect.ts @@ -16,7 +16,7 @@ * @param maxCoveragePerSplat - Hard upper bound on per-splat tile count. * @returns WGSL source for the equirect tile-coverage block. */ -const tileAabbEquirect = (maxCoveragePerSplat: number) => /* wgsl */` +const tileAabbEquirect = (maxCoveragePerSplat: number) => /* wgsl */ ` let minTXraw = i32(floor((screenX - radius - gox) / tsz)); let maxTXraw = i32(floor((screenX + radius - gox) / tsz)); let txCountRaw = maxTXraw - minTXraw + 1; diff --git a/src/lib/gpu/shaders/chunks/tile-aabb-pinhole.ts b/src/lib/gpu/shaders/chunks/tile-aabb-pinhole.ts index 24fa4c94..81ecbc75 100644 --- a/src/lib/gpu/shaders/chunks/tile-aabb-pinhole.ts +++ b/src/lib/gpu/shaders/chunks/tile-aabb-pinhole.ts @@ -18,7 +18,7 @@ * @param maxCoveragePerSplat - Hard upper bound on per-splat tile count. * @returns WGSL source for the pinhole tile-coverage block. */ -const tileAabbPinhole = (maxCoveragePerSplat: number) => /* wgsl */` +const tileAabbPinhole = (maxCoveragePerSplat: number) => /* wgsl */ ` let minTX = max(0, i32(floor((screenX - radius - gox) / tsz))); let maxTX = min(i32(uniforms.groupTilesX) - 1, i32(floor((screenX + radius - gox) / tsz))); let minTY = max(0, i32(floor((screenY - radius - goy) / tsz))); diff --git a/src/lib/gpu/shaders/chunks/tile-walk-equirect.ts b/src/lib/gpu/shaders/chunks/tile-walk-equirect.ts index 93ec8012..3763bd5d 100644 --- a/src/lib/gpu/shaders/chunks/tile-walk-equirect.ts +++ b/src/lib/gpu/shaders/chunks/tile-walk-equirect.ts @@ -13,7 +13,7 @@ * wrapping its per-pixel dx into [-W/2, W/2], so a wrapped tile pulls * the splat's footprint from the correct copy across the seam. */ -const tileWalkEquirect = /* wgsl */` +const tileWalkEquirect = /* wgsl */ ` let minTXraw = i32(floor((sX - radius - gox) / tsz)); let maxTXraw = i32(floor((sX + radius - gox) / tsz)); let txCountRaw = maxTXraw - minTXraw + 1; diff --git a/src/lib/gpu/shaders/chunks/tile-walk-pinhole.ts b/src/lib/gpu/shaders/chunks/tile-walk-pinhole.ts index c7059f44..defb57c8 100644 --- a/src/lib/gpu/shaders/chunks/tile-walk-pinhole.ts +++ b/src/lib/gpu/shaders/chunks/tile-walk-pinhole.ts @@ -11,7 +11,7 @@ * `coverage[i]` cap is hit. Mirrors the bbox computed by the pinhole * project shader's `tile-aabb-pinhole` chunk. */ -const tileWalkPinhole = /* wgsl */` +const tileWalkPinhole = /* wgsl */ ` let minTX = max(0, i32(floor((sX - radius - gox) / tsz))); let maxTX = min(i32(uniforms.groupTilesX) - 1, i32(floor((sX + radius - gox) / tsz))); let minTY = max(0, i32(floor((sY - radius - goy) / tsz))); diff --git a/src/lib/gpu/shaders/dilation.ts b/src/lib/gpu/shaders/dilation.ts index 8b2ebfe9..24e2120d 100644 --- a/src/lib/gpu/shaders/dilation.ts +++ b/src/lib/gpu/shaders/dilation.ts @@ -17,7 +17,7 @@ * `BlockMaskMap.slot` formula so the GPU's hash probe lands in the same * slot. */ -const dilationConstants = /* wgsl */` +const dilationConstants = /* wgsl */ ` const BLOCK_EMPTY: u32 = 0u; const BLOCK_SOLID: u32 = 1u; const BLOCK_MIXED: u32 = 2u; @@ -38,7 +38,7 @@ const FIBONACCI_HASH: u32 = 0x9E3779B9u; * * @returns WGSL source for the extract compute shader. */ -const extractWgsl = () => /* wgsl */` +const extractWgsl = () => /* wgsl */ ` ${dilationConstants} struct ExtractUniforms { @@ -145,7 +145,7 @@ fn main(@builtin(global_invocation_id) gid: vec3u) { * * @returns WGSL source for the compact compute shader. */ -const compactWgsl = () => /* wgsl */` +const compactWgsl = () => /* wgsl */ ` ${dilationConstants} struct CompactUniforms { @@ -227,7 +227,7 @@ fn main(@builtin(global_invocation_id) gid: vec3u) { * * @returns WGSL source for the clear compute shader. */ -const clearWgsl = () => /* wgsl */` +const clearWgsl = () => /* wgsl */ ` struct ClearUniforms { clearNumWords: u32, clearRowStride: u32 @@ -263,7 +263,7 @@ fn main(@builtin(global_invocation_id) gid: vec3u) { * * @returns WGSL source for the X-axis dilation compute shader. */ -const dilateXWgsl = () => /* wgsl */` +const dilateXWgsl = () => /* wgsl */ ` struct DilateXUniforms { numXWords: u32, ny: u32, @@ -340,7 +340,7 @@ fn main(@builtin(global_invocation_id) gid: vec3u) { * * @returns WGSL source for the Y/Z-axis dilation compute shader. */ -const dilateYZWgsl = () => /* wgsl */` +const dilateYZWgsl = () => /* wgsl */ ` struct DilateYZUniforms { numXWords: u32, ny: u32, diff --git a/src/lib/gpu/shaders/finalize.ts b/src/lib/gpu/shaders/finalize.ts index f37226a1..ab55766f 100644 --- a/src/lib/gpu/shaders/finalize.ts +++ b/src/lib/gpu/shaders/finalize.ts @@ -6,7 +6,7 @@ * * @returns WGSL source for the finalize compute shader. */ -const finalizeWgsl = () => /* wgsl */` +const finalizeWgsl = () => /* wgsl */ ` #include "uniformsStruct" #include "constants" diff --git a/src/lib/gpu/shaders/find-boundaries.ts b/src/lib/gpu/shaders/find-boundaries.ts index 6f8c4dae..ea402c42 100644 --- a/src/lib/gpu/shaders/find-boundaries.ts +++ b/src/lib/gpu/shaders/find-boundaries.ts @@ -11,7 +11,7 @@ * * @returns WGSL source for the find-boundaries compute shader. */ -const findBoundariesWgsl = () => /* wgsl */` +const findBoundariesWgsl = () => /* wgsl */ ` #include "uniformsStruct" #include "constants" diff --git a/src/lib/gpu/shaders/init-tile-offsets.ts b/src/lib/gpu/shaders/init-tile-offsets.ts index 723d37ec..73439a83 100644 --- a/src/lib/gpu/shaders/init-tile-offsets.ts +++ b/src/lib/gpu/shaders/init-tile-offsets.ts @@ -7,7 +7,7 @@ * * @returns WGSL source for the init-tile-offsets compute shader. */ -const initTileOffsetsWgsl = () => /* wgsl */` +const initTileOffsetsWgsl = () => /* wgsl */ ` #include "uniformsStruct" #include "constants" diff --git a/src/lib/gpu/shaders/prefix-sum.ts b/src/lib/gpu/shaders/prefix-sum.ts index 2346f851..afd7db52 100644 --- a/src/lib/gpu/shaders/prefix-sum.ts +++ b/src/lib/gpu/shaders/prefix-sum.ts @@ -13,7 +13,7 @@ * @param scanPerThread - Per-thread element budget; must satisfy `256 * scanPerThread >= chunkCap`. * @returns WGSL source for the prefix-sum compute shader. */ -const prefixSumWgsl = (scanPerThread: number) => /* wgsl */` +const prefixSumWgsl = (scanPerThread: number) => /* wgsl */ ` #include "uniformsStruct" #include "constants" diff --git a/src/lib/gpu/shaders/prepare-indirect.ts b/src/lib/gpu/shaders/prepare-indirect.ts index 360e0bee..7240cd5a 100644 --- a/src/lib/gpu/shaders/prepare-indirect.ts +++ b/src/lib/gpu/shaders/prepare-indirect.ts @@ -15,7 +15,7 @@ * * @returns WGSL source for the prepare-indirect compute shader. */ -const prepareIndirectWgsl = () => /* wgsl */` +const prepareIndirectWgsl = () => /* wgsl */ ` struct PrepareUniforms { sortSlotBase: u32, boundariesSlotBase: u32, diff --git a/src/lib/gpu/shaders/project.ts b/src/lib/gpu/shaders/project.ts index fc4748e0..e0f2a30a 100644 --- a/src/lib/gpu/shaders/project.ts +++ b/src/lib/gpu/shaders/project.ts @@ -16,7 +16,7 @@ * @param coeffsPerChannel - Per-channel SH coefficient count (0/3/8/15). * @returns WGSL source for the project compute shader. */ -const projectWgsl = (coeffsPerChannel: number) => /* wgsl */` +const projectWgsl = (coeffsPerChannel: number) => /* wgsl */ ` #include "uniformsStruct" #include "constants" diff --git a/src/lib/gpu/shaders/rasterize-binned.ts b/src/lib/gpu/shaders/rasterize-binned.ts index 83e84f72..221503b4 100644 --- a/src/lib/gpu/shaders/rasterize-binned.ts +++ b/src/lib/gpu/shaders/rasterize-binned.ts @@ -19,7 +19,7 @@ * * @returns WGSL source for the binned-rasterize compute shader. */ -const rasterizeBinnedWgsl = () => /* wgsl */` +const rasterizeBinnedWgsl = () => /* wgsl */ ` #include "uniformsStruct" #include "constants" diff --git a/src/lib/gpu/shaders/tile-bin-emit-pairs.ts b/src/lib/gpu/shaders/tile-bin-emit-pairs.ts index 9fbf6d56..f77c6eb9 100644 --- a/src/lib/gpu/shaders/tile-bin-emit-pairs.ts +++ b/src/lib/gpu/shaders/tile-bin-emit-pairs.ts @@ -22,7 +22,7 @@ * * @returns WGSL source for the emit-pairs compute shader. */ -const tileBinEmitPairsWgsl = () => /* wgsl */` +const tileBinEmitPairsWgsl = () => /* wgsl */ ` #include "uniformsStruct" #include "constants" diff --git a/src/lib/gpu/shaders/uniforms.ts b/src/lib/gpu/shaders/uniforms.ts index 94694dd0..227e9bf9 100644 --- a/src/lib/gpu/shaders/uniforms.ts +++ b/src/lib/gpu/shaders/uniforms.ts @@ -1,8 +1,4 @@ -import { - UNIFORMTYPE_FLOAT, - UNIFORMTYPE_UINT, - UniformFormat -} from 'playcanvas'; +import { UNIFORMTYPE_FLOAT, UNIFORMTYPE_UINT, UniformFormat } from 'playcanvas'; /** * Shared WGSL declaration of the `struct Uniforms` block consumed by @@ -19,7 +15,7 @@ import { * `playcanvas/src/platform/graphics/shader.js` for the preprocessor * wiring (the `cincludes` Map on the `Shader` constructor's definition). */ -const uniformsStruct = /* wgsl */` +const uniformsStruct = /* wgsl */ ` struct Uniforms { rightX: f32, rightY: f32, rightZ: f32, _p0: f32, downX: f32, downY: f32, downZ: f32, _p1: f32, diff --git a/src/lib/index.ts b/src/lib/index.ts index d51a1669..21aeb3ae 100644 --- a/src/lib/index.ts +++ b/src/lib/index.ts @@ -6,9 +6,18 @@ // Core contract + pool export { createChunkDataPool } from './chunk'; export type { - ChunkSource, ChunkSourceMetadata, ReadRequest, ReadTarget, - ChunkData, ChunkDataPool, - ChunkLayer, SHBands, ChunkField, ChunkFieldMap, LayerLayout, ExtraColumn + ChunkSource, + ChunkSourceMetadata, + ReadRequest, + ReadTarget, + ChunkData, + ChunkDataPool, + ChunkLayer, + SHBands, + ChunkField, + ChunkFieldMap, + LayerLayout, + ExtraColumn } from './chunk'; // Structural combinators (lazy views over sources) @@ -79,7 +88,17 @@ export { processDataTable } from './process'; export { readKsplat, readMjs, readPly, readSog, readSplat, readSpz } from './readers'; // Individual writers (advanced use; DataTable-input compat set) -export { writeSog, writeSpz, writePly, writeCompressedPly, writeCsv, writeHtml, writeImage, writeGlb, writeVoxel } from './writers'; +export { + writeSog, + writeSpz, + writePly, + writeCompressedPly, + writeCsv, + writeHtml, + writeImage, + writeGlb, + writeVoxel +} from './writers'; export type { WriteImageOptions, WriteVoxelOptions, VoxelMetadata } from './writers'; // --------------------------------------------------------------------------- @@ -87,10 +106,7 @@ export type { WriteImageOptions, WriteVoxelOptions, VoxelMetadata } from './writ // --------------------------------------------------------------------------- // Utils -export { - fmtBytes, fmtCount, fmtTime, - logger, TextRenderer, Transform, WebPCodec -} from './utils'; +export { fmtBytes, fmtCount, fmtTime, logger, TextRenderer, Transform, WebPCodec } from './utils'; export type { Bar, Group, LogEvent, Logger, MessageKind, Renderer, TextRendererOptions, Verbosity } from './utils'; // Worker pool for CPU-heavy tasks diff --git a/src/lib/io/read/buffered-read-stream.ts b/src/lib/io/read/buffered-read-stream.ts index 4a28d545..38433318 100644 --- a/src/lib/io/read/buffered-read-stream.ts +++ b/src/lib/io/read/buffered-read-stream.ts @@ -23,7 +23,7 @@ class BufferedReadStream extends ReadStream { * @param inner - The underlying stream to read from * @param chunkSize - Minimum bytes to read at once from inner stream (default 64KB) */ - constructor(inner: ReadStream, chunkSize: number = 65536) { + constructor(inner: ReadStream, chunkSize = 65536) { super(inner.expectedSize); this.inner = inner; this.chunkSize = chunkSize; diff --git a/src/lib/io/read/file-system.ts b/src/lib/io/read/file-system.ts index d56d5513..1d90924e 100644 --- a/src/lib/io/read/file-system.ts +++ b/src/lib/io/read/file-system.ts @@ -12,7 +12,7 @@ abstract class ReadStream { /** * Total bytes read from this stream so far. */ - bytesRead: number = 0; + bytesRead = 0; /** * @param expectedSize - Optional size hint for buffer pre-allocation @@ -67,6 +67,7 @@ abstract class ReadStream { * Interface representing a readable data source. * Provides size information and creates streams for reading. */ +// eslint-disable-next-line @typescript-eslint/consistent-type-definitions -- preserve public declaration merging interface ReadSource { /** * The size of the source in bytes, or undefined if unknown. @@ -106,6 +107,7 @@ type ProgressCallback = (bytesLoaded: number, totalBytes: number | undefined) => * Interface for a file system that can create readable sources. * Implementations exist for various backends (URL, Node FS, Zip, Memory). */ +// eslint-disable-next-line @typescript-eslint/consistent-type-definitions -- preserve public declaration merging interface ReadFileSystem { /** * Create a readable source for the given path/identifier. diff --git a/src/lib/io/read/memory-file-system.ts b/src/lib/io/read/memory-file-system.ts index 06044aca..a8acb089 100644 --- a/src/lib/io/read/memory-file-system.ts +++ b/src/lib/io/read/memory-file-system.ts @@ -1,4 +1,5 @@ -import { type ReadFileSystem, type ProgressCallback, type ReadSource, ReadStream } from './file-system'; +import { ReadStream } from './file-system'; +import type { ReadFileSystem, ProgressCallback, ReadSource } from './file-system'; /** * ReadStream implementation for reading from memory buffers. @@ -38,14 +39,14 @@ class MemoryReadSource implements ReadSource { readonly seekable: boolean = true; private data: Uint8Array; - private closed: boolean = false; + private closed = false; constructor(data: Uint8Array | ArrayBuffer) { this.data = data instanceof ArrayBuffer ? new Uint8Array(data) : data; this.size = this.data.length; } - read(start: number = 0, end: number = this.size): ReadStream { + read(start = 0, end: number = this.size): ReadStream { if (this.closed) { throw new Error('Source has been closed'); } @@ -67,7 +68,7 @@ class MemoryReadSource implements ReadSource { * Useful for testing or when data is already in memory. */ class MemoryReadFileSystem implements ReadFileSystem { - private buffers: Map = new Map(); + private buffers = new Map(); /** * Store a named buffer. diff --git a/src/lib/io/read/url-file-system.ts b/src/lib/io/read/url-file-system.ts index 67cdae32..4de47540 100644 --- a/src/lib/io/read/url-file-system.ts +++ b/src/lib/io/read/url-file-system.ts @@ -1,4 +1,5 @@ -import { type ReadFileSystem, type ProgressCallback, type ReadSource, ReadStream } from './file-system'; +import { ReadStream } from './file-system'; +import type { ReadFileSystem, ProgressCallback, ReadSource } from './file-system'; import { MemoryReadSource } from './memory-file-system'; /** @@ -14,7 +15,7 @@ const probeRangeSupport = async (url: string): Promise<{ rangeSupported: boolean try { const response = await fetch(url, { method: 'GET', - headers: { 'Range': 'bytes=0-0' }, + headers: { Range: 'bytes=0-0' }, signal: controller.signal }); @@ -48,8 +49,8 @@ class UrlReadStream extends ReadStream { private reader: ReadableStreamDefaultReader | null = null; private response: Response | null = null; private buffer: Uint8Array | null = null; - private bufferOffset: number = 0; - private closed: boolean = false; + private bufferOffset = 0; + private closed = false; private progress: ProgressCallback | undefined; private totalSize: number | undefined; @@ -145,7 +146,7 @@ class LazyUrlReadStream extends ReadStream { private headers: Record; private innerStream: UrlReadStream | null = null; private fetchPromise: Promise | null = null; - private closed: boolean = false; + private closed = false; private progress: ProgressCallback | undefined; constructor(url: string, headers: Record, expectedSize?: number, progress?: ProgressCallback) { @@ -208,7 +209,7 @@ class UrlReadSource implements ReadSource { readonly seekable: boolean = true; private url: string; - private closed: boolean = false; + private closed = false; private progress: ProgressCallback | undefined; constructor(url: string, size: number | undefined, progress?: ProgressCallback) { @@ -267,7 +268,7 @@ class UrlReadFileSystem implements ReadFileSystem { /** * @param baseUrl - Optional base URL to prepend to filenames */ - constructor(baseUrl: string = '') { + constructor(baseUrl = '') { this.baseUrl = baseUrl; } diff --git a/src/lib/io/read/zip-file-system.ts b/src/lib/io/read/zip-file-system.ts index cf8cc353..294382d8 100644 --- a/src/lib/io/read/zip-file-system.ts +++ b/src/lib/io/read/zip-file-system.ts @@ -1,4 +1,5 @@ -import { type ReadFileSystem, type ProgressCallback, type ReadSource, ReadStream } from './file-system'; +import { ReadStream } from './file-system'; +import type { ReadFileSystem, ProgressCallback, ReadSource } from './file-system'; /** * Metadata for a zip file entry. @@ -7,8 +8,8 @@ type ZipEntry = { name: string; compressedSize: number; uncompressedSize: number; - offset: number; // Local header offset - method: number; // 0=store, 8=deflate + offset: number; // Local header offset + method: number; // 0=store, 8=deflate }; /** @@ -41,9 +42,9 @@ class DeflateEntryReadStream extends ReadStream { private sourceStream: ReadStream; private reader: ReadableStreamDefaultReader | null = null; private buffer: Uint8Array | null = null; - private bufferOffset: number = 0; - private closed: boolean = false; - private initialized: boolean = false; + private bufferOffset = 0; + private closed = false; + private initialized = false; constructor(sourceStream: ReadStream, expectedSize: number) { super(expectedSize); @@ -152,7 +153,7 @@ class ZipEntrySource implements ReadSource { private source: ReadSource; private entry: ZipEntry; private dataOffset: number; - private closed: boolean = false; + private closed = false; constructor(source: ReadSource, entry: ZipEntry, dataOffset: number) { this.source = source; @@ -172,10 +173,7 @@ class ZipEntrySource implements ReadSource { // Stored (uncompressed) - direct range read const rangeStart = start ?? 0; const rangeEnd = end ?? this.entry.uncompressedSize; - const sourceStream = this.source.read( - this.dataOffset + rangeStart, - this.dataOffset + rangeEnd - ); + const sourceStream = this.source.read(this.dataOffset + rangeStart, this.dataOffset + rangeEnd); return new StoredEntryReadStream(sourceStream, rangeEnd - rangeStart); } @@ -185,10 +183,7 @@ class ZipEntrySource implements ReadSource { throw new Error('Range reads not supported on compressed zip entries'); } - const sourceStream = this.source.read( - this.dataOffset, - this.dataOffset + this.entry.compressedSize - ); + const sourceStream = this.source.read(this.dataOffset, this.dataOffset + this.entry.compressedSize); return new DeflateEntryReadStream(sourceStream, this.entry.uncompressedSize); } @@ -208,7 +203,7 @@ class ZipReadFileSystem implements ReadFileSystem { private source: ReadSource; private entries: Map | null = null; private parsePromise: Promise> | null = null; - private closed: boolean = false; + private closed = false; constructor(source: ReadSource) { this.source = source; @@ -238,10 +233,12 @@ class ZipReadFileSystem implements ReadFileSystem { // Find EOCD signature (0x06054b50) let eocdOffset = -1; for (let i = eocdData.length - 22; i >= 0; i--) { - if (eocdData[i] === 0x50 && + if ( + eocdData[i] === 0x50 && eocdData[i + 1] === 0x4b && eocdData[i + 2] === 0x05 && - eocdData[i + 3] === 0x06) { + eocdData[i + 3] === 0x06 + ) { eocdOffset = i; break; } diff --git a/src/lib/io/write/crc.ts b/src/lib/io/write/crc.ts index 3b9aebcc..22a001c2 100644 --- a/src/lib/io/write/crc.ts +++ b/src/lib/io/write/crc.ts @@ -4,7 +4,7 @@ const crc32_table = (() => { for (let n = 0; n < 256; n++) { c = n; for (let k = 0; k < 8; k++) { - c = ((c & 1) ? (0xEDB88320 ^ (c >>> 1)) : (c >>> 1)); + c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1; } tbl[n] = c; } @@ -20,10 +20,10 @@ class Crc { let bits = -1; this.update = (data: Uint8Array) => { for (let i = 0; i < data.length; i++) { - bits = (bits >>> 8) ^ crc32_table[(bits ^ data[i]) & 0xFF]; + bits = (bits >>> 8) ^ crc32_table[(bits ^ data[i]) & 0xff]; } }; - this.value = () => (bits ^ (-1)) >>> 0; + this.value = () => (bits ^ -1) >>> 0; } } diff --git a/src/lib/io/write/file-system.ts b/src/lib/io/write/file-system.ts index 576ae9fb..d27469f5 100644 --- a/src/lib/io/write/file-system.ts +++ b/src/lib/io/write/file-system.ts @@ -1,4 +1,5 @@ // defines the interface for a stream writer class +// eslint-disable-next-line @typescript-eslint/consistent-type-definitions -- preserve public declaration merging interface Writer { // total bytes successfully passed to write() so far readonly bytesWritten: number; @@ -17,6 +18,7 @@ interface Writer { abort(): void | Promise; } +// eslint-disable-next-line @typescript-eslint/consistent-type-definitions -- preserve public declaration merging interface FileSystem { // create a writer for the given filename createWriter(filename: string): Writer | Promise; diff --git a/src/lib/io/write/memory-file-system.ts b/src/lib/io/write/memory-file-system.ts index 7052b271..c3ac5d74 100644 --- a/src/lib/io/write/memory-file-system.ts +++ b/src/lib/io/write/memory-file-system.ts @@ -1,4 +1,4 @@ -import { type FileSystem, type Writer } from './file-system'; +import type { FileSystem, Writer } from './file-system'; // write data to a memory buffer class MemoryWriter implements Writer { @@ -76,7 +76,7 @@ class MemoryWriter implements Writer { * ``` */ class MemoryFileSystem implements FileSystem { - results: Map = new Map(); + results = new Map(); createWriter(filename: string): Writer { return new MemoryWriter((result: Uint8Array[]) => { diff --git a/src/lib/io/write/write-helpers.ts b/src/lib/io/write/write-helpers.ts index 8a0e3a4c..fbe7d679 100644 --- a/src/lib/io/write/write-helpers.ts +++ b/src/lib/io/write/write-helpers.ts @@ -1,4 +1,4 @@ -import { type FileSystem } from './file-system'; +import type { FileSystem } from './file-system'; const writeFile = async (fs: FileSystem, filename: string, data: Uint8Array | string) => { const outputFile = await fs.createWriter(filename); diff --git a/src/lib/io/write/zip-file-system.ts b/src/lib/io/write/zip-file-system.ts index f1d72fc2..45dd76e8 100644 --- a/src/lib/io/write/zip-file-system.ts +++ b/src/lib/io/write/zip-file-system.ts @@ -1,5 +1,5 @@ import { Crc } from './crc'; -import { type FileSystem, type Writer } from './file-system'; +import type { FileSystem, Writer } from './file-system'; // https://gist.github.com/rvaiya/4a2192df729056880a027789ae3cd4b7 @@ -88,9 +88,9 @@ class ZipFileSystem implements FileSystem { const view = new DataView(header.buffer); view.setUint32(0, 0x04034b50, true); - view.setUint16(4, 20, true); // version needed to extract = 2.0 - view.setUint16(6, 0x8 | 0x800, true); // indicate crc and size comes after, utf-8 encoding - view.setUint16(8, 0, true); // method = 0 (store) + view.setUint16(4, 20, true); // version needed to extract = 2.0 + view.setUint16(6, 0x8 | 0x800, true); // indicate crc and size comes after, utf-8 encoding + view.setUint16(8, 0, true); // method = 0 (store) view.setUint16(10, dosTime, true); view.setUint16(12, dosDate, true); view.setUint16(26, nameLen, true); @@ -112,7 +112,9 @@ class ZipFileSystem implements FileSystem { // when the archive crosses 4 GiB rather than silently writing // wrapped offsets (an unreadable file). if (offset >= 0xffffffff) { - throw new Error('Zip output exceeds 4 GiB and zip64 is not supported — write unbundled output instead.'); + throw new Error( + 'Zip output exceeds 4 GiB and zip64 is not supported — write unbundled output instead.' + ); } const data = new Uint8Array(16); const view = new DataView(data.buffer); diff --git a/src/lib/mesh/coplanar-merge.ts b/src/lib/mesh/coplanar-merge.ts index 96c8f0e4..f30123f3 100644 --- a/src/lib/mesh/coplanar-merge.ts +++ b/src/lib/mesh/coplanar-merge.ts @@ -149,8 +149,12 @@ const coplanarMerge = (mesh: Mesh, voxelResolution: number): Mesh => { const cy = positions[ic * 3 + 1]; const cz = positions[ic * 3 + 2]; - const ex = bx - ax, ey = by - ay, ez = bz - az; - const fx = cx - ax, fy = cy - ay, fz = cz - az; + const ex = bx - ax, + ey = by - ay, + ez = bz - az; + const fx = cx - ax, + fy = cy - ay, + fz = cz - az; let nx = ey * fz - ez * fy; let ny = ez * fx - ex * fz; let nz = ex * fy - ey * fx; @@ -161,7 +165,9 @@ const coplanarMerge = (mesh: Mesh, voxelResolution: number): Mesh => { continue; } const inv = 1 / nLen; - nx *= inv; ny *= inv; nz *= inv; + nx *= inv; + ny *= inv; + nz *= inv; triNxArr[t] = nx; triNyArr[t] = ny; triNzArr[t] = nz; @@ -244,26 +250,32 @@ const coplanarMerge = (mesh: Mesh, voxelResolution: number): Mesh => { // avoid precision loss in the cross product. By construction // t x b = n, so a polygon traced CCW around +n in 3D projects to a // CCW polygon in (t, b) coordinates (positive 2D signed area). - const buildBasis = (nx: number, ny: number, nz: number): [ - number, number, number, number, number, number - ] => { + const buildBasis = (nx: number, ny: number, nz: number): [number, number, number, number, number, number] => { const ax = Math.abs(nx); const ay = Math.abs(ny); const az = Math.abs(nz); let tx: number, ty: number, tz: number; if (ax <= ay && ax <= az) { // X axis least aligned: t = (1, 0, 0) x n - tx = 0; ty = -nz; tz = ny; + tx = 0; + ty = -nz; + tz = ny; } else if (ay <= az) { // Y axis least aligned: t = (0, 1, 0) x n - tx = nz; ty = 0; tz = -nx; + tx = nz; + ty = 0; + tz = -nx; } else { // Z axis least aligned: t = (0, 0, 1) x n - tx = -ny; ty = nx; tz = 0; + tx = -ny; + ty = nx; + tz = 0; } const tlen = Math.sqrt(tx * tx + ty * ty + tz * tz); const inv = 1 / tlen; - tx *= inv; ty *= inv; tz *= inv; + tx *= inv; + ty *= inv; + tz *= inv; // bitangent = n x t (unit length because n and t are unit and // perpendicular). const bx = ny * tz - nz * ty; @@ -323,11 +335,14 @@ const coplanarMerge = (mesh: Mesh, voxelResolution: number): Mesh => { const c = triVertsArr[t * 3 + 2]; let from: number, to: number; if (a === v) { - from = b; to = c; + from = b; + to = c; } else if (b === v) { - from = c; to = a; + from = c; + to = a; } else { - from = a; to = b; + from = a; + to = b; } for (let j = 0; j < i; j++) { if (fanFromScratch[j] === from) return -1; @@ -390,13 +405,7 @@ const coplanarMerge = (mesh: Mesh, voxelResolution: number): Mesh => { // drops the vertex from this polygon would create a T-junction // with the vertex's other incident tris (which still reference it), // so we leave the cleanup to the worklist. - const earClip = ( - px: Float64Array, - py: Float64Array, - n: number, - out: Int32Array, - outOffset: number - ): number => { + const earClip = (px: Float64Array, py: Float64Array, n: number, out: Int32Array, outOffset: number): number => { if (n < 3) return -1; if (n === 3) { out[outOffset] = 0; @@ -423,12 +432,12 @@ const coplanarMerge = (mesh: Mesh, voxelResolution: number): Mesh => { } const isConvex = (a: number, b: number, c: number): boolean => { - return (px[b] - px[a]) * (py[c] - py[a]) - - (py[b] - py[a]) * (px[c] - px[a]) > 0; + return (px[b] - px[a]) * (py[c] - py[a]) - (py[b] - py[a]) * (px[c] - px[a]) > 0; }; const inTri = (p: number, a: number, b: number, c: number): boolean => { - const x = px[p], y = py[p]; + const x = px[p], + y = py[p]; const d1 = (x - px[b]) * (py[a] - py[b]) - (px[a] - px[b]) * (y - py[b]); const d2 = (x - px[c]) * (py[b] - py[c]) - (px[b] - px[c]) * (y - py[c]); const d3 = (x - px[a]) * (py[c] - py[a]) - (px[c] - px[a]) * (y - py[a]); @@ -637,8 +646,12 @@ const coplanarMerge = (mesh: Mesh, voxelResolution: number): Mesh => { // (mB tris, mB+1 polygon verts). const mA = i2 - i1; const mB = k - mA; - arcStartIdx[0] = i1; arcPolySize[0] = mA + 1; arcPlaneT[0] = fanTris[i1]; - arcStartIdx[1] = i2; arcPolySize[1] = mB + 1; arcPlaneT[1] = fanTris[i2]; + arcStartIdx[0] = i1; + arcPolySize[0] = mA + 1; + arcPlaneT[0] = fanTris[i1]; + arcStartIdx[1] = i2; + arcPolySize[1] = mB + 1; + arcPlaneT[1] = fanTris[i2]; arcCount = 2; } diff --git a/src/lib/mesh/marching-cubes.ts b/src/lib/mesh/marching-cubes.ts index daebf208..1fb5c2cd 100644 --- a/src/lib/mesh/marching-cubes.ts +++ b/src/lib/mesh/marching-cubes.ts @@ -1,23 +1,17 @@ import type { Bounds } from '../data-table'; -import { - BLOCK_EMPTY, - BLOCK_SOLID, - BLOCKS_PER_WORD, - EVEN_BITS, - SparseVoxelGrid, - readBlockType -} from '../voxel/sparse-voxel-grid'; +import type { SparseVoxelGrid } from '../voxel/sparse-voxel-grid'; +import { BLOCK_EMPTY, BLOCK_SOLID, BLOCKS_PER_WORD, EVEN_BITS, readBlockType } from '../voxel/sparse-voxel-grid'; /** * A simple triangle mesh with positions and indices. */ -interface Mesh { +type Mesh = { /** Vertex positions (3 floats per vertex) */ positions: Float32Array; /** Triangle indices (3 indices per triangle) */ indices: Uint32Array; -} +}; /** * Result of marching cubes surface extraction. @@ -27,14 +21,14 @@ type MarchingCubesMesh = Mesh; /** * Options for marching cubes extraction. */ -interface MarchingCubesOptions { +type MarchingCubesOptions = { /** * Pre-merge exact full-face cells on flat axis-aligned regions before * creating the mesh. Ambiguous and bevel cases still use normal marching * cubes, so coplanarMerge can apply the final lossless optimization. */ mergeFlatFaces?: boolean; -} +}; // ============================================================================ // Voxel bit helpers @@ -120,7 +114,7 @@ function marchingCubes( for (let j = 0; j < oldCap; j++) { const k = oldKeys[j]; if (k === -1) continue; - let i = (Math.imul(k | 0, 0x9E3779B9) >>> 0) & vMask; + let i = (Math.imul(k | 0, 0x9e3779b9) >>> 0) & vMask; while (vKeys[i] !== -1) i = (i + 1) & vMask; vKeys[i] = k; vVals[i] = oldVals[j]; @@ -141,7 +135,7 @@ function marchingCubes( for (let j = 0; j < oldCap; j++) { const k = oldKeys[j]; if (k === -1) continue; - let i = (Math.imul(k | 0, 0x9E3779B9) >>> 0) & oMask; + let i = (Math.imul(k | 0, 0x9e3779b9) >>> 0) & oMask; while (oKeys[i] !== -1) i = (i + 1) & oMask; oKeys[i] = k; } @@ -184,11 +178,13 @@ function marchingCubes( // Block coordinate of the block currently being processed. Captured by // isOccupiedLocal so it can fold the per-corner block lookup into a // direct typed-array index instead of a hash lookup. - let bx = 0, by = 0, bz = 0; + let bx = 0, + by = 0, + bz = 0; const isOccupiedLocal = (cx: number, cy: number, cz: number): boolean => { if (cx < 0 || cy < 0 || cz < 0) return false; - const idx = ((cx >> 2) - bx + 1) + ((cy >> 2) - by + 1) * 3 + ((cz >> 2) - bz + 1) * 9; + const idx = (cx >> 2) - bx + 1 + ((cy >> 2) - by + 1) * 3 + ((cz >> 2) - bz + 1) * 9; const entry = neighborEntry[idx]; if (entry === NEIGHBOR_EMPTY) return false; if (entry === NEIGHBOR_SOLID) return true; @@ -273,10 +269,10 @@ function marchingCubes( const getVertex = (vx: number, vy: number, vz: number, axis: number): number => { // Pack (vx, vy, vz, axis) into a single key. Offset by 1 so that // vx = -1 (from the boundary extension) maps to 0, keeping keys non-negative. - const key = ((vx + 1) + (vy + 1) * strideX + (vz + 1) * strideXY) * 3 + axis; + const key = (vx + 1 + (vy + 1) * strideX + (vz + 1) * strideXY) * 3 + axis; // Probe for either the matching slot or the next empty one. - let i = (Math.imul(key | 0, 0x9E3779B9) >>> 0) & vMask; + let i = (Math.imul(key | 0, 0x9e3779b9) >>> 0) & vMask; while (true) { const k = vKeys[i]; if (k === key) return vVals[i]; @@ -324,8 +320,7 @@ function marchingCubes( faceCellKeys = grown; } faceCellKeys[faceCellLen++] = - (((bucket * faceCoordStride + (p + 1)) * faceCoordStride + (u + 1)) * - faceCoordStride + (v + 1)); + ((bucket * faceCoordStride + (p + 1)) * faceCoordStride + (u + 1)) * faceCoordStride + (v + 1); }; const addDiagCell = (bucket: number, plane: number, u: number, e: number): void => { @@ -336,8 +331,9 @@ function marchingCubes( diagCellKeys = grown; } diagCellKeys[diagCellLen++] = - (((bucket * diagCoordStride + (plane + diagCoordOffset)) * diagCoordStride + - (u + diagCoordOffset)) * diagCoordStride + (e + diagCoordOffset)); + ((bucket * diagCoordStride + (plane + diagCoordOffset)) * diagCoordStride + (u + diagCoordOffset)) * + diagCoordStride + + (e + diagCoordOffset); }; const collectFlatFace = (cubeIndex: number, vx: number, vy: number, vz: number): boolean => { @@ -379,7 +375,9 @@ function marchingCubes( return pair * 4 + signBits; }; - const decodeDiagBucket = (bucket: number): { + const decodeDiagBucket = ( + bucket: number + ): { axisA: number; axisB: number; axisE: number; @@ -422,29 +420,74 @@ function marchingCubes( const y = vy * 2; const z = vz * 2; switch (edge) { - case 0: out[offset] = x + 1; out[offset + 1] = y; out[offset + 2] = z; break; - case 1: out[offset] = x + 2; out[offset + 1] = y + 1; out[offset + 2] = z; break; - case 2: out[offset] = x + 1; out[offset + 1] = y + 2; out[offset + 2] = z; break; - case 3: out[offset] = x; out[offset + 1] = y + 1; out[offset + 2] = z; break; - case 4: out[offset] = x + 1; out[offset + 1] = y; out[offset + 2] = z + 2; break; - case 5: out[offset] = x + 2; out[offset + 1] = y + 1; out[offset + 2] = z + 2; break; - case 6: out[offset] = x + 1; out[offset + 1] = y + 2; out[offset + 2] = z + 2; break; - case 7: out[offset] = x; out[offset + 1] = y + 1; out[offset + 2] = z + 2; break; - case 8: out[offset] = x; out[offset + 1] = y; out[offset + 2] = z + 1; break; - case 9: out[offset] = x + 2; out[offset + 1] = y; out[offset + 2] = z + 1; break; - case 10: out[offset] = x + 2; out[offset + 1] = y + 2; out[offset + 2] = z + 1; break; - default: out[offset] = x; out[offset + 1] = y + 2; out[offset + 2] = z + 1; break; + case 0: + out[offset] = x + 1; + out[offset + 1] = y; + out[offset + 2] = z; + break; + case 1: + out[offset] = x + 2; + out[offset + 1] = y + 1; + out[offset + 2] = z; + break; + case 2: + out[offset] = x + 1; + out[offset + 1] = y + 2; + out[offset + 2] = z; + break; + case 3: + out[offset] = x; + out[offset + 1] = y + 1; + out[offset + 2] = z; + break; + case 4: + out[offset] = x + 1; + out[offset + 1] = y; + out[offset + 2] = z + 2; + break; + case 5: + out[offset] = x + 2; + out[offset + 1] = y + 1; + out[offset + 2] = z + 2; + break; + case 6: + out[offset] = x + 1; + out[offset + 1] = y + 2; + out[offset + 2] = z + 2; + break; + case 7: + out[offset] = x; + out[offset + 1] = y + 1; + out[offset + 2] = z + 2; + break; + case 8: + out[offset] = x; + out[offset + 1] = y; + out[offset + 2] = z + 1; + break; + case 9: + out[offset] = x + 2; + out[offset + 1] = y; + out[offset + 2] = z + 1; + break; + case 10: + out[offset] = x + 2; + out[offset + 1] = y + 2; + out[offset + 2] = z + 1; + break; + default: + out[offset] = x; + out[offset + 1] = y + 2; + out[offset + 2] = z + 1; + break; } }; const pairVerts = new Int32Array(18); const uniqueVerts = new Int32Array(12); - const samePoint = ( - src: Int32Array, - a: number, - b: number - ): boolean => src[a] === src[b] && src[a + 1] === src[b + 1] && src[a + 2] === src[b + 2]; + const samePoint = (src: Int32Array, a: number, b: number): boolean => + src[a] === src[b] && src[a + 1] === src[b + 1] && src[a + 2] === src[b + 2]; const pointInUnique = (x: number, y: number, z: number, uniqueCount: number): boolean => { for (let i = 0; i < uniqueCount; i++) { @@ -479,9 +522,11 @@ function marchingCubes( let found = false; for (let j = 0; j < uniqueCount; j++) { const dst = j * 3; - if (pairVerts[src] === uniqueVerts[dst] && + if ( + pairVerts[src] === uniqueVerts[dst] && pairVerts[src + 1] === uniqueVerts[dst + 1] && - pairVerts[src + 2] === uniqueVerts[dst + 2]) { + pairVerts[src + 2] === uniqueVerts[dst + 2] + ) { found = true; break; } @@ -503,11 +548,7 @@ function marchingCubes( const bx = pairVerts[6] - pairVerts[0]; const by = pairVerts[7] - pairVerts[1]; const bz = pairVerts[8] - pairVerts[2]; - const normal = [ - ay * bz - az * by, - az * bx - ax * bz, - ax * by - ay * bx - ]; + const normal = [ay * bz - az * by, az * bx - ax * bz, ax * by - ay * bx]; const absNormal = [Math.abs(normal[0]), Math.abs(normal[1]), Math.abs(normal[2])]; let axisE = -1; let axisA = -1; @@ -523,7 +564,8 @@ function marchingCubes( const signA = normal[axisA] > 0 ? 1 : -1; const signB = normal[axisB] > 0 ? 1 : -1; const bucket = diagBucket(axisA, axisB, signA, signB); - const plane = signA * coordByAxis(uniqueVerts[0], uniqueVerts[1], uniqueVerts[2], axisA) + + const plane = + signA * coordByAxis(uniqueVerts[0], uniqueVerts[1], uniqueVerts[2], axisA) + signB * coordByAxis(uniqueVerts[0], uniqueVerts[1], uniqueVerts[2], axisB); let minU = Infinity; let maxU = -Infinity; @@ -547,10 +589,12 @@ function marchingCubes( const p10 = diagPoint(bucket, plane, maxU, minE); const p11 = diagPoint(bucket, plane, maxU, maxE); const p01 = diagPoint(bucket, plane, minU, maxE); - if (!pointInUnique(p00[0], p00[1], p00[2], uniqueCount) || + if ( + !pointInUnique(p00[0], p00[1], p00[2], uniqueCount) || !pointInUnique(p10[0], p10[1], p10[2], uniqueCount) || !pointInUnique(p11[0], p11[1], p11[2], uniqueCount) || - !pointInUnique(p01[0], p01[1], p01[2], uniqueCount)) { + !pointInUnique(p01[0], p01[1], p01[2], uniqueCount) + ) { return false; } @@ -649,10 +693,7 @@ function marchingCubes( addPerimeterVertex(getScaledVertex(x2, y2, z2), signA * a - signB * b, e); }; - const addSplitSegment = ( - x0: number, y0: number, z0: number, - x1: number, y1: number, z1: number - ): void => { + const addSplitSegment = (x0: number, y0: number, z0: number, x1: number, y1: number, z1: number): void => { const changes = (x0 !== x1 ? 1 : 0) + (y0 !== y1 ? 1 : 0) + (z0 !== z1 ? 1 : 0); if (changes !== 1) return; if (x0 !== x1) { @@ -672,8 +713,12 @@ function marchingCubes( const addSplitEdgeVertices = ( axis: number, - x0: number, y0: number, z0: number, - x1: number, y1: number, z1: number, + x0: number, + y0: number, + z0: number, + x1: number, + y1: number, + z1: number, addPoint: (x2: number, y2: number, z2: number) => void = (px, py, pz) => { addPerimeterPoint(axis, px, py, pz); } @@ -761,14 +806,7 @@ function marchingCubes( } }; - const emitFaceRectangle = ( - bucket: number, - p: number, - u0: number, - v0: number, - u1: number, - v1: number - ): void => { + const emitFaceRectangle = (bucket: number, p: number, u0: number, v0: number, u1: number, v1: number): void => { const axis = bucket >> 1; const positive = (bucket & 1) === 1; const a = scaledFacePoint(axis, p, u0, v0); @@ -796,14 +834,7 @@ function marchingCubes( triangulateTwoSideChain(side2Start, side2End, side3Start, side3End, useLocalCcw); }; - const emitDiagRectangle = ( - bucket: number, - plane: number, - u0: number, - e0: number, - u1: number, - e1: number - ): void => { + const emitDiagRectangle = (bucket: number, plane: number, u0: number, e0: number, u1: number, e1: number): void => { const a = diagPoint(bucket, plane, u0, e0); const b = diagPoint(bucket, plane, u1, e0); const c = diagPoint(bucket, plane, u1, e1); @@ -952,7 +983,7 @@ function marchingCubes( const hash = (key: number): number => { const hi = (key / 0x100000000) | 0; - return (Math.imul((key | 0) ^ hi, 0x9E3779B9) >>> 0) & hMask; + return (Math.imul((key | 0) ^ hi, 0x9e3779b9) >>> 0) & hMask; }; for (let i = 0; i < count; i++) { @@ -1022,7 +1053,9 @@ function marchingCubes( diagCellKeys = new Float64Array(0); diagKeys.sort(); - const decodeDiagKey = (key: number): { + const decodeDiagKey = ( + key: number + ): { bucket: number; plane: number; u: number; @@ -1121,14 +1154,7 @@ function marchingCubes( collectSplitPoints = false; for (let r = 0; r < rectLen; r++) { - emitFaceRectangle( - rectBucket[r], - rectP[r], - rectU0[r], - rectV0[r], - rectU1[r], - rectV1[r] - ); + emitFaceRectangle(rectBucket[r], rectP[r], rectU0[r], rectV0[r], rectU1[r], rectV1[r]); } for (let r = 0; r < diagRectLen; r++) { emitDiagRectangle( @@ -1187,9 +1213,8 @@ function marchingCubes( const nbY = by + dy; for (let dx = -1; dx <= 1; dx++) { const nbX = bx + dx; - const slot = (dx + 1) + (dy + 1) * 3 + (dz + 1) * 9; - if (nbX < 0 || nbY < 0 || nbZ < 0 || - nbX >= nbx || nbY >= nby || nbZ >= nbz) { + const slot = dx + 1 + (dy + 1) * 3 + (dz + 1) * 9; + if (nbX < 0 || nbY < 0 || nbZ < 0 || nbX >= nbx || nbY >= nby || nbZ >= nbz) { neighborEntry[slot] = NEIGHBOR_EMPTY; allNeighborsSolid = false; continue; @@ -1225,9 +1250,9 @@ function marchingCubes( for (let ly = -1; ly < 4; ly++) { const lyInside = ly >= 0 && ly <= 2; for (let lx = -1; lx < 4; lx++) { - // For solid blocks, the 27 cells with all axes in 0..2 - // are fully inside the block. All 8 corners are 1 so - // cubeIndex == 255 and no triangles are emitted -- skip. + // For solid blocks, the 27 cells with all axes in 0..2 + // are fully inside the block. All 8 corners are 1 so + // cubeIndex == 255 and no triangles are emitted -- skip. if (currentBlockIsSolid && lzInside && lyInside && lx >= 0 && lx <= 2) continue; const vx = bx * 4 + lx; @@ -1240,10 +1265,16 @@ function marchingCubes( const ownerBz = vz >> 2; if (ownerBx !== bx || ownerBy !== by || ownerBz !== bz) { - // Cell belongs to a different block — skip if that - // block is non-empty (it will process the cell itself). - if (ownerBx >= 0 && ownerBy >= 0 && ownerBz >= 0 && - ownerBx < nbx && ownerBy < nby && ownerBz < nbz) { + // Cell belongs to a different block — skip if that + // block is non-empty (it will process the cell itself). + if ( + ownerBx >= 0 && + ownerBy >= 0 && + ownerBz >= 0 && + ownerBx < nbx && + ownerBy < nby && + ownerBz < nbz + ) { const ownerIdx = ownerBx + ownerBy * nbx + ownerBz * bStride; if (readBlockType(types, ownerIdx) !== BLOCK_EMPTY) continue; } @@ -1251,8 +1282,8 @@ function marchingCubes( // Owner block doesn't exist or is out-of-bounds — // deduplicate so only the first neighboring block to // reach this cell emits triangles. - const cellKey = (vx + 1) + (vy + 1) * strideX + (vz + 1) * strideXY; - let oi = (Math.imul(cellKey | 0, 0x9E3779B9) >>> 0) & oMask; + const cellKey = vx + 1 + (vy + 1) * strideX + (vz + 1) * strideXY; + let oi = (Math.imul(cellKey | 0, 0x9e3779b9) >>> 0) & oMask; let oFound = false; while (true) { const ok = oKeys[oi]; @@ -1281,17 +1312,17 @@ function marchingCubes( const c6 = isOccupiedLocal(vx + 1, vy + 1, vz + 1) ? 1 : 0; const c7 = isOccupiedLocal(vx, vy + 1, vz + 1) ? 1 : 0; - const cubeIndex = c0 | (c1 << 1) | (c2 << 2) | (c3 << 3) | - (c4 << 4) | (c5 << 5) | (c6 << 6) | (c7 << 7); + const cubeIndex = + c0 | (c1 << 1) | (c2 << 2) | (c3 << 3) | (c4 << 4) | (c5 << 5) | (c6 << 6) | (c7 << 7); if (cubeIndex === 0 || cubeIndex === 255) continue; if (collectFlatFace(cubeIndex, vx, vy, vz)) continue; - const edges = EDGE_TABLE[cubeIndex]; // eslint-disable-line no-use-before-define + const edges = EDGE_TABLE[cubeIndex]; if (edges === 0) continue; - const triRow = TRI_TABLE[cubeIndex]; // eslint-disable-line no-use-before-define + const triRow = TRI_TABLE[cubeIndex]; const triLen = triRow.length; const usedMask = collectDiagFaces(triRow, vx, vy, vz); let neededEdges = 0; @@ -1306,18 +1337,18 @@ function marchingCubes( // Compute vertices on edges used by triangles that // were not absorbed into a merged binary-MC rectangle. - if (neededEdges & 1) edgeVerts[0] = getVertex(vx, vy, vz, 0); // edge 0: x-axis at (vx, vy, vz) - if (neededEdges & 2) edgeVerts[1] = getVertex(vx + 1, vy, vz, 1); // edge 1: y-axis at (vx+1, vy, vz) - if (neededEdges & 4) edgeVerts[2] = getVertex(vx, vy + 1, vz, 0); // edge 2: x-axis at (vx, vy+1, vz) - if (neededEdges & 8) edgeVerts[3] = getVertex(vx, vy, vz, 1); // edge 3: y-axis at (vx, vy, vz) - if (neededEdges & 16) edgeVerts[4] = getVertex(vx, vy, vz + 1, 0); // edge 4: x-axis at (vx, vy, vz+1) - if (neededEdges & 32) edgeVerts[5] = getVertex(vx + 1, vy, vz + 1, 1); // edge 5: y-axis at (vx+1, vy, vz+1) - if (neededEdges & 64) edgeVerts[6] = getVertex(vx, vy + 1, vz + 1, 0); // edge 6: x-axis at (vx, vy+1, vz+1) - if (neededEdges & 128) edgeVerts[7] = getVertex(vx, vy, vz + 1, 1); // edge 7: y-axis at (vx, vy, vz+1) - if (neededEdges & 256) edgeVerts[8] = getVertex(vx, vy, vz, 2); // edge 8: z-axis at (vx, vy, vz) - if (neededEdges & 512) edgeVerts[9] = getVertex(vx + 1, vy, vz, 2); // edge 9: z-axis at (vx+1, vy, vz) + if (neededEdges & 1) edgeVerts[0] = getVertex(vx, vy, vz, 0); // edge 0: x-axis at (vx, vy, vz) + if (neededEdges & 2) edgeVerts[1] = getVertex(vx + 1, vy, vz, 1); // edge 1: y-axis at (vx+1, vy, vz) + if (neededEdges & 4) edgeVerts[2] = getVertex(vx, vy + 1, vz, 0); // edge 2: x-axis at (vx, vy+1, vz) + if (neededEdges & 8) edgeVerts[3] = getVertex(vx, vy, vz, 1); // edge 3: y-axis at (vx, vy, vz) + if (neededEdges & 16) edgeVerts[4] = getVertex(vx, vy, vz + 1, 0); // edge 4: x-axis at (vx, vy, vz+1) + if (neededEdges & 32) edgeVerts[5] = getVertex(vx + 1, vy, vz + 1, 1); // edge 5: y-axis at (vx+1, vy, vz+1) + if (neededEdges & 64) edgeVerts[6] = getVertex(vx, vy + 1, vz + 1, 0); // edge 6: x-axis at (vx, vy+1, vz+1) + if (neededEdges & 128) edgeVerts[7] = getVertex(vx, vy, vz + 1, 1); // edge 7: y-axis at (vx, vy, vz+1) + if (neededEdges & 256) edgeVerts[8] = getVertex(vx, vy, vz, 2); // edge 8: z-axis at (vx, vy, vz) + if (neededEdges & 512) edgeVerts[9] = getVertex(vx + 1, vy, vz, 2); // edge 9: z-axis at (vx+1, vy, vz) if (neededEdges & 1024) edgeVerts[10] = getVertex(vx + 1, vy + 1, vz, 2); // edge 10: z-axis at (vx+1, vy+1, vz) - if (neededEdges & 2048) edgeVerts[11] = getVertex(vx, vy + 1, vz, 2); // edge 11: z-axis at (vx, vy+1, vz) + if (neededEdges & 2048) edgeVerts[11] = getVertex(vx, vy + 1, vz, 2); // edge 11: z-axis at (vx, vy+1, vz) // Emit triangles (reversed winding to face outward) ensureIndexCapacity(emitTriLen); @@ -1350,38 +1381,22 @@ function marchingCubes( // TRI_TABLE: 256 entries, each an array of edge indices forming triangles. const EDGE_TABLE: number[] = [ - 0x000, 0x109, 0x203, 0x30a, 0x406, 0x50f, 0x605, 0x70c, - 0x80c, 0x905, 0xa0f, 0xb06, 0xc0a, 0xd03, 0xe09, 0xf00, - 0x190, 0x099, 0x393, 0x29a, 0x596, 0x49f, 0x795, 0x69c, - 0x99c, 0x895, 0xb9f, 0xa96, 0xd9a, 0xc93, 0xf99, 0xe90, - 0x230, 0x339, 0x033, 0x13a, 0x636, 0x73f, 0x435, 0x53c, - 0xa3c, 0xb35, 0x83f, 0x936, 0xe3a, 0xf33, 0xc39, 0xd30, - 0x3a0, 0x2a9, 0x1a3, 0x0aa, 0x7a6, 0x6af, 0x5a5, 0x4ac, - 0xbac, 0xaa5, 0x9af, 0x8a6, 0xfaa, 0xea3, 0xda9, 0xca0, - 0x460, 0x569, 0x663, 0x76a, 0x066, 0x16f, 0x265, 0x36c, - 0xc6c, 0xd65, 0xe6f, 0xf66, 0x86a, 0x963, 0xa69, 0xb60, - 0x5f0, 0x4f9, 0x7f3, 0x6fa, 0x1f6, 0x0ff, 0x3f5, 0x2fc, - 0xdfc, 0xcf5, 0xfff, 0xef6, 0x9fa, 0x8f3, 0xbf9, 0xaf0, - 0x650, 0x759, 0x453, 0x55a, 0x256, 0x35f, 0x055, 0x15c, - 0xe5c, 0xf55, 0xc5f, 0xd56, 0xa5a, 0xb53, 0x859, 0x950, - 0x7c0, 0x6c9, 0x5c3, 0x4ca, 0x3c6, 0x2cf, 0x1c5, 0x0cc, - 0xfcc, 0xec5, 0xdcf, 0xcc6, 0xbca, 0xac3, 0x9c9, 0x8c0, - 0x8c0, 0x9c9, 0xac3, 0xbca, 0xcc6, 0xdcf, 0xec5, 0xfcc, - 0x0cc, 0x1c5, 0x2cf, 0x3c6, 0x4ca, 0x5c3, 0x6c9, 0x7c0, - 0x950, 0x859, 0xb53, 0xa5a, 0xd56, 0xc5f, 0xf55, 0xe5c, - 0x15c, 0x055, 0x35f, 0x256, 0x55a, 0x453, 0x759, 0x650, - 0xaf0, 0xbf9, 0x8f3, 0x9fa, 0xef6, 0xfff, 0xcf5, 0xdfc, - 0x2fc, 0x3f5, 0x0ff, 0x1f6, 0x6fa, 0x7f3, 0x4f9, 0x5f0, - 0xb60, 0xa69, 0x963, 0x86a, 0xf66, 0xe6f, 0xd65, 0xc6c, - 0x36c, 0x265, 0x16f, 0x066, 0x76a, 0x663, 0x569, 0x460, - 0xca0, 0xda9, 0xea3, 0xfaa, 0x8a6, 0x9af, 0xaa5, 0xbac, - 0x4ac, 0x5a5, 0x6af, 0x7a6, 0x0aa, 0x1a3, 0x2a9, 0x3a0, - 0xd30, 0xc39, 0xf33, 0xe3a, 0x936, 0x83f, 0xb35, 0xa3c, - 0x53c, 0x435, 0x73f, 0x636, 0x13a, 0x033, 0x339, 0x230, - 0xe90, 0xf99, 0xc93, 0xd9a, 0xa96, 0xb9f, 0x895, 0x99c, - 0x69c, 0x795, 0x49f, 0x596, 0x29a, 0x393, 0x099, 0x190, - 0xf00, 0xe09, 0xd03, 0xc0a, 0xb06, 0xa0f, 0x905, 0x80c, - 0x70c, 0x605, 0x50f, 0x406, 0x30a, 0x203, 0x109, 0x000 + 0x000, 0x109, 0x203, 0x30a, 0x406, 0x50f, 0x605, 0x70c, 0x80c, 0x905, 0xa0f, 0xb06, 0xc0a, 0xd03, 0xe09, 0xf00, + 0x190, 0x099, 0x393, 0x29a, 0x596, 0x49f, 0x795, 0x69c, 0x99c, 0x895, 0xb9f, 0xa96, 0xd9a, 0xc93, 0xf99, 0xe90, + 0x230, 0x339, 0x033, 0x13a, 0x636, 0x73f, 0x435, 0x53c, 0xa3c, 0xb35, 0x83f, 0x936, 0xe3a, 0xf33, 0xc39, 0xd30, + 0x3a0, 0x2a9, 0x1a3, 0x0aa, 0x7a6, 0x6af, 0x5a5, 0x4ac, 0xbac, 0xaa5, 0x9af, 0x8a6, 0xfaa, 0xea3, 0xda9, 0xca0, + 0x460, 0x569, 0x663, 0x76a, 0x066, 0x16f, 0x265, 0x36c, 0xc6c, 0xd65, 0xe6f, 0xf66, 0x86a, 0x963, 0xa69, 0xb60, + 0x5f0, 0x4f9, 0x7f3, 0x6fa, 0x1f6, 0x0ff, 0x3f5, 0x2fc, 0xdfc, 0xcf5, 0xfff, 0xef6, 0x9fa, 0x8f3, 0xbf9, 0xaf0, + 0x650, 0x759, 0x453, 0x55a, 0x256, 0x35f, 0x055, 0x15c, 0xe5c, 0xf55, 0xc5f, 0xd56, 0xa5a, 0xb53, 0x859, 0x950, + 0x7c0, 0x6c9, 0x5c3, 0x4ca, 0x3c6, 0x2cf, 0x1c5, 0x0cc, 0xfcc, 0xec5, 0xdcf, 0xcc6, 0xbca, 0xac3, 0x9c9, 0x8c0, + 0x8c0, 0x9c9, 0xac3, 0xbca, 0xcc6, 0xdcf, 0xec5, 0xfcc, 0x0cc, 0x1c5, 0x2cf, 0x3c6, 0x4ca, 0x5c3, 0x6c9, 0x7c0, + 0x950, 0x859, 0xb53, 0xa5a, 0xd56, 0xc5f, 0xf55, 0xe5c, 0x15c, 0x055, 0x35f, 0x256, 0x55a, 0x453, 0x759, 0x650, + 0xaf0, 0xbf9, 0x8f3, 0x9fa, 0xef6, 0xfff, 0xcf5, 0xdfc, 0x2fc, 0x3f5, 0x0ff, 0x1f6, 0x6fa, 0x7f3, 0x4f9, 0x5f0, + 0xb60, 0xa69, 0x963, 0x86a, 0xf66, 0xe6f, 0xd65, 0xc6c, 0x36c, 0x265, 0x16f, 0x066, 0x76a, 0x663, 0x569, 0x460, + 0xca0, 0xda9, 0xea3, 0xfaa, 0x8a6, 0x9af, 0xaa5, 0xbac, 0x4ac, 0x5a5, 0x6af, 0x7a6, 0x0aa, 0x1a3, 0x2a9, 0x3a0, + 0xd30, 0xc39, 0xf33, 0xe3a, 0x936, 0x83f, 0xb35, 0xa3c, 0x53c, 0x435, 0x73f, 0x636, 0x13a, 0x033, 0x339, 0x230, + 0xe90, 0xf99, 0xc93, 0xd9a, 0xa96, 0xb9f, 0x895, 0x99c, 0x69c, 0x795, 0x49f, 0x596, 0x29a, 0x393, 0x099, 0x190, + 0xf00, 0xe09, 0xd03, 0xc0a, 0xb06, 0xa0f, 0x905, 0x80c, 0x70c, 0x605, 0x50f, 0x406, 0x30a, 0x203, 0x109, 0x000 ]; const TRI_TABLE: number[][] = [ diff --git a/src/lib/mesh/voxel-faces.ts b/src/lib/mesh/voxel-faces.ts index 20b815a9..19861ac7 100644 --- a/src/lib/mesh/voxel-faces.ts +++ b/src/lib/mesh/voxel-faces.ts @@ -1,15 +1,10 @@ import type { Bounds } from '../data-table'; +import type { SparseVoxelGrid } from '../voxel/sparse-voxel-grid'; +import { BLOCK_EMPTY, BLOCK_SOLID, BLOCKS_PER_WORD, EVEN_BITS, readBlockType } from '../voxel/sparse-voxel-grid'; + import type { Mesh } from './marching-cubes'; -import { - BLOCK_EMPTY, - BLOCK_SOLID, - BLOCKS_PER_WORD, - EVEN_BITS, - SparseVoxelGrid, - readBlockType -} from '../voxel/sparse-voxel-grid'; -const HASH_MUL = 0x9E3779B9; +const HASH_MUL = 0x9e3779b9; /** * Extract a watertight voxel-boundary mesh from a SparseVoxelGrid. @@ -24,11 +19,7 @@ const HASH_MUL = 0x9E3779B9; * @param voxelResolution - Size of each voxel in world units. * @returns Mesh with positions and indices. */ -const voxelFaces = ( - grid: SparseVoxelGrid, - gridBounds: Bounds, - voxelResolution: number -): Mesh => { +const voxelFaces = (grid: SparseVoxelGrid, gridBounds: Bounds, voxelResolution: number): Mesh => { const { nbx, nby, nbz, bStride, types, masks, nx, ny, nz } = grid; const totalBlocks = nbx * nby * nbz; const coordStride = Math.max(nx, ny, nz) + 1; @@ -44,8 +35,7 @@ const voxelFaces = ( grown.set(faceKeys); faceKeys = grown; } - faceKeys[faceLen++] = - (((bucket * coordStride + p) * coordStride + u) * coordStride + v); + faceKeys[faceLen++] = ((bucket * coordStride + p) * coordStride + u) * coordStride + v; }; const blockTypeAt = (bx: number, by: number, bz: number): number => { @@ -57,9 +47,7 @@ const voxelFaces = ( const isVoxelSetLocal = (lo: number, hi: number, lx: number, ly: number, lz: number): boolean => { const bitIdx = lx + (ly << 2) + (lz << 4); - return bitIdx < 32 ? - ((lo >>> bitIdx) & 1) !== 0 : - ((hi >>> (bitIdx - 32)) & 1) !== 0; + return bitIdx < 32 ? ((lo >>> bitIdx) & 1) !== 0 : ((hi >>> (bitIdx - 32)) & 1) !== 0; }; const isVoxelSetGlobal = (ix: number, iy: number, iz: number): boolean => { @@ -76,12 +64,24 @@ const voxelFaces = ( const addVoxelFace = (ix: number, iy: number, iz: number, bucket: number): void => { switch (bucket) { - case 0: addFace(0, ix, iy, iz); break; // -X - case 1: addFace(1, ix + 1, iy, iz); break; // +X - case 2: addFace(2, iy, ix, iz); break; // -Y - case 3: addFace(3, iy + 1, ix, iz); break; // +Y - case 4: addFace(4, iz, ix, iy); break; // -Z - default: addFace(5, iz + 1, ix, iy); break; // +Z + case 0: + addFace(0, ix, iy, iz); + break; // -X + case 1: + addFace(1, ix + 1, iy, iz); + break; // +X + case 2: + addFace(2, iy, ix, iz); + break; // -Y + case 3: + addFace(3, iy + 1, ix, iz); + break; // +Y + case 4: + addFace(4, iz, ix, iy); + break; // -Z + default: + addFace(5, iz + 1, ix, iy); + break; // +Z } }; @@ -326,12 +326,7 @@ const voxelFaces = ( groupStart = groupEnd; } - const globalPoint = ( - axis: number, - p: number, - u: number, - v: number - ): [number, number, number] => { + const globalPoint = (axis: number, p: number, u: number, v: number): [number, number, number] => { if (axis === 0) return [p, u, v]; if (axis === 1) return [u, p, v]; return [u, v, p]; @@ -354,10 +349,7 @@ const voxelFaces = ( points.push(value); }; - const addLineSegment = ( - x0: number, y0: number, z0: number, - x1: number, y1: number, z1: number - ): void => { + const addLineSegment = (x0: number, y0: number, z0: number, x1: number, y1: number, z1: number): void => { if (x0 !== x1) { const key = lineKey(0, x0, y0, z0); addLinePoint(key, x0); @@ -426,7 +418,7 @@ const voxelFaces = ( }; const vertexKey = (x: number, y: number, z: number): number => { - return (x + y * coordStride + z * coordStride * coordStride); + return x + y * coordStride + z * coordStride * coordStride; }; const getVertex = (x: number, y: number, z: number): number => { @@ -483,8 +475,12 @@ const voxelFaces = ( const addEdgeVertices = ( axis: number, - x0: number, y0: number, z0: number, - x1: number, y1: number, z1: number + x0: number, + y0: number, + z0: number, + x1: number, + y1: number, + z1: number ): void => { let varAxis: number; let start: number; @@ -605,12 +601,7 @@ const voxelFaces = ( const b = triNext[a]; const c = triNext[b]; if (isConvexEar(a, b, c)) { - appendOrientedTri( - perimeterScratch[a], - perimeterScratch[b], - perimeterScratch[c], - useLocalCcw - ); + appendOrientedTri(perimeterScratch[a], perimeterScratch[b], perimeterScratch[c], useLocalCcw); } } }; diff --git a/src/lib/ops/bake-transform.ts b/src/lib/ops/bake-transform.ts index 59e57c88..d58e4835 100644 --- a/src/lib/ops/bake-transform.ts +++ b/src/lib/ops/bake-transform.ts @@ -1,13 +1,9 @@ import { Mat3 } from 'playcanvas'; -import { - type ChunkData, - type ReadRequest, - type ChunkSource, - type ChunkSourceMetadata, - SH_REST_COUNTS -} from '../chunk'; -import { RotateSH, Transform } from '../utils'; +import { SH_REST_COUNTS } from '../chunk'; +import type { ChunkData, ReadRequest, ChunkSource, ChunkSourceMetadata } from '../chunk'; +import type { Transform } from '../utils'; +import { RotateSH } from '../utils'; const SH_PER_CHANNEL = [0, 3, 8, 15]; @@ -37,19 +33,24 @@ const bakeTransform = (src: ChunkSource, targetSpace: Transform): ChunkSource => const delta = targetSpace.clone().invert().mul(src.meta.transform); if (delta.isIdentity()) { - return { meta, read: req => src.read(req), close: () => src.close() }; + return { meta, read: (req) => src.read(req), close: () => src.close() }; } const r = delta.rotation; const s = delta.scale; - const rx = r.x, ry = r.y, rz = r.z, rw = r.w; - const tx = delta.translation.x, ty = delta.translation.y, tz = delta.translation.z; + const rx = r.x, + ry = r.y, + rz = r.z, + rw = r.w; + const tx = delta.translation.x, + ty = delta.translation.y, + tz = delta.translation.z; const shBands = src.meta.shBands; const shPerCh = SH_PER_CHANNEL[shBands]; const rotIdentity = Math.abs(Math.abs(rw) - 1) < 1e-6; const logS = Math.log(s); - const rotateSH = (!rotIdentity && shPerCh > 0) ? new RotateSH(new Mat3().setFromQuat(r)) : null; + const rotateSH = !rotIdentity && shPerCh > 0 ? new RotateSH(new Mat3().setFromQuat(r)) : null; const shScratch = rotateSH ? new Float32Array(shPerCh) : null; // Inlined per-element kernels (no Vec3/Quat objects). The float-op order @@ -60,14 +61,16 @@ const bakeTransform = (src: ChunkSource, targetSpace: Transform): ChunkSource => const p = new Float32Array(cd.data); for (let i = 0; i < cd.count; i++) { const o = i * 3; - const x = p[o] * s, y = p[o + 1] * s, z = p[o + 2] * s; // point * scale - const ix = rw * x + ry * z - rz * y; // rotation.transformVector + const x = p[o] * s, + y = p[o + 1] * s, + z = p[o + 2] * s; // point * scale + const ix = rw * x + ry * z - rz * y; // rotation.transformVector const iy = rw * y + rz * x - rx * z; const iz = rw * z + rx * y - ry * x; const iw = -rx * x - ry * y - rz * z; - p[o] = (ix * rw + iw * -rx + iy * -rz - iz * -ry) + tx; // + translation - p[o + 1] = (iy * rw + iw * -ry + iz * -rx - ix * -rz) + ty; - p[o + 2] = (iz * rw + iw * -rz + ix * -ry - iy * -rx) + tz; + p[o] = ix * rw + iw * -rx + iy * -rz - iz * -ry + tx; // + translation + p[o + 1] = iy * rw + iw * -ry + iz * -rx - ix * -rz + ty; + p[o + 2] = iz * rw + iw * -rz + ix * -ry - iy * -rx + tz; } }; @@ -77,8 +80,11 @@ const bakeTransform = (src: ChunkSource, targetSpace: Transform): ChunkSource => const o = i * 8; if (!rotIdentity) { // rot_0 = w, rot_1..3 = x,y,z; q' = r * q (Quat.mul2, lhs = r) - const qx = g[o + 1], qy = g[o + 2], qz = g[o + 3], qw = g[o]; - g[o] = rw * qw - rx * qx - ry * qy - rz * qz; + const qx = g[o + 1], + qy = g[o + 2], + qz = g[o + 3], + qw = g[o]; + g[o] = rw * qw - rx * qx - ry * qy - rz * qz; g[o + 1] = rw * qx + rx * qw + ry * qz - rz * qy; g[o + 2] = rw * qy + ry * qw + rz * qx - rx * qz; g[o + 3] = rw * qz + rz * qw + rx * qy - ry * qx; diff --git a/src/lib/ops/concat-source.ts b/src/lib/ops/concat-source.ts index f8720422..098be51d 100644 --- a/src/lib/ops/concat-source.ts +++ b/src/lib/ops/concat-source.ts @@ -1,11 +1,4 @@ -import { - type ChunkData, - type ChunkDataPool, - type ChunkLayer, - type ReadRequest, - type ChunkSource, - type ChunkSourceMetadata -} from '../chunk'; +import type { ChunkData, ChunkDataPool, ChunkLayer, ReadRequest, ChunkSource, ChunkSourceMetadata } from '../chunk'; import { resolveSplatModel } from '../splat-model'; import { logger } from '../utils'; @@ -14,8 +7,10 @@ const LAYERS: ChunkLayer[] = ['position', 'geometric', 'color', 'other']; // Mutable forms of a read request, built layer-by-layer before passing to a // source's `read` (whose `ReadRequest` fields are readonly): one for a // contiguous chunk read, one for a scatter-gather read. -type MutableReadRequest = { chunkIndex: number; lod: number } & { [L in ChunkLayer]?: ChunkData }; -type MutableRowReadRequest = { indices: Uint32Array; indexOffset: number; count: number } & { [L in ChunkLayer]?: ChunkData }; +type MutableReadRequest = { chunkIndex: number; lod: number } & Partial>; +type MutableRowReadRequest = { indices: Uint32Array; indexOffset: number; count: number } & Partial< + Record +>; /** * Concatenate several sources end-to-end into one, as a lazy view. @@ -42,8 +37,8 @@ const concatSource = (allSources: ChunkSource[], pool: ChunkDataPool): ChunkSour } const ref = allSources[0].meta; - const layerKey = (m: ChunkSourceMetadata) => LAYERS.filter(l => m.availableLayers.has(l)).join(','); - const extraKey = (m: ChunkSourceMetadata) => m.extraColumns.map(e => `${e.name}:${e.type}`).join(','); + const layerKey = (m: ChunkSourceMetadata) => LAYERS.filter((l) => m.availableLayers.has(l)).join(','); + const extraKey = (m: ChunkSourceMetadata) => m.extraColumns.map((e) => `${e.name}:${e.type}`).join(','); const refLayers = layerKey(ref); const refExtras = extraKey(ref); @@ -73,7 +68,7 @@ const concatSource = (allSources: ChunkSource[], pool: ChunkDataPool): ChunkSour // the chunk stitcher's one-step source advance and the pool rejects // zero-count acquires); close() still closes ALL inputs, dropped or not. // Keep one source when everything is empty so the metadata stays derivable. - let sources = allSources.filter(s => s.meta.numGaussians > 0); + let sources = allSources.filter((s) => s.meta.numGaussians > 0); if (sources.length === 0) { sources = [allSources[0]]; } @@ -81,15 +76,15 @@ const concatSource = (allSources: ChunkSource[], pool: ChunkDataPool): ChunkSour // Unlike the structural mismatches above, disagreeing models don't block the // concat: no output format can hold two, so fall back to plain gaussians // (which every variant renders acceptably as) rather than mistagging. - const model = resolveSplatModel(sources.map(s => s.meta.model)); - if (sources.some(s => s.meta.model !== model)) { - const seen = [...new Set(sources.map(s => s.meta.model))].join(', '); + const model = resolveSplatModel(sources.map((s) => s.meta.model)); + if (sources.some((s) => s.meta.model !== model)) { + const seen = [...new Set(sources.map((s) => s.meta.model))].join(', '); logger.warn(`mixed splat models (${seen}); writing the result as '${model}'`); } const S = ref.chunkSize; // Per-source gaussian counts and the output-row offset each source begins at. - const counts = sources.map(s => s.meta.numGaussians); + const counts = sources.map((s) => s.meta.numGaussians); const starts: number[] = []; let total = 0; for (const c of counts) { @@ -115,7 +110,7 @@ const concatSource = (allSources: ChunkSource[], pool: ChunkDataPool): ChunkSour // rows from disk. const { indices, indexOffset, count } = request; if (count <= 0) return; - const wanted: ChunkLayer[] = LAYERS.filter(l => request[l]); + const wanted: ChunkLayer[] = LAYERS.filter((l) => request[l]); if (wanted.length === 0) return; // Bucket rows by source: binary-search `starts` per row (sources @@ -134,7 +129,8 @@ const concatSource = (allSources: ChunkSource[], pool: ChunkDataPool): ChunkSour let hi = nSrc - 1; while (lo < hi) { const mid = (lo + hi + 1) >> 1; - if (starts[mid] <= g) lo = mid; else hi = mid - 1; + if (starts[mid] <= g) lo = mid; + else hi = mid - 1; } srcOf[j] = lo; bucketCount[lo]++; @@ -156,8 +152,12 @@ const concatSource = (allSources: ChunkSource[], pool: ChunkDataPool): ChunkSour const m = bucketCount[s]; if (m === 0) continue; const base = offsets[s]; - const temps = wanted.map(layer => ({ layer, tmp: pool.acquire(layer, ref.layouts[layer]!, m) })); - const req: MutableRowReadRequest = { indices: localIdx.subarray(base, base + m), indexOffset: 0, count: m }; + const temps = wanted.map((layer) => ({ layer, tmp: pool.acquire(layer, ref.layouts[layer]!, m) })); + const req: MutableRowReadRequest = { + indices: localIdx.subarray(base, base + m), + indexOffset: 0, + count: m + }; for (const { layer, tmp } of temps) req[layer] = tmp; await sources[s].read(req); @@ -230,7 +230,7 @@ const concatSource = (allSources: ChunkSource[], pool: ChunkDataPool): ChunkSour }; const close = async (): Promise => { - await Promise.all(allSources.map(s => s.close())); + await Promise.all(allSources.map((s) => s.close())); }; return { meta, read, close }; diff --git a/src/lib/ops/filter-mask.ts b/src/lib/ops/filter-mask.ts index 1f6d0039..8537f5d8 100644 --- a/src/lib/ops/filter-mask.ts +++ b/src/lib/ops/filter-mask.ts @@ -1,15 +1,10 @@ import { Vec3 } from 'playcanvas'; -import { - type ChunkData, - type ChunkDataPool, - type ChunkLayer, - type ChunkSource, - type ChunkSourceMetadata, - SH_REST_COUNTS -} from '../chunk'; +import { SH_REST_COUNTS } from '../chunk'; +import type { ChunkData, ChunkDataPool, ChunkLayer, ChunkSource, ChunkSourceMetadata } from '../chunk'; import { Transform } from '../utils'; import { inverseTransforms, isTransformColumn, rawColumnMap } from '../value-transforms'; + import { bakeTransform } from './bake-transform'; /** @@ -24,7 +19,7 @@ const LAYERS: ChunkLayer[] = ['position', 'geometric', 'color', 'other']; // Mutable form of a chunk read request, built layer-by-layer before passing to // a source's `read` (whose `ReadRequest` fields are readonly). -type MutableReadRequest = { chunkIndex: number; lod: number } & { [L in ChunkLayer]?: ChunkData }; +type MutableReadRequest = { chunkIndex: number; lod: number } & Partial>; type Comparator = 'lt' | 'lte' | 'gt' | 'gte' | 'eq' | 'neq'; @@ -105,7 +100,7 @@ const selectRows = async ( */ const filterNaNRows = (src: ChunkSource, pool: ChunkDataPool): Promise => { const { meta } = src; - const layers = LAYERS.filter(l => meta.availableLayers.has(l)); + const layers = LAYERS.filter((l) => meta.availableLayers.has(l)); const colStride32 = 3 + SH_REST_COUNTS[meta.shBands]; const extras = meta.extraColumns; @@ -158,7 +153,14 @@ type ColumnLoc = { layer: ChunkLayer; elem: number; type: 'float32' | 'uint32' } const POSITION_ELEMS: Record = { x: 0, y: 1, z: 2 }; const GEOMETRIC_ELEMS: Record = { - rot_0: 0, rot_1: 1, rot_2: 2, rot_3: 3, scale_0: 4, scale_1: 5, scale_2: 6, opacity: 7 + rot_0: 0, + rot_1: 1, + rot_2: 2, + rot_3: 3, + scale_0: 4, + scale_1: 5, + scale_2: 6, + opacity: 7 }; const DC_ELEMS: Record = { f_dc_0: 0, f_dc_1: 1, f_dc_2: 2 }; @@ -173,7 +175,7 @@ const columnLocation = (name: string, meta: ChunkSourceMetadata): ColumnLoc | nu const k = parseInt(rest[1], 10); return k < SH_REST_COUNTS[meta.shBands] ? { layer: 'color', elem: 3 + k, type: 'float32' } : null; } - const ei = meta.extraColumns.findIndex(e => e.name === name); + const ei = meta.extraColumns.findIndex((e) => e.name === name); if (ei >= 0) return { layer: 'other', elem: ei, type: meta.extraColumns[ei].type }; return null; }; @@ -212,7 +214,9 @@ const filterByValueRows = (src: ChunkSource, pool: ChunkDataPool, params: ByValu const compare = comparators[params.comparator as Comparator]; if (!compare) { - throw new Error(`filterByValue: unknown comparator '${params.comparator}', expected one of: ${Object.keys(comparators).join(', ')}`); + throw new Error( + `filterByValue: unknown comparator '${params.comparator}', expected one of: ${Object.keys(comparators).join(', ')}` + ); } // Transform-affected columns must be compared in baked (world) space, exactly @@ -253,10 +257,10 @@ const filterBoxRows = (src: ChunkSource, pool: ChunkDataPool, params: BoxParams) const p = new Float32Array(buffers.position!); return (r: number) => { const o = r * 3; - const x = p[o], y = p[o + 1], z = p[o + 2]; - return x >= min.x && x <= max.x && - y >= min.y && y <= max.y && - z >= min.z && z <= max.z; + const x = p[o], + y = p[o + 1], + z = p[o + 2]; + return x >= min.x && x <= max.x && y >= min.y && y <= max.y && z >= min.z && z <= max.z; }; }); } @@ -287,7 +291,9 @@ const filterBoxRows = (src: ChunkSource, pool: ChunkDataPool, params: BoxParams) const p = new Float32Array(buffers.position!); return (r: number) => { const o = r * 3; - const x = p[o], y = p[o + 1], z = p[o + 2]; + const x = p[o], + y = p[o + 1], + z = p[o + 2]; for (let j = 0; j < 3; j++) { const proj = rawAxes[j].x * x + rawAxes[j].y * y + rawAxes[j].z * z; if (proj < rawMin[j] || proj > rawMax[j]) return false; @@ -317,13 +323,17 @@ const filterSphereRows = (src: ChunkSource, pool: ChunkDataPool, params: SphereP rawRadius /= transform.scale; } const radiusSq = rawRadius * rawRadius; - const cx = rawCenter.x, cy = rawCenter.y, cz = rawCenter.z; + const cx = rawCenter.x, + cy = rawCenter.y, + cz = rawCenter.z; return selectRows(src, pool, ['position'], (buffers) => { const p = new Float32Array(buffers.position!); return (r: number) => { const o = r * 3; - const dx = p[o] - cx, dy = p[o + 1] - cy, dz = p[o + 2] - cz; + const dx = p[o] - cx, + dy = p[o + 1] - cy, + dz = p[o + 2] - cz; return dx * dx + dy * dy + dz * dz < radiusSq; }; }); diff --git a/src/lib/ops/filter-source.ts b/src/lib/ops/filter-source.ts index da13b64c..3ebe45fc 100644 --- a/src/lib/ops/filter-source.ts +++ b/src/lib/ops/filter-source.ts @@ -1,15 +1,8 @@ -import { - type ChunkData, - type ChunkDataPool, - type ChunkLayer, - type ReadRequest, - type ChunkSource, - type ChunkSourceMetadata -} from '../chunk'; +import type { ChunkData, ChunkDataPool, ChunkLayer, ReadRequest, ChunkSource, ChunkSourceMetadata } from '../chunk'; // Mutable form of a chunk read request, for building one layer-by-layer before // passing it to a parent's `read` (whose `ReadRequest` fields are readonly). -type MutableReadRequest = { chunkIndex: number; lod: number } & { [L in ChunkLayer]?: ChunkData }; +type MutableReadRequest = { chunkIndex: number; lod: number } & Partial>; /** * Select a subset of a source's gaussians by index, as a lazy view. diff --git a/src/lib/ops/map-source.ts b/src/lib/ops/map-source.ts index 10c7ed4a..03c30966 100644 --- a/src/lib/ops/map-source.ts +++ b/src/lib/ops/map-source.ts @@ -1,5 +1,5 @@ -import { type ChunkSource, type ChunkSourceMetadata } from '../chunk'; -import { type Transform } from '../utils'; +import type { ChunkSource, ChunkSourceMetadata } from '../chunk'; +import type { Transform } from '../utils'; /** * Pre-apply a coordinate-space transform onto a {@link ChunkSource}, lazily. @@ -20,7 +20,7 @@ const mapSource = (src: ChunkSource, transform: Transform): ChunkSource => { const meta: ChunkSourceMetadata = { ...src.meta, transform: composed }; return { meta, - read: req => src.read(req), + read: (req) => src.read(req), close: () => src.close() }; }; diff --git a/src/lib/ops/morton-order.ts b/src/lib/ops/morton-order.ts index 1804b59e..48079dc3 100644 --- a/src/lib/ops/morton-order.ts +++ b/src/lib/ops/morton-order.ts @@ -1,4 +1,4 @@ -import { type ChunkDataPool, type ChunkSource } from '../chunk'; +import type { ChunkDataPool, ChunkSource } from '../chunk'; import { logger } from '../utils'; // Sort `indices` in place into morton (Z-order), reading each point's xyz from @@ -11,8 +11,13 @@ import { logger } from '../utils'; // re-sorted by a finer pass over its sub-range. const sortMortonStrided = ( indices: Uint32Array, - xa: ArrayLike, ya: ArrayLike, za: ArrayLike, - stride: number, ox: number, oy: number, oz: number + xa: ArrayLike, + ya: ArrayLike, + za: ArrayLike, + stride: number, + ox: number, + oy: number, + oz: number ): void => { const generate = (indices: Uint32Array) => { if (indices.length === 0) { @@ -32,13 +37,19 @@ const sortMortonStrided = ( return (Part1By2(z) << 2) + (Part1By2(y) << 1) + Part1By2(x); }; - let mx = Infinity, my = Infinity, mz = Infinity; - let Mx = -Infinity, My = -Infinity, Mz = -Infinity; + let mx = Infinity, + my = Infinity, + mz = Infinity; + let Mx = -Infinity, + My = -Infinity, + Mz = -Infinity; // scene extents across these splats (world-space positions) for (let i = 0; i < indices.length; ++i) { const b = indices[i] * stride; - const x = xa[b + ox], y = ya[b + oy], z = za[b + oz]; + const x = xa[b + ox], + y = ya[b + oy], + z = za[b + oz]; if (x < mx) mx = x; if (x > Mx) Mx = x; if (y < my) my = y; @@ -47,7 +58,9 @@ const sortMortonStrided = ( if (z > Mz) Mz = z; } - const xlen = Mx - mx, ylen = My - my, zlen = Mz - mz; + const xlen = Mx - mx, + ylen = My - my, + zlen = Mz - mz; if (!isFinite(xlen) || !isFinite(ylen) || !isFinite(zlen)) { logger.debug('invalid extents', xlen, ylen, zlen); return; @@ -56,9 +69,9 @@ const sortMortonStrided = ( return; // all points identical } - const xmul = (xlen === 0) ? 0 : 1024 / xlen; - const ymul = (ylen === 0) ? 0 : 1024 / ylen; - const zmul = (zlen === 0) ? 0 : 1024 / zlen; + const xmul = xlen === 0 ? 0 : 1024 / xlen; + const ymul = ylen === 0 ? 0 : 1024 / ylen; + const zmul = zlen === 0 ? 0 : 1024 / zlen; const morton = new Uint32Array(indices.length); for (let i = 0; i < indices.length; ++i) { @@ -82,7 +95,8 @@ const sortMortonStrided = ( } // recursively refine the largest equal-code buckets - let start = 0, end = 1; + let start = 0, + end = 1; while (start < indices.length) { while (end < indices.length && morton[order[end]] === morton[order[start]]) { ++end; @@ -119,7 +133,12 @@ const sortMortonInterleaved = (positions: ArrayLike, indices: Uint32Arra * @param z - Z coordinates, indexed by gaussian. * @param indices - Indices to sort in place. */ -const sortMortonColumns = (x: ArrayLike, y: ArrayLike, z: ArrayLike, indices: Uint32Array): void => { +const sortMortonColumns = ( + x: ArrayLike, + y: ArrayLike, + z: ArrayLike, + indices: Uint32Array +): void => { sortMortonStrided(indices, x, y, z, 1, 0, 0, 0); }; diff --git a/src/lib/ops/permute-source.ts b/src/lib/ops/permute-source.ts index 85211960..edf5c02b 100644 --- a/src/lib/ops/permute-source.ts +++ b/src/lib/ops/permute-source.ts @@ -1,8 +1,4 @@ -import { - type ChunkSource, - type ChunkSourceMetadata, - type ReadRequest -} from '../chunk'; +import type { ChunkSource, ChunkSourceMetadata, ReadRequest } from '../chunk'; /** * Gather a source's gaussians by an ordered index list, as a lazy view. diff --git a/src/lib/ops/reduce-bands-source.ts b/src/lib/ops/reduce-bands-source.ts index 3e902f2c..7ab9492f 100644 --- a/src/lib/ops/reduce-bands-source.ts +++ b/src/lib/ops/reduce-bands-source.ts @@ -1,13 +1,5 @@ -import { - type ChunkDataPool, - type ChunkSource, - type ChunkSourceMetadata, - type ReadRequest, - type SHBands, - SH_REST_COUNTS, - colorStride, - colorFields -} from '../chunk'; +import { SH_REST_COUNTS, colorStride, colorFields } from '../chunk'; +import type { ChunkDataPool, ChunkSource, ChunkSourceMetadata, ReadRequest, SHBands } from '../chunk'; /** * Reduce a source's SH band count (drop the higher-order coefficients), as a @@ -44,10 +36,10 @@ const reduceBandsSource = (src: ChunkSource, outputBands: SHBands, pool: ChunkDa layouts: { ...src.meta.layouts, color: outColorLayout } }; - const ic = SH_REST_COUNTS[inBands] / 3; // input coefficients per channel - const oc = SH_REST_COUNTS[outputBands] / 3; // output coefficients per channel - const inSw = inColorLayout.stride >>> 2; // input color floats per record - const outSw = outColorLayout.stride >>> 2; // output color floats per record + const ic = SH_REST_COUNTS[inBands] / 3; // input coefficients per channel + const oc = SH_REST_COUNTS[outputBands] / 3; // output coefficients per channel + const inSw = inColorLayout.stride >>> 2; // input color floats per record + const outSw = outColorLayout.stride >>> 2; // output color floats per record const read = async (request: ReadRequest): Promise => { const outColor = request.color; @@ -68,10 +60,11 @@ const reduceBandsSource = (src: ChunkSource, outputBands: SHBands, pool: ChunkDa for (let i = 0; i < count; i++) { const so = i * inSw; const dof = i * outSw; - dstF[dof] = srcF[so]; // DC + dstF[dof] = srcF[so]; // DC dstF[dof + 1] = srcF[so + 1]; dstF[dof + 2] = srcF[so + 2]; - for (let c = 0; c < 3; c++) { // kept rest: first `oc` coeffs per channel (channel-major) + for (let c = 0; c < 3; c++) { + // kept rest: first `oc` coeffs per channel (channel-major) for (let k = 0; k < oc; k++) { dstF[dof + 3 + c * oc + k] = srcF[so + 3 + c * ic + k]; } diff --git a/src/lib/ops/select-lod.ts b/src/lib/ops/select-lod.ts index adbbdc53..abde7958 100644 --- a/src/lib/ops/select-lod.ts +++ b/src/lib/ops/select-lod.ts @@ -1,8 +1,4 @@ -import { - type ChunkSource, - type ChunkSourceMetadata, - type ReadRequest -} from '../chunk'; +import type { ChunkSource, ChunkSourceMetadata, ReadRequest } from '../chunk'; /** * View a single LOD level of a multi-LOD source as a single-LOD source — the @@ -50,9 +46,7 @@ const selectLod = (src: ChunkSource, level: number): ChunkSource => { */ const resolveLodLevels = (lodSelect: number[], numLods: number): number[] => { if (lodSelect.length > 0) { - return lodSelect - .map(lod => (lod < 0 ? numLods + lod : lod)) - .filter(lod => lod >= 0 && lod < numLods); + return lodSelect.map((lod) => (lod < 0 ? numLods + lod : lod)).filter((lod) => lod >= 0 && lod < numLods); } return Array.from({ length: numLods }, (_, i) => i); }; diff --git a/src/lib/ops/stack-lods.ts b/src/lib/ops/stack-lods.ts index 0bbb360b..fa0f36c0 100644 --- a/src/lib/ops/stack-lods.ts +++ b/src/lib/ops/stack-lods.ts @@ -1,9 +1,4 @@ -import { - type ChunkLayer, - type ChunkSource, - type ChunkSourceMetadata, - type ReadRequest -} from '../chunk'; +import type { ChunkLayer, ChunkSource, ChunkSourceMetadata, ReadRequest } from '../chunk'; const LAYERS: ChunkLayer[] = ['position', 'geometric', 'color', 'other']; @@ -29,8 +24,8 @@ const stackLods = (sources: ChunkSource[]): ChunkSource => { throw new Error('stackLods: at least one source is required'); } const ref = sources[0].meta; - const layerKey = (m: ChunkSourceMetadata) => LAYERS.filter(l => m.availableLayers.has(l)).join(','); - const extraKey = (m: ChunkSourceMetadata) => m.extraColumns.map(e => `${e.name}:${e.type}`).join(','); + const layerKey = (m: ChunkSourceMetadata) => LAYERS.filter((l) => m.availableLayers.has(l)).join(','); + const extraKey = (m: ChunkSourceMetadata) => m.extraColumns.map((e) => `${e.name}:${e.type}`).join(','); for (const s of sources) { if (s.meta.numLods !== 1) { throw new Error('stackLods: every input must be single-LOD'); @@ -39,7 +34,9 @@ const stackLods = (sources: ChunkSource[]): ChunkSource => { throw new Error('stackLods: inputs must share chunk size and SH band count'); } if (layerKey(s.meta) !== layerKey(ref)) { - throw new Error(`stackLods: available-layer mismatch between LOD levels ([${layerKey(s.meta)}] vs [${layerKey(ref)}])`); + throw new Error( + `stackLods: available-layer mismatch between LOD levels ([${layerKey(s.meta)}] vs [${layerKey(ref)}])` + ); } if (extraKey(s.meta) !== extraKey(ref)) { throw new Error('stackLods: extra-column mismatch between LOD levels'); @@ -60,13 +57,13 @@ const stackLods = (sources: ChunkSource[]): ChunkSource => { } } - const lodCounts = sources.map(s => s.meta.numGaussians); + const lodCounts = sources.map((s) => s.meta.numGaussians); const meta: ChunkSourceMetadata = { ...ref, numGaussians: lodCounts[0], numLods: sources.length, lodCounts, - numChunks: sources.map(s => s.meta.numChunks[0]) + numChunks: sources.map((s) => s.meta.numChunks[0]) }; const pick = (lod: number): ChunkSource => { @@ -87,7 +84,7 @@ const stackLods = (sources: ChunkSource[]): ChunkSource => { meta, read, close: async () => { - await Promise.all(sources.map(s => s.close())); + await Promise.all(sources.map((s) => s.close())); } }; }; diff --git a/src/lib/ops/stats.ts b/src/lib/ops/stats.ts index 2a1eadcf..a8bc2f06 100644 --- a/src/lib/ops/stats.ts +++ b/src/lib/ops/stats.ts @@ -1,11 +1,5 @@ -import { - type ChunkData, - type ChunkDataPool, - type ChunkLayer, - type ChunkSource, - type ChunkSourceMetadata, - SH_REST_COUNTS -} from '../chunk'; +import { SH_REST_COUNTS } from '../chunk'; +import type { ChunkData, ChunkDataPool, ChunkLayer, ChunkSource, ChunkSourceMetadata } from '../chunk'; /** * One-pass streaming statistics over a {@link ChunkSource}: exact @@ -173,7 +167,7 @@ class StatsAccumulator { } private insert(v: number, count: number): void { - const bin = Math.min(FINE_BINS - 1, Math.floor((v - this.lo) * FINE_BINS / (this.hi - this.lo))); + const bin = Math.min(FINE_BINS - 1, Math.floor(((v - this.lo) * FINE_BINS) / (this.hi - this.lo))); this.bins![bin] += count; } @@ -227,13 +221,22 @@ class StatsAccumulator { const c = this.bins[b]; if (c === 0) continue; const mid = this.lo + (b + 0.5) * width; - const idx = Math.min(NUM_BINS - 1, Math.max(0, Math.floor((mid - this.min) / range * NUM_BINS))); + const idx = Math.min(NUM_BINS - 1, Math.max(0, Math.floor(((mid - this.min) / range) * NUM_BINS))); out[idx] += c; } return out; } - finalize(): { min: number; max: number; median: number; mean: number; stdDev: number; nanCount: number; infCount: number; histogram: number[] } { + finalize(): { + min: number; + max: number; + median: number; + mean: number; + stdDev: number; + nanCount: number; + infCount: number; + histogram: number[]; + } { const empty = this.n === 0; return { min: empty ? NaN : round(this.min), @@ -260,8 +263,9 @@ const buildColumnPlans = (meta: ChunkSourceMetadata): ColumnPlan[] => { ['x', 'y', 'z'].forEach((name, elem) => plans.push({ name, layer: 'position', elem, uint: false })); } if (meta.availableLayers.has('geometric')) { - ['rot_0', 'rot_1', 'rot_2', 'rot_3', 'scale_0', 'scale_1', 'scale_2', 'opacity'] - .forEach((name, elem) => plans.push({ name, layer: 'geometric', elem, uint: false })); + ['rot_0', 'rot_1', 'rot_2', 'rot_3', 'scale_0', 'scale_1', 'scale_2', 'opacity'].forEach((name, elem) => + plans.push({ name, layer: 'geometric', elem, uint: false }) + ); } if (meta.availableLayers.has('color')) { ['f_dc_0', 'f_dc_1', 'f_dc_2'].forEach((name, elem) => plans.push({ name, layer: 'color', elem, uint: false })); @@ -270,12 +274,14 @@ const buildColumnPlans = (meta: ChunkSourceMetadata): ColumnPlan[] => { } } if (meta.availableLayers.has('other')) { - meta.extraColumns.forEach((e, elem) => plans.push({ name: e.name, layer: 'other', elem, uint: e.type === 'uint32' })); + meta.extraColumns.forEach((e, elem) => + plans.push({ name: e.name, layer: 'other', elem, uint: e.type === 'uint32' }) + ); } return plans; }; -type MutableReadRequest = { chunkIndex: number; lod: number } & { [L in ChunkLayer]?: ChunkData }; +type MutableReadRequest = { chunkIndex: number; lod: number } & Partial>; /** * Compute per-LOD, per-column statistics for a source in a single streaming @@ -290,7 +296,7 @@ type MutableReadRequest = { chunkIndex: number; lod: number } & { [L in ChunkLay const computeSourceStats = async (src: ChunkSource, pool: ChunkDataPool): Promise => { const { meta } = src; const plans = buildColumnPlans(meta); - const layers = [...new Set(plans.map(p => p.layer))]; + const layers = [...new Set(plans.map((p) => p.layer))]; const lods: LodStats[] = []; const hasFill = meta.availableLayers.has('position') && meta.availableLayers.has('geometric'); @@ -342,7 +348,7 @@ const computeSourceStats = async (src: ChunkSource, pool: ChunkDataPool): Promis // The smallest may be 0 (a -Infinity flat-splat scale), so // pick the top two explicitly rather than dividing it out. const lo = Math.min(s0, Math.min(s1, s2)); - const top2 = s0 === lo ? s1 * s2 : (s1 === lo ? s0 * s2 : s0 * s1); + const top2 = s0 === lo ? s1 * s2 : s1 === lo ? s0 * s2 : s0 * s1; const area = Math.PI * top2; areaAcc.add(area); if (!Number.isNaN(area)) totalArea += area; @@ -352,13 +358,13 @@ const computeSourceStats = async (src: ChunkSource, pool: ChunkDataPool): Promis for (const cd of acquired) cd.release(); } - const results = accs.map(a => a.finalize()); + const results = accs.map((a) => a.finalize()); let fillRatio: number | undefined; if (hasFill) { // Robust extents (p1-p99 per axis) so flyaway splats can't inflate // the cross-section and mask the fill. - const axis = (name: string): StatsAccumulator => accs[plans.findIndex(p => p.name === name)]; + const axis = (name: string): StatsAccumulator => accs[plans.findIndex((p) => p.name === name)]; const [dx, dy, dz] = ['x', 'y', 'z'].map((name) => { const acc = axis(name); return Math.max(0, acc.quantile(0.99) - acc.quantile(0.01)); @@ -371,22 +377,22 @@ const computeSourceStats = async (src: ChunkSource, pool: ChunkDataPool): Promis const medianArea = areaAcc.quantile(0.5); crossSection = Number.isFinite(medianArea) ? medianArea : 0; } - fillRatio = round(crossSection > 0 ? totalArea / crossSection : (totalArea > 0 ? Infinity : 0)); + fillRatio = round(crossSection > 0 ? totalArea / crossSection : totalArea > 0 ? Infinity : 0); } lods.push({ lod, numGaussians: lodCount, - columns: plans.map(p => p.name), + columns: plans.map((p) => p.name), data: { - min: results.map(r => r.min), - max: results.map(r => r.max), - median: results.map(r => r.median), - mean: results.map(r => r.mean), - stdDev: results.map(r => r.stdDev), - nanCount: results.map(r => r.nanCount), - infCount: results.map(r => r.infCount), - histogram: results.map(r => r.histogram) + min: results.map((r) => r.min), + max: results.map((r) => r.max), + median: results.map((r) => r.median), + mean: results.map((r) => r.mean), + stdDev: results.map((r) => r.stdDev), + nanCount: results.map((r) => r.nanCount), + infCount: results.map((r) => r.infCount), + histogram: results.map((r) => r.histogram) }, ...(fillRatio !== undefined ? { fillRatio } : {}) }); diff --git a/src/lib/process-source.ts b/src/lib/process-source.ts index 9cd1cd4e..f0690ba1 100644 --- a/src/lib/process-source.ts +++ b/src/lib/process-source.ts @@ -1,4 +1,4 @@ -import { type ChunkDataPool, type ChunkSource } from './chunk'; +import type { ChunkDataPool, ChunkSource } from './chunk'; import { dataTableToChunkSource, materializeToDataTable } from './compat/data-table'; import { computeSourceStats, @@ -12,7 +12,8 @@ import { permuteSource, reduceBandsSource } from './ops'; -import { processDataTable, type ProcessAction, type ProcessOptions } from './process'; +import { processDataTable } from './process'; +import type { ProcessAction, ProcessOptions } from './process'; import { formatSourceInfo, formatSourceStats } from './source-info'; import { logger, Transform } from './utils'; @@ -22,10 +23,18 @@ import { logger, Transform } from './utils'; * {@link processSourceBridged} as a `processDataTable` island. */ const SOURCE_ACTION_KINDS: ReadonlySet = new Set([ - 'translate', 'rotate', 'scale', - 'filterNaN', 'filterByValue', 'filterBox', 'filterSphere', 'filterBands', + 'translate', + 'rotate', + 'scale', + 'filterNaN', + 'filterByValue', + 'filterBox', + 'filterSphere', + 'filterBands', 'mortonOrder', - 'stats', 'info', 'param' + 'stats', + 'info', + 'param' ]); /** @@ -98,7 +107,14 @@ const processSource = async ( // source's current, unbaked values — matching processDataTable // for every ordering except a transform-baking filterByValue // immediately followed by stats (a rare case). - logger.output(formatSourceStats(src.meta, await computeSourceStats(src, pool), action.format, options?.sourceFormat)); + logger.output( + formatSourceStats( + src.meta, + await computeSourceStats(src, pool), + action.format, + options?.sourceFormat + ) + ); break; case 'info': // Structural metadata only (meta-level) — no materialization; the diff --git a/src/lib/process.ts b/src/lib/process.ts index 1c25c63b..88b58450 100644 --- a/src/lib/process.ts +++ b/src/lib/process.ts @@ -5,10 +5,11 @@ import { dataTableToChunkSource, materializeToDataTable } from './compat/data-ta import { Column, DataTable, sortMortonOrder, convertToSpace, getSHBands } from './data-table'; import { decimateSource } from './decimate'; import { computeSourceStats } from './ops'; -import { type InputFormat } from './read'; +import type { InputFormat } from './read'; import { formatSourceInfo, formatSourceStats } from './source-info'; import type { DeviceCreator } from './types'; -import { fmtCount, type Group, logger, Transform } from './utils'; +import { fmtCount, logger, Transform } from './utils'; +import type { Group } from './utils'; import { inverseTransforms, rawColumnMap, isTransformColumn } from './value-transforms'; import { filterCluster as filterClusterFn } from './voxel/filter-cluster'; import { filterFloaters as filterFloatersFn } from './voxel/filter-floaters'; @@ -235,7 +236,22 @@ type ProcessOptions = { * - `mortonOrder` - Reorder splats by Morton code for spatial locality * - `decimate` - Simplify to target count via progressive pairwise merging */ -type ProcessAction = Translate | Rotate | Scale | FilterNaN | FilterByValue | FilterBands | FilterBox | FilterSphere | FilterFloaters | FilterCluster | Param | Stats | Info | MortonOrder | Decimate; +type ProcessAction = + | Translate + | Rotate + | Scale + | FilterNaN + | FilterByValue + | FilterBands + | FilterBox + | FilterSphere + | FilterFloaters + | FilterCluster + | Param + | Stats + | Info + | MortonOrder + | Decimate; // Describe a delta as "removed N" / "added N" relative to the previous count. const describeDelta = (delta: number, noun: string): string => { @@ -295,7 +311,11 @@ const filter = (dataTable: DataTable, predicate: (row: any, rowIndex: number) => * ]); * ``` */ -const processDataTable = async (dataTable: DataTable, processActions: ProcessAction[], options?: ProcessOptions): Promise => { +const processDataTable = async ( + dataTable: DataTable, + processActions: ProcessAction[], + options?: ProcessOptions +): Promise => { let result = dataTable; for (let i = 0; i < processActions.length; i++) { @@ -306,11 +326,9 @@ const processDataTable = async (dataTable: DataTable, processActions: ProcessAct result.transform = new Transform(processAction.value).mul(result.transform); break; case 'rotate': - result.transform = new Transform().fromEulers( - processAction.value.x, - processAction.value.y, - processAction.value.z - ).mul(result.transform); + result.transform = new Transform() + .fromEulers(processAction.value.x, processAction.value.y, processAction.value.z) + .mul(result.transform); break; case 'scale': result.transform = new Transform(undefined, undefined, processAction.value).mul(result.transform); @@ -321,7 +339,7 @@ const processDataTable = async (dataTable: DataTable, processActions: ProcessAct const infOk = new Set(['opacity']); const negInfOk = new Set(['scale_0', 'scale_1', 'scale_2']); const columnNames = result.columnNames; - const hasRotation = ['rot_0', 'rot_1', 'rot_2', 'rot_3'].every(c => columnNames.includes(c)); + const hasRotation = ['rot_0', 'rot_1', 'rot_2', 'rot_3'].every((c) => columnNames.includes(c)); const predicate = (row: any) => { // a zero-norm rotation quaternion cannot be normalized, so the @@ -354,7 +372,9 @@ const processDataTable = async (dataTable: DataTable, processActions: ProcessAct columnName = rawColumnMap[columnName]; } else if (inverseTransforms[columnName]) { if (columnName === 'opacity' && (value <= 0 || value >= 1)) { - throw new Error(`filterByValue: opacity value must be between 0 and 1 (exclusive), got ${value}`); + throw new Error( + `filterByValue: opacity value must be between 0 and 1 (exclusive), got ${value}` + ); } value = inverseTransforms[columnName](value); } @@ -368,16 +388,18 @@ const processDataTable = async (dataTable: DataTable, processActions: ProcessAct } const Predicates = { - 'lt': (row: any) => row[columnName] < value, - 'lte': (row: any) => row[columnName] <= value, - 'gt': (row: any) => row[columnName] > value, - 'gte': (row: any) => row[columnName] >= value, - 'eq': (row: any) => row[columnName] === value, - 'neq': (row: any) => row[columnName] !== value + lt: (row: any) => row[columnName] < value, + lte: (row: any) => row[columnName] <= value, + gt: (row: any) => row[columnName] > value, + gte: (row: any) => row[columnName] >= value, + eq: (row: any) => row[columnName] === value, + neq: (row: any) => row[columnName] !== value }; const predicate = Predicates[comparator as keyof typeof Predicates]; if (!predicate) { - throw new Error(`filterByValue: unknown comparator '${comparator}', expected one of: ${Object.keys(Predicates).join(', ')}`); + throw new Error( + `filterByValue: unknown comparator '${comparator}', expected one of: ${Object.keys(Predicates).join(', ')}` + ); } result = filter(result, predicate); endFilterGroup(g, prev, result); @@ -402,14 +424,18 @@ const processDataTable = async (dataTable: DataTable, processActions: ProcessAct } } - result = new DataTable(result.columns.map((column) => { - if (map.hasOwnProperty(column.name)) { - const name = map[column.name]; - return name ? new Column(name, column.data) : null; - } - return column; - - }).filter(c => c !== null), result.transform); + result = new DataTable( + result.columns + .map((column) => { + if (Object.hasOwn(map, column.name)) { + const name = map[column.name]; + return name ? new Column(name, column.data) : null; + } + return column; + }) + .filter((c) => c !== null), + result.transform + ); } endFilterGroup(g, prev, result); break; @@ -422,9 +448,7 @@ const processDataTable = async (dataTable: DataTable, processActions: ProcessAct if (result.transform.isIdentity()) { const predicate = (row: any) => { const { x, y, z } = row; - return x >= min.x && x <= max.x && - y >= min.y && y <= max.y && - z >= min.z && z <= max.z; + return x >= min.x && x <= max.x && y >= min.y && y <= max.y && z >= min.z && z <= max.z; }; result = filter(result, predicate); } else { @@ -515,7 +539,7 @@ const processDataTable = async (dataTable: DataTable, processActions: ProcessAct if (processAction.count !== null) { keepCount = Math.min(processAction.count, result.numRows); } else { - keepCount = Math.round(result.numRows * (processAction.percent ?? 100) / 100); + keepCount = Math.round((result.numRows * (processAction.percent ?? 100)) / 100); } keepCount = Math.max(0, keepCount); @@ -573,7 +597,6 @@ const processDataTable = async (dataTable: DataTable, processActions: ProcessAct break; } } - } return result; diff --git a/src/lib/read.ts b/src/lib/read.ts index 3db63df3..e06644c7 100644 --- a/src/lib/read.ts +++ b/src/lib/read.ts @@ -1,11 +1,13 @@ -import { type ChunkLayer, type ChunkSource, type ChunkSourceMetadata, type ExtraColumn, type SHBands, createChunkDataPool, hasGaussianLayers, orderedLayers } from './chunk'; +import { createChunkDataPool, hasGaussianLayers, orderedLayers } from './chunk'; +import type { ChunkLayer, ChunkSource, ChunkSourceMetadata, ExtraColumn, SHBands } from './chunk'; import { dataTableToChunkSource } from './compat/data-table'; -import { ReadFileSystem, ZipReadFileSystem } from './io/read'; +import type { ReadFileSystem } from './io/read'; +import { ZipReadFileSystem } from './io/read'; import { readKsplat, readMjs, readPly, readSogSource, readSplat, readSpz, statSogSource } from './readers'; import { readLccSource } from './readers/read-lcc'; import { readLcc2Source } from './readers/read-lcc2'; import { readLodSource } from './readers/read-lod'; -import { Options, Param } from './types'; +import type { Options, Param } from './types'; /** * Supported input file formats for Gaussian splat data. @@ -222,8 +224,10 @@ type FileInfo = { // The subset of ChunkSourceMetadata a FileInfo is built from — satisfied by a // full ChunkSource.meta as well as the header-only SOG/PLY peeks. -type MetaSummary = Pick; +type MetaSummary = Pick< + ChunkSourceMetadata, + 'numGaussians' | 'numLods' | 'lodCounts' | 'shBands' | 'availableLayers' | 'extraColumns' +>; const buildFileInfo = (format: InputFormat, meta: MetaSummary): FileInfo => ({ format, diff --git a/src/lib/readers/container-source.ts b/src/lib/readers/container-source.ts index a77faa46..d65a0f0d 100644 --- a/src/lib/readers/container-source.ts +++ b/src/lib/readers/container-source.ts @@ -1,11 +1,6 @@ -import { - type ChunkDataPool, - type ReadRequest, - type ChunkSource, - type ChunkSourceMetadata -} from '../chunk'; +import type { ChunkDataPool, ReadRequest, ChunkSource, ChunkSourceMetadata } from '../chunk'; import { concatSource } from '../ops'; -import { type Transform } from '../utils'; +import type { Transform } from '../utils'; /** * One contiguous run of gaussians within a LOD, backed by a sub-file that is @@ -61,12 +56,13 @@ const containerSource = async ( const evicted = order.shift()!; const ep = cache.get(evicted)!; cache.delete(evicted); - ep.then(s => s.close()).catch(() => {}); + // eslint-disable-next-line @typescript-eslint/no-empty-function -- preserve rejection-only control flow + ep.then((s) => s.close()).catch(() => {}); } return p; }; - const firstSeg = segmentsByLod.find(segs => segs.length > 0)?.[0]; + const firstSeg = segmentsByLod.find((segs) => segs.length > 0)?.[0]; if (!firstSeg) { throw new Error('containerSource: no segments'); } @@ -76,7 +72,7 @@ const containerSource = async ( const layout = firstSrc.meta; const { chunkSize } = layout; - const lodCounts = segmentsByLod.map(segs => segs.reduce((acc, s) => acc + s.count, 0)); + const lodCounts = segmentsByLod.map((segs) => segs.reduce((acc, s) => acc + s.count, 0)); // Static-meta proxy: concatSource reads its `meta` to lay out the LOD up // front; the actual sub-source is decoded (and cached) only on `read`, then @@ -108,7 +104,7 @@ const containerSource = async ( close: () => Promise.resolve() }); - const perLod = segmentsByLod.map(segs => (segs.length > 0 ? concatSource(segs.map(proxy), pool) : null)); + const perLod = segmentsByLod.map((segs) => (segs.length > 0 ? concatSource(segs.map(proxy), pool) : null)); const meta: ChunkSourceMetadata = { ...layout, @@ -116,7 +112,7 @@ const containerSource = async ( numGaussians: lodCounts[0] ?? 0, numLods: segmentsByLod.length, lodCounts, - numChunks: lodCounts.map(c => Math.ceil(c / chunkSize)) + numChunks: lodCounts.map((c) => Math.ceil(c / chunkSize)) }; // Dispatch to the requested LOD's concatSource (which buckets by sub-file and @@ -131,7 +127,8 @@ const containerSource = async ( }; const close = async (): Promise => { - await Promise.all([...cache.values()].map(p => p.then(s => s.close()).catch(() => {}))); + // eslint-disable-next-line @typescript-eslint/no-empty-function -- preserve rejection-only control flow + await Promise.all([...cache.values()].map((p) => p.then((s) => s.close()).catch(() => {}))); cache.clear(); order.length = 0; }; diff --git a/src/lib/readers/decompress-ply.ts b/src/lib/readers/decompress-ply.ts index 5321f878..48fa52ff 100644 --- a/src/lib/readers/decompress-ply.ts +++ b/src/lib/readers/decompress-ply.ts @@ -1,6 +1,7 @@ -import type { PlyData } from './read-ply'; import { Column, DataTable } from '../data-table'; +import type { PlyData } from './read-ply'; + // Size of a chunk in the compressed PLY format (number of splats per chunk) const CHUNK_SIZE = 256; @@ -27,26 +28,14 @@ const isCompressedPly = (ply: PlyData): boolean => { 'max_scale_z' ]; - const colorChunkProperties = [ - 'min_r', - 'min_g', - 'min_b', - 'max_r', - 'max_g', - 'max_b' - ]; + const colorChunkProperties = ['min_r', 'min_g', 'min_b', 'max_r', 'max_g', 'max_b']; - const vertexProperties = [ - 'packed_position', - 'packed_rotation', - 'packed_scale', - 'packed_color' - ]; + const vertexProperties = ['packed_position', 'packed_rotation', 'packed_scale', 'packed_color']; const numElements = ply.elements.length; if (numElements !== 2 && numElements !== 3) return false; - const chunk = ply.elements.find(e => e.name === 'chunk'); + const chunk = ply.elements.find((e) => e.name === 'chunk'); if (!chunk || !hasShape(chunk.dataTable, requiredChunkProperties, 'float32')) return false; // accept either 12 (no per-chunk color) or 18 (with per-chunk color) chunk properties @@ -57,7 +46,7 @@ const isCompressedPly = (ply: PlyData): boolean => { return false; } - const vertex = ply.elements.find(e => e.name === 'vertex'); + const vertex = ply.elements.find((e) => e.name === 'vertex'); if (!vertex || !hasShape(vertex.dataTable, vertexProperties, 'uint32')) return false; if (Math.ceil(vertex.dataTable.numRows / CHUNK_SIZE) !== chunk.dataTable.numRows) { @@ -66,7 +55,7 @@ const isCompressedPly = (ply: PlyData): boolean => { // check optional spherical harmonics if (numElements === 3) { - const sh = ply.elements.find(e => e.name === 'sh'); + const sh = ply.elements.find((e) => e.name === 'sh'); if (!sh) { return false; } @@ -91,10 +80,10 @@ const isCompressedPly = (ply: PlyData): boolean => { // Detects the compressed PLY schema and returns a decompressed DataTable, or null if not compressed. const decompressPly = (ply: PlyData): DataTable => { - const chunkData = ply.elements.find(e => e.name === 'chunk').dataTable; + const chunkData = ply.elements.find((e) => e.name === 'chunk').dataTable; const getChunk = (name: string) => chunkData.getColumnByName(name)!.data as Float32Array; - const vertexData = ply.elements.find(e => e.name === 'vertex').dataTable; + const vertexData = ply.elements.find((e) => e.name === 'vertex').dataTable; const packed_position = vertexData.getColumnByName('packed_position')!.data as Uint32Array; const packed_rotation = vertexData.getColumnByName('packed_rotation')!.data as Uint32Array; const packed_scale = vertexData.getColumnByName('packed_scale')!.data as Uint32Array; @@ -225,7 +214,7 @@ const decompressPly = (ply: PlyData): DataTable => { } // extract spherical harmonics - const shElem = ply.elements.find(e => e.name === 'sh'); + const shElem = ply.elements.find((e) => e.name === 'sh'); if (shElem) { const shData = shElem.dataTable; for (let k = 0; k < shData.numColumns; ++k) { @@ -233,7 +222,7 @@ const decompressPly = (ply: PlyData): DataTable => { const src = col.data as Uint8Array; const dst = new Float32Array(numSplats); for (let i = 0; i < numSplats; ++i) { - const n = (src[i] === 0) ? 0 : (src[i] === 255) ? 1 : (src[i] + 0.5) / 256; + const n = src[i] === 0 ? 0 : src[i] === 255 ? 1 : (src[i] + 0.5) / 256; dst[i] = (n - 0.5) * 8; } result.addColumn(new Column(col.name, dst)); diff --git a/src/lib/readers/read-ksplat.ts b/src/lib/readers/read-ksplat.ts index 30188717..9d93ac98 100644 --- a/src/lib/readers/read-ksplat.ts +++ b/src/lib/readers/read-ksplat.ts @@ -1,11 +1,11 @@ import { Column, DataTable } from '../data-table'; -import { ReadSource } from '../io/read'; +import type { ReadSource } from '../io/read'; import { logger, Transform } from '../utils'; const TICK_BATCH = 1 << 16; // Format configuration for different compression modes -interface CompressionConfig { +type CompressionConfig = { centerBytes: number; scaleBytes: number; rotationBytes: number; @@ -16,7 +16,7 @@ interface CompressionConfig { colorStartByte: number; harmonicsStartByte: number; scaleQuantRange: number; -} +}; // Half-precision floating point decoder function decodeFloat16(encoded: number): number { @@ -144,7 +144,11 @@ const readKsplat = async (source: ReadSource): Promise => { let maxHarmonicsDegree = 0; for (let sectionIdx = 0; sectionIdx < maxSections; sectionIdx++) { const sectionHeaderOffset = MAIN_HEADER_SIZE + sectionIdx * SECTION_HEADER_SIZE; - const sectionHeader = new DataView(fileBuffer.buffer, fileBuffer.byteOffset + sectionHeaderOffset, SECTION_HEADER_SIZE); + const sectionHeader = new DataView( + fileBuffer.buffer, + fileBuffer.byteOffset + sectionHeaderOffset, + SECTION_HEADER_SIZE + ); const sectionSplatCount = sectionHeader.getUint32(0, true); if (sectionSplatCount === 0) continue; // Skip empty sections @@ -198,7 +202,11 @@ const readKsplat = async (source: ReadSource): Promise => { // Process each section for (let sectionIdx = 0; sectionIdx < maxSections; sectionIdx++) { const sectionHeaderOffset = MAIN_HEADER_SIZE + sectionIdx * SECTION_HEADER_SIZE; - const sectionHeader = new DataView(fileBuffer.buffer, fileBuffer.byteOffset + sectionHeaderOffset, SECTION_HEADER_SIZE); + const sectionHeader = new DataView( + fileBuffer.buffer, + fileBuffer.byteOffset + sectionHeaderOffset, + SECTION_HEADER_SIZE + ); const sectionSplatCount = sectionHeader.getUint32(0, true); const maxSectionSplats = sectionHeader.getUint32(4, true); @@ -216,8 +224,8 @@ const readKsplat = async (source: ReadSource): Promise => { const partialBucketMetaSize = partialBuckets * 4; const totalBucketStorageSize = bucketStorageSize * bucketCount + partialBucketMetaSize; const harmonicsComponentCount = HARMONICS_COMPONENT_COUNT[harmonicsDegree]; - const bytesPerSplat = centerBytes + scaleBytes + rotationBytes + - colorBytes + harmonicsComponentCount * harmonicsBytes; + const bytesPerSplat = + centerBytes + scaleBytes + rotationBytes + colorBytes + harmonicsComponentCount * harmonicsBytes; const sectionDataSize = bytesPerSplat * maxSectionSplats; // Calculate decompression parameters @@ -225,10 +233,18 @@ const readKsplat = async (source: ReadSource): Promise => { // Get bucket centers const bucketCentersOffset = currentSectionDataOffset + partialBucketMetaSize; - const bucketCenters = new Float32Array(fileBuffer.buffer, fileBuffer.byteOffset + bucketCentersOffset, bucketCount * 3); + const bucketCenters = new Float32Array( + fileBuffer.buffer, + fileBuffer.byteOffset + bucketCentersOffset, + bucketCount * 3 + ); // Get partial bucket sizes - const partialBucketSizes = new Uint32Array(fileBuffer.buffer, fileBuffer.byteOffset + currentSectionDataOffset, partialBuckets); + const partialBucketSizes = new Uint32Array( + fileBuffer.buffer, + fileBuffer.byteOffset + currentSectionDataOffset, + partialBuckets + ); // Get splat data const splatDataOffset = currentSectionDataOffset + totalBucketStorageSize; @@ -278,9 +294,15 @@ const readKsplat = async (source: ReadSource): Promise => { y = splatData.getFloat32(splatByteOffset + 4, true); z = splatData.getFloat32(splatByteOffset + 8, true); } else { - x = (splatData.getUint16(splatByteOffset, true) - quantizationRange) * positionScale + bucketCenters[bucketIdx * 3]; - y = (splatData.getUint16(splatByteOffset + 2, true) - quantizationRange) * positionScale + bucketCenters[bucketIdx * 3 + 1]; - z = (splatData.getUint16(splatByteOffset + 4, true) - quantizationRange) * positionScale + bucketCenters[bucketIdx * 3 + 2]; + x = + (splatData.getUint16(splatByteOffset, true) - quantizationRange) * positionScale + + bucketCenters[bucketIdx * 3]; + y = + (splatData.getUint16(splatByteOffset + 2, true) - quantizationRange) * positionScale + + bucketCenters[bucketIdx * 3 + 1]; + z = + (splatData.getUint16(splatByteOffset + 4, true) - quantizationRange) * positionScale + + bucketCenters[bucketIdx * 3 + 2]; } // Decode scales @@ -353,11 +375,11 @@ const readKsplat = async (source: ReadSource): Promise => { coeff = i % 3; } else if (i < 24) { channel = Math.floor((i - 9) / 5); - coeff = (i - 9) % 5 + 3; + coeff = ((i - 9) % 5) + 3; } else { // don't think 3 bands are supported, but here just in case channel = Math.floor((i - 24) / 7); - coeff = (i - 24) % 7 + 8; + coeff = ((i - 24) % 7) + 8; } const col = channel * (harmonicsComponentCount / 3) + coeff; diff --git a/src/lib/readers/read-lcc.ts b/src/lib/readers/read-lcc.ts index c16e5a81..68b53e7e 100644 --- a/src/lib/readers/read-lcc.ts +++ b/src/lib/readers/read-lcc.ts @@ -1,16 +1,6 @@ import { Vec3 } from 'playcanvas'; -import { readExact, sortGatherSlots, gatherRuns } from './reader-utils'; import { - type ChunkData, - type ChunkDataPool, - type ChunkLayer, - type ReadRequest, - type ChunkSource, - type ChunkSourceMetadata, - type ExtraColumn, - type LayerLayout, - type SHBands, POSITION_STRIDE, GEOMETRIC_STRIDE, colorStride, @@ -19,12 +9,26 @@ import { colorFields, otherLayout } from '../chunk'; +import type { + ChunkData, + ChunkDataPool, + ChunkLayer, + ReadRequest, + ChunkSource, + ChunkSourceMetadata, + ExtraColumn, + LayerLayout, + SHBands +} from '../chunk'; import { dataTableToChunkSource } from '../compat/data-table'; import { Column, DataTable } from '../data-table'; -import { dirname, join, ReadFileSystem, ReadSource, readFile } from '../io/read'; -import { Options } from '../types'; +import type { ReadFileSystem, ReadSource } from '../io/read'; +import { dirname, join, readFile } from '../io/read'; +import type { Options } from '../types'; import { logger, Transform } from '../utils'; +import { readExact, sortGatherSlots, gatherRuns } from './reader-utils'; + const kSH_C0 = 0.28209479177387814; const SQRT_2 = 1.414213562373095; const SQRT_2_INV = 0.7071067811865475; @@ -33,34 +37,34 @@ const IO_CONCURRENCY = 16; // lod data in data.bin type LccLod = { - points: number; // number of splats - offset: bigint; // offset - size: number; // data size -} + points: number; // number of splats + offset: bigint; // offset + size: number; // data size +}; // The scene uses a quadtree for spatial partitioning, // with each unit having its own xy index (starting from 0) and multiple layers of lod data type LccUnitInfo = { - x: number; // x index - y: number; // y index - lods: Array; // lods -} + x: number; // x index + y: number; // y index + lods: LccLod[]; // lods +}; // Used to decompress scale in data.bin and sh in shcoef.bin type CompressInfo = { - scaleMin: Vec3; // min scale - scaleMax: Vec3; // max scale - shMin: Vec3; // min sh - shMax: Vec3; // max sh - envScaleMin: Vec3; // min environment scale - envScaleMax: Vec3; // max environment scale - envShMin: Vec3; // min environment sh - envShMax: Vec3; // max environment sh -} + scaleMin: Vec3; // min scale + scaleMax: Vec3; // max scale + shMin: Vec3; // min sh + shMax: Vec3; // max sh + envScaleMin: Vec3; // min environment scale + envScaleMax: Vec3; // max environment scale + envShMin: Vec3; // min environment sh + envShMax: Vec3; // max environment sh +}; // parse .lcc files, such as meta.lcc const parseMeta = (obj: any): CompressInfo => { - const attributes: { [key: string]: any } = {}; + const attributes: Record = {}; obj.attributes.forEach((attr: any) => { attributes[attr.name] = attr; }); @@ -77,11 +81,11 @@ const parseMeta = (obj: any): CompressInfo => { return { scaleMin, scaleMax, shMin, shMax, envScaleMin, envScaleMax, envShMin, envShMax }; }; -const parseIndexBin = (raw: ArrayBuffer, meta: any): Array => { +const parseIndexBin = (raw: ArrayBuffer, meta: any): LccUnitInfo[] => { let offset = 0; const buff = new DataView(raw); - const infos: Array = []; + const infos: LccUnitInfo[] = []; while (true) { if (offset > buff.byteLength - 1) { break; @@ -92,7 +96,7 @@ const parseIndexBin = (raw: ArrayBuffer, meta: any): Array => { const y = buff.getInt16(offset, true); offset += 2; - const lods: Array = []; + const lods: LccLod[] = []; for (let i = 0; i < meta.totalLevel; i++) { const ldPoints = buff.getInt32(offset, true); offset += 4; @@ -108,7 +112,6 @@ const parseIndexBin = (raw: ArrayBuffer, meta: any): Array => { offset: ldOffset, size: ldSize }); - } const info: LccUnitInfo = { x, @@ -139,12 +142,23 @@ const mix = (min: number, max: number, s: number): number => { }; const floatProps = [ - 'x', 'y', 'z', - 'nx', 'ny', 'nz', + 'x', + 'y', + 'z', + 'nx', + 'ny', + 'nz', 'opacity', - 'rot_0', 'rot_1', 'rot_2', 'rot_3', - 'f_dc_0', 'f_dc_1', 'f_dc_2', - 'scale_0', 'scale_1', 'scale_2' + 'rot_0', + 'rot_1', + 'rot_2', + 'rot_3', + 'f_dc_0', + 'f_dc_1', + 'f_dc_2', + 'scale_0', + 'scale_1', + 'scale_2' ]; const initProperties = (length: number): Record => { @@ -160,7 +174,10 @@ const initProperties = (length: number): Record => { // indicating which component was omitted (the largest, which is reconstructed). const decodeRotationInto = ( v: number, - rot0: Float32Array, rot1: Float32Array, rot2: Float32Array, rot3: Float32Array, + rot0: Float32Array, + rot1: Float32Array, + rot2: Float32Array, + rot3: Float32Array, idx: number ) => { const d0 = (v & 1023) / 1023.0; @@ -176,13 +193,25 @@ const decodeRotationInto = ( // Reconstruct full quaternion with qw inserted at position d3. // Output mapping matches original: rot_0 = q[3], rot_1 = q[0], rot_2 = q[1], rot_3 = q[2] if (d3 === 0) { - rot0[idx] = qz; rot1[idx] = qw; rot2[idx] = qx; rot3[idx] = qy; + rot0[idx] = qz; + rot1[idx] = qw; + rot2[idx] = qx; + rot3[idx] = qy; } else if (d3 === 1) { - rot0[idx] = qz; rot1[idx] = qx; rot2[idx] = qw; rot3[idx] = qy; + rot0[idx] = qz; + rot1[idx] = qx; + rot2[idx] = qw; + rot3[idx] = qy; } else if (d3 === 2) { - rot0[idx] = qz; rot1[idx] = qx; rot2[idx] = qy; rot3[idx] = qw; + rot0[idx] = qz; + rot1[idx] = qx; + rot2[idx] = qy; + rot3[idx] = qw; } else { - rot0[idx] = qw; rot1[idx] = qx; rot2[idx] = qy; rot3[idx] = qz; + rot0[idx] = qw; + rot1[idx] = qx; + rot2[idx] = qy; + rot3[idx] = qz; } }; @@ -213,7 +242,9 @@ const processUnit = async ( const dataBytes = await dataSource.read(offset, offset + size).readAll(); const expectedDataSize = unitSplats * 32; if (dataBytes.byteLength < expectedDataSize) { - throw new Error(`LCC unit data too short: expected ${expectedDataSize} bytes for ${unitSplats} splats, got ${dataBytes.byteLength}`); + throw new Error( + `LCC unit data too short: expected ${expectedDataSize} bytes for ${unitSplats} splats, got ${dataBytes.byteLength}` + ); } // Typed array views over the same buffer -- avoids DataView overhead. @@ -229,7 +260,9 @@ const processUnit = async ( const shBytes = await shSource.read(offset * 2, offset * 2 + size * 2).readAll(); const expectedShSize = unitSplats * 64; if (shBytes.byteLength < expectedShSize) { - throw new Error(`LCC unit SH data too short: expected ${expectedShSize} bytes for ${unitSplats} splats, got ${shBytes.byteLength}`); + throw new Error( + `LCC unit SH data too short: expected ${expectedShSize} bytes for ${unitSplats} splats, got ${shBytes.byteLength}` + ); } shU32 = new Uint32Array(shBytes.buffer, shBytes.byteOffset, shBytes.byteLength >> 2); } @@ -253,10 +286,18 @@ const processUnit = async ( const ps1 = properties.property_scale_1; const ps2 = properties.property_scale_2; - const sMinX = compressInfo.scaleMin.x, sMinY = compressInfo.scaleMin.y, sMinZ = compressInfo.scaleMin.z; - const sMaxX = compressInfo.scaleMax.x, sMaxY = compressInfo.scaleMax.y, sMaxZ = compressInfo.scaleMax.z; - const shMinX = compressInfo.shMin.x, shMinY = compressInfo.shMin.y, shMinZ = compressInfo.shMin.z; - const shMaxX = compressInfo.shMax.x, shMaxY = compressInfo.shMax.y, shMaxZ = compressInfo.shMax.z; + const sMinX = compressInfo.scaleMin.x, + sMinY = compressInfo.scaleMin.y, + sMinZ = compressInfo.scaleMin.z; + const sMaxX = compressInfo.scaleMax.x, + sMaxY = compressInfo.scaleMax.y, + sMaxZ = compressInfo.scaleMax.z; + const shMinX = compressInfo.shMin.x, + shMinY = compressInfo.shMin.y, + shMinZ = compressInfo.shMin.z; + const shMaxX = compressInfo.shMax.x, + shMaxY = compressInfo.shMax.y, + shMaxZ = compressInfo.shMax.z; for (let i = 0; i < unitSplats; i++) { const g = propertyOffset + i; @@ -292,9 +333,9 @@ const processUnit = async ( const si = i << 4; for (let j = 0; j < 15; j++) { const enc = shU32[si + j]; - properties_f_rest[j][g] = mix(shMinX, shMaxX, (enc & 0x7FF) / 2047.0); - properties_f_rest[j + 15][g] = mix(shMinY, shMaxY, ((enc >> 11) & 0x3FF) / 1023.0); - properties_f_rest[j + 30][g] = mix(shMinZ, shMaxZ, ((enc >> 21) & 0x7FF) / 2047.0); + properties_f_rest[j][g] = mix(shMinX, shMaxX, (enc & 0x7ff) / 2047.0); + properties_f_rest[j + 15][g] = mix(shMinY, shMaxY, ((enc >> 11) & 0x3ff) / 1023.0); + properties_f_rest[j + 30][g] = mix(shMinZ, shMaxZ, ((enc >> 21) & 0x7ff) / 2047.0); } } } @@ -328,15 +369,19 @@ const decodeUnitsForLod = async ( const idx = nextUnit++; if (idx >= unitInfos.length) break; await processUnit( - unitInfos[idx], targetLod, dataSource, shSource, - compressInfo, offsets[idx], properties, properties_f_rest, + unitInfos[idx], + targetLod, + dataSource, + shSource, + compressInfo, + offsets[idx], + properties, + properties_f_rest, onUnitDone ); } }; - await Promise.all( - Array.from({ length: Math.min(IO_CONCURRENCY, unitInfos.length) }, () => worker()) - ); + await Promise.all(Array.from({ length: Math.min(IO_CONCURRENCY, unitInfos.length) }, () => worker())); }; const deserializeEnvironment = (raw: Uint8Array, compressInfo: CompressInfo, hasSH: boolean) => { @@ -349,11 +394,23 @@ const deserializeEnvironment = (raw: Uint8Array, compressInfo: CompressInfo, has } const columns = [ - 'x', 'y', 'z', - 'f_dc_0', 'f_dc_1', 'f_dc_2', 'opacity', - 'scale_0', 'scale_1', 'scale_2', - 'rot_0', 'rot_1', 'rot_2', 'rot_3' - ].concat(hasSH ? new Array(45).fill('').map((_, i) => `f_rest_${i}`) : []).map(name => new Column(name, new Float32Array(numGaussians))); + 'x', + 'y', + 'z', + 'f_dc_0', + 'f_dc_1', + 'f_dc_2', + 'opacity', + 'scale_0', + 'scale_1', + 'scale_2', + 'rot_0', + 'rot_1', + 'rot_2', + 'rot_3' + ] + .concat(hasSH ? new Array(45).fill('').map((_, i) => `f_rest_${i}`) : []) + .map((name) => new Column(name, new Float32Array(numGaussians))); const scaleMin = compressInfo.envScaleMin; const scaleMax = compressInfo.envScaleMax; @@ -370,14 +427,14 @@ const deserializeEnvironment = (raw: Uint8Array, compressInfo: CompressInfo, has for (let i = 0; i < numGaussians; i++) { const off = i * stride; - columns[0].data[i] = dataView.getFloat32(off + 0, true); // x - columns[1].data[i] = dataView.getFloat32(off + 4, true); // y - columns[2].data[i] = dataView.getFloat32(off + 8, true); // z + columns[0].data[i] = dataView.getFloat32(off + 0, true); // x + columns[1].data[i] = dataView.getFloat32(off + 4, true); // y + columns[2].data[i] = dataView.getFloat32(off + 8, true); // z - columns[3].data[i] = invSH0ToColor(dataView.getUint8(off + 12) / 255.0); // f_dc_0 - columns[4].data[i] = invSH0ToColor(dataView.getUint8(off + 13) / 255.0); // f_dc_1 - columns[5].data[i] = invSH0ToColor(dataView.getUint8(off + 14) / 255.0); // f_dc_2 - columns[6].data[i] = invSigmoid(dataView.getUint8(off + 15) / 255.0); // opacity + columns[3].data[i] = invSH0ToColor(dataView.getUint8(off + 12) / 255.0); // f_dc_0 + columns[4].data[i] = invSH0ToColor(dataView.getUint8(off + 13) / 255.0); // f_dc_1 + columns[5].data[i] = invSH0ToColor(dataView.getUint8(off + 14) / 255.0); // f_dc_2 + columns[6].data[i] = invSigmoid(dataView.getUint8(off + 15) / 255.0); // opacity columns[7].data[i] = invLinearScale(mix(scaleMin.x, scaleMax.x, dataView.getUint16(off + 16, true) / 65535.0)); // scale_0 columns[8].data[i] = invLinearScale(mix(scaleMin.y, scaleMax.y, dataView.getUint16(off + 18, true) / 65535.0)); // scale_1 @@ -390,9 +447,9 @@ const deserializeEnvironment = (raw: Uint8Array, compressInfo: CompressInfo, has if (hasSH) { for (let j = 0; j < 15; ++j) { const enc = dataView.getUint32(off + 32 + j * 4, true); - const nx = (enc & 0x7FF) / 2047.0; - const ny = ((enc >> 11) & 0x3FF) / 1023.0; - const nz = ((enc >> 21) & 0x7FF) / 2047.0; + const nx = (enc & 0x7ff) / 2047.0; + const ny = ((enc >> 11) & 0x3ff) / 1023.0; + const nz = ((enc >> 21) & 0x7ff) / 2047.0; columns[14 + j].data[i] = mix(shMin.x, shMax.x, nx); columns[14 + j + 15].data[i] = mix(shMin.y, shMax.y, ny); columns[14 + j + 30].data[i] = mix(shMin.z, shMax.z, nz); @@ -465,15 +522,19 @@ const readLcc = async (fileSystem: ReadFileSystem, filename: string, options: Op dataSource = await openSource('data.bin'); if (hasSH) shSource = await openSource('shcoef.bin'); - const unitInfos: LccUnitInfo[] = parseIndexBin(indexData.buffer.slice(indexData.byteOffset, indexData.byteOffset + indexData.byteLength) as ArrayBuffer, lccJson); + const unitInfos: LccUnitInfo[] = parseIndexBin( + indexData.buffer.slice(indexData.byteOffset, indexData.byteOffset + indexData.byteLength) as ArrayBuffer, + lccJson + ); // build table of input -> output lods const lodSelect = options.lodSelect ?? []; - const lods = lodSelect.length > 0 ? - lodSelect - .map(lod => (lod < 0 ? splats.length + lod : lod)) // negative indices map from the end of lod - .filter(lod => lod >= 0 && lod < splats.length) : - new Array(splats.length).fill(0).map((_, i) => i); + const lods = + lodSelect.length > 0 + ? lodSelect + .map((lod) => (lod < 0 ? splats.length + lod : lod)) // negative indices map from the end of lod + .filter((lod) => lod >= 0 && lod < splats.length) + : new Array(splats.length).fill(0).map((_, i) => i); if (lods.length === 0) { throw new Error(`No valid LODs selected for LCC input file: ${filename} lods: ${JSON.stringify(lods)}`); @@ -502,8 +563,14 @@ const readLcc = async (fileSystem: ReadFileSystem, filename: string, options: Op const totalSplats = splats[inputLod]; await decodeUnitsForLod( - unitInfos, inputLod, dataSource, shSource ?? undefined, - compressInfo, lodOffset, properties, properties_f_rest, + unitInfos, + inputLod, + dataSource, + shSource ?? undefined, + compressInfo, + lodOffset, + properties, + properties_f_rest, tick ); @@ -512,7 +579,7 @@ const readLcc = async (fileSystem: ReadFileSystem, filename: string, options: Op } const columns = [ - ...floatProps.map(name => new Column(name, properties[`property_${name}`])), + ...floatProps.map((name) => new Column(name, properties[`property_${name}`])), ...(properties_f_rest ? properties_f_rest.map((storage, i) => new Column(`f_rest_${i}`, storage)) : []), new Column('lod', lodColumn) ]; @@ -544,9 +611,8 @@ const readLcc = async (fileSystem: ReadFileSystem, filename: string, options: Op } catch (err) { const code = (err as { code?: string })?.code; const message = (err as Error)?.message ?? ''; - const isMissing = code === 'ENOENT' || - message.startsWith('Entry not found') || - message.startsWith('HTTP error 404'); + const isMissing = + code === 'ENOENT' || message.startsWith('Entry not found') || message.startsWith('HTTP error 404'); if (!isMissing) { logger.warn(`failed to load environment.bin: ${message || err}`); } @@ -614,11 +680,12 @@ const readLccSource = async ( // input -> output LOD mapping (mirrors readLcc). const lodSelect = options.lodSelect ?? []; - const lods = lodSelect.length > 0 ? - lodSelect - .map(lod => (lod < 0 ? splats.length + lod : lod)) - .filter(lod => lod >= 0 && lod < splats.length) : - new Array(splats.length).fill(0).map((_, i) => i); + const lods = + lodSelect.length > 0 + ? lodSelect + .map((lod) => (lod < 0 ? splats.length + lod : lod)) + .filter((lod) => lod >= 0 && lod < splats.length) + : new Array(splats.length).fill(0).map((_, i) => i); if (lods.length === 0) { throw new Error(`No valid LODs selected for LCC input file: ${filename} lods: ${JSON.stringify(lods)}`); } @@ -682,7 +749,7 @@ const readLccSource = async ( numLods: lods.length, lodCounts, chunkSize, - numChunks: lodCounts.map(c => Math.ceil(c / chunkSize)), + numChunks: lodCounts.map((c) => Math.ceil(c / chunkSize)), shBands, model: 'default', // lcc carries no training-model tag extraColumns, @@ -692,15 +759,25 @@ const readLccSource = async ( }; // Global (scene-wide) dequant constants, hoisted once (see processUnit). - const sMinX = compressInfo.scaleMin.x, sMinY = compressInfo.scaleMin.y, sMinZ = compressInfo.scaleMin.z; - const sMaxX = compressInfo.scaleMax.x, sMaxY = compressInfo.scaleMax.y, sMaxZ = compressInfo.scaleMax.z; - const shMinX = compressInfo.shMin.x, shMinY = compressInfo.shMin.y, shMinZ = compressInfo.shMin.z; - const shMaxX = compressInfo.shMax.x, shMaxY = compressInfo.shMax.y, shMaxZ = compressInfo.shMax.z; + const sMinX = compressInfo.scaleMin.x, + sMinY = compressInfo.scaleMin.y, + sMinZ = compressInfo.scaleMin.z; + const sMaxX = compressInfo.scaleMax.x, + sMaxY = compressInfo.scaleMax.y, + sMaxZ = compressInfo.scaleMax.z; + const shMinX = compressInfo.shMin.x, + shMinY = compressInfo.shMin.y, + shMinZ = compressInfo.shMin.z; + const shMaxX = compressInfo.shMax.x, + shMaxY = compressInfo.shMax.y, + shMaxZ = compressInfo.shMax.z; const colorSw = 3 + restCount; // Largest run index r with starts[r] <= g. const findRun = (starts: number[], g: number): number => { - let lo = 0, hi = starts.length - 1, ans = 0; + let lo = 0, + hi = starts.length - 1, + ans = 0; while (lo <= hi) { const mid = (lo + hi) >> 1; if (starts[mid] <= g) { @@ -716,9 +793,7 @@ const readLccSource = async ( // Per-record dequantizer bound to the caller's output layer buffers. `srcIdx` // indexes the current source views (rebound per sub-block / gather run via // setViews); `dstRow` is the output gaussian row. Mirrors processUnit exactly. - const makeDecoder = ( - position?: ChunkData, geometric?: ChunkData, color?: ChunkData, other?: ChunkData - ) => { + const makeDecoder = (position?: ChunkData, geometric?: ChunkData, color?: ChunkData, other?: ChunkData) => { const pos = position ? new Float32Array(position.data) : null; const geo = geometric ? new Float32Array(geometric.data) : null; const col = color ? new Float32Array(color.data) : null; @@ -728,13 +803,18 @@ const readLccSource = async ( let u8!: Uint8Array; let shU32: Uint32Array | null = null; const setViews = (df: Float32Array, du16: Uint16Array, du8: Uint8Array, dsh: Uint32Array | null) => { - f32 = df; u16 = du16; u8 = du8; shU32 = dsh; + f32 = df; + u16 = du16; + u8 = du8; + shU32 = dsh; }; const decode = (srcIdx: number, dstRow: number) => { if (pos) { const fi = srcIdx << 3; const o = dstRow * 3; - pos[o] = f32[fi]; pos[o + 1] = f32[fi + 1]; pos[o + 2] = f32[fi + 2]; + pos[o] = f32[fi]; + pos[o + 1] = f32[fi + 1]; + pos[o + 2] = f32[fi + 2]; } if (geo) { const bi = srcIdx << 5; @@ -752,13 +832,25 @@ const readLccSource = async ( const qz = d2 * SQRT_2 - SQRT_2_INV; const qw = Math.sqrt(1 - Math.min(1.0, qx * qx + qy * qy + qz * qz)); if (d3 === 0) { - geo[o] = qz; geo[o + 1] = qw; geo[o + 2] = qx; geo[o + 3] = qy; + geo[o] = qz; + geo[o + 1] = qw; + geo[o + 2] = qx; + geo[o + 3] = qy; } else if (d3 === 1) { - geo[o] = qz; geo[o + 1] = qx; geo[o + 2] = qw; geo[o + 3] = qy; + geo[o] = qz; + geo[o + 1] = qx; + geo[o + 2] = qw; + geo[o + 3] = qy; } else if (d3 === 2) { - geo[o] = qz; geo[o + 1] = qx; geo[o + 2] = qy; geo[o + 3] = qw; + geo[o] = qz; + geo[o + 1] = qx; + geo[o + 2] = qy; + geo[o + 3] = qw; } else { - geo[o] = qw; geo[o + 1] = qx; geo[o + 2] = qy; geo[o + 3] = qz; + geo[o] = qw; + geo[o + 1] = qx; + geo[o + 2] = qy; + geo[o + 3] = qz; } geo[o + 4] = invLinearScale(mix(sMinX, sMaxX, u16[hi + 8] / 65535.0)); geo[o + 5] = invLinearScale(mix(sMinY, sMaxY, u16[hi + 9] / 65535.0)); @@ -775,16 +867,18 @@ const readLccSource = async ( const si = srcIdx << 4; for (let j = 0; j < 15; j++) { const enc = shU32[si + j]; - col[o + 3 + j] = mix(shMinX, shMaxX, (enc & 0x7FF) / 2047.0); - col[o + 3 + j + 15] = mix(shMinY, shMaxY, ((enc >> 11) & 0x3FF) / 1023.0); - col[o + 3 + j + 30] = mix(shMinZ, shMaxZ, ((enc >> 21) & 0x7FF) / 2047.0); + col[o + 3 + j] = mix(shMinX, shMaxX, (enc & 0x7ff) / 2047.0); + col[o + 3 + j + 15] = mix(shMinY, shMaxY, ((enc >> 11) & 0x3ff) / 1023.0); + col[o + 3 + j + 30] = mix(shMinZ, shMaxZ, ((enc >> 21) & 0x7ff) / 2047.0); } } } if (oth) { const hi = srcIdx << 4; const o = dstRow * 3; - oth[o] = u16[hi + 13]; oth[o + 1] = u16[hi + 14]; oth[o + 2] = u16[hi + 15]; + oth[o] = u16[hi + 13]; + oth[o + 1] = u16[hi + 14]; + oth[o + 2] = u16[hi + 15]; } }; return { setViews, decode, wantColor: !!col }; @@ -792,10 +886,10 @@ const readLccSource = async ( // Sub-block size: bounds the raw record scratch independent of chunkSize. const SUB_BLOCK = 1 << 16; - let dScratch: Uint8Array | null = null; // SUB_BLOCK * 32 (data records) - let sScratch: Uint8Array | null = null; // SUB_BLOCK * 64 (sh records) - let gScratch: Uint8Array | null = null; // readRows data gather - let gShScratch: Uint8Array | null = null; // readRows sh gather + let dScratch: Uint8Array | null = null; // SUB_BLOCK * 32 (data records) + let sScratch: Uint8Array | null = null; // SUB_BLOCK * 64 (sh records) + let gScratch: Uint8Array | null = null; // readRows data gather + let gShScratch: Uint8Array | null = null; // readRows sh gather const read = async (request: ReadRequest): Promise => { const lod = request.lod ?? 0; @@ -823,18 +917,18 @@ const readLccSource = async ( const run = runs[findRun(starts, g)]; boff[j] = run.dataByteOffset + (g - run.globalStart) * 32; } - const slot = sortGatherSlots(count, s => boff[s]); + const slot = sortGatherSlots(count, (s) => boff[s]); const wantSh = !!(shSource && dec.wantColor); const costBytes = 32 + (wantSh ? 64 : 0); - for (const gr of gatherRuns(count, t => boff[slot[t]], 32, costBytes)) { + for (const gr of gatherRuns(count, (t) => boff[slot[t]], 32, costBytes)) { const first = gr.firstByte; const rc = gr.recordCount; const need = rc * 32; if (!gScratch || gScratch.length < need) gScratch = new Uint8Array(need); const dataBytes = gScratch.subarray(0, need); - if (await readExact(dataSource.read(first, first + need), dataBytes, 0, need) !== need) { + if ((await readExact(dataSource.read(first, first + need), dataBytes, 0, need)) !== need) { throw new Error(`readLcc: short gather read at byte ${first}`); } let shU32: Uint32Array | null = null; @@ -843,7 +937,7 @@ const readLccSource = async ( const shStart = first * 2; if (!gShScratch || gShScratch.length < shNeed) gShScratch = new Uint8Array(shNeed); const shBytes = gShScratch.subarray(0, shNeed); - if (await readExact(shSource!.read(shStart, shStart + shNeed), shBytes, 0, shNeed) !== shNeed) { + if ((await readExact(shSource!.read(shStart, shStart + shNeed), shBytes, 0, shNeed)) !== shNeed) { throw new Error(`readLcc: short sh gather read at byte ${shStart}`); } shU32 = new Uint32Array(shBytes.buffer, shBytes.byteOffset, rc * 16); @@ -880,7 +974,10 @@ const readLccSource = async ( const dataStart = run.dataByteOffset + off * 32; dScratch ??= new Uint8Array(SUB_BLOCK * 32); const dataBytes = dScratch.subarray(0, bb * 32); - if (await readExact(dataSource.read(dataStart, dataStart + bb * 32), dataBytes, 0, bb * 32) !== bb * 32) { + if ( + (await readExact(dataSource.read(dataStart, dataStart + bb * 32), dataBytes, 0, bb * 32)) !== + bb * 32 + ) { throw new Error(`readLcc: short data read for chunk ${request.chunkIndex}`); } let shU32: Uint32Array | null = null; @@ -888,7 +985,7 @@ const readLccSource = async ( const shStart = run.dataByteOffset * 2 + off * 64; sScratch ??= new Uint8Array(SUB_BLOCK * 64); const shBytes = sScratch.subarray(0, bb * 64); - if (await readExact(shSource.read(shStart, shStart + bb * 64), shBytes, 0, bb * 64) !== bb * 64) { + if ((await readExact(shSource.read(shStart, shStart + bb * 64), shBytes, 0, bb * 64)) !== bb * 64) { throw new Error(`readLcc: short sh read for chunk ${request.chunkIndex}`); } shU32 = new Uint32Array(shBytes.buffer, shBytes.byteOffset, bb * 16); @@ -957,9 +1054,8 @@ const readLccEnvironmentSource = async ( // case (signalled differently per backend; mirror readLcc's suppression). const code = (err as { code?: string })?.code; const message = (err as Error)?.message ?? ''; - const isMissing = code === 'ENOENT' || - message.startsWith('Entry not found') || - message.startsWith('HTTP error 404'); + const isMissing = + code === 'ENOENT' || message.startsWith('Entry not found') || message.startsWith('HTTP error 404'); if (!isMissing) { logger.warn(`failed to load environment.bin: ${message || err}`); } diff --git a/src/lib/readers/read-lcc2.ts b/src/lib/readers/read-lcc2.ts index 7b4b4211..7ea1f24e 100644 --- a/src/lib/readers/read-lcc2.ts +++ b/src/lib/readers/read-lcc2.ts @@ -1,13 +1,18 @@ -import { containerSource, type ContainerSegment } from './container-source'; -import { readSog, readSogSource } from './read-sog'; -import { readSpz } from './read-spz'; -import { type ChunkDataPool, type ChunkSource, createChunkDataPool } from '../chunk'; +import { createChunkDataPool } from '../chunk'; +import type { ChunkDataPool, ChunkSource } from '../chunk'; import { dataTableToChunkSource, materializeToDataTable } from '../compat/data-table'; -import { Column, DataTable, TypedArray } from '../data-table'; -import { basename, dirname, join, readFile, ReadFileSystem, ReadSource, ReadStream, ZipReadFileSystem } from '../io/read'; -import { Options } from '../types'; +import type { TypedArray } from '../data-table'; +import { Column, DataTable } from '../data-table'; +import type { ReadFileSystem, ReadSource, ReadStream } from '../io/read'; +import { basename, dirname, join, readFile, ZipReadFileSystem } from '../io/read'; +import type { Options } from '../types'; import { logger, Transform } from '../utils'; +import { containerSource } from './container-source'; +import type { ContainerSegment } from './container-source'; +import { readSog, readSogSource } from './read-sog'; +import { readSpz } from './read-spz'; + // Bounded concurrency for chunk decoding. SOG/SPZ decoding is heavier than // LCC v1's range reads (WebP decode / WASM calls), so we stay conservative. const LOAD_CONCURRENCY = 4; @@ -196,9 +201,7 @@ const parseLcc2Meta = (metaText: string, filename: string): Lcc2Meta => { meta = JSON.parse(stripTrailingCommas(metaText)) as RawLcc2Meta; } catch (e) { const reason = (e as Error)?.message ?? String(e); - throw new Error( - `Failed to parse meta.lcc2 as JSON: ${filename}: ${reason}` - ); + throw new Error(`Failed to parse meta.lcc2 as JSON: ${filename}: ${reason}`); } } @@ -206,11 +209,7 @@ const parseLcc2Meta = (metaText: string, filename: string): Lcc2Meta => { // legacy fields are present. Use explicit presence checks (not truthiness) // so legitimate zero values (e.g. total_splats: 0, lod_level: 0) don't // incorrectly skip the legacy normalization branch. - if ( - meta.total_splats !== undefined && - meta.lod_3dgs_info !== undefined && - meta.lod_level !== undefined - ) { + if (meta.total_splats !== undefined && meta.lod_3dgs_info !== undefined && meta.lod_level !== undefined) { // Recursively normalize node field names. const normalizeNode = (node: RawLcc2Node) => { if (node.datatype !== undefined) { @@ -239,33 +238,23 @@ const parseLcc2Meta = (metaText: string, filename: string): Lcc2Meta => { } // 3) env detection. - const envFileIndex = meta.root?.data?.env ? - meta.root.data.env.name : - undefined; + const envFileIndex = meta.root?.data?.env ? meta.root.data.env.name : undefined; // 4) Validate required fields rather than masking absence with type // assertions. totalLevels and root.splatFiles are mandatory for the reader // to resolve LODs and address chunk files; totalSplats / lodSplats are // scene metadata that downstream consumers may rely on. if (typeof meta.totalLevels !== 'number') { - throw new Error( - `Invalid meta.lcc2 (missing or non-numeric totalLevels): ${filename}` - ); + throw new Error(`Invalid meta.lcc2 (missing or non-numeric totalLevels): ${filename}`); } if (!Array.isArray(meta.root?.splatFiles)) { - throw new Error( - `Invalid meta.lcc2 (missing root.splatFiles): ${filename}` - ); + throw new Error(`Invalid meta.lcc2 (missing root.splatFiles): ${filename}`); } if (typeof meta.totalSplats !== 'number') { - throw new Error( - `Invalid meta.lcc2 (missing or non-numeric totalSplats): ${filename}` - ); + throw new Error(`Invalid meta.lcc2 (missing or non-numeric totalSplats): ${filename}`); } if (!Array.isArray(meta.lodSplats)) { - throw new Error( - `Invalid meta.lcc2 (missing lodSplats): ${filename}` - ); + throw new Error(`Invalid meta.lcc2 (missing lodSplats): ${filename}`); } // 5) Return normalized model. @@ -344,14 +333,11 @@ const collectChunksByLevel = ( * @returns Ordered list of input LOD levels. Its index is the output LOD index. * @ignore */ -const resolveLodSelection = ( - lodSelect: number[], - totalLevels: number -): number[] => { +const resolveLodSelection = (lodSelect: number[], totalLevels: number): number[] => { if (lodSelect.length > 0) { return lodSelect - .map(lod => (lod < 0 ? totalLevels + lod : lod)) // negative -> from end - .filter(lod => lod >= 0 && lod < totalLevels); // drop out-of-range + .map((lod) => (lod < 0 ? totalLevels + lod : lod)) // negative -> from end + .filter((lod) => lod >= 0 && lod < totalLevels); // drop out-of-range } // empty = all [0, totalLevels) return Array.from({ length: totalLevels }, (_, i) => i); @@ -369,9 +355,7 @@ const resolveLodSelection = ( const isMissingError = (err: unknown): boolean => { const code = (err as { code?: string })?.code; const message = (err as Error)?.message ?? ''; - return code === 'ENOENT' || - message.startsWith('Entry not found') || - message.startsWith('HTTP error 404'); + return code === 'ENOENT' || message.startsWith('Entry not found') || message.startsWith('HTTP error 404'); }; /** @@ -391,10 +375,7 @@ const isMissingError = (err: unknown): boolean => { * @returns An open {@link ReadSource} for the chunk. * @ignore */ -const openChunkSource = async ( - fileSystem: ReadFileSystem, - fullPath: string -): Promise => { +const openChunkSource = async (fileSystem: ReadFileSystem, fullPath: string): Promise => { try { return await fileSystem.createSource(fullPath); } catch (err) { @@ -426,11 +407,7 @@ const openChunkSource = async ( * @returns Decoded DataTable. * @ignore */ -const decodeChunk = async ( - fileSystem: ReadFileSystem, - splatType: string, - fullPath: string -): Promise => { +const decodeChunk = async (fileSystem: ReadFileSystem, splatType: string, fullPath: string): Promise => { const source = await openChunkSource(fileSystem, fullPath); try { if (splatType === '.sog') { @@ -509,9 +486,9 @@ const gunzipPrefix = async (stream: ReadStream, length: number): Promise - ).getReader(); + const reader = inputStream + .pipeThrough(new DecompressionStream('gzip') as unknown as TransformStream) + .getReader(); try { const result = new Uint8Array(length); let read = 0; @@ -528,6 +505,7 @@ const gunzipPrefix = async (stream: ReadStream, length: number): Promise {}); } }; @@ -541,8 +519,13 @@ const gunzipPrefix = async (stream: ReadStream, length: number): Promise { - if (header.length < SPZ_HEADER_SIZE || - header[0] !== 0x4e || header[1] !== 0x47 || header[2] !== 0x53 || header[3] !== 0x50) { + if ( + header.length < SPZ_HEADER_SIZE || + header[0] !== 0x4e || + header[1] !== 0x47 || + header[2] !== 0x53 || + header[3] !== 0x50 + ) { throw new Error(`Invalid SPZ chunk header: ${context}`); } const view = new DataView(header.buffer, header.byteOffset, header.byteLength); @@ -565,11 +548,7 @@ const parseSpzNumPoints = (header: Uint8Array, context: string): number => { * @returns The chunk's splat count. * @ignore */ -const readChunkCount = async ( - fileSystem: ReadFileSystem, - splatType: string, - fullPath: string -): Promise => { +const readChunkCount = async (fileSystem: ReadFileSystem, splatType: string, fullPath: string): Promise => { const source = await openChunkSource(fileSystem, fullPath); try { if (splatType === '.sog') { @@ -577,9 +556,12 @@ const readChunkCount = async ( try { const sogMeta = JSON.parse(new TextDecoder().decode(await readFile(zipFs, 'meta.json'))); const version = sogMeta.version; - const count = version === undefined ? - sogMeta.means?.shape?.[0] : // V1: texture shape - (version === 2 ? sogMeta.count : undefined); + const count = + version === undefined + ? sogMeta.means?.shape?.[0] // V1: texture shape + : version === 2 + ? sogMeta.count + : undefined; if (!Number.isInteger(count) || count < 0) { throw new Error(`Cannot determine SOG chunk splat count: ${fullPath}`); } @@ -637,11 +619,7 @@ const readChunkCount = async ( * @returns Promise resolving to an array of DataTables (combined LODs + optional environment). * @ignore */ -const readLcc2 = async ( - fileSystem: ReadFileSystem, - filename: string, - options: Options -): Promise => { +const readLcc2 = async (fileSystem: ReadFileSystem, filename: string, options: Options): Promise => { // 1) Read and parse meta.lcc2. const baseDir = dirname(filename); const related = (name: string) => (baseDir ? join(baseDir, name) : name); @@ -666,8 +644,12 @@ const readLcc2 = async ( const files = byLevel.get(inputLods[outputLod]); const indices = files ? Array.from(files.keys()).sort((a, b) => a - b) : []; for (const fileIndex of indices) { - if (!Number.isInteger(fileIndex) || fileIndex < 0 || - fileIndex >= splatFiles.length || !splatFiles[fileIndex]) { + if ( + !Number.isInteger(fileIndex) || + fileIndex < 0 || + fileIndex >= splatFiles.length || + !splatFiles[fileIndex] + ) { throw new Error( `Invalid chunk file index ${fileIndex} (root.splatFiles has ${splatFiles.length} entries) in LCC2 input file: ${filename}` ); @@ -707,15 +689,11 @@ const readLcc2 = async ( const m = nextScan++; if (m >= missing.length) break; const i = missing[m]; - counts[i] = await readChunkCount( - fileSystem, splatType, related(splatFiles[tasks[i].fileIndex]) - ); + counts[i] = await readChunkCount(fileSystem, splatType, related(splatFiles[tasks[i].fileIndex])); scanBar.update(++scanned); } }; - await Promise.all( - Array.from({ length: Math.min(LOAD_CONCURRENCY, missing.length) }, () => scanWorker()) - ); + await Promise.all(Array.from({ length: Math.min(LOAD_CONCURRENCY, missing.length) }, () => scanWorker())); // Close the bar only on success path (matches the decode bar below). scanBar.end(); } @@ -781,14 +759,12 @@ const readLcc2 = async ( // dt goes out of scope here, releasing the chunk's memory. } }; - await Promise.all( - Array.from({ length: Math.min(LOAD_CONCURRENCY, tasks.length) }, () => worker()) - ); + await Promise.all(Array.from({ length: Math.min(LOAD_CONCURRENCY, tasks.length) }, () => worker())); // 8) Assemble the merged table, columns ordered by first sighting. const columns = [...outputs.entries()] - .sort(([, a], [, b]) => (a.task - b.task) || (a.pos - b.pos)) - .map(([name, output]) => new Column(name, output.data)); + .sort(([, a], [, b]) => a.task - b.task || a.pos - b.pos) + .map(([name, output]) => new Column(name, output.data)); columns.push(new Column('lod', lodData)); const merged = new DataTable(columns, LCC2_TRANSFORM()); // Close the bar only on success path. @@ -801,9 +777,7 @@ const readLcc2 = async ( try { const envFull = related(splatFiles[envFileIndex]); const envTable = await decodeChunk(fileSystem, splatType, envFull); - envTable.addColumn( - new Column('lod', new Float32Array(envTable.numRows).fill(-1)) - ); + envTable.addColumn(new Column('lod', new Float32Array(envTable.numRows).fill(-1))); envTable.transform = LCC2_TRANSFORM(); result.push(envTable); } catch (err) { @@ -910,15 +884,19 @@ const readLcc2Source = async ( const indices = files ? Array.from(files.keys()).sort((a, b) => a - b) : []; const segs: ContainerSegment[] = []; for (const fileIndex of indices) { - if (!Number.isInteger(fileIndex) || fileIndex < 0 || - fileIndex >= splatFiles.length || !splatFiles[fileIndex]) { + if ( + !Number.isInteger(fileIndex) || + fileIndex < 0 || + fileIndex >= splatFiles.length || + !splatFiles[fileIndex] + ) { throw new Error( `Invalid chunk file index ${fileIndex} (root.splatFiles has ${splatFiles.length} entries) in LCC2 input file: ${filename}` ); } const fullPath = related(splatFiles[fileIndex]); const metaCount = files!.get(fileIndex); - const count = metaCount ?? await readChunkCount(fileSystem, splatType, fullPath); + const count = metaCount ?? (await readChunkCount(fileSystem, splatType, fullPath)); segs.push({ count, decode: () => decodeChunkSource(fileSystem, splatType, fullPath, pool) }); total += count; } @@ -956,8 +934,12 @@ const readLcc2EnvironmentSource = async ( const related = (name: string) => (baseDir ? join(baseDir, name) : name); const meta = parseLcc2Meta(new TextDecoder().decode(await readFile(fileSystem, filename)), filename); const { splatFiles, splatType, envFileIndex } = meta; - if (envFileIndex === undefined || envFileIndex < 0 || - envFileIndex >= splatFiles.length || !splatFiles[envFileIndex]) { + if ( + envFileIndex === undefined || + envFileIndex < 0 || + envFileIndex >= splatFiles.length || + !splatFiles[envFileIndex] + ) { return null; } try { diff --git a/src/lib/readers/read-lod.ts b/src/lib/readers/read-lod.ts index 2f5ce630..ef11f6df 100644 --- a/src/lib/readers/read-lod.ts +++ b/src/lib/readers/read-lod.ts @@ -1,8 +1,11 @@ -import { containerSource, type ContainerSegment } from './container-source'; +import type { ChunkDataPool, ChunkSource } from '../chunk'; +import { dirname, join, readFile } from '../io/read'; +import type { ReadFileSystem } from '../io/read'; +import type { Options } from '../types'; + +import { containerSource } from './container-source'; +import type { ContainerSegment } from './container-source'; import { readSogSource } from './read-sog'; -import { type ChunkDataPool, type ChunkSource } from '../chunk'; -import { dirname, join, readFile, type ReadFileSystem } from '../io/read'; -import { type Options } from '../types'; type LodReference = { file: number; @@ -43,12 +46,15 @@ const parseLodMeta = (text: string, filename: string): LodMeta => { if (!Number.isInteger(meta.lodLevels) || meta.lodLevels <= 0) { throw new Error(`Invalid lod-meta.json lodLevels: ${filename}`); } - if (meta.counts !== undefined && - (!Array.isArray(meta.counts) || meta.counts.length !== meta.lodLevels || - meta.counts.some(count => !Number.isInteger(count) || count < 0))) { + if ( + meta.counts !== undefined && + (!Array.isArray(meta.counts) || + meta.counts.length !== meta.lodLevels || + meta.counts.some((count) => !Number.isInteger(count) || count < 0)) + ) { throw new Error(`Invalid lod-meta.json counts: ${filename}`); } - if (!Array.isArray(meta.filenames) || meta.filenames.some(name => typeof name !== 'string')) { + if (!Array.isArray(meta.filenames) || meta.filenames.some((name) => typeof name !== 'string')) { throw new Error(`Invalid lod-meta.json filenames: ${filename}`); } if (!meta.tree || typeof meta.tree !== 'object') { @@ -61,16 +67,29 @@ const parseLodMeta = (text: string, filename: string): LodMeta => { const collectFilesByLod = (meta: LodMeta, filename: string): Map> => { const result = new Map>(); const traverse = (node: LodNode): void => { - if (!node || typeof node !== 'object' || Array.isArray(node) || - (node.children !== undefined && !Array.isArray(node.children))) { + if ( + !node || + typeof node !== 'object' || + Array.isArray(node) || + (node.children !== undefined && !Array.isArray(node.children)) + ) { throw new Error(`Invalid lod-meta.json tree: ${filename}`); } for (const [key, ref] of Object.entries(node.lods ?? {})) { const lod = Number(key); - if (!Number.isInteger(lod) || lod < 0 || lod >= meta.lodLevels || - !ref || !Number.isInteger(ref.file) || ref.file < 0 || ref.file >= meta.filenames.length || - !Number.isInteger(ref.offset) || ref.offset < 0 || - !Number.isInteger(ref.count) || ref.count < 0) { + if ( + !Number.isInteger(lod) || + lod < 0 || + lod >= meta.lodLevels || + !ref || + !Number.isInteger(ref.file) || + ref.file < 0 || + ref.file >= meta.filenames.length || + !Number.isInteger(ref.offset) || + ref.offset < 0 || + !Number.isInteger(ref.count) || + ref.count < 0 + ) { throw new Error(`Invalid lod-meta.json LOD reference: ${filename}`); } let files = result.get(lod); @@ -102,9 +121,7 @@ const collectFilesByLod = (meta: LodMeta, filename: string): Map { if (lodSelect.length === 0) return Array.from({ length: numLods }, (_, i) => i); - return lodSelect - .map(lod => (lod < 0 ? numLods + lod : lod)) - .filter(lod => lod >= 0 && lod < numLods); + return lodSelect.map((lod) => (lod < 0 ? numLods + lod : lod)).filter((lod) => lod >= 0 && lod < numLods); }; /** @@ -139,14 +156,14 @@ const readLodSource = async ( const segmentsByLod = inputLods.map((lod): ContainerSegment[] => { const files = filesByLod.get(lod); return [...(files?.entries() ?? [])] - .sort(([a], [b]) => a - b) - .map(([file, count]) => { - const path = related(meta.filenames[file]); - return { - count, - decode: () => readSogSource(fileSystem, path, pool, { logging: 'silent' }) - }; - }); + .sort(([a], [b]) => a - b) + .map(([file, count]) => { + const path = related(meta.filenames[file]); + return { + count, + decode: () => readSogSource(fileSystem, path, pool, { logging: 'silent' }) + }; + }); }); return containerSource(segmentsByLod, pool); diff --git a/src/lib/readers/read-mjs.ts b/src/lib/readers/read-mjs.ts index 9bacfa46..b9d9e8c6 100644 --- a/src/lib/readers/read-mjs.ts +++ b/src/lib/readers/read-mjs.ts @@ -1,5 +1,5 @@ import { Column, DataTable } from '../data-table'; -import { Param } from '../types'; +import type { Param } from '../types'; type Generator = { count: number; diff --git a/src/lib/readers/read-ply.ts b/src/lib/readers/read-ply.ts index 3d96c86b..602dcdbd 100644 --- a/src/lib/readers/read-ply.ts +++ b/src/lib/readers/read-ply.ts @@ -1,15 +1,4 @@ -import { isCompressedPly, decompressPly } from './decompress-ply'; -import { fileChunkSource, readExact, sortGatherSlots, gatherRuns } from './reader-utils'; import { - type ChunkData, - type ChunkLayer, - type ReadRequest, - type ChunkSource, - type ChunkSourceMetadata, - type ChunkDataPool, - type ExtraColumn, - type LayerLayout, - type SHBands, SH_REST_COUNTS, POSITION_STRIDE, GEOMETRIC_STRIDE, @@ -19,18 +8,32 @@ import { colorFields, otherLayout } from '../chunk'; +import type { + ChunkData, + ChunkLayer, + ReadRequest, + ChunkSource, + ChunkSourceMetadata, + ChunkDataPool, + ExtraColumn, + LayerLayout, + SHBands +} from '../chunk'; import { Column, DataTable } from '../data-table'; -import { type ReadSource, type ReadStream } from '../io/read'; -import { type SplatModel } from '../splat-model'; +import type { ReadSource, ReadStream } from '../io/read'; +import type { SplatModel } from '../splat-model'; import { logger, Transform } from '../utils'; +import { isCompressedPly, decompressPly } from './decompress-ply'; +import { fileChunkSource, readExact, sortGatherSlots, gatherRuns } from './reader-utils'; + type PlyProperty = { - name: string; // 'x', 'f_dc_0', etc - type: string; // 'float', 'char', etc + name: string; // 'x', 'f_dc_0', etc + type: string; // 'float', 'char', etc }; type PlyElement = { - name: string; // 'vertex', etc + name: string; // 'vertex', etc count: number; properties: PlyProperty[]; }; @@ -38,16 +41,16 @@ type PlyElement = { type PlyHeader = { comments: string[]; elements: PlyElement[]; - headerBytes: number; // byte offset at which binary data begins - format: string; // 'binary_little_endian' | 'binary_big_endian' | 'ascii' + headerBytes: number; // byte offset at which binary data begins + format: string; // 'binary_little_endian' | 'binary_big_endian' | 'ascii' }; // A whole-PLY decode result: one DataTable per element (consumed by decompressPly). type PlyData = { comments: string[]; elements: { - name: string, - dataTable: DataTable + name: string; + dataTable: DataTable; }[]; }; @@ -55,7 +58,7 @@ const GEOMETRIC_COLS = ['rot_0', 'rot_1', 'rot_2', 'rot_3', 'scale_0', 'scale_1' const COLOR_DC_COLS = ['f_dc_0', 'f_dc_1', 'f_dc_2']; const SCALE_2_WORD = GEOMETRIC_COLS.indexOf('scale_2'); // The geometric columns a 2DGS record carries (everything but the third scale). -const GEOMETRIC_COLS_2DGS = GEOMETRIC_COLS.filter(c => c !== 'scale_2'); +const GEOMETRIC_COLS_2DGS = GEOMETRIC_COLS.filter((c) => c !== 'scale_2'); // Brush's `SplatRenderMode: ` (brush-serde export.rs/import.rs) — the wire // form we also write, see `splatModelComment` in the writers. @@ -99,15 +102,24 @@ const splatModelFromComments = (comments: string[]): SplatModel => { const getDataType = (type: string) => { switch (type) { - case 'char': return Int8Array; - case 'uchar': return Uint8Array; - case 'short': return Int16Array; - case 'ushort': return Uint16Array; - case 'int': return Int32Array; - case 'uint': return Uint32Array; - case 'float': return Float32Array; - case 'double': return Float64Array; - default: return null; + case 'char': + return Int8Array; + case 'uchar': + return Uint8Array; + case 'short': + return Int16Array; + case 'ushort': + return Uint16Array; + case 'int': + return Int32Array; + case 'uint': + return Uint32Array; + case 'float': + return Float32Array; + case 'double': + return Float64Array; + default: + return null; } }; @@ -116,25 +128,43 @@ type ValueReader = (view: DataView, offset: number) => number; const getReader = (type: string): ValueReader => { switch (type) { - case 'char': return (v, o) => v.getInt8(o); - case 'uchar': return (v, o) => v.getUint8(o); - case 'short': return (v, o) => v.getInt16(o, true); - case 'ushort': return (v, o) => v.getUint16(o, true); - case 'int': return (v, o) => v.getInt32(o, true); - case 'uint': return (v, o) => v.getUint32(o, true); - case 'float': return (v, o) => v.getFloat32(o, true); - case 'double': return (v, o) => v.getFloat64(o, true); - default: throw new Error(`readPly: unsupported ply type '${type}'`); + case 'char': + return (v, o) => v.getInt8(o); + case 'uchar': + return (v, o) => v.getUint8(o); + case 'short': + return (v, o) => v.getInt16(o, true); + case 'ushort': + return (v, o) => v.getUint16(o, true); + case 'int': + return (v, o) => v.getInt32(o, true); + case 'uint': + return (v, o) => v.getUint32(o, true); + case 'float': + return (v, o) => v.getFloat32(o, true); + case 'double': + return (v, o) => v.getFloat64(o, true); + default: + throw new Error(`readPly: unsupported ply type '${type}'`); } }; const typeSize = (type: string): number => { switch (type) { - case 'char': case 'uchar': return 1; - case 'short': case 'ushort': return 2; - case 'int': case 'uint': case 'float': return 4; - case 'double': return 8; - default: throw new Error(`readPly: unsupported ply type '${type}'`); + case 'char': + case 'uchar': + return 1; + case 'short': + case 'ushort': + return 2; + case 'int': + case 'uint': + case 'float': + return 4; + case 'double': + return 8; + default: + throw new Error(`readPly: unsupported ply type '${type}'`); } }; @@ -183,7 +213,8 @@ const readHeader = async (source: ReadSource): Promise => { for (let i = 1; i < lines.length; i++) { const words = lines[i].split(' ').filter(Boolean); switch (words[0]) { - case 'ply': case 'end_header': + case 'ply': + case 'end_header': break; case 'format': format = words[1] ?? ''; @@ -197,7 +228,8 @@ const readHeader = async (source: ReadSource): Promise => { elements.push(current); break; case 'property': - if (!current || words.length !== 3 || !getDataType(words[1])) throw new Error('readPly: invalid property line'); + if (!current || words.length !== 3 || !getDataType(words[1])) + throw new Error('readPly: invalid property line'); current.properties.push({ name: words[2], type: words[1] }); break; default: @@ -218,9 +250,9 @@ const decodeElement = async ( count: number, onProgress?: (rows: number) => void ): Promise => { - const columns = properties.map(p => new Column(p.name, new (getDataType(p.type)!)(count))); + const columns = properties.map((p) => new Column(p.name, new (getDataType(p.type)!)(count))); const numProperties = columns.length; - const allFloat = properties.every(p => p.type === 'float'); + const allFloat = properties.every((p) => p.type === 'float'); const blockRows = 1024; const numBlocks = Math.ceil(count / blockRows); @@ -228,7 +260,7 @@ const decodeElement = async ( const rowSize = numProperties * 4; const blockData = new Uint8Array(blockRows * rowSize); const floatData = new Float32Array(blockData.buffer); - const storage = columns.map(c => c.data as Float32Array); + const storage = columns.map((c) => c.data as Float32Array); for (let b = 0; b < numBlocks; ++b) { const rows = Math.min(blockRows, count - b * blockRows); @@ -292,7 +324,9 @@ const decodePlyToDataTable = async (source: ReadSource): Promise => { const decoded = []; for (const element of headerElements) { const priorRows = prior; // snapshot for the progress closure (prior is reassigned below) - const dataTable = await decodeElement(stream, element.properties, element.count, r => bar.update(priorRows + r)); + const dataTable = await decodeElement(stream, element.properties, element.count, (r) => + bar.update(priorRows + r) + ); prior += element.count; decoded.push({ name: element.name, dataTable }); } @@ -303,7 +337,7 @@ const decodePlyToDataTable = async (source: ReadSource): Promise => { if (isCompressedPly(plyData)) { result = decompressPly(plyData); } else { - const vertexElement = plyData.elements.find(e => e.name === 'vertex'); + const vertexElement = plyData.elements.find((e) => e.name === 'vertex'); if (!vertexElement) { throw new Error('PLY file does not contain vertex element'); } @@ -335,9 +369,9 @@ const lerp = (a: number, b: number, t: number): number => a * (1 - t) + b * t; // attributes** straight into the layer buffers — no whole-scene decode, no // intermediate DataTable, no per-gaussian object churn. const readCompressedChunked = (source: ReadSource, header: PlyHeader, pool: ChunkDataPool): ChunkSource => { - const chunkEl = header.elements.find(e => e.name === 'chunk'); - const vertexEl = header.elements.find(e => e.name === 'vertex'); - const shEl = header.elements.find(e => e.name === 'sh'); + const chunkEl = header.elements.find((e) => e.name === 'chunk'); + const vertexEl = header.elements.find((e) => e.name === 'vertex'); + const shEl = header.elements.find((e) => e.name === 'sh'); if (!chunkEl || !vertexEl) { throw new Error('readPly: compressed PLY missing chunk/vertex element'); } @@ -376,7 +410,12 @@ const readCompressedChunked = (source: ReadSource, header: PlyHeader, pool: Chun const numGaussians = vertexEl.count; const restCount = shEl ? shEl.properties.length : 0; - const shBands: SHBands = (restCount === 0 ? 0 : ({ [SH_REST_COUNTS[1]]: 1, [SH_REST_COUNTS[2]]: 2, [SH_REST_COUNTS[3]]: 3 } as Record)[restCount]); + const shBands: SHBands = + restCount === 0 + ? 0 + : ({ [SH_REST_COUNTS[1]]: 1, [SH_REST_COUNTS[2]]: 2, [SH_REST_COUNTS[3]]: 3 } as Record)[ + restCount + ]; if (shBands === undefined) { throw new Error(`readPly: unrecognized compressed sh column count ${restCount}`); } @@ -447,25 +486,44 @@ const readCompressedChunked = (source: ReadSource, header: PlyHeader, pool: Chun const bcol = (name: string): Float32Array => bnd.getColumnByName(name)!.data as Float32Array; // Bound columns, fetched once per read; indexed by 256-block `gi >> 8`. - const minX = pos ? bcol('min_x') : null, minY = pos ? bcol('min_y') : null, minZ = pos ? bcol('min_z') : null; - const maxX = pos ? bcol('max_x') : null, maxY = pos ? bcol('max_y') : null, maxZ = pos ? bcol('max_z') : null; - const minSX = geo ? bcol('min_scale_x') : null, minSY = geo ? bcol('min_scale_y') : null, minSZ = geo ? bcol('min_scale_z') : null; - const maxSX = geo ? bcol('max_scale_x') : null, maxSY = geo ? bcol('max_scale_y') : null, maxSZ = geo ? bcol('max_scale_z') : null; + const minX = pos ? bcol('min_x') : null, + minY = pos ? bcol('min_y') : null, + minZ = pos ? bcol('min_z') : null; + const maxX = pos ? bcol('max_x') : null, + maxY = pos ? bcol('max_y') : null, + maxZ = pos ? bcol('max_z') : null; + const minSX = geo ? bcol('min_scale_x') : null, + minSY = geo ? bcol('min_scale_y') : null, + minSZ = geo ? bcol('min_scale_z') : null; + const maxSX = geo ? bcol('max_scale_x') : null, + maxSY = geo ? bcol('max_scale_y') : null, + maxSZ = geo ? bcol('max_scale_z') : null; const sw = 3 + restCount; const hasColors = col ? bnd.hasColumn('min_r') : false; - const minR = hasColors ? bcol('min_r') : null, minG = hasColors ? bcol('min_g') : null, minB = hasColors ? bcol('min_b') : null; - const maxR = hasColors ? bcol('max_r') : null, maxG = hasColors ? bcol('max_g') : null, maxB = hasColors ? bcol('max_b') : null; + const minR = hasColors ? bcol('min_r') : null, + minG = hasColors ? bcol('min_g') : null, + minB = hasColors ? bcol('min_b') : null; + const maxR = hasColors ? bcol('max_r') : null, + maxG = hasColors ? bcol('max_g') : null, + maxB = hasColors ? bcol('max_b') : null; // Decode one packed record: vertex words at local index `vi` in `v`, sh // bytes at local index `si` in `sBytes`, bounds from global index `gi`, // written to output row `dstRow`. Shared by the chunk and gather paths. - const decodeRecord = (v: Uint32Array, vi: number, sBytes: Uint8Array | null, si: number, gi: number, dstRow: number): void => { + const decodeRecord = ( + v: Uint32Array, + vi: number, + sBytes: Uint8Array | null, + si: number, + gi: number, + dstRow: number + ): void => { const ci = gi >> 8; const base = vi * vWords; if (pos) { const pp = v[base + ppIdx]; const o = dstRow * 3; - pos[o] = lerp(minX![ci], maxX![ci], unpackUnorm(pp >>> 21, 11)); + pos[o] = lerp(minX![ci], maxX![ci], unpackUnorm(pp >>> 21, 11)); pos[o + 1] = lerp(minY![ci], maxY![ci], unpackUnorm(pp >>> 11, 10)); pos[o + 2] = lerp(minZ![ci], maxZ![ci], unpackUnorm(pp, 11)); } @@ -478,10 +536,29 @@ const readCompressedChunked = (source: ReadSource, header: PlyHeader, pool: Chun const rc = (unpackUnorm(pr, 10) - 0.5) * ROT_NORM; const rm = Math.sqrt(Math.max(0, 1 - (ra * ra + rb * rb + rc * rc))); switch (pr >>> 30) { - case 0: geo[o] = rm; geo[o + 1] = ra; geo[o + 2] = rb; geo[o + 3] = rc; break; - case 1: geo[o] = ra; geo[o + 1] = rm; geo[o + 2] = rb; geo[o + 3] = rc; break; - case 2: geo[o] = ra; geo[o + 1] = rb; geo[o + 2] = rm; geo[o + 3] = rc; break; - default: geo[o] = ra; geo[o + 1] = rb; geo[o + 2] = rc; geo[o + 3] = rm; + case 0: + geo[o] = rm; + geo[o + 1] = ra; + geo[o + 2] = rb; + geo[o + 3] = rc; + break; + case 1: + geo[o] = ra; + geo[o + 1] = rm; + geo[o + 2] = rb; + geo[o + 3] = rc; + break; + case 2: + geo[o] = ra; + geo[o + 1] = rb; + geo[o + 2] = rm; + geo[o + 3] = rc; + break; + default: + geo[o] = ra; + geo[o + 1] = rb; + geo[o + 2] = rc; + geo[o + 3] = rm; } // scale const ps = v[base + psIdx]; @@ -508,7 +585,7 @@ const readCompressedChunked = (source: ReadSource, header: PlyHeader, pool: Chun const sb = si * shRowSize; for (let r = 0; r < restCount; r++) { const byte = sBytes[sb + shOff[r]]; - const nrm = (byte === 0) ? 0 : (byte === 255) ? 1 : (byte + 0.5) / 256; + const nrm = byte === 0 ? 0 : byte === 255 ? 1 : (byte + 0.5) / 256; col[o + 3 + r] = (nrm - 0.5) * 8; } } @@ -521,19 +598,29 @@ const readCompressedChunked = (source: ReadSource, header: PlyHeader, pool: Chun // combined per-record cost drives the gap/window accounting). const { indices, indexOffset, count } = request; if (count <= 0) return; - const slot = sortGatherSlots(count, s => indices[indexOffset + s]); + const slot = sortGatherSlots(count, (s) => indices[indexOffset + s]); const rowAt = (t: number) => indices[indexOffset + slot[t]]; const wantSh = !!(col && shEl); const costBytes = vertexRowSize + (wantSh ? shRowSize : 0); - for (const run of gatherRuns(count, t => rowAt(t) * vertexRowSize, vertexRowSize, costBytes)) { + for (const run of gatherRuns(count, (t) => rowAt(t) * vertexRowSize, vertexRowSize, costBytes)) { const firstRow = run.firstByte / vertexRowSize; const rc = run.recordCount; const vNeed = rc * vertexRowSize; if (!gatherV || gatherV.length < vNeed) gatherV = new Uint8Array(vNeed); const vBytes = gatherV.subarray(0, vNeed); - if (await readExact(source.read(vertexOffset + firstRow * vertexRowSize, vertexOffset + (firstRow + rc) * vertexRowSize), vBytes, 0, vNeed) !== vNeed) { + if ( + (await readExact( + source.read( + vertexOffset + firstRow * vertexRowSize, + vertexOffset + (firstRow + rc) * vertexRowSize + ), + vBytes, + 0, + vNeed + )) !== vNeed + ) { throw new Error(`readPly: short gather vertex read at row ${firstRow}`); } const vU32 = new Uint32Array(gatherV.buffer, gatherV.byteOffset, rc * vWords); @@ -543,7 +630,14 @@ const readCompressedChunked = (source: ReadSource, header: PlyHeader, pool: Chun const sNeed = rc * shRowSize; if (!gatherS || gatherS.length < sNeed) gatherS = new Uint8Array(sNeed); sBytes = gatherS.subarray(0, sNeed); - if (await readExact(source.read(shOffset + firstRow * shRowSize, shOffset + (firstRow + rc) * shRowSize), sBytes, 0, sNeed) !== sNeed) { + if ( + (await readExact( + source.read(shOffset + firstRow * shRowSize, shOffset + (firstRow + rc) * shRowSize), + sBytes, + 0, + sNeed + )) !== sNeed + ) { throw new Error(`readPly: short gather sh read at row ${firstRow}`); } } @@ -567,7 +661,14 @@ const readCompressedChunked = (source: ReadSource, header: PlyHeader, pool: Chun vScratch ??= new Uint8Array(chunkSize * vertexRowSize); const vBytes = vScratch.subarray(0, count * vertexRowSize); - if (await readExact(source.read(vertexOffset + start * vertexRowSize, vertexOffset + (start + count) * vertexRowSize), vBytes, 0, vBytes.length) !== vBytes.length) { + if ( + (await readExact( + source.read(vertexOffset + start * vertexRowSize, vertexOffset + (start + count) * vertexRowSize), + vBytes, + 0, + vBytes.length + )) !== vBytes.length + ) { throw new Error(`readPly: short vertex read for chunk ${request.chunkIndex}`); } const vU32 = new Uint32Array(vScratch.buffer, 0, count * vWords); @@ -576,7 +677,14 @@ const readCompressedChunked = (source: ReadSource, header: PlyHeader, pool: Chun if (col && shEl) { sScratch ??= new Uint8Array(chunkSize * shRowSize); sBytes = sScratch.subarray(0, count * shRowSize); - if (await readExact(source.read(shOffset + start * shRowSize, shOffset + (start + count) * shRowSize), sBytes, 0, sBytes.length) !== sBytes.length) { + if ( + (await readExact( + source.read(shOffset + start * shRowSize, shOffset + (start + count) * shRowSize), + sBytes, + 0, + sBytes.length + )) !== sBytes.length + ) { throw new Error(`readPly: short sh read for chunk ${request.chunkIndex}`); } } @@ -630,7 +738,7 @@ const readPly = async (source: ReadSource, pool: ChunkDataPool): Promise e.name === 'vertex'); + const vertex = header.elements.find((e) => e.name === 'vertex'); if (!vertex) { throw new Error('readPly: PLY has no vertex element'); } @@ -639,7 +747,7 @@ const readPly = async (source: ReadSource, pool: ChunkDataPool): Promise p.name === 'packed_position')) { + if (properties.some((p) => p.name === 'packed_position')) { return readCompressedChunked(source, header, pool); } @@ -650,7 +758,9 @@ const readPly = async (source: ReadSource, pool: ChunkDataPool): Promise sum + e.count * rowSizeOf(e.properties), 0); if (source.size !== expected) { - throw new Error(`readPly: file size ${source.size} does not match header-implied size ${expected} (truncated or corrupt PLY)`); + throw new Error( + `readPly: file size ${source.size} does not match header-implied size ${expected} (truncated or corrupt PLY)` + ); } } @@ -666,7 +776,7 @@ const readPly = async (source: ReadSource, pool: ChunkDataPool): Promise p.type === 'float'); + const recordAllFloat = properties.every((p) => p.type === 'float'); const has = (name: string) => recordOffset.has(name); const hasPosition = ['x', 'y', 'z'].every(has); @@ -690,7 +800,12 @@ const readPly = async (source: ReadSource, pool: ChunkDataPool): Promise)[restCount]); + const shBands: SHBands = + restCount === 0 + ? 0 + : ({ [SH_REST_COUNTS[1]]: 1, [SH_REST_COUNTS[2]]: 2, [SH_REST_COUNTS[3]]: 3 } as Record)[ + restCount + ]; if (shBands === undefined) { throw new Error(`readPly: unrecognized f_rest_* count ${restCount}`); } @@ -699,23 +814,27 @@ const readPly = async (source: ReadSource, pool: ChunkDataPool): Promise(['x', 'y', 'z', ...GEOMETRIC_COLS, ...COLOR_DC_COLS]); const extras: ExtraColumn[] = properties - .filter(p => !standard.has(p.name) && !/^f_rest_\d+$/.test(p.name)) - .map((p) => { - const type: 'float32' | 'uint32' = p.type === 'float' || p.type === 'double' ? 'float32' : 'uint32'; - return { name: p.name, type }; - }); + .filter((p) => !standard.has(p.name) && !/^f_rest_\d+$/.test(p.name)) + .map((p) => { + const type: 'float32' | 'uint32' = p.type === 'float' || p.type === 'double' ? 'float32' : 'uint32'; + return { name: p.name, type }; + }); const hasOther = extras.length > 0; // Build per-layer fill plans + layouts. const fieldNames = (layer: ChunkLayer): string[] => { switch (layer) { - case 'position': return ['x', 'y', 'z']; - case 'geometric': return geometricCols; - case 'color': return [...COLOR_DC_COLS, ...Array.from({ length: SH_REST_COUNTS[shBands] }, (_, k) => `f_rest_${k}`)]; - case 'other': return extras.map(e => e.name); + case 'position': + return ['x', 'y', 'z']; + case 'geometric': + return geometricCols; + case 'color': + return [...COLOR_DC_COLS, ...Array.from({ length: SH_REST_COUNTS[shBands] }, (_, k) => `f_rest_${k}`)]; + case 'other': + return extras.map((e) => e.name); } }; - const uintByName = new Map(extras.map(e => [e.name, e.type === 'uint32'])); + const uintByName = new Map(extras.map((e) => [e.name, e.type === 'uint32'])); const layerPlan = (layer: ChunkLayer, stride: number): LayerPlan => { // The geometric layer's destination slot is its canonical position (so a @@ -727,9 +846,9 @@ const readPly = async (source: ReadSource, pool: ChunkDataPool): Promise !f.uint); - const srcIdx = new Uint32Array(fields.map(f => f.recordOffset >> 2)); - const dstIdx = new Uint32Array(fields.map(f => f.dstByteOffset >> 2)); + const allFloat = recordAllFloat && fields.every((f) => !f.uint); + const srcIdx = new Uint32Array(fields.map((f) => f.recordOffset >> 2)); + const dstIdx = new Uint32Array(fields.map((f) => f.dstByteOffset >> 2)); return { stride, fields, allFloat, srcIdx, dstIdx, synthScale2: is2dgs && layer === 'geometric' }; }; @@ -775,7 +894,13 @@ const readPly = async (source: ReadSource, pool: ChunkDataPool): Promise { + const fill = ( + recordBytes: Uint8Array, + count: number, + chunkData: ChunkData, + plan: LayerPlan, + dstRow: number + ): void => { // 2DGS: no source field targets the scale_2 slot, so fill it here (order // relative to the de-interleave below doesn't matter). -Infinity log scale // is linear 0 — a zero-thickness surfel. @@ -840,8 +965,9 @@ const readPly = async (source: ReadSource, pool: ChunkDataPool): Promise request[layer]); + const requested: ChunkLayer[] = (['position', 'geometric', 'color', 'other'] as ChunkLayer[]).filter( + (layer) => request[layer] + ); for (const layer of requested) { if (!plans[layer]) { throw new Error(`readPly: layer '${layer}' not available`); @@ -858,7 +984,7 @@ const readPly = async (source: ReadSource, pool: ChunkDataPool): Promise indices[indexOffset + s]); + const slot = sortGatherSlots(count, (s) => indices[indexOffset + s]); const byteAt = (t: number) => indices[indexOffset + slot[t]] * recordStride; for (const run of gatherRuns(count, byteAt, recordStride)) { diff --git a/src/lib/readers/read-sog-v1.ts b/src/lib/readers/read-sog-v1.ts index 5ed899b1..74fbc046 100644 --- a/src/lib/readers/read-sog-v1.ts +++ b/src/lib/readers/read-sog-v1.ts @@ -1,6 +1,8 @@ import { Column, DataTable } from '../data-table'; -import { join, type ReadFileSystem } from '../io/read'; +import { join } from '../io/read'; +import type { ReadFileSystem } from '../io/read'; import { logger, Transform, WebPCodec } from '../utils'; + import type { ReadSogOptions } from './read-sog'; // V1 (legacy) SOG meta layout. Quantization is a per-channel linear lerp @@ -54,9 +56,9 @@ const invLogTransform = (v: number) => { const unpackQuat = (px: number, py: number, pz: number, tag: number): [number, number, number, number] => { const maxComp = tag - 252; - const a = px / 255 * 2 - 1; - const b = py / 255 * 2 - 1; - const c = pz / 255 * 2 - 1; + const a = (px / 255) * 2 - 1; + const b = (py / 255) * 2 - 1; + const c = (pz / 255) * 2 - 1; const sqrt2 = Math.sqrt(2); const comps = [0, 0, 0, 0]; const idx = [ @@ -95,7 +97,12 @@ const v1ShBandsWidths: Record = { 192: 1, 512: 2, 960: 3 }; * @returns DataTable with Gaussian splat data * @ignore */ -const readSogV1 = async (fileSystem: ReadFileSystem, baseDir: string, meta: MetaV1, options: ReadSogOptions = {}): Promise => { +const readSogV1 = async ( + fileSystem: ReadFileSystem, + baseDir: string, + meta: MetaV1, + options: ReadSogOptions = {} +): Promise => { const decoder = await WebPCodec.create(); const count = meta.means.shape[0]; @@ -144,9 +151,12 @@ const readSogV1 = async (fileSystem: ReadFileSystem, baseDir: string, meta: Meta const xCol = columns[0].data as Float32Array; const yCol = columns[1].data as Float32Array; const zCol = columns[2].data as Float32Array; - const xMin = mins[0], xScale = (maxs[0] - mins[0]) || 1; - const yMin = mins[1], yScale = (maxs[1] - mins[1]) || 1; - const zMin = mins[2], zScale = (maxs[2] - mins[2]) || 1; + const xMin = mins[0], + xScale = maxs[0] - mins[0] || 1; + const yMin = mins[1], + yScale = maxs[1] - mins[1] || 1; + const zMin = mins[2], + zScale = maxs[2] - mins[2] || 1; for (let i = 0; i < count; i++) { xCol[i] = invLogTransform(xMin + xScale * (xs[i] / 65535)); yCol[i] = invLogTransform(yMin + yScale * (ys[i] / 65535)); @@ -167,13 +177,20 @@ const readSogV1 = async (fileSystem: ReadFileSystem, baseDir: string, meta: Meta for (let i = 0; i < count; i++) { const o = i * 4; const tag = qr[o + 3]; - if (tag < 252 || tag > 255) { // invalid tag, default to identity (rot_0 = w) - r0[i] = 1; r1[i] = 0; r2[i] = 0; r3[i] = 0; + if (tag < 252 || tag > 255) { + // invalid tag, default to identity (rot_0 = w) + r0[i] = 1; + r1[i] = 0; + r2[i] = 0; + r3[i] = 0; continue; } // unpackQuat returns components in (w, x, y, z) order; rot_0..rot_3 map to (w, x, y, z). const [w, x, y, z] = unpackQuat(qr[o], qr[o + 1], qr[o + 2], tag); - r0[i] = w; r1[i] = x; r2[i] = y; r3[i] = z; + r0[i] = w; + r1[i] = x; + r2[i] = y; + r3[i] = z; } tickPass(); @@ -233,7 +250,8 @@ const readSogV1 = async (fileSystem: ReadFileSystem, baseDir: string, meta: Meta // shN payload is malformed; throw rather than silently skipping. const bands = v1ShBandsWidths[cW] ?? 0; const shCoeffs = [0, 3, 8, 15][bands]; - if (bands === 0) throw new Error(`SOG shN centroids texture has unrecognized width ${cW}, expected one of 192 / 512 / 960`); + if (bands === 0) + throw new Error(`SOG shN centroids texture has unrecognized width ${cW}, expected one of 192 / 512 / 960`); if (shCoeffs > 0) { const shMin = meta.shN.mins; diff --git a/src/lib/readers/read-sog.ts b/src/lib/readers/read-sog.ts index a648fc88..9a832ea2 100644 --- a/src/lib/readers/read-sog.ts +++ b/src/lib/readers/read-sog.ts @@ -1,11 +1,4 @@ import { - type ReadRequest, - type ChunkSource, - type ChunkSourceMetadata, - type ChunkDataPool, - type ChunkLayer, - type LayerLayout, - type SHBands, POSITION_STRIDE, GEOMETRIC_STRIDE, colorStride, @@ -14,12 +7,25 @@ import { colorFields, createChunkDataPool } from '../chunk'; +import type { + ReadRequest, + ChunkSource, + ChunkSourceMetadata, + ChunkDataPool, + ChunkLayer, + LayerLayout, + SHBands +} from '../chunk'; import { dataTableToChunkSource, materializeToDataTable } from '../compat/data-table'; -import { type DataTable } from '../data-table'; -import { basename, dirname, join, type ReadFileSystem, readFile } from '../io/read'; -import { isSplatModel, type SplatModel } from '../splat-model'; +import type { DataTable } from '../data-table'; +import { basename, dirname, join, readFile } from '../io/read'; +import type { ReadFileSystem } from '../io/read'; +import { isSplatModel } from '../splat-model'; +import type { SplatModel } from '../splat-model'; import { logger, Transform, WebPCodec } from '../utils'; -import { readSogV1, type MetaV1 } from './read-sog-v1'; + +import { readSogV1 } from './read-sog-v1'; +import type { MetaV1 } from './read-sog-v1'; // V2 (current) SOG meta layout - codebook-based quantization for scales / // sh0 / shN. Legacy V1 uses a different per-channel mins/maxs scheme and is @@ -72,9 +78,9 @@ const tmpQuat = new Float32Array(4); // Decode a smallest-three packed quaternion into `tmpQuat` as [w, x, y, z]. const unpackQuat = (px: number, py: number, pz: number, tag: number): void => { const maxComp = tag - 252; - const a = (px / 255 * 2 - 1) / SQRT2; - const b = (py / 255 * 2 - 1) / SQRT2; - const c = (pz / 255 * 2 - 1) / SQRT2; + const a = ((px / 255) * 2 - 1) / SQRT2; + const b = ((py / 255) * 2 - 1) / SQRT2; + const c = ((pz / 255) * 2 - 1) / SQRT2; tmpQuat[0] = 0; tmpQuat[1] = 0; tmpQuat[2] = 0; @@ -135,9 +141,12 @@ const readSogSourceV2 = async ( const hi = decoder.decodeRGBA(await load(meta.means.files[1])).rgba; if (meansLo.width * meansLo.height < count) throw new Error('SOG means texture too small for count'); const { mins, maxs } = meta.means; - const xMin = mins[0], xScale = (maxs[0] - mins[0]) || 1; - const yMin = mins[1], yScale = (maxs[1] - mins[1]) || 1; - const zMin = mins[2], zScale = (maxs[2] - mins[2]) || 1; + const xMin = mins[0], + xScale = maxs[0] - mins[0] || 1; + const yMin = mins[1], + yScale = maxs[1] - mins[1] || 1; + const zMin = mins[2], + zScale = maxs[2] - mins[2] || 1; // quats: 4 bytes/splat, last byte is the largest-component tag (252-255). const quats = decoder.decodeRGBA(await load(meta.quats.files[0])); @@ -172,18 +181,21 @@ const readSogSourceV2 = async ( restCount = shCoeffs * 3; shCodebook = new Float32Array(meta.shN.codebook); const cen = decoder.decodeRGBA(await load(meta.shN.files[0])); - centroidsRGBA = cen.rgba; cW = cen.width; cH = cen.height; + centroidsRGBA = cen.rgba; + cW = cen.width; + cH = cen.height; const lab = decoder.decodeRGBA(await load(meta.shN.files[1])); labelsRGBA = lab.rgba; if (lab.width * lab.height < count) throw new Error('SOG shN labels texture too small for count'); if (cW !== 64 * shCoeffs) { - throw new Error(`SOG shN centroids texture width ${cW} does not match expected ${64 * shCoeffs} for ${shBands}-band palette`); + throw new Error( + `SOG shN centroids texture width ${cW} does not match expected ${64 * shCoeffs} for ${shBands}-band palette` + ); } paletteCount = meta.shN.count; } const colorSw = 3 + restCount; - const layouts: Partial> = { position: { stride: POSITION_STRIDE, fields: positionFields() }, geometric: { stride: GEOMETRIC_STRIDE, fields: geometricFields() }, @@ -222,7 +234,7 @@ const readSogSourceV2 = async ( const xv = lo[o4] | (hi[o4] << 8); const yv = lo[o4 + 1] | (hi[o4 + 1] << 8); const zv = lo[o4 + 2] | (hi[o4 + 2] << 8); - posOut[o] = invLogTransform(xMin + xScale * (xv / 65535)); + posOut[o] = invLogTransform(xMin + xScale * (xv / 65535)); posOut[o + 1] = invLogTransform(yMin + yScale * (yv / 65535)); posOut[o + 2] = invLogTransform(zMin + zScale * (zv / 65535)); } @@ -231,10 +243,16 @@ const readSogSourceV2 = async ( const o = r * 8; // [rot0..3, scale0..2, opacity] const tag = qr[o4 + 3]; if (tag < 252 || tag > 255) { - geoOut[o] = 1; geoOut[o + 1] = 0; geoOut[o + 2] = 0; geoOut[o + 3] = 0; + geoOut[o] = 1; + geoOut[o + 1] = 0; + geoOut[o + 2] = 0; + geoOut[o + 3] = 0; } else { unpackQuat(qr[o4], qr[o4 + 1], qr[o4 + 2], tag); - geoOut[o] = tmpQuat[0]; geoOut[o + 1] = tmpQuat[1]; geoOut[o + 2] = tmpQuat[2]; geoOut[o + 3] = tmpQuat[3]; + geoOut[o] = tmpQuat[0]; + geoOut[o + 1] = tmpQuat[1]; + geoOut[o + 2] = tmpQuat[2]; + geoOut[o + 3] = tmpQuat[3]; } geoOut[o + 4] = sCode[sl[o4]]; geoOut[o + 5] = sCode[sl[o4 + 1]]; @@ -244,7 +262,7 @@ const readSogSourceV2 = async ( if (colOut) { const o = r * colorSw; // [dc0..2, f_rest_0..N] - colOut[o] = cCode[c0[o4]]; + colOut[o] = cCode[c0[o4]]; colOut[o + 1] = cCode[c0[o4 + 1]]; colOut[o + 2] = cCode[c0[o4 + 2]]; if (restCount > 0) { @@ -263,8 +281,8 @@ const readSogSourceV2 = async ( const lr = ok ? centroidsRGBA![idx] : 0; const lg = ok ? centroidsRGBA![idx + 1] : 0; const lb = ok ? centroidsRGBA![idx + 2] : 0; - colOut[o + 3 + j] = shCodebook![lr] ?? 0; - colOut[o + 3 + j + shCoeffs] = shCodebook![lg] ?? 0; + colOut[o + 3 + j] = shCodebook![lr] ?? 0; + colOut[o + 3 + j + shCoeffs] = shCodebook![lg] ?? 0; colOut[o + 3 + j + shCoeffs * 2] = shCodebook![lb] ?? 0; } } else { @@ -327,13 +345,16 @@ const fetchSogMeta = async (fileSystem: ReadFileSystem, filename: string) => { const statSogSource = async ( fileSystem: ReadFileSystem, filename: string -): Promise | null> => { +): Promise | null> => { const { rawMeta } = await fetchSogMeta(fileSystem, filename); if (rawMeta.version !== 2) return null; const meta = rawMeta as MetaV2; const count = meta.count; const rawBands = meta.shN?.bands ?? 0; - const shBands = ((rawBands === 1 || rawBands === 2 || rawBands === 3) ? rawBands : 0) as SHBands; + const shBands = (rawBands === 1 || rawBands === 2 || rawBands === 3 ? rawBands : 0) as SHBands; return { numGaussians: count, numLods: 1, @@ -391,7 +412,11 @@ const readSogSource = async ( * @returns DataTable with Gaussian splat data. * @ignore */ -const readSog = async (fileSystem: ReadFileSystem, filename: string, options: ReadSogOptions = {}): Promise => { +const readSog = async ( + fileSystem: ReadFileSystem, + filename: string, + options: ReadSogOptions = {} +): Promise => { const { baseDir, resolve, rawMeta } = await fetchSogMeta(fileSystem, filename); const version = rawMeta.version; if (version === undefined) { diff --git a/src/lib/readers/read-splat.ts b/src/lib/readers/read-splat.ts index a5a51f8b..b7081041 100644 --- a/src/lib/readers/read-splat.ts +++ b/src/lib/readers/read-splat.ts @@ -1,22 +1,11 @@ -import { fileChunkSource, readExact, sortGatherSlots, gatherRuns } from './reader-utils'; -import { - type ReadRequest, - type ChunkSource, - type ChunkSourceMetadata, - type ChunkDataPool, - type LayerLayout, - type ChunkLayer, - POSITION_STRIDE, - GEOMETRIC_STRIDE, - colorStride, - positionFields, - geometricFields, - colorFields -} from '../chunk'; +import { POSITION_STRIDE, GEOMETRIC_STRIDE, colorStride, positionFields, geometricFields, colorFields } from '../chunk'; +import type { ReadRequest, ChunkSource, ChunkSourceMetadata, ChunkDataPool, LayerLayout, ChunkLayer } from '../chunk'; import { Column, DataTable } from '../data-table'; -import { type ReadSource } from '../io/read'; +import type { ReadSource } from '../io/read'; import { Transform } from '../utils'; +import { fileChunkSource, readExact, sortGatherSlots, gatherRuns } from './reader-utils'; + const SH_C0 = 0.28209479177387814; // Each Antimatter15 .splat record is 32 bytes: pos(3×f32) scale(3×f32) @@ -47,7 +36,10 @@ const decodeGeometric = (dv: DataView, u8: Uint8Array, o: number, geo: Float32Ar geo[g + 2] = r2 / len; geo[g + 3] = r3 / len; } else { - geo[g] = 1; geo[g + 1] = 0; geo[g + 2] = 0; geo[g + 3] = 0; + geo[g] = 1; + geo[g + 1] = 0; + geo[g + 2] = 0; + geo[g + 3] = 0; } // scale: linear in .splat -> log space geo[g + 4] = Math.log(dv.getFloat32(o + 12, true)); @@ -140,7 +132,12 @@ const readSplat = async (source: ReadSource, pool: ChunkDataPool): Promise number, srcRec: (t: number) => number = t => t): void => { + const decode = ( + buf: Uint8Array, + n: number, + dstRow: (t: number) => number, + srcRec: (t: number) => number = (t) => t + ): void => { const dv = new DataView(buf.buffer, buf.byteOffset, buf.byteLength); for (let t = 0; t < n; t++) { const o = srcRec(t) * BYTES_PER_SPLAT; @@ -156,10 +153,10 @@ const readSplat = async (source: ReadSource, pool: ChunkDataPool): Promise indices[indexOffset + s]); + const slot = sortGatherSlots(count, (s) => indices[indexOffset + s]); const rowAt = (t: number) => indices[indexOffset + slot[t]]; - for (const run of gatherRuns(count, t => rowAt(t) * BYTES_PER_SPLAT, BYTES_PER_SPLAT)) { + for (const run of gatherRuns(count, (t) => rowAt(t) * BYTES_PER_SPLAT, BYTES_PER_SPLAT)) { const firstRow = run.firstByte / BYTES_PER_SPLAT; const byteLen = run.recordCount * BYTES_PER_SPLAT; @@ -171,12 +168,20 @@ const readSplat = async (source: ReadSource, pool: ChunkDataPool): Promise slot[j0 + t], t => rowAt(j0 + t) - firstRow); + decode( + buf, + run.j1 - j0, + (t) => slot[j0 + t], + (t) => rowAt(j0 + t) - firstRow + ); } return; } @@ -195,11 +200,11 @@ const readSplat = async (source: ReadSource, pool: ChunkDataPool): Promise t); + decode(buf, count, (t) => t); }; return fileChunkSource(source, meta, read); @@ -224,11 +229,20 @@ const decodeSplatToDataTable = async (source: ReadSource): Promise => throw new Error('Invalid .splat file: file is empty'); } - const x = new Float32Array(numSplats), y = new Float32Array(numSplats), z = new Float32Array(numSplats); - const s0 = new Float32Array(numSplats), s1 = new Float32Array(numSplats), s2 = new Float32Array(numSplats); - const c0 = new Float32Array(numSplats), c1 = new Float32Array(numSplats), c2 = new Float32Array(numSplats); + const x = new Float32Array(numSplats), + y = new Float32Array(numSplats), + z = new Float32Array(numSplats); + const s0 = new Float32Array(numSplats), + s1 = new Float32Array(numSplats), + s2 = new Float32Array(numSplats); + const c0 = new Float32Array(numSplats), + c1 = new Float32Array(numSplats), + c2 = new Float32Array(numSplats); const op = new Float32Array(numSplats); - const r0 = new Float32Array(numSplats), r1 = new Float32Array(numSplats), r2 = new Float32Array(numSplats), r3 = new Float32Array(numSplats); + const r0 = new Float32Array(numSplats), + r1 = new Float32Array(numSplats), + r2 = new Float32Array(numSplats), + r3 = new Float32Array(numSplats); const dv = new DataView(fileBuffer.buffer, fileBuffer.byteOffset, fileBuffer.byteLength); const pos = new Float32Array(3); @@ -239,19 +253,41 @@ const decodeSplatToDataTable = async (source: ReadSource): Promise => decodePosition(dv, o, pos, 0); decodeGeometric(dv, fileBuffer, o, geo, 0); decodeColor(fileBuffer, o, col, 0); - x[i] = pos[0]; y[i] = pos[1]; z[i] = pos[2]; - r0[i] = geo[0]; r1[i] = geo[1]; r2[i] = geo[2]; r3[i] = geo[3]; - s0[i] = geo[4]; s1[i] = geo[5]; s2[i] = geo[6]; op[i] = geo[7]; - c0[i] = col[0]; c1[i] = col[1]; c2[i] = col[2]; + x[i] = pos[0]; + y[i] = pos[1]; + z[i] = pos[2]; + r0[i] = geo[0]; + r1[i] = geo[1]; + r2[i] = geo[2]; + r3[i] = geo[3]; + s0[i] = geo[4]; + s1[i] = geo[5]; + s2[i] = geo[6]; + op[i] = geo[7]; + c0[i] = col[0]; + c1[i] = col[1]; + c2[i] = col[2]; } - return new DataTable([ - new Column('x', x), new Column('y', y), new Column('z', z), - new Column('scale_0', s0), new Column('scale_1', s1), new Column('scale_2', s2), - new Column('f_dc_0', c0), new Column('f_dc_1', c1), new Column('f_dc_2', c2), - new Column('opacity', op), - new Column('rot_0', r0), new Column('rot_1', r1), new Column('rot_2', r2), new Column('rot_3', r3) - ], Transform.PLY); + return new DataTable( + [ + new Column('x', x), + new Column('y', y), + new Column('z', z), + new Column('scale_0', s0), + new Column('scale_1', s1), + new Column('scale_2', s2), + new Column('f_dc_0', c0), + new Column('f_dc_1', c1), + new Column('f_dc_2', c2), + new Column('opacity', op), + new Column('rot_0', r0), + new Column('rot_1', r1), + new Column('rot_2', r2), + new Column('rot_3', r3) + ], + Transform.PLY + ); }; export { readSplat, decodeSplatToDataTable }; diff --git a/src/lib/readers/read-spz.ts b/src/lib/readers/read-spz.ts index cf169e51..f1dfa6ca 100644 --- a/src/lib/readers/read-spz.ts +++ b/src/lib/readers/read-spz.ts @@ -1,14 +1,6 @@ import { decompress as decompressZstd } from 'fzstd'; -import { fileChunkSource } from './reader-utils'; import { - type ReadRequest, - type ChunkSource, - type ChunkSourceMetadata, - type ChunkDataPool, - type ChunkLayer, - type LayerLayout, - type SHBands, SH_REST_COUNTS, POSITION_STRIDE, GEOMETRIC_STRIDE, @@ -17,9 +9,20 @@ import { geometricFields, colorFields } from '../chunk'; -import { ReadSource } from '../io/read'; +import type { + ReadRequest, + ChunkSource, + ChunkSourceMetadata, + ChunkDataPool, + ChunkLayer, + LayerLayout, + SHBands +} from '../chunk'; +import type { ReadSource } from '../io/read'; import { Transform } from '../utils'; +import { fileChunkSource } from './reader-utils'; + // Niantic Spatial .spz format. Pure-JS, lazy/layered decode (no WASM): v2/v3 are // a single gzip stream with a 16-byte header; v4 is a 32-byte plaintext header + // TOC + per-attribute ZSTD streams. The compact packed attribute bytes are held @@ -88,7 +91,8 @@ const parseSpz = async (source: ReadSource): Promise => { } const magicView = new DataView(fileBuffer.buffer, fileBuffer.byteOffset, MIN_HEADER_SIZE); - if (magicView.getUint32(0, true) !== 0x5053474e) { // 'NGSP' + if (magicView.getUint32(0, true) !== 0x5053474e) { + // 'NGSP' throw new Error('invalid .spz file header'); } @@ -117,10 +121,10 @@ const parseSpz = async (source: ReadSource): Promise => { const shBands = shDegree as SHBands; const harmonicsComponentCount = HARMONICS_COMPONENT_COUNT[shDegree]; - const positionsByteSize = numSplats * 9; // 3 × int24 - const alphasByteSize = numSplats; // u8 - const colorsByteSize = numSplats * 3; // u8 × 3 - const scalesByteSize = numSplats * 3; // u8 × 3 + const positionsByteSize = numSplats * 9; // 3 × int24 + const alphasByteSize = numSplats; // u8 + const colorsByteSize = numSplats * 3; // u8 × 3 + const scalesByteSize = numSplats * 3; // u8 × 3 const rotationsByteSize = numSplats * (version === MIN_SPZ_VERSION ? 3 : 4); const shByteSize = numSplats * harmonicsComponentCount; @@ -172,19 +176,43 @@ const parseSpz = async (source: ReadSource): Promise => { } // v2/v3: the (already-gunzipped) buffer holds the attribute blocks sequentially. - const required = HEADER_SIZE + positionsByteSize + alphasByteSize + colorsByteSize + scalesByteSize + rotationsByteSize + shByteSize; + const required = + HEADER_SIZE + + positionsByteSize + + alphasByteSize + + colorsByteSize + + scalesByteSize + + rotationsByteSize + + shByteSize; if (totalSize < required) { throw new Error(`File too small for .spz payload: expected at least ${required} bytes, got ${totalSize}`); } const buf = fileBuffer.buffer; let off = fileBuffer.byteOffset + HEADER_SIZE; - const positions = new DataView(buf, off, positionsByteSize); off += positionsByteSize; - const alphas = new DataView(buf, off, alphasByteSize); off += alphasByteSize; - const colors = new DataView(buf, off, colorsByteSize); off += colorsByteSize; - const scales = new DataView(buf, off, scalesByteSize); off += scalesByteSize; - const rotations = new DataView(buf, off, rotationsByteSize); off += rotationsByteSize; + const positions = new DataView(buf, off, positionsByteSize); + off += positionsByteSize; + const alphas = new DataView(buf, off, alphasByteSize); + off += alphasByteSize; + const colors = new DataView(buf, off, colorsByteSize); + off += colorsByteSize; + const scales = new DataView(buf, off, scalesByteSize); + off += scalesByteSize; + const rotations = new DataView(buf, off, rotationsByteSize); + off += rotationsByteSize; const sh = new DataView(buf, off, shByteSize); - return { version, numSplats, shBands, fractionalBits, antialiased, positions, alphas, colors, scales, rotations, sh }; + return { + version, + numSplats, + shBands, + fractionalBits, + antialiased, + positions, + alphas, + colors, + scales, + rotations, + sh + }; }; // Reusable scratch for smallest-three quaternion decoding. @@ -253,7 +281,7 @@ const readSpz = async (source: ReadSource, pool: ChunkDataPool): Promise>> 9) & 1; packed >>>= 10; - let v = Math.SQRT1_2 * mag / cMask; + let v = (Math.SQRT1_2 * mag) / cMask; if (neg === 1) v = -v; tmpQuat[j] = v; sumSq += v * v; } } tmpQuat[largest] = Math.sqrt(Math.max(0.0, 1.0 - sumSq)); - geoOut[o] = tmpQuat[3]; // w + geoOut[o] = tmpQuat[3]; // w geoOut[o + 1] = tmpQuat[0]; // x geoOut[o + 2] = tmpQuat[1]; // y geoOut[o + 3] = tmpQuat[2]; // z @@ -308,7 +336,7 @@ const readSpz = async (source: ReadSource, pool: ChunkDataPool): Promise de-interleave to f_rest diff --git a/src/lib/readers/reader-utils.ts b/src/lib/readers/reader-utils.ts index 0fac4a0e..5343e74a 100644 --- a/src/lib/readers/reader-utils.ts +++ b/src/lib/readers/reader-utils.ts @@ -1,5 +1,5 @@ -import { type ChunkSource, type ChunkSourceMetadata, type ReadRequest } from '../chunk'; -import { type ReadSource, type ReadStream } from '../io/read'; +import type { ChunkSource, ChunkSourceMetadata, ReadRequest } from '../chunk'; +import type { ReadSource, ReadStream } from '../io/read'; /** * Read exactly `length` bytes from `stream` into `buffer` at `offset`. A stream @@ -75,7 +75,7 @@ const sortGatherSlots = (count: number, keyOf: (slot: number) => number): Uint32 * files (e.g. data + SH), so the combined scratch stays bounded. * @yields One {@link GatherRun} per coalesced range read. */ -function *gatherRuns( +function* gatherRuns( count: number, byteAt: (t: number) => number, recordBytes: number, @@ -88,7 +88,7 @@ function *gatherRuns( let k = j + 1; while (k < count) { const nextByte = byteAt(k); - if ((nextByte - lastByte) / recordBytes * costBytes > GATHER_MERGE_GAP) break; + if (((nextByte - lastByte) / recordBytes) * costBytes > GATHER_MERGE_GAP) break; if (((nextByte - firstByte) / recordBytes + 1) * costBytes > GATHER_MAX_WINDOW) break; if ((nextByte - firstByte) % recordBytes !== 0) break; lastByte = nextByte; diff --git a/src/lib/render/camera.ts b/src/lib/render/camera.ts index 80683565..8ad4cc0f 100644 --- a/src/lib/render/camera.ts +++ b/src/lib/render/camera.ts @@ -85,11 +85,7 @@ type CameraBasis = { }; const sub = (a: Vec3, b: Vec3) => new Vec3(a.x - b.x, a.y - b.y, a.z - b.z); -const cross = (a: Vec3, b: Vec3) => new Vec3( - a.y * b.z - a.z * b.y, - a.z * b.x - a.x * b.z, - a.x * b.y - a.y * b.x -); +const cross = (a: Vec3, b: Vec3) => new Vec3(a.y * b.z - a.z * b.y, a.z * b.x - a.x * b.z, a.x * b.y - a.y * b.x); const normalize = (v: Vec3): Vec3 => { const len = Math.hypot(v.x, v.y, v.z); if (len === 0) return new Vec3(0, 0, 0); diff --git a/src/lib/render/config.ts b/src/lib/render/config.ts index c43cae36..efa45269 100644 --- a/src/lib/render/config.ts +++ b/src/lib/render/config.ts @@ -111,7 +111,6 @@ export const TILE_SIZE = 16; */ export const POLE_EPS = 0.005; - /** * Total GPU memory budget for ALL pair-sized buffers combined: the * tile-key and splat-value buffers the rasterizer owns (2× pairsCap × 4 B) diff --git a/src/lib/render/preprocess.ts b/src/lib/render/preprocess.ts index f90cee53..7d84509e 100644 --- a/src/lib/render/preprocess.ts +++ b/src/lib/render/preprocess.ts @@ -1,7 +1,9 @@ -import { DataTable, getSHBands } from '../data-table'; -import { type CameraBasis, type Projection } from './camera'; +import type { DataTable } from '../data-table'; +import { getSHBands } from '../data-table'; import { RadixSortScratch, radixSortIndicesByFloat } from '../spatial/radix-sort'; +import type { CameraBasis, Projection } from './camera'; + /** * Number of SH coefficients per color channel for the scene's SH band * count. Matches the `f_rest_*` layout in DataTable (channel-major). @@ -88,10 +90,14 @@ const sortCandidatesByDepth = ( scratch.ensure(count); const { x, y, z } = cols; const { depth, radix } = scratch; - const ex = camera.eye.x, ey = camera.eye.y, ez = camera.eye.z; + const ex = camera.eye.x, + ey = camera.eye.y, + ez = camera.eye.z; if (projection === 'pinhole') { - const fx = camera.forward.x, fy = camera.forward.y, fz = camera.forward.z; + const fx = camera.forward.x, + fy = camera.forward.y, + fz = camera.forward.z; for (let i = 0; i < count; i++) { const s = candidateIndices[i]; depth[i] = fx * (x[s] - ex) + fy * (y[s] - ey) + fz * (z[s] - ez); @@ -191,11 +197,7 @@ const packChunkInput = ( ): void => { const stride = splatInputStride(numSHBands); const numShFloats = numSHCoeffsPerChannel(numSHBands) * 3; - const { - x, y, z, rotW, rotX, rotY, rotZ, - scaleX, scaleY, scaleZ, opacity, - fdcR, fdcG, fdcB, shRest - } = cols; + const { x, y, z, rotW, rotX, rotY, rotZ, scaleX, scaleY, scaleZ, opacity, fdcR, fdcG, fdcB, shRest } = cols; for (let i = 0; i < chunkSize; i++) { const s = chunkIndices[chunkStart + i]; diff --git a/src/lib/render/raster-pass.ts b/src/lib/render/raster-pass.ts index 1332cce9..d7302beb 100644 --- a/src/lib/render/raster-pass.ts +++ b/src/lib/render/raster-pass.ts @@ -1,6 +1,11 @@ -import { GraphicsDevice } from 'playcanvas'; +import type { GraphicsDevice } from 'playcanvas'; -import { type Projection, type RenderCamera, buildCameraBasis } from './camera'; +import type { DataTable } from '../data-table'; +import { GpuSplatRasterizer } from '../gpu'; +import { logger } from '../utils'; + +import { buildCameraBasis } from './camera'; +import type { Projection, RenderCamera } from './camera'; import { AA_DILATION_COV, DISCRIMINANT_FLOOR, @@ -18,9 +23,6 @@ import { sortCandidatesByDepth, splatInputStride } from './preprocess'; -import { DataTable } from '../data-table'; -import { GpuSplatRasterizer } from '../gpu'; -import { logger } from '../utils'; /** * Max gaussians per GPU dispatch. Bounds per-render input/projection @@ -50,15 +52,15 @@ const CHUNK_CAP = 200_000; * sub-frame, and the global CPU depth sort is shared across sub-frames * (no seam artifacts at sub-frame boundaries). */ -const MAX_SUB_FRAME_TILES_X = Math.ceil(1920 / TILE_SIZE); // 120 -const MAX_SUB_FRAME_TILES_Y = Math.ceil(1080 / TILE_SIZE); // 68 +const MAX_SUB_FRAME_TILES_X = Math.ceil(1920 / TILE_SIZE); // 120 +const MAX_SUB_FRAME_TILES_Y = Math.ceil(1080 / TILE_SIZE); // 68 -interface BackgroundRGBA { +type BackgroundRGBA = { r: number; g: number; b: number; a: number; -} +}; /** * Render a splat scene to an RGBA byte buffer. @@ -95,8 +97,12 @@ const renderRasterPass = async ( camera: RenderCamera, background: BackgroundRGBA ): Promise => { - if (!Number.isInteger(camera.width) || !Number.isInteger(camera.height) || - camera.width <= 0 || camera.height <= 0) { + if ( + !Number.isInteger(camera.width) || + !Number.isInteger(camera.height) || + camera.width <= 0 || + camera.height <= 0 + ) { throw new Error(`Invalid resolution: ${camera.width}x${camera.height}`); } @@ -129,8 +135,12 @@ const renderRasterPass = async ( const zCol = dataTable.getColumnByName('z')!.data as Float32Array; const numRows = dataTable.numRows; - const ex = basis.eye.x, ey = basis.eye.y, ez = basis.eye.z; - const fx = basis.forward.x, fy = basis.forward.y, fz = basis.forward.z; + const ex = basis.eye.x, + ey = basis.eye.y, + ez = basis.eye.z; + const fx = basis.forward.x, + fy = basis.forward.y, + fz = basis.forward.z; const near = camera.near; // Worst-case visible-count allocation. Right-sized at the end via subarray. @@ -163,12 +173,8 @@ const renderRasterPass = async ( // max(W/(2π), H/π · 1/POLE_EPS)) for >4K equirect renders. const imageTilesX = Math.ceil(width / TILE_SIZE); const imageTilesY = Math.ceil(height / TILE_SIZE); - const subFrameTilesX = projection === 'equirect' ? - imageTilesX : - Math.min(imageTilesX, MAX_SUB_FRAME_TILES_X); - const subFrameTilesY = projection === 'equirect' ? - imageTilesY : - Math.min(imageTilesY, MAX_SUB_FRAME_TILES_Y); + const subFrameTilesX = projection === 'equirect' ? imageTilesX : Math.min(imageTilesX, MAX_SUB_FRAME_TILES_X); + const subFrameTilesY = projection === 'equirect' ? imageTilesY : Math.min(imageTilesY, MAX_SUB_FRAME_TILES_Y); const numSubFramesX = Math.ceil(imageTilesX / subFrameTilesX); const numSubFramesY = Math.ceil(imageTilesY / subFrameTilesY); const numSubFrames = numSubFramesX * numSubFramesY; @@ -216,14 +222,22 @@ const renderRasterPass = async ( } else { const subFramePixelsX = subFrameTilesX * TILE_SIZE; const subFramePixelsY = subFrameTilesY * TILE_SIZE; - const rx2 = basis.right.x, ry2 = basis.right.y, rz2 = basis.right.z; - const dx2 = basis.down.x, dy2 = basis.down.y, dz2 = basis.down.z; - const focalX = basis.focalX, focalY = basis.focalY; - const halfW = width * 0.5, halfH = height * 0.5; - const sxColRef = cols.scaleX, syColRef = cols.scaleY, szColRef = cols.scaleZ; + const rx2 = basis.right.x, + ry2 = basis.right.y, + rz2 = basis.right.z; + const dx2 = basis.down.x, + dy2 = basis.down.y, + dz2 = basis.down.z; + const focalX = basis.focalX, + focalY = basis.focalY; + const halfW = width * 0.5, + halfH = height * 0.5; + const sxColRef = cols.scaleX, + syColRef = cols.scaleY, + szColRef = cols.scaleZ; // Tan-of-half-FOV cap; matches the project shader's Jacobian clamp. - const limX = JACOBIAN_LIMIT_FACTOR * halfW / focalX; - const limY = JACOBIAN_LIMIT_FACTOR * halfH / focalY; + const limX = (JACOBIAN_LIMIT_FACTOR * halfW) / focalX; + const limY = (JACOBIAN_LIMIT_FACTOR * halfH) / focalY; const limX2 = limX * limX; const limY2 = limY * limY; // Additive squared-radius safety: AA dilation + disc-floor bump. @@ -236,7 +250,7 @@ const renderRasterPass = async ( // sx0 = 0xFFFF sentinel marks "off-screen, skip in pass 2". // Uint16 (not Uint8) so sub-frame counts per axis aren't capped // at 254 by the storage width / sentinel collision. - const SF_OFFSCREEN = 0xFFFF; + const SF_OFFSCREEN = 0xffff; const ranges = new Uint16Array(candidateCount * 4); const subFrameCounts = new Uint32Array(numSubFrames); @@ -254,21 +268,18 @@ const renderRasterPass = async ( const invZ = 1.0 / cz; const screenX = focalX * cx * invZ + halfW; const screenY = focalY * cy * invZ + halfH; - const maxScale = Math.max( - Math.exp(sxColRef[idx]), - Math.exp(syColRef[idx]), - Math.exp(szColRef[idx]) - ); + const maxScale = Math.max(Math.exp(sxColRef[idx]), Math.exp(syColRef[idx]), Math.exp(szColRef[idx])); // Jacobian factor: (focal/cz)² · (1 + tx² + ty²) with // tx = cx/cz, ty = cy/cz both clamped to ±lim. Matches the // project shader's clamp so the bound is tight. const tx = cx * invZ; const ty = cy * invZ; - const txClamped = tx > limX ? limX : (tx < -limX ? -limX : tx); - const tyClamped = ty > limY ? limY : (ty < -limY ? -limY : ty); + const txClamped = tx > limX ? limX : tx < -limX ? -limX : tx; + const tyClamped = ty > limY ? limY : ty < -limY ? -limY : ty; const tx2 = Math.min(txClamped * txClamped, limX2); const ty2 = Math.min(tyClamped * tyClamped, limY2); const jFactorSq = 1 + tx2 + ty2; + // prettier-ignore const lambdaMaxBound = (focalMax * invZ) * (focalMax * invZ) * jFactorSq * maxScale * maxScale + lambdaSafety; // +1 px ceil safety to match the GPU's `ceil(radius)`. const screenR = Math.ceil(SIGMA_CUTOFF * Math.sqrt(lambdaMaxBound)) + 1; @@ -345,11 +356,10 @@ const renderRasterPass = async ( const wgpuLimits = (device as { limits?: { maxStorageBufferBindingSize?: number } }).limits; const maxBindingBytes = wgpuLimits?.maxStorageBufferBindingSize ?? 128 * 1024 * 1024; const bindingChunkCap = Math.floor(maxBindingBytes / (maxCoveragePerSplat * 4)); - const budgetChunkCap = Math.floor(PAIR_BUFFER_BUDGET_BYTES / (maxCoveragePerSplat * PAIR_BUFFER_TOTAL_BYTES_PER_ELEMENT)); - const effectiveChunkCap = Math.max( - 1, - Math.min(CHUNK_CAP, budgetChunkCap, bindingChunkCap, candidateCount) + const budgetChunkCap = Math.floor( + PAIR_BUFFER_BUDGET_BYTES / (maxCoveragePerSplat * PAIR_BUFFER_TOTAL_BYTES_PER_ELEMENT) ); + const effectiveChunkCap = Math.max(1, Math.min(CHUNK_CAP, budgetChunkCap, bindingChunkCap, candidateCount)); const rasterizer = new GpuSplatRasterizer(device, { numSHBands, @@ -431,10 +441,7 @@ const renderRasterPass = async ( for (let row = 0; row < copyH; row++) { const srcOffset = row * subPixelW * 4; const dstOffset = ((subPixelOriginY + row) * width + subPixelOriginX) * 4; - finalImage.set( - subFrameBytes.subarray(srcOffset, srcOffset + copyW * 4), - dstOffset - ); + finalImage.set(subFrameBytes.subarray(srcOffset, srcOffset + copyW * 4), dstOffset); } } } diff --git a/src/lib/source-info.ts b/src/lib/source-info.ts index 17877ca0..cf80f377 100644 --- a/src/lib/source-info.ts +++ b/src/lib/source-info.ts @@ -1,6 +1,7 @@ -import { type ChunkSourceMetadata, hasGaussianLayers, orderedLayers } from './chunk'; -import { type LodStats, type SourceStats } from './ops'; -import { type InputFormat } from './read'; +import { hasGaussianLayers, orderedLayers } from './chunk'; +import type { ChunkSourceMetadata } from './chunk'; +import type { LodStats, SourceStats } from './ops'; +import type { InputFormat } from './read'; import { forwardTransforms } from './value-transforms'; /** @@ -16,8 +17,9 @@ type OutputFormat = 'text' | 'json'; // nested brackets) onto one line so the columnar stat arrays read as table // rows instead of one value per line. const stringifyCompact = (value: unknown): string => { - return JSON.stringify(value, null, 4) - .replace(/\[[^[\]{}]*\]/g, m => m.replace(/\s+/g, ' ').replace('[ ', '[').replace(' ]', ']')); + return JSON.stringify(value, null, 4).replace(/\[[^[\]{}]*\]/g, (m) => + m.replace(/\s+/g, ' ').replace('[ ', '[').replace(' ]', ']') + ); }; /** @@ -38,7 +40,7 @@ const buildSourceInfo = (meta: ChunkSourceMetadata, format?: InputFormat) => ({ shBands: meta.shBands, model: meta.model, layers: orderedLayers(meta.availableLayers), - extraColumns: meta.extraColumns.map(e => ({ name: e.name, type: e.type })) + extraColumns: meta.extraColumns.map((e) => ({ name: e.name, type: e.type })) }); /** @@ -59,7 +61,7 @@ const sourceInfoLines = (meta: ChunkSourceMetadata, format?: InputFormat): strin // untagged scenes say nothing, so existing output is unchanged ...(meta.model === 'default' ? [] : [`model: ${meta.model}`]), `layers: ${orderedLayers(meta.availableLayers).join(', ')}`, - `extra columns: ${meta.extraColumns.length > 0 ? meta.extraColumns.map(e => `${e.name} (${e.type})`).join(', ') : '(none)'}` + `extra columns: ${meta.extraColumns.length > 0 ? meta.extraColumns.map((e) => `${e.name} (${e.type})`).join(', ') : '(none)'}` ]; }; @@ -74,7 +76,11 @@ const sourceInfoLines = (meta: ChunkSourceMetadata, format?: InputFormat): strin * @param sourceFormat - Detected input format; reported when provided. * @returns A text or JSON block for `logger.output`. */ -const formatSourceInfo = (meta: ChunkSourceMetadata, format: OutputFormat = 'text', sourceFormat?: InputFormat): string => { +const formatSourceInfo = ( + meta: ChunkSourceMetadata, + format: OutputFormat = 'text', + sourceFormat?: InputFormat +): string => { if (format === 'json') { return stringifyCompact(buildSourceInfo(meta, sourceFormat)); } @@ -96,10 +102,12 @@ const BARS = '▁▂▃▄▅▆▇█'; // all-blank for an empty column). const sparkline = (counts: number[]): string => { const maxBin = Math.max(...counts); - return counts.map((c) => { - if (c === 0) return ' '; - return BARS[maxBin > 0 ? Math.floor(c / maxBin * (BARS.length - 1)) : 0]; - }).join(''); + return counts + .map((c) => { + if (c === 0) return ' '; + return BARS[maxBin > 0 ? Math.floor((c / maxBin) * (BARS.length - 1)) : 0]; + }) + .join(''); }; // Map a LOD's stats to display space for JSON output: value arrays through the @@ -136,12 +144,12 @@ const statsTable = (lod: LodStats): string[] => { ]); const colWidths = headers.map((header, colIndex) => { - const dataWidths = rows.map(row => row[colIndex].length); + const dataWidths = rows.map((row) => row[colIndex].length); return Math.max(header.length, ...dataWidths); }); const padRow = (cells: string[]) => `| ${cells.map((cell, i) => cell.padEnd(colWidths[i])).join(' | ')} |`; - const separator = `|${colWidths.map(w => '-'.repeat(w + 2)).join('|')}|`; + const separator = `|${colWidths.map((w) => '-'.repeat(w + 2)).join('|')}|`; return [padRow(headers), separator, ...rows.map(padRow)]; }; @@ -158,7 +166,12 @@ const statsTable = (lod: LodStats): string[] => { * @param sourceFormat - Detected input format; reported when provided. * @returns A text or JSON block for `logger.output`. */ -const formatSourceStats = (meta: ChunkSourceMetadata, stats: SourceStats, format: OutputFormat = 'text', sourceFormat?: InputFormat): string => { +const formatSourceStats = ( + meta: ChunkSourceMetadata, + stats: SourceStats, + format: OutputFormat = 'text', + sourceFormat?: InputFormat +): string => { if (format === 'json') { return stringifyCompact({ ...buildSourceInfo(meta, sourceFormat), diff --git a/src/lib/spatial/b-tree.ts b/src/lib/spatial/b-tree.ts index 98172a5a..6cc63155 100644 --- a/src/lib/spatial/b-tree.ts +++ b/src/lib/spatial/b-tree.ts @@ -1,4 +1,4 @@ -import { DataTable } from '../data-table'; +import type { DataTable } from '../data-table'; import { quickselect } from '../utils'; class Aabb { @@ -49,13 +49,13 @@ class Aabb { } } -interface BTreeNode { +type BTreeNode = { count: number; aabb: Aabb; - indices?: Uint32Array; // only for leaf nodes + indices?: Uint32Array; // only for leaf nodes left?: BTreeNode; right?: BTreeNode; -} +}; class BTree { centroids: DataTable; diff --git a/src/lib/spatial/gaussian-bvh.ts b/src/lib/spatial/gaussian-bvh.ts index 8318c1bd..8327866f 100644 --- a/src/lib/spatial/gaussian-bvh.ts +++ b/src/lib/spatial/gaussian-bvh.ts @@ -1,24 +1,24 @@ -import { Vec3 } from 'playcanvas'; +import type { Vec3 } from 'playcanvas'; -import { DataTable, type TypedArray } from '../data-table'; +import type { DataTable, TypedArray } from '../data-table'; import { quickselect } from '../utils'; /** * Axis-aligned bounding box for BVH nodes. */ -interface BVHBounds { +type BVHBounds = { minX: number; minY: number; minZ: number; maxX: number; maxY: number; maxZ: number; -} +}; /** * BVH node for Gaussian AABBs. */ -interface GaussianBVHNode { +type GaussianBVHNode = { /** Number of Gaussians in this subtree */ count: number; @@ -33,7 +33,7 @@ interface GaussianBVHNode { /** Right child (only for interior nodes) */ right?: GaussianBVHNode; -} +}; /** * Check if two AABBs overlap. @@ -47,10 +47,16 @@ interface GaussianBVHNode { * @param bMaxZ - Second AABB maximum Z * @returns True if the AABBs overlap */ -const boundsOverlap = (a: BVHBounds, bMinX: number, bMinY: number, bMinZ: number, bMaxX: number, bMaxY: number, bMaxZ: number): boolean => { - return !(a.maxX < bMinX || a.minX > bMaxX || - a.maxY < bMinY || a.minY > bMaxY || - a.maxZ < bMinZ || a.minZ > bMaxZ); +const boundsOverlap = ( + a: BVHBounds, + bMinX: number, + bMinY: number, + bMinZ: number, + bMaxX: number, + bMaxY: number, + bMaxZ: number +): boolean => { + return !(a.maxX < bMinX || a.minX > bMaxX || a.maxY < bMinY || a.minY > bMaxY || a.maxZ < bMinZ || a.minZ > bMaxZ); }; /** @@ -156,9 +162,12 @@ class GaussianBVH { // Find the largest axis to split on (based on centroid positions for better balance) const { x, y, z } = this; - let centroidMinX = Infinity, centroidMaxX = -Infinity; - let centroidMinY = Infinity, centroidMaxY = -Infinity; - let centroidMinZ = Infinity, centroidMaxZ = -Infinity; + let centroidMinX = Infinity, + centroidMaxX = -Infinity; + let centroidMinY = Infinity, + centroidMaxY = -Infinity; + let centroidMinZ = Infinity, + centroidMaxZ = -Infinity; for (let i = 0; i < indices.length; i++) { const idx = indices[i]; @@ -251,11 +260,7 @@ class GaussianBVH { * @param offset - Starting element offset in `result`. * @returns Number of matching Gaussian indices. */ - queryFrustumRawInto( - planes: Float32Array, - result: Uint32Array, - offset: number - ): number { + queryFrustumRawInto(planes: Float32Array, result: Uint32Array, offset: number): number { return this.queryFrustumNode(this.root, planes, result, offset, 0); } @@ -411,9 +416,7 @@ class GaussianBVH { const gMaxZ = z[idx] + extentZ[idx]; // Check overlap - if (!(gMaxX < minX || gMinX > maxX || - gMaxY < minY || gMinY > maxY || - gMaxZ < minZ || gMinZ > maxZ)) { + if (!(gMaxX < minX || gMinX > maxX || gMaxY < minY || gMinY > maxY || gMaxZ < minZ || gMinZ > maxZ)) { result.push(idx); } } @@ -472,9 +475,7 @@ class GaussianBVH { const gMaxY = y[idx] + extentY[idx]; const gMaxZ = z[idx] + extentZ[idx]; - if (!(gMaxX < minX || gMinX > maxX || - gMaxY < minY || gMinY > maxY || - gMaxZ < minZ || gMinZ > maxZ)) { + if (!(gMaxX < minX || gMinX > maxX || gMaxY < minY || gMinY > maxY || gMaxZ < minZ || gMinZ > maxZ)) { const writeIdx = offset + count; if (writeIdx < result.length) { result[writeIdx] = idx; diff --git a/src/lib/spatial/k-means.ts b/src/lib/spatial/k-means.ts index 620bfad0..20c2150d 100644 --- a/src/lib/spatial/k-means.ts +++ b/src/lib/spatial/k-means.ts @@ -1,9 +1,10 @@ -import { GraphicsDevice } from 'playcanvas'; +import type { GraphicsDevice } from 'playcanvas'; -import { KdTree } from './kd-tree'; import { GpuKmeans } from '../gpu'; import { logger } from '../utils'; +import { KdTree } from './kd-tree'; + // use floyd's algorithm to pick m unique random indices from 0..n-1 const pickRandomIndices = (n: number, m: number) => { const chosen = new Set(); @@ -38,7 +39,14 @@ const initCentroids1D = (points: Float32Array, numRows: number, centroids: Float // CPU assignment fallback (no GPU): build a kd-tree over the centroids and find // each point's nearest. points/centroids are row-major interleaved. -const assignCpu = (points: Float32Array, numRows: number, nc: number, centroids: Float32Array, k: number, labels: Uint32Array) => { +const assignCpu = ( + points: Float32Array, + numRows: number, + nc: number, + centroids: Float32Array, + k: number, + labels: Uint32Array +) => { // de-interleave centroids into columns for the KdTree (k is small) const cols: Float32Array[] = []; for (let j = 0; j < nc; ++j) { @@ -76,7 +84,7 @@ const kmeansInterleaved = async ( k: number, iterations: number, device?: GraphicsDevice -): Promise<{ centroids: Float32Array, labels: Uint32Array }> => { +): Promise<{ centroids: Float32Array; labels: Uint32Array }> => { const nc = numColumns; // too few data points: each point is its own centroid diff --git a/src/lib/spatial/kd-tree.ts b/src/lib/spatial/kd-tree.ts index d494ac19..c2f5de4e 100644 --- a/src/lib/spatial/kd-tree.ts +++ b/src/lib/spatial/kd-tree.ts @@ -11,12 +11,12 @@ * arbitrary for k-means centroid assignment. `buildFlatKdTree` is 3D only. */ -interface KdTreeNode { +type KdTreeNode = { index: number; - count: number; // self + children indices + count: number; // self + children indices left?: KdTreeNode; right?: KdTreeNode; -} +}; /** * A kd-tree in GPU-friendly flat typed arrays (the exact layout GpuKnn @@ -37,7 +37,9 @@ type FlatKdTree = { const nthElement = (arr: Uint32Array, lo: number, hi: number, k: number, values: ArrayLike) => { while (lo < hi) { const mid = (lo + hi) >> 1; - const va = values[arr[lo]], vb = values[arr[mid]], vc = values[arr[hi]]; + const va = values[arr[lo]], + vb = values[arr[mid]], + vc = values[arr[hi]]; let pivotIdx: number; if ((vb - va) * (vc - vb) >= 0) pivotIdx = mid; else if ((va - vb) * (vc - va) >= 0) pivotIdx = lo; @@ -51,15 +53,22 @@ const nthElement = (arr: Uint32Array, lo: number, hi: number, k: number, values: // elements, so an all-equal range shrank by one per pass and degenerated // to O(N^2) — fatal for inputs where many points share a coordinate // (e.g. a splat with every gaussian at the origin). - let lt = lo, gt = hi, i = lo; + let lt = lo, + gt = hi, + i = lo; let tmp: number; while (i <= gt) { const v = values[arr[i]]; if (v < pivotVal) { - tmp = arr[i]; arr[i] = arr[lt]; arr[lt] = tmp; - lt++; i++; + tmp = arr[i]; + arr[i] = arr[lt]; + arr[lt] = tmp; + lt++; + i++; } else if (v > pivotVal) { - tmp = arr[i]; arr[i] = arr[gt]; arr[gt] = tmp; + tmp = arr[i]; + arr[i] = arr[gt]; + arr[gt] = tmp; gt--; } else { i++; @@ -96,7 +105,7 @@ const buildFlatKdTree = (x: Float32Array, y: Float32Array, z: Float32Array): Fla const nodeSplatIdx = new Uint32Array(n); const nodePositions = new Float32Array(n * 3); - const nodeChildren = new Uint32Array(n * 2).fill(0xFFFFFFFF); + const nodeChildren = new Uint32Array(n * 2).fill(0xffffffff); let cursor = 0; const emit = (splat: number): number => { @@ -118,7 +127,9 @@ const buildFlatKdTree = (x: Float32Array, y: Float32Array, z: Float32Array): Fla if (count === 2) { if (values[indices[lo]] > values[indices[hi]]) { - const tmp = indices[lo]; indices[lo] = indices[hi]; indices[hi] = tmp; + const tmp = indices[lo]; + indices[lo] = indices[hi]; + indices[hi] = tmp; } const t = emit(indices[lo]); nodeChildren[t * 2 + 1] = emit(indices[hi]); @@ -164,7 +175,9 @@ class KdTree { if (count === 2) { if (values[indices[lo]] > values[indices[hi]]) { - const tmp = indices[lo]; indices[lo] = indices[hi]; indices[hi] = tmp; + const tmp = indices[lo]; + indices[lo] = indices[hi]; + indices[hi] = tmp; } return { index: indices[lo], @@ -202,7 +215,7 @@ class KdTree { const recurse = (node: KdTreeNode, axis: number) => { const distance = point[axis] - colData[axis][node.index]; - const next = (distance > 0) ? node.right : node.left; + const next = distance > 0 ? node.right : node.left; const nextAxis = axis + 1 < numCols ? axis + 1 : 0; cnt++; @@ -259,8 +272,12 @@ class KdTree { while (pos > 0) { const parent = (pos - 1) >> 1; if (heapDist[parent] < heapDist[pos]) { - const td = heapDist[parent]; heapDist[parent] = heapDist[pos]; heapDist[pos] = td; - const ti = heapIdx[parent]; heapIdx[parent] = heapIdx[pos]; heapIdx[pos] = ti; + const td = heapDist[parent]; + heapDist[parent] = heapDist[pos]; + heapDist[pos] = td; + const ti = heapIdx[parent]; + heapIdx[parent] = heapIdx[pos]; + heapIdx[pos] = ti; pos = parent; } else { break; @@ -277,8 +294,12 @@ class KdTree { if (left < k && heapDist[left] > heapDist[largest]) largest = left; if (right < k && heapDist[right] > heapDist[largest]) largest = right; if (largest === pos) break; - const td = heapDist[pos]; heapDist[pos] = heapDist[largest]; heapDist[largest] = td; - const ti = heapIdx[pos]; heapIdx[pos] = heapIdx[largest]; heapIdx[largest] = ti; + const td = heapDist[pos]; + heapDist[pos] = heapDist[largest]; + heapDist[largest] = td; + const ti = heapIdx[pos]; + heapIdx[pos] = heapIdx[largest]; + heapIdx[largest] = ti; pos = largest; } } @@ -286,7 +307,7 @@ class KdTree { const recurse = (node: KdTreeNode, axis: number) => { const distance = point[axis] - colData[axis][node.index]; - const next = (distance > 0) ? node.right : node.left; + const next = distance > 0 ? node.right : node.left; const nextAxis = axis + 1 < numCols ? axis + 1 : 0; if (next) { diff --git a/src/lib/spatial/quantize-1d-core.ts b/src/lib/spatial/quantize-1d-core.ts index b56837ce..72ead47c 100644 --- a/src/lib/spatial/quantize-1d-core.ts +++ b/src/lib/spatial/quantize-1d-core.ts @@ -8,7 +8,7 @@ import type { TypedArray } from '../data-table/data-table'; */ type QuantizedColumns = { centroids: Float32Array; - labels: { name: string, data: Uint8Array }[]; + labels: { name: string; data: Uint8Array }[]; }; // The histogram + DP core over sorted finite values: returns up to kTarget @@ -41,7 +41,7 @@ const quantizeFinite = (sortedData: Float32Array, N: number, kTarget: number, al const centers = new Float64Array(H); for (let i = 0; i < H; ++i) { - centers[i] = counts[i] > 0 ? sums[i] / counts[i] : vMin + (i + 0.5) / H * vRange; + centers[i] = counts[i] > 0 ? sums[i] / counts[i] : vMin + ((i + 0.5) / H) * vRange; } // compute weights: w = count^alpha (sub-linear density weighting) @@ -175,7 +175,7 @@ const quantizeFinite = (sortedData: Float32Array, N: number, kTarget: number, al * `labels` (same column layout as input, each holding Uint8Array indices * into the codebook). */ -const quantize1dColumns = (columns: { name: string, data: TypedArray }[], k = 256, alpha = 0.5): QuantizedColumns => { +const quantize1dColumns = (columns: { name: string; data: TypedArray }[], k = 256, alpha = 0.5): QuantizedColumns => { const numColumns = columns.length; const numRows = numColumns > 0 ? columns[0].data.length : 0; @@ -185,7 +185,7 @@ const quantize1dColumns = (columns: { name: string, data: TypedArray }[], k = 25 if (N === 0) { return { centroids: new Float32Array(k), - labels: columns.map(c => ({ name: c.name, data: new Uint8Array(numRows) })) + labels: columns.map((c) => ({ name: c.name, data: new Uint8Array(numRows) })) }; } @@ -229,7 +229,7 @@ const quantize1dColumns = (columns: { name: string, data: TypedArray }[], k = 25 return { centroids, - labels: columns.map(c => ({ name: c.name, data: new Uint8Array(numRows) })) + labels: columns.map((c) => ({ name: c.name, data: new Uint8Array(numRows) })) }; } @@ -255,7 +255,7 @@ const quantize1dColumns = (columns: { name: string, data: TypedArray }[], k = 25 // padded with the last][+Inf sentinel] const finalCentroids = new Float32Array(k); finalCentroids.set(centroidValues, loSlots); - const padValue = effectiveK > 0 ? centroidValues[effectiveK - 1] : (hasNegInf ? negInfCentroid : posInfCentroid); + const padValue = effectiveK > 0 ? centroidValues[effectiveK - 1] : hasNegInf ? negInfCentroid : posInfCentroid; for (let i = loSlots + effectiveK; i < k - hiSlots; ++i) { finalCentroids[i] = padValue; } diff --git a/src/lib/spatial/radix-sort.ts b/src/lib/spatial/radix-sort.ts index c8fb637a..44dcff17 100644 --- a/src/lib/spatial/radix-sort.ts +++ b/src/lib/spatial/radix-sort.ts @@ -39,7 +39,7 @@ class RadixSortScratch { * as comparator-sorting the original Float32s ascending. */ const encodeFloatKey = (bits: number): number => { - return (bits & 0x80000000) !== 0 ? (~bits >>> 0) : ((bits ^ 0x80000000) >>> 0); + return (bits & 0x80000000) !== 0 ? ~bits >>> 0 : (bits ^ 0x80000000) >>> 0; }; /** @@ -84,8 +84,10 @@ const radixSortIndicesByFloat = ( // 4-pass LSD radix sort, 8 bits per pass. After an even number of passes // (4) the sorted output is back in the originally-supplied indices buffer. - let kIn = keysWork, iIn = indices; - let kOut = keysAlt, iOut = indicesAlt; + let kIn = keysWork, + iIn = indices; + let kOut = keysAlt, + iOut = indicesAlt; for (let shift = 0; shift < 32; shift += 8) { counts.fill(0); @@ -107,8 +109,12 @@ const radixSortIndicesByFloat = ( iOut[pos] = iIn[i]; } // Swap input/output. - const tk = kIn; kIn = kOut; kOut = tk; - const ti = iIn; iIn = iOut; iOut = ti; + const tk = kIn; + kIn = kOut; + kOut = tk; + const ti = iIn; + iIn = iOut; + iOut = ti; } }; @@ -120,7 +126,7 @@ const radixSortIndicesByFloat = ( * @returns true if the value is NaN or infinite. */ const isFloatBitsNonFinite = (bits: number): boolean => { - return (bits & 0x7F800000) === 0x7F800000; + return (bits & 0x7f800000) === 0x7f800000; }; export { RadixSortScratch, radixSortIndicesByFloat, isFloatBitsNonFinite }; diff --git a/src/lib/splat-model.ts b/src/lib/splat-model.ts index e0eba79f..dbb29d1a 100644 --- a/src/lib/splat-model.ts +++ b/src/lib/splat-model.ts @@ -11,7 +11,7 @@ type SplatModel = 'default' | 'antialiased' | '2dgs'; /** Every model value, for validating externally-supplied strings. */ -const SPLAT_MODELS: ReadonlyArray = ['default', 'antialiased', '2dgs']; +const SPLAT_MODELS: readonly SplatModel[] = ['default', 'antialiased', '2dgs']; /** * Narrow an externally-supplied string to a {@link SplatModel}. Per-format @@ -34,7 +34,7 @@ const isSplatModel = (value: unknown): value is SplatModel => { */ const resolveSplatModel = (models: SplatModel[]): SplatModel => { const first = models[0] ?? 'default'; - return models.every(m => m === first) ? first : 'default'; + return models.every((m) => m === first) ? first : 'default'; }; export { type SplatModel, isSplatModel, resolveSplatModel }; diff --git a/src/lib/spz-module.ts b/src/lib/spz-module.ts index c8b09eec..c40a6940 100644 --- a/src/lib/spz-module.ts +++ b/src/lib/spz-module.ts @@ -1,5 +1,5 @@ import { Column, DataTable, convertToSpace } from './data-table'; -import { type SplatModel } from './splat-model'; +import type { SplatModel } from './splat-model'; import { Transform } from './utils'; type CoordinateSystem = { @@ -56,7 +56,7 @@ const getCreateSpzModule = async () => { const getSpzModule = (): Promise => { if (!spzModulePromise) { - spzModulePromise = getCreateSpzModule().then(createModule => createModule()); + spzModulePromise = getCreateSpzModule().then((createModule) => createModule()); } return spzModulePromise; }; @@ -70,7 +70,7 @@ const getShColumnCount = (dataTable: DataTable) => { }; const getShDegreeFromCount = (count: number) => { - const degree = SPZ_SH_COMPONENTS.indexOf(count as typeof SPZ_SH_COMPONENTS[number]); + const degree = SPZ_SH_COMPONENTS.indexOf(count as (typeof SPZ_SH_COMPONENTS)[number]); if (degree === -1) { throw new Error(`Unsupported SH coefficient count for SPZ: ${count}`); } @@ -252,10 +252,4 @@ const makeSpzPackOptions = async (overrides: Partial = {}): Promise }; }; -export { - SPZ_SH_COMPONENTS, - dataTableToGaussianCloud, - gaussianCloudToDataTable, - getSpzModule, - makeSpzPackOptions -}; +export { SPZ_SH_COMPONENTS, dataTableToGaussianCloud, gaussianCloudToDataTable, getSpzModule, makeSpzPackOptions }; diff --git a/src/lib/stats.ts b/src/lib/stats.ts index 8441edba..9f59b9a1 100644 --- a/src/lib/stats.ts +++ b/src/lib/stats.ts @@ -1,7 +1,9 @@ -import { type ChunkDataPool, type ChunkSource, createChunkDataPool } from './chunk'; +import { createChunkDataPool } from './chunk'; +import type { ChunkDataPool, ChunkSource } from './chunk'; import { dataTableToChunkSource } from './compat/data-table'; import { DataTable } from './data-table'; -import { computeSourceStats, type SourceStats } from './ops'; +import { computeSourceStats } from './ops'; +import type { SourceStats } from './ops'; /** * Compute per-LOD, per-column statistics for splat data in a single streaming diff --git a/src/lib/types.ts b/src/lib/types.ts index 122923d5..3e483a58 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -158,6 +158,7 @@ type Param = { * * @returns Promise resolving to a GraphicsDevice instance. */ -type DeviceCreator = () => Promise; +type DeviceCreator = () => Promise; export type { CollisionMeshShape, Options, Param, DeviceCreator }; +import type { GraphicsDevice } from 'playcanvas'; diff --git a/src/lib/utils/logger.ts b/src/lib/utils/logger.ts index 40db8c71..f565bfc8 100644 --- a/src/lib/utils/logger.ts +++ b/src/lib/utils/logger.ts @@ -30,10 +30,26 @@ type MessageKind = 'error' | 'warn' | 'info' | 'debug'; */ type LogEvent = | { kind: 'scopeStart'; depth: number; name: string; index?: number; total?: number } - | { kind: 'scopeEnd'; depth: number; name: string; durationMs: number; failed?: boolean; index?: number; total?: number } + | { + kind: 'scopeEnd'; + depth: number; + name: string; + durationMs: number; + failed?: boolean; + index?: number; + total?: number; + } | { kind: 'barStart'; depth: number; name: string; total: number } | { kind: 'barTick'; depth: number; name: string; current: number; total: number } - | { kind: 'barEnd'; depth: number; name: string; durationMs: number; current: number; total: number; failed?: boolean } + | { + kind: 'barEnd'; + depth: number; + name: string; + durationMs: number; + current: number; + total: number; + failed?: boolean; + } | { kind: 'message'; depth: number; level: MessageKind; text: string } | { kind: 'output'; text: string }; @@ -52,6 +68,7 @@ type LogEvent = * at the façade (see {@link LoggerCore.isLevelVisible}) before reaching * the renderer; `error` is always delivered. */ +// eslint-disable-next-line @typescript-eslint/consistent-type-definitions -- preserve public declaration merging interface Renderer { /** * Handle a log event. @@ -74,6 +91,7 @@ interface Renderer { * can adopt `using bar = logger.bar(...)` because `using` only requires * the `[Symbol.dispose]` shape structurally. */ +// eslint-disable-next-line @typescript-eslint/consistent-type-definitions -- preserve public declaration merging interface Bar { /** * Advance the bar by `n` ticks. @@ -115,6 +133,7 @@ interface Bar { * can adopt `using g = logger.group(...)` because `using` only requires * the `[Symbol.dispose]` shape structurally. */ +// eslint-disable-next-line @typescript-eslint/consistent-type-definitions -- preserve public declaration merging interface Group { /** * Close the group, popping anything still open above it on the stack @@ -142,16 +161,18 @@ const now = (): number => { }; const fmtArgs = (args: any[]): string => { - return args.map((a) => { - if (a instanceof Error) return a.stack ?? a.message; - if (typeof a === 'string') return a; - if (typeof a === 'number' || typeof a === 'boolean' || a == null) return String(a); - try { - return JSON.stringify(a); - } catch { - return String(a); - } - }).join(' '); + return args + .map((a) => { + if (a instanceof Error) return a.stack ?? a.message; + if (typeof a === 'string') return a; + if (typeof a === 'number' || typeof a === 'boolean' || a == null) return String(a); + try { + return JSON.stringify(a); + } catch { + return String(a); + } + }) + .join(' '); }; /** @@ -161,7 +182,9 @@ const fmtArgs = (args: any[]): string => { * every event silently. */ class NullRenderer implements Renderer { - handle(_event: LogEvent): void { /* no-op */ } + handle(_event: LogEvent): void { + /* no-op */ + } } const verbosityRank: Record = { @@ -254,9 +277,8 @@ class LoggerCore { }); return; } - const numbering = top.index !== undefined && top.total !== undefined ? - { index: top.index, total: top.total } : - {}; + const numbering = + top.index !== undefined && top.total !== undefined ? { index: top.index, total: top.total } : {}; this.emit({ kind: 'scopeEnd', depth: top.depth, name: top.name, durationMs, failed, ...numbering }); } @@ -278,9 +300,9 @@ class LoggerCore { } const depth = this.stack.length; const numbering = index !== undefined && total !== undefined ? { index, total } : undefined; - const scope: Scope = numbering ? - { kind: 'group', name, depth, start: now(), index: numbering.index, total: numbering.total } : - { kind: 'group', name, depth, start: now() }; + const scope: Scope = numbering + ? { kind: 'group', name, depth, start: now(), index: numbering.index, total: numbering.total } + : { kind: 'group', name, depth, start: now() }; this.stack.push(scope); this.emit({ kind: 'scopeStart', depth, name, ...(numbering ?? {}) }); return this.makeGroup(scope); @@ -336,7 +358,13 @@ class LoggerCore { if (next === scope.current) return; scope.current = next; if (!isTopOfStack()) return; - this.emit({ kind: 'barTick', depth: scope.depth, name: scope.name, current: scope.current, total: scope.total }); + this.emit({ + kind: 'barTick', + depth: scope.depth, + name: scope.name, + current: scope.current, + total: scope.total + }); }, update: (current: number) => { if (closed) return; @@ -348,7 +376,13 @@ class LoggerCore { if (next === scope.current) return; scope.current = next; if (!isTopOfStack()) return; - this.emit({ kind: 'barTick', depth: scope.depth, name: scope.name, current: scope.current, total: scope.total }); + this.emit({ + kind: 'barTick', + depth: scope.depth, + name: scope.name, + current: scope.current, + total: scope.total + }); }, end: () => { if (closed) return; diff --git a/src/lib/utils/math.ts b/src/lib/utils/math.ts index 121c860b..6060e2e8 100644 --- a/src/lib/utils/math.ts +++ b/src/lib/utils/math.ts @@ -1,4 +1,5 @@ -import { Mat4, Quat, Vec3 } from 'playcanvas'; +import type { Mat4 } from 'playcanvas'; +import { Quat, Vec3 } from 'playcanvas'; const sigmoid = (v: number) => 1 / (1 + Math.exp(-v)); diff --git a/src/lib/utils/quickselect.ts b/src/lib/utils/quickselect.ts index 4a4f624b..42ceb0df 100644 --- a/src/lib/utils/quickselect.ts +++ b/src/lib/utils/quickselect.ts @@ -1,4 +1,4 @@ -import { TypedArray } from '../data-table'; +import type { TypedArray } from '../data-table'; /** * Partition indices around the k-th smallest element using quickselect diff --git a/src/lib/utils/rotate-sh.ts b/src/lib/utils/rotate-sh.ts index 679f2675..23a3fd36 100644 --- a/src/lib/utils/rotate-sh.ts +++ b/src/lib/utils/rotate-sh.ts @@ -1,33 +1,31 @@ -import { Mat3 } from 'playcanvas'; +import type { Mat3 } from 'playcanvas'; -/* eslint-disable indent */ - -const kSqrt03_02 = Math.sqrt(3.0 / 2.0); -const kSqrt01_03 = Math.sqrt(1.0 / 3.0); -const kSqrt02_03 = Math.sqrt(2.0 / 3.0); -const kSqrt04_03 = Math.sqrt(4.0 / 3.0); -const kSqrt01_04 = Math.sqrt(1.0 / 4.0); -const kSqrt03_04 = Math.sqrt(3.0 / 4.0); -const kSqrt01_05 = Math.sqrt(1.0 / 5.0); -const kSqrt03_05 = Math.sqrt(3.0 / 5.0); -const kSqrt06_05 = Math.sqrt(6.0 / 5.0); -const kSqrt08_05 = Math.sqrt(8.0 / 5.0); -const kSqrt09_05 = Math.sqrt(9.0 / 5.0); -const kSqrt01_06 = Math.sqrt(1.0 / 6.0); -const kSqrt05_06 = Math.sqrt(5.0 / 6.0); -const kSqrt03_08 = Math.sqrt(3.0 / 8.0); -const kSqrt05_08 = Math.sqrt(5.0 / 8.0); -const kSqrt09_08 = Math.sqrt(9.0 / 8.0); -const kSqrt05_09 = Math.sqrt(5.0 / 9.0); -const kSqrt08_09 = Math.sqrt(8.0 / 9.0); -const kSqrt01_10 = Math.sqrt(1.0 / 10.0); -const kSqrt03_10 = Math.sqrt(3.0 / 10.0); -const kSqrt01_12 = Math.sqrt(1.0 / 12.0); -const kSqrt04_15 = Math.sqrt(4.0 / 15.0); -const kSqrt01_16 = Math.sqrt(1.0 / 16.0); -const kSqrt15_16 = Math.sqrt(15.0 / 16.0); -const kSqrt01_18 = Math.sqrt(1.0 / 18.0); -const kSqrt01_60 = Math.sqrt(1.0 / 60.0); +const kSqrt03_02 = Math.sqrt(3.0 / 2.0); +const kSqrt01_03 = Math.sqrt(1.0 / 3.0); +const kSqrt02_03 = Math.sqrt(2.0 / 3.0); +const kSqrt04_03 = Math.sqrt(4.0 / 3.0); +const kSqrt01_04 = Math.sqrt(1.0 / 4.0); +const kSqrt03_04 = Math.sqrt(3.0 / 4.0); +const kSqrt01_05 = Math.sqrt(1.0 / 5.0); +const kSqrt03_05 = Math.sqrt(3.0 / 5.0); +const kSqrt06_05 = Math.sqrt(6.0 / 5.0); +const kSqrt08_05 = Math.sqrt(8.0 / 5.0); +const kSqrt09_05 = Math.sqrt(9.0 / 5.0); +const kSqrt01_06 = Math.sqrt(1.0 / 6.0); +const kSqrt05_06 = Math.sqrt(5.0 / 6.0); +const kSqrt03_08 = Math.sqrt(3.0 / 8.0); +const kSqrt05_08 = Math.sqrt(5.0 / 8.0); +const kSqrt09_08 = Math.sqrt(9.0 / 8.0); +const kSqrt05_09 = Math.sqrt(5.0 / 9.0); +const kSqrt08_09 = Math.sqrt(8.0 / 9.0); +const kSqrt01_10 = Math.sqrt(1.0 / 10.0); +const kSqrt03_10 = Math.sqrt(3.0 / 10.0); +const kSqrt01_12 = Math.sqrt(1.0 / 12.0); +const kSqrt04_15 = Math.sqrt(4.0 / 15.0); +const kSqrt01_16 = Math.sqrt(1.0 / 16.0); +const kSqrt15_16 = Math.sqrt(15.0 / 16.0); +const kSqrt01_18 = Math.sqrt(1.0 / 18.0); +const kSqrt01_60 = Math.sqrt(1.0 / 60.0); const dp = (n: number, start: number, a: number[] | Float32Array, b: number[] | Float32Array) => { let sum = 0; @@ -96,96 +94,194 @@ class RotateSH { ]; // band 2 - const sh2 = [[ - kSqrt01_04 * ((sh1[2][2] * sh1[0][0] + sh1[2][0] * sh1[0][2]) + (sh1[0][2] * sh1[2][0] + sh1[0][0] * sh1[2][2])), - (sh1[2][1] * sh1[0][0] + sh1[0][1] * sh1[2][0]), - kSqrt03_04 * (sh1[2][1] * sh1[0][1] + sh1[0][1] * sh1[2][1]), - (sh1[2][1] * sh1[0][2] + sh1[0][1] * sh1[2][2]), - kSqrt01_04 * ((sh1[2][2] * sh1[0][2] - sh1[2][0] * sh1[0][0]) + (sh1[0][2] * sh1[2][2] - sh1[0][0] * sh1[2][0])) - ], [ - kSqrt01_04 * ((sh1[1][2] * sh1[0][0] + sh1[1][0] * sh1[0][2]) + (sh1[0][2] * sh1[1][0] + sh1[0][0] * sh1[1][2])), - sh1[1][1] * sh1[0][0] + sh1[0][1] * sh1[1][0], - kSqrt03_04 * (sh1[1][1] * sh1[0][1] + sh1[0][1] * sh1[1][1]), - sh1[1][1] * sh1[0][2] + sh1[0][1] * sh1[1][2], - kSqrt01_04 * ((sh1[1][2] * sh1[0][2] - sh1[1][0] * sh1[0][0]) + (sh1[0][2] * sh1[1][2] - sh1[0][0] * sh1[1][0])) - ], [ - kSqrt01_03 * (sh1[1][2] * sh1[1][0] + sh1[1][0] * sh1[1][2]) - kSqrt01_12 * ((sh1[2][2] * sh1[2][0] + sh1[2][0] * sh1[2][2]) + (sh1[0][2] * sh1[0][0] + sh1[0][0] * sh1[0][2])), - kSqrt04_03 * sh1[1][1] * sh1[1][0] - kSqrt01_03 * (sh1[2][1] * sh1[2][0] + sh1[0][1] * sh1[0][0]), - sh1[1][1] * sh1[1][1] - kSqrt01_04 * (sh1[2][1] * sh1[2][1] + sh1[0][1] * sh1[0][1]), - kSqrt04_03 * sh1[1][1] * sh1[1][2] - kSqrt01_03 * (sh1[2][1] * sh1[2][2] + sh1[0][1] * sh1[0][2]), - kSqrt01_03 * (sh1[1][2] * sh1[1][2] - sh1[1][0] * sh1[1][0]) - kSqrt01_12 * ((sh1[2][2] * sh1[2][2] - sh1[2][0] * sh1[2][0]) + (sh1[0][2] * sh1[0][2] - sh1[0][0] * sh1[0][0])) - ], [ - kSqrt01_04 * ((sh1[1][2] * sh1[2][0] + sh1[1][0] * sh1[2][2]) + (sh1[2][2] * sh1[1][0] + sh1[2][0] * sh1[1][2])), - sh1[1][1] * sh1[2][0] + sh1[2][1] * sh1[1][0], - kSqrt03_04 * (sh1[1][1] * sh1[2][1] + sh1[2][1] * sh1[1][1]), - sh1[1][1] * sh1[2][2] + sh1[2][1] * sh1[1][2], - kSqrt01_04 * ((sh1[1][2] * sh1[2][2] - sh1[1][0] * sh1[2][0]) + (sh1[2][2] * sh1[1][2] - sh1[2][0] * sh1[1][0])) - ], [ - kSqrt01_04 * ((sh1[2][2] * sh1[2][0] + sh1[2][0] * sh1[2][2]) - (sh1[0][2] * sh1[0][0] + sh1[0][0] * sh1[0][2])), - (sh1[2][1] * sh1[2][0] - sh1[0][1] * sh1[0][0]), - kSqrt03_04 * (sh1[2][1] * sh1[2][1] - sh1[0][1] * sh1[0][1]), - (sh1[2][1] * sh1[2][2] - sh1[0][1] * sh1[0][2]), - kSqrt01_04 * ((sh1[2][2] * sh1[2][2] - sh1[2][0] * sh1[2][0]) - (sh1[0][2] * sh1[0][2] - sh1[0][0] * sh1[0][0])) - ]]; + const sh2 = [ + [ + kSqrt01_04 * + (sh1[2][2] * sh1[0][0] + sh1[2][0] * sh1[0][2] + (sh1[0][2] * sh1[2][0] + sh1[0][0] * sh1[2][2])), + sh1[2][1] * sh1[0][0] + sh1[0][1] * sh1[2][0], + kSqrt03_04 * (sh1[2][1] * sh1[0][1] + sh1[0][1] * sh1[2][1]), + sh1[2][1] * sh1[0][2] + sh1[0][1] * sh1[2][2], + kSqrt01_04 * + (sh1[2][2] * sh1[0][2] - sh1[2][0] * sh1[0][0] + (sh1[0][2] * sh1[2][2] - sh1[0][0] * sh1[2][0])) + ], + [ + kSqrt01_04 * + (sh1[1][2] * sh1[0][0] + sh1[1][0] * sh1[0][2] + (sh1[0][2] * sh1[1][0] + sh1[0][0] * sh1[1][2])), + sh1[1][1] * sh1[0][0] + sh1[0][1] * sh1[1][0], + kSqrt03_04 * (sh1[1][1] * sh1[0][1] + sh1[0][1] * sh1[1][1]), + sh1[1][1] * sh1[0][2] + sh1[0][1] * sh1[1][2], + kSqrt01_04 * + (sh1[1][2] * sh1[0][2] - sh1[1][0] * sh1[0][0] + (sh1[0][2] * sh1[1][2] - sh1[0][0] * sh1[1][0])) + ], + [ + kSqrt01_03 * (sh1[1][2] * sh1[1][0] + sh1[1][0] * sh1[1][2]) - + kSqrt01_12 * + (sh1[2][2] * sh1[2][0] + + sh1[2][0] * sh1[2][2] + + (sh1[0][2] * sh1[0][0] + sh1[0][0] * sh1[0][2])), + kSqrt04_03 * sh1[1][1] * sh1[1][0] - kSqrt01_03 * (sh1[2][1] * sh1[2][0] + sh1[0][1] * sh1[0][0]), + sh1[1][1] * sh1[1][1] - kSqrt01_04 * (sh1[2][1] * sh1[2][1] + sh1[0][1] * sh1[0][1]), + kSqrt04_03 * sh1[1][1] * sh1[1][2] - kSqrt01_03 * (sh1[2][1] * sh1[2][2] + sh1[0][1] * sh1[0][2]), + kSqrt01_03 * (sh1[1][2] * sh1[1][2] - sh1[1][0] * sh1[1][0]) - + kSqrt01_12 * + (sh1[2][2] * sh1[2][2] - + sh1[2][0] * sh1[2][0] + + (sh1[0][2] * sh1[0][2] - sh1[0][0] * sh1[0][0])) + ], + [ + kSqrt01_04 * + (sh1[1][2] * sh1[2][0] + sh1[1][0] * sh1[2][2] + (sh1[2][2] * sh1[1][0] + sh1[2][0] * sh1[1][2])), + sh1[1][1] * sh1[2][0] + sh1[2][1] * sh1[1][0], + kSqrt03_04 * (sh1[1][1] * sh1[2][1] + sh1[2][1] * sh1[1][1]), + sh1[1][1] * sh1[2][2] + sh1[2][1] * sh1[1][2], + kSqrt01_04 * + (sh1[1][2] * sh1[2][2] - sh1[1][0] * sh1[2][0] + (sh1[2][2] * sh1[1][2] - sh1[2][0] * sh1[1][0])) + ], + [ + kSqrt01_04 * + (sh1[2][2] * sh1[2][0] + sh1[2][0] * sh1[2][2] - (sh1[0][2] * sh1[0][0] + sh1[0][0] * sh1[0][2])), + sh1[2][1] * sh1[2][0] - sh1[0][1] * sh1[0][0], + kSqrt03_04 * (sh1[2][1] * sh1[2][1] - sh1[0][1] * sh1[0][1]), + sh1[2][1] * sh1[2][2] - sh1[0][1] * sh1[0][2], + kSqrt01_04 * + (sh1[2][2] * sh1[2][2] - sh1[2][0] * sh1[2][0] - (sh1[0][2] * sh1[0][2] - sh1[0][0] * sh1[0][0])) + ] + ]; // band 3 - const sh3 = [[ - kSqrt01_04 * ((sh1[2][2] * sh2[0][0] + sh1[2][0] * sh2[0][4]) + (sh1[0][2] * sh2[4][0] + sh1[0][0] * sh2[4][4])), - kSqrt03_02 * (sh1[2][1] * sh2[0][0] + sh1[0][1] * sh2[4][0]), - kSqrt15_16 * (sh1[2][1] * sh2[0][1] + sh1[0][1] * sh2[4][1]), - kSqrt05_06 * (sh1[2][1] * sh2[0][2] + sh1[0][1] * sh2[4][2]), - kSqrt15_16 * (sh1[2][1] * sh2[0][3] + sh1[0][1] * sh2[4][3]), - kSqrt03_02 * (sh1[2][1] * sh2[0][4] + sh1[0][1] * sh2[4][4]), - kSqrt01_04 * ((sh1[2][2] * sh2[0][4] - sh1[2][0] * sh2[0][0]) + (sh1[0][2] * sh2[4][4] - sh1[0][0] * sh2[4][0])) - ], [ - kSqrt01_06 * (sh1[1][2] * sh2[0][0] + sh1[1][0] * sh2[0][4]) + kSqrt01_06 * ((sh1[2][2] * sh2[1][0] + sh1[2][0] * sh2[1][4]) + (sh1[0][2] * sh2[3][0] + sh1[0][0] * sh2[3][4])), - sh1[1][1] * sh2[0][0] + (sh1[2][1] * sh2[1][0] + sh1[0][1] * sh2[3][0]), - kSqrt05_08 * sh1[1][1] * sh2[0][1] + kSqrt05_08 * (sh1[2][1] * sh2[1][1] + sh1[0][1] * sh2[3][1]), - kSqrt05_09 * sh1[1][1] * sh2[0][2] + kSqrt05_09 * (sh1[2][1] * sh2[1][2] + sh1[0][1] * sh2[3][2]), - kSqrt05_08 * sh1[1][1] * sh2[0][3] + kSqrt05_08 * (sh1[2][1] * sh2[1][3] + sh1[0][1] * sh2[3][3]), - sh1[1][1] * sh2[0][4] + (sh1[2][1] * sh2[1][4] + sh1[0][1] * sh2[3][4]), - kSqrt01_06 * (sh1[1][2] * sh2[0][4] - sh1[1][0] * sh2[0][0]) + kSqrt01_06 * ((sh1[2][2] * sh2[1][4] - sh1[2][0] * sh2[1][0]) + (sh1[0][2] * sh2[3][4] - sh1[0][0] * sh2[3][0])) - ], [ - kSqrt04_15 * (sh1[1][2] * sh2[1][0] + sh1[1][0] * sh2[1][4]) + kSqrt01_05 * (sh1[0][2] * sh2[2][0] + sh1[0][0] * sh2[2][4]) - kSqrt01_60 * ((sh1[2][2] * sh2[0][0] + sh1[2][0] * sh2[0][4]) - (sh1[0][2] * sh2[4][0] + sh1[0][0] * sh2[4][4])), - kSqrt08_05 * sh1[1][1] * sh2[1][0] + kSqrt06_05 * sh1[0][1] * sh2[2][0] - kSqrt01_10 * (sh1[2][1] * sh2[0][0] - sh1[0][1] * sh2[4][0]), - sh1[1][1] * sh2[1][1] + kSqrt03_04 * sh1[0][1] * sh2[2][1] - kSqrt01_16 * (sh1[2][1] * sh2[0][1] - sh1[0][1] * sh2[4][1]), - kSqrt08_09 * sh1[1][1] * sh2[1][2] + kSqrt02_03 * sh1[0][1] * sh2[2][2] - kSqrt01_18 * (sh1[2][1] * sh2[0][2] - sh1[0][1] * sh2[4][2]), - sh1[1][1] * sh2[1][3] + kSqrt03_04 * sh1[0][1] * sh2[2][3] - kSqrt01_16 * (sh1[2][1] * sh2[0][3] - sh1[0][1] * sh2[4][3]), - kSqrt08_05 * sh1[1][1] * sh2[1][4] + kSqrt06_05 * sh1[0][1] * sh2[2][4] - kSqrt01_10 * (sh1[2][1] * sh2[0][4] - sh1[0][1] * sh2[4][4]), - kSqrt04_15 * (sh1[1][2] * sh2[1][4] - sh1[1][0] * sh2[1][0]) + kSqrt01_05 * (sh1[0][2] * sh2[2][4] - sh1[0][0] * sh2[2][0]) - kSqrt01_60 * ((sh1[2][2] * sh2[0][4] - sh1[2][0] * sh2[0][0]) - (sh1[0][2] * sh2[4][4] - sh1[0][0] * sh2[4][0])) - ], [ - kSqrt03_10 * (sh1[1][2] * sh2[2][0] + sh1[1][0] * sh2[2][4]) - kSqrt01_10 * ((sh1[2][2] * sh2[3][0] + sh1[2][0] * sh2[3][4]) + (sh1[0][2] * sh2[1][0] + sh1[0][0] * sh2[1][4])), - kSqrt09_05 * sh1[1][1] * sh2[2][0] - kSqrt03_05 * (sh1[2][1] * sh2[3][0] + sh1[0][1] * sh2[1][0]), - kSqrt09_08 * sh1[1][1] * sh2[2][1] - kSqrt03_08 * (sh1[2][1] * sh2[3][1] + sh1[0][1] * sh2[1][1]), - sh1[1][1] * sh2[2][2] - kSqrt01_03 * (sh1[2][1] * sh2[3][2] + sh1[0][1] * sh2[1][2]), - kSqrt09_08 * sh1[1][1] * sh2[2][3] - kSqrt03_08 * (sh1[2][1] * sh2[3][3] + sh1[0][1] * sh2[1][3]), - kSqrt09_05 * sh1[1][1] * sh2[2][4] - kSqrt03_05 * (sh1[2][1] * sh2[3][4] + sh1[0][1] * sh2[1][4]), - kSqrt03_10 * (sh1[1][2] * sh2[2][4] - sh1[1][0] * sh2[2][0]) - kSqrt01_10 * ((sh1[2][2] * sh2[3][4] - sh1[2][0] * sh2[3][0]) + (sh1[0][2] * sh2[1][4] - sh1[0][0] * sh2[1][0])) - ], [ - kSqrt04_15 * (sh1[1][2] * sh2[3][0] + sh1[1][0] * sh2[3][4]) + kSqrt01_05 * (sh1[2][2] * sh2[2][0] + sh1[2][0] * sh2[2][4]) - kSqrt01_60 * ((sh1[2][2] * sh2[4][0] + sh1[2][0] * sh2[4][4]) + (sh1[0][2] * sh2[0][0] + sh1[0][0] * sh2[0][4])), - kSqrt08_05 * sh1[1][1] * sh2[3][0] + kSqrt06_05 * sh1[2][1] * sh2[2][0] - kSqrt01_10 * (sh1[2][1] * sh2[4][0] + sh1[0][1] * sh2[0][0]), - sh1[1][1] * sh2[3][1] + kSqrt03_04 * sh1[2][1] * sh2[2][1] - kSqrt01_16 * (sh1[2][1] * sh2[4][1] + sh1[0][1] * sh2[0][1]), - kSqrt08_09 * sh1[1][1] * sh2[3][2] + kSqrt02_03 * sh1[2][1] * sh2[2][2] - kSqrt01_18 * (sh1[2][1] * sh2[4][2] + sh1[0][1] * sh2[0][2]), - sh1[1][1] * sh2[3][3] + kSqrt03_04 * sh1[2][1] * sh2[2][3] - kSqrt01_16 * (sh1[2][1] * sh2[4][3] + sh1[0][1] * sh2[0][3]), - kSqrt08_05 * sh1[1][1] * sh2[3][4] + kSqrt06_05 * sh1[2][1] * sh2[2][4] - kSqrt01_10 * (sh1[2][1] * sh2[4][4] + sh1[0][1] * sh2[0][4]), - kSqrt04_15 * (sh1[1][2] * sh2[3][4] - sh1[1][0] * sh2[3][0]) + kSqrt01_05 * (sh1[2][2] * sh2[2][4] - sh1[2][0] * sh2[2][0]) - kSqrt01_60 * ((sh1[2][2] * sh2[4][4] - sh1[2][0] * sh2[4][0]) + (sh1[0][2] * sh2[0][4] - sh1[0][0] * sh2[0][0])) - ], [ - kSqrt01_06 * (sh1[1][2] * sh2[4][0] + sh1[1][0] * sh2[4][4]) + kSqrt01_06 * ((sh1[2][2] * sh2[3][0] + sh1[2][0] * sh2[3][4]) - (sh1[0][2] * sh2[1][0] + sh1[0][0] * sh2[1][4])), - sh1[1][1] * sh2[4][0] + (sh1[2][1] * sh2[3][0] - sh1[0][1] * sh2[1][0]), - kSqrt05_08 * sh1[1][1] * sh2[4][1] + kSqrt05_08 * (sh1[2][1] * sh2[3][1] - sh1[0][1] * sh2[1][1]), - kSqrt05_09 * sh1[1][1] * sh2[4][2] + kSqrt05_09 * (sh1[2][1] * sh2[3][2] - sh1[0][1] * sh2[1][2]), - kSqrt05_08 * sh1[1][1] * sh2[4][3] + kSqrt05_08 * (sh1[2][1] * sh2[3][3] - sh1[0][1] * sh2[1][3]), - sh1[1][1] * sh2[4][4] + (sh1[2][1] * sh2[3][4] - sh1[0][1] * sh2[1][4]), - kSqrt01_06 * (sh1[1][2] * sh2[4][4] - sh1[1][0] * sh2[4][0]) + kSqrt01_06 * ((sh1[2][2] * sh2[3][4] - sh1[2][0] * sh2[3][0]) - (sh1[0][2] * sh2[1][4] - sh1[0][0] * sh2[1][0])) - ], [ - kSqrt01_04 * ((sh1[2][2] * sh2[4][0] + sh1[2][0] * sh2[4][4]) - (sh1[0][2] * sh2[0][0] + sh1[0][0] * sh2[0][4])), - kSqrt03_02 * (sh1[2][1] * sh2[4][0] - sh1[0][1] * sh2[0][0]), - kSqrt15_16 * (sh1[2][1] * sh2[4][1] - sh1[0][1] * sh2[0][1]), - kSqrt05_06 * (sh1[2][1] * sh2[4][2] - sh1[0][1] * sh2[0][2]), - kSqrt15_16 * (sh1[2][1] * sh2[4][3] - sh1[0][1] * sh2[0][3]), - kSqrt03_02 * (sh1[2][1] * sh2[4][4] - sh1[0][1] * sh2[0][4]), - kSqrt01_04 * ((sh1[2][2] * sh2[4][4] - sh1[2][0] * sh2[4][0]) - (sh1[0][2] * sh2[0][4] - sh1[0][0] * sh2[0][0])) - ]]; + const sh3 = [ + [ + kSqrt01_04 * + (sh1[2][2] * sh2[0][0] + sh1[2][0] * sh2[0][4] + (sh1[0][2] * sh2[4][0] + sh1[0][0] * sh2[4][4])), + kSqrt03_02 * (sh1[2][1] * sh2[0][0] + sh1[0][1] * sh2[4][0]), + kSqrt15_16 * (sh1[2][1] * sh2[0][1] + sh1[0][1] * sh2[4][1]), + kSqrt05_06 * (sh1[2][1] * sh2[0][2] + sh1[0][1] * sh2[4][2]), + kSqrt15_16 * (sh1[2][1] * sh2[0][3] + sh1[0][1] * sh2[4][3]), + kSqrt03_02 * (sh1[2][1] * sh2[0][4] + sh1[0][1] * sh2[4][4]), + kSqrt01_04 * + (sh1[2][2] * sh2[0][4] - sh1[2][0] * sh2[0][0] + (sh1[0][2] * sh2[4][4] - sh1[0][0] * sh2[4][0])) + ], + [ + kSqrt01_06 * (sh1[1][2] * sh2[0][0] + sh1[1][0] * sh2[0][4]) + + kSqrt01_06 * + (sh1[2][2] * sh2[1][0] + + sh1[2][0] * sh2[1][4] + + (sh1[0][2] * sh2[3][0] + sh1[0][0] * sh2[3][4])), + sh1[1][1] * sh2[0][0] + (sh1[2][1] * sh2[1][0] + sh1[0][1] * sh2[3][0]), + kSqrt05_08 * sh1[1][1] * sh2[0][1] + kSqrt05_08 * (sh1[2][1] * sh2[1][1] + sh1[0][1] * sh2[3][1]), + kSqrt05_09 * sh1[1][1] * sh2[0][2] + kSqrt05_09 * (sh1[2][1] * sh2[1][2] + sh1[0][1] * sh2[3][2]), + kSqrt05_08 * sh1[1][1] * sh2[0][3] + kSqrt05_08 * (sh1[2][1] * sh2[1][3] + sh1[0][1] * sh2[3][3]), + sh1[1][1] * sh2[0][4] + (sh1[2][1] * sh2[1][4] + sh1[0][1] * sh2[3][4]), + kSqrt01_06 * (sh1[1][2] * sh2[0][4] - sh1[1][0] * sh2[0][0]) + + kSqrt01_06 * + (sh1[2][2] * sh2[1][4] - + sh1[2][0] * sh2[1][0] + + (sh1[0][2] * sh2[3][4] - sh1[0][0] * sh2[3][0])) + ], + [ + kSqrt04_15 * (sh1[1][2] * sh2[1][0] + sh1[1][0] * sh2[1][4]) + + kSqrt01_05 * (sh1[0][2] * sh2[2][0] + sh1[0][0] * sh2[2][4]) - + kSqrt01_60 * + (sh1[2][2] * sh2[0][0] + + sh1[2][0] * sh2[0][4] - + (sh1[0][2] * sh2[4][0] + sh1[0][0] * sh2[4][4])), + kSqrt08_05 * sh1[1][1] * sh2[1][0] + + kSqrt06_05 * sh1[0][1] * sh2[2][0] - + kSqrt01_10 * (sh1[2][1] * sh2[0][0] - sh1[0][1] * sh2[4][0]), + sh1[1][1] * sh2[1][1] + + kSqrt03_04 * sh1[0][1] * sh2[2][1] - + kSqrt01_16 * (sh1[2][1] * sh2[0][1] - sh1[0][1] * sh2[4][1]), + kSqrt08_09 * sh1[1][1] * sh2[1][2] + + kSqrt02_03 * sh1[0][1] * sh2[2][2] - + kSqrt01_18 * (sh1[2][1] * sh2[0][2] - sh1[0][1] * sh2[4][2]), + sh1[1][1] * sh2[1][3] + + kSqrt03_04 * sh1[0][1] * sh2[2][3] - + kSqrt01_16 * (sh1[2][1] * sh2[0][3] - sh1[0][1] * sh2[4][3]), + kSqrt08_05 * sh1[1][1] * sh2[1][4] + + kSqrt06_05 * sh1[0][1] * sh2[2][4] - + kSqrt01_10 * (sh1[2][1] * sh2[0][4] - sh1[0][1] * sh2[4][4]), + kSqrt04_15 * (sh1[1][2] * sh2[1][4] - sh1[1][0] * sh2[1][0]) + + kSqrt01_05 * (sh1[0][2] * sh2[2][4] - sh1[0][0] * sh2[2][0]) - + kSqrt01_60 * + (sh1[2][2] * sh2[0][4] - + sh1[2][0] * sh2[0][0] - + (sh1[0][2] * sh2[4][4] - sh1[0][0] * sh2[4][0])) + ], + [ + kSqrt03_10 * (sh1[1][2] * sh2[2][0] + sh1[1][0] * sh2[2][4]) - + kSqrt01_10 * + (sh1[2][2] * sh2[3][0] + + sh1[2][0] * sh2[3][4] + + (sh1[0][2] * sh2[1][0] + sh1[0][0] * sh2[1][4])), + kSqrt09_05 * sh1[1][1] * sh2[2][0] - kSqrt03_05 * (sh1[2][1] * sh2[3][0] + sh1[0][1] * sh2[1][0]), + kSqrt09_08 * sh1[1][1] * sh2[2][1] - kSqrt03_08 * (sh1[2][1] * sh2[3][1] + sh1[0][1] * sh2[1][1]), + sh1[1][1] * sh2[2][2] - kSqrt01_03 * (sh1[2][1] * sh2[3][2] + sh1[0][1] * sh2[1][2]), + kSqrt09_08 * sh1[1][1] * sh2[2][3] - kSqrt03_08 * (sh1[2][1] * sh2[3][3] + sh1[0][1] * sh2[1][3]), + kSqrt09_05 * sh1[1][1] * sh2[2][4] - kSqrt03_05 * (sh1[2][1] * sh2[3][4] + sh1[0][1] * sh2[1][4]), + kSqrt03_10 * (sh1[1][2] * sh2[2][4] - sh1[1][0] * sh2[2][0]) - + kSqrt01_10 * + (sh1[2][2] * sh2[3][4] - + sh1[2][0] * sh2[3][0] + + (sh1[0][2] * sh2[1][4] - sh1[0][0] * sh2[1][0])) + ], + [ + kSqrt04_15 * (sh1[1][2] * sh2[3][0] + sh1[1][0] * sh2[3][4]) + + kSqrt01_05 * (sh1[2][2] * sh2[2][0] + sh1[2][0] * sh2[2][4]) - + kSqrt01_60 * + (sh1[2][2] * sh2[4][0] + + sh1[2][0] * sh2[4][4] + + (sh1[0][2] * sh2[0][0] + sh1[0][0] * sh2[0][4])), + kSqrt08_05 * sh1[1][1] * sh2[3][0] + + kSqrt06_05 * sh1[2][1] * sh2[2][0] - + kSqrt01_10 * (sh1[2][1] * sh2[4][0] + sh1[0][1] * sh2[0][0]), + sh1[1][1] * sh2[3][1] + + kSqrt03_04 * sh1[2][1] * sh2[2][1] - + kSqrt01_16 * (sh1[2][1] * sh2[4][1] + sh1[0][1] * sh2[0][1]), + kSqrt08_09 * sh1[1][1] * sh2[3][2] + + kSqrt02_03 * sh1[2][1] * sh2[2][2] - + kSqrt01_18 * (sh1[2][1] * sh2[4][2] + sh1[0][1] * sh2[0][2]), + sh1[1][1] * sh2[3][3] + + kSqrt03_04 * sh1[2][1] * sh2[2][3] - + kSqrt01_16 * (sh1[2][1] * sh2[4][3] + sh1[0][1] * sh2[0][3]), + kSqrt08_05 * sh1[1][1] * sh2[3][4] + + kSqrt06_05 * sh1[2][1] * sh2[2][4] - + kSqrt01_10 * (sh1[2][1] * sh2[4][4] + sh1[0][1] * sh2[0][4]), + kSqrt04_15 * (sh1[1][2] * sh2[3][4] - sh1[1][0] * sh2[3][0]) + + kSqrt01_05 * (sh1[2][2] * sh2[2][4] - sh1[2][0] * sh2[2][0]) - + kSqrt01_60 * + (sh1[2][2] * sh2[4][4] - + sh1[2][0] * sh2[4][0] + + (sh1[0][2] * sh2[0][4] - sh1[0][0] * sh2[0][0])) + ], + [ + kSqrt01_06 * (sh1[1][2] * sh2[4][0] + sh1[1][0] * sh2[4][4]) + + kSqrt01_06 * + (sh1[2][2] * sh2[3][0] + + sh1[2][0] * sh2[3][4] - + (sh1[0][2] * sh2[1][0] + sh1[0][0] * sh2[1][4])), + sh1[1][1] * sh2[4][0] + (sh1[2][1] * sh2[3][0] - sh1[0][1] * sh2[1][0]), + kSqrt05_08 * sh1[1][1] * sh2[4][1] + kSqrt05_08 * (sh1[2][1] * sh2[3][1] - sh1[0][1] * sh2[1][1]), + kSqrt05_09 * sh1[1][1] * sh2[4][2] + kSqrt05_09 * (sh1[2][1] * sh2[3][2] - sh1[0][1] * sh2[1][2]), + kSqrt05_08 * sh1[1][1] * sh2[4][3] + kSqrt05_08 * (sh1[2][1] * sh2[3][3] - sh1[0][1] * sh2[1][3]), + sh1[1][1] * sh2[4][4] + (sh1[2][1] * sh2[3][4] - sh1[0][1] * sh2[1][4]), + kSqrt01_06 * (sh1[1][2] * sh2[4][4] - sh1[1][0] * sh2[4][0]) + + kSqrt01_06 * + (sh1[2][2] * sh2[3][4] - + sh1[2][0] * sh2[3][0] - + (sh1[0][2] * sh2[1][4] - sh1[0][0] * sh2[1][0])) + ], + [ + kSqrt01_04 * + (sh1[2][2] * sh2[4][0] + sh1[2][0] * sh2[4][4] - (sh1[0][2] * sh2[0][0] + sh1[0][0] * sh2[0][4])), + kSqrt03_02 * (sh1[2][1] * sh2[4][0] - sh1[0][1] * sh2[0][0]), + kSqrt15_16 * (sh1[2][1] * sh2[4][1] - sh1[0][1] * sh2[0][1]), + kSqrt05_06 * (sh1[2][1] * sh2[4][2] - sh1[0][1] * sh2[0][2]), + kSqrt15_16 * (sh1[2][1] * sh2[4][3] - sh1[0][1] * sh2[0][3]), + kSqrt03_02 * (sh1[2][1] * sh2[4][4] - sh1[0][1] * sh2[0][4]), + kSqrt01_04 * + (sh1[2][2] * sh2[4][4] - sh1[2][0] * sh2[4][0] - (sh1[0][2] * sh2[0][4] - sh1[0][0] * sh2[0][0])) + ] + ]; if (isAxisAligned(rot)) { const { counts, indices, values } = buildSparse(sh1, sh2, sh3); @@ -257,8 +353,8 @@ class RotateSH { if (result.length < 15) { return; } - result[8] = dp(7, 8, src, sh3[0]); - result[9] = dp(7, 8, src, sh3[1]); + result[8] = dp(7, 8, src, sh3[0]); + result[9] = dp(7, 8, src, sh3[1]); result[10] = dp(7, 8, src, sh3[2]); result[11] = dp(7, 8, src, sh3[3]); result[12] = dp(7, 8, src, sh3[4]); diff --git a/src/lib/utils/text-renderer.ts b/src/lib/utils/text-renderer.ts index 5eec8fbb..e3f92116 100644 --- a/src/lib/utils/text-renderer.ts +++ b/src/lib/utils/text-renderer.ts @@ -1,9 +1,11 @@ import { fmtBytes, fmtTime } from './fmt'; -import { logger, verbosityRank, type LogEvent, type Renderer } from './logger'; +import { logger, verbosityRank } from './logger'; +import type { LogEvent, Renderer } from './logger'; /** * Output streams and optional memory-usage probe for {@link TextRenderer}. */ +// eslint-disable-next-line @typescript-eslint/consistent-type-definitions -- preserve public declaration merging interface TextRendererOptions { /** * Receives all status chunks (scopes, bars, messages). May contain @@ -122,8 +124,8 @@ class TextRenderer implements Renderer { case 'scopeStart': { if (this.rank() < verbosityRank.normal) return; this.commitDirty(); - const numbered = event.index !== undefined && event.total !== undefined ? - `[${event.index}/${event.total}] ` : ''; + const numbered = + event.index !== undefined && event.total !== undefined ? `[${event.index}/${event.total}] ` : ''; this.write(`${indent(event.depth)}\u25b8 ${numbered}${event.name}\n`); return; } @@ -150,8 +152,8 @@ class TextRenderer implements Renderer { case 'barTick': { if (!this.lineDirty) return; if (this.rank() < verbosityRank.normal) return; - const target = event.total <= 0 ? 0 : - Math.min(BAR_WIDTH, Math.floor((event.current / event.total) * BAR_WIDTH)); + const target = + event.total <= 0 ? 0 : Math.min(BAR_WIDTH, Math.floor((event.current / event.total) * BAR_WIDTH)); if (target > this.barFilled) { this.write('#'.repeat(target - this.barFilled)); this.barFilled = target; @@ -160,9 +162,9 @@ class TextRenderer implements Renderer { } case 'barEnd': { if (this.rank() < verbosityRank.normal) return; - const suffix = event.failed ? - `] (failed) ${fmtTime(event.durationMs)}` : - `] ${fmtTime(event.durationMs)}`; + const suffix = event.failed + ? `] (failed) ${fmtTime(event.durationMs)}` + : `] ${fmtTime(event.durationMs)}`; if (this.lineDirty) { const remaining = Math.max(0, BAR_WIDTH - this.barFilled); this.write(`${'.'.repeat(remaining)}${suffix}${this.memSuffix()}\n`); @@ -173,8 +175,10 @@ class TextRenderer implements Renderer { // event (e.g. a child group/message). Emit a recap // line whose fill reflects actual progress, so bars // that ended early or failed don't read as complete. - const filled = event.total <= 0 ? 0 : - Math.min(BAR_WIDTH, Math.floor((event.current / event.total) * BAR_WIDTH)); + const filled = + event.total <= 0 + ? 0 + : Math.min(BAR_WIDTH, Math.floor((event.current / event.total) * BAR_WIDTH)); const bar = `${'#'.repeat(filled)}${'.'.repeat(BAR_WIDTH - filled)}`; this.write(`${indent(event.depth)}\u25b8 ${event.name} [${bar}${suffix}${this.memSuffix()}\n`); } diff --git a/src/lib/utils/webp-codec.ts b/src/lib/utils/webp-codec.ts index 155b77ac..18521100 100644 --- a/src/lib/utils/webp-codec.ts +++ b/src/lib/utils/webp-codec.ts @@ -79,12 +79,14 @@ class WebPCodec { const bytes = Module.HEAPU8.slice(outPtr, outPtr + outSize); Module._webp_free(outPtr); - Module._free(inPtr); Module._free(outPtrPtr); Module._free(outSizePtr); + Module._free(inPtr); + Module._free(outPtrPtr); + Module._free(outSizePtr); return bytes; } - decodeRGBA(webp: Uint8Array): { rgba: Uint8Array, width: number, height: number } { + decodeRGBA(webp: Uint8Array): { rgba: Uint8Array; width: number; height: number } { const { Module } = this; const input = webp; @@ -98,7 +100,10 @@ class WebPCodec { const ok = Module._webp_decode_rgba(inPtr, input.length, outPtrPtr, widthPtr, heightPtr); if (!ok) { - Module._free(inPtr); Module._free(outPtrPtr); Module._free(widthPtr); Module._free(heightPtr); + Module._free(inPtr); + Module._free(outPtrPtr); + Module._free(widthPtr); + Module._free(heightPtr); throw new Error('WebP decode failed'); } @@ -109,7 +114,10 @@ class WebPCodec { const bytes = Module.HEAPU8.slice(outPtr, outPtr + size); Module._webp_free(outPtr); - Module._free(inPtr); Module._free(outPtrPtr); Module._free(widthPtr); Module._free(heightPtr); + Module._free(inPtr); + Module._free(outPtrPtr); + Module._free(widthPtr); + Module._free(heightPtr); return { rgba: bytes, width, height }; } diff --git a/src/lib/value-transforms.ts b/src/lib/value-transforms.ts index 6e26b021..b52fc43c 100644 --- a/src/lib/value-transforms.ts +++ b/src/lib/value-transforms.ts @@ -17,41 +17,48 @@ const SH_C0 = 0.28209479177387814; // Inverse transforms: convert user-friendly values to raw PLY space. // All transforms are monotonic increasing, so comparison direction is preserved. const inverseTransforms: Record number> = { - 'opacity': v => Math.log(v / (1 - v)), - 'scale_0': Math.log, - 'scale_1': Math.log, - 'scale_2': Math.log, - 'f_dc_0': v => (v - 0.5) / SH_C0, - 'f_dc_1': v => (v - 0.5) / SH_C0, - 'f_dc_2': v => (v - 0.5) / SH_C0 + opacity: (v) => Math.log(v / (1 - v)), + scale_0: Math.log, + scale_1: Math.log, + scale_2: Math.log, + f_dc_0: (v) => (v - 0.5) / SH_C0, + f_dc_1: (v) => (v - 0.5) / SH_C0, + f_dc_2: (v) => (v - 0.5) / SH_C0 }; // Forward transforms: convert raw PLY values to user-friendly space (for summary display). const forwardTransforms: Record number> = { - 'opacity': v => 1 / (1 + Math.exp(-v)), - 'scale_0': Math.exp, - 'scale_1': Math.exp, - 'scale_2': Math.exp, - 'f_dc_0': v => 0.5 + v * SH_C0, - 'f_dc_1': v => 0.5 + v * SH_C0, - 'f_dc_2': v => 0.5 + v * SH_C0 + opacity: (v) => 1 / (1 + Math.exp(-v)), + scale_0: Math.exp, + scale_1: Math.exp, + scale_2: Math.exp, + f_dc_0: (v) => 0.5 + v * SH_C0, + f_dc_1: (v) => 0.5 + v * SH_C0, + f_dc_2: (v) => 0.5 + v * SH_C0 }; // Maps `_raw` suffixed column names to their underlying PLY column. const rawColumnMap: Record = { - 'opacity_raw': 'opacity', - 'scale_0_raw': 'scale_0', - 'scale_1_raw': 'scale_1', - 'scale_2_raw': 'scale_2', - 'f_dc_0_raw': 'f_dc_0', - 'f_dc_1_raw': 'f_dc_1', - 'f_dc_2_raw': 'f_dc_2' + opacity_raw: 'opacity', + scale_0_raw: 'scale_0', + scale_1_raw: 'scale_1', + scale_2_raw: 'scale_2', + f_dc_0_raw: 'f_dc_0', + f_dc_1_raw: 'f_dc_1', + f_dc_2_raw: 'f_dc_2' }; const transformColumnNames = new Set([ - 'x', 'y', 'z', - 'rot_0', 'rot_1', 'rot_2', 'rot_3', - 'scale_0', 'scale_1', 'scale_2' + 'x', + 'y', + 'z', + 'rot_0', + 'rot_1', + 'rot_2', + 'rot_3', + 'scale_0', + 'scale_1', + 'scale_2' ]); /** diff --git a/src/lib/version.ts b/src/lib/version.ts index e6d8c84d..93cb19b3 100644 --- a/src/lib/version.ts +++ b/src/lib/version.ts @@ -1,9 +1,9 @@ /** * The splat-transform version (semver MAJOR.MINOR.PATCH). */ -export const version: string = '$_CURRENT_VERSION'; +export const version = '$_CURRENT_VERSION'; /** * The splat-transform revision (short Git hash of HEAD at build time). */ -export const revision: string = '$_CURRENT_REVISION'; +export const revision = '$_CURRENT_REVISION'; diff --git a/src/lib/voxel/block-cleanup.ts b/src/lib/voxel/block-cleanup.ts index 4ccdc9a1..4eb86afe 100644 --- a/src/lib/voxel/block-cleanup.ts +++ b/src/lib/voxel/block-cleanup.ts @@ -1,7 +1,8 @@ +import { logger } from '../utils'; + import { BlockMaskBuffer } from './block-mask-buffer'; import { popcount } from './morton'; import { sortKeyMaskPairs } from './sort-key-mask'; -import { logger } from '../utils'; // ============================================================================ // Edge mask constants for 4x4x4 voxel blocks @@ -15,15 +16,15 @@ const FACE_X0 = 0x11111111; /** lx=3 positions in each 32-bit word */ const FACE_X3 = 0x88888888; /** ly=0 positions in each 32-bit word */ -const FACE_Y0 = 0x000F000F; +const FACE_Y0 = 0x000f000f; /** ly=3 positions in each 32-bit word */ -const FACE_Y3 = 0xF000F000; +const FACE_Y3 = 0xf000f000; /** lz=0 positions: lo bits 0-15 */ -const FACE_Z0_LO = 0x0000FFFF; +const FACE_Z0_LO = 0x0000ffff; /** lz=3 positions: hi bits 16-31 */ -const FACE_Z3_HI = 0xFFFF0000 >>> 0; +const FACE_Z3_HI = 0xffff0000 >>> 0; -const SOLID_MASK = 0xFFFFFFFF >>> 0; +const SOLID_MASK = 0xffffffff >>> 0; // ============================================================================ // Main function @@ -170,12 +171,18 @@ function filterAndFillBlocks( const origLo = masks[i * 2]; const origHi = masks[i * 2 + 1]; const w = local * 12; - const pxLo = work[w], pxHi = work[w + 1]; - const mxLo = work[w + 2], mxHi = work[w + 3]; - const pyLo = work[w + 4], pyHi = work[w + 5]; - const myLo = work[w + 6], myHi = work[w + 7]; - const pzLo = work[w + 8], pzHi = work[w + 9]; - const mzLo = work[w + 10], mzHi = work[w + 11]; + const pxLo = work[w], + pxHi = work[w + 1]; + const mxLo = work[w + 2], + mxHi = work[w + 3]; + const pyLo = work[w + 4], + pyHi = work[w + 5]; + const myLo = work[w + 6], + myHi = work[w + 7]; + const pzLo = work[w + 8], + pzHi = work[w + 9]; + const mzLo = work[w + 10], + mzHi = work[w + 11]; const neighborLo = pxLo | mxLo | pyLo | myLo | pzLo | mzLo; const neighborHi = pxHi | mxHi | pyHi | myHi | pzHi | mzHi; let lo = origLo & neighborLo; @@ -188,12 +195,14 @@ function filterAndFillBlocks( newMasks[i * 2] = lo; newMasks[i * 2 + 1] = hi; - if ((lo >>> 0) === SOLID_MASK && (hi >>> 0) === SOLID_MASK) { + if (lo >>> 0 === SOLID_MASK && hi >>> 0 === SOLID_MASK) { promotedSolidBlocks++; } else if (lo !== 0 || hi !== 0) { mixedBlocks++; if (mixedBlocks > maxMixedBlocks) { - throw new Error(`Voxel output has more than ${maxMixedBlocks} mixed blocks. Use a coarser voxel resolution.`); + throw new Error( + `Voxel output has more than ${maxMixedBlocks} mixed blocks. Use a coarser voxel resolution.` + ); } } } diff --git a/src/lib/voxel/block-index-map.ts b/src/lib/voxel/block-index-map.ts index 5242995e..029cd34f 100644 --- a/src/lib/voxel/block-index-map.ts +++ b/src/lib/voxel/block-index-map.ts @@ -47,7 +47,7 @@ class BlockIndexMap { set(key: number, val: number): void { const keys = this.keys; const mask = this.mask; - let i = ((Math.imul(key >>> 0, 0x9E3779B9) ^ Math.imul((key / 4294967296) >>> 0, 0x85EBCA77)) >>> 0) & mask; + let i = ((Math.imul(key >>> 0, 0x9e3779b9) ^ Math.imul((key / 4294967296) >>> 0, 0x85ebca77)) >>> 0) & mask; while (keys[i] !== EMPTY_KEY && keys[i] !== key) { i = (i + 1) & mask; } @@ -58,7 +58,7 @@ class BlockIndexMap { get(key: number): number { const keys = this.keys; const mask = this.mask; - let i = ((Math.imul(key >>> 0, 0x9E3779B9) ^ Math.imul((key / 4294967296) >>> 0, 0x85EBCA77)) >>> 0) & mask; + let i = ((Math.imul(key >>> 0, 0x9e3779b9) ^ Math.imul((key / 4294967296) >>> 0, 0x85ebca77)) >>> 0) & mask; while (true) { const k = keys[i]; if (k === key) return this.vals[i]; diff --git a/src/lib/voxel/block-mask-map.ts b/src/lib/voxel/block-mask-map.ts index 6a3d7ebb..ec307a3b 100644 --- a/src/lib/voxel/block-mask-map.ts +++ b/src/lib/voxel/block-mask-map.ts @@ -109,7 +109,7 @@ class BlockMaskMap { j = (j + 1) & mask; if (this.keys[j] === EMPTY) break; const k = this._hash(this.keys[j]) & mask; - if ((i < j) ? (k <= i || k > j) : (k <= i && k > j)) { + if (i < j ? k <= i || k > j : k <= i && k > j) { this.keys[i] = this.keys[j]; this.lo[i] = this.lo[j]; this.hi[i] = this.hi[j]; @@ -193,7 +193,7 @@ class BlockMaskMap { private _hash(key: number): number { const lo = key >>> 0; const hi = Math.floor(key / 0x100000000) >>> 0; - return (Math.imul(lo, 0x9E3779B9) ^ Math.imul(hi, 0x85EBCA77)) >>> 0; + return (Math.imul(lo, 0x9e3779b9) ^ Math.imul(hi, 0x85ebca77)) >>> 0; } } diff --git a/src/lib/voxel/carve.ts b/src/lib/voxel/carve.ts index 341ce72c..1e45cca0 100644 --- a/src/lib/voxel/carve.ts +++ b/src/lib/voxel/carve.ts @@ -1,16 +1,14 @@ import { Vec3 } from 'playcanvas'; +import type { Bounds } from '../data-table'; +import type { GpuDilation } from '../gpu'; +import { logger } from '../utils'; + import { gpuDilate3 } from './dilation'; import type { NavSeed, NavSimplifyResult } from './fill-exterior'; import { twoLevelBFS } from './flood-fill'; import { computeEmptyGrid } from './grid-ops'; -import type { Bounds } from '../data-table'; -import type { GpuDilation } from '../gpu'; -import { - BLOCK_EMPTY, - SparseVoxelGrid -} from './sparse-voxel-grid'; -import { logger } from '../utils'; +import { BLOCK_EMPTY, SparseVoxelGrid } from './sparse-voxel-grid'; const carve = async ( gridA: SparseVoxelGrid, @@ -55,7 +53,9 @@ const carve = async ( const maxRadius = Math.max(kernelR, yHalfExtent) * 2; const found = SparseVoxelGrid.findNearestFreeCell(blocked, seedIx, seedIy, seedIz, maxRadius); if (!found) { - logger.warn(`seed (${seed.x}, ${seed.y}, ${seed.z}) blocked after dilation, no free cell within ${maxRadius} voxels, skipping carve`); + logger.warn( + `seed (${seed.x}, ${seed.y}, ${seed.z}) blocked after dilation, no free cell within ${maxRadius} voxels, skipping carve` + ); return { grid: gridA, gridBounds }; } seedIx = found.ix; @@ -72,10 +72,7 @@ const carve = async ( // so the bar usually finishes shy of 100% (mixed/solid blocks aren't // counted). Good enough for visual feedback on a long BFS. const bfsBar = logger.bar('BFS', nbx * nby * nbz); - const visited = twoLevelBFS( - blocked, bSeeds, vSeeds, nx, ny, nz, - count => bfsBar.update(count) - ); + const visited = twoLevelBFS(blocked, bSeeds, vSeeds, nx, ny, nz, (count) => bfsBar.update(count)); bfsBar.end(); const emptyGrid = computeEmptyGrid(visited, blocked); @@ -83,7 +80,7 @@ const carve = async ( const navRegion = await gpuDilate3(gpu, emptyGrid, kernelR, yHalfExtent); const boundsBar = logger.bar('Scanning bounds', navRegion.types.length); - const navBounds = navRegion.getOccupiedBlockBounds(done => boundsBar.update(done)); + const navBounds = navRegion.getOccupiedBlockBounds((done) => boundsBar.update(done)); boundsBar.end(); if (!navBounds) { @@ -118,10 +115,8 @@ const carve = async ( }; const buildBar = logger.bar('Building grid', navRegion.types.length); - const outGrid = navRegion.cropToInverted( - cropMinBx, cropMinBy, cropMinBz, - cropMaxBx, cropMaxBy, cropMaxBz, - done => buildBar.update(done) + const outGrid = navRegion.cropToInverted(cropMinBx, cropMinBy, cropMinBz, cropMaxBx, cropMaxBy, cropMaxBz, (done) => + buildBar.update(done) ); buildBar.end(); diff --git a/src/lib/voxel/dilation.ts b/src/lib/voxel/dilation.ts index fb20dd62..1f41cb27 100644 --- a/src/lib/voxel/dilation.ts +++ b/src/lib/voxel/dilation.ts @@ -1,13 +1,8 @@ -import { - BLOCK_EMPTY, - BLOCK_SOLID, - SOLID_WORD, - SparseVoxelGrid, - readBlockType -} from './sparse-voxel-grid'; import { GpuDilation } from '../gpu'; import { logger } from '../utils'; +import { BLOCK_EMPTY, BLOCK_SOLID, SOLID_WORD, SparseVoxelGrid, readBlockType } from './sparse-voxel-grid'; + // ============================================================================ // GPU Dilation // @@ -41,8 +36,12 @@ const blockAlignedExtent = (halfExtent: number): number => { */ function chunkIsEmpty( src: SparseVoxelGrid, - ox: number, oy: number, oz: number, - cx: number, cy: number, cz: number + ox: number, + oy: number, + oz: number, + cx: number, + cy: number, + cz: number ): boolean { const minBx = Math.max(0, Math.floor(ox / 4)); const minBy = Math.max(0, Math.floor(oy / 4)); @@ -83,8 +82,12 @@ function chunkIsEmpty( */ function chunkIsSaturated( src: SparseVoxelGrid, - ox: number, oy: number, oz: number, - cx: number, cy: number, cz: number + ox: number, + oy: number, + oz: number, + cx: number, + cy: number, + cz: number ): boolean { // Halo extends past grid → those bits are 0, not saturated. if (ox < 0 || oy < 0 || oz < 0) return false; @@ -131,8 +134,12 @@ function chunkIsSaturated( */ function insertSaturatedInner( dst: SparseVoxelGrid, - innerOx: number, innerOy: number, innerOz: number, - innerCx: number, innerCy: number, innerCz: number + innerOx: number, + innerOy: number, + innerOz: number, + innerCx: number, + innerCy: number, + innerCz: number ): void { const minBx = Math.max(0, innerOx >> 2); const minBy = Math.max(0, innerOy >> 2); @@ -190,8 +197,12 @@ function applyChunkToDst( dst: SparseVoxelGrid, typesOut: Uint32Array, masksOut: Uint32Array, - cx: number, cy: number, cz: number, - innerNx: number, innerNy: number, innerNz: number + cx: number, + cy: number, + cz: number, + innerNx: number, + innerNy: number, + innerNz: number ): void { const innerBx = innerNx >> 2; const innerBy = innerNy >> 2; @@ -215,14 +226,15 @@ function applyChunkToDst( const wordIdx = innerIdx >>> 4; const bitShift = (innerIdx & 15) << 1; const bt = (typesOut[wordIdx] >>> bitShift) & 3; - if (bt === 0) continue; // EMPTY + if (bt === 0) continue; // EMPTY const globalBlockIdx = baseGlobalIdx + bx; const w = globalBlockIdx >>> 4; const shift = (globalBlockIdx & 15) << 1; dstTypes[w] |= bt << shift; - if (bt === 2) { // MIXED + if (bt === 2) { + // MIXED const m2 = innerIdx * 2; dstMasks.set(globalBlockIdx, masksOut[m2], masksOut[m2 + 1]); } @@ -276,12 +288,16 @@ async function gpuDilate3( const numChunksZ = Math.ceil(src.nz / innerStep); const totalChunks = numChunksX * numChunksY * numChunksZ; - interface InFlight { + type InFlight = { typesPromise: Promise; masksPromise: Promise; - cx: number; cy: number; cz: number; - innerNx: number; innerNy: number; innerNz: number; - } + cx: number; + cy: number; + cz: number; + innerNx: number; + innerNy: number; + innerNz: number; + }; let currentSlot = 0; let inflight: InFlight | null = null; @@ -334,11 +350,20 @@ async function gpuDilate3( const { types: typesPromise, masks: masksPromise } = gpu.submitChunkSparse( currentSlot, - minBx, minBy, minBz, - outerBx, outerBy, outerBz, - haloBx, haloBy, haloBz, - innerBx, innerBy, innerBz, - halfExtentXZ, halfExtentY + minBx, + minBy, + minBz, + outerBx, + outerBy, + outerBz, + haloBx, + haloBy, + haloBz, + innerBx, + innerBy, + innerBz, + halfExtentXZ, + halfExtentY ); if (inflight) { diff --git a/src/lib/voxel/fill-exterior.ts b/src/lib/voxel/fill-exterior.ts index 50b9a2c9..04a441f5 100644 --- a/src/lib/voxel/fill-exterior.ts +++ b/src/lib/voxel/fill-exterior.ts @@ -1,10 +1,12 @@ import { Vec3 } from 'playcanvas'; +import type { Bounds } from '../data-table'; +import type { GpuDilation } from '../gpu'; +import { logger } from '../utils'; + import { gpuDilate3 } from './dilation'; import { twoLevelBFS } from './flood-fill'; import { sparseOrGrids } from './grid-ops'; -import type { Bounds } from '../data-table'; -import type { GpuDilation } from '../gpu'; import { BLOCK_MIXED, BLOCK_SOLID, @@ -14,7 +16,6 @@ import { SOLID_LO, SparseVoxelGrid } from './sparse-voxel-grid'; -import { logger } from '../utils'; type NavSeed = { x: number; @@ -92,7 +93,7 @@ const fillExterior = async ( for (let bz = 0; bz < nbz; bz++) { for (let by = 0; by < nby; by++) { - seedBoundaryBlock((nbx - 1) + by * nbx + bz * bStride, nbx - 1, by, bz, 1); + seedBoundaryBlock(nbx - 1 + by * nbx + bz * bStride, nbx - 1, by, bz, 1); } } @@ -140,8 +141,12 @@ const fillExterior = async ( const combined = sparseOrGrids(gridOriginal, dilatedVisited); - let minIx = nx, minIy = ny, minIz = nz; - let maxIx = 0, maxIy = 0, maxIz = 0; + let minIx = nx, + minIy = ny, + minIz = nz; + let maxIx = 0, + maxIy = 0, + maxIz = 0; for (let bz = 0; bz < nbz; bz++) { for (let by = 0; by < nby; by++) { @@ -198,10 +203,7 @@ const fillExterior = async ( }; return { - grid: combined.cropTo( - cropMinBx, cropMinBy, cropMinBz, - cropMaxBx, cropMaxBy, cropMaxBz - ), + grid: combined.cropTo(cropMinBx, cropMinBy, cropMinBz, cropMaxBx, cropMaxBy, cropMaxBz), gridBounds: croppedBounds }; }; diff --git a/src/lib/voxel/fill-floor.ts b/src/lib/voxel/fill-floor.ts index fdfa8179..43e4e914 100644 --- a/src/lib/voxel/fill-floor.ts +++ b/src/lib/voxel/fill-floor.ts @@ -1,18 +1,12 @@ -import { gpuDilate3 } from './dilation'; -import type { NavSimplifyResult } from './fill-exterior'; -import { sparseOrGrids } from './grid-ops'; import type { Bounds } from '../data-table'; import type { GpuDilation } from '../gpu'; -import { - BLOCK_EMPTY, - BLOCK_SOLID, - SOLID_HI, - SOLID_LO, - SparseVoxelGrid, - readBlockType -} from './sparse-voxel-grid'; import { logger } from '../utils'; +import { gpuDilate3 } from './dilation'; +import type { NavSimplifyResult } from './fill-exterior'; +import { sparseOrGrids } from './grid-ops'; +import { BLOCK_EMPTY, BLOCK_SOLID, SOLID_HI, SOLID_LO, SparseVoxelGrid, readBlockType } from './sparse-voxel-grid'; + /** * Floor-fill via XZ dilate -> per-column upward walk -> XZ dilate -> OR. * @@ -52,7 +46,7 @@ const fillFloor = async ( grid: SparseVoxelGrid, gridBounds: Bounds, voxelResolution: number, - dilation: number = 0, + dilation = 0, gpu: GpuDilation | null = null ): Promise => { if (!Number.isFinite(voxelResolution) || voxelResolution <= 0) { @@ -83,7 +77,7 @@ const fillFloor = async ( const dilatedTypes = dilatedSolid.types; for (let bz = 0; bz < nbz; bz++) { for (let bx = 0; bx < nbx; bx++) { - let walking = 0xFFFF; + let walking = 0xffff; for (let by = 0; by < nby && walking; by++) { const blockIdx = bx + by * nbx + bz * bStride; @@ -94,17 +88,18 @@ const fillFloor = async ( } if (bt === BLOCK_EMPTY) { - if (walking === 0xFFFF) { + if (walking === 0xffff) { foundEmpty.orBlock(blockIdx, SOLID_LO, SOLID_HI); } else { - let lo = 0, hi = 0; + let lo = 0, + hi = 0; for (let lz = 0; lz < 4; lz++) { for (let lx = 0; lx < 4; lx++) { if (!(walking & (1 << (lz * 4 + lx)))) continue; for (let ly = 0; ly < 4; ly++) { const bitIdx = lx + (ly << 2) + (lz << 4); - if (bitIdx < 32) lo |= (1 << bitIdx); - else hi |= (1 << (bitIdx - 32)); + if (bitIdx < 32) lo |= 1 << bitIdx; + else hi |= 1 << (bitIdx - 32); } } } @@ -161,7 +156,7 @@ const fillFloor = async ( // cloning — saves a full SparseVoxelGrid clone right at the end of // the fill-floor phase. const combineBar = logger.bar('Combining', grid.types.length); - const combined = sparseOrGrids(grid, dilatedFound, true, done => combineBar.update(done)); + const combined = sparseOrGrids(grid, dilatedFound, true, (done) => combineBar.update(done)); combineBar.end(); return { grid: combined, gridBounds }; diff --git a/src/lib/voxel/filter-cluster.ts b/src/lib/voxel/filter-cluster.ts index 88f9870f..a0d627d4 100644 --- a/src/lib/voxel/filter-cluster.ts +++ b/src/lib/voxel/filter-cluster.ts @@ -1,31 +1,17 @@ import { Vec3 } from 'playcanvas'; -import { BlockMaskBuffer } from './block-mask-buffer'; -import { - setupVoxelFilter, - buildGaussianColumns, - buildBlockGridParams, - type VoxelFilterContext -} from './filter-pipeline'; -import { twoLevelBFS } from './flood-fill'; -import { - BLOCK_EMPTY, - BLOCK_MIXED, - BLOCKS_PER_WORD, - EVEN_BITS, - SOLID_WORD, - SparseVoxelGrid -} from './sparse-voxel-grid'; -import { - buildBlockLookup, - isCenterInOccupiedVoxel, - gaussianContributesToVoxels -} from './voxel-query'; -import { alignGridBounds, voxelizeToBuffer } from './voxelize'; -import { DataTable } from '../data-table'; +import type { DataTable } from '../data-table'; import type { DeviceCreator } from '../types'; import { fmtCount, fmtDistance, logger } from '../utils'; +import type { BlockMaskBuffer } from './block-mask-buffer'; +import { setupVoxelFilter, buildGaussianColumns, buildBlockGridParams } from './filter-pipeline'; +import type { VoxelFilterContext } from './filter-pipeline'; +import { twoLevelBFS } from './flood-fill'; +import { BLOCK_EMPTY, BLOCK_MIXED, BLOCKS_PER_WORD, EVEN_BITS, SOLID_WORD, SparseVoxelGrid } from './sparse-voxel-grid'; +import { buildBlockLookup, isCenterInOccupiedVoxel, gaussianContributesToVoxels } from './voxel-query'; +import { alignGridBounds, voxelizeToBuffer } from './voxelize'; + /** * Build an inverted SparseVoxelGrid from a BlockMaskBuffer for flood-filling * through occupied voxels. In the returned grid, originally-occupied voxels @@ -37,10 +23,7 @@ import { fmtCount, fmtDistance, logger } from '../utils'; * @param nz - Grid dimension Z in voxels. * @returns Inverted grid suitable for twoLevelBFS. */ -const buildInvertedGrid = ( - buffer: BlockMaskBuffer, - nx: number, ny: number, nz: number -): SparseVoxelGrid => { +const buildInvertedGrid = (buffer: BlockMaskBuffer, nx: number, ny: number, nz: number): SparseVoxelGrid => { const grid = new SparseVoxelGrid(nx, ny, nz); // Inverted grid: every block defaults to SOLID (fully blocked). SOLID is @@ -70,7 +53,7 @@ const buildInvertedGrid = ( for (let i = 0; i < mixed.blockIdx.length; i++) { const blockIdx = mixed.blockIdx[i]; grid.setBlockType(blockIdx, BLOCK_MIXED); - grid.masks.set(blockIdx, (~mixed.masks[i * 2]) >>> 0, (~mixed.masks[i * 2 + 1]) >>> 0); + grid.masks.set(blockIdx, ~mixed.masks[i * 2] >>> 0, ~mixed.masks[i * 2 + 1] >>> 0); } return grid; @@ -95,8 +78,12 @@ const buildInvertedGrid = ( */ const findClusterVoxelFlood = ( buffer: BlockMaskBuffer, - nx: number, ny: number, nz: number, - seedIx: number, seedIy: number, seedIz: number + nx: number, + ny: number, + nz: number, + seedIx: number, + seedIy: number, + seedIz: number ): { ccCount: number; visited: SparseVoxelGrid; resolvedSeed: { ix: number; iy: number; iz: number } } | null => { const blocked = buildInvertedGrid(buffer, nx, ny, nz); const nbx = nx >> 2; @@ -156,10 +143,10 @@ const findClusterVoxelFlood = ( const filterCluster = async ( dataTable: DataTable, createDevice: DeviceCreator, - voxelResolution: number = 1.0, + voxelResolution = 1.0, seed: Vec3 = Vec3.ZERO, - opacityCutoff: number = 0.999, - minContribution: number = 0.1 + opacityCutoff = 0.999, + minContribution = 0.1 ): Promise => { if (!Number.isFinite(voxelResolution) || voxelResolution <= 0) { throw new Error(`filterCluster: voxelResolution must be a positive finite number, got ${voxelResolution}`); @@ -203,8 +190,12 @@ const filterCluster = async ( const sceneExtentZ = ctx.sceneBounds.max.z - ctx.sceneBounds.min.z; const gridBounds = alignGridBounds( - ctx.sceneBounds.min.x, ctx.sceneBounds.min.y, ctx.sceneBounds.min.z, - ctx.sceneBounds.max.x, ctx.sceneBounds.max.y, ctx.sceneBounds.max.z, + ctx.sceneBounds.min.x, + ctx.sceneBounds.min.y, + ctx.sceneBounds.min.z, + ctx.sceneBounds.max.x, + ctx.sceneBounds.max.y, + ctx.sceneBounds.max.z, clampedResolution ); @@ -233,10 +224,16 @@ const filterCluster = async ( const nz = nbz * 4; const totalVoxels = nx * ny * nz; - logger.info(`scene: ${fmtDistance(sceneExtentX)} x ${fmtDistance(sceneExtentY)} x ${fmtDistance(sceneExtentZ)}, grid: ${nx} x ${ny} x ${nz} voxels (${fmtCount(totalVoxels)}) @ ${fmtDistance(clampedResolution)}`); + logger.info( + `scene: ${fmtDistance(sceneExtentX)} x ${fmtDistance(sceneExtentY)} x ${fmtDistance(sceneExtentZ)}, grid: ${nx} x ${ny} x ${nz} voxels (${fmtCount(totalVoxels)}) @ ${fmtDistance(clampedResolution)}` + ); const buffer = await voxelizeToBuffer( - ctx.bvh, ctx.gpuVoxelization!, gridBounds, clampedResolution, opacityCutoff + ctx.bvh, + ctx.gpuVoxelization!, + gridBounds, + clampedResolution, + opacityCutoff ); ctx.gpuVoxelization.destroy(); @@ -262,7 +259,9 @@ const filterCluster = async ( const worldX = gridBounds.min.x + (resolvedSeed.ix + 0.5) * clampedResolution; const worldY = gridBounds.min.y + (resolvedSeed.iy + 0.5) * clampedResolution; const worldZ = gridBounds.min.z + (resolvedSeed.iz + 0.5) * clampedResolution; - logger.warn(`seed (${seed.x.toFixed(2)}, ${seed.y.toFixed(2)}, ${seed.z.toFixed(2)}) unoccupied; resolved to nearest at (${worldX.toFixed(2)}, ${worldY.toFixed(2)}, ${worldZ.toFixed(2)})`); + logger.warn( + `seed (${seed.x.toFixed(2)}, ${seed.y.toFixed(2)}, ${seed.z.toFixed(2)}) unoccupied; resolved to nearest at (${worldX.toFixed(2)}, ${worldY.toFixed(2)}, ${worldZ.toFixed(2)})` + ); } logger.info(`cluster is ${fmtCount(ccCount)} of ${fmtCount(buffer.count)} blocks`); @@ -306,7 +305,7 @@ const filterCluster = async ( let minHits = 1; if (Math.max(ex, ey, ez) * 2 > largeThreshold) { - const aabbVoxels = (2 * ex * invVoxel) * (2 * ey * invVoxel) * (2 * ez * invVoxel); + const aabbVoxels = 2 * ex * invVoxel * (2 * ey * invVoxel) * (2 * ez * invVoxel); minHits = Math.max(1, Math.ceil(aabbVoxels * minOccupancyRatio)); } diff --git a/src/lib/voxel/filter-floaters.ts b/src/lib/voxel/filter-floaters.ts index 1333cfa1..8cc0a3bd 100644 --- a/src/lib/voxel/filter-floaters.ts +++ b/src/lib/voxel/filter-floaters.ts @@ -1,19 +1,12 @@ -import { - setupVoxelFilter, - buildGaussianColumns, - buildBlockGridParams, - type VoxelFilterContext -} from './filter-pipeline'; -import { - buildBlockLookup, - isCenterInOccupiedVoxel, - gaussianContributesToVoxels -} from './voxel-query'; -import { alignGridBounds, voxelizeToBuffer } from './voxelize'; -import { DataTable } from '../data-table'; +import type { DataTable } from '../data-table'; import type { DeviceCreator } from '../types'; import { fmtCount, fmtDistance, logger } from '../utils'; +import { setupVoxelFilter, buildGaussianColumns, buildBlockGridParams } from './filter-pipeline'; +import type { VoxelFilterContext } from './filter-pipeline'; +import { buildBlockLookup, isCenterInOccupiedVoxel, gaussianContributesToVoxels } from './voxel-query'; +import { alignGridBounds, voxelizeToBuffer } from './voxelize'; + /** * Remove Gaussians that don't meaningfully contribute to any solid voxel. * @@ -32,8 +25,8 @@ import { fmtCount, fmtDistance, logger } from '../utils'; const filterFloaters = async ( dataTable: DataTable, createDevice: DeviceCreator, - voxelResolution: number = 0.05, - opacityCutoff: number = 0.1, + voxelResolution = 0.05, + opacityCutoff = 0.1, minContribution: number = 1 / 255 ): Promise => { if (!Number.isFinite(voxelResolution) || voxelResolution <= 0) { @@ -71,8 +64,12 @@ const filterFloaters = async ( const sceneExtentZ = ctx.sceneBounds.max.z - ctx.sceneBounds.min.z; const gridBounds = alignGridBounds( - ctx.sceneBounds.min.x, ctx.sceneBounds.min.y, ctx.sceneBounds.min.z, - ctx.sceneBounds.max.x, ctx.sceneBounds.max.y, ctx.sceneBounds.max.z, + ctx.sceneBounds.min.x, + ctx.sceneBounds.min.y, + ctx.sceneBounds.min.z, + ctx.sceneBounds.max.x, + ctx.sceneBounds.max.y, + ctx.sceneBounds.max.z, voxelResolution ); @@ -82,10 +79,16 @@ const filterFloaters = async ( const nz = grid.numBlocksZ * 4; const totalVoxels = nx * ny * nz; - logger.info(`scene: ${fmtDistance(sceneExtentX)} x ${fmtDistance(sceneExtentY)} x ${fmtDistance(sceneExtentZ)}, grid: ${nx} x ${ny} x ${nz} voxels (${fmtCount(totalVoxels)}) @ ${fmtDistance(voxelResolution)}`); + logger.info( + `scene: ${fmtDistance(sceneExtentX)} x ${fmtDistance(sceneExtentY)} x ${fmtDistance(sceneExtentZ)}, grid: ${nx} x ${ny} x ${nz} voxels (${fmtCount(totalVoxels)}) @ ${fmtDistance(voxelResolution)}` + ); const buffer = await voxelizeToBuffer( - ctx.bvh, ctx.gpuVoxelization!, gridBounds, voxelResolution, opacityCutoff + ctx.bvh, + ctx.gpuVoxelization!, + gridBounds, + voxelResolution, + opacityCutoff ); ctx.gpuVoxelization.destroy(); @@ -93,7 +96,9 @@ const filterFloaters = async ( const lookup = buildBlockLookup(buffer); - logger.info(`occupied blocks: ${fmtCount(lookup.solidCount + lookup.mixedCount)} (${fmtCount(lookup.solidCount)} solid, ${fmtCount(lookup.mixedCount)} mixed)`); + logger.info( + `occupied blocks: ${fmtCount(lookup.solidCount + lookup.mixedCount)} (${fmtCount(lookup.solidCount)} solid, ${fmtCount(lookup.mixedCount)} mixed)` + ); const gaussianCols = buildGaussianColumns(ctx); const keepIndices: number[] = []; diff --git a/src/lib/voxel/filter-pipeline.ts b/src/lib/voxel/filter-pipeline.ts index 9bb94403..d37ff282 100644 --- a/src/lib/voxel/filter-pipeline.ts +++ b/src/lib/voxel/filter-pipeline.ts @@ -1,20 +1,22 @@ -import { type BlockGridParams } from './voxel-query'; -import { Column, DataTable, computeGaussianExtents, type Bounds, type GaussianExtentsResult, type GaussianColumns, computeWriteTransform, transformColumns } from '../data-table'; +import { Column, DataTable, computeGaussianExtents, computeWriteTransform, transformColumns } from '../data-table'; +import type { Bounds, GaussianExtentsResult, GaussianColumns } from '../data-table'; import { GpuVoxelization } from '../gpu'; import { GaussianBVH } from '../spatial'; import type { DeviceCreator } from '../types'; import { Transform } from '../utils'; +import type { BlockGridParams } from './voxel-query'; + /** * Context produced by the shared voxel filter setup pipeline. */ -interface VoxelFilterContext { +type VoxelFilterContext = { pcDataTable: DataTable; extentsResult: GaussianExtentsResult; sceneBounds: Bounds; bvh: GaussianBVH; gpuVoxelization: GpuVoxelization | null; -} +}; /** * Set up the common voxelization pipeline used by both filterCluster and filterFloaters. @@ -26,25 +28,29 @@ interface VoxelFilterContext { * @param createDevice - Function to create a GPU device for voxelization. * @returns Context containing all shared resources. */ -const setupVoxelFilter = async ( - dataTable: DataTable, - createDevice: DeviceCreator -): Promise => { +const setupVoxelFilter = async (dataTable: DataTable, createDevice: DeviceCreator): Promise => { const voxelColumns = [ - 'x', 'y', 'z', - 'rot_0', 'rot_1', 'rot_2', 'rot_3', - 'scale_0', 'scale_1', 'scale_2', + 'x', + 'y', + 'z', + 'rot_0', + 'rot_1', + 'rot_2', + 'rot_3', + 'scale_0', + 'scale_1', + 'scale_2', 'opacity' ]; const delta = computeWriteTransform(dataTable.transform, Transform.IDENTITY); const cols = transformColumns(dataTable, voxelColumns, delta); - const missing = voxelColumns.filter(name => !cols.has(name)); + const missing = voxelColumns.filter((name) => !cols.has(name)); if (missing.length > 0) { throw new Error(`setupVoxelFilter: input DataTable is missing required columns: ${missing.join(', ')}`); } - const pcDataTable = new DataTable(voxelColumns.map(name => new Column(name, cols.get(name)!))); + const pcDataTable = new DataTable(voxelColumns.map((name) => new Column(name, cols.get(name)!))); const extentsResult = computeGaussianExtents(pcDataTable); const sceneBounds = extentsResult.sceneBounds; @@ -113,9 +119,4 @@ const buildBlockGridParams = (gridBounds: Bounds, voxelResolution: number): Bloc }; }; -export { - setupVoxelFilter, - buildGaussianColumns, - buildBlockGridParams, - type VoxelFilterContext -}; +export { setupVoxelFilter, buildGaussianColumns, buildBlockGridParams, type VoxelFilterContext }; diff --git a/src/lib/voxel/flood-fill.ts b/src/lib/voxel/flood-fill.ts index 7953aadd..bbdc93d5 100644 --- a/src/lib/voxel/flood-fill.ts +++ b/src/lib/voxel/flood-fill.ts @@ -41,7 +41,9 @@ function twoLevelBFS( blocked: SparseVoxelGrid, blockSeeds: number[], voxelSeeds: { ix: number; iy: number; iz: number }[], - nx: number, ny: number, nz: number, + nx: number, + ny: number, + nz: number, onBlockFilled?: (count: number) => void ): SparseVoxelGrid { const visited = new SparseVoxelGrid(nx, ny, nz); @@ -78,7 +80,9 @@ function twoLevelBFS( const growBlockQueue = (): void => { if (bqCap >= QUEUE_CAP_MAX) { - throw new Error(`flood-fill: block queue exceeded ${QUEUE_CAP_MAX} entries — scene likely contains pathologically large gaussians or a connected component too large to flood-fill at this resolution. Try a coarser --filter-cluster resolution or pre-filter outliers (e.g. --filter-box).`); + throw new Error( + `flood-fill: block queue exceeded ${QUEUE_CAP_MAX} entries — scene likely contains pathologically large gaussians or a connected component too large to flood-fill at this resolution. Try a coarser --filter-cluster resolution or pre-filter outliers (e.g. --filter-box).` + ); } const newCap = bqCap * 2; const nb = new Uint32Array(newCap); @@ -92,7 +96,9 @@ function twoLevelBFS( const growVoxelQueue = (): void => { if (vqCap >= QUEUE_CAP_MAX) { - throw new Error(`flood-fill: voxel queue exceeded ${QUEUE_CAP_MAX} entries — scene likely contains pathologically large gaussians or a connected component too large to flood-fill at this resolution. Try a coarser --filter-cluster resolution or pre-filter outliers (e.g. --filter-box).`); + throw new Error( + `flood-fill: voxel queue exceeded ${QUEUE_CAP_MAX} entries — scene likely contains pathologically large gaussians or a connected component too large to flood-fill at this resolution. Try a coarser --filter-cluster resolution or pre-filter outliers (e.g. --filter-box).` + ); } const newCap = vqCap * 2; const nix = new Uint32Array(newCap); @@ -147,7 +153,8 @@ function twoLevelBFS( if (vbt === BLOCK_SOLID) return; const bs = bMasks.slot(nBlockIdx); - let vLo = 0, vHi = 0; + let vLo = 0, + vHi = 0; let vs = -1; if (vbt === BLOCK_MIXED) { vs = vMasks.slot(nBlockIdx); @@ -261,10 +268,7 @@ function twoLevelBFS( } } else { writeBlockType(visitedTypes, blockIdx, BLOCK_MIXED); - vMasks.set(blockIdx, - bitIdx < 32 ? (1 << bitIdx) >>> 0 : 0, - bitIdx >= 32 ? (1 << (bitIdx - 32)) >>> 0 : 0 - ); + vMasks.set(blockIdx, bitIdx < 32 ? (1 << bitIdx) >>> 0 : 0, bitIdx >= 32 ? (1 << (bitIdx - 32)) >>> 0 : 0); } enqueueVoxel(ix, iy, iz); diff --git a/src/lib/voxel/grid-ops.ts b/src/lib/voxel/grid-ops.ts index 4eeb7d07..1a764b2c 100644 --- a/src/lib/voxel/grid-ops.ts +++ b/src/lib/voxel/grid-ops.ts @@ -85,7 +85,7 @@ function computeEmptyGrid(visited: SparseVoxelGrid, blocked: SparseVoxelGrid): S function sparseOrGrids( a: SparseVoxelGrid, b: SparseVoxelGrid, - consumeA: boolean = false, + consumeA = false, onProgress?: (done: number, total: number) => void ): SparseVoxelGrid { const result = consumeA ? a : a.clone(); diff --git a/src/lib/voxel/morton.ts b/src/lib/voxel/morton.ts index b4c77a27..aa3c2ff8 100644 --- a/src/lib/voxel/morton.ts +++ b/src/lib/voxel/morton.ts @@ -1,5 +1,5 @@ /** All 64 bits set (as unsigned 32-bit) */ -const SOLID_MASK = 0xFFFFFFFF >>> 0; +const SOLID_MASK = 0xffffffff >>> 0; /** * Encode block coordinates to Morton code (17 bits per axis = 51 bits total). @@ -33,9 +33,9 @@ function xyzToMorton(x: number, y: number, z: number): number { */ function popcount(n: number): number { n >>>= 0; - n -= ((n >>> 1) & 0x55555555); + n -= (n >>> 1) & 0x55555555; n = (n & 0x33333333) + ((n >>> 2) & 0x33333333); - return (((n + (n >>> 4)) & 0x0F0F0F0F) * 0x01010101) >>> 24; + return (((n + (n >>> 4)) & 0x0f0f0f0f) * 0x01010101) >>> 24; } /** @@ -46,7 +46,7 @@ function popcount(n: number): number { * @returns True if all 64 voxels are solid */ function isSolid(lo: number, hi: number): boolean { - return (lo >>> 0) === SOLID_MASK && (hi >>> 0) === SOLID_MASK; + return lo >>> 0 === SOLID_MASK && hi >>> 0 === SOLID_MASK; } /** @@ -60,9 +60,4 @@ function isEmpty(lo: number, hi: number): boolean { return lo === 0 && hi === 0; } -export { - xyzToMorton, - popcount, - isSolid, - isEmpty -}; +export { xyzToMorton, popcount, isSolid, isEmpty }; diff --git a/src/lib/voxel/sort-key-mask.ts b/src/lib/voxel/sort-key-mask.ts index 5f5bbeba..ecdc6ce8 100644 --- a/src/lib/voxel/sort-key-mask.ts +++ b/src/lib/voxel/sort-key-mask.ts @@ -11,10 +11,17 @@ function sortKeyMaskPairs(keys: Float64Array, values: Uint32Array, length: numbe if (length < 2) return; const swap = (a: number, b: number): void => { - const key = keys[a]; keys[a] = keys[b]; keys[b] = key; - const a2 = a * 2, b2 = b * 2; - const lo = values[a2]; values[a2] = values[b2]; values[b2] = lo; - const hi = values[a2 + 1]; values[a2 + 1] = values[b2 + 1]; values[b2 + 1] = hi; + const key = keys[a]; + keys[a] = keys[b]; + keys[b] = key; + const a2 = a * 2, + b2 = b * 2; + const lo = values[a2]; + values[a2] = values[b2]; + values[b2] = lo; + const hi = values[a2 + 1]; + values[a2 + 1] = values[b2 + 1]; + values[b2 + 1] = hi; }; const stack = new Int32Array(64); @@ -55,8 +62,12 @@ function sortKeyMaskPairs(keys: Float64Array, values: Uint32Array, length: numbe let i = lo; let j = hi - 1; while (true) { - while (keys[++i] < pivot) { /* sentinel at hi */ } - while (keys[--j] > pivot) { /* sentinel at lo */ } + while (keys[++i] < pivot) { + /* sentinel at hi */ + } + while (keys[--j] > pivot) { + /* sentinel at lo */ + } if (i >= j) break; swap(i, j); } diff --git a/src/lib/voxel/sparse-voxel-grid.ts b/src/lib/voxel/sparse-voxel-grid.ts index 0bd965a0..eeb43332 100644 --- a/src/lib/voxel/sparse-voxel-grid.ts +++ b/src/lib/voxel/sparse-voxel-grid.ts @@ -1,8 +1,8 @@ import { BlockMaskBuffer } from './block-mask-buffer'; import { BlockMaskMap } from './block-mask-map'; -const SOLID_LO = 0xFFFFFFFF >>> 0; -const SOLID_HI = 0xFFFFFFFF >>> 0; +const SOLID_LO = 0xffffffff >>> 0; +const SOLID_HI = 0xffffffff >>> 0; const BLOCK_EMPTY = 0; const BLOCK_SOLID = 1; @@ -14,18 +14,18 @@ const BLOCK_MIXED = 2; const FACE_MASKS_LO = [ 0x11111111 >>> 0, // -X: lx=0 0x88888888 >>> 0, // +X: lx=3 - 0x000F000F >>> 0, // -Y: ly=0 - 0xF000F000 >>> 0, // +Y: ly=3 - 0x0000FFFF >>> 0, // -Z: lz=0 - 0x00000000 >>> 0 // +Z: lz=3 + 0x000f000f >>> 0, // -Y: ly=0 + 0xf000f000 >>> 0, // +Y: ly=3 + 0x0000ffff >>> 0, // -Z: lz=0 + 0x00000000 >>> 0 // +Z: lz=3 ]; const FACE_MASKS_HI = [ 0x11111111 >>> 0, 0x88888888 >>> 0, - 0x000F000F >>> 0, - 0xF000F000 >>> 0, + 0x000f000f >>> 0, + 0xf000f000 >>> 0, 0x00000000 >>> 0, - 0xFFFF0000 >>> 0 + 0xffff0000 >>> 0 ]; // ============================================================================ @@ -60,8 +60,8 @@ const FACE_MASKS_HI = [ const TYPE_BITS_PER_BLOCK = 2; const BLOCKS_PER_WORD = 32 / TYPE_BITS_PER_BLOCK; // 16 const TYPE_MASK = (1 << TYPE_BITS_PER_BLOCK) - 1; // 0b11 -const SOLID_WORD = 0x55555555 >>> 0; // 16 lanes, each = SOLID -const EVEN_BITS = 0x55555555 >>> 0; // mask for even bit positions +const SOLID_WORD = 0x55555555 >>> 0; // 16 lanes, each = SOLID +const EVEN_BITS = 0x55555555 >>> 0; // mask for even bit positions /** * Read the 2-bit block type directly from a packed `types` Uint32Array. @@ -173,7 +173,8 @@ class SparseVoxelGrid { } } else { this.setBlockType(blockIdx, BLOCK_MIXED); - this.masks.set(blockIdx, + this.masks.set( + blockIdx, bitIdx < 32 ? (1 << bitIdx) >>> 0 : 0, bitIdx >= 32 ? (1 << (bitIdx - 32)) >>> 0 : 0 ); @@ -193,7 +194,7 @@ class SparseVoxelGrid { this.setBlockType(blockIdx, BLOCK_SOLID); } } else { - if ((lo >>> 0) === SOLID_LO && (hi >>> 0) === SOLID_HI) { + if (lo >>> 0 === SOLID_LO && hi >>> 0 === SOLID_HI) { this.setBlockType(blockIdx, BLOCK_SOLID); } else { this.setBlockType(blockIdx, BLOCK_MIXED); @@ -224,7 +225,9 @@ class SparseVoxelGrid { static fromBuffer( acc: BlockMaskBuffer, - nx: number, ny: number, nz: number, + nx: number, + ny: number, + nz: number, onProgress?: (done: number, total: number) => void ): SparseVoxelGrid { const solidIdx = acc.getSolidBlocks(); @@ -257,8 +260,12 @@ class SparseVoxelGrid { } toBuffer( - cropMinBx: number, cropMinBy: number, cropMinBz: number, - cropMaxBx: number, cropMaxBy: number, cropMaxBz: number, + cropMinBx: number, + cropMinBy: number, + cropMinBz: number, + cropMaxBx: number, + cropMaxBy: number, + cropMaxBz: number, onProgress?: (done: number, total: number) => void ): BlockMaskBuffer { const acc = new BlockMaskBuffer(); @@ -284,9 +291,7 @@ class SparseVoxelGrid { continue; } if (lo || hi) { - const outIdx = (bx - cropMinBx) + - (by - cropMinBy) * outNbx + - (bz - cropMinBz) * outBStride; + const outIdx = bx - cropMinBx + (by - cropMinBy) * outNbx + (bz - cropMinBz) * outBStride; acc.addBlock(outIdx, lo, hi); } } @@ -313,8 +318,12 @@ class SparseVoxelGrid { * @returns Newly allocated cropped grid. */ cropTo( - cropMinBx: number, cropMinBy: number, cropMinBz: number, - cropMaxBx: number, cropMaxBy: number, cropMaxBz: number, + cropMinBx: number, + cropMinBy: number, + cropMinBz: number, + cropMaxBx: number, + cropMaxBy: number, + cropMaxBz: number, onProgress?: (done: number, total: number) => void ): SparseVoxelGrid { const outNbx = cropMaxBx - cropMinBx; @@ -365,12 +374,16 @@ class SparseVoxelGrid { bz++; } } - if (bx < cropMinBx || bx >= cropMaxBx || - by < cropMinBy || by >= cropMaxBy || - bz < cropMinBz || bz >= cropMaxBz) continue; - const outIdx = (bx - cropMinBx) + - (by - cropMinBy) * outNbx + - (bz - cropMinBz) * outBStride; + if ( + bx < cropMinBx || + bx >= cropMaxBx || + by < cropMinBy || + by >= cropMaxBy || + bz < cropMinBz || + bz >= cropMaxBz + ) + continue; + const outIdx = bx - cropMinBx + (by - cropMinBy) * outNbx + (bz - cropMinBz) * outBStride; const bt = (word >>> (lane << 1)) & TYPE_MASK; writeBlockType(outTypes, outIdx, bt); if (bt === BLOCK_MIXED) { @@ -408,8 +421,12 @@ class SparseVoxelGrid { * @returns Newly allocated cropped + inverted grid. */ cropToInverted( - cropMinBx: number, cropMinBy: number, cropMinBz: number, - cropMaxBx: number, cropMaxBy: number, cropMaxBz: number, + cropMinBx: number, + cropMinBy: number, + cropMinBz: number, + cropMaxBx: number, + cropMaxBy: number, + cropMaxBz: number, onProgress?: (done: number, total: number) => void ): SparseVoxelGrid { const outNbx = cropMaxBx - cropMinBx; @@ -472,12 +489,16 @@ class SparseVoxelGrid { bz++; } } - if (bx < cropMinBx || bx >= cropMaxBx || - by < cropMinBy || by >= cropMaxBy || - bz < cropMinBz || bz >= cropMaxBz) continue; - const outIdx = (bx - cropMinBx) + - (by - cropMinBy) * outNbx + - (bz - cropMinBz) * outBStride; + if ( + bx < cropMinBx || + bx >= cropMaxBx || + by < cropMinBy || + by >= cropMaxBy || + bz < cropMinBz || + bz >= cropMaxBz + ) + continue; + const outIdx = bx - cropMinBx + (by - cropMinBy) * outNbx + (bz - cropMinBz) * outBStride; const bt = (word >>> (lane << 1)) & TYPE_MASK; if (bt === BLOCK_SOLID) { // Source SOLID → dest EMPTY (overwrite the SOLID default). @@ -486,7 +507,7 @@ class SparseVoxelGrid { // Source MIXED → dest MIXED with inverted mask. writeBlockType(outTypes, outIdx, BLOCK_MIXED); const s = masks.slot(blockIdx); - outMasks.set(outIdx, (~masks.lo[s]) >>> 0, (~masks.hi[s]) >>> 0); + outMasks.set(outIdx, ~masks.lo[s] >>> 0, ~masks.hi[s] >>> 0); } } } @@ -500,16 +521,22 @@ class SparseVoxelGrid { * @param onProgress - Optional progress callback over `types` words. * @returns Block coordinate bounds, or null if no blocks are occupied. */ - getOccupiedBlockBounds( - onProgress?: (done: number, total: number) => void - ): { - minBx: number; minBy: number; minBz: number; - maxBx: number; maxBy: number; maxBz: number; + getOccupiedBlockBounds(onProgress?: (done: number, total: number) => void): { + minBx: number; + minBy: number; + minBz: number; + maxBx: number; + maxBy: number; + maxBz: number; } | null { const { nbx, nby } = this; const totalBlocks = nbx * nby * this.nbz; - let minBx = nbx, minBy = nby, minBz = this.nbz; - let maxBx = 0, maxBy = 0, maxBz = 0; + let minBx = nbx, + minBy = nby, + minBz = this.nbz; + let maxBx = 0, + maxBy = 0, + maxBz = 0; let found = false; const PROGRESS_INTERVAL = 1 << 13; let nextTick = PROGRESS_INTERVAL; @@ -569,11 +596,13 @@ class SparseVoxelGrid { * @param onProgress - Optional progress callback over `types` words. * @returns Block coordinate bounds, or null if every block is solid. */ - getNavigableBlockBounds( - onProgress?: (done: number, total: number) => void - ): { - minBx: number; minBy: number; minBz: number; - maxBx: number; maxBy: number; maxBz: number; + getNavigableBlockBounds(onProgress?: (done: number, total: number) => void): { + minBx: number; + minBy: number; + minBz: number; + maxBx: number; + maxBy: number; + maxBz: number; } | null { const { nbx, nby } = this; const totalBlocks = nbx * nby * this.nbz; @@ -587,12 +616,15 @@ class SparseVoxelGrid { // navMask in the last word: only the first `lastLanes` lanes are valid; // each lane occupies 2 bits, so the valid even-bit mask spans // 2 * lastLanes bits. - const lastNonEmptyMask = lastLanes >= BLOCKS_PER_WORD ? - EVEN_BITS : - (((1 << (lastLanes * 2)) - 1) >>> 0) & EVEN_BITS; + const lastNonEmptyMask = + lastLanes >= BLOCKS_PER_WORD ? EVEN_BITS : (((1 << (lastLanes * 2)) - 1) >>> 0) & EVEN_BITS; - let minBx = nbx, minBy = nby, minBz = this.nbz; - let maxBx = -1, maxBy = 0, maxBz = 0; + let minBx = nbx, + minBy = nby, + minBz = this.nbz; + let maxBx = -1, + maxBy = 0, + maxBz = 0; const PROGRESS_INTERVAL = 1 << 13; let nextTick = PROGRESS_INTERVAL; @@ -655,7 +687,9 @@ class SparseVoxelGrid { */ static findNearestFreeCell( blocked: SparseVoxelGrid, - seedIx: number, seedIy: number, seedIz: number, + seedIx: number, + seedIy: number, + seedIz: number, maxRadius: number ): { ix: number; iy: number; iz: number } | null { const { nx, ny, nz } = blocked; diff --git a/src/lib/voxel/voxel-query.ts b/src/lib/voxel/voxel-query.ts index 026c7dad..a5b2e738 100644 --- a/src/lib/voxel/voxel-query.ts +++ b/src/lib/voxel/voxel-query.ts @@ -1,26 +1,24 @@ +import { computeGaussianInverse, evaluateGaussianAt } from '../data-table'; +import type { GaussianColumns } from '../data-table'; + import { ABSENT, BlockIndexMap, SOLID } from './block-index-map'; -import { BlockMaskBuffer } from './block-mask-buffer'; -import { - computeGaussianInverse, - evaluateGaussianAt, - type GaussianColumns -} from '../data-table'; +import type { BlockMaskBuffer } from './block-mask-buffer'; /** * Pre-computed lookup structures for efficient voxel block queries. */ -interface BlockLookup { +type BlockLookup = { /** Block index -> state: `SOLID`, a mixed-array slot (`>= 0`), or `ABSENT`. */ blocks: BlockIndexMap; masks: Uint32Array; solidCount: number; mixedCount: number; -} +}; /** * Grid parameters for block-based voxel queries. */ -interface BlockGridParams { +type BlockGridParams = { gridMinX: number; gridMinY: number; gridMinZ: number; @@ -31,7 +29,7 @@ interface BlockGridParams { numBlocksZ: number; strideY: number; strideZ: number; -} +}; /** * Build block lookup structures from the buffer's linear block indices. @@ -41,9 +39,7 @@ interface BlockGridParams { * @param buffer - Block mask buffer containing voxelized blocks. * @returns Block-state lookup, masks array, and solid/mixed counts. */ -const buildBlockLookup = ( - buffer: BlockMaskBuffer -): BlockLookup => { +const buildBlockLookup = (buffer: BlockMaskBuffer): BlockLookup => { const solidIdx = buffer.getSolidBlocks(); const mixed = buffer.getMixedBlocks(); const blocks = new BlockIndexMap(solidIdx.length + mixed.blockIdx.length); @@ -68,7 +64,9 @@ const buildBlockLookup = ( * @returns True if the center is in an occupied (and optionally filtered) voxel. */ const isCenterInOccupiedVoxel = ( - px: number, py: number, pz: number, + px: number, + py: number, + pz: number, grid: BlockGridParams, lookup: BlockLookup, blockFilter?: Set @@ -77,9 +75,14 @@ const isCenterInOccupiedVoxel = ( const centerBy = Math.floor((py - grid.gridMinY) / grid.blockSize); const centerBz = Math.floor((pz - grid.gridMinZ) / grid.blockSize); - if (centerBx < 0 || centerBx >= grid.numBlocksX || - centerBy < 0 || centerBy >= grid.numBlocksY || - centerBz < 0 || centerBz >= grid.numBlocksZ) { + if ( + centerBx < 0 || + centerBx >= grid.numBlocksX || + centerBy < 0 || + centerBy >= grid.numBlocksY || + centerBz < 0 || + centerBz >= grid.numBlocksZ + ) { return false; } @@ -135,7 +138,7 @@ const gaussianContributesToVoxels = ( lookup: BlockLookup, minContribution: number, blockFilter?: Set, - minHits: number = 1 + minHits = 1 ): boolean => { const px = columns.posX[gaussianIdx]; const py = columns.posY[gaussianIdx]; @@ -152,10 +155,14 @@ const gaussianContributesToVoxels = ( const aabbMaxBz = Math.min(grid.numBlocksZ - 1, Math.floor((pz + ez - grid.gridMinZ) / grid.blockSize)); const g = computeGaussianInverse( - columns.rotW[gaussianIdx], columns.rotX[gaussianIdx], - columns.rotY[gaussianIdx], columns.rotZ[gaussianIdx], - columns.scaleX[gaussianIdx], columns.scaleY[gaussianIdx], - columns.scaleZ[gaussianIdx], columns.opacity[gaussianIdx] + columns.rotW[gaussianIdx], + columns.rotX[gaussianIdx], + columns.rotY[gaussianIdx], + columns.rotZ[gaussianIdx], + columns.scaleX[gaussianIdx], + columns.scaleY[gaussianIdx], + columns.scaleZ[gaussianIdx], + columns.opacity[gaussianIdx] ); let hits = 0; @@ -174,8 +181,8 @@ const gaussianContributesToVoxels = ( const blockOriginY = grid.gridMinY + bby * grid.blockSize; const blockOriginZ = grid.gridMinZ + bbz * grid.blockSize; - const lo = isSolid ? 0xFFFFFFFF : lookup.masks[state * 2]; - const hi = isSolid ? 0xFFFFFFFF : lookup.masks[state * 2 + 1]; + const lo = isSolid ? 0xffffffff : lookup.masks[state * 2]; + const hi = isSolid ? 0xffffffff : lookup.masks[state * 2 + 1]; for (let lz = 0; lz < 4; lz++) { const vz = blockOriginZ + (lz + 0.5) * grid.voxelResolution; diff --git a/src/lib/voxel/voxelize.ts b/src/lib/voxel/voxelize.ts index c5115b41..8fbc4730 100644 --- a/src/lib/voxel/voxelize.ts +++ b/src/lib/voxel/voxelize.ts @@ -1,23 +1,21 @@ import { Vec3 } from 'playcanvas'; -import { BlockMaskBuffer } from './block-mask-buffer'; import type { Bounds } from '../data-table'; -import { - GpuVoxelization, - type BatchSpec, - type MultiBatchResult -} from '../gpu'; -import { type GaussianBVH } from '../spatial'; +import { GpuVoxelization } from '../gpu'; +import type { BatchSpec, MultiBatchResult } from '../gpu'; +import type { GaussianBVH } from '../spatial'; import { logger } from '../utils'; -interface PendingBatch extends BatchSpec { +import { BlockMaskBuffer } from './block-mask-buffer'; + +type PendingBatch = { bx: number; by: number; bz: number; numBlocksX: number; numBlocksY: number; numBlocksZ: number; -} +} & BatchSpec; /** * GPU-accelerated voxelization of Gaussian splat data into a block mask buffer. @@ -97,7 +95,7 @@ const voxelizeToBuffer = async ( if (maskLo === 0 && maskHi === 0) continue; const localX = blockIdx % batch.numBlocksX; - const localY = (blockIdx / batch.numBlocksX | 0) % batch.numBlocksY; + const localY = ((blockIdx / batch.numBlocksX) | 0) % batch.numBlocksY; const localZ = (blockIdx / (batch.numBlocksX * batch.numBlocksY)) | 0; const absBlockX = batch.bx + localX; @@ -156,8 +154,12 @@ const voxelizeToBuffer = async ( const blockMaxZ = blockMinZ + currBatchZ * blockSize; let overlappingCount = bvh.queryOverlappingRawInto( - blockMinX, blockMinY, blockMinZ, - blockMaxX, blockMaxY, blockMaxZ, + blockMinX, + blockMinY, + blockMinZ, + blockMaxX, + blockMaxY, + blockMaxZ, slotIndexArrays[currentSlot], indexOffset ); @@ -170,8 +172,12 @@ const voxelizeToBuffer = async ( if (needed > slotCapacities[currentSlot]) { ensureSlotIndexCapacity(currentSlot, needed, indexOffset); overlappingCount = bvh.queryOverlappingRawInto( - blockMinX, blockMinY, blockMinZ, - blockMaxX, blockMaxY, blockMaxZ, + blockMinX, + blockMinY, + blockMinZ, + blockMaxX, + blockMaxY, + blockMaxZ, slotIndexArrays[currentSlot], indexOffset ); @@ -224,8 +230,12 @@ const voxelizeToBuffer = async ( * @returns Aligned bounds */ function alignGridBounds( - minX: number, minY: number, minZ: number, - maxX: number, maxY: number, maxZ: number, + minX: number, + minY: number, + minZ: number, + maxX: number, + maxY: number, + maxZ: number, voxelResolution: number ): Bounds { const blockSize = 4 * voxelResolution; diff --git a/src/lib/workers/index.ts b/src/lib/workers/index.ts index cd60034e..28d0e8a2 100644 --- a/src/lib/workers/index.ts +++ b/src/lib/workers/index.ts @@ -1,6 +1,8 @@ +import type { TypedArray } from '../data-table/data-table'; +import { quantize1dColumns } from '../spatial/quantize-1d-core'; +import type { QuantizedColumns } from '../spatial/quantize-1d-core'; + import { WorkerQueue } from './worker-queue'; -import { type TypedArray } from '../data-table/data-table'; -import { quantize1dColumns, type QuantizedColumns } from '../spatial/quantize-1d-core'; /** * Typed client wrappers around WorkerQueue tasks. These own the marshalling @@ -17,7 +19,11 @@ import { quantize1dColumns, type QuantizedColumns } from '../spatial/quantize-1d * @returns Centroids (k Float32 codebook) and per-column Uint8 labels. * @ignore */ -const runQuantize1dColumns = async (columns: { name: string, data: TypedArray }[], k?: number, alpha?: number): Promise => { +const runQuantize1dColumns = async ( + columns: { name: string; data: TypedArray }[], + k?: number, + alpha?: number +): Promise => { if (WorkerQueue.isInline) { // zero-copy: no point marshalling when running on this thread anyway return quantize1dColumns(columns, k, alpha); @@ -25,8 +31,12 @@ const runQuantize1dColumns = async (columns: { name: string, data: TypedArray }[ // compact copies: column data may be views into larger shared buffers, // and the originals must remain usable after the transfer - const copies = columns.map(c => ({ name: c.name, data: c.data.slice() })); - return await WorkerQueue.run('quantize1d', { columns: copies, k, alpha }, copies.map(c => c.data.buffer as ArrayBuffer)); + const copies = columns.map((c) => ({ name: c.name, data: c.data.slice() })); + return await WorkerQueue.run( + 'quantize1d', + { columns: copies, k, alpha }, + copies.map((c) => c.data.buffer as ArrayBuffer) + ); }; /** diff --git a/src/lib/workers/tasks.ts b/src/lib/workers/tasks.ts index 61408e21..9818cc15 100644 --- a/src/lib/workers/tasks.ts +++ b/src/lib/workers/tasks.ts @@ -1,9 +1,12 @@ import type { TypedArray } from '../data-table/data-table'; -import { knnForestQuery, type ForestPart } from '../decimate/knn-core'; +import { knnForestQuery } from '../decimate/knn-core'; +import type { ForestPart } from '../decimate/knn-core'; import { mergeGroup, createMergeScratch, splatMass } from '../decimate/moment-match'; import { knnQueryBlock } from '../decimate-uniform/knn-core'; -import { buildFlatKdTree, type FlatKdTree } from '../spatial/kd-tree'; -import { quantize1dColumns, type QuantizedColumns } from '../spatial/quantize-1d-core'; +import { buildFlatKdTree } from '../spatial/kd-tree'; +import type { FlatKdTree } from '../spatial/kd-tree'; +import { quantize1dColumns } from '../spatial/quantize-1d-core'; +import type { QuantizedColumns } from '../spatial/quantize-1d-core'; import { WebPCodec } from '../utils/webp-codec'; /** @@ -20,19 +23,25 @@ import { WebPCodec } from '../utils/webp-codec'; * drags in the playcanvas engine. */ -type TaskOutput = { result: T, transfer: ArrayBuffer[] }; +type TaskOutput = { result: T; transfer: ArrayBuffer[] }; const taskHandlers = { - quantize1d: (args: { columns: { name: string, data: TypedArray }[], k?: number, alpha?: number }): - TaskOutput => { + quantize1d: (args: { + columns: { name: string; data: TypedArray }[]; + k?: number; + alpha?: number; + }): TaskOutput => { const result = quantize1dColumns(args.columns, args.k, args.alpha); return { result, - transfer: [result.centroids.buffer as ArrayBuffer, ...result.labels.map(c => c.data.buffer as ArrayBuffer)] + transfer: [ + result.centroids.buffer as ArrayBuffer, + ...result.labels.map((c) => c.data.buffer as ArrayBuffer) + ] }; }, - encodeWebp: async (args: { rgba: Uint8Array, width: number, height: number }): Promise> => { + encodeWebp: async (args: { rgba: Uint8Array; width: number; height: number }): Promise> => { // create() memoizes the wasm module per realm (each worker compiles // its own copy on first use) const codec = await WebPCodec.create(); @@ -46,7 +55,11 @@ const taskHandlers = { // move to SharedArrayBuffers so knnForest query tasks read them without // copies (structured clone shares SABs). buildKdForestPart: (args: { - x: Float32Array, y: Float32Array, z: Float32Array, ids: Uint32Array, shared?: boolean + x: Float32Array; + y: Float32Array; + z: Float32Array; + ids: Uint32Array; + shared?: boolean; }): TaskOutput => { const { x, y, z, ids } = args; const flat = buildFlatKdTree(x, y, z); @@ -86,7 +99,10 @@ const taskHandlers = { return { result: { ...flat, aabb }, transfer: [ - flat.nodeSplatIdx.buffer, flat.nodePositions.buffer, flat.nodeChildren.buffer, aabb.buffer + flat.nodeSplatIdx.buffer, + flat.nodePositions.buffer, + flat.nodeChildren.buffer, + aabb.buffer ] as ArrayBuffer[] }; }, @@ -94,7 +110,10 @@ const taskHandlers = { // Decimation CPU-fallback KNN: exact forest k-NN for a slice of queries, // as global ids. Part arrays are SAB-backed (shared, no copies). knnForest: (args: { - parts: ForestPart[], queryPos: Float32Array, queryIds: Uint32Array, k: number + parts: ForestPart[]; + queryPos: Float32Array; + queryIds: Uint32Array; + k: number; }): TaskOutput => { const count = args.queryIds.length; const out = new Uint32Array(count * args.k); @@ -119,15 +138,13 @@ const taskHandlers = { const flat = buildFlatKdTree(x, y, z); return { result: flat, - transfer: [ - flat.nodeSplatIdx.buffer, flat.nodePositions.buffer, flat.nodeChildren.buffer - ] as ArrayBuffer[] + transfer: [flat.nodeSplatIdx.buffer, flat.nodePositions.buffer, flat.nodeChildren.buffer] as ArrayBuffer[] }; }, // `--decimate-uniform` CPU-fallback block KNN: exact k-NN of the owned // prefix within the local point set, as local indices. Frozen. - knnBlock: (args: { positions: Float32Array, ownedCount: number, k: number }): TaskOutput => { + knnBlock: (args: { positions: Float32Array; ownedCount: number; k: number }): TaskOutput => { const result = knnQueryBlock(args.positions, args.ownedCount, args.k); return { result, transfer: [result.buffer as ArrayBuffer] }; }, @@ -137,14 +154,14 @@ const taskHandlers = { // per member, groups back to back per `sizes`); outputs are group-major. // `other` columns (when present) copy from the dominant-mass member. mergeGroups: (args: { - pos: Float32Array, - geo: Float32Array, - color: Float32Array, - sizes: Uint32Array, - colorDim: number, - other?: Uint32Array, - otherDim?: number - }): TaskOutput<{ pos: Float32Array, geo: Float32Array, color: Float32Array, other?: Uint32Array }> => { + pos: Float32Array; + geo: Float32Array; + color: Float32Array; + sizes: Uint32Array; + colorDim: number; + other?: Uint32Array; + otherDim?: number; + }): TaskOutput<{ pos: Float32Array; geo: Float32Array; color: Float32Array; other?: Uint32Array }> => { const { sizes, colorDim } = args; const g = sizes.length; const otherDim = args.otherDim ?? 0; @@ -170,7 +187,8 @@ const taskHandlers = { outGeo.set(merged.geo, gi * 8); outColor.set(merged.color, gi * colorDim); if (outOther) { - let dominant = base, best = -Infinity; + let dominant = base, + best = -Infinity; for (let m = 0; m < size; m++) { const mass = splatMass(args.geo, base + m); if (mass > best) { @@ -184,26 +202,26 @@ const taskHandlers = { } base += size; } - const transfer: ArrayBuffer[] = [outPos.buffer as ArrayBuffer, outGeo.buffer as ArrayBuffer, outColor.buffer as ArrayBuffer]; + const transfer: ArrayBuffer[] = [ + outPos.buffer as ArrayBuffer, + outGeo.buffer as ArrayBuffer, + outColor.buffer as ArrayBuffer + ]; if (outOther) transfer.push(outOther.buffer as ArrayBuffer); return { result: { pos: outPos, geo: outGeo, color: outColor, other: outOther }, transfer }; } }; type TaskName = keyof typeof taskHandlers; -type TaskArgs = Parameters[0]; -type TaskResult = Awaited>['result']; +type TaskArgs = Parameters<(typeof taskHandlers)[T]>[0]; +type TaskResult = Awaited>['result']; // Message protocol between host and worker. There are no per-task ids: each // worker runs strictly one task at a time (enforced by the host's slot state // machine), so every reply pairs with the single in-flight `run`. -type HostMessage = - | { type: 'init', wasmUrl: string } - | { type: 'run', task: TaskName, args: any }; +type HostMessage = { type: 'init'; wasmUrl: string } | { type: 'run'; task: TaskName; args: any }; type WorkerMessage = - | { type: 'ready' } - | { type: 'result', result: any } - | { type: 'error', message: string, stack?: string }; + { type: 'ready' } | { type: 'result'; result: any } | { type: 'error'; message: string; stack?: string }; export { taskHandlers, type TaskName, type TaskArgs, type TaskResult, type HostMessage, type WorkerMessage }; diff --git a/src/lib/workers/worker-entry.ts b/src/lib/workers/worker-entry.ts index 1a04f434..0bc3b478 100644 --- a/src/lib/workers/worker-entry.ts +++ b/src/lib/workers/worker-entry.ts @@ -1,6 +1,8 @@ -import { taskHandlers, type HostMessage, type WorkerMessage } from './tasks'; import { WebPCodec } from '../utils/webp-codec'; +import { taskHandlers } from './tasks'; +import type { HostMessage, WorkerMessage } from './tasks'; + /** * Worker-side entry point, built and shipped as `dist/worker.mjs` (see * rollup.config.mjs) and spawned by WorkerQueue from a URL. Runs one task at a @@ -23,11 +25,14 @@ const bind = ( const { result, transfer } = await taskHandlers[message.task](message.args); post({ type: 'result', result }, transfer); } catch (err) { - post({ - type: 'error', - message: err instanceof Error ? err.message : String(err), - stack: err instanceof Error ? err.stack : undefined - }, []); + post( + { + type: 'error', + message: err instanceof Error ? err.message : String(err), + stack: err instanceof Error ? err.stack : undefined + }, + [] + ); } }); @@ -46,7 +51,7 @@ if (typeof process !== 'undefined' && !!process.versions?.node && (process as an import('node:worker_threads').then(({ parentPort }) => { bind( (message, transfer) => parentPort.postMessage(message, transfer), - handler => parentPort.on('message', handler) + (handler) => parentPort.on('message', handler) ); }); } else { diff --git a/src/lib/workers/worker-queue.ts b/src/lib/workers/worker-queue.ts index a8031b00..051e8c53 100644 --- a/src/lib/workers/worker-queue.ts +++ b/src/lib/workers/worker-queue.ts @@ -1,7 +1,9 @@ -import { taskHandlers, type TaskName, type TaskArgs, type TaskResult, type WorkerMessage } from './tasks'; -import { workerBundled } from './worker-bundled'; import { WebPCodec } from '../utils/webp-codec'; +import { taskHandlers } from './tasks'; +import type { TaskName, TaskArgs, TaskResult, WorkerMessage } from './tasks'; +import { workerBundled } from './worker-bundled'; + type PendingTask = { task: TaskName; args: any; @@ -151,7 +153,7 @@ async function spawnSlot(slot: Slot) { slot.post = (message, transfer) => worker.postMessage(message, transfer); slot.terminate = () => worker.terminate(); worker.onmessage = (event: MessageEvent) => onSlotMessage(slot, event.data); - worker.onerror = event => onSlotDeath(slot, new Error(event.message || 'worker load failed')); + worker.onerror = (event) => onSlotDeath(slot, new Error(event.message || 'worker load failed')); } if (slot.dead) { @@ -178,24 +180,24 @@ async function ensureSpawned() { spawnLoopActive = true; try { if (resolvedMaxWorkers === null) { - const cores = isNode ? - (await import('node:os')).availableParallelism() : - (navigator.hardwareConcurrency || 2); + const cores = isNode + ? (await import('node:os')).availableParallelism() + : navigator.hardwareConcurrency || 2; resolvedMaxWorkers = Math.max(1, Math.min(4, cores - 1)); } const max = maxWorkers ?? resolvedMaxWorkers; - let available = slots.filter(s => s.state !== 'busy').length; + let available = slots.filter((s) => s.state !== 'busy').length; while (slots.length < max && available < queue.length) { const slot: Slot = { state: 'starting', current: null, dead: false, - post: () => {}, - terminate: () => {}, - ref: () => {}, - unref: () => {} + post: () => undefined, + terminate: () => undefined, + ref: () => undefined, + unref: () => undefined }; slots.push(slot); spawnSlot(slot); @@ -213,7 +215,7 @@ async function ensureSpawned() { function dispatch() { while (queue.length > 0) { - const slot = slots.find(s => s.state === 'idle'); + const slot = slots.find((s) => s.state === 'idle'); if (!slot) { ensureSpawned(); break; @@ -257,6 +259,7 @@ const enqueue = (task: T, args: TaskArgs, transfer: Array }); outstanding.add(promise); + // eslint-disable-next-line @typescript-eslint/no-empty-function -- preserve rejection-only control flow promise.catch(() => {}).then(() => outstanding.delete(promise)); return promise; @@ -286,6 +289,7 @@ const destroyPool = async () => { * every task runs inline on the calling thread instead - same code, same * results, just serial. */ +// eslint-disable-next-line @typescript-eslint/no-extraneous-class class WorkerQueue { /** * Sets the URL of the worker script (the shipped dist/worker.mjs). Set diff --git a/src/lib/write.ts b/src/lib/write.ts index 8ae3838a..27cd813d 100644 --- a/src/lib/write.ts +++ b/src/lib/write.ts @@ -1,10 +1,21 @@ -import { type ChunkDataPool, type ChunkLayer, type ChunkSource } from './chunk'; +import type { ChunkDataPool, ChunkLayer, ChunkSource } from './chunk'; import { materializeToDataTable } from './compat/data-table'; import { DataTable } from './data-table'; -import { type FileSystem } from './io/write'; -import { type SplatModel } from './splat-model'; -import { type DeviceCreator, type Options } from './types'; -import { writeCompressedPly, writeCsv, writeGlb, writeHtml, writeImage, writePly, writeSog, writeSogSource, writeSpz, writeVoxel } from './writers'; +import type { FileSystem } from './io/write'; +import type { SplatModel } from './splat-model'; +import type { DeviceCreator, Options } from './types'; +import { + writeCompressedPly, + writeCsv, + writeGlb, + writeHtml, + writeImage, + writePly, + writeSog, + writeSogSource, + writeSpz, + writeVoxel +} from './writers'; import { splatModelComment } from './writers/utils'; import { writeCompressedPlySource } from './writers/write-compressed-ply'; import { writePlyStreaming } from './writers/write-ply-streaming'; @@ -27,7 +38,20 @@ import { writeSplatStreaming } from './writers/write-splat-streaming'; * - `voxel` - Sparse voxel octree format for collision detection * - `image` - Rasterized RGBA image (lossless WebP) rendered from a camera view */ -type OutputFormat = 'csv' | 'sog' | 'sog-bundle' | 'lod' | 'compressed-ply' | 'ply' | 'splat' | 'spz' | 'glb' | 'html' | 'html-bundle' | 'voxel' | 'image'; +type OutputFormat = + | 'csv' + | 'sog' + | 'sog-bundle' + | 'lod' + | 'compressed-ply' + | 'ply' + | 'splat' + | 'spz' + | 'glb' + | 'html' + | 'html-bundle' + | 'voxel' + | 'image'; /** * Options for writing a Gaussian splat file. @@ -126,17 +150,22 @@ const writeFile = async (writeOptions: WriteOptions, fs: FileSystem) => { break; case 'sog': case 'sog-bundle': - await writeSog({ - filename, - dataTable, - model, - bundle: outputFormat === 'sog-bundle', - iterations: options.iterations ?? 10, - createDevice - }, fs); + await writeSog( + { + filename, + dataTable, + model, + bundle: outputFormat === 'sog-bundle', + iterations: options.iterations ?? 10, + createDevice + }, + fs + ); break; case 'lod': - throw new Error('lod-meta.json output is written from a multi-LOD ChunkSource via writeLodSource, not from a DataTable.'); + throw new Error( + 'lod-meta.json output is written from a multi-LOD ChunkSource via writeLodSource, not from a DataTable.' + ); case 'compressed-ply': await writeCompressedPly({ filename, dataTable, model }, fs); break; @@ -146,83 +175,100 @@ const writeFile = async (writeOptions: WriteOptions, fs: FileSystem) => { const comment = splatModelComment(model); // 2DGS has no third scale axis: it was materialized on read to keep // the pipeline uniform, so drop it again here. - const columns = model === '2dgs' ? - dataTable.columns.filter(c => c.name !== 'scale_2') : - dataTable.columns; - await writePly({ - filename, - plyData: { - comments: comment ? [comment] : [], - elements: [{ - name: 'vertex', - dataTable: columns.length === dataTable.columns.length ? - dataTable : - new DataTable(columns, dataTable.transform) - }] - } - }, fs); + const columns = + model === '2dgs' ? dataTable.columns.filter((c) => c.name !== 'scale_2') : dataTable.columns; + await writePly( + { + filename, + plyData: { + comments: comment ? [comment] : [], + elements: [ + { + name: 'vertex', + dataTable: + columns.length === dataTable.columns.length + ? dataTable + : new DataTable(columns, dataTable.transform) + } + ] + } + }, + fs + ); break; } case 'spz': - await writeSpz({ - filename, - dataTable, - model, - version: options.spzVersion ?? 4 - }, fs); + await writeSpz( + { + filename, + dataTable, + model, + version: options.spzVersion ?? 4 + }, + fs + ); break; case 'glb': await writeGlb({ filename, dataTable }, fs); break; case 'html': case 'html-bundle': - await writeHtml({ - filename, - dataTable, - viewerSettingsJson: options.viewerSettingsJson, - bundle: outputFormat === 'html-bundle', - iterations: options.iterations ?? 10, - createDevice - }, fs); + await writeHtml( + { + filename, + dataTable, + viewerSettingsJson: options.viewerSettingsJson, + bundle: outputFormat === 'html-bundle', + iterations: options.iterations ?? 10, + createDevice + }, + fs + ); break; case 'voxel': - await writeVoxel({ - filename, - dataTable, - voxelResolution: options.voxelResolution, - opacityCutoff: options.opacityCutoff, - navExteriorRadius: options.navExteriorRadius, - floorFill: options.floorFill, - floorFillDilation: options.floorFillDilation, - navCapsule: options.navCapsule, - navSeed: options.navSeed, - collisionMesh: options.collisionMesh, - createDevice - }, fs); + await writeVoxel( + { + filename, + dataTable, + voxelResolution: options.voxelResolution, + opacityCutoff: options.opacityCutoff, + navExteriorRadius: options.navExteriorRadius, + floorFill: options.floorFill, + floorFillDilation: options.floorFillDilation, + navCapsule: options.navCapsule, + navSeed: options.navSeed, + collisionMesh: options.collisionMesh, + createDevice + }, + fs + ); break; case 'image': - await writeImage({ - filename, - dataTable, - projection: options.renderProjection, - cameraPosition: options.renderCameraPosition, - lookAt: options.renderLookAt, - up: options.renderUp, - fov: options.renderFov, - width: options.renderWidth, - height: options.renderHeight, - near: options.renderNear, - background: options.renderBackground, - fStop: options.renderFStop, - focusDistance: options.renderFocusDistance, - sensorSize: options.renderSensorSize, - cameraEndPosition: options.renderCameraEndPosition, - lookAtEnd: options.renderLookAtEnd, - upEnd: options.renderUpEnd, - shutter: options.renderShutter, - motionSamples: options.renderMotionSamples, - createDevice - }, fs); + await writeImage( + { + filename, + dataTable, + projection: options.renderProjection, + cameraPosition: options.renderCameraPosition, + lookAt: options.renderLookAt, + up: options.renderUp, + fov: options.renderFov, + width: options.renderWidth, + height: options.renderHeight, + near: options.renderNear, + background: options.renderBackground, + fStop: options.renderFStop, + focusDistance: options.renderFocusDistance, + sensorSize: options.renderSensorSize, + cameraEndPosition: options.renderCameraEndPosition, + lookAtEnd: options.renderLookAtEnd, + upEnd: options.renderUpEnd, + shutter: options.renderShutter, + motionSamples: options.renderMotionSamples, + createDevice + }, + fs + ); break; } }; @@ -266,12 +312,17 @@ const writeSource = async (writeSourceOptions: WriteSourceOptions, fs: FileSyste break; case 'sog': case 'sog-bundle': - await writeSogSource(source, pool, { - filename, - bundle: outputFormat === 'sog-bundle', - iterations: options.iterations ?? 10, - createDevice - }, fs); + await writeSogSource( + source, + pool, + { + filename, + bundle: outputFormat === 'sog-bundle', + iterations: options.iterations ?? 10, + createDevice + }, + fs + ); break; case 'compressed-ply': await writeCompressedPlySource(source, pool, { filename }, fs); @@ -286,7 +337,11 @@ const writeSource = async (writeSourceOptions: WriteSourceOptions, fs: FileSyste // x/y/z, rot, scale, opacity — no color/SH). Materialize just those // layers so color and SH are never loaded (they were previously read // into the full table and discarded). - const dataTable = await materializeToDataTable(source, pool, new Set(['position', 'geometric'])); + const dataTable = await materializeToDataTable( + source, + pool, + new Set(['position', 'geometric']) + ); await writeFile({ filename, outputFormat, dataTable, model: source.meta.model, options, createDevice }, fs); break; } diff --git a/src/lib/writers/collision-glb.ts b/src/lib/writers/collision-glb.ts index c1894085..36d4e6b0 100644 --- a/src/lib/writers/collision-glb.ts +++ b/src/lib/writers/collision-glb.ts @@ -1,8 +1,9 @@ import type { Bounds } from '../data-table'; -import { coplanarMerge, marchingCubes, voxelFaces, type Mesh } from '../mesh'; +import { coplanarMerge, marchingCubes, voxelFaces } from '../mesh'; +import type { Mesh } from '../mesh'; import type { CollisionMeshShape } from '../types'; import { fmtCount, logger } from '../utils'; -import { SparseVoxelGrid } from '../voxel/sparse-voxel-grid'; +import type { SparseVoxelGrid } from '../voxel/sparse-voxel-grid'; /** * Build a minimal GLB (glTF 2.0 binary) file containing a single triangle mesh. @@ -18,10 +19,16 @@ function encodeGlb(positions: Float32Array, indices: Uint32Array): Uint8Array { const vertexCount = positions.length / 3; const indexCount = indices.length; - let minX = Infinity, minY = Infinity, minZ = Infinity; - let maxX = -Infinity, maxY = -Infinity, maxZ = -Infinity; + let minX = Infinity, + minY = Infinity, + minZ = Infinity; + let maxX = -Infinity, + maxY = -Infinity, + maxZ = -Infinity; for (let i = 0; i < positions.length; i += 3) { - const x = positions[i], y = positions[i + 1], z = positions[i + 2]; + const x = positions[i], + y = positions[i + 1], + z = positions[i + 2]; if (x < minX) minX = x; if (y < minY) minY = y; if (z < minZ) minZ = z; @@ -39,12 +46,16 @@ function encodeGlb(positions: Float32Array, indices: Uint32Array): Uint8Array { scene: 0, scenes: [{ nodes: [0] }], nodes: [{ mesh: 0 }], - meshes: [{ - primitives: [{ - attributes: { POSITION: 0 }, - indices: 1 - }] - }], + meshes: [ + { + primitives: [ + { + attributes: { POSITION: 0 }, + indices: 1 + } + ] + } + ], accessors: [ { bufferView: 0, @@ -98,23 +109,31 @@ function encodeGlb(positions: Float32Array, indices: Uint32Array): Uint8Array { let offset = 0; // GLB header - view.setUint32(offset, 0x46546C67, true); offset += 4; // magic: "glTF" - view.setUint32(offset, 2, true); offset += 4; // version: 2 - view.setUint32(offset, totalLength, true); offset += 4; // total length + view.setUint32(offset, 0x46546c67, true); + offset += 4; // magic: "glTF" + view.setUint32(offset, 2, true); + offset += 4; // version: 2 + view.setUint32(offset, totalLength, true); + offset += 4; // total length // JSON chunk header - view.setUint32(offset, jsonChunkLength, true); offset += 4; - view.setUint32(offset, 0x4E4F534A, true); offset += 4; // type: "JSON" + view.setUint32(offset, jsonChunkLength, true); + offset += 4; + view.setUint32(offset, 0x4e4f534a, true); + offset += 4; // type: "JSON" // JSON chunk data - byteArray.set(jsonBytes, offset); offset += jsonBytes.length; + byteArray.set(jsonBytes, offset); + offset += jsonBytes.length; for (let i = 0; i < jsonPadding; i++) { byteArray[offset++] = 0x20; } // BIN chunk header - view.setUint32(offset, binChunkLength, true); offset += 4; - view.setUint32(offset, 0x004E4942, true); offset += 4; // type: "BIN\0" + view.setUint32(offset, binChunkLength, true); + offset += 4; + view.setUint32(offset, 0x004e4942, true); + offset += 4; // type: "BIN\0" // BIN chunk data: positions then indices byteArray.set(new Uint8Array(positions.buffer, positions.byteOffset, positionsByteLength), offset); diff --git a/src/lib/writers/compressed-chunk.ts b/src/lib/writers/compressed-chunk.ts index b8b9063c..0703096a 100644 --- a/src/lib/writers/compressed-chunk.ts +++ b/src/lib/writers/compressed-chunk.ts @@ -7,10 +7,20 @@ const q = new Quat(); // process and compress a chunk of 256 splats class CompressedChunk { static members = [ - 'x', 'y', 'z', - 'scale_0', 'scale_1', 'scale_2', - 'f_dc_0', 'f_dc_1', 'f_dc_2', 'opacity', - 'rot_0', 'rot_1', 'rot_2', 'rot_3' + 'x', + 'y', + 'z', + 'scale_0', + 'scale_1', + 'scale_2', + 'f_dc_0', + 'f_dc_1', + 'f_dc_2', + 'opacity', + 'rot_0', + 'rot_1', + 'rot_2', + 'rot_3' ]; size: number; @@ -57,7 +67,7 @@ class CompressedChunk { const normalize = (x: number, min: number, max: number) => { if (x <= min) return 0; if (x >= max) return 1; - return (max - min < 0.00001) ? 0 : (x - min) / (max - min); + return max - min < 0.00001 ? 0 : (x - min) / (max - min); }; const data = this.data; @@ -112,16 +122,11 @@ class CompressedChunk { }; const pack111011 = (x: number, y: number, z: number) => { - return packUnorm(x, 11) << 21 | - packUnorm(y, 10) << 11 | - packUnorm(z, 11); + return (packUnorm(x, 11) << 21) | (packUnorm(y, 10) << 11) | packUnorm(z, 11); }; const pack8888 = (x: number, y: number, z: number, w: number) => { - return packUnorm(x, 8) << 24 | - packUnorm(y, 8) << 16 | - packUnorm(z, 8) << 8 | - packUnorm(w, 8); + return (packUnorm(x, 8) << 24) | (packUnorm(y, 8) << 16) | (packUnorm(z, 8) << 8) | packUnorm(w, 8); }; // pack quaternion into 2,10,10,10 @@ -172,11 +177,29 @@ class CompressedChunk { ); } - this.chunkData.set([ - px.min, py.min, pz.min, px.max, py.max, pz.max, - sx.min, sy.min, sz.min, sx.max, sy.max, sz.max, - cr.min, cg.min, cb.min, cr.max, cg.max, cb.max - ], 0); + this.chunkData.set( + [ + px.min, + py.min, + pz.min, + px.max, + py.max, + pz.max, + sx.min, + sy.min, + sz.min, + sx.max, + sy.max, + sz.max, + cr.min, + cg.min, + cb.min, + cr.max, + cg.max, + cb.max + ], + 0 + ); } } diff --git a/src/lib/writers/sparse-octree.ts b/src/lib/writers/sparse-octree.ts index 2ddb7b59..d3610751 100644 --- a/src/lib/writers/sparse-octree.ts +++ b/src/lib/writers/sparse-octree.ts @@ -2,16 +2,16 @@ import { Vec3 } from 'playcanvas'; import type { Bounds } from '../data-table'; import { logger } from '../utils'; -import { BlockMaskBuffer } from '../voxel/block-mask-buffer'; +import type { BlockMaskBuffer } from '../voxel/block-mask-buffer'; import { popcount, xyzToMorton } from '../voxel/morton'; import { sortKeyMaskPairs } from '../voxel/sort-key-mask'; +import type { SparseVoxelGrid } from '../voxel/sparse-voxel-grid'; import { BLOCK_EMPTY, BLOCK_MIXED, BLOCK_SOLID, BLOCKS_PER_WORD, EVEN_BITS, - SparseVoxelGrid, TYPE_MASK, readBlockType, writeBlockType @@ -22,7 +22,7 @@ import { * This is unambiguous because BFS layout guarantees children always come after * their parent, so baseOffset = 0 is never valid for an interior node. */ -const SOLID_LEAF_MARKER = 0xFF000000 >>> 0; +const SOLID_LEAF_MARKER = 0xff000000 >>> 0; /** * Maximum value encodable in the low 24 bits of a Laine-Karras node word. @@ -32,7 +32,7 @@ const SOLID_LEAF_MARKER = 0xFF000000 >>> 0; * values. (Interior nodes additionally reserve baseOffset=0 for * `SOLID_LEAF_MARKER`, so the practical interior cap is 16,777,215.) */ -const MAX_24BIT_OFFSET = 0x00FFFFFF; +const MAX_24BIT_OFFSET = 0x00ffffff; const MAX_V1_MIXED_LEAVES = MAX_24BIT_OFFSET + 1; const DENSE_SOLID_STREAM_THRESHOLD = 8_000_000; @@ -44,7 +44,7 @@ const DENSE_SOLID_STREAM_THRESHOLD = 8_000_000; /** * Sparse voxel octree using Laine-Karras node format. */ -interface SparseOctree { +type SparseOctree = { /** Grid bounds aligned to 4x4x4 block boundaries */ gridBounds: Bounds; @@ -71,14 +71,14 @@ interface SparseOctree { /** Voxel masks for mixed leaves: pairs of u32 (lo, hi) */ leafData: Uint32Array; -} +}; -interface BuildSparseOctreeOptions { +type BuildSparseOctreeOptions = { /** Release the input grid's backing storage after the octree has copied the data it needs. */ consumeGrid?: boolean; /** Force dense-mip construction; intended for tests and benchmarks. */ dense?: boolean; -} +}; // ============================================================================ // Octree Node Types (during construction) @@ -99,33 +99,33 @@ const enum BlockType { * (in `solidStream` + `mixedStream` + `mixedMasks`), so every entry stored * here is an interior node and `childMasks` is always non-null. */ -interface LevelData { +type LevelData = { /** Sorted Morton codes for nodes at this level */ mortons: Float64Array; /** Block type for each node (Solid or Mixed) */ types: Uint8Array; /** 8-bit child presence mask for each node */ childMasks: Uint8Array; -} +}; /** * Interior nodes waiting for child emission during BFS flattening. * Children are written immediately, so this wave never contains leaves. */ -interface InteriorWave { +type InteriorWave = { pos: Uint32Array; li: Int32Array; ii: Uint32Array; length: number; -} +}; -interface DenseLevel { +type DenseLevel = { types: Uint32Array; nbx: number; nby: number; nbz: number; nonEmptyCount: number; -} +}; // ============================================================================ // Mixed-stream Sort @@ -192,12 +192,7 @@ function createInteriorWave(initialCapacity: number): InteriorWave { * @param li - Interior level index. * @param ii - Node index within the level. */ -function pushInteriorWave( - wave: InteriorWave, - pos: number, - li: number, - ii: number -): void { +function pushInteriorWave(wave: InteriorWave, pos: number, li: number, ii: number): void { if (wave.length === wave.pos.length) { const cap = wave.pos.length * 2; const grownPos = new Uint32Array(cap); @@ -249,9 +244,7 @@ function calculateTreeDepth(gridBounds: Bounds, voxelResolution: number): number * @returns True when dense construction is likely to use less memory. */ function shouldUseDenseMipBuild(totalBlocks: number, nSolid: number, nMixed: number): boolean { - return nSolid >= DENSE_SOLID_STREAM_THRESHOLD && - nSolid > nMixed * 4 && - nSolid > totalBlocks * 0.25; + return nSolid >= DENSE_SOLID_STREAM_THRESHOLD && nSolid > nMixed * 4 && nSolid > totalBlocks * 0.25; } /** @@ -263,13 +256,15 @@ function shouldUseDenseMipBuild(totalBlocks: number, nSolid: number, nMixed: num * @returns Dense type levels from leaf blocks to root. */ function buildDenseTypeLevels(grid: SparseVoxelGrid, maxDepth: number): DenseLevel[] { - const levels: DenseLevel[] = [{ - types: grid.types, - nbx: grid.nbx, - nby: grid.nby, - nbz: grid.nbz, - nonEmptyCount: 0 - }]; + const levels: DenseLevel[] = [ + { + types: grid.types, + nbx: grid.nbx, + nby: grid.nby, + nbz: grid.nbz, + nonEmptyCount: 0 + } + ]; for (let li = 1; li <= maxDepth; li++) { const prev = levels[li - 1]; @@ -336,7 +331,7 @@ function buildDenseTypeLevels(grid: SparseVoxelGrid, maxDepth: number): DenseLev * @returns Sparse octree structure. */ function flattenDenseLevels( - levels: Array, + levels: (DenseLevel | null)[], grid: SparseVoxelGrid, gridBounds: Bounds, sceneBounds: Bounds, @@ -345,8 +340,8 @@ function flattenDenseLevels( if (grid.masks.size > MAX_V1_MIXED_LEAVES) { throw new Error( `Sparse octree mixed-leaf count (${grid.masks.size}) exceeds the ` + - `Laine-Karras 24-bit baseOffset limit (${MAX_V1_MIXED_LEAVES}). ` + - 'Use a coarser voxel resolution.' + `Laine-Karras 24-bit baseOffset limit (${MAX_V1_MIXED_LEAVES}). ` + + 'Use a coarser voxel resolution.' ); } const treeDepth = Math.max(1, levels.length - 1); @@ -379,8 +374,8 @@ function flattenDenseLevels( if (nodeLen >= MAX_24BIT_OFFSET + 1) { throw new Error( `Sparse octree node count (${nodeLen + 1}) exceeds the ` + - `Laine-Karras 24-bit baseOffset limit (${MAX_24BIT_OFFSET + 1}). ` + - 'Use a coarser voxel resolution.' + `Laine-Karras 24-bit baseOffset limit (${MAX_24BIT_OFFSET + 1}). ` + + 'Use a coarser voxel resolution.' ); } if (nodeLen === nodes.length) { @@ -397,8 +392,8 @@ function flattenDenseLevels( if (leafDataIndex > MAX_24BIT_OFFSET) { throw new Error( `Sparse octree mixed-leaf count (${leafDataIndex + 1}) exceeds the ` + - `Laine-Karras 24-bit baseOffset limit (${MAX_24BIT_OFFSET + 1}). ` + - 'Use a coarser voxel resolution.' + `Laine-Karras 24-bit baseOffset limit (${MAX_24BIT_OFFSET + 1}). ` + + 'Use a coarser voxel resolution.' ); } if (leafDataLen + 2 > leafData.length) { @@ -453,8 +448,8 @@ function flattenDenseLevels( if (childStart > MAX_24BIT_OFFSET) { throw new Error( `Sparse octree node count (${childStart + 1}) exceeds the ` + - `Laine-Karras 24-bit baseOffset limit (${MAX_24BIT_OFFSET + 1}). ` + - 'Use a coarser voxel resolution.' + `Laine-Karras 24-bit baseOffset limit (${MAX_24BIT_OFFSET + 1}). ` + + 'Use a coarser voxel resolution.' ); } @@ -479,7 +474,7 @@ function flattenDenseLevels( } } - nodes[curWave.pos[w]] = ((childMask & 0xFF) << 24) | childStart; + nodes[curWave.pos[w]] = ((childMask & 0xff) << 24) | childStart; } levels[currentLi] = null; @@ -582,9 +577,7 @@ function buildSparseOctree( const treeDepth = calculateTreeDepth(gridBounds, voxelResolution); const lastWordIdx = gridTypes.length - 1; const lastLanes = totalBlocks - lastWordIdx * BLOCKS_PER_WORD; - const lastValidWordMask = lastLanes >= BLOCKS_PER_WORD ? - 0xFFFFFFFF >>> 0 : - ((1 << (lastLanes * 2)) - 1) >>> 0; + const lastValidWordMask = lastLanes >= BLOCKS_PER_WORD ? 0xffffffff >>> 0 : ((1 << (lastLanes * 2)) - 1) >>> 0; // Pass 1: count. let nSolid = 0; @@ -594,16 +587,14 @@ function buildSparseOctree( if (w === lastWordIdx) word = (word & lastValidWordMask) >>> 0; if (word === 0) continue; // Solid lanes: 0b01 = 1; Mixed lanes: 0b10 = 2. - const solidMask = (word & EVEN_BITS) & ~((word >>> 1) & EVEN_BITS); - const mixedMask = ((word >>> 1) & EVEN_BITS) & ~(word & EVEN_BITS); + const solidMask = word & EVEN_BITS & ~((word >>> 1) & EVEN_BITS); + const mixedMask = (word >>> 1) & EVEN_BITS & ~(word & EVEN_BITS); nSolid += popcount(solidMask >>> 0); nMixed += popcount(mixedMask >>> 0); } if (options.dense || shouldUseDenseMipBuild(totalBlocks, nSolid, nMixed)) { - return buildSparseOctreeDense( - grid, gridBounds, sceneBounds, voxelResolution, treeDepth, !!options.consumeGrid - ); + return buildSparseOctreeDense(grid, gridBounds, sceneBounds, voxelResolution, treeDepth, !!options.consumeGrid); } const solidStream = new Float64Array(nSolid); @@ -651,16 +642,21 @@ function buildSparseOctree( if (nMixed > 1) sortKeyMaskPairs(mixedStream, mixedMasks, nMixed); return buildSparseOctreeFromStreams( - solidStream, mixedStream, mixedMasks, - gridBounds, sceneBounds, voxelResolution, treeDepth + solidStream, + mixedStream, + mixedMasks, + gridBounds, + sceneBounds, + voxelResolution, + treeDepth ); } -interface BlockBufferRegion { +type BlockBufferRegion = { minBx: number; minBy: number; minBz: number; -} +}; /** * Build a sparse octree directly from a voxelization buffer. The block-index @@ -693,8 +689,8 @@ function buildSparseOctreeFromBuffer( if (mixedStream.length > MAX_V1_MIXED_LEAVES) { throw new Error( `Sparse octree mixed-leaf count (${mixedStream.length}) exceeds the ` + - `Laine-Karras 24-bit baseOffset limit (${MAX_V1_MIXED_LEAVES}). ` + - 'Use a coarser voxel resolution.' + `Laine-Karras 24-bit baseOffset limit (${MAX_V1_MIXED_LEAVES}). ` + + 'Use a coarser voxel resolution.' ); } const sourceStride = sourceNbx * sourceNby; @@ -702,7 +698,7 @@ function buildSparseOctreeFromBuffer( if (!Number.isSafeInteger(totalBlocks)) { throw new Error( `Voxel source grid ${sourceNbx}x${sourceNby}x${sourceNbz} exceeds the safe-integer block-index limit. ` + - 'Use a coarser voxel resolution.' + 'Use a coarser voxel resolution.' ); } @@ -710,7 +706,9 @@ function buildSparseOctreeFromBuffer( for (let i = 0; i < stream.length; i++) { const blockIdx = stream[i]; if (!Number.isSafeInteger(blockIdx) || blockIdx < 0 || blockIdx >= totalBlocks) { - throw new Error(`Voxel block index ${blockIdx} is outside the ${sourceNbx}x${sourceNby}x${sourceNbz} grid`); + throw new Error( + `Voxel block index ${blockIdx} is outside the ${sourceNbx}x${sourceNby}x${sourceNbz} grid` + ); } const bx = blockIdx % sourceNbx; const byBz = Math.floor(blockIdx / sourceNbx); @@ -733,8 +731,13 @@ function buildSparseOctreeFromBuffer( const treeDepth = calculateTreeDepth(gridBounds, voxelResolution); return buildSparseOctreeFromStreams( - solidStream, mixedStream, mixed.masks, - gridBounds, sceneBounds, voxelResolution, treeDepth + solidStream, + mixedStream, + mixed.masks, + gridBounds, + sceneBounds, + voxelResolution, + treeDepth ); } @@ -762,13 +765,15 @@ function buildSparseOctreeFromStreams( const nSolid = solidStream.length; const nMixed = mixedStream.length; if (treeDepth > 17) { - throw new Error(`Sparse octree depth ${treeDepth} exceeds the 17-level Morton encoding limit. Use a coarser voxel resolution.`); + throw new Error( + `Sparse octree depth ${treeDepth} exceeds the 17-level Morton encoding limit. Use a coarser voxel resolution.` + ); } if (nMixed > MAX_V1_MIXED_LEAVES) { throw new Error( `Sparse octree mixed-leaf count (${nMixed}) exceeds the ` + - `Laine-Karras 24-bit baseOffset limit (${MAX_V1_MIXED_LEAVES}). ` + - 'Use a coarser voxel resolution.' + `Laine-Karras 24-bit baseOffset limit (${MAX_V1_MIXED_LEAVES}). ` + + 'Use a coarser voxel resolution.' ); } @@ -872,7 +877,7 @@ function buildSparseOctreeFromStreams( }); } else { for (let level = 1; level < treeDepth; level++) { - const targetStep = 1 + Math.min(levelSteps, Math.floor((level + 1) / treeDepth * levelSteps)); + const targetStep = 1 + Math.min(levelSteps, Math.floor(((level + 1) / treeDepth) * levelSteps)); while (octreeStep < targetStep) { bar.tick(); octreeStep++; @@ -909,7 +914,7 @@ function buildSparseOctreeFromStreams( while (i < n && Math.floor(curMortons[i] / 8) === parentMorton) { const octant = curMortons[i] % 8; - childMask |= (1 << octant); + childMask |= 1 << octant; if (curTypes[i] !== BlockType.Solid) { allSolid = false; } @@ -955,8 +960,16 @@ function buildSparseOctreeFromStreams( // --- Phase 3: Flatten tree to Laine-Karras format --- const result = flattenTreeFromLevels( - interiorLevels, solidStream, mixedStream, mixedMasks, nSolid, nMixed, - gridBounds, sceneBounds, voxelResolution, actualDepth + interiorLevels, + solidStream, + mixedStream, + mixedMasks, + nSolid, + nMixed, + gridBounds, + sceneBounds, + voxelResolution, + actualDepth ); bar.tick(); @@ -1060,8 +1073,8 @@ function flattenTreeFromLevels( if (leafDataIndex > MAX_24BIT_OFFSET) { throw new Error( `Sparse octree mixed-leaf count (${leafDataIndex + 1}) exceeds the ` + - `Laine-Karras 24-bit baseOffset limit (${MAX_24BIT_OFFSET + 1}). ` + - 'Use a coarser voxel resolution.' + `Laine-Karras 24-bit baseOffset limit (${MAX_24BIT_OFFSET + 1}). ` + + 'Use a coarser voxel resolution.' ); } leafData[leafDataLen++] = mixedMasks[ii * 2]; @@ -1102,8 +1115,8 @@ function flattenTreeFromLevels( if (emitPos + nextLength > MAX_24BIT_OFFSET + 1) { throw new Error( `Sparse octree node count (${emitPos + nextLength}) exceeds the ` + - `Laine-Karras 24-bit baseOffset limit (${MAX_24BIT_OFFSET + 1}). ` + - 'Use a coarser voxel resolution.' + `Laine-Karras 24-bit baseOffset limit (${MAX_24BIT_OFFSET + 1}). ` + + 'Use a coarser voxel resolution.' ); } const nextWave = createInteriorWave(nextLength); @@ -1115,7 +1128,7 @@ function flattenTreeFromLevels( const childMask = interiorLevels[myLi].childMasks[myIi]; const childCount = popcount(childMask); - nodes[interiors.pos[j]] = ((childMask & 0xFF) << 24) | nextChildStart; + nodes[interiors.pos[j]] = ((childMask & 0xff) << 24) | nextChildStart; const myMorton = interiorLevels[myLi].mortons[myIi]; const childMortonBase = myMorton * 8; @@ -1130,10 +1143,14 @@ function flattenTreeFromLevels( let sIdx = lowerBoundF64(solidStream, childMortonBase, nSolid); let mIdx = lowerBoundF64(mixedStream, childMortonBase, nMixed); while (true) { - const sM = sIdx < nSolid && solidStream[sIdx] < childMortonEnd ? - solidStream[sIdx] : Number.POSITIVE_INFINITY; - const mM = mIdx < nMixed && mixedStream[mIdx] < childMortonEnd ? - mixedStream[mIdx] : Number.POSITIVE_INFINITY; + const sM = + sIdx < nSolid && solidStream[sIdx] < childMortonEnd + ? solidStream[sIdx] + : Number.POSITIVE_INFINITY; + const mM = + mIdx < nMixed && mixedStream[mIdx] < childMortonEnd + ? mixedStream[mIdx] + : Number.POSITIVE_INFINITY; if (!isFinite(sM) && !isFinite(mM)) break; if (sM < mM) { pushInteriorWave(nextWave, 0, -1, nMixed + sIdx); @@ -1182,11 +1199,6 @@ function flattenTreeFromLevels( }; } -export { - buildSparseOctree, - buildSparseOctreeFromBuffer, - MAX_V1_MIXED_LEAVES, - SOLID_LEAF_MARKER -}; +export { buildSparseOctree, buildSparseOctreeFromBuffer, MAX_V1_MIXED_LEAVES, SOLID_LEAF_MARKER }; export type { SparseOctree }; diff --git a/src/lib/writers/utils.ts b/src/lib/writers/utils.ts index d63b3d52..0bf4966c 100644 --- a/src/lib/writers/utils.ts +++ b/src/lib/writers/utils.ts @@ -1,4 +1,4 @@ -import { type SplatModel } from '../splat-model'; +import type { SplatModel } from '../splat-model'; import { fmtBytes, logger } from '../utils'; // Brush's spelling of the model, which its importer (brush-serde import.rs) and diff --git a/src/lib/writers/write-compressed-ply.ts b/src/lib/writers/write-compressed-ply.ts index 481866d4..28dec59c 100644 --- a/src/lib/writers/write-compressed-ply.ts +++ b/src/lib/writers/write-compressed-ply.ts @@ -1,32 +1,41 @@ import { basename } from 'pathe'; -import { CompressedChunk } from './compressed-chunk'; -import { logWrittenFile, splatModelComment } from './utils'; -import { type ChunkSource, type ChunkDataPool } from '../chunk'; +import type { ChunkSource, ChunkDataPool } from '../chunk'; import { materializeToDataTable } from '../compat/data-table'; -import { DataTable, sortMortonOrder, convertToSpace, getSHBands, shRestNames } from '../data-table'; -import { type FileSystem } from '../io/write'; -import { type SplatModel } from '../splat-model'; +import type { DataTable } from '../data-table'; +import { sortMortonOrder, convertToSpace, getSHBands, shRestNames } from '../data-table'; +import type { FileSystem } from '../io/write'; +import type { SplatModel } from '../splat-model'; import { logger, Transform } from '../utils'; import { version } from '../version'; +import { CompressedChunk } from './compressed-chunk'; +import { logWrittenFile, splatModelComment } from './utils'; + const generatedByString = `Generated by splat-transform ${version}`; const chunkProps = [ - 'min_x', 'min_y', 'min_z', - 'max_x', 'max_y', 'max_z', - 'min_scale_x', 'min_scale_y', 'min_scale_z', - 'max_scale_x', 'max_scale_y', 'max_scale_z', - 'min_r', 'min_g', 'min_b', - 'max_r', 'max_g', 'max_b' + 'min_x', + 'min_y', + 'min_z', + 'max_x', + 'max_y', + 'max_z', + 'min_scale_x', + 'min_scale_y', + 'min_scale_z', + 'max_scale_x', + 'max_scale_y', + 'max_scale_z', + 'min_r', + 'min_g', + 'min_b', + 'max_r', + 'max_g', + 'max_b' ]; -const vertexProps = [ - 'packed_position', - 'packed_rotation', - 'packed_scale', - 'packed_color' -]; +const vertexProps = ['packed_position', 'packed_rotation', 'packed_scale', 'packed_color']; // Size of a chunk in the compressed PLY format (number of splats per chunk) const CHUNK_SIZE = 256; @@ -58,10 +67,12 @@ const writeCompressedPly = async (options: WriteCompressedPlyOptions, fs: FileSy const numSplats = dataTable.numRows; const numChunks = Math.ceil(numSplats / CHUNK_SIZE); - const shHeader = shBands ? [ - `element sh ${numSplats}`, - new Array(outputSHCoeffs * 3).fill('').map((_, i) => `property uchar f_rest_${i}`) - ].flat() : []; + const shHeader = shBands + ? [ + `element sh ${numSplats}`, + new Array(outputSHCoeffs * 3).fill('').map((_, i) => `property uchar f_rest_${i}`) + ].flat() + : []; // The packed layout is fixed, so a 2DGS scene keeps its (materialized) third // scale — the comment is what tells a viewer to ignore it. @@ -73,14 +84,16 @@ const writeCompressedPly = async (options: WriteCompressedPlyOptions, fs: FileSy `comment ${generatedByString}`, ...(modelComment ? [`comment ${modelComment}`] : []), `element chunk ${numChunks}`, - chunkProps.map(p => `property float ${p}`), + chunkProps.map((p) => `property float ${p}`), `element vertex ${numSplats}`, - vertexProps.map(p => `property uint ${p}`), + vertexProps.map((p) => `property uint ${p}`), shHeader, 'end_header\n' - ].flat().join('\n'); + ] + .flat() + .join('\n'); - const header = (new TextEncoder()).encode(headerText); + const header = new TextEncoder().encode(headerText); const chunkData = new Float32Array(numChunks * chunkProps.length); const splatIData = new Uint32Array(numSplats * vertexProps.length); const shData = new Uint8Array(numSplats * outputSHCoeffs * 3); diff --git a/src/lib/writers/write-csv.ts b/src/lib/writers/write-csv.ts index cc4545ff..93f20c82 100644 --- a/src/lib/writers/write-csv.ts +++ b/src/lib/writers/write-csv.ts @@ -1,10 +1,11 @@ import { basename } from 'pathe'; -import { logWrittenFile } from './utils'; -import { DataTable } from '../data-table'; -import { type FileSystem } from '../io/write'; +import type { DataTable } from '../data-table'; +import type { FileSystem } from '../io/write'; import { logger } from '../utils'; +import { logWrittenFile } from './utils'; + type WriteCSVOptions = { filename: string; dataTable: DataTable; @@ -34,7 +35,7 @@ const writeCsv = async (options: WriteCSVOptions, fs: FileSystem) => { // write header await writer.write(textEncoder.encode(`${dataTable.columnNames.join(',')}\n`)); - const columns = dataTable.columns.map(c => c.data); + const columns = dataTable.columns.map((c) => c.data); // write rows for (let i = 0; i < len; ++i) { diff --git a/src/lib/writers/write-glb.ts b/src/lib/writers/write-glb.ts index 3e17f889..e57f810a 100644 --- a/src/lib/writers/write-glb.ts +++ b/src/lib/writers/write-glb.ts @@ -1,11 +1,13 @@ import { basename } from 'pathe'; -import { logWrittenFile } from './utils'; -import { DataTable, convertToSpace, getSHBands, shRestNames } from '../data-table'; -import { type FileSystem } from '../io/write'; +import type { DataTable } from '../data-table'; +import { convertToSpace, getSHBands, shRestNames } from '../data-table'; +import type { FileSystem } from '../io/write'; import { logger, Transform, sigmoid } from '../utils'; import { version } from '../version'; +import { logWrittenFile } from './utils'; + const SH_C0 = 0.2820947917738781; type WriteGlbOptions = { @@ -21,10 +23,10 @@ const UNSIGNED_BYTE = 5121; const ARRAY_BUFFER = 34962; // GLB chunk types -const GLB_MAGIC = 0x46546C67; +const GLB_MAGIC = 0x46546c67; const GLB_VERSION = 2; -const JSON_CHUNK_TYPE = 0x4E4F534A; -const BIN_CHUNK_TYPE = 0x004E4942; +const JSON_CHUNK_TYPE = 0x4e4f534a; +const BIN_CHUNK_TYPE = 0x004e4942; /** * Computes POSITION accessor min/max bounds required by the glTF spec. @@ -35,8 +37,12 @@ const BIN_CHUNK_TYPE = 0x004E4942; * @returns Object with min and max arrays. */ const computePositionBounds = (x: Float32Array, y: Float32Array, z: Float32Array) => { - let minX = Infinity, minY = Infinity, minZ = Infinity; - let maxX = -Infinity, maxY = -Infinity, maxZ = -Infinity; + let minX = Infinity, + minY = Infinity, + minZ = Infinity; + let maxX = -Infinity, + maxY = -Infinity, + maxZ = -Infinity; for (let i = 0; i < x.length; i++) { if (x[i] < minX) minX = x[i]; @@ -147,7 +153,7 @@ const buildBinaryBuffer = (dataTable: DataTable, numSplats: number, shBands: num // KHR_gaussian_splatting:ROTATION (VEC4 float, xyzw order) const rotData = new Float32Array(numSplats * 4); for (let i = 0; i < numSplats; i++) { - rotData[i * 4] = rot1[i]; // x + rotData[i * 4] = rot1[i]; // x rotData[i * 4 + 1] = rot2[i]; // y rotData[i * 4 + 2] = rot3[i]; // z rotData[i * 4 + 3] = rot0[i]; // w @@ -209,8 +215,8 @@ const buildBinaryBuffer = (dataTable: DataTable, numSplats: number, shBands: num if (shDegrees > 0) { const restData: Float32Array[] = []; for (let k = 0; k < coeffsPerChannel; k++) { - restData.push(dataTable.getColumnByName(shRestNames[k])!.data as Float32Array); // red - restData.push(dataTable.getColumnByName(shRestNames[k + coeffsPerChannel])!.data as Float32Array); // green + restData.push(dataTable.getColumnByName(shRestNames[k])!.data as Float32Array); // red + restData.push(dataTable.getColumnByName(shRestNames[k + coeffsPerChannel])!.data as Float32Array); // green restData.push(dataTable.getColumnByName(shRestNames[k + 2 * coeffsPerChannel])!.data as Float32Array); // blue } @@ -320,20 +326,24 @@ const writeGlb = async (options: WriteGlbOptions, fs: FileSystem) => { buffers: [{ byteLength: binBuffer.byteLength }], bufferViews, accessors, - meshes: [{ - primitives: [{ - attributes, - mode: 0, - extensions: { - KHR_gaussian_splatting: { - kernel: 'ellipse', - colorSpace: 'srgb_rec709_display', - sortingMethod: 'cameraDistance', - projection: 'perspective' + meshes: [ + { + primitives: [ + { + attributes, + mode: 0, + extensions: { + KHR_gaussian_splatting: { + kernel: 'ellipse', + colorSpace: 'srgb_rec709_display', + sortingMethod: 'cameraDistance', + projection: 'perspective' + } + } } - } - }] - }] + ] + } + ] }; const jsonString = JSON.stringify(gltf); diff --git a/src/lib/writers/write-html.ts b/src/lib/writers/write-html.ts index 5a732fdc..71bbae79 100644 --- a/src/lib/writers/write-html.ts +++ b/src/lib/writers/write-html.ts @@ -1,13 +1,15 @@ import { html, css, js } from '@playcanvas/supersplat-viewer'; import { basename, dirname, join } from 'pathe'; -import { logWrittenFile } from './utils'; -import { writeSog } from './write-sog'; -import { DataTable } from '../data-table'; -import { type FileSystem, MemoryFileSystem, writeFile } from '../io/write'; +import type { DataTable } from '../data-table'; +import { MemoryFileSystem, writeFile } from '../io/write'; +import type { FileSystem } from '../io/write'; import type { DeviceCreator } from '../types'; import { logger, toBase64 } from '../utils'; +import { logWrittenFile } from './utils'; +import { writeSog } from './write-sog'; + const defaultSettings = { version: 2, tonemapping: 'none', @@ -21,13 +23,15 @@ const defaultSettings = { fringing: { enabled: false, intensity: 0.5 } }, animTracks: [] as any[], - cameras: [{ - initial: { - position: [2, 2, -2], - target: [0, 0, 0], - fov: 75 + cameras: [ + { + initial: { + position: [2, 2, -2], + target: [0, 0, 0], + fov: 75 + } } - }], + ], annotations: [] as any[], startMode: 'default' }; @@ -56,7 +60,10 @@ const writeHtml = async (options: WriteHtmlOptions, fs: FileSystem) => { const pad = (text: string, spaces: number) => { const whitespace = ' '.repeat(spaces); - return text.split('\n').map(line => whitespace + line).join('\n'); + return text + .split('\n') + .map((line) => whitespace + line) + .join('\n'); }; const viewerSettings = viewerSettingsJson || defaultSettings; @@ -67,29 +74,32 @@ const writeHtml = async (options: WriteHtmlOptions, fs: FileSystem) => { const memoryFs = new MemoryFileSystem(); const sogFilename = 'temp.sog'; - await writeSog({ - filename: sogFilename, - dataTable, - bundle: true, - iterations, - createDevice, - logging: 'silent' - }, memoryFs); + await writeSog( + { + filename: sogFilename, + dataTable, + bundle: true, + iterations, + createDevice, + logging: 'silent' + }, + memoryFs + ); // get the memory buffer const sogData = toBase64(memoryFs.results.get(sogFilename)); const style = ''; - const script = 'import { main } from \'./index.js\';'; + const script = "import { main } from './index.js';"; const settings = 'settings: fetch(settingsUrl).then(response => response.json())'; const content = 'fetch(contentUrl)'; const resultHtml = html - .replace(style, ``) - .replace(script, js) - .replace(settings, `settings: ${JSON.stringify(viewerSettings)}`) - .replace(content, `fetch("data:application/octet-stream;base64,${sogData}")`) - .replace('.compressed.ply', '.sog'); + .replace(style, ``) + .replace(script, js) + .replace(settings, `settings: ${JSON.stringify(viewerSettings)}`) + .replace(content, `fetch("data:application/octet-stream;base64,${sogData}")`) + .replace('.compressed.ply', '.sog'); const htmlBytes = encoder.encode(resultHtml); @@ -107,14 +117,17 @@ const writeHtml = async (options: WriteHtmlOptions, fs: FileSystem) => { const writingGroup = logger.group('Writing'); // Write .sog file (its files are emitted flat into our Writing group) - await writeSog({ - filename: sogPath, - dataTable, - bundle: true, - iterations, - createDevice, - logging: 'flat' - }, fs); + await writeSog( + { + filename: sogPath, + dataTable, + bundle: true, + iterations, + createDevice, + logging: 'flat' + }, + fs + ); // Write CSS file const cssPath = join(outputDir, 'index.css'); @@ -137,9 +150,7 @@ const writeHtml = async (options: WriteHtmlOptions, fs: FileSystem) => { // Generate HTML with external references const content = 'fetch(contentUrl)'; - const resultHtml = html - .replace(content, `fetch("${sogFilename}")`) - .replace('.compressed.ply', '.sog'); + const resultHtml = html.replace(content, `fetch("${sogFilename}")`).replace('.compressed.ply', '.sog'); const htmlBytes = encoder.encode(resultHtml); await writeFile(fs, filename, htmlBytes); diff --git a/src/lib/writers/write-image.ts b/src/lib/writers/write-image.ts index 4e845afc..e2c370e7 100644 --- a/src/lib/writers/write-image.ts +++ b/src/lib/writers/write-image.ts @@ -1,14 +1,17 @@ import { basename } from 'pathe'; import { Vec3 } from 'playcanvas'; -import { logWrittenFile } from './utils'; -import { convertToSpace, DataTable } from '../data-table'; -import { type FileSystem, writeFile } from '../io/write'; +import type { DataTable } from '../data-table'; +import { convertToSpace } from '../data-table'; +import { writeFile } from '../io/write'; +import type { FileSystem } from '../io/write'; import { renderSplats } from '../render'; -import { type Projection, type RenderCamera } from '../render/camera'; +import type { Projection, RenderCamera } from '../render/camera'; import type { DeviceCreator } from '../types'; import { logger, Transform, WebPCodec } from '../utils'; +import { logWrittenFile } from './utils'; + /** * Options for writing a rendered splat image. */ @@ -165,10 +168,14 @@ const writeImage = async (options: WriteImageOptions, fs: FileSystem): Promise { + u: { x: number; y: number; z: number } + ): RenderCamera => { let fDist = 0; let aScale = 0; if (dofEnabled) { @@ -259,11 +270,13 @@ const writeImage = async (options: WriteImageOptions, fs: FileSystem): Promise 0) { - logger.info(`${width}x${height} fov ${fov}° f/${fStop} focus ${startCamera.focusDistance!.toFixed(3)} sensor ${sensorSize}`); + logger.info( + `${width}x${height} fov ${fov}° f/${fStop} focus ${startCamera.focusDistance!.toFixed(3)} sensor ${sensorSize}` + ); } else { logger.info(`${width}x${height} fov ${fov}°`); } diff --git a/src/lib/writers/write-lod.ts b/src/lib/writers/write-lod.ts index 31cffcae..6b7200d1 100644 --- a/src/lib/writers/write-lod.ts +++ b/src/lib/writers/write-lod.ts @@ -1,20 +1,23 @@ import { basename, dirname, resolve } from 'pathe'; import { BoundingBox, Mat4, Quat, Vec3 } from 'playcanvas'; -import { logWrittenFile } from './utils'; -import { writeSogSource } from './write-sog.js'; -import { type ChunkDataPool, type ChunkSource, type ReadRequest, createChunkDataPool } from '../chunk'; +import { createChunkDataPool } from '../chunk'; +import type { ChunkDataPool, ChunkSource, ReadRequest } from '../chunk'; import { Column, DataTable } from '../data-table'; -import { type FileSystem } from '../io/write'; +import type { FileSystem } from '../io/write'; import { bakeTransform, permuteSource, sortMortonColumns } from '../ops'; -import { BTreeNode, BTree } from '../spatial'; +import type { BTreeNode } from '../spatial'; +import { BTree } from '../spatial'; import type { DeviceCreator } from '../types'; import { logger, Transform } from '../utils'; import { version } from '../version'; +import { logWrittenFile } from './utils'; +import { writeSogSource } from './write-sog.js'; + type Aabb = { - min: number[], - max: number[] + min: number[]; + max: number[]; }; type MetaLod = { @@ -26,7 +29,7 @@ type MetaLod = { type MetaNode = { bound: Aabb; children?: MetaNode[]; - lods?: { [key: number]: MetaLod }; + lods?: Record; }; type LodMeta = { @@ -65,7 +68,9 @@ const boundUnion = (result: Aabb, a: Aabb, b: Aabb) => { * SH degree — the point of the streaming LOD writer for very large scenes. */ type SlimColumns = { - x: Float32Array; y: Float32Array; z: Float32Array; + x: Float32Array; + y: Float32Array; + z: Float32Array; }; /** @@ -91,14 +96,18 @@ const positionsFromSlim = (parent: ChunkSource, slim: SlimColumns, flat: Uint32A const { indices, indexOffset, count } = request; for (let j = 0; j < count; j++) { const g = flat[indices[indexOffset + j]]; - out[j * 3] = slim.x[g]; out[j * 3 + 1] = slim.y[g]; out[j * 3 + 2] = slim.z[g]; + out[j * 3] = slim.x[g]; + out[j * 3 + 1] = slim.y[g]; + out[j * 3 + 2] = slim.z[g]; } } else { const base = request.chunkIndex * chunkSize; const count = Math.min(chunkSize, flat.length - base); for (let j = 0; j < count; j++) { const g = flat[base + j]; - out[j * 3] = slim.x[g]; out[j * 3 + 1] = slim.y[g]; out[j * 3 + 2] = slim.z[g]; + out[j * 3] = slim.x[g]; + out[j * 3 + 1] = slim.y[g]; + out[j * 3 + 2] = slim.z[g]; } } if (request.geometric || request.color || request.other) { @@ -113,8 +122,12 @@ const positionsFromSlim = (parent: ChunkSource, slim: SlimColumns, flat: Uint32A // pass can run over gathered batches; the math mirrors the legacy per-gaussian // path exactly (quaternion order (rot_1, rot_2, rot_3, rot_0); scale = exp). const accumulateBound = ( - min: number[], max: number[], - pos: Float32Array, rot: Float32Array, scale: Float32Array, count: number + min: number[], + max: number[], + pos: Float32Array, + rot: Float32Array, + scale: Float32Array, + count: number ): void => { const p = new Vec3(); const r = new Quat(); @@ -157,7 +170,10 @@ const accumulateBound = ( // `bins` maps LOD -> flat analysis indices; each is gathered from its own LOD // (flat index `g` -> local row `g - cum[lod]`). const calcBound = async ( - source: ChunkSource, pool: ChunkDataPool, bins: Map, cum: number[], + source: ChunkSource, + pool: ChunkDataPool, + bins: Map, + cum: number[], tick?: (n: number) => void ): Promise => { const min = [Infinity, Infinity, Infinity]; @@ -175,9 +191,17 @@ const calcBound = async ( const count = Math.min(batch, local.length - off); const pos = pool.acquire('position', layouts.position!, count); const geo = pool.acquire('geometric', layouts.geometric!, count); - await source.read({ indices: local, indexOffset: off, count, lod: lodValue, position: pos, geometric: geo }); + await source.read({ + indices: local, + indexOffset: off, + count, + lod: lodValue, + position: pos, + geometric: geo + }); accumulateBound( - min, max, + min, + max, // position is full-stride packed xyz — read the pool buffer // in place rather than copying it out per batch new Float32Array(pos.data, 0, count * 3), @@ -277,7 +301,9 @@ const extractSlim = async (source: ChunkSource, pool: ChunkDataPool): Promise await fs.mkdir(outputDir); // construct a kd-tree based on centroids from all lods - const centroidsTable = new DataTable([ - new Column('x', slim.x), - new Column('y', slim.y), - new Column('z', slim.z) - ]); + const centroidsTable = new DataTable([new Column('x', slim.x), new Column('y', slim.y), new Column('z', slim.z)]); let bTree: BTree | null = new BTree(centroidsTable); @@ -368,7 +390,7 @@ const writeLodSource = async (options: WriteLodSourceOptions, fs: FileSystem) => // map of lod -> file units -> subunits (each subunit a tight Uint32Array of // gaussian indices). This is the bulk retained bookkeeping; Uint32Array keeps // it off the V8 heap at 4 B/gaussian. - const lodFiles: Map = new Map(); + const lodFiles = new Map(); const filenames: string[] = []; let lodLevels = 0; @@ -378,10 +400,7 @@ const writeLodSource = async (options: WriteLodSourceOptions, fs: FileSystem) => const build = async (node: BTreeNode): Promise => { if (!node.indices && (node.count > binSize || (node.aabb && node.aabb.largestDim() > binDim))) { - const children = [ - await build(node.left), - await build(node.right) - ]; + const children = [await build(node.left), await build(node.right)]; const bound = { min: [0, 0, 0], @@ -392,7 +411,7 @@ const writeLodSource = async (options: WriteLodSourceOptions, fs: FileSystem) => return { bound, children }; } - const lods: { [key: number]: MetaLod } = { }; + const lods: Record = {}; const bins = binIndices(node, lodOf); for (const [lodValue, indices] of bins) { @@ -425,7 +444,7 @@ const writeLodSource = async (options: WriteLodSourceOptions, fs: FileSystem) => } // bound over the leaf's gaussians, gathered per structural LOD. - const bound = await calcBound(mainSource, pool, bins, cum, n => chunkingBar.tick(n)); + const bound = await calcBound(mainSource, pool, bins, cum, (n) => chunkingBar.tick(n)); return { bound, lods }; }; @@ -508,7 +527,7 @@ const writeLodSource = async (options: WriteLodSourceOptions, fs: FileSystem) => } // write lod-meta.json - const metaJson = (new TextEncoder()).encode(JSON.stringify(meta, replacer)); + const metaJson = new TextEncoder().encode(JSON.stringify(meta, replacer)); const writer = await fs.createWriter(filename); await writer.write(metaJson); await writer.close(); @@ -538,7 +557,12 @@ const writeLodSource = async (options: WriteLodSourceOptions, fs: FileSystem) => const orderedIndices = new Uint32Array(totalIndices); for (let j = 0, offset = 0; j < fileUnit.length; ++j) { orderedIndices.set(fileUnit[j], offset); - sortMortonColumns(slim.x, slim.y, slim.z, orderedIndices.subarray(offset, offset + fileUnit[j].length)); + sortMortonColumns( + slim.x, + slim.y, + slim.z, + orderedIndices.subarray(offset, offset + fileUnit[j].length) + ); offset += fileUnit[j].length; } @@ -554,19 +578,25 @@ const writeLodSource = async (options: WriteLodSourceOptions, fs: FileSystem) => // writer's own Morton pass. const unitSource = positionsFromSlim( permuteSource(mainSource, orderedLocal, { lod: lodValue }), - slim, orderedIndices + slim, + orderedIndices ); const identity = new Uint32Array(totalIndices); for (let j = 0; j < totalIndices; ++j) identity[j] = j; - await writeSogSource(unitSource, pool, { - filename: pathname, - bundle: false, - iterations, - createDevice, - indices: identity, - logging: 'flat' - }, fs); + await writeSogSource( + unitSource, + pool, + { + filename: pathname, + bundle: false, + iterations, + createDevice, + indices: identity, + logging: 'flat' + }, + fs + ); } finally { unitGroup.end(); } diff --git a/src/lib/writers/write-ply-streaming.ts b/src/lib/writers/write-ply-streaming.ts index 038e4a2e..c01858f5 100644 --- a/src/lib/writers/write-ply-streaming.ts +++ b/src/lib/writers/write-ply-streaming.ts @@ -1,15 +1,11 @@ -import { - type ChunkData, - type ChunkLayer, - type ChunkSource, - type ChunkDataPool, - SH_REST_COUNTS -} from '../chunk'; -import { splatModelComment } from './utils'; -import { type FileSystem } from '../io/write'; +import { SH_REST_COUNTS } from '../chunk'; +import type { ChunkData, ChunkLayer, ChunkSource, ChunkDataPool } from '../chunk'; +import type { FileSystem } from '../io/write'; import { bakeTransform } from '../ops'; import { logger, Transform } from '../utils'; +import { splatModelComment } from './utils'; + const GEOMETRIC_COLS = ['rot_0', 'rot_1', 'rot_2', 'rot_3', 'scale_0', 'scale_1', 'scale_2', 'opacity']; const plyType = (uint: boolean): string => (uint ? 'uint' : 'float'); @@ -72,7 +68,12 @@ const writePlyStreaming = async ( }); } if (layers.has('color')) { - const names = ['f_dc_0', 'f_dc_1', 'f_dc_2', ...Array.from({ length: SH_REST_COUNTS[meta.shBands] }, (_, k) => `f_rest_${k}`)]; + const names = [ + 'f_dc_0', + 'f_dc_1', + 'f_dc_2', + ...Array.from({ length: SH_REST_COUNTS[meta.shBands] }, (_, k) => `f_rest_${k}`) + ]; names.forEach((name, i) => { columns.push({ name, layer: 'color', layerByteOffset: i * 4, uint: false }); }); @@ -107,9 +108,11 @@ const writePlyStreaming = async ( const layer = columns[c].layer; const srcStart = columns[c].layerByteOffset >> 2; let e = c; - while (e < columns.length && - columns[e].layer === layer && - (columns[e].layerByteOffset >> 2) === srcStart + (e - c)) { + while ( + e < columns.length && + columns[e].layer === layer && + columns[e].layerByteOffset >> 2 === srcStart + (e - c) + ) { e++; } runs.push({ layer, srcStart, dstStart: c, words: e - c, srcStrideWords: meta.layouts[layer]!.stride >> 2 }); @@ -123,7 +126,7 @@ const writePlyStreaming = async ( 'format binary_little_endian 1.0', ...(modelComment ? [`comment ${modelComment}`] : []), `element vertex ${N}`, - ...columns.map(c => `property ${plyType(c.uint)} ${c.name}`), + ...columns.map((c) => `property ${plyType(c.uint)} ${c.name}`), 'end_header' ]; diff --git a/src/lib/writers/write-ply.ts b/src/lib/writers/write-ply.ts index 26bfa0ba..23326cdb 100644 --- a/src/lib/writers/write-ply.ts +++ b/src/lib/writers/write-ply.ts @@ -1,21 +1,30 @@ import { basename } from 'pathe'; -import { logWrittenFile } from './utils'; import { convertToSpace } from '../data-table'; -import { type FileSystem } from '../io/write'; -import { PlyData } from '../readers'; +import type { FileSystem } from '../io/write'; +import type { PlyData } from '../readers'; import { logger, Transform } from '../utils'; +import { logWrittenFile } from './utils'; + const columnTypeToPlyType = (type: string): string => { switch (type) { - case 'float32': return 'float'; - case 'float64': return 'double'; - case 'int8': return 'char'; - case 'uint8': return 'uchar'; - case 'int16': return 'short'; - case 'uint16': return 'ushort'; - case 'int32': return 'int'; - case 'uint32': return 'uint'; + case 'float32': + return 'float'; + case 'float64': + return 'double'; + case 'int8': + return 'char'; + case 'uint8': + return 'uchar'; + case 'int16': + return 'short'; + case 'uint16': + return 'ushort'; + case 'int32': + return 'int'; + case 'uint32': + return 'uint'; } }; @@ -38,7 +47,7 @@ const writePly = async (options: WritePlyOptions, fs: FileSystem) => { const { filename } = options; const plyData: PlyData = { ...options.plyData, - elements: options.plyData.elements.map(e => ({ + elements: options.plyData.elements.map((e) => ({ ...e, dataTable: convertToSpace(e.dataTable, Transform.PLY) })) @@ -47,7 +56,7 @@ const writePly = async (options: WritePlyOptions, fs: FileSystem) => { const header = [ 'ply', 'format binary_little_endian 1.0', - plyData.comments.map(c => `comment ${c}`), + plyData.comments.map((c) => `comment ${c}`), plyData.elements.map((element) => { return [ `element ${element.name} ${element.dataTable.numRows}`, @@ -63,13 +72,13 @@ const writePly = async (options: WritePlyOptions, fs: FileSystem) => { // write the header const writer = await fs.createWriter(filename); - await writer.write((new TextEncoder()).encode(`${header.flat(3).join('\n')}\n`)); + await writer.write(new TextEncoder().encode(`${header.flat(3).join('\n')}\n`)); for (let i = 0; i < plyData.elements.length; ++i) { const table = plyData.elements[i].dataTable; const columns = table.columns; - const buffers = columns.map(c => new Uint8Array(c.data.buffer, c.data.byteOffset, c.data.byteLength)); - const sizes = columns.map(c => c.data.BYTES_PER_ELEMENT); + const buffers = columns.map((c) => new Uint8Array(c.data.buffer, c.data.byteOffset, c.data.byteLength)); + const sizes = columns.map((c) => c.data.BYTES_PER_ELEMENT); const rowSize = sizes.reduce((total, size) => total + size, 0); // write to file in chunks of 1024 rows diff --git a/src/lib/writers/write-sog.ts b/src/lib/writers/write-sog.ts index 84672185..573fb47b 100644 --- a/src/lib/writers/write-sog.ts +++ b/src/lib/writers/write-sog.ts @@ -1,25 +1,23 @@ import { basename, dirname, resolve } from 'pathe'; -import { logWrittenFile } from './utils'; -import { - createChunkDataPool, - type ChunkDataPool, - type ChunkLayer, - type ReadRequest, - type ChunkSource -} from '../chunk'; +import { createChunkDataPool } from '../chunk'; +import type { ChunkDataPool, ChunkLayer, ReadRequest, ChunkSource } from '../chunk'; import { dataTableToChunkSource } from '../compat/data-table'; -import { type DataTable, shRestNames } from '../data-table'; -import { type FileSystem, writeFile, ZipFileSystem } from '../io/write'; +import { shRestNames } from '../data-table'; +import type { DataTable } from '../data-table'; +import { writeFile, ZipFileSystem } from '../io/write'; +import type { FileSystem } from '../io/write'; import { bakeTransform } from '../ops'; import { sortMortonInterleaved } from '../ops/morton-order'; import { kmeansInterleaved } from '../spatial'; -import { type SplatModel } from '../splat-model'; +import type { SplatModel } from '../splat-model'; import type { DeviceCreator } from '../types'; import { logger, sigmoid, Transform } from '../utils'; import { version } from '../version'; import { runEncodeWebp, runQuantize1dColumns } from '../workers'; +import { logWrittenFile } from './utils'; + const GEOMETRIC_COLS = ['rot_0', 'rot_1', 'rot_2', 'rot_3', 'scale_0', 'scale_1', 'scale_2', 'opacity']; const logTransform = (value: number): number => { @@ -28,7 +26,11 @@ const logTransform = (value: number): number => { // Gather a layer's data as one interleaved Float32Array `[g0 words, g1 words, ...]` // (a contiguous copy per chunk — the layer is already packed). -const gatherInterleaved = async (source: ChunkSource, pool: ChunkDataPool, layer: ChunkLayer): Promise => { +const gatherInterleaved = async ( + source: ChunkSource, + pool: ChunkDataPool, + layer: ChunkLayer +): Promise => { const { meta } = source; const n = meta.numGaussians; const layout = meta.layouts[layer]!; @@ -50,7 +52,12 @@ const gatherInterleaved = async (source: ChunkSource, pool: ChunkDataPool, layer // Gather a layer into per-column Float32Arrays, in the layer's canonical word // order (so `names[c]` maps to source word `c`). Used where downstream helpers // (quantize1d / kmeans) want individual columns. -const gatherColumns = async (source: ChunkSource, pool: ChunkDataPool, layer: ChunkLayer, names: string[]): Promise => { +const gatherColumns = async ( + source: ChunkSource, + pool: ChunkDataPool, + layer: ChunkLayer, + names: string[] +): Promise => { const { meta } = source; const n = meta.numGaussians; const layout = meta.layouts[layer]!; @@ -132,7 +139,7 @@ const writeSogSource = async ( if (options.indices && options.indices.length !== numRows) { throw new Error( `writeSogSource: indices length ${options.indices.length} must equal the source's gaussian count ${numRows} ` + - '(indices is a full-length ordering, not a subset filter — filter the source upstream with filterSource)' + '(indices is a full-length ordering, not a subset filter — filter the source upstream with filterSource)' ); } @@ -148,7 +155,7 @@ const writeSogSource = async ( if (width > 16383 || height > 16383) { throw new Error( `SOG output is capped at 16383x16383 WebP texels (~268M gaussians); got ${numRows}. ` + - 'Write streamed SOG (lod-meta.json output) instead — recommended for any scene beyond ~1-2M gaussians.' + 'Write streamed SOG (lod-meta.json output) instead — recommended for any scene beyond ~1-2M gaussians.' ); } @@ -170,6 +177,7 @@ const writeSogSource = async ( logWrittenFile(filename, webp.byteLength); } }); + // eslint-disable-next-line @typescript-eslint/no-empty-function -- preserve rejection-only control flow writeChain = write.catch(() => {}); return write; }; @@ -204,7 +212,11 @@ const writeSogSource = async ( const pos = await gatherInterleaved(baked, pool, 'position'); if (!externalOrder) sortMortonInterleaved(pos, indices); - const mm = [[Infinity, -Infinity], [Infinity, -Infinity], [Infinity, -Infinity]]; + const mm = [ + [Infinity, -Infinity], + [Infinity, -Infinity], + [Infinity, -Infinity] + ]; for (let g = 0; g < numRows; g++) { for (let a = 0; a < 3; a++) { const v = pos[g * 3 + a]; @@ -212,14 +224,14 @@ const writeSogSource = async ( if (v > mm[a][1]) mm[a][1] = v; } } - const minMax = mm.map(v => v.map(logTransform)); + const minMax = mm.map((v) => v.map(logTransform)); const meansL = new Uint8Array(width * height * channels); const meansU = new Uint8Array(width * height * channels); for (let i = 0; i < numRows; ++i) { const g = indices[i]; - const x = 65535 * (logTransform(pos[g * 3 + 0]) - minMax[0][0]) / (minMax[0][1] - minMax[0][0]); - const y = 65535 * (logTransform(pos[g * 3 + 1]) - minMax[1][0]) / (minMax[1][1] - minMax[1][0]); - const z = 65535 * (logTransform(pos[g * 3 + 2]) - minMax[2][0]) / (minMax[2][1] - minMax[2][0]); + const x = (65535 * (logTransform(pos[g * 3 + 0]) - minMax[0][0])) / (minMax[0][1] - minMax[0][0]); + const y = (65535 * (logTransform(pos[g * 3 + 1]) - minMax[1][0])) / (minMax[1][1] - minMax[1][0]); + const z = (65535 * (logTransform(pos[g * 3 + 2]) - minMax[2][0])) / (minMax[2][1] - minMax[2][0]); const ti = i; meansL[ti * 4] = x & 0xff; meansL[ti * 4 + 1] = y & 0xff; @@ -231,7 +243,7 @@ const writeSogSource = async ( meansU[ti * 4 + 3] = 0xff; } pending.push(writeWebp('means_l.webp', meansL), writeWebp('means_u.webp', meansU)); - return { mins: minMax.map(v => v[0]), maxs: minMax.map(v => v[1]) }; + return { mins: minMax.map((v) => v[0]), maxs: minMax.map((v) => v[1]) }; })(); // ---- Phase 2: geometric — quaternions + scales. The 32 B/gaussian layer @@ -244,21 +256,35 @@ const writeSogSource = async ( const q = [0, 0, 0, 0]; const sqrt2 = Math.sqrt(2); // Largest-3 component orders, indexed by the dropped component. - const quatIdx = [[1, 2, 3], [0, 2, 3], [0, 1, 3], [0, 1, 2]]; + const quatIdx = [ + [1, 2, 3], + [0, 2, 3], + [0, 1, 3], + [0, 1, 2] + ]; for (let i = 0; i < numRows; ++i) { const g = indices[i]; - q[0] = r0[g]; q[1] = r1[g]; q[2] = r2[g]; q[3] = r3[g]; + q[0] = r0[g]; + q[1] = r1[g]; + q[2] = r2[g]; + q[3] = r3[g]; const l = Math.sqrt(q[0] * q[0] + q[1] * q[1] + q[2] * q[2] + q[3] * q[3]); - q[0] /= l; q[1] /= l; q[2] /= l; q[3] /= l; + q[0] /= l; + q[1] /= l; + q[2] /= l; + q[3] /= l; let maxComp = 0; if (Math.abs(q[1]) > Math.abs(q[maxComp])) maxComp = 1; if (Math.abs(q[2]) > Math.abs(q[maxComp])) maxComp = 2; if (Math.abs(q[3]) > Math.abs(q[maxComp])) maxComp = 3; const s = (q[maxComp] < 0 ? -1 : 1) * sqrt2; - q[0] *= s; q[1] *= s; q[2] *= s; q[3] *= s; + q[0] *= s; + q[1] *= s; + q[2] *= s; + q[3] *= s; const idx = quatIdx[maxComp]; const ti = i; - quats[ti * 4] = 255 * (q[idx[0]] * 0.5 + 0.5); + quats[ti * 4] = 255 * (q[idx[0]] * 0.5 + 0.5); quats[ti * 4 + 1] = 255 * (q[idx[1]] * 0.5 + 0.5); quats[ti * 4 + 2] = 255 * (q[idx[2]] * 0.5 + 0.5); quats[ti * 4 + 3] = 252 + maxComp; @@ -266,9 +292,17 @@ const writeSogSource = async ( pending.push(writeWebp('quats.webp', quats)); const sd = await runQuantize1dColumns([ - { name: 'scale_0', data: s0 }, { name: 'scale_1', data: s1 }, { name: 'scale_2', data: s2 } + { name: 'scale_0', data: s0 }, + { name: 'scale_1', data: s1 }, + { name: 'scale_2', data: s2 } ]); - pending.push(writeLabels('scales.webp', sd.labels.map(c => c.data), indices)); + pending.push( + writeLabels( + 'scales.webp', + sd.labels.map((c) => c.data), + indices + ) + ); const od = new Uint8Array(numRows); for (let i = 0; i < numRows; ++i) { @@ -317,9 +351,11 @@ const writeSogSource = async ( } const cd = await runQuantize1dColumns([ - { name: 'f_dc_0', data: fdc0 }, { name: 'f_dc_1', data: fdc1 }, { name: 'f_dc_2', data: fdc2 } + { name: 'f_dc_0', data: fdc0 }, + { name: 'f_dc_1', data: fdc1 }, + { name: 'f_dc_2', data: fdc2 } ]); - pending.push(writeLabels('sh0.webp', [...cd.labels.map(c => c.data), opacityData], indices)); + pending.push(writeLabels('sh0.webp', [...cd.labels.map((c) => c.data), opacityData], indices)); const codebook = Array.from(cd.centroids); let shNLocal: ShNMeta | null = null; @@ -327,12 +363,19 @@ const writeSogSource = async ( const shCoeffs = [0, 3, 8, 15][shBands]; const paletteSize = Math.min(64, 2 ** Math.floor(Math.log2(numRows / 1024))) * 1024; const gpuDevice = createDevice ? await createDevice() : undefined; - const { centroids, labels } = await kmeansInterleaved(shRest, numRows, restCount, paletteSize, iterations, gpuDevice); + const { centroids, labels } = await kmeansInterleaved( + shRest, + numRows, + restCount, + paletteSize, + iterations, + gpuDevice + ); const numCentroids = centroids.length / restCount; // quantize the centroid palette to a uint8 codebook. De-interleave // the (small) centroids into restCount columns for the quantizer. - const cbCols: { name: string, data: Float32Array }[] = []; + const cbCols: { name: string; data: Float32Array }[] = []; for (let j = 0; j < restCount; ++j) { const col = new Float32Array(numCentroids); for (let i = 0; i < numCentroids; ++i) col[i] = centroids[i * restCount + j]; @@ -351,7 +394,7 @@ const writeSogSource = async ( } const cb = await codebookPromise; - const cbLabels = cb.labels.map(c => c.data); // restCount columns, length numCentroids + const cbLabels = cb.labels.map((c) => c.data); // restCount columns, length numCentroids const centroidsBuf = new Uint8Array(64 * shCoeffs * Math.ceil(numCentroids / 64) * channels); for (let i = 0; i < numCentroids; ++i) { for (let j = 0; j < shCoeffs; ++j) { @@ -390,7 +433,7 @@ const writeSogSource = async ( sh0: { codebook: colorsCodebook, files: ['sh0.webp'] }, ...(shN ? { shN } : {}) }; - const metaJson = (new TextEncoder()).encode(JSON.stringify(metaObj)); + const metaJson = new TextEncoder().encode(JSON.stringify(metaObj)); const metaFilename = zipFs ? 'meta.json' : outputFilename; await writeFile(outputFs, metaFilename, metaJson); if (emitInfo && !zipFs) { diff --git a/src/lib/writers/write-splat-streaming.ts b/src/lib/writers/write-splat-streaming.ts index f496d996..a54301b7 100644 --- a/src/lib/writers/write-splat-streaming.ts +++ b/src/lib/writers/write-splat-streaming.ts @@ -1,5 +1,5 @@ -import { type ChunkDataPool, type ChunkSource } from '../chunk'; -import { type FileSystem } from '../io/write'; +import type { ChunkDataPool, ChunkSource } from '../chunk'; +import type { FileSystem } from '../io/write'; import { bakeTransform } from '../ops'; import { logger, Transform } from '../utils'; @@ -60,11 +60,11 @@ const writeSplatStreaming = async ( await baked.read({ chunkIndex: k, position: pos, geometric: geo, color: col }); - const P = pos.field('position'); // count × 3 - const R = geo.field('rotation'); // count × 4 - const S = geo.field('scale'); // count × 3 - const O = geo.field('opacity'); // count × 1 - const C = col.field('dc'); // count × 3 + const P = pos.field('position'); // count × 3 + const R = geo.field('rotation'); // count × 4 + const S = geo.field('scale'); // count × 3 + const O = geo.field('opacity'); // count × 1 + const C = col.field('dc'); // count × 3 for (let i = 0; i < count; i++) { const off = i * 32; diff --git a/src/lib/writers/write-spz.ts b/src/lib/writers/write-spz.ts index a6b0b3c8..f81d9211 100644 --- a/src/lib/writers/write-spz.ts +++ b/src/lib/writers/write-spz.ts @@ -1,14 +1,17 @@ import { basename } from 'pathe'; -import { logWrittenFile } from './utils'; -import { type FileSystem, writeFile } from '../io/write'; -import { type SplatModel } from '../splat-model'; +import type { DataTable } from '../data-table'; +import { writeFile } from '../io/write'; +import type { FileSystem } from '../io/write'; +import type { SplatModel } from '../splat-model'; import { dataTableToGaussianCloud, getSpzModule, makeSpzPackOptions } from '../spz-module'; import { logger } from '../utils'; +import { logWrittenFile } from './utils'; + type WriteSpzOptions = { filename: string; - dataTable: import('../data-table').DataTable; + dataTable: DataTable; model?: SplatModel; version?: 3 | 4; }; diff --git a/src/lib/writers/write-voxel.ts b/src/lib/writers/write-voxel.ts index cef259bd..b80da50e 100644 --- a/src/lib/writers/write-voxel.ts +++ b/src/lib/writers/write-voxel.ts @@ -1,38 +1,35 @@ import { basename } from 'pathe'; import { Vec3 } from 'playcanvas'; -import { buildCollisionMesh } from './collision-glb'; -import { logWrittenFile } from './utils'; -import { Column, DataTable, computeGaussianExtents, computeWriteTransform, transformColumns, type Bounds } from '../data-table'; +import { Column, DataTable, computeGaussianExtents, computeWriteTransform, transformColumns } from '../data-table'; +import type { Bounds } from '../data-table'; import { GpuDilation, GpuVoxelization } from '../gpu'; -import { type FileSystem, writeFile } from '../io/write'; +import { writeFile } from '../io/write'; +import type { FileSystem } from '../io/write'; import { GaussianBVH } from '../spatial'; import type { CollisionMeshShape, DeviceCreator } from '../types'; import { fmtCount, logger, Transform } from '../utils'; import { version } from '../version'; -import { buildSparseOctree, buildSparseOctreeFromBuffer, MAX_V1_MIXED_LEAVES, type SparseOctree } from './sparse-octree'; -import { - filterAndFillBlocks, - alignGridBounds, - carve, - fillExterior, - fillFloor, - type NavSeed, - voxelizeToBuffer -} from '../voxel'; +import { filterAndFillBlocks, alignGridBounds, carve, fillExterior, fillFloor, voxelizeToBuffer } from '../voxel'; +import type { NavSeed } from '../voxel'; import type { BlockMaskBuffer } from '../voxel/block-mask-buffer'; import { SparseVoxelGrid } from '../voxel/sparse-voxel-grid'; +import { buildCollisionMesh } from './collision-glb'; +import { buildSparseOctree, buildSparseOctreeFromBuffer, MAX_V1_MIXED_LEAVES } from './sparse-octree'; +import type { SparseOctree } from './sparse-octree'; +import { logWrittenFile } from './utils'; + const MAX_MUTABLE_GRID_PEAK_BYTES = 1024 * 1024 * 1024; -interface OccupiedBlockBounds { +type OccupiedBlockBounds = { minBx: number; minBy: number; minBz: number; maxBx: number; maxBy: number; maxBz: number; -} +}; const getBufferOccupiedBounds = ( buffer: BlockMaskBuffer, @@ -40,8 +37,12 @@ const getBufferOccupiedBounds = ( nby: number, nbz: number ): OccupiedBlockBounds | null => { - let minBx = nbx, minBy = nby, minBz = nbz; - let maxBx = -1, maxBy = -1, maxBz = -1; + let minBx = nbx, + minBy = nby, + minBz = nbz; + let maxBx = -1, + maxBy = -1, + maxBz = -1; const scan = (indices: Float64Array): void => { for (let i = 0; i < indices.length; i++) { @@ -105,7 +106,9 @@ const assertMutableGridFits = ( ): void => { const totalBlocks = nbx * nby * nbz; if (!Number.isSafeInteger(totalBlocks) || totalBlocks > 0x100000000) { - throw new Error(`Voxel mutation requires ${fmtCount(totalBlocks)} blocks, exceeding the 32-bit mutable-grid limit. Use a coarser voxel resolution or disable navigation, fill, and collision output.`); + throw new Error( + `Voxel mutation requires ${fmtCount(totalBlocks)} blocks, exceeding the 32-bit mutable-grid limit. Use a coarser voxel resolution or disable navigation, fill, and collision output.` + ); } const typeBytes = Math.ceil(totalBlocks / 16) * 4; @@ -115,8 +118,8 @@ const assertMutableGridFits = ( if (estimatedPeak > MAX_MUTABLE_GRID_PEAK_BYTES) { throw new Error( `Voxel mutation would require approximately ${fmtCount(Math.ceil(estimatedPeak / (1024 * 1024)))} MiB ` + - `of mutable-grid storage, exceeding the ${MAX_MUTABLE_GRID_PEAK_BYTES / (1024 * 1024)} MiB safety limit. ` + - 'Use a coarser voxel resolution or disable navigation, fill, and collision output.' + `of mutable-grid storage, exceeding the ${MAX_MUTABLE_GRID_PEAK_BYTES / (1024 * 1024)} MiB safety limit. ` + + 'Use a coarser voxel resolution or disable navigation, fill, and collision output.' ); } }; @@ -162,6 +165,7 @@ type WriteVoxelOptions = { /** * Metadata for a voxel octree file. */ +// eslint-disable-next-line @typescript-eslint/consistent-type-definitions -- preserve public declaration merging interface VoxelMetadata { /** File format version */ version: string; @@ -218,7 +222,7 @@ const cropToOccupied = ( const { nbx, nby, nbz } = grid; const boundsBar = logger.bar('Scanning bounds', grid.types.length); - const occupiedBounds = grid.getOccupiedBlockBounds(done => boundsBar.update(done)); + const occupiedBounds = grid.getOccupiedBlockBounds((done) => boundsBar.update(done)); boundsBar.end(); if (!occupiedBounds) { @@ -230,15 +234,13 @@ const cropToOccupied = ( const cropMaxBy = maxBy + 1; const cropMaxBz = maxBz + 1; - if (minBx === 0 && minBy === 0 && minBz === 0 && - cropMaxBx === nbx && cropMaxBy === nby && cropMaxBz === nbz) { + if (minBx === 0 && minBy === 0 && minBz === 0 && cropMaxBx === nbx && cropMaxBy === nby && cropMaxBz === nbz) { return { grid, gridBounds }; } const cropBar = logger.bar('Cropping grid', grid.types.length); - const croppedGrid = grid.cropTo( - minBx, minBy, minBz, cropMaxBx, cropMaxBy, cropMaxBz, - done => cropBar.update(done) + const croppedGrid = grid.cropTo(minBx, minBy, minBz, cropMaxBx, cropMaxBy, cropMaxBz, (done) => + cropBar.update(done) ); cropBar.end(); @@ -279,7 +281,7 @@ const cropToNavigable = ( const { nbx, nby, nbz } = grid; const boundsBar = logger.bar('Scanning bounds', grid.types.length); - const navBounds = grid.getNavigableBlockBounds(done => boundsBar.update(done)); + const navBounds = grid.getNavigableBlockBounds((done) => boundsBar.update(done)); boundsBar.end(); if (!navBounds) { return { grid, gridBounds }; @@ -302,15 +304,20 @@ const cropToNavigable = ( const cropMaxBy = Math.min(nby, maxBy + 1 + MARGIN); const cropMaxBz = Math.min(nbz, maxBz + 1 + MARGIN); - if (cropMinBx === 0 && cropMinBy === 0 && cropMinBz === 0 && - cropMaxBx === nbx && cropMaxBy === nby && cropMaxBz === nbz) { + if ( + cropMinBx === 0 && + cropMinBy === 0 && + cropMinBz === 0 && + cropMaxBx === nbx && + cropMaxBy === nby && + cropMaxBz === nbz + ) { return { grid, gridBounds }; } const cropBar = logger.bar('Cropping grid', grid.types.length); - const croppedGrid = grid.cropTo( - cropMinBx, cropMinBy, cropMinBz, cropMaxBx, cropMaxBy, cropMaxBz, - done => cropBar.update(done) + const croppedGrid = grid.cropTo(cropMinBx, cropMinBy, cropMinBz, cropMaxBx, cropMaxBy, cropMaxBz, (done) => + cropBar.update(done) ); cropBar.end(); @@ -339,11 +346,7 @@ const cropToNavigable = ( * @param jsonFilename - Output filename for JSON metadata. * @param octree - Sparse octree structure to write. */ -const writeOctreeFiles = async ( - fs: FileSystem, - jsonFilename: string, - octree: SparseOctree -): Promise => { +const writeOctreeFiles = async (fs: FileSystem, jsonFilename: string, octree: SparseOctree): Promise => { // Build metadata object const metadata: VoxelMetadata = { version: '1.1', @@ -367,7 +370,7 @@ const writeOctreeFiles = async ( leafDataCount: octree.leafData.length }; - const jsonBytes = (new TextEncoder()).encode(JSON.stringify(metadata, null, 2)); + const jsonBytes = new TextEncoder().encode(JSON.stringify(metadata, null, 2)); await writeFile(fs, jsonFilename, jsonBytes); logWrittenFile(basename(jsonFilename), jsonBytes.byteLength); @@ -437,7 +440,9 @@ const writeVoxel = async (options: WriteVoxelOptions, fs: FileSystem): Promise !dataTable.hasColumn(name)); + const missingColumns = voxelColumns.filter((name) => !dataTable.hasColumn(name)); if (missingColumns.length > 0) { throw new Error(`writeVoxel: missing required column(s): ${missingColumns.join(', ')}`); } const delta = computeWriteTransform(dataTable.transform, Transform.IDENTITY); let cols: ReturnType | null = transformColumns(dataTable, voxelColumns, delta); - let pcDataTable: DataTable | null = new DataTable(voxelColumns.map(name => new Column(name, cols!.get(name)!))); + let pcDataTable: DataTable | null = new DataTable(voxelColumns.map((name) => new Column(name, cols!.get(name)!))); let extentsResult: ReturnType | null = computeGaussianExtents(pcDataTable); const bounds = extentsResult.sceneBounds; @@ -476,7 +488,9 @@ const writeVoxel = async (options: WriteVoxelOptions, fs: FileSystem): Promise loadBar.update(Math.min(done, total)) + let grid = SparseVoxelGrid.fromBuffer(filteredBuffer, nxInit, nyInit, nzInit, (done, total) => + loadBar.update(Math.min(done, total)) ); loadBar.end(); filteredBuffer.clear(); @@ -578,8 +591,11 @@ const writeVoxel = async (options: WriteVoxelOptions, fs: FileSystem): Promise { } })(); +const skip = !distExists + ? 'dist/cli.mjs missing — run `npm run build` first (or `npm test`, which builds via the pretest hook)' + : (await enumerateAdapters()).length === 0 + ? 'no WebGPU adapter available' + : false; + const runCli = (args) => { return new Promise((resolve, reject) => { const child = spawn(process.execPath, [cliPath, ...args], { @@ -68,7 +76,7 @@ const runCli = (args) => { }); }; -describe('Render goldens', { skip: distExists ? false : 'dist/cli.mjs missing — run `npm run build` first (or `npm test`, which builds via the pretest hook)' }, () => { +describe('Render goldens', { skip }, () => { for (const { name, args, goldenPath } of CASES) { it(`${name} matches golden`, async () => { const outPath = join(tmpdir(), `golden-${name}-${process.pid}.webp`); From 9931623262ce9a8763181788da75ab218a01a631 Mon Sep 17 00:00:00 2001 From: KPal Date: Wed, 5 Aug 2026 18:12:23 +0100 Subject: [PATCH 2/2] chore: replace explicit any types --- eslint.config.mjs | 1 - src/cli/index.ts | 4 +- src/cli/node-device.ts | 46 ++++++++++++++++++----- src/cli/node-file-system.ts | 6 +-- src/lib/data-table/data-table.ts | 2 +- src/lib/decimate-uniform/gpu-edge-cost.ts | 3 +- src/lib/global.d.ts | 18 ++++++++- src/lib/gpu/gpu-edge-cost.ts | 3 +- src/lib/gpu/gpu-kmeans.ts | 13 ++++++- src/lib/gpu/gpu-recost.ts | 6 ++- src/lib/process.ts | 27 ++++++------- src/lib/readers/container-source.ts | 6 +-- src/lib/readers/read-lcc.ts | 33 +++++++++++----- src/lib/readers/read-lcc2.ts | 23 ++++++------ src/lib/readers/read-mjs.ts | 5 ++- src/lib/types.ts | 2 +- src/lib/utils/logger.ts | 10 ++--- src/lib/utils/webp-codec.ts | 4 +- src/lib/workers/tasks.ts | 4 +- src/lib/workers/worker-entry.ts | 13 +++++-- src/lib/workers/worker-queue.ts | 28 +++++++++----- src/lib/writers/compressed-chunk.ts | 5 ++- src/lib/writers/write-compressed-ply.ts | 4 +- src/lib/writers/write-glb.ts | 19 ++++++++-- src/lib/writers/write-html.ts | 6 +-- src/lib/writers/write-lod.ts | 2 +- src/lib/writers/write-sog.ts | 5 +-- 27 files changed, 196 insertions(+), 102 deletions(-) diff --git a/eslint.config.mjs b/eslint.config.mjs index 5e46c602..64666ade 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -13,7 +13,6 @@ export default [ }, rules: { '@typescript-eslint/ban-ts-comment': 'off', - '@typescript-eslint/no-explicit-any': 'off', '@typescript-eslint/no-unused-vars': 'off', 'lines-between-class-members': 'off', 'no-await-in-loop': 'off', diff --git a/src/cli/index.ts b/src/cli/index.ts index 1b7a2a8b..72f0e285 100644 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -80,8 +80,8 @@ const fileExists = async (filename: string) => { try { await lstat(filename); return true; - } catch (e: any) { - if (e?.code === 'ENOENT') { + } catch (e) { + if ((e as { code?: string })?.code === 'ENOENT') { return false; } throw e; // real error (permissions, etc) diff --git a/src/cli/node-device.ts b/src/cli/node-device.ts index b7930ecb..7f24883a 100644 --- a/src/cli/node-device.ts +++ b/src/cli/node-device.ts @@ -8,12 +8,32 @@ const initializeGlobals = () => { Object.assign(globalThis, globals); // window stub - (globalThis as any).window = { + (globalThis as unknown as { window: { navigator: { userAgent: string } } }).window = { navigator: { userAgent: 'node.js' } }; // document stub - (globalThis as any).document = { + ( + globalThis as unknown as { + document: { + createElement: (type: string) => + | { + getContext: () => null; + getBoundingClientRect: () => { + left: number; + top: number; + width: number; + height: number; + right: number; + bottom: number; + }; + width: number; + height: number; + } + | undefined; + }; + } + ).document = { createElement: (type: string) => { if (type === 'canvas') { return { @@ -135,8 +155,17 @@ const createDevice = async (adapterName?: string): Promise => { // LOD chain ended up with several identical full-resolution levels. Handling // it here once means every GPU consumer (decimate, filters, voxelization, …) // fails loudly without wrapping each call site in its own error scope. - // @ts-ignore - wgpu is private on WebgpuGraphicsDevice but exposed in practice - const wgpu = (graphicsDevice as any).wgpu; + const device = graphicsDevice as unknown as GraphicsDevice & { + wgpu?: { + addEventListener?: ( + type: 'uncapturederror', + handler: (event: { error?: { constructor?: { name?: string }; message?: string } }) => void + ) => void; + lost?: Promise<{ reason?: string; message?: string }>; + }; + _vram: Record; + }; + const wgpu = device.wgpu; // A corrupted GPU result must never be written out, so we escalate to a hard // failure: re-raise on the next tick so main()'s uncaughtException handler @@ -150,7 +179,7 @@ const createDevice = async (adapterName?: string): Promise => { }); }; - wgpu?.addEventListener?.('uncapturederror', (ev: any) => { + wgpu?.addEventListener?.('uncapturederror', (ev) => { const e = ev?.error; const kind = e?.constructor?.name === 'GPUOutOfMemoryError' ? 'out-of-memory' : 'error'; escalateGpuError(`${kind}: ${e?.message || '(no message)'}`); @@ -158,7 +187,7 @@ const createDevice = async (adapterName?: string): Promise => { // Skip the `destroyed` reason — that fires on intentional device.destroy() // during normal shutdown. - wgpu?.lost?.then((info: any) => { + wgpu?.lost?.then((info) => { if (info?.reason === 'destroyed') return; escalateGpuError(`device lost: reason=${info?.reason || 'unknown'}, message=${info?.message || '(none)'}`); }); @@ -173,9 +202,8 @@ const createDevice = async (adapterName?: string): Promise => { // captured. Blind spots: engine-internal readback staging buffers // bypass `_vram`, and Dawn's own overhead (pipelines, heap padding) // is invisible — the peak is a lower bound on true device memory. - // @ts-ignore - _vram is private on GraphicsDevice - const vram = (graphicsDevice as any)._vram; - (graphicsDevice as any)._vram = new Proxy(vram, { + const vram = device._vram; + device._vram = new Proxy(vram, { set(target, prop, value) { target[prop] = value; const total = target.tex + target.vb + target.ib + target.ub + target.sb; diff --git a/src/cli/node-file-system.ts b/src/cli/node-file-system.ts index f5f88f67..2a91181a 100644 --- a/src/cli/node-file-system.ts +++ b/src/cli/node-file-system.ts @@ -147,10 +147,8 @@ class FileWriter implements Writer { this.abort = async () => { // discard: close and remove the temp file, never touching the // target filename (best-effort — this runs on already-failing paths) - // eslint-disable-next-line @typescript-eslint/no-empty-function -- preserve rejection-only control flow - await fileHandle.close().catch(() => {}); - // eslint-disable-next-line @typescript-eslint/no-empty-function -- preserve rejection-only control flow - await unlink(tmpFilename).catch(() => {}); + await fileHandle.close().catch(() => undefined); + await unlink(tmpFilename).catch(() => undefined); }; } } diff --git a/src/lib/data-table/data-table.ts b/src/lib/data-table/data-table.ts index f76beacc..984f64a8 100644 --- a/src/lib/data-table/data-table.ts +++ b/src/lib/data-table/data-table.ts @@ -128,7 +128,7 @@ class DataTable { setRow(index: number, row: Row, columns = this.columns) { for (const column of columns) { - if (Object.hasOwn(row, column.name)) { + if (Reflect.apply(row.hasOwnProperty, row, [column.name])) { column.data[index] = row[column.name]; } } diff --git a/src/lib/decimate-uniform/gpu-edge-cost.ts b/src/lib/decimate-uniform/gpu-edge-cost.ts index f49d6853..8f7db838 100644 --- a/src/lib/decimate-uniform/gpu-edge-cost.ts +++ b/src/lib/decimate-uniform/gpu-edge-cost.ts @@ -361,7 +361,8 @@ class GpuEdgeCost { // the limit; the widest appearance chunk and rotR are the candidates. // posScalars (8 floats/splat) is strictly smaller than rotR (9), so the // rotR check already bounds it — no separate check needed. - const maxStorage = (device as any).limits?.maxStorageBufferBindingSize; + const maxStorage = (device as GraphicsDevice & { limits?: { maxStorageBufferBindingSize?: number } }).limits + ?.maxStorageBufferBindingSize; if (typeof maxStorage === 'number') { const checkLimit = (label: string, bytes: number) => { if (bytes > maxStorage) { diff --git a/src/lib/global.d.ts b/src/lib/global.d.ts index c5a44a94..589627f7 100644 --- a/src/lib/global.d.ts +++ b/src/lib/global.d.ts @@ -1,4 +1,20 @@ declare module '*.mjs' { - const mod: any; + type Module = { + HEAPU8: Uint8Array; + HEAPU32: Uint32Array; + _malloc: (size: number) => number; + _free: (ptr: number) => void; + _webp_free: (ptr: number) => void; + _webp_encode_lossless_rgba: ( + input: number, + width: number, + height: number, + stride: number, + output: number, + size: number + ) => number; + _webp_decode_rgba: (input: number, size: number, output: number, width: number, height: number) => number; + }; + const mod: (options: { locateFile: (path: string) => string }) => Promise; export default mod; } diff --git a/src/lib/gpu/gpu-edge-cost.ts b/src/lib/gpu/gpu-edge-cost.ts index d4558c36..3e1b0872 100644 --- a/src/lib/gpu/gpu-edge-cost.ts +++ b/src/lib/gpu/gpu-edge-cost.ts @@ -219,7 +219,8 @@ class GpuEdgeCost { // Pre-flight the per-N splat buffer against the device's storage limit // so we fail with a clear message instead of a driver-side error. - const maxStorage = (device as any).limits?.maxStorageBufferBindingSize; + const maxStorage = (device as GraphicsDevice & { limits?: { maxStorageBufferBindingSize?: number } }).limits + ?.maxStorageBufferBindingSize; if (typeof maxStorage === 'number') { const bytes = maxN * SPLAT_STRIDE * 4; if (bytes > maxStorage) { diff --git a/src/lib/gpu/gpu-kmeans.ts b/src/lib/gpu/gpu-kmeans.ts index 711f0f77..068d13d2 100644 --- a/src/lib/gpu/gpu-kmeans.ts +++ b/src/lib/gpu/gpu-kmeans.ts @@ -404,8 +404,17 @@ class GpuKmeans { const floatType = useF16 ? 'f16' : 'f32'; const bytesPerElem = useF16 ? 2 : 4; - // @ts-ignore - wgpu is private on WebgpuGraphicsDevice but exposed in practice - const wgpuLimits = (device as any).wgpu?.limits; + const wgpuLimits = ( + device as GraphicsDevice & { + wgpu?: { + limits?: { + maxComputeWorkgroupStorageSize?: number; + maxComputeInvocationsPerWorkgroup?: number; + maxStorageBufferBindingSize?: number; + }; + }; + } + ).wgpu?.limits; // flush recorded work to the queue without any CPU synchronization const submit = () => (device as unknown as { submit: () => void }).submit(); diff --git a/src/lib/gpu/gpu-recost.ts b/src/lib/gpu/gpu-recost.ts index dfb7d7e9..821540c4 100644 --- a/src/lib/gpu/gpu-recost.ts +++ b/src/lib/gpu/gpu-recost.ts @@ -503,7 +503,11 @@ class GpuRecost { allowSmall = false ): boolean { if (n < 1024 && !allowSmall) return false; // inline is instant below this on the global path - const limits = (device as any).limits; + const limits = ( + device as GraphicsDevice & { + limits?: { maxStorageBufferBindingSize?: number; maxBufferSize?: number }; + } + ).limits; const maxBinding = Math.min( typeof limits?.maxStorageBufferBindingSize === 'number' ? limits.maxStorageBufferBindingSize diff --git a/src/lib/process.ts b/src/lib/process.ts index 88b58450..4eca08e1 100644 --- a/src/lib/process.ts +++ b/src/lib/process.ts @@ -3,6 +3,7 @@ import { Vec3 } from 'playcanvas'; import { createChunkDataPool } from './chunk'; import { dataTableToChunkSource, materializeToDataTable } from './compat/data-table'; import { Column, DataTable, sortMortonOrder, convertToSpace, getSHBands } from './data-table'; +import type { Row } from './data-table'; import { decimateSource } from './decimate'; import { computeSourceStats } from './ops'; import type { InputFormat } from './read'; @@ -271,7 +272,7 @@ const endFilterGroup = (g: Group, prev: DataTable, next: DataTable) => { g.end(); }; -const filter = (dataTable: DataTable, predicate: (row: any, rowIndex: number) => boolean): DataTable => { +const filter = (dataTable: DataTable, predicate: (row: Row, rowIndex: number) => boolean): DataTable => { const indices = new Uint32Array(dataTable.numRows); let index = 0; const row = {}; @@ -341,7 +342,7 @@ const processDataTable = async ( const columnNames = result.columnNames; const hasRotation = ['rot_0', 'rot_1', 'rot_2', 'rot_3'].every((c) => columnNames.includes(c)); - const predicate = (row: any) => { + const predicate = (row: Row) => { // a zero-norm rotation quaternion cannot be normalized, so the // gaussian is unrenderable (zero-padded exporter rows otherwise // survive as visible alpha-0.5 splats at the origin) @@ -388,12 +389,12 @@ const processDataTable = async ( } const Predicates = { - lt: (row: any) => row[columnName] < value, - lte: (row: any) => row[columnName] <= value, - gt: (row: any) => row[columnName] > value, - gte: (row: any) => row[columnName] >= value, - eq: (row: any) => row[columnName] === value, - neq: (row: any) => row[columnName] !== value + lt: (row: Row) => row[columnName] < value, + lte: (row: Row) => row[columnName] <= value, + gt: (row: Row) => row[columnName] > value, + gte: (row: Row) => row[columnName] >= value, + eq: (row: Row) => row[columnName] === value, + neq: (row: Row) => row[columnName] !== value }; const predicate = Predicates[comparator as keyof typeof Predicates]; if (!predicate) { @@ -416,7 +417,7 @@ const processDataTable = async ( const inputCoeffs = [0, 3, 8, 15][inputBands]; const outputCoeffs = [0, 3, 8, 15][outputBands]; - const map: any = {}; + const map: Record = {}; for (let i = 0; i < inputCoeffs; ++i) { for (let j = 0; j < 3; ++j) { const inputName = `f_rest_${i + j * inputCoeffs}`; @@ -427,7 +428,7 @@ const processDataTable = async ( result = new DataTable( result.columns .map((column) => { - if (Object.hasOwn(map, column.name)) { + if (Reflect.apply(map.hasOwnProperty, map, [column.name])) { const name = map[column.name]; return name ? new Column(name, column.data) : null; } @@ -446,7 +447,7 @@ const processDataTable = async ( const { min, max } = processAction; if (result.transform.isIdentity()) { - const predicate = (row: any) => { + const predicate = (row: Row) => { const { x, y, z } = row; return x >= min.x && x <= max.x && y >= min.y && y <= max.y && z >= min.z && z <= max.z; }; @@ -475,7 +476,7 @@ const processDataTable = async ( rawMax[j] = (maxArr[j] - dot) / scale; } - const predicate = (row: any) => { + const predicate = (row: Row) => { const { x, y, z } = row; for (let j = 0; j < 3; j++) { const proj = rawAxes[j].x * x + rawAxes[j].y * y + rawAxes[j].z * z; @@ -498,7 +499,7 @@ const processDataTable = async ( rawRadius /= result.transform.scale; } const radiusSq = rawRadius * rawRadius; - const predicate = (row: any) => { + const predicate = (row: Row) => { const { x, y, z } = row; return (x - rawCenter.x) ** 2 + (y - rawCenter.y) ** 2 + (z - rawCenter.z) ** 2 < radiusSq; }; diff --git a/src/lib/readers/container-source.ts b/src/lib/readers/container-source.ts index d65a0f0d..692b35dc 100644 --- a/src/lib/readers/container-source.ts +++ b/src/lib/readers/container-source.ts @@ -56,8 +56,7 @@ const containerSource = async ( const evicted = order.shift()!; const ep = cache.get(evicted)!; cache.delete(evicted); - // eslint-disable-next-line @typescript-eslint/no-empty-function -- preserve rejection-only control flow - ep.then((s) => s.close()).catch(() => {}); + ep.then((s) => s.close()).catch(() => undefined); } return p; }; @@ -127,8 +126,7 @@ const containerSource = async ( }; const close = async (): Promise => { - // eslint-disable-next-line @typescript-eslint/no-empty-function -- preserve rejection-only control flow - await Promise.all([...cache.values()].map((p) => p.then((s) => s.close()).catch(() => {}))); + await Promise.all([...cache.values()].map((p) => p.then((s) => s.close()).catch(() => undefined))); cache.clear(); order.length = 0; }; diff --git a/src/lib/readers/read-lcc.ts b/src/lib/readers/read-lcc.ts index 68b53e7e..56de00ed 100644 --- a/src/lib/readers/read-lcc.ts +++ b/src/lib/readers/read-lcc.ts @@ -62,10 +62,23 @@ type CompressInfo = { envShMax: Vec3; // max environment sh }; +type LccAttribute = { + name: string; + min: number[]; + max: number[]; +}; + +type LccMeta = { + attributes: LccAttribute[]; + totalLevel: number; + splats: number[]; + fileType?: string; +}; + // parse .lcc files, such as meta.lcc -const parseMeta = (obj: any): CompressInfo => { - const attributes: Record = {}; - obj.attributes.forEach((attr: any) => { +const parseMeta = (obj: LccMeta): CompressInfo => { + const attributes: Record = {}; + obj.attributes.forEach((attr) => { attributes[attr.name] = attr; }); @@ -81,7 +94,7 @@ const parseMeta = (obj: any): CompressInfo => { return { scaleMin, scaleMax, shMin, shMax, envScaleMin, envScaleMax, envShMin, envShMax }; }; -const parseIndexBin = (raw: ArrayBuffer, meta: any): LccUnitInfo[] => { +const parseIndexBin = (raw: ArrayBuffer, meta: LccMeta): LccUnitInfo[] => { let offset = 0; const buff = new DataView(raw); @@ -479,7 +492,7 @@ const deserializeEnvironment = (raw: Uint8Array, compressInfo: CompressInfo, has const readLcc = async (fileSystem: ReadFileSystem, filename: string, options: Options): Promise => { const lccData = await readFile(fileSystem, filename); const lccText = new TextDecoder().decode(lccData); - const lccJson = JSON.parse(lccText); + const lccJson = JSON.parse(lccText) as LccMeta; const determineSH = () => { if (lccJson.fileType === 'Portable') { @@ -491,7 +504,7 @@ const readLcc = async (fileSystem: ReadFileSystem, filename: string, options: Op } // before version 4 sh seems to have always been present, but we test for shcoef attribute anyway - return lccJson.attributes.findIndex((attr: any) => attr.name === 'shcoef') !== -1; + return lccJson.attributes.findIndex((attr) => attr.name === 'shcoef') !== -1; }; // FIXME: it seems some meta.lcc files at https://developer.xgrids.com/#/download?page=sampledata do not have @@ -625,11 +638,11 @@ const readLcc = async (fileSystem: ReadFileSystem, filename: string, options: Op const LCC_TRANSFORM = (): Transform => new Transform().fromEulers(90, 0, 180); // Whether this LCC scene carries spherical harmonics (mirrors readLcc). -const lccHasSH = (lccJson: any): boolean => { +const lccHasSH = (lccJson: LccMeta): boolean => { if (lccJson.fileType === 'Portable') return false; if (lccJson.fileType === 'Quality') return true; // Pre-v4 / missing fileType: assume SH if a shcoef attribute is present. - return lccJson.attributes.findIndex((attr: any) => attr.name === 'shcoef') !== -1; + return lccJson.attributes.findIndex((attr) => attr.name === 'shcoef') !== -1; }; /** @@ -665,7 +678,7 @@ const readLccSource = async ( options: Options, pool: ChunkDataPool ): Promise => { - const lccJson = JSON.parse(new TextDecoder().decode(await readFile(fileSystem, filename))); + const lccJson = JSON.parse(new TextDecoder().decode(await readFile(fileSystem, filename))) as LccMeta; const hasSH = lccHasSH(lccJson); const compressInfo = parseMeta(lccJson); const splats = lccJson.splats; @@ -1034,7 +1047,7 @@ const readLccEnvironmentSource = async ( filename: string, pool: ChunkDataPool ): Promise => { - const lccJson = JSON.parse(new TextDecoder().decode(await readFile(fileSystem, filename))); + const lccJson = JSON.parse(new TextDecoder().decode(await readFile(fileSystem, filename))) as LccMeta; const hasSH = lccHasSH(lccJson); const compressInfo = parseMeta(lccJson); diff --git a/src/lib/readers/read-lcc2.ts b/src/lib/readers/read-lcc2.ts index 7ea1f24e..45622962 100644 --- a/src/lib/readers/read-lcc2.ts +++ b/src/lib/readers/read-lcc2.ts @@ -33,7 +33,7 @@ type RawLcc2Node = { data?: { '3dgs'?: { name: number; start?: number; count?: number }; env?: { name: number }; - [key: string]: any; + [key: string]: unknown; }; // Children: array or { "0": node, "1": node, ... } object map. child?: RawLcc2Node[] | Record; @@ -41,14 +41,14 @@ type RawLcc2Node = { // Old protocol fields (need mapping). files?: string[]; child_num?: number; - datatype?: any; + datatype?: unknown; // New protocol fields (used directly). splatFiles?: string[]; childNum?: number; - dataType?: any; + dataType?: unknown; - [key: string]: any; + [key: string]: unknown; }; // Top-level meta.lcc2 raw shape. @@ -67,12 +67,12 @@ type RawLcc2Meta = { splatType?: '.sog' | '.spz'; // Bounding box (passed through for downstream/debug use). - boundingBox?: any; + boundingBox?: unknown; // Octree root node. root: RawLcc2Node; - [key: string]: any; + [key: string]: unknown; }; // --- Normalized model (post-parse) ------------------------------------------ @@ -83,13 +83,13 @@ type Lcc2TreeNode = { data?: { '3dgs'?: { name: number; start?: number; count?: number }; env?: { name: number }; - [key: string]: any; + [key: string]: unknown; }; child?: Lcc2TreeNode[] | Record; splatFiles?: string[]; childNum?: number; - dataType?: any; - [key: string]: any; + dataType?: unknown; + [key: string]: unknown; }; // Return value of parseLcc2Meta. @@ -105,7 +105,7 @@ type Lcc2Meta = { /** Unified chunk encoding type, defaults to '.sog'. */ splatType: '.sog' | '.spz'; /** Bounding box (passed through). */ - boundingBox: any; + boundingBox: unknown; /** Optional environment chunk file index (root.data.env.name); undefined if absent. */ envFileIndex: number | undefined; /** Octree root node (normalized). */ @@ -505,8 +505,7 @@ const gunzipPrefix = async (stream: ReadStream, length: number): Promise {}); + await reader.cancel().catch(() => undefined); } }; diff --git a/src/lib/readers/read-mjs.ts b/src/lib/readers/read-mjs.ts index b9d9e8c6..504ebcdd 100644 --- a/src/lib/readers/read-mjs.ts +++ b/src/lib/readers/read-mjs.ts @@ -1,10 +1,11 @@ import { Column, DataTable } from '../data-table'; +import type { Row } from '../data-table'; import type { Param } from '../types'; type Generator = { count: number; columnNames: string[]; - getRow: (index: number, row: any) => void; + getRow: (index: number, row: Row) => void; }; /** @@ -35,7 +36,7 @@ const readMjs = async (moduleUrl: string, params: Param[]): Promise = return new Column(name, new Float32Array(generator.count)); }); - const row: any = {}; + const row: Row = {}; for (let i = 0; i < generator.count; ++i) { generator.getRow(i, row); columns.forEach((c) => { diff --git a/src/lib/types.ts b/src/lib/types.ts index 3e483a58..54e05cfd 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -17,7 +17,7 @@ type Options = { lodSelect?: number[]; /** Viewer settings JSON for HTML output */ - viewerSettingsJson?: any; + viewerSettingsJson?: unknown; /** Whether to generate unbundled HTML output with separate files. Default: false */ unbundled?: boolean; diff --git a/src/lib/utils/logger.ts b/src/lib/utils/logger.ts index f565bfc8..b7657e41 100644 --- a/src/lib/utils/logger.ts +++ b/src/lib/utils/logger.ts @@ -160,7 +160,7 @@ const now = (): number => { return Date.now(); }; -const fmtArgs = (args: any[]): string => { +const fmtArgs = (args: unknown[]): string => { return args .map((a) => { if (a instanceof Error) return a.stack ?? a.message; @@ -492,7 +492,7 @@ const logger = { * Emit an info message indented under the innermost active scope. * @param args - Message parts (joined with a space). */ - info(...args: any[]): void { + info(...args: unknown[]): void { if (!core.isLevelVisible('info')) return; core.message('info', fmtArgs(args)); }, @@ -501,7 +501,7 @@ const logger = { * Emit a warning indented under the innermost active scope. * @param args - Message parts. */ - warn(...args: any[]): void { + warn(...args: unknown[]): void { if (!core.isLevelVisible('warn')) return; core.message('warn', fmtArgs(args)); }, @@ -511,7 +511,7 @@ const logger = { * an automatic unwind of all open scopes, marking each as failed. * @param args - Message parts. */ - error(...args: any[]): void { + error(...args: unknown[]): void { core.message('error', fmtArgs(args)); }, @@ -519,7 +519,7 @@ const logger = { * Emit a debug message. Shown only at `verbose` verbosity. * @param args - Message parts. */ - debug(...args: any[]): void { + debug(...args: unknown[]): void { if (!core.isLevelVisible('debug')) return; core.message('debug', fmtArgs(args)); }, diff --git a/src/lib/utils/webp-codec.ts b/src/lib/utils/webp-codec.ts index 18521100..c85860d9 100644 --- a/src/lib/utils/webp-codec.ts +++ b/src/lib/utils/webp-codec.ts @@ -14,9 +14,9 @@ class WebPCodec { */ static wasmUrl: string | null = null; - private static modulePromise: Promise | null = null; + private static modulePromise: ReturnType | null = null; - Module: any; + Module: Awaited>; /** * The effective webp.wasm location to hand to worker threads (which can't diff --git a/src/lib/workers/tasks.ts b/src/lib/workers/tasks.ts index 9818cc15..aa586e99 100644 --- a/src/lib/workers/tasks.ts +++ b/src/lib/workers/tasks.ts @@ -219,9 +219,9 @@ type TaskResult = Awaited { @@ -57,7 +61,10 @@ if (typeof process !== 'undefined' && !!process.versions?.node && (process as an } else { // tsconfig lib "dom" types postMessage/onmessage as Window's; cast to the // dedicated worker scope shape - const scope = globalThis as any; + const scope = globalThis as unknown as { + postMessage: (message: WorkerMessage, transfer: ArrayBuffer[]) => void; + onmessage: ((event: MessageEvent) => void) | null; + }; bind( (message, transfer) => scope.postMessage(message, transfer), (handler) => { diff --git a/src/lib/workers/worker-queue.ts b/src/lib/workers/worker-queue.ts index 051e8c53..c7003ac9 100644 --- a/src/lib/workers/worker-queue.ts +++ b/src/lib/workers/worker-queue.ts @@ -1,14 +1,14 @@ import { WebPCodec } from '../utils/webp-codec'; import { taskHandlers } from './tasks'; -import type { TaskName, TaskArgs, TaskResult, WorkerMessage } from './tasks'; +import type { TaskName, TaskArgs, TaskResult, HostMessage, WorkerMessage } from './tasks'; import { workerBundled } from './worker-bundled'; type PendingTask = { task: TaskName; - args: any; + args: unknown; transfer: ArrayBuffer[]; - resolve: (result: any) => void; + resolve: (result: unknown) => void; reject: (err: Error) => void; }; @@ -18,18 +18,21 @@ type Slot = { state: 'starting' | 'idle' | 'busy'; current: PendingTask | null; dead: boolean; - post: (message: any, transfer: ArrayBuffer[]) => void; + post: (message: HostMessage, transfer: ArrayBuffer[]) => void; terminate: () => void; ref: () => void; unref: () => void; }; // same runtime check as the emscripten glue in lib/webp.mjs -const isNode = typeof process !== 'undefined' && !!process.versions?.node && (process as any).type !== 'renderer'; +const isNode = + typeof process !== 'undefined' && + !!process.versions?.node && + (process as NodeJS.Process & { type?: string }).type !== 'renderer'; let slots: Slot[] = []; const queue: PendingTask[] = []; -const outstanding = new Set>(); +const outstanding = new Set>(); // user-configurable: max worker threads (null = auto), 0 forces inline let maxWorkers: number | null = null; @@ -52,7 +55,7 @@ const inlineMode = () => !workerBundled || unavailable || maxWorkers === 0; const runTaskInline = async (task: PendingTask) => { try { - const { result } = await taskHandlers[task.task](task.args); + const { result } = await taskHandlers[task.task](task.args as never); task.resolve(result); } catch (err) { task.reject(err instanceof Error ? err : new Error(String(err))); @@ -249,7 +252,13 @@ function dispatch() { const enqueue = (task: T, args: TaskArgs, transfer: ArrayBuffer[]): Promise> => { const promise = new Promise>((resolve, reject) => { - const pending: PendingTask = { task, args, transfer, resolve, reject }; + const pending: PendingTask = { + task, + args, + transfer, + resolve: (result) => resolve(result as TaskResult), + reject + }; if (inlineMode()) { runTaskInline(pending); } else { @@ -259,8 +268,7 @@ const enqueue = (task: T, args: TaskArgs, transfer: Array }); outstanding.add(promise); - // eslint-disable-next-line @typescript-eslint/no-empty-function -- preserve rejection-only control flow - promise.catch(() => {}).then(() => outstanding.delete(promise)); + promise.catch(() => undefined).then(() => outstanding.delete(promise)); return promise; }; diff --git a/src/lib/writers/compressed-chunk.ts b/src/lib/writers/compressed-chunk.ts index 0703096a..30e7d4b9 100644 --- a/src/lib/writers/compressed-chunk.ts +++ b/src/lib/writers/compressed-chunk.ts @@ -1,5 +1,6 @@ import { Quat } from 'playcanvas'; +import type { Row } from '../data-table'; import { sigmoid } from '../utils'; const q = new Quat(); @@ -24,7 +25,7 @@ class CompressedChunk { ]; size: number; - data: any = {}; + data: Record = {}; // compressed data chunkData: Float32Array; @@ -45,7 +46,7 @@ class CompressedChunk { this.color = new Uint32Array(size); } - set(index: number, data: any) { + set(index: number, data: Row) { CompressedChunk.members.forEach((m) => { this.data[m][index] = data[m]; }); diff --git a/src/lib/writers/write-compressed-ply.ts b/src/lib/writers/write-compressed-ply.ts index 28dec59c..25d4a518 100644 --- a/src/lib/writers/write-compressed-ply.ts +++ b/src/lib/writers/write-compressed-ply.ts @@ -2,7 +2,7 @@ import { basename } from 'pathe'; import type { ChunkSource, ChunkDataPool } from '../chunk'; import { materializeToDataTable } from '../compat/data-table'; -import type { DataTable } from '../data-table'; +import type { DataTable, Row } from '../data-table'; import { sortMortonOrder, convertToSpace, getSHBands, shRestNames } from '../data-table'; import type { FileSystem } from '../io/write'; import type { SplatModel } from '../splat-model'; @@ -105,7 +105,7 @@ const writeCompressedPly = async (options: WriteCompressedPlyOptions, fs: FileSy } sortMortonOrder(dataTable, sortIndices); - const row: any = {}; + const row: Row = {}; const chunk = new CompressedChunk(); diff --git a/src/lib/writers/write-glb.ts b/src/lib/writers/write-glb.ts index e57f810a..f47668b8 100644 --- a/src/lib/writers/write-glb.ts +++ b/src/lib/writers/write-glb.ts @@ -15,6 +15,17 @@ type WriteGlbOptions = { dataTable: DataTable; }; +type Accessor = { + bufferView: number; + byteOffset: number; + componentType: number; + count: number; + type: string; + normalized?: boolean; + min?: number[]; + max?: number[]; +}; + // glTF accessor component types const FLOAT = 5126; const UNSIGNED_BYTE = 5121; @@ -280,8 +291,8 @@ const writeGlb = async (options: WriteGlbOptions, fs: FileSystem) => { const { segments, offsets, binBuffer } = buildBinaryBuffer(dataTable, numSplats, shBands); // Build glTF JSON - const bufferViews: any[] = []; - const accessors: any[] = []; + const bufferViews: { buffer: number; byteOffset: number; byteLength: number; target: number }[] = []; + const accessors: Accessor[] = []; const attributes: Record = {}; for (let i = 0; i < segments.length; i++) { @@ -294,7 +305,7 @@ const writeGlb = async (options: WriteGlbOptions, fs: FileSystem) => { target: ARRAY_BUFFER }); - const accessor: any = { + const accessor: Accessor = { bufferView: i, byteOffset: 0, componentType: seg.componentType, @@ -314,7 +325,7 @@ const writeGlb = async (options: WriteGlbOptions, fs: FileSystem) => { attributes[seg.name] = i; } - const gltf: any = { + const gltf = { asset: { version: '2.0', generator: `splat-transform ${version}` diff --git a/src/lib/writers/write-html.ts b/src/lib/writers/write-html.ts index 71bbae79..f00568bd 100644 --- a/src/lib/writers/write-html.ts +++ b/src/lib/writers/write-html.ts @@ -22,7 +22,7 @@ const defaultSettings = { vignette: { enabled: false, intensity: 0.5, inner: 0.3, outer: 0.75, curvature: 1 }, fringing: { enabled: false, intensity: 0.5 } }, - animTracks: [] as any[], + animTracks: [] as unknown[], cameras: [ { initial: { @@ -32,14 +32,14 @@ const defaultSettings = { } } ], - annotations: [] as any[], + annotations: [] as unknown[], startMode: 'default' }; type WriteHtmlOptions = { filename: string; dataTable: DataTable; - viewerSettingsJson?: any; + viewerSettingsJson?: unknown; bundle: boolean; iterations: number; createDevice?: DeviceCreator; diff --git a/src/lib/writers/write-lod.ts b/src/lib/writers/write-lod.ts index 6b7200d1..f9110336 100644 --- a/src/lib/writers/write-lod.ts +++ b/src/lib/writers/write-lod.ts @@ -483,7 +483,7 @@ const writeLodSource = async (options: WriteLodSourceOptions, fs: FileSystem) => }; // write the meta file with float precision quantization (approx. 32-bit float => ~7 significant digits) - const replacer = (_key: string, value: any) => { + const replacer = (_key: string, value: unknown) => { if (typeof value === 'number') { if (!Number.isFinite(value)) return value; return Number.isInteger(value) ? value : +value.toPrecision(7); diff --git a/src/lib/writers/write-sog.ts b/src/lib/writers/write-sog.ts index 573fb47b..501886d3 100644 --- a/src/lib/writers/write-sog.ts +++ b/src/lib/writers/write-sog.ts @@ -177,8 +177,7 @@ const writeSogSource = async ( logWrittenFile(filename, webp.byteLength); } }); - // eslint-disable-next-line @typescript-eslint/no-empty-function -- preserve rejection-only control flow - writeChain = write.catch(() => {}); + writeChain = write.catch(() => undefined); return write; }; @@ -421,7 +420,7 @@ const writeSogSource = async ( await Promise.all(pending); // ---- meta.json -------------------------------------------------- - const metaObj: any = { + const metaObj = { version: 2, asset: { generator: `splat-transform v${version}` }, count: numRows,