@voluma/vlam 0.3.0 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +0 -2
- package/dist/chunk-loader-DlNwggfG.js.map +1 -1
- package/dist/{index.d.ts → core/index.d.ts} +1 -1
- package/dist/{splat-budget.d.ts → core/splat-budget.d.ts} +1 -1
- package/dist/{splat-data.d.ts → core/splat-data.d.ts} +1 -1
- package/dist/{splat-mesh-types.d.ts → core/splat-mesh-types.d.ts} +5 -2
- package/dist/{effects.d.ts → effects/index.d.ts} +3 -3
- package/dist/effects.js.map +1 -1
- package/dist/formats/ksplat/parse-ksplat.d.ts +1 -1
- package/dist/formats/ksplat.js.map +1 -1
- package/dist/formats/lcc/collision-mesh.d.ts +2 -2
- package/dist/formats/lcc/collision-partition.d.ts +1 -1
- package/dist/formats/lcc/lcc.d.ts +3 -3
- package/dist/formats/lcc/lcc2.d.ts +2 -2
- package/dist/formats/lcc/parse-lcc.d.ts +1 -1
- package/dist/formats/lcc.js.map +1 -1
- package/dist/formats/ply/parse-compressed-ply.d.ts +2 -2
- package/dist/formats/ply/parse-splat-ply.d.ts +2 -2
- package/dist/formats/ply.js.map +1 -1
- package/dist/formats/rad/frontier-worker-protocol.d.ts +8 -1
- package/dist/formats/rad/index.d.ts +1 -1
- package/dist/formats/rad/parse-rad.d.ts +1 -1
- package/dist/formats/rad/rad-foveated-source.d.ts +3 -3
- package/dist/formats/rad/rad.d.ts +5 -5
- package/dist/formats/rad.js.map +1 -1
- package/dist/formats/sog/parse-sog.d.ts +2 -2
- package/dist/formats/sog.js.map +1 -1
- package/dist/formats/splat/parse-splat.d.ts +1 -1
- package/dist/formats/splat.js.map +1 -1
- package/dist/formats/spz/parse-spz.d.ts +1 -1
- package/dist/formats/spz.js.map +1 -1
- package/dist/half-float-Cs4FDq8Q.js.map +1 -1
- package/dist/index.js +3 -3
- package/dist/index.js.map +1 -1
- package/dist/{chunk-loader.d.ts → loaders/chunk-loader.d.ts} +2 -2
- package/dist/{load-splat-data.d.ts → loaders/load-splat-data.d.ts} +1 -1
- package/dist/{load-worker-protocol.d.ts → loaders/load-worker-protocol.d.ts} +2 -2
- package/dist/loaders.js.map +1 -1
- package/dist/loading-wLo8vRbA.js.map +1 -1
- package/dist/lod-scheduler-B0a_uBlv.js.map +1 -1
- package/dist/logging-BfPdd7NJ.js.map +1 -1
- package/dist/ply-header-B9WllbPg.js.map +1 -1
- package/dist/{radix-sorter-R0FCSqKu.js → radix-sorter-BrbUg_CV.js} +2 -2
- package/dist/radix-sorter-BrbUg_CV.js.map +1 -0
- package/dist/{relighting-CSQF3UcL.js → relighting-Tiwep8yd.js} +302 -294
- package/dist/relighting-Tiwep8yd.js.map +1 -0
- package/dist/{selection-volume.d.ts → selection/selection-volume.d.ts} +1 -1
- package/dist/{splat-partition.d.ts → selection/splat-partition.d.ts} +1 -1
- package/dist/selection.js.map +1 -1
- package/dist/sh-pack-D5wAe5gg.js.map +1 -1
- package/dist/splat-budget-DuVZspPQ.js.map +1 -1
- package/dist/splat-data-BZM5dhhx.js.map +1 -1
- package/dist/{splat-mesh-aQdcWA6v.js → splat-mesh-CsLOQb08.js} +3 -3
- package/dist/splat-mesh-CsLOQb08.js.map +1 -0
- package/dist/splat-mesh-types-BZIko-_9.js.map +1 -1
- package/dist/{static-lod-splat-mesh.d.ts → static-lod/static-lod-splat-mesh.d.ts} +2 -2
- package/dist/{static-lod-worker-protocol.d.ts → static-lod/static-lod-worker-protocol.d.ts} +1 -1
- package/dist/{static-lod.d.ts → static-lod/static-lod.d.ts} +1 -1
- package/dist/static-lod.js +1 -1
- package/dist/static-lod.js.map +1 -1
- package/dist/{dataset-source.d.ts → streaming/dataset-source.d.ts} +1 -1
- package/dist/{streaming.d.ts → streaming/index.d.ts} +3 -3
- package/dist/{lod-source.d.ts → streaming/lod-source.d.ts} +4 -4
- package/dist/{streamed-splat-mesh-utils.d.ts → streaming/streamed-splat-mesh-utils.d.ts} +2 -2
- package/dist/{streamed-splat-mesh.d.ts → streaming/streamed-splat-mesh.d.ts} +45 -5
- package/dist/streaming.js +844 -810
- package/dist/streaming.js.map +1 -1
- package/dist/{unified-splat-mesh.d.ts → unified/unified-splat-mesh.d.ts} +13 -6
- package/dist/{work-buffer-gather.d.ts → unified/work-buffer-gather.d.ts} +5 -5
- package/dist/{work-buffer-material.d.ts → unified/work-buffer-material.d.ts} +1 -1
- package/dist/unified.js +280 -272
- package/dist/unified.js.map +1 -1
- package/dist/webgpu-limits-Y3CI_6_6.js.map +1 -1
- package/package.json +9 -9
- package/dist/radix-sorter-R0FCSqKu.js.map +0 -1
- package/dist/relighting-CSQF3UcL.js.map +0 -1
- package/dist/splat-mesh-aQdcWA6v.js.map +0 -1
- /package/dist/{compute-sorter.d.ts → core/compute-sorter.d.ts} +0 -0
- /package/dist/{create-splat-renderer.d.ts → core/create-splat-renderer.d.ts} +0 -0
- /package/dist/{depth-of-field.d.ts → core/depth-of-field.d.ts} +0 -0
- /package/dist/{half-float.d.ts → core/half-float.d.ts} +0 -0
- /package/dist/{logging.d.ts → core/logging.d.ts} +0 -0
- /package/dist/{merged-splat-mesh.d.ts → core/merged-splat-mesh.d.ts} +0 -0
- /package/dist/{orientation.d.ts → core/orientation.d.ts} +0 -0
- /package/dist/{radix-sort.d.ts → core/radix-sort.d.ts} +0 -0
- /package/dist/{radix-sorter.d.ts → core/radix-sorter.d.ts} +0 -0
- /package/dist/{relighting-pass.d.ts → core/relighting-pass.d.ts} +0 -0
- /package/dist/{relighting.d.ts → core/relighting.d.ts} +0 -0
- /package/dist/{sh-pack.d.ts → core/sh-pack.d.ts} +0 -0
- /package/dist/{sort-scheduler.d.ts → core/sort-scheduler.d.ts} +0 -0
- /package/dist/{sort-worker-protocol.d.ts → core/sort-worker-protocol.d.ts} +0 -0
- /package/dist/{sorter.d.ts → core/sorter.d.ts} +0 -0
- /package/dist/{source-transform.d.ts → core/source-transform.d.ts} +0 -0
- /package/dist/{splat-depth-pack.d.ts → core/splat-depth-pack.d.ts} +0 -0
- /package/dist/{splat-mesh-material.d.ts → core/splat-mesh-material.d.ts} +0 -0
- /package/dist/{splat-mesh-picking.d.ts → core/splat-mesh-picking.d.ts} +0 -0
- /package/dist/{splat-mesh-pool.d.ts → core/splat-mesh-pool.d.ts} +0 -0
- /package/dist/{splat-mesh.d.ts → core/splat-mesh.d.ts} +0 -0
- /package/dist/{splat-modifier-stack.d.ts → core/splat-modifier-stack.d.ts} +0 -0
- /package/dist/{splat-modifier.d.ts → core/splat-modifier.d.ts} +0 -0
- /package/dist/{splat-query.d.ts → core/splat-query.d.ts} +0 -0
- /package/dist/{splat-sort-bounds.d.ts → core/splat-sort-bounds.d.ts} +0 -0
- /package/dist/{storage-attribute-mirror.d.ts → core/storage-attribute-mirror.d.ts} +0 -0
- /package/dist/{webgpu-limits.d.ts → core/webgpu-limits.d.ts} +0 -0
- /package/dist/{worker-sorter.d.ts → core/worker-sorter.d.ts} +0 -0
- /package/dist/{xr-view.d.ts → core/xr-view.d.ts} +0 -0
- /package/dist/{loaders.d.ts → loaders/index.d.ts} +0 -0
- /package/dist/{loading.d.ts → loaders/loading.d.ts} +0 -0
- /package/dist/{ply-header.d.ts → loaders/ply-header.d.ts} +0 -0
- /package/dist/{selection.d.ts → selection/index.d.ts} +0 -0
- /package/dist/{static-lod-entry.d.ts → static-lod/index.d.ts} +0 -0
- /package/dist/{budget-governor.d.ts → streaming/budget-governor.d.ts} +0 -0
- /package/dist/{camera-budget-governor.d.ts → streaming/camera-budget-governor.d.ts} +0 -0
- /package/dist/{chunk-cache-budget.d.ts → streaming/chunk-cache-budget.d.ts} +0 -0
- /package/dist/{chunk-fetch-scheduler.d.ts → streaming/chunk-fetch-scheduler.d.ts} +0 -0
- /package/dist/{lod-manifest.d.ts → streaming/lod-manifest.d.ts} +0 -0
- /package/dist/{lod-scheduler.d.ts → streaming/lod-scheduler.d.ts} +0 -0
- /package/dist/{unified.d.ts → unified/index.d.ts} +0 -0
- /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 '
|
|
1
|
+
import type { SplatData } from '../core/splat-data';
|
|
2
2
|
import type { RadChunkRangeRequest } from './load-worker-protocol';
|
|
3
|
-
import type { LccChunkParams } from '
|
|
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 '
|
|
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 '
|
|
11
|
-
import type { RadShCodebook, SplatData } from '
|
|
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. */
|
package/dist/loaders.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"loaders.js","sources":["../src/lib/load-splat-data.ts"],"sourcesContent":["import type { SplatData } from '
|
|
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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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lod-scheduler-B0a_uBlv.js","sources":["../src/lib/lod-scheduler.ts"],"sourcesContent":["import * as THREE from 'three/webgpu';\nimport type { LodLeaf, LodManifest } from './lod-manifest';\nimport type { LodSource } from './lod-source';\n\n/**\n * A contiguous splat range within one chunk file, at one LOD level - the\n * unit the pool activates. Adjacent manifest leaves that resolve to the same\n * `(file, level)` and whose ranges abut are coalesced into one run, so the\n * pool sees a handful of large ranges instead of thousands of tiny leaves.\n */\nexport interface LodRun {\n readonly file: number;\n readonly level: number;\n readonly offset: number;\n readonly count: number;\n /** First manifest-leaf index this run covers. */\n readonly leafStart: number;\n /** One past the last covered leaf index. */\n readonly leafEnd: number;\n /**\n * Physical-coverage identity when several manifest leaves form one region.\n * A classic LCC cell may be split into fetch-sized leaves, but its slices\n * must still swap as one visible unit. Undefined retains interval-based\n * grouping for hierarchical sources.\n */\n readonly coverageGroup?: number;\n /**\n * Camera distance (mesh-local) of the nearest leaf in this run, from the\n * last {@link LodScheduler.computeDesiredRuns} pass. Streamed meshes use it\n * to fetch near detail before far coarse coverage. Absent on sources that\n * do not track per-leaf distance.\n */\n readonly distance?: number;\n /**\n * True when any leaf in this run intersects the view frustum (with the\n * scheduler's edge margin). Used to rank classic-path fetches so in-view\n * detail beats behind-camera work; absent when the source does not track it.\n */\n readonly inView?: boolean;\n /**\n * Fetch-only angular distance from the camera's forward axis. Smaller values\n * are nearer the screen centre. It never affects LOD selection or budget.\n */\n readonly screenImportance?: number;\n}\n\n/** Stable identity of a run, for diffing desired vs. resident sets. */\nexport function runKey(run: LodRun): string {\n return `${run.file}:${run.level}:${run.offset}:${run.count}`;\n}\n\n/** Sentinel level meaning \"this leaf is currently dropped (renders nothing)\". */\nconst DROPPED = -1;\n\n/**\n * Maximum splats in one run. Large contiguous regions are split into several\n * runs so no single pool append (and thus no single frame) does an unbounded\n * amount of copy + upload work. Split runs stay contiguous, so they still\n * upload as one coalesced rectangle.\n */\nconst MAX_RUN_SPLATS = 128_000;\n\n/** Hysteresis dead-band around each LOD distance threshold. */\nconst THRESHOLD_MARGIN = 0.1;\n/** Minimum time a leaf must hold a level before changing again, ms. */\nconst DWELL_MS = 500;\n/** Frustum test margin, as a fraction of each leaf's own size. */\nconst FRUSTUM_MARGIN = 0.1;\n/**\n * Out-of-frustum leaves act this many times farther away instead of being\n * hard-coarsened. Rotating the camera then re-ranks leaves smoothly (and\n * dwell applies), rather than mass-flipping edge leaves every frame -\n * PlayCanvas uses the same behind-camera-penalty idea.\n */\nconst FRUSTUM_PENALTY = 3;\n\n/**\n * Chooses a level of detail per spatial leaf from the camera, keeps the\n * total within a splat budget, and coalesces the result into runs.\n *\n * The scheduler is pure with respect to rendering: it takes a camera\n * position and frustum (both in the mesh's local space) and returns the set\n * of runs that should be resident. It owns only the small amount of state\n * needed for temporal stability (each leaf's current level and when it last\n * changed), so it is straightforward to drive from a test harness.\n *\n * LOD distance model (PlayCanvas-compatible): level 0 is used within\n * `lodBaseDistance`; each successive level covers a band `lodMultiplier`×\n * farther out. By default out-of-frustum leaves act {@link FRUSTUM_PENALTY}×\n * farther away. Formats with broad spatial cells can disable that bias so an\n * orbit does not churn already-refined cells at the frustum edges.\n *\n * The distance model only sets the *floor* and the *priority*: when the\n * distance-chosen set leaves budget unused, {@link fillBudget} promotes\n * in-frustum leaves nearest-first until its configured headroom target is\n * spent (90% by default). Formats can cap that optional refinement and avoid\n * a full-finest cut even when an explicitly large budget could hold it. The\n * budget\n * therefore decides how far refinement reaches - a desktop budget yields\n * full detail even with the whole scene in view, a mobile budget refines\n * only near the camera.\n */\nexport class LodScheduler implements LodSource {\n /** World-unit distance inside which the finest level (0) is used. */\n lodBaseDistance: number;\n /** Distance ratio between successive LOD levels. */\n lodMultiplier: number;\n /** Maximum active splats; the desired set is demoted to fit. */\n budget: number;\n\n private readonly leaves: readonly LodLeaf[];\n private readonly coarsest: number;\n /** Per leaf: lowest/highest LOD level that has data. */\n private readonly minLevel: Int32Array;\n private readonly maxLevel: Int32Array;\n /**\n * Per leaf: the distance *ambition* - the finest level the camera distance\n * asks for, with hysteresis/dwell applied. This is the hysteresis *state*:\n * only {@link selectLevels} writes it, so budget demotion can never feed\n * back into the dead-band/dwell logic and oscillate. With `fillPastDistance`\n * false, {@link fillBudget} restores {@link resolved} only up to here.\n */\n private readonly level: Int32Array;\n /**\n * Per leaf: the resolved target - the ambition capped to fit the budget by\n * {@link enforceBudget}, then refilled from leftover headroom by\n * {@link fillBudget} (or DROPPED). This is what {@link coalesceRuns} emits.\n */\n private readonly resolved: Int32Array;\n /** Per leaf: timestamp of the last distance-level change, for dwell. */\n private readonly changedAt: Float64Array;\n\n private readonly distance: Float64Array;\n private readonly inFrustum: Uint8Array;\n /** Leaves that must move through the budget cut together. */\n private readonly budgetGroups: readonly Uint32Array[];\n /** Per-leaf physical coverage group, when the manifest defines one. */\n private readonly coverageGroups: Int32Array;\n /** Whether view-frustum membership affects distance and fill priority. */\n private readonly frustumAware: boolean;\n /** Fraction of the usable budget that optional refinement may consume. */\n private readonly budgetFillFraction: number;\n /** Absolute cap on optional refinement; distance-selected detail may exceed it. */\n private readonly budgetFillCap: number;\n /** Whether a budget that holds every finest leaf forces a full-finest cut. */\n private readonly forceFinestWhenFits: boolean;\n /**\n * Leaves at or inside this camera distance always resolve to their finest\n * available level (no hysteresis climb). Classic LCC uses a multi-band\n * radius so adjacent broad cells do not paint coarse discs at startup.\n */\n private readonly forceFinestWithin: number | undefined;\n /**\n * When false, {@link fillBudget} only restores toward each leaf's\n * distance-selected {@link level} - never finer. Classic LCC sets this so\n * a mid-range cell does not fetch L0 only to demote to L1 once the cut\n * settles.\n */\n private readonly fillPastDistance: boolean;\n\n /**\n * What the last cut actually decided, for the HUD.\n *\n * The resident splat count alone cannot distinguish \"the scheduler asked for\n * this\" from \"the scheduler asked for more and the mesh could not apply it\",\n * and those have opposite fixes. Measured rather than inferred after a\n * zoomed-out `sandwijck` sat at exactly its coarsest total (190,730) with a\n * 600k budget - 68% of the budget unspent, with no way to see which stage\n * declined to spend it.\n */\n readonly stats = {\n /** Leaves the frustum test accepted - `fillBudget`'s candidate set. */\n inFrustum: 0,\n /** Total leaves in the tree. */\n leaves: 0,\n /** Splats implied by the resolved cut, before the mesh applies anything. */\n desired: 0,\n /** Splats `fillBudget` added on top of the distance-chosen levels. */\n filled: 0,\n };\n\n private readonly scratchBox = new THREE.Box3();\n private readonly scratchSize = new THREE.Vector3();\n private readonly scratchCenter = new THREE.Vector3();\n private readonly screenImportance: Float32Array;\n /** Whether the last selection pass received a camera-forward vector. */\n private hasScreenImportance = false;\n /**\n * Persistent leaf-index ordering scratch for {@link enforceBudget} and\n * {@link fillBudget}, sorted in place per call instead of allocating a\n * fresh `[...keys]` array. Comparators break ties on the index itself so\n * the order is identical to the stable `Array#sort` this replaces\n * (`TypedArray#sort` stability is not guaranteed).\n */\n private readonly orderScratch: Uint32Array;\n\n constructor(\n manifest: LodManifest,\n options: {\n budget: number;\n lodBaseDistance: number;\n lodMultiplier: number;\n frustumAware?: boolean;\n budgetFillFraction?: number;\n budgetFillCap?: number;\n forceFinestWhenFits?: boolean;\n forceFinestWithin?: number;\n /**\n * When false, leftover-budget fill never refines past the distance-\n * selected level (see {@link fillPastDistance}). Default true.\n */\n fillPastDistance?: boolean;\n },\n ) {\n this.leaves = manifest.leaves;\n this.coarsest = manifest.lodLevels - 1;\n this.budget = options.budget;\n this.lodBaseDistance = options.lodBaseDistance;\n this.lodMultiplier = options.lodMultiplier;\n this.frustumAware = options.frustumAware !== false;\n this.budgetFillFraction = options.budgetFillFraction ?? 0.9;\n this.budgetFillCap = options.budgetFillCap ?? Number.POSITIVE_INFINITY;\n this.forceFinestWhenFits = options.forceFinestWhenFits !== false;\n this.forceFinestWithin = options.forceFinestWithin;\n this.fillPastDistance = options.fillPastDistance !== false;\n if (this.budgetFillFraction <= 0 || this.budgetFillFraction > 1) {\n throw new RangeError('LodScheduler budgetFillFraction must be in (0, 1].');\n }\n if (!(this.budgetFillCap > 0)) {\n throw new RangeError('LodScheduler budgetFillCap must be positive.');\n }\n if (this.forceFinestWithin !== undefined && !(this.forceFinestWithin >= 0)) {\n throw new RangeError('LodScheduler forceFinestWithin must be >= 0.');\n }\n\n const n = this.leaves.length;\n this.minLevel = new Int32Array(n);\n this.maxLevel = new Int32Array(n);\n this.level = new Int32Array(n).fill(this.coarsest);\n this.resolved = new Int32Array(n).fill(this.coarsest);\n this.changedAt = new Float64Array(n);\n this.distance = new Float64Array(n);\n this.inFrustum = new Uint8Array(n);\n this.screenImportance = new Float32Array(n);\n this.orderScratch = new Uint32Array(n);\n this.coverageGroups = new Int32Array(n).fill(-1);\n\n const groups: number[][] = [];\n const explicitGroups = new Map<number, number>();\n for (let i = 0; i < n; i++) {\n const key = (this.leaves[i] as LodLeaf).budgetGroup;\n if (key === undefined) {\n groups.push([i]);\n continue;\n }\n let groupIndex = explicitGroups.get(key);\n if (groupIndex === undefined) {\n groupIndex = groups.length;\n explicitGroups.set(key, groupIndex);\n groups.push([]);\n }\n (groups[groupIndex] as number[]).push(i);\n this.coverageGroups[i] = key;\n }\n this.budgetGroups = groups.map((members) => Uint32Array.from(members));\n\n for (let i = 0; i < n; i++) {\n const lods = (this.leaves[i] as LodLeaf).lods;\n let min = this.coarsest;\n let max = 0;\n for (let l = 0; l < lods.length; l++) {\n if (lods[l] === undefined) continue;\n min = Math.min(min, l);\n max = Math.max(max, l);\n }\n this.minLevel[i] = min;\n this.maxLevel[i] = max;\n this.level[i] = max; // start coarse\n }\n }\n\n /**\n * Recomputes the desired resident run set for the current camera.\n *\n * @param cameraLocal - Camera position in the mesh's local space.\n * @param frustum - View frustum in the mesh's local space.\n * @param now - Monotonic timestamp (ms) for dwell hysteresis.\n */\n computeDesiredRuns(\n cameraLocal: THREE.Vector3,\n frustum: THREE.Frustum,\n now: number,\n cameraForward?: THREE.Vector3,\n ): LodRun[] {\n this.selectLevels(cameraLocal, frustum, now, cameraForward);\n // When the budget already holds every leaf's finest level, skip distance /\n // frustum demotion entirely. Otherwise a small rotate marks leaves\n // out-of-frustum (×{@link FRUSTUM_PENALTY}), coarsens them, and the mesh\n // aborts fine fetches / swaps to coarse LCC discs - then has to climb\n // back when they re-enter the view.\n if (this.forceFinestWhenFits && this.budget >= this.finestTotal()) {\n for (let i = 0; i < this.leaves.length; i++) {\n this.resolved[i] = this.minLevel[i] as number;\n }\n this.stats.desired = this.activeTotal();\n this.stats.filled = 0;\n return this.coalesceRuns();\n }\n // Copy the ambition into the resolved target, cap it to the budget, then\n // refill leftover headroom. Budget stages touch only `resolved`, never the\n // ambition/hysteresis state - a stationary camera therefore resolves the\n // same cut every pass instead of oscillating red↔orange.\n this.resolved.set(this.level);\n this.enforceBudget();\n const beforeFill = this.activeTotal();\n this.fillBudget();\n this.stats.desired = this.activeTotal();\n this.stats.filled = this.stats.desired - beforeFill;\n return this.coalesceRuns();\n }\n\n /** Total active splats implied by the current resolved levels. */\n private activeTotal(): number {\n let total = 0;\n for (let i = 0; i < this.leaves.length; i++) {\n const level = this.resolved[i] as number;\n if (level === DROPPED) continue;\n total += (this.leaves[i] as LodLeaf).lods[level]?.count ?? 0;\n }\n return total;\n }\n\n /** Splats if every leaf sat at its finest available level (none dropped). */\n private finestTotal(): number {\n let total = 0;\n for (let i = 0; i < this.leaves.length; i++) {\n const level = this.minLevel[i] as number;\n total += (this.leaves[i] as LodLeaf).lods[level]?.count ?? 0;\n }\n return total;\n }\n\n private selectLevels(\n cameraLocal: THREE.Vector3,\n frustum: THREE.Frustum,\n now: number,\n cameraForward?: THREE.Vector3,\n ): void {\n const { lodBaseDistance: base, lodMultiplier: m } = this;\n this.hasScreenImportance = cameraForward !== undefined;\n let visibleCount = 0;\n this.stats.leaves = this.leaves.length;\n for (let i = 0; i < this.leaves.length; i++) {\n const leaf = this.leaves[i] as LodLeaf;\n const d = leaf.bounds.distanceToPoint(cameraLocal);\n this.distance[i] = d;\n\n // Widen the box by a fraction of its own size for edge stability.\n this.scratchBox.copy(leaf.bounds);\n leaf.bounds.getSize(this.scratchSize);\n this.scratchBox.expandByScalar(this.scratchSize.length() * FRUSTUM_MARGIN);\n const visible = frustum.intersectsBox(this.scratchBox);\n this.inFrustum[i] = visible ? 1 : 0;\n if (visible) visibleCount++;\n\n // Broad classic-LCC tiles frequently all intersect the frustum, so the\n // boolean above cannot tell the centre of the screen from its edges.\n // This is deliberately fetch metadata only: distance remains the source\n // of truth for the resolved LOD and the budget cut.\n if (cameraForward) {\n leaf.bounds.getCenter(this.scratchCenter).sub(cameraLocal);\n const depth = this.scratchCenter.dot(cameraForward);\n const lateralSquared = Math.max(0, this.scratchCenter.lengthSq() - depth * depth);\n this.screenImportance[i] =\n depth > 0 ? Math.sqrt(lateralSquared) / Math.max(depth, 0.25) : Number.POSITIVE_INFINITY;\n } else {\n this.screenImportance[i] = 0;\n }\n\n const current = this.level[i] as number;\n // Blanket force-finest only when fill may still climb past the distance\n // band (octree / page-table). Classic LCC never uses this path.\n if (\n this.fillPastDistance &&\n this.forceFinestWithin !== undefined &&\n d <= this.forceFinestWithin\n ) {\n const finest = this.minLevel[i] as number;\n if (current !== finest) {\n this.level[i] = finest;\n this.changedAt[i] = now;\n }\n continue;\n }\n\n const effectiveDistance = !this.frustumAware || visible ? d : d * FRUSTUM_PENALTY;\n\n // Classic LCC: the ambition tracks the pure distance band only. Budget\n // demotions live in `resolved`, never here, so they cannot feed back\n // into hysteresis. A cold cell snaps straight to its band (no dwell\n // climb through intermediate LODs); afterward only a band *crossing*\n // moves the ambition - a stationary camera in the same band holds,\n // even if the budget just demoted its resolved level.\n if (!this.fillPastDistance) {\n const band = this.distanceBandLevel(i, effectiveDistance, base, m);\n const coldStart =\n (this.changedAt[i] as number) === 0 && current === (this.maxLevel[i] as number);\n if (coldStart) {\n if (band !== current) {\n this.level[i] = band;\n this.changedAt[i] = now;\n }\n continue;\n }\n if (band === current) continue;\n const proposed = this.hysteresisLevel(i, effectiveDistance, base, m, current);\n if (proposed === current) continue;\n // Coarsen immediately (moving away should not linger over budget);\n // only refinement waits out the dwell window.\n const coarsening = proposed > current;\n if (coarsening || now - (this.changedAt[i] as number) >= DWELL_MS) {\n this.level[i] = proposed;\n this.changedAt[i] = now;\n }\n continue;\n }\n\n const proposed = this.hysteresisLevel(i, effectiveDistance, base, m, current);\n if (proposed === current) continue;\n const coldSnap =\n (this.changedAt[i] as number) === 0 && current === (this.maxLevel[i] as number);\n if (coldSnap || now - (this.changedAt[i] as number) >= DWELL_MS) {\n this.level[i] = proposed;\n this.changedAt[i] = now;\n }\n }\n this.stats.inFrustum = visibleCount;\n }\n\n /**\n * Pure distance→level band (independent of the leaf's current level), so a\n * classic LCC cut never has to dwell through intermediate rungs.\n */\n private distanceBandLevel(index: number, d: number, base: number, m: number): number {\n const min = this.minLevel[index] as number;\n const max = this.maxLevel[index] as number;\n let level = min;\n while (level < max && d > base * m ** level * (1 + THRESHOLD_MARGIN)) level++;\n return this.resolveAvailable(index, level);\n }\n\n /**\n * Stable level from distance: coarsen only past `threshold·(1 + margin)`,\n * refine only within `threshold·(1 − margin)`. The available-level clamp\n * keeps leaves that lack a level on their nearest coarser one.\n */\n private hysteresisLevel(\n index: number,\n d: number,\n base: number,\n m: number,\n current: number,\n ): number {\n const min = this.minLevel[index] as number;\n const max = this.maxLevel[index] as number;\n let level = Math.min(max, Math.max(min, current));\n // threshold(L) = base·m^L is the boundary between level L and L+1.\n while (level < max && d > base * m ** level * (1 + THRESHOLD_MARGIN)) level++;\n while (level > min && d <= base * m ** (level - 1) * (1 - THRESHOLD_MARGIN)) level--;\n // Resolve to the nearest available level (prefer coarser) if this exact\n // one has no data (leaves need not carry every level).\n return this.resolveAvailable(index, level);\n }\n\n private resolveAvailable(index: number, target: number): number {\n const lods = (this.leaves[index] as LodLeaf).lods;\n for (let l = target; l <= (this.maxLevel[index] as number); l++) if (lods[l]) return l;\n for (let l = target - 1; l >= (this.minLevel[index] as number); l--) if (lods[l]) return l;\n return this.maxLevel[index] as number;\n }\n\n /** Demotes/drops lowest-priority leaves until the total fits the budget. */\n private enforceBudget(): void {\n let total = this.activeTotal();\n if (total <= this.budget) return;\n\n // Demote lowest priority first. When frustum-aware: out-of-frustum before\n // in-frustum, then farthest. Classic LCC sets frustumAware false - broad\n // XY cells often sit mostly behind the camera while the user stands in\n // them (d≈0); frustum demotion would coarsen that cell first and let\n // thinner cells ahead keep fine detail (backwards load). Distance only.\n const order = this.orderScratch.subarray(0, this.budgetGroups.length);\n for (let i = 0; i < order.length; i++) order[i] = i;\n order.sort((a, b) => {\n const ia = (this.budgetGroups[a] as Uint32Array)[0] as number;\n const ib = (this.budgetGroups[b] as Uint32Array)[0] as number;\n if (this.frustumAware) {\n const fa = this.inFrustum[ia] as number;\n const fb = this.inFrustum[ib] as number;\n if (fa !== fb) return fa - fb; // out-of-frustum (0) first\n }\n return (this.distance[ib] as number) - (this.distance[ia] as number) || ia - ib;\n });\n\n // Pass 1: coarsen toward the coarsest available level.\n for (const groupIndex of order) {\n const group = this.budgetGroups[groupIndex] as Uint32Array;\n while (total > this.budget) {\n let reduction = 0;\n let changed = false;\n for (const i of group) {\n const current = this.resolved[i] as number;\n if (current >= (this.maxLevel[i] as number)) continue;\n const lods = (this.leaves[i] as LodLeaf).lods;\n const nextLevel = this.resolveAvailable(i, current + 1);\n if (nextLevel === current) continue;\n reduction += (lods[current]?.count ?? 0) - (lods[nextLevel]?.count ?? 0);\n this.resolved[i] = nextLevel;\n changed = true;\n }\n if (!changed) break;\n total -= reduction;\n }\n if (total <= this.budget) return;\n }\n\n // Pass 2 (last resort): drop groups entirely, lowest priority first.\n for (const groupIndex of order) {\n if (total <= this.budget) return;\n for (const i of this.budgetGroups[groupIndex] as Uint32Array) {\n if ((this.resolved[i] as number) === DROPPED) continue;\n total -= (this.leaves[i] as LodLeaf).lods[this.resolved[i] as number]?.count ?? 0;\n this.resolved[i] = DROPPED;\n }\n }\n }\n\n /**\n * Uses leftover budget to restore detail after {@link enforceBudget}\n * demoted leaves, nearest first, while the total stays under the fill\n * target. By default this may also refine *past* the distance-chosen level\n * when headroom remains (octree / page-table streams). With\n * {@link fillPastDistance} false (classic LCC), fill never goes finer than\n * each leaf's distance-selected level - so a cell at the L1 band does not\n * fetch L0 only to demote moments later with the camera still.\n */\n private fillBudget(): void {\n const target =\n this.forceFinestWhenFits && this.budget >= this.finestTotal()\n ? this.budget\n : Math.min(this.budget, this.budgetFillCap) * this.budgetFillFraction;\n let total = this.activeTotal();\n if (total >= target) return;\n\n // Candidate in-frustum groups, nearest first (ties keep manifest order),\n // packed into the front of the shared index scratch.\n let candidateCount = 0;\n for (let groupIndex = 0; groupIndex < this.budgetGroups.length; groupIndex++) {\n const first = (this.budgetGroups[groupIndex] as Uint32Array)[0] as number;\n if (\n (!this.frustumAware || this.inFrustum[first] === 1) &&\n (this.resolved[first] as number) !== DROPPED\n ) {\n this.orderScratch[candidateCount++] = groupIndex;\n }\n }\n const order = this.orderScratch.subarray(0, candidateCount);\n order.sort((a, b) => {\n const ia = (this.budgetGroups[a] as Uint32Array)[0] as number;\n const ib = (this.budgetGroups[b] as Uint32Array)[0] as number;\n return (this.distance[ia] as number) - (this.distance[ib] as number) || ia - ib;\n });\n\n // Nearest groups refine first (up to the distance floor when capped).\n for (const groupIndex of order) {\n const group = this.budgetGroups[groupIndex] as Uint32Array;\n while (true) {\n let growth = 0;\n let canPromote = false;\n for (const i of group) {\n const current = this.resolved[i] as number;\n const floor = this.fillPastDistance\n ? (this.minLevel[i] as number)\n : (this.level[i] as number);\n if (current <= floor) continue;\n const leaf = this.leaves[i] as LodLeaf;\n let finer = current - 1;\n while (finer > floor && !leaf.lods[finer]) finer--;\n if (finer < floor || !leaf.lods[finer]) continue;\n growth += (leaf.lods[finer]?.count ?? 0) - (leaf.lods[current]?.count ?? 0);\n canPromote = true;\n }\n if (!canPromote || total + growth > target) break;\n for (const i of group) {\n const current = this.resolved[i] as number;\n const floor = this.fillPastDistance\n ? (this.minLevel[i] as number)\n : (this.level[i] as number);\n if (current <= floor) continue;\n const leaf = this.leaves[i] as LodLeaf;\n let finer = current - 1;\n while (finer > floor && !leaf.lods[finer]) finer--;\n if (finer >= floor && leaf.lods[finer]) this.resolved[i] = finer;\n }\n total += growth;\n }\n }\n }\n\n private coalesceRuns(): LodRun[] {\n return this.buildRuns(0, this.leaves.length, (i) => this.resolved[i] as number);\n }\n\n /**\n * Runs covering [from, to) at each leaf's coarsest available level - used\n * to substitute always-cached coverage while a finer level is fetching.\n */\n coarsestRunsFor(from: number, to: number): LodRun[] {\n return this.buildRuns(from, to, (i) => this.maxLevel[i] as number);\n }\n\n /**\n * Runs covering [from, to) at `level`, clamped per leaf to an available rung\n * (prefer the requested level, else the next coarser, else the next finer).\n */\n runsAtLevelFor(from: number, to: number, level: number): LodRun[] {\n const wanted = Math.floor(level);\n return this.buildRuns(from, to, (i) => this.clampLeafLevel(i, wanted));\n }\n\n /** Prefer `wanted`, else next coarser, else next finer; {@link DROPPED} if none. */\n private clampLeafLevel(leafIndex: number, wanted: number): number {\n const leaf = this.leaves[leafIndex] as LodLeaf;\n if (leaf.lods[wanted]) return wanted;\n for (let l = wanted + 1; l < leaf.lods.length; l++) {\n if (leaf.lods[l]) return l;\n }\n for (let l = wanted - 1; l >= 0; l--) {\n if (leaf.lods[l]) return l;\n }\n return DROPPED;\n }\n\n /** Coalesces leaves [from, to) at `levelOf(leaf)` into contiguous runs. */\n private buildRuns(from: number, to: number, levelOf: (index: number) => number): LodRun[] {\n const runs: LodRun[] = [];\n let file = -1;\n let level = -1;\n let offset = 0;\n let count = 0;\n let leafStart = -1;\n let leafEnd = -1;\n let coverageGroup = -1;\n let distance = Number.POSITIVE_INFINITY;\n let inView = false;\n let screenImportance = Number.POSITIVE_INFINITY;\n const flush = (): void => {\n if (count > 0) {\n runs.push({\n file,\n level,\n offset,\n count,\n leafStart,\n leafEnd,\n distance,\n inView,\n ...(this.hasScreenImportance ? { screenImportance } : {}),\n ...(coverageGroup >= 0 ? { coverageGroup } : {}),\n });\n }\n count = 0;\n };\n\n for (let i = from; i < to; i++) {\n const l = levelOf(i);\n if (l === DROPPED) {\n flush();\n continue;\n }\n const range = (this.leaves[i] as LodLeaf).lods[l];\n if (!range) {\n flush();\n continue;\n }\n const leafCoverageGroup = this.coverageGroups[i] as number;\n const leafDistance = this.distance[i] as number;\n const leafInView = (this.inFrustum[i] as number) === 1;\n if (\n count > 0 &&\n range.file === file &&\n l === level &&\n leafCoverageGroup === coverageGroup &&\n range.offset === offset + count &&\n count + range.count <= MAX_RUN_SPLATS\n ) {\n count += range.count; // extend the current run\n leafEnd = i + 1;\n if (leafDistance < distance) distance = leafDistance;\n if (leafInView) inView = true;\n screenImportance = Math.min(screenImportance, this.screenImportance[i] as number);\n } else {\n flush();\n file = range.file;\n level = l;\n offset = range.offset;\n count = range.count;\n leafStart = i;\n leafEnd = i + 1;\n coverageGroup = leafCoverageGroup;\n distance = leafDistance;\n inView = leafInView;\n screenImportance = this.screenImportance[i] as number;\n }\n }\n flush();\n return runs;\n }\n}\n"],"names":["runKey","run","DROPPED","MAX_RUN_SPLATS","THRESHOLD_MARGIN","DWELL_MS","FRUSTUM_MARGIN","FRUSTUM_PENALTY","LodScheduler","manifest","options","__publicField","THREE","n","groups","explicitGroups","i","key","groupIndex","members","lods","min","max","l","cameraLocal","frustum","now","cameraForward","beforeFill","total","level","_a","base","m","visibleCount","leaf","d","visible","depth","lateralSquared","current","finest","effectiveDistance","band","proposed","index","target","order","a","b","ia","ib","fa","fb","group","reduction","changed","nextLevel","_b","_c","candidateCount","first","growth","canPromote","floor","finer","from","to","wanted","leafIndex","levelOf","runs","file","offset","count","leafStart","leafEnd","coverageGroup","distance","inView","screenImportance","flush","range","leafCoverageGroup","leafDistance","leafInView"],"mappings":";;;;AA+CO,SAASA,EAAOC,GAAqB;AAC1C,SAAO,GAAGA,EAAI,IAAI,IAAIA,EAAI,KAAK,IAAIA,EAAI,MAAM,IAAIA,EAAI,KAAK;AAC5D;AAGA,MAAMC,IAAU,IAQVC,IAAiB,OAGjBC,IAAmB,KAEnBC,IAAW,KAEXC,IAAiB,KAOjBC,IAAkB;AA4BjB,MAAMC,EAAkC;AAAA,EA8F7C,YACEC,GACAC,GAeA;AA7GF;AAAA,IAAAC,EAAA;AAEA;AAAA,IAAAA,EAAA;AAEA;AAAA,IAAAA,EAAA;AAEiB,IAAAA,EAAA;AACA,IAAAA,EAAA;AAEA;AAAA,IAAAA,EAAA;AACA,IAAAA,EAAA;AAQA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAAAA,EAAA;AAMA;AAAA;AAAA;AAAA;AAAA;AAAA,IAAAA,EAAA;AAEA;AAAA,IAAAA,EAAA;AAEA,IAAAA,EAAA;AACA,IAAAA,EAAA;AAEA;AAAA,IAAAA,EAAA;AAEA;AAAA,IAAAA,EAAA;AAEA;AAAA,IAAAA,EAAA;AAEA;AAAA,IAAAA,EAAA;AAEA;AAAA,IAAAA,EAAA;AAEA;AAAA,IAAAA,EAAA;AAMA;AAAA;AAAA;AAAA;AAAA;AAAA,IAAAA,EAAA;AAOA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAAAA,EAAA;AAYR;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAAAA,EAAA,eAAQ;AAAA;AAAA,MAEf,WAAW;AAAA;AAAA,MAEX,QAAQ;AAAA;AAAA,MAER,SAAS;AAAA;AAAA,MAET,QAAQ;AAAA,IAAA;AAGO,IAAAA,EAAA,oBAAa,IAAIC,EAAM,KAAA;AACvB,IAAAD,EAAA,qBAAc,IAAIC,EAAM,QAAA;AACxB,IAAAD,EAAA,uBAAgB,IAAIC,EAAM,QAAA;AAC1B,IAAAD,EAAA;AAET;AAAA,IAAAA,EAAA,6BAAsB;AAQb;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAAAA,EAAA;AA+Bf,QAXA,KAAK,SAASF,EAAS,QACvB,KAAK,WAAWA,EAAS,YAAY,GACrC,KAAK,SAASC,EAAQ,QACtB,KAAK,kBAAkBA,EAAQ,iBAC/B,KAAK,gBAAgBA,EAAQ,eAC7B,KAAK,eAAeA,EAAQ,iBAAiB,IAC7C,KAAK,qBAAqBA,EAAQ,sBAAsB,KACxD,KAAK,gBAAgBA,EAAQ,iBAAiB,OAAO,mBACrD,KAAK,sBAAsBA,EAAQ,wBAAwB,IAC3D,KAAK,oBAAoBA,EAAQ,mBACjC,KAAK,mBAAmBA,EAAQ,qBAAqB,IACjD,KAAK,sBAAsB,KAAK,KAAK,qBAAqB;AAC5D,YAAM,IAAI,WAAW,oDAAoD;AAE3E,QAAI,EAAE,KAAK,gBAAgB;AACzB,YAAM,IAAI,WAAW,8CAA8C;AAErE,QAAI,KAAK,sBAAsB,UAAa,EAAE,KAAK,qBAAqB;AACtE,YAAM,IAAI,WAAW,8CAA8C;AAGrE,UAAMG,IAAI,KAAK,OAAO;AACtB,SAAK,WAAW,IAAI,WAAWA,CAAC,GAChC,KAAK,WAAW,IAAI,WAAWA,CAAC,GAChC,KAAK,QAAQ,IAAI,WAAWA,CAAC,EAAE,KAAK,KAAK,QAAQ,GACjD,KAAK,WAAW,IAAI,WAAWA,CAAC,EAAE,KAAK,KAAK,QAAQ,GACpD,KAAK,YAAY,IAAI,aAAaA,CAAC,GACnC,KAAK,WAAW,IAAI,aAAaA,CAAC,GAClC,KAAK,YAAY,IAAI,WAAWA,CAAC,GACjC,KAAK,mBAAmB,IAAI,aAAaA,CAAC,GAC1C,KAAK,eAAe,IAAI,YAAYA,CAAC,GACrC,KAAK,iBAAiB,IAAI,WAAWA,CAAC,EAAE,KAAK,EAAE;AAE/C,UAAMC,IAAqB,CAAA,GACrBC,wBAAqB,IAAA;AAC3B,aAASC,IAAI,GAAGA,IAAIH,GAAGG,KAAK;AAC1B,YAAMC,IAAO,KAAK,OAAOD,CAAC,EAAc;AACxC,UAAIC,MAAQ,QAAW;AACrB,QAAAH,EAAO,KAAK,CAACE,CAAC,CAAC;AACf;AAAA,MACF;AACA,UAAIE,IAAaH,EAAe,IAAIE,CAAG;AACvC,MAAIC,MAAe,WACjBA,IAAaJ,EAAO,QACpBC,EAAe,IAAIE,GAAKC,CAAU,GAClCJ,EAAO,KAAK,EAAE,IAEfA,EAAOI,CAAU,EAAe,KAAKF,CAAC,GACvC,KAAK,eAAeA,CAAC,IAAIC;AAAA,IAC3B;AACA,SAAK,eAAeH,EAAO,IAAI,CAACK,MAAY,YAAY,KAAKA,CAAO,CAAC;AAErE,aAASH,IAAI,GAAGA,IAAIH,GAAGG,KAAK;AAC1B,YAAMI,IAAQ,KAAK,OAAOJ,CAAC,EAAc;AACzC,UAAIK,IAAM,KAAK,UACXC,IAAM;AACV,eAASC,IAAI,GAAGA,IAAIH,EAAK,QAAQG;AAC/B,QAAIH,EAAKG,CAAC,MAAM,WAChBF,IAAM,KAAK,IAAIA,GAAKE,CAAC,GACrBD,IAAM,KAAK,IAAIA,GAAKC,CAAC;AAEvB,WAAK,SAASP,CAAC,IAAIK,GACnB,KAAK,SAASL,CAAC,IAAIM,GACnB,KAAK,MAAMN,CAAC,IAAIM;AAAA,IAClB;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,mBACEE,GACAC,GACAC,GACAC,GACU;AAOV,QANA,KAAK,aAAaH,GAAaC,GAASC,GAAKC,CAAa,GAMtD,KAAK,uBAAuB,KAAK,UAAU,KAAK,eAAe;AACjE,eAASX,IAAI,GAAGA,IAAI,KAAK,OAAO,QAAQA;AACtC,aAAK,SAASA,CAAC,IAAI,KAAK,SAASA,CAAC;AAEpC,kBAAK,MAAM,UAAU,KAAK,YAAA,GAC1B,KAAK,MAAM,SAAS,GACb,KAAK,aAAA;AAAA,IACd;AAKA,SAAK,SAAS,IAAI,KAAK,KAAK,GAC5B,KAAK,cAAA;AACL,UAAMY,IAAa,KAAK,YAAA;AACxB,gBAAK,WAAA,GACL,KAAK,MAAM,UAAU,KAAK,YAAA,GAC1B,KAAK,MAAM,SAAS,KAAK,MAAM,UAAUA,GAClC,KAAK,aAAA;AAAA,EACd;AAAA;AAAA,EAGQ,cAAsB;;AAC5B,QAAIC,IAAQ;AACZ,aAASb,IAAI,GAAGA,IAAI,KAAK,OAAO,QAAQA,KAAK;AAC3C,YAAMc,IAAQ,KAAK,SAASd,CAAC;AAC7B,MAAIc,MAAU5B,MACd2B,OAAUE,IAAA,KAAK,OAAOf,CAAC,EAAc,KAAKc,CAAK,MAArC,gBAAAC,EAAwC,UAAS;AAAA,IAC7D;AACA,WAAOF;AAAA,EACT;AAAA;AAAA,EAGQ,cAAsB;;AAC5B,QAAIA,IAAQ;AACZ,aAASb,IAAI,GAAGA,IAAI,KAAK,OAAO,QAAQA,KAAK;AAC3C,YAAMc,IAAQ,KAAK,SAASd,CAAC;AAC7B,MAAAa,OAAUE,IAAA,KAAK,OAAOf,CAAC,EAAc,KAAKc,CAAK,MAArC,gBAAAC,EAAwC,UAAS;AAAA,IAC7D;AACA,WAAOF;AAAA,EACT;AAAA,EAEQ,aACNL,GACAC,GACAC,GACAC,GACM;AACN,UAAM,EAAE,iBAAiBK,GAAM,eAAeC,MAAM;AACpD,SAAK,sBAAsBN,MAAkB;AAC7C,QAAIO,IAAe;AACnB,SAAK,MAAM,SAAS,KAAK,OAAO;AAChC,aAASlB,IAAI,GAAGA,IAAI,KAAK,OAAO,QAAQA,KAAK;AAC3C,YAAMmB,IAAO,KAAK,OAAOnB,CAAC,GACpBoB,IAAID,EAAK,OAAO,gBAAgBX,CAAW;AACjD,WAAK,SAASR,CAAC,IAAIoB,GAGnB,KAAK,WAAW,KAAKD,EAAK,MAAM,GAChCA,EAAK,OAAO,QAAQ,KAAK,WAAW,GACpC,KAAK,WAAW,eAAe,KAAK,YAAY,OAAA,IAAW7B,CAAc;AACzE,YAAM+B,IAAUZ,EAAQ,cAAc,KAAK,UAAU;AAQrD,UAPA,KAAK,UAAUT,CAAC,IAAIqB,IAAU,IAAI,GAC9BA,KAASH,KAMTP,GAAe;AACjB,QAAAQ,EAAK,OAAO,UAAU,KAAK,aAAa,EAAE,IAAIX,CAAW;AACzD,cAAMc,IAAQ,KAAK,cAAc,IAAIX,CAAa,GAC5CY,IAAiB,KAAK,IAAI,GAAG,KAAK,cAAc,SAAA,IAAaD,IAAQA,CAAK;AAChF,aAAK,iBAAiBtB,CAAC,IACrBsB,IAAQ,IAAI,KAAK,KAAKC,CAAc,IAAI,KAAK,IAAID,GAAO,IAAI,IAAI,OAAO;AAAA,MAC3E;AACE,aAAK,iBAAiBtB,CAAC,IAAI;AAG7B,YAAMwB,IAAU,KAAK,MAAMxB,CAAC;AAG5B,UACE,KAAK,oBACL,KAAK,sBAAsB,UAC3BoB,KAAK,KAAK,mBACV;AACA,cAAMK,IAAS,KAAK,SAASzB,CAAC;AAC9B,QAAIwB,MAAYC,MACd,KAAK,MAAMzB,CAAC,IAAIyB,GAChB,KAAK,UAAUzB,CAAC,IAAIU;AAEtB;AAAA,MACF;AAEA,YAAMgB,IAAoB,CAAC,KAAK,gBAAgBL,IAAUD,IAAIA,IAAI7B;AAQlE,UAAI,CAAC,KAAK,kBAAkB;AAC1B,cAAMoC,IAAO,KAAK,kBAAkB3B,GAAG0B,GAAmBV,GAAMC,CAAC;AAGjE,YADG,KAAK,UAAUjB,CAAC,MAAiB,KAAKwB,MAAa,KAAK,SAASxB,CAAC,GACtD;AACb,UAAI2B,MAASH,MACX,KAAK,MAAMxB,CAAC,IAAI2B,GAChB,KAAK,UAAU3B,CAAC,IAAIU;AAEtB;AAAA,QACF;AACA,YAAIiB,MAASH,EAAS;AACtB,cAAMI,IAAW,KAAK,gBAAgB5B,GAAG0B,GAAmBV,GAAMC,GAAGO,CAAO;AAC5E,YAAII,MAAaJ,EAAS;AAI1B,SADmBI,IAAWJ,KACZd,IAAO,KAAK,UAAUV,CAAC,KAAgBX,OACvD,KAAK,MAAMW,CAAC,IAAI4B,GAChB,KAAK,UAAU5B,CAAC,IAAIU;AAEtB;AAAA,MACF;AAEA,YAAMkB,IAAW,KAAK,gBAAgB5B,GAAG0B,GAAmBV,GAAMC,GAAGO,CAAO;AAC5E,UAAII,MAAaJ,EAAS;AAG1B,OADG,KAAK,UAAUxB,CAAC,MAAiB,KAAKwB,MAAa,KAAK,SAASxB,CAAC,KACrDU,IAAO,KAAK,UAAUV,CAAC,KAAgBX,OACrD,KAAK,MAAMW,CAAC,IAAI4B,GAChB,KAAK,UAAU5B,CAAC,IAAIU;AAAA,IAExB;AACA,SAAK,MAAM,YAAYQ;AAAA,EACzB;AAAA;AAAA;AAAA;AAAA;AAAA,EAMQ,kBAAkBW,GAAeT,GAAWJ,GAAcC,GAAmB;AACnF,UAAMZ,IAAM,KAAK,SAASwB,CAAK,GACzBvB,IAAM,KAAK,SAASuB,CAAK;AAC/B,QAAIf,IAAQT;AACZ,WAAOS,IAAQR,KAAOc,IAAIJ,IAAOC,KAAKH,KAAS,IAAI1B,KAAmB,CAAA0B;AACtE,WAAO,KAAK,iBAAiBe,GAAOf,CAAK;AAAA,EAC3C;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOQ,gBACNe,GACAT,GACAJ,GACAC,GACAO,GACQ;AACR,UAAMnB,IAAM,KAAK,SAASwB,CAAK,GACzBvB,IAAM,KAAK,SAASuB,CAAK;AAC/B,QAAIf,IAAQ,KAAK,IAAIR,GAAK,KAAK,IAAID,GAAKmB,CAAO,CAAC;AAEhD,WAAOV,IAAQR,KAAOc,IAAIJ,IAAOC,KAAKH,KAAS,IAAI1B,KAAmB,CAAA0B;AACtE,WAAOA,IAAQT,KAAOe,KAAKJ,IAAOC,MAAMH,IAAQ,MAAM,IAAI1B,KAAmB,CAAA0B;AAG7E,WAAO,KAAK,iBAAiBe,GAAOf,CAAK;AAAA,EAC3C;AAAA,EAEQ,iBAAiBe,GAAeC,GAAwB;AAC9D,UAAM1B,IAAQ,KAAK,OAAOyB,CAAK,EAAc;AAC7C,aAAS,IAAIC,GAAQ,KAAM,KAAK,SAASD,CAAK,GAAc,IAAK,KAAIzB,EAAK,CAAC,EAAG,QAAO;AACrF,aAAS,IAAI0B,IAAS,GAAG,KAAM,KAAK,SAASD,CAAK,GAAc,IAAK,KAAIzB,EAAK,CAAC,EAAG,QAAO;AACzF,WAAO,KAAK,SAASyB,CAAK;AAAA,EAC5B;AAAA;AAAA,EAGQ,gBAAsB;;AAC5B,QAAIhB,IAAQ,KAAK,YAAA;AACjB,QAAIA,KAAS,KAAK,OAAQ;AAO1B,UAAMkB,IAAQ,KAAK,aAAa,SAAS,GAAG,KAAK,aAAa,MAAM;AACpE,aAAS/B,IAAI,GAAGA,IAAI+B,EAAM,QAAQ/B,IAAK,CAAA+B,EAAM/B,CAAC,IAAIA;AAClD,IAAA+B,EAAM,KAAK,CAACC,GAAGC,MAAM;AACnB,YAAMC,IAAM,KAAK,aAAaF,CAAC,EAAkB,CAAC,GAC5CG,IAAM,KAAK,aAAaF,CAAC,EAAkB,CAAC;AAClD,UAAI,KAAK,cAAc;AACrB,cAAMG,IAAK,KAAK,UAAUF,CAAE,GACtBG,IAAK,KAAK,UAAUF,CAAE;AAC5B,YAAIC,MAAOC,EAAI,QAAOD,IAAKC;AAAA,MAC7B;AACA,aAAQ,KAAK,SAASF,CAAE,IAAgB,KAAK,SAASD,CAAE,KAAgBA,IAAKC;AAAA,IAC/E,CAAC;AAGD,eAAWjC,KAAc6B,GAAO;AAC9B,YAAMO,IAAQ,KAAK,aAAapC,CAAU;AAC1C,aAAOW,IAAQ,KAAK,UAAQ;AAC1B,YAAI0B,IAAY,GACZC,IAAU;AACd,mBAAWxC,KAAKsC,GAAO;AACrB,gBAAMd,IAAU,KAAK,SAASxB,CAAC;AAC/B,cAAIwB,KAAY,KAAK,SAASxB,CAAC,EAAc;AAC7C,gBAAMI,IAAQ,KAAK,OAAOJ,CAAC,EAAc,MACnCyC,IAAY,KAAK,iBAAiBzC,GAAGwB,IAAU,CAAC;AACtD,UAAIiB,MAAcjB,MAClBe,QAAcxB,IAAAX,EAAKoB,CAAO,MAAZ,gBAAAT,EAAe,UAAS,QAAM2B,IAAAtC,EAAKqC,CAAS,MAAd,gBAAAC,EAAiB,UAAS,IACtE,KAAK,SAAS1C,CAAC,IAAIyC,GACnBD,IAAU;AAAA,QACZ;AACA,YAAI,CAACA,EAAS;AACd,QAAA3B,KAAS0B;AAAA,MACX;AACA,UAAI1B,KAAS,KAAK,OAAQ;AAAA,IAC5B;AAGA,eAAWX,KAAc6B,GAAO;AAC9B,UAAIlB,KAAS,KAAK,OAAQ;AAC1B,iBAAWb,KAAK,KAAK,aAAaE,CAAU;AAC1C,QAAK,KAAK,SAASF,CAAC,MAAiBd,MACrC2B,OAAU8B,IAAA,KAAK,OAAO3C,CAAC,EAAc,KAAK,KAAK,SAASA,CAAC,CAAW,MAA1D,gBAAA2C,EAA6D,UAAS,GAChF,KAAK,SAAS3C,CAAC,IAAId;AAAA,IAEvB;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAWQ,aAAmB;;AACzB,UAAM4C,IACJ,KAAK,uBAAuB,KAAK,UAAU,KAAK,gBAC5C,KAAK,SACL,KAAK,IAAI,KAAK,QAAQ,KAAK,aAAa,IAAI,KAAK;AACvD,QAAIjB,IAAQ,KAAK,YAAA;AACjB,QAAIA,KAASiB,EAAQ;AAIrB,QAAIc,IAAiB;AACrB,aAAS1C,IAAa,GAAGA,IAAa,KAAK,aAAa,QAAQA,KAAc;AAC5E,YAAM2C,IAAS,KAAK,aAAa3C,CAAU,EAAkB,CAAC;AAC9D,OACG,CAAC,KAAK,gBAAgB,KAAK,UAAU2C,CAAK,MAAM,MAChD,KAAK,SAASA,CAAK,MAAiB3D,MAErC,KAAK,aAAa0D,GAAgB,IAAI1C;AAAA,IAE1C;AACA,UAAM6B,IAAQ,KAAK,aAAa,SAAS,GAAGa,CAAc;AAC1D,IAAAb,EAAM,KAAK,CAACC,GAAGC,MAAM;AACnB,YAAMC,IAAM,KAAK,aAAaF,CAAC,EAAkB,CAAC,GAC5CG,IAAM,KAAK,aAAaF,CAAC,EAAkB,CAAC;AAClD,aAAQ,KAAK,SAASC,CAAE,IAAgB,KAAK,SAASC,CAAE,KAAgBD,IAAKC;AAAA,IAC/E,CAAC;AAGD,eAAWjC,KAAc6B,GAAO;AAC9B,YAAMO,IAAQ,KAAK,aAAapC,CAAU;AAC1C,iBAAa;AACX,YAAI4C,IAAS,GACTC,IAAa;AACjB,mBAAW/C,KAAKsC,GAAO;AACrB,gBAAMd,IAAU,KAAK,SAASxB,CAAC,GACzBgD,IAAQ,KAAK,mBACd,KAAK,SAAShD,CAAC,IACf,KAAK,MAAMA,CAAC;AACjB,cAAIwB,KAAWwB,EAAO;AACtB,gBAAM7B,IAAO,KAAK,OAAOnB,CAAC;AAC1B,cAAIiD,IAAQzB,IAAU;AACtB,iBAAOyB,IAAQD,KAAS,CAAC7B,EAAK,KAAK8B,CAAK,IAAG,CAAAA;AAC3C,UAAIA,IAAQD,KAAS,CAAC7B,EAAK,KAAK8B,CAAK,MACrCH,QAAW/B,IAAAI,EAAK,KAAK8B,CAAK,MAAf,gBAAAlC,EAAkB,UAAS,QAAM2B,IAAAvB,EAAK,KAAKK,CAAO,MAAjB,gBAAAkB,EAAoB,UAAS,IACzEK,IAAa;AAAA,QACf;AACA,YAAI,CAACA,KAAclC,IAAQiC,IAAShB,EAAQ;AAC5C,mBAAW9B,KAAKsC,GAAO;AACrB,gBAAMd,IAAU,KAAK,SAASxB,CAAC,GACzBgD,IAAQ,KAAK,mBACd,KAAK,SAAShD,CAAC,IACf,KAAK,MAAMA,CAAC;AACjB,cAAIwB,KAAWwB,EAAO;AACtB,gBAAM7B,IAAO,KAAK,OAAOnB,CAAC;AAC1B,cAAIiD,IAAQzB,IAAU;AACtB,iBAAOyB,IAAQD,KAAS,CAAC7B,EAAK,KAAK8B,CAAK,IAAG,CAAAA;AAC3C,UAAIA,KAASD,KAAS7B,EAAK,KAAK8B,CAAK,MAAG,KAAK,SAASjD,CAAC,IAAIiD;AAAA,QAC7D;AACA,QAAApC,KAASiC;AAAA,MACX;AAAA,IACF;AAAA,EACF;AAAA,EAEQ,eAAyB;AAC/B,WAAO,KAAK,UAAU,GAAG,KAAK,OAAO,QAAQ,CAAC9C,MAAM,KAAK,SAASA,CAAC,CAAW;AAAA,EAChF;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,gBAAgBkD,GAAcC,GAAsB;AAClD,WAAO,KAAK,UAAUD,GAAMC,GAAI,CAACnD,MAAM,KAAK,SAASA,CAAC,CAAW;AAAA,EACnE;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,eAAekD,GAAcC,GAAYrC,GAAyB;AAChE,UAAMsC,IAAS,KAAK,MAAMtC,CAAK;AAC/B,WAAO,KAAK,UAAUoC,GAAMC,GAAI,CAACnD,MAAM,KAAK,eAAeA,GAAGoD,CAAM,CAAC;AAAA,EACvE;AAAA;AAAA,EAGQ,eAAeC,GAAmBD,GAAwB;AAChE,UAAMjC,IAAO,KAAK,OAAOkC,CAAS;AAClC,QAAIlC,EAAK,KAAKiC,CAAM,EAAG,QAAOA;AAC9B,aAAS,IAAIA,IAAS,GAAG,IAAIjC,EAAK,KAAK,QAAQ;AAC7C,UAAIA,EAAK,KAAK,CAAC,EAAG,QAAO;AAE3B,aAAS,IAAIiC,IAAS,GAAG,KAAK,GAAG;AAC/B,UAAIjC,EAAK,KAAK,CAAC,EAAG,QAAO;AAE3B,WAAOjC;AAAA,EACT;AAAA;AAAA,EAGQ,UAAUgE,GAAcC,GAAYG,GAA8C;AACxF,UAAMC,IAAiB,CAAA;AACvB,QAAIC,IAAO,IACP1C,IAAQ,IACR2C,IAAS,GACTC,IAAQ,GACRC,IAAY,IACZC,IAAU,IACVC,IAAgB,IAChBC,IAAW,OAAO,mBAClBC,IAAS,IACTC,IAAmB,OAAO;AAC9B,UAAMC,IAAQ,MAAY;AACxB,MAAIP,IAAQ,KACVH,EAAK,KAAK;AAAA,QACR,MAAAC;AAAA,QACA,OAAA1C;AAAA,QACA,QAAA2C;AAAA,QACA,OAAAC;AAAA,QACA,WAAAC;AAAA,QACA,SAAAC;AAAA,QACA,UAAAE;AAAA,QACA,QAAAC;AAAA,QACA,GAAI,KAAK,sBAAsB,EAAE,kBAAAC,EAAA,IAAqB,CAAA;AAAA,QACtD,GAAIH,KAAiB,IAAI,EAAE,eAAAA,MAAkB,CAAA;AAAA,MAAC,CAC/C,GAEHH,IAAQ;AAAA,IACV;AAEA,aAAS1D,IAAIkD,GAAMlD,IAAImD,GAAInD,KAAK;AAC9B,YAAMO,IAAI+C,EAAQtD,CAAC;AACnB,UAAIO,MAAMrB,GAAS;AACjB,QAAA+E,EAAA;AACA;AAAA,MACF;AACA,YAAMC,IAAS,KAAK,OAAOlE,CAAC,EAAc,KAAKO,CAAC;AAChD,UAAI,CAAC2D,GAAO;AACV,QAAAD,EAAA;AACA;AAAA,MACF;AACA,YAAME,IAAoB,KAAK,eAAenE,CAAC,GACzCoE,IAAe,KAAK,SAASpE,CAAC,GAC9BqE,IAAc,KAAK,UAAUrE,CAAC,MAAiB;AACrD,MACE0D,IAAQ,KACRQ,EAAM,SAASV,KACfjD,MAAMO,KACNqD,MAAsBN,KACtBK,EAAM,WAAWT,IAASC,KAC1BA,IAAQQ,EAAM,SAAS/E,KAEvBuE,KAASQ,EAAM,OACfN,IAAU5D,IAAI,GACVoE,IAAeN,MAAUA,IAAWM,IACpCC,MAAYN,IAAS,KACzBC,IAAmB,KAAK,IAAIA,GAAkB,KAAK,iBAAiBhE,CAAC,CAAW,MAEhFiE,EAAA,GACAT,IAAOU,EAAM,MACbpD,IAAQP,GACRkD,IAASS,EAAM,QACfR,IAAQQ,EAAM,OACdP,IAAY3D,GACZ4D,IAAU5D,IAAI,GACd6D,IAAgBM,GAChBL,IAAWM,GACXL,IAASM,GACTL,IAAmB,KAAK,iBAAiBhE,CAAC;AAAA,IAE9C;AACA,WAAAiE,EAAA,GACOV;AAAA,EACT;AACF;"}
|
|
1
|
+
{"version":3,"file":"lod-scheduler-B0a_uBlv.js","sources":["../src/lib/streaming/lod-scheduler.ts"],"sourcesContent":["import * as THREE from 'three/webgpu';\nimport type { LodLeaf, LodManifest } from './lod-manifest';\nimport type { LodSource } from './lod-source';\n\n/**\n * A contiguous splat range within one chunk file, at one LOD level - the\n * unit the pool activates. Adjacent manifest leaves that resolve to the same\n * `(file, level)` and whose ranges abut are coalesced into one run, so the\n * pool sees a handful of large ranges instead of thousands of tiny leaves.\n */\nexport interface LodRun {\n readonly file: number;\n readonly level: number;\n readonly offset: number;\n readonly count: number;\n /** First manifest-leaf index this run covers. */\n readonly leafStart: number;\n /** One past the last covered leaf index. */\n readonly leafEnd: number;\n /**\n * Physical-coverage identity when several manifest leaves form one region.\n * A classic LCC cell may be split into fetch-sized leaves, but its slices\n * must still swap as one visible unit. Undefined retains interval-based\n * grouping for hierarchical sources.\n */\n readonly coverageGroup?: number;\n /**\n * Camera distance (mesh-local) of the nearest leaf in this run, from the\n * last {@link LodScheduler.computeDesiredRuns} pass. Streamed meshes use it\n * to fetch near detail before far coarse coverage. Absent on sources that\n * do not track per-leaf distance.\n */\n readonly distance?: number;\n /**\n * True when any leaf in this run intersects the view frustum (with the\n * scheduler's edge margin). Used to rank classic-path fetches so in-view\n * detail beats behind-camera work; absent when the source does not track it.\n */\n readonly inView?: boolean;\n /**\n * Fetch-only angular distance from the camera's forward axis. Smaller values\n * are nearer the screen centre. It never affects LOD selection or budget.\n */\n readonly screenImportance?: number;\n}\n\n/** Stable identity of a run, for diffing desired vs. resident sets. */\nexport function runKey(run: LodRun): string {\n return `${run.file}:${run.level}:${run.offset}:${run.count}`;\n}\n\n/** Sentinel level meaning \"this leaf is currently dropped (renders nothing)\". */\nconst DROPPED = -1;\n\n/**\n * Maximum splats in one run. Large contiguous regions are split into several\n * runs so no single pool append (and thus no single frame) does an unbounded\n * amount of copy + upload work. Split runs stay contiguous, so they still\n * upload as one coalesced rectangle.\n */\nconst MAX_RUN_SPLATS = 128_000;\n\n/** Hysteresis dead-band around each LOD distance threshold. */\nconst THRESHOLD_MARGIN = 0.1;\n/** Minimum time a leaf must hold a level before changing again, ms. */\nconst DWELL_MS = 500;\n/** Frustum test margin, as a fraction of each leaf's own size. */\nconst FRUSTUM_MARGIN = 0.1;\n/**\n * Out-of-frustum leaves act this many times farther away instead of being\n * hard-coarsened. Rotating the camera then re-ranks leaves smoothly (and\n * dwell applies), rather than mass-flipping edge leaves every frame -\n * PlayCanvas uses the same behind-camera-penalty idea.\n */\nconst FRUSTUM_PENALTY = 3;\n\n/**\n * Chooses a level of detail per spatial leaf from the camera, keeps the\n * total within a splat budget, and coalesces the result into runs.\n *\n * The scheduler is pure with respect to rendering: it takes a camera\n * position and frustum (both in the mesh's local space) and returns the set\n * of runs that should be resident. It owns only the small amount of state\n * needed for temporal stability (each leaf's current level and when it last\n * changed), so it is straightforward to drive from a test harness.\n *\n * LOD distance model (PlayCanvas-compatible): level 0 is used within\n * `lodBaseDistance`; each successive level covers a band `lodMultiplier`×\n * farther out. By default out-of-frustum leaves act {@link FRUSTUM_PENALTY}×\n * farther away. Formats with broad spatial cells can disable that bias so an\n * orbit does not churn already-refined cells at the frustum edges.\n *\n * The distance model only sets the *floor* and the *priority*: when the\n * distance-chosen set leaves budget unused, {@link fillBudget} promotes\n * in-frustum leaves nearest-first until its configured headroom target is\n * spent (90% by default). Formats can cap that optional refinement and avoid\n * a full-finest cut even when an explicitly large budget could hold it. The\n * budget\n * therefore decides how far refinement reaches - a desktop budget yields\n * full detail even with the whole scene in view, a mobile budget refines\n * only near the camera.\n */\nexport class LodScheduler implements LodSource {\n /** World-unit distance inside which the finest level (0) is used. */\n lodBaseDistance: number;\n /** Distance ratio between successive LOD levels. */\n lodMultiplier: number;\n /** Maximum active splats; the desired set is demoted to fit. */\n budget: number;\n\n private readonly leaves: readonly LodLeaf[];\n private readonly coarsest: number;\n /** Per leaf: lowest/highest LOD level that has data. */\n private readonly minLevel: Int32Array;\n private readonly maxLevel: Int32Array;\n /**\n * Per leaf: the distance *ambition* - the finest level the camera distance\n * asks for, with hysteresis/dwell applied. This is the hysteresis *state*:\n * only {@link selectLevels} writes it, so budget demotion can never feed\n * back into the dead-band/dwell logic and oscillate. With `fillPastDistance`\n * false, {@link fillBudget} restores {@link resolved} only up to here.\n */\n private readonly level: Int32Array;\n /**\n * Per leaf: the resolved target - the ambition capped to fit the budget by\n * {@link enforceBudget}, then refilled from leftover headroom by\n * {@link fillBudget} (or DROPPED). This is what {@link coalesceRuns} emits.\n */\n private readonly resolved: Int32Array;\n /** Per leaf: timestamp of the last distance-level change, for dwell. */\n private readonly changedAt: Float64Array;\n\n private readonly distance: Float64Array;\n private readonly inFrustum: Uint8Array;\n /** Leaves that must move through the budget cut together. */\n private readonly budgetGroups: readonly Uint32Array[];\n /** Per-leaf physical coverage group, when the manifest defines one. */\n private readonly coverageGroups: Int32Array;\n /** Whether view-frustum membership affects distance and fill priority. */\n private readonly frustumAware: boolean;\n /** Fraction of the usable budget that optional refinement may consume. */\n private readonly budgetFillFraction: number;\n /** Absolute cap on optional refinement; distance-selected detail may exceed it. */\n private readonly budgetFillCap: number;\n /** Whether a budget that holds every finest leaf forces a full-finest cut. */\n private readonly forceFinestWhenFits: boolean;\n /**\n * Leaves at or inside this camera distance always resolve to their finest\n * available level (no hysteresis climb). Classic LCC uses a multi-band\n * radius so adjacent broad cells do not paint coarse discs at startup.\n */\n private readonly forceFinestWithin: number | undefined;\n /**\n * When false, {@link fillBudget} only restores toward each leaf's\n * distance-selected {@link level} - never finer. Classic LCC sets this so\n * a mid-range cell does not fetch L0 only to demote to L1 once the cut\n * settles.\n */\n private readonly fillPastDistance: boolean;\n\n /**\n * What the last cut actually decided, for the HUD.\n *\n * The resident splat count alone cannot distinguish \"the scheduler asked for\n * this\" from \"the scheduler asked for more and the mesh could not apply it\",\n * and those have opposite fixes. Measured rather than inferred after a\n * zoomed-out `sandwijck` sat at exactly its coarsest total (190,730) with a\n * 600k budget - 68% of the budget unspent, with no way to see which stage\n * declined to spend it.\n */\n readonly stats = {\n /** Leaves the frustum test accepted - `fillBudget`'s candidate set. */\n inFrustum: 0,\n /** Total leaves in the tree. */\n leaves: 0,\n /** Splats implied by the resolved cut, before the mesh applies anything. */\n desired: 0,\n /** Splats `fillBudget` added on top of the distance-chosen levels. */\n filled: 0,\n };\n\n private readonly scratchBox = new THREE.Box3();\n private readonly scratchSize = new THREE.Vector3();\n private readonly scratchCenter = new THREE.Vector3();\n private readonly screenImportance: Float32Array;\n /** Whether the last selection pass received a camera-forward vector. */\n private hasScreenImportance = false;\n /**\n * Persistent leaf-index ordering scratch for {@link enforceBudget} and\n * {@link fillBudget}, sorted in place per call instead of allocating a\n * fresh `[...keys]` array. Comparators break ties on the index itself so\n * the order is identical to the stable `Array#sort` this replaces\n * (`TypedArray#sort` stability is not guaranteed).\n */\n private readonly orderScratch: Uint32Array;\n\n constructor(\n manifest: LodManifest,\n options: {\n budget: number;\n lodBaseDistance: number;\n lodMultiplier: number;\n frustumAware?: boolean;\n budgetFillFraction?: number;\n budgetFillCap?: number;\n forceFinestWhenFits?: boolean;\n forceFinestWithin?: number;\n /**\n * When false, leftover-budget fill never refines past the distance-\n * selected level (see {@link fillPastDistance}). Default true.\n */\n fillPastDistance?: boolean;\n },\n ) {\n this.leaves = manifest.leaves;\n this.coarsest = manifest.lodLevels - 1;\n this.budget = options.budget;\n this.lodBaseDistance = options.lodBaseDistance;\n this.lodMultiplier = options.lodMultiplier;\n this.frustumAware = options.frustumAware !== false;\n this.budgetFillFraction = options.budgetFillFraction ?? 0.9;\n this.budgetFillCap = options.budgetFillCap ?? Number.POSITIVE_INFINITY;\n this.forceFinestWhenFits = options.forceFinestWhenFits !== false;\n this.forceFinestWithin = options.forceFinestWithin;\n this.fillPastDistance = options.fillPastDistance !== false;\n if (this.budgetFillFraction <= 0 || this.budgetFillFraction > 1) {\n throw new RangeError('LodScheduler budgetFillFraction must be in (0, 1].');\n }\n if (!(this.budgetFillCap > 0)) {\n throw new RangeError('LodScheduler budgetFillCap must be positive.');\n }\n if (this.forceFinestWithin !== undefined && !(this.forceFinestWithin >= 0)) {\n throw new RangeError('LodScheduler forceFinestWithin must be >= 0.');\n }\n\n const n = this.leaves.length;\n this.minLevel = new Int32Array(n);\n this.maxLevel = new Int32Array(n);\n this.level = new Int32Array(n).fill(this.coarsest);\n this.resolved = new Int32Array(n).fill(this.coarsest);\n this.changedAt = new Float64Array(n);\n this.distance = new Float64Array(n);\n this.inFrustum = new Uint8Array(n);\n this.screenImportance = new Float32Array(n);\n this.orderScratch = new Uint32Array(n);\n this.coverageGroups = new Int32Array(n).fill(-1);\n\n const groups: number[][] = [];\n const explicitGroups = new Map<number, number>();\n for (let i = 0; i < n; i++) {\n const key = (this.leaves[i] as LodLeaf).budgetGroup;\n if (key === undefined) {\n groups.push([i]);\n continue;\n }\n let groupIndex = explicitGroups.get(key);\n if (groupIndex === undefined) {\n groupIndex = groups.length;\n explicitGroups.set(key, groupIndex);\n groups.push([]);\n }\n (groups[groupIndex] as number[]).push(i);\n this.coverageGroups[i] = key;\n }\n this.budgetGroups = groups.map((members) => Uint32Array.from(members));\n\n for (let i = 0; i < n; i++) {\n const lods = (this.leaves[i] as LodLeaf).lods;\n let min = this.coarsest;\n let max = 0;\n for (let l = 0; l < lods.length; l++) {\n if (lods[l] === undefined) continue;\n min = Math.min(min, l);\n max = Math.max(max, l);\n }\n this.minLevel[i] = min;\n this.maxLevel[i] = max;\n this.level[i] = max; // start coarse\n }\n }\n\n /**\n * Recomputes the desired resident run set for the current camera.\n *\n * @param cameraLocal - Camera position in the mesh's local space.\n * @param frustum - View frustum in the mesh's local space.\n * @param now - Monotonic timestamp (ms) for dwell hysteresis.\n */\n computeDesiredRuns(\n cameraLocal: THREE.Vector3,\n frustum: THREE.Frustum,\n now: number,\n cameraForward?: THREE.Vector3,\n ): LodRun[] {\n this.selectLevels(cameraLocal, frustum, now, cameraForward);\n // When the budget already holds every leaf's finest level, skip distance /\n // frustum demotion entirely. Otherwise a small rotate marks leaves\n // out-of-frustum (×{@link FRUSTUM_PENALTY}), coarsens them, and the mesh\n // aborts fine fetches / swaps to coarse LCC discs - then has to climb\n // back when they re-enter the view.\n if (this.forceFinestWhenFits && this.budget >= this.finestTotal()) {\n for (let i = 0; i < this.leaves.length; i++) {\n this.resolved[i] = this.minLevel[i] as number;\n }\n this.stats.desired = this.activeTotal();\n this.stats.filled = 0;\n return this.coalesceRuns();\n }\n // Copy the ambition into the resolved target, cap it to the budget, then\n // refill leftover headroom. Budget stages touch only `resolved`, never the\n // ambition/hysteresis state - a stationary camera therefore resolves the\n // same cut every pass instead of oscillating red↔orange.\n this.resolved.set(this.level);\n this.enforceBudget();\n const beforeFill = this.activeTotal();\n this.fillBudget();\n this.stats.desired = this.activeTotal();\n this.stats.filled = this.stats.desired - beforeFill;\n return this.coalesceRuns();\n }\n\n /** Total active splats implied by the current resolved levels. */\n private activeTotal(): number {\n let total = 0;\n for (let i = 0; i < this.leaves.length; i++) {\n const level = this.resolved[i] as number;\n if (level === DROPPED) continue;\n total += (this.leaves[i] as LodLeaf).lods[level]?.count ?? 0;\n }\n return total;\n }\n\n /** Splats if every leaf sat at its finest available level (none dropped). */\n private finestTotal(): number {\n let total = 0;\n for (let i = 0; i < this.leaves.length; i++) {\n const level = this.minLevel[i] as number;\n total += (this.leaves[i] as LodLeaf).lods[level]?.count ?? 0;\n }\n return total;\n }\n\n private selectLevels(\n cameraLocal: THREE.Vector3,\n frustum: THREE.Frustum,\n now: number,\n cameraForward?: THREE.Vector3,\n ): void {\n const { lodBaseDistance: base, lodMultiplier: m } = this;\n this.hasScreenImportance = cameraForward !== undefined;\n let visibleCount = 0;\n this.stats.leaves = this.leaves.length;\n for (let i = 0; i < this.leaves.length; i++) {\n const leaf = this.leaves[i] as LodLeaf;\n const d = leaf.bounds.distanceToPoint(cameraLocal);\n this.distance[i] = d;\n\n // Widen the box by a fraction of its own size for edge stability.\n this.scratchBox.copy(leaf.bounds);\n leaf.bounds.getSize(this.scratchSize);\n this.scratchBox.expandByScalar(this.scratchSize.length() * FRUSTUM_MARGIN);\n const visible = frustum.intersectsBox(this.scratchBox);\n this.inFrustum[i] = visible ? 1 : 0;\n if (visible) visibleCount++;\n\n // Broad classic-LCC tiles frequently all intersect the frustum, so the\n // boolean above cannot tell the centre of the screen from its edges.\n // This is deliberately fetch metadata only: distance remains the source\n // of truth for the resolved LOD and the budget cut.\n if (cameraForward) {\n leaf.bounds.getCenter(this.scratchCenter).sub(cameraLocal);\n const depth = this.scratchCenter.dot(cameraForward);\n const lateralSquared = Math.max(0, this.scratchCenter.lengthSq() - depth * depth);\n this.screenImportance[i] =\n depth > 0 ? Math.sqrt(lateralSquared) / Math.max(depth, 0.25) : Number.POSITIVE_INFINITY;\n } else {\n this.screenImportance[i] = 0;\n }\n\n const current = this.level[i] as number;\n // Blanket force-finest only when fill may still climb past the distance\n // band (octree / page-table). Classic LCC never uses this path.\n if (\n this.fillPastDistance &&\n this.forceFinestWithin !== undefined &&\n d <= this.forceFinestWithin\n ) {\n const finest = this.minLevel[i] as number;\n if (current !== finest) {\n this.level[i] = finest;\n this.changedAt[i] = now;\n }\n continue;\n }\n\n const effectiveDistance = !this.frustumAware || visible ? d : d * FRUSTUM_PENALTY;\n\n // Classic LCC: the ambition tracks the pure distance band only. Budget\n // demotions live in `resolved`, never here, so they cannot feed back\n // into hysteresis. A cold cell snaps straight to its band (no dwell\n // climb through intermediate LODs); afterward only a band *crossing*\n // moves the ambition - a stationary camera in the same band holds,\n // even if the budget just demoted its resolved level.\n if (!this.fillPastDistance) {\n const band = this.distanceBandLevel(i, effectiveDistance, base, m);\n const coldStart =\n (this.changedAt[i] as number) === 0 && current === (this.maxLevel[i] as number);\n if (coldStart) {\n if (band !== current) {\n this.level[i] = band;\n this.changedAt[i] = now;\n }\n continue;\n }\n if (band === current) continue;\n const proposed = this.hysteresisLevel(i, effectiveDistance, base, m, current);\n if (proposed === current) continue;\n // Coarsen immediately (moving away should not linger over budget);\n // only refinement waits out the dwell window.\n const coarsening = proposed > current;\n if (coarsening || now - (this.changedAt[i] as number) >= DWELL_MS) {\n this.level[i] = proposed;\n this.changedAt[i] = now;\n }\n continue;\n }\n\n const proposed = this.hysteresisLevel(i, effectiveDistance, base, m, current);\n if (proposed === current) continue;\n const coldSnap =\n (this.changedAt[i] as number) === 0 && current === (this.maxLevel[i] as number);\n if (coldSnap || now - (this.changedAt[i] as number) >= DWELL_MS) {\n this.level[i] = proposed;\n this.changedAt[i] = now;\n }\n }\n this.stats.inFrustum = visibleCount;\n }\n\n /**\n * Pure distance→level band (independent of the leaf's current level), so a\n * classic LCC cut never has to dwell through intermediate rungs.\n */\n private distanceBandLevel(index: number, d: number, base: number, m: number): number {\n const min = this.minLevel[index] as number;\n const max = this.maxLevel[index] as number;\n let level = min;\n while (level < max && d > base * m ** level * (1 + THRESHOLD_MARGIN)) level++;\n return this.resolveAvailable(index, level);\n }\n\n /**\n * Stable level from distance: coarsen only past `threshold·(1 + margin)`,\n * refine only within `threshold·(1 − margin)`. The available-level clamp\n * keeps leaves that lack a level on their nearest coarser one.\n */\n private hysteresisLevel(\n index: number,\n d: number,\n base: number,\n m: number,\n current: number,\n ): number {\n const min = this.minLevel[index] as number;\n const max = this.maxLevel[index] as number;\n let level = Math.min(max, Math.max(min, current));\n // threshold(L) = base·m^L is the boundary between level L and L+1.\n while (level < max && d > base * m ** level * (1 + THRESHOLD_MARGIN)) level++;\n while (level > min && d <= base * m ** (level - 1) * (1 - THRESHOLD_MARGIN)) level--;\n // Resolve to the nearest available level (prefer coarser) if this exact\n // one has no data (leaves need not carry every level).\n return this.resolveAvailable(index, level);\n }\n\n private resolveAvailable(index: number, target: number): number {\n const lods = (this.leaves[index] as LodLeaf).lods;\n for (let l = target; l <= (this.maxLevel[index] as number); l++) if (lods[l]) return l;\n for (let l = target - 1; l >= (this.minLevel[index] as number); l--) if (lods[l]) return l;\n return this.maxLevel[index] as number;\n }\n\n /** Demotes/drops lowest-priority leaves until the total fits the budget. */\n private enforceBudget(): void {\n let total = this.activeTotal();\n if (total <= this.budget) return;\n\n // Demote lowest priority first. When frustum-aware: out-of-frustum before\n // in-frustum, then farthest. Classic LCC sets frustumAware false - broad\n // XY cells often sit mostly behind the camera while the user stands in\n // them (d≈0); frustum demotion would coarsen that cell first and let\n // thinner cells ahead keep fine detail (backwards load). Distance only.\n const order = this.orderScratch.subarray(0, this.budgetGroups.length);\n for (let i = 0; i < order.length; i++) order[i] = i;\n order.sort((a, b) => {\n const ia = (this.budgetGroups[a] as Uint32Array)[0] as number;\n const ib = (this.budgetGroups[b] as Uint32Array)[0] as number;\n if (this.frustumAware) {\n const fa = this.inFrustum[ia] as number;\n const fb = this.inFrustum[ib] as number;\n if (fa !== fb) return fa - fb; // out-of-frustum (0) first\n }\n return (this.distance[ib] as number) - (this.distance[ia] as number) || ia - ib;\n });\n\n // Pass 1: coarsen toward the coarsest available level.\n for (const groupIndex of order) {\n const group = this.budgetGroups[groupIndex] as Uint32Array;\n while (total > this.budget) {\n let reduction = 0;\n let changed = false;\n for (const i of group) {\n const current = this.resolved[i] as number;\n if (current >= (this.maxLevel[i] as number)) continue;\n const lods = (this.leaves[i] as LodLeaf).lods;\n const nextLevel = this.resolveAvailable(i, current + 1);\n if (nextLevel === current) continue;\n reduction += (lods[current]?.count ?? 0) - (lods[nextLevel]?.count ?? 0);\n this.resolved[i] = nextLevel;\n changed = true;\n }\n if (!changed) break;\n total -= reduction;\n }\n if (total <= this.budget) return;\n }\n\n // Pass 2 (last resort): drop groups entirely, lowest priority first.\n for (const groupIndex of order) {\n if (total <= this.budget) return;\n for (const i of this.budgetGroups[groupIndex] as Uint32Array) {\n if ((this.resolved[i] as number) === DROPPED) continue;\n total -= (this.leaves[i] as LodLeaf).lods[this.resolved[i] as number]?.count ?? 0;\n this.resolved[i] = DROPPED;\n }\n }\n }\n\n /**\n * Uses leftover budget to restore detail after {@link enforceBudget}\n * demoted leaves, nearest first, while the total stays under the fill\n * target. By default this may also refine *past* the distance-chosen level\n * when headroom remains (octree / page-table streams). With\n * {@link fillPastDistance} false (classic LCC), fill never goes finer than\n * each leaf's distance-selected level - so a cell at the L1 band does not\n * fetch L0 only to demote moments later with the camera still.\n */\n private fillBudget(): void {\n const target =\n this.forceFinestWhenFits && this.budget >= this.finestTotal()\n ? this.budget\n : Math.min(this.budget, this.budgetFillCap) * this.budgetFillFraction;\n let total = this.activeTotal();\n if (total >= target) return;\n\n // Candidate in-frustum groups, nearest first (ties keep manifest order),\n // packed into the front of the shared index scratch.\n let candidateCount = 0;\n for (let groupIndex = 0; groupIndex < this.budgetGroups.length; groupIndex++) {\n const first = (this.budgetGroups[groupIndex] as Uint32Array)[0] as number;\n if (\n (!this.frustumAware || this.inFrustum[first] === 1) &&\n (this.resolved[first] as number) !== DROPPED\n ) {\n this.orderScratch[candidateCount++] = groupIndex;\n }\n }\n const order = this.orderScratch.subarray(0, candidateCount);\n order.sort((a, b) => {\n const ia = (this.budgetGroups[a] as Uint32Array)[0] as number;\n const ib = (this.budgetGroups[b] as Uint32Array)[0] as number;\n return (this.distance[ia] as number) - (this.distance[ib] as number) || ia - ib;\n });\n\n // Nearest groups refine first (up to the distance floor when capped).\n for (const groupIndex of order) {\n const group = this.budgetGroups[groupIndex] as Uint32Array;\n while (true) {\n let growth = 0;\n let canPromote = false;\n for (const i of group) {\n const current = this.resolved[i] as number;\n const floor = this.fillPastDistance\n ? (this.minLevel[i] as number)\n : (this.level[i] as number);\n if (current <= floor) continue;\n const leaf = this.leaves[i] as LodLeaf;\n let finer = current - 1;\n while (finer > floor && !leaf.lods[finer]) finer--;\n if (finer < floor || !leaf.lods[finer]) continue;\n growth += (leaf.lods[finer]?.count ?? 0) - (leaf.lods[current]?.count ?? 0);\n canPromote = true;\n }\n if (!canPromote || total + growth > target) break;\n for (const i of group) {\n const current = this.resolved[i] as number;\n const floor = this.fillPastDistance\n ? (this.minLevel[i] as number)\n : (this.level[i] as number);\n if (current <= floor) continue;\n const leaf = this.leaves[i] as LodLeaf;\n let finer = current - 1;\n while (finer > floor && !leaf.lods[finer]) finer--;\n if (finer >= floor && leaf.lods[finer]) this.resolved[i] = finer;\n }\n total += growth;\n }\n }\n }\n\n private coalesceRuns(): LodRun[] {\n return this.buildRuns(0, this.leaves.length, (i) => this.resolved[i] as number);\n }\n\n /**\n * Runs covering [from, to) at each leaf's coarsest available level - used\n * to substitute always-cached coverage while a finer level is fetching.\n */\n coarsestRunsFor(from: number, to: number): LodRun[] {\n return this.buildRuns(from, to, (i) => this.maxLevel[i] as number);\n }\n\n /**\n * Runs covering [from, to) at `level`, clamped per leaf to an available rung\n * (prefer the requested level, else the next coarser, else the next finer).\n */\n runsAtLevelFor(from: number, to: number, level: number): LodRun[] {\n const wanted = Math.floor(level);\n return this.buildRuns(from, to, (i) => this.clampLeafLevel(i, wanted));\n }\n\n /** Prefer `wanted`, else next coarser, else next finer; {@link DROPPED} if none. */\n private clampLeafLevel(leafIndex: number, wanted: number): number {\n const leaf = this.leaves[leafIndex] as LodLeaf;\n if (leaf.lods[wanted]) return wanted;\n for (let l = wanted + 1; l < leaf.lods.length; l++) {\n if (leaf.lods[l]) return l;\n }\n for (let l = wanted - 1; l >= 0; l--) {\n if (leaf.lods[l]) return l;\n }\n return DROPPED;\n }\n\n /** Coalesces leaves [from, to) at `levelOf(leaf)` into contiguous runs. */\n private buildRuns(from: number, to: number, levelOf: (index: number) => number): LodRun[] {\n const runs: LodRun[] = [];\n let file = -1;\n let level = -1;\n let offset = 0;\n let count = 0;\n let leafStart = -1;\n let leafEnd = -1;\n let coverageGroup = -1;\n let distance = Number.POSITIVE_INFINITY;\n let inView = false;\n let screenImportance = Number.POSITIVE_INFINITY;\n const flush = (): void => {\n if (count > 0) {\n runs.push({\n file,\n level,\n offset,\n count,\n leafStart,\n leafEnd,\n distance,\n inView,\n ...(this.hasScreenImportance ? { screenImportance } : {}),\n ...(coverageGroup >= 0 ? { coverageGroup } : {}),\n });\n }\n count = 0;\n };\n\n for (let i = from; i < to; i++) {\n const l = levelOf(i);\n if (l === DROPPED) {\n flush();\n continue;\n }\n const range = (this.leaves[i] as LodLeaf).lods[l];\n if (!range) {\n flush();\n continue;\n }\n const leafCoverageGroup = this.coverageGroups[i] as number;\n const leafDistance = this.distance[i] as number;\n const leafInView = (this.inFrustum[i] as number) === 1;\n if (\n count > 0 &&\n range.file === file &&\n l === level &&\n leafCoverageGroup === coverageGroup &&\n range.offset === offset + count &&\n count + range.count <= MAX_RUN_SPLATS\n ) {\n count += range.count; // extend the current run\n leafEnd = i + 1;\n if (leafDistance < distance) distance = leafDistance;\n if (leafInView) inView = true;\n screenImportance = Math.min(screenImportance, this.screenImportance[i] as number);\n } else {\n flush();\n file = range.file;\n level = l;\n offset = range.offset;\n count = range.count;\n leafStart = i;\n leafEnd = i + 1;\n coverageGroup = leafCoverageGroup;\n distance = leafDistance;\n inView = leafInView;\n screenImportance = this.screenImportance[i] as number;\n }\n }\n flush();\n return runs;\n }\n}\n"],"names":["runKey","run","DROPPED","MAX_RUN_SPLATS","THRESHOLD_MARGIN","DWELL_MS","FRUSTUM_MARGIN","FRUSTUM_PENALTY","LodScheduler","manifest","options","__publicField","THREE","n","groups","explicitGroups","i","key","groupIndex","members","lods","min","max","l","cameraLocal","frustum","now","cameraForward","beforeFill","total","level","_a","base","m","visibleCount","leaf","d","visible","depth","lateralSquared","current","finest","effectiveDistance","band","proposed","index","target","order","a","b","ia","ib","fa","fb","group","reduction","changed","nextLevel","_b","_c","candidateCount","first","growth","canPromote","floor","finer","from","to","wanted","leafIndex","levelOf","runs","file","offset","count","leafStart","leafEnd","coverageGroup","distance","inView","screenImportance","flush","range","leafCoverageGroup","leafDistance","leafInView"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"logging-BfPdd7NJ.js","sources":["../src/lib/logging.ts"],"sourcesContent":["/**\n * The library's diagnostic output, behind one replaceable hook.\n *\n * A library that writes straight to the host console gives embedders no way\n * out - and some of these sites are per-item (a malformed chunk in a large\n * streamed scene can warn on every tick). Everything routes through\n * {@link setVlamLogHandler} instead, so a host can forward VLAM! diagnostics\n * into its own logger, or silence them entirely.\n *\n * Worker scopes are the deliberate exception: `load-worker.ts` and\n * `sort-worker.ts` run in a separate realm with no access to this module's\n * state, and plumbing a handler across `postMessage` would mean serializing\n * user callbacks. Neither worker logs today - failures travel back as\n * structured messages - so there is nothing to route.\n */\n\n/** Severity of a library diagnostic. */\nexport type VlamLogLevel = 'warn' | 'error';\n\n/**\n * Receives library diagnostics. `message` is already prefixed with `vlam:`;\n * `details` carries any extra values the call site passed.\n */\nexport type VlamLogHandler = (level: VlamLogLevel, message: string, ...details: unknown[]) => void;\n\nconst defaultHandler: VlamLogHandler = (level, message, ...details) => {\n if (level === 'error') console.error(message, ...details);\n else console.warn(message, ...details);\n};\n\nlet handler: VlamLogHandler | null = defaultHandler;\n\n/**\n * Redirects (or silences) every diagnostic the library emits.\n *\n * @param next - A handler to receive diagnostics, `null` to drop them\n * silently, or omitted/`undefined` to restore the default console handler.\n *\n * @example\n * setVlamLogHandler((level, message) => myLogger.log(level, message));\n * setVlamLogHandler(null); // silence VLAM! entirely\n */\nexport function setVlamLogHandler(next?: VlamLogHandler | null): void {\n handler = next === undefined ? defaultHandler : next;\n}\n\n/** Emits a `vlam:`-prefixed warning through the current handler. */\nexport function warn(message: string, ...details: unknown[]): void {\n handler?.('warn', `vlam: ${message}`, ...details);\n}\n\n/** Emits a `vlam:`-prefixed error through the current handler. */\nexport function logError(message: string, ...details: unknown[]): void {\n handler?.('error', `vlam: ${message}`, ...details);\n}\n"],"names":["defaultHandler","level","message","details","handler","setVlamLogHandler","next","warn","logError"],"mappings":"AAyBA,MAAMA,IAAiC,CAACC,GAAOC,MAAYC,MAAY;AACrE,EAAIF,MAAU,UAAS,QAAQ,MAAMC,GAAS,GAAGC,CAAO,IACnD,QAAQ,KAAKD,GAAS,GAAGC,CAAO;AACvC;AAEA,IAAIC,IAAiCJ;AAY9B,SAASK,EAAkBC,GAAoC;AACpE,EAAAF,IAAUE,MAAS,SAAYN,IAAiBM;AAClD;AAGO,SAASC,EAAKL,MAAoBC,GAA0B;AACjE,EAAAC,KAAA,QAAAA,EAAU,QAAQ,SAASF,CAAO,IAAI,GAAGC;AAC3C;AAGO,SAASK,EAASN,MAAoBC,GAA0B;AACrE,EAAAC,KAAA,QAAAA,EAAU,SAAS,SAASF,CAAO,IAAI,GAAGC;AAC5C;"}
|
|
1
|
+
{"version":3,"file":"logging-BfPdd7NJ.js","sources":["../src/lib/core/logging.ts"],"sourcesContent":["/**\n * The library's diagnostic output, behind one replaceable hook.\n *\n * A library that writes straight to the host console gives embedders no way\n * out - and some of these sites are per-item (a malformed chunk in a large\n * streamed scene can warn on every tick). Everything routes through\n * {@link setVlamLogHandler} instead, so a host can forward VLAM! diagnostics\n * into its own logger, or silence them entirely.\n *\n * Worker scopes are the deliberate exception: `load-worker.ts` and\n * `sort-worker.ts` run in a separate realm with no access to this module's\n * state, and plumbing a handler across `postMessage` would mean serializing\n * user callbacks. Neither worker logs today - failures travel back as\n * structured messages - so there is nothing to route.\n */\n\n/** Severity of a library diagnostic. */\nexport type VlamLogLevel = 'warn' | 'error';\n\n/**\n * Receives library diagnostics. `message` is already prefixed with `vlam:`;\n * `details` carries any extra values the call site passed.\n */\nexport type VlamLogHandler = (level: VlamLogLevel, message: string, ...details: unknown[]) => void;\n\nconst defaultHandler: VlamLogHandler = (level, message, ...details) => {\n if (level === 'error') console.error(message, ...details);\n else console.warn(message, ...details);\n};\n\nlet handler: VlamLogHandler | null = defaultHandler;\n\n/**\n * Redirects (or silences) every diagnostic the library emits.\n *\n * @param next - A handler to receive diagnostics, `null` to drop them\n * silently, or omitted/`undefined` to restore the default console handler.\n *\n * @example\n * setVlamLogHandler((level, message) => myLogger.log(level, message));\n * setVlamLogHandler(null); // silence VLAM! entirely\n */\nexport function setVlamLogHandler(next?: VlamLogHandler | null): void {\n handler = next === undefined ? defaultHandler : next;\n}\n\n/** Emits a `vlam:`-prefixed warning through the current handler. */\nexport function warn(message: string, ...details: unknown[]): void {\n handler?.('warn', `vlam: ${message}`, ...details);\n}\n\n/** Emits a `vlam:`-prefixed error through the current handler. */\nexport function logError(message: string, ...details: unknown[]): void {\n handler?.('error', `vlam: ${message}`, ...details);\n}\n"],"names":["defaultHandler","level","message","details","handler","setVlamLogHandler","next","warn","logError"],"mappings":"AAyBA,MAAMA,IAAiC,CAACC,GAAOC,MAAYC,MAAY;AACrE,EAAIF,MAAU,UAAS,QAAQ,MAAMC,GAAS,GAAGC,CAAO,IACnD,QAAQ,KAAKD,GAAS,GAAGC,CAAO;AACvC;AAEA,IAAIC,IAAiCJ;AAY9B,SAASK,EAAkBC,GAAoC;AACpE,EAAAF,IAAUE,MAAS,SAAYN,IAAiBM;AAClD;AAGO,SAASC,EAAKL,MAAoBC,GAA0B;AACjE,EAAAC,KAAA,QAAAA,EAAU,QAAQ,SAASF,CAAO,IAAI,GAAGC;AAC3C;AAGO,SAASK,EAASN,MAAoBC,GAA0B;AACrE,EAAAC,KAAA,QAAAA,EAAU,SAAS,SAASF,CAAO,IAAI,GAAGC;AAC5C;"}
|