| 287 | |
| 288 | // Extract rotation angle from CSS transform and writing-mode |
| 289 | const getRotation = (transform, writingMode) => { |
| 290 | let angle = 0; |
| 291 | |
| 292 | // Handle writing-mode first |
| 293 | // PowerPoint: 90° = text rotated 90° clockwise (reads top to bottom, letters upright) |
| 294 | // PowerPoint: 270° = text rotated 270° clockwise (reads bottom to top, letters upright) |
| 295 | if (writingMode === 'vertical-rl') { |
| 296 | // vertical-rl alone = text reads top to bottom = 90° in PowerPoint |
| 297 | angle = 90; |
| 298 | } else if (writingMode === 'vertical-lr') { |
| 299 | // vertical-lr alone = text reads bottom to top = 270° in PowerPoint |
| 300 | angle = 270; |
| 301 | } |
| 302 | |
| 303 | // Then add any transform rotation |
| 304 | if (transform && transform !== 'none') { |
| 305 | // Try to match rotate() function |
| 306 | const rotateMatch = transform.match(/rotate\((-?\d+(?:\.\d+)?)deg\)/); |
| 307 | if (rotateMatch) { |
| 308 | angle += parseFloat(rotateMatch[1]); |
| 309 | } else { |
| 310 | // Browser may compute as matrix - extract rotation from matrix |
| 311 | const matrixMatch = transform.match(/matrix\(([^)]+)\)/); |
| 312 | if (matrixMatch) { |
| 313 | const values = matrixMatch[1].split(',').map(parseFloat); |
| 314 | // matrix(a, b, c, d, e, f) where rotation = atan2(b, a) |
| 315 | const matrixAngle = Math.atan2(values[1], values[0]) * (180 / Math.PI); |
| 316 | angle += Math.round(matrixAngle); |
| 317 | } |
| 318 | } |
| 319 | } |
| 320 | |
| 321 | // Normalize to 0-359 range |
| 322 | angle = angle % 360; |
| 323 | if (angle < 0) angle += 360; |
| 324 | |
| 325 | return angle === 0 ? null : angle; |
| 326 | }; |
| 327 | |
| 328 | // Get position/dimensions accounting for rotation |
| 329 | const getPositionAndSize = (el, rect, rotation) => { |