minecraft-renderer 0.1.0

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 (187) hide show
  1. package/README.md +297 -0
  2. package/dist/index.html +83 -0
  3. package/dist/static/image/arrow.6f27b59f.png +0 -0
  4. package/dist/static/image/blocksAtlasLatest.7850afa3.png +0 -0
  5. package/dist/static/image/blocksAtlasLegacy.5c76823d.png +0 -0
  6. package/dist/static/image/itemsAtlasLatest.36036f95.png +0 -0
  7. package/dist/static/image/itemsAtlasLegacy.dcb1b58d.png +0 -0
  8. package/dist/static/image/tipped_arrow.6f27b59f.png +0 -0
  9. package/dist/static/js/365.f05233ab.js +8462 -0
  10. package/dist/static/js/365.f05233ab.js.LICENSE.txt +52 -0
  11. package/dist/static/js/async/738.efa27644.js +1 -0
  12. package/dist/static/js/index.092ec5be.js +56 -0
  13. package/dist/static/js/lib-polyfill.98986ac5.js +1 -0
  14. package/dist/static/js/lib-react.5c9129e0.js +2 -0
  15. package/dist/static/js/lib-react.5c9129e0.js.LICENSE.txt +39 -0
  16. package/package.json +104 -0
  17. package/src/assets/destroy_stage_0.png +0 -0
  18. package/src/assets/destroy_stage_1.png +0 -0
  19. package/src/assets/destroy_stage_2.png +0 -0
  20. package/src/assets/destroy_stage_3.png +0 -0
  21. package/src/assets/destroy_stage_4.png +0 -0
  22. package/src/assets/destroy_stage_5.png +0 -0
  23. package/src/assets/destroy_stage_6.png +0 -0
  24. package/src/assets/destroy_stage_7.png +0 -0
  25. package/src/assets/destroy_stage_8.png +0 -0
  26. package/src/assets/destroy_stage_9.png +0 -0
  27. package/src/examples/README.md +146 -0
  28. package/src/examples/appViewerExample.ts +205 -0
  29. package/src/examples/initialMenuStart.ts +161 -0
  30. package/src/graphicsBackend/appViewer.ts +297 -0
  31. package/src/graphicsBackend/config.ts +119 -0
  32. package/src/graphicsBackend/index.ts +10 -0
  33. package/src/graphicsBackend/playerState.ts +61 -0
  34. package/src/graphicsBackend/types.ts +143 -0
  35. package/src/index.ts +97 -0
  36. package/src/lib/DebugGui.ts +190 -0
  37. package/src/lib/animationController.ts +85 -0
  38. package/src/lib/buildSharedConfig.mjs +1 -0
  39. package/src/lib/cameraBobbing.ts +94 -0
  40. package/src/lib/canvas2DOverlay.example.ts +361 -0
  41. package/src/lib/canvas2DOverlay.quickstart.ts +242 -0
  42. package/src/lib/canvas2DOverlay.ts +381 -0
  43. package/src/lib/cleanupDecorator.ts +29 -0
  44. package/src/lib/createPlayerObject.ts +55 -0
  45. package/src/lib/frameTimingCollector.ts +164 -0
  46. package/src/lib/guiRenderer.ts +283 -0
  47. package/src/lib/items.ts +140 -0
  48. package/src/lib/mesherlogReader.ts +131 -0
  49. package/src/lib/moreBlockDataGenerated.json +714 -0
  50. package/src/lib/preflatMap.json +1741 -0
  51. package/src/lib/simpleUtils.ts +40 -0
  52. package/src/lib/smoothSwitcher.ts +168 -0
  53. package/src/lib/spiral.ts +29 -0
  54. package/src/lib/ui/newStats.ts +120 -0
  55. package/src/lib/utils/proxy.ts +23 -0
  56. package/src/lib/utils/skins.ts +63 -0
  57. package/src/lib/utils.ts +76 -0
  58. package/src/lib/workerProxy.ts +342 -0
  59. package/src/lib/worldrendererCommon.ts +1088 -0
  60. package/src/mesher/mesher.ts +253 -0
  61. package/src/mesher/models.ts +769 -0
  62. package/src/mesher/modelsGeometryCommon.ts +142 -0
  63. package/src/mesher/shared.ts +80 -0
  64. package/src/mesher/standaloneRenderer.ts +270 -0
  65. package/src/mesher/test/a.ts +3 -0
  66. package/src/mesher/test/mesherTester.ts +76 -0
  67. package/src/mesher/test/playground.ts +19 -0
  68. package/src/mesher/test/test-perf.ts +74 -0
  69. package/src/mesher/test/tests.test.ts +56 -0
  70. package/src/mesher/world.ts +294 -0
  71. package/src/mesher/worldConstants.ts +1 -0
  72. package/src/modules/index.ts +11 -0
  73. package/src/modules/starfield.ts +313 -0
  74. package/src/modules/types.ts +110 -0
  75. package/src/playerState/playerState.ts +78 -0
  76. package/src/playerState/types.ts +36 -0
  77. package/src/playground/allEntitiesDebug.ts +170 -0
  78. package/src/playground/baseScene.ts +587 -0
  79. package/src/playground/mobileControls.tsx +268 -0
  80. package/src/playground/playground.html +83 -0
  81. package/src/playground/playground.ts +11 -0
  82. package/src/playground/playgroundUi.tsx +140 -0
  83. package/src/playground/reactUtils.ts +71 -0
  84. package/src/playground/scenes/allEntities.ts +13 -0
  85. package/src/playground/scenes/entities.ts +37 -0
  86. package/src/playground/scenes/floorRandom.ts +33 -0
  87. package/src/playground/scenes/frequentUpdates.ts +148 -0
  88. package/src/playground/scenes/geometryExport.ts +142 -0
  89. package/src/playground/scenes/index.ts +12 -0
  90. package/src/playground/scenes/lightingStarfield.ts +40 -0
  91. package/src/playground/scenes/main.ts +313 -0
  92. package/src/playground/scenes/railsCobweb.ts +14 -0
  93. package/src/playground/scenes/rotationIssue.ts +7 -0
  94. package/src/playground/scenes/slabsOptimization.ts +16 -0
  95. package/src/playground/scenes/transparencyIssue.ts +11 -0
  96. package/src/playground/shared.ts +79 -0
  97. package/src/resourcesManager/index.ts +5 -0
  98. package/src/resourcesManager/resourcesManager.ts +314 -0
  99. package/src/shims/minecraftData.ts +41 -0
  100. package/src/sign-renderer/index.html +21 -0
  101. package/src/sign-renderer/index.ts +216 -0
  102. package/src/sign-renderer/noop.js +1 -0
  103. package/src/sign-renderer/playground.ts +38 -0
  104. package/src/sign-renderer/tests.test.ts +69 -0
  105. package/src/sign-renderer/vite.config.ts +10 -0
  106. package/src/three/appShared.ts +75 -0
  107. package/src/three/bannerRenderer.ts +275 -0
  108. package/src/three/cameraShake.ts +120 -0
  109. package/src/three/cinimaticScript.ts +350 -0
  110. package/src/three/documentRenderer.ts +491 -0
  111. package/src/three/entities.ts +1580 -0
  112. package/src/three/entity/EntityMesh.ts +707 -0
  113. package/src/three/entity/animations.js +171 -0
  114. package/src/three/entity/armorModels.json +204 -0
  115. package/src/three/entity/armorModels.ts +36 -0
  116. package/src/three/entity/entities.json +6230 -0
  117. package/src/three/entity/exportedModels.js +38 -0
  118. package/src/three/entity/externalTextures.json +1 -0
  119. package/src/three/entity/models/allay.obj +325 -0
  120. package/src/three/entity/models/arrow.obj +60 -0
  121. package/src/three/entity/models/axolotl.obj +509 -0
  122. package/src/three/entity/models/blaze.obj +601 -0
  123. package/src/three/entity/models/boat.obj +417 -0
  124. package/src/three/entity/models/camel.obj +1061 -0
  125. package/src/three/entity/models/cat.obj +509 -0
  126. package/src/three/entity/models/chicken.obj +371 -0
  127. package/src/three/entity/models/cod.obj +371 -0
  128. package/src/three/entity/models/creeper.obj +279 -0
  129. package/src/three/entity/models/dolphin.obj +371 -0
  130. package/src/three/entity/models/ender_dragon.obj +2993 -0
  131. package/src/three/entity/models/enderman.obj +325 -0
  132. package/src/three/entity/models/endermite.obj +187 -0
  133. package/src/three/entity/models/fox.obj +463 -0
  134. package/src/three/entity/models/frog.obj +739 -0
  135. package/src/three/entity/models/ghast.obj +463 -0
  136. package/src/three/entity/models/goat.obj +601 -0
  137. package/src/three/entity/models/guardian.obj +1015 -0
  138. package/src/three/entity/models/horse.obj +1061 -0
  139. package/src/three/entity/models/llama.obj +509 -0
  140. package/src/three/entity/models/minecart.obj +233 -0
  141. package/src/three/entity/models/parrot.obj +509 -0
  142. package/src/three/entity/models/piglin.obj +739 -0
  143. package/src/three/entity/models/pillager.obj +371 -0
  144. package/src/three/entity/models/rabbit.obj +555 -0
  145. package/src/three/entity/models/sheep.obj +555 -0
  146. package/src/three/entity/models/shulker.obj +141 -0
  147. package/src/three/entity/models/sniffer.obj +693 -0
  148. package/src/three/entity/models/spider.obj +509 -0
  149. package/src/three/entity/models/tadpole.obj +95 -0
  150. package/src/three/entity/models/turtle.obj +371 -0
  151. package/src/three/entity/models/vex.obj +325 -0
  152. package/src/three/entity/models/villager.obj +509 -0
  153. package/src/three/entity/models/warden.obj +463 -0
  154. package/src/three/entity/models/witch.obj +647 -0
  155. package/src/three/entity/models/wolf.obj +509 -0
  156. package/src/three/entity/models/zombie_villager.obj +463 -0
  157. package/src/three/entity/objModels.js +1 -0
  158. package/src/three/fireworks.ts +661 -0
  159. package/src/three/fireworksRenderer.ts +434 -0
  160. package/src/three/globals.d.ts +7 -0
  161. package/src/three/graphicsBackend.ts +274 -0
  162. package/src/three/graphicsBackendOffThread.ts +107 -0
  163. package/src/three/hand.ts +89 -0
  164. package/src/three/holdingBlock.ts +926 -0
  165. package/src/three/index.ts +20 -0
  166. package/src/three/itemMesh.ts +427 -0
  167. package/src/three/modules.d.ts +14 -0
  168. package/src/three/panorama.ts +308 -0
  169. package/src/three/panoramaShared.ts +1 -0
  170. package/src/three/renderSlot.ts +82 -0
  171. package/src/three/skyboxRenderer.ts +406 -0
  172. package/src/three/starField.ts +13 -0
  173. package/src/three/threeJsMedia.ts +731 -0
  174. package/src/three/threeJsMethods.ts +15 -0
  175. package/src/three/threeJsParticles.ts +160 -0
  176. package/src/three/threeJsSound.ts +95 -0
  177. package/src/three/threeJsUtils.ts +90 -0
  178. package/src/three/waypointSprite.ts +435 -0
  179. package/src/three/waypoints.ts +163 -0
  180. package/src/three/world/cursorBlock.ts +172 -0
  181. package/src/three/world/vr.ts +257 -0
  182. package/src/three/worldGeometryExport.ts +259 -0
  183. package/src/three/worldGeometryHandler.ts +279 -0
  184. package/src/three/worldRendererThree.ts +1381 -0
  185. package/src/worldView/index.ts +6 -0
  186. package/src/worldView/types.ts +66 -0
  187. package/src/worldView/worldView.ts +424 -0
@@ -0,0 +1,587 @@
1
+ import { Vec3 } from 'vec3'
2
+ import * as THREE from 'three'
3
+ import MinecraftData, { IndexedData } from 'minecraft-data'
4
+ import BlockLoader from 'prismarine-block'
5
+ import ChunkLoader from 'prismarine-chunk'
6
+ import WorldLoader from 'prismarine-world'
7
+ import { proxy } from 'valtio'
8
+ import { BlockNames } from 'mc-bridge/dist/names.generated'
9
+
10
+ import { OrbitControls } from 'three/addons/controls/OrbitControls.js'
11
+ // eslint-disable-next-line import/no-named-as-default
12
+ import GUI from 'lil-gui'
13
+ import _ from 'lodash'
14
+ import { defaultWorldRendererConfig, WorldRendererConfig } from '@/lib/worldrendererCommon'
15
+ import { getSyncWorld } from './shared'
16
+ import { AppViewer, getInitialPlayerState } from '@/graphicsBackend'
17
+ import { WorldView } from '@/worldView'
18
+ import { createGraphicsBackend } from '@/three'
19
+
20
+ window.THREE = THREE
21
+
22
+ // Scene configuration interface
23
+ export interface PlaygroundSceneConfig {
24
+ version?: string
25
+ viewDistance?: number
26
+ targetPos?: Vec3
27
+ enableCameraControls?: boolean
28
+ enableCameraOrbitControl?: boolean
29
+ worldConfig?: WorldRendererConfig
30
+ continuousRender?: boolean
31
+ }
32
+
33
+ const appGraphicBackends = [
34
+ createGraphicsBackend,
35
+ ]
36
+
37
+ const includedVersions = globalThis.includedVersions
38
+
39
+ export class BasePlaygroundScene {
40
+ appViewer = new AppViewer({
41
+ config: {
42
+ statsVisible: 2,
43
+ }
44
+ })
45
+
46
+ mcData!: IndexedData
47
+
48
+ // Rendering state
49
+ continuousRender = false
50
+ stopRender = false
51
+ windowHidden = false
52
+
53
+ // Scene configuration
54
+ viewDistance = 0
55
+ targetPos = new Vec3(2, 90, 2)
56
+ version: string = new URLSearchParams(window.location.search).get('version') ?? includedVersions.at(-1)!
57
+
58
+ // World data
59
+ Chunk!: typeof import('prismarine-chunk/types/index').PCChunk
60
+ Block!: typeof import('prismarine-block').Block
61
+ world!: ReturnType<typeof getSyncWorld>
62
+
63
+ // GUI
64
+ gui = new GUI()
65
+ params = {} as Record<string, any>
66
+ paramOptions = {} as Partial<Record<keyof typeof this.params, {
67
+ hide?: boolean
68
+ options?: string[]
69
+ min?: number
70
+ max?: number
71
+ reloadOnChange?: boolean
72
+ }>>
73
+ onParamUpdate = {} as Record<string, () => void>
74
+ alwaysIgnoreQs = [] as string[]
75
+ skipUpdateQs = false
76
+
77
+ // Camera controls - own camera synced to backend
78
+ enableCameraControls = true
79
+ enableCameraOrbitControl = true
80
+ controls: OrbitControls | undefined
81
+ camera!: THREE.PerspectiveCamera
82
+
83
+ // World data emitter (from appViewer)
84
+ worldView: WorldView | undefined
85
+
86
+ // Debug FPS tracking
87
+ private debugFpsElement: HTMLElement | undefined
88
+ private frameCount = 0
89
+ private lastSecondTime = performance.now()
90
+ private frameTimes: number[] = []
91
+ private currentFps = 0
92
+ private maxFrameDelay = 0
93
+
94
+ // Getter for worldRenderer - accesses via window.world for advanced scene features
95
+ // This allows derived scenes to access worldRenderer when needed without storing it
96
+ get worldRenderer() {
97
+ //@ts-ignore
98
+ return window.world
99
+ }
100
+
101
+ // World config - syncs with appViewer.inWorldRenderingConfig
102
+ get worldConfig() {
103
+ return this.appViewer.inWorldRenderingConfig
104
+ }
105
+ set worldConfig(value) {
106
+ // Merge the new values into appViewer's config to maintain reactivity
107
+ Object.assign(this.appViewer.inWorldRenderingConfig, value)
108
+ }
109
+
110
+ constructor(config: PlaygroundSceneConfig = {}) {
111
+ // Apply config
112
+ if (config.version) this.version = config.version
113
+
114
+ // Ensure version is always set (fallback to latest supported version)
115
+ if (!this.version) {
116
+ throw new Error('Minecraft version is not set')
117
+ }
118
+
119
+ if (config.viewDistance !== undefined) this.viewDistance = config.viewDistance
120
+ if (config.targetPos) this.targetPos = config.targetPos
121
+ if (config.enableCameraControls !== undefined) this.enableCameraControls = config.enableCameraControls
122
+ if (config.enableCameraOrbitControl !== undefined) this.enableCameraOrbitControl = config.enableCameraOrbitControl
123
+ if (config.worldConfig) {
124
+ // Merge config into appViewer's config to maintain reactivity
125
+ Object.assign(this.appViewer.inWorldRenderingConfig, config.worldConfig)
126
+ }
127
+ this.appViewer.inWorldRenderingConfig.showHand = false
128
+ this.appViewer.inWorldRenderingConfig.isPlayground = true
129
+ this.appViewer.inWorldRenderingConfig.instantCameraUpdate = this.enableCameraOrbitControl
130
+ this.appViewer.config.statsVisible = 2
131
+ if (config.continuousRender !== undefined) this.continuousRender = config.continuousRender
132
+
133
+ void this.initData().then(() => {
134
+ this.addKeyboardShortcuts()
135
+ })
136
+ }
137
+
138
+ onParamsUpdate(paramName: string, object: any) { }
139
+
140
+ updateQs(paramName: string, valueSet: any) {
141
+ if (this.skipUpdateQs) return
142
+ const newQs = new URLSearchParams(window.location.search)
143
+ for (const [key, value] of Object.entries({ [paramName]: valueSet })) {
144
+ if (typeof value === 'function' || this.params.skipQs?.includes(key) || this.alwaysIgnoreQs.includes(key)) continue
145
+ if (value) {
146
+ newQs.set(key, value)
147
+ } else {
148
+ newQs.delete(key)
149
+ }
150
+ }
151
+ window.history.replaceState({}, '', `${window.location.pathname}?${newQs.toString()}`)
152
+ }
153
+
154
+ renderFinish() {
155
+ this.requestRender()
156
+ }
157
+
158
+ initGui() {
159
+ const qs = new URLSearchParams(window.location.search)
160
+ for (const key of Object.keys(this.params)) {
161
+ const value = qs.get(key)
162
+ if (!value) continue
163
+ const parsed = /^-?\d+$/.test(value) ? Number(value) : value === 'true' ? true : value === 'false' ? false : value
164
+ this.params[key] = parsed
165
+ }
166
+
167
+ for (const param of Object.keys(this.params)) {
168
+ const option = this.paramOptions[param]
169
+ if (option?.hide) continue
170
+ this.gui.add(this.params, param, option?.options ?? option?.min, option?.max)
171
+ }
172
+ if (window.innerHeight < 700) {
173
+ this.gui.open(false)
174
+ } else {
175
+ setTimeout(() => {
176
+ this.gui.domElement.classList.remove('transition')
177
+ }, 500)
178
+ }
179
+
180
+ this.gui.onChange(({ property, object }) => {
181
+ if (object === this.params) {
182
+ this.onParamUpdate[property]?.()
183
+ this.onParamsUpdate(property, object)
184
+ const value = this.params[property]
185
+ if (this.paramOptions[property]?.reloadOnChange && (typeof value === 'boolean' || this.paramOptions[property].options)) {
186
+ setTimeout(() => {
187
+ window.location.reload()
188
+ })
189
+ }
190
+ this.updateQs(property, value)
191
+ } else {
192
+ this.onParamsUpdate(property, object)
193
+ }
194
+ })
195
+ }
196
+
197
+ // Overridable methods
198
+ setupWorld() { }
199
+ sceneReset() { }
200
+
201
+ // eslint-disable-next-line max-params
202
+ addWorldBlock(xOffset: number, yOffset: number, zOffset: number, blockName: BlockNames, properties?: Record<string, any>) {
203
+ if (xOffset > 16 || yOffset > 16 || zOffset > 16) throw new Error('Offset too big')
204
+ const block =
205
+ properties ?
206
+ this.Block.fromProperties(this.mcData.blocksByName[blockName].id, properties ?? {}, 0) :
207
+ this.Block.fromStateId(this.mcData.blocksByName[blockName].defaultState, 0)
208
+ this.world.setBlock(this.targetPos.offset(xOffset, yOffset, zOffset), block)
209
+ }
210
+
211
+ // Sync our camera state to the graphics backend
212
+ // Extract rotation from OrbitControls spherical coordinates to avoid flip issues
213
+ protected syncCameraToBackend(onlyRotation = false) {
214
+ if (!this.appViewer.backend || !this.camera) return
215
+
216
+ // Extract rotation from camera's quaternion to avoid gimbal lock issues
217
+ // Get forward direction vector to extract yaw/pitch properly
218
+ const forward = new THREE.Vector3(0, 0, -1)
219
+ forward.applyQuaternion(this.camera.quaternion)
220
+
221
+ // Calculate yaw and pitch from forward vector
222
+ // Yaw: rotation around Y axis (horizontal)
223
+ const yaw = Math.atan2(-forward.x, -forward.z)
224
+ // Pitch: angle from horizontal plane (vertical)
225
+ const pitch = Math.asin(forward.y)
226
+
227
+ if (onlyRotation) {
228
+ this.appViewer.backend.updateCamera(null, yaw, pitch)
229
+ return
230
+ }
231
+
232
+ const pos = new Vec3(this.camera.position.x, this.camera.position.y, this.camera.position.z)
233
+ this.appViewer.backend.updateCamera(pos, yaw, pitch)
234
+ }
235
+
236
+ resetCamera() {
237
+ if (!this.camera) return
238
+ const { targetPos } = this
239
+ this.controls?.target.set(targetPos.x + 0.5, targetPos.y + 0.5, targetPos.z + 0.5)
240
+
241
+ const cameraPos = targetPos.offset(2, 2, 2)
242
+ this.camera.position.set(cameraPos.x + 0.5, cameraPos.y + 0.5, cameraPos.z + 0.5)
243
+ this.camera.lookAt(targetPos.x + 0.5, targetPos.y + 0.5, targetPos.z + 0.5)
244
+ this.controls?.update()
245
+ // Sync after reset - this uses quaternion extraction which avoids flip issues
246
+ this.syncCameraToBackend()
247
+ }
248
+
249
+ async initData() {
250
+ const mcData: IndexedData = MinecraftData(this.version)
251
+ this.mcData = mcData
252
+ //@ts-ignore
253
+ window.loadedData = window.mcData = mcData
254
+
255
+ this.Chunk = (ChunkLoader as any)(this.version)
256
+ this.Block = (BlockLoader as any)(this.version)
257
+
258
+ const world = getSyncWorld(this.version)
259
+ world.setBlockStateId(this.targetPos, 0)
260
+ this.world = world
261
+
262
+ this.initGui()
263
+
264
+ // Use appViewer for resource management and world rendering
265
+ // worldConfig is already synced with appViewer.inWorldRenderingConfig via getter/setter
266
+
267
+ // Initialize resources manager via appViewer
268
+ this.appViewer.resourcesManager.currentConfig = { version: this.version, noInventoryGui: true }
269
+ await this.appViewer.resourcesManager.loadSourceData?.(this.version)
270
+ await this.appViewer.resourcesManager.updateAssetsData?.({})
271
+
272
+ // Load backend if not already loaded
273
+ if (!this.appViewer.backend) {
274
+ await this.appViewer.loadBackend(appGraphicBackends[0])
275
+ }
276
+
277
+ // Start world using appViewer
278
+ // This creates WorldDataEmitter, GraphicsBackend, and WorldRendererThree internally
279
+ await this.appViewer.startWorld(world, this.viewDistance, proxy(getInitialPlayerState()), this.targetPos)
280
+
281
+ // Get world view from appViewer
282
+ this.worldView = this.appViewer.worldView
283
+
284
+ // Create our own camera for OrbitControls - this is separate from the internal worldRenderer camera
285
+ // We sync our camera state to the backend via updateCamera()
286
+ this.camera = new THREE.PerspectiveCamera(
287
+ this.appViewer.inWorldRenderingConfig.fov || 75,
288
+ window.innerWidth / window.innerHeight,
289
+ 0.1,
290
+ 1000
291
+ )
292
+
293
+ // Setup world (adds blocks, etc.)
294
+ this.setupWorld()
295
+
296
+ // Initialize world view with target position (loads chunks after setup)
297
+ if (this.worldView) {
298
+ this.worldView.addWaitTime = 0
299
+ await this.worldView.init(this.targetPos)
300
+ }
301
+
302
+ // Setup camera controls with our own camera
303
+ if (this.enableCameraControls) {
304
+ const canvas = document.querySelector('#viewer-canvas')
305
+ if (canvas) {
306
+ const controls = this.enableCameraOrbitControl
307
+ ? new OrbitControls(this.camera, canvas as HTMLElement)
308
+ : undefined
309
+ this.controls = controls
310
+
311
+ this.resetCamera()
312
+
313
+ // Camera position from query string or localStorage
314
+ const cameraSet = this.params.camera || localStorage.camera
315
+ if (cameraSet) {
316
+ const [x, y, z, rx, ry] = cameraSet.split(',').map(Number)
317
+ this.camera.position.set(x, y, z)
318
+ this.camera.rotation.set(rx, ry, 0, 'ZYX')
319
+ this.controls?.update()
320
+ // this.syncCameraToBackend()
321
+ }
322
+
323
+ const throttledCamQsUpdate = _.throttle(() => {
324
+ if (!this.camera) return
325
+ localStorage.camera = [
326
+ this.camera.position.x.toFixed(2),
327
+ this.camera.position.y.toFixed(2),
328
+ this.camera.position.z.toFixed(2),
329
+ this.camera.rotation.x.toFixed(2),
330
+ this.camera.rotation.y.toFixed(2),
331
+ ].join(',')
332
+ }, 200)
333
+
334
+ if (this.controls) {
335
+ const throttledCameraSync = _.throttle(() => {
336
+ // this.syncCameraToBackend(true) // Only sync rotation when OrbitControls changes
337
+ }, 16) // ~60fps sync rate
338
+
339
+ this.controls.addEventListener('change', () => {
340
+ throttledCameraSync()
341
+ throttledCamQsUpdate()
342
+ this.requestRender()
343
+ })
344
+ } else {
345
+ setInterval(() => {
346
+ throttledCamQsUpdate()
347
+ }, 200)
348
+ }
349
+ }
350
+ }
351
+
352
+ // Manual camera controls (if orbit controls disabled)
353
+ if (!this.enableCameraOrbitControl && this.camera) {
354
+ let mouseMoveCounter = 0
355
+ const mouseMove = (e: PointerEvent) => {
356
+ if ((e.target as HTMLElement).closest('.lil-gui')) return
357
+ if (e.buttons === 1 || e.pointerType === 'touch') {
358
+ mouseMoveCounter++
359
+ this.camera.rotation.x -= e.movementY / 100
360
+ this.camera.rotation.y -= e.movementX / 100
361
+ if (this.camera.rotation.x < -Math.PI / 2) this.camera.rotation.x = -Math.PI / 2
362
+ if (this.camera.rotation.x > Math.PI / 2) this.camera.rotation.x = Math.PI / 2
363
+ this.syncCameraToBackend(true)
364
+ }
365
+ if (e.buttons === 2) {
366
+ this.camera.position.set(0, 0, 0)
367
+ this.syncCameraToBackend()
368
+ }
369
+ }
370
+ setInterval(() => {
371
+ mouseMoveCounter = 0
372
+ }, 1000)
373
+ window.addEventListener('pointermove', mouseMove)
374
+ }
375
+
376
+ // Setup resize handler
377
+ this.onResize()
378
+ window.addEventListener('resize', () => this.onResize())
379
+
380
+ // Setup debug FPS GUI
381
+ this.setupDebugFpsGui()
382
+
383
+ // Wait for chunks and finish setup
384
+ // Access worldRenderer via window.world for this one-time operation
385
+ // const worldRenderer = window.world
386
+ // if (worldRenderer) {
387
+ // void worldRenderer.waitForChunksToRender().then(async () => {
388
+ // this.renderFinish()
389
+ // })
390
+
391
+ // // Listen for world updates to trigger on-demand renders
392
+ // worldRenderer.renderUpdateEmitter.addListener('update', () => {
393
+ // this.requestRender()
394
+ // })
395
+ // }
396
+
397
+ // // Start render loop if continuous, otherwise use on-demand rendering
398
+ // if (this.continuousRender) {
399
+ // this.loop()
400
+ // }
401
+ this.renderFinish()
402
+ this.mainDebugLoop()
403
+ }
404
+
405
+ mainDebugLoop() {
406
+ requestAnimationFrame(() => this.mainDebugLoop())
407
+ this.trackFrame()
408
+ }
409
+
410
+ loop() {
411
+ if (this.continuousRender && !this.windowHidden) {
412
+ this.requestRender()
413
+ requestAnimationFrame(() => this.loop())
414
+ }
415
+ }
416
+
417
+ // Request a render from the backend (on-demand rendering)
418
+ // The DocumentRenderer loop handles actual rendering continuously
419
+ // Camera sync happens via syncCameraToBackend() which updates the internal camera
420
+ requestRender() {
421
+ // No-op: rendering is handled by DocumentRenderer's continuous loop
422
+ // This method exists for API compatibility
423
+ }
424
+
425
+ private setupDebugFpsGui() {
426
+ // Create simple DOM element for debug FPS display in bottom left corner
427
+ this.debugFpsElement = document.createElement('div')
428
+ this.debugFpsElement.style.position = 'fixed'
429
+ this.debugFpsElement.style.bottom = '0'
430
+ this.debugFpsElement.style.left = '0'
431
+ this.debugFpsElement.style.zIndex = '1000'
432
+ this.debugFpsElement.style.backgroundColor = 'rgba(0, 0, 0, 0.7)'
433
+ this.debugFpsElement.style.color = '#fff'
434
+ this.debugFpsElement.style.padding = '4px 6px'
435
+ this.debugFpsElement.style.fontFamily = 'monospace'
436
+ this.debugFpsElement.style.fontSize = '11px'
437
+ this.debugFpsElement.style.lineHeight = '1.2'
438
+ this.debugFpsElement.style.pointerEvents = 'none'
439
+ this.debugFpsElement.style.userSelect = 'none'
440
+ this.debugFpsElement.textContent = 'FPS: 0 | Max: 0 ms'
441
+
442
+ document.body.appendChild(this.debugFpsElement)
443
+
444
+ // Update debug info every second
445
+ setInterval(() => {
446
+ this.updateDebugInfo()
447
+ }, 1000)
448
+ }
449
+
450
+ private trackFrame() {
451
+ const now = performance.now()
452
+ this.frameTimes.push(now)
453
+ this.frameCount++
454
+
455
+ // Calculate frame delay (time since last frame)
456
+ if (this.frameTimes.length > 1) {
457
+ const delay = now - this.frameTimes.at(-2)!
458
+ if (delay > this.maxFrameDelay) {
459
+ this.maxFrameDelay = delay
460
+ }
461
+ }
462
+
463
+ // Keep only last second of frame times
464
+ const oneSecondAgo = now - 1000
465
+ this.frameTimes = this.frameTimes.filter(time => time > oneSecondAgo)
466
+ }
467
+
468
+ private updateDebugInfo() {
469
+ if (!this.debugFpsElement) return
470
+
471
+ // Calculate FPS from number of frames in the last second
472
+ // frameTimes array contains timestamps from the last second after filtering
473
+ const fps = this.frameTimes.length
474
+
475
+ this.currentFps = fps
476
+
477
+ const isSeriousDelay = this.maxFrameDelay > 150
478
+ const delayText = isSeriousDelay
479
+ ? `<span style="color: #ff4444;">${this.maxFrameDelay.toFixed(0)}ms</span>`
480
+ : `${this.maxFrameDelay.toFixed(0)}ms`
481
+
482
+ // Update the DOM element directly - single line format
483
+ this.debugFpsElement.innerHTML = `FPS: ${fps} | Max Delay: ${delayText}`
484
+
485
+ // Reset for next second
486
+ this.lastSecondTime = performance.now()
487
+ this.maxFrameDelay = 0
488
+ this.frameCount = 0
489
+ }
490
+
491
+ // Legacy render method for compatibility
492
+ render(fromLoop = false) {
493
+ this.requestRender()
494
+ }
495
+
496
+ addKeyboardShortcuts() {
497
+ document.addEventListener('keydown', (e) => {
498
+ if (!e.shiftKey && !e.ctrlKey && !e.altKey && !e.metaKey) {
499
+ if (e.code === 'KeyR') {
500
+ this.controls?.reset()
501
+ this.resetCamera()
502
+ }
503
+ if (e.code === 'KeyE') { // refresh block (main)
504
+ this.worldView!.setBlockStateId(this.targetPos, this.world.getBlockStateId(this.targetPos))
505
+ }
506
+ if (e.code === 'KeyF') { // reload all chunks
507
+ this.sceneReset()
508
+ this.worldView!.unloadAllChunks()
509
+ void this.worldView!.init(this.targetPos)
510
+ }
511
+ }
512
+ })
513
+ document.addEventListener('visibilitychange', () => {
514
+ this.windowHidden = document.visibilityState === 'hidden'
515
+ })
516
+ document.addEventListener('blur', () => {
517
+ this.windowHidden = true
518
+ })
519
+ document.addEventListener('focus', () => {
520
+ this.windowHidden = false
521
+ })
522
+
523
+ const pressedKeys = new Set<string>()
524
+ const updateKeys = () => {
525
+ if (pressedKeys.has('ControlLeft') || pressedKeys.has('MetaLeft')) {
526
+ return
527
+ }
528
+ if (!this.camera) return
529
+
530
+ const direction = new THREE.Vector3(0, 0, 0)
531
+ if (pressedKeys.has('KeyW')) {
532
+ direction.z = -0.5
533
+ }
534
+ if (pressedKeys.has('KeyS')) {
535
+ direction.z += 0.5
536
+ }
537
+ if (pressedKeys.has('KeyA')) {
538
+ direction.x -= 0.5
539
+ }
540
+ if (pressedKeys.has('KeyD')) {
541
+ direction.x += 0.5
542
+ }
543
+
544
+ if (pressedKeys.has('ShiftLeft')) {
545
+ this.camera.position.y -= 0.5
546
+ }
547
+ if (pressedKeys.has('Space')) {
548
+ this.camera.position.y += 0.5
549
+ }
550
+ direction.applyQuaternion(this.camera.quaternion)
551
+ direction.y = 0
552
+
553
+ if (pressedKeys.has('ShiftLeft')) {
554
+ direction.y *= 2
555
+ direction.x *= 2
556
+ direction.z *= 2
557
+ }
558
+ this.camera.position.add(direction.normalize())
559
+ this.controls?.update()
560
+ this.syncCameraToBackend()
561
+ this.requestRender()
562
+ }
563
+ setInterval(updateKeys, 1000 / 20)
564
+
565
+ const keys = (e: KeyboardEvent) => {
566
+ const { code } = e
567
+ const pressed = e.type === 'keydown'
568
+ if (pressed) {
569
+ pressedKeys.add(code)
570
+ } else {
571
+ pressedKeys.delete(code)
572
+ }
573
+ }
574
+
575
+ window.addEventListener('keydown', keys)
576
+ window.addEventListener('keyup', keys)
577
+ window.addEventListener('blur', () => {
578
+ for (const key of pressedKeys) {
579
+ keys(new KeyboardEvent('keyup', { code: key }))
580
+ }
581
+ })
582
+ }
583
+
584
+ onResize() {
585
+ this.requestRender()
586
+ }
587
+ }