Skip to content

Commit 9048fef

Browse files
committed
feat: add bounding box test for measurement entity
1 parent cda05ac commit 9048fef

File tree

1 file changed

+129
-4
lines changed

1 file changed

+129
-4
lines changed

src/entities/MeasurementEntity.test.ts

Lines changed: 129 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
import {Point, type Vector} from '@flatten-js/core'; // Added Box, Segment for completeness
1+
import {type Box, Line, Point, Vector} from '@flatten-js/core'; // Added Box, Segment for completeness
2+
import {round} from 'es-toolkit'; // 1. Mocking for ../state.ts
23
import {beforeEach, describe, expect, it, type Mock, vi} from 'vitest';
3-
import {MEASUREMENT_FONT_SIZE} from '../App.consts';
4-
import type {DrawController} from '../drawControllers/DrawController.ts'; // Import mocked functions after the mock definition
4+
import {EPSILON, MEASUREMENT_DECIMAL_PLACES, MEASUREMENT_FONT_SIZE, MEASUREMENT_LABEL_OFFSET,} from '../App.consts';
5+
import type {DrawController} from '../drawControllers/DrawController.ts'; // Import mocked functions after the mock definition // Import mocked functions after the mock definition
56
import {isEntityHighlighted, isEntitySelected} from '../state.ts';
6-
import {MeasurementEntity} from './MeasurementEntity'; // 1. Mocking for ../state.ts
7+
import {MeasurementEntity} from './MeasurementEntity';
78

89
// 1. Mocking for ../state.ts
910
vi.mock('../state.ts', () => ({
@@ -117,6 +118,130 @@ describe('MeasurementEntity text orientation in draw() method', () => {
117118
});
118119
});
119120

121+
describe('MeasurementEntity.getBoundingBox', () => {
122+
const layerId = 'test-layer'; // Changed to specified layerId
123+
124+
it('should return a bounding box that includes the text label', () => {
125+
const startPoint = new Point(0, 0);
126+
const endPoint = new Point(100, 0); // Distance = 100
127+
const offsetPoint = new Point(50, 50); // Text above the line
128+
129+
const entity = new MeasurementEntity(layerId, startPoint, endPoint, offsetPoint);
130+
const actualBoundingBox: Box = entity.getBoundingBox();
131+
132+
// --- Start: Recalculate expected text properties (similar to getDrawPoints and draw) ---
133+
134+
const lineStartToEnd = new Line(startPoint, endPoint); // Corrected Line creation
135+
const [, segmentToOffset] = offsetPoint.distanceTo(lineStartToEnd);
136+
const closestPointToOffsetOnLine = segmentToOffset.end;
137+
138+
let vectorPerpendicularFromLineTowardsOffsetPoint: Vector; // Correct type
139+
if (closestPointToOffsetOnLine.equalTo(offsetPoint)) {
140+
// This case implies offsetPoint is on the line, so norm might be ambiguous.
141+
// For this specific test (50,50) and line (0,0)-(100,0), closestPointToOffsetOnLine is (50,0).
142+
// So the 'else' branch will be taken.
143+
// If offsetPoint was, for example, (50,0), then norm would be (0,1) or (0,-1)
144+
// The original implementation of getDrawPoints uses lineStartToEnd.norm in this case.
145+
// Let's assume standard orientation for norm (e.g. points "up" or "left" from segment direction)
146+
vectorPerpendicularFromLineTowardsOffsetPoint = lineStartToEnd.norm.clone();
147+
// Check if the offsetPoint is "on the other side" of the norm
148+
// For horizontal line (0,0) to (100,0), norm is (0,1)
149+
// If offsetPoint was (50, -1), it's on the other side, so norm should be (0,-1)
150+
// This logic is complex and might need direct use of the offsetPoint if it's collinear
151+
// For this test case, offsetPoint is NOT on the line, so the else is fine.
152+
} else {
153+
vectorPerpendicularFromLineTowardsOffsetPoint = new Vector(
154+
closestPointToOffsetOnLine,
155+
offsetPoint
156+
);
157+
}
158+
const normalUnit = vectorPerpendicularFromLineTowardsOffsetPoint.normalize();
159+
160+
// Points for horizontal measurement line (used to find its midpoint)
161+
const offsetStart = startPoint.translate(vectorPerpendicularFromLineTowardsOffsetPoint);
162+
const offsetEnd = endPoint.translate(vectorPerpendicularFromLineTowardsOffsetPoint);
163+
164+
// Location for label
165+
const midpointMeasurementLine = new Point(
166+
(offsetStart.x + offsetEnd.x) / 2,
167+
(offsetStart.y + offsetEnd.y) / 2
168+
);
169+
170+
// Using imported constants directly
171+
const totalOffsetText = MEASUREMENT_LABEL_OFFSET + MEASUREMENT_FONT_SIZE / 2;
172+
const midpointMeasurementLineOffset = midpointMeasurementLine
173+
.clone()
174+
.translate(normalUnit.multiply(totalOffsetText)); // This is the text center
175+
176+
// Correct distance calculation and rounding
177+
const distanceVal = startPoint.distanceTo(endPoint)[0]; // distanceTo returns [distance, segment]
178+
const distanceString = round(distanceVal, MEASUREMENT_DECIMAL_PLACES).toString();
179+
180+
const textHeight = MEASUREMENT_FONT_SIZE;
181+
const textWidth = distanceString.length * MEASUREMENT_FONT_SIZE * 0.6; // As per implementation
182+
183+
const originalTextDirection = normalUnit.rotate90CW();
184+
let finalTextDirection = originalTextDirection.clone(); // Clone before potential modification
185+
if (
186+
originalTextDirection.x < -EPSILON || // Using imported EPSILON
187+
(Math.abs(originalTextDirection.x) < EPSILON && originalTextDirection.y > EPSILON)
188+
) {
189+
finalTextDirection = new Vector(-originalTextDirection.x, -originalTextDirection.y);
190+
}
191+
192+
// Text center
193+
const textCenterX = midpointMeasurementLineOffset.x;
194+
const textCenterY = midpointMeasurementLineOffset.y;
195+
196+
// Half dimensions
197+
const halfTextWidth = textWidth / 2;
198+
const halfTextHeight = textHeight / 2;
199+
200+
// Text corner calculations
201+
// dirVec is along the finalTextDirection (for width)
202+
// perpVec is perpendicular to finalTextDirection (for height)
203+
const dirVec = finalTextDirection.normalize();
204+
const perpVec = dirVec.rotate90CW(); // Perpendicular to text flow, for height offset
205+
206+
const textCorners = [
207+
new Point(
208+
// Top-left
209+
textCenterX - dirVec.x * halfTextWidth - perpVec.x * halfTextHeight,
210+
textCenterY - dirVec.y * halfTextWidth - perpVec.y * halfTextHeight
211+
),
212+
new Point(
213+
// Top-right
214+
textCenterX + dirVec.x * halfTextWidth - perpVec.x * halfTextHeight,
215+
textCenterY + dirVec.y * halfTextWidth - perpVec.y * halfTextHeight
216+
),
217+
new Point(
218+
// Bottom-right
219+
textCenterX + dirVec.x * halfTextWidth + perpVec.x * halfTextHeight,
220+
textCenterY + dirVec.y * halfTextWidth + perpVec.y * halfTextHeight
221+
),
222+
new Point(
223+
// Bottom-left
224+
textCenterX - dirVec.x * halfTextWidth + perpVec.x * halfTextHeight,
225+
textCenterY - dirVec.y * halfTextWidth + perpVec.y * halfTextHeight
226+
),
227+
];
228+
// --- End: Recalculate expected text properties ---
229+
230+
// Assert that the actualBoundingBox contains all text corners
231+
// It's important to also consider that the bounding box might be larger due to the lines,
232+
// so we check that the box *at least* encompasses the text.
233+
const minTextX = Math.min(...textCorners.map((c) => c.x));
234+
const maxTextX = Math.max(...textCorners.map((c) => c.x));
235+
const minTextY = Math.min(...textCorners.map((c) => c.y));
236+
const maxTextY = Math.max(...textCorners.map((c) => c.y));
237+
238+
expect(actualBoundingBox.xmin).toBeLessThanOrEqual(minTextX + EPSILON); // Add epsilon for float comparisons
239+
expect(actualBoundingBox.ymin).toBeLessThanOrEqual(minTextY + EPSILON);
240+
expect(actualBoundingBox.xmax).toBeGreaterThanOrEqual(maxTextX - EPSILON);
241+
expect(actualBoundingBox.ymax).toBeGreaterThanOrEqual(maxTextY - EPSILON);
242+
});
243+
});
244+
120245
// 4. Test Suite: 'MeasurementEntity.distanceTo'
121246
describe('MeasurementEntity.distanceTo', () => {
122247
const layerId = 'mockLayerIdGlobal';

0 commit comments

Comments
 (0)