@voluma/vlam 0.2.4 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/README.md +3 -2
  2. package/dist/budget-governor.d.ts +4 -4
  3. package/dist/camera-budget-governor.d.ts +11 -11
  4. package/dist/chunk-cache-budget.d.ts +5 -5
  5. package/dist/chunk-fetch-scheduler.d.ts +4 -4
  6. package/dist/chunk-loader-DlNwggfG.js +1859 -0
  7. package/dist/chunk-loader-DlNwggfG.js.map +1 -0
  8. package/dist/chunk-loader.d.ts +2 -2
  9. package/dist/compute-sorter.d.ts +1 -1
  10. package/dist/effects.d.ts +11 -10
  11. package/dist/effects.js.map +1 -1
  12. package/dist/formats/ksplat/index.d.ts +1 -1
  13. package/dist/formats/lcc/collision-mesh.d.ts +2 -2
  14. package/dist/formats/lcc.js +17 -16
  15. package/dist/formats/lcc.js.map +1 -1
  16. package/dist/formats/ply/index.d.ts +1 -1
  17. package/dist/formats/ply/parse-splat-ply.d.ts +2 -2
  18. package/dist/formats/ply.js +1 -1
  19. package/dist/formats/ply.js.map +1 -1
  20. package/dist/formats/rad/index.d.ts +1 -1
  21. package/dist/formats/rad/parse-rad.d.ts +1 -1
  22. package/dist/formats/rad/rad.d.ts +2 -2
  23. package/dist/formats/rad.js +3 -3
  24. package/dist/formats/rad.js.map +1 -1
  25. package/dist/formats/sog/index.d.ts +1 -1
  26. package/dist/formats/sog/parse-sog.d.ts +4 -4
  27. package/dist/formats/sog.js.map +1 -1
  28. package/dist/formats/splat/index.d.ts +1 -1
  29. package/dist/formats/spz/index.d.ts +1 -1
  30. package/dist/index.d.ts +16 -25
  31. package/dist/index.js +253 -9047
  32. package/dist/index.js.map +1 -1
  33. package/dist/{load-scene.d.ts → load-splat-data.d.ts} +15 -13
  34. package/dist/loaders.d.ts +15 -0
  35. package/dist/loaders.js +26 -0
  36. package/dist/loaders.js.map +1 -0
  37. package/dist/loading-wLo8vRbA.js.map +1 -1
  38. package/dist/loading.d.ts +8 -8
  39. package/dist/{splat-mesh-types-CTN_duw2.js → lod-scheduler-B0a_uBlv.js} +133 -140
  40. package/dist/lod-scheduler-B0a_uBlv.js.map +1 -0
  41. package/dist/lod-scheduler.d.ts +1 -1
  42. package/dist/lod-source.d.ts +5 -5
  43. package/dist/{splat-scene.d.ts → merged-splat-mesh.d.ts} +14 -15
  44. package/dist/orientation.d.ts +7 -7
  45. package/dist/{ply-header-BwgExCn-.js → ply-header-B9WllbPg.js} +3 -3
  46. package/dist/{ply-header-BwgExCn-.js.map → ply-header-B9WllbPg.js.map} +1 -1
  47. package/dist/radix-sorter-R0FCSqKu.js +178 -0
  48. package/dist/radix-sorter-R0FCSqKu.js.map +1 -0
  49. package/dist/relighting-CSQF3UcL.js +969 -0
  50. package/dist/relighting-CSQF3UcL.js.map +1 -0
  51. package/dist/relighting.d.ts +1 -1
  52. package/dist/selection.d.ts +13 -0
  53. package/dist/selection.js +137 -0
  54. package/dist/selection.js.map +1 -0
  55. package/dist/source-transform.d.ts +4 -4
  56. package/dist/{splat-budget-DXew_86c.js → splat-budget-DuVZspPQ.js} +7 -7
  57. package/dist/{splat-budget-DXew_86c.js.map → splat-budget-DuVZspPQ.js.map} +1 -1
  58. package/dist/splat-budget.d.ts +1 -1
  59. package/dist/splat-data-BZM5dhhx.js.map +1 -1
  60. package/dist/splat-data.d.ts +4 -4
  61. package/dist/splat-mesh-aQdcWA6v.js +1942 -0
  62. package/dist/splat-mesh-aQdcWA6v.js.map +1 -0
  63. package/dist/splat-mesh-material.d.ts +2 -2
  64. package/dist/splat-mesh-types-BZIko-_9.js +18 -0
  65. package/dist/splat-mesh-types-BZIko-_9.js.map +1 -0
  66. package/dist/splat-mesh-types.d.ts +14 -8
  67. package/dist/splat-mesh.d.ts +4 -4
  68. package/dist/splat-modifier.d.ts +5 -5
  69. package/dist/splat-partition.d.ts +1 -1
  70. package/dist/static-lod-entry.d.ts +12 -0
  71. package/dist/static-lod-splat-mesh.d.ts +2 -2
  72. package/dist/{static-lod-worker-D2fyqQZO.js → static-lod-worker-DktKnJkR.js} +46 -46
  73. package/dist/{static-lod-worker-D2fyqQZO.js.map → static-lod-worker-DktKnJkR.js.map} +1 -1
  74. package/dist/static-lod.js +148 -0
  75. package/dist/static-lod.js.map +1 -0
  76. package/dist/streamed-splat-mesh.d.ts +21 -27
  77. package/dist/streaming.d.ts +23 -0
  78. package/dist/streaming.js +2877 -0
  79. package/dist/streaming.js.map +1 -0
  80. package/dist/{unified-splat-renderer.d.ts → unified-splat-mesh.d.ts} +10 -10
  81. package/dist/unified-work-buffer.d.ts +66 -0
  82. package/dist/unified.d.ts +11 -0
  83. package/dist/unified.js +738 -0
  84. package/dist/unified.js.map +1 -0
  85. package/dist/webgpu-limits-Y3CI_6_6.js +43 -0
  86. package/dist/webgpu-limits-Y3CI_6_6.js.map +1 -0
  87. package/dist/webgpu-limits.d.ts +0 -59
  88. package/dist/work-buffer-gather.d.ts +1 -1
  89. package/package.json +24 -4
  90. package/dist/splat-mesh-types-CTN_duw2.js.map +0 -1
@@ -0,0 +1,1942 @@
1
+ var pt = Object.defineProperty;
2
+ var ft = (g, u, e) => u in g ? pt(g, u, { enumerable: !0, configurable: !0, writable: !0, value: e }) : g[u] = e;
3
+ var o = (g, u, e) => ft(g, typeof u != "symbol" ? u + "" : u, e);
4
+ import { r as Y, M as gt, a as mt } from "./splat-mesh-types-BZIko-_9.js";
5
+ import * as d from "three/webgpu";
6
+ import { uniform as b } from "three/tsl";
7
+ import { z as wt, e as xt, m as St, b as vt, D as yt, c as Rt, A as Z, B as bt, E as Tt, W as At, F as K, G as Q, H as J, I as Mt, i as It, j as kt, y as Pt, n as Ct, r as Ut, J as z, C as tt, f as Dt } from "./relighting-CSQF3UcL.js";
8
+ import { l as et, w as O } from "./logging-BfPdd7NJ.js";
9
+ import { d as Bt } from "./splat-budget-DuVZspPQ.js";
10
+ import { e as G } from "./half-float-Cs4FDq8Q.js";
11
+ import { a as Et, r as Wt, n as Lt } from "./sh-pack-D5wAe5gg.js";
12
+ function Ft() {
13
+ return new d.Matrix4().makeRotationX(Math.PI);
14
+ }
15
+ function Vt(g) {
16
+ switch (g) {
17
+ case "ply":
18
+ case "splat":
19
+ case "ksplat":
20
+ case "sog":
21
+ case "rad":
22
+ case "streamed-sog":
23
+ return Ft();
24
+ case "spz":
25
+ case "lcc":
26
+ case "lcc2":
27
+ case void 0:
28
+ return null;
29
+ }
30
+ }
31
+ const lt = `let w = new Float32Array(0), x = new Float32Array(0);
32
+ const f = new Uint32Array(65536);
33
+ let E = 0, S = new Uint32Array(0), M = new Float32Array(0), I = new Uint32Array(0), R = new Uint32Array(0), Y = new Uint32Array(0);
34
+ function j(e) {
35
+ e <= E || (E = e, S = new Uint32Array(e), M = new Float32Array(e), I = new Uint32Array(e), R = new Uint32Array(e), Y = new Uint32Array(e));
36
+ }
37
+ function q(e, r, n) {
38
+ const D = e[2], K = e[6], _ = e[10], k = e[14];
39
+ let c = 0;
40
+ for (let t = 0; t < r.length; t += 2) c += r[t + 1];
41
+ j(c);
42
+ let d = 1 / 0, F = -1 / 0, X = 0;
43
+ for (let t = 0; t < r.length; t += 2) {
44
+ const i = r[t], o = i + r[t + 1];
45
+ for (let s = i; s < o; s++) {
46
+ const a = s * 4, h = x[s], l = n ? h * 16 : 0, u = w[a], U = w[a + 1], g = w[a + 2], B = n ? n[l] * u + n[l + 4] * U + n[l + 8] * g + n[l + 12] : u, T = n ? n[l + 1] * u + n[l + 5] * U + n[l + 9] * g + n[l + 13] : U, Z = n ? n[l + 2] * u + n[l + 6] * U + n[l + 10] * g + n[l + 14] : g, A = D * B + K * T + _ * Z + k;
47
+ S[X] = s, M[X] = A, X++, A < d && (d = A), A > F && (F = A);
48
+ }
49
+ }
50
+ const z = 16777215 / (F - d || 1);
51
+ let y = R;
52
+ for (let t = 0; t < c; t++)
53
+ I[t] = (M[t] - d) * z, y[t] = t;
54
+ let p = Y;
55
+ for (let t = 0; t < 24; t += 16) {
56
+ f.fill(0);
57
+ for (let o = 0; o < c; o++) {
58
+ const s = I[y[o]] >>> t & 65535;
59
+ f[s] = f[s] + 1;
60
+ }
61
+ let i = 0;
62
+ for (let o = 0; o < 65536; o++) {
63
+ const s = f[o];
64
+ f[o] = i, i += s;
65
+ }
66
+ for (let o = 0; o < c; o++) {
67
+ const s = y[o], a = I[s] >>> t & 65535, h = f[a];
68
+ p[h] = s, f[a] = h + 1;
69
+ }
70
+ [y, p] = [p, y];
71
+ }
72
+ R = y, Y = p;
73
+ const b = new Uint32Array(c);
74
+ for (let t = 0; t < c; t++) b[t] = S[y[t]];
75
+ return b;
76
+ }
77
+ self.onmessage = (e) => {
78
+ const r = e.data;
79
+ if (r.type === "init") {
80
+ w = new Float32Array(r.capacity * 4), x = new Float32Array(r.capacity);
81
+ return;
82
+ }
83
+ if (r.type === "write") {
84
+ w.set(r.centers, r.start * 4), r.sourceIds && x.set(r.sourceIds, r.start);
85
+ return;
86
+ }
87
+ const n = q(r.modelView, r.spans, r.matrices), D = { type: "order", order: n };
88
+ self.postMessage(D, [n.buffer]);
89
+ };
90
+ //# sourceMappingURL=sort-worker-CDRn-GLW.js.map
91
+ `, it = typeof self < "u" && self.Blob && new Blob(["URL.revokeObjectURL(import.meta.url);", lt], { type: "text/javascript;charset=utf-8" });
92
+ function _t(g) {
93
+ let u;
94
+ try {
95
+ if (u = it && (self.URL || self.webkitURL).createObjectURL(it), !u) throw "";
96
+ const e = new Worker(u, {
97
+ type: "module",
98
+ name: g == null ? void 0 : g.name
99
+ });
100
+ return e.addEventListener("error", () => {
101
+ (self.URL || self.webkitURL).revokeObjectURL(u);
102
+ }), e;
103
+ } catch {
104
+ return new Worker(
105
+ "data:text/javascript;charset=utf-8," + encodeURIComponent(lt),
106
+ {
107
+ type: "module",
108
+ name: g == null ? void 0 : g.name
109
+ }
110
+ );
111
+ }
112
+ }
113
+ class qt {
114
+ constructor(u) {
115
+ o(this, "kind", "worker");
116
+ o(this, "worker");
117
+ o(this, "splatIndexAttribute");
118
+ o(this, "host");
119
+ o(this, "inFlight", !1);
120
+ /** Set by {@link dispose}; drops any already-delivered order message. */
121
+ o(this, "disposed", !1);
122
+ /** The active spans the in-flight sort was computed against. */
123
+ o(this, "sentSpans", null);
124
+ o(this, "submittedCount", 0);
125
+ o(this, "completedCount", 0);
126
+ o(this, "lastSubmittedAt", -1 / 0);
127
+ o(this, "lastCompletedAt", -1 / 0);
128
+ o(this, "lastLatencyMs", Number.NaN);
129
+ this.host = u, this.splatIndexAttribute = u.splatIndexAttribute, this.worker = new _t(), this.worker.onmessage = (t) => {
130
+ this.applyOrder(t.data.order);
131
+ }, this.worker.onerror = (t) => {
132
+ et("sort worker error - retrying on a later frame.", t.message), this.inFlight = !1;
133
+ }, this.worker.onmessageerror = () => {
134
+ et("sort worker message deserialization failed."), this.inFlight = !1;
135
+ };
136
+ const e = { type: "init", capacity: u.capacity };
137
+ this.worker.postMessage(e), this.pushCenters([{ start: 0, count: Math.ceil(u.capacity / u.rowWidth) }]);
138
+ }
139
+ sort(u, e, t) {
140
+ if (this.disposed || this.inFlight) return !1;
141
+ this.inFlight = !0, this.submittedCount++, this.lastSubmittedAt = performance.now(), this.pushCenters(this.host.takeDirtyRows());
142
+ const i = this.host.getActiveSpans();
143
+ this.sentSpans = i;
144
+ const s = {
145
+ type: "sort",
146
+ modelView: new Float32Array(u.elements),
147
+ spans: i,
148
+ matrices: this.host.perSource ? new Float32Array(this.host.perSource.matrices) : void 0
149
+ };
150
+ return this.worker.postMessage(s), !0;
151
+ }
152
+ /** Internal timing diagnostics used by the demo's opt-in XR A/B harness. */
153
+ snapshot() {
154
+ return {
155
+ submittedCount: this.submittedCount,
156
+ completedCount: this.completedCount,
157
+ lastSubmittedAt: this.lastSubmittedAt,
158
+ lastCompletedAt: this.lastCompletedAt,
159
+ lastLatencyMs: this.lastLatencyMs
160
+ };
161
+ }
162
+ dispose() {
163
+ this.disposed || (this.disposed = !0, this.worker.terminate());
164
+ }
165
+ /** Sends written pool rows to keep the worker's centers mirror current. */
166
+ pushCenters(u) {
167
+ var t;
168
+ const e = this.host.rowWidth;
169
+ for (const i of u) {
170
+ const s = this.host.centers.slice(
171
+ i.start * e * 4,
172
+ (i.start + i.count) * e * 4
173
+ ), r = i.start * e, n = i.count * e, a = (t = this.host.perSource) == null ? void 0 : t.sourceIds.slice(r, r + n), c = { type: "write", start: r, centers: s, sourceIds: a };
174
+ this.worker.postMessage(c, [s.buffer]);
175
+ }
176
+ }
177
+ applyOrder(u) {
178
+ var s, r;
179
+ if (this.inFlight = !1, this.completedCount++, this.lastCompletedAt = performance.now(), this.lastLatencyMs = this.lastCompletedAt - this.lastSubmittedAt, this.disposed) return;
180
+ const e = this.host.getActiveSpans(), t = this.sentSpans;
181
+ if (this.sentSpans = null, !t || t.length !== e.length) return;
182
+ for (let n = 0; n < t.length; n++)
183
+ if (t[n] !== e[n]) return;
184
+ const i = this.splatIndexAttribute.array;
185
+ for (let n = 0; n < u.length; n++)
186
+ i[n] = u[n];
187
+ this.splatIndexAttribute.addUpdateRange(0, u.length), this.splatIndexAttribute.needsUpdate = !0, (r = (s = this.host).onOrderApplied) == null || r.call(s);
188
+ }
189
+ }
190
+ const zt = (1 << 24) - 1, st = new d.Vector2(), V = new d.Raycaster(), rt = new d.Vector3();
191
+ function Ot(g, u, e) {
192
+ return ((g & 255) << 16 | (u & 255) << 8 | e & 255) / zt;
193
+ }
194
+ function Gt(g, u, e) {
195
+ return u + g * (e - u);
196
+ }
197
+ function Ht(g, u, e, t, i) {
198
+ st.set(g, u), V.setFromCamera(st, t), rt.copy(V.ray.direction).transformDirection(t.matrixWorldInverse);
199
+ const s = e / -rt.z;
200
+ return i.copy(V.ray.origin).addScaledVector(V.ray.direction, s), { point: i, distance: i.distanceTo(V.ray.origin) };
201
+ }
202
+ class Nt {
203
+ constructor(u) {
204
+ /** Alpha below which a splat is transparent enough to pick through. */
205
+ o(this, "alphaThreshold", b(0.1));
206
+ /** Camera planes for the pick material's depth encoding. */
207
+ o(this, "near", b(0.1));
208
+ o(this, "far", b(1e3));
209
+ o(this, "material", null);
210
+ o(this, "proxy", null);
211
+ o(this, "target", null);
212
+ /** Reused sub-frustum camera; preserves the caller's concrete camera type. */
213
+ o(this, "pickCamera", null);
214
+ o(this, "scene", new d.Scene());
215
+ /** Serializes picks: they share one render target and one renderer. */
216
+ o(this, "queue", Promise.resolve());
217
+ o(this, "point", new d.Vector3());
218
+ o(this, "savedClearColor", new d.Color());
219
+ this.host = u;
220
+ }
221
+ /** The pick uniforms the material graph binds. */
222
+ get uniforms() {
223
+ return { alphaThreshold: this.alphaThreshold, near: this.near, far: this.far };
224
+ }
225
+ pick(u, e, t, i) {
226
+ const s = this.queue.then(() => this.run(u, e, t, i));
227
+ return this.queue = s.then(
228
+ () => {
229
+ },
230
+ () => {
231
+ }
232
+ ), s;
233
+ }
234
+ /** Rebuilds the pick graph after a settings change. No-op before the first pick. */
235
+ rebuildMaterial() {
236
+ this.material && this.host.applyPickGraph(this.material);
237
+ }
238
+ /** Flags the pick graph for recompile (e.g. the modifier list changed). */
239
+ markNeedsUpdate() {
240
+ this.material && (this.material.needsUpdate = !0);
241
+ }
242
+ dispose() {
243
+ var u, e;
244
+ (u = this.material) == null || u.dispose(), this.material = null, this.scene.clear(), this.proxy = null, (e = this.target) == null || e.dispose(), this.target = null, this.pickCamera = null;
245
+ }
246
+ async run(u, e, t, i) {
247
+ if (this.host.isDisposed() || this.host.getActiveCount() === 0 || u.x < -1 || u.x > 1 || u.y < -1 || u.y > 1) return null;
248
+ if (wt(e))
249
+ throw new Error(
250
+ "SplatMesh.pick: an XR array camera has no single frustum to pick through. Pass one eye (renderer.xr.getCamera().cameras[i]) or a mono camera, and give `ndc` in that eye’s viewport."
251
+ );
252
+ e.updateMatrixWorld(!0), this.host.updateWorldMatrix(), this.host.prepare(e, t);
253
+ const s = Math.max(1, Math.floor(this.host.getViewportSize().x)), r = Math.max(1, Math.floor(this.host.getViewportSize().y)), n = Math.floor((u.x * 0.5 + 0.5) * s), a = Math.floor((u.y * 0.5 + 0.5) * r);
254
+ if (n < 0 || a < 0 || n >= s || a >= r) return null;
255
+ this.ensureResources(e);
256
+ const c = this.target, h = this.proxy, l = this.pickCamera;
257
+ this.cropCameraToPixel(l, e, s, r, n, a), this.host.setView(l, 1, 1);
258
+ const f = "near" in e && typeof e.near == "number" ? e.near : 0.1, x = "far" in e && typeof e.far == "number" ? e.far : 1e3;
259
+ this.near.value = f, this.far.value = x, this.alphaThreshold.value = (i == null ? void 0 : i.alphaThreshold) !== void 0 ? i.alphaThreshold : 0.1;
260
+ const p = this.host.mesh, w = t.getRenderTarget(), m = t.getScissorTest();
261
+ t.getClearColor(this.savedClearColor);
262
+ const S = t.getClearAlpha(), A = t.autoClear;
263
+ h.matrix.copy(p.matrixWorld), h.matrixWorld.copy(p.matrixWorld), h.visible = this.host.getPickVisible(), h.layers.mask = p.layers.mask, h.renderOrder = p.renderOrder, c.viewport.set(0, 0, 1, 1), c.scissor.set(0, 0, 1, 1);
264
+ const R = (() => {
265
+ try {
266
+ return t.setRenderTarget(c), t.setScissorTest(!1), t.setClearColor(0, 0), t.autoClear = !0, t.clear(), t.setScissorTest(!0), t.autoClear = !1, t.render(this.scene, l), t.readRenderTargetPixelsAsync(c, 0, 0, 1, 1);
267
+ } finally {
268
+ this.host.setView(e, s, r), t.setRenderTarget(w), t.setScissorTest(m), t.setClearColor(this.savedClearColor, S), t.autoClear = A;
269
+ }
270
+ })();
271
+ let y;
272
+ try {
273
+ y = await R;
274
+ } catch (L) {
275
+ if (this.host.isDisposed()) return null;
276
+ throw L;
277
+ }
278
+ if (this.host.isDisposed()) return null;
279
+ const M = y[0], U = y[1], I = y[2];
280
+ if (y[3] === 0) return null;
281
+ const B = Gt(Ot(M, U, I), f, x), { point: E, distance: W } = Ht(u.x, u.y, B, e, this.point);
282
+ return { point: E.clone(), distance: W };
283
+ }
284
+ ensureResources(u) {
285
+ this.target === null && (this.target = new d.RenderTarget(1, 1, {
286
+ format: d.RGBAFormat,
287
+ type: d.UnsignedByteType,
288
+ depthBuffer: !0,
289
+ stencilBuffer: !1
290
+ })), this.material === null && (this.material = new d.NodeMaterial(), this.host.applyPickGraph(this.material), this.proxy = new d.Mesh(this.host.mesh.geometry, this.material), this.proxy.matrixAutoUpdate = !1, this.proxy.frustumCulled = !1, this.scene.add(this.proxy)), (this.pickCamera === null || this.pickCamera.constructor !== u.constructor) && (this.pickCamera = u.clone());
291
+ }
292
+ /** Maps one original framebuffer pixel onto the complete 1×1 pick target. */
293
+ cropCameraToPixel(u, e, t, i, s, r) {
294
+ u.copy(e);
295
+ const n = u.projectionMatrix, a = n.elements, c = 2 * s + 1 - t, h = 2 * r + 1 - i;
296
+ for (let l = 0; l < 4; l++) {
297
+ const f = a[l * 4 + 3];
298
+ a[l * 4] = a[l * 4] * t - c * f, a[l * 4 + 1] = a[l * 4 + 1] * i - h * f;
299
+ }
300
+ u.projectionMatrixInverse.copy(n).invert();
301
+ }
302
+ }
303
+ const Xt = 1024, jt = 1 << 21;
304
+ class $t {
305
+ /**
306
+ * @param centers - The pool's CPU centers (stride 4: x,y,z,_ per splat).
307
+ * @param poolIndices - The pool indices to index (e.g. the active list).
308
+ * @param count - How many entries of `poolIndices` are valid.
309
+ */
310
+ constructor(u, e, t) {
311
+ /** Populated-region minimum corner (local space). */
312
+ o(this, "min", new Float64Array(3));
313
+ /** Per-axis cell size (local units). */
314
+ o(this, "cellSize", new Float64Array(3));
315
+ /** Per-axis cell counts. */
316
+ o(this, "dims", new Int32Array(3));
317
+ /** CSR cell offsets: cell c owns `items[start[c] .. start[c+1])`. */
318
+ o(this, "cellStart");
319
+ /** Pool indices, bucketed by cell. */
320
+ o(this, "items");
321
+ /** Pool centers array this grid points into (RGBA stride 4). */
322
+ o(this, "centers");
323
+ o(this, "count");
324
+ if (this.centers = u, this.count = t, t === 0) {
325
+ this.dims.set([1, 1, 1]), this.cellSize.set([1, 1, 1]), this.cellStart = new Int32Array(2), this.items = new Uint32Array(0);
326
+ return;
327
+ }
328
+ const i = [1 / 0, 1 / 0, 1 / 0], s = [-1 / 0, -1 / 0, -1 / 0];
329
+ for (let p = 0; p < t; p++) {
330
+ const w = e[p] * 4;
331
+ for (let m = 0; m < 3; m++) {
332
+ const S = u[w + m];
333
+ S < i[m] && (i[m] = S), S > s[m] && (s[m] = S);
334
+ }
335
+ }
336
+ const r = [
337
+ s[0] - i[0],
338
+ s[1] - i[1],
339
+ s[2] - i[2]
340
+ ], n = r.filter((p) => p > 0), a = n.reduce((p, w) => p * w, 1);
341
+ let c = n.length > 0 ? Math.pow(a / t, 1 / n.length) : 1;
342
+ (!(c > 0) || !Number.isFinite(c)) && (c = 1);
343
+ const h = (p) => {
344
+ for (let w = 0; w < 3; w++) {
345
+ const m = r[w];
346
+ this.dims[w] = m > 0 ? Math.max(1, Math.min(Xt, Math.round(m / p) || 1)) : 1, this.cellSize[w] = this.dims[w] > 0 && m / this.dims[w] || 1, this.min[w] = i[w];
347
+ }
348
+ };
349
+ for (h(c); this.dims[0] * this.dims[1] * this.dims[2] > jt; )
350
+ c *= 1.5, h(c);
351
+ const l = this.dims[0] * this.dims[1] * this.dims[2], f = new Int32Array(l + 1);
352
+ for (let p = 0; p < t; p++) f[this.cellOf(e[p]) + 1]++;
353
+ for (let p = 0; p < l; p++) f[p + 1] += f[p];
354
+ this.cellStart = f, this.items = new Uint32Array(t);
355
+ const x = Int32Array.from(f.subarray(0, l));
356
+ for (let p = 0; p < t; p++) {
357
+ const w = e[p];
358
+ this.items[x[this.cellOf(w)]++] = w;
359
+ }
360
+ }
361
+ /** The cell index of pool splat `p`, clamped into range. */
362
+ cellOf(u) {
363
+ const e = u * 4, t = this.axisCell(0, this.centers[e]), i = this.axisCell(1, this.centers[e + 1]);
364
+ return (this.axisCell(2, this.centers[e + 2]) * this.dims[1] + i) * this.dims[0] + t;
365
+ }
366
+ axisCell(u, e) {
367
+ const t = Math.floor((e - this.min[u]) / this.cellSize[u]);
368
+ return Math.max(0, Math.min(this.dims[u] - 1, t));
369
+ }
370
+ /**
371
+ * Visits every indexed splat within `radius` (local units) of the local
372
+ * point, calling `visit(poolIndex, distanceSq)`. Cells are pruned by their
373
+ * axis span and each candidate is distance-filtered here, so the callback
374
+ * only ever sees splats truly within the radius.
375
+ */
376
+ forEachWithin(u, e, t, i, s) {
377
+ if (this.count === 0 || i < 0) return;
378
+ const r = i * i, n = [0, 0, 0], a = [0, 0, 0], c = [u, e, t];
379
+ for (let h = 0; h < 3; h++) {
380
+ const l = Math.ceil(i / this.cellSize[h]), f = this.axisCell(h, c[h]);
381
+ n[h] = Math.max(0, f - l), a[h] = Math.min(this.dims[h] - 1, f + l);
382
+ }
383
+ for (let h = n[2]; h <= a[2]; h++)
384
+ for (let l = n[1]; l <= a[1]; l++) {
385
+ const f = (h * this.dims[1] + l) * this.dims[0], x = this.cellStart[f + n[0]], p = this.cellStart[f + a[0] + 1];
386
+ for (let w = x; w < p; w++) {
387
+ const m = this.items[w], S = m * 4, A = this.centers[S] - u, R = this.centers[S + 1] - e, y = this.centers[S + 2] - t, M = A * A + R * R + y * y;
388
+ M <= r && s(m, M);
389
+ }
390
+ }
391
+ }
392
+ /**
393
+ * The nearest indexed splat to the local point within `radius`, or null.
394
+ * Returns its pool index and squared local distance.
395
+ */
396
+ nearest(u, e, t, i) {
397
+ let s = -1, r = 1 / 0;
398
+ return this.forEachWithin(u, e, t, i, (n, a) => {
399
+ a < r && (r = a, s = n);
400
+ }), s < 0 ? null : { poolIndex: s, distSq: r };
401
+ }
402
+ }
403
+ const Yt = 4, v = class v extends d.Mesh {
404
+ constructor(e, t = {}) {
405
+ var W, L;
406
+ const i = bt(t.sortIntervalMs), s = ((W = Bt()) == null ? void 0 : W.isMobile) === !0, r = ct(t.maxStdDev) ?? 3, n = Jt(t.minSplatSizePx) ?? (s ? 1.5 : 0), a = !("capacity" in e), c = a ? e.count : e.capacity;
407
+ if (c <= 0) throw new Error("SplatMesh capacity must be positive.");
408
+ const h = (t.shBands ?? (Y(t.performanceProfile) === "smooth" ? 0 : gt)) !== 0, l = a ? h ? ((L = e.shPacked) == null ? void 0 : L.bands) ?? 0 : 0 : t.shBands ?? 0, f = t.pool, x = () => new Dt({
409
+ capacity: c,
410
+ floatTextures: t.poolFloatTextures,
411
+ packedShBands: l,
412
+ packedShTextureCount: l === 0 ? 0 : Math.ceil(J(l) / 4),
413
+ ...t.maxTextureSize === void 0 ? {} : { maxTextureSize: t.maxTextureSize }
414
+ }), p = f !== void 0 && l !== f.packedShBands;
415
+ p && O(
416
+ `a mesh with shBands ${l} cannot share a pool built for ${f == null ? void 0 : f.packedShBands}; it allocated its own pool instead.`
417
+ );
418
+ const w = f === void 0 || p, m = w ? x() : f, S = Math.min(
419
+ m.capacity,
420
+ Math.ceil(c / v.DATA_TEXTURE_WIDTH) * v.DATA_TEXTURE_WIDTH
421
+ ), A = m.centersTexture, R = m.colorsTexture, y = m.covarianceATexture, M = m.covarianceBTexture, U = a && e.sh && h ? Tt(
422
+ e.sh.palette,
423
+ e.sh.paletteWidth,
424
+ e.sh.paletteHeight,
425
+ d.FloatType
426
+ ) : null, I = m.shPackedTextures, D = new d.InstancedBufferGeometry();
427
+ D.setIndex([0, 1, 2, 0, 2, 3]), D.setAttribute(
428
+ "position",
429
+ new d.BufferAttribute(new Float32Array([-1, -1, 0, 1, -1, 0, 1, 1, 0, -1, 1, 0]), 3)
430
+ );
431
+ const B = new Float32Array(S);
432
+ for (let C = 0; C < S; C++) B[C] = C;
433
+ const E = new d.StorageInstancedBufferAttribute(B, 1);
434
+ D.setAttribute("splatIndex", E), D.instanceCount = 0;
435
+ super(D, new d.NodeMaterial());
436
+ /**
437
+ * The storage this mesh draws from. Held rather than inlined because a
438
+ * splat's draw identity is already independent of its pool slot (`splatIndex`
439
+ * indirects every instance), so the pool is a separable concern - see
440
+ * {@link SplatPool}.
441
+ */
442
+ o(this, "pool");
443
+ /** False when the pool was supplied by the caller, and so outlives this mesh. */
444
+ o(this, "ownsPool");
445
+ o(this, "splatIndexAttribute");
446
+ o(this, "sourceIndexAttribute");
447
+ o(this, "dataTextures");
448
+ /** True when constructed from a complete SplatData (single fixed range). */
449
+ o(this, "isStatic");
450
+ /** Constructor-created range, retained for static in-place LOD replacement. */
451
+ o(this, "staticRange");
452
+ o(this, "shEnabled");
453
+ /**
454
+ * Dequantization range for the packed SH, shared by every coefficient.
455
+ * Uniforms rather than constants: the first chunk to arrive supplies it,
456
+ * long after the material is built.
457
+ */
458
+ o(this, "shRange");
459
+ /** Set once the first packed-SH chunk has supplied {@link shRange}. */
460
+ o(this, "shRangeSet", !1);
461
+ /** Whether SH-less rows were neutral-filled before the scene range locked. */
462
+ o(this, "wrotePreLockNeutralSh", !1);
463
+ o(this, "ranges", /* @__PURE__ */ new Map());
464
+ o(this, "activeCount", 0);
465
+ /**
466
+ * Bumped whenever the resident splat set or its positions change (activate,
467
+ * deactivate, compact) - the signal the lazy spatial-query grid rebuilds on.
468
+ * A depth re-sort reorders the active list but changes neither, so it does
469
+ * not bump this. See {@link queryNearest}.
470
+ */
471
+ o(this, "queryEpoch", 0);
472
+ o(this, "queryGrid", null);
473
+ o(this, "queryGridEpoch", -1);
474
+ /**
475
+ * True when the shared depth-order buffer holds a *secondary* view's order
476
+ * (left by {@link renderView}), so the next primary {@link update} must
477
+ * re-sort even if its camera has not moved. See {@link renderView} (M10).
478
+ */
479
+ o(this, "orderIsForeign", !1);
480
+ /**
481
+ * True while the draw list holds a CPU-worker depth permutation rather than
482
+ * the identity active order. Active-list mutations must then resync the
483
+ * whole drawn prefix instead of patching slots: patching a *permutation*
484
+ * with identity active-list values creates duplicates and drops live splats
485
+ * for every frame until the worker's next order lands (the WebGL2 streamed
486
+ * flicker, ROADMAP L5).
487
+ */
488
+ o(this, "drawListSorted", !1);
489
+ /** CPU timings from the most recent render-preparation update (reset in place per frame). */
490
+ o(this, "updateTimings", {
491
+ activeListMs: 0,
492
+ uploadMs: 0,
493
+ sortSubmitMs: 0,
494
+ stagingTextureAllocations: 0,
495
+ activeListUpdateRanges: 0
496
+ });
497
+ // Protected so a unified {@link MergedSplatMesh} subclass can substitute a
498
+ // world-space sort bound (see {@link refreshSortBounds}).
499
+ o(this, "localBounds", new d.Box3().makeEmpty());
500
+ o(this, "boundingSphereLocal", new d.Sphere());
501
+ o(this, "boundsDirty", !0);
502
+ /** Cached {@link getUnifiedSourceView} result; see the invalidation checks there. */
503
+ o(this, "cachedUnifiedView", null);
504
+ o(this, "cachedUnifiedViewMatrixWorld", new d.Matrix4().makeScale(0, 0, 0));
505
+ o(this, "cachedUnifiedViewLocalBounds", new d.Sphere());
506
+ o(this, "cachedUnifiedViewWorldBounds", new d.Sphere());
507
+ /** Row spans written since the last flush, awaiting GPU upload. */
508
+ o(this, "pendingUploadRows", []);
509
+ /** Row spans awaiting mirroring to the CPU sort worker (WebGL2 path). */
510
+ o(this, "workerDirtyRows", []);
511
+ /** Persistent copy sources avoid allocating four temporary textures per upload region. */
512
+ o(this, "uploadStaging", /* @__PURE__ */ new Map());
513
+ /** Reusable float→half encode buffers keyed like {@link uploadStaging}. */
514
+ o(this, "halfEncodeBuffers", /* @__PURE__ */ new Map());
515
+ /** Opt-in per-splat channels by name; see {@link defineChannel}. */
516
+ o(this, "channels", /* @__PURE__ */ new Map());
517
+ /** Created on the first update, when the renderer backend is known. */
518
+ o(this, "sorter", null);
519
+ /**
520
+ * Set by a unified {@link MergedSplatMesh} subclass before the first sort: makes
521
+ * the WebGPU sorter transform each splat's center to world space by its
522
+ * source's matrix before measuring depth. `null` for an ordinary mesh, whose
523
+ * sorter is byte-identical to before. See `source-transform.ts`.
524
+ */
525
+ o(this, "perSourceSort", null);
526
+ /** Effect hooks folded into the vertex graph; see {@link modifiers}. */
527
+ o(this, "modifierList", []);
528
+ /** Increments when a unified gather must rebuild its source graph. */
529
+ o(this, "graphRevision", 0);
530
+ /** Version for unified work-buffer caching; source pool writes are observable. */
531
+ o(this, "contentRevision", 0);
532
+ /** Material-graph inputs kept for rebuilds when the modifier list changes. */
533
+ o(this, "materialInputs");
534
+ /** Projection focal lengths in pixels; updated every frame. */
535
+ o(this, "focal", b(new d.Vector2()));
536
+ /** Drawing-buffer size in pixels; updated every frame. */
537
+ o(this, "viewport", b(new d.Vector2()));
538
+ /** Camera position in this mesh's local space, for SH view dependence. */
539
+ o(this, "localCameraPosition", b(new d.Vector3()));
540
+ /**
541
+ * Frontier-cut limit for `foveationMode: 'frontier'` - the maximum
542
+ * `own_size / distance` a splat may have and still draw (Spark's
543
+ * `pixelScaleLimit`). Set each frame to `foveationTargetPx / focalPx` so the
544
+ * cut targets a fixed on-screen size. Unused in `'band'` mode.
545
+ */
546
+ o(this, "pixelScaleLimit", b(0));
547
+ /**
548
+ * Core projected-2D depth of field (live uniforms). Aperture `0` disables.
549
+ * Prefer this over the M13 `depthOfFieldPreset` modifier for camera DoF.
550
+ */
551
+ o(this, "dofFocusDistance", b(10));
552
+ o(this, "dofAperture", b(0));
553
+ /**
554
+ * Proxy-mesh screen-space relighting (live uniforms + map). `blend === 0`
555
+ * disables. Swapping the map rebuilds the material graph once.
556
+ */
557
+ o(this, "relightPlaceholder", St());
558
+ o(this, "relightMap", this.relightPlaceholder);
559
+ o(this, "relightBlend", b(0));
560
+ o(this, "relightBrightness", b(vt));
561
+ o(this, "relightBackground", b(yt));
562
+ o(this, "relightSoftness", b(Rt));
563
+ /**
564
+ * Live screen-radius band bounds (px), seeded from
565
+ * {@link SplatMeshOptions.minSplatScreenRadius} / `maxSplatScreenRadius` and
566
+ * moved by {@link setScreenRadiusBand} - so a foveated mesh can widen the band
567
+ * as its LOD cut refines rather than culling the detail the refinement bought.
568
+ */
569
+ o(this, "screenBandMin", b(0));
570
+ o(this, "screenBandMax", b(0));
571
+ /** The pick pass and everything it allocates; see `splat-mesh-picking.ts`. */
572
+ o(this, "picker", new Nt({
573
+ mesh: this,
574
+ isDisposed: () => this.disposed,
575
+ getActiveCount: () => this.activeCount,
576
+ getViewportSize: () => this.viewport.value,
577
+ getPickVisible: () => this.effectiveVisibility,
578
+ hasSorter: () => this.sorter !== null,
579
+ updateWorldMatrix: () => this.updateWorldMatrix(!0, !1),
580
+ prepare: (e, t) => {
581
+ this.flushPendingUploads(t), this.refreshProjectionUniforms(e, t), this.sorter !== null && this.requestSortIfNeeded(e, t);
582
+ },
583
+ setView: (e, t, i) => this.writeViewUniforms(e, t, i),
584
+ applyPickGraph: (e) => Z(
585
+ e,
586
+ "pick",
587
+ this.graphInputs(this.materialInputs.textures, this.materialInputs.sh)
588
+ )
589
+ }));
590
+ /** Visibility supplied by an owning unified renderer for source-only picks. */
591
+ o(this, "unifiedPickVisibility", null);
592
+ /** Set by {@link dispose}; subclasses use it to drop late async results. */
593
+ o(this, "disposed", !1);
594
+ /** The renderer this mesh last updated with, so {@link dispose} can free
595
+ * storage-attribute GPU buffers that never sat in a geometry. */
596
+ o(this, "lastRenderer", null);
597
+ o(this, "currentModelView", new d.Matrix4());
598
+ /** Initialized to an impossible matrix so the first frame always sorts. */
599
+ o(this, "lastSortedModelView", new d.Matrix4().makeScale(0, 0, 0));
600
+ o(this, "sortScheduler");
601
+ /** One-frame queue-headroom hint used before a staged atomic commit. */
602
+ o(this, "deferSortRequestOnce", !1);
603
+ /** Bumped by every active-list mutation; identifies a draw list. */
604
+ o(this, "activeListVersion", 0);
605
+ /** The active list the current depth order was built from. */
606
+ o(this, "sortedActiveListVersion", -1);
607
+ o(this, "sortStrategy");
608
+ /** Resolved only when `sortStrategy === 'radix'`; see {@link ensureRadixSorter}. */
609
+ o(this, "RadixSorterCtor", null);
610
+ o(this, "radixSorterLoad", null);
611
+ o(this, "performanceProfileValue");
612
+ /** Gaussian cutoff radius in σ; baked into the material graph. */
613
+ o(this, "maxStdDevValue");
614
+ /** Screen-space minimum splat radius in px; baked into the material graph. */
615
+ o(this, "minSplatSizePx");
616
+ /** Mip-Splatting 2D antialiasing filter; baked into the material graph. */
617
+ o(this, "antialias");
618
+ /** Format-selected low-pass and opacity-compensation policy. */
619
+ o(this, "projectedFilterProfile");
620
+ /** Emit sRGB colors for gamma-space compositing; baked into the material graph. */
621
+ o(this, "srgbOutput");
622
+ /** Screen-radius cull threshold in px (0 = off); baked into the material graph. */
623
+ o(this, "maxSplatScreenRadius");
624
+ o(this, "minSplatScreenRadius");
625
+ /** `.rad` foveation cut selector; baked into the material graph. */
626
+ o(this, "foveationMode");
627
+ /** Finest on-screen node size (px) the frontier cut may request. */
628
+ o(this, "foveationTargetPx");
629
+ /** Target upper bound on the frontier cut's drawn-splat count. */
630
+ o(this, "foveationDrawBudget");
631
+ /** Rendered major/minor axis-ratio cap (0 = off); baked into the material graph. */
632
+ o(this, "maxSplatAspect");
633
+ /** Spark LOD alpha encoding + merged-node rendering; baked into the material. */
634
+ o(this, "lodAlpha");
635
+ /** Live frontier limit in px; self-adjusts between `foveationTargetPx` (finest)
636
+ * and coarser to hold `foveationDrawBudget`. Feeds `pixelScaleLimit` each frame. */
637
+ o(this, "foveationLimitPx");
638
+ /** Throttle timestamp for the adaptive-limit estimate (ms, `performance.now`). */
639
+ o(this, "lastFoveationAdaptAt", -1 / 0);
640
+ /** Y-up normalization policy; drives the static self-orientation below. */
641
+ o(this, "orientation");
642
+ /** Set once the pool has been checked against the device texture limit. */
643
+ o(this, "textureLimitChecked", !1);
644
+ if (this.pool = m, this.ownsPool = w, m.register(this), this.sortScheduler = new At(i, s), this.sortStrategy = t.sortStrategy ?? "counting", this.sortStrategy === "radix" && this.ensureRadixSorter(), this.performanceProfileValue = Y(t.performanceProfile), this.maxStdDevValue = r, this.minSplatSizePx = n, this.antialias = t.antialias ?? (a ? e.antialias ?? !1 : !1), this.projectedFilterProfile = t.projectedFilterProfile ?? "default", this.srgbOutput = t.srgbOutput ?? !1, this.maxSplatScreenRadius = H(t.maxSplatScreenRadius), this.minSplatScreenRadius = H(t.minSplatScreenRadius), this.screenBandMin.value = this.minSplatScreenRadius, this.screenBandMax.value = this.maxSplatScreenRadius, this.foveationMode = mt(t.foveationMode), this.foveationTargetPx = ee(t.foveationTargetPx), this.foveationDrawBudget = se(t.foveationDrawBudget), this.foveationLimitPx = this.foveationTargetPx, this.maxSplatAspect = H(t.maxSplatAspect), this.lodAlpha = t.lodAlpha ?? !1, this.orientation = t.orientation ?? "y-up", this.splatIndexAttribute = E, this.sourceIndexAttribute = new d.StorageBufferAttribute(new Uint32Array(S), 1), this.dataTextures = m.coreTextures, this.isStatic = a, this.shEnabled = U !== null, this.shRange = { min: K(), max: K() }, this.frustumCulled = !1, U && (this.dataTextures = [...this.dataTextures, U]), I.length > 0 && (this.dataTextures = [...this.dataTextures, ...I]), this.materialInputs = {
645
+ textures: { centersTexture: A, colorsTexture: R, covarianceATexture: y, covarianceBTexture: M },
646
+ sh: a && e.sh && U ? { mode: "palette", bands: e.sh.bands, paletteTexture: U } : l !== 0 ? {
647
+ mode: "packed",
648
+ bands: l,
649
+ textures: I,
650
+ range: this.shRange
651
+ } : null
652
+ }, this.buildMaterial(this.materialInputs.textures, this.materialInputs.sh), a) {
653
+ this.staticRange = this.appendRange(e), this.pendingUploadRows = [], this.poolFloatTextures === "float16" && this.syncHalfFloatPoolImages();
654
+ const C = this.orientation === "y-up" ? Vt(e.format) : null;
655
+ C && (this.matrix.copy(C), this.matrix.decompose(this.position, this.quaternion, this.scale), this.matrixWorldNeedsUpdate = !0);
656
+ }
657
+ }
658
+ /**
659
+ * Pool index → this mesh's packed active-list slot. Lives on the pool: it is
660
+ * keyed by pool index, so a per-mesh copy would span the whole pool and
661
+ * several meshes sharing one would each pay for all of it. A row has one
662
+ * owner at a time, so the single array is unambiguous.
663
+ */
664
+ get activeSlotByPoolIndex() {
665
+ return this.pool.activeSlotByPoolIndex;
666
+ }
667
+ /** Rows in the pool backing this mesh. */
668
+ get poolRows() {
669
+ return this.pool.rows;
670
+ }
671
+ get centersTexture() {
672
+ return this.pool.centersTexture;
673
+ }
674
+ get backing() {
675
+ return this.pool.backing;
676
+ }
677
+ get freeRowSpans() {
678
+ return this.pool.freeRowSpans;
679
+ }
680
+ set freeRowSpans(e) {
681
+ this.pool.freeRowSpans = e;
682
+ }
683
+ get poolFloatTextures() {
684
+ return this.pool.floatTextures;
685
+ }
686
+ /** Bands of per-splat (non-palette) SH this pool stores; 0 when disabled. */
687
+ get packedShBands() {
688
+ return this.pool.packedShBands;
689
+ }
690
+ get shPackedTextures() {
691
+ return this.pool.shPackedTextures;
692
+ }
693
+ /**
694
+ * Higher-order SH bands this mesh actually renders (0 when it has none),
695
+ * whatever the source: a static mesh's palette shN, or per-splat SH in a
696
+ * dynamic pool. Reflects what was resolved at construction, not what was
697
+ * asked for - useful for a debug readout.
698
+ */
699
+ get shBands() {
700
+ var e;
701
+ return ((e = this.materialInputs.sh) == null ? void 0 : e.bands) ?? 0;
702
+ }
703
+ /**
704
+ * The contribution-culling profile currently in effect (resolved at
705
+ * construction, or the last {@link setPerformanceProfile} value).
706
+ */
707
+ get performanceProfile() {
708
+ return this.performanceProfileValue;
709
+ }
710
+ /**
711
+ * Changes contribution culling without changing scene data or persisted
712
+ * settings.
713
+ *
714
+ * Mutator convention: state that is plain data is a property (get/set pair);
715
+ * a `setX` method exists where the write has behavior - this one rebuilds the
716
+ * material graph, {@link StreamedSplatMesh.setBudget} returns its clamp.
717
+ * Every mutator has a matching readable property.
718
+ */
719
+ setPerformanceProfile(e) {
720
+ e !== this.performanceProfileValue && (this.performanceProfileValue = e, this.buildMaterial(this.materialInputs.textures, this.materialInputs.sh));
721
+ }
722
+ /**
723
+ * Gaussian cutoff radius currently in effect, in standard deviations.
724
+ * Resolved at construction, or the last {@link setMaxStdDev} value.
725
+ */
726
+ get maxStdDev() {
727
+ return this.maxStdDevValue;
728
+ }
729
+ /**
730
+ * Changes the Gaussian cutoff without changing scene data.
731
+ *
732
+ * Lowering it shrinks every quad and is the main fill-rate lever in a busy
733
+ * view; raising it restores the faint outer tail. Rebuilds the material
734
+ * graph, same convention as {@link setPerformanceProfile}.
735
+ */
736
+ setMaxStdDev(e) {
737
+ const t = ct(e);
738
+ t === void 0 || t === this.maxStdDevValue || (this.maxStdDevValue = t, this.cachedUnifiedView = null, this.buildMaterial(this.materialInputs.textures, this.materialInputs.sh));
739
+ }
740
+ /**
741
+ * Copies a decoded scene (or chunk) into the pool and activates it.
742
+ * Ranges are row-aligned in the pool textures, so each allocation may
743
+ * round up to the next multiple of the texture width internally.
744
+ *
745
+ * @returns A handle to pass to {@link removeRange}.
746
+ * @throws {Error} when the remaining pool capacity cannot fit the range.
747
+ */
748
+ appendRange(e) {
749
+ return this.appendRangeWithState(e, !0);
750
+ }
751
+ /** Appends uploaded data without exposing it to draw/sort until activated. */
752
+ appendInactiveRange(e) {
753
+ return this.appendRangeWithState(e, !1);
754
+ }
755
+ /** Reserves an inactive pool range whose data can be filled over multiple frames. */
756
+ reserveInactiveRange(e) {
757
+ if (!Number.isInteger(e) || e < 0)
758
+ throw new RangeError("SplatMesh.reserveInactiveRange: count must be a non-negative integer.");
759
+ if (e === 0) {
760
+ const a = Object.freeze({ count: 0 });
761
+ return this.ranges.set(a, { startRow: 0, rowCount: 0, start: 0, count: 0, active: !1 }), a;
762
+ }
763
+ const t = v.DATA_TEXTURE_WIDTH, i = Math.ceil(e / t), s = Q(this.freeRowSpans, i, this.poolRows), r = s * t, n = Object.freeze({ count: e });
764
+ this.ranges.set(n, { startRow: s, rowCount: i, start: r, count: e, active: !1 });
765
+ for (const a of this.channels.values())
766
+ a.backing.fill(a.fill, r, r + i * t), a.pendingRows.push({ start: s, count: i });
767
+ return n;
768
+ }
769
+ /** Writes one contiguous segment of a reserved inactive range. */
770
+ writeInactiveRange(e, t, i) {
771
+ const s = this.ranges.get(e);
772
+ if (!s) throw new Error("SplatMesh.writeInactiveRange: unknown range handle.");
773
+ if (s.active) throw new Error("SplatMesh.writeInactiveRange: range is already active.");
774
+ if (!Number.isInteger(i) || i < 0 || i + t.count > s.count)
775
+ throw new RangeError("SplatMesh.writeInactiveRange: write exceeds the reserved range.");
776
+ if (t.count === 0) return;
777
+ this.warnOnIgnoredSh(t, "writeInactiveRange");
778
+ const r = s.start + i;
779
+ this.writeSplatRows(r, t);
780
+ const n = v.DATA_TEXTURE_WIDTH, a = Math.floor(r / n), c = Math.floor((r + t.count - 1) / n);
781
+ this.markRowsWritten(a, c - a + 1), F.setFromArray(t.positions), this.localBounds.union(F), this.boundsDirty = !0;
782
+ }
783
+ /**
784
+ * Overwrites splats at `offset` within a range **in place** - unlike
785
+ * {@link writeInactiveRange} this allows an *active* range, so the frontier
786
+ * page table can page individual slots of its always-active slab. Queues the
787
+ * touched rows for upload; the caller must {@link invalidateSort} once per
788
+ * batch since splat depths changed.
789
+ */
790
+ overwriteRangeData(e, t, i) {
791
+ const s = this.ranges.get(e);
792
+ if (!s) throw new Error("SplatMesh.overwriteRangeData: unknown range handle.");
793
+ if (!Number.isInteger(i) || i < 0 || i + t.count > s.count)
794
+ throw new RangeError("SplatMesh.overwriteRangeData: write exceeds the range.");
795
+ if (t.count === 0) return;
796
+ this.warnOnIgnoredSh(t, "overwriteRangeData");
797
+ const r = s.start + i;
798
+ this.writeSplatRows(r, t);
799
+ const n = v.DATA_TEXTURE_WIDTH, a = Math.floor(r / n), c = Math.floor((r + t.count - 1) / n);
800
+ this.markRowsWritten(a, c - a + 1), F.setFromArray(t.positions), this.localBounds.union(F), this.boundsDirty = !0, this.contentRevision++;
801
+ }
802
+ /**
803
+ * Zeros splats `[offset, offset + count)` of a range so they draw nothing
804
+ * (zero covariance → degenerate quad, zero color). Used to free frontier slab
805
+ * slots that leave the frontier. Queues the rows for upload.
806
+ */
807
+ degenerateRange(e, t, i) {
808
+ const s = this.ranges.get(e);
809
+ if (!s) throw new Error("SplatMesh.degenerateRange: unknown range handle.");
810
+ if (i <= 0) return;
811
+ if (!Number.isInteger(t) || t < 0 || t + i > s.count)
812
+ throw new RangeError("SplatMesh.degenerateRange: range out of bounds.");
813
+ const r = s.start + t, { centers: n, colors: a, covarianceA: c, covarianceB: h } = this.backing;
814
+ n.fill(0, r * 4, (r + i) * 4), a.fill(0, r * 4, (r + i) * 4), c.fill(0, r * 4, (r + i) * 4), h.fill(0, r * 4, (r + i) * 4);
815
+ const l = v.DATA_TEXTURE_WIDTH, f = Math.floor(r / l), x = Math.floor((r + i - 1) / l);
816
+ this.markRowsWritten(f, x - f + 1), this.contentRevision++;
817
+ }
818
+ /**
819
+ * Copies one chunk's splats into the pool's backing arrays at `destination`.
820
+ * Shared by the append and staged-write paths, which differ only in where
821
+ * the rows come from and when they are activated.
822
+ */
823
+ writeSplatRows(e, t) {
824
+ const { centers: i, colors: s, covarianceA: r, covarianceB: n } = this.backing;
825
+ s.set(t.colors, e * 4);
826
+ const a = t.count, c = t.positions, h = t.covariances, l = this.shEnabled && t.sh ? t.sh.labels : null, f = t.frontierParent ?? null;
827
+ for (let x = 0; x < a; x++) {
828
+ const p = (e + x) * 4, w = x * 3, m = x * 6;
829
+ i[p + 0] = c[w + 0], i[p + 1] = c[w + 1], i[p + 2] = c[w + 2], r[p + 0] = h[m + 0], r[p + 1] = h[m + 1], r[p + 2] = h[m + 2], r[p + 3] = h[m + 3], n[p + 0] = h[m + 4], n[p + 1] = h[m + 5], n[p + 2] = l ? l[x] : 0, n[p + 3] = f ? f[x] : 0;
830
+ }
831
+ this.writePackedSh(e, t);
832
+ }
833
+ /**
834
+ * Scatters a chunk's packed SH words into the per-group pool textures.
835
+ *
836
+ * A chunk without SH still writes: pool rows are reused, so leaving the
837
+ * previous occupant's words behind would give a DC-only chunk somebody
838
+ * else's view-dependent color. Zero is not neutral (the range is signed and
839
+ * rarely symmetric), so the code for "0.0" in this scene's range is.
840
+ */
841
+ writePackedSh(e, t) {
842
+ if (this.packedShBands === 0) return;
843
+ const i = this.backing.shPacked, s = J(this.packedShBands), r = t.shPacked;
844
+ if (r && r.bands === this.packedShBands) {
845
+ this.applyShRange(r);
846
+ const a = this.currentShRange(), c = a !== null && !Et(r.range, a);
847
+ for (let h = 0; h < t.count; h++)
848
+ for (let l = 0; l < s; l++) {
849
+ const f = r.packed[h * s + l];
850
+ i[l >> 2][(e + h) * 4 + (l & 3)] = c ? Wt(f, r.range, a) : f;
851
+ }
852
+ return;
853
+ }
854
+ this.shRangeSet || (this.wrotePreLockNeutralSh = !0);
855
+ const n = this.neutralShWord();
856
+ for (let a = 0; a < t.count; a++)
857
+ for (let c = 0; c < s; c++)
858
+ i[c >> 2][(e + a) * 4 + (c & 3)] = n;
859
+ }
860
+ /**
861
+ * Adopts the first packed-SH chunk's dequantization range as the scene's one
862
+ * pool uniform. LCC/`.rad` chunks already share one range; a streamed SOG
863
+ * chunk measures its own extent, so later chunks are requantized into this
864
+ * one at write time ({@link writePackedSh}) rather than ignored.
865
+ */
866
+ applyShRange(e) {
867
+ if (!this.shRangeSet && (this.shRange.min.value.set(e.range.min[0], e.range.min[1], e.range.min[2]), this.shRange.max.value.set(e.range.max[0], e.range.max[1], e.range.max[2]), this.shRangeSet = !0, this.wrotePreLockNeutralSh)) {
868
+ const t = this.neutralShWord();
869
+ for (const i of this.backing.shPacked) i.fill(t);
870
+ this.markRowsWritten(0, this.poolRows), this.wrotePreLockNeutralSh = !1;
871
+ }
872
+ }
873
+ /** The scene's locked packed-SH range as a plain tuple, or null if unset. */
874
+ currentShRange() {
875
+ if (!this.shRangeSet) return null;
876
+ const { min: e, max: t } = this.shRange;
877
+ return {
878
+ min: [e.value.x, e.value.y, e.value.z],
879
+ max: [t.value.x, t.value.y, t.value.z]
880
+ };
881
+ }
882
+ /** The packed word decoding to 0 in every channel under the scene's range. */
883
+ neutralShWord() {
884
+ const e = this.currentShRange();
885
+ return e ? Lt(e) : 0;
886
+ }
887
+ /** Warns once per call site when a source's SH cannot be stored. */
888
+ warnOnIgnoredSh(e, t) {
889
+ e.sh && !this.isStatic && O(
890
+ `SplatMesh.${t}: palette shN data on appended ranges is ignored in dynamic-capacity mode (per-file palettes cannot be merged into a shared pool).`
891
+ ), e.shPacked && this.packedShBands === 0 && O(
892
+ `SplatMesh.${t}: per-splat SH was supplied but the pool has none allocated ` + (this.isStatic ? "(the static source had no `shPacked` at construction)." : "(construct the mesh with `shBands` to store it).")
893
+ );
894
+ }
895
+ /** Atomically includes or excludes a resident range on the next active-list rebuild. */
896
+ setRangeActive(e, t) {
897
+ const i = this.ranges.get(e);
898
+ if (!i) throw new Error("SplatMesh.setRangeActive: unknown range handle.");
899
+ i.active !== t && (i.active = t, t ? this.activateRecord(i) : this.deactivateRecord(i), this.contentRevision++);
900
+ }
901
+ /**
902
+ * Activates only the first `prefix` splats of a range (see
903
+ * {@link RangeRecord.activePrefix}). The page-table slab keeps its used slots
904
+ * densely packed at the front, so this bounds per-frame sorting and vertex
905
+ * work to the *drawn* frontier instead of the whole pool-sized slab. Write
906
+ * APIs (`overwriteRangeData` / `degenerateRange`) still address the full range.
907
+ */
908
+ setRangeActivePrefix(e, t) {
909
+ const i = this.ranges.get(e);
910
+ if (!i) throw new Error("SplatMesh.setRangeActivePrefix: unknown range handle.");
911
+ const s = Math.max(0, Math.min(i.count, Math.floor(t))), r = i.active ? i.activePrefix ?? i.count : 0;
912
+ s !== r && (i.active && (this.deactivateRecord(i), i.active = !1), i.activePrefix = s, s > 0 && (i.active = !0, this.activateRecord(i)), this.contentRevision++);
913
+ }
914
+ /**
915
+ * Replaces the drawn pool indices for an immutable static hierarchy.
916
+ *
917
+ * This is intentionally narrower than the range API: subclasses may select
918
+ * an arbitrary hierarchy frontier, but the referenced pool rows must remain
919
+ * resident for the lifetime of the mesh.
920
+ */
921
+ replaceActiveIndices(e) {
922
+ const t = this.sourceIndexAttribute.array;
923
+ if (e.length > t.length)
924
+ throw new RangeError("SplatMesh.replaceActiveIndices: frontier exceeds pool capacity.");
925
+ this.activeSlotByPoolIndex.fill(4294967295);
926
+ for (let s = 0; s < e.length; s++) {
927
+ const r = e[s];
928
+ if (r >= t.length)
929
+ throw new RangeError(
930
+ "SplatMesh.replaceActiveIndices: frontier contains an invalid pool index."
931
+ );
932
+ if (this.activeSlotByPoolIndex[r] !== 4294967295)
933
+ throw new Error(
934
+ "SplatMesh.replaceActiveIndices: frontier contains duplicate pool indices."
935
+ );
936
+ t[s] = r, this.activeSlotByPoolIndex[r] = s;
937
+ }
938
+ const i = this.activeCount;
939
+ this.activeCount = e.length, this.commitActiveListMutation(0, Math.max(i, this.activeCount)), this.queryEpoch++, this.contentRevision++;
940
+ }
941
+ /** Fast identity-frontier variant that avoids allocating a large index array. */
942
+ replaceActivePrefix(e) {
943
+ const t = this.sourceIndexAttribute.array, i = Math.max(0, Math.min(t.length, Math.floor(e)));
944
+ this.activeSlotByPoolIndex.fill(4294967295);
945
+ for (let r = 0; r < i; r++)
946
+ t[r] = r, this.activeSlotByPoolIndex[r] = r;
947
+ const s = this.activeCount;
948
+ this.activeCount = i, this.commitActiveListMutation(0, Math.max(s, i)), this.queryEpoch++, this.contentRevision++;
949
+ }
950
+ /** Replaces a static mesh's resident cut without increasing its fixed pool allocation. */
951
+ replaceStaticData(e) {
952
+ if (!this.isStatic || !this.staticRange)
953
+ throw new Error("SplatMesh.replaceStaticData: mesh was not constructed from static data.");
954
+ if (e.count > this.capacity)
955
+ throw new RangeError("SplatMesh.replaceStaticData: cut exceeds pool capacity.");
956
+ this.overwriteRangeData(this.staticRange, e, 0), this.setRangeActivePrefix(this.staticRange, e.count), this.invalidateSort();
957
+ }
958
+ appendRangeWithState(e, t) {
959
+ if (e.count === 0) {
960
+ const h = Object.freeze({ count: 0 });
961
+ return this.ranges.set(h, { startRow: 0, rowCount: 0, start: 0, count: 0, active: t }), h;
962
+ }
963
+ const i = v.DATA_TEXTURE_WIDTH, s = Math.ceil(e.count / i), r = Q(this.freeRowSpans, s, this.poolRows), n = r * i;
964
+ this.warnOnIgnoredSh(e, "appendRange"), this.writeSplatRows(n, e);
965
+ const a = Object.freeze({ count: e.count }), c = { startRow: r, rowCount: s, start: n, count: e.count, active: t };
966
+ this.ranges.set(a, c), this.markRowsWritten(r, s);
967
+ for (const h of this.channels.values())
968
+ h.backing.fill(h.fill, n, n + s * i), h.pendingRows.push({ start: r, count: s });
969
+ return F.setFromArray(e.positions), this.localBounds.union(F), this.boundsDirty = !0, t && this.activateRecord(c), this.contentRevision++, a;
970
+ }
971
+ /**
972
+ * Deactivates a previously appended range and frees its pool rows for
973
+ * reuse. The bounding sphere used for depth quantization stays a
974
+ * conservative superset until new ranges grow it again.
975
+ */
976
+ removeRange(e) {
977
+ const t = this.ranges.get(e);
978
+ if (!t) throw new Error("SplatMesh.removeRange: unknown range handle.");
979
+ t.active && this.deactivateRecord(t), this.ranges.delete(e), t.rowCount > 0 && (this.freeRowSpans = Mt(this.freeRowSpans, t.startRow, t.rowCount)), this.contentRevision++;
980
+ }
981
+ /** Maximum number of splats the pool can hold (row-aligned internally). */
982
+ get capacity() {
983
+ return this.poolRows * v.DATA_TEXTURE_WIDTH;
984
+ }
985
+ /** Number of splats currently active (drawn and depth-sorted). */
986
+ get activeSplatCount() {
987
+ return this.activeCount;
988
+ }
989
+ /**
990
+ * Returns this mesh's current pool state for an internal unified gather.
991
+ * Call {@link update} first for streamed sources so the active list reflects
992
+ * the current LOD cut. Consumers must treat the returned GPU resources as
993
+ * read-only; range lifecycle remains owned by this mesh.
994
+ */
995
+ getUnifiedSourceView() {
996
+ this.updateWorldMatrix(!0, !1), this.refreshSortBounds();
997
+ const e = this.cachedUnifiedView;
998
+ return e !== null && e.contentRevision === this.contentRevision && e.graphRevision === this.graphRevision && e.activeCount === this.activeCount && e.sh === this.materialInputs.sh && e.modifiers === this.modifierList && e.hasSourcePlacement === (this.perSourceSort !== null) && e.centersTexture === this.centersTexture && e.colorsTexture === this.materialInputs.textures.colorsTexture && this.cachedUnifiedViewMatrixWorld.equals(this.matrixWorld) && this.cachedUnifiedViewLocalBounds.equals(this.boundingSphereLocal) ? e : (this.cachedUnifiedViewMatrixWorld.copy(this.matrixWorld), this.cachedUnifiedViewLocalBounds.copy(this.boundingSphereLocal), this.cachedUnifiedViewWorldBounds.copy(this.boundingSphereLocal).applyMatrix4(this.matrixWorld), this.cachedUnifiedView = {
999
+ capacity: this.capacity,
1000
+ sourceIndex: this.sourceIndexAttribute,
1001
+ activeCount: this.activeCount,
1002
+ centersTexture: this.centersTexture,
1003
+ colorsTexture: this.materialInputs.textures.colorsTexture,
1004
+ covarianceATexture: this.materialInputs.textures.covarianceATexture,
1005
+ covarianceBTexture: this.materialInputs.textures.covarianceBTexture,
1006
+ dataTextureWidth: v.DATA_TEXTURE_WIDTH,
1007
+ matrixWorld: this.matrixWorld,
1008
+ worldBounds: this.cachedUnifiedViewWorldBounds,
1009
+ sh: this.materialInputs.sh,
1010
+ modifiers: this.modifierList,
1011
+ hasSourcePlacement: this.perSourceSort !== null,
1012
+ channels: this.channels,
1013
+ localCameraPosition: this.localCameraPosition,
1014
+ graphRevision: this.graphRevision,
1015
+ srgbOutput: this.srgbOutput,
1016
+ maxStdDev: this.maxStdDev,
1017
+ minSplatSizePx: this.minSplatSizePx,
1018
+ antialias: this.antialias,
1019
+ projectedFilterProfile: this.projectedFilterProfile,
1020
+ // Fixed at construction, so it needs no cache-invalidation key.
1021
+ lodAlpha: this.lodAlpha,
1022
+ contentRevision: this.contentRevision
1023
+ }, this.cachedUnifiedView);
1024
+ }
1025
+ /**
1026
+ * Marks this mesh as a source drawn by {@link UnifiedSplatMesh}.
1027
+ * The source stays invisible to the regular scene draw, while its own picker
1028
+ * mirrors this resolved visibility so existing per-source hit testing works.
1029
+ * Internal consumers should clear the state with `null` before disposing.
1030
+ */
1031
+ setUnifiedPickVisibility(e) {
1032
+ this.unifiedPickVisibility = e;
1033
+ }
1034
+ /**
1035
+ * The visibility that actually decides whether this mesh's splats reach the
1036
+ * screen: the owning {@link UnifiedSplatMesh}'s per-source visibility
1037
+ * while one owns the draw, else `Object3D.visible`. Consumers that gate on
1038
+ * "is this mesh showing" - the picker, `CameraBudgetGovernor` - must read
1039
+ * this rather than `visible`, which a unified renderer forces to `false` on
1040
+ * every source it owns purely to keep the regular scene draw from
1041
+ * double-drawing them.
1042
+ */
1043
+ get effectiveVisibility() {
1044
+ return this.unifiedPickVisibility ?? this.visible;
1045
+ }
1046
+ /**
1047
+ * Pool splats still allocatable, in whole rows. Because allocation is
1048
+ * row-aligned, an append of n splats fits when
1049
+ * `ceil(n / rowWidth) · rowWidth ≤ freeSplatCapacity` *and* a contiguous
1050
+ * span exists; callers use this for conservative pre-checks before
1051
+ * falling back to {@link compact}.
1052
+ */
1053
+ get freeSplatCapacity() {
1054
+ let e = 0;
1055
+ for (const t of this.freeRowSpans) e += t.count;
1056
+ return e * v.DATA_TEXTURE_WIDTH;
1057
+ }
1058
+ /**
1059
+ * Declares a named per-splat data channel - one extra value per pool slot,
1060
+ * in its own pool-aligned data texture. Channels are opt-in: nothing is
1061
+ * allocated until you call this. Read a channel from a modifier with
1062
+ * `ctx.channel(name)` and write values per range with {@link writeChannel}.
1063
+ *
1064
+ * Because a splat keeps its pool row for its whole residency, a channel
1065
+ * value follows the splat across depth re-sorts and pool {@link compact}ion
1066
+ * - the persistent per-splat label the SDF/selection effects build on. Call
1067
+ * this **before** assigning a modifier that reads the channel; a modifier
1068
+ * that reads an undeclared channel is a material-build error.
1069
+ *
1070
+ * @throws {Error} if a channel of this name already exists.
1071
+ */
1072
+ defineChannel(e, t = {}) {
1073
+ if (this.channels.has(e))
1074
+ throw new Error(`SplatMesh.defineChannel: channel "${e}" already defined.`);
1075
+ const i = t.type ?? "float", s = v.DATA_TEXTURE_WIDTH, r = s * this.poolRows, n = i === "byte" ? new Uint8Array(r) : new Float32Array(r), a = t.fill ?? 0;
1076
+ a && n.fill(a);
1077
+ const c = i === "byte" ? d.UnsignedByteType : d.FloatType, h = new d.DataTexture(
1078
+ n,
1079
+ s,
1080
+ this.poolRows,
1081
+ d.RedFormat,
1082
+ c
1083
+ );
1084
+ h.needsUpdate = !0, this.channels.set(e, { type: i, fill: a, backing: n, texture: h, textureType: c, pendingRows: [] });
1085
+ }
1086
+ /**
1087
+ * Writes channel values for a resident range, at `[offset, offset + data.length)`
1088
+ * splats within it (default `offset` 0). Values ride the same staging-upload
1089
+ * path as pool data, flushed on the next {@link update}. `byte` channels take
1090
+ * raw `0..255`; `float` channels take the value verbatim.
1091
+ *
1092
+ * @throws {Error} for an unknown channel or range, or a write past the range.
1093
+ */
1094
+ writeChannel(e, t, i, s = 0) {
1095
+ const r = this.channels.get(t);
1096
+ if (!r) throw new Error(`SplatMesh.writeChannel: channel "${t}" is not defined.`);
1097
+ const n = this.ranges.get(e);
1098
+ if (!n) throw new Error("SplatMesh.writeChannel: unknown range handle.");
1099
+ if (s < 0 || s + i.length > n.count)
1100
+ throw new Error(
1101
+ `SplatMesh.writeChannel: write of ${i.length} at offset ${s} exceeds range count ${n.count}.`
1102
+ );
1103
+ if (i.length === 0) return;
1104
+ const a = v.DATA_TEXTURE_WIDTH, c = n.start + s;
1105
+ r.backing.set(i, c);
1106
+ const h = Math.floor(c / a), l = Math.floor((c + i.length - 1) / a);
1107
+ r.pendingRows.push({ start: h, count: l - h + 1 }), this.contentRevision++;
1108
+ }
1109
+ /**
1110
+ * Effect hooks, folded into the vertex stage in array order - each
1111
+ * modifier sees the running result of the ones before it. Assigning a
1112
+ * changed list rebuilds the material (a pipeline recompile); animating a
1113
+ * modifier's own uniforms or storage buffers never does. With an empty
1114
+ * list the material is exactly the unhooked renderer.
1115
+ * See `docs/guide/effects-and-modifiers.md`.
1116
+ */
1117
+ get modifiers() {
1118
+ return this.modifierList;
1119
+ }
1120
+ set modifiers(e) {
1121
+ if (e.length === this.modifierList.length && e.every((s, r) => s === this.modifierList[r])) return;
1122
+ const i = this.modifierList;
1123
+ this.modifierList = [...e];
1124
+ try {
1125
+ this.rebuildGraph();
1126
+ } catch (s) {
1127
+ throw this.modifierList = i, this.buildMaterial(this.materialInputs.textures, this.materialInputs.sh), s;
1128
+ }
1129
+ }
1130
+ /**
1131
+ * Recompiles the material graph from the mesh's current inputs and publishes
1132
+ * the rebuild (`needsUpdate`, `graphRevision`, picker invalidation).
1133
+ *
1134
+ * A seam rather than inline code because a subclass can change a *build
1135
+ * input* without changing the modifier list - `MergedSplatMesh` installs its
1136
+ * per-source placement in the constructor, after the base class has already
1137
+ * built a placement-free graph. Note `defineChannel` deliberately does *not*
1138
+ * rebuild; only structural graph changes come through here.
1139
+ *
1140
+ * @internal
1141
+ */
1142
+ rebuildGraph() {
1143
+ this.buildMaterial(this.materialInputs.textures, this.materialInputs.sh), this.material.needsUpdate = !0, this.graphRevision++, this.picker.markNeedsUpdate();
1144
+ }
1145
+ /**
1146
+ * Core projected-2D depth of field. Adds an isotropic screen-space CoC disc
1147
+ * after EWA projection (not the stylized M13 scale modifier). Live uniforms
1148
+ * - no material rebuild. Pass `aperture: 0` to disable.
1149
+ */
1150
+ setDepthOfField(e) {
1151
+ const t = It(e, this.getDepthOfField());
1152
+ this.dofFocusDistance.value = t.focusDistance, this.dofAperture.value = t.aperture;
1153
+ }
1154
+ /**
1155
+ * PlayCanvas-style proxy-mesh relighting. Multiplies baked splat color in the
1156
+ * **display** fragment by a screen-space sample of `map` (RGB = lit proxy,
1157
+ * A = coverage). Pass `null` to disable (`blend → 0`, placeholder map).
1158
+ *
1159
+ * Blend / brightness / background are live uniforms (no rebuild). Changing
1160
+ * the map texture identity rebuilds the material once. Does not affect the
1161
+ * pick pass. Does not invalidate a unified gather cache.
1162
+ */
1163
+ setRelighting(e) {
1164
+ if (e === null) {
1165
+ this.relightBlend.value = 0, this.relightMap !== this.relightPlaceholder && (this.relightMap = this.relightPlaceholder, this.rebuildGraph());
1166
+ return;
1167
+ }
1168
+ const t = kt(e, this.getRelighting());
1169
+ this.relightBlend.value = t.blend, this.relightBrightness.value = t.brightness, this.relightBackground.value = t.background, this.relightSoftness.value = t.softness, e.map !== this.relightMap && (this.relightMap = e.map, this.rebuildGraph());
1170
+ }
1171
+ /** Current relight numeric uniforms (`blend === 0` means off). */
1172
+ getRelighting() {
1173
+ return {
1174
+ blend: this.relightBlend.value,
1175
+ brightness: this.relightBrightness.value,
1176
+ background: this.relightBackground.value,
1177
+ softness: this.relightSoftness.value
1178
+ };
1179
+ }
1180
+ /**
1181
+ * Moves the screen-radius band (px) without rebuilding the material.
1182
+ *
1183
+ * Only meaningful on a mesh constructed *with* a band - the graph decides at
1184
+ * build time whether to cull on radius at all, from
1185
+ * {@link SplatMeshOptions.minSplatScreenRadius} / `maxSplatScreenRadius`.
1186
+ *
1187
+ * A foveated mesh uses this to keep the band aligned with its LOD cut: the
1188
+ * band spans roughly one level, so refining the cut to spend spare budget
1189
+ * selects smaller splats, and a band left where it was would cull precisely
1190
+ * the detail that refinement bought.
1191
+ */
1192
+ setScreenRadiusBand(e, t) {
1193
+ this.screenBandMin.value = Math.max(0, e), this.screenBandMax.value = Math.max(0, t);
1194
+ }
1195
+ /** Current core DoF uniforms (`aperture === 0` means off). */
1196
+ getDepthOfField() {
1197
+ return {
1198
+ focusDistance: this.dofFocusDistance.value,
1199
+ aperture: this.dofAperture.value
1200
+ };
1201
+ }
1202
+ /**
1203
+ * Packs the resident ranges toward the start of the pool, removing the
1204
+ * gaps that add/remove churn leaves behind and restoring a single
1205
+ * contiguous free span. Use this when {@link appendRange} throws despite
1206
+ * enough total free rows (row-alignment fragmentation).
1207
+ *
1208
+ * The CPU backing arrays are authoritative, so no data is re-fetched;
1209
+ * moved rows are re-uploaded to the GPU on the next {@link update}.
1210
+ */
1211
+ compact() {
1212
+ this.pool.compact();
1213
+ }
1214
+ /** {@link SplatPoolTenant}: the ranges this mesh holds in the pool. */
1215
+ poolRanges() {
1216
+ return this.ranges.values();
1217
+ }
1218
+ /**
1219
+ * {@link SplatPoolTenant}: follow one range to its new rows. The pool has
1220
+ * already moved the splat data; this moves what the mesh keys by pool row.
1221
+ */
1222
+ relocatePoolRange(e, t) {
1223
+ const i = e, s = v.DATA_TEXTURE_WIDTH;
1224
+ for (const r of this.channels.values()) {
1225
+ const n = i.startRow * s, a = t * s, c = i.rowCount * s;
1226
+ r.backing.copyWithin(a, n, n + c), r.pendingRows.push({ start: t, count: i.rowCount });
1227
+ }
1228
+ i.startRow = t, i.start = t * s, this.markRowsWritten(t, i.rowCount);
1229
+ }
1230
+ /** {@link SplatPoolTenant}: rebuild everything keyed by pool index. */
1231
+ onPoolCompacted() {
1232
+ this.queryEpoch++, this.rebuildActiveList(), this.contentRevision++;
1233
+ }
1234
+ /**
1235
+ * Per-frame update: uploads pending pool writes, refreshes the projection
1236
+ * uniforms and requests a depth re-sort when the camera or the content
1237
+ * has changed. Call this every frame, before `renderer.render`.
1238
+ */
1239
+ update(e, t, i = {}) {
1240
+ if (this.disposed) return;
1241
+ this.lastRenderer = t, this.assertPoolFitsDevice(t), e.updateMatrixWorld(), this.updateWorldMatrix(!0, !1), this.updateTimings.activeListMs = 0, this.updateTimings.uploadMs = 0, this.updateTimings.sortSubmitMs = 0, this.updateTimings.stagingTextureAllocations = 0, this.updateTimings.activeListUpdateRanges = 0;
1242
+ const s = performance.now();
1243
+ this.flushPendingUploads(t), this.updateTimings.uploadMs = performance.now() - s;
1244
+ const r = Pt(e, t);
1245
+ let n = e, a = e, c, h;
1246
+ if (r ? (n = r.eye, a = r.head, c = r.width, h = r.height) : (t.getDrawingBufferSize(P), c = P.x, h = P.y), this.adaptFoveationLimit(n, h), this.writeViewUniforms(n, c, h, a), this.updateTimings.activeListUpdateRanges = this.sourceIndexAttribute.updateRanges.length, i.sort !== !1) {
1247
+ const l = performance.now();
1248
+ this.requestSortIfNeeded(a, t), this.updateTimings.sortSubmitMs = performance.now() - l;
1249
+ }
1250
+ }
1251
+ /** Returns the render-preparation CPU timings for the current update. */
1252
+ getUpdateTimings() {
1253
+ return this.updateTimings;
1254
+ }
1255
+ /**
1256
+ * Skips one otherwise ordinary sort request, but only while the active list
1257
+ * is unchanged since the last sort. Streamed staging uses this immediately
1258
+ * before a forced commit sort so the GPU queue has a frame to drain; a
1259
+ * concurrent swap group that mutates the active list revokes the request,
1260
+ * because the depth order must always describe the list being drawn.
1261
+ */
1262
+ deferNextSortRequest() {
1263
+ this.deferSortRequestOnce = !0;
1264
+ }
1265
+ /**
1266
+ * Once, on the first update (when the backend is known), fails loudly if the
1267
+ * pool's `2048 × poolRows` data textures exceed the device's maximum texture
1268
+ * dimension - otherwise the upload fails with a cryptic backend error and a
1269
+ * frozen canvas. Streamed scenes stay under the limit (budget-bounded); this
1270
+ * guards very large *static* scenes, especially on mobile (≈8192 max → about
1271
+ * 16.7 M splats). If the limit can't be read, the check is skipped rather
1272
+ * than risk a false rejection.
1273
+ */
1274
+ assertPoolFitsDevice(e) {
1275
+ if (this.textureLimitChecked) return;
1276
+ this.textureLimitChecked = !0;
1277
+ const t = Ct(e);
1278
+ if (t > 0 && this.poolRows > t) {
1279
+ const i = v.DATA_TEXTURE_WIDTH;
1280
+ throw new Error(
1281
+ `SplatMesh: this scene needs a ${i}×${this.poolRows} data texture, but this device caps texture dimensions at ${t} (about ${(t * i).toLocaleString("en-US")} splats). Stream it with StreamedSplatMesh, or reduce the splat count.`
1282
+ );
1283
+ }
1284
+ }
1285
+ /** Bounding box of the appended splats, in this mesh's local space. */
1286
+ computeSplatBounds() {
1287
+ return this.localBounds.clone();
1288
+ }
1289
+ /**
1290
+ * Asynchronously picks the frontmost visible splat under an NDC coordinate.
1291
+ *
1292
+ * Renders a one-pixel GPU depth pass (shared projection/Gaussian math with
1293
+ * the display material), reads the encoded view depth, and reconstructs a
1294
+ * world-space point on the splat's rendered center plane. Concurrent calls
1295
+ * on this mesh are serialized. Returns `null` for misses, empty meshes,
1296
+ * out-of-canvas coordinates, or after {@link dispose}.
1297
+ *
1298
+ * Does not return a persistent splat id - LOD streaming and pool reuse make
1299
+ * identity unstable across frames.
1300
+ */
1301
+ pick(e, t, i, s) {
1302
+ return this.picker.pick(e, t, i, s);
1303
+ }
1304
+ /**
1305
+ * The resident splat center nearest a world point, within `radius` (world
1306
+ * units), or `null` if none. A synchronous CPU query over the pool's decoded
1307
+ * centers - no GPU round-trip - backed by a uniform grid rebuilt only when
1308
+ * the resident set changes (M9). The primitive behind measurement markers and
1309
+ * proximity tests.
1310
+ *
1311
+ * **Resident-only:** a {@link StreamedSplatMesh} searches only the splats
1312
+ * currently in the pool (the LOD the camera has resolved), so the answer is
1313
+ * the nearest *loaded* splat - coarser far from the camera, absent where no
1314
+ * chunk has streamed in. A static mesh searches its whole scene.
1315
+ *
1316
+ * World-correct under any rotation + per-axis (including non-uniform) scale:
1317
+ * candidates are gathered in local space with a conservative radius
1318
+ * (`radius / min axis scale`) and then ranked by **world** distance, so the
1319
+ * nearest-in-world splat wins even when the axes stretch differently. Shear
1320
+ * in an ancestor transform is not supported. Splats displaced by GPU
1321
+ * modifiers are queried at their undisplaced CPU positions.
1322
+ */
1323
+ queryNearest(e, t) {
1324
+ if (this.activeCount === 0 || !(t >= 0)) return null;
1325
+ this.updateWorldMatrix(!0, !1), T.copy(e), this.worldToLocal(T);
1326
+ const i = this.ensureQueryGrid(), s = t / this.queryWorldMinScale(), r = t * t;
1327
+ let n = -1, a = 1 / 0;
1328
+ return i.forEachWithin(T.x, T.y, T.z, s, (h) => {
1329
+ this.splatWorldPosition(h, k);
1330
+ const l = k.distanceToSquared(e);
1331
+ l <= r && l < a && (a = l, n = h);
1332
+ }), n < 0 ? null : { point: this.splatWorldPosition(n, new d.Vector3()), distance: Math.sqrt(a) };
1333
+ }
1334
+ /**
1335
+ * Returns the first resident splat center inside a world-space ray cone.
1336
+ * This is a synchronous CPU query, intended for interactions that cannot
1337
+ * wait for (or rely on) a GPU readback. `radiusAtUnitDistance` is the cone's
1338
+ * world-space radius one unit from the origin; `minimumRadius` keeps nearby
1339
+ * point-like splats practical to target.
1340
+ *
1341
+ * Like {@link queryNearest}, this searches resident, undisplaced CPU centers.
1342
+ */
1343
+ queryRay(e, t = 0.025, i = 0.05) {
1344
+ if (this.activeCount === 0 || !(t >= 0) || !(i >= 0) || e.direction.lengthSq() === 0)
1345
+ return null;
1346
+ this.updateWorldMatrix(!0, !1);
1347
+ const s = this.sourceIndexAttribute.array.subarray(0, this.activeCount);
1348
+ let r = -1, n = 1 / 0;
1349
+ for (const a of s) {
1350
+ this.splatWorldPosition(a, k), T.subVectors(k, e.origin);
1351
+ const c = T.dot(e.direction);
1352
+ if (c < 0 || c >= n) continue;
1353
+ T.addScaledVector(e.direction, -c);
1354
+ const h = Math.max(i, c * t);
1355
+ T.lengthSq() <= h * h && (r = a, n = c);
1356
+ }
1357
+ return r < 0 ? null : {
1358
+ point: this.splatWorldPosition(r, new d.Vector3()),
1359
+ distance: n
1360
+ };
1361
+ }
1362
+ /**
1363
+ * The supporting surface beneath a world point: the highest resident splat
1364
+ * that lies no more than `maxDrop` below it (world −Y) and within `radius`
1365
+ * horizontally, or `null` if none. A downward probe over splat centers - the
1366
+ * primitive behind floor-following and teleport validation - without a GPU
1367
+ * pick or a collision mesh (M9), so it works for every format, not only the
1368
+ * `.lcc2` captures that ship collision geometry.
1369
+ *
1370
+ * `radius` (default `maxDrop / 2`) is the horizontal tolerance: splats are
1371
+ * points, so an exact vertical hit is unlikely and a small disc is searched.
1372
+ * Resident-only semantics and the world-correctness contract (rotation +
1373
+ * per-axis scale supported, shear not) are the same as {@link queryNearest};
1374
+ * every candidate is judged in world space, where the −Y probe direction is
1375
+ * defined, so the vertical test never assumes local axis alignment.
1376
+ */
1377
+ queryHeight(e, t, i = t / 2) {
1378
+ if (this.activeCount === 0 || !(t >= 0) || !(i >= 0)) return null;
1379
+ this.updateWorldMatrix(!0, !1), T.copy(e), this.worldToLocal(T);
1380
+ const s = this.ensureQueryGrid(), r = (t + i) / this.queryWorldMinScale(), n = i * i;
1381
+ let a = -1 / 0, c = -1;
1382
+ if (s.forEachWithin(T.x, T.y, T.z, r, (l) => {
1383
+ this.splatWorldPosition(l, k);
1384
+ const f = e.y - k.y;
1385
+ if (f < 0 || f > t) return;
1386
+ const x = k.x - e.x, p = k.z - e.z;
1387
+ x * x + p * p > n || k.y > a && (a = k.y, c = l);
1388
+ }), c < 0) return null;
1389
+ const h = this.splatWorldPosition(c, new d.Vector3());
1390
+ return { point: h, drop: e.y - h.y };
1391
+ }
1392
+ /**
1393
+ * Renders this mesh from a second camera - into `target`, or the canvas when
1394
+ * omitted - with depth order **and** projection correct for *that* camera
1395
+ * (M10). The primitive behind mirrors, portals, split panes, and thumbnails.
1396
+ *
1397
+ * Depth order is view-dependent, but a mesh keeps one sorted order buffer:
1398
+ * `update()` sorts it for the single camera it is given, so a second view
1399
+ * drawn with that order shows transparency/pop errors. `renderView` re-sorts
1400
+ * for `camera` into the shared buffer, sets the view-dependent uniforms
1401
+ * (viewport from `target`), draws, and marks the primary order stale so the
1402
+ * next `update()` re-sorts for the main camera. GPU submissions execute in
1403
+ * order, so each view's draw reads the order and uniforms it just wrote.
1404
+ *
1405
+ * Call once per extra view per frame, before or after the primary
1406
+ * `update()` + render - both are correct. On the WebGL2 fallback the sorter
1407
+ * is an asynchronous worker owned by the primary view, so `renderView` does
1408
+ * not sort at all there: the secondary view draws with the primary view's
1409
+ * order (projection is still per-camera correct) - a documented single-view
1410
+ * ordering limitation; WebGPU gets exact per-view order.
1411
+ *
1412
+ * @param target - Destination render target, or `null`/omitted for the canvas.
1413
+ */
1414
+ renderView(e, t, i = null) {
1415
+ if (this.disposed) return;
1416
+ this.lastRenderer = t, e.updateMatrixWorld(), this.updateWorldMatrix(!0, !1), this.flushPendingUploads(t), i ? P.set(i.width, i.height) : t.getDrawingBufferSize(P), this.writeViewUniforms(e, P.x, P.y);
1417
+ const s = this.sortForView(e, t), r = t.getRenderTarget();
1418
+ try {
1419
+ t.setRenderTarget(i), t.render(this, e);
1420
+ } finally {
1421
+ t.setRenderTarget(r), s && (this.orderIsForeign = !0);
1422
+ }
1423
+ }
1424
+ /**
1425
+ * Sorts the shared order buffer for a secondary view's camera, bypassing the
1426
+ * primary view's sort scheduler and its `lastSortedModelView` record (that
1427
+ * state belongs to `update()`'s camera). WebGPU dispatches synchronously into
1428
+ * the render queue, so the following draw reads this order.
1429
+ */
1430
+ sortForView(e, t) {
1431
+ return this.activeCount === 0 || !(t.backend.isWebGPUBackend === !0) || (this.currentModelView.multiplyMatrices(e.matrixWorldInverse, this.matrixWorld), this.refreshSortBounds(), this.sorter ?? (this.sorter = this.createSorter(t)), !this.sorter) ? !1 : (this.sorter.sort(this.currentModelView, this.activeCount, this.boundingSphereLocal), !0);
1432
+ }
1433
+ /** Rebuilds the query grid if the resident set changed since it was built. */
1434
+ ensureQueryGrid() {
1435
+ if (this.queryGrid && this.queryGridEpoch === this.queryEpoch) return this.queryGrid;
1436
+ const e = this.sourceIndexAttribute.array.subarray(0, this.activeCount);
1437
+ return this.queryGrid = new $t(this.backing.centers, e, this.activeCount), this.queryGridEpoch = this.queryEpoch, this.queryGrid;
1438
+ }
1439
+ /** World-space position of pool splat `poolIndex`, written into `out`. */
1440
+ splatWorldPosition(e, t) {
1441
+ const i = e * 4;
1442
+ return t.set(
1443
+ this.backing.centers[i],
1444
+ this.backing.centers[i + 1],
1445
+ this.backing.centers[i + 2]
1446
+ ), t.applyMatrix4(this.matrixWorld);
1447
+ }
1448
+ /**
1449
+ * The smallest world axis scale, for conservatively mapping a world radius
1450
+ * into local space: a world sphere of radius `r` fits inside a local sphere
1451
+ * of `r / minAxisScale`, for any per-axis (non-uniform) scale.
1452
+ */
1453
+ queryWorldMinScale() {
1454
+ return this.getWorldScale(q), Math.min(Math.abs(q.x), Math.abs(q.y), Math.abs(q.z)) || 1;
1455
+ }
1456
+ /**
1457
+ * Releases every GPU resource and worker this mesh owns. Idempotent: a
1458
+ * second call is a no-op, and a render loop that outlives the mesh by a
1459
+ * frame is safe - {@link update}, {@link renderView} and {@link pick} all
1460
+ * become no-ops after dispose.
1461
+ */
1462
+ dispose() {
1463
+ var e;
1464
+ if (!this.disposed) {
1465
+ this.disposed = !0, (e = this.sorter) == null || e.dispose(), this.sorter = null, this.geometry.dispose(), this.material.dispose(), this.picker.dispose(), this.relightPlaceholder.dispose();
1466
+ for (const t of this.ranges.values())
1467
+ this.pool.releaseRows(t.startRow, t.rowCount);
1468
+ this.pool.unregister(this), this.ownsPool && this.pool.dispose();
1469
+ for (const t of this.dataTextures)
1470
+ !this.ownsPool && this.pool.isPoolTexture(t) || t.dispose();
1471
+ for (const t of this.channels.values()) t.texture.dispose();
1472
+ this.channels.clear();
1473
+ for (const t of this.uploadStaging.values())
1474
+ for (const i of t.values()) i.dispose();
1475
+ this.uploadStaging.clear(), this.halfEncodeBuffers.clear(), this.lastRenderer && (Ut(this.lastRenderer, [this.sourceIndexAttribute]), this.lastRenderer = null), this.pendingUploadRows = [], this.workerDirtyRows = [], this.queryGrid = null, this.ranges.clear();
1476
+ }
1477
+ }
1478
+ /** Matches the viewport / focal / local-camera uniforms written by {@link update}. */
1479
+ refreshProjectionUniforms(e, t) {
1480
+ t.getDrawingBufferSize(P), this.writeViewUniforms(e, P.x, P.y);
1481
+ }
1482
+ /**
1483
+ * Writes the three view-dependent uniforms - viewport, focal (splat screen
1484
+ * size), and the camera position in mesh-local space (SH view direction +
1485
+ * screen-radius math) - for a camera drawing into a viewport of the given
1486
+ * pixel size. {@link update} passes the canvas size (or, while an XR session
1487
+ * presents, the per-eye viewport); {@link renderView} passes its render
1488
+ * target's size.
1489
+ *
1490
+ * @param positionCamera - Camera whose world position seeds the SH view
1491
+ * direction and screen-radius math. Defaults to `camera`; in XR it is the
1492
+ * head, so both eyes shade from one cyclopean point while each still
1493
+ * projects through its own eye.
1494
+ */
1495
+ writeViewUniforms(e, t, i, s = e) {
1496
+ this.viewport.value.set(t, i);
1497
+ const r = e.projectionMatrix.elements, n = r[5] * i / 2;
1498
+ this.focal.value.set(r[0] * t / 2, n), s.getWorldPosition(this.localCameraPosition.value), this.worldToLocal(this.localCameraPosition.value), this.foveationMode === "frontier" && n > 0 && (this.pixelScaleLimit.value = this.foveationLimitPx / n);
1499
+ }
1500
+ /**
1501
+ * Self-adjusts {@link foveationLimitPx} so the frontier cut's drawn-splat count
1502
+ * tracks {@link foveationDrawBudget} (Spark's `maxSplats` feedback). Estimates
1503
+ * the drawn count by replaying the cut on a strided sample of the pool, then
1504
+ * coarsens (grows the limit) when over budget and refines (shrinks toward the
1505
+ * finest {@link foveationTargetPx}) when comfortably under. Throttled; a no-op
1506
+ * outside `'frontier'` mode.
1507
+ *
1508
+ * `viewportY` must be the **same** height {@link writeViewUniforms} is given
1509
+ * for this frame - the two share `foveationLimitPx` in pixels and each
1510
+ * converts it with its own `focalY`, so a mismatch (the full drawing buffer
1511
+ * here against a per-eye viewport there) makes the estimator judge a cut the
1512
+ * shader is not applying, and the ratchet runs away toward the maximum.
1513
+ */
1514
+ adaptFoveationLimit(e, t) {
1515
+ if (this.foveationMode !== "frontier") return;
1516
+ const i = performance.now();
1517
+ if (i - this.lastFoveationAdaptAt < Kt) return;
1518
+ this.lastFoveationAdaptAt = i;
1519
+ const s = e.projectionMatrix.elements[5] * t / 2;
1520
+ if (!(s > 0)) return;
1521
+ const r = this.foveationLimitPx / s;
1522
+ e.getWorldPosition(_), this.worldToLocal(_), ot.copy(e.matrixWorld).invert(), at.multiplyMatrices(ot, this.matrixWorld).premultiply(e.projectionMatrix);
1523
+ const n = at.elements, { covarianceA: a, covarianceB: c, centers: h } = this.backing, l = c.length / 4;
1524
+ let f = 0, x = 0;
1525
+ for (let m = 0; m < l; m += ht) {
1526
+ const S = m * 4, A = c[S + 3];
1527
+ if (A === 0) continue;
1528
+ f++;
1529
+ const R = h[S], y = h[S + 1], M = h[S + 2], I = (n[3] * R + n[7] * y + n[11] * M + n[15]) * 1.2;
1530
+ if (n[2] * R + n[6] * y + n[10] * M + n[14] <= -I) continue;
1531
+ const B = n[0] * R + n[4] * y + n[8] * M + n[12];
1532
+ if (B > I || B < -I) continue;
1533
+ const E = n[1] * R + n[5] * y + n[9] * M + n[13];
1534
+ if (E > I || E < -I) continue;
1535
+ const W = a[S] + a[S + 3] + c[S + 1], L = 2 * Math.sqrt(Math.max(W, 0) / 3), C = A < 0, ut = Math.abs(A), N = R - _.x, X = y - _.y, j = M - _.z, $ = r * Math.sqrt(N * N + X * X + j * j), dt = C ? 0 : L;
1536
+ ut > $ && dt <= $ && x++;
1537
+ }
1538
+ if (f === 0) return;
1539
+ const p = x * ht, w = this.foveationDrawBudget;
1540
+ p > w * 1.1 ? this.foveationLimitPx = Math.min(this.foveationLimitPx * 1.15, Qt) : p < w * 0.7 && (this.foveationLimitPx = Math.max(this.foveationLimitPx / 1.12, this.foveationTargetPx));
1541
+ }
1542
+ /** Records a written row span for GPU upload and the sort-worker mirror. */
1543
+ markRowsWritten(e, t) {
1544
+ var i;
1545
+ this.pendingUploadRows.push({ start: e, count: t }), ((i = this.sorter) == null ? void 0 : i.kind) === "worker" && this.workerDirtyRows.push({ start: e, count: t });
1546
+ }
1547
+ /** Appends one newly active pool range to the packed source-index list. */
1548
+ activateRecord(e) {
1549
+ const t = e.activePrefix ?? e.count;
1550
+ if (t === 0) return;
1551
+ this.queryEpoch++;
1552
+ const i = this.sourceIndexAttribute.array, s = this.getPoolIndexTemplate(), r = this.activeCount;
1553
+ i.set(s.subarray(e.start, e.start + t), r), this.activeSlotByPoolIndex.set(
1554
+ s.subarray(r, r + t),
1555
+ e.start
1556
+ ), this.activeCount += t, this.commitActiveListMutation(r, t);
1557
+ }
1558
+ /**
1559
+ * Removes one active range and backfills its holes from the packed tail.
1560
+ * The common contiguous case uses one native block copy; the fallback
1561
+ * handles a range split by earlier tail backfills in O(record.count).
1562
+ */
1563
+ deactivateRecord(e) {
1564
+ const t = e.activePrefix ?? e.count;
1565
+ if (t === 0) return;
1566
+ this.queryEpoch++;
1567
+ const i = this.sourceIndexAttribute.array, s = this.activeSlotByPoolIndex[e.start];
1568
+ let r = !0;
1569
+ for (let c = 1; c < t; c++)
1570
+ if (this.activeSlotByPoolIndex[e.start + c] !== s + c) {
1571
+ r = !1;
1572
+ break;
1573
+ }
1574
+ let n = this.activeCount, a = 0;
1575
+ if (r) {
1576
+ const c = this.activeCount - t, h = Math.min(t, Math.max(0, c - s));
1577
+ if (h > 0) {
1578
+ i.copyWithin(s, this.activeCount - h, this.activeCount);
1579
+ for (let l = 0; l < h; l++) {
1580
+ const f = i[s + l];
1581
+ this.activeSlotByPoolIndex[f] = s + l;
1582
+ }
1583
+ n = s, a = s + h;
1584
+ }
1585
+ this.activeCount = c;
1586
+ } else {
1587
+ for (let c = e.start; c < e.start + t; c++) {
1588
+ const h = this.activeSlotByPoolIndex[c], l = this.activeCount - 1;
1589
+ if (h !== l) {
1590
+ const f = i[l];
1591
+ i[h] = f, this.activeSlotByPoolIndex[f] = h, n = Math.min(n, h), a = Math.max(a, h + 1);
1592
+ }
1593
+ this.activeCount--;
1594
+ }
1595
+ a = Math.min(a, this.activeCount);
1596
+ }
1597
+ this.commitActiveListMutation(n, Math.max(0, a - n));
1598
+ }
1599
+ /** Uploads only the changed packed slots and invalidates the current depth order. */
1600
+ commitActiveListMutation(e, t) {
1601
+ var i;
1602
+ if (t > 0 && (z(this.sourceIndexAttribute, e, t), this.sourceIndexAttribute.needsUpdate = !0), ((i = this.sorter) == null ? void 0 : i.kind) === "worker" || this.sorter === null) {
1603
+ const s = this.sourceIndexAttribute.array, r = this.splatIndexAttribute.array;
1604
+ this.drawListSorted ? (r.set(s.subarray(0, this.activeCount)), this.activeCount > 0 && z(this.splatIndexAttribute, 0, this.activeCount), this.splatIndexAttribute.needsUpdate = !0, this.drawListSorted = !1) : t > 0 && (r.set(s.subarray(e, e + t), e), z(this.splatIndexAttribute, e, t), this.splatIndexAttribute.needsUpdate = !0);
1605
+ }
1606
+ this.geometry.instanceCount = this.activeCount, this.activeListVersion++, this.sortScheduler.invalidateContent();
1607
+ }
1608
+ /** Rewrites the active-splat list (pool indices, range by range). */
1609
+ rebuildActiveList() {
1610
+ var s;
1611
+ const e = this.sourceIndexAttribute.array, t = this.getPoolIndexTemplate();
1612
+ let i = 0;
1613
+ for (const r of this.ranges.values()) {
1614
+ if (!r.active) continue;
1615
+ const n = r.activePrefix ?? r.count;
1616
+ e.set(t.subarray(r.start, r.start + n), i), this.activeSlotByPoolIndex.set(t.subarray(i, i + n), r.start), i += n;
1617
+ }
1618
+ this.activeCount = i, this.sourceIndexAttribute.clearUpdateRanges(), i > 0 && this.sourceIndexAttribute.addUpdateRange(0, i), this.sourceIndexAttribute.needsUpdate = !0, (((s = this.sorter) == null ? void 0 : s.kind) === "worker" || this.sorter === null) && (this.splatIndexAttribute.array.set(e.subarray(0, i)), this.splatIndexAttribute.clearUpdateRanges(), i > 0 && this.splatIndexAttribute.addUpdateRange(0, i), this.splatIndexAttribute.needsUpdate = !0, this.drawListSorted = !1), this.geometry.instanceCount = i, this.activeListVersion++, this.sortScheduler.invalidateContent();
1619
+ }
1620
+ /** The pool's reusable index ramp; shared by every mesh drawing from it. */
1621
+ getPoolIndexTemplate() {
1622
+ return this.pool.indexTemplate();
1623
+ }
1624
+ /**
1625
+ * Uploads rows written since the last flush by copying only those rows
1626
+ * through a staging texture. (Constructor-time writes never appear here;
1627
+ * they ride the textures' initial full upload instead.)
1628
+ */
1629
+ flushPendingUploads(e) {
1630
+ if (this.pendingUploadRows.length > 0) {
1631
+ const t = this.poolFloatTextures === "float16" ? d.HalfFloatType : d.FloatType;
1632
+ this.uploadRows(e, this.pendingUploadRows, d.RGBAFormat, 4, [
1633
+ {
1634
+ key: "centers",
1635
+ texture: this.dataTextures[0],
1636
+ data: this.backing.centers,
1637
+ type: t,
1638
+ encodeHalf: this.poolFloatTextures === "float16"
1639
+ },
1640
+ {
1641
+ key: "colors",
1642
+ texture: this.dataTextures[1],
1643
+ data: this.backing.colors,
1644
+ type: d.UnsignedByteType
1645
+ },
1646
+ {
1647
+ key: "covarianceA",
1648
+ texture: this.dataTextures[2],
1649
+ data: this.backing.covarianceA,
1650
+ type: t,
1651
+ encodeHalf: this.poolFloatTextures === "float16"
1652
+ },
1653
+ {
1654
+ key: "covarianceB",
1655
+ texture: this.dataTextures[3],
1656
+ data: this.backing.covarianceB,
1657
+ type: d.FloatType
1658
+ }
1659
+ ]), this.shPackedTextures.length > 0 && this.uploadRows(
1660
+ e,
1661
+ this.pendingUploadRows,
1662
+ d.RGBAIntegerFormat,
1663
+ 4,
1664
+ this.shPackedTextures.map((i, s) => ({
1665
+ key: `shPacked${s}`,
1666
+ texture: i,
1667
+ data: this.backing.shPacked[s],
1668
+ type: d.UnsignedIntType
1669
+ }))
1670
+ ), this.pendingUploadRows = [];
1671
+ }
1672
+ for (const [t, i] of this.channels.entries())
1673
+ i.pendingRows.length !== 0 && (this.uploadRows(e, i.pendingRows, d.RedFormat, 1, [
1674
+ {
1675
+ key: `channel:${t}`,
1676
+ texture: i.texture,
1677
+ data: i.backing,
1678
+ type: i.textureType
1679
+ }
1680
+ ]), i.pendingRows = []);
1681
+ }
1682
+ /**
1683
+ * Packs float32 CPU backing into the half GPU images for centers/covA.
1684
+ * Used once after a static constructor write so the initial `needsUpdate`
1685
+ * upload carries half bits (staging is skipped for that path).
1686
+ */
1687
+ syncHalfFloatPoolImages() {
1688
+ const e = this.dataTextures[0], t = this.dataTextures[2], i = e.image.data, s = t.image.data;
1689
+ G(this.backing.centers, i), G(this.backing.covarianceA, s), e.needsUpdate = !0, t.needsUpdate = !0;
1690
+ }
1691
+ /**
1692
+ * Uploads the given row spans of one or more same-layout pool textures via
1693
+ * per-region staging copies. Spans are merged first so consecutive appends
1694
+ * upload as one rectangle each - the copyTextureToTexture call count, not
1695
+ * the pixel volume, dominates.
1696
+ */
1697
+ uploadRows(e, t, i, s, r) {
1698
+ const n = v.DATA_TEXTURE_WIDTH, a = t.sort((h, l) => h.start - l.start), c = [];
1699
+ for (const h of a) {
1700
+ const l = c[c.length - 1];
1701
+ l && h.start <= l.start + l.count ? l.count = Math.max(l.count, h.start + h.count - l.start) : c.push({ start: h.start, count: h.count });
1702
+ }
1703
+ for (const h of c)
1704
+ for (const { key: l, texture: f, data: x, type: p, encodeHalf: w } of r) {
1705
+ const m = x.subarray(
1706
+ h.start * n * s,
1707
+ (h.start + h.count) * n * s
1708
+ );
1709
+ let S = m;
1710
+ if (w) {
1711
+ const R = this.acquireHalfEncodeBuffer(l, m.length);
1712
+ G(m, R, 0, m.length), S = R;
1713
+ }
1714
+ const A = this.acquireUploadStaging(
1715
+ l,
1716
+ S,
1717
+ n,
1718
+ h.count,
1719
+ i,
1720
+ p
1721
+ );
1722
+ e.copyTextureToTexture(A, f, null, Zt.set(0, h.start));
1723
+ }
1724
+ }
1725
+ /** Reuses a same-length half encode buffer for one staging key. */
1726
+ acquireHalfEncodeBuffer(e, t) {
1727
+ const i = this.halfEncodeBuffers.get(e);
1728
+ if (i && i.length === t) return i;
1729
+ const s = new Uint16Array(t);
1730
+ return this.halfEncodeBuffers.set(e, s), s;
1731
+ }
1732
+ /**
1733
+ * Reuses one of the recent same-sized staging textures for this upload. WebGPU
1734
+ * texture dimensions are immutable, so each cached entry remains exact-sized. Keeping
1735
+ * a small LRU per channel avoids recreating all core/SH staging resources when
1736
+ * a stream alternates among a few chunk heights, while bounding GPU memory.
1737
+ */
1738
+ acquireUploadStaging(e, t, i, s, r, n) {
1739
+ var l;
1740
+ let a = this.uploadStaging.get(e);
1741
+ a || (a = /* @__PURE__ */ new Map(), this.uploadStaging.set(e, a));
1742
+ const c = a.get(s);
1743
+ if (c)
1744
+ return c.image = { data: t, width: i, height: s }, c.needsUpdate = !0, a.delete(s), a.set(s, c), c;
1745
+ const h = new d.DataTexture(t, i, s, r, n);
1746
+ if (r === d.RGBAIntegerFormat && (h.magFilter = d.NearestFilter, h.minFilter = d.NearestFilter), h.needsUpdate = !0, a.set(s, h), a.size > Yt) {
1747
+ const f = a.keys().next().value;
1748
+ (l = a.get(f)) == null || l.dispose(), a.delete(f);
1749
+ }
1750
+ return this.updateTimings.stagingTextureAllocations++, h;
1751
+ }
1752
+ /** Gathers everything the material graph reads. See `splat-mesh-material.ts`. */
1753
+ graphInputs(e, t) {
1754
+ return {
1755
+ textures: e,
1756
+ sh: t,
1757
+ sourcePlacement: this.perSourceSort,
1758
+ // The uniform node instances, shared with the pick graph on purpose: one
1759
+ // per-frame write then reaches both.
1760
+ uniforms: {
1761
+ focal: this.focal,
1762
+ viewport: this.viewport,
1763
+ localCameraPosition: this.localCameraPosition,
1764
+ pixelScaleLimit: this.pixelScaleLimit,
1765
+ dofFocusDistance: this.dofFocusDistance,
1766
+ dofAperture: this.dofAperture,
1767
+ screenBandMin: this.screenBandMin,
1768
+ screenBandMax: this.screenBandMax,
1769
+ relightMap: this.relightMap,
1770
+ relightBlend: this.relightBlend,
1771
+ relightBrightness: this.relightBrightness,
1772
+ relightBackground: this.relightBackground,
1773
+ relightSoftness: this.relightSoftness
1774
+ },
1775
+ pick: this.picker.uniforms,
1776
+ settings: {
1777
+ maxStdDev: this.maxStdDev,
1778
+ minSplatSizePx: this.minSplatSizePx,
1779
+ antialias: this.antialias,
1780
+ projectedFilterProfile: this.projectedFilterProfile,
1781
+ srgbOutput: this.srgbOutput,
1782
+ performanceProfile: this.performanceProfileValue,
1783
+ maxScreenRadiusPx: this.maxSplatScreenRadius,
1784
+ minScreenRadiusPx: this.minSplatScreenRadius,
1785
+ foveationMode: this.foveationMode,
1786
+ maxAspect: this.maxSplatAspect,
1787
+ lodAlpha: this.lodAlpha
1788
+ },
1789
+ // The live map: a rebuild after defineChannel must see the new entry.
1790
+ channels: this.channels,
1791
+ modifiers: this.modifierList
1792
+ };
1793
+ }
1794
+ buildMaterial(e, t) {
1795
+ Z(
1796
+ this.material,
1797
+ "display",
1798
+ this.graphInputs(e, t)
1799
+ ), this.picker.rebuildMaterial();
1800
+ }
1801
+ requestSortIfNeeded(e, t) {
1802
+ if (this.deferSortRequestOnce && (this.deferSortRequestOnce = !1, this.activeListVersion === this.sortedActiveListVersion && !this.orderIsForeign) || this.activeCount === 0) return;
1803
+ this.currentModelView.multiplyMatrices(e.matrixWorldInverse, this.matrixWorld);
1804
+ const i = t.backend.isWebGPUBackend === !0, s = i ? performance.now() : 0;
1805
+ if (!this.orderIsForeign) {
1806
+ if (i) {
1807
+ if (!this.sortScheduler.shouldSubmit(
1808
+ this.currentModelView,
1809
+ this.lastSortedModelView,
1810
+ this.activeCount,
1811
+ s
1812
+ ))
1813
+ return;
1814
+ } else if (
1815
+ // WebGL2 has no cadence, but a content swap under a stationary camera
1816
+ // still invalidated the draw order (the swap reset it to unsorted
1817
+ // active order) - skipping here would leave the scene blend-order
1818
+ // broken until the camera next moved.
1819
+ !this.sortScheduler.hasPendingForce() && this.currentModelView.equals(this.lastSortedModelView)
1820
+ )
1821
+ return;
1822
+ }
1823
+ this.refreshSortBounds(), this.sorter ?? (this.sorter = this.createSorter(t)), this.sorter && this.sorter.sort(this.currentModelView, this.activeCount, this.boundingSphereLocal) && (this.lastSortedModelView.copy(this.currentModelView), this.sortedActiveListVersion = this.activeListVersion, this.orderIsForeign = !1, this.sortScheduler.markAccepted(s));
1824
+ }
1825
+ /**
1826
+ * Refreshes {@link boundingSphereLocal} - the sphere the sorter quantizes
1827
+ * depth over - when {@link boundsDirty}. The base mesh uses its local splat
1828
+ * bounds; a unified {@link MergedSplatMesh} overrides this to supply a world-space
1829
+ * bound spanning all its sources (whose transforms live in the shader).
1830
+ */
1831
+ refreshSortBounds() {
1832
+ this.boundsDirty && (this.localBounds.getBoundingSphere(this.boundingSphereLocal), this.boundsDirty = !1);
1833
+ }
1834
+ /** The data texture backing a channel defined with {@link defineChannel}. */
1835
+ channelTexture(e) {
1836
+ var t;
1837
+ return (t = this.channels.get(e)) == null ? void 0 : t.texture;
1838
+ }
1839
+ /** CPU backing storage of a float channel, for internal CPU-side consumers. */
1840
+ channelBacking(e) {
1841
+ const t = this.channels.get(e);
1842
+ if (!t || !(t.backing instanceof Float32Array))
1843
+ throw new Error(`SplatMesh: float channel "${e}" is missing.`);
1844
+ return t.backing;
1845
+ }
1846
+ /**
1847
+ * Forces the next {@link update} to re-sort even if the camera has not moved
1848
+ * - used by a unified pool when a source's transform changes, since the depth
1849
+ * order then changes without any camera motion.
1850
+ */
1851
+ invalidateSort() {
1852
+ this.sortScheduler.invalidate(), this.boundsDirty = !0;
1853
+ }
1854
+ createSorter(e) {
1855
+ if (!(e.backend.isWebGPUBackend === !0))
1856
+ return new qt({
1857
+ capacity: this.capacity,
1858
+ rowWidth: v.DATA_TEXTURE_WIDTH,
1859
+ centers: this.backing.centers,
1860
+ perSource: this.perSourceSort ? {
1861
+ sourceIds: this.perSourceSort.sourceIds,
1862
+ matrices: this.perSourceSort.matrices
1863
+ } : void 0,
1864
+ splatIndexAttribute: this.splatIndexAttribute,
1865
+ takeDirtyRows: () => {
1866
+ const s = this.workerDirtyRows;
1867
+ return this.workerDirtyRows = [], s;
1868
+ },
1869
+ getActiveSpans: () => {
1870
+ const s = new Uint32Array(this.ranges.size * 2);
1871
+ let r = 0;
1872
+ for (const n of this.ranges.values()) {
1873
+ if (!n.active) continue;
1874
+ const a = n.activePrefix ?? n.count;
1875
+ a !== 0 && (s[r++] = n.start, s[r++] = a);
1876
+ }
1877
+ return s.subarray(0, r);
1878
+ },
1879
+ onOrderApplied: () => {
1880
+ this.drawListSorted = !0;
1881
+ }
1882
+ });
1883
+ const i = {
1884
+ renderer: e,
1885
+ capacity: this.poolRows * v.DATA_TEXTURE_WIDTH,
1886
+ centersTexture: this.centersTexture,
1887
+ dataTextureWidth: v.DATA_TEXTURE_WIDTH,
1888
+ splatIndexAttribute: this.splatIndexAttribute,
1889
+ sourceIndexAttribute: this.sourceIndexAttribute
1890
+ };
1891
+ return this.perSourceSort ? new tt({ ...i, perSource: this.perSourceSort }) : this.sortStrategy === "radix" || this.sortStrategy === "exact" ? (this.ensureRadixSorter(), this.RadixSorterCtor ? new this.RadixSorterCtor({ ...i, exactDepth: this.sortStrategy === "exact" }) : null) : new tt(i);
1892
+ }
1893
+ /** Prefetches the experimental radix sorter; safe to call repeatedly. */
1894
+ ensureRadixSorter() {
1895
+ this.RadixSorterCtor || this.radixSorterLoad || (this.radixSorterLoad = import("./radix-sorter-R0FCSqKu.js").then((e) => {
1896
+ this.RadixSorterCtor = e.RadixSorter;
1897
+ }).finally(() => {
1898
+ this.radixSorterLoad = null;
1899
+ }));
1900
+ }
1901
+ };
1902
+ o(v, "DATA_TEXTURE_WIDTH", xt);
1903
+ let nt = v;
1904
+ const F = new d.Box3(), Zt = new d.Vector2(), T = new d.Vector3(), k = new d.Vector3(), q = new d.Vector3(), P = new d.Vector2(), _ = new d.Vector3(), ot = new d.Matrix4(), at = new d.Matrix4(), Kt = 180, ht = 31, Qt = 64;
1905
+ function ct(g) {
1906
+ if (g !== void 0 && (!Number.isFinite(g) || g <= 0))
1907
+ throw new RangeError("SplatMesh maxStdDev must be a finite number greater than 0.");
1908
+ return g;
1909
+ }
1910
+ function Jt(g) {
1911
+ if (g !== void 0 && (!Number.isFinite(g) || g < 0))
1912
+ throw new RangeError("SplatMesh minSplatSizePx must be a finite number >= 0.");
1913
+ return g;
1914
+ }
1915
+ function H(g) {
1916
+ if (g === void 0) return 0;
1917
+ if (!Number.isFinite(g) || g < 0)
1918
+ throw new RangeError("SplatMesh screen-radius cull must be a finite number ≥ 0.");
1919
+ return g;
1920
+ }
1921
+ const te = 1;
1922
+ function ee(g) {
1923
+ if (g === void 0) return te;
1924
+ if (!Number.isFinite(g) || g <= 0)
1925
+ throw new RangeError("SplatMesh foveationTargetPx must be a finite number > 0.");
1926
+ return g;
1927
+ }
1928
+ const ie = 9e5;
1929
+ function se(g) {
1930
+ if (g === void 0) return ie;
1931
+ if (!Number.isFinite(g) || g <= 0)
1932
+ throw new RangeError("SplatMesh foveationDrawBudget must be a finite number > 0.");
1933
+ return g;
1934
+ }
1935
+ export {
1936
+ ie as D,
1937
+ nt as S,
1938
+ te as a,
1939
+ Ft as c,
1940
+ Vt as y
1941
+ };
1942
+ //# sourceMappingURL=splat-mesh-aQdcWA6v.js.map