Skip to content

Commit 9643773

Browse files
authored
Merge pull request #33 from bertyhell/fix/measurement-text-orientation-17
Fix: Correct measurement text orientation
2 parents 72c9d4c + 11683e8 commit 9643773

File tree

3 files changed

+112
-4
lines changed

3 files changed

+112
-4
lines changed

src/drawControllers/screenCanvas.drawController.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -370,6 +370,7 @@ export class ScreenCanvasDrawController implements DrawController {
370370
this.context.font = `${opts.fontSize}px ${opts.fontFamily}`;
371371
this.context.textAlign = opts.textAlign;
372372
this.context.fillStyle = opts.textColor;
373+
this.context.textBaseline = 'middle';
373374
this.context.fillText(label, 0, 0);
374375
this.context.restore();
375376
}
@@ -467,9 +468,9 @@ export class ScreenCanvasDrawController implements DrawController {
467468
this.context.beginPath();
468469
screenPoints.forEach((screenPoint, index) => {
469470
if (index === 0) {
470-
this.context.moveTo(screenPoint.x, screenPoint.y);
471+
this.context.moveTo(screenPoint.x, this.canvasSize.y - screenPoint.y);
471472
} else {
472-
this.context.lineTo(screenPoint.x, screenPoint.y);
473+
this.context.lineTo(screenPoint.x, this.canvasSize.y - screenPoint.y);
473474
}
474475
});
475476
this.context.closePath();
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import { describe, it, expect, vi, beforeEach } from 'vitest';
2+
import { Point, Vector } from '@flatten-js/core';
3+
import { MeasurementEntity } from './MeasurementEntity';
4+
import { MEASUREMENT_FONT_SIZE } from '../App.consts'; // For default options
5+
6+
// Mock dependencies from '../state.ts'
7+
vi.mock('../state.ts', () => ({
8+
getActiveLayerId: () => 'mockLayerId',
9+
isEntityHighlighted: () => false,
10+
isEntitySelected: () => false,
11+
}));
12+
13+
describe('MeasurementEntity text orientation in draw() method', () => {
14+
// Mock DrawController
15+
const mockDrawController = {
16+
drawText: vi.fn(),
17+
setLineStyles: vi.fn(),
18+
setFillStyles: vi.fn(),
19+
drawLine: vi.fn(),
20+
fillPolygon: vi.fn(),
21+
getScreenScale: vi.fn().mockReturnValue(1), // Used by drawArrowHead
22+
// Add any other methods called by MeasurementEntity.draw or its private helpers like drawArrowHead
23+
// For now, these cover the primary interactions.
24+
};
25+
26+
beforeEach(() => {
27+
// Reset mocks before each test
28+
mockDrawController.drawText.mockClear();
29+
mockDrawController.setLineStyles.mockClear();
30+
mockDrawController.setFillStyles.mockClear();
31+
mockDrawController.drawLine.mockClear();
32+
mockDrawController.fillPolygon.mockClear();
33+
mockDrawController.getScreenScale.mockClear().mockReturnValue(1);
34+
});
35+
36+
const runTest = (startPoint: Point, endPoint: Point, offsetPoint: Point, expectedDirectionX: number, expectedDirectionY: number) => {
37+
const measurement = new MeasurementEntity('mockLayerId', startPoint, endPoint, offsetPoint);
38+
measurement.lineColor = '#fff'; // Set a default color
39+
measurement.draw(mockDrawController as any);
40+
41+
expect(mockDrawController.drawText).toHaveBeenCalledOnce();
42+
const callArgs = mockDrawController.drawText.mock.calls[0];
43+
const textOptions = callArgs[2]; // Third argument to drawText is the options object
44+
const actualDirection = textOptions.textDirection as Vector;
45+
46+
// Use a small epsilon for comparing floating point vector components
47+
const epsilon = 1e-5;
48+
expect(actualDirection.x).toBeCloseTo(expectedDirectionX, epsilon);
49+
expect(actualDirection.y).toBeCloseTo(expectedDirectionY, epsilon);
50+
expect(textOptions.textAlign).toBe('center');
51+
expect(textOptions.fontSize).toBe(MEASUREMENT_FONT_SIZE);
52+
expect(textOptions.textColor).toBe('#fff');
53+
};
54+
55+
it('should orient text left-to-right for horizontal line, text below', () => {
56+
// normalUnit = (0, -1) => originalTextDirection = (-1, 0) => finalTextDirection = (1, 0)
57+
runTest(new Point(0, 0), new Point(10, 0), new Point(5, -5), 1, 0);
58+
});
59+
60+
it('should orient text left-to-right for horizontal line, text above', () => {
61+
// normalUnit = (0, 1) => originalTextDirection = (1, 0) => finalTextDirection = (1, 0)
62+
runTest(new Point(0, 0), new Point(10, 0), new Point(5, 5), 1, 0);
63+
});
64+
65+
it('should orient text bottom-to-top for vertical line, text right', () => {
66+
// normalUnit = (1, 0) => originalTextDirection = (0, -1) => finalTextDirection = (0, -1)
67+
runTest(new Point(0, 0), new Point(0, 10), new Point(5, 5), 0, -1);
68+
});
69+
70+
it('should orient text bottom-to-top for vertical line, text left (flips from top-to-bottom)', () => {
71+
// normalUnit = (-1, 0) => originalTextDirection = (0, 1) => finalTextDirection = (0, -1)
72+
runTest(new Point(0, 0), new Point(0, 10), new Point(-5, 5), 0, -1);
73+
});
74+
75+
it('should orient text correctly for a 45 degree line, offset "below-right"', () => {
76+
// Line from (0,0) to (10,10). Offset to (10,0) (perpendicularly "downwards")
77+
// normalUnit is approx (0.707, -0.707)
78+
// originalTextDirection = normalUnit.rotate90CW() = (-0.707, -0.707) (points towards Q3)
79+
// Expected finalTextDirection should be (0.707, 0.707) (points towards Q1)
80+
runTest(new Point(0,0), new Point(10,10), new Point(10,0), Math.sqrt(2)/2, Math.sqrt(2)/2);
81+
});
82+
83+
it('should orient text correctly for a -45 degree line, offset "above-right"', () => {
84+
// Line from (0,0) to (10,-10). Offset to (10,0) (perpendicularly "upwards")
85+
// normalUnit is approx (0.707, 0.707)
86+
// originalTextDirection = normalUnit.rotate90CW() = (0.707, -0.707) (points towards Q4)
87+
// Expected finalTextDirection should be (0.707, -0.707)
88+
runTest(new Point(0,0), new Point(10,-10), new Point(10,0), Math.sqrt(2)/2, -Math.sqrt(2)/2);
89+
});
90+
91+
it('should not draw text if start and end points are the same', () => {
92+
const measurement = new MeasurementEntity('mockLayerId', new Point(0,0), new Point(0,0), new Point(5,5));
93+
measurement.draw(mockDrawController as any);
94+
expect(mockDrawController.drawText).not.toHaveBeenCalled();
95+
});
96+
});

src/entities/MeasurementEntity.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
MEASUREMENT_LABEL_OFFSET,
1111
MEASUREMENT_ORIGIN_MARGIN,
1212
TO_RADIANS,
13+
EPSILON,
1314
} from '../App.consts';
1415
import type {Shape, SnapPoint} from '../App.types';
1516
import type {DrawController} from '../drawControllers/DrawController';
@@ -96,10 +97,12 @@ export class MeasurementEntity implements Entity {
9697
(offsetStartPoint.x + offsetEndPoint.x) / 2,
9798
(offsetStartPoint.y + offsetEndPoint.y) / 2
9899
);
100+
const textHeight = MEASUREMENT_FONT_SIZE;
101+
const totalOffset = MEASUREMENT_LABEL_OFFSET + textHeight / 2;
99102
const midpointMeasurementLineOffset = midpointMeasurementLine
100103
.clone()
101104
.translate(
102-
vectorPerpendicularFromLineTowardsOffsetPointUnit.multiply(MEASUREMENT_LABEL_OFFSET)
105+
vectorPerpendicularFromLineTowardsOffsetPointUnit.multiply(totalOffset)
103106
);
104107

105108
return {
@@ -200,9 +203,17 @@ export class MeasurementEntity implements Entity {
200203
const distance = String(
201204
round(pointDistance(this.startPoint, this.endPoint), MEASUREMENT_DECIMAL_PLACES)
202205
);
206+
const originalTextDirection = normalUnit.rotate90CW();
207+
let finalTextDirection = originalTextDirection;
208+
if (
209+
originalTextDirection.x < -EPSILON ||
210+
(Math.abs(originalTextDirection.x) < EPSILON && originalTextDirection.y > EPSILON)
211+
) {
212+
finalTextDirection = new Vector(-originalTextDirection.x, -originalTextDirection.y);
213+
}
203214
drawController.drawText(distance, midpointMeasurementLineOffset, {
204215
textAlign: 'center',
205-
textDirection: normalUnit.rotate90CW(),
216+
textDirection: finalTextDirection,
206217
fontSize: MEASUREMENT_FONT_SIZE,
207218
textColor: this.lineColor,
208219
});

0 commit comments

Comments
 (0)