Skip to content

Commit

Permalink
feat: 516 localstate for custom renderer node instances instead of us…
Browse files Browse the repository at this point in the history
…erdata (#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
alvarosabu authored Feb 1, 2024
1 parent c4547f9 commit 08717ef
Showing 9 changed files with 143 additions and 117 deletions.
1 change: 1 addition & 0 deletions playground/src/components/TheExperience.vue
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion src/components/TresCanvas.vue
Original file line number Diff line number Diff line change
@@ -136,7 +136,7 @@ onMounted(() => {
emit,
})
usePointerEventHandler({ scene: scene.value, contextParts: context.value })
usePointerEventHandler(context.value)
const { registerCamera, camera, cameras, deregisterCamera } = context.value
3 changes: 0 additions & 3 deletions src/composables/useCamera/index.ts
Original file line number Diff line number Diff line change
@@ -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 = []
})
24 changes: 10 additions & 14 deletions src/composables/usePointerEventHandler/index.ts
Original file line number Diff line number Diff line change
@@ -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,
}
}
10 changes: 5 additions & 5 deletions src/composables/useRaycaster/index.ts
Original file line number Diff line number Diff line change
@@ -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) => {
26 changes: 19 additions & 7 deletions src/composables/useTresContextProvider/index.ts
Original file line number Diff line number Diff line change
@@ -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
131 changes: 62 additions & 69 deletions src/core/nodeOps.ts
Original file line number Diff line number Diff line change
@@ -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))
29 changes: 27 additions & 2 deletions src/core/nodeOpts.test.ts
Original file line number Diff line number Diff line change
@@ -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

34 changes: 18 additions & 16 deletions src/types/index.ts
Original file line number Diff line number Diff line change
@@ -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
}
}

0 comments on commit 08717ef

Please sign in to comment.