|
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 |
2 | 3 | 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 |
5 | 6 | import {isEntityHighlighted, isEntitySelected} from '../state.ts'; |
6 | | -import {MeasurementEntity} from './MeasurementEntity'; // 1. Mocking for ../state.ts |
| 7 | +import {MeasurementEntity} from './MeasurementEntity'; |
7 | 8 |
|
8 | 9 | // 1. Mocking for ../state.ts |
9 | 10 | vi.mock('../state.ts', () => ({ |
@@ -117,6 +118,130 @@ describe('MeasurementEntity text orientation in draw() method', () => { |
117 | 118 | }); |
118 | 119 | }); |
119 | 120 |
|
| 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 | + |
120 | 245 | // 4. Test Suite: 'MeasurementEntity.distanceTo' |
121 | 246 | describe('MeasurementEntity.distanceTo', () => { |
122 | 247 | const layerId = 'mockLayerIdGlobal'; |
|
0 commit comments