minecraft-renderer 0.1.42 → 0.1.44
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/dist/mesher.js +52 -52
- package/dist/mesher.js.map +3 -3
- package/dist/mesherWasm.js +51 -51
- package/dist/minecraft-renderer.js +59 -59
- package/dist/minecraft-renderer.js.meta.json +1 -1
- package/dist/threeWorker.js +458 -458
- package/package.json +1 -1
- package/src/graphicsBackend/appViewer.ts +19 -7
- package/src/graphicsBackend/types.ts +5 -1
- package/src/index.ts +33 -0
- package/src/lib/ui/newStats.ts +16 -2
- package/src/lib/worldrendererCommon.ts +3 -2
- package/src/mesher-legacy/mesher.ts +10 -1
- package/src/mesher-shared/models.ts +41 -0
- package/src/mesher-shared/shared.ts +2 -0
- package/src/three/entities.ts +22 -6
- package/src/three/graphicsBackendBase.ts +28 -20
- package/src/three/graphicsBackendOffThread.ts +1 -2
- package/src/three/menuBackground/activeView.ts +19 -0
- package/src/three/menuBackground/classic.ts +148 -0
- package/src/three/menuBackground/config.ts +23 -0
- package/src/three/menuBackground/defaultOptions.ts +141 -0
- package/src/three/menuBackground/futuristic.ts +859 -0
- package/src/three/menuBackground/index.ts +36 -0
- package/src/three/menuBackground/renderer.ts +97 -0
- package/src/three/menuBackground/shared.ts +3 -0
- package/src/three/menuBackground/types.ts +37 -0
- package/src/three/menuBackground/worldBlocks.ts +144 -0
- package/src/three/modules/sciFiWorldReveal.ts +6 -0
- package/src/three/waypointSprite.ts +108 -106
- package/src/three/worldRendererThree.ts +10 -13
- package/src/wasm-mesher/runtime-build/wasm_mesher.d.ts +25 -0
- package/src/wasm-mesher/runtime-build/wasm_mesher.js +68 -0
- package/src/wasm-mesher/runtime-build/wasm_mesher_bg.wasm +0 -0
- package/src/wasm-mesher/runtime-build/wasm_mesher_bg.wasm.d.ts +2 -0
- package/src/wasm-mesher/worker/mesherWasm.ts +14 -0
- package/src/three/panorama.ts +0 -312
- package/src/three/panoramaShared.ts +0 -2
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
//@ts-nocheck
|
|
2
|
+
export { MENU_BACKGROUND_MC_VERSION } from './shared'
|
|
3
|
+
export type { MenuBackgroundView } from './activeView'
|
|
4
|
+
export { resizeMenuBackgroundCamera } from './activeView'
|
|
5
|
+
export type { MenuBackgroundMode, MenuBackgroundOptions } from './types'
|
|
6
|
+
export { resolveMenuBackgroundMode } from './types'
|
|
7
|
+
export { ClassicMenuBackground } from './classic'
|
|
8
|
+
export type {
|
|
9
|
+
FuturisticSceneId,
|
|
10
|
+
FuturisticCameraId,
|
|
11
|
+
FuturisticMenuBackgroundOptions,
|
|
12
|
+
MinecraftBlockGroupId
|
|
13
|
+
} from './futuristic'
|
|
14
|
+
export {
|
|
15
|
+
FuturisticMenuBackground,
|
|
16
|
+
FUTURISTIC_SCENE_IDS,
|
|
17
|
+
FUTURISTIC_CAMERA_IDS,
|
|
18
|
+
FUTURISTIC_SCENE_LABELS,
|
|
19
|
+
FUTURISTIC_CAMERA_LABELS,
|
|
20
|
+
MINECRAFT_BLOCK_GROUPS,
|
|
21
|
+
MINECRAFT_BLOCK_GROUP_IDS,
|
|
22
|
+
MINECRAFT_BLOCK_GROUP_LABELS
|
|
23
|
+
} from './futuristic'
|
|
24
|
+
export { WorldBlocksMenuBackground } from './worldBlocks'
|
|
25
|
+
export { MenuBackgroundRenderer } from './renderer'
|
|
26
|
+
export {
|
|
27
|
+
MENU_BACKGROUND_OPTION_DEFAULTS,
|
|
28
|
+
MENU_BACKGROUND_MOTION_DEFAULTS,
|
|
29
|
+
menuBackgroundSpeedToMultiplier
|
|
30
|
+
} from './config'
|
|
31
|
+
export {
|
|
32
|
+
RENDERER_DEFAULT_OPTIONS,
|
|
33
|
+
RENDERER_OPTIONS_META,
|
|
34
|
+
RENDERER_RENDER_GUI_SECTIONS
|
|
35
|
+
} from './defaultOptions'
|
|
36
|
+
export type { RendererDefaultOptionKey, RendererOptionMeta } from './defaultOptions'
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
//@ts-nocheck
|
|
2
|
+
import * as THREE from 'three'
|
|
3
|
+
import type { GraphicsInitOptions } from '../../graphicsBackend/types'
|
|
4
|
+
import type { DocumentRenderer } from '../documentRenderer'
|
|
5
|
+
import { ClassicMenuBackground } from './classic'
|
|
6
|
+
import { FuturisticMenuBackground } from './futuristic'
|
|
7
|
+
import { WorldBlocksMenuBackground } from './worldBlocks'
|
|
8
|
+
import type { MenuBackgroundView } from './activeView'
|
|
9
|
+
import type { MenuBackgroundOptions } from './types'
|
|
10
|
+
import { resolveMenuBackgroundMode } from './types'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Orchestrates main-menu background rendering (dispatches to classic / futuristic / world-blocks).
|
|
14
|
+
*/
|
|
15
|
+
export class MenuBackgroundRenderer {
|
|
16
|
+
private active?: MenuBackgroundView
|
|
17
|
+
private readonly abortController = new AbortController()
|
|
18
|
+
private readonly mode: ReturnType<typeof resolveMenuBackgroundMode>
|
|
19
|
+
private lastFrameTime = 0
|
|
20
|
+
|
|
21
|
+
constructor(
|
|
22
|
+
private readonly documentRenderer: DocumentRenderer,
|
|
23
|
+
private readonly options: GraphicsInitOptions,
|
|
24
|
+
menuBackgroundOptions: MenuBackgroundOptions = {},
|
|
25
|
+
singleFileBuild = false
|
|
26
|
+
) {
|
|
27
|
+
this.mode = resolveMenuBackgroundMode(menuBackgroundOptions, singleFileBuild)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Active futuristic instance when that style is running. */
|
|
31
|
+
get futuristic(): FuturisticMenuBackground | undefined {
|
|
32
|
+
return this.active instanceof FuturisticMenuBackground ? this.active : undefined
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
get scene(): THREE.Scene | undefined {
|
|
36
|
+
return this.active?.scene
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
get camera(): THREE.PerspectiveCamera | undefined {
|
|
40
|
+
return this.active?.camera
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async start(menuBackgroundOptions: MenuBackgroundOptions = {}) {
|
|
44
|
+
this.active = this.createImplementation(menuBackgroundOptions)
|
|
45
|
+
await this.active.init()
|
|
46
|
+
|
|
47
|
+
if (this.active.scene.background instanceof THREE.Color) {
|
|
48
|
+
this.documentRenderer.renderer.setClearColor(this.active.scene.background)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
this.lastFrameTime = performance.now()
|
|
52
|
+
this.documentRenderer.render = (sizeChanged = false) => {
|
|
53
|
+
const now = performance.now()
|
|
54
|
+
const dt = Math.min((now - this.lastFrameTime) / 1000, 0.05)
|
|
55
|
+
this.lastFrameTime = now
|
|
56
|
+
|
|
57
|
+
const view = this.active
|
|
58
|
+
if (!view) return
|
|
59
|
+
|
|
60
|
+
view.update(dt, sizeChanged)
|
|
61
|
+
this.documentRenderer.renderer.render(view.scene, view.camera)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
private createImplementation(options: MenuBackgroundOptions): MenuBackgroundView {
|
|
66
|
+
switch (this.mode) {
|
|
67
|
+
case 'futuristic':
|
|
68
|
+
return new FuturisticMenuBackground(
|
|
69
|
+
this.documentRenderer,
|
|
70
|
+
{
|
|
71
|
+
useMinecraftTextures: options.useMinecraftTextures,
|
|
72
|
+
initialScene: options.futuristicScene,
|
|
73
|
+
initialCamera: options.futuristicCamera,
|
|
74
|
+
initialBlockGroup: options.futuristicBlockGroup,
|
|
75
|
+
initialCameraSpeed: options.futuristicCameraSpeed,
|
|
76
|
+
initialBlockSpeed: options.futuristicBlockSpeed,
|
|
77
|
+
resourcesManager: options.resourcesManager
|
|
78
|
+
},
|
|
79
|
+
this.abortController.signal
|
|
80
|
+
)
|
|
81
|
+
case 'worldBlocks':
|
|
82
|
+
return new WorldBlocksMenuBackground(
|
|
83
|
+
this.documentRenderer,
|
|
84
|
+
this.options,
|
|
85
|
+
this.abortController.signal
|
|
86
|
+
)
|
|
87
|
+
default:
|
|
88
|
+
return new ClassicMenuBackground(this.documentRenderer)
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
dispose() {
|
|
93
|
+
this.active?.dispose()
|
|
94
|
+
this.active = undefined
|
|
95
|
+
this.abortController.abort()
|
|
96
|
+
}
|
|
97
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
//@ts-nocheck
|
|
2
|
+
import type { ResourcesManager } from '../../resourcesManager/resourcesManager'
|
|
3
|
+
import type { FuturisticCameraId, FuturisticSceneId, MinecraftBlockGroupId } from './futuristic'
|
|
4
|
+
import { MENU_BACKGROUND_OPTION_DEFAULTS } from './config'
|
|
5
|
+
|
|
6
|
+
export type { FuturisticCameraId, FuturisticSceneId, MinecraftBlockGroupId } from './futuristic'
|
|
7
|
+
|
|
8
|
+
export type MenuBackgroundMode = 'classic' | 'futuristic' | 'worldBlocks'
|
|
9
|
+
|
|
10
|
+
export interface MenuBackgroundOptions {
|
|
11
|
+
/** Visual style. Defaults to {@link MENU_BACKGROUND_OPTION_DEFAULTS.mode}, or `worldBlocks` in single-file build. */
|
|
12
|
+
mode?: MenuBackgroundMode
|
|
13
|
+
/** Futuristic style: load block atlas and render textured cubes (requires assets / mcData). */
|
|
14
|
+
useMinecraftTextures?: boolean
|
|
15
|
+
futuristicScene?: FuturisticSceneId
|
|
16
|
+
futuristicCamera?: FuturisticCameraId
|
|
17
|
+
/** Block pool when {@link useMinecraftTextures} is enabled. */
|
|
18
|
+
futuristicBlockGroup?: MinecraftBlockGroupId
|
|
19
|
+
/** Camera path speed (1 = 100%). */
|
|
20
|
+
futuristicCameraSpeed?: number
|
|
21
|
+
/** Block fly-through + sky drift speed (1 = 100%). */
|
|
22
|
+
futuristicBlockSpeed?: number
|
|
23
|
+
/**
|
|
24
|
+
* Optional shared resource manager (e.g. appViewer.resourcesManager).
|
|
25
|
+
* Caller should run `updateAssetsData` after mcData is loaded when using textured cubes.
|
|
26
|
+
*/
|
|
27
|
+
resourcesManager?: ResourcesManager
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function resolveMenuBackgroundMode(
|
|
31
|
+
options?: MenuBackgroundOptions,
|
|
32
|
+
singleFileBuild = false
|
|
33
|
+
): MenuBackgroundMode {
|
|
34
|
+
if (options?.mode) return options.mode
|
|
35
|
+
if (singleFileBuild) return 'worldBlocks'
|
|
36
|
+
return MENU_BACKGROUND_OPTION_DEFAULTS.mode
|
|
37
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
//@ts-nocheck
|
|
2
|
+
import * as THREE from 'three'
|
|
3
|
+
import { Vec3 } from 'vec3'
|
|
4
|
+
import * as tweenJs from '@tweenjs/tween.js'
|
|
5
|
+
import { getSyncWorld } from '../../playground/shared'
|
|
6
|
+
import type { GraphicsInitOptions } from '../../graphicsBackend/types'
|
|
7
|
+
import { WorldRendererCommon } from '../../lib/worldrendererCommon'
|
|
8
|
+
import { defaultWorldRendererConfig, getDefaultRendererState } from '../../graphicsBackend/config'
|
|
9
|
+
import { ResourcesManager, ResourcesManagerTransferred } from '../../resourcesManager/resourcesManager'
|
|
10
|
+
import { getInitialPlayerStateRenderer } from '../../graphicsBackend/playerState'
|
|
11
|
+
import { WorldRendererThree } from '../worldRendererThree'
|
|
12
|
+
import type { DocumentRenderer } from '../documentRenderer'
|
|
13
|
+
import { MENU_BACKGROUND_MC_VERSION } from './shared'
|
|
14
|
+
import { WorldView } from '../../worldView'
|
|
15
|
+
import type { MenuBackgroundView } from './activeView'
|
|
16
|
+
import { resizeMenuBackgroundCamera } from './activeView'
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Menu background built from a wall of random stained-glass blocks (single-file / demo style).
|
|
20
|
+
*/
|
|
21
|
+
export class WorldBlocksMenuBackground implements MenuBackgroundView {
|
|
22
|
+
private _scene: THREE.Scene
|
|
23
|
+
private _camera: THREE.PerspectiveCamera
|
|
24
|
+
|
|
25
|
+
get scene() { return this._scene }
|
|
26
|
+
get camera() { return this._camera }
|
|
27
|
+
|
|
28
|
+
private worldRenderer?: WorldRendererCommon | WorldRendererThree
|
|
29
|
+
WorldRendererClass = WorldRendererThree
|
|
30
|
+
|
|
31
|
+
constructor(
|
|
32
|
+
private readonly documentRenderer: DocumentRenderer,
|
|
33
|
+
private readonly options: GraphicsInitOptions,
|
|
34
|
+
private readonly abortSignal: AbortSignal
|
|
35
|
+
) {
|
|
36
|
+
this._scene = new THREE.Scene()
|
|
37
|
+
this._scene.background = new THREE.Color(0x32_45_68)
|
|
38
|
+
this._camera = new THREE.PerspectiveCamera(
|
|
39
|
+
85,
|
|
40
|
+
documentRenderer.canvas.width / documentRenderer.canvas.height,
|
|
41
|
+
0.05,
|
|
42
|
+
1000
|
|
43
|
+
)
|
|
44
|
+
this.camera.position.set(0, 0, 0)
|
|
45
|
+
this.camera.rotation.set(0, 0, 0)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async init() {
|
|
49
|
+
const version = MENU_BACKGROUND_MC_VERSION
|
|
50
|
+
const fullResourceManager = new ResourcesManager()
|
|
51
|
+
fullResourceManager.currentConfig = { version, noInventoryGui: true }
|
|
52
|
+
await fullResourceManager.updateAssetsData?.({})
|
|
53
|
+
if (this.abortSignal.aborted) return
|
|
54
|
+
|
|
55
|
+
console.time('load menu background scene')
|
|
56
|
+
const world = getSyncWorld(version)
|
|
57
|
+
const PrismarineBlock = require('prismarine-block')
|
|
58
|
+
const Block = PrismarineBlock(version)
|
|
59
|
+
const mcData = (globalThis as any).mcData
|
|
60
|
+
const fullBlocks = mcData.blocksArray.filter((block: { name: string, defaultState: number }) => {
|
|
61
|
+
if (!block.name.includes('stained_glass')) return false
|
|
62
|
+
const b = Block.fromStateId(block.defaultState, 0)
|
|
63
|
+
if (b.shapes?.length !== 1) return false
|
|
64
|
+
const shape = b.shapes[0]
|
|
65
|
+
return shape[0] === 0 && shape[1] === 0 && shape[2] === 0 && shape[3] === 1 && shape[4] === 1 && shape[5] === 1
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
const Z = -15
|
|
69
|
+
const sizeX = 100
|
|
70
|
+
const sizeY = 100
|
|
71
|
+
for (let x = -sizeX; x < sizeX; x++) {
|
|
72
|
+
for (let y = -sizeY; y < sizeY; y++) {
|
|
73
|
+
const block = fullBlocks[Math.floor(Math.random() * fullBlocks.length)]
|
|
74
|
+
world.setBlockStateId(new Vec3(x, y, Z), block.defaultState)
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
this._camera.updateProjectionMatrix()
|
|
79
|
+
this._camera.position.set(0.5, sizeY / 2 + 0.5, 0.5)
|
|
80
|
+
this._camera.rotation.set(0, 0, 0)
|
|
81
|
+
const initPos = new Vec3(...this._camera.position.toArray())
|
|
82
|
+
const worldView = new WorldView(world, 2, initPos)
|
|
83
|
+
if (this.abortSignal.aborted) return
|
|
84
|
+
|
|
85
|
+
this.worldRenderer = new this.WorldRendererClass(
|
|
86
|
+
this.documentRenderer.renderer,
|
|
87
|
+
this.options,
|
|
88
|
+
{
|
|
89
|
+
version,
|
|
90
|
+
worldView,
|
|
91
|
+
inWorldRenderingConfig: defaultWorldRendererConfig,
|
|
92
|
+
playerStateReactive: getInitialPlayerStateRenderer().reactive,
|
|
93
|
+
rendererState: getDefaultRendererState().reactive,
|
|
94
|
+
nonReactiveState: getDefaultRendererState().nonReactive,
|
|
95
|
+
resourcesManager: fullResourceManager as ResourcesManagerTransferred
|
|
96
|
+
}
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
if (this.worldRenderer instanceof WorldRendererThree) {
|
|
100
|
+
this._scene = this.worldRenderer.realScene
|
|
101
|
+
this._camera = this.worldRenderer.camera
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
void worldView.init(initPos)
|
|
105
|
+
await this.worldRenderer.waitForChunksToRender()
|
|
106
|
+
if (this.abortSignal.aborted) return
|
|
107
|
+
|
|
108
|
+
this.setupMouseParallax()
|
|
109
|
+
console.timeEnd('load menu background scene')
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
update(_dt: number, sizeChanged: boolean) {
|
|
113
|
+
if (sizeChanged) {
|
|
114
|
+
resizeMenuBackgroundCamera(this.camera, this.documentRenderer.canvas)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
dispose() {
|
|
119
|
+
this.worldRenderer?.destroy()
|
|
120
|
+
this.worldRenderer = undefined
|
|
121
|
+
this._scene.clear()
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
private setupMouseParallax() {
|
|
125
|
+
const camera = this._camera
|
|
126
|
+
const initX = camera.position.x
|
|
127
|
+
const initY = camera.position.y
|
|
128
|
+
let prevTween: tweenJs.Tween<THREE.Vector3> | undefined
|
|
129
|
+
|
|
130
|
+
document.body.addEventListener('pointermove', (e) => {
|
|
131
|
+
if (e.pointerType !== 'mouse') return
|
|
132
|
+
const SCALE = 0.2
|
|
133
|
+
const xRel = e.clientX / window.innerWidth - 0.5
|
|
134
|
+
const yRel = -(e.clientY / window.innerHeight - 0.5)
|
|
135
|
+
prevTween?.stop()
|
|
136
|
+
prevTween = new tweenJs.Tween(camera.position).to({
|
|
137
|
+
x: initX + xRel * SCALE,
|
|
138
|
+
y: initY + yRel * SCALE
|
|
139
|
+
}, 0)
|
|
140
|
+
prevTween.start()
|
|
141
|
+
camera.updateProjectionMatrix()
|
|
142
|
+
}, { signal: this.abortSignal })
|
|
143
|
+
}
|
|
144
|
+
}
|
|
@@ -433,6 +433,12 @@ export class SciFiWorldRevealModule implements RendererModuleController {
|
|
|
433
433
|
* Create wireframe geometry from mesh geometry
|
|
434
434
|
*/
|
|
435
435
|
private createWireframeGeometry(geometry: MesherGeometryOutput): THREE.BufferGeometry {
|
|
436
|
+
if (geometry.wireframePositions && geometry.wireframePositions.length > 0) {
|
|
437
|
+
const wireframeGeom = new THREE.BufferGeometry()
|
|
438
|
+
wireframeGeom.setAttribute('position', new THREE.Float32BufferAttribute(geometry.wireframePositions, 3))
|
|
439
|
+
return wireframeGeom
|
|
440
|
+
}
|
|
441
|
+
|
|
436
442
|
const positions = geometry.positions as Float32Array
|
|
437
443
|
const indices = geometry.indices as Uint32Array | Uint16Array
|
|
438
444
|
|
|
@@ -2,6 +2,15 @@
|
|
|
2
2
|
import * as THREE from 'three'
|
|
3
3
|
import { createCanvas } from '../lib/utils'
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Limits label texture resolution on high-DPR devices (sprite still sizes in screen px via Three.js;
|
|
7
|
+
* main win is fewer canvas pixels / less GPU memory — especially on iOS).
|
|
8
|
+
*/
|
|
9
|
+
const LABEL_CANVAS_MAX_DEVICE_PIXEL_RATIO = 1
|
|
10
|
+
|
|
11
|
+
/** Distance label repaints when this bucket (meters) changes — fewer canvas uploads while moving. */
|
|
12
|
+
const DISTANCE_LABEL_STEP_M = 10
|
|
13
|
+
|
|
5
14
|
// Centralized visual configuration (in screen pixels)
|
|
6
15
|
export const WAYPOINT_CONFIG = {
|
|
7
16
|
// Target size in screen pixels (this controls the final sprite size)
|
|
@@ -60,9 +69,8 @@ export function createWaypointSprite (options: {
|
|
|
60
69
|
visualScale?: number,
|
|
61
70
|
opacity?: number,
|
|
62
71
|
}): WaypointSprite {
|
|
63
|
-
|
|
72
|
+
let displayColor = options.color ?? 0xFF_00_00
|
|
64
73
|
const depthTest = options.depthTest ?? false
|
|
65
|
-
const labelYOffset = options.labelYOffset ?? 1.5
|
|
66
74
|
|
|
67
75
|
// Get visual scale from options, metadata, server metadata, or default
|
|
68
76
|
// Priority: options.visualScale > metadata.visualScale > window.serverMetadata?.waypointVisualScale > DEFAULT
|
|
@@ -78,61 +86,87 @@ export function createWaypointSprite (options: {
|
|
|
78
86
|
?? (typeof window === 'undefined' ? undefined : (window as any).serverMetadata?.waypointOpacity)
|
|
79
87
|
?? WAYPOINT_CONFIG.DEFAULT_OPACITY
|
|
80
88
|
|
|
81
|
-
|
|
82
|
-
|
|
89
|
+
const labelCanvas = createCanvas(getLabelCanvasSize(), getLabelCanvasSize())
|
|
90
|
+
drawCombinedOntoCanvas(labelCanvas, displayColor, options.label ?? '', '0m', visualScale)
|
|
91
|
+
|
|
92
|
+
const labelTexture = new THREE.CanvasTexture(labelCanvas)
|
|
93
|
+
labelTexture.anisotropy = 1
|
|
94
|
+
labelTexture.magFilter = THREE.LinearFilter
|
|
95
|
+
labelTexture.minFilter = THREE.LinearFilter
|
|
96
|
+
const material = new THREE.SpriteMaterial({
|
|
97
|
+
map: labelTexture,
|
|
98
|
+
transparent: true,
|
|
99
|
+
opacity: 1,
|
|
100
|
+
depthTest,
|
|
101
|
+
depthWrite: false,
|
|
102
|
+
})
|
|
103
|
+
const sprite = new THREE.Sprite(material)
|
|
104
|
+
sprite.position.set(0, 0, 0)
|
|
83
105
|
sprite.renderOrder = 10
|
|
84
106
|
sprite.material.opacity = opacity
|
|
85
107
|
let currentLabel = options.label ?? ''
|
|
86
108
|
|
|
87
|
-
// Performance optimization: cache distance text to avoid unnecessary updates
|
|
88
109
|
let lastDistanceText = '0m'
|
|
89
|
-
let
|
|
110
|
+
let lastDistanceBucket = Number.NaN
|
|
90
111
|
|
|
91
|
-
// Offscreen arrow (detached by default)
|
|
92
112
|
let arrowSprite: THREE.Sprite | undefined
|
|
113
|
+
let arrowCanvas: OffscreenCanvas | undefined
|
|
114
|
+
let arrowCtx: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D | undefined
|
|
115
|
+
let arrowTexture: THREE.CanvasTexture | undefined
|
|
93
116
|
let arrowParent: THREE.Object3D | null = null
|
|
94
117
|
let arrowEnabled = WAYPOINT_CONFIG.ARROW.enabledDefault
|
|
95
118
|
|
|
96
|
-
// Group for easy add/remove
|
|
97
119
|
const group = new THREE.Group()
|
|
98
120
|
group.add(sprite)
|
|
99
121
|
|
|
100
|
-
// Initial position
|
|
101
122
|
const { x, y, z } = options.position
|
|
102
123
|
group.position.set(x, y, z)
|
|
103
124
|
|
|
125
|
+
function refreshLabelTexture () {
|
|
126
|
+
labelTexture.needsUpdate = true
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function paintArrowOnCanvas () {
|
|
130
|
+
if (!arrowCanvas || !arrowCtx) return
|
|
131
|
+
const size = arrowCanvas.width
|
|
132
|
+
arrowCtx.clearRect(0, 0, size, size)
|
|
133
|
+
arrowCtx.beginPath()
|
|
134
|
+
arrowCtx.moveTo(size * 0.15, size * 0.5)
|
|
135
|
+
arrowCtx.lineTo(size * 0.85, size * 0.5)
|
|
136
|
+
arrowCtx.lineTo(size * 0.5, size * 0.15)
|
|
137
|
+
arrowCtx.closePath()
|
|
138
|
+
const colorHex = `#${displayColor.toString(16).padStart(6, '0')}`
|
|
139
|
+
arrowCtx.lineWidth = 6
|
|
140
|
+
arrowCtx.strokeStyle = 'black'
|
|
141
|
+
arrowCtx.stroke()
|
|
142
|
+
arrowCtx.fillStyle = colorHex
|
|
143
|
+
arrowCtx.fill()
|
|
144
|
+
if (arrowTexture) arrowTexture.needsUpdate = true
|
|
145
|
+
}
|
|
146
|
+
|
|
104
147
|
function setColor (newColor: number) {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
148
|
+
displayColor = newColor
|
|
149
|
+
lastDistanceText = '0m'
|
|
150
|
+
lastDistanceBucket = 0
|
|
151
|
+
drawCombinedOntoCanvas(labelCanvas, displayColor, currentLabel, '0m', visualScale)
|
|
152
|
+
refreshLabelTexture()
|
|
153
|
+
if (arrowSprite) paintArrowOnCanvas()
|
|
111
154
|
}
|
|
112
155
|
|
|
113
156
|
function setLabel (newLabel?: string) {
|
|
114
157
|
currentLabel = newLabel ?? ''
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
const mat = sprite.material
|
|
118
|
-
mat.map?.dispose()
|
|
119
|
-
mat.map = texture
|
|
120
|
-
mat.needsUpdate = true
|
|
158
|
+
drawCombinedOntoCanvas(labelCanvas, displayColor, currentLabel, lastDistanceText, visualScale)
|
|
159
|
+
refreshLabelTexture()
|
|
121
160
|
}
|
|
122
161
|
|
|
123
162
|
function updateDistanceText (label: string, distanceText: string) {
|
|
124
|
-
// Performance optimization: only update if distance text actually changed
|
|
125
163
|
if (distanceText === lastDistanceText) {
|
|
126
164
|
return
|
|
127
165
|
}
|
|
128
166
|
lastDistanceText = distanceText
|
|
129
167
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
const mat = sprite.material
|
|
133
|
-
mat.map?.dispose()
|
|
134
|
-
mat.map = texture
|
|
135
|
-
mat.needsUpdate = true
|
|
168
|
+
drawCombinedOntoCanvas(labelCanvas, displayColor, label, distanceText, visualScale)
|
|
169
|
+
refreshLabelTexture()
|
|
136
170
|
}
|
|
137
171
|
|
|
138
172
|
function setVisible (visible: boolean) {
|
|
@@ -143,7 +177,6 @@ export function createWaypointSprite (options: {
|
|
|
143
177
|
group.position.set(nx, ny, nz)
|
|
144
178
|
}
|
|
145
179
|
|
|
146
|
-
// Keep constant pixel size on screen using global config
|
|
147
180
|
function updateScaleScreenPixels (
|
|
148
181
|
cameraPosition: THREE.Vector3,
|
|
149
182
|
cameraFov: number,
|
|
@@ -152,7 +185,6 @@ export function createWaypointSprite (options: {
|
|
|
152
185
|
) {
|
|
153
186
|
const vFovRad = cameraFov * Math.PI / 180
|
|
154
187
|
const worldUnitsPerScreenHeightAtDist = Math.tan(vFovRad / 2) * 2 * distance
|
|
155
|
-
// Use configured target screen size with visual scale multiplier
|
|
156
188
|
const scale = worldUnitsPerScreenHeightAtDist * (WAYPOINT_CONFIG.TARGET_SCREEN_PX * visualScale / viewportHeightPx)
|
|
157
189
|
sprite.scale.set(scale, scale, 1)
|
|
158
190
|
}
|
|
@@ -160,28 +192,15 @@ export function createWaypointSprite (options: {
|
|
|
160
192
|
function ensureArrow () {
|
|
161
193
|
if (arrowSprite) return
|
|
162
194
|
const size = 128
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
ctx.closePath()
|
|
173
|
-
|
|
174
|
-
// Use waypoint color for arrow
|
|
175
|
-
const colorHex = `#${color.toString(16).padStart(6, '0')}`
|
|
176
|
-
ctx.lineWidth = 6
|
|
177
|
-
ctx.strokeStyle = 'black'
|
|
178
|
-
ctx.stroke()
|
|
179
|
-
ctx.fillStyle = colorHex
|
|
180
|
-
ctx.fill()
|
|
181
|
-
|
|
182
|
-
const texture = new THREE.CanvasTexture(canvas)
|
|
183
|
-
const material = new THREE.SpriteMaterial({ map: texture, transparent: true, depthTest: false, depthWrite: false, opacity })
|
|
184
|
-
arrowSprite = new THREE.Sprite(material)
|
|
195
|
+
arrowCanvas = createCanvas(size, size)
|
|
196
|
+
arrowCtx = arrowCanvas.getContext('2d')!
|
|
197
|
+
paintArrowOnCanvas()
|
|
198
|
+
arrowTexture = new THREE.CanvasTexture(arrowCanvas)
|
|
199
|
+
arrowTexture.anisotropy = 1
|
|
200
|
+
arrowTexture.magFilter = THREE.LinearFilter
|
|
201
|
+
arrowTexture.minFilter = THREE.LinearFilter
|
|
202
|
+
const matTex = new THREE.SpriteMaterial({ map: arrowTexture, transparent: true, depthTest: false, depthWrite: false, opacity })
|
|
203
|
+
arrowSprite = new THREE.Sprite(matTex)
|
|
185
204
|
arrowSprite.renderOrder = 12
|
|
186
205
|
arrowSprite.visible = false
|
|
187
206
|
if (arrowParent) arrowParent.add(arrowSprite)
|
|
@@ -306,9 +325,8 @@ export function createWaypointSprite (options: {
|
|
|
306
325
|
return false
|
|
307
326
|
}
|
|
308
327
|
|
|
309
|
-
function computeDistance (
|
|
310
|
-
|
|
311
|
-
return group.position.length()
|
|
328
|
+
function computeDistance (cameraPosition: THREE.Vector3): number {
|
|
329
|
+
return cameraPosition.distanceTo(group.position)
|
|
312
330
|
}
|
|
313
331
|
|
|
314
332
|
function updateForCamera (
|
|
@@ -318,17 +336,14 @@ export function createWaypointSprite (options: {
|
|
|
318
336
|
viewportHeightPx: number
|
|
319
337
|
): boolean {
|
|
320
338
|
const distance = computeDistance(cameraPosition)
|
|
321
|
-
// Keep constant pixel size
|
|
322
339
|
updateScaleScreenPixels(cameraPosition, camera.fov, distance, viewportHeightPx)
|
|
323
340
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
updateDistanceText(currentLabel, `${roundedDistance}m`)
|
|
341
|
+
const bucket = Math.round(distance / DISTANCE_LABEL_STEP_M) * DISTANCE_LABEL_STEP_M
|
|
342
|
+
if (bucket !== lastDistanceBucket) {
|
|
343
|
+
lastDistanceBucket = bucket
|
|
344
|
+
updateDistanceText(currentLabel, `${Math.max(0, bucket)}m`)
|
|
329
345
|
}
|
|
330
346
|
|
|
331
|
-
// Update arrow and visibility
|
|
332
347
|
const onScreen = updateOffscreenArrow(camera, viewportWidthPx, viewportHeightPx)
|
|
333
348
|
setVisible(onScreen)
|
|
334
349
|
return onScreen
|
|
@@ -339,7 +354,6 @@ export function createWaypointSprite (options: {
|
|
|
339
354
|
mat.map?.dispose()
|
|
340
355
|
mat.dispose()
|
|
341
356
|
if (arrowSprite) {
|
|
342
|
-
// Remove arrow from parent before disposing
|
|
343
357
|
if (arrowSprite.parent) {
|
|
344
358
|
arrowSprite.parent.remove(arrowSprite)
|
|
345
359
|
}
|
|
@@ -347,6 +361,10 @@ export function createWaypointSprite (options: {
|
|
|
347
361
|
am.map?.dispose()
|
|
348
362
|
am.dispose()
|
|
349
363
|
}
|
|
364
|
+
arrowSprite = undefined
|
|
365
|
+
arrowCanvas = undefined
|
|
366
|
+
arrowCtx = undefined
|
|
367
|
+
arrowTexture = undefined
|
|
350
368
|
}
|
|
351
369
|
|
|
352
370
|
return {
|
|
@@ -365,41 +383,46 @@ export function createWaypointSprite (options: {
|
|
|
365
383
|
}
|
|
366
384
|
|
|
367
385
|
// Internal helpers
|
|
368
|
-
function
|
|
369
|
-
const
|
|
370
|
-
const
|
|
371
|
-
|
|
386
|
+
function computeLabelCanvasLineScale (): number {
|
|
387
|
+
const dpr = globalThis.devicePixelRatio || 1
|
|
388
|
+
const effectiveDpr = Math.min(dpr, LABEL_CANVAS_MAX_DEVICE_PIXEL_RATIO)
|
|
389
|
+
return WAYPOINT_CONFIG.CANVAS_SCALE * effectiveDpr
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function getLabelCanvasSize (): number {
|
|
393
|
+
return Math.round(WAYPOINT_CONFIG.CANVAS_SIZE * computeLabelCanvasLineScale())
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function drawCombinedOntoCanvas (
|
|
397
|
+
canvas: OffscreenCanvas,
|
|
398
|
+
color: number,
|
|
399
|
+
id: string,
|
|
400
|
+
distance: string,
|
|
401
|
+
visualScale: number
|
|
402
|
+
): void {
|
|
403
|
+
const size = canvas.width
|
|
404
|
+
const scale = computeLabelCanvasLineScale()
|
|
372
405
|
const ctx = canvas.getContext('2d')!
|
|
373
406
|
|
|
374
|
-
// Clear canvas
|
|
375
407
|
ctx.clearRect(0, 0, size, size)
|
|
376
408
|
|
|
377
|
-
// Draw dot with visual scale applied
|
|
378
409
|
const centerX = size / 2
|
|
379
410
|
const dotY = Math.round(size * WAYPOINT_CONFIG.LAYOUT.DOT_Y)
|
|
380
|
-
const
|
|
381
|
-
const
|
|
411
|
+
const innerRadius = Math.round(size * 0.05 * visualScale)
|
|
412
|
+
const outlinePad = Math.max(2, Math.round(4 * scale * visualScale))
|
|
413
|
+
const dotRadius = innerRadius + outlinePad
|
|
382
414
|
|
|
383
|
-
// Outer border (black)
|
|
384
415
|
ctx.beginPath()
|
|
385
|
-
ctx.arc(centerX, dotY,
|
|
386
|
-
ctx.fillStyle = 'black'
|
|
387
|
-
ctx.fill()
|
|
388
|
-
|
|
389
|
-
// Inner circle (colored)
|
|
390
|
-
ctx.beginPath()
|
|
391
|
-
ctx.arc(centerX, dotY, radius, 0, Math.PI * 2)
|
|
416
|
+
ctx.arc(centerX, dotY, dotRadius, 0, Math.PI * 2)
|
|
392
417
|
ctx.fillStyle = `#${color.toString(16).padStart(6, '0')}`
|
|
393
418
|
ctx.fill()
|
|
394
419
|
|
|
395
|
-
// Text properties
|
|
396
420
|
ctx.textAlign = 'center'
|
|
397
421
|
ctx.textBaseline = 'middle'
|
|
398
422
|
|
|
399
|
-
|
|
400
|
-
const
|
|
401
|
-
|
|
402
|
-
ctx.font = `bold ${nameFontPx}px mojangles`
|
|
423
|
+
const nameFontPx = Math.round(size * 0.08 * visualScale)
|
|
424
|
+
const distanceFontPx = Math.round(size * 0.06 * visualScale)
|
|
425
|
+
ctx.font = `800 ${nameFontPx}px mojangles`
|
|
403
426
|
ctx.lineWidth = Math.max(2, Math.round(3 * scale * visualScale))
|
|
404
427
|
const nameY = Math.round(size * WAYPOINT_CONFIG.LAYOUT.NAME_Y)
|
|
405
428
|
|
|
@@ -408,8 +431,7 @@ function drawCombinedCanvas (color: number, id: string, distance: string, visual
|
|
|
408
431
|
ctx.fillStyle = 'white'
|
|
409
432
|
ctx.fillText(id, centerX, nameY)
|
|
410
433
|
|
|
411
|
-
|
|
412
|
-
ctx.font = `bold ${distanceFontPx}px mojangles`
|
|
434
|
+
ctx.font = `800 ${distanceFontPx}px mojangles`
|
|
413
435
|
ctx.lineWidth = Math.max(2, Math.round(2 * scale * visualScale))
|
|
414
436
|
const distanceY = Math.round(size * WAYPOINT_CONFIG.LAYOUT.DISTANCE_Y)
|
|
415
437
|
|
|
@@ -417,26 +439,6 @@ function drawCombinedCanvas (color: number, id: string, distance: string, visual
|
|
|
417
439
|
ctx.strokeText(distance, centerX, distanceY)
|
|
418
440
|
ctx.fillStyle = '#CCCCCC'
|
|
419
441
|
ctx.fillText(distance, centerX, distanceY)
|
|
420
|
-
|
|
421
|
-
return canvas
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
function createCombinedSprite (color: number, id: string, distance: string, depthTest: boolean, visualScale = 1): THREE.Sprite {
|
|
425
|
-
const canvas = drawCombinedCanvas(color, id, distance, visualScale)
|
|
426
|
-
const texture = new THREE.CanvasTexture(canvas)
|
|
427
|
-
texture.anisotropy = 1
|
|
428
|
-
texture.magFilter = THREE.LinearFilter
|
|
429
|
-
texture.minFilter = THREE.LinearFilter
|
|
430
|
-
const material = new THREE.SpriteMaterial({
|
|
431
|
-
map: texture,
|
|
432
|
-
transparent: true,
|
|
433
|
-
opacity: 1,
|
|
434
|
-
depthTest,
|
|
435
|
-
depthWrite: false,
|
|
436
|
-
})
|
|
437
|
-
const sprite = new THREE.Sprite(material)
|
|
438
|
-
sprite.position.set(0, 0, 0)
|
|
439
|
-
return sprite
|
|
440
442
|
}
|
|
441
443
|
|
|
442
444
|
export const WaypointHelpers = {
|