diff --git a/common/api/core-backend.api.md b/common/api/core-backend.api.md index 9d1934ed613..2252f1bf639 100644 --- a/common/api/core-backend.api.md +++ b/common/api/core-backend.api.md @@ -326,9 +326,11 @@ export class AnnotationTextStyle extends DefinitionElement { description?: string; static deserialize(props: DeserializeEntityArgs): AnnotationTextStyleProps; static fromJSON(props: AnnotationTextStyleProps, iModel: IModelDb): AnnotationTextStyle; + // (undocumented) + protected static onCloned(context: IModelElementCloneContext, srcProps: AnnotationTextStyleProps, dstProps: AnnotationTextStyleProps): Promise; protected static onInsert(arg: OnElementPropsArg): void; protected static onUpdate(arg: OnElementPropsArg): void; - static remapTextStyleId(sourceTextStyleId: Id64String, context: IModelElementCloneContext): Id64String; + static remapTextStyleId(sourceTextStyleId: Id64String, context: IModelElementCloneContext): Promise; static serialize(props: AnnotationTextStyleProps, iModel: IModelDb): ECSqlRow; settings: TextStyleSettings; toJSON(): AnnotationTextStyleProps; @@ -1900,7 +1902,7 @@ export abstract class DisplayStyle extends DefinitionElement { // (undocumented) loadScheduleScript(): RenderSchedule.ScriptReference | undefined; // @alpha (undocumented) - protected static onCloned(context: IModelElementCloneContext, sourceElementProps: DisplayStyleProps, targetElementProps: DisplayStyleProps): void; + protected static onCloned(context: IModelElementCloneContext, sourceElementProps: DisplayStyleProps, targetElementProps: DisplayStyleProps): Promise; // @beta (undocumented) static serialize(props: DisplayStyleProps, iModel: IModelDb): ECSqlRow; // (undocumented) @@ -1928,7 +1930,7 @@ export class DisplayStyle3d extends DisplayStyle { static create(iModelDb: IModelDb, definitionModelId: Id64String, name: string, options?: DisplayStyleCreationOptions): DisplayStyle3d; static insert(iModelDb: IModelDb, definitionModelId: Id64String, name: string, options?: DisplayStyleCreationOptions): Id64String; // @alpha (undocumented) - protected static onCloned(context: IModelElementCloneContext, sourceElementProps: DisplayStyle3dProps, targetElementProps: DisplayStyle3dProps): void; + protected static onCloned(context: IModelElementCloneContext, sourceElementProps: DisplayStyle3dProps, targetElementProps: DisplayStyle3dProps): Promise; // (undocumented) get settings(): DisplayStyle3dSettings; } @@ -2449,7 +2451,7 @@ class Element_2 extends Entity { // @beta protected static onChildUpdated(arg: OnChildElementIdArg): void; // @beta - protected static onCloned(_context: IModelElementCloneContext, _sourceProps: ElementProps, _targetProps: ElementProps): void; + protected static onCloned(_context: IModelElementCloneContext, _sourceProps: ElementProps, _targetProps: ElementProps): Promise | void; // @beta protected static onDelete(arg: OnElementIdArg): void; // @beta @@ -3869,7 +3871,7 @@ export class IModelElementCloneContext { [Symbol.dispose](): void; constructor(sourceDb: IModelDb, targetDb?: IModelDb); // @internal - cloneElement(sourceElement: Element_2, cloneOptions?: IModelJsNative.CloneElementOptions): ElementProps; + cloneElement(sourceElement: Element_2, cloneOptions?: IModelJsNative.CloneElementOptions): Promise; static create(...args: ConstructorParameters): Promise; // @deprecated (undocumented) dispose(): void; @@ -5281,7 +5283,7 @@ export class RenderMaterialElement extends DefinitionElement { static deserialize(props: DeserializeEntityArgs): RenderMaterialProps; static insert(iModelDb: IModelDb, definitionModelId: Id64String, materialName: string, params: RenderMaterialElementParams): Id64String; // @beta (undocumented) - protected static onCloned(context: IModelElementCloneContext, sourceProps: ElementProps, targetProps: ElementProps): void; + protected static onCloned(context: IModelElementCloneContext, sourceProps: ElementProps, targetProps: ElementProps): Promise; paletteName: string; // @beta static serialize(props: RenderMaterialProps, iModel: IModelDb): ECSqlRow; @@ -5335,7 +5337,7 @@ export class RenderTimeline extends InformationRecordElement { // (undocumented) static fromJSON(props: RenderTimelineProps, iModel: IModelDb): RenderTimeline; // @alpha (undocumented) - protected static onCloned(context: IModelElementCloneContext, sourceProps: RenderTimelineProps, targetProps: RenderTimelineProps): void; + protected static onCloned(context: IModelElementCloneContext, sourceProps: RenderTimelineProps, targetProps: RenderTimelineProps): Promise; // @beta static remapScript(context: IModelElementCloneContext, input: RenderSchedule.ScriptProps): RenderSchedule.ScriptProps; scriptProps: RenderSchedule.ScriptProps; @@ -6461,7 +6463,7 @@ export class TextAnnotation2d extends AnnotationElement2d { // @internal (undocumented) getTextBlocks(): Iterable; // @internal (undocumented) - protected static onCloned(context: IModelElementCloneContext, srcProps: TextAnnotation2dProps, dstProps: TextAnnotation2dProps): void; + protected static onCloned(context: IModelElementCloneContext, srcProps: TextAnnotation2dProps, dstProps: TextAnnotation2dProps): Promise; // @beta protected static onInsert(arg: OnElementPropsArg): void; // @internal (undocumented) @@ -6510,7 +6512,7 @@ export class TextAnnotation3d extends GraphicalElement3d { // @internal (undocumented) getTextBlocks(): Iterable; // @internal (undocumented) - protected static onCloned(context: IModelElementCloneContext, srcProps: TextAnnotation3dProps, dstProps: TextAnnotation3dProps): void; + protected static onCloned(context: IModelElementCloneContext, srcProps: TextAnnotation3dProps, dstProps: TextAnnotation3dProps): Promise; // @beta protected static onInsert(arg: OnElementPropsArg): void; // @internal (undocumented) @@ -6952,7 +6954,7 @@ export abstract class ViewDefinition extends DefinitionElement { loadCategorySelector(): CategorySelector; loadDisplayStyle(): DisplayStyle; // @beta (undocumented) - protected static onCloned(context: IModelElementCloneContext, sourceElementProps: ViewDefinitionProps, targetElementProps: ViewDefinitionProps): void; + protected static onCloned(context: IModelElementCloneContext, sourceElementProps: ViewDefinitionProps, targetElementProps: ViewDefinitionProps): Promise; // @beta (undocumented) static readonly requiredReferenceKeys: ReadonlyArray; // @alpha (undocumented) diff --git a/common/changes/@itwin/core-backend/pmc-clone-text-style_2025-10-14-12-22.json b/common/changes/@itwin/core-backend/pmc-clone-text-style_2025-10-14-12-22.json new file mode 100644 index 00000000000..b636ec5dcb8 --- /dev/null +++ b/common/changes/@itwin/core-backend/pmc-clone-text-style_2025-10-14-12-22.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@itwin/core-backend", + "comment": "Element.onCloned can now invoke asynchronous operations.", + "type": "none" + } + ], + "packageName": "@itwin/core-backend" +} \ No newline at end of file diff --git a/core/backend/src/DisplayStyle.ts b/core/backend/src/DisplayStyle.ts index 9c3aea81c0b..1d9e203150b 100644 --- a/core/backend/src/DisplayStyle.ts +++ b/core/backend/src/DisplayStyle.ts @@ -87,8 +87,8 @@ export abstract class DisplayStyle extends DefinitionElement { } /** @alpha */ - protected static override onCloned(context: IModelElementCloneContext, sourceElementProps: DisplayStyleProps, targetElementProps: DisplayStyleProps): void { - super.onCloned(context, sourceElementProps, targetElementProps); + protected static override async onCloned(context: IModelElementCloneContext, sourceElementProps: DisplayStyleProps, targetElementProps: DisplayStyleProps): Promise { + await super.onCloned(context, sourceElementProps, targetElementProps); if (!context.isBetweenIModels || !targetElementProps.jsonProperties?.styles) return; @@ -252,8 +252,8 @@ export class DisplayStyle3d extends DisplayStyle { } /** @alpha */ - protected static override onCloned(context: IModelElementCloneContext, sourceElementProps: DisplayStyle3dProps, targetElementProps: DisplayStyle3dProps): void { - super.onCloned(context, sourceElementProps, targetElementProps); + protected static override async onCloned(context: IModelElementCloneContext, sourceElementProps: DisplayStyle3dProps, targetElementProps: DisplayStyle3dProps): Promise { + await super.onCloned(context, sourceElementProps, targetElementProps); if (context.isBetweenIModels) { const convertTexture = (id: string) => Id64.isValidId64(id) ? context.findTargetElementId(id) : id; diff --git a/core/backend/src/Element.ts b/core/backend/src/Element.ts index 150b7aa93b8..38957a7bc3a 100644 --- a/core/backend/src/Element.ts +++ b/core/backend/src/Element.ts @@ -387,7 +387,7 @@ export class Element extends Entity { * @note If you override this method, you must call super. * @beta */ - protected static onCloned(_context: IModelElementCloneContext, _sourceProps: ElementProps, _targetProps: ElementProps): void { } + protected static onCloned(_context: IModelElementCloneContext, _sourceProps: ElementProps, _targetProps: ElementProps): Promise | void { } /** Called when a *root* element in a subgraph is changed and before its outputs are processed. * This special callback is made when: @@ -2167,8 +2167,8 @@ export class RenderTimeline extends InformationRecordElement { } /** @alpha */ - protected static override onCloned(context: IModelElementCloneContext, sourceProps: RenderTimelineProps, targetProps: RenderTimelineProps): void { - super.onCloned(context, sourceProps, targetProps); + protected static override async onCloned(context: IModelElementCloneContext, sourceProps: RenderTimelineProps, targetProps: RenderTimelineProps): Promise { + await super.onCloned(context, sourceProps, targetProps); if (context.isBetweenIModels) targetProps.script = JSON.stringify(this.remapScript(context, this.parseScriptProps(targetProps.script))); } diff --git a/core/backend/src/IModelElementCloneContext.ts b/core/backend/src/IModelElementCloneContext.ts index 9b0eb892eea..7227af8e51d 100644 --- a/core/backend/src/IModelElementCloneContext.ts +++ b/core/backend/src/IModelElementCloneContext.ts @@ -148,7 +148,7 @@ export class IModelElementCloneContext { /** Clone the specified source Element into ElementProps for the target iModel. * @internal */ - public cloneElement(sourceElement: Element, cloneOptions?: IModelJsNative.CloneElementOptions): ElementProps { + public async cloneElement(sourceElement: Element, cloneOptions?: IModelJsNative.CloneElementOptions): Promise { const targetElementProps: ElementProps = this._nativeContext.cloneElement(sourceElement.id, cloneOptions); // Ensure that all NavigationProperties in targetElementProps have a defined value so "clearing" changes will be part of the JSON used for update // eslint-disable-next-line @typescript-eslint/no-deprecated @@ -170,7 +170,7 @@ export class IModelElementCloneContext { } const jsClass = this.sourceDb.getJsClass(sourceElement.classFullName); // eslint-disable-next-line @typescript-eslint/dot-notation - jsClass["onCloned"](this, sourceElement.toJSON(), targetElementProps); + await jsClass["onCloned"](this, sourceElement.toJSON(), targetElementProps); return targetElementProps; } diff --git a/core/backend/src/Material.ts b/core/backend/src/Material.ts index 399659f0be4..f8da9f61176 100644 --- a/core/backend/src/Material.ts +++ b/core/backend/src/Material.ts @@ -218,8 +218,8 @@ export class RenderMaterialElement extends DefinitionElement { } /** @beta */ - protected static override onCloned(context: IModelElementCloneContext, sourceProps: ElementProps, targetProps: ElementProps) { - super.onCloned(context, sourceProps, targetProps); + protected static override async onCloned(context: IModelElementCloneContext, sourceProps: ElementProps, targetProps: ElementProps) { + await super.onCloned(context, sourceProps, targetProps); for (const mapName in sourceProps.jsonProperties?.materialAssets?.renderMaterial?.Map ?? {}) { if (typeof mapName !== "string") continue; diff --git a/core/backend/src/ViewDefinition.ts b/core/backend/src/ViewDefinition.ts index 5807d8ab188..181239bac35 100644 --- a/core/backend/src/ViewDefinition.ts +++ b/core/backend/src/ViewDefinition.ts @@ -312,8 +312,8 @@ export abstract class ViewDefinition extends DefinitionElement { }; /** @beta */ - protected static override onCloned(context: IModelElementCloneContext, sourceElementProps: ViewDefinitionProps, targetElementProps: ViewDefinitionProps): void { - super.onCloned(context, sourceElementProps, targetElementProps); + protected static override async onCloned(context: IModelElementCloneContext, sourceElementProps: ViewDefinitionProps, targetElementProps: ViewDefinitionProps): Promise { + await super.onCloned(context, sourceElementProps, targetElementProps); if (context.isBetweenIModels && targetElementProps.jsonProperties && targetElementProps.jsonProperties.viewDetails) { const acsId: Id64String = Id64.fromJSON(targetElementProps.jsonProperties.viewDetails.acs); if (Id64.isValidId64(acsId)) { diff --git a/core/backend/src/annotations/ElementDrivesTextAnnotation.ts b/core/backend/src/annotations/ElementDrivesTextAnnotation.ts index 47e809028e7..7fa529ed3f6 100644 --- a/core/backend/src/annotations/ElementDrivesTextAnnotation.ts +++ b/core/backend/src/annotations/ElementDrivesTextAnnotation.ts @@ -148,7 +148,7 @@ export class ElementDrivesTextAnnotation extends ElementDrivesElement { /** When copying an [[ITextAnnotation]] from one iModel into another, remaps the element Ids in any [FieldPropertyHost]($common) within the cloned element * so that they refer to elements in the `context`'s target iModel, and sets any Ids that cannot be remapped to [Id64.invalid]($bentley). - * Implementations of `ITextAnnotation` should invoke this function from their implementations of [[Element._onCloned]]. + * Implementations of `ITextAnnotation` should invoke this function from their implementations of [[Element.onCloned]]. */ public static remapFields(clone: ITextAnnotation, context: IModelElementCloneContext): void { if (!context.isBetweenIModels) { diff --git a/core/backend/src/annotations/TextAnnotationElement.ts b/core/backend/src/annotations/TextAnnotationElement.ts index 9d7e3014648..dc08b79c5b1 100644 --- a/core/backend/src/annotations/TextAnnotationElement.ts +++ b/core/backend/src/annotations/TextAnnotationElement.ts @@ -297,15 +297,15 @@ export class TextAnnotation2d extends AnnotationElement2d /* implements ITextAnn } /** @internal */ - protected static override onCloned(context: IModelElementCloneContext, srcProps: TextAnnotation2dProps, dstProps: TextAnnotation2dProps): void { - super.onCloned(context, srcProps, dstProps); + protected static override async onCloned(context: IModelElementCloneContext, srcProps: TextAnnotation2dProps, dstProps: TextAnnotation2dProps): Promise { + await super.onCloned(context, srcProps, dstProps); const srcElem = TextAnnotation2d.fromJSON(srcProps, context.sourceDb); ElementDrivesTextAnnotation.remapFields(srcElem, context); const anno = srcElem.getAnnotation(); dstProps.textAnnotationData = anno ? JSON.stringify({ version: TEXT_ANNOTATION_JSON_VERSION, data: anno.toJSON() }) : undefined; - remapTextStyle(context, srcElem, dstProps); + return remapTextStyle(context, srcElem, dstProps); } } @@ -484,24 +484,24 @@ export class TextAnnotation3d extends GraphicalElement3d /* implements ITextAnno } /** @internal */ - protected static override onCloned(context: IModelElementCloneContext, srcProps: TextAnnotation3dProps, dstProps: TextAnnotation3dProps): void { - super.onCloned(context, srcProps, dstProps); + protected static override async onCloned(context: IModelElementCloneContext, srcProps: TextAnnotation3dProps, dstProps: TextAnnotation3dProps): Promise { + await super.onCloned(context, srcProps, dstProps); const srcElem = TextAnnotation3d.fromJSON(srcProps, context.sourceDb); ElementDrivesTextAnnotation.remapFields(srcElem, context); const anno = srcElem.getAnnotation(); dstProps.textAnnotationData = anno ? JSON.stringify({ version: TEXT_ANNOTATION_JSON_VERSION, data: anno.toJSON() }) : undefined; - remapTextStyle(context, srcElem, dstProps); + return remapTextStyle(context, srcElem, dstProps); } } -function remapTextStyle( +async function remapTextStyle( context: IModelElementCloneContext, srcElem: TextAnnotation2d | TextAnnotation3d, dstProps: TextAnnotation2dProps | TextAnnotation3dProps -): void { - const dstStyleId = AnnotationTextStyle.remapTextStyleId(srcElem.defaultTextStyle?.id ?? Id64.invalid, context); +): Promise { + const dstStyleId = await AnnotationTextStyle.remapTextStyleId(srcElem.defaultTextStyle?.id ?? Id64.invalid, context); dstProps.defaultTextStyle = Id64.isValid(dstStyleId) ? new TextAnnotationUsesTextStyleByDefault(dstStyleId).toJSON() : undefined; } @@ -721,10 +721,10 @@ export class AnnotationTextStyle extends DefinitionElement { * corresponding to `sourceTextStyleId`, or [Id64.invalid]($bentley) if no corresponding text style exists. * If a text style with the same [Code]($common) exists in the target iModel, the style Id will be remapped to refer to that style. * Otherwise, a copy of the style will be imported into the target iModel and its element Id returned. - * Implementations of [[ITextAnnotation]] should invoke this function when implementing their [[Element._onCloned]] method. + * Implementations of [[ITextAnnotation]] should invoke this function when implementing their [[Element.onCloned]] method. * @throws Error if an attempt to import the text style failed. */ - public static remapTextStyleId(sourceTextStyleId: Id64String, context: IModelElementCloneContext): Id64String { + public static async remapTextStyleId(sourceTextStyleId: Id64String, context: IModelElementCloneContext): Promise { // No remapping necessary if there's no text style or we're not copying to a different iModel. if (!Id64.isValid(sourceTextStyleId) || !context.isBetweenIModels) { return sourceTextStyleId; @@ -749,9 +749,28 @@ export class AnnotationTextStyle extends DefinitionElement { } // Copy the style into the target iModel and remap its Id. - const dstStyleProps = context.cloneElement(srcStyle); + const dstStyleProps = await context.cloneElement(srcStyle); dstStyleId = context.targetDb.elements.insertElement(dstStyleProps); context.remapElement(sourceTextStyleId, dstStyleId); return dstStyleId; } + + protected static override async onCloned(context: IModelElementCloneContext, srcProps: AnnotationTextStyleProps, dstProps: AnnotationTextStyleProps): Promise { + await super.onCloned(context, srcProps, dstProps); + if (!context.isBetweenIModels) { + return; + } + + const settingsProps = AnnotationTextStyle.parseTextStyleSettings(srcProps.settings); + const font = TextStyleSettings.fromJSON(settingsProps?.data).font; + + const fontsToEmbed = []; + for (const file of context.sourceDb.fonts.queryEmbeddedFontFiles()) { + if (file.type === font.type && file.faces.some((face) => face.familyName === font.name)) { + fontsToEmbed.push(file); + } + } + + await Promise.all(fontsToEmbed.map(async (file) => context.targetDb.fonts.embedFontFile({ file }))); + } } diff --git a/core/backend/src/test/annotations/TextAnnotation.test.ts b/core/backend/src/test/annotations/TextAnnotation.test.ts index fa6144b6a88..bab174fd6fa 100644 --- a/core/backend/src/test/annotations/TextAnnotation.test.ts +++ b/core/backend/src/test/annotations/TextAnnotation.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { expect } from "chai"; import { Angle, Point3d, Range2d, Range3d, YawPitchRollAngles } from "@itwin/core-geometry"; -import { AnnotationTextStyleProps, FieldRun, FractionRun, Placement2dProps, Placement3dProps, SubCategoryAppearance, TextAnnotation, TextAnnotation2dProps, TextBlock, TextRun, TextStyleSettings, TextStyleSettingsProps } from "@itwin/core-common"; +import { AnnotationTextStyleProps, FieldRun, FontType, FractionRun, Placement2dProps, Placement3dProps, SubCategoryAppearance, TextAnnotation, TextAnnotation2dProps, TextBlock, TextRun, TextStyleSettings, TextStyleSettingsProps } from "@itwin/core-common"; import { IModelDb, StandaloneDb } from "../../IModelDb"; import { AnnotationTextStyle, parseTextAnnotationData, TEXT_ANNOTATION_JSON_VERSION, TEXT_STYLE_SETTINGS_JSON_VERSION, TextAnnotation2d, TextAnnotation2dCreateArgs, TextAnnotation3d, TextAnnotation3dCreateArgs } from "../../annotations/TextAnnotationElement"; import { IModelTestUtils } from "../IModelTestUtils"; @@ -20,7 +20,7 @@ import { TextAnnotationUsesTextStyleByDefault } from "../../annotations/ElementD import { layoutTextBlock, TextStyleResolver } from "../../annotations/TextBlockLayout"; import { appendTextAnnotationGeometry } from "../../annotations/TextAnnotationGeometry"; import { IModelElementCloneContext } from "../../IModelElementCloneContext"; - +import * as fs from "fs"; function mockIModel(): IModelDb { const iModel: Pick = { @@ -564,7 +564,7 @@ describe("TextAnnotation element", () => { } describe("within a single iModel", () => { - it("leaves property hosts intact", () => { + it("leaves property hosts intact", async () => { const textBlock = TextBlock.create({ children: [{ children: [{ @@ -597,7 +597,7 @@ describe("TextAnnotation element", () => { context.remapElement("0xabc", "0xdef"); context.remapElement(createElement2dArgs.model, createElement2dArgs.model); - const props = context.cloneElement(elem) as TextAnnotation2dProps; + const props = await context.cloneElement(elem) as TextAnnotation2dProps; expect(props.textAnnotationData).not.to.be.undefined; const anno = TextAnnotation.fromJSON(parseTextAnnotationData(props.textAnnotationData)?.data); const para = anno.textBlock.children[0]; @@ -606,27 +606,27 @@ describe("TextAnnotation element", () => { }); - it("leaves default text style intact", () => { - function clone(styleId: Id64String | undefined, expectedStyleId: Id64String | undefined): void { + it("leaves default text style intact", async () => { + async function clone(styleId: Id64String | undefined, expectedStyleId: Id64String | undefined): Promise { const elem = insertStyledElement(styleId, imodel); const context = new IModelElementCloneContext(imodel); context.remapElement(createElement2dArgs.model, createElement2dArgs.model); - const props = context.cloneElement(elem) as TextAnnotation2dProps; + const props = await context.cloneElement(elem) as TextAnnotation2dProps; 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 = context.cloneElement(elem) as TextAnnotation2dProps; + const props2 = await context.cloneElement(elem) as TextAnnotation2dProps; expect(props2.defaultTextStyle?.id).to.equal(expectedStyleId); } } - clone(seedStyleId, seedStyleId); - clone(undefined, undefined); - clone("0x12345", "0x12345"); - clone(Id64.invalid, undefined); + await clone(seedStyleId, seedStyleId); + await clone(undefined, undefined); + await clone("0x12345", "0x12345"); + await clone(Id64.invalid, undefined); }); }); @@ -649,7 +649,7 @@ describe("TextAnnotation element", () => { after(() => dstDb.close()); - it("remaps property hosts", () => { + it("remaps property hosts", async () => { const textBlock = TextBlock.create({ children: [{ children: [{ @@ -682,7 +682,7 @@ describe("TextAnnotation element", () => { context.remapElement("0xabc", "0xdef"); context.remapElement(createElement2dArgs.model, dstElemArgs.model); - const props = context.cloneElement(elem) as TextAnnotation2dProps; + const props = await context.cloneElement(elem) as TextAnnotation2dProps; expect(props.textAnnotationData).not.to.be.undefined; const anno = TextAnnotation.fromJSON(parseTextAnnotationData(props.textAnnotationData)?.data); const para = anno.textBlock.children[0]; @@ -690,15 +690,15 @@ describe("TextAnnotation element", () => { expect((para.children[1] as FieldRun).propertyHost.elementId).to.equal("0xdef"); }); - it("sets default text style to undefined if source style does not exist", () => { + 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 = context.cloneElement(elem) as TextAnnotation2dProps; + const props = await context.cloneElement(elem) as TextAnnotation2dProps; expect(props.defaultTextStyle).to.be.undefined; }); - it("remaps to an existing text style with the same code if present", () => { + 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); @@ -706,32 +706,32 @@ describe("TextAnnotation element", () => { const context = new IModelElementCloneContext(imodel, dstDb); context.remapElement(createElement2dArgs.model, dstElemArgs.model); - const props = context.cloneElement(srcElem) as TextAnnotation2dProps; + const props = await context.cloneElement(srcElem) as TextAnnotation2dProps; expect(props.defaultTextStyle?.id).to.equal(dstStyleId); }); - it("throws an error if definition model is not remapped", () => { + 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); - expect(() => context.cloneElement(srcElem)).to.throw("Invalid target model"); + await expect(context.cloneElement(srcElem)).to.be.rejectedWith("Invalid target model"); }); - it("imports default text style if necessary", () => { + 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 = context.cloneElement(srcElem) as TextAnnotation2dProps; + const props = await context.cloneElement(srcElem) as TextAnnotation2dProps; 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", () => { + 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); @@ -741,8 +741,8 @@ describe("TextAnnotation element", () => { context.remapElement(createElement2dArgs.model, dstElemArgs.model); context.remapElement(seedDefinitionModelId, dstDefModel); - const props1 = context.cloneElement(srcElem1) as TextAnnotation2dProps; - const props2 = context.cloneElement(srcElem2) as TextAnnotation2dProps; + const props1 = await context.cloneElement(srcElem1) as TextAnnotation2dProps; + const props2 = await context.cloneElement(srcElem2) as TextAnnotation2dProps; expect(props1.defaultTextStyle).not.to.be.undefined; expect(props1.defaultTextStyle?.id).not.to.equal(srcStyleId); expect(props2.defaultTextStyle?.id).to.equal(props1.defaultTextStyle?.id); @@ -750,7 +750,7 @@ describe("TextAnnotation element", () => { const context2 = new IModelElementCloneContext(imodel, dstDb); context2.remapElement(createElement2dArgs.model, dstElemArgs.model); context2.remapElement(seedDefinitionModelId, dstDefModel); - const props3 = context2.cloneElement(srcElem3) as TextAnnotation2dProps; + const props3 = await context2.cloneElement(srcElem3) as TextAnnotation2dProps; expect(props3.defaultTextStyle?.id).to.equal(props1.defaultTextStyle?.id); }); }); @@ -959,6 +959,63 @@ describe("AnnotationTextStyle", () => { })).to.throw(`Migration for settings from version 0.0.1 to ${TEXT_STYLE_SETTINGS_JSON_VERSION} failed.`); }); }) + + describe("onCloned", () => { + let targetDb: StandaloneDb; + let targetDefModel: string; + + 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", () => { diff --git a/core/backend/src/test/standalone/DisplayStyle.test.ts b/core/backend/src/test/standalone/DisplayStyle.test.ts index 7e69e437e03..714def400a8 100644 --- a/core/backend/src/test/standalone/DisplayStyle.test.ts +++ b/core/backend/src/test/standalone/DisplayStyle.test.ts @@ -92,7 +92,7 @@ describe("DisplayStyle", () => { db2.close(); }); - it("remaps contour display subCategories when cloning", () => { + it("remaps contour display subCategories when cloning", async () => { const cloneContext = new IModelElementCloneContext(db, db2); const displayStyleJsonProps: DisplayStyle3dSettingsProps = { contours: { @@ -108,14 +108,14 @@ describe("DisplayStyle", () => { cloneContext.remapElement("0x1", "0xa"); cloneContext.remapElement("0x3", "0xc"); const displayStyle = db.elements.getElement(displayStyleId); - const displayStyleClone = cloneContext.cloneElement(displayStyle); + const displayStyleClone = await cloneContext.cloneElement(displayStyle); const contourSubCatsClone = CompressedId64Set.decompressArray(displayStyleClone.jsonProperties.styles.contours.groups[0].subCategories); expect(contourSubCatsClone.length).to.equal(2); expect(contourSubCatsClone).to.contain.members(["0xa", "0xc"]); }); - it("remaps excludedElements when cloning", () => { + it("remaps excludedElements when cloning", async () => { const cloneContext = new IModelElementCloneContext(db, db2); const displayStyleJsonProps: DisplayStyleSettingsProps = {excludedElements: ["0x1", "0x2", "0x3", "0x4"]}; const displayStyleId = DisplayStyle3d.insert(db, IModel.dictionaryId, "TestStyle", displayStyleJsonProps); @@ -123,14 +123,14 @@ describe("DisplayStyle", () => { cloneContext.remapElement("0x1", "0xa"); cloneContext.remapElement("0x3", "0xc"); const displayStyle = db.elements.getElement(displayStyleId); - const displayStyleClone = cloneContext.cloneElement(displayStyle); + const displayStyleClone = await cloneContext.cloneElement(displayStyle); const excludedElementsClone = CompressedId64Set.decompressArray(displayStyleClone.jsonProperties.styles.excludedElements); expect(excludedElementsClone.length).to.equal(2); expect(excludedElementsClone).to.contain.members(["0xa", "0xc"]); }); - it("remaps subCategory overrides when cloning", () => { + it("remaps subCategory overrides when cloning", async () => { const cloneContext = new IModelElementCloneContext(db, db2); const categoryId = SpatialCategory.insert(db, IModel.dictionaryId, "testCat", {}); const subCategoryId1 = SubCategory.insert(db, categoryId, "subC1", {}); @@ -148,7 +148,7 @@ describe("DisplayStyle", () => { cloneContext.remapElement(subCategoryId1, "0xa"); cloneContext.remapElement(subCategoryId4, "0xd"); const displayStyle = db.elements.getElement(displayStyleId); - const displayStyleClone = cloneContext.cloneElement(displayStyle); + const displayStyleClone = await cloneContext.cloneElement(displayStyle); const subCategoryOvrClone = displayStyleClone.jsonProperties.styles.subCategoryOvr; expect(subCategoryOvrClone.length).to.equal(2); diff --git a/core/backend/src/test/standalone/RenderMaterialElement.test.ts b/core/backend/src/test/standalone/RenderMaterialElement.test.ts index df63e387762..ba9c752c37b 100644 --- a/core/backend/src/test/standalone/RenderMaterialElement.test.ts +++ b/core/backend/src/test/standalone/RenderMaterialElement.test.ts @@ -396,7 +396,7 @@ describe("RenderMaterialElement", () => { }); describe("clone", () => { - it("clone maps", () => { + it("clone maps", async () => { const textureId = insertTexture(); const unknownTextureId = "0xffffff"; @@ -439,7 +439,7 @@ describe("RenderMaterialElement", () => { const targetProps = structuredClone(sourceProps); // eslint-disable-next-line @typescript-eslint/dot-notation - RenderMaterialElement["onCloned"](context, sourceProps, targetProps); + await RenderMaterialElement["onCloned"](context, sourceProps, targetProps); expect(targetProps.jsonProperties?.materialAssets?.renderMaterial?.Map).to.deep.equal({ Pattern: { TextureId: "CLONED" }, @@ -461,12 +461,12 @@ describe("RenderMaterialElement", () => { jsonProps.materialAssets.renderMaterial.Map = {Pattern: undefined}; // eslint-disable-next-line @typescript-eslint/dot-notation - RenderMaterialElement["onCloned"](context, sourceProps, targetProps); + await RenderMaterialElement["onCloned"](context, sourceProps, targetProps); // keep the sourceMap the same in targetProps expect(targetProps.jsonProperties?.materialAssets?.renderMaterial?.Map).to.have.property("Pattern").that.is.undefined; jsonProps.materialAssets.renderMaterial.Map = {Pattern: null as any}; // eslint-disable-next-line @typescript-eslint/dot-notation - RenderMaterialElement["onCloned"](context, sourceProps, targetProps); + await RenderMaterialElement["onCloned"](context, sourceProps, targetProps); // keep the sourceMap the same in targetProps expect(targetProps.jsonProperties?.materialAssets?.renderMaterial?.Map).to.have.property("Pattern").that.is.null; });