Преглед на файлове

feat!: useLoop and useRenderLoop refactoring (#1035)

BREAKING CHANGE: onAfterRender has been renamed to onRender

* refactor: type improvements in createPriorityEventHook

* wip

* wip

* improved test

* added context to useCreateRenderLoop and tests

* fixed typing

* added clock

* wip

* refactor: remove deprecated loop implementation and update related composables

- Removed the old `useCreateRenderLoop` and its associated tests.
- Updated `useLoop` to utilize the new `useRenderLoop` for better event handling.
- Commented out `OrbitControls` in relevant Vue components for potential future use.
- Cleaned up unused code and comments in `useTresContextProvider`.

* added some todos

* refactor: removed redundant eventhook

* refactor: simplify renderer setup by removing loop parameter

* refactor: enhance useRenderLoop to accept notifyFrameRendered callback

* chore: removed obsolete comment

* chore: made TresCanvas use correct event hook

* refactor!: omitted frame in render manager return

* refactor: restored renderEventHook

* chore: added comments after meeting

* refactor: made render loop more generic in it's naming

* refactor: made useLoop use useTres context

* chore: added jsdoc description for useLoop

* refactor: rename LoopContext to RafLoopContext and update related references

* refactor: replaced setTimeout with useTimeout

* refactor: update useLoop tests to use useCreateRafLoop and adjust rendering logic

* test: enhance useLoop tests with fake timers and add new callback functionality

* chore: removed debug code

* fix: initialize frames based on renderMode to ensure correct rendering behavior

* refactor: fixed multiple playgrounds

* feat: added loop and beforeLoop events to canvas

* fix: fixed playground experience
rafactor: renamed replaceCycleFunction to replaceLoopFunction

* fix: fixed some more playgrounds

* fixed some more playgrounds

* refactor!: renamed onBeforeRender to onBeforeLoop and onRender to onLoop
fix: fixed playgrounds

* fixed test

* refactor: tiny code styling improvement

* refactor: omitted export of useCreateRafLoop

* linting fixes

* refactor: tiny improvement for fbo demo

* refactor: rename fboRef to groupRef for clarity in FBO demo

* refactor: renamed onBeforeLoop back to onBeforeRender and onLoop back to onRender

* restored usage of orbit controls

* more restoring of orbitcontrols usages

* refactor: updated loop handling by renaming onLoop to onRender in TakeOverRenderExperience.vue and adjusted related emit definitions in TresCanvas.vue

* removed obsolete comment

* chore(playground): fixed related playgrounds (#1039)

* refactor!: TresCanvas: changed type of event "render" to TresContext.

---------

Co-authored-by: Alvaro Saburido <alvaro.saburido@gmail.com>
Tino Koch преди 1 седмица
родител
ревизия
db65f3f1ea
променени са 41 файла, в които са добавени 492 реда и са изтрити 881 реда
  1. 0 3
      playground/vue/src/App.vue
  2. 0 65
      playground/vue/src/components/AnimatedObjectUseUpdate.vue
  3. 11 1
      playground/vue/src/components/BlenderCube.vue
  4. 3 3
      playground/vue/src/components/LocalOrbitControls.vue
  5. 3 3
      playground/vue/src/components/TheSphere.vue
  6. 1 0
      playground/vue/src/pages/advanced/fbo/FBOCube.vue
  7. 17 17
      playground/vue/src/pages/advanced/fbo/index.vue
  8. 3 2
      playground/vue/src/pages/advanced/fbo/useFBO.ts
  9. 1 1
      playground/vue/src/pages/advanced/manual/experience.vue
  10. 24 35
      playground/vue/src/pages/advanced/takeOverRender/TakeOverRenderExperience.vue
  11. 2 2
      playground/vue/src/pages/advanced/webGPU/index.vue
  12. 4 4
      playground/vue/src/pages/basic/Multiple.vue
  13. 8 10
      playground/vue/src/pages/basic/PiercedProps.vue
  14. 5 3
      playground/vue/src/pages/basic/Primitives.vue
  15. 2 2
      playground/vue/src/pages/basic/index.vue
  16. 6 6
      playground/vue/src/pages/events/DynamicObjects.vue
  17. 3 3
      playground/vue/src/pages/events/FpsDropsReproduction.vue
  18. 1 1
      playground/vue/src/pages/loaders/fbx-loader/TheExperience.vue
  19. 1 1
      playground/vue/src/pages/loaders/gltf-loader/TheExperience.vue
  20. 2 2
      playground/vue/src/pages/loaders/multiple-models/TheExperience.vue
  21. 1 1
      playground/vue/src/pages/loaders/texture-loader/TheExperience.vue
  22. 5 82
      playground/vue/src/pages/misc/BrownianDistribution.vue
  23. 147 0
      playground/vue/src/pages/misc/BrownianDistributionExperience.vue
  24. 3 2
      playground/vue/src/pages/misc/use-graph/index.vue
  25. 6 6
      pnpm-lock.yaml
  26. 23 6
      src/components/TresCanvas.vue
  27. 0 1
      src/composables/index.ts
  28. 60 0
      src/composables/useCreateRafLoop/index.ts
  29. 3 3
      src/composables/useEventManager/index.ts
  30. 27 35
      src/composables/useLoop/index.ts
  31. 80 0
      src/composables/useLoop/useLoop.test.ts
  32. 0 61
      src/composables/useRenderLoop/index.ts
  33. 26 23
      src/composables/useRenderer/useRendererManager.ts
  34. 3 4
      src/composables/useTres/index.ts
  35. 3 20
      src/composables/useTresContextProvider/index.ts
  36. 0 274
      src/core/loop.test.ts
  37. 0 185
      src/core/loop.ts
  38. 0 1
      src/index.ts
  39. 1 0
      src/types/index.ts
  40. 7 12
      src/utils/createPriorityEventHook.ts
  41. 0 1
      vite.config.ts

+ 0 - 3
playground/vue/src/App.vue

@@ -10,9 +10,6 @@ function setBodyClass(routeName: string) {
 }
 watch([route], () => setBodyClass(route.name?.toString() ?? ''))
 provide('v-route', route)
-provide('useTres', {
-  message: `Im not the real useTres, but I can provide you with some data!`,
-})
 </script>
 
 <template>

+ 0 - 65
playground/vue/src/components/AnimatedObjectUseUpdate.vue

@@ -1,65 +0,0 @@
-<!-- eslint-disable no-console -->
-<script setup lang="ts">
-import { type LoopCallbackWithCtx, useLoop } from '@tresjs/core'
-import { useControls } from '@tresjs/leches'
-import { useThrottleFn } from '@vueuse/core'
-
-const sphereRef = ref()
-
-const log = useThrottleFn(state => console.log('updating sphere', state), 3000)
-const log2 = useThrottleFn(() => console.log('this should happen before updating the sphere'), 3000)
-
-const { onBeforeRender, pause, resume } = useLoop()
-
-const updateCallback = (state: LoopCallbackWithCtx) => {
-  if (!sphereRef.value) { return }
-  log(state)
-  sphereRef.value.position.y += Math.sin(state.elapsed) * 0.01
-}
-
-const { off } = onBeforeRender(updateCallback)
-
-onBeforeRender(() => {
-  log2()
-}, -1)
-
-const { areUpdatesPaused, unregister } = useControls({
-  areUpdatesPaused: {
-    value: false,
-    type: 'boolean',
-    label: 'Pause Updates',
-  },
-  unregister: {
-    value: false,
-    type: 'boolean',
-    label: 'Unregister update callback',
-  },
-})
-
-watchEffect(() => {
-  if (areUpdatesPaused.value) {
-    pause()
-  }
-  else {
-    resume()
-  }
-})
-
-watchEffect(() => {
-  if (unregister.value) {
-    off()
-  }
-})
-</script>
-
-<template>
-  <TresMesh
-    ref="sphereRef"
-    :position="[2, 0, 0]"
-    name="sphere"
-    cast-shadow
-  >
-    <TresSphereGeometry />
-    <TresMeshToonMaterial color="#FBB03B" />
-  </TresMesh>
-</template>

+ 11 - 1
playground/vue/src/components/BlenderCube.vue

@@ -1,12 +1,22 @@
 <script setup lang="ts">
 import { useGLTF } from '@tresjs/cientos'
+import { whenever } from '@vueuse/core'
 
-const { nodes } = useGLTF('https://raw.githubusercontent.com/Tresjs/assets/main/models/gltf/blender-cube.glb', { draco: true })
+const emit = defineEmits<{
+  ready: []
+}>()
+const { nodes, isReady } = useGLTF('https://raw.githubusercontent.com/Tresjs/assets/main/models/gltf/blender-cube.glb', { draco: true })
 const model = computed(() => nodes.value.BlenderCube)
 
 defineExpose({
   model,
 })
+
+whenever(
+  isReady,
+  () => emit('ready'),
+  { once: true },
+)
 </script>
 
 <template>

+ 3 - 3
playground/vue/src/components/LocalOrbitControls.vue

@@ -298,7 +298,7 @@ const {
   mouseButtons,
 } = toRefs(props)
 
-const { camera: activeCamera, renderer, extend, controls, invalidate } = useTresContext()
+const { camera: activeCamera, renderer, extend, controls } = useTresContext()
 
 const controlsRef = shallowRef<OrbitControls | null>(null)
 
@@ -317,7 +317,7 @@ watch(controlsRef, (value) => {
 function addEventListeners() {
   useEventListener(controlsRef.value as any, 'change', () => {
     emit('change', controlsRef.value)
-    invalidate()
+    renderer.invalidate()
   })
   useEventListener(controlsRef.value as any, 'start', () => emit('start', controlsRef.value))
   useEventListener(controlsRef.value as any, 'end', () => emit('end', controlsRef.value))
@@ -370,6 +370,6 @@ defineExpose({ instance: controlsRef })
     :enable-rotate="enableRotate"
     :rotate-speed="rotateSpeed"
     :mouse-buttons="mouseButtons"
-    :args="[camera || activeCamera, domElement || renderer.domElement]"
+    :args="[camera || activeCamera, domElement || renderer.instance.domElement]"
   />
 </template>

+ 3 - 3
playground/vue/src/components/TheSphere.vue

@@ -6,10 +6,10 @@ const sphereRef = ref()
 
 const { onBeforeRender } = useLoop()
 
-onBeforeRender((state) => {
+onBeforeRender(({ invalidate, elapsed }) => {
   if (!sphereRef.value) { return }
-  sphereRef.value.position.y += Math.sin(state.elapsed) * 0.01
-  state.invalidate()
+  sphereRef.value.position.y += Math.sin(elapsed) * 0.01
+  invalidate()
 })
 </script>
 

+ 1 - 0
playground/vue/src/pages/advanced/fbo/FBOCube.vue

@@ -20,6 +20,7 @@ watchEffect(() => {
   <TresMesh>
     <TresBoxGeometry :args="[1, 1, 1]" />
     <TresMeshBasicMaterial
+      v-if="fboTarget"
       :color="0xFF8833"
       :map="fboTarget.texture ?? null"
     />

+ 17 - 17
playground/vue/src/pages/advanced/fbo/index.vue

@@ -1,31 +1,31 @@
 <script setup lang="ts">
 import { OrbitControls } from '@tresjs/cientos'
 import { TresCanvas } from '@tresjs/core'
-import { BasicShadowMap, NoToneMapping, SRGBColorSpace } from 'three'
 import FBOCube from './FBOCube.vue'
 import '@tresjs/leches/styles'
 
-const gl = {
-  clearColor: '#82DBC5',
-  shadows: true,
-  alpha: false,
-  shadowMapType: BasicShadowMap,
-  outputColorSpace: SRGBColorSpace,
-  toneMapping: NoToneMapping,
+const groupRef = ref()
+
+const onLoop = ({ elapsed }: { elapsed: number }) => {
+  groupRef.value.position.y = Math.sin(elapsed)
 }
 </script>
 
 <template>
-  <TresCanvas v-bind="gl">
-    <TresPerspectiveCamera :position="[3, 3, 3]" />
+  <TresCanvas clear-color="#82DBC5" @loop="onLoop">
+    <TresPerspectiveCamera :position="[3, 3, 3]" :look-at="[0, 0, 0]" />
     <OrbitControls />
-    <!--  <Fbo
-      ref="fboRef"
-      v-bind="state"
-    /> -->
-
-    <FBOCube />
-    <AnimatedObjectUseUpdate />
+    <TresGroup ref="groupRef">
+      <FBOCube />
+    </TresGroup>
+    <TresMesh :position="[4, 0, 0]">
+      <TresTorusKnotGeometry />
+      <TresMeshNormalMaterial />
+    </TresMesh>
+    <TresMesh :position="[-4, 0, 0]">
+      <TresTorusGeometry />
+      <TresMeshNormalMaterial />
+    </TresMesh>
     <TresAmbientLight :intensity="1" />
   </TresCanvas>
 </template>

+ 3 - 2
playground/vue/src/pages/advanced/fbo/useFBO.ts

@@ -1,4 +1,3 @@
-import type { Camera } from 'three'
 import type { Ref } from 'vue'
 /* eslint-disable no-console */
 import { useLoop, useTres } from '@tresjs/core'
@@ -75,7 +74,9 @@ export function useFBO(options: FboOptions) {
     logBefore()
     renderer.setRenderTarget(target.value)
     renderer.clear()
-    renderer.render(scene, camera as Camera)
+    if (camera.value) {
+      renderer.render(scene.value, camera.value)
+    }
     renderer.setRenderTarget(null)
   }, Number.POSITIVE_INFINITY)
 

+ 1 - 1
playground/vue/src/pages/advanced/manual/experience.vue

@@ -15,7 +15,7 @@ onMounted(() => {
     :position="[5, 5, 5]"
     :look-at="[0, 0, 0]"
   />
-  <BlenderCube />
+  <BlenderCube @ready="advance" />
 
   <TresGridHelper />
   <OrbitControls @change="advance" />

+ 24 - 35
playground/vue/src/pages/advanced/takeOverRender/TakeOverRenderExperience.vue

@@ -1,54 +1,43 @@
 <script setup lang="ts">
 import { OrbitControls } from '@tresjs/cientos'
 
-import { useLoop } from '@tresjs/core'
+import { useLoop, useTres } from '@tresjs/core'
 import { useControls } from '@tresjs/leches'
+import type { Mesh } from 'three'
 
-const { render, pauseRender, resumeRender } = useLoop()
+const { render, onRender } = useLoop()
+const { renderer, scene, camera } = useTres()
 
-const { off } = render(({ renderer, scene, camera }) => {
-  renderer.instance.render(scene, camera)
+const { shouldRender } = useControls({
+  shouldRender: true,
 })
 
-const { isRenderPaused, unregisterRender } = useControls({
-  isRenderPaused: {
-    value: false,
-    type: 'boolean',
-    label: 'Pause Render',
-  },
-  unregisterRender: {
-    value: false,
-    type: 'boolean',
-    label: 'Unregister render callback',
-  },
-})
-
-watchEffect(() => {
-  if (unregisterRender.value) {
-    off()
+render((notifySuccess) => {
+  if (shouldRender.value && camera.value) {
+    renderer.render(scene.value, camera.value)
+    notifySuccess()
   }
 })
 
-watchEffect(() => {
-  if (isRenderPaused.value) {
-    pauseRender()
-  }
-  else {
-    resumeRender()
+const boxRef = ref<Mesh>()
+
+onRender(() => {
+  if (boxRef.value) {
+    boxRef.value.rotation.y += 0.01
   }
 })
-
-const showGrid = ref(true)
-
-setTimeout(() => {
-  showGrid.value = false
-}, 10000)
 </script>
 
 <template>
-  <TresPerspectiveCamera :position="[3, 3, 3]" />
+  <TresPerspectiveCamera :position="[3, 3, 3]" :look-at="[0, 0, 0]" />
   <OrbitControls make-default />
-  <AnimatedObjectUseUpdate />
-  <TresGridHelper v-if="showGrid" />
+  <TresMesh
+    ref="boxRef"
+    :position="[0, 0, 0]"
+    cast-shadow
+  >
+    <TresBoxGeometry />
+    <TresMeshNormalMaterial />
+  </TresMesh>
   <TresAmbientLight :intensity="1" />
 </template>

+ 2 - 2
playground/vue/src/pages/advanced/webGPU/index.vue

@@ -4,7 +4,7 @@ import { WebGPURenderer } from 'three/webgpu'
 import type { ShadowMapType, ToneMapping } from 'three'
 import type { TresRendererSetupContext } from '@tresjs/core'
 import { ACESFilmicToneMapping, AgXToneMapping, BasicShadowMap, CineonToneMapping, LinearToneMapping, NeutralToneMapping, NoToneMapping, PCFShadowMap, PCFSoftShadowMap, ReinhardToneMapping, VSMShadowMap } from 'three'
-// import { OrbitControls } from '@tresjs/cientos'
+import { OrbitControls } from '@tresjs/cientos'
 import { TresLeches, useControls } from '@tresjs/leches'
 import '@tresjs/leches/styles'
 
@@ -73,7 +73,7 @@ const formattedShadowMapType = computed(() => {
     <Suspense>
       <HologramCube />
     </Suspense>
-    <!-- <OrbitControls /> -->
+    <OrbitControls />
     <TresAmbientLight :intensity="1" />
   </TresCanvas>
 </template>

+ 4 - 4
playground/vue/src/pages/basic/Multiple.vue

@@ -3,10 +3,10 @@ import { TresCanvas } from '@tresjs/core'
 import { ref, shallowRef } from 'vue'
 
 const boxRef = shallowRef(null)
-const showBox = ref(true)
+const showBoxAndSphere = ref(true)
 
 setInterval(() => {
-  showBox.value = !showBox.value
+  showBoxAndSphere.value = !showBoxAndSphere.value
 }, 3000)
 </script>
 
@@ -24,7 +24,7 @@ setInterval(() => {
           color="red"
         />
         <TresMesh
-          v-if="showBox"
+          v-if="showBoxAndSphere"
           ref="boxRef"
           :position="[0, 2, 0]"
         >
@@ -46,7 +46,7 @@ setInterval(() => {
           :position="[5, 5, 5]"
           :look-at="[0, 0, 0]"
         />
-        <TresMesh>
+        <TresMesh v-if="showBoxAndSphere">
           <TresSphereGeometry :args="[1, 32, 32]" />
           <TresMeshNormalMaterial />
         </TresMesh>

+ 8 - 10
playground/vue/src/pages/basic/PiercedProps.vue

@@ -1,6 +1,6 @@
 <script setup lang="ts">
-import { TresCanvas, useRenderLoop } from '@tresjs/core'
-import { TresLeches, useControls } from '@tresjs/leches'
+import { TresCanvas } from '@tresjs/core'
+import { TresLeches } from '@tresjs/leches'
 import '@tresjs/leches/styles'
 import { shallowRef } from 'vue'
 import LocalOrbitControls from '../../components/LocalOrbitControls.vue'
@@ -29,17 +29,15 @@ const labels = [
   'scale-z',
 ]
 
-/* const PI2 = Math.PI * 2 */
-
-useRenderLoop().onLoop(({ elapsed }) => {
+const onLoop = ({ elapsed }: { elapsed: number }) => {
   const i = Math.floor(elapsed % refs.length)
   refs[i].value = Math.cos(elapsed * Math.PI * 2)
   label.value = `${labels[i]} ${Math.trunc(refs[i].value * 10) / 10}`
-})
+}
 
-const { enableZoom } = useControls({
-  enableZoom: false,
-})
+// const { enableZoom } = useControls({
+//   enableZoom: false,
+// })
 </script>
 
 <template>
@@ -48,7 +46,7 @@ const { enableZoom } = useControls({
     {{ label }}
   </div>
   <TresLeches />
-  <TresCanvas>
+  <TresCanvas @loop="onLoop">
     <TresMesh
       :position-x="x"
       :position-y="y"

+ 5 - 3
playground/vue/src/pages/basic/Primitives.vue

@@ -1,7 +1,7 @@
 <!-- eslint-disable no-console -->
 <script setup lang="ts">
 import { OrbitControls } from '@tresjs/cientos'
-import { TresCanvas, useRenderLoop } from '@tresjs/core'
+import { TresCanvas } from '@tresjs/core'
 import { TresLeches, useControls } from '@tresjs/leches'
 import {
   BasicShadowMap,
@@ -71,7 +71,7 @@ secondGroup.add(sphere)
 
 const primitiveRef = ref()
 
-useRenderLoop().onLoop(() => {
+const rotate = () => {
   if (primitiveRef.value) {
     // This doesn't work
     /* torusKnot.rotation.x += 0.01 */
@@ -79,7 +79,7 @@ useRenderLoop().onLoop(() => {
     primitiveRef.value.rotation.x += 0.01
     primitiveRef.value.rotation.y += 0.01
   }
-})
+}
 
 watchEffect(() => {
   console.log('primitiveRef.value', primitiveRef.value)
@@ -103,9 +103,11 @@ const modelArray = ref([torus, torusKnot, sphere]) */
     window-size
     class="awiwi"
     :style="{ background: '#008080' }"
+    @render="rotate"
   >
     <TresPerspectiveCamera
       :position="[7, 7, 7]"
+      :look-at="[0, 0, 0]"
     />
     <OrbitControls />
     <!--  <primitive

+ 2 - 2
playground/vue/src/pages/basic/index.vue

@@ -57,7 +57,7 @@ const formattedShadowMapType = computed(() => {
     :shadows="shadows"
     :shadow-map-type="formattedShadowMapType"
   >
-    <TresPerspectiveCamera :position="[5, 5, 5]" />
+    <TresPerspectiveCamera :position="[5, 5, 5]" :look-at="[0, 0, 0]" />
     <OrbitControls />
     <TresMesh :position="[0, 1, 0]" cast-shadow>
       <TresBoxGeometry />
@@ -65,7 +65,7 @@ const formattedShadowMapType = computed(() => {
     </TresMesh>
 
     <TresMesh
-      :rotation="[-Math.PI / 2, 0, 0]"
+      :rotation-x="-Math.PI / 2"
       receive-shadow
     >
       <TresPlaneGeometry :args="[10, 10, 10, 10]" />

+ 6 - 6
playground/vue/src/pages/events/DynamicObjects.vue

@@ -1,6 +1,6 @@
 <script setup lang="ts">
-import { Box, OrbitControls, Sphere, StatsGl } from '@tresjs/cientos'
-import { TresCanvas } from '@tresjs/core'
+import { Box, OrbitControls, Sphere } from '@tresjs/cientos'
+import { TresCanvas, type TresPointerEvent } from '@tresjs/core'
 import { reactive } from 'vue'
 
 const hotspots = reactive([
@@ -26,11 +26,11 @@ const removeHotspot = () => {
   hotspots.pop()
 }
 
-const grow = (event) => {
+const grow = (event: TresPointerEvent) => {
   event.object.scale.set(1.5, 1.5, 1.5)
 }
 
-const shrink = (event) => {
+const shrink = (event: TresPointerEvent) => {
   event.object.scale.set(1, 1, 1)
 }
 </script>
@@ -52,8 +52,8 @@ const shrink = (event) => {
       :args="[0.5, 16, 16]"
       :position="hotspot.position"
       @click="console.log('click', index)"
-      @pointer-enter="grow"
-      @pointer-leave="shrink"
+      @pointerenter="grow"
+      @pointerleave="shrink"
     >
       <TresMeshNormalMaterial />
     </Sphere>

+ 3 - 3
playground/vue/src/pages/events/FpsDropsReproduction.vue

@@ -1,5 +1,6 @@
 <script lang="ts" setup>
-import { Icosahedron, OrbitControls, StatsGl } from '@tresjs/cientos'
+import { Icosahedron, OrbitControls } from '@tresjs/cientos'
+
 import { TresCanvas } from '@tresjs/core'
 import {
   AgXToneMapping,
@@ -16,14 +17,13 @@ const gl = {
   toneMapping: AgXToneMapping,
   toneMappingExposure: 2.2,
   shadowMapType: PCFSoftShadowMap,
-  powerPreference: 'high-performance',
   antialias: true,
 }
 </script>
 
 <template>
   <TresCanvas clear-color="#ccc" v-bind="gl" window-size preset="realistic">
-    <StatsGl />
+    <!-- <StatsGl /> -->
     <TresPerspectiveCamera :position="[0, 0, 15]" :args="[45, 1, 0.1, 1000]" />
     <OrbitControls />
     <TresDirectionalLight

+ 1 - 1
playground/vue/src/pages/loaders/fbx-loader/TheExperience.vue

@@ -4,7 +4,7 @@ import TheModel from './TheModel.vue'
 </script>
 
 <template>
-  <TresPerspectiveCamera :position="[8, 8, 8]" />
+  <TresPerspectiveCamera :position="[8, 8, 8]" :look-at="[0, 0, 0]" />
   <OrbitControls />
   <TresGridHelper />
   <TresAmbientLight :intensity="1" />

+ 1 - 1
playground/vue/src/pages/loaders/gltf-loader/TheExperience.vue

@@ -4,7 +4,7 @@ import TheModel from './TheModel.vue'
 </script>
 
 <template>
-  <TresPerspectiveCamera :position="[8, 8, 8]" />
+  <TresPerspectiveCamera :position="[8, 8, 8]" :look-at="[0, 0, 0]" />
   <OrbitControls />
   <TresGridHelper />
   <TresAmbientLight :intensity="1" />

+ 2 - 2
playground/vue/src/pages/loaders/multiple-models/TheExperience.vue

@@ -4,7 +4,7 @@ import { OrbitControls } from '@tresjs/cientos'
 import { useLoader } from '@tresjs/core'
 import { LoadingManager } from 'three'
 import type { GLTF } from 'three/examples/jsm/Addons.js'
-import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader'
+import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
 
 const state = inject<{
   hasFinishLoading: boolean
@@ -28,7 +28,7 @@ const models = ref(modelPaths.map(path => useLoader<GLTF>(GLTFLoader, path, {
   manager,
 })))
 
-const computedIsLoading = computed(() => models.value.some(model => model.isLoading.value))
+const computedIsLoading = computed(() => models.value.some(model => model.isLoading))
 
 // Check if all models have loaded successfully and their scenes are available
 const allModelsLoaded = computed(() => models.value.every(model =>

+ 1 - 1
playground/vue/src/pages/loaders/texture-loader/TheExperience.vue

@@ -40,7 +40,7 @@ watch(texture, (newTexture) => {
 </script>
 
 <template>
-  <TresPerspectiveCamera :position="[3, 3, 3]" />
+  <TresPerspectiveCamera :position="[3, 3, 3]" :look-at="[0, 0, 0]" />
   <OrbitControls />
   <TresGridHelper />
   <TresAmbientLight :intensity="1" />

Файловите разлики са ограничени, защото са твърде много
+ 5 - 82
playground/vue/src/pages/misc/BrownianDistribution.vue


+ 147 - 0
playground/vue/src/pages/misc/BrownianDistributionExperience.vue

@@ -0,0 +1,147 @@
+<script setup lang="ts">
+import type { TresPointerEvent } from '@tresjs/core'
+import { OrbitControls } from '@tresjs/cientos'
+import { useLoop } from '@tresjs/core'
+import type { Mesh } from 'three'
+import { BoxGeometry, CylinderGeometry, Euler, MathUtils, MeshToonMaterial, SphereGeometry, Vector3 } from 'three'
+
+const lerp = MathUtils.lerp
+const clamp = MathUtils.clamp
+
+const COUNT = 2000
+
+const brownian = (stepSize: number, xMin: number, xMax: number, yMin: number, yMax: number, zMin: number, zMax: number) => {
+  let x = 0; let y = 0; let z = 0
+  const r = () => (Math.random() - 0.5) * 2 * stepSize
+  const isInBounds = () => xMin < x && x < xMax && yMin < y && y < yMax && zMin < z && z < zMax
+  const reset = () => {
+    x = lerp(xMin, xMax, Math.random())
+    y = lerp(yMin, yMax, Math.random())
+    z = lerp(zMin, zMax, Math.random())
+  }
+  reset()
+  return () => {
+    x += r()
+    y += r()
+    z += r()
+    if (!isInBounds()) { reset() }
+    return [x, y, z]
+  }
+}
+
+const getPosition = brownian(2, -60, 60, -40, 40, -30, 0)
+const getRotation = brownian(1, -20, 20, -10, 10, -20, 0)
+const cubePositions = Array.from({ length: COUNT }).map(() => new Vector3(...getPosition()))
+const cubeRotations = Array.from({ length: COUNT }).map(() => new Euler(...getRotation()))
+
+const pyramidRef = shallowRef({ position: new Vector3(), scale: new Vector3(1, 1, 1) })
+const boxRef = shallowRef({ position: new Vector3(), scale: new Vector3(1, 1, 1) })
+const sphereRef = shallowRef({ position: new Vector3(), scale: new Vector3(1, 1, 1) })
+
+const whiteMaterial = new MeshToonMaterial({ color: '#f8f8f8' })
+const orangeMaterial = new MeshToonMaterial({ color: '#eeac35' })
+const blueMaterial = new MeshToonMaterial({ color: '#7fdac6' })
+const grayMaterial = new MeshToonMaterial({ color: '#1e1f22' })
+const hoverMaterial = new MeshToonMaterial({ color: '#ffff00' })
+
+const sphereGeometry = new SphereGeometry()
+const cubeGeometry = new BoxGeometry()
+const pyramidGeometry = new CylinderGeometry(0, 0.6, 1)
+
+function onPointerEnter(ev: TresPointerEvent) {
+  if ((ev.eventObject as Mesh).material !== hoverMaterial) {
+    (ev.eventObject as Mesh).userData.material = (ev.eventObject as Mesh).material
+  }
+  (ev.eventObject as Mesh).material = hoverMaterial
+}
+
+function onPointerLeave(ev: TresPointerEvent) {
+  (ev.eventObject as Mesh).material = (ev.eventObject as Mesh).userData.material ?? grayMaterial
+}
+
+const PI = Math.PI
+
+const { onBeforeRender } = useLoop()
+
+onBeforeRender(({ elapsed }) => {
+  elapsed = elapsed * 3 + 7
+  pyramidRef.value.position.y = Math.tan(clamp((1 + elapsed) % 9, 0, PI))
+  boxRef.value.position.y = Math.tan(clamp((0.5 + elapsed) % 9, 0, PI))
+  sphereRef.value.position.y = Math.tan(clamp(elapsed % 9, 0, PI))
+
+  const scale0 = Math.abs(Math.cos(clamp((1 + elapsed) % 9, 0, PI)))
+  const scale1 = Math.abs(Math.cos(clamp((0.5 + elapsed) % 9, 0, PI)))
+  const scale2 = Math.abs(Math.cos(clamp(elapsed % 9, 0, PI)))
+  pyramidRef.value.scale.set(scale0, scale0, scale0)
+  boxRef.value.scale.set(scale1, scale1, scale1)
+  sphereRef.value.scale.set(scale2, scale2, scale2)
+})
+</script>
+
+<template>
+  <TresPerspectiveCamera
+    :position="[0, 0, 22]"
+    :fov="45"
+    :near="0.1"
+    :far="1000"
+    :look-at="[0, 5, 0]"
+  />
+  <OrbitControls />
+  <TresAmbientLight :intensity="0.5" />
+
+  <TresGroup>
+    <TresMesh
+      ref="pyramidRef"
+      :material="blueMaterial"
+      :position="[-1.5, 0, 0]"
+      @pointerenter="onPointerEnter"
+      @pointerleave="onPointerLeave"
+    >
+      <TresCylinderGeometry :args="[0, 0.60, 1]" />
+    </TresMesh>
+    <TresMesh
+      ref="boxRef"
+      cast-shadow
+      :material="whiteMaterial"
+      @pointerenter="onPointerEnter"
+      @pointerleave="onPointerLeave"
+    >
+      <TresBoxGeometry />
+    </TresMesh>
+
+    <TresMesh
+      ref="sphereRef"
+      :position="[1.5, 0, 0]"
+      cast-shadow
+      :material="orangeMaterial"
+      @pointerenter="onPointerEnter"
+      @pointerout="onPointerLeave"
+    >
+      <TresSphereGeometry :args="[0.5, 32, 32]" />
+    </TresMesh>
+  </TresGroup>
+
+  <TresGroup :position="[0, 0, -30]">
+    <TresMesh
+      v-for="position, i of cubePositions"
+      :key="i"
+      :geometry="[sphereGeometry, cubeGeometry, pyramidGeometry][i % 3]"
+      :material="grayMaterial"
+      :position="position"
+      :rotation="cubeRotations[i]"
+      @pointerenter="onPointerEnter"
+      @pointerleave="onPointerLeave"
+    />
+  </TresGroup>
+
+  <TresDirectionalLight
+    :position="[0, 8, 4]"
+    :intensity="0.7"
+    cast-shadow
+  />
+  <TresDirectionalLight
+    :position="[0, 2, 4]"
+    :intensity="1"
+    cast-shadow
+  />
+</template>

+ 3 - 2
playground/vue/src/pages/misc/use-graph/index.vue

@@ -13,8 +13,9 @@ const group = new Group()
 
 group.add(new Mesh(new BoxGeometry(1, 1, 1), new MeshStandardMaterial({ name: 'FancyMaterial', color: 'red' })))
 
-const { nodes, materials } = useGraph(group)
+const graph = useGraph(group)
 
+const { nodes, materials } = graph.value
 console.log('nodes', nodes)
 console.log('materials', materials)
 
@@ -23,7 +24,7 @@ materials.FancyMaterial.color.set('blue')
 
 <template>
   <TresCanvas v-bind="gl">
-    <TresPerspectiveCamera :position="[3, 3, 3]" />
+    <TresPerspectiveCamera :position="[3, 3, 3]" :look-at="[0, 0, 0]" />
     <OrbitControls />
     <TresGridHelper />
     <TresAmbientLight :intensity="1" />

+ 6 - 6
pnpm-lock.yaml

@@ -29,7 +29,7 @@ importers:
         version: 1.11.0
       '@tresjs/cientos':
         specifier: 5.0.0-next.0
-        version: 5.0.0-next.0(@tresjs/core@5.0.0-next.0(three@0.173.0)(typescript@5.8.2)(vue@3.5.13(typescript@5.8.2)))(@types/three@0.173.0)(three@0.173.0)(typescript@5.8.2)(vue@3.5.13(typescript@5.8.2))
+        version: 5.0.0-next.0(@tresjs/core@5.0.0-next.1(three@0.173.0)(typescript@5.8.2)(vue@3.5.13(typescript@5.8.2)))(@types/three@0.173.0)(three@0.173.0)(typescript@5.8.2)(vue@3.5.13(typescript@5.8.2))
       '@tresjs/eslint-config':
         specifier: ^1.4.0
         version: 1.4.0(@typescript-eslint/utils@8.33.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.2))(@vue/compiler-sfc@3.5.16)(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.2)(vitest@3.0.5)
@@ -1559,8 +1559,8 @@ packages:
       three: '>=0.133'
       vue: '>=3.3'
 
-  '@tresjs/core@5.0.0-next.0':
-    resolution: {integrity: sha512-AQom0UlFudxhlVpKaSjivF+8OpgZUdHsy/LxQ4VwUMJyGhqaVQfkb/og/5PLG45tdu8bNiklk8noQfEGL6ba9g==}
+  '@tresjs/core@5.0.0-next.1':
+    resolution: {integrity: sha512-FRV/f89stpjZ558qjTpP34aGsATZ5UF+m4n3sQ+kVy6n1/a3ravG5TAzJZmwNKM9Qql9zb/S4Cn5KsgMR2GAsA==}
     peerDependencies:
       three: '>=0.133'
       vue: '>=3.4'
@@ -6834,9 +6834,9 @@ snapshots:
       - react
       - typescript
 
-  '@tresjs/cientos@5.0.0-next.0(@tresjs/core@5.0.0-next.0(three@0.173.0)(typescript@5.8.2)(vue@3.5.13(typescript@5.8.2)))(@types/three@0.173.0)(three@0.173.0)(typescript@5.8.2)(vue@3.5.13(typescript@5.8.2))':
+  '@tresjs/cientos@5.0.0-next.0(@tresjs/core@5.0.0-next.1(three@0.173.0)(typescript@5.8.2)(vue@3.5.13(typescript@5.8.2)))(@types/three@0.173.0)(three@0.173.0)(typescript@5.8.2)(vue@3.5.13(typescript@5.8.2))':
     dependencies:
-      '@tresjs/core': 5.0.0-next.0(three@0.173.0)(typescript@5.8.2)(vue@3.5.13(typescript@5.8.2))
+      '@tresjs/core': 5.0.0-next.1(three@0.173.0)(typescript@5.8.2)(vue@3.5.13(typescript@5.8.2))
       '@vueuse/core': 12.8.2(typescript@5.8.2)
       camera-controls: 2.10.1(three@0.173.0)
       stats-gl: 2.4.2(@types/three@0.173.0)(three@0.173.0)
@@ -6851,7 +6851,7 @@ snapshots:
       - react
       - typescript
 
-  '@tresjs/core@5.0.0-next.0(three@0.173.0)(typescript@5.8.2)(vue@3.5.13(typescript@5.8.2))':
+  '@tresjs/core@5.0.0-next.1(three@0.173.0)(typescript@5.8.2)(vue@3.5.13(typescript@5.8.2))':
     dependencies:
       '@alvarosabu/utils': 3.2.0
       '@vue/devtools-api': 7.7.6

+ 23 - 6
src/components/TresCanvas.vue

@@ -7,7 +7,7 @@ import {
   WebGLRenderer,
 } from 'three'
 import type { App, Ref } from 'vue'
-import type { TresCamera, TresObject, TresScene } from '../types/'
+import type { TresCamera, TresContextWithClock, TresObject, TresScene } from '../types/'
 import type { PointerEvent } from '@pmndrs/pointer-events'
 import * as THREE from 'three'
 
@@ -27,7 +27,7 @@ import {
   watchEffect,
 } from 'vue'
 import pkg from '../../package.json'
-import type { RendererOptions, TresContext, TresRenderer } from '../composables'
+import type { RendererOptions, TresContext } from '../composables'
 import { useTresContextProvider } from '../composables'
 import { extend } from '../core/catalogue'
 import { nodeOps } from '../core/nodeOps'
@@ -57,12 +57,15 @@ const props = withDefaults(defineProps<TresCanvasProps>(), {
 
 const emit = defineEmits<{
   ready: [context: TresContext]
-  render: [renderer: TresRenderer]
   pointermissed: [event: PointerEvent<MouseEvent>]
+  render: [context: TresContext]
+  beforeLoop: [context: TresContextWithClock]
+  loop: [context: TresContextWithClock]
 } & {
   // all pointer events are supported because they bubble up
   [key in TresPointerEventName]: [event: PointerEvent<MouseEvent>]
-}>()
+}
+>()
 
 const slots = defineSlots<{
   default: () => any
@@ -214,8 +217,22 @@ onMounted(() => {
     addDefaultCamera()
   }
 
-  renderer.onRender((renderer) => {
-    emit('render', renderer)
+  renderer.onRender(() => {
+    if (context.value) {
+      emit('render', context.value)
+    }
+  })
+
+  renderer.loop.onLoop((loopContext) => {
+    if (context.value) {
+      emit('loop', { ...context.value, ...loopContext })
+    }
+  })
+
+  renderer.loop.onBeforeLoop((loopContext) => {
+    if (context.value) {
+      emit('beforeLoop', { ...context.value, ...loopContext })
+    }
   })
 
   renderer.onReady(() => {

+ 0 - 1
src/composables/index.ts

@@ -5,7 +5,6 @@ export * from './useGraph'
 export * from './useLoader'
 export * from './useLoop'
 export * from './useRenderer/useRendererManager'
-export * from './useRenderLoop'
 export * from './useTres'
 
 export * from './useTresContextProvider'

+ 60 - 0
src/composables/useCreateRafLoop/index.ts

@@ -0,0 +1,60 @@
+import { createEventHook, useRafFn } from '@vueuse/core'
+import { Clock } from 'three'
+
+export interface RafLoopContext { delta: number, elapsed: number }
+
+type LoopFunction = (notifySuccess: () => void) => void
+
+/**
+ * @param defaultFunction the default function that is called before after the after event hook is triggered and after the before is triggered.
+ * @param notifySuccess a callback that should be called to indicate a successfull cycle.
+ */
+export const useCreateRafLoop = (
+  defaultFunction: LoopFunction,
+  notifySuccess: () => void,
+) => {
+  const clock = new Clock()
+
+  let cycleFn: LoopFunction = defaultFunction
+
+  const eventHooks = {
+    before: createEventHook<RafLoopContext>(),
+    after: createEventHook<RafLoopContext>(),
+  }
+
+  const { pause, resume, isActive } = useRafFn(() => {
+    const getContextWithClock = (): RafLoopContext => ({
+      delta: clock.getDelta(),
+      elapsed: clock.getElapsedTime(),
+    })
+
+    eventHooks.before.trigger(getContextWithClock())
+    cycleFn(notifySuccess)
+    eventHooks.after.trigger(getContextWithClock())
+  }, {
+    immediate: false,
+  })
+
+  const start = () => {
+    clock.start()
+    resume()
+  }
+
+  const stop = () => {
+    clock.stop()
+    pause()
+  }
+
+  const replaceLoopFunction = (fn: LoopFunction) => {
+    cycleFn = fn
+  }
+
+  return {
+    start,
+    stop,
+    isActive,
+    onBeforeLoop: eventHooks.before.on,
+    onLoop: eventHooks.after.on,
+    replaceLoopFunction,
+  }
+}

+ 3 - 3
src/composables/useEventManager/index.ts

@@ -8,13 +8,13 @@ import { createEventHook } from '@vueuse/core'
 
 export function useEventManager({
   canvas,
-  contextParts: { scene, camera, loop },
+  contextParts: { scene, camera, renderer },
 }: {
   canvas: MaybeRef<HTMLCanvasElement>
-  contextParts: Pick<TresContext, 'scene' | 'camera' | 'loop' >
+  contextParts: Pick<TresContext, 'scene' | 'camera' | 'renderer' >
 }) {
   const { update, destroy } = forwardHtmlEvents(toValue(canvas), () => toValue(camera.activeCamera), scene.value)
-  const { off } = loop.register(update, 'before')
+  const { off } = renderer.loop.onLoop(update)
   onUnmounted(destroy)
   onUnmounted(off)
 

+ 27 - 35
src/composables/useLoop/index.ts

@@ -1,45 +1,37 @@
-import type { LoopCallbackFn } from './../../core/loop'
-import { useTresContext } from '../useTresContextProvider'
+import { useTresContext } from '..'
+import { createPriorityEventHook } from '../../utils/createPriorityEventHook'
+import type { RafLoopContext } from '../useCreateRafLoop'
+import type { TresPartialContext } from '../useTres'
+import { useTres } from '../useTres'
 
-export function useLoop() {
-  const {
-    camera,
-    scene,
-    renderer,
-    loop,
-    controls,
-    events,
-  } = useTresContext()
+export type LoopContext = RafLoopContext & TresPartialContext
 
-  // Pass context to loop
-  loop.setContext({
-    camera,
-    scene,
-    renderer: renderer.instance,
-    controls,
-    events,
-  })
+/**
+ * Composable that provides control over the render loop and animation lifecycle.
+ */
+export const useLoop = () => {
+  const tresContext = useTres()
+  const { renderer: rendererManager } = useTresContext()
 
-  function onBeforeRender(cb: LoopCallbackFn, index = 0) {
-    return loop.register(cb, 'before', index)
-  }
+  const eventHookBeforeRender = createPriorityEventHook<LoopContext>()
+  const eventHookAfterRender = createPriorityEventHook<LoopContext>()
 
-  function render(cb: LoopCallbackFn) {
-    return loop.register(cb, 'render')
-  }
+  rendererManager.loop.onBeforeLoop((loopContext) => {
+    eventHookBeforeRender.trigger({ ...tresContext, ...loopContext })
+  })
 
-  function onAfterRender(cb: LoopCallbackFn, index = 0) {
-    return loop.register(cb, 'after', index)
-  }
+  rendererManager.loop.onLoop((loopContext) => {
+    eventHookAfterRender.trigger({ ...tresContext, ...loopContext })
+  })
+
+  const render = rendererManager.loop.replaceLoopFunction
 
   return {
-    pause: loop.pause,
-    resume: loop.resume,
-    pauseRender: loop.pauseRender,
-    resumeRender: loop.resumeRender,
-    isActive: loop.isActive,
-    onBeforeRender,
+    stop: rendererManager.loop.stop,
+    start: rendererManager.loop.start,
+    isActive: rendererManager.loop.isActive,
+    onBeforeRender: eventHookBeforeRender.on,
+    onRender: eventHookAfterRender.on,
     render,
-    onAfterRender,
   }
 }

+ 80 - 0
src/composables/useLoop/useLoop.test.ts

@@ -0,0 +1,80 @@
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
+import { useLoop } from './index'
+import { useCreateRafLoop } from '../useCreateRafLoop'
+
+let loop: ReturnType<typeof useLoop>
+
+describe(useLoop.name, () => {
+  beforeEach(() => {
+    vi.useFakeTimers()
+    loop = useLoop()
+    vi.mock('../useTresContextProvider', () => ({
+      useTresContext: vi.fn(() => ({
+        camera: {},
+        scene: {},
+        renderer: {
+          loop: useCreateRafLoop(() => {}, () => {}),
+        },
+        controls: {},
+        events: {},
+      })),
+    }))
+  })
+  afterEach(() => {
+    loop.stop()
+    vi.useRealTimers()
+  })
+
+  it('should start and stop the loop', () => {
+    const requestAnimationFrameSpy = vi.spyOn(window, 'requestAnimationFrame')
+    requestAnimationFrameSpy.mockImplementation((_callback: FrameRequestCallback) => 0)
+    expect(loop.isActive.value).toBe(false)
+
+    loop.start()
+    expect(loop.isActive.value).toBe(true)
+
+    expect(requestAnimationFrameSpy).toHaveBeenCalled()
+    requestAnimationFrameSpy.mockClear()
+    loop.stop()
+    expect(loop.isActive.value).toBe(false)
+    expect(requestAnimationFrameSpy).not.toHaveBeenCalled()
+
+    requestAnimationFrameSpy.mockReset()
+  })
+
+  it('should call registered callbacks in the right order', async () => {
+    let toTest = ''
+
+    const add = (num: number) => () => { toTest += num }
+
+    loop.onBeforeRender(add(0), -1)
+    loop.onBeforeRender(add(1), 0)
+    loop.onBeforeRender(add(2), 1)
+
+    loop.onRender(add(3), -1)
+    loop.onRender(add(4), 0)
+    loop.onRender(add(5), 1)
+
+    // check if elements with the same index are called by insertion order
+    loop.onRender(add(6), 1)
+    loop.onRender(add(7), 1)
+
+    loop.start()
+    vi.advanceTimersToNextFrame()
+
+    expect(toTest).toBe('01234567')
+    vi.useRealTimers()
+  })
+
+  it('should be possible to replace the loop function', () => {
+    let toTest = 0
+    loop.start()
+    loop.render(() => {
+      toTest++
+    })
+    vi.advanceTimersToNextFrame()
+    vi.advanceTimersToNextFrame()
+    vi.advanceTimersToNextFrame()
+    expect(toTest).toBe(3)
+  })
+})

+ 0 - 61
src/composables/useRenderLoop/index.ts

@@ -1,61 +0,0 @@
-import type { EventHookOn, Fn } from '@vueuse/core'
-import type { Ref } from 'vue'
-import { createEventHook, useRafFn } from '@vueuse/core'
-import { Clock } from 'three'
-
-export interface RenderLoop {
-  delta: number
-  elapsed: number
-  clock: Clock
-}
-
-export interface UseRenderLoopReturn {
-  onBeforeLoop: EventHookOn<RenderLoop>
-  onLoop: EventHookOn<RenderLoop>
-  onAfterLoop: EventHookOn<RenderLoop>
-  pause: Fn
-  resume: Fn
-  isActive: Ref<boolean>
-}
-
-const onBeforeLoop = createEventHook<RenderLoop>()
-const onLoop = createEventHook<RenderLoop>()
-const onAfterLoop = createEventHook<RenderLoop>()
-
-const clock = new Clock()
-let delta = 0
-let elapsed = 0
-
-const { pause, resume, isActive } = useRafFn(
-  () => {
-    onBeforeLoop.trigger({ delta, elapsed, clock })
-    onLoop.trigger({ delta, elapsed, clock })
-    onAfterLoop.trigger({ delta, elapsed, clock })
-  },
-  { immediate: false },
-)
-
-onAfterLoop.on(() => {
-  delta = clock.getDelta()
-  elapsed = clock.getElapsedTime()
-})
-
-let startedOnce = false
-export const useRenderLoop = (): UseRenderLoopReturn => {
-  if (!startedOnce) {
-    // NOTE: `useRenderLoop` is not started by default
-    // in order not to waste user resources. Instead, we'll
-    // start the loop the first time the user uses
-    // `useRenderLoop`.
-    startedOnce = true
-    resume()
-  }
-  return {
-    onBeforeLoop: onBeforeLoop.on,
-    onLoop: onLoop.on,
-    onAfterLoop: onAfterLoop.on,
-    pause,
-    resume,
-    isActive,
-  }
-}

+ 26 - 23
src/composables/useRenderer/useRendererManager.ts

@@ -1,4 +1,3 @@
-import type { RendererLoop } from './../../core/loop'
 import type { ColorRepresentation, ColorSpace, Object3D, ShadowMapType, ToneMapping } from 'three'
 
 import type { TresContext } from '../useTresContextProvider'
@@ -7,10 +6,10 @@ import {
   createEventHook,
   unrefElement,
   useDevicePixelRatio,
+  useTimeout,
 } from '@vueuse/core'
 import { Material, Mesh, WebGLRenderer } from 'three'
-import { computed, onUnmounted, ref, toValue, watch, watchEffect } from 'vue'
-import type { MaybeRef, ShallowRef } from 'vue'
+import { computed, type MaybeRef, onUnmounted, type Reactive, ref, type ShallowRef, toValue, watch, watchEffect } from 'vue'
 import type { Renderer } from 'three/webgpu'
 
 // Solution taken from Thretle that actually support different versions https://github.com/threlte/threlte/blob/5fa541179460f0dadc7dc17ae5e6854d1689379e/packages/core/src/lib/lib/useRenderer.ts
@@ -21,6 +20,7 @@ import type { SizesType } from '../useSizes'
 import type { UseCameraReturn } from '../useCamera'
 import type { TresScene } from '../../types'
 import { isFunction, isObject } from '../../utils/is'
+import { useCreateRafLoop } from '../useCreateRafLoop'
 
 /**
  * If set to 'on-demand', the scene will only be rendered when the current frame is invalidated
@@ -188,15 +188,14 @@ export interface TresRendererSetupContext {
   sizes: SizesType
   scene: ShallowRef<TresScene>
   camera: UseCameraReturn
-  loop: RendererLoop
   canvas: MaybeRef<HTMLCanvasElement>
 }
 
 export interface UseRendererOptions {
   scene: ShallowRef<TresScene>
   canvas: MaybeRef<HTMLCanvasElement>
-  options: RendererOptions
-  contextParts: Pick<TresContext, 'sizes' | 'camera' | 'loop'>
+  options: Reactive<RendererOptions>
+  contextParts: Pick<TresContext, 'sizes' | 'camera'>
 }
 
 export function useRendererManager(
@@ -204,7 +203,7 @@ export function useRendererManager(
     scene,
     canvas,
     options,
-    contextParts: { sizes, loop, camera },
+    contextParts: { sizes, camera },
   }: UseRendererOptions,
 ) {
   const getRenderer = () => {
@@ -213,7 +212,6 @@ export function useRendererManager(
         sizes,
         scene,
         camera,
-        loop,
         canvas,
       })
     }
@@ -226,7 +224,7 @@ export function useRendererManager(
 
   const renderer = getRenderer()
 
-  const frames = ref(0)
+  const frames = ref(toValue(options.renderMode) === 'manual' ? 0 : 1) // 1 to make sure the first frame is rendered
   const maxFrames = 60
   const canBeInvalidated = computed(() => toValue(options.renderMode) === 'on-demand' && frames.value === 0)
 
@@ -267,8 +265,6 @@ export function useRendererManager(
 
   const isModeAlways = computed(() => toValue(options.renderMode) === 'always')
 
-  const renderEventHook = createEventHook<TresRenderer>()
-
   // be aware that the WebGLRenderer does not extend from Renderer
   const isRenderer = (value: unknown): value is Renderer =>
     isObject(value) && 'isRenderer' in value && Boolean(value.isRenderer)
@@ -282,17 +278,24 @@ export function useRendererManager(
     readyEventHook.trigger(renderer)
   }
 
-  loop.register(() => {
-    if (camera.activeCamera.value && frames.value) {
-      renderer.render(scene.value, camera.activeCamera.value)
-
-      renderEventHook.trigger(renderer)
-    }
+  const renderEventHook = createEventHook<TresRenderer>()
 
+  const notifyFrameRendered = () => {
     frames.value = isModeAlways.value
       ? 1
       : Math.max(0, frames.value - 1)
-  }, 'render')
+
+    renderEventHook.trigger(renderer)
+  }
+
+  const loop = useCreateRafLoop((_notifyFrameRendered) => {
+    if (camera.activeCamera.value && frames.value) {
+      renderer.render(scene.value, camera.activeCamera.value)
+      _notifyFrameRendered()
+    }
+  }, notifyFrameRendered)
+
+  readyEventHook.on(loop.start)
 
   // Watch the sizes and invalidate the renderer when they change
   watch([sizes.width, sizes.height], () => {
@@ -321,9 +324,9 @@ export function useRendererManager(
 
   if (toValue(options.renderMode) === 'manual') {
     // Advance for the first time, setTimeout to make sure there is something to render
-    setTimeout(() => {
-      advance()
-    }, 100)
+    useTimeout(100, {
+      callback: advance,
+    })
   }
 
   const clearColorAndAlpha = computed(() => {
@@ -399,13 +402,13 @@ export function useRendererManager(
   })
 
   return {
+    loop,
     instance: renderer,
     advance,
-    onRender: renderEventHook.on,
     onReady: readyEventHook.on,
+    onRender: renderEventHook.on,
     invalidate,
     canBeInvalidated,
-    frames,
     mode: toValue(options.renderMode),
   }
 }

+ 3 - 4
src/composables/useTres/index.ts

@@ -38,7 +38,7 @@ export interface TresPartialContext extends Omit<TresContext, 'renderer' | 'came
 }
 
 export function useTres(): TresPartialContext {
-  const { scene, renderer, camera, sizes, controls, loop, extend, events } = useTresContext()
+  const { scene, renderer, camera, sizes, controls, extend, events } = useTresContext()
 
   return {
     scene,
@@ -46,10 +46,9 @@ export function useTres(): TresPartialContext {
     camera: camera.activeCamera,
     sizes,
     controls,
-    loop,
     extend,
     events,
-    invalidate: () => renderer.invalidate(),
-    advance: () => renderer.advance(),
+    invalidate: renderer.invalidate,
+    advance: renderer.advance,
   }
 }

+ 3 - 20
src/composables/useTresContextProvider/index.ts

@@ -1,11 +1,9 @@
 import type { MaybeRef, MaybeRefOrGetter, Ref, ShallowRef } from 'vue'
 
-import type { RendererLoop } from '../../core/loop'
 import type { TresControl, TresScene } from '../../types'
 import type { RendererOptions, UseRendererManagerReturn } from '../useRenderer/useRendererManager'
-import { inject, onUnmounted, provide, ref, shallowRef } from 'vue'
+import { inject, provide, ref, shallowRef } from 'vue'
 import { extend } from '../../core/catalogue'
-import { createRenderLoop } from '../../core/loop'
 
 import type { UseCameraReturn } from '../useCamera/'
 
@@ -21,7 +19,6 @@ export interface TresContext {
   camera: UseCameraReturn
   controls: Ref<TresControl | null>
   renderer: UseRendererManagerReturn
-  loop: RendererLoop
   events: ReturnType<typeof useEventManager>
 }
 
@@ -41,20 +38,18 @@ export function useTresContextProvider({
 
   const camera = useCameraManager({ sizes })
 
-  const loop = createRenderLoop()
-
   const renderer = useRendererManager(
     {
       scene: localScene,
       canvas,
       options: rendererOptions,
-      contextParts: { sizes, camera, loop },
+      contextParts: { sizes, camera },
     },
   )
 
   const events = useEventManager({
     canvas,
-    contextParts: { scene: localScene, camera, loop },
+    contextParts: { scene: localScene, camera, renderer },
   })
 
   const ctx: TresContext = {
@@ -64,7 +59,6 @@ export function useTresContextProvider({
     renderer,
     controls: ref(null),
     extend,
-    loop,
     events,
   }
 
@@ -75,17 +69,6 @@ export function useTresContextProvider({
     root: ctx,
   }
 
-  ctx.loop.setReady(false)
-  ctx.loop.start()
-
-  renderer.onReady(() => {
-    ctx.loop.setReady(true)
-  })
-
-  onUnmounted(() => {
-    ctx.loop.stop()
-  })
-
   return ctx
 }
 

+ 0 - 274
src/core/loop.test.ts

@@ -1,274 +0,0 @@
-import type { TresContext } from '../composables/useTresContextProvider'
-import { afterEach, beforeEach, it } from 'vitest'
-import { createRenderLoop } from './loop'
-
-let renderLoop
-
-describe('createRenderLoop', () => {
-  beforeEach(() => {
-    renderLoop = createRenderLoop({} as TresContext)
-  })
-  afterEach(() => {
-    renderLoop.stop()
-  })
-
-  it('should start and stop the loop', () => {
-    // Spy
-    const requestAnimationFrameSpy = vi.spyOn(window, 'requestAnimationFrame')
-    requestAnimationFrameSpy.mockImplementation((_callback: FrameRequestCallback) => {
-      return 0 // Return a number as a placeholder
-    })
-
-    renderLoop.start()
-    expect(requestAnimationFrameSpy).toHaveBeenCalled()
-    requestAnimationFrameSpy.mockClear()
-    renderLoop.stop()
-    expect(requestAnimationFrameSpy).not.toHaveBeenCalled()
-  })
-
-  it('should pause and resume the loop', () => {
-    renderLoop.start()
-    renderLoop.pause()
-    expect(renderLoop.isActive.value).toBe(false)
-    renderLoop.resume()
-    expect(renderLoop.isActive.value).toBe(true)
-  })
-
-  it('should pause and resume the renderer', () => {
-    renderLoop.start()
-    renderLoop.pauseRender()
-    expect(renderLoop.isRenderPaused.value).toBe(true)
-    renderLoop.resumeRender()
-    expect(renderLoop.isRenderPaused.value).toBe(false)
-  })
-
-  it('should register a callback before render', () => {
-    let result = ''
-    const callback = () => { result += '0' }
-    renderLoop.register(callback, 'before')
-    renderLoop.start()
-    expect(result).toBe('0')
-  })
-
-  it('should register callbacks in order before render', () => {
-    const callbackIndexes = []
-    const callback1 = () => { callbackIndexes.push(-1) }
-    const callback2 = () => { callbackIndexes.push(0) }
-    const callback3 = () => { callbackIndexes.push(1) }
-    const callback4 = () => { callbackIndexes.push(2) }
-    renderLoop.register(callback2, 'before')
-    renderLoop.register(callback1, 'before', -1)
-    renderLoop.register(callback3, 'before')
-    renderLoop.register(callback4, 'before', 2)
-    renderLoop.start()
-    expect(callbackIndexes).toStrictEqual([-1, 0, 1, 2])
-  })
-
-  it('should register a callback for render', () => {
-    let result = ''
-    const callback = () => { result += '0' }
-    renderLoop.register(callback, 'render')
-    renderLoop.start()
-    expect(result).toBe('0')
-  })
-
-  it('should take over the render loop', async () => {
-    let result = ''
-    const originalRenderCallback = () => { result = 'original' }
-    const takeOver = () => { result = 'takeover' }
-
-    renderLoop.register(originalRenderCallback, 'render')
-    renderLoop.register(takeOver, 'render')
-
-    renderLoop.start()
-    expect(result).toBe('takeover')
-  })
-
-  it('does not register the same callback twice', () => {
-    let result = ''
-    const callback1 = () => { result += '1' }
-    renderLoop.register(callback1, 'before', 0)
-    renderLoop.register(callback1, 'before', 0)
-    renderLoop.start()
-    renderLoop.stop()
-    expect(result).toEqual('1')
-  })
-
-  it('should register a callback after render', () => {
-    let result = ''
-    const callback = () => { result += '0' }
-    renderLoop.register(callback, 'after')
-    renderLoop.start()
-    expect(result).toBe('0')
-  })
-
-  it('should render first all before render callbacks, then render callbacks, and finally after render callbacks', async () => {
-    const executionOrder = []
-    const beforeCb = () => { executionOrder.push('before') }
-    const fboCb = () => { executionOrder.push('fbo') }
-    const renderCb = () => { executionOrder.push('render') }
-    const afterCb = () => { executionOrder.push('after') }
-    renderLoop.register(beforeCb, 'before')
-    renderLoop.register(fboCb, 'before', Number.POSITIVE_INFINITY)
-    renderLoop.register(renderCb, 'render')
-    renderLoop.register(afterCb, 'after', -1)
-
-    renderLoop.start()
-    renderLoop.stop()
-
-    expect(executionOrder).toEqual(['before', 'fbo', 'render', 'after'])
-  })
-
-  describe('`stop`, `start`, `pause`, `resume` call order', () => {
-    it('does not trigger a callback on `start()` unless `stop()`ped', () => {
-      const callbackBefore = vi.fn()
-      const callbackRender = vi.fn()
-      const callbackAfter = vi.fn()
-      renderLoop.register(callbackBefore, 'before')
-      renderLoop.register(callbackRender, 'render')
-      renderLoop.register(callbackAfter, 'after')
-      renderLoop.start()
-      expect(callbackBefore).toBeCalledTimes(1)
-      expect(callbackRender).toBeCalledTimes(1)
-      expect(callbackAfter).toBeCalledTimes(1)
-
-      renderLoop.start()
-      renderLoop.start()
-      renderLoop.start()
-      renderLoop.start()
-      expect(callbackBefore).toBeCalledTimes(1)
-      expect(callbackRender).toBeCalledTimes(1)
-      expect(callbackAfter).toBeCalledTimes(1)
-
-      renderLoop.stop()
-      renderLoop.start()
-      expect(callbackBefore).toBeCalledTimes(2)
-      expect(callbackRender).toBeCalledTimes(2)
-      expect(callbackAfter).toBeCalledTimes(2)
-    })
-
-    it('can `start()` even if `resume()`d while `stop()`ped', () => {
-      const callbackBefore = vi.fn()
-      const callbackRender = vi.fn()
-      const callbackAfter = vi.fn()
-      renderLoop.register(callbackBefore, 'before')
-      renderLoop.register(callbackRender, 'render')
-      renderLoop.register(callbackAfter, 'after')
-      renderLoop.stop()
-      renderLoop.resume()
-      expect(callbackBefore).toBeCalledTimes(0)
-      expect(callbackRender).toBeCalledTimes(0)
-      expect(callbackAfter).toBeCalledTimes(0)
-
-      renderLoop.start()
-      expect(callbackBefore).toBeCalledTimes(1)
-      expect(callbackRender).toBeCalledTimes(1)
-      expect(callbackAfter).toBeCalledTimes(1)
-    })
-
-    it('`isActive.value` is `true` only if both `start()`ed and `resume()`d, regardless of call order', () => {
-      const callbackBefore = vi.fn()
-      const callbackRender = vi.fn()
-      const callbackAfter = vi.fn()
-      renderLoop.register(callbackBefore, 'before')
-      renderLoop.register(callbackRender, 'render')
-      renderLoop.register(callbackAfter, 'after')
-
-      const { start, stop, resume, pause } = renderLoop
-
-      // NOTE: stop, pause | stop, resume | start, resume
-      // NOTE: stop, pause
-      stop()
-      pause()
-      expect(renderLoop.isActive.value).toBe(false)
-      // NOTE: stop, resume
-      resume()
-      expect(renderLoop.isActive.value).toBe(false)
-      // NOTE: start, resume
-      start()
-      expect(renderLoop.isActive.value).toBe(true)
-
-      // NOTE: stop, pause | start, pause | start, resume
-      // NOTE: stop, pause
-      stop()
-      pause()
-      expect(renderLoop.isActive.value).toBe(false)
-      // NOTE: start, pause
-      start()
-      expect(renderLoop.isActive.value).toBe(false)
-      // NOTE: start, resume
-      resume()
-      expect(renderLoop.isActive.value).toBe(true)
-
-      // NOTE: start, resume | start, pause | start, resume
-      // NOTE: start, resume
-      resume()
-      start()
-      expect(renderLoop.isActive.value).toBe(true)
-      // NOTE: start, pause
-      pause()
-      expect(renderLoop.isActive.value).toBe(false)
-      // NOTE: start, resume
-      resume()
-      expect(renderLoop.isActive.value).toBe(true)
-
-      // NOTE: start, resume | stop, resume | start, resume
-      // NOTE: start, resume
-      resume()
-      start()
-      expect(renderLoop.isActive.value).toBe(true)
-      // NOTE: stop, resume
-      stop()
-      expect(renderLoop.isActive.value).toBe(false)
-      // NOTE: start, resume
-      start()
-      expect(renderLoop.isActive.value).toBe(true)
-
-      // NOTE: make some random calls
-      const ons = [start, resume]
-      const offs = [stop, pause]
-      const onsAndOffs = [start, stop, resume, pause]
-      const TEST_COUNT = 100
-
-      for (let i = 0; i < TEST_COUNT; i++) {
-        const ARRAY_COUNT = 25 + Math.floor(Math.random() * 10)
-        const _offs = Array.from({ length: ARRAY_COUNT }).fill(0).map(() => choose(offs))
-        _offs.forEach(fn => fn())
-        expect(renderLoop.isActive.value).toBe(false)
-        shuffle(ons)
-        ons.forEach(fn => fn())
-        expect(renderLoop.isActive.value).toBe(true)
-      }
-
-      for (let i = 0; i < TEST_COUNT; i++) {
-        const ARRAY_COUNT = 25 + Math.floor(Math.random() * 10)
-        const _onsAndOffs = Array.from({ length: ARRAY_COUNT }).fill(0).map(() => choose(onsAndOffs))
-        _onsAndOffs.forEach(fn => fn())
-        shuffle(offs)
-        offs[0]()
-        expect(renderLoop.isActive.value).toBe(false)
-        shuffle(ons)
-        ons.forEach(fn => fn())
-        expect(renderLoop.isActive.value).toBe(true)
-      }
-    })
-  })
-})
-
-function choose(array: any[]) {
-  const i = Math.floor(Math.random() * array.length)
-  return array[i]
-}
-
-function shuffle(array: any[]) {
-  let currentIndex = array.length
-  while (currentIndex !== 0) {
-    const randomIndex = Math.floor(Math.random() * currentIndex)
-    currentIndex--;
-    [array[currentIndex], array[randomIndex]] = [
-      array[randomIndex],
-      array[currentIndex],
-    ]
-  }
-  return array
-};

+ 0 - 185
src/core/loop.ts

@@ -1,185 +0,0 @@
-import type { Fn } from '@vueuse/core'
-import type { Camera, EventDispatcher, Raycaster, Scene, WebGLRenderer } from 'three'
-import type { Ref } from 'vue'
-import type { Callback } from '../utils/createPriorityEventHook'
-import { Clock, MathUtils } from 'three'
-import { ref, unref } from 'vue'
-import { createPriorityEventHook } from '../utils/createPriorityEventHook'
-
-export type LoopStage = 'before' | 'render' | 'after'
-
-export interface LoopCallback {
-  delta: number
-  elapsed: number
-  clock: Clock
-}
-
-export interface LoopCallbackWithCtx extends LoopCallback {
-  camera: Camera
-  scene: Scene
-  renderer: WebGLRenderer
-  raycaster: Raycaster
-  controls: Ref<(EventDispatcher<object> & {
-    enabled: boolean
-  }) | null>
-}
-
-export type LoopCallbackFn = (params: LoopCallbackWithCtx) => void
-
-export interface RendererLoop {
-  loopId: string
-  register: (callback: LoopCallbackFn, stage: LoopStage, index?: number) => { off: Fn }
-  start: Fn
-  stop: Fn
-  pause: Fn
-  resume: Fn
-  pauseRender: Fn
-  resumeRender: Fn
-  isActive: Ref<boolean>
-  isRenderPaused: Ref<boolean>
-  setContext: (newContext: Record<string, any>) => void
-  setReady: (isReady: boolean) => void
-}
-
-export function createRenderLoop(): RendererLoop {
-  let isReady = true
-  let isStopped = true
-  let isPaused = false
-  const clock = new Clock(false)
-  const isActive = ref(clock.running)
-  const isRenderPaused = ref(false)
-  let animationFrameId: number
-  const loopId = MathUtils.generateUUID()
-  let defaultRenderFn: Callback<LoopCallbackWithCtx> | null = null
-  const subscribersBefore = createPriorityEventHook<LoopCallbackWithCtx>()
-  const subscriberRender = createPriorityEventHook<LoopCallbackWithCtx>()
-  const subscribersAfter = createPriorityEventHook<LoopCallbackWithCtx>()
-
-  _syncState()
-
-  // Context to be passed to callbacks
-  let context: Record<string, any> = {}
-
-  function setContext(newContext: Record<string, any>) {
-    context = newContext
-  }
-
-  function registerCallback(callback: LoopCallbackFn, stage: 'before' | 'render' | 'after', index = 0): { off: Fn } {
-    switch (stage) {
-      case 'before':
-        return subscribersBefore.on(callback, index)
-      case 'render':
-        if (!defaultRenderFn) {
-          defaultRenderFn = callback
-        }
-        subscriberRender.dispose()
-        return subscriberRender.on(callback)
-      case 'after':
-        return subscribersAfter.on(callback, index)
-    }
-  }
-
-  function start() {
-    // NOTE: `loop()` produces side effects on each call.
-    // Those side effects are only desired if `isStopped` goes
-    // from `true` to `false` below.  So while we don't need
-    // a guard in `stop`, `resume`, and `pause`, we do need
-    // a guard here.
-    if (!isStopped) { return }
-    isStopped = false
-    _syncState()
-    loop()
-  }
-
-  function stop() {
-    isStopped = true
-    _syncState()
-    cancelAnimationFrame(animationFrameId)
-  }
-
-  function resume() {
-    isPaused = false
-    _syncState()
-  }
-
-  function pause() {
-    isPaused = true
-    _syncState()
-  }
-
-  function pauseRender() {
-    isRenderPaused.value = true
-  }
-
-  function resumeRender() {
-    isRenderPaused.value = false
-  }
-
-  function loop() {
-    if (!isReady) {
-      animationFrameId = requestAnimationFrame(loop)
-      return
-    }
-    const delta = clock.getDelta()
-    const elapsed = clock.getElapsedTime()
-    const snapshotCtx = {
-      camera: unref(context.camera?.activeCamera),
-      scene: unref(context.scene),
-      renderer: context.renderer,
-      raycaster: unref(context.raycaster),
-      controls: unref(context.controls),
-      invalidate: context.invalidate,
-      advance: context.advance,
-    }
-    const params = { delta, elapsed, clock, ...snapshotCtx }
-
-    if (isActive.value) {
-      subscribersBefore.trigger(params)
-    }
-
-    if (!isRenderPaused.value) {
-      if (subscriberRender.count) {
-        subscriberRender.trigger(params)
-      }
-      else {
-        if (defaultRenderFn) {
-          defaultRenderFn(params) // <-- keep the default render function separate
-        }
-      }
-    }
-
-    if (isActive.value) {
-      subscribersAfter.trigger(params)
-    }
-
-    animationFrameId = requestAnimationFrame(loop)
-  }
-
-  function _syncState() {
-    const shouldClockBeRunning = !isStopped && !isPaused
-    if (clock.running !== shouldClockBeRunning) {
-      if (!clock.running) {
-        clock.start()
-      }
-      else {
-        clock.stop()
-      }
-    }
-    isActive.value = clock.running
-  }
-
-  return {
-    loopId,
-    register: (callback: LoopCallbackFn, stage: 'before' | 'render' | 'after', index) => registerCallback(callback, stage, index),
-    start,
-    stop,
-    pause,
-    resume,
-    pauseRender,
-    resumeRender,
-    isRenderPaused,
-    isActive,
-    setContext,
-    setReady: (b: boolean) => isReady = b,
-  }
-}

+ 0 - 1
src/index.ts

@@ -7,7 +7,6 @@ import templateCompilerOptions from './utils/template-compiler-options'
 export * from './components'
 export * from './composables'
 export * from './core/catalogue'
-export * from './core/loop'
 export * from './directives'
 export * from './types'
 export * from './utils/graph'

+ 1 - 0
src/types/index.ts

@@ -121,6 +121,7 @@ export type TresLayers = THREE.Layers | Parameters<THREE.Layers['set']>[0]
 export type TresQuaternion = THREE.Quaternion | Parameters<THREE.Quaternion['set']>
 export type TresEuler = THREE.Euler
 export type TresControl = THREE.EventDispatcher & { enabled: boolean }
+export type TresContextWithClock = TresContext & { delta: number, elapsed: number }
 
 export type WithMathProps<P> = { [K in keyof P]: P[K] extends MathRepresentation | THREE.Euler ? MathType<P[K]> : P[K] }
 

+ 7 - 12
src/utils/createPriorityEventHook.ts

@@ -1,21 +1,13 @@
-import type { EventHookOff, IsAny } from '@vueuse/core'
+import type { EventHookOff, EventHookTrigger } from '@vueuse/core'
 import { tryOnScopeDispose } from '@vueuse/core'
 
 // NOTE: Based on vueuse's createEventHook
 // https://github.com/vueuse/vueuse/blob/1558cd2b5b019abc1feda6d702caa1053a182903/packages/shared/createEventHook/index.ts
 
-// NOTE: any extends void = true
-// So we need to check if T is any first
-export type Callback<T> = IsAny<T> extends true
-  ? (param: any) => void
-  : (
-      [T] extends [void]
-        ? () => void
-        : (param: T) => void
-    )
+export type Callback<T> = Parameters<EventHookOff<T>>[0]
 export type PriorityEventHookOn<T> = (fn: Callback<T>, priority?: number) => { off: () => void }
 export type PriorityEventHookOff<T> = EventHookOff<T>
-export type PriorityEventHookTrigger<T = any> = (param?: T) => void
+export type PriorityEventHookTrigger<T> = EventHookTrigger<T>
 
 export interface PriorityEventHook<T = any> {
   on: PriorityEventHookOn<T>
@@ -61,7 +53,10 @@ export function createPriorityEventHook<T>(): PriorityEventHook<T> {
       sort()
       dirty = false
     }
-    ascending.forEach(fn => fn(...(args as [T])))
+
+    return Promise.all(
+      Array.from(ascending).map(fn => fn(...args)),
+    )
   }
 
   const dispose = () => {

+ 0 - 1
vite.config.ts

@@ -43,7 +43,6 @@ export default defineConfig({
   test: {
     environment: 'jsdom',
     globals: true,
-    threads: false,
   },
   build: {
     // vite.config.ts

Някои файлове не бяха показани, защото твърде много файлове са промени