UNPKG

@itwin/core-backend

Version:
184 lines • 10.9 kB
"use strict"; /*--------------------------------------------------------------------------------------------- * Copyright (c) Bentley Systems, Incorporated. All rights reserved. * See LICENSE.md in the project root for license terms and full copyright notice. *--------------------------------------------------------------------------------------------*/ /** @packageDocumentation * @module ElementGeometry */ Object.defineProperty(exports, "__esModule", { value: true }); exports.appendLeadersToBuilder = appendLeadersToBuilder; exports.computeElbowDirection = computeElbowDirection; exports.computeLeaderAttachmentPoint = computeLeaderAttachmentPoint; const core_common_1 = require("@itwin/core-common"); const core_geometry_1 = require("@itwin/core-geometry"); const FrameGeometry_1 = require("./FrameGeometry"); /** * Constructs and appends leader lines and their terminators to the provided geometry builder for a text annotation. * * This function processes an array of `TextAnnotationLeader` objects, computes their attachment points * relative to a text frame (or a default rectangular frame if none is provided), and appends the leader * line and terminator geometry to the builder. It also applies color overrides if specified * in the leader's style overrides. * * @param builder - The geometry builder to which the leader geometries will be appended. * @param leaders - An array of leader properties. * @param layout - The layout information for the text block, including its range. * @param transform - The transform to apply to the frame and leader geometry. * @param params - The geometry parameters, such as color, to use for the leader lines. * @param frame - (Optional) The style properties for the text frame. If not provided or set to "none", a default rectangle is used. * @returns `true` if at least one leader with a terminator was successfully appended; otherwise, `false`. * @beta */ function appendLeadersToBuilder(builder, leaders, layout, transform, params, textStyleResolver) { let result = true; const scaledLineHeight = textStyleResolver.blockSettings.lineHeight * textStyleResolver.scaleFactor; let frame = textStyleResolver.blockSettings.frame; // If there is no frame, use a rectangular frame to compute the attachmentPoints for leaders. if (frame === undefined || frame.shape === "none") { frame = { shape: "rectangle" }; } if (frame.shape === undefined || frame.shape === "none") return false; const frameCurve = (0, FrameGeometry_1.computeFrame)({ frame: frame.shape, range: layout.range, transform }); for (const leader of leaders) { const leaderStyle = textStyleResolver.resolveTextAnnotationLeaderSettings(leader); let effectiveColor = "subcategory"; if (leaderStyle.leader.color === "inherit") { effectiveColor = leaderStyle.color; } else if (leaderStyle.leader.color !== "subcategory") { effectiveColor = leaderStyle.leader.color; } if (effectiveColor !== "subcategory") { params.lineColor = core_common_1.ColorDef.fromJSON(effectiveColor); result = result && builder.appendGeometryParamsChange(params); } const attachmentPoint = computeLeaderAttachmentPoint(leader, frameCurve, layout, transform); if (!attachmentPoint) return false; // Leader line geometry const leaderLinePoints = []; leaderLinePoints.push(leader.startPoint); leader.intermediatePoints?.forEach((point) => { leaderLinePoints.push(point); }); if (leaderStyle.leader.wantElbow) { const elbowLength = leaderStyle.leader.elbowLength * scaledLineHeight; const elbowDirection = computeElbowDirection(attachmentPoint, frameCurve, elbowLength); if (elbowDirection) leaderLinePoints.push(attachmentPoint.plusScaled(elbowDirection, elbowLength)); } leaderLinePoints.push(attachmentPoint); result = result && builder.appendGeometryQuery(core_geometry_1.LineString3d.create(leaderLinePoints)); // Terminator geometry const terminatorDirection = core_geometry_1.Vector3d.createStartEnd(leaderLinePoints[0], leaderLinePoints[1]).normalize(); const termY = terminatorDirection?.unitCrossProduct(core_geometry_1.Vector3d.unitZ()); if (!termY || !terminatorDirection) continue; // Assuming leaders without terminators is a valid case. const terminatorHeight = leaderStyle.leader.terminatorHeightFactor * scaledLineHeight; const terminatorWidth = leaderStyle.leader.terminatorWidthFactor * scaledLineHeight; const basePoint = leader.startPoint.plusScaled(terminatorDirection, terminatorWidth); const termPointA = basePoint.plusScaled(termY, terminatorHeight); const termPointB = basePoint.plusScaled(termY.negate(), terminatorHeight); result = result && builder.appendGeometryQuery(core_geometry_1.LineString3d.create([termPointA, leader.startPoint, termPointB])); } return result; } /** * Computes the direction vector for an "elbow" for leader based on the attachment point and a frame curve. * The elbow direction is determined by whether the attachment point is closer to the left or right side of the frame. * If the computed elbow would be tangent to the frame at the intersection, no elbow direction is returned. * * @param attachmentPoint - The point where the leader attaches. * @param frameCurve - The frame curve (either a Loop or Path) to which the leader is attached. * @param elbowLength - The length of the elbow segment to be created. * @returns The direction vector for the elbow, or `undefined` if the elbow would be tangent to the frame. * @beta */ function computeElbowDirection(attachmentPoint, frameCurve, elbowLength) { let elbowDirection; // Determine the direction based on the closest point's position relative to the frame const isCloserToLeft = Math.abs(attachmentPoint.x - frameCurve.range().low.x) < Math.abs(attachmentPoint.x - frameCurve.range().high.x); // Decide the direction: left (-X) or right (+X) elbowDirection = isCloserToLeft ? core_geometry_1.Vector3d.unitX().negate() : core_geometry_1.Vector3d.unitX(); // Verify if the elbow is a tangent to the frame, if yes, do not create an elbow const elbowPoint = attachmentPoint.plusScaled(elbowDirection, elbowLength); const elbowLine = core_geometry_1.LineSegment3d.create(attachmentPoint, elbowPoint); // Find intersection points between the elbow and the frame const intersections = core_geometry_1.CurveCurve.intersectionXYZPairs(elbowLine, false, frameCurve, false); // As the elbow will intersect the frame only at one point, we can safely use the first intersection const intersection = intersections[0]; const curveFraction = intersection.detailB.fraction; const derivative = intersection.detailB.curve?.fractionToPointAndDerivative(curveFraction); const tangent = derivative?.direction.normalize(); const lineDirection = core_geometry_1.Vector3d.createStartEnd(elbowLine.point0Ref, elbowLine.point1Ref).normalize(); if (tangent && lineDirection) { const dot = tangent.dotProduct(lineDirection); // If the tangent and line direction are aligned (dot product close to 1 or -1), it's tangent if (Math.abs(dot) > 0.999) { elbowDirection = undefined; } } return elbowDirection; } /** * Computes the attachment point for a leader line on a text annotation frame. * * The attachment point is determined based on the leader's attachment mode: * - `"Nearest"`: Finds the closest point on the frame curve to the leader's start point. * - `"KeyPoint"`: Uses a specific curve segment and fraction along that segment to determine the point. * - `"TextPoint"`: Calculates a point on the text layout (top/bottom, left/right) and projects it onto the frame curve. * * @param leader - The leader props. * @param frameCurve - The curve (Loop or Path) representing the annotation frame. * @param textLayout - The layout information for the text block. * @param transform - The transform applied to the text layout. * @returns The computed attachment point as a `Point3d`, or `undefined` if it cannot be determined. * @beta */ function computeLeaderAttachmentPoint(leader, frameCurve, textLayout, transform) { let attachmentPoint; if (leader.attachment.mode === "Nearest") { attachmentPoint = frameCurve.closestPoint(leader.startPoint)?.point; } else if (leader.attachment.mode === "KeyPoint") { const curves = frameCurve.collectCurvePrimitives(undefined, false, true); const curveIndex = leader.attachment.curveIndex; const fraction = leader.attachment.fraction; if (curveIndex >= curves.length) { // If the curveIndex is invalid, use the last curve // This is a fallback to avoid out-of-bounds access attachmentPoint = curves[curves.length - 1].fractionToPoint(fraction); } else { attachmentPoint = curves[curveIndex].fractionToPoint(fraction); } } else { // attachment.mode="TextPoint" let scaleDirection = transform.matrix.getColumn(0).negate(); // direction to draw a scaled line from text attachment point to find intersection point on frame let lineIndex; if (leader.attachment.position.includes("Top")) { lineIndex = 0; } else { lineIndex = textLayout.lines.length - 1; } const lineRange = textLayout.lines[lineIndex].range; const lineOffset = textLayout.lines[lineIndex].offsetFromDocument; const origin = transform.multiplyPoint3d(core_geometry_1.Point3d.fromJSON(lineOffset)); let attachmentPointOnText = origin.plusScaled(transform.matrix.getColumn(1), ((lineRange.yLength()) / 2)); if (leader.attachment.position.includes("Right")) { attachmentPointOnText = attachmentPointOnText.plusScaled(transform.matrix.getColumn(0), lineRange.xLength()); scaleDirection = scaleDirection.negate(); } // Find the nearest intersection point on the frame to get the correct attachment point // Extend the direction vector to create a target point far along the direction const targetPoint = attachmentPointOnText.plusScaled(scaleDirection, 1e6); // Scale the direction vector to a large value const intersectionLine = core_geometry_1.LineSegment3d.create(attachmentPointOnText, targetPoint); const closestPointDetail = core_geometry_1.CurveCurve.intersectionXYZPairs(intersectionLine, false, frameCurve, false); attachmentPoint = closestPointDetail[0]?.detailA.point; } return attachmentPoint; } //# sourceMappingURL=LeaderGeometry.js.map