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,491 @@
1
+ /**
2
+ * DocumentRenderer - Manages Three.js WebGLRenderer and render loop.
3
+ *
4
+ * Features:
5
+ * - Automatic canvas sizing with resize event optimization
6
+ * - FPS limiting support
7
+ * - Stats overlay (optional)
8
+ * - Timeout-based rendering option for background tabs
9
+ */
10
+
11
+ import * as THREE from 'three'
12
+ import Stats from 'stats.js'
13
+ import StatsGl from 'stats-gl'
14
+ import * as tween from '@tweenjs/tween.js'
15
+ import { WorldRendererConfig } from '../lib/worldrendererCommon'
16
+
17
+ // ============================================================================
18
+ // Types (co-located with implementation)
19
+ // ============================================================================
20
+
21
+ export interface GraphicsBackendConfig {
22
+ fpsLimit?: number
23
+ powerPreference?: 'high-performance' | 'low-power'
24
+ statsVisible?: number
25
+ sceneBackground: string
26
+ timeoutRendering?: boolean
27
+ }
28
+
29
+ export interface FrameTimingEvent {
30
+ type: 'frameStart' | 'frameEnd' | 'cameraUpdate' | 'frameDisplay'
31
+ timestamp: number
32
+ duration?: number
33
+ }
34
+
35
+ export interface NonReactiveState {
36
+ fps: number
37
+ worstRenderTime: number
38
+ avgRenderTime: number
39
+ world: {
40
+ chunksLoaded: Set<string>
41
+ chunksTotalNumber: number
42
+ }
43
+ renderer: {
44
+ timeline: {
45
+ live: FrameTimingEvent[]
46
+ frozen: FrameTimingEvent[]
47
+ lastSecond: FrameTimingEvent[]
48
+ }
49
+ }
50
+ }
51
+
52
+ export interface GraphicsInitOptions<S = any> {
53
+ resourcesManager: any
54
+ config: GraphicsBackendConfig
55
+ rendererSpecificSettings: S
56
+ callbacks: {
57
+ displayCriticalError: (error: Error) => void
58
+ setRendererSpecificSettings: (key: string, value: any) => void
59
+ fireCustomEvent: (eventName: string, ...args: any[]) => void
60
+ }
61
+ }
62
+
63
+ export interface ThreeRendererMainData {
64
+ canvas: OffscreenCanvas
65
+ }
66
+
67
+ export const isWebWorker = typeof (globalThis as any).WorkerGlobalScope !== 'undefined' &&
68
+ globalThis instanceof (globalThis as any).WorkerGlobalScope
69
+
70
+ // ============================================================================
71
+ // TopRightStats - Performance stats display
72
+ // ============================================================================
73
+
74
+ class TopRightStats {
75
+ private readonly stats: Stats
76
+ private readonly stats2: Stats
77
+ private readonly statsGl: StatsGl
78
+ private total = 0
79
+ private readonly denseMode: boolean
80
+
81
+ constructor(private readonly canvas: HTMLCanvasElement, initialStatsVisible = 0) {
82
+ this.stats = new Stats()
83
+ this.stats2 = new Stats()
84
+ this.statsGl = new StatsGl({ minimal: true })
85
+ this.stats2.showPanel(2)
86
+ this.denseMode = typeof process !== 'undefined' && process.env?.NODE_ENV === 'production' ||
87
+ (typeof window !== 'undefined' && window.innerHeight < 500)
88
+
89
+ this.initStats()
90
+ this.setVisibility(initialStatsVisible)
91
+ }
92
+
93
+ private addStat(dom: HTMLElement, size = 80) {
94
+ dom.style.position = 'absolute'
95
+ if (this.denseMode) dom.style.height = '12px'
96
+ dom.style.overflow = 'hidden'
97
+ dom.style.left = ''
98
+ dom.style.top = '0'
99
+ dom.style.right = `${this.total}px`
100
+ dom.style.width = '80px'
101
+ dom.style.zIndex = '1'
102
+ dom.style.opacity = '0.8'
103
+ const hasAppContainer = document.getElementById('corner-indicator-stats')
104
+ const container = hasAppContainer ?? document.body
105
+ if (hasAppContainer) {
106
+ dom.style.position = 'relative'
107
+ }
108
+ container.appendChild(dom)
109
+ this.total += size
110
+ }
111
+
112
+ private initStats() {
113
+ const hasRamPanel = this.stats2.dom.children.length === 3
114
+
115
+ this.addStat(this.stats.dom)
116
+ if (hasRamPanel) {
117
+ this.addStat(this.stats2.dom)
118
+ }
119
+
120
+ this.statsGl.init(this.canvas)
121
+ this.statsGl.container.style.display = 'flex'
122
+ this.statsGl.container.style.justifyContent = 'flex-end'
123
+
124
+ let i = 0
125
+ for (const _child of this.statsGl.container.children) {
126
+ const child = _child as HTMLElement
127
+ if (i++ === 0) {
128
+ child.style.display = 'none'
129
+ }
130
+ child.style.position = ''
131
+ }
132
+ }
133
+
134
+ setVisibility(level: number) {
135
+ const visible = level > 0
136
+ if (visible) {
137
+ this.stats.dom.style.display = 'block'
138
+ this.stats2.dom.style.display = level >= 2 ? 'block' : 'none'
139
+ this.statsGl.container.style.display = level >= 2 ? 'block' : 'none'
140
+ } else {
141
+ this.stats.dom.style.display = 'none'
142
+ this.stats2.dom.style.display = 'none'
143
+ this.statsGl.container.style.display = 'none'
144
+ }
145
+ }
146
+
147
+ markStart() {
148
+ this.stats.begin()
149
+ this.stats2.begin()
150
+ this.statsGl.begin()
151
+ }
152
+
153
+ markEnd() {
154
+ this.stats.end()
155
+ this.stats2.end()
156
+ this.statsGl.end()
157
+ }
158
+
159
+ dispose() {
160
+ this.stats.dom.remove()
161
+ this.stats2.dom.remove()
162
+ this.statsGl.container.remove()
163
+ }
164
+ }
165
+
166
+ // ============================================================================
167
+ // DocumentRenderer - Core rendering loop manager
168
+ // ============================================================================
169
+
170
+ export class DocumentRenderer {
171
+ canvas!: HTMLCanvasElement | OffscreenCanvas
172
+ readonly renderer: THREE.WebGLRenderer
173
+ private animationFrameId?: number
174
+ readonly timeoutId?: number
175
+ private lastRenderTime = 0
176
+
177
+ // Size tracking - optimized to only update on resize
178
+ private previousCanvasWidth = 0
179
+ private previousCanvasHeight = 0
180
+ private currentWidth = 0
181
+ private currentHeight = 0
182
+ private pendingResize = false
183
+
184
+ private renderedFps = 0
185
+ private fpsInterval: any
186
+ private readonly stats: TopRightStats | undefined
187
+ private paused = false
188
+ disconnected = false
189
+
190
+ // Render hooks
191
+ preRender = () => { }
192
+ render = (sizeChanged: boolean) => { }
193
+ postRender = () => { }
194
+ sizeChanged = () => { }
195
+
196
+ droppedFpsPercentage = 0
197
+ config: GraphicsBackendConfig
198
+ onRender: Array<(sizeChanged: boolean) => void> = []
199
+ inWorldRenderingConfig: WorldRendererConfig | undefined
200
+ public nonReactiveState: NonReactiveState | undefined
201
+
202
+ constructor(
203
+ public initOptions: GraphicsInitOptions,
204
+ public externalCanvas?: OffscreenCanvas,
205
+ mainData?: ThreeRendererMainData
206
+ ) {
207
+ this.config = initOptions.config
208
+
209
+ // Handle canvas creation/transfer based on context
210
+ if (externalCanvas) {
211
+ this.canvas = externalCanvas
212
+ } else {
213
+ this.addToPage()
214
+ }
215
+
216
+ try {
217
+ this.renderer = new THREE.WebGLRenderer({
218
+ canvas: this.canvas,
219
+ preserveDrawingBuffer: true,
220
+ logarithmicDepthBuffer: true,
221
+ powerPreference: this.config.powerPreference
222
+ })
223
+ } catch (err: any) {
224
+ initOptions.callbacks.displayCriticalError(
225
+ new Error(`Failed to create WebGL context, not possible to render (restart browser): ${err.message}`)
226
+ )
227
+ throw err
228
+ }
229
+
230
+ // @ts-ignore - legacy lights API
231
+ this.renderer.useLegacyLights = true
232
+ this.renderer.outputColorSpace = THREE.LinearSRGBColorSpace
233
+
234
+ if (!externalCanvas) {
235
+ this.updatePixelRatio()
236
+ // Setup resize listener - only update size on actual resize events
237
+ this.setupResizeListener()
238
+ }
239
+
240
+ this.sizeUpdated()
241
+ // Initialize previous dimensions
242
+ this.previousCanvasWidth = this.canvas.width
243
+ this.previousCanvasHeight = this.canvas.height
244
+
245
+ const supportsWebGL2 = 'WebGL2RenderingContext' in globalThis
246
+ // Only initialize stats and DOM-related features in main thread (not worker)
247
+ if (!externalCanvas && supportsWebGL2 && !isWebWorker) {
248
+ this.stats = new TopRightStats(this.canvas as HTMLCanvasElement, this.config.statsVisible)
249
+ }
250
+
251
+ this.setupFpsTracking()
252
+ this.startRenderLoop()
253
+ }
254
+
255
+ /**
256
+ * Setup resize event listener for optimized size updates.
257
+ * Only reads document.body dimensions when resize event fires.
258
+ */
259
+ private setupResizeListener(): void {
260
+ if (typeof window === 'undefined') return
261
+
262
+ let resizeTimeout: ReturnType<typeof setTimeout> | undefined
263
+
264
+ const handleResize = () => {
265
+ // Debounce resize to avoid excessive updates
266
+ if (resizeTimeout) {
267
+ clearTimeout(resizeTimeout)
268
+ }
269
+ resizeTimeout = setTimeout(() => {
270
+ this.pendingResize = true
271
+ }, 16) // ~60fps debounce
272
+ }
273
+
274
+ window.addEventListener('resize', handleResize, { passive: true })
275
+ }
276
+
277
+ updatePixelRatio(): void {
278
+ if (typeof window === 'undefined') return
279
+
280
+ let pixelRatio = window.devicePixelRatio || 1
281
+ if (!this.renderer.capabilities.isWebGL2) {
282
+ pixelRatio = 1 // WebGL1 has issues with high pixel ratio
283
+ }
284
+ this.renderer.setPixelRatio(pixelRatio)
285
+ }
286
+
287
+ sizeUpdated(): void {
288
+ this.renderer.setSize(this.currentWidth, this.currentHeight, false)
289
+ }
290
+
291
+ private addToPage(): void {
292
+ this.canvas = addCanvasToPage()
293
+ // Initial size read
294
+ this.updateCanvasSize()
295
+ }
296
+
297
+ /**
298
+ * Update size from external source (for worker/offscreen scenarios).
299
+ */
300
+ updateSizeExternal(newWidth: number, newHeight: number, pixelRatio: number): void {
301
+ this.currentWidth = newWidth
302
+ this.currentHeight = newHeight
303
+ this.renderer.setPixelRatio(pixelRatio)
304
+ this.sizeUpdated()
305
+ }
306
+
307
+ /**
308
+ * Update canvas size from document body.
309
+ * Only called when pendingResize is true (after resize event).
310
+ */
311
+ private updateCanvasSize(): void {
312
+ if (this.externalCanvas) return
313
+ if (typeof document === 'undefined') return
314
+
315
+ // Only read body dimensions when we know a resize occurred
316
+ const innerWidth = document.body.offsetWidth
317
+ const innerHeight = document.body.offsetHeight
318
+
319
+ if (this.currentWidth !== innerWidth) {
320
+ this.currentWidth = innerWidth
321
+ }
322
+ if (this.currentHeight !== innerHeight) {
323
+ this.currentHeight = innerHeight
324
+ }
325
+ }
326
+
327
+ private setupFpsTracking(): void {
328
+ let max = 0
329
+ this.fpsInterval = setInterval(() => {
330
+ if (max > 0) {
331
+ this.droppedFpsPercentage = this.renderedFps / max
332
+ }
333
+ max = Math.max(this.renderedFps, max)
334
+ if (this.nonReactiveState) {
335
+ this.nonReactiveState.fps = this.renderedFps
336
+ }
337
+ this.renderedFps = 0
338
+ }, 1000)
339
+ }
340
+
341
+ private startRenderLoop(): void {
342
+ const animate = () => {
343
+ if (this.disconnected) return
344
+
345
+ // Schedule next frame based on rendering mode
346
+ if (this.config.timeoutRendering) {
347
+ const targetFps = this.config.fpsLimit ? Math.min(this.config.fpsLimit, 60) : 60
348
+ const timeoutMs = 1000 / targetFps
349
+ ; (this as any).timeoutId = setTimeout(animate, timeoutMs)
350
+ } else {
351
+ this.animationFrameId = requestAnimationFrame(animate)
352
+ }
353
+
354
+ if (this.paused ||
355
+ (this.renderer.xr.isPresenting && !this.inWorldRenderingConfig?.vrPageGameRendering)) {
356
+ return
357
+ }
358
+
359
+ // Handle FPS limiting (for requestAnimationFrame mode)
360
+ if (!this.config.timeoutRendering && this.config.fpsLimit) {
361
+ const now = performance.now()
362
+ const elapsed = now - this.lastRenderTime
363
+ const fpsInterval = 1000 / this.config.fpsLimit
364
+
365
+ if (elapsed < fpsInterval) {
366
+ return
367
+ }
368
+
369
+ this.lastRenderTime = now - (elapsed % fpsInterval)
370
+ }
371
+
372
+ let sizeChanged = false
373
+
374
+ // Only update canvas size if a resize event occurred
375
+ if (this.pendingResize) {
376
+ this.updateCanvasSize()
377
+ this.pendingResize = false
378
+ }
379
+
380
+ if (this.previousCanvasWidth !== this.currentWidth ||
381
+ this.previousCanvasHeight !== this.currentHeight) {
382
+ this.previousCanvasWidth = this.currentWidth
383
+ this.previousCanvasHeight = this.currentHeight
384
+ this.sizeUpdated()
385
+ sizeChanged = true
386
+ }
387
+
388
+ this.frameRender(sizeChanged)
389
+
390
+ // Update stats visibility each frame (main thread only)
391
+ if (this.config.statsVisible !== undefined) {
392
+ this.stats?.setVisibility(this.config.statsVisible)
393
+ }
394
+ }
395
+
396
+ animate()
397
+ }
398
+
399
+ frameRender(sizeChanged: boolean): void {
400
+ this.preRender()
401
+ this.stats?.markStart()
402
+ tween.update()
403
+
404
+ if (!(globalThis as any).freezeRender) {
405
+ this.render(sizeChanged)
406
+ }
407
+
408
+ for (const fn of this.onRender) {
409
+ fn(sizeChanged)
410
+ }
411
+
412
+ this.renderedFps++
413
+ this.stats?.markEnd()
414
+ this.postRender()
415
+ }
416
+
417
+ setPaused(paused: boolean): void {
418
+ this.paused = paused
419
+ }
420
+
421
+ dispose(): void {
422
+ this.disconnected = true
423
+
424
+ if (this.animationFrameId) {
425
+ cancelAnimationFrame(this.animationFrameId)
426
+ }
427
+ if (this.timeoutId) {
428
+ clearTimeout(this.timeoutId)
429
+ }
430
+ if (this.canvas instanceof HTMLCanvasElement) {
431
+ this.canvas.remove()
432
+ }
433
+ clearInterval(this.fpsInterval)
434
+ this.stats?.dispose()
435
+ this.renderer.dispose()
436
+ }
437
+ }
438
+
439
+ // ============================================================================
440
+ // Helper functions
441
+ // ============================================================================
442
+
443
+ /**
444
+ * Adds a canvas element to the page.
445
+ */
446
+ function addCanvasToPage(): HTMLCanvasElement {
447
+ const canvas = document.createElement('canvas')
448
+ canvas.id = 'viewer-canvas'
449
+ document.body.appendChild(canvas)
450
+ return canvas
451
+ }
452
+
453
+ /**
454
+ * Creates a canvas for worker thread rendering.
455
+ */
456
+ export const addCanvasForWorker = (): {
457
+ canvas: OffscreenCanvas
458
+ destroy: () => void
459
+ onSizeChanged: (cb: (width: number, height: number) => void) => void
460
+ size: { width: number; height: number }
461
+ } => {
462
+ const canvas = addCanvasToPage()
463
+ const transferred = canvas.transferControlToOffscreen()
464
+ let removed = false
465
+ let onSizeChanged = (w: number, h: number) => { }
466
+ let oldSize = { width: 0, height: 0 }
467
+
468
+ const checkSize = () => {
469
+ if (removed) return
470
+ if (oldSize.width !== window.innerWidth || oldSize.height !== window.innerHeight) {
471
+ onSizeChanged(window.innerWidth, window.innerHeight)
472
+ oldSize = { width: window.innerWidth, height: window.innerHeight }
473
+ }
474
+ requestAnimationFrame(checkSize)
475
+ }
476
+ requestAnimationFrame(checkSize)
477
+
478
+ return {
479
+ canvas: transferred,
480
+ destroy() {
481
+ removed = true
482
+ canvas.remove()
483
+ },
484
+ onSizeChanged(cb: (width: number, height: number) => void) {
485
+ onSizeChanged = cb
486
+ },
487
+ get size() {
488
+ return { width: window.innerWidth, height: window.innerHeight }
489
+ }
490
+ }
491
+ }