UNPKG

@tresjs/core

Version:

Declarative ThreeJS using Vue Components

1,554 lines (1,518 loc) 80.4 kB
/** * 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 =