浏览代码

refactor(nodeOps): remove predicates/optional chaining, use is/context (#726)

* refactor: add 'is' module/tests

* refactor: push predicates down into functions

* refactor: pass TresContext to nodeOps

* refactor: remove predicates/optional chaining, use is/context

* chore: lint

* refactor: use unknown type, not any

* fix: re-add changes from #717

* refactor: use uknown type, not any

* refactor: use uknown type, not any
andretchen0 1 年之前
父节点
当前提交
431735121c

+ 1 - 3
src/components/TresCanvas.vue

@@ -126,9 +126,7 @@ const createInternalComponent = (context: TresContext) =>
 
 
 const mountCustomRenderer = (context: TresContext) => {
 const mountCustomRenderer = (context: TresContext) => {
   const InternalComponent = createInternalComponent(context)
   const InternalComponent = createInternalComponent(context)
-
-  const { render } = createRenderer(nodeOps())
-
+  const { render } = createRenderer(nodeOps(context))
   render(h(InternalComponent), scene.value as unknown as TresObject)
   render(h(InternalComponent), scene.value as unknown as TresObject)
 }
 }
 
 

+ 13 - 6
src/composables/useCamera/index.ts

@@ -4,6 +4,7 @@ import { Camera, PerspectiveCamera } from 'three'
 
 
 import type { TresScene } from '../../types'
 import type { TresScene } from '../../types'
 import type { TresContext } from '../useTresContextProvider'
 import type { TresContext } from '../useTresContextProvider'
+import { camera as isCamera } from '../../utils/is'
 
 
 export const useCamera = ({ sizes }: Pick<TresContext, 'sizes'> & { scene: TresScene }) => {
 export const useCamera = ({ sizes }: Pick<TresContext, 'sizes'> & { scene: TresScene }) => {
   // the computed does not trigger, when for example the camera position changes
   // the computed does not trigger, when for example the camera position changes
@@ -23,15 +24,21 @@ export const useCamera = ({ sizes }: Pick<TresContext, 'sizes'> & { scene: TresS
     cameras.value = [camera, ...otherCameras]
     cameras.value = [camera, ...otherCameras]
   }
   }
 
 
-  const registerCamera = (newCamera: Camera, active = false) => {
-    if (cameras.value.some(({ uuid }) => uuid === newCamera.uuid)) { return }
+  const registerCamera = (maybeCamera: unknown, active = false) => {
+    if (isCamera(maybeCamera)) {
+      const camera = maybeCamera
+      if (cameras.value.some(({ uuid }) => uuid === camera.uuid)) { return }
 
 
-    if (active) { setCameraActive(newCamera) }
-    else { cameras.value.push(newCamera) }
+      if (active) { setCameraActive(camera) }
+      else { cameras.value.push(camera) }
+    }
   }
   }
 
 
-  const deregisterCamera = (camera: Camera) => {
-    cameras.value = cameras.value.filter(({ uuid }) => uuid !== camera.uuid)
+  const deregisterCamera = (maybeCamera: unknown) => {
+    if (isCamera(maybeCamera)) {
+      const camera = maybeCamera
+      cameras.value = cameras.value.filter(({ uuid }) => uuid !== camera.uuid)
+    }
   }
   }
 
 
   watchEffect(() => {
   watchEffect(() => {

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

@@ -70,9 +70,9 @@ export interface TresContext {
    */
    */
   advance: () => void
   advance: () => void
   // Camera
   // Camera
-  registerCamera: (camera: Camera) => void
+  registerCamera: (maybeCamera: unknown) => void
   setCameraActive: (cameraOrUuid: Camera | string) => void
   setCameraActive: (cameraOrUuid: Camera | string) => void
-  deregisterCamera: (camera: Camera) => void
+  deregisterCamera: (maybeCamera: unknown) => void
   eventManager?: TresEventManager
   eventManager?: TresEventManager
   // Events
   // 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
   // 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

+ 13 - 8
src/composables/useTresEventManager/index.ts

@@ -4,6 +4,7 @@ import type { EmitEventFn, EmitEventName, Intersection, TresEvent, TresObject }
 import type { TresContext } from '../useTresContextProvider'
 import type { TresContext } from '../useTresContextProvider'
 import { useRaycaster } from '../useRaycaster'
 import { useRaycaster } from '../useRaycaster'
 import { hyphenate } from '../../utils'
 import { hyphenate } from '../../utils'
+import * as is from '../../utils/is'
 
 
 export interface TresEventManager {
 export interface TresEventManager {
   /**
   /**
@@ -15,8 +16,8 @@ export interface TresEventManager {
    * So we need to track them separately
    * So we need to track them separately
    * Note: These are used in nodeOps
    * Note: These are used in nodeOps
    */
    */
-  registerPointerMissedObject: (object: TresObject) => void
-  deregisterPointerMissedObject: (object: TresObject) => void
+  registerPointerMissedObject: (object: unknown) => void
+  deregisterPointerMissedObject: (object: unknown) => void
 }
 }
 
 
 export function useTresEventManager(
 export function useTresEventManager(
@@ -179,14 +180,18 @@ export function useTresEventManager(
     emit('pointer-missed', { event })
     emit('pointer-missed', { event })
   })
   })
 
 
-  function registerPointerMissedObject(object: TresObject) {
-    pointerMissedObjects.push(object)
+  function registerPointerMissedObject(maybeTresObject: unknown) {
+    if (is.tresObject(maybeTresObject) && is.object3D(maybeTresObject) && maybeTresObject.onPointerMissed) {
+      pointerMissedObjects.push(maybeTresObject)
+    }
   }
   }
 
 
-  function deregisterPointerMissedObject(object: TresObject) {
-    const index = pointerMissedObjects.indexOf(object)
-    if (index > -1) {
-      pointerMissedObjects.splice(index, 1)
+  function deregisterPointerMissedObject(maybeTresObject: unknown) {
+    if (is.tresObject(maybeTresObject) && is.object3D(maybeTresObject)) {
+      const index = pointerMissedObjects.indexOf(maybeTresObject)
+      if (index > -1) {
+        pointerMissedObjects.splice(index, 1)
+      }
     }
     }
   }
   }
 
 

+ 12 - 2
src/core/nodeOps.test.ts

@@ -2,17 +2,19 @@ import { beforeAll, describe, expect, it, vi } from 'vitest'
 import * as THREE from 'three'
 import * as THREE from 'three'
 import type { Vector3 } from 'three'
 import type { Vector3 } from 'three'
 import { Mesh, Scene } from 'three'
 import { Mesh, Scene } from 'three'
+import type { TresContext } from 'src/composables'
+import { shallowRef } from 'vue'
 import type { TresObject } from '../types'
 import type { TresObject } from '../types'
 import { nodeOps as getNodeOps } from './nodeOps'
 import { nodeOps as getNodeOps } from './nodeOps'
 import { extend } from './catalogue'
 import { extend } from './catalogue'
 
 
-let nodeOps = getNodeOps()
+let nodeOps = getNodeOps(mockTresContext())
 const pool = []
 const pool = []
 
 
 describe('nodeOps', () => {
 describe('nodeOps', () => {
   beforeAll(() => {
   beforeAll(() => {
     extend(THREE)
     extend(THREE)
-    nodeOps = getNodeOps()
+    nodeOps = getNodeOps(mockTresContext())
     const ce = nodeOps.createElement
     const ce = nodeOps.createElement
     // NOTE: Overwrite createElement in order to push
     // NOTE: Overwrite createElement in order to push
     // all objects into a pool, later to be disposed.
     // all objects into a pool, later to be disposed.
@@ -458,3 +460,11 @@ function mockTresObjectRootInObject(obj) {
   }
   }
   return obj
   return obj
 }
 }
+
+function mockTresContext() {
+  return {
+    scene: shallowRef(new Scene()),
+    registerCamera: () => {},
+    deregisterCamera: () => {},
+  } as unknown as TresContext
+}

+ 113 - 134
src/core/nodeOps.ts

@@ -1,11 +1,10 @@
 import type { RendererOptions } from 'vue'
 import type { RendererOptions } from 'vue'
 import { BufferAttribute, Object3D } from 'three'
 import { BufferAttribute, Object3D } from 'three'
-import { isFunction } from '@alvarosabu/utils'
-import type { Camera } from 'three'
 import type { TresContext } from '../composables'
 import type { TresContext } from '../composables'
 import { useLogger } from '../composables'
 import { useLogger } from '../composables'
 import { deepArrayEqual, disposeObject3D, isHTMLTag, kebabToCamel } from '../utils'
 import { deepArrayEqual, disposeObject3D, isHTMLTag, kebabToCamel } from '../utils'
-import type { InstanceProps, TresObject, TresObject3D, TresScene } from '../types'
+import type { InstanceProps, TresObject, TresObject3D } from '../types'
+import * as is from '../utils/is'
 import { catalogue } from './catalogue'
 import { catalogue } from './catalogue'
 
 
 function noop(fn: string): any {
 function noop(fn: string): any {
@@ -42,8 +41,9 @@ export function invalidateInstance(instance: TresObject) {
   }
   }
 }
 }
 
 
-export const nodeOps: () => RendererOptions<TresObject, TresObject | null> = () => {
-  let scene: TresScene | null = null
+export const nodeOps: (context: TresContext) => RendererOptions<TresObject, TresObject | null> = (context) => {
+  const scene = context.scene.value
+
   function createElement(tag: string, _isSVG: undefined, _anchor: any, props: InstanceProps): TresObject | null {
   function createElement(tag: string, _isSVG: undefined, _anchor: any, props: InstanceProps): TresObject | null {
     if (!props) { props = {} }
     if (!props) { props = {} }
 
 
@@ -99,46 +99,34 @@ export const nodeOps: () => RendererOptions<TresObject, TresObject | null> = ()
 
 
     // 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
-
     if (instance.isObject3D && instance.__tres && (props?.material || props?.geometry)) {
     if (instance.isObject3D && instance.__tres && (props?.material || props?.geometry)) {
       instance.__tres.disposable = false
       instance.__tres.disposable = false
     }
     }
 
 
     return instance as TresObject
     return instance as TresObject
   }
   }
+
   function insert(child: TresObject, parent: TresObject) {
   function insert(child: TresObject, parent: TresObject) {
     if (!child) { return }
     if (!child) { return }
 
 
-    if (parent && parent.isScene) {
-      scene = parent as unknown as TresScene
-    }
-
-    if (scene && child.__tres) {
-      child.__tres.root = scene.__tres.root as TresContext
+    if (child.__tres) {
+      child.__tres.root = context
     }
     }
 
 
     const parentObject = parent || scene
     const parentObject = parent || scene
 
 
-    if (child?.isObject3D) {
-      const { registerCamera } = child?.__tres?.root as TresContext
-      if (child?.isCamera) {
-        registerCamera(child as unknown as Camera)
-      }
-
-      // Track onPointerMissed objects separate from the scene
-      if (child.onPointerMissed && child?.__tres?.root) {
-        child?.__tres?.root?.eventManager?.registerPointerMissedObject(child)
-      }
-    }
+    context.registerCamera(child)
+    // NOTE: Track onPointerMissed objects separate from the scene
+    context.eventManager?.registerPointerMissedObject(child)
 
 
-    if (child?.isObject3D && parentObject?.isObject3D) {
+    if (is.object3D(child) && is.object3D(parentObject)) {
       parentObject.add(child)
       parentObject.add(child)
       child.dispatchEvent({ type: 'added' })
       child.dispatchEvent({ type: 'added' })
     }
     }
-    else if (child?.isFog) {
+    else if (is.fog(child)) {
       parentObject.fog = child
       parentObject.fog = child
     }
     }
-    else if (typeof child?.attach === 'string') {
+    else if (typeof child.attach === 'string') {
       child.__previousAttach = child[parentObject?.attach as string]
       child.__previousAttach = child[parentObject?.attach as string]
       if (parentObject) {
       if (parentObject) {
         parentObject[child.attach] = child
         parentObject[child.attach] = child
@@ -148,31 +136,21 @@ export const nodeOps: () => RendererOptions<TresObject, TresObject | null> = ()
 
 
   function remove(node: TresObject | null) {
   function remove(node: TresObject | null) {
     if (!node) { return }
     if (!node) { return }
-    const ctx = node.__tres
     // remove is only called on the node being removed and not on child nodes.
     // remove is only called on the node being removed and not on child nodes.
     node.parent = node.parent || scene
     node.parent = node.parent || scene
 
 
-    if (node.isObject3D) {
-      const deregisterCameraIfRequired = (object: TresObject) => {
-        const deregisterCamera = node?.__tres?.root?.deregisterCamera
-
-        if ((object as unknown as Camera).isCamera) { deregisterCamera?.(object as unknown as Camera) }
-      }
-
+    if (is.object3D(node)) {
       node.removeFromParent?.()
       node.removeFromParent?.()
 
 
       // Remove nested child objects. Primitives should not have objects and children that are
       // Remove nested child objects. Primitives should not have objects and children that are
       // attached to them declaratively ...
       // attached to them declaratively ...
-
-      node.traverse((child: TresObject) => {
-        deregisterCameraIfRequired(child)
+      node.traverse((child) => {
+        context.deregisterCamera(child)
         // deregisterAtPointerEventHandlerIfRequired?.(child as TresObject)
         // deregisterAtPointerEventHandlerIfRequired?.(child as TresObject)
-        if (child.onPointerMissed) {
-          ctx?.root?.eventManager?.deregisterPointerMissedObject(child)
-        }
+        context.eventManager?.deregisterPointerMissedObject(child)
       })
       })
 
 
-      deregisterCameraIfRequired(node)
+      context.deregisterCamera(node)
       /*  deregisterAtPointerEventHandlerIfRequired?.(node as TresObject) */
       /*  deregisterAtPointerEventHandlerIfRequired?.(node as TresObject) */
       invalidateInstance(node as TresObject)
       invalidateInstance(node as TresObject)
 
 
@@ -186,111 +164,112 @@ export const nodeOps: () => RendererOptions<TresObject, TresObject | null> = ()
       node.dispose?.()
       node.dispose?.()
     }
     }
   }
   }
+
   function patchProp(node: TresObject, prop: string, prevValue: any, nextValue: any) {
   function patchProp(node: TresObject, prop: string, prevValue: any, nextValue: any) {
-    if (node) {
-      let root = node
-      let key = prop
-      if (node?.__tres?.primitive && key === 'object' && prevValue !== null) {
-        // If the prop 'object' is changed, we need to re-instance the object and swap the old one with the new one
-        const newInstance = createElement('primitive', undefined, undefined, {
-          object: nextValue,
-        })
-        for (const subkey in newInstance) {
-          if (subkey === 'uuid') { continue }
-          const target = node[subkey]
-          const value = newInstance[subkey]
-          if (!target?.set && !isFunction(target)) { node[subkey] = value }
-          else if (target.constructor === value.constructor && target?.copy) { target?.copy(value) }
-          else if (Array.isArray(value)) { target.set(...value) }
-          else if (!target.isColor && target.setScalar) { target.setScalar(value) }
-          else { target.set(value) }
-        }
-        if (newInstance?.__tres) {
-          newInstance.__tres.root = scene?.__tres.root
-        }
-        // This code is needed to handle the case where the prop 'object' type change from a group to a mesh or vice versa, otherwise the object will not be rendered correctly (models will be invisible)
-        if (newInstance?.isGroup) {
-          node.geometry = undefined
-          node.material = undefined
-        }
-        else {
-          delete node.isGroup
-        }
-      }
+    if (!node) { return }
 
 
-      if (node?.isObject3D && key === 'blocks-pointer-events') {
-        if (nextValue || nextValue === '') { node[key] = nextValue }
-        else { delete node[key] }
-        return
+    let root = node
+    let key = prop
+    if (node.__tres?.primitive && key === 'object' && prevValue !== null) {
+      // If the prop 'object' is changed, we need to re-instance the object and swap the old one with the new one
+      const newInstance = createElement('primitive', undefined, undefined, {
+        object: nextValue,
+      })
+      for (const subkey in newInstance) {
+        if (subkey === 'uuid') { continue }
+        const target = node[subkey]
+        const value = newInstance[subkey]
+        if (!target?.set && !is.fun(target)) { node[subkey] = value }
+        else if (target.constructor === value.constructor && target?.copy) { target?.copy(value) }
+        else if (Array.isArray(value)) { target.set(...value) }
+        else if (!target.isColor && target.setScalar) { target.setScalar(value) }
+        else { target.set(value) }
+      }
+      if (newInstance?.__tres) {
+        newInstance.__tres.root = context
       }
       }
-      // Has events
-      if (supportedPointerEvents.includes(prop)) {
-        node.__tres.eventCount += 1
+      // This code is needed to handle the case where the prop 'object' type change from a group to a mesh or vice versa, otherwise the object will not be rendered correctly (models will be invisible)
+      if (newInstance?.isGroup) {
+        node.geometry = undefined
+        node.material = undefined
       }
       }
-      let finalKey = kebabToCamel(key)
-      let target = root?.[finalKey]
-
-      if (key === 'args') {
-        const prevNode = node as TresObject3D
-        const prevArgs = prevValue ?? []
-        const args = nextValue ?? []
-        const instanceName = node?.__tres?.type || node.type
-
-        if (
-          instanceName
-          && prevArgs.length
-          && !deepArrayEqual(prevArgs, args)
-        ) {
-          root = Object.assign(
-            prevNode,
-            new catalogue.value[instanceName](...nextValue),
-          )
-        }
-        return
+      else {
+        delete node.isGroup
       }
       }
+    }
 
 
-      if (root.type === 'BufferGeometry') {
-        if (key === 'args') { return }
-        root.setAttribute(
-          kebabToCamel(key),
-          new BufferAttribute(...(nextValue as ConstructorParameters<typeof BufferAttribute>)),
+    if (is.object3D(node) && key === 'blocks-pointer-events') {
+      if (nextValue || nextValue === '') { node[key] = nextValue }
+      else { delete node[key] }
+      return
+    }
+    // Has events
+    if (supportedPointerEvents.includes(prop)) {
+      node.__tres.eventCount += 1
+    }
+    let finalKey = kebabToCamel(key)
+    let target = root?.[finalKey]
+
+    if (key === 'args') {
+      const prevNode = node as TresObject3D
+      const prevArgs = prevValue ?? []
+      const args = nextValue ?? []
+      const instanceName = node.__tres?.type || node.type
+
+      if (
+        instanceName
+        && prevArgs.length
+        && !deepArrayEqual(prevArgs, args)
+      ) {
+        root = Object.assign(
+          prevNode,
+          new catalogue.value[instanceName](...nextValue),
         )
         )
-        return
       }
       }
+      return
+    }
+
+    if (root.type === 'BufferGeometry') {
+      if (key === 'args') { return }
+      root.setAttribute(
+        kebabToCamel(key),
+        new BufferAttribute(...(nextValue as ConstructorParameters<typeof BufferAttribute>)),
+      )
+      return
+    }
 
 
-      // Traverse pierced props (e.g. foo-bar=value => foo.bar = value)
-      if (key.includes('-') && target === undefined) {
-        const chain = key.split('-')
-        target = chain.reduce((acc, key) => acc[kebabToCamel(key)], root)
-        key = chain.pop() as string
-        finalKey = key
-        if (!target?.set) { root = chain.reduce((acc, key) => acc[kebabToCamel(key)], root) }
+    // Traverse pierced props (e.g. foo-bar=value => foo.bar = value)
+    if (key.includes('-') && target === undefined) {
+      const chain = key.split('-')
+      target = chain.reduce((acc, key) => acc[kebabToCamel(key)], root)
+      key = chain.pop() as string
+      finalKey = key
+      if (!target?.set) { root = chain.reduce((acc, key) => acc[kebabToCamel(key)], root) }
+    }
+    let value = nextValue
+    if (value === '') { value = true }
+    // Set prop, prefer atomic methods if applicable
+    if (is.fun(target)) {
+      // don't call pointer event callback functions
+
+      if (!supportedPointerEvents.includes(prop)) {
+        if (is.arr(value)) { node[finalKey](...value) }
+        else { node[finalKey](value) }
       }
       }
-      let value = nextValue
-      if (value === '') { value = true }
-      // Set prop, prefer atomic methods if applicable
-      if (isFunction(target)) {
-        // don't call pointer event callback functions
-
-        if (!supportedPointerEvents.includes(prop)) {
-          if (Array.isArray(value)) { node[finalKey](...value) }
-          else { node[finalKey](value) }
-        }
-        // NOTE: Set on* callbacks
-        // Issue: https://github.com/Tresjs/tres/issues/360
-        if (finalKey.startsWith('on') && isFunction(value)) {
-          root[finalKey] = value
-        }
-        return
+      // NOTE: Set on* callbacks
+      // Issue: https://github.com/Tresjs/tres/issues/360
+      if (finalKey.startsWith('on') && is.fun(value)) {
+        root[finalKey] = value
       }
       }
-      if (!target?.set && !isFunction(target)) { root[finalKey] = value }
-      else if (target.constructor === value.constructor && target?.copy) { target?.copy(value) }
-      else if (Array.isArray(value)) { target.set(...value) }
-      else if (!target.isColor && target.setScalar) { target.setScalar(value) }
-      else { target.set(value) }
-
-      invalidateInstance(node as TresObject)
+      return
     }
     }
+    if (!target?.set && !is.fun(target)) { root[finalKey] = value }
+    else if (target.constructor === value.constructor && target?.copy) { target?.copy(value) }
+    else if (is.arr(value)) { target.set(...value) }
+    else if (!target.isColor && target.setScalar) { target.setScalar(value) }
+    else { target.set(value) }
+
+    invalidateInstance(node as TresObject)
   }
   }
 
 
   function parentNode(node: TresObject): TresObject | null {
   function parentNode(node: TresObject): TresObject | null {

+ 220 - 0
src/utils/is.test.ts

@@ -0,0 +1,220 @@
+import { BufferGeometry, Fog, MeshBasicMaterial, MeshNormalMaterial, Object3D, PerspectiveCamera } from 'three'
+import * as is from './is'
+
+describe('is', () => {
+  describe('is.tresObject(a: any)', () => {
+    describe('true', () => {
+      it('object3D', () => {
+        assert(is.tresObject(new Object3D()))
+      })
+      it('bufferGeometry', () => {
+        assert(is.tresObject(new BufferGeometry()))
+      })
+      it('material', () => {
+        assert(is.tresObject(new MeshNormalMaterial()))
+      })
+      it('fog', () => {
+        assert(is.tresObject(new Fog('red')))
+      })
+      it('camera', () => {
+        assert(is.tresObject(new PerspectiveCamera()))
+      })
+    })
+    describe('false', () => {
+      it('undefined', () => {
+        assert(!is.tresObject(undefined))
+      })
+      it('null', () => {
+        assert(!is.tresObject(null))
+      })
+      it('number', () => {
+        assert(!is.tresObject(0))
+        assert(!is.tresObject(Math.PI))
+        assert(!is.tresObject(Number.POSITIVE_INFINITY))
+        assert(!is.tresObject(Number.NEGATIVE_INFINITY))
+        assert(!is.tresObject(42))
+      })
+      it('string', () => {
+        assert(!is.tresObject(''))
+        assert(!is.tresObject('tresObject'))
+      })
+      it('function', () => {
+        assert(!is.tresObject(() => {}))
+        assert(!is.tresObject(() => {}))
+      })
+    })
+  })
+
+  describe('is.bufferGeometry(a: any)', () => {
+    describe('true', () => {
+      it('bufferGeometry', () => {
+        assert(is.bufferGeometry(new BufferGeometry()))
+      })
+    })
+    describe('false', () => {
+      it('object3D', () => {
+        assert(!is.bufferGeometry(new Object3D()))
+      })
+      it('material', () => {
+        assert(!is.bufferGeometry(new MeshNormalMaterial()))
+      })
+      it('fog', () => {
+        assert(!is.bufferGeometry(new Fog('red')))
+      })
+      it('camera', () => {
+        assert(!is.bufferGeometry(new PerspectiveCamera()))
+      })
+      it('undefined', () => {
+        assert(!is.bufferGeometry(undefined))
+      })
+      it('null', () => {
+        assert(!is.bufferGeometry(null))
+      })
+      it('number', () => {
+        assert(!is.bufferGeometry(0))
+        assert(!is.bufferGeometry(Math.PI))
+        assert(!is.bufferGeometry(Number.POSITIVE_INFINITY))
+        assert(!is.bufferGeometry(Number.NEGATIVE_INFINITY))
+        assert(!is.bufferGeometry(42))
+      })
+      it('string', () => {
+        assert(!is.bufferGeometry(''))
+        assert(!is.bufferGeometry('bufferGeometry'))
+      })
+      it('function', () => {
+        assert(!is.bufferGeometry(() => {}))
+        assert(!is.bufferGeometry(() => {}))
+      })
+    })
+  })
+
+  describe('is.material(a: any)', () => {
+    describe('true', () => {
+      it('material', () => {
+        assert(is.material(new MeshNormalMaterial()))
+        assert(is.material(new MeshBasicMaterial()))
+      })
+    })
+    describe('false', () => {
+      it('object3D', () => {
+        assert(!is.bufferGeometry(new Object3D()))
+      })
+      it('bufferGeometry', () => {
+        assert(!is.bufferGeometry(new MeshNormalMaterial()))
+      })
+      it('fog', () => {
+        assert(!is.bufferGeometry(new Fog('red')))
+      })
+      it('camera', () => {
+        assert(!is.bufferGeometry(new PerspectiveCamera()))
+      })
+      it('undefined', () => {
+        assert(!is.bufferGeometry(undefined))
+      })
+      it('null', () => {
+        assert(!is.bufferGeometry(null))
+      })
+      it('number', () => {
+        assert(!is.bufferGeometry(0))
+        assert(!is.bufferGeometry(Math.PI))
+        assert(!is.bufferGeometry(Number.POSITIVE_INFINITY))
+        assert(!is.bufferGeometry(Number.NEGATIVE_INFINITY))
+        assert(!is.bufferGeometry(42))
+      })
+      it('string', () => {
+        assert(!is.bufferGeometry(''))
+        assert(!is.bufferGeometry('bufferGeometry'))
+      })
+      it('function', () => {
+        assert(!is.bufferGeometry(() => {}))
+        assert(!is.bufferGeometry(() => {}))
+      })
+    })
+  })
+
+  describe('is.camera(a: any)', () => {
+    describe('true', () => {
+      it('camera', () => {
+        assert(is.camera(new PerspectiveCamera()))
+      })
+    })
+    describe('false', () => {
+      it('object3D', () => {
+        assert(!is.camera(new Object3D()))
+      })
+      it('bufferGeometry', () => {
+        assert(!is.camera(new BufferGeometry()))
+      })
+      it('material', () => {
+        assert(!is.camera(new MeshNormalMaterial()))
+      })
+      it('fog', () => {
+        assert(!is.camera(new Fog('red')))
+      })
+      it('undefined', () => {
+        assert(!is.camera(undefined))
+      })
+      it('null', () => {
+        assert(!is.camera(null))
+      })
+      it('number', () => {
+        assert(!is.camera(0))
+        assert(!is.camera(Math.PI))
+        assert(!is.camera(Number.POSITIVE_INFINITY))
+        assert(!is.camera(Number.NEGATIVE_INFINITY))
+        assert(!is.camera(42))
+      })
+      it('string', () => {
+        assert(!is.camera(''))
+        assert(!is.camera('camera'))
+      })
+      it('function', () => {
+        assert(!is.camera(() => {}))
+        assert(!is.camera(() => {}))
+      })
+    })
+  })
+
+  describe('is.fog(a: any)', () => {
+    describe('true', () => {
+      it('fog', () => {
+        assert(is.fog(new Fog('red')))
+      })
+    })
+    describe('false', () => {
+      it('object3D', () => {
+        assert(!is.fog(new Object3D()))
+      })
+      it('camera', () => {
+        assert(!is.fog(new PerspectiveCamera()))
+      })
+      it('bufferGeometry', () => {
+        assert(!is.fog(new BufferGeometry()))
+      })
+      it('material', () => {
+        assert(!is.fog(new MeshNormalMaterial()))
+      })
+      it('undefined', () => {
+        assert(!is.fog(undefined))
+      })
+      it('null', () => {
+        assert(!is.fog(null))
+      })
+      it('number', () => {
+        assert(!is.fog(0))
+        assert(!is.fog(Math.PI))
+        assert(!is.fog(Number.POSITIVE_INFINITY))
+        assert(!is.fog(Number.NEGATIVE_INFINITY))
+        assert(!is.fog(42))
+      })
+      it('string', () => {
+        assert(!is.fog(''))
+        assert(!is.fog('camera'))
+      })
+      it('function', () => {
+        assert(!is.fog(() => {}))
+        assert(!is.fog(() => {}))
+      })
+    })
+  })
+})

+ 40 - 0
src/utils/is.ts

@@ -0,0 +1,40 @@
+import type { TresObject } from 'src/types'
+import type { BufferGeometry, Camera, Fog, Material, Object3D } from 'three'
+
+export function arr(u: unknown) {
+  return Array.isArray(u)
+}
+
+export function fun(u: unknown): u is Function {
+  return typeof u === 'function'
+}
+
+export function obj(u: unknown): u is Record<string | number | symbol, unknown> {
+  return u === Object(u) && !arr(u) && !fun(u)
+}
+
+export function object3D(u: unknown): u is Object3D {
+  return obj(u) && ('isObject3D' in u) && !!(u.isObject3D)
+}
+
+export function camera(u: unknown): u is Camera {
+  return obj(u) && 'isCamera' in u && !!(u.isCamera)
+}
+
+export function bufferGeometry(u: unknown): u is BufferGeometry {
+  return obj(u) && 'isBufferGeometry' in u && !!(u.isBufferGeometry)
+}
+
+export function material(u: unknown): u is Material {
+  return obj(u) && 'isMaterial' in u && !!(u.isMaterial)
+}
+
+export function fog(u: unknown): u is Fog {
+  return obj(u) && 'isFog' in u && !!(u.isFog)
+}
+
+export function tresObject(u: unknown): u is TresObject {
+  // NOTE: TresObject is currently defined as
+  // TresObject3D | THREE.BufferGeometry | THREE.Material | THREE.Fog
+  return object3D(u) || bufferGeometry(u) || material(u) || fog(u)
+}