@itwin/core-backend
Version:
iTwin.js backend components
633 lines • 27.1 kB
JavaScript
/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
/** @packageDocumentation
* @module Elements
*/
import { BisCodeSpec, Code, ElementGeometry, Placement2d, Placement3d, TextAnnotation, TextStyleSettings, traverseTextBlockComponent } from "@itwin/core-common";
import { AnnotationElement2d, DefinitionElement, Drawing, GraphicalElement3d } from "../Element";
import { assert, Id64 } from "@itwin/core-bentley";
import { layoutTextBlock, TextStyleResolver } from "./TextBlockLayout";
import { appendTextAnnotationGeometry } from "./TextAnnotationGeometry";
import { ElementDrivesTextAnnotation, TextAnnotationUsesTextStyleByDefault } from "./ElementDrivesTextAnnotation";
import * as semver from "semver";
/** The version of the JSON stored in `TextAnnotation2d/3dProps.textAnnotationData` used by the code.
* Uses the same semantics as [ECVersion]($ecschema-metadata).
* @internal
*/
export const TEXT_ANNOTATION_JSON_VERSION = "1.0.0";
function validateAndMigrateVersionedJSON(json, currentVersion, migrate) {
let parsed;
try {
parsed = JSON.parse(json);
}
catch {
return undefined;
}
const version = parsed.version;
if (typeof version !== "string" || !semver.valid(version))
throw new Error("JSON version is missing or invalid.");
if (typeof parsed.data !== "object" || parsed.data === null)
throw new Error("JSON data is missing or invalid.");
// Newer
if (semver.gt(version, currentVersion))
throw new Error(`JSON version ${parsed.version} is newer than supported version ${currentVersion}. Application update required to understand data.`);
// Older
if (semver.lt(version, currentVersion)) {
parsed.data = migrate(parsed);
parsed.version = currentVersion;
}
return parsed;
}
function migrateTextAnnotationData(oldData) {
if (oldData.version === TEXT_ANNOTATION_JSON_VERSION)
return oldData.data;
// Place migration logic here.
throw new Error(`Migration for textAnnotationData from version ${oldData.version} to ${TEXT_ANNOTATION_JSON_VERSION} failed.`);
}
/** Parses, validates, and potentially migrates the text annotation data from a JSON string.
* @internal
*/
export function parseTextAnnotationData(json) {
if (!json)
return undefined;
return validateAndMigrateVersionedJSON(json, TEXT_ANNOTATION_JSON_VERSION, migrateTextAnnotationData);
}
function getElementGeometryBuilderParams(iModel, modelId, categoryId, _placementProps, annotationProps, textStyleId, _subCategory) {
const textBlock = TextAnnotation.fromJSON(annotationProps).textBlock;
const textStyleResolver = new TextStyleResolver({ textBlock, textStyleId: textStyleId ?? "", iModel });
const layout = layoutTextBlock({ iModel, textBlock, textStyleResolver });
const builder = new ElementGeometry.Builder();
let scaleFactor = 1;
const element = iModel.elements.getElement(modelId);
if (element instanceof Drawing)
scaleFactor = element.scaleFactor;
appendTextAnnotationGeometry({ layout, textStyleResolver, scaleFactor, annotationProps: annotationProps ?? {}, builder, categoryId });
return { entryArray: builder.entries };
}
/** An element that displays textual content within a 2d model.
* The text is stored as a [TextAnnotation]($common) from which the element's [geometry]($docs/learning/common/GeometryStream.md) and [Placement]($common) are computed.
* @see [[setAnnotation]] to change the textual content.
* @public @preview
*/
export class TextAnnotation2d extends AnnotationElement2d /* implements ITextAnnotation */ {
/** @internal */
static get className() { return "TextAnnotation2d"; }
/**
* The default [[AnnotationTextStyle]] used by the TextAnnotation2d.
* @beta
*/
defaultTextStyle;
/** The data associated with the text annotation. */
_textAnnotationProps;
/** Extract the textual content, if present.
* @see [[setAnnotation]] to change it.
*/
getAnnotation() {
return this._textAnnotationProps ? TextAnnotation.fromJSON(this._textAnnotationProps) : undefined;
}
/** Change the textual content of the `TextAnnotation2d`.
* @see [[getAnnotation]] to extract the current annotation.
* @param annotation The new annotation
*/
setAnnotation(annotation) {
this._textAnnotationProps = annotation.toJSON();
}
constructor(props, iModel) {
super(props, iModel);
if (props.defaultTextStyle) {
this.defaultTextStyle = new TextAnnotationUsesTextStyleByDefault(props.defaultTextStyle.id);
}
this._textAnnotationProps = parseTextAnnotationData(props.textAnnotationData)?.data;
}
/** Creates a new instance of `TextAnnotation2d` from its JSON representation. */
static fromJSON(props, iModel) {
return new TextAnnotation2d(props, iModel);
}
/**
* Converts the current `TextAnnotation2d` instance to its JSON representation.
* It also computes the `elementGeometryBuilderParams` property used to create the GeometryStream.
* @inheritdoc
*/
toJSON() {
const props = super.toJSON();
props.textAnnotationData = this._textAnnotationProps ? JSON.stringify({ version: TEXT_ANNOTATION_JSON_VERSION, data: this._textAnnotationProps }) : undefined;
if (this._textAnnotationProps) {
props.elementGeometryBuilderParams = getElementGeometryBuilderParams(this.iModel, this.model, this.category, this.placement, this._textAnnotationProps, this.defaultTextStyle ? this.defaultTextStyle.id : undefined);
}
return props;
}
/** Creates a new `TextAnnotation2d` instance with the specified properties.
* @param iModelDb The iModel.
* @param arg The arguments for creating the TextAnnotation2d.
* @beta
*/
static create(iModelDb, arg) {
const elementProps = {
classFullName: this.classFullName,
textAnnotationData: arg.textAnnotationProps ? JSON.stringify({ version: TEXT_ANNOTATION_JSON_VERSION, data: arg.textAnnotationProps }) : undefined,
defaultTextStyle: arg.defaultTextStyleId ? new TextAnnotationUsesTextStyleByDefault(arg.defaultTextStyleId).toJSON() : undefined,
placement: arg.placement,
model: arg.model,
category: arg.category,
code: arg.code ?? Code.createEmpty(),
};
return new this(elementProps, iModelDb);
}
/**
* Updates the geometry of the TextAnnotation2d on insert and validates version.
* @inheritdoc
* @beta
*/
static onInsert(arg) {
super.onInsert(arg);
this.validateVersionAndUpdateGeometry(arg);
}
/**
* Updates the geometry of the TextAnnotation2d on update and validates version.
* @inheritdoc
* @beta
*/
static onUpdate(arg) {
super.onUpdate(arg);
this.validateVersionAndUpdateGeometry(arg);
}
/**
* Populates the `elementGeometryBuilderParams` property in the [TextAnnotation2dProps]($common).
* Only does this if the `elementGeometryBuilderParams` is not already set and if there is actually a text annotation to produce geometry for.
* Also, validates the version of the text annotation data and migrates it if necessary.
* @beta
*/
static validateVersionAndUpdateGeometry(arg) {
const props = arg.props;
const textAnnotationData = parseTextAnnotationData(props.textAnnotationData);
if (!props.elementGeometryBuilderParams && textAnnotationData) {
props.elementGeometryBuilderParams = getElementGeometryBuilderParams(arg.iModel, props.model, props.category, props.placement ?? Placement2d.fromJSON(), textAnnotationData.data, props.defaultTextStyle?.id);
}
}
/**
* TextAnnotation2d custom HandledProps include 'textAnnotationData'.
* @inheritdoc
* @internal
*/
static _customHandledProps = [
{ propertyName: "textAnnotationData", source: "Class" },
];
/**
* TextAnnotation2d deserializes 'textAnnotationData'.
* @inheritdoc
* @beta
*/
static deserialize(props) {
const elProps = super.deserialize(props);
const textAnnotationData = parseTextAnnotationData(props.row.textAnnotationData);
if (textAnnotationData) {
elProps.textAnnotationData = JSON.stringify(textAnnotationData);
}
return elProps;
}
/**
* TextAnnotation2d serializes 'textAnnotationData'.
* @inheritdoc
* @beta
*/
static serialize(props, iModel) {
const inst = super.serialize(props, iModel);
if (props.textAnnotationData !== undefined) {
inst.textAnnotationData = props.textAnnotationData;
}
return inst;
}
/** @internal */
getTextBlocks() {
return getTextBlocks(this);
}
/** @internal */
updateTextBlocks(textBlocks) {
return updateTextBlocks(this, textBlocks);
}
/** @internal */
static onInserted(arg) {
super.onInserted(arg);
ElementDrivesTextAnnotation.updateFieldDependencies(arg.id, arg.iModel);
}
/** @internal */
static onUpdated(arg) {
super.onUpdated(arg);
ElementDrivesTextAnnotation.updateFieldDependencies(arg.id, arg.iModel);
}
collectReferenceIds(referenceIds) {
super.collectReferenceIds(referenceIds);
collectReferenceIds(this, referenceIds);
}
/** @internal */
static async onCloned(context, srcProps, dstProps) {
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;
return remapTextStyle(context, srcElem, dstProps);
}
}
/** An element that displays textual content within a 3d model.
* The text is stored as a [TextAnnotation]($common) from which the element's [geometry]($docs/learning/common/GeometryStream.md) and [Placement]($common) are computed.
* @see [[setAnnotation]] to change the textual content.
* @public @preview
*/
export class TextAnnotation3d extends GraphicalElement3d /* implements ITextAnnotation */ {
/** @internal */
static get className() { return "TextAnnotation3d"; }
/**
* The default [[AnnotationTextStyle]] used by the TextAnnotation3d.
* @beta
*/
defaultTextStyle;
/** The data associated with the text annotation. */
_textAnnotationProps;
/** Extract the textual content, if present.
* @see [[setAnnotation]] to change it.
*/
getAnnotation() {
return this._textAnnotationProps ? TextAnnotation.fromJSON(this._textAnnotationProps) : undefined;
}
/** Change the textual content of the `TextAnnotation3d`.
* @see [[getAnnotation]] to extract the current annotation.
* @param annotation The new annotation
*/
setAnnotation(annotation) {
this._textAnnotationProps = annotation.toJSON();
}
constructor(props, iModel) {
super(props, iModel);
if (props.defaultTextStyle) {
this.defaultTextStyle = new TextAnnotationUsesTextStyleByDefault(props.defaultTextStyle.id);
}
this._textAnnotationProps = parseTextAnnotationData(props.textAnnotationData)?.data;
}
/** Creates a new instance of `TextAnnotation3d` from its JSON representation. */
static fromJSON(props, iModel) {
return new TextAnnotation3d(props, iModel);
}
/**
* Converts the current `TextAnnotation3d` instance to its JSON representation.
* It also computes the `elementGeometryBuilderParams` property used to create the GeometryStream.
* @inheritdoc
*/
toJSON() {
const props = super.toJSON();
props.textAnnotationData = this._textAnnotationProps ? JSON.stringify({ version: TEXT_ANNOTATION_JSON_VERSION, data: this._textAnnotationProps }) : undefined;
if (this._textAnnotationProps) {
props.elementGeometryBuilderParams = getElementGeometryBuilderParams(this.iModel, this.model, this.category, this.placement, this._textAnnotationProps, this.defaultTextStyle ? this.defaultTextStyle.id : undefined);
}
return props;
}
/** Creates a new `TextAnnotation3d` instance with the specified properties.
* @param iModelDb The iModel.
* @param arg The arguments for creating the TextAnnotation3d.
* @beta
*/
static create(iModelDb, arg) {
const elementProps = {
classFullName: this.classFullName,
textAnnotationData: arg.textAnnotationProps ? JSON.stringify({ version: TEXT_ANNOTATION_JSON_VERSION, data: arg.textAnnotationProps }) : undefined,
defaultTextStyle: arg.defaultTextStyleId ? new TextAnnotationUsesTextStyleByDefault(arg.defaultTextStyleId).toJSON() : undefined,
placement: arg.placement,
model: arg.model,
category: arg.category,
code: arg.code ?? Code.createEmpty(),
};
return new this(elementProps, iModelDb);
}
/**
* Updates the geometry of the TextAnnotation3d on insert and validates version..
* @inheritdoc
* @beta
*/
static onInsert(arg) {
super.onInsert(arg);
this.validateVersionAndUpdateGeometry(arg);
}
/**
* Updates the geometry of the TextAnnotation3d on update and validates version..
* @inheritdoc
* @beta
*/
static onUpdate(arg) {
super.onUpdate(arg);
this.validateVersionAndUpdateGeometry(arg);
}
/**
* Populates the `elementGeometryBuilderParams` property in the [TextAnnotation3dProps]($common).
* Only does this if the `elementGeometryBuilderParams` is not already set and if there is actually a text annotation to produce geometry for.
* Also, validates the version of the text annotation data and migrates it if necessary.
* @beta
*/
static validateVersionAndUpdateGeometry(arg) {
const props = arg.props;
const textAnnotationData = parseTextAnnotationData(props.textAnnotationData);
if (!props.elementGeometryBuilderParams && textAnnotationData) {
props.elementGeometryBuilderParams = getElementGeometryBuilderParams(arg.iModel, props.model, props.category, props.placement ?? Placement3d.fromJSON(), textAnnotationData.data, props.defaultTextStyle?.id);
}
}
/**
* TextAnnotation3d custom HandledProps include 'textAnnotationData'.
* @inheritdoc
* @internal
*/
static _customHandledProps = [
{ propertyName: "textAnnotationData", source: "Class" },
];
/**
* TextAnnotation3d deserializes 'textAnnotationData'.
* @inheritdoc
* @beta
*/
static deserialize(props) {
const elProps = super.deserialize(props);
const textAnnotationData = parseTextAnnotationData(props.row.textAnnotationData);
if (textAnnotationData) {
elProps.textAnnotationData = JSON.stringify(textAnnotationData);
}
return elProps;
}
/**
* TextAnnotation3d serializes 'textAnnotationData'.
* @inheritdoc
* @beta
*/
static serialize(props, iModel) {
const inst = super.serialize(props, iModel);
if (props.textAnnotationData !== undefined) {
inst.textAnnotationData = props.textAnnotationData;
}
return inst;
}
/** @internal */
getTextBlocks() {
return getTextBlocks(this);
}
/** @internal */
updateTextBlocks(textBlocks) {
return updateTextBlocks(this, textBlocks);
}
/** @internal */
static onInserted(arg) {
super.onInserted(arg);
ElementDrivesTextAnnotation.updateFieldDependencies(arg.id, arg.iModel);
}
/** @internal */
static onUpdated(arg) {
super.onUpdated(arg);
ElementDrivesTextAnnotation.updateFieldDependencies(arg.id, arg.iModel);
}
collectReferenceIds(referenceIds) {
super.collectReferenceIds(referenceIds);
collectReferenceIds(this, referenceIds);
}
/** @internal */
static async onCloned(context, srcProps, dstProps) {
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;
return remapTextStyle(context, srcElem, dstProps);
}
}
async function remapTextStyle(context, srcElem, dstProps) {
const dstStyleId = await AnnotationTextStyle.remapTextStyleId(srcElem.defaultTextStyle?.id ?? Id64.invalid, context);
dstProps.defaultTextStyle = Id64.isValid(dstStyleId) ? new TextAnnotationUsesTextStyleByDefault(dstStyleId).toJSON() : undefined;
}
function collectReferenceIds(elem, referenceIds) {
const style = elem.defaultTextStyle?.id;
if (style && Id64.isValidId64(style)) {
referenceIds.addElement(style);
}
const block = elem.getAnnotation()?.textBlock;
if (block) {
for (const { child } of traverseTextBlockComponent(block)) {
if (child.type === "field") {
const hostId = child.propertyHost.elementId;
if (Id64.isValidId64(hostId)) {
referenceIds.addElement(hostId);
}
}
}
}
}
function getTextBlocks(elem) {
const annotation = elem.getAnnotation();
return annotation ? [{ textBlock: annotation.textBlock, id: undefined }] : [];
}
function updateTextBlocks(elem, textBlocks) {
assert(textBlocks.length === 1);
assert(textBlocks[0].id === undefined);
const annotation = elem.getAnnotation();
if (!annotation) {
// We must obtain the TextBlockAndId from the element in the first place, so the only way we could end up here is if
// somebody removed the text annotation after we called getTextBlocks. That's gotta be a mistake.
throw new Error("Text annotation element has no text");
}
annotation.textBlock = textBlocks[0].textBlock;
elem.setAnnotation(annotation);
}
/** The version of the JSON stored in `AnnotationTextStyleProps.settings` used by the code.
* Uses the same semantics as [ECVersion]($ecschema-metadata).
* @internal
*/
// 1.0.1 - Added terminatorShapes for leaders
export const TEXT_STYLE_SETTINGS_JSON_VERSION = "1.0.1";
function migrateTextStyleSettings(oldData) {
if (oldData.version === TEXT_STYLE_SETTINGS_JSON_VERSION)
return oldData.data;
// Migrate from 1.0.0 to 1.0.1
if (oldData.data.leader && !oldData.data.leader.terminatorShape) {
oldData.data.leader.terminatorShape = TextStyleSettings.defaultProps.leader.terminatorShape;
}
return oldData.data;
}
/**
* The definition element that holds text style information.
* The style is stored as a [TextStyleSettings]($common).
* @beta
*/
export class AnnotationTextStyle extends DefinitionElement {
/** @internal */
static get className() { return "AnnotationTextStyle"; }
/**
* Optional text describing the `AnnotationTextStyle`.
*/
description;
/**
* The text style settings for the `AnnotationTextStyle`.
* @see [[TextStyleSettings]] for more information.
*/
settings;
constructor(props, iModel) {
super(props, iModel);
this.description = props.description;
const settingsProps = AnnotationTextStyle.parseTextStyleSettings(props.settings);
this.settings = TextStyleSettings.fromJSON(settingsProps?.data);
}
/**
* Creates a Code for an `AnnotationTextStyle` given a name that is meant to be unique within the scope of the specified DefinitionModel.
*
* @param iModel - The IModelDb.
* @param definitionModelId - The ID of the DefinitionModel that contains the AnnotationTextStyle and provides the scope for its name.
* @param name - The AnnotationTextStyle name.
* @beta
*/
static createCode(iModel, definitionModelId, name) {
const codeSpec = iModel.codeSpecs.getByName(BisCodeSpec.annotationTextStyle);
return new Code({ spec: codeSpec.id, scope: definitionModelId, value: name });
}
/**
* Creates a new instance of `AnnotationTextStyle` with the specified properties.
*
* @param iModelDb - The iModelDb.
* @param arg - The arguments for creating the AnnotationTextStyle.
* @beta
*/
static create(iModelDb, arg) {
const props = {
classFullName: this.classFullName,
model: arg.definitionModelId,
code: this.createCode(iModelDb, arg.definitionModelId, arg.name).toJSON(),
description: arg.description,
settings: arg.settings ? JSON.stringify({ version: TEXT_STYLE_SETTINGS_JSON_VERSION, data: arg.settings }) : undefined,
};
return new this(props, iModelDb);
}
/**
* Converts the current `AnnotationTextStyle` instance to its JSON representation.
* @inheritdoc
*/
toJSON() {
const props = super.toJSON();
props.description = this.description;
props.settings = JSON.stringify({ version: TEXT_STYLE_SETTINGS_JSON_VERSION, data: this.settings.toJSON() });
return props;
}
/** Creates a new instance of `AnnotationTextStyle` from its JSON representation. */
static fromJSON(props, iModel) {
return new AnnotationTextStyle(props, iModel);
}
/**
* Validates that the AnnotationTextStyle's settings are valid before insert.
* @inheritdoc
* @beta
*/
static onInsert(arg) {
super.onInsert(arg);
this.validateSettings(arg.props);
}
/**
* Validates that the AnnotationTextStyle's settings are valid before update.
* @inheritdoc
* @beta
*/
static onUpdate(arg) {
super.onUpdate(arg);
this.validateSettings(arg.props);
}
static validateSettings(props) {
const settingProps = AnnotationTextStyle.parseTextStyleSettings(props.settings);
if (!settingProps)
return;
const settings = TextStyleSettings.fromJSON(settingProps.data);
const errors = settings.getValidationErrors();
if (errors.length > 0) {
throw new Error(`Invalid AnnotationTextStyle settings: ${errors.join(", ")}`);
}
}
/**
* AnnotationTextStyle custom HandledProps include 'settings'.
* @inheritdoc
* @beta
*/
static _customHandledProps = [
{ propertyName: "settings", source: "Class" },
];
/**
* AnnotationTextStyle deserializes 'settings'.
* @inheritdoc
* @beta
*/
static deserialize(props) {
const elProps = super.deserialize(props);
const settings = this.parseTextStyleSettings(props.row.settings);
if (settings) {
elProps.settings = JSON.stringify(settings);
}
return elProps;
}
/**
* AnnotationTextStyle serializes 'settings'.
* @inheritdoc
* @beta
*/
static serialize(props, iModel) {
const inst = super.serialize(props, iModel);
if (props.settings !== undefined) {
inst.settings = props.settings;
}
return inst;
}
/** Parses, validates, and potentially migrates the text style settings data from a JSON string. */
static parseTextStyleSettings(json) {
if (!json)
return undefined;
return validateAndMigrateVersionedJSON(json, TEXT_STYLE_SETTINGS_JSON_VERSION, migrateTextStyleSettings);
}
/** When copying an element from one iModel to another, returns the Id of the AnnotationTextStyle in the `context`'s target iModel
* 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.
* @throws Error if an attempt to import the text style failed.
*/
static async remapTextStyleId(sourceTextStyleId, context) {
// 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;
}
// If the style's already been remapped, we're finished.
let dstStyleId = context.findTargetElementId(sourceTextStyleId);
if (Id64.isValid(dstStyleId)) {
return dstStyleId;
}
// Look up the style. It really ought to exist.
const srcStyle = context.sourceDb.elements.tryGetElement(sourceTextStyleId);
if (!srcStyle) {
return Id64.invalid;
}
// If a style with the same code exists in the target iModel, remap to that one.
dstStyleId = context.targetDb.elements.queryElementIdByCode(srcStyle.code);
if (undefined !== dstStyleId) {
return dstStyleId;
}
// Copy the style into the target iModel and remap its Id.
const dstStyleProps = await context.cloneElement(srcStyle);
dstStyleId = context.targetDb.elements.insertElement(dstStyleProps);
context.remapElement(sourceTextStyleId, dstStyleId);
return dstStyleId;
}
static async onCloned(context, srcProps, dstProps) {
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 })));
}
}
//# sourceMappingURL=TextAnnotationElement.js.map