@itwin/core-backend
Version:
iTwin.js backend components
849 lines • 60.5 kB
JavaScript
/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
import { assert } from "chai";
import * as fs from "fs";
import { DbResult, Id64 } from "@itwin/core-bentley";
import { Code, ColorDef, FillDisplay, GeometryClass, GeometryParams, GeometryStreamBuilder, ImageSourceFormat, IModel, LineStyle, TextureMapUnits, } from "@itwin/core-common";
import { Angle, Box, Geometry, GrowableXYArray, GrowableXYZArray, LineSegment3d, LineString3d, Loop, NumberArray, Point2dArray, Point3d, Point3dArray, PolyfaceBuilder, Range3d, Sphere, StrokeOptions, Vector3d, Vector3dArray, } from "@itwin/core-geometry";
import { ExportGraphics, ExportGraphicsMeshVisitor, LineStyleDefinition, PhysicalObject, RenderMaterialElement, SnapshotDb, Texture, } from "../../core-backend";
import { GeometryPart } from "../../Element";
import { IModelTestUtils } from "../IModelTestUtils";
describe("exportGraphics", () => {
let iModel;
let seedModel;
let seedCategory;
function insertPhysicalElement(geometryStream) {
const elementProps = {
classFullName: PhysicalObject.classFullName,
model: seedModel,
category: seedCategory,
code: Code.createEmpty(),
geom: geometryStream,
};
return iModel.elements.insertElement(elementProps);
}
function insertRenderMaterialWithTexture(name, textureId, patternScale, patternScaleMode) {
// eslint-disable-next-line @typescript-eslint/naming-convention
const props = { TextureId: textureId, pattern_offset: [0.0, 0.0] };
if (patternScale)
props.pattern_scale = patternScale;
if (patternScaleMode)
props.pattern_scalemode = patternScaleMode;
return RenderMaterialElement.insert(iModel, IModel.dictionaryId, name, { paletteName: "test-palette", patternMap: props });
}
function insertRenderMaterial(name, colorDef) {
const colors = colorDef.colors;
return RenderMaterialElement.insert(iModel, IModel.dictionaryId, name, {
paletteName: "test-palette",
color: [colors.r / 255, colors.g / 255, colors.b / 255],
transmit: colors.t / 255,
});
}
before(() => {
const seedFileName = IModelTestUtils.resolveAssetFile("CompatibilityTestSeed.bim");
const testFileName = IModelTestUtils.prepareOutputFile("ExportGraphics", "ExportGraphicsTest.bim");
iModel = IModelTestUtils.createSnapshotFromSeed(testFileName, seedFileName);
// Get known model/category from seed element a la GeometryStream.test.ts
const seedElement = iModel.elements.getElement("0x1d");
assert.exists(seedElement);
assert.isTrue(seedElement.federationGuid === "18eb4650-b074-414f-b961-d9cfaa6c8746");
seedModel = seedElement.model;
seedCategory = seedElement.category;
});
after(() => {
iModel.close();
});
it("resolves element color correctly", () => {
const elementColor = ColorDef.fromString("cadetBlue");
const builder = new GeometryStreamBuilder();
const geometryParams = new GeometryParams(seedCategory);
geometryParams.lineColor = elementColor;
builder.appendGeometryParamsChange(geometryParams);
builder.appendGeometry(Sphere.createCenterRadius(Point3d.createZero(), 1));
const geometricElementId = insertPhysicalElement(builder.geometryStream);
const infos = [];
const exportGraphicsOptions = {
elementIdArray: [geometricElementId],
onGraphics: (info) => infos.push(info),
};
const exportStatus = iModel.exportGraphics(exportGraphicsOptions);
assert.strictEqual(exportStatus, DbResult.BE_SQLITE_OK);
assert.strictEqual(infos.length, 1);
assert.strictEqual(infos[0].elementId, geometricElementId);
assert.strictEqual(infos[0].color, elementColor.tbgr);
});
it("resolves material color and transparency correctly", () => {
const materialColor = ColorDef.fromString("honeydew").withTransparency(80);
const materialId = insertRenderMaterial("test-material", materialColor);
const builder = new GeometryStreamBuilder();
const geometryParams = new GeometryParams(seedCategory);
geometryParams.lineColor = ColorDef.fromString("rebeccaPurple"); // line color should be superceded by material color
geometryParams.materialId = materialId;
builder.appendGeometryParamsChange(geometryParams);
builder.appendGeometry(Sphere.createCenterRadius(Point3d.createZero(), 1));
const geometricElementId = insertPhysicalElement(builder.geometryStream);
const infos = [];
const exportGraphicsOptions = {
elementIdArray: [geometricElementId],
onGraphics: (info) => infos.push(info),
};
const exportStatus = iModel.exportGraphics(exportGraphicsOptions);
assert.strictEqual(exportStatus, DbResult.BE_SQLITE_OK);
assert.strictEqual(infos.length, 1);
assert.strictEqual(infos[0].elementId, geometricElementId);
assert.strictEqual(infos[0].materialId, materialId);
assert.strictEqual(infos[0].color, materialColor.tbgr);
assert.isUndefined(infos[0].textureId);
});
it("resolves color face symbology correctly", () => {
const color0 = ColorDef.fromString("sienna");
const color1 = ColorDef.fromString("plum");
const builder = new GeometryStreamBuilder();
const geometryParams = new GeometryParams(seedCategory);
geometryParams.lineColor = ColorDef.fromString("peachPuff"); // line color should be superceded by face color
builder.appendGeometryParamsChange(geometryParams);
// Cube with one material attached to two faces and another material attached to other four faces
const testBrepData = JSON.parse(fs.readFileSync(IModelTestUtils.resolveAssetFile("brep-face-symb.json"), { encoding: "utf8" })).data;
builder.appendBRepData({
data: testBrepData,
faceSymbology: [
{ color: color0.toJSON() },
{ color: color1.toJSON() },
],
});
const geometricElementId = insertPhysicalElement(builder.geometryStream);
const infos = [];
const exportGraphicsOptions = {
elementIdArray: [geometricElementId],
onGraphics: (info) => infos.push(info),
};
const exportStatus = iModel.exportGraphics(exportGraphicsOptions);
assert.strictEqual(exportStatus, DbResult.BE_SQLITE_OK);
// 4 faces of slab have color1, 2 faces have color0.
// Triangulated, should split into info with 24 indices and info with 12 indices.
assert.strictEqual(infos.length, 2);
assert.strictEqual(infos[0].elementId, geometricElementId);
assert.strictEqual(infos[0].mesh.indices.length, 24);
assert.strictEqual(infos[0].color, color1.tbgr);
assert.strictEqual(infos[1].elementId, geometricElementId);
assert.strictEqual(infos[1].mesh.indices.length, 12);
assert.strictEqual(infos[1].color, color0.tbgr);
});
it("resolves material face symbology correctly", () => {
const materialColor0 = ColorDef.fromString("honeydew").withTransparency(80);
const materialId0 = insertRenderMaterial("test-material-0", materialColor0);
const materialColor1 = ColorDef.fromString("wheat").withTransparency(240);
const materialId1 = insertRenderMaterial("test-material-1", materialColor1);
const builder = new GeometryStreamBuilder();
const geometryParams = new GeometryParams(seedCategory);
geometryParams.lineColor = ColorDef.fromString("peachPuff"); // line color should be superceded by material color
builder.appendGeometryParamsChange(geometryParams);
const testBrepData = JSON.parse(fs.readFileSync(IModelTestUtils.resolveAssetFile("brep-face-symb.json"), { encoding: "utf8" })).data;
builder.appendBRepData({
// Cube with one material attached to two faces and another material attached to other four faces
data: testBrepData,
faceSymbology: [
{ materialId: materialId0 },
{ materialId: materialId1 },
],
});
const geometricElementId = insertPhysicalElement(builder.geometryStream);
const infos = [];
const exportGraphicsOptions = {
elementIdArray: [geometricElementId],
onGraphics: (info) => infos.push(info),
};
const exportStatus = iModel.exportGraphics(exportGraphicsOptions);
assert.strictEqual(exportStatus, DbResult.BE_SQLITE_OK);
assert.strictEqual(infos.length, 2);
// 4 faces of slab have material1, 2 faces have material0.
// Triangulated, should split into info with 24 indices and info with 12 indices.
assert.strictEqual(infos.length, 2);
assert.strictEqual(infos[0].elementId, geometricElementId);
assert.strictEqual(infos[0].mesh.indices.length, 24);
assert.strictEqual(infos[0].color, materialColor1.tbgr);
assert.strictEqual(infos[1].elementId, geometricElementId);
assert.strictEqual(infos[1].mesh.indices.length, 12);
assert.strictEqual(infos[1].color, materialColor0.tbgr);
});
let textureIdString;
function getTextureId() {
if (textureIdString !== undefined)
return textureIdString;
// This is an encoded png containing a 3x3 square with white in top left pixel, blue in middle pixel, and green in
// bottom right pixel. The rest of the square is red.
const pngData = new Uint8Array([137, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, 82, 0, 0, 0, 3, 0, 0, 0, 3, 8, 2, 0, 0, 0, 217,
74, 34, 232, 0, 0, 0, 1, 115, 82, 71, 66, 0, 174, 206, 28, 233, 0, 0, 0, 4, 103, 65, 77, 65, 0, 0, 177, 143, 11, 252,
97, 5, 0, 0, 0, 9, 112, 72, 89, 115, 0, 0, 14, 195, 0, 0, 14, 195, 1, 199, 111, 168, 100, 0, 0, 0, 24, 73, 68, 65,
84, 24, 87, 99, 248, 15, 4, 12, 12, 64, 4, 198, 64, 46, 132, 5, 162, 254, 51, 0, 0, 195, 90, 10, 246, 127, 175, 154, 145, 0,
0, 0, 0, 73, 69, 78, 68, 174, 66, 96, 130,
]);
return textureIdString = Texture.insertTexture(iModel, IModel.dictionaryId, "test-texture", ImageSourceFormat.Png, pngData);
}
it("handles materials with textures", () => {
const textureId = getTextureId();
const materialId = insertRenderMaterialWithTexture("test-material-2", textureId);
const elementColor = ColorDef.fromString("aquamarine");
const builder = new GeometryStreamBuilder();
const geometryParams = new GeometryParams(seedCategory);
geometryParams.materialId = materialId;
geometryParams.lineColor = elementColor; // element color should still come through with no material color set
builder.appendGeometryParamsChange(geometryParams);
builder.appendGeometry(Sphere.createCenterRadius(Point3d.createZero(), 1));
const geometricElementId = insertPhysicalElement(builder.geometryStream);
const infos = [];
const exportGraphicsOptions = {
elementIdArray: [geometricElementId],
onGraphics: (info) => infos.push(info),
};
const exportStatus = iModel.exportGraphics(exportGraphicsOptions);
assert.strictEqual(exportStatus, DbResult.BE_SQLITE_OK);
assert.strictEqual(infos.length, 1);
assert.strictEqual(infos[0].elementId, geometricElementId);
assert.strictEqual(infos[0].materialId, materialId);
assert.strictEqual(infos[0].textureId, textureId);
assert.strictEqual(infos[0].color, elementColor.tbgr);
});
it("creates meshes with expected parameters", () => {
const makeTriangle = (data, i0, i1, i2, dim) => {
const tri = [];
for (const i of [i0, i1, i2])
for (let j = 0; j < dim; ++j)
tri.push(data[dim * i + j]);
return tri;
};
const makeFacet = (builder, xyz, normals, uv, i0, i1, i2) => {
builder.addFacetFromGrowableArrays(GrowableXYZArray.create(makeTriangle(xyz, i0, i1, i2, 3)), GrowableXYZArray.create(makeTriangle(normals, i0, i1, i2, 3)), GrowableXYArray.create(makeTriangle(uv, i0, i1, i2, 2)), undefined);
};
const negateV = (param, index) => {
return (index % 2) ? 1 - param : param;
};
const unNegateV = (param, index) => {
return (index % 2) ? 2 - param : param;
};
/** return 2x2 array of uvParams: [vNegate][meters] given raw uv and the [1][1] entry */
const mutateUV = (uvRaw, uvVNegatedMeters) => {
const uvArray = [[], []];
uvArray[1].push(Float32Array.from(uvRaw, negateV));
uvArray[1].push(uvVNegatedMeters);
for (let i = 0; i < 2; ++i)
uvArray[0].push(Float32Array.from(uvArray[1][i], unNegateV));
return uvArray;
};
const materials = [["", ""], ["", ""]];
const getMaterial = (vNegate, meters) => {
const i = vNegate ? 1 : 0;
const j = meters ? 1 : 0;
if (materials[i][j] !== "")
return materials[i][j];
const matName = `test-material-${vNegate ? `vScaleMinus1` : `vScale1`}-${meters ? `meters` : `relative`}`;
const matScale = vNegate ? [1, -1] : [1, 1];
const matUnits = meters ? TextureMapUnits.Meters : TextureMapUnits.Relative;
return materials[i][j] = insertRenderMaterialWithTexture(matName, getTextureId(), matScale, matUnits);
};
const triangleBuilder = PolyfaceBuilder.create();
triangleBuilder.options.needParams = triangleBuilder.options.needNormals = true;
const triangleIndices = new Int32Array([0, 1, 2]);
const triangleXYZ = new Float32Array([1, 0, 1, 0, 0, -1, -1, 0, 1]);
const triangleNormals = new Float32Array([0, 1, 0, 0, 1, 0, 0, 1, 0]);
const triangleUV = new Float32Array([1, 1, 0, 0, 0, 1]);
const triangleUVScaled = new Float32Array([2, 1 - Math.sqrt(5), 0, 1, 0, 1 - Math.sqrt(5)]);
for (let i = 0; i < triangleIndices.length; i += 3)
makeFacet(triangleBuilder, triangleXYZ, triangleNormals, triangleUV, triangleIndices[i], triangleIndices[i + 1], triangleIndices[i + 2]);
const triangle = triangleBuilder.claimPolyface();
const triangleParams = mutateUV(triangleUV, triangleUVScaled);
const cubeBuilder = PolyfaceBuilder.create();
cubeBuilder.options.needParams = cubeBuilder.options.needNormals = true;
const cubeIndices = new Int32Array([0, 1, 2, 2, 1, 3, 4, 5, 6, 6, 5, 7, 8, 9, 10, 10, 9, 11, 12, 13, 14, 14, 13, 15, 16, 17, 18, 18, 17, 19, 20, 21, 22, 22, 21, 23]);
const cubeXYZ = new Float32Array([-1, 1, -1, -1, 1, 1, 1, 1, -1, 1, 1, 1, 1, 1, 1, -1, 1, 1, 1, -1, 1, -1, -1, 1, -1, 1, 1, -1, 1, -1, -1, -1, 1, -1, -1, -1, 1, -1, -1, 1, -1, 1, -1, -1, -1, -1, -1, 1, 1, 1, -1, 1, 1, 1, 1, -1, -1, 1, -1, 1, -1, 1, -1, 1, 1, -1, -1, -1, -1, 1, -1, -1]);
const cubeNormals = new Float32Array([0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, -1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, -1]);
const cubeUV = new Float32Array([0.875, 0.5, 0.875, 0.25, 0.625, 0.5, 0.625, 0.25, 0.625, 0.25, 0.625, 0, 0.375, 0.25, 0.375, 0, 0.625, 1, 0.625, 0.75, 0.375, 1, 0.375, 0.75, 0.375, 0.5, 0.375, 0.25, 0.125, 0.5, 0.125, 0.25, 0.625, 0.5, 0.625, 0.25, 0.375, 0.5, 0.375, 0.25, 0.625, 0.75, 0.625, 0.5, 0.375, 0.75, 0.375, 0.5]);
const cubeUVScaled = new Float32Array([5.25, -3, 5.25, -1, 3.75, -3, 3.75, -1, 3.75, -1, 3.75, 1, 2.25, -1, 2.25, 1, 3.75, -7, 3.75, -5, 2.25, -7, 2.25, -5, 2.25, -3, 2.25, -1, 0.75, -3, 0.75, -1, 3.75, -3, 3.75, -1, 2.25, -3, 2.25, -1, 3.75, -5, 3.75, -3, 2.25, -5, 2.25, -3]);
for (let i = 0; i < cubeIndices.length; i += 3)
makeFacet(cubeBuilder, cubeXYZ, cubeNormals, cubeUV, cubeIndices[i], cubeIndices[i + 1], cubeIndices[i + 2]);
const cube = cubeBuilder.claimPolyface();
const cubeParams = mutateUV(cubeUV, cubeUVScaled);
const testUVParamExport = (geomId, expectedParams) => {
const infos = [];
const exportGraphicsOptions = {
elementIdArray: [geomId],
onGraphics: (info) => infos.push(info),
};
const exportStatus = iModel.exportGraphics(exportGraphicsOptions);
assert.strictEqual(exportStatus, DbResult.BE_SQLITE_OK);
assert.strictEqual(infos.length, 1);
assert.strictEqual(infos[0].mesh.params.length, expectedParams.length);
assert.deepStrictEqual(infos[0].mesh.params, expectedParams, "exported params as expected");
};
const testMeshWithTexture = (geom, expectedUV, material) => {
const streamBuilder = new GeometryStreamBuilder();
const geometryParams = new GeometryParams(seedCategory);
geometryParams.materialId = material;
streamBuilder.appendGeometryParamsChange(geometryParams);
streamBuilder.appendGeometry(geom);
const geomId = insertPhysicalElement(streamBuilder.geometryStream);
testUVParamExport(geomId, expectedUV);
};
const testMesh = (geom, expectedParams) => {
for (const vNegate of [false, true])
for (const meters of [false, true])
testMeshWithTexture(geom, expectedParams[vNegate ? 1 : 0][meters ? 1 : 0], getMaterial(vNegate, meters));
};
testMesh(triangle, triangleParams);
testMesh(cube, cubeParams);
});
it("verifies export of 3d linestyle as parts", () => {
const resolvedBimFile = IModelTestUtils.resolveAssetFile("3dLinestyle.bim");
assert.isNotEmpty(resolvedBimFile);
const myIModel = SnapshotDb.openFile(resolvedBimFile);
const elementIdArray = ["0x5a"];
const infos = [];
const partInstanceArray = [];
const onGraphics = info => infos.push(info);
const countPartIds = (partId) => partInstanceArray.reduce((sum, instance) => sum + (instance.partId === partId ? 1 : 0), 0);
// exportGraphics populates partInstanceArray with any part instances found; onGraphics gets non-part meshes
assert.strictEqual(DbResult.BE_SQLITE_OK, myIModel.exportGraphics({ elementIdArray, onGraphics, partInstanceArray }), "export with instancing successful");
assert.strictEqual(infos.length, 2); // TODO: infos looks like the part 0x4c transformed. Why isn't this geometry instanced and infos empty?
assert.strictEqual(partInstanceArray.length, 144, "export with instancing returned expected # part instances");
assert.strictEqual(142, countPartIds('0x4c'), "guardrail geometry has expected # post assembly instances");
assert.strictEqual(1, countPartIds('0x4d'), "guardrail geometry has expected # start assembly instances");
assert.strictEqual(1, countPartIds('0x4e'), "guardrail geometry has expected # end assembly instances");
// TODO: why do parts 0x4d and 0x4e look identical?
myIModel.close();
});
it("creates meshes with vertices shared as expected", () => {
const builder = new GeometryStreamBuilder();
// 1, 1, 1 box
const box = Box.createDgnBox(Point3d.createZero(), Vector3d.create(1, 0, 0), Vector3d.create(0, 1, 0), Point3d.create(0, 0, 1), 1, 1, 1, 1, true);
assert.isDefined(box);
builder.appendGeometry(box);
const newId = insertPhysicalElement(builder.geometryStream);
const infos = [];
const exportGraphicsOptions = {
elementIdArray: [newId],
onGraphics: (info) => infos.push(info),
};
// exportGraphics is expected to compress vertices to share them where point, normal and param are all equal.
// So, given a triangulated box, the two vertices along the diagonal should be shared inside the face (where the normal matches)
// but not across any of the neighboring perpendicular faces.
// For a box, 6 faces with 4 unique vertices each made up of 3 (xyz) values means we should expect points.length === 72
// Without compression, we'd expect to see points.length === 108
const exportStatus = iModel.exportGraphics(exportGraphicsOptions);
assert.strictEqual(exportStatus, DbResult.BE_SQLITE_OK);
assert.strictEqual(infos.length, 1);
const numUniqueVertices = 24;
assert.strictEqual(infos[0].mesh.points.length, numUniqueVertices * 3);
assert.strictEqual(infos[0].mesh.normals.length, numUniqueVertices * 3);
assert.strictEqual(infos[0].mesh.params.length, numUniqueVertices * 2);
});
it("creates line graphics as expected", () => {
const elementColor = ColorDef.fromString("blanchedAlmond");
const builder = new GeometryStreamBuilder();
const geometryParams = new GeometryParams(seedCategory);
geometryParams.lineColor = elementColor;
builder.appendGeometryParamsChange(geometryParams);
builder.appendGeometry(LineString3d.createPoints([Point3d.createZero(), Point3d.create(1, 0, 0)]));
const newId = insertPhysicalElement(builder.geometryStream);
const infos = [];
const lineInfos = [];
const exportGraphicsOptions = {
elementIdArray: [newId],
onGraphics: (info) => infos.push(info),
onLineGraphics: (lineInfo) => lineInfos.push(lineInfo),
};
const exportStatus = iModel.exportGraphics(exportGraphicsOptions);
assert.strictEqual(exportStatus, DbResult.BE_SQLITE_OK);
assert.strictEqual(infos.length, 0);
assert.strictEqual(lineInfos.length, 1);
assert.strictEqual(lineInfos[0].color, elementColor.tbgr);
assert.deepStrictEqual(Array.from(lineInfos[0].lines.indices), [0, 1]);
assert.deepStrictEqual(Array.from(lineInfos[0].lines.points), [0, 0, 0, 1, 0, 0]);
});
it("process multiple elements in one call", () => {
const builder0 = new GeometryStreamBuilder();
builder0.appendGeometry(Loop.createPolygon([Point3d.createZero(), Point3d.create(1, 0, 0), Point3d.create(1, 1, 0), Point3d.create(0, 1, 0)]));
const id0 = insertPhysicalElement(builder0.geometryStream);
const builder1 = new GeometryStreamBuilder();
builder1.appendGeometry(Loop.createPolygon([Point3d.createZero(), Point3d.create(-1, 0, 0), Point3d.create(-1, -1, 0), Point3d.create(0, -1, 0)]));
const id1 = insertPhysicalElement(builder1.geometryStream);
const infos = [];
const exportGraphicsOptions = {
elementIdArray: [id0, id1],
onGraphics: (info) => infos.push(info),
};
const exportStatus = iModel.exportGraphics(exportGraphicsOptions);
assert.strictEqual(exportStatus, DbResult.BE_SQLITE_OK);
assert.strictEqual(infos.length, 2);
// Sorting since output order is arbitrary
assert.deepStrictEqual([infos[0].elementId, infos[1].elementId].sort(), [id0, id1].sort());
});
it("produces expected indices, points, normals, params in smoketest", () => {
const builder = new GeometryStreamBuilder();
builder.appendGeometry(Loop.createPolygon([Point3d.createZero(), Point3d.create(1, 0, 0), Point3d.create(1, 1, 0), Point3d.create(0, 1, 0)]));
const newId = insertPhysicalElement(builder.geometryStream);
const infos = [];
const exportGraphicsOptions = {
elementIdArray: [newId],
onGraphics: (info) => infos.push(info),
};
const exportStatus = iModel.exportGraphics(exportGraphicsOptions);
assert.strictEqual(exportStatus, DbResult.BE_SQLITE_OK);
assert.strictEqual(infos.length, 1);
// The ordering of these values is arbitrary, but should be consistent between runs.
// Baselines may need to be updated if native GeomLibs is refactored, but:
// * Lengths of all fields should remain the same
// * Actual point, normal and param values should remain the same
assert.strictEqual(infos[0].mesh.indices.length, 6);
assert.strictEqual(infos[0].mesh.points.length, 12);
assert.strictEqual(infos[0].mesh.normals.length, 12);
assert.strictEqual(infos[0].mesh.params.length, 8);
assert.deepStrictEqual(Array.from(infos[0].mesh.indices), [0, 1, 2, 1, 0, 3]);
assert.deepStrictEqual(Array.from(infos[0].mesh.points), [1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 0]);
assert.deepStrictEqual(Array.from(infos[0].mesh.normals), [0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1]);
assert.deepStrictEqual(Array.from(infos[0].mesh.params), [1, 0, 0, 1, 0, 0, 1, 1]);
});
it("sets two-sided flag correctly for closed geometry", () => {
const builder = new GeometryStreamBuilder();
const box = Box.createDgnBox(Point3d.createZero(), Vector3d.create(1, 0, 0), Vector3d.create(0, 1, 0), Point3d.create(0, 0, 1), 1, 1, 1, 1, true);
assert.isDefined(box);
builder.appendGeometry(box);
const newId = insertPhysicalElement(builder.geometryStream);
const infos = [];
const exportGraphicsOptions = {
elementIdArray: [newId],
onGraphics: (info) => infos.push(info),
};
const exportStatus = iModel.exportGraphics(exportGraphicsOptions);
assert.strictEqual(exportStatus, DbResult.BE_SQLITE_OK);
assert.strictEqual(infos.length, 1);
assert.isFalse(infos[0].mesh.isTwoSided);
});
it("sets two-sided flag correctly for open geometry", () => {
const builder = new GeometryStreamBuilder();
const quad = Loop.createPolygon([Point3d.createZero(), Point3d.create(2, 0, 0), Point3d.create(2, 2, 0), Point3d.create(0, 2, 0)]);
builder.appendGeometry(quad);
const newId = insertPhysicalElement(builder.geometryStream);
const infos = [];
const exportGraphicsOptions = {
elementIdArray: [newId],
onGraphics: (info) => infos.push(info),
};
const exportStatus = iModel.exportGraphics(exportGraphicsOptions);
assert.strictEqual(exportStatus, DbResult.BE_SQLITE_OK);
assert.strictEqual(infos.length, 1);
assert.isTrue(infos[0].mesh.isTwoSided);
});
it("applies chordTol", () => {
const builder = new GeometryStreamBuilder();
builder.appendGeometry(Sphere.createCenterRadius(Point3d.createZero(), 1));
const newId = insertPhysicalElement(builder.geometryStream);
const coarseInfos = [];
iModel.exportGraphics({
elementIdArray: [newId],
onGraphics: (info) => coarseInfos.push(info),
chordTol: 1,
});
const fineInfos = [];
iModel.exportGraphics({
elementIdArray: [newId],
onGraphics: (info) => fineInfos.push(info),
chordTol: 0.001,
});
assert.strictEqual(coarseInfos.length, 1);
assert.strictEqual(fineInfos.length, 1);
assert.isTrue(coarseInfos[0].mesh.indices.length < fineInfos[0].mesh.indices.length);
});
it("applies angleTol", () => {
const builder = new GeometryStreamBuilder();
builder.appendGeometry(Sphere.createCenterRadius(Point3d.createZero(), 1));
const newId = insertPhysicalElement(builder.geometryStream);
const coarseInfos = [];
iModel.exportGraphics({
elementIdArray: [newId],
onGraphics: (info) => coarseInfos.push(info),
angleTol: 30 * Angle.radiansPerDegree,
});
const fineInfos = [];
iModel.exportGraphics({
elementIdArray: [newId],
onGraphics: (info) => fineInfos.push(info),
angleTol: 15 * Angle.radiansPerDegree,
});
assert.strictEqual(coarseInfos.length, 1);
assert.strictEqual(fineInfos.length, 1);
assert.isTrue(coarseInfos[0].mesh.indices.length < fineInfos[0].mesh.indices.length);
});
it("applies decimationTol", () => {
const strokeOptions = StrokeOptions.createForCurves();
strokeOptions.chordTol = 0.001;
const polyfaceBuilder = PolyfaceBuilder.create(strokeOptions);
polyfaceBuilder.addSphere(Sphere.createCenterRadius(Point3d.createZero(), 1));
const gsBuilder = new GeometryStreamBuilder();
gsBuilder.appendGeometry(polyfaceBuilder.claimPolyface());
const newId = insertPhysicalElement(gsBuilder.geometryStream);
const noDecimationInfos = [];
const noDecimationStatus = iModel.exportGraphics({
elementIdArray: [newId],
onGraphics: (info) => noDecimationInfos.push(info),
});
assert.strictEqual(noDecimationStatus, DbResult.BE_SQLITE_OK);
assert.strictEqual(noDecimationInfos.length, 1);
const decimationInfos = [];
const decimationStatus = iModel.exportGraphics({
elementIdArray: [newId],
onGraphics: (info) => decimationInfos.push(info),
decimationTol: 0.1,
});
assert.strictEqual(decimationStatus, DbResult.BE_SQLITE_OK);
assert.strictEqual(decimationInfos.length, 1);
assert.isTrue(decimationInfos[0].mesh.indices < noDecimationInfos[0].mesh.indices);
});
it("applies maxEdgeLength", () => {
const builder = new GeometryStreamBuilder();
const quad = Loop.createPolygon([Point3d.createZero(), Point3d.create(2, 0, 0), Point3d.create(2, 2, 0), Point3d.create(0, 2, 0)]);
builder.appendGeometry(quad);
const newId = insertPhysicalElement(builder.geometryStream);
const infos = [];
const exportGraphicsOptions = {
elementIdArray: [newId],
onGraphics: (info) => infos.push(info),
maxEdgeLength: 1,
};
const exportStatus = iModel.exportGraphics(exportGraphicsOptions);
assert.strictEqual(exportStatus, DbResult.BE_SQLITE_OK);
assert.strictEqual(infos.length, 1);
assert.isTrue(infos[0].mesh.indices.length > 6); // not validating particulars of subdivision, just that the option is applied
});
it("applies minBRepFeatureSize", () => {
const builder = new GeometryStreamBuilder();
// 4x4x4m slab with a ~1x1x1cm slab cut out of it.
const testBrep = JSON.parse(fs.readFileSync(IModelTestUtils.resolveAssetFile("brep-small-feature.json"), { encoding: "utf8" }));
builder.appendBRepData(testBrep);
const newId = insertPhysicalElement(builder.geometryStream);
const noMinSizeInfos = [];
const noMinSizeStatus = iModel.exportGraphics({
elementIdArray: [newId],
onGraphics: (info) => noMinSizeInfos.push(info),
});
assert.strictEqual(noMinSizeStatus, DbResult.BE_SQLITE_OK);
assert.strictEqual(noMinSizeInfos.length, 1);
const minSizeInfos = [];
const minSizeStatus = iModel.exportGraphics({
elementIdArray: [newId],
onGraphics: (info) => minSizeInfos.push(info),
minBRepFeatureSize: 0.1,
});
assert.strictEqual(minSizeStatus, DbResult.BE_SQLITE_OK);
assert.strictEqual(minSizeInfos.length, 1);
assert.strictEqual(minSizeInfos[0].mesh.indices.length, 36); // should be a simple cube
assert.isTrue(minSizeInfos[0].mesh.indices.length < noMinSizeInfos[0].mesh.indices.length);
});
it("applies minLineStyleComponentSize", () => {
const partBuilder = new GeometryStreamBuilder();
const partParams = new GeometryParams(Id64.invalid); // category won't be used
// Create line style with small component - logic copied from line style tests in GeometryStream.test.ts
partParams.fillDisplay = FillDisplay.Always;
partBuilder.appendGeometryParamsChange(partParams);
partBuilder.appendGeometry(Loop.create(LineString3d.create(Point3d.create(0.1, 0, 0), Point3d.create(0, -0.05, 0), Point3d.create(0, 0.05, 0), Point3d.create(0.1, 0, 0))));
const partProps = {
classFullName: GeometryPart.classFullName,
model: IModel.dictionaryId,
code: Code.createEmpty(),
geom: partBuilder.geometryStream,
};
const geomPartId = iModel.elements.insertElement(partProps);
const pointSymbolData = LineStyleDefinition.Utils.createPointSymbolComponent(iModel, { geomPartId }); // base and size will be set automatically...
assert.isTrue(undefined !== pointSymbolData);
const strokePointData = LineStyleDefinition.Utils.createStrokePointComponent(iModel, { descr: "TestArrowHead", lcId: 0, lcType: LineStyleDefinition.ComponentType.Internal, symbols: [{ symId: pointSymbolData.compId, strokeNum: -1, mod1: LineStyleDefinition.SymbolOptions.CurveEnd }] });
assert.isTrue(undefined !== strokePointData);
const compoundData = LineStyleDefinition.Utils.createCompoundComponent(iModel, { comps: [{ id: strokePointData.compId, type: strokePointData.compType }, { id: 0, type: LineStyleDefinition.ComponentType.Internal }] });
assert.isTrue(undefined !== compoundData);
const styleId = LineStyleDefinition.Utils.createStyle(iModel, IModel.dictionaryId, "TestArrowStyle", compoundData);
assert.isTrue(Id64.isValidId64(styleId));
const builder = new GeometryStreamBuilder();
const geometryParams = new GeometryParams(seedCategory);
geometryParams.styleInfo = new LineStyle.Info(styleId);
builder.appendGeometryParamsChange(geometryParams);
builder.appendGeometry(LineSegment3d.create(Point3d.createZero(), Point3d.create(10, 10, 0)));
const elementId = insertPhysicalElement(builder.geometryStream);
// Should include everything with no minimum component size
const noMinInfos = [];
const noMinLineInfos = [];
let exportStatus = iModel.exportGraphics({
elementIdArray: [elementId],
onGraphics: (info) => noMinInfos.push(info),
onLineGraphics: (lineInfo) => noMinLineInfos.push(lineInfo),
});
assert.strictEqual(exportStatus, DbResult.BE_SQLITE_OK);
assert.strictEqual(noMinInfos.length, 1);
assert.strictEqual(noMinLineInfos.length, 1);
assert.strictEqual(noMinInfos[0].mesh.indices.length, 3);
assert.strictEqual(noMinLineInfos[0].lines.indices.length, 2);
// Should filter out arrow shape with large minimum component size
const largeMinInfos = [];
const largeMinLineInfos = [];
exportStatus = iModel.exportGraphics({
elementIdArray: [elementId],
onGraphics: (info) => largeMinInfos.push(info),
onLineGraphics: (lineInfo) => largeMinLineInfos.push(lineInfo),
minLineStyleComponentSize: 0.5,
});
assert.strictEqual(exportStatus, DbResult.BE_SQLITE_OK);
assert.strictEqual(largeMinInfos.length, 0);
assert.strictEqual(largeMinLineInfos.length, 1);
assert.strictEqual(largeMinLineInfos[0].lines.indices.length, 2);
// Should include arrow shape with small minimum component size
const smallMinInfos = [];
const smallMinLineInfos = [];
exportStatus = iModel.exportGraphics({
elementIdArray: [elementId],
onGraphics: (info) => smallMinInfos.push(info),
onLineGraphics: (lineInfo) => smallMinLineInfos.push(lineInfo),
minLineStyleComponentSize: 0.01,
});
assert.strictEqual(exportStatus, DbResult.BE_SQLITE_OK);
assert.strictEqual(smallMinInfos.length, 1);
assert.strictEqual(smallMinLineInfos.length, 1);
assert.strictEqual(smallMinInfos[0].mesh.indices.length, 3);
assert.strictEqual(smallMinLineInfos[0].lines.indices.length, 2);
});
it("applies GeometryPart transforms as expected", () => {
const partBuilder = new GeometryStreamBuilder();
partBuilder.appendGeometry(Loop.createPolygon([Point3d.createZero(), Point3d.create(1, 0, 0), Point3d.create(1, 1, 0), Point3d.create(0, 1, 0)]));
const partProps = {
classFullName: GeometryPart.classFullName,
model: IModel.dictionaryId,
code: Code.createEmpty(),
geom: partBuilder.geometryStream,
};
const partId = iModel.elements.insertElement(partProps);
const partInstanceBuilder = new GeometryStreamBuilder();
partInstanceBuilder.appendGeometryPart3d(partId, Point3d.create(7, 8, 9));
const partInstanceId = insertPhysicalElement(partInstanceBuilder.geometryStream);
const infos = [];
const exportGraphicsOptions = {
elementIdArray: [partInstanceId],
onGraphics: (info) => infos.push(info),
};
const exportStatus = iModel.exportGraphics(exportGraphicsOptions);
assert.strictEqual(exportStatus, DbResult.BE_SQLITE_OK);
assert.strictEqual(infos.length, 1);
// The ordering of these values is arbitrary, but should be consistent between runs.
// Baselines may need to be updated if native GeomLibs is refactored, but:
// * Lengths of all fields should remain the same
// * Actual point, normal and param values should remain the same
assert.strictEqual(infos[0].mesh.indices.length, 6);
assert.strictEqual(infos[0].mesh.points.length, 12);
assert.strictEqual(infos[0].mesh.normals.length, 12);
assert.strictEqual(infos[0].mesh.params.length, 8);
assert.deepStrictEqual(Array.from(infos[0].mesh.indices), [0, 1, 2, 1, 0, 3]);
assert.deepStrictEqual(Array.from(infos[0].mesh.points), [8, 8, 9, 7, 9, 9, 7, 8, 9, 8, 9, 9]);
assert.deepStrictEqual(Array.from(infos[0].mesh.normals), [0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1]);
assert.deepStrictEqual(Array.from(infos[0].mesh.params), [1, 0, 0, 1, 0, 0, 1, 1]);
});
it("exposes instances through partInstanceArray and exportPartGraphics", () => {
const partBuilder = new GeometryStreamBuilder();
partBuilder.appendGeometry(Loop.createPolygon([Point3d.createZero(), Point3d.create(1, 0, 0), Point3d.create(1, 1, 0), Point3d.create(0, 1, 0)]));
const partProps = {
classFullName: GeometryPart.classFullName,
model: IModel.dictionaryId,
code: Code.createEmpty(),
geom: partBuilder.geometryStream,
};
const partId = iModel.elements.insertElement(partProps);
const partInstanceBuilder = new GeometryStreamBuilder();
partInstanceBuilder.appendGeometryPart3d(partId, Point3d.create(7, 8, 9));
const partInstanceId = insertPhysicalElement(partInstanceBuilder.geometryStream);
const infos = [];
const partInstanceArray = [];
const exportGraphicsOptions = {
elementIdArray: [partInstanceId],
onGraphics: (info) => infos.push(info),
partInstanceArray,
};
const exportStatus = iModel.exportGraphics(exportGraphicsOptions);
assert.strictEqual(exportStatus, DbResult.BE_SQLITE_OK);
assert.strictEqual(infos.length, 0);
assert.strictEqual(partInstanceArray.length, 1);
assert.strictEqual(partInstanceArray[0].partId, partId);
assert.strictEqual(partInstanceArray[0].partInstanceId, partInstanceId);
assert.isDefined(partInstanceArray[0].transform);
assert.deepStrictEqual(Array.from(partInstanceArray[0].transform), [1, 0, 0, 7, 0, 1, 0, 8, 0, 0, 1, 9]);
const partInfos = [];
const exportPartStatus = iModel.exportPartGraphics({
elementId: partInstanceArray[0].partId,
displayProps: partInstanceArray[0].displayProps,
onPartGraphics: (partInfo) => partInfos.push(partInfo),
});
assert.strictEqual(exportPartStatus, DbResult.BE_SQLITE_OK);
assert.strictEqual(partInfos.length, 1);
// The ordering of these values is arbitrary, but should be consistent between runs.
// Baselines may need to be updated if native GeomLibs is refactored, but:
// * Lengths of all fields should remain the same
// * Actual point, normal and param values should remain the same
assert.strictEqual(partInfos[0].mesh.indices.length, 6);
assert.strictEqual(partInfos[0].mesh.points.length, 12);
assert.strictEqual(partInfos[0].mesh.normals.length, 12);
assert.strictEqual(partInfos[0].mesh.params.length, 8);
assert.deepStrictEqual(Array.from(partInfos[0].mesh.indices), [0, 1, 2, 1, 0, 3]);
assert.deepStrictEqual(Array.from(partInfos[0].mesh.points), [1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 0]);
assert.deepStrictEqual(Array.from(partInfos[0].mesh.normals), [0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1]);
assert.deepStrictEqual(Array.from(partInfos[0].mesh.params), [1, 0, 0, 1, 0, 0, 1, 1]);
});
it("handles single geometryClass in GeometryStream", () => {
const builder = new GeometryStreamBuilder();
const geometryParams = new GeometryParams(seedCategory);
geometryParams.geometryClass = GeometryClass.Construction;
builder.appendGeometryParamsChange(geometryParams);
builder.appendGeometry(Sphere.createCenterRadius(Point3d.createZero(), 1));
const newId = insertPhysicalElement(builder.geometryStream);
const infos = [];
const exportGraphicsOptions = {
elementIdArray: [newId],
onGraphics: (info) => infos.push(info),
};
const exportStatus = iModel.exportGraphics(exportGraphicsOptions);
assert.strictEqual(exportStatus, DbResult.BE_SQLITE_OK);
assert.strictEqual(infos.length, 1);
assert.strictEqual(infos[0].geometryClass, GeometryClass.Construction);
});
it("handles multiple geometryClass in GeometryStream", () => {
const builder = new GeometryStreamBuilder();
const geometryParams = new GeometryParams(seedCategory);
geometryParams.geometryClass = GeometryClass.Construction;
builder.appendGeometryParamsChange(geometryParams);
builder.appendGeometry(Sphere.createCenterRadius(Point3d.createZero(), 1));
geometryParams.geometryClass = GeometryClass.Primary;
builder.appendGeometryParamsChange(geometryParams);
builder.appendGeometry(Box.createRange(Range3d.create(Point3d.createZero(), Point3d.create(1.0, 1.0, 1.0)), true));
const newId = insertPhysicalElement(builder.geometryStream);
const infos = [];
const exportGraphicsOptions = {
elementIdArray: [newId],
onGraphics: (info) => infos.push(info),
chordTol: 0.01,
};
const exportStatus = iModel.exportGraphics(exportGraphicsOptions);
assert.strictEqual(exportStatus, DbResult.BE_SQLITE_OK);
assert.strictEqual(infos.length, 2);
// Sphere is construction, box is primary. Output order is arbitrary, use mesh size to figure out which is which.
if (infos[0].mesh.indices.length > 36) {
assert.strictEqual(infos[0].geometryClass, GeometryClass.Construction);
assert.strictEqual(infos[1].geometryClass, GeometryClass.Primary);
}
else {
assert.strictEqual(infos[0].geometryClass, GeometryClass.Primary);
assert.strictEqual(infos[1].geometryClass, GeometryClass.Construction);
}
});
it("handles geometryClass in lines", () => {
const builder = new GeometryStreamBuilder();
const geometryParams = new GeometryParams(seedCategory);
geometryParams.geometryClass = GeometryClass.Construction;
builder.appendGeometryParamsChange(geometryParams);
builder.appendGeometry(LineString3d.createPoints([Point3d.createZero(), Point3d.create(1, 0, 0)]));
const newId = insertPhysicalElement(builder.geometryStream);
const infos = [];
const lineInfos = [];
const exportGraphicsOptions = {
elementIdArray: [newId],
onGraphics: (info) => infos.push(info),
onLineGraphics: (lineInfo) => lineInfos.push(lineInfo),
chordTol: 0.01,
};
const exportStatus = iModel.exportGraphics(exportGraphicsOptions);
assert.strictEqual(exportStatus, DbResult.BE_SQLITE_OK);
assert.strictEqual(infos.length, 0);
assert.strictEqual(lineInfos.length, 1);
assert.strictEqual(lineInfos[0].geometryClass, GeometryClass.Construction);
});
it("handles geometryClass defined in parts", () => {
const partBuilder = new GeometryStreamBuilder();
const partGeometryParams = new GeometryParams(Id64.invalid); // category unused for GeometryPart
partGeometryParams.geometryClass = GeometryClass.Construction;
partBuilder.appendGeometryParamsChange(partGeometryParams);
partBuilder.appendGeometry(Sphere.createCenterRadius(Point3d.createZero(), 1));
const partProps = {
classFullName: GeometryPart.classFullName,
model: IModel.dictionaryId,
code: Code.createEmpty(),
geom: partBuilder.geometryStream,
};
const partId = iModel.elements.insertElement(partProps);
const partInstanceBuilder = new GeometryStreamBuilder();
partInstanceBuilder.appendGeometryPart3d(partId);
const partInstanceId = insertPhysicalElement(partInstanceBuilder.geometryStream);
const infos = [];
const partInstanceArray = [];
const exportGraphicsOptions = {
elementIdArray: [partInstanceId],
onGraphics: (info) => infos.push(info),
partInstanceArray,
};
const exportStatus = iModel.exportGraphics(exportGraphicsOptions);
assert.strictEqual(exportStatus, DbResult.BE_SQLITE_OK);
assert.strictEqual(infos.length, 0);
assert.strictEqual(partInstanceArray.length, 1);
const partInfos = [];
const exportPartStatus = iModel.exportPartGraphics({
elementId: partInstanceArray[0].partId,
displayProps: partInstanceArray[0].displayProps,
onPartGraphics: (info) => partInfos.push(info),
});
assert.strictEqual(exportPartStatus, DbResult.BE_SQLITE_OK);
assert.strictEqual(partInfos.length, 1);
assert.strictEqual(partInfos[0].geometryClass, GeometryClass.Construction);
});
it("handles geometryClass defined outside parts", () => {
const partBuilder = new GeometryStreamBuilder();
partBuilder.appendGeometry(Sphere.createCenterRadius(Point3d.createZero(), 1));
const partProps = {
classFullName: GeometryPart.classFullName,
model: IModel.dictionaryId,
code: Code.createEmpty(),
geom: partBuilder.geometryStream,
};
const partId = iModel.elements.insertElement(partProps);
const partInstanceBuilder = new GeometryStreamBuilder();
const partInstanceGeometryParams = new GeometryParams(seedCategory);
partInstanceGeometryParams.geometryClass = GeometryClass.Construction;
partInstanceBuilder.appendGeometryParamsChange(partInstanceGeometryParams);
partInstanceBuilder.appendGeometryPart3d(partId);
const partInstanceId = insertPhysicalElement(partInstanceBuilder.geometryStream);
const infos = [];
const partInstanceArray = [];
const exportGraphicsOptions = {
elementIdArray: [partInstanceId],
onGraphics: (info) => infos.push(info),
partInstanceArray,
};
const exportStatus = iModel.exportGraphics(exportGraphicsOptions);
assert.strictEqual(exportStatus, DbResult.BE_SQLITE_OK);
assert.strictEqual(infos.length, 0);
assert.strictEqual(partInstanceArray.length, 1);
assert.strictEqual(partInstanceArray[0].displayProps.geometryClass, GeometryClass.Construction);
const partInfos = [];
const exportPartStatus = iModel.exportPartGraphics({
elementId: partInstanceArray[0].partId,
displayProps: partInstanceArray[0].displayProps,
onPartGraphics: (info) => partInfos.push(info),
});
assert.strictEqual(exportPartStatus, DbResult.BE_SQLITE_OK);
assert.strictEqual(partInfos.length, 1);
assert.strictEqual(partInfos[0].geometryClass, GeometryClass.Construction);
});
it("handles geometryClass for lines in parts", () => {
const partBuilder = new GeometryStreamBuilder();
const partGeometryParams = new GeometryParams(Id64.invalid); // category unused for GeometryPart
partGeometryParams.geometryClass = GeometryClass.Construction;
partBuilder.appendGeometryParamsChange(partGeometryParams);
partBuilder.appendGeometry(LineString3d.createPoints([Point3d.createZero(), Point3d.create(1, 0, 0)]));
const partProps = {
classFullName: GeometryPart.classFullName,
model: IModel.dictionaryId,
code: Code.createEmpty(),
geom: partBuilder.geometryStream,
};
const pa