@voluma/vlam 0.3.0 → 0.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. package/README.md +2 -4
  2. package/dist/chunk-loader-DlNwggfG.js.map +1 -1
  3. package/dist/{index.d.ts → core/index.d.ts} +2 -2
  4. package/dist/{splat-budget.d.ts → core/splat-budget.d.ts} +1 -1
  5. package/dist/{splat-data.d.ts → core/splat-data.d.ts} +1 -1
  6. package/dist/{splat-mesh-types.d.ts → core/splat-mesh-types.d.ts} +5 -2
  7. package/dist/{effects.d.ts → effects/index.d.ts} +3 -3
  8. package/dist/effects.js.map +1 -1
  9. package/dist/formats/ksplat/parse-ksplat.d.ts +1 -1
  10. package/dist/formats/ksplat.js.map +1 -1
  11. package/dist/formats/lcc/collision-mesh.d.ts +2 -2
  12. package/dist/formats/lcc/collision-partition.d.ts +1 -1
  13. package/dist/formats/lcc/lcc.d.ts +3 -3
  14. package/dist/formats/lcc/lcc2.d.ts +2 -2
  15. package/dist/formats/lcc/parse-lcc.d.ts +1 -1
  16. package/dist/formats/lcc.js +276 -277
  17. package/dist/formats/lcc.js.map +1 -1
  18. package/dist/formats/ply/parse-compressed-ply.d.ts +2 -2
  19. package/dist/formats/ply/parse-splat-ply.d.ts +2 -2
  20. package/dist/formats/ply.js.map +1 -1
  21. package/dist/formats/rad/frontier-worker-protocol.d.ts +8 -1
  22. package/dist/formats/rad/index.d.ts +2 -2
  23. package/dist/formats/rad/parse-rad.d.ts +2 -2
  24. package/dist/formats/rad/rad-foveated-source.d.ts +3 -3
  25. package/dist/formats/rad/rad.d.ts +5 -5
  26. package/dist/formats/rad.js.map +1 -1
  27. package/dist/formats/sog/parse-sog.d.ts +2 -2
  28. package/dist/formats/sog.js.map +1 -1
  29. package/dist/formats/splat/parse-splat.d.ts +1 -1
  30. package/dist/formats/splat.js.map +1 -1
  31. package/dist/formats/spz/parse-spz.d.ts +1 -1
  32. package/dist/formats/spz.js.map +1 -1
  33. package/dist/half-float-Cs4FDq8Q.js.map +1 -1
  34. package/dist/index.js +3 -3
  35. package/dist/index.js.map +1 -1
  36. package/dist/{chunk-loader.d.ts → loaders/chunk-loader.d.ts} +2 -2
  37. package/dist/{load-splat-data.d.ts → loaders/load-splat-data.d.ts} +1 -1
  38. package/dist/{load-worker-protocol.d.ts → loaders/load-worker-protocol.d.ts} +2 -2
  39. package/dist/loaders.js.map +1 -1
  40. package/dist/loading-wLo8vRbA.js.map +1 -1
  41. package/dist/lod-scheduler-ChoZyEeq.js +466 -0
  42. package/dist/lod-scheduler-ChoZyEeq.js.map +1 -0
  43. package/dist/logging-BfPdd7NJ.js.map +1 -1
  44. package/dist/ply-header-B9WllbPg.js.map +1 -1
  45. package/dist/{radix-sorter-R0FCSqKu.js → radix-sorter-BrbUg_CV.js} +2 -2
  46. package/dist/radix-sorter-BrbUg_CV.js.map +1 -0
  47. package/dist/{relighting-CSQF3UcL.js → relighting-Tiwep8yd.js} +302 -294
  48. package/dist/relighting-Tiwep8yd.js.map +1 -0
  49. package/dist/{selection-volume.d.ts → selection/selection-volume.d.ts} +1 -1
  50. package/dist/{splat-partition.d.ts → selection/splat-partition.d.ts} +1 -1
  51. package/dist/selection.js.map +1 -1
  52. package/dist/sh-pack-D5wAe5gg.js.map +1 -1
  53. package/dist/splat-budget-DuVZspPQ.js.map +1 -1
  54. package/dist/splat-data-BZM5dhhx.js.map +1 -1
  55. package/dist/{splat-mesh-aQdcWA6v.js → splat-mesh-CsLOQb08.js} +3 -3
  56. package/dist/splat-mesh-CsLOQb08.js.map +1 -0
  57. package/dist/splat-mesh-types-BZIko-_9.js.map +1 -1
  58. package/dist/{static-lod-splat-mesh.d.ts → static-lod/static-lod-splat-mesh.d.ts} +2 -2
  59. package/dist/{static-lod-worker-protocol.d.ts → static-lod/static-lod-worker-protocol.d.ts} +1 -1
  60. package/dist/{static-lod.d.ts → static-lod/static-lod.d.ts} +1 -1
  61. package/dist/static-lod.js +1 -1
  62. package/dist/static-lod.js.map +1 -1
  63. package/dist/{dataset-source.d.ts → streaming/dataset-source.d.ts} +1 -1
  64. package/dist/{streaming.d.ts → streaming/index.d.ts} +3 -3
  65. package/dist/{lod-scheduler.d.ts → streaming/lod-scheduler.d.ts} +32 -0
  66. package/dist/{lod-source.d.ts → streaming/lod-source.d.ts} +19 -13
  67. package/dist/{streamed-splat-mesh-utils.d.ts → streaming/streamed-splat-mesh-utils.d.ts} +2 -2
  68. package/dist/{streamed-splat-mesh.d.ts → streaming/streamed-splat-mesh.d.ts} +83 -27
  69. package/dist/streaming.js +711 -645
  70. package/dist/streaming.js.map +1 -1
  71. package/dist/{unified-splat-mesh.d.ts → unified/unified-splat-mesh.d.ts} +13 -6
  72. package/dist/{work-buffer-gather.d.ts → unified/work-buffer-gather.d.ts} +5 -5
  73. package/dist/{work-buffer-material.d.ts → unified/work-buffer-material.d.ts} +1 -1
  74. package/dist/unified.js +280 -272
  75. package/dist/unified.js.map +1 -1
  76. package/dist/webgpu-limits-Y3CI_6_6.js.map +1 -1
  77. package/package.json +9 -9
  78. package/dist/lod-scheduler-B0a_uBlv.js +0 -383
  79. package/dist/lod-scheduler-B0a_uBlv.js.map +0 -1
  80. package/dist/radix-sorter-R0FCSqKu.js.map +0 -1
  81. package/dist/relighting-CSQF3UcL.js.map +0 -1
  82. package/dist/splat-mesh-aQdcWA6v.js.map +0 -1
  83. /package/dist/{compute-sorter.d.ts → core/compute-sorter.d.ts} +0 -0
  84. /package/dist/{create-splat-renderer.d.ts → core/create-splat-renderer.d.ts} +0 -0
  85. /package/dist/{depth-of-field.d.ts → core/depth-of-field.d.ts} +0 -0
  86. /package/dist/{half-float.d.ts → core/half-float.d.ts} +0 -0
  87. /package/dist/{logging.d.ts → core/logging.d.ts} +0 -0
  88. /package/dist/{merged-splat-mesh.d.ts → core/merged-splat-mesh.d.ts} +0 -0
  89. /package/dist/{orientation.d.ts → core/orientation.d.ts} +0 -0
  90. /package/dist/{radix-sort.d.ts → core/radix-sort.d.ts} +0 -0
  91. /package/dist/{radix-sorter.d.ts → core/radix-sorter.d.ts} +0 -0
  92. /package/dist/{relighting-pass.d.ts → core/relighting-pass.d.ts} +0 -0
  93. /package/dist/{relighting.d.ts → core/relighting.d.ts} +0 -0
  94. /package/dist/{sh-pack.d.ts → core/sh-pack.d.ts} +0 -0
  95. /package/dist/{sort-scheduler.d.ts → core/sort-scheduler.d.ts} +0 -0
  96. /package/dist/{sort-worker-protocol.d.ts → core/sort-worker-protocol.d.ts} +0 -0
  97. /package/dist/{sorter.d.ts → core/sorter.d.ts} +0 -0
  98. /package/dist/{source-transform.d.ts → core/source-transform.d.ts} +0 -0
  99. /package/dist/{splat-depth-pack.d.ts → core/splat-depth-pack.d.ts} +0 -0
  100. /package/dist/{splat-mesh-material.d.ts → core/splat-mesh-material.d.ts} +0 -0
  101. /package/dist/{splat-mesh-picking.d.ts → core/splat-mesh-picking.d.ts} +0 -0
  102. /package/dist/{splat-mesh-pool.d.ts → core/splat-mesh-pool.d.ts} +0 -0
  103. /package/dist/{splat-mesh.d.ts → core/splat-mesh.d.ts} +0 -0
  104. /package/dist/{splat-modifier-stack.d.ts → core/splat-modifier-stack.d.ts} +0 -0
  105. /package/dist/{splat-modifier.d.ts → core/splat-modifier.d.ts} +0 -0
  106. /package/dist/{splat-query.d.ts → core/splat-query.d.ts} +0 -0
  107. /package/dist/{splat-sort-bounds.d.ts → core/splat-sort-bounds.d.ts} +0 -0
  108. /package/dist/{storage-attribute-mirror.d.ts → core/storage-attribute-mirror.d.ts} +0 -0
  109. /package/dist/{webgpu-limits.d.ts → core/webgpu-limits.d.ts} +0 -0
  110. /package/dist/{worker-sorter.d.ts → core/worker-sorter.d.ts} +0 -0
  111. /package/dist/{xr-view.d.ts → core/xr-view.d.ts} +0 -0
  112. /package/dist/{loaders.d.ts → loaders/index.d.ts} +0 -0
  113. /package/dist/{loading.d.ts → loaders/loading.d.ts} +0 -0
  114. /package/dist/{ply-header.d.ts → loaders/ply-header.d.ts} +0 -0
  115. /package/dist/{selection.d.ts → selection/index.d.ts} +0 -0
  116. /package/dist/{static-lod-entry.d.ts → static-lod/index.d.ts} +0 -0
  117. /package/dist/{budget-governor.d.ts → streaming/budget-governor.d.ts} +0 -0
  118. /package/dist/{camera-budget-governor.d.ts → streaming/camera-budget-governor.d.ts} +0 -0
  119. /package/dist/{chunk-cache-budget.d.ts → streaming/chunk-cache-budget.d.ts} +0 -0
  120. /package/dist/{chunk-fetch-scheduler.d.ts → streaming/chunk-fetch-scheduler.d.ts} +0 -0
  121. /package/dist/{lod-manifest.d.ts → streaming/lod-manifest.d.ts} +0 -0
  122. /package/dist/{unified.d.ts → unified/index.d.ts} +0 -0
  123. /package/dist/{unified-work-buffer.d.ts → unified/unified-work-buffer.d.ts} +0 -0
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":["../src/lib/source-transform.ts","../src/lib/merged-splat-mesh.ts","../src/lib/create-splat-renderer.ts","../src/lib/splat-modifier.ts"],"sourcesContent":["/**\n * Per-source world transforms for a unified {@link MergedSplatMesh} pool.\n *\n * Multiple splat clouds are concatenated into one pool in their own local\n * space; each cloud (\"source\") then carries a world matrix applied **in the\n * shader** - never baked into the pool data. That is what lets a source move\n * every frame at full rate: only this small uniform array changes, and neither\n * the pool textures nor the sort keys are rewritten.\n *\n * The same matrix array node is shared by two consumers so they always agree,\n * both through `sourceWorldTransform` in `splat-mesh-material.ts`:\n * - the material graph, which places a splat's center and covariance **before**\n * the modifier stack runs, so effects see the splat where it visually is, and\n * - the depth sorter, which transforms each splat's center to world space\n * before computing its view-space depth (see `ComputeSorter`).\n *\n * Internal. Nothing here is exported from `index.ts`.\n */\nimport * as THREE from 'three/webgpu';\nimport { uniformArray } from 'three/tsl';\n\n/**\n * Maximum number of sources a unified pool can hold. Sized generously - the\n * uniform array costs `4 · MAX_MERGED_SPLAT_SOURCES` vec4s (16 KB at 256), well under the\n * uniform-buffer limit - while bounding the per-splat index range.\n */\nexport const MAX_MERGED_SPLAT_SOURCES = 256;\n\n/** The channel both the material graph and the sorter read as a source id. */\nexport const SOURCE_ID_CHANNEL = 'sourceId';\n\n/**\n * A live, shader-visible array of source world matrices, stored as four vec4\n * **columns** per source (three.js `Matrix4.elements` are column-major, so a\n * column maps to `elements[k*4 .. k*4+3]`). Mutating an entry re-uploads with\n * the frame's uniforms - no material or pipeline rebuild.\n */\nexport class SourceMatrixArray {\n /** `4 · MAX_MERGED_SPLAT_SOURCES` vec4 columns; the TSL node both consumers index. */\n readonly node: ReturnType<typeof uniformArray>;\n /** CPU mirror of the same column-major matrices, for the WebGL2 sorter. */\n readonly matrices: Float32Array;\n private readonly columns: THREE.Vector4[];\n\n constructor(maxSources = MAX_MERGED_SPLAT_SOURCES) {\n this.matrices = new Float32Array(16 * maxSources);\n this.columns = Array.from({ length: 4 * maxSources }, (_, i) => {\n // Initialize every slot to the identity matrix's columns.\n const column = i % 4;\n return new THREE.Vector4(\n column === 0 ? 1 : 0,\n column === 1 ? 1 : 0,\n column === 2 ? 1 : 0,\n column === 3 ? 1 : 0,\n );\n });\n for (let i = 0; i < maxSources; i++) {\n const base = i * 16;\n this.matrices[base] = 1;\n this.matrices[base + 5] = 1;\n this.matrices[base + 10] = 1;\n this.matrices[base + 15] = 1;\n }\n this.node = uniformArray(this.columns, 'vec4');\n }\n\n /** Writes source `id`'s world matrix (column-major columns copied verbatim). */\n set(id: number, matrix: THREE.Matrix4): void {\n const e = matrix.elements;\n const base = id * 4;\n for (let c = 0; c < 4; c++) {\n (this.columns[base + c] as THREE.Vector4).set(\n e[c * 4 + 0] as number,\n e[c * 4 + 1] as number,\n e[c * 4 + 2] as number,\n e[c * 4 + 3] as number,\n );\n this.matrices[id * 16 + c * 4 + 0] = e[c * 4 + 0] as number;\n this.matrices[id * 16 + c * 4 + 1] = e[c * 4 + 1] as number;\n this.matrices[id * 16 + c * 4 + 2] = e[c * 4 + 2] as number;\n this.matrices[id * 16 + c * 4 + 3] = e[c * 4 + 3] as number;\n }\n }\n}\n\n/** A source's local bounding sphere plus its world matrix, for pool bounds. */\nexport interface SourceBounds {\n readonly center: THREE.Vector3;\n readonly radius: number;\n readonly matrix: THREE.Matrix4;\n}\n\n/**\n * The world-space bounding sphere enclosing every source, for the sorter's\n * depth quantization range. Each source sphere is transformed by its matrix\n * (radius scaled by a conservative maximum linear stretch) and the results\n * unioned.\n */\nexport function worldBoundsOf(sources: readonly SourceBounds[], out: THREE.Sphere): THREE.Sphere {\n out.makeEmpty();\n const worldCenter = new THREE.Vector3();\n const child = new THREE.Sphere();\n for (const source of sources) {\n worldCenter.copy(source.center).applyMatrix4(source.matrix);\n child.set(worldCenter, source.radius * matrixMaxStretch(source.matrix));\n out.union(child);\n }\n return out;\n}\n\n/**\n * A safe upper bound on the largest singular value of a matrix's linear part.\n *\n * This is `sqrt(||AᵀA||∞)`: exact for rotation + per-axis scale, and still\n * conservative when hierarchy composition introduces shear. The latter is\n * essential because a maximum column length can under-bound a sheared sphere.\n */\nfunction matrixMaxStretch(m: THREE.Matrix4): number {\n const e = m.elements;\n const x0 = e[0],\n y0 = e[1],\n z0 = e[2];\n const x1 = e[4],\n y1 = e[5],\n z1 = e[6];\n const x2 = e[8],\n y2 = e[9],\n z2 = e[10];\n const g00 = x0 * x0 + y0 * y0 + z0 * z0;\n const g11 = x1 * x1 + y1 * y1 + z1 * z1;\n const g22 = x2 * x2 + y2 * y2 + z2 * z2;\n const g01 = x0 * x1 + y0 * y1 + z0 * z1;\n const g02 = x0 * x2 + y0 * y2 + z0 * z2;\n const g12 = x1 * x2 + y1 * y2 + z1 * z2;\n return Math.sqrt(\n Math.max(\n g00 + Math.abs(g01) + Math.abs(g02),\n g11 + Math.abs(g01) + Math.abs(g12),\n g22 + Math.abs(g02) + Math.abs(g12),\n ),\n );\n}\n","/**\n * A scene of several independently-posed Gaussian splat clouds that all sort\n * and blend as one.\n *\n * The problem it solves: two separate {@link SplatMesh} instances are two\n * transparent draw calls, each depth-sorted only within itself, so where the\n * clouds overlap one is painted wholesale in front of the other (\"pasted over\")\n * - the same artifact PlayCanvas's unified rendering and Spark's accumulator\n * exist to fix. `MergedSplatMesh` concatenates every cloud into **one pool** and\n * runs **one global depth sort**, so splats from different clouds interleave\n * correctly, from any angle.\n *\n * Each cloud (\"source\") keeps its own world transform, applied in the shader\n * (never baked into the pool): the sorter transforms each splat's center to\n * world space before measuring depth, and the material places each splat's\n * center and covariance by its source's matrix. So {@link setSourceTransform}\n * is a handful of uniform writes - a source can be dragged or animated every\n * frame at full rate, at any splat count, with no data re-upload.\n *\n * The placement runs **before** the modifier stack, so an application effect sees each\n * splat where it visually is: an SDF light shape spanning two sources paints\n * one continuous shape, and a reveal sweeps them together. A modifier that\n * wants to travel *with* a moved source reads `ctx.sourceCenter` instead of\n * `ctx.localCenter`. See `docs/guide/effects-and-modifiers.md`.\n *\n * On WebGPU the world-depth sort runs in a compute pass; on the WebGL2\n * fallback the worker applies the same source matrices on the CPU before\n * sorting. Both backends therefore inter-sort sources correctly.\n *\n * Limitations, all inherited from the dynamic pool: sources may not carry\n * palette (SOG `shN`) view-dependent color - per-file palettes cannot be merged\n * - but DC color and per-splat (LCC) SH are fine; all sources share one\n * antialias setting (from the mesh options).\n *\n * Sources are fully decoded {@link SplatData} copied once into the pool.\n * {@link StreamedSplatMesh} swaps its resident LOD cut every frame, so it cannot\n * be a merged source. Compose streamed meshes with fully loaded meshes through\n * {@link UnifiedSplatMesh} instead.\n */\nimport * as THREE from 'three/webgpu';\nimport { SplatMesh, type SplatMeshOptions, type SplatRange } from './splat-mesh';\nimport type { SplatData } from './splat-data';\nimport { yUpTransformForFormat, type SplatOrientation } from './orientation';\nimport {\n MAX_MERGED_SPLAT_SOURCES,\n SOURCE_ID_CHANNEL,\n SourceMatrixArray,\n worldBoundsOf,\n type SourceBounds,\n} from './source-transform';\n\n/** Construction options for a {@link MergedSplatMesh}. */\nexport interface MergedSplatMeshOptions extends SplatMeshOptions {\n /** Total pool size in splats - must cover the sum of every source added. */\n capacity: number;\n /** Maximum number of sources (default {@link MAX_MERGED_SPLAT_SOURCES}). */\n maxSources?: number;\n}\n\n/** Per-source placement options. */\nexport interface MergedSplatSourceOptions {\n /**\n * Y-up normalization for this source's data frame, like {@link SplatMesh}'s\n * `orientation`. `'y-up'` (default) applies the format's stand-up correction\n * before the placement matrix; `'source'` places the raw data frame.\n */\n orientation?: SplatOrientation;\n}\n\n/** Internal per-source record. */\ninterface SourceRecord extends SourceBounds {\n /** The pool range this source's splats occupy. */\n range: SplatRange;\n /** Data-frame → world correction applied before the placement matrix. */\n correction: THREE.Matrix4 | null;\n /** Live placement (world = placement · correction); kept for re-placement. */\n placement: THREE.Matrix4;\n /** The source's own data-frame bounds, for {@link MergedSplatMesh.computeSplatBounds}. */\n box: THREE.Box3;\n}\n\n/**\n * One pool, one global sort, many independently-posed splat sources - see the\n * module overview above for the artifact this exists to remove.\n *\n * @experimental May change in a minor release.\n */\nexport class MergedSplatMesh extends SplatMesh {\n private readonly matrices: SourceMatrixArray;\n private readonly maxSources: number;\n /** Indexed by source id; holes are left by {@link removeSource}. */\n private readonly sources: (SourceRecord | undefined)[] = [];\n private nextId = 0;\n\n constructor(options: MergedSplatMeshOptions) {\n const maxSources = options.maxSources ?? MAX_MERGED_SPLAT_SOURCES;\n super({ capacity: options.capacity }, options);\n this.matrices = new SourceMatrixArray(maxSources);\n this.maxSources = maxSources;\n\n // One float per pool slot naming the splat's source; read by both the\n // material graph and the world-depth sorter.\n this.defineChannel(SOURCE_ID_CHANNEL, { type: 'float' });\n const sourceIdTexture = this.channelTexture(SOURCE_ID_CHANNEL);\n if (!sourceIdTexture) throw new Error('MergedSplatMesh: sourceId channel texture missing.');\n this.perSourceSort = {\n sourceIdTexture,\n columns: this.matrices.node,\n sourceIds: this.channelBacking(SOURCE_ID_CHANNEL),\n matrices: this.matrices.matrices,\n };\n // The base constructor already built a graph, before `perSourceSort` was\n // set - rebuild so the material actually places its splats. Without this\n // every source renders at its pool-local position.\n this.rebuildGraph();\n }\n\n /** Number of live sources. */\n get sourceCount(): number {\n let count = 0;\n for (const source of this.sources) if (source) count++;\n return count;\n }\n\n /**\n * Adds a cloud at a world placement and returns its id (for\n * {@link setSourceTransform} / {@link removeSource}). The data is copied into\n * the shared pool in its own local frame; the placement is applied live in\n * the shader.\n *\n * @throws if the pool cannot fit the cloud, or the source limit is reached.\n */\n addSource(\n data: SplatData,\n placement: THREE.Matrix4 = new THREE.Matrix4(),\n options: MergedSplatSourceOptions = {},\n ): number {\n const id = this.nextId;\n if (id >= this.maxSources) {\n throw new Error(`MergedSplatMesh: source limit reached (${this.maxSources}).`);\n }\n const range = this.appendRange(data); // local space; transform is a uniform\n this.nextId++;\n\n const ids = new Float32Array(data.count);\n ids.fill(id);\n this.writeChannel(range, SOURCE_ID_CHANNEL, ids);\n\n const orientation = options.orientation ?? 'y-up';\n const correction = orientation === 'y-up' ? yUpTransformForFormat(data.format) : null;\n const box = new THREE.Box3().setFromArray(data.positions);\n const sphere = box.getBoundingSphere(new THREE.Sphere());\n const record: SourceRecord = {\n range,\n correction,\n placement: placement.clone(),\n box,\n center: sphere.center,\n radius: sphere.radius,\n matrix: new THREE.Matrix4(),\n };\n this.sources[id] = record;\n this.applyPlacement(record);\n return id;\n }\n\n /**\n * Re-places a source. Cheap by design: only the shared matrix uniform and the\n * sort bound change, so it is safe to call every frame while dragging.\n *\n * Bad-id semantics across the triad: this method **throws** (a write to a\n * dead source is a caller bug that would otherwise vanish silently),\n * {@link getSourceTransform} returns `undefined` (querying is how you ask\n * whether a source is live), and {@link removeSource} returns `false`\n * (removal is idempotent).\n *\n * @throws if `id` is not a live source.\n */\n setSourceTransform(id: number, placement: THREE.Matrix4): void {\n const record = this.sources[id];\n if (!record) throw new Error(`MergedSplatMesh: no live source ${id}.`);\n record.placement.copy(placement);\n this.applyPlacement(record);\n }\n\n /**\n * The current world matrix of a source (a copy), or `undefined` if `id` is\n * not a live source - see {@link setSourceTransform} for the bad-id triad.\n */\n getSourceTransform(id: number): THREE.Matrix4 | undefined {\n return this.sources[id]?.matrix.clone();\n }\n\n /**\n * Removes a source and frees its pool range. The id is not reused (later ids\n * keep their matrix slots), so long-lived churn should prefer reusing a scene\n * over unbounded add/remove.\n *\n * @returns `true` when a live source was removed, `false` when `id` was not\n * live (idempotent - see {@link setSourceTransform} for the bad-id triad).\n */\n removeSource(id: number): boolean {\n const record = this.sources[id];\n if (!record) return false;\n this.removeRange(record.range);\n this.sources[id] = undefined;\n this.invalidateSort();\n return true;\n }\n\n /** Recomputes a source's world matrix (placement · correction) and republishes it. */\n private applyPlacement(record: SourceRecord): void {\n if (record.correction) record.matrix.multiplyMatrices(record.placement, record.correction);\n else record.matrix.copy(record.placement);\n const id = this.sources.indexOf(record);\n this.matrices.set(id, record.matrix);\n this.invalidateSort();\n }\n\n /** Sort depth is quantized over the world span of every source, not the pool's local box. */\n protected override refreshSortBounds(): void {\n if (!this.boundsDirty) return;\n const live: SourceBounds[] = [];\n for (const source of this.sources) if (source) live.push(source);\n worldBoundsOf(live, this.boundingSphereLocal);\n this.boundsDirty = false;\n }\n\n /**\n * Mesh-local bounds spanning every source at its **current placement** -\n * the pool's own box would be the union of the sources' data frames, which\n * is not where any of them is drawn. Each source's data-frame box is\n * transformed by its matrix, so a rotated source contributes the (standard,\n * conservative) axis-aligned box of its rotated bounds.\n *\n * Hosts use this to place effects, so it has to agree with what\n * `ctx.localCenter` reports; it tracks {@link setSourceTransform}.\n */\n override computeSplatBounds(): THREE.Box3 {\n const bounds = new THREE.Box3().makeEmpty();\n const placed = new THREE.Box3();\n for (const source of this.sources) {\n if (!source) continue;\n bounds.union(placed.copy(source.box).applyMatrix4(source.matrix));\n }\n return bounds;\n }\n}\n","/**\n * One-call WebGPU renderer construction for VLAM! scenes.\n *\n * Every host needs the same four things right - adapter, device, raised limits\n * and the platform-dependent `powerPreference` - and getting any of them wrong\n * fails *late*: past ~8.4 M unified splats, or with MSAA silently dropped.\n * {@link createSplatRenderer} is that block, once.\n *\n * The primitives it composes ({@link recommendedWebGpuRequiredLimits},\n * {@link webGpuPowerPreferenceOptions}) stay exported for hosts that own device\n * creation themselves - an XR session sharing a device, say.\n */\nimport { WebGPURenderer, type WebGPURendererParameters } from 'three/webgpu';\n\nimport { warn } from './logging';\nimport {\n recommendedWebGpuRequiredLimits,\n webGpuPowerPreferenceOptions,\n type WebGpuPowerPreference,\n} from './webgpu-limits';\n\n/*\n * The WebGPU surface is declared structurally, not via `navigator.gpu`.\n * `tsconfig.lib.json` compiles `src/lib` alone (`rootDir`, and an `include`\n * listing only the published entry points), so the ambient\n * `interface Navigator { gpu?: ... }` the viewer declares is not in scope for\n * the published build. A module here that reaches for `navigator.gpu`'s type\n * passes `npm run typecheck` and fails `npm run build:lib`.\n */\n\n/** The device handle `createSplatRenderer` hands to `WebGPURenderer`. */\nexport interface SplatRendererGpuDevice {\n readonly features: ReadonlySet<string>;\n}\n\n/** The adapter surface `createSplatRenderer` reads limits and features from. */\nexport interface SplatRendererGpuAdapter {\n readonly features: ReadonlySet<string>;\n readonly limits: {\n readonly maxStorageBufferBindingSize: number;\n readonly maxBufferSize: number;\n readonly maxTextureDimension2D?: number;\n };\n requestDevice(descriptor?: {\n requiredFeatures?: readonly string[];\n requiredLimits?: Record<string, number>;\n }): Promise<SplatRendererGpuDevice>;\n}\n\n/** The `navigator.gpu` surface `createSplatRenderer` probes. */\nexport interface SplatRendererGpu {\n requestAdapter(options?: {\n powerPreference?: WebGpuPowerPreference;\n }): Promise<SplatRendererGpuAdapter | null>;\n}\n\n/**\n * Options for {@link createSplatRenderer}.\n *\n * Everything `THREE.WebGPURenderer` accepts passes straight through -\n * `antialias`, `forceWebGL`, `trackTimestamp`, `canvas`, `alpha`, `samples`,\n * `getFallback`, … - except the three members the helper owns (`device`,\n * `requiredLimits`, and `powerPreference`, which is widened here to allow\n * `null`).\n */\nexport interface CreateSplatRendererOptions extends Omit<\n WebGPURendererParameters,\n 'device' | 'requiredLimits' | 'powerPreference'\n> {\n /**\n * Adapter/renderer power hint. Omitted automatically on Windows, where Chrome\n * ignores it and warns ([crbug.com/369219127](https://crbug.com/369219127)).\n * Pass `null` to never send it.\n *\n * @defaultValue `'high-performance'`\n */\n powerPreference?: WebGpuPowerPreference | null;\n /**\n * Throw instead of degrading to three's WebGL2 backend. Adapter and device\n * failures rethrow their original error, unwrapped.\n *\n * @defaultValue `false`\n */\n requireWebGpu?: boolean;\n /**\n * The WebGPU entry point to probe. Defaults to `navigator.gpu`; pass `null`\n * to skip the probe entirely (tests, non-DOM hosts).\n */\n gpu?: SplatRendererGpu | null;\n}\n\nfunction ambientGpu(): SplatRendererGpu | null {\n const nav = typeof navigator !== 'undefined' ? (navigator as { gpu?: unknown }) : undefined;\n const gpu = nav?.gpu;\n return gpu ? (gpu as SplatRendererGpu) : null;\n}\n\n/**\n * Creates a `THREE.WebGPURenderer` configured for splat rendering: the\n * adapter's advertised buffer/texture maxima as `requiredLimits`, every adapter\n * feature as `requiredFeatures`, and `powerPreference` only where the platform\n * honours it.\n *\n * Both raised values matter late rather than loudly. Without the limits, large\n * unified or streamed scenes throw past ~8.4 M splats. Without\n * `core-features-and-limits` among the requested features, three treats the\n * backend as compatibility-mode and **silently drops MSAA** - which is why the\n * device is requested here rather than left to `WebGPUBackend.init`. Owning the\n * request also keeps the real failure: three's `getFallback` hook logs a flat\n * \"WebGPU is not available\" and drops the cause.\n *\n * Degrades to three's WebGL2 backend exactly as an unconfigured\n * `new THREE.WebGPURenderer()` would, warning through\n * {@link setVlamLogHandler} when the cause was a fixable failure rather than a\n * browser without WebGPU. Pass `requireWebGpu: true` to throw instead.\n *\n * Does **not** call `setSize`, `renderer.init()`, or append the canvas - those\n * stay in host code - and does not touch `outputColorSpace`, whose three.js\n * default (sRGB) is already what the splat shader expects.\n *\n * @example\n * const renderer = await createSplatRenderer();\n * renderer.setSize(innerWidth, innerHeight);\n * document.body.appendChild(renderer.domElement);\n *\n * @throws {TypeError} if `requireWebGpu` and `forceWebGL` are both set.\n */\nexport async function createSplatRenderer(\n options: CreateSplatRendererOptions = {},\n): Promise<WebGPURenderer> {\n const {\n powerPreference = 'high-performance',\n requireWebGpu = false,\n gpu,\n ...rendererOptions\n } = options;\n const powerOptions =\n powerPreference === null ? {} : webGpuPowerPreferenceOptions(powerPreference);\n\n let adapter: SplatRendererGpuAdapter | null = null;\n let device: SplatRendererGpuDevice | null = null;\n\n if (rendererOptions.forceWebGL === true) {\n if (requireWebGpu) {\n throw new TypeError(\n 'vlam: createSplatRenderer cannot honour both requireWebGpu and forceWebGL.',\n );\n }\n } else {\n const entry = gpu === undefined ? ambientGpu() : gpu;\n if (!entry) {\n // Not a warning: a browser without WebGPU is not a fixable misconfiguration.\n if (requireWebGpu) {\n throw new Error('vlam: WebGPU is unavailable (no navigator.gpu) and requireWebGpu is set.');\n }\n } else {\n let adapterFailed = false;\n try {\n adapter = await entry.requestAdapter({ ...powerOptions });\n } catch (error) {\n if (requireWebGpu) throw error;\n // One warning per failure: an adapter that threw has already explained\n // itself, so the \"no adapter\" branch below must not warn again.\n adapterFailed = true;\n warn('WebGPU adapter request failed; falling back to WebGL2.', error);\n }\n if (!adapter && !adapterFailed) {\n if (requireWebGpu) {\n throw new Error('vlam: no WebGPU adapter was available and requireWebGpu is set.');\n }\n warn('No WebGPU adapter was available; falling back to WebGL2.');\n }\n if (adapter) {\n try {\n device = await adapter.requestDevice({\n // Every feature the adapter advertises. `core-features-and-limits`\n // is the one that matters: without it three treats the backend as\n // compatibility-mode and forces the sample count to 0.\n requiredFeatures: [...adapter.features],\n requiredLimits: recommendedWebGpuRequiredLimits(adapter),\n });\n } catch (error) {\n if (requireWebGpu) throw error;\n // The error object itself, not just a message: this is the value\n // three's `getFallback` swallows, and it distinguishes a fixable\n // failure (a GPU process out of memory) from an absent backend.\n warn('WebGPU device request failed; requesting limits without an owned device.', error);\n }\n }\n }\n }\n\n return new WebGPURenderer({\n antialias: true,\n ...rendererOptions,\n ...powerOptions,\n // Without a device, leave three to its own request - it fails the same way,\n // but the WebGL2 fallback behind it is still the right outcome.\n ...(device\n ? { device }\n : adapter\n ? { requiredLimits: recommendedWebGpuRequiredLimits(adapter) }\n : {}),\n });\n}\n","import type { Node } from 'three/webgpu';\n\n/**\n * Per-splat context a {@link SplatModifier} reads, evaluated in the vertex\n * stage. All fields are TSL nodes; see `docs/guide/effects-and-modifiers.md`.\n */\nexport interface SplatContext {\n // --- read-only inputs ---\n /** Pool splat index (stable identity for per-splat channels). */\n readonly index: Node<'int'>;\n /**\n * Splat center in mesh-local space (pre-displacement).\n *\n * Inside a `MergedSplatMesh` this is the splat's **placed** position\n * (`sourceMatrix · poolCenter`) - the placement is applied before the stack\n * runs, so effects stay anchored to the scene rather than travelling with a\n * moved source. Use {@link sourceCenter} for the pre-placement position.\n */\n readonly localCenter: Node<'vec3'>;\n /**\n * The splat's position in its own source's data frame. Identical to\n * {@link localCenter} on a plain mesh; inside a `MergedSplatMesh` it is the\n * **pre-placement** position, for effects that should travel *with* a moved\n * source instead of staying put in the scene.\n */\n readonly sourceCenter: Node<'vec3'>;\n /**\n * Linear transform from the source data frame to mesh-local space. This is\n * identity on a plain mesh; inside a {@link MergedSplatMesh} it is the linear part\n * of the source placement. Apply it to source-frame displacement vectors.\n * To express a source-frame rotation `R` as a mesh-local `rotation` output,\n * conjugate it: `sourceToLocal · R · sourceToLocal.inverse()`.\n */\n readonly sourceToLocal: Node<'mat3'>;\n /** `modelMatrix · localCenter` (world space). */\n readonly worldCenter: Node<'vec3'>;\n /** `modelViewMatrix · localCenter` (view space). */\n readonly viewCenter: Node<'vec3'>;\n /** Camera position in mesh-local space. */\n readonly cameraLocal: Node<'vec3'>;\n /** Color + opacity after SH - the stack's input color. */\n readonly baseColor: Node<'vec4'>;\n /** Approximate surface normal (least-variance axis of the splat's\n * covariance), mesh-local, oriented toward the camera. Built lazily -\n * only modifiers that read it pay for the eigen approximation. */\n readonly normal: Node<'vec3'>;\n /**\n * Reads a per-splat channel by name (see `SplatMesh.defineChannel` /\n * `writeChannel`). Float channels return their stored value; byte channels\n * return the value normalized to `[0, 1]`. Reading an undefined channel is\n * a build-time error (a missing mask is a bug, not a silent zero). The read\n * is memoized per name, so referencing the same channel twice costs one\n * texture fetch.\n */\n channel(name: string): Node<'float'>;\n\n // --- running transform (threaded through the fold) ---\n /** rgb + opacity. */\n readonly color: Node<'vec4'>;\n /** Local-space center displacement. */\n readonly offset: Node<'vec3'>;\n /** Uniform scale multiplier. */\n readonly scale: Node<'float'>;\n /** Rigid rotation. */\n readonly rotation: Node<'mat3'>;\n /** Visibility. */\n readonly visible: Node<'bool'>;\n}\n\n/** The transform fields a modifier changes; omitted fields pass through. */\nexport interface SplatOutputs {\n color?: Node<'vec4'>;\n offset?: Node<'vec3'>;\n scale?: Node<'float'>;\n rotation?: Node<'mat3'>;\n visible?: Node<'bool'>;\n /** Blend `Σ` toward σ²·I (0 = keep, 1 = full collapse). Screen λ equalized when > 0. */\n isotropicCovarianceMix?: Node<'float'>;\n /** Multiplier on max(n·Σ·n, ε); defaults to 0.35² when omitted. */\n isotropicVarianceScale?: Node<'float'>;\n /**\n * Optional isotropic screen-space σ radius in pixels. When set (> 0), caps\n * projected λ for zoom-stable dots. Omit for Spark-style world-space points\n * (size grows mildly on zoom-in).\n */\n isotropicScreenRadiusPx?: Node<'float'>;\n}\n\n/**\n * A splat effect hook. Reads the per-splat {@link SplatContext} and returns\n * the transform fields it wants to change. Pure TSL, so it compiles to WGSL\n * and GLSL and runs on both backends. Modifiers compose by folding over\n * `SplatMesh.modifiers` in order - each sees the running result of the ones\n * before it. Adding/removing/reordering modifiers recompiles the material;\n * changing a modifier's own uniforms or storage buffers never does.\n *\n * Constraints (see `docs/guide/effects-and-modifiers.md`): covariance is pre-baked,\n * so `scale` is uniform-only and `rotation` rigid-only; displaced splats\n * keep their pre-displacement depth-sort order.\n */\nexport type SplatModifier = (context: SplatContext) => SplatOutputs;\n\n/** Anything with a `modifiers` list - {@link ModifierSlots.apply} target. */\nexport interface ModifierStackTarget {\n modifiers: readonly SplatModifier[];\n}\n\n/**\n * Named, ordered modifier slots for hosts that stack several effects (reveal,\n * SDF, lighting, fog, opacity, …) on one mesh. The slot **order is fixed at\n * construction** - it defines the fold order of the compiled stack - while\n * each slot's occupant can change at runtime.\n *\n * The rebuild vs uniform-update contract (see\n * `docs/guide/effects-and-modifiers.md`, \"Multi-slot stacks\"):\n *\n * - An **empty slot costs nothing**: it is omitted from the compacted list\n * entirely, never compiled as a passthrough. All slots empty ⇒ the mesh\n * gets an empty list ⇒ the unhooked graph (and the unified renderer's\n * zero-modifier gather fast path stays eligible - except for a `MergedSplatMesh`,\n * which `UnifiedSplatMesh` does not accept as a source at all).\n * - {@link apply} hands the mesh a **compacted array whose reference is\n * stable** while the occupancy is unchanged, so re-applying after\n * uniform-only changes is a guaranteed no-op on the mesh's identity diff -\n * no material rebuild, no `graphRevision` bump.\n * - Filling, clearing, or replacing a slot with a *different* function is a\n * structural change: the next {@link apply} rebuilds the material once.\n * Setting a slot to the function it already holds is a no-op.\n */\nexport class ModifierSlots {\n private readonly order: readonly string[];\n private readonly occupants = new Map<string, SplatModifier>();\n /** Compacted stack in slot order; rebuilt only on structural change so the\n * array reference itself is stable across uniform-only frames. */\n private compacted: readonly SplatModifier[] = [];\n\n /** @param order Slot names, in fold order. Must be non-empty and unique. */\n constructor(order: readonly string[]) {\n if (order.length === 0) {\n throw new Error('ModifierSlots: the slot order must name at least one slot.');\n }\n if (new Set(order).size !== order.length) {\n throw new Error(`ModifierSlots: duplicate slot name in order [${order.join(', ')}].`);\n }\n this.order = [...order];\n }\n\n /** The declared slot names, in fold order. */\n get slotNames(): readonly string[] {\n return this.order;\n }\n\n /**\n * Fills or clears a slot (`null` clears). Returns `true` when the change is\n * structural - i.e. the next {@link apply} will trigger a material rebuild -\n * and `false` when it was a no-op (same function, or clearing an already\n * empty slot). Unknown slot names throw: a typo must not silently drop an\n * effect.\n */\n set(name: string, modifier: SplatModifier | null): boolean {\n if (!this.order.includes(name)) {\n throw new Error(\n `ModifierSlots: unknown slot \"${name}\" (declared: ${this.order.join(', ')}).`,\n );\n }\n const current = this.occupants.get(name) ?? null;\n if (current === modifier) return false;\n if (modifier === null) this.occupants.delete(name);\n else this.occupants.set(name, modifier);\n const next: SplatModifier[] = [];\n for (const slot of this.order) {\n const occupant = this.occupants.get(slot);\n if (occupant) next.push(occupant);\n }\n this.compacted = next;\n return true;\n }\n\n /** The slot's current occupant, or `null` when empty. */\n get(name: string): SplatModifier | null {\n return this.occupants.get(name) ?? null;\n }\n\n /** Sugar for `set(name, null)`; returns whether the slot was occupied. */\n clear(name: string): boolean {\n return this.set(name, null);\n }\n\n /**\n * The compacted stack: occupied slots only, in slot order. The reference is\n * stable while occupancy is unchanged, so it is safe to assign every frame.\n */\n get modifiers(): readonly SplatModifier[] {\n return this.compacted;\n }\n\n /**\n * Assigns the compacted stack to `target.modifiers` (a `SplatMesh` or\n * anything with the same setter). The mesh's identity diff makes this free\n * when nothing structural changed, so calling it after every `set` - or\n * even every frame - is fine.\n */\n apply(target: ModifierStackTarget): void {\n target.modifiers = this.compacted;\n }\n}\n"],"names":["MAX_MERGED_SPLAT_SOURCES","SOURCE_ID_CHANNEL","SourceMatrixArray","maxSources","__publicField","_","i","column","THREE","base","uniformArray","id","matrix","e","c","worldBoundsOf","sources","out","worldCenter","child","source","matrixMaxStretch","m","x0","y0","z0","x1","y1","z1","x2","y2","z2","g00","g11","g22","g01","g02","g12","MergedSplatMesh","SplatMesh","options","sourceIdTexture","count","data","placement","range","ids","correction","yUpTransformForFormat","box","sphere","record","_a","live","bounds","placed","ambientGpu","nav","gpu","createSplatRenderer","powerPreference","requireWebGpu","rendererOptions","powerOptions","webGpuPowerPreferenceOptions","adapter","device","entry","adapterFailed","error","warn","recommendedWebGpuRequiredLimits","WebGPURenderer","ModifierSlots","order","name","modifier","next","slot","occupant","target"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","sources":["../src/lib/core/source-transform.ts","../src/lib/core/merged-splat-mesh.ts","../src/lib/core/create-splat-renderer.ts","../src/lib/core/splat-modifier.ts"],"sourcesContent":["/**\n * Per-source world transforms for a unified {@link MergedSplatMesh} pool.\n *\n * Multiple splat clouds are concatenated into one pool in their own local\n * space; each cloud (\"source\") then carries a world matrix applied **in the\n * shader** - never baked into the pool data. That is what lets a source move\n * every frame at full rate: only this small uniform array changes, and neither\n * the pool textures nor the sort keys are rewritten.\n *\n * The same matrix array node is shared by two consumers so they always agree,\n * both through `sourceWorldTransform` in `splat-mesh-material.ts`:\n * - the material graph, which places a splat's center and covariance **before**\n * the modifier stack runs, so effects see the splat where it visually is, and\n * - the depth sorter, which transforms each splat's center to world space\n * before computing its view-space depth (see `ComputeSorter`).\n *\n * Internal. Nothing here is exported from `index.ts`.\n */\nimport * as THREE from 'three/webgpu';\nimport { uniformArray } from 'three/tsl';\n\n/**\n * Maximum number of sources a unified pool can hold. Sized generously - the\n * uniform array costs `4 · MAX_MERGED_SPLAT_SOURCES` vec4s (16 KB at 256), well under the\n * uniform-buffer limit - while bounding the per-splat index range.\n */\nexport const MAX_MERGED_SPLAT_SOURCES = 256;\n\n/** The channel both the material graph and the sorter read as a source id. */\nexport const SOURCE_ID_CHANNEL = 'sourceId';\n\n/**\n * A live, shader-visible array of source world matrices, stored as four vec4\n * **columns** per source (three.js `Matrix4.elements` are column-major, so a\n * column maps to `elements[k*4 .. k*4+3]`). Mutating an entry re-uploads with\n * the frame's uniforms - no material or pipeline rebuild.\n */\nexport class SourceMatrixArray {\n /** `4 · MAX_MERGED_SPLAT_SOURCES` vec4 columns; the TSL node both consumers index. */\n readonly node: ReturnType<typeof uniformArray>;\n /** CPU mirror of the same column-major matrices, for the WebGL2 sorter. */\n readonly matrices: Float32Array;\n private readonly columns: THREE.Vector4[];\n\n constructor(maxSources = MAX_MERGED_SPLAT_SOURCES) {\n this.matrices = new Float32Array(16 * maxSources);\n this.columns = Array.from({ length: 4 * maxSources }, (_, i) => {\n // Initialize every slot to the identity matrix's columns.\n const column = i % 4;\n return new THREE.Vector4(\n column === 0 ? 1 : 0,\n column === 1 ? 1 : 0,\n column === 2 ? 1 : 0,\n column === 3 ? 1 : 0,\n );\n });\n for (let i = 0; i < maxSources; i++) {\n const base = i * 16;\n this.matrices[base] = 1;\n this.matrices[base + 5] = 1;\n this.matrices[base + 10] = 1;\n this.matrices[base + 15] = 1;\n }\n this.node = uniformArray(this.columns, 'vec4');\n }\n\n /** Writes source `id`'s world matrix (column-major columns copied verbatim). */\n set(id: number, matrix: THREE.Matrix4): void {\n const e = matrix.elements;\n const base = id * 4;\n for (let c = 0; c < 4; c++) {\n (this.columns[base + c] as THREE.Vector4).set(\n e[c * 4 + 0] as number,\n e[c * 4 + 1] as number,\n e[c * 4 + 2] as number,\n e[c * 4 + 3] as number,\n );\n this.matrices[id * 16 + c * 4 + 0] = e[c * 4 + 0] as number;\n this.matrices[id * 16 + c * 4 + 1] = e[c * 4 + 1] as number;\n this.matrices[id * 16 + c * 4 + 2] = e[c * 4 + 2] as number;\n this.matrices[id * 16 + c * 4 + 3] = e[c * 4 + 3] as number;\n }\n }\n}\n\n/** A source's local bounding sphere plus its world matrix, for pool bounds. */\nexport interface SourceBounds {\n readonly center: THREE.Vector3;\n readonly radius: number;\n readonly matrix: THREE.Matrix4;\n}\n\n/**\n * The world-space bounding sphere enclosing every source, for the sorter's\n * depth quantization range. Each source sphere is transformed by its matrix\n * (radius scaled by a conservative maximum linear stretch) and the results\n * unioned.\n */\nexport function worldBoundsOf(sources: readonly SourceBounds[], out: THREE.Sphere): THREE.Sphere {\n out.makeEmpty();\n const worldCenter = new THREE.Vector3();\n const child = new THREE.Sphere();\n for (const source of sources) {\n worldCenter.copy(source.center).applyMatrix4(source.matrix);\n child.set(worldCenter, source.radius * matrixMaxStretch(source.matrix));\n out.union(child);\n }\n return out;\n}\n\n/**\n * A safe upper bound on the largest singular value of a matrix's linear part.\n *\n * This is `sqrt(||AᵀA||∞)`: exact for rotation + per-axis scale, and still\n * conservative when hierarchy composition introduces shear. The latter is\n * essential because a maximum column length can under-bound a sheared sphere.\n */\nfunction matrixMaxStretch(m: THREE.Matrix4): number {\n const e = m.elements;\n const x0 = e[0],\n y0 = e[1],\n z0 = e[2];\n const x1 = e[4],\n y1 = e[5],\n z1 = e[6];\n const x2 = e[8],\n y2 = e[9],\n z2 = e[10];\n const g00 = x0 * x0 + y0 * y0 + z0 * z0;\n const g11 = x1 * x1 + y1 * y1 + z1 * z1;\n const g22 = x2 * x2 + y2 * y2 + z2 * z2;\n const g01 = x0 * x1 + y0 * y1 + z0 * z1;\n const g02 = x0 * x2 + y0 * y2 + z0 * z2;\n const g12 = x1 * x2 + y1 * y2 + z1 * z2;\n return Math.sqrt(\n Math.max(\n g00 + Math.abs(g01) + Math.abs(g02),\n g11 + Math.abs(g01) + Math.abs(g12),\n g22 + Math.abs(g02) + Math.abs(g12),\n ),\n );\n}\n","/**\n * A scene of several independently-posed Gaussian splat clouds that all sort\n * and blend as one.\n *\n * The problem it solves: two separate {@link SplatMesh} instances are two\n * transparent draw calls, each depth-sorted only within itself, so where the\n * clouds overlap one is painted wholesale in front of the other (\"pasted over\")\n * - the same artifact PlayCanvas's unified rendering and Spark's accumulator\n * exist to fix. `MergedSplatMesh` concatenates every cloud into **one pool** and\n * runs **one global depth sort**, so splats from different clouds interleave\n * correctly, from any angle.\n *\n * Each cloud (\"source\") keeps its own world transform, applied in the shader\n * (never baked into the pool): the sorter transforms each splat's center to\n * world space before measuring depth, and the material places each splat's\n * center and covariance by its source's matrix. So {@link setSourceTransform}\n * is a handful of uniform writes - a source can be dragged or animated every\n * frame at full rate, at any splat count, with no data re-upload.\n *\n * The placement runs **before** the modifier stack, so an application effect sees each\n * splat where it visually is: an SDF light shape spanning two sources paints\n * one continuous shape, and a reveal sweeps them together. A modifier that\n * wants to travel *with* a moved source reads `ctx.sourceCenter` instead of\n * `ctx.localCenter`. See `docs/guide/effects-and-modifiers.md`.\n *\n * On WebGPU the world-depth sort runs in a compute pass; on the WebGL2\n * fallback the worker applies the same source matrices on the CPU before\n * sorting. Both backends therefore inter-sort sources correctly.\n *\n * Limitations, all inherited from the dynamic pool: sources may not carry\n * palette (SOG `shN`) view-dependent color - per-file palettes cannot be merged\n * - but DC color and per-splat (LCC) SH are fine; all sources share one\n * antialias setting (from the mesh options).\n *\n * Sources are fully decoded {@link SplatData} copied once into the pool.\n * {@link StreamedSplatMesh} swaps its resident LOD cut every frame, so it cannot\n * be a merged source. Compose streamed meshes with fully loaded meshes through\n * {@link UnifiedSplatMesh} instead.\n */\nimport * as THREE from 'three/webgpu';\nimport { SplatMesh, type SplatMeshOptions, type SplatRange } from './splat-mesh';\nimport type { SplatData } from './splat-data';\nimport { yUpTransformForFormat, type SplatOrientation } from './orientation';\nimport {\n MAX_MERGED_SPLAT_SOURCES,\n SOURCE_ID_CHANNEL,\n SourceMatrixArray,\n worldBoundsOf,\n type SourceBounds,\n} from './source-transform';\n\n/** Construction options for a {@link MergedSplatMesh}. */\nexport interface MergedSplatMeshOptions extends SplatMeshOptions {\n /** Total pool size in splats - must cover the sum of every source added. */\n capacity: number;\n /** Maximum number of sources (default {@link MAX_MERGED_SPLAT_SOURCES}). */\n maxSources?: number;\n}\n\n/** Per-source placement options. */\nexport interface MergedSplatSourceOptions {\n /**\n * Y-up normalization for this source's data frame, like {@link SplatMesh}'s\n * `orientation`. `'y-up'` (default) applies the format's stand-up correction\n * before the placement matrix; `'source'` places the raw data frame.\n */\n orientation?: SplatOrientation;\n}\n\n/** Internal per-source record. */\ninterface SourceRecord extends SourceBounds {\n /** The pool range this source's splats occupy. */\n range: SplatRange;\n /** Data-frame → world correction applied before the placement matrix. */\n correction: THREE.Matrix4 | null;\n /** Live placement (world = placement · correction); kept for re-placement. */\n placement: THREE.Matrix4;\n /** The source's own data-frame bounds, for {@link MergedSplatMesh.computeSplatBounds}. */\n box: THREE.Box3;\n}\n\n/**\n * One pool, one global sort, many independently-posed splat sources - see the\n * module overview above for the artifact this exists to remove.\n *\n * @experimental May change in a minor release.\n */\nexport class MergedSplatMesh extends SplatMesh {\n private readonly matrices: SourceMatrixArray;\n private readonly maxSources: number;\n /** Indexed by source id; holes are left by {@link removeSource}. */\n private readonly sources: (SourceRecord | undefined)[] = [];\n private nextId = 0;\n\n constructor(options: MergedSplatMeshOptions) {\n const maxSources = options.maxSources ?? MAX_MERGED_SPLAT_SOURCES;\n super({ capacity: options.capacity }, options);\n this.matrices = new SourceMatrixArray(maxSources);\n this.maxSources = maxSources;\n\n // One float per pool slot naming the splat's source; read by both the\n // material graph and the world-depth sorter.\n this.defineChannel(SOURCE_ID_CHANNEL, { type: 'float' });\n const sourceIdTexture = this.channelTexture(SOURCE_ID_CHANNEL);\n if (!sourceIdTexture) throw new Error('MergedSplatMesh: sourceId channel texture missing.');\n this.perSourceSort = {\n sourceIdTexture,\n columns: this.matrices.node,\n sourceIds: this.channelBacking(SOURCE_ID_CHANNEL),\n matrices: this.matrices.matrices,\n };\n // The base constructor already built a graph, before `perSourceSort` was\n // set - rebuild so the material actually places its splats. Without this\n // every source renders at its pool-local position.\n this.rebuildGraph();\n }\n\n /** Number of live sources. */\n get sourceCount(): number {\n let count = 0;\n for (const source of this.sources) if (source) count++;\n return count;\n }\n\n /**\n * Adds a cloud at a world placement and returns its id (for\n * {@link setSourceTransform} / {@link removeSource}). The data is copied into\n * the shared pool in its own local frame; the placement is applied live in\n * the shader.\n *\n * @throws if the pool cannot fit the cloud, or the source limit is reached.\n */\n addSource(\n data: SplatData,\n placement: THREE.Matrix4 = new THREE.Matrix4(),\n options: MergedSplatSourceOptions = {},\n ): number {\n const id = this.nextId;\n if (id >= this.maxSources) {\n throw new Error(`MergedSplatMesh: source limit reached (${this.maxSources}).`);\n }\n const range = this.appendRange(data); // local space; transform is a uniform\n this.nextId++;\n\n const ids = new Float32Array(data.count);\n ids.fill(id);\n this.writeChannel(range, SOURCE_ID_CHANNEL, ids);\n\n const orientation = options.orientation ?? 'y-up';\n const correction = orientation === 'y-up' ? yUpTransformForFormat(data.format) : null;\n const box = new THREE.Box3().setFromArray(data.positions);\n const sphere = box.getBoundingSphere(new THREE.Sphere());\n const record: SourceRecord = {\n range,\n correction,\n placement: placement.clone(),\n box,\n center: sphere.center,\n radius: sphere.radius,\n matrix: new THREE.Matrix4(),\n };\n this.sources[id] = record;\n this.applyPlacement(record);\n return id;\n }\n\n /**\n * Re-places a source. Cheap by design: only the shared matrix uniform and the\n * sort bound change, so it is safe to call every frame while dragging.\n *\n * Bad-id semantics across the triad: this method **throws** (a write to a\n * dead source is a caller bug that would otherwise vanish silently),\n * {@link getSourceTransform} returns `undefined` (querying is how you ask\n * whether a source is live), and {@link removeSource} returns `false`\n * (removal is idempotent).\n *\n * @throws if `id` is not a live source.\n */\n setSourceTransform(id: number, placement: THREE.Matrix4): void {\n const record = this.sources[id];\n if (!record) throw new Error(`MergedSplatMesh: no live source ${id}.`);\n record.placement.copy(placement);\n this.applyPlacement(record);\n }\n\n /**\n * The current world matrix of a source (a copy), or `undefined` if `id` is\n * not a live source - see {@link setSourceTransform} for the bad-id triad.\n */\n getSourceTransform(id: number): THREE.Matrix4 | undefined {\n return this.sources[id]?.matrix.clone();\n }\n\n /**\n * Removes a source and frees its pool range. The id is not reused (later ids\n * keep their matrix slots), so long-lived churn should prefer reusing a scene\n * over unbounded add/remove.\n *\n * @returns `true` when a live source was removed, `false` when `id` was not\n * live (idempotent - see {@link setSourceTransform} for the bad-id triad).\n */\n removeSource(id: number): boolean {\n const record = this.sources[id];\n if (!record) return false;\n this.removeRange(record.range);\n this.sources[id] = undefined;\n this.invalidateSort();\n return true;\n }\n\n /** Recomputes a source's world matrix (placement · correction) and republishes it. */\n private applyPlacement(record: SourceRecord): void {\n if (record.correction) record.matrix.multiplyMatrices(record.placement, record.correction);\n else record.matrix.copy(record.placement);\n const id = this.sources.indexOf(record);\n this.matrices.set(id, record.matrix);\n this.invalidateSort();\n }\n\n /** Sort depth is quantized over the world span of every source, not the pool's local box. */\n protected override refreshSortBounds(): void {\n if (!this.boundsDirty) return;\n const live: SourceBounds[] = [];\n for (const source of this.sources) if (source) live.push(source);\n worldBoundsOf(live, this.boundingSphereLocal);\n this.boundsDirty = false;\n }\n\n /**\n * Mesh-local bounds spanning every source at its **current placement** -\n * the pool's own box would be the union of the sources' data frames, which\n * is not where any of them is drawn. Each source's data-frame box is\n * transformed by its matrix, so a rotated source contributes the (standard,\n * conservative) axis-aligned box of its rotated bounds.\n *\n * Hosts use this to place effects, so it has to agree with what\n * `ctx.localCenter` reports; it tracks {@link setSourceTransform}.\n */\n override computeSplatBounds(): THREE.Box3 {\n const bounds = new THREE.Box3().makeEmpty();\n const placed = new THREE.Box3();\n for (const source of this.sources) {\n if (!source) continue;\n bounds.union(placed.copy(source.box).applyMatrix4(source.matrix));\n }\n return bounds;\n }\n}\n","/**\n * One-call WebGPU renderer construction for VLAM! scenes.\n *\n * Every host needs the same four things right - adapter, device, raised limits\n * and the platform-dependent `powerPreference` - and getting any of them wrong\n * fails *late*: past ~8.4 M unified splats, or with MSAA silently dropped.\n * {@link createSplatRenderer} is that block, once.\n *\n * The primitives it composes ({@link recommendedWebGpuRequiredLimits},\n * {@link webGpuPowerPreferenceOptions}) stay exported for hosts that own device\n * creation themselves - an XR session sharing a device, say.\n */\nimport { WebGPURenderer, type WebGPURendererParameters } from 'three/webgpu';\n\nimport { warn } from './logging';\nimport {\n recommendedWebGpuRequiredLimits,\n webGpuPowerPreferenceOptions,\n type WebGpuPowerPreference,\n} from './webgpu-limits';\n\n/*\n * The WebGPU surface is declared structurally, not via `navigator.gpu`.\n * `tsconfig.lib.json` compiles `src/lib` alone (`rootDir`, and an `include`\n * listing only the published entry points), so the ambient\n * `interface Navigator { gpu?: ... }` the viewer declares is not in scope for\n * the published build. A module here that reaches for `navigator.gpu`'s type\n * passes `npm run typecheck` and fails `npm run build:lib`.\n */\n\n/** The device handle `createSplatRenderer` hands to `WebGPURenderer`. */\nexport interface SplatRendererGpuDevice {\n readonly features: ReadonlySet<string>;\n}\n\n/** The adapter surface `createSplatRenderer` reads limits and features from. */\nexport interface SplatRendererGpuAdapter {\n readonly features: ReadonlySet<string>;\n readonly limits: {\n readonly maxStorageBufferBindingSize: number;\n readonly maxBufferSize: number;\n readonly maxTextureDimension2D?: number;\n };\n requestDevice(descriptor?: {\n requiredFeatures?: readonly string[];\n requiredLimits?: Record<string, number>;\n }): Promise<SplatRendererGpuDevice>;\n}\n\n/** The `navigator.gpu` surface `createSplatRenderer` probes. */\nexport interface SplatRendererGpu {\n requestAdapter(options?: {\n powerPreference?: WebGpuPowerPreference;\n }): Promise<SplatRendererGpuAdapter | null>;\n}\n\n/**\n * Options for {@link createSplatRenderer}.\n *\n * Everything `THREE.WebGPURenderer` accepts passes straight through -\n * `antialias`, `forceWebGL`, `trackTimestamp`, `canvas`, `alpha`, `samples`,\n * `getFallback`, … - except the three members the helper owns (`device`,\n * `requiredLimits`, and `powerPreference`, which is widened here to allow\n * `null`).\n */\nexport interface CreateSplatRendererOptions extends Omit<\n WebGPURendererParameters,\n 'device' | 'requiredLimits' | 'powerPreference'\n> {\n /**\n * Adapter/renderer power hint. Omitted automatically on Windows, where Chrome\n * ignores it and warns ([crbug.com/369219127](https://crbug.com/369219127)).\n * Pass `null` to never send it.\n *\n * @defaultValue `'high-performance'`\n */\n powerPreference?: WebGpuPowerPreference | null;\n /**\n * Throw instead of degrading to three's WebGL2 backend. Adapter and device\n * failures rethrow their original error, unwrapped.\n *\n * @defaultValue `false`\n */\n requireWebGpu?: boolean;\n /**\n * The WebGPU entry point to probe. Defaults to `navigator.gpu`; pass `null`\n * to skip the probe entirely (tests, non-DOM hosts).\n */\n gpu?: SplatRendererGpu | null;\n}\n\nfunction ambientGpu(): SplatRendererGpu | null {\n const nav = typeof navigator !== 'undefined' ? (navigator as { gpu?: unknown }) : undefined;\n const gpu = nav?.gpu;\n return gpu ? (gpu as SplatRendererGpu) : null;\n}\n\n/**\n * Creates a `THREE.WebGPURenderer` configured for splat rendering: the\n * adapter's advertised buffer/texture maxima as `requiredLimits`, every adapter\n * feature as `requiredFeatures`, and `powerPreference` only where the platform\n * honours it.\n *\n * Both raised values matter late rather than loudly. Without the limits, large\n * unified or streamed scenes throw past ~8.4 M splats. Without\n * `core-features-and-limits` among the requested features, three treats the\n * backend as compatibility-mode and **silently drops MSAA** - which is why the\n * device is requested here rather than left to `WebGPUBackend.init`. Owning the\n * request also keeps the real failure: three's `getFallback` hook logs a flat\n * \"WebGPU is not available\" and drops the cause.\n *\n * Degrades to three's WebGL2 backend exactly as an unconfigured\n * `new THREE.WebGPURenderer()` would, warning through\n * {@link setVlamLogHandler} when the cause was a fixable failure rather than a\n * browser without WebGPU. Pass `requireWebGpu: true` to throw instead.\n *\n * Does **not** call `setSize`, `renderer.init()`, or append the canvas - those\n * stay in host code - and does not touch `outputColorSpace`, whose three.js\n * default (sRGB) is already what the splat shader expects.\n *\n * @example\n * const renderer = await createSplatRenderer();\n * renderer.setSize(innerWidth, innerHeight);\n * document.body.appendChild(renderer.domElement);\n *\n * @throws {TypeError} if `requireWebGpu` and `forceWebGL` are both set.\n */\nexport async function createSplatRenderer(\n options: CreateSplatRendererOptions = {},\n): Promise<WebGPURenderer> {\n const {\n powerPreference = 'high-performance',\n requireWebGpu = false,\n gpu,\n ...rendererOptions\n } = options;\n const powerOptions =\n powerPreference === null ? {} : webGpuPowerPreferenceOptions(powerPreference);\n\n let adapter: SplatRendererGpuAdapter | null = null;\n let device: SplatRendererGpuDevice | null = null;\n\n if (rendererOptions.forceWebGL === true) {\n if (requireWebGpu) {\n throw new TypeError(\n 'vlam: createSplatRenderer cannot honour both requireWebGpu and forceWebGL.',\n );\n }\n } else {\n const entry = gpu === undefined ? ambientGpu() : gpu;\n if (!entry) {\n // Not a warning: a browser without WebGPU is not a fixable misconfiguration.\n if (requireWebGpu) {\n throw new Error('vlam: WebGPU is unavailable (no navigator.gpu) and requireWebGpu is set.');\n }\n } else {\n let adapterFailed = false;\n try {\n adapter = await entry.requestAdapter({ ...powerOptions });\n } catch (error) {\n if (requireWebGpu) throw error;\n // One warning per failure: an adapter that threw has already explained\n // itself, so the \"no adapter\" branch below must not warn again.\n adapterFailed = true;\n warn('WebGPU adapter request failed; falling back to WebGL2.', error);\n }\n if (!adapter && !adapterFailed) {\n if (requireWebGpu) {\n throw new Error('vlam: no WebGPU adapter was available and requireWebGpu is set.');\n }\n warn('No WebGPU adapter was available; falling back to WebGL2.');\n }\n if (adapter) {\n try {\n device = await adapter.requestDevice({\n // Every feature the adapter advertises. `core-features-and-limits`\n // is the one that matters: without it three treats the backend as\n // compatibility-mode and forces the sample count to 0.\n requiredFeatures: [...adapter.features],\n requiredLimits: recommendedWebGpuRequiredLimits(adapter),\n });\n } catch (error) {\n if (requireWebGpu) throw error;\n // The error object itself, not just a message: this is the value\n // three's `getFallback` swallows, and it distinguishes a fixable\n // failure (a GPU process out of memory) from an absent backend.\n warn('WebGPU device request failed; requesting limits without an owned device.', error);\n }\n }\n }\n }\n\n return new WebGPURenderer({\n antialias: true,\n ...rendererOptions,\n ...powerOptions,\n // Without a device, leave three to its own request - it fails the same way,\n // but the WebGL2 fallback behind it is still the right outcome.\n ...(device\n ? { device }\n : adapter\n ? { requiredLimits: recommendedWebGpuRequiredLimits(adapter) }\n : {}),\n });\n}\n","import type { Node } from 'three/webgpu';\n\n/**\n * Per-splat context a {@link SplatModifier} reads, evaluated in the vertex\n * stage. All fields are TSL nodes; see `docs/guide/effects-and-modifiers.md`.\n */\nexport interface SplatContext {\n // --- read-only inputs ---\n /** Pool splat index (stable identity for per-splat channels). */\n readonly index: Node<'int'>;\n /**\n * Splat center in mesh-local space (pre-displacement).\n *\n * Inside a `MergedSplatMesh` this is the splat's **placed** position\n * (`sourceMatrix · poolCenter`) - the placement is applied before the stack\n * runs, so effects stay anchored to the scene rather than travelling with a\n * moved source. Use {@link sourceCenter} for the pre-placement position.\n */\n readonly localCenter: Node<'vec3'>;\n /**\n * The splat's position in its own source's data frame. Identical to\n * {@link localCenter} on a plain mesh; inside a `MergedSplatMesh` it is the\n * **pre-placement** position, for effects that should travel *with* a moved\n * source instead of staying put in the scene.\n */\n readonly sourceCenter: Node<'vec3'>;\n /**\n * Linear transform from the source data frame to mesh-local space. This is\n * identity on a plain mesh; inside a {@link MergedSplatMesh} it is the linear part\n * of the source placement. Apply it to source-frame displacement vectors.\n * To express a source-frame rotation `R` as a mesh-local `rotation` output,\n * conjugate it: `sourceToLocal · R · sourceToLocal.inverse()`.\n */\n readonly sourceToLocal: Node<'mat3'>;\n /** `modelMatrix · localCenter` (world space). */\n readonly worldCenter: Node<'vec3'>;\n /** `modelViewMatrix · localCenter` (view space). */\n readonly viewCenter: Node<'vec3'>;\n /** Camera position in mesh-local space. */\n readonly cameraLocal: Node<'vec3'>;\n /** Color + opacity after SH - the stack's input color. */\n readonly baseColor: Node<'vec4'>;\n /** Approximate surface normal (least-variance axis of the splat's\n * covariance), mesh-local, oriented toward the camera. Built lazily -\n * only modifiers that read it pay for the eigen approximation. */\n readonly normal: Node<'vec3'>;\n /**\n * Reads a per-splat channel by name (see `SplatMesh.defineChannel` /\n * `writeChannel`). Float channels return their stored value; byte channels\n * return the value normalized to `[0, 1]`. Reading an undefined channel is\n * a build-time error (a missing mask is a bug, not a silent zero). The read\n * is memoized per name, so referencing the same channel twice costs one\n * texture fetch.\n */\n channel(name: string): Node<'float'>;\n\n // --- running transform (threaded through the fold) ---\n /** rgb + opacity. */\n readonly color: Node<'vec4'>;\n /** Local-space center displacement. */\n readonly offset: Node<'vec3'>;\n /** Uniform scale multiplier. */\n readonly scale: Node<'float'>;\n /** Rigid rotation. */\n readonly rotation: Node<'mat3'>;\n /** Visibility. */\n readonly visible: Node<'bool'>;\n}\n\n/** The transform fields a modifier changes; omitted fields pass through. */\nexport interface SplatOutputs {\n color?: Node<'vec4'>;\n offset?: Node<'vec3'>;\n scale?: Node<'float'>;\n rotation?: Node<'mat3'>;\n visible?: Node<'bool'>;\n /** Blend `Σ` toward σ²·I (0 = keep, 1 = full collapse). Screen λ equalized when > 0. */\n isotropicCovarianceMix?: Node<'float'>;\n /** Multiplier on max(n·Σ·n, ε); defaults to 0.35² when omitted. */\n isotropicVarianceScale?: Node<'float'>;\n /**\n * Optional isotropic screen-space σ radius in pixels. When set (> 0), caps\n * projected λ for zoom-stable dots. Omit for Spark-style world-space points\n * (size grows mildly on zoom-in).\n */\n isotropicScreenRadiusPx?: Node<'float'>;\n}\n\n/**\n * A splat effect hook. Reads the per-splat {@link SplatContext} and returns\n * the transform fields it wants to change. Pure TSL, so it compiles to WGSL\n * and GLSL and runs on both backends. Modifiers compose by folding over\n * `SplatMesh.modifiers` in order - each sees the running result of the ones\n * before it. Adding/removing/reordering modifiers recompiles the material;\n * changing a modifier's own uniforms or storage buffers never does.\n *\n * Constraints (see `docs/guide/effects-and-modifiers.md`): covariance is pre-baked,\n * so `scale` is uniform-only and `rotation` rigid-only; displaced splats\n * keep their pre-displacement depth-sort order.\n */\nexport type SplatModifier = (context: SplatContext) => SplatOutputs;\n\n/** Anything with a `modifiers` list - {@link ModifierSlots.apply} target. */\nexport interface ModifierStackTarget {\n modifiers: readonly SplatModifier[];\n}\n\n/**\n * Named, ordered modifier slots for hosts that stack several effects (reveal,\n * SDF, lighting, fog, opacity, …) on one mesh. The slot **order is fixed at\n * construction** - it defines the fold order of the compiled stack - while\n * each slot's occupant can change at runtime.\n *\n * The rebuild vs uniform-update contract (see\n * `docs/guide/effects-and-modifiers.md`, \"Multi-slot stacks\"):\n *\n * - An **empty slot costs nothing**: it is omitted from the compacted list\n * entirely, never compiled as a passthrough. All slots empty ⇒ the mesh\n * gets an empty list ⇒ the unhooked graph (and the unified renderer's\n * zero-modifier gather fast path stays eligible - except for a `MergedSplatMesh`,\n * which `UnifiedSplatMesh` does not accept as a source at all).\n * - {@link apply} hands the mesh a **compacted array whose reference is\n * stable** while the occupancy is unchanged, so re-applying after\n * uniform-only changes is a guaranteed no-op on the mesh's identity diff -\n * no material rebuild, no `graphRevision` bump.\n * - Filling, clearing, or replacing a slot with a *different* function is a\n * structural change: the next {@link apply} rebuilds the material once.\n * Setting a slot to the function it already holds is a no-op.\n */\nexport class ModifierSlots {\n private readonly order: readonly string[];\n private readonly occupants = new Map<string, SplatModifier>();\n /** Compacted stack in slot order; rebuilt only on structural change so the\n * array reference itself is stable across uniform-only frames. */\n private compacted: readonly SplatModifier[] = [];\n\n /** @param order Slot names, in fold order. Must be non-empty and unique. */\n constructor(order: readonly string[]) {\n if (order.length === 0) {\n throw new Error('ModifierSlots: the slot order must name at least one slot.');\n }\n if (new Set(order).size !== order.length) {\n throw new Error(`ModifierSlots: duplicate slot name in order [${order.join(', ')}].`);\n }\n this.order = [...order];\n }\n\n /** The declared slot names, in fold order. */\n get slotNames(): readonly string[] {\n return this.order;\n }\n\n /**\n * Fills or clears a slot (`null` clears). Returns `true` when the change is\n * structural - i.e. the next {@link apply} will trigger a material rebuild -\n * and `false` when it was a no-op (same function, or clearing an already\n * empty slot). Unknown slot names throw: a typo must not silently drop an\n * effect.\n */\n set(name: string, modifier: SplatModifier | null): boolean {\n if (!this.order.includes(name)) {\n throw new Error(\n `ModifierSlots: unknown slot \"${name}\" (declared: ${this.order.join(', ')}).`,\n );\n }\n const current = this.occupants.get(name) ?? null;\n if (current === modifier) return false;\n if (modifier === null) this.occupants.delete(name);\n else this.occupants.set(name, modifier);\n const next: SplatModifier[] = [];\n for (const slot of this.order) {\n const occupant = this.occupants.get(slot);\n if (occupant) next.push(occupant);\n }\n this.compacted = next;\n return true;\n }\n\n /** The slot's current occupant, or `null` when empty. */\n get(name: string): SplatModifier | null {\n return this.occupants.get(name) ?? null;\n }\n\n /** Sugar for `set(name, null)`; returns whether the slot was occupied. */\n clear(name: string): boolean {\n return this.set(name, null);\n }\n\n /**\n * The compacted stack: occupied slots only, in slot order. The reference is\n * stable while occupancy is unchanged, so it is safe to assign every frame.\n */\n get modifiers(): readonly SplatModifier[] {\n return this.compacted;\n }\n\n /**\n * Assigns the compacted stack to `target.modifiers` (a `SplatMesh` or\n * anything with the same setter). The mesh's identity diff makes this free\n * when nothing structural changed, so calling it after every `set` - or\n * even every frame - is fine.\n */\n apply(target: ModifierStackTarget): void {\n target.modifiers = this.compacted;\n }\n}\n"],"names":["MAX_MERGED_SPLAT_SOURCES","SOURCE_ID_CHANNEL","SourceMatrixArray","maxSources","__publicField","_","i","column","THREE","base","uniformArray","id","matrix","e","c","worldBoundsOf","sources","out","worldCenter","child","source","matrixMaxStretch","m","x0","y0","z0","x1","y1","z1","x2","y2","z2","g00","g11","g22","g01","g02","g12","MergedSplatMesh","SplatMesh","options","sourceIdTexture","count","data","placement","range","ids","correction","yUpTransformForFormat","box","sphere","record","_a","live","bounds","placed","ambientGpu","nav","gpu","createSplatRenderer","powerPreference","requireWebGpu","rendererOptions","powerOptions","webGpuPowerPreferenceOptions","adapter","device","entry","adapterFailed","error","warn","recommendedWebGpuRequiredLimits","WebGPURenderer","ModifierSlots","order","name","modifier","next","slot","occupant","target"],"mappings":"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"}
@@ -1,6 +1,6 @@
1
- import type { SplatData } from './splat-data';
1
+ import type { SplatData } from '../core/splat-data';
2
2
  import type { RadChunkRangeRequest } from './load-worker-protocol';
3
- import type { LccChunkParams } from './formats/lcc/parse-lcc';
3
+ import type { LccChunkParams } from '../formats/lcc/parse-lcc';
4
4
  import { type SplatDataFileLoadOptions, type SplatFormat, type SplatInputOptions } from './loading';
5
5
  /**
6
6
  * Client for the persistent scene/chunk loading workers. Multiple requests
@@ -1,4 +1,4 @@
1
- import type { SplatData } from './splat-data';
1
+ import type { SplatData } from '../core/splat-data';
2
2
  import type { SplatFormat, SplatDataFileLoadOptions, SplatInputOptions } from './loading';
3
3
  export type { SplatDataFileLoadOptions };
4
4
  /** Options for {@link loadSplatData}. */
@@ -7,8 +7,8 @@
7
7
  * client ({@link ChunkLoader}) be typed without dragging worker source into
8
8
  * the shipped declarations.
9
9
  */
10
- import type { LccChunkParams } from './formats/lcc/parse-lcc';
11
- import type { RadShCodebook, SplatData } from './splat-data';
10
+ import type { LccChunkParams } from '../formats/lcc/parse-lcc';
11
+ import type { RadShCodebook, SplatData } from '../core/splat-data';
12
12
  import type { ChunkFileFormat, SerializedSplatLoadError, SplatRequestOptions } from './loading';
13
13
  /** Where a load's bytes come from. `File` crosses the worker boundary by
14
14
  * structured clone, so a local file is read (and decoded) in the worker. */
@@ -1 +1 @@
1
- {"version":3,"file":"loaders.js","sources":["../src/lib/load-splat-data.ts"],"sourcesContent":["import type { SplatData } from './splat-data';\nimport { ChunkLoader } from './chunk-loader';\nimport type { SplatFormat, SplatDataFileLoadOptions, SplatInputOptions } from './loading';\n\nexport type { SplatDataFileLoadOptions };\n\n/** Options for {@link loadSplatData}. */\nexport interface SplatDataLoadOptions extends SplatInputOptions {\n /** Parser selection; URL extension detection is used only for `auto`. */\n format?: SplatFormat;\n}\n\n/**\n * Fetches and decodes a splat scene (`.sog`, `.ply`, `.spz`, `.splat` or\n * `.ksplat`) in a Web Worker, so\n * even multi-million-splat decodes never block the main thread. The\n * decoded arrays are transferred back, not copied.\n *\n * Whole-file convenience over {@link ChunkLoader}; use `ChunkLoader`\n * directly when loading many chunks or when cancellation is needed.\n *\n * @param input - Capture URL; its extension selects the parser unless an\n * explicit `format` option is provided.\n * @returns The decoded splat data, ready for {@link SplatMesh}.\n * @throws Rejects only with `SplatLoadError` (resolve, fetch or decode\n * failures - `error.phase` says which) or a `DOMException` named `AbortError`\n * when `options.signal` fires.\n *\n * @example\n * import { SplatMesh } from '@voluma/vlam';\n * import { loadSplatData } from '@voluma/vlam/loaders';\n * const splats = new SplatMesh(await loadSplatData('/scene.sog'));\n */\nexport async function loadSplatData(\n input: string | URL,\n options: SplatDataLoadOptions = {},\n): Promise<SplatData> {\n const loader = new ChunkLoader();\n try {\n return await loader.load(input instanceof URL ? input.href : input, options);\n } finally {\n loader.dispose();\n }\n}\n\n/**\n * Decodes a local splat file - from a `<input type=\"file\">` or a drag-and-drop\n * - in a Web Worker, without uploading it anywhere. The file's bytes are read\n * worker-side and the decoded arrays are transferred back, not copied.\n *\n * Only self-contained files are supported: `.ply`, `.spz`, `.splat`, `.ksplat`\n * and bundled `.sog`. An unbundled SOG directory needs sibling fetches, so it\n * must be served over HTTP and loaded with {@link loadSplatData}.\n *\n * @param file - The file; its name selects the parser unless an explicit\n * `format` option is provided.\n * @returns The decoded splat data, ready for {@link SplatMesh}.\n * @throws Rejects only with `SplatLoadError` (unknown extension, read or\n * decode failures) or a `DOMException` named `AbortError` when\n * `options.signal` fires.\n *\n * @example\n * import { SplatMesh } from '@voluma/vlam';\n * import { loadSplatDataFile } from '@voluma/vlam/loaders';\n * const splats = new SplatMesh(await loadSplatDataFile(event.dataTransfer.files[0]));\n */\nexport async function loadSplatDataFile(\n file: File,\n options: SplatDataFileLoadOptions = {},\n): Promise<SplatData> {\n const loader = new ChunkLoader();\n try {\n return await loader.loadFile(file, options);\n } finally {\n loader.dispose();\n }\n}\n"],"names":["loadSplatData","input","options","loader","ChunkLoader","loadSplatDataFile","file"],"mappings":";;AAiCA,eAAsBA,EACpBC,GACAC,IAAgC,IACZ;AACpB,QAAMC,IAAS,IAAIC,EAAA;AACnB,MAAI;AACF,WAAO,MAAMD,EAAO,KAAKF,aAAiB,MAAMA,EAAM,OAAOA,GAAOC,CAAO;AAAA,EAC7E,UAAA;AACE,IAAAC,EAAO,QAAA;AAAA,EACT;AACF;AAuBA,eAAsBE,EACpBC,GACAJ,IAAoC,IAChB;AACpB,QAAMC,IAAS,IAAIC,EAAA;AACnB,MAAI;AACF,WAAO,MAAMD,EAAO,SAASG,GAAMJ,CAAO;AAAA,EAC5C,UAAA;AACE,IAAAC,EAAO,QAAA;AAAA,EACT;AACF;"}
1
+ {"version":3,"file":"loaders.js","sources":["../src/lib/loaders/load-splat-data.ts"],"sourcesContent":["import type { SplatData } from '../core/splat-data';\nimport { ChunkLoader } from './chunk-loader';\nimport type { SplatFormat, SplatDataFileLoadOptions, SplatInputOptions } from './loading';\n\nexport type { SplatDataFileLoadOptions };\n\n/** Options for {@link loadSplatData}. */\nexport interface SplatDataLoadOptions extends SplatInputOptions {\n /** Parser selection; URL extension detection is used only for `auto`. */\n format?: SplatFormat;\n}\n\n/**\n * Fetches and decodes a splat scene (`.sog`, `.ply`, `.spz`, `.splat` or\n * `.ksplat`) in a Web Worker, so\n * even multi-million-splat decodes never block the main thread. The\n * decoded arrays are transferred back, not copied.\n *\n * Whole-file convenience over {@link ChunkLoader}; use `ChunkLoader`\n * directly when loading many chunks or when cancellation is needed.\n *\n * @param input - Capture URL; its extension selects the parser unless an\n * explicit `format` option is provided.\n * @returns The decoded splat data, ready for {@link SplatMesh}.\n * @throws Rejects only with `SplatLoadError` (resolve, fetch or decode\n * failures - `error.phase` says which) or a `DOMException` named `AbortError`\n * when `options.signal` fires.\n *\n * @example\n * import { SplatMesh } from '@voluma/vlam';\n * import { loadSplatData } from '@voluma/vlam/loaders';\n * const splats = new SplatMesh(await loadSplatData('/scene.sog'));\n */\nexport async function loadSplatData(\n input: string | URL,\n options: SplatDataLoadOptions = {},\n): Promise<SplatData> {\n const loader = new ChunkLoader();\n try {\n return await loader.load(input instanceof URL ? input.href : input, options);\n } finally {\n loader.dispose();\n }\n}\n\n/**\n * Decodes a local splat file - from a `<input type=\"file\">` or a drag-and-drop\n * - in a Web Worker, without uploading it anywhere. The file's bytes are read\n * worker-side and the decoded arrays are transferred back, not copied.\n *\n * Only self-contained files are supported: `.ply`, `.spz`, `.splat`, `.ksplat`\n * and bundled `.sog`. An unbundled SOG directory needs sibling fetches, so it\n * must be served over HTTP and loaded with {@link loadSplatData}.\n *\n * @param file - The file; its name selects the parser unless an explicit\n * `format` option is provided.\n * @returns The decoded splat data, ready for {@link SplatMesh}.\n * @throws Rejects only with `SplatLoadError` (unknown extension, read or\n * decode failures) or a `DOMException` named `AbortError` when\n * `options.signal` fires.\n *\n * @example\n * import { SplatMesh } from '@voluma/vlam';\n * import { loadSplatDataFile } from '@voluma/vlam/loaders';\n * const splats = new SplatMesh(await loadSplatDataFile(event.dataTransfer.files[0]));\n */\nexport async function loadSplatDataFile(\n file: File,\n options: SplatDataFileLoadOptions = {},\n): Promise<SplatData> {\n const loader = new ChunkLoader();\n try {\n return await loader.loadFile(file, options);\n } finally {\n loader.dispose();\n }\n}\n"],"names":["loadSplatData","input","options","loader","ChunkLoader","loadSplatDataFile","file"],"mappings":";;AAiCA,eAAsBA,EACpBC,GACAC,IAAgC,IACZ;AACpB,QAAMC,IAAS,IAAIC,EAAA;AACnB,MAAI;AACF,WAAO,MAAMD,EAAO,KAAKF,aAAiB,MAAMA,EAAM,OAAOA,GAAOC,CAAO;AAAA,EAC7E,UAAA;AACE,IAAAC,EAAO,QAAA;AAAA,EACT;AACF;AAuBA,eAAsBE,EACpBC,GACAJ,IAAoC,IAChB;AACpB,QAAMC,IAAS,IAAIC,EAAA;AACnB,MAAI;AACF,WAAO,MAAMD,EAAO,SAASG,GAAMJ,CAAO;AAAA,EAC5C,UAAA;AACE,IAAAC,EAAO,QAAA;AAAA,EACT;AACF;"}
@@ -1 +1 @@
1
- {"version":3,"file":"loading-wLo8vRbA.js","sources":["../src/lib/loading.ts"],"sourcesContent":["/**\n * The self-contained scene formats - files (or bundles) that decode to a whole\n * scene on their own, with no external manifest. This is the value set of\n * {@link SplatData.format} and the non-`auto` half of {@link SplatFormat}.\n */\nexport type SplatDataFormat = 'ply' | 'sog' | 'spz' | 'splat' | 'ksplat' | 'rad';\n\n/** Explicit formats accepted by {@link loadSplatData}. */\nexport type SplatFormat = 'auto' | SplatDataFormat;\n\n/** Explicit formats accepted by {@link StreamedSplatMesh.load}. */\nexport type StreamedSplatFormat = 'auto' | 'streamed-sog' | 'lcc2' | 'lcc' | 'rad';\n\n/**\n * Parsers the loading worker can run on a chunk. This extends {@link SplatFormat}\n * with formats that are only ever reached as a streamed chunk, never as a\n * standalone scene handed to {@link loadSplatData}: `lcc-bin` is a byte range of an\n * LCC (`.lcc`) `data.bin`, and `rad-chunk` a byte range of a single-file `.rad` -\n * both meaningless without their manifest/header.\n */\nexport type ChunkFileFormat = Exclude<SplatFormat, 'auto'> | 'lcc-bin' | 'rad-chunk';\n\n/** Serializable fetch settings that can cross the loading-worker boundary. */\nexport interface SplatRequestOptions {\n headers?: Readonly<Record<string, string>>;\n credentials?: RequestCredentials;\n mode?: RequestMode;\n cache?: RequestCache;\n}\n\n/**\n * Reports load progress in bytes, for a determinate progress indicator.\n *\n * `total` is 0 when the size is not known ahead of time (a response with no\n * `Content-Length`), which means \"still working\" rather than \"finished\".\n */\nexport type SplatProgressCallback = (loaded: number, total: number) => void;\n\n/** Shortest gap between two progress reports, in milliseconds (~10/s). */\nconst PROGRESS_INTERVAL_MS = 100;\n\n/**\n * Rate-limits progress reports to ~10/s.\n *\n * A body arrives in ~64 KB chunks, so an unthrottled report would post\n * thousands of messages no indicator can render. The final report is exempt -\n * it is what leaves a determinate bar exactly full rather than at 97%.\n *\n * Identifying that final report needs a known size, so the exemption requires\n * `total > 0`. When the size is unknown there is no bar to fill (the caller\n * shows a spinner) and every report is throttled alike - without the `total`\n * check, `loaded < total` would be false for all of them and the throttle\n * would never engage at all.\n */\nexport function createProgressThrottle(\n report: SplatProgressCallback,\n intervalMs: number = PROGRESS_INTERVAL_MS,\n): SplatProgressCallback {\n let lastReportAt = -Infinity;\n return (loaded: number, total: number): void => {\n const isFinal = total > 0 && loaded >= total;\n const now = monotonicNow();\n if (!isFinal && now - lastReportAt < intervalMs) return;\n lastReportAt = now;\n report(loaded, total);\n };\n}\n\n/**\n * Options for decoding a local file.\n *\n * Shared by {@link loadSplatDataFile} and `ChunkLoader.loadFile`. There is no\n * `baseUrl`/`request` counterpart to {@link SplatInputOptions} here: a file has\n * no URL to resolve and no request to configure.\n */\nexport interface SplatDataFileLoadOptions {\n signal?: AbortSignal;\n /** Parser selection; the file name's extension is used only for `auto`. */\n format?: SplatFormat;\n /**\n * Called as the file is read, for a determinate progress bar. A multi-\n * gigabyte PLY takes tens of seconds, so the streamed PLY path reports as it\n * goes; the whole-buffer formats have nothing to report until they are done.\n */\n onProgress?: SplatProgressCallback;\n}\n\n/** Shared URL, cancellation, and request options for package loaders. */\nexport interface SplatInputOptions {\n signal?: AbortSignal;\n baseUrl?: string | URL;\n request?: SplatRequestOptions;\n /**\n * Called as bytes are read. Fires from the loading worker, so it is\n * throttled to whole read windows rather than every byte.\n */\n onProgress?: SplatProgressCallback;\n}\n\n/** The stage at which a splat load failed. */\nexport type SplatLoadPhase = 'resolve' | 'manifest' | 'fetch' | 'decode' | 'worker';\n\n/** Serializable error details returned by the loading worker. */\nexport interface SerializedSplatLoadError {\n name: string;\n message: string;\n phase: SplatLoadPhase;\n url: string;\n status?: number;\n retryable: boolean;\n}\n\n/** Structured error thrown for non-cancellation loading failures. */\nexport class SplatLoadError extends Error {\n readonly phase: SplatLoadPhase;\n readonly url: string;\n readonly status?: number;\n readonly retryable: boolean;\n\n constructor(\n message: string,\n options: {\n phase: SplatLoadPhase;\n url: string;\n status?: number;\n retryable: boolean;\n cause?: unknown;\n },\n ) {\n super(message, { cause: options.cause });\n this.name = 'SplatLoadError';\n this.phase = options.phase;\n this.url = options.url;\n this.status = options.status;\n this.retryable = options.retryable;\n }\n}\n\n/** Resolves a loader input without assuming that a browser location exists. */\nexport function resolveSplatUrl(input: string | URL, baseUrl?: string | URL): URL {\n if (input instanceof URL) return new URL(input.href);\n try {\n const fallbackBase = baseUrl ?? globalThis.location?.href;\n if (fallbackBase === undefined) return new URL(input);\n return new URL(input, fallbackBase);\n } catch (cause) {\n throw new SplatLoadError(`Unable to resolve splat URL \"${input}\".`, {\n phase: 'resolve',\n url: input,\n retryable: false,\n cause,\n });\n }\n}\n\n/** Returns the lower-case pathname extension, excluding query/hash text. */\nexport function splatUrlExtension(url: URL): string {\n return splatNameExtension(url.pathname);\n}\n\n/** Returns the lower-case extension of a plain file name or path. */\nexport function splatNameExtension(name: string): string {\n const lower = name.toLowerCase();\n const dot = lower.lastIndexOf('.');\n return dot < 0 ? '' : lower.slice(dot);\n}\n\n/**\n * Maps a file extension to its parser. `label` names the input in the\n * failure message - a URL for a fetched scene, a file name for a local one.\n *\n * @throws {SplatLoadError} (phase `'resolve'`, not retryable) for an unknown\n * extension, keeping the loader contract uniform: callers of the loading\n * pipeline only ever see `SplatLoadError` or `AbortError`.\n */\nexport function splatFormatForExtension(extension: string, label: string): SplatDataFormat {\n switch (extension) {\n case '.ply':\n return 'ply';\n case '.spz':\n return 'spz';\n case '.splat':\n return 'splat';\n case '.ksplat':\n return 'ksplat';\n case '.sog':\n return 'sog';\n case '.rad':\n return 'rad';\n default:\n throw new SplatLoadError(`Unsupported splat file extension in ${label}.`, {\n phase: 'resolve',\n url: label,\n retryable: false,\n });\n }\n}\n\n/** Creates a fetch init object without sharing mutable header state. */\nexport function toRequestInit(options?: SplatRequestOptions, signal?: AbortSignal): RequestInit {\n return {\n ...(options?.headers ? { headers: { ...options.headers } } : {}),\n ...(options?.credentials ? { credentials: options.credentials } : {}),\n ...(options?.mode ? { mode: options.mode } : {}),\n ...(options?.cache ? { cache: options.cache } : {}),\n ...(signal ? { signal } : {}),\n };\n}\n\n/**\n * The largest `ArrayBuffer` a browser will allocate: 2 GiB, in every current\n * engine. A file bigger than this cannot be read in one piece at all - the\n * read fails rather than the decode.\n */\nexport const MAX_ARRAY_BUFFER_BYTES = 2 ** 31;\n\n/** Bytes as a short \"1.7 GB\" for a message. */\nfunction gigabytes(bytes: number): string {\n return `${(bytes / 1e9).toFixed(1)} GB`;\n}\n\n/**\n * Reads a whole local file, turning the browser's two unhelpful failure modes\n * into messages that name the real cause.\n *\n * Chrome reports an over-2 GiB read as `NotReadableError`, whose stock text\n * blames file permissions - so a perfectly good 4 GB capture reads as \"your\n * file is broken\". Check the size up front instead.\n *\n * Formats with a streaming parser (see `parseSplatPlyFile`) never come here.\n */\nexport async function readWholeFile(file: File): Promise<ArrayBuffer> {\n if (file.size > MAX_ARRAY_BUFFER_BYTES) {\n throw new Error(\n `${file.name} is ${gigabytes(file.size)}. A browser cannot read more than 2 GB of a file ` +\n 'at once, and this format has to be decoded in one piece. Convert it to SOG ' +\n '(npx @playcanvas/splat-transform in.ply out.sog) or a compressed .ply.',\n );\n }\n try {\n return await file.arrayBuffer();\n } catch (error) {\n if (isAbortError(error)) throw error;\n // NotReadableError also covers a file that moved or changed since the drop\n // - the reference is a snapshot, and reading revalidates it.\n if (error instanceof Error && error.name === 'NotReadableError') {\n throw new Error(\n `${file.name} could not be read. It may have been moved, renamed or changed since it ` +\n 'was dropped, or it may be on a drive the browser cannot reach.',\n { cause: error },\n );\n }\n throw error;\n }\n}\n\n/** Creates an AbortError even where DOMException is unavailable. */\nexport function createAbortError(message: string): Error {\n if (typeof DOMException !== 'undefined') return new DOMException(message, 'AbortError');\n const error = new Error(message);\n error.name = 'AbortError';\n return error;\n}\n\n/** Whether an unknown failure represents cancellation. */\nexport function isAbortError(error: unknown): boolean {\n return error instanceof Error && error.name === 'AbortError';\n}\n\n/** Maps an arbitrary failure to the package's structured loading error. */\nexport function toSplatLoadError(\n error: unknown,\n options: { phase: SplatLoadPhase; url: string; status?: number },\n): SplatLoadError {\n if (error instanceof SplatLoadError) return error;\n const message = error instanceof Error ? error.message : String(error);\n return new SplatLoadError(message, {\n ...options,\n retryable: isRetryableFailure(options.status, options.phase),\n cause: error,\n });\n}\n\n/** Converts a structured load failure into worker-safe data. */\nexport function serializeSplatLoadError(error: SplatLoadError): SerializedSplatLoadError {\n return {\n name: error.name,\n message: error.message,\n phase: error.phase,\n url: error.url,\n ...(error.status === undefined ? {} : { status: error.status }),\n retryable: error.retryable,\n };\n}\n\n/** Reconstructs a package error received from a loading worker. */\nexport function deserializeSplatLoadError(error: SerializedSplatLoadError): SplatLoadError {\n return new SplatLoadError(error.message, error);\n}\n\n/** Monotonic time where available, with an SSR-safe fallback. */\nexport function monotonicNow(): number {\n return globalThis.performance?.now() ?? Date.now();\n}\n\nfunction isRetryableFailure(status: number | undefined, phase: SplatLoadPhase): boolean {\n if (status !== undefined)\n return status === 408 || status === 425 || status === 429 || status >= 500;\n return phase === 'fetch' || phase === 'manifest' || phase === 'worker';\n}\n"],"names":["SplatLoadError","message","options","__publicField","resolveSplatUrl","input","baseUrl","_a","fallbackBase","cause","splatUrlExtension","url","splatNameExtension","name","lower","dot","splatFormatForExtension","extension","label","toRequestInit","signal","createAbortError","error","isAbortError","toSplatLoadError","isRetryableFailure","deserializeSplatLoadError","status","phase"],"mappings":"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"}
1
+ {"version":3,"file":"loading-wLo8vRbA.js","sources":["../src/lib/loaders/loading.ts"],"sourcesContent":["/**\n * The self-contained scene formats - files (or bundles) that decode to a whole\n * scene on their own, with no external manifest. This is the value set of\n * {@link SplatData.format} and the non-`auto` half of {@link SplatFormat}.\n */\nexport type SplatDataFormat = 'ply' | 'sog' | 'spz' | 'splat' | 'ksplat' | 'rad';\n\n/** Explicit formats accepted by {@link loadSplatData}. */\nexport type SplatFormat = 'auto' | SplatDataFormat;\n\n/** Explicit formats accepted by {@link StreamedSplatMesh.load}. */\nexport type StreamedSplatFormat = 'auto' | 'streamed-sog' | 'lcc2' | 'lcc' | 'rad';\n\n/**\n * Parsers the loading worker can run on a chunk. This extends {@link SplatFormat}\n * with formats that are only ever reached as a streamed chunk, never as a\n * standalone scene handed to {@link loadSplatData}: `lcc-bin` is a byte range of an\n * LCC (`.lcc`) `data.bin`, and `rad-chunk` a byte range of a single-file `.rad` -\n * both meaningless without their manifest/header.\n */\nexport type ChunkFileFormat = Exclude<SplatFormat, 'auto'> | 'lcc-bin' | 'rad-chunk';\n\n/** Serializable fetch settings that can cross the loading-worker boundary. */\nexport interface SplatRequestOptions {\n headers?: Readonly<Record<string, string>>;\n credentials?: RequestCredentials;\n mode?: RequestMode;\n cache?: RequestCache;\n}\n\n/**\n * Reports load progress in bytes, for a determinate progress indicator.\n *\n * `total` is 0 when the size is not known ahead of time (a response with no\n * `Content-Length`), which means \"still working\" rather than \"finished\".\n */\nexport type SplatProgressCallback = (loaded: number, total: number) => void;\n\n/** Shortest gap between two progress reports, in milliseconds (~10/s). */\nconst PROGRESS_INTERVAL_MS = 100;\n\n/**\n * Rate-limits progress reports to ~10/s.\n *\n * A body arrives in ~64 KB chunks, so an unthrottled report would post\n * thousands of messages no indicator can render. The final report is exempt -\n * it is what leaves a determinate bar exactly full rather than at 97%.\n *\n * Identifying that final report needs a known size, so the exemption requires\n * `total > 0`. When the size is unknown there is no bar to fill (the caller\n * shows a spinner) and every report is throttled alike - without the `total`\n * check, `loaded < total` would be false for all of them and the throttle\n * would never engage at all.\n */\nexport function createProgressThrottle(\n report: SplatProgressCallback,\n intervalMs: number = PROGRESS_INTERVAL_MS,\n): SplatProgressCallback {\n let lastReportAt = -Infinity;\n return (loaded: number, total: number): void => {\n const isFinal = total > 0 && loaded >= total;\n const now = monotonicNow();\n if (!isFinal && now - lastReportAt < intervalMs) return;\n lastReportAt = now;\n report(loaded, total);\n };\n}\n\n/**\n * Options for decoding a local file.\n *\n * Shared by {@link loadSplatDataFile} and `ChunkLoader.loadFile`. There is no\n * `baseUrl`/`request` counterpart to {@link SplatInputOptions} here: a file has\n * no URL to resolve and no request to configure.\n */\nexport interface SplatDataFileLoadOptions {\n signal?: AbortSignal;\n /** Parser selection; the file name's extension is used only for `auto`. */\n format?: SplatFormat;\n /**\n * Called as the file is read, for a determinate progress bar. A multi-\n * gigabyte PLY takes tens of seconds, so the streamed PLY path reports as it\n * goes; the whole-buffer formats have nothing to report until they are done.\n */\n onProgress?: SplatProgressCallback;\n}\n\n/** Shared URL, cancellation, and request options for package loaders. */\nexport interface SplatInputOptions {\n signal?: AbortSignal;\n baseUrl?: string | URL;\n request?: SplatRequestOptions;\n /**\n * Called as bytes are read. Fires from the loading worker, so it is\n * throttled to whole read windows rather than every byte.\n */\n onProgress?: SplatProgressCallback;\n}\n\n/** The stage at which a splat load failed. */\nexport type SplatLoadPhase = 'resolve' | 'manifest' | 'fetch' | 'decode' | 'worker';\n\n/** Serializable error details returned by the loading worker. */\nexport interface SerializedSplatLoadError {\n name: string;\n message: string;\n phase: SplatLoadPhase;\n url: string;\n status?: number;\n retryable: boolean;\n}\n\n/** Structured error thrown for non-cancellation loading failures. */\nexport class SplatLoadError extends Error {\n readonly phase: SplatLoadPhase;\n readonly url: string;\n readonly status?: number;\n readonly retryable: boolean;\n\n constructor(\n message: string,\n options: {\n phase: SplatLoadPhase;\n url: string;\n status?: number;\n retryable: boolean;\n cause?: unknown;\n },\n ) {\n super(message, { cause: options.cause });\n this.name = 'SplatLoadError';\n this.phase = options.phase;\n this.url = options.url;\n this.status = options.status;\n this.retryable = options.retryable;\n }\n}\n\n/** Resolves a loader input without assuming that a browser location exists. */\nexport function resolveSplatUrl(input: string | URL, baseUrl?: string | URL): URL {\n if (input instanceof URL) return new URL(input.href);\n try {\n const fallbackBase = baseUrl ?? globalThis.location?.href;\n if (fallbackBase === undefined) return new URL(input);\n return new URL(input, fallbackBase);\n } catch (cause) {\n throw new SplatLoadError(`Unable to resolve splat URL \"${input}\".`, {\n phase: 'resolve',\n url: input,\n retryable: false,\n cause,\n });\n }\n}\n\n/** Returns the lower-case pathname extension, excluding query/hash text. */\nexport function splatUrlExtension(url: URL): string {\n return splatNameExtension(url.pathname);\n}\n\n/** Returns the lower-case extension of a plain file name or path. */\nexport function splatNameExtension(name: string): string {\n const lower = name.toLowerCase();\n const dot = lower.lastIndexOf('.');\n return dot < 0 ? '' : lower.slice(dot);\n}\n\n/**\n * Maps a file extension to its parser. `label` names the input in the\n * failure message - a URL for a fetched scene, a file name for a local one.\n *\n * @throws {SplatLoadError} (phase `'resolve'`, not retryable) for an unknown\n * extension, keeping the loader contract uniform: callers of the loading\n * pipeline only ever see `SplatLoadError` or `AbortError`.\n */\nexport function splatFormatForExtension(extension: string, label: string): SplatDataFormat {\n switch (extension) {\n case '.ply':\n return 'ply';\n case '.spz':\n return 'spz';\n case '.splat':\n return 'splat';\n case '.ksplat':\n return 'ksplat';\n case '.sog':\n return 'sog';\n case '.rad':\n return 'rad';\n default:\n throw new SplatLoadError(`Unsupported splat file extension in ${label}.`, {\n phase: 'resolve',\n url: label,\n retryable: false,\n });\n }\n}\n\n/** Creates a fetch init object without sharing mutable header state. */\nexport function toRequestInit(options?: SplatRequestOptions, signal?: AbortSignal): RequestInit {\n return {\n ...(options?.headers ? { headers: { ...options.headers } } : {}),\n ...(options?.credentials ? { credentials: options.credentials } : {}),\n ...(options?.mode ? { mode: options.mode } : {}),\n ...(options?.cache ? { cache: options.cache } : {}),\n ...(signal ? { signal } : {}),\n };\n}\n\n/**\n * The largest `ArrayBuffer` a browser will allocate: 2 GiB, in every current\n * engine. A file bigger than this cannot be read in one piece at all - the\n * read fails rather than the decode.\n */\nexport const MAX_ARRAY_BUFFER_BYTES = 2 ** 31;\n\n/** Bytes as a short \"1.7 GB\" for a message. */\nfunction gigabytes(bytes: number): string {\n return `${(bytes / 1e9).toFixed(1)} GB`;\n}\n\n/**\n * Reads a whole local file, turning the browser's two unhelpful failure modes\n * into messages that name the real cause.\n *\n * Chrome reports an over-2 GiB read as `NotReadableError`, whose stock text\n * blames file permissions - so a perfectly good 4 GB capture reads as \"your\n * file is broken\". Check the size up front instead.\n *\n * Formats with a streaming parser (see `parseSplatPlyFile`) never come here.\n */\nexport async function readWholeFile(file: File): Promise<ArrayBuffer> {\n if (file.size > MAX_ARRAY_BUFFER_BYTES) {\n throw new Error(\n `${file.name} is ${gigabytes(file.size)}. A browser cannot read more than 2 GB of a file ` +\n 'at once, and this format has to be decoded in one piece. Convert it to SOG ' +\n '(npx @playcanvas/splat-transform in.ply out.sog) or a compressed .ply.',\n );\n }\n try {\n return await file.arrayBuffer();\n } catch (error) {\n if (isAbortError(error)) throw error;\n // NotReadableError also covers a file that moved or changed since the drop\n // - the reference is a snapshot, and reading revalidates it.\n if (error instanceof Error && error.name === 'NotReadableError') {\n throw new Error(\n `${file.name} could not be read. It may have been moved, renamed or changed since it ` +\n 'was dropped, or it may be on a drive the browser cannot reach.',\n { cause: error },\n );\n }\n throw error;\n }\n}\n\n/** Creates an AbortError even where DOMException is unavailable. */\nexport function createAbortError(message: string): Error {\n if (typeof DOMException !== 'undefined') return new DOMException(message, 'AbortError');\n const error = new Error(message);\n error.name = 'AbortError';\n return error;\n}\n\n/** Whether an unknown failure represents cancellation. */\nexport function isAbortError(error: unknown): boolean {\n return error instanceof Error && error.name === 'AbortError';\n}\n\n/** Maps an arbitrary failure to the package's structured loading error. */\nexport function toSplatLoadError(\n error: unknown,\n options: { phase: SplatLoadPhase; url: string; status?: number },\n): SplatLoadError {\n if (error instanceof SplatLoadError) return error;\n const message = error instanceof Error ? error.message : String(error);\n return new SplatLoadError(message, {\n ...options,\n retryable: isRetryableFailure(options.status, options.phase),\n cause: error,\n });\n}\n\n/** Converts a structured load failure into worker-safe data. */\nexport function serializeSplatLoadError(error: SplatLoadError): SerializedSplatLoadError {\n return {\n name: error.name,\n message: error.message,\n phase: error.phase,\n url: error.url,\n ...(error.status === undefined ? {} : { status: error.status }),\n retryable: error.retryable,\n };\n}\n\n/** Reconstructs a package error received from a loading worker. */\nexport function deserializeSplatLoadError(error: SerializedSplatLoadError): SplatLoadError {\n return new SplatLoadError(error.message, error);\n}\n\n/** Monotonic time where available, with an SSR-safe fallback. */\nexport function monotonicNow(): number {\n return globalThis.performance?.now() ?? Date.now();\n}\n\nfunction isRetryableFailure(status: number | undefined, phase: SplatLoadPhase): boolean {\n if (status !== undefined)\n return status === 408 || status === 425 || status === 429 || status >= 500;\n return phase === 'fetch' || phase === 'manifest' || phase === 'worker';\n}\n"],"names":["SplatLoadError","message","options","__publicField","resolveSplatUrl","input","baseUrl","_a","fallbackBase","cause","splatUrlExtension","url","splatNameExtension","name","lower","dot","splatFormatForExtension","extension","label","toRequestInit","signal","createAbortError","error","isAbortError","toSplatLoadError","isRetryableFailure","deserializeSplatLoadError","status","phase"],"mappings":"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"}