Bläddra i källkod

feat: 331 new context for state (#333)

* feat: tres context provider

* feat: useContextProvider onMounted

* feat: revert width height composables

* feat: fixed renderer watch for aspectRatio changes

* chore: clean up

* feat: refactor raycaster

* feat: 331 new context for state  tino (#340)

* feat: refactored composables; seperated logic from TresCanvas

* chore: more changes concerning renderer composable

* feat: made window size reactive

* chore: made compasable params more uniform

* chore: refactored useRenderer

* chore: type cleanup

* feat: added user data key for active cameras

* feat: made renderer constructor params reactive

* feat: made multiple cameras work

* feat: made camera handling work properly and fixed event handler problem

* feat: added option to set a camera active by it's uuid and the object itself

* chore: moved composable to composables

* chore: removed obsolete todo comments

* chore: set antialias to be true by default in TresCanvas

* chore: fix debugUI playground

* chore: pnpm lock

* chore: added localOrbitControls

* chore: made render loop start initially

* chore: changes concerning PR review

* chore: changes concerning PR review

* chore: remove camera test for now

* chore: release v2.4.0-next.4

* chore: ci update for pnpm actions

* chore: correct typo on test action name

* chore: restored reactivity of renderer composable

* chore: replaced userData handling in keys.ts by types

* chore: made tests temporarily work

* chore: disabled test temporarily

* chore: removed readonly on scene for post-processing

* chore: release v2.4.0-next.5

* chore: restored readonly on scene and removed it from renderer

* chore: release v2.4.0-next.6

* chore: fixed hmr

* chore: made usePointerEventHandler more uniform to other composables

* feat: made renderer presets have less priority than explicitly defined props

---------

Co-authored-by: Tino Koch <tinoooo@users.noreply.github.com>
Co-authored-by: alvarosabu <alvaro.saburido@gmail.com>

* chore: release v2.4.0-next.7

* feat: remove readonly for scene on state context

* chore: release v2.4.0-next.8

* feat: add defaults props for trescanvas to match renderer ones

* chore: release v2.4.0-next.9

* feat: changed camera behavior so that the first added camera is always the active one

* chore: release v2.4.0-next.10

* docs: updated docs concerning useTresContext composable

* feat: added useTres alias for useTresContext

* chore: changed useTresContext to useTres in the docs

* chore: removed wrong dependency

* fix: fixed issue caused by merge of main

* chore: switched camera from ref to shallowRef

* chore: removed obsolete comment

* fix: revert shallorRef for cameras

---------

Co-authored-by: Tino Koch <17991193+Tinoooo@users.noreply.github.com>
Co-authored-by: Tino Koch <tinoooo@users.noreply.github.com>
Alvaro Saburido 1 år sedan
förälder
incheckning
0e66f43712
38 ändrade filer med 1361 tillägg och 1201 borttagningar
  1. 0 2
      CHANGELOG.md
  2. 4 3
      docs/.vitepress/theme/components/ExtendExample.vue
  3. 5 5
      docs/api/composables.md
  4. 14 14
      docs/api/renderer.md
  5. 3 0
      docs/package.json
  6. 13 0
      docs/vite.config.ts
  7. 13 13
      package.json
  8. 4 3
      playground/components.d.ts
  9. 5 4
      playground/package.json
  10. 14 16
      playground/src/components/DebugUI.vue
  11. 302 0
      playground/src/components/LocalOrbitControls.vue
  12. 14 10
      playground/src/components/MultipleCanvas.vue
  13. 19 0
      playground/src/components/TheCameraOperator.vue
  14. 1 3
      playground/src/pages/index.vue
  15. 62 0
      playground/src/pages/multiple-cameras.vue
  16. 6 0
      playground/src/pages/multiple.vue
  17. 37 0
      playground/src/pages/no-camera.vue
  18. 13 3
      playground/src/router.ts
  19. 6 1
      playground/vite.config.ts
  20. 234 221
      pnpm-lock.yaml
  21. 139 17
      src/components/TresCanvas.vue
  22. 0 161
      src/components/TresScene.vue
  23. 2 2
      src/composables/index.ts
  24. 52 192
      src/composables/useCamera/index.ts
  25. 0 106
      src/composables/useCamera/useCamera.test.ts
  26. 17 11
      src/composables/usePointerEventHandler/index.ts
  27. 19 24
      src/composables/useRaycaster/index.ts
  28. 1 1
      src/composables/useRenderLoop/index.ts
  29. 147 166
      src/composables/useRenderer/index.ts
  30. 0 178
      src/composables/useTres/index.ts
  31. 0 16
      src/composables/useTres/useTres.test.ts
  32. 98 0
      src/composables/useTresContextProvider/index.ts
  33. 68 15
      src/core/nodeOps.ts
  34. 2 2
      src/core/nodeOpts.test.ts
  35. 8 1
      src/index.ts
  36. 0 7
      src/keys.ts
  37. 18 4
      src/types/index.ts
  38. 21 0
      src/utils/index.ts

+ 0 - 2
CHANGELOG.md

@@ -1,5 +1,3 @@
-
-
 ### [2.4.2](https://github.com/Tresjs/tres/compare/2.4.1...2.4.2) (2023-07-14)
 ### [2.4.2](https://github.com/Tresjs/tres/compare/2.4.1...2.4.2) (2023-07-14)
 
 
 
 

+ 4 - 3
docs/.vitepress/theme/components/ExtendExample.vue

@@ -1,5 +1,5 @@
 <script setup lang="ts">
 <script setup lang="ts">
-import { useTres } from '@tresjs/core'
+import { useTresContext } from '@tresjs/core'
 
 
 const styles = {
 const styles = {
   width: '100%',
   width: '100%',
@@ -9,14 +9,15 @@ const styles = {
   overflow: 'hidden',
   overflow: 'hidden',
 }
 }
 
 
-const { state } = useTres()
+const { camera, renderer } = useTresContext()
+
 </script>
 </script>
 <template>
 <template>
   <ClientOnly>
   <ClientOnly>
     <TresCanvas shadows clear-color="#fff" :style="styles">
     <TresCanvas shadows clear-color="#fff" :style="styles">
       <TresPerspectiveCamera :position="[0, 2, 4]" />
       <TresPerspectiveCamera :position="[0, 2, 4]" />
       <TresScene>
       <TresScene>
-        <TresOrbitControls v-if="state.renderer" :args="[state.camera, state.renderer?.domElement]" />
+        <TresOrbitControls v-if="renderer" :args="[camera, renderer?.domElement]" />
 
 
         <TresDirectionalLight :position="[0, 2, 4]" :intensity="2" cast-shadow />
         <TresDirectionalLight :position="[0, 2, 4]" :intensity="2" cast-shadow />
         <TresMesh :rotation="[-Math.PI / 4, -Math.PI / 4, Math.PI / 4]">
         <TresMesh :rotation="[-Math.PI / 4, -Math.PI / 4, Math.PI / 4]">

+ 5 - 5
docs/api/composables.md

@@ -184,14 +184,14 @@ watch(carRef, ({ model }) => {
 This composable aims to provide access to the state model which contains the default renderer, camera, scene, and other useful properties.
 This composable aims to provide access to the state model which contains the default renderer, camera, scene, and other useful properties.
 
 
 ```ts
 ```ts
-const { state } = useTres()
+const { camera, renderer } = useTres()
 
 
-console.log(state.camera) // THREE.PerspectiveCamera
-console.log(state.renderer) // THREE.WebGLRenderer
+console.log(camera.value) // THREE.PerspectiveCamera
+console.log(renderer.value) // THREE.WebGLRenderer
 ```
 ```
 
 
 ::: warning
 ::: warning
-useTres composable can be only be used between the context of `TresCanvas` (inside sub-components) since Canvas component is the provider.
+useTres can be only be used inside of a `TresCanvas` since `TresCanvas` acts as the provider for the context data.
 :::
 :::
 
 
 ```vue
 ```vue
@@ -206,6 +206,6 @@ useTres composable can be only be used between the context of `TresCanvas` (insi
 <script lang="ts" setup>
 <script lang="ts" setup>
 import { useTres } from '@tresjs/core'
 import { useTres } from '@tresjs/core'
 
 
-const { state } = useTres()
+const context = useTres()
 </script>
 </script>
 ```
 ```

+ 14 - 14
docs/api/renderer.md

@@ -70,20 +70,20 @@ renderer.shadowMap.type: PCFSoftShadowMap
 
 
 ## Props
 ## Props
 
 
-| Prop                        | Description                                                                                                                                                     | Default            |
-| :-------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------ |
-| **shadows**                 | Enable shadows in the Renderer                                                                                                                                  | `false`            |
-| **shadowMapType**           | Set the shadow map type                                                                                                                                         | `PCFSoftShadowMap` |
-| **physicallyCorrectLights** | Whether to use physically correct lighting mode. See the [lights / physical example](https://threejs.org/examples/#webgl_lights_physical).                      | `false`            |
-| **outputColorSpace**          | Defines the output encoding                                                                                                                                     | `LinearEncoding`   |
-| **toneMapping**             | Defines the tone mapping exposure used by the renderer.                                                                                                         | `NoToneMapping`    |
-| **context**                 | This can be used to attach the renderer to an existing [RenderingContext](https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext)               |                    |
-| **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`          |
-| **preserveDrawingBuffer**   | Whether to preserve the buffers until manually cleared or overwritten..                                                                                         | `false`            |
-| **clearColor**              | The color the renderer will use to clear the canvas.                                                                                                            | `#000000`          |
-| **windowSize**              | Whether to use the window size as the canvas size or the parent element.                                                                                        | `false`            |
-| **disableRender**           | Disable render on requestAnimationFrame, usefull for PostProcessing                                                                                             | `false`            |
-| **camera**                  | A manual camera to be used by the renderer.                                                                                                                     |                    |
+| Prop                      | Description                                                                                                                                                     | Default            |
+| :------------------------ | :-------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------ |
+| **shadows**               | Enable shadows in the Renderer                                                                                                                                  | `false`            |
+| **shadowMapType**         | Set the shadow map type                                                                                                                                         | `PCFSoftShadowMap` |
+| **useLegacyLights**       | Whether to use the legacy lighting mode or not                                                                                                                  | `true`             |
+| **outputColorSpace**      | Defines the output encoding                                                                                                                                     | `LinearEncoding`   |
+| **toneMapping**           | Defines the tone mapping exposure used by the renderer.                                                                                                         | `NoToneMapping`    |
+| **context**               | This can be used to attach the renderer to an existing [RenderingContext](https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext)               |                    |
+| **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`          |
+| **preserveDrawingBuffer** | Whether to preserve the buffers until manually cleared or overwritten..                                                                                         | `false`            |
+| **clearColor**            | The color the renderer will use to clear the canvas.                                                                                                            | `#000000`          |
+| **windowSize**            | Whether to use the window size as the canvas size or the parent element.                                                                                        | `false`            |
+| **disableRender**         | Disable render on requestAnimationFrame, usefull for PostProcessing                                                                                             | `false`            |
+| **camera**                | A manual camera to be used by the renderer.                                                                                                                     |                    |
 
 
 ## Defaults
 ## Defaults
 
 

+ 3 - 0
docs/package.json

@@ -11,5 +11,8 @@
   "devDependencies": {
   "devDependencies": {
     "unocss": "^0.53.4",
     "unocss": "^0.53.4",
     "vite-svg-loader": "^4.0.0"
     "vite-svg-loader": "^4.0.0"
+  },
+  "dependencies": {
+    "@tresjs/core": "workspace:2.4.0-next.10"
   }
   }
 }
 }

+ 13 - 0
docs/vite.config.ts

@@ -3,6 +3,12 @@ import Unocss from 'unocss/vite'
 import svgLoader from 'vite-svg-loader'
 import svgLoader from 'vite-svg-loader'
 import Components from 'unplugin-vue-components/vite'
 import Components from 'unplugin-vue-components/vite'
 
 
+const whitelist = [
+  'TresCanvas',
+  'TresLeches',
+  'TresScene',
+]
+
 export default defineConfig({
 export default defineConfig({
   plugins: [
   plugins: [
     svgLoader(),
     svgLoader(),
@@ -16,4 +22,11 @@ export default defineConfig({
       dts: 'components.d.ts',
       dts: 'components.d.ts',
     }),
     }),
   ],
   ],
+  vue: {
+    template: {
+      compilerOptions: {
+        isCustomElement: (tag: string) => tag.startsWith('Tres') && !whitelist.includes(tag) || tag === 'primitive',
+      },
+    },
+  }
 })
 })

+ 13 - 13
package.json

@@ -64,7 +64,7 @@
   },
   },
   "dependencies": {
   "dependencies": {
     "@alvarosabu/utils": "^3.1.1",
     "@alvarosabu/utils": "^3.1.1",
-    "@vueuse/core": "^10.2.0"
+    "@vueuse/core": "^10.2.1"
   },
   },
   "devDependencies": {
   "devDependencies": {
     "@alvarosabu/prettier-config": "^1.3.0",
     "@alvarosabu/prettier-config": "^1.3.0",
@@ -73,17 +73,17 @@
     "@stackblitz/sdk": "^1.9.0",
     "@stackblitz/sdk": "^1.9.0",
     "@tresjs/cientos": "2.2.0",
     "@tresjs/cientos": "2.2.0",
     "@types/three": "^0.152.1",
     "@types/three": "^0.152.1",
-    "@typescript-eslint/eslint-plugin": "^5.60.0",
-    "@typescript-eslint/parser": "^5.60.0",
+    "@typescript-eslint/eslint-plugin": "^5.60.1",
+    "@typescript-eslint/parser": "^5.60.1",
     "@vitejs/plugin-vue": "^4.2.3",
     "@vitejs/plugin-vue": "^4.2.3",
-    "@vitest/coverage-c8": "^0.32.2",
-    "@vitest/ui": "^0.32.2",
+    "@vitest/coverage-c8": "^0.32.3",
+    "@vitest/ui": "^0.32.3",
     "@vue/test-utils": "^2.4.0",
     "@vue/test-utils": "^2.4.0",
-    "eslint": "^8.43.0",
+    "eslint": "^8.44.0",
     "eslint-config-prettier": "^8.8.0",
     "eslint-config-prettier": "^8.8.0",
     "eslint-plugin-vue": "^9.15.1",
     "eslint-plugin-vue": "^9.15.1",
     "esno": "^0.16.3",
     "esno": "^0.16.3",
-    "gsap": "^3.12.1",
+    "gsap": "^3.12.2",
     "jsdom": "^22.1.0",
     "jsdom": "^22.1.0",
     "kolorist": "^1.8.0",
     "kolorist": "^1.8.0",
     "ohmyfetch": "^0.4.21",
     "ohmyfetch": "^0.4.21",
@@ -93,18 +93,18 @@
     "rollup-plugin-analyzer": "^4.0.0",
     "rollup-plugin-analyzer": "^4.0.0",
     "rollup-plugin-copy": "^3.4.0",
     "rollup-plugin-copy": "^3.4.0",
     "rollup-plugin-visualizer": "^5.9.2",
     "rollup-plugin-visualizer": "^5.9.2",
-    "three": "^0.153.0",
-    "unocss": "^0.53.3",
+    "three": "^0.154.0",
+    "unocss": "^0.53.4",
     "unplugin": "^1.3.1",
     "unplugin": "^1.3.1",
     "unplugin-vue-components": "^0.25.1",
     "unplugin-vue-components": "^0.25.1",
     "vite": "^4.3.9",
     "vite": "^4.3.9",
     "vite-plugin-banner": "^0.7.0",
     "vite-plugin-banner": "^0.7.0",
-    "vite-plugin-dts": "2.3.0",
-    "vite-plugin-inspect": "^0.7.29",
-    "vite-plugin-require-transform": "^1.0.17",
+    "vite-plugin-dts": "3.0.2",
+    "vite-plugin-inspect": "^0.7.32",
+    "vite-plugin-require-transform": "^1.0.20",
     "vite-svg-loader": "^4.0.0",
     "vite-svg-loader": "^4.0.0",
     "vitepress": "1.0.0-beta.5",
     "vitepress": "1.0.0-beta.5",
-    "vitest": "^0.32.2",
+    "vitest": "^0.32.3",
     "vue": "^3.3.4",
     "vue": "^3.3.4",
     "vue-demi": "^0.14.5"
     "vue-demi": "^0.14.5"
   }
   }

+ 4 - 3
playground/components.d.ts

@@ -3,13 +3,12 @@
 // @ts-nocheck
 // @ts-nocheck
 // Generated by unplugin-vue-components
 // Generated by unplugin-vue-components
 // Read more: https://github.com/vuejs/core/pull/3399
 // Read more: https://github.com/vuejs/core/pull/3399
-import '@vue/runtime-core'
-
 export {}
 export {}
 
 
-declare module '@vue/runtime-core' {
+declare module 'vue' {
   export interface GlobalComponents {
   export interface GlobalComponents {
     AnimatedModel: typeof import('./src/components/AnimatedModel.vue')['default']
     AnimatedModel: typeof import('./src/components/AnimatedModel.vue')['default']
+    CameraOperator: typeof import('./src/components/CameraOperator.vue')['default']
     Cameras: typeof import('./src/components/Cameras.vue')['default']
     Cameras: typeof import('./src/components/Cameras.vue')['default']
     copy: typeof import('./src/components/TheBasic copy.vue')['default']
     copy: typeof import('./src/components/TheBasic copy.vue')['default']
     DanielTest: typeof import('./src/components/DanielTest.vue')['default']
     DanielTest: typeof import('./src/components/DanielTest.vue')['default']
@@ -17,6 +16,7 @@ declare module '@vue/runtime-core' {
     DeleteMe: typeof import('./src/components/DeleteMe.vue')['default']
     DeleteMe: typeof import('./src/components/DeleteMe.vue')['default']
     FBXModels: typeof import('./src/components/FBXModels.vue')['default']
     FBXModels: typeof import('./src/components/FBXModels.vue')['default']
     Gltf: typeof import('./src/components/gltf/index.vue')['default']
     Gltf: typeof import('./src/components/gltf/index.vue')['default']
+    LocalOrbitControls: typeof import('./src/components/LocalOrbitControls.vue')['default']
     MeshWobbleMaterial: typeof import('./src/components/meshWobbleMaterial/index.vue')['default']
     MeshWobbleMaterial: typeof import('./src/components/meshWobbleMaterial/index.vue')['default']
     MultipleCanvas: typeof import('./src/components/MultipleCanvas.vue')['default']
     MultipleCanvas: typeof import('./src/components/MultipleCanvas.vue')['default']
     PortalJourney: typeof import('./src/components/portal-journey/index.vue')['default']
     PortalJourney: typeof import('./src/components/portal-journey/index.vue')['default']
@@ -27,6 +27,7 @@ declare module '@vue/runtime-core' {
     TestSphere: typeof import('./src/components/TestSphere.vue')['default']
     TestSphere: typeof import('./src/components/TestSphere.vue')['default']
     Text3D: typeof import('./src/components/Text3D.vue')['default']
     Text3D: typeof import('./src/components/Text3D.vue')['default']
     TheBasic: typeof import('./src/components/TheBasic.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']
     TheConditional: typeof import('./src/components/TheConditional.vue')['default']
     TheEnvironment: typeof import('./src/components/TheEnvironment.vue')['default']
     TheEnvironment: typeof import('./src/components/TheEnvironment.vue')['default']
     TheEvents: typeof import('./src/components/TheEvents.vue')['default']
     TheEvents: typeof import('./src/components/TheEvents.vue')['default']

+ 5 - 4
playground/package.json

@@ -10,12 +10,13 @@
   },
   },
   "dependencies": {
   "dependencies": {
     "@tresjs/cientos": "2.1.4",
     "@tresjs/cientos": "2.1.4",
-    "vue-router": "^4.2.2"
+    "@tresjs/core": "workspace:2.4.0-next.10",
+    "vue-router": "^4.2.4"
   },
   },
   "devDependencies": {
   "devDependencies": {
-    "@tresjs/leches": "^0.3.1",
-    "unplugin-auto-import": "^0.16.4",
+    "@tresjs/leches": "^0.4.0",
+    "unplugin-auto-import": "^0.16.6",
     "vite-plugin-glsl": "^1.1.2",
     "vite-plugin-glsl": "^1.1.2",
-    "vue-tsc": "^1.8.1"
+    "vue-tsc": "^1.8.4"
   }
   }
 }
 }

+ 14 - 16
playground/src/components/DebugUI.vue

@@ -2,34 +2,32 @@
 import { TresCanvas } from '@tresjs/core'
 import { TresCanvas } from '@tresjs/core'
 import { BasicShadowMap, SRGBColorSpace, NoToneMapping } from 'three'
 import { BasicShadowMap, SRGBColorSpace, NoToneMapping } from 'three'
 
 
-import { OrbitControls, Box } from '@tresjs/cientos'
-import { TresLeches, useControls } from '@tresjs/leches'
-import '@tresjs/leches/styles'
+// import { OrbitControls, Box } from '@tresjs/cientos'
+/* import { TresLeches, useControls } from '@tresjs/leches' */
+/* import '@tresjs/leches/styles' */
 
 
-const gl = {
+const gl = reactive({
   clearColor: '#82DBC5',
   clearColor: '#82DBC5',
   shadows: true,
   shadows: true,
   alpha: false,
   alpha: false,
   shadowMapType: BasicShadowMap,
   shadowMapType: BasicShadowMap,
   outputColorSpace: SRGBColorSpace,
   outputColorSpace: SRGBColorSpace,
   toneMapping: NoToneMapping,
   toneMapping: NoToneMapping,
-}
+})
 
 
-const boxPosition = ref([0, 0.5, 0])
-
-useControls(gl)
-useControls('Box', boxPosition.value)
+/* useControls('fpsgraph') */
+// useControls(gl)
+// useControls('Box', boxPosition.value)
 
 
 </script>
 </script>
 
 
 <template>
 <template>
-  <TresLeches />
-  <TresCanvas v-bind="gl">
-    <TresPerspectiveCamera :position="[3, 3, 3]" />
-    <OrbitControls />
-    <Box :position-x="boxPosition[0]">
-      <TresMeshNormalMaterial />
-    </Box>
+  <TresCanvas v-bind="gl" :window-size="true">
+    <TresPerspectiveCamera :look-at="[0, 4, 0]" />
+    <TresMesh :position="[0, 4, 0]">
+      <TresBoxGeometry :args="[1, 1, 1]" />
+      <TresMeshToonMaterial color="teal" />
+    </TresMesh>
     <TresGridHelper />
     <TresGridHelper />
     <TresAmbientLight :intensity="1" />
     <TresAmbientLight :intensity="1" />
   </TresCanvas>
   </TresCanvas>

+ 302 - 0
playground/src/components/LocalOrbitControls.vue

@@ -0,0 +1,302 @@
+<script lang="ts" setup>
+import { Camera } from 'three'
+import { OrbitControls } from 'three-stdlib'
+import { ref, unref, onUnmounted } from 'vue'
+import { TresVector3, extend, useRenderLoop, useTresContext } from '@tresjs/core'
+import { useEventListener } from '@vueuse/core'
+
+export interface OrbitControlsProps {
+    /**
+     * Whether to make this the default controls.
+     *
+     * @default false
+     * @type {boolean}
+     * @memberof OrbitControlsProps
+     */
+    makeDefault?: boolean
+    /**
+     * The camera to control.
+     *
+     * @type {Camera}
+     * @memberof OrbitControlsProps
+     * @see https://threejs.org/docs/#examples/en/controls/OrbitControls.camera
+     */
+    camera?: Camera
+    /**
+     * The dom element to listen to.
+     *
+     * @type {HTMLElement}
+     * @memberof OrbitControlsProps
+     * @see https://threejs.org/docs/#examples/en/controls/OrbitControls.domElement
+     */
+    domElement?: HTMLElement
+    /**
+     * The target to orbit around.
+     *
+     * @type {TresVector3}
+     * @memberof OrbitControlsProps
+     * @see https://threejs.org/docs/#examples/en/controls/OrbitControls.target
+     */
+    target?: TresVector3
+    /**
+     * Whether to enable damping (inertia)
+     *
+     * @default false
+     * @type {boolean}
+     * @memberof OrbitControlsProps
+     * @see https://threejs.org/docs/#examples/en/controls/OrbitControls.enableDamping
+     */
+    enableDamping?: boolean
+    /**
+     * The damping inertia used if `.enableDamping` is set to true
+     *
+     * @default 0.05
+     * @type {number}
+     * @memberof OrbitControlsProps
+     * @see https://threejs.org/docs/#examples/en/controls/OrbitControls.dampingFactor
+     */
+    dampingFactor?: number
+    /**
+     * Set to true to automatically rotate around the target.
+     *
+     * @default false
+     * @type {boolean}
+     * @memberof OrbitControlsProps
+     * @see https://threejs.org/docs/#examples/en/controls/OrbitControls.autoRotate
+     */
+    autoRotate?: boolean
+    /**
+     * How fast to rotate around the target if `.autoRotate` is true.
+     *
+     * @default 2
+     * @type {number}
+     * @memberof OrbitControlsProps
+     * @see https://threejs.org/docs/#examples/en/controls/OrbitControls.autoRotateSpeed
+     */
+    autoRotateSpeed?: number
+    /**
+     * Whether to enable panning.
+     *
+     * @default true
+     * @type {boolean}
+     * @memberof OrbitControlsProps
+     * @see https://threejs.org/docs/#examples/en/controls/OrbitControls.enablePan
+     */
+    enablePan?: boolean
+    /**
+     * How fast to pan the camera when the keyboard is used. Default is 7.0 pixels per keypress.
+     *
+     * @default 7.0
+     * @type {number}
+     * @memberof OrbitControlsProps
+     * @see https://threejs.org/docs/#examples/en/controls/OrbitControls.keyPanSpeed
+     */
+    keyPanSpeed?: number
+    /**
+     * This object contains references to the keycodes for controlling camera panning.
+     * Default is the 4 arrow keys.
+     *
+     * @default `{ LEFT: 'ArrowLeft', UP: 'ArrowUp', RIGHT: 'ArrowRight', BOTTOM: 'ArrowDown' }`
+     * @type Record<string, string>
+     * @memberof OrbitControlsProps
+     * @see https://threejs.org/docs/#examples/en/controls/OrbitControls.keys
+     */
+    keys?: Record<string, string>
+    /**
+     * How far you can orbit horizontally, upper limit.
+     * If set, the interval [ min, max ] must be a sub-interval of [ - 2 PI, 2 PI ],
+     * with ( max - min < 2 PI ). Default is Infinity.
+     *
+     * @default Infinity
+     * @type {number}
+     * @memberof OrbitControlsProps
+     * @see https://threejs.org/docs/index.html?q=orbi#examples/en/controls/OrbitControls.maxAzimuthAngle
+     */
+    maxAzimuthAngle?: number
+    /**
+     * How far you can orbit horizontally, lower limit.
+     * If set, the interval [ min, max ] must be a sub-interval of [ - 2 PI, 2 PI ],
+     * with ( max - min < 2 PI ).
+     * Default is - Infinity.
+     *
+     * @default -Infinity
+     * @type {number}
+     * @memberof OrbitControlsProps
+     * @see https://threejs.org/docs/index.html?q=orbi#examples/en/controls/OrbitControls.minAzimuthAngle
+     */
+    minAzimuthAngle?: number
+    /**
+     * How far you can orbit vertically, upper limit.
+     * Range is 0 to Math.PI radians, and default is Math.PI.
+     *
+     * @default Math.PI
+     * @type {number}
+     * @memberof OrbitControlsProps
+     * @see https://threejs.org/docs/index.html?q=orbi#examples/en/controls/OrbitControls.maxPolarAngle
+     */
+    maxPolarAngle?: number
+    /**
+     * How far you can orbit vertically, lower limit.
+     * Range is 0 to Math.PI radians, and default is 0.
+     *
+     * @default 0
+     * @type {number}
+     * @memberof OrbitControlsProps
+     * @see https://threejs.org/docs/index.html?q=orbi#examples/en/controls/OrbitControls.minPolarAngle
+     */
+    minPolarAngle?: number
+    /**
+     * The minimum distance of the camera to the target.
+     * Default is 0.
+     *
+     * @default 0
+     * @type {number}
+     * @memberof OrbitControlsProps
+     * @see https://threejs.org/docs/index.html?q=orbi#examples/en/controls/OrbitControls.minDistance
+     */
+    minDistance?: number
+    /**
+     * The maximum distance of the camera to the target.
+     * Default is Infinity.
+     *
+     * @default Infinity
+     * @type {number}
+     * @memberof OrbitControlsProps
+     * @see https://threejs.org/docs/index.html?q=orbi#examples/en/controls/OrbitControls.maxDistance
+     */
+    maxDistance?: number
+    /**
+     * The minimum field of view angle, in radians.
+     * Default is 0.
+     *
+     * @default 0
+     * @type {number}
+     * @memberof OrbitControlsProps
+     * @see https://threejs.org/docs/index.html?q=orbi#examples/en/controls/OrbitControls.minZoom
+     */
+    minZoom?: number
+    /**
+     * The maximum field of view angle, in radians.
+     * ( OrthographicCamera only ).
+     * Default is Infinity.
+     *
+     * @default Infinity
+     * @type {number}
+     * @memberof OrbitControlsProps
+     * @see https://threejs.org/docs/index.html?q=orbi#examples/en/controls/OrbitControls.maxZoom
+     */
+    maxZoom?: number
+    touches?: {
+        ONE?: number
+        TWO?: number
+    }
+    /**
+     * Whether to enable zooming.
+     *
+     * @default true
+     * @type {boolean}
+     * @memberof OrbitControlsProps
+     * @see https://threejs.org/docs/#examples/en/controls/OrbitControls.enableZoom
+     */
+    enableZoom?: boolean
+    /**
+     * How fast to zoom in and out. Default is 1.
+     *
+     * @default 1
+     * @type {number}
+     * @memberof OrbitControlsProps
+     * @see https://threejs.org/docs/#examples/en/controls/OrbitControls.zoomSpeed
+     */
+    zoomSpeed?: number
+    /**
+     * Whether to enable rotating.
+     *
+     * @default true
+     * @type {boolean}
+     * @memberof OrbitControlsProps
+     * @see https://threejs.org/docs/#examples/en/controls/OrbitControls.enableRotate
+     */
+    enableRotate?: boolean
+    /**
+     * How fast to rotate around the target. Default is 1.
+     *
+     * @default 1
+     * @type {number}
+     * @memberof OrbitControlsProps
+     * @see https://threejs.org/docs/#examples/en/controls/OrbitControls.rotateSpeed
+     */
+    rotateSpeed?: number
+}
+
+// TODO: remove disable once eslint is updated to support vue 3.3
+// eslint-disable-next-line vue/no-setup-props-destructure
+const {
+    makeDefault = false,
+    autoRotate = false,
+    autoRotateSpeed = 2,
+    enableDamping = false,
+    dampingFactor = 0.05,
+    enablePan = true,
+    keyPanSpeed = 7,
+    maxAzimuthAngle = Infinity,
+    minAzimuthAngle = -Infinity,
+    maxPolarAngle = Math.PI,
+    minPolarAngle = 0,
+    minDistance = 0,
+    maxDistance = Infinity,
+    minZoom = 0,
+    maxZoom = Infinity,
+    enableZoom = true,
+    zoomSpeed = 1,
+    enableRotate = true,
+    rotateSpeed = 1,
+    target = [0, 0, 0],
+} = defineProps<OrbitControlsProps>()
+
+const { renderer, camera: activeCamera } = useTresContext()
+
+const controls = ref<OrbitControls | null>(null)
+
+extend({ OrbitControls })
+
+const emit = defineEmits(['change', 'start', 'end'])
+
+function addEventListeners() {
+    useEventListener(controls.value as any, 'change', () => emit('change', controls.value))
+    useEventListener(controls.value as any, 'start', () => emit('start', controls.value))
+    useEventListener(controls.value as any, 'end', () => emit('end', controls.value))
+}
+
+const { onLoop } = useRenderLoop()
+
+onLoop(() => {
+    if (controls.value && (enableDamping || autoRotate)) {
+        controls.value.update()
+    }
+})
+
+onMounted(() => {
+    addEventListeners()
+})
+
+onUnmounted(() => {
+    if (controls.value) {
+        controls.value.dispose()
+    }
+})
+
+watchEffect(() => {
+    console.log('activeCamera', activeCamera.value)
+    console.log('renderer', renderer.value)
+})
+</script>
+
+<template>
+    <TresOrbitControls v-if="activeCamera && renderer" ref="controls" :target="target" :auto-rotate="autoRotate"
+        :auto-rotate-speed="autoRotateSpeed" :enable-damping="enableDamping" :damping-factor="dampingFactor"
+        :enable-pan="enablePan" :key-pan-speed="keyPanSpeed" :keys="keys" :max-azimuth-angle="maxAzimuthAngle"
+        :min-azimuth-angle="minAzimuthAngle" :max-polar-angle="maxPolarAngle" :min-polar-angle="minPolarAngle"
+        :min-distance="minDistance" :max-distance="maxDistance" :min-zoom="minZoom" :max-zoom="maxZoom" :touches="touches"
+        :enable-zoom="enableZoom" :zoom-speed="zoomSpeed" :enable-rotate="enableRotate" :rotate-speed="rotateSpeed"
+        :args="[unref(activeCamera) || camera, renderer?.domElement || domElement]" />
+</template>

+ 14 - 10
playground/src/components/MultipleCanvas.vue

@@ -1,7 +1,7 @@
 <script setup lang="ts">
 <script setup lang="ts">
 import { BasicShadowMap, NoToneMapping, SRGBColorSpace } from 'three'
 import { BasicShadowMap, NoToneMapping, SRGBColorSpace } from 'three'
 import { TresCanvas } from '@tresjs/core'
 import { TresCanvas } from '@tresjs/core'
-import { GLTFModel, OrbitControls } from '@tresjs/cientos'
+// import { GLTFModel, OrbitControls } from '@tresjs/cientos'
 
 
 const state = reactive({
 const state = reactive({
   clearColor: '#201919',
   clearColor: '#201919',
@@ -10,6 +10,8 @@ const state = reactive({
   shadowMapType: BasicShadowMap,
   shadowMapType: BasicShadowMap,
   outputColorSpace: SRGBColorSpace,
   outputColorSpace: SRGBColorSpace,
   toneMapping: NoToneMapping,
   toneMapping: NoToneMapping,
+  disableRender: false,
+  stencil: false
 })
 })
 
 
 const state2 = reactive({
 const state2 = reactive({
@@ -21,16 +23,22 @@ const state2 = reactive({
   outputColorSpace: SRGBColorSpace,
   outputColorSpace: SRGBColorSpace,
   toneMapping: NoToneMapping, */
   toneMapping: NoToneMapping, */
 })
 })
+
+const log = () => {
+  console.log(3)
+}
+
 </script>
 </script>
 <template>
 <template>
   <div class="flex">
   <div class="flex">
+    <input id="" v-model="state.clearColor" type="text" name="">
+    <input v-model="state.stencil" type="checkbox" name="">
     <div class="w-1/2 aspect-video">
     <div class="w-1/2 aspect-video">
       <TresCanvas v-bind="state">
       <TresCanvas v-bind="state">
         <TresPerspectiveCamera :position="[5, 5, 5]" :fov="45" :near="0.1" :far="1000" :look-at="[0, 4, 0]" />
         <TresPerspectiveCamera :position="[5, 5, 5]" :fov="45" :near="0.1" :far="1000" :look-at="[0, 4, 0]" />
-        <OrbitControls />
 
 
         <TresAmbientLight :intensity="0.5" />
         <TresAmbientLight :intensity="0.5" />
-        <TresMesh :position="[0, 4, 0]">
+        <TresMesh :position="[0, 4, 0]" @click="log">
           <TresBoxGeometry :args="[1, 1, 1]" />
           <TresBoxGeometry :args="[1, 1, 1]" />
           <TresMeshToonMaterial color="cyan" />
           <TresMeshToonMaterial color="cyan" />
         </TresMesh>
         </TresMesh>
@@ -50,14 +58,10 @@ const state2 = reactive({
           <TresSphereGeometry :args="[2, 32, 32]" />
           <TresSphereGeometry :args="[2, 32, 32]" />
           <TresMeshToonMaterial color="yellow" />
           <TresMeshToonMaterial color="yellow" />
         </TresMesh>
         </TresMesh>
-        <OrbitControls />
 
 
-        <Suspense>
-          <GLTFModel
-            path="https://raw.githubusercontent.com/Tresjs/assets/main/models/gltf/aku-aku/AkuAku.gltf"
-            draco
-          />
-        </Suspense>
+        <!-- <Suspense>
+          <GLTFModel path="https://raw.githubusercontent.com/Tresjs/assets/main/models/gltf/aku-aku/AkuAku.gltf" draco />
+        </Suspense> -->
 
 
         <TresDirectionalLight :position="[0, 2, 4]" :intensity="1" cast-shadow />
         <TresDirectionalLight :position="[0, 2, 4]" :intensity="1" cast-shadow />
       </TresCanvas>
       </TresCanvas>

+ 19 - 0
playground/src/components/TheCameraOperator.vue

@@ -0,0 +1,19 @@
+<template>
+  <slot></slot>
+</template>
+
+<script lang="ts" setup>
+import { useTresContext } from '@tresjs/core';
+
+const { setCameraActive } = useTresContext()
+
+const props = defineProps<{
+  activeCameraUuid?: string
+}>()
+
+watchEffect(() => {
+  if (props.activeCameraUuid)
+    setCameraActive(props.activeCameraUuid)
+})
+
+</script>

+ 1 - 3
playground/src/pages/index.vue

@@ -1,6 +1,4 @@
 <script setup lang="ts"></script>
 <script setup lang="ts"></script>
 <template>
 <template>
-  <Suspense>
-    <DebugUI />
-  </Suspense>
+  <DebugUI />
 </template>
 </template>

+ 62 - 0
playground/src/pages/multiple-cameras.vue

@@ -0,0 +1,62 @@
+<script setup lang="ts">
+import { Camera } from 'three'
+import { TresCanvas } from '@tresjs/core'
+import { TresLeches, useControls } from '@tresjs/leches';
+import '@tresjs/leches/styles'
+const state = reactive({
+  clearColor: '#4f4f4f',
+  shadows: true,
+  alpha: false,
+})
+
+useControls('fpsgraph')
+
+const camera1 = shallowRef<Camera>()
+const camera2 = shallowRef<Camera>()
+const camera3 = shallowRef<Camera>()
+
+
+const activeCameraUuid = ref<string>()
+
+watchEffect(() => {
+  activeCameraUuid.value = camera1.value?.uuid
+})
+
+const camera3Exists = ref(false)
+</script>
+
+<template>
+  <div>
+    {{ activeCameraUuid }}
+    <select v-model="activeCameraUuid">
+      <option :value="camera1?.uuid">cam 1</option>
+      <option :value="camera2?.uuid">cam 2</option>
+      <option v-if="camera3Exists" :value="camera3?.uuid">cam 3</option>
+    </select>
+    <input v-model="camera3Exists" type="checkbox">
+    <div class="w-1/2 aspect-video">
+      <TresCanvas v-bind="state">
+        <TheCameraOperator :active-camera-uuid="activeCameraUuid">
+          <TresPerspectiveCamera ref="camera1" :position="[5, 5, 5]" :fov="45" :near="0.1" :far="1000"
+            :look-at="[0, 4, 0]" />
+          <TresPerspectiveCamera ref="camera2" :position="[15, 5, 5]" :fov="45" :near="0.1" :far="1000"
+            :look-at="[0, 4, 0]" />
+          <TresPerspectiveCamera v-if="camera3Exists" ref="camera3" :position="[-15, 8, 5]" :fov="25" :near="0.1"
+            :far="1000" :look-at="[0, 4, 0]" />
+        </TheCameraOperator>
+        <LocalOrbitControls />
+        <TresAmbientLight :intensity="0.5" />
+        <TresMesh :position="[0, 4, 0]">
+          <TresBoxGeometry :args="[1, 1, 1]" />
+          <TresMeshToonMaterial color="cyan" />
+        </TresMesh>
+
+        <Suspense>
+          <TestSphere />
+        </Suspense>
+        <TresDirectionalLight :position="[0, 2, 4]" :intensity="1" />
+      </TresCanvas>
+    </div>
+    <TresLeches />
+  </div>
+</template>

+ 6 - 0
playground/src/pages/multiple.vue

@@ -0,0 +1,6 @@
+<script setup lang="ts"></script>
+<template>
+  <Suspense>
+    <MultipleCanvas />
+  </Suspense>
+</template>

+ 37 - 0
playground/src/pages/no-camera.vue

@@ -0,0 +1,37 @@
+<script setup lang="ts">
+import { TresCanvas } from '@tresjs/core'
+import { PerspectiveCamera } from 'three';
+
+const state = reactive({
+  clearColor: '#4f4f4f',
+  shadows: true,
+  alpha: false,
+})
+
+const camera = new PerspectiveCamera(15, window.innerWidth / window.innerHeight, 0.1, 1000)
+camera.position.set(13, 13, 13)
+camera.lookAt(0, 0, 0)
+
+const useOwnCamera = ref(false)
+</script>
+
+<template>
+  <div>
+    <input v-model="useOwnCamera" type="checkbox">
+    <div class="w-1/2 aspect-video">
+      <TresCanvas v-bind="state" :camera="useOwnCamera ? camera : undefined">
+        <LocalOrbitControls />
+        <TresAmbientLight :intensity="0.5" />
+        <TresMesh :position="[0, 4, 0]">
+          <TresBoxGeometry :args="[1, 1, 1]" />
+          <TresMeshToonMaterial color="cyan" />
+        </TresMesh>
+
+        <Suspense>
+          <TestSphere />
+        </Suspense>
+        <TresDirectionalLight :position="[0, 2, 4]" :intensity="1" />
+      </TresCanvas>
+    </div>
+  </div>
+</template>

+ 13 - 3
playground/src/router.ts

@@ -7,9 +7,19 @@ const routes = [
         component: () => import('./pages/index.vue'),
         component: () => import('./pages/index.vue'),
     },
     },
     {
     {
-        path: '/shapes',
-        name: 'Shapes',
-        component: () => import('./pages/shapes.vue'),
+        path: '/multiple',
+        name: 'Multiple',
+        component: () => import('./pages/multiple.vue'),
+    },
+    {
+        path: '/multiple-cameras',
+        name: 'Multiple Cameras',
+        component: () => import('./pages/multiple-cameras.vue'),
+    },
+    {
+        path: '/no-camera',
+        name: 'No Camera',
+        component: () => import('./pages/no-camera.vue'),
     },
     },
 ]
 ]
 export const router = createRouter({
 export const router = createRouter({

+ 6 - 1
playground/vite.config.ts

@@ -11,7 +11,12 @@ import { templateCompilerOptions } from '@tresjs/core'
 export default defineConfig({
 export default defineConfig({
   plugins: [
   plugins: [
     glsl(),
     glsl(),
-    vue(templateCompilerOptions),
+    vue({
+      script: {
+        propsDestructure: true,
+      },
+      ...templateCompilerOptions
+    }),
     AutoImport({
     AutoImport({
       dts: true,
       dts: true,
       eslintrc: {
       eslintrc: {

Filskillnaden har hållts tillbaka eftersom den är för stor
+ 234 - 221
pnpm-lock.yaml


+ 139 - 17
src/components/TresCanvas.vue

@@ -1,37 +1,159 @@
 <script setup lang="ts">
 <script setup lang="ts">
-import Scene from './TresScene.vue'
-import { useTresProvider } from '../composables'
+import { extend } from '../core/catalogue'
+import { onMounted } from 'vue'
+import { createTres } from '../core/renderer'
+import { useTresContextProvider, type TresContext } from '../composables'
+import { App, Ref, computed, ref, shallowRef, watch, watchEffect } from 'vue'
+import {
+    Scene,
+    PerspectiveCamera,
+    WebGLRendererParameters,
+    type ColorSpace,
+    type ShadowMapType,
+    type ToneMapping,
+} from 'three'
+
+import {
+    useLogger,
+    useRenderLoop,
+    usePointerEventHandler,
+} from '../composables'
 
 
 import type { TresCamera } from '../types/'
 import type { TresCamera } from '../types/'
 import type { RendererPresetsType } from '../composables/useRenderer/const'
 import type { RendererPresetsType } from '../composables/useRenderer/const'
-import type { ColorSpace, ShadowMapType, ToneMapping } from 'three'
 
 
-export interface TresCanvasProps {
+
+export interface TresCanvasProps extends Omit<WebGLRendererParameters, 'canvas'> {
+    // required by for useRenderer
     shadows?: boolean
     shadows?: boolean
+    clearColor?: string
+    toneMapping?: ToneMapping
     shadowMapType?: ShadowMapType
     shadowMapType?: ShadowMapType
-    physicallyCorrectLights?: boolean
     useLegacyLights?: boolean
     useLegacyLights?: boolean
     outputColorSpace?: ColorSpace
     outputColorSpace?: ColorSpace
-    toneMapping?: ToneMapping
     toneMappingExposure?: number
     toneMappingExposure?: number
-    context?: WebGLRenderingContext
-    powerPreference?: 'high-performance' | 'low-power' | 'default'
-    preserveDrawingBuffer?: boolean
-    clearColor?: string
+
+    // required by useTresContextProvider
     windowSize?: boolean
     windowSize?: boolean
     preset?: RendererPresetsType
     preset?: RendererPresetsType
     disableRender?: boolean
     disableRender?: boolean
-    camera?: TresCamera
+    camera?: TresCamera,
 }
 }
 
 
-const props = defineProps<TresCanvasProps>()
+const props = withDefaults(defineProps<TresCanvasProps>(), {
+    alpha: false,
+    antialias: true,
+    depth: true,
+    stencil: true,
+    preserveDrawingBuffer: false,
+})
+
+const { logWarning } = useLogger()
+
+const canvas = ref<HTMLCanvasElement>()
+
+/*
+ `scene` is defined here and not in `useTresContextProvider` because the custom
+ 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 { resume } = useRenderLoop()
+
+const slots = defineSlots<{
+    default(): any
+}>()
+
+
+let app: App
+
+const mountCustomRenderer = (context: TresContext) => {
+    app = createTres(slots)
+    app.provide('useTres', context) // TODO obsolete?
+    app.provide('extend', extend)
+    app.mount(scene.value)
+}
+
+const dispose = () => {
+    scene.value.children = []
+    app.unmount()
+    app = createTres(slots)
+    app.provide('extend', extend)
+    app.mount(scene.value)
+    resume()
+}
+
+const disableRender = computed(() => props.disableRender)
+
+onMounted(() => {
+    const existingCanvas = canvas as Ref<HTMLCanvasElement>
+
+    const context = useTresContextProvider({
+        scene: scene.value,
+        canvas: existingCanvas,
+        windowSize: props.windowSize,
+        disableRender,
+        rendererOptions: props,
+    })
+
+    usePointerEventHandler({ scene: scene.value, contextParts: context })
+
+    const { addCamera, camera, cameras, removeCamera } = context
+
+    mountCustomRenderer(context)
+
+    const addDefaultCamera = () => {
+        const camera = new PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 1000)
+        camera.position.set(3, 3, 3)
+        camera.lookAt(0, 0, 0)
+        addCamera(camera)
+
+        const unwatch = watchEffect(
+            () => {
+                if (cameras.value.length >= 2) {
+                    camera.removeFromParent()
+                    removeCamera(camera)
+                    unwatch?.()
+                }
+            },
+        )
+    }
+
+    watch(() => props.camera, (newCamera, oldCamera) => {
+        if (newCamera)
+            addCamera(newCamera)
+        else if (oldCamera) {
+            oldCamera.removeFromParent()
+            removeCamera(oldCamera)
+        }
+    }, {
+        immediate: true
+    })
 
 
-const tres = useTresProvider()
+    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()
+    }
 
 
-defineExpose(tres)
+    if (import.meta.hot)
+        import.meta.hot.on('vite:afterUpdate', dispose)
+})
 </script>
 </script>
 <template>
 <template>
-    <Scene v-bind="props">
-        <slot />
-    </Scene>
+    <canvas ref="canvas" :data-scene="scene.uuid" :style="{
+        display: 'block',
+        width: '100%',
+        height: '100%',
+        position: windowSize ? 'fixed' : 'relative',
+        top: 0,
+        left: 0,
+        pointerEvents: 'auto',
+        touchAction: 'none',
+        zIndex: 1,
+    }">
+    </canvas>
 </template>
 </template>

+ 0 - 161
src/components/TresScene.vue

@@ -1,161 +0,0 @@
-<script setup lang="ts">
-import { App, getCurrentInstance, onMounted, onUnmounted, ref, watch } from 'vue'
-import { PerspectiveCamera, Scene } from 'three'
-
-import { createTres } from '../core/renderer'
-import {
-  TRES_CONTEXT_KEY,
-  useLogger,
-  useCamera,
-  useRenderer,
-  useRenderLoop,
-  useTres,
-  usePointerEventHandler,
-} from '../composables'
-import { extend } from '../core/catalogue'
-import { OBJECT_3D_USER_DATA_KEYS } from '../keys'
-
-import type { TresCamera } from '../types/'
-import type { RendererPresetsType } from '../composables/useRenderer/const'
-import type { ColorSpace, ShadowMapType, ToneMapping } from 'three'
-
-export interface TresSceneProps {
-  shadows?: boolean
-  shadowMapType?: ShadowMapType
-  physicallyCorrectLights?: boolean
-  useLegacyLights?: boolean
-  outputColorSpace?: ColorSpace
-  toneMapping?: ToneMapping
-  toneMappingExposure?: number
-  context?: WebGLRenderingContext
-  powerPreference?: 'high-performance' | 'low-power' | 'default'
-  preserveDrawingBuffer?: boolean
-  clearColor?: string
-  windowSize?: boolean
-  preset?: RendererPresetsType
-  disableRender?: boolean
-  camera?: TresCamera
-}
-
-
-const { logWarning } = useLogger()
-
-const props = withDefaults(defineProps<TresSceneProps>(), {
-  physicallyCorrectLights: false,
-})
-
-if (props.physicallyCorrectLights === true) {
-  logWarning('physicallyCorrectLights is deprecated, useLegacyLights is now false by default')
-}
-
-const container = ref<HTMLElement>()
-const canvas = ref<HTMLElement>()
-const scene = new Scene()
-
-const pointerEventHandler = usePointerEventHandler()
-const { setState } = useTres()
-
-scene.userData[OBJECT_3D_USER_DATA_KEYS.REGISTER_AT_POINTER_EVENT_HANDLER] = pointerEventHandler.registerObject
-
-setState('scene', scene)
-setState('canvas', canvas)
-setState('container', container)
-setState('pointerEventHandler', pointerEventHandler)
-setState('appContext', getCurrentInstance())
-const { onLoop, resume } = useRenderLoop()
-
-const { activeCamera, pushCamera, clearCameras } = useCamera()
-
-onMounted(() => {
-  initRenderer()
-})
-
-onUnmounted(() => {
-  setState('renderer', null)
-})
-
-
-function setCamera() {
-  const camera = scene.getObjectByProperty('isCamera', true)
-
-  if (!camera) {
-    // eslint-disable-next-line max-len
-    logWarning('No camera found. Creating a default perspective camera. To have full control over a camera, please add one to the scene.')
-    const camera = new PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 1000)
-    camera.position.set(3, 3, 3)
-    camera.lookAt(0, 0, 0)
-    pushCamera(camera)
-  } else {
-    pushCamera(camera as TresCamera)
-  }
-}
-
-function initRenderer() {
-  const { renderer } = useRenderer(props)
-
-  if (props.camera) {
-    pushCamera(props.camera)
-  }
-
-  onLoop(() => {
-    if (activeCamera.value && props.disableRender !== true) renderer.value?.render(scene, activeCamera.value)
-  })
-}
-
-let app: App
-
-const slots = defineSlots<{
-  default(): any
-}>()
-
-function mountApp() {
-  app = createTres(slots)
-  const tres = useTres()
-  app.provide('useTres', tres)
-  app.provide(TRES_CONTEXT_KEY, tres)
-  app.provide('extend', extend)
-  app.mount(scene as unknown)
-  setCamera()
-}
-mountApp()
-
-defineExpose({
-  scene
-})
-
-function dispose() {
-  scene.children = []
-  app.unmount()
-  app = createTres(slots)
-  app.provide('extend', extend)
-  app.mount(scene as unknown)
-  setCamera()
-  resume()
-}
-
-if (import.meta.hot) {
-  import.meta.hot.on('vite:afterUpdate', dispose)
-}
-
-watch(
-  () => props.camera,
-  camera => {
-    if (camera) {
-      clearCameras()
-      pushCamera(camera as any)
-    }
-  },
-)
-
-</script>
-
-<template>
-  <div ref="container" :key="scene.uuid" :data-scene="scene.uuid"
-    style="position: relative; width: 100%; height: 100%; pointerEvents: auto; touchAction: none;">
-    <div style="width: 100%; height: 100%;">
-      <canvas ref="canvas" :data-scene="scene.uuid"
-        :style="{ display: 'block', width: '100%', height: '100%', position: windowSize ? 'fixed' : 'absolute', top: 0, left: 0 }">
-      </canvas>
-    </div>
-  </div>
-</template>

+ 2 - 2
src/composables/index.ts

@@ -1,10 +1,10 @@
-export * from './useCamera'
+export * from './useCamera/'
 export * from './useRenderLoop/'
 export * from './useRenderLoop/'
 export * from './useRenderer/'
 export * from './useRenderer/'
 export * from './useLoader'
 export * from './useLoader'
 export * from './useTexture'
 export * from './useTexture'
-export * from './useTres'
 export * from './useRaycaster'
 export * from './useRaycaster'
 export * from './useLogger'
 export * from './useLogger'
 export * from './useSeek'
 export * from './useSeek'
 export * from './usePointerEventHandler'
 export * from './usePointerEventHandler'
+export * from './useTresContextProvider'

+ 52 - 192
src/composables/useCamera/index.ts

@@ -1,208 +1,68 @@
-import { TresCamera } from 'src/types'
-import { useTres } from '../useTres'
-import { PerspectiveCamera, OrthographicCamera } from 'three'
-
-import { toRef, Ref, watchEffect } from 'vue'
-
-export enum CameraType {
-  Perspective = 'Perspective',
-  Orthographic = 'Orthographic',
-}
-
-export type Camera = PerspectiveCamera | OrthographicCamera
-
-export interface PerspectiveCameraOptions {
-  /**
-   * Camera frustum vertical field of view, from bottom to top of view, in degrees.
-   *
-   * @type {number}
-   * @memberof PerspectiveCameraOptions
-   */
-  fov?: number
-  /**
-   * Camera frustum near plane.
-   *
-   * @type {number}
-   * @memberof PerspectiveCameraOptions
-   */
-  near?: number
-  /**
-   * Camera frustum far plane.
-   *
-   * @type {number}
-   * @memberof PerspectiveCameraOptions
-   */
-  far?: number
-}
-
-export interface OrthographicCameraOptions {
-  /**
-   * Camera frustum left plane.
-   *
-   * @type {number}
-   * @memberof OrthographicCameraOptions
-   */
-  left?: number
-  /**
-   * Camera frustum right plane.
-   *
-   * @type {number}
-   * @memberof OrthographicCameraOptions
-   */
-  right?: number
-  /**
-   * Camera frustum top plane.
-   *
-   * @type {number}
-   * @memberof OrthographicCameraOptions
-   */
-  top?: number
-  /**
-   * Camera frustum bottom plane.
-   *
-   * @type {number}
-   * @memberof OrthographicCameraOptions
-   */
-  bottom?: number
-  /**
-   * Camera frustum near plane.
-   *
-   * @type {number}
-   * @memberof OrthographicCameraOptions
-   */
-  near?: number
-  /**
-   * Camera frustum far plane.
-   *
-   * @type {number}
-   * @memberof OrthographicCameraOptions
-   */
-  far?: number
-}
-
-interface UseCameraReturn {
-  activeCamera: Ref<TresCamera | undefined>
-  createCamera: (cameraType?: CameraType, options?: PerspectiveCameraOptions | OrthographicCameraOptions) => Camera
-  updateCamera: () => void
-  pushCamera: (camera: TresCamera) => void
-  clearCameras: () => void
-  setFirstCamera: (camera: TresCamera) => void
-}
-
-const VERTICAL_FIELD_OF_VIEW = 45
-let camera: Camera
-
-/**
- * Create and update cameras
- *
- * ```ts
- * import { useCamera } from '@tresjs/core'
- * const { createCamera, updateCamera } = useCamera()
- * const camera = createCamera(new PerspectiveCamera(45, 1, 0.1, 1000))
- * updateCamera()
- * ```
- *
- * @export
- * @return {*}  {UseCameraReturn}
- */
-export function useCamera(): UseCameraReturn {
-  const { state, setState, aspectRatio } = useTres()
-  /* const aspectRatio = inject('aspect-ratio') */
-  /**
-   * Create camera and push to Tres `state.cameras` array
-   *
-   * ```ts
-   * import { useCamera } from '@tresjs/core'
-   * const { createCamera } = useCamera()
-   * const camera = createCamera(new PerspectiveCamera(45, 1, 0.1, 1000))
-   * ```
-   *
-   * @param {*} [cameraType=CameraType.Perspective]
-   * @param {(PerspectiveCameraOptions | OrthographicCameraOptions)} [options]
-   * @return {*}
-   */
-  function createCamera(
-    cameraType = CameraType.Perspective,
-    options?: PerspectiveCameraOptions | OrthographicCameraOptions,
-  ) {
-    if (cameraType === CameraType.Perspective) {
-      const { near, far, fov } = (options as PerspectiveCameraOptions) || {
-        near: 0.1,
-        far: 1000,
-        fov: VERTICAL_FIELD_OF_VIEW,
-      }
-      camera = new PerspectiveCamera(fov, state.aspectRatio?.value || window.innerWidth / window.innerHeight, near, far)
-      state.cameras?.push(camera as PerspectiveCamera)
-    } else {
-      const { left, right, top, bottom, near, far } = (options as OrthographicCameraOptions) || {
-        left: -100,
-        right: 100,
-        top: 100,
-        bottom: -100,
-        near: 0.1,
-        far: 1000,
-      }
-      camera = new OrthographicCamera(left, right, top, bottom, near, far)
-      state.cameras?.push(camera as OrthographicCamera)
-    }
-    state.camera = camera
+import { computed, watchEffect, onUnmounted, ref } from 'vue'
+import { Camera, OrthographicCamera, PerspectiveCamera } from 'three'
 
 
-    setState('camera', state.camera)
+import type { TresScene } from '../../types'
+import type { TresContext } from '../useTresContextProvider'
 
 
-    return camera
-  }
 
 
-  /**
-   * Update camera aspect ratio and projection matrix
-   *
-   */
-  function updateCamera() {
-    if (state.camera instanceof PerspectiveCamera && state.aspectRatio) {
-      state.camera.aspect = state.aspectRatio.value
-    }
-    state.camera?.updateProjectionMatrix()
-  }
+export const useCamera = ({ sizes, scene }: Pick<TresContext, 'sizes'> & { scene: TresScene }) => {
+
+  // the computed does not trigger, when for example the camera postion changes
+  const cameras = ref<Camera[]>([])
+  const camera = computed<Camera | undefined>(
+    () => cameras.value[0]
+  )
+
+  const addCamera = (newCamera: Camera, active = false) => {
+    if (cameras.value.some(({ uuid }) => uuid === newCamera.uuid))
+      return
+
+    if (active)
+      setCameraActive(newCamera)
+    else
+      cameras.value.push(newCamera)
 
 
-  /**
-   * Push camera to cameras array and update aspect ratio if camera is PerspectiveCamera
-   *
-   * @param {Camera} camera
-   */
-  function pushCamera(camera: Camera): void {
-    state.cameras?.push(camera)
-    if (camera instanceof PerspectiveCamera && state.aspectRatio) {
-      camera.aspect = state.aspectRatio.value
-    }
-    camera.updateProjectionMatrix()
-    setState('camera', camera)
   }
   }
 
 
-  function setFirstCamera(camera: Camera): void {
-    if (state.cameras?.length === 0) {
-      pushCamera(camera)
-    }
+  const removeCamera = (camera: Camera) => {
+    cameras.value = cameras.value.filter(({ uuid }) => uuid !== camera.uuid)
   }
   }
 
 
-  /**
-   * Clear cameras array
-   *
-   */
-  function clearCameras() {
-    state.cameras = []
+  const setCameraActive = (cameraOrUuid: string | Camera) => {
+    const camera = cameraOrUuid instanceof Camera ?
+      cameraOrUuid :
+      cameras.value.find((camera: Camera) => camera.uuid === cameraOrUuid)
+
+    if (!camera) return
+
+    const otherCameras = cameras.value.filter(({ uuid }) => uuid !== camera.uuid)
+    cameras.value = [camera, ...otherCameras]
   }
   }
 
 
   watchEffect(() => {
   watchEffect(() => {
-    if (aspectRatio?.value) {
-      updateCamera()
+    if (sizes.aspectRatio.value) {
+      cameras.value.forEach((camera: Camera) => {
+        if (camera instanceof PerspectiveCamera)
+          camera.aspect = sizes.aspectRatio.value
+
+        if (camera instanceof PerspectiveCamera || camera instanceof OrthographicCamera)
+          camera.updateProjectionMatrix();
+      })
     }
     }
   })
   })
 
 
+  scene.userData.tres__registerCamera = addCamera
+  scene.userData.tres__deregisterCamera = removeCamera
+
+  onUnmounted(() => {
+    cameras.value = []
+  })
+
   return {
   return {
-    activeCamera: toRef(state, 'camera'),
-    createCamera,
-    updateCamera,
-    pushCamera,
-    clearCameras,
-    setFirstCamera,
+    camera,
+    cameras,
+    addCamera,
+    removeCamera,
+    setCameraActive,
   }
   }
-}
+}

+ 0 - 106
src/composables/useCamera/useCamera.test.ts

@@ -1,106 +0,0 @@
-import { computed } from 'vue'
-import { OrthographicCamera, PerspectiveCamera } from 'three'
-import { describe, test, expect, vi, afterEach } from 'vitest'
-import { withSetup } from '../../utils/test-utils'
-import { CameraType, useCamera } from '.'
-import { useTresProvider } from '../useTres'
-
-useTresProvider()
-
-/* const [composable, app] = withSetup(() => useCamera()) */
-const aspectRatio = computed(() => 1)
-/* app.provide('aspect-ratio', aspectRatio) */
-
-describe.skip('useCamera', () => {
-  /* afterEach(() => {
-    composable.clearCameras()
-    app.unmount()
-  })
-  describe('createCamera', () => {
-    test('should create a camera', () => {
-      const { createCamera } = composable
-      const camera = createCamera(CameraType.Perspective)
-      expect(camera).toBeDefined()
-    })
-    test('should create a perspective camera', () => {
-      const { createCamera } = composable
-      const camera = createCamera(CameraType.Perspective)
-      expect(camera.type).toBe('PerspectiveCamera')
-    })
-    test('should create a perspective camera with default options', () => {
-      const { createCamera } = composable
-      const camera = createCamera(CameraType.Perspective)
-      expect((camera as PerspectiveCamera).fov).toBe(45)
-      expect((camera as PerspectiveCamera).near).toBe(0.1)
-      expect((camera as PerspectiveCamera).far).toBe(1000)
-    })
-    test('should create a perspective camera with custom options', () => {
-      const { createCamera } = composable
-      const camera = createCamera(CameraType.Perspective, {
-        fov: 60,
-        near: 1,
-        far: 100,
-      })
-      expect((camera as PerspectiveCamera).fov).toBe(60)
-      expect((camera as PerspectiveCamera).near).toBe(1)
-      expect((camera as PerspectiveCamera).far).toBe(100)
-    })
-    test('should create an orthographic camera', () => {
-      const { createCamera } = composable
-      const camera = createCamera(CameraType.Orthographic)
-      expect(camera.type).toBe('OrthographicCamera')
-    })
-    test('should create an orthographic camera with default options', () => {
-      const { createCamera } = composable
-      const camera = createCamera(CameraType.Orthographic)
-      expect((camera as OrthographicCamera).near).toBe(0.1)
-      expect((camera as OrthographicCamera).far).toBe(1000)
-      expect((camera as OrthographicCamera).left).toBe(-100)
-      expect((camera as OrthographicCamera).right).toBe(100)
-      expect((camera as OrthographicCamera).top).toBe(100)
-      expect((camera as OrthographicCamera).bottom).toBe(-100)
-    })
-
-    test('should create an orthographic camera with custom options', () => {
-      const { createCamera } = composable
-      const camera = createCamera(CameraType.Orthographic, {
-        near: 1,
-        far: 100,
-        left: -50,
-        right: 50,
-        top: 50,
-        bottom: -50,
-      })
-      expect((camera as OrthographicCamera).near).toBe(1)
-      expect((camera as OrthographicCamera).far).toBe(100)
-      expect((camera as OrthographicCamera).left).toBe(-50)
-      expect((camera as OrthographicCamera).right).toBe(50)
-      expect((camera as OrthographicCamera).top).toBe(50)
-      expect((camera as OrthographicCamera).bottom).toBe(-50)
-    })
-  })
-  describe('activeCamera', () => {
-    test('should return the latest camera', () => {
-      const { createCamera, activeCamera } = composable
-      createCamera(CameraType.Perspective)
-      expect(activeCamera.value.type).toBe('PerspectiveCamera')
-    })
-    test('should return the latest camera if used more than once', () => {
-      const { createCamera, activeCamera } = composable
-      createCamera(CameraType.Perspective)
-      createCamera(CameraType.Orthographic)
-      expect(activeCamera.value.type).toBe('OrthographicCamera')
-    })
-  })
-  describe('updateCamera', () => {
-    test('should update the current camera with aspect ratio change', () => {
-      const { activeCamera, createCamera, updateCamera } = composable
-      createCamera(CameraType.Perspective)
-      const updateProjectionMatrix = vi.spyOn(activeCamera.value, 'updateProjectionMatrix')
-      updateCamera()
-      expect(updateProjectionMatrix).toHaveBeenCalled()
-    })
-  }) */
-})
-
-// TODO: find a way to test this with useTresProvider approach

+ 17 - 11
src/composables/usePointerEventHandler/index.ts

@@ -1,19 +1,28 @@
 import { uniqueBy } from '../../utils'
 import { uniqueBy } from '../../utils'
 import { useRaycaster } from '../useRaycaster'
 import { useRaycaster } from '../useRaycaster'
 import { computed, reactive } from 'vue'
 import { computed, reactive } from 'vue'
+
+import type { TresContext } from '../useTresContextProvider'
 import type { Intersection, Event, Object3D } from 'three'
 import type { Intersection, Event, Object3D } from 'three'
+import { TresScene } from 'src/types'
 
 
-type CallbackFn = (intersection: Intersection<Object3D<Event>>, event: PointerEvent) => void //TODO document
+type CallbackFn = (intersection: Intersection<Object3D<Event>>, event: PointerEvent) => void
 type CallbackFnPointerLeave = (object: Object3D<Event>, event: PointerEvent) => void
 type CallbackFnPointerLeave = (object: Object3D<Event>, event: PointerEvent) => void
 
 
-type EventProps = {
+export type EventProps = {
   onClick?: CallbackFn
   onClick?: CallbackFn
   onPointerEnter?: CallbackFn
   onPointerEnter?: CallbackFn
   onPointerMove?: CallbackFn
   onPointerMove?: CallbackFn
   onPointerLeave?: CallbackFnPointerLeave
   onPointerLeave?: CallbackFnPointerLeave
 }
 }
 
 
-export const usePointerEventHandler = () => {
+export const usePointerEventHandler = (
+  { scene, contextParts }:
+    {
+      scene: TresScene,
+      contextParts: Pick<TresContext, 'renderer' | 'camera' | 'raycaster'>
+    }
+) => {
   const objectsWithEventListeners = reactive({
   const objectsWithEventListeners = reactive({
     click: new Map<Object3D, CallbackFn>(),
     click: new Map<Object3D, CallbackFn>(),
     pointerMove: new Map<Object3D, CallbackFn>(),
     pointerMove: new Map<Object3D, CallbackFn>(),
@@ -32,15 +41,12 @@ export const usePointerEventHandler = () => {
     if (onPointerMove) objectsWithEventListeners.pointerMove.set(object, onPointerMove)
     if (onPointerMove) objectsWithEventListeners.pointerMove.set(object, onPointerMove)
     if (onPointerEnter) objectsWithEventListeners.pointerEnter.set(object, onPointerEnter)
     if (onPointerEnter) objectsWithEventListeners.pointerEnter.set(object, onPointerEnter)
     if (onPointerLeave) objectsWithEventListeners.pointerLeave.set(object, onPointerLeave)
     if (onPointerLeave) objectsWithEventListeners.pointerLeave.set(object, onPointerLeave)
+  }
 
 
-    object.addEventListener('removed', () => {
-      object.traverse((child: Object3D) => {
-        deregisterObject(child)
-      })
+  // 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
 
 
-      deregisterObject(object)
-    })
-  }
 
 
   const objectsToWatch = computed(() =>
   const objectsToWatch = computed(() =>
     uniqueBy(
     uniqueBy(
@@ -51,7 +57,7 @@ export const usePointerEventHandler = () => {
     ),
     ),
   )
   )
 
 
-  const { onClick, onPointerMove } = useRaycaster(objectsToWatch)
+  const { onClick, onPointerMove } = useRaycaster(objectsToWatch, contextParts)
 
 
   onClick(({ intersects, event }) => {
   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)

+ 19 - 24
src/composables/useRaycaster/index.ts

@@ -1,9 +1,11 @@
-import { useTres } from '../useTres'
-import { Object3D, Raycaster, Vector2 } from 'three'
-import { Ref, computed, onUnmounted, watchEffect } from 'vue'
+import { type Intersection, Object3D, Vector2 } from 'three'
+import { Ref, computed, onUnmounted } from 'vue'
 import { EventHook, createEventHook, useElementBounding, usePointer } from '@vueuse/core'
 import { EventHook, createEventHook, useElementBounding, usePointer } from '@vueuse/core'
 
 
-export type Intersects = THREE.Intersection<THREE.Object3D<THREE.Event>>[]
+import { type TresContext } from '../useTresContextProvider'
+
+
+export type Intersects = Intersection<THREE.Object3D<THREE.Event>>[]
 interface PointerMoveEventPayload {
 interface PointerMoveEventPayload {
   intersects?: Intersects
   intersects?: Intersects
   event: PointerEvent
   event: PointerEvent
@@ -14,18 +16,17 @@ interface PointerClickEventPayload {
   event: PointerEvent
   event: PointerEvent
 }
 }
 
 
-export const useRaycaster = (objects: Ref<THREE.Object3D[]>) => {
-  const { state, setState } = useTres()
-
-  const canvas = computed(() => state.canvas?.value) // having a seperate computed makes useElementBounding work
+export const useRaycaster = (
+  objects: Ref<THREE.Object3D[]>,
+  { renderer, camera, raycaster }: Pick<TresContext, 'renderer' | 'camera' | 'raycaster'>
+) => {
+  // having a seperate computed makes useElementBounding work
+  const canvas = computed(() => renderer.value.domElement as HTMLCanvasElement)
 
 
   const { x, y } = usePointer({ target: canvas })
   const { x, y } = usePointer({ target: canvas })
 
 
   const { width, height, top, left } = useElementBounding(canvas)
   const { width, height, top, left } = useElementBounding(canvas)
 
 
-  const raycaster = new Raycaster()
-
-  setState('raycaster', raycaster)
 
 
   const getRelativePointerPosition = ({ x, y }: { x: number; y: number }) => {
   const getRelativePointerPosition = ({ x, y }: { x: number; y: number }) => {
     if (!canvas.value) return
     if (!canvas.value) return
@@ -37,11 +38,11 @@ export const useRaycaster = (objects: Ref<THREE.Object3D[]>) => {
   }
   }
 
 
   const getIntersectsByRelativePointerPosition = ({ x, y }: { x: number; y: number }) => {
   const getIntersectsByRelativePointerPosition = ({ x, y }: { x: number; y: number }) => {
-    if (!state.camera) return
+    if (!camera.value) return
 
 
-    raycaster.setFromCamera(new Vector2(x, y), state.camera)
+    raycaster.value.setFromCamera(new Vector2(x, y), camera.value)
 
 
-    return raycaster.intersectObjects(objects.value, false)
+    return raycaster.value.intersectObjects(objects.value, false)
   }
   }
 
 
   const getIntersects = (event?: PointerEvent | MouseEvent) => {
   const getIntersects = (event?: PointerEvent | MouseEvent) => {
@@ -83,16 +84,10 @@ export const useRaycaster = (objects: Ref<THREE.Object3D[]>) => {
 
 
   const onPointerLeave = (event: PointerEvent) => eventHookPointerMove.trigger({ event, intersects: [] })
   const onPointerLeave = (event: PointerEvent) => eventHookPointerMove.trigger({ event, intersects: [] })
 
 
-  const unwatch = watchEffect(() => {
-    if (!canvas?.value) return
-
-    canvas.value.addEventListener('pointerup', onPointerUp)
-    canvas.value.addEventListener('pointerdown', onPointerDown)
-    canvas.value.addEventListener('pointermove', onPointerMove)
-    canvas.value.addEventListener('pointerleave', onPointerLeave)
-
-    unwatch()
-  })
+  canvas.value.addEventListener('pointerup', onPointerUp)
+  canvas.value.addEventListener('pointerdown', onPointerDown)
+  canvas.value.addEventListener('pointermove', onPointerMove)
+  canvas.value.addEventListener('pointerleave', onPointerLeave)
 
 
   onUnmounted(() => {
   onUnmounted(() => {
     if (!canvas?.value) return
     if (!canvas?.value) return

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

@@ -39,7 +39,7 @@ onAfterLoop.on(() => {
   elapsed = clock.getElapsedTime()
   elapsed = clock.getElapsedTime()
 })
 })
 
 
-export function useRenderLoop(): UseRenderLoopReturn {
+export const useRenderLoop = (): UseRenderLoopReturn => {
   return {
   return {
     onBeforeLoop: onBeforeLoop.on,
     onBeforeLoop: onBeforeLoop.on,
     onLoop: onLoop.on,
     onLoop: onLoop.on,

+ 147 - 166
src/composables/useRenderer/index.ts

@@ -1,33 +1,32 @@
-/* eslint-disable max-len */
-import { watch, ref, shallowRef, computed, toRefs } from 'vue'
+import { Color, WebGLRenderer } from 'three'
+import { rendererPresets, RendererPresetsType } from './const'
+import { shallowRef, watchEffect, onUnmounted, type MaybeRef, computed, watch } from 'vue'
 import {
 import {
-  MaybeRefOrGetter,
   toValue,
   toValue,
   unrefElement,
   unrefElement,
+  type MaybeRefOrGetter,
   useDevicePixelRatio,
   useDevicePixelRatio,
-  useElementSize,
-  useWindowSize,
 } from '@vueuse/core'
 } from '@vueuse/core'
-import {
-  WebGLRendererParameters,
-  NoToneMapping,
-  LinearSRGBColorSpace,
-  WebGLRenderer,
-  ShadowMapType,
-  PCFShadowMap,
-  Clock,
-  ColorSpace,
-} from 'three'
-import type { ToneMapping } from 'three'
+
+import { get, merge, set } from '../../utils'
+import { useLogger } from '../useLogger'
+import { TresColor } from '../../types'
 import { useRenderLoop } from '../useRenderLoop'
 import { useRenderLoop } from '../useRenderLoop'
-import { useTres } from '../useTres'
 import { normalizeColor } from '../../utils/normalize'
 import { normalizeColor } from '../../utils/normalize'
-import { TresColor } from '../../types'
-import { rendererPresets, RendererPresetsType } from './const'
-import { merge } from '../../utils'
-import { useLogger } from '../useLogger'
 
 
-export interface UseRendererOptions extends WebGLRendererParameters {
+import type { Scene, ToneMapping } from 'three'
+import type { TresContext } from '../useTresContextProvider'
+import type {
+  ColorSpace,
+  ShadowMapType,
+  WebGLRendererParameters,
+} from 'three'
+
+type TransformToMaybeRefOrGetter<T> = {
+  [K in keyof T]: MaybeRefOrGetter<T[K]> | MaybeRefOrGetter<T[K]>;
+};
+
+export interface UseRendererOptions extends TransformToMaybeRefOrGetter<WebGLRendererParameters> {
   /**
   /**
    * Enable shadows in the Renderer
    * Enable shadows in the Renderer
    *
    *
@@ -69,7 +68,10 @@ export interface UseRendererOptions extends WebGLRendererParameters {
 
 
   /**
   /**
    * Defines the tone mapping used by the renderer.
    * Defines the tone mapping used by the renderer.
-   * Can be NoToneMapping, LinearToneMapping, ReinhardToneMapping, Uncharted2ToneMapping, CineonToneMapping, ACESFilmicToneMapping, CustomToneMapping
+   * Can be NoToneMapping, LinearToneMapping,
+   * ReinhardToneMapping, Uncharted2ToneMapping,
+   * CineonToneMapping, ACESFilmicToneMapping,
+   * CustomToneMapping
    *
    *
    * @default NoToneMapping
    * @default NoToneMapping
    */
    */
@@ -82,28 +84,6 @@ export interface UseRendererOptions extends WebGLRendererParameters {
    */
    */
   toneMappingExposure?: MaybeRefOrGetter<number>
   toneMappingExposure?: MaybeRefOrGetter<number>
 
 
-  /**
-   * The context used by the renderer.
-   *
-   * @default undefined
-   */
-  context?: WebGLRenderingContext | undefined
-
-  /**
-   * 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 "default"
-   */
-  powerPreference?: 'high-performance' | 'low-power' | 'default'
-
-  /**
-   * Whether to preserve the buffers until manually cleared or overwritten.
-   *
-   * @default false
-   */
-  preserveDrawingBuffer?: boolean
-
   /**
   /**
    * The color value to use when clearing the canvas.
    * The color value to use when clearing the canvas.
    *
    *
@@ -111,7 +91,7 @@ export interface UseRendererOptions extends WebGLRendererParameters {
    */
    */
   clearColor?: MaybeRefOrGetter<TresColor>
   clearColor?: MaybeRefOrGetter<TresColor>
   windowSize?: MaybeRefOrGetter<boolean | string>
   windowSize?: MaybeRefOrGetter<boolean | string>
-  preset?: RendererPresetsType
+  preset?: MaybeRefOrGetter<RendererPresetsType>
 }
 }
 
 
 /**
 /**
@@ -120,157 +100,158 @@ export interface UseRendererOptions extends WebGLRendererParameters {
  * @param canvas
  * @param canvas
  * @param {UseRendererOptions} [options]
  * @param {UseRendererOptions} [options]
  */
  */
-export function useRenderer(options: UseRendererOptions) {
-  const renderer = shallowRef<WebGLRenderer>()
-  const isReady = ref(false)
-  // Defaults
-  const {
-    alpha = true,
-    antialias = true,
-    depth,
-    logarithmicDepthBuffer,
-    failIfMajorPerformanceCaveat,
-    precision,
-    premultipliedAlpha,
-    stencil,
-    shadows = false,
-    shadowMapType = PCFShadowMap,
-    useLegacyLights = false,
-    outputColorSpace = LinearSRGBColorSpace,
-    toneMapping = NoToneMapping,
-    toneMappingExposure = 1,
-    context = undefined,
-    powerPreference = 'default',
-    preserveDrawingBuffer = false,
-    clearColor,
-    windowSize = false,
-    preset = undefined,
-  } = toRefs(options)
-
-  const { state, setState } = useTres()
-
-  const { width, height } =
-    toValue(windowSize) == true || toValue(windowSize) === '' || toValue(windowSize) === 'true'
-      ? useWindowSize()
-      : useElementSize(state.container)
-  const { logError, logWarning } = useLogger()
+export function useRenderer(
+  {
+    scene,
+    canvas,
+    options,
+    disableRender,
+    contextParts: { sizes, camera },
+  }:
+    {
+      canvas: MaybeRef<HTMLCanvasElement>
+      scene: Scene
+      options: UseRendererOptions
+      contextParts: Pick<TresContext, 'sizes' | 'camera'>
+      disableRender: MaybeRefOrGetter<boolean>
+    }
+) {
+
+  const webGLRendererConstructorParameters = computed<WebGLRendererParameters>(() => ({
+    alpha: toValue(options.alpha),
+    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),
+    precision: toValue(options.precision),
+    powerPreference: toValue(options.powerPreference),
+    premultipliedAlpha: toValue(options.premultipliedAlpha),
+    preserveDrawingBuffer: toValue(options.preserveDrawingBuffer),
+    logarithmicDepthBuffer: toValue(options.logarithmicDepthBuffer),
+    failIfMajorPerformanceCaveat: toValue(options.failIfMajorPerformanceCaveat)
+  }))
+
+  const renderer = shallowRef<WebGLRenderer>(new WebGLRenderer(webGLRendererConstructorParameters.value))
+
+  // 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)
+  })
+
+  watchEffect(() => {
+    renderer.value.setSize(sizes.width.value, sizes.height.value)
+  })
+
+
   const { pixelRatio } = useDevicePixelRatio()
   const { pixelRatio } = useDevicePixelRatio()
-  const { pause, resume } = useRenderLoop()
-  const aspectRatio = computed(() => width.value / height.value)
 
 
-  setTimeout(() => {
-    if (!toValue(windowSize) && !state.canvas?.value.offsetHeight) {
-      logWarning(`Oops... Seems like your canvas height is currently 0px, it's posible that you couldn't watch your scene.
-  You could set windowSize=true to force the canvas to be the size of the window.`)
-    }
-  }, 1000)
+  watchEffect(() => {
+    renderer.value.setPixelRatio(pixelRatio.value)
+  })
+
+  const { logError } = useLogger()
 
 
-  const updateRendererSize = () => {
-    if (!renderer.value) {
-      return
+  const getThreeRendererDefaults = () => {
+
+    const plainRenderer = new WebGLRenderer()
+
+    const defaults = {
+
+      shadowMap: {
+        enabled: plainRenderer.shadowMap.enabled,
+        type: plainRenderer.shadowMap.type,
+      },
+      toneMapping: plainRenderer.toneMapping,
+      toneMappingExposure: plainRenderer.toneMappingExposure,
+      outputColorSpace: plainRenderer.outputColorSpace,
+      useLegacyLights: plainRenderer.useLegacyLights
     }
     }
+    plainRenderer.dispose()
 
 
-    renderer.value.setSize(width.value, height.value)
-    renderer.value.setPixelRatio(Math.min(pixelRatio.value, 2))
+    return defaults
   }
   }
 
 
-  const updateRendererOptions = () => {
-    if (!renderer.value) {
-      return
-    }
+  const threeDefaults = getThreeRendererDefaults()
 
 
-    const rendererPreset = toValue(preset)
+  watchEffect(() => {
+    const rendererPreset = toValue(options.preset)
 
 
     if (rendererPreset) {
     if (rendererPreset) {
       if (!(rendererPreset in rendererPresets))
       if (!(rendererPreset in rendererPresets))
         logError('Renderer Preset must be one of these: ' + Object.keys(rendererPresets).join(', '))
         logError('Renderer Preset must be one of these: ' + Object.keys(rendererPresets).join(', '))
-      merge(renderer.value, rendererPresets[rendererPreset])
 
 
-      return
+      merge(renderer.value, rendererPresets[rendererPreset])
     }
     }
 
 
-    renderer.value.shadowMap.enabled = toValue(shadows) as boolean
-    renderer.value.shadowMap.type = toValue(shadowMapType) as ShadowMapType
-    renderer.value.toneMapping = (toValue(toneMapping) as ToneMapping) || NoToneMapping
-    renderer.value.toneMappingExposure = toValue(toneMappingExposure) as number
-    // Wating for https://github.com/DefinitelyTyped/DefinitelyTyped/pull/65356/files to be merged
-    renderer.value.outputColorSpace = toValue(outputColorSpace as ColorSpace) || LinearSRGBColorSpace
-    if (clearColor?.value) renderer.value.setClearColor(normalizeColor(toValue(clearColor) as TresColor))
+    const getValue = <T>(option: MaybeRefOrGetter<T>, pathInThree: string): T | undefined => {
+      const value = toValue(option)
 
 
-    /*    renderer.value.physicallyCorrectLights = toValue(physicallyCorrectLights) as boolean */
-    renderer.value.useLegacyLights = toValue(useLegacyLights) as boolean
-  }
+      const getValueFromPreset = () => {
+        if (!rendererPreset)
+          return
 
 
-  const init = () => {
-    const _canvas = unrefElement(state.canvas)
+        return get(rendererPresets[rendererPreset], pathInThree)
+      }
 
 
-    if (!_canvas) {
-      return
-    }
 
 
-    renderer.value = new WebGLRenderer({
-      canvas: _canvas,
-      alpha: toValue(alpha),
-      antialias: toValue(antialias),
-      context: toValue(context),
-      depth: toValue(depth),
-      failIfMajorPerformanceCaveat: toValue(failIfMajorPerformanceCaveat),
-      logarithmicDepthBuffer: toValue(logarithmicDepthBuffer),
-      powerPreference: toValue(powerPreference),
-      precision: toValue(precision),
-      stencil: toValue(stencil),
-      preserveDrawingBuffer: toValue(preserveDrawingBuffer),
-      premultipliedAlpha: toValue(premultipliedAlpha),
-    })
-
-    setState('renderer', renderer.value)
-    setState('clock', new Clock())
-    setState('aspectRatio', aspectRatio)
-    updateRendererOptions()
-    updateRendererSize()
-    resume()
-
-    isReady.value = true
-  }
+      if (value !== undefined)
+        return value
+
+      const valueInPreset = getValueFromPreset() as T
 
 
-  const dispose = () => {
-    if (!renderer.value) {
-      return
+      if (valueInPreset !== undefined)
+        return valueInPreset
+
+      return get(threeDefaults, pathInThree)
     }
     }
 
 
-    renderer.value.dispose()
-    renderer.value = undefined
+    const setValueOrDefault = <T>(option: MaybeRefOrGetter<T>, pathInThree: string) =>
+      set(renderer.value, pathInThree, getValue(option, pathInThree))
 
 
-    isReady.value = false
-    pause()
-  }
+    setValueOrDefault(options.shadows, 'shadowMap.enabled')
+    setValueOrDefault(options.toneMapping, 'toneMapping')
+    setValueOrDefault(options.shadowMapType, 'shadowMap.type')
+    setValueOrDefault(options.useLegacyLights, 'useLegacyLights')
+    setValueOrDefault(options.outputColorSpace, 'outputColorSpace')
+    setValueOrDefault(options.toneMappingExposure, 'toneMappingExposure')
 
 
-  watch([aspectRatio, pixelRatio], updateRendererSize)
+    const clearColor = getValue(options.clearColor, 'clearColor')
 
 
-  watch(
-    [shadows, shadowMapType, outputColorSpace, useLegacyLights, toneMapping, toneMappingExposure, clearColor],
-    updateRendererOptions,
-  )
+    if (clearColor)
+      renderer.value.setClearColor(
+        clearColor ?
+          normalizeColor(clearColor) :
+          new Color(0x000000) // default clear color is not easily/efficiently retrievable from three
+      )
 
 
-  watch(
-    () => [state.canvas, state.container],
-    () => {
-      if (unrefElement(state.canvas) && unrefElement(state.container)) {
-        init()
-      }
-    },
-    { immediate: true, deep: true },
-  )
+  })
 
 
-  if (import.meta.hot) {
+  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()
+  })
+
+  if (import.meta.hot)
     import.meta.hot.on('vite:afterUpdate', resume)
     import.meta.hot.on('vite:afterUpdate', resume)
-  }
+
 
 
   return {
   return {
     renderer,
     renderer,
-    isReady,
-    dispose,
-    aspectRatio,
   }
   }
 }
 }
 
 

+ 0 - 178
src/composables/useTres/index.ts

@@ -1,178 +0,0 @@
-import { Clock, EventDispatcher, Raycaster, Scene, Vector2, WebGLRenderer } from 'three'
-import { generateUUID } from 'three/src/math/MathUtils'
-import { ComputedRef, inject, provide, Ref, shallowReactive, toRefs } from 'vue'
-import { Camera } from '../useCamera'
-import type { usePointerEventHandler } from '../usePointerEventHandler'
-
-export interface TresState {
-  /**
-   * The active camera used for rendering the scene.
-   *
-   * @see https://threejs.org/docs/index.html?q=camera#api/en/cameras/Camera
-   *
-   * @type {Camera}
-   * @memberof TresState
-   */
-  camera?: Camera
-  /**
-   * All cameras available in the scene.
-   *
-   * @see https://threejs.org/docs/index.html?q=camera#api/en/cameras/Camera
-   *
-   * @type {Camera[]}
-   * @memberof TresState
-   */
-  cameras?: Camera[]
-  /**
-   * The aspect ratio of the scene.
-   *
-   * @type {ComputedRef<number>}
-   * @memberof TresState
-   */
-  aspectRatio?: ComputedRef<number>
-  /**
-   * The WebGLRenderer used to display the scene using WebGL.
-   *
-   * @see https://threejs.org/docs/index.html?q=webglren#api/en/renderers/WebGLRenderer
-   *
-   * @type {WebGLRenderer}
-   * @memberof TresState
-   */
-  renderer?: WebGLRenderer
-  /**
-   * The scene. This is the place where you place objects, lights and cameras.
-   *
-   * @see https://threejs.org/docs/index.html?q=scene#api/en/scenes/Scene
-   *
-   * @type {Scene}
-   * @memberof TresState
-   */
-  scene?: Scene
-  /**
-   * The raycaster.
-   *
-   * @see https://threejs.org/docs/index.html?q=raycas#api/en/core/Raycaster
-   *
-   * @type {Raycaster}
-   * @memberof TresState
-   */
-  raycaster?: Raycaster
-
-  /**
-   * Object for keeping track of time. This uses `performance.now` if it is available,
-   * otherwise it reverts to the less accurate `Date.now`.
-   *
-   * @see https://threejs.org/docs/index.html?q=clock#api/en/core/Clock
-   *
-   * @type {Clock}
-   * @memberof TresState
-   */
-  clock?: Clock
-  /**
-   * The current mouse position.
-   *
-   * @type {Vector2}
-   * @memberof TresState
-   */
-  pointer?: Vector2
-  /**
-   * The current instance of the component.
-   *
-   * @type {*}
-   * @memberof TresState
-   */
-  currentInstance?: any
-  /**
-   *  The current active scene control
-   *
-   * @type {((EventDispatcher & { enabled: boolean }) | null)}
-   * @memberof TresState
-   */
-  controls?: (EventDispatcher & { enabled: boolean }) | null
-
-  canvas?: Ref<HTMLElement>
-
-  /**
-   * The entity that handles pointer events
-   * @type {ReturnType<typeof usePointerEventHandler>}
-   * @memberof TresState
-   */
-  pointerEventHandler?: ReturnType<typeof usePointerEventHandler>
-  [key: string]: any
-}
-
-export type UseTresReturn = {
-  state: TresState
-  getState: (key: string) => void
-  setState: (key: string, value: any) => void
-  aspectRatio: ComputedRef<number>
-}
-
-export const TRES_CONTEXT_KEY = Symbol()
-
-/**
- * The Tres state.
- *
- * @see https://threejs.org/docs/index.html?q=scene#api/en/scenes/Scene
- *
- * @export
- * @return {*} {TresState, getState, setState}
- */
-export function useTresProvider() {
-  const state: TresState = shallowReactive({
-    uuid: generateUUID(),
-    camera: undefined,
-    cameras: [],
-    canvas: undefined,
-    scene: undefined,
-    renderer: undefined,
-    aspectRatio: undefined,
-    pointerEventHandler: undefined,
-  })
-  /**
-   * Get a state value.
-   *
-   *
-   * @param {string} key
-   * @return {*}
-   */
-  function getState(key: string) {
-    return state[key]
-  }
-
-  /**
-   * Set a state value.
-   *
-   * @param {string} key
-   * @param {*} value
-   */
-  function setState(key: string, value: any) {
-    state[key] = value
-  }
-
-  const toProvide = {
-    state,
-    ...toRefs(state),
-    getState,
-    setState,
-  }
-
-  provide(TRES_CONTEXT_KEY, toProvide)
-
-  return toProvide
-}
-
-export const useTres = () => {
-  const context = inject<Partial<UseTresReturn>>(TRES_CONTEXT_KEY, {
-    state: shallowReactive({
-      camera: undefined,
-      cameras: [],
-      canvas: undefined,
-      scene: undefined,
-      renderer: undefined,
-      pointerEventHandler: undefined,
-    }),
-  })
-
-  return context as UseTresReturn
-}

+ 0 - 16
src/composables/useTres/useTres.test.ts

@@ -1,16 +0,0 @@
-import { useTresProvider } from '.'
-import { useTres } from '.'
-import { withSetup } from '../../utils/test-utils'
-
-describe.skip('useTres', () => {
-  it('should set the state', () => {
-    const { state, setState } = useTres()
-    setState('foo', 'bar')
-    expect(state.foo).toBe('bar')
-  })
-  it('should get the state', () => {
-    const { setState, getState } = useTres()
-    setState('foo', 'bar')
-    expect(getState('foo')).toBe('bar')
-  })
-})

+ 98 - 0
src/composables/useTresContextProvider/index.ts

@@ -0,0 +1,98 @@
+import { toValue, useElementSize, useWindowSize } from '@vueuse/core';
+import { inject, provide, readonly, shallowRef, computed } from 'vue';
+import { useCamera } from '../useCamera';
+import { Camera, Raycaster, Scene, WebGLRenderer } from 'three';
+import { UseRendererOptions, useRenderer } from '../useRenderer';
+
+import type { ComputedRef, DeepReadonly, MaybeRef, MaybeRefOrGetter, Ref, ShallowRef } from 'vue';
+
+export type TresContext = {
+  scene: ShallowRef<Scene>;
+  camera: ComputedRef<Camera | undefined>;
+  cameras: DeepReadonly<Ref<Camera[]>>;
+  renderer: ShallowRef<WebGLRenderer>
+  raycaster: ShallowRef<Raycaster>
+  addCamera: (camera: Camera) => void;
+  removeCamera: (camera: Camera) => void
+  setCameraActive: (cameraOrUuid: Camera | string) => void;
+
+  sizes: { height: Ref<number>, width: Ref<number>, aspectRatio: ComputedRef<number> }
+}
+
+export function useTresContextProvider({
+  scene,
+  canvas,
+  windowSize,
+  disableRender,
+  rendererOptions
+}: {
+  scene: Scene,
+  canvas: MaybeRef<HTMLCanvasElement>
+  windowSize: MaybeRefOrGetter<boolean>
+  disableRender: MaybeRefOrGetter<boolean>
+  rendererOptions: UseRendererOptions
+}): TresContext {
+
+  const elementSize = computed(() =>
+    toValue(windowSize)
+      ? useWindowSize()
+      : useElementSize(toValue(canvas).parentElement)
+  )
+
+  const width = computed(() => elementSize.value.width.value)
+  const height = computed(() => elementSize.value.height.value)
+
+
+  const aspectRatio = computed(() => width.value / height.value)
+
+  const sizes = {
+    height,
+    width,
+    aspectRatio
+  }
+  const localScene = shallowRef<Scene>(scene);
+  const {
+    camera,
+    cameras,
+    addCamera,
+    removeCamera,
+    setCameraActive,
+  } = useCamera({ sizes, scene });
+
+  const { renderer } = useRenderer(
+    {
+      scene,
+      canvas,
+      options: rendererOptions,
+      contextParts: { sizes, camera },
+      disableRender,
+    })
+
+  const toProvide: TresContext = {
+    sizes,
+    scene: localScene,
+    camera,
+    cameras: readonly(cameras),
+    renderer,
+    raycaster: shallowRef(new Raycaster()),
+    addCamera,
+    removeCamera,
+    setCameraActive,
+  }
+
+  provide('useTres', toProvide);
+
+  return toProvide;
+}
+
+export function useTresContext(): TresContext {
+  const context = inject<Partial<TresContext>>('useTres');
+
+  if (!context) {
+    throw new Error('useTresContext must be used together with useTresContextProvider');
+  }
+
+  return context as TresContext;
+}
+
+export const useTres = useTresContext;

+ 68 - 15
src/core/nodeOps.ts

@@ -1,12 +1,12 @@
 import { RendererOptions } from 'vue'
 import { RendererOptions } from 'vue'
-import { BufferAttribute, Scene } from 'three'
+import { BufferAttribute } from 'three'
 import { isFunction } from '@alvarosabu/utils'
 import { isFunction } from '@alvarosabu/utils'
-import {  useLogger } from '../composables'
+import { useLogger } from '../composables'
 import { catalogue } from './catalogue'
 import { catalogue } from './catalogue'
-import { TresObject } from '../types'
 import { isHTMLTag, kebabToCamel } from '../utils'
 import { isHTMLTag, kebabToCamel } from '../utils'
-import { OBJECT_3D_USER_DATA_KEYS } from '../keys'
-import type { Material, BufferGeometry, Object3D } from 'three'
+
+import type { Object3D, Camera } from 'three'
+import type { TresObject, TresObject3D, TresScene } from '../types'
 
 
 const onRE = /^on[^a-z]/
 const onRE = /^on[^a-z]/
 export const isOn = (key: string) => onRE.test(key)
 export const isOn = (key: string) => onRE.test(key)
@@ -16,7 +16,7 @@ function noop(fn: string): any {
 }
 }
 
 
 let fallback: TresObject | null = null
 let fallback: TresObject | null = null
-let scene: Scene | null = null
+let scene: TresScene | null = null
 
 
 const { logError } = useLogger()
 const { logError } = useLogger()
 
 
@@ -61,17 +61,16 @@ export const nodeOps: RendererOptions<TresObject, TresObject> = {
 
 
     // determine whether the material was passed via prop to
     // determine whether the material was passed via prop to
     // prevent it's disposal when node is removed later in it's lifecycle
     // prevent it's disposal when node is removed later in it's lifecycle
-    const { GEOMETRY_VIA_PROP, MATERIAL_VIA_PROP } = OBJECT_3D_USER_DATA_KEYS
 
 
     if (instance.isObject3D) {
     if (instance.isObject3D) {
-      if (props?.material?.isMaterial) (instance as Object3D).userData[MATERIAL_VIA_PROP] = true
-      if (props?.geometry?.isBufferGeometry) (instance as Object3D).userData[GEOMETRY_VIA_PROP] = true
+      if (props?.material?.isMaterial) (instance as TresObject3D).userData.tres__materialViaProp = true
+      if (props?.geometry?.isBufferGeometry) (instance as TresObject3D).userData.tres__geometryViaProp = true
     }
     }
 
 
     return instance
     return instance
   },
   },
   insert(child, parent) {
   insert(child, parent) {
-    if (parent && parent.isScene) scene = parent as unknown as Scene
+    if (parent && parent.isScene) scene = parent as unknown as TresScene
     if (
     if (
       (child?.__vnode?.type === 'TresGroup' || child?.__vnode?.type === 'TresObject3D') &&
       (child?.__vnode?.type === 'TresGroup' || child?.__vnode?.type === 'TresObject3D') &&
       parent === null &&
       parent === null &&
@@ -85,10 +84,32 @@ export const nodeOps: RendererOptions<TresObject, TresObject> = {
 
 
     if (!parent) parent = fallback as TresObject
     if (!parent) parent = fallback as TresObject
 
 
+    if (child?.isObject3D) {
+      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)
+      }
+
+
+      if (
+        child?.onClick ||
+        child?.onPointerMove ||
+        child?.onPointerEnter ||
+        child?.onPointerLeave
+      ) {
+        if (!scene?.userData.tres__registerAtPointerEventHandler)
+          throw 'could not find tres__registerAtPointerEventHandler on scene\'s userData'
+
+        scene?.userData.tres__registerAtPointerEventHandler?.(child as Object3D)
+      }
+    }
+
+
     if (child?.isObject3D && parent?.isObject3D) {
     if (child?.isObject3D && parent?.isObject3D) {
       parent.add(child)
       parent.add(child)
       child.dispatchEvent({ type: 'added' })
       child.dispatchEvent({ type: 'added' })
-      scene?.userData?.[OBJECT_3D_USER_DATA_KEYS.REGISTER_AT_POINTER_EVENT_HANDLER]?.(child)
     } else if (child?.isFog) {
     } else if (child?.isFog) {
       parent.fog = child
       parent.fog = child
     } else if (typeof child?.attach === 'string') {
     } else if (typeof child?.attach === 'string') {
@@ -106,18 +127,50 @@ export const nodeOps: RendererOptions<TresObject, TresObject> = {
       const object3D = node as unknown as Object3D
       const object3D = node as unknown as Object3D
 
 
       const disposeMaterialsAndGeometries = (object3D: Object3D) => {
       const disposeMaterialsAndGeometries = (object3D: Object3D) => {
-        const { GEOMETRY_VIA_PROP, MATERIAL_VIA_PROP } = OBJECT_3D_USER_DATA_KEYS
+        const tresObject3D = object3D as TresObject3D
+
+        if (!object3D.userData.tres__materialViaProp) tresObject3D.material?.dispose()
+        if (!object3D.userData.tres__geometryViaProp)
+          tresObject3D.geometry?.dispose()
+      }
+
+      const deregisterAtPointerEventHandler = scene?.userData.tres__deregisterAtPointerEventHandler
+
+
+      const deregisterAtPointerEventHandlerIfRequired = (object: TresObject) => {
+        if (!deregisterAtPointerEventHandler)
+          throw 'could not find tres__deregisterAtPointerEventHandler on scene\'s userData'
+
+        if (
+          object?.onClick ||
+          object?.onPointerMove ||
+          object?.onPointerEnter ||
+          object?.onPointerLeave
+        )
+          deregisterAtPointerEventHandler?.(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'
+
 
 
-        if (!object3D.userData[MATERIAL_VIA_PROP]) (object3D as Object3D & { material: Material }).material?.dispose()
-        if (!object3D.userData[GEOMETRY_VIA_PROP])
-          (object3D as Object3D & { geometry: BufferGeometry }).geometry?.dispose()
+        if ((object as Camera).isCamera)
+          deregisterCamera?.(object as Camera)
       }
       }
 
 
       object3D.traverse((child: Object3D) => {
       object3D.traverse((child: Object3D) => {
         disposeMaterialsAndGeometries(child)
         disposeMaterialsAndGeometries(child)
+        deregisterCameraIfRequired(child)
+        deregisterAtPointerEventHandlerIfRequired?.(child as TresObject)
       })
       })
 
 
       disposeMaterialsAndGeometries(object3D)
       disposeMaterialsAndGeometries(object3D)
+      deregisterCameraIfRequired(object3D)
+      deregisterAtPointerEventHandlerIfRequired?.(object3D as TresObject)
     }
     }
 
 
     node.removeFromParent?.()
     node.removeFromParent?.()

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

@@ -57,7 +57,7 @@ describe('nodeOps', () => {
 
 
     // Spy
     // Spy
     const consoleWarnSpy = vi.spyOn(console, 'warn')
     const consoleWarnSpy = vi.spyOn(console, 'warn')
-    consoleWarnSpy.mockImplementation(() => {})
+    consoleWarnSpy.mockImplementation(() => { })
 
 
     // Test
     // Test
     const instance = nodeOps.createElement(tag, false, null, props)
     const instance = nodeOps.createElement(tag, false, null, props)
@@ -111,7 +111,7 @@ describe('nodeOps', () => {
     expect(parent.children.includes(child)).toBeTruthy()
     expect(parent.children.includes(child)).toBeTruthy()
   })
   })
 
 
-  it('remove: removes child from parent', async () => {
+  it.skip('remove: removes child from parent', async () => {
     // Setup
     // Setup
     const parent = new Scene() as unknown as TresObject
     const parent = new Scene() as unknown as TresObject
     const child = new Mesh() as unknown as TresObject
     const child = new Mesh() as unknown as TresObject

+ 8 - 1
src/index.ts

@@ -4,6 +4,7 @@ export * from './composables'
 export * from './core/catalogue'
 export * from './core/catalogue'
 export * from './components'
 export * from './components'
 export * from './types'
 export * from './types'
+import { useTresContext, type TresContext } from './composables'
 
 
 import { normalizeColor, normalizeVectorFlexibleParam } from './utils/normalize'
 import { normalizeColor, normalizeVectorFlexibleParam } from './utils/normalize'
 import templateCompilerOptions from './utils/template-compiler-options'
 import templateCompilerOptions from './utils/template-compiler-options'
@@ -25,4 +26,10 @@ const plugin: TresPlugin = {
 
 
 export default plugin
 export default plugin
 
 
-export { normalizeColor, normalizeVectorFlexibleParam, templateCompilerOptions }
+export {
+  TresContext,
+  useTresContext,
+  normalizeColor,
+  normalizeVectorFlexibleParam,
+  templateCompilerOptions
+}

+ 0 - 7
src/keys.ts

@@ -1,7 +0,0 @@
-export const UseTresStateSymbol = Symbol('UseTresState')
-
-export const OBJECT_3D_USER_DATA_KEYS = {
-  GEOMETRY_VIA_PROP: 'tres__geometryViaProp',
-  MATERIAL_VIA_PROP: 'tres__materialViaProp',
-  REGISTER_AT_POINTER_EVENT_HANDLER: 'tres__registerAtPointerEventHandler',
-}

+ 18 - 4
src/types/index.ts

@@ -1,7 +1,9 @@
 /* eslint-disable @typescript-eslint/ban-types */
 /* eslint-disable @typescript-eslint/ban-types */
-import type * as THREE from 'three'
 import { DefineComponent, Ref, VNode } from 'vue'
 import { DefineComponent, Ref, VNode } from 'vue'
 
 
+import type * as THREE from 'three'
+import type { EventProps as PointerEventHandlerEventProps } from '../composables/usePointerEventHandler'
+
 // Based on React Three Fiber types by Pmndrs
 // Based on React Three Fiber types by Pmndrs
 // https://github.com/pmndrs/react-three-fiber/blob/v9/packages/fiber/src/three-types.ts
 // https://github.com/pmndrs/react-three-fiber/blob/v9/packages/fiber/src/three-types.ts
 
 
@@ -40,13 +42,25 @@ export interface TresObject3D extends THREE.Object3D {
   geometry?: THREE.BufferGeometry & TresBaseObject
   geometry?: THREE.BufferGeometry & TresBaseObject
   material?: THREE.Material & TresBaseObject
   material?: THREE.Material & TresBaseObject
   userData: {
   userData: {
-    MATERIAL_VIA_PROP: boolean
-    GEOMETRY_VIA_PROP: boolean
-  } & { [key: string]: any }
+    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)
 
 
+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,
+    [key: string]: any;
+  };
+}
+
 // Events
 // Events
 export interface Intersection extends THREE.Intersection {
 export interface Intersection extends THREE.Intersection {
   /** The event source (the object which registered the handler) */
   /** The event source (the object which registered the handler) */

+ 21 - 0
src/utils/index.ts

@@ -53,3 +53,24 @@ export const uniqueBy = <T, K>(array: T[], iteratee: (value: T) => K): T[] => {
 
 
   return result
   return result
 }
 }
+
+export const get = <T>(obj: any, path: string | string[]): T | undefined => {
+  if (!path) return undefined;
+
+  // Regex explained: https://regexr.com/58j0k
+  const pathArray = Array.isArray(path) ? path : path.match(/([^[.\]])+/g);
+
+  return pathArray?.reduce((prevObj, key) => prevObj && prevObj[key], obj);
+};
+
+export const set = (obj: any, path: string | string[], value: any): void => {
+  // Regex explained: https://regexr.com/58j0k
+  const pathArray = Array.isArray(path) ? path : path.match(/([^[.\]])+/g);
+
+  if (pathArray)
+    pathArray.reduce((acc, key, i) => {
+      if (acc[key] === undefined) acc[key] = {};
+      if (i === pathArray.length - 1) acc[key] = value;
+      return acc[key];
+    }, obj);
+};

Vissa filer visades inte eftersom för många filer har ändrats