diff --git a/playground/src/pages/advanced/materialArray/index.vue b/playground/src/pages/advanced/materialArray/index.vue new file mode 100644 index 000000000..843276d16 --- /dev/null +++ b/playground/src/pages/advanced/materialArray/index.vue @@ -0,0 +1,47 @@ + + + diff --git a/playground/src/pages/models/RiggedModel.vue b/playground/src/pages/models/RiggedModel.vue deleted file mode 100644 index 68a0396f6..000000000 --- a/playground/src/pages/models/RiggedModel.vue +++ /dev/null @@ -1,36 +0,0 @@ - - - diff --git a/playground/src/components/UgglyBunny.vue b/playground/src/pages/models/RiggedModel/UgglyBunny.vue similarity index 100% rename from playground/src/components/UgglyBunny.vue rename to playground/src/pages/models/RiggedModel/UgglyBunny.vue diff --git a/playground/src/pages/models/RiggedModel/index.vue b/playground/src/pages/models/RiggedModel/index.vue new file mode 100644 index 000000000..9909ca7f6 --- /dev/null +++ b/playground/src/pages/models/RiggedModel/index.vue @@ -0,0 +1,48 @@ + + + diff --git a/playground/src/router/routes/advanced.ts b/playground/src/router/routes/advanced.ts index a2c5cdd96..86a96f5ae 100644 --- a/playground/src/router/routes/advanced.ts +++ b/playground/src/router/routes/advanced.ts @@ -24,4 +24,9 @@ export const advancedRoutes = [ name: 'Suspense', component: () => import('../../pages/advanced/suspense/index.vue'), }, + { + path: '/advanced/material-array', + name: 'Material array', + component: () => import('../../pages/advanced/materialArray/index.vue'), + }, ] diff --git a/playground/src/router/routes/models.ts b/playground/src/router/routes/models.ts index 4abd3172e..d61d2be0a 100644 --- a/playground/src/router/routes/models.ts +++ b/playground/src/router/routes/models.ts @@ -7,6 +7,6 @@ export const modelsRoutes = [ { path: '/models/rigged', name: 'Rigged Models', - component: () => import('../../pages/models/RiggedModel.vue'), + component: () => import('../../pages/models/RiggedModel/index.vue'), }, ] diff --git a/src/core/nodeOps.test.ts b/src/core/nodeOps.test.ts index 1e71920ee..62d9f3a16 100644 --- a/src/core/nodeOps.test.ts +++ b/src/core/nodeOps.test.ts @@ -129,32 +129,6 @@ describe('nodeOps', () => { it('throws an error if tag does not exist in catalogue', () => { expect(() => { nodeOps.createElement('THIS_TAG_DOES_NOT_EXIST', undefined, undefined, {}) }).toThrow() }) - - it('adds material with "attach" property if instance is a material', () => { - // Setup - const tag = 'TresMeshStandardMaterial' - const props = { args: [] } - - // Test - const instance = nodeOps.createElement(tag, undefined, undefined, props) - - // Assert - expect(instance?.isMaterial).toBeTruthy() - expect(instance?.attach).toBe('material') - }) - - it('adds attach geometry property if instance is a geometry', () => { - // Setup - const tag = 'TresTorusGeometry' - const props = { args: [] } - - // Test - const instance = nodeOps.createElement(tag, undefined, undefined, props) - - // Assert - expect(instance?.isBufferGeometry).toBeTruthy() - expect(instance?.attach).toBe('geometry') - }) }) describe('insert', () => { @@ -185,46 +159,47 @@ describe('nodeOps', () => { expect(parent.children.includes(child)).toBeTruthy() }) - describe('primitive :object', () => { + describe.skip('primitive :object', () => { describe('into mesh', () => { - it.skip('inserts a mesh :object', () => { + it('inserts a mesh :object', () => { const parent = nodeOps.createElement('Mesh', undefined, undefined, {}) const object = new THREE.Mesh() const primitive = nodeOps.createElement('primitive', undefined, undefined, { object }) - expect(parent.material.uuid).not.toBe(object.uuid) + expect(parent.children.length).toBe(0) nodeOps.insert(primitive, parent) - expect(parent.material.uuid).toBe(object.uuid) + expect(parent.children.length).toBe(1) + expect(parent.children[0]).toBe(object) }) - it.skip('inserts a material :object', () => { + it('inserts a material :object', () => { const parent = nodeOps.createElement('Mesh', undefined, undefined, {}) const object = new THREE.MeshNormalMaterial() const primitive = nodeOps.createElement('primitive', undefined, undefined, { object }) - expect(parent.material.uuid).not.toBe(object.uuid) + expect(parent.material.uuid).not.toBe(object) nodeOps.insert(primitive, parent) - expect(parent.material.uuid).toBe(object.uuid) + expect(parent.material).toBe(object) }) - it.skip('inserts a geometry :object', () => { + it('inserts a geometry :object', () => { const parent = nodeOps.createElement('Mesh', undefined, undefined, {}) const object = new THREE.BoxGeometry() const primitive = nodeOps.createElement('primitive', undefined, undefined, { object }) - expect(parent.material.uuid).not.toBe(object.uuid) + expect(parent.geometry).not.toBe(object) nodeOps.insert(primitive, parent) - expect(parent.material.uuid).toBe(object.uuid) + expect(parent.geometry).toBe(object) }) - it.skip('inserts a group :object', () => { + it('inserts a group :object', () => { const parent = nodeOps.createElement('Mesh', undefined, undefined, {}) const object = new THREE.Group() const primitive = nodeOps.createElement('primitive', undefined, undefined, { object }) - expect(parent.material.uuid).not.toBe(object.uuid) + expect(parent.children.length).toBe(0) nodeOps.insert(primitive, parent) - expect(parent.material.uuid).toBe(object.uuid) + expect(parent.children[0]).toBe(object) }) }) }) @@ -238,22 +213,179 @@ describe('nodeOps', () => { } }) - it('inserts Fog as a property', () => { - const parent = nodeOps.createElement('Object3D', undefined, undefined, {}) - const fog = nodeOps.createElement('Fog', undefined, undefined, {}) - nodeOps.insert(fog, parent) - expect(parent.fog).toBe(fog) - }) + describe('attach/detach', () => { + // NOTE: Special implementation case: `attach`/`detach` + // + // Objects that aren't added to the Scene using + // `THREE.Object3D`'s `add` will generally be inserted + // using `attach` and removed using `detach`. + // + // This way of inserting/removing has special challenges: + // - The user can specify how the object is `attach`/`detach`ed + // by setting the `attach` prop. + // - Before a new value is `attach`ed, the system must record + // the current value and restore it when the new value is + // `detach`ed. + it('if "attach" prop is provided, sets `parent[attach], even if the field does not exist on the parent`', () => { + const parent = nodeOps.createElement('Object3D', undefined, undefined, {}) + for (const attach of ['material', 'foo', 'bar', 'baz']) { + const child = nodeOps.createElement('MeshNormalMaterial', undefined, undefined, { attach }) + nodeOps.insert(child, parent) + expect(parent[attach]).toBe(child) + expect(parent.children.length).toBe(0) + } + }) - it('if "attach" prop is provided, sets `parent[attach]`', () => { - const parent = nodeOps.createElement('Object3D', undefined, undefined, {}) - for (const attach of ['material', 'foo', 'bar', 'baz']) { - const child = nodeOps.createElement('MeshNormalMaterial', undefined, undefined, {}) - child.attach = attach - nodeOps.insert(child, parent) - expect(parent[attach]).toBe(child) - expect(parent.children.length).toBe(0) - } + it('can attach and detach a BufferGeometry', () => { + const parent = nodeOps.createElement('Mesh', undefined, undefined, {}) + const previousAttach = parent.geometry + const geometry0 = nodeOps.createElement('BoxGeometry', undefined, undefined, {}) + const geometry1 = nodeOps.createElement('BoxGeometry', undefined, undefined, {}) + + nodeOps.insert(geometry0, parent) + expect(parent.geometry).not.toBe(previousAttach) + expect(parent.geometry).toBe(geometry0) + + nodeOps.remove(geometry0) + nodeOps.insert(geometry1, parent) + expect(parent.geometry).toBe(geometry1) + + nodeOps.remove(geometry1) + expect(parent.geometry).toBe(previousAttach) + }) + + it('can attach and detach a material', () => { + const parent = nodeOps.createElement('Mesh', undefined, undefined, {}) + const previousAttach = parent.material + const material0 = nodeOps.createElement('MeshNormalMaterial', undefined, undefined, {}) + const material1 = nodeOps.createElement('MeshNormalMaterial', undefined, undefined, {}) + nodeOps.insert(material0, parent) + expect(parent.material).toBe(material0) + + nodeOps.remove(material0) + nodeOps.insert(material1, parent) + expect(parent.material).toBe(material1) + + nodeOps.remove(material1) + expect(parent.material).toBe(previousAttach) + }) + + it('can attach and detach a material array', () => { + const parent = nodeOps.createElement('Mesh', undefined, undefined, {}) + const previousMaterial = parent.material + const material0 = nodeOps.createElement('MeshNormalMaterial', undefined, undefined, { attach: 'material-0' }) + const material1 = nodeOps.createElement('MeshNormalMaterial', undefined, undefined, { attach: 'material-1' }) + const material2 = nodeOps.createElement('MeshNormalMaterial', undefined, undefined, { attach: 'material-2' }) + nodeOps.insert(material0, parent) + expect(parent.material[0]).toStrictEqual(material0) + nodeOps.insert(material1, parent) + expect(parent.material[1]).toStrictEqual(material1) + nodeOps.insert(material2, parent) + expect(parent.material[2]).toStrictEqual(material2) + + nodeOps.remove(material0) + expect(parent.material[0]).toBeUndefined() + expect(parent.material[1]).toBe(material1) + expect(parent.material[2]).toBe(material2) + nodeOps.remove(material2) + expect(parent.material[0]).toBeUndefined() + expect(parent.material[1]).toBe(material1) + expect(parent.material[2]).toBeUndefined() + + nodeOps.patchProp(material1, 'attach', undefined, 'material-2') + expect(parent.material[0]).toBeUndefined() + expect(parent.material[1]).toBeUndefined() + expect(parent.material[2]).toBe(material1) + + nodeOps.remove(material1) + expect(parent.material).toBe(previousMaterial) + }) + + it('can attach and detach fog', () => { + const parent = nodeOps.createElement('Mesh', undefined, undefined, {}) + const fog = nodeOps.createElement('Fog', undefined, undefined, {}) + nodeOps.insert(fog, parent) + expect(parent.fog).toBe(fog) + nodeOps.remove(fog) + expect('fog' in parent).toBe(false) + }) + + it('can attach and detach a "pierced" string', () => { + const parent = nodeOps.createElement('Mesh', undefined, undefined, {}) + const material = nodeOps.createElement('MeshBasicMaterial', undefined, undefined, { color: 'red' }) + const previousColor = material.color + const color = nodeOps.createElement('Color', undefined, undefined, { attach: 'material-color' }) + nodeOps.insert(material, parent) + nodeOps.insert(color, parent) + expect(parent.material.color).toBe(color) + nodeOps.remove(color) + expect(parent.material.color).toBe(previousColor) + + material.alphaMap = new THREE.Texture() + const previousAlphaMap = material.alphaMap + const alphaMap = nodeOps.createElement('Texture', undefined, undefined, { attach: 'material-alpha-map' }) + nodeOps.insert(alphaMap, parent) + expect(parent.material.alphaMap).toBe(alphaMap) + nodeOps.remove(alphaMap) + expect(parent.material.alphaMap).toBe(previousAlphaMap) + }) + + it('attach can be patched', () => { + const parent = nodeOps.createElement('Mesh', undefined, undefined, {}) + const previousMaterial = parent.material + const material = nodeOps.createElement('MeshBasicMaterial', undefined, undefined, { color: 'red', attach: 'material' }) + nodeOps.insert(material, parent) + expect(parent.material).toBe(material) + + nodeOps.patchProp(material, 'attach', undefined, 'foo') + expect(parent.foo).toBe(material) + expect(parent.material).toBe(previousMaterial) + + nodeOps.patchProp(material, 'attach', undefined, 'material') + expect(parent.foo).toBeUndefined() + expect(parent.material).toBe(material) + + nodeOps.patchProp(material, 'attach', undefined, 'bar') + expect(parent.bar).toBe(material) + expect(parent.material).toBe(previousMaterial) + }) + + it('can attach and detach a material array by patching `attach`', () => { + const parent = nodeOps.createElement('Mesh', undefined, undefined, {}) + const previousMaterial = parent.material + const material0 = nodeOps.createElement('MeshNormalMaterial', undefined, undefined, { attach: 'material-0' }) + const material1 = nodeOps.createElement('MeshNormalMaterial', undefined, undefined, { attach: 'material-1' }) + const material2 = nodeOps.createElement('MeshNormalMaterial', undefined, undefined, { attach: 'material-2' }) + nodeOps.insert(material0, parent) + nodeOps.insert(material1, parent) + nodeOps.insert(material2, parent) + expect(parent.material[0]).toBe(material0) + expect(parent.material[1]).toBe(material1) + expect(parent.material[2]).toBe(material2) + + nodeOps.patchProp(material1, 'attach', undefined, 'material-0') + expect(parent.material[0]).toBe(material1) + expect(parent.material[1]).toBeUndefined() + expect(parent.material[2]).toBe(material2) + + nodeOps.patchProp(material1, 'attach', undefined, 'material-2') + expect(parent.material[0]).toBe(material0) + expect(parent.material[1]).toBeUndefined() + expect(parent.material[2]).toBe(material1) + + nodeOps.patchProp(material0, 'attach', undefined, 'foo') + expect(parent.material[0]).toBeUndefined() + expect(parent.material[1]).toBeUndefined() + expect(parent.material[2]).toBe(material1) + + nodeOps.patchProp(material1, 'attach', undefined, 'foo') + expect(parent.material[0]).toBeUndefined() + expect(parent.material[1]).toBeUndefined() + expect(parent.material[2]).toBe(material2) + + nodeOps.patchProp(material2, 'attach', undefined, 'foo') + expect(parent.material).toBe(previousMaterial) + }) }) it('adds a material to parent.__tres.objects', () => { @@ -299,10 +431,86 @@ describe('nodeOps', () => { 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) + const childrenSet = new Set(parent.__tres.objects) + expect(childrenSet.has(material)).toBe(true) + expect(childrenSet.has(geometry)).toBe(true) + expect(childrenSet.has(fog)).toBe(true) + }) + + it.skip('can insert the same `primitive :object` in multiple places in the scene graph', () => { + const material = new THREE.MeshNormalMaterial() + const geometry = new THREE.BoxGeometry() + const otherMaterial = new THREE.MeshBasicMaterial() + const otherGeometry = new THREE.SphereGeometry() + + const grandparent = nodeOps.createElement('Mesh', undefined, undefined, {}) + const parent0 = nodeOps.createElement('Mesh', undefined, undefined, {}) + const parent1 = nodeOps.createElement('Mesh', undefined, undefined, {}) + const parent2 = nodeOps.createElement('Mesh', undefined, undefined, {}) + + const materialPrimitive0 = nodeOps.createElement('primitive', undefined, undefined, { object: material }) + const materialPrimitive1 = nodeOps.createElement('primitive', undefined, undefined, { object: material }) + const materialPrimitive2 = nodeOps.createElement('primitive', undefined, undefined, { object: material }) + const materialPrimitiveOther = nodeOps.createElement('primitive', undefined, undefined, { object: otherMaterial }) + + const geometryPrimitive0 = nodeOps.createElement('primitive', undefined, undefined, { object: geometry }) + const geometryPrimitive1 = nodeOps.createElement('primitive', undefined, undefined, { object: geometry }) + const geometryPrimitive2 = nodeOps.createElement('primitive', undefined, undefined, { object: geometry }) + const geometryPrimitiveOther = nodeOps.createElement('primitive', undefined, undefined, { object: otherGeometry }) + + nodeOps.insert(parent0, grandparent) + nodeOps.insert(parent1, grandparent) + nodeOps.insert(parent2, grandparent) + nodeOps.insert(materialPrimitive0, parent0) + nodeOps.insert(materialPrimitive1, parent1) + nodeOps.insert(materialPrimitive2, parent2) + nodeOps.insert(geometryPrimitive0, parent0) + nodeOps.insert(geometryPrimitive1, parent1) + nodeOps.insert(geometryPrimitive2, parent2) + + expect(parent0.material).toBe(material) + expect(parent1.material).toBe(material) + expect(parent2.material).toBe(material) + expect(parent0.geometry).toBe(geometry) + expect(parent1.geometry).toBe(geometry) + expect(parent2.geometry).toBe(geometry) + + nodeOps.insert(materialPrimitiveOther, parent0) + nodeOps.insert(geometryPrimitiveOther, parent1) + + expect(parent0.material).not.toBe(material) + expect(parent1.material).toBe(material) + expect(parent2.material).toBe(material) + expect(parent0.geometry).toBe(geometry) + expect(parent1.geometry).not.toBe(geometry) + expect(parent2.geometry).toBe(geometry) + + nodeOps.insert(materialPrimitiveOther, parent1) + nodeOps.insert(geometryPrimitiveOther, parent0) + + expect(parent0.material).not.toBe(material) + expect(parent1.material).not.toBe(material) + expect(parent2.material).toBe(material) + expect(parent0.geometry).not.toBe(geometry) + expect(parent1.geometry).not.toBe(geometry) + expect(parent2.geometry).toBe(geometry) + + nodeOps.insert(materialPrimitiveOther, parent2) + nodeOps.insert(geometryPrimitiveOther, parent2) + + expect(parent0.material).not.toBe(material) + expect(parent1.material).not.toBe(material) + expect(parent2.material).not.toBe(material) + expect(parent0.geometry).not.toBe(geometry) + expect(parent1.geometry).not.toBe(geometry) + expect(parent2.geometry).not.toBe(geometry) + + expect(parent0.material).toBe(otherMaterial) + expect(parent1.material).toBe(otherMaterial) + expect(parent2.material).toBe(otherMaterial) + expect(parent0.geometry).toBe(otherGeometry) + expect(parent1.geometry).toBe(otherGeometry) + expect(parent2.geometry).toBe(otherGeometry) }) }) @@ -312,7 +520,7 @@ describe('nodeOps', () => { const child = mockTresObjectRootInObject(new Mesh() as unknown as TresObject) nodeOps.insert(child, parent) nodeOps.remove(child) - expect(!parent.children.includes(child)).toBeTruthy() + expect(parent.children.includes(child)).toBeFalsy() }) it('silently does not remove a falsy child', () => { @@ -330,7 +538,9 @@ describe('nodeOps', () => { expect(spy).toHaveBeenCalledOnce() }) - it('calls dispose on a material array', () => { + it.skip('calls dispose on a material array', () => { + // TODO: Make this test pass. + // No way to add a material array via nodeOps currently. const parent = mockTresObjectRootInObject(nodeOps.createElement('Mesh', undefined, undefined, {})) const material0 = new THREE.MeshNormalMaterial() const material1 = new THREE.MeshNormalMaterial() @@ -351,6 +561,19 @@ describe('nodeOps', () => { expect(spy).toHaveBeenCalledOnce() }) + it('calls dispose on material/geometry in a TresMesh child of a TresMesh', () => { + const { mesh: grandparent } = createElementMesh(nodeOps) + const { mesh: parent } = createElementMesh(nodeOps) + const { mesh: child } = createElementMesh(nodeOps) + nodeOps.insert(parent, grandparent) + nodeOps.insert(child, parent) + const childMaterialDisposalSpy = vi.spyOn(child.material, 'dispose') + const childGeometryDisposalSpy = vi.spyOn(child.geometry, 'dispose') + nodeOps.remove(parent) + expect(childGeometryDisposalSpy).toHaveBeenCalledOnce() + expect(childMaterialDisposalSpy).toHaveBeenCalledOnce() + }) + it('calls dispose on every material/geometry in a TresMesh tree', () => { const NUM_LEVEL = 5 const NUM_CHILD_PER_NODE = 3 @@ -417,7 +640,7 @@ describe('nodeOps', () => { expect(spy1).not.toBeCalled() }) - it.skip('does not dispose primitive material/geometries on remove(ascestorOfPrimitive)', () => { + it('does not dispose primitive material/geometries on remove(ascestorOfPrimitive)', () => { const { primitive, material, geometry } = createElementPrimitiveMesh(nodeOps) const spy0 = vi.spyOn(material, 'dispose') const spy1 = vi.spyOn(geometry, 'dispose') @@ -430,7 +653,7 @@ describe('nodeOps', () => { expect(spy1).not.toBeCalled() }) - it.skip('does not call dispose on primitive materials/geometries in a tree of Mesh/Groups/Primitives created by nodeOps', () => { + it('does not call dispose on primitive materials/geometries in a tree of Mesh/Groups/Primitives created by nodeOps', () => { const NUM_LEVEL = 5 const NUM_CHILD_PER_NODE = 3 const rootNode = mockTresObjectRootInObject(nodeOps.createElement('Group')) @@ -466,7 +689,31 @@ describe('nodeOps', () => { }) describe(':dispose="null"', () => { - it.skip('does not call dispose on any element in a subtree where the root :dispose==="null"', () => { + it('does not call dispose on geometry/material in a Mesh where :dispose==="null"', () => { + const { mesh: parent } = createElementMesh(nodeOps) + const { mesh, geometry, material } = createElementMesh(nodeOps) + const spy0 = vi.spyOn(geometry, 'dispose') + const spy1 = vi.spyOn(material, 'dispose') + nodeOps.patchProp(mesh, 'dispose', undefined, null) + nodeOps.insert(mesh, parent) + nodeOps.remove(mesh) + expect(spy0).not.toBeCalled() + expect(spy1).not.toBeCalled() + }) + it('does not call dispose on child\'s geometry/material, for remove()', () => { + const { mesh: grandparent } = createElementMesh(nodeOps) + const { mesh: parent } = createElementMesh(nodeOps) + const { mesh: child, geometry, material } = createElementMesh(nodeOps) + const spy0 = vi.spyOn(geometry, 'dispose') + const spy1 = vi.spyOn(material, 'dispose') + nodeOps.patchProp(child, 'dispose', undefined, null) + nodeOps.insert(parent, grandparent) + nodeOps.insert(child, parent) + nodeOps.remove(parent) + expect(spy0).not.toBeCalled() + expect(spy1).not.toBeCalled() + }) + it('does not call dispose on any element in a subtree where the root :dispose==="null"', () => { const NUM_LEVEL = 5 const NUM_CHILD_PER_NODE = 3 const rootNode = mockTresObjectRootInObject(nodeOps.createElement('Group')) @@ -528,44 +775,46 @@ describe('nodeOps', () => { nodeOps.remove(child) expect(child.parent?.uuid).toBeFalsy() }) - it.skip('detaches mesh (in primitive :object) from mesh', () => { - const { mesh: parent } = createElementMesh(nodeOps) - const { primitive, mesh } = createElementPrimitiveMesh(nodeOps) - nodeOps.insert(primitive, parent) - expect(primitive.parent?.uuid).toBe(mesh.uuid) + describe.skip('primitive', () => { + it('detaches mesh (in primitive :object) from mesh', () => { + const { mesh: parent } = createElementMesh(nodeOps) + const { primitive, mesh } = createElementPrimitiveMesh(nodeOps) + nodeOps.insert(primitive, parent) + expect(primitive.parent?.uuid).toBe(parent.uuid) - nodeOps.remove(primitive) - expect(mesh.parent?.uuid).toBeFalsy() - }) - it.skip('detaches mesh (in primitive :object) when mesh ancestor is removed', () => { - const { mesh: grandparent } = createElementMesh(nodeOps) - const { mesh: parent } = createElementMesh(nodeOps) - const { primitive, mesh: primitiveMesh } = createElementPrimitiveMesh(nodeOps) - nodeOps.insert(parent, grandparent) - nodeOps.insert(primitive, parent) - expect(primitiveMesh.parent?.uuid).toBe(parent.uuid) + nodeOps.remove(primitive) + expect(mesh.parent?.uuid).toBeFalsy() + }) + it('detaches mesh (in primitive :object) when mesh ancestor is removed', () => { + const { mesh: grandparent } = createElementMesh(nodeOps) + const { mesh: parent } = createElementMesh(nodeOps) + const { primitive, mesh: primitiveMesh } = createElementPrimitiveMesh(nodeOps) + nodeOps.insert(parent, grandparent) + nodeOps.insert(primitive, parent) + expect(primitiveMesh.parent?.uuid).toBe(parent.uuid) - nodeOps.remove(parent) - expect(primitiveMesh.parent?.uuid).toBeFalsy() - }) - it('does not detach primitive :object descendants', () => { - const { mesh: parent } = createElementMesh(nodeOps) - const { primitive, mesh: primitiveMesh } = createElementPrimitiveMesh(nodeOps) - const grandChild0 = new THREE.Mesh() - const grandChild1 = new THREE.Group() - primitiveMesh.add(grandChild0, grandChild1) + nodeOps.remove(parent) + expect(primitiveMesh.parent?.type).toBeFalsy() + }) + it('does not detach primitive :object descendants', () => { + const { mesh: parent } = createElementMesh(nodeOps) + const { primitive, mesh: primitiveMesh } = createElementPrimitiveMesh(nodeOps) + const grandChild0 = new THREE.Mesh() + const grandChild1 = new THREE.Group() + primitiveMesh.add(grandChild0, grandChild1) - nodeOps.insert(primitive, parent) - expect(grandChild0.parent.uuid).toBe(primitiveMesh.uuid) - expect(grandChild1.parent.uuid).toBe(primitiveMesh.uuid) + nodeOps.insert(primitive, parent) + expect(grandChild0.parent.uuid).toBe(primitiveMesh.uuid) + expect(grandChild1.parent.uuid).toBe(primitiveMesh.uuid) - nodeOps.remove(primitive) - expect(grandChild0.parent.uuid).toBe(primitiveMesh.uuid) - expect(grandChild1.parent.uuid).toBe(primitiveMesh.uuid) + nodeOps.remove(primitive) + expect(grandChild0.parent.uuid).toBe(primitiveMesh.uuid) + expect(grandChild1.parent.uuid).toBe(primitiveMesh.uuid) + }) }) }) - describe('in the __tres parent-object graph', () => { - it('removes parent-object relationship when object is removed', () => { + describe('in the __tres parent-objects graph', () => { + it('removes parent-objects relationship when object is removed', () => { const parent = nodeOps.createElement('Mesh', undefined, undefined, {}) const material = nodeOps.createElement('MeshNormalMaterial') const geometry = nodeOps.createElement('BoxGeometry') @@ -578,17 +827,17 @@ describe('nodeOps', () => { expect(fog.__tres.parent).toBe(parent) nodeOps.remove(fog) - expect(fog.__tres.parent).toBe(null) + expect(fog.__tres?.parent).toBeFalsy() 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(material.__tres?.parent).toBeFalsy() 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(geometry.__tres?.parent).toBeFalsy() expect(parent.__tres.objects.length).toBe(0) expect(parent.__tres.objects.includes(geometry)).toBe(false) }) @@ -699,20 +948,135 @@ describe('nodeOps', () => { expect(spy).toHaveBeenCalledTimes(3) }) - describe('patch `:object` on primitives', () => { + describe.skip('patch `:object` on primitives', () => { it('replaces original object', () => { const material0 = new THREE.MeshNormalMaterial() - const material1 = new THREE.MeshNormalMaterial() + const material1 = new THREE.MeshBasicMaterial() const primitive = nodeOps.createElement('primitive', undefined, undefined, { object: material0 }) nodeOps.patchProp(primitive, 'object', material0, material1) expect(primitive.object).toBe(material1) }) + + it('does not alter __tres on another primitive sharing the same object', () => { + const materialA = new THREE.MeshNormalMaterial() + const materialB = new THREE.MeshNormalMaterial() + const primitive0 = nodeOps.createElement('primitive', undefined, undefined, { object: materialA }) + const primitive0TresJson = JSON.stringify(primitive0.__tres) + const primitive1 = nodeOps.createElement('primitive', undefined, undefined, { object: materialA }) + + expect(primitive0.__tres).not.toBe(primitive1.__tres) + expect(JSON.stringify(primitive0.__tres)).toBe(primitive0TresJson) + + nodeOps.patchProp(primitive1, 'object', undefined, materialB) + expect(primitive0.__tres).not.toBe(primitive1.__tres) + expect(JSON.stringify(primitive0.__tres)).toBe(primitive0TresJson) + + nodeOps.patchProp(primitive1, 'object', undefined, materialA) + expect(primitive0.__tres).not.toBe(primitive1.__tres) + expect(JSON.stringify(primitive0.__tres)).toBe(primitive0TresJson) + }) + + it('does not replace the object in other primitives who point to the same object', () => { + const { mesh: parent } = createElementMesh(nodeOps) + const { mesh: child0 } = createElementMesh(nodeOps) + const { mesh: child1 } = createElementMesh(nodeOps) + const materialA = new THREE.MeshNormalMaterial() + const materialB = new THREE.MeshBasicMaterial() + const primitive1 = nodeOps.createElement('primitive', undefined, undefined, { object: materialA }) + const primitive0 = nodeOps.createElement('primitive', undefined, undefined, { object: materialA }) + + nodeOps.insert(primitive0, child0) + nodeOps.insert(primitive1, child1) + nodeOps.insert(child0, parent) + nodeOps.insert(child1, parent) + + expect(child0.material).toBe(materialA) + expect(child1.material).toBe(materialA) + + nodeOps.patchProp(primitive1, 'object', undefined, materialB) + expect(child0.material).toBe(materialA) + expect(child1.material).not.toBe(materialA) + + nodeOps.patchProp(primitive1, 'object', undefined, materialA) + expect(child0.material).toBe(materialA) + expect(child1.material).toBe(materialA) + + nodeOps.patchProp(primitive0, 'object', undefined, materialB) + expect(child0.material).not.toBe(materialA) + expect(child1.material).toBe(materialA) + + nodeOps.patchProp(primitive1, 'object', undefined, materialB) + expect(child0.material).not.toBe(materialA) + expect(child1.material).not.toBe(materialA) + expect(child0.material).toBe(materialB) + expect(child1.material).toBe(materialB) + }) + it('attaches the new object to the old object\'s parent; clears old object\'s parent', () => { + const { mesh: parent } = createElementMesh(nodeOps) + const { mesh: child0 } = createThreeBox() + const { mesh: child1 } = createThreeBox() + const primitive = nodeOps.createElement('primitive', undefined, undefined, { object: child0 }) + nodeOps.insert(primitive, parent) + expect(child0.parent).toBe(parent) + expect(parent.children[0]).toBe(child0) + expect(parent.children.length).toBe(1) + + nodeOps.patchProp(primitive, 'object', undefined, child1) + expect(child0.parent?.uuid).toBeFalsy() + expect(child1.parent?.uuid).toBe(parent.uuid) + expect(parent.children[0]).toBe(child1) + expect(parent.children.length).toBe(1) + }) + it('if old :object had been patched, those patches are applied to new :object', () => { + const { mesh: parent } = createElementMesh(nodeOps) + const { mesh: child0 } = createElementMesh(nodeOps) + const { mesh: child1 } = createElementMesh(nodeOps) + const primitive = nodeOps.createElement('primitive', undefined, undefined, { object: child0 }) + nodeOps.insert(primitive, parent) + nodeOps.patchProp(primitive, 'position-x', undefined, -999) + expect(child0.position.x).toBe(-999) + + nodeOps.patchProp(primitive, 'object', undefined, child1) + expect(child1.position.x).toBe(-999) + + nodeOps.patchProp(primitive, 'position-x', undefined, 1000) + nodeOps.patchProp(primitive, 'object', undefined, child0) + expect(child0.position.x).toBe(1000) + }) + it('does not attach old :object children to new :object', () => { + const { mesh: parent } = createElementMesh(nodeOps) + const { mesh: child0 } = createElementMesh(nodeOps) + const { mesh: child1 } = createElementMesh(nodeOps) + const grandchild0 = new THREE.Mesh() + const grandchild1 = new THREE.Mesh() + child0.add(grandchild0) + child1.add(grandchild1) + const primitive = nodeOps.createElement('primitive', undefined, undefined, { object: child0 }) + nodeOps.insert(primitive, parent) + expect(primitive.children[0]).toBe(grandchild0) + expect(primitive.children.length).toBe(1) + + nodeOps.patchProp(primitive, 'object', undefined, child1) + expect(primitive.children[0]).toBe(grandchild1) + expect(primitive.children.length).toBe(1) + + nodeOps.patchProp(primitive, 'object', undefined, child0) + expect(primitive.children[0].uuid).toBe(grandchild0.uuid) + expect(primitive.children.length).toBe(1) + + nodeOps.patchProp(primitive, 'object', undefined, child1) + expect(primitive.children[0]).toBe(grandchild1) + expect(primitive.children.length).toBe(1) + }) it('does not copy UUID', () => { const material0 = new THREE.MeshNormalMaterial() const material1 = new THREE.MeshNormalMaterial() const primitive = nodeOps.createElement('primitive', undefined, undefined, { object: material0 }) nodeOps.patchProp(primitive, 'object', material0, material1) expect(material0.uuid).not.toBe(material1.uuid) + + nodeOps.patchProp(primitive, 'object', material1, material0) + expect(material0.uuid).not.toBe(material1.uuid) }) }) @@ -817,6 +1181,13 @@ function mockTresContext() { } as unknown as TresContext } +function createThreeBox() { + const geometry = new THREE.BoxGeometry() + const material = new THREE.MeshNormalMaterial() + const mesh = new THREE.Mesh(geometry, material) + return { mesh, geometry, material } +} + function createElementMesh(nodeOps: ReturnType) { const geometry = nodeOps.createElement('BoxGeometry') const material = nodeOps.createElement('MeshNormalMaterial') @@ -827,9 +1198,7 @@ function createElementMesh(nodeOps: ReturnType) { } function createElementPrimitiveMesh(nodeOps: ReturnType) { - const geometry = new THREE.BoxGeometry() - const material = new THREE.MeshNormalMaterial() - const mesh = new THREE.Mesh(geometry, material) + const { mesh, geometry, material } = createThreeBox() const primitive = nodeOps.createElement('primitive', undefined, undefined, { object: mesh }) return { primitive, mesh, geometry, material } } diff --git a/src/core/nodeOps.ts b/src/core/nodeOps.ts index 6623e7755..622473015 100644 --- a/src/core/nodeOps.ts +++ b/src/core/nodeOps.ts @@ -2,10 +2,9 @@ import type { RendererOptions } from 'vue' import { BufferAttribute, Object3D } from 'three' import type { TresContext } from '../composables' import { useLogger } from '../composables' -import { deepArrayEqual, disposeObject3D, filterInPlace, isHTMLTag, kebabToCamel } from '../utils' +import { attach, deepArrayEqual, detach, filterInPlace, invalidateInstance, isHTMLTag, kebabToCamel, noop, prepareTresInstance } 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' const { logError } = useLogger() @@ -39,13 +38,13 @@ export const nodeOps: (context: TresContext) => RendererOptions RendererOptions + // 2) it has :dispose="null" + // 3) it was bailed out by a parent passing `remove(..., false)` + const isPrimitive = node.__tres?.primitive + const isDisposeNull = node.dispose === null + const isBailedOut = dispose === false + const shouldDispose = !(isPrimitive || isDisposeNull || isBailedOut) // TODO: // Figure out why `parent` is being set on `node` here // and remove/refactor. node.parent = node.parent || scene - // NOTE: Update __tres parent/objects graph + // NOTE: Remove `node` from __tres parent/objects graph const parent = node.__tres?.parent || scene if (node.__tres) { node.__tres.parent = null } if (parent.__tres && 'objects' in parent.__tres) { filterInPlace(parent.__tres.objects, obj => obj !== node) } - if (is.object3D(node)) { + // NOTE: THREE.removeFromParent removes `node` from + // `parent.children`. + if (node.__tres?.attach) { + detach(parent, node, node.__tres.attach) + } + else { node.removeFromParent?.() + } - // Remove nested child objects. Primitives should not have objects and children that are - // attached to them declaratively ... - node.traverse((child) => { - context.deregisterCamera(child) - // deregisterAtPointerEventHandlerIfRequired?.(child as TresObject) - context.eventManager?.deregisterPointerMissedObject(child) - }) - - context.deregisterCamera(node) - /* deregisterAtPointerEventHandlerIfRequired?.(node as TresObject) */ - invalidateInstance(node as TresObject) + // NOTE: Deregister `node` THREE.Object3D children + node.traverse?.((child) => { + context.deregisterCamera(child) + // deregisterAtPointerEventHandlerIfRequired?.(child as TresObject) + context.eventManager?.deregisterPointerMissedObject(child) + }) - // Dispose the object if it's disposable, primitives needs to be manually disposed by - // calling dispose from `@tresjs/core` package like this `dispose(model)` - const isPrimitive = node.__tres?.primitive + // NOTE: Deregister `node` + context.deregisterCamera(node) + /* deregisterAtPointerEventHandlerIfRequired?.(node as TresObject) */ + invalidateInstance(node as TresObject) - if (!isPrimitive && node.__tres?.disposable) { - disposeObject3D(node) + // TODO: support removing `attach`ed components + + // NOTE: Recursively `remove` children and objects. + // Never on primitives: + // - removing children would alter the primitive :object. + // - primitives are not expected to have declarative children + // and so should not have `objects`. + if (!isPrimitive) { + // NOTE: In recursive `remove`, the array elements will + // remove themselves from these arrays, resulting in + // skipped elements. Make shallow copies of the arrays. + if (node.children) { + [...node.children].forEach(child => remove(child, shouldDispose)) + } + if (node.__tres && 'objects' in node.__tres) { + [...node.__tres.objects].forEach(obj => remove(obj, shouldDispose)) } - node.dispose?.() } + + // NOTE: Dispose `node` + if (shouldDispose && node.dispose && !is.scene(node)) { + node.dispose() + } + + delete node.__tres } function patchProp(node: TresObject, prop: string, prevValue: any, nextValue: any) { @@ -178,6 +208,19 @@ export const nodeOps: (context: TresContext) => RendererOptions RendererOptions foo.bar = value) if (key.includes('-') && target === undefined) { + // TODO: A standalone function called `resolve` is + // available in /src/utils/index.ts. It's covered by tests. + // Refactor below to DRY. const chain = key.split('-') target = chain.reduce((acc, key) => acc[kebabToCamel(key)], root) key = chain.pop() as string diff --git a/src/types/index.ts b/src/types/index.ts index 34beb158e..afbfbd91b 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -6,8 +6,8 @@ import type { TresContext } from '../composables/useTresContextProvider' // Based on React Three Fiber types by Pmndrs // https://github.com/pmndrs/react-three-fiber/blob/v9/packages/fiber/src/three-types.ts -export type AttachFnType = (parent: any, self: O) => () => void -export type AttachType = string | AttachFnType +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 -}