Explorar o código

test: add insert/remove tests (#738)

andretchen0 hai 10 meses
pai
achega
e1df8a91b5
Modificáronse 1 ficheiros con 312 adicións e 1 borrados
  1. 312 1
      src/core/nodeOps.test.ts

+ 312 - 1
src/core/nodeOps.test.ts

@@ -185,6 +185,50 @@ describe('nodeOps', () => {
       expect(parent.children.includes(child)).toBeTruthy()
     })
 
+    describe('primitive :object', () => {
+      describe('into mesh', () => {
+        it.skip('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)
+          nodeOps.insert(primitive, parent)
+          expect(parent.material.uuid).toBe(object.uuid)
+        })
+
+        it.skip('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)
+          nodeOps.insert(primitive, parent)
+          expect(parent.material.uuid).toBe(object.uuid)
+        })
+
+        it.skip('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)
+          nodeOps.insert(primitive, parent)
+          expect(parent.material.uuid).toBe(object.uuid)
+        })
+
+        it.skip('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)
+          nodeOps.insert(primitive, parent)
+          expect(parent.material.uuid).toBe(object.uuid)
+        })
+      })
+    })
+
     it('does not insert a falsy child', () => {
       const parent = nodeOps.createElement('Object3D', undefined, undefined, {})
       for (const falsyChild of [undefined, null]) {
@@ -228,7 +272,7 @@ describe('nodeOps', () => {
       }
     })
 
-    it('calls dispose on materials', () => {
+    it('calls dispose on a material', () => {
       const parent = mockTresObjectRootInObject(nodeOps.createElement('Mesh', undefined, undefined, {}))
       const material = nodeOps.createElement('MeshNormalMaterial', undefined, undefined, {})
       const spy = vi.spyOn(material, 'dispose')
@@ -237,6 +281,18 @@ describe('nodeOps', () => {
       expect(spy).toHaveBeenCalledOnce()
     })
 
+    it('calls dispose on a material array', () => {
+      const parent = mockTresObjectRootInObject(nodeOps.createElement('Mesh', undefined, undefined, {}))
+      const material0 = new THREE.MeshNormalMaterial()
+      const material1 = new THREE.MeshNormalMaterial()
+      const spy0 = vi.spyOn(material0, 'dispose')
+      const spy1 = vi.spyOn(material1, 'dispose')
+      parent.material = [material0, material1]
+      nodeOps.remove(parent)
+      expect(spy0).toHaveBeenCalledOnce()
+      expect(spy1).toHaveBeenCalledOnce()
+    })
+
     it('calls dispose on geometries', () => {
       const parent = mockTresObjectRootInObject(nodeOps.createElement('Mesh', undefined, undefined, {}))
       const geometry = nodeOps.createElement('SphereGeometry', undefined, undefined, {})
@@ -245,6 +301,220 @@ describe('nodeOps', () => {
       nodeOps.remove(parent)
       expect(spy).toHaveBeenCalledOnce()
     })
+
+    it('calls dispose on every material/geometry in a TresMesh tree', () => {
+      const NUM_LEVEL = 5
+      const NUM_CHILD_PER_NODE = 3
+      const rootNode = mockTresObjectRootInObject(nodeOps.createElement('Mesh'))
+      const disposalSpies = []
+
+      createTreeIn(rootNode, (parent, childI, levelI) => {
+        if (levelI > NUM_LEVEL || childI >= NUM_CHILD_PER_NODE) {
+          return false
+        }
+        const { mesh, material, geometry } = createElementMesh(nodeOps)
+        nodeOps.insert(mesh, parent)
+        disposalSpies.push(vi.spyOn(geometry, 'dispose'))
+        disposalSpies.push(vi.spyOn(material, 'dispose'))
+        return mesh
+      })
+
+      nodeOps.remove(rootNode)
+      for (const spy of disposalSpies) {
+        expect(spy).toHaveBeenCalledOnce()
+      }
+    })
+
+    it('calls dispose on every material/geometry in a TresMesh/TresGroup tree', () => {
+      const NUM_LEVEL = 5
+      const NUM_CHILD_PER_NODE = 3
+      const rootNode = mockTresObjectRootInObject(nodeOps.createElement('Group'))
+      const disposalSpies = []
+
+      createTreeIn(rootNode, (parent, childI, levelI) => {
+        if (childI > NUM_CHILD_PER_NODE || levelI > NUM_LEVEL) {
+          return false
+        }
+        if (Math.random() > 0.3) {
+          const { mesh, material, geometry } = createElementMesh(nodeOps)
+          nodeOps.insert(mesh, parent)
+          disposalSpies.push(vi.spyOn(geometry, 'dispose'))
+          disposalSpies.push(vi.spyOn(material, 'dispose'))
+          return mesh
+        }
+        else {
+          const group = nodeOps.createElement('Group')
+          nodeOps.insert(group, parent)
+          return group
+        }
+      })
+
+      nodeOps.remove(rootNode)
+      for (const spy of disposalSpies) {
+        expect(spy).toHaveBeenCalledOnce()
+      }
+    })
+
+    it('does not dispose primitive material/geometries on remove(primitive)', () => {
+      const { primitive, material, geometry } = createElementPrimitiveMesh(nodeOps)
+      const spy0 = vi.spyOn(material, 'dispose')
+      const spy1 = vi.spyOn(geometry, 'dispose')
+
+      const group = nodeOps.createElement('Group')
+      nodeOps.insert(primitive, group)
+      nodeOps.remove(primitive)
+
+      expect(spy0).not.toBeCalled()
+      expect(spy1).not.toBeCalled()
+    })
+
+    it.skip('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')
+
+      const group = nodeOps.createElement('Group')
+      nodeOps.insert(primitive, group)
+      nodeOps.remove(group)
+
+      expect(spy0).not.toBeCalled()
+      expect(spy1).not.toBeCalled()
+    })
+
+    it.skip('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'))
+      const disposalSpies = []
+
+      createTreeIn(rootNode, (parent, childI, levelI) => {
+        if (childI > NUM_CHILD_PER_NODE || levelI > NUM_LEVEL) {
+          return false
+        }
+        if (Math.random() > 0.5) {
+          const { mesh } = createElementMesh(nodeOps)
+          nodeOps.insert(mesh, parent)
+          return mesh
+        }
+        else if (Math.random() > 0.5) {
+          const group = nodeOps.createElement('Group')
+          nodeOps.insert(group, parent)
+          return group
+        }
+        else {
+          const { primitive, material, geometry } = createElementPrimitiveMesh(nodeOps)
+          disposalSpies.push(vi.spyOn(geometry, 'dispose'))
+          disposalSpies.push(vi.spyOn(material, 'dispose'))
+          nodeOps.insert(primitive, parent)
+          return primitive
+        }
+      })
+
+      nodeOps.remove(rootNode)
+      for (const spy of disposalSpies) {
+        expect(spy).not.toHaveBeenCalled()
+      }
+    })
+
+    describe(':dispose="null"', () => {
+      it.skip('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'))
+        const disposalSpies = []
+        const nullDisposeObjects = new Set()
+
+        createTreeIn(rootNode, (parent, childI, levelI) => {
+          if (childI > NUM_CHILD_PER_NODE || levelI > NUM_LEVEL) {
+            return false
+          }
+          const { mesh, material, geometry } = createElementMesh(nodeOps)
+          if (nullDisposeObjects.has(parent)) {
+            nullDisposeObjects.add(mesh)
+            disposalSpies.push(vi.spyOn(geometry, 'dispose'))
+            disposalSpies.push(vi.spyOn(material, 'dispose'))
+          }
+          else if (levelI > 2 && Math.random() > 0.8) {
+            nodeOps.patchProp(mesh, 'dispose', undefined, null)
+            nullDisposeObjects.add(mesh)
+            disposalSpies.push(vi.spyOn(geometry, 'dispose'))
+            disposalSpies.push(vi.spyOn(material, 'dispose'))
+          }
+          nodeOps.insert(mesh, parent)
+          return mesh
+        })
+
+        nodeOps.remove(rootNode)
+        for (const spy of disposalSpies) {
+          expect(spy).not.toHaveBeenCalled()
+        }
+      })
+    })
+
+    describe('in the THREE parent-child graph', () => {
+      it('detaches mesh from mesh', () => {
+        const { mesh: parent } = createElementMesh(nodeOps)
+        const { mesh: child } = createElementMesh(nodeOps)
+        nodeOps.insert(child, parent)
+        expect(child.parent.uuid).toBe(parent.uuid)
+
+        nodeOps.remove(child)
+        expect(child.parent?.uuid).toBeFalsy()
+      })
+      it('detaches group from mesh', () => {
+        const { mesh: parent } = createElementMesh(nodeOps)
+        const child = nodeOps.createElement('Group')
+        nodeOps.insert(child, parent)
+        expect(child.parent.uuid).toBe(parent.uuid)
+
+        nodeOps.remove(child)
+        expect(child.parent?.uuid).toBeFalsy()
+      })
+      it('detaches mesh from group', () => {
+        const parent = nodeOps.createElement('Group')
+        const { mesh: child } = createElementMesh(nodeOps)
+        nodeOps.insert(child, parent)
+        expect(child.parent.uuid).toBe(parent.uuid)
+
+        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)
+
+        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(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.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)
+      })
+    })
   })
 
   describe('patchProp', () => {
@@ -468,3 +738,44 @@ function mockTresContext() {
     deregisterCamera: () => {},
   } as unknown as TresContext
 }
+
+function createElementMesh(nodeOps: ReturnType<typeof getNodeOps>) {
+  const geometry = nodeOps.createElement('BoxGeometry')
+  const material = nodeOps.createElement('MeshNormalMaterial')
+  const mesh = nodeOps.createElement('Mesh')
+  nodeOps.insert(geometry, mesh)
+  nodeOps.insert(material, mesh)
+  return { mesh, geometry, material }
+}
+
+function createElementPrimitiveMesh(nodeOps: ReturnType<typeof getNodeOps>) {
+  const geometry = new THREE.BoxGeometry()
+  const material = new THREE.MeshNormalMaterial()
+  const mesh = new THREE.Mesh(geometry, material)
+  const primitive = nodeOps.createElement('primitive', undefined, undefined, { object: mesh })
+  return { primitive, mesh, geometry, material }
+}
+
+function createTreeIn<T>(root: T, insertCallback: (parent: T, childI: number, levelI: number) => T) {
+  let levelII = 0
+  const nextLevel = [root] as T[]
+  while (nextLevel.length) {
+    const currLevel = Array.from(nextLevel)
+    nextLevel.length = 0
+
+    while (currLevel.length) {
+      const parent = currLevel.shift()
+      let childI = 0
+      while (true) {
+        const child = insertCallback(parent, childI++, levelII)
+        if (child) {
+          nextLevel.push(child)
+        }
+        else {
+          break
+        }
+      }
+    }
+    levelII++
+  }
+}