浏览代码

Merge branch 'next' into chore/985-add-usecamera-return-object-to-state-instead-of-spreading-it

alvarosabu 2 周之前
父节点
当前提交
e015e5d7cb
共有 32 个文件被更改,包括 215 次插入839 次删除
  1. 1 1
      docs/.vitepress/theme/components/BlenderCube.vue
  2. 1 1
      docs/advanced/attach.md
  3. 7 7
      docs/advanced/performance.md
  4. 8 10
      docs/api/composables.md
  5. 1 1
      docs/cookbook/orbit-controls.md
  6. 1 1
      playground/vue/src/pages/advanced/devicePixelRatio/index.vue
  7. 3 3
      playground/vue/src/pages/advanced/manual/experience.vue
  8. 3 3
      playground/vue/src/pages/advanced/on-demand/experience.vue
  9. 3 1
      playground/vue/src/pages/advanced/suspense/AsyncComponent.vue
  10. 1 1
      playground/vue/src/pages/advanced/suspense/index.vue
  11. 1 1
      playground/vue/src/pages/advanced/takeOverRender/TakeOverRenderExperience.vue
  12. 0 62
      playground/vue/src/pages/basic/ready/LoopCallbackWatcher.vue
  13. 0 69
      playground/vue/src/pages/basic/ready/OnTresReadyWatcher.vue
  14. 0 163
      playground/vue/src/pages/basic/ready/index.vue
  15. 0 5
      playground/vue/src/router/routes/basic.ts
  16. 40 25
      src/components/TresCanvas.vue
  17. 1 2
      src/composables/index.ts
  18. 1 1
      src/composables/useCamera/index.ts
  19. 0 1
      src/composables/useLoader/component.vue
  20. 1 5
      src/composables/useLoop/index.ts
  21. 1 1
      src/composables/useRaycaster/index.ts
  22. 91 29
      src/composables/useRenderer/useRendererManager.ts
  23. 3 3
      src/composables/useSizes/index.ts
  24. 22 97
      src/composables/useTresContextProvider/index.ts
  25. 13 7
      src/composables/useTresEventManager/index.ts
  26. 0 187
      src/composables/useTresReady/createReadyEventHook/createReadyHook.test.ts
  27. 0 87
      src/composables/useTresReady/createReadyEventHook/index.ts
  28. 0 53
      src/composables/useTresReady/index.ts
  29. 0 2
      src/core/loop.ts
  30. 5 5
      src/devtools/plugin.ts
  31. 4 2
      src/types/index.ts
  32. 3 3
      src/utils/index.ts

+ 1 - 1
docs/.vitepress/theme/components/BlenderCube.vue

@@ -9,7 +9,7 @@ model.position.set(0, 1, 0)
 
 const state = useTresContext()
 
-state.invalidate()
+state.renderer.invalidate()
 </script>
 
 <template>

+ 1 - 1
docs/advanced/attach.md

@@ -170,7 +170,7 @@ useLoop().render(() => {
 <template>
   <TresEffectComposer
     ref="composer"
-    :args="[renderer]"
+    :args="[renderer.instance]"
     :set-size="[sizes.width.value, sizes.height.value]"
   >
     <TresRenderPass

+ 7 - 7
docs/advanced/performance.md

@@ -57,7 +57,7 @@ setTimeout(() => {
 
 #### Manual Invalidation
 
-Since it is not really possible to observe all the possible changes in your application, you can also manually invalidate the frame by calling the `invalidate()` method from the [`useTresContext` composable](../api/composables.md#usetrescontext):
+Since it is not really possible to observe all the possible changes in your application, you can also manually invalidate the frame by calling the `invalidate()` method from `renderer` provided by the [`useTresContext` composable](../api/composables.md#usetrescontext):
 
 ::: code-group
 
@@ -69,7 +69,7 @@ import Scene from './Scene.vue'
 
 <template>
   <TresCanvas
-    render-mode="manual"
+    render-mode="on-demand"
   >
     <Scene />
   </TresCanvas>
@@ -82,13 +82,13 @@ import { useTres } from '@tresjs/core'
 import { shallowRef, watch } from 'vue'
 
 const boxRef = shallowRef(null)
-const { invalidate } = useTres()
+const { renderer } = useTres()
 
 watch(boxRef, () => {
   if (boxRef.value?.position) {
     boxRef.value.position.x = 1
   }
-  invalidate()
+  renderer.invalidate()
 })
 </script>
 
@@ -116,15 +116,15 @@ If you want to have full control of when the scene is rendered, you can set the
 </TresCanvas>
 ```
 
-In this mode, Tres will not render the scene automatically. You will need to call the `advance()` method from the [`useTresContext` composable](../api/composables.md#usetrescontext) to render the scene:
+In this mode, Tres will not render the scene automatically. You will need to call the `advance()` method from `renderer` provided by the [`useTresContext` composable](../api/composables.md#usetrescontext) to render the scene:
 
 ```vue
 <script setup>
 import { useTres } from '@tresjs/core'
 
-const { advance } = useTres()
+const { renderer } = useTres()
 
-advance()
+renderer.advance()
 </script>
 ```
 

+ 8 - 10
docs/api/composables.md

@@ -47,19 +47,17 @@ const context = useTresContext()
 ### Properties of context
 | Property | Description |
 | --- | --- |
-| **camera** | The currently active camera |
-| **cameras** | The cameras that exist in the scene |
-| **controls** | The controls of your scene |
+| **camera** | the currently active camera |
+| **cameras** | the cameras that exist in the scene |
+| **controls** | the controls of your scene |
 | **deregisterCamera** | A method to deregister a camera. This is only required if you manually create a camera. Cameras in the template are deregistered automatically. |
-| **extend** | Extends the component catalogue. See [extending](/advanced/extending) |
+| **extend** | Extends the component catalogue. See [extending](/advanced/extending). |
 | **raycaster** | the global raycaster used for pointer events |
-| **registerCamera** | a method to register a camera. This is only required if you manually create a camera. Cameras in the template are registered automatically. |
-| **renderer** | the [WebGLRenderer](https://threejs.org/docs/#api/en/renderers/WebGLRenderer) of your scene |
-| **scene** | the [scene](https://threejs.org/docs/?q=sce#api/en/scenes/Scene). |
+| **registerCamera** | A method to register a camera. This is only required if you manually create a camera. Cameras in the template are registered automatically. |
+| **renderer** | Contains the [WebGLRenderer](https://threejs.org/docs/#api/en/renderers/WebGLRenderer) instance of your scene, a method the invalidate the render loop (only required if you set the `render-mode` prop to `on-demand`), a computed that indicates whether invalidating is possible and a method to advance the render loop (only required if you set the `render-mode` prop to `manual`). |
+| **scene** | the [scene](https://threejs.org/docs/?q=sce#api/en/scenes/Scene) |
 | **setCameraActive** | a method to set a camera active |
 | **sizes** | contains width, height and aspect ratio of your canvas |
-| **invalidate** | a method to invalidate the render loop. This is only required if you set the `render-mode` prop to `on-demand`. |
-| **advance** | a method to advance the render loop. This is only required if you set the `render-mode` prop to `manual`. |
 | **loop** | the renderer loop |
 
 ### useLoop <Badge text="v4.0.0" />
@@ -123,7 +121,7 @@ You can take over the render call by using the `render` method.
 const { render } = useLoop()
 
 render(({ renderer, scene, camera }) => {
-  renderer.render(scene, camera)
+  renderer.instance.value.render(scene, camera)
 })
 ```
 

+ 1 - 1
docs/cookbook/orbit-controls.md

@@ -80,7 +80,7 @@ const { camera, renderer } = useTresContext()
 <template>
   <TresOrbitControls
     v-if="renderer"
-    :args="[camera, renderer?.domElement]"
+    :args="[camera, renderer.instance.domElement]"
   />
 </template>
 ```

+ 1 - 1
playground/vue/src/pages/advanced/devicePixelRatio/index.vue

@@ -10,7 +10,7 @@ const currDprRef = shallowRef(-1)
 const dpr = shallowRef<number | [number, number]>([minDpr, maxDpr])
 
 const onReady = ({ renderer }) => {
-  rendererRef.value = renderer.value
+  rendererRef.value = renderer.instance.value
 }
 
 const isRendererDprClamped = (renderer: WebGLRenderer) => {

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

@@ -3,10 +3,10 @@ import { OrbitControls } from '@tresjs/cientos'
 import { useTres } from '@tresjs/core'
 import BlenderCube from '../../../components/BlenderCube.vue'
 
-const { advance } = useTres()
+const { renderer } = useTres()
 
 onMounted(() => {
-  advance()
+  renderer.advance()
 })
 </script>
 
@@ -18,7 +18,7 @@ onMounted(() => {
   <BlenderCube />
 
   <TresGridHelper />
-  <OrbitControls @change="advance" />
+  <OrbitControls @change="renderer.advance" />
   <TresAmbientLight :intensity="1" />
   <TresDirectionalLight
     :position="[0, 8, 4]"

+ 3 - 3
playground/vue/src/pages/advanced/on-demand/experience.vue

@@ -4,17 +4,17 @@ import { useTres } from '@tresjs/core'
 import { ref, watch } from 'vue'
 import BlenderCube from '../../../components/BlenderCube.vue'
 
-const { invalidate } = useTres()
+const { renderer } = useTres()
 
 const blenderCubeRef = ref()
 
 watch(blenderCubeRef, (prev, next) => {
   if (!next) { return }
-  invalidate()
+  renderer.invalidate()
 })
 
 function onControlChange() {
-  invalidate()
+  renderer.invalidate()
 }
 </script>
 

+ 3 - 1
playground/vue/src/pages/advanced/suspense/AsyncComponent.vue

@@ -3,7 +3,9 @@ import { useGLTF } from '@tresjs/cientos'
 import { useTresContext } from '@tresjs/core'
 
 const { scene } = await useGLTF('https://raw.githubusercontent.com/Tresjs/assets/main/models/gltf/blender-cube.glb', { draco: true })
-useTresContext().invalidate()
+const { renderer } = useTresContext()
+
+renderer.invalidate()
 </script>
 
 <template>

+ 1 - 1
playground/vue/src/pages/advanced/suspense/index.vue

@@ -13,7 +13,7 @@ const { show } = useControls({
     label: 'Render dispose',
     type: 'button',
     onClick() {
-      ctx?.value?.dispose()
+      ctx?.value?.renderer?.instance?.value?.dispose()
     },
   },
 })

+ 1 - 1
playground/vue/src/pages/advanced/takeOverRender/TakeOverRenderExperience.vue

@@ -7,7 +7,7 @@ import { useControls } from '@tresjs/leches'
 const { render, pauseRender, resumeRender } = useLoop()
 
 const { off } = render(({ renderer, scene, camera }) => {
-  renderer.render(scene, camera)
+  renderer.instance.value.render(scene, camera)
 })
 
 const { isRenderPaused, unregisterRender } = useControls({

+ 0 - 62
playground/vue/src/pages/basic/ready/LoopCallbackWatcher.vue

@@ -1,62 +0,0 @@
-<script setup lang="ts">
-import { useLoop } from '@tresjs/core'
-import { ref, type ShallowRef, shallowRef } from 'vue'
-
-const isCalled = ref(false)
-
-interface TestResult { passed: boolean, msg: string }
-const messages = shallowRef([
-  {
-    passed: false,
-    msg: 'callback was not called',
-  },
-]) as ShallowRef<TestResult[]>
-
-const captureCallback = (renderer: any, _elapsed: number) => {
-  if (!isCalled.value) {
-    isCalled.value = true
-    const isRendererOk = !!renderer
-    const domElement = renderer?.domElement
-    const isDomElementOk = !!(domElement) && domElement.width > 0 && domElement.height > 0
-
-    messages.value = [
-      {
-        passed: true,
-        msg: 'When the callback was called for the first time ...',
-      },
-      {
-        passed: isRendererOk,
-        msg: isRendererOk ? '... the renderer existed.' : '... the renderer did not exist.',
-      },
-      {
-        passed: !!domElement,
-        msg: domElement ? '... the canvas existed.' : '... the canvas did not exist.',
-      },
-      {
-        passed: isDomElementOk,
-        msg: isDomElementOk
-          ? `... the canvas was not degenerate: ${domElement.width} px × ${domElement.height} px.`
-          : `... the canvas was degenerate.`,
-      },
-    ]
-  }
-}
-
-useLoop().onBeforeRender(({ elapsed: _elapsed, renderer }) => {
-  captureCallback(renderer, _elapsed)
-})
-
-useLoop().render(({ elapsed: _elapsed, renderer, scene, camera }) => {
-  captureCallback(renderer, _elapsed)
-  renderer.render(scene, camera)
-})
-
-useLoop().onAfterRender(({ elapsed: _elapsed, renderer }) => {
-  captureCallback(renderer, _elapsed)
-})
-
-defineExpose({
-  isCalled,
-  messages,
-})
-</script>

+ 0 - 69
playground/vue/src/pages/basic/ready/OnTresReadyWatcher.vue

@@ -1,69 +0,0 @@
-<script setup lang="ts">
-import { onTresReady, type TresContext } from '@tresjs/core'
-import { ref, type ShallowRef, shallowRef } from 'vue'
-
-const isCalled = ref(false)
-
-interface TestResult { passed: boolean, msg: string }
-const messages = shallowRef([
-  {
-    passed: false,
-    msg: 'callback was not called',
-  },
-]) as ShallowRef<TestResult[]>
-
-const captureCallback = (ctx: TresContext) => {
-  if (isCalled.value) {
-    messages.value = [
-      {
-        passed: false,
-        msg: 'Callback was called twice.',
-      },
-    ]
-  }
-  if (!isCalled.value) {
-    isCalled.value = true
-    const isCtxOk = !!(ctx && 'renderer' in ctx && 'scene' in ctx)
-    const renderer = ctx.renderer.value
-    const isRendererOk = !!renderer
-    const domElement = renderer?.domElement
-    const isDomElementOk = !!(domElement) && domElement.width > 0 && domElement.height > 0
-
-    messages.value = [
-      {
-        passed: true,
-        msg: 'When the callback was called ...',
-      },
-      {
-        passed: true,
-        msg: '... it had not previously been called.',
-      },
-      {
-        passed: isCtxOk,
-        msg: isCtxOk ? '... TresContext was passed.' : '... TresContext was not passed.',
-      },
-      {
-        passed: isRendererOk,
-        msg: isRendererOk ? '... the renderer existed.' : '... the renderer did not exist.',
-      },
-      {
-        passed: !!domElement,
-        msg: domElement ? '... the canvas existed.' : '... the canvas did not exist.',
-      },
-      {
-        passed: isDomElementOk,
-        msg: isDomElementOk
-          ? `... the canvas was not degenerate: ${domElement.width} px × ${domElement.height} px.`
-          : `... the canvas was degenerate.`,
-      },
-    ]
-  }
-}
-
-onTresReady(captureCallback)
-
-defineExpose({
-  isCalled,
-  messages,
-})
-</script>

+ 0 - 163
playground/vue/src/pages/basic/ready/index.vue

@@ -1,163 +0,0 @@
-<script setup lang="ts">
-import type { TresContext } from '@tresjs/core'
-import type { ShallowRef } from 'vue'
-import { TresCanvas } from '@tresjs/core'
-import { ref } from 'vue'
-import LoopCallbackWatcher from './LoopCallbackWatcher.vue'
-import OnTresReadyWatcher from './OnTresReadyWatcher.vue'
-
-interface TestResult { passed: boolean, msg: string }
-
-const onReadyMessages = shallowRef([
-  {
-    passed: false,
-    msg: '@ready callback was not called',
-  },
-]) as ShallowRef<TestResult[]>
-
-let numOnReadyCalls = 0
-const onReady = function (ctx: TresContext) {
-  numOnReadyCalls++
-  const renderer = ctx.renderer.value
-  const domElement = renderer?.domElement
-  const isPassedCanvas = domElement.width > 0 && domElement.width > 0
-  const isPassedCtx = !!renderer && 'camera' in ctx && !!(ctx.camera.value)
-
-  onReadyMessages.value = [
-    {
-      passed: true,
-      msg: 'When the callback was called ...',
-    },
-    {
-      passed: numOnReadyCalls === 1,
-      msg: '... it had not previously been called.',
-    },
-    {
-      passed: isPassedCtx,
-      msg: isPassedCtx ? '... TresContext was passed.' : '... TresContext was not passed or was missing elements',
-    },
-    {
-      passed: !!renderer,
-      msg: renderer ? '... the renderer existed.' : '... the renderer did not exist.',
-    },
-    {
-      passed: !!domElement,
-      msg: domElement ? '... the canvas existed.' : '... the canvas did not exist.',
-    },
-    {
-      passed: isPassedCanvas,
-      msg: isPassedCanvas
-        ? `... the canvas was not degenerate: ${domElement.width} px × ${domElement.height} px.`
-        : `... the canvas was degenerate.`,
-    },
-  ]
-}
-
-const onTresReadyWatcherRef = ref({
-  isCalled: false,
-  messages: [] as TestResult[],
-})
-
-const loopCallbackWatcherRef = ref({
-  isCalled: false,
-  messages: [] as TestResult[],
-})
-</script>
-
-<template>
-  <div class="overlay">
-    <h1>When is Tres ready?</h1>
-    <p>
-      Tres is "ready" if either:
-    </p>
-    <ul>
-      <li>
-        The scene can be meaningfully rendered.
-      </li>
-      <ul>
-        <li>the renderer exists</li>
-        <li>the canvas width and height are > 0</li>
-      </ul>
-      <li>Tres has waited 100 ms - assumes setup is intentionally degenerate.</li>
-    </ul>
-    <hr />
-    <h1>"ready" in user space</h1>
-    <h2><code>&lt;TresCanvas @ready="(ctx:TresContext) => {}"&gt;</code></h2>
-    <p>A callback can be defined in the <code>&lt;script setup /&gt;</code> of a &lt;TresCanvas&gt;.</p>
-    <ul>
-      <li
-        v-for="({ passed, msg }, i) of onReadyMessages"
-        :key="i"
-        :class="passed ? 'pass' : 'fail'"
-      >
-        <span>{{ passed ? "✅" : "❌" }} {{ msg }}</span>
-      </li>
-    </ul>
-    <h2><code>onTresReady((ctx:TresContext) => {})</code></h2>
-    <p><code>onTresReady</code> can only be called in a child component.</p>
-    <ul>
-      <li
-        v-for="({ passed, msg }, i) of onTresReadyWatcherRef.messages"
-        :key="i"
-        :class="passed ? 'pass' : 'fail'"
-      >
-        <span>{{ passed ? "✅" : "❌" }} {{ msg }}</span>
-      </li>
-    </ul>
-    <h2><code>useLoop()...(callback)</code></h2>
-    <p><code>useLoop</code> can only be called in a child component.</p>
-    <ul>
-      <li
-        v-for="({ passed, msg }, i) of loopCallbackWatcherRef.messages"
-        :key="i"
-        :class="passed ? 'pass' : 'fail'"
-      >
-        <span>{{ passed ? "✅" : "❌" }} {{ msg }}</span>
-      </li>
-    </ul>
-    <hr />
-    <h1>Context</h1>
-    <p>
-      <a href="https://github.com/Tresjs/tres/issues/595">See this Github issue for further explanation.</a>
-    </p>
-  </div>
-  <TresCanvas clear-color="gray" @ready="onReady">
-    <LoopCallbackWatcher ref="loopCallbackWatcherRef" />
-    <OnTresReadyWatcher ref="onTresReadyWatcherRef" />
-    <TresMesh>
-      <TresBoxGeometry />
-      <TresMeshNormalMaterial />
-    </TresMesh>
-  </TresCanvas>
-</template>
-
-<style scoped>
-.overlay {
-  position: fixed;
-  z-index: 1000;
-  margin: 10px;
-  padding: 10px;
-  border-radius: 6px;
-  max-width: 400px;
-  font-family: sans-serif;
-  font-size: small;
-  background-color: white;
-}
-
-.overlay .pass {
-  color: green;
-}
-
-.overlay .fail {
-  color: red;
-}
-
-.overlay li {
-  padding-left: 0;
-  margin-left: 0;
-}
-.overlay ul {
-  padding-left: 0;
-  margin-left: 1.5em;
-}
-</style>

+ 0 - 5
playground/vue/src/router/routes/basic.ts

@@ -44,11 +44,6 @@ export const basicRoutes = [
     name: 'Pierced Props',
     component: () => import('../../pages/basic/PiercedProps.vue'),
   },
-  {
-    path: '/basic/ready',
-    name: '@ready',
-    component: () => import('../../pages/basic/ready/index.vue'),
-  },
   {
     path: '/basic/textures',
     name: 'Textures',

+ 40 - 25
src/components/TresCanvas.vue

@@ -3,11 +3,12 @@ import type {
   ColorSpace,
   ShadowMapType,
   ToneMapping,
+  WebGLRenderer,
   WebGLRendererParameters,
 } from 'three'
 import type { App, MaybeRef, Ref } from 'vue'
 import type { RendererPresetsType } from '../composables/useRenderer/const'
-import type { TresCamera, TresObject, TresScene } from '../types/'
+import type { TresCamera, TresObject, TresPointerEvent, TresScene } from '../types/'
 import { PerspectiveCamera, Scene } from 'three'
 import * as THREE from 'three'
 
@@ -34,8 +35,9 @@ import {
 import { extend } from '../core/catalogue'
 import { nodeOps } from '../core/nodeOps'
 
-import { disposeObject3D } from '../utils/'
+import { disposeObject3D, kebabToCamel } from '../utils/'
 import { registerTresDevtools } from '../devtools'
+import { whenever } from '@vueuse/core'
 
 export interface TresCanvasProps
   extends Omit<WebGLRendererParameters, 'canvas'> {
@@ -74,24 +76,23 @@ const props = withDefaults(defineProps<TresCanvasProps>(), {
   enableProvideBridge: true,
 })
 
-// Define emits for Pointer events, pass `emit` into useTresEventManager so we can emit events off of TresCanvas
-// Not sure of this solution, but you have to have emits defined on the component to emit them in vue
-const emit = defineEmits([
-  'render',
-  'click',
-  'double-click',
-  'context-menu',
-  'pointer-move',
-  'pointer-up',
-  'pointer-down',
-  'pointer-enter',
-  'pointer-leave',
-  'pointer-over',
-  'pointer-out',
-  'pointer-missed',
-  'wheel',
-  'ready',
-])
+const emit = defineEmits<{
+  ready: [context: TresContext]
+  render: [renderer: WebGLRenderer]
+
+  click: [event: TresPointerEvent]
+  doubleClick: [event: TresPointerEvent]
+  contextMenu: [event: TresPointerEvent]
+  pointerMove: [event: TresPointerEvent]
+  pointerUp: [event: TresPointerEvent]
+  pointerDown: [event: TresPointerEvent]
+  pointerEnter: [event: TresPointerEvent]
+  pointerLeave: [event: TresPointerEvent]
+  pointerOver: [event: TresPointerEvent]
+  pointerOut: [event: TresPointerEvent]
+  pointerMissed: [event: TresPointerEvent]
+  wheel: [event: TresPointerEvent]
+}>()
 
 const slots = defineSlots<{
   default: () => any
@@ -159,9 +160,9 @@ const mountCustomRenderer = (context: TresContext, empty = false) => {
 const dispose = (context: TresContext, force = false) => {
   disposeObject3D(context.scene.value as unknown as TresObject)
   if (force) {
-    context.renderer.value.dispose()
-    context.renderer.value.renderLists.dispose()
-    context.renderer.value.forceContextLoss()
+    context.renderer.instance.value.dispose()
+    context.renderer.instance.value.renderLists.dispose()
+    context.renderer.instance.value.forceContextLoss()
   }
   (scene.value as TresScene).__tres = {
     root: context,
@@ -190,10 +191,9 @@ onMounted(() => {
     canvas: existingCanvas,
     windowSize: props.windowSize ?? false,
     rendererOptions: props,
-    emit,
   })
 
-  const { camera } = context.value
+  const { camera, renderer } = context.value
   const { registerCamera, cameras, activeCamera, deregisterCamera } = camera
 
   mountCustomRenderer(context.value)
@@ -238,10 +238,25 @@ onMounted(() => {
     addDefaultCamera()
   }
 
+  renderer.onRender.on((renderer) => {
+    emit('render', renderer)
+  })
+
+  context.value.eventManager?.onEvent(({ type, event, intersection }) => {
+    emit(
+      kebabToCamel(type) as any, // typescript doesn't know that kebabToCamel(type) is a valid key of PointerEmits
+      { type, event, intersection },
+    )
+  })
+
   // HMR support
   if (import.meta.hot && context.value) { import.meta.hot.on('vite:afterUpdate', () => handleHMR(context.value as TresContext)) }
 })
 
+whenever(() => context.value?.renderer.isReady, () => {
+  if (context.value) { emit('ready', context.value) }
+}, { once: true })
+
 onUnmounted(unmountCanvas)
 </script>
 

+ 1 - 2
src/composables/index.ts

@@ -6,10 +6,9 @@ export * from './useGraph'
 export * from './useLoader'
 export * from './useLoop'
 export * from './useRaycaster'
-export * from './useRenderer/'
+export * from './useRenderer/useRendererManager'
 export * from './useRenderLoop'
 export * from './useTexture'
 export * from './useTresContextProvider'
 export * from './useTresEventManager'
-export { onTresReady } from './useTresReady'
 export { UseLoader, UseTexture }

+ 1 - 1
src/composables/useCamera/index.ts

@@ -48,7 +48,7 @@ interface UseCameraParams {
  * @param params.sizes - The sizes object containing window dimensions
  * @returns The camera management functions and state
  */
-export const useCamera = ({ sizes }: UseCameraParams): UseCameraReturn => {
+export const useCameraManager = ({ sizes }: UseCameraParams): UseCameraReturn => {
   // Store all registered cameras
   const cameras = ref<TresCamera[]>([])
   // Store the UUID of the active camera

+ 0 - 1
src/composables/useLoader/component.vue

@@ -2,7 +2,6 @@
 import type { LoadingManager } from 'three'
 import type { LoaderProto } from './index'
 import { useLoader } from './index'
-import { defineEmits, defineProps } from 'vue'
 import { whenever } from '@vueuse/core'
 import type { TresObjectMap } from '../../utils/graph'
 

+ 1 - 5
src/composables/useLoop/index.ts

@@ -9,19 +9,15 @@ export function useLoop() {
     loop,
     raycaster,
     controls,
-    invalidate,
-    advance,
   } = useTresContext()
 
   // Pass context to loop
   loop.setContext({
     camera,
     scene,
-    renderer,
+    renderer: renderer.instance,
     raycaster,
     controls,
-    invalidate,
-    advance,
   })
 
   function onBeforeRender(cb: LoopCallbackFn, index = 0) {

+ 1 - 1
src/composables/useRaycaster/index.ts

@@ -13,7 +13,7 @@ export const useRaycaster = (
   ctx: TresContext,
 ) => {
   // having a separate computed makes useElementBounding work
-  const canvas = computed(() => ctx.renderer.value.domElement as HTMLCanvasElement)
+  const canvas = computed(() => ctx.renderer.instance.value.domElement as HTMLCanvasElement)
   const intersects: ShallowRef<Intersection[]> = shallowRef([])
   const { x, y } = usePointer({ target: canvas })
   let delta = 0

+ 91 - 29
src/composables/useRenderer/index.ts → src/composables/useRenderer/useRendererManager.ts

@@ -1,17 +1,17 @@
 import type { ColorSpace, Scene, ShadowMapType, ToneMapping, WebGLRendererParameters } from 'three'
-import type { EmitEventFn, TresColor } from '../../types'
+import type { TresColor } from '../../types'
 
 import type { TresContext } from '../useTresContextProvider'
 
 import type { RendererPresetsType } from './const'
 import {
+  createEventHook,
   type MaybeRefOrGetter,
-  toValue,
   unrefElement,
   useDevicePixelRatio,
 } from '@vueuse/core'
 import { ACESFilmicToneMapping, Color, WebGLRenderer } from 'three'
-import { computed, type MaybeRef, onUnmounted, shallowRef, watch, watchEffect } from 'vue'
+import { computed, type MaybeRef, onUnmounted, readonly, ref, shallowRef, toValue, triggerRef, watch, watchEffect } from 'vue'
 
 // Solution taken from Thretle that actually support different versions https://github.com/threlte/threlte/blob/5fa541179460f0dadc7dc17ae5e6854d1689379e/packages/core/src/lib/lib/useRenderer.ts
 import { revision } from '../../core/revision'
@@ -25,6 +25,13 @@ type TransformToMaybeRefOrGetter<T> = {
   [K in keyof T]: MaybeRefOrGetter<T[K]> | MaybeRefOrGetter<T[K]>;
 }
 
+/**
+ * If set to 'on-demand', the scene will only be rendered when the current frame is invalidated
+ * If set to 'manual', the scene will only be rendered when advance() is called
+ * If set to 'always', the scene will be rendered every frame
+ */
+export type RenderMode = 'always' | 'on-demand' | 'manual'
+
 export interface UseRendererOptions extends TransformToMaybeRefOrGetter<WebGLRendererParameters> {
   /**
    * Enable shadows in the Renderer
@@ -91,7 +98,7 @@ export interface UseRendererOptions extends TransformToMaybeRefOrGetter<WebGLRen
   clearColor?: MaybeRefOrGetter<TresColor>
   windowSize?: MaybeRefOrGetter<boolean | string>
   preset?: MaybeRefOrGetter<RendererPresetsType>
-  renderMode?: MaybeRefOrGetter<'always' | 'on-demand' | 'manual'>
+  renderMode?: MaybeRef<RenderMode>
   /**
    * A `number` sets the renderer's device pixel ratio.
    * `[number, number]` clamp's the renderer's device pixel ratio.
@@ -99,18 +106,18 @@ export interface UseRendererOptions extends TransformToMaybeRefOrGetter<WebGLRen
   dpr?: MaybeRefOrGetter<number | [number, number]>
 }
 
-export function useRenderer(
+export function useRendererManager(
   {
+    scene,
     canvas,
     options,
-    contextParts: { sizes, render, invalidate, advance },
+    contextParts: { sizes, loop, camera },
   }:
   {
-    canvas: MaybeRef<HTMLCanvasElement>
     scene: Scene
+    canvas: MaybeRef<HTMLCanvasElement>
     options: UseRendererOptions
-    emit: EmitEventFn
-    contextParts: Pick<TresContext, 'sizes' | 'render'> & { invalidate: () => void, advance: () => void }
+    contextParts: Pick<TresContext, 'sizes' | 'camera' | 'loop'>
   },
 ) {
   const webGLRendererConstructorParameters = computed<WebGLRendererParameters>(() => ({
@@ -128,25 +135,76 @@ export function useRenderer(
     failIfMajorPerformanceCaveat: toValue(options.failIfMajorPerformanceCaveat),
   }))
 
-  const renderer = shallowRef<WebGLRenderer>(new WebGLRenderer(webGLRendererConstructorParameters.value))
+  const instance = shallowRef<WebGLRenderer>(new WebGLRenderer(webGLRendererConstructorParameters.value))
+
+  const amountOfFramesToRender = ref(0)
+  const maxFrames = 60
+  const canBeInvalidated = computed(() => toValue(options.renderMode) === 'on-demand' && amountOfFramesToRender.value === 0)
+
+  /**
+   * Invalidates the current frame when in on-demand render mode.
+   */
+  const invalidate = (amountOfFramesToInvalidate = 1) => {
+    if (!canBeInvalidated.value) {
+      if (toValue(options.renderMode) !== 'on-demand') { throw new Error('invalidate can only be called in on-demand render mode.') }
+
+      return
+    }
+
+    amountOfFramesToRender.value = Math.min(maxFrames, amountOfFramesToRender.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.')
+    }
+
+    amountOfFramesToRender.value = 1
+  }
 
-  function invalidateOnDemand() {
-    if (options.renderMode === 'on-demand') {
+  const invalidateOnDemand = () => {
+    if (toValue(options.renderMode) === 'on-demand') {
       invalidate()
     }
   }
+
+  const isModeAlways = computed(() => toValue(options.renderMode) === 'always')
+
+  const onRender = createEventHook<WebGLRenderer>()
+
+  loop.register(() => {
+    if (camera.value && amountOfFramesToRender.value) {
+      instance.value.render(scene, camera.value)
+
+      onRender.trigger(instance.value)
+    }
+
+    amountOfFramesToRender.value = isModeAlways.value
+      ? 1
+      : Math.max(0, amountOfFramesToRender.value - 1)
+  }, 'render')
+
   // since the properties set via the constructor can't be updated dynamically,
   // the renderer is recreated once they change
   watch(webGLRendererConstructorParameters, () => {
-    renderer.value.dispose()
-    renderer.value = new WebGLRenderer(webGLRendererConstructorParameters.value)
+    instance.value.dispose()
+    instance.value = new WebGLRenderer(webGLRendererConstructorParameters.value)
 
     invalidateOnDemand()
   })
 
+  const isReady = computed(() =>
+    !!(instance.value.domElement.width && instance.value.domElement.height),
+  )
+
   watch([sizes.width, sizes.height], () => {
-    renderer.value.setSize(sizes.width.value, sizes.height.value)
+    instance.value.setSize(sizes.width.value, sizes.height.value)
     invalidateOnDemand()
+
+    triggerRef(instance)
   }, {
     immediate: true,
   })
@@ -174,14 +232,12 @@ export function useRenderer(
 
   const threeDefaults = getThreeRendererDefaults()
 
-  const renderMode = toValue(options.renderMode)
-
-  if (renderMode === 'on-demand') {
+  if (toValue(options.renderMode) === 'on-demand') {
     // Invalidate for the first time
     invalidate()
   }
 
-  if (renderMode === 'manual') {
+  if (toValue(options.renderMode) === 'manual') {
     // Advance for the first time, setTimeout to make sure there is something to render
     setTimeout(() => {
       advance()
@@ -194,16 +250,16 @@ export function useRenderer(
     if (rendererPreset) {
       if (!(rendererPreset in rendererPresets)) { logError(`Renderer Preset must be one of these: ${Object.keys(rendererPresets).join(', ')}`) }
 
-      merge(renderer.value, rendererPresets[rendererPreset])
+      merge(instance.value, rendererPresets[rendererPreset])
     }
 
-    setPixelRatio(renderer.value, pixelRatio.value, toValue(options.dpr))
+    setPixelRatio(instance.value, pixelRatio.value, toValue(options.dpr))
 
     // Render mode
 
-    if (renderMode === 'always') {
+    if (isModeAlways.value) {
       // If the render mode is 'always', ensure there's always a frame pending
-      render.frames.value = Math.max(1, render.frames.value)
+      amountOfFramesToRender.value = Math.max(1, amountOfFramesToRender.value)
     }
 
     const getValue = <T>(option: MaybeRefOrGetter<T>, pathInThree: string): T | undefined => {
@@ -225,7 +281,7 @@ export function useRenderer(
     }
 
     const setValueOrDefault = <T>(option: MaybeRefOrGetter<T>, pathInThree: string) =>
-      set(renderer.value, pathInThree, getValue(option, pathInThree))
+      set(instance.value, pathInThree, getValue(option, pathInThree))
 
     setValueOrDefault(options.shadows, 'shadowMap.enabled')
     setValueOrDefault(options.toneMapping ?? ACESFilmicToneMapping, 'toneMapping')
@@ -239,7 +295,7 @@ export function useRenderer(
     const clearColor = getValue(options.clearColor, 'clearColor')
 
     if (clearColor) {
-      renderer.value.setClearColor(
+      instance.value.setClearColor(
         clearColor
           ? normalizeColor(clearColor)
           : new Color(0x000000), // default clear color is not easily/efficiently retrievable from three
@@ -248,13 +304,19 @@ export function useRenderer(
   })
 
   onUnmounted(() => {
-    renderer.value.dispose()
-    renderer.value.forceContextLoss()
+    instance.value.dispose()
+    instance.value.forceContextLoss()
   })
 
   return {
-    renderer,
+    instance,
+
+    isReady: readonly(isReady),
+    advance,
+    onRender,
+    invalidate,
+    canBeInvalidated,
   }
 }
 
-export type UseRendererReturn = ReturnType<typeof useRenderer>
+export type UseRendererManagerReturn = ReturnType<typeof useRendererManager>

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

@@ -1,6 +1,6 @@
 import type { ComputedRef, MaybeRef, MaybeRefOrGetter, Ref } from 'vue'
-import { refDebounced, toValue, useElementSize, useWindowSize } from '@vueuse/core'
-import { computed, readonly } from 'vue'
+import { refDebounced, useElementSize, useWindowSize } from '@vueuse/core'
+import { computed, readonly, toValue } from 'vue'
 
 export interface SizesType {
   height: Readonly<Ref<number>>
@@ -27,4 +27,4 @@ export default function useSizes(
     width: debouncedReactiveWidth,
     aspectRatio,
   }
-}
+}

+ 22 - 97
src/composables/useTresContextProvider/index.ts

@@ -1,38 +1,20 @@
-import type { WebGLRenderer } from 'three'
-import type { ComputedRef, MaybeRef, MaybeRefOrGetter, Ref, ShallowRef } from 'vue'
-import type { RendererLoop } from '../../core/loop'
-import type { EmitEventFn, TresControl, TresObject, TresScene } from '../../types'
-import type { UseRendererOptions } from '../useRenderer'
 import { Raycaster } from 'three'
-import { computed, inject, onUnmounted, provide, ref, shallowRef } from 'vue'
+import type { MaybeRef, MaybeRefOrGetter, Ref, ShallowRef } from 'vue'
+import { whenever } from '@vueuse/core'
+
+import type { RendererLoop } from '../../core/loop'
+import type { TresControl, TresObject, TresScene } from '../../types'
+import type { UseRendererManagerReturn, UseRendererOptions } from '../useRenderer/useRendererManager'
+import { inject, onUnmounted, provide, ref, shallowRef } from 'vue'
 import { extend } from '../../core/catalogue'
 import { createRenderLoop } from '../../core/loop'
 
 import type { UseCameraReturn } from '../useCamera/'
-import { useCamera } from '../useCamera/'
-import { useRenderer } from '../useRenderer'
+
+import { useCameraManager } from '../useCamera'
+import { useRendererManager } from '../useRenderer/useRendererManager'
 import useSizes, { type SizesType } from '../useSizes'
 import { type TresEventManager, useTresEventManager } from '../useTresEventManager'
-import { useTresReady } from '../useTresReady'
-
-export interface InternalState {
-  priority: Ref<number>
-  frames: Ref<number>
-  maxFrames: number
-}
-
-export interface RenderState {
-  /**
-   * If set to 'on-demand', the scene will only be rendered when the current frame is invalidated
-   * If set to 'manual', the scene will only be rendered when advance() is called
-   * If set to 'always', the scene will be rendered every frame
-   */
-  mode: Ref<'always' | 'on-demand' | 'manual'>
-  priority: Ref<number>
-  frames: Ref<number>
-  maxFrames: number
-  canBeInvalidated: ComputedRef<boolean>
-}
 
 export interface PerformanceState {
   maxFrames: number
@@ -53,20 +35,11 @@ export interface TresContext {
   extend: (objects: any) => void
   camera: UseCameraReturn
   controls: Ref<TresControl | null>
-  renderer: ShallowRef<WebGLRenderer>
+  renderer: UseRendererManagerReturn
   raycaster: ShallowRef<Raycaster>
   perf: PerformanceState
-  render: RenderState
   // Loop
   loop: RendererLoop
-  /**
-   * Invalidates the current frame when renderMode === 'on-demand'
-   */
-  invalidate: () => void
-  /**
-   * Advance one frame when renderMode === 'manual'
-   */
-  advance: () => void
   eventManager?: TresEventManager
   // Events
   // Temporaly add the methods to the context, this should be handled later by the EventManager state on the context https://github.com/Tresjs/tres/issues/515
@@ -82,51 +55,25 @@ export function useTresContextProvider({
   canvas,
   windowSize,
   rendererOptions,
-  emit,
 }: {
   scene: TresScene
   canvas: MaybeRef<HTMLCanvasElement>
   windowSize: MaybeRefOrGetter<boolean>
   rendererOptions: UseRendererOptions
-  emit: EmitEventFn
-
 }): TresContext {
   const localScene = shallowRef<TresScene>(scene)
   const sizes = useSizes(windowSize, canvas)
 
-  const camera = useCamera({ sizes })
+  const camera = useCameraManager({ sizes })
 
-  // Render state
+  const loop = createRenderLoop()
 
-  const render: RenderState = {
-    mode: ref(rendererOptions.renderMode || 'always') as Ref<'always' | 'on-demand' | 'manual'>,
-    priority: ref(0),
-    frames: ref(0),
-    maxFrames: 60,
-    canBeInvalidated: computed(() => render.mode.value === 'on-demand' && render.frames.value === 0),
-  }
-
-  function invalidate(frames = 1) {
-    // Increase the frame count, ensuring not to exceed a maximum if desired
-    if (rendererOptions.renderMode === 'on-demand') {
-      render.frames.value = Math.min(render.maxFrames, render.frames.value + frames)
-    }
-  }
-
-  function advance() {
-    if (rendererOptions.renderMode === 'manual') {
-      render.frames.value = 1
-    }
-  }
-
-  const { renderer } = useRenderer(
+  const renderer = useRendererManager(
     {
       scene,
       canvas,
       options: rendererOptions,
-      emit,
-      // TODO: replace contextParts with full ctx at https://github.com/Tresjs/tres/issues/516
-      contextParts: { sizes, render, invalidate, advance },
+      contextParts: { sizes, camera, loop },
     },
   )
 
@@ -149,11 +96,8 @@ export function useTresContextProvider({
         accumulator: [],
       },
     },
-    render,
-    advance,
     extend,
-    invalidate,
-    loop: createRenderLoop(),
+    loop,
   }
 
   provide('useTres', ctx)
@@ -163,38 +107,19 @@ export function useTresContextProvider({
     root: ctx,
   }
 
-  // The loop
-
-  ctx.loop.register(() => {
-    if (camera.activeCamera.value && render.frames.value > 0) {
-      renderer.value.render(scene, camera.activeCamera.value)
-      emit('render', ctx.renderer.value)
-    }
-
-    // Reset priority
-    render.priority.value = 0
-
-    if (render.mode.value === 'always') {
-      render.frames.value = 1
-    }
-    else {
-      render.frames.value = Math.max(0, render.frames.value - 1)
-    }
-  }, 'render')
-
-  const { on: onTresReady, cancel: cancelTresReady } = useTresReady(ctx)!
-
   ctx.loop.setReady(false)
   ctx.loop.start()
 
-  onTresReady(() => {
-    emit('ready', ctx)
+  whenever(renderer.isReady, () => { // TODO #994 This does not belong here, see https://github.com/Tresjs/tres/issues/595
     ctx.loop.setReady(true)
-    useTresEventManager(scene, ctx, emit)
+  }, {
+    once: true,
+    immediate: true,
   })
 
+  useTresEventManager(scene, ctx)
+
   onUnmounted(() => {
-    cancelTresReady()
     ctx.loop.stop()
   })
 

+ 13 - 7
src/composables/useTresEventManager/index.ts

@@ -1,12 +1,15 @@
-import type { EmitEventFn, EmitEventName, Intersection, TresEvent, TresInstance, TresObject } from 'src/types'
+import type { Intersection, PointerEventType, TresEvent, TresInstance, TresObject, TresPointerEvent } from 'src/types'
 import type { Object3D, Object3DEventMap, Scene } from 'three'
 import type { TresContext } from '../useTresContextProvider'
 import { shallowRef } from 'vue'
 import { hyphenate } from '../../utils'
 import { useRaycaster } from '../useRaycaster'
 import { isObject3D, isTresObject } from '../../utils/is'
+import type { EventHookOff } from '@vueuse/core'
+import { createEventHook } from '@vueuse/core'
 
 export interface TresEventManager {
+  onEvent: EventHookOff<TresPointerEvent>
   /**
    * Forces the event system to refire events with the previous mouse event
    */
@@ -21,7 +24,6 @@ export interface TresEventManager {
   registerPointerMissedObject: (object: unknown) => void
   deregisterPointerMissedObject: (object: unknown) => void
 }
-
 function executeEventListeners(
   listeners: (event: TresEvent) => void | ((event: TresEvent) => void)[],
   event: TresEvent,
@@ -42,7 +44,6 @@ function executeEventListeners(
 export function useTresEventManager(
   scene: Scene,
   context: TresContext,
-  emit: EmitEventFn,
 ) {
   const _scene = shallowRef<Scene>()
   const _context = shallowRef<TresContext>()
@@ -55,6 +56,8 @@ export function useTresEventManager(
   // TODO: Optimize to not hit test on the whole scene
   const objectsWithEvents = shallowRef((_scene.value?.children as TresInstance[]).filter(hasChildrenWithEvents) || [])
 
+  const eventHook = createEventHook<TresPointerEvent>()
+
   /**
    * propogateEvent
    *
@@ -98,8 +101,9 @@ export function useTresEventManager(
       }
 
       // Convert eventName to kebab case and emit event from TresCanvas
-      const kebabEventName = hyphenate(eventName.slice(2)) as EmitEventName
-      emit(kebabEventName, { intersection, event })
+      const kebabEventName = hyphenate(eventName.slice(2)) as PointerEventType
+
+      eventHook.trigger({ type: kebabEventName, event, intersection })
     }
   }
 
@@ -178,8 +182,8 @@ export function useTresEventManager(
 
       executeEventListeners(object.onPointerMissed, event)
     })
-    // Emit pointer-missed from TresCanvas
-    emit('pointer-missed', { event })
+
+    eventHook.trigger({ type: 'pointer-missed', event })
   })
 
   function registerObject(maybeTresObject: unknown) {
@@ -214,6 +218,7 @@ export function useTresEventManager(
 
   // Attach methods to tres context
   context.eventManager = {
+    onEvent: eventHook.on,
     forceUpdate,
     registerObject,
     deregisterObject,
@@ -222,6 +227,7 @@ export function useTresEventManager(
   }
 
   return {
+    onEvent: eventHook.on,
     forceUpdate,
     registerObject,
     deregisterObject,

+ 0 - 187
src/composables/useTresReady/createReadyEventHook/createReadyHook.test.ts

@@ -1,187 +0,0 @@
-import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
-import { createReadyEventHook } from './index'
-
-describe('createReadyEventHook', () => {
-  beforeEach(() => {
-    vi.useFakeTimers()
-  })
-
-  afterEach(() => {
-    vi.restoreAllMocks()
-  })
-
-  describe('createReadyEventHook(getIsReady)', () => {
-    it('calls getIsReady when created', () => {
-      const getIsReady = vi.fn(() => true)
-      createReadyEventHook(getIsReady, null)
-      expect(getIsReady).toBeCalled()
-    })
-
-    it('calls getIsReady periodically', () => {
-      const fn = vi.fn(() => false)
-      createReadyEventHook(fn, null, 1)
-      vi.advanceTimersByTime(1000)
-      expect(fn).toHaveBeenCalledTimes(1000 + 1)
-    })
-
-    it('calls getIsReady periodically, but only until `getIsReady()` is truthy', () => {
-      let i = 0
-      const fn0 = () => {
-        i++
-        return i === 5
-      }
-      createReadyEventHook(fn0, null)
-      vi.advanceTimersByTime(1000)
-      expect(i).toBe(5)
-
-      i = -1
-      const fn1 = () => {
-        i++
-        return i
-      }
-      createReadyEventHook(fn1 as any, null)
-      vi.advanceTimersByTime(1000)
-      expect(i).toBe(1)
-    })
-
-    it('calls getIsReady periodically, but only while not cancelled', () => {
-      const fn = vi.fn(() => false)
-      const { cancel } = createReadyEventHook(fn, null, 1)
-      vi.advanceTimersByTime(99)
-      cancel()
-      vi.advanceTimersByTime(1000)
-      expect(fn).toHaveBeenCalledTimes(100)
-    })
-  })
-
-  describe('createReadyEventHook(getIsReady, intervalMs)', () => {
-    it('calls getIsReady at the provided interval', () => {
-      const fn = vi.fn(() => false)
-      createReadyEventHook(fn, null, 100)
-      expect(fn).toHaveBeenCalledTimes(1)
-      vi.advanceTimersByTime(99)
-      expect(fn).toHaveBeenCalledTimes(1)
-      vi.advanceTimersByTime(1000)
-      expect(fn).toHaveBeenCalledTimes(10 + 1)
-      vi.advanceTimersByTime(5000)
-      expect(fn).toHaveBeenCalledTimes(50 + 10 + 1)
-    })
-  })
-
-  describe('createReadyEventHook().on', () => {
-    it('registers a function and calls it once `getIsReady() === true`', () => {
-      const fn = vi.fn()
-      const { on } = createReadyEventHook(trueIfCalledNTimes(10), null)
-
-      on(fn)
-      vi.advanceTimersByTime(10000)
-
-      expect(fn).toHaveBeenCalledTimes(1)
-    })
-
-    it('calls registered functions with args', () => {
-      const fn0 = vi.fn()
-      const fn1 = vi.fn()
-      const arg0 = { foo: 'bar' }
-      const arg1 = { baz: 'boo' }
-      const { on } = createReadyEventHook(() => true, [arg0, arg1])
-
-      on(fn0)
-      on(fn1)
-
-      expect(fn0).toHaveBeenCalledWith([{ foo: 'bar' }, { baz: 'boo' }])
-      expect(fn1).toHaveBeenCalledWith([{ foo: 'bar' }, { baz: 'boo' }])
-    })
-
-    it('calls a function immediately if `getIsReady() === true`', () => {
-      const fn = vi.fn()
-      const { on } = createReadyEventHook(() => true, null)
-
-      on(fn)
-
-      expect(fn).toHaveBeenCalledTimes(1)
-    })
-
-    it('calls functions with arg immediately if `getIsReady() === true`', () => {
-      const fn0 = vi.fn()
-      const fn1 = vi.fn()
-      const arg = { foo: 'bar' }
-      const { on } = createReadyEventHook(() => true, arg)
-
-      on(fn0)
-      on(fn1)
-
-      expect(fn0).toHaveBeenCalledWith({ foo: 'bar' })
-      expect(fn1).toHaveBeenCalledWith({ foo: 'bar' })
-    })
-
-    it('can register many functions, one at a time', () => {
-      const fns = Array.from({ length: 100 })
-        .fill(0)
-        .map(_ => vi.fn())
-
-      const { on } = createReadyEventHook(trueIfCalledNTimes(10), null)
-      fns.forEach(fn => on(fn))
-      vi.advanceTimersByTime(10000)
-
-      for (const fn of fns) {
-        expect(fn).toHaveBeenCalledTimes(1)
-      }
-    })
-  })
-
-  describe('createReadyEventHook().off(fn)', () => {
-    it('unregisters a function', () => {
-      const fns = Array.from({ length: 100 })
-        .fill(0)
-        .map(_ => vi.fn())
-
-      const { on, off } = createReadyEventHook(trueIfCalledNTimes(10), null)
-      fns.forEach(fn => on(fn))
-
-      const offedFns = new Set()
-      fns.forEach((fn) => {
-        if (Math.random() < 0.5) {
-          offedFns.add(fn)
-          off(fn)
-        }
-      })
-      vi.advanceTimersByTime(10000)
-
-      fns.forEach((fn) => {
-        expect(fn).toHaveBeenCalledTimes(offedFns.has(fn) ? 0 : 1)
-      })
-    })
-  })
-
-  describe('createReadyEventHook().on(fn).off()', () => {
-    it('unregisters a function', () => {
-      const fns = Array.from({ length: 100 })
-        .fill(0)
-        .map(_ => vi.fn())
-
-      const { on } = createReadyEventHook(trueIfCalledNTimes(10), null)
-
-      const offedFns = new Set()
-      fns.forEach((fn) => {
-        const { off } = on(fn)
-        if (Math.random() < 0.5) {
-          offedFns.add(fn)
-          off()
-        }
-      })
-      vi.advanceTimersByTime(1000)
-
-      fns.forEach((fn) => {
-        expect(fn).toHaveBeenCalledTimes(offedFns.has(fn) ? 0 : 1)
-      })
-    })
-  })
-})
-
-function trueIfCalledNTimes(n: number) {
-  return () => {
-    n = Math.max(n - 1, 0)
-    return n === 0
-  }
-}

+ 0 - 87
src/composables/useTresReady/createReadyEventHook/index.ts

@@ -1,87 +0,0 @@
-import type { EventHook, EventHookOn, IsAny } from '@vueuse/core'
-import { createEventHook } from '@vueuse/core'
-
-type Callback<T> =
-  IsAny<T> extends true
-    ? (param: any) => void
-    : [T] extends [void]
-        ? () => void
-        : (param: T) => void
-
-export function createReadyEventHook<T>(
-  getIsReady: () => boolean,
-  triggerParams: T,
-  pollIntervalMs = 100,
-): EventHook<T> & { cancel: () => void } {
-  pollIntervalMs = pollIntervalMs <= 0 ? 100 : pollIntervalMs
-  const hook = createEventHook()
-  // NOTE: This hook will likely be long-lived and
-  // we don't want to interfere with garbage collection
-  // in the meantime.
-  // Keep a set of `offFns` and call them after `getIsReady`
-  // in order to remove them from the `hook`.
-  const offFns = new Set<() => void>()
-  let ready = false
-  let cancelled = false
-  let timeoutId: ReturnType<typeof setTimeout> | null = null
-
-  function doReadyTest() {
-    if (timeoutId) {
-      clearTimeout(timeoutId)
-    }
-    if (!cancelled && !ready && getIsReady()) {
-      hook.trigger(triggerParams)
-      offFns.forEach(offFn => offFn())
-      offFns.clear()
-      ready = true
-    }
-    else if (!cancelled && !ready) {
-      timeoutId = setTimeout(doReadyTest, pollIntervalMs)
-    }
-  }
-
-  function cancel() {
-    cancelled = true
-    if (timeoutId) {
-      clearTimeout(timeoutId)
-    }
-  }
-
-  if (import.meta.hot) {
-    import.meta.hot.on('vite:afterUpdate', () => {
-      ready = false
-      doReadyTest()
-    })
-  }
-
-  doReadyTest()
-
-  const triggerSingleCallback = (callback: Callback<T>, ...args: [T]) => {
-    callback(...args)
-  }
-
-  const onOrCall: EventHookOn<T> = (callback) => {
-    if (!ready) {
-      const onFn = hook.on(callback)
-
-      if (!import.meta.hot) {
-        // NOTE: We must keep callbacks around for HMR.
-        // But if it doesn't exist, remove callbacks.
-        offFns.add(onFn.off)
-      }
-      return hook.on(callback)
-    }
-    else {
-      triggerSingleCallback(callback as Callback<T>, triggerParams)
-      return { off: () => {} }
-    }
-  }
-
-  return {
-    on: onOrCall,
-    off: hook.off,
-    trigger: hook.trigger,
-    clear: hook.clear,
-    cancel,
-  }
-}

+ 0 - 53
src/composables/useTresReady/index.ts

@@ -1,53 +0,0 @@
-import type { TresContext } from '../useTresContextProvider'
-import { useTresContext } from '../useTresContextProvider'
-import { createReadyEventHook } from './createReadyEventHook'
-
-const ctxToUseTresReady = new WeakMap<
-  TresContext,
-  ReturnType<typeof createReadyEventHook<TresContext>>
->()
-
-export function useTresReady(ctx?: TresContext) {
-  ctx = ctx || useTresContext()
-  if (ctxToUseTresReady.has(ctx)) {
-    return ctxToUseTresReady.get(ctx)!
-  }
-
-  const MAX_READY_WAIT_MS = 100
-  const start = Date.now()
-
-  // NOTE: Consider Tres to be "ready" if either is true:
-  // - MAX_READY_WAIT_MS has passed (assume Tres is intentionally degenerate)
-  // - Tres is not degenerate
-  //     - A renderer exists
-  //     - A DOM element exists
-  //     - The DOM element's height/width is not 0
-  const getTresIsReady = () => {
-    if (Date.now() - start >= MAX_READY_WAIT_MS) {
-      return true
-    }
-    else {
-      const renderer = ctx.renderer.value
-      const domElement = renderer?.domElement || { width: 0, height: 0 }
-      return !!(renderer && domElement.width > 0 && domElement.height > 0)
-    }
-  }
-
-  const args = ctx as TresContext
-  const result = createReadyEventHook(getTresIsReady, args)
-  ctxToUseTresReady.set(ctx, result)
-
-  return result
-}
-
-export function onTresReady(fn: (ctx: TresContext) => void) {
-  const ctx = useTresContext()
-  if (ctx) {
-    if (ctxToUseTresReady.has(ctx)) {
-      return ctxToUseTresReady.get(ctx)!.on(fn)
-    }
-    else {
-      return useTresReady(ctx).on(fn)
-    }
-  }
-}

+ 0 - 2
src/core/loop.ts

@@ -22,8 +22,6 @@ export interface LoopCallbackWithCtx extends LoopCallback {
   controls: Ref<(EventDispatcher<object> & {
     enabled: boolean
   }) | null>
-  invalidate: Fn
-  advance: Fn
 }
 
 export type LoopCallbackFn = (params: LoopCallbackWithCtx) => void

+ 5 - 5
src/devtools/plugin.ts

@@ -188,15 +188,15 @@ export function registerTresDevtools(app: any, tres: TresContext) {
                   value: {
                     objects: instance.children.length,
                     memory: calculateMemoryUsage(instance),
-                    calls: tres.renderer.value.info.render.calls,
-                    triangles: tres.renderer.value.info.render.triangles,
-                    points: tres.renderer.value.info.render.points,
-                    lines: tres.renderer.value.info.render.lines,
+                    calls: tres.renderer.instance.value.info.render.calls,
+                    triangles: tres.renderer.instance.value.info.render.triangles,
+                    points: tres.renderer.instance.value.info.render.points,
+                    lines: tres.renderer.instance.value.info.render.lines,
                   },
                 },
                 {
                   key: 'Programs',
-                  value: tres.renderer.value.info.programs?.map(program => ({
+                  value: tres.renderer.instance.value.info.programs?.map(program => ({
                     ...program,
                     programName: program.name,
                   })) || [],

+ 4 - 2
src/types/index.ts

@@ -23,8 +23,10 @@ export interface TresCatalogue {
   [name: string]: ConstructorRepresentation
 }
 
-export type EmitEventName = 'render' | 'ready' | 'click' | 'double-click' | 'context-menu' | 'pointer-move' | 'pointer-up' | 'pointer-down' | 'pointer-enter' | 'pointer-leave' | 'pointer-over' | 'pointer-out' | 'pointer-missed' | 'wheel'
-export type EmitEventFn = (event: EmitEventName, ...args: any[]) => void
+export const pointerEventTypes = ['click', 'double-click', 'context-menu', 'pointer-move', 'pointer-up', 'pointer-down', 'pointer-enter', 'pointer-leave', 'pointer-over', 'pointer-out', 'pointer-missed', 'wheel'] as const
+export type PointerEventType = typeof pointerEventTypes[number]
+export interface TresPointerEvent { type: PointerEventType, event: TresEvent, intersection?: Intersection }
+
 export type TresCamera = THREE.OrthographicCamera | THREE.PerspectiveCamera
 
 /**

+ 3 - 3
src/utils/index.ts

@@ -436,10 +436,10 @@ export function prepareTresInstance<T extends TresObject>(obj: T, state: Partial
 export function invalidateInstance(instance: TresObject) {
   const ctx = instance?.__tres?.root
 
-  if (!ctx) { return }
+  if (!ctx?.renderer) { return }
 
-  if (ctx.render && ctx.render.canBeInvalidated.value) {
-    ctx.invalidate()
+  if (ctx.renderer.canBeInvalidated.value) {
+    ctx.renderer.invalidate()
   }
 }