@itwin/core-backend
Version:
iTwin.js backend components
357 lines • 19.6 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 { expect } from "chai";
import { Angle, Point3d, Range2d, Range3d, YawPitchRollAngles } from "@itwin/core-geometry";
import { FractionRun, SubCategoryAppearance, TextAnnotation, TextBlock, TextRun, TextStyleSettings } from "@itwin/core-common";
import { StandaloneDb } from "../../IModelDb";
import { AnnotationTextStyle, TextAnnotation2d, TextAnnotation3d } from "../../annotations/TextAnnotationElement";
import { IModelTestUtils } from "../IModelTestUtils";
import { Subject } from "../../Element";
import { Guid, Id64 } from "@itwin/core-bentley";
import { DefinitionModel } from "../../Model";
import { DrawingCategory, SpatialCategory } from "../../Category";
import { DisplayStyle2d, DisplayStyle3d } from "../../DisplayStyle";
import { CategorySelector, DrawingViewDefinition, ModelSelector, SpatialViewDefinition } from "../../ViewDefinition";
import { FontFile } from "../../FontFile";
import { computeTextRangeAsStringLength } from "../AnnotationTestUtils";
function mockIModel() {
const iModel = {
fonts: {
findId: () => 0,
},
computeRangesForText: computeTextRangeAsStringLength,
forEachMetaData: () => undefined,
};
return iModel;
}
function createAnnotation(styleId) {
const styleOverrides = { fontName: "Karla" };
const block = TextBlock.create({ styleId: styleId ?? "0x42", styleOverrides });
block.appendRun(TextRun.create({ content: "Run, Barry,", styleOverrides }));
block.appendRun(TextRun.create({ content: " RUN!!! ", styleOverrides }));
block.appendRun(FractionRun.create({ numerator: "Harrison", denominator: "Wells", styleOverrides }));
block.margins = { left: 0, right: 1, top: 2, bottom: 3 };
const annotation = TextAnnotation.fromJSON({ textBlock: block.toJSON() });
annotation.anchor = { vertical: "middle", horizontal: "right" };
annotation.orientation = YawPitchRollAngles.createDegrees(1, 0, -1);
annotation.offset = Point3d.create(10, -5, 0);
annotation.leaders = [{ startPoint: Point3d.createZero(), attachment: { mode: "Nearest" } }];
return annotation;
}
const createJobSubjectElement = (iModel, name) => {
const subj = Subject.create(iModel, iModel.elements.getRootSubject().id, name);
subj.setJsonProperty("Subject", { Job: name }); // eslint-disable-line @typescript-eslint/naming-convention
return subj;
};
const insertDrawingModel = (standaloneModel, parentId, definitionModel) => {
const category = DrawingCategory.insert(standaloneModel, definitionModel, "DrawingCategory", new SubCategoryAppearance());
const [_, model] = IModelTestUtils.createAndInsertDrawingPartitionAndModel(standaloneModel, { spec: '0x1', scope: '0x1', value: 'Drawing' }, undefined, parentId);
const displayStyle = DisplayStyle2d.insert(standaloneModel, definitionModel, "DisplayStyle2d");
const categorySelector = CategorySelector.insert(standaloneModel, definitionModel, "DrawingCategories", [category]);
const viewRange = new Range2d(0, 0, 500, 500);
DrawingViewDefinition.insert(standaloneModel, definitionModel, "Drawing View", model, categorySelector, displayStyle, viewRange);
return { category, model };
};
const insertSpatialModel = (standaloneModel, parentId, definitionModel) => {
const category = SpatialCategory.insert(standaloneModel, definitionModel, "spatialCategory", new SubCategoryAppearance());
const [_, model] = IModelTestUtils.createAndInsertPhysicalPartitionAndModel(standaloneModel, { spec: '0x1', scope: '0x1', value: 'Spatial' }, undefined, parentId);
const modelSelector = ModelSelector.insert(standaloneModel, definitionModel, "SpatialModelSelector", [model]);
const displayStyle = DisplayStyle3d.insert(standaloneModel, definitionModel, "DisplayStyle3d");
const categorySelector = CategorySelector.insert(standaloneModel, definitionModel, "spatialCategories", [category]);
const viewRange = new Range3d(0, 0, 0, 500, 500, 500);
SpatialViewDefinition.insertWithCamera(standaloneModel, definitionModel, "spatial View", modelSelector, categorySelector, displayStyle, viewRange);
return { category, model };
};
const createIModel = async (name) => {
const filePath = IModelTestUtils.prepareOutputFile("annotationTests", `${name}.bim`);
const iModel = StandaloneDb.createEmpty(filePath, {
rootSubject: { name: `${name} tests`, description: `${name} tests` },
client: "integration tests",
globalOrigin: { x: 0, y: 0 },
projectExtents: { low: { x: -500, y: -500, z: -50 }, high: { x: 500, y: 500, z: 50 } },
guid: Guid.createValue(),
});
await iModel.fonts.embedFontFile({
file: FontFile.createFromTrueTypeFileName(IModelTestUtils.resolveFontFile("Karla-Regular.ttf"))
});
return iModel;
};
const createAnnotationTextStyle = (iModel, definitionModel, name, settings = TextStyleSettings.defaultProps) => {
return AnnotationTextStyle.create(iModel, definitionModel, name, settings, "description");
};
describe("TextAnnotation element", () => {
function makeElement(props) {
return TextAnnotation2d.fromJSON({
category: "0x12",
model: "0x34",
code: {
spec: "0x56",
scope: "0x78",
},
classFullName: TextAnnotation2d.classFullName,
placement: {
origin: { x: 0, y: 0 },
angle: 0,
},
...props,
}, mockIModel());
}
describe("getAnnotation", () => {
it("returns undefined if not provided", () => {
expect(makeElement().getAnnotation()).to.be.undefined;
});
it("converts JSON string to class instance", () => {
const elem = makeElement({
textAnnotationData: JSON.stringify({ textBlock: TextBlock.create({ styleId: "0x42" }).toJSON() })
});
const anno = elem.getAnnotation();
expect(anno).not.to.be.undefined;
expect(anno.textBlock.isEmpty).to.be.true;
expect(anno.textBlock.styleId).to.equal("0x42");
});
it("produces a new object each time it is called", () => {
const elem = makeElement({
textAnnotationData: JSON.stringify({ textBlock: TextBlock.create({ styleId: "0x42" }).toJSON() })
});
const anno1 = elem.getAnnotation();
const anno2 = elem.getAnnotation();
expect(anno1).not.to.equal(anno2);
expect(anno1.textBlock.equals(anno2.textBlock)).to.be.true;
});
});
describe("setAnnotation", () => {
it("updates properties", () => {
const elem = makeElement();
const textBlock = TextBlock.create({ styleId: "0x42" });
textBlock.appendRun(TextRun.create({ content: "text" }));
const annotation = TextAnnotation.fromJSON({ textBlock: textBlock.toJSON() });
elem.setAnnotation(annotation);
expect(elem.getAnnotation().toJSON()).to.deep.equal(annotation.toJSON());
expect(elem.getAnnotation().toJSON()).not.to.equal(annotation.toJSON());
});
});
describe("TextAnnotation3d Persistence", () => {
let imodel;
let seedCategoryId;
let seedModelId;
let seedStyleId;
before(async () => {
imodel = await createIModel("TextAnnotation3d");
const jobSubjectId = createJobSubjectElement(imodel, "Job").insert();
const definitionModel = DefinitionModel.insert(imodel, jobSubjectId, "Definition");
const { category, model } = insertSpatialModel(imodel, jobSubjectId, definitionModel);
const styleId = createAnnotationTextStyle(imodel, definitionModel, "test", { fontName: "Totally Real Font", lineHeight: 0.25, isItalic: true }).insert();
expect(jobSubjectId).not.to.be.undefined;
expect(category).not.to.be.undefined;
expect(model).not.to.be.undefined;
expect(styleId).not.to.be.undefined;
seedCategoryId = category;
seedModelId = model;
seedStyleId = styleId;
});
after(() => imodel.close());
function createElement3d(createArgs) {
return TextAnnotation3d.create(imodel, seedCategoryId, seedModelId, {
origin: { x: 0, y: 0, z: 0 },
angles: YawPitchRollAngles.createDegrees(0, 0, 0).toJSON(),
}, createArgs?.textAnnotationData);
}
it("creating element does not automatically compute the geometry", () => {
const annotation = createAnnotation();
const el = createElement3d({ textAnnotationData: annotation.toJSON() });
expect(el.getAnnotation().equals(annotation)).to.be.true;
expect(el.geom).to.be.undefined;
});
function expectPlacement3d(el, expectValidBBox, expectedOrigin = [0, 0, 0], expectedYPR = [0, 0, 0]) {
expect(el.placement.origin.x).to.equal(expectedOrigin[0]);
expect(el.placement.origin.y).to.equal(expectedOrigin[1]);
expect(el.placement.origin.z).to.equal(expectedOrigin[2]);
expect(el.placement.angles.yaw.radians).to.equal(expectedYPR[0]);
expect(el.placement.angles.pitch.radians).to.equal(expectedYPR[1]);
expect(el.placement.angles.roll.radians).to.equal(expectedYPR[2]);
expect(el.placement.bbox.isNull).to.equal(!expectValidBBox);
}
describe("inserts 3d element and round-trips through JSON", async () => {
async function test(annotation) {
const el0 = createElement3d();
if (annotation) {
el0.setAnnotation(annotation);
}
expectPlacement3d(el0, false);
const elId = el0.insert();
expect(Id64.isValidId64(elId)).to.be.true;
const el1 = imodel.elements.getElement(elId);
expect(el1).not.to.be.undefined;
expect(el1 instanceof TextAnnotation3d).to.be.true;
expectPlacement3d(el1, undefined !== annotation && !annotation.textBlock.isEmpty);
const anno = el1.getAnnotation();
if (!annotation) {
expect(anno).to.be.undefined;
expect(el0.toJSON().elementGeometryBuilderParams).to.be.undefined;
}
else {
expect(anno).not.to.be.undefined;
expect(anno.equals(annotation)).to.be.true;
expect(el0.toJSON().elementGeometryBuilderParams).not.to.be.undefined;
}
}
it("roundtrips an empty annotation", async () => { await test(); });
it("roundtrips an annotation with a style", async () => { await test(TextAnnotation.fromJSON({ textBlock: { styleId: seedStyleId } })); });
it("roundtrips an annotation with a textBlock", async () => { await test(createAnnotation()); });
});
});
describe("TextAnnotation2d Persistence", () => {
let imodel;
let seedCategoryId;
let seedModelId;
let seedStyleId;
before(async () => {
imodel = await createIModel("TextAnnotation2d");
const jobSubjectId = createJobSubjectElement(imodel, "Job").insert();
const definitionModel = DefinitionModel.insert(imodel, jobSubjectId, "Definition");
const { category, model } = insertDrawingModel(imodel, jobSubjectId, definitionModel);
const styleId = createAnnotationTextStyle(imodel, definitionModel, "test", { fontName: "Totally Real Font", lineHeight: 0.25, isItalic: true }).insert();
expect(jobSubjectId).not.to.be.undefined;
expect(category).not.to.be.undefined;
expect(model).not.to.be.undefined;
expect(styleId).not.to.be.undefined;
seedCategoryId = category;
seedModelId = model;
seedStyleId = styleId;
});
after(() => {
imodel.saveChanges("tests");
imodel.close();
});
function createElement2d(createArgs) {
return TextAnnotation2d.create(imodel, seedCategoryId, seedModelId, {
origin: { x: 0, y: 0 },
angle: Angle.createDegrees(0).toJSON(),
}, createArgs?.textAnnotationData);
}
it("creating element does not automatically compute the geometry", () => {
const annotation = createAnnotation();
const el = createElement2d({ textAnnotationData: annotation.toJSON() });
expect(el.getAnnotation().equals(annotation)).to.be.true;
expect(el.geom).to.be.undefined;
});
function expectPlacement2d(el, expectValidBBox, expectedOrigin = [0, 0, 0], expectedYPR = [0, 0, 0]) {
expect(el.placement.origin.x).to.equal(expectedOrigin[0]);
expect(el.placement.origin.y).to.equal(expectedOrigin[1]);
expect(el.placement.angle.degrees).to.equal(expectedYPR[0]);
expect(el.placement.bbox.isNull).to.equal(!expectValidBBox);
}
describe("inserts 2d element and round-trips through JSON", async () => {
async function test(annotation) {
const el0 = createElement2d();
if (annotation) {
el0.setAnnotation(annotation);
}
expectPlacement2d(el0, false);
const elId = el0.insert();
expect(Id64.isValidId64(elId)).to.be.true;
const el1 = imodel.elements.getElement(elId);
expect(el1).not.to.be.undefined;
expect(el1 instanceof TextAnnotation2d).to.be.true;
expectPlacement2d(el1, undefined !== annotation && !annotation.textBlock.isEmpty);
const anno = el1.getAnnotation();
if (!annotation) {
expect(anno).to.be.undefined;
expect(el0.toJSON().elementGeometryBuilderParams).to.be.undefined;
}
else {
expect(anno).not.to.be.undefined;
expect(anno.equals(annotation)).to.be.true;
expect(el0.toJSON().elementGeometryBuilderParams).not.to.be.undefined;
}
}
it("roundtrips an empty annotation", async () => { await test(); });
it("roundtrips an annotation with a style", async () => { await test(TextAnnotation.fromJSON({ textBlock: { styleId: seedStyleId } })); });
it("roundtrips an annotation with a textBlock", async () => { await test(createAnnotation()); });
});
});
});
describe("AnnotationTextStyle", () => {
let imodel;
let seedSubjectId;
let seedDefinitionModel;
before(async () => {
imodel = await createIModel("AnnotationTextStyle");
const jobSubjectId = createJobSubjectElement(imodel, "Job").insert();
const definitionModel = DefinitionModel.insert(imodel, jobSubjectId, "Definition");
expect(jobSubjectId).not.to.be.undefined;
expect(definitionModel).not.to.be.undefined;
seedSubjectId = jobSubjectId;
seedDefinitionModel = definitionModel;
});
after(() => {
imodel.close();
});
it("inserts a style and round-trips through JSON", async () => {
const textStyle = TextStyleSettings.fromJSON({
fontName: "Totally Real Font",
isUnderlined: true,
lineHeight: 0.5
});
const el0 = createAnnotationTextStyle(imodel, seedDefinitionModel, "round-trip", textStyle.toJSON());
const elId = el0.insert();
expect(Id64.isValidId64(elId)).to.be.true;
const el1 = imodel.elements.getElement(elId);
expect(el1).not.to.be.undefined;
expect(el1 instanceof AnnotationTextStyle).to.be.true;
const style = el1.settings;
expect(style).not.to.be.undefined;
expect(style.toJSON()).to.deep.equal(textStyle.toJSON());
});
it("does not allow elements with invalid styles to be inserted", async () => {
// Default style should fail since it has no font
let annotationTextStyle = createAnnotationTextStyle(imodel, seedDefinitionModel, "default");
expect(() => annotationTextStyle.insert()).to.throw();
// font is required
annotationTextStyle = createAnnotationTextStyle(imodel, seedDefinitionModel, "no font", { fontName: "" });
expect(() => annotationTextStyle.insert()).to.throw();
// lineHeight should be positive
annotationTextStyle = createAnnotationTextStyle(imodel, seedDefinitionModel, "invalid lineHeight", { fontName: "Totally Real Font", lineHeight: 0 });
expect(() => annotationTextStyle.insert()).to.throw();
// stackedFractionScale should be positive
annotationTextStyle = createAnnotationTextStyle(imodel, seedDefinitionModel, "invalid stackedFractionScale", { fontName: "Totally Real Font", stackedFractionScale: 0 });
expect(() => annotationTextStyle.insert()).to.throw();
});
it("does not allow updating of elements to invalid styles", async () => {
const annotationTextStyle = createAnnotationTextStyle(imodel, seedDefinitionModel, "valid style", { fontName: "Totally Real Font" });
const elId = annotationTextStyle.insert();
expect(Id64.isValidId64(elId)).to.be.true;
const el1 = imodel.elements.getElement(elId);
expect(el1).not.to.be.undefined;
expect(el1 instanceof AnnotationTextStyle).to.be.true;
el1.settings = el1.settings.clone({ fontName: "" });
expect(() => el1.update()).to.throw();
el1.settings = el1.settings.clone({ fontName: "Totally Real Font", lineHeight: 0 });
expect(() => el1.update()).to.throw();
el1.settings = el1.settings.clone({ lineHeight: 2, stackedFractionScale: 0 });
expect(() => el1.update()).to.throw();
el1.settings = el1.settings.clone({ stackedFractionScale: 0.45 });
el1.update();
const updatedElement = imodel.elements.getElement(elId);
expect(updatedElement.settings.toJSON()).to.deep.equal(el1.settings.toJSON());
});
it("uses default style if none specified", async () => {
const el0 = AnnotationTextStyle.fromJSON({
classFullName: AnnotationTextStyle.classFullName,
model: seedSubjectId,
code: AnnotationTextStyle.createCode(imodel, seedSubjectId, "style1"),
}, imodel);
expect(el0.settings).not.to.be.undefined;
expect(el0.settings.toJSON()).to.deep.equal(TextStyleSettings.defaultProps);
});
it("can update style via cloning", async () => {
const el0 = createAnnotationTextStyle(imodel, seedDefinitionModel, "cloning", { fontName: "Totally Real Font" });
const newStyle = el0.settings.clone({ isBold: true, lineSpacingFactor: 3 });
expect(el0.settings.toJSON()).to.not.deep.equal(newStyle.toJSON());
el0.settings = newStyle;
expect(el0.settings.toJSON()).to.deep.equal(newStyle.toJSON());
});
});
//# sourceMappingURL=TextAnnotation.test.js.map