UNPKG

@itwin/measure-tools-react

Version:
454 lines 21.2 kB
/*--------------------------------------------------------------------------------------------- * Copyright (c) Bentley Systems, Incorporated. All rights reserved. * See LICENSE.md in the project root for license terms and full copyright notice. *--------------------------------------------------------------------------------------------*/ import { Geometry, Range1d } from "@itwin/core-geometry"; import { IModelJson, LineSegment3d, Point3d, PointString3d, Ray3d, Vector3d, } from "@itwin/core-geometry"; import { GraphicType, IModelApp, QuantityType } from "@itwin/core-frontend"; import { FormatterUtils } from "../api/FormatterUtils.js"; import { StyleSet, TextOffsetType, WellKnownGraphicStyleType, WellKnownTextStyleType, } from "../api/GraphicStyle.js"; import { Measurement, MeasurementPickContext, MeasurementSerializer, } from "../api/Measurement.js"; import { MeasurementManager } from "../api/MeasurementManager.js"; import { MeasurementPreferences, MeasurementPreferencesProperty, } from "../api/MeasurementPreferences.js"; import { MeasurementPropertyHelper } from "../api/MeasurementPropertyHelper.js"; import { MeasurementSelectionSet } from "../api/MeasurementSelectionSet.js"; import { TextMarker } from "../api/TextMarker.js"; import { MeasureTools } from "../MeasureTools.js"; /** Serializer for a [[DistanceMeasurement]]. */ export class DistanceMeasurementSerializer extends MeasurementSerializer { get measurementName() { return DistanceMeasurementSerializer.distanceMeasurementName; } isValidType(measurement) { return measurement instanceof DistanceMeasurement; } isValidJSON(json) { if (!super.isValidJSON(json) || !json.hasOwnProperty("startPoint") || !json.hasOwnProperty("endPoint")) return false; return true; } parseSingle(data) { if (!this.isValidJSON(data)) return undefined; const props = data; return DistanceMeasurement.fromJSON(props); } } DistanceMeasurementSerializer.distanceMeasurementName = "distanceMeasurement"; /** * Distance measurement. Composed of a line and a formatted distance in a "text pill" in the middle of the line. */ export class DistanceMeasurement extends Measurement { get startPointRef() { return this._startPoint; } get endPointRef() { return this._endPoint; } get isDynamic() { return this._isDynamic; } set isDynamic(v) { this._isDynamic = v; this._runRiseAxes.forEach((axis) => (axis.isDynamic = v)); if (this._textMarker) this._textMarker.pickable = !v; } get showAxes() { return this._showAxes; } set showAxes(v) { this._showAxes = v; } // eslint-disable-next-line @typescript-eslint/naming-convention get isAxis() { return (undefined !== this._textStyleOverride || undefined !== this._graphicStyleOverride); } constructor(props) { super(props); this._startPoint = Point3d.createZero(); this._endPoint = Point3d.createZero(); this._isDynamic = false; this._showAxes = MeasurementPreferences.current.displayMeasurementAxes; this._runRiseAxes = []; if (props) this.readFromJSON(props); this.createTextMarker().catch(); // eslint-disable-line @typescript-eslint/no-floating-promises } setStartPoint(point) { this._startPoint.setFrom(point); this.createTextMarker().catch(); // eslint-disable-line @typescript-eslint/no-floating-promises this.buildRunRiseAxes(); } setEndPoint(point) { this._endPoint.setFrom(point); this.createTextMarker().catch(); // eslint-disable-line @typescript-eslint/no-floating-promises this.buildRunRiseAxes(); } setStartEndPoints(start, end) { this._startPoint.setFrom(start); this._endPoint.setFrom(end); this.createTextMarker().catch(); // eslint-disable-line @typescript-eslint/no-floating-promises this.buildRunRiseAxes(); } setupAxis(ovrGraphicStyle, ovrTextStyle, start, end) { this._graphicStyleOverride = ovrGraphicStyle; this._textStyleOverride = ovrTextStyle; this._startPoint.setFrom(start); this._endPoint.setFrom(end); } testDecorationHit(pickContext) { if (this.transientId && this.transientId === pickContext.geomId) return true; if (pickContext.buttonEvent && this._textMarker && this.displayLabels && this._textMarker.pick(pickContext.buttonEvent.viewPoint)) return true; if (this._showAxes) { for (const axis of this._runRiseAxes) { if (axis.testDecorationHit(pickContext)) return true; } } return false; } getDecorationGeometry(pickContext) { const geometry = [ IModelJson.Writer.toIModelJson(PointString3d.create(this._startPoint, this._endPoint)), ]; if (this._showAxes) { this._runRiseAxes.forEach((axis) => { const geom = axis.getDecorationGeometry(pickContext); if (geom) geometry.push(geom); }); } return geometry; } async getDecorationToolTip(_pickContext) { return MeasureTools.localization.getLocalizedString("MeasureTools:Measurements.distanceMeasurement"); } getSnapId() { if (!this.transientId) this.transientId = MeasurementSelectionSet.nextTransientId; if (this.isDynamic) return undefined; return this.transientId; } onTransientIdChanged(_prevId) { this._runRiseAxes.forEach((axis) => (axis.transientId = this.transientId)); if (this._textMarker) this._textMarker.transientHiliteId = this.transientId; } decorate(context) { super.decorate(context); const styleTheme = StyleSet.getOrDefault(this.activeStyle); const points = [this._startPoint, this._endPoint]; const style = styleTheme.getGraphicStyle(this._graphicStyleOverride || WellKnownGraphicStyleType.DistanceMeasurement); const xBuilder = context.createGraphicBuilder(GraphicType.WorldOverlay, undefined, this.getSnapId()); style.addStyledLineString(xBuilder, points, true); style.addStyledPointString(xBuilder, points, true); context.addDecorationFromBuilder(xBuilder); if (!this._textMarker || this._startPoint.isAlmostEqual(this._endPoint)) return; const textLocation = this.calculateWorldTextLocation(context); if (undefined === textLocation) return; this._textMarker.worldLocation = textLocation; // Determine which side to place the text marker relative to its anchor point const vPoints = [this._startPoint.clone(), this._endPoint.clone()]; context.viewport.worldToViewArray(vPoints); const v0 = Vector3d.createStartEnd(vPoints[1], vPoints[0]); v0.z = 0.0; v0.rotate90CCWXY(v0); v0.normalizeInPlace(); const xMax = 0.65; const yMax = 0.9; const x = v0.x < 0 ? Math.max(v0.x, -xMax) : Math.min(v0.x, xMax); const y = v0.y < 0 ? Math.max(v0.y, -yMax) : Math.min(v0.y, yMax); this._textMarker.offset = { type: TextOffsetType.Percentage, x, y }; if (this.displayLabels) this._textMarker.addDecoration(context); if (this._showAxes) this._runRiseAxes.forEach((axis) => axis.decorate(context)); } /** Clamps the segment to the current view frustum and return its midpoint. * @remarks Returns undefined if the segment is entirely 'behind' the camera eye */ calculateWorldTextLocation(context) { const clipFront = context.viewport.view.is3d() && context.viewport.view.isCameraOn; const clipPlanes = context.viewport.getWorldFrustum().getRangePlanes(clipFront, false, 0.0); const startIn = clipPlanes.isPointOnOrInside(this._startPoint, Geometry.smallMetricDistance); const endIn = clipPlanes.isPointOnOrInside(this._endPoint, Geometry.smallMetricDistance); if (startIn && endIn) return Point3d.createAdd2Scaled(this._startPoint, 0.5, this._endPoint, 0.5); const range = Range1d.createNull(); let ray = Ray3d.createStartEnd(this._startPoint.clone(), this._endPoint); // Either start/end or BOTH are outside the clip planes. If nothing intersects, don't bother displaying anything. if (!clipPlanes.hasIntersectionWithRay(ray, range)) return undefined; let clampedStartPoint = this._startPoint; let clampedEndPoint = this._endPoint; if (!endIn) { if (range.high < 0) return undefined; clampedEndPoint = ray.fractionToPoint(range.high); } if (!startIn) { ray = Ray3d.createStartEnd(this._endPoint.clone(), this._startPoint); if (!clipPlanes.hasIntersectionWithRay(ray, range) || range.high < 0) return undefined; clampedStartPoint = ray.fractionToPoint(range.high); } return Point3d.createAdd2Scaled(clampedStartPoint, 0.5, clampedEndPoint, 0.5); } buildRunRiseAxes() { this._runRiseAxes = []; if (this.isAxis) return; // Run point const runPoint = this._endPoint.clone(); runPoint.z = this._startPoint.z; // It"s irrelevant to draw the axes because we"re in 2D already. if (runPoint.isAlmostEqual(this._endPoint)) return; const lines = [ LineSegment3d.create(this._endPoint, runPoint), LineSegment3d.create(runPoint, this._startPoint), ]; const graphicStyles = [ WellKnownGraphicStyleType.Rise, WellKnownGraphicStyleType.Run, ]; const textStyles = [ WellKnownTextStyleType.Rise, WellKnownTextStyleType.Run, ]; for (let i = 0; i < 2; ++i) { const line = lines[i]; const dm = new DistanceMeasurement(); dm.setupAxis(graphicStyles[i], textStyles[i], line.point0Ref, line.point1Ref); dm.viewTarget.copyFrom(this.viewTarget); dm.isDynamic = this.isDynamic; dm.transientId = this.transientId; dm.displayLabels = this.displayLabels; this._runRiseAxes.push(dm); } // When all text markers are ready for display, trigger a refresh const promises = this._runRiseAxes.map(async (value) => value.createTextMarker()); // eslint-disable-line @typescript-eslint/no-floating-promises // eslint-disable-next-line @typescript-eslint/no-floating-promises Promise.all(promises) .then(() => { for (const axis of this._runRiseAxes) { if (axis._textMarker) { axis._textMarker.pickable = !this._isDynamic; axis._textMarker.transientHiliteId = this.transientId; axis._textMarker.setMouseButtonHandler(this.handleTextMarkerButtonEvent.bind(this)); } } if (this._showAxes) IModelApp.viewManager.invalidateDecorationsAllViews(); }) .catch(); } onStyleChanged(_isLock, _prevStyle) { this.updateMarkerStyle(); } onLockToggled() { this.updateMarkerStyle(); } onDisplayLabelsToggled() { for (const dm of this._runRiseAxes) dm.displayLabels = this.displayLabels; } onDisplayUnitsChanged() { this.createTextMarker().catch(); // eslint-disable-line @typescript-eslint/no-floating-promises this._runRiseAxes.forEach((axis) => axis.onDisplayUnitsChanged()); } updateMarkerStyle() { if (!this._textMarker) return; const styleTheme = StyleSet.getOrDefault(this.activeStyle); const tStyle = styleTheme.getTextStyle(this._textStyleOverride || WellKnownTextStyleType.DistanceMeasurement); this._textMarker.applyStyle(tStyle); } async createTextMarker() { const lengthSpec = await IModelApp.quantityFormatter.getFormatterSpecByQuantityType(QuantityType.LengthEngineering); const distance = this._startPoint.distance(this._endPoint); const fDistance = IModelApp.quantityFormatter.formatQuantity(distance * this.worldScale, lengthSpec); const midPoint = Point3d.createAdd2Scaled(this._startPoint, 0.5, this._endPoint, 0.5); const styleTheme = StyleSet.getOrDefault(this.activeStyle); const tStyle = styleTheme.getTextStyle(this._textStyleOverride || WellKnownTextStyleType.DistanceMeasurement); this._textMarker = TextMarker.createStyled([fDistance], midPoint, tStyle); this._textMarker.transientHiliteId = this.transientId; this._textMarker.pickable = !this.isDynamic; this._textMarker.setMouseButtonHandler(this.handleTextMarkerButtonEvent.bind(this)); } handleTextMarkerButtonEvent(ev) { if (!this._isDynamic) { void this.onDecorationButtonEvent(MeasurementPickContext.createFromSourceId("Invalid", ev)); } return true; } async getDataForMeasurementWidgetInternal() { const lengthSpec = await IModelApp.quantityFormatter.getFormatterSpecByQuantityType(QuantityType.LengthEngineering); const distance = this.worldScale * this._startPoint.distance(this._endPoint); const run = this.drawingMetadata?.worldScale !== undefined ? this.worldScale * Math.abs(this._endPoint.x - this._startPoint.x) : this._startPoint.distanceXY(this._endPoint); const rise = this.drawingMetadata?.worldScale !== undefined ? this.worldScale * (this._endPoint.y - this._startPoint.y) : this._endPoint.z - this._startPoint.z; const slope = 0.0 < run ? (100 * rise) / run : 0.0; const dx = Math.abs(this._endPoint.x - this._startPoint.x); const dy = Math.abs(this._endPoint.y - this._startPoint.y); const adjustedStart = this.adjustPointForGlobalOrigin(this._startPoint); const adjustedEnd = this.adjustPointForGlobalOrigin(this._endPoint); const fDistance = IModelApp.quantityFormatter.formatQuantity(distance, lengthSpec); const fStartCoords = FormatterUtils.formatCoordinatesImmediate(adjustedStart); const fEndCoords = FormatterUtils.formatCoordinatesImmediate(adjustedEnd); const fSlope = FormatterUtils.formatSlope(slope, true); const fRun = IModelApp.quantityFormatter.formatQuantity(run, lengthSpec); const fDeltaX = IModelApp.quantityFormatter.formatQuantity(dx, lengthSpec); const fDeltaY = IModelApp.quantityFormatter.formatQuantity(dy, lengthSpec); const fRise = IModelApp.quantityFormatter.formatQuantity(rise, lengthSpec); let title = MeasureTools.localization.getLocalizedString("MeasureTools:Measurements.distanceMeasurement"); title += ` [${fDistance}]`; const data = { title, properties: [] }; MeasurementPropertyHelper.tryAddNameProperty(this, data.properties); data.properties.push({ label: MeasureTools.localization.getLocalizedString("MeasureTools:tools.MeasureDistance.distance"), name: "DistanceMeasurement_Distance", value: fDistance, aggregatableValue: lengthSpec !== undefined ? { value: distance, formatSpec: lengthSpec } : undefined, }, { label: MeasureTools.localization.getLocalizedString("MeasureTools:tools.MeasureDistance.run"), name: "DistanceMeasurement_Run", value: fRun, aggregatableValue: lengthSpec !== undefined ? { value: run, formatSpec: lengthSpec } : undefined, }, { label: MeasureTools.localization.getLocalizedString("MeasureTools:tools.MeasureDistance.rise"), name: "DistanceMeasurement_Rise", value: fRise, }, { label: MeasureTools.localization.getLocalizedString("MeasureTools:tools.MeasureDistance.slope"), name: "DistanceMeasurement_Slope", value: fSlope, }); if (this.drawingMetadata?.worldScale === undefined) { data.properties.push({ label: MeasureTools.localization.getLocalizedString("MeasureTools:tools.MeasureDistance.delta_x"), name: "DistanceMeasurement_Dx", value: fDeltaX, }, { label: MeasureTools.localization.getLocalizedString("MeasureTools:tools.MeasureDistance.delta_y"), name: "DistanceMeasurement_Dy", value: fDeltaY, }, { label: MeasureTools.localization.getLocalizedString("MeasureTools:tools.MeasureDistance.startCoordinates"), name: "DistanceMeasurement_StartPoint", value: fStartCoords, }, { label: MeasureTools.localization.getLocalizedString("MeasureTools:tools.MeasureDistance.endCoordinates"), name: "DistanceMeasurement_EndPoint", value: fEndCoords, }); } return data; } /** * Tests equality with another measurement. * @param other Measurement to test equality for. * @param opts Options for equality testing. * @returns true if the other measurement is equal, false if some property is not the same or if the measurement is not of the same type. */ equals(other, opts) { if (!super.equals(other, opts)) return false; // Compare data (ignore isDynamic) const tol = opts ? opts.tolerance : undefined; const otherDist = other; if (otherDist === undefined || !this._startPoint.isAlmostEqual(otherDist._startPoint, tol) || !this._endPoint.isAlmostEqual(otherDist._endPoint, tol) || this._showAxes !== otherDist._showAxes) return false; return true; } /** * Copies data from the other measurement into this instance. * @param other Measurement to copy property values from. */ copyFrom(other) { super.copyFrom(other); if (other instanceof DistanceMeasurement) { this._isDynamic = other._isDynamic; this._showAxes = other._showAxes; this._startPoint.setFrom(other._startPoint); this._endPoint.setFrom(other._endPoint); this.buildRunRiseAxes(); this.createTextMarker().catch(); // eslint-disable-line @typescript-eslint/no-floating-promises } } /** * Deserializes properties (if they exist) from the JSON object. * @param json JSON object to read data from. */ readFromJSON(json) { super.readFromJSON(json); const jsonDist = json; if (jsonDist.startPoint !== undefined) this._startPoint.setFromJSON(jsonDist.startPoint); if (jsonDist.endPoint !== undefined) this._endPoint.setFromJSON(jsonDist.endPoint); this._showAxes = jsonDist.showAxes !== undefined ? jsonDist.showAxes : MeasurementPreferences.current.displayMeasurementAxes; this.buildRunRiseAxes(); this.createTextMarker().catch(); // eslint-disable-line @typescript-eslint/no-floating-promises } /** * Serializes properties to a JSON object. * @param json JSON object to append data to. */ writeToJSON(json) { super.writeToJSON(json); const jsonDist = json; jsonDist.startPoint = this._startPoint.toJSON(); jsonDist.endPoint = this._endPoint.toJSON(); jsonDist.showAxes = this._showAxes; } static create(start, end, viewType) { // Don't ned to serialize the points, will just work as is const measurement = new DistanceMeasurement({ startPoint: start, endPoint: end, }); if (viewType) measurement.viewTarget.include(viewType); return measurement; } static fromJSON(data) { return new DistanceMeasurement(data); } } DistanceMeasurement.serializer = Measurement.registerSerializer(new DistanceMeasurementSerializer()); // Ensure all distance measurements respond to when show axes is turned on/off in preferences function onDisplayMeasurementAxesHandler(propChanged) { if (propChanged !== MeasurementPreferencesProperty.displayMeasurementAxes) return; const showAxes = MeasurementPreferences.current.displayMeasurementAxes; MeasurementManager.instance.forAllMeasurements((measurement) => { if (measurement instanceof DistanceMeasurement) measurement.showAxes = showAxes; return true; }); } MeasurementPreferences.current.onPreferenceChanged.addListener(onDisplayMeasurementAxesHandler); //# sourceMappingURL=DistanceMeasurement.js.map