@google/model-viewer
Version:
Easily display interactive 3D models on the web and in AR!
341 lines (273 loc) • 13.1 kB
text/typescript
/* @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 {MeshStandardMaterial} from 'three/src/materials/MeshStandardMaterial.js';
import {Mesh} from 'three/src/objects/Mesh.js';
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: Set<MeshStandardMaterial> = new Set();
threeGLTF.scene.traverse((object) => {
if ((object as Mesh).isMesh) {
const material = (object as Mesh).material;
if (Array.isArray(material)) {
material.forEach(
(material) => materials.add(material as MeshStandardMaterial));
} else {
materials.add(material as MeshStandardMaterial);
}
}
});
const model = new Model(CorrelatedSceneGraph.from(threeGLTF));
const collectedMaterials = new Set<MeshStandardMaterial>();
model.materials.forEach((material) => {
for (const threeMaterial of material[$correlatedObjects] as
Set<MeshStandardMaterial>) {
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: ModelViewerElement;
let model: Model;
setup(async () => {
element = new ModelViewerElement();
});
teardown(() => {
document.body.removeChild(element);
});
const loadModel = async (src: string) => {
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;
});
});
});
});
});