minecraft-renderer 0.1.27 → 0.1.29

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 (37) hide show
  1. package/dist/mesher.js +22 -22
  2. package/dist/mesher.js.map +3 -3
  3. package/dist/mesherWasm.js +17 -17
  4. package/dist/minecraft-renderer.js +65 -619
  5. package/dist/minecraft-renderer.js.meta.json +1 -0
  6. package/dist/threeWorker.js +453 -1007
  7. package/package.json +1 -1
  8. package/src/lib/guiRenderer.ts +0 -1
  9. package/src/lib/moreBlockDataGenerated.json +8 -0
  10. package/src/lib/skyLight.ts +125 -0
  11. package/src/lib/worldrendererCommon.ts +2 -12
  12. package/src/mesher/models.ts +28 -2
  13. package/src/mesher/test/mesherTester.ts +1 -1
  14. package/src/sign-renderer/index.ts +4 -1
  15. package/src/three/bannerRenderer.ts +5 -4
  16. package/src/three/cinimaticScript.ts +2 -2
  17. package/src/three/entities.ts +42 -11
  18. package/src/three/entity/EntityMesh.ts +1 -1
  19. package/src/three/entity/entities.json +108 -2
  20. package/src/three/entity/exportedModels.js +0 -1
  21. package/src/three/entity/externalTextures.json +1 -1
  22. package/src/three/fireworks.ts +18 -5
  23. package/src/three/fireworksRenderer.ts +15 -13
  24. package/src/three/modules/rain.ts +6 -1
  25. package/src/three/modules/sciFiWorldReveal.ts +14 -10
  26. package/src/three/modules/starfield.ts +1 -1
  27. package/src/three/sceneOrigin.ts +215 -0
  28. package/src/three/skyboxRenderer.ts +3 -3
  29. package/src/three/threeJsMedia.ts +12 -6
  30. package/src/three/threeJsParticles.ts +42 -14
  31. package/src/three/threeJsSound.ts +3 -3
  32. package/src/three/waypointSprite.ts +45 -23
  33. package/src/three/waypoints.ts +12 -4
  34. package/src/three/world/cursorBlock.ts +5 -5
  35. package/src/three/worldBlockGeometry.ts +14 -5
  36. package/src/three/worldGeometryExport.ts +4 -3
  37. package/src/three/worldRendererThree.ts +155 -30
@@ -0,0 +1,215 @@
1
+ //@ts-nocheck
2
+ import type { Object3D, Scene, Vector3 } from 'three'
3
+
4
+ const IS_TRACKED_PROXY = Symbol('tracked-proxy')
5
+
6
+ const MUTATING_METHODS = new Set([
7
+ 'add', 'addScalar', 'addScaledVector', 'addVectors',
8
+ 'sub', 'subScalar', 'subVectors',
9
+ 'multiply', 'multiplyScalar', 'multiplyVectors',
10
+ 'divide', 'divideScalar',
11
+ 'applyEuler', 'applyAxisAngle', 'applyMatrix3', 'applyMatrix4', 'applyNormalMatrix', 'applyQuaternion',
12
+ 'negate', 'floor', 'ceil', 'round', 'roundToZero',
13
+ 'min', 'max', 'clamp', 'clampLength', 'clampScalar',
14
+ 'project', 'unproject', 'reflect',
15
+ 'lerp', 'lerpVectors',
16
+ 'cross', 'crossVectors',
17
+ 'setFromMatrixPosition', 'setFromMatrixColumn', 'setFromMatrix3Column',
18
+ 'setFromEuler', 'setFromSpherical', 'setFromSphericalCoords', 'setFromCylindrical',
19
+ 'fromArray', 'fromBufferAttribute',
20
+ 'setComponent', 'randomDirection', 'random',
21
+ ])
22
+
23
+ interface TrackOptions {
24
+ updateMatrix?: boolean
25
+ }
26
+
27
+ export class SceneOrigin {
28
+ private scene: Scene
29
+
30
+ // World coordinates of origin in float64 (JavaScript number)
31
+ private _x = 0
32
+ private _y = 0
33
+ private _z = 0
34
+
35
+ private readonly _tracked = new Set<Object3D>()
36
+ private readonly _worldCoords = new WeakMap<Object3D, { x: number; y: number; z: number }>()
37
+ private readonly _originalPositions = new WeakMap<Object3D, Vector3>()
38
+ private readonly _trackOptions = new WeakMap<Object3D, TrackOptions>()
39
+
40
+ constructor(scene: Scene) {
41
+ this.scene = scene
42
+ }
43
+
44
+ get x(): number { return this._x }
45
+ get y(): number { return this._y }
46
+ get z(): number { return this._z }
47
+
48
+ /** Update origin (called each frame with camera world position) */
49
+ update(worldX: number, worldY: number, worldZ: number): void {
50
+ this._x = worldX
51
+ this._y = worldY
52
+ this._z = worldZ
53
+
54
+ for (const obj of this._tracked) {
55
+ const worldData = this._worldCoords.get(obj)!
56
+ const realPos = this._originalPositions.get(obj)!
57
+ realPos.set(worldData.x - worldX, worldData.y - worldY, worldData.z - worldZ)
58
+ const opts = this._trackOptions.get(obj)
59
+ if (opts?.updateMatrix) obj.updateMatrix()
60
+ }
61
+ }
62
+
63
+ /** Track an Object3D so its position is automatically adjusted on origin changes */
64
+ track(obj: Object3D, options?: TrackOptions): void {
65
+ // If already tracked, use the stored original position to avoid nesting Proxies
66
+ const realPosition = this._originalPositions.get(obj) ?? obj.position
67
+ const worldData = { x: 0, y: 0, z: 0 }
68
+
69
+ this._originalPositions.set(obj, realPosition)
70
+ this._worldCoords.set(obj, worldData)
71
+ if (options) {
72
+ this._trackOptions.set(obj, options)
73
+ } else {
74
+ this._trackOptions.delete(obj)
75
+ }
76
+
77
+ // eslint-disable-next-line @typescript-eslint/no-this-alias
78
+ const origin = this
79
+ const opts = options
80
+
81
+ const proxy = new Proxy(realPosition, {
82
+ get(target, prop, receiver) {
83
+ if (prop === IS_TRACKED_PROXY) return worldData
84
+ if (prop === 'set') {
85
+ return (x: number, y: number, z: number) => {
86
+ worldData.x = x; worldData.y = y; worldData.z = z
87
+ target.set(x - origin._x, y - origin._y, z - origin._z)
88
+ if (opts?.updateMatrix) obj.updateMatrix()
89
+ return receiver
90
+ }
91
+ }
92
+ if (prop === 'copy') {
93
+ return (v: Vector3) => {
94
+ const srcWorld = (v as any)[IS_TRACKED_PROXY] as { x: number; y: number; z: number } | undefined
95
+ const wx = srcWorld ? srcWorld.x : v.x
96
+ const wy = srcWorld ? srcWorld.y : v.y
97
+ const wz = srcWorld ? srcWorld.z : v.z
98
+ worldData.x = wx; worldData.y = wy; worldData.z = wz
99
+ target.set(wx - origin._x, wy - origin._y, wz - origin._z)
100
+ if (opts?.updateMatrix) obj.updateMatrix()
101
+ return receiver
102
+ }
103
+ }
104
+ if (prop === 'setX') {
105
+ return (val: number) => {
106
+ worldData.x = val; target.x = val - origin._x
107
+ if (opts?.updateMatrix) obj.updateMatrix()
108
+ return receiver
109
+ }
110
+ }
111
+ if (prop === 'setY') {
112
+ return (val: number) => {
113
+ worldData.y = val; target.y = val - origin._y
114
+ if (opts?.updateMatrix) obj.updateMatrix()
115
+ return receiver
116
+ }
117
+ }
118
+ if (prop === 'setZ') {
119
+ return (val: number) => {
120
+ worldData.z = val; target.z = val - origin._z
121
+ if (opts?.updateMatrix) obj.updateMatrix()
122
+ return receiver
123
+ }
124
+ }
125
+ if (typeof prop === 'string' && MUTATING_METHODS.has(prop)) {
126
+ return () => { throw new Error(`Cannot call position.${prop}() on a tracked object. Use position.set(x, y, z) instead.`) }
127
+ }
128
+ const value = (target as any)[prop]
129
+ if (typeof value === 'function') return value.bind(target)
130
+ return value
131
+ },
132
+ set(target, prop, value) {
133
+ if (prop === 'x') {
134
+ worldData.x = value; target.x = value - origin._x
135
+ if (opts?.updateMatrix) obj.updateMatrix()
136
+ return true
137
+ }
138
+ if (prop === 'y') {
139
+ worldData.y = value; target.y = value - origin._y
140
+ if (opts?.updateMatrix) obj.updateMatrix()
141
+ return true
142
+ }
143
+ if (prop === 'z') {
144
+ worldData.z = value; target.z = value - origin._z
145
+ if (opts?.updateMatrix) obj.updateMatrix()
146
+ return true
147
+ }
148
+ ;(target as any)[prop] = value
149
+ return true
150
+ }
151
+ })
152
+
153
+ Object.defineProperty(obj, 'position', { value: proxy, configurable: true, enumerable: true })
154
+ this._tracked.add(obj)
155
+ }
156
+
157
+ /** Stop tracking an Object3D, restoring its original position Vector3 */
158
+ untrack(obj: Object3D): void {
159
+ const originalPos = this._originalPositions.get(obj)
160
+ if (!originalPos) return
161
+ Object.defineProperty(obj, 'position', { value: originalPos, configurable: true, enumerable: true })
162
+ this._tracked.delete(obj)
163
+ this._worldCoords.delete(obj)
164
+ this._originalPositions.delete(obj)
165
+ this._trackOptions.delete(obj)
166
+ }
167
+
168
+ /** Track an Object3D and add it to the scene */
169
+ addAndTrack(obj: Object3D, options?: TrackOptions): void {
170
+ this.track(obj, options)
171
+ this.scene.add(obj)
172
+ }
173
+
174
+ /** Untrack an Object3D and remove it from the scene */
175
+ removeAndUntrack(obj: Object3D): void {
176
+ this.untrack(obj)
177
+ this.scene.remove(obj)
178
+ }
179
+
180
+ /** Untrack an Object3D and all its descendants, then remove from the scene */
181
+ removeAndUntrackAll(obj: Object3D): void {
182
+ obj.traverse((child) => {
183
+ this.untrack(child)
184
+ })
185
+ this.scene.remove(obj)
186
+ }
187
+
188
+ /** Get stored world position for a tracked object */
189
+ getWorldPosition(obj: Object3D): { x: number; y: number; z: number } | undefined {
190
+ const w = this._worldCoords.get(obj)
191
+ return w ? { x: w.x, y: w.y, z: w.z } : undefined
192
+ }
193
+
194
+ /** Clear all tracked objects (call on scene reset) */
195
+ clear(): void {
196
+ for (const obj of this._tracked) {
197
+ this.untrack(obj)
198
+ }
199
+ }
200
+
201
+ /** Number of currently tracked objects (for debugging) */
202
+ get trackedCount(): number {
203
+ return this._tracked.size
204
+ }
205
+
206
+ /** Convert world coordinates → scene coordinates */
207
+ toSceneX(worldX: number): number { return worldX - this._x }
208
+ toSceneY(worldY: number): number { return worldY - this._y }
209
+ toSceneZ(worldZ: number): number { return worldZ - this._z }
210
+
211
+ /** Convert scene coordinates → world coordinates */
212
+ toWorldX(sceneX: number): number { return sceneX + this._x }
213
+ toWorldY(sceneY: number): number { return sceneY + this._y }
214
+ toWorldZ(sceneZ: number): number { return sceneZ + this._z }
215
+ }
@@ -108,11 +108,11 @@ export class SkyboxRenderer {
108
108
 
109
109
  if (this.mesh) {
110
110
  // Update skybox position
111
- this.mesh.position.copy(cameraPosition)
111
+ this.mesh.position.set(0, 0, 0)
112
112
  } else if (this.skyMesh) {
113
113
  // Update gradient sky position
114
- this.skyMesh.position.copy(cameraPosition)
115
- this.voidMesh?.position.copy(cameraPosition)
114
+ this.skyMesh.position.set(0, 0, 0)
115
+ this.voidMesh?.position.set(0, 0, 0)
116
116
  this.updateSkyColors() // Update colors based on time of day
117
117
  }
118
118
  }
@@ -203,7 +203,7 @@ export class ThreeJsMedia {
203
203
  positionalAudio = new THREE.PositionalAudio(soundSystem.audioListener)
204
204
  positionalAudio.setRefDistance(6)
205
205
  positionalAudio.setVolume(volume)
206
- scene.add(positionalAudio)
206
+ this.worldRenderer.sceneOrigin.addAndTrack(positionalAudio)
207
207
  positionalAudio.position.set(props.position.x, props.position.y, props.position.z)
208
208
 
209
209
  // Connect video to positional audio
@@ -461,9 +461,9 @@ export class ThreeJsMedia {
461
461
  if (mediaData.positionalAudio) {
462
462
  // mediaData.positionalAudio.stop()
463
463
  // mediaData.positionalAudio.disconnect()
464
- scene.remove(mediaData.positionalAudio)
464
+ this.worldRenderer.sceneOrigin.removeAndUntrack(mediaData.positionalAudio)
465
465
  }
466
- scene.remove(mediaData.mesh)
466
+ this.worldRenderer.sceneOrigin.removeAndUntrack(mediaData.mesh)
467
467
  mediaData.texture.dispose()
468
468
 
469
469
  // Get the inner mesh from the group
@@ -549,7 +549,8 @@ export class ThreeJsMedia {
549
549
  mesh.geometry.translate(0.5, 0.5, 0)
550
550
  mesh.geometry.attributes.position.needsUpdate = true
551
551
 
552
- // Now place the mesh at the start position
552
+ // Now place the mesh at the start position (convert world → scene coords)
553
+ this.worldRenderer.sceneOrigin.track(mesh)
553
554
  mesh.position.set(startPosition.x, startPosition.y, startPosition.z)
554
555
 
555
556
  // Create a group to hold our mesh and markers
@@ -561,7 +562,7 @@ export class ThreeJsMedia {
561
562
  new THREE.BoxGeometry(0.1, 0.1, 0.1),
562
563
  new THREE.MeshBasicMaterial({ color: 0xff_00_00 })
563
564
  )
564
- startMarker.position.copy(new THREE.Vector3(startPosition.x, startPosition.y, startPosition.z))
565
+ startMarker.position.set(startPosition.x, startPosition.y, startPosition.z)
565
566
  debugGroup.add(startMarker)
566
567
 
567
568
  // Add a marker at the end position (width units away in the rotated direction)
@@ -591,9 +592,14 @@ export class ThreeJsMedia {
591
592
  debugGroup.add(endCornerMarker)
592
593
 
593
594
  // Also add a visual helper to show the rotation direction
595
+ const sceneStartPos = new THREE.Vector3(
596
+ this.worldRenderer.sceneOrigin.toSceneX(startPosition.x),
597
+ this.worldRenderer.sceneOrigin.toSceneY(startPosition.y),
598
+ this.worldRenderer.sceneOrigin.toSceneZ(startPosition.z)
599
+ )
594
600
  const directionHelper = new THREE.ArrowHelper(
595
601
  new THREE.Vector3(Math.cos(rotation), 0, Math.sin(rotation)),
596
- new THREE.Vector3(startPosition.x, startPosition.y, startPosition.z),
602
+ sceneStartPos,
597
603
  1,
598
604
  0xff_00_00
599
605
  )
@@ -1,8 +1,10 @@
1
1
  //@ts-nocheck
2
2
  import * as THREE from 'three'
3
+ import type { SceneOrigin } from './sceneOrigin'
3
4
 
4
5
  interface ParticleMesh extends THREE.Mesh {
5
6
  velocity: THREE.Vector3;
7
+ worldPos: THREE.Vector3;
6
8
  }
7
9
 
8
10
  interface ParticleConfig {
@@ -24,11 +26,13 @@ export class Fountain {
24
26
  private readonly particles: ParticleMesh[] = []
25
27
  private readonly config: { particleConfig: ParticleConfig }
26
28
  private readonly position: THREE.Vector3
29
+ private readonly sceneOrigin: SceneOrigin | undefined
27
30
  container: THREE.Object3D | undefined
28
31
 
29
- constructor (public sectionId: string, options: FountainOptions = {}) {
32
+ constructor (public sectionId: string, options: FountainOptions = {}, sceneOrigin?: SceneOrigin) {
30
33
  this.position = options.position ? new THREE.Vector3(options.position.x, options.position.y, options.position.z) : new THREE.Vector3(0, 0, 0)
31
34
  this.config = this.createConfig(options.particleConfig)
35
+ this.sceneOrigin = sceneOrigin
32
36
  }
33
37
 
34
38
  private createConfig (
@@ -48,6 +52,18 @@ export class Fountain {
48
52
  return { particleConfig }
49
53
  }
50
54
 
55
+ private toSceneX (worldX: number): number {
56
+ return this.sceneOrigin ? this.sceneOrigin.toSceneX(worldX) : worldX
57
+ }
58
+
59
+ private toSceneY (worldY: number): number {
60
+ return this.sceneOrigin ? this.sceneOrigin.toSceneY(worldY) : worldY
61
+ }
62
+
63
+ private toSceneZ (worldZ: number): number {
64
+ return this.sceneOrigin ? this.sceneOrigin.toSceneZ(worldZ) : worldZ
65
+ }
66
+
51
67
 
52
68
  createParticles (container: THREE.Object3D): void {
53
69
  this.container = container
@@ -65,11 +81,12 @@ export class Fountain {
65
81
  const mesh = new THREE.Mesh(geometry, material)
66
82
  const particle = mesh as unknown as ParticleMesh
67
83
 
68
- particle.position.set(
69
- this.position.x + (Math.random() - 0.5) * this.config.particleConfig.xVelocityRange * 2,
70
- this.position.y + this.config.particleConfig.fountainHeight,
71
- this.position.z + (Math.random() - 0.5) * this.config.particleConfig.zVelocityRange * 2
72
- )
84
+ const worldX = this.position.x + (Math.random() - 0.5) * this.config.particleConfig.xVelocityRange * 2
85
+ const worldY = this.position.y + this.config.particleConfig.fountainHeight
86
+ const worldZ = this.position.z + (Math.random() - 0.5) * this.config.particleConfig.zVelocityRange * 2
87
+
88
+ particle.worldPos = new THREE.Vector3(worldX, worldY, worldZ)
89
+ particle.position.set(this.toSceneX(worldX), this.toSceneY(worldY), this.toSceneZ(worldZ))
73
90
 
74
91
  particle.velocity = new THREE.Vector3(
75
92
  (Math.random() - 0.5) * this.config.particleConfig.xVelocityRange,
@@ -89,20 +106,26 @@ export class Fountain {
89
106
  render (): void {
90
107
  for (const particle of this.particles) {
91
108
  particle.velocity.y -= 0.01 + Math.random() * 0.1
92
- particle.position.add(particle.velocity)
109
+ particle.worldPos.add(particle.velocity)
93
110
 
94
- if (particle.position.y < this.position.y + this.config.particleConfig.resetHeight) {
95
- particle.position.set(
96
- this.position.x + (Math.random() - 0.5) * this.config.particleConfig.xVelocityRange * 2,
97
- this.position.y + this.config.particleConfig.fountainHeight,
98
- this.position.z + (Math.random() - 0.5) * this.config.particleConfig.zVelocityRange * 2
99
- )
111
+ if (particle.worldPos.y < this.position.y + this.config.particleConfig.resetHeight) {
112
+ const worldX = this.position.x + (Math.random() - 0.5) * this.config.particleConfig.xVelocityRange * 2
113
+ const worldY = this.position.y + this.config.particleConfig.fountainHeight
114
+ const worldZ = this.position.z + (Math.random() - 0.5) * this.config.particleConfig.zVelocityRange * 2
115
+
116
+ particle.worldPos.set(worldX, worldY, worldZ)
100
117
  particle.velocity.set(
101
118
  (Math.random() - 0.5) * this.config.particleConfig.xVelocityRange,
102
119
  -Math.random() * this.config.particleConfig.yVelocityRange.max,
103
120
  (Math.random() - 0.5) * this.config.particleConfig.zVelocityRange
104
121
  )
105
122
  }
123
+
124
+ particle.position.set(
125
+ this.toSceneX(particle.worldPos.x),
126
+ this.toSceneY(particle.worldPos.y),
127
+ this.toSceneZ(particle.worldPos.z)
128
+ )
106
129
  }
107
130
  }
108
131
 
@@ -126,7 +149,12 @@ export class Fountain {
126
149
  for (let i = 0; i < count; i++) {
127
150
  const mesh = new THREE.Mesh(geometry, material)
128
151
  const particle = mesh as unknown as ParticleMesh
129
- particle.position.copy(this.position)
152
+ particle.worldPos = this.position.clone()
153
+ particle.position.set(
154
+ this.toSceneX(this.position.x),
155
+ this.toSceneY(this.position.y),
156
+ this.toSceneZ(this.position.z)
157
+ )
130
158
  particle.velocity = new THREE.Vector3(
131
159
  Math.random() * this.config.particleConfig.xVelocityRange -
132
160
  this.config.particleConfig.xVelocityRange / 2,
@@ -45,11 +45,11 @@ export class ThreeJsSound implements SoundSystem {
45
45
  sound.setRefDistance(20)
46
46
  sound.setVolume(volume * this.baseVolume)
47
47
  sound.setPlaybackRate(pitch) // set the pitch
48
- this.worldRenderer.scene.add(sound)
49
48
  // set sound position
49
+ this.worldRenderer.sceneOrigin.addAndTrack(sound)
50
50
  sound.position.set(position.x, position.y, position.z)
51
51
  sound.onEnded = () => {
52
- this.worldRenderer.scene.remove(sound)
52
+ this.worldRenderer.sceneOrigin.removeAndUntrack(sound)
53
53
  if (sound.source) {
54
54
  sound.disconnect()
55
55
  }
@@ -68,7 +68,7 @@ export class ThreeJsSound implements SoundSystem {
68
68
  if (sound.source) {
69
69
  sound.disconnect()
70
70
  }
71
- this.worldRenderer.scene.remove(sound)
71
+ this.worldRenderer.sceneOrigin.removeAndUntrack(sound)
72
72
  }
73
73
  this.activeSounds.clear()
74
74
  this.soundVolumes.clear()
@@ -21,6 +21,10 @@ export const WAYPOINT_CONFIG = {
21
21
  pixelSize: 50,
22
22
  paddingPx: 50,
23
23
  },
24
+ // Default visual scale factor (can be overridden globally or per-waypoint)
25
+ DEFAULT_VISUAL_SCALE: 1,
26
+ // Default opacity (can be overridden globally or per-waypoint)
27
+ DEFAULT_OPACITY: 1,
24
28
  }
25
29
 
26
30
  export type WaypointSprite = {
@@ -53,14 +57,31 @@ export function createWaypointSprite (options: {
53
57
  // Y offset in world units used by updateScaleWorld only (screen-pixel API ignores this)
54
58
  labelYOffset?: number,
55
59
  metadata?: any,
60
+ visualScale?: number,
61
+ opacity?: number,
56
62
  }): WaypointSprite {
57
63
  const color = options.color ?? 0xFF_00_00
58
64
  const depthTest = options.depthTest ?? false
59
65
  const labelYOffset = options.labelYOffset ?? 1.5
60
66
 
67
+ // Get visual scale from options, metadata, server metadata, or default
68
+ // Priority: options.visualScale > metadata.visualScale > window.serverMetadata?.waypointVisualScale > DEFAULT
69
+ const visualScale = options.visualScale
70
+ ?? options.metadata?.visualScale
71
+ ?? (typeof window === 'undefined' ? undefined : (window as any).serverMetadata?.waypointVisualScale)
72
+ ?? WAYPOINT_CONFIG.DEFAULT_VISUAL_SCALE
73
+
74
+ // Get opacity from options, metadata, server metadata, or default
75
+ // Priority: options.opacity > metadata.opacity > window.serverMetadata?.waypointOpacity > DEFAULT
76
+ const opacity = options.opacity
77
+ ?? options.metadata?.opacity
78
+ ?? (typeof window === 'undefined' ? undefined : (window as any).serverMetadata?.waypointOpacity)
79
+ ?? WAYPOINT_CONFIG.DEFAULT_OPACITY
80
+
61
81
  // Build combined sprite
62
- const sprite = createCombinedSprite(color, options.label ?? '', '0m', depthTest)
82
+ const sprite = createCombinedSprite(color, options.label ?? '', '0m', depthTest, visualScale)
63
83
  sprite.renderOrder = 10
84
+ sprite.material.opacity = opacity
64
85
  let currentLabel = options.label ?? ''
65
86
 
66
87
  // Performance optimization: cache distance text to avoid unnecessary updates
@@ -81,7 +102,7 @@ export function createWaypointSprite (options: {
81
102
  group.position.set(x, y, z)
82
103
 
83
104
  function setColor (newColor: number) {
84
- const canvas = drawCombinedCanvas(newColor, currentLabel, '0m')
105
+ const canvas = drawCombinedCanvas(newColor, currentLabel, '0m', visualScale)
85
106
  const texture = new THREE.CanvasTexture(canvas)
86
107
  const mat = sprite.material
87
108
  mat.map?.dispose()
@@ -91,7 +112,7 @@ export function createWaypointSprite (options: {
91
112
 
92
113
  function setLabel (newLabel?: string) {
93
114
  currentLabel = newLabel ?? ''
94
- const canvas = drawCombinedCanvas(color, currentLabel, '0m')
115
+ const canvas = drawCombinedCanvas(color, currentLabel, '0m', visualScale)
95
116
  const texture = new THREE.CanvasTexture(canvas)
96
117
  const mat = sprite.material
97
118
  mat.map?.dispose()
@@ -106,7 +127,7 @@ export function createWaypointSprite (options: {
106
127
  }
107
128
  lastDistanceText = distanceText
108
129
 
109
- const canvas = drawCombinedCanvas(color, label, distanceText)
130
+ const canvas = drawCombinedCanvas(color, label, distanceText, visualScale)
110
131
  const texture = new THREE.CanvasTexture(canvas)
111
132
  const mat = sprite.material
112
133
  mat.map?.dispose()
@@ -131,8 +152,8 @@ export function createWaypointSprite (options: {
131
152
  ) {
132
153
  const vFovRad = cameraFov * Math.PI / 180
133
154
  const worldUnitsPerScreenHeightAtDist = Math.tan(vFovRad / 2) * 2 * distance
134
- // Use configured target screen size
135
- const scale = worldUnitsPerScreenHeightAtDist * (WAYPOINT_CONFIG.TARGET_SCREEN_PX / viewportHeightPx)
155
+ // Use configured target screen size with visual scale multiplier
156
+ const scale = worldUnitsPerScreenHeightAtDist * (WAYPOINT_CONFIG.TARGET_SCREEN_PX * visualScale / viewportHeightPx)
136
157
  sprite.scale.set(scale, scale, 1)
137
158
  }
138
159
 
@@ -159,7 +180,7 @@ export function createWaypointSprite (options: {
159
180
  ctx.fill()
160
181
 
161
182
  const texture = new THREE.CanvasTexture(canvas)
162
- const material = new THREE.SpriteMaterial({ map: texture, transparent: true, depthTest: false, depthWrite: false })
183
+ const material = new THREE.SpriteMaterial({ map: texture, transparent: true, depthTest: false, depthWrite: false, opacity })
163
184
  arrowSprite = new THREE.Sprite(material)
164
185
  arrowSprite.renderOrder = 12
165
186
  arrowSprite.visible = false
@@ -278,15 +299,16 @@ export function createWaypointSprite (options: {
278
299
  const angle = Math.atan2(ry, rx)
279
300
  arrowSprite.material.rotation = angle - Math.PI / 2
280
301
 
281
- // Constant pixel size for arrow (use fixed placement distance)
302
+ // Constant pixel size for arrow (use fixed placement distance) with visual scale
282
303
  const worldUnitsPerScreenHeightAtDist = Math.tan(vFovRad / 2) * 2 * placeDist
283
- const sPx = worldUnitsPerScreenHeightAtDist * (WAYPOINT_CONFIG.ARROW.pixelSize / viewportHeightPx)
304
+ const sPx = worldUnitsPerScreenHeightAtDist * (WAYPOINT_CONFIG.ARROW.pixelSize * visualScale / viewportHeightPx)
284
305
  arrowSprite.scale.set(sPx, sPx, 1)
285
306
  return false
286
307
  }
287
308
 
288
- function computeDistance (cameraPosition: THREE.Vector3): number {
289
- return cameraPosition.distanceTo(group.position)
309
+ function computeDistance (_cameraPosition: THREE.Vector3): number {
310
+ // group.position is in scene space; camera is at scene origin (0,0,0)
311
+ return group.position.length()
290
312
  }
291
313
 
292
314
  function updateForCamera (
@@ -343,7 +365,7 @@ export function createWaypointSprite (options: {
343
365
  }
344
366
 
345
367
  // Internal helpers
346
- function drawCombinedCanvas (color: number, id: string, distance: string): OffscreenCanvas {
368
+ function drawCombinedCanvas (color: number, id: string, distance: string, visualScale = 1): OffscreenCanvas {
347
369
  const scale = WAYPOINT_CONFIG.CANVAS_SCALE * (globalThis.devicePixelRatio || 1)
348
370
  const size = WAYPOINT_CONFIG.CANVAS_SIZE * scale
349
371
  const canvas = createCanvas(size, size)
@@ -352,11 +374,11 @@ function drawCombinedCanvas (color: number, id: string, distance: string): Offsc
352
374
  // Clear canvas
353
375
  ctx.clearRect(0, 0, size, size)
354
376
 
355
- // Draw dot
377
+ // Draw dot with visual scale applied
356
378
  const centerX = size / 2
357
379
  const dotY = Math.round(size * WAYPOINT_CONFIG.LAYOUT.DOT_Y)
358
- const radius = Math.round(size * 0.05) // Dot takes up ~12% of canvas height
359
- const borderWidth = Math.max(2, Math.round(4 * scale))
380
+ const radius = Math.round(size * 0.05 * visualScale) // Dot takes up ~5% of canvas height, scaled
381
+ const borderWidth = Math.max(2, Math.round(4 * scale * visualScale))
360
382
 
361
383
  // Outer border (black)
362
384
  ctx.beginPath()
@@ -374,11 +396,11 @@ function drawCombinedCanvas (color: number, id: string, distance: string): Offsc
374
396
  ctx.textAlign = 'center'
375
397
  ctx.textBaseline = 'middle'
376
398
 
377
- // Title
378
- const nameFontPx = Math.round(size * 0.08) // ~8% of canvas height
379
- const distanceFontPx = Math.round(size * 0.06) // ~6% of canvas height
399
+ // Title with visual scale applied
400
+ const nameFontPx = Math.round(size * 0.08 * visualScale) // ~8% of canvas height, scaled
401
+ const distanceFontPx = Math.round(size * 0.06 * visualScale) // ~6% of canvas height, scaled
380
402
  ctx.font = `bold ${nameFontPx}px mojangles`
381
- ctx.lineWidth = Math.max(2, Math.round(3 * scale))
403
+ ctx.lineWidth = Math.max(2, Math.round(3 * scale * visualScale))
382
404
  const nameY = Math.round(size * WAYPOINT_CONFIG.LAYOUT.NAME_Y)
383
405
 
384
406
  ctx.strokeStyle = 'black'
@@ -386,9 +408,9 @@ function drawCombinedCanvas (color: number, id: string, distance: string): Offsc
386
408
  ctx.fillStyle = 'white'
387
409
  ctx.fillText(id, centerX, nameY)
388
410
 
389
- // Distance
411
+ // Distance with visual scale applied
390
412
  ctx.font = `bold ${distanceFontPx}px mojangles`
391
- ctx.lineWidth = Math.max(2, Math.round(2 * scale))
413
+ ctx.lineWidth = Math.max(2, Math.round(2 * scale * visualScale))
392
414
  const distanceY = Math.round(size * WAYPOINT_CONFIG.LAYOUT.DISTANCE_Y)
393
415
 
394
416
  ctx.strokeStyle = 'black'
@@ -399,8 +421,8 @@ function drawCombinedCanvas (color: number, id: string, distance: string): Offsc
399
421
  return canvas
400
422
  }
401
423
 
402
- function createCombinedSprite (color: number, id: string, distance: string, depthTest: boolean): THREE.Sprite {
403
- const canvas = drawCombinedCanvas(color, id, distance)
424
+ function createCombinedSprite (color: number, id: string, distance: string, depthTest: boolean, visualScale = 1): THREE.Sprite {
425
+ const canvas = drawCombinedCanvas(color, id, distance, visualScale)
404
426
  const texture = new THREE.CanvasTexture(canvas)
405
427
  texture.anisotropy = 1
406
428
  texture.magFilter = THREE.LinearFilter
@@ -9,6 +9,7 @@ interface Waypoint {
9
9
  y: number
10
10
  z: number
11
11
  minDistance: number
12
+ maxDistance: number
12
13
  color: number
13
14
  label?: string
14
15
  sprite: WaypointSprite
@@ -18,6 +19,7 @@ interface WaypointOptions {
18
19
  color?: number
19
20
  label?: string
20
21
  minDistance?: number
22
+ maxDistance?: number
21
23
  metadata?: any
22
24
  }
23
25
 
@@ -40,7 +42,7 @@ export class WaypointsRenderer {
40
42
 
41
43
  private updateWaypoints() {
42
44
  const currentTime = performance.now()
43
- const playerPos = this.worldRenderer.cameraObject.position
45
+ const playerPos = this.worldRenderer.getCameraPosition()
44
46
 
45
47
  // Performance optimization: throttle updates and check for significant camera movement
46
48
  const cameraMovedSignificantly = this.lastCameraPosition.distanceTo(playerPos) > 0.5
@@ -58,13 +60,18 @@ export class WaypointsRenderer {
58
60
  for (const waypoint of this.waypoints.values()) {
59
61
  const waypointPos = new THREE.Vector3(waypoint.x, waypoint.y, waypoint.z)
60
62
  const distance = playerPos.distanceTo(waypointPos)
61
- const visible = !waypoint.minDistance || distance >= waypoint.minDistance
63
+ const visible = (!waypoint.minDistance || distance >= waypoint.minDistance) &&
64
+ (waypoint.maxDistance === Infinity || distance <= waypoint.maxDistance)
62
65
 
63
66
  waypoint.sprite.setVisible(visible)
64
67
 
65
68
  if (visible) {
66
69
  // Update position
67
- waypoint.sprite.setPosition(waypoint.x, waypoint.y, waypoint.z)
70
+ waypoint.sprite.setPosition(
71
+ this.worldRenderer.sceneOrigin.toSceneX(waypoint.x),
72
+ this.worldRenderer.sceneOrigin.toSceneY(waypoint.y),
73
+ this.worldRenderer.sceneOrigin.toSceneZ(waypoint.z)
74
+ )
68
75
  // Ensure camera-based update each frame
69
76
  waypoint.sprite.updateForCamera(this.worldRenderer.getCameraPosition(), this.worldRenderer.camera, sizeVec.width, sizeVec.height)
70
77
  }
@@ -96,6 +103,7 @@ export class WaypointsRenderer {
96
103
  const color = options.color ?? 0xFF_00_00
97
104
  const { label, metadata } = options
98
105
  const minDistance = options.minDistance ?? 0
106
+ const maxDistance = options.maxDistance ?? Infinity
99
107
 
100
108
  const sprite = createWaypointSprite({
101
109
  position: new THREE.Vector3(x, y, z),
@@ -109,7 +117,7 @@ export class WaypointsRenderer {
109
117
  this.waypointScene.add(sprite.group)
110
118
 
111
119
  this.waypoints.set(id, {
112
- id, x: x + 0.5, y: y + 0.5, z: z + 0.5, minDistance,
120
+ id, x: x + 0.5, y: y + 0.5, z: z + 0.5, minDistance, maxDistance,
113
121
  color, label,
114
122
  sprite,
115
123
  })