UNPKG

@itwin/core-common

Version:

iTwin.js components common to frontend and backend

179 lines 9.33 kB
/*--------------------------------------------------------------------------------------------- * Copyright (c) Bentley Systems, Incorporated. All rights reserved. * See LICENSE.md in the project root for license terms and full copyright notice. *--------------------------------------------------------------------------------------------*/ /** @packageDocumentation * @module Annotation */ import { Point3d, Transform, YawPitchRollAngles } from "@itwin/core-geometry"; import { TextBlock } from "./TextBlock"; /** * Represents a formatted block of text positioned in 2d or 3d space. * [TextAnnotation2d]($backend) and [TextAnnotation3d]($backend) elements store a single TextAnnotation from which their geometric representation is generated. * Other types of elements may store multiple TextAnnotations, positioned relative to one another. * The annotation's position and orientation relative to the host element's [Placement]($common) is determined as follows: * - First, a bounding box is computed enclosing the contents of the [[textBlock]. * - Then, an "anchor point" is computed based on the bounding box and the [[anchor]] property. The anchor point can be at one of the four corners of the box, in the middle of one of its four * edges, or in the center of the box. * - The [[orientation]] is applied to rotate the box around the anchor point. * - Finally, the [[offset]] is added to the anchor point to apply translation. * @see [appendTextAnnotationGeometry]($backend) to construct the geometry and append it to an [[ElementGeometry.Builder]]. * @beta */ export class TextAnnotation { /** The rotation of the annotation. * @note When defining an annotation for a [TextAnnotation2d]($backend), only the `yaw` component (rotation around the Z axis) is used. */ orientation; /** The formatted document. */ textBlock; /** Describes how to compute the [[textBlock]]'s anchor point. */ anchor; /** An offset applied to the anchor point that can be used to position annotations within the same geometry stream relative to one another. */ offset; /** The leaders of the text annotation. */ leaders; constructor(offset, angles, textBlock, anchor, leaders) { this.offset = offset; this.orientation = angles; this.textBlock = textBlock; this.anchor = anchor; this.leaders = leaders; } /** Creates a new TextAnnotation. */ static create(args) { const offset = args?.offset ?? new Point3d(); const angles = args?.orientation ?? new YawPitchRollAngles(); const textBlock = args?.textBlock ?? TextBlock.create(); const anchor = args?.anchor ?? { vertical: "top", horizontal: "left" }; const leaders = args?.leaders ?? undefined; return new TextAnnotation(offset, angles, textBlock, anchor, leaders); } /** * Creates a new TextAnnotation instance from its JSON representation. */ static fromJSON(props) { return TextAnnotation.create({ offset: props?.offset ? Point3d.fromJSON(props.offset) : undefined, orientation: props?.orientation ? YawPitchRollAngles.fromJSON(props.orientation) : undefined, textBlock: props?.textBlock ? TextBlock.create(props.textBlock) : undefined, anchor: props?.anchor ? { ...props.anchor } : undefined, leaders: props?.leaders ? props.leaders.map((leader) => ({ startPoint: Point3d.fromJSON(leader.startPoint), attachment: leader.attachment, styleOverrides: leader.styleOverrides ?? undefined, intermediatePoints: leader.intermediatePoints ? leader.intermediatePoints.map((point) => Point3d.fromJSON(point)) : undefined, })) : undefined, }); } /** * Converts this annotation to its JSON representation. */ toJSON() { const props = {}; // Even if the text block is empty, we want to record its style ID and overrides, e.g., // so the user can pick up where they left off editing it next time. props.textBlock = this.textBlock.toJSON(); if (!this.offset.isZero) { props.offset = this.offset.toJSON(); } if (!this.orientation.isIdentity()) { props.orientation = this.orientation.toJSON(); } if (this.anchor.vertical !== "top" || this.anchor.horizontal !== "left") { props.anchor = { ...this.anchor }; } props.leaders = this.leaders?.map((leader) => ({ startPoint: leader.startPoint.toJSON(), attachment: leader.attachment, styleOverrides: leader.styleOverrides ?? undefined, intermediatePoints: leader.intermediatePoints ? leader.intermediatePoints.map((point) => point.toJSON()) : undefined, })) ?? undefined; return props; } /** Compute the transform that positions and orients this annotation relative to its anchor point, based on the [[textBlock]]'s computed bounding box. * The anchor point is computed as specified by this annotation's [[anchor]] setting. For example, if the text block is anchored * at the bottom left, then the transform will be relative to the bottom-left corner of `textBlockExtents`. * The text block will be rotated around the fixed anchor point according to [[orientation]], then translated by [[offset]]. * The anchor point will coincide with (0, 0, 0) unless an [[offset]] is present. * If a scale factor is specified, the transform will also scale the annotation by that factor. Usually, this should come from the [[Drawing]] containing the annotation. * @param boundingBox A box fully containing the [[textBlock]]. This range should include the margins. * @param scaleFactor A factor by which to scale the annotation. Default: 1 (no scaling). * @see [[computeAnchorPoint]] to compute the transform's anchor point. * @see [computeLayoutTextBlockResult]($backend) to lay out a `TextBlock`. */ computeTransform(boundingBox, scaleFactor = 1) { const anchorPt = this.computeAnchorPoint(boundingBox); const matrix = this.orientation.toMatrix3d(); const transform = Transform.createIdentity(); const translation = Transform.createTranslation(this.offset.minus(anchorPt)); const scaleTransform = Transform.createScaleAboutPoint(anchorPt, scaleFactor); const rotation = Transform.createFixedPointAndMatrix(anchorPt, matrix); transform.multiplyTransformTransform(translation, transform); transform.multiplyTransformTransform(scaleTransform, transform); transform.multiplyTransformTransform(rotation, transform); return transform; } /** Compute the anchor point of this annotation as specified by [[anchor]]. * @param boundingBox A box fully containing the [[textBlock]]. * @see [[computeTransform]] to compute the transform relative to the anchor point. */ computeAnchorPoint(boundingBox) { let x = boundingBox.low.x; let y = boundingBox.high.y; switch (this.anchor.horizontal) { case "center": x += boundingBox.xLength() / 2; break; case "right": x += boundingBox.xLength(); break; } switch (this.anchor.vertical) { case "middle": y -= boundingBox.yLength() / 2; break; case "bottom": y -= boundingBox.yLength(); break; } return new Point3d(x, y, 0); } /** Returns true if the leaders of this annotation are equal to the leaders of `other`. */ areLeadersEqual(leadersA, leadersB) { if (leadersA === leadersB) return true; if (!leadersA || !leadersB || leadersA.length !== leadersB.length) return false; for (let i = 0; i < leadersA.length; ++i) { const a = leadersA[i]; const b = leadersB[i]; if (!a.startPoint.isAlmostEqual(b.startPoint)) return false; if (JSON.stringify(a.attachment) !== JSON.stringify(b.attachment)) return false; if (JSON.stringify(a.styleOverrides) !== JSON.stringify(b.styleOverrides)) return false; const pointsA = a.intermediatePoints ?? []; const pointsB = b.intermediatePoints ?? []; if (pointsA.length !== pointsB.length) return false; for (let j = 0; j < pointsA.length; ++j) { if (!pointsA[j].isAlmostEqual(pointsB[j])) return false; } } return true; } /** Returns true if this annotation is logically equivalent to `other`. */ equals(other) { if (this.anchor.horizontal !== other.anchor.horizontal || this.anchor.vertical !== other.anchor.vertical || !this.orientation.isAlmostEqual(other.orientation) || !this.offset.isAlmostEqual(other.offset) || !this.textBlock.equals(other.textBlock)) return false; return this.areLeadersEqual(this.leaders, other.leaders); } } //# sourceMappingURL=TextAnnotation.js.map