UNPKG

@google/model-viewer

Version:

Easily display interactive 3D models on the web and in AR!

245 lines 13.9 kB
/* @license * Copyright 2020 Google LLC. All Rights Reserved. * Licensed under the Apache License, Version 2.0 (the 'License'); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an 'AS IS' BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import { expect } from 'chai'; import { $lazyLoadGLTFInfo } from '../../../features/scene-graph/material.js'; import { $availableVariants, $materials, $primitivesList, $switchVariant, Model } from '../../../features/scene-graph/model.js'; import { $correlatedObjects } from '../../../features/scene-graph/three-dom-element.js'; import { $scene } from '../../../model-viewer-base.js'; import { ModelViewerElement } from '../../../model-viewer.js'; import { CorrelatedSceneGraph } from '../../../three-components/gltf-instance/correlated-scene-graph.js'; import { waitForEvent } from '../../../utilities.js'; import { assetPath, loadThreeGLTF, rafPasses } from '../../helpers.js'; const ASTRONAUT_GLB_PATH = assetPath('models/Astronaut.glb'); const KHRONOS_TRIANGLE_GLB_PATH = assetPath('models/glTF-Sample-Assets/Models/Triangle/glTF/Triangle.gltf'); const CUBES_GLTF_PATH = assetPath('models/cubes.gltf'); suite('scene-graph/model', () => { suite('Model', () => { test('creates a "default" material, when none is specified', async () => { const threeGLTF = await loadThreeGLTF(KHRONOS_TRIANGLE_GLB_PATH); const model = new Model(CorrelatedSceneGraph.from(threeGLTF)); expect(model.materials.length).to.be.eq(1); expect(model.materials[0].name).to.be.eq('Default'); }); test.skip('exposes a list of materials in the scene', async () => { // TODO: This test is skipped because [$correlatedObjects] can contain // unused materials, because it can contain a base material and the // derived material (from assignFinalMaterial(), if for instance // vertexTangents are used) even if only the derived material is assigned // to a mesh. These extras make the test fail. We may want to remove these // unused materials from [$correlatedObjects] at which point this test // will pass, but it's not hurting anything. const threeGLTF = await loadThreeGLTF(ASTRONAUT_GLB_PATH); const materials = new Set(); threeGLTF.scene.traverse((object) => { if (object.isMesh) { const material = object.material; if (Array.isArray(material)) { material.forEach((material) => materials.add(material)); } else { materials.add(material); } } }); const model = new Model(CorrelatedSceneGraph.from(threeGLTF)); const collectedMaterials = new Set(); model.materials.forEach((material) => { for (const threeMaterial of material[$correlatedObjects]) { collectedMaterials.add(threeMaterial); expect(materials.has(threeMaterial)).to.be.true; } }); expect(collectedMaterials.size).to.be.equal(materials.size); }); suite('Model Variants', () => { test('Switch variant and lazy load', async () => { const threeGLTF = await loadThreeGLTF(CUBES_GLTF_PATH); const model = new Model(CorrelatedSceneGraph.from(threeGLTF)); expect(model[$materials][2][$correlatedObjects]).to.be.empty; expect(model[$materials][2][$lazyLoadGLTFInfo]).to.be.ok; await model[$switchVariant]('Yellow Red'); expect(model[$materials][2][$correlatedObjects]).to.not.be.empty; expect(model[$materials][2][$lazyLoadGLTFInfo]).to.not.be.ok; }); test('Switch back to default variant does not change correlations', async () => { const threeGLTF = await loadThreeGLTF(CUBES_GLTF_PATH); const model = new Model(CorrelatedSceneGraph.from(threeGLTF)); const sizeBeforeSwitch = model[$materials][0][$correlatedObjects].size; await model[$switchVariant]('Yellow Yellow'); // Switches back to default. await model[$switchVariant]('Purple Yellow'); expect(model[$materials][0][$correlatedObjects].size) .equals(sizeBeforeSwitch); }); test('Switching variant when model has no variants has not effect', async () => { const threeGLTF = await loadThreeGLTF(KHRONOS_TRIANGLE_GLB_PATH); const model = new Model(CorrelatedSceneGraph.from(threeGLTF)); const threeMaterial = model[$materials][0][$correlatedObjects].values().next().value; const sizeBeforeSwitch = model[$materials][0][$correlatedObjects].size; await model[$switchVariant]('Does not exist'); expect(model[$materials][0][$correlatedObjects].values().next().value) .equals(threeMaterial); expect(model[$materials][0][$correlatedObjects].size) .equals(sizeBeforeSwitch); }); }); suite('Model e2e test', () => { let element; let model; setup(async () => { element = new ModelViewerElement(); }); teardown(() => { document.body.removeChild(element); }); const loadModel = async (src) => { element.src = src; document.body.insertBefore(element, document.body.firstChild); await waitForEvent(element, 'load'); model = element.model; }; test('getMaterialByName returns material when name exists', async () => { await loadModel(CUBES_GLTF_PATH); const material = model.getMaterialByName('red'); expect(material).to.be.ok; expect(material.name).to.be.equal('red'); }); test('getMaterialByName returns null when name does not exists', async () => { await loadModel(CUBES_GLTF_PATH); const material = model.getMaterialByName('does-not-exist'); expect(material).to.be.null; }); suite('Create Variant', () => { test(`createMaterialInstanceForVariant() adds new primitive variants mapping only to primitives that use the source material`, async () => { await loadModel(CUBES_GLTF_PATH); const primitive1 = model[$primitivesList].find(prim => { return prim.mesh.name === 'Box'; }); const primitive2 = model[$primitivesList].find(prim => { return prim.mesh.name === 'Box_1'; }); const startingSize = primitive1.variantInfo.size; const startingSize2 = primitive2.variantInfo.size; // Creates a variant from material 0. expect(model.createMaterialInstanceForVariant(0, 'test-material', 'test-variant')) .to.be.ok; // primitive1 uses material '0' so it should have a vew variant. expect(primitive1.variantInfo.size).to.equal(startingSize + 1); // primitive2 to should remain unchanged. expect(primitive2.variantInfo.size).to.equal(startingSize2); }); test('Create variant and switch to it', async () => { await loadModel(CUBES_GLTF_PATH); const primitive = model[$primitivesList].find(prim => { return prim.mesh.name === 'Box'; }); model.createMaterialInstanceForVariant(0, 'test-material', 'test-variant'); element.variantName = 'test-variant'; await element.updateComplete; expect(primitive.getActiveMaterial().name).to.equal('test-material'); }); test('New variant is available to model-viewer', async () => { await loadModel(CUBES_GLTF_PATH); model.createMaterialInstanceForVariant(0, 'test-material', 'test-variant'); expect(element.availableVariants.find(variant => { return variant === 'test-variant'; })).to.be.ok; }); test(`createMaterialInstanceForVariant() fails when there is a variant name collision`, async () => { await loadModel(CUBES_GLTF_PATH); expect(model.createMaterialInstanceForVariant(0, 'test-material', 'Purple Yellow')) .to.be.null; }); test('createVariant() creates a variant', async () => { await loadModel(CUBES_GLTF_PATH); model.createVariant('test-variant'); expect(element.availableVariants.find(variant => { return variant === 'test-variant'; })).to.be.ok; }); test('createVariant() is a noop if the variant exists', async () => { await loadModel(CUBES_GLTF_PATH); const length = model[$availableVariants]().length; model.createVariant('Purple Yellow'); expect(length).to.equal(model[$availableVariants]().length); }); test(`setMaterialToVariant() adds variants mapping only to primitives that use the source material`, async () => { await loadModel(CUBES_GLTF_PATH); const primitive1 = model[$primitivesList].find(prim => { return prim.mesh.name === 'Box'; }); const primitive2 = model[$primitivesList].find(prim => { return prim.mesh.name === 'Box_1'; }); const startingSize = primitive1.variantInfo.size; const startingSize2 = primitive2.variantInfo.size; model.createVariant('test-variant'); // Adds material 0 to the variant. model.setMaterialToVariant(0, 'test-variant'); // primitive1 uses material '0' so it should have a vew variant. expect(primitive1.variantInfo.size).to.equal(startingSize + 1); // primitive2 to should remain unchanged. expect(primitive2.variantInfo.size).to.equal(startingSize2); }); test('updateVariantName() updates the variant name', async () => { await loadModel(CUBES_GLTF_PATH); element.variantName = 'Yellow Red'; await element.updateComplete; element.model.updateVariantName('Yellow Red', 'NewName'); expect(element.availableVariants[2]).equal('NewName'); }); test('deleteVariant() removes variant from primitives, materials and available variants.', async () => { await loadModel(CUBES_GLTF_PATH); element.model.deleteVariant('Yellow Red'); // Removed from the list of available variants. expect(element.availableVariants.length).equal(2); // No longer present in primitives for (const primitive of model[$primitivesList]) { if (primitive.variantInfo.size > 0) { expect(primitive.variantInfo.has(2)).to.be.false; } } // Materials do not reference the variant. for (const material of model.materials) { expect(material.hasVariant('Yellow Red')).to.be.false; } }); test('hasVariant() positive and negative test', async () => { await loadModel(CUBES_GLTF_PATH); expect(model.hasVariant('Yellow Red')).to.be.true; expect(model.hasVariant('DoesNotExist')).to.be.false; }); }); suite('Intersecting', () => { test('materialFromPoint returns material', async () => { await loadModel(ASTRONAUT_GLB_PATH); await rafPasses(); const material = element.materialFromPoint(element[$scene].width / 2, element[$scene].height / 2); expect(material).to.be.ok; }); test('materialFromPoint returns null when intersect fails', async () => { await loadModel(ASTRONAUT_GLB_PATH); await rafPasses(); const material = element.materialFromPoint(element[$scene].width, element[$scene].height); expect(material).to.be.null; }); }); }); }); }); //# sourceMappingURL=model-spec.js.map