UNPKG

@itwin/core-backend

Version:
897 lines • 56.9 kB
/*--------------------------------------------------------------------------------------------- * 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 { FieldRun, FontType, FractionRun, SubCategoryAppearance, TextAnnotation, TextBlock, TextRun, TextStyleSettings } from "@itwin/core-common"; import { StandaloneDb } from "../../IModelDb"; import { AnnotationTextStyle, parseTextAnnotationData, TEXT_ANNOTATION_JSON_VERSION, TEXT_STYLE_SETTINGS_JSON_VERSION, 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, MockBuilder } from "../AnnotationTestUtils"; import { TextAnnotationUsesTextStyleByDefault } from "../../annotations/ElementDrivesTextAnnotation"; import { layoutTextBlock, TextStyleResolver } from "../../annotations/TextBlockLayout"; import { appendTextAnnotationGeometry } from "../../annotations/TextAnnotationGeometry"; import { IModelElementCloneContext } from "../../IModelElementCloneContext"; import * as fs from "fs"; function mockIModel() { const iModel = { fonts: { findId: () => 0, }, computeRangesForText: computeTextRangeAsStringLength, forEachMetaData: () => undefined, }; return iModel; } function createAnnotation(textBlock) { const styleOverrides = { font: { name: "Karla" }, margins: { left: 0, right: 1, top: 2, bottom: 3 } }; const block = textBlock ?? TextBlock.create({ styleOverrides }); if (!textBlock) { block.appendRun(TextRun.create({ content: "Run, Barry,", styleOverrides })); block.appendRun(TextRun.create({ content: " RUN!!! ", styleOverrides })); block.appendRun(FractionRun.create({ numerator: "Harrison", denominator: "Wells", styleOverrides })); } 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, definitionModelId, name, settings = TextStyleSettings.defaultProps) => { return AnnotationTextStyle.create(iModel, { definitionModelId, name, settings, description: "description", }); }; function createElement2d(imodel, createArgs) { const placement = { origin: { x: 0, y: 0 }, angle: Angle.createDegrees(0).toJSON(), }; return TextAnnotation2d.create(imodel, { ...createArgs, placement, }); } function createElement3d(imodel, createArgs) { const placement = { origin: { x: 0, y: 0, z: 0 }, angles: YawPitchRollAngles.createDegrees(0, 0, 0).toJSON(), }; return TextAnnotation3d.create(imodel, { ...createArgs, placement, }); } 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, }, defaultTextStyle: new TextAnnotationUsesTextStyleByDefault("0x21").toJSON(), ...props, }, mockIModel()); } describe("versioning", () => { it("throws if the JSON has no version", () => { expect(() => makeElement({ textAnnotationData: JSON.stringify({ data: { textBlock: TextBlock.create().toJSON() } }), })).to.throw("JSON version is missing or invalid."); }); it("throws if the JSON has no data", () => { expect(() => makeElement({ textAnnotationData: JSON.stringify({ version: TEXT_ANNOTATION_JSON_VERSION, }), })).to.throw("JSON data is missing or invalid."); }); it("throws if the JSON version is too new", () => { expect(() => makeElement({ textAnnotationData: JSON.stringify({ version: "999.999.999", data: { textBlock: TextBlock.create().toJSON() } }), })).to.throw(`JSON version 999.999.999 is newer than supported version ${TEXT_ANNOTATION_JSON_VERSION}. Application update required to understand data.`); }); it("throws if the JSON version is old and cannot be migrated", () => { expect(() => makeElement({ textAnnotationData: JSON.stringify({ version: "0.0.1", data: { textBlock: TextBlock.create().toJSON() } }), })).to.throw(`Migration for textAnnotationData from version 0.0.1 to ${TEXT_ANNOTATION_JSON_VERSION} failed.`); }); }); 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({ version: TEXT_ANNOTATION_JSON_VERSION, data: { textBlock: TextBlock.create().toJSON() } }), defaultTextStyle: new TextAnnotationUsesTextStyleByDefault("0x42").toJSON() }); const anno = elem.getAnnotation(); expect(anno).not.to.be.undefined; expect(anno.textBlock.isEmpty).to.be.true; expect(elem.defaultTextStyle).not.to.be.undefined; expect(elem.defaultTextStyle.id).to.equal("0x42"); }); it("produces a new object each time it is called", () => { const elem = makeElement({ textAnnotationData: JSON.stringify({ version: TEXT_ANNOTATION_JSON_VERSION, data: { textBlock: TextBlock.create().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(); 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("getReferenceIds", () => { function expectReferenceIds(expected, element) { const actual = Array.from(element.getReferenceIds()).sort(); // reference Ids get a prefix indicating their type ('e' for 'element') expected = expected.map((id) => `e${id}`); // the superclasses provide some reference Ids (code spec, model, category) const baseIds = ["e0x12", "e0x78", "m0x34"]; expected.push(...baseIds); expected = expected.sort(); expect(actual).to.deep.equal(expected); } it("reports default text style and field hosts", () => { // makeElement sets defaultTextStyle to "0x21" const elem = makeElement(); expectReferenceIds(["0x21"], elem); elem.defaultTextStyle = new TextAnnotationUsesTextStyleByDefault("0x123"); expectReferenceIds(["0x123"], elem); const textBlock = TextBlock.create(); textBlock.appendRun(FieldRun.create({ propertyHost: { elementId: "0x456", schemaName: "BisCore", className: "GeometricElement3d" }, propertyPath: { propertyName: "CodeValue" }, })); textBlock.appendRun(FieldRun.create({ propertyHost: { elementId: "0x789", schemaName: "BisCore", className: "GeometricElement3d" }, propertyPath: { propertyName: "LastMod" }, })); elem.setAnnotation(TextAnnotation.create({ textBlock })); expectReferenceIds(["0x123", "0x456", "0x789"], elem); elem.defaultTextStyle = undefined; expectReferenceIds(["0x456", "0x789"], elem); elem.setAnnotation(TextAnnotation.create()); expectReferenceIds([], elem); }); it("does not report invalid Ids", () => { const elem = makeElement(); elem.defaultTextStyle = undefined; expectReferenceIds([], elem); elem.defaultTextStyle = new TextAnnotationUsesTextStyleByDefault("0"); expectReferenceIds([], elem); const textBlock = TextBlock.create(); textBlock.appendRun(FieldRun.create({ propertyHost: { elementId: "0", schemaName: "BisCore", className: "GeometricElement3d" }, propertyPath: { propertyName: "CodeValue" }, })); textBlock.appendRun(FieldRun.create({ propertyHost: { elementId: "0x123", schemaName: "BisCore", className: "GeometricElement3d" }, propertyPath: { propertyName: "LastMod" }, })); elem.setAnnotation(TextAnnotation.create({ textBlock })); expectReferenceIds(["0x123"], elem); }); }); describe("TextAnnotation3d Persistence", () => { let imodel; let createElement3dArgs; 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", { font: { name: "Totally Real Font" }, textHeight: 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; createElement3dArgs = { category, model }; }); after(() => imodel.close()); it("creating element does not automatically compute the geometry", () => { const annotation = createAnnotation(); const args = { ...createElement3dArgs, textAnnotationProps: annotation.toJSON() }; const el = createElement3d(imodel, args); 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(imodel, { ...createElement3dArgs }); 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 textBlock", async () => { await test(createAnnotation()); }); }); }); describe("TextAnnotation2d Persistence", () => { let imodel; let createElement2dArgs; 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); expect(jobSubjectId).not.to.be.undefined; expect(category).not.to.be.undefined; expect(model).not.to.be.undefined; createElement2dArgs = { category, model }; }); after(() => { imodel.saveChanges("tests"); imodel.close(); }); it("creating element does not automatically compute the geometry", () => { const annotation = createAnnotation(); const args = { ...createElement2dArgs, textAnnotationProps: annotation.toJSON() }; const el = createElement2d(imodel, args); 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(imodel, createElement2dArgs); 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; expect(el0.toJSON().elementGeometryBuilderParams).to.deep.equal(el1.toJSON().elementGeometryBuilderParams); } } it("roundtrips an empty annotation", async () => { await test(); }); it("roundtrips an annotation with a textBlock", async () => { await test(createAnnotation()); }); }); }); describe("defaultTextStyle", () => { let imodel; let seedSubjectId; let seedDefinitionModelId; let seedStyleId; let seedStyleId2; before(async () => { imodel = await createIModel("DefaultTextStyle"); const jobSubjectId = createJobSubjectElement(imodel, "Job").insert(); const definitionModel = DefinitionModel.insert(imodel, jobSubjectId, "Definition"); const styleId = createAnnotationTextStyle(imodel, definitionModel, "test", { font: { name: "Totally Real Font" }, textHeight: 0.25, isItalic: true }).insert(); const differentStyleId = createAnnotationTextStyle(imodel, definitionModel, "alt", { font: { name: "Karla" }, textHeight: 0.5, isBold: true }).insert(); expect(jobSubjectId).not.to.be.undefined; expect(definitionModel).not.to.be.undefined; expect(styleId).not.to.be.undefined; expect(differentStyleId).not.to.be.undefined; seedSubjectId = jobSubjectId; seedDefinitionModelId = definitionModel; seedStyleId = styleId; seedStyleId2 = differentStyleId; }); after(() => { imodel.saveChanges("tests"); imodel.close(); }); describe("TextAnnotation2d", () => { let createElement2dArgs; before(() => { const { category, model } = insertDrawingModel(imodel, seedSubjectId, seedDefinitionModelId); expect(category).not.to.be.undefined; expect(model).not.to.be.undefined; createElement2dArgs = { category, model }; }); it("preserves defaultTextStyle after round trip", () => { const annotation = createAnnotation(); const args = { ...createElement2dArgs, textAnnotationProps: annotation.toJSON(), defaultTextStyleId: seedStyleId }; const el0 = createElement2d(imodel, args); expect(el0.defaultTextStyle).not.to.be.undefined; expect(el0.defaultTextStyle.id).to.equal(seedStyleId); el0.insert(); const el1 = imodel.elements.getElement(el0.id); expect(el1).not.to.be.undefined; expect(el1.defaultTextStyle).not.to.be.undefined; expect(el1.defaultTextStyle.id).to.equal(seedStyleId); expect(el0.toJSON().elementGeometryBuilderParams).to.deep.equal(el1.toJSON().elementGeometryBuilderParams); }); it("produces different geometry when defaultTextStyle changes", () => { const annotation = createAnnotation(); const args = { ...createElement2dArgs, textAnnotationProps: annotation.toJSON() }; const el0 = createElement2d(imodel, args); el0.defaultTextStyle = new TextAnnotationUsesTextStyleByDefault(seedStyleId); const geom1 = el0.toJSON().elementGeometryBuilderParams; el0.defaultTextStyle = new TextAnnotationUsesTextStyleByDefault(seedStyleId2); const geom2 = el0.toJSON().elementGeometryBuilderParams; expect(geom1).not.to.deep.equal(geom2); }); it("allows defaultTextStyle to be undefined", () => { const annotation = createAnnotation(); const args = { ...createElement2dArgs, textAnnotationProps: annotation.toJSON() }; const el0 = createElement2d(imodel, args); el0.defaultTextStyle = undefined; 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; expect(el1.defaultTextStyle).to.be.undefined; }); describe("onCloned", () => { function insertStyledElement(styleId, db) { const args = { ...createElement2dArgs, defaultTextStyleId: styleId }; const elem = createElement2d(db, args); elem.insert(); imodel.saveChanges(); return elem; } describe("within a single iModel", () => { it("leaves property hosts intact", async () => { const textBlock = TextBlock.create({ styleOverrides: { font: { name: "Karla" } }, children: [{ children: [{ type: "field", propertyHost: { elementId: "0x123", schemaName: "Fields", className: "TestElement", }, propertyPath: { propertyName: "intProp" }, }, { type: "field", propertyHost: { elementId: "0xabc", schemaName: "BisCore", className: "Element", }, propertyPath: { propertyName: "CodeValue" }, }], }], }); const annotation = TextAnnotation.create({ textBlock, }); const elem = createElement2d(imodel, { ...createElement2dArgs, textAnnotationProps: annotation.toJSON() }); elem.insert(); imodel.saveChanges(); const context = new IModelElementCloneContext(imodel); context.remapElement("0x123", "0x456"); context.remapElement("0xabc", "0xdef"); context.remapElement(createElement2dArgs.model, createElement2dArgs.model); const props = await context.cloneElement(elem); expect(props.textAnnotationData).not.to.be.undefined; const anno = TextAnnotation.fromJSON(parseTextAnnotationData(props.textAnnotationData)?.data); const para = anno.textBlock.children[0]; expect(para.children[0].propertyHost.elementId).to.equal("0x123"); expect(para.children[1].propertyHost.elementId).to.equal("0xabc"); }); it("leaves default text style intact", async () => { async function clone(styleId, expectedStyleId) { const elem = insertStyledElement(styleId, imodel); const context = new IModelElementCloneContext(imodel); context.remapElement(createElement2dArgs.model, createElement2dArgs.model); const props = await context.cloneElement(elem); expect(props.defaultTextStyle?.id).to.equal(expectedStyleId); if (styleId) { // Even an explicit remapping is ignored when cloning within a single iModel // (per the examples set by most other elements, excluding RenderMaterial). context.remapElement(styleId, "0x99887"); const props2 = await context.cloneElement(elem); expect(props2.defaultTextStyle?.id).to.equal(expectedStyleId); } } await clone(seedStyleId, seedStyleId); await clone(undefined, undefined); await clone("0x12345", "0x12345"); await clone(Id64.invalid, undefined); }); }); describe("between iModels", () => { let dstDb; let dstDefModel; let dstElemArgs; before(async () => { dstDb = await createIModel("CloneTarget"); const jobSubjectId = createJobSubjectElement(dstDb, "Job").insert(); dstDefModel = DefinitionModel.insert(dstDb, jobSubjectId, "Definition"); const { category, model } = insertDrawingModel(dstDb, jobSubjectId, dstDefModel); expect(category).not.to.equal(createElement2dArgs.category); expect(model).not.to.equal(createElement2dArgs.model); dstElemArgs = { category, model }; }); after(() => dstDb.close()); it("remaps property hosts", async () => { const textBlock = TextBlock.create({ styleOverrides: { font: { name: "Karla" } }, children: [{ children: [{ type: "field", propertyHost: { elementId: "0x123", schemaName: "Fields", className: "TestElement", }, propertyPath: { propertyName: "intProp" }, }, { type: "field", propertyHost: { elementId: "0xabc", schemaName: "BisCore", className: "Element", }, propertyPath: { propertyName: "CodeValue" }, }], }], }); const annotation = TextAnnotation.create({ textBlock }); const elem = createElement2d(imodel, { ...createElement2dArgs, textAnnotationProps: annotation.toJSON() }); elem.insert(); imodel.saveChanges(); const context = new IModelElementCloneContext(imodel, dstDb); context.remapElement("0x123", "0x456"); context.remapElement("0xabc", "0xdef"); context.remapElement(createElement2dArgs.model, dstElemArgs.model); const props = await context.cloneElement(elem); expect(props.textAnnotationData).not.to.be.undefined; const anno = TextAnnotation.fromJSON(parseTextAnnotationData(props.textAnnotationData)?.data); const para = anno.textBlock.children[0]; expect(para.children[0].propertyHost.elementId).to.equal("0x456"); expect(para.children[1].propertyHost.elementId).to.equal("0xdef"); }); it("sets default text style to undefined if source style does not exist", async () => { const elem = insertStyledElement("0x12345", imodel); const context = new IModelElementCloneContext(imodel, dstDb); context.remapElement(createElement2dArgs.model, dstElemArgs.model); const props = await context.cloneElement(elem); expect(props.defaultTextStyle).to.be.undefined; }); it("remaps to an existing text style with the same code if present", async () => { const dstStyleId = createAnnotationTextStyle(dstDb, dstDefModel, "test", { font: { name: "Karla" } }).insert(); expect(dstStyleId).not.to.equal(seedStyleId); const srcElem = insertStyledElement(seedStyleId, imodel); const context = new IModelElementCloneContext(imodel, dstDb); context.remapElement(createElement2dArgs.model, dstElemArgs.model); const props = await context.cloneElement(srcElem); expect(props.defaultTextStyle?.id).to.equal(dstStyleId); }); it("throws an error if definition model is not remapped", async () => { const srcElem = insertStyledElement(seedStyleId2, imodel); const context = new IModelElementCloneContext(imodel, dstDb); context.remapElement(createElement2dArgs.model, dstElemArgs.model); await expect(context.cloneElement(srcElem)).to.be.rejectedWith("Invalid target model"); }); it("imports default text style if necessary", async () => { const srcElem = insertStyledElement(seedStyleId2, imodel); const context = new IModelElementCloneContext(imodel, dstDb); context.remapElement(createElement2dArgs.model, dstElemArgs.model); context.remapElement(seedDefinitionModelId, dstDefModel); const props = await context.cloneElement(srcElem); const dstStyleId = props.defaultTextStyle.id; expect(dstStyleId).not.to.be.undefined; expect(dstStyleId).not.to.equal(seedStyleId2); expect(dstDb.elements.tryGetElement(dstStyleId)).not.to.be.undefined; }); it("remaps multiple occurrences of same style to same Id", async () => { const srcStyleId = createAnnotationTextStyle(imodel, seedDefinitionModelId, "styyyle", { font: { name: "Karla" } }).insert(); const srcElem1 = insertStyledElement(srcStyleId, imodel); const srcElem2 = insertStyledElement(srcStyleId, imodel); const srcElem3 = insertStyledElement(srcStyleId, imodel); const context = new IModelElementCloneContext(imodel, dstDb); context.remapElement(createElement2dArgs.model, dstElemArgs.model); context.remapElement(seedDefinitionModelId, dstDefModel); const props1 = await context.cloneElement(srcElem1); const props2 = await context.cloneElement(srcElem2); expect(props1.defaultTextStyle).not.to.be.undefined; expect(props1.defaultTextStyle?.id).not.to.equal(srcStyleId); expect(props2.defaultTextStyle?.id).to.equal(props1.defaultTextStyle?.id); const context2 = new IModelElementCloneContext(imodel, dstDb); context2.remapElement(createElement2dArgs.model, dstElemArgs.model); context2.remapElement(seedDefinitionModelId, dstDefModel); const props3 = await context2.cloneElement(srcElem3); expect(props3.defaultTextStyle?.id).to.equal(props1.defaultTextStyle?.id); }); }); }); }); describe("TextAnnotation3d", () => { let createElement3dArgs; before(() => { const { category, model } = insertSpatialModel(imodel, seedSubjectId, seedDefinitionModelId); expect(category).not.to.be.undefined; expect(model).not.to.be.undefined; createElement3dArgs = { category, model }; }); it("preserves defaultTextStyle after round trip", () => { const annotation = createAnnotation(); const args = { ...createElement3dArgs, textAnnotationProps: annotation.toJSON(), defaultTextStyleId: seedStyleId }; const el0 = createElement3d(imodel, args); expect(el0.defaultTextStyle).not.to.be.undefined; expect(el0.defaultTextStyle.id).to.equal(seedStyleId); el0.insert(); const el1 = imodel.elements.getElement(el0.id); expect(el1).not.to.be.undefined; expect(el1.defaultTextStyle).not.to.be.undefined; expect(el1.defaultTextStyle.id).to.equal(seedStyleId); expect(el0.toJSON().elementGeometryBuilderParams).to.deep.equal(el1.toJSON().elementGeometryBuilderParams); }); it("produces different geometry when defaultTextStyle changes", () => { const annotation = createAnnotation(); const args = { ...createElement3dArgs, textAnnotationProps: annotation.toJSON() }; const el0 = createElement3d(imodel, args); el0.defaultTextStyle = new TextAnnotationUsesTextStyleByDefault(seedStyleId); const geom1 = el0.toJSON().elementGeometryBuilderParams; el0.defaultTextStyle = new TextAnnotationUsesTextStyleByDefault(seedStyleId2); const geom2 = el0.toJSON().elementGeometryBuilderParams; expect(geom1).not.to.deep.equal(geom2); }); it("allows defaultTextStyle to be undefined", () => { const annotation = createAnnotation(); const args = { ...createElement3dArgs, textAnnotationProps: annotation.toJSON() }; const el0 = createElement3d(imodel, args); el0.defaultTextStyle = undefined; 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; expect(el1.defaultTextStyle).to.be.undefined; }); }); }); }); 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({ font: { name: "Totally Real Font" }, isUnderlined: true, textHeight: 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", { font: { name: "" } }); expect(() => annotationTextStyle.insert()).to.throw(); // textHeight should be positive annotationTextStyle = createAnnotationTextStyle(imodel, seedDefinitionModel, "invalid textHeight", { font: { name: "Totally Real Font" }, textHeight: 0 }); expect(() => annotationTextStyle.insert()).to.throw(); // stackedFractionScale should be positive annotationTextStyle = createAnnotationTextStyle(imodel, seedDefinitionModel, "invalid stackedFractionScale", { font: { name: "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", { font: { name: "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({ font: { name: "" } }); expect(() => el1.update()).to.throw(); el1.settings = el1.settings.clone({ font: { name: "Totally Real Font" }, textHeight: 0 }); expect(() => el1.update()).to.throw(); el1.settings = el1.settings.clone({ textHeight: 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", { font: { name: "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()); }); describe("versioning", () => { function makeStyle(props) { return AnnotationTextStyle.fromJSON({ model: "0x34", code: { spec: "0x56", scope: "0x78", value: "style" }, classFullName: AnnotationTextStyle.classFullName, ...props, }, mockIModel()); } it("throws if the JSON has no version", () => { expect(() => makeStyle({ settings: JSON.stringify({ data: TextStyleSettings.defaultProps }), })).to.throw("JSON version is missing or invalid."); }); it("throws if the JSON has no data", () => { expect(() => makeStyle({ settings: JSON.stringify({ version: TEXT_STYLE_SETTINGS_JSON_VERSION, }), })).to.throw("JSON data is missing or invalid."); }); it("throws if the JSON version is too new", () => { expect(() => makeStyle({ settings: JSON.stringify({ version: "999.999.999", data: TextStyleSettings.defaultProps }), })).to.throw(`JSON version 999.999.999 is newer than supported version ${TEXT_STYLE_SETTINGS_JSON_VERSION}. Application update required to understand data.`); }); it("should migrate text style settings from 1.0.0", () => { const oldStyleData = { ...TextStyleSettings.defaultProps, leader: { ...TextStyleSettings.defaultProps.leader, // Explicitly remove terminatorShape to simulate old data terminatorShape: undefined } }; const migratedStyle = makeStyle({ settings: JSON.stringify({ version: "1.0.0", data: oldStyleData }), }); const jsonStyleData = migratedStyle.toJSON(); if (jsonStyleData.settings) { const jsonVersion = JSON.parse(jsonStyleData.settings).version; expect(jsonVersion).to.equal(TEXT_STYLE_SETTINGS_JSON_VERSION); } expect(migratedStyle.settings.leader.terminatorShape).to.not.be.undefined; }); it("should return same data when version is 1.0.1", () => { const styleData = { version: "1.0.1", data: TextStyleSettings.defaultProps }; const migratedStyle = makeStyle({ settings: JSON.stringify({ version: styleData.version, data: styleData.data }), }); const jsonStyleData = migratedStyle.toJSON(); if (jsonStyleData.settings) { const parsedJson = JSON.parse(jsonStyleData.settings); expect(parsedJson.version).to.equal(styleData.version); expect(parsedJson.data).to.deep.equal(styleData.data); } }); it("should return defaultProps when styleData is unrecognized", () => { const textStyle = makeStyle({ settings: JSON.stringify({ version: "1.0.1", data: { invalid: "data" } }), }); expect(textStyle.settings).to.be.deep.equal(TextStyleSettings.defaultProps); }); }); describe("onCloned", () => { let targetDb; let targetDefModel; before(async () => { // The source and target iModel will both contain the Karla font family. targetDb = await createIModel("AnnotationTextStyleTargetDb"); const jobSubjectId = createJobSubjectElement(targetDb, "Job").insert(); targetDefModel = DefinitionModel.insert(targetDb, jobSubjectId, "Definition"); // Embed a font into the source iModel that doesn't exist in the target iModel. const shxName = IModelTestUtils.resolveFontFile("Cdm.shx"); const shxBlob = fs.readFileSync(shxName); const shxFile = FontFile.createFromShxFontBlob({ blob: shxBlob, familyName: "Cdm" }); await imodel.fonts.embedFontFile({ file: shxFile }); }); after(() => targetDb.close()); it("embeds font into target Db if not already embedded", async () => { const getFontCounts = () => { let files = 0; for (const _ of targetDb.fonts.queryEmbeddedFontFiles()) { files++; } let families = 0; for (const _ of targetDb.fonts.queryMappedFamilies()) { families++; } return { files, families }; }; const initialCounts = getFontCounts(); const karlaStyle = createAnnotationTextStyle(imodel, seedDefinitionModel, "karla-style", TextStyleSettings.fromJSON({ font: { name: "Karla" } })); karlaStyle.insert(); const cdmStyle = createAnnotationTextStyle(imodel, seedDefinitionModel, "cdm-style", TextStyleSettings.fromJSON({ font: { name: "Cdm", type: FontType.Shx } })); cdmStyle.insert(); const context = new IModelElementCloneContext(imodel, targetDb); context.remapElement(seedDefinitionModel, targetDefModel); expect(targetDb.fonts.findId({ name: "Karla" })).not.to.be.undefined; await context.cloneElement(karlaStyle); expect(getFontCounts()).to.deep.equal(initialCounts); expect(targetDb.fonts.findId({ name: "Cdm", type: FontType.Shx })).to.be.undefined; await context.cloneElement(cdmStyle); expect(targetDb.fonts.findId({ name: "Cdm", type: FontType.Shx })).not.to.be.undefined; const finalCounts = getFontCounts(); expect(finalCounts.files).greaterThan(initialCounts.files); expect(finalCounts.families).greaterThan(initialCounts.families); }); }); }); describe("appendTextAnnotationGeometry", () => { let imodel; let seedDefinitionModelId; let seedCategoryId; let seedStyleId; let seedStyleId2; before(async () => { imodel = await createIModel("DefaultTextStyle"); const jobSubjectId = createJobSubjectElement(imodel, "Job").insert(); const definitionModel = DefinitionModel.insert(imodel, jobSubje