@luma.gl/engine
Version:
3D Engine Components for luma.gl
4 lines • 399 kB
Source Map (JSON)
{
"version": 3,
"sources": ["../src/index.ts", "../src/animation/timeline.ts", "../src/animation/key-frames.ts", "../src/animation-loop/animation-loop-template.ts", "../src/animation-loop/animation-loop.ts", "../src/animation-loop/request-animation-frame.ts", "../src/animation-loop/make-animation-loop.ts", "../src/model/model.ts", "../src/geometry/gpu-geometry.ts", "../src/utils/uid.ts", "../src/debug/debug-shader-layout.ts", "../src/debug/debug-framebuffer.ts", "../src/utils/deep-equal.ts", "../src/utils/buffer-layout-helper.ts", "../src/utils/buffer-layout-order.ts", "../src/utils/shader-module-utils.ts", "../src/shader-inputs.ts", "../src/model/split-uniforms-and-bindings.ts", "../src/dynamic-texture/dynamic-texture.ts", "../src/dynamic-texture/texture-data.ts", "../src/material/material.ts", "../src/material/material-factory.ts", "../src/compute/buffer-transform.ts", "../src/compute/texture-transform.ts", "../src/geometry/geometry.ts", "../src/models/clip-space.ts", "../src/models/billboard-texture-model.ts", "../src/geometries/sphere-geometry.ts", "../src/models/light-model-utils.ts", "../src/models/point-light-model.ts", "../src/geometries/truncated-cone-geometry.ts", "../src/geometries/cone-geometry.ts", "../src/models/spot-light-model.ts", "../src/models/directional-light-model.ts", "../src/scenegraph/scenegraph-node.ts", "../src/scenegraph/group-node.ts", "../src/scenegraph/model-node.ts", "../src/geometries/cube-geometry.ts", "../src/geometries/cylinder-geometry.ts", "../src/geometries/ico-sphere-geometry.ts", "../src/geometry/geometry-utils.ts", "../src/geometries/plane-geometry.ts", "../src/application-utils/random.ts", "../src/application-utils/load-file.ts", "../src/passes/shader-pass-renderer.ts", "../src/compute/swap.ts", "../src/passes/get-fragment-shader.ts", "../src/compute/computation.ts", "../src/modules/picking/picking-manager.ts", "../src/modules/picking/picking-uniforms.ts", "../src/modules/picking/color-picking.ts", "../src/modules/picking/index-picking.ts", "../src/modules/picking/picking.ts", "../src/modules/picking/legacy-picking-manager.ts", "../src/modules/picking/legacy-color-picking.ts"],
"sourcesContent": ["// luma.gl\n// SPDX-License-Identifier: MIT\n// Copyright (c) vis.gl contributors\n\n// luma.gl Engine API\n\n// Animation\nexport {Timeline} from './animation/timeline';\nexport {KeyFrames} from './animation/key-frames';\nexport type {AnimationProps} from './animation-loop/animation-props';\n\nexport {AnimationLoopTemplate} from './animation-loop/animation-loop-template';\n\nexport type {AnimationLoopProps} from './animation-loop/animation-loop';\nexport {AnimationLoop} from './animation-loop/animation-loop';\n\nexport type {MakeAnimationLoopProps} from './animation-loop/make-animation-loop';\nexport {makeAnimationLoop} from './animation-loop/make-animation-loop';\n\nexport type {ModelProps} from './model/model';\nexport {Model} from './model/model';\nexport type {MaterialProps} from './material/material';\nexport {Material} from './material/material';\nexport type {MaterialFactoryProps} from './material/material-factory';\nexport {MaterialFactory} from './material/material-factory';\n\n// Transforms\nexport type {BufferTransformProps} from './compute/buffer-transform';\nexport {BufferTransform} from './compute/buffer-transform';\nexport type {TextureTransformProps} from './compute/texture-transform';\nexport {TextureTransform} from './compute/texture-transform';\n\n// Models\nexport type {ClipSpaceProps} from './models/clip-space';\nexport {ClipSpace} from './models/clip-space';\nexport type {BackgroundTextureModelProps} from './models/billboard-texture-model';\nexport {BackgroundTextureModel} from './models/billboard-texture-model';\nexport type {\n BaseLightModelProps,\n PointLightModelProps,\n SpotLightModelProps,\n DirectionalLightModelProps\n} from './models/light-model-utils';\nexport {PointLightModel} from './models/point-light-model';\nexport {SpotLightModel} from './models/spot-light-model';\nexport {DirectionalLightModel} from './models/directional-light-model';\n\n// Scenegraph Core nodes\nexport {ScenegraphNode} from './scenegraph/scenegraph-node';\nexport {GroupNode} from './scenegraph/group-node';\nexport type {ModelNodeProps} from './scenegraph/model-node';\nexport {ModelNode} from './scenegraph/model-node';\n\n// Geometries\nexport type {GeometryProps, GeometryAttribute} from './geometry/geometry';\nexport {Geometry} from './geometry/geometry';\nexport type {GPUGeometryProps} from './geometry/gpu-geometry';\nexport {GPUGeometry} from './geometry/gpu-geometry';\n\n// Primitives\nexport type {ConeGeometryProps} from './geometries/cone-geometry';\nexport {ConeGeometry} from './geometries/cone-geometry';\nexport type {CubeGeometryProps} from './geometries/cube-geometry';\nexport {CubeGeometry} from './geometries/cube-geometry';\nexport type {CylinderGeometryProps} from './geometries/cylinder-geometry';\nexport {CylinderGeometry} from './geometries/cylinder-geometry';\nexport type {IcoSphereGeometryProps} from './geometries/ico-sphere-geometry';\nexport {IcoSphereGeometry} from './geometries/ico-sphere-geometry';\nexport type {PlaneGeometryProps} from './geometries/plane-geometry';\nexport {PlaneGeometry} from './geometries/plane-geometry';\nexport type {SphereGeometryProps} from './geometries/sphere-geometry';\nexport {SphereGeometry} from './geometries/sphere-geometry';\nexport type {TruncatedConeGeometryProps} from './geometries/truncated-cone-geometry';\nexport {TruncatedConeGeometry} from './geometries/truncated-cone-geometry';\n\nexport {ShaderInputs} from './shader-inputs';\n\n// Application Utilities\nexport {makeRandomGenerator} from './application-utils/random';\nexport {setPathPrefix, loadImage, loadImageBitmap} from './application-utils/load-file';\n\n// EXPERIMENTAL\nexport type {ShaderPassRendererProps} from './passes/shader-pass-renderer';\nexport {ShaderPassRenderer} from './passes/shader-pass-renderer';\n\nexport {Swap} from './compute/swap';\nexport {SwapBuffers} from './compute/swap';\nexport {SwapFramebuffers} from './compute/swap';\n\nexport type {ComputationProps} from './compute/computation';\nexport {Computation} from './compute/computation';\n\nexport type {\n TextureCubeFace,\n TextureImageData,\n Texture1DData,\n Texture2DData,\n Texture3DData,\n TextureCubeData,\n TextureArrayData,\n TextureCubeArrayData\n} from './dynamic-texture/texture-data';\n\nexport type {DynamicTextureProps} from './dynamic-texture/dynamic-texture';\nexport {DynamicTexture} from './dynamic-texture/dynamic-texture';\n\nexport type {\n PickInfo,\n PickingMode,\n ResolvedPickingMode,\n PickingBackend,\n PickingManagerProps,\n ResolvedPickingBackend\n} from './modules/picking/picking-manager';\nexport {\n PickingManager,\n supportsIndexPicking,\n resolvePickingMode,\n resolvePickingBackend\n} from './modules/picking/picking-manager';\nexport {picking} from './modules/picking/picking';\nexport {picking as indexPicking} from './modules/picking/index-picking';\nexport {picking as colorPicking} from './modules/picking/color-picking';\n\nexport {\n requestAnimationFramePolyfill,\n cancelAnimationFramePolyfill\n} from './animation-loop/request-animation-frame';\n\n// DEPRECATED\n\nexport {LegacyPickingManager} from './modules/picking/legacy-picking-manager';\nexport {legacyColorPicking} from './modules/picking/legacy-color-picking';\n\nimport {DynamicTexture, type DynamicTextureProps} from './dynamic-texture/dynamic-texture';\n/** @deprecated use DynamicTexture */\nexport const AsyncTexture = DynamicTexture;\n/** @deprecated use DynamicTextureProps */\nexport type AsyncTextureProps = DynamicTextureProps;\n", "// luma.gl\n// SPDX-License-Identifier: MIT\n// Copyright (c) vis.gl contributors\n\n/**\n * Timeline channel properties\n * @param delay = 0;\n * @param duration = Number.POSITIVE_INFINITY;\n * @param rate = 1\n * @param repeat = 1\n */\nexport type ChannelOptions = {\n delay?: number;\n duration?: number;\n rate?: number;\n repeat?: number;\n};\n\nexport type AnimationOptions = {\n setTime: (time: number) => void;\n};\n\ntype Channel = {\n time: number;\n delay: number;\n duration: number;\n rate: number;\n repeat: number;\n};\n\ntype Animation = {\n channel?: number;\n animation: {\n setTime: (time: number) => void;\n };\n};\n\nlet channelHandles = 1;\nlet animationHandles = 1;\n\nexport class Timeline {\n time: number = 0;\n channels = new Map<number, Channel>();\n animations = new Map<number, Animation>();\n playing: boolean = false;\n lastEngineTime: number = -1;\n\n constructor() {}\n\n addChannel(props: ChannelOptions): number {\n const {delay = 0, duration = Number.POSITIVE_INFINITY, rate = 1, repeat = 1} = props;\n\n const channelId = channelHandles++;\n const channel: Channel = {\n time: 0,\n delay,\n duration,\n rate,\n repeat\n };\n this._setChannelTime(channel, this.time);\n this.channels.set(channelId, channel);\n\n return channelId;\n }\n\n removeChannel(channelId: number): void {\n this.channels.delete(channelId);\n\n for (const [animationHandle, animation] of this.animations) {\n if (animation.channel === channelId) {\n this.detachAnimation(animationHandle);\n }\n }\n }\n\n isFinished(channelId: number): boolean {\n const channel = this.channels.get(channelId);\n if (channel === undefined) {\n return false;\n }\n\n return this.time >= channel.delay + channel.duration * channel.repeat;\n }\n\n getTime(channelId?: number): number {\n if (channelId === undefined) {\n return this.time;\n }\n\n const channel = this.channels.get(channelId);\n\n if (channel === undefined) {\n return -1;\n }\n\n return channel.time;\n }\n\n setTime(time: number): void {\n this.time = Math.max(0, time);\n\n const channels = this.channels.values();\n for (const channel of channels) {\n this._setChannelTime(channel, this.time);\n }\n\n const animations = this.animations.values();\n for (const animationData of animations) {\n const {animation, channel} = animationData;\n animation.setTime(this.getTime(channel));\n }\n }\n\n play(): void {\n this.playing = true;\n }\n\n pause(): void {\n this.playing = false;\n this.lastEngineTime = -1;\n }\n\n reset(): void {\n this.setTime(0);\n }\n\n attachAnimation(animation: AnimationOptions, channelHandle?: number): number {\n const animationHandle = animationHandles++;\n\n this.animations.set(animationHandle, {\n animation,\n channel: channelHandle\n });\n\n animation.setTime(this.getTime(channelHandle));\n\n return animationHandle;\n }\n\n detachAnimation(channelId: number): void {\n this.animations.delete(channelId);\n }\n\n update(engineTime: number): void {\n if (this.playing) {\n if (this.lastEngineTime === -1) {\n this.lastEngineTime = engineTime;\n }\n this.setTime(this.time + (engineTime - this.lastEngineTime));\n this.lastEngineTime = engineTime;\n }\n }\n\n _setChannelTime(channel: Channel, time: number): void {\n const offsetTime = time - channel.delay;\n const totalDuration = channel.duration * channel.repeat;\n // Note(Tarek): Don't loop on final repeat.\n if (offsetTime >= totalDuration) {\n channel.time = channel.duration * channel.rate;\n } else {\n channel.time = Math.max(0, offsetTime) % channel.duration;\n channel.time *= channel.rate;\n }\n }\n}\n", "// luma.gl\n// SPDX-License-Identifier: MIT\n// Copyright (c) vis.gl contributors\n\n// keyframes\nexport type KeyFrame<T> = [number, T];\n\n/** Holds a list of key frames (timestamped values) */\nexport class KeyFrames<T = number> {\n startIndex: number = -1;\n endIndex: number = -1;\n factor: number = 0;\n times: number[] = [];\n values: T[] = [];\n private _lastTime = -1;\n\n constructor(keyFrames: KeyFrame<T>[]) {\n this.setKeyFrames(keyFrames);\n this.setTime(0);\n }\n\n setKeyFrames(keyFrames: KeyFrame<T>[]): void {\n const numKeys = keyFrames.length;\n this.times.length = numKeys;\n this.values.length = numKeys;\n\n for (let i = 0; i < numKeys; ++i) {\n this.times[i] = keyFrames[i][0];\n this.values[i] = keyFrames[i][1];\n }\n\n this._calculateKeys(this._lastTime);\n }\n\n setTime(time: number): void {\n time = Math.max(0, time);\n\n if (time !== this._lastTime) {\n this._calculateKeys(time);\n this._lastTime = time;\n }\n }\n\n getStartTime(): number {\n return this.times[this.startIndex];\n }\n\n getEndTime(): number {\n return this.times[this.endIndex];\n }\n\n getStartData(): T {\n return this.values[this.startIndex];\n }\n\n getEndData(): T {\n return this.values[this.endIndex];\n }\n\n _calculateKeys(time: number): void {\n let index = 0;\n const numKeys = this.times.length;\n\n for (index = 0; index < numKeys - 2; ++index) {\n if (this.times[index + 1] > time) {\n break;\n }\n }\n\n this.startIndex = index;\n this.endIndex = index + 1;\n\n const startTime = this.times[this.startIndex];\n const endTime = this.times[this.endIndex];\n this.factor = Math.min(Math.max(0, (time - startTime) / (endTime - startTime)), 1);\n }\n}\n", "// luma.gl\n// SPDX-License-Identifier: MIT\n// Copyright (c) vis.gl contributors\n\nimport type {AnimationProps} from './animation-props';\n\n/**\n * Minimal class that represents a \"componentized\" rendering life cycle\n * (resource construction, repeated rendering, resource destruction)\n *\n * @note A motivation for this class compared to the raw animation loop is\n * that it simplifies TypeScript code by allowing resources to be typed unconditionally\n * since they are allocated in the constructor rather than in onInitialized\n *\n * @note Introduced in luma.gl v9\n *\n * @example AnimationLoopTemplate is intended to be subclassed,\n * but the subclass should not be instantiated directly. Instead the subclass\n * (i.e. the constructor of the subclass) should be used\n * as an argument to create an AnimationLoop.\n */\nexport abstract class AnimationLoopTemplate {\n constructor(animationProps?: AnimationProps) {}\n async onInitialize(animationProps: AnimationProps): Promise<unknown> {\n return null;\n }\n abstract onRender(animationProps: AnimationProps): unknown;\n abstract onFinalize(animationProps: AnimationProps): void;\n}\n", "// luma.gl\n// SPDX-License-Identifier: MIT\n// Copyright (c) vis.gl contributors\n\nimport {luma, Device} from '@luma.gl/core';\nimport {\n requestAnimationFramePolyfill,\n cancelAnimationFramePolyfill\n} from './request-animation-frame';\nimport {Timeline} from '../animation/timeline';\nimport {AnimationProps} from './animation-props';\nimport {Stats, Stat} from '@probe.gl/stats';\n\nlet statIdCounter = 0;\nconst ANIMATION_LOOP_STATS = 'Animation Loop';\n\n/** AnimationLoop properties */\nexport type AnimationLoopProps = {\n device: Device | Promise<Device>;\n\n onAddHTML?: (div: HTMLDivElement) => string; // innerHTML\n onInitialize?: (animationProps: AnimationProps) => Promise<unknown>;\n onRender?: (animationProps: AnimationProps) => unknown;\n onFinalize?: (animationProps: AnimationProps) => void;\n onError?: (reason: Error) => void;\n\n stats?: Stats;\n\n // view parameters - TODO move to CanvasContext?\n autoResizeViewport?: boolean;\n};\n\nexport type MutableAnimationLoopProps = {\n // view parameters\n autoResizeViewport?: boolean;\n};\n\n/** Convenient animation loop */\nexport class AnimationLoop {\n static defaultAnimationLoopProps = {\n device: null!,\n\n onAddHTML: () => '',\n onInitialize: async () => null,\n onRender: () => {},\n onFinalize: () => {},\n onError: error => console.error(error), // eslint-disable-line no-console\n\n stats: undefined!,\n\n // view parameters\n autoResizeViewport: false\n } as const satisfies Readonly<Required<AnimationLoopProps>>;\n\n device: Device | null = null;\n canvas: HTMLCanvasElement | OffscreenCanvas | null = null;\n\n props: Required<AnimationLoopProps>;\n animationProps: AnimationProps | null = null;\n timeline: Timeline | null = null;\n stats: Stats;\n sharedStats: Stats;\n cpuTime: Stat;\n gpuTime: Stat;\n frameRate: Stat;\n\n display: any;\n\n private _needsRedraw: string | false = 'initialized';\n\n _initialized: boolean = false;\n _running: boolean = false;\n _animationFrameId: any = null;\n _nextFramePromise: Promise<AnimationLoop> | null = null;\n _resolveNextFrame: ((animationLoop: AnimationLoop) => void) | null = null;\n _cpuStartTime: number = 0;\n _error: Error | null = null;\n _lastFrameTime: number = 0;\n\n /*\n * @param {HTMLCanvasElement} canvas - if provided, width and height will be passed to context\n */\n constructor(props: AnimationLoopProps) {\n this.props = {...AnimationLoop.defaultAnimationLoopProps, ...props};\n props = this.props;\n\n if (!props.device) {\n throw new Error('No device provided');\n }\n\n // state\n this.stats = props.stats || new Stats({id: `animation-loop-${statIdCounter++}`});\n this.sharedStats = luma.stats.get(ANIMATION_LOOP_STATS);\n this.frameRate = this.stats.get('Frame Rate');\n this.frameRate.setSampleSize(1);\n this.cpuTime = this.stats.get('CPU Time');\n this.gpuTime = this.stats.get('GPU Time');\n\n this.setProps({autoResizeViewport: props.autoResizeViewport});\n\n // Bind methods\n this.start = this.start.bind(this);\n this.stop = this.stop.bind(this);\n\n this._onMousemove = this._onMousemove.bind(this);\n this._onMouseleave = this._onMouseleave.bind(this);\n }\n\n destroy(): void {\n this.stop();\n this._setDisplay(null);\n this.device?._disableDebugGPUTime();\n }\n\n /** @deprecated Use .destroy() */\n delete(): void {\n this.destroy();\n }\n\n reportError(error: Error): void {\n this.props.onError(error);\n this._error = error;\n }\n\n /** Flags this animation loop as needing redraw */\n setNeedsRedraw(reason: string): this {\n this._needsRedraw = this._needsRedraw || reason;\n return this;\n }\n\n /** Query redraw status. Clears the flag. */\n needsRedraw(): false | string {\n const reason = this._needsRedraw;\n this._needsRedraw = false;\n return reason;\n }\n\n setProps(props: MutableAnimationLoopProps): this {\n if ('autoResizeViewport' in props) {\n this.props.autoResizeViewport = props.autoResizeViewport || false;\n }\n return this;\n }\n\n /** Starts a render loop if not already running */\n async start() {\n if (this._running) {\n return this;\n }\n this._running = true;\n\n try {\n let appContext;\n if (!this._initialized) {\n this._initialized = true;\n // Create the WebGL context\n await this._initDevice();\n this._initialize();\n if (!this._running) {\n return null;\n }\n\n // Note: onIntialize can return a promise (e.g. in case app needs to load resources)\n await this.props.onInitialize(this._getAnimationProps());\n }\n\n // check that we haven't been stopped\n if (!this._running) {\n return null;\n }\n\n // Start the loop\n if (appContext !== false) {\n // cancel any pending renders to ensure only one loop can ever run\n this._cancelAnimationFrame();\n this._requestAnimationFrame();\n }\n\n return this;\n } catch (err: unknown) {\n const error = err instanceof Error ? err : new Error('Unknown error');\n this.props.onError(error);\n // this._running = false; // TODO\n throw error;\n }\n }\n\n /** Stops a render loop if already running, finalizing */\n stop() {\n // console.debug(`Stopping ${this.constructor.name}`);\n if (this._running) {\n // call callback\n // If stop is called immediately, we can end up in a state where props haven't been initialized...\n if (this.animationProps && !this._error) {\n this.props.onFinalize(this.animationProps);\n }\n\n this._cancelAnimationFrame();\n this._nextFramePromise = null;\n this._resolveNextFrame = null;\n this._running = false;\n this._lastFrameTime = 0;\n }\n return this;\n }\n\n /** Explicitly draw a frame */\n redraw(time?: number): this {\n if (this.device?.isLost || this._error) {\n return this;\n }\n\n this._beginFrameTimers(time);\n\n this._setupFrame();\n this._updateAnimationProps();\n\n this._renderFrame(this._getAnimationProps());\n\n // clear needsRedraw flag\n this._clearNeedsRedraw();\n\n if (this._resolveNextFrame) {\n this._resolveNextFrame(this);\n this._nextFramePromise = null;\n this._resolveNextFrame = null;\n }\n\n this._endFrameTimers();\n\n return this;\n }\n\n /** Add a timeline, it will be automatically updated by the animation loop. */\n attachTimeline(timeline: Timeline): Timeline {\n this.timeline = timeline;\n return this.timeline;\n }\n\n /** Remove a timeline */\n detachTimeline(): void {\n this.timeline = null;\n }\n\n /** Wait until a render completes */\n waitForRender(): Promise<AnimationLoop> {\n this.setNeedsRedraw('waitForRender');\n\n if (!this._nextFramePromise) {\n this._nextFramePromise = new Promise(resolve => {\n this._resolveNextFrame = resolve;\n });\n }\n return this._nextFramePromise;\n }\n\n /** TODO - should use device.deviceContext */\n async toDataURL(): Promise<string> {\n this.setNeedsRedraw('toDataURL');\n await this.waitForRender();\n if (this.canvas instanceof HTMLCanvasElement) {\n return this.canvas.toDataURL();\n }\n throw new Error('OffscreenCanvas');\n }\n\n // PRIVATE METHODS\n\n _initialize(): void {\n this._startEventHandling();\n\n // Initialize the callback data\n this._initializeAnimationProps();\n this._updateAnimationProps();\n\n // Default viewport setup, in case onInitialize wants to render\n this._resizeViewport();\n\n this.device?._enableDebugGPUTime();\n }\n\n _setDisplay(display: any): void {\n if (this.display) {\n this.display.destroy();\n this.display.animationLoop = null;\n }\n\n // store animation loop on the display\n if (display) {\n display.animationLoop = this;\n }\n\n this.display = display;\n }\n\n _requestAnimationFrame(): void {\n if (!this._running) {\n return;\n }\n\n // VR display has a separate animation frame to sync with headset\n // TODO WebVR API discontinued, replaced by WebXR: https://immersive-web.github.io/webxr/\n // See https://developer.mozilla.org/en-US/docs/Web/API/VRDisplay/requestAnimationFrame\n // if (this.display && this.display.requestAnimationFrame) {\n // this._animationFrameId = this.display.requestAnimationFrame(this._animationFrame.bind(this));\n // }\n this._animationFrameId = requestAnimationFramePolyfill(this._animationFrame.bind(this));\n }\n\n _cancelAnimationFrame(): void {\n if (this._animationFrameId === null) {\n return;\n }\n\n // VR display has a separate animation frame to sync with headset\n // TODO WebVR API discontinued, replaced by WebXR: https://immersive-web.github.io/webxr/\n // See https://developer.mozilla.org/en-US/docs/Web/API/VRDisplay/requestAnimationFrame\n // if (this.display && this.display.cancelAnimationFramePolyfill) {\n // this.display.cancelAnimationFrame(this._animationFrameId);\n // }\n cancelAnimationFramePolyfill(this._animationFrameId);\n this._animationFrameId = null;\n }\n\n _animationFrame(time: number): void {\n if (!this._running) {\n return;\n }\n this.redraw(time);\n this._requestAnimationFrame();\n }\n\n // Called on each frame, can be overridden to call onRender multiple times\n // to support e.g. stereoscopic rendering\n _renderFrame(animationProps: AnimationProps): void {\n // Allow e.g. VR display to render multiple frames.\n if (this.display) {\n this.display._renderFrame(animationProps);\n return;\n }\n\n // call callback\n this.props.onRender(this._getAnimationProps());\n // end callback\n\n // Submit commands (necessary on WebGPU)\n this.device?.submit();\n }\n\n _clearNeedsRedraw(): void {\n this._needsRedraw = false;\n }\n\n _setupFrame(): void {\n this._resizeViewport();\n }\n\n // Initialize the object that will be passed to app callbacks\n _initializeAnimationProps(): void {\n const canvasContext = this.device?.getDefaultCanvasContext();\n if (!this.device || !canvasContext) {\n throw new Error('loop');\n }\n\n const canvas = canvasContext?.canvas;\n const useDevicePixels = canvasContext.props.useDevicePixels;\n\n this.animationProps = {\n animationLoop: this,\n\n device: this.device,\n canvasContext,\n canvas,\n // @ts-expect-error Deprecated\n useDevicePixels,\n\n timeline: this.timeline,\n\n needsRedraw: false,\n\n // Placeholders\n width: 1,\n height: 1,\n aspect: 1,\n\n // Animation props\n time: 0,\n startTime: Date.now(),\n engineTime: 0,\n tick: 0,\n tock: 0,\n\n // Experimental\n _mousePosition: null // Event props\n };\n }\n\n _getAnimationProps(): AnimationProps {\n if (!this.animationProps) {\n throw new Error('animationProps');\n }\n return this.animationProps;\n }\n\n // Update the context object that will be passed to app callbacks\n _updateAnimationProps(): void {\n if (!this.animationProps) {\n return;\n }\n\n // Can this be replaced with canvas context?\n const {width, height, aspect} = this._getSizeAndAspect();\n if (width !== this.animationProps.width || height !== this.animationProps.height) {\n this.setNeedsRedraw('drawing buffer resized');\n }\n if (aspect !== this.animationProps.aspect) {\n this.setNeedsRedraw('drawing buffer aspect changed');\n }\n\n this.animationProps.width = width;\n this.animationProps.height = height;\n this.animationProps.aspect = aspect;\n\n this.animationProps.needsRedraw = this._needsRedraw;\n\n // Update time properties\n this.animationProps.engineTime = Date.now() - this.animationProps.startTime;\n\n if (this.timeline) {\n this.timeline.update(this.animationProps.engineTime);\n }\n\n this.animationProps.tick = Math.floor((this.animationProps.time / 1000) * 60);\n this.animationProps.tock++;\n\n // For back compatibility\n this.animationProps.time = this.timeline\n ? this.timeline.getTime()\n : this.animationProps.engineTime;\n }\n\n /** Wait for supplied device */\n async _initDevice() {\n this.device = await this.props.device;\n if (!this.device) {\n throw new Error('No device provided');\n }\n this.canvas = this.device.getDefaultCanvasContext().canvas || null;\n // this._createInfoDiv();\n }\n\n _createInfoDiv(): void {\n if (this.canvas && this.props.onAddHTML) {\n const wrapperDiv = document.createElement('div');\n document.body.appendChild(wrapperDiv);\n wrapperDiv.style.position = 'relative';\n const div = document.createElement('div');\n div.style.position = 'absolute';\n div.style.left = '10px';\n div.style.bottom = '10px';\n div.style.width = '300px';\n div.style.background = 'white';\n if (this.canvas instanceof HTMLCanvasElement) {\n wrapperDiv.appendChild(this.canvas);\n }\n wrapperDiv.appendChild(div);\n const html = this.props.onAddHTML(div);\n if (html) {\n div.innerHTML = html;\n }\n }\n }\n\n _getSizeAndAspect(): {width: number; height: number; aspect: number} {\n if (!this.device) {\n return {width: 1, height: 1, aspect: 1};\n }\n // Match projection setup to the actual render target dimensions, which may\n // differ from the CSS size when device-pixel scaling or backend clamping applies.\n const [width, height] = this.device.getDefaultCanvasContext().getDrawingBufferSize();\n const aspect = width > 0 && height > 0 ? width / height : 1;\n\n return {width, height, aspect};\n }\n\n /** @deprecated Default viewport setup */\n _resizeViewport(): void {\n // TODO can we use canvas context to code this in a portable way?\n // @ts-expect-error Expose on canvasContext\n if (this.props.autoResizeViewport && this.device.gl) {\n // @ts-expect-error Expose canvasContext\n this.device.gl.viewport(\n 0,\n 0,\n // @ts-expect-error Expose canvasContext\n this.device.gl.drawingBufferWidth,\n // @ts-expect-error Expose canvasContext\n this.device.gl.drawingBufferHeight\n );\n }\n }\n\n _beginFrameTimers(time?: number) {\n const now = time ?? (typeof performance !== 'undefined' ? performance.now() : Date.now());\n if (this._lastFrameTime) {\n const frameTime = now - this._lastFrameTime;\n if (frameTime > 0) {\n this.frameRate.addTime(frameTime);\n }\n }\n this._lastFrameTime = now;\n\n if (this.device?._isDebugGPUTimeEnabled()) {\n this._consumeEncodedGpuTime();\n }\n\n this.cpuTime.timeStart();\n }\n\n _endFrameTimers() {\n if (this.device?._isDebugGPUTimeEnabled()) {\n this._consumeEncodedGpuTime();\n }\n\n this.cpuTime.timeEnd();\n this._updateSharedStats();\n }\n\n _consumeEncodedGpuTime(): void {\n if (!this.device) {\n return;\n }\n\n const gpuTimeMs = this.device.commandEncoder._gpuTimeMs;\n if (gpuTimeMs !== undefined) {\n this.gpuTime.addTime(gpuTimeMs);\n this.device.commandEncoder._gpuTimeMs = undefined;\n }\n }\n\n _updateSharedStats(): void {\n if (this.stats === this.sharedStats) {\n return;\n }\n\n for (const name of Object.keys(this.sharedStats.stats)) {\n if (!this.stats.stats[name]) {\n delete this.sharedStats.stats[name];\n }\n }\n\n this.stats.forEach(sourceStat => {\n const targetStat = this.sharedStats.get(sourceStat.name, sourceStat.type);\n targetStat.sampleSize = sourceStat.sampleSize;\n targetStat.time = sourceStat.time;\n targetStat.count = sourceStat.count;\n targetStat.samples = sourceStat.samples;\n targetStat.lastTiming = sourceStat.lastTiming;\n targetStat.lastSampleTime = sourceStat.lastSampleTime;\n targetStat.lastSampleCount = sourceStat.lastSampleCount;\n targetStat._count = sourceStat._count;\n targetStat._time = sourceStat._time;\n targetStat._samples = sourceStat._samples;\n targetStat._startTime = sourceStat._startTime;\n targetStat._timerPending = sourceStat._timerPending;\n });\n }\n\n // Event handling\n\n _startEventHandling() {\n if (this.canvas) {\n this.canvas.addEventListener('mousemove', this._onMousemove.bind(this));\n this.canvas.addEventListener('mouseleave', this._onMouseleave.bind(this));\n }\n }\n\n _onMousemove(event: Event) {\n if (event instanceof MouseEvent) {\n this._getAnimationProps()._mousePosition = [event.offsetX, event.offsetY];\n }\n }\n\n _onMouseleave(event: Event) {\n this._getAnimationProps()._mousePosition = null;\n }\n}\n", "// luma.gl\n// SPDX-License-Identifier: MIT\n// Copyright (c) vis.gl contributors\n\n/* global window, setTimeout, clearTimeout */\n\n/** Node.js polyfill for requestAnimationFrame */\n// / <reference types=\"@types/node\" />\nexport function requestAnimationFramePolyfill(callback: (time?: any) => void): any {\n const browserRequestAnimationFrame =\n typeof window !== 'undefined'\n ? window.requestAnimationFrame ||\n (window as Window & {webkitRequestAnimationFrame?: (cb: FrameRequestCallback) => number})\n .webkitRequestAnimationFrame ||\n (window as Window & {mozRequestAnimationFrame?: (cb: FrameRequestCallback) => number})\n .mozRequestAnimationFrame\n : null;\n\n if (browserRequestAnimationFrame) {\n return browserRequestAnimationFrame.call(window, callback);\n }\n\n return setTimeout(\n () => callback(typeof performance !== 'undefined' ? performance.now() : Date.now()),\n 1000 / 60\n );\n}\n\n/** Node.js polyfill for cancelAnimationFrame */\nexport function cancelAnimationFramePolyfill(timerId: any): void {\n const browserCancelAnimationFrame =\n typeof window !== 'undefined'\n ? window.cancelAnimationFrame ||\n (window as Window & {webkitCancelAnimationFrame?: (handle: number) => void})\n .webkitCancelAnimationFrame ||\n (window as Window & {mozCancelAnimationFrame?: (handle: number) => void})\n .mozCancelAnimationFrame\n : null;\n\n if (browserCancelAnimationFrame) {\n browserCancelAnimationFrame.call(window, timerId);\n return;\n }\n\n clearTimeout(timerId);\n}\n", "// luma.gl\n// SPDX-License-Identifier: MIT\n// Copyright (c) vis.gl contributors\n\nimport {luma, Adapter, Device} from '@luma.gl/core';\nimport {AnimationLoopTemplate} from './animation-loop-template';\nimport {AnimationLoop, AnimationLoopProps} from './animation-loop';\nimport type {AnimationProps} from './animation-props';\n\nexport type MakeAnimationLoopProps = Omit<\n AnimationLoopProps,\n 'onCreateDevice' | 'onInitialize' | 'onRedraw' | 'onFinalize'\n> & {\n /** List of adapters to use when creating the device */\n adapters?: Adapter[];\n};\n\n/**\n * Instantiates an animation loop and initializes it with the template.\n * @note The application needs to call `start()` on the returned animation loop to start the rendering loop.\n */\nexport function makeAnimationLoop(\n AnimationLoopTemplateCtor: typeof AnimationLoopTemplate,\n props?: MakeAnimationLoopProps\n): AnimationLoop {\n let renderLoop: AnimationLoopTemplate | null = null;\n\n const device =\n props?.device ||\n luma.createDevice({id: 'animation-loop', adapters: props?.adapters, createCanvasContext: true});\n\n // Create an animation loop;\n const animationLoop = new AnimationLoop({\n ...props,\n\n device,\n\n async onInitialize(animationProps: AnimationProps): Promise<unknown> {\n clearError(animationProps.animationLoop.device);\n try {\n // @ts-expect-error abstract to prevent instantiation\n renderLoop = new AnimationLoopTemplateCtor(animationProps);\n // Any async loading can be handled here\n return await renderLoop?.onInitialize(animationProps);\n } catch (error) {\n console.error(error);\n setError(animationProps.animationLoop.device, error as Error);\n return null;\n }\n },\n\n onRender: (animationProps: AnimationProps) => renderLoop?.onRender(animationProps),\n\n onFinalize: (animationProps: AnimationProps) => renderLoop?.onFinalize(animationProps)\n });\n\n // @ts-expect-error Hack: adds info for the website to find\n animationLoop.getInfo = () => {\n // @ts-ignore\n // eslint-disable-next-line no-invalid-this\n return this.AnimationLoopTemplateCtor.info;\n };\n\n return animationLoop;\n}\n\nfunction setError(device: Device | null, error: Error): void {\n if (!device) {\n return;\n }\n\n const canvas = device.getDefaultCanvasContext().canvas;\n if (canvas instanceof HTMLCanvasElement) {\n canvas.style.overflow = 'visible';\n let errorDiv = document.getElementById('animation-loop-error');\n errorDiv?.remove();\n errorDiv = document.createElement('h1');\n errorDiv.id = 'animation-loop-error';\n errorDiv.innerHTML = error.message;\n errorDiv.style.position = 'absolute';\n errorDiv.style.top = '10px'; // left: 50%; transform: translate(-50%, -50%);';\n errorDiv.style.left = '10px';\n errorDiv.style.color = 'black';\n errorDiv.style.backgroundColor = 'red';\n canvas.parentElement?.appendChild(errorDiv);\n // canvas.style.position = 'absolute';\n }\n}\n\nfunction clearError(device: Device | null): void {\n if (!device) {\n return;\n }\n\n const errorDiv = document.getElementById('animation-loop-error');\n if (errorDiv) {\n errorDiv.remove();\n }\n}\n", "// luma.gl\n// SPDX-License-Identifier: MIT\n// Copyright (c) vis.gl contributors\n\n// A lot of imports, but then Model is where it all comes together...\nimport {type TypedArray} from '@math.gl/types';\nimport {\n type RenderPipelineProps,\n type RenderPipelineParameters,\n type BufferLayout,\n type Shader,\n type VertexArray,\n type TransformFeedback,\n type AttributeInfo,\n type Binding,\n type BindingsByGroup,\n type PrimitiveTopology,\n Device,\n DeviceFeature,\n Buffer,\n Texture,\n TextureView,\n Sampler,\n RenderPipeline,\n RenderPass,\n PipelineFactory,\n ShaderFactory,\n UniformStore,\n log,\n dataTypeDecoder,\n getAttributeInfosFromLayouts,\n normalizeBindingsByGroup\n} from '@luma.gl/core';\n\nimport type {ShaderBindingDebugRow, ShaderModule, PlatformInfo} from '@luma.gl/shadertools';\nimport {ShaderAssembler} from '@luma.gl/shadertools';\n\nimport type {Geometry} from '../geometry/geometry';\nimport {GPUGeometry, makeGPUGeometry} from '../geometry/gpu-geometry';\nimport {getDebugTableForShaderLayout} from '../debug/debug-shader-layout';\nimport {debugFramebuffer} from '../debug/debug-framebuffer';\nimport {deepEqual} from '../utils/deep-equal';\nimport {BufferLayoutHelper} from '../utils/buffer-layout-helper';\nimport {sortedBufferLayoutByShaderSourceLocations} from '../utils/buffer-layout-order';\nimport {\n mergeShaderModuleBindingsIntoLayout,\n shaderModuleHasUniforms\n} from '../utils/shader-module-utils';\nimport {uid} from '../utils/uid';\nimport {ShaderInputs} from '../shader-inputs';\nimport {DynamicTexture} from '../dynamic-texture/dynamic-texture';\nimport {Material} from '../material/material';\n\nconst LOG_DRAW_PRIORITY = 2;\nconst LOG_DRAW_TIMEOUT = 10000;\nconst PIPELINE_INITIALIZATION_FAILED = 'render pipeline initialization failed';\n\nexport type ModelProps = Omit<RenderPipelineProps, 'vs' | 'fs' | 'bindings'> & {\n source?: string;\n vs?: string | null;\n fs?: string | null;\n\n /** shadertool shader modules (added to shader code) */\n modules?: ShaderModule[];\n /** Shadertool module defines (configures shader code)*/\n defines?: Record<string, boolean>;\n // TODO - injections, hooks etc?\n\n /** Shader inputs, used to generated uniform buffers and bindings */\n shaderInputs?: ShaderInputs;\n /** Material-owned group-3 bindings */\n material?: Material;\n /** Bindings */\n bindings?: Record<string, Binding | DynamicTexture>;\n /** WebGL-only uniforms */\n uniforms?: Record<string, unknown>;\n /** Parameters that are built into the pipeline */\n parameters?: RenderPipelineParameters;\n\n /** Geometry */\n geometry?: GPUGeometry | Geometry | null;\n\n /** @deprecated Use instanced rendering? Will be auto-detected in 9.1 */\n isInstanced?: boolean;\n /** instance count */\n instanceCount?: number;\n /** Vertex count */\n vertexCount?: number;\n\n indexBuffer?: Buffer | null;\n /** @note this is really a map of buffers, not a map of attributes */\n attributes?: Record<string, Buffer>;\n /** */\n constantAttributes?: Record<string, TypedArray>;\n\n /** Some applications intentionally supply unused attributes and bindings, and want to disable warnings */\n disableWarnings?: boolean;\n\n /** @internal For use with {@link TransformFeedback}, WebGL only. */\n varyings?: string[];\n\n transformFeedback?: TransformFeedback;\n\n /** Show shader source in browser? */\n debugShaders?: 'never' | 'errors' | 'warnings' | 'always';\n\n /** Factory used to create a {@link RenderPipeline}. Defaults to {@link Device} default factory. */\n pipelineFactory?: PipelineFactory;\n /** Factory used to create a {@link Shader}. Defaults to {@link Device} default factory. */\n shaderFactory?: ShaderFactory;\n /** Shader assembler. Defaults to the ShaderAssembler.getShaderAssembler() */\n shaderAssembler?: ShaderAssembler;\n};\n\n/**\n * High level draw API for luma.gl.\n *\n * A `Model` encapsulates shaders, geometry attributes, bindings and render\n * pipeline state into a single object. It automatically reuses and rebuilds\n * pipelines as render parameters change and exposes convenient hooks for\n * updating uniforms and attributes.\n *\n * Features:\n * - Reuses and lazily recompiles {@link RenderPipeline | pipelines} as needed.\n * - Integrates with `@luma.gl/shadertools` to assemble GLSL or WGSL from shader modules.\n * - Manages geometry attributes and buffer bindings.\n * - Accepts textures, samplers and uniform buffers as bindings, including `DynamicTexture`.\n * - Provides detailed debug logging and optional shader source inspection.\n */\nexport class Model {\n static defaultProps: Required<ModelProps> = {\n ...RenderPipeline.defaultProps,\n source: undefined!,\n vs: null,\n fs: null,\n id: 'unnamed',\n handle: undefined,\n userData: {},\n defines: {},\n modules: [],\n geometry: null,\n indexBuffer: null,\n attributes: {},\n constantAttributes: {},\n bindings: {},\n uniforms: {},\n varyings: [],\n\n isInstanced: undefined!,\n instanceCount: 0,\n vertexCount: 0,\n\n shaderInputs: undefined!,\n material: undefined!,\n pipelineFactory: undefined!,\n shaderFactory: undefined!,\n transformFeedback: undefined!,\n shaderAssembler: ShaderAssembler.getDefaultShaderAssembler(),\n\n debugShaders: undefined!,\n disableWarnings: undefined!\n };\n\n /** Device that created this model */\n readonly device: Device;\n /** Application provided identifier */\n readonly id: string;\n /** WGSL shader source when using unified shader */\n // @ts-expect-error assigned in function called from constructor\n readonly source: string;\n /** GLSL vertex shader source */\n // @ts-expect-error assigned in function called from constructor\n readonly vs: string;\n /** GLSL fragment shader source */\n // @ts-expect-error assigned in function called from constructor\n readonly fs: string;\n /** Factory used to create render pipelines */\n readonly pipelineFactory: PipelineFactory;\n /** Factory used to create shaders */\n readonly shaderFactory: ShaderFactory;\n /** User-supplied per-model data */\n userData: {[key: string]: any} = {};\n\n // Fixed properties (change can trigger pipeline rebuild)\n\n /** The render pipeline GPU parameters, depth testing etc */\n parameters: RenderPipelineParameters;\n\n /** The primitive topology */\n topology: PrimitiveTopology;\n /** Buffer layout */\n bufferLayout: BufferLayout[];\n\n // Dynamic properties\n\n /** Use instanced rendering */\n isInstanced: boolean | undefined = undefined;\n /** instance count. `undefined` means not instanced */\n instanceCount: number = 0;\n /** Vertex count */\n vertexCount: number;\n\n /** Index buffer */\n indexBuffer: Buffer | null = null;\n /** Buffer-valued attributes */\n bufferAttributes: Record<string, Buffer> = {};\n /** Constant-valued attributes */\n constantAttributes: Record<string, TypedArray> = {};\n /** Bindings (textures, samplers, uniform buffers) */\n bindings: Record<string, Binding | DynamicTexture> = {};\n\n /**\n * VertexArray\n * @note not implemented: if bufferLayout is updated, vertex array has to be rebuilt!\n * @todo - allow application to define multiple vertex arrays?\n * */\n vertexArray: VertexArray;\n\n /** TransformFeedback, WebGL 2 only. */\n transformFeedback: TransformFeedback | null = null;\n\n /** The underlying GPU \"program\". @note May be recreated if parameters change */\n pipeline: RenderPipeline;\n\n /** ShaderInputs instance */\n // @ts-expect-error Assigned in function called by constructor\n shaderInputs: ShaderInputs;\n material: Material | null = null;\n // @ts-expect-error Assigned in function called by constructor\n _uniformStore: UniformStore;\n\n _attributeInfos: Record<string, AttributeInfo> = {};\n _gpuGeometry: GPUGeometry | null = null;\n private props: Required<ModelProps>;\n\n _pipelineNeedsUpdate: string | false = 'newly created';\n private _needsRedraw: string | false = 'initializing';\n private _destroyed = false;\n\n /** \"Time\" of last draw. Monotonically increasing timestamp */\n _lastDrawTimestamp: number = -1;\n private _bindingTable: ShaderBindingDebugRow[] = [];\n\n get [Symbol.toStringTag](): string {\n return 'Model';\n }\n\n toString(): string {\n return `Model(${this.id})`;\n }\n\n constructor(device: Device, props: ModelProps) {\n this.props = {...Model.defaultProps, ...props};\n props = this.props;\n this.id = props.id || uid('model');\n this.device = device;\n\n Object.assign(this.userData, props.userData);\n\n this.material = props.material || null;\n\n // Setup shader module inputs\n const moduleMap = Object.fromEntries(\n this.props.modules?.map(module => [module.name, module]) || []\n );\n\n const shaderInputs =\n props.shaderInputs ||\n new ShaderInputs(moduleMap, {disableWarnings: this.props.disableWarnings});\n // @ts-ignore\n this.setShaderInputs(shaderInputs);\n\n // Setup shader assembler\n const platformInfo = getPlatformInfo(device);\n\n // Extract modules from shader inputs if not supplied\n const modules =\n // @ts-ignore shaderInputs is assigned in setShaderInputs above.\n (this.props.modules?.length > 0 ? this.props.modules : this.shaderInputs?.getModules()) || [];\n\n this.props.shaderLayout =\n mergeShaderModuleBindingsIntoLayout(this.props.shaderLayout, modules) || null;\n\n const isWebGPU = this.device.type === 'webgpu';\n\n // WebGPU\n // TODO - hack to support unified WGSL shader\n // TODO - this is wrong, compile a single shader\n if (isWebGPU && this.props.source) {\n // WGSL\n const {source, getUniforms, bindingTable} = this.props.shaderAssembler.assembleWGSLShader({\n platformInfo,\n ...this.props,\n modules\n });\n this.source = source;\n // @ts-expect-error\n this._getModuleUniforms = getUniforms;\n this._bindingTable = bindingTable;\n // Extract shader layout after modules have been added to WGSL source, to include any bindings added by modules\n const inferredShaderLayout = (\n device as Device & {getShaderLayout?: (source: string) => any}\n ).getShaderLayout?.(this.source);\n this.props.shaderLayout =\n mergeShaderModuleBindingsIntoLayout(\n this.props.shaderLayout || inferredShaderLayout || null,\n modules\n ) || null;\n } else {\n // GLSL\n const {vs, fs, getUniforms} = this.props.shaderAssembler.assembleGLSLShaderPair({\n platformInfo,\n ...this.props,\n modules\n });\n\n this.vs = vs;\n this.fs = fs;\n // @ts-expect-error\n this._getModuleUniforms = getUniforms;\n this._bindingTable = [];\n }\n\n this.vertexCount = this.props.vertexCount;\n this.instanceCount = this.props.instanceCount;\n\n this.topology = this.props.topology;\n this.bufferLayout = this.props.bufferLayout;\n this.parameters = this.props.parameters;\n\n // Geometry, if provided, sets topology and vertex cound\n if (props.geometry) {\n this.setGeometry(props.geometry);\n }\n\n this.pipelineFactory =\n props.pipelineFactory || PipelineFactory.getDefaultPipelineFactory(this.device);\n this.shaderFactory = props.shaderFactory || ShaderFactory.getDefaultShaderFactory(this.device);\n\n // Create the pipeline\n // @note order is important\n this.pipeline = this._updatePipeline();\n\n this.vertexArray = device.createVertexArray({\n shaderLayout: this.pipeline.shaderLayout,\n bufferLayout: this.pipeline.bufferLayout\n });\n\n // Now we can apply geometry attributes\n if (this._gpuGeometry) {\n this._setGeometryAttributes(this._gpuGeometry);\n }\n\n // Apply any dynamic settings that will not trigger pipeline change\n if ('isInstanced' in props) {\n this.isInstanced = props.isInstanced;\n }\n\n if (props.instanceCount) {\n this.setInstanceCount(props.instanceCount);\n }\n if (props.vertexCount) {\n this.setVertexCount(props.vertexCount);\n }\n if (props.indexBuffer) {\n this.setIndexBuffer(props.indexBuffer);\n }\n if (props.attributes) {\n this.setAttributes(props.attributes);\n }\n if (props.constantAttributes) {\n this.setConstantAttributes(props.constantAttributes);\n }\n if (props.bindings) {\n this.setBindings(props.bindings);\n }\n if (props.transformFeedback) {\n this.transformFeedback = props.transformFeedback;\n }\n }\n\n destroy(): void {\n if (!this._destroyed) {\n // Release pipeline before we destroy the shaders used by the pipeline\n this.pipelineFactory.release(this.pipeline);\n // Release the shaders\n this.shaderFactory.release(this.pipeline.vs);\n if (this.pipeline.fs && this.pipeline.fs !== this.pipeline.vs) {\n this.shaderFactory.release(this.pipeline.fs);\n }\n this._uniformStore.destroy();\n // TODO - mark resource as managed and destroyIfManaged() ?\n this._gpuGeometry?.destroy();\n this._destroyed = true;\n }\n }\n\n // Draw call\n\n /** Query redraw status. Clears the status. */\n needsRedraw(): false | string {\n // Catch any writes to already bound resources\n if (this._getBindingsUpdateTimestamp() > this._lastDrawTimestamp) {\n this.setNeedsRedraw('contents of bound textures or buffers updated');\n }\n const needsRedraw = this._needsRedraw;\n thi