Skip to content

Commit f9f2042

Browse files
Fix: Improve measurement entity selection and distance calculation
This commit addresses issue #34 (Cannot select measurements). The following changes were made: 1. Implemented `MeasurementEntity.containsPointOnShape`: This method was previously unimplemented. It now correctly checks if a given point lies on any of the visible line segments (main line or extension lines) of the measurement entity. Unit tests have been added to cover various scenarios. 2. Corrected `MeasurementEntity.distanceTo`: The method was using `Line.distanceTo` from `@flatten-js/core`, which calculates distance to an infinite line. This was changed to use `Segment.distanceTo` to accurately calculate the distance to the finite, visible segments of the measurement entity. This is crucial for selection and other interactions that rely on proximity to the entity's visual representation. Unit tests have been added to verify this change, including cases where a point is collinear but outside a segment. These changes ensure that measurement entities can be more reliably selected and that distance calculations are accurate with respect to their drawn geometry.
1 parent d453241 commit f9f2042

File tree

2 files changed

+271
-9
lines changed

2 files changed

+271
-9
lines changed

src/entities/MeasurementEntity.test.ts

Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,3 +94,236 @@ describe('MeasurementEntity text orientation in draw() method', () => {
9494
expect(mockDrawController.drawText).not.toHaveBeenCalled();
9595
});
9696
});
97+
98+
describe('MeasurementEntity.distanceTo', () => {
99+
const layerId = 'test-layer';
100+
101+
// Re-define createMeasurement for this test suite
102+
// (Alternatively, this could be hoisted to a common scope in a real-world scenario)
103+
const createMeasurement = (
104+
start = new Point(0, 0),
105+
end = new Point(100, 0),
106+
offset = new Point(50, 50) // Offset above the line (positive y for horizontal line)
107+
) => new MeasurementEntity(layerId, start, end, offset);
108+
109+
it('should return correct distance for a point closest to the main horizontal segment', () => {
110+
// Measurement: start(0,0), end(100,0), offset(0,20) -> main segment from (0,20) to (100,20)
111+
const measurement = createMeasurement(new Point(0,0), new Point(100,0), new Point(0,20));
112+
const testPoint = new Point(50, 30); // 10 units "above" the main segment's Y-coordinate
113+
114+
const distanceInfo = measurement.distanceTo(testPoint);
115+
expect(distanceInfo).not.toBeNull();
116+
expect(distanceInfo![0]).toBeCloseTo(10);
117+
expect(distanceInfo![1].ps.equalTo(testPoint)).toBe(true); // Segment starts at testPoint
118+
expect(distanceInfo![1].pe.equalTo(new Point(50, 20))).toBe(true); // Segment ends at closest point on main segment
119+
});
120+
121+
it('should return correct distance for a point closest to an endpoint of the main horizontal segment', () => {
122+
// Main segment from (0,20) to (100,20)
123+
const measurement = createMeasurement(new Point(0,0), new Point(100,0), new Point(0,20));
124+
const testPoint = new Point(110, 30); // Diagonally off the endPoint (100,20)
125+
// dx = 10, dy = 10. Distance = sqrt(10^2 + 10^2) = sqrt(200)
126+
127+
const distanceInfo = measurement.distanceTo(testPoint);
128+
expect(distanceInfo).not.toBeNull();
129+
expect(distanceInfo![0]).toBeCloseTo(Math.sqrt(200));
130+
expect(distanceInfo![1].ps.equalTo(testPoint)).toBe(true);
131+
expect(distanceInfo![1].pe.equalTo(new Point(100, 20))).toBe(true); // Closest to offsetEndPoint
132+
});
133+
134+
it('should return correct distance for a point closest to one of the vertical extension lines', () => {
135+
// Measurement: start(0,0), end(100,0), offset(0,20)
136+
// Main segment: (0,20) to (100,20)
137+
// getDrawPoints will calculate extension lines. Let's use default MEASUREMENT_ORIGIN_MARGIN and MEASUREMENT_EXTENSION_LENGTH.
138+
// For simplicity, let's assume MEASUREMENT_ORIGIN_MARGIN = 2, MEASUREMENT_EXTENSION_LENGTH = 10 for calculation.
139+
// Actual values from App.consts are: MEASUREMENT_ORIGIN_MARGIN = 2, MEASUREMENT_EXTENSION_LENGTH = 8
140+
// So, first extension line for startPoint(0,0) with offset (0,20) (vectorPerpendicularFromLineTowardsOffsetPoint is (0,20))
141+
// offsetStartPointMargin: (0,0) + (0,1).normalize() * 2 = (0,2)
142+
// offsetStartPoint: (0,0) + (0,20) = (0,20)
143+
// offsetStartPointExtend: (0,20) + (0,1).normalize() * 8 = (0,28)
144+
// So, first extension line is from (0,2) to (0,28).
145+
// Note: The above calculation of offsetStartPointExtend is relative to offsetStartPoint, not startPoint
146+
// The actual calculation in getDrawPoints for offsetStartPointExtend is:
147+
// offsetStartPoint.clone().translate(vectorPerpendicularFromLineTowardsOffsetPointUnit.multiply(MEASUREMENT_EXTENSION_LENGTH))
148+
// vectorPerpendicularFromLineTowardsOffsetPointUnit for offset (0,20) is (0,1) (assuming start/end on x-axis)
149+
// So, offsetStartPointExtend = (0,20) + (0,1)*8 = (0,28)
150+
// And offsetStartPointMargin = (0,0) + (0,1)*2 = (0,2)
151+
152+
const measurement = createMeasurement(new Point(0,0), new Point(100,0), new Point(0,20));
153+
// Test point closest to the first extension line (0,2) to (0,28)
154+
const testPoint = new Point(5, 15); // 5 units away horizontally from the extension line
155+
156+
const distanceInfo = measurement.distanceTo(testPoint);
157+
expect(distanceInfo).not.toBeNull();
158+
expect(distanceInfo![0]).toBeCloseTo(5);
159+
expect(distanceInfo![1].ps.equalTo(testPoint)).toBe(true);
160+
expect(distanceInfo![1].pe.equalTo(new Point(0, 15))).toBe(true); // Closest point on segment (0,2)-(0,28)
161+
});
162+
163+
it('should return correct distance for a point collinear with main segment but outside', () => {
164+
// Main segment from (0,20) to (100,20)
165+
const measurement = createMeasurement(new Point(0,0), new Point(100,0), new Point(0,20));
166+
const testPoint = new Point(120, 20); // Collinear, 20 units away from offsetEndPoint (100,20)
167+
168+
const distanceInfo = measurement.distanceTo(testPoint);
169+
expect(distanceInfo).not.toBeNull();
170+
expect(distanceInfo![0]).toBeCloseTo(20);
171+
expect(distanceInfo![1].ps.equalTo(testPoint)).toBe(true);
172+
expect(distanceInfo![1].pe.equalTo(new Point(100, 20))).toBe(true);
173+
});
174+
175+
it('should return null for a zero-length measurement', () => {
176+
// startPoint and endPoint are the same
177+
const measurement = createMeasurement(new Point(0,0), new Point(0,0), new Point(0,20));
178+
const testPoint = new Point(50, 30);
179+
const distanceInfo = measurement.distanceTo(testPoint);
180+
expect(distanceInfo).toBeNull();
181+
});
182+
183+
it('should correctly calculate distance to a point closer to the second extension line', () => {
184+
// Measurement: start(0,0), end(100,0), offset(0,20)
185+
// Second extension line for endPoint(100,0) with offset (0,20)
186+
// offsetEndPointMargin: (100,0) + (0,1)*2 = (100,2)
187+
// offsetEndPointExtend: (100,20) + (0,1)*8 = (100,28)
188+
// Second extension line is from (100,2) to (100,28)
189+
const measurement = createMeasurement(new Point(0,0), new Point(100,0), new Point(0,20));
190+
const testPoint = new Point(95, 15); // 5 units away horizontally from the second extension line
191+
192+
const distanceInfo = measurement.distanceTo(testPoint);
193+
expect(distanceInfo).not.toBeNull();
194+
expect(distanceInfo![0]).toBeCloseTo(5);
195+
expect(distanceInfo![1].ps.equalTo(testPoint)).toBe(true);
196+
expect(distanceInfo![1].pe.equalTo(new Point(100, 15))).toBe(true); // Closest point on segment (100,2)-(100,28)
197+
});
198+
});
199+
200+
describe('MeasurementEntity.containsPointOnShape', () => {
201+
const layerId = 'test-layer';
202+
203+
const createMeasurement = (
204+
start = new Point(0, 0),
205+
end = new Point(100, 0),
206+
offset = new Point(50, 50) // Offset above the line (positive y)
207+
) => new MeasurementEntity(layerId, start, end, offset);
208+
209+
it('should return true for a point on the main measurement line', () => {
210+
const measurement = createMeasurement();
211+
const drawPoints = (measurement as any).getDrawPoints();
212+
213+
const pointOnMainLineMid = new Point(
214+
(drawPoints.offsetStartPoint.x + drawPoints.offsetEndPoint.x) / 2,
215+
drawPoints.offsetStartPoint.y
216+
);
217+
expect(measurement.containsPointOnShape(pointOnMainLineMid)).toBe(true);
218+
expect(measurement.containsPointOnShape(drawPoints.offsetStartPoint.clone())).toBe(true); // Use clone for safety
219+
expect(measurement.containsPointOnShape(drawPoints.offsetEndPoint.clone())).toBe(true);
220+
});
221+
222+
it('should return true for a point on the first extension line', () => {
223+
const measurement = createMeasurement();
224+
const drawPoints = (measurement as any).getDrawPoints();
225+
226+
const pointOnExtLine1Mid = new Point(
227+
drawPoints.offsetStartPointMargin.x,
228+
(drawPoints.offsetStartPointMargin.y + drawPoints.offsetStartPointExtend.y) / 2
229+
);
230+
expect(measurement.containsPointOnShape(pointOnExtLine1Mid)).toBe(true);
231+
expect(measurement.containsPointOnShape(drawPoints.offsetStartPointMargin.clone())).toBe(true);
232+
expect(measurement.containsPointOnShape(drawPoints.offsetStartPointExtend.clone())).toBe(true);
233+
});
234+
235+
it('should return true for a point on the second extension line', () => {
236+
const measurement = createMeasurement();
237+
const drawPoints = (measurement as any).getDrawPoints();
238+
239+
const pointOnExtLine2Mid = new Point(
240+
drawPoints.offsetEndPointMargin.x,
241+
(drawPoints.offsetEndPointMargin.y + drawPoints.offsetEndPointExtend.y) / 2
242+
);
243+
expect(measurement.containsPointOnShape(pointOnExtLine2Mid)).toBe(true);
244+
expect(measurement.containsPointOnShape(drawPoints.offsetEndPointMargin.clone())).toBe(true);
245+
expect(measurement.containsPointOnShape(drawPoints.offsetEndPointExtend.clone())).toBe(true);
246+
});
247+
248+
it('should return false for a point not on any line', () => {
249+
const measurement = createMeasurement();
250+
const testPoint = new Point(500, 500); // Clearly off lines
251+
expect(measurement.containsPointOnShape(testPoint)).toBe(false);
252+
});
253+
254+
it('should return false for a point very close to a line but not on it', () => {
255+
const measurement = createMeasurement();
256+
const drawPoints = (measurement as any).getDrawPoints();
257+
// Point 0.1 units above the middle of the main line
258+
const pointNearMainLine = new Point(
259+
(drawPoints.offsetStartPoint.x + drawPoints.offsetEndPoint.x) / 2,
260+
drawPoints.offsetStartPoint.y + 0.1
261+
);
262+
expect(measurement.containsPointOnShape(pointNearMainLine)).toBe(false);
263+
264+
// Point 0.1 units to the side of the first extension line
265+
const pointNearExtLine1 = new Point(
266+
drawPoints.offsetStartPointMargin.x + 0.1,
267+
(drawPoints.offsetStartPointMargin.y + drawPoints.offsetStartPointExtend.y) / 2
268+
);
269+
expect(measurement.containsPointOnShape(pointNearExtLine1)).toBe(false);
270+
});
271+
272+
it('should return false if getDrawPoints returns null (e.g. zero-length measurement)', () => {
273+
const startPoint = new Point(10, 10);
274+
const offsetPoint = new Point(60, 50);
275+
// EndPoint is the same as startPoint
276+
const measurement = new MeasurementEntity(layerId, startPoint, startPoint.clone(), offsetPoint);
277+
const testPoint = new Point(10,10); // Test with the start point itself
278+
expect(measurement.containsPointOnShape(testPoint)).toBe(false);
279+
280+
const anotherTestPoint = new Point(50,50); // Test with an arbitrary point
281+
expect(measurement.containsPointOnShape(anotherTestPoint)).toBe(false);
282+
});
283+
284+
it('should correctly identify points on a measurement with negative offset (text below)', () => {
285+
const measurement = createMeasurement(
286+
new Point(0, 100), // Start
287+
new Point(100, 100), // End
288+
new Point(50, 50) // Offset below the line (y is smaller)
289+
);
290+
const drawPoints = (measurement as any).getDrawPoints();
291+
292+
const pointOnMainLineMid = new Point(
293+
(drawPoints.offsetStartPoint.x + drawPoints.offsetEndPoint.x) / 2,
294+
drawPoints.offsetStartPoint.y
295+
);
296+
expect(measurement.containsPointOnShape(pointOnMainLineMid)).toBe(true);
297+
expect(measurement.containsPointOnShape(drawPoints.offsetStartPoint.clone())).toBe(true);
298+
expect(measurement.containsPointOnShape(drawPoints.offsetEndPoint.clone())).toBe(true);
299+
300+
const pointOnExtLine1Mid = new Point(
301+
drawPoints.offsetStartPointMargin.x,
302+
(drawPoints.offsetStartPointMargin.y + drawPoints.offsetStartPointExtend.y) / 2
303+
);
304+
expect(measurement.containsPointOnShape(pointOnExtLine1Mid)).toBe(true);
305+
});
306+
307+
it('should correctly identify points on a vertical measurement', () => {
308+
const measurement = createMeasurement(
309+
new Point(50, 0), // Start
310+
new Point(50, 100), // End
311+
new Point(100, 50) // Offset to the right
312+
);
313+
const drawPoints = (measurement as any).getDrawPoints();
314+
315+
const pointOnMainLineMid = new Point(
316+
drawPoints.offsetStartPoint.x,
317+
(drawPoints.offsetStartPoint.y + drawPoints.offsetEndPoint.y) / 2
318+
);
319+
expect(measurement.containsPointOnShape(pointOnMainLineMid)).toBe(true);
320+
expect(measurement.containsPointOnShape(drawPoints.offsetStartPoint.clone())).toBe(true);
321+
expect(measurement.containsPointOnShape(drawPoints.offsetEndPoint.clone())).toBe(true);
322+
323+
const pointOnExtLine2Mid = new Point(
324+
(drawPoints.offsetEndPointMargin.x + drawPoints.offsetEndPointExtend.x) / 2,
325+
drawPoints.offsetEndPointMargin.y
326+
);
327+
expect(measurement.containsPointOnShape(pointOnExtLine2Mid)).toBe(true);
328+
});
329+
});

src/entities/MeasurementEntity.ts

Lines changed: 38 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -339,14 +339,14 @@ export class MeasurementEntity implements Entity {
339339
offsetEndPointMargin,
340340
} = drawPoints;
341341

342-
const lineStartToEnd = new Line(offsetStartPoint, offsetEndPoint);
343-
const horizontalLineDistanceInfo = lineStartToEnd.distanceTo(shape);
342+
const mainSegment = new Segment(offsetStartPoint, offsetEndPoint);
343+
const horizontalLineDistanceInfo = mainSegment.distanceTo(shape);
344344

345-
const leftVerticalLine = new Line(offsetStartPointMargin, offsetStartPointExtend);
346-
const leftVerticalLineDistanceInfo = leftVerticalLine.distanceTo(shape);
345+
const leftExtensionSegment = new Segment(offsetStartPointMargin, offsetStartPointExtend);
346+
const leftVerticalLineDistanceInfo = leftExtensionSegment.distanceTo(shape);
347347

348-
const rightVerticalLine = new Line(offsetEndPointMargin, offsetEndPointExtend);
349-
const rightVerticalLineDistanceInfo = rightVerticalLine.distanceTo(shape);
348+
const rightExtensionSegment = new Segment(offsetEndPointMargin, offsetEndPointExtend);
349+
const rightVerticalLineDistanceInfo = rightExtensionSegment.distanceTo(shape);
350350

351351
return minBy(
352352
[horizontalLineDistanceInfo, leftVerticalLineDistanceInfo, rightVerticalLineDistanceInfo],
@@ -369,9 +369,38 @@ export class MeasurementEntity implements Entity {
369369
}
370370

371371
// eslint-disable-next-line @typescript-eslint/no-unused-vars
372-
public containsPointOnShape(_point: Point): boolean {
373-
throw new Error('containsPointOnShape for MeasurementEntity not yet implemented');
374-
// return this.segment.contains(point);
372+
public containsPointOnShape(point: Point): boolean {
373+
const drawPoints = this.getDrawPoints();
374+
375+
if (!drawPoints) {
376+
return false; // No visual representation, so no point can be on it.
377+
}
378+
379+
const {
380+
offsetStartPoint,
381+
offsetEndPoint,
382+
offsetStartPointMargin,
383+
offsetStartPointExtend,
384+
offsetEndPointMargin,
385+
offsetEndPointExtend,
386+
} = drawPoints;
387+
388+
const measurementLine = new Segment(offsetStartPoint, offsetEndPoint);
389+
if (measurementLine.contains(point)) {
390+
return true;
391+
}
392+
393+
const extensionLine1 = new Segment(offsetStartPointMargin, offsetStartPointExtend);
394+
if (extensionLine1.contains(point)) {
395+
return true;
396+
}
397+
398+
const extensionLine2 = new Segment(offsetEndPointMargin, offsetEndPointExtend);
399+
if (extensionLine2.contains(point)) {
400+
return true;
401+
}
402+
403+
return false;
375404
}
376405

377406
public async toJson(): Promise<JsonEntity<MeasurementJsonData> | null> {

0 commit comments

Comments
 (0)