mirror of
https://github.com/CesiumGS/obj2gltf.git
synced 2024-11-23 16:44:05 -05:00
b87c761073
* Upgrade prettier to 3.0.3 and format files. * Upgrade `eslint-config-cesium` and `eslint-config-prettier`. * Switch from the defunct `eslint-plugin-node` to `eslint-plugin-n` Since that is what `eslint-config-cesium` now uses. * Switch from pretty-quick to lint-staged
712 lines
25 KiB
JavaScript
712 lines
25 KiB
JavaScript
"use strict";
|
|
const Cesium = require("cesium");
|
|
const path = require("path");
|
|
|
|
const loadObj = require("../../lib/loadObj");
|
|
const obj2gltf = require("../../lib/obj2gltf");
|
|
|
|
const Cartesian3 = Cesium.Cartesian3;
|
|
const CesiumMath = Cesium.Math;
|
|
const clone = Cesium.clone;
|
|
const RuntimeError = Cesium.RuntimeError;
|
|
|
|
const objPath = "specs/data/box/box.obj";
|
|
const objRotatedUrl = "specs/data/box-rotated/box-rotated.obj";
|
|
const objNormalsPath = "specs/data/box-normals/box-normals.obj";
|
|
const objUvsPath = "specs/data/box-uvs/box-uvs.obj";
|
|
const objPositionsOnlyPath =
|
|
"specs/data/box-positions-only/box-positions-only.obj";
|
|
const objNegativeIndicesPath =
|
|
"specs/data/box-negative-indices/box-negative-indices.obj";
|
|
const objTrianglesPath = "specs/data/box-triangles/box-triangles.obj";
|
|
const objObjectsPath = "specs/data/box-objects/box-objects.obj";
|
|
const objGroupsPath = "specs/data/box-groups/box-groups.obj";
|
|
const objObjectsGroupsPath =
|
|
"specs/data/box-objects-groups/box-objects-groups.obj";
|
|
const objObjectsGroupsMaterialsPath =
|
|
"specs/data/box-objects-groups-materials/box-objects-groups-materials.obj";
|
|
const objObjectsGroupsMaterialsPath2 =
|
|
"specs/data/box-objects-groups-materials-2/box-objects-groups-materials-2.obj";
|
|
const objUsemtlPath = "specs/data/box-usemtl/box-usemtl.obj";
|
|
const objNoMaterialsPath = "specs/data/box-no-materials/box-no-materials.obj";
|
|
const objMultipleMaterialsPath =
|
|
"specs/data/box-multiple-materials/box-multiple-materials.obj";
|
|
const objUncleanedPath = "specs/data/box-uncleaned/box-uncleaned.obj";
|
|
const objMtllibPath = "specs/data/box-mtllib/box-mtllib.obj";
|
|
const objMtllibSpacesPath = "specs/data/box-mtllib-spaces/box mtllib.obj";
|
|
const objMissingMtllibPath =
|
|
"specs/data/box-missing-mtllib/box-missing-mtllib.obj";
|
|
const objMissingUsemtlPath =
|
|
"specs/data/box-missing-usemtl/box-missing-usemtl.obj";
|
|
const objUnnamedMaterialPath =
|
|
"specs/data/box-unnamed-material/box-unnamed-material.obj";
|
|
const objExternalResourcesPath =
|
|
"specs/data/box-external-resources/box-external-resources.obj";
|
|
const objResourcesInRootPath =
|
|
"specs/data/box-resources-in-root/box-resources-in-root.obj";
|
|
const objExternalResourcesInRootPath =
|
|
"specs/data/box-external-resources-in-root/box-external-resources-in-root.obj";
|
|
const objTexturedPath = "specs/data/box-textured/box-textured.obj";
|
|
const objMissingTexturePath =
|
|
"specs/data/box-missing-texture/box-missing-texture.obj";
|
|
const objSubdirectoriesPath = "specs/data/box-subdirectories/box-textured.obj";
|
|
const objWindowsPaths = "specs/data/box-windows-paths/box-windows-paths.obj";
|
|
const objInvalidContentsPath = "specs/data/box/box.mtl";
|
|
const objConcavePath = "specs/data/concave/concave.obj";
|
|
const objUnnormalizedPath = "specs/data/box-unnormalized/box-unnormalized.obj";
|
|
const objMixedAttributesPath =
|
|
"specs/data/box-mixed-attributes/box-mixed-attributes.obj";
|
|
const objMissingAttributesPath =
|
|
"specs/data/box-missing-attributes/box-missing-attributes.obj";
|
|
const objIncompletePositionsPath =
|
|
"specs/data/box-incomplete-attributes/box-incomplete-positions.obj";
|
|
const objIncompleteNormalsPath =
|
|
"specs/data/box-incomplete-attributes/box-incomplete-normals.obj";
|
|
const objIncompleteUvsPath =
|
|
"specs/data/box-incomplete-attributes/box-incomplete-uvs.obj";
|
|
const objIncorrectWindingOrderPath =
|
|
"specs/data/box-incorrect-winding-order/box-incorrect-winding-order.obj";
|
|
const objWithTabs = "specs/data/box-with-tabs/box-with-tabs.obj";
|
|
const objInvalidPath = "invalid.obj";
|
|
|
|
function getMeshes(data) {
|
|
let meshes = [];
|
|
const nodes = data.nodes;
|
|
const nodesLength = nodes.length;
|
|
for (let i = 0; i < nodesLength; ++i) {
|
|
meshes = meshes.concat(nodes[i].meshes);
|
|
}
|
|
return meshes;
|
|
}
|
|
|
|
function getPrimitives(data) {
|
|
let primitives = [];
|
|
const nodes = data.nodes;
|
|
const nodesLength = nodes.length;
|
|
for (let i = 0; i < nodesLength; ++i) {
|
|
const meshes = nodes[i].meshes;
|
|
const meshesLength = meshes.length;
|
|
for (let j = 0; j < meshesLength; ++j) {
|
|
primitives = primitives.concat(meshes[j].primitives);
|
|
}
|
|
}
|
|
return primitives;
|
|
}
|
|
|
|
let options;
|
|
|
|
describe("loadObj", () => {
|
|
beforeEach(() => {
|
|
options = clone(obj2gltf.defaults);
|
|
options.overridingTextures = {};
|
|
options.logger = () => {};
|
|
});
|
|
|
|
it("loads obj with positions, normals, and uvs", async () => {
|
|
const data = await loadObj(objPath, options);
|
|
const materials = data.materials;
|
|
const nodes = data.nodes;
|
|
const name = data.name;
|
|
const meshes = getMeshes(data);
|
|
const primitives = getPrimitives(data);
|
|
|
|
expect(name).toBe("box");
|
|
expect(materials.length).toBe(1);
|
|
expect(nodes.length).toBe(1);
|
|
expect(meshes.length).toBe(1);
|
|
expect(primitives.length).toBe(1);
|
|
|
|
const node = nodes[0];
|
|
const mesh = meshes[0];
|
|
const primitive = primitives[0];
|
|
|
|
expect(node.name).toBe("Cube");
|
|
expect(mesh.name).toBe("Cube-Mesh");
|
|
expect(primitive.positions.length / 3).toBe(24);
|
|
expect(primitive.normals.length / 3).toBe(24);
|
|
expect(primitive.uvs.length / 2).toBe(24);
|
|
expect(primitive.indices.length).toBe(36);
|
|
expect(primitive.material).toBe("Material");
|
|
});
|
|
|
|
it("loads obj with normals", async () => {
|
|
const data = await loadObj(objNormalsPath, options);
|
|
const primitive = getPrimitives(data)[0];
|
|
expect(primitive.positions.length / 3).toBe(24);
|
|
expect(primitive.normals.length / 3).toBe(24);
|
|
expect(primitive.uvs.length / 2).toBe(0);
|
|
});
|
|
|
|
it("normalizes normals", async () => {
|
|
const data = await loadObj(objUnnormalizedPath, options);
|
|
const scratchNormal = new Cesium.Cartesian3();
|
|
const primitive = getPrimitives(data)[0];
|
|
const normals = primitive.normals;
|
|
const normalsLength = normals.length / 3;
|
|
for (let i = 0; i < normalsLength; ++i) {
|
|
const normalX = normals.get(i * 3);
|
|
const normalY = normals.get(i * 3 + 1);
|
|
const normalZ = normals.get(i * 3 + 2);
|
|
const normal = Cartesian3.fromElements(
|
|
normalX,
|
|
normalY,
|
|
normalZ,
|
|
scratchNormal,
|
|
);
|
|
expect(
|
|
CesiumMath.equalsEpsilon(
|
|
Cartesian3.magnitude(normal),
|
|
1.0,
|
|
CesiumMath.EPSILON5,
|
|
),
|
|
).toBe(true);
|
|
}
|
|
});
|
|
|
|
it("loads obj with uvs", async () => {
|
|
const data = await loadObj(objUvsPath, options);
|
|
const primitive = getPrimitives(data)[0];
|
|
expect(primitive.positions.length / 3).toBe(20);
|
|
expect(primitive.normals.length / 3).toBe(0);
|
|
expect(primitive.uvs.length / 2).toBe(20);
|
|
});
|
|
|
|
it("loads obj with negative indices", async () => {
|
|
const results = [
|
|
await loadObj(objPositionsOnlyPath, options),
|
|
await loadObj(objNegativeIndicesPath, options),
|
|
];
|
|
const positionsReference = getPrimitives(
|
|
results[0],
|
|
)[0].positions.toFloatBuffer();
|
|
const positions = getPrimitives(results[1])[0].positions.toFloatBuffer();
|
|
expect(positions).toEqual(positionsReference);
|
|
});
|
|
|
|
it("loads obj with triangle faces", async () => {
|
|
const data = await loadObj(objTrianglesPath, options);
|
|
const primitive = getPrimitives(data)[0];
|
|
expect(primitive.positions.length / 3).toBe(24);
|
|
expect(primitive.indices.length).toBe(36);
|
|
});
|
|
|
|
it("loads obj with objects", async () => {
|
|
const data = await loadObj(objObjectsPath, options);
|
|
const nodes = data.nodes;
|
|
expect(nodes.length).toBe(3);
|
|
expect(nodes[0].name).toBe("CubeBlue");
|
|
expect(nodes[1].name).toBe("CubeGreen");
|
|
expect(nodes[2].name).toBe("CubeRed");
|
|
|
|
const primitives = getPrimitives(data);
|
|
expect(primitives.length).toBe(3);
|
|
expect(primitives[0].material).toBe("Blue");
|
|
expect(primitives[1].material).toBe("Green");
|
|
expect(primitives[2].material).toBe("Red");
|
|
});
|
|
|
|
it("loads obj with groups", async () => {
|
|
const data = await loadObj(objGroupsPath, options);
|
|
const nodes = data.nodes;
|
|
expect(nodes.length).toBe(3);
|
|
expect(nodes[0].name).toBe("CubeBlue");
|
|
expect(nodes[1].name).toBe("CubeGreen");
|
|
expect(nodes[2].name).toBe("CubeRed");
|
|
|
|
const primitives = getPrimitives(data);
|
|
expect(primitives.length).toBe(3);
|
|
expect(primitives[0].material).toBe("Blue");
|
|
expect(primitives[1].material).toBe("Green");
|
|
expect(primitives[2].material).toBe("Red");
|
|
});
|
|
|
|
it("loads obj with objects and groups", async () => {
|
|
const data = await loadObj(objObjectsGroupsPath, options);
|
|
const nodes = data.nodes;
|
|
expect(nodes.length).toBe(3);
|
|
expect(nodes[0].name).toBe("CubeBlue");
|
|
expect(nodes[1].name).toBe("CubeGreen");
|
|
expect(nodes[2].name).toBe("CubeRed");
|
|
|
|
const meshes = getMeshes(data);
|
|
expect(meshes.length).toBe(3);
|
|
expect(meshes[0].name).toBe("CubeBlue_CubeBlue_Blue");
|
|
expect(meshes[1].name).toBe("CubeGreen_CubeGreen_Green");
|
|
expect(meshes[2].name).toBe("CubeRed_CubeRed_Red");
|
|
|
|
const primitives = getPrimitives(data);
|
|
expect(primitives.length).toBe(3);
|
|
expect(primitives[0].material).toBe("Blue");
|
|
expect(primitives[1].material).toBe("Green");
|
|
expect(primitives[2].material).toBe("Red");
|
|
});
|
|
|
|
function loadsObjWithObjectsGroupsAndMaterials(data) {
|
|
const nodes = data.nodes;
|
|
expect(nodes.length).toBe(1);
|
|
expect(nodes[0].name).toBe("Cube");
|
|
const meshes = getMeshes(data);
|
|
expect(meshes.length).toBe(3);
|
|
expect(meshes[0].name).toBe("Blue");
|
|
expect(meshes[1].name).toBe("Green");
|
|
expect(meshes[2].name).toBe("Red");
|
|
const primitives = getPrimitives(data);
|
|
expect(primitives.length).toBe(6);
|
|
expect(primitives[0].material).toBe("Blue");
|
|
expect(primitives[1].material).toBe("Green");
|
|
expect(primitives[2].material).toBe("Green");
|
|
expect(primitives[3].material).toBe("Red");
|
|
expect(primitives[4].material).toBe("Red");
|
|
expect(primitives[5].material).toBe("Blue");
|
|
}
|
|
|
|
it("loads obj with objects, groups, and materials", async () => {
|
|
const data = await loadObj(objObjectsGroupsMaterialsPath, options);
|
|
loadsObjWithObjectsGroupsAndMaterials(data);
|
|
});
|
|
|
|
it("loads obj with objects, groups, and materials (2)", async () => {
|
|
// The usemtl lines are placed in an unordered fashion but
|
|
// should produce the same result as the previous test
|
|
const data = await loadObj(objObjectsGroupsMaterialsPath2, options);
|
|
loadsObjWithObjectsGroupsAndMaterials(data);
|
|
});
|
|
|
|
it("loads obj with concave face containing 5 vertices", async () => {
|
|
const data = await loadObj(objConcavePath, options);
|
|
const primitive = getPrimitives(data)[0];
|
|
expect(primitive.positions.length / 3).toBe(30);
|
|
expect(primitive.indices.length).toBe(48);
|
|
});
|
|
|
|
it("loads obj with usemtl only", async () => {
|
|
const data = await loadObj(objUsemtlPath, options);
|
|
const nodes = data.nodes;
|
|
expect(nodes.length).toBe(1);
|
|
expect(nodes[0].name).toBe("Node"); // default name
|
|
|
|
const meshes = getMeshes(data);
|
|
expect(meshes.length).toBe(1);
|
|
expect(meshes[0].name).toBe("Node-Mesh");
|
|
|
|
const primitives = getPrimitives(data);
|
|
expect(primitives.length).toBe(3);
|
|
expect(primitives[0].material).toBe("Blue");
|
|
expect(primitives[1].material).toBe("Green");
|
|
expect(primitives[2].material).toBe("Red");
|
|
});
|
|
|
|
it("loads obj with no materials", async () => {
|
|
const data = await loadObj(objNoMaterialsPath, options);
|
|
const nodes = data.nodes;
|
|
expect(nodes.length).toBe(1);
|
|
expect(nodes[0].name).toBe("Node"); // default name
|
|
|
|
const primitives = getPrimitives(data);
|
|
expect(primitives.length).toBe(1);
|
|
});
|
|
|
|
it("loads obj with multiple materials", async () => {
|
|
// The usemtl markers are interleaved, but should condense to just three primitives
|
|
const data = await loadObj(objMultipleMaterialsPath, options);
|
|
const nodes = data.nodes;
|
|
expect(nodes.length).toBe(1);
|
|
|
|
const primitives = getPrimitives(data);
|
|
expect(primitives.length).toBe(3);
|
|
|
|
expect(primitives[0].indices.length).toBe(12);
|
|
expect(primitives[1].indices.length).toBe(12);
|
|
expect(primitives[2].indices.length).toBe(12);
|
|
expect(primitives[0].material).toBe("Red");
|
|
expect(primitives[1].material).toBe("Green");
|
|
expect(primitives[2].material).toBe("Blue");
|
|
|
|
for (let i = 0; i < 3; ++i) {
|
|
const indices = primitives[i].indices;
|
|
for (let j = 0; j < indices.length; ++j) {
|
|
expect(indices.get(j)).toBeLessThan(8);
|
|
}
|
|
}
|
|
});
|
|
|
|
it("loads obj uncleaned", async () => {
|
|
// Obj with extraneous o, g, and usemtl lines
|
|
// Also tests handling of o and g lines with the same names
|
|
const data = await loadObj(objUncleanedPath, options);
|
|
const nodes = data.nodes;
|
|
const meshes = getMeshes(data);
|
|
const primitives = getPrimitives(data);
|
|
|
|
expect(nodes.length).toBe(1);
|
|
expect(meshes.length).toBe(1);
|
|
expect(primitives.length).toBe(1);
|
|
|
|
expect(nodes[0].name).toBe("Cube");
|
|
expect(meshes[0].name).toBe("Cube_1");
|
|
});
|
|
|
|
it("loads obj with multiple mtllibs", async () => {
|
|
const data = await loadObj(objMtllibPath, options);
|
|
const materials = data.materials;
|
|
expect(materials.length).toBe(3);
|
|
|
|
// .mtl files are loaded in an arbitrary order, so sort for testing purposes
|
|
materials.sort((a, b) => {
|
|
return a.name.localeCompare(b.name);
|
|
});
|
|
|
|
expect(materials[0].name).toBe("Blue");
|
|
expect(materials[0].pbrMetallicRoughness.baseColorFactor).toEqual([
|
|
0.0, 0.0, 0.64, 1.0,
|
|
]);
|
|
expect(materials[1].name).toBe("Green");
|
|
expect(materials[1].pbrMetallicRoughness.baseColorFactor).toEqual([
|
|
0.0, 0.64, 0.0, 1.0,
|
|
]);
|
|
expect(materials[2].name).toBe("Red");
|
|
expect(materials[2].pbrMetallicRoughness.baseColorFactor).toEqual([
|
|
0.64, 0.0, 0.0, 1.0,
|
|
]);
|
|
});
|
|
|
|
it("loads obj with mtllib paths with spaces", async () => {
|
|
const data = await loadObj(objMtllibSpacesPath, options);
|
|
const materials = data.materials;
|
|
expect(materials.length).toBe(3);
|
|
|
|
// .mtl files are loaded in an arbitrary order, so sort for testing purposes
|
|
materials.sort((a, b) => {
|
|
return a.name.localeCompare(b.name);
|
|
});
|
|
|
|
expect(materials[0].name).toBe("Blue");
|
|
expect(materials[0].pbrMetallicRoughness.baseColorFactor).toEqual([
|
|
0.0, 0.0, 0.64, 1.0,
|
|
]);
|
|
expect(materials[1].name).toBe("Green");
|
|
expect(materials[1].pbrMetallicRoughness.baseColorFactor).toEqual([
|
|
0.0, 0.64, 0.0, 1.0,
|
|
]);
|
|
expect(materials[2].name).toBe("Red");
|
|
expect(materials[2].pbrMetallicRoughness.baseColorFactor).toEqual([
|
|
0.64, 0.0, 0.0, 1.0,
|
|
]);
|
|
});
|
|
|
|
it("loads obj with missing mtllib", async () => {
|
|
const spy = jasmine.createSpy("logger");
|
|
options.logger = spy;
|
|
|
|
const data = await loadObj(objMissingMtllibPath, options);
|
|
expect(data.materials.length).toBe(0);
|
|
expect(spy.calls.argsFor(0)[0].indexOf("ENOENT") >= 0).toBe(true);
|
|
expect(spy.calls.argsFor(0)[0].indexOf(path.resolve("/box.mtl")) >= 0).toBe(
|
|
true,
|
|
);
|
|
expect(
|
|
spy.calls
|
|
.argsFor(1)[0]
|
|
.indexOf(
|
|
"Attempting to read the material file from within the obj directory instead.",
|
|
) >= 0,
|
|
).toBe(true);
|
|
expect(spy.calls.argsFor(2)[0].indexOf("ENOENT") >= 0).toBe(true);
|
|
expect(
|
|
spy.calls.argsFor(3)[0].indexOf("Could not read material file") >= 0,
|
|
).toBe(true);
|
|
});
|
|
|
|
it("loads obj with missing usemtl", async () => {
|
|
const data = await loadObj(objMissingUsemtlPath, options);
|
|
expect(data.materials.length).toBe(1);
|
|
expect(data.nodes[0].meshes[0].primitives[0].material).toBe("Material");
|
|
});
|
|
|
|
it("loads obj with unnamed material", async () => {
|
|
const data = await loadObj(objUnnamedMaterialPath, options);
|
|
expect(data.materials.length).toBe(1);
|
|
expect(data.nodes[0].meshes[0].primitives[0].material).toBe("");
|
|
});
|
|
|
|
it("loads .mtl outside of the obj directory", async () => {
|
|
const data = await loadObj(objExternalResourcesPath, options);
|
|
const materials = data.materials;
|
|
expect(materials.length).toBe(2);
|
|
|
|
// .mtl files are loaded in an arbitrary order, so find the "MaterialTextured" material
|
|
const materialTextured =
|
|
materials[0].name === "MaterialTextured" ? materials[0] : materials[1];
|
|
const baseColorTexture =
|
|
materialTextured.pbrMetallicRoughness.baseColorTexture;
|
|
expect(baseColorTexture.source).toBeDefined();
|
|
expect(baseColorTexture.name).toEqual("cesium");
|
|
});
|
|
|
|
it("does not load .mtl outside of the obj directory when secure is true", async () => {
|
|
const spy = jasmine.createSpy("logger");
|
|
options.logger = spy;
|
|
options.secure = true;
|
|
|
|
const data = await loadObj(objExternalResourcesPath, options);
|
|
expect(data.materials.length).toBe(1); // obj references 2 materials, one of which is outside the input directory
|
|
expect(
|
|
spy.calls
|
|
.argsFor(0)[0]
|
|
.indexOf(
|
|
"The material file is outside of the obj directory and the secure flag is true. Attempting to read the material file from within the obj directory instead.",
|
|
) >= 0,
|
|
).toBe(true);
|
|
expect(spy.calls.argsFor(1)[0].indexOf("ENOENT") >= 0).toBe(true);
|
|
expect(
|
|
spy.calls.argsFor(2)[0].indexOf("Could not read material file") >= 0,
|
|
).toBe(true);
|
|
});
|
|
|
|
it("loads .mtl from root directory when the .mtl path does not exist", async () => {
|
|
const data = await loadObj(objResourcesInRootPath, options);
|
|
const baseColorTexture =
|
|
data.materials[0].pbrMetallicRoughness.baseColorTexture;
|
|
expect(baseColorTexture.name).toBe("cesium");
|
|
expect(baseColorTexture.source).toBeDefined();
|
|
});
|
|
|
|
it("loads .mtl from root directory when the .mtl path is outside of the obj directory and secure is true", async () => {
|
|
options.secure = true;
|
|
|
|
const data = await loadObj(objExternalResourcesInRootPath, options);
|
|
const materials = data.materials;
|
|
expect(materials.length).toBe(2);
|
|
|
|
// .mtl files are loaded in an arbitrary order, so find the "MaterialTextured" material
|
|
const materialTextured =
|
|
materials[0].name === "MaterialTextured" ? materials[0] : materials[1];
|
|
const baseColorTexture =
|
|
materialTextured.pbrMetallicRoughness.baseColorTexture;
|
|
expect(baseColorTexture.source).toBeDefined();
|
|
expect(baseColorTexture.name).toEqual("cesium");
|
|
});
|
|
|
|
it("loads obj with texture", async () => {
|
|
const data = await loadObj(objTexturedPath, options);
|
|
const baseColorTexture =
|
|
data.materials[0].pbrMetallicRoughness.baseColorTexture;
|
|
expect(baseColorTexture.name).toBe("cesium");
|
|
expect(baseColorTexture.source).toBeDefined();
|
|
});
|
|
|
|
it("loads obj with missing texture", async () => {
|
|
const spy = jasmine.createSpy("logger");
|
|
options.logger = spy;
|
|
|
|
const data = await loadObj(objMissingTexturePath, options);
|
|
const baseColorTexture =
|
|
data.materials[0].pbrMetallicRoughness.baseColorTexture;
|
|
expect(baseColorTexture).toBeUndefined();
|
|
expect(spy.calls.argsFor(0)[0].indexOf("ENOENT") >= 0).toBe(true);
|
|
expect(
|
|
spy.calls.argsFor(0)[0].indexOf(path.resolve("/cesium.png")) >= 0,
|
|
).toBe(true);
|
|
expect(
|
|
spy.calls
|
|
.argsFor(1)[0]
|
|
.indexOf(
|
|
"Attempting to read the texture file from within the obj directory instead.",
|
|
) >= 0,
|
|
).toBe(true);
|
|
expect(spy.calls.argsFor(2)[0].indexOf("ENOENT") >= 0).toBe(true);
|
|
expect(
|
|
spy.calls.argsFor(3)[0].indexOf("Could not read texture file") >= 0,
|
|
).toBe(true);
|
|
});
|
|
|
|
it("loads obj with subdirectories", async () => {
|
|
const data = await loadObj(objSubdirectoriesPath, options);
|
|
const baseColorTexture =
|
|
data.materials[0].pbrMetallicRoughness.baseColorTexture;
|
|
expect(baseColorTexture.name).toBe("cesium");
|
|
expect(baseColorTexture.source).toBeDefined();
|
|
});
|
|
|
|
it("loads obj with windows paths", async () => {
|
|
const data = await loadObj(objWindowsPaths, options);
|
|
const baseColorTexture =
|
|
data.materials[0].pbrMetallicRoughness.baseColorTexture;
|
|
expect(baseColorTexture.name).toBe("cesium");
|
|
expect(baseColorTexture.source).toBeDefined();
|
|
});
|
|
|
|
it("loads an obj where coordinates are separated by tabs", async () => {
|
|
/**
|
|
* We know Tinkercad to produce files with coordinates separated by tabs.
|
|
*/
|
|
const data = await loadObj(objWithTabs, options);
|
|
const primitive = getPrimitives(data)[0];
|
|
expect(primitive.positions.length / 3).toBe(24);
|
|
expect(primitive.normals.length / 3).toBe(24);
|
|
expect(primitive.uvs.length / 2).toBe(24);
|
|
});
|
|
|
|
it("separates faces that don't use the same attributes as other faces in the primitive", async () => {
|
|
const data = await loadObj(objMixedAttributesPath, options);
|
|
const primitives = getPrimitives(data);
|
|
expect(primitives.length).toBe(4);
|
|
expect(primitives[0].indices.length).toBe(18); // 6 faces
|
|
expect(primitives[1].indices.length).toBe(6); // 2 faces
|
|
expect(primitives[2].indices.length).toBe(6); // 2 faces
|
|
expect(primitives[3].indices.length).toBe(6); // 2 faces
|
|
});
|
|
|
|
function getFirstPosition(data) {
|
|
const primitive = getPrimitives(data)[0];
|
|
return new Cartesian3(
|
|
primitive.positions.get(0),
|
|
primitive.positions.get(1),
|
|
primitive.positions.get(2),
|
|
);
|
|
}
|
|
|
|
function getFirstNormal(data) {
|
|
const primitive = getPrimitives(data)[0];
|
|
return new Cartesian3(
|
|
primitive.normals.get(0),
|
|
primitive.normals.get(1),
|
|
primitive.normals.get(2),
|
|
);
|
|
}
|
|
|
|
async function checkAxisConversion(
|
|
inputUpAxis,
|
|
outputUpAxis,
|
|
position,
|
|
normal,
|
|
) {
|
|
const sameAxis = inputUpAxis === outputUpAxis;
|
|
options.inputUpAxis = inputUpAxis;
|
|
options.outputUpAxis = outputUpAxis;
|
|
const data = await loadObj(objRotatedUrl, options);
|
|
const rotatedPosition = getFirstPosition(data);
|
|
const rotatedNormal = getFirstNormal(data);
|
|
if (sameAxis) {
|
|
expect(rotatedPosition).toEqual(position);
|
|
expect(rotatedNormal).toEqual(normal);
|
|
} else {
|
|
expect(rotatedPosition).not.toEqual(position);
|
|
expect(rotatedNormal).not.toEqual(normal);
|
|
}
|
|
}
|
|
|
|
it("performs up axis conversion", async () => {
|
|
const data = await loadObj(objRotatedUrl, options);
|
|
const position = getFirstPosition(data);
|
|
const normal = getFirstNormal(data);
|
|
|
|
const axes = ["X", "Y", "Z"];
|
|
const axesLength = axes.length;
|
|
for (let i = 0; i < axesLength; ++i) {
|
|
for (let j = 0; j < axesLength; ++j) {
|
|
await checkAxisConversion(axes[i], axes[j], position, normal);
|
|
}
|
|
}
|
|
});
|
|
|
|
it("ignores missing normals and uvs", async () => {
|
|
const data = await loadObj(objMissingAttributesPath, options);
|
|
const primitive = getPrimitives(data)[0];
|
|
expect(primitive.positions.length).toBeGreaterThan(0);
|
|
expect(primitive.normals.length).toBe(0);
|
|
expect(primitive.uvs.length).toBe(0);
|
|
});
|
|
|
|
async function loadAndGetIndices(objPath, options) {
|
|
const data = await loadObj(objPath, options);
|
|
const primitive = getPrimitives(data)[0];
|
|
const indices = primitive.indices;
|
|
return new Uint16Array(indices.toUint16Buffer().buffer);
|
|
}
|
|
|
|
it("applies triangle winding order sanitization", async () => {
|
|
options.triangleWindingOrderSanitization = false;
|
|
const indicesIncorrect = await loadAndGetIndices(
|
|
objIncorrectWindingOrderPath,
|
|
options,
|
|
);
|
|
|
|
options.triangleWindingOrderSanitization = true;
|
|
const indicesCorrect = await loadAndGetIndices(
|
|
objIncorrectWindingOrderPath,
|
|
options,
|
|
);
|
|
|
|
expect(indicesIncorrect[0]).toBe(0);
|
|
expect(indicesIncorrect[2]).toBe(2);
|
|
expect(indicesIncorrect[1]).toBe(1);
|
|
|
|
expect(indicesCorrect[0]).toBe(0);
|
|
expect(indicesCorrect[2]).toBe(1);
|
|
expect(indicesCorrect[1]).toBe(2);
|
|
});
|
|
|
|
it("throws when position index is out of bounds", async () => {
|
|
let thrownError;
|
|
try {
|
|
await loadObj(objIncompletePositionsPath, options);
|
|
} catch (e) {
|
|
thrownError = e;
|
|
}
|
|
expect(thrownError).toEqual(
|
|
new RuntimeError("Position index 1 is out of bounds"),
|
|
);
|
|
});
|
|
|
|
it("throws when normal index is out of bounds", async () => {
|
|
let thrownError;
|
|
try {
|
|
await loadObj(objIncompleteNormalsPath, options);
|
|
} catch (e) {
|
|
thrownError = e;
|
|
}
|
|
expect(thrownError).toEqual(
|
|
new RuntimeError("Normal index 1 is out of bounds"),
|
|
);
|
|
});
|
|
|
|
it("throws when uv index is out of bounds", async () => {
|
|
let thrownError;
|
|
try {
|
|
await loadObj(objIncompleteUvsPath, options);
|
|
} catch (e) {
|
|
thrownError = e;
|
|
}
|
|
expect(thrownError).toEqual(
|
|
new RuntimeError("UV index 1 is out of bounds"),
|
|
);
|
|
});
|
|
|
|
it("throws when file has invalid contents", async () => {
|
|
let thrownError;
|
|
try {
|
|
await loadObj(objInvalidContentsPath, options);
|
|
} catch (e) {
|
|
thrownError = e;
|
|
}
|
|
expect(thrownError).toEqual(
|
|
new RuntimeError(
|
|
`${objInvalidContentsPath} does not have any geometry data`,
|
|
),
|
|
);
|
|
});
|
|
|
|
it("throw when reading invalid file", async () => {
|
|
let thrownError;
|
|
try {
|
|
await loadObj(objInvalidPath, options);
|
|
} catch (e) {
|
|
thrownError = e;
|
|
}
|
|
expect(
|
|
thrownError.message.startsWith("ENOENT: no such file or directory"),
|
|
).toBe(true);
|
|
});
|
|
});
|