Browse Source

feat: 516 localstate for custom renderer node instances instead of userdata (#522)

* feat: conditional rendering

* chore: remove subscribe system

* feat: on-demand automatic invalidation with prop changes

* feat: invalidate once first when is `renderMode !== 'always'`

* docs: performance page, on-demand rendering

* chore: fix windowsize issue

* chore(lint): fix maximum line length issues

* feat: invalidate on-demand on window resize

* feat: add advance method for manual mode

* feat: fix manual first render with advance

* docs: performance manual mode

* docs: add badge with version

* chore: correct typos and PR suggestions

* chore: tell dont ask fix

* feat: render state instead of internal

* feat: add __tres local state to nodeOps instances

* feat: add context to root on instances localstate

* feat: camera registration ops from node local state ctx

* feat: event handling registration from localState of nodes

* feature: disposable flag on node localstate

* feat: remove userData from types

* chore: remove unused import

* fix(test): fake localstate `.__tres` on tests

* fix(types): fix nodeOps instances localstate type
Alvaro Saburido 1 year ago
parent
commit
08717efd0f

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

@@ -64,6 +64,7 @@ watchEffect(() => {
       :rotation="[-Math.PI / 2, 0, Math.PI / 2]"
       name="floor"
       receive-shadow
+      @click="wireframe = !wireframe"
     >
       <TresPlaneGeometry :args="[20, 20, 20]" />
       <TresMeshToonMaterial

+ 1 - 1
src/components/TresCanvas.vue

@@ -136,7 +136,7 @@ onMounted(() => {
     emit,
   })
 
-  usePointerEventHandler({ scene: scene.value, contextParts: context.value })
+  usePointerEventHandler(context.value)
 
   const { registerCamera, camera, cameras, deregisterCamera } = context.value
 

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

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

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

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

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

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

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

@@ -1,6 +1,6 @@
 import { toValue, useElementSize, useFps, useMemory, useRafFn, useWindowSize, refDebounced } from '@vueuse/core'
 import { inject, provide, readonly, shallowRef, computed, ref, onUnmounted, watchEffect } from 'vue'
-import type { Camera, EventDispatcher, Scene, WebGLRenderer } from 'three'
+import type { Camera, EventDispatcher, Object3D, WebGLRenderer } from 'three'
 import { Raycaster } from 'three'
 import type { ComputedRef, DeepReadonly, MaybeRef, MaybeRefOrGetter, Ref, ShallowRef } from 'vue'
 import { calculateMemoryUsage } from '../../utils/perf'
@@ -9,6 +9,8 @@ import type { UseRendererOptions } from '../useRenderer'
 import { useRenderer } from '../useRenderer'
 import { extend } from '../../core/catalogue'
 import { useLogger } from '../useLogger'
+import type { TresScene } from '../../types'
+import type { EventProps } from '../usePointerEventHandler'
 
 export interface InternalState {
   priority: Ref<number>
@@ -43,7 +45,7 @@ export interface PerformanceState {
 }
 
 export interface TresContext {
-  scene: ShallowRef<Scene>
+  scene: ShallowRef<TresScene>
   sizes: { height: Ref<number>; width: Ref<number>; aspectRatio: ComputedRef<number> }
   extend: (objects: any) => void
   camera: ComputedRef<Camera | undefined>
@@ -61,9 +63,17 @@ export interface TresContext {
      * Advance one frame when renderMode === 'manual'
      */
   advance: () => void
+  // Camera
   registerCamera: (camera: Camera) => void
   setCameraActive: (cameraOrUuid: Camera | string) => void
   deregisterCamera: (camera: Camera) => void
+  // Events
+  // Temporaly add the methods to the context, this should be handled later by the EventManager state on the context https://github.com/Tresjs/tres/issues/515
+  // When thats done maybe we can short the names of the methods since the parent will give the context.
+  registerObjectAtPointerEventHandler: (object: Object3D & EventProps) => void
+  deregisterObjectAtPointerEventHandler: (object: Object3D) => void
+  registerBlockingObjectAtPointerEventHandler: (object: Object3D) => void
+  deregisterBlockingObjectAtPointerEventHandler: (object: Object3D) => void
 }
 
 export function useTresContextProvider({
@@ -74,7 +84,7 @@ export function useTresContextProvider({
   rendererOptions,
   emit,
 }: {
-  scene: Scene
+  scene: TresScene
   canvas: MaybeRef<HTMLCanvasElement>
   windowSize: MaybeRefOrGetter<boolean>
   disableRender: MaybeRefOrGetter<boolean>
@@ -109,7 +119,7 @@ export function useTresContextProvider({
     width: computed(() => debouncedReactiveSize.value.width),
     aspectRatio,
   }
-  const localScene = shallowRef<Scene>(scene)
+  const localScene = shallowRef<TresScene>(scene)
   const {
     camera,
     cameras,
@@ -121,7 +131,7 @@ export function useTresContextProvider({
   // Render state
 
   const render: RenderState = {
-    mode: ref<'always' | 'on-demand' | 'manual'>(rendererOptions.renderMode || 'always'),
+    mode: ref(rendererOptions.renderMode || 'always') as Ref<'always' | 'on-demand' | 'manual'>,
     priority: ref(0),
     frames: ref(0),
     maxFrames: 60,
@@ -189,8 +199,10 @@ export function useTresContextProvider({
 
   provide('useTres', ctx)
 
-  // Add context to scene.userData
-  ctx.scene.value.userData.tres__context = ctx
+  // Add context to scene local state
+  ctx.scene.value.__tres = {
+    root: ctx,
+  }
 
   // Performance
   const updateInterval = 100 // Update interval in milliseconds

+ 62 - 69
src/core/nodeOps.ts

@@ -2,9 +2,9 @@ import type { RendererOptions } from 'vue'
 import { BufferAttribute } from 'three'
 import { isFunction } from '@alvarosabu/utils'
 import type { Object3D, Camera } from 'three'
+import type { TresContext } from '../composables'
 import { useLogger } from '../composables'
 import { deepArrayEqual, isHTMLTag, kebabToCamel } from '../utils'
-
 import type { TresObject, TresObject3D, TresScene } from '../types'
 import { catalogue } from './catalogue'
 
@@ -13,7 +13,6 @@ function noop(fn: string): any {
 }
 
 let scene: TresScene | null = null
-
 const { logError } = useLogger()
 
 const supportedPointerEvents = [
@@ -24,7 +23,7 @@ const supportedPointerEvents = [
 ]
 
 export function invalidateInstance(instance: TresObject) {
-  const ctx = instance.userData.tres__root?.userData?.tres__context
+  const ctx = instance.__tres.root
   
   if (!ctx) return
   
@@ -34,8 +33,8 @@ export function invalidateInstance(instance: TresObject) {
 
 }
 
-export const nodeOps: RendererOptions<TresObject, TresObject> = {
-  createElement(tag, _isSVG, _anchor, props) {
+export const nodeOps: RendererOptions<TresObject, TresObject | null> = {
+  createElement(tag, _isSVG, _anchor, props): TresObject | null {
     if (!props) props = {}
 
     if (!props.args) {
@@ -44,13 +43,13 @@ export const nodeOps: RendererOptions<TresObject, TresObject> = {
     if (tag === 'template') return null
     if (isHTMLTag(tag)) return null
     let name = tag.replace('Tres', '')
-    let instance
+    let instance: TresObject | null
 
     if (tag === 'primitive') {
       if (props?.object === undefined) logError('Tres primitives need a prop \'object\'')
       const object = props.object as TresObject
       name = object.type
-      instance = Object.assign(object, { type: name, attach: props.attach, primitive: true })
+      instance = Object.assign(object, { type: name, attach: props.attach })
     }
     else {
       const target = catalogue.value[name]
@@ -60,6 +59,8 @@ export const nodeOps: RendererOptions<TresObject, TresObject> = {
       instance = new target(...props.args)
     }
 
+    if (!instance) return null
+
     if (instance.isCamera) {
       if (!props?.position) {
         instance.position.set(3, 3, 3)
@@ -74,49 +75,46 @@ export const nodeOps: RendererOptions<TresObject, TresObject> = {
       else if (instance.isBufferGeometry) instance.attach = 'geometry'
     }
 
+    instance.__tres = {
+      ...instance.__tres,
+      type: name,
+      memoizedProps: props,
+      eventCount: 0,
+      disposable: true,
+      primitive: tag === 'primitive',
+    }
+
     // determine whether the material was passed via prop to
     // prevent it's disposal when node is removed later in it's lifecycle
 
-    if (instance.isObject3D) {
-      if (props?.material?.isMaterial) (instance as TresObject3D).userData.tres__materialViaProp = true
-      if (props?.geometry?.isBufferGeometry) (instance as TresObject3D).userData.tres__geometryViaProp = true
-    }
-
-    // Since THREE instances properties are not consistent, (Orbit Controls doesn't have a `type` property) 
-    // we take the tag name and we save it on the userData for later use in the re-instancing process.
-    instance.userData = {
-      ...instance.userData,
-      tres__name: name,
+    if (instance.isObject3D && (props?.material || props?.geometry)) {
+      instance.__tres.disposable = false
     }
 
-    return instance
+    return instance as TresObject
   },
   insert(child, parent) {
+    if (!child) return
+    
     if (parent && parent.isScene) {
       scene = parent as unknown as TresScene
-      if (child) {
-        child.userData.tres__root = scene
-      }
     }
 
-    const parentObject = parent || scene
+    if (scene) {
+      child.__tres.root = scene.__tres.root as TresContext
+    }
 
+    const parentObject = parent || scene
+    
     if (child?.isObject3D) {
-
+      const { registerCamera, registerObjectAtPointerEventHandler } = child.__tres.root
       if (child?.isCamera) {
-        if (!scene?.userData.tres__registerCamera)
-          throw 'could not find tres__registerCamera on scene\'s userData'
-
-        scene?.userData.tres__registerCamera?.(child as unknown as Camera)
+        registerCamera(child as unknown as Camera)
       }
-
       if (
         child && supportedPointerEvents.some(eventName => child[eventName])
       ) {
-        if (!scene?.userData.tres__registerAtPointerEventHandler)
-          throw 'could not find tres__registerAtPointerEventHandler on scene\'s userData'
-
-        scene?.userData.tres__registerAtPointerEventHandler?.(child as Object3D)
+        registerObjectAtPointerEventHandler(child as Object3D)
       }
     }
 
@@ -136,65 +134,52 @@ export const nodeOps: RendererOptions<TresObject, TresObject> = {
   },
   remove(node) {
     if (!node) return
+    const ctx = node.__tres
     // remove is only called on the node being removed and not on child nodes.
+    const { 
+      deregisterObjectAtPointerEventHandler,
+      deregisterBlockingObjectAtPointerEventHandler, 
+    } = ctx.root
 
     if (node.isObject3D) {
-      const object3D = node as unknown as Object3D
 
-      const disposeMaterialsAndGeometries = (object3D: Object3D) => {
+      const disposeMaterialsAndGeometries = (object3D: TresObject) => {
         const tresObject3D = object3D as TresObject3D
-
-        if (!object3D.userData.tres__materialViaProp) {
+        // TODO: to be improved on https://github.com/Tresjs/tres/pull/466/files
+        if (ctx.disposable) {
           tresObject3D.material?.dispose()
           tresObject3D.material = undefined
-        }
-
-        if (!object3D.userData.tres__geometryViaProp) {
           tresObject3D.geometry?.dispose()
           tresObject3D.geometry = undefined
         }
       }
 
-      const deregisterAtPointerEventHandler = scene?.userData.tres__deregisterAtPointerEventHandler
-      const deregisterBlockingObjectAtPointerEventHandler
-        = scene?.userData.tres__deregisterBlockingObjectAtPointerEventHandler
-
       const deregisterAtPointerEventHandlerIfRequired = (object: TresObject) => {
-
-        if (!deregisterBlockingObjectAtPointerEventHandler)
-          throw 'could not find tres__deregisterBlockingObjectAtPointerEventHandler on scene\'s userData'
-
-        scene?.userData.tres__deregisterBlockingObjectAtPointerEventHandler?.(object as Object3D)
-
-        if (!deregisterAtPointerEventHandler)
-          throw 'could not find tres__deregisterAtPointerEventHandler on scene\'s userData'
-
+        deregisterBlockingObjectAtPointerEventHandler(object as Object3D)
         if (
           object && supportedPointerEvents.some(eventName => object[eventName])
         )
-          deregisterAtPointerEventHandler?.(object as Object3D)
+          deregisterObjectAtPointerEventHandler?.(object as Object3D)
       }
 
       const deregisterCameraIfRequired = (object: Object3D) => {
-        const deregisterCamera = scene?.userData.tres__deregisterCamera
-
-        if (!deregisterCamera)
-          throw 'could not find tres__deregisterCamera on scene\'s userData'
+        const deregisterCamera = node.__tres.root.deregisterCamera
 
         if ((object as Camera).isCamera)
           deregisterCamera?.(object as Camera)
       }
 
       node.removeFromParent?.()
-      object3D.traverse((child: Object3D) => {
-        disposeMaterialsAndGeometries(child)
+
+      node.traverse((child: Object3D) => {
+        disposeMaterialsAndGeometries(child as TresObject)
         deregisterCameraIfRequired(child)
         deregisterAtPointerEventHandlerIfRequired?.(child as TresObject)
       })
 
-      disposeMaterialsAndGeometries(object3D)
-      deregisterCameraIfRequired(object3D)
-      deregisterAtPointerEventHandlerIfRequired?.(object3D as TresObject)
+      disposeMaterialsAndGeometries(node)
+      deregisterCameraIfRequired(node as Object3D)
+      deregisterAtPointerEventHandlerIfRequired?.(node as TresObject)
     }
 
     invalidateInstance(node as TresObject)
@@ -204,13 +189,21 @@ export const nodeOps: RendererOptions<TresObject, TresObject> = {
     if (node) {
       let root = node
       let key = prop
-      if (node.isObject3D && key === 'blocks-pointer-events') {
-        if (nextValue || nextValue === '')
-          scene?.userData.tres__registerBlockingObjectAtPointerEventHandler?.(node as Object3D)
-        else
-          scene?.userData.tres__deregisterBlockingObjectAtPointerEventHandler?.(node as Object3D)
 
-        return
+      if (node.__tres.root) {
+        const { 
+          registerBlockingObjectAtPointerEventHandler,
+          deregisterBlockingObjectAtPointerEventHandler, 
+        } = node.__tres.root
+  
+        if (node.isObject3D && key === 'blocks-pointer-events') {
+          if (nextValue || nextValue === '')
+            registerBlockingObjectAtPointerEventHandler(node as Object3D)
+          else
+            deregisterBlockingObjectAtPointerEventHandler(node as Object3D)
+  
+          return
+        }
       }
 
       let finalKey = kebabToCamel(key)
@@ -220,7 +213,7 @@ export const nodeOps: RendererOptions<TresObject, TresObject> = {
         const prevNode = node as TresObject3D
         const prevArgs = _prevValue ?? []
         const args = nextValue ?? []
-        const instanceName = node.userData.tres__name || node.type
+        const instanceName = node.__tres.type || node.type
 
         if (instanceName && prevArgs.length && !deepArrayEqual(prevArgs, args)) {
           root = Object.assign(prevNode, new catalogue.value[instanceName](...nextValue))

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

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

+ 18 - 16
src/types/index.ts

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