577 lines
18 KiB
TypeScript
577 lines
18 KiB
TypeScript
import {Box, Line, Point, Segment, Vector} from '@flatten-js/core';
|
|
import {minBy, round} from 'es-toolkit';
|
|
import {max, min} from 'es-toolkit/compat';
|
|
import {
|
|
ARROW_HEAD_LENGTH,
|
|
ARROW_HEAD_WIDTH,
|
|
EPSILON,
|
|
MEASUREMENT_DECIMAL_PLACES,
|
|
MEASUREMENT_EXTENSION_LENGTH,
|
|
MEASUREMENT_FONT_SIZE,
|
|
MEASUREMENT_LABEL_OFFSET,
|
|
MEASUREMENT_ORIGIN_MARGIN,
|
|
TO_RADIANS,
|
|
} from '../App.consts';
|
|
import type {Shape, SnapPoint} from '../App.types';
|
|
import type {DrawController} from '../drawControllers/DrawController';
|
|
import {pointDistance} from '../helpers/distance-between-points';
|
|
import {isPointEqual} from '../helpers/is-point-equal';
|
|
import {mirrorPointOverAxis} from '../helpers/mirror-point-over-axis.ts';
|
|
import {scalePoint} from '../helpers/scale-point';
|
|
import {getActiveLayerId, isEntityHighlighted, isEntitySelected} from '../state.ts';
|
|
import {type Entity, EntityName, type JsonEntity} from './Entity';
|
|
import type {LineEntity} from './LineEntity.ts';
|
|
|
|
export class MeasurementEntity implements Entity {
|
|
public id: string = crypto.randomUUID();
|
|
public lineColor = '#fff';
|
|
public lineWidth = 1;
|
|
public lineDash: number[] | undefined = undefined;
|
|
public layerId: string;
|
|
|
|
private startPoint: Point;
|
|
private endPoint: Point;
|
|
private offsetPoint: Point;
|
|
|
|
constructor(layerId: string, startPoint: Point, endPoint: Point, offsetPoint: Point) {
|
|
this.layerId = layerId;
|
|
this.startPoint = startPoint;
|
|
this.endPoint = endPoint;
|
|
this.offsetPoint = offsetPoint;
|
|
}
|
|
|
|
public getDrawPoints() {
|
|
// Return if measurement is zero length
|
|
if (isPointEqual(this.startPoint, this.endPoint)) {
|
|
return null;
|
|
}
|
|
// Base line of measurement
|
|
const lineStartToEnd = new Line(this.startPoint, this.endPoint);
|
|
|
|
// Calculate distance to offset point
|
|
const [, segment] = this.offsetPoint.distanceTo(lineStartToEnd);
|
|
const closestPointToOffsetOnLine = segment.end;
|
|
|
|
// Calculate 2 extension lines
|
|
let vectorPerpendicularFromLineTowardsOffsetPoint: Vector;
|
|
if (isPointEqual(closestPointToOffsetOnLine, this.offsetPoint)) {
|
|
// Offset point lies on baseline
|
|
vectorPerpendicularFromLineTowardsOffsetPoint = lineStartToEnd.norm;
|
|
} else {
|
|
// Offset point doesn't lie on baseline
|
|
vectorPerpendicularFromLineTowardsOffsetPoint = new Vector(
|
|
closestPointToOffsetOnLine,
|
|
this.offsetPoint
|
|
);
|
|
}
|
|
|
|
// Unit vector for offset direction
|
|
const vectorPerpendicularFromLineTowardsOffsetPointUnit =
|
|
vectorPerpendicularFromLineTowardsOffsetPoint.normalize();
|
|
|
|
// Points for horizontal measurement line
|
|
const offsetStartPoint = this.startPoint
|
|
.clone()
|
|
.translate(vectorPerpendicularFromLineTowardsOffsetPoint);
|
|
const offsetEndPoint = this.endPoint
|
|
.clone()
|
|
.translate(vectorPerpendicularFromLineTowardsOffsetPoint);
|
|
|
|
// Start of the perpendicular lines
|
|
const offsetStartPointMargin = this.startPoint
|
|
.clone()
|
|
.translate(
|
|
vectorPerpendicularFromLineTowardsOffsetPointUnit.multiply(MEASUREMENT_ORIGIN_MARGIN)
|
|
);
|
|
|
|
const offsetEndPointMargin = this.endPoint
|
|
.clone()
|
|
.translate(
|
|
vectorPerpendicularFromLineTowardsOffsetPointUnit.multiply(MEASUREMENT_ORIGIN_MARGIN)
|
|
);
|
|
|
|
// End of the perpendicular lines
|
|
const offsetStartPointExtend = offsetStartPoint
|
|
.clone()
|
|
.translate(
|
|
vectorPerpendicularFromLineTowardsOffsetPointUnit.multiply(MEASUREMENT_EXTENSION_LENGTH)
|
|
);
|
|
|
|
const offsetEndPointExtend = offsetEndPoint
|
|
.clone()
|
|
.translate(
|
|
vectorPerpendicularFromLineTowardsOffsetPointUnit.multiply(MEASUREMENT_EXTENSION_LENGTH)
|
|
);
|
|
|
|
// Location for label
|
|
const midpointMeasurementLine = new Point(
|
|
(offsetStartPoint.x + offsetEndPoint.x) / 2,
|
|
(offsetStartPoint.y + offsetEndPoint.y) / 2
|
|
);
|
|
const textHeight = MEASUREMENT_FONT_SIZE;
|
|
const totalOffset = MEASUREMENT_LABEL_OFFSET + textHeight / 2;
|
|
const midpointMeasurementLineOffset = midpointMeasurementLine
|
|
.clone()
|
|
.translate(vectorPerpendicularFromLineTowardsOffsetPointUnit.multiply(totalOffset));
|
|
|
|
// TEMPORARY LOGGING START
|
|
if (
|
|
this.startPoint.x === 0 &&
|
|
this.startPoint.y === 0 &&
|
|
this.endPoint.x === 100 &&
|
|
this.endPoint.y === 0 &&
|
|
this.offsetPoint.x === 0 &&
|
|
this.offsetPoint.y === 20
|
|
) {
|
|
// Condition to target the specific test
|
|
console.log('[DEBUG getDrawPoints] For test ((0,0)-(100,0), offset(0,20)):');
|
|
console.log('startPoint:', JSON.stringify(this.startPoint));
|
|
console.log('endPoint:', JSON.stringify(this.endPoint));
|
|
console.log('offsetPoint:', JSON.stringify(this.offsetPoint));
|
|
console.log('offsetStartPoint:', JSON.stringify(offsetStartPoint));
|
|
console.log('offsetEndPoint:', JSON.stringify(offsetEndPoint));
|
|
console.log('offsetStartPointMargin:', JSON.stringify(offsetStartPointMargin));
|
|
console.log('offsetStartPointExtend:', JSON.stringify(offsetStartPointExtend));
|
|
console.log('offsetEndPointMargin:', JSON.stringify(offsetEndPointMargin));
|
|
console.log('offsetEndPointExtend:', JSON.stringify(offsetEndPointExtend));
|
|
}
|
|
// TEMPORARY LOGGING END
|
|
return {
|
|
offsetStartPoint,
|
|
offsetEndPoint,
|
|
offsetStartPointExtend,
|
|
offsetEndPointExtend,
|
|
offsetStartPointMargin,
|
|
offsetEndPointMargin,
|
|
midpointMeasurementLineOffset,
|
|
normalUnit: vectorPerpendicularFromLineTowardsOffsetPointUnit,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Draws an arrow head which ends at the endPoint
|
|
* The start point doesn't really matter, only the direction
|
|
* the size of the arrow is determined by ARROW_HEAD_SIZE
|
|
* @param drawController
|
|
* @param startPoint
|
|
* @param endPoint
|
|
*/
|
|
private drawArrowHead = (
|
|
drawController: DrawController,
|
|
startPoint: Point,
|
|
endPoint: Point,
|
|
isHighlighted: boolean,
|
|
isSelected: boolean
|
|
): void => {
|
|
const screenScale = drawController.getScreenScale();
|
|
const vectorFromEndToStart = new Vector(endPoint, startPoint);
|
|
const vectorFromEndToStartUnit = vectorFromEndToStart.normalize();
|
|
const baseOfArrow = endPoint
|
|
.clone()
|
|
.translate(vectorFromEndToStartUnit.multiply(ARROW_HEAD_LENGTH * screenScale));
|
|
const perpendicularVector1 = vectorFromEndToStartUnit.rotate(90 * TO_RADIANS);
|
|
const perpendicularVector2 = vectorFromEndToStartUnit.rotate(-90 * TO_RADIANS);
|
|
const leftCornerOfArrow = baseOfArrow
|
|
.clone()
|
|
.translate(perpendicularVector1.multiply(ARROW_HEAD_WIDTH * screenScale));
|
|
const rightCornerOfArrow = baseOfArrow
|
|
.clone()
|
|
.translate(perpendicularVector2.multiply(ARROW_HEAD_WIDTH * screenScale));
|
|
|
|
drawController.setLineStyles(
|
|
isHighlighted,
|
|
isSelected,
|
|
this.lineColor,
|
|
this.lineWidth,
|
|
this.lineDash
|
|
);
|
|
drawController.drawLine(endPoint, leftCornerOfArrow);
|
|
drawController.drawLine(endPoint, rightCornerOfArrow);
|
|
drawController.drawLine(leftCornerOfArrow, rightCornerOfArrow);
|
|
drawController.setFillStyles(this.lineColor);
|
|
drawController.fillPolygon(endPoint, leftCornerOfArrow, rightCornerOfArrow);
|
|
};
|
|
|
|
/**
|
|
* Drawing of measurement:
|
|
*
|
|
* offsetPoint offsetEndPoint
|
|
* __x___--->x
|
|
* offsetStartPoint ______----- \
|
|
* x<---- \
|
|
* \ x
|
|
* \ endPoint
|
|
* x
|
|
* startPoint
|
|
*
|
|
* @param drawController
|
|
* @param parentHighlighted
|
|
* @param parentSelected
|
|
*/
|
|
public draw(
|
|
drawController: DrawController,
|
|
parentHighlighted?: boolean,
|
|
parentSelected?: boolean
|
|
): void {
|
|
if (isPointEqual(this.startPoint, this.endPoint)) {
|
|
return; // We can't draw a measurement with 0 length
|
|
}
|
|
const isHighlighted = parentHighlighted ?? isEntityHighlighted(this);
|
|
const isSelected = parentSelected ?? isEntitySelected(this);
|
|
drawController.setLineStyles(
|
|
isHighlighted,
|
|
isSelected,
|
|
this.lineColor,
|
|
this.lineWidth,
|
|
this.lineDash
|
|
);
|
|
drawController.setFillStyles(this.lineColor);
|
|
const drawPoints = this.getDrawPoints();
|
|
if (!drawPoints) {
|
|
return;
|
|
}
|
|
const {
|
|
offsetStartPoint,
|
|
offsetEndPoint,
|
|
offsetStartPointExtend,
|
|
offsetEndPointExtend,
|
|
offsetStartPointMargin,
|
|
offsetEndPointMargin,
|
|
midpointMeasurementLineOffset,
|
|
normalUnit,
|
|
} = drawPoints;
|
|
|
|
this.drawArrowHead(drawController, offsetStartPoint, offsetEndPoint, isHighlighted, isSelected);
|
|
this.drawArrowHead(drawController, offsetEndPoint, offsetStartPoint, isHighlighted, isSelected);
|
|
drawController.setLineStyles(
|
|
isHighlighted,
|
|
isSelected,
|
|
this.lineColor,
|
|
this.lineWidth,
|
|
this.lineDash
|
|
);
|
|
drawController.drawLine(offsetStartPoint, offsetEndPoint);
|
|
drawController.drawLine(offsetStartPointMargin, offsetStartPointExtend);
|
|
drawController.drawLine(offsetEndPointMargin, offsetEndPointExtend);
|
|
|
|
const distance = String(
|
|
round(pointDistance(this.startPoint, this.endPoint), MEASUREMENT_DECIMAL_PLACES)
|
|
);
|
|
const originalTextDirection = normalUnit.rotate90CW();
|
|
let finalTextDirection = originalTextDirection;
|
|
if (
|
|
originalTextDirection.x < -EPSILON ||
|
|
(Math.abs(originalTextDirection.x) < EPSILON && originalTextDirection.y > EPSILON)
|
|
) {
|
|
finalTextDirection = new Vector(-originalTextDirection.x, -originalTextDirection.y);
|
|
}
|
|
drawController.drawText(distance, midpointMeasurementLineOffset, {
|
|
textAlign: 'center',
|
|
textDirection: finalTextDirection,
|
|
fontSize: MEASUREMENT_FONT_SIZE,
|
|
textColor: this.lineColor,
|
|
});
|
|
}
|
|
|
|
public move(x: number, y: number) {
|
|
this.startPoint = this.startPoint.translate(x, y);
|
|
this.endPoint = this.endPoint.translate(x, y);
|
|
this.offsetPoint = this.offsetPoint.translate(x, y);
|
|
}
|
|
|
|
public scale(scaleOrigin: Point, scaleFactor: number) {
|
|
this.startPoint = scalePoint(this.startPoint, scaleOrigin, scaleFactor);
|
|
this.endPoint = scalePoint(this.endPoint, scaleOrigin, scaleFactor);
|
|
this.offsetPoint = scalePoint(this.offsetPoint, scaleOrigin, scaleFactor);
|
|
}
|
|
|
|
public rotate(rotateOrigin: Point, angle: number) {
|
|
this.startPoint = this.startPoint.rotate(angle, rotateOrigin);
|
|
this.endPoint = this.endPoint.rotate(angle, rotateOrigin);
|
|
this.offsetPoint = this.offsetPoint.rotate(angle, rotateOrigin);
|
|
}
|
|
|
|
public mirror(mirrorAxis: LineEntity) {
|
|
this.startPoint = mirrorPointOverAxis(this.startPoint, mirrorAxis);
|
|
this.endPoint = mirrorPointOverAxis(this.endPoint, mirrorAxis);
|
|
this.offsetPoint = mirrorPointOverAxis(this.offsetPoint, mirrorAxis);
|
|
}
|
|
|
|
public clone(): MeasurementEntity {
|
|
return new MeasurementEntity(
|
|
getActiveLayerId(),
|
|
this.startPoint.clone(),
|
|
this.endPoint.clone(),
|
|
this.offsetPoint.clone()
|
|
);
|
|
}
|
|
|
|
public intersectsWithBox(box: Box): boolean {
|
|
const drawPoints = this.getDrawPoints();
|
|
|
|
if (!drawPoints) {
|
|
return false;
|
|
}
|
|
|
|
const measurementLines = [
|
|
new Segment(drawPoints.offsetStartPoint, drawPoints.offsetEndPoint),
|
|
new Segment(drawPoints.offsetStartPointMargin, drawPoints.offsetStartPointExtend),
|
|
new Segment(drawPoints.offsetEndPointMargin, drawPoints.offsetEndPointExtend),
|
|
];
|
|
|
|
for (const line of measurementLines) {
|
|
if (line.intersect(box).length > 0) {
|
|
return true;
|
|
}
|
|
if (box.contains(line)) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
public isContainedInBox(box: Box): boolean {
|
|
const drawPoints = this.getDrawPoints();
|
|
|
|
if (!drawPoints) {
|
|
return false;
|
|
}
|
|
|
|
const measurementLines = [
|
|
new Segment(drawPoints.offsetStartPoint, drawPoints.offsetEndPoint),
|
|
new Segment(drawPoints.offsetStartPointMargin, drawPoints.offsetStartPointExtend),
|
|
new Segment(drawPoints.offsetEndPointMargin, drawPoints.offsetEndPointExtend),
|
|
];
|
|
|
|
for (const line of measurementLines) {
|
|
if (!box.contains(line)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
public getBoundingBox(): Box {
|
|
const drawPoints = this.getDrawPoints();
|
|
|
|
if (!drawPoints) {
|
|
throw new Error('Failed to get draw points from measurement entity');
|
|
}
|
|
|
|
const lineExtremePoints = [
|
|
drawPoints.offsetStartPointMargin,
|
|
drawPoints.offsetStartPointExtend,
|
|
drawPoints.offsetEndPointMargin,
|
|
drawPoints.offsetEndPointExtend,
|
|
// Also include the main measurement line itself in the bounding box calculation for lines
|
|
drawPoints.offsetStartPoint,
|
|
drawPoints.offsetEndPoint,
|
|
];
|
|
|
|
// Calculate text properties
|
|
const distance = String(
|
|
round(pointDistance(this.startPoint, this.endPoint), MEASUREMENT_DECIMAL_PLACES)
|
|
);
|
|
const textHeight = MEASUREMENT_FONT_SIZE;
|
|
// Estimate width: textString.length * fontSize * aspectRatioFactor
|
|
const textWidth = distance.length * MEASUREMENT_FONT_SIZE * 0.6;
|
|
|
|
const { midpointMeasurementLineOffset, normalUnit } = drawPoints;
|
|
|
|
// Determine text direction (similar to draw method)
|
|
const originalTextDirection = normalUnit.rotate90CW();
|
|
let finalTextDirection = originalTextDirection;
|
|
if (
|
|
originalTextDirection.x < -EPSILON ||
|
|
(Math.abs(originalTextDirection.x) < EPSILON && originalTextDirection.y > EPSILON)
|
|
) {
|
|
finalTextDirection = new Vector(-originalTextDirection.x, -originalTextDirection.y);
|
|
}
|
|
|
|
// Text center
|
|
const textCenterX = midpointMeasurementLineOffset.x;
|
|
const textCenterY = midpointMeasurementLineOffset.y;
|
|
|
|
// Half dimensions
|
|
const halfTextWidth = textWidth / 2;
|
|
const halfTextHeight = textHeight / 2;
|
|
|
|
// Text corner calculations
|
|
// Vector along the text direction for width, and perpendicular for height
|
|
const dirVec = finalTextDirection.normalize(); // Vector along the text direction
|
|
const perpVec = dirVec.rotate90CW(); // Vector perpendicular to text direction (for height offset)
|
|
|
|
const textCorners = [
|
|
new Point(
|
|
textCenterX - dirVec.x * halfTextWidth - perpVec.x * halfTextHeight,
|
|
textCenterY - dirVec.y * halfTextWidth - perpVec.y * halfTextHeight
|
|
),
|
|
new Point(
|
|
textCenterX + dirVec.x * halfTextWidth - perpVec.x * halfTextHeight,
|
|
textCenterY + dirVec.y * halfTextWidth - perpVec.y * halfTextHeight
|
|
),
|
|
new Point(
|
|
textCenterX + dirVec.x * halfTextWidth + perpVec.x * halfTextHeight,
|
|
textCenterY + dirVec.y * halfTextWidth + perpVec.y * halfTextHeight
|
|
),
|
|
new Point(
|
|
textCenterX - dirVec.x * halfTextWidth + perpVec.x * halfTextHeight,
|
|
textCenterY - dirVec.y * halfTextWidth + perpVec.y * halfTextHeight
|
|
),
|
|
];
|
|
|
|
const allExtremePoints = [...lineExtremePoints, ...textCorners];
|
|
|
|
return new Box(
|
|
min(allExtremePoints.map((point) => point.x)),
|
|
min(allExtremePoints.map((point) => point.y)),
|
|
max(allExtremePoints.map((point) => point.x)),
|
|
max(allExtremePoints.map((point) => point.y))
|
|
);
|
|
}
|
|
|
|
public getShape(): Shape | null {
|
|
return null;
|
|
}
|
|
|
|
public getSnapPoints(): SnapPoint[] {
|
|
return [];
|
|
}
|
|
|
|
public getIntersections(): Point[] {
|
|
return [];
|
|
}
|
|
|
|
public getFirstPoint(): Point | null {
|
|
return this.startPoint;
|
|
}
|
|
|
|
public distanceTo(shape: Shape): [number, Segment] | null {
|
|
const drawPoints = this.getDrawPoints();
|
|
if (!drawPoints) {
|
|
return null;
|
|
}
|
|
const {
|
|
offsetStartPoint,
|
|
offsetEndPoint,
|
|
offsetStartPointExtend,
|
|
offsetEndPointExtend,
|
|
offsetStartPointMargin,
|
|
offsetEndPointMargin,
|
|
} = drawPoints;
|
|
|
|
const mainSegment = new Segment(offsetStartPoint, offsetEndPoint);
|
|
const horizontalLineDistanceInfo = mainSegment.distanceTo(shape);
|
|
|
|
const leftExtensionSegment = new Segment(offsetStartPointMargin, offsetStartPointExtend);
|
|
const leftVerticalLineDistanceInfo = leftExtensionSegment.distanceTo(shape);
|
|
|
|
const rightExtensionSegment = new Segment(offsetEndPointMargin, offsetEndPointExtend);
|
|
const rightVerticalLineDistanceInfo = rightExtensionSegment.distanceTo(shape);
|
|
|
|
return minBy(
|
|
[horizontalLineDistanceInfo, leftVerticalLineDistanceInfo, rightVerticalLineDistanceInfo],
|
|
(distanceInfo) => distanceInfo[0]
|
|
);
|
|
}
|
|
|
|
public getSvgString(): string | null {
|
|
throw new Error('getSvgString for MeasurementEntity not yet implemented');
|
|
// return (
|
|
// this.segment.svg({
|
|
// strokeWidth: this.lineWidth,
|
|
// stroke: getExportColor(this.lineColor),
|
|
// }) || null
|
|
// );
|
|
}
|
|
|
|
public getType(): EntityName {
|
|
return EntityName.Measurement;
|
|
}
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
public containsPointOnShape(point: Point): boolean {
|
|
const drawPoints = this.getDrawPoints();
|
|
|
|
if (!drawPoints) {
|
|
return false; // No visual representation, so no point can be on it.
|
|
}
|
|
|
|
const {
|
|
offsetStartPoint,
|
|
offsetEndPoint,
|
|
offsetStartPointMargin,
|
|
offsetStartPointExtend,
|
|
offsetEndPointMargin,
|
|
offsetEndPointExtend,
|
|
} = drawPoints;
|
|
|
|
const measurementLine = new Segment(offsetStartPoint, offsetEndPoint);
|
|
if (measurementLine.contains(point)) {
|
|
return true;
|
|
}
|
|
|
|
const extensionLine1 = new Segment(offsetStartPointMargin, offsetStartPointExtend);
|
|
if (extensionLine1.contains(point)) {
|
|
return true;
|
|
}
|
|
|
|
const extensionLine2 = new Segment(offsetEndPointMargin, offsetEndPointExtend);
|
|
if (extensionLine2.contains(point)) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
public async toJson(): Promise<JsonEntity<MeasurementJsonData> | null> {
|
|
return {
|
|
id: this.id,
|
|
type: EntityName.Measurement,
|
|
lineColor: this.lineColor,
|
|
lineWidth: this.lineWidth,
|
|
layerId: this.layerId,
|
|
shapeData: {
|
|
startPoint: { x: this.startPoint.x, y: this.startPoint.y },
|
|
endPoint: { x: this.endPoint.x, y: this.endPoint.y },
|
|
offsetPoint: { x: this.offsetPoint.x, y: this.offsetPoint.y },
|
|
},
|
|
};
|
|
}
|
|
|
|
public static async fromJson(
|
|
jsonEntity: JsonEntity<MeasurementJsonData>
|
|
): Promise<MeasurementEntity> {
|
|
if (!jsonEntity.shapeData) {
|
|
throw new Error('Invalid JSON entity of type Measurement: missing shapeData');
|
|
}
|
|
const startPoint = new Point(
|
|
jsonEntity.shapeData.startPoint.x,
|
|
jsonEntity.shapeData.startPoint.y
|
|
);
|
|
const endPoint = new Point(jsonEntity.shapeData.endPoint.x, jsonEntity.shapeData.endPoint.y);
|
|
const offsetPoint = new Point(
|
|
jsonEntity.shapeData.offsetPoint.x,
|
|
jsonEntity.shapeData.offsetPoint.y
|
|
);
|
|
const measurementEntity = new MeasurementEntity(
|
|
jsonEntity.layerId || getActiveLayerId(),
|
|
startPoint,
|
|
endPoint,
|
|
offsetPoint
|
|
);
|
|
measurementEntity.id = jsonEntity.id;
|
|
measurementEntity.lineColor = jsonEntity.lineColor;
|
|
measurementEntity.lineWidth = jsonEntity.lineWidth;
|
|
return measurementEntity;
|
|
}
|
|
}
|
|
|
|
export interface MeasurementJsonData {
|
|
startPoint: { x: number; y: number };
|
|
endPoint: { x: number; y: number };
|
|
offsetPoint: { x: number; y: number };
|
|
}
|