@luma.gl/engine
Version:
3D Engine Components for luma.gl
1,530 lines (1,503 loc) • 274 kB
JavaScript
(function webpackUniversalModuleDefinition(root, factory) {
if (typeof exports === 'object' && typeof module === 'object')
module.exports = factory();
else if (typeof define === 'function' && define.amd) define([], factory);
else if (typeof exports === 'object') exports['luma'] = factory();
else root['luma'] = factory();})(globalThis, function () {
"use strict";
var __exports__ = (() => {
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __commonJS = (cb, mod) => function __require() {
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
};
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __reExport = (target, mod, secondTarget) => (__copyProps(target, mod, "default"), secondTarget && __copyProps(secondTarget, mod, "default"));
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var __publicField = (obj, key, value) => {
__defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
return value;
};
// external-global-plugin:@luma.gl/core
var require_core = __commonJS({
"external-global-plugin:@luma.gl/core"(exports, module) {
module.exports = globalThis.luma;
}
});
// external-global-plugin:@luma.gl/shadertools
var require_shadertools = __commonJS({
"external-global-plugin:@luma.gl/shadertools"(exports, module) {
module.exports = globalThis.luma;
}
});
// bundle.ts
var bundle_exports = {};
__export(bundle_exports, {
AnimationLoop: () => AnimationLoop,
AnimationLoopTemplate: () => AnimationLoopTemplate,
AsyncTexture: () => AsyncTexture,
BackgroundTextureModel: () => BackgroundTextureModel,
BufferTransform: () => BufferTransform,
ClipSpace: () => ClipSpace,
Computation: () => Computation,
ConeGeometry: () => ConeGeometry,
CubeGeometry: () => CubeGeometry,
CylinderGeometry: () => CylinderGeometry,
DirectionalLightModel: () => DirectionalLightModel,
DynamicTexture: () => DynamicTexture,
GPUGeometry: () => GPUGeometry,
Geometry: () => Geometry,
GroupNode: () => GroupNode,
IcoSphereGeometry: () => IcoSphereGeometry,
KeyFrames: () => KeyFrames,
LegacyPickingManager: () => LegacyPickingManager,
Material: () => Material,
MaterialFactory: () => MaterialFactory,
Model: () => Model,
ModelNode: () => ModelNode,
PickingManager: () => PickingManager,
PlaneGeometry: () => PlaneGeometry,
PointLightModel: () => PointLightModel,
ScenegraphNode: () => ScenegraphNode,
ShaderInputs: () => ShaderInputs,
ShaderPassRenderer: () => ShaderPassRenderer,
SphereGeometry: () => SphereGeometry,
SpotLightModel: () => SpotLightModel,
Swap: () => Swap,
SwapBuffers: () => SwapBuffers,
SwapFramebuffers: () => SwapFramebuffers,
TextureTransform: () => TextureTransform,
Timeline: () => Timeline,
TruncatedConeGeometry: () => TruncatedConeGeometry,
cancelAnimationFramePolyfill: () => cancelAnimationFramePolyfill,
colorPicking: () => picking,
indexPicking: () => picking2,
legacyColorPicking: () => legacyColorPicking,
loadImage: () => loadImage,
loadImageBitmap: () => loadImageBitmap,
makeAnimationLoop: () => makeAnimationLoop,
makeRandomGenerator: () => makeRandomGenerator,
picking: () => picking3,
requestAnimationFramePolyfill: () => requestAnimationFramePolyfill,
resolvePickingBackend: () => resolvePickingBackend,
resolvePickingMode: () => resolvePickingMode,
setPathPrefix: () => setPathPrefix,
supportsIndexPicking: () => supportsIndexPicking
});
__reExport(bundle_exports, __toESM(require_core(), 1));
// src/animation/timeline.ts
var channelHandles = 1;
var animationHandles = 1;
var Timeline = class {
time = 0;
channels = /* @__PURE__ */ new Map();
animations = /* @__PURE__ */ new Map();
playing = false;
lastEngineTime = -1;
constructor() {
}
addChannel(props) {
const { delay = 0, duration = Number.POSITIVE_INFINITY, rate = 1, repeat = 1 } = props;
const channelId = channelHandles++;
const channel = {
time: 0,
delay,
duration,
rate,
repeat
};
this._setChannelTime(channel, this.time);
this.channels.set(channelId, channel);
return channelId;
}
removeChannel(channelId) {
this.channels.delete(channelId);
for (const [animationHandle, animation] of this.animations) {
if (animation.channel === channelId) {
this.detachAnimation(animationHandle);
}
}
}
isFinished(channelId) {
const channel = this.channels.get(channelId);
if (channel === void 0) {
return false;
}
return this.time >= channel.delay + channel.duration * channel.repeat;
}
getTime(channelId) {
if (channelId === void 0) {
return this.time;
}
const channel = this.channels.get(channelId);
if (channel === void 0) {
return -1;
}
return channel.time;
}
setTime(time) {
this.time = Math.max(0, time);
const channels = this.channels.values();
for (const channel of channels) {
this._setChannelTime(channel, this.time);
}
const animations = this.animations.values();
for (const animationData of animations) {
const { animation, channel } = animationData;
animation.setTime(this.getTime(channel));
}
}
play() {
this.playing = true;
}
pause() {
this.playing = false;
this.lastEngineTime = -1;
}
reset() {
this.setTime(0);
}
attachAnimation(animation, channelHandle) {
const animationHandle = animationHandles++;
this.animations.set(animationHandle, {
animation,
channel: channelHandle
});
animation.setTime(this.getTime(channelHandle));
return animationHandle;
}
detachAnimation(channelId) {
this.animations.delete(channelId);
}
update(engineTime) {
if (this.playing) {
if (this.lastEngineTime === -1) {
this.lastEngineTime = engineTime;
}
this.setTime(this.time + (engineTime - this.lastEngineTime));
this.lastEngineTime = engineTime;
}
}
_setChannelTime(channel, time) {
const offsetTime = time - channel.delay;
const totalDuration = channel.duration * channel.repeat;
if (offsetTime >= totalDuration) {
channel.time = channel.duration * channel.rate;
} else {
channel.time = Math.max(0, offsetTime) % channel.duration;
channel.time *= channel.rate;
}
}
};
// src/animation/key-frames.ts
var KeyFrames = class {
startIndex = -1;
endIndex = -1;
factor = 0;
times = [];
values = [];
_lastTime = -1;
constructor(keyFrames) {
this.setKeyFrames(keyFrames);
this.setTime(0);
}
setKeyFrames(keyFrames) {
const numKeys = keyFrames.length;
this.times.length = numKeys;
this.values.length = numKeys;
for (let i = 0; i < numKeys; ++i) {
this.times[i] = keyFrames[i][0];
this.values[i] = keyFrames[i][1];
}
this._calculateKeys(this._lastTime);
}
setTime(time) {
time = Math.max(0, time);
if (time !== this._lastTime) {
this._calculateKeys(time);
this._lastTime = time;
}
}
getStartTime() {
return this.times[this.startIndex];
}
getEndTime() {
return this.times[this.endIndex];
}
getStartData() {
return this.values[this.startIndex];
}
getEndData() {
return this.values[this.endIndex];
}
_calculateKeys(time) {
let index = 0;
const numKeys = this.times.length;
for (index = 0; index < numKeys - 2; ++index) {
if (this.times[index + 1] > time) {
break;
}
}
this.startIndex = index;
this.endIndex = index + 1;
const startTime = this.times[this.startIndex];
const endTime = this.times[this.endIndex];
this.factor = Math.min(Math.max(0, (time - startTime) / (endTime - startTime)), 1);
}
};
// src/animation-loop/animation-loop-template.ts
var AnimationLoopTemplate = class {
constructor(animationProps) {
}
async onInitialize(animationProps) {
return null;
}
};
// src/animation-loop/animation-loop.ts
var import_core = __toESM(require_core(), 1);
// src/animation-loop/request-animation-frame.ts
function requestAnimationFramePolyfill(callback) {
const browserRequestAnimationFrame = typeof window !== "undefined" ? window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame : null;
if (browserRequestAnimationFrame) {
return browserRequestAnimationFrame.call(window, callback);
}
return setTimeout(
() => callback(typeof performance !== "undefined" ? performance.now() : Date.now()),
1e3 / 60
);
}
function cancelAnimationFramePolyfill(timerId) {
const browserCancelAnimationFrame = typeof window !== "undefined" ? window.cancelAnimationFrame || window.webkitCancelAnimationFrame || window.mozCancelAnimationFrame : null;
if (browserCancelAnimationFrame) {
browserCancelAnimationFrame.call(window, timerId);
return;
}
clearTimeout(timerId);
}
// ../../node_modules/@probe.gl/stats/dist/utils/hi-res-timestamp.js
function getHiResTimestamp() {
let timestamp;
if (typeof window !== "undefined" && window.performance) {
timestamp = window.performance.now();
} else if (typeof process !== "undefined" && process.hrtime) {
const timeParts = process.hrtime();
timestamp = timeParts[0] * 1e3 + timeParts[1] / 1e6;
} else {
timestamp = Date.now();
}
return timestamp;
}
// ../../node_modules/@probe.gl/stats/dist/lib/stat.js
var Stat = class {
constructor(name, type) {
this.sampleSize = 1;
this.time = 0;
this.count = 0;
this.samples = 0;
this.lastTiming = 0;
this.lastSampleTime = 0;
this.lastSampleCount = 0;
this._count = 0;
this._time = 0;
this._samples = 0;
this._startTime = 0;
this._timerPending = false;
this.name = name;
this.type = type;
this.reset();
}
reset() {
this.time = 0;
this.count = 0;
this.samples = 0;
this.lastTiming = 0;
this.lastSampleTime = 0;
this.lastSampleCount = 0;
this._count = 0;
this._time = 0;
this._samples = 0;
this._startTime = 0;
this._timerPending = false;
return this;
}
setSampleSize(samples) {
this.sampleSize = samples;
return this;
}
/** Call to increment count (+1) */
incrementCount() {
this.addCount(1);
return this;
}
/** Call to decrement count (-1) */
decrementCount() {
this.subtractCount(1);
return this;
}
/** Increase count */
addCount(value) {
this._count += value;
this._samples++;
this._checkSampling();
return this;
}
/** Decrease count */
subtractCount(value) {
this._count -= value;
this._samples++;
this._checkSampling();
return this;
}
/** Add an arbitrary timing and bump the count */
addTime(time) {
this._time += time;
this.lastTiming = time;
this._samples++;
this._checkSampling();
return this;
}
/** Start a timer */
timeStart() {
this._startTime = getHiResTimestamp();
this._timerPending = true;
return this;
}
/** End a timer. Adds to time and bumps the timing count. */
timeEnd() {
if (!this._timerPending) {
return this;
}
this.addTime(getHiResTimestamp() - this._startTime);
this._timerPending = false;
this._checkSampling();
return this;
}
getSampleAverageCount() {
return this.sampleSize > 0 ? this.lastSampleCount / this.sampleSize : 0;
}
/** Calculate average time / count for the previous window */
getSampleAverageTime() {
return this.sampleSize > 0 ? this.lastSampleTime / this.sampleSize : 0;
}
/** Calculate counts per second for the previous window */
getSampleHz() {
return this.lastSampleTime > 0 ? this.sampleSize / (this.lastSampleTime / 1e3) : 0;
}
getAverageCount() {
return this.samples > 0 ? this.count / this.samples : 0;
}
/** Calculate average time / count */
getAverageTime() {
return this.samples > 0 ? this.time / this.samples : 0;
}
/** Calculate counts per second */
getHz() {
return this.time > 0 ? this.samples / (this.time / 1e3) : 0;
}
_checkSampling() {
if (this._samples === this.sampleSize) {
this.lastSampleTime = this._time;
this.lastSampleCount = this._count;
this.count += this._count;
this.time += this._time;
this.samples += this._samples;
this._time = 0;
this._count = 0;
this._samples = 0;
}
}
};
// ../../node_modules/@probe.gl/stats/dist/lib/stats.js
var Stats = class {
constructor(options) {
this.stats = {};
this.id = options.id;
this.stats = {};
this._initializeStats(options.stats);
Object.seal(this);
}
/** Acquire a stat. Create if it doesn't exist. */
get(name, type = "count") {
return this._getOrCreate({ name, type });
}
get size() {
return Object.keys(this.stats).length;
}
/** Reset all stats */
reset() {
for (const stat of Object.values(this.stats)) {
stat.reset();
}
return this;
}
forEach(fn) {
for (const stat of Object.values(this.stats)) {
fn(stat);
}
}
getTable() {
const table = {};
this.forEach((stat) => {
table[stat.name] = {
time: stat.time || 0,
count: stat.count || 0,
average: stat.getAverageTime() || 0,
hz: stat.getHz() || 0
};
});
return table;
}
_initializeStats(stats = []) {
stats.forEach((stat) => this._getOrCreate(stat));
}
_getOrCreate(stat) {
const { name, type } = stat;
let result = this.stats[name];
if (!result) {
if (stat instanceof Stat) {
result = stat;
} else {
result = new Stat(name, type);
}
this.stats[name] = result;
}
return result;
}
};
// src/animation-loop/animation-loop.ts
var statIdCounter = 0;
var ANIMATION_LOOP_STATS = "Animation Loop";
var _AnimationLoop = class {
device = null;
canvas = null;
props;
animationProps = null;
timeline = null;
stats;
sharedStats;
cpuTime;
gpuTime;
frameRate;
display;
_needsRedraw = "initialized";
_initialized = false;
_running = false;
_animationFrameId = null;
_nextFramePromise = null;
_resolveNextFrame = null;
_cpuStartTime = 0;
_error = null;
_lastFrameTime = 0;
/*
* @param {HTMLCanvasElement} canvas - if provided, width and height will be passed to context
*/
constructor(props) {
this.props = { ..._AnimationLoop.defaultAnimationLoopProps, ...props };
props = this.props;
if (!props.device) {
throw new Error("No device provided");
}
this.stats = props.stats || new Stats({ id: `animation-loop-${statIdCounter++}` });
this.sharedStats = import_core.luma.stats.get(ANIMATION_LOOP_STATS);
this.frameRate = this.stats.get("Frame Rate");
this.frameRate.setSampleSize(1);
this.cpuTime = this.stats.get("CPU Time");
this.gpuTime = this.stats.get("GPU Time");
this.setProps({ autoResizeViewport: props.autoResizeViewport });
this.start = this.start.bind(this);
this.stop = this.stop.bind(this);
this._onMousemove = this._onMousemove.bind(this);
this._onMouseleave = this._onMouseleave.bind(this);
}
destroy() {
this.stop();
this._setDisplay(null);
this.device?._disableDebugGPUTime();
}
/** @deprecated Use .destroy() */
delete() {
this.destroy();
}
reportError(error) {
this.props.onError(error);
this._error = error;
}
/** Flags this animation loop as needing redraw */
setNeedsRedraw(reason) {
this._needsRedraw = this._needsRedraw || reason;
return this;
}
/** Query redraw status. Clears the flag. */
needsRedraw() {
const reason = this._needsRedraw;
this._needsRedraw = false;
return reason;
}
setProps(props) {
if ("autoResizeViewport" in props) {
this.props.autoResizeViewport = props.autoResizeViewport || false;
}
return this;
}
/** Starts a render loop if not already running */
async start() {
if (this._running) {
return this;
}
this._running = true;
try {
let appContext;
if (!this._initialized) {
this._initialized = true;
await this._initDevice();
this._initialize();
if (!this._running) {
return null;
}
await this.props.onInitialize(this._getAnimationProps());
}
if (!this._running) {
return null;
}
if (appContext !== false) {
this._cancelAnimationFrame();
this._requestAnimationFrame();
}
return this;
} catch (err) {
const error = err instanceof Error ? err : new Error("Unknown error");
this.props.onError(error);
throw error;
}
}
/** Stops a render loop if already running, finalizing */
stop() {
if (this._running) {
if (this.animationProps && !this._error) {
this.props.onFinalize(this.animationProps);
}
this._cancelAnimationFrame();
this._nextFramePromise = null;
this._resolveNextFrame = null;
this._running = false;
this._lastFrameTime = 0;
}
return this;
}
/** Explicitly draw a frame */
redraw(time) {
if (this.device?.isLost || this._error) {
return this;
}
this._beginFrameTimers(time);
this._setupFrame();
this._updateAnimationProps();
this._renderFrame(this._getAnimationProps());
this._clearNeedsRedraw();
if (this._resolveNextFrame) {
this._resolveNextFrame(this);
this._nextFramePromise = null;
this._resolveNextFrame = null;
}
this._endFrameTimers();
return this;
}
/** Add a timeline, it will be automatically updated by the animation loop. */
attachTimeline(timeline) {
this.timeline = timeline;
return this.timeline;
}
/** Remove a timeline */
detachTimeline() {
this.timeline = null;
}
/** Wait until a render completes */
waitForRender() {
this.setNeedsRedraw("waitForRender");
if (!this._nextFramePromise) {
this._nextFramePromise = new Promise((resolve) => {
this._resolveNextFrame = resolve;
});
}
return this._nextFramePromise;
}
/** TODO - should use device.deviceContext */
async toDataURL() {
this.setNeedsRedraw("toDataURL");
await this.waitForRender();
if (this.canvas instanceof HTMLCanvasElement) {
return this.canvas.toDataURL();
}
throw new Error("OffscreenCanvas");
}
// PRIVATE METHODS
_initialize() {
this._startEventHandling();
this._initializeAnimationProps();
this._updateAnimationProps();
this._resizeViewport();
this.device?._enableDebugGPUTime();
}
_setDisplay(display) {
if (this.display) {
this.display.destroy();
this.display.animationLoop = null;
}
if (display) {
display.animationLoop = this;
}
this.display = display;
}
_requestAnimationFrame() {
if (!this._running) {
return;
}
this._animationFrameId = requestAnimationFramePolyfill(this._animationFrame.bind(this));
}
_cancelAnimationFrame() {
if (this._animationFrameId === null) {
return;
}
cancelAnimationFramePolyfill(this._animationFrameId);
this._animationFrameId = null;
}
_animationFrame(time) {
if (!this._running) {
return;
}
this.redraw(time);
this._requestAnimationFrame();
}
// Called on each frame, can be overridden to call onRender multiple times
// to support e.g. stereoscopic rendering
_renderFrame(animationProps) {
if (this.display) {
this.display._renderFrame(animationProps);
return;
}
this.props.onRender(this._getAnimationProps());
this.device?.submit();
}
_clearNeedsRedraw() {
this._needsRedraw = false;
}
_setupFrame() {
this._resizeViewport();
}
// Initialize the object that will be passed to app callbacks
_initializeAnimationProps() {
const canvasContext = this.device?.getDefaultCanvasContext();
if (!this.device || !canvasContext) {
throw new Error("loop");
}
const canvas = canvasContext?.canvas;
const useDevicePixels = canvasContext.props.useDevicePixels;
this.animationProps = {
animationLoop: this,
device: this.device,
canvasContext,
canvas,
// @ts-expect-error Deprecated
useDevicePixels,
timeline: this.timeline,
needsRedraw: false,
// Placeholders
width: 1,
height: 1,
aspect: 1,
// Animation props
time: 0,
startTime: Date.now(),
engineTime: 0,
tick: 0,
tock: 0,
// Experimental
_mousePosition: null
// Event props
};
}
_getAnimationProps() {
if (!this.animationProps) {
throw new Error("animationProps");
}
return this.animationProps;
}
// Update the context object that will be passed to app callbacks
_updateAnimationProps() {
if (!this.animationProps) {
return;
}
const { width, height, aspect } = this._getSizeAndAspect();
if (width !== this.animationProps.width || height !== this.animationProps.height) {
this.setNeedsRedraw("drawing buffer resized");
}
if (aspect !== this.animationProps.aspect) {
this.setNeedsRedraw("drawing buffer aspect changed");
}
this.animationProps.width = width;
this.animationProps.height = height;
this.animationProps.aspect = aspect;
this.animationProps.needsRedraw = this._needsRedraw;
this.animationProps.engineTime = Date.now() - this.animationProps.startTime;
if (this.timeline) {
this.timeline.update(this.animationProps.engineTime);
}
this.animationProps.tick = Math.floor(this.animationProps.time / 1e3 * 60);
this.animationProps.tock++;
this.animationProps.time = this.timeline ? this.timeline.getTime() : this.animationProps.engineTime;
}
/** Wait for supplied device */
async _initDevice() {
this.device = await this.props.device;
if (!this.device) {
throw new Error("No device provided");
}
this.canvas = this.device.getDefaultCanvasContext().canvas || null;
}
_createInfoDiv() {
if (this.canvas && this.props.onAddHTML) {
const wrapperDiv = document.createElement("div");
document.body.appendChild(wrapperDiv);
wrapperDiv.style.position = "relative";
const div = document.createElement("div");
div.style.position = "absolute";
div.style.left = "10px";
div.style.bottom = "10px";
div.style.width = "300px";
div.style.background = "white";
if (this.canvas instanceof HTMLCanvasElement) {
wrapperDiv.appendChild(this.canvas);
}
wrapperDiv.appendChild(div);
const html = this.props.onAddHTML(div);
if (html) {
div.innerHTML = html;
}
}
}
_getSizeAndAspect() {
if (!this.device) {
return { width: 1, height: 1, aspect: 1 };
}
const [width, height] = this.device.getDefaultCanvasContext().getDrawingBufferSize();
const aspect = width > 0 && height > 0 ? width / height : 1;
return { width, height, aspect };
}
/** @deprecated Default viewport setup */
_resizeViewport() {
if (this.props.autoResizeViewport && this.device.gl) {
this.device.gl.viewport(
0,
0,
// @ts-expect-error Expose canvasContext
this.device.gl.drawingBufferWidth,
// @ts-expect-error Expose canvasContext
this.device.gl.drawingBufferHeight
);
}
}
_beginFrameTimers(time) {
const now = time ?? (typeof performance !== "undefined" ? performance.now() : Date.now());
if (this._lastFrameTime) {
const frameTime = now - this._lastFrameTime;
if (frameTime > 0) {
this.frameRate.addTime(frameTime);
}
}
this._lastFrameTime = now;
if (this.device?._isDebugGPUTimeEnabled()) {
this._consumeEncodedGpuTime();
}
this.cpuTime.timeStart();
}
_endFrameTimers() {
if (this.device?._isDebugGPUTimeEnabled()) {
this._consumeEncodedGpuTime();
}
this.cpuTime.timeEnd();
this._updateSharedStats();
}
_consumeEncodedGpuTime() {
if (!this.device) {
return;
}
const gpuTimeMs = this.device.commandEncoder._gpuTimeMs;
if (gpuTimeMs !== void 0) {
this.gpuTime.addTime(gpuTimeMs);
this.device.commandEncoder._gpuTimeMs = void 0;
}
}
_updateSharedStats() {
if (this.stats === this.sharedStats) {
return;
}
for (const name of Object.keys(this.sharedStats.stats)) {
if (!this.stats.stats[name]) {
delete this.sharedStats.stats[name];
}
}
this.stats.forEach((sourceStat) => {
const targetStat = this.sharedStats.get(sourceStat.name, sourceStat.type);
targetStat.sampleSize = sourceStat.sampleSize;
targetStat.time = sourceStat.time;
targetStat.count = sourceStat.count;
targetStat.samples = sourceStat.samples;
targetStat.lastTiming = sourceStat.lastTiming;
targetStat.lastSampleTime = sourceStat.lastSampleTime;
targetStat.lastSampleCount = sourceStat.lastSampleCount;
targetStat._count = sourceStat._count;
targetStat._time = sourceStat._time;
targetStat._samples = sourceStat._samples;
targetStat._startTime = sourceStat._startTime;
targetStat._timerPending = sourceStat._timerPending;
});
}
// Event handling
_startEventHandling() {
if (this.canvas) {
this.canvas.addEventListener("mousemove", this._onMousemove.bind(this));
this.canvas.addEventListener("mouseleave", this._onMouseleave.bind(this));
}
}
_onMousemove(event) {
if (event instanceof MouseEvent) {
this._getAnimationProps()._mousePosition = [event.offsetX, event.offsetY];
}
}
_onMouseleave(event) {
this._getAnimationProps()._mousePosition = null;
}
};
var AnimationLoop = _AnimationLoop;
__publicField(AnimationLoop, "defaultAnimationLoopProps", {
device: null,
onAddHTML: () => "",
onInitialize: async () => null,
onRender: () => {
},
onFinalize: () => {
},
onError: (error) => console.error(error),
// eslint-disable-line no-console
stats: void 0,
// view parameters
autoResizeViewport: false
});
// src/animation-loop/make-animation-loop.ts
var import_core2 = __toESM(require_core(), 1);
function makeAnimationLoop(AnimationLoopTemplateCtor, props) {
let renderLoop = null;
const device = props?.device || import_core2.luma.createDevice({ id: "animation-loop", adapters: props?.adapters, createCanvasContext: true });
const animationLoop = new AnimationLoop({
...props,
device,
async onInitialize(animationProps) {
clearError(animationProps.animationLoop.device);
try {
renderLoop = new AnimationLoopTemplateCtor(animationProps);
return await renderLoop?.onInitialize(animationProps);
} catch (error) {
console.error(error);
setError(animationProps.animationLoop.device, error);
return null;
}
},
onRender: (animationProps) => renderLoop?.onRender(animationProps),
onFinalize: (animationProps) => renderLoop?.onFinalize(animationProps)
});
animationLoop.getInfo = () => {
return this.AnimationLoopTemplateCtor.info;
};
return animationLoop;
}
function setError(device, error) {
if (!device) {
return;
}
const canvas = device.getDefaultCanvasContext().canvas;
if (canvas instanceof HTMLCanvasElement) {
canvas.style.overflow = "visible";
let errorDiv = document.getElementById("animation-loop-error");
errorDiv?.remove();
errorDiv = document.createElement("h1");
errorDiv.id = "animation-loop-error";
errorDiv.innerHTML = error.message;
errorDiv.style.position = "absolute";
errorDiv.style.top = "10px";
errorDiv.style.left = "10px";
errorDiv.style.color = "black";
errorDiv.style.backgroundColor = "red";
canvas.parentElement?.appendChild(errorDiv);
}
}
function clearError(device) {
if (!device) {
return;
}
const errorDiv = document.getElementById("animation-loop-error");
if (errorDiv) {
errorDiv.remove();
}
}
// src/model/model.ts
var import_core8 = __toESM(require_core(), 1);
var import_shadertools2 = __toESM(require_shadertools(), 1);
// src/geometry/gpu-geometry.ts
var import_core3 = __toESM(require_core(), 1);
// src/utils/uid.ts
var uidCounters = {};
function uid(id = "id") {
uidCounters[id] = uidCounters[id] || 1;
const count = uidCounters[id]++;
return `${id}-${count}`;
}
// src/geometry/gpu-geometry.ts
var GPUGeometry = class {
id;
userData = {};
/** Determines how vertices are read from the 'vertex' attributes */
topology;
bufferLayout = [];
vertexCount;
indices;
attributes;
constructor(props) {
this.id = props.id || uid("geometry");
this.topology = props.topology;
this.indices = props.indices || null;
this.attributes = props.attributes;
this.vertexCount = props.vertexCount;
this.bufferLayout = props.bufferLayout || [];
if (this.indices) {
if (!(this.indices.usage & import_core3.Buffer.INDEX)) {
throw new Error("Index buffer must have INDEX usage");
}
}
}
destroy() {
this.indices?.destroy();
for (const attribute of Object.values(this.attributes)) {
attribute.destroy();
}
}
getVertexCount() {
return this.vertexCount;
}
getAttributes() {
return this.attributes;
}
getIndexes() {
return this.indices || null;
}
_calculateVertexCount(positions) {
const vertexCount = positions.byteLength / 12;
return vertexCount;
}
};
function makeGPUGeometry(device, geometry) {
if (geometry instanceof GPUGeometry) {
return geometry;
}
const indices = getIndexBufferFromGeometry(device, geometry);
const { attributes, bufferLayout } = getAttributeBuffersFromGeometry(device, geometry);
return new GPUGeometry({
topology: geometry.topology || "triangle-list",
bufferLayout,
vertexCount: geometry.vertexCount,
indices,
attributes
});
}
function getIndexBufferFromGeometry(device, geometry) {
if (!geometry.indices) {
return void 0;
}
const data = geometry.indices.value;
return device.createBuffer({ usage: import_core3.Buffer.INDEX, data });
}
function getAttributeBuffersFromGeometry(device, geometry) {
const bufferLayout = [];
const attributes = {};
for (const [attributeName, attribute] of Object.entries(geometry.attributes)) {
let name = attributeName;
switch (attributeName) {
case "POSITION":
name = "positions";
break;
case "NORMAL":
name = "normals";
break;
case "TEXCOORD_0":
name = "texCoords";
break;
case "TEXCOORD_1":
name = "texCoords1";
break;
case "COLOR_0":
name = "colors";
break;
}
if (attribute) {
attributes[name] = device.createBuffer({
data: attribute.value,
id: `${attributeName}-buffer`
});
const { value, size, normalized } = attribute;
if (size === void 0) {
throw new Error(`Attribute ${attributeName} is missing a size`);
}
bufferLayout.push({
name,
format: import_core3.vertexFormatDecoder.getVertexFormatFromAttribute(value, size, normalized)
});
}
}
const vertexCount = geometry._calculateVertexCount(geometry.attributes, geometry.indices);
return { attributes, bufferLayout, vertexCount };
}
// src/debug/debug-shader-layout.ts
function getDebugTableForShaderLayout(layout, name) {
const table = {};
const header = "Values";
if (layout.attributes.length === 0 && !layout.varyings?.length) {
return { "No attributes or varyings": { [header]: "N/A" } };
}
for (const attributeDeclaration of layout.attributes) {
if (attributeDeclaration) {
const glslDeclaration = `${attributeDeclaration.location} ${attributeDeclaration.name}: ${attributeDeclaration.type}`;
table[`in ${glslDeclaration}`] = { [header]: attributeDeclaration.stepMode || "vertex" };
}
}
for (const varyingDeclaration of layout.varyings || []) {
const glslDeclaration = `${varyingDeclaration.location} ${varyingDeclaration.name}`;
table[`out ${glslDeclaration}`] = { [header]: JSON.stringify(varyingDeclaration) };
}
return table;
}
// src/debug/debug-framebuffer.ts
var DEBUG_FRAMEBUFFER_STATE_KEY = "__debugFramebufferState";
var DEFAULT_MARGIN_PX = 8;
function debugFramebuffer(renderPass, source3, options) {
if (renderPass.device.type !== "webgl") {
return;
}
const state = getDebugFramebufferState(renderPass.device);
if (state.flushing) {
return;
}
if (isDefaultRenderPass(renderPass)) {
flushDebugFramebuffers(renderPass, options, state);
return;
}
if (source3 && isFramebuffer(source3) && source3.handle !== null) {
if (!state.queuedFramebuffers.includes(source3)) {
state.queuedFramebuffers.push(source3);
}
}
}
function flushDebugFramebuffers(renderPass, options, state) {
if (state.queuedFramebuffers.length === 0) {
return;
}
const webglDevice = renderPass.device;
const { gl } = webglDevice;
const previousReadFramebuffer = gl.getParameter(gl.READ_FRAMEBUFFER_BINDING);
const previousDrawFramebuffer = gl.getParameter(gl.DRAW_FRAMEBUFFER_BINDING);
const [targetWidth, targetHeight] = renderPass.device.getDefaultCanvasContext().getDrawingBufferSize();
let topPx = parseCssPixel(options.top, DEFAULT_MARGIN_PX);
const leftPx = parseCssPixel(options.left, DEFAULT_MARGIN_PX);
state.flushing = true;
try {
for (const framebuffer of state.queuedFramebuffers) {
const [targetX0, targetY0, targetX1, targetY1, previewHeight] = getOverlayRect({
framebuffer,
targetWidth,
targetHeight,
topPx,
leftPx,
minimap: options.minimap
});
gl.bindFramebuffer(gl.READ_FRAMEBUFFER, framebuffer.handle);
gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, null);
gl.blitFramebuffer(
0,
0,
framebuffer.width,
framebuffer.height,
targetX0,
targetY0,
targetX1,
targetY1,
gl.COLOR_BUFFER_BIT,
gl.NEAREST
);
topPx += previewHeight + DEFAULT_MARGIN_PX;
}
} finally {
gl.bindFramebuffer(gl.READ_FRAMEBUFFER, previousReadFramebuffer);
gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, previousDrawFramebuffer);
state.flushing = false;
}
}
function getOverlayRect(options) {
const { framebuffer, targetWidth, targetHeight, topPx, leftPx, minimap } = options;
const maxWidth = minimap ? Math.max(Math.floor(targetWidth / 4), 1) : targetWidth;
const maxHeight = minimap ? Math.max(Math.floor(targetHeight / 4), 1) : targetHeight;
const scale2 = Math.min(maxWidth / framebuffer.width, maxHeight / framebuffer.height);
const previewWidth = Math.max(Math.floor(framebuffer.width * scale2), 1);
const previewHeight = Math.max(Math.floor(framebuffer.height * scale2), 1);
const targetX0 = leftPx;
const targetY0 = Math.max(targetHeight - topPx - previewHeight, 0);
const targetX1 = targetX0 + previewWidth;
const targetY1 = targetY0 + previewHeight;
return [targetX0, targetY0, targetX1, targetY1, previewHeight];
}
function getDebugFramebufferState(device) {
device.userData[DEBUG_FRAMEBUFFER_STATE_KEY] ||= {
flushing: false,
queuedFramebuffers: []
};
return device.userData[DEBUG_FRAMEBUFFER_STATE_KEY];
}
function isFramebuffer(value) {
return "colorAttachments" in value;
}
function isDefaultRenderPass(renderPass) {
const framebuffer = renderPass.props.framebuffer;
return !framebuffer || framebuffer.handle === null;
}
function parseCssPixel(value, defaultValue) {
if (!value) {
return defaultValue;
}
const parsedValue = Number.parseInt(value, 10);
return Number.isFinite(parsedValue) ? parsedValue : defaultValue;
}
// src/utils/deep-equal.ts
function deepEqual(a, b, depth) {
if (a === b) {
return true;
}
if (!depth || !a || !b) {
return false;
}
if (Array.isArray(a)) {
if (!Array.isArray(b) || a.length !== b.length) {
return false;
}
for (let i = 0; i < a.length; i++) {
if (!deepEqual(a[i], b[i], depth - 1)) {
return false;
}
}
return true;
}
if (Array.isArray(b)) {
return false;
}
if (typeof a === "object" && typeof b === "object") {
const aKeys = Object.keys(a);
const bKeys = Object.keys(b);
if (aKeys.length !== bKeys.length) {
return false;
}
for (const key of aKeys) {
if (!b.hasOwnProperty(key)) {
return false;
}
if (!deepEqual(a[key], b[key], depth - 1)) {
return false;
}
}
return true;
}
return false;
}
// src/utils/buffer-layout-helper.ts
var import_core4 = __toESM(require_core(), 1);
var BufferLayoutHelper = class {
bufferLayouts;
constructor(bufferLayouts) {
this.bufferLayouts = bufferLayouts;
}
getBufferLayout(name) {
return this.bufferLayouts.find((layout) => layout.name === name) || null;
}
/** Get attribute names from a BufferLayout */
getAttributeNamesForBuffer(bufferLayout) {
return bufferLayout.attributes ? bufferLayout.attributes?.map((layout) => layout.attribute) : [bufferLayout.name];
}
mergeBufferLayouts(bufferLayouts1, bufferLayouts2) {
const mergedLayouts = [...bufferLayouts1];
for (const attribute of bufferLayouts2) {
const index = mergedLayouts.findIndex((attribute2) => attribute2.name === attribute.name);
if (index < 0) {
mergedLayouts.push(attribute);
} else {
mergedLayouts[index] = attribute;
}
}
return mergedLayouts;
}
getBufferIndex(bufferName) {
const bufferIndex = this.bufferLayouts.findIndex((layout) => layout.name === bufferName);
if (bufferIndex === -1) {
import_core4.log.warn(`BufferLayout: Missing buffer for "${bufferName}".`)();
}
return bufferIndex;
}
};
// src/utils/buffer-layout-order.ts
function getMinLocation(attributeNames, shaderLayoutMap) {
let minLocation = Infinity;
for (const name of attributeNames) {
const location = shaderLayoutMap[name];
if (location !== void 0) {
minLocation = Math.min(minLocation, location);
}
}
return minLocation;
}
function sortedBufferLayoutByShaderSourceLocations(shaderLayout, bufferLayout) {
const shaderLayoutMap = Object.fromEntries(
shaderLayout.attributes.map((attr) => [attr.name, attr.location])
);
const sortedLayout = bufferLayout.slice();
sortedLayout.sort((a, b) => {
const attributeNamesA = a.attributes ? a.attributes.map((attr) => attr.attribute) : [a.name];
const attributeNamesB = b.attributes ? b.attributes.map((attr) => attr.attribute) : [b.name];
const minLocationA = getMinLocation(attributeNamesA, shaderLayoutMap);
const minLocationB = getMinLocation(attributeNamesB, shaderLayoutMap);
return minLocationA - minLocationB;
});
return sortedLayout;
}
// src/utils/shader-module-utils.ts
function mergeShaderModuleBindingsIntoLayout(shaderLayout, modules) {
if (!shaderLayout || !modules.some((module) => module.bindingLayout?.length)) {
return shaderLayout;
}
const mergedLayout = {
...shaderLayout,
bindings: shaderLayout.bindings.map((binding) => ({ ...binding }))
};
if ("attributes" in (shaderLayout || {})) {
mergedLayout.attributes = shaderLayout?.attributes || [];
}
for (const module of modules) {
for (const bindingLayout of module.bindingLayout || []) {
for (const relatedBindingName of getRelatedBindingNames(bindingLayout.name)) {
const binding = mergedLayout.bindings.find(
(candidate) => candidate.name === relatedBindingName
);
if (binding?.group === 0) {
binding.group = bindingLayout.group;
}
}
}
}
return mergedLayout;
}
function shaderModuleHasUniforms(module) {
return Boolean(module.uniformTypes && !isObjectEmpty(module.uniformTypes));
}
function getRelatedBindingNames(bindingName) {
const bindingNames = /* @__PURE__ */ new Set([bindingName, `${bindingName}Uniforms`]);
if (!bindingName.endsWith("Uniforms")) {
bindingNames.add(`${bindingName}Sampler`);
}
return [...bindingNames];
}
function isObjectEmpty(obj) {
for (const key in obj) {
return false;
}
return true;
}
// src/shader-inputs.ts
var import_core5 = __toESM(require_core(), 1);
var import_shadertools = __toESM(require_shadertools(), 1);
// ../../node_modules/@math.gl/types/dist/is-array.js
function isTypedArray(value) {
return ArrayBuffer.isView(value) && !(value instanceof DataView);
}
function isNumberArray(value) {
if (Array.isArray(value)) {
return value.length === 0 || typeof value[0] === "number";
}
return false;
}
function isNumericArray(value) {
return isTypedArray(value) || isNumberArray(value);
}
// src/model/split-uniforms-and-bindings.ts
function isUniformValue(value) {
return isNumericArray(value) || typeof value === "number" || typeof value === "boolean";
}
function splitUniformsAndBindings(uniforms, uniformTypes2 = {}) {
const result = { bindings: {}, uniforms: {} };
Object.keys(uniforms).forEach((name) => {
const uniform = uniforms[name];
if (Object.prototype.hasOwnProperty.call(uniformTypes2, name) || isUniformValue(uniform)) {
result.uniforms[name] = uniform;
} else {
result.bindings[name] = uniform;
}
});
return result;
}
// src/shader-inputs.ts
var ShaderInputs = class {
options = {
disableWarnings: false
};
/**
* The map of modules
* @todo should should this include the resolved dependencies?
*/
// @ts-ignore Fix typings
modules;
/** Stores the uniform values for each module */
moduleUniforms;
/** Stores the uniform bindings for each module */
moduleBindings;
/** Tracks if uniforms have changed */
// moduleUniformsChanged: Record<keyof ShaderPropsT, false | string>;
/**
* Create a new UniformStore instance
* @param modules
*/
constructor(modules, options) {
Object.assign(this.options, options);
const resolvedModules = (0, import_shadertools.getShaderModuleDependencies)(
Object.values(modules).filter(isShaderInputsModuleWithDependencies)
);
for (const resolvedModule of resolvedModules) {
modules[resolvedModule.name] = resolvedModule;
}
import_core5.log.log(1, "Creating ShaderInputs with modules", Object.keys(modules))();
this.modules = modules;
this.moduleUniforms = {};
this.moduleBindings = {};
for (const [name, module] of Object.entries(modules)) {
if (module) {
this._addModule(module);
if (module.name && name !== module.name && !this.options.disableWarnings) {
import_core5.log.warn(`Module name: ${name} vs ${module.name}`)();
}
}
}
}
/** Destroy */
destroy() {
}
/**
* Set module props
*/
setProps(props) {
for (const name of Object.keys(props)) {
const moduleName = name;
const moduleProps = props[moduleName] || {};
const module = this.modules[moduleName];
if (!module) {
if (!this.options.disableWarnings) {
import_core5.log.warn(`Module ${name} not found`)();
}
} else {
const oldUniforms = this.moduleUniforms[moduleName];
const oldBindings = this.moduleBindings[moduleName];
const uniformsAndBindings = module.getUniforms?.(moduleProps, oldUniforms) || moduleProps;
const { uniforms, bindings } = splitUniformsAndBindings(
uniformsAndBindings,
module.uniformTypes
);
this.moduleUniforms[moduleName] = mergeModuleUniforms(
oldUniforms,
uniforms,
module.uniformTypes
);
this.moduleBindings[moduleName] = { ...oldBindings, ...bindings };
}
}
}
/**
* Return the map of modules
* @todo should should this include the resolved dependencies?
*/
getModules() {
return Object.values(this.modules);
}
/** Get all uniform values for all modules */
getUniformValues() {
return this.moduleUniforms;
}
/** Merges all bindings for the shader (from the various modules) */
getBindingValues() {
const bindings = {};
for (const moduleBindings of Object.values(this.moduleBindings)) {
Object.assign(bindings, moduleBindings);
}
return bindings;
}
// INTERNAL
/** Return a debug table that can be used for console.table() or log.table() */
getDebugTable() {
const table = {};
for (const [moduleName, module] of Object.entries(this.moduleUniforms)) {
for (const [key, value] of Object.entries(module)) {
table[`${moduleName}.${key}`] = {
type: this.modules[moduleName].uniformTypes?.[key],
value: String(value)
};
}
}
return table;
}
_addModule(module) {
const moduleName = module.name;
this.moduleUniforms[moduleName] = mergeModuleUniforms(
{},
module.defaultUniforms || {},
module.uniformTypes
);
this.moduleBindings[moduleName] = {};
}
};
function mergeModuleUniforms(currentUniforms = {}, nextUniforms = {}, uniformTypes2 = {}) {
const mergedUniforms = { ...currentUniforms };
for (const [key, value] of Object.entries(nextUniforms)) {
if (value !== void 0) {
mergedUniforms[key] = mergeModuleUniformValue(currentUniforms[key], value, uniformTypes2[key]);
}
}
return mergedUniforms;
}
function mergeModuleUniformValue(currentValue, nextValue, uniformType) {
if (!uniformType || typeof uniformType === "string") {
return cloneModuleUniformValue(nextValue);
}
if (Array.isArray(unifor