@itwin/core-common
Version:
iTwin.js components common to frontend and backend
179 lines • 9.33 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 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