minecraft-renderer 0.1.72 → 0.1.74

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 (70) hide show
  1. package/README.md +1 -1
  2. package/dist/mesher.js +81 -81
  3. package/dist/mesher.js.map +3 -3
  4. package/dist/mesherWasm.js +1183 -943
  5. package/dist/minecraft-renderer.js +253 -80
  6. package/dist/minecraft-renderer.js.meta.json +1 -1
  7. package/dist/threeWorker.js +1735 -1002
  8. package/package.json +3 -3
  9. package/src/graphicsBackend/config.ts +4 -0
  10. package/src/graphicsBackend/rendererDefaultOptions.ts +2 -7
  11. package/src/graphicsBackend/rendererOptionsSync.ts +1 -1
  12. package/src/graphicsBackend/types.ts +1 -0
  13. package/src/lib/bakeLegacyLight.ts +17 -0
  14. package/src/lib/bindAbortableListener.ts +1 -1
  15. package/src/lib/blockEntityLightRegistry.test.ts +18 -0
  16. package/src/lib/blockEntityLightRegistry.ts +75 -0
  17. package/src/lib/blockEntityLighting.test.ts +30 -0
  18. package/src/lib/blockEntityLighting.ts +53 -0
  19. package/src/lib/createPlayerObject.ts +1 -1
  20. package/src/lib/worldrendererCommon.reconfigure.test.ts +4 -1
  21. package/src/lib/worldrendererCommon.removeColumn.test.ts +8 -4
  22. package/src/lib/worldrendererCommon.ts +15 -7
  23. package/src/mesher-shared/blockEntityMetadata.test.ts +33 -0
  24. package/src/mesher-shared/blockEntityMetadata.ts +19 -3
  25. package/src/mesher-shared/exportedGeometryTypes.ts +11 -0
  26. package/src/mesher-shared/models.ts +161 -92
  27. package/src/mesher-shared/shared.ts +15 -4
  28. package/src/mesher-shared/tests/liquidQuadInvariant.test.ts +40 -0
  29. package/src/mesher-shared/world.ts +12 -0
  30. package/src/mesher-shared/worldLighting.test.ts +54 -0
  31. package/src/playground/baseScene.ts +1 -1
  32. package/src/three/bannerRenderer.ts +14 -4
  33. package/src/three/chunkMeshManager.ts +663 -69
  34. package/src/three/cubeDrawSpans.ts +74 -0
  35. package/src/three/cubeMultiDraw.ts +119 -0
  36. package/src/three/documentRenderer.ts +0 -2
  37. package/src/three/entities.ts +7 -7
  38. package/src/three/entity/EntityMesh.ts +7 -5
  39. package/src/three/entity/gltfAnimationUtils.ts +5 -3
  40. package/src/three/globalBlockBuffer.ts +208 -12
  41. package/src/three/globalLegacyBuffer.ts +701 -0
  42. package/src/three/graphicsBackendBase.ts +9 -5
  43. package/src/three/itemMesh.ts +6 -3
  44. package/src/three/legacySectionCull.ts +85 -0
  45. package/src/three/modules/rain.ts +22 -21
  46. package/src/three/modules/sciFiWorldReveal.ts +347 -703
  47. package/src/three/modules/starfield.ts +19 -6
  48. package/src/three/sectionRaycastAabb.ts +25 -0
  49. package/src/three/shaders/cubeBlockShader.ts +80 -17
  50. package/src/three/shaders/legacyBlockShader.ts +292 -0
  51. package/src/three/skyboxRenderer.ts +1 -1
  52. package/src/three/tests/chunkMeshManagerLegacy.test.ts +286 -0
  53. package/src/three/tests/cubeDrawSpans.test.ts +73 -0
  54. package/src/three/tests/globalLegacyBuffer.test.ts +360 -0
  55. package/src/three/tests/legacySectionCull.test.ts +80 -0
  56. package/src/three/tests/signTextureCache.test.ts +83 -0
  57. package/src/three/threeJsMedia.ts +2 -2
  58. package/src/three/waypointSprite.ts +2 -2
  59. package/src/three/world/cursorBlock.ts +1 -0
  60. package/src/three/world/vr.ts +2 -2
  61. package/src/three/worldGeometryExport.ts +83 -26
  62. package/src/three/worldRendererThree.ts +100 -30
  63. package/src/wasm-mesher/bridge/render-from-wasm.ts +214 -72
  64. package/src/wasm-mesher/bridge/shaderCubeBridge.ts +18 -6
  65. package/src/wasm-mesher/runtime-build/wasm_mesher_bg.wasm +0 -0
  66. package/src/wasm-mesher/tests/sectionRaycastAabb.test.ts +20 -0
  67. package/src/wasm-mesher/tests/shaderCubeInstances.test.ts +80 -12
  68. package/src/wasm-mesher/worker/mesherWasm.ts +70 -14
  69. package/src/wasm-mesher/worker/mesherWasmLightDirty.test.ts +11 -0
  70. package/src/wasm-mesher/worker/mesherWasmLightDirty.ts +15 -0
@@ -1,847 +1,491 @@
1
1
  //@ts-nocheck
2
+ /**
3
+ * Wireframe-to-solid chunk reveal.
4
+ * Ported from web-client-2/renderer/viewer/three/sciFiWorldReveal.ts
5
+ */
2
6
  import * as THREE from 'three'
3
7
  import type { WorldRendererThree } from '../worldRendererThree'
4
8
  import type { RendererModuleController, RendererModuleManifest } from '../rendererModuleSystem'
5
9
  import type { MesherGeometryOutput } from '../../mesher-shared/shared'
6
- import type { GlobalBlockBufferShaderData } from '../globalBlockBuffer'
10
+ import type { SectionObject } from '../chunkMeshManager'
7
11
 
8
12
  const SCI_FI_CYAN = new THREE.Color(13 / 255, 234 / 255, 238 / 255)
13
+
9
14
  const CHUNKS_THRESHOLD = 9
10
- const REVEAL_DURATION = 3500 // ms for full reveal transition
11
- const WIREFRAME_FADE_DELAY = 1200 // ms before wireframe starts fading
15
+ const GLOBAL_START_FALLBACK_MS = 12_000
16
+ const WAVE_SPREAD_MS = 1500
17
+ const MAX_SECTION_LIFETIME_MS = 10_000
12
18
 
13
19
  const INITIAL_WIREFRAME_MS = 350
14
- const INITIAL_REVEAL_MS = 650
15
- const INITIAL_WAVE_SPREAD_MS = 650
16
-
20
+ const INITIAL_FADE_MS = 650
17
21
  const CHUNK_WIREFRAME_MS = 120
18
- const CHUNK_REVEAL_MS = 280
22
+ const CHUNK_FADE_MS = 280
19
23
 
20
- interface RevealingSection {
24
+ type RevealPhase = 'queued' | 'wireframe' | 'fade' | 'done'
25
+
26
+ interface SectionReveal {
21
27
  key: string
22
- wireframeGroup: THREE.Group
23
- revealStartTime: number
24
- phase: 'wireframe' | 'transitioning' | 'complete'
25
- /** Legacy + shader render meshes hidden during reveal */
26
- renderMeshRefs: THREE.Mesh[]
27
- /** Shader cubes temporarily removed from globalBlockBuffer during reveal. */
28
- globalShaderRestore?: GlobalBlockBufferShaderData
28
+ geometry: MesherGeometryOutput
29
+ phase: RevealPhase
30
+ phaseStartMs: number
31
+ revealAtMs: number
29
32
  wireframeMs: number
30
- revealMs: number
33
+ fadeMs: number
34
+ wireframeGroup: THREE.Group | null
35
+ mesh: THREE.Mesh | null
36
+ savedMaterial: THREE.Material | null
37
+ pulseOffset: number
31
38
  }
32
39
 
33
-
34
- /**
35
- * SciFiWorldReveal - Creates a futuristic wireframe-to-solid reveal effect
36
- *
37
- * When chunks load, they first appear as glowing cyan wireframes that pulse
38
- * and emanate from the camera, then gradually transition to solid geometry.
39
- */
40
40
  export class SciFiWorldRevealModule implements RendererModuleController {
41
- private readonly pendingGeometries = new Map<string, MesherGeometryOutput>()
42
- private readonly revealingSections = new Map<string, RevealingSection>()
43
- private finishedChunkCount = 0
44
- private revealTriggered = false
45
- private revealStartTime = 0
46
- private enabled = false
47
-
48
- private onWorldSwitchedCb: (() => void) | null = null
49
- private patched = false
50
- private initialWaveDone = false
51
- /** True after every section from the first reveal wave has finished animating. */
52
- private initialRevealWaveSettled = false
41
+ private enabled = true
42
+ private readonly sections = new Map<string, SectionReveal>()
43
+ private readonly completed = new Set<string>()
53
44
 
54
- // Wireframe materials
55
- private readonly wireframeMaterial!: THREE.LineBasicMaterial
56
- private readonly wireframeGlowMaterial!: THREE.LineBasicMaterial
57
-
58
- // For pulsing animation
45
+ private globalWaveStarted = false
46
+ private globalWaveStartMs = 0
47
+ private finishedChunkCount = 0
48
+ private firstQueuedMs: number | null = null
59
49
  private pulseTime = 0
60
50
 
61
- // Track which chunks have been revealed
62
- private readonly revealedChunks = new Set<string>()
63
-
64
- // Store original methods for patching
65
- private originalFinishChunk: ((chunkKey: string) => void) | null = null
66
- private originalDestroy: (() => void) | null = null
67
- private originalSceneAdd: ((...object: THREE.Object3D[]) => THREE.Scene) | null = null
68
- private originalHandleWorkerMessage: ((data: { geometry: MesherGeometryOutput; key: string; type: string }) => void) | null = null
51
+ private readonly wireframeMaterial = new THREE.LineBasicMaterial({
52
+ color: SCI_FI_CYAN,
53
+ transparent: true,
54
+ opacity: 1,
55
+ blending: THREE.AdditiveBlending,
56
+ depthWrite: false,
57
+ })
58
+
59
+ private readonly wireframeGlowMaterial = new THREE.LineBasicMaterial({
60
+ color: SCI_FI_CYAN,
61
+ transparent: true,
62
+ opacity: 0.55,
63
+ blending: THREE.AdditiveBlending,
64
+ depthWrite: false,
65
+ })
69
66
 
70
67
  constructor(private readonly worldRenderer: WorldRendererThree) {
71
- this.wireframeMaterial = new THREE.LineBasicMaterial({
72
- color: SCI_FI_CYAN,
73
- transparent: true,
74
- opacity: 1,
75
- blending: THREE.AdditiveBlending,
76
- depthWrite: false,
77
- })
78
-
79
- this.wireframeGlowMaterial = new THREE.LineBasicMaterial({
80
- color: SCI_FI_CYAN,
81
- transparent: true,
82
- opacity: 0.55,
83
- blending: THREE.AdditiveBlending,
84
- depthWrite: false,
85
- })
68
+ if (worldRenderer.worldRendererConfig.futuristicReveal !== true) {
69
+ this.enabled = false
70
+ }
86
71
  }
87
72
 
88
- /** Read live config — option may sync after module construction (watchOptions). */
89
- isFuturisticRevealConfigured (): boolean {
73
+ autoEnableCheck(): boolean {
90
74
  return this.worldRenderer.worldRendererConfig.futuristicReveal === true
91
75
  }
92
76
 
93
- /** Until the first cinematic wave fully completes, shader sections stay off the global buffer. */
94
- isInInitialRevealCampaign (): boolean {
95
- return this.enabled && !this.initialRevealWaveSettled
77
+ enable(): void {
78
+ if (!this.autoEnableCheck()) return
79
+ this.setEnabled(true)
96
80
  }
97
81
 
98
- autoEnableCheck (): boolean {
99
- return this.isFuturisticRevealConfigured()
82
+ disable(): void {
83
+ this.setEnabled(false)
100
84
  }
101
85
 
102
- enable(): void {
103
- if (!this.isFuturisticRevealConfigured()) return
104
- if (this.enabled && this.patched) return
105
- if (this.enabled && !this.patched) {
106
- this.patchWorldRenderer()
107
- return
108
- }
109
- this.enabled = true
110
- this.patchWorldRenderer()
86
+ dispose(): void {
87
+ this.reset()
88
+ this.wireframeMaterial.dispose()
89
+ this.wireframeGlowMaterial.dispose()
111
90
  }
112
91
 
113
- disable(): void {
114
- if (!this.enabled) return
115
- this.enabled = false
116
- this.unpatchWorldRenderer()
117
- this.reset()
92
+ shouldDeferSectionGeometry(sectionKey: string): boolean {
93
+ return this.enabled && !this.completed.has(sectionKey)
118
94
  }
119
95
 
120
- toggle(): boolean {
121
- if (this.enabled) {
122
- this.disable()
123
- } else {
124
- this.enable()
125
- }
126
- return this.enabled
96
+ onSectionMeshed(key: string, geometry: MesherGeometryOutput, sectionObject: SectionObject): void {
97
+ const mesh = sectionObject.children.find(c => c.name === 'mesh')
98
+ if (!(mesh instanceof THREE.Mesh)) return
99
+ this.onSectionMeshedMesh(key, geometry, mesh)
127
100
  }
128
101
 
129
- render?: (deltaTime: number) => void = (deltaTime) => {
102
+ onChunkFinished(): void {
130
103
  if (!this.enabled) return
131
- this.update(deltaTime * 1000)
104
+ this.finishedChunkCount++
105
+ this.tryStartGlobalWave(performance.now())
132
106
  }
133
107
 
134
- dispose(): void {
135
- this.disable()
136
- this.wireframeMaterial.dispose()
137
- this.wireframeGlowMaterial.dispose()
108
+ onSectionRemoved(key: string): void {
109
+ this.cancelSection(key, true)
110
+ this.completed.delete(key)
138
111
  }
139
112
 
140
- /**
141
- * Patch world renderer methods to integrate the reveal effect
142
- */
143
- private patchWorldRenderer(): void {
144
- if (this.patched) return
145
- this.patched = true
146
- const wr = this.worldRenderer
147
-
148
- // Hook into onWorldSwitched
149
- this.onWorldSwitchedCb = () => this.reset()
150
- wr.onWorldSwitched.push(this.onWorldSwitchedCb)
151
-
113
+ onWorldSwitched(): void {
114
+ this.reset()
115
+ }
152
116
 
153
- // Patch finishChunk
154
- this.originalFinishChunk = wr.finishChunk.bind(wr)
155
- wr.finishChunk = (chunkKey: string) => {
156
- this.originalFinishChunk!(chunkKey)
157
- this.onChunkFinished(chunkKey)
158
- }
117
+ tick(deltaMs: number, now = performance.now()): void {
118
+ if (!this.enabled) return
159
119
 
160
- // Patch destroy
161
- this.originalDestroy = wr.destroy.bind(wr)
162
- wr.destroy = () => {
163
- this.dispose()
164
- this.originalDestroy!()
165
- }
120
+ this.tryStartGlobalWave(now)
166
121
 
167
- // Patch handleWorkerMessage to intercept geometry
168
- this.originalHandleWorkerMessage = wr.handleWorkerMessage.bind(wr)
169
- wr.handleWorkerMessage = (data: any) => {
170
- this.originalHandleWorkerMessage!(data)
171
-
172
- if (this.enabled && data?.type === 'geometry') {
173
- Promise.resolve().then(() => {
174
- try {
175
- this.registerSection(data.key, data.geometry)
176
- } catch (err) {
177
- console.error('[SciFiReveal] registerSection failed', err)
178
- }
179
- })
180
- }
122
+ if (
123
+ !this.globalWaveStarted &&
124
+ this.firstQueuedMs !== null &&
125
+ this.sections.size > 0 &&
126
+ now - this.firstQueuedMs >= GLOBAL_START_FALLBACK_MS
127
+ ) {
128
+ this.startGlobalWave(now)
181
129
  }
182
130
 
131
+ if (this.sections.size === 0) return
183
132
 
184
- // Patch scene.add to intercept mesh additions
185
- this.originalSceneAdd = wr.scene.add.bind(wr.scene)
186
- wr.scene.add = (...objects: THREE.Object3D[]): THREE.Scene => {
187
- // Call original add first
188
- const result = this.originalSceneAdd!(...objects)
133
+ this.pulseTime += deltaMs * 0.001
134
+ const toFinish: SectionReveal[] = []
189
135
 
190
- // Check each added object for meshes that need reveal effect
191
- for (const obj of objects) {
192
- this.checkAndPatchMesh(obj)
136
+ for (const section of this.sections.values()) {
137
+ if (now - section.phaseStartMs > MAX_SECTION_LIFETIME_MS) {
138
+ toFinish.push(section)
139
+ continue
193
140
  }
194
141
 
195
- return result
196
- }
197
- }
198
-
199
- /**
200
- * Unpatch world renderer methods
201
- */
202
- private unpatchWorldRenderer(): void {
203
- const wr = this.worldRenderer
142
+ if (section.phase === 'queued') {
143
+ if (this.globalWaveStarted && now >= section.revealAtMs) {
144
+ this.beginWireframe(section, now)
145
+ }
146
+ continue
147
+ }
204
148
 
205
- if (this.originalFinishChunk) {
206
- wr.finishChunk = this.originalFinishChunk
207
- this.originalFinishChunk = null
208
- }
149
+ const phaseElapsed = now - section.phaseStartMs
209
150
 
210
- if (this.originalDestroy) {
211
- wr.destroy = this.originalDestroy
212
- this.originalDestroy = null
213
- }
214
-
215
- if (this.originalHandleWorkerMessage) {
216
- wr.handleWorkerMessage = this.originalHandleWorkerMessage
217
- this.originalHandleWorkerMessage = null
218
- }
151
+ if (section.phase === 'wireframe') {
152
+ this.animateWireframe(section, phaseElapsed)
153
+ if (phaseElapsed >= section.wireframeMs) {
154
+ this.beginFade(section, now)
155
+ }
156
+ continue
157
+ }
219
158
 
220
- if (this.originalSceneAdd) {
221
- wr.scene.add = this.originalSceneAdd
222
- this.originalSceneAdd = null
159
+ if (section.phase === 'fade') {
160
+ const progress = Math.min(1, phaseElapsed / section.fadeMs)
161
+ this.animateFade(section, progress)
162
+ if (progress >= 1) {
163
+ toFinish.push(section)
164
+ }
165
+ }
223
166
  }
224
167
 
225
- if (this.onWorldSwitchedCb) {
226
- const i = wr.onWorldSwitched.indexOf(this.onWorldSwitchedCb)
227
- if (i !== -1) wr.onWorldSwitched.splice(i, 1)
228
- this.onWorldSwitchedCb = null
168
+ for (const section of toFinish) {
169
+ this.finishSection(section)
229
170
  }
230
- this.patched = false
231
171
  }
232
172
 
233
- /**
234
- * Check if an object or its children is a mesh that needs reveal effect visibility patch
235
- */
236
- private checkAndPatchMesh(obj: THREE.Object3D): void {
237
- if (obj instanceof THREE.Mesh && (obj.name === 'mesh' || obj.name === 'shaderMesh')) {
238
- const sectionKey = this.findSectionKeyForMesh(obj)
239
- if (sectionKey && this.shouldUseRevealEffect(sectionKey)) {
240
- obj.visible = false
241
- ; (obj as any).hiddenByReveal = true
242
- }
243
- }
244
-
245
- // Recursively check children
246
- for (const child of obj.children) {
247
- this.checkAndPatchMesh(child)
173
+ private setEnabled(enabled: boolean): void {
174
+ if (enabled === this.enabled) return
175
+ this.enabled = enabled
176
+ if (!enabled) {
177
+ this.forceFinishAll()
248
178
  }
249
179
  }
250
180
 
251
- /**
252
- * Find the section key for a mesh by traversing up to find the parent group
253
- * and checking for sectionKey property
254
- */
255
- private findSectionKeyForMesh(mesh: THREE.Mesh): string | null {
256
- // Traverse up to find the parent group with sectionKey
257
- let current: THREE.Object3D | null = mesh
258
- while (current) {
259
- const { sectionKey } = (current as any)
260
- if (sectionKey && this.worldRenderer.chunkMeshManager.sectionObjects[sectionKey] === current) {
261
- return sectionKey
181
+ private onSectionMeshedMesh(key: string, geometry: MesherGeometryOutput, mesh: THREE.Mesh): void {
182
+ if (!this.enabled || !geometry.positions?.length) return
183
+ if (this.completed.has(key)) return
184
+
185
+ const existing = this.sections.get(key)
186
+ if (existing) {
187
+ existing.mesh = mesh
188
+ existing.geometry = geometry
189
+ if (existing.phase === 'wireframe' || existing.phase === 'fade') {
190
+ this.finishSection(existing)
262
191
  }
263
- current = current.parent
192
+ return
264
193
  }
265
194
 
266
- // Fallback: try to derive key from mesh world position
267
- // mesh.position is scene-relative (near 0 in camera-relative rendering),
268
- // so use stored world coords or convert back to world coords
269
- const wp = this.worldRenderer.sceneOrigin.getWorldPosition(mesh)
270
- const worldX = wp?.x ?? this.worldRenderer.sceneOrigin.toWorldX(mesh.position.x)
271
- const worldY = wp?.y ?? this.worldRenderer.sceneOrigin.toWorldY(mesh.position.y)
272
- const worldZ = wp?.z ?? this.worldRenderer.sceneOrigin.toWorldZ(mesh.position.z)
273
- const CHUNK_SIZE = 16
274
- const sectionHeight = this.worldRenderer.getSectionHeight()
275
- const sectionX = Math.floor(worldX / CHUNK_SIZE) * CHUNK_SIZE
276
- const sectionY = Math.floor(worldY / sectionHeight) * sectionHeight
277
- const sectionZ = Math.floor(worldZ / CHUNK_SIZE) * CHUNK_SIZE
278
- const derivedKey = `${sectionX},${sectionY},${sectionZ}`
279
-
280
- // Verify this key exists in sectionObjects
281
- if (this.worldRenderer.chunkMeshManager.sectionObjects[derivedKey]) {
282
- return derivedKey
195
+ const now = performance.now()
196
+ if (this.firstQueuedMs === null) {
197
+ this.firstQueuedMs = now
283
198
  }
284
199
 
285
- return null
286
- }
287
-
288
- /**
289
- * Get the scene from world renderer
290
- */
291
- private get scene(): THREE.Scene {
292
- return this.worldRenderer.realScene
293
- }
200
+ const useChunkTimings = this.globalWaveStarted
201
+ this.sections.set(key, {
202
+ key,
203
+ geometry,
204
+ phase: 'queued',
205
+ phaseStartMs: now,
206
+ revealAtMs: this.globalWaveStarted ? now : 0,
207
+ wireframeMs: useChunkTimings ? CHUNK_WIREFRAME_MS : INITIAL_WIREFRAME_MS,
208
+ fadeMs: useChunkTimings ? CHUNK_FADE_MS : INITIAL_FADE_MS,
209
+ wireframeGroup: null,
210
+ mesh,
211
+ savedMaterial: null,
212
+ pulseOffset: Math.random() * Math.PI * 2,
213
+ })
294
214
 
295
- /**
296
- * Get camera position from world renderer
297
- */
298
- private getCameraPosition(): THREE.Vector3 {
299
- return this.worldRenderer.getCameraPosition()
215
+ mesh.visible = false
216
+ this.setShaderMeshesVisible(key, false)
217
+ this.tryStartGlobalWave(now)
300
218
  }
301
219
 
302
- private sectionHasRevealContent(geometry: MesherGeometryOutput): boolean {
303
- if ((geometry.wireframePositions?.length ?? 0) > 0) return true
304
- if ((geometry.positions?.length ?? 0) > 0) return true
305
- return (geometry.shaderCubes?.count ?? 0) > 0
220
+ private reset(): void {
221
+ this.forceFinishAll()
222
+ this.sections.clear()
223
+ this.completed.clear()
224
+ this.globalWaveStarted = false
225
+ this.globalWaveStartMs = 0
226
+ this.finishedChunkCount = 0
227
+ this.firstQueuedMs = null
228
+ this.pulseTime = 0
306
229
  }
307
230
 
308
- /** Legacy `mesh` and instanced `shaderMesh` for a section. */
309
- private getSectionRenderMeshes(key: string): THREE.Mesh[] {
310
- const sectionObject = this.worldRenderer.chunkMeshManager.sectionObjects[key]
311
- if (!sectionObject) return []
312
- const meshes: THREE.Mesh[] = []
313
- for (const name of ['mesh', 'shaderMesh'] as const) {
314
- const child = sectionObject.children.find(c => c.name === name)
315
- if (child instanceof THREE.Mesh) meshes.push(child)
231
+ private forceFinishAll(): void {
232
+ for (const section of [...this.sections.values()]) {
233
+ this.finishSection(section)
316
234
  }
317
- return meshes
235
+ this.sections.clear()
236
+ this.unhideAllSectionMeshes()
318
237
  }
319
238
 
320
- private hideSectionRenderMeshes(key: string): THREE.Mesh[] {
321
- const meshes = this.getSectionRenderMeshes(key)
322
- for (const m of meshes) {
323
- m.visible = false
324
- ;(m as any).hiddenByReveal = true
325
- }
326
- return meshes
327
- }
239
+ private tryStartGlobalWave(now: number): void {
240
+ if (this.globalWaveStarted) return
241
+ if (this.sections.size === 0) return
328
242
 
329
- private setMeshFadeOpacity(mesh: THREE.Mesh, opacity: number): void {
330
- // ShaderMaterial ignores material.opacity; cloning breaks atlas uniforms.
331
- if (mesh.name === 'shaderMesh') {
332
- mesh.visible = opacity > 0.001
243
+ if (this.finishedChunkCount >= CHUNKS_THRESHOLD) {
244
+ this.startGlobalWave(now)
333
245
  return
334
246
  }
335
- const mat = mesh.material
336
- if (Array.isArray(mat)) return
337
- if (!(mat as any).originalMaterial) {
338
- ;(mat as any).originalMaterial = mat
339
- const fadeMat = mat.clone()
340
- fadeMat.transparent = true
341
- fadeMat.opacity = opacity
342
- fadeMat.needsUpdate = true
343
- mesh.material = fadeMat
344
- } else {
345
- mat.opacity = opacity
346
- mat.transparent = true
347
- mat.needsUpdate = true
348
- }
349
- }
350
247
 
351
- private restoreMeshMaterial(mesh: THREE.Mesh): void {
352
- const originalMat = (mesh as any).originalMaterial as THREE.Material | undefined
353
- if (!originalMat) return
354
- const currentMat = mesh.material as THREE.Material
355
- mesh.material = originalMat
356
- if (currentMat !== originalMat) currentMat.dispose()
357
- delete (mesh as any).originalMaterial
358
- }
359
-
360
- /**
361
- * Call this when a chunk finishes loading
362
- */
363
- onChunkFinished(_chunkKey: string): void {
364
- this.finishedChunkCount++
365
-
366
- if (!this.revealTriggered && this.finishedChunkCount >= CHUNKS_THRESHOLD) {
367
- this.triggerReveal()
248
+ if (this.worldRenderer.allChunksFinished) {
249
+ this.startGlobalWave(now)
368
250
  }
369
251
  }
370
252
 
371
- /**
372
- * Register a new section geometry for the reveal effect
373
- */
374
- registerSection(key: string, geometry: MesherGeometryOutput): void {
375
- // If already revealed or currently revealing, skip
376
- if (this.revealedChunks.has(key) || this.revealingSections.has(key)) return
253
+ private startGlobalWave(now: number): void {
254
+ if (this.globalWaveStarted) return
255
+ this.globalWaveStarted = true
256
+ this.globalWaveStartMs = now
377
257
 
378
- // After the initial spawn wave, show streaming sections immediately (no wireframe flash).
379
- if (this.revealTriggered && this.initialWaveDone) {
380
- this.revealedChunks.add(key)
381
- return
382
- }
258
+ const cameraPos = this.worldRenderer.getCameraPosition()
259
+ let maxDistance = 1
260
+ const distances = new Map<string, number>()
383
261
 
384
- // If reveal already triggered, start effect immediately (don't store in pending)
385
- if (this.revealTriggered) {
386
- this.startSectionReveal(key, geometry)
387
- } else {
388
- // Store geometry for later
389
- this.pendingGeometries.set(key, geometry)
262
+ for (const [key, section] of this.sections) {
263
+ if (section.phase !== 'queued') continue
264
+ const { sx, sy, sz } = section.geometry
265
+ const distance = Math.hypot(sx - cameraPos.x, sy - cameraPos.y, sz - cameraPos.z)
266
+ distances.set(key, distance)
267
+ maxDistance = Math.max(maxDistance, distance)
390
268
  }
391
- }
392
269
 
393
- /**
394
- * Check if a section should use the reveal effect
395
- */
396
- shouldUseRevealEffect(key: string): boolean {
397
- if (!this.enabled) return false
398
- // Match registerSection: no cinematic hide for chunks loaded while moving.
399
- if (this.revealTriggered && this.initialWaveDone) return false
400
- return !this.revealedChunks.has(key) && !this.revealingSections.has(key)
401
- }
402
-
403
- /**
404
- * Trigger the reveal sequence
405
- */
406
- private triggerReveal(): void {
407
- this.revealTriggered = true
408
- this.initialWaveDone = true
409
-
410
- this.revealStartTime = performance.now()
411
-
412
- const cameraPos = this.getCameraPosition()
413
-
414
- // Copy and clear pending geometries before processing
415
- const toProcess = [...this.pendingGeometries.entries()]
416
- this.pendingGeometries.clear()
417
-
418
- // Sort by distance from camera for wave effect
419
- const sorted = toProcess
420
- .map(([key, geometry]) => {
421
- const distance = Math.hypot(
422
- (geometry.sx - cameraPos.x),
423
- (geometry.sy - cameraPos.y),
424
- (geometry.sz - cameraPos.z)
425
- )
426
- return { key, geometry, distance }
427
- })
428
- .sort((a, b) => a.distance - b.distance)
429
-
430
- const maxDistance = sorted.at(-1)?.distance || 1
431
-
432
- // Start reveal for each section with staggered timing
433
- for (const { key, geometry, distance } of sorted) {
434
- const delay = (distance / maxDistance) * 1500 // 1500ms spread for wave effect
435
- setTimeout(() => {
436
- // Double check the section hasn't been revealed already
437
- if (!this.revealedChunks.has(key) && !this.revealingSections.has(key)) {
438
- this.startSectionReveal(key, geometry)
439
- }
440
- }, delay)
270
+ for (const [, section] of this.sections) {
271
+ if (section.phase !== 'queued') continue
272
+ const distance = distances.get(section.key) ?? 0
273
+ section.revealAtMs = now + (distance / maxDistance) * WAVE_SPREAD_MS
274
+ section.wireframeMs = CHUNK_WIREFRAME_MS
275
+ section.fadeMs = CHUNK_FADE_MS
441
276
  }
442
277
  }
443
278
 
444
- /**
445
- * Start the reveal effect for a single section
446
- */
447
- private startSectionReveal(key: string, geometry: MesherGeometryOutput): void {
448
- if (!this.sectionHasRevealContent(geometry)) return
449
-
450
- // Don't create if already exists
451
- if (this.revealingSections.has(key) || this.revealedChunks.has(key)) return
452
-
453
- // Create wireframe geometry
454
- const wireframeGeom = this.createWireframeGeometry(geometry)
455
-
456
- const global = this.worldRenderer.chunkMeshManager.globalBlockBuffer
457
- const globalShaderRestore = global?.hasSection(key) ? global.takeSectionData(key) : undefined
279
+ private beginWireframe(section: SectionReveal, now: number): void {
280
+ const mesh = section.mesh ?? this.getSectionMesh(section.key)
281
+ section.mesh = mesh
282
+ if (!mesh) {
283
+ this.sections.delete(section.key)
284
+ return
285
+ }
458
286
 
459
- const renderMeshRefs = this.hideSectionRenderMeshes(key)
460
- // Main wireframe
287
+ const wireframeGeom = this.createWireframeGeometry(section.geometry)
461
288
  const wireframe = new THREE.LineSegments(wireframeGeom, this.wireframeMaterial.clone())
462
289
  this.worldRenderer.sceneOrigin.track(wireframe)
463
- wireframe.position.set(geometry.sx, geometry.sy, geometry.sz)
290
+ wireframe.position.set(section.geometry.sx, section.geometry.sy, section.geometry.sz)
464
291
  wireframe.name = 'scifi-wireframe'
465
292
  wireframe.renderOrder = 1000
466
293
 
467
- // Glow layer
468
- const glowWireframe = new THREE.LineSegments(wireframeGeom.clone(), this.wireframeGlowMaterial.clone())
469
- this.worldRenderer.sceneOrigin.track(glowWireframe)
470
- glowWireframe.position.set(geometry.sx, geometry.sy, geometry.sz)
471
- glowWireframe.scale.set(1.02, 1.02, 1.02)
472
- glowWireframe.name = 'scifi-glow'
473
- glowWireframe.renderOrder = 999
294
+ const glow = new THREE.LineSegments(wireframeGeom.clone(), this.wireframeGlowMaterial.clone())
295
+ this.worldRenderer.sceneOrigin.track(glow)
296
+ glow.position.copy(wireframe.position)
297
+ glow.scale.setScalar(1.02)
298
+ glow.name = 'scifi-glow'
299
+ glow.renderOrder = 999
474
300
 
475
301
  const group = new THREE.Group()
476
- group.add(wireframe)
477
- group.add(glowWireframe)
478
302
  group.name = 'scifi-reveal-group'
479
- // Store key on group for debugging
480
- ; (group as any).sectionKey = key
303
+ group.add(wireframe, glow)
304
+ this.worldRenderer.realScene.add(group)
481
305
 
482
- this.scene.add(group)
306
+ mesh.visible = false
307
+ this.setShaderMeshesVisible(section.key, false)
483
308
 
484
- const wireframeMs = this.initialWaveDone ? CHUNK_WIREFRAME_MS : INITIAL_WIREFRAME_MS
485
- const revealMs = this.initialWaveDone ? CHUNK_REVEAL_MS : INITIAL_REVEAL_MS
309
+ section.wireframeGroup = group
310
+ section.phase = 'wireframe'
311
+ section.phaseStartMs = now
312
+ }
486
313
 
487
- const section: RevealingSection = {
488
- key,
489
- wireframeGroup: group,
490
- revealStartTime: performance.now(),
491
- phase: 'wireframe',
492
- renderMeshRefs,
493
- globalShaderRestore,
494
- wireframeMs,
495
- revealMs,
314
+ private beginFade(section: SectionReveal, now: number): void {
315
+ const mesh = section.mesh ?? this.getSectionMesh(section.key)
316
+ section.mesh = mesh
317
+ if (mesh) {
318
+ mesh.visible = true
319
+ const rawMat = mesh.material
320
+ if (!Array.isArray(rawMat) && rawMat && typeof rawMat.clone === 'function') {
321
+ section.savedMaterial = rawMat
322
+ const fadeMat = rawMat.clone()
323
+ fadeMat.transparent = true
324
+ fadeMat.opacity = 0
325
+ fadeMat.needsUpdate = true
326
+ mesh.material = fadeMat
327
+ }
496
328
  }
329
+ this.setShaderMeshesVisible(section.key, true)
330
+ section.phase = 'fade'
331
+ section.phaseStartMs = now
332
+ }
497
333
 
498
- setTimeout(() => {
499
- for (const m of this.getSectionRenderMeshes(key)) {
500
- if (!(m as any).hiddenByReveal) {
501
- this.hideSectionRenderMeshes(key)
502
- }
503
- }
504
- }, 0)
334
+ private animateWireframe(section: SectionReveal, phaseElapsed: number): void {
335
+ if (!section.wireframeGroup) return
336
+ const wireframe = section.wireframeGroup.children[0] as THREE.LineSegments
337
+ const glow = section.wireframeGroup.children[1] as THREE.LineSegments
338
+ const basePulse = 0.6 + 0.4 * Math.sin(this.pulseTime * 4 + section.pulseOffset)
339
+ if (wireframe?.material) {
340
+ const mat = wireframe.material as THREE.LineBasicMaterial
341
+ mat.opacity = basePulse
342
+ const intensity = 0.85 + 0.15 * Math.sin(this.pulseTime * 6 + phaseElapsed * 0.002)
343
+ mat.color.setRGB((13 / 255) * intensity, (234 / 255) * intensity, (238 / 255) * intensity)
344
+ }
345
+ if (glow?.material) {
346
+ (glow.material as THREE.LineBasicMaterial).opacity = basePulse * 0.4
347
+ }
348
+ }
505
349
 
506
- this.revealingSections.set(key, section)
350
+ private animateFade(section: SectionReveal, progress: number): void {
351
+ const eased = 1 - (1 - progress) ** 3
352
+ if (section.wireframeGroup) {
353
+ const wireframe = section.wireframeGroup.children[0] as THREE.LineSegments
354
+ const glow = section.wireframeGroup.children[1] as THREE.LineSegments
355
+ if (wireframe?.material) (wireframe.material as THREE.LineBasicMaterial).opacity = 1 - eased
356
+ if (glow?.material) (glow.material as THREE.LineBasicMaterial).opacity = (1 - eased) * 0.55
357
+ }
358
+ const mesh = section.mesh
359
+ if (mesh && section.savedMaterial && !Array.isArray(mesh.material)) {
360
+ (mesh.material as THREE.Material).opacity = eased
361
+ }
362
+ this.setShaderMeshesVisible(section.key, eased > 0.001)
507
363
  }
508
364
 
509
- /** 16³ section bounds wireframe in section-local coords (−8…+8). */
510
- private createSectionBoundsWireframe(): THREE.BufferGeometry {
511
- const min = -8
512
- const max = 8
513
- const c = [
514
- [min, min, min], [max, min, min], [min, max, min], [max, max, min],
515
- [min, min, max], [max, min, max], [min, max, max], [max, max, max],
516
- ] as const
517
- const edges: [number, number][] = [
518
- [0, 1], [1, 3], [3, 2], [2, 0],
519
- [4, 5], [5, 7], [7, 6], [6, 4],
520
- [0, 4], [1, 5], [2, 6], [3, 7],
521
- ]
522
- const linePositions: number[] = []
523
- for (const [a, b] of edges) {
524
- linePositions.push(...c[a]!, ...c[b]!)
365
+ private finishSection(section: SectionReveal): void {
366
+ const mesh = section.mesh ?? this.getSectionMesh(section.key)
367
+ if (mesh) {
368
+ if (section.savedMaterial) {
369
+ const fadeMat = mesh.material as THREE.Material
370
+ mesh.material = section.savedMaterial
371
+ fadeMat.dispose()
372
+ section.savedMaterial = null
373
+ }
374
+ mesh.visible = true
525
375
  }
526
- const wireframeGeom = new THREE.BufferGeometry()
527
- wireframeGeom.setAttribute('position', new THREE.Float32BufferAttribute(linePositions, 3))
528
- return wireframeGeom
376
+ this.setShaderMeshesVisible(section.key, true)
377
+
378
+ const { chunkMeshManager } = this.worldRenderer
379
+ chunkMeshManager.migrateDeferredShaderToGlobal(section.key)
380
+ chunkMeshManager.migrateDeferredLegacyToGlobal(section.key)
381
+
382
+ if (section.wireframeGroup) {
383
+ this.disposeWireframeGroup(section.wireframeGroup)
384
+ section.wireframeGroup = null
385
+ }
386
+ this.sections.delete(section.key)
387
+ this.completed.add(section.key)
529
388
  }
530
389
 
531
- private createWireframeGeometry(geometry: MesherGeometryOutput): THREE.BufferGeometry {
532
- if (geometry.wireframePositions && geometry.wireframePositions.length > 0) {
533
- const wireframeGeom = new THREE.BufferGeometry()
534
- wireframeGeom.setAttribute('position', new THREE.Float32BufferAttribute(geometry.wireframePositions, 3))
535
- return wireframeGeom
390
+ private cancelSection(key: string, unhide: boolean): void {
391
+ const section = this.sections.get(key)
392
+ if (!section) return
393
+ if (section.wireframeGroup) {
394
+ this.disposeWireframeGroup(section.wireframeGroup)
536
395
  }
396
+ if (unhide) {
397
+ const mesh = section.mesh ?? this.getSectionMesh(key)
398
+ if (mesh) {
399
+ if (section.savedMaterial) {
400
+ const fadeMat = mesh.material as THREE.Material
401
+ mesh.material = section.savedMaterial
402
+ fadeMat.dispose()
403
+ }
404
+ mesh.visible = true
405
+ }
406
+ this.setShaderMeshesVisible(key, true)
407
+ }
408
+ this.sections.delete(key)
409
+ }
537
410
 
538
- const positions = geometry.positions as Float32Array
539
- const indices = geometry.indices as Uint32Array | Uint16Array
411
+ private getSectionMesh(key: string): THREE.Mesh | null {
412
+ const sectionObject = this.worldRenderer.chunkMeshManager.sectionObjects[key]
413
+ if (!sectionObject) return null
414
+ const mesh = sectionObject.children.find(c => c.name === 'mesh')
415
+ return mesh instanceof THREE.Mesh ? mesh : null
416
+ }
540
417
 
541
- if (!positions?.length || !indices?.length) {
542
- return this.createSectionBoundsWireframe()
418
+ private getShaderMesh(key: string): THREE.Mesh | null {
419
+ const sectionObject = this.worldRenderer.chunkMeshManager.sectionObjects[key]
420
+ if (!sectionObject) return null
421
+ const mesh = sectionObject.children.find(c => c.name === 'shaderMesh')
422
+ return mesh instanceof THREE.Mesh ? mesh : null
423
+ }
424
+
425
+ private setShaderMeshesVisible(key: string, visible: boolean): void {
426
+ const shaderMesh = this.getShaderMesh(key)
427
+ if (shaderMesh) shaderMesh.visible = visible
428
+ }
429
+
430
+ private unhideAllSectionMeshes(): void {
431
+ for (const obj of Object.values(this.worldRenderer.chunkMeshManager.sectionObjects)) {
432
+ if (!obj) continue
433
+ obj.traverse((child) => {
434
+ if (child instanceof THREE.Mesh && (child.name === 'mesh' || child.name === 'shaderMesh')) {
435
+ child.visible = true
436
+ }
437
+ })
543
438
  }
439
+ }
440
+
441
+ private disposeWireframeGroup(group: THREE.Group): void {
442
+ this.worldRenderer.sceneOrigin.removeAndUntrackAll(group)
443
+ this.worldRenderer.realScene.remove(group)
444
+ group.traverse((child) => {
445
+ const line = child as THREE.LineSegments
446
+ line.geometry?.dispose()
447
+ const mat = line.material
448
+ if (Array.isArray(mat)) mat.forEach(m => m.dispose())
449
+ else mat?.dispose()
450
+ })
451
+ group.clear()
452
+ }
544
453
 
454
+ private createWireframeGeometry(geometry: MesherGeometryOutput): THREE.BufferGeometry {
455
+ const positions = geometry.positions as Float32Array
456
+ const indices = geometry.indices as Uint32Array | Uint16Array
545
457
  const linePositions: number[] = []
546
458
  const edgeSet = new Set<string>()
547
-
548
- // Create edges from triangles
549
459
  for (let i = 0; i < indices.length; i += 3) {
550
460
  const i0 = indices[i]!
551
461
  const i1 = indices[i + 1]!
552
462
  const i2 = indices[i + 2]!
553
-
554
463
  this.addEdge(positions, i0, i1, linePositions, edgeSet)
555
464
  this.addEdge(positions, i1, i2, linePositions, edgeSet)
556
465
  this.addEdge(positions, i2, i0, linePositions, edgeSet)
557
466
  }
558
-
559
467
  const wireframeGeom = new THREE.BufferGeometry()
560
468
  wireframeGeom.setAttribute('position', new THREE.Float32BufferAttribute(linePositions, 3))
561
-
562
469
  return wireframeGeom
563
470
  }
564
471
 
565
- /**
566
- * Add edge to line positions if not duplicate
567
- */
568
472
  private addEdge(
569
473
  positions: Float32Array,
570
474
  i0: number,
571
475
  i1: number,
572
476
  linePositions: number[],
573
- edgeSet: Set<string>
477
+ edgeSet: Set<string>,
574
478
  ): void {
575
479
  const minI = Math.min(i0, i1)
576
480
  const maxI = Math.max(i0, i1)
577
- const key = `${minI}-${maxI}`
578
-
579
- if (edgeSet.has(key)) return
580
- edgeSet.add(key)
581
-
481
+ const edgeKey = `${minI}-${maxI}`
482
+ if (edgeSet.has(edgeKey)) return
483
+ edgeSet.add(edgeKey)
582
484
  linePositions.push(
583
485
  positions[i0 * 3]!, positions[i0 * 3 + 1]!, positions[i0 * 3 + 2]!,
584
- positions[i1 * 3]!, positions[i1 * 3 + 1]!, positions[i1 * 3 + 2]!
486
+ positions[i1 * 3]!, positions[i1 * 3 + 1]!, positions[i1 * 3 + 2]!,
585
487
  )
586
488
  }
587
-
588
- /**
589
- * Update the reveal animation - call this every frame
590
- */
591
- update(deltaTime: number): void {
592
- if (!this.enabled || this.revealingSections.size === 0) return
593
-
594
- this.pulseTime += deltaTime * 0.001 // Convert to seconds
595
- const currentTime = performance.now()
596
-
597
- // Pulse effect parameters
598
- const basePulse = 0.6 + 0.4 * Math.sin(this.pulseTime * 4)
599
-
600
- const toComplete: RevealingSection[] = []
601
-
602
- for (const [key, section] of this.revealingSections) {
603
- const elapsed = currentTime - section.revealStartTime
604
-
605
- if (section.phase === 'wireframe') {
606
- // Animate wireframe
607
- const wireframe = section.wireframeGroup.children[0] as THREE.LineSegments
608
- const glow = section.wireframeGroup.children[1] as THREE.LineSegments
609
-
610
- if (wireframe?.material) {
611
- const mat = wireframe.material as THREE.LineBasicMaterial
612
- mat.opacity = basePulse
613
-
614
- // Color pulse with slight variation
615
- const colorIntensity = 0.85 + 0.15 * Math.sin(this.pulseTime * 6 + elapsed * 0.002)
616
- mat.color.setRGB(
617
- (13 / 255) * colorIntensity,
618
- (234 / 255) * colorIntensity,
619
- (238 / 255) * colorIntensity
620
- )
621
- }
622
-
623
- if (glow?.material) {
624
- const glowMat = glow.material as THREE.LineBasicMaterial
625
- glowMat.opacity = basePulse * 0.4
626
- }
627
-
628
- // Transition to fading phase
629
- if (elapsed > section.wireframeMs) {
630
- section.phase = 'transitioning'
631
-
632
- section.renderMeshRefs = this.getSectionRenderMeshes(key)
633
- for (const mesh of section.renderMeshRefs) {
634
- mesh.visible = true
635
- this.setMeshFadeOpacity(mesh, 0)
636
- }
637
- }
638
- } else if (section.phase === 'transitioning') {
639
- const transitionElapsed = elapsed - section.wireframeMs
640
- const progress = Math.min(1, transitionElapsed / section.revealMs)
641
-
642
- // Smooth ease-out curve
643
- const eased = 1 - (1 - progress) ** 3
644
-
645
- // Fade out wireframe
646
- const wireframe = section.wireframeGroup.children[0] as THREE.LineSegments
647
- const glow = section.wireframeGroup.children[1] as THREE.LineSegments
648
-
649
- if (wireframe?.material) {
650
- const mat = wireframe.material as THREE.LineBasicMaterial
651
- mat.opacity = (1 - eased)
652
- }
653
-
654
- if (glow?.material) {
655
- const glowMat = glow.material as THREE.LineBasicMaterial
656
- glowMat.opacity = (1 - eased) * 0.55
657
- }
658
-
659
- for (const mesh of section.renderMeshRefs) {
660
- this.setMeshFadeOpacity(mesh, eased)
661
- }
662
-
663
- // Complete transition
664
- if (progress >= 1) {
665
- section.phase = 'complete'
666
- toComplete.push(section)
667
- }
668
- }
669
- }
670
-
671
- // Complete all finished sections after iteration
672
- for (const section of toComplete) {
673
- this.completeReveal(section)
674
- }
675
- }
676
-
677
- /**
678
- * Complete the reveal and clean up
679
- */
680
- private completeReveal(section: RevealingSection): void {
681
- // Remove from map first to prevent re-processing
682
- this.revealingSections.delete(section.key)
683
- this.revealedChunks.add(section.key)
684
-
685
- if (this.revealTriggered && this.revealingSections.size === 0) {
686
- this.initialRevealWaveSettled = true
687
- }
688
-
689
- for (const mesh of section.renderMeshRefs) {
690
- this.restoreMeshMaterial(mesh)
691
- mesh.visible = true
692
- delete (mesh as any).hiddenByReveal
693
- }
694
-
695
- this.worldRenderer.chunkMeshManager.migrateDeferredShaderToGlobal(section.key)
696
-
697
- if (section.globalShaderRestore) {
698
- this.worldRenderer.chunkMeshManager.globalBlockBuffer?.addSection(
699
- section.key,
700
- section.globalShaderRestore.words,
701
- section.globalShaderRestore.count,
702
- )
703
- }
704
-
705
- // Clean up wireframe group
706
- this.disposeWireframeGroup(section.wireframeGroup)
707
- }
708
-
709
- /**
710
- * Dispose a wireframe group and remove from scene
711
- */
712
- private disposeWireframeGroup(group: THREE.Group): void {
713
- this.worldRenderer.sceneOrigin.removeAndUntrackAll(group)
714
-
715
- // Collect all objects to dispose
716
- const toDispose: THREE.Object3D[] = []
717
- group.traverse((child) => {
718
- toDispose.push(child)
719
- })
720
-
721
- // Dispose all collected objects
722
- for (const child of toDispose) {
723
- const lineSegments = child as THREE.LineSegments
724
- if (lineSegments.geometry) {
725
- lineSegments.geometry.dispose()
726
- }
727
- if (lineSegments.material) {
728
- const mat = lineSegments.material
729
- if (Array.isArray(mat)) {
730
- for (const m of mat) m.dispose()
731
- } else if (mat && typeof mat.dispose === 'function') {
732
- mat.dispose()
733
- }
734
- }
735
- }
736
-
737
- // Clear children
738
- group.clear()
739
- }
740
-
741
- /**
742
- * Reset the reveal system
743
- */
744
- reset(): void {
745
- // Clean up all revealing sections
746
- for (const section of this.revealingSections.values()) {
747
- this.disposeWireframeGroup(section.wireframeGroup)
748
- }
749
-
750
- this.pendingGeometries.clear()
751
- this.revealingSections.clear()
752
- this.revealedChunks.clear()
753
- this.finishedChunkCount = 0
754
- this.revealTriggered = false
755
- this.initialWaveDone = false
756
- this.initialRevealWaveSettled = false
757
- this.revealStartTime = 0
758
- this.pulseTime = 0
759
- }
760
-
761
- /**
762
- * Force complete all reveals (skip animation)
763
- */
764
- forceCompleteAll(): void {
765
- const sections = [...this.revealingSections.values()]
766
- for (const section of sections) {
767
- section.renderMeshRefs = this.getSectionRenderMeshes(section.key)
768
- for (const mesh of section.renderMeshRefs) {
769
- this.restoreMeshMaterial(mesh)
770
- mesh.visible = true
771
- delete (mesh as any).hiddenByReveal
772
- }
773
- this.completeReveal(section)
774
- }
775
- }
776
-
777
- // ============ DEBUG METHODS ============
778
-
779
- /**
780
- * Debug: Get all wireframe groups still in scene
781
- */
782
- debugGetWireframeGroups(): THREE.Group[] {
783
- const groups: THREE.Group[] = []
784
- this.scene.traverse((child) => {
785
- if (child.name === 'scifi-reveal-group') {
786
- groups.push(child as THREE.Group)
787
- }
788
- })
789
- return groups
790
- }
791
-
792
- /**
793
- * Debug: Force remove all wireframe groups from scene
794
- */
795
- debugForceCleanup(): void {
796
- const groups = this.debugGetWireframeGroups()
797
- console.log(`[SciFiReveal] Found ${groups.length} wireframe groups in scene`)
798
-
799
- for (const group of groups) {
800
- console.log(`[SciFiReveal] Removing group:`, group)
801
- this.disposeWireframeGroup(group)
802
- }
803
-
804
- // Also clean up any tracked sections
805
- for (const section of this.revealingSections.values()) {
806
- this.disposeWireframeGroup(section.wireframeGroup)
807
- }
808
- this.revealingSections.clear()
809
-
810
- console.log(`[SciFiReveal] Cleanup complete. Remaining groups: ${this.debugGetWireframeGroups().length}`)
811
- }
812
-
813
- /**
814
- * Debug: Get status of the reveal system
815
- */
816
- debugStatus() {
817
- const wireframeGroups = this.debugGetWireframeGroups()
818
- const trackedKeys = new Set(this.revealingSections.keys())
819
- const orphanedGroups = wireframeGroups.filter(g => !trackedKeys.has((g as any).sectionKey))
820
-
821
- return {
822
- revealTriggered: this.revealTriggered,
823
- finishedChunkCount: this.finishedChunkCount,
824
- pendingGeometries: this.pendingGeometries.size,
825
- revealingSections: this.revealingSections.size,
826
- revealedChunks: this.revealedChunks.size,
827
- wireframeGroupsInScene: wireframeGroups.length,
828
- orphanedWireframeGroups: orphanedGroups.length,
829
- orphanedKeys: orphanedGroups.map(g => (g as any).sectionKey),
830
- sections: [...this.revealingSections.entries()].map(([key, s]) => ({
831
- key,
832
- phase: s.phase,
833
- renderMeshCount: s.renderMeshRefs.length,
834
- wireframeInScene: s.wireframeGroup.parent !== null
835
- }))
836
- }
837
- }
838
-
839
- /**
840
- * Debug: Log current status to console
841
- */
842
- debugLog(): void {
843
- console.log('[SciFiReveal] Status:', this.debugStatus())
844
- }
845
489
  }
846
490
 
847
491
  export const sciFiWorldRevealManifest: RendererModuleManifest = {