@tresjs/core
Version:
Declarative ThreeJS using Vue Components
1,554 lines (1,518 loc) • 80.4 kB
JavaScript
/**
* name: @tresjs/core
* version: v5.8.3
* (c) 2026
* description: Declarative ThreeJS using Vue Components
* author: Alvaro Saburido <hola@alvarosaburido.dev> (https://github.com/alvarosabu/)
*/
import { Fragment, computed, createBlock, createCommentVNode, createElementBlock, createRenderer, defineComponent, getCurrentInstance, h, isRef, mergeProps, nextTick, normalizeClass, normalizeStyle, onBeforeUnmount, onMounted, onUnmounted, openBlock, provide, reactive, readonly, ref, renderSlot, shallowRef, toValue, unref, useSlots, watch, watchEffect, withCtx } from "vue";
import * as THREE from "three";
import { ACESFilmicToneMapping, ArrowHelper, BufferAttribute, Color, DirectionalLightHelper, DoubleSide, HemisphereLightHelper, Layers, Material, MathUtils, Mesh, MeshBasicMaterial, PCFSoftShadowMap, PerspectiveCamera, PointLightHelper, REVISION, Scene, SpotLightHelper, Vector3, WebGLRenderer } from "three";
import { createEventHook, createInjectionState, promiseTimeout, refDebounced, tryOnScopeDispose, unrefElement, useAsyncState, useDevicePixelRatio, useElementSize, useFps, useMemory, useRafFn, useTimeout, useWindowSize, whenever } from "@vueuse/core";
import { camel, isEqual, isFunction, isNumber, isObject, isString, isUndefined } from "radashi";
import { forwardHtmlEvents, getVoidObject } from "@pmndrs/pointer-events";
import { setupDevtoolsPlugin } from "@vue/devtools-api";
import { RectAreaLightHelper } from "three/examples/jsm/helpers/RectAreaLightHelper.js";
//#region package.json
var version = "5.8.3";
//#endregion
//#region src/utils/makeMap.ts
/**
* Make a map and return a function for checking if a key
* is in that map.
* IMPORTANT: all calls of this function must be prefixed with
* \/\*#\_\_PURE\_\_\*\/
* So that rollup can tree-shake them if necessary.
*/
/*! #__NO_SIDE_EFFECTS__ */
function makeMap(str) {
const map = Object.create(null);
for (const key of str.split(",")) map[key] = 1;
return (val) => val in map;
}
//#endregion
//#region src/utils/is/dom.ts
const HTML_TAGS = "html,body,base,head,link,meta,style,title,address,article,aside,footer,header,hgroup,h1,h2,h3,h4,h5,h6,nav,section,div,dd,dl,dt,figcaption,figure,picture,hr,img,li,main,ol,p,pre,ul,a,b,abbr,bdi,bdo,br,cite,code,data,dfn,em,i,kbd,mark,q,rp,rt,ruby,s,samp,small,span,strong,sub,sup,time,u,var,wbr,area,audio,map,track,video,embed,object,param,source,canvas,script,noscript,del,ins,caption,col,colgroup,table,thead,tbody,td,th,tr,button,datalist,fieldset,form,input,label,legend,meter,optgroup,option,output,progress,select,textarea,details,dialog,menu,summary,template,blockquote,iframe,tfoot";
const isHTMLTag = /* @__PURE__ */ makeMap(HTML_TAGS);
//#endregion
//#region src/utils/is/util.ts
const createTypeGuard = (property) => (value) => isObject(value) && property in value && !!value[property];
//#endregion
//#region src/utils/is/three.ts
/**
* Type guard to check if a value is a Three.js Object3D
* @param value - The value to check
* @returns True if the value is a Three.js Object3D instance, false otherwise
* @example
* ```ts
* const value = new THREE.Object3D()
* if (isObject3D(value)) {
* // TypeScript knows value is Object3D here
* value.position // OK
* value.rotation // OK
* value.scale // OK
* }
* ```
*/
const isObject3D = createTypeGuard("isObject3D");
/**
* Type guard to check if a value is a Three.js Mesh
* @param value - The value to check
* @returns True if the value is a Three.js Mesh instance, false otherwise
* @example
* ```ts
* const value = new THREE.Mesh()
* if (isMesh(value)) {
* // TypeScript knows value is Mesh here
* value.geometry // OK
*/
const isMesh = createTypeGuard("isMesh");
/**
* Type guard to check if a value is a Three.js Camera
* @param value - The value to check
* @returns True if the value is a Three.js Camera instance, false otherwise
* @example
* ```ts
* const value = new THREE.PerspectiveCamera()
* if (isCamera(value)) {
* // TypeScript knows value is Camera here
* value.fov // OK
* value.near // OK
* value.far // OK
* }
* ```
*/
const isCamera = createTypeGuard("isCamera");
/**
* Type guard to check if a value is a Three.js OrthographicCamera
* @param value - The value to check
* @returns True if the value is a Three.js OrthographicCamera instance, false otherwise
*/
const isOrthographicCamera = createTypeGuard("isOrthographicCamera");
/**
* Type guard to check if a value is a Three.js PerspectiveCamera
* @param value - The value to check
* @returns True if the value is a Three.js PerspectiveCamera instance, false otherwise
*/
const isPerspectiveCamera = createTypeGuard("isPerspectiveCamera");
/**
* Type guard to check if a value is a Three.js Color
* @param value - The value to check
* @returns True if the value is a Three.js Color instance, false otherwise
*/
const isColor = createTypeGuard("isColor");
/**
* Type guard to check if a value is a Three.js ColorRepresentation
* @param value - The value to check
* @returns True if the value is a Three.js ColorRepresentation instance, false otherwise
*/
const isColorRepresentation = (value) => isString(value) || isNumber(value) || isColor(value);
/**
* Type guard to check if a value is a Three.js Layers
* @param value - The value to check
* @returns True if the value is a Three.js Layers instance, false otherwise
*/
const isLayers = (value) => value instanceof Layers;
/**
* Type guard to check if a value is a Three.js BufferGeometry
* @param value - The value to check
* @returns True if the value is a Three.js BufferGeometry instance, false otherwise
* @example
* ```ts
* const value = new THREE.BufferGeometry()
* if (isBufferGeometry(value)) {
* // TypeScript knows value is BufferGeometry here
* value.attributes // OK
* value.index // OK
* value.computeVertexNormals() // OK
* }
* ```
*/
const isBufferGeometry = createTypeGuard("isBufferGeometry");
/**
* Type guard to check if a value is a Three.js Material
* @param value - The value to check
* @returns True if the value is a Three.js Material instance, false otherwise
* @example
* ```ts
* const value = new THREE.MeshStandardMaterial()
* if (isMaterial(value)) {
* // TypeScript knows value is Material here
* value.color // OK
* value.metalness // OK
* value.roughness // OK
* }
* ```
*/
const isMaterial = createTypeGuard("isMaterial");
/**
* Type guard to check if a value is a Three.js Light
* @param value - The value to check
* @returns True if the value is a Three.js Light instance, false otherwise
* @example
* ```ts
* const value = new THREE.DirectionalLight()
* if (isLight(value)) {
* // TypeScript knows value is Light here
* value.intensity // OK
* value.color // OK
* value.position // OK
* }
* ```
*/
const isLight = createTypeGuard("isLight");
/**
* Type guard to check if a value is a Three.js Fog
* @param value - The value to check
* @returns True if the value is a Three.js Fog instance, false otherwise
* @example
* ```ts
* const value = new THREE.Fog(0x000000, 1, 1000)
* if (isFog(value)) {
* // TypeScript knows value is Fog here
* value.color // OK
* value.near // OK
* value.far // OK
* }
* ```
*/
const isFog = createTypeGuard("isFog");
/**
* Type guard to check if a value is a Three.js Scene
* @param value - The value to check
* @returns True if the value is a Three.js Scene instance, false otherwise
* @example
* ```ts
* const value = new THREE.Scene()
* if (isScene(value)) {
* // TypeScript knows value is Scene here
* value.children // OK
* value.add(new THREE.Object3D()) // OK
* value.remove(new THREE.Object3D()) // OK
* }
* ```
*/
const isScene = createTypeGuard("isScene");
/**
* Type guard to check if a value is a Three.js Group
* @param value - The value to check
* @returns True if the value is a Three.js Group instance, false otherwise
* ```
*/
const isGroup = createTypeGuard("isGroup");
//#endregion
//#region src/utils/is/tres.ts
const isVectorLike = (value) => value !== null && typeof value === "object" && "set" in value && typeof value.set === "function";
const isCopyable = (value) => isVectorLike(value) && "copy" in value && typeof value.copy === "function";
const isClassInstance = (object) => !!object?.constructor;
/**
* Type guard to check if a value is a TresCamera
* @param value - The value to check
* @returns True if the value is a TresCamera instance, false otherwise
*/
const isTresCamera = (value) => isCamera(value) || isOrthographicCamera(value) || isPerspectiveCamera(value);
/**
* Type guard to check if a value is a TresObject
* @param value - The value to check
* @returns True if the value is a TresObject (Object3D | BufferGeometry | Material | Fog), false otherwise
* @example
* ```ts
* const value = new THREE.Mesh()
* if (isTresObject(value)) {
* // TypeScript knows value is TresObject here
* // You can use common properties and methods shared by all TresObjects
* }
* ```
* @remarks
* TresObject is a union type that represents the core Three.js objects that can be used in TresJS.
* This includes Object3D, BufferGeometry, Material, and Fog instances.
*/
const isTresObject = (value) => isObject3D(value) || isBufferGeometry(value) || isMaterial(value) || isFog(value);
/**
* Type guard to check if a value is a TresPrimitive
* @param value - The value to check
* @returns True if the value is a TresPrimitive instance, false otherwise
* @example
* ```ts
* const value = { isPrimitive: true }
* if (isTresPrimitive(value)) {
* // TypeScript knows value is TresPrimitive here
* // You can use properties and methods specific to TresPrimitives
* }
* ```
* @remarks
* TresPrimitive is a special type in TresJS that represents primitive objects
* that can be used directly in the scene without needing to be wrapped in a Three.js object.
*/
const isTresPrimitive = createTypeGuard("isPrimitive");
/**
* Type guard to check if a value is a TresInstance (has __tres property)
* @param value - The value to check
* @returns True if the value is a TresInstance (has __tres property), false otherwise
* @example
* ```ts
* const value = new THREE.Mesh()
* if (isTresInstance(value)) {
* // TypeScript knows value is TresInstance here
* // You can safely access value.__tres
* }
* ```
*/
const isTresInstance = (value) => isTresObject(value) && "__tres" in value;
//#endregion
//#region src/utils/array.ts
/**
* Like Array.filter, but modifies the array in place.
* @param array - Array to modify
* @param callbackFn - A function called for each element of the array. It should return a truthy value to keep the element in the array.
*/
const filterInPlace = (array, callbackFn) => {
let i = 0;
for (let ii = 0; ii < array.length; ii++) if (callbackFn(array[ii], ii)) {
array[i] = array[ii];
i++;
}
array.length = i;
return array;
};
//#endregion
//#region src/utils/logger.ts
/**
* Logger utility for TresJS
* @module logger
*/
function resolveRuntimeMode() {
try {
const modeFromImportMeta = import.meta?.env?.MODE;
if (modeFromImportMeta) return modeFromImportMeta;
} catch {}
return typeof process !== "undefined" && process.env && process.env.NODE_ENV ? process.env.NODE_ENV : "production";
}
const isProd = resolveRuntimeMode() === "production";
const logPrefix = "[TresJS ▲ ■ ●] ";
/**
* Logs an error message with the TresJS prefix
* @param args - Arguments to log
*/
function logError(...args) {
if (typeof args[0] === "string") args[0] = logPrefix + args[0];
else args.unshift(logPrefix);
console.error(...args);
}
/**
* Logs a warning message with the TresJS prefix
* @param args - Arguments to log
*/
function logWarning(...args) {
if (typeof args[0] === "string") args[0] = logPrefix + args[0];
else args.unshift(logPrefix);
console.warn(...args);
}
/**
* Logs a message with the TresJS prefix (only in development mode)
* @param name - Name of the message
* @param value - Value to log
*/
function logMessage(name, value) {
if (!isProd) console.log(`${logPrefix} - ${name}:`, value);
}
//#endregion
//#region src/utils/index.ts
function disposeMaterial(material) {
const hasMap = (material$1) => "map" in material$1 && !!material$1.map;
if (hasMap(material)) material.map.dispose();
material.dispose();
}
function disposeObject3D(object) {
if (object.parent) object.removeFromParent?.();
delete object.__tres;
[...object.children].forEach((child) => disposeObject3D(child));
if (object instanceof Scene) {} else {
const mesh = object;
if (object) object.dispose?.();
if (mesh.geometry) mesh.geometry.dispose();
if (Array.isArray(mesh.material)) mesh.material.forEach((material) => disposeMaterial(material));
else if (mesh.material) disposeMaterial(mesh.material);
}
}
function resolve(obj, key) {
let target = obj;
if (key.includes("-")) {
const entries = key.split("-");
let currKey = entries.shift();
while (target && entries.length) if (!(currKey in target)) currKey = joinAsCamelCase(currKey, entries.shift());
else {
target = target[currKey];
currKey = entries.shift();
}
return {
target,
key: joinAsCamelCase(currKey, ...entries)
};
} else return {
target,
key
};
}
function joinAsCamelCase(...strings) {
return strings.map((s, i) => i === 0 ? s : s.charAt(0).toUpperCase() + s.slice(1)).join("");
}
function attach(parent, child, type) {
const INDEX_REGEX = /-\d+$/;
if (isString(type)) {
if (INDEX_REGEX.test(type)) {
const { target: target$1, key: key$1 } = resolve(parent, type.replace(INDEX_REGEX, ""));
if (!Array.isArray(target$1[key$1])) {
const previousAttach = target$1[key$1];
const augmentedArray = [];
augmentedArray.__tresDetach = () => {
if (augmentedArray.every((v) => isUndefined(v))) target$1[key$1] = previousAttach;
};
target$1[key$1] = augmentedArray;
}
}
const { target, key } = resolve(parent, type);
child.__tres.previousAttach = target[key];
target[key] = unboxTresPrimitive(child);
} else child.__tres.previousAttach = type(parent, child);
}
function detach(parent, child, type) {
if (isString(type)) {
const { target, key } = resolve(parent, type);
const previous = child.__tres.previousAttach;
if (previous === void 0) delete target[key];
else target[key] = previous;
if ("__tresDetach" in target) target.__tresDetach();
} else child.__tres?.previousAttach?.(parent, child);
delete child.__tres?.previousAttach;
}
function prepareTresInstance(obj, state, context) {
const instance = obj;
instance.__tres = {
type: "unknown",
root: context,
memoizedProps: {},
objects: [],
parent: null,
previousAttach: null,
...state
};
if (!instance.__tres.attach) {
if (isMaterial(instance)) instance.__tres.attach = "material";
else if (isBufferGeometry(instance)) instance.__tres.attach = "geometry";
else if (isFog(instance)) instance.__tres.attach = "fog";
}
return instance;
}
function invalidateInstance(instance) {
const ctx = instance?.__tres?.root;
if (!ctx?.renderer) return;
if (ctx.renderer.canBeInvalidated.value) ctx.renderer.invalidate();
}
function setPrimitiveObject(newObject, primitive, setTarget, nodeOpsFns, context) {
const objectsToAttach = [...primitive.__tres.objects];
const oldObject = unboxTresPrimitive(primitive);
newObject = unboxTresPrimitive(newObject);
if (oldObject === newObject) return true;
const newInstance = prepareTresInstance(newObject, primitive.__tres ?? {}, context);
const parent = primitive.parent ?? primitive.__tres.parent ?? null;
const propsToPatch = { ...primitive.__tres.memoizedProps };
delete propsToPatch.object;
for (const obj of objectsToAttach) {
doRemoveDetach(obj, context);
doRemoveDeregister(obj, context);
}
oldObject.__tres.objects = [];
nodeOpsFns.remove(primitive);
for (const [key, value] of Object.entries(propsToPatch)) nodeOpsFns.patchProp(newInstance, key, newInstance[key], value);
setTarget(newObject);
nodeOpsFns.insert(primitive, parent);
for (const obj of objectsToAttach) nodeOpsFns.insert(obj, primitive);
return true;
}
function unboxTresPrimitive(maybePrimitive) {
if (isTresPrimitive(maybePrimitive)) {
const primitive = maybePrimitive;
primitive.object.__tres = primitive.__tres;
return primitive.object;
} else return maybePrimitive;
}
function doRemoveDetach(node, context) {
const parent = node.__tres?.parent || context.scene.value;
if (node.__tres) node.__tres.parent = null;
if (parent && parent.__tres && "objects" in parent.__tres) filterInPlace(parent.__tres.objects, (obj) => obj !== node);
if (node.__tres?.attach) detach(parent, node, node.__tres.attach);
else {
node.parent?.remove?.(unboxTresPrimitive(node));
node.parent = null;
}
}
function doRemoveDeregister(node, context) {
node.traverse?.((child) => {
if (isTresCamera(child)) context.camera.deregisterCamera(child);
});
if (isTresCamera(node)) context.camera.deregisterCamera(node);
invalidateInstance(node);
}
//#endregion
//#region src/composables/useLoader/index.ts
/**
* Vue composable for loading 3D models using Three.js loaders
* @param Loader - The Three.js loader constructor
* @param path - The path to the model file
* @param options - Optional configuration for the loader
* @returns UseAsyncState composable with the loaded model
*/
function useLoader(Loader$1, path, options) {
const proto = new Loader$1(options?.manager);
const progress = reactive({
loaded: 0,
total: 0,
percentage: 0
});
if (options?.extensions) options.extensions(proto);
const initialPath = toValue(path);
const result = useAsyncState((path$1) => new Promise((resolve$1, reject) => {
const assetPath = path$1 || initialPath || "";
proto.load(assetPath, (result$1) => {
resolve$1(result$1);
}, (event) => {
progress.loaded = event.loaded;
progress.total = event.total;
progress.percentage = progress.loaded / progress.total * 100;
}, (err) => {
reject(err);
});
}), options?.initialValue ?? null, {
...options?.asyncOptions,
immediate: options?.asyncOptions?.immediate ?? true
});
const unsub = watch(() => toValue(path), (newPath) => {
if (newPath) {
const value = result.state.value;
if (value && typeof value === "object" && "scene" in value && value.scene) disposeObject3D(value.scene);
result.execute(0, newPath);
}
});
onUnmounted(() => {
unsub();
const value = result.state.value;
if (value && typeof value === "object" && "scene" in value && value.scene) disposeObject3D(value.scene);
});
return {
...result,
load: (path$1) => {
result.execute(0, path$1);
},
progress
};
}
//#endregion
//#region src/composables/useLoader/component.vue?vue&type=script&setup=true&lang.ts
var component_vue_vue_type_script_setup_true_lang_default = /* @__PURE__ */ defineComponent({
__name: "component",
props: {
loader: {
type: null,
required: true
},
path: {
type: String,
required: true
},
manager: {
type: Object,
required: false
},
extensions: {
type: Function,
required: false
}
},
emits: ["loaded", "error"],
setup(__props, { emit: __emit }) {
const props = __props;
const emit = __emit;
const { state, isLoading, error } = useLoader(props.loader, props.path, {
manager: props.manager,
extensions: props.extensions
});
whenever(error, (err) => {
if (err) emit("error", err);
});
whenever(state, (value) => {
if (value) emit("loaded", value);
});
return (_ctx, _cache) => {
return renderSlot(_ctx.$slots, "default", {
state: unref(state),
isLoading: unref(isLoading),
error: unref(error)
});
};
}
});
//#endregion
//#region src/composables/useLoader/component.vue
var component_default = component_vue_vue_type_script_setup_true_lang_default;
//#endregion
//#region src/composables/useCamera/index.ts
/**
* Composable for managing cameras in a Three.js scene
* @param params - The parameters for the composable
* @param params.sizes - The sizes object containing window dimensions
* @returns The camera management functions and state
*/
const useCameraManager = ({ sizes }) => {
const cameras = ref([]);
const activeCamera = computed(() => cameras.value[0]);
/**
* Set the active camera
* @param cameraOrUuid - The camera or its UUID to set as active
*/
const setActiveCamera = (cameraOrUuid) => {
const camera = isCamera(cameraOrUuid) ? cameraOrUuid : cameras.value.find((camera$1) => camera$1.uuid === cameraOrUuid);
if (!camera) return;
cameras.value = [camera, ...cameras.value.filter(({ uuid }) => uuid !== camera.uuid)];
};
/**
* Register a camera
* @param camera - The camera to register
* @param active - Whether to set the camera as active
*/
const registerCamera = (camera, active = false) => {
if (cameras.value.some(({ uuid }) => uuid === camera.uuid)) return;
cameras.value.push(camera);
if (active) setActiveCamera(camera.uuid);
};
/**
* Deregister a camera
* @param camera - The camera to deregister
*/
const deregisterCamera = (camera) => {
cameras.value = cameras.value.filter(({ uuid }) => uuid !== camera.uuid);
};
/**
* Update camera aspect ratios when the window size changes
*/
watchEffect(() => {
if (sizes.aspectRatio.value) cameras.value.forEach((camera) => {
if (isPerspectiveCamera(camera)) {
camera.aspect = sizes.aspectRatio.value;
camera.updateProjectionMatrix();
}
});
});
return {
activeCamera,
cameras,
registerCamera,
deregisterCamera,
setActiveCamera
};
};
//#endregion
//#region src/utils/graph.ts
function buildGraph(object) {
const data = {
nodes: {},
materials: {},
meshes: {}
};
if (object) object.traverse((obj) => {
if (obj.name) data.nodes[obj.name] = obj;
if (isMesh(obj)) {
if (!data.meshes[obj.name]) data.meshes[obj.name] = obj;
(Array.isArray(obj.material) ? obj.material : [obj.material]).forEach((material) => {
if (material.name && !data.materials[material.name]) data.materials[material.name] = material;
});
}
});
return data;
}
//#endregion
//#region src/composables/useGraph/index.ts
const useGraph = (object) => {
return computed(() => {
const obj = toValue(object);
if (!obj) return;
return buildGraph(obj);
});
};
//#endregion
//#region src/utils/createPriorityEventHook.ts
function createPriorityEventHook() {
const eventToPriority = /* @__PURE__ */ new Map();
const ascending = /* @__PURE__ */ new Set();
let ADD_COUNT = 0;
let dirty = false;
const sort = () => {
const sorted = Array.from(eventToPriority.entries()).sort((a, b) => {
const priorityDiff = a[1].priority - b[1].priority;
return priorityDiff === 0 ? a[1].addI - b[1].addI : priorityDiff;
});
ascending.clear();
sorted.forEach((entry) => ascending.add(entry[0]));
};
const off = (fn) => {
eventToPriority.delete(fn);
ascending.delete(fn);
};
const on = (fn, priority = 0) => {
eventToPriority.set(fn, {
priority,
addI: ADD_COUNT++
});
const offFn = () => off(fn);
tryOnScopeDispose(offFn);
dirty = true;
return { off: offFn };
};
const trigger = (...args) => {
if (dirty) {
sort();
dirty = false;
}
return Promise.all(Array.from(ascending).map((fn) => fn(...args)));
};
const dispose = () => {
eventToPriority.clear();
ascending.clear();
};
return {
on,
off,
trigger,
dispose,
get count() {
return eventToPriority.size;
}
};
}
//#endregion
//#region src/core/catalogue.ts
const catalogue = ref({});
const extend = (objects) => Object.assign(catalogue.value, objects);
//#endregion
//#region src/composables/useRenderer/pixelRatio.ts
const setPixelRatio = (renderer, systemDpr, userDpr) => {
if (!isFunction(renderer.setPixelRatio)) return;
let newDpr = 0;
if (userDpr && Array.isArray(userDpr) && userDpr.length >= 2) {
const [min, max] = userDpr;
newDpr = MathUtils.clamp(systemDpr, min, max);
} else if (isNumber(userDpr)) newDpr = userDpr;
else newDpr = systemDpr;
if (newDpr !== renderer.getPixelRatio?.()) renderer.setPixelRatio(newDpr);
};
//#endregion
//#region src/core/revision.ts
const revision = Number.parseInt(REVISION.replace("dev", ""));
//#endregion
//#region src/utils/createTimer.ts
/**
* Minimum Three.js revision that ships the `Timer` class.
*/
const TIMER_MIN_REVISION = 179;
/**
* Creates a unified timer abstraction that uses `Timer` on Three.js r179+
* and falls back to `Clock` on older versions.
*
* - On r179+: uses `Timer` with its Page Visibility API support (`connect`/`disconnect`).
* - On older versions: uses `Clock` with `start`/`stop`.
*
*/
function createTimer() {
if (revision >= TIMER_MIN_REVISION) {
const timer = new THREE.Timer();
return {
getDelta: () => timer.getDelta(),
getElapsed: () => timer.getElapsed(),
update: () => timer.update(),
start: () => {
if (typeof document !== "undefined") timer.connect(document);
},
stop: () => timer.disconnect()
};
} else {
const clock = new THREE.Clock();
return {
getDelta: () => clock.getDelta(),
getElapsed: () => clock.elapsedTime,
update: () => {},
start: () => clock.start(),
stop: () => clock.stop()
};
}
}
//#endregion
//#region src/composables/useCreateRafLoop/index.ts
/**
* @param cycleFn the function that is called before the after event hook is triggered and after the before event hook is triggered.
*/
const useCreateRafLoop = (cycleFn, { fpsLimit } = {}) => {
const timer = createTimer();
const eventHooks = {
before: createEventHook(),
after: createEventHook()
};
const { pause, resume, isActive } = useRafFn(() => {
timer.update();
const context = {
delta: timer.getDelta(),
elapsed: timer.getElapsed()
};
eventHooks.before.trigger(context);
cycleFn();
eventHooks.after.trigger(context);
}, {
immediate: false,
fpsLimit
});
const start = () => {
timer.start();
resume();
};
const stop = () => {
timer.stop();
pause();
};
return {
start,
stop,
isActive,
onBeforeLoop: eventHooks.before.on,
onLoop: eventHooks.after.on
};
};
//#endregion
//#region src/utils/error.ts
var TresRendererError = class extends Error {
name = "TresRendererError";
constructor(message, code, options) {
super(message, options);
this.code = code;
}
};
//#endregion
//#region src/composables/useRenderer/useRendererManager.ts
function useRendererManager({ scene, canvas, options, fpsLimit, contextParts: { sizes, camera } }) {
const getRenderer = () => {
if (isFunction(options.renderer)) return options.renderer({
sizes,
scene,
camera,
canvas
});
return new WebGLRenderer({
...options,
canvas: unrefElement(canvas)
});
};
const renderer = getRenderer();
const frames = ref(toValue(options.renderMode) === "manual" ? 0 : 1);
const maxFrames = 60;
const canBeInvalidated = computed(() => toValue(options.renderMode) === "on-demand" && frames.value === 0);
const forceMaterialUpdate = () => scene.value.traverse((child) => {
if (child instanceof Mesh && child.material instanceof Material) child.material.needsUpdate = true;
});
/**
* Invalidates the current frame when in on-demand render mode.
*/
const invalidate = (amountOfFramesToInvalidate = 1) => {
if (!canBeInvalidated.value) return;
frames.value = Math.min(maxFrames, frames.value + amountOfFramesToInvalidate);
};
/**
* Advances one frame when in manual render mode.
*/
const advance = () => {
if (toValue(options.renderMode) !== "manual") throw new Error("advance can only be called in manual render mode.");
frames.value = 1;
};
const invalidateOnDemand = () => {
if (toValue(options.renderMode) === "on-demand") invalidate();
};
const isModeAlways = computed(() => toValue(options.renderMode) === "always");
const isRenderer = (value) => isObject(value) && "isRenderer" in value && Boolean(value.isRenderer);
const readyEventHook = createEventHook();
const errorEventHook = createEventHook();
let hasTriggeredReady = false;
const isInitialized = ref(false);
const error = ref(null);
const initializeRenderer = async () => {
try {
if (isRenderer(renderer)) await renderer.init();
isInitialized.value = true;
} catch (e) {
const rendererError = new TresRendererError(e instanceof Error ? e.message : "Unknown error", "INITIALIZATION_FAILED", { cause: e instanceof Error ? e : void 0 });
error.value = rendererError;
console.error(`[TresJS] Renderer initialization failed. This may occur if:
- WebGPU is not supported by your browser
- GPU is not available or lacks required features
- GPU drivers are outdated
Error details: ${rendererError.message}`, rendererError);
errorEventHook.trigger(rendererError);
}
};
const renderEventHook = createEventHook();
const notifyFrameRendered = () => {
frames.value = isModeAlways.value ? 1 : Math.max(0, frames.value - 1);
renderEventHook.trigger(renderer);
};
let renderFunction = (_notifyFrameRendered) => {
if (camera.activeCamera.value) {
renderer.render(scene.value, camera.activeCamera.value);
_notifyFrameRendered();
}
};
const replaceRenderFunction = (fn) => {
renderFunction = fn;
};
const loop = useCreateRafLoop(() => {
if (frames.value) renderFunction(notifyFrameRendered);
}, { fpsLimit });
readyEventHook.on(() => {
if (isInitialized.value) loop.start();
});
watch([
sizes.width,
sizes.height,
isInitialized
], () => {
if (!isInitialized.value) return;
renderer.setSize(sizes.width.value, sizes.height.value);
if (!hasTriggeredReady && renderer.domElement.width && renderer.domElement.height) {
hasTriggeredReady = true;
nextTick(() => {
readyEventHook.trigger(renderer);
});
}
invalidateOnDemand();
}, { immediate: true });
watchEffect(() => {
if (!isInitialized.value) return;
setPixelRatio(renderer, sizes.pixelRatio.value, toValue(options.dpr));
});
initializeRenderer();
if (toValue(options.renderMode) === "on-demand") invalidate();
if (toValue(options.renderMode) === "manual") useTimeout(100, { callback: advance });
const clearColorAndAlpha = computed(() => {
const clearColor = toValue(options.clearColor);
const clearAlpha = toValue(options.clearAlpha);
const isClearColorWithAlpha = typeof clearColor === "string" && clearColor.length === 9 && clearColor.startsWith("#");
if (isClearColorWithAlpha && clearAlpha !== void 0) logWarning(`clearColor with alpha (e.g. ${clearColor}) and clearAlpha cannot both be set, using clearColor as source of truth`);
if (isClearColorWithAlpha) return {
alpha: Number.parseInt(clearColor.slice(7, 9), 16) / 255,
color: clearColor.slice(0, 7)
};
return {
alpha: clearAlpha,
color: clearColor
};
});
watchEffect(() => {
if (!isInitialized.value) return;
const value = clearColorAndAlpha.value;
if (value.color === void 0 || value.alpha === void 0) return;
renderer.setClearColor(value.color, value.alpha);
});
watchEffect(() => {
if (!isInitialized.value) return;
const value = options.toneMapping;
if (value) renderer.toneMapping = value;
});
watchEffect(() => {
if (!isInitialized.value) return;
const value = options.toneMappingExposure;
if (value) renderer.toneMappingExposure = value;
});
watchEffect(() => {
if (!isInitialized.value) return;
const value = options.outputColorSpace;
if (value) renderer.outputColorSpace = value;
});
watchEffect(() => {
if (!isInitialized.value) return;
const value = options.shadows;
if (value === void 0) return;
renderer.shadowMap.enabled = value;
forceMaterialUpdate();
});
watchEffect(() => {
if (!isInitialized.value) return;
const value = options.shadowMapType;
if (value === void 0) return;
renderer.shadowMap.type = value;
forceMaterialUpdate();
});
onUnmounted(() => {
renderer.dispose();
if ("forceContextLoss" in renderer) renderer.forceContextLoss();
});
return {
loop,
instance: renderer,
advance,
onReady: readyEventHook.on,
onRender: renderEventHook.on,
onError: errorEventHook.on,
invalidate,
canBeInvalidated,
mode: toValue(options.renderMode),
replaceRenderFunction,
isInitialized,
error
};
}
//#endregion
//#region src/composables/useSizes/index.ts
function useSizes(windowSize, canvas, debounceMs = 10) {
const { pixelRatio } = useDevicePixelRatio();
const reactiveSize = toValue(windowSize) ? useWindowSize() : useElementSize(computed(() => toValue(canvas).parentElement));
const debouncedReactiveWidth = readonly(refDebounced(reactiveSize.width, debounceMs));
const debouncedReactiveHeight = readonly(refDebounced(reactiveSize.height, debounceMs));
return {
width: debouncedReactiveWidth,
height: debouncedReactiveHeight,
pixelRatio,
aspectRatio: computed(() => debouncedReactiveWidth.value / debouncedReactiveHeight.value)
};
}
//#endregion
//#region src/composables/useEventManager/index.ts
function useEventManager({ canvas, contextParts: { scene, camera, renderer } }) {
const { update, destroy } = forwardHtmlEvents(toValue(canvas), () => toValue(camera.activeCamera), scene.value);
const { off } = renderer.loop.onLoop(update);
onUnmounted(destroy);
onUnmounted(off);
const voidObject = getVoidObject(scene.value);
const pointerMissedEventHook = createEventHook();
voidObject.addEventListener("click", pointerMissedEventHook.trigger);
return { onPointerMissed: pointerMissedEventHook.on };
}
//#endregion
//#region src/composables/useTresContextProvider/index.ts
const INJECTION_KEY = "useTres";
const [useTresContextProvider, _useTresContext] = createInjectionState(({ scene, canvas, fpsLimit, windowSize, rendererOptions }) => {
const localScene = shallowRef(scene);
const sizes = useSizes(windowSize, canvas);
const camera = useCameraManager({ sizes });
const renderer = useRendererManager({
scene: localScene,
canvas,
options: rendererOptions,
fpsLimit,
contextParts: {
sizes,
camera
}
});
const events = useEventManager({
canvas,
contextParts: {
scene: localScene,
camera,
renderer
}
});
const ctx = {
sizes,
scene: localScene,
camera,
renderer,
controls: ref(null),
extend,
events
};
ctx.scene.value.__tres = {
root: ctx,
objects: [],
isUnmounting: false
};
return ctx;
}, { injectionKey: "useTres" });
const useTresContext = () => {
const ctx = _useTresContext();
if (!ctx) throw new Error("useTresContext must be used together with useTresContextProvider.\n You probably tried to use it above or on the same level as a TresCanvas component.\n It should be used in child components of a TresCanvas instance.");
return ctx;
};
//#endregion
//#region src/composables/useTres/index.ts
function useTres() {
const { scene, renderer, camera, sizes, controls, extend: extend$1, events } = useTresContext();
return {
scene,
renderer: renderer.instance,
camera: camera.activeCamera,
sizes,
controls,
extend: extend$1,
events,
invalidate: renderer.invalidate,
advance: renderer.advance
};
}
//#endregion
//#region src/composables/useLoop/index.ts
/**
* Composable that provides control over the render loop and animation lifecycle.
*/
const useLoop = () => {
const tresContext = useTres();
const { renderer: rendererManager } = useTresContext();
const eventHookBeforeRender = createPriorityEventHook();
const eventHookAfterRender = createPriorityEventHook();
rendererManager.loop.onBeforeLoop((loopContext) => {
eventHookBeforeRender.trigger({
...tresContext,
...loopContext
});
});
rendererManager.loop.onLoop((loopContext) => {
eventHookAfterRender.trigger({
...tresContext,
...loopContext
});
});
const render = rendererManager.replaceRenderFunction;
return {
stop: rendererManager.loop.stop,
start: rendererManager.loop.start,
isActive: rendererManager.loop.isActive,
onBeforeRender: eventHookBeforeRender.on,
onRender: eventHookAfterRender.on,
render
};
};
//#endregion
//#region src/utils/primitive/createRetargetingProxy.ts
function createRetargetingProxy(target, getters = {}, setters = {}) {
let _target = target;
const setTarget = (newTarget) => {
_target = newTarget;
};
let proxy = new Proxy({}, {});
proxy = new Proxy({}, {
has(_, key) {
return key in getters || key in _target;
},
get(_, prop, __) {
if (prop in getters) return getters[prop](_target);
return _target[prop];
},
set(_, prop, val) {
if (setters[prop]) setters[prop](val, _target, proxy, setTarget);
else _target[prop] = val;
return true;
}
});
return proxy;
}
//#endregion
//#region src/utils/pointerEvents.ts
const supportedPointerEvents = [
"onClick",
"onContextmenu",
"onPointermove",
"onPointerenter",
"onPointerleave",
"onPointerover",
"onPointerout",
"onDblclick",
"onPointerdown",
"onPointerup",
"onPointercancel",
"onLostpointercapture",
"onWheel"
];
const pointerEventsMapVueToThree = {
onClick: "click",
onContextmenu: "contextmenu",
onPointermove: "pointermove",
onPointerenter: "pointerenter",
onPointerleave: "pointerleave",
onPointerover: "pointerover",
onPointerout: "pointerout",
onDblclick: "dblclick",
onPointerdown: "pointerdown",
onPointerup: "pointerup",
onPointercancel: "pointercancel",
onLostpointercapture: "lostpointercapture",
onWheel: "wheel"
};
const isSupportedPointerEvent = (event) => supportedPointerEvents.includes(event);
//#endregion
//#region src/core/nodeOps.ts
const textNodeSymbol = Symbol("tresTextNode");
const commentNodeSymbol = Symbol("tresComment");
const nodeOps = ({ context, options = { primitivePrefix: "" } }) => {
const scene = context.scene.value;
const createTextNode = () => {
if (scene?.__tres?.isUnmounting) return;
return {
[textNodeSymbol]: true,
__tres: { parent: null }
};
};
const createCommentNode = () => ({
[commentNodeSymbol]: true,
__tres: { parent: null }
});
const isTextNode = (node) => !!node && typeof node === "object" && textNodeSymbol in node;
const isCommentNode = (node) => !!node && typeof node === "object" && commentNodeSymbol in node;
function createElement(tag, _namespace, _isCustomizedBuiltIn, props) {
if (!props) props = {};
if (!props.args) props.args = [];
if (isHTMLTag(tag)) return null;
if (tag.includes("-")) tag = tag.replace(/-([a-z])/g, (_, c) => c.toUpperCase()).replace(/^[a-z]/, (c) => c.toUpperCase());
let name = tag.replace("Tres", "");
let obj;
if (tag === `${options?.primitivePrefix ?? ""}primitive`) {
if (!isObject(props.object) || isRef(props.object)) logError("Tres primitives need an 'object' prop, whose value is an object or shallowRef<object>");
name = props.object.type;
const __tres = {};
obj = createRetargetingProxy(props.object, {
object: (t) => t,
isPrimitive: () => true,
__tres: () => __tres
}, {
object: (object, _, primitive, setTarget) => {
setPrimitiveObject(object, primitive, setTarget, {
patchProp,
remove,
insert
}, context);
},
__tres: (t) => {
Object.assign(__tres, t);
}
});
} else {
const target = catalogue.value[name];
if (!target) logError(`${name} is not defined on the THREE namespace. Use extend to add it to the catalog.`);
obj = new target(...props.args);
}
if (!obj) return null;
if (isTresCamera(obj)) {
if (!props?.position) obj.position.set(3, 3, 3);
if (!props?.lookAt) obj.lookAt(0, 0, 0);
}
obj = prepareTresInstance(obj, {
...isTresInstance(obj) ? obj.__tres : {},
type: name,
memoizedProps: props,
primitive: tag === "primitive",
attach: props.attach
}, context);
return obj;
}
function insert(child, parent, anchor) {
if (!child) return;
parent = parent || scene;
if (isTextNode(child) || isCommentNode(child)) {
const parentInstance$1 = parent.__tres ? parent : prepareTresInstance(parent, {}, context);
child.__tres.parent = parentInstance$1;
if (parentInstance$1.__tres.objects) {
const insertIndex = anchor ? parentInstance$1.__tres.objects.findIndex((obj) => obj === anchor) : -1;
if (insertIndex >= 0) parentInstance$1.__tres.objects.splice(insertIndex, 0, child);
else parentInstance$1.__tres.objects.push(child);
}
return;
}
const childInstance = child.__tres ? child : prepareTresInstance(child, {}, context);
const parentInstance = parent.__tres ? parent : prepareTresInstance(parent, {}, context);
child = unboxTresPrimitive(childInstance);
parent = unboxTresPrimitive(parentInstance);
if (isTresCamera(child)) context.camera?.registerCamera(child);
if (childInstance.__tres.attach) attach(parentInstance, childInstance, childInstance.__tres.attach);
else if (isObject3D(child) && isObject3D(parentInstance)) {
parentInstance.add(child);
child.dispatchEvent({ type: "added" });
}
childInstance.__tres.parent = parentInstance;
if (parentInstance.__tres.objects && !parentInstance.__tres.objects.includes(childInstance)) {
const insertIndex = anchor ? parentInstance.__tres.objects.findIndex((obj) => obj === anchor) : -1;
if (~insertIndex) parentInstance.__tres.objects.splice(insertIndex, 0, childInstance);
else parentInstance.__tres.objects.push(childInstance);
}
}
/**
* @param node – the node root to remove
* @param dispose – the disposal type
*/
function remove(node, dispose) {
if (!node) return;
if (isTextNode(node) || isCommentNode(node)) {
const parent = node.__tres.parent;
if (parent?.__tres?.objects) filterInPlace(parent.__tres.objects, (obj) => obj !== node);
node.__tres.parent = null;
return;
}
dispose = isUndefined(dispose) ? "default" : dispose;
const userDispose = node.__tres?.dispose;
if (!isUndefined(userDispose)) if (userDispose === null) dispose = false;
else dispose = userDispose;
const isPrimitive = node.__tres?.primitive;
const shouldDispose = dispose === "default" ? !isPrimitive : !!dispose;
if (node.__tres && "objects" in node.__tres) [...node.__tres.objects].forEach((obj) => remove(obj, dispose));
if (shouldDispose) {
if (node.children) [...node.children].forEach((child) => remove(child, dispose));
}
doRemoveDetach(node, context);
doRemoveDeregister(node, context);
if (shouldDispose && !isScene(node)) {
if (isFunction(dispose)) dispose(node);
else if (isFunction(node.dispose)) try {
node.dispose();
} catch (e) {}
}
if ("__tres" in node) delete node.__tres;
}
function patchProp(node, prop, prevValue, nextValue) {
if (!node) return;
let root = node;
const key = prop;
if (node.__tres) node.__tres.memoizedProps[prop] = nextValue;
if (prop === "attach") {
const maybeParent = node.__tres?.parent || node.parent;
remove(node);
prepareTresInstance(node, { attach: nextValue }, context);
if (maybeParent) insert(node, maybeParent);
return;
}
if (prop === "dispose") {
if (!node.__tres) node = prepareTresInstance(node, {}, context);
node.__tres.dispose = nextValue;
return;
}
if (isSupportedPointerEvent(prop) && isFunction(nextValue)) node.addEventListener(pointerEventsMapVueToThree[prop], nextValue);
let finalKey = camel(key);
let target = root?.[finalKey];
if (key === "args") {
const prevNode = node;
const prevArgs = prevValue ?? [];
const args = nextValue ?? [];
const instanceName = node.__tres?.type || node.type;
if (instanceName && prevArgs.length && !isEqual(prevArgs, args)) {
const newInstance = new catalogue.value[instanceName](...nextValue);
const descriptors = Object.getOwnPropertyDescriptors(newInstance);
Object.entries(descriptors).forEach(([key$1, descriptor]) => {
if (!descriptor.writable && !descriptor.set) return;
if (key$1 in prevNode) try {
prevNode[key$1] = newInstance[key$1];
} catch (e) {
console.warn(`Could not set property ${key$1} on ${instanceName}:`, e);
}
});
root = prevNode;
}
return;
}
if (root.type === "BufferGeometry") {
if (key === "args") return;
root.setAttribute(camel(key), new BufferAttribute(...nextValue));
return;
}
if (key.includes("-") && target === void 0) {
const resolved = resolve(root, key);
target = resolved.target;
root = resolved.target;
finalKey = resolved.key;
if (target && finalKey) {
target[finalKey] = nextValue;
if (isTresCamera(node)) node.updateProjectionMatrix();
invalidateInstance(node);
return;
}
}
let value = nextValue;
if (value === "") value = true;
if (isFunction(target)) {
if (!isSupportedPointerEvent(prop)) if (Array.isArray(value)) node[finalKey](...value);
else node[finalKey](value);
if (finalKey.startsWith("on") && isFunction(value)) root[finalKey] = value;
return;
}
if (isLayers(target) && isLayers(value)) target.mask = value.mask;
else if (isColor(target) && isColorRepresentation(value)) target.set(value);
else if (isCopyable(target) && isClassInstance(value) && target.constructor === value.constructor) target.copy(value);
else if (isVectorLike(target) && Array.isArray(value)) if ("fromArray" in target && typeof target.fromArray === "function") target.fromArray(value);
else target.set(...value);
else if (isVectorLike(target) && typeof value === "number") if ("setScalar" in target && typeof target.setScalar === "function") target.setScalar(value);
else target.set(value);
else root[finalKey] = value;
if (isTresCamera(node)) node.updateProjectionMatrix();
invalidateInstance(node);
}
function parentNode(node) {
return node?.__tres?.parent || null;
}
function nextSibling(node) {
if (!node || !("__tres" in node)) return null;
const siblings = parentNode(node)?.__tres?.objects || [];
const index = siblings.findIndex((sibling) => sibling === node);
if (index < 0 || index >= siblings.length - 1) return null;
return siblings[index + 1];
}
const noop = () => {};
return {
insert,
remove,
createElement,
patchProp,
parentNode,
createText: createTextNode,
createComment: createCommentNode,
setText: noop,
setElementText: noop,
nextSibling,
querySelector: noop,
setScopeId: noop,
cloneNode: noop,
insertStaticContent: noop
};
};
//#endregion
//#region src/core/roots.ts
const _roots = /* @__PURE__ */ new Map();
const getRoot = (canvas) => _roots.get(canvas);
const setRoot = (canvas, root) => {
_roots.set(canvas, root);
};
const deleteRoot = (canvas) => _roots.delete(canvas);
//#endregion
//#region src/devtools/DevtoolsMessenger.ts
const QUEUEABLE_MESSAGE_TYPES = ["asset-load"];
/**
* Messenger class for communicating with Tres DevTools
* This class will be attached to window.__TRES__DEVTOOLS__
*/
var DevtoolsMessenger = class {
subscribers = /* @__PURE__ */ new Set();
messageQueue = [];
maxQueueSize = 100;
/**
* Send a message to devtools subscribers
* If no subscribers are available, only queueable message types are queued
*/
send(type, data) {
const message = {
type,
data,
timestamp: Date.now()
};
if (this.subscribers.size > 0) this.subscribers.forEach((subscriber) => subscriber(message));
else if (QUEUEABLE_MESSAGE_TYPES.includes(type)) this.queueMessage(message);
}
/**
* Queue a message for later delivery
*/
queueMessage(message) {
this.messageQueue.push(message);
if (this.messageQueue.length > this.maxQueueSize) this.messageQueue.shift();
}
/**
* Flush all queued messages to current subscribers
*/
flushQueue() {
if (this.messageQueue.length === 0 || this.subscribers.size === 0) return;
this.messageQueue.forEach((message) => {
this.subscribers.forEach((subscriber) => subscriber(message));
});
this.messageQueue = [];
}
/**
* Subscribe to devtools messages
* When a new subscriber is added, all queued messages (asset-load events) are immediately delivered
*/
subscribe(subscriber) {
this.subscribers.add(subscriber);
this.flushQueue();
return () => {
this.subscribers.delete(subscriber);
};
}
/**
* Check if there are any subscribers
*/
get hasSubscribers() {
return this.subscribers.size > 0;
}
/**
* Get the current queue size
*/
get queueSize() {
return this.messageQueue.length;
}
/**
* Clear all queued messages
*/
clearQueue() {
this.messageQueue = [];
}
};
//#endregion
//#region src/devtools/utils.ts
/**
* Shows a toast or console.log
*
* @param message - message to log
* @param type - different color of the tooltip
*/
function toastMessage(message, type) {
const tresMessage = `▲ ■ ●${message}`;
if (typeof __VUE_DEVTOOLS_TOAST__ === "function") __VUE_DEVTOOLS_TOAST__(tresMessage, type);
else if (type === "error") console.error(tresMessage);
else if (type === "warn") console.warn(tresMessage);
else console.log(tresMessage);
}
function __VUE_DEVTOOLS_TOAST__(tresMessage, type) {
throw new Error(tresMessage + type);
}
//#endregion
//#region src/utils/perf.ts
function calculateMemoryUsage(object) {
let totalMemory = 0;
object.traverse((node) => {
if (isMesh(node) && node.type !== "HightlightMesh") {
const geometry = node.geometry;
const verticesMemory = geometry.attributes.position.count * 3 * Float32Array.BYTES_PER_ELEMENT;
const facesMemory = geometry.index ? geometry.index.count * Uint32Array.BYTES_PER_ELEMENT : 0;
const normalsMemory = geometry.attributes.normal ? geometry.attributes.normal.count * 3 * Float32Array.BYTES_PER_ELEMENT : 0;
const uvsMemory = geometry.attributes.uv ? geometry.attributes.uv.count * 2 * Float32Array.BYTES_PER_ELEMENT : 0;
const geometryMemory = verticesMemory + facesMemory + normalsMemory + uvsMemory;
totalMemory += geometryMemory;
}
});
return totalMemory;
}
function boundedPush(arr, value, max) {
arr.push(value);
if (arr.length > max) arr.shift();
}
function bytesToKB(bytes) {
return (bytes / 1024).toFixed(2);
}
//#endregion
//#region src/devtools/setupDevtools.ts
function setupTresDevtools(ctx) {
if (!ctx) return;
if (typeof window !== "undefined" && !window.__TRES__DEVTOOLS__) window.__TRES__DEVTOOLS__ = new DevtoolsMessenger();
const performanceState = {
maxFrames: 160,
fps: {
value: 0,
accumulator: []
},
memory: {
currentMem: 0,
allocatedMem: 0,
accumulator: []
}
};
const updateInterval = 100;
const fps = useFps({ every: updateInterval });
const { isSupported, memory } = useMemory({ interval: updateInterval });
const maxFrames = 160;
let lastUpdateTime =