minecraft-renderer 0.1.78 → 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 +32 -32
  6. package/dist/minecraft-renderer.js.meta.json +1 -1
  7. package/dist/threeWorker.js +415 -415
  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 +329 -301
  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 +174 -99
  126. package/src/three/globalLegacyBuffer.ts +465 -136
  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 +93 -0
  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 +133 -25
  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 +460 -58
  170. package/src/three/tests/legacyMultiDraw.test.ts +88 -0
  171. package/src/three/tests/pr3PerFrameCpuCuts.test.ts +370 -0
  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 +131 -133
  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
@@ -1,5 +1,14 @@
1
1
  //@ts-nocheck
2
2
  import * as THREE from 'three'
3
+ import {
4
+ createLegacyMultiDrawScratch,
5
+ detectLegacyMultiDrawCaps,
6
+ drawLegacySpans,
7
+ logLegacyMultiDrawTierOnce,
8
+ type LegacyDrawSpan,
9
+ type LegacyMultiDrawCaps,
10
+ type LegacyMultiDrawScratch
11
+ } from './legacyMultiDraw'
3
12
  import { computeCameraRelativeUniforms, type RenderOrigin } from './shaders/legacyBlockShader'
4
13
 
5
14
  const VERTS_PER_QUAD = 4
@@ -11,23 +20,85 @@ const FLOATS_PER_LIGHT_VERT = 1
11
20
  const DEFAULT_INITIAL_CAPACITY_QUADS = 128_000
12
21
  const DEFAULT_GROWTH_INCREMENT_QUADS = 128_000
13
22
  const MAX_UPLOAD_QUADS_PER_FRAME = 5_000
23
+ const FRAGMENTATION_THRESHOLD = 0.25
24
+
25
+ type PendingMove = { key: string; oldStart: number; newStart: number; count: number }
26
+ type PendingReplace = { oldStart: number; oldCount: number }
14
27
 
15
28
  /** CPU bytes per allocated quad slot (all legacy vertex/index attrs). */
16
- export const LEGACY_BYTES_PER_QUAD =
17
- VERTS_PER_QUAD * (FLOATS_PER_VERT * 3 + FLOATS_PER_LIGHT_VERT * 2 + FLOATS_PER_UV_VERT) * 4
18
- + 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
19
30
 
20
31
  export const FULL_DRAW_VISIBLE_FRACTION = 0.75
21
- export const SPAN_GAP_TOLERANCE_QUADS = 256
32
+ /** Initial multi_draw scratch size; arrays auto-grow — not a draw-call cap. */
22
33
  export const MAX_OPAQUE_SPANS = 64
23
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
+
24
95
  export type GlobalLegacyBufferOptions = {
25
96
  name?: string
26
97
  initialCapacityQuads?: number
27
98
  growthIncrementQuads?: number
28
99
  }
29
100
 
30
- export type VisibleSectionSpan = { key: string, distSq: number }
101
+ export type VisibleSectionSpan = { key: string; distSq: number }
31
102
 
32
103
  export type LegacySectionGeometry = {
33
104
  positions: Float32Array
@@ -44,6 +115,8 @@ export type LegacySectionGeometryData = LegacySectionGeometry & {
44
115
  sz: number
45
116
  }
46
117
 
118
+ export type { LegacyDrawSpan } from './legacyMultiDraw'
119
+
47
120
  /**
48
121
  * Single GPU mesh for legacy quads (opaque+cutout or transparent blend).
49
122
  * Camera-relative via per-vertex a_origin (relative to render origin) + u_originDelta uniforms.
@@ -61,18 +134,22 @@ export class GlobalLegacyBuffer {
61
134
  private uvs: Float32Array
62
135
  private aOrigin: Float32Array
63
136
  private indices: Uint32Array
64
- private readonly sectionSlots = new Map<string, { start: number, count: number }>()
65
- 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 }> = []
66
139
  private highWatermark = 0
67
- private pendingRanges: Array<{ start: number, end: number }> = []
68
- 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 }> = []
69
142
  private renderOrigin: RenderOrigin = { x: 0, y: 0, z: 0 }
70
-
71
- constructor (
72
- material: THREE.ShaderMaterial,
73
- scene: THREE.Object3D,
74
- opts?: GlobalLegacyBufferOptions,
75
- ) {
143
+ private layoutVersion = 0
144
+ private pendingMove: PendingMove | null = null
145
+ private readonly pendingReplace = new Map<string, PendingReplace>()
146
+ private uploadEpoch = 0
147
+ private visibleIndexSpans: LegacyDrawSpan[] = []
148
+ private readonly _drawScratch: LegacyMultiDrawScratch = createLegacyMultiDrawScratch()
149
+ private multiDrawCaps: LegacyMultiDrawCaps | null = null
150
+ private debugOverlay = false
151
+
152
+ constructor(material: THREE.ShaderMaterial, scene: THREE.Object3D, opts?: GlobalLegacyBufferOptions) {
76
153
  this.material = material
77
154
  this.growthIncrementQuads = opts?.growthIncrementQuads ?? DEFAULT_GROWTH_INCREMENT_QUADS
78
155
  this.capacityQuads = opts?.initialCapacityQuads ?? DEFAULT_INITIAL_CAPACITY_QUADS
@@ -114,9 +191,41 @@ export class GlobalLegacyBuffer {
114
191
  this.mesh.position.set(0, 0, 0)
115
192
  scene.add(this.mesh)
116
193
  this.syncDefaultDrawGroups()
194
+
195
+ this.mesh.onAfterRender = (renderer, _scene, _camera, _geometry, material) => {
196
+ if (this.visibleIndexSpans.length === 0) return
197
+ const gl = renderer.getContext() as WebGL2RenderingContext
198
+ if (!this.multiDrawCaps) {
199
+ this.multiDrawCaps = detectLegacyMultiDrawCaps(gl)
200
+ logLegacyMultiDrawTierOnce(this.multiDrawCaps.tier, this.debugOverlay)
201
+ }
202
+ drawLegacySpans(gl, this.multiDrawCaps, this.visibleIndexSpans, this._drawScratch)
203
+ }
204
+ }
205
+
206
+ setDebugOverlay(enabled: boolean): void {
207
+ this.debugOverlay = enabled
208
+ }
209
+
210
+ /**
211
+ * Suppress three's full-buffer indexed draw; onAfterRender issues visible spans only.
212
+ * setDrawRange(0,0) skips bindingStates.setup — use a minimal non-zero range so
213
+ * program/VAO/ELEMENT_ARRAY_BUFFER stay bound while three draws ~nothing.
214
+ * Draws one triangle from index 0 (usually harmless; if quad 0 is culled, one stray tri).
215
+ */
216
+ suppressThreeDraw(): void {
217
+ this.mesh.geometry.setDrawRange(0, 3)
218
+ }
219
+
220
+ setVisibleIndexSpans(spans: LegacyDrawSpan[]): void {
221
+ this.visibleIndexSpans = spans
222
+ }
223
+
224
+ getVisibleIndexSpans(): readonly LegacyDrawSpan[] {
225
+ return this.visibleIndexSpans
117
226
  }
118
227
 
119
- private syncDefaultDrawGroups (): void {
228
+ private syncDefaultDrawGroups(): void {
120
229
  const geometry = this.mesh.geometry
121
230
  geometry.clearGroups()
122
231
  const indexCount = this.highWatermark * INDICES_PER_QUAD
@@ -126,13 +235,7 @@ export class GlobalLegacyBuffer {
126
235
  geometry.setDrawRange(0, indexCount)
127
236
  }
128
237
 
129
- addSection (
130
- sectionKey: string,
131
- geo: LegacySectionGeometry,
132
- sx: number,
133
- sy: number,
134
- sz: number,
135
- ): boolean {
238
+ addSection(sectionKey: string, geo: LegacySectionGeometry, sx: number, sy: number, sz: number): boolean {
136
239
  const vertCount = geo.positions.length / FLOATS_PER_VERT
137
240
  const quadCount = vertCount / VERTS_PER_QUAD
138
241
  if (vertCount === 0 || quadCount * VERTS_PER_QUAD !== vertCount) {
@@ -143,8 +246,23 @@ export class GlobalLegacyBuffer {
143
246
  return false
144
247
  }
145
248
 
146
- if (this.sectionSlots.has(sectionKey)) {
147
- 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
+ }
148
266
  }
149
267
 
150
268
  if (quadCount > this.capacityQuads) {
@@ -171,14 +289,14 @@ export class GlobalLegacyBuffer {
171
289
  this.uvs.set(geo.uvs, dstUvBase)
172
290
 
173
291
  const originOff = dstFloatBase
174
- const rx = this.renderOrigin.x
175
- const ry = this.renderOrigin.y
176
- const rz = this.renderOrigin.z
292
+ const ox = sx - this.renderOrigin.x
293
+ const oy = sy - this.renderOrigin.y
294
+ const oz = sz - this.renderOrigin.z
177
295
  for (let v = 0; v < vertCount; v++) {
178
296
  const o = originOff + v * FLOATS_PER_VERT
179
- this.aOrigin[o] = sx - rx
180
- this.aOrigin[o + 1] = sy - ry
181
- this.aOrigin[o + 2] = sz - rz
297
+ this.aOrigin[o] = ox
298
+ this.aOrigin[o + 1] = oy
299
+ this.aOrigin[o + 2] = oz
182
300
  }
183
301
 
184
302
  const dstIndexBase = slot.start * INDICES_PER_QUAD
@@ -188,17 +306,106 @@ export class GlobalLegacyBuffer {
188
306
  }
189
307
 
190
308
  this.sectionSlots.set(sectionKey, slot)
309
+ if (isRemesh && previousSlot) {
310
+ this.pendingReplace.set(sectionKey, { oldStart: previousSlot.start, oldCount: previousSlot.count })
311
+ }
191
312
  this.markDirty(slot.start, slot.start + quadCount - 1)
192
313
  this.syncDefaultDrawGroups()
314
+ this.layoutVersion++
193
315
  return true
194
316
  }
195
317
 
196
- updateDrawSpans (visible: VisibleSectionSpan[], mode: 'opaque' | 'sortedBlend'): void {
197
- const geometry = this.mesh.geometry
198
- geometry.clearGroups()
318
+ getLayoutVersion(): number {
319
+ return this.layoutVersion
320
+ }
321
+
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 {
343
+ const slot = this.sectionSlots.get(sectionKey)
344
+ if (!slot) return undefined
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
349
+ return slot.start
350
+ }
351
+
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 {
363
+ return this.pendingMove
364
+ }
365
+
366
+ /** One interior-hole move per frame when fragmentation exceeds threshold; deferred shrink. */
367
+ compactStep(): void {
368
+ if (this.pendingMove) {
369
+ const { newStart, count } = this.pendingMove
370
+ if (this.rangeFullyUploaded(newStart, newStart + count - 1)) {
371
+ this.finalizePendingMove()
372
+ }
373
+ return
374
+ }
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
+
384
+ if (this.highWatermark === 0) return
385
+ const interiorFree = this.interiorFreeQuads()
386
+ if (interiorFree / this.highWatermark <= FRAGMENTATION_THRESHOLD) return
387
+
388
+ const section = this.findMovableSection(MAX_UPLOAD_QUADS_PER_FRAME)
389
+ if (!section) return
390
+
391
+ const hole = this.findLowestInteriorHole(section.start, section.count)
392
+ if (!hole) return
393
+
394
+ const reserved = this.reserveFreeSlotAt(hole.index, section.count)
395
+ const oldStart = section.start
396
+ const newStart = reserved.start
397
+
398
+ this.copySectionRange(oldStart, newStart, section.count)
399
+ this.sectionSlots.set(section.key, { start: newStart, count: section.count })
400
+ this.markDirty(newStart, newStart + section.count - 1)
401
+ this.pendingMove = { key: section.key, oldStart, newStart, count: section.count }
402
+ this.layoutVersion++
403
+ }
404
+
405
+ updateDrawSpans(visible: VisibleSectionSpan[], mode: 'opaque' | 'sortedBlend'): void {
406
+ this.visibleIndexSpans = []
199
407
 
200
408
  if (this.highWatermark === 0) {
201
- geometry.setDrawRange(0, 0)
202
409
  return
203
410
  }
204
411
 
@@ -207,51 +414,62 @@ export class GlobalLegacyBuffer {
207
414
  let visibleQuadCount = 0
208
415
 
209
416
  for (const entry of visible) {
417
+ const drawStart = this.getSectionDrawStart(entry.key)
418
+ const drawCount = this.getSectionDrawCount(entry.key)
210
419
  const slot = this.sectionSlots.get(entry.key)
211
- if (!slot) continue
212
- spans.push({ start: slot.start, count: slot.count })
213
- visibleQuadCount += slot.count
420
+ if (drawStart === undefined || drawCount === undefined || !slot) continue
421
+ spans.push({ start: drawStart, count: drawCount })
422
+ visibleQuadCount += drawCount
214
423
  }
215
424
 
216
425
  if (spans.length === 0) {
217
- geometry.setDrawRange(0, 0)
218
426
  return
219
427
  }
220
428
 
429
+ const pushIndexSpan = (quadStart: number, quadCount: number): void => {
430
+ this.visibleIndexSpans.push({
431
+ indexStart: quadStart * INDICES_PER_QUAD,
432
+ indexCount: quadCount * INDICES_PER_QUAD
433
+ })
434
+ }
435
+
221
436
  if (mode === 'opaque') {
222
- if (visibleQuadCount >= this.highWatermark * FULL_DRAW_VISIBLE_FRACTION) {
223
- geometry.addGroup(0, this.highWatermark * INDICES_PER_QUAD, 0)
224
- geometry.setDrawRange(0, this.highWatermark * INDICES_PER_QUAD)
225
- 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
226
446
  }
227
-
228
- spans.sort((a, b) => a.start - b.start)
229
- this.mergeOpaqueSpans(spans)
230
- this.capOpaqueSpans(spans)
231
-
232
- for (const span of spans) {
233
- geometry.addGroup(span.start * INDICES_PER_QUAD, span.count * INDICES_PER_QUAD, 0)
447
+ finalQuads = carveSpansAroundPendingRanges(finalQuads, this.pendingRanges)
448
+ if (!usedFullDraw) {
449
+ assertDrawSpansWithinLiveRanges(finalQuads, liveDrawRanges, this.mesh.name)
450
+ }
451
+ for (const span of finalQuads) {
452
+ pushIndexSpan(span.start, span.count)
234
453
  }
235
454
  } else {
236
455
  visible.sort((a, b) => b.distSq - a.distSq)
237
456
  for (const entry of visible) {
238
- const slot = this.sectionSlots.get(entry.key)
239
- if (!slot) continue
240
- geometry.addGroup(slot.start * INDICES_PER_QUAD, slot.count * INDICES_PER_QUAD, 0)
457
+ const drawStart = this.getSectionDrawStart(entry.key)
458
+ const drawCount = this.getSectionDrawCount(entry.key)
459
+ if (drawStart === undefined || drawCount === undefined) continue
460
+ pushIndexSpan(drawStart, drawCount)
241
461
  }
242
462
  }
243
-
244
- geometry.setDrawRange(0, this.highWatermark * INDICES_PER_QUAD)
245
463
  }
246
464
 
247
- 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 {
248
467
  if (spans.length < 2) return
249
468
  let i = 0
250
469
  while (i < spans.length - 1) {
251
470
  const cur = spans[i]!
252
471
  const next = spans[i + 1]!
253
- const gap = next.start - (cur.start + cur.count)
254
- if (gap <= SPAN_GAP_TOLERANCE_QUADS) {
472
+ if (cur.start + cur.count === next.start) {
255
473
  cur.count = next.start + next.count - cur.start
256
474
  spans.splice(i + 1, 1)
257
475
  } else {
@@ -260,40 +478,22 @@ export class GlobalLegacyBuffer {
260
478
  }
261
479
  }
262
480
 
263
- private capOpaqueSpans (spans: Array<{ start: number, count: number }>): void {
264
- while (spans.length > MAX_OPAQUE_SPANS) {
265
- let bestIdx = 0
266
- let bestGap = Infinity
267
- for (let i = 0; i < spans.length - 1; i++) {
268
- const gap = spans[i + 1]!.start - (spans[i]!.start + spans[i]!.count)
269
- if (gap < bestGap) {
270
- bestGap = gap
271
- bestIdx = i
272
- }
273
- }
274
- const cur = spans[bestIdx]!
275
- const next = spans[bestIdx + 1]!
276
- cur.count = next.start + next.count - cur.start
277
- spans.splice(bestIdx + 1, 1)
278
- }
279
- }
280
-
281
- hasSection (sectionKey: string): boolean {
481
+ hasSection(sectionKey: string): boolean {
282
482
  return this.sectionSlots.has(sectionKey)
283
483
  }
284
484
 
285
- getSectionSlot (sectionKey: string): { start: number, count: number } | undefined {
485
+ getSectionSlot(sectionKey: string): { start: number; count: number } | undefined {
286
486
  return this.sectionSlots.get(sectionKey)
287
487
  }
288
488
 
289
- takeSectionData (sectionKey: string): LegacySectionGeometryData | undefined {
489
+ takeSectionData(sectionKey: string): LegacySectionGeometryData | undefined {
290
490
  const data = this.getSectionGeometryData(sectionKey)
291
491
  if (!data) return undefined
292
492
  this.removeSection(sectionKey)
293
493
  return data
294
494
  }
295
495
 
296
- getSectionGeometryData (sectionKey: string): LegacySectionGeometryData | undefined {
496
+ getSectionGeometryData(sectionKey: string): LegacySectionGeometryData | undefined {
297
497
  const slot = this.sectionSlots.get(sectionKey)
298
498
  if (!slot) return undefined
299
499
 
@@ -324,10 +524,22 @@ export class GlobalLegacyBuffer {
324
524
  return { positions, colors, skyLights, blockLights, uvs, indices, sx, sy, sz }
325
525
  }
326
526
 
327
- removeSection (sectionKey: string): void {
527
+ removeSection(sectionKey: string): void {
328
528
  const slot = this.sectionSlots.get(sectionKey)
329
529
  if (!slot) return
330
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
+
537
+ if (this.pendingMove?.key === sectionKey) {
538
+ const { oldStart, count } = this.pendingMove
539
+ this.zeroAndFreeSlot(oldStart, count)
540
+ this.pendingMove = null
541
+ }
542
+
331
543
  const dstIndexBase = slot.start * INDICES_PER_QUAD
332
544
  const indexLen = slot.count * INDICES_PER_QUAD
333
545
  for (let i = 0; i < indexLen; i++) {
@@ -339,13 +551,14 @@ export class GlobalLegacyBuffer {
339
551
  this.insertFreeSlot(slot)
340
552
  this.shrinkHighWatermark()
341
553
  this.syncDefaultDrawGroups()
554
+ this.layoutVersion++
342
555
  }
343
556
 
344
- hasPendingUploads (): boolean {
557
+ hasPendingUploads(): boolean {
345
558
  return this.pendingRanges.length > 0
346
559
  }
347
560
 
348
- uploadDirtyRange (): void {
561
+ uploadDirtyRange(): void {
349
562
  const r = this.pendingRanges[0]
350
563
  if (!r) return
351
564
 
@@ -392,15 +605,19 @@ export class GlobalLegacyBuffer {
392
605
  indexAttr.addUpdateRange(indexOffset, indexCount)
393
606
  indexAttr.needsUpdate = true
394
607
 
395
- if (quadOffset + quadCount > r.end) this.pendingRanges.shift()
396
- 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++
397
614
  }
398
615
 
399
- setRenderOrigin (renderOrigin: RenderOrigin): void {
616
+ setRenderOrigin(renderOrigin: RenderOrigin): void {
400
617
  this.renderOrigin = { ...renderOrigin }
401
618
  }
402
619
 
403
- rebase (delta: RenderOrigin): void {
620
+ rebase(delta: RenderOrigin): void {
404
621
  if (this.highWatermark === 0) return
405
622
  for (const slot of this.sectionSlots.values()) {
406
623
  const dstVertBase = slot.start * VERTS_PER_QUAD
@@ -419,7 +636,7 @@ export class GlobalLegacyBuffer {
419
636
  this.renderOrigin.z += delta.z
420
637
  }
421
638
 
422
- setCameraOrigin (x: number, y: number, z: number): void {
639
+ setCameraOrigin(x: number, y: number, z: number): void {
423
640
  const { originDelta, cameraOriginFrac } = computeCameraRelativeUniforms(this.renderOrigin, x, y, z)
424
641
  const u = this.material.uniforms.u_originDelta
425
642
  if (u?.value?.set) u.value.set(originDelta.x, originDelta.y, originDelta.z)
@@ -427,19 +644,11 @@ export class GlobalLegacyBuffer {
427
644
  if (uf?.value?.set) uf.value.set(cameraOriginFrac.x, cameraOriginFrac.y, cameraOriginFrac.z)
428
645
  }
429
646
 
430
- raycastSections (
431
- raycaster: THREE.Raycaster,
432
- sectionKeys: Iterable<string>,
433
- out: THREE.Intersection[],
434
- ): THREE.Intersection[] {
647
+ raycastSections(raycaster: THREE.Raycaster, sectionKeys: Iterable<string>, out: THREE.Intersection[]): THREE.Intersection[] {
435
648
  const ray = raycaster.ray
436
649
  const closest = raycaster.near
437
650
  const far = raycaster.far
438
- _raycastOrigin.copy(ray.origin).sub(_raycastRenderOrigin.set(
439
- this.renderOrigin.x,
440
- this.renderOrigin.y,
441
- this.renderOrigin.z,
442
- ))
651
+ _raycastOrigin.copy(ray.origin).sub(_raycastRenderOrigin.set(this.renderOrigin.x, this.renderOrigin.y, this.renderOrigin.z))
443
652
  _raycastRay.origin.copy(_raycastOrigin)
444
653
  _raycastRay.direction.copy(ray.direction)
445
654
 
@@ -458,19 +667,14 @@ export class GlobalLegacyBuffer {
458
667
  const i2 = this.indices[dstIndexBase + i + 2]!
459
668
  if (i0 === i1 && i1 === i2) continue
460
669
 
461
- const hit = intersectTriangle(
462
- _raycastRay,
463
- this.positions, this.aOrigin, dstFloatBase,
464
- i0, i1, i2,
465
- closest, far,
466
- )
670
+ const hit = intersectTriangle(_raycastRay, this.positions, this.aOrigin, dstFloatBase, i0, i1, i2, closest, far)
467
671
  if (hit !== null) {
468
672
  out.push({
469
673
  distance: hit,
470
674
  point: ray.at(hit, new THREE.Vector3()),
471
675
  object: this.mesh,
472
676
  face: null,
473
- faceIndex: Math.floor(i / 3),
677
+ faceIndex: Math.floor(i / 3)
474
678
  })
475
679
  }
476
680
  }
@@ -480,49 +684,53 @@ export class GlobalLegacyBuffer {
480
684
  return out
481
685
  }
482
686
 
483
- getHighWatermark (): number {
687
+ getHighWatermark(): number {
484
688
  return this.highWatermark
485
689
  }
486
690
 
487
- getCapacityQuads (): number {
691
+ getCapacityQuads(): number {
488
692
  return this.capacityQuads
489
693
  }
490
694
 
491
- getSectionCount (): number {
695
+ getSectionCount(): number {
492
696
  return this.sectionSlots.size
493
697
  }
494
698
 
495
- getMemoryBytes (): number {
699
+ getMemoryBytes(): number {
496
700
  return this.capacityQuads * LEGACY_BYTES_PER_QUAD
497
701
  }
498
702
 
499
- getUsedMemoryBytes (): number {
703
+ getUsedMemoryBytes(): number {
500
704
  return this.highWatermark * LEGACY_BYTES_PER_QUAD
501
705
  }
502
706
 
503
- reset (): void {
707
+ reset(): void {
504
708
  this.sectionSlots.clear()
505
709
  this.freeList.length = 0
506
710
  this.highWatermark = 0
507
711
  this.pendingRanges.length = 0
712
+ this.pendingMove = null
713
+ this.pendingReplace.clear()
714
+ this.uploadEpoch = 0
715
+ this.visibleIndexSpans = []
508
716
  this.syncDefaultDrawGroups()
509
717
  }
510
718
 
511
- dispose (): void {
719
+ dispose(): void {
512
720
  this.mesh.parent?.remove(this.mesh)
513
721
  this.mesh.geometry.dispose()
514
722
  this.reset()
515
723
  }
516
724
 
517
- private markDirty (start: number, end: number): void {
725
+ private markDirty(start: number, end: number): void {
518
726
  this.pendingRanges.push({ start, end })
519
727
  this.pendingRanges.sort((a, b) => a.start - b.start)
520
728
  this.mergePendingRanges()
521
729
  }
522
730
 
523
- private mergePendingRanges (): void {
731
+ private mergePendingRanges(): void {
524
732
  if (this.pendingRanges.length < 2) return
525
- const merged: Array<{ start: number, end: number }> = []
733
+ const merged: Array<{ start: number; end: number }> = []
526
734
  let cur = this.pendingRanges[0]!
527
735
  for (let i = 1; i < this.pendingRanges.length; i++) {
528
736
  const next = this.pendingRanges[i]!
@@ -537,7 +745,7 @@ export class GlobalLegacyBuffer {
537
745
  this.pendingRanges = merged
538
746
  }
539
747
 
540
- private takeFreeSlot (count: number): { start: number, count: number } | undefined {
748
+ private takeFreeSlot(count: number): { start: number; count: number } | undefined {
541
749
  for (let i = 0; i < this.freeList.length; i++) {
542
750
  const slot = this.freeList[i]!
543
751
  if (slot.count >= count) {
@@ -551,15 +759,15 @@ export class GlobalLegacyBuffer {
551
759
  return undefined
552
760
  }
553
761
 
554
- private insertFreeSlot (slot: { start: number, count: number }): void {
762
+ private insertFreeSlot(slot: { start: number; count: number }): void {
555
763
  this.freeList.push(slot)
556
764
  this.freeList.sort((a, b) => a.start - b.start)
557
765
  this.mergeFreeList()
558
766
  }
559
767
 
560
- private mergeFreeList (): void {
768
+ private mergeFreeList(): void {
561
769
  if (this.freeList.length < 2) return
562
- const merged: Array<{ start: number, count: number }> = []
770
+ const merged: Array<{ start: number; count: number }> = []
563
771
  let cur = this.freeList[0]!
564
772
  for (let i = 1; i < this.freeList.length; i++) {
565
773
  const next = this.freeList[i]!
@@ -574,7 +782,7 @@ export class GlobalLegacyBuffer {
574
782
  this.freeList = merged
575
783
  }
576
784
 
577
- private shrinkHighWatermark (): void {
785
+ private shrinkHighWatermark(): void {
578
786
  while (this.highWatermark > 0) {
579
787
  const tail = this.highWatermark - 1
580
788
  const free = this.freeList.find(s => s.start <= tail && s.start + s.count > tail)
@@ -585,7 +793,138 @@ export class GlobalLegacyBuffer {
585
793
  }
586
794
  }
587
795
 
588
- private growCapacity (minQuads: number): void {
796
+ private interiorFreeQuads(): number {
797
+ let total = 0
798
+ for (const slot of this.freeList) {
799
+ if (slot.start < this.highWatermark) total += slot.count
800
+ }
801
+ return total
802
+ }
803
+
804
+ private findMovableSection(maxCount: number): { key: string; start: number; count: number } | undefined {
805
+ const sections: Array<{ key: string; start: number; count: number }> = []
806
+ for (const [key, slot] of this.sectionSlots) {
807
+ sections.push({ key, start: slot.start, count: slot.count })
808
+ }
809
+ if (sections.length === 0) return undefined
810
+
811
+ sections.sort((a, b) => {
812
+ if (b.start !== a.start) return b.start - a.start
813
+ return b.start + b.count - (a.start + a.count)
814
+ })
815
+
816
+ const tailmost = sections[0]!
817
+ if (tailmost.count <= maxCount && this.findLowestInteriorHole(tailmost.start, tailmost.count)) {
818
+ return tailmost
819
+ }
820
+
821
+ const candidates = sections.filter(s => s.count <= maxCount).sort((a, b) => b.count - a.count)
822
+
823
+ for (const s of candidates) {
824
+ if (this.findLowestInteriorHole(s.start, s.count)) return s
825
+ }
826
+ return undefined
827
+ }
828
+
829
+ private findLowestInteriorHole(sectionStart: number, count: number): { start: number; count: number; index: number } | undefined {
830
+ for (let i = 0; i < this.freeList.length; i++) {
831
+ const slot = this.freeList[i]!
832
+ if (slot.start < sectionStart && slot.count >= count) {
833
+ return { start: slot.start, count: slot.count, index: i }
834
+ }
835
+ }
836
+ return undefined
837
+ }
838
+
839
+ private reserveFreeSlotAt(index: number, count: number): { start: number; count: number } {
840
+ const slot = this.freeList[index]!
841
+ this.freeList.splice(index, 1)
842
+ if (slot.count === count) return { start: slot.start, count }
843
+ const used = { start: slot.start, count }
844
+ this.insertFreeSlot({ start: slot.start + count, count: slot.count - count })
845
+ return used
846
+ }
847
+
848
+ private copySectionRange(oldStart: number, newStart: number, quadCount: number): void {
849
+ const oldVertBase = oldStart * VERTS_PER_QUAD
850
+ const newVertBase = newStart * VERTS_PER_QUAD
851
+ const vertCount = quadCount * VERTS_PER_QUAD
852
+ const vertDelta = newVertBase - oldVertBase
853
+
854
+ const oldFloatBase = oldVertBase * FLOATS_PER_VERT
855
+ const newFloatBase = newVertBase * FLOATS_PER_VERT
856
+ const floatLen = vertCount * FLOATS_PER_VERT
857
+ this.positions.copyWithin(newFloatBase, oldFloatBase, oldFloatBase + floatLen)
858
+ this.colors.copyWithin(newFloatBase, oldFloatBase, oldFloatBase + floatLen)
859
+ this.aOrigin.copyWithin(newFloatBase, oldFloatBase, oldFloatBase + floatLen)
860
+
861
+ const oldLightBase = oldVertBase * FLOATS_PER_LIGHT_VERT
862
+ const newLightBase = newVertBase * FLOATS_PER_LIGHT_VERT
863
+ const lightLen = vertCount * FLOATS_PER_LIGHT_VERT
864
+ this.skyLights.copyWithin(newLightBase, oldLightBase, oldLightBase + lightLen)
865
+ this.blockLights.copyWithin(newLightBase, oldLightBase, oldLightBase + lightLen)
866
+
867
+ const oldUvBase = oldVertBase * FLOATS_PER_UV_VERT
868
+ const newUvBase = newVertBase * FLOATS_PER_UV_VERT
869
+ const uvLen = vertCount * FLOATS_PER_UV_VERT
870
+ this.uvs.copyWithin(newUvBase, oldUvBase, oldUvBase + uvLen)
871
+
872
+ const oldIndexBase = oldStart * INDICES_PER_QUAD
873
+ const newIndexBase = newStart * INDICES_PER_QUAD
874
+ const indexLen = quadCount * INDICES_PER_QUAD
875
+ for (let i = 0; i < indexLen; i++) {
876
+ this.indices[newIndexBase + i] = this.indices[oldIndexBase + i]! + vertDelta
877
+ }
878
+ }
879
+
880
+ private rangeFullyUploaded(start: number, end: number): boolean {
881
+ for (const r of this.pendingRanges) {
882
+ if (r.start <= end && r.end >= start) return false
883
+ }
884
+ return true
885
+ }
886
+
887
+ private zeroAndFreeSlot(start: number, count: number): void {
888
+ const oldIndexBase = start * INDICES_PER_QUAD
889
+ const oldIndexLen = count * INDICES_PER_QUAD
890
+ for (let i = 0; i < oldIndexLen; i++) {
891
+ this.indices[oldIndexBase + i] = 0
892
+ }
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)
915
+ this.shrinkHighWatermark()
916
+ this.syncDefaultDrawGroups()
917
+ this.pendingMove = null
918
+ this.layoutVersion++
919
+ this.uploadEpoch++
920
+ }
921
+
922
+ private growCapacity(minQuads: number): void {
923
+ if (this.pendingMove) this.finalizePendingMove()
924
+ for (const key of [...this.pendingReplace.keys()]) {
925
+ this.finalizePendingReplace(key)
926
+ }
927
+
589
928
  let newCap = this.capacityQuads
590
929
  while (newCap < minQuads) newCap += this.growthIncrementQuads
591
930
 
@@ -652,22 +991,12 @@ const _raycastOrigin = new THREE.Vector3()
652
991
  const _raycastRenderOrigin = new THREE.Vector3()
653
992
  const _raycastRay = new THREE.Ray()
654
993
 
655
- function readWorldVertex (
656
- positions: Float32Array,
657
- aOrigin: Float32Array,
658
- floatBase: number,
659
- vertIndex: number,
660
- target: THREE.Vector3,
661
- ): void {
994
+ function readWorldVertex(positions: Float32Array, aOrigin: Float32Array, floatBase: number, vertIndex: number, target: THREE.Vector3): void {
662
995
  const f = floatBase + vertIndex * FLOATS_PER_VERT
663
- target.set(
664
- aOrigin[f]! + positions[f]!,
665
- aOrigin[f + 1]! + positions[f + 1]!,
666
- aOrigin[f + 2]! + positions[f + 2]!,
667
- )
996
+ target.set(aOrigin[f]! + positions[f]!, aOrigin[f + 1]! + positions[f + 1]!, aOrigin[f + 2]! + positions[f + 2]!)
668
997
  }
669
998
 
670
- function intersectTriangle (
999
+ function intersectTriangle(
671
1000
  ray: THREE.Ray,
672
1001
  positions: Float32Array,
673
1002
  aOrigin: Float32Array,
@@ -676,7 +1005,7 @@ function intersectTriangle (
676
1005
  i1: number,
677
1006
  i2: number,
678
1007
  near: number,
679
- far: number,
1008
+ far: number
680
1009
  ): number | null {
681
1010
  readWorldVertex(positions, aOrigin, floatBase, i0, _vA)
682
1011
  readWorldVertex(positions, aOrigin, floatBase, i1, _vB)
@@ -697,7 +1026,7 @@ function intersectTriangle (
697
1026
  return t
698
1027
  }
699
1028
 
700
- 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 {
701
1030
  _edge1.subVectors(b, a)
702
1031
  _edge2.subVectors(c, a)
703
1032
  const n = _normal.crossVectors(_edge1, _edge2).normalize()