+export type AttachFnType = (parent: any, self: TresInstance) => () => void
+export type AttachType = string | AttachFnType
export type ConstructorRepresentation = new (...args: any[]) => any
export type NonFunctionKeys = { [K in keyof P]-?: P[K] extends Function ? never : K }[keyof P]
@@ -29,12 +29,10 @@ export interface InstanceProps {
object?: T
visible?: boolean
dispose?: null
- attach?: AttachType
[prop: string]: any
}
interface TresBaseObject {
- attach?: string
removeFromParent?: () => void
dispose?: () => void
[prop: string]: any // for arbitrary properties
@@ -58,6 +56,8 @@ export interface LocalState {
primitive?: boolean
disposable?: boolean
+ attach?: AttachType
+ previousAttach: any
}
// Custom type for geometry and material properties in Object3D
diff --git a/src/utils/index.test.ts b/src/utils/index.test.ts
index e39f822a7..861e961c7 100644
--- a/src/utils/index.test.ts
+++ b/src/utils/index.test.ts
@@ -60,3 +60,78 @@ function shuffle(array: any[]) {
}
return array
};
+
+describe('resolve', () => {
+ it('returns the first argument if it contains the key', () => {
+ const instance = { ab: 0 }
+ const { target, key } = utils.resolve(instance, 'ab')
+ expect(target).toBe(instance)
+ expect(key).toBe('ab')
+ })
+ it('splits the key by "-" and traverses the obj using the pieces', () => {
+ const instance = { ab: { cd: { ef: 0 } } }
+ const { target, key } = utils.resolve(instance, 'ab-cd-ef')
+ expect(target).toBe(instance.ab.cd)
+ expect(key).toBe('ef')
+ })
+ it('returns the current target holding the end of the key, and the end of the key', () => {
+ const instance = { ab: { cd: { ef: { gh: 0 } } } }
+ const { target, key } = utils.resolve(instance, 'ab-cd-ef')
+ expect(target).toBe(instance.ab.cd)
+ expect(key).toBe('ef')
+ })
+ it('joins pierced props as camelCase, non-greedily', () => {
+ {
+ const instance = { abCdEfGh: { ij: 0 } }
+ const { target, key } = utils.resolve(instance, 'ab-cd-ef-gh-ij')
+ expect(target).toBe(instance.abCdEfGh)
+ expect(key).toBe('ij')
+ }
+
+ {
+ const instance = {
+ abCdEfGh: { ij: 0 },
+ abCdEf: { gh: { ij: 0 } },
+ }
+ const { target, key } = utils.resolve(instance, 'ab-cd-ef-gh-ij')
+ expect(target).toBe(instance.abCdEf.gh)
+ expect(key).toBe('ij')
+ }
+
+ {
+ const instance = {
+ abCdEfGh: { ij: 0 },
+ abCd: { ef: { gh: { ij: 0 } }, efGh: { ij: 0 } },
+ abCdEf: { gh: { ij: 0 } },
+ }
+ const { target, key } = utils.resolve(instance, 'ab-cd-ef-gh-ij')
+ expect(target).toBe(instance.abCd.ef.gh)
+ expect(key).toBe('ij')
+ }
+
+ {
+ const instance = {
+ abCdEfGh: { ij: 0 },
+ abCdEf: { ghIj: 0 },
+ ab: { cdEfGhIj: 0 },
+ abCd: { ef: { gh: { ij: 0 } }, efGh: { ij: 0 } },
+ }
+ const { target, key } = utils.resolve(instance, 'ab-cd-ef-gh-ij')
+ expect(target).toBe(instance.ab)
+ expect(key).toBe('cdEfGhIj')
+ }
+ })
+
+ it('joins my-key-and-the-unfindable-suffix as andTheUnfindableSuffix, for key suffixes that do not exist', () => {
+ expect(utils.resolve({}, 'zz').key).toBe('zz')
+ expect(utils.resolve({}, 'ab-cd-ef-gh-ij').key).toBe('abCdEfGhIj')
+
+ const instance = { ab: { cd: { ef: { gh: { ij: 0 } } } } }
+ expect(utils.resolve(instance, 'ab-cd-ef-gh-ij-xx-yy-zz').key).toBe('xxYyZz')
+ expect(utils.resolve(instance, 'xx-yy-zz').key).toBe('xxYyZz')
+ expect(utils.resolve(instance, 'ab-xx-yy-zz').key).toBe('xxYyZz')
+ expect(utils.resolve(instance, 'ab-cd-zz').key).toBe('zz')
+ expect(utils.resolve(instance, 'ab-cd-xx-yy-zz').key).toBe('xxYyZz')
+ expect(utils.resolve(instance, 'ab-cd-xx-yy-zz').key).toBe('xxYyZz')
+ })
+})
diff --git a/src/utils/index.ts b/src/utils/index.ts
index f85c5f04b..bde0fbd4e 100644
--- a/src/utils/index.ts
+++ b/src/utils/index.ts
@@ -1,7 +1,9 @@
import type { Material, Mesh, Object3D, Texture } from 'three'
import { DoubleSide, MeshBasicMaterial, Scene, Vector3 } from 'three'
-import type { TresObject } from 'src/types'
+import type { AttachType, LocalState, TresInstance, TresObject } from 'src/types'
import { HightlightMesh } from '../devtools/highlight'
+import type { TresContext } from '../composables/useTresContextProvider'
+import * as is from './is'
export function toSetMethodName(key: string) {
return `set${key[0].toUpperCase()}${key.slice(1)}`
@@ -330,3 +332,116 @@ export function filterInPlace(array: T[], callbackFn: (element: T, index: num
array.length = i
return array
}
+
+export function resolve(obj: Record, key: string) {
+ let target = obj
+ if (key.includes('-')) {
+ const entries = key.split('-')
+ let currKey = entries.shift() as string
+ while (target && entries.length) {
+ if (!(currKey in target)) {
+ currKey = joinAsCamelCase(currKey, entries.shift() as string)
+ }
+ else {
+ target = target[currKey]
+ currKey = entries.shift() as string
+ }
+ }
+ return { target, key: joinAsCamelCase(currKey, ...entries) }
+ }
+ else {
+ return { target, key }
+ }
+}
+
+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) {
+ if (is.str(type)) {
+ // NOTE: If attaching into an array (foo-0), create one
+ if (INDEX_REGEX.test(type)) {
+ const typeWithoutTrailingIndex = type.replace(INDEX_REGEX, '')
+ const { target, key } = resolve(parent, typeWithoutTrailingIndex)
+ if (!Array.isArray(target[key])) {
+ // NOTE: Create the array and augment it with a function
+ // that resets the original value if the array is empty or
+ // `[undefined, undefined, ...]`. The function will be run
+ // every time an element is `detach`ed from the array.
+ const previousAttach = target[key]
+ const augmentedArray: any[] & { __tresDetach?: () => void } = []
+ augmentedArray.__tresDetach = () => {
+ if (augmentedArray.every(v => is.und(v))) {
+ target[key] = previousAttach
+ }
+ }
+ target[key] = augmentedArray
+ }
+ }
+
+ const { target, key } = resolve(parent, type)
+ child.__tres.previousAttach = target[key]
+ target[key] = child
+ }
+ else {
+ child.__tres.previousAttach = type(parent, child)
+ }
+}
+
+export function detach(parent: any, child: TresInstance, type: AttachType) {
+ if (is.str(type)) {
+ const { target, key } = resolve(parent, type)
+ const previous = child.__tres.previousAttach
+ // When the previous value was undefined, it means the value was never set to begin with
+ if (previous === undefined) {
+ delete target[key]
+ }
+ // NOTE: If the previous value was not an array, and `attach` turned it into an array
+ // then it also set `__tresOnArrayElementsUndefined`. Check for it and revert
+ // Otherwise set the previous value
+ else {
+ target[key] = previous
+ }
+
+ if ('__tresDetach' in target) { target.__tresDetach() }
+ }
+ else {
+ child.__tres?.previousAttach?.(parent, child)
+ }
+ delete child.__tres?.previousAttach
+}
+
+export function prepareTresInstance(obj: T, state: Partial, context: TresContext): TresInstance {
+ const instance = obj as unknown as TresInstance
+ instance.__tres = {
+ type: 'unknown',
+ eventCount: 0,
+ root: context,
+ handlers: {},
+ memoizedProps: {},
+ objects: [],
+ parent: null,
+ previousAttach: null,
+ ...state,
+ }
+ return instance
+}
+
+export function invalidateInstance(instance: TresObject) {
+ const ctx = instance?.__tres?.root
+
+ if (!ctx) { return }
+
+ if (ctx.render && ctx.render.canBeInvalidated.value) {
+ ctx.invalidate()
+ }
+}
+
+export function noop(fn: string): any {
+ // eslint-disable-next-line no-unused-expressions
+ fn
+}
diff --git a/src/utils/is.test.ts b/src/utils/is.test.ts
index f51c055e2..c60a14a0d 100644
--- a/src/utils/is.test.ts
+++ b/src/utils/is.test.ts
@@ -2,6 +2,36 @@ import { BufferGeometry, Fog, MeshBasicMaterial, MeshNormalMaterial, Object3D, P
import * as is from './is'
describe('is', () => {
+ describe('is.und(a: any)', () => {
+ describe('true', () => {
+ it('undefined', () => {
+ assert(is.und(undefined))
+ })
+ })
+ describe('false', () => {
+ it('null', () => {
+ assert(!is.und(null))
+ })
+ it('number', () => {
+ assert(!is.und(0))
+ assert(!is.und(-1))
+ assert(!is.und(Math.PI))
+ assert(!is.und(Number.POSITIVE_INFINITY))
+ assert(!is.und(Number.NEGATIVE_INFINITY))
+ assert(!is.und(42))
+ })
+ it('string', () => {
+ assert(!is.und(''))
+ assert(!is.und('tresObject'))
+ })
+ it('function', () => {
+ assert(!is.und(() => {}))
+ })
+ it('array', () => {
+ assert(!is.und([]))
+ })
+ })
+ })
describe('is.tresObject(a: any)', () => {
describe('true', () => {
it('object3D', () => {
diff --git a/src/utils/is.ts b/src/utils/is.ts
index 0f1a4ef15..67fa0f70c 100644
--- a/src/utils/is.ts
+++ b/src/utils/is.ts
@@ -1,10 +1,18 @@
import type { TresObject } from 'src/types'
-import type { BufferGeometry, Camera, Fog, Material, Object3D } from 'three'
+import type { BufferGeometry, Camera, Fog, Material, Object3D, Scene } from 'three'
+
+export function und(u: unknown) {
+ return typeof u === 'undefined'
+}
export function arr(u: unknown) {
return Array.isArray(u)
}
+export function str(u: unknown): u is string {
+ return typeof u === 'string'
+}
+
export function fun(u: unknown): u is Function {
return typeof u === 'function'
}
@@ -33,6 +41,10 @@ export function fog(u: unknown): u is Fog {
return obj(u) && 'isFog' in u && !!(u.isFog)
}
+export function scene(u: unknown): u is Scene {
+ return obj(u) && 'isScene' in u && !!(u.isScene)
+}
+
export function tresObject(u: unknown): u is TresObject {
// NOTE: TresObject is currently defined as
// TresObject3D | THREE.BufferGeometry | THREE.Material | THREE.Fog
diff --git a/src/utils/nodeOpsUtils.ts b/src/utils/nodeOpsUtils.ts
deleted file mode 100644
index 2c3ada316..000000000
--- a/src/utils/nodeOpsUtils.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-import type { TresContext } from '../composables/useTresContextProvider'
-import type { LocalState, TresInstance, TresObject } from '../types'
-
-export function prepareTresInstance(obj: T, state: Partial, context: TresContext): TresInstance {
- const instance = obj as unknown as TresInstance
- instance.__tres = {
- type: 'unknown',
- eventCount: 0,
- root: context,
- handlers: {},
- memoizedProps: {},
- objects: [],
- parent: null,
- ...state,
- }
- return instance
-}
-
-export function invalidateInstance(instance: TresObject) {
- const ctx = instance?.__tres?.root
-
- if (!ctx) { return }
-
- if (ctx.render && ctx.render.canBeInvalidated.value) {
- ctx.invalidate()
- }
-}
-
-export function noop(fn: string): any {
- // eslint-disable-next-line no-unused-expressions
- fn
-}