Skip to content

Commit 9bd1f8b

Browse files
authored
Merge pull request #35 from cosmanovich/fix/measurement-selection
Fix: Improve measurement entity selection and distance calculation
2 parents d453241 + f9f2042 commit 9bd1f8b

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)