minecraft-renderer 0.1.79 → 0.1.80

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 (200) hide show
  1. package/README.md +29 -25
  2. package/dist/mesher.js +36 -36
  3. package/dist/mesher.js.map +4 -4
  4. package/dist/mesherWasm.js +217 -217
  5. package/dist/minecraft-renderer.js +27 -27
  6. package/dist/minecraft-renderer.js.meta.json +1 -1
  7. package/dist/threeWorker.js +133 -133
  8. package/package.json +5 -2
  9. package/src/bundler/bundlePrepare.ts +3 -8
  10. package/src/graphicsBackend/appViewer.ts +10 -7
  11. package/src/graphicsBackend/config.ts +1 -5
  12. package/src/graphicsBackend/playerState.ts +35 -34
  13. package/src/graphicsBackend/preloadWorkers.ts +43 -41
  14. package/src/graphicsBackend/rendererDefaultOptions.ts +68 -70
  15. package/src/graphicsBackend/rendererOptionsSync.ts +24 -50
  16. package/src/graphicsBackend/types.ts +2 -2
  17. package/src/index.ts +6 -27
  18. package/src/lib/DebugGui.ts +34 -31
  19. package/src/lib/animationController.ts +9 -9
  20. package/src/lib/bakeLegacyLight.ts +1 -6
  21. package/src/lib/bindAbortableListener.test.ts +32 -12
  22. package/src/lib/bindAbortableListener.ts +14 -6
  23. package/src/lib/blockEntityLightRegistry.ts +11 -20
  24. package/src/lib/blockEntityLighting.test.ts +1 -6
  25. package/src/lib/blockEntityLighting.ts +5 -5
  26. package/src/lib/buildSharedConfig.mjs +4 -4
  27. package/src/lib/buildWorkerMcDataIndexes.test.ts +4 -4
  28. package/src/lib/buildWorkerMcDataIndexes.ts +11 -19
  29. package/src/lib/cameraBobbing.ts +1 -1
  30. package/src/lib/canvas2DOverlay.example.ts +13 -33
  31. package/src/lib/canvas2DOverlay.quickstart.ts +24 -54
  32. package/src/lib/canvas2DOverlay.ts +28 -47
  33. package/src/lib/cleanupDecorator.ts +2 -2
  34. package/src/lib/createPlayerObject.test.ts +2 -7
  35. package/src/lib/createPlayerObject.ts +18 -25
  36. package/src/lib/frameTimingCollector.ts +2 -5
  37. package/src/lib/guiRenderer.ts +57 -42
  38. package/src/lib/items.ts +15 -9
  39. package/src/lib/mesherlogReader.ts +5 -6
  40. package/src/lib/simpleUtils.ts +5 -5
  41. package/src/lib/skyLight.ts +3 -3
  42. package/src/lib/smoothSwitcher.ts +11 -18
  43. package/src/lib/ui/newStats.ts +10 -12
  44. package/src/lib/utils/proxy.ts +7 -1
  45. package/src/lib/utils/skins.ts +4 -4
  46. package/src/lib/utils.ts +1 -1
  47. package/src/lib/workerMessageSanitize.ts +5 -12
  48. package/src/lib/workerProxy.ts +52 -42
  49. package/src/lib/workerSyncOps.test.ts +58 -41
  50. package/src/lib/worldrendererCommon.reconfigure.test.ts +24 -21
  51. package/src/lib/worldrendererCommon.removeColumn.test.ts +11 -13
  52. package/src/lib/worldrendererCommon.ts +338 -211
  53. package/src/mesher-legacy/mesher.ts +9 -4
  54. package/src/mesher-legacy/test/a.ts +1 -1
  55. package/src/mesher-legacy/test/mesherTester.ts +2 -2
  56. package/src/mesher-legacy/test/playground.ts +1 -1
  57. package/src/mesher-legacy/test/run/test-js.ts +1 -1
  58. package/src/mesher-legacy/test/snapshotUtils.ts +3 -8
  59. package/src/mesher-legacy/test/test-perf.ts +1 -1
  60. package/src/mesher-legacy/test/tests.test.ts +2 -2
  61. package/src/mesher-shared/blockEntityMetadata.test.ts +7 -5
  62. package/src/mesher-shared/blockEntityMetadata.ts +28 -19
  63. package/src/mesher-shared/blockPropertiesForMeshing.ts +2 -2
  64. package/src/mesher-shared/computeHeightmap.ts +2 -4
  65. package/src/mesher-shared/exportedGeometryTypes.ts +3 -3
  66. package/src/mesher-shared/faceOcclusion.ts +324 -0
  67. package/src/mesher-shared/models.ts +80 -131
  68. package/src/mesher-shared/modelsGeometryCommon.ts +30 -15
  69. package/src/mesher-shared/shared.ts +46 -46
  70. package/src/mesher-shared/standaloneRenderer.ts +40 -44
  71. package/src/mesher-shared/tests/faceOcclusion.test.ts +179 -0
  72. package/src/mesher-shared/tests/liquidQuadInvariant.test.ts +12 -8
  73. package/src/mesher-shared/vertexShading.ts +2 -11
  74. package/src/mesher-shared/world.ts +19 -19
  75. package/src/mesher-shared/worldLighting.test.ts +2 -2
  76. package/src/performanceMonitor/PerformanceMonitor.ts +4 -13
  77. package/src/performanceMonitor/formatPerformanceFactorsDebug.ts +2 -2
  78. package/src/performanceMonitor/index.ts +1 -5
  79. package/src/performanceMonitor/types.ts +1 -1
  80. package/src/playerState/playerState.ts +55 -50
  81. package/src/playerState/types.ts +2 -8
  82. package/src/playground/allEntitiesDebug.ts +19 -16
  83. package/src/playground/baseScene.ts +29 -32
  84. package/src/playground/mobileControls.tsx +133 -129
  85. package/src/playground/playground.html +76 -73
  86. package/src/playground/playground.ts +14 -1
  87. package/src/playground/playgroundUi.tsx +100 -74
  88. package/src/playground/reactUtils.ts +3 -7
  89. package/src/playground/scenes/entities.ts +10 -7
  90. package/src/playground/scenes/frequentUpdates.ts +5 -5
  91. package/src/playground/scenes/geometryExport.ts +6 -20
  92. package/src/playground/scenes/highPosEntityUpdates.ts +2 -10
  93. package/src/playground/scenes/index.ts +1 -1
  94. package/src/playground/scenes/lightingStarfield.ts +18 -12
  95. package/src/playground/scenes/main.ts +15 -11
  96. package/src/playground/scenes/partialBlockCulling.ts +31 -0
  97. package/src/playground/scenes/railsCobweb.ts +1 -1
  98. package/src/playground/scenes/rotationIssue.ts +1 -1
  99. package/src/playground/scenes/slabsOptimization.ts +1 -2
  100. package/src/playground/scenes/transparencyIssue.ts +1 -1
  101. package/src/playground/shared.ts +2 -2
  102. package/src/resourcesManager/resourcesManager.ts +29 -54
  103. package/src/resourcesManager/resourcesManager.worker.test.ts +6 -10
  104. package/src/shims/minecraftData.ts +70 -24
  105. package/src/sign-renderer/index.html +10 -12
  106. package/src/sign-renderer/index.ts +29 -29
  107. package/src/sign-renderer/playground.ts +22 -15
  108. package/src/sign-renderer/tests.test.ts +16 -13
  109. package/src/sign-renderer/vite.config.ts +2 -2
  110. package/src/three/appShared.ts +46 -25
  111. package/src/three/bannerRenderer.ts +33 -61
  112. package/src/three/cameraShake.ts +15 -6
  113. package/src/three/chunkMeshManager.ts +239 -282
  114. package/src/three/cinimaticScript.ts +26 -15
  115. package/src/three/cubeDrawSpans.ts +27 -37
  116. package/src/three/cubeMultiDraw.ts +9 -22
  117. package/src/three/documentRenderer.ts +15 -18
  118. package/src/three/entities.ts +175 -150
  119. package/src/three/entity/EntityMesh.ts +114 -108
  120. package/src/three/entity/animations.js +3 -3
  121. package/src/three/entity/armorModels.ts +15 -15
  122. package/src/three/entity/gltfAnimationUtils.ts +2 -5
  123. package/src/three/fireworks.ts +34 -32
  124. package/src/three/fireworksRenderer.ts +32 -29
  125. package/src/three/globalBlockBuffer.ts +167 -100
  126. package/src/three/globalLegacyBuffer.ts +257 -166
  127. package/src/three/graphicsBackendBase.ts +19 -25
  128. package/src/three/graphicsBackendOffThread.ts +12 -15
  129. package/src/three/hand.ts +21 -21
  130. package/src/three/handLegacy.ts +3 -13
  131. package/src/three/holdingBlock.ts +116 -79
  132. package/src/three/holdingBlockFactory.ts +1 -4
  133. package/src/three/holdingBlockItemIdentity.test.ts +25 -17
  134. package/src/three/holdingBlockItemIdentity.ts +9 -6
  135. package/src/three/holdingBlockLegacy.ts +113 -132
  136. package/src/three/itemMesh.ts +20 -29
  137. package/src/three/legacyMultiDraw.ts +10 -16
  138. package/src/three/legacySectionCull.ts +8 -14
  139. package/src/three/menuBackground/activeView.ts +1 -4
  140. package/src/three/menuBackground/assetUrl.ts +1 -1
  141. package/src/three/menuBackground/classic.ts +4 -12
  142. package/src/three/menuBackground/gpuPreference.ts +1 -3
  143. package/src/three/menuBackground/index.ts +2 -11
  144. package/src/three/menuBackground/renderer.ts +1 -5
  145. package/src/three/menuBackground/types.ts +1 -4
  146. package/src/three/menuBackground/v2.ts +196 -96
  147. package/src/three/menuBackground/v2Meta.ts +3 -3
  148. package/src/three/menuBackground/worldBlocks.ts +37 -35
  149. package/src/three/modules/blockBreakParticles.ts +59 -28
  150. package/src/three/modules/cameraBobbing.ts +3 -6
  151. package/src/three/modules/index.ts +1 -1
  152. package/src/three/modules/rain.ts +10 -23
  153. package/src/three/modules/sciFiWorldReveal.ts +11 -25
  154. package/src/three/modules/starfield.ts +7 -13
  155. package/src/three/renderSlot.ts +28 -16
  156. package/src/three/rendererModuleSystem.ts +1 -3
  157. package/src/three/sceneOrigin.ts +96 -33
  158. package/src/three/sectionRaycastAabb.ts +30 -18
  159. package/src/three/shaderCubeMesh.ts +6 -17
  160. package/src/three/shaders/cubeBlockShader.ts +85 -96
  161. package/src/three/shaders/legacyBlockShader.ts +21 -43
  162. package/src/three/shaders/textureIndexMapping.ts +89 -95
  163. package/src/three/shaders/tintPalette.ts +156 -170
  164. package/src/three/signTextureCache.ts +9 -12
  165. package/src/three/skyboxRenderer.ts +62 -70
  166. package/src/three/tests/chunkMeshManagerLegacy.test.ts +34 -34
  167. package/src/three/tests/cubeDrawSpans.test.ts +55 -25
  168. package/src/three/tests/globalBlockBuffer.test.ts +217 -0
  169. package/src/three/tests/globalLegacyBuffer.test.ts +300 -41
  170. package/src/three/tests/legacyMultiDraw.test.ts +8 -29
  171. package/src/three/tests/pr3PerFrameCpuCuts.test.ts +106 -29
  172. package/src/three/tests/signTextureCache.test.ts +4 -4
  173. package/src/three/threeJsMedia.ts +55 -72
  174. package/src/three/threeJsParticles.ts +32 -41
  175. package/src/three/threeJsSound.ts +3 -3
  176. package/src/three/threeJsUtils.ts +3 -3
  177. package/src/three/threeWorker.ts +2 -2
  178. package/src/three/waypointSprite.ts +55 -82
  179. package/src/three/waypoints.ts +20 -23
  180. package/src/three/world/cursorBlock.ts +25 -12
  181. package/src/three/world/vr.ts +11 -17
  182. package/src/three/worldGeometryExport.ts +15 -34
  183. package/src/three/worldRendererThree.ts +117 -129
  184. package/src/wasm-mesher/bridge/convertChunk.ts +3 -9
  185. package/src/wasm-mesher/bridge/render-from-wasm.ts +204 -229
  186. package/src/wasm-mesher/bridge/shaderCubeBridge.ts +26 -67
  187. package/src/wasm-mesher/tests/cullingRegression.test.ts +307 -0
  188. package/src/wasm-mesher/tests/heightmapParity.test.ts +8 -18
  189. package/src/wasm-mesher/tests/mesherWasmConversionCache.test.ts +2 -2
  190. package/src/wasm-mesher/tests/sectionRaycastAabb.test.ts +17 -4
  191. package/src/wasm-mesher/tests/shaderCubeInstances.test.ts +103 -141
  192. package/src/wasm-mesher/tests/splitColumnWasmOutput.test.ts +14 -22
  193. package/src/wasm-mesher/worker/mesherWasm.ts +150 -141
  194. package/src/wasm-mesher/worker/mesherWasmConversionCache.ts +2 -8
  195. package/src/wasm-mesher/worker/mesherWasmLightDirty.test.ts +1 -3
  196. package/src/wasm-mesher/worker/mesherWasmLightDirty.ts +1 -5
  197. package/src/wasm-mesher/worker/mesherWasmRequestTracker.ts +42 -42
  198. package/src/worldView/worldView.spiral.test.ts +1 -1
  199. package/src/worldView/worldView.ts +34 -39
  200. package/src/worldView/worldViewWorkerBridge.test.ts +4 -4
@@ -7,7 +7,7 @@ import {
7
7
  logLegacyMultiDrawTierOnce,
8
8
  type LegacyDrawSpan,
9
9
  type LegacyMultiDrawCaps,
10
- type LegacyMultiDrawScratch,
10
+ type LegacyMultiDrawScratch
11
11
  } from './legacyMultiDraw'
12
12
  import { computeCameraRelativeUniforms, type RenderOrigin } from './shaders/legacyBlockShader'
13
13
 
@@ -22,24 +22,83 @@ const DEFAULT_GROWTH_INCREMENT_QUADS = 128_000
22
22
  const MAX_UPLOAD_QUADS_PER_FRAME = 5_000
23
23
  const FRAGMENTATION_THRESHOLD = 0.25
24
24
 
25
- type PendingMove = { key: string, oldStart: number, newStart: number, count: number }
25
+ type PendingMove = { key: string; oldStart: number; newStart: number; count: number }
26
+ type PendingReplace = { oldStart: number; oldCount: number }
26
27
 
27
28
  /** CPU bytes per allocated quad slot (all legacy vertex/index attrs). */
28
- export const LEGACY_BYTES_PER_QUAD =
29
- VERTS_PER_QUAD * (FLOATS_PER_VERT * 3 + FLOATS_PER_LIGHT_VERT * 2 + FLOATS_PER_UV_VERT) * 4
30
- + INDICES_PER_QUAD * 4
29
+ export const LEGACY_BYTES_PER_QUAD = VERTS_PER_QUAD * (FLOATS_PER_VERT * 3 + FLOATS_PER_LIGHT_VERT * 2 + FLOATS_PER_UV_VERT) * 4 + INDICES_PER_QUAD * 4
31
30
 
32
31
  export const FULL_DRAW_VISIBLE_FRACTION = 0.75
33
- export const SPAN_GAP_TOLERANCE_QUADS = 256
32
+ /** Initial multi_draw scratch size; arrays auto-grow — not a draw-call cap. */
34
33
  export const MAX_OPAQUE_SPANS = 64
35
34
 
35
+ /** Dev assert: every quad in draw spans must lie in a live section's drawable range. */
36
+ export function assertDrawSpansWithinLiveRanges(
37
+ spans: ReadonlyArray<{ start: number; count: number }>,
38
+ liveRanges: ReadonlyArray<{ start: number; count: number }>,
39
+ bufferName: string
40
+ ): void {
41
+ for (const span of spans) {
42
+ for (let q = span.start; q < span.start + span.count; q++) {
43
+ let inLive = false
44
+ for (const live of liveRanges) {
45
+ if (q >= live.start && q < live.start + live.count) {
46
+ inLive = true
47
+ break
48
+ }
49
+ }
50
+ if (!inLive) {
51
+ console.error('[GlobalLegacyBuffer] draw span covers non-live quad', {
52
+ buffer: bufferName,
53
+ quad: q,
54
+ span,
55
+ liveRanges
56
+ })
57
+ }
58
+ }
59
+ }
60
+ }
61
+
62
+ export type DirtyRange = { start: number; end: number }
63
+
64
+ /** Split draw spans to exclude quad/face ranges still in pendingRanges (not yet on GPU). */
65
+ export function carveSpansAroundPendingRanges(
66
+ spans: Array<{ start: number; count: number }>,
67
+ pendingRanges: ReadonlyArray<DirtyRange>
68
+ ): Array<{ start: number; count: number }> {
69
+ if (pendingRanges.length === 0) return spans
70
+ const out: Array<{ start: number; count: number }> = []
71
+ for (const span of spans) {
72
+ let segments: Array<{ start: number; count: number }> = [span]
73
+ for (const pr of pendingRanges) {
74
+ const next: Array<{ start: number; count: number }> = []
75
+ for (const seg of segments) {
76
+ const segEnd = seg.start + seg.count - 1
77
+ if (pr.end < seg.start || pr.start > segEnd) {
78
+ next.push(seg)
79
+ continue
80
+ }
81
+ if (pr.start > seg.start) {
82
+ next.push({ start: seg.start, count: pr.start - seg.start })
83
+ }
84
+ if (pr.end < segEnd) {
85
+ next.push({ start: pr.end + 1, count: segEnd - pr.end })
86
+ }
87
+ }
88
+ segments = next
89
+ }
90
+ out.push(...segments)
91
+ }
92
+ return out.filter(s => s.count > 0)
93
+ }
94
+
36
95
  export type GlobalLegacyBufferOptions = {
37
96
  name?: string
38
97
  initialCapacityQuads?: number
39
98
  growthIncrementQuads?: number
40
99
  }
41
100
 
42
- export type VisibleSectionSpan = { key: string, distSq: number }
101
+ export type VisibleSectionSpan = { key: string; distSq: number }
43
102
 
44
103
  export type LegacySectionGeometry = {
45
104
  positions: Float32Array
@@ -75,24 +134,22 @@ export class GlobalLegacyBuffer {
75
134
  private uvs: Float32Array
76
135
  private aOrigin: Float32Array
77
136
  private indices: Uint32Array
78
- private readonly sectionSlots = new Map<string, { start: number, count: number }>()
79
- private freeList: Array<{ start: number, count: number }> = []
137
+ private readonly sectionSlots = new Map<string, { start: number; count: number }>()
138
+ private freeList: Array<{ start: number; count: number }> = []
80
139
  private highWatermark = 0
81
- private pendingRanges: Array<{ start: number, end: number }> = []
82
- private readonly _spanScratch: Array<{ start: number, count: number }> = []
140
+ private pendingRanges: Array<{ start: number; end: number }> = []
141
+ private readonly _spanScratch: Array<{ start: number; count: number }> = []
83
142
  private renderOrigin: RenderOrigin = { x: 0, y: 0, z: 0 }
84
143
  private layoutVersion = 0
85
144
  private pendingMove: PendingMove | null = null
145
+ private readonly pendingReplace = new Map<string, PendingReplace>()
146
+ private uploadEpoch = 0
86
147
  private visibleIndexSpans: LegacyDrawSpan[] = []
87
148
  private readonly _drawScratch: LegacyMultiDrawScratch = createLegacyMultiDrawScratch()
88
149
  private multiDrawCaps: LegacyMultiDrawCaps | null = null
89
150
  private debugOverlay = false
90
151
 
91
- constructor (
92
- material: THREE.ShaderMaterial,
93
- scene: THREE.Object3D,
94
- opts?: GlobalLegacyBufferOptions,
95
- ) {
152
+ constructor(material: THREE.ShaderMaterial, scene: THREE.Object3D, opts?: GlobalLegacyBufferOptions) {
96
153
  this.material = material
97
154
  this.growthIncrementQuads = opts?.growthIncrementQuads ?? DEFAULT_GROWTH_INCREMENT_QUADS
98
155
  this.capacityQuads = opts?.initialCapacityQuads ?? DEFAULT_INITIAL_CAPACITY_QUADS
@@ -146,7 +203,7 @@ export class GlobalLegacyBuffer {
146
203
  }
147
204
  }
148
205
 
149
- setDebugOverlay (enabled: boolean): void {
206
+ setDebugOverlay(enabled: boolean): void {
150
207
  this.debugOverlay = enabled
151
208
  }
152
209
 
@@ -156,19 +213,19 @@ export class GlobalLegacyBuffer {
156
213
  * program/VAO/ELEMENT_ARRAY_BUFFER stay bound while three draws ~nothing.
157
214
  * Draws one triangle from index 0 (usually harmless; if quad 0 is culled, one stray tri).
158
215
  */
159
- suppressThreeDraw (): void {
216
+ suppressThreeDraw(): void {
160
217
  this.mesh.geometry.setDrawRange(0, 3)
161
218
  }
162
219
 
163
- setVisibleIndexSpans (spans: LegacyDrawSpan[]): void {
220
+ setVisibleIndexSpans(spans: LegacyDrawSpan[]): void {
164
221
  this.visibleIndexSpans = spans
165
222
  }
166
223
 
167
- getVisibleIndexSpans (): readonly LegacyDrawSpan[] {
224
+ getVisibleIndexSpans(): readonly LegacyDrawSpan[] {
168
225
  return this.visibleIndexSpans
169
226
  }
170
227
 
171
- private syncDefaultDrawGroups (): void {
228
+ private syncDefaultDrawGroups(): void {
172
229
  const geometry = this.mesh.geometry
173
230
  geometry.clearGroups()
174
231
  const indexCount = this.highWatermark * INDICES_PER_QUAD
@@ -178,13 +235,7 @@ export class GlobalLegacyBuffer {
178
235
  geometry.setDrawRange(0, indexCount)
179
236
  }
180
237
 
181
- addSection (
182
- sectionKey: string,
183
- geo: LegacySectionGeometry,
184
- sx: number,
185
- sy: number,
186
- sz: number,
187
- ): boolean {
238
+ addSection(sectionKey: string, geo: LegacySectionGeometry, sx: number, sy: number, sz: number): boolean {
188
239
  const vertCount = geo.positions.length / FLOATS_PER_VERT
189
240
  const quadCount = vertCount / VERTS_PER_QUAD
190
241
  if (vertCount === 0 || quadCount * VERTS_PER_QUAD !== vertCount) {
@@ -195,8 +246,23 @@ export class GlobalLegacyBuffer {
195
246
  return false
196
247
  }
197
248
 
198
- if (this.sectionSlots.has(sectionKey)) {
199
- this.removeSection(sectionKey)
249
+ const isRemesh = this.sectionSlots.has(sectionKey)
250
+ let previousSlot: { start: number; count: number } | undefined
251
+ if (isRemesh) {
252
+ const currentSlot = this.sectionSlots.get(sectionKey)!
253
+ const inflightReplace = this.pendingReplace.get(sectionKey)
254
+ const inflightMove = this.pendingMove?.key === sectionKey ? this.pendingMove : undefined
255
+ if (inflightReplace) {
256
+ this.zeroAndFreeSlot(currentSlot.start, currentSlot.count)
257
+ previousSlot = { start: inflightReplace.oldStart, count: inflightReplace.oldCount }
258
+ this.pendingReplace.delete(sectionKey)
259
+ } else if (inflightMove) {
260
+ this.zeroAndFreeSlot(currentSlot.start, currentSlot.count)
261
+ previousSlot = { start: inflightMove.oldStart, count: inflightMove.count }
262
+ this.pendingMove = null
263
+ } else {
264
+ previousSlot = currentSlot
265
+ }
200
266
  }
201
267
 
202
268
  if (quadCount > this.capacityQuads) {
@@ -240,29 +306,65 @@ export class GlobalLegacyBuffer {
240
306
  }
241
307
 
242
308
  this.sectionSlots.set(sectionKey, slot)
309
+ if (isRemesh && previousSlot) {
310
+ this.pendingReplace.set(sectionKey, { oldStart: previousSlot.start, oldCount: previousSlot.count })
311
+ }
243
312
  this.markDirty(slot.start, slot.start + quadCount - 1)
244
313
  this.syncDefaultDrawGroups()
245
314
  this.layoutVersion++
246
315
  return true
247
316
  }
248
317
 
249
- getLayoutVersion (): number {
318
+ getLayoutVersion(): number {
250
319
  return this.layoutVersion
251
320
  }
252
321
 
253
- getSectionDrawStart (sectionKey: string): number | undefined {
322
+ getUploadEpoch(): number {
323
+ return this.uploadEpoch
324
+ }
325
+
326
+ hasPendingReplace(): boolean {
327
+ return this.pendingReplace.size > 0
328
+ }
329
+
330
+ canUseFullDrawShortcut(): boolean {
331
+ return this.pendingRanges.length === 0 && this.interiorFreeQuads() === 0 && this.pendingMove === null && this.pendingReplace.size === 0
332
+ }
333
+
334
+ isRangeFullyUploaded(start: number, end: number): boolean {
335
+ return this.rangeFullyUploaded(start, end)
336
+ }
337
+
338
+ getPendingDirtyRanges(): ReadonlyArray<DirtyRange> {
339
+ return this.pendingRanges
340
+ }
341
+
342
+ getSectionDrawStart(sectionKey: string): number | undefined {
254
343
  const slot = this.sectionSlots.get(sectionKey)
255
344
  if (!slot) return undefined
256
345
  if (this.pendingMove?.key === sectionKey) return this.pendingMove.oldStart
346
+ const replace = this.pendingReplace.get(sectionKey)
347
+ if (replace) return replace.oldStart
348
+ if (!this.rangeFullyUploaded(slot.start, slot.start + slot.count - 1)) return undefined
257
349
  return slot.start
258
350
  }
259
351
 
260
- getPendingMove (): PendingMove | null {
352
+ getSectionDrawCount(sectionKey: string): number | undefined {
353
+ const slot = this.sectionSlots.get(sectionKey)
354
+ if (!slot) return undefined
355
+ if (this.pendingMove?.key === sectionKey) return this.pendingMove.count
356
+ const replace = this.pendingReplace.get(sectionKey)
357
+ if (replace) return replace.oldCount
358
+ if (!this.rangeFullyUploaded(slot.start, slot.start + slot.count - 1)) return undefined
359
+ return slot.count
360
+ }
361
+
362
+ getPendingMove(): PendingMove | null {
261
363
  return this.pendingMove
262
364
  }
263
365
 
264
366
  /** One interior-hole move per frame when fragmentation exceeds threshold; deferred shrink. */
265
- compactStep (): void {
367
+ compactStep(): void {
266
368
  if (this.pendingMove) {
267
369
  const { newStart, count } = this.pendingMove
268
370
  if (this.rangeFullyUploaded(newStart, newStart + count - 1)) {
@@ -271,6 +373,14 @@ export class GlobalLegacyBuffer {
271
373
  return
272
374
  }
273
375
 
376
+ for (const key of [...this.pendingReplace.keys()]) {
377
+ const slot = this.sectionSlots.get(key)
378
+ if (!slot) continue
379
+ if (this.rangeFullyUploaded(slot.start, slot.start + slot.count - 1)) {
380
+ this.finalizePendingReplace(key)
381
+ }
382
+ }
383
+
274
384
  if (this.highWatermark === 0) return
275
385
  const interiorFree = this.interiorFreeQuads()
276
386
  if (interiorFree / this.highWatermark <= FRAGMENTATION_THRESHOLD) return
@@ -292,7 +402,7 @@ export class GlobalLegacyBuffer {
292
402
  this.layoutVersion++
293
403
  }
294
404
 
295
- updateDrawSpans (visible: VisibleSectionSpan[], mode: 'opaque' | 'sortedBlend'): void {
405
+ updateDrawSpans(visible: VisibleSectionSpan[], mode: 'opaque' | 'sortedBlend'): void {
296
406
  this.visibleIndexSpans = []
297
407
 
298
408
  if (this.highWatermark === 0) {
@@ -305,10 +415,11 @@ export class GlobalLegacyBuffer {
305
415
 
306
416
  for (const entry of visible) {
307
417
  const drawStart = this.getSectionDrawStart(entry.key)
418
+ const drawCount = this.getSectionDrawCount(entry.key)
308
419
  const slot = this.sectionSlots.get(entry.key)
309
- if (drawStart === undefined || !slot) continue
310
- spans.push({ start: drawStart, count: slot.count })
311
- visibleQuadCount += slot.count
420
+ if (drawStart === undefined || drawCount === undefined || !slot) continue
421
+ spans.push({ start: drawStart, count: drawCount })
422
+ visibleQuadCount += drawCount
312
423
  }
313
424
 
314
425
  if (spans.length === 0) {
@@ -318,45 +429,47 @@ export class GlobalLegacyBuffer {
318
429
  const pushIndexSpan = (quadStart: number, quadCount: number): void => {
319
430
  this.visibleIndexSpans.push({
320
431
  indexStart: quadStart * INDICES_PER_QUAD,
321
- indexCount: quadCount * INDICES_PER_QUAD,
432
+ indexCount: quadCount * INDICES_PER_QUAD
322
433
  })
323
434
  }
324
435
 
325
436
  if (mode === 'opaque') {
326
- if (visibleQuadCount >= this.highWatermark * FULL_DRAW_VISIBLE_FRACTION) {
327
- this.visibleIndexSpans.push({
328
- indexStart: 0,
329
- indexCount: this.highWatermark * INDICES_PER_QUAD,
330
- })
331
- return
437
+ let finalQuads: Array<{ start: number; count: number }>
438
+ const liveDrawRanges = spans.map(s => ({ ...s }))
439
+ const usedFullDraw = this.canUseFullDrawShortcut() && visibleQuadCount >= this.highWatermark * FULL_DRAW_VISIBLE_FRACTION
440
+ if (usedFullDraw) {
441
+ finalQuads = [{ start: 0, count: this.highWatermark }]
442
+ } else {
443
+ spans.sort((a, b) => a.start - b.start)
444
+ this.mergeOpaqueSpans(spans)
445
+ finalQuads = spans
332
446
  }
333
-
334
- spans.sort((a, b) => a.start - b.start)
335
- this.mergeOpaqueSpans(spans)
336
- this.capOpaqueSpans(spans)
337
-
338
- for (const span of spans) {
447
+ finalQuads = carveSpansAroundPendingRanges(finalQuads, this.pendingRanges)
448
+ if (!usedFullDraw) {
449
+ assertDrawSpansWithinLiveRanges(finalQuads, liveDrawRanges, this.mesh.name)
450
+ }
451
+ for (const span of finalQuads) {
339
452
  pushIndexSpan(span.start, span.count)
340
453
  }
341
454
  } else {
342
455
  visible.sort((a, b) => b.distSq - a.distSq)
343
456
  for (const entry of visible) {
344
457
  const drawStart = this.getSectionDrawStart(entry.key)
345
- const slot = this.sectionSlots.get(entry.key)
346
- if (drawStart === undefined || !slot) continue
347
- pushIndexSpan(drawStart, slot.count)
458
+ const drawCount = this.getSectionDrawCount(entry.key)
459
+ if (drawStart === undefined || drawCount === undefined) continue
460
+ pushIndexSpan(drawStart, drawCount)
348
461
  }
349
462
  }
350
463
  }
351
464
 
352
- private mergeOpaqueSpans (spans: Array<{ start: number, count: number }>): void {
465
+ /** Merge only physically adjacent section slots (gap === 0). Never bridge interior holes. */
466
+ private mergeOpaqueSpans(spans: Array<{ start: number; count: number }>): void {
353
467
  if (spans.length < 2) return
354
468
  let i = 0
355
469
  while (i < spans.length - 1) {
356
470
  const cur = spans[i]!
357
471
  const next = spans[i + 1]!
358
- const gap = next.start - (cur.start + cur.count)
359
- if (gap <= SPAN_GAP_TOLERANCE_QUADS) {
472
+ if (cur.start + cur.count === next.start) {
360
473
  cur.count = next.start + next.count - cur.start
361
474
  spans.splice(i + 1, 1)
362
475
  } else {
@@ -365,40 +478,22 @@ export class GlobalLegacyBuffer {
365
478
  }
366
479
  }
367
480
 
368
- private capOpaqueSpans (spans: Array<{ start: number, count: number }>): void {
369
- while (spans.length > MAX_OPAQUE_SPANS) {
370
- let bestIdx = 0
371
- let bestGap = Infinity
372
- for (let i = 0; i < spans.length - 1; i++) {
373
- const gap = spans[i + 1]!.start - (spans[i]!.start + spans[i]!.count)
374
- if (gap < bestGap) {
375
- bestGap = gap
376
- bestIdx = i
377
- }
378
- }
379
- const cur = spans[bestIdx]!
380
- const next = spans[bestIdx + 1]!
381
- cur.count = next.start + next.count - cur.start
382
- spans.splice(bestIdx + 1, 1)
383
- }
384
- }
385
-
386
- hasSection (sectionKey: string): boolean {
481
+ hasSection(sectionKey: string): boolean {
387
482
  return this.sectionSlots.has(sectionKey)
388
483
  }
389
484
 
390
- getSectionSlot (sectionKey: string): { start: number, count: number } | undefined {
485
+ getSectionSlot(sectionKey: string): { start: number; count: number } | undefined {
391
486
  return this.sectionSlots.get(sectionKey)
392
487
  }
393
488
 
394
- takeSectionData (sectionKey: string): LegacySectionGeometryData | undefined {
489
+ takeSectionData(sectionKey: string): LegacySectionGeometryData | undefined {
395
490
  const data = this.getSectionGeometryData(sectionKey)
396
491
  if (!data) return undefined
397
492
  this.removeSection(sectionKey)
398
493
  return data
399
494
  }
400
495
 
401
- getSectionGeometryData (sectionKey: string): LegacySectionGeometryData | undefined {
496
+ getSectionGeometryData(sectionKey: string): LegacySectionGeometryData | undefined {
402
497
  const slot = this.sectionSlots.get(sectionKey)
403
498
  if (!slot) return undefined
404
499
 
@@ -429,19 +524,19 @@ export class GlobalLegacyBuffer {
429
524
  return { positions, colors, skyLights, blockLights, uvs, indices, sx, sy, sz }
430
525
  }
431
526
 
432
- removeSection (sectionKey: string): void {
527
+ removeSection(sectionKey: string): void {
433
528
  const slot = this.sectionSlots.get(sectionKey)
434
529
  if (!slot) return
435
530
 
531
+ if (this.pendingReplace.has(sectionKey)) {
532
+ const pr = this.pendingReplace.get(sectionKey)!
533
+ this.zeroAndFreeSlot(pr.oldStart, pr.oldCount)
534
+ this.pendingReplace.delete(sectionKey)
535
+ }
536
+
436
537
  if (this.pendingMove?.key === sectionKey) {
437
538
  const { oldStart, count } = this.pendingMove
438
- const oldIndexBase = oldStart * INDICES_PER_QUAD
439
- const oldIndexLen = count * INDICES_PER_QUAD
440
- for (let i = 0; i < oldIndexLen; i++) {
441
- this.indices[oldIndexBase + i] = 0
442
- }
443
- this.markDirty(oldStart, oldStart + count - 1)
444
- this.insertFreeSlot({ start: oldStart, count })
539
+ this.zeroAndFreeSlot(oldStart, count)
445
540
  this.pendingMove = null
446
541
  }
447
542
 
@@ -459,11 +554,11 @@ export class GlobalLegacyBuffer {
459
554
  this.layoutVersion++
460
555
  }
461
556
 
462
- hasPendingUploads (): boolean {
557
+ hasPendingUploads(): boolean {
463
558
  return this.pendingRanges.length > 0
464
559
  }
465
560
 
466
- uploadDirtyRange (): void {
561
+ uploadDirtyRange(): void {
467
562
  const r = this.pendingRanges[0]
468
563
  if (!r) return
469
564
 
@@ -510,15 +605,19 @@ export class GlobalLegacyBuffer {
510
605
  indexAttr.addUpdateRange(indexOffset, indexCount)
511
606
  indexAttr.needsUpdate = true
512
607
 
513
- if (quadOffset + quadCount > r.end) this.pendingRanges.shift()
514
- else r.start = quadOffset + quadCount
608
+ if (quadOffset + quadCount > r.end) {
609
+ this.pendingRanges.shift()
610
+ } else {
611
+ r.start = quadOffset + quadCount
612
+ }
613
+ this.uploadEpoch++
515
614
  }
516
615
 
517
- setRenderOrigin (renderOrigin: RenderOrigin): void {
616
+ setRenderOrigin(renderOrigin: RenderOrigin): void {
518
617
  this.renderOrigin = { ...renderOrigin }
519
618
  }
520
619
 
521
- rebase (delta: RenderOrigin): void {
620
+ rebase(delta: RenderOrigin): void {
522
621
  if (this.highWatermark === 0) return
523
622
  for (const slot of this.sectionSlots.values()) {
524
623
  const dstVertBase = slot.start * VERTS_PER_QUAD
@@ -537,7 +636,7 @@ export class GlobalLegacyBuffer {
537
636
  this.renderOrigin.z += delta.z
538
637
  }
539
638
 
540
- setCameraOrigin (x: number, y: number, z: number): void {
639
+ setCameraOrigin(x: number, y: number, z: number): void {
541
640
  const { originDelta, cameraOriginFrac } = computeCameraRelativeUniforms(this.renderOrigin, x, y, z)
542
641
  const u = this.material.uniforms.u_originDelta
543
642
  if (u?.value?.set) u.value.set(originDelta.x, originDelta.y, originDelta.z)
@@ -545,19 +644,11 @@ export class GlobalLegacyBuffer {
545
644
  if (uf?.value?.set) uf.value.set(cameraOriginFrac.x, cameraOriginFrac.y, cameraOriginFrac.z)
546
645
  }
547
646
 
548
- raycastSections (
549
- raycaster: THREE.Raycaster,
550
- sectionKeys: Iterable<string>,
551
- out: THREE.Intersection[],
552
- ): THREE.Intersection[] {
647
+ raycastSections(raycaster: THREE.Raycaster, sectionKeys: Iterable<string>, out: THREE.Intersection[]): THREE.Intersection[] {
553
648
  const ray = raycaster.ray
554
649
  const closest = raycaster.near
555
650
  const far = raycaster.far
556
- _raycastOrigin.copy(ray.origin).sub(_raycastRenderOrigin.set(
557
- this.renderOrigin.x,
558
- this.renderOrigin.y,
559
- this.renderOrigin.z,
560
- ))
651
+ _raycastOrigin.copy(ray.origin).sub(_raycastRenderOrigin.set(this.renderOrigin.x, this.renderOrigin.y, this.renderOrigin.z))
561
652
  _raycastRay.origin.copy(_raycastOrigin)
562
653
  _raycastRay.direction.copy(ray.direction)
563
654
 
@@ -576,19 +667,14 @@ export class GlobalLegacyBuffer {
576
667
  const i2 = this.indices[dstIndexBase + i + 2]!
577
668
  if (i0 === i1 && i1 === i2) continue
578
669
 
579
- const hit = intersectTriangle(
580
- _raycastRay,
581
- this.positions, this.aOrigin, dstFloatBase,
582
- i0, i1, i2,
583
- closest, far,
584
- )
670
+ const hit = intersectTriangle(_raycastRay, this.positions, this.aOrigin, dstFloatBase, i0, i1, i2, closest, far)
585
671
  if (hit !== null) {
586
672
  out.push({
587
673
  distance: hit,
588
674
  point: ray.at(hit, new THREE.Vector3()),
589
675
  object: this.mesh,
590
676
  face: null,
591
- faceIndex: Math.floor(i / 3),
677
+ faceIndex: Math.floor(i / 3)
592
678
  })
593
679
  }
594
680
  }
@@ -598,51 +684,53 @@ export class GlobalLegacyBuffer {
598
684
  return out
599
685
  }
600
686
 
601
- getHighWatermark (): number {
687
+ getHighWatermark(): number {
602
688
  return this.highWatermark
603
689
  }
604
690
 
605
- getCapacityQuads (): number {
691
+ getCapacityQuads(): number {
606
692
  return this.capacityQuads
607
693
  }
608
694
 
609
- getSectionCount (): number {
695
+ getSectionCount(): number {
610
696
  return this.sectionSlots.size
611
697
  }
612
698
 
613
- getMemoryBytes (): number {
699
+ getMemoryBytes(): number {
614
700
  return this.capacityQuads * LEGACY_BYTES_PER_QUAD
615
701
  }
616
702
 
617
- getUsedMemoryBytes (): number {
703
+ getUsedMemoryBytes(): number {
618
704
  return this.highWatermark * LEGACY_BYTES_PER_QUAD
619
705
  }
620
706
 
621
- reset (): void {
707
+ reset(): void {
622
708
  this.sectionSlots.clear()
623
709
  this.freeList.length = 0
624
710
  this.highWatermark = 0
625
711
  this.pendingRanges.length = 0
626
712
  this.pendingMove = null
713
+ this.pendingReplace.clear()
714
+ this.uploadEpoch = 0
627
715
  this.visibleIndexSpans = []
628
716
  this.syncDefaultDrawGroups()
629
717
  }
630
718
 
631
- dispose (): void {
719
+ dispose(): void {
632
720
  this.mesh.parent?.remove(this.mesh)
633
721
  this.mesh.geometry.dispose()
634
722
  this.reset()
635
723
  }
636
724
 
637
- private markDirty (start: number, end: number): void {
725
+ private markDirty(start: number, end: number): void {
638
726
  this.pendingRanges.push({ start, end })
639
727
  this.pendingRanges.sort((a, b) => a.start - b.start)
640
728
  this.mergePendingRanges()
641
729
  }
642
730
 
643
- private mergePendingRanges (): void {
731
+ private mergePendingRanges(): void {
644
732
  if (this.pendingRanges.length < 2) return
645
- const merged: Array<{ start: number, end: number }> = []
733
+ const merged: Array<{ start: number; end: number }> = []
646
734
  let cur = this.pendingRanges[0]!
647
735
  for (let i = 1; i < this.pendingRanges.length; i++) {
648
736
  const next = this.pendingRanges[i]!
@@ -657,7 +745,7 @@ export class GlobalLegacyBuffer {
657
745
  this.pendingRanges = merged
658
746
  }
659
747
 
660
- private takeFreeSlot (count: number): { start: number, count: number } | undefined {
748
+ private takeFreeSlot(count: number): { start: number; count: number } | undefined {
661
749
  for (let i = 0; i < this.freeList.length; i++) {
662
750
  const slot = this.freeList[i]!
663
751
  if (slot.count >= count) {
@@ -671,15 +759,15 @@ export class GlobalLegacyBuffer {
671
759
  return undefined
672
760
  }
673
761
 
674
- private insertFreeSlot (slot: { start: number, count: number }): void {
762
+ private insertFreeSlot(slot: { start: number; count: number }): void {
675
763
  this.freeList.push(slot)
676
764
  this.freeList.sort((a, b) => a.start - b.start)
677
765
  this.mergeFreeList()
678
766
  }
679
767
 
680
- private mergeFreeList (): void {
768
+ private mergeFreeList(): void {
681
769
  if (this.freeList.length < 2) return
682
- const merged: Array<{ start: number, count: number }> = []
770
+ const merged: Array<{ start: number; count: number }> = []
683
771
  let cur = this.freeList[0]!
684
772
  for (let i = 1; i < this.freeList.length; i++) {
685
773
  const next = this.freeList[i]!
@@ -694,7 +782,7 @@ export class GlobalLegacyBuffer {
694
782
  this.freeList = merged
695
783
  }
696
784
 
697
- private shrinkHighWatermark (): void {
785
+ private shrinkHighWatermark(): void {
698
786
  while (this.highWatermark > 0) {
699
787
  const tail = this.highWatermark - 1
700
788
  const free = this.freeList.find(s => s.start <= tail && s.start + s.count > tail)
@@ -705,7 +793,7 @@ export class GlobalLegacyBuffer {
705
793
  }
706
794
  }
707
795
 
708
- private interiorFreeQuads (): number {
796
+ private interiorFreeQuads(): number {
709
797
  let total = 0
710
798
  for (const slot of this.freeList) {
711
799
  if (slot.start < this.highWatermark) total += slot.count
@@ -713,8 +801,8 @@ export class GlobalLegacyBuffer {
713
801
  return total
714
802
  }
715
803
 
716
- private findMovableSection (maxCount: number): { key: string, start: number, count: number } | undefined {
717
- const sections: Array<{ key: string, start: number, count: number }> = []
804
+ private findMovableSection(maxCount: number): { key: string; start: number; count: number } | undefined {
805
+ const sections: Array<{ key: string; start: number; count: number }> = []
718
806
  for (const [key, slot] of this.sectionSlots) {
719
807
  sections.push({ key, start: slot.start, count: slot.count })
720
808
  }
@@ -722,7 +810,7 @@ export class GlobalLegacyBuffer {
722
810
 
723
811
  sections.sort((a, b) => {
724
812
  if (b.start !== a.start) return b.start - a.start
725
- return (b.start + b.count) - (a.start + a.count)
813
+ return b.start + b.count - (a.start + a.count)
726
814
  })
727
815
 
728
816
  const tailmost = sections[0]!
@@ -730,9 +818,7 @@ export class GlobalLegacyBuffer {
730
818
  return tailmost
731
819
  }
732
820
 
733
- const candidates = sections
734
- .filter(s => s.count <= maxCount)
735
- .sort((a, b) => b.count - a.count)
821
+ const candidates = sections.filter(s => s.count <= maxCount).sort((a, b) => b.count - a.count)
736
822
 
737
823
  for (const s of candidates) {
738
824
  if (this.findLowestInteriorHole(s.start, s.count)) return s
@@ -740,10 +826,7 @@ export class GlobalLegacyBuffer {
740
826
  return undefined
741
827
  }
742
828
 
743
- private findLowestInteriorHole (
744
- sectionStart: number,
745
- count: number,
746
- ): { start: number, count: number, index: number } | undefined {
829
+ private findLowestInteriorHole(sectionStart: number, count: number): { start: number; count: number; index: number } | undefined {
747
830
  for (let i = 0; i < this.freeList.length; i++) {
748
831
  const slot = this.freeList[i]!
749
832
  if (slot.start < sectionStart && slot.count >= count) {
@@ -753,7 +836,7 @@ export class GlobalLegacyBuffer {
753
836
  return undefined
754
837
  }
755
838
 
756
- private reserveFreeSlotAt (index: number, count: number): { start: number, count: number } {
839
+ private reserveFreeSlotAt(index: number, count: number): { start: number; count: number } {
757
840
  const slot = this.freeList[index]!
758
841
  this.freeList.splice(index, 1)
759
842
  if (slot.count === count) return { start: slot.start, count }
@@ -762,7 +845,7 @@ export class GlobalLegacyBuffer {
762
845
  return used
763
846
  }
764
847
 
765
- private copySectionRange (oldStart: number, newStart: number, quadCount: number): void {
848
+ private copySectionRange(oldStart: number, newStart: number, quadCount: number): void {
766
849
  const oldVertBase = oldStart * VERTS_PER_QUAD
767
850
  const newVertBase = newStart * VERTS_PER_QUAD
768
851
  const vertCount = quadCount * VERTS_PER_QUAD
@@ -794,35 +877,53 @@ export class GlobalLegacyBuffer {
794
877
  }
795
878
  }
796
879
 
797
- private rangeFullyUploaded (start: number, end: number): boolean {
880
+ private rangeFullyUploaded(start: number, end: number): boolean {
798
881
  for (const r of this.pendingRanges) {
799
882
  if (r.start <= end && r.end >= start) return false
800
883
  }
801
884
  return true
802
885
  }
803
886
 
804
- private finalizePendingMove (): void {
805
- const move = this.pendingMove
806
- if (!move) return
807
-
808
- const { oldStart, count } = move
809
- const oldIndexBase = oldStart * INDICES_PER_QUAD
887
+ private zeroAndFreeSlot(start: number, count: number): void {
888
+ const oldIndexBase = start * INDICES_PER_QUAD
810
889
  const oldIndexLen = count * INDICES_PER_QUAD
811
890
  for (let i = 0; i < oldIndexLen; i++) {
812
891
  this.indices[oldIndexBase + i] = 0
813
892
  }
814
- this.insertFreeSlot({ start: oldStart, count })
893
+ this.markDirty(start, start + count - 1)
894
+ this.insertFreeSlot({ start, count })
895
+ }
896
+
897
+ private finalizePendingReplace(key: string): void {
898
+ const pr = this.pendingReplace.get(key)
899
+ if (!pr) return
900
+
901
+ this.zeroAndFreeSlot(pr.oldStart, pr.oldCount)
902
+ this.pendingReplace.delete(key)
903
+ this.shrinkHighWatermark()
904
+ this.syncDefaultDrawGroups()
905
+ this.layoutVersion++
906
+ this.uploadEpoch++
907
+ }
908
+
909
+ private finalizePendingMove(): void {
910
+ const move = this.pendingMove
911
+ if (!move) return
912
+
913
+ const { oldStart, count } = move
914
+ this.zeroAndFreeSlot(oldStart, count)
815
915
  this.shrinkHighWatermark()
816
- if (oldStart < this.highWatermark) {
817
- this.markDirty(oldStart, oldStart + count - 1)
818
- }
819
916
  this.syncDefaultDrawGroups()
820
917
  this.pendingMove = null
821
918
  this.layoutVersion++
919
+ this.uploadEpoch++
822
920
  }
823
921
 
824
- private growCapacity (minQuads: number): void {
922
+ private growCapacity(minQuads: number): void {
825
923
  if (this.pendingMove) this.finalizePendingMove()
924
+ for (const key of [...this.pendingReplace.keys()]) {
925
+ this.finalizePendingReplace(key)
926
+ }
826
927
 
827
928
  let newCap = this.capacityQuads
828
929
  while (newCap < minQuads) newCap += this.growthIncrementQuads
@@ -890,22 +991,12 @@ const _raycastOrigin = new THREE.Vector3()
890
991
  const _raycastRenderOrigin = new THREE.Vector3()
891
992
  const _raycastRay = new THREE.Ray()
892
993
 
893
- function readWorldVertex (
894
- positions: Float32Array,
895
- aOrigin: Float32Array,
896
- floatBase: number,
897
- vertIndex: number,
898
- target: THREE.Vector3,
899
- ): void {
994
+ function readWorldVertex(positions: Float32Array, aOrigin: Float32Array, floatBase: number, vertIndex: number, target: THREE.Vector3): void {
900
995
  const f = floatBase + vertIndex * FLOATS_PER_VERT
901
- target.set(
902
- aOrigin[f]! + positions[f]!,
903
- aOrigin[f + 1]! + positions[f + 1]!,
904
- aOrigin[f + 2]! + positions[f + 2]!,
905
- )
996
+ target.set(aOrigin[f]! + positions[f]!, aOrigin[f + 1]! + positions[f + 1]!, aOrigin[f + 2]! + positions[f + 2]!)
906
997
  }
907
998
 
908
- function intersectTriangle (
999
+ function intersectTriangle(
909
1000
  ray: THREE.Ray,
910
1001
  positions: Float32Array,
911
1002
  aOrigin: Float32Array,
@@ -914,7 +1005,7 @@ function intersectTriangle (
914
1005
  i1: number,
915
1006
  i2: number,
916
1007
  near: number,
917
- far: number,
1008
+ far: number
918
1009
  ): number | null {
919
1010
  readWorldVertex(positions, aOrigin, floatBase, i0, _vA)
920
1011
  readWorldVertex(positions, aOrigin, floatBase, i1, _vB)
@@ -935,7 +1026,7 @@ function intersectTriangle (
935
1026
  return t
936
1027
  }
937
1028
 
938
- function pointInTriangle (p: THREE.Vector3, a: THREE.Vector3, b: THREE.Vector3, c: THREE.Vector3): boolean {
1029
+ function pointInTriangle(p: THREE.Vector3, a: THREE.Vector3, b: THREE.Vector3, c: THREE.Vector3): boolean {
939
1030
  _edge1.subVectors(b, a)
940
1031
  _edge2.subVectors(c, a)
941
1032
  const n = _normal.crossVectors(_edge1, _edge2).normalize()