UNPKG

@itwin/core-common

Version:

iTwin.js components common to frontend and backend

911 lines • 98.9 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 Geometry */ import { flatbuffers } from "flatbuffers"; import { Id64 } from "@itwin/core-bentley"; import { Angle, AngleSweep, Arc3d, BentleyGeometryFlatBuffer, FrameBuilder, LineSegment3d, LineString3d, Loop, Matrix3d, Plane3dByOriginAndUnitNormal, Point2d, Point3d, Point3dArray, PointString3d, PolyfaceQuery, Range2d, Range3d, Transform, Vector3d, YawPitchRollAngles } from "@itwin/core-geometry"; import { EGFBAccessors } from "./ElementGeometryFB"; import { Base64EncodedString } from "../Base64EncodedString"; import { TextString } from "./TextString"; import { ColorDef } from "../ColorDef"; import { BackgroundFill, FillDisplay, GeometryClass, GeometryParams } from "../GeometryParams"; import { Gradient } from "../Gradient"; import { ThematicGradientSettings } from "../ThematicDisplay"; import { AreaPattern } from "./AreaPattern"; import { BRepEntity } from "./GeometryStream"; import { ImageGraphic, ImageGraphicCorners } from "./ImageGraphic"; import { LineStyle } from "./LineStyle"; import { Placement2d, Placement3d } from "./Placement"; import { isPlacement2dProps } from "../ElementProps"; /** Specifies the type of an entry in a geometry stream. * @see [[ElementGeometryDataEntry.opcode]]. * @public * @extensions */ export var ElementGeometryOpcode; (function (ElementGeometryOpcode) { /** Local range of the next geometric primitive in the geometry stream. */ ElementGeometryOpcode[ElementGeometryOpcode["SubGraphicRange"] = 2] = "SubGraphicRange"; /** A reference to a [GeometryPart]($backend). */ ElementGeometryOpcode[ElementGeometryOpcode["PartReference"] = 3] = "PartReference"; /** Sets symbology for subsequent geometry to override [SubCategory]($backend) appearance */ ElementGeometryOpcode[ElementGeometryOpcode["BasicSymbology"] = 4] = "BasicSymbology"; /** A line, line string, shape, or point string (automatic simplification of a [CurvePrimitive]($core-geometry) or [CurveCollection]($core-geometry)) */ ElementGeometryOpcode[ElementGeometryOpcode["PointPrimitive"] = 5] = "PointPrimitive"; /** A 2d line, line string, shape, or point string (automatic simplification of a [CurvePrimitive]($core-geometry) or [CurveCollection]($core-geometry)) */ ElementGeometryOpcode[ElementGeometryOpcode["PointPrimitive2d"] = 6] = "PointPrimitive2d"; /** Arc or ellipse (automatic simplification of a [CurvePrimitive]($core-geometry) or [CurveCollection]($core-geometry)) */ ElementGeometryOpcode[ElementGeometryOpcode["ArcPrimitive"] = 7] = "ArcPrimitive"; /** [CurveCollection]($core-geometry) */ // eslint-disable-next-line @typescript-eslint/no-shadow ElementGeometryOpcode[ElementGeometryOpcode["CurveCollection"] = 8] = "CurveCollection"; /** [Polyface]($core-geometry) */ // eslint-disable-next-line @typescript-eslint/no-shadow ElementGeometryOpcode[ElementGeometryOpcode["Polyface"] = 9] = "Polyface"; /** [CurvePrimitive]($core-geometry) */ ElementGeometryOpcode[ElementGeometryOpcode["CurvePrimitive"] = 10] = "CurvePrimitive"; /** [SolidPrimitive]($core-geometry) */ // eslint-disable-next-line @typescript-eslint/no-shadow ElementGeometryOpcode[ElementGeometryOpcode["SolidPrimitive"] = 11] = "SolidPrimitive"; /** [BSplineSurface3d]($core-geometry) */ ElementGeometryOpcode[ElementGeometryOpcode["BsplineSurface"] = 12] = "BsplineSurface"; /** Opaque and [[Gradient]] fills. */ ElementGeometryOpcode[ElementGeometryOpcode["Fill"] = 19] = "Fill"; /** Hatch, cross-hatch, or [[AreaPattern]]. */ ElementGeometryOpcode[ElementGeometryOpcode["Pattern"] = 20] = "Pattern"; /** [[RenderMaterial]] */ ElementGeometryOpcode[ElementGeometryOpcode["Material"] = 21] = "Material"; /** [[TextString]] */ // eslint-disable-next-line @typescript-eslint/no-shadow ElementGeometryOpcode[ElementGeometryOpcode["TextString"] = 22] = "TextString"; /** Specifies line style overrides as a [[LineStyle.Modifier]] */ ElementGeometryOpcode[ElementGeometryOpcode["LineStyleModifiers"] = 23] = "LineStyleModifiers"; /** Boundary representation solid, sheet, or wire body as a [[BRepEntity.DataProps]] */ ElementGeometryOpcode[ElementGeometryOpcode["BRep"] = 25] = "BRep"; /** Small single-tile raster image as an [[ImageGraphic]] */ ElementGeometryOpcode[ElementGeometryOpcode["Image"] = 28] = "Image"; })(ElementGeometryOpcode || (ElementGeometryOpcode = {})); /** Values for [[BRepGeometryCreate.operation]] * @alpha */ export var BRepGeometryOperation; (function (BRepGeometryOperation) { /** Unite target (first entry) with one or more tool entities. */ BRepGeometryOperation[BRepGeometryOperation["Unite"] = 0] = "Unite"; /** Subtract one or more tool entities from target entity (first entry) */ BRepGeometryOperation[BRepGeometryOperation["Subtract"] = 1] = "Subtract"; /** Intersect target (first entry) with one or more tool entities */ BRepGeometryOperation[BRepGeometryOperation["Intersect"] = 2] = "Intersect"; /** Sew the given set of surfaces together by joining those that share edges in common */ BRepGeometryOperation[BRepGeometryOperation["Sew"] = 3] = "Sew"; /** Create a cut in the target (first entry) using a planar region (second entry) and optional depth */ BRepGeometryOperation[BRepGeometryOperation["Cut"] = 4] = "Cut"; /** Create a pad or pocket in the target (first entry) using a planar region (second entry) */ BRepGeometryOperation[BRepGeometryOperation["Emboss"] = 5] = "Emboss"; /** Create a solid from a surface by offsetting using the specified forward and backward distances */ BRepGeometryOperation[BRepGeometryOperation["Thicken"] = 6] = "Thicken"; /** Create a shelled solid by offsetting all faces by the supplied distance */ BRepGeometryOperation[BRepGeometryOperation["Hollow"] = 7] = "Hollow"; /** Create a solid or surface by sweeping a planar profile (first entry) along a path (second entry) */ BRepGeometryOperation[BRepGeometryOperation["Sweep"] = 8] = "Sweep"; /** Create a solid or surface by lofting through a set of paths or regions */ BRepGeometryOperation[BRepGeometryOperation["Loft"] = 9] = "Loft"; /** Create a solid or sheet with all non-smooth/non-laminar edges rounded */ BRepGeometryOperation[BRepGeometryOperation["Round"] = 10] = "Round"; /** Offset all faces of a solid or sheet target by the supplied distance. */ BRepGeometryOperation[BRepGeometryOperation["Offset"] = 11] = "Offset"; })(BRepGeometryOperation || (BRepGeometryOperation = {})); /** Provides utility functions for working with [[ElementGeometryDataEntry]]. * @beta */ export var ElementGeometry; (function (ElementGeometry) { /** [[ElementGeometry.Builder]] is a helper class for populating a [[ElementGeometryDataEntry]] array needed to create a [[GeometricElement]] or [[GeometryPart]]. */ class Builder { _localToWorld; _worldToLocal; /** GeometryStream entries */ entries = []; /** Current placement transform, converts local coordinate (placement relative) input to world */ get localToWorld() { return this._localToWorld; } /** Current inverse placement transform, converts world coordinate input to local (placement relative) */ get worldToLocal() { return this._worldToLocal; } /** Supply optional local to world transform. Used to transform world coordinate input relative to element placement. * For a [[GeometricElement]]'s placement to be meaningful, world coordinate geometry should never be appended to an element with an identity placement. * Can be called with undefined or identity transform to start appending geometry supplied in local coordinates again. */ setLocalToWorld(localToWorld) { this._localToWorld = (undefined === localToWorld || localToWorld.isIdentity ? undefined : localToWorld.clone()); this._worldToLocal = (undefined === this._localToWorld ? undefined : this._localToWorld.inverse()); } /** Supply local to world transform from a Point3d and optional YawPitchRollAngles. * @see [[Placement3d]] */ setLocalToWorld3d(origin, angles = YawPitchRollAngles.createDegrees(0.0, 0.0, 0.0)) { this.setLocalToWorld(Transform.createOriginAndMatrix(origin, angles.toMatrix3d())); } /** Supply local to world transform from a Point2d and optional Angle. * @see [[Placement2d]] */ setLocalToWorld2d(origin, angle = Angle.createDegrees(0.0)) { this.setLocalToWorld(Transform.createOriginAndMatrix(Point3d.createFrom(origin), Matrix3d.createRotationAroundVector(Vector3d.unitZ(), angle))); } /** Supply local to world transform from a PlacementProps2d or PlacementProps3d. * @see [[PlacementProps]] */ setLocalToWorldFromPlacement(props) { const placement = isPlacement2dProps(props) ? Placement2d.fromJSON(props) : Placement3d.fromJSON(props); this.setLocalToWorld(placement.transform); } /** Compute angles suitable for passing to [[setLocalToWorld3d]] from an array of 3d points. */ static placementAnglesFromPoints(pts, defaultUp, result) { const angles = result ? result : new YawPitchRollAngles(); const zVec = defaultUp ? defaultUp.clone() : Vector3d.unitZ(); const matrix = Matrix3d.createRigidHeadsUp(zVec); YawPitchRollAngles.createFromMatrix3d(matrix, angles); if (pts.length < 2 || pts[0].isAlmostEqual(pts[1])) return angles; // Check if points have a well defined normal to use instead of defaultUp... const frameTransform = FrameBuilder.createFrameToDistantPoints(pts); if (undefined !== frameTransform) { const plane = Plane3dByOriginAndUnitNormal.create(pts[0], frameTransform.matrix.getColumn(2)); if (undefined !== plane && Point3dArray.isCloseToPlane(pts, plane)) zVec.setFrom(plane.getNormalRef()); } const xVec = Vector3d.createStartEnd(pts[0], pts[1]); if (xVec.isParallelTo(zVec, true)) return angles; const yVec = xVec.unitCrossProduct(zVec); if (undefined === yVec) return angles; Matrix3d.createColumns(xVec, yVec, zVec, matrix); if (undefined === Matrix3d.createRigidFromMatrix3d(matrix, undefined, matrix)) return angles; YawPitchRollAngles.createFromMatrix3d(matrix, angles); return angles; } /** Compute angle suitable for passing to [[setLocalToWorld2d]] from an array of xy plane points. */ static placementAngleFromPoints(pts, result) { const angles = ElementGeometry.Builder.placementAnglesFromPoints(pts); if (undefined === result) return angles.yaw; result.setFrom(angles.yaw); return result; } /** Store local ranges for all subsequent geometry appended. Can improve performance of range testing for elements with a GeometryStream * containing more than one [[GeometryQuery]] differentiable by range. Not useful for a single [[GeometryQuery]] as its range and that of the [[GeometricElement]] are the same. * Ignored when defining a [[GeometryPart]] and not needed when only appending [[GeometryPart]] instances to a [[GeometricElement]] as these store their own range. */ appendGeometryRanges() { const entry = fromSubGraphicRange(Range3d.create()); // Computed on backend, just need opcode... if (undefined === entry) return false; this.entries.push(entry); return true; } /** Change [[GeometryParams]] for subsequent geometry. * It is not valid to change the sub-category when defining a [[GeometryPart]]. A [[GeometryPart]] inherits the symbology of their instance for anything not explicitly overridden. */ appendGeometryParamsChange(geomParams) { return appendGeometryParams(geomParams, this.entries, this._worldToLocal); } /** Append a [[GeometryQuery]] supplied in either local or world coordinates to the [[ElementGeometryDataEntry]] array */ appendGeometryQuery(geometry) { const entry = ElementGeometry.fromGeometryQuery(geometry, this._worldToLocal); if (undefined === entry) return false; this.entries.push(entry); return true; } /** Append a [[TextString]] supplied in either local or world coordinates to the [[ElementGeometryDataEntry]] array */ appendTextString(text) { const entry = ElementGeometry.fromTextString(text.toJSON(), this._worldToLocal); if (undefined === entry) return false; this.entries.push(entry); return true; } /** Append a series of entries representing a [[TextBlock]] to the [[ElementGeometryDataEntry]] array. * @beta */ appendTextBlock(block, geomParams) { for (const entry of block.entries) { let result; if (entry.text) { result = this.appendTextString(new TextString(entry.text)); } else if (entry.color) { const params = geomParams?.clone() ?? new GeometryParams(Id64.invalid); if (entry.color !== "subcategory") { params.lineColor = ColorDef.fromJSON(entry.color); } result = this.appendGeometryParamsChange(params); } else { result = this.appendGeometryQuery(LineSegment3d.fromJSON(entry.separator)); } if (!result) { return false; } } return true; } /** Append a [[ImageGraphic]] supplied in either local or world coordinates to the [[ElementGeometryDataEntry]] array */ appendImageGraphic(image) { const entry = ElementGeometry.fromImageGraphic(image.toJSON(), this._worldToLocal); if (undefined === entry) return false; this.entries.push(entry); return true; } /** Append a [[BRepEntity.DataProps]] supplied in either local or world coordinates to the [[ElementGeometryDataEntry]] array. * Provided for compatibility with GeometryStreamBuilder only. * Backend code should use IModelDb.createBRepGeometry to create a brep [[ElementGeometryDataEntry]] directly. */ appendBRepData(brep) { const entry = ElementGeometry.fromBRep(brep, this._worldToLocal); if (undefined === entry) return false; this.entries.push(entry); return true; } /** Append a [[GeometryPart]] instance with relative transform to the [[ElementGeometryDataEntry]] array for creating a [[GeometricElement]]. * Not valid when defining a [[GeometryPart]] as nesting of parts is not supported. */ appendGeometryPart(partId, partTransform) { const entry = ElementGeometry.fromGeometryPart(partId, partTransform, this._worldToLocal); if (undefined === entry) return false; this.entries.push(entry); return true; } /** Append a [[GeometryPart]] instance with relative position, orientation, and scale to the [[ElementGeometryDataEntry]] array for creating a [[GeometricElement3d]]. * Not valid when defining a [[GeometryPart]] as nesting of parts is not supported. */ appendGeometryPart3d(partId, instanceOrigin, instanceRotation, instanceScale) { const partTransform = Transform.createOriginAndMatrix(instanceOrigin, instanceRotation ? instanceRotation.toMatrix3d() : Matrix3d.createIdentity()); if (undefined !== instanceScale) partTransform.matrix.scaleColumnsInPlace(instanceScale, instanceScale, instanceScale); return this.appendGeometryPart(partId, partTransform); } /** Append a [[GeometryPart]] instance with relative position, orientation, and scale to the [[ElementGeometryDataEntry]] array for creating a [[GeometricElement2d]]. * Not valid when defining a [[GeometryPart]] as nesting of parts is not supported. */ appendGeometryPart2d(partId, instanceOrigin, instanceRotation, instanceScale) { return this.appendGeometryPart3d(partId, instanceOrigin ? Point3d.createFrom(instanceOrigin) : undefined, instanceRotation ? new YawPitchRollAngles(instanceRotation) : undefined, instanceScale); } } ElementGeometry.Builder = Builder; class IteratorEntry { geomParams; localToWorld; localRange; _value; _applyLocalToWorld; constructor(geomParams, localToWorld, applyLocalToWorld) { this.geomParams = geomParams; this.localToWorld = localToWorld; this._applyLocalToWorld = applyLocalToWorld ? !localToWorld.isIdentity : false; } get value() { return this._value; } set value(value) { this._value = value; } get outputTransform() { return this._applyLocalToWorld ? this.localToWorld : undefined; } /** Return the [[GeometryQuery]] representation for the current entry */ toGeometryQuery() { return toGeometryQuery(this.value, this.outputTransform); } /** Return the [[BRepEntity.DataProps]] representation for the current entry for checking brep type and face attachments. */ toBRepData(wantBRepData = false) { return toBRep(this.value, wantBRepData, this.outputTransform); } /** Return the [[TextString]] representation for the current entry */ toTextString() { const props = toTextString(this.value, this.outputTransform); return (undefined !== props ? new TextString(props) : undefined); } /** Return the [[ImageGraphic]] representation for the current entry */ toImageGraphic() { const props = toImageGraphic(this.value, this.outputTransform); return (undefined !== props ? ImageGraphic.fromJSON(props) : undefined); } /** Return the GeometryPart information for the current entry */ toGeometryPart(partToLocal, partToWorld) { if (undefined === partToLocal && undefined !== partToWorld) partToLocal = Transform.createIdentity(); const partId = toGeometryPart(this.value, partToLocal); if (undefined === partId || undefined === partToLocal || undefined === partToWorld) return partId; if (undefined !== this.localToWorld) this.localToWorld.multiplyTransformTransform(partToLocal, partToWorld); return partId; } } ElementGeometry.IteratorEntry = IteratorEntry; /** [[ElementGeometry.Iterator]] is a helper class for iterating a [[ElementGeometryDataEntry]] array. * Each [[ElementGeometryDataEntry]] returned by the iterator represents exactly one displayable entry. */ class Iterator { /** GeometryStream entries */ entryArray; /** The geometric element's placement or geometry part's local range (placement.bbox) */ placement; /** If true, geometry displays oriented to face the camera */ viewIndependent; /** If true, geometry stream contained breps that were omitted or replaced as requested */ brepsPresent; /** Current entry position */ _index = 0; /** Allocated on first call to next() and reused thereafter */ _entry; /** Used to initialize this._entry */ _appearance; _localToWorld; /** Whether deserialized entry data is returned in world or local coordinates */ _applyLocalToWorld = false; /** Construct a new Iterator given a [[ElementGeometryInfo]] from either a [[GeometricElement3d]], [[GeometricElement2d]], or [[GeometryPart]]. * Supply the optional [[GeometryParams]] and localToWorld transform to iterate a [[GeometryPart]] in the context of a [[GeometricElement]] reference. */ constructor(info, categoryOrGeometryParams, localToWorld) { this.entryArray = info.entryArray; this.viewIndependent = info.viewIndependent; this.brepsPresent = info.brepsPresent; if (undefined !== info.categoryId) categoryOrGeometryParams = info.categoryId; if (undefined !== categoryOrGeometryParams) this._appearance = typeof categoryOrGeometryParams === "string" ? new GeometryParams(categoryOrGeometryParams) : categoryOrGeometryParams; else this._appearance = new GeometryParams(Id64.invalid); if (undefined !== info.sourceToWorld) localToWorld = ElementGeometry.toTransform(info.sourceToWorld); if (undefined !== localToWorld) this._localToWorld = localToWorld; else this._localToWorld = Transform.createIdentity(); const orgAng = YawPitchRollAngles.tryFromTransform(this._localToWorld); if (undefined === orgAng.angles) orgAng.angles = YawPitchRollAngles.createDegrees(0, 0, 0); let bbox = (undefined !== info.bbox ? ElementGeometry.toElementAlignedBox3d(info.bbox) : undefined); if (undefined === bbox) bbox = Range3d.createNull(); this.placement = new Placement3d(orgAng.origin, orgAng.angles, bbox); } /** Call to return deserialized entry data in world coordinates */ requestWorldCoordinates() { this._applyLocalToWorld = !this._localToWorld.isIdentity; } // eslint-disable-next-line @typescript-eslint/naming-convention get entry() { if (undefined === this._entry) this._entry = new IteratorEntry(this._appearance, this._localToWorld, this._applyLocalToWorld); return this._entry; } /** Advance to next displayable opcode (geometric entry or geometry part) while updating the current [[GeometryParams]] from appearance related opcodes. */ next() { while (this._index < this.entryArray.length) { const value = this.entryArray[this._index++]; if (ElementGeometry.isAppearanceEntry(value)) { const localToWorld = (this._applyLocalToWorld ? this._localToWorld : undefined); ElementGeometry.updateGeometryParams(value, this.entry.geomParams, localToWorld); } else if (ElementGeometryOpcode.SubGraphicRange === value.opcode) { // NOTE: localRange remains valid until the next sub-range entry is encountered... this.entry.localRange = ElementGeometry.toSubGraphicRange(value); } else if (ElementGeometryOpcode.PartReference === value.opcode) { this.entry.value = value; return { value: this.entry, done: false }; } else if (ElementGeometry.isGeometricEntry(value)) { this.entry.value = value; return { value: this.entry, done: false }; } } return { value: this.entry, done: true }; } [Symbol.iterator]() { return this; } } ElementGeometry.Iterator = Iterator; /** Return whether the supplied entry can be represented as a [[GeometryQuery]] */ function isGeometryQueryEntry(entry) { switch (entry.opcode) { case ElementGeometryOpcode.PointPrimitive: case ElementGeometryOpcode.PointPrimitive2d: case ElementGeometryOpcode.ArcPrimitive: case ElementGeometryOpcode.CurveCollection: case ElementGeometryOpcode.Polyface: case ElementGeometryOpcode.CurvePrimitive: case ElementGeometryOpcode.SolidPrimitive: case ElementGeometryOpcode.BsplineSurface: return true; default: return false; } } ElementGeometry.isGeometryQueryEntry = isGeometryQueryEntry; /** Return whether the supplied entry is displayable geometry [[GeometryQuery]], [[BRepEntity.DataProps]], [[TextString]], or [[ImageGraphic]] */ function isGeometricEntry(entry) { switch (entry.opcode) { case ElementGeometryOpcode.BRep: case ElementGeometryOpcode.TextString: case ElementGeometryOpcode.Image: return true; default: return isGeometryQueryEntry(entry); } } ElementGeometry.isGeometricEntry = isGeometricEntry; /** Return whether the supplied entry is geometric or a part reference */ function isDisplayableEntry(entry) { switch (entry.opcode) { case ElementGeometryOpcode.PartReference: return true; default: return isGeometricEntry(entry); } } ElementGeometry.isDisplayableEntry = isDisplayableEntry; /** Return whether the supplied entry represents appearance information */ function isAppearanceEntry(entry) { switch (entry.opcode) { case ElementGeometryOpcode.BasicSymbology: case ElementGeometryOpcode.Fill: case ElementGeometryOpcode.Pattern: case ElementGeometryOpcode.Material: case ElementGeometryOpcode.LineStyleModifiers: return true; default: return false; } } ElementGeometry.isAppearanceEntry = isAppearanceEntry; /** Return whether the supplied entry represents a single open curve or path */ function isCurve(entry) { switch (entry.opcode) { case ElementGeometryOpcode.PointPrimitive: { const buffer = new flatbuffers.ByteBuffer(entry.data); const ppfb = EGFBAccessors.PointPrimitive.getRootAsPointPrimitive(buffer); return (EGFBAccessors.BoundaryType.Open === ppfb.boundary()); } case ElementGeometryOpcode.PointPrimitive2d: { const buffer = new flatbuffers.ByteBuffer(entry.data); const ppfb = EGFBAccessors.PointPrimitive2d.getRootAsPointPrimitive2d(buffer); return (EGFBAccessors.BoundaryType.Open === ppfb.boundary()); } case ElementGeometryOpcode.ArcPrimitive: { const buffer = new flatbuffers.ByteBuffer(entry.data); const ppfb = EGFBAccessors.ArcPrimitive.getRootAsArcPrimitive(buffer); return (EGFBAccessors.BoundaryType.Open === ppfb.boundary()); } case ElementGeometryOpcode.CurvePrimitive: { // should never be a point string or closed bcurve... return true; } case ElementGeometryOpcode.CurveCollection: { const geom = BentleyGeometryFlatBuffer.bytesToGeometry(entry.data, true); if (undefined === geom || Array.isArray(geom)) return false; return ("curveCollection" === geom.geometryCategory && !geom.isAnyRegionType); } default: return false; } } ElementGeometry.isCurve = isCurve; /** Return whether the supplied entry represents a loop, planar region, open polyface, or sheet body */ function isSurface(entry) { switch (entry.opcode) { case ElementGeometryOpcode.PointPrimitive: { const buffer = new flatbuffers.ByteBuffer(entry.data); const ppfb = EGFBAccessors.PointPrimitive.getRootAsPointPrimitive(buffer); return (EGFBAccessors.BoundaryType.Closed === ppfb.boundary()); } case ElementGeometryOpcode.PointPrimitive2d: { const buffer = new flatbuffers.ByteBuffer(entry.data); const ppfb = EGFBAccessors.PointPrimitive2d.getRootAsPointPrimitive2d(buffer); return (EGFBAccessors.BoundaryType.Closed === ppfb.boundary()); } case ElementGeometryOpcode.ArcPrimitive: { const buffer = new flatbuffers.ByteBuffer(entry.data); const ppfb = EGFBAccessors.ArcPrimitive.getRootAsArcPrimitive(buffer); return (EGFBAccessors.BoundaryType.Closed === ppfb.boundary()); } case ElementGeometryOpcode.CurvePrimitive: { // should never be a closed bcurve... return false; } case ElementGeometryOpcode.CurveCollection: { const geom = BentleyGeometryFlatBuffer.bytesToGeometry(entry.data, true); if (undefined === geom || Array.isArray(geom)) return false; return ("curveCollection" === geom.geometryCategory && geom.isAnyRegionType); } case ElementGeometryOpcode.SolidPrimitive: { const geom = BentleyGeometryFlatBuffer.bytesToGeometry(entry.data, true); if (undefined === geom || Array.isArray(geom)) return false; return ("solid" === geom.geometryCategory && !geom.isClosedVolume); } case ElementGeometryOpcode.Polyface: { const geom = BentleyGeometryFlatBuffer.bytesToGeometry(entry.data, true); if (undefined === geom || Array.isArray(geom)) return false; if ("polyface" !== geom.geometryCategory) return false; const polyface = geom; switch (polyface.expectedClosure) { case 0: return !PolyfaceQuery.isPolyfaceClosedByEdgePairing(polyface); case 1: return true; case 2: default: return false; } } case ElementGeometryOpcode.BsplineSurface: { // never treated as a solid even if closed/periodic in u/v... return true; } case ElementGeometryOpcode.BRep: { const buffer = new flatbuffers.ByteBuffer(entry.data); const ppfb = EGFBAccessors.BRepData.getRootAsBRepData(buffer); return (EGFBAccessors.BRepType.Sheet === ppfb.brepType()); } default: return false; } } ElementGeometry.isSurface = isSurface; /** Return whether the supplied entry represents a capped solid, closed polyface, or solid body */ function isSolid(entry) { switch (entry.opcode) { case ElementGeometryOpcode.SolidPrimitive: { const geom = BentleyGeometryFlatBuffer.bytesToGeometry(entry.data, true); if (undefined === geom || Array.isArray(geom)) return false; return ("solid" === geom.geometryCategory && geom.isClosedVolume); } case ElementGeometryOpcode.Polyface: { const geom = BentleyGeometryFlatBuffer.bytesToGeometry(entry.data, true); if (undefined === geom || Array.isArray(geom)) return false; if ("polyface" !== geom.geometryCategory) return false; const polyface = geom; switch (polyface.expectedClosure) { case 0: return PolyfaceQuery.isPolyfaceClosedByEdgePairing(polyface); case 2: return true; case 1: default: return false; } } case ElementGeometryOpcode.BRep: { const buffer = new flatbuffers.ByteBuffer(entry.data); const ppfb = EGFBAccessors.BRepData.getRootAsBRepData(buffer); return (EGFBAccessors.BRepType.Solid === ppfb.brepType()); } default: return false; } } ElementGeometry.isSolid = isSolid; /** Return the body type that would be used to represent the supplied entry */ function getBRepEntityType(entry) { switch (entry.opcode) { case ElementGeometryOpcode.PointPrimitive: { const buffer = new flatbuffers.ByteBuffer(entry.data); const ppfb = EGFBAccessors.PointPrimitive.getRootAsPointPrimitive(buffer); if (EGFBAccessors.BoundaryType.None === ppfb.boundary()) return undefined; return (EGFBAccessors.BoundaryType.Closed === ppfb.boundary() ? BRepEntity.Type.Sheet : BRepEntity.Type.Wire); } case ElementGeometryOpcode.PointPrimitive2d: { const buffer = new flatbuffers.ByteBuffer(entry.data); const ppfb = EGFBAccessors.PointPrimitive2d.getRootAsPointPrimitive2d(buffer); if (EGFBAccessors.BoundaryType.None === ppfb.boundary()) return undefined; return (EGFBAccessors.BoundaryType.Closed === ppfb.boundary() ? BRepEntity.Type.Sheet : BRepEntity.Type.Wire); } case ElementGeometryOpcode.ArcPrimitive: { const buffer = new flatbuffers.ByteBuffer(entry.data); const ppfb = EGFBAccessors.ArcPrimitive.getRootAsArcPrimitive(buffer); return (EGFBAccessors.BoundaryType.Closed === ppfb.boundary() ? BRepEntity.Type.Sheet : BRepEntity.Type.Wire); } case ElementGeometryOpcode.CurvePrimitive: { // should never be a point string or closed bcurve... return BRepEntity.Type.Wire; } case ElementGeometryOpcode.CurveCollection: { const geom = BentleyGeometryFlatBuffer.bytesToGeometry(entry.data, true); if (undefined === geom || Array.isArray(geom)) return undefined; if ("curveCollection" !== geom.geometryCategory) return undefined; const curves = geom; return (curves.isAnyRegionType ? BRepEntity.Type.Sheet : BRepEntity.Type.Wire); } case ElementGeometryOpcode.SolidPrimitive: { const geom = BentleyGeometryFlatBuffer.bytesToGeometry(entry.data, true); if (undefined === geom || Array.isArray(geom)) return undefined; if ("solid" !== geom.geometryCategory) return undefined; const solid = geom; return (solid.isClosedVolume ? BRepEntity.Type.Solid : BRepEntity.Type.Sheet); } case ElementGeometryOpcode.BsplineSurface: { // always a surface... return BRepEntity.Type.Sheet; } case ElementGeometryOpcode.Polyface: { const geom = BentleyGeometryFlatBuffer.bytesToGeometry(entry.data, true); if (undefined === geom || Array.isArray(geom)) return undefined; if ("polyface" !== geom.geometryCategory) return undefined; const polyface = geom; switch (polyface.expectedClosure) { case 0: return PolyfaceQuery.isPolyfaceClosedByEdgePairing(polyface) ? BRepEntity.Type.Solid : BRepEntity.Type.Sheet; case 1: return BRepEntity.Type.Sheet; case 2: return BRepEntity.Type.Solid; default: return undefined; } } case ElementGeometryOpcode.BRep: { const buffer = new flatbuffers.ByteBuffer(entry.data); const ppfb = EGFBAccessors.BRepData.getRootAsBRepData(buffer); switch (ppfb.brepType()) { case EGFBAccessors.BRepType.Wire: return BRepEntity.Type.Wire; // always be persisted as a curve type... case EGFBAccessors.BRepType.Sheet: return BRepEntity.Type.Sheet; case EGFBAccessors.BRepType.Solid: return BRepEntity.Type.Solid; default: return undefined; } } default: return undefined; } } ElementGeometry.getBRepEntityType = getBRepEntityType; /** Return entry as a [[GeometryQuery]] */ function toGeometryQuery(entry, localToWorld) { if (!isGeometryQueryEntry(entry)) return undefined; switch (entry.opcode) { case ElementGeometryOpcode.PointPrimitive: { const buffer = new flatbuffers.ByteBuffer(entry.data); const ppfb = EGFBAccessors.PointPrimitive.getRootAsPointPrimitive(buffer); const pts = []; for (let i = 0; i < ppfb.coordsLength(); i++) pts.push(Point3d.create(ppfb.coords(i).x(), ppfb.coords(i).y(), ppfb.coords(i).z())); if (0 === pts.length) return undefined; if (undefined !== localToWorld) localToWorld.multiplyPoint3dArrayInPlace(pts); switch (ppfb.boundary()) { case EGFBAccessors.BoundaryType.Open: return LineString3d.createPoints(pts); case EGFBAccessors.BoundaryType.Closed: return Loop.createPolygon(pts); default: return PointString3d.createPoints(pts); } } case ElementGeometryOpcode.PointPrimitive2d: { const buffer = new flatbuffers.ByteBuffer(entry.data); const ppfb = EGFBAccessors.PointPrimitive2d.getRootAsPointPrimitive2d(buffer); const pts = []; for (let i = 0; i < ppfb.coordsLength(); i++) pts.push(Point3d.create(ppfb.coords(i).x(), ppfb.coords(i).y())); if (0 === pts.length) return undefined; if (undefined !== localToWorld) localToWorld.multiplyPoint3dArrayInPlace(pts); switch (ppfb.boundary()) { case EGFBAccessors.BoundaryType.Open: return LineString3d.createPoints(pts); case EGFBAccessors.BoundaryType.Closed: return Loop.createPolygon(pts); default: return PointString3d.createPoints(pts); } } case ElementGeometryOpcode.ArcPrimitive: { const buffer = new flatbuffers.ByteBuffer(entry.data); const ppfb = EGFBAccessors.ArcPrimitive.getRootAsArcPrimitive(buffer); const center = Point3d.create(ppfb.center().x(), ppfb.center().y(), ppfb.center().z()); const vector0 = Vector3d.create(ppfb.vector0().x(), ppfb.vector0().y(), ppfb.vector0().z()); const vector90 = Vector3d.create(ppfb.vector90().x(), ppfb.vector90().y(), ppfb.vector90().z()); const arc = Arc3d.create(center, vector0, vector90, AngleSweep.createStartSweepRadians(ppfb.start(), ppfb.sweep())); if (undefined !== localToWorld && !arc.tryTransformInPlace(localToWorld)) return undefined; return (EGFBAccessors.BoundaryType.Closed === ppfb.boundary() ? Loop.create(arc) : arc); } case ElementGeometryOpcode.CurvePrimitive: case ElementGeometryOpcode.CurveCollection: case ElementGeometryOpcode.SolidPrimitive: case ElementGeometryOpcode.BsplineSurface: case ElementGeometryOpcode.Polyface: const geom = BentleyGeometryFlatBuffer.bytesToGeometry(entry.data, true); if (undefined === geom || Array.isArray(geom)) return undefined; // Should always be a single entry not an array... if (undefined !== localToWorld && !geom.tryTransformInPlace(localToWorld)) return undefined; return geom; default: return undefined; // Not a GeometryQuery, need to be handled explicitly... } } ElementGeometry.toGeometryQuery = toGeometryQuery; /** Create entry from a [[GeometryQuery]] */ function fromGeometryQuery(geom, worldToLocal) { let opcode; switch (geom.geometryCategory) { case "bsurf": opcode = ElementGeometryOpcode.BsplineSurface; break; case "curveCollection": opcode = ElementGeometryOpcode.CurveCollection; break; case "curvePrimitive": case "pointCollection": opcode = ElementGeometryOpcode.CurvePrimitive; break; case "polyface": opcode = ElementGeometryOpcode.Polyface; break; case "solid": opcode = ElementGeometryOpcode.SolidPrimitive; break; default: return undefined; } if (undefined !== worldToLocal) { const localGeom = geom.cloneTransformed(worldToLocal); if (undefined === localGeom) return undefined; geom = localGeom; } const data = BentleyGeometryFlatBuffer.geometryToBytes(geom, true); if (undefined === data) return undefined; return { opcode, data }; } ElementGeometry.fromGeometryQuery = fromGeometryQuery; /** Return entry as a [[TextString]] */ function toTextString(entry, localToWorld) { if (ElementGeometryOpcode.TextString !== entry.opcode) return undefined; const buffer = new flatbuffers.ByteBuffer(entry.data); const ppfb = EGFBAccessors.TextString.getRootAsTextString(buffer); const style = ppfb.style(); if (null === style) return undefined; const text = ppfb.text(); const props = { text: (null !== text ? text : ""), font: style.fontId(), height: style.height() }; props.widthFactor = style.widthFactor(); props.bold = style.isBold(); props.italic = style.isItalic(); props.underline = style.isUnderlined(); const transform = ppfb.transform(); if (null !== transform) { props.origin = Point3d.create(transform.form3d03(), transform.form3d13(), transform.form3d23()); props.rotation = YawPitchRollAngles.createFromMatrix3d(Matrix3d.createRowValues(transform.form3d00(), transform.form3d01(), transform.form3d02(), transform.form3d10(), transform.form3d11(), transform.form3d12(), transform.form3d20(), transform.form3d21(), transform.form3d22())); } if (undefined === localToWorld) return props; const textString = new TextString(props); if (!textString.transformInPlace(localToWorld)) return undefined; return textString.toJSON(); } ElementGeometry.toTextString = toTextString; /** Returns only the [[TextStringGlyphData]] embedded in the [[TextString]] flatbuffer. This data is only internal to the native display libaries. */ function toTextStringGlyphData(entry) { if (ElementGeometryOpcode.TextString !== entry.opcode) return undefined; const buffer = new flatbuffers.ByteBuffer(entry.data); const ppfb = EGFBAccessors.TextString.getRootAsTextString(buffer); const glyphOriginToPoint2d = (origin) => { if (!origin) throw new Error("Value cannot be null."); return new Point2d(origin.x(), origin.y()); }; const textStringRangeToRange2d = (r) => r ? new Range2d(r.lowx(), r.lowy(), r.highx(), r.highy()) : null; const range = textStringRangeToRange2d(ppfb.range()); const glyphIds = Array.from({ length: ppfb.glyphIdsLength() }, (_, i) => ppfb.glyphIds(i) ?? 0); const glyphOrigins = Array.from({ length: ppfb.glyphOriginsLength() }, (_, i) => glyphOriginToPoint2d(ppfb.glyphOrigins(i))); if (!range || range.isNull || glyphIds.length === 0 || glyphOrigins.length === 0) return undefined; return { glyphIds, glyphOrigins, range }; } ElementGeometry.toTextStringGlyphData = toTextStringGlyphData; /** Create entry from a [[TextString]] */ function fromTextString(text, worldToLocal, glyphs) { if (undefined !== worldToLocal) { const localText = new TextString(text); if (!localText.transformInPlace(worldToLocal)) return undefined; text = localText.toJSON(); } const fbb = new flatbuffers.Builder(); const builder = EGFBAccessors.TextString; const textOffset = fbb.createString(text.text); const styleOffset = EGFBAccessors.TextStringStyle.createTextStringStyle(fbb, 1, 0, text.font, undefined === text.bold ? false : text.bold, undefined === text.italic ? false : text.italic, undefined === text.underline ? false : text.underline, text.height, undefined === text.widthFactor ? 1.0 : text.widthFactor); const fbbGlyphs = (() => { if (!glyphs || glyphs.range.isNull) return undefined; const glyphIdsOffset = builder.createGlyphIdsVector(fbb, glyphs.glyphIds); builder.startGlyphOriginsVector(fbb, glyphs.glyphOrigins.length); for (const origin of [...glyphs.glyphOrigins].reverse()) { EGFBAccessors.TextStringGlyphOrigin.createTextStringGlyphOrigin(fbb, origin.x, origin.y); } const glyphOriginsOffset = fbb.endVector(); return { glyphIdsOffset, glyphOriginsOffset, range: glyphs.range }; })(); builder.startTextString(fbb); builder.addMajorVersion(fbb, 1); builder.addMinorVersion(fbb, 0); builder.addText(fbb, textOffset); builder.addStyle(fbb, styleOffset); if (undefined !== text.origin || undefined !== text.rotation) { const origin = Point3d.fromJSON(text.origin); const angles = YawPitchRollAngles.fromJSON(text.rotation); const matrix = angles.toMatrix3d(); const coffs = matrix.coffs; const transformOffset = EGFBAccessors.TextStringTransform.createTextStringTransform(fbb, coffs[0], coffs[1], coffs[2], origin.x, coffs[3], coffs[4], coffs[5], origin.y, coffs[6], coffs[7], coffs[8], origin.z); builder.addTransform(fbb, transformOffset); } if (fbbGlyphs) { builder.addRange(fbb, EGFBAccessors.TextStringRange.createTextStringRange(fbb, fbbGlyphs.range.low.x, fbbGlyphs.range.low.y, fbbGlyphs.range.high.x, fbbGlyphs.range.high.y)); builder.addGlyphIds(fbb, fbbGlyphs.glyphIdsOffset); builder.addGlyphOrigins(fbb, fbbGlyphs.glyphOriginsOffset); } const mLoc = builder.endTextString(fbb); fbb.finish(mLoc); const data = fbb.asUint8Array(); return { opcode: ElementGeometryOpcode.TextString, data }; } ElementGeometry.fromTextString = fromTextString; /** Return entry as a [[ImageGraphic]] */ function toImageGraphic(entry, localToWorld) { if (ElementGeometryOpcode.Image !== entry.opcode) return undefined; const buffer = new flatbuffers.ByteBuffer(entry.data); const ppfb = EGFBAccessors.Image.getRootAsImage(buffer); const textureLong = ppfb.textureId(); const textureId = Id64.fromUint32Pair(textureLong.low, textureLong.high); const hasBorder = (1 === ppfb.drawBorder()); const corners = new ImageGraphicCorners(Point3d.createZero(), Point3d.createZero(), Point3d.createZero(), Point3d.createZero()); const corner0 = ppfb.tileCorner0(); const corner1 = ppfb.tileCorner1(); const corner2 = ppfb.tileCorner2(); const corner3 = ppfb.tileCorner3(); if (null !== corner0) corners[0].setFrom(Point3d.create(corner0.x(), corner0.y(), corner0.z())); if (null !== corner1) corners[1].setFrom(Point3d.create(corner1.x(), corner1.y(), corner1.z())); if (null !== corner2) corners[2].setFrom(Point3d.create(corner2.x(), corner2.y(), corner2.z())); if (null !== corner3) corners[3].setFrom(Point3d.create(corner3.x(), corner3.y(), corner3.z())); if (undefined !== localToWorld) { localToWorld.multiplyXYAn