@voluma/vlam 0.2.4 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (138) hide show
  1. package/README.md +3 -4
  2. package/dist/chunk-loader-DlNwggfG.js +1859 -0
  3. package/dist/chunk-loader-DlNwggfG.js.map +1 -0
  4. package/dist/{compute-sorter.d.ts → core/compute-sorter.d.ts} +1 -1
  5. package/dist/core/index.d.ts +45 -0
  6. package/dist/{splat-scene.d.ts → core/merged-splat-mesh.d.ts} +14 -15
  7. package/dist/{orientation.d.ts → core/orientation.d.ts} +7 -7
  8. package/dist/{relighting.d.ts → core/relighting.d.ts} +1 -1
  9. package/dist/{source-transform.d.ts → core/source-transform.d.ts} +4 -4
  10. package/dist/{splat-budget.d.ts → core/splat-budget.d.ts} +2 -2
  11. package/dist/{splat-data.d.ts → core/splat-data.d.ts} +4 -4
  12. package/dist/{splat-mesh-material.d.ts → core/splat-mesh-material.d.ts} +2 -2
  13. package/dist/{splat-mesh-types.d.ts → core/splat-mesh-types.d.ts} +19 -10
  14. package/dist/{splat-mesh.d.ts → core/splat-mesh.d.ts} +4 -4
  15. package/dist/{splat-modifier.d.ts → core/splat-modifier.d.ts} +5 -5
  16. package/dist/{webgpu-limits.d.ts → core/webgpu-limits.d.ts} +0 -59
  17. package/dist/{effects.d.ts → effects/index.d.ts} +14 -13
  18. package/dist/effects.js.map +1 -1
  19. package/dist/formats/ksplat/index.d.ts +1 -1
  20. package/dist/formats/ksplat/parse-ksplat.d.ts +1 -1
  21. package/dist/formats/ksplat.js.map +1 -1
  22. package/dist/formats/lcc/collision-mesh.d.ts +3 -3
  23. package/dist/formats/lcc/collision-partition.d.ts +1 -1
  24. package/dist/formats/lcc/lcc.d.ts +3 -3
  25. package/dist/formats/lcc/lcc2.d.ts +2 -2
  26. package/dist/formats/lcc/parse-lcc.d.ts +1 -1
  27. package/dist/formats/lcc.js +17 -16
  28. package/dist/formats/lcc.js.map +1 -1
  29. package/dist/formats/ply/index.d.ts +1 -1
  30. package/dist/formats/ply/parse-compressed-ply.d.ts +2 -2
  31. package/dist/formats/ply/parse-splat-ply.d.ts +4 -4
  32. package/dist/formats/ply.js +1 -1
  33. package/dist/formats/ply.js.map +1 -1
  34. package/dist/formats/rad/frontier-worker-protocol.d.ts +8 -1
  35. package/dist/formats/rad/index.d.ts +2 -2
  36. package/dist/formats/rad/parse-rad.d.ts +2 -2
  37. package/dist/formats/rad/rad-foveated-source.d.ts +3 -3
  38. package/dist/formats/rad/rad.d.ts +7 -7
  39. package/dist/formats/rad.js +3 -3
  40. package/dist/formats/rad.js.map +1 -1
  41. package/dist/formats/sog/index.d.ts +1 -1
  42. package/dist/formats/sog/parse-sog.d.ts +6 -6
  43. package/dist/formats/sog.js.map +1 -1
  44. package/dist/formats/splat/index.d.ts +1 -1
  45. package/dist/formats/splat/parse-splat.d.ts +1 -1
  46. package/dist/formats/splat.js.map +1 -1
  47. package/dist/formats/spz/index.d.ts +1 -1
  48. package/dist/formats/spz/parse-spz.d.ts +1 -1
  49. package/dist/formats/spz.js.map +1 -1
  50. package/dist/half-float-Cs4FDq8Q.js.map +1 -1
  51. package/dist/index.js +253 -9047
  52. package/dist/index.js.map +1 -1
  53. package/dist/{chunk-loader.d.ts → loaders/chunk-loader.d.ts} +4 -4
  54. package/dist/loaders/index.d.ts +15 -0
  55. package/dist/{load-scene.d.ts → loaders/load-splat-data.d.ts} +16 -14
  56. package/dist/{load-worker-protocol.d.ts → loaders/load-worker-protocol.d.ts} +2 -2
  57. package/dist/{loading.d.ts → loaders/loading.d.ts} +8 -8
  58. package/dist/loaders.js +26 -0
  59. package/dist/loaders.js.map +1 -0
  60. package/dist/loading-wLo8vRbA.js.map +1 -1
  61. package/dist/{splat-mesh-types-CTN_duw2.js → lod-scheduler-B0a_uBlv.js} +133 -140
  62. package/dist/lod-scheduler-B0a_uBlv.js.map +1 -0
  63. package/dist/logging-BfPdd7NJ.js.map +1 -1
  64. package/dist/{ply-header-BwgExCn-.js → ply-header-B9WllbPg.js} +3 -3
  65. package/dist/ply-header-B9WllbPg.js.map +1 -0
  66. package/dist/radix-sorter-BrbUg_CV.js +178 -0
  67. package/dist/radix-sorter-BrbUg_CV.js.map +1 -0
  68. package/dist/relighting-Tiwep8yd.js +977 -0
  69. package/dist/relighting-Tiwep8yd.js.map +1 -0
  70. package/dist/selection/index.d.ts +13 -0
  71. package/dist/{selection-volume.d.ts → selection/selection-volume.d.ts} +1 -1
  72. package/dist/{splat-partition.d.ts → selection/splat-partition.d.ts} +2 -2
  73. package/dist/selection.js +137 -0
  74. package/dist/selection.js.map +1 -0
  75. package/dist/sh-pack-D5wAe5gg.js.map +1 -1
  76. package/dist/{splat-budget-DXew_86c.js → splat-budget-DuVZspPQ.js} +7 -7
  77. package/dist/splat-budget-DuVZspPQ.js.map +1 -0
  78. package/dist/splat-data-BZM5dhhx.js.map +1 -1
  79. package/dist/splat-mesh-CsLOQb08.js +1942 -0
  80. package/dist/splat-mesh-CsLOQb08.js.map +1 -0
  81. package/dist/splat-mesh-types-BZIko-_9.js +18 -0
  82. package/dist/splat-mesh-types-BZIko-_9.js.map +1 -0
  83. package/dist/static-lod/index.d.ts +12 -0
  84. package/dist/{static-lod-splat-mesh.d.ts → static-lod/static-lod-splat-mesh.d.ts} +3 -3
  85. package/dist/{static-lod-worker-protocol.d.ts → static-lod/static-lod-worker-protocol.d.ts} +1 -1
  86. package/dist/{static-lod.d.ts → static-lod/static-lod.d.ts} +1 -1
  87. package/dist/{static-lod-worker-D2fyqQZO.js → static-lod-worker-DktKnJkR.js} +46 -46
  88. package/dist/{static-lod-worker-D2fyqQZO.js.map → static-lod-worker-DktKnJkR.js.map} +1 -1
  89. package/dist/static-lod.js +148 -0
  90. package/dist/static-lod.js.map +1 -0
  91. package/dist/{budget-governor.d.ts → streaming/budget-governor.d.ts} +4 -4
  92. package/dist/{camera-budget-governor.d.ts → streaming/camera-budget-governor.d.ts} +11 -11
  93. package/dist/{chunk-cache-budget.d.ts → streaming/chunk-cache-budget.d.ts} +5 -5
  94. package/dist/{chunk-fetch-scheduler.d.ts → streaming/chunk-fetch-scheduler.d.ts} +4 -4
  95. package/dist/{dataset-source.d.ts → streaming/dataset-source.d.ts} +1 -1
  96. package/dist/streaming/index.d.ts +23 -0
  97. package/dist/{lod-scheduler.d.ts → streaming/lod-scheduler.d.ts} +1 -1
  98. package/dist/{lod-source.d.ts → streaming/lod-source.d.ts} +9 -9
  99. package/dist/{streamed-splat-mesh-utils.d.ts → streaming/streamed-splat-mesh-utils.d.ts} +2 -2
  100. package/dist/{streamed-splat-mesh.d.ts → streaming/streamed-splat-mesh.d.ts} +66 -32
  101. package/dist/streaming.js +2911 -0
  102. package/dist/streaming.js.map +1 -0
  103. package/dist/unified/index.d.ts +11 -0
  104. package/dist/{unified-splat-renderer.d.ts → unified/unified-splat-mesh.d.ts} +23 -16
  105. package/dist/unified/unified-work-buffer.d.ts +66 -0
  106. package/dist/{work-buffer-gather.d.ts → unified/work-buffer-gather.d.ts} +6 -6
  107. package/dist/{work-buffer-material.d.ts → unified/work-buffer-material.d.ts} +1 -1
  108. package/dist/unified.js +746 -0
  109. package/dist/unified.js.map +1 -0
  110. package/dist/webgpu-limits-Y3CI_6_6.js +43 -0
  111. package/dist/webgpu-limits-Y3CI_6_6.js.map +1 -0
  112. package/package.json +27 -7
  113. package/dist/index.d.ts +0 -54
  114. package/dist/ply-header-BwgExCn-.js.map +0 -1
  115. package/dist/splat-budget-DXew_86c.js.map +0 -1
  116. package/dist/splat-mesh-types-CTN_duw2.js.map +0 -1
  117. /package/dist/{create-splat-renderer.d.ts → core/create-splat-renderer.d.ts} +0 -0
  118. /package/dist/{depth-of-field.d.ts → core/depth-of-field.d.ts} +0 -0
  119. /package/dist/{half-float.d.ts → core/half-float.d.ts} +0 -0
  120. /package/dist/{logging.d.ts → core/logging.d.ts} +0 -0
  121. /package/dist/{radix-sort.d.ts → core/radix-sort.d.ts} +0 -0
  122. /package/dist/{radix-sorter.d.ts → core/radix-sorter.d.ts} +0 -0
  123. /package/dist/{relighting-pass.d.ts → core/relighting-pass.d.ts} +0 -0
  124. /package/dist/{sh-pack.d.ts → core/sh-pack.d.ts} +0 -0
  125. /package/dist/{sort-scheduler.d.ts → core/sort-scheduler.d.ts} +0 -0
  126. /package/dist/{sort-worker-protocol.d.ts → core/sort-worker-protocol.d.ts} +0 -0
  127. /package/dist/{sorter.d.ts → core/sorter.d.ts} +0 -0
  128. /package/dist/{splat-depth-pack.d.ts → core/splat-depth-pack.d.ts} +0 -0
  129. /package/dist/{splat-mesh-picking.d.ts → core/splat-mesh-picking.d.ts} +0 -0
  130. /package/dist/{splat-mesh-pool.d.ts → core/splat-mesh-pool.d.ts} +0 -0
  131. /package/dist/{splat-modifier-stack.d.ts → core/splat-modifier-stack.d.ts} +0 -0
  132. /package/dist/{splat-query.d.ts → core/splat-query.d.ts} +0 -0
  133. /package/dist/{splat-sort-bounds.d.ts → core/splat-sort-bounds.d.ts} +0 -0
  134. /package/dist/{storage-attribute-mirror.d.ts → core/storage-attribute-mirror.d.ts} +0 -0
  135. /package/dist/{worker-sorter.d.ts → core/worker-sorter.d.ts} +0 -0
  136. /package/dist/{xr-view.d.ts → core/xr-view.d.ts} +0 -0
  137. /package/dist/{ply-header.d.ts → loaders/ply-header.d.ts} +0 -0
  138. /package/dist/{lod-manifest.d.ts → streaming/lod-manifest.d.ts} +0 -0
@@ -0,0 +1,2911 @@
1
+ var Me = Object.defineProperty;
2
+ var Fe = (o, s, e) => s in o ? Me(o, s, { enumerable: !0, configurable: !0, writable: !0, value: e }) : o[s] = e;
3
+ var u = (o, s, e) => Fe(o, typeof s != "symbol" ? s + "" : s, e);
4
+ import { h as Be, f as W, a as Pe, l as ke } from "./splat-budget-DuVZspPQ.js";
5
+ import * as w from "three/webgpu";
6
+ import { S as Ee, a as ie, y as xe } from "./splat-mesh-CsLOQb08.js";
7
+ import { L as We, r as C } from "./lod-scheduler-B0a_uBlv.js";
8
+ import { S as q, t as ee, r as Ae, i as N, a as _ } from "./loading-wLo8vRbA.js";
9
+ import { y as Le } from "./relighting-Tiwep8yd.js";
10
+ import { C as Ve } from "./chunk-loader-DlNwggfG.js";
11
+ import { s as Ie } from "./sh-pack-D5wAe5gg.js";
12
+ import { w as E } from "./logging-BfPdd7NJ.js";
13
+ import { r as De, M as Ge, a as ne, i as j } from "./splat-mesh-types-BZIko-_9.js";
14
+ const Ne = 32e3;
15
+ function _e(o) {
16
+ return o.map((s) => ({
17
+ adds: [s],
18
+ removes: [],
19
+ leafStart: s.leafStart,
20
+ leafEnd: s.leafEnd,
21
+ addCount: s.count
22
+ })).sort((s, e) => s.leafStart - e.leafStart);
23
+ }
24
+ function Oe(o, s) {
25
+ const e = [...o, ...s.map(([, n]) => n.run)];
26
+ if (e.length > 0 && e.every((n) => n.coverageGroup !== void 0)) {
27
+ const n = /* @__PURE__ */ new Map(), a = (l) => {
28
+ let c = n.get(l);
29
+ return c || (c = { adds: [], removes: [] }, n.set(l, c)), c;
30
+ };
31
+ for (const l of o)
32
+ a(l.coverageGroup).adds.push(l);
33
+ for (const l of s)
34
+ a(l[1].run.coverageGroup).removes.push(l);
35
+ const r = [];
36
+ for (const l of n.values()) {
37
+ const c = /* @__PURE__ */ new Map(), h = (d, g) => `${d}:${g}`, f = (d, g) => {
38
+ const v = h(d, g);
39
+ let m = c.get(v);
40
+ return m || (m = { adds: [], removes: [], leafStart: d, leafEnd: g, addCount: 0 }, c.set(v, m)), m;
41
+ };
42
+ for (const d of l.adds) {
43
+ const g = f(d.leafStart, d.leafEnd);
44
+ g.adds.push(d), g.addCount += d.count;
45
+ }
46
+ for (const d of l.removes) {
47
+ const g = d[1].run, v = f(g.leafStart, g.leafEnd);
48
+ v.removes.push(d), v.leafStart = Math.min(v.leafStart, g.leafStart), v.leafEnd = Math.max(v.leafEnd, g.leafEnd);
49
+ }
50
+ r.push(...c.values());
51
+ }
52
+ return r.sort((l, c) => l.leafStart - c.leafStart);
53
+ }
54
+ const t = [
55
+ ...o.map((n) => ({
56
+ start: n.leafStart,
57
+ end: n.leafEnd,
58
+ add: n,
59
+ remove: void 0
60
+ })),
61
+ ...s.map((n) => ({
62
+ start: n[1].run.leafStart,
63
+ end: n[1].run.leafEnd,
64
+ add: void 0,
65
+ remove: n
66
+ }))
67
+ ].sort((n, a) => n.start - a.start || a.end - n.end), i = [];
68
+ for (const n of t) {
69
+ const a = i[i.length - 1];
70
+ (!a || n.start >= a.leafEnd) && i.push({
71
+ adds: [],
72
+ removes: [],
73
+ leafStart: n.start,
74
+ leafEnd: n.end,
75
+ addCount: 0
76
+ });
77
+ const r = i[i.length - 1];
78
+ r.leafEnd = Math.max(r.leafEnd, n.end), n.add && (r.adds.push(n.add), r.addCount += n.add.count), n.remove && r.removes.push(n.remove);
79
+ }
80
+ return i;
81
+ }
82
+ function ae(o) {
83
+ if (o.adds.length === 0) return 1e3;
84
+ const s = -Math.max(...o.adds.map((e) => e.level));
85
+ return o.removes.length === 0 ? -1e3 + s : s;
86
+ }
87
+ function ze(o) {
88
+ return o.length > 0 && o.every(
89
+ (s) => [...s.adds, ...s.removes.map(([, e]) => e.run)].every(
90
+ (e) => e.coverageGroup !== void 0
91
+ )
92
+ );
93
+ }
94
+ function Ue(o, s) {
95
+ const e = (n) => {
96
+ const a = [...n.adds, ...n.removes.map(([, r]) => r.run)];
97
+ return {
98
+ removeOnly: n.adds.length === 0 ? 1 : 0,
99
+ view: a.some((r) => r.inView !== !1) ? 0 : 1,
100
+ finest: n.adds.some((r) => r.level === 0) ? 0 : 1,
101
+ screen: Math.min(...a.map((r) => r.screenImportance ?? Number.POSITIVE_INFINITY)),
102
+ distance: Math.min(...a.map((r) => r.distance ?? Number.POSITIVE_INFINITY))
103
+ };
104
+ }, t = e(o), i = e(s);
105
+ return t.removeOnly - i.removeOnly || t.view - i.view || t.finest - i.finest || t.screen - i.screen || t.distance - i.distance || o.leafStart - s.leafStart;
106
+ }
107
+ function re(o) {
108
+ switch (o) {
109
+ case "environment":
110
+ return -1;
111
+ case "finest-target":
112
+ return 0;
113
+ case "coverage":
114
+ return 1;
115
+ case "target":
116
+ return 2;
117
+ default:
118
+ return 3;
119
+ }
120
+ }
121
+ function He(o) {
122
+ return o === "background" ? "base" : "priority";
123
+ }
124
+ function G(o, s) {
125
+ return o.coverageGroup < 0 ? `file:${s}` : o.phase === "finest-target" ? `cell:${o.coverageGroup}` : `slice:${o.coverageGroup}:${o.leafStart}:${o.leafEnd}`;
126
+ }
127
+ function $e(o, s = 10, e = !1) {
128
+ const t = te(s), i = /* @__PURE__ */ new Map();
129
+ for (const [n, a] of o) {
130
+ if (a.phase === "environment") continue;
131
+ const r = G(a, n), l = i.get(r);
132
+ if (!l) {
133
+ i.set(r, {
134
+ distance: a.distance,
135
+ count: 1,
136
+ inView: a.inView,
137
+ screenImportance: a.screenImportance,
138
+ finest: a.phase === "finest-target"
139
+ });
140
+ continue;
141
+ }
142
+ a.distance < l.distance && (l.distance = a.distance), a.inView && (l.inView = !0), a.screenImportance < l.screenImportance && (l.screenImportance = a.screenImportance), a.phase === "finest-target" && (l.finest = !0), l.count++;
143
+ }
144
+ for (const [n, a] of o) {
145
+ if (a.phase === "environment") {
146
+ a.kind = "priority", a.groupDistance = 0, a.groupPending = 1, a.groupInView = !0, a.groupScreenImportance = Number.NEGATIVE_INFINITY, a.groupFinest = !0, a.groupId = "environment", a.groupClass = 0;
147
+ continue;
148
+ }
149
+ const r = G(a, n), l = i.get(r);
150
+ if (!l) continue;
151
+ const c = l.inView ? 0 : l.distance <= t ? 1 : 2, h = c === 0 || c === 1 && e ? "priority" : "base";
152
+ a.groupDistance = l.distance, a.groupPending = l.count, a.groupInView = l.inView, a.groupScreenImportance = l.screenImportance, a.groupFinest = l.finest, a.groupId = r, a.groupClass = c, a.kind = h;
153
+ }
154
+ }
155
+ function te(o, s = 2) {
156
+ return o;
157
+ }
158
+ function Ye(o) {
159
+ return o.adds.some((s) => s.level === 0);
160
+ }
161
+ function qe(o, s, e = 2) {
162
+ if (o.level === 0) return "finest-target";
163
+ const t = o.distance ?? Number.POSITIVE_INFINITY;
164
+ return o.inView === !1 && t > te(s) ? "background" : "target";
165
+ }
166
+ function je(o, s, e = 2) {
167
+ const t = o.distance ?? Number.POSITIVE_INFINITY;
168
+ return o.inView === !1 && t > te(s) ? "background" : "coverage";
169
+ }
170
+ function oe(o, s, e, t) {
171
+ const i = t.distance ?? Number.POSITIVE_INFINITY, n = t.level, a = t.inView !== !1, r = t.coverageGroup ?? -1, l = t.screenImportance ?? Number.POSITIVE_INFINITY, c = He(e), h = o.get(s);
172
+ if (!h) {
173
+ const v = G(
174
+ { phase: e, coverageGroup: r, leafStart: t.leafStart, leafEnd: t.leafEnd },
175
+ s
176
+ );
177
+ o.set(s, {
178
+ kind: c,
179
+ phase: e,
180
+ distance: i,
181
+ level: n,
182
+ inView: a,
183
+ coverageGroup: r,
184
+ leafStart: t.leafStart,
185
+ leafEnd: t.leafEnd,
186
+ screenImportance: l,
187
+ groupDistance: i,
188
+ groupPending: 1,
189
+ groupInView: a,
190
+ groupScreenImportance: l,
191
+ groupFinest: e === "finest-target",
192
+ groupId: v,
193
+ // Provisional; finalized in stampClassicFetchGroups().
194
+ groupClass: 2
195
+ });
196
+ return;
197
+ }
198
+ const f = re(e) < re(h.phase), d = e === h.phase && (i < h.distance || i === h.distance && n < h.level), g = e === h.phase && i === h.distance && n === h.level && a && !h.inView;
199
+ (f || d || g) && o.set(s, {
200
+ kind: c,
201
+ phase: e,
202
+ distance: i,
203
+ level: n,
204
+ inView: a || h.inView,
205
+ coverageGroup: r >= 0 ? r : h.coverageGroup,
206
+ leafStart: t.leafStart,
207
+ leafEnd: t.leafEnd,
208
+ screenImportance: l,
209
+ groupDistance: Math.min(i, h.groupDistance),
210
+ groupPending: h.groupPending,
211
+ groupInView: a || h.groupInView,
212
+ groupScreenImportance: Math.min(l, h.groupScreenImportance),
213
+ groupFinest: e === "finest-target" || h.groupFinest,
214
+ groupId: h.groupId,
215
+ groupClass: h.groupClass
216
+ });
217
+ }
218
+ function X(o, s, e, t) {
219
+ const i = o.groupDistance ?? o.distance, n = s.groupDistance ?? s.distance, a = o.groupPending ?? 1, r = s.groupPending ?? 1, l = o.groupId ?? G(o, e), c = s.groupId ?? G(s, t), h = o.groupClass ?? (o.inView ? 0 : 2), f = s.groupClass ?? (s.inView ? 0 : 2), d = o.groupScreenImportance ?? o.screenImportance, g = s.groupScreenImportance ?? s.screenImportance, v = o.groupFinest ? 0 : 1, m = s.groupFinest ? 0 : 1, b = (y) => y === "coverage" ? 0 : y === "background" ? 2 : 1, T = o.phase === "environment" ? 0 : 1, I = s.phase === "environment" ? 0 : 1;
220
+ return T - I || h - f || v - m || d - g || i - n || r - a || (l < c ? -1 : l > c ? 1 : 0) || b(o.phase) - b(s.phase) || o.level - s.level || e - t;
221
+ }
222
+ function K(o, s, e) {
223
+ if (s < 0 || e < 0 || s + e > o.count)
224
+ throw new Error(
225
+ `Splat range [${s}, ${s + e}) exceeds its chunk's ${o.count} splats; the manifest and chunk data disagree.`
226
+ );
227
+ const t = o.shPacked;
228
+ return {
229
+ count: e,
230
+ positions: o.positions.subarray(s * 3, (s + e) * 3),
231
+ colors: o.colors.subarray(s * 4, (s + e) * 4),
232
+ covariances: o.covariances.subarray(s * 6, (s + e) * 6),
233
+ // Per-splat SH is splat-major, so it slices like everything else. Palette
234
+ // shN (`chunk.sh`) is deliberately not carried: its labels index a
235
+ // per-file codebook the shared pool has no way to hold.
236
+ ...t ? {
237
+ shPacked: {
238
+ ...t,
239
+ packed: t.packed.subarray(
240
+ s * le(t.bands),
241
+ (s + e) * le(t.bands)
242
+ )
243
+ }
244
+ } : {},
245
+ // Per-splat frontier `parent_size` (foveated `.rad`) slices splat-major like
246
+ // the rest; uploaded into `covarianceB.w` by the pool.
247
+ ...o.frontierParent ? { frontierParent: o.frontierParent.subarray(s, s + e) } : {}
248
+ };
249
+ }
250
+ function le(o) {
251
+ return o === 1 ? 3 : o === 2 ? 8 : 15;
252
+ }
253
+ function Xe(o) {
254
+ var s;
255
+ return o.positions.byteLength + o.colors.byteLength + o.covariances.byteLength + // SH is the largest part of an LCC Quality chunk (64 B/splat against the
256
+ // base 32); omitting it would let the CPU cache run ~3x over its cap.
257
+ (((s = o.shPacked) == null ? void 0 : s.packed.byteLength) ?? 0);
258
+ }
259
+ function Ke(o) {
260
+ const s = o.reason;
261
+ return s instanceof Error ? s : new DOMException("Aborted", "AbortError");
262
+ }
263
+ function Qe(o) {
264
+ const s = o ?? Ne;
265
+ if (!Number.isInteger(s) || s <= 0)
266
+ throw new RangeError("StreamedSplatMesh maxSplatsPerSwap must be a positive integer.");
267
+ return s;
268
+ }
269
+ function ce(o) {
270
+ const s = o ?? 1;
271
+ if (!Number.isFinite(s) || s <= 0)
272
+ throw new RangeError("StreamedSplatMesh lodScale must be a positive finite number.");
273
+ return s;
274
+ }
275
+ function Je() {
276
+ return Be();
277
+ }
278
+ function Ze(o, s) {
279
+ const e = o;
280
+ if (typeof e != "object" || e === null)
281
+ throw new Error("Streamed SOG manifest is not a JSON object.");
282
+ if (e.version !== 1)
283
+ throw new Error(`Unsupported Streamed SOG manifest version: ${e.version} (expected 1).`);
284
+ if (!Array.isArray(e.filenames) || e.filenames.some((l) => typeof l != "string"))
285
+ throw new Error('Streamed SOG manifest "filenames" must be an array of strings.');
286
+ if (!Array.isArray(e.counts) || e.counts.some((l) => !Number.isSafeInteger(l) || l < 0))
287
+ throw new Error('Streamed SOG manifest "counts" must be an array of non-negative integers.');
288
+ if (!Number.isSafeInteger(e.lodLevels) || e.lodLevels < 1 || e.lodLevels > 64)
289
+ throw new Error(`Streamed SOG manifest declares an invalid lodLevels: ${e.lodLevels}.`);
290
+ const t = e.filenames.map((l) => l.replace(/\/?meta\.json$/, "/")), i = t.map(
291
+ (l) => s.resolve(l) ?? `${s.manifestUrl}#${l}`
292
+ ), n = [], a = e.lodLevels, r = [e.tree];
293
+ for (; r.length > 0; ) {
294
+ const l = r.pop();
295
+ if (typeof l != "object" || l === null)
296
+ throw new Error("Streamed SOG manifest tree contains a malformed node.");
297
+ if (l.children) {
298
+ if (!Array.isArray(l.children) || l.children.length !== 2)
299
+ throw new Error("Streamed SOG manifest tree node must have exactly two children.");
300
+ r.push(l.children[1], l.children[0]);
301
+ continue;
302
+ }
303
+ const c = new Array(a).fill(
304
+ void 0
305
+ );
306
+ for (const [h, f] of Object.entries(l.lods ?? {})) {
307
+ const d = Number(h);
308
+ d >= 0 && d < a && (et(f, e.filenames.length), c[d] = f);
309
+ }
310
+ n.push({ bounds: he(l.bound), lods: c });
311
+ }
312
+ return {
313
+ leaves: n,
314
+ chunkUrls: i,
315
+ chunkDirectories: t,
316
+ counts: e.counts,
317
+ lodLevels: a,
318
+ bounds: he(e.tree.bound)
319
+ };
320
+ }
321
+ function et(o, s) {
322
+ if (typeof o != "object" || o === null || !Number.isSafeInteger(o.file) || o.file < 0 || o.file >= s || !Number.isSafeInteger(o.offset) || o.offset < 0 || !Number.isSafeInteger(o.count) || o.count < 0)
323
+ throw new Error(
324
+ `Streamed SOG manifest leaf declares an invalid LOD range (file ${o == null ? void 0 : o.file}, offset ${o == null ? void 0 : o.offset}, count ${o == null ? void 0 : o.count}).`
325
+ );
326
+ }
327
+ function he(o) {
328
+ return new w.Box3(
329
+ new w.Vector3(o.min[0], o.min[1], o.min[2]),
330
+ new w.Vector3(o.max[0], o.max[1], o.max[2])
331
+ );
332
+ }
333
+ function tt(o, s, e, t = 0) {
334
+ const i = Ze(o, s), n = t >= 1 ? t : void 0, a = i.chunkUrls.map((r, l) => {
335
+ var f;
336
+ const c = (f = i.chunkDirectories) == null ? void 0 : f[l], h = c ? s.directoryFiles(c) : null;
337
+ if (!(!h && !n))
338
+ return {
339
+ format: "sog",
340
+ ...h ? { files: h } : {},
341
+ ...n ? { sog: { packShBands: n } } : {}
342
+ };
343
+ });
344
+ return {
345
+ source: new We(i, e),
346
+ chunkUrls: i.chunkUrls,
347
+ ...a.some(Boolean) ? { chunkOptions: a } : {},
348
+ ...n ? { shBands: n } : {},
349
+ chunkKind: "directory",
350
+ // Streamed SOG chunks are unbundled directories
351
+ bounds: i.bounds,
352
+ pinnedFiles: it(i),
353
+ maxResidentSplats: i.counts[0] ?? e.budget,
354
+ minimumCoverageSplats: st(i)
355
+ };
356
+ }
357
+ function st(o) {
358
+ let s = 0;
359
+ for (const e of o.leaves)
360
+ for (let t = e.lods.length - 1; t >= 0; t--) {
361
+ const i = e.lods[t];
362
+ if (i) {
363
+ s += i.count;
364
+ break;
365
+ }
366
+ }
367
+ return Math.max(1, s);
368
+ }
369
+ function it(o) {
370
+ const s = /* @__PURE__ */ new Set();
371
+ for (const e of o.leaves)
372
+ for (let t = e.lods.length - 1; t >= 0; t--) {
373
+ const i = e.lods[t];
374
+ if (i) {
375
+ s.add(i.file);
376
+ break;
377
+ }
378
+ }
379
+ return s;
380
+ }
381
+ function nt(o, s) {
382
+ return {
383
+ manifestUrl: o,
384
+ resolve: (e) => new URL(e, o).href,
385
+ size: (e) => at(new URL(e, o).href, s),
386
+ directoryFiles: () => null,
387
+ dispose: () => {
388
+ }
389
+ };
390
+ }
391
+ async function at(o, s) {
392
+ var e;
393
+ try {
394
+ const t = await fetch(o, { ...ee(s), method: "HEAD" });
395
+ if (await de(t), t.ok) {
396
+ const i = t.headers.get("content-length");
397
+ if (i !== null) return ue(Number(i));
398
+ } else if (t.status === 404)
399
+ return null;
400
+ } catch {
401
+ }
402
+ try {
403
+ const t = await fetch(o, {
404
+ ...ee(s),
405
+ headers: { ...(s == null ? void 0 : s.headers) ?? {}, Range: "bytes=0-0" }
406
+ });
407
+ if (await de(t), t.status !== 206) return null;
408
+ const i = (e = t.headers.get("content-range")) == null ? void 0 : e.split("/")[1];
409
+ return i === void 0 || i === "*" ? null : ue(Number(i));
410
+ } catch {
411
+ return null;
412
+ }
413
+ }
414
+ async function de(o) {
415
+ var s;
416
+ try {
417
+ await ((s = o.body) == null ? void 0 : s.cancel());
418
+ } catch {
419
+ }
420
+ }
421
+ function ue(o) {
422
+ return Number.isSafeInteger(o) && o >= 0 ? o : null;
423
+ }
424
+ const rt = [
425
+ { test: (o) => o.endsWith(".lcc2"), format: "lcc2" },
426
+ { test: (o) => o.endsWith(".lcc"), format: "lcc" },
427
+ // The `.rad` header of a `--rad-chunked` set; `.radc` chunk files are not
428
+ // manifests (they end in `.radc`, so `endsWith('.rad')` excludes them).
429
+ { test: (o) => o.endsWith(".rad"), format: "rad" },
430
+ { test: (o) => o === "lod-meta.json", format: "streamed-sog" }
431
+ ];
432
+ function ot(o) {
433
+ const s = [];
434
+ for (const r of o.keys()) {
435
+ const l = $(r).toLowerCase(), c = rt.find((h) => h.test(l));
436
+ c && Re(r) === lt(o, c) && s.push({ path: r, format: c.format });
437
+ }
438
+ if (s.length === 0)
439
+ throw new q(
440
+ "That folder has no streamed-scene manifest in it (expected a .lcc, .lcc2 or lod-meta.json file).",
441
+ { phase: "manifest", url: "local-folder", retryable: !1 }
442
+ );
443
+ if (s.length > 1) {
444
+ const r = s.map((l) => $(l.path)).join(", ");
445
+ throw new q(
446
+ `That folder holds more than one scene manifest (${r}) - drop one scene at a time.`,
447
+ { phase: "manifest", url: "local-folder", retryable: !1 }
448
+ );
449
+ }
450
+ const e = s[0], t = e.path.includes("/") ? e.path.slice(0, e.path.lastIndexOf("/") + 1) : "", i = /* @__PURE__ */ new Map(), n = (r) => {
451
+ const l = i.get(r);
452
+ if (l !== void 0) return l;
453
+ const c = o.get(r);
454
+ if (!c) return null;
455
+ const h = URL.createObjectURL(c);
456
+ return i.set(r, h), h;
457
+ };
458
+ return { source: {
459
+ manifestUrl: n(e.path),
460
+ resolve: (r) => n(t + Q(r)),
461
+ size: (r) => {
462
+ var l;
463
+ return Promise.resolve(((l = o.get(t + Q(r))) == null ? void 0 : l.size) ?? null);
464
+ },
465
+ directoryFiles: (r) => {
466
+ const l = t + Q(r).replace(/\/?$/, "/"), c = {};
467
+ for (const h of o.keys()) {
468
+ if (!h.startsWith(l)) continue;
469
+ const f = n(h);
470
+ f && (c[h.slice(l.length)] = f);
471
+ }
472
+ return Object.keys(c).length > 0 ? c : null;
473
+ },
474
+ dispose: () => {
475
+ for (const r of i.values()) URL.revokeObjectURL(r);
476
+ i.clear();
477
+ }
478
+ }, format: e.format, name: $(e.path) };
479
+ }
480
+ function Q(o) {
481
+ return o.replace(/^\.?\//, "");
482
+ }
483
+ function $(o) {
484
+ return o.slice(o.lastIndexOf("/") + 1);
485
+ }
486
+ function Re(o) {
487
+ return o.split("/").length - 1;
488
+ }
489
+ function lt(o, s) {
490
+ let e = 1 / 0;
491
+ for (const t of o.keys())
492
+ s.test($(t).toLowerCase()) && (e = Math.min(e, Re(t)));
493
+ return e;
494
+ }
495
+ const fe = {
496
+ coneFov0: 90,
497
+ coneFov: 120,
498
+ coneFoveate: 0.4,
499
+ behindFoveate: 0.2
500
+ }, F = 2048, ct = 6e4, ht = 0.25, dt = 8, ut = 600, ft = 250, gt = 4, pt = 500, mt = 3, vt = 4e6, ge = 65536, St = 2 * 1024 * 1024 * 1024;
501
+ function bt(o) {
502
+ const s = 52 + (o.shBands ? 16 * Math.ceil(Ie(o.shBands) / 4) : 0), t = (o.chunkSize === void 0 ? void 0 : o.chunkSize * o.chunkUrls.length) ?? o.contentSplatCount ?? o.maxResidentSplats;
503
+ return Math.max(1, t) * s;
504
+ }
505
+ class Y extends Ee {
506
+ constructor(e, t, i, n, a, r = !1) {
507
+ var d, g, v;
508
+ super({ capacity: i }, n);
509
+ u(this, "scene");
510
+ u(this, "loader", new Ve());
511
+ /**
512
+ * Cap the *classic* (non-page-table) chunk cache evicts against.
513
+ *
514
+ * Mutable because a shared {@link ChunkCacheBudget} re-splits it as the camera
515
+ * moves; without a budget it stays at the value `options.cpuCacheBytes` or the
516
+ * device default set at construction.
517
+ */
518
+ u(this, "cpuCacheBytes");
519
+ u(this, "budgetValue");
520
+ u(this, "maximumBudget");
521
+ /** Spark's per-mesh `lodScale`; divides the page-table cut limit. */
522
+ u(this, "lodScaleValue");
523
+ /** Set once the governed budget has been reported as exceeding an explicit
524
+ * `foveationDrawBudget`, so the warning is issued at most once. */
525
+ u(this, "warnedDrawTargetCap", !1);
526
+ u(this, "stagedSwapsEnabled");
527
+ /** Classic LCC must keep old cell coverage while a replacement is pending. */
528
+ u(this, "neverRetireCoverageEarly");
529
+ u(this, "appendCap");
530
+ u(this, "onPerformanceEvent");
531
+ u(this, "compactionCount", 0);
532
+ u(this, "cache", /* @__PURE__ */ new Map());
533
+ /** Running byte total of {@link cache}; maintained by {@link cacheChunk}, eviction and dispose. */
534
+ u(this, "cacheBytesTotal", 0);
535
+ /** In-flight chunk fetches. The kind is kept so a weight change can shed the
536
+ * speculative ones without touching the detail that is actually on screen. */
537
+ u(this, "fetching", /* @__PURE__ */ new Map());
538
+ /** This mesh's camera-projected share of the scene's fetch bandwidth. */
539
+ u(this, "fetchWeight");
540
+ /** Scene-wide fetch arbitration, when the host shares one; see `requestChunk`. */
541
+ u(this, "fetchScheduler");
542
+ u(this, "fetchHandle");
543
+ /**
544
+ * Blob-URL dataset from {@link loadLocal}, owned by this mesh so its object
545
+ * URLs are revoked on {@link dispose} rather than leaking for the document's
546
+ * lifetime. Undefined for every network-loaded mesh.
547
+ */
548
+ u(this, "localSource");
549
+ /** Scene-wide chunk-cache ceiling, when the host shares one. */
550
+ u(this, "cacheBudget");
551
+ u(this, "cacheBudgetHandle");
552
+ /**
553
+ * The cap this mesh's frontier worker is currently evicting against.
554
+ *
555
+ * Mirrored on the main thread so `applyCacheAllowance` can skip no-op posts
556
+ * and so `fetchCounts.cacheLimitBytes` stays truthful between plans.
557
+ */
558
+ u(this, "cacheLimitBytes", 0);
559
+ u(this, "resident", /* @__PURE__ */ new Map());
560
+ /** Replacement runs hidden while their pool data is uploaded in bounded segments. */
561
+ u(this, "staged", /* @__PURE__ */ new Map());
562
+ /** Files awaiting a backoff retry after a transient fetch/decode error. */
563
+ u(this, "retrying", /* @__PURE__ */ new Map());
564
+ /** Files given up on after {@link MAX_CHUNK_ATTEMPTS} failures. */
565
+ u(this, "failedFiles", /* @__PURE__ */ new Set());
566
+ /**
567
+ * When true, each resident run writes its LOD `level` into the `lodLevel`
568
+ * float channel for false-color debug modifiers.
569
+ */
570
+ u(this, "lodLevelDebug", !1);
571
+ u(this, "lodLevelChannelReady", !1);
572
+ u(this, "lodLevelScratch");
573
+ /** Desired-but-not-resident files this tick; protected from cache eviction. */
574
+ u(this, "neededFiles", /* @__PURE__ */ new Set());
575
+ /** Non-null in `foveationMode: 'page-table'`: the worker that owns the chunk
576
+ * cache + traversal + pager off the main thread, and the always-active slab it
577
+ * pages the returned frontier into. */
578
+ u(this, "frontierWorker");
579
+ /**
580
+ * The frontier's slots, as a list of equally sized pages rather than one
581
+ * contiguous run.
582
+ *
583
+ * Slot `i` lives in page `i / slabPageSplats` at offset `i %
584
+ * slabPageSplats`. The pager only ever addresses slots, so where those
585
+ * pages sit in the pool is the mesh's business - which is what lets a mesh
586
+ * hold non-contiguous storage, and ultimately lets several meshes interleave
587
+ * in one pool instead of each reserving its whole ceiling as one block.
588
+ * (Spark's pager does the same thing one level down, binding fixed pages to
589
+ * `(source, chunk)` pairs.)
590
+ */
591
+ u(this, "slabPages", []);
592
+ /** Most slots the slab may ever hold - the construction capacity. */
593
+ u(this, "slabCeiling", 0);
594
+ /** Slot count the worker's pager was last told about. */
595
+ u(this, "pagerSlots", 0);
596
+ /** Consecutive ticks the wave gate has held retirements back. */
597
+ u(this, "retireHeldTicks", 0);
598
+ /** Backing store for {@link planTimings}. */
599
+ u(this, "planTimingsValue", {
600
+ applyMs: 0,
601
+ worstApplyMs: 0,
602
+ writeMs: 0,
603
+ residentMs: 0,
604
+ moves: 0,
605
+ appends: 0,
606
+ worstSplats: 0
607
+ });
608
+ /**
609
+ * Backing store for {@link fetchCounts}. Lifetime totals, because the question
610
+ * they answer is about a *steady state* - "this keeps streaming after the view
611
+ * settled" - which a per-frame or windowed number cannot express.
612
+ */
613
+ u(this, "fetchCountsValue", {
614
+ priority: 0,
615
+ base: 0,
616
+ sweep: 0,
617
+ evicted: 0,
618
+ uncovered: 0,
619
+ retiredEarly: 0,
620
+ cacheFull: !1,
621
+ cacheBytes: 0,
622
+ cacheLimitBytes: 0
623
+ });
624
+ /**
625
+ * The screen-radius band the scene asked for, kept so the band can be scaled
626
+ * with the solved frontier cut and always relative to the original - scaling
627
+ * the live values repeatedly would drift. Null when the scene has no band.
628
+ */
629
+ u(this, "frontierBandBase", null);
630
+ /**
631
+ * Splats per slab page for this mesh: {@link SLAB_PAGE_SPLATS}, or the whole
632
+ * capacity when that is smaller. Spark can use one fixed page size because
633
+ * its pool is a single large arena; here a mesh may be smaller than a page,
634
+ * and rounding it up to one would waste most of the reservation.
635
+ */
636
+ u(this, "slabPageSplats", ge);
637
+ /** Target drawn-splat count for the page-table frontier; see the constructor. */
638
+ u(this, "pageTableDrawBudget", 0);
639
+ /** The unclamped draw target (`foveationDrawBudget` or the default), kept so
640
+ * `setBudget` can re-derive the effective draw budget when the pool budget
641
+ * moves (e.g. under a `BudgetGovernor`). */
642
+ u(this, "pageTableDrawTarget", 0);
643
+ /** Whether {@link pageTableDrawTarget} came from an explicit
644
+ * `foveationDrawBudget` - a caller-chosen hard cap worth warning about when a
645
+ * governed budget outgrows it, rather than the library's own default. */
646
+ u(this, "pageTableDrawTargetExplicit", !1);
647
+ /** Last frontier's drawn (non-degenerate) splat count - the true on-screen size
648
+ * in `page-table` mode, where the slab is fully "active" but mostly degenerate. */
649
+ u(this, "pageTableDrawn", 0);
650
+ u(this, "frontierConverged", !0);
651
+ u(this, "pendingFrontierSplats", 0);
652
+ u(this, "staleResidentSplats", 0);
653
+ u(this, "lastPlanAppends", 0);
654
+ u(this, "lastPlanMoves", 0);
655
+ u(this, "lastPlanGeneration", 0);
656
+ u(this, "lastPlanBudget", 0);
657
+ u(this, "lastPlanCamera", null);
658
+ u(this, "firstFrontierCamera", null);
659
+ /** Monotonic reschedule id; a stale plan (superseded by a newer request) is
660
+ * dropped. `pageTableInFlight` coalesces to one outstanding traversal. */
661
+ u(this, "pageTableSeq", 0);
662
+ u(this, "pageTableInFlight", !1);
663
+ u(this, "pageTableDisposed", !1);
664
+ /** Files whose data has been forwarded to the worker (so we don't refetch). */
665
+ u(this, "pageTableCachedFiles", /* @__PURE__ */ new Set());
666
+ /** Chunks the last frontier wanted but did not have, biggest-on-screen first.
667
+ * These outrank the background sweep - they are the detail actually on screen. */
668
+ u(this, "pageTableFetchPriority", []);
669
+ /** Frontier-cut target node size (px) and foveation ramp; see `frontierView`. */
670
+ u(this, "pageTableTargetPx", ie);
671
+ u(this, "pageTableFoveation", fe);
672
+ /** Drawing-buffer height, sampled in `update` so `reschedule` can derive the
673
+ * cut limit the same way the material does (`targetPx / focalY`). */
674
+ u(this, "pageTableViewportY", 0);
675
+ /** Frontier cut on foveated `size / distance`. Re-derived each reschedule once
676
+ * the drawing buffer is known; the initial value only covers the first frame. */
677
+ u(this, "pageTableLimit", 0.02);
678
+ /**
679
+ * Whether the worker's cache is sitting at its cap: sweeping past that point
680
+ * only evicts what the frontier is using.
681
+ *
682
+ * Re-derived from every plan rather than latched. It used to latch on the
683
+ * first eviction, which was safe only while the cap was sized to the capture
684
+ * and evictions therefore meant "this will never fit". Under a scene-wide
685
+ * {@link ChunkCacheBudget} evictions are routine - a far mesh gives bytes back
686
+ * and is trimmed - and latching would kill its sweep for the session, so a
687
+ * mesh that went cold could never re-warm when the camera returned.
688
+ */
689
+ u(this, "pageTableCacheAtLimit", !1);
690
+ /** Per-splat channels whose edits survive chunk eviction/reload (M7.6). */
691
+ u(this, "persistentChannels", /* @__PURE__ */ new Map());
692
+ /** Chunk-file index of the always-resident environment tile, if the scene ships one. */
693
+ u(this, "envFile");
694
+ /** Whether the environment tile should be visible; toggled live. */
695
+ u(this, "envEnabled");
696
+ /** Pool handle of the environment tile once it has loaded (kept for toggling). */
697
+ u(this, "envHandle");
698
+ /** Env splat count, measured when the tile decodes; 0 until then. */
699
+ u(this, "envSplatCount", 0);
700
+ /** Set when the env tile is larger than the whole pool - terminal, warned once. */
701
+ u(this, "envUnfit", !1);
702
+ /**
703
+ * Startup hold. `'capture'` waits for the first schedule after the host
704
+ * applies the final camera; `'holding'` freezes that coverage set.
705
+ */
706
+ u(this, "initialRevealPhase", "off");
707
+ /** Which hold, if any, was armed at construction. Survives release for recapture. */
708
+ u(this, "initialRevealHold", "off");
709
+ /** Frozen nearby-detail / in-view coverage runs for {@link initialRevealPhase} `'holding'`. */
710
+ u(this, "frozenCriticalRuns", null);
711
+ /** Timestamp of the final-camera capture that began the current hold. */
712
+ u(this, "initialRevealStartedAt");
713
+ u(this, "initialRevealStateValue", { status: "disabled" });
714
+ /** Fetch settings this mesh was loaded with, reused for collision meshes. */
715
+ u(this, "requestOptions");
716
+ /** In-flight or settled collision load; see {@link loadCollisionMeshes}. */
717
+ u(this, "collisionTiles");
718
+ u(this, "collisionAbort");
719
+ u(this, "pendingWork", !0);
720
+ u(this, "lastScheduleTime", -1 / 0);
721
+ /** Reused leaf-coverage bitmap for {@link substituteCoverage}; grows only. */
722
+ u(this, "coverageScratch");
723
+ u(this, "lastCameraPos", new w.Vector3(1 / 0, 1 / 0, 1 / 0));
724
+ u(this, "lastCameraQuat", new w.Quaternion());
725
+ this.scene = e, this.budgetValue = t, this.maximumBudget = n.maxBudget === void 0 ? t : Math.max(t, W(n.maxBudget)), this.lodScaleValue = ce(n.lodScale), this.stagedSwapsEnabled = n.experimentalStagedSwaps !== !1, this.neverRetireCoverageEarly = r, this.appendCap = Qe(n.maxSplatsPerSwap);
726
+ const l = n.initialReveal === "hold-coverage" && this.scene.source.coverageRunsFor !== void 0, c = n.initialReveal === "hold-near-l0" && r;
727
+ l || c ? (this.initialRevealHold = l ? "hold-coverage" : "hold-near-l0", this.initialRevealPhase = "capture", this.initialRevealStateValue = {
728
+ status: "pending",
729
+ stagedSplats: 0,
730
+ totalSplats: 0,
731
+ readyGroups: 0,
732
+ totalGroups: 0
733
+ }) : (this.initialRevealHold = "off", this.initialRevealPhase = "off", this.initialRevealStateValue = { status: "disabled" }), this.onPerformanceEvent = n.onPerformanceEvent, this.cpuCacheBytes = n.cpuCacheBytes ?? Je(), this.requestOptions = n.request, this.envFile = (d = e.environment) == null ? void 0 : d.file, this.envEnabled = n.environmentEnabled !== !1, this.fetchWeight = n.fetchWeight, this.fetchScheduler = n.fetchScheduler, this.cacheBudget = n.cacheBudget, this.fetchHandle = (g = this.fetchScheduler) == null ? void 0 : g.register({
734
+ // No weight supplied: claim an equal share rather than none, so a partly
735
+ // wired host degrades to round-robin instead of silently starving.
736
+ weight: () => {
737
+ var m;
738
+ return ((m = this.fetchWeight) == null ? void 0 : m.call(this)) ?? 1;
739
+ },
740
+ onSlotAvailable: () => {
741
+ this.pendingWork = !0;
742
+ },
743
+ shedFetches: (m) => this.abortFetches(m)
744
+ });
745
+ const h = j(n.foveationMode), f = h ? Math.max(
746
+ this.cpuCacheBytes,
747
+ Math.min(St, bt(e))
748
+ ) : this.cpuCacheBytes;
749
+ if (this.cacheBudgetHandle = (v = this.cacheBudget) == null ? void 0 : v.register({
750
+ // The governor weight `fetchWeight` already carries, so cache and network
751
+ // follow the same camera-projected measure. The `1` fallback matches
752
+ // `requestChunk`: a host that never wired weights gives every mesh an
753
+ // equal claim rather than none.
754
+ weight: () => {
755
+ var m;
756
+ return ((m = this.fetchWeight) == null ? void 0 : m.call(this)) ?? 1;
757
+ },
758
+ ceilingBytes: f,
759
+ onAllowanceChanged: (m) => this.applyCacheAllowance(m)
760
+ }), this.cacheLimitBytes = this.cacheBudget && this.cacheBudgetHandle ? this.cacheBudget.allowanceFor(this.cacheBudgetHandle) : f, h || (this.cpuCacheBytes = this.cacheLimitBytes), this.fetchCountsValue.cacheLimitBytes = this.cacheLimitBytes, j(n.foveationMode)) {
761
+ if (!a)
762
+ throw new Error("StreamedSplatMesh: page-table foveation requires the frontier worker.");
763
+ this.pageTableDrawTarget = n.foveationDrawBudget ?? vt, this.pageTableDrawTargetExplicit = n.foveationDrawBudget !== void 0, this.pageTableDrawBudget = Math.min(t, this.pageTableDrawTarget), this.pageTableTargetPx = n.foveationTargetPx ?? ie, this.pageTableFoveation = { ...fe, ...n.frontierFoveation }, (n.minSplatScreenRadius !== void 0 || n.maxSplatScreenRadius !== void 0) && (this.frontierBandBase = {
764
+ min: n.minSplatScreenRadius ?? 0,
765
+ max: n.maxSplatScreenRadius ?? 0
766
+ }), this.slabPageSplats = Math.min(ge, i), this.slabCeiling = i, this.syncSlabPages(this.pageTableDrawBudget), this.frontierWorker = new a(), this.frontierWorker.onmessage = (b) => this.applyFrontierPlan(b.data), this.pagerSlots = this.slabSlots, this.postToWorker({
767
+ type: "init",
768
+ capacity: this.pagerSlots,
769
+ chunkSize: e.chunkSize ?? 65536,
770
+ cpuCacheBytes: this.cacheLimitBytes
771
+ });
772
+ const m = e.bootstrapChunk;
773
+ m && this.forwardChunkToWorker(m.file, m.data);
774
+ } else
775
+ this.frontierWorker = null;
776
+ }
777
+ /**
778
+ * Fetches a scene manifest and prepares a mesh sized to the budget.
779
+ * Accepts a Streamed SOG manifest (`lod-meta.json`) or an XGRIDS `.lcc2` or
780
+ * `.lcc` (manifest v3–v5) dataset - all stream through the same machinery. Both LCC
781
+ * generations are normalized to the established XGRIDS/Spark Three.js
782
+ * coordinate frame; streamed SOG orientation is unchanged.
783
+ *
784
+ * A `.lcc` dataset needs a server that answers HTTP range requests: its
785
+ * splats live in one large `data.bin` that is never fetched whole.
786
+ *
787
+ * @param manifestUrl - URL of the scene's `lod-meta.json`, `.lcc2` or `.lcc`
788
+ * file; relative URLs resolve against `options.baseUrl` (or the page).
789
+ * @throws Rejects with {@link SplatLoadError} on any resolve/fetch/parse
790
+ * failure, or a `DOMException` named `AbortError` when `options.signal` fires.
791
+ */
792
+ static async load(e, t = {}) {
793
+ const i = Ae(e, t.baseUrl).href, n = i.toLowerCase(), a = t.format !== void 0 && t.format !== "auto" ? t.format : n.endsWith(".lcc2") ? "lcc2" : n.endsWith(".lcc") ? "lcc" : n.endsWith(".rad") ? "rad" : "streamed-sog";
794
+ return Y.fromSource(
795
+ nt(i, t.request),
796
+ a,
797
+ t
798
+ );
799
+ }
800
+ /**
801
+ * Prepares a mesh from a folder dropped into the page - the same streamed
802
+ * formats, read straight off the user's disk with no server and no upload.
803
+ *
804
+ * Every file becomes a `blob:` URL, which answers range requests exactly as
805
+ * an HTTP origin does, so a multi-hundred-megabyte `.lcc` `data.bin` streams
806
+ * chunk-by-chunk rather than being read whole.
807
+ *
808
+ * @param files - The folder's files, keyed by path relative to its root
809
+ * (as the demo drop-zone `readDirectory` walk produces).
810
+ * @throws Rejects with {@link SplatLoadError} - phase `'manifest'` when the
811
+ * folder holds no (or more than one) recognizable scene manifest - or a
812
+ * `DOMException` named `AbortError` when `options.signal` fires.
813
+ */
814
+ static async loadLocal(e, t = {}) {
815
+ let i;
816
+ try {
817
+ i = ot(e);
818
+ } catch (n) {
819
+ throw n instanceof q ? n : new q(n instanceof Error ? n.message : String(n), {
820
+ phase: "manifest",
821
+ url: "local-folder",
822
+ retryable: !1,
823
+ cause: n
824
+ });
825
+ }
826
+ try {
827
+ const n = await Y.fromSource(i.source, i.format, t);
828
+ return n.localSource = i.source, n;
829
+ } catch (n) {
830
+ throw i.source.dispose(), n;
831
+ }
832
+ }
833
+ /** Shared load path: fetch the manifest from a source, then build the scene. */
834
+ static async fromSource(e, t, i) {
835
+ const n = i.deviceProfile, a = W(i.budget, n, {
836
+ format: t,
837
+ ...i.budgetCap === void 0 ? {} : { cap: i.budgetCap }
838
+ }), r = i.maxBudget === void 0 ? a : W(i.maxBudget, n);
839
+ if (r < a)
840
+ throw new RangeError(
841
+ `StreamedSplatMesh: maxBudget (${r}) must be >= budget (${a}).`
842
+ );
843
+ const l = {
844
+ budget: r,
845
+ lodBaseDistance: i.lodBaseDistance ?? 10,
846
+ lodMultiplier: i.lodMultiplier ?? 2
847
+ }, c = i.shBands ?? (De(i.performanceProfile, n) === "smooth" ? 0 : Ge), h = Pe(n), f = (i.allowFinestLevelLift === !0 || i.budget === void 0 && i.maxBudget === void 0) && i.budgetCap === void 0, d = i.signal;
848
+ d == null || d.throwIfAborted();
849
+ let g;
850
+ if (t === "rad")
851
+ try {
852
+ const { buildRadScene: p } = await import("./formats/rad.js");
853
+ g = await p(e, l, i.request, c, f);
854
+ } catch (p) {
855
+ throw N(p) ? p : _(p, { phase: "manifest", url: e.manifestUrl });
856
+ }
857
+ else {
858
+ let p;
859
+ try {
860
+ p = await fetch(e.manifestUrl, ee(i.request, d));
861
+ } catch (S) {
862
+ throw N(S) ? S : _(S, { phase: "fetch", url: e.manifestUrl });
863
+ }
864
+ if (!p.ok)
865
+ throw _(
866
+ new Error(`Failed to load manifest ${e.manifestUrl}: HTTP ${p.status}`),
867
+ { phase: "manifest", url: e.manifestUrl, status: p.status }
868
+ );
869
+ try {
870
+ const S = await p.json();
871
+ if (t === "lcc2") {
872
+ const { buildLcc2Scene: M } = await import("./formats/lcc.js");
873
+ g = M(S, e, l, i.shBands ?? 0);
874
+ } else if (t === "lcc") {
875
+ const { buildLccScene: M } = await import("./formats/lcc.js");
876
+ g = await M(S, e, { ...l, shBands: c });
877
+ } else
878
+ g = tt(S, e, l, i.shBands ?? 0);
879
+ } catch (S) {
880
+ throw N(S) ? S : _(S, { phase: "manifest", url: e.manifestUrl });
881
+ }
882
+ }
883
+ d == null || d.throwIfAborted();
884
+ const v = (t === "lcc" || t === "rad") && f ? ke(r, g.maxResidentSplats, n) : r, m = i.maxBudget === void 0 ? v : Math.min(a, v);
885
+ g.source.budget = m;
886
+ const b = Math.min(v, g.maxResidentSplats), T = i.experimentalStagedSwaps !== !1 ? 1.5 : 1.4, I = Math.max(
887
+ 1,
888
+ Math.ceil(b * T / F)
889
+ ), y = g.foveation ? ne(
890
+ i.foveationMode,
891
+ t === "rad" ? "page-table" : "frontier"
892
+ ) : i.foveationMode === void 0 ? void 0 : ne(i.foveationMode);
893
+ let A;
894
+ if (j(y)) {
895
+ A = (await import("./frontier-worker-CAnK_Cmu.js")).default, d == null || d.throwIfAborted();
896
+ const S = g.source;
897
+ S.needsParentSizes !== void 0 && (S.needsParentSizes = !1);
898
+ }
899
+ const R = new Y(
900
+ g,
901
+ m,
902
+ I * F,
903
+ {
904
+ ...i,
905
+ // Classic LCC's useful first frame is the bounded nearby-detail set,
906
+ // not its coarse shell. `.lcc2` waits for in-view coarsest coverage
907
+ // so empty octree squares do not flash. Keep every other format
908
+ // progressive, and let a caller explicitly request progressive for A/B.
909
+ ...t === "lcc" && i.initialReveal === void 0 ? { initialReveal: "hold-near-l0" } : {},
910
+ ...t === "lcc2" && i.initialReveal === void 0 ? { initialReveal: "hold-coverage" } : {},
911
+ // The resolved ceiling, not the caller's raw option: it may have been
912
+ // lifted to a moderate capture's leaf count above.
913
+ maxBudget: v,
914
+ // This line overrides `...options` above, so a declined request has to
915
+ // survive it - that is how `.rad` came to ignore both the `smooth`
916
+ // profile and an explicit `shBands: 0`. Only *zero* is re-applied here,
917
+ // never a partial reduction: the builders already honour partial
918
+ // requests by generating that many bands, whereas forcing a smaller
919
+ // count past one would mismatch the decoded chunk and degrade to
920
+ // neutral SH (see `SplatMesh.writePackedSh`).
921
+ shBands: c === 0 ? 0 : g.shBands ?? 0,
922
+ // Spark ships Mip-Splatting antialiasing ON (blurAmount 0.3 *with* opacity
923
+ // compensation `α·√(detRaw/detBlur)`). Match that default for `.rad`: the
924
+ // 0.3 low-pass without the compensation makes splats too opaque (uniform
925
+ // blur) and leaves anisotropic splats bright (needle spikes).
926
+ antialias: i.antialias ?? (t === "rad" ? !0 : void 0),
927
+ // Older XGRIDS LCC uses a smaller, compensated projected low-pass.
928
+ ...t === "lcc" ? { projectedFilterProfile: "lcc" } : {},
929
+ // Match Spark's `.rad` render exactly: the LOD alpha encoding + merged-node
930
+ // σ-cutoff/super-Gaussian, and the √8 (≈2.83σ) base cutoff Spark defaults to.
931
+ // An explicit `lodAlpha` (e.g. `?lodAlpha=0`) wins for A/B.
932
+ //
933
+ // The √8 cutoff is *desktop only* - see `recommendedRadMaxStdDev`, which
934
+ // returns undefined on mobile so the `SplatMesh` constructor applies the
935
+ // same 4 ceiling `.rad` was the only format escaping. An explicit
936
+ // `maxStdDev` still wins, through `...options` above.
937
+ ...t === "rad" ? {
938
+ lodAlpha: i.lodAlpha ?? !0,
939
+ ...i.maxStdDev === void 0 && h !== void 0 ? { maxStdDev: h } : {}
940
+ } : {},
941
+ // A foveated scene renders whole chunks and picks the LOD cut per splat.
942
+ // `.rad` defaults to Spark's selected-index page table (only the frontier is
943
+ // paged to the GPU, so the whole device budget buys on-screen detail); other
944
+ // foveated formats keep the GPU `frontier` cut. `foveationMode: 'band'` (or
945
+ // `'frontier'`) forces the legacy paths for A/B. Overrides any caller blob cull.
946
+ ...g.foveation ? {
947
+ foveationMode: y,
948
+ minSplatScreenRadius: g.foveation.minScreenRadiusPx,
949
+ maxSplatScreenRadius: g.foveation.maxScreenRadiusPx
950
+ } : {}
951
+ },
952
+ A,
953
+ t === "lcc"
954
+ ), L = g.formatTransform ?? (R.orientation === "y-up" ? xe(t) : null);
955
+ return L && (R.matrix.copy(L), R.matrix.decompose(R.position, R.quaternion, R.scale), R.matrixWorldNeedsUpdate = !0), d != null && d.aborted && (R.dispose(), d.throwIfAborted()), R;
956
+ }
957
+ /** Slots the currently reserved pages can hold. */
958
+ get slabSlots() {
959
+ let e = 0;
960
+ for (const t of this.slabPages) e += t.count;
961
+ return e;
962
+ }
963
+ /**
964
+ * Reserves or releases slab pages so the slab can hold `wanted` slots, and
965
+ * tells the worker's pager the new slot count.
966
+ *
967
+ * This is the mechanism that makes a shared pool worth having: storage follows
968
+ * the governed budget, so approaching a mesh grows its pages while the ones
969
+ * behind you hand theirs back, instead of every mesh holding its ceiling for
970
+ * the whole session. Growth stops at the construction ceiling and at whatever
971
+ * the pool can actually spare - a mesh that cannot grow simply stays coarse
972
+ * rather than throwing.
973
+ */
974
+ syncSlabPages(e) {
975
+ if (this.slabCeiling === 0) return;
976
+ const t = Math.max(this.slabPageSplats, Math.min(this.slabCeiling, e));
977
+ let i = this.slabSlots;
978
+ for (; i < t; ) {
979
+ const n = Math.min(this.slabPageSplats, this.slabCeiling - i);
980
+ if (n <= 0) break;
981
+ try {
982
+ this.slabPages.push(this.reserveInactiveRange(n));
983
+ } catch {
984
+ break;
985
+ }
986
+ i += n;
987
+ }
988
+ for (; this.slabPages.length > 1; ) {
989
+ const n = this.slabPages[this.slabPages.length - 1];
990
+ if (i - n.count < t) break;
991
+ this.slabPages.pop(), i -= n.count, this.removeRange(n);
992
+ }
993
+ i !== this.pagerSlots && (this.pagerSlots = i, this.postToWorker({ type: "resize", capacity: i }), this.pageTableDrawn > i && this.setSlabResident(i), this.pendingWork = !0, this.lastScheduleTime = -1 / 0);
994
+ }
995
+ /**
996
+ * Writes `data` at slot `slot`, splitting the write where it crosses a page
997
+ * boundary. The pager's runs are contiguous in *slot* space, which page
998
+ * storage no longer guarantees is contiguous in the pool.
999
+ */
1000
+ writeSlabSlots(e, t, i) {
1001
+ let n = 0;
1002
+ for (; n < i; ) {
1003
+ const a = t + n, r = this.slabPages[Math.floor(a / this.slabPageSplats)];
1004
+ if (!r) return;
1005
+ const l = a % this.slabPageSplats, c = Math.min(i - n, this.slabPageSplats - l);
1006
+ this.overwriteRangeData(r, K(e, n, c), l), n += c;
1007
+ }
1008
+ }
1009
+ /** Zeros slots `[slot, slot + count)`, splitting at page boundaries like
1010
+ * {@link writeSlabSlots}, so freed slots hold nothing drawable. */
1011
+ degenerateSlabSlots(e, t) {
1012
+ let i = 0;
1013
+ for (; i < t; ) {
1014
+ const n = e + i, a = this.slabPages[Math.floor(n / this.slabPageSplats)];
1015
+ if (!a) return;
1016
+ const r = n % this.slabPageSplats, l = Math.min(t - i, this.slabPageSplats - r);
1017
+ this.degenerateRange(a, r, l), i += l;
1018
+ }
1019
+ }
1020
+ /**
1021
+ * Draws exactly the first `resident` slots: pages below the boundary are
1022
+ * fully active, the page containing it is partially active, the rest are
1023
+ * inactive. Freed tail slots simply leave the active list; they are also
1024
+ * degenerated (see {@link degenerateSlabSlots}) so that even a slot drawn by
1025
+ * mistake shows nothing.
1026
+ */
1027
+ setSlabResident(e) {
1028
+ for (let t = 0; t < this.slabPages.length; t++) {
1029
+ const i = Math.min(
1030
+ this.slabPageSplats,
1031
+ Math.max(0, e - t * this.slabPageSplats)
1032
+ );
1033
+ this.setRangeActivePrefix(this.slabPages[t], i);
1034
+ }
1035
+ }
1036
+ /** Typed post to the frontier worker. */
1037
+ postToWorker(e, t = []) {
1038
+ var i;
1039
+ (i = this.frontierWorker) == null || i.postMessage(e, t);
1040
+ }
1041
+ /**
1042
+ * Applies a new decoded-chunk allowance from the scene's shared
1043
+ * {@link ChunkCacheBudget}.
1044
+ *
1045
+ * Only the cap moves; nothing is dropped here. Both cache implementations
1046
+ * evict lazily against it - the page-table worker inside its next
1047
+ * `reschedule`, against a frontier that is still current, and the classic
1048
+ * path in `evictChunks` on the next tick. Dropping chunks synchronously would
1049
+ * pull them out from under resident splats.
1050
+ *
1051
+ * `pageTableCacheAtLimit` is recomputed here rather than waiting for the next
1052
+ * plan, so a *raised* allowance re-arms the background sweep on this tick
1053
+ * instead of one idle interval later.
1054
+ */
1055
+ applyCacheAllowance(e) {
1056
+ this.disposed || e !== this.cacheLimitBytes && (this.cacheLimitBytes = e, this.frontierWorker ? this.postToWorker({ type: "cacheBudget", cpuCacheBytes: e }) : this.cpuCacheBytes = e, this.fetchCountsValue.cacheLimitBytes = e, this.pageTableCacheAtLimit = this.fetchCountsValue.cacheBytes >= e, this.pendingWork = !0);
1057
+ }
1058
+ /**
1059
+ * Whether this scene ships collision meshes - true for an XGRIDS `.lcc` /
1060
+ * `.lcc2` dataset that carries them, false for a Streamed SOG scene, which
1061
+ * has none.
1062
+ */
1063
+ get hasCollisionMeshes() {
1064
+ var e;
1065
+ return (((e = this.scene.collision) == null ? void 0 : e.meshes.length) ?? 0) > 0;
1066
+ }
1067
+ /**
1068
+ * Fetches and parses this scene's collision geometry: the triangle meshes an
1069
+ * XGRIDS `.lcc` (`collision.lci`) or `.lcc2` (`data/mesh/*.ply`) capture
1070
+ * ships beside its splats, for hosts that want collision, ground probes or
1071
+ * other spatial queries.
1072
+ *
1073
+ * The geometry is source-local, like {@link StreamedScene.bounds} - apply
1074
+ * this mesh's `matrixWorld` to put it in the frame the splats render in.
1075
+ * VLAM! builds no acceleration structure over it and never consults it.
1076
+ *
1077
+ * Tiles are fetched once and cached; concurrent callers share one load, and
1078
+ * a failed load can be retried by calling again. Resolves `[]` for a scene
1079
+ * without collision.
1080
+ *
1081
+ * @throws a `DOMException` named `AbortError` if cancelled, or if
1082
+ * {@link dispose} is called while the load is in flight.
1083
+ */
1084
+ async loadCollisionMeshes(e = {}) {
1085
+ var a;
1086
+ (a = e.signal) == null || a.throwIfAborted();
1087
+ const t = this.scene.collision;
1088
+ if (!t || t.meshes.length === 0) return [];
1089
+ if (!this.collisionTiles) {
1090
+ const r = new AbortController();
1091
+ this.collisionAbort = r, this.collisionTiles = import("./formats/lcc.js").then(
1092
+ ({ loadCollisionMeshTiles: l }) => l(t, {
1093
+ ...this.requestOptions ? { request: this.requestOptions } : {},
1094
+ signal: r.signal
1095
+ })
1096
+ ).catch((l) => {
1097
+ throw this.collisionTiles = void 0, l;
1098
+ });
1099
+ }
1100
+ const { signal: i } = e;
1101
+ if (!i) return this.collisionTiles;
1102
+ let n;
1103
+ try {
1104
+ return await Promise.race([
1105
+ this.collisionTiles,
1106
+ new Promise((r, l) => {
1107
+ n = () => l(Ke(i)), i.addEventListener("abort", n, { once: !0 });
1108
+ })
1109
+ ]);
1110
+ } finally {
1111
+ n && i.removeEventListener("abort", n);
1112
+ }
1113
+ }
1114
+ /**
1115
+ * Whether this scene ships an always-resident environment/background tile -
1116
+ * true for an XGRIDS `.lcc2` capture that carries one (its `env.sog` sky),
1117
+ * false for Streamed SOG, `.lcc`, `.rad`, or an `.lcc2` without one.
1118
+ */
1119
+ get hasEnvironment() {
1120
+ return this.envFile !== void 0;
1121
+ }
1122
+ /** Whether the environment tile is currently set to render. */
1123
+ get environmentEnabled() {
1124
+ return this.envEnabled;
1125
+ }
1126
+ /**
1127
+ * Splats in the environment tile, measured when it decoded - the manifest
1128
+ * does not carry the count. 0 until the tile has loaded (or if the scene
1129
+ * ships none). These sit outside the LOD budget, drawing from the pool's
1130
+ * capacity headroom.
1131
+ */
1132
+ get environmentSplatCount() {
1133
+ return this.envSplatCount;
1134
+ }
1135
+ /**
1136
+ * Shows or hides the scene's environment/background tile. The switch is
1137
+ * instant and never refetches: once loaded, the tile stays in the pool and
1138
+ * only its active flag flips. Enabling before the tile has loaded triggers
1139
+ * its (one-time) load on the next update. No-op on a scene without one.
1140
+ */
1141
+ setEnvironmentEnabled(e) {
1142
+ this.envFile === void 0 || e === this.envEnabled || (this.envEnabled = e, this.envHandle !== void 0 ? this.setRangeActive(this.envHandle, e) : e && (this.pendingWork = !0, this.lastScheduleTime = -1 / 0));
1143
+ }
1144
+ /** The active-splat budget this mesh keeps within. */
1145
+ get budget() {
1146
+ return this.budgetValue;
1147
+ }
1148
+ /**
1149
+ * The ceiling {@link setBudget} clamps to - {@link StreamedSplatMeshOptions.maxBudget}
1150
+ * when one was given, otherwise the construction budget.
1151
+ *
1152
+ * The pool was allocated for this number and cannot grow, so it is a hard
1153
+ * limit on what any governor can hand this mesh. Read it to check that a
1154
+ * shared-budget setup can actually deliver the share it is computing.
1155
+ */
1156
+ get maxBudget() {
1157
+ return this.maximumBudget;
1158
+ }
1159
+ /** Alias used by hosts that manage static and streamed auto-LOD uniformly. */
1160
+ get budgetCeiling() {
1161
+ return this.maximumBudget;
1162
+ }
1163
+ /**
1164
+ * The capture's real content size, when the format declares it (`.rad` reports
1165
+ * its leaf count) - the splat count needed to hold this mesh at full
1166
+ * resolution, independent of the budget it was constructed with.
1167
+ *
1168
+ * A host splitting one budget across several streamed meshes should clamp each
1169
+ * share to this: a mesh cannot spend more than it contains, so budget handed
1170
+ * past it buys nothing and is better given to a mesh that can use it. Note it
1171
+ * is *not* `maxBudget`: a foveated `.rad` reports `maxResidentSplats` as the
1172
+ * requested budget, because its pool holds a camera-directed resident set
1173
+ * rather than the whole tree.
1174
+ *
1175
+ * `undefined` when the format does not declare a content size.
1176
+ */
1177
+ get contentSplatCount() {
1178
+ return this.scene.contentSplatCount;
1179
+ }
1180
+ /**
1181
+ * Which `.rad` streaming strategy this mesh selected at load, or `null` when
1182
+ * the scene is not a Spark `.rad` capture.
1183
+ */
1184
+ get radStrategy() {
1185
+ var e, t;
1186
+ return ((t = (e = this.scene.chunkOptions) == null ? void 0 : e[0]) == null ? void 0 : t.format) !== "rad-chunk" ? null : this.frontierWorker ? "page-table" : "prefix";
1187
+ }
1188
+ /**
1189
+ * The drawn-splat target currently driving the `.rad` page-table frontier -
1190
+ * the governed budget, capped by
1191
+ * {@link SplatMeshOptions.foveationDrawBudget}. `0` on a mesh that is not in
1192
+ * `foveationMode: 'page-table'`, which has no frontier to target.
1193
+ *
1194
+ * This is the number that decides how deep the traversal descends, so it is
1195
+ * what to watch when checking that a near mesh really did receive more
1196
+ * detail: {@link budget} is the pool's allowance, this is what is spent.
1197
+ */
1198
+ get drawBudget() {
1199
+ return this.frontierWorker ? this.pageTableDrawBudget : 0;
1200
+ }
1201
+ /**
1202
+ * Page-table frontier coherence for hosts that gate preload/transitions.
1203
+ * `undefined` fields stay 0 when this mesh is not in page-table mode.
1204
+ */
1205
+ get frontierState() {
1206
+ return {
1207
+ frontierConverged: this.frontierWorker ? this.frontierConverged : !0,
1208
+ pendingFrontierSplats: this.pendingFrontierSplats,
1209
+ staleResidentSplats: this.staleResidentSplats,
1210
+ lastPlanAppends: this.lastPlanAppends,
1211
+ lastPlanMoves: this.lastPlanMoves,
1212
+ planGeneration: this.lastPlanGeneration,
1213
+ planBudget: this.lastPlanBudget,
1214
+ lastPlanCamera: this.lastPlanCamera,
1215
+ firstFrontierCamera: this.firstFrontierCamera
1216
+ };
1217
+ }
1218
+ /**
1219
+ * Spark's per-mesh `lodScale` (see {@link StreamedSplatMeshOptions.lodScale}).
1220
+ * Mutable: raise it to sharpen a focused mesh, lower it to coarsen a
1221
+ * background one. Page-table `.rad` only.
1222
+ *
1223
+ * @throws {RangeError} if set to a value that is not positive and finite.
1224
+ */
1225
+ get lodScale() {
1226
+ return this.lodScaleValue;
1227
+ }
1228
+ set lodScale(e) {
1229
+ const t = ce(e);
1230
+ t !== this.lodScaleValue && (this.lodScaleValue = t, this.pendingWork = !0, this.lastScheduleTime = -1 / 0);
1231
+ }
1232
+ /** In `page-table` mode the slab is fully active but mostly degenerate, so the
1233
+ * base `activeSplatCount` (slab size) is not the on-screen count - report the
1234
+ * frontier's drawn size instead. */
1235
+ get activeSplatCount() {
1236
+ return this.frontierWorker ? this.pageTableDrawn : super.activeSplatCount;
1237
+ }
1238
+ /**
1239
+ * Updates the LOD budget used for future scheduling within allocated capacity.
1240
+ *
1241
+ * @returns the budget actually in effect, which is `budget` clamped to
1242
+ * {@link maxBudget}. A `BudgetGovernor` reads this return value to detect a
1243
+ * capped member and hand the remainder to the others, so the clamp is
1244
+ * reported rather than hidden.
1245
+ */
1246
+ setBudget(e) {
1247
+ const t = Math.min(W(e), this.maximumBudget);
1248
+ return t === this.budgetValue ? this.budgetValue : (this.budgetValue = t, this.scene.source.budget = t, this.frontierWorker && (this.pageTableDrawBudget = Math.min(t, this.pageTableDrawTarget), this.syncSlabPages(this.pageTableDrawBudget), this.pageTableDrawTargetExplicit && this.pageTableDrawTarget < t && !this.warnedDrawTargetCap && (this.warnedDrawTargetCap = !0, E(
1249
+ `StreamedSplatMesh: budget raised to ${t} but foveationDrawBudget caps the drawn frontier at ${this.pageTableDrawTarget}; the extra budget cannot buy detail. Raise or drop foveationDrawBudget to let the shared budget through.`
1250
+ ))), this.pendingWork = !0, this.lastScheduleTime = -1 / 0, t);
1251
+ }
1252
+ /**
1253
+ * What the LOD scheduler last decided, or `undefined` on sources that do not
1254
+ * schedule by leaf (the `.rad` page table, prefix readers).
1255
+ *
1256
+ * Distinct from {@link activeSplatCount}, and the distinction is the whole
1257
+ * point: `desired` is what the scheduler asked for, `activeSplatCount` is
1258
+ * what the pool ended up drawing. Equal means the cut is applied; `desired`
1259
+ * far below the budget means the *scheduler* declined to spend it, which is a
1260
+ * different bug from the mesh failing to apply what it was given.
1261
+ */
1262
+ get lodStats() {
1263
+ return this.scene.source.stats;
1264
+ }
1265
+ /** Number of chunk files currently decoded and held. In page-table mode the
1266
+ * worker owns the cache - the main-thread map is always empty there, so report
1267
+ * what has been forwarded to it instead of a permanent zero. */
1268
+ get residentChunkCount() {
1269
+ return this.frontierWorker ? this.pageTableCachedFiles.size : this.cache.size;
1270
+ }
1271
+ /** Chunk fetches currently in flight. */
1272
+ get pendingChunkCount() {
1273
+ return this.fetching.size;
1274
+ }
1275
+ /**
1276
+ * Main-thread cost of applying paging plans in `foveationMode: 'page-table'`.
1277
+ *
1278
+ * A plan is applied whole, off the render loop's own timing, so its cost does
1279
+ * not appear in {@link getUpdateTimings} - but it lands on the same thread and
1280
+ * a churning frontier can make it the largest stall in a frame. `worst*`
1281
+ * accumulate over the mesh's lifetime; the rest describe the most recent plan.
1282
+ */
1283
+ get planTimings() {
1284
+ return this.planTimingsValue;
1285
+ }
1286
+ /**
1287
+ * Lifetime chunk-fetch totals by kind, plus page-table cache state.
1288
+ *
1289
+ * Diagnostic for the question "why is this still streaming after the view
1290
+ * settled?", which the three fetch sources answer differently and which no
1291
+ * other reading distinguishes:
1292
+ *
1293
+ * - **`sweep` climbing** - speculative file-order pre-warming of the whole
1294
+ * capture. Declined by the `smooth` profile; see `sweepAllowed`.
1295
+ * - **`priority` / `base` climbing while `evicted` climbs too** - the
1296
+ * frontier's touched set does not fit the worker cache, so chunks are
1297
+ * evicted and immediately refetched. Streaming never ends because it cannot.
1298
+ * - **`priority` / `base` climbing with `evicted` flat** - ordinary refinement
1299
+ * still converging on the cut; it should stop on its own.
1300
+ *
1301
+ * `uncovered` and `retiredEarly` answer a different question - "why are there
1302
+ * holes?" - and between them cover both ways this class can render nothing
1303
+ * where it should render something:
1304
+ *
1305
+ * - **`uncovered` climbing after the scene settles** - `substituteCoverage`
1306
+ * wanted a leaf's coarsest level as a stand-in and its chunk was not
1307
+ * cached. Expected briefly during initial load; afterwards it should not
1308
+ * move, because the coarsest files are pinned against eviction.
1309
+ * - **`retiredEarly` climbing** - coverage was retired before its replacement
1310
+ * landed, under pool pressure or past the retirement hold bound. This is
1311
+ * the swap path rather than the substitute path, and it is the one that
1312
+ * scales with the budget.
1313
+ *
1314
+ * Both are counted in whole leaves/groups, monotonically: they answer "did
1315
+ * this happen, and is it still happening", not "how much is missing now".
1316
+ */
1317
+ get fetchCounts() {
1318
+ return this.fetchCountsValue;
1319
+ }
1320
+ /** Chunk files given up on after repeated fetch/decode failures. */
1321
+ get failedChunkCount() {
1322
+ return this.failedFiles.size;
1323
+ }
1324
+ /**
1325
+ * Forgets all permanent chunk failures so their regions are fetched again.
1326
+ * Failures are otherwise terminal for the mesh's lifetime - call this when
1327
+ * the cause was transient (e.g. connectivity restored, `online` event).
1328
+ */
1329
+ retryFailedChunks() {
1330
+ this.failedFiles.size !== 0 && (this.failedFiles.clear(), this.retrying.clear(), this.pendingWork = !0);
1331
+ }
1332
+ /**
1333
+ * Whether the scene is still resolving toward its target detail - chunks
1334
+ * are fetching, or a retry/append is pending. Goes false once the view
1335
+ * has settled (useful to drive a loading indicator).
1336
+ */
1337
+ get isStreaming() {
1338
+ return this.pendingWork || this.fetching.size > 0 || this.retrying.size > 0;
1339
+ }
1340
+ /** The LOD distance model (mutable; e.g. raise to force the finest level). */
1341
+ get lodBaseDistance() {
1342
+ return this.scene.source.lodBaseDistance;
1343
+ }
1344
+ set lodBaseDistance(e) {
1345
+ this.scene.source.lodBaseDistance = e, this.pendingWork = !0;
1346
+ }
1347
+ update(e, t, i = {}) {
1348
+ const n = performance.now();
1349
+ e.updateMatrixWorld(), this.updateWorldMatrix(!0, !1);
1350
+ const a = Le(e, t);
1351
+ this.frontierWorker && (a ? this.pageTableViewportY = a.height : (t.getDrawingBufferSize(Se), this.pageTableViewportY = Se.y));
1352
+ const r = (a == null ? void 0 : a.head) ?? e, l = this.shouldReschedule(r, n) ? this.reschedule(r, n) : null;
1353
+ if (super.update(e, t, i), l && this.onPerformanceEvent) {
1354
+ const c = this.getUpdateTimings();
1355
+ l.cpuMs += performance.now() - l.timestamp, l.activeListMs = c.activeListMs, l.uploadMs = c.uploadMs, l.sortSubmitMs = c.sortSubmitMs, l.stagingTextureAllocations = c.stagingTextureAllocations, l.activeListUpdateRanges = c.activeListUpdateRanges, this.onPerformanceEvent(l);
1356
+ }
1357
+ }
1358
+ /** Root bounds of the whole scene, valid before any chunk has loaded. */
1359
+ computeSplatBounds() {
1360
+ return this.scene.bounds.clone();
1361
+ }
1362
+ /**
1363
+ * Enables or disables writing each resident run's **resolved** LOD level into
1364
+ * the `lodLevel` float channel (for a false-color debug modifier: 0 = finest).
1365
+ * Values come from applied desired runs after budget resolution - not from
1366
+ * distance ambition alone. Call before assigning a modifier that reads the channel.
1367
+ */
1368
+ setLodLevelDebug(e) {
1369
+ if (e) {
1370
+ this.lodLevelChannelReady || (this.defineChannel("lodLevel", { type: "float", fill: -1 }), this.lodLevelChannelReady = !0), this.lodLevelDebug = !0;
1371
+ for (const { run: t, handle: i } of this.resident.values())
1372
+ this.writeLodLevelChannel(i, t.level);
1373
+ for (const { run: t, handle: i, uploadedCount: n } of this.staged.values())
1374
+ n === t.count && this.writeLodLevelChannel(i, t.level);
1375
+ return;
1376
+ }
1377
+ this.lodLevelDebug = !1;
1378
+ }
1379
+ /** Whether {@link setLodLevelDebug} is currently writing levels. */
1380
+ get isLodLevelDebug() {
1381
+ return this.lodLevelDebug;
1382
+ }
1383
+ /**
1384
+ * Startup-hold progress for {@link StreamedSplatMeshOptions.initialReveal}.
1385
+ * Hosts using `'hold-near-l0'` or `'hold-coverage'` should keep the mesh
1386
+ * invisible while `status === 'pending'`, then reveal on `'ready'` or
1387
+ * `'degraded'`.
1388
+ */
1389
+ get initialRevealState() {
1390
+ return this.initialRevealStateValue;
1391
+ }
1392
+ /**
1393
+ * Captures a fresh startup-hold set on the next {@link update}. Hosts that
1394
+ * apply their final initial camera pose after the mesh first receives frames
1395
+ * should call this before lifting their loading cover. It is a no-op when
1396
+ * the hold was never armed (progressive startup, or a format without the
1397
+ * matching LodSource hook).
1398
+ */
1399
+ recaptureInitialReveal() {
1400
+ this.initialRevealHold !== "off" && (this.frozenCriticalRuns = null, this.initialRevealStartedAt = void 0, this.initialRevealPhase = "capture", this.initialRevealStateValue = {
1401
+ status: "pending",
1402
+ stagedSplats: 0,
1403
+ totalSplats: 0,
1404
+ readyGroups: 0,
1405
+ totalGroups: 0
1406
+ }, this.pendingWork = !0);
1407
+ }
1408
+ writeLodLevelChannel(e, t) {
1409
+ !this.lodLevelDebug || e.count === 0 || ((!this.lodLevelScratch || this.lodLevelScratch.length < e.count) && (this.lodLevelScratch = new Float32Array(e.count)), this.lodLevelScratch.fill(t, 0, e.count), this.writeChannel(e, "lodLevel", this.lodLevelScratch.subarray(0, e.count)));
1410
+ }
1411
+ /**
1412
+ * Declares a per-splat channel whose values **persist across LOD churn**:
1413
+ * edits are stored sparsely keyed by `(chunk file, local index)` - a stable
1414
+ * splat identity in the streaming design - and re-applied whenever a chunk
1415
+ * is (re)appended. Paint a region with {@link paintPersistent}, orbit away
1416
+ * until it is evicted, come back, and the values return. See M7.6.
1417
+ *
1418
+ * Wraps {@link SplatMesh.defineChannel}; read it from a modifier with
1419
+ * `ctx.channel(name)` as usual.
1420
+ */
1421
+ definePersistentChannel(e, t = {}) {
1422
+ this.defineChannel(e, t), this.persistentChannels.set(e, {
1423
+ type: t.type ?? "float",
1424
+ fill: t.fill ?? 0,
1425
+ maxEdits: Math.max(1, Math.floor(t.maxEdits ?? 1e6)),
1426
+ edits: /* @__PURE__ */ new Map(),
1427
+ total: 0,
1428
+ warned: !1
1429
+ });
1430
+ }
1431
+ /**
1432
+ * Sets a persistent channel to `value` for every currently-resident splat
1433
+ * within `radius` (world units) of `worldPoint`, and records the edit so it
1434
+ * survives eviction/reload. Splats whose chunk is not currently decoded on
1435
+ * the CPU cannot be located and are skipped (they are usually far from the
1436
+ * camera); their painted neighbours in resident chunks are unaffected.
1437
+ *
1438
+ * The radius assumes this mesh's world transform is rigid (rotation +
1439
+ * translation, as the built-in format transforms are); a scaled mesh would
1440
+ * distort the brush. Edit a persistent channel only through this method -
1441
+ * direct {@link SplatMesh.writeChannel} writes are not recorded and are
1442
+ * overwritten by the next re-apply.
1443
+ *
1444
+ * @returns the number of splats edited this call.
1445
+ * @throws {Error} if the channel was not declared with
1446
+ * {@link definePersistentChannel}.
1447
+ */
1448
+ paintPersistent(e, t, i, n) {
1449
+ const a = this.persistentChannels.get(e);
1450
+ if (!a)
1451
+ throw new Error(
1452
+ `StreamedSplatMesh.paintPersistent: channel "${e}" is not a persistent channel. Call definePersistentChannel("${e}") first.`
1453
+ );
1454
+ V.copy(t), this.worldToLocal(V);
1455
+ const r = i * i;
1456
+ let l = 0;
1457
+ const c = /* @__PURE__ */ new Set();
1458
+ for (const { run: h } of this.resident.values()) {
1459
+ const f = this.cache.get(h.file);
1460
+ if (!f) continue;
1461
+ const d = f.data.positions, g = a.edits.get(h.file) ?? /* @__PURE__ */ new Map();
1462
+ let v = !1;
1463
+ for (let m = 0; m < h.count; m++) {
1464
+ const b = h.offset + m, T = d[b * 3 + 0] - V.x, I = d[b * 3 + 1] - V.y, y = d[b * 3 + 2] - V.z;
1465
+ if (!(T * T + I * I + y * y > r) && !g.has(b)) {
1466
+ if (a.total >= a.maxEdits) {
1467
+ a.warned || (a.warned = !0, E(
1468
+ `StreamedSplatMesh.paintPersistent: channel "${e}" hit its maxEdits cap (${a.maxEdits}); further new edits are dropped.`
1469
+ ));
1470
+ continue;
1471
+ }
1472
+ a.total++, g.set(b, n), v = !0, l++;
1473
+ }
1474
+ }
1475
+ v && (a.edits.set(h.file, g), c.add(h.file));
1476
+ }
1477
+ if (c.size > 0)
1478
+ for (const { run: h, handle: f } of this.resident.values())
1479
+ c.has(h.file) && this.applyPersistentRun(e, a, h, f);
1480
+ return l;
1481
+ }
1482
+ /**
1483
+ * Clears every stored edit for a persistent channel and zeroes the value on
1484
+ * all currently-resident splats. Chunks that are not resident are covered by
1485
+ * the emptied store - they reload at the channel's fill value.
1486
+ *
1487
+ * @throws {Error} if the channel is not a persistent channel.
1488
+ */
1489
+ clearPersistentChannel(e) {
1490
+ const t = this.persistentChannels.get(e);
1491
+ if (!t)
1492
+ throw new Error(
1493
+ `StreamedSplatMesh.clearPersistentChannel: channel "${e}" is not a persistent channel.`
1494
+ );
1495
+ t.edits.clear(), t.total = 0;
1496
+ for (const { run: i, handle: n } of this.resident.values()) {
1497
+ const a = t.type === "byte" ? new Uint8Array(i.count) : new Float32Array(i.count);
1498
+ this.writeChannel(n, e, a);
1499
+ }
1500
+ }
1501
+ dispose() {
1502
+ var e, t, i, n, a;
1503
+ if (!this.disposed) {
1504
+ this.loader.dispose(), this.frontierWorker && (this.frontierWorker.onmessage = null, this.frontierWorker.terminate()), this.pageTableDisposed = !0, (e = this.collisionAbort) == null || e.abort(), (t = this.collisionTiles) == null || t.catch(() => {
1505
+ }), this.collisionTiles = void 0;
1506
+ for (const { controller: r } of this.fetching.values()) r.abort();
1507
+ if (this.fetching.clear(), this.fetchHandle && ((i = this.fetchScheduler) == null || i.unregister(this.fetchHandle)), this.cacheBudgetHandle) {
1508
+ const r = this.cacheBudgetHandle;
1509
+ this.cacheBudgetHandle = void 0, (n = this.cacheBudget) == null || n.unregister(r);
1510
+ }
1511
+ (a = this.localSource) == null || a.dispose(), this.localSource = void 0, this.cache.clear(), this.cacheBytesTotal = 0, this.retrying.clear(), this.failedFiles.clear(), this.neededFiles.clear(), this.persistentChannels.clear(), this.resident.clear(), this.staged.clear(), this.pageTableCachedFiles.clear(), this.envHandle = void 0, this.envSplatCount = 0, super.dispose();
1512
+ }
1513
+ }
1514
+ shouldReschedule(e, t) {
1515
+ if (this.pendingWork || t - this.lastScheduleTime > ft) return !0;
1516
+ e.getWorldPosition(pe);
1517
+ const i = this.scene.bounds.getBoundingSphere(wt).radius || 1;
1518
+ return pe.distanceTo(this.lastCameraPos) > i * 25e-4 ? !0 : (e.getWorldQuaternion(me), me.angleTo(this.lastCameraQuat) > 87e-4);
1519
+ }
1520
+ reschedule(e, t) {
1521
+ var L;
1522
+ const i = performance.now();
1523
+ let n = null;
1524
+ if (this.onPerformanceEvent !== void 0) {
1525
+ n = { resident: /* @__PURE__ */ new Map(), staged: /* @__PURE__ */ new Map() };
1526
+ for (const [p, S] of this.resident) n.resident.set(p, S.run.count);
1527
+ for (const [p, S] of this.staged) n.staged.set(p, S.uploadedCount);
1528
+ }
1529
+ const a = this.compactionCount;
1530
+ if (this.pendingWork = !1, this.lastScheduleTime = t, e.getWorldPosition(this.lastCameraPos), e.getWorldQuaternion(this.lastCameraQuat), x.copy(this.lastCameraPos), this.worldToLocal(x), ve.multiplyMatrices(e.projectionMatrix, e.matrixWorldInverse).multiply(this.matrixWorld), O.setFromProjectionMatrix(ve), this.frontierWorker) {
1531
+ e.getWorldDirection(B).add(this.lastCameraPos), this.worldToLocal(B), B.sub(x).normalize();
1532
+ const p = e.projectionMatrix.elements[5] * this.pageTableViewportY / 2;
1533
+ if (p > 0 && (this.pageTableLimit = this.pageTableTargetPx / p), this.reschedulePageTable(x, B, O, t), this.onPerformanceEvent === void 0) return null;
1534
+ const S = performance.now();
1535
+ return {
1536
+ timestamp: S,
1537
+ cpuMs: S - i,
1538
+ activeListMs: 0,
1539
+ uploadMs: 0,
1540
+ sortSubmitMs: 0,
1541
+ stagingTextureAllocations: 0,
1542
+ activeListUpdateRanges: 0,
1543
+ appendedCount: 0,
1544
+ removedCount: 0,
1545
+ stagedCount: 0,
1546
+ uploadCount: 0,
1547
+ activeCount: this.pageTableDrawn,
1548
+ forcedSort: !1,
1549
+ compacted: !1
1550
+ };
1551
+ }
1552
+ e.getWorldDirection(B).add(this.lastCameraPos), this.worldToLocal(B), B.sub(x).normalize();
1553
+ const r = this.scene.source.computeDesiredRuns(
1554
+ x,
1555
+ O,
1556
+ t,
1557
+ B
1558
+ ), l = this.captureOrContinueInitialReveal(
1559
+ r,
1560
+ t,
1561
+ x,
1562
+ O
1563
+ ), c = l !== null, h = l ?? r, f = /* @__PURE__ */ new Map(), d = /* @__PURE__ */ new Set();
1564
+ for (const p of h)
1565
+ f.set(C(p), p), d.add(p.file);
1566
+ for (const [p, S] of this.staged)
1567
+ f.has(p) || c && ((L = this.frozenCriticalRuns) != null && L.some((M) => C(M) === p)) || (this.removeRange(S.handle), this.staged.delete(p));
1568
+ for (const [p, { controller: S }] of this.fetching)
1569
+ !d.has(p) && !this.scene.pinnedFiles.has(p) && S.abort();
1570
+ for (const p of this.retrying.keys())
1571
+ !d.has(p) && !this.scene.pinnedFiles.has(p) && this.retrying.delete(p);
1572
+ this.neededFiles.clear();
1573
+ for (const p of h) {
1574
+ const S = C(p);
1575
+ if (this.resident.has(S)) continue;
1576
+ const M = this.staged.get(S);
1577
+ M && M.uploadedCount === p.count || this.neededFiles.add(p.file);
1578
+ }
1579
+ this.envFile !== void 0 && this.envEnabled && this.envHandle === void 0 && !this.envUnfit && !this.failedFiles.has(this.envFile) && this.neededFiles.add(this.envFile);
1580
+ const g = h.filter((p) => !this.resident.has(C(p))), v = c ? [] : [...this.resident.entries()].filter(([p]) => !f.has(p)), m = c ? (
1581
+ // Mesh is invisible during the hold, so L0 cell-atomicity (no holes) is
1582
+ // irrelevant - commit each frozen slice as it lands so a partial home
1583
+ // cell cannot block reveal behind sibling subchunks still fetching.
1584
+ _e(g)
1585
+ ) : Oe(g, v), b = !c && ze(m);
1586
+ m.sort(
1587
+ (p, S) => b ? Ue(p, S) : ae(p) - ae(S)
1588
+ );
1589
+ const T = /* @__PURE__ */ new Map();
1590
+ this.updateEnvironment(t, T);
1591
+ let I = 0, y = !1, A = !1, R = !1;
1592
+ for (const p of m) {
1593
+ if (!b && p.removes.length > 0 && y) {
1594
+ if (this.neverRetireCoverageEarly || !A && this.retireHeldTicks < ut) {
1595
+ this.pendingWork = !0, R = !0;
1596
+ continue;
1597
+ }
1598
+ this.fetchCountsValue.retiredEarly++;
1599
+ }
1600
+ if (p.adds.length === 0) {
1601
+ this.applyGroup(p, t);
1602
+ continue;
1603
+ }
1604
+ const S = p.adds.filter((P) => !this.cache.has(P.file)), M = c || Ye(p) && this.initialRevealHold !== "hold-coverage";
1605
+ if (S.length > 0) {
1606
+ let P = !1;
1607
+ for (const k of S)
1608
+ this.failedFiles.has(k.file) || (oe(
1609
+ T,
1610
+ k.file,
1611
+ qe(k, this.scene.source.lodBaseDistance),
1612
+ k
1613
+ ), P = !0);
1614
+ if (c || this.substituteCoverage(p, t, T, M), P && (this.pendingWork = !0, y = !0), !c) continue;
1615
+ }
1616
+ if (c && this.environmentPendingForReveal()) {
1617
+ this.pendingWork = !0, y = !0;
1618
+ continue;
1619
+ }
1620
+ if ((c || this.stagedSwapsEnabled && p.addCount > this.appendCap) && this.canStageGroup(p)) {
1621
+ const P = this.stageGroup(p, t, Math.max(0, this.appendCap - I));
1622
+ if (I += P, !p.adds.every((k) => {
1623
+ var se;
1624
+ return ((se = this.staged.get(C(k))) == null ? void 0 : se.uploadedCount) === k.count;
1625
+ })) {
1626
+ this.pendingWork = !0, y = !0;
1627
+ continue;
1628
+ }
1629
+ if (P > 0 && !c) {
1630
+ this.deferNextSortRequest(), this.pendingWork = !0, y = !0;
1631
+ continue;
1632
+ }
1633
+ this.commitStagedGroup(p);
1634
+ continue;
1635
+ }
1636
+ if (!c && I > 0 && I + p.addCount > this.appendCap) {
1637
+ this.pendingWork = !0, y = !0;
1638
+ continue;
1639
+ }
1640
+ if (c) {
1641
+ this.pendingWork = !0, y = !0;
1642
+ continue;
1643
+ }
1644
+ if (!this.applyGroup(p, t)) {
1645
+ this.pendingWork = !0, A = !0;
1646
+ continue;
1647
+ }
1648
+ I += p.addCount;
1649
+ }
1650
+ return this.flushClassicFetches(T, this.scene.source.lodBaseDistance, c), this.retireHeldTicks = R ? this.retireHeldTicks + 1 : 0, c && (this.finishInitialRevealIfComplete(), this.initialRevealPhase === "holding" && (this.pendingWork = !0)), this.fetchCountsValue.cacheBytes = this.cacheBytesTotal, this.fetchCountsValue.cacheLimitBytes = this.cpuCacheBytes, this.cacheBytesTotal > this.cpuCacheBytes && (this.fetchCountsValue.cacheFull = !0), this.evictChunks(t), n === null ? null : this.createPerformanceEvent(
1651
+ n.resident,
1652
+ n.staged,
1653
+ a,
1654
+ i
1655
+ );
1656
+ }
1657
+ rowAlignedSplats(e) {
1658
+ return Math.ceil(e / F) * F;
1659
+ }
1660
+ /**
1661
+ * Startup hold seeds: the coverage group containing (or nearest to) the
1662
+ * camera within {@link LodSource.lodBaseDistance}. HiRes tiles often fail the
1663
+ * frustum test when most of the cell sits behind the camera - do **not**
1664
+ * require `inView`, or the hold seeds a screen-facing neighbour instead.
1665
+ * Coarser home levels come from {@link LodSource.runsAtLevelFor}.
1666
+ */
1667
+ selectHomeSeedRuns(e) {
1668
+ var a;
1669
+ const t = this.scene.source.lodBaseDistance, i = e.filter(
1670
+ (r) => r.coverageGroup !== void 0 && (r.distance ?? Number.POSITIVE_INFINITY) <= t
1671
+ ).sort(
1672
+ (r, l) => (r.distance ?? Number.POSITIVE_INFINITY) - (l.distance ?? Number.POSITIVE_INFINITY) || // Same distance: prefer in-view, then finer.
1673
+ (r.inView === !0 ? 0 : 1) - (l.inView === !0 ? 0 : 1) || r.level - l.level || r.leafStart - l.leafStart
1674
+ ), n = (a = i[0]) == null ? void 0 : a.coverageGroup;
1675
+ return n === void 0 ? [] : i.filter((r) => r.coverageGroup === n);
1676
+ }
1677
+ coarsenHomeRuns(e, t) {
1678
+ var a;
1679
+ const i = [], n = this.scene.source;
1680
+ for (const r of e) {
1681
+ if (r.level === t) {
1682
+ i.push(r);
1683
+ continue;
1684
+ }
1685
+ const l = ((a = n.runsAtLevelFor) == null ? void 0 : a.call(n, r.leafStart, r.leafEnd, t)) ?? [];
1686
+ if (l.length !== 0)
1687
+ for (const c of l)
1688
+ i.push({
1689
+ ...c,
1690
+ distance: r.distance,
1691
+ inView: r.inView,
1692
+ ...r.coverageGroup === void 0 ? {} : { coverageGroup: r.coverageGroup },
1693
+ ...r.screenImportance === void 0 ? {} : { screenImportance: r.screenImportance }
1694
+ });
1695
+ }
1696
+ return i;
1697
+ }
1698
+ criticalRunsFitCapacity(e) {
1699
+ let t = 0;
1700
+ for (const i of e) t += this.rowAlignedSplats(i.count);
1701
+ return t <= this.freeSplatCapacity;
1702
+ }
1703
+ publishInitialRevealProgress(e) {
1704
+ const t = /* @__PURE__ */ new Map();
1705
+ for (const l of e) {
1706
+ const c = l.coverageGroup !== void 0 ? `g:${l.coverageGroup}` : C(l);
1707
+ let h = t.get(c);
1708
+ h || (h = [], t.set(c, h)), h.push(l);
1709
+ }
1710
+ let i = 0, n = 0, a = 0;
1711
+ for (const l of t.values()) {
1712
+ let c = !0;
1713
+ for (const h of l) {
1714
+ n += h.count;
1715
+ const f = C(h);
1716
+ if (this.resident.has(f)) {
1717
+ i += h.count;
1718
+ continue;
1719
+ }
1720
+ const d = this.staged.get(f);
1721
+ i += (d == null ? void 0 : d.uploadedCount) ?? 0, (!d || d.uploadedCount !== h.count) && (c = !1);
1722
+ }
1723
+ c && a++;
1724
+ }
1725
+ const r = this.initialRevealStateValue;
1726
+ if (r.status === "degraded") {
1727
+ this.initialRevealStateValue = {
1728
+ status: "degraded",
1729
+ reason: r.reason,
1730
+ stagedSplats: i,
1731
+ totalSplats: n,
1732
+ readyGroups: a,
1733
+ totalGroups: t.size
1734
+ };
1735
+ return;
1736
+ }
1737
+ this.initialRevealStateValue = {
1738
+ status: "pending",
1739
+ stagedSplats: i,
1740
+ totalSplats: n,
1741
+ readyGroups: a,
1742
+ totalGroups: t.size
1743
+ };
1744
+ }
1745
+ releaseInitialReveal(e, t) {
1746
+ const i = this.frozenCriticalRuns ?? [];
1747
+ this.publishInitialRevealProgress(i);
1748
+ const n = this.initialRevealStateValue, a = n.status === "pending" || n.status === "degraded" ? n.stagedSplats : 0, r = n.status === "pending" || n.status === "degraded" ? n.totalSplats : 0, l = n.status === "pending" || n.status === "degraded" ? n.readyGroups : 0, c = n.status === "pending" || n.status === "degraded" ? n.totalGroups : 0;
1749
+ e === "ready" ? this.initialRevealStateValue = { status: "ready" } : this.initialRevealStateValue = {
1750
+ status: "degraded",
1751
+ reason: t ?? "fetch-failed",
1752
+ stagedSplats: a,
1753
+ totalSplats: r,
1754
+ readyGroups: l,
1755
+ totalGroups: c
1756
+ }, this.frozenCriticalRuns = null, this.initialRevealPhase = "released", this.pendingWork = !0;
1757
+ }
1758
+ /**
1759
+ * `.lcc2` coverage hold: freeze coarsest covering runs for in-view cells.
1760
+ * Missing `coverageRunsFor` (or an empty result after fallback) releases
1761
+ * immediately so the mesh does not stay hidden with nothing to fetch.
1762
+ */
1763
+ captureCoverageHold(e, t, i) {
1764
+ var a, r;
1765
+ const n = ((r = (a = this.scene.source).coverageRunsFor) == null ? void 0 : r.call(a, e, t)) ?? [];
1766
+ if (n.length === 0) {
1767
+ if (this.environmentPendingForReveal()) {
1768
+ this.frozenCriticalRuns = [], this.initialRevealStartedAt = i, this.initialRevealPhase = "holding", this.publishInitialRevealProgress([]);
1769
+ return;
1770
+ }
1771
+ this.initialRevealStateValue = { status: "ready" }, this.initialRevealPhase = "released";
1772
+ return;
1773
+ }
1774
+ if (!this.criticalRunsFitCapacity(n)) {
1775
+ this.frozenCriticalRuns = n, this.releaseInitialReveal("degraded", "capacity");
1776
+ return;
1777
+ }
1778
+ this.frozenCriticalRuns = n, this.initialRevealStartedAt = i, this.initialRevealPhase = "holding", this.publishInitialRevealProgress(n);
1779
+ }
1780
+ captureOrContinueInitialReveal(e, t, i, n) {
1781
+ if (this.initialRevealPhase === "off" || this.initialRevealPhase === "released") return null;
1782
+ if (this.initialRevealPhase === "capture")
1783
+ if (this.initialRevealHold === "hold-coverage")
1784
+ this.captureCoverageHold(i, n, t);
1785
+ else {
1786
+ const a = this.selectHomeSeedRuns(e);
1787
+ let r = [];
1788
+ if (a.length === 0) {
1789
+ const l = this.scene.source.lodBaseDistance * this.scene.source.lodMultiplier * this.scene.source.lodMultiplier, c = e.filter(
1790
+ (f) => f.level <= 2 && f.coverageGroup !== void 0 && (f.distance ?? Number.POSITIVE_INFINITY) <= l
1791
+ ), h = [...c].sort(
1792
+ (f, d) => (f.distance ?? Number.POSITIVE_INFINITY) - (d.distance ?? Number.POSITIVE_INFINITY) || (f.inView === !0 ? 0 : 1) - (d.inView === !0 ? 0 : 1) || (f.screenImportance ?? Number.POSITIVE_INFINITY) - (d.screenImportance ?? Number.POSITIVE_INFINITY) || f.leafStart - d.leafStart
1793
+ )[0];
1794
+ if (!h)
1795
+ return this.initialRevealStateValue = { status: "ready" }, this.initialRevealPhase = "released", null;
1796
+ if (r = h.coverageGroup !== void 0 ? c.filter((f) => f.coverageGroup === h.coverageGroup) : [h], this.criticalRunsFitCapacity(r) || (r = h.coverageGroup !== void 0 ? c.filter(
1797
+ (f) => f.coverageGroup === h.coverageGroup && this.criticalRunsFitCapacity([f])
1798
+ ).slice(0, 1) : c.filter((f) => this.criticalRunsFitCapacity([f])).slice(0, 1)), r.length === 0 || !this.criticalRunsFitCapacity(r))
1799
+ return this.frozenCriticalRuns = r.length > 0 ? r : [h], this.releaseInitialReveal("degraded", "capacity"), null;
1800
+ this.frozenCriticalRuns = r, this.initialRevealStartedAt = t, this.initialRevealPhase = "holding", this.publishInitialRevealProgress(r);
1801
+ } else {
1802
+ for (const l of [0, 1, 2]) {
1803
+ const c = this.coarsenHomeRuns(a, l);
1804
+ if (c.length !== 0) {
1805
+ if (this.criticalRunsFitCapacity(c)) {
1806
+ r = c;
1807
+ break;
1808
+ }
1809
+ r = c;
1810
+ }
1811
+ }
1812
+ if (r.length === 0)
1813
+ return this.initialRevealStateValue = { status: "ready" }, this.initialRevealPhase = "released", null;
1814
+ if (!this.criticalRunsFitCapacity(r))
1815
+ return this.frozenCriticalRuns = r, this.releaseInitialReveal("degraded", "capacity"), null;
1816
+ this.frozenCriticalRuns = r, this.initialRevealStartedAt = t, this.initialRevealPhase = "holding", this.publishInitialRevealProgress(r);
1817
+ }
1818
+ }
1819
+ if (this.initialRevealPhase !== "holding" || !this.frozenCriticalRuns) return null;
1820
+ if (this.initialRevealStartedAt !== void 0 && t - this.initialRevealStartedAt >= ct)
1821
+ return this.releaseInitialReveal("degraded", "timeout"), null;
1822
+ for (const a of this.frozenCriticalRuns)
1823
+ if (this.failedFiles.has(a.file) && !this.resident.has(C(a))) {
1824
+ const r = this.staged.get(C(a));
1825
+ if (!r || r.uploadedCount !== a.count)
1826
+ return this.releaseInitialReveal("degraded", "fetch-failed"), null;
1827
+ }
1828
+ return this.publishInitialRevealProgress(this.frozenCriticalRuns), this.frozenCriticalRuns;
1829
+ }
1830
+ /** After staging/commits, release the hold when every frozen run is resident. */
1831
+ finishInitialRevealIfComplete() {
1832
+ this.initialRevealPhase !== "holding" || !this.frozenCriticalRuns || (this.publishInitialRevealProgress(this.frozenCriticalRuns), !this.environmentPendingForReveal() && this.frozenCriticalRuns.every((e) => this.resident.has(C(e))) && this.releaseInitialReveal("ready"));
1833
+ }
1834
+ /**
1835
+ * Startup hold still needs the environment tile when the scene ships one
1836
+ * and it starts enabled. Failed / unfit / disabled tiles do not block reveal.
1837
+ */
1838
+ environmentPendingForReveal() {
1839
+ return this.envFile !== void 0 && this.envEnabled && !this.envUnfit && this.envHandle === void 0 && !this.failedFiles.has(this.envFile);
1840
+ }
1841
+ /** Creates a performance event for a changed streamed-LOD tick, if any. */
1842
+ createPerformanceEvent(e, t, i, n) {
1843
+ let a = 0, r = 0;
1844
+ for (const [d, g] of this.resident)
1845
+ r += g.run.count, e.has(d) || (a += g.run.count);
1846
+ let l = 0;
1847
+ for (const [d, g] of e)
1848
+ this.resident.has(d) || (l += g);
1849
+ let c = 0;
1850
+ for (const [d, g] of this.staged)
1851
+ c += Math.max(0, g.uploadedCount - (t.get(d) ?? 0));
1852
+ const h = this.compactionCount !== i;
1853
+ if (a === 0 && l === 0 && c === 0 && !h) return null;
1854
+ const f = performance.now();
1855
+ return {
1856
+ timestamp: f,
1857
+ cpuMs: f - n,
1858
+ activeListMs: 0,
1859
+ uploadMs: 0,
1860
+ sortSubmitMs: 0,
1861
+ stagingTextureAllocations: 0,
1862
+ activeListUpdateRanges: 0,
1863
+ appendedCount: a,
1864
+ removedCount: l,
1865
+ stagedCount: c,
1866
+ uploadCount: a + c,
1867
+ activeCount: r,
1868
+ forcedSort: r === 0 || a + l >= r * ht,
1869
+ compacted: h
1870
+ };
1871
+ }
1872
+ /** Returns whether all new rows can coexist with the currently visible region. */
1873
+ canStageGroup(e) {
1874
+ return e.adds.filter((i) => !this.staged.has(C(i))).reduce((i, n) => i + this.rowAlignedSplats(n.count), 0) <= this.freeSplatCapacity;
1875
+ }
1876
+ /**
1877
+ * Uploads a bounded part of a replacement without rendering it.
1878
+ * Skips runs whose chunks are not yet cached so siblings can stage out of order.
1879
+ */
1880
+ stageGroup(e, t, i) {
1881
+ if (i <= 0) return 0;
1882
+ let n = 0;
1883
+ for (const a of e.adds) {
1884
+ const r = C(a), l = this.cache.get(a.file);
1885
+ if (!l) continue;
1886
+ let c = this.staged.get(r);
1887
+ if (!c) {
1888
+ let f;
1889
+ try {
1890
+ f = this.reserveInactiveRange(a.count);
1891
+ } catch {
1892
+ this.compactionCount++, this.compact();
1893
+ try {
1894
+ f = this.reserveInactiveRange(a.count);
1895
+ } catch {
1896
+ this.pendingWork = !0;
1897
+ break;
1898
+ }
1899
+ }
1900
+ c = { run: a, handle: f, uploadedCount: 0 }, this.staged.set(r, c);
1901
+ }
1902
+ if (c.uploadedCount === a.count) continue;
1903
+ const h = Math.min(a.count - c.uploadedCount, i - n);
1904
+ if (h <= 0) break;
1905
+ if (this.writeInactiveRange(
1906
+ c.handle,
1907
+ K(l.data, a.offset + c.uploadedCount, h),
1908
+ c.uploadedCount
1909
+ ), c.uploadedCount += h, l.lastUsed = t, n += h, c.uploadedCount === a.count) {
1910
+ this.writeLodLevelChannel(c.handle, a.level);
1911
+ for (const [f, d] of this.persistentChannels)
1912
+ this.applyPersistentRun(f, d, a, c.handle);
1913
+ }
1914
+ if (n >= i) break;
1915
+ }
1916
+ return n;
1917
+ }
1918
+ /** Switches a fully staged region from old to new visibility in one tick. */
1919
+ commitStagedGroup(e) {
1920
+ for (const t of e.adds) {
1921
+ const i = this.staged.get(C(t));
1922
+ if (!i || i.uploadedCount !== t.count)
1923
+ throw new Error("StreamedSplatMesh: incomplete staged group commit.");
1924
+ }
1925
+ for (const [t, i] of e.removes)
1926
+ this.resident.has(t) && (this.removeRange(i.handle), this.resident.delete(t));
1927
+ for (const t of e.adds) {
1928
+ const i = C(t), n = this.staged.get(i);
1929
+ this.setRangeActive(n.handle, !0), this.resident.set(i, n), this.staged.delete(i);
1930
+ }
1931
+ }
1932
+ /**
1933
+ * Applies one swap group atomically within this tick: removals first
1934
+ * (freeing pool rows for the replacements), then all adds. Returns false
1935
+ * without touching anything when the group cannot fit even after its own
1936
+ * removals - the caller defers it and the old runs keep rendering.
1937
+ */
1938
+ applyGroup(e, t) {
1939
+ const i = (r) => Math.ceil(r / F) * F, n = e.adds.reduce((r, l) => r + i(l.count), 0), a = e.removes.reduce((r, [, l]) => r + i(l.run.count), 0);
1940
+ if (n > this.freeSplatCapacity + a) return !1;
1941
+ for (const [r, l] of e.removes)
1942
+ this.resident.has(r) && (this.removeRange(l.handle), this.resident.delete(r));
1943
+ for (const r of e.adds)
1944
+ this.appendRun(r, t);
1945
+ return !0;
1946
+ }
1947
+ /** Appends one run from the cache, compacting the pool on fragmentation. */
1948
+ appendRun(e, t) {
1949
+ const i = this.cache.get(e.file);
1950
+ if (!i) return;
1951
+ const n = K(i.data, e.offset, e.count);
1952
+ let a;
1953
+ try {
1954
+ a = this.appendRange(n);
1955
+ } catch {
1956
+ this.compactionCount++, this.compact();
1957
+ try {
1958
+ a = this.appendRange(n);
1959
+ } catch {
1960
+ this.pendingWork = !0;
1961
+ return;
1962
+ }
1963
+ }
1964
+ this.resident.set(C(e), { run: e, handle: a }), i.lastUsed = t, this.writeLodLevelChannel(a, e.level);
1965
+ for (const [r, l] of this.persistentChannels)
1966
+ this.applyPersistentRun(r, l, e, a);
1967
+ }
1968
+ /**
1969
+ * Writes the stored edits for one run's `[offset, offset + count)` splats
1970
+ * into its freshly appended pool range. No-op when the file has no edits.
1971
+ */
1972
+ applyPersistentRun(e, t, i, n) {
1973
+ const a = t.edits.get(i.file);
1974
+ if (!a || a.size === 0) return;
1975
+ const r = t.type === "byte" ? new Uint8Array(i.count) : new Float32Array(i.count);
1976
+ t.fill && r.fill(t.fill);
1977
+ let l = !1;
1978
+ for (let c = 0; c < i.count; c++) {
1979
+ const h = a.get(i.offset + c);
1980
+ h !== void 0 && (r[c] = h, l = !0);
1981
+ }
1982
+ l && this.writeChannel(n, e, r);
1983
+ }
1984
+ /**
1985
+ * Covers a deferred group's leaves that no resident run covers with each
1986
+ * leaf's coarsest (pinned, hence cached) level, so a region waiting on a
1987
+ * fetch shows coarse detail instead of nothing. The substitutes are
1988
+ * intentionally not "desired": the next reschedule swaps them for the
1989
+ * real level once its chunk has arrived.
1990
+ *
1991
+ * Near-camera refinements (`distance <= lodBaseDistance`) skip the coarse
1992
+ * paint and its pin fetch entirely - cold load requests only the target cut
1993
+ * for that cell. Far gaps keep the shell.
1994
+ */
1995
+ substituteCoverage(e, t, i, n) {
1996
+ const a = e.leafEnd - e.leafStart;
1997
+ (this.coverageScratch === void 0 || this.coverageScratch.length < a) && (this.coverageScratch = new Uint8Array(a));
1998
+ const r = this.coverageScratch;
1999
+ r.fill(0, 0, a);
2000
+ for (const { run: h } of this.resident.values()) {
2001
+ const f = Math.max(h.leafStart, e.leafStart), d = Math.min(h.leafEnd, e.leafEnd);
2002
+ d > f && r.fill(1, f - e.leafStart, d - e.leafStart);
2003
+ }
2004
+ const l = r.subarray(0, a);
2005
+ let c = 0;
2006
+ for (; c < a; ) {
2007
+ const h = l.indexOf(0, c);
2008
+ if (h < 0) break;
2009
+ const f = l.indexOf(1, h), d = f < 0 ? a : f, g = e.leafStart + h, v = e.leafStart + d;
2010
+ for (const m of this.scene.source.coarsestRunsFor(g, v))
2011
+ if (!this.resident.has(C(m))) {
2012
+ if (n) {
2013
+ this.fetchCountsValue.uncovered += Math.min(m.leafEnd, v) - Math.max(m.leafStart, g);
2014
+ continue;
2015
+ }
2016
+ if (!this.cache.has(m.file)) {
2017
+ oe(
2018
+ i,
2019
+ m.file,
2020
+ je(m, this.scene.source.lodBaseDistance),
2021
+ m
2022
+ ), this.fetchCountsValue.uncovered += Math.min(m.leafEnd, v) - Math.max(m.leafStart, g);
2023
+ continue;
2024
+ }
2025
+ for (const [b, T] of this.resident)
2026
+ T.run.leafStart < m.leafEnd && T.run.leafEnd > m.leafStart && (this.removeRange(T.handle), this.resident.delete(b));
2027
+ this.appendRun(m, t);
2028
+ }
2029
+ c = d;
2030
+ }
2031
+ }
2032
+ /** Issues pending classic-path chunk wants in group-priority order. */
2033
+ flushClassicFetches(e, t, i = !1) {
2034
+ if (e.size === 0) return;
2035
+ $e(e, t, i);
2036
+ const n = [...e.entries()].sort(
2037
+ (a, r) => X(a[1], r[1], a[0], r[0])
2038
+ );
2039
+ this.preemptClassicFetches(n);
2040
+ for (const [a, r] of n) this.requestChunk(a, r.kind, r);
2041
+ }
2042
+ /**
2043
+ * A camera turn must not wait for all eight old visible requests to finish.
2044
+ * Only classic requests carry a precise rank; page-table work retains its
2045
+ * own scheduler and is never cancelled here.
2046
+ */
2047
+ preemptClassicFetches(e) {
2048
+ var t;
2049
+ for (const [i, n] of e) {
2050
+ if (this.cache.has(i) || this.fetching.has(i)) continue;
2051
+ let a, r;
2052
+ for (const [l, c] of this.fetching)
2053
+ c.classicWant && (!r || X(c.classicWant, r, l, a) > 0) && (a = l, r = c.classicWant);
2054
+ if (r && a !== void 0 && X(n, r, i, a) < 0 && ((t = this.fetching.get(a)) == null || t.controller.abort()), this.fetching.size >= this.maxInflight) return;
2055
+ }
2056
+ }
2057
+ /**
2058
+ * Loads the always-resident environment tile once, on the first update after
2059
+ * it is wanted. The tile has no LOD ladder and no manifest count, so it is
2060
+ * appended whole (measuring its splat count at decode) and thereafter toggled
2061
+ * by flipping its pool range active - never scheduled, refetched, or evicted.
2062
+ * When `pending` is supplied, a miss is ranked as an `'environment'` want so
2063
+ * it issues ahead of LOD coverage.
2064
+ */
2065
+ updateEnvironment(e, t) {
2066
+ const i = this.envFile;
2067
+ if (i === void 0 || this.envHandle !== void 0 || !this.envEnabled || this.envUnfit)
2068
+ return;
2069
+ const n = this.cache.get(i);
2070
+ if (!n) {
2071
+ this.failedFiles.has(i) || (t ? this.enqueueEnvironmentFetch(t, i) : this.requestChunk(i, "priority"), this.pendingWork = !0);
2072
+ return;
2073
+ }
2074
+ const a = Math.ceil(n.data.count / F) * F;
2075
+ if (a > this.capacity) {
2076
+ this.envUnfit = !0, E(
2077
+ `the environment tile (${n.data.count} splats) exceeds the pool capacity (${this.capacity}); it will not be shown. Raise the splat budget to fit it.`
2078
+ );
2079
+ return;
2080
+ }
2081
+ if (a > this.freeSplatCapacity) {
2082
+ this.pendingWork = !0;
2083
+ return;
2084
+ }
2085
+ let r;
2086
+ try {
2087
+ r = this.appendRange(n.data);
2088
+ } catch {
2089
+ this.compactionCount++, this.compact();
2090
+ try {
2091
+ r = this.appendRange(n.data);
2092
+ } catch {
2093
+ this.pendingWork = !0;
2094
+ return;
2095
+ }
2096
+ }
2097
+ this.envHandle = r, this.envSplatCount = n.data.count, n.lastUsed = e;
2098
+ }
2099
+ /** Ranks the env tile ahead of every LOD want in {@link flushClassicFetches}. */
2100
+ enqueueEnvironmentFetch(e, t) {
2101
+ e.has(t) || e.set(t, {
2102
+ kind: "priority",
2103
+ phase: "environment",
2104
+ distance: 0,
2105
+ level: 0,
2106
+ inView: !0,
2107
+ coverageGroup: -1,
2108
+ leafStart: 0,
2109
+ leafEnd: 0,
2110
+ screenImportance: Number.NEGATIVE_INFINITY,
2111
+ groupDistance: 0,
2112
+ groupPending: 1,
2113
+ groupInView: !0,
2114
+ groupScreenImportance: Number.NEGATIVE_INFINITY,
2115
+ groupFinest: !0,
2116
+ groupId: "environment",
2117
+ groupClass: 0
2118
+ });
2119
+ }
2120
+ /**
2121
+ * Page-table reschedule (`foveationMode: 'page-table'`): posts the camera to the
2122
+ * worker, which owns the cache + traversal + pager and replies asynchronously
2123
+ * with a paging plan. Coalesced to one outstanding request so the main thread
2124
+ * never blocks. Also drives chunk fetching, in priority order.
2125
+ */
2126
+ reschedulePageTable(e, t, i, n) {
2127
+ for (const r of this.pageTableFetchPriority) this.requestChunk(r, "priority");
2128
+ const a = new Set(this.pageTableFetchPriority);
2129
+ for (const r of this.scene.source.computeDesiredRuns(e, i, n))
2130
+ a.add(r.file), this.requestChunk(r.file, "base");
2131
+ for (const [r, l] of this.fetching)
2132
+ l.kind !== "sweep" && !a.has(r) && !this.scene.pinnedFiles.has(r) && l.controller.abort();
2133
+ if (!this.pageTableCacheAtLimit && this.sweepAllowed()) {
2134
+ const r = Math.max(1, this.maxInflight - mt), l = this.scene.chunkUrls.length;
2135
+ for (let c = 0; c < l && this.fetching.size < r; c++)
2136
+ this.pageTableCachedFiles.has(c) || this.requestChunk(c, "sweep");
2137
+ }
2138
+ this.pageTableInFlight || (this.pageTableInFlight = !0, this.postToWorker({
2139
+ type: "reschedule",
2140
+ seq: ++this.pageTableSeq,
2141
+ cameraLocal: [e.x, e.y, e.z],
2142
+ cameraForward: [t.x, t.y, t.z],
2143
+ ...this.pageTableFoveation,
2144
+ // Spark's cut is `pixel_scale × lodScale ≤ limit`, and the traversal only
2145
+ // ever sees one side of that - so scaling the limit down by `lodScale` is
2146
+ // the same comparison, with no protocol change.
2147
+ limit: this.pageTableLimit / this.lodScaleValue,
2148
+ budget: this.pageTableDrawBudget
2149
+ }));
2150
+ }
2151
+ /**
2152
+ * Applies a paging plan from the worker to the slab - fast memcpy writes only,
2153
+ * no traversal or gather on the main thread - then fetches the chunks the
2154
+ * frontier wants next, and reschedules again if chunks are still streaming.
2155
+ */
2156
+ applyFrontierPlan(e) {
2157
+ if (this.pageTableInFlight = !1, this.pageTableDisposed || this.slabPages.length === 0) return;
2158
+ const t = this.pagerSlots;
2159
+ e.capacity !== t && (this.pendingWork = !0, this.lastScheduleTime = -1 / 0);
2160
+ const i = performance.now(), n = e.moveSlots;
2161
+ for (let d = 0; d < n.length; ) {
2162
+ let g = 1;
2163
+ for (; d + g < n.length && n[d + g] === n[d] + g; )
2164
+ g++;
2165
+ const v = n[d], m = Math.min(g, t - v);
2166
+ m > 0 && this.writeSlabSlots(yt(e.moves, d, m), v, m), d += g;
2167
+ }
2168
+ if (e.appends.count > 0) {
2169
+ const d = Math.min(e.appends.count, t - e.appendStart);
2170
+ d > 0 && this.writeSlabSlots(e.appends, e.appendStart, d);
2171
+ }
2172
+ const a = performance.now(), r = Math.min(e.residentCount, t);
2173
+ this.setSlabResident(r);
2174
+ const l = Math.min(e.degenerateStart, t), c = Math.min(e.degenerateCount, t - l);
2175
+ c > 0 && this.degenerateSlabSlots(l, c);
2176
+ const h = performance.now();
2177
+ this.pageTableDrawn = r, this.frontierConverged = e.converged, this.pendingFrontierSplats = e.pendingFrontierSplats ?? 0, this.staleResidentSplats = e.staleResidentSplats ?? 0, this.lastPlanAppends = e.lastPlanAppends ?? e.appends.count, this.lastPlanMoves = e.lastPlanMoves ?? e.moveSlots.length, this.lastPlanGeneration = e.planGeneration ?? this.lastPlanGeneration + 1, this.lastPlanBudget = e.planBudget ?? this.pageTableDrawBudget, e.cameraLocal && (this.lastPlanCamera = e.cameraLocal, this.firstFrontierCamera ?? (this.firstFrontierCamera = e.cameraLocal)), e.gatherMissing > 0 && E(
2178
+ `StreamedSplatMesh: page-table plan gathered ${e.gatherMissing} splats from evicted chunks; they render as holes.`
2179
+ );
2180
+ const f = this.planTimingsValue;
2181
+ if (f.applyMs = h - i, f.writeMs = a - i, f.residentMs = h - a, f.moves = e.moveSlots.length, f.appends = e.appends.count, f.applyMs > f.worstApplyMs && (f.worstApplyMs = f.applyMs, f.worstSplats = f.moves + f.appends), this.frontierBandBase !== null && e.solvedLimit > 0 && this.pageTableLimit > 0) {
2182
+ const d = Math.min(1, e.solvedLimit / this.pageTableLimit);
2183
+ this.setScreenRadiusBand(
2184
+ this.frontierBandBase.min * d,
2185
+ this.frontierBandBase.max * d
2186
+ );
2187
+ }
2188
+ this.invalidateSort(), e.dropped > 0 && E(
2189
+ `StreamedSplatMesh: page-table slab full, dropped ${e.dropped} frontier splats (draw budget ${this.pageTableDrawBudget} exceeds the pool).`
2190
+ );
2191
+ for (let d = 0; d < e.evicted.length; d++)
2192
+ this.pageTableCachedFiles.delete(e.evicted[d]);
2193
+ this.fetchCountsValue.cacheBytes = e.cacheBytes, this.fetchCountsValue.cacheLimitBytes = e.cacheLimitBytes, this.pageTableCacheAtLimit = e.cacheBytes >= e.cacheLimitBytes, e.evicted.length > 0 && (this.fetchCountsValue.cacheFull = !0, this.fetchCountsValue.evicted += e.evicted.length), this.pageTableFetchPriority = Array.from(e.touched);
2194
+ for (const d of this.pageTableFetchPriority) this.requestChunk(d, "priority");
2195
+ (this.fetching.size > 0 || !e.converged) && (this.pendingWork = !0, e.converged || (this.lastScheduleTime = -1 / 0));
2196
+ }
2197
+ /** Forwards a decoded chunk's arrays to the worker (buffers transferred) so the
2198
+ * worker's cache/traversal/gather can use it. */
2199
+ forwardChunkToWorker(e, t) {
2200
+ const i = t.radTree;
2201
+ if (!i) return;
2202
+ this.pageTableCachedFiles.add(e);
2203
+ const n = this.shBands > 0 ? t.shPacked : void 0;
2204
+ this.postToWorker(
2205
+ {
2206
+ type: "chunk",
2207
+ file: e,
2208
+ count: t.count,
2209
+ positions: t.positions,
2210
+ colors: t.colors,
2211
+ covariances: t.covariances,
2212
+ childCount: i.childCount,
2213
+ childStart: i.childStart,
2214
+ size: i.size,
2215
+ shBands: (n == null ? void 0 : n.bands) ?? 0,
2216
+ ...n ? { shPacked: n.packed, shRange: n.range } : {}
2217
+ },
2218
+ [
2219
+ t.positions.buffer,
2220
+ t.colors.buffer,
2221
+ t.covariances.buffer,
2222
+ i.childCount.buffer,
2223
+ i.childStart.buffer,
2224
+ i.size.buffer,
2225
+ ...n ? [n.packed.buffer] : []
2226
+ ]
2227
+ );
2228
+ }
2229
+ /** Parallel chunk fetches. HTTP/2 multiplexes them; on HTTP/1.1 the browser's
2230
+ * per-host cap simply queues. Same cap for classic and page-table so near
2231
+ * detail is not structurally starved on the non-page-table path. */
2232
+ get maxInflight() {
2233
+ return dt;
2234
+ }
2235
+ /**
2236
+ * Whether this mesh may run its speculative background sweep. A mesh with no
2237
+ * weight is hidden or suspended; a mesh with no `fetchWeight` at all is a
2238
+ * host that never asked for arbitration, and keeps the old behaviour.
2239
+ */
2240
+ /**
2241
+ * Sets this mesh's share of the scene's fetch bandwidth, as
2242
+ * {@link StreamedSplatMeshOptions.fetchWeight} does at load.
2243
+ *
2244
+ * The weight normally closes over the mesh itself (`() =>
2245
+ * governor.weightOf(mesh)`), which a host cannot express until `load`
2246
+ * resolves - hence a setter as well as an option. Pass `undefined` to go back
2247
+ * to unarbitrated sweeping.
2248
+ */
2249
+ setFetchWeight(e) {
2250
+ this.fetchWeight = e;
2251
+ }
2252
+ sweepAllowed() {
2253
+ if (this.performanceProfile === "smooth") return !1;
2254
+ if (this.fetchWeight === void 0) return !0;
2255
+ const e = this.fetchWeight();
2256
+ return Number.isFinite(e) && e > 0;
2257
+ }
2258
+ /** Aborts in-flight fetches of one kind; their slots return through `finally`. */
2259
+ abortFetches(e) {
2260
+ for (const t of this.fetching.values())
2261
+ t.kind === e && t.controller.abort();
2262
+ }
2263
+ requestChunk(e, t, i) {
2264
+ var l, c;
2265
+ if (this.cache.has(e) || this.pageTableCachedFiles.has(e) || this.fetching.has(e) || this.fetching.size >= this.maxInflight || this.failedFiles.has(e)) return;
2266
+ const n = this.retrying.get(e);
2267
+ if (n && performance.now() < n.readyAt) return;
2268
+ this.fetchCountsValue[t]++;
2269
+ const a = this.scene.chunkUrls[e];
2270
+ if (a === void 0) {
2271
+ E(`StreamedSplatMesh: manifest references unknown chunk file #${e}.`), this.failedFiles.add(e);
2272
+ return;
2273
+ }
2274
+ if (this.fetchHandle && !((l = this.fetchScheduler) != null && l.tryAcquire(this.fetchHandle, t))) return;
2275
+ const r = new AbortController();
2276
+ this.fetching.set(e, { controller: r, kind: t, classicWant: i }), this.loader.load(a, {
2277
+ kind: this.scene.chunkKind,
2278
+ signal: r.signal,
2279
+ ...(c = this.scene.chunkOptions) == null ? void 0 : c[e]
2280
+ }).then((h) => {
2281
+ var f, d;
2282
+ this.disposed || (this.retrying.delete(e), (d = (f = this.scene.source).onChunkDecoded) == null || d.call(f, e, h), this.frontierWorker ? (this.forwardChunkToWorker(e, h), this.pendingWork = !0) : this.cacheChunk(e, h));
2283
+ }).catch((h) => {
2284
+ var d;
2285
+ if (N(h)) return;
2286
+ const f = (((d = this.retrying.get(e)) == null ? void 0 : d.attempts) ?? 0) + 1;
2287
+ if (f >= gt)
2288
+ this.retrying.delete(e), this.failedFiles.add(e), E(
2289
+ `StreamedSplatMesh: gave up on chunk #${e} (${a}) after ${f} attempts.`,
2290
+ h
2291
+ );
2292
+ else {
2293
+ const g = pt * 2 ** (f - 1);
2294
+ this.retrying.set(e, { attempts: f, readyAt: performance.now() + g });
2295
+ }
2296
+ }).finally(() => {
2297
+ var h;
2298
+ this.fetching.delete(e), this.fetchHandle && ((h = this.fetchScheduler) == null || h.release(this.fetchHandle)), this.pendingWork = !0;
2299
+ });
2300
+ }
2301
+ /**
2302
+ * Stores a decoded chunk while keeping {@link cacheBytesTotal} in step. The
2303
+ * counter replaces a full-cache re-sum on every reschedule; every mutation
2304
+ * of {@link cache} (this method, eviction, dispose's clear) maintains it.
2305
+ */
2306
+ cacheChunk(e, t) {
2307
+ const i = this.cache.get(e);
2308
+ i !== void 0 && (this.cacheBytesTotal -= i.bytes);
2309
+ const n = Xe(t);
2310
+ this.cache.set(e, { data: t, bytes: n, lastUsed: performance.now() }), this.cacheBytesTotal += n;
2311
+ }
2312
+ evictChunks(e) {
2313
+ let t = this.cacheBytesTotal;
2314
+ if (t <= this.cpuCacheBytes) return;
2315
+ const i = [...this.cache.entries()].filter(
2316
+ ([n, a]) => a.lastUsed !== e && !this.scene.pinnedFiles.has(n) && !this.neededFiles.has(n)
2317
+ ).sort((n, a) => n[1].lastUsed - a[1].lastUsed);
2318
+ for (const [n, a] of i) {
2319
+ if (t <= this.cpuCacheBytes) break;
2320
+ this.cache.delete(n), this.cacheBytesTotal -= a.bytes, t -= a.bytes, this.fetchCountsValue.evicted++;
2321
+ }
2322
+ }
2323
+ }
2324
+ const V = new w.Vector3(), pe = new w.Vector3(), me = new w.Quaternion(), x = new w.Vector3(), ve = new w.Matrix4(), O = new w.Frustum(), wt = new w.Sphere(), B = new w.Vector3(), Se = new w.Vector2();
2325
+ function be(o) {
2326
+ return Math.ceil(3 * Ie(o) / 4);
2327
+ }
2328
+ function yt(o, s, e) {
2329
+ const t = o.shPacked;
2330
+ return {
2331
+ count: e,
2332
+ positions: o.positions.subarray(s * 3, (s + e) * 3),
2333
+ colors: o.colors.subarray(s * 4, (s + e) * 4),
2334
+ covariances: o.covariances.subarray(s * 6, (s + e) * 6),
2335
+ ...t ? {
2336
+ shPacked: {
2337
+ ...t,
2338
+ packed: t.packed.subarray(
2339
+ s * be(t.bands),
2340
+ (s + e) * be(t.bands)
2341
+ )
2342
+ }
2343
+ } : {}
2344
+ };
2345
+ }
2346
+ const Ct = 1;
2347
+ class Tt {
2348
+ constructor(s = {}) {
2349
+ u(this, "entries", /* @__PURE__ */ new Map());
2350
+ u(this, "total");
2351
+ u(this, "hysteresis");
2352
+ this.total = W(s.totalBudget);
2353
+ const e = s.hysteresis ?? 0.1;
2354
+ if (!Number.isFinite(e) || e < 0)
2355
+ throw new RangeError("BudgetGovernor hysteresis must be a non-negative finite number.");
2356
+ this.hysteresis = e;
2357
+ }
2358
+ /** The shared budget currently being split across members. */
2359
+ get totalBudget() {
2360
+ return this.total;
2361
+ }
2362
+ /** Replaces the shared total and reallocates. */
2363
+ setTotalBudget(s) {
2364
+ const e = W(s);
2365
+ e !== this.total && (this.total = e, this.reallocate());
2366
+ }
2367
+ /** Number of registered members. */
2368
+ get size() {
2369
+ return this.entries.size;
2370
+ }
2371
+ /**
2372
+ * Adds a member and reallocates the shared budget. The member's current
2373
+ * budget is remembered and restored when it leaves the governor.
2374
+ *
2375
+ * @param member - The mesh (or compatible object) to govern.
2376
+ * @param options - `weight` (default `1`): the member's share is
2377
+ * proportional to its weight - e.g. main mesh `7`, additional mesh `3` reproduces
2378
+ * the old 0.7 host split. `0` registers the member suspended.
2379
+ */
2380
+ register(s, e = {}) {
2381
+ if (this.entries.has(s))
2382
+ throw new Error("BudgetGovernor: member is already registered.");
2383
+ const t = J(e.weight ?? 1);
2384
+ this.entries.set(s, {
2385
+ member: s,
2386
+ weight: t,
2387
+ applied: s.budget,
2388
+ restoreBudget: s.budget
2389
+ }), this.reallocate();
2390
+ }
2391
+ /**
2392
+ * Removes a member, restores the budget it had when it registered, and
2393
+ * reallocates the total across the remaining members. No-op for a member
2394
+ * that is not registered (so disposing hosts need not track membership).
2395
+ */
2396
+ unregister(s) {
2397
+ const e = this.entries.get(s);
2398
+ e !== void 0 && (this.entries.delete(s), e.member.setBudget(e.restoreBudget), this.reallocate());
2399
+ }
2400
+ /**
2401
+ * Changes a member's priority weight and reallocates. `0` suspends the
2402
+ * member (see the class doc); any positive weight resumes it.
2403
+ */
2404
+ setWeight(s, e) {
2405
+ const t = this.entryOf(s), i = J(e);
2406
+ i !== t.weight && (t.weight = i, this.reallocate());
2407
+ }
2408
+ /**
2409
+ * Writes several weights, then reallocates **once**.
2410
+ *
2411
+ * Prefer this to a loop of {@link setWeight} whenever more than one weight
2412
+ * changes together - as a camera-driven reweight does. Each reallocation
2413
+ * pushes `setBudget` to every member, and for a streamed mesh that forces an
2414
+ * LOD reschedule, so N separate calls cost N passes over the whole group to
2415
+ * reach a state one pass would have produced.
2416
+ *
2417
+ * @param weights - `[member, weight]` pairs. Every member must be registered;
2418
+ * unlisted members keep their current weight.
2419
+ * @throws {Error} if any member is not registered - checked before anything
2420
+ * is written, so a bad pair leaves every weight untouched.
2421
+ * @throws {RangeError} if any weight is not a non-negative finite number.
2422
+ */
2423
+ setWeights(s) {
2424
+ const e = [...s].map(
2425
+ ([i, n]) => [this.entryOf(i), J(n)]
2426
+ );
2427
+ let t = !1;
2428
+ for (const [i, n] of e)
2429
+ i.weight !== n && (i.weight = n, t = !0);
2430
+ t && this.reallocate();
2431
+ }
2432
+ /** The budget the governor last applied to a member, if registered. */
2433
+ budgetOf(s) {
2434
+ var e;
2435
+ return (e = this.entries.get(s)) == null ? void 0 : e.applied;
2436
+ }
2437
+ /**
2438
+ * Recomputes and applies every member's share. Called automatically by all
2439
+ * mutators; call it manually only if a member's internal ceiling changed
2440
+ * outside the governor's view.
2441
+ */
2442
+ reallocate() {
2443
+ let s = [];
2444
+ for (const t of this.entries.values())
2445
+ t.weight === 0 ? this.applyTarget(t, Ct) : s.push(t);
2446
+ let e = this.total;
2447
+ for (; s.length > 0; ) {
2448
+ const t = s.reduce((a, r) => a + r.weight, 0), i = [];
2449
+ let n = 0;
2450
+ for (const a of s) {
2451
+ const r = Math.max(1, Math.floor(e * a.weight / t));
2452
+ this.applyTarget(a, r) && (i.push(a), n += a.applied);
2453
+ }
2454
+ if (i.length === 0) break;
2455
+ e = Math.max(0, e - n), s = s.filter((a) => !i.includes(a));
2456
+ }
2457
+ }
2458
+ /**
2459
+ * Restores every member's pre-registration budget and empties the governor.
2460
+ * The governor itself stays usable (dispose is just "unregister everyone").
2461
+ */
2462
+ dispose() {
2463
+ for (const s of this.entries.values())
2464
+ s.member.setBudget(s.restoreBudget);
2465
+ this.entries.clear();
2466
+ }
2467
+ /** The entry for a registered member, or a thrown error naming the problem. */
2468
+ entryOf(s) {
2469
+ const e = this.entries.get(s);
2470
+ if (e === void 0)
2471
+ throw new Error("BudgetGovernor: member is not registered.");
2472
+ return e;
2473
+ }
2474
+ /**
2475
+ * Pushes `target` to a member, with the grow dead-band. Returns true when
2476
+ * the member clamped below its target (it is capped and cannot absorb more).
2477
+ */
2478
+ applyTarget(s, e) {
2479
+ if (e > s.applied) {
2480
+ if (e - s.applied <= this.hysteresis * s.applied) return !1;
2481
+ } else if (e === s.applied)
2482
+ return !1;
2483
+ return s.applied = s.member.setBudget(e), s.applied < e;
2484
+ }
2485
+ }
2486
+ function J(o) {
2487
+ if (!Number.isFinite(o) || o < 0)
2488
+ throw new RangeError(
2489
+ "BudgetGovernor member weight must be a non-negative finite number (0 suspends the member)."
2490
+ );
2491
+ return o;
2492
+ }
2493
+ const we = new w.Vector3(), ye = new w.Matrix4(), Ce = new w.Frustum(), Z = new w.Box3(), z = new w.Sphere();
2494
+ class zt {
2495
+ constructor(s = {}) {
2496
+ u(this, "entries", /* @__PURE__ */ new Map());
2497
+ u(this, "budgetGovernor");
2498
+ u(this, "minIntervalMs");
2499
+ u(this, "weightDeadband");
2500
+ u(this, "falloff");
2501
+ u(this, "offScreenWeight");
2502
+ u(this, "minWeight");
2503
+ u(this, "maxWeight");
2504
+ u(this, "lastUpdateAt", -1 / 0);
2505
+ /** Set by membership/priority changes: the next update ignores both damps. */
2506
+ u(this, "forceNext", !0);
2507
+ if (this.budgetGovernor = s.governor ?? new Tt(s), this.minIntervalMs = D(s.minIntervalMs ?? 250, "minIntervalMs"), this.weightDeadband = D(s.weightDeadband ?? 0.15, "weightDeadband"), this.falloff = U(s.falloff ?? 1, "falloff"), this.offScreenWeight = U(s.offScreenWeight ?? 0.25, "offScreenWeight"), this.minWeight = U(s.minWeight ?? 0.05, "minWeight"), this.maxWeight = U(s.maxWeight ?? 8, "maxWeight"), this.maxWeight < this.minWeight)
2508
+ throw new RangeError("CameraBudgetGovernor maxWeight must be >= minWeight.");
2509
+ }
2510
+ /** The governor this drives; use it for fixed-weight members and diagnostics. */
2511
+ get governor() {
2512
+ return this.budgetGovernor;
2513
+ }
2514
+ /** The shared total being split, from the underlying governor. */
2515
+ get totalBudget() {
2516
+ return this.budgetGovernor.totalBudget;
2517
+ }
2518
+ /** Replaces the shared total (e.g. the presenting budget on `sessionstart`). */
2519
+ setTotalBudget(s) {
2520
+ this.budgetGovernor.setTotalBudget(s);
2521
+ }
2522
+ /** Number of camera-weighted members. */
2523
+ get size() {
2524
+ return this.entries.size;
2525
+ }
2526
+ /**
2527
+ * Adds a member, registers it with the underlying governor, and forces a
2528
+ * reweight on the next {@link update}.
2529
+ *
2530
+ * @throws {Error} if the member is already registered here.
2531
+ * @throws {RangeError} if `priority` or `fixedWeight` is invalid.
2532
+ */
2533
+ register(s, e = {}) {
2534
+ if (this.entries.has(s))
2535
+ throw new Error("CameraBudgetGovernor: member is already registered.");
2536
+ const t = D(e.priority ?? 1, "priority"), i = e.fixedWeight === void 0 ? void 0 : D(e.fixedWeight, "fixedWeight"), n = i ?? t;
2537
+ this.budgetGovernor.register(s, { weight: n }), this.entries.set(s, { member: s, priority: t, fixedWeight: i, applied: n }), this.forceNext = !0;
2538
+ }
2539
+ /**
2540
+ * Removes a member from this helper and the underlying governor, restoring
2541
+ * the budget it had when it joined. No-op for an unknown member.
2542
+ */
2543
+ unregister(s) {
2544
+ this.entries.delete(s) && (this.budgetGovernor.unregister(s), this.forceNext = !0);
2545
+ }
2546
+ /**
2547
+ * Changes a member's priority tier. Takes effect on the next {@link update},
2548
+ * which is forced - a deliberate focus change should not wait out the
2549
+ * interval or be swallowed by the dead-band.
2550
+ *
2551
+ * @throws {Error} if the member is not registered here.
2552
+ * @throws {RangeError} if `priority` is negative or not finite.
2553
+ */
2554
+ setPriority(s, e) {
2555
+ const t = this.entries.get(s);
2556
+ if (t === void 0)
2557
+ throw new Error("CameraBudgetGovernor: member is not registered.");
2558
+ const i = D(e, "priority");
2559
+ i !== t.priority && (t.priority = i, this.forceNext = !0);
2560
+ }
2561
+ /**
2562
+ * Recomputes every member's weight from the camera and applies them in one
2563
+ * batch. Call once per frame, after the scene graph is up to date.
2564
+ *
2565
+ * Needs no renderer: weights are a ratio, so viewport size cancels out.
2566
+ *
2567
+ * Skipped - returning `false` - when called inside `minIntervalMs` of the
2568
+ * last reweight, or when no member's weight moved by more than
2569
+ * `weightDeadband`. Membership and priority changes force it through both.
2570
+ *
2571
+ * @param camera - The view detail should follow. In an immersive session pass
2572
+ * the head/`ArrayCamera`, not the idle application camera.
2573
+ * @param now - Timestamp in ms on the `performance.now` clock; defaults to it.
2574
+ * @returns whether weights were reapplied.
2575
+ */
2576
+ update(s, e = performance.now()) {
2577
+ if (this.entries.size === 0) return !1;
2578
+ const t = this.forceNext;
2579
+ if (!t && e - this.lastUpdateAt < this.minIntervalMs) return !1;
2580
+ s.updateMatrixWorld(), s.getWorldPosition(we), ye.multiplyMatrices(s.projectionMatrix, s.matrixWorldInverse), Ce.setFromProjectionMatrix(ye);
2581
+ const i = [];
2582
+ let n = !1;
2583
+ for (const a of this.entries.values()) {
2584
+ const r = this.weightFor(a);
2585
+ i.push([a.member, r]), !n && this.isSignificant(a.applied, r) && (n = !0);
2586
+ }
2587
+ if (!t && !n) return !1;
2588
+ this.budgetGovernor.setWeights(i);
2589
+ for (const [a, r] of i) {
2590
+ const l = this.entries.get(a);
2591
+ l !== void 0 && (l.applied = r);
2592
+ }
2593
+ return this.lastUpdateAt = e, this.forceNext = !1, !0;
2594
+ }
2595
+ /** The weight currently written for a member, if registered here. */
2596
+ weightOf(s) {
2597
+ var e;
2598
+ return (e = this.entries.get(s)) == null ? void 0 : e.applied;
2599
+ }
2600
+ /** The budget the underlying governor last applied to a member. */
2601
+ budgetOf(s) {
2602
+ return this.budgetGovernor.budgetOf(s);
2603
+ }
2604
+ /**
2605
+ * Unregisters every member, restoring the budget each had when it joined.
2606
+ * The helper stays usable afterwards. A governor passed in by the host keeps
2607
+ * any members the host registered on it directly.
2608
+ */
2609
+ dispose() {
2610
+ for (const s of [...this.entries.keys()]) this.budgetGovernor.unregister(s);
2611
+ this.entries.clear(), this.lastUpdateAt = -1 / 0, this.forceNext = !0;
2612
+ }
2613
+ /**
2614
+ * A member's weight: `priority × clamp((radius / distance) ^ falloff) ×
2615
+ * offScreen`.
2616
+ *
2617
+ * `radius / distance` is the tangent of the member's half angular size - the
2618
+ * same `size / distance` measure Spark's `pixel_scale` traversal ranks nodes
2619
+ * by, one level up at whole-mesh granularity. Distance is measured to the
2620
+ * bounding sphere's *surface*, so a large mesh is not penalized for having a
2621
+ * distant center, and is floored so a camera inside the bounds saturates at
2622
+ * `maxWeight` rather than dividing by zero.
2623
+ */
2624
+ weightFor(s) {
2625
+ if (s.fixedWeight !== void 0) return s.fixedWeight;
2626
+ const { member: e, priority: t } = s;
2627
+ if (t === 0 || !(e.effectiveVisibility ?? e.visible)) return 0;
2628
+ if (e.updateWorldMatrix(!0, !1), Z.copy(e.computeSplatBounds()), Z.isEmpty()) return t * this.minWeight;
2629
+ Z.applyMatrix4(e.matrixWorld).getBoundingSphere(z);
2630
+ const i = z.radius;
2631
+ if (!(i > 0)) return t * this.minWeight;
2632
+ const n = z.center.distanceTo(we) - i, a = Math.max(n, i * 1e-3, 1e-6), r = It((i / a) ** this.falloff, this.minWeight, this.maxWeight);
2633
+ if (!Number.isFinite(r)) return t * this.minWeight;
2634
+ const l = Ce.intersectsSphere(z) ? 1 : this.offScreenWeight;
2635
+ return t * r * l;
2636
+ }
2637
+ /** Whether a weight moved enough to be worth a reallocation. */
2638
+ isSignificant(s, e) {
2639
+ return s === 0 || e === 0 ? s !== e : Math.abs(e - s) > this.weightDeadband * s;
2640
+ }
2641
+ }
2642
+ function It(o, s, e) {
2643
+ return Math.min(e, Math.max(s, o));
2644
+ }
2645
+ function D(o, s) {
2646
+ if (!Number.isFinite(o) || o < 0)
2647
+ throw new RangeError(`CameraBudgetGovernor ${s} must be a non-negative finite number.`);
2648
+ return o;
2649
+ }
2650
+ function U(o, s) {
2651
+ if (!Number.isFinite(o) || o <= 0)
2652
+ throw new RangeError(`CameraBudgetGovernor ${s} must be a positive finite number.`);
2653
+ return o;
2654
+ }
2655
+ const Rt = 16, Mt = 1;
2656
+ class Ut {
2657
+ constructor(s = {}) {
2658
+ u(this, "entries", /* @__PURE__ */ new Set());
2659
+ u(this, "maxGlobalInflight");
2660
+ u(this, "perMeshFloor");
2661
+ u(this, "inflightCount", 0);
2662
+ u(this, "disposed", !1);
2663
+ this.maxGlobalInflight = Math.max(
2664
+ 1,
2665
+ Math.floor(s.maxGlobalInflight ?? Rt)
2666
+ ), this.perMeshFloor = Math.max(0, Math.floor(s.perMeshFloor ?? Mt));
2667
+ }
2668
+ /** Total slots granted and not yet released. */
2669
+ get inflight() {
2670
+ return this.inflightCount;
2671
+ }
2672
+ /** Meshes currently registered. */
2673
+ get clientCount() {
2674
+ return this.entries.size;
2675
+ }
2676
+ register(s) {
2677
+ const e = { client: s, held: 0, demanding: !1, live: !0 };
2678
+ return this.entries.add(e), e;
2679
+ }
2680
+ /**
2681
+ * Drops a mesh. Slots it still holds are released here: a disposing mesh
2682
+ * aborts its fetches, and those aborts land after the handle is gone.
2683
+ */
2684
+ unregister(s) {
2685
+ const e = s;
2686
+ this.entries.delete(e) && (e.live = !1, e.held > 0 && (this.inflightCount -= e.held, e.held = 0, this.wake()));
2687
+ }
2688
+ /**
2689
+ * Grants a fetch slot, or denies and remembers the demand.
2690
+ *
2691
+ * A denial is not a failure and must not feed a mesh's retry backoff - the
2692
+ * mesh simply did not fetch this tick, and will be woken (or reschedule on
2693
+ * its own within the idle interval) to ask again.
2694
+ */
2695
+ tryAcquire(s, e) {
2696
+ const t = s;
2697
+ if (this.disposed || !t.live) return !1;
2698
+ const i = H(t.client.weight());
2699
+ return e === "sweep" && i <= 0 ? (t.demanding = !1, !1) : this.inflightCount >= this.maxGlobalInflight || t.held >= this.shareFor(t, i) ? (t.demanding = !0, !1) : (t.held++, t.demanding = !1, this.inflightCount++, !0);
2700
+ }
2701
+ /**
2702
+ * Returns a slot. Must be called exactly once for every granted acquire -
2703
+ * on success, on failure **and** on abort - or the pipe leaks capacity until
2704
+ * the scene is torn down.
2705
+ */
2706
+ release(s) {
2707
+ const e = s;
2708
+ e.held <= 0 || (e.held--, this.inflightCount--, this.wake());
2709
+ }
2710
+ /**
2711
+ * Re-examines weights after the camera moved. Call it once per frame, right
2712
+ * after the budget governor's own update.
2713
+ *
2714
+ * Meshes that just lost all weight shed their pre-warming sweeps, so a focus
2715
+ * change frees the pipe now rather than when a dozen far requests happen to
2716
+ * finish.
2717
+ */
2718
+ weightsChanged() {
2719
+ if (!this.disposed) {
2720
+ for (const s of this.entries)
2721
+ s.held > 0 && H(s.client.weight()) <= 0 && s.client.shedFetches("sweep");
2722
+ this.wake();
2723
+ }
2724
+ }
2725
+ /**
2726
+ * Releases every registration. Meshes are not disposed - the scheduler is
2727
+ * shared and does not own them, exactly as a shared `SplatPool` does not own
2728
+ * its meshes.
2729
+ */
2730
+ dispose() {
2731
+ this.disposed = !0;
2732
+ for (const s of this.entries)
2733
+ s.live = !1, s.held = 0, s.demanding = !1;
2734
+ this.entries.clear(), this.inflightCount = 0;
2735
+ }
2736
+ /**
2737
+ * The most slots this mesh may hold: its weight-proportional share of the
2738
+ * *whole* pipe, floored so it can always trickle and capped so its siblings
2739
+ * can always reach their own floors.
2740
+ *
2741
+ * This is a ceiling, not a reservation. The global cap does the real
2742
+ * limiting, so a heavy mesh can use capacity its idle siblings are leaving on
2743
+ * the table, while a far mesh's ceiling stays at its floor however early it
2744
+ * asks - which is what keeps the first mesh to reschedule from taking the
2745
+ * pipe and holding it. Dividing the *remainder* after every participant's
2746
+ * floor instead would invert the whole point on a large scene: thirteen
2747
+ * additional meshes and a main at floor 1 consume a 16-slot pipe entirely, leaving the
2748
+ * focused mesh a smaller share than the far ones it is competing with.
2749
+ *
2750
+ * The denominator counts every mesh that plausibly wants the pipe - all
2751
+ * registered meshes except those both weightless and idle, plus the caller.
2752
+ */
2753
+ shareFor(s, e) {
2754
+ let t = 0, i = 0;
2755
+ for (const l of this.entries) {
2756
+ const c = l === s ? e : H(l.client.weight());
2757
+ l !== s && c <= 0 && l.held === 0 && !l.demanding || (t++, i += c);
2758
+ }
2759
+ if (t <= 1) return this.maxGlobalInflight;
2760
+ const n = i > 0 ? this.maxGlobalInflight * e / i : this.maxGlobalInflight / t, a = Math.max(1, this.perMeshFloor, Math.round(n)), r = Math.max(1, this.perMeshFloor) * (t - 1);
2761
+ return Math.max(1, Math.min(a, this.maxGlobalInflight - r));
2762
+ }
2763
+ /** Pokes the heaviest mesh that was denied since its last grant. */
2764
+ wake() {
2765
+ if (this.disposed || this.inflightCount >= this.maxGlobalInflight) return;
2766
+ let s = null, e = -1 / 0;
2767
+ for (const t of this.entries) {
2768
+ if (!t.demanding) continue;
2769
+ const i = H(t.client.weight());
2770
+ i > e && (s = t, e = i);
2771
+ }
2772
+ s && (s.demanding = !1, s.client.onSlotAvailable());
2773
+ }
2774
+ }
2775
+ function H(o) {
2776
+ return Number.isFinite(o) && o > 0 ? o : 0;
2777
+ }
2778
+ const Ft = 32 * 1024 * 1024, Bt = 250, Pt = 0.15;
2779
+ class Ht {
2780
+ constructor(s) {
2781
+ u(this, "entries", /* @__PURE__ */ new Set());
2782
+ u(this, "perMeshFloorBytes");
2783
+ u(this, "minIntervalMs");
2784
+ u(this, "deadband");
2785
+ u(this, "totalBytesValue");
2786
+ u(this, "lastAllocationMs", Number.NEGATIVE_INFINITY);
2787
+ u(this, "disposed", !1);
2788
+ if (!Number.isFinite(s.totalBytes) || s.totalBytes <= 0)
2789
+ throw new RangeError("Chunk cache totalBytes must be a positive finite number.");
2790
+ this.totalBytesValue = Math.floor(s.totalBytes), this.perMeshFloorBytes = Math.max(
2791
+ 0,
2792
+ Math.floor(s.perMeshFloorBytes ?? Ft)
2793
+ ), this.minIntervalMs = Math.max(0, s.minIntervalMs ?? Bt), this.deadband = Math.max(0, s.deadband ?? Pt);
2794
+ }
2795
+ /** Bytes shared across every registered mesh. */
2796
+ get totalBytes() {
2797
+ return this.totalBytesValue;
2798
+ }
2799
+ /**
2800
+ * Resizes the scene envelope and re-splits immediately. For a host reacting
2801
+ * to a quality change or a device-memory signal.
2802
+ */
2803
+ setTotalBytes(s) {
2804
+ if (!Number.isFinite(s) || s <= 0)
2805
+ throw new RangeError("Chunk cache totalBytes must be a positive finite number.");
2806
+ const e = Math.floor(s);
2807
+ e !== this.totalBytesValue && (this.totalBytesValue = e, this.allocate());
2808
+ }
2809
+ /** Meshes currently registered. */
2810
+ get clientCount() {
2811
+ return this.entries.size;
2812
+ }
2813
+ /**
2814
+ * Adds a mesh and re-splits at once, so the caller can read its opening
2815
+ * allowance out of {@link allowanceFor} before posting worker init.
2816
+ */
2817
+ register(s) {
2818
+ const e = { client: s, allowance: 0, live: !0 };
2819
+ return this.entries.add(e), this.allocate({ force: !0, silentFor: e }), e;
2820
+ }
2821
+ /** Drops a mesh and hands its bytes back to the siblings. */
2822
+ unregister(s) {
2823
+ const e = s;
2824
+ this.entries.delete(e) && (e.live = !1, e.allowance = 0, this.allocate({ force: !0 }));
2825
+ }
2826
+ /** This mesh's current allowance in bytes, or 0 once unregistered. */
2827
+ allowanceFor(s) {
2828
+ const e = s;
2829
+ return e.live ? e.allowance : 0;
2830
+ }
2831
+ /**
2832
+ * Re-splits after the camera moved. Call it once per frame, right beside
2833
+ * {@link ChunkFetchScheduler.weightsChanged} - the two read the same weights,
2834
+ * so cache and network follow the same measure.
2835
+ *
2836
+ * Rate-limited and dead-banded; calling it every frame is the intended use.
2837
+ */
2838
+ weightsChanged(s = Date.now()) {
2839
+ this.disposed || s - this.lastAllocationMs < this.minIntervalMs || (this.lastAllocationMs = s, this.allocate());
2840
+ }
2841
+ /**
2842
+ * Releases every registration. Meshes are not disposed - the budget is shared
2843
+ * and does not own them, exactly as a shared `SplatPool` does not own its
2844
+ * meshes.
2845
+ */
2846
+ dispose() {
2847
+ this.disposed = !0;
2848
+ for (const s of this.entries)
2849
+ s.live = !1, s.allowance = 0;
2850
+ this.entries.clear();
2851
+ }
2852
+ /**
2853
+ * Cap-aware waterfill: weight-proportional shares, floored so every mesh can
2854
+ * hold its coarse base, clamped at each mesh's `ceilingBytes`, and the surplus
2855
+ * from clamped meshes redistributed over the rest until it settles.
2856
+ *
2857
+ * The invariant callers depend on is `Σ allowance <= totalBytes`. It holds in
2858
+ * every branch, including the degenerate one where the floors alone exceed
2859
+ * the envelope - there the floors are scaled down proportionally rather than
2860
+ * silently overcommitting, because a budget that can be exceeded by
2861
+ * registering more meshes is the bug this class exists to fix.
2862
+ */
2863
+ allocate(s = {}) {
2864
+ if (this.disposed) return;
2865
+ const e = [...this.entries];
2866
+ if (e.length === 0) return;
2867
+ const t = this.totalBytesValue, i = Math.min(this.perMeshFloorBytes, Math.floor(t / e.length)), n = /* @__PURE__ */ new Map(), a = new Set(e);
2868
+ let r = t;
2869
+ for (let l = 0; l <= e.length && a.size > 0; l++) {
2870
+ const c = Math.max(0, r - i * a.size);
2871
+ let h = 0;
2872
+ for (const d of a) h += Te(d.client.weight());
2873
+ const f = [];
2874
+ for (const d of a) {
2875
+ const g = Te(d.client.weight()), v = h > 0 ? c * g / h : c / a.size, m = kt(d.client.ceilingBytes), b = Math.floor(i + v);
2876
+ b >= m ? (n.set(d, m), f.push(d)) : n.set(d, b);
2877
+ }
2878
+ if (f.length === 0) break;
2879
+ for (const d of f)
2880
+ a.delete(d), r -= n.get(d) ?? 0;
2881
+ r = Math.max(0, r);
2882
+ }
2883
+ for (const l of e) {
2884
+ const c = n.get(l) ?? 0, h = l.allowance;
2885
+ l.allowance = c, l !== s.silentFor && (!s.force && !this.movedEnough(h, c) || h !== c && l.client.onAllowanceChanged(c));
2886
+ }
2887
+ }
2888
+ /** Suppresses reposts for changes too small to alter any mesh's behaviour. */
2889
+ movedEnough(s, e) {
2890
+ return s === e ? !1 : s === 0 || e === 0 ? !0 : Math.abs(e - s) / s >= this.deadband;
2891
+ }
2892
+ }
2893
+ function Te(o) {
2894
+ return Number.isFinite(o) && o > 0 ? o : 0;
2895
+ }
2896
+ function kt(o) {
2897
+ return Number.isFinite(o) && o > 0 ? Math.floor(o) : Number.MAX_SAFE_INTEGER;
2898
+ }
2899
+ export {
2900
+ Tt as BudgetGovernor,
2901
+ zt as CameraBudgetGovernor,
2902
+ Ht as ChunkCacheBudget,
2903
+ Ut as ChunkFetchScheduler,
2904
+ q as SplatLoadError,
2905
+ Y as StreamedSplatMesh,
2906
+ ot as createLocalDataset,
2907
+ nt as httpDatasetSource,
2908
+ N as isAbortError,
2909
+ Be as resolveCpuCacheBytes
2910
+ };
2911
+ //# sourceMappingURL=streaming.js.map