1
0
Peter 1 жил өмнө
parent
commit
a3d7bbe104
55 өөрчлөгдсөн 1753 нэмэгдсэн , 576 устгасан
  1. 1 1
      .github/workflows/lint.yml
  2. 1 1
      .github/workflows/test.yml
  3. 65 0
      CHANGELOG.md
  4. 1 1
      CONTRIBUTING.md
  5. 2 1
      docs/.vitepress/config/de.ts
  6. 4 2
      docs/.vitepress/config/en.ts
  7. 2 1
      docs/.vitepress/config/es.ts
  8. 2 1
      docs/.vitepress/config/fr.ts
  9. 2 1
      docs/.vitepress/config/nl.ts
  10. 2 1
      docs/.vitepress/config/zh.ts
  11. 91 6
      docs/.vitepress/theme/components/HomeSponsors.vue
  12. 4 0
      docs/.vitepress/theme/custom.css
  13. 31 0
      docs/advanced/performance.md
  14. 52 12
      docs/api/events.md
  15. 6 0
      docs/debug/devtools.md
  16. 4 4
      docs/directives/v-always-look-at.md
  17. 2 1
      docs/directives/v-distance-to.md
  18. 2 1
      docs/directives/v-light-helper.md
  19. 2 1
      docs/directives/v-log.md
  20. 80 0
      docs/directives/v-rotate.md
  21. 3 1
      docs/package.json
  22. BIN
      docs/public/devtools-v4.png
  23. 15 1
      docs/vite.config.ts
  24. 15 15
      package.json
  25. 2 22
      playground/components.d.ts
  26. 5 5
      playground/package.json
  27. 15 3
      playground/src/components/BlenderCube.vue
  28. 44 0
      playground/src/components/Box.vue
  29. 0 1
      playground/src/main.ts
  30. 13 3
      playground/src/pages/basic/Lights.vue
  31. 15 3
      playground/src/pages/basic/index.vue
  32. 1 1
      playground/src/pages/empty.vue
  33. 188 0
      playground/src/pages/events/Propagation.vue
  34. 67 50
      playground/src/pages/events/index.vue
  35. 65 0
      playground/src/pages/perf/Memory.vue
  36. 5 0
      playground/src/router/index.ts
  37. 5 0
      playground/src/router/routes/performance.ts
  38. 304 250
      pnpm-lock.yaml
  39. 36 8
      src/components/TresCanvas.vue
  40. 1 0
      src/composables/index.ts
  41. 16 15
      src/composables/usePointerEventHandler/index.ts
  42. 136 30
      src/composables/useRaycaster/index.ts
  43. 2 2
      src/composables/useRenderer/index.ts
  44. 10 7
      src/composables/useTresContextProvider/index.ts
  45. 194 0
      src/composables/useTresEventManager/index.ts
  46. 71 68
      src/core/nodeOps.ts
  47. 1 1
      src/devtools/highlight.ts
  48. 30 35
      src/devtools/plugin.ts
  49. 4 0
      src/devtools/utils.ts
  50. 2 1
      src/directives/index.ts
  51. 40 0
      src/directives/vRotate.ts
  52. 2 0
      src/index.ts
  53. 35 15
      src/types/index.ts
  54. 53 3
      src/utils/index.ts
  55. 2 1
      src/utils/test-utils.ts

+ 1 - 1
.github/workflows/lint.yml

@@ -11,7 +11,7 @@ jobs:
     runs-on: ubuntu-22.04
     strategy:
       matrix:
-        node-version: [16]
+        node-version: [18]
     steps:
       - name: Checkout
         uses: actions/checkout@v4

+ 1 - 1
.github/workflows/test.yml

@@ -11,7 +11,7 @@ jobs:
     runs-on: ubuntu-22.04
     strategy:
       matrix:
-        node-version: [16]
+        node-version: [18]
     steps:
       - name: Checkout
         uses: actions/checkout@v4

+ 65 - 0
CHANGELOG.md

@@ -1,9 +1,74 @@
+
+
+## [4.0.0-rc.0](https://github.com/Tresjs/tres/compare/3.9.0...4.0.0-rc.0) (2024-04-25)
+
+
+### ⚠ BREAKING CHANGES
+
+* **events:** pointerevents manager and state (#529)
+
+### Features
+
+* 499 better memory management ([#606](https://github.com/Tresjs/tres/issues/606)) ([e98ca6d](https://github.com/Tresjs/tres/commit/e98ca6dea15973b3a00e4b485199d9906eb772eb))
+* devtools renderer improvements ([#614](https://github.com/Tresjs/tres/issues/614)) ([cdf6b6f](https://github.com/Tresjs/tres/commit/cdf6b6fefbd58dbf1dfbe396f219ac6f7c6fc92d))
+* **events:** pointerevents manager and state ([#529](https://github.com/Tresjs/tres/issues/529)) ([b536ab1](https://github.com/Tresjs/tres/commit/b536ab19d1f4082c2db926e24d8c52f92949964b))
+
+
+### Bug Fixes
+
+* do not change pierced props case ([#608](https://github.com/Tresjs/tres/issues/608)) ([906f2e1](https://github.com/Tresjs/tres/commit/906f2e157aab7aa6daef5682c3282cf6e84fa891))
+
+## [4.0.0-next.2](https://github.com/Tresjs/tres/compare/3.9.0...4.0.0-rc.0) (2024-03-27)
+
+
+### Bug Fixes
+
+* refactor nodeOps to return methods at the end of the function ([#602](https://github.com/Tresjs/tres/issues/602)) ([cd0c3bc](https://github.com/Tresjs/tres/commit/cd0c3bcd891f019cf91f30e5fdd547630332a065))
+
+## [4.0.0-next.1](https://github.com/Tresjs/tres/compare/3.9.0...4.0.0-rc.0) (2024-03-18)
+
+
+### Features
+
+* 140 on demand rendering ([#497](https://github.com/Tresjs/tres/issues/497)) ([f688c64](https://github.com/Tresjs/tres/commit/f688c6447be887c4675a57ecabb5182d8b7d02cf))
+* 492 set tone mapping default to acesfilmictonemapping ([#498](https://github.com/Tresjs/tres/issues/498)) ([c4547f9](https://github.com/Tresjs/tres/commit/c4547f92615a43b7b56b34c0e1ee9f4b78a2230b))
+* 503 conditional rendering of primitives ([#514](https://github.com/Tresjs/tres/issues/514)) ([79d8a76](https://github.com/Tresjs/tres/commit/79d8a762da6b6e23771a20314f7902eff4635acf))
+* 516 localstate for custom renderer node instances instead of userdata ([#522](https://github.com/Tresjs/tres/issues/522)) ([08717ef](https://github.com/Tresjs/tres/commit/08717efd0f631c085340b1fea4eb6c154c63608b))
+* remove default camera warning ([#499](https://github.com/Tresjs/tres/issues/499)) ([8bbafde](https://github.com/Tresjs/tres/commit/8bbafde48a33753f0b6560da36a4d128aaa83cc6))
+* update to three `v160` and vue `v3.4` ([#488](https://github.com/Tresjs/tres/issues/488)) ([5fad3b8](https://github.com/Tresjs/tres/commit/5fad3b8095c09cfe758e2553da3df49b29b1ce1a))
+
+
+### Bug Fixes
+
+* `nodeOps` is now a function ([#579](https://github.com/Tresjs/tres/issues/579)) ([ddc229e](https://github.com/Tresjs/tres/commit/ddc229e6e492b9e7887add0fcc679a9ae4e47f5c))
+* camera aspect ([52dad5c](https://github.com/Tresjs/tres/commit/52dad5c98271f80f4d454bbcce1bb5844960f943))
+* **types:** added `Object3DEventMap` to `Object3D` generics for point event handling ([#491](https://github.com/Tresjs/tres/issues/491)) ([a63eb90](https://github.com/Tresjs/tres/commit/a63eb9099fcaf97b1c96abe5667ee71ca2fd611f))
+
+## [4.0.0-next.0](https://github.com/Tresjs/tres/compare/3.9.0...4.0.0-rc.0) (2023-12-22)
+
+
+### Features
+
+* 474 vue chrome devtools plugin ([#479](https://github.com/Tresjs/tres/issues/479)) ([224ab06](https://github.com/Tresjs/tres/commit/224ab06a4404e2ae5a0cbd2f43041961862b09fd))
+
 ## [4.0.0-next.2](https://github.com/Tresjs/tres/compare/4.0.0-next.1...4.0.0-next.2) (2024-03-27)
 
 ## [4.0.0-next.1](https://github.com/Tresjs/tres/compare/3.7.0...4.0.0-next.1) (2024-03-18)
 * correct type exporting issues ([#625](https://github.com/Tresjs/tres/issues/625)) ([8e52cf1](https://github.com/Tresjs/tres/commit/8e52cf1935d7b725b87c9a41e44ba61e33bd3e85))
 
 
+## [3.9.0](https://github.com/Tresjs/tres/compare/3.8.1...3.9.0) (2024-04-24)
+
+
+### Features
+
+* **app:** Add a new directive, v-rotate ([ccf5313](https://github.com/Tresjs/tres/commit/ccf53135a81c795bc08b343baaa823fa33bd064d))
+
+
+### Bug Fixes
+
+* **deps:** update dependency vue-router to v4.3.1 ([#650](https://github.com/Tresjs/tres/issues/650)) ([9bbb676](https://github.com/Tresjs/tres/commit/9bbb6768a5a7400bf163c06b3688505915bfd074))
+
 ## [3.8.1](https://github.com/Tresjs/tres/compare/3.8.0...3.8.1) (2024-04-08)
 
 

+ 1 - 1
CONTRIBUTING.md

@@ -15,7 +15,7 @@ Thanks from the heart 💚 for taking the time to help out. This guide will help
 
 ## Setup
 
-All the packages in the ecosystem use [pnpm workspaces](https://pnpm.io/workspaces). PnPM is a package manager that is faster than npm and yarn. It also uses symlinks to avoid code duplication.
+All the packages in the ecosystem use [pnpm workspaces](https://pnpm.io/workspaces). Pnpm is a package manager that is faster than npm and yarn. It also uses symlinks to avoid code duplication.
 
 The `workspace` has the following structure:
 

+ 2 - 1
docs/.vitepress/config/de.ts

@@ -3,7 +3,7 @@ import type { DefaultTheme, LocaleSpecificConfig } from 'vitepress'
 export const deConfig: LocaleSpecificConfig<DefaultTheme.Config> = {
   themeConfig: {
     editLink: {
-      pattern: 'https://github.com/tresjs/tres/edit/main/packages/docs/:path',
+      pattern: 'https://github.com/tresjs/tres/edit/main/docs/:path',
       text: 'Änderungen an dieser Seite vorschlagen',
     },
     sidebar: [
@@ -77,6 +77,7 @@ export const deConfig: LocaleSpecificConfig<DefaultTheme.Config> = {
           { text: 'v-light-helper', link: '/de/directives/v-light-helper' },
           { text: 'v-always-look-at', link: '/de/directives/v-always-look-at' },
           { text: 'v-distance-to', link: '/de/directives/v-distance-to' },
+          { text: 'v-rotate', link: '/directives/v-rotate' },
         ],
       },
       {

+ 4 - 2
docs/.vitepress/config/en.ts

@@ -3,7 +3,7 @@ import type { DefaultTheme, LocaleSpecificConfig } from 'vitepress'
 export const enConfig: LocaleSpecificConfig<DefaultTheme.Config> = {
   themeConfig: {
     editLink: {
-      pattern: 'https://github.com/tresjs/tres/edit/main/packages/docs/:path',
+      pattern: 'https://github.com/tresjs/tres/edit/main/docs/:path',
       text: 'Suggest changes to this page',
     },
     sidebar: [
@@ -43,7 +43,8 @@ export const enConfig: LocaleSpecificConfig<DefaultTheme.Config> = {
 
         items: [
           { text: 'Extending', link: '/advanced/extending' },
-          { text: 'Primitive', link: '/advanced/primitive' },
+          { text: 'Primitives', link: '/advanced/primitive' },
+          { text: 'Scaling Performance 🚀', link: '/advanced/performance' },
           {
             text: 'Caveats',
             link: '/advanced/caveats',
@@ -78,6 +79,7 @@ export const enConfig: LocaleSpecificConfig<DefaultTheme.Config> = {
           { text: 'v-light-helper', link: '/directives/v-light-helper' },
           { text: 'v-always-look-at', link: '/directives/v-always-look-at' },
           { text: 'v-distance-to', link: '/directives/v-distance-to' },
+          { text: 'v-rotate', link: '/directives/v-rotate' },
         ],
       },
       {

+ 2 - 1
docs/.vitepress/config/es.ts

@@ -3,7 +3,7 @@ import type { DefaultTheme, LocaleSpecificConfig } from 'vitepress'
 export const esConfig: LocaleSpecificConfig<DefaultTheme.Config> = {
   themeConfig: {
     editLink: {
-      pattern: 'https://github.com/tresjs/tres/edit/main/packages/docs/:path',
+      pattern: 'https://github.com/tresjs/tres/edit/main/docs/:path',
       text: 'Sugerir cambios a esta página',
     },
     sidebar: [
@@ -78,6 +78,7 @@ export const esConfig: LocaleSpecificConfig<DefaultTheme.Config> = {
           { text: 'v-light-helper', link: '/es/directives/v-light-helper' },
           { text: 'v-always-look-at', link: '/es/directives/v-always-look-at' },
           { text: 'v-distance-to', link: '/es/directives/v-distance-to' },
+          { text: 'v-rotate', link: '/directives/v-rotate' },
         ],
       },
       {

+ 2 - 1
docs/.vitepress/config/fr.ts

@@ -3,7 +3,7 @@ import type { DefaultTheme, LocaleSpecificConfig } from 'vitepress'
 export const frConfig: LocaleSpecificConfig<DefaultTheme.Config> = {
   themeConfig: {
     editLink: {
-      pattern: 'https://github.com/tresjs/tres/edit/main/packages/docs/:path',
+      pattern: 'https://github.com/tresjs/tres/edit/main/docs/:path',
       text: 'Suggérer un changement à cette page.',
     },
     sidebar: [
@@ -79,6 +79,7 @@ export const frConfig: LocaleSpecificConfig<DefaultTheme.Config> = {
           { text: 'v-light-helper', link: '/fr/directives/v-light-helper' },
           { text: 'v-always-look-at', link: '/fr/directives/v-always-look-at' },
           { text: 'v-distance-to', link: '/fr/directives/v-distance-to' },
+          { text: 'v-rotate', link: '/directives/v-rotate' },
         ],
       },
       {

+ 2 - 1
docs/.vitepress/config/nl.ts

@@ -3,7 +3,7 @@ import type { DefaultTheme, LocaleSpecificConfig } from 'vitepress'
 export const nlConfig: LocaleSpecificConfig<DefaultTheme.Config> = {
   themeConfig: {
     editLink: {
-      pattern: 'https://github.com/tresjs/tres/edit/main/packages/docs/:path',
+      pattern: 'https://github.com/tresjs/tres/edit/main/docs/:path',
       text: 'Stel wijzigingen op deze pagina voor',
     },
     sidebar: [
@@ -78,6 +78,7 @@ export const nlConfig: LocaleSpecificConfig<DefaultTheme.Config> = {
           { text: 'v-light-helper', link: '/nl/directives/v-light-helper' },
           { text: 'v-always-look-at', link: '/nl/directives/v-always-look-at' },
           { text: 'v-distance-to', link: '/nl/directives/v-distance-to' },
+          { text: 'v-rotate', link: '/directives/v-rotate' },
         ],
       },
       {

+ 2 - 1
docs/.vitepress/config/zh.ts

@@ -3,7 +3,7 @@ import type { DefaultTheme, LocaleSpecificConfig } from 'vitepress'
 export const zhConfig: LocaleSpecificConfig<DefaultTheme.Config> = {
   themeConfig: {
     editLink: {
-      pattern: 'https://github.com/tresjs/tres/edit/main/packages/docs/:path',
+      pattern: 'https://github.com/tresjs/tres/edit/main/docs/:path',
       text: '对本页内容给出建议',
     },
     sidebar: [
@@ -77,6 +77,7 @@ export const zhConfig: LocaleSpecificConfig<DefaultTheme.Config> = {
           { text: 'v-light-helper', link: '/zh/directives/v-light-helper' },
           { text: 'v-always-look-at', link: '/zh/directives/v-always-look-at' },
           { text: 'v-distance-to', link: '/zh/directives/v-distance-to' },
+          { text: 'v-rotate', link: '/directives/v-rotate' },
         ],
       },
       {

+ 91 - 6
docs/.vitepress/theme/components/HomeSponsors.vue

@@ -1,15 +1,13 @@
 <script setup lang="ts">
 import { VPHomeSponsors } from 'vitepress/theme'
-import { useSponsor } from '../composables/sponsor'
 
-const { data } = useSponsor()
+/* const { data } = useSponsor() */
 </script>
 
 <template>
   <VPHomeSponsors
-    v-if="data"
     message="TresJS is free and open source, made possible by wonderful sponsors."
-    :data="data"
+    :data="[]"
   />
   <div class="action">
     <a
@@ -18,9 +16,94 @@ const { data } = useSponsor()
       target="_blank"
       rel="noreferrer"
     >
-      Become a sponsor <i class="i-carbon-heart"></i>
+      Become a sponsor
     </a>
   </div>
+  <div class="action">
+    <p class="message">Consider supporting our friends</p>
+  </div>
+  <ul class="action flex flex-wrap w-full sm:w-2/3 md:1/3 mx-auto">
+    <li>
+      <a
+        class="sponsor"
+        href="https://github.com/sponsors/vitejs"
+        target="_blank"
+        rel="noreferrer"
+      >
+        Vite <i class="i-logos-vitejs ml-2"></i>
+      </a>
+    </li>
+    <li>
+      <a
+        class="sponsor"
+        href="https://github.com/sponsors/patak-dev"
+        target="_blank"
+        rel="noreferrer"
+      >
+        Patak <img class="w-5 h-5 rounded ml-2" src="https://avatars.githubusercontent.com/u/583075?v=4" alt="" />
+      </a>
+    </li>
+    <li>
+      <a
+        class="sponsor"
+        href="https://github.com/sponsors/antfu"
+        target="_blank"
+        rel="noreferrer"
+      >
+        Anthony Fu <img class="w-5 h-5 rounded ml-2" src="https://avatars.githubusercontent.com/u/11247099?v=4" alt="" />
+      </a>
+    </li>
+    <li>
+      <a
+        class="sponsor"
+        href="https://github.com/sponsors/posva"
+        target="_blank"
+        rel="noreferrer"
+      >
+        posva <img class="w-5 h-5 rounded ml-2" src="https://avatars.githubusercontent.com/u/664177?v=4" alt="" />
+      </a>
+    </li>
+    <li>
+      <a
+        class="sponsor"
+        href="https://github.com/sponsors/johnsoncodehk"
+        target="_blank"
+        rel="noreferrer"
+      >
+        Johnson Chu <img class="w-5 h-5 rounded ml-2" src="https://avatars.githubusercontent.com/u/16279759?v=4" alt="" />
+      </a>
+    </li>
+    <li>
+      <a
+        class="sponsor"
+        href="https://github.com/sponsors/CodyJasonBennett"
+        target="_blank"
+        rel="noreferrer"
+      >
+        Cody Bennet <img class="w-5 h-5 rounded ml-2" src="https://avatars.githubusercontent.com/u/23324155?v=4" alt="" />
+      </a>
+    </li>
+    <li>
+      <a
+        class="sponsor"
+        href="https://github.com/sponsors/nuxt"
+        target="_blank"
+        rel="noreferrer"
+      >
+        Daniel Roe <img class="w-5 h-5 rounded ml-2" src="https://avatars.githubusercontent.com/u/23360933?s=160&v=4" alt="" />
+      </a>
+    </li>
+    <li>
+      <a
+        class="sponsor"
+        href="https://github.com/sponsors/danielroe"
+        target="_blank"
+        rel="noreferrer"
+      >
+        Nuxt <img class="w-5 h-5 rounded ml-2" src="https://avatars.githubusercontent.com/u/28706372?v=4" alt="" />
+      </a>
+    </li>
+  </ul>
 </template>
 
 <style scoped>
@@ -33,7 +116,9 @@ const { data } = useSponsor()
 
 .sponsor {
   /* .VPButton */
-  display: inline-block;
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
   border: 1px solid transparent;
   text-align: center;
   font-weight: 600;

+ 4 - 0
docs/.vitepress/theme/custom.css

@@ -58,3 +58,7 @@
   color: var(--vp-button-alt-text);
   background-color: var(--vp-button-alt-bg);
 }
+
+.VPHomeSponsors {
+  margin-bottom: 0px !important;
+}

+ 31 - 0
docs/advanced/performance.md

@@ -124,3 +124,34 @@ const { advance } = useTres()
 advance()
 </script>
 ```
+
+## Dispose resources `dispose()` <Badge type="tip" text="^4.0.0" />
+
+When you are done with a resource, like a texture, geometry, or material, you should dispose of it to free up memory. This is especially important when you are creating and destroying resources frequently, like in a game.
+
+TresJS will automatically dispose of resources recursively when the component is unmounted, but you can also perform this manually by calling the `dispose()` directly from the package:
+
+::: warning
+To avoid errors and unwanted sideeffects, resources created programatically with the use of `primitives` need to be manually disposed.
+:::
+
+```html {2,12}
+<script setup lang="ts">
+  import { dispose } from '@tresjs/core'
+  import { useGLTF } from '@tresjs/cientos'
+
+  const { nodes } = await useGLTF(
+    'https://raw.githubusercontent.com/Tresjs/assets/main/models/gltf/blender-cube.glb',
+    { draco: true },
+  )
+  const model = nodes.Cube
+
+  onUnmounted(() => {
+    dispose(model)
+  })
+</script>
+
+<template>
+  <primitive :object="model" />
+</template>
+```

+ 52 - 12
docs/api/events.md

@@ -6,22 +6,62 @@
 
 ## Pointer Events
 
+The following pointer events are available on `v3` and previous:
+
+- `click`
+- `pointer-move`
+- `pointer-enter`
+- `pointer-leave`
+
+From `v4.x` on, the following pointer events are been added to the list:
+
+- `context-menu` (right click)
+- `double-click`
+- `pointer-down`
+- `pointer-up`
+- `wheel`
+- `pointer-missed`
+
 ```html
 <TresMesh
-  @click="(intersection, pointerEvent) => console.log('click', intersection, pointerEvent)"
-  @pointer-move="(intersection, pointerEvent) => console.log('pointer-move', intersection, pointerEvent)"
-  @pointer-enter="(intersection, pointerEvent) => console.log('pointer-enter', intersection, pointerEvent)"
-  @pointer-leave="(intersection, pointerEvent) => console.log('pointer-leave', pointerEvent)"
+  @click="(event) => console.log('click')"
+  @context-menu="(event) => console.log('context-menu (right click)')"
+  @double-click="(event) => console.log('double-click')"
+  @pointer-move="(event) => console.log('pointer-move')"
+  @pointer-enter="(event) => console.log('pointer-enter')"
+  @pointer-leave="(event) => console.log('pointer-leave')"
+  @pointer-down="(event) => console.log('pointer-down')"
+  @pointer-up="(event) => console.log('pointer-up')"
+  @wheel="(event) => console.log('wheel')"
+  @pointer-missed="(event) => console.log('pointer-missed')"
 />
 ```
 
-| Event         | fires when ...                                                                        | Event Handler Parameter Type(s)                                                                                                                                                                       |
-| ------------- | ------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| click        | ... the events pointerdown and pointerup fired on the same object one after the other | [Intersection](https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/three/src/core/Raycaster.d.ts#L16), [PointerEvent](https://developer.mozilla.org/en-US/docs/Web/API/PointerEvent) |
-| pointer-move  | ... the pointer is moving above the object                                            | [Intersection](https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/three/src/core/Raycaster.d.ts#L16), [PointerEvent](https://developer.mozilla.org/en-US/docs/Web/API/PointerEvent) |
-| pointer-enter | ... the pointer is entering the object                                                | [Intersection](https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/three/src/core/Raycaster.d.ts#L16), [PointerEvent](https://developer.mozilla.org/en-US/docs/Web/API/PointerEvent) |
-| pointer-leave | ... the pointer is leaves the object                                                  | [PointerEvent](https://developer.mozilla.org/en-US/docs/Web/API/PointerEvent)                                                                                                                         |
+| <div style="width:160px">Event</div>            | fires when ...                                                                       | Event Handler Parameter Type(s)                                                                                                                                                                       |
+| ---------------- | ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| click            | the events pointerdown and pointerup fired on the same object one after the other    | [Intersection](https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/three/src/core/Raycaster.d.ts#L16), [PointerEvent](https://developer.mozilla.org/en-US/docs/Web/API/PointerEvent) |
+| contextMenu <Badge type="warning" text="4.0.0" />     | the user triggers a context menu, often by right-clicking                            | [PointerEvent](https://developer.mozilla.org/en-US/docs/Web/API/PointerEvent)                                                                                                                         |
+| double-click <Badge type="warning" text="4.0.0" />      | the user clicks the mouse button twice in quick succession on the same object        | [Intersection](https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/three/src/core/Raycaster.d.ts#L16), [PointerEvent](https://developer.mozilla.org/en-US/docs/Web/API/PointerEvent) |
+| wheel <Badge type="warning" text="4.0.0" />              | the mouse wheel or similar device is rotated                                         | [WheelEvent](https://developer.mozilla.org/en-US/docs/Web/API/WheelEvent)                                                                                                                             |
+| pointer-down <Badge type="warning" text="4.0.0" />       | the pointer is pressed down over the object                                          | [Intersection](https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/three/src/core/Raycaster.d.ts#L16), [PointerEvent](https://developer.mozilla.org/en-US/docs/Web/API/PointerEvent) |
+| pointer-up <Badge type="warning" text="4.0.0" />        | the pointer is released over the object                                              | [Intersection](https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/three/src/core/Raycaster.d.ts#L16), [PointerEvent](https://developer.mozilla.org/en-US/docs/Web/API/PointerEvent) |
+| pointer-leave    | the pointer is leaves the object                                                     | [PointerEvent](https://developer.mozilla.org/en-US/docs/Web/API/PointerEvent)                                                                                                                         |
+| pointer-move     | the pointer is moving above the object                                               | [Intersection](https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/three/src/core/Raycaster.d.ts#L16), [PointerEvent](https://developer.mozilla.org/en-US/docs/Web/API/PointerEvent) |
+| pointer-missed <Badge type="warning" text="4.0.0" />    | the pointer interaction is attempted but misses the object                           | [PointerEvent](https://developer.mozilla.org/en-US/docs/Web/API/PointerEvent)                                                                                                                         |
+
+## Event Propagation (Bubbling 🫧) <Badge type="warning" text="^4.0.0" />
+
+Propagation of events on 3D scenes works differently than in the DOM because objects can **occlude each other in 3D**. The `intersections` array contains all the objects that the raycaster intersects with, sorted by distance from the camera. The first object in the array is the closest one to the camera.
 
-The returned [Intersection](https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/three/src/core/Raycaster.d.ts#L16) includes the [Object3D](https://threejs.org/docs/index.html?q=object#api/en/core/Object3D) that triggered the event. You can access it via `intersection.object`.
+When an event is triggered, the event is propagated to the closest object in the `intersections` array. If the event is not handled by the object, it will be propagated to the next object in the array.
 
-By default, objects positioned in front of others with event handlers do not prevent those events from being triggered. This behavior can be achieved by using the prop `blocks-pointer-events`.
+`event.stopPropagation()` can be used to stop the event from propagating to the next object in the array, stoping the event from bubbling up and reaching to farther objects (the oens behind the first one).
+
+```html
+<TresMesh
+  @pointer-down="(event) => {
+    console.log('pointer-down')
+    event.stopPropagation()
+  }"
+/>
+```

+ 6 - 0
docs/debug/devtools.md

@@ -24,3 +24,9 @@ From <Badge text="^3.7.0" /> we are introducing the TresJS Devtools, a customize
 ![](/devtools-scene-inspector.png)
 
 Enjoy the new Devtools and let us know what you think! 🎉
+
+## Renderer info <Badge text="^4.0.0" />
+
+From `v4` it's possible to see the renderer information in the Devtools when inspecting the root object (Scene). This is useful to know what renderer is being used and its properties including the programs (shaders) and the capabilities of the renderer.
+
+![](/devtools-v4.png)

+ 4 - 4
docs/directives/v-always-look-at.md

@@ -6,8 +6,8 @@ With the new directive v-always-look-at provided by **TresJS**, you can add easi
 
 ```vue{3,9}
 <script setup lang="ts">
-import { TresCanvas } from '@tresjs/core'
-import { Box, vAlwaysLookAt } from '@tresjs/cientos'
+import { TresCanvas, vAlwaysLookAt } from '@tresjs/core'
+import { Box } from '@tresjs/cientos'
 </script>
 <template>
     <TresCanvas >
@@ -33,8 +33,8 @@ Another advantage is that you can look at an instance in movement, for example w
 ```vue{4,6,20,23}
 <script setup lang="ts">
 import { shallowRef } from 'vue'
-import { TresCanvas, useRenderLoop } from '@tresjs/core'
-import { Box, vAlwaysLookAt } from '@tresjs/cientos'
+import { TresCanvas, useRenderLoop, vAlwaysLookAt } from '@tresjs/core'
+import { Box } from '@tresjs/cientos'
 
 const sphereRef = shallowRef()
 

+ 2 - 1
docs/directives/v-distance-to.md

@@ -8,7 +8,8 @@ In addition, an arrow will be created to indicate which objects you're measuring
 
 ```vue{2,8,13}
 <script setup lang="ts">
-import { OrbitControls, Sphere, vLog } from '@tresjs/cientos'
+import { vDistanceTo } from '@tresjs/core'
+import { OrbitControls, Sphere } from '@tresjs/cientos'
 </script>
 <template>
   <TresCanvas v-bind="gl">

+ 2 - 1
docs/directives/v-light-helper.md

@@ -12,7 +12,8 @@ The following lights are supported:
 
 ```vue{2,8,11,14,17}
 <script setup lang="ts">
-import { OrbitControls, Sphere, vLightHelper } from '@tresjs/cientos'
+import { vLightHelper } from '@tresjs/core'
+import { OrbitControls, Sphere,  } from '@tresjs/cientos'
 </script>
 <template>
   <TresCanvas >

+ 2 - 1
docs/directives/v-log.md

@@ -35,7 +35,8 @@ With the new directive v-log provided by **TresJS**, you can do this by just add
 
 ```vue{2,10,12}
 <script setup lang="ts">
-import { OrbitControls, Sphere, vLog } from '@tresjs/cientos'
+import { vLog } from '@tresjs/core'
+import { OrbitControls, Sphere } from '@tresjs/cientos'
 </script>
 <template>
     <TresCanvas >

+ 80 - 0
docs/directives/v-rotate.md

@@ -0,0 +1,80 @@
+# v-rotate
+
+## Problem
+
+When you want to simply add rotation to your mesh, you have to use the template reference, [useRenderLoop](/api/composables#userenderloop) and then assign the axis and the speed, but before check if you mesh is already available:
+
+```vue
+<script setup lang="ts">
+import { shallowRef, watch } from 'vue'
+import { useRenderLoop } from '@tresjs/core'
+
+const boxRef = shallowRef()
+
+const { onLoop } = useRenderLoop()
+
+onLoop(({ elapsed }) => {
+  if (boxRef.value) {
+    boxRef.value.rotation.x = elapsed
+  }
+})
+</script>
+
+<template>
+  <TresCanvas>
+    <TresPerspectiveCamera :position="[0, 2, 5]" />
+    <TresMesh
+      ref="boxRef"
+      :scale="0.5"
+    >
+      <TresBoxGeometry />
+      <TresMesh>
+        <OrbitControls />
+      </TresMesh>
+    </TresMesh>
+  </TresCanvas>
+</template>
+```
+
+And is A LOT of code just for a simple rotation right? Normally we need something fast to see if something is working
+
+## Usage
+
+With the new directive v-rotate provided by **TresJS**, you can do this by just adding `v-rotate` to the instance.
+
+```vue{2,8}
+<script setup lang="ts">
+import { vRotate } from '@tresjs/core'
+</script>
+<template>
+    <TresCanvas >
+    <TresPerspectiveCamera :position="[0, 2, 5]" />
+    <TresMesh
+      v-rotate // 😍
+    >
+      <TresBoxGeometry />
+    </TresMesh>
+  </TresCanvas>
+</template>
+```
+By default `v-rotate` uses [Quaternions](https://threejs.org/docs/index.html?q=quater#api/en/math/Quaternion) so you don't have to worry by [Gimbal Lock](https://en.wikipedia.org/wiki/Gimbal_lock), or check if you mesh is available in the first frames.
+
+## Modifiers
+
+You can control the axis and the rotation speed by adding modifiers
+
+```vue{2,8}
+<script setup lang="ts">
+import { vRotate } from '@tresjs/core'
+</script>
+<template>
+    <TresCanvas >
+    <TresPerspectiveCamera :position="[0, 2, 5]" />
+    <TresMesh
+      v-rotate:x.y="0.1" // the axis will be x and y with a speed of 0.1
+    >
+      <TresBoxGeometry />
+    </TresMesh>
+  </TresCanvas>
+</template>
+```

+ 3 - 1
docs/package.json

@@ -12,7 +12,9 @@
     "@tresjs/core": "workspace:^"
   },
   "devDependencies": {
-    "unocss": "^0.58.3",
+    "@iconify-json/logos": "^1.1.42",
+    "@iconify-json/mdi": "^1.1.66",
+    "unocss": "^0.59.4",
     "vite-svg-loader": "^5.1.0"
   }
 }

BIN
docs/public/devtools-v4.png


+ 15 - 1
docs/vite.config.ts

@@ -2,11 +2,25 @@ import { defineConfig } from 'vite'
 import Unocss from 'unocss/vite'
 import svgLoader from 'vite-svg-loader'
 import Components from 'unplugin-vue-components/vite'
+import { presetIcons, presetUno } from 'unocss'
 
 export default defineConfig({
   plugins: [
     svgLoader(),
-    Unocss(),
+    Unocss({
+      presets: [
+        presetUno(),
+        presetIcons({
+          scale: 1.2,
+          warn: true,
+          extraProperties: {
+            'display': 'inline-block',
+            'vertical-align': 'middle',
+            // ...
+          },
+        }),
+      ],
+    }),
     Components({
       // allow auto load markdown components under `.vitepress/theme/components`
       dirs: ['.vitepress/theme/components'],

+ 15 - 15
package.json

@@ -1,7 +1,7 @@
 {
   "name": "@tresjs/core",
   "type": "module",
-  "version": "4.0.0-next.2",
+  "version": "4.0.0-rc.0",
   "packageManager": "pnpm@8.15.6",
   "description": "Declarative ThreeJS using Vue Components",
   "author": "Alvaro Saburido <hola@alvarosaburido.dev> (https://github.com/alvarosabu/)",
@@ -77,15 +77,15 @@
     "@tresjs/cientos": "3.8.0",
     "@tresjs/eslint-config": "^1.0.0",
     "@types/three": "^0.163.0",
-    "@typescript-eslint/eslint-plugin": "^7.5.0",
-    "@typescript-eslint/parser": "^7.5.0",
+    "@typescript-eslint/eslint-plugin": "^7.7.1",
+    "@typescript-eslint/parser": "^7.7.1",
     "@vitejs/plugin-vue": "^5.0.4",
     "@vitest/coverage-c8": "^0.33.0",
     "@vitest/coverage-v8": "^1.5.0",
-    "@vitest/ui": "^1.4.0",
+    "@vitest/ui": "^1.5.0",
     "@vue/test-utils": "^2.4.5",
-    "eslint": "^8.57.0",
-    "eslint-plugin-vue": "^9.24.0",
+    "eslint": "^9.1.1",
+    "eslint-plugin-vue": "^9.25.0",
     "esno": "^4.7.0",
     "gsap": "^3.12.5",
     "husky": "^9.0.11",
@@ -93,23 +93,23 @@
     "kolorist": "^1.8.0",
     "ohmyfetch": "^0.4.21",
     "pathe": "^1.1.2",
-    "release-it": "^17.1.1",
+    "release-it": "^17.2.0",
     "rollup-plugin-analyzer": "^4.0.0",
     "rollup-plugin-copy": "^3.5.0",
     "rollup-plugin-visualizer": "^5.12.0",
     "three": "^0.163.0",
-    "unocss": "^0.58.7",
-    "unplugin": "^1.10.0",
+    "unocss": "^0.59.4",
+    "unplugin": "^1.10.1",
     "unplugin-vue-components": "^0.26.0",
-    "vite": "^5.2.7",
+    "vite": "^5.2.10",
     "vite-plugin-banner": "^0.7.1",
-    "vite-plugin-dts": "3.8.1",
-    "vite-plugin-inspect": "^0.8.3",
+    "vite-plugin-dts": "3.9.0",
+    "vite-plugin-inspect": "^0.8.4",
     "vite-plugin-require-transform": "^1.0.21",
     "vite-svg-loader": "^5.1.0",
-    "vitepress": "1.1.0",
-    "vitest": "^1.0.4",
-    "vue": "^3.4.21",
+    "vitepress": "1.1.3",
+    "vitest": "^1.5.0",
+    "vue": "^3.4.24",
     "vue-demi": "^0.14.7"
   }
 }

+ 2 - 22
playground/components.d.ts

@@ -8,8 +8,8 @@ export {}
 declare module 'vue' {
   export interface GlobalComponents {
     AkuAku: typeof import('./src/components/AkuAku.vue')['default']
-    AnimatedModel: typeof import('./src/components/AnimatedModel.vue')['default']
     BlenderCube: typeof import('./src/components/BlenderCube.vue')['default']
+    Box: typeof import('./src/components/Box.vue')['default']
     CameraOperator: typeof import('./src/components/CameraOperator.vue')['default']
     Cameras: typeof import('./src/components/Cameras.vue')['default']
     copy: typeof import('./src/components/TheBasic copy.vue')['default']
@@ -17,38 +17,18 @@ declare module 'vue' {
     DebugUI: typeof import('./src/components/DebugUI.vue')['default']
     DeleteMe: typeof import('./src/components/DeleteMe.vue')['default']
     DynamicModel: typeof import('./src/components/DynamicModel.vue')['default']
+    EventsPropogation: typeof import('./src/components/EventsPropogation.vue')['default']
     FBXModels: typeof import('./src/components/FBXModels.vue')['default']
     Gltf: typeof import('./src/components/gltf/index.vue')['default']
     GraphPane: typeof import('./src/components/GraphPane.vue')['default']
     LocalOrbitControls: typeof import('./src/components/LocalOrbitControls.vue')['default']
-    MeshWobbleMaterial: typeof import('./src/components/meshWobbleMaterial/index.vue')['default']
-    MultipleCanvas: typeof import('./src/components/MultipleCanvas.vue')['default']
-    PortalJourney: typeof import('./src/components/portal-journey/index.vue')['default']
     RenderingLogger: typeof import('./src/components/RenderingLogger.vue')['default']
-    Responsiveness: typeof import('./src/components/Responsiveness.vue')['default']
     RouterLink: typeof import('vue-router')['RouterLink']
     RouterView: typeof import('vue-router')['RouterView']
-    ShadersExperiment: typeof import('./src/components/shaders-experiment/index.vue')['default']
     TestSphere: typeof import('./src/components/TestSphere.vue')['default']
     Text3D: typeof import('./src/components/Text3D.vue')['default']
-    TheBasic: typeof import('./src/components/TheBasic.vue')['default']
     TheCameraOperator: typeof import('./src/components/TheCameraOperator.vue')['default']
-    TheConditional: typeof import('./src/components/TheConditional.vue')['default']
-    TheEnvironment: typeof import('./src/components/TheEnvironment.vue')['default']
-    TheEvents: typeof import('./src/components/TheEvents.vue')['default']
     TheExperience: typeof import('./src/components/TheExperience.vue')['default']
-    TheFireFlies: typeof import('./src/components/portal-journey/TheFireFlies.vue')['default']
-    TheFirstScene: typeof import('./src/components/TheFirstScene.vue')['default']
-    TheGizmos: typeof import('./src/components/TheGizmos.vue')['default']
-    TheGroups: typeof import('./src/components/TheGroups.vue')['default']
-    TheModel: typeof import('./src/components/gltf/TheModel.vue')['default']
-    TheParticles: typeof import('./src/components/TheParticles.vue')['default']
-    ThePortal: typeof import('./src/components/portal-journey/ThePortal.vue')['default']
-    TheSmallExperience: typeof import('./src/components/TheSmallExperience.vue')['default']
     TheSphere: typeof import('./src/components/TheSphere.vue')['default']
-    TheUSDZModel: typeof import('./src/components/udsz/TheUSDZModel.vue')['default']
-    TresLechesTest: typeof import('./src/components/TresLechesTest.vue')['default']
-    Udsz: typeof import('./src/components/udsz/index.vue')['default']
-    VectorSetProps: typeof import('./src/components/VectorSetProps.vue')['default']
   }
 }

+ 5 - 5
playground/package.json

@@ -5,13 +5,13 @@
   "private": true,
   "scripts": {
     "dev": "vite --host",
-    "build": "vue-tsc && vite build",
+    "build": "vite build",
     "preview": "vite preview"
   },
   "dependencies": {
-    "@tresjs/cientos": "3.6.0",
+    "@tresjs/cientos": "3.8.0",
     "@tresjs/core": "workspace:^",
-    "vue-router": "^4.2.5"
+    "vue-router": "^4.3.2"
   },
   "devDependencies": {
     "@tresjs/leches": "0.15.0-next.3",
@@ -19,7 +19,7 @@
     "unplugin-auto-import": "^0.17.2",
     "vite-plugin-glsl": "^1.2.1",
     "vite-plugin-qrcode": "^0.2.3",
-    "vite-plugin-vue-devtools": "1.0.0-rc.6",
-    "vue-tsc": "^2.0.0"
+    "vite-plugin-vue-devtools": "7.1.2",
+    "vue-tsc": "^2.0.14"
   }
 }

+ 15 - 3
playground/src/components/BlenderCube.vue

@@ -1,15 +1,27 @@
 <script setup lang="ts">
-import { useTresContext } from '@tresjs/core'
+import { dispose } from '@tresjs/core'
 import { useGLTF } from '@tresjs/cientos'
+import { useControls } from '@tresjs/leches'
 
 const { nodes } = await useGLTF('https://raw.githubusercontent.com/Tresjs/assets/main/models/gltf/blender-cube.glb', { draco: true })
 const model = nodes.Cube
 
 model.position.set(0, 1, 0)
 
-const state = useTresContext()
+useControls({
+  disposeBtn: {
+    label: 'Dispose',
+    type: 'button',
+    onClick: () => {
+      dispose(model)
+    },
+    size: 'sm',
+  },
+})
 
-state.invalidate()
+onUnmounted(() => {
+  dispose(model)
+})
 </script>
 
 <template>

+ 44 - 0
playground/src/components/Box.vue

@@ -0,0 +1,44 @@
+<script setup lang="ts">
+import { ref, shallowRef } from 'vue'
+import { useRenderLoop } from '@tresjs/core'
+import { Color } from 'three'
+
+const props = defineProps(['position', 'name'])
+
+// TODO: Once we have troika text in cientos, display the count over each box
+const count = ref(0)
+const boxRef = shallowRef()
+
+// Event Testing Colors
+const black = new Color('black')
+const green = new Color('green')
+
+const blue = new Color('blue')
+
+// Once the box has flashed green, lerp it back to black
+const { onLoop } = useRenderLoop()
+onLoop(() => {
+  boxRef.value?.material.color.lerp(black, 0.1)
+})
+
+// onClick flash the box a color and update the counter
+function handleClick(color: Color, ev) {
+  count.value++
+  ev?.eventObject?.material.color.set(color)
+  // eslint-disable-next-line no-console
+  console.log(`Box ${boxRef.value.name} count=${count.value}`)
+}
+</script>
+
+<template>
+  <TresMesh
+    ref="boxRef"
+    v-bind="props"
+    @click.self="ev => handleClick(green, ev)"
+    @pointer-missed="ev => handleClick(blue, ev)"
+  >
+    <TresBoxGeometry />
+    <TresMeshStandardMaterial />
+    <slot></slot>
+  </TresMesh>
+</template>

+ 0 - 1
playground/src/main.ts

@@ -7,5 +7,4 @@ import 'uno.css'
 const app = createApp(App)
 
 app.use(router)
-
 app.mount('#app')

+ 13 - 3
playground/src/pages/basic/Lights.vue

@@ -1,6 +1,6 @@
 <script setup lang="ts">
 import type { TresObject } from '@tresjs/core'
-import { TresCanvas, vAlwaysLookAt, vDistanceTo, vLightHelper, vLog } from '@tresjs/core'
+import { TresCanvas, vAlwaysLookAt, vDistanceTo, vLightHelper, vLog, vRotate } from '@tresjs/core'
 import { BasicShadowMap, NoToneMapping, SRGBColorSpace } from 'three'
 
 import { OrbitControls } from '@tresjs/cientos'
@@ -22,7 +22,11 @@ const planeRef: Ref<TresObject | null> = ref(null)
 
     v-bind="gl"
   >
-    <TresPerspectiveCamera :position="[3, 3, 3]" />
+    <TresPerspectiveCamera
+      v-distance-to="planeRef"
+      v-rotate
+      :position="[3, 3, 3]"
+    />
     <OrbitControls />
 
     <TresDirectionalLight
@@ -43,7 +47,13 @@ const planeRef: Ref<TresObject | null> = ref(null)
       <TresMeshToonMaterial />
     </TresMesh>
     <TresMesh
-      v-distance-to="planeRef"
+      v-rotate.x="0.005"
+      :position="[-2, 2, 0]"
+    >
+      <TresBoxGeometry :args="[1, 1, 1]" />
+      <TresMeshToonMaterial color="red" />
+    </TresMesh>
+    <TresMesh
       :position="[2, 4, 0]"
       cast-shadow
     >

+ 15 - 3
playground/src/pages/basic/index.vue

@@ -14,6 +14,7 @@ const state = reactive({
   toneMapping: NoToneMapping,
 })
 
+const canvasRef = ref()
 const sphereRef = ref()
 
 const { onLoop } = useRenderLoop()
@@ -21,6 +22,9 @@ const { onLoop } = useRenderLoop()
 onLoop(({ elapsed }) => {
   if (!sphereRef.value) { return }
   sphereRef.value.position.y += Math.sin(elapsed) * 0.01
+
+  // Update events without needing the mouse to move
+  canvasRef.value?.context?.eventManager.forceUpdate()
 })
 
 function onPointerEnter(ev) {
@@ -29,6 +33,10 @@ function onPointerEnter(ev) {
   }
 }
 
+function onPointerOut(ev) {
+  ev.object.material.color.set('teal')
+}
+
 const sphereExists = ref(true)
 </script>
 
@@ -37,7 +45,10 @@ const sphereExists = ref(true)
     v-model="sphereExists"
     type="checkbox"
   />
-  <TresCanvas v-bind="state">
+  <TresCanvas
+    ref="canvasRef"
+    v-bind="state"
+  >
     <TresPerspectiveCamera
       :position="[5, 5, 5]"
       :fov="45"
@@ -56,9 +67,10 @@ const sphereExists = ref(true)
         :position="[0, 4, 0]"
         cast-shadow
         @pointer-enter="onPointerEnter"
+        @pointer-out="onPointerOut"
       >
         <TresSphereGeometry :args="[2, 32, 32]" />
-        <TresMeshToonMaterial color="teal" />
+        <TresMeshBasicMaterial color="teal" />
       </TresMesh>
     </TresGroup>
 
@@ -72,7 +84,7 @@ const sphereExists = ref(true)
       receive-shadow
     >
       <TresPlaneGeometry :args="[10, 10, 10, 10]" />
-      <TresMeshToonMaterial />
+      <TresMeshBasicMaterial />
     </TresMesh>
 
     <TresDirectionalLight

+ 1 - 1
playground/src/pages/empty.vue

@@ -1,4 +1,4 @@
-<script setup>
+<script setup lang="ts">
 
 </script>
 

+ 188 - 0
playground/src/pages/events/Propagation.vue

@@ -0,0 +1,188 @@
+<script setup lang="ts">
+import { onUnmounted, ref } from 'vue'
+import {
+  TresCanvas,
+} from '@tresjs/core'
+import { BasicShadowMap, NoToneMapping, SRGBColorSpace } from 'three'
+import { OrbitControls } from '@tresjs/cientos'
+import '@tresjs/leches/styles'
+import Box from '../../components/Box.vue'
+
+const gl = {
+  clearColor: '#202020',
+  shadows: true,
+  alpha: false,
+  shadowMapType: BasicShadowMap,
+  outputColorSpace: SRGBColorSpace,
+  toneMapping: NoToneMapping,
+}
+
+const showBox = ref(true)
+
+const intervalRef = setInterval(() => {
+  // showBox.value = !showBox.value;
+}, 1000)
+
+onUnmounted(() => {
+  clearInterval(intervalRef)
+})
+</script>
+
+<template>
+  <TresCanvas
+    window-size
+    v-bind="gl"
+    @pointer-missed="event => console.log('pointer-missed', event)"
+  >
+    <TresPerspectiveCamera
+      :position="[0, 0, 6]"
+      :look-at="[0, 0, 0]"
+    />
+    <OrbitControls />
+
+    <TresDirectionalLight
+      :intensity="1"
+      :position="[1, 1, 1]"
+    />
+    <TresAmbientLight :intensity="1" />
+    <Box
+      :position="[0, 1.5, 0]"
+      name="A0"
+    >
+      <Box
+        :position="[-0.66, -1, 0]"
+        name="B0"
+      >
+        <Box
+          :position="[-0.66, -1, 0]"
+          name="C0"
+        >
+          <Box
+            :position="[-0.66, -1, 0]"
+            name="D0"
+          />
+          <Box
+            :position="[0.66, -1, 0]"
+            name="D1"
+          />
+        </Box>
+        <Box
+          :position="[0.66, -1, 0]"
+          name="C1"
+        >
+          <Box
+            :position="[0.66, -1, 0]"
+            name="D2"
+          />
+        </Box>
+      </Box>
+      <Box
+        :position="[0.66, -1, 0]"
+        name="B1"
+      >
+        <Box
+          :position="[0.66, -1, 0]"
+          name="C2"
+        >
+          <Box
+            v-if="showBox"
+            :position="[0.66, -1, 0]"
+            name="D3"
+          />
+        </Box>
+      </Box>
+    </Box>
+    <Box
+      :position="[0, 1.5, -3]"
+      name="A0"
+    >
+      <Box
+        :position="[-0.66, -1, 0]"
+        name="B0"
+      >
+        <Box
+          :position="[-0.66, -1, 0]"
+          name="C0"
+        >
+          <Box
+            :position="[-0.66, -1, 0]"
+            name="D0"
+          />
+          <Box
+            :position="[0.66, -1, 0]"
+            name="D1"
+          />
+        </Box>
+        <Box
+          :position="[0.66, -1, 0]"
+          name="C1"
+        >
+          <Box
+            :position="[0.66, -1, 0]"
+            name="D2"
+          />
+        </Box>
+      </Box>
+      <Box
+        :position="[0.66, -1, 0]"
+        name="B1"
+      >
+        <Box
+          :position="[0.66, -1, 0]"
+          name="C2"
+        >
+          <Box
+            :position="[0.66, -1, 0]"
+            name="D3"
+          />
+        </Box>
+      </Box>
+    </Box>
+    <Box
+      :position="[0, 1.5, -6]"
+      name="A0"
+    >
+      <Box
+        :position="[-0.66, -1, 0]"
+        name="B0"
+      >
+        <Box
+          :position="[-0.66, -1, 0]"
+          name="C0"
+        >
+          <Box
+            :position="[-0.66, -1, 0]"
+            name="D0"
+          />
+          <Box
+            :position="[0.66, -1, 0]"
+            name="D1"
+          />
+        </Box>
+        <Box
+          :position="[0.66, -1, 0]"
+          name="C1"
+        >
+          <Box
+            :position="[0.66, -1, 0]"
+            name="D2"
+          />
+        </Box>
+      </Box>
+      <Box
+        :position="[0.66, -1, 0]"
+        name="B1"
+      >
+        <Box
+          :position="[0.66, -1, 0]"
+          name="C2"
+        >
+          <Box
+            :position="[0.66, -1, 0]"
+            name="D3"
+          />
+        </Box>
+      </Box>
+    </Box>
+  </TresCanvas>
+</template>

+ 67 - 50
playground/src/pages/events/index.vue

@@ -1,8 +1,11 @@
+<!-- eslint-disable no-console -->
 <script setup lang="ts">
+import type { ThreeEvent } from '@tresjs/core'
 import { TresCanvas } from '@tresjs/core'
 import { BasicShadowMap, NoToneMapping, SRGBColorSpace } from 'three'
-
+import { TresLeches, useControls } from '@tresjs/leches'
 import { OrbitControls } from '@tresjs/cientos'
+import '@tresjs/leches/styles'
 
 const gl = {
   clearColor: '#202020',
@@ -13,65 +16,79 @@ const gl = {
   toneMapping: NoToneMapping,
 }
 
-function onClick(ev) {
-  if (ev) {
-    ev.object.material.color.set('#008080')
-  }
+const { stopPropagation } = useControls({
+  stopPropagation: false,
+})
+
+function onClick(ev: ThreeEvent<MouseEvent>) {
+  console.log('click', ev)
+  if (stopPropagation.value) { ev.stopPropagation() }
+  ev.object.material.color.set('#008080')
+}
+
+function onDoubleClick(ev: ThreeEvent<MouseEvent>) {
+  console.log('double-click', ev)
+  if (stopPropagation.value) { ev.stopPropagation() }
+  ev.object.material.color.set('#FFD700')
 }
 
-function onPointerEnter(ev) {
-  if (ev) {
-    ev.object.material.color.set('#CCFF03')
-  }
+function onPointerEnter(ev: ThreeEvent<MouseEvent>) {
+  if (stopPropagation.value) { ev.stopPropagation() }
+  ev.object.material.color.set('#CCFF03')
 }
 
-function onPointerLeave(ev) {
-  if (ev) {
-    /*  ev.object.material.color.set('#efefef') */
-  }
+function onPointerLeave(ev: ThreeEvent<MouseEvent>) {
+  if (stopPropagation.value) { ev.stopPropagation() }
+  /*  ev.object.material.color.set('#efefef') */
 }
 
-function onPointerMove(ev) {
-  if (ev) {
-    // eslint-disable-next-line no-console
-    console.log(ev)
-  }
+function onPointerMove(ev: ThreeEvent<MouseEvent>) {
+  if (stopPropagation.value) { ev.stopPropagation() }
 }
 
-const visible = ref(true)
+function onContextMenu(ev: ThreeEvent<MouseEvent>) {
+  console.log('context-menu', ev)
+  if (stopPropagation.value) { ev.stopPropagation() }
+  ev.object.material.color.set('#FF4500')
+}
+
+function onPointerMissed(ev: ThreeEvent<MouseEvent>) {
+  console.log('pointer-missed', ev)
+  if (stopPropagation.value) { ev.stopPropagation() }
+}
 </script>
 
 <template>
-  <button @click="visible = !visible"></button>
-  <div v-if="visible">
-    <TresCanvas
-      window-size
-      v-bind="gl"
-    >
-      <TresPerspectiveCamera
-        :position="[11, 11, 11]"
-        :look-at="[0, 0, 0]"
-      />
-      <OrbitControls />
-
-      <template v-for="x in [-2.5, 0, 2.5]">
-        <template v-for="y in [-2.5, 0, 2.5]">
-          <TresMesh
-            v-for="z in [-2.5, 0, 2.5]"
-            :key="`${[x, y, z]}`"
-            :position="[x, y, z]"
-            @click="onClick"
-            @pointer-enter="onPointerEnter"
-            @pointer-leave="onPointerLeave"
-            @pointer-move="onPointerMove"
-          >
-            <TresBoxGeometry :args="[1, 1, 1]" />
-            <TresMeshToonMaterial color="#efefef" />
-          </TresMesh>
-        </template>
+  <TresLeches />
+  <TresCanvas
+    window-size
+    v-bind="gl"
+  >
+    <TresPerspectiveCamera
+      :position="[11, 11, 11]"
+      :look-at="[0, 0, 0]"
+    />
+    <OrbitControls />
+    <template v-for="x in [-2.5, 0, 2.5]">
+      <template v-for="y in [-2.5, 0, 2.5]">
+        <TresMesh
+          v-for="z in [-2.5, 0, 2.5]"
+          :key="`${[x, y, z]}`"
+          :position="[x, y, z]"
+          @click="onClick"
+          @double-click="onDoubleClick"
+          @pointer-enter="onPointerEnter"
+          @pointer-leave="onPointerLeave"
+          @pointer-move="onPointerMove"
+          @context-menu="onContextMenu"
+          @pointer-missed="onPointerMissed"
+        >
+          <TresBoxGeometry :args="[1, 1, 1]" />
+          <TresMeshToonMaterial color="#efefef" />
+        </TresMesh>
       </template>
-      <TresDirectionalLight :intensity="1" />
-      <TresAmbientLight :intensity="1" />
-    </TresCanvas>
-  </div>
+    </template>
+    <TresDirectionalLight :intensity="1" />
+    <TresAmbientLight :intensity="1" />
+  </TresCanvas>
 </template>

+ 65 - 0
playground/src/pages/perf/Memory.vue

@@ -0,0 +1,65 @@
+<script setup lang="ts">
+import { TresCanvas } from '@tresjs/core'
+import { BasicShadowMap, NoToneMapping, SRGBColorSpace } from 'three'
+
+import { TresLeches, useControls } from '@tresjs/leches'
+import '@tresjs/leches/styles'
+
+const gl = {
+  clearColor: '#fff',
+  shadows: true,
+  alpha: false,
+  shadowMapType: BasicShadowMap,
+  outputColorSpace: SRGBColorSpace,
+  toneMapping: NoToneMapping,
+}
+
+const { isVisible } = useControls({
+  isVisible: true,
+})
+
+/* const mesh = new Mesh(
+  new BoxGeometry(),
+  new MeshToonMaterial({ color: 0x00ff00 }),
+)
+ */
+
+onUnmounted(() => {
+  // dispose(mesh)
+})
+</script>
+
+<template>
+  <RouterLink to="/basic">
+    Go to another page
+  </RouterLink>
+  <TresLeches />
+  <TresCanvas v-bind="gl">
+    <TresPerspectiveCamera
+      :position="[3, 3, 3]"
+      :look-at="[0, 0, 0]"
+    />
+    <TresGroup v-if="isVisible">
+      <TresMesh :position="[0, 0, 0]">
+        <TresBoxGeometry />
+        <TresMeshToonMaterial :color="0x00FF00" />
+      </TresMesh>
+    </TresGroup>
+    <!--  <Suspense> -->
+    <!--    <BlenderC -->ube v-if="isVisible" />
+    <!--  </Suspense> -->
+    <!-- <TresMesh :position="[0,0,0]" v-if="isVisible">
+      <TresBoxGeometry />
+      <TresMeshToonMaterial :color="0x00ff00" />
+    </TresMesh> -->
+    <!--     <TresGridHelper /> -->
+    <!-- <TresGroup v-if="isVisible">
+      <TresMesh :position="[0,0,0]" >
+        <TresBoxGeometry />
+        <TresMeshToonMaterial :color="0x00ff00" />
+      </TresMesh>
+    </TresGroup> -->
+
+    <TresAmbientLight :intensity="1" />
+  </TresCanvas>
+</template>

+ 5 - 0
playground/src/router/index.ts

@@ -69,6 +69,11 @@ const routes = [
     name: 'Raycaster',
     component: () => import('./pages/raycaster/TheEvents.vue'),
   },
+  {
+    path: '/raycaster/propagation',
+    name: 'Event Propogation',
+    component: () => import('./pages/raycaster/Propagation.vue'),
+  },
   {
     path: '/misc/text-3d',
     name: 'Text3D',

+ 5 - 0
playground/src/router/routes/performance.ts

@@ -4,4 +4,9 @@ export const perfRoutes = [
     name: 'On Demand',
     component: () => import('../../pages/perf/OnDemand.vue'),
   },
+  {
+    path: '/perf/memory',
+    name: 'Memory',
+    component: () => import('../../pages/perf/Memory.vue'),
+  },
 ]

Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 304 - 250
pnpm-lock.yaml


+ 36 - 8
src/components/TresCanvas.vue

@@ -16,6 +16,7 @@ import {
   getCurrentInstance,
   h,
   onMounted,
+  onUnmounted,
   provide,
   ref,
   shallowRef,
@@ -27,16 +28,17 @@ import pkg from '../../package.json'
 import {
   type TresContext,
   useLogger,
-  usePointerEventHandler,
   useRenderLoop,
   useTresContextProvider,
+  useTresEventManager,
 } from '../composables'
 import { extend } from '../core/catalogue'
 import { nodeOps } from '../core/nodeOps'
+import { registerTresDevtools } from '../devtools'
+import { disposeObject3D } from '../utils/'
 
 import type { RendererPresetsType } from '../composables/useRenderer/const'
-import type { TresCamera, TresObject } from '../types/'
-import { registerTresDevtools } from '../devtools'
+import type { TresCamera, TresObject, TresScene } from '../types/'
 
 export interface TresCanvasProps
   extends Omit<WebGLRendererParameters, 'canvas'> {
@@ -72,7 +74,24 @@ const props = withDefaults(defineProps<TresCanvasProps>(), {
   renderMode: 'always',
 })
 
-const emit = defineEmits(['render'])
+// 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',
+])
+
 const slots = defineSlots<{
   default: () => any
 }>()
@@ -86,7 +105,7 @@ const canvas = ref<HTMLCanvasElement>()
  renderer uses it to mount the app nodes. This happens before `useTresContextProvider` is called.
  The custom renderer requires `scene` to be editable (not readonly).
 */
-const scene = shallowRef(new Scene())
+const scene = shallowRef<TresScene | Scene>(new Scene())
 
 const { resume } = useRenderLoop()
 
@@ -117,12 +136,15 @@ const mountCustomRenderer = (context: TresContext) => {
 }
 
 const dispose = (context: TresContext, force = false) => {
-  scene.value.children = []
+  disposeObject3D(context.scene.value as unknown as TresObject)
   if (force) {
     context.renderer.value.dispose()
     context.renderer.value.renderLists.dispose()
     context.renderer.value.forceContextLoss()
   }
+  (scene.value as TresScene).__tres = {
+    root: context,
+  }
   mountCustomRenderer(context)
   resume()
 }
@@ -132,11 +154,12 @@ const disableRender = computed(() => props.disableRender)
 const context = shallowRef<TresContext | null>(null)
 
 defineExpose({ context, dispose: () => dispose(context.value as TresContext, true) })
+
 onMounted(() => {
   const existingCanvas = canvas as Ref<HTMLCanvasElement>
 
   context.value = useTresContextProvider({
-    scene: scene.value,
+    scene: scene.value as TresScene,
     canvas: existingCanvas,
     windowSize: props.windowSize ?? false,
     disableRender: disableRender.value ?? false,
@@ -144,7 +167,7 @@ onMounted(() => {
     emit,
   })
 
-  usePointerEventHandler(context.value)
+  useTresEventManager(scene.value, context.value, emit)
 
   const { registerCamera, camera, cameras, deregisterCamera } = context.value
 
@@ -192,8 +215,13 @@ onMounted(() => {
     addDefaultCamera()
   }
 
+  // HMR support
   if (import.meta.hot && context.value) { import.meta.hot.on('vite:afterUpdate', () => dispose(context.value as TresContext)) }
 })
+
+onUnmounted(() => {
+  dispose(context.value as TresContext)
+})
 </script>
 
 <template>

+ 1 - 0
src/composables/index.ts

@@ -8,3 +8,4 @@ export * from './useLogger'
 export * from './useSeek'
 export * from './usePointerEventHandler'
 export * from './useTresContextProvider'
+export * from './useTresEventManager'

+ 16 - 15
src/composables/usePointerEventHandler/index.ts

@@ -1,5 +1,6 @@
 import type { Intersection, Object3D, Object3DEventMap } from 'three'
 import { computed, reactive, ref } from 'vue'
+import type { TresObject } from 'src/types'
 import { uniqueBy } from '../../utils'
 import { useRaycaster } from '../useRaycaster'
 
@@ -27,26 +28,26 @@ export const usePointerEventHandler = (
 
   const blockingObjects = ref(new Set<Object3D>())
 
-  const registerBlockingObject = (object: Object3D) => {
-    blockingObjects.value.add(object)
+  const registerBlockingObject = (object: TresObject) => {
+    blockingObjects.value.add(object as Object3D)
   }
 
-  const deregisterBlockingObject = (object: Object3D) => {
-    blockingObjects.value.delete(object)
+  const deregisterBlockingObject = (object: TresObject) => {
+    blockingObjects.value.delete(object as Object3D)
   }
 
-  const deregisterObject = (object: Object3D) => {
-    Object.values(objectsWithEventListeners).forEach(map => map.delete(object))
+  const deregisterObject = (object: TresObject) => {
+    Object.values(objectsWithEventListeners).forEach(map => map.delete(object as Object3D))
     deregisterBlockingObject(object)
   }
 
-  const registerObject = (object: Object3D & EventProps) => {
+  const registerObject = (object: TresObject & EventProps) => {
     const { onClick, onPointerMove, onPointerEnter, onPointerLeave } = object
 
-    if (onClick) { objectsWithEventListeners.click.set(object, onClick) }
-    if (onPointerMove) { objectsWithEventListeners.pointerMove.set(object, onPointerMove) }
-    if (onPointerEnter) { objectsWithEventListeners.pointerEnter.set(object, onPointerEnter) }
-    if (onPointerLeave) { objectsWithEventListeners.pointerLeave.set(object, onPointerLeave) }
+    if (onClick) { objectsWithEventListeners.click.set(object as Object3D, onClick) }
+    if (onPointerMove) { objectsWithEventListeners.pointerMove.set(object as Object3D, onPointerMove) }
+    if (onPointerEnter) { objectsWithEventListeners.pointerEnter.set(object as Object3D, onPointerEnter) }
+    if (onPointerLeave) { objectsWithEventListeners.pointerLeave.set(object as Object3D, onPointerLeave) }
   }
 
   const objectsToWatch = computed(() =>
@@ -70,7 +71,7 @@ export const usePointerEventHandler = (
   const { onClick, onPointerMove } = useRaycaster(objectsToWatch, ctx)
 
   onClick(({ intersects, event }) => {
-    if (intersects.length) { objectsWithEventListeners.click.get(intersects[0].object)?.(intersects[0], event) }
+    if (intersects.length) { objectsWithEventListeners.click.get(intersects[0].object)?.(intersects[0], event as PointerEvent) }
   })
 
   let previouslyIntersectedObject: Object3D | null
@@ -80,12 +81,12 @@ export const usePointerEventHandler = (
 
     const { pointerLeave, pointerEnter, pointerMove } = objectsWithEventListeners
 
-    if (previouslyIntersectedObject && previouslyIntersectedObject !== firstObject) { pointerLeave.get(previouslyIntersectedObject)?.(previouslyIntersectedObject, event) }
+    if (previouslyIntersectedObject && previouslyIntersectedObject !== firstObject) { pointerLeave.get(previouslyIntersectedObject)?.(previouslyIntersectedObject, event as PointerEvent) }
 
     if (firstObject) {
-      if (previouslyIntersectedObject !== firstObject) { pointerEnter.get(firstObject)?.(intersects[0], event) }
+      if (previouslyIntersectedObject !== firstObject) { pointerEnter.get(firstObject)?.(intersects[0], event as PointerEvent) }
 
-      pointerMove.get(firstObject)?.(intersects[0], event)
+      pointerMove.get(firstObject)?.(intersects[0], event as PointerEvent)
     }
 
     previouslyIntersectedObject = firstObject || null

+ 136 - 30
src/composables/useRaycaster/index.ts

@@ -1,31 +1,22 @@
-import { Vector2 } from 'three'
-import type { Intersection, Object3D, Object3DEventMap } from 'three'
-import type { Ref } from 'vue'
-import { computed, onUnmounted } from 'vue'
+import { Vector2, Vector3 } from 'three'
+import type { Intersection, Object3D } from 'three'
+import type { Ref, ShallowRef } from 'vue'
+import { computed, onUnmounted, shallowRef } from 'vue'
 import type { EventHook } from '@vueuse/core'
 import { createEventHook, useElementBounding, usePointer } from '@vueuse/core'
 
+import type { DomEvent, TresCamera, TresEvent } from 'src/types'
 import type { TresContext } from '../useTresContextProvider'
 
-export type Intersects = Intersection<Object3D<Object3DEventMap>>[]
-interface PointerMoveEventPayload {
-  intersects?: Intersects
-  event: PointerEvent
-}
-
-interface PointerClickEventPayload {
-  intersects: Intersects
-  event: PointerEvent
-}
-
 export const useRaycaster = (
   objects: Ref<Object3D[]>,
   ctx: TresContext,
 ) => {
   // having a separate computed makes useElementBounding work
   const canvas = computed(() => ctx.renderer.value.domElement as HTMLCanvasElement)
-
+  const intersects: ShallowRef<Intersection[]> = shallowRef([])
   const { x, y } = usePointer({ target: canvas })
+  let delta = 0
 
   const { width, height, top, left } = useElementBounding(canvas)
 
@@ -43,10 +34,11 @@ export const useRaycaster = (
 
     ctx.raycaster.value.setFromCamera(new Vector2(x, y), ctx.camera.value)
 
-    return ctx.raycaster.value.intersectObjects(objects.value, false)
+    intersects.value = ctx.raycaster.value.intersectObjects(objects.value, true)
+    return intersects.value
   }
 
-  const getIntersects = (event?: PointerEvent | MouseEvent) => {
+  const getIntersects = (event?: DomEvent) => {
     const pointerPosition = getRelativePointerPosition({
       x: event?.clientX ?? x.value,
       y: event?.clientY ?? y.value,
@@ -56,39 +48,144 @@ export const useRaycaster = (
     return getIntersectsByRelativePointerPosition(pointerPosition) || []
   }
 
-  const intersects = computed<Intersects>(() => getIntersects())
-
-  const eventHookClick = createEventHook<PointerClickEventPayload>()
-  const eventHookPointerMove = createEventHook<PointerMoveEventPayload>()
+  const eventHookClick = createEventHook<TresEvent>()
+  const eventHookDblClick = createEventHook<TresEvent>()
+  const eventHookPointerMove = createEventHook<TresEvent>()
+  const eventHookPointerUp = createEventHook<TresEvent>()
+  const eventHookPointerDown = createEventHook<TresEvent>()
+  const eventHookPointerMissed = createEventHook<TresEvent>()
+  const eventHookContextMenu = createEventHook<TresEvent>()
+  const eventHookWheel = createEventHook<TresEvent>()
+
+  /* ({
+    ...DomEvent                   // All the original event data
+    ...Intersection               // All of Three's intersection data - see note 2
+    intersections: Intersection[] // The first intersection of each intersected object
+    object: Object3D              // The object that was actually hit (added to event payload in TresEventManager)
+    eventObject: Object3D         // The object that registered the event (added to event payload in TresEventManager)
+    unprojectedPoint: Vector3     // Camera-unprojected point
+    ray: Ray                      // The ray that was used to strike the object
+    camera: Camera                // The camera that was used in the raycaster
+    sourceEvent: DomEvent         // A reference to the host event
+    delta: number                 // Distance between mouse down and mouse up event in pixels
+  }) => ... */
+
+  // Mouse Event props aren't enumerable, so we can't be simple and use Object.assign or the spread operator
+  // Manually copies the mouse event props into a new object that we can spread in triggerEventHook
+  function copyMouseEventProperties(event: MouseEvent | PointerEvent | WheelEvent) {
+    const mouseEventProperties: any = {}
+
+    for (const property in event) {
+      // Copy all non-function properties
+      if (typeof property !== 'function') { mouseEventProperties[property] = (event as Record<string, any>)[property] }
+    }
+    return mouseEventProperties
+  }
 
-  const triggerEventHook = (eventHook: EventHook, event: PointerEvent | MouseEvent) => {
-    eventHook.trigger({ event, intersects: getIntersects(event) })
+  const triggerEventHook = (eventHook: EventHook, event: PointerEvent | MouseEvent | WheelEvent) => {
+    const eventProperties = copyMouseEventProperties(event)
+    const unprojectedPoint = new Vector3(event?.clientX, event?.clientY, 0).unproject(ctx.camera?.value as TresCamera)
+    eventHook.trigger({
+      ...eventProperties,
+      intersections: intersects.value,
+      // The unprojectedPoint is wrong, math needs to be fixed
+      unprojectedPoint,
+      ray: ctx.raycaster?.value.ray,
+      camera: ctx.camera?.value,
+      sourceEvent: event,
+      delta,
+      stopPropagating: false,
+    })
   }
 
+  let previousPointerMoveEvent: PointerEvent | undefined
   const onPointerMove = (event: PointerEvent) => {
+    // Update the raycast intersects
+    getIntersects(event)
     triggerEventHook(eventHookPointerMove, event)
+    previousPointerMoveEvent = event
   }
 
-  // a click event is fired whenever a pointerdown happened after pointerup on the same object
+  const forceUpdate = () => {
+    if (previousPointerMoveEvent) { onPointerMove(previousPointerMoveEvent) }
+  }
 
+  // a click event is fired whenever a pointerdown happened after pointerup on the same object
   let mouseDownObject: Object3D | undefined
+  let mouseDownPosition: Vector2
+  let mouseUpPosition: Vector2
 
   const onPointerDown = (event: PointerEvent) => {
-    mouseDownObject = getIntersects(event)[0]?.object
+    mouseDownObject = intersects.value[0]?.object
+
+    delta = 0
+    mouseDownPosition = new Vector2(
+      event?.clientX ?? x.value,
+      event?.clientY ?? y.value,
+    )
+
+    triggerEventHook(eventHookPointerDown, event)
   }
 
+  let previousClickObject: Object3D | undefined
+  let doubleClickConfirmed: boolean = false
+
   const onPointerUp = (event: MouseEvent) => {
     if (!(event instanceof PointerEvent)) { return } // prevents triggering twice on mobile devices
 
-    if (mouseDownObject === getIntersects(event)[0]?.object) { triggerEventHook(eventHookClick, event) }
+    // We missed every object, trigger the pointer missed event
+    if (intersects.value.length === 0) {
+      triggerEventHook(eventHookPointerMissed, event)
+    }
+
+    if (mouseDownObject === intersects.value[0]?.object) {
+      mouseUpPosition = new Vector2(
+        event?.clientX ?? x.value,
+        event?.clientY ?? y.value,
+      )
+
+      // Compute the distance between the mouse down and mouse up events
+      delta = mouseDownPosition?.distanceTo(mouseUpPosition)
+
+      if (event.button === 0) {
+        // Left click
+        triggerEventHook(eventHookClick, event)
+
+        if (previousClickObject === intersects.value[0]?.object) {
+          doubleClickConfirmed = true
+        }
+        else {
+          previousClickObject = intersects.value[0]?.object
+          doubleClickConfirmed = false
+        }
+      }
+      else if (event.button === 2) {
+        // Right click
+        triggerEventHook(eventHookContextMenu, event)
+      }
+    }
+
+    triggerEventHook(eventHookPointerUp, event)
   }
 
-  const onPointerLeave = (event: PointerEvent) => eventHookPointerMove.trigger({ event, intersects: [] })
+  const onDoubleClick = (event: MouseEvent) => {
+    if (doubleClickConfirmed) {
+      triggerEventHook(eventHookDblClick, event)
+      previousClickObject = undefined
+      doubleClickConfirmed = false
+    }
+  }
+
+  const onPointerLeave = (event: PointerEvent) => triggerEventHook(eventHookPointerMove, event)
+
+  const onWheel = (event: WheelEvent) => triggerEventHook(eventHookWheel, event)
 
   canvas.value.addEventListener('pointerup', onPointerUp)
   canvas.value.addEventListener('pointerdown', onPointerDown)
   canvas.value.addEventListener('pointermove', onPointerMove)
   canvas.value.addEventListener('pointerleave', onPointerLeave)
+  canvas.value.addEventListener('dblclick', onDoubleClick)
+  canvas.value.addEventListener('wheel', onWheel)
 
   onUnmounted(() => {
     if (!canvas?.value) { return }
@@ -96,11 +193,20 @@ export const useRaycaster = (
     canvas.value.removeEventListener('pointerdown', onPointerDown)
     canvas.value.removeEventListener('pointermove', onPointerMove)
     canvas.value.removeEventListener('pointerleave', onPointerLeave)
+    canvas.value.removeEventListener('dblclick', onDoubleClick)
+    canvas.value.removeEventListener('wheel', onWheel)
   })
 
   return {
     intersects,
-    onClick: (fn: (value: PointerClickEventPayload) => void) => eventHookClick.on(fn).off,
-    onPointerMove: (fn: (value: PointerMoveEventPayload) => void) => eventHookPointerMove.on(fn).off,
+    onClick: (fn: (value: TresEvent) => void) => eventHookClick.on(fn).off,
+    onDblClick: (fn: (value: TresEvent) => void) => eventHookDblClick.on(fn).off,
+    onContextMenu: (fn: (value: TresEvent) => void) => eventHookContextMenu.on(fn).off,
+    onPointerMove: (fn: (value: TresEvent) => void) => eventHookPointerMove.on(fn).off,
+    onPointerUp: (fn: (value: TresEvent) => void) => eventHookPointerUp.on(fn).off,
+    onPointerDown: (fn: (value: TresEvent) => void) => eventHookPointerDown.on(fn).off,
+    onPointerMissed: (fn: (value: TresEvent) => void) => eventHookPointerMissed.on(fn).off,
+    onWheel: (fn: (value: TresEvent) => void) => eventHookWheel.on(fn).off,
+    forceUpdate,
   }
 }

+ 2 - 2
src/composables/useRenderer/index.ts

@@ -10,7 +10,7 @@ import {
 
 import type { ColorSpace, Scene, ShadowMapType, ToneMapping, WebGLRendererParameters } from 'three'
 import { useLogger } from '../useLogger'
-import type { TresColor } from '../../types'
+import type { EmitEventFn, TresColor } from '../../types'
 import { useRenderLoop } from '../useRenderLoop'
 import { normalizeColor } from '../../utils/normalize'
 
@@ -108,7 +108,7 @@ export function useRenderer(
     canvas: MaybeRef<HTMLCanvasElement>
     scene: Scene
     options: UseRendererOptions
-    emit: (event: string, ...args: any[]) => void
+    emit: EmitEventFn
     contextParts: Pick<TresContext, 'sizes' | 'camera' | 'render'> & { invalidate: () => void, advance: () => void }
     disableRender: MaybeRefOrGetter<boolean>
   },

+ 10 - 7
src/composables/useTresContextProvider/index.ts

@@ -1,6 +1,6 @@
 import { useFps, useMemory, useRafFn } from '@vueuse/core'
 import { computed, inject, onUnmounted, provide, readonly, ref, shallowRef } from 'vue'
-import type { Camera, EventDispatcher, Object3D, WebGLRenderer } from 'three'
+import type { Camera, EventDispatcher, WebGLRenderer } from 'three'
 import { Raycaster } from 'three'
 import type { ComputedRef, DeepReadonly, MaybeRef, MaybeRefOrGetter, Ref, ShallowRef } from 'vue'
 import { calculateMemoryUsage } from '../../utils/perf'
@@ -9,8 +9,9 @@ import type { UseRendererOptions } from '../useRenderer'
 import { useRenderer } from '../useRenderer'
 import { extend } from '../../core/catalogue'
 import { useLogger } from '../useLogger'
-import type { TresScene } from '../../types'
+import type { EmitEventFn, TresObject, TresScene } from '../../types'
 import type { EventProps } from '../usePointerEventHandler'
+import type { TresEventManager } from '../useTresEventManager'
 import useSizes, { type SizesType } from '../useSizes'
 
 export interface InternalState {
@@ -68,13 +69,14 @@ export interface TresContext {
   registerCamera: (camera: Camera) => void
   setCameraActive: (cameraOrUuid: Camera | string) => void
   deregisterCamera: (camera: Camera) => 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
   // When thats done maybe we can short the names of the methods since the parent will give the context.
-  registerObjectAtPointerEventHandler: (object: Object3D & EventProps) => void
-  deregisterObjectAtPointerEventHandler: (object: Object3D) => void
-  registerBlockingObjectAtPointerEventHandler: (object: Object3D) => void
-  deregisterBlockingObjectAtPointerEventHandler: (object: Object3D) => void
+  registerObjectAtPointerEventHandler?: (object: TresObject & EventProps) => void
+  deregisterObjectAtPointerEventHandler?: (object: TresObject) => void
+  registerBlockingObjectAtPointerEventHandler?: (object: TresObject) => void
+  deregisterBlockingObjectAtPointerEventHandler?: (object: TresObject) => void
 }
 
 export function useTresContextProvider({
@@ -90,7 +92,8 @@ export function useTresContextProvider({
   windowSize: MaybeRefOrGetter<boolean>
   disableRender: MaybeRefOrGetter<boolean>
   rendererOptions: UseRendererOptions
-  emit: (event: string, ...args: any[]) => void
+  emit: EmitEventFn
+
 }): TresContext {
   const { logWarning } = useLogger()
 

+ 194 - 0
src/composables/useTresEventManager/index.ts

@@ -0,0 +1,194 @@
+import { computed, shallowRef } from 'vue'
+import type { Object3D, Object3DEventMap, Scene } from 'three'
+import type { EmitEventFn, EmitEventName, Intersection, TresEvent, TresObject } from 'src/types'
+import type { TresContext } from '../useTresContextProvider'
+import { useRaycaster } from '../useRaycaster'
+import { hyphenate } from '../../utils'
+
+export interface TresEventManager {
+  /**
+   * Forces the event system to refire events with the previous mouse event
+   */
+  forceUpdate: () => void
+  /**
+   * pointer-missed events by definition are fired when the pointer missed every object in the scene
+   * So we need to track them separately
+   * Note: These are used in nodeOps
+   */
+  registerPointerMissedObject: (object: TresObject) => void
+  deregisterPointerMissedObject: (object: TresObject) => void
+}
+
+export function useTresEventManager(
+  scene: Scene,
+  context: TresContext,
+  emit: EmitEventFn,
+) {
+  const _scene = shallowRef<Scene>()
+  const _context = shallowRef<TresContext>()
+
+  if (scene) { _scene.value = scene }
+  if (context) { _context.value = context }
+
+  // TODO: Optimize to not hit test on the whole scene
+  const sceneChildren = computed(() =>
+    _scene.value ? _scene.value.children : [],
+  )
+
+  function executeEventListeners(
+    listeners: Function | Function[],
+    event: TresEvent,
+  ) {
+    // Components with multiple event listeners will have an array of functions
+    if (Array.isArray(listeners)) {
+      for (const listener of listeners) {
+        listener(event)
+      }
+    }
+
+    // Single listener will be a function
+    if (typeof listeners === 'function') {
+      listeners(event)
+    }
+  }
+
+  /**
+   * propogateEvent
+   *
+   * Propogates an event to all intersected objects and their parents
+   * @param eventName - The name of the event to propogate
+   * @param event - The event object to propogate
+   */
+  function propogateEvent(eventName: string, event: TresEvent) {
+    // Array of objects we've already propogated to
+    const duplicates = []
+
+    // Flag that is set to true when the stopProgatingFn is called
+    const stopPropagatingFn = () => (event.stopPropagating = true)
+    event.stopPropagation = stopPropagatingFn
+
+    // Loop through all intersected objects and call their event handler
+    for (const intersection of event?.intersections) {
+      if (event.stopPropagating) { return }
+
+      // Add intersection data to event object
+      event = { ...event, ...intersection }
+
+      const { object } = intersection
+      event.eventObject = object as TresObject
+      executeEventListeners((object as Record<string, any>)[eventName], event)
+      duplicates.push(object)
+
+      // Propogate the event up the parent chain before moving on to the next intersected object
+      let parentObj = object.parent
+      while (parentObj !== null && !event.stopPropagating) {
+        // We've already been here, break the loop
+        if (duplicates.includes(parentObj)) {
+          break
+        }
+
+        // Sets eventObject to object that registered the event listener
+        event.eventObject = parentObj as TresObject
+        executeEventListeners((parentObj as Record<string, any>)[eventName], event)
+        duplicates.push(parentObj)
+        parentObj = parentObj.parent
+      }
+
+      // Convert eventName to kebab case and emit event from TresCanvas
+      const kebabEventName = hyphenate(eventName.slice(2)) as EmitEventName
+      emit(kebabEventName, { intersection, event })
+    }
+  }
+
+  const {
+    onClick,
+    onDblClick,
+    onContextMenu,
+    onPointerMove,
+    onPointerDown,
+    onPointerUp,
+    onPointerMissed,
+    onWheel,
+    forceUpdate,
+  } = useRaycaster(sceneChildren, context)
+
+  onPointerUp(event => propogateEvent('onPointerUp', event))
+  onPointerDown(event => propogateEvent('onPointerDown', event))
+  onClick(event => propogateEvent('onClick', event))
+  onDblClick(event => propogateEvent('onDoubleClick', event))
+  onContextMenu(event => propogateEvent('onContextMenu', event))
+  onWheel(event => propogateEvent('onWheel', event))
+
+  let prevIntersections: Intersection[] = []
+
+  onPointerMove((event) => {
+    // Current intersections mapped as meshes
+    const hits = event.intersections.map(({ object }) => object)
+
+    // Previously intersected mesh is no longer intersected, fire onPointerLeave
+    prevIntersections.forEach((hit: Intersection) => {
+      if (
+        !hits.includes(hit as unknown as Object3D<Object3DEventMap>)
+      ) {
+        propogateEvent('onPointerLeave', event)
+        propogateEvent('onPointerOut', event)
+      }
+    })
+
+    // Newly intersected mesh is not in the previous intersections, fire onPointerEnter
+    event.intersections.forEach(({ object: hit }) => {
+      if (!prevIntersections.includes(hit as unknown as Intersection)) {
+        propogateEvent('onPointerEnter', event)
+        propogateEvent('onPointerOver', event)
+      }
+    })
+
+    // Fire onPointerMove for all intersected objects
+    propogateEvent('onPointerMove', event)
+
+    // Update previous intersections
+    prevIntersections = hits as unknown as Intersection[]
+  })
+
+  /**
+   * We need to track pointer missed objects separately
+   * since they will not be a part of the raycaster intersection
+   */
+  const pointerMissedObjects: TresObject[] = []
+  onPointerMissed((event: TresEvent) => {
+    // Flag that is set to true when the stopProgatingFn is called
+    const stopPropagatingFn = () => (event.stopPropagating = true)
+    event.stopPropagation = stopPropagatingFn
+
+    pointerMissedObjects.forEach((object: TresObject) => {
+      if (event.stopPropagating) { return }
+
+      // Set eventObject to object that registered the event
+      event.eventObject = object
+
+      executeEventListeners(object.onPointerMissed, event)
+    })
+    // Emit pointer-missed from TresCanvas
+    emit('pointer-missed', { event })
+  })
+
+  function registerPointerMissedObject(object: TresObject) {
+    pointerMissedObjects.push(object)
+  }
+
+  function deregisterPointerMissedObject(object: TresObject) {
+    const index = pointerMissedObjects.indexOf(object)
+    if (index > -1) {
+      pointerMissedObjects.splice(index, 1)
+    }
+  }
+
+  // Attach methods to tres context
+  context.eventManager = {
+    forceUpdate,
+    registerPointerMissedObject,
+    deregisterPointerMissedObject,
+  }
+
+  return { forceUpdate, registerPointerMissedObject, deregisterPointerMissedObject }
+}

+ 71 - 68
src/core/nodeOps.ts

@@ -1,11 +1,11 @@
 import type { RendererOptions } from 'vue'
 import { BufferAttribute } from 'three'
 import { isFunction } from '@alvarosabu/utils'
-import type { Camera, Object3D } from 'three'
+import type { Camera } from 'three'
 import type { TresContext } from '../composables'
 import { useLogger } from '../composables'
-import { deepArrayEqual, isHTMLTag, kebabToCamel } from '../utils'
-import type { TresObject, TresObject3D, TresScene } from '../types'
+import { deepArrayEqual, disposeObject3D, isHTMLTag, kebabToCamel } from '../utils'
+import type { InstanceProps, TresObject, TresObject3D, TresScene } from '../types'
 import { catalogue } from './catalogue'
 
 function noop(fn: string): any {
@@ -17,13 +17,23 @@ const { logError } = useLogger()
 
 const supportedPointerEvents = [
   'onClick',
+  'onContextMenu',
   'onPointerMove',
   'onPointerEnter',
   'onPointerLeave',
+  'onPointerOver',
+  'onPointerOut',
+  'onDoubleClick',
+  'onPointerDown',
+  'onPointerUp',
+  'onPointerCancel',
+  'onPointerMissed',
+  'onLostPointerCapture',
+  'onWheel',
 ]
 
 export function invalidateInstance(instance: TresObject) {
-  const ctx = instance.__tres.root
+  const ctx = instance?.__tres?.root
 
   if (!ctx) { return }
 
@@ -34,7 +44,7 @@ export function invalidateInstance(instance: TresObject) {
 
 export const nodeOps: () => RendererOptions<TresObject, TresObject | null> = () => {
   let scene: TresScene | null = null
-  function createElement(tag, _isSVG, _anchor, props): TresObject | null {
+  function createElement(tag: string, _isSVG: undefined, _anchor: any, props: InstanceProps): TresObject | null {
     if (!props) { props = {} }
 
     if (!props.args) {
@@ -54,10 +64,12 @@ export const nodeOps: () => RendererOptions<TresObject, TresObject | null> = ()
     else {
       const target = catalogue.value[name]
       if (!target) {
-        logError(`${name} is not defined on the THREE namespace. Use extend to add it to the catalog.`)
+        logError(
+          `${name} is not defined on the THREE namespace. Use extend to add it to the catalog.`,
+        )
       }
       // eslint-disable-next-line new-cap
-      instance = new target(...props.args)
+      instance = new target(...props.args) as TresObject
     }
 
     if (!instance) { return null }
@@ -88,34 +100,34 @@ export const nodeOps: () => RendererOptions<TresObject, TresObject | null> = ()
     // determine whether the material was passed via prop to
     // prevent it's disposal when node is removed later in it's lifecycle
 
-    if (instance.isObject3D && (props?.material || props?.geometry)) {
+    if (instance.isObject3D && instance.__tres && (props?.material || props?.geometry)) {
       instance.__tres.disposable = false
     }
 
     return instance as TresObject
   }
-  function insert(child, parent) {
+  function insert(child: TresObject, parent: TresObject) {
     if (!child) { return }
 
     if (parent && parent.isScene) {
       scene = parent as unknown as TresScene
     }
 
-    if (scene) {
+    if (scene && child.__tres) {
       child.__tres.root = scene.__tres.root as TresContext
     }
 
     const parentObject = parent || scene
 
     if (child?.isObject3D) {
-      const { registerCamera, registerObjectAtPointerEventHandler } = child.__tres.root
+      const { registerCamera } = child?.__tres?.root as TresContext
       if (child?.isCamera) {
         registerCamera(child as unknown as Camera)
       }
-      if (
-        child && supportedPointerEvents.some(eventName => child[eventName])
-      ) {
-        registerObjectAtPointerEventHandler(child as Object3D)
+
+      // Track onPointerMissed objects separate from the scene
+      if (child.onPointerMissed && child?.__tres?.root) {
+        child?.__tres?.root?.eventManager?.registerPointerMissedObject(child)
       }
     }
 
@@ -133,62 +145,52 @@ export const nodeOps: () => RendererOptions<TresObject, TresObject | null> = ()
       }
     }
   }
-  function remove(node) {
+
+  function remove(node: TresObject | null) {
     if (!node) { return }
     const ctx = node.__tres
     // remove is only called on the node being removed and not on child nodes.
     node.parent = node.parent || scene
 
-    const {
-      deregisterObjectAtPointerEventHandler,
-      deregisterBlockingObjectAtPointerEventHandler,
-    } = ctx.root
-
     if (node.isObject3D) {
-      const disposeMaterialsAndGeometries = (object3D: TresObject) => {
-        const tresObject3D = object3D as TresObject3D
-        // TODO: to be improved on https://github.com/Tresjs/tres/pull/466/files
-        if (ctx.disposable) {
-          tresObject3D.material?.dispose()
-          tresObject3D.material = undefined
-          tresObject3D.geometry?.dispose()
-          tresObject3D.geometry = undefined
-        }
-      }
+      const deregisterCameraIfRequired = (object: TresObject) => {
+        const deregisterCamera = node?.__tres?.root?.deregisterCamera
 
-      const deregisterAtPointerEventHandlerIfRequired = (object: TresObject) => {
-        deregisterBlockingObjectAtPointerEventHandler(object as Object3D)
-        if (
-          object && supportedPointerEvents.some(eventName => object[eventName])
-        ) { deregisterObjectAtPointerEventHandler?.(object as Object3D) }
-      }
-
-      const deregisterCameraIfRequired = (object: Object3D) => {
-        const deregisterCamera = node.__tres.root.deregisterCamera
-
-        if ((object as Camera).isCamera) { deregisterCamera?.(object as Camera) }
+        if ((object as unknown as Camera).isCamera) { deregisterCamera?.(object as unknown as Camera) }
       }
 
       node.removeFromParent?.()
 
-      node.traverse((child: Object3D) => {
-        disposeMaterialsAndGeometries(child as TresObject)
+      // Remove nested child objects. Primitives should not have objects and children that are
+      // attached to them declaratively ...
+
+      node.traverse((child: TresObject) => {
         deregisterCameraIfRequired(child)
-        deregisterAtPointerEventHandlerIfRequired?.(child as TresObject)
+        // deregisterAtPointerEventHandlerIfRequired?.(child as TresObject)
+        if (child.onPointerMissed) {
+          ctx?.root?.eventManager?.deregisterPointerMissedObject(child)
+        }
       })
 
-      disposeMaterialsAndGeometries(node)
-      deregisterCameraIfRequired(node as Object3D)
-      deregisterAtPointerEventHandlerIfRequired?.(node as TresObject)
+      deregisterCameraIfRequired(node)
+      /*  deregisterAtPointerEventHandlerIfRequired?.(node as TresObject) */
       invalidateInstance(node as TresObject)
+
+      // Dispose the object if it's disposable, primitives needs to be manually disposed by
+      // calling dispose from `@tresjs/core` package like this `dispose(model)`
+      const isPrimitive = node.__tres?.primitive
+
+      if (!isPrimitive && node.__tres?.disposable) {
+        disposeObject3D(node)
+      }
       node.dispose?.()
     }
   }
-  function patchProp(node, prop, prevValue, nextValue) {
+  function patchProp(node: TresObject, prop: string, prevValue: any, nextValue: any) {
     if (node) {
       let root = node
       let key = prop
-      if (node.__tres.primitive && key === 'object' && prevValue !== null) {
+      if (node?.__tres?.primitive && key === 'object' && prevValue !== null) {
         // If the prop 'object' is changed, we need to re-instance the object and swap the old one with the new one
         const newInstance = createElement('primitive', undefined, undefined, {
           object: nextValue,
@@ -203,9 +205,11 @@ export const nodeOps: () => RendererOptions<TresObject, TresObject | null> = ()
           else if (!target.isColor && target.setScalar) { target.setScalar(value) }
           else { target.set(value) }
         }
-        newInstance.__tres.root = scene?.__tres.root
+        if (newInstance?.__tres) {
+          newInstance.__tres.root = scene?.__tres.root
+        }
         // This code is needed to handle the case where the prop 'object' type change from a group to a mesh or vice versa, otherwise the object will not be rendered correctly (models will be invisible)
-        if (newInstance.isGroup) {
+        if (newInstance?.isGroup) {
           node.geometry = undefined
           node.material = undefined
         }
@@ -214,18 +218,10 @@ export const nodeOps: () => RendererOptions<TresObject, TresObject | null> = ()
         }
       }
 
-      if (node.__tres.root) {
-        const {
-          registerBlockingObjectAtPointerEventHandler,
-          deregisterBlockingObjectAtPointerEventHandler,
-        } = node.__tres.root
-
-        if (node.isObject3D && key === 'blocks-pointer-events') {
-          if (nextValue || nextValue === '') { registerBlockingObjectAtPointerEventHandler(node as Object3D) }
-          else { deregisterBlockingObjectAtPointerEventHandler(node as Object3D) }
-
-          return
-        }
+      if (node?.isObject3D && key === 'blocks-pointer-events') {
+        if (nextValue || nextValue === '') { node[key] = nextValue }
+        else { delete node[key] }
+        return
       }
 
       let finalKey = kebabToCamel(key)
@@ -235,10 +231,17 @@ export const nodeOps: () => RendererOptions<TresObject, TresObject | null> = ()
         const prevNode = node as TresObject3D
         const prevArgs = prevValue ?? []
         const args = nextValue ?? []
-        const instanceName = node.__tres.type || node.type
+        const instanceName = node?.__tres?.type || node.type
 
-        if (instanceName && prevArgs.length && !deepArrayEqual(prevArgs, args)) {
-          root = Object.assign(prevNode, new catalogue.value[instanceName](...nextValue))
+        if (
+          instanceName
+          && prevArgs.length
+          && !deepArrayEqual(prevArgs, args)
+        ) {
+          root = Object.assign(
+            prevNode,
+            new catalogue.value[instanceName](...nextValue),
+          )
         }
         return
       }
@@ -281,7 +284,7 @@ export const nodeOps: () => RendererOptions<TresObject, TresObject | null> = ()
     }
   }
 
-  function parentNode(node) {
+  function parentNode(node: TresObject) {
     return node?.parent || null
   }
 

+ 1 - 1
src/devtools/highlight.ts

@@ -3,7 +3,7 @@ import * as THREE from 'three'
 export class HightlightMesh extends THREE.Mesh {
   type = 'HightlightMesh'
   createTime: number
-  constructor(...args: THREE.Mesh['args']) {
+  constructor(...args: any[]) {
     super(...args)
     this.createTime = Date.now()
   }

+ 30 - 35
src/devtools/plugin.ts

@@ -5,7 +5,7 @@ import {
   setupDevtoolsPlugin,
 } from '@vue/devtools-api'
 import { reactive } from 'vue'
-import type { Mesh, Object3D } from 'three'
+import type { Mesh } from 'three'
 import { createHighlightMesh, editSceneObject } from '../utils'
 import { bytesToKB, calculateMemoryUsage } from '../utils/perf'
 import type { TresContext } from '../composables'
@@ -91,14 +91,14 @@ const createNode = (object: TresObject): SceneGraphObject => {
   return node
 }
 
-function buildGraph(object: TresObject, node: SceneGraphObject) {
+function buildGraph(object: TresObject, node: SceneGraphObject, filter: string = '') {
   object.children.forEach((child: TresObject) => {
-    if (child.type === 'HightlightMesh') {
-      return
-    }
+    if (child.type === 'HightlightMesh') { return }
+    if (filter && !child.type.includes(filter) && !child.name.includes(filter)) { return }
+
     const childNode = createNode(child)
     node.children.push(childNode)
-    buildGraph(child, childNode)
+    buildGraph(child, childNode, filter)
   })
 }
 
@@ -122,7 +122,6 @@ export function registerTresDevtools(app: DevtoolsApp, tres: TresContext) {
     (api) => {
       if (typeof api.now !== 'function') {
         toastMessage(
-
           'You seem to be using an outdated version of Vue Devtools. Are you still using the Beta release instead of the stable one? You can find the links at https://devtools.vuejs.org/guide/installation.html.',
         )
       }
@@ -145,43 +144,19 @@ export function registerTresDevtools(app: DevtoolsApp, tres: TresContext) {
       api.on.getInspectorTree((payload) => {
         if (payload.inspectorId === INSPECTOR_ID) {
           // Your logic here
-          const root = createNode(tres.scene.value)
-          buildGraph(tres.scene.value, root)
+          const root = createNode(tres.scene.value as unknown as TresObject)
+          buildGraph(tres.scene.value as unknown as TresObject, root, payload.filter)
           state.sceneGraph = root
           payload.rootNodes = [root]
-          /*  payload.rootNodes = [
-            {
-              id: 'root',
-              label: 'Root ',
-              children: [
-                {
-                  id: 'child',
-                  label: `Child ${payload.filter}`,
-                  tags: [
-                    {
-                      label: 'active',
-                      textColor: 0x000000,
-                      backgroundColor: 0xFF984F,
-                    },
-                    {
-                      label: 'test',
-                      textColor: 0xffffff,
-                      backgroundColor: 0x000000,
-                    },
-                  ],
-                },
-              ],
-            },
-          ] */
         }
       })
       let highlightMesh: Mesh | null = null
-      let prevInstance: Object3D | null = null
+      let prevInstance: TresObject | null = null
 
       api.on.getInspectorState((payload) => {
         if (payload.inspectorId === INSPECTOR_ID) {
           // Your logic here
-          const [instance] = tres.scene.value.getObjectsByProperty('uuid', payload.nodeId)
+          const [instance] = tres.scene.value.getObjectsByProperty('uuid', payload.nodeId) as TresObject[]
           if (!instance) { return }
           if (prevInstance && highlightMesh && highlightMesh.parent) {
             prevInstance.remove(highlightMesh)
@@ -282,6 +257,26 @@ export function registerTresDevtools(app: DevtoolsApp, tres: TresContext) {
               },
             ],
           }
+
+          if (instance.isScene) {
+            payload.state.info = {
+              memory: calculateMemoryUsage(instance),
+              objects: instance.children.length,
+              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,
+            }
+            payload.state.programs = tres.renderer.value.info.programs?.map(program => ({
+              key: program.name,
+              value: {
+                ...program,
+                vertexShader: program.vertexShader,
+                attributes: program.getAttributes(),
+                uniforms: program.getUniforms(),
+              },
+            })) || []
+          }
         }
       })
 

+ 4 - 0
src/devtools/utils.ts

@@ -25,3 +25,7 @@ export function toastMessage(
     console.log(tresMessage)
   }
 }
+
+function __VUE_DEVTOOLS_TOAST__(tresMessage: string, type: string | undefined) {
+  throw new Error(tresMessage + type)
+}

+ 2 - 1
src/directives/index.ts

@@ -2,5 +2,6 @@ import { vLog } from './vLog'
 import { vLightHelper } from './vLightHelper'
 import { vAlwaysLookAt } from './vAlwaysLookAt'
 import { vDistanceTo } from './vDistanceTo'
+import { vRotate } from './vRotate'
 
-export { vLog, vLightHelper, vAlwaysLookAt, vDistanceTo }
+export { vLog, vLightHelper, vAlwaysLookAt, vDistanceTo, vRotate }

+ 40 - 0
src/directives/vRotate.ts

@@ -0,0 +1,40 @@
+import { ref } from 'vue'
+import { Quaternion, Vector3 } from 'three'
+import type { TresObject } from '../types'
+import { useLogger, useRenderLoop } from '../composables'
+
+const { logWarning } = useLogger()
+
+export const vRotate = {
+  mounted: (
+    el: TresObject,
+    binding: {
+      arg: 'x' | 'y' | 'z'
+      value: number
+      modifiers: Partial<{ x: boolean, y: boolean, z: boolean }>
+    },
+  ) => {
+    if (el.isCamera) {
+      logWarning(`Rotate the ${el.type} is not a good idea`)
+      return
+    }
+    const radiansPerFrame = binding.value ?? 0.01
+    const x = ref(binding.modifiers.x || binding.arg === 'x' ? 1 : 0)
+    const y = ref(binding.modifiers.y || binding.arg === 'y' ? 1 : 0)
+    const z = ref(binding.modifiers.z || binding.arg === 'z' ? 1 : 0)
+
+    if (x.value + y.value + z.value === 0) {
+      x.value = 1
+      y.value = 1
+    }
+
+    const quaternion = new Quaternion().setFromAxisAngle(new Vector3(x.value, y.value, z.value)
+      .normalize(), radiansPerFrame)
+
+    const { onLoop } = useRenderLoop()
+
+    onLoop(() => {
+      el.applyQuaternion(quaternion)
+    })
+  },
+}

+ 2 - 0
src/index.ts

@@ -2,6 +2,7 @@ import type { App } from 'vue'
 import TresCanvas from './components/TresCanvas.vue'
 import { normalizeColor, normalizeVectorFlexibleParam } from './utils/normalize'
 import templateCompilerOptions from './utils/template-compiler-options'
+import { disposeObject3D as dispose } from './utils'
 
 export * from './composables'
 export * from './core/catalogue'
@@ -30,4 +31,5 @@ export {
   normalizeColor,
   normalizeVectorFlexibleParam,
   templateCompilerOptions,
+  dispose,
 }

+ 35 - 15
src/types/index.ts

@@ -19,6 +19,9 @@ export type Args<T> = T extends ConstructorRepresentation ? ConstructorParameter
 export interface TresCatalogue {
   [name: string]: ConstructorRepresentation
 }
+
+export type EmitEventName = 'render' | '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 type TresCamera = THREE.OrthographicCamera | THREE.PerspectiveCamera
 
 export interface InstanceProps<T = any, P = any> {
@@ -27,6 +30,7 @@ export interface InstanceProps<T = any, P = any> {
   visible?: boolean
   dispose?: null
   attach?: AttachType<T>
+  [prop: string]: any
 }
 
 interface TresBaseObject {
@@ -39,14 +43,14 @@ interface TresBaseObject {
 export interface LocalState {
   type: string
   // objects and parent are used when children are added with `attach` instead of being added to the Object3D scene graph
-  objects: TresObject3D[]
-  parent: TresObject3D | null
+  objects?: TresObject3D[]
+  parent?: TresObject3D | null
   primitive?: boolean
-  eventCount: number
-  handlers: Partial<EventHandlers>
-  memoizedProps: { [key: string]: any }
-  disposable: boolean
-  root: TresContext
+  eventCount?: number
+  handlers?: Partial<EventHandlers>
+  memoizedProps?: { [key: string]: any }
+  disposable?: boolean
+  root?: TresContext
 }
 
 // Custom type for geometry and material properties in Object3D
@@ -56,7 +60,7 @@ export interface TresObject3D extends THREE.Object3D<THREE.Object3DEventMap> {
 }
 
 export type TresObject =
-  TresBaseObject & (TresObject3D | THREE.BufferGeometry | THREE.Material | THREE.Fog) & { __tres: LocalState }
+  TresBaseObject & (TresObject3D | THREE.BufferGeometry | THREE.Material | THREE.Fog) & { __tres?: LocalState }
 
 export interface TresScene extends THREE.Scene {
   __tres: {
@@ -97,6 +101,15 @@ export interface IntersectionEvent<TSourceEvent> extends Intersection {
 export type ThreeEvent<TEvent> = IntersectionEvent<TEvent> & Properties<TEvent>
 export type DomEvent = PointerEvent | MouseEvent | WheelEvent
 
+export interface TresEvent {
+  eventObject: TresObject
+  event: DomEvent
+  stopPropagation: () => void
+  stopPropagating: boolean
+  intersections: Intersection[]
+  intersects: Intersection[]
+}
+
 export interface Events {
   onClick: EventListener
   onContextMenu: EventListener
@@ -144,13 +157,20 @@ export type MathType<T extends MathRepresentation | THREE.Euler> = T extends THR
 
   : T extends VectorRepresentation | THREE.Layers | THREE.Euler ? T | Parameters<T['set']> | number | VectorCoordinates : T | Parameters<T['set']>
 
-export type TresVector2 = MathType<THREE.Vector2>
-export type TresVector3 = MathType<THREE.Vector3>
-export type TresVector4 = MathType<THREE.Vector4>
-export type TresColor = MathType<THREE.Color>
-export type TresLayers = MathType<THREE.Layers>
-export type TresQuaternion = MathType<THREE.Quaternion>
-export type TresEuler = MathType<THREE.Euler>
+type VectorLike<VectorClass extends THREE.Vector2 | THREE.Vector3 | THREE.Vector4> =
+  | VectorClass
+  | Parameters<VectorClass['set']>
+  | Readonly<Parameters<VectorClass['set']>>
+  | Parameters<VectorClass['setScalar']>[0]
+
+export type TresVector2 = VectorLike<THREE.Vector2>
+export type TresVector3 = VectorLike<THREE.Vector3>
+export type TresVector4 = VectorLike<THREE.Vector4>
+export type TresColor = ConstructorParameters<typeof THREE.Color> | THREE.Color | number | string // Parameters<T> will not work here because of multiple function signatures in three.js types
+export type TresColorArray = typeof THREE.Color | [color: THREE.ColorRepresentation]
+export type TresLayers = THREE.Layers | Parameters<THREE.Layers['set']>[0]
+export type TresQuaternion = THREE.Quaternion | Parameters<THREE.Quaternion['set']>
+export type TresEuler = THREE.Euler
 
 type WithMathProps<P> = { [K in keyof P]: P[K] extends MathRepresentation | THREE.Euler ? MathType<P[K]> : P[K] }
 

+ 53 - 3
src/utils/index.ts

@@ -1,5 +1,6 @@
-import { DoubleSide, MeshBasicMaterial, Vector3 } from 'three'
-import type { Mesh, Object3D, Scene } from 'three'
+import type { Material, Mesh, Object3D, Texture } from 'three'
+import { DoubleSide, MeshBasicMaterial, Scene, Vector3 } from 'three'
+import type { TresObject } from 'src/types'
 import { HightlightMesh } from '../devtools/highlight'
 
 export function toSetMethodName(key: string) {
@@ -40,6 +41,12 @@ export function kebabToCamel(str: string) {
   return str.replace(/-([a-z])/g, (_, c) => c.toUpperCase())
 }
 
+// CamelCase to kebab-case
+const hyphenateRE = /\B([A-Z])/g
+export function hyphenate(str: string) {
+  return str.replace(hyphenateRE, '-$1').toLowerCase()
+}
+
 export function makeMap(str: string, expectsLowerCase?: boolean): (key: string) => boolean {
   const map: Record<string, boolean> = Object.create(null)
   const list: Array<string> = str.split(',')
@@ -238,7 +245,7 @@ export function stopHighlightAnimation(): void {
   }
 }
 
-export function createHighlightMesh(object: Object3D): Mesh {
+export function createHighlightMesh(object: TresObject): Mesh {
   const highlightMaterial = new MeshBasicMaterial({
     color: 0xA7E6D7, // Highlight color, e.g., yellow
     transparent: true,
@@ -261,3 +268,46 @@ export function extractBindingPosition(binding: any): Vector3 {
   if (Array.isArray(binding.value)) { observer = new Vector3(...observer) }
   return observer
 }
+
+function hasMap(material: Material): material is Material & { map: Texture | null } {
+  return 'map' in material
+}
+
+export function disposeMaterial(material: Material): void {
+  if (hasMap(material) && material.map) {
+    material.map.dispose()
+  }
+
+  material.dispose()
+}
+
+export function disposeObject3D(object: TresObject): void {
+  if (object.parent) {
+    object.removeFromParent?.()
+  }
+  delete object.__tres
+  // Clone the children array to safely iterate
+  const children = [...object.children]
+  children.forEach(child => disposeObject3D(child))
+
+  if (object instanceof Scene) {
+    // Optionally handle Scene-specific cleanup
+  }
+  else {
+    const mesh = object as unknown as Partial<Mesh>
+    if (mesh.geometry) {
+      mesh.geometry.dispose()
+      delete mesh.geometry
+    }
+
+    if (Array.isArray(mesh.material)) {
+      mesh.material.forEach(material => disposeMaterial(material))
+      delete mesh.material
+    }
+    else if (mesh.material) {
+      disposeMaterial(mesh.material)
+      delete mesh.material
+    }
+    object.dispose?.()
+  }
+}

+ 2 - 1
src/utils/test-utils.ts

@@ -1,6 +1,7 @@
+import type { Fn } from '@vueuse/core'
 import { createApp } from 'vue'
 
-export function withSetup(composable) {
+export function withSetup(composable: Fn) {
   let result
   const app = createApp({
     setup() {

Энэ ялгаанд хэт олон файл өөрчлөгдсөн тул зарим файлыг харуулаагүй болно