|
1 | 1 | import { describe, it, expect, vi, beforeEach } from 'vitest'; |
2 | | -import { Point, Vector } from '@flatten-js/core'; |
| 2 | +import { Point, Vector, Line, Box } from '@flatten-js/core'; |
| 3 | +import { round } from 'es-toolkit'; |
3 | 4 | import { MeasurementEntity } from './MeasurementEntity'; |
4 | | -import { MEASUREMENT_FONT_SIZE } from '../App.consts'; // For default options |
| 5 | +import { |
| 6 | + MEASUREMENT_FONT_SIZE, |
| 7 | + MEASUREMENT_DECIMAL_PLACES, |
| 8 | + MEASUREMENT_LABEL_OFFSET, |
| 9 | + EPSILON |
| 10 | +} from '../App.consts'; |
5 | 11 |
|
6 | 12 | // Mock dependencies from '../state.ts' |
7 | 13 | vi.mock('../state.ts', () => ({ |
@@ -95,6 +101,217 @@ describe('MeasurementEntity text orientation in draw() method', () => { |
95 | 101 | }); |
96 | 102 | }); |
97 | 103 |
|
| 104 | +describe('MeasurementEntity.getBoundingBox', () => { |
| 105 | + const layerId = 'test-layer'; // Changed to specified layerId |
| 106 | + |
| 107 | + it('should return a bounding box that includes the text label', () => { |
| 108 | + const startPoint = new Point(0, 0); |
| 109 | + const endPoint = new Point(100, 0); // Distance = 100 |
| 110 | + const offsetPoint = new Point(50, 50); // Text above the line |
| 111 | + |
| 112 | + const entity = new MeasurementEntity(layerId, startPoint, endPoint, offsetPoint); |
| 113 | + const actualBoundingBox: Box = entity.getBoundingBox(); |
| 114 | + |
| 115 | + // --- Start: Recalculate expected text properties (similar to getDrawPoints and draw) --- |
| 116 | + |
| 117 | + const lineStartToEnd = new Line(startPoint, endPoint); // Corrected Line creation |
| 118 | + const [, segmentToOffset] = offsetPoint.distanceTo(lineStartToEnd); |
| 119 | + const closestPointToOffsetOnLine = segmentToOffset.end; |
| 120 | + |
| 121 | + let vectorPerpendicularFromLineTowardsOffsetPoint: Vector; // Correct type |
| 122 | + if (closestPointToOffsetOnLine.equalTo(offsetPoint)) { |
| 123 | + // This case implies offsetPoint is on the line, so norm might be ambiguous. |
| 124 | + // For this specific test (50,50) and line (0,0)-(100,0), closestPointToOffsetOnLine is (50,0). |
| 125 | + // So the 'else' branch will be taken. |
| 126 | + // If offsetPoint was, for example, (50,0), then norm would be (0,1) or (0,-1) |
| 127 | + // The original implementation of getDrawPoints uses lineStartToEnd.norm in this case. |
| 128 | + // Let's assume standard orientation for norm (e.g. points "up" or "left" from segment direction) |
| 129 | + vectorPerpendicularFromLineTowardsOffsetPoint = lineStartToEnd.norm.clone(); |
| 130 | + // Check if the offsetPoint is "on the other side" of the norm |
| 131 | + // For horizontal line (0,0) to (100,0), norm is (0,1) |
| 132 | + // If offsetPoint was (50, -1), it's on the other side, so norm should be (0,-1) |
| 133 | + // This logic is complex and might need direct use of the offsetPoint if it's collinear |
| 134 | + // For this test case, offsetPoint is NOT on the line, so the else is fine. |
| 135 | + } else { |
| 136 | + vectorPerpendicularFromLineTowardsOffsetPoint = new Vector( |
| 137 | + closestPointToOffsetOnLine, |
| 138 | + offsetPoint |
| 139 | + ); |
| 140 | + } |
| 141 | + const normalUnit = vectorPerpendicularFromLineTowardsOffsetPoint.normalize(); |
| 142 | + |
| 143 | + // Points for horizontal measurement line (used to find its midpoint) |
| 144 | + const offsetStart = startPoint.translate(vectorPerpendicularFromLineTowardsOffsetPoint); |
| 145 | + const offsetEnd = endPoint.translate(vectorPerpendicularFromLineTowardsOffsetPoint); |
| 146 | + |
| 147 | + // Location for label |
| 148 | + const midpointMeasurementLine = new Point( |
| 149 | + (offsetStart.x + offsetEnd.x) / 2, |
| 150 | + (offsetStart.y + offsetEnd.y) / 2 |
| 151 | + ); |
| 152 | + |
| 153 | + // Using imported constants directly |
| 154 | + const totalOffsetText = MEASUREMENT_LABEL_OFFSET + MEASUREMENT_FONT_SIZE / 2; |
| 155 | + const midpointMeasurementLineOffset = midpointMeasurementLine |
| 156 | + .clone() |
| 157 | + .translate(normalUnit.multiply(totalOffsetText)); // This is the text center |
| 158 | + |
| 159 | + // Correct distance calculation and rounding |
| 160 | + const distanceVal = startPoint.distanceTo(endPoint)[0]; // distanceTo returns [distance, segment] |
| 161 | + const distanceString = round(distanceVal, MEASUREMENT_DECIMAL_PLACES).toString(); |
| 162 | + |
| 163 | + |
| 164 | + const textHeight = MEASUREMENT_FONT_SIZE; |
| 165 | + const textWidth = distanceString.length * MEASUREMENT_FONT_SIZE * 0.6; // As per implementation |
| 166 | + |
| 167 | + const originalTextDirection = normalUnit.rotate90CW(); |
| 168 | + let finalTextDirection = originalTextDirection.clone(); // Clone before potential modification |
| 169 | + if ( |
| 170 | + originalTextDirection.x < -EPSILON || // Using imported EPSILON |
| 171 | + (Math.abs(originalTextDirection.x) < EPSILON && originalTextDirection.y > EPSILON) |
| 172 | + ) { |
| 173 | + finalTextDirection = new Vector(-originalTextDirection.x, -originalTextDirection.y); |
| 174 | + } |
| 175 | + |
| 176 | + // Text center |
| 177 | + const textCenterX = midpointMeasurementLineOffset.x; |
| 178 | + const textCenterY = midpointMeasurementLineOffset.y; |
| 179 | + |
| 180 | + // Half dimensions |
| 181 | + const halfTextWidth = textWidth / 2; |
| 182 | + const halfTextHeight = textHeight / 2; |
| 183 | + |
| 184 | + // Text corner calculations |
| 185 | + // dirVec is along the finalTextDirection (for width) |
| 186 | + // perpVec is perpendicular to finalTextDirection (for height) |
| 187 | + const dirVec = finalTextDirection.normalize(); |
| 188 | + const perpVec = dirVec.rotate90CW(); // Perpendicular to text flow, for height offset |
| 189 | + |
| 190 | + const textCorners = [ |
| 191 | + new Point( // Top-left |
| 192 | + textCenterX - dirVec.x * halfTextWidth - perpVec.x * halfTextHeight, |
| 193 | + textCenterY - dirVec.y * halfTextWidth - perpVec.y * halfTextHeight |
| 194 | + ), |
| 195 | + new Point( // Top-right |
| 196 | + textCenterX + dirVec.x * halfTextWidth - perpVec.x * halfTextHeight, |
| 197 | + textCenterY + dirVec.y * halfTextWidth - perpVec.y * halfTextHeight |
| 198 | + ), |
| 199 | + new Point( // Bottom-right |
| 200 | + textCenterX + dirVec.x * halfTextWidth + perpVec.x * halfTextHeight, |
| 201 | + textCenterY + dirVec.y * halfTextWidth + perpVec.y * halfTextHeight |
| 202 | + ), |
| 203 | + new Point( // Bottom-left |
| 204 | + textCenterX - dirVec.x * halfTextWidth + perpVec.x * halfTextHeight, |
| 205 | + textCenterY - dirVec.y * halfTextWidth + perpVec.y * halfTextHeight |
| 206 | + ), |
| 207 | + ]; |
| 208 | + // --- End: Recalculate expected text properties --- |
| 209 | + |
| 210 | + // Assert that the actualBoundingBox contains all text corners |
| 211 | + // It's important to also consider that the bounding box might be larger due to the lines, |
| 212 | + // so we check that the box *at least* encompasses the text. |
| 213 | + const minTextX = Math.min(...textCorners.map(c => c.x)); |
| 214 | + const maxTextX = Math.max(...textCorners.map(c => c.x)); |
| 215 | + const minTextY = Math.min(...textCorners.map(c => c.y)); |
| 216 | + const maxTextY = Math.max(...textCorners.map(c => c.y)); |
| 217 | + |
| 218 | + expect(actualBoundingBox.xmin).toBeLessThanOrEqual(minTextX + EPSILON); // Add epsilon for float comparisons |
| 219 | + expect(actualBoundingBox.ymin).toBeLessThanOrEqual(minTextY + EPSILON); |
| 220 | + expect(actualBoundingBox.xmax).toBeGreaterThanOrEqual(maxTextX - EPSILON); |
| 221 | + expect(actualBoundingBox.ymax).toBeGreaterThanOrEqual(maxTextY - EPSILON); |
| 222 | + |
| 223 | + // For this specific case: |
| 224 | + // startPoint = (0,0), endPoint = (100,0), offsetPoint = (50,50) |
| 225 | + // vectorPerpendicularFromLineTowardsOffsetPoint = (0,50) |
| 226 | + // normalUnit = (0,1) |
| 227 | + // offsetStart = (0,50), offsetEnd = (100,50) |
| 228 | + // midpointMeasurementLine = (50,50) |
| 229 | + // totalOffsetText = MEASUREMENT_LABEL_OFFSET (20) + MEASUREMENT_FONT_SIZE/2 (20) = 40 |
| 230 | + // midpointMeasurementLineOffset (textCenter) = (50,50) + (0,1)*40 = (50,90) |
| 231 | + // distanceVal = 100. MEASUREMENT_DECIMAL_PLACES = 2. distanceString = "100.00" (length 6) |
| 232 | + // textHeight = 40. textWidth = 6 * 40 * 0.6 = 144 |
| 233 | + // originalTextDirection = (0,1).rotate90CW() = (1,0). finalTextDirection = (1,0) |
| 234 | + // dirVec = (1,0). perpVec = (0,1) |
| 235 | + // halfTextWidth = 72, halfTextHeight = 20 |
| 236 | + // textCenterX = 50, textCenterY = 90 |
| 237 | + // Corners: |
| 238 | + // TL: (50 - 1*72 - 0*20, 90 - 0*72 - 1*20) = (50 - 72, 90 - 20) = (-22, 70) |
| 239 | + // TR: (50 + 1*72 - 0*20, 90 + 0*72 - 1*20) = (50 + 72, 90 - 20) = (122, 70) |
| 240 | + // BR: (50 + 1*72 + 0*20, 90 + 0*72 + 1*20) = (50 + 72, 90 + 20) = (122, 110) |
| 241 | + // BL: (50 - 1*72 + 0*20, 90 - 0*72 + 1*20) = (50 - 72, 90 + 20) = (-22, 110) |
| 242 | + // minTextX = -22, maxTextX = 122, minTextY = 70, maxTextY = 110 |
| 243 | + |
| 244 | + // The line parts of the bounding box are: |
| 245 | + // drawPoints.offsetStartPointMargin, drawPoints.offsetStartPointExtend, |
| 246 | + // drawPoints.offsetEndPointMargin, drawPoints.offsetEndPointExtend, |
| 247 | + // drawPoints.offsetStartPoint, drawPoints.offsetEndPoint |
| 248 | + // offsetStartPoint = (0,50), offsetEndPoint = (100,50) |
| 249 | + // MEASUREMENT_ORIGIN_MARGIN = 20, MEASUREMENT_EXTENSION_LENGTH = 20 (from App.consts.ts - actually 8, but test used 20 before) |
| 250 | + // Let's use actual consts: EXTENSION_LENGTH = 8, ORIGIN_MARGIN = 20 |
| 251 | + // offsetStartPointMargin = startPoint.translate(normalUnit.multiply(MEASUREMENT_ORIGIN_MARGIN)) = (0,0) + (0,1)*20 = (0,20) |
| 252 | + // offsetStartPointExtend = offsetStartPoint.translate(normalUnit.multiply(MEASUREMENT_EXTENSION_LENGTH)) = (0,50) + (0,1)*8 = (0,58) |
| 253 | + // offsetEndPointMargin = endPoint.translate(normalUnit.multiply(MEASUREMENT_ORIGIN_MARGIN)) = (100,0) + (0,1)*20 = (100,20) |
| 254 | + // offsetEndPointExtend = offsetEndPoint.translate(normalUnit.multiply(MEASUREMENT_EXTENSION_LENGTH)) = (100,50) + (0,1)*8 = (100,58) |
| 255 | + // Line extreme points: (0,50), (100,50), (0,20), (0,58), (100,20), (100,58) |
| 256 | + // Line minX = 0, maxX = 100, minY = 20, maxY = 58 |
| 257 | + // Combined with text: |
| 258 | + // allExtremePoints.map(p => p.x): -22, 122, 0, 100 => minX = -22, maxX = 122 |
| 259 | + // allExtremePoints.map(p => p.y): 70, 110, 20, 58 => minY = 20, maxY = 110 |
| 260 | + // So, actualBoundingBox should be: Box{xmin: -22, ymin: 20, xmax: 122, ymax: 110} |
| 261 | + // The assertions check this: |
| 262 | + // actual.xmin (-22) <= minTextX (-22) (true) |
| 263 | + // actual.ymin (20) <= minTextY (70) (true) |
| 264 | + // actual.xmax (122) >= maxTextX (122) (true) |
| 265 | + // actual.ymax (110) >= maxTextY (110) (true) |
| 266 | + }); |
| 267 | +}); |
| 268 | + |
| 269 | + // Text center |
| 270 | + const textCenterX = midpointMeasurementLineOffset.x; |
| 271 | + const textCenterY = midpointMeasurementLineOffset.y; |
| 272 | + |
| 273 | + // Half dimensions |
| 274 | + const halfTextWidth = textWidth / 2; |
| 275 | + const halfTextHeight = textHeight / 2; |
| 276 | + |
| 277 | + // Text corner calculations |
| 278 | + const dirVec = finalTextDirection.normalize(); |
| 279 | + const perpVec = dirVec.rotate90CW(); |
| 280 | + |
| 281 | + const textCorners = [ |
| 282 | + new Point( |
| 283 | + textCenterX - dirVec.x * halfTextWidth - perpVec.x * halfTextHeight, |
| 284 | + textCenterY - dirVec.y * halfTextWidth - perpVec.y * halfTextHeight |
| 285 | + ), |
| 286 | + new Point( |
| 287 | + textCenterX + dirVec.x * halfTextWidth - perpVec.x * halfTextHeight, |
| 288 | + textCenterY + dirVec.y * halfTextWidth - perpVec.y * halfTextHeight |
| 289 | + ), |
| 290 | + new Point( |
| 291 | + textCenterX + dirVec.x * halfTextWidth + perpVec.x * halfTextHeight, |
| 292 | + textCenterY + dirVec.y * halfTextWidth + perpVec.y * halfTextHeight |
| 293 | + ), |
| 294 | + new Point( |
| 295 | + textCenterX - dirVec.x * halfTextWidth + perpVec.x * halfTextHeight, |
| 296 | + textCenterY - dirVec.y * halfTextWidth + perpVec.y * halfTextHeight |
| 297 | + ), |
| 298 | + ]; |
| 299 | + // --- End: Recalculate expected text properties --- |
| 300 | + |
| 301 | + // Assert that the actualBoundingBox contains all text corners |
| 302 | + textCorners.forEach(corner => { |
| 303 | + expect(actualBoundingBox.xmin).toBeLessThanOrEqual(corner.x); |
| 304 | + expect(actualBoundingBox.ymin).toBeLessThanOrEqual(corner.y); |
| 305 | + expect(actualBoundingBox.xmax).toBeGreaterThanOrEqual(corner.x); |
| 306 | + expect(actualBoundingBox.ymax).toBeGreaterThanOrEqual(corner.y); |
| 307 | + }); |
| 308 | + |
| 309 | + // Additionally, check if the box.contains(point) method works (if available and reliable) |
| 310 | + // For example: textCorners.forEach(corner => expect(actualBoundingBox.contains(corner)).toBe(true)); |
| 311 | + // However, checking min/max is more direct for this assertion. |
| 312 | + }); |
| 313 | +}); |
| 314 | + |
98 | 315 | describe('MeasurementEntity.distanceTo', () => { |
99 | 316 | const layerId = 'test-layer'; |
100 | 317 |
|
|
0 commit comments