diff --git a/src/core/nodeOps.test.ts b/src/core/nodeOps.test.ts index f298a95e9..1e71920ee 100644 --- a/src/core/nodeOps.test.ts +++ b/src/core/nodeOps.test.ts @@ -255,6 +255,55 @@ describe('nodeOps', () => { expect(parent.children.length).toBe(0) } }) + + it('adds a material to parent.__tres.objects', () => { + const parent = nodeOps.createElement('Mesh', undefined, undefined, {}) + const material = nodeOps.createElement('MeshNormalMaterial') + nodeOps.insert(material, parent) + expect(parent.__tres.objects.map(child => child.uuid)).toStrictEqual([material.uuid]) + }) + + it('adds a geometry to parent.__tres.objects', () => { + const parent = nodeOps.createElement('Mesh', undefined, undefined, {}) + const geometry = nodeOps.createElement('BoxGeometry') + nodeOps.insert(geometry, parent) + expect(parent.__tres.objects.map(child => child.uuid)).toStrictEqual([geometry.uuid]) + }) + + it('adds a fog to parent.__tres.objects', () => { + const parent = nodeOps.createElement('Mesh', undefined, undefined, {}) + const fog = nodeOps.createElement('Fog') + nodeOps.insert(fog, parent) + expect(parent.__tres.objects.map(child => child.uuid)).toStrictEqual([fog.uuid]) + }) + + it('adds parent to child.__tres.parent', () => { + const parent = nodeOps.createElement('Mesh', undefined, undefined, {}) + const material = nodeOps.createElement('MeshNormalMaterial') + const geometry = nodeOps.createElement('BoxGeometry') + const fog = nodeOps.createElement('Fog') + nodeOps.insert(material, parent) + nodeOps.insert(geometry, parent) + nodeOps.insert(fog, parent) + expect(material.__tres.parent).toBe(parent) + expect(geometry.__tres.parent).toBe(parent) + expect(fog.__tres.parent).toBe(parent) + }) + + it('adds non-Object3D children to parent.__tres.objects, but no more than once', () => { + const parent = nodeOps.createElement('Mesh', undefined, undefined, {}) + const material = nodeOps.createElement('MeshNormalMaterial') + const geometry = nodeOps.createElement('BoxGeometry') + const fog = nodeOps.createElement('Fog') + nodeOps.insert(material, parent) + nodeOps.insert(geometry, parent) + nodeOps.insert(fog, parent) + expect(parent.__tres.objects.length).toBe(3) + const objectSet = new Set(parent.__tres.objects) + expect(objectSet.has(material)).toBe(true) + expect(objectSet.has(geometry)).toBe(true) + expect(objectSet.has(fog)).toBe(true) + }) }) describe('remove', () => { @@ -515,6 +564,35 @@ describe('nodeOps', () => { expect(grandChild1.parent.uuid).toBe(primitiveMesh.uuid) }) }) + describe('in the __tres parent-object graph', () => { + it('removes parent-object relationship when object is removed', () => { + const parent = nodeOps.createElement('Mesh', undefined, undefined, {}) + const material = nodeOps.createElement('MeshNormalMaterial') + const geometry = nodeOps.createElement('BoxGeometry') + const fog = nodeOps.createElement('Fog') + nodeOps.insert(material, parent) + nodeOps.insert(geometry, parent) + nodeOps.insert(fog, parent) + expect(material.__tres.parent).toBe(parent) + expect(geometry.__tres.parent).toBe(parent) + expect(fog.__tres.parent).toBe(parent) + + nodeOps.remove(fog) + expect(fog.__tres.parent).toBe(null) + expect(parent.__tres.objects.length).toBe(2) + expect(parent.__tres.objects.includes(fog)).toBe(false) + + nodeOps.remove(material) + expect(material.__tres.parent).toBe(null) + expect(parent.__tres.objects.length).toBe(1) + expect(parent.__tres.objects.includes(material)).toBe(false) + + nodeOps.remove(geometry) + expect(geometry.__tres.parent).toBe(null) + expect(parent.__tres.objects.length).toBe(0) + expect(parent.__tres.objects.includes(geometry)).toBe(false) + }) + }) }) describe('patchProp', () => { diff --git a/src/core/nodeOps.ts b/src/core/nodeOps.ts index 4325dffd7..6623e7755 100644 --- a/src/core/nodeOps.ts +++ b/src/core/nodeOps.ts @@ -2,16 +2,12 @@ import type { RendererOptions } from 'vue' import { BufferAttribute, Object3D } from 'three' import type { TresContext } from '../composables' import { useLogger } from '../composables' -import { deepArrayEqual, disposeObject3D, isHTMLTag, kebabToCamel } from '../utils' -import type { InstanceProps, TresObject, TresObject3D } from '../types' +import { deepArrayEqual, disposeObject3D, filterInPlace, isHTMLTag, kebabToCamel } from '../utils' +import type { InstanceProps, TresInstance, TresObject, TresObject3D } from '../types' import * as is from '../utils/is' +import { invalidateInstance, noop, prepareTresInstance } from '../utils/nodeOpsUtils' import { catalogue } from './catalogue' -function noop(fn: string): any { - // eslint-disable-next-line no-unused-expressions - fn -} - const { logError } = useLogger() const supportedPointerEvents = [ @@ -31,16 +27,6 @@ const supportedPointerEvents = [ 'onWheel', ] -export function invalidateInstance(instance: TresObject) { - const ctx = instance?.__tres?.root - - if (!ctx) { return } - - if (ctx.render && ctx.render.canBeInvalidated.value) { - ctx.invalidate() - } -} - export const nodeOps: (context: TresContext) => RendererOptions = (context) => { const scene = context.scene.value @@ -88,14 +74,14 @@ export const nodeOps: (context: TresContext) => RendererOptions RendererOptions RendererOptions RendererOptions obj !== node) + } + if (is.object3D(node)) { node.removeFromParent?.() diff --git a/src/types/index.ts b/src/types/index.ts index 0f7550d93..34beb158e 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -42,15 +42,22 @@ interface TresBaseObject { export interface LocalState { type: string - // objects and parent are used when children are added with `attach` instead of being added to the Object3D scene graph - objects?: TresObject3D[] - parent?: TresObject3D | null + eventCount: number + root: TresContext + handlers: Partial + memoizedProps: { [key: string]: any } + // NOTE: + // LocalState holds information about the parent/child relationship + // in the Vue graph. If a child is `insert`ed into a parent using + // anything but THREE's `add`, it's put into the parent's `objects`. + // objects and parent are used when children are added with `attach` + // instead of being added to the Object3D scene graph + objects: TresObject[] + parent: TresObject | null + // NOTE: End graph info + primitive?: boolean - eventCount?: number - handlers?: Partial - memoizedProps?: { [key: string]: any } disposable?: boolean - root?: TresContext } // Custom type for geometry and material properties in Object3D @@ -62,6 +69,8 @@ export interface TresObject3D extends THREE.Object3D { export type TresObject = TresBaseObject & (TresObject3D | THREE.BufferGeometry | THREE.Material | THREE.Fog) & { __tres?: LocalState } +export type TresInstance = TresObject & { __tres: LocalState } + export interface TresScene extends THREE.Scene { __tres: { root: TresContext diff --git a/src/utils/index.test.ts b/src/utils/index.test.ts new file mode 100644 index 000000000..e39f822a7 --- /dev/null +++ b/src/utils/index.test.ts @@ -0,0 +1,62 @@ +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 +}; diff --git a/src/utils/index.ts b/src/utils/index.ts index 77faf0019..f85c5f04b 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -313,3 +313,20 @@ 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(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 +} diff --git a/src/utils/nodeOpsUtils.ts b/src/utils/nodeOpsUtils.ts new file mode 100644 index 000000000..2c3ada316 --- /dev/null +++ b/src/utils/nodeOpsUtils.ts @@ -0,0 +1,32 @@ +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 +}