UNPKG

@itwin/core-backend

Version:
903 lines • 64 kB
var __addDisposableResource = (this && this.__addDisposableResource) || function (env, value, async) { if (value !== null && value !== void 0) { if (typeof value !== "object" && typeof value !== "function") throw new TypeError("Object expected."); var dispose, inner; if (async) { if (!Symbol.asyncDispose) throw new TypeError("Symbol.asyncDispose is not defined."); dispose = value[Symbol.asyncDispose]; } if (dispose === void 0) { if (!Symbol.dispose) throw new TypeError("Symbol.dispose is not defined."); dispose = value[Symbol.dispose]; if (async) inner = dispose; } if (typeof dispose !== "function") throw new TypeError("Object not disposable."); if (inner) dispose = function() { try { inner.call(this); } catch (e) { return Promise.reject(e); } }; env.stack.push({ value: value, dispose: dispose, async: async }); } else if (async) { env.stack.push({ async: true }); } return value; }; var __disposeResources = (this && this.__disposeResources) || (function (SuppressedError) { return function (env) { function fail(e) { env.error = env.hasError ? new SuppressedError(e, env.error, "An error was suppressed during disposal.") : e; env.hasError = true; } var r, s = 0; function next() { while (r = env.stack.pop()) { try { if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next); if (r.dispose) { var result = r.dispose.call(r.value); if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) { fail(e); return next(); }); } else s |= 1; } catch (e) { fail(e); } } if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve(); if (env.hasError) throw env.error; } return next(); }; })(typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) { var e = new Error(message); return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e; }); /*--------------------------------------------------------------------------------------------- * 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 { EditTxn, withEditTxn } from "../../EditTxn"; import { DisableNativeAssertions } from "../TestUtils"; 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 = (txn, parentId, definitionModel) => { const category = DrawingCategory.insert(txn, definitionModel, "DrawingCategory", new SubCategoryAppearance()); const [_, model] = IModelTestUtils.createAndInsertDrawingPartitionAndModel(txn, { spec: '0x1', scope: '0x1', value: 'Drawing' }, undefined, parentId); const displayStyle = DisplayStyle2d.insert(txn, definitionModel, "DisplayStyle2d"); const categorySelector = CategorySelector.insert(txn, definitionModel, "DrawingCategories", [category]); const viewRange = new Range2d(0, 0, 500, 500); DrawingViewDefinition.insert(txn, definitionModel, "Drawing View", model, categorySelector, displayStyle, viewRange); return { category, model }; }; const insertSpatialModel = (txn, parentId, definitionModel) => { const category = SpatialCategory.insert(txn, definitionModel, "spatialCategory", new SubCategoryAppearance()); const [_, model] = IModelTestUtils.createAndInsertPhysicalPartitionAndModel(txn, { spec: '0x1', scope: '0x1', value: 'Spatial' }, undefined, parentId); const modelSelector = ModelSelector.insert(txn, definitionModel, "SpatialModelSelector", [model]); const displayStyle = DisplayStyle3d.insert(txn, definitionModel, "DisplayStyle3d"); const categorySelector = CategorySelector.insert(txn, definitionModel, "spatialCategories", [category]); const viewRange = new Range3d(0, 0, 0, 500, 500, 500); SpatialViewDefinition.insertWithCamera(txn, 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 withEditTxn(iModel, async () => { 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"); let jobSubjectId; let definitionModel; let category; let model; let styleId; withEditTxn(imodel, (txn) => { jobSubjectId = createJobSubjectElement(imodel, "Job").insert(txn); definitionModel = DefinitionModel.insert(txn, jobSubjectId, "Definition"); ({ category, model } = insertSpatialModel(txn, jobSubjectId, definitionModel)); styleId = createAnnotationTextStyle(imodel, definitionModel, "test", { font: { name: "Totally Real Font" }, textHeight: 0.25, isItalic: true }).insert(txn); }); 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 = withEditTxn(imodel, (txn) => el0.insert(txn)); 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"); let jobSubjectId; let definitionModel; let category; let model; withEditTxn(imodel, (txn) => { jobSubjectId = createJobSubjectElement(imodel, "Job").insert(txn); definitionModel = DefinitionModel.insert(txn, jobSubjectId, "Definition"); ({ category, model } = insertDrawingModel(txn, 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.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 = withEditTxn(imodel, (txn) => el0.insert(txn)); 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"); let jobSubjectId; let definitionModel; let styleId; let differentStyleId; withEditTxn(imodel, (txn) => { jobSubjectId = createJobSubjectElement(imodel, "Job").insert(txn); definitionModel = DefinitionModel.insert(txn, jobSubjectId, "Definition"); styleId = createAnnotationTextStyle(imodel, definitionModel, "test", { font: { name: "Totally Real Font" }, textHeight: 0.25, isItalic: true }).insert(txn); differentStyleId = createAnnotationTextStyle(imodel, definitionModel, "alt", { font: { name: "Karla" }, textHeight: 0.5, isBold: true }).insert(txn); }); 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.close()); describe("TextAnnotation2d", () => { let createElement2dArgs; before(() => { let category; let model; withEditTxn(imodel, (txn) => { ({ category, model } = insertDrawingModel(txn, 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); withEditTxn(imodel, (txn) => el0.insert(txn)); 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 = withEditTxn(imodel, (txn) => el0.insert(txn)); 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 }; return withEditTxn(db, (txn) => { const elem = createElement2d(db, args); elem.insert(txn); 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() }); withEditTxn(imodel, (txn) => elem.insert(txn)); 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 () => { const env_1 = { stack: [], error: void 0, hasError: false }; try { 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"); // The native code asserts on an invalid ID. Suppress this. const _disableAssertions = __addDisposableResource(env_1, new DisableNativeAssertions(), false); await clone(Id64.invalid, undefined); } catch (e_1) { env_1.error = e_1; env_1.hasError = true; } finally { __disposeResources(env_1); } }); }); describe("between iModels", () => { let dstDb; let dstDefModel; let dstElemArgs; before(async () => { dstDb = await createIModel("CloneTarget"); let jobSubjectId; let category; let model; withEditTxn(dstDb, (txn) => { jobSubjectId = createJobSubjectElement(dstDb, "Job").insert(txn); dstDefModel = DefinitionModel.insert(txn, jobSubjectId, "Definition"); ({ category, model } = insertDrawingModel(txn, 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() }); withEditTxn(imodel, (txn) => elem.insert(txn)); 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 = withEditTxn(dstDb, (txn) => createAnnotationTextStyle(dstDb, dstDefModel, "test", { font: { name: "Karla" } }).insert(txn)); 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 txn = new EditTxn(dstDb, "import default text style if necessary"); txn.start(); const props = await context.cloneElement(srcElem); txn.end(); 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 = withEditTxn(imodel, (editTxn) => createAnnotationTextStyle(imodel, seedDefinitionModelId, "styyyle", { font: { name: "Karla" } }).insert(editTxn)); 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 context2 = new IModelElementCloneContext(imodel, dstDb); context2.remapElement(createElement2dArgs.model, dstElemArgs.model); context2.remapElement(seedDefinitionModelId, dstDefModel); const txn = new EditTxn(dstDb, "remap repeated text styles"); txn.start(); const props1 = await context.cloneElement(srcElem1); const props2 = await context.cloneElement(srcElem2); const props3 = await context2.cloneElement(srcElem3); txn.end(); expect(props1.defaultTextStyle).not.to.be.undefined; expect(props1.defaultTextStyle?.id).not.to.equal(srcStyleId); expect(props2.defaultTextStyle?.id).to.equal(props1.defaultTextStyle?.id); expect(props3.defaultTextStyle?.id).to.equal(props1.defaultTextStyle?.id); }); }); }); }); describe("TextAnnotation3d", () => { let createElement3dArgs; before(() => { let category; let model; withEditTxn(imodel, (txn) => { ({ category, model } = insertSpatialModel(txn, 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); withEditTxn(imodel, (txn) => el0.insert(txn)); 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 = withEditTxn(imodel, (txn) => el0.insert(txn)); 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"); let jobSubjectId; let definitionModel; withEditTxn(imodel, (txn) => { jobSubjectId = createJobSubjectElement(imodel, "Job").insert(txn); definitionModel = DefinitionModel.insert(txn, 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 = withEditTxn(imodel, (txn) => el0.insert(txn)); 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 () => { withEditTxn(imodel, (txn) => { // Default style should fail since it has no font let annotationTextStyle = createAnnotationTextStyle(imodel, seedDefinitionModel, "default"); expect(() => annotationTextStyle.insert(txn)).to.throw(); // font is required annotationTextStyle = createAnnotationTextStyle(imodel, seedDefinitionModel, "no font", { font: { name: "" } }); expect(() => annotationTextStyle.insert(txn)).to.throw(); // textHeight should be positive annotationTextStyle = createAnnotationTextStyle(imodel, seedDefinitionModel, "invalid textHeight", { font: { name: "Totally Real Font" }, textHeight: 0 }); expect(() => annotationTextStyle.insert(txn)).to.throw(); // stackedFractionScale should be positive annotationTextStyle = createAnnotationTextStyle(imodel, seedDefinitionModel, "invalid stackedFractionScale", { font: { name: "Totally Real Font" }, stackedFractionScale: 0 }); expect(() => annotationTextStyle.insert(txn)).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 = withEditTxn(imodel, (txn) => annotationTextStyle.insert(txn)); expect(Id64.isValidId64(elId)).to.be.true; withEditTxn(imodel, (txn) => { 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(txn)).to.throw(); el1.settings = el1.settings.clone({ font: { name: "Totally Real Font" }, textHeight: 0 }); expect(() => el1.update(txn)).to.throw(); el1.settings = el1.settings.clone({ textHeight: 2, stackedFractionScale: 0 }); expect(() => el1.update(txn)).to.throw(); el1.settings = el1.settings.clone({ stackedFractionScale: 0.45 }); el1.update(txn); }); const updatedElement = imodel.elements.getElement(elId); expect(updatedElement.settings.stackedFractionScale).to.equal(0.45); }); 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: { ...Text