Ver código fonte

feat: make some of the utility methods available to users (#1068)

* refactor: reorganize HTML tag handling and utility functions

- Moved the `HTML_TAGS` constant and `isHTMLTag` function from `src/utils/index.ts` to `src/utils/is.ts` for better modularity.
- Removed the `makeMap` function from `index.ts` and imported it in `is.ts`, enhancing code clarity and separation of concerns.
- Cleaned up unused code in `index.ts` to streamline the utility functions.

* fix: fixed import

* refactor: made use of radashis type guards and removed unused ones.

* refactor: structured type guards and made them smaller and more readable

* refactor: remove unused type guard tests and consolidate exports in utility files

* refactor: Refactor utility functions and improve equality checks in node operations, removed unused methods

* refactor: removed more unused code; added string util

* refactor: moved noop

* refactor: moved pixelRatio stuff

* refactor: replaced shuffle

* refactor: separated getObjectByUuid

* refactor: removed unused highlight material methods, moved used one

* chore: removed unused dependency

* refactor: moved filterInPlace

* moved extractBindingPosition

* refactor: moved hasMap

* chore: removed comment

* Add exports for three and tres utility modules

Export utility functions from utils/is/three and utils/is/tres to make them available for external use.

* fix: added null check

* chore: made use of isMesh in docs

* made use of type guards

* chore: beautified code a bit

* docs: added type guard docs

---------

Co-authored-by: alvarosabu <alvaro.saburido@gmail.com>
Tino Koch 1 mês atrás
pai
commit
a225230cd6

+ 1 - 3
docs/app/components/examples/web-gpu/HologramCube.vue

@@ -1,7 +1,7 @@
 <script setup lang="ts">
+import { isMesh } from '@tresjs/core'
 import { useGLTF } from '@tresjs/cientos'
 import { add, cameraProjectionMatrix, cameraViewMatrix, color, Fn, hash, mix, normalView, positionWorld, sin, timerGlobal, uniform, varying, vec3, vec4 } from 'three/tsl'
-import type { Mesh, Object3D } from 'three/webgpu'
 import { AdditiveBlending, DoubleSide, MeshBasicNodeMaterial } from 'three/webgpu'
 
 const { nodes } = useGLTF('https://raw.githubusercontent.com/Tresjs/assets/main/models/gltf/blender-cube.glb', { draco: true })
@@ -46,8 +46,6 @@ material.colorNode = Fn(() => {
 })()
 
 watch(model, (newModel) => {
-  const isMesh = (child: Object3D): child is Mesh => 'isMesh' in child && !!(child.isMesh)
-
   newModel?.traverse((child) => {
     if (isMesh(child)) {
       child.material = material

+ 2 - 0
docs/content/3.api/4.utils/.navigation.yml

@@ -0,0 +1,2 @@
+title: Utils
+icon: i-lucide-wrench

+ 22 - 0
docs/content/3.api/4.utils/1.type-guards.md

@@ -0,0 +1,22 @@
+---
+title: Type Guards
+description: TresJS provides type guard methods to help you determine the type of a Three.js object.
+---
+
+To help you work with Three.js objects more effectively, TresJS provides a set of type guard methods. These methods allow you to determine the type of a Three.js object, making your code more robust and easier to maintain.
+The supported type guards are:
+
+- `isBufferGeometry`
+- `isCamera`
+- `isColor`
+- `isColorRepresentation`
+- `isFog`
+- `isGroup`
+- `isLayers`
+- `isLight`
+- `isMaterial`
+- `isMesh`
+- `isObject3D`
+- `isOrthographicCamera`
+- `isPerspectiveCamera`
+- `isScene`

+ 0 - 0
docs/content/3.api/4.advanced/.navigation.yml → docs/content/3.api/5.advanced/.navigation.yml


+ 0 - 0
docs/content/3.api/4.advanced/performance.md → docs/content/3.api/5.advanced/performance.md


+ 0 - 0
docs/content/3.api/4.advanced/primitives.md → docs/content/3.api/5.advanced/primitives.md


+ 2 - 1
docs/content/3.api/4.advanced/web-gpu.md → docs/content/3.api/5.advanced/web-gpu.md

@@ -72,6 +72,7 @@ const createWebGPURenderer = (ctx: TresRendererSetupContext) => {
 
   ```vue [components/HologramCube.vue]
   <script setup lang="ts">
+  import { isMesh } from '@tesjs/core'
   import { useGLTF } from '@tresjs/cientos'
   import { add, cameraProjectionMatrix, cameraViewMatrix, color, Fn, hash, mix, normalView, positionWorld, sin, timerGlobal, uniform, varying, vec3, vec4 } from 'three/tsl'
   import { AdditiveBlending, DoubleSide, MeshBasicNodeMaterial } from 'three/webgpu'
@@ -119,7 +120,7 @@ const createWebGPURenderer = (ctx: TresRendererSetupContext) => {
 
   watch(model, (newModel) => {
     newModel.traverse((child) => {
-      if (child.isMesh) {
+      if (isMesh(child)) {
         child.material = material
       }
     })

+ 2 - 2
package.json

@@ -78,10 +78,10 @@
     "vue": ">=3.4"
   },
   "dependencies": {
-    "@alvarosabu/utils": "^3.2.0",
     "@pmndrs/pointer-events": "^6.6.17",
     "@vue/devtools-api": "^7.7.2",
-    "@vueuse/core": "^12.5.0"
+    "@vueuse/core": "^12.5.0",
+    "radashi": "^12.6.0"
   },
   "devDependencies": {
     "@release-it/conventional-changelog": "^10.0.0",

+ 2 - 1
playground/vue/src/pages/advanced/webGPU/HologramCube.vue

@@ -1,4 +1,5 @@
 <script setup lang="ts">
+import { isMesh } from '@tresjs/core'
 import { useGLTF } from '@tresjs/cientos'
 import { add, cameraProjectionMatrix, cameraViewMatrix, color, Fn, hash, mix, normalView, positionWorld, sin, timerGlobal, uniform, varying, vec3, vec4 } from 'three/tsl'
 import { AdditiveBlending, DoubleSide, MeshBasicNodeMaterial } from 'three/webgpu'
@@ -46,7 +47,7 @@ material.colorNode = Fn(() => {
 
 watch(model, (newModel) => {
   newModel.traverse((child) => {
-    if (child.isMesh) {
+    if (isMesh(child)) {
       child.material = material
     }
   })

+ 9 - 3
pnpm-lock.yaml

@@ -13,9 +13,6 @@ importers:
 
   .:
     dependencies:
-      '@alvarosabu/utils':
-        specifier: ^3.2.0
-        version: 3.2.0
       '@pmndrs/pointer-events':
         specifier: ^6.6.17
         version: 6.6.20
@@ -25,6 +22,9 @@ importers:
       '@vueuse/core':
         specifier: ^12.5.0
         version: 12.8.2(typescript@5.9.2)
+      radashi:
+        specifier: ^12.6.0
+        version: 12.6.0
     devDependencies:
       '@release-it/conventional-changelog':
         specifier: ^10.0.0
@@ -7035,6 +7035,10 @@ packages:
   quote-unquote@1.0.0:
     resolution: {integrity: sha512-twwRO/ilhlG/FIgYeKGFqyHhoEhqgnKVkcmqMKi2r524gz3ZbDTcyFt38E9xjJI2vT+KbRNHVbnJ/e0I25Azwg==}
 
+  radashi@12.6.0:
+    resolution: {integrity: sha512-nJcpgS3La+yRlxm4bLjCcjsi1G5VeOycSo8nt5BzSEDWqSY0XNAUAwckvCa8uMHXtZqCK8fntJPXPlLve+vKhA==}
+    engines: {node: '>=16.0.0'}
+
   radix3@1.1.2:
     resolution: {integrity: sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==}
 
@@ -16932,6 +16936,8 @@ snapshots:
 
   quote-unquote@1.0.0: {}
 
+  radashi@12.6.0: {}
+
   radix3@1.1.2: {}
 
   randombytes@2.1.0:

+ 130 - 0
src/composables/useRenderer/pixelRatio.test.ts

@@ -0,0 +1,130 @@
+import { setPixelRatio } from './pixelRatio'
+
+describe('setPixelRatio', () => {
+  const INITIAL_DPR = 1
+  let dpr = INITIAL_DPR
+  const mockRenderer = {
+    setPixelRatio: (n: number) => { dpr = n },
+    getPixelRatio: () => dpr,
+  }
+  const setPixelRatioSpy = vi.spyOn(mockRenderer, 'setPixelRatio')
+
+  beforeEach(() => {
+    dpr = 1
+    setPixelRatioSpy.mockClear()
+  })
+
+  describe('setPixelRatio(renderer: WebGLRenderer, systemDpr: number)', () => {
+    it('calls the renderer\'s setPixelRatio method with systemDpr', () => {
+      expect(setPixelRatioSpy).not.toBeCalled()
+      setPixelRatio(mockRenderer, 2)
+      expect(setPixelRatioSpy).toBeCalledWith(2)
+
+      setPixelRatio(mockRenderer, 2.1)
+      expect(setPixelRatioSpy).toBeCalledWith(2.1)
+
+      setPixelRatio(mockRenderer, 1.44444)
+      expect(setPixelRatioSpy).toBeCalledWith(1.44444)
+    })
+    it('does not set the renderer\'s pixelRatio if systemDpr === pixelRatio', () => {
+      setPixelRatio(mockRenderer, 1)
+      expect(setPixelRatioSpy).not.toBeCalled()
+
+      setPixelRatio(mockRenderer, 2)
+      expect(setPixelRatioSpy).toBeCalledTimes(1)
+
+      setPixelRatio(mockRenderer, 2)
+      expect(setPixelRatioSpy).toBeCalledTimes(1)
+
+      setPixelRatio(mockRenderer, 1)
+      expect(setPixelRatioSpy).toBeCalledTimes(2)
+
+      setPixelRatio(mockRenderer, 1)
+      expect(setPixelRatioSpy).toBeCalledTimes(2)
+    })
+    it('does not throw if passed a "renderer" without a `setPixelRatio` method', () => {
+      const mockSVGRenderer = {}
+      expect(() => setPixelRatio(mockSVGRenderer, 2)).not.toThrow()
+    })
+    it('calls `setPixelRatio` even if passed a "renderer" without a `getPixelRatio` method', () => {
+      const mockSVGRenderer = { setPixelRatio: () => {} }
+      const setPixelRatioSpy = vi.spyOn(mockSVGRenderer, 'setPixelRatio')
+      expect(() => setPixelRatio(mockSVGRenderer, 2)).not.toThrow()
+      expect(setPixelRatioSpy).toBeCalledWith(2)
+      expect(setPixelRatioSpy).toBeCalledTimes(1)
+
+      setPixelRatio(mockSVGRenderer, 1.99)
+      expect(setPixelRatioSpy).toBeCalledWith(1.99)
+      expect(setPixelRatioSpy).toBeCalledTimes(2)
+
+      setPixelRatio(mockSVGRenderer, 2.1)
+      expect(setPixelRatioSpy).toBeCalledWith(2.1)
+      expect(setPixelRatioSpy).toBeCalledTimes(3)
+    })
+  })
+
+  describe('setPixelRatio(renderer: WebGLRenderer, systemDpr: number, userDpr: number)', () => {
+    it('calls the renderer\'s setPixelRatio method with userDpr', () => {
+      expect(setPixelRatioSpy).not.toBeCalled()
+      setPixelRatio(mockRenderer, 2, 100)
+      expect(setPixelRatioSpy).toBeCalledWith(100)
+    })
+    it('does not call the renderer\'s setPixelRatio method if current dpr === new dpr', () => {
+      expect(setPixelRatioSpy).not.toBeCalled()
+      setPixelRatio(mockRenderer, 2, 1)
+      expect(setPixelRatioSpy).not.toBeCalledWith()
+
+      setPixelRatio(mockRenderer, 3, 1.4)
+      expect(setPixelRatioSpy).toBeCalledTimes(1)
+      expect(setPixelRatioSpy).toBeCalledWith(1.4)
+
+      setPixelRatio(mockRenderer, 3, 1.4)
+      expect(setPixelRatioSpy).toBeCalledTimes(1)
+      expect(setPixelRatioSpy).toBeCalledWith(1.4)
+
+      setPixelRatio(mockRenderer, 2, 1.4)
+      expect(setPixelRatioSpy).toBeCalledTimes(1)
+      expect(setPixelRatioSpy).toBeCalledWith(1.4)
+
+      setPixelRatio(mockRenderer, 42, 0.1)
+      expect(setPixelRatioSpy).toBeCalledTimes(2)
+      expect(setPixelRatioSpy).toBeCalledWith(0.1)
+
+      setPixelRatio(mockRenderer, 4, 0.1)
+      expect(setPixelRatioSpy).toBeCalledTimes(2)
+      expect(setPixelRatioSpy).toBeCalledWith(0.1)
+    })
+  })
+
+  describe('setPixelRatio(renderer: WebGLRenderer, systemDpr: number, userDpr: [number, number])', () => {
+    it('clamps systemDpr to userDpr', () => {
+      setPixelRatio(mockRenderer, 2, [0, 4])
+      expect(setPixelRatioSpy).toBeCalledTimes(1)
+      expect(setPixelRatioSpy).toBeCalledWith(2)
+
+      setPixelRatio(mockRenderer, 2, [3, 4])
+      expect(setPixelRatioSpy).toBeCalledTimes(2)
+      expect(setPixelRatioSpy).toBeCalledWith(3)
+
+      setPixelRatio(mockRenderer, 5, [3, 4])
+      expect(setPixelRatioSpy).toBeCalledTimes(3)
+      expect(setPixelRatioSpy).toBeCalledWith(4)
+
+      setPixelRatio(mockRenderer, 100, [3, 4])
+      expect(setPixelRatioSpy).toBeCalledTimes(3)
+      expect(setPixelRatioSpy).toBeCalledWith(4)
+
+      setPixelRatio(mockRenderer, 100, [3.5, 4])
+      expect(setPixelRatioSpy).toBeCalledTimes(3)
+      expect(setPixelRatioSpy).toBeCalledWith(4)
+
+      setPixelRatio(mockRenderer, 100, [3, 6.1])
+      expect(setPixelRatioSpy).toBeCalledTimes(4)
+      expect(setPixelRatioSpy).toBeCalledWith(6.1)
+
+      setPixelRatio(mockRenderer, 1, [2.99, 6.1])
+      expect(setPixelRatioSpy).toBeCalledTimes(5)
+      expect(setPixelRatioSpy).toBeCalledWith(2.99)
+    })
+  })
+})

+ 26 - 0
src/composables/useRenderer/pixelRatio.ts

@@ -0,0 +1,26 @@
+import { isFunction, isNumber } from '../../utils/is'
+import { MathUtils } from 'three'
+
+export const setPixelRatio = (
+  renderer: { setPixelRatio?: (dpr: number) => void, getPixelRatio?: () => number },
+  systemDpr: number,
+  userDpr?: number | [number, number],
+) => {
+  // NOTE: Optional `setPixelRatio` allows this function to accept
+  // THREE renderers like SVGRenderer.
+  if (!isFunction(renderer.setPixelRatio)) { return }
+
+  let newDpr = 0
+
+  if (userDpr && Array.isArray(userDpr) && userDpr.length >= 2) {
+    const [min, max] = userDpr
+    newDpr = MathUtils.clamp(systemDpr, min, max)
+  }
+  else if (isNumber(userDpr)) { newDpr = userDpr }
+  else { newDpr = systemDpr }
+
+  // NOTE: Don't call `setPixelRatio` unless both:
+  // - the dpr value has changed
+  // - the renderer has `setPixelRatio`; this check allows us to pass any THREE renderer
+  if (newDpr !== renderer.getPixelRatio?.()) { renderer.setPixelRatio(newDpr) }
+}

+ 1 - 1
src/composables/useRenderer/useRendererManager.ts

@@ -13,7 +13,7 @@ import { computed, type MaybeRef, onUnmounted, type Reactive, ref, type ShallowR
 import type { Renderer } from 'three/webgpu'
 
 // Solution taken from Thretle that actually support different versions https://github.com/threlte/threlte/blob/5fa541179460f0dadc7dc17ae5e6854d1689379e/packages/core/src/lib/lib/useRenderer.ts
-import { setPixelRatio } from '../../utils'
+import { setPixelRatio } from './pixelRatio'
 
 import { logWarning } from '../../utils/logger'
 import type { SizesType } from '../useSizes'

+ 19 - 17
src/core/nodeOps.ts

@@ -2,9 +2,10 @@ import type { TresContext } from '../composables'
 import type { DisposeType, LocalState, TresInstance, TresObject, TresObject3D, TresPrimitive, WithMathProps } from '../types'
 import { BufferAttribute, Object3D } from 'three'
 import { isRef, type RendererOptions } from 'vue'
-import { attach, deepArrayEqual, doRemoveDeregister, doRemoveDetach, invalidateInstance, isHTMLTag, kebabToCamel, noop, prepareTresInstance, resolve, setPrimitiveObject, unboxTresPrimitive } from '../utils'
+import { attach, doRemoveDeregister, doRemoveDetach, invalidateInstance, prepareTresInstance, resolve, setPrimitiveObject, unboxTresPrimitive } from '../utils'
 import { logError } from '../utils/logger'
-import { isArray, isCamera, isClassInstance, isColor, isColorRepresentation, isCopyable, isFunction, isLayers, isObject, isObject3D, isScene, isTresInstance, isUndefined, isVectorLike } from '../utils/is'
+import { isClassInstance, isColor, isColorRepresentation, isCopyable, isEqual, isFunction, isHTMLTag, isLayers, isObject, isObject3D, isScene, isTresCamera, isTresInstance, isUndefined, isVectorLike } from '../utils/is'
+import { camel } from '../utils/string'
 import { createRetargetingProxy } from '../utils/primitive/createRetargetingProxy'
 import { catalogue } from './catalogue'
 import { isSupportedPointerEvent, pointerEventsMapVueToThree } from '../utils/pointerEvents'
@@ -18,7 +19,6 @@ export const nodeOps: (context: TresContext) => RendererOptions<TresObject, Tres
     if (!props.args) {
       props.args = []
     }
-    if (tag === 'template') { return null }
     if (isHTMLTag(tag)) { return null }
     let name = tag.replace('Tres', '')
     let obj: TresObject | null
@@ -61,7 +61,7 @@ export const nodeOps: (context: TresContext) => RendererOptions<TresObject, Tres
     if (!obj) { return null }
 
     // Opinionated default to avoid user issue not seeing anything if camera is on origin
-    if (isCamera(obj)) {
+    if (isTresCamera(obj)) {
       if (!props?.position) {
         obj.position.set(3, 3, 3)
       }
@@ -94,7 +94,7 @@ export const nodeOps: (context: TresContext) => RendererOptions<TresObject, Tres
     child = unboxTresPrimitive(childInstance)
     parent = unboxTresPrimitive(parentInstance)
 
-    if (isCamera(child)) {
+    if (isTresCamera(child)) {
       context.camera?.registerCamera(child)
     }
 
@@ -244,7 +244,7 @@ export const nodeOps: (context: TresContext) => RendererOptions<TresObject, Tres
     if (isSupportedPointerEvent(prop) && isFunction(nextValue)) {
       node.addEventListener(pointerEventsMapVueToThree[prop], nextValue)
     }
-    let finalKey = kebabToCamel(key)
+    let finalKey = camel(key)
     let target = root?.[finalKey] as Record<string, unknown>
 
     if (key === 'args') {
@@ -256,7 +256,7 @@ export const nodeOps: (context: TresContext) => RendererOptions<TresObject, Tres
       if (
         instanceName
         && prevArgs.length
-        && !deepArrayEqual(prevArgs, args)
+        && !isEqual(prevArgs, args)
       ) {
         // Create a new instance
         const newInstance = new catalogue.value[instanceName](...nextValue)
@@ -290,7 +290,7 @@ export const nodeOps: (context: TresContext) => RendererOptions<TresObject, Tres
     if (root.type === 'BufferGeometry') {
       if (key === 'args') { return }
       (root as TresObject).setAttribute(
-        kebabToCamel(key),
+        camel(key),
         new BufferAttribute(...(nextValue as ConstructorParameters<typeof BufferAttribute>)),
       )
       return
@@ -310,7 +310,7 @@ export const nodeOps: (context: TresContext) => RendererOptions<TresObject, Tres
       // don't call pointer event callback functions
 
       if (!isSupportedPointerEvent(prop)) {
-        if (isArray(value)) { node[finalKey](...value) }
+        if (Array.isArray(value)) { node[finalKey](...value) }
         else { node[finalKey](value) }
       }
       // NOTE: Set on* callbacks
@@ -360,7 +360,7 @@ export const nodeOps: (context: TresContext) => RendererOptions<TresObject, Tres
       root[finalKey] = value
     }
 
-    if (isCamera(node)) {
+    if (isTresCamera(node)) {
       node.updateProjectionMatrix()
     }
 
@@ -399,20 +399,22 @@ export const nodeOps: (context: TresContext) => RendererOptions<TresObject, Tres
     return siblings[index + 1]
   }
 
+  const noop = (): any => {}
+
   return {
     insert,
     remove,
     createElement,
     patchProp,
     parentNode,
-    createText: () => noop('createText'),
+    createText: noop,
     createComment,
-    setText: () => noop('setText'),
-    setElementText: () => noop('setElementText'),
+    setText: noop,
+    setElementText: noop,
     nextSibling,
-    querySelector: () => noop('querySelector'),
-    setScopeId: () => noop('setScopeId'),
-    cloneNode: () => noop('cloneNode'),
-    insertStaticContent: () => noop('insertStaticContent'),
+    querySelector: noop,
+    setScopeId: noop,
+    cloneNode: noop,
+    insertStaticContent: noop,
   }
 }

+ 150 - 111
src/devtools/inspectorHandlers.ts

@@ -1,13 +1,14 @@
-import type { Mesh } from 'three'
-import { Color } from 'three'
+import type { Mesh, Scene } from 'three'
+import { Color, DoubleSide, MeshBasicMaterial } from 'three'
 import type { TresObject } from '../types'
 import { bytesToKB, calculateMemoryUsage } from '../utils/perf'
-import { isLight } from '../utils/is'
+import { isLight, isMesh, isScene } from '../utils/is'
 import type { SceneGraphObject } from './types'
 import { isRef } from 'vue'
 import type { TresContext } from '../composables/useTresContextProvider'
 import { INSPECTOR_ID } from './plugin'
-import { createHighlightMesh, editSceneObject } from '../utils'
+import { getObjectByUuid } from '../utils/three'
+import { HightlightMesh } from '../devtools/highlight'
 
 /**
  * Creates a node representation of a Three.js object for the inspector tree
@@ -235,137 +236,175 @@ export const inspectorTreeHandler = (tres: TresContext) => (payload: any) => {
  * @returns A function that handles inspector state payload updates
  */
 export const inspectorStateHandler = (tres: TresContext, { highlightMesh, prevInstance }: { highlightMesh: Mesh | null, prevInstance: TresObject | null }) => (payload: any) => {
-  if (payload.inspectorId === INSPECTOR_ID) {
-    if (payload.nodeId.includes('scene')) {
-      // Extract UUID from scene-uuid format
-      const match = payload.nodeId.match(/^scene-(.+)$/)
-      const uuid = match ? match[1] : null
-      if (!uuid) { return }
+  if (payload.inspectorId !== INSPECTOR_ID) { return }
 
-      const [instance] = tres.scene.value.getObjectsByProperty('uuid', uuid) as TresObject[]
-      if (!instance) { return }
+  const highlightMaterial = new MeshBasicMaterial({
+    color: 0xA7E6D7, // Highlight color, e.g., yellow
+    transparent: true,
+    opacity: 0.2,
+    depthTest: false, // So the highlight is always visible
+    side: DoubleSide, // To ensure the highlight is visible from all angles
+  })
 
-      if (prevInstance && highlightMesh && highlightMesh.parent) {
-        prevInstance.remove(highlightMesh)
-      }
+  if (payload.nodeId.includes('scene')) {
+    // Extract UUID from scene-uuid format
+    const match = payload.nodeId.match(/^scene-(.+)$/)
+    const uuid = match ? match[1] : null
+    if (!uuid) { return }
 
-      if (instance.isMesh) {
-        const newHighlightMesh = createHighlightMesh(instance)
-        instance.add(newHighlightMesh)
+    const [instance] = tres.scene.value.getObjectsByProperty('uuid', uuid) as TresObject[]
+    if (!instance) { return }
 
-        highlightMesh = newHighlightMesh
-        prevInstance = instance
-      }
+    if (prevInstance && highlightMesh && highlightMesh.parent) {
+      prevInstance.remove(highlightMesh)
+    }
 
-      payload.state = {
-        object: Object.entries(instance)
-          .map(([key, value]) => {
-            if (key === 'children') {
-              return { key, value: value.filter((child: { type: string }) => child.type !== 'HightlightMesh') }
-            }
-            return { key, value, editable: true }
-          })
-          .filter(({ key }) => {
-            return key !== 'parent'
-          }),
-      }
+    if (isMesh(instance)) {
+      const newHighlightMesh = new HightlightMesh(instance.geometry.clone(), highlightMaterial)
+      instance.add(newHighlightMesh)
 
-      if (instance.isScene) {
-        const sceneState = {
-          ...payload.state,
-          state: [
-            {
-              key: 'Scene Info',
-              value: {
-                objects: instance.children.length,
-                memory: calculateMemoryUsage(instance),
-                calls: tres.renderer.instance.info.render.calls,
-                triangles: tres.renderer.instance.info.render.triangles,
-                points: tres.renderer.instance.info.render.points,
-                lines: tres.renderer.instance.info.render.lines,
-              },
+      highlightMesh = newHighlightMesh
+      prevInstance = instance
+    }
+
+    payload.state = {
+      object: Object.entries(instance)
+        .map(([key, value]) => {
+          if (key === 'children') {
+            return { key, value: value.filter((child: { type: string }) => child.type !== 'HightlightMesh') }
+          }
+          return { key, value, editable: true }
+        })
+        .filter(({ key }) => {
+          return key !== 'parent'
+        }),
+    }
+
+    if (isScene(instance)) {
+      const sceneState = {
+        ...payload.state,
+        state: [
+          {
+            key: 'Scene Info',
+            value: {
+              objects: instance.children.length,
+              memory: calculateMemoryUsage(instance),
+              calls: tres.renderer.instance.info.render.calls,
+              triangles: tres.renderer.instance.info.render.triangles,
+              points: tres.renderer.instance.info.render.points,
+              lines: tres.renderer.instance.info.render.lines,
             },
-          ],
-        }
+          },
+        ],
+      }
 
-        if ('programs' in tres.renderer.instance.info) {
-          sceneState.state.push({
-            key: 'Programs',
-            value: tres.renderer.instance.info.programs?.map(program => ({
-              ...program,
-              programName: program.name,
-            })),
-          })
-        }
-        payload.state = sceneState
+      if ('programs' in tres.renderer.instance.info) {
+        sceneState.state.push({
+          key: 'Programs',
+          value: tres.renderer.instance.info.programs?.map(program => ({
+            ...program,
+            programName: program.name,
+          })),
+        })
       }
+      payload.state = sceneState
     }
-    else if (payload.nodeId.includes('context')) {
-      // Format is: context-uuid-chainedKey
-      // Use regex to match: 'context-' followed by UUID (which may contain dashes) followed by '-' and the chainedKey
-      const match = payload.nodeId.match(/^context-([^-]+(?:-[^-]+)*)-(.+)$/)
-      const chainedKey = match ? match[2] : 'context'
-
-      if (!chainedKey || chainedKey === 'context') {
-        // Root context node
-        payload.state = {
-          object: Object.entries(tres)
-            .filter(([key]) => !key.startsWith('_') && key !== 'parent')
-            .map(([key, value]) => ({
-              key,
-              value: isRef(value) ? value.value : value,
-              editable: false,
-            })),
-        }
-        return
-      }
+  }
+  else if (payload.nodeId.includes('context')) {
+    // Format is: context-uuid-chainedKey
+    // Use regex to match: 'context-' followed by UUID (which may contain dashes) followed by '-' and the chainedKey
+    const match = payload.nodeId.match(/^context-([^-]+(?:-[^-]+)*)-(.+)$/)
+    const chainedKey = match ? match[2] : 'context'
 
-      // Traverse the object path
-      const parts = chainedKey.split('.')
-      let value = tres as Record<string, any>
-      for (const part of parts) {
-        if (!value || typeof value !== 'object') { break }
-        value = isRef(value[part]) ? value[part].value : value[part]
+    if (!chainedKey || chainedKey === 'context') {
+      // Root context node
+      payload.state = {
+        object: Object.entries(tres)
+          .filter(([key]) => !key.startsWith('_') && key !== 'parent')
+          .map(([key, value]) => ({
+            key,
+            value: isRef(value) ? value.value : value,
+            editable: false,
+          })),
       }
+      return
+    }
 
-      if (value !== undefined) {
-        payload.state = {
-          object: Object.entries(value)
-            .filter(([key]) => !key.startsWith('_') && key !== 'parent')
-            .map(([key, val]) => {
-              if (isRef(val)) {
-                return {
-                  key,
-                  value: val.value,
-                  editable: false,
-                }
-              }
-              if (typeof val === 'function') {
-                return {
-                  key,
-                  value: 'ƒ()',
-                  editable: false,
-                }
+    // Traverse the object path
+    const parts = chainedKey.split('.')
+    let value = tres as Record<string, any>
+    for (const part of parts) {
+      if (!value || typeof value !== 'object') { break }
+      value = isRef(value[part]) ? value[part].value : value[part]
+    }
+
+    if (value !== undefined) {
+      payload.state = {
+        object: Object.entries(value)
+          .filter(([key]) => !key.startsWith('_') && key !== 'parent')
+          .map(([key, val]) => {
+            if (isRef(val)) {
+              return {
+                key,
+                value: val.value,
+                editable: false,
               }
-              if (val && typeof val === 'object') {
-                return {
-                  key,
-                  value: Array.isArray(val) ? `Array(${val.length})` : 'Object',
-                  editable: false,
-                }
+            }
+            if (typeof val === 'function') {
+              return {
+                key,
+                value: 'ƒ()',
+                editable: false,
               }
+            }
+            if (val && typeof val === 'object') {
               return {
                 key,
-                value: val,
+                value: Array.isArray(val) ? `Array(${val.length})` : 'Object',
                 editable: false,
               }
-            }),
-        }
+            }
+            return {
+              key,
+              value: val,
+              editable: false,
+            }
+          }),
       }
     }
   }
 }
 
+const editSceneObject = (scene: Scene, objectUuid: string, propertyPath: string[], value: any) => {
+  // Find the target object
+  const targetObject = getObjectByUuid(scene, objectUuid)
+  if (!targetObject) {
+    console.warn('Object with UUID not found in the scene.')
+    return
+  }
+
+  // Traverse the property path to get to the desired property
+  let currentProperty: any = targetObject
+  for (let i = 0; i < propertyPath.length - 1; i++) {
+    if (currentProperty[propertyPath[i]] !== undefined) {
+      currentProperty = currentProperty[propertyPath[i]]
+    }
+    else {
+      console.warn(`Property path is not valid: ${propertyPath.join('.')}`)
+      return
+    }
+  }
+
+  // Set the new value
+  const lastProperty = propertyPath[propertyPath.length - 1]
+  if (currentProperty[lastProperty] !== undefined) {
+    currentProperty[lastProperty] = value
+  }
+  else {
+    console.warn(`Property path is not valid: ${propertyPath.join('.')}`)
+  }
+}
+
 /**
  * Handler for inspector state edits
  * @param tres - The TresContext instance

+ 11 - 2
src/directives/vDistanceTo.ts

@@ -1,13 +1,22 @@
 import type { Ref } from 'vue'
 import type { TresObject } from '../types'
-import { ArrowHelper } from 'three'
+import { ArrowHelper, Vector3 } from 'three'
 import { logWarning } from '../utils/logger'
-import { extractBindingPosition } from '../utils'
+import { isMesh } from '../utils/is'
 
 let arrowHelper: ArrowHelper | null = null
 
 export const vDistanceTo = {
   updated: (el: TresObject, binding: Ref<TresObject>) => {
+    const extractBindingPosition = (binding: any): Vector3 => {
+      let observer = binding.value
+      if (binding.value && isMesh(binding.value)) {
+        observer = binding.value.position
+      }
+      if (Array.isArray(binding.value)) { observer = new Vector3(...observer) }
+      return observer
+    }
+
     const observer = extractBindingPosition(binding)
     if (!observer) {
       logWarning(`v-distance-to: problem with binding value: ${binding.value}`)

+ 3 - 2
src/directives/vLightHelper.ts

@@ -10,6 +10,7 @@ import {
 } from 'three'
 import { RectAreaLightHelper } from 'three-stdlib'
 import { logWarning } from '../utils/logger'
+import { isLight } from '../utils/is'
 
 type LightHelper = typeof DirectionalLightHelper
   | typeof PointLightHelper
@@ -30,12 +31,12 @@ const helpers: Record<Light['type'], LightHelper> = {
 
 export const vLightHelper = {
   mounted: (el: TresObject) => {
-    if (!el.isLight) {
+    if (!isLight(el)) {
       logWarning(`${el.type} is not a light`)
       return
     }
     CurrentHelper = helpers[el.type]
-    el.parent.add(new CurrentHelper(el as never, 1, el.color.getHex()))
+    el.parent?.add(new CurrentHelper(el as never, 1, el.color.getHex()))
   },
   updated: (el: TresObject) => {
     currentInstance = el.parent.children.find((child: TresObject) => child instanceof CurrentHelper)

+ 2 - 0
src/index.ts

@@ -10,6 +10,8 @@ export * from './core/catalogue'
 export * from './directives'
 export * from './types'
 export * from './utils/graph'
+export * from './utils/is/three'
+export * from './utils/is/tres'
 export * from './utils/logger'
 
 export interface TresOptions {

+ 50 - 0
src/utils/array.test.ts

@@ -0,0 +1,50 @@
+import { shuffle } from 'radashi'
+import { filterInPlace } from './array'
+
+describe('filterInPlace', () => {
+  it('returns the passed array', () => {
+    const arr = [1, 2, 3]
+    const result = filterInPlace(arr, v => v !== 0)
+    expect(result).toBe(arr)
+  })
+  it('removes a single occurence', () => {
+    const arr = [1, 2, 3]
+    filterInPlace(arr, v => v !== 1)
+    expect(arr).toStrictEqual([2, 3])
+  })
+  it('removes every occurence 0', () => {
+    const arr = [1, 1, 2, 1, 3, 1]
+    filterInPlace(arr, v => v !== 1)
+    expect(arr).toStrictEqual([2, 3])
+  })
+
+  it('removes every occurence 1', () => {
+    const [a, b, c] = [{}, {}, {}]
+    const COUNT = 400
+    const arr = []
+    for (const val of [a, b, c]) {
+      for (let i = 0; i < COUNT; i++) {
+        arr.push(val)
+      }
+    }
+    shuffle(arr)
+
+    let filtered = [...arr]
+    filterInPlace(arr, v => v !== b)
+    filtered = filtered.filter(v => v !== b)
+    expect(arr).toStrictEqual(filtered)
+
+    filterInPlace(arr, v => v !== c)
+    filtered = filtered.filter(v => v !== c)
+    expect(arr).toStrictEqual(filtered)
+
+    filterInPlace(arr, v => v !== a)
+    expect(arr).toStrictEqual([])
+  })
+
+  it('sends an index to the callbackFn', () => {
+    const arr = 'abcdefghi'.split('')
+    filterInPlace(arr, (_, i) => i % 2 === 0)
+    expect(arr).toStrictEqual('acegi'.split(''))
+  })
+})

+ 16 - 0
src/utils/array.ts

@@ -0,0 +1,16 @@
+/**
+ * Like Array.filter, but modifies the array in place.
+ * @param array - Array to modify
+ * @param callbackFn - A function called for each element of the array. It should return a truthy value to keep the element in the array.
+ */
+export const filterInPlace = <T>(array: T[], callbackFn: (element: T, index: number) => unknown) => {
+  let i = 0
+  for (let ii = 0; ii < array.length; ii++) {
+    if (callbackFn(array[ii], ii)) {
+      array[i] = array[ii]
+      i++
+    }
+  }
+  array.length = i
+  return array
+}

+ 0 - 190
src/utils/index.test.ts

@@ -1,66 +1,5 @@
 import * as utils from './index'
 
-describe('filterInPlace', () => {
-  it('returns the passed array', () => {
-    const arr = [1, 2, 3]
-    const result = utils.filterInPlace(arr, v => v !== 0)
-    expect(result).toBe(arr)
-  })
-  it('removes a single occurence', () => {
-    const arr = [1, 2, 3]
-    utils.filterInPlace(arr, v => v !== 1)
-    expect(arr).toStrictEqual([2, 3])
-  })
-  it('removes every occurence 0', () => {
-    const arr = [1, 1, 2, 1, 3, 1]
-    utils.filterInPlace(arr, v => v !== 1)
-    expect(arr).toStrictEqual([2, 3])
-  })
-
-  it('removes every occurence 1', () => {
-    const [a, b, c] = [{}, {}, {}]
-    const COUNT = 400
-    const arr = []
-    for (const val of [a, b, c]) {
-      for (let i = 0; i < COUNT; i++) {
-        arr.push(val)
-      }
-    }
-    shuffle(arr)
-
-    let filtered = [...arr]
-    utils.filterInPlace(arr, v => v !== b)
-    filtered = filtered.filter(v => v !== b)
-    expect(arr).toStrictEqual(filtered)
-
-    utils.filterInPlace(arr, v => v !== c)
-    filtered = filtered.filter(v => v !== c)
-    expect(arr).toStrictEqual(filtered)
-
-    utils.filterInPlace(arr, v => v !== a)
-    expect(arr).toStrictEqual([])
-  })
-
-  it('sends an index to the callbackFn', () => {
-    const arr = 'abcdefghi'.split('')
-    utils.filterInPlace(arr, (_, i) => i % 2 === 0)
-    expect(arr).toStrictEqual('acegi'.split(''))
-  })
-})
-
-function shuffle(array: any[]) {
-  let currentIndex = array.length
-  while (currentIndex !== 0) {
-    const randomIndex = Math.floor(Math.random() * currentIndex)
-    currentIndex--;
-    [array[currentIndex], array[randomIndex]] = [
-      array[randomIndex],
-      array[currentIndex],
-    ]
-  }
-  return array
-};
-
 describe('resolve', () => {
   it('returns the first argument if it contains the key', () => {
     const instance = { ab: 0 }
@@ -207,132 +146,3 @@ describe('resolve', () => {
     })
   })
 })
-
-describe('setPixelRatio', () => {
-  const INITIAL_DPR = 1
-  let dpr = INITIAL_DPR
-  const mockRenderer = {
-    setPixelRatio: (n: number) => { dpr = n },
-    getPixelRatio: () => dpr,
-  }
-  const setPixelRatioSpy = vi.spyOn(mockRenderer, 'setPixelRatio')
-
-  beforeEach(() => {
-    dpr = 1
-    setPixelRatioSpy.mockClear()
-  })
-
-  describe('setPixelRatio(renderer: WebGLRenderer, systemDpr: number)', () => {
-    it('calls the renderer\'s setPixelRatio method with systemDpr', () => {
-      expect(setPixelRatioSpy).not.toBeCalled()
-      utils.setPixelRatio(mockRenderer, 2)
-      expect(setPixelRatioSpy).toBeCalledWith(2)
-
-      utils.setPixelRatio(mockRenderer, 2.1)
-      expect(setPixelRatioSpy).toBeCalledWith(2.1)
-
-      utils.setPixelRatio(mockRenderer, 1.44444)
-      expect(setPixelRatioSpy).toBeCalledWith(1.44444)
-    })
-    it('does not set the renderer\'s pixelRatio if systemDpr === pixelRatio', () => {
-      utils.setPixelRatio(mockRenderer, 1)
-      expect(setPixelRatioSpy).not.toBeCalled()
-
-      utils.setPixelRatio(mockRenderer, 2)
-      expect(setPixelRatioSpy).toBeCalledTimes(1)
-
-      utils.setPixelRatio(mockRenderer, 2)
-      expect(setPixelRatioSpy).toBeCalledTimes(1)
-
-      utils.setPixelRatio(mockRenderer, 1)
-      expect(setPixelRatioSpy).toBeCalledTimes(2)
-
-      utils.setPixelRatio(mockRenderer, 1)
-      expect(setPixelRatioSpy).toBeCalledTimes(2)
-    })
-    it('does not throw if passed a "renderer" without a `setPixelRatio` method', () => {
-      const mockSVGRenderer = {}
-      expect(() => utils.setPixelRatio(mockSVGRenderer, 2)).not.toThrow()
-    })
-    it('calls `setPixelRatio` even if passed a "renderer" without a `getPixelRatio` method', () => {
-      const mockSVGRenderer = { setPixelRatio: () => {} }
-      const setPixelRatioSpy = vi.spyOn(mockSVGRenderer, 'setPixelRatio')
-      expect(() => utils.setPixelRatio(mockSVGRenderer, 2)).not.toThrow()
-      expect(setPixelRatioSpy).toBeCalledWith(2)
-      expect(setPixelRatioSpy).toBeCalledTimes(1)
-
-      utils.setPixelRatio(mockSVGRenderer, 1.99)
-      expect(setPixelRatioSpy).toBeCalledWith(1.99)
-      expect(setPixelRatioSpy).toBeCalledTimes(2)
-
-      utils.setPixelRatio(mockSVGRenderer, 2.1)
-      expect(setPixelRatioSpy).toBeCalledWith(2.1)
-      expect(setPixelRatioSpy).toBeCalledTimes(3)
-    })
-  })
-
-  describe('setPixelRatio(renderer: WebGLRenderer, systemDpr: number, userDpr: number)', () => {
-    it('calls the renderer\'s setPixelRatio method with userDpr', () => {
-      expect(setPixelRatioSpy).not.toBeCalled()
-      utils.setPixelRatio(mockRenderer, 2, 100)
-      expect(setPixelRatioSpy).toBeCalledWith(100)
-    })
-    it('does not call the renderer\'s setPixelRatio method if current dpr === new dpr', () => {
-      expect(setPixelRatioSpy).not.toBeCalled()
-      utils.setPixelRatio(mockRenderer, 2, 1)
-      expect(setPixelRatioSpy).not.toBeCalledWith()
-
-      utils.setPixelRatio(mockRenderer, 3, 1.4)
-      expect(setPixelRatioSpy).toBeCalledTimes(1)
-      expect(setPixelRatioSpy).toBeCalledWith(1.4)
-
-      utils.setPixelRatio(mockRenderer, 3, 1.4)
-      expect(setPixelRatioSpy).toBeCalledTimes(1)
-      expect(setPixelRatioSpy).toBeCalledWith(1.4)
-
-      utils.setPixelRatio(mockRenderer, 2, 1.4)
-      expect(setPixelRatioSpy).toBeCalledTimes(1)
-      expect(setPixelRatioSpy).toBeCalledWith(1.4)
-
-      utils.setPixelRatio(mockRenderer, 42, 0.1)
-      expect(setPixelRatioSpy).toBeCalledTimes(2)
-      expect(setPixelRatioSpy).toBeCalledWith(0.1)
-
-      utils.setPixelRatio(mockRenderer, 4, 0.1)
-      expect(setPixelRatioSpy).toBeCalledTimes(2)
-      expect(setPixelRatioSpy).toBeCalledWith(0.1)
-    })
-  })
-
-  describe('setPixelRatio(renderer: WebGLRenderer, systemDpr: number, userDpr: [number, number])', () => {
-    it('clamps systemDpr to userDpr', () => {
-      utils.setPixelRatio(mockRenderer, 2, [0, 4])
-      expect(setPixelRatioSpy).toBeCalledTimes(1)
-      expect(setPixelRatioSpy).toBeCalledWith(2)
-
-      utils.setPixelRatio(mockRenderer, 2, [3, 4])
-      expect(setPixelRatioSpy).toBeCalledTimes(2)
-      expect(setPixelRatioSpy).toBeCalledWith(3)
-
-      utils.setPixelRatio(mockRenderer, 5, [3, 4])
-      expect(setPixelRatioSpy).toBeCalledTimes(3)
-      expect(setPixelRatioSpy).toBeCalledWith(4)
-
-      utils.setPixelRatio(mockRenderer, 100, [3, 4])
-      expect(setPixelRatioSpy).toBeCalledTimes(3)
-      expect(setPixelRatioSpy).toBeCalledWith(4)
-
-      utils.setPixelRatio(mockRenderer, 100, [3.5, 4])
-      expect(setPixelRatioSpy).toBeCalledTimes(3)
-      expect(setPixelRatioSpy).toBeCalledWith(4)
-
-      utils.setPixelRatio(mockRenderer, 100, [3, 6.1])
-      expect(setPixelRatioSpy).toBeCalledTimes(4)
-      expect(setPixelRatioSpy).toBeCalledWith(6.1)
-
-      utils.setPixelRatio(mockRenderer, 1, [2.99, 6.1])
-      expect(setPixelRatioSpy).toBeCalledTimes(5)
-      expect(setPixelRatioSpy).toBeCalledWith(2.99)
-    })
-  })
-})

+ 17 - 320
src/utils/index.ts

@@ -1,279 +1,18 @@
 import type { nodeOps } from 'src/core/nodeOps'
 import type { AttachType, LocalState, TresInstance, TresObject, TresPrimitive } from 'src/types'
-import type { Material, Mesh, Object3D, Texture } from 'three'
+import type { Material, Mesh, Texture } from 'three'
 import type { TresContext } from '../composables/useTresContextProvider'
-import { DoubleSide, MathUtils, MeshBasicMaterial, Scene, Vector3 } from 'three'
-import { HightlightMesh } from '../devtools/highlight'
-import { isCamera, isFunction, isNumber, isString, isTresPrimitive, isUndefined } from './is'
+import { Scene } from 'three'
+import { isBufferGeometry, isFog, isMaterial, isString, isTresCamera, isTresPrimitive, isUndefined } from './is'
+import { filterInPlace } from './array'
 
 export * from './logger'
-export function toSetMethodName(key: string) {
-  return `set${key[0].toUpperCase()}${key.slice(1)}`
-}
-
-export const merge = (target: any, source: any) => {
-  // Iterate through `source` properties and if an `Object` set property to merge of `target` and `source` properties
-  for (const key of Object.keys(source)) {
-    if (source[key] instanceof Object) {
-      Object.assign(source[key], merge(target[key], source[key]))
-    }
-  }
-
-  // Join `target` and modified `source`
-  Object.assign(target || {}, source)
-  return target
-}
-
-const HTML_TAGS
-  = 'html,body,base,head,link,meta,style,title,address,article,aside,footer,'
-    + 'header,hgroup,h1,h2,h3,h4,h5,h6,nav,section,div,dd,dl,dt,figcaption,'
-    + 'figure,picture,hr,img,li,main,ol,p,pre,ul,a,b,abbr,bdi,bdo,br,cite,code,'
-    + 'data,dfn,em,i,kbd,mark,q,rp,rt,ruby,s,samp,small,span,strong,sub,sup,'
-    + 'time,u,var,wbr,area,audio,map,track,video,embed,object,param,source,'
-    + 'canvas,script,noscript,del,ins,caption,col,colgroup,table,thead,tbody,td,'
-    + 'th,tr,button,datalist,fieldset,form,input,label,legend,meter,optgroup,'
-    + 'option,output,progress,select,textarea,details,dialog,menu,'
-    + 'summary,template,blockquote,iframe,tfoot'
-
-export const isHTMLTag = /* #__PURE__ */ makeMap(HTML_TAGS)
-
-export function isDOMElement(obj: any): obj is HTMLElement {
-  return obj && obj.nodeType === 1
-}
-
-export function kebabToCamel(str: string) {
-  return str.replace(/-([a-z])/g, (_, c) => c.toUpperCase())
-}
-
-// CamelCase to kebab-case
-const hyphenateRE = /\B([A-Z])/g
-export function hyphenate(str: string) {
-  return str.replace(hyphenateRE, '-$1').toLowerCase()
-}
-
-export function makeMap(str: string, expectsLowerCase?: boolean): (key: string) => boolean {
-  const map: Record<string, boolean> = Object.create(null)
-  const list: Array<string> = str.split(',')
-  for (let i = 0; i < list.length; i++) {
-    map[list[i]] = true
-  }
-  return expectsLowerCase ? val => !!map[val.toLowerCase()] : val => !!map[val]
-}
-
-export const uniqueBy = <T, K>(array: T[], iteratee: (value: T) => K): T[] => {
-  const seen = new Set<K>()
-  const result: T[] = []
-
-  for (const item of array) {
-    const identifier = iteratee(item)
-    if (!seen.has(identifier)) {
-      seen.add(identifier)
-      result.push(item)
-    }
-  }
-
-  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)
-  }
-}
-
-export function deepEqual(a: any, b: any): boolean {
-  if (isDOMElement(a) && isDOMElement(b)) {
-    const attrsA = a.attributes
-    const attrsB = b.attributes
-
-    if (attrsA.length !== attrsB.length) {
-      return false
-    }
-
-    return Array.from(attrsA).every(({ name, value }) => b.getAttribute(name) === value)
-  }
-  // If both are primitives, return true if they are equal
-  if (a === b) {
-    return true
-  }
-
-  // If either of them is null or not an object, return false
-  if (a === null || typeof a !== 'object' || b === null || typeof b !== 'object') {
-    return false
-  }
-
-  // Get the keys of both objects
-  const keysA = Object.keys(a); const keysB = Object.keys(b)
-
-  // If they have different number of keys, they are not equal
-  if (keysA.length !== keysB.length) {
-    return false
-  }
-
-  // Check each key in A to see if it exists in B and its value is the same in both
-  for (const key of keysA) {
-    if (!keysB.includes(key) || !deepEqual(a[key], b[key])) {
-      return false
-    }
-  }
-
-  return true
-}
-
-export function deepArrayEqual(arr1: any[], arr2: any[]): boolean {
-  // If they're not both arrays, return false
-  if (!Array.isArray(arr1) || !Array.isArray(arr2)) {
-    return false
-  }
-
-  // If they don't have the same length, they're not equal
-  if (arr1.length !== arr2.length) {
-    return false
-  }
-
-  // Check each element of arr1 against the corresponding element of arr2
-  for (let i = 0; i < arr1.length; i++) {
-    if (!deepEqual(arr1[i], arr2[i])) {
-      return false
-    }
-  }
-
-  return true
-}
-
-export function editSceneObject(scene: Scene, objectUuid: string, propertyPath: string[], value: any): void {
-  // Function to recursively find the object by UUID
-  const findObjectByUuid = (node: Object3D): Object3D | undefined => {
-    if (node.uuid === objectUuid) {
-      return node
-    }
-
-    for (const child of node.children) {
-      const found = findObjectByUuid(child)
-      if (found) {
-        return found
-      }
-    }
-
-    return undefined
-  }
-
-  // Find the target object
-  const targetObject = findObjectByUuid(scene)
-  if (!targetObject) {
-    console.warn('Object with UUID not found in the scene.')
-    return
-  }
-
-  // Traverse the property path to get to the desired property
-  let currentProperty: any = targetObject
-  for (let i = 0; i < propertyPath.length - 1; i++) {
-    if (currentProperty[propertyPath[i]] !== undefined) {
-      currentProperty = currentProperty[propertyPath[i]]
-    }
-    else {
-      console.warn(`Property path is not valid: ${propertyPath.join('.')}`)
-      return
-    }
-  }
-
-  // Set the new value
-  const lastProperty = propertyPath[propertyPath.length - 1]
-  if (currentProperty[lastProperty] !== undefined) {
-    currentProperty[lastProperty] = value
-  }
-  else {
-    console.warn(`Property path is not valid: ${propertyPath.join('.')}`)
-  }
-}
-
-export function createHighlightMaterial(): MeshBasicMaterial {
-  return new MeshBasicMaterial({
-    color: 0xA7E6D7, // Highlight color, e.g., yellow
-    transparent: true,
-    opacity: 0.2,
-    depthTest: false, // So the highlight is always visible
-    side: DoubleSide, // To ensure the highlight is visible from all angles
-  })
-}
-let animationFrameId: number | null = null
-export function animateHighlight(highlightMesh: Mesh, startTime: number): void {
-  const currentTime = Date.now()
-  const time = (currentTime - startTime) / 1000 // convert to seconds
-
-  // Pulsing effect parameters
-  const scaleAmplitude = 0.07 // Amplitude of the scale pulsation
-  const pulseSpeed = 2.5 // Speed of the pulsation
-
-  // Calculate the scale factor with a sine function for pulsing effect
-  const scaleFactor = 1 + scaleAmplitude * Math.sin(pulseSpeed * time)
-
-  // Apply the scale factor
-  highlightMesh.scale.set(scaleFactor, scaleFactor, scaleFactor)
-
-  // Update the animation frame ID
-  animationFrameId = requestAnimationFrame(() => animateHighlight(highlightMesh, startTime))
-}
-
-export function stopHighlightAnimation(): void {
-  if (animationFrameId !== null) {
-    cancelAnimationFrame(animationFrameId)
-    animationFrameId = null
-  }
-}
-
-export function createHighlightMesh(object: TresObject): Mesh {
-  const highlightMaterial = new MeshBasicMaterial({
-    color: 0xA7E6D7, // Highlight color, e.g., yellow
-    transparent: true,
-    opacity: 0.2,
-    depthTest: false, // So the highlight is always visible
-    side: DoubleSide, // To e
-  })
-  // Clone the geometry of the object. You might need a more complex approach
-  // if the object's geometry is not straightforward.
-  const highlightMesh = new HightlightMesh(object.geometry.clone(), highlightMaterial)
-
-  return highlightMesh
-}
-
-export function extractBindingPosition(binding: any): Vector3 {
-  let observer = binding.value
-  if (binding.value && binding.value?.isMesh) {
-    observer = binding.value.position
-  }
-  if (Array.isArray(binding.value)) { observer = new Vector3(...observer) }
-  return observer
-}
-
-function hasMap(material: Material): material is Material & { map: Texture | null } {
-  return 'map' in material
-}
 
 export function disposeMaterial(material: Material): void {
-  if (hasMap(material) && material.map) {
+  const hasMap = (material: Material): material is Material & { map: Texture } =>
+    'map' in material && !!material.map
+
+  if (hasMap(material)) {
     material.map.dispose()
   }
 
@@ -310,23 +49,6 @@ export function disposeObject3D(object: TresObject): void {
   }
 }
 
-/**
- * Like Array.filter, but modifies the array in place.
- * @param array - Array to modify
- * @param callbackFn - A function called for each element of the array. It should return a truthy value to keep the element in the array.
- */
-export function filterInPlace<T>(array: T[], callbackFn: (element: T, index: number) => unknown) {
-  let i = 0
-  for (let ii = 0; ii < array.length; ii++) {
-    if (callbackFn(array[ii], ii)) {
-      array[i] = array[ii]
-      i++
-    }
-  }
-  array.length = i
-  return array
-}
-
 export function resolve(obj: Record<string, any>, key: string) {
   let target = obj
   if (key.includes('-')) {
@@ -352,10 +74,10 @@ function joinAsCamelCase(...strings: string[]): string {
   return strings.map((s, i) => i === 0 ? s : s.charAt(0).toUpperCase() + s.slice(1)).join('')
 }
 
-// Checks if a dash-cased string ends with an integer
-const INDEX_REGEX = /-\d+$/
-
 export function attach(parent: TresInstance, child: TresInstance, type: AttachType) {
+  // Checks if a dash-cased string ends with an integer
+  const INDEX_REGEX = /-\d+$/
+
   if (isString(type)) {
     // NOTE: If attaching into an array (foo-0), create one
     if (INDEX_REGEX.test(type)) {
@@ -423,9 +145,9 @@ export function prepareTresInstance<T extends TresObject>(obj: T, state: Partial
   }
 
   if (!instance.__tres.attach) {
-    if (instance.isMaterial) { instance.__tres.attach = 'material' }
-    else if (instance.isBufferGeometry) { instance.__tres.attach = 'geometry' }
-    else if (instance.isFog) { instance.__tres.attach = 'fog' }
+    if (isMaterial(instance)) { instance.__tres.attach = 'material' }
+    else if (isBufferGeometry(instance)) { instance.__tres.attach = 'geometry' }
+    else if (isFog(instance)) { instance.__tres.attach = 'fog' }
   }
 
   return instance
@@ -441,31 +163,6 @@ export function invalidateInstance(instance: TresObject) {
   }
 }
 
-export function noop(fn: string): any {
-  // eslint-disable-next-line ts/no-unused-expressions
-  fn
-}
-
-export function setPixelRatio(renderer: { setPixelRatio?: (dpr: number) => void, getPixelRatio?: () => number }, systemDpr: number, userDpr?: number | [number, number]) {
-  // NOTE: Optional `setPixelRatio` allows this function to accept
-  // THREE renderers like SVGRenderer.
-  if (!isFunction(renderer.setPixelRatio)) { return }
-
-  let newDpr = 0
-
-  if (userDpr && Array.isArray(userDpr) && userDpr.length >= 2) {
-    const [min, max] = userDpr
-    newDpr = MathUtils.clamp(systemDpr, min, max)
-  }
-  else if (isNumber(userDpr)) { newDpr = userDpr }
-  else { newDpr = systemDpr }
-
-  // NOTE: Don't call `setPixelRatio` unless both:
-  // - the dpr value has changed
-  // - the renderer has `setPixelRatio`; this check allows us to pass any THREE renderer
-  if (newDpr !== renderer.getPixelRatio?.()) { renderer.setPixelRatio(newDpr) }
-}
-
 export function setPrimitiveObject(
   newObject: TresObject,
   primitive: TresPrimitive,
@@ -564,13 +261,13 @@ export function doRemoveDeregister(node: TresObject, context: TresContext) {
   // TODO: Refactor as `context.deregister`?
   // That would eliminate `context.deregisterCamera`.
   node.traverse?.((child: TresObject) => {
-    if (isCamera(child)) {
+    if (isTresCamera(child)) {
       context.camera.deregisterCamera(child)
     }
   })
 
-  if (isCamera(node)) {
+  if (isTresCamera(node)) {
     context.camera.deregisterCamera(node)
   }
-  invalidateInstance(node as TresObject)
+  invalidateInstance(node)
 }

+ 0 - 368
src/utils/is.ts

@@ -1,368 +0,0 @@
-import type { TresCamera, TresInstance, TresObject, TresPrimitive } from 'src/types'
-import type { BufferGeometry, Color, ColorRepresentation, Fog, Light, Material, Mesh, Object3D, OrthographicCamera, PerspectiveCamera, Scene } from 'three'
-import { Layers } from 'three'
-
-/**
- * Type guard to check if a value is undefined
- * @param value - The value to check
- * @returns True if the value is undefined, false otherwise
- * @example
- * ```ts
- * const value = undefined
- * if (isUndefined(value)) {
- *   // TypeScript knows value is undefined here
- * }
- * ```
- */
-export function isUndefined(value: unknown): value is undefined {
-  return typeof value === 'undefined'
-}
-
-/**
- * Type guard to check if a value is an array
- * @param value - The value to check
- * @returns True if the value is an array, false otherwise
- * @example
- * ```ts
- * const value = [1, 2, 3]
- * if (isArray(value)) {
- *   // TypeScript knows value is Array<unknown> here
- *   value.length // OK
- *   value.map(x => x) // OK
- * }
- * ```
- */
-export function isArray(value: unknown): value is Array<unknown> {
-  return Array.isArray(value)
-}
-
-/**
- * Type guard to check if a value is a number
- * @param value - The value to check
- * @returns True if the value is a number (including NaN and Infinity), false otherwise
- * @example
- * ```ts
- * const value = 42
- * if (isNumber(value)) {
- *   // TypeScript knows value is number here
- *   value.toFixed(2) // OK
- *   value * 2 // OK
- * }
- * ```
- */
-export function isNumber(value: unknown): value is number {
-  return typeof value === 'number'
-}
-
-/**
- * Type guard to check if a value is a string
- * @param value - The value to check
- * @returns True if the value is a string, false otherwise
- * @example
- * ```ts
- * const value = "hello"
- * if (isString(value)) {
- *   // TypeScript knows value is string here
- *   value.length // OK
- *   value.toUpperCase() // OK
- * }
- * ```
- */
-export function isString(value: unknown): value is string {
-  return typeof value === 'string'
-}
-
-/**
- * Type guard to check if a value is a boolean
- * @param value - The value to check
- * @returns True if the value is strictly true or false, false otherwise
- * @example
- * ```ts
- * const value = true
- * if (isBoolean(value)) {
- *   // TypeScript knows value is boolean here
- *   !value // OK
- *   value && true // OK
- * }
- * ```
- */
-export function isBoolean(value: unknown): value is boolean {
-  return value === true || value === false
-}
-
-/**
- * Type guard to check if a value is a function
- * @param value - The value to check
- * @returns True if the value is a callable function, false otherwise
- * @example
- * ```ts
- * const value = () => {}
- * if (isFunction(value)) {
- *   // TypeScript knows value is (...args: any[]) => any here
- *   value() // OK
- *   value.call(null) // OK
- * }
- * ```
- */
-export function isFunction(value: unknown): value is (...args: any[]) => any {
-  return typeof value === 'function'
-}
-
-/**
- * Type guard to check if a value is a plain object
- * @param value - The value to check
- * @returns True if the value is a plain object (not null, array, or function), false otherwise
- * @example
- * ```ts
- * const value = { key: 'value' }
- * if (isObject(value)) {
- *   // TypeScript knows value is Record<string | number | symbol, unknown> here
- *   Object.keys(value) // OK
- *   value.key // OK
- * }
- * ```
- */
-export function isObject(value: unknown): value is Record<string | number | symbol, unknown> {
-  return value === Object(value) && !isArray(value) && !isFunction(value)
-}
-
-/**
- * Type guard to check if a value is a Three.js Object3D
- * @param value - The value to check
- * @returns True if the value is a Three.js Object3D instance, false otherwise
- * @example
- * ```ts
- * const value = new THREE.Object3D()
- * if (isObject3D(value)) {
- *   // TypeScript knows value is Object3D here
- *   value.position // OK
- *   value.rotation // OK
- *   value.scale // OK
- * }
- * ```
- */
-export function isObject3D(value: unknown): value is Object3D {
-  return isObject(value) && !!(value.isObject3D)
-}
-
-export function isMesh(value: unknown): value is Mesh {
-  return isObject3D(value) && 'isMesh' in value && !!(value.isMesh)
-}
-
-/**
- * Type guard to check if a value is a Three.js Camera
- * @param value - The value to check
- * @returns True if the value is a Three.js Camera instance, false otherwise
- * @example
- * ```ts
- * const value = new THREE.PerspectiveCamera()
- * if (isCamera(value)) {
- *   // TypeScript knows value is Camera here
- *   value.fov // OK
- *   value.near // OK
- *   value.far // OK
- * }
- * ```
- */
-export function isCamera(value: unknown): value is TresCamera {
-  return isObject(value) && !!(value.isCamera)
-}
-
-export function isColor(value: unknown): value is Color {
-  return isObject(value) && !!(value.isColor)
-}
-
-export function isColorRepresentation(value: unknown): value is ColorRepresentation {
-  return value != null && (typeof value === 'string' || typeof value === 'number' || isColor(value))
-}
-
-interface VectorLike { set: (...args: any[]) => void, constructor?: (...args: any[]) => any }
-export function isVectorLike(value: unknown): value is VectorLike {
-  return value !== null && typeof value === 'object' && 'set' in value && typeof value.set === 'function'
-}
-
-interface Copyable { copy: (...args: any[]) => void, constructor?: (...args: any[]) => any }
-export function isCopyable(value: unknown): value is Copyable {
-  return isVectorLike(value) && 'copy' in value && typeof value.copy === 'function'
-}
-
-interface ClassInstance { constructor?: (...args: any[]) => any }
-export function isClassInstance(object: unknown): object is ClassInstance {
-  return !!(object)?.constructor
-}
-
-export function isLayers(value: unknown): value is Layers {
-  return value instanceof Layers // three does not implement .isLayers
-}
-
-/**
- * Type guard to check if a value is a Three.js OrthographicCamera
- * @param value - The value to check
- * @returns True if the value is a Three.js OrthographicCamera instance, false otherwise
- */
-export function isOrthographicCamera(value: unknown): value is OrthographicCamera {
-  return isObject(value) && !!(value.isOrthographicCamera)
-}
-
-/**
- * Type guard to check if a value is a Three.js PerspectiveCamera
- * @param value - The value to check
- * @returns True if the value is a Three.js PerspectiveCamera instance, false otherwise
- */
-export function isPerspectiveCamera(value: unknown): value is PerspectiveCamera {
-  return isObject(value) && !!(value.isPerspectiveCamera)
-}
-
-/**
- * Type guard to check if a value is a Three.js BufferGeometry
- * @param value - The value to check
- * @returns True if the value is a Three.js BufferGeometry instance, false otherwise
- * @example
- * ```ts
- * const value = new THREE.BufferGeometry()
- * if (isBufferGeometry(value)) {
- *   // TypeScript knows value is BufferGeometry here
- *   value.attributes // OK
- *   value.index // OK
- *   value.computeVertexNormals() // OK
- * }
- * ```
- */
-export function isBufferGeometry(value: unknown): value is BufferGeometry {
-  return isObject(value) && !!(value.isBufferGeometry)
-}
-
-/**
- * Type guard to check if a value is a Three.js Material
- * @param value - The value to check
- * @returns True if the value is a Three.js Material instance, false otherwise
- * @example
- * ```ts
- * const value = new THREE.MeshStandardMaterial()
- * if (isMaterial(value)) {
- *   // TypeScript knows value is Material here
- *   value.color // OK
- *   value.metalness // OK
- *   value.roughness // OK
- * }
- * ```
- */
-export function isMaterial(value: unknown): value is Material {
-  return isObject(value) && !!(value.isMaterial)
-}
-
-/**
- * Type guard to check if a value is a Three.js Light
- * @param value - The value to check
- * @returns True if the value is a Three.js Light instance, false otherwise
- * @example
- * ```ts
- * const value = new THREE.DirectionalLight()
- * if (isLight(value)) {
- *   // TypeScript knows value is Light here
- *   value.intensity // OK
- *   value.color // OK
- *   value.position // OK
- * }
- * ```
- */
-export function isLight(value: unknown): value is Light {
-  return isObject(value) && !!(value.isLight)
-}
-
-/**
- * Type guard to check if a value is a Three.js Fog
- * @param value - The value to check
- * @returns True if the value is a Three.js Fog instance, false otherwise
- * @example
- * ```ts
- * const value = new THREE.Fog(0x000000, 1, 1000)
- * if (isFog(value)) {
- *   // TypeScript knows value is Fog here
- *   value.color // OK
- *   value.near // OK
- *   value.far // OK
- * }
- * ```
- */
-export function isFog(value: unknown): value is Fog {
-  return isObject(value) && !!(value.isFog)
-}
-
-/**
- * Type guard to check if a value is a Three.js Scene
- * @param value - The value to check
- * @returns True if the value is a Three.js Scene instance, false otherwise
- * @example
- * ```ts
- * const value = new THREE.Scene()
- * if (isScene(value)) {
- *   // TypeScript knows value is Scene here
- *   value.children // OK
- *   value.add(new THREE.Object3D()) // OK
- *   value.remove(new THREE.Object3D()) // OK
- * }
- * ```
- */
-export function isScene(value: unknown): value is Scene {
-  return isObject(value) && !!(value.isScene)
-}
-
-/**
- * Type guard to check if a value is a TresObject
- * @param value - The value to check
- * @returns True if the value is a TresObject (Object3D | BufferGeometry | Material | Fog), false otherwise
- * @example
- * ```ts
- * const value = new THREE.Mesh()
- * if (isTresObject(value)) {
- *   // TypeScript knows value is TresObject here
- *   // You can use common properties and methods shared by all TresObjects
- * }
- * ```
- * @remarks
- * TresObject is a union type that represents the core Three.js objects that can be used in TresJS.
- * This includes Object3D, BufferGeometry, Material, and Fog instances.
- */
-export function isTresObject(value: unknown): value is TresObject {
-  // NOTE: TresObject is currently defined as
-  // TresObject3D | THREE.BufferGeometry | THREE.Material | THREE.Fog
-  return isObject3D(value) || isBufferGeometry(value) || isMaterial(value) || isFog(value)
-}
-
-/**
- * Type guard to check if a value is a TresPrimitive
- * @param value - The value to check
- * @returns True if the value is a TresPrimitive instance, false otherwise
- * @example
- * ```ts
- * const value = { isPrimitive: true }
- * if (isTresPrimitive(value)) {
- *   // TypeScript knows value is TresPrimitive here
- *   // You can use properties and methods specific to TresPrimitives
- * }
- * ```
- * @remarks
- * TresPrimitive is a special type in TresJS that represents primitive objects
- * that can be used directly in the scene without needing to be wrapped in a Three.js object.
- */
-export function isTresPrimitive(value: unknown): value is TresPrimitive {
-  return isObject(value) && !!(value.isPrimitive)
-}
-
-/**
- * Type guard to check if a value is a TresInstance (has __tres property)
- * @param value - The value to check
- * @returns True if the value is a TresInstance (has __tres property), false otherwise
- * @example
- * ```ts
- * const value = new THREE.Mesh()
- * if (isTresInstance(value)) {
- *   // TypeScript knows value is TresInstance here
- *   // You can safely access value.__tres
- * }
- * ```
- */
-export function isTresInstance(value: unknown): value is TresInstance {
-  return isTresObject(value) && '__tres' in value
-}

+ 14 - 0
src/utils/is/dom.ts

@@ -0,0 +1,14 @@
+import { makeMap } from '../makeMap'
+
+const HTML_TAGS
+  = 'html,body,base,head,link,meta,style,title,address,article,aside,footer,'
+    + 'header,hgroup,h1,h2,h3,h4,h5,h6,nav,section,div,dd,dl,dt,figcaption,'
+    + 'figure,picture,hr,img,li,main,ol,p,pre,ul,a,b,abbr,bdi,bdo,br,cite,code,'
+    + 'data,dfn,em,i,kbd,mark,q,rp,rt,ruby,s,samp,small,span,strong,sub,sup,'
+    + 'time,u,var,wbr,area,audio,map,track,video,embed,object,param,source,'
+    + 'canvas,script,noscript,del,ins,caption,col,colgroup,table,thead,tbody,td,'
+    + 'th,tr,button,datalist,fieldset,form,input,label,legend,meter,optgroup,'
+    + 'option,output,progress,select,textarea,details,dialog,menu,'
+    + 'summary,template,blockquote,iframe,tfoot'
+
+export const isHTMLTag = /* #__PURE__ */ makeMap(HTML_TAGS)

+ 4 - 0
src/utils/is/index.ts

@@ -0,0 +1,4 @@
+export * from './dom'
+export * from './three'
+export * from './tres'
+export * from './typed'

+ 2 - 8
src/utils/is.test.ts → src/utils/is/is.test.ts

@@ -1,7 +1,8 @@
 import type { Camera, Light, Material, Object3D } from 'three'
 import { AmbientLight, BufferGeometry, DirectionalLight, Fog, Group, Mesh, MeshBasicMaterial, MeshNormalMaterial, OrthographicCamera, PerspectiveCamera, PointLight, Scene } from 'three'
-import { isArray, isBoolean, isBufferGeometry, isCamera, isFog, isFunction, isLight, isMaterial, isNumber, isObject, isObject3D, isScene, isString, isTresObject, isUndefined } from './is'
+import { isBufferGeometry, isCamera, isFog, isLight, isMaterial, isObject3D, isScene, isTresObject } from '../is/index'
 
+// TODO move file
 const NUMBERS: Record<string, number> = {
   '0': 0,
   '1': 1,
@@ -94,13 +95,6 @@ const TRES_OBJECTS = Object.assign({}, MATERIALS, OBJECT3DS, BUFFER_GEOMETRIES,
 const ALL = Object.assign({}, NUMBERS, BOOLEANS, STRINGS, NULL, UNDEFINED, ARRAYS, FUNCTIONS, OBJECTS)
 
 describe('is', () => {
-  describe('isUndefined(a: any)', () => { test(isUndefined, UNDEFINED) })
-  describe('isArray(a: any)', () => { test(isArray, ARRAYS) })
-  describe('isNumber(a: any)', () => { test(isNumber, NUMBERS) })
-  describe('isString(a: any)', () => { test(isString, STRINGS) })
-  describe('isBoolean(a: any)', () => { test(isBoolean, BOOLEANS) })
-  describe('isFunction(a: any)', () => { test(isFunction, FUNCTIONS) })
-  describe('isObject(a: any)', () => { test(isObject, OBJECTS) })
   describe('isObject3D(a: any)', () => { test(isObject3D, OBJECT3DS) })
   describe('isCamera(a: any)', () => { test(isCamera, CAMERAS) })
   describe('isBufferGeometry(a: any)', () => { test(isBufferGeometry, BUFFER_GEOMETRIES) })

+ 180 - 0
src/utils/is/three.ts

@@ -0,0 +1,180 @@
+import type { BufferGeometry, Camera, Color, ColorRepresentation, Fog, Group, Light, Material, Mesh, Object3D, OrthographicCamera, PerspectiveCamera, Scene } from 'three'
+import { Layers } from 'three'
+import { isNumber, isString } from './typed'
+import { createTypeGuard } from './util'
+
+/**
+ * Type guard to check if a value is a Three.js Object3D
+ * @param value - The value to check
+ * @returns True if the value is a Three.js Object3D instance, false otherwise
+ * @example
+ * ```ts
+ * const value = new THREE.Object3D()
+ * if (isObject3D(value)) {
+ *   // TypeScript knows value is Object3D here
+ *   value.position // OK
+ *   value.rotation // OK
+ *   value.scale // OK
+ * }
+ * ```
+ */
+export const isObject3D = createTypeGuard<Object3D>('isObject3D')
+
+/**
+ * Type guard to check if a value is a Three.js Mesh
+ * @param value - The value to check
+ * @returns True if the value is a Three.js Mesh instance, false otherwise
+ * @example
+ * ```ts
+ * const value = new THREE.Mesh()
+ * if (isMesh(value)) {
+ *   // TypeScript knows value is Mesh here
+ *   value.geometry // OK
+ */
+export const isMesh = createTypeGuard<Mesh>('isMesh')
+
+/**
+ * Type guard to check if a value is a Three.js Camera
+ * @param value - The value to check
+ * @returns True if the value is a Three.js Camera instance, false otherwise
+ * @example
+ * ```ts
+ * const value = new THREE.PerspectiveCamera()
+ * if (isCamera(value)) {
+ *   // TypeScript knows value is Camera here
+ *   value.fov // OK
+ *   value.near // OK
+ *   value.far // OK
+ * }
+ * ```
+ */
+export const isCamera = createTypeGuard<Camera>('isCamera')
+
+/**
+ * Type guard to check if a value is a Three.js OrthographicCamera
+ * @param value - The value to check
+ * @returns True if the value is a Three.js OrthographicCamera instance, false otherwise
+ */
+export const isOrthographicCamera = createTypeGuard<OrthographicCamera>('isOrthographicCamera')
+
+/**
+ * Type guard to check if a value is a Three.js PerspectiveCamera
+ * @param value - The value to check
+ * @returns True if the value is a Three.js PerspectiveCamera instance, false otherwise
+ */
+export const isPerspectiveCamera = createTypeGuard<PerspectiveCamera>('isPerspectiveCamera')
+
+/**
+ * Type guard to check if a value is a Three.js Color
+ * @param value - The value to check
+ * @returns True if the value is a Three.js Color instance, false otherwise
+ */
+export const isColor = createTypeGuard<Color>('isColor')
+
+/**
+ * Type guard to check if a value is a Three.js ColorRepresentation
+ * @param value - The value to check
+ * @returns True if the value is a Three.js ColorRepresentation instance, false otherwise
+ */
+export const isColorRepresentation = (value: unknown): value is ColorRepresentation =>
+  isString(value) || isNumber(value) || isColor(value)
+
+/**
+ * Type guard to check if a value is a Three.js Layers
+ * @param value - The value to check
+ * @returns True if the value is a Three.js Layers instance, false otherwise
+ */
+export const isLayers = (value: unknown): value is Layers => value instanceof Layers // three does not implement .isLayers
+
+/**
+ * Type guard to check if a value is a Three.js BufferGeometry
+ * @param value - The value to check
+ * @returns True if the value is a Three.js BufferGeometry instance, false otherwise
+ * @example
+ * ```ts
+ * const value = new THREE.BufferGeometry()
+ * if (isBufferGeometry(value)) {
+ *   // TypeScript knows value is BufferGeometry here
+ *   value.attributes // OK
+ *   value.index // OK
+ *   value.computeVertexNormals() // OK
+ * }
+ * ```
+ */
+export const isBufferGeometry = createTypeGuard<BufferGeometry>('isBufferGeometry')
+
+/**
+ * Type guard to check if a value is a Three.js Material
+ * @param value - The value to check
+ * @returns True if the value is a Three.js Material instance, false otherwise
+ * @example
+ * ```ts
+ * const value = new THREE.MeshStandardMaterial()
+ * if (isMaterial(value)) {
+ *   // TypeScript knows value is Material here
+ *   value.color // OK
+ *   value.metalness // OK
+ *   value.roughness // OK
+ * }
+ * ```
+ */
+export const isMaterial = createTypeGuard<Material>('isMaterial')
+
+/**
+ * Type guard to check if a value is a Three.js Light
+ * @param value - The value to check
+ * @returns True if the value is a Three.js Light instance, false otherwise
+ * @example
+ * ```ts
+ * const value = new THREE.DirectionalLight()
+ * if (isLight(value)) {
+ *   // TypeScript knows value is Light here
+ *   value.intensity // OK
+ *   value.color // OK
+ *   value.position // OK
+ * }
+ * ```
+ */
+export const isLight = createTypeGuard<Light>('isLight')
+
+/**
+ * Type guard to check if a value is a Three.js Fog
+ * @param value - The value to check
+ * @returns True if the value is a Three.js Fog instance, false otherwise
+ * @example
+ * ```ts
+ * const value = new THREE.Fog(0x000000, 1, 1000)
+ * if (isFog(value)) {
+ *   // TypeScript knows value is Fog here
+ *   value.color // OK
+ *   value.near // OK
+ *   value.far // OK
+ * }
+ * ```
+ */
+export const isFog = createTypeGuard<Fog>('isFog')
+
+/**
+ * Type guard to check if a value is a Three.js Scene
+ * @param value - The value to check
+ * @returns True if the value is a Three.js Scene instance, false otherwise
+ * @example
+ * ```ts
+ * const value = new THREE.Scene()
+ * if (isScene(value)) {
+ *   // TypeScript knows value is Scene here
+ *   value.children // OK
+ *   value.add(new THREE.Object3D()) // OK
+ *   value.remove(new THREE.Object3D()) // OK
+ * }
+ * ```
+ */
+export const isScene = createTypeGuard<Scene>('isScene')
+
+/**
+ * Type guard to check if a value is a Three.js Group
+ * @param value - The value to check
+ * @returns True if the value is a Three.js Group instance, false otherwise
+ * ```
+ */
+export const isGroup = createTypeGuard<Group>('isGroup')

+ 77 - 0
src/utils/is/tres.ts

@@ -0,0 +1,77 @@
+import type { TresCamera, TresInstance, TresObject, TresPrimitive } from 'src/types'
+import { isBufferGeometry, isCamera, isFog, isMaterial, isObject3D, isOrthographicCamera, isPerspectiveCamera } from './three'
+import { createTypeGuard } from './util'
+
+interface VectorLike { set: (...args: any[]) => void, constructor?: (...args: any[]) => any }
+export const isVectorLike = (value: unknown): value is VectorLike =>
+  value !== null && typeof value === 'object' && 'set' in value && typeof value.set === 'function'
+
+interface Copyable { copy: (...args: any[]) => void, constructor?: (...args: any[]) => any }
+export const isCopyable = (value: unknown): value is Copyable =>
+  isVectorLike(value) && 'copy' in value && typeof value.copy === 'function'
+
+interface ClassInstance { constructor?: (...args: any[]) => any }
+export const isClassInstance = (object: unknown): object is ClassInstance =>
+  !!(object)?.constructor
+
+/**
+ * Type guard to check if a value is a TresCamera
+ * @param value - The value to check
+ * @returns True if the value is a TresCamera instance, false otherwise
+ */
+export const isTresCamera = (value: unknown): value is TresCamera => isCamera(value) || isOrthographicCamera(value) || isPerspectiveCamera(value)
+
+/**
+ * Type guard to check if a value is a TresObject
+ * @param value - The value to check
+ * @returns True if the value is a TresObject (Object3D | BufferGeometry | Material | Fog), false otherwise
+ * @example
+ * ```ts
+ * const value = new THREE.Mesh()
+ * if (isTresObject(value)) {
+ *   // TypeScript knows value is TresObject here
+ *   // You can use common properties and methods shared by all TresObjects
+ * }
+ * ```
+ * @remarks
+ * TresObject is a union type that represents the core Three.js objects that can be used in TresJS.
+ * This includes Object3D, BufferGeometry, Material, and Fog instances.
+ */
+export const isTresObject = (value: unknown): value is TresObject =>
+  // NOTE: TresObject is currently defined as
+  // TresObject3D | THREE.BufferGeometry | THREE.Material | THREE.Fog
+  isObject3D(value) || isBufferGeometry(value) || isMaterial(value) || isFog(value)
+
+/**
+ * Type guard to check if a value is a TresPrimitive
+ * @param value - The value to check
+ * @returns True if the value is a TresPrimitive instance, false otherwise
+ * @example
+ * ```ts
+ * const value = { isPrimitive: true }
+ * if (isTresPrimitive(value)) {
+ *   // TypeScript knows value is TresPrimitive here
+ *   // You can use properties and methods specific to TresPrimitives
+ * }
+ * ```
+ * @remarks
+ * TresPrimitive is a special type in TresJS that represents primitive objects
+ * that can be used directly in the scene without needing to be wrapped in a Three.js object.
+ */
+export const isTresPrimitive = createTypeGuard<TresPrimitive>('isPrimitive')
+
+/**
+ * Type guard to check if a value is a TresInstance (has __tres property)
+ * @param value - The value to check
+ * @returns True if the value is a TresInstance (has __tres property), false otherwise
+ * @example
+ * ```ts
+ * const value = new THREE.Mesh()
+ * if (isTresInstance(value)) {
+ *   // TypeScript knows value is TresInstance here
+ *   // You can safely access value.__tres
+ * }
+ * ```
+ */
+export const isTresInstance = (value: unknown): value is TresInstance =>
+  isTresObject(value) && '__tres' in value

+ 1 - 0
src/utils/is/typed.ts

@@ -0,0 +1 @@
+export { isBoolean, isEqual, isFunction, isNumber, isObject, isString, isUndefined } from 'radashi'

+ 5 - 0
src/utils/is/util.ts

@@ -0,0 +1,5 @@
+import { isObject } from './typed'
+
+export const createTypeGuard = <T>(property: keyof T) =>
+  (value: unknown): value is T =>
+    isObject(value) && property in value && !!((value as T)[property])

+ 15 - 0
src/utils/makeMap.ts

@@ -0,0 +1,15 @@
+// This file is from taken from Vue.js: https://github.com/vuejs/core/blob/main/packages/shared/src/makeMap.ts
+/**
+ * Make a map and return a function for checking if a key
+ * is in that map.
+ * IMPORTANT: all calls of this function must be prefixed with
+ * \/\*#\_\_PURE\_\_\*\/
+ * So that rollup can tree-shake them if necessary.
+ */
+
+/*! #__NO_SIDE_EFFECTS__ */
+export function makeMap(str: string): (key: string) => boolean {
+  const map = Object.create(null)
+  for (const key of str.split(',')) { map[key] = 1 }
+  return val => val in map
+}

+ 2 - 1
src/utils/perf.ts

@@ -1,11 +1,12 @@
 import type { Scene } from 'three'
 import type { TresObject } from './../types'
+import { isMesh } from './is'
 
 export function calculateMemoryUsage(object: TresObject | Scene) {
   let totalMemory = 0
 
   object.traverse((node: TresObject) => {
-    if (node.isMesh && node.geometry && node.type !== 'HightlightMesh') {
+    if (isMesh(node) && node.type !== 'HightlightMesh') {
       const geometry = node.geometry
       const verticesMemory = geometry.attributes.position.count * 3 * Float32Array.BYTES_PER_ELEMENT
       const facesMemory = geometry.index ? geometry.index.count * Uint32Array.BYTES_PER_ELEMENT : 0

+ 1 - 0
src/utils/string.ts

@@ -0,0 +1 @@
+export { camel } from 'radashi'

+ 23 - 0
src/utils/three.ts

@@ -0,0 +1,23 @@
+import type { Object3D } from 'three'
+
+/**
+ * Recursively searches for an Object3D with the specified UUID within a Three.js scene graph.
+ *
+ * @param node - The root Object3D to start searching from
+ * @param uuid - The unique identifier of the object to find
+ * @returns The Object3D with the matching UUID, or undefined if not found
+ */
+export const getObjectByUuid = (node: Object3D, uuid: string): Object3D | undefined => {
+  if (node.uuid === uuid) {
+    return node
+  }
+
+  for (const child of node.children) {
+    const found = getObjectByUuid(child, uuid)
+    if (found) {
+      return found
+    }
+  }
+
+  return undefined
+}