minecraft-renderer 0.1.39 → 0.1.41

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 (57) hide show
  1. package/dist/mesher.js +8 -8
  2. package/dist/mesher.js.map +4 -4
  3. package/dist/mesherWasm.js +94 -94
  4. package/dist/minecraft-renderer.js +57 -57
  5. package/dist/minecraft-renderer.js.meta.json +1 -1
  6. package/dist/threeWorker.js +66 -66
  7. package/package.json +3 -4
  8. package/src/bundler/bundlePrepare.ts +56 -0
  9. package/src/graphicsBackend/appViewer.ts +10 -0
  10. package/src/graphicsBackend/config.ts +5 -1
  11. package/src/graphicsBackend/preloadWorkers.ts +187 -0
  12. package/src/lib/worldrendererCommon.ts +26 -2
  13. package/src/{mesher → mesher-legacy}/mesher.ts +14 -4
  14. package/src/{mesher → mesher-legacy}/test/mesherTester.ts +2 -2
  15. package/src/{mesher → mesher-legacy}/test/run/test-js.ts +1 -1
  16. package/src/{mesher → mesher-legacy}/test/test-perf.ts +1 -1
  17. package/src/{mesher → mesher-legacy}/test/tests.test.ts +1 -1
  18. package/src/{mesher → mesher-shared}/shared.ts +2 -0
  19. package/src/playground/allEntitiesDebug.ts +1 -1
  20. package/src/three/chunkMeshManager.ts +1 -1
  21. package/src/three/entities.ts +19 -6
  22. package/src/three/entity/EntityMesh.ts +123 -140
  23. package/src/three/graphicsBackendBase.ts +13 -0
  24. package/src/three/holdingBlock.ts +1 -1
  25. package/src/three/holdingBlockLegacy.ts +1 -1
  26. package/src/three/modules/sciFiWorldReveal.ts +1 -1
  27. package/src/three/worldRendererThree.ts +2 -2
  28. package/src/wasm-mesher/README.md +90 -0
  29. package/src/{wasm-lib → wasm-mesher/bridge}/convertChunk.ts +2 -2
  30. package/src/{wasm-lib → wasm-mesher/bridge}/render-from-wasm.ts +4 -4
  31. package/src/wasm-mesher/runtime-build/wasm_mesher.d.ts +210 -0
  32. package/src/wasm-mesher/runtime-build/wasm_mesher.js +881 -0
  33. package/src/wasm-mesher/runtime-build/wasm_mesher_bg.wasm +0 -0
  34. package/src/wasm-mesher/runtime-build/wasm_mesher_bg.wasm.d.ts +24 -0
  35. package/src/{mesher/test → wasm-mesher/tests}/heightmapParity.test.ts +4 -4
  36. package/src/{mesher/test → wasm-mesher/tests}/mesherWasmConversionCache.test.ts +2 -2
  37. package/src/{mesher/test → wasm-mesher/tests}/splitColumnWasmOutput.test.ts +1 -1
  38. package/src/wasm-mesher/worker/mesherWasm.ts +1247 -0
  39. package/src/{mesher → wasm-mesher/worker}/mesherWasmConversionCache.ts +1 -1
  40. package/src/worldView/types.ts +90 -0
  41. package/src/mesher/mesherWasm.ts +0 -696
  42. package/wasm/wasm_mesher.d.ts +0 -46
  43. package/wasm/wasm_mesher.js +0 -443
  44. package/wasm/wasm_mesher_bg.wasm +0 -0
  45. package/wasm/wasm_mesher_bg.wasm.d.ts +0 -9
  46. /package/src/{mesher → mesher-legacy}/test/a.ts +0 -0
  47. /package/src/{mesher → mesher-legacy}/test/playground.ts +0 -0
  48. /package/src/{mesher → mesher-legacy}/test/run/chunk.ts +0 -0
  49. /package/src/{mesher → mesher-legacy}/test/snapshotUtils.ts +0 -0
  50. /package/src/{mesher → mesher-shared}/blockEntityMetadata.ts +0 -0
  51. /package/src/{mesher → mesher-shared}/computeHeightmap.ts +0 -0
  52. /package/src/{mesher → mesher-shared}/models.ts +0 -0
  53. /package/src/{mesher → mesher-shared}/modelsGeometryCommon.ts +0 -0
  54. /package/src/{mesher → mesher-shared}/standaloneRenderer.ts +0 -0
  55. /package/src/{mesher → mesher-shared}/world.ts +0 -0
  56. /package/src/{mesher → mesher-shared}/worldConstants.ts +0 -0
  57. /package/src/{mesher → wasm-mesher/worker}/mesherWasmRequestTracker.ts +0 -0
@@ -57,21 +57,33 @@ interface JsonModel {
57
57
  bones: JsonBone[]
58
58
  }
59
59
 
60
+ export type CustomModelMetadata = {
61
+ scale?: number
62
+ offset?: { x?: number, y?: number, z?: number }
63
+ texture?: string
64
+ textures?: Record<string, string>
65
+ animation?: string
66
+ animationLoop?: boolean
67
+ }
68
+
69
+ export type CustomModelPart = {
70
+ modelPath: string | ArrayBuffer
71
+ modelType: 'obj' | 'bedrock' | 'gltf'
72
+ metadata?: CustomModelMetadata
73
+ }
74
+
75
+ /** One part, or several (same idea as `models[]` on the custom-model-overlay). */
76
+ export type EntityCustomModel = CustomModelPart | { parts: CustomModelPart[] }
77
+
60
78
  interface EntityOverrides {
61
79
  textures?: Record<string, string>
62
80
  rotation?: Record<string, { x?: number; y?: number; z?: number }>
63
- customModel?: {
64
- modelPath: string | ArrayBuffer
65
- modelType: 'obj' | 'bedrock' | 'gltf'
66
- metadata?: {
67
- scale?: number
68
- offset?: { x?: number, y?: number, z?: number }
69
- texture?: string
70
- textures?: Record<string, string>
71
- animation?: string
72
- animationLoop?: boolean
73
- }
74
- }
81
+ customModel?: EntityCustomModel
82
+ }
83
+
84
+ function normalizeCustomModelParts (custom: EntityCustomModel): CustomModelPart[] {
85
+ if ('parts' in custom && Array.isArray(custom.parts)) return custom.parts
86
+ return [custom as CustomModelPart]
75
87
  }
76
88
 
77
89
  const elemFaces: Record<string, ElemFace> = {
@@ -443,7 +455,7 @@ interface EntityGeometry {
443
455
  export type EntityModelType = 'obj' | 'bedrock' | 'gltf'
444
456
 
445
457
  export type EntityDebugFlags = {
446
- type?: 'obj' | 'bedrock' | 'special'
458
+ type?: 'obj' | 'bedrock' | 'gltf' | 'special'
447
459
  tempMap?: string
448
460
  textureMap?: boolean
449
461
  errors?: string[]
@@ -453,9 +465,7 @@ export type EntityDebugFlags = {
453
465
  export class EntityMesh {
454
466
  mesh!: THREE.Object3D
455
467
  animations?: THREE.AnimationClip[]
456
- private animationController?: ReturnType<typeof createAnimatedObject>
457
- private initialAnimation?: string
458
- private initialLoop?: boolean
468
+ private animationControllers: Array<ReturnType<typeof createAnimatedObject>> = []
459
469
 
460
470
  constructor(
461
471
  version: string,
@@ -471,145 +481,117 @@ export class EntityMesh {
471
481
  debugFlags.tempMap = mappedValue
472
482
  }
473
483
 
474
- // Handle custom model override
484
+ // Handle custom model override (single or multiple parts)
475
485
  if (overrides.customModel) {
476
- // empty mesh to allow "handled" entity and not pink box
486
+ const parts = normalizeCustomModelParts(overrides.customModel)
477
487
  this.mesh = new THREE.Object3D()
478
488
 
479
- const { modelPath, modelType, metadata } = overrides.customModel
480
-
481
- switch (modelType) {
482
- case 'gltf': {
483
- const loader = new GLTFLoader()
484
- const gltfData = loader.parseAsync(modelPath, '')
485
-
486
- gltfData.then(gltf => {
487
- this.mesh.add(gltf.scene)
488
- this.animations = gltf.animations
489
-
490
- // Apply metadata overrides if available
489
+ for (let i = 0; i < parts.length; i++) {
490
+ const { modelPath, modelType, metadata } = parts[i]
491
+ const partRoot = new THREE.Object3D()
492
+ partRoot.name = `custom_part_${i}`
493
+
494
+ switch (modelType) {
495
+ case 'gltf': {
496
+ const loader = new GLTFLoader()
497
+ void loader.parseAsync(modelPath, '').then(gltf => {
498
+ partRoot.add(gltf.scene)
499
+ if (metadata?.scale) {
500
+ const s = metadata.scale
501
+ partRoot.scale.set(s, s, s)
502
+ }
503
+ if (metadata?.offset) {
504
+ const { x = 0, y = 0, z = 0 } = metadata.offset
505
+ partRoot.position.set(x, y, z)
506
+ }
507
+ if (metadata?.texture) {
508
+ const texture = new THREE.TextureLoader().load(metadata.texture)
509
+ texture.minFilter = THREE.NearestFilter
510
+ texture.magFilter = THREE.NearestFilter
511
+ partRoot.traverse((child) => {
512
+ if (child instanceof THREE.Mesh) {
513
+ child.material = new THREE.MeshBasicMaterial({
514
+ map: texture,
515
+ transparent: true,
516
+ alphaTest: 0.1
517
+ })
518
+ }
519
+ })
520
+ }
521
+ if (gltf.animations?.length) {
522
+ this.animations = [...(this.animations ?? []), ...gltf.animations]
523
+ const controller = createAnimatedObject(partRoot, gltf.animations)
524
+ this.animationControllers.push(controller)
525
+ const animationName = metadata?.animation
526
+ const loop = metadata?.animationLoop ?? true
527
+ if (animationName) {
528
+ controller.playAnimation(animationName, loop)
529
+ } else {
530
+ controller.playAnimation(gltf.animations[0].name, loop)
531
+ }
532
+ }
533
+ }).catch(err => {
534
+ console.error('Failed to load GLTF model:', err)
535
+ })
536
+ break
537
+ }
538
+ case 'obj': {
539
+ const objLoader = new OBJLoader()
540
+ const obj = objLoader.parse(modelPath as string)
491
541
  if (metadata?.scale) {
492
542
  const { scale } = metadata
493
- this.mesh.scale.set(scale, scale, scale)
543
+ obj.scale.set(scale, scale, scale)
494
544
  }
495
545
  if (metadata?.offset) {
496
546
  const { x = 0, y = 0, z = 0 } = metadata.offset
497
- this.mesh.position.set(x, y, z)
547
+ obj.position.set(x, y, z)
498
548
  }
499
-
500
- // Apply texture if provided
501
549
  if (metadata?.texture) {
502
550
  const texture = new THREE.TextureLoader().load(metadata.texture)
503
551
  texture.minFilter = THREE.NearestFilter
504
552
  texture.magFilter = THREE.NearestFilter
505
- this.mesh.traverse((child) => {
553
+ const material = new THREE.MeshBasicMaterial({
554
+ map: texture,
555
+ transparent: true,
556
+ alphaTest: 0.1
557
+ })
558
+ obj.traverse((child) => {
506
559
  if (child instanceof THREE.Mesh) {
507
- child.material = new THREE.MeshBasicMaterial({
508
- map: texture,
509
- transparent: true,
510
- alphaTest: 0.1
511
- })
560
+ child.material = material
512
561
  }
513
562
  })
514
563
  }
515
-
516
- // Handle animations: play from config if provided, otherwise play first animation if present
517
- if (gltf.animations && gltf.animations.length > 0) {
518
- this.animations = gltf.animations
519
- const animationName = metadata?.animation
520
- const loop = metadata?.animationLoop ?? true
521
-
522
- // Store initial animation settings for later use
523
- this.initialAnimation = animationName
524
- this.initialLoop = loop
525
-
526
- // Create animation controller with onBeforeRender support
527
- this.animationController = createAnimatedObject(this.mesh, gltf.animations)
528
-
529
- if (animationName) {
530
- // Play animation from config
531
- this.playAnimation(animationName, loop)
532
- } else {
533
- // Play first animation
534
- const clip = gltf.animations[0]
535
- this.animationController.playAnimation(clip.name, loop)
536
- }
537
- }
538
- }).catch(err => {
539
- console.error('Failed to load GLTF model:', err)
540
- })
541
-
542
- // debugFlags.type = 'gltf'
543
- return
544
- }
545
- case 'obj': {
546
- const objLoader = new OBJLoader()
547
- const obj = objLoader.parse(modelPath as string)
548
-
549
- // Apply metadata overrides if available
550
- if (metadata?.scale) {
551
- const { scale } = metadata
552
- obj.scale.set(scale, scale, scale)
553
- }
554
- if (metadata?.offset) {
555
- const { x = 0, y = 0, z = 0 } = metadata.offset
556
- obj.position.set(x, y, z)
557
- }
558
-
559
- // Apply texture if provided
560
- if (metadata?.texture) {
561
- const texture = new THREE.TextureLoader().load(metadata.texture)
562
- texture.minFilter = THREE.NearestFilter
563
- texture.magFilter = THREE.NearestFilter
564
- const material = new THREE.MeshBasicMaterial({
565
- map: texture,
566
- transparent: true,
567
- alphaTest: 0.1
568
- })
569
- obj.traverse((child) => {
570
- if (child instanceof THREE.Mesh) {
571
- child.material = material
572
- }
573
- })
574
- }
575
-
576
- this.mesh = obj
577
- debugFlags.type = 'obj'
578
- return
579
- }
580
- case 'bedrock': {
581
- // Parse bedrock model JSON
582
- const modelData = JSON.parse(modelPath as string)
583
- this.mesh = new THREE.Object3D()
584
-
585
- // Apply metadata overrides
586
- if (metadata?.scale) {
587
- this.mesh.scale.set(metadata.scale, metadata.scale, metadata.scale)
564
+ partRoot.add(obj)
565
+ break
588
566
  }
589
- if (metadata?.offset) {
590
- const { x = 0, y = 0, z = 0 } = metadata.offset
591
- this.mesh.position.set(x, y, z)
592
- }
593
-
594
- // Create mesh from bedrock model
595
- for (const [name, jsonModel] of Object.entries(modelData.geometry)) {
596
- const texture = metadata?.textures?.[name] ?? modelData.textures?.[name]
597
- if (!texture) continue
598
-
599
- const mesh = getMesh(worldRenderer,
600
- texture.endsWith('.png') || texture.startsWith('data:image/') || texture.startsWith('block:')
601
- ? texture : texture + '.png',
602
- jsonModel,
603
- overrides,
604
- debugFlags)
605
- mesh.name = `geometry_${name}`
606
- this.mesh.add(mesh)
567
+ case 'bedrock': {
568
+ const modelData = JSON.parse(modelPath as string)
569
+ if (metadata?.scale) {
570
+ partRoot.scale.set(metadata.scale, metadata.scale, metadata.scale)
571
+ }
572
+ if (metadata?.offset) {
573
+ const { x = 0, y = 0, z = 0 } = metadata.offset
574
+ partRoot.position.set(x, y, z)
575
+ }
576
+ for (const [name, jsonModel] of Object.entries(modelData.geometry)) {
577
+ const texture = metadata?.textures?.[name] ?? modelData.textures?.[name]
578
+ if (!texture) continue
579
+ const mesh = getMesh(worldRenderer,
580
+ texture.endsWith('.png') || texture.startsWith('data:image/') || texture.startsWith('block:')
581
+ ? texture : texture + '.png',
582
+ jsonModel as JsonModel,
583
+ overrides,
584
+ debugFlags)
585
+ mesh.name = `geometry_${name}`
586
+ partRoot.add(mesh)
587
+ }
588
+ break
607
589
  }
608
- debugFlags.type = 'bedrock'
609
- return
610
590
  }
611
- // No default
591
+ this.mesh.add(partRoot)
612
592
  }
593
+ debugFlags.type = parts.length === 1 ? parts[0].modelType : 'special'
594
+ return
613
595
  }
614
596
 
615
597
  if (externalModels[type]) {
@@ -696,15 +678,16 @@ export class EntityMesh {
696
678
  }
697
679
 
698
680
  playAnimation(name: string, loop = false) {
699
- if (!this.animationController || !this.animations) {
681
+ if (!this.animationControllers.length) {
700
682
  console.warn('No animation controller available')
701
683
  return
702
684
  }
703
-
704
- // Play animation using the controller
705
- const success = this.animationController.playAnimation(name, loop)
706
- if (!success) {
707
- console.warn(`Animation "${name}" not found`)
685
+ let ok = false
686
+ for (const c of this.animationControllers) {
687
+ if (c.playAnimation(name, loop)) ok = true
688
+ }
689
+ if (!ok) {
690
+ console.warn(`Animation "${name}" not found on any custom model part`)
708
691
  }
709
692
  }
710
693
 
@@ -16,6 +16,7 @@ import { WorldRendererThree } from './worldRendererThree'
16
16
  import { DocumentRenderer, isWebWorker, ThreeRendererMainData } from './documentRenderer'
17
17
  import { PanoramaRenderer } from './panorama'
18
18
  import { WorldViewWorker } from '../worldView'
19
+ import type { FeedChunkPacketPayload } from '../worldView/types'
19
20
 
20
21
  // Disable Three.js color management for compatibility
21
22
  THREE.ColorManagement.enabled = false
@@ -80,6 +81,18 @@ export const getBackendMethods = (worldRenderer: WorldRendererThree): any => {
80
81
  // Import dynamically to avoid circular dependencies
81
82
  const { applyWorldGeometryExport } = await import('./worldGeometryExport')
82
83
  return applyWorldGeometryExport(worldRenderer, exportData)
84
+ },
85
+ feedChunkPacket(payload: FeedChunkPacketPayload) {
86
+ // Forward parsed/raw map_chunk + update_light packets from the
87
+ // web-client to all WASM mesher workers. The fan-out below uses
88
+ // structured clone (one Uint8Array can only be transferred to a
89
+ // single recipient); useWorkerProxy still gives zero-copy transfer
90
+ // from main into the off-thread renderer worker for free.
91
+ const { kind, ...rest } = payload
92
+ const message = { type: kind, ...rest }
93
+ for (const worker of worldRenderer.workers) {
94
+ worker.postMessage(message)
95
+ }
83
96
  }
84
97
  }
85
98
  }
@@ -12,7 +12,7 @@ import { disposeObject } from './threeJsUtils'
12
12
  import type { IHoldingBlock } from './holdingBlockTypes'
13
13
  import { HandItemBlock, MovementState } from '../playerState/types'
14
14
  import { PlayerStateRenderer } from '../playerState/playerState'
15
- import { getThreeBlockModelGroup } from '../mesher/standaloneRenderer'
15
+ import { getThreeBlockModelGroup } from '../mesher-shared/standaloneRenderer'
16
16
  import { IndexedData } from 'minecraft-data'
17
17
  import { WorldRendererConfig } from '../graphicsBackend'
18
18
  import { computeCameraBob, type CameraBobInput } from '../lib/cameraBobbing'
@@ -12,7 +12,7 @@ import { WorldRendererThree } from './worldRendererThree'
12
12
  import { disposeObject } from './threeJsUtils'
13
13
  import { HandItemBlock, MovementState } from '../playerState/types'
14
14
  import { PlayerStateRenderer } from '../playerState/playerState'
15
- import { getThreeBlockModelGroup } from '../mesher/standaloneRenderer'
15
+ import { getThreeBlockModelGroup } from '../mesher-shared/standaloneRenderer'
16
16
  import { IndexedData } from 'minecraft-data'
17
17
  import { WorldRendererConfig } from '../graphicsBackend'
18
18
  import { IHoldingBlock } from './holdingBlockTypes'
@@ -2,7 +2,7 @@
2
2
  import * as THREE from 'three'
3
3
  import type { WorldRendererThree } from '../worldRendererThree'
4
4
  import type { RendererModuleController, RendererModuleManifest } from '../rendererModuleSystem'
5
- import type { MesherGeometryOutput } from '../../mesher/shared'
5
+ import type { MesherGeometryOutput } from '../../mesher-shared/shared'
6
6
 
7
7
  const SCI_FI_CYAN = new THREE.Color(13 / 255, 234 / 255, 238 / 255)
8
8
  const CHUNKS_THRESHOLD = 9
@@ -10,9 +10,9 @@ import { DisplayWorldOptions, GraphicsInitOptions } from '../graphicsBackend/typ
10
10
  import { chunkPos, sectionPos } from '../lib/simpleUtils'
11
11
  import { WorldRendererCommon } from '../lib/worldrendererCommon'
12
12
  import { addNewStat } from '../lib/ui/newStats'
13
- import { MesherGeometryOutput } from '../mesher/shared'
13
+ import { MesherGeometryOutput } from '../mesher-shared/shared'
14
14
  import { ItemSpecificContextProperties } from '../playerState/types'
15
- import { setBlockPosition } from '../mesher/standaloneRenderer'
15
+ import { setBlockPosition } from '../mesher-shared/standaloneRenderer'
16
16
  import { getMyHand } from './hand'
17
17
  import { createHoldingBlock } from './holdingBlockFactory'
18
18
  import type { IHoldingBlock } from './holdingBlockTypes'
@@ -0,0 +1,90 @@
1
+ # src/wasm-mesher (JS side of the WASM mesher)
2
+
3
+ This directory is the JS-side counterpart of the Rust crate at
4
+ `/wasm-mesher/`. It contains everything that runs in the browser/Node:
5
+ the worker, the JS↔Rust bridge, the wasm-pack runtime artefacts, and
6
+ the WASM-only unit tests.
7
+
8
+ The Rust source itself stays at the repo root (`/wasm-mesher/`); only
9
+ its compiled output is mirrored here.
10
+
11
+ ## Layout
12
+
13
+ ```
14
+ src/wasm-mesher/
15
+ ├── runtime-build/ ← wasm-pack output (--target web)
16
+ │ ├── wasm_mesher.js ← JS shim generated by wasm-bindgen
17
+ │ ├── wasm_mesher_bg.wasm ← compiled WASM module
18
+ │ └── *.d.ts ← typings
19
+ ├── bridge/ ← thin JS layer between Rust output and the renderer
20
+ │ ├── convertChunk.ts ← legacy slow path: prismarine-chunk → typed arrays
21
+ │ └── render-from-wasm.ts ← geometry → Three.js + section split + heightmap
22
+ ├── worker/ ← runs inside the dedicated mesher Web Worker
23
+ │ ├── mesherWasm.ts ← worker entry; routes by version (1.16/17/18+)
24
+ │ ├── mesherWasmConversionCache.ts
25
+ │ └── mesherWasmRequestTracker.ts
26
+ ├── tests/ ← WASM-only vitest tests (run by `pnpm unit-test`)
27
+ │ ├── heightmapParity.test.ts
28
+ │ ├── splitColumnWasmOutput.test.ts
29
+ │ └── mesherWasmConversionCache.test.ts
30
+ └── README.md ← you are here
31
+ ```
32
+
33
+ Other related directories:
34
+
35
+ - **`/wasm-mesher/`** (repo root) — the Rust crate (`src/*.rs`,
36
+ `Cargo.toml`, `build.sh`) and standalone TS test harnesses
37
+ (`tests/test-chunk.ts`, `tests/test-section-boundary.ts`).
38
+ See `wasm-mesher/README.md` for the public Rust API.
39
+ - **`src/mesher-shared/`** — modules used by **both** this WASM path and
40
+ the legacy JS mesher (`models`, `world`, `shared`, `worldConstants`,
41
+ `computeHeightmap`, …).
42
+ - **`src/mesher-legacy/`** — the original JS mesher, kept until the WASM
43
+ path proves itself in production. Will be removed in a follow-up PR.
44
+
45
+ ## Building
46
+
47
+ The worker bundle is built by `scripts/buildMesherWorker.mjs`:
48
+
49
+ ```bash
50
+ pnpm build:mesher # esbuild → dist/mesherWasm.js (web-client contract)
51
+ pnpm watch:mesher # incremental rebuild on change
52
+ ```
53
+
54
+ The output filename `dist/mesherWasm.js` is **load-bearing** — the
55
+ web-client imports the worker by that path. `entryNames: '[name]'` in
56
+ the build config keeps the output flat regardless of source-tree depth.
57
+
58
+ The WASM module itself (`runtime-build/wasm_mesher_bg.wasm`) is rebuilt
59
+ by:
60
+
61
+ ```bash
62
+ pnpm build:wasm # → runs wasm-mesher/build.sh web (release)
63
+ ```
64
+
65
+ This requires `wasm-pack` on PATH; see `wasm-mesher/README.md` for setup.
66
+
67
+ ## Testing
68
+
69
+ ```bash
70
+ pnpm unit-test --run # all vitest tests, including this dir's tests/
71
+ pnpm test:wasm # snapshot + boundary + heightmap (cjs harnesses)
72
+ pnpm test:wasm:boundary # boundary + heightmap only
73
+ ```
74
+
75
+ The vitest tests in `tests/` exercise the worker logic against real WASM:
76
+
77
+ - `heightmapParity.test.ts` — JS `computeHeightmap` vs Rust output.
78
+ - `splitColumnWasmOutput.test.ts` — column-split correctness.
79
+ - `mesherWasmConversionCache.test.ts` — cache eviction and column reuse.
80
+
81
+ ## Notes
82
+
83
+ - `bridge/convertChunk.ts` is the **slow path** (prismarine-chunk → typed
84
+ arrays in JS). For 1.18+ chunks the worker prefers the fast path:
85
+ `parseChunkDump118FullColumnAll` (a single Rust call) consumed via
86
+ `worker/mesherWasm.ts`. See `docs/issues/issue-15-wasm/history.md` for
87
+ the rationale and benchmarks.
88
+ - Routing of raw `map_chunk` packets (1.16 / 1.17 / 1.18+) lives in
89
+ `worker/mesherWasm.ts`; per-version Rust parsers are in
90
+ `wasm-mesher/src/parser_*.rs`.
@@ -2,7 +2,7 @@
2
2
  import { Vec3 } from 'vec3'
3
3
  import MinecraftData from 'minecraft-data'
4
4
  import PrismarineBlockLoader from 'prismarine-block'
5
- import moreBlockDataGeneratedJson from '../lib/moreBlockDataGenerated.json'
5
+ import moreBlockDataGeneratedJson from '../../lib/moreBlockDataGenerated.json'
6
6
 
7
7
  type BlockMeta = {
8
8
  invisibleBlocks: Uint16Array
@@ -54,7 +54,7 @@ const isLikelyFullCubeBlockName = (name: string) => {
54
54
  return true
55
55
  }
56
56
 
57
- const getBlockMeta = (version: string): BlockMeta => {
57
+ export const getBlockMeta = (version: string): BlockMeta => {
58
58
  const cached = metaCache.get(version)
59
59
  if (cached) return cached
60
60
 
@@ -9,10 +9,10 @@ import blockStatesModels from 'mc-assets/dist/blockStatesModels.json'
9
9
  import MinecraftData from 'minecraft-data'
10
10
  import PrismarineBlockLoader from 'prismarine-block'
11
11
  import { Vec3 } from 'vec3'
12
- import { elemFaces, buildRotationMatrix, matmul3, matmulmat3, vecadd3, vecsub3 } from '../mesher/modelsGeometryCommon'
13
- import type { ExportedWorldGeometry, ExportedSection } from '../three/worldGeometryExport'
14
- import type { MesherGeometryOutput } from '../mesher/shared'
15
- import type { World } from '../mesher/world'
12
+ import { elemFaces, buildRotationMatrix, matmul3, matmulmat3, vecadd3, vecsub3 } from '../../mesher-shared/modelsGeometryCommon'
13
+ import type { ExportedWorldGeometry, ExportedSection } from '../../three/worldGeometryExport'
14
+ import type { MesherGeometryOutput } from '../../mesher-shared/shared'
15
+ import type { World } from '../../mesher-shared/world'
16
16
 
17
17
  // Handle both default and named export
18
18
  const worldBlockProvider = (worldBlockProviderModule as any).default || worldBlockProviderModule