minecraft-renderer 0.1.62 → 0.1.64

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 (36) hide show
  1. package/README.md +1 -1
  2. package/dist/mesherWasm.js +22 -22
  3. package/dist/minecraft-renderer.js +54 -54
  4. package/dist/minecraft-renderer.js.meta.json +1 -1
  5. package/dist/threeWorker.js +407 -407
  6. package/package.json +1 -1
  7. package/src/graphicsBackend/config.ts +3 -3
  8. package/src/graphicsBackend/rendererDefaultOptions.ts +41 -24
  9. package/src/graphicsBackend/rendererOptionsSync.ts +23 -23
  10. package/src/graphicsBackend/types.ts +3 -3
  11. package/src/index.ts +8 -8
  12. package/src/lib/bindAbortableListener.test.ts +65 -0
  13. package/src/lib/bindAbortableListener.ts +41 -0
  14. package/src/lib/workerProxy.ts +238 -118
  15. package/src/lib/workerSyncOps.test.ts +154 -0
  16. package/src/lib/worldrendererCommon.removeColumn.test.ts +182 -0
  17. package/src/lib/worldrendererCommon.ts +86 -54
  18. package/src/three/documentRenderer.ts +1 -1
  19. package/src/three/entities.ts +21 -11
  20. package/src/three/graphicsBackendBase.ts +18 -8
  21. package/src/three/menuBackground/activeView.ts +1 -1
  22. package/src/three/menuBackground/config.ts +9 -9
  23. package/src/three/menuBackground/index.ts +10 -10
  24. package/src/three/menuBackground/renderer.ts +12 -12
  25. package/src/three/menuBackground/types.ts +9 -9
  26. package/src/three/menuBackground/{futuristic.ts → v2.ts} +110 -59
  27. package/src/three/menuBackground/{futuristicMeta.ts → v2Meta.ts} +6 -6
  28. package/src/three/modules/rain.ts +1 -1
  29. package/src/three/worldRendererThree.ts +2 -1
  30. package/src/wasm-mesher/tests/mesherWasmRequestTracker.test.ts +29 -0
  31. package/src/wasm-mesher/worker/mesherWasm.ts +7 -0
  32. package/src/wasm-mesher/worker/mesherWasmRequestTracker.ts +10 -0
  33. package/src/worldView/worldView.spiral.test.ts +38 -0
  34. package/src/worldView/worldView.ts +41 -8
  35. package/src/worldView/worldViewWorkerBridge.test.ts +59 -0
  36. package/src/lib/workerProxy.restore.test.ts +0 -29
@@ -3,14 +3,14 @@ import * as THREE from 'three'
3
3
  import type { GraphicsInitOptions } from '../../graphicsBackend/types'
4
4
  import type { DocumentRenderer } from '../documentRenderer'
5
5
  import { ClassicMenuBackground } from './classic'
6
- import { FuturisticMenuBackground } from './futuristic'
6
+ import { V2MenuBackground } from './v2'
7
7
  import { WorldBlocksMenuBackground } from './worldBlocks'
8
8
  import type { MenuBackgroundView } from './activeView'
9
9
  import type { MenuBackgroundOptions } from './types'
10
10
  import { resolveMenuBackgroundMode } from './types'
11
11
 
12
12
  /**
13
- * Orchestrates main-menu background rendering (dispatches to classic / futuristic / world-blocks).
13
+ * Orchestrates main-menu background rendering (dispatches to classic / v2 / world-blocks).
14
14
  */
15
15
  export class MenuBackgroundRenderer {
16
16
  private active?: MenuBackgroundView
@@ -27,9 +27,9 @@ export class MenuBackgroundRenderer {
27
27
  this.mode = resolveMenuBackgroundMode(menuBackgroundOptions, singleFileBuild)
28
28
  }
29
29
 
30
- /** Active futuristic instance when that style is running. */
31
- get futuristic(): FuturisticMenuBackground | undefined {
32
- return this.active instanceof FuturisticMenuBackground ? this.active : undefined
30
+ /** Active v2 instance when that style is running. */
31
+ get v2(): V2MenuBackground | undefined {
32
+ return this.active instanceof V2MenuBackground ? this.active : undefined
33
33
  }
34
34
 
35
35
  get scene(): THREE.Scene | undefined {
@@ -64,16 +64,16 @@ export class MenuBackgroundRenderer {
64
64
 
65
65
  private createImplementation(options: MenuBackgroundOptions): MenuBackgroundView {
66
66
  switch (this.mode) {
67
- case 'futuristic':
68
- return new FuturisticMenuBackground(
67
+ case 'v2':
68
+ return new V2MenuBackground(
69
69
  this.documentRenderer,
70
70
  {
71
71
  useMinecraftTextures: options.useMinecraftTextures,
72
- initialScene: options.futuristicScene,
73
- initialCamera: options.futuristicCamera,
74
- initialBlockGroup: options.futuristicBlockGroup,
75
- initialCameraSpeed: options.futuristicCameraSpeed,
76
- initialBlockSpeed: options.futuristicBlockSpeed,
72
+ initialScene: options.v2Scene,
73
+ initialCamera: options.v2Camera,
74
+ initialBlockGroup: options.v2BlockGroup,
75
+ initialCameraSpeed: options.v2CameraSpeed,
76
+ initialBlockSpeed: options.v2BlockSpeed,
77
77
  resourcesManager: options.resourcesManager
78
78
  },
79
79
  this.abortController.signal
@@ -1,25 +1,25 @@
1
1
  //@ts-nocheck
2
2
  import type { ResourcesManager } from '../../resourcesManager/resourcesManager'
3
- import type { FuturisticCameraId, FuturisticSceneId, MinecraftBlockGroupId } from './futuristic'
3
+ import type { V2CameraId, V2SceneId, MinecraftBlockGroupId } from './v2'
4
4
  import { MENU_BACKGROUND_OPTION_DEFAULTS } from './config'
5
5
 
6
- export type { FuturisticCameraId, FuturisticSceneId, MinecraftBlockGroupId } from './futuristic'
6
+ export type { V2CameraId, V2SceneId, MinecraftBlockGroupId } from './v2'
7
7
 
8
- export type MenuBackgroundMode = 'classic' | 'futuristic' | 'worldBlocks'
8
+ export type MenuBackgroundMode = 'classic' | 'v2' | 'worldBlocks'
9
9
 
10
10
  export interface MenuBackgroundOptions {
11
11
  /** Visual style. Defaults to {@link MENU_BACKGROUND_OPTION_DEFAULTS.mode}, or `worldBlocks` in single-file build. */
12
12
  mode?: MenuBackgroundMode
13
- /** Futuristic style: load block atlas and render textured cubes (requires assets / mcData). */
13
+ /** V2 style: load block atlas and render textured cubes (requires assets / mcData). */
14
14
  useMinecraftTextures?: boolean
15
- futuristicScene?: FuturisticSceneId
16
- futuristicCamera?: FuturisticCameraId
15
+ v2Scene?: V2SceneId
16
+ v2Camera?: V2CameraId
17
17
  /** Block pool when {@link useMinecraftTextures} is enabled. */
18
- futuristicBlockGroup?: MinecraftBlockGroupId
18
+ v2BlockGroup?: MinecraftBlockGroupId
19
19
  /** Camera path speed (1 = 100%). */
20
- futuristicCameraSpeed?: number
20
+ v2CameraSpeed?: number
21
21
  /** Block fly-through + sky drift speed (1 = 100%). */
22
- futuristicBlockSpeed?: number
22
+ v2BlockSpeed?: number
23
23
  /**
24
24
  * Optional shared resource manager (e.g. appViewer.resourcesManager).
25
25
  * Caller should run `updateAssetsData` after mcData is loaded when using textured cubes.
@@ -8,31 +8,31 @@ import { resizeMenuBackgroundCamera } from './activeView'
8
8
  import { loadThreeJsTextureFromBitmap } from '../threeJsUtils'
9
9
  import { MENU_BACKGROUND_MOTION_DEFAULTS, MENU_BACKGROUND_OPTION_DEFAULTS } from './config'
10
10
  import {
11
- FUTURISTIC_CAMERA_IDS,
12
- FUTURISTIC_SCENE_IDS,
11
+ V2_CAMERA_IDS,
12
+ V2_SCENE_IDS,
13
13
  MINECRAFT_BLOCK_GROUP_IDS,
14
- type FuturisticCameraId,
15
- type FuturisticSceneId,
14
+ type V2CameraId,
15
+ type V2SceneId,
16
16
  type MinecraftBlockGroupId,
17
- } from './futuristicMeta'
17
+ } from './v2Meta'
18
18
 
19
19
  export {
20
- FUTURISTIC_SCENE_IDS,
21
- FUTURISTIC_SCENE_LABELS,
22
- FUTURISTIC_CAMERA_IDS,
23
- FUTURISTIC_CAMERA_LABELS,
20
+ V2_SCENE_IDS,
21
+ V2_SCENE_LABELS,
22
+ V2_CAMERA_IDS,
23
+ V2_CAMERA_LABELS,
24
24
  MINECRAFT_BLOCK_GROUP_IDS,
25
25
  MINECRAFT_BLOCK_GROUP_LABELS,
26
- } from './futuristicMeta'
27
- export type { FuturisticSceneId, FuturisticCameraId, MinecraftBlockGroupId } from './futuristicMeta'
26
+ } from './v2Meta'
27
+ export type { V2SceneId, V2CameraId, MinecraftBlockGroupId } from './v2Meta'
28
28
 
29
29
  /** Mouse parallax scale (HTML prototype uses 1). */
30
30
  const MOUSE_INFLUENCE = 0.1
31
31
 
32
- export interface FuturisticMenuBackgroundOptions {
32
+ export interface V2MenuBackgroundOptions {
33
33
  useMinecraftTextures?: boolean
34
- initialScene?: FuturisticSceneId
35
- initialCamera?: FuturisticCameraId
34
+ initialScene?: V2SceneId
35
+ initialCamera?: V2CameraId
36
36
  initialBlockGroup?: MinecraftBlockGroupId
37
37
  /** Camera path speed multiplier (0 = frozen path; mouse parallax unchanged). */
38
38
  initialCameraSpeed?: number
@@ -41,7 +41,7 @@ export interface FuturisticMenuBackgroundOptions {
41
41
  resourcesManager?: ResourcesManager
42
42
  }
43
43
 
44
- /** Block pools for textured floating cubes (selected via {@link FuturisticMenuBackground.setBlockGroup}). */
44
+ /** Block pools for textured floating cubes (selected via {@link V2MenuBackground.setBlockGroup}). */
45
45
  export const MINECRAFT_BLOCK_GROUPS = {
46
46
  mixed: [
47
47
  'white_wool', 'cyan_wool', 'blue_wool', 'purple_wool',
@@ -123,7 +123,7 @@ interface FloatingBlock {
123
123
  minecraftBlockName?: string
124
124
  }
125
125
 
126
- const PAL: Record<FuturisticSceneId, ScenePalette> = {
126
+ const PAL: Record<V2SceneId, ScenePalette> = {
127
127
  galaxy: {
128
128
  bg: 0x02_04_12, fog: 0x02_04_12, fogD: 0.011,
129
129
  blocks: [0x00_f0_ff, 0x00_d4_ff, 0x00_b8_ff, 0x00_e8_ff, 0x22_cc_ff, 0x00_a8_ff],
@@ -192,7 +192,7 @@ const PAL: Record<FuturisticSceneId, ScenePalette> = {
192
192
  }
193
193
  }
194
194
 
195
- const CAMS: Record<FuturisticCameraId, CameraMode> = {
195
+ const CAMS: Record<V2CameraId, CameraMode> = {
196
196
  cruise: {
197
197
  pos: (t, mx, my) => ({ x: Math.sin(t * 0.28) * 18 + Math.cos(t * 0.11) * 7 + mx * 10, y: Math.sin(t * 0.19) * 6 + Math.cos(t * 0.31) * 3 + my * 6, z: 0 }),
198
198
  look: (t, mx, my) => ({ x: Math.sin((t + 0.18) * 0.28) * 18 + mx * 8, y: Math.sin((t + 0.18) * 0.19) * 6 + my * 4, z: -25 }),
@@ -232,7 +232,7 @@ const CAMS: Record<FuturisticCameraId, CameraMode> = {
232
232
  }
233
233
  }
234
234
 
235
- const CAM_SPD: Record<FuturisticCameraId, number> = {
235
+ const CAM_SPD: Record<V2CameraId, number> = {
236
236
  cruise: 1,
237
237
  barrel: 1.6,
238
238
  dive: 2.2,
@@ -270,7 +270,7 @@ const makeSkyGradientTexture = (gradient: NonNullable<ScenePalette['gradientBg']
270
270
  return tex
271
271
  }
272
272
 
273
- export class FuturisticMenuBackground implements MenuBackgroundView {
273
+ export class V2MenuBackground implements MenuBackgroundView {
274
274
  readonly scene: THREE.Scene
275
275
  readonly camera: THREE.PerspectiveCamera
276
276
 
@@ -288,8 +288,8 @@ export class FuturisticMenuBackground implements MenuBackgroundView {
288
288
  private readonly bGeo = new THREE.BoxGeometry(1, 1, 1)
289
289
  private readonly eGeo = new THREE.EdgesGeometry(this.bGeo)
290
290
 
291
- private curScene: FuturisticSceneId
292
- private curCam: FuturisticCameraId
291
+ private curScene: V2SceneId
292
+ private curCam: V2CameraId
293
293
  private blockGroup: MinecraftBlockGroupId
294
294
  private cameraSpeed: number
295
295
  private blockSpeed: number
@@ -307,16 +307,19 @@ export class FuturisticMenuBackground implements MenuBackgroundView {
307
307
  private gradientSkyTexture: THREE.CanvasTexture | null = null
308
308
  private disposed = false
309
309
  private animTime = 0
310
+ private texturesApplied = false
311
+ private textureLoadInProgress = false
312
+ private onAssetsTexturesUpdated?: () => void
310
313
 
311
314
  constructor(
312
315
  private readonly documentRenderer: DocumentRenderer,
313
- options: FuturisticMenuBackgroundOptions = {},
316
+ options: V2MenuBackgroundOptions = {},
314
317
  private readonly abortSignal?: AbortSignal
315
318
  ) {
316
319
  const d = MENU_BACKGROUND_OPTION_DEFAULTS
317
- this.curScene = options.initialScene ?? d.futuristicScene
318
- this.curCam = options.initialCamera ?? d.futuristicCamera
319
- this.blockGroup = options.initialBlockGroup ?? d.futuristicBlockGroup
320
+ this.curScene = options.initialScene ?? d.v2Scene
321
+ this.curCam = options.initialCamera ?? d.v2Camera
322
+ this.blockGroup = options.initialBlockGroup ?? d.v2BlockGroup
320
323
  this.cameraSpeed = options.initialCameraSpeed ?? MENU_BACKGROUND_MOTION_DEFAULTS.camera
321
324
  this.blockSpeed = options.initialBlockSpeed ?? MENU_BACKGROUND_MOTION_DEFAULTS.block
322
325
  this.useMinecraftTextures = options.useMinecraftTextures ?? d.minecraftTextures
@@ -420,13 +423,79 @@ export class FuturisticMenuBackground implements MenuBackgroundView {
420
423
  }
421
424
 
422
425
  async init() {
423
- if (this.useMinecraftTextures) {
424
- try {
425
- await this.loadMinecraftTextures()
426
- } catch (err) {
427
- console.warn('[FuturisticMenuBackground] Failed to load Minecraft textures, using solid colors:', err)
428
- this.useMinecraftTextures = false
426
+ if (!this.useMinecraftTextures) return
427
+ void this.scheduleMinecraftTextureLoad()
428
+ }
429
+
430
+ private scheduleMinecraftTextureLoad() {
431
+ if (!this.useMinecraftTextures || this.disposed || this.texturesApplied || this.textureLoadInProgress) return
432
+ void this.tryApplyMinecraftTextures()
433
+ }
434
+
435
+ private attachAssetsListener() {
436
+ const rm = this.resourcesManager
437
+ if (!rm || this.onAssetsTexturesUpdated) return
438
+ this.onAssetsTexturesUpdated = () => this.scheduleMinecraftTextureLoad()
439
+ rm.on('assetsTexturesUpdated', this.onAssetsTexturesUpdated)
440
+ }
441
+
442
+ private detachAssetsListener() {
443
+ const rm = this.resourcesManager
444
+ if (!rm || !this.onAssetsTexturesUpdated) return
445
+ rm.off('assetsTexturesUpdated', this.onAssetsTexturesUpdated)
446
+ this.onAssetsTexturesUpdated = undefined
447
+ }
448
+
449
+ private hasBlockAtlas(resourcesManager: ResourcesManager): boolean {
450
+ const resources = resourcesManager.currentResources
451
+ return !!(resources?.blocksAtlasImage && resources.blocksAtlasJson)
452
+ }
453
+
454
+ private async ensureAtlasReady(resourcesManager: ResourcesManager): Promise<boolean> {
455
+ await this.ensureMcDataLoaded()
456
+ if (this.hasBlockAtlas(resourcesManager)) return true
457
+
458
+ if (typeof document === 'undefined' && resourcesManager !== this.resourcesManager) {
459
+ return false
460
+ }
461
+
462
+ resourcesManager.currentConfig = {
463
+ ...resourcesManager.currentConfig,
464
+ version: MENU_BACKGROUND_MC_VERSION,
465
+ noInventoryGui: true
466
+ }
467
+
468
+ try {
469
+ await resourcesManager.updateAssetsData?.({})
470
+ } catch {
471
+ return false
472
+ }
473
+
474
+ return this.hasBlockAtlas(resourcesManager)
475
+ }
476
+
477
+ private async tryApplyMinecraftTextures() {
478
+ if (this.disposed || !this.useMinecraftTextures || this.texturesApplied) return
479
+
480
+ this.textureLoadInProgress = true
481
+ try {
482
+ const resourcesManager = this.resourcesManager ?? new ResourcesManager()
483
+ const ready = await this.ensureAtlasReady(resourcesManager)
484
+ if (!ready) {
485
+ if (this.resourcesManager) this.attachAssetsListener()
486
+ return
429
487
  }
488
+ if (this.disposed) return
489
+
490
+ this.applyMinecraftTexturesFromAtlas(resourcesManager)
491
+ this.texturesApplied = true
492
+ this.detachAssetsListener()
493
+ } catch (err) {
494
+ console.warn('[V2MenuBackground] Failed to load Minecraft textures, using solid colors:', err)
495
+ this.useMinecraftTextures = false
496
+ this.detachAssetsListener()
497
+ } finally {
498
+ this.textureLoadInProgress = false
430
499
  }
431
500
  }
432
501
 
@@ -626,23 +695,7 @@ export class FuturisticMenuBackground implements MenuBackgroundView {
626
695
  return null
627
696
  }
628
697
 
629
- private async loadMinecraftTextures() {
630
- await this.ensureMcDataLoaded()
631
-
632
- const resourcesManager = this.resourcesManager ?? new ResourcesManager()
633
- const needsAssetUpdate = !resourcesManager.currentResources?.blocksAtlasImage
634
- if (needsAssetUpdate) {
635
- if (typeof document === 'undefined') {
636
- throw new Error('Menu atlas missing in worker; pass resourcesManager from main thread')
637
- }
638
- resourcesManager.currentConfig = {
639
- ...resourcesManager.currentConfig,
640
- version: MENU_BACKGROUND_MC_VERSION,
641
- noInventoryGui: true
642
- }
643
- await resourcesManager.updateAssetsData?.({})
644
- }
645
-
698
+ private applyMinecraftTexturesFromAtlas(resourcesManager: ResourcesManager) {
646
699
  const resources = resourcesManager.currentResources
647
700
  if (!resources?.blocksAtlasImage || !resources.blocksAtlasJson) {
648
701
  throw new Error('Block atlas not available')
@@ -690,8 +743,8 @@ export class FuturisticMenuBackground implements MenuBackgroundView {
690
743
  }
691
744
  }
692
745
 
693
- setScene(name: FuturisticSceneId) {
694
- if (!(FUTURISTIC_SCENE_IDS as readonly string[]).includes(name)) return
746
+ setScene(name: V2SceneId) {
747
+ if (!(V2_SCENE_IDS as readonly string[]).includes(name)) return
695
748
  if (name === this.curScene || this.transitioning) return
696
749
  this.transitioning = true
697
750
  this.curScene = name
@@ -726,8 +779,8 @@ export class FuturisticMenuBackground implements MenuBackgroundView {
726
779
  }, 150)
727
780
  }
728
781
 
729
- setCamera(name: FuturisticCameraId) {
730
- if (!(FUTURISTIC_CAMERA_IDS as readonly string[]).includes(name)) return
782
+ setCamera(name: V2CameraId) {
783
+ if (!(V2_CAMERA_IDS as readonly string[]).includes(name)) return
731
784
  this.curCam = name
732
785
  }
733
786
 
@@ -749,18 +802,15 @@ export class FuturisticMenuBackground implements MenuBackgroundView {
749
802
  mat.dispose()
750
803
  }
751
804
  this.blockMaterialPool.clear()
752
- try {
753
- await this.loadMinecraftTextures()
754
- } catch (err) {
755
- console.warn('[FuturisticMenuBackground] Failed to reload block group textures:', err)
756
- }
805
+ this.texturesApplied = false
806
+ this.scheduleMinecraftTextureLoad()
757
807
  }
758
808
 
759
- getSceneId(): FuturisticSceneId {
809
+ getSceneId(): V2SceneId {
760
810
  return this.curScene
761
811
  }
762
812
 
763
- getCameraId(): FuturisticCameraId {
813
+ getCameraId(): V2CameraId {
764
814
  return this.curCam
765
815
  }
766
816
 
@@ -830,6 +880,7 @@ export class FuturisticMenuBackground implements MenuBackgroundView {
830
880
 
831
881
  dispose() {
832
882
  this.disposed = true
883
+ this.detachAssetsListener()
833
884
  this.scene.clear()
834
885
  this.bGeo.dispose()
835
886
  this.eGeo.dispose()
@@ -1,13 +1,13 @@
1
1
  //@ts-nocheck
2
2
  /** Settings / labels only — no Three.js or DocumentRenderer (safe for defaultOptions imports). */
3
3
 
4
- export const FUTURISTIC_SCENE_IDS = ['galaxy', 'nether', 'end', 'cyber', 'light'] as const
5
- export type FuturisticSceneId = typeof FUTURISTIC_SCENE_IDS[number]
4
+ export const V2_SCENE_IDS = ['galaxy', 'nether', 'end', 'cyber', 'light'] as const
5
+ export type V2SceneId = typeof V2_SCENE_IDS[number]
6
6
 
7
- export const FUTURISTIC_CAMERA_IDS = ['cruise', 'barrel', 'dive', 'orbit', 'snake'] as const
8
- export type FuturisticCameraId = typeof FUTURISTIC_CAMERA_IDS[number]
7
+ export const V2_CAMERA_IDS = ['cruise', 'barrel', 'dive', 'orbit', 'snake'] as const
8
+ export type V2CameraId = typeof V2_CAMERA_IDS[number]
9
9
 
10
- export const FUTURISTIC_SCENE_LABELS: Record<FuturisticSceneId, string> = {
10
+ export const V2_SCENE_LABELS: Record<V2SceneId, string> = {
11
11
  galaxy: 'Galaxy',
12
12
  nether: 'Nether',
13
13
  end: 'The End',
@@ -15,7 +15,7 @@ export const FUTURISTIC_SCENE_LABELS: Record<FuturisticSceneId, string> = {
15
15
  light: 'Light Space'
16
16
  }
17
17
 
18
- export const FUTURISTIC_CAMERA_LABELS: Record<FuturisticCameraId, string> = {
18
+ export const V2_CAMERA_LABELS: Record<V2CameraId, string> = {
19
19
  cruise: 'Cruise',
20
20
  barrel: 'Barrel',
21
21
  dive: 'Dive',
@@ -93,7 +93,7 @@ export class RainModule implements RendererModuleController {
93
93
  const chunkX = Math.floor(worldX / 16)
94
94
  const chunkZ = Math.floor(worldZ / 16)
95
95
  if (chunkX !== prevChunkX || chunkZ !== prevChunkZ) {
96
- cachedHeightmap = heightmaps.get(`${chunkX},${chunkZ}`)
96
+ cachedHeightmap = heightmaps[`${chunkX},${chunkZ}`]
97
97
  prevChunkX = chunkX
98
98
  prevChunkZ = chunkZ
99
99
  }
@@ -167,7 +167,8 @@ export class WorldRendererThree extends WorldRendererCommon {
167
167
  this.performanceMonitor = new PerformanceMonitor(this.reactiveState.world.instabilityFactors)
168
168
 
169
169
  this.renderer = renderer
170
- displayOptions.rendererState.renderer = WorldRendererThree.getRendererInfo(renderer) ?? '...'
170
+ const rendererInfo = WorldRendererThree.getRendererInfo(renderer) ?? '...'
171
+ displayOptions.rendererState.renderer = rendererInfo
171
172
 
172
173
  // Initialize chunk mesh manager
173
174
  this.chunkMeshManager = new ChunkMeshManager(this, this.scene, this.material, this.worldSizeParams.worldHeight, this.viewDistance)
@@ -0,0 +1,29 @@
1
+ //@ts-nocheck
2
+ import { describe, expect, test } from 'vitest'
3
+ import { SectionRequestTracker } from '../worker/mesherWasmRequestTracker'
4
+
5
+ describe('SectionRequestTracker.clearColumn', () => {
6
+ test('removes all pending keys for the column', () => {
7
+ const tracker = new SectionRequestTracker()
8
+ tracker.addRequest('160,64,0')
9
+ tracker.addRequest('160,80,0')
10
+ tracker.addRequest('0,64,0')
11
+
12
+ tracker.clearColumn(160, 0)
13
+
14
+ expect(tracker.hasPending('160,64,0')).toBe(false)
15
+ expect(tracker.hasPending('160,80,0')).toBe(false)
16
+ expect(tracker.hasPending('0,64,0')).toBe(true)
17
+ expect(tracker.size()).toBe(1)
18
+ })
19
+
20
+ test('is a no-op when the column has no pending keys', () => {
21
+ const tracker = new SectionRequestTracker()
22
+ tracker.addRequest('0,64,0')
23
+
24
+ tracker.clearColumn(160, 0)
25
+
26
+ expect(tracker.hasPending('0,64,0')).toBe(true)
27
+ expect(tracker.size()).toBe(1)
28
+ })
29
+ })
@@ -767,6 +767,13 @@ const handleMessage = async (data: any) => {
767
767
  if (!world) break
768
768
  world.removeColumn(data.x, data.z)
769
769
  world.customBlockModels.delete(`${data.x},${data.z}`)
770
+ requestTracker.clearColumn(data.x, data.z)
771
+ for (const key of [...dirtySections.keys()]) {
772
+ const [sx, , sz] = key.split(',').map(Number)
773
+ if (sx === data.x && sz === data.z) {
774
+ dirtySections.delete(key)
775
+ }
776
+ }
770
777
  if (Object.keys(world.columns).length === 0) softCleanup()
771
778
  break
772
779
  }
@@ -49,6 +49,16 @@ export class SectionRequestTracker {
49
49
  this.counts.clear()
50
50
  }
51
51
 
52
+ /** Drop all pending requests for one column (`cx`,`cz` = column origin in block coords). */
53
+ clearColumn (cx: number, cz: number): void {
54
+ for (const key of [...this.counts.keys()]) {
55
+ const [x, , z] = key.split(',').map(Number)
56
+ if (x === cx && z === cz) {
57
+ this.counts.delete(key)
58
+ }
59
+ }
60
+ }
61
+
52
62
  /** Number of distinct keys with pending requests. */
53
63
  size (): number {
54
64
  return this.counts.size
@@ -0,0 +1,38 @@
1
+ //@ts-nocheck
2
+ import { describe, expect, it, vi } from 'vitest'
3
+ import { Vec3 } from 'vec3'
4
+ import { WorldView } from './worldView'
5
+
6
+ describe('WorldView._loadChunks spiral guard', () => {
7
+ it('does not let a superseded spiral reset inLoading or panic state', async () => {
8
+ const world = {
9
+ getColumnAt: () => null,
10
+ setBlockStateId: vi.fn(),
11
+ }
12
+ const view = new WorldView(world, 8, new Vec3(0, 64, 0))
13
+ view.addWaitTime = 0
14
+ view.loadChunk = vi.fn(async () => {})
15
+
16
+ const positions = [new Vec3(0, 0, 0)]
17
+ const spiralA = view._loadChunks(positions, new Vec3(0, 64, 0))
18
+ await Promise.resolve()
19
+
20
+ expect(view.inLoading).toBe(true)
21
+ expect(view.spiralNumber).toBe(1)
22
+
23
+ const spiralB = view._loadChunks(positions, new Vec3(0, 64, 0))
24
+ await Promise.resolve()
25
+
26
+ expect(view.spiralNumber).toBe(2)
27
+ expect(view.inLoading).toBe(true)
28
+ expect(view.gotPanicLastTime).toBe(false)
29
+
30
+ await spiralA
31
+ expect(view.inLoading).toBe(true)
32
+ expect(view.gotPanicLastTime).toBe(false)
33
+
34
+ view.waitingSpiralChunksLoad['0,0']?.(true)
35
+ await spiralB
36
+ expect(view.inLoading).toBe(false)
37
+ })
38
+ })
@@ -44,6 +44,13 @@ export const delayedIterator = async <T>(
44
44
  }
45
45
  }
46
46
 
47
+ type WorkerWorldViewBridge = {
48
+ activeWorldView: WorldViewWorker
49
+ handler: (event: MessageEvent) => void
50
+ }
51
+
52
+ const workerWorldViewBridges = new WeakMap<Worker, WorkerWorldViewBridge>()
53
+
47
54
  /**
48
55
  * WorldView for worker thread communication.
49
56
  * This is a lightweight version that receives events from the main thread.
@@ -51,19 +58,43 @@ export const delayedIterator = async <T>(
51
58
  export class WorldViewWorker extends (EventEmitter as new () => TypedEmitter<WorldViewEvents>) {
52
59
  static readonly restorerName = 'WorldViewWorker'
53
60
 
54
- static restoreTransferred(data: any, worker?: Worker): WorldViewWorker {
61
+ static restoreTransferred(_data: any, worker?: Worker): WorldViewWorker {
55
62
  const worldView = new WorldViewWorker()
56
- if (worker) {
57
- worker.addEventListener('message', ({ data }) => {
58
- if (data.class === WorldViewWorker.restorerName) {
59
- if (data.type === 'event') {
60
- worldView.emit(data.eventName, ...data.args)
61
- }
63
+ if (!worker) {
64
+ return worldView
65
+ }
66
+
67
+ let bridge = workerWorldViewBridges.get(worker)
68
+ if (!bridge) {
69
+ const handler = ({ data }: MessageEvent) => {
70
+ const state = workerWorldViewBridges.get(worker)
71
+ const active = state?.activeWorldView
72
+ if (!active || data?.class !== WorldViewWorker.restorerName) return
73
+ if (data.type === 'event') {
74
+ active.emit(data.eventName, ...data.args)
62
75
  }
63
- })
76
+ }
77
+ worker.addEventListener('message', handler as EventListener)
78
+ bridge = { activeWorldView: worldView, handler }
79
+ workerWorldViewBridges.set(worker, bridge)
80
+ } else {
81
+ bridge.activeWorldView = worldView
64
82
  }
65
83
  return worldView
66
84
  }
85
+
86
+ /** @internal vitest — remove bridge listener from worker */
87
+ static clearWorkerBridgeForTest(worker: Worker): void {
88
+ const bridge = workerWorldViewBridges.get(worker)
89
+ if (!bridge) return
90
+ worker.removeEventListener('message', bridge.handler as EventListener)
91
+ workerWorldViewBridges.delete(worker)
92
+ }
93
+
94
+ /** @internal vitest — count of bridge listeners on worker */
95
+ static getWorkerBridgeListenerCountForTest(worker: Worker): number {
96
+ return workerWorldViewBridges.has(worker) ? 1 : 0
97
+ }
67
98
  }
68
99
 
69
100
  /**
@@ -234,6 +265,8 @@ export class WorldView extends (EventEmitter as new () => TypedEmitter<WorldView
234
265
  this.chunkProgress()
235
266
  })
236
267
 
268
+ if (spiralNumber !== this.spiralNumber) return
269
+
237
270
  if (this.panicTimeout) clearTimeout(this.panicTimeout)
238
271
  this.inLoading = false
239
272
  this.gotPanicLastTime = false
@@ -0,0 +1,59 @@
1
+ //@ts-nocheck
2
+ import { describe, expect, it, vi } from 'vitest'
3
+ import { WorldViewWorker } from './worldView'
4
+
5
+ describe('WorldViewWorker.restoreTransferred bridge', () => {
6
+ it('registers only one message listener per worker across restores', () => {
7
+ const addSpy = vi.fn()
8
+ const worker = {
9
+ addEventListener: addSpy,
10
+ removeEventListener: vi.fn(),
11
+ } as unknown as Worker
12
+
13
+ WorldViewWorker.restoreTransferred({}, worker)
14
+ WorldViewWorker.restoreTransferred({}, worker)
15
+ WorldViewWorker.restoreTransferred({}, worker)
16
+
17
+ expect(addSpy).toHaveBeenCalledTimes(1)
18
+ expect(WorldViewWorker.getWorkerBridgeListenerCountForTest(worker)).toBe(1)
19
+
20
+ WorldViewWorker.clearWorkerBridgeForTest(worker)
21
+ })
22
+
23
+ it('routes events to the latest restored worldView only', () => {
24
+ const listeners: Array<(event: MessageEvent) => void> = []
25
+ const worker = {
26
+ addEventListener: (_type: string, handler: (event: MessageEvent) => void) => {
27
+ listeners.push(handler)
28
+ },
29
+ removeEventListener: () => {},
30
+ } as unknown as Worker
31
+
32
+ const first = WorldViewWorker.restoreTransferred({}, worker)
33
+ const second = WorldViewWorker.restoreTransferred({}, worker)
34
+
35
+ let firstCalls = 0
36
+ let secondCalls = 0
37
+ first.on('loadChunk', () => {
38
+ firstCalls++
39
+ })
40
+ second.on('loadChunk', () => {
41
+ secondCalls++
42
+ })
43
+
44
+ const handler = listeners[0]!
45
+ handler({
46
+ data: {
47
+ class: WorldViewWorker.restorerName,
48
+ type: 'event',
49
+ eventName: 'loadChunk',
50
+ args: [{ x: 0, z: 0, chunk: {}, worldConfig: {}, isLightUpdate: false }],
51
+ },
52
+ } as MessageEvent)
53
+
54
+ expect(firstCalls).toBe(0)
55
+ expect(secondCalls).toBe(1)
56
+
57
+ WorldViewWorker.clearWorkerBridgeForTest(worker)
58
+ })
59
+ })