Jelajahi Sumber

Merge branch 'v4' into feature/503-conditional-rendering-of-primitives

alvarosabu 1 tahun lalu
induk
melakukan
3490c4a3c3
51 mengubah file dengan 1865 tambahan dan 547 penghapusan
  1. 7 8
      CHANGELOG.md
  2. 29 8
      docs/.vitepress/config.ts
  3. 18 0
      docs/.vitepress/theme/components/BlenderCube.vue
  4. 101 0
      docs/.vitepress/theme/components/GraphPane.vue
  5. 40 0
      docs/.vitepress/theme/components/OnDemandRendering.vue
  6. 24 0
      docs/.vitepress/theme/components/RenderingLogger.vue
  7. 11 0
      docs/.vitepress/theme/composables/state.ts
  8. 131 0
      docs/advanced/performance.md
  9. 3 0
      docs/api/composables.md
  10. 3 2
      docs/api/tres-canvas.md
  11. 4 0
      docs/components.d.ts
  12. 28 0
      docs/debug/devtools.md
  13. 61 0
      docs/directives/v-always-look-at.md
  14. 36 0
      docs/directives/v-distance-to.md
  15. 34 0
      docs/directives/v-light-helper.md
  16. 53 0
      docs/directives/v-log.md
  17. 174 0
      docs/examples/lights-shadows.md
  18. 7 9
      docs/examples/load-models.md
  19. 174 0
      docs/examples/shaders.md
  20. 1 1
      docs/guide/index.md
  21. 1 1
      docs/package.json
  22. TEMPAT SAMPAH
      docs/public/debug-3D.png
  23. TEMPAT SAMPAH
      docs/public/devtools-scene-inspector.png
  24. TEMPAT SAMPAH
      docs/public/meme-debugging.jpg
  25. TEMPAT SAMPAH
      docs/public/vue-chrome-devtools.png
  26. 1 1
      package.json
  27. 1 6
      playground/src/components/TheExperience.vue
  28. 55 0
      playground/src/pages/lights.vue
  29. 21 0
      playground/src/pages/rendering-modes/index.vue
  30. 32 0
      playground/src/pages/rendering-modes/scene.vue
  31. 11 0
      playground/src/router.ts
  32. 49 0
      playground/vite.config.ts.timestamp-1706539768400-58c91108b32e6.mjs
  33. 228 306
      pnpm-lock.yaml
  34. 7 10
      src/components/TresCanvas.vue
  35. 0 3
      src/composables/useCamera/index.ts
  36. 10 14
      src/composables/usePointerEventHandler/index.ts
  37. 5 5
      src/composables/useRaycaster/index.ts
  38. 5 1
      src/composables/useRenderer/const.ts
  39. 70 22
      src/composables/useRenderer/index.ts
  40. 30 0
      src/composables/useSizes/index.ts
  41. 121 60
      src/composables/useTresContextProvider/index.ts
  42. 83 70
      src/core/nodeOps.ts
  43. 27 2
      src/core/nodeOpts.test.ts
  44. 6 0
      src/directives/index.ts
  45. 21 0
      src/directives/vAlwaysLookAt.ts
  46. 37 0
      src/directives/vDistanceTo.ts
  47. 62 0
      src/directives/vLightHelper.ts
  48. 13 0
      src/directives/vLog.ts
  49. 1 0
      src/index.ts
  50. 18 16
      src/types/index.ts
  51. 11 2
      src/utils/index.ts

+ 7 - 8
CHANGELOG.md

@@ -1,12 +1,14 @@
+## [3.7.0](https://github.com/Tresjs/tres/compare/3.6.1...3.7.0) (2024-01-29)
 
-
-## [4.0.0-next.0](https://github.com/Tresjs/tres/compare/3.6.0...4.0.0-next.0) (2023-12-22)
+### Features
+  
+* 474 vue chrome devtools plugin ([#526](https://github.com/Tresjs/tres/issues/526)) ([0185bfa](https://github.com/Tresjs/tres/commit/0185bfa6f04faff5eabbc526616713ef7747ebeb))
+* 524 feat add directives to core ([#525](https://github.com/Tresjs/tres/issues/525)) ([5268e9f](https://github.com/Tresjs/tres/commit/5268e9f13bf65c61d5ddfe7153b71b335449b81d))
 
 
-### Features
+### Bug Fixes
 
-* 474 vue chrome devtools plugin ([#479](https://github.com/Tresjs/tres/issues/479)) ([224ab06](https://github.com/Tresjs/tres/commit/224ab06a4404e2ae5a0cbd2f43041961862b09fd))
-  
+* **docs:** change image path to silence warning ([#519](https://github.com/Tresjs/tres/issues/519)) ([280d248](https://github.com/Tresjs/tres/commit/280d2482760bde1032e24c4a9e96af4beea954ed))
 
 ## [3.6.1](https://github.com/Tresjs/tres/compare/3.6.0...3.6.1) (2024-01-16)
 
@@ -15,10 +17,7 @@
 
 * correct minor typos ([#438](https://github.com/Tresjs/tres/issues/438)) ([341faac](https://github.com/Tresjs/tres/commit/341faacb93fd347aced7f1bc7e0484ecbc12e6ce)), closes [#452](https://github.com/Tresjs/tres/issues/452)
 * incorrect MathRepresentation type ([#456](https://github.com/Tresjs/tres/issues/456)) ([314b088](https://github.com/Tresjs/tres/commit/314b0883b78ded0cad2bdf9f2506bbeac4a0817e))
-<<<<<<< HEAD
-=======
 * **usetrescontextprovider:** fixed rendering issues caused when resize is triggered ([#512](https://github.com/Tresjs/tres/issues/512)) ([a16b12b](https://github.com/Tresjs/tres/commit/a16b12b160098e97993b14a7bb054103c88b6263)), closes [#511](https://github.com/Tresjs/tres/issues/511)
->>>>>>> main
 
 ## [3.6.0](https://github.com/Tresjs/tres/compare/3.5.2...3.6.0) (2023-12-12)
 

+ 29 - 8
docs/.vitepress/config.ts

@@ -64,8 +64,29 @@ export default defineConfig({
           },
         ],
       },
+
+      {
+        text: 'Advanced',
+
+        items: [
+          { text: 'Extending', link: '/advanced/extending' },
+          { text: 'primitive', link: '/advanced/primitive' },
+          { text: 'Performance', link: '/advanced/performance' },
+          {
+            text: 'Caveats',
+            link: '/advanced/caveats',
+          },
+        ],
+      },
+      {
+        text: 'Debug',
+        items: [
+          { text: 'Devtools', link: '/debug/devtools' },
+        ],
+      },
       {
         text: 'Examples',
+        collapsed: true,
         items: [
           { text: 'Orbit Controls', link: '/examples/orbit-controls' },
           { text: 'Basic Animations', link: '/examples/basic-animations' },
@@ -73,18 +94,18 @@ export default defineConfig({
           { text: 'Load Textures', link: '/examples/load-textures' },
           { text: 'Load Models', link: '/examples/load-models' },
           { text: 'Load Text', link: '/examples/text-3d' },
+          { text: 'Lights & Shadows', link: '/examples/lights-shadows' },
+          { text: 'Shaders', link: '/examples/shaders' },
         ],
       },
       {
-        text: 'Advanced',
-
+        text: 'Directives',
+        collapsed: true,
         items: [
-          { text: 'Extending', link: '/advanced/extending' },
-          { text: 'primitive', link: '/advanced/primitive' },
-          {
-            text: 'Caveats',
-            link: '/advanced/caveats',
-          },
+          { text: 'v-log', link: '/directives/v-log' },
+          { 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' },
         ],
       },
       {

+ 18 - 0
docs/.vitepress/theme/components/BlenderCube.vue

@@ -0,0 +1,18 @@
+<script setup lang="ts">
+import { useTresContext } 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
+
+model.position.set(0, 1, 0)
+
+const state = useTresContext()
+
+state.invalidate()
+</script>
+
+<template>
+  <primitive :object="model" />
+</template>

+ 101 - 0
docs/.vitepress/theme/components/GraphPane.vue

@@ -0,0 +1,101 @@
+<script lang="ts" setup>
+import { ref } from 'vue'
+import { useRafFn } from '@vueuse/core'
+import { useState } from '../composables/state'
+
+const width = 160
+const height = 40
+const strokeWidth = 2
+const updateInterval = 100 // Update interval in milliseconds
+const topOffset = 0 // Offset from the top
+
+const points = ref('')
+const frameTimes = ref([])
+const maxFrames = ref(width / strokeWidth)
+
+let lastUpdateTime = performance.now()
+
+const { renderingTimes } = useState()
+
+useRafFn(({ timestamp }) => {
+  if (timestamp - lastUpdateTime >= updateInterval) {
+    lastUpdateTime = timestamp
+
+    frameTimes.value.push(renderingTimes?.value)
+    renderingTimes.value = 0
+
+    if (frameTimes.value.length > maxFrames.value) {
+      frameTimes.value.shift()
+    }
+
+    points.value = frameTimes.value
+      .map(
+        (value, index) =>
+          `${index * strokeWidth},${
+            height + topOffset - strokeWidth / 2 - (value * (height + topOffset - strokeWidth)) / 2
+          }`,
+      )
+      .join(' ')
+  }
+})
+</script>
+
+<template>
+  <div
+    class="absolute
+      right-2
+      top-2
+      flex
+      px-4
+      py-1
+      justify-between
+      gap-4
+      items-center
+      mb-2
+      z-10
+      bg-white
+      dark:bg-dark
+      shadow-xl
+      rounded 
+      border-4 
+      border-solid 
+      bg-primary 
+      border-primary 
+      pointer-events-none
+      overflow-hidden"
+  >
+    <label class="text-secondary text-xs w-1/3">Rendering Activity</label>
+
+    <div
+      class="
+        bg-gray-100
+        dark:bg-gray-600
+        relative
+        w-2/3
+        p-1
+        rounded
+        text-right
+        text-xs
+        focus:border-gray-200
+        outline-none
+        border-none
+        font-sans
+      "
+    >
+      <svg
+        :width="width"
+        :height="height"
+        xmlns="http://www.w3.org/2000/svg"
+        fill="none"
+      >
+        <polyline
+          :points="points"
+          stroke="lightgray"
+          :stroke-width="strokeWidth"
+          stroke-linecap="round"
+          stroke-linejoin="round"
+        />
+      </svg>
+    </div>
+  </div>
+</template>

+ 40 - 0
docs/.vitepress/theme/components/OnDemandRendering.vue

@@ -0,0 +1,40 @@
+<script setup lang="ts">
+import { TresCanvas } from '@tresjs/core'
+import { BasicShadowMap, SRGBColorSpace, NoToneMapping } from 'three'
+import { OrbitControls } from '@tresjs/cientos'
+import { useState } from '../composables/state'
+import BlenderCube from './BlenderCube.vue'
+import GraphPane from './GraphPane.vue'
+import RenderingLogger from './RenderingLogger.vue'
+
+const { renderingTimes } = useState()
+
+function onRender() {
+  renderingTimes.value = 1
+
+}
+</script>
+
+<template>
+  <GraphPane />
+  <TresCanvas
+    render-mode="on-demand"
+    clear-color="#82DBC5"
+    @render="onRender"
+  >
+    <TresPerspectiveCamera
+      :position="[5, 5, 5]"
+      :look-at="[0, 0, 0]"
+    />
+    <Suspense>
+      <BlenderCube />
+    </Suspense>
+    <TresGridHelper />
+    <RenderingLogger />
+    <TresAmbientLight :intensity="1" />
+    <TresDirectionalLight
+      :position="[0, 8, 4]"
+      :intensity="0.7"
+    />
+  </TresCanvas>
+</template>

+ 24 - 0
docs/.vitepress/theme/components/RenderingLogger.vue

@@ -0,0 +1,24 @@
+<script setup lang="ts">
+import { useRenderLoop, useTresContext } from '@tresjs/core'
+import { OrbitControls } from '@tresjs/cientos'
+import { onMounted } from 'vue'
+import { useState } from '../composables/state'
+
+const { renderingTimes } = useState()
+
+const state = useTresContext()
+
+function manualInvalidate() {
+  state.invalidate()
+}
+
+onMounted(() => {
+  manualInvalidate()
+})
+</script>
+
+<template>
+  <OrbitControls
+    @change="manualInvalidate"
+  />
+</template>

+ 11 - 0
docs/.vitepress/theme/composables/state.ts

@@ -0,0 +1,11 @@
+import { reactive, toRefs } from 'vue'
+
+const state = reactive({
+  renderingTimes: 0,
+})
+export function useState() {
+  return {
+    ...toRefs(state),
+    
+  }
+}

+ 131 - 0
docs/advanced/performance.md

@@ -0,0 +1,131 @@
+# Scaling performance 🚀
+
+> Quick guide with tips to improve performance of your Tres.js application.
+
+We are running WebGL on the browser, which can be quite expensive and it will depend on how powerful the user's device is. To make 3D accessible to everyone, we need to make sure our applications are optimized to run also on low-end devices. This guide will provide some tips to improve the performance of your Tres.js application.
+
+## On-demand rendering <Badge type="tip" text="^4.0.0" />
+
+By default, Tres.js will render your scene on every frame. This is great for most applications, but if you are building a game or a complex application, you might want to control when the scene is rendered. 
+
+Otherwise it might drain your device battery 🔋 🔜 🪫 and make your computer sound like an airplane 🛫.
+
+Ideally, you only want to **render the scene when necessary**, for example when the user interacts with the scene and the camera moves, or when objects in the scene are animated.
+
+You can do that by setting the `renderMode` prop to `on-demand` or `manual`:
+
+
+### Mode `on-demand`
+
+<ClientOnly>
+  <div style="position: relative; aspect-ratio: 16/9; height: auto; margin: 2rem 0; border-radius: 8px; overflow:hidden;">
+    <onDemandRendering />
+  </div>
+</ClientOnly>
+
+
+```vue
+<TresCanvas render-mode="on-demand">
+  <!-- Your scene goes here -->
+</TresCanvas>
+```
+
+#### Automatic Invalidation
+
+When using `render-mode="on-demand"`, Tres.js will automatically invalidate the current frame by observing component props and lifecycle hooks like `onMounted` and `onUnmounted`. It will also invalidate the frame when resizing the window or changing any prop from the `<TresCanvas>` component like `clearColor` or `antialias`.
+
+The code below updates TresMesh's position-x prop every second, triggering a new render.
+
+```vue
+<script setup>
+import { ref } from 'vue'
+
+const positionX = ref(0)
+
+setTimeout(() => {
+  positionX.value = 1
+}, 1000)
+</script>
+
+<template>
+  <TresCanvas render-mode="on-demand">
+    <TresMesh :position-x="positionX">
+      <TresBoxGeometry />
+      <TresMeshBasicMaterial color="teal" />
+    </TresMesh>
+  </TresCanvas>
+</template>
+```
+
+#### Manual Invalidation
+
+Since it is not really possible to observe all the possible changes in your application, you can also manually invalidate the frame by calling the `invalidate()` method from the [`useTresContext` composable](../api/composables.md#usetrescontext):
+
+
+::: code-group
+
+```vue [App.vue]
+<script setup>
+import { TresCanvas } from '@tresjs/core'
+import Scene from './Scene.vue'
+</script>
+
+<template>
+  <TresCanvas
+    render-mode="manual"
+  >
+    <Scene />
+  </TresCanvas>
+</template>
+```
+
+```vue [Scene.vue]
+<script setup>
+import { useTres } from '@tresjs/core'
+
+const boxRef = ref()
+const { invalidate } = useTres()
+
+watch(boxRef.value, () => {
+  boxRef.value.position.x = 1
+  invalidate()
+})
+</script>
+
+<template>
+  <TresMesh ref="boxRef">
+    <TresBoxGeometry />
+    <TresMeshBasicMaterial color="teal" />
+  </TresMesh>
+</template>
+```
+
+:::
+
+### Mode `always` 
+
+In this rendering mode, Tres will continously render the scene on every frame. This is the default mode and the easiest to use, but it's also the most resource expensive one.
+
+
+### Mode `manual`
+
+If you want to have full control of when the scene is rendered, you can set the `render-mode` prop to `manual`:
+
+```vue
+<TresCanvas render-mode="manual">
+  <!-- Your scene goes here -->
+</TresCanvas>
+```
+
+In this mode, Tres will not render the scene automatically. You will need to call the `advance()` method from the [`useTresContext` composable](../api/composables.md#usetrescontext) to render the scene:
+
+```vue
+<script setup>
+import { useTres } from '@tresjs/core'
+
+const { advance } = useTres()
+
+advance()
+</script>
+```
+

+ 3 - 0
docs/api/composables.md

@@ -233,4 +233,7 @@ const context = useTresContext()
 | **scene** | the [scene](https://threejs.org/docs/?q=sce#api/en/scenes/Scene). |
 | **setCameraActive** | a method to set a camera active |
 | **sizes** | contains width, height and aspect ratio of your canvas |
+| **invalidate** | a method to invalidate the render loop. This is only required if you set the `render-mode` prop to `on-demand`. |
+| **advance** | a method to advance the render loop. This is only required if you set the `render-mode` prop to `manual`. |
+
 

+ 3 - 2
docs/api/tres-canvas.md

@@ -77,12 +77,13 @@ renderer.shadowMap.type = PCFSoftShadowMap
 | **clearColor** | The color the renderer will use to clear the canvas. | `#000000` |
 | **context** | This can be used to attach the renderer to an existing [RenderingContext](https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext) | |
 | **depth** | Whether the drawing buffer has a [depth buffer](https://en.wikipedia.org/wiki/Z-buffering) of at least 16 bits. | `true` |
+| **renderMode** | Render mode, can be `always`, `on-demand` or `manual`. See [Performance](../advanced/performance)  | `always` |
 | **disableRender** | Disable render on requestAnimationFrame, useful for PostProcessing | `false` |
 | **failIfMajorPerformanceCaveat** | Whether the renderer creation will fail upon low performance is detected. See [WebGL spec](https://registry.khronos.org/webgl/specs/latest/1.0/#5.2) for details. | `false` |
 | **logarithmicDepthBuffer** | Whether to use a logarithmic depth buffer. It may be necessary to use this if dealing with huge differences in scale in a single scene. Note that this setting uses gl_FragDepth if available which disables the [Early Fragment Test](https://www.khronos.org/opengl/wiki/Early_Fragment_Test) optimization and can cause a decrease in performance. | `false` |
 | **outputColorSpace** | Defines the output encoding | `LinearEncoding` |
-| **powerPreference** | Provides a hint to the user agent indicating what configuration of GPU is suitable for this WebGL context. Can be "high-performance", "low-power" or "default". | `default` |
-| **precision** | Shader precision. Can be "highp", "mediump" or "lowp". | "highp" if supported by the device |
+| **powerPreference** | Provides a hint to the user agent indicating what configuration of GPU is suitable for this WebGL context. Can be `high-performance`, `low-power` or `default`. | `default` |
+| **precision** | Shader precision. Can be `highp`, `mediump` or `lowp`. | "highp" if supported by the device |
 | **premultipliedAlpha** | Whether the renderer will assume that colors have [premultiplied alpha](https://en.wikipedia.org/wiki/Glossary_of_computer_graphics#premultiplied_alpha). | `true` |
 | **preserveDrawingBuffer** | Whether to preserve the buffers until manually cleared or overwritten.. | `false` |
 | **shadows** | Enable shadows in the renderer | `false` |

+ 4 - 0
docs/components.d.ts

@@ -7,14 +7,18 @@ export {}
 
 declare module 'vue' {
   export interface GlobalComponents {
+    BlenderCube: typeof import('./.vitepress/theme/components/BlenderCube.vue')['default']
     DonutExample: typeof import('./.vitepress/theme/components/DonutExample.vue')['default']
     EmbedExperiment: typeof import('./.vitepress/theme/components/EmbedExperiment.vue')['default']
     ExtendExample: typeof import('./.vitepress/theme/components/ExtendExample.vue')['default']
     FirstScene: typeof import('./.vitepress/theme/components/FirstScene.vue')['default']
     FirstSceneLightToon: typeof import('./.vitepress/theme/components/FirstSceneLightToon.vue')['default']
+    GraphPane: typeof import('./.vitepress/theme/components/GraphPane.vue')['default']
     HomeSponsors: typeof import('./.vitepress/theme/components/HomeSponsors.vue')['default']
     LocalOrbitControls: typeof import('./.vitepress/theme/components/LocalOrbitControls.vue')['default']
     LoveVueThreeJS: typeof import('./.vitepress/theme/components/LoveVueThreeJS.vue')['default']
+    OnDemandRendering: typeof import('./.vitepress/theme/components/OnDemandRendering.vue')['default']
+    RenderingLogger: typeof import('./.vitepress/theme/components/RenderingLogger.vue')['default']
     RouterLink: typeof import('vue-router')['RouterLink']
     RouterView: typeof import('vue-router')['RouterView']
     SandboxDemo: typeof import('./.vitepress/theme/components/SandboxDemo.vue')['default']

+ 28 - 0
docs/debug/devtools.md

@@ -0,0 +1,28 @@
+# Devtools
+
+
+
+One of the most difficult things a developer faces when creating 3D experiences on the browser is debugging. The browser `canvas` is a black box, and it's hard to know what's going on inside. The imperative nature of [ThreeJS](https://threejs.org/) makes it incredibly difficult to debug, having to depend on `console.log` to see what's going on, or third party to fine-tune and inspect the scene.
+
+Don't make me get started with checking the performance of your scene. 😱
+
+![developer debugging 3D](/debug-3D.png)
+
+One of our goals with TresJS is to offer **the best DX (Developer Experience)** when dealing with 3D scenes on the browser. Thanks to the declarative nature of the ecosystem plus the variety of solutions the Vue ecosystem offers such as the Vue Devtools, Nuxt and Vite, we can offer a better tooling for devs to debug their scenes.
+
+## Introducing the Devtools
+
+From <Badge text="^3.7.0" /> we are introducing the TresJS Devtools, a customized inspector tab for the [Official Vue Chrome Devtools](https://devtools.vuejs.org/guide/installation.html) that allows you to inspect your TresJS scenes and components.
+
+![TresJS Devtools](/vue-chrome-devtools.png)
+
+### Features
+
+- **Scene Inspector**: Inspect the current scene and its components using a tree view similar to the Vue Devtools component inspector.
+- **Memory Allocation**: See how much memory is being by the components.
+- **Object Inspector**: Inspect the properties of the selected object in the scene, including its children.
+- **Editable Properties**: And yes, you can edit the properties of the selected object and see the changes in real-time.
+
+![](/devtools-scene-inspector.png)
+
+Enjoy the new Devtools and let us know what you think! 🎉

+ 61 - 0
docs/directives/v-always-look-at.md

@@ -0,0 +1,61 @@
+# v-always-look-at 👀
+
+With the new directive v-always-look-at provided by **TresJS**, you can add easily command an [Object3D](https://threejs.org/docs/index.html?q=object#api/en/core/Object3D) to always look at a specific position, this could be passed as a Vector3 or an Array.
+
+## Usage
+
+```vue{3,9}
+<script setup lang="ts">
+import { TresCanvas } from '@tresjs/core'
+import { Box, vAlwaysLookAt } from '@tresjs/cientos'
+</script>
+<template>
+    <TresCanvas >
+      <TresPerspectiveCamera :position="[0, 2, 5]" />
+      <Box
+        v-always-look-at="new Vector3(0, 0, 0)"
+      />
+  </TresCanvas>
+</template>
+```
+No matter where the Box move will always look-at the position [0,0,0]
+
+### Why not use the in built method look-at?
+
+You could ask, this is fine but I can use the `:look-at` method directly in the component, why should I need this?
+
+The answers is that with the method `:look-at` you will indicated to look at that position just once, when the instance is mounted, then if the object changes this will not get updated
+
+### You can look at other instance too!
+
+Another advantage is that you can look at an instance in movement, for example with the camera, like so:
+
+```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'
+
+const sphereRef = shallowRef()
+
+const { onLoop } = useRenderLoop()
+
+// here we update the position of the sphere and the camera will always follow the object
+onLoop(({ elapsed }) => {
+  if (sphereRef.value) {
+    sphereRef.value.value.position.y = Math.sin(elapsed) * 1.5
+  }
+})
+</script>
+<template>
+    <TresCanvas >
+      <TresPerspectiveCamera :position="[0, 2, 5]"
+        v-always-look-at="sphereRef"
+      />
+      <Sphere
+        ref="sphereRef"
+        :scale="0.5"
+      />
+  </TresCanvas>
+</template>
+```

+ 36 - 0
docs/directives/v-distance-to.md

@@ -0,0 +1,36 @@
+# v-distance-to
+
+Have you tried to calculate the distance between two Object3Ds?
+
+With the new directive `v-distance-to` it's easier than ever, you should only indicate the target object to perform the measure and the result will appear in your console.
+
+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'
+</script>
+<template>
+  <TresCanvas v-bind="gl">
+    <TresPerspectiveCamera :position="[0, 2, 5]" />
+    <Sphere
+      ref="sphere1Ref"
+      :position="[-2, slider, 0]"
+      :scale="0.5"
+    />
+    <Sphere
+      v-distance-to="sphere1Ref"
+      :position="[2, 0, 0]"
+      :scale="0.5"
+    />
+    <TresGridHelper :args="[10, 10]" />
+    <OrbitControls />
+  </TresCanvas>
+</template>
+```
+
+The use of `v-distance-to` is reactive, so it works perfectly with @tres/leches 🍰.
+
+::: warning
+`v-distance-to` will not measure an object in movement within the renderLoop.
+:::

+ 34 - 0
docs/directives/v-light-helper.md

@@ -0,0 +1,34 @@
+# v-light-helper 🔆
+
+With the new directive v-light-helper provided by **TresJS**, you can add fast the respective helper to your lights with just one line of code 😍.
+
+The following lights are supported:
+- DirectionalLight
+- PointLight
+- SpotLight
+- HemisphereLight
+
+## Usage
+
+```vue{2,8,11,14,17}
+<script setup lang="ts">
+import { OrbitControls, Sphere, vLightHelper } from '@tresjs/cientos'
+</script>
+<template>
+  <TresCanvas >
+    <TresPerspectiveCamera :position="[0, 2, 5]" />
+    <TresDirectionalLight
+      v-light-helper
+    />
+    <TresPointLight
+      v-light-helper
+    />
+    <TresSpotLight
+      v-light-helper
+    />
+    <TresHemisphereLight
+      v-light-helper
+    />
+  </TresCanvas>
+</template>
+```

+ 53 - 0
docs/directives/v-log.md

@@ -0,0 +1,53 @@
+# v-log
+
+### Problem
+
+When you have to log your instance you have to use the template reference and then log them:
+
+```vue
+<script setup lang="ts">
+import { shallowRef, watch } from 'vue'
+
+const sphereRef = shallowRef()
+
+watch(sphereRef, (value) => {
+  console.log(value) // Really for a log?!!! 😫
+})
+</script>
+
+<template>
+  <TresCanvas>
+    <TresPerspectiveCamera :position="[0, 2, 5]" />
+    <Sphere
+      ref="sphereRef"
+      :scale="0.5"
+    />
+    <OrbitControls />
+  </TresCanvas>
+</template>
+```
+
+And is A LOT of code just for a simple log right?
+
+## Usage
+
+With the new directive v-log provided by **TresJS**, you can do this by just adding `v-log` to the instance.
+
+```vue{2,10,12}
+<script setup lang="ts">
+import { OrbitControls, Sphere, vLog } from '@tresjs/cientos'
+</script>
+<template>
+    <TresCanvas >
+    <TresPerspectiveCamera :position="[0, 2, 5]" />
+    <Sphere
+      ref="sphereRef"
+      :scale="0.5"
+      v-log:material  <!-- will print just the material 🎉 -->
+    />
+    <OrbitControls v-log />
+  </TresCanvas>
+</template>
+```
+
+Note that you can pass a modifier with the name of a property, for example `v-log:material`, and will log directly the `material` property 😍

+ 174 - 0
docs/examples/lights-shadows.md

@@ -0,0 +1,174 @@
+# Light-shadows
+
+This guide will help you get started with simple light and shadows in TresJS.
+
+We will build a simple scene with three meshes and a plane but only two will have shadows.
+<SandboxDemo url="https://play.tresjs.org/#eNqVVt1y2jwQfRUN30WSKdimhLbjL3Qo9GfaadpM4K7uhbAXUGpLGkn8pJm8e1eSDXZCMmRCGGv37NHZ1XrFXWuqQH+QMlivoBW3LnSqmDREg1lJklO+GCQto5PW+4SzQgplyB3RS5rnYnMNc3JP5koU5ASjT/6vQSzrmPI11W2y0nANPAP1XQhZBQwNIm50mArVjPypZsyMBTdK5HrHv4Mz4EboRsSIapZOljQTm0sq22Ry/WU0FrlQE0lTaJMfYio4oEsyvtgxmqUCOEl4wlPBtSGLnAzIXcIJSXOgyhHE5OS/d68/jsb9k7b1YOK4iY6JUStwFprLJY3JnObaGzwEN5veSogfarMIsTJyhRlWAuOHgi3I7BXHzQTQfb9XPRNbewyD2pmcnu3dd0RwW3XMetA8B4/y3tPTMzJ475Nn81PPGaxpvoIzZ6xbAiUMNUzw4Ja8GpAoiLoWgpruHWXCL0LfRNgyuDBQyJwawBUhF/u+IOvOjPEM22uRJy2ywWex6Wj21yMR2+yEsDJbiitQWkJq2BrGtABFSSyFZlYWEv7qt8nbwH/9Ru54LtZoPu/bZ+oCcdm1K45Hjc9R4FZzt+hGUYSrxoaXoJfNPTqv2wQ/kdugqol1RG1ySc0yuPrqvSVNlTye5BcQBRh1i2LUQtuYbpt0reCeZas2rm09FYIjKShGc5LaVsGosjXrUsMq4JF2BXMM8QeJESnVpuN7tZkWqrefR7pHYntAttVcfb1I+vln+3ec9LrWplisvz2Gx2oncglqX+ejZX0ejaLe6NiKpoD991QVO71DzdEpW4OErnkOab/CqXuoRRC8/3+i2BNDeUZV9jiz+Vv791Rmtdw+FDM7Y7+zxdKQmHEDHPO6LV+YxkvxkWENbGY09/Dnumr3rhym9HL8aEDDRVibG612yw/7TkFlcKMFx5vKDaakdOAFFfv5ZW31u8U6ktbSGKnjMEwzjvEZ5GytAg4m5LII6/BhL+gHUZgxbUJrRnTSchO5QexvoZdw+wikf1OnL83NXcwG6B+JTXAE/w47PA9wiJXMlTEomI2pc9tb7xheixsiY/8d6n0FuqiXAW97vEyOrm8NPuxGrsA47WEbFM3qljhsIAXZC4h9wHPUCOxkULAjSCuoTf48eBPmbFanrO467Emj8ZKds8WDjkxFIVkO6qe03d/sTHdHf3O23U8IF7OE9M8B+43eeslX2Cyg1lju/VHiZADj3Z8mP2CLzztnIbJVXh7OE85r0CJfWY0eNlrxDGXXcE7tV/eC4Q+Pqf60dW9umVRDqMFfO876q5pJu17zht+ucA7vjmP8TJX2mfWC3q7g9/8AWlN6bg==" />
+
+## Setting up the scene (optional)
+
+We import all the modules that we need, for comfort we can use the orbit-controls from cientos,
+[check here to know how](/examples/orbit-controls).
+
+Let's put four objects in our scene, one will be the plane that receive shadows, two of them will cast shadows and the last one will not cast any shadow at all.
+
+I'm going to use [MeshToonMaterial](https://threejs.org/docs/index.html?q=toon#api/en/materials/MeshToonMaterial). Simply because we can see the "soft shadow" easily.
+
+```vue
+<script setup lang="ts">
+import { TresCanvas } from '@tresjs/core'
+import { OrbitControls } from '@tresjs/cientos'
+</script>
+
+<template>
+  <TresCanvas
+    clear-color="#111"
+    window-size
+  >
+    <OrbitControls />
+    <TresPerspectiveCamera :position="[5, 7.5, 7.5]" />
+
+    <TresMesh
+      :position="[-2, 2, 0]"
+      :rotation="[0, Math.PI, 0]"
+    >
+      <TresConeGeometry :args="[1, 1.5, 3]" />
+      <TresMeshToonMaterial color="#82DBC5" />
+    </TresMesh>
+    <TresMesh
+      :position="[0, 0, 0]"
+    >
+      <TresBoxGeometry :args="[1.5, 1.5, 1.5]" />
+      <TresMeshToonMaterial color="#4F4F4F" />
+    </TresMesh>
+    <TresMesh
+      :position="[2, -2, 0]"
+    >
+      <TresSphereGeometry />
+      <TresMeshToonMaterial color="#FBB03B" />
+    </TresMesh>
+    <TresMesh
+      :position="[0, -3, 0]"
+      :rotation="[-Math.PI / 2, 0, 0]"
+    >
+      <TresPlaneGeometry :args="[10, 10, 10, 10]" />
+      <TresMeshStandardMaterial color="#f7f7f7" />
+    </TresMesh>
+  </TresCanvas>
+</template>
+```
+
+## Lights (explanation)
+
+As you know every instance in [ThreeJs](https://threejs.org/) is available in **TresJs** so are all the light types, we just need to add the `Tres` prefix to use them.
+
+But not all lights can cast shadows, this definition comes directly from ThreeJs and makes sense, for example the purpose of an [ambientLight](https://threejs.org/docs/index.html?q=ambient#api/en/lights/AmbientLight) is to iluminate everysingle side of your scene, so it makes no sense for it to cast shadows, on the contrary, a [DirectionalLight](https://threejs.org/docs/index.html?q=light#api/en/helpers/DirectionalLightHelper) immitating the sun can and should cast shadows.
+
+## Shadows (explanation)
+
+There are also many types of shadows, for example the "soft shadow" is generated automatially when an object receives more light from one side, but in summary a "ThreeJS default shadow" that is directed towards another surface needs to be cast by a mesh and another mesh needs to receive it. As we see in our example, the `Plane` is receiving a shadow but not casting it. Please note that not all materials can cast or receive shadows.
+
+Internally, ThreeJS automatically generates a new mesh with a [ShadowMaterial](https://threejs.org/docs/index.html?q=shado#api/en/materials/ShadowMaterial) which gets updated in each frame, that is why if you apply animations, the shadow also is animated, but also why you have to use shadows carefully, because they could slow your performance down.
+
+::: warning
+The overuse of shadows in this way could drop your performance. However, there are ways to increase your performance, for more information please check out [this video](https://youtu.be/WGNvVGrS0kY?si=q7XyL5eABKUh3gbS&t=1256)
+:::
+
+## Enabling shadows
+
+We could divide this into three steps:
+
+### Activate shadows on the renderer
+
+```vue
+//...
+
+<template>
+  <TresCanvas
+    clear-color="#111"
+    shadows
+    window-size
+  />
+  //...
+</template>
+```
+### Set the light to cast shadows
+
+We can simple put the boolean `cast-shadow`, Vue understand this as a `prop` with `true` value
+
+_The AmbientLight doesn't generate any type of shadow here_
+
+```vue
+//...
+
+<template>
+  <TresAmbientLight :intensity="1" />
+  <TresDirectionalLight
+    cast-shadow
+    :position="[0, 2, 0]"
+    :intensity="1"
+  />
+  
+  //...
+</template>
+```
+### Set the objects to cast or receive shadows
+
+Similarly to the previous step, we set the mesh that we want to cast shadow (our sphere) with the `cast-shadow` prop, and set the object to receive shadow (our plane) with the `receive-shadow` prop.
+
+```vue
+//...
+
+<template>
+  <TresMesh
+    cast-shadow
+    :position="[2, -2, 0]"
+  >
+    <TresSphereGeometry />
+    <TresMeshToonMaterial color="#FBB03B" />
+  </TresMesh>
+  <TresMesh
+    receive-shadow
+    :position="[0, -3, 0]"
+    :rotation="[-Math.PI / 2, 0, 0]"
+  >
+    <TresPlaneGeometry :args="[10, 10, 10, 10]" />
+    <TresMeshStandardMaterial color="#f7f7f7" />
+  </TresMesh>
+  //...
+</template>
+```
+
+Now we have all the necessary steps to add shadows to our scene, and if we apply what we learned in [basic animations](/examples/basic-animations), and we add movement to our cube, you will see the shadow is animated as well 🤩
+
+```vue
+<script setup>
+import { shallowRef } from 'vue'
+import { TresCanvas, useRenderLoop } from '@tresjs/core'
+
+const boxRef = shallowRef()
+
+const { onLoop } = useRenderLoop()
+
+onLoop(() => {
+  if (boxRef.value) {
+    boxRef.value.rotation.y += 0.01
+  }
+})
+</script>
+
+<template>
+  //...
+  <TresMesh
+    ref="boxRef"
+    cast-shadow
+    :position="[0, 0, 0]"
+  >
+    <TresBoxGeometry :args="[1.5, 1.5, 1.5]" />
+    <TresMeshToonMaterial color="#4F4F4F" />
+  </TresMesh>
+  //...
+</template>
+```
+
+_Note that I intentionally did not apply `cast-shadow` to the `Cone` so it doesn't cast any shadow_

+ 7 - 9
docs/examples/load-models.md

@@ -10,6 +10,10 @@ For this guide we are going to focus on loading gLTF (GL Transmission Format) mo
 
 There are several ways to load models on TresJS:
 
+::: warning
+Please note that the examples above we use top level await, make sure you wrap it with a [Suspense](https://vuejs.org/guide/built-ins/suspense.html#suspense) component. See Suspense for more information. .
+:::
+
 ## Using `useLoader`
 
 The `useLoader` composable allows you to pass any type of three.js loader and a URL to load the resource from. It returns a `Promise` with the loaded resource.
@@ -25,11 +29,9 @@ const { scene } = await useLoader(GLTFLoader, '/models/AkuAku.gltf')
 
 Then you can pass the model scene to a TresJS [`primitive`](/advanced/primitive) component to render it:
 
-```html{3}
+```html{2}
 <TresCanvas>
-  <Suspense>
     <primitive :object="scene" />
-  </Suspense>
 </TresCanvas>
 ```
 
@@ -72,9 +74,7 @@ const { scene, nodes, animations, materials } = await useGLTF('/models/AkuAku.gl
   >
     <TresPerspectiveCamera :position="[11, 11, 11]" />
     <OrbitControls />
-    <Suspense>
-      <primitive :object="nodes.MyModel" />
-    </Suspense>
+    <primitive :object="nodes.MyModel" /> // please note that "MyModel" here is just a placeholder 
   </TresCanvas>
 </template>
 ```
@@ -113,11 +113,9 @@ const model = await useFBX('/models/AkuAku.fbx')
 
 Then is as straightforward as adding the scene to your scene:
 
-```html{3}
+```html{2}
 <TresCanvas shadows alpha>
-  <Suspense>
     <primitive :object="scene" />
-  </Suspense>
 </TresCanvas>
 ```
 

+ 174 - 0
docs/examples/shaders.md

@@ -0,0 +1,174 @@
+# Shaders
+
+This guide will help you get started with shaders in TresJS.
+
+We will build a simple scene with a blob. We will then animate the blob to softly distorted it.
+
+::: warning
+_Basic knowledge of how shaders work is necessary_
+:::
+
+<SandboxDemo url="https://play.tresjs.org/#eNqVVltv2zYU/iuE91BntSU7cYrBS4q0QTt0WNcgyfZSFxsjH9tMJVIjKdle4P++j9TFVJMU3oMDndvH71x4mIferSbzJs+jsqDetHdmEi1yywzZImcpl8vzWc+aWe/1TIosV9qyB2ZWPE3V+poWbMcWWmXsBaJf/By4ONRLLktuBqwwdE1yTvo3pfI24sLC5d7EidLd0E/6TthLJa1WqXnsLkhaZToRf1JilT5ufe1KE72YyZlMlDSW3aXqzpE9D5j3ZZGmR0BpnAopFkpnBl4PM8lYcSsymgK95GmBjxHbDbz+TZanwhbz0Chp3bDoj6LxgOHPURPwXtM/Bclk+0zA8WjATivv3Z5PSdrS5mbFUThw+nsma4awJMcBDeTQtbTnBZZFqjhydDn5nEuut0Iuq4jyj7JSKjFnGReyf1TVgDn7hGVqTumVMsIKJcHFyx+51WLDfvQu/by2Dtg4GrmyuuBOXLRlL9EAgHfVDmJPGeKwonnk9G2S0eZJzI3DTJT5BnPbxdw+g+kKFKRZCloHWTqxTbKDX1NZpn8F7rlW92gohH1lAsA6BqWGb+HqjV6jqU27F5ovM4x22PBcUyKMg89oLoosr9qI2EPbB4rvAXypUuUwfavQoIGLibZuTE/bjlV8KjYPTMn6toJteH/71Z2pzP3+A0NdLB8wSnluaM52R+z8dX28WLB+ffciP/ctr442yrglLXgaNXcw8t2qrCBQY7tQkNw5BmdxtaiwliBYQk8BAomxs/3uYUlKXA8Tlz722A/j8XjWc0tgrtaG8TRfcbYWEtLQiH+rcAB0N1DcqB3uFWmTuzaXdMkz0pxNm9HHAZ/HuPrV7wsOmi5UCe3k1H1zHwfRUZhK8MI31oT388J4NBpB6pz3kcyKaVrAXNfM+YdHopkTNBLn1XF15E2+Ik2/kMrI6i3O10vj/I8H7MT/HMPmrCbGDx/m17eDTcMdhNhQ9LQ7MwuHrsK5NB2FsfkMU4ybHH0fu1lPtbK8yXIIUqvo6gOLGcgj58cJX+G1eiLfMZz3vyeSdoe95UYkbd7tvEwmk+fYNmI1aFCcxcEU9ga96nUaZjyP7o2SeFv97M9qA8qA56ACnvXCx9AZZr2VtbmZxnEyl4jHJROljiTZWOZZHLpfnESn0SieC2Njp4b3rOcfng5w9Wz+H+wqAvCvQvha3T3Frol/zVH+A/Bb34tJhPGvkRtllAkXE2K7x/wQXOd3AcTTn8D3JZksLAP+P8EaO7i+gfvFGEsSiFgTtImybnVrP2wUjf10OHAV8D1oOA7nlIkDQBtXl/wkehWn4i6EbNYmZtIarPeFWH4zkYnKcpGS/pS769adTP//0q9eZ3VBLb9kRcnXJ/T3ZlNRvsKwkC5R7n0rcSfJVuZ3N7/TBt+tES9skdbNecZ4TUalheNYub0t5By0Az/P9oO/YHgeb827jSXpXtDHRO02J6/93GyDdtYqxRdfOO/v23H5nSrtMzuJTtqC7/4DVvHLxg==" />
+
+## Setting up the scene (optional)
+
+We import all the modules that we need, for comfort we can use the orbit-controls from cientos,
+[look here to see how](/examples/orbit-controls).
+
+Now, let's put our camera in the `[11,11,11]` position.
+
+Lastly just to help us with the location, let's add a simple plane, rotated in the X axis, with `[10, 10]` units.
+
+```vue
+<script setup lang="ts">
+import { TresCanvas } from '@tresjs/core'
+import { OrbitControls } from '@tresjs/cientos'
+</script>
+
+<template>
+  <TresCanvas
+    clear-color="#111"
+    window-size
+  >
+    <OrbitControls />
+    <TresPerspectiveCamera :position="[11, 11, 11]" />
+
+    <TresMesh :rotation="[-Math.PI / 2, 0, 0]">
+      <TresPlaneGeometry :args="[10, 10]" />
+      <TresMeshBasicMaterial color="#444" />
+    </TresMesh>
+  </TresCanvas>
+</template>
+```
+
+## ShaderMaterial
+
+As you know every instance in [ThreeJs](https://threejs.org/) is available in **TresJs**, so is the `ShaderMaterial`, we just need to add the `Tres` prefix to use it.
+
+For our blob, we could use a simple `SphereGeometry` adding some widthSegments and heightSegments to create a smooth effect, and put our blob 4 units in the Y positive axis
+
+```vue
+<TresMesh :position="[0, 4, 0]">
+  <TresSphereGeometry :args="[2, 32, 32]" />
+  <TresShaderMaterial />
+</TresMesh>
+```
+
+The `ShaderMaterial` accepts special properties, like `uniforms` `vertexShader` and `fragmentShader`, so we can create it in our script section and make the bind with our instance.
+
+For this example, our uniforms look like this:
+
+```ts
+import { Vector2 } from 'three'
+
+//...
+const uniforms = {
+  uTime: { value: 0 },
+  uAmplitude: { value: new Vector2(0.1, 0.1) },
+  uFrequency: { value: new Vector2(20, 5) },
+}
+//..
+```
+
+Our fragment shader looks like this:
+
+```ts
+//...
+const fragmentShader = `
+precision mediump float;
+varying vec2 vUv;
+
+void main() {
+    gl_FragColor = vec4(1.0, vUv.y, 0.5, 1.0);
+}
+`
+//..
+```
+
+And lastly our vertexShader:
+
+```ts
+const vertexShader = `
+uniform vec2 uAmplitude;
+uniform vec2 uFrequency;
+uniform float uTime;
+
+varying vec2 vUv;
+
+void main() {
+    vec4 modelPosition = modelMatrix * vec4(position, 1.0);
+    modelPosition.y += sin(modelPosition.x * uFrequency.x - uTime) * uAmplitude.x;
+    modelPosition.x += cos(modelPosition.y * uFrequency.y - uTime) * uAmplitude.y;
+
+    vec4 viewPosition = viewMatrix * modelPosition;
+    gl_Position = projectionMatrix * viewPosition;
+    vUv = uv;
+}
+`
+//..
+```
+
+## Animating the blob
+
+Similar to what we learn in the [Basic animations](/examples/basic-animations) example, we start by referencing our blob, using [Template Ref](https://vuejs.org/guide/essentials/template-refs.html)
+
+```vue
+<script setup lang="ts">
+import { shallowRef } from 'vue'
+import { TresCanvas } from '@tresjs/core'
+import { OrbitControls } from '@tresjs/cientos'
+
+const blobRef = shallowRef(null)
+//...
+</script>
+
+<template>
+  <TresCanvas
+    clear-color="#111"
+    window-size
+  >
+    <OrbitControls />
+    <TresPerspectiveCamera :position="[11, 11, 11]" />
+    <TresMesh
+      ref="blobRef"
+      :position="[0, 4, 0]"
+    >
+      <TresSphereGeometry :args="[2, 32, 32]" />
+      <TresShaderMaterial />
+    </TresMesh>
+  </TresCanvas>
+</template>
+```
+ Once we have got that, we could use the `onLoop` callback to animate our `uTime`.
+
+ ```ts
+import { TresCanvas, useRenderLoop } from '@tresjs/core'
+ 
+ //...
+ const { onLoop } = useRenderLoop()
+ 
+onLoop(({ elapsed }) => {
+   if (blobRef.value) {
+     blobRef.value.material.uniforms.uTime.value = elapsed
+   }
+})
+ //...
+```
+
+And that it is, we have our basic shader running smoothly.
+
+## Using GLSL vite-pluging (optional)
+
+_This step is completly optional and is out of the scope of the **TresJs** team_
+
+Defining our shader inline is not always the best idea, but if you're using [vite](https://vitejs.dev/) you can put your `GLSL` files in a different file just by using the [vite-plugin-glsl](https://www.npmjs.com/package/vite-plugin-glsl) (check out the link for the official documentation).
+
+And you could have a structure similar to this:
+
+```
+├── src/
+│   ├── myTresJsComponent.vue
+│   ├── shaders/
+│       ├── vertexShader.glsl
+│       ├── fragmentShader.glsl
+```

+ 1 - 1
docs/guide/index.md

@@ -81,7 +81,7 @@ We have a brand new [StackBlitz](https://stackblitz.com/) starter to try TresJS
 
 We also have a playground where you can try TresJS online. Check it out [here](https://playground.tresjs.org/).
 
-![](/public/playground.png)
+![](/playground.png)
 
 ## Motivation
 

+ 1 - 1
docs/package.json

@@ -9,7 +9,7 @@
     "preview": "vitepress preview"
   },
   "dependencies": {
-    "@tresjs/core": "workspace:*"
+    "@tresjs/core": "workspace:^"
   },
   "devDependencies": {
     "unocss": "^0.58.3",

TEMPAT SAMPAH
docs/public/debug-3D.png


TEMPAT SAMPAH
docs/public/devtools-scene-inspector.png


TEMPAT SAMPAH
docs/public/meme-debugging.jpg


TEMPAT SAMPAH
docs/public/vue-chrome-devtools.png


+ 1 - 1
package.json

@@ -1,7 +1,7 @@
 {
   "name": "@tresjs/core",
   "type": "module",
-  "version": "4.0.0-next.0",
+  "version": "3.7.0",
   "packageManager": "pnpm@8.10.2",
   "description": "Declarative ThreeJS using Vue Components",
   "author": "Alvaro Saburido <hola@alvarosaburido.dev> (https://github.com/alvarosabu/)",

+ 1 - 6
playground/src/components/TheExperience.vue

@@ -1,6 +1,5 @@
 <script setup lang="ts">
 import { ref, watchEffect } from 'vue'
-import { BasicShadowMap, SRGBColorSpace, NoToneMapping } from 'three'
 import { TresCanvas } from '@tresjs/core'
 import { OrbitControls } from '@tresjs/cientos'
 import { TresLeches, useControls } from '@tresjs/leches'
@@ -10,10 +9,6 @@ import '@tresjs/leches/styles'
 const gl = {
   clearColor: '#82DBC5',
   shadows: true,
-  alpha: false,
-  shadowMapType: BasicShadowMap,
-  outputColorSpace: SRGBColorSpace,
-  toneMapping: NoToneMapping,
 }
 
 const wireframe = ref(true)
@@ -35,7 +30,6 @@ watchEffect(() => {
     v-bind="gl"
     ref="canvas"
     class="awiwi"
-    :style="{ background: '#008080' }"
   >
     <TresPerspectiveCamera
       :position="[7, 7, 7]"
@@ -69,6 +63,7 @@ watchEffect(() => {
     <TresMesh
       :rotation="[-Math.PI / 2, 0, 0]"
       receive-shadow
+      @click="wireframe = !wireframe"
     >
       <TresPlaneGeometry :args="[10, 10, 10, 10]" />
       <TresMeshToonMaterial color="#D3FC8A" />

+ 55 - 0
playground/src/pages/lights.vue

@@ -0,0 +1,55 @@
+<script setup lang="ts">
+import type { TresObject } from '@tresjs/core'
+import { TresCanvas, vLightHelper, vAlwaysLookAt, vDistanceTo, vLog } from '@tresjs/core'
+import { BasicShadowMap, SRGBColorSpace, NoToneMapping } from 'three'
+
+import { OrbitControls } from '@tresjs/cientos'
+
+const gl = {
+  clearColor: '#82DBC5',
+  shadows: true,
+  alpha: false,
+  shadowMapType: BasicShadowMap,
+  outputColorSpace: SRGBColorSpace,
+  toneMapping: NoToneMapping,
+}
+
+const planeRef: Ref<TresObject | null> = ref(null)
+</script>
+
+<template>
+  <TresCanvas
+   
+    v-bind="gl"
+  >
+    <TresPerspectiveCamera :position="[3, 3, 3]" />
+    <OrbitControls />
+   
+    <TresDirectionalLight
+      v-light-helper
+      v-always-look-at="[8, 16, 0]"
+      :position="[0, 8, 4]"
+      :intensity="0.7"
+      color="yellow"
+      cast-shadow
+    />
+    <TresMesh
+      ref="planeRef"
+      v-log:material
+      :rotation="[-Math.PI / 2, 0, 0]"
+      receive-shadow
+    >
+      <TresPlaneGeometry :args="[10, 10, 10, 10]" />
+      <TresMeshToonMaterial />
+    </TresMesh>
+    <TresMesh
+      v-distance-to="planeRef"
+      :position="[2, 4, 0]"
+      cast-shadow
+    >
+      <TresSphereGeometry :args="[1, 32, 32]" />
+      <TresMeshToonMaterial color="yellow" />
+    </TresMesh>
+    <TresAmbientLight :intensity="1" />
+  </TresCanvas>
+</template>

+ 21 - 0
playground/src/pages/rendering-modes/index.vue

@@ -0,0 +1,21 @@
+<script setup lang="ts">
+import { TresCanvas } from '@tresjs/core'
+
+import Scene from './scene.vue'
+
+const clearColor = ref('#82DBC5')
+
+setTimeout(() => {
+  clearColor.value = '#000000'
+}, 3000)
+</script>
+
+<template>
+  <TresCanvas
+    :clear-color="clearColor"
+    render-mode="on-demand"
+    @render="() => console.log('onRender')"
+  >
+    <Scene />
+  </TresCanvas>
+</template>

+ 32 - 0
playground/src/pages/rendering-modes/scene.vue

@@ -0,0 +1,32 @@
+<script setup lang="ts">
+import { useRenderLoop, useTres } from '@tresjs/core'
+import { OrbitControls } from '@tresjs/cientos'
+
+const { invalidate, advance } = useTres()
+
+function onControlChange() {
+  invalidate()
+}
+
+const positionX = ref(0)
+const showMesh = ref(true)
+
+setTimeout(() => {
+  positionX.value = 1
+  /*   showMesh.value = false */
+
+}, 3000)
+</script>
+
+<template>
+  <OrbitControls @change="onControlChange" />
+  <TresGridHelper />
+  <TresMesh
+    v-if="showMesh"
+    :position-x="positionX"
+  >
+    <TresBoxGeometry />
+    <TresMeshNormalMaterial />
+  </TresMesh>
+  <TresAmbientLight :intensity="1" />
+</template>

+ 11 - 0
playground/src/router.ts

@@ -11,6 +11,11 @@ const routes = [
     name: 'Basic',
     component: () => import('./pages/TheBasic.vue'),
   },
+  {
+    path: '/lights',
+    name: 'lights',
+    component: () => import('./pages/lights.vue'),
+  },
   {
     path: '/groups',
     name: 'Groups',
@@ -81,11 +86,17 @@ const routes = [
     name: 'Primitives',
     component: () => import('./pages/primitives.vue'),
   },
+  {
+    path: '/rendering-modes',
+    name: 'Rendering Modes',
+    component: () => import('./pages/rendering-modes/index.vue'),
+  },
   {
     path: '/empty',
     name: 'empty',
     component: () => import('./pages/empty.vue'),
   },
+  
 ]
 export const router = createRouter({
   history: createWebHistory(),

File diff ditekan karena terlalu besar
+ 49 - 0
playground/vite.config.ts.timestamp-1706539768400-58c91108b32e6.mjs


File diff ditekan karena terlalu besar
+ 228 - 306
pnpm-lock.yaml


+ 7 - 10
src/components/TresCanvas.vue

@@ -24,7 +24,6 @@ import {
 import pkg from '../../package.json'
 import {
   useTresContextProvider,
-  useLogger,
   usePointerEventHandler,
   useRenderLoop,
   type TresContext,
@@ -46,6 +45,7 @@ export interface TresCanvasProps
   useLegacyLights?: boolean
   outputColorSpace?: ColorSpace
   toneMappingExposure?: number
+  renderMode?: 'always' | 'on-demand' | 'manual' 
 
   // required by useTresContextProvider
   camera?: TresCamera
@@ -66,9 +66,10 @@ const props = withDefaults(defineProps<TresCanvasProps>(), {
   preserveDrawingBuffer: undefined,
   logarithmicDepthBuffer: undefined,
   failIfMajorPerformanceCaveat: undefined,
+  renderMode: 'always',
 })
 
-const { logWarning } = useLogger()
+const emit = defineEmits(['render'])
 
 const canvas = ref<HTMLCanvasElement>()
 
@@ -123,19 +124,19 @@ 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,
     canvas: existingCanvas,
-    windowSize: props.windowSize,
-    disableRender,
+    windowSize: props.windowSize ?? false,
+    disableRender: disableRender.value ?? false,
     rendererOptions: props,
+    emit,
   })
 
-  usePointerEventHandler({ scene: scene.value, contextParts: context.value })
+  usePointerEventHandler(context.value)
 
   const { registerCamera, camera, cameras, deregisterCamera } = context.value
 
@@ -176,10 +177,6 @@ onMounted(() => {
   )
 
   if (!camera.value) {
-    logWarning(
-      'No camera found. Creating a default perspective camera. '
-        + 'To have full control over a camera, please add one to the scene.',
-    )
     addDefaultCamera()
   }
 

+ 0 - 3
src/composables/useCamera/index.ts

@@ -50,9 +50,6 @@ export const useCamera = ({ sizes, scene }: Pick<TresContext, 'sizes'> & { scene
     }
   })
 
-  scene.userData.tres__registerCamera = registerCamera
-  scene.userData.tres__deregisterCamera = deregisterCamera
-
   onUnmounted(() => {
     cameras.value = []
   })

+ 10 - 14
src/composables/usePointerEventHandler/index.ts

@@ -1,5 +1,4 @@
 import type { Intersection, Object3D, Object3DEventMap } from 'three'
-import type { TresScene } from 'src/types'
 import { computed, reactive, ref } from 'vue'
 import { uniqueBy } from '../../utils'
 import { useRaycaster } from '../useRaycaster'
@@ -17,11 +16,7 @@ export interface EventProps {
 }
 
 export const usePointerEventHandler = (
-  { scene, contextParts }:
-  {
-    scene: TresScene
-    contextParts: Pick<TresContext, 'renderer' | 'camera' | 'raycaster'>
-  },
+  ctx: TresContext,
 ) => {
   const objectsWithEventListeners = reactive({
     click: new Map<Object3D<Object3DEventMap>, CallbackFn>(),
@@ -54,13 +49,6 @@ export const usePointerEventHandler = (
     if (onPointerLeave) objectsWithEventListeners.pointerLeave.set(object, onPointerLeave)
   }
 
-  // to make the registerObject available in the custom renderer (nodeOps), it is attached to the scene
-  scene.userData.tres__registerAtPointerEventHandler = registerObject
-  scene.userData.tres__deregisterAtPointerEventHandler = deregisterObject
-
-  scene.userData.tres__registerBlockingObjectAtPointerEventHandler = registerBlockingObject
-  scene.userData.tres__deregisterBlockingObjectAtPointerEventHandler = deregisterBlockingObject
-
   const objectsToWatch = computed(() =>
     uniqueBy(
       [
@@ -73,7 +61,13 @@ export const usePointerEventHandler = (
     ),
   )
 
-  const { onClick, onPointerMove } = useRaycaster(objectsToWatch, contextParts)
+  // 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
+  ctx.registerObjectAtPointerEventHandler = registerObject
+  ctx.deregisterObjectAtPointerEventHandler = deregisterObject
+  ctx.registerBlockingObjectAtPointerEventHandler = registerBlockingObject
+  ctx.deregisterBlockingObjectAtPointerEventHandler = deregisterBlockingObject
+
+  const { onClick, onPointerMove } = useRaycaster(objectsToWatch, ctx)
 
   onClick(({ intersects, event }) => {
     if (intersects.length) objectsWithEventListeners.click.get(intersects[0].object)?.(intersects[0], event)
@@ -101,5 +95,7 @@ export const usePointerEventHandler = (
   return {
     registerObject,
     deregisterObject,
+    registerBlockingObject,
+    deregisterBlockingObject,
   }
 }

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

@@ -20,10 +20,10 @@ interface PointerClickEventPayload {
 
 export const useRaycaster = (
   objects: Ref<THREE.Object3D[]>,
-  { renderer, camera, raycaster }: Pick<TresContext, 'renderer' | 'camera' | 'raycaster'>,
+  ctx: TresContext,
 ) => {
   // having a separate computed makes useElementBounding work
-  const canvas = computed(() => renderer.value.domElement as HTMLCanvasElement)
+  const canvas = computed(() => ctx.renderer.value.domElement as HTMLCanvasElement)
 
   const { x, y } = usePointer({ target: canvas })
 
@@ -39,11 +39,11 @@ export const useRaycaster = (
   }
 
   const getIntersectsByRelativePointerPosition = ({ x, y }: { x: number; y: number }) => {
-    if (!camera.value) return
+    if (!ctx.camera.value) return
 
-    raycaster.value.setFromCamera(new Vector2(x, y), camera.value)
+    ctx.raycaster.value.setFromCamera(new Vector2(x, y), ctx.camera.value)
 
-    return raycaster.value.intersectObjects(objects.value, false)
+    return ctx.raycaster.value.intersectObjects(objects.value, false)
   }
 
   const getIntersects = (event?: PointerEvent | MouseEvent) => {

+ 5 - 1
src/composables/useRenderer/const.ts

@@ -1,4 +1,4 @@
-import { ACESFilmicToneMapping, PCFSoftShadowMap, SRGBColorSpace } from 'three'
+import { ACESFilmicToneMapping, NoToneMapping, PCFSoftShadowMap, SRGBColorSpace } from 'three'
 
 export const rendererPresets = {
   realistic: {
@@ -12,6 +12,10 @@ export const rendererPresets = {
       type: PCFSoftShadowMap,
     },
   },
+  flat: {
+    toneMapping: NoToneMapping,
+    toneMappingExposure: 1,
+  },
 }
 
 export type RendererPresetsType = keyof typeof rendererPresets

+ 70 - 22
src/composables/useRenderer/index.ts

@@ -1,5 +1,6 @@
-import { Color, WebGLRenderer } from 'three'
+import { ACESFilmicToneMapping, Color, WebGLRenderer } from 'three'
 import { shallowRef, watchEffect, onUnmounted, type MaybeRef, computed, watch } from 'vue'
+
 import {
   toValue,
   unrefElement,
@@ -77,7 +78,7 @@ export interface UseRendererOptions extends TransformToMaybeRefOrGetter<WebGLRen
    * CineonToneMapping, ACESFilmicToneMapping,
    * CustomToneMapping
    *
-   * @default NoToneMapping
+   * @default ACESFilmicToneMapping
    */
   toneMapping?: MaybeRefOrGetter<ToneMapping>
 
@@ -96,8 +97,8 @@ export interface UseRendererOptions extends TransformToMaybeRefOrGetter<WebGLRen
   clearColor?: MaybeRefOrGetter<TresColor>
   windowSize?: MaybeRefOrGetter<boolean | string>
   preset?: MaybeRefOrGetter<RendererPresetsType>
+  renderMode?: MaybeRefOrGetter<'always' | 'on-demand' | 'manual'>
 }
-
 /**
  * Reactive three.js WebGLRenderer instance
  *
@@ -110,26 +111,26 @@ export function useRenderer(
     canvas,
     options,
     disableRender,
-    contextParts: { sizes, camera },
+    emit,
+    contextParts: { sizes, camera, render, invalidate, advance },
   }:
   {
     canvas: MaybeRef<HTMLCanvasElement>
     scene: Scene
     options: UseRendererOptions
-    contextParts: Pick<TresContext, 'sizes' | 'camera'>
+    emit: (event: string, ...args: any[]) => void
+    contextParts: Pick<TresContext, 'sizes' | 'camera' | 'render'> & { invalidate: () => void; advance: () => void }
     disableRender: MaybeRefOrGetter<boolean>
   },
 ) {
 
   const webGLRendererConstructorParameters = computed<WebGLRendererParameters>(() => ({
-    alpha: toValue(options.alpha),
+    alpha: toValue(options.alpha) ?? true,
     depth: toValue(options.depth),
     canvas: unrefElement(canvas),
     context: toValue(options.context),
     stencil: toValue(options.stencil),
-    antialias: toValue(options.antialias) === undefined // an opinionated default of tres
-      ? true
-      : toValue(options.antialias),
+    antialias: toValue(options.antialias) ?? true,
     precision: toValue(options.precision),
     powerPreference: toValue(options.powerPreference),
     premultipliedAlpha: toValue(options.premultipliedAlpha),
@@ -140,25 +141,61 @@ export function useRenderer(
 
   const renderer = shallowRef<WebGLRenderer>(new WebGLRenderer(webGLRendererConstructorParameters.value))
 
+  function invalidateOnDemand() {
+    if (options.renderMode === 'on-demand') {
+      invalidate()
+    }
+  }
   // since the properties set via the constructor can't be updated dynamically,
   // the renderer is recreated once they change
   watch(webGLRendererConstructorParameters, () => {
     renderer.value.dispose()
     renderer.value = new WebGLRenderer(webGLRendererConstructorParameters.value)
+
+    invalidateOnDemand()
   })
 
-  watchEffect(() => {
+  watch([sizes.width, sizes.height], () => {
     renderer.value.setSize(sizes.width.value, sizes.height.value)
+    invalidateOnDemand()
+  }, {
+    immediate: true,
   })
 
+  watch(() => options.clearColor, invalidateOnDemand)
+
   const { pixelRatio } = useDevicePixelRatio()
 
-  watchEffect(() => {
+  watch(pixelRatio, () => {
     renderer.value.setPixelRatio(pixelRatio.value)
   })
 
   const { logError } = useLogger()
 
+  // TheLoop
+
+  const { resume, onLoop } = useRenderLoop()
+
+  onLoop(() => {
+    if (camera.value && !toValue(disableRender) && render.frames.value > 0) {
+      renderer.value.render(scene, camera.value)
+      emit('render', renderer.value)
+    }
+
+    // Reset priority
+    render.priority.value = 0
+
+    if (toValue(options.renderMode) === 'always') {
+      render.frames.value = 1
+    }
+    else {
+      render.frames.value = Math.max(0, render.frames.value - 1)
+    }
+
+  })
+
+  resume()
+
   const getThreeRendererDefaults = () => {
 
     const plainRenderer = new WebGLRenderer()
@@ -179,6 +216,20 @@ export function useRenderer(
 
   const threeDefaults = getThreeRendererDefaults()
 
+  const renderMode = toValue(options.renderMode)
+
+  if (renderMode === 'on-demand') { 
+    // Invalidate for the first time
+    invalidate()
+  }
+
+  if (renderMode === 'manual') {
+    // Advance for the first time, setTimeout to make sure there is something to render
+    setTimeout(() => {
+      advance()
+    }, 1)
+  }
+
   watchEffect(() => {
     const rendererPreset = toValue(options.preset)
 
@@ -189,6 +240,13 @@ export function useRenderer(
       merge(renderer.value, rendererPresets[rendererPreset])
     }
 
+    // Render mode
+
+    if (renderMode === 'always') {
+      // If the render mode is 'always', ensure there's always a frame pending
+      render.frames.value = Math.max(1, render.frames.value)
+    }
+
     const getValue = <T>(option: MaybeRefOrGetter<T>, pathInThree: string): T | undefined => {
       const value = toValue(option)
 
@@ -214,7 +272,7 @@ export function useRenderer(
       set(renderer.value, pathInThree, getValue(option, pathInThree))
 
     setValueOrDefault(options.shadows, 'shadowMap.enabled')
-    setValueOrDefault(options.toneMapping, 'toneMapping')
+    setValueOrDefault(options.toneMapping ?? ACESFilmicToneMapping, 'toneMapping')
     setValueOrDefault(options.shadowMapType, 'shadowMap.type')
 
     if (revision < 150)
@@ -234,17 +292,7 @@ export function useRenderer(
 
   })
 
-  const { pause, resume, onLoop } = useRenderLoop()
-
-  onLoop(() => {
-    if (camera.value && !toValue(disableRender))
-      renderer.value.render(scene, camera.value)
-  })
-
-  resume()
-
   onUnmounted(() => {
-    pause() // TODO should the render loop pause itself if there is no more renderer? 🤔 What if there is another renderer which needs the loop?
     renderer.value.dispose()
     renderer.value.forceContextLoss()
   })

+ 30 - 0
src/composables/useSizes/index.ts

@@ -0,0 +1,30 @@
+import { computed, readonly } from 'vue'
+import type { MaybeRefOrGetter, MaybeRef, ComputedRef, Ref } from 'vue'
+import { refDebounced, toValue, useElementSize, useWindowSize } from '@vueuse/core'
+
+export interface SizesType {
+  height: Readonly<Ref<number>>
+  width: Readonly<Ref<number>>
+  aspectRatio: ComputedRef<number>
+}
+
+export default function useSizes(
+  windowSize: MaybeRefOrGetter<boolean>,
+  canvas: MaybeRef<HTMLCanvasElement>,
+  debounceMs: number = 10,
+) {
+  const reactiveSize = toValue(windowSize)
+    ? useWindowSize()
+    : useElementSize(computed(() => toValue(canvas).parentElement))
+
+  const debouncedReactiveWidth = readonly(refDebounced(reactiveSize.width, debounceMs))
+  const debouncedReactiveHeight = readonly(refDebounced(reactiveSize.height, debounceMs))
+
+  const aspectRatio = computed(() => debouncedReactiveWidth.value / debouncedReactiveHeight.value)
+
+  return {
+    height: debouncedReactiveHeight,
+    width: debouncedReactiveWidth,
+    aspectRatio,
+  }
+}

+ 121 - 60
src/composables/useTresContextProvider/index.ts

@@ -1,6 +1,6 @@
-import { toValue, useElementSize, useFps, useMemory, useRafFn, useWindowSize, refDebounced } from '@vueuse/core'
-import { inject, provide, readonly, shallowRef, computed, ref, onUnmounted, watchEffect } from 'vue'
-import type { Camera, EventDispatcher, Scene, WebGLRenderer } from 'three'
+import { useFps, useMemory, useRafFn } from '@vueuse/core'
+import { inject, provide, readonly, shallowRef, computed, ref, onUnmounted } from 'vue'
+import type { Camera, EventDispatcher, Object3D, WebGLRenderer } from 'three'
 import { Raycaster } from 'three'
 import type { ComputedRef, DeepReadonly, MaybeRef, MaybeRefOrGetter, Ref, ShallowRef } from 'vue'
 import { calculateMemoryUsage } from '../../utils/perf'
@@ -8,31 +8,73 @@ import { useCamera } from '../useCamera'
 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 { EventProps } from '../usePointerEventHandler'
+import useSizes, { type SizesType } from '../useSizes'
+
+export interface InternalState {
+  priority: Ref<number>
+  frames: Ref<number>
+  maxFrames: number
+}
+
+export interface RenderState {
+  /**
+   * If set to 'on-demand', the scene will only be rendered when the current frame is invalidated
+   * If set to 'manual', the scene will only be rendered when advance() is called
+   * If set to 'always', the scene will be rendered every frame
+   */
+  mode: Ref<'always' | 'on-demand' | 'manual'>
+  priority: Ref<number>
+  frames: Ref<number>
+  maxFrames: number
+  canBeInvalidated: ComputedRef<boolean>
+}
+
+export interface PerformanceState {
+  maxFrames: number
+  fps: {
+    value: number
+    accumulator: number[]
+  }
+  memory: {
+    currentMem: number
+    allocatedMem: number
+    accumulator: number[]
+  }
+}
 
 export interface TresContext {
-  scene: ShallowRef<Scene>
-  sizes: { height: Ref<number>; width: Ref<number>; aspectRatio: ComputedRef<number> }
+  scene: ShallowRef<TresScene>
+  sizes: SizesType
   extend: (objects: any) => void
   camera: ComputedRef<Camera | undefined>
   cameras: DeepReadonly<Ref<Camera[]>>
   controls: Ref<(EventDispatcher & { enabled: boolean }) | null>
   renderer: ShallowRef<WebGLRenderer>
   raycaster: ShallowRef<Raycaster>
-  perf: {
-    maxFrames: number
-    fps: {
-      value: number
-      accumulator: number[]
-    }
-    memory: {
-      currentMem: number
-      allocatedMem: number
-      accumulator: number[]
-    }
-  }
+  perf: PerformanceState
+  render: RenderState
+  /**
+   * Invalidates the current frame when renderMode === 'on-demand'
+   */
+  invalidate: () => void
+  /**
+     * Advance one frame when renderMode === 'manual'
+     */
+  advance: () => void
+  // Camera
   registerCamera: (camera: Camera) => void
   setCameraActive: (cameraOrUuid: Camera | string) => void
   deregisterCamera: (camera: Camera) => void
+  // 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
 }
 
 export function useTresContextProvider({
@@ -41,40 +83,21 @@ export function useTresContextProvider({
   windowSize,
   disableRender,
   rendererOptions,
+  emit,
 }: {
-  scene: Scene
+  scene: TresScene
   canvas: MaybeRef<HTMLCanvasElement>
   windowSize: MaybeRefOrGetter<boolean>
   disableRender: MaybeRefOrGetter<boolean>
   rendererOptions: UseRendererOptions
+  emit: (event: string, ...args: any[]) => void
 }): TresContext {
 
-  const elementSize = computed(() =>
-    toValue(windowSize)
-      ? useWindowSize()
-      : useElementSize(toValue(canvas).parentElement),
-  )
-
-  const reactiveSize = shallowRef({
-    width: 0,
-    height: 0,
-  })
-  const debouncedReactiveSize = refDebounced(reactiveSize, 10)
-  const unWatchSize = watchEffect(() => {
-    reactiveSize.value = {
-      width: elementSize.value.width.value,
-      height: elementSize.value.height.value,
-    }
-  })
+  const { logWarning } = useLogger()
 
-  const aspectRatio = computed(() => debouncedReactiveSize.value.width / debouncedReactiveSize.value.height)
+  const localScene = shallowRef<TresScene>(scene)
+  const sizes = useSizes(windowSize, canvas)
 
-  const sizes = {
-    height: computed(() => debouncedReactiveSize.value.height),
-    width: computed(() => debouncedReactiveSize.value.width),
-    aspectRatio,
-  }
-  const localScene = shallowRef<Scene>(scene)
   const {
     camera,
     cameras,
@@ -83,16 +106,47 @@ export function useTresContextProvider({
     setCameraActive,
   } = useCamera({ sizes, scene })
 
+  // Render state
+
+  const render: RenderState = {
+    mode: ref(rendererOptions.renderMode || 'always') as Ref<'always' | 'on-demand' | 'manual'>,
+    priority: ref(0),
+    frames: ref(0),
+    maxFrames: 60,
+    canBeInvalidated: computed(() => render.mode.value === 'on-demand' && render.frames.value === 0),
+  }
+
+  function invalidate(frames = 1) {
+    // Increase the frame count, ensuring not to exceed a maximum if desired
+    if (rendererOptions.renderMode === 'on-demand') {
+      render.frames.value = Math.min(render.maxFrames, render.frames.value + frames)
+    }
+    else {
+      logWarning('`invalidate` can only be used when `renderMode` is set to `on-demand`')
+    }
+  }
+
+  function advance() {
+    if (rendererOptions.renderMode === 'manual') {
+      render.frames.value = 1
+    }
+    else {
+      logWarning('`advance` can only be used when `renderMode` is set to `manual`')
+    }
+  }
+
   const { renderer } = useRenderer(
     {
       scene,
       canvas,
       options: rendererOptions,
-      contextParts: { sizes, camera },
+      emit,
+      // TODO: replace contextParts with full ctx at https://github.com/Tresjs/tres/issues/516
+      contextParts: { sizes, camera, render, invalidate, advance },
       disableRender,
     })
 
-  const toProvide: TresContext = {
+  const ctx: TresContext = {
     sizes,
     scene: localScene,
     camera,
@@ -112,13 +166,21 @@ export function useTresContextProvider({
         accumulator: [],
       },
     },
+    render,
+    advance,
     extend,
+    invalidate,
     registerCamera,
     setCameraActive,
     deregisterCamera,
   }
 
-  provide('useTres', toProvide)
+  provide('useTres', ctx)
+
+  // Add context to scene local state
+  ctx.scene.value.__tres = {
+    root: ctx,
+  }
 
   // Performance
   const updateInterval = 100 // Update interval in milliseconds
@@ -131,8 +193,8 @@ export function useTresContextProvider({
 
     // Update WebGL Memory Usage (Placeholder for actual logic)
     // perf.memory.value = calculateMemoryUsage(gl)
-    if (toProvide.scene.value) {
-      toProvide.perf.memory.allocatedMem = calculateMemoryUsage(toProvide.scene.value as unknown as TresObject)
+    if (ctx.scene.value) {
+      ctx.perf.memory.allocatedMem = calculateMemoryUsage(ctx.scene.value as unknown as TresObject)
     }
 
     // Update memory usage
@@ -140,24 +202,24 @@ export function useTresContextProvider({
       lastUpdateTime = timestamp
 
       // Update FPS
-      toProvide.perf.fps.accumulator.push(fps.value as never)
+      ctx.perf.fps.accumulator.push(fps.value as never)
 
-      if (toProvide.perf.fps.accumulator.length > maxFrames) {
-        toProvide.perf.fps.accumulator.shift()
+      if (ctx.perf.fps.accumulator.length > maxFrames) {
+        ctx.perf.fps.accumulator.shift()
       }
 
-      toProvide.perf.fps.value = fps.value
+      ctx.perf.fps.value = fps.value
 
       // Update memory
       if (isSupported.value && memory.value) {
-        toProvide.perf.memory.accumulator.push(memory.value.usedJSHeapSize / 1024 / 1024 as never)
+        ctx.perf.memory.accumulator.push(memory.value.usedJSHeapSize / 1024 / 1024 as never)
 
-        if (toProvide.perf.memory.accumulator.length > maxFrames) {
-          toProvide.perf.memory.accumulator.shift()
+        if (ctx.perf.memory.accumulator.length > maxFrames) {
+          ctx.perf.memory.accumulator.shift()
         }
 
-        toProvide.perf.memory.currentMem
-        = toProvide.perf.memory.accumulator.reduce((a, b) => a + b, 0) / toProvide.perf.memory.accumulator.length
+        ctx.perf.memory.currentMem
+        = ctx.perf.memory.accumulator.reduce((a, b) => a + b, 0) / ctx.perf.memory.accumulator.length
 
       }
     }
@@ -167,7 +229,7 @@ export function useTresContextProvider({
   let accumulatedTime = 0
   const interval = 1 // Interval in milliseconds, e.g., 1000 ms = 1 second
 
-  const { pause, resume } = useRafFn(({ delta }) => {
+  const { pause } = useRafFn(({ delta }) => {
     if (!window.__TRES__DEVTOOLS__) return
 
     updatePerformanceData({ timestamp: performance.now() })
@@ -177,7 +239,7 @@ export function useTresContextProvider({
 
     // Check if the accumulated time is greater than or equal to the interval
     if (accumulatedTime >= interval) {
-      window.__TRES__DEVTOOLS__.cb(toProvide)
+      window.__TRES__DEVTOOLS__.cb(ctx)
 
       // Reset the accumulated time
       accumulatedTime = 0
@@ -185,11 +247,10 @@ export function useTresContextProvider({
   }, { immediate: true })
 
   onUnmounted(() => {
-    unWatchSize()
     pause()
   })
 
-  return toProvide
+  return ctx
 }
 
 export function useTresContext(): TresContext {

+ 83 - 70
src/core/nodeOps.ts

@@ -1,10 +1,10 @@
-import type { RendererOptions } from 'vue'
+import { watch, type RendererOptions } from 'vue'
 import { BufferAttribute } from 'three'
 import { isFunction } from '@alvarosabu/utils'
 import type { Object3D, 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 { catalogue } from './catalogue'
 
@@ -13,7 +13,6 @@ function noop(fn: string): any {
 }
 
 let scene: TresScene | null = null
-
 const { logError } = useLogger()
 
 const supportedPointerEvents = [
@@ -23,8 +22,19 @@ const supportedPointerEvents = [
   'onPointerLeave',
 ]
 
-export const nodeOps: RendererOptions<TresObject, TresObject> = {
-  createElement(tag, _isSVG, _anchor, props) {
+export function invalidateInstance(instance: TresObject) {
+  const ctx = instance.__tres.root
+  
+  if (!ctx) return
+  
+  if (ctx.render && ctx.render.canBeInvalidated.value) {
+    ctx.invalidate()
+  }
+
+}
+
+export const nodeOps: RendererOptions<TresObject, TresObject | null> = {
+  createElement(tag, _isSVG, _anchor, props): TresObject | null {
     if (!props) props = {}
 
     if (!props.args) {
@@ -33,14 +43,13 @@ export const nodeOps: RendererOptions<TresObject, TresObject> = {
     if (tag === 'template') return null
     if (isHTMLTag(tag)) return null
     let name = tag.replace('Tres', '')
-    let instance
+    let instance: TresObject | null
 
     if (tag === 'primitive') {
       if (props?.object === undefined) logError('Tres primitives need a prop \'object\'')
       const object = props.object as TresObject
       name = object.type
       instance = Object.assign(object.clone(), { type: name }) as TresObject
-      instance.userData.tres__primitive = true
     }
     else {
       const target = catalogue.value[name]
@@ -50,6 +59,8 @@ export const nodeOps: RendererOptions<TresObject, TresObject> = {
       instance = new target(...props.args)
     }
 
+    if (!instance) return null
+
     if (instance.isCamera) {
       if (!props?.position) {
         instance.position.set(3, 3, 3)
@@ -64,44 +75,46 @@ export const nodeOps: RendererOptions<TresObject, TresObject> = {
       else if (instance.isBufferGeometry) instance.attach = 'geometry'
     }
 
+    instance.__tres = {
+      ...instance.__tres,
+      type: name,
+      memoizedProps: props,
+      eventCount: 0,
+      disposable: true,
+      primitive: tag === 'primitive',
+    }
+
     // 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) {
-      if (props?.material?.isMaterial) (instance as TresObject3D).userData.tres__materialViaProp = true
-      if (props?.geometry?.isBufferGeometry) (instance as TresObject3D).userData.tres__geometryViaProp = true
+    if (instance.isObject3D && (props?.material || props?.geometry)) {
+      instance.__tres.disposable = false
     }
 
-    // Since THREE instances properties are not consistent, (Orbit Controls doesn't have a `type` property) 
-    // we take the tag name and we save it on the userData for later use in the re-instancing process.
-    instance.userData = {
-      ...instance.userData,
-      tres__name: name,
-      tres__memoisedProps: props,
-    }
-
-    return instance
+    return instance as TresObject
   },
   insert(child, parent) {
-    if (parent && parent.isScene) scene = parent as unknown as TresScene
+    if (!child) return
+    
+    if (parent && parent.isScene) {
+      scene = parent as unknown as TresScene
+    }
 
-    const parentObject = parent || scene
+    if (scene) {
+      child.__tres.root = scene.__tres.root as TresContext
+    }
 
+    const parentObject = parent || scene
+    
     if (child?.isObject3D) {
+      const { registerCamera, registerObjectAtPointerEventHandler } = child.__tres.root
       if (child?.isCamera) {
-        if (!scene?.userData.tres__registerCamera)
-          throw 'could not find tres__registerCamera on scene\'s userData'
-
-        scene?.userData.tres__registerCamera?.(child as unknown as Camera)
+        registerCamera(child as unknown as Camera)
       }
-
       if (
         child && supportedPointerEvents.some(eventName => child[eventName])
       ) {
-        if (!scene?.userData.tres__registerAtPointerEventHandler)
-          throw 'could not find tres__registerAtPointerEventHandler on scene\'s userData'
-
-        scene?.userData.tres__registerAtPointerEventHandler?.(child as Object3D)
+        registerObjectAtPointerEventHandler(child as Object3D)
       }
     }
 
@@ -121,67 +134,55 @@ export const nodeOps: RendererOptions<TresObject, TresObject> = {
   },
   remove(node) {
     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 object3D = node as unknown as Object3D
 
-      const disposeMaterialsAndGeometries = (object3D: Object3D) => {
+      const disposeMaterialsAndGeometries = (object3D: TresObject) => {
         const tresObject3D = object3D as TresObject3D
-
-        if (!object3D.userData.tres__materialViaProp) {
+        // TODO: to be improved on https://github.com/Tresjs/tres/pull/466/files
+        if (ctx.disposable) {
           tresObject3D.material?.dispose()
           tresObject3D.material = undefined
-        }
-
-        if (!object3D.userData.tres__geometryViaProp) {
           tresObject3D.geometry?.dispose()
           tresObject3D.geometry = undefined
         }
       }
 
-      const deregisterAtPointerEventHandler = scene?.userData.tres__deregisterAtPointerEventHandler
-      const deregisterBlockingObjectAtPointerEventHandler
-        = scene?.userData.tres__deregisterBlockingObjectAtPointerEventHandler
-
       const deregisterAtPointerEventHandlerIfRequired = (object: TresObject) => {
-
-        if (!deregisterBlockingObjectAtPointerEventHandler)
-          throw 'could not find tres__deregisterBlockingObjectAtPointerEventHandler on scene\'s userData'
-
-        scene?.userData.tres__deregisterBlockingObjectAtPointerEventHandler?.(object as Object3D)
-
-        if (!deregisterAtPointerEventHandler)
-          throw 'could not find tres__deregisterAtPointerEventHandler on scene\'s userData'
-
+        deregisterBlockingObjectAtPointerEventHandler(object as Object3D)
         if (
           object && supportedPointerEvents.some(eventName => object[eventName])
         )
-          deregisterAtPointerEventHandler?.(object as Object3D)
+          deregisterObjectAtPointerEventHandler?.(object as Object3D)
       }
 
       const deregisterCameraIfRequired = (object: Object3D) => {
-        const deregisterCamera = scene?.userData.tres__deregisterCamera
-
-        if (!deregisterCamera)
-          throw 'could not find tres__deregisterCamera on scene\'s userData'
+        const deregisterCamera = node.__tres.root.deregisterCamera
 
         if ((object as Camera).isCamera)
           deregisterCamera?.(object as Camera)
       }
 
       node.removeFromParent?.()
-      
-      object3D.traverse((child: Object3D) => {
-        disposeMaterialsAndGeometries(child)
+
+      node.traverse((child: Object3D) => {
+        disposeMaterialsAndGeometries(child as TresObject)
         deregisterCameraIfRequired(child)
         deregisterAtPointerEventHandlerIfRequired?.(child as TresObject)
       })
 
-      disposeMaterialsAndGeometries(object3D)
-      deregisterCameraIfRequired(object3D)
-      deregisterAtPointerEventHandlerIfRequired?.(object3D as TresObject)
-
+      disposeMaterialsAndGeometries(node)
+      deregisterCameraIfRequired(node as Object3D)
+      deregisterAtPointerEventHandlerIfRequired?.(node as TresObject)
+      invalidateInstance(node as TresObject)
       node.dispose?.()
       
     }
@@ -192,11 +193,13 @@ export const nodeOps: RendererOptions<TresObject, TresObject> = {
       let root = node
       let key = prop
       if (key === 'object' && prevValue !== null) {
+        console.log('patchProp', { prop, prevValue, nextValue, node })
         // If the prop 'object' is changed, we need to re-instance the object and swap the old one with the new one
         const newInstance = nodeOps.createElement('primitive', undefined, undefined, { 
           object: nextValue, 
         })
         for (const subkey in newInstance) {
+          if (subkey === 'uuid') continue
           const target = node[subkey]
           const value = newInstance[subkey]
           if (!target?.set && !isFunction(target)) node[subkey] = value
@@ -205,6 +208,7 @@ export const nodeOps: RendererOptions<TresObject, TresObject> = {
           else if (!target.isColor && target.setScalar) target.setScalar(value)
           else target.set(value)
         }
+        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) {
           node.geometry = undefined
@@ -215,13 +219,20 @@ export const nodeOps: RendererOptions<TresObject, TresObject> = {
         }
       }
 
-      if (node.isObject3D && key === 'blocks-pointer-events') {
-        if (nextValue || nextValue === '')
-          scene?.userData.tres__registerBlockingObjectAtPointerEventHandler?.(node as Object3D)
-        else
-          scene?.userData.tres__deregisterBlockingObjectAtPointerEventHandler?.(node as Object3D)
-
-        return
+      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
+        }
       }
 
       let finalKey = kebabToCamel(key)
@@ -231,7 +242,7 @@ export const nodeOps: RendererOptions<TresObject, TresObject> = {
         const prevNode = node as TresObject3D
         const prevArgs = prevValue ?? []
         const args = nextValue ?? []
-        const instanceName = node.userData.tres__name || 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))
@@ -272,6 +283,8 @@ export const nodeOps: RendererOptions<TresObject, TresObject> = {
       else if (Array.isArray(value)) target.set(...value)
       else if (!target.isColor && target.setScalar) target.setScalar(value)
       else target.set(value)
+
+      invalidateInstance(node as TresObject)
     }
   },
 

+ 27 - 2
src/core/nodeOpts.test.ts

@@ -96,8 +96,18 @@ describe('nodeOps', () => {
 
   it('insert should insert child into parent', async () => {
     // Setup
-    const parent: TresObject = new Scene()
-    const child: TresObject = new Mesh()
+    const parent = new Scene()
+    parent.__tres = {
+      root: {
+        registerCamera: () => { },
+        registerObjectAtPointerEventHandler: () => { },
+      }
+    }
+    const child = new Mesh()
+
+    child.__tres = {
+      root: null
+    }
 
     // Fake vnodes
     child.__vnode = {
@@ -132,6 +142,11 @@ describe('nodeOps', () => {
   it('patchProp should patch property of node', async () => {
     // Setup
     const node: TresObject = new Mesh()
+    node.__tres = {
+      root: {
+        invalidate: () => { },
+      }
+    }
     const prop = 'visible'
     const nextValue = false
 
@@ -145,6 +160,11 @@ describe('nodeOps', () => {
   it('patchProp should patch traverse pierced props', async () => {
     // Setup
     const node: TresObject = new Mesh()
+    node.__tres = {
+      root: {
+        invalidate: () => { },
+      }
+    }
     const prop = 'position-x'
     const nextValue = 5
 
@@ -158,6 +178,11 @@ describe('nodeOps', () => {
   it('patchProp it should not patch traverse pierced props of existing dashed properties', async () => {
     // Setup
     const node: TresObject = new Mesh()
+    node.__tres = {
+      root: {
+        invalidate: () => { },
+      }
+    }
     const prop = 'cast-shadow'
     const nextValue = true
 

+ 6 - 0
src/directives/index.ts

@@ -0,0 +1,6 @@
+import { vLog } from './vLog'
+import { vLightHelper } from './vLightHelper'
+import { vAlwaysLookAt } from './vAlwaysLookAt'
+import { vDistanceTo } from './vDistanceTo'
+
+export { vLog, vLightHelper, vAlwaysLookAt, vDistanceTo }

+ 21 - 0
src/directives/vAlwaysLookAt.ts

@@ -0,0 +1,21 @@
+import type { Object3D } from 'three'
+import type { Ref } from 'vue'
+import { extractBindingPosition } from '../utils'
+import type { TresVector3 } from '../types'
+import { useLogger, useRenderLoop } from '../composables'
+
+const { logWarning } = useLogger()
+
+export const vAlwaysLookAt = {
+  updated: (el: Object3D, binding: Ref<TresVector3>) => {
+    const observer = extractBindingPosition(binding)
+    if (!observer) {
+      logWarning(`v-always-look-at: problem with binding value: ${binding.value}`)
+      return
+    }
+    const { onLoop } = useRenderLoop()
+    onLoop(() => {
+      el.lookAt(observer)
+    })
+  },
+}

+ 37 - 0
src/directives/vDistanceTo.ts

@@ -0,0 +1,37 @@
+import { ArrowHelper } from 'three'
+import type { Ref } from 'vue'
+import { extractBindingPosition } from '../utils'
+import type { TresObject } from '../types'
+import { useLogger } from '../composables'
+
+const { logWarning } = useLogger()
+
+export const vDistanceTo = {
+  updated: (el: TresObject, binding: Ref<TresObject>) => {
+    const observer = extractBindingPosition(binding)
+    if (!observer) {
+      logWarning(`v-distance-to: problem with binding value: ${binding.value}`)
+      return
+    }
+    if (arrowHelper) {
+      arrowHelper.dispose()
+      el.parent.remove(arrowHelper)
+    }
+    const dir = observer.clone().sub(el.position)
+    dir.normalize()
+    arrowHelper = new ArrowHelper( dir, el.position, el.position.distanceTo(observer), 0xffff00 )
+    el.parent.add( arrowHelper )
+    // eslint-disable-next-line no-console
+    console.table([
+      ['Distance:', el.position.distanceTo(observer)],
+      [`origin: ${el.name || el.type}`, `x:${el.position.x}, y:${el.position.y}, z:${el.position?.z}`],
+      [`Destiny: ${el.name || el.type}`, `x:${observer.x}, y:${observer.y}, z:${observer?.z}`],
+    ],
+    )
+  },
+  unmounted: (el: TresObject) => {
+    arrowHelper?.dispose()
+    el.parent.remove(arrowHelper)
+  },
+}
+let arrowHelper: ArrowHelper | null = null

+ 62 - 0
src/directives/vLightHelper.ts

@@ -0,0 +1,62 @@
+
+import type {
+  Light,
+} from 'three'
+import { 
+  DirectionalLightHelper,
+  PointLightHelper,
+  SpotLightHelper,
+  HemisphereLightHelper,
+} from 'three'
+import { RectAreaLightHelper } from 'three-stdlib'
+import { useLogger } from '../composables'
+import type { TresObject } from '../types'
+
+const { logWarning } = useLogger()
+
+type LightHelper = typeof DirectionalLightHelper 
+| typeof PointLightHelper 
+| typeof SpotLightHelper 
+| typeof HemisphereLightHelper 
+| typeof RectAreaLightHelper
+
+let currentHelper: LightHelper
+let currentInstance: TresObject
+
+const helpers: Record<Light['type'], LightHelper> = {
+  DirectionalLight: DirectionalLightHelper,
+  PointLight: PointLightHelper,
+  SpotLight: SpotLightHelper,
+  HemisphereLight: HemisphereLightHelper,
+  RectAreaLight: RectAreaLightHelper,
+}
+
+export const vLightHelper = {
+  mounted: (el: TresObject) => {
+    if (!el.isLight) {
+      logWarning(`${el.type} is not a light`)
+      return
+    }
+    currentHelper = helpers[el.type]
+    el.parent.add(new currentHelper(el as never, 1, el.color.getHex()))
+  },
+  updated: (el: TresObject) => {
+    currentInstance = el.parent.children.find((child: TresObject) => child instanceof currentHelper)
+    if (currentInstance instanceof RectAreaLightHelper) return
+    currentInstance.update()
+
+  },
+  unmounted: (el: TresObject) => {
+    if (!el.isLight) {
+      logWarning(`${el.type} is not a light`)
+      return
+    }
+    currentInstance = el.parent.children.find((child: TresObject) => child instanceof currentHelper)
+
+    if (currentInstance && currentInstance.dispose) {
+      currentInstance.dispose()
+    }
+    el.parent.remove(currentInstance)
+  },
+}
+

+ 13 - 0
src/directives/vLog.ts

@@ -0,0 +1,13 @@
+import type { TresObject } from '../types'
+
+export const vLog = {
+  mounted: (el: TresObject, binding: { arg: string }) => {
+    if (binding.arg) {
+    // eslint-disable-next-line no-console
+      console.log(`v-log:${binding.arg}`, el[binding.arg])
+      return
+    }
+    // eslint-disable-next-line no-console
+    console.log('v-log', el)
+  },
+}

+ 1 - 0
src/index.ts

@@ -7,6 +7,7 @@ export * from './composables'
 export * from './core/catalogue'
 export * from './components'
 export * from './types'
+export * from './directives'
 
 export interface TresOptions {
   extends?: Record<string, unknown>

+ 18 - 16
src/types/index.ts

@@ -2,7 +2,7 @@
 import type { DefineComponent, VNode, VNodeRef } from 'vue'
 
 import type * as THREE from 'three'
-import type { EventProps as PointerEventHandlerEventProps } from '../composables/usePointerEventHandler'
+import type { TresContext } from '../composables/useTresContextProvider'
 
 // Based on React Three Fiber types by Pmndrs
 // https://github.com/pmndrs/react-three-fiber/blob/v9/packages/fiber/src/three-types.ts
@@ -37,29 +37,31 @@ interface TresBaseObject {
   [prop: string]: any // for arbitrary properties
 }
 
+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
+  primitive?: boolean
+  eventCount: number
+  handlers: Partial<EventHandlers>
+  memoizedProps: { [key: string]: any }
+  disposable: boolean
+  root: TresContext
+}
+
 // Custom type for geometry and material properties in Object3D
 export interface TresObject3D extends THREE.Object3D<THREE.Object3DEventMap> {
   geometry?: THREE.BufferGeometry & TresBaseObject
   material?: THREE.Material & TresBaseObject
-  userData: {
-    tres__materialViaProp: boolean
-    tres__geometryViaProp: boolean
-    [key: string]: any
-  }
 }
 
-export type TresObject = TresBaseObject & (TresObject3D | THREE.BufferGeometry | THREE.Material | THREE.Fog)
+export type TresObject = 
+  TresBaseObject & (TresObject3D | THREE.BufferGeometry | THREE.Material | THREE.Fog) & { __tres: LocalState }
 
 export interface TresScene extends THREE.Scene {
-  userData: {
-    // keys are prefixed with tres__ to avoid name collisions
-    tres__registerCamera?: (newCamera: THREE.Camera, active?: boolean) => void
-    tres__deregisterCamera?: (camera: THREE.Camera) => void
-    tres__registerAtPointerEventHandler?: (object: THREE.Object3D & PointerEventHandlerEventProps) => void
-    tres__deregisterAtPointerEventHandler?: (object: THREE.Object3D) => void
-    tres__registerBlockingObjectAtPointerEventHandler?: (object: THREE.Object3D) => void
-    tres__deregisterBlockingObjectAtPointerEventHandler?: (object: THREE.Object3D) => void
-    [key: string]: any
+  __tres: {
+    root: TresContext
   }
 }
 

+ 11 - 2
src/utils/index.ts

@@ -1,5 +1,5 @@
-import THREE, { MeshBasicMaterial, DoubleSide } from 'three'
-import type { Mesh, type Scene, type Object3D } from 'three'
+import { MeshBasicMaterial, DoubleSide, Vector3 } from 'three'
+import type { Mesh, Scene, Object3D } from 'three'
 import { HightlightMesh } from '../devtools/highlight'
 
 export function toSetMethodName(key: string) {
@@ -226,4 +226,13 @@ export function createHighlightMesh(object: Object3D): Mesh {
   const highlightMesh = new HightlightMesh(object.geometry.clone(), highlightMaterial)
 
   return highlightMesh
+}
+
+export function extractBindingPosition(binding: any): Vector3 {
+  let observer = binding.value
+  if (binding.value && binding.value?.isMesh) {
+    observer = binding.value.position
+  }
+  if (Array.isArray(binding.value)) observer = new Vector3(...observer)
+  return observer
 }

Beberapa file tidak ditampilkan karena terlalu banyak file yang berubah dalam diff ini