minecraft-renderer 0.1.36 → 0.1.38

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "minecraft-renderer",
3
- "version": "0.1.36",
3
+ "version": "0.1.38",
4
4
  "description": "The most Modular Minecraft world renderer with Three.js WebGL backend",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -19,7 +19,7 @@ export const defaultWorldRendererConfig = {
19
19
 
20
20
  // Debug settings
21
21
  showChunkBorders: false,
22
- enableDebugOverlay: true,
22
+ enableDebugOverlay: false,
23
23
  debugModelVariant: undefined as undefined | number[],
24
24
  futuristicReveal: false,
25
25
 
@@ -28,17 +28,46 @@ export interface SectionObject extends THREE.Group {
28
28
  headsContainer?: THREE.Group
29
29
  bannersContainer?: THREE.Group
30
30
  boxHelper?: THREE.BoxHelper
31
+ /**
32
+ * World-space coordinates of the section origin. Cached so that
33
+ * {@link ChunkMeshManager.updateBoxHelper} can position lazily-created
34
+ * border helpers correctly under camera-relative rendering, where
35
+ * `mesh.position` is proxied to (world - sceneOrigin) and cannot be
36
+ * reused directly for objects that are tracked separately.
37
+ */
38
+ worldX?: number
39
+ worldY?: number
40
+ worldZ?: number
31
41
  foutain?: boolean
42
+ /**
43
+ * True while the section is held invisible by the "Batch Chunks Display"
44
+ * (`_renderByChunks`) feature, waiting for the parent chunk to finish meshing
45
+ * before being shown together with the rest of the chunk.
46
+ */
47
+ _waitingForChunkDisplay?: boolean
32
48
  }
33
49
 
34
50
  export class ChunkMeshManager {
35
51
  private readonly meshPool: ChunkMeshPool[] = []
36
52
  private readonly activeSections = new Map<string, ChunkMeshPool>()
37
53
  readonly sectionObjects: Record<string, SectionObject> = {}
54
+ /**
55
+ * Sections kept invisible because the "Batch Chunks Display" option is on
56
+ * and their parent chunk hasn't finished meshing yet. Keyed by chunk key
57
+ * (`x,z`); flushed by `WorldRendererThree.finishChunk(chunkKey)`.
58
+ */
59
+ readonly waitingChunksToDisplay: Record<string, string[]> = {}
38
60
  private poolSize!: number
39
61
  private maxPoolSize!: number
40
62
  private minPoolSize!: number
41
63
  private readonly signHeadsRenderer: SignHeadsRenderer
64
+ /**
65
+ * Shared transparent material used as the basis for the wireframe chunk
66
+ * border `BoxHelper` created lazily in {@link updateBoxHelper}. Kept on the
67
+ * manager so the BoxHelper machinery doesn't allocate a new material per
68
+ * section.
69
+ */
70
+ private readonly chunkBoxMaterial = new THREE.MeshBasicMaterial({ color: 0x00_00_00, transparent: true, opacity: 0 })
42
71
 
43
72
  // Performance tracking
44
73
  private hits = 0
@@ -64,7 +93,7 @@ export class ChunkMeshManager {
64
93
 
65
94
  constructor (
66
95
  public worldRenderer: WorldRendererThree,
67
- public scene: THREE.Group,
96
+ public scene: THREE.Object3D,
68
97
  public material: THREE.Material,
69
98
  public worldHeight: number,
70
99
  viewDistance = 3,
@@ -156,6 +185,17 @@ export class ChunkMeshManager {
156
185
  // Store metadata
157
186
  sectionObject.tilesCount = geometryData.positions.length / 3 / 4
158
187
  sectionObject.blocksCount = geometryData.blocksCount
188
+ sectionObject.worldX = geometryData.sx
189
+ sectionObject.worldY = geometryData.sy
190
+ sectionObject.worldZ = geometryData.sz
191
+ // Stamp the section key so modules (e.g. sciFiWorldReveal) can resolve
192
+ // mesh -> section without falling back to sceneOrigin world-position math.
193
+ ;(sectionObject as any).sectionKey = sectionKey
194
+ // Tag the group so `WorldRendererThree.getThirdPersonCamera` raycast can
195
+ // still find chunk meshes — the old `WorldBlockGeometry` set this name
196
+ // unconditionally; the pooling port lost that and only the border-helper
197
+ // path used to restore it.
198
+ sectionObject.name = 'chunk'
159
199
 
160
200
  try {
161
201
  // Add signs container
@@ -218,13 +258,64 @@ export class ChunkMeshManager {
218
258
  this.scene.add(sectionObject)
219
259
  sectionObject.matrixAutoUpdate = false
220
260
 
261
+ // Create chunk border helper eagerly when the option is on so freshly
262
+ // streamed sections immediately get the F3+G yellow wireframe instead of
263
+ // appearing only on the next toggle.
264
+ if (this.worldRenderer.displayOptions?.inWorldRenderingConfig?.showChunkBorders) {
265
+ this.updateBoxHelper(sectionKey, true)
266
+ }
267
+
268
+ // Honor "Batch Chunks Display" (`_renderByChunks`): keep this section's
269
+ // mesh hidden until the whole chunk has finished meshing, so users see a
270
+ // chunk appear as a single 16xHx16 tile instead of streaming per-section.
271
+ // Updates to chunks that are already finished bypass batching to avoid
272
+ // flickering on block changes / lighting updates.
273
+ const chunkCoords = sectionKey.split(',')
274
+ const chunkKey = `${chunkCoords[0]},${chunkCoords[2]}`
275
+ const renderByChunks = !!this.worldRenderer.displayOptions
276
+ ?.inWorldRenderingConfig?._renderByChunks
277
+ if (renderByChunks && !this.worldRenderer.finishedChunks[chunkKey]) {
278
+ sectionObject.visible = false
279
+ sectionObject._waitingForChunkDisplay = true
280
+ const list = this.waitingChunksToDisplay[chunkKey] ?? (this.waitingChunksToDisplay[chunkKey] = [])
281
+ if (!list.includes(sectionKey)) list.push(sectionKey)
282
+ }
283
+
221
284
  return sectionObject
222
285
  }
223
286
 
287
+ /**
288
+ * Reveal all sections of a chunk that were held invisible by the
289
+ * "Batch Chunks Display" option. Called from `WorldRendererThree.finishChunk`.
290
+ */
291
+ finishChunkDisplay (chunkKey: string): void {
292
+ const sectionKeys = this.waitingChunksToDisplay[chunkKey]
293
+ if (!sectionKeys) return
294
+ for (const sectionKey of sectionKeys) {
295
+ const sectionObject = this.sectionObjects[sectionKey]
296
+ if (!sectionObject) continue
297
+ sectionObject._waitingForChunkDisplay = false
298
+ sectionObject.visible = true
299
+ }
300
+ delete this.waitingChunksToDisplay[chunkKey]
301
+ }
302
+
224
303
  cleanupSection (sectionKey: string) {
225
304
  // Remove section object from scene
226
305
  const sectionObject = this.sectionObjects[sectionKey]
227
306
  if (sectionObject) {
307
+ // Drop from any pending "batch display" queue so we don't try to flip
308
+ // visibility on a stale (released) object later.
309
+ if (sectionObject._waitingForChunkDisplay) {
310
+ const chunkCoords = sectionKey.split(',')
311
+ const chunkKey = `${chunkCoords[0]},${chunkCoords[2]}`
312
+ const list = this.waitingChunksToDisplay[chunkKey]
313
+ if (list) {
314
+ const idx = list.indexOf(sectionKey)
315
+ if (idx !== -1) list.splice(idx, 1)
316
+ if (list.length === 0) delete this.waitingChunksToDisplay[chunkKey]
317
+ }
318
+ }
228
319
  // Cleanup banner textures before disposing
229
320
  if (sectionObject.bannersContainer) {
230
321
  sectionObject.bannersContainer.traverse((child) => {
@@ -243,6 +334,21 @@ export class ChunkMeshManager {
243
334
  }
244
335
  this.worldRenderer.sceneOrigin.removeAndUntrackAll(sectionObject)
245
336
  this.scene.remove(sectionObject)
337
+ // boxHelper lives directly on the scene (so it stays world-anchored
338
+ // under camera-relative rendering), so it must be cleaned up explicitly
339
+ // — `removeAndUntrackAll` above only walks `sectionObject` descendants.
340
+ if (sectionObject.boxHelper) {
341
+ this.worldRenderer.sceneOrigin.removeAndUntrack(sectionObject.boxHelper)
342
+ this.scene.remove(sectionObject.boxHelper)
343
+ sectionObject.boxHelper.geometry.dispose()
344
+ const helperMat = sectionObject.boxHelper.material as THREE.Material | THREE.Material[]
345
+ if (Array.isArray(helperMat)) {
346
+ for (const m of helperMat) m.dispose()
347
+ } else {
348
+ helperMat.dispose()
349
+ }
350
+ sectionObject.boxHelper = undefined
351
+ }
246
352
  delete this.sectionObjects[sectionKey]
247
353
  }
248
354
  }
@@ -285,19 +391,28 @@ export class ChunkMeshManager {
285
391
  /**
286
392
  * Update box helper for a section
287
393
  */
288
- updateBoxHelper (sectionKey: string, showChunkBorders: boolean, chunkBoxMaterial: THREE.Material) {
394
+ updateBoxHelper (sectionKey: string, showChunkBorders: boolean, chunkBoxMaterial: THREE.Material = this.chunkBoxMaterial) {
289
395
  const sectionObject = this.sectionObjects[sectionKey]
290
396
  if (!sectionObject?.mesh) return
291
397
 
292
398
  if (showChunkBorders) {
293
399
  if (!sectionObject.boxHelper) {
294
- // mesh with static dimensions: 16x16x16
400
+ // Build a 16x16x16 reference mesh in world coordinates so BoxHelper's
401
+ // `setFromObject` produces the correct geometry. The reference mesh is
402
+ // not added to the scene; only the resulting BoxHelper is.
295
403
  const staticChunkMesh = new THREE.Mesh(new THREE.BoxGeometry(16, 16, 16), chunkBoxMaterial)
296
- staticChunkMesh.position.copy(sectionObject.mesh.position)
297
404
  const boxHelper = new THREE.BoxHelper(staticChunkMesh, 0xff_ff_00)
298
405
  boxHelper.name = 'helper'
299
- sectionObject.add(boxHelper)
300
- sectionObject.name = 'chunk'
406
+ // Add directly to the scene and track it through sceneOrigin so that
407
+ // camera-relative rendering (floating origin) keeps the helper pinned
408
+ // to its world coordinates instead of following the camera.
409
+ const sx = sectionObject.worldX ?? 0
410
+ const sy = sectionObject.worldY ?? 0
411
+ const sz = sectionObject.worldZ ?? 0
412
+ this.worldRenderer.sceneOrigin.track(boxHelper, { updateMatrix: true })
413
+ boxHelper.position.set(sx, sy, sz)
414
+ boxHelper.updateMatrix()
415
+ this.scene.add(boxHelper)
301
416
  sectionObject.boxHelper = boxHelper
302
417
  }
303
418
  sectionObject.boxHelper.visible = true
@@ -306,6 +421,28 @@ export class ChunkMeshManager {
306
421
  }
307
422
  }
308
423
 
424
+ /**
425
+ * Create / toggle chunk border helpers for every active section. Used by
426
+ * `WorldRendererThree.updateShowChunksBorder` so the F3+G hotkey works
427
+ * after the move from `WorldBlockGeometry` (which created the helpers
428
+ * eagerly per section) to the pooled `ChunkMeshManager`.
429
+ */
430
+ updateAllBoxHelpers (showChunkBorders: boolean) {
431
+ for (const sectionKey of Object.keys(this.sectionObjects)) {
432
+ this.updateBoxHelper(sectionKey, showChunkBorders)
433
+ }
434
+ }
435
+
436
+ /**
437
+ * Forward to {@link SignHeadsRenderer.cleanChunkTextures} so callers in
438
+ * `WorldRendererThree` (which historically owned the sign-texture cache)
439
+ * can invalidate cached sign textures when a section is marked dirty,
440
+ * without reaching into the manager's private members.
441
+ */
442
+ cleanSignChunkTextures (x: number, z: number) {
443
+ this.signHeadsRenderer.cleanChunkTextures(x, z)
444
+ }
445
+
309
446
  /**
310
447
  * Get mesh for section if it exists
311
448
  */
@@ -459,6 +596,7 @@ export class ChunkMeshManager {
459
596
 
460
597
  this.meshPool.length = 0
461
598
  this.activeSections.clear()
599
+ this.chunkBoxMaterial.dispose()
462
600
  }
463
601
 
464
602
  // Private helper methods
@@ -667,6 +805,12 @@ export class ChunkMeshManager {
667
805
  updateSectionsVisibility (): void {
668
806
  const cameraPos = this.worldRenderer.cameraSectionPos
669
807
  for (const [sectionKey, sectionObject] of Object.entries(this.sectionObjects)) {
808
+ // Don't override "Batch Chunks Display" hiding — those sections must
809
+ // stay invisible until their chunk finishes meshing.
810
+ if (sectionObject._waitingForChunkDisplay) {
811
+ sectionObject.visible = false
812
+ continue
813
+ }
670
814
  if (!this.performanceOverrideDistance) {
671
815
  sectionObject.visible = true
672
816
  continue
@@ -805,4 +949,21 @@ class SignHeadsRenderer {
805
949
  textures[texturekey] = tex
806
950
  return tex
807
951
  }
952
+
953
+ /**
954
+ * Dispose all cached sign textures for the chunk containing world coords
955
+ * (x, z). Called from `WorldRendererThree.cleanChunkTextures` so that
956
+ * re-meshes triggered by `setSectionDirty` (e.g. a player edits a sign)
957
+ * pick up fresh block-entity NBT instead of returning the stale cached
958
+ * texture from {@link SignHeadsRenderer.getSignTexture}.
959
+ */
960
+ cleanChunkTextures (x: number, z: number) {
961
+ const key = `${Math.floor(x / 16)},${Math.floor(z / 16)}`
962
+ const textures = this.chunkTextures.get(key)
963
+ if (!textures) return
964
+ for (const k of Object.keys(textures)) {
965
+ textures[k].dispose()
966
+ delete textures[k]
967
+ }
968
+ }
808
969
  }
@@ -1,7 +1,6 @@
1
1
  //@ts-nocheck
2
2
  import * as THREE from 'three'
3
3
  import * as tweenJs from '@tweenjs/tween.js'
4
- import PrismarineItem from 'prismarine-item'
5
4
  import { WorldBlockProvider } from 'mc-assets/dist/worldBlockProvider'
6
5
  import { BlockModel } from 'mc-assets'
7
6
  import { DebugGui } from '../lib/DebugGui'
@@ -17,8 +16,14 @@ import { getThreeBlockModelGroup } from '../mesher/standaloneRenderer'
17
16
  import { IndexedData } from 'minecraft-data'
18
17
  import { WorldRendererConfig } from '../graphicsBackend'
19
18
  import { computeCameraBob, type CameraBobInput } from '../lib/cameraBobbing'
19
+ import { getFirstPersonItemSpecificProps, getHandItemRenderKey } from './holdingBlockItemIdentity'
20
20
 
21
21
  const _tempMat = new THREE.Matrix4()
22
+ const wrapPi = (a: number) => {
23
+ a = (a + Math.PI) % (Math.PI * 2)
24
+ if (a < 0) a += Math.PI * 2
25
+ return a - Math.PI
26
+ }
22
27
 
23
28
  // Vanilla renderPlayerArm transform chain
24
29
  function buildBareHandMatrix(swingProgress: number, equipProgress: number): THREE.Matrix4 {
@@ -94,6 +99,7 @@ export default class HoldingBlock implements IHoldingBlock {
94
99
  equipProgress = 0 // 0 = fully visible, 1 = hidden
95
100
  stopUpdate = false
96
101
  lastHeldItem: HandItemBlock | undefined
102
+ lastHeldItemRenderKey: string | undefined
97
103
  currentDisplayType: 'hand' | 'item' | 'block' = 'hand'
98
104
  isSwinging = false
99
105
  nextIterStopCallbacks: Array<() => void> | undefined
@@ -116,6 +122,7 @@ export default class HoldingBlock implements IHoldingBlock {
116
122
 
117
123
  constructor(public worldRenderer: WorldRendererThree, public offHand = false) {
118
124
  this.initCameraGroup()
125
+ this.swingAnimator = new HandSwingAnimator()
119
126
  this.unsubs.push(
120
127
  this.worldRenderer.onReactivePlayerStateUpdated('heldItemMain', () => {
121
128
  if (!this.offHand) {
@@ -332,44 +339,32 @@ export default class HoldingBlock implements IHoldingBlock {
332
339
  }
333
340
 
334
341
  isDifferentItem(block: HandItemBlock | undefined) {
335
- const Item = PrismarineItem(this.worldRenderer.version)
336
- if (!this.lastHeldItem) {
337
- return true
338
- }
339
- if (this.lastHeldItem.name !== block?.name) {
340
- return true
341
- }
342
- // eslint-disable-next-line sonarjs/prefer-single-boolean-return
343
- if (!Item.equal(this.lastHeldItem.fullItem, block?.fullItem ?? {}) || JSON.stringify(this.lastHeldItem.fullItem.components) !== JSON.stringify(block?.fullItem?.components)) {
344
- return true
345
- }
346
-
347
- return false
342
+ return this.lastHeldItemRenderKey !== getHandItemRenderKey(this.worldRenderer, block)
348
343
  }
349
344
 
350
345
  updateCameraGroup() {
351
346
  if (this.stopUpdate) return
352
347
  const { camera } = this
353
348
 
354
- // Hand rotation momentum (xBob/yBob) — vanilla Minecraft inertia effect
355
- // Use base rotation from CameraShake (actual player view angles)
356
349
  const now = performance.now()
357
350
  const baseRotation = this.worldRenderer.cameraShake.getBaseRotation()
358
351
  const actualPitch = baseRotation.pitch
359
352
  const actualYaw = baseRotation.yaw
353
+
360
354
  if (this.lastBobUpdateTime === 0) {
361
355
  this.xBob = actualPitch
362
356
  this.yBob = actualYaw
363
- this.lastBobUpdateTime = now
364
357
  } else {
365
358
  const dt = Math.min((now - this.lastBobUpdateTime) / 1000, 0.1)
366
- this.lastBobUpdateTime = now
367
- const factor = 1 - Math.pow(0.5, dt * 20)
368
- this.xBob += (actualPitch - this.xBob) * factor
369
- this.yBob += (actualYaw - this.yBob) * factor
359
+ const pitchFactor = 1 - Math.pow(0.5, dt * 28)
360
+ const yawFactor = 1 - Math.pow(0.5, dt * 36)
361
+ this.xBob += (actualPitch - this.xBob) * pitchFactor
362
+ this.yBob += wrapPi(actualYaw - this.yBob) * yawFactor
370
363
  }
371
- const pitchOffset = (actualPitch - this.xBob) * -0.1
372
- const yawOffset = (actualYaw - this.yBob) * -0.1
364
+
365
+ this.lastBobUpdateTime = now
366
+ const pitchOffset = (actualPitch - this.xBob) * -0.05
367
+ const yawOffset = wrapPi(actualYaw - this.yBob) * -0.035
373
368
 
374
369
  this.cameraGroup.position.copy(camera.position)
375
370
  this.cameraGroup.rotation.copy(camera.rotation)
@@ -431,11 +426,7 @@ export default class HoldingBlock implements IHoldingBlock {
431
426
  const result = this.worldRenderer.entities.getItemMesh({
432
427
  ...handItem.fullItem,
433
428
  itemId: handItem.id,
434
- }, {
435
- 'minecraft:display_context': 'firstperson',
436
- 'minecraft:use_duration': this.worldRenderer.playerStateReactive.itemUsageTicks,
437
- 'minecraft:using_item': !!this.worldRenderer.playerStateReactive.itemUsageTicks,
438
- }, false, this.lastItemModelName)
429
+ }, getFirstPersonItemSpecificProps(this.worldRenderer), false, this.lastItemModelName)
439
430
  if (result) {
440
431
  const { mesh: itemMesh, isBlock, modelName } = result
441
432
  if (isBlock) {
@@ -487,8 +478,11 @@ export default class HoldingBlock implements IHoldingBlock {
487
478
  this.holdingBlock?.removeFromParent()
488
479
  this.holdingBlock = undefined
489
480
  this.currentDisplayType = 'hand'
490
- this.swingAnimator?.stopSwing()
491
- this.swingAnimator = undefined
481
+ const swingAnimator = this.swingAnimator
482
+ swingAnimator?.stopSwing()
483
+ if (swingAnimator) {
484
+ swingAnimator.type = 'hand'
485
+ }
492
486
  this.idleAnimator = undefined
493
487
  return
494
488
  }
@@ -516,10 +510,14 @@ export default class HoldingBlock implements IHoldingBlock {
516
510
 
517
511
  switchRequest = 0
518
512
  async setNewItem(handItem?: HandItemBlock) {
519
- if (!this.isDifferentItem(handItem)) return
513
+ const nextRenderKey = getHandItemRenderKey(this.worldRenderer, handItem)
514
+ const itemChanged = this.lastHeldItemRenderKey !== nextRenderKey
515
+ this.lastHeldItem = handItem
516
+ if (!itemChanged) return
517
+
518
+ this.lastHeldItemRenderKey = nextRenderKey
520
519
  this.lastItemModelName = undefined
521
520
  const switchRequest = ++this.switchRequest
522
- this.lastHeldItem = handItem
523
521
 
524
522
  let playAppearAnimation = false
525
523
  if (this.holdingBlock) {
@@ -535,7 +533,11 @@ export default class HoldingBlock implements IHoldingBlock {
535
533
 
536
534
  if (!handItem) {
537
535
  this.currentDisplayType = 'hand'
538
- this.swingAnimator = undefined
536
+ const swingAnimator = this.swingAnimator
537
+ swingAnimator?.stopSwing()
538
+ if (swingAnimator) {
539
+ swingAnimator.type = 'hand'
540
+ }
539
541
  this.idleAnimator = undefined
540
542
  this.blockSwapAnimation = undefined
541
543
  return
@@ -553,8 +555,10 @@ export default class HoldingBlock implements IHoldingBlock {
553
555
  await this.playBlockSwapAnimation('appeared')
554
556
  }
555
557
 
556
- this.swingAnimator = new HandSwingAnimator()
557
- this.swingAnimator.type = result.type
558
+ const swingAnimator = this.swingAnimator
559
+ if (swingAnimator) {
560
+ swingAnimator.type = result.type
561
+ }
558
562
  // Idle animation disabled — walking bob is handled by vanilla bobView applied to cameraGroup
559
563
  this.idleAnimator = undefined
560
564
  }
@@ -0,0 +1,43 @@
1
+ //@ts-nocheck
2
+ import { expect, test, vi } from 'vitest'
3
+ import { getHandItemRenderKey } from './holdingBlockItemIdentity'
4
+
5
+ test('hand item render key ignores live use state but keeps first-person display context', () => {
6
+ const getItemRenderData = vi.fn(() => ({ modelName: 'item/bow' }))
7
+ const worldRenderer = {
8
+ playerStateReactive: {
9
+ itemUsageTicks: 20,
10
+ },
11
+ resourcesManager: {
12
+ currentResources: {},
13
+ },
14
+ getItemRenderData,
15
+ } as any
16
+
17
+ const handItem = {
18
+ type: 'item',
19
+ id: 261,
20
+ name: 'minecraft:bow',
21
+ fullItem: {
22
+ count: 1,
23
+ },
24
+ } as const
25
+
26
+ const activeKey = getHandItemRenderKey(worldRenderer, handItem)
27
+ worldRenderer.playerStateReactive.itemUsageTicks = 0
28
+ const idleKey = getHandItemRenderKey(worldRenderer, handItem)
29
+
30
+ expect(activeKey).toBe(idleKey)
31
+ expect(getItemRenderData).toHaveBeenNthCalledWith(1, {
32
+ ...handItem.fullItem,
33
+ itemId: handItem.id,
34
+ }, {
35
+ 'minecraft:display_context': 'firstperson',
36
+ })
37
+ expect(getItemRenderData).toHaveBeenNthCalledWith(2, {
38
+ ...handItem.fullItem,
39
+ itemId: handItem.id,
40
+ }, {
41
+ 'minecraft:display_context': 'firstperson',
42
+ })
43
+ })
@@ -0,0 +1,30 @@
1
+ //@ts-nocheck
2
+ import type { HandItemBlock, ItemSpecificContextProperties } from '../playerState/types'
3
+ import type { WorldRendererThree } from './worldRendererThree'
4
+
5
+ export const getFirstPersonItemSpecificProps = (worldRenderer: WorldRendererThree): ItemSpecificContextProperties => ({
6
+ 'minecraft:display_context': 'firstperson',
7
+ 'minecraft:use_duration': worldRenderer.playerStateReactive.itemUsageTicks,
8
+ 'minecraft:using_item': !!worldRenderer.playerStateReactive.itemUsageTicks,
9
+ })
10
+
11
+ const getFirstPersonItemIdentityProps = (): ItemSpecificContextProperties => ({
12
+ 'minecraft:display_context': 'firstperson',
13
+ })
14
+
15
+ export const getHandItemRenderKey = (worldRenderer: WorldRendererThree, handItem?: HandItemBlock) => {
16
+ if (!handItem) return 'empty'
17
+ if (handItem.type === 'hand') return 'hand'
18
+
19
+ const itemIdentifier = handItem.name ?? (handItem.id !== undefined ? `#${handItem.id}` : 'unknown')
20
+ if (!worldRenderer.resourcesManager.currentResources) {
21
+ return `${handItem.type}:${itemIdentifier}`
22
+ }
23
+
24
+ const renderData = worldRenderer.getItemRenderData({
25
+ ...handItem.fullItem,
26
+ itemId: handItem.id,
27
+ }, getFirstPersonItemIdentityProps())
28
+
29
+ return `${handItem.type}:${itemIdentifier}:${renderData.modelName}`
30
+ }
@@ -1,7 +1,6 @@
1
1
  //@ts-nocheck
2
2
  import * as THREE from 'three'
3
3
  import * as tweenJs from '@tweenjs/tween.js'
4
- import PrismarineItem from 'prismarine-item'
5
4
  import { WorldBlockProvider } from 'mc-assets/dist/worldBlockProvider'
6
5
  import { BlockModel } from 'mc-assets'
7
6
  import { DebugGui } from '../lib/DebugGui'
@@ -17,6 +16,7 @@ import { getThreeBlockModelGroup } from '../mesher/standaloneRenderer'
17
16
  import { IndexedData } from 'minecraft-data'
18
17
  import { WorldRendererConfig } from '../graphicsBackend'
19
18
  import { IHoldingBlock } from './holdingBlockTypes'
19
+ import { getFirstPersonItemSpecificProps, getHandItemRenderKey } from './holdingBlockItemIdentity'
20
20
 
21
21
  const rotationPositionData = {
22
22
  itemRight: {
@@ -103,6 +103,7 @@ export default class HoldingBlockLegacy implements IHoldingBlock {
103
103
  camera = new THREE.PerspectiveCamera(75, 1, 0.1, 100)
104
104
  stopUpdate = false
105
105
  lastHeldItem: HandItemBlock | undefined
106
+ lastHeldItemRenderKey: string | undefined
106
107
  isSwinging = false
107
108
  nextIterStopCallbacks: Array<() => void> | undefined
108
109
  idleAnimator: HandIdleAnimator | undefined
@@ -304,19 +305,7 @@ export default class HoldingBlockLegacy implements IHoldingBlock {
304
305
  }
305
306
 
306
307
  isDifferentItem(block: HandItemBlock | undefined) {
307
- const Item = PrismarineItem(this.worldRenderer.version)
308
- if (!this.lastHeldItem) {
309
- return true
310
- }
311
- if (this.lastHeldItem.name !== block?.name) {
312
- return true
313
- }
314
- // eslint-disable-next-line sonarjs/prefer-single-boolean-return
315
- if (!Item.equal(this.lastHeldItem.fullItem, block?.fullItem ?? {}) || JSON.stringify(this.lastHeldItem.fullItem.components) !== JSON.stringify(block?.fullItem?.components)) {
316
- return true
317
- }
318
-
319
- return false
308
+ return this.lastHeldItemRenderKey !== getHandItemRenderKey(this.worldRenderer, block)
320
309
  }
321
310
 
322
311
  updateCameraGroup() {
@@ -355,11 +344,7 @@ export default class HoldingBlockLegacy implements IHoldingBlock {
355
344
  const result = this.worldRenderer.entities.getItemMesh({
356
345
  ...handItem.fullItem,
357
346
  itemId: handItem.id,
358
- }, {
359
- 'minecraft:display_context': 'firstperson',
360
- 'minecraft:use_duration': this.worldRenderer.playerStateReactive.itemUsageTicks,
361
- 'minecraft:using_item': !!this.worldRenderer.playerStateReactive.itemUsageTicks,
362
- }, false, this.lastItemModelName)
347
+ }, getFirstPersonItemSpecificProps(this.worldRenderer), false, this.lastItemModelName)
363
348
  if (result) {
364
349
  const { mesh: itemMesh, isBlock, modelName } = result
365
350
  if (isBlock) {
@@ -421,10 +406,14 @@ export default class HoldingBlockLegacy implements IHoldingBlock {
421
406
 
422
407
  switchRequest = 0
423
408
  async setNewItem(handItem?: HandItemBlock) {
424
- if (!this.isDifferentItem(handItem)) return
409
+ const nextRenderKey = getHandItemRenderKey(this.worldRenderer, handItem)
410
+ const itemChanged = this.lastHeldItemRenderKey !== nextRenderKey
411
+ this.lastHeldItem = handItem
412
+ if (!itemChanged) return
413
+
414
+ this.lastHeldItemRenderKey = nextRenderKey
425
415
  this.lastItemModelName = undefined
426
416
  const switchRequest = ++this.switchRequest
427
- this.lastHeldItem = handItem
428
417
  let playAppearAnimation = false
429
418
  if (this.holdingBlock) {
430
419
  // play disappear animation
@@ -472,7 +461,8 @@ export default class HoldingBlockLegacy implements IHoldingBlock {
472
461
  await this.playBlockSwapAnimation('appeared')
473
462
  }
474
463
 
475
- this.swingAnimator = new HandSwingAnimator(this.holdingBlockInnerGroup)
464
+ this.swingAnimator ??= new HandSwingAnimator(this.holdingBlockInnerGroup)
465
+ this.swingAnimator.setHandMesh(this.holdingBlockInnerGroup)
476
466
  this.swingAnimator.type = result.type
477
467
  if (this.config.viewBobbing) {
478
468
  this.idleAnimator = new HandIdleAnimator(this.holdingBlockInnerGroup, this.worldRenderer.playerStateReactive)
@@ -783,9 +773,9 @@ class HandSwingAnimator {
783
773
  private lastTime = 0
784
774
  private isAnimating = false
785
775
  private stopRequested = false
786
- private readonly originalRotation: THREE.Euler
787
- private readonly originalPosition: THREE.Vector3
788
- private readonly originalScale: THREE.Vector3
776
+ private originalRotation: THREE.Euler
777
+ private originalPosition: THREE.Vector3
778
+ private originalScale: THREE.Vector3
789
779
 
790
780
  readonly debugParams = {
791
781
  // Animation timing
@@ -849,6 +839,13 @@ class HandSwingAnimator {
849
839
  // this.debugGui.activate()
850
840
  }
851
841
 
842
+ setHandMesh(handMesh: THREE.Object3D) {
843
+ this.handMesh = handMesh
844
+ this.originalRotation.copy(handMesh.rotation)
845
+ this.originalPosition.copy(handMesh.position)
846
+ this.originalScale.copy(handMesh.scale)
847
+ }
848
+
852
849
  update() {
853
850
  if (!this.isAnimating && !this.debugParams.animationStage) {
854
851
  // If not animating, ensure we're at original position
@@ -58,7 +58,7 @@ export class SciFiWorldRevealModule implements RendererModuleController {
58
58
  // Store original methods for patching
59
59
  private originalFinishChunk: ((chunkKey: string) => void) | null = null
60
60
  private originalDestroy: (() => void) | null = null
61
- private originalSceneAdd: ((...object: THREE.Object3D[]) => THREE.Group) | null = null
61
+ private originalSceneAdd: ((...object: THREE.Object3D[]) => THREE.Scene) | null = null
62
62
  private originalHandleWorkerMessage: ((data: { geometry: MesherGeometryOutput; key: string; type: string }) => void) | null = null
63
63
 
64
64
  private configEnabled = true
@@ -163,7 +163,7 @@ export class SciFiWorldRevealModule implements RendererModuleController {
163
163
 
164
164
  // Patch scene.add to intercept mesh additions
165
165
  this.originalSceneAdd = wr.scene.add.bind(wr.scene)
166
- wr.scene.add = (...objects: THREE.Object3D[]): THREE.Group => {
166
+ wr.scene.add = (...objects: THREE.Object3D[]): THREE.Scene => {
167
167
  // Call original add first
168
168
  const result = this.originalSceneAdd!(...objects)
169
169
 
@@ -174,7 +174,7 @@ export class SceneOrigin {
174
174
  /** Untrack an Object3D and remove it from the scene */
175
175
  removeAndUntrack(obj: Object3D): void {
176
176
  this.untrack(obj)
177
- this.scene.remove(obj)
177
+ obj.removeFromParent()
178
178
  }
179
179
 
180
180
  /** Untrack an Object3D and all its descendants, then remove from the scene */
@@ -182,7 +182,7 @@ export class SceneOrigin {
182
182
  obj.traverse((child) => {
183
183
  this.untrack(child)
184
184
  })
185
- this.scene.remove(obj)
185
+ obj.removeFromParent()
186
186
  }
187
187
 
188
188
  /** Get stored world position for a tracked object */