From 56f1a2774b7f9834603d4cde9e10a186891c5895 Mon Sep 17 00:00:00 2001 From: vladtechno Date: Mon, 18 May 2026 00:25:59 +1000 Subject: [PATCH] feat(damage-flow): rotate zoomed detail view to natural orientation (90/-90/180 per zone) --- .../frontend/public/data/zone_bboxes.json | 171 ++++++++++++------ .../components/damage-flow/ZoneDetailView.tsx | 69 +++++-- 2 files changed, 164 insertions(+), 76 deletions(-) diff --git a/mechanic-pwa/frontend/public/data/zone_bboxes.json b/mechanic-pwa/frontend/public/data/zone_bboxes.json index 1d714da..95c42bb 100644 --- a/mechanic-pwa/frontend/public/data/zone_bboxes.json +++ b/mechanic-pwa/frontend/public/data/zone_bboxes.json @@ -3,342 +3,399 @@ "x": 278.11, "y": 88.77, "w": 262.74, - "h": 45.43 + "h": 45.43, + "rotation": 0 }, "52": { "x": 699.8, "y": 296.93, "w": 41.31, - "h": 70.61 + "h": 70.61, + "rotation": -90 }, "51": { "x": 81.04, "y": 295.79, "w": 41.4, - "h": 71.68 + "h": 71.68, + "rotation": 90 }, "1": { "x": 488.5, "y": 132.88, "w": 50.92, - "h": 20.77 + "h": 20.77, + "rotation": 0 }, "2": { "x": 279.61, "y": 132.82, "w": 50.93, - "h": 21.24 + "h": 21.24, + "rotation": 0 }, "3": { "x": 350.0, "y": 130.0, "w": 120.0, - "h": 22.0 + "h": 22.0, + "rotation": 0 }, "30": { "x": 509.22, "y": 734.61, "w": 130.89, - "h": 41.63 + "h": 41.63, + "rotation": 0 }, "31": { "x": 182.12, "y": 734.5, "w": 129.26, - "h": 41.56 + "h": 41.56, + "rotation": 0 }, "28": { "x": 170.41, "y": 496.19, "w": 6.35, - "h": 8.34 + "h": 8.34, + "rotation": 90 }, "29": { "x": 645.28, "y": 496.19, "w": 6.53, - "h": 8.58 + "h": 8.58, + "rotation": -90 }, "11": { "x": 278.5, "y": 477.86, "w": 465.53, - "h": 152.09 + "h": 152.09, + "rotation": 0 }, "10": { "x": 78.52, "y": 478.98, "w": 91.6, - "h": 150.99 + "h": 150.99, + "rotation": 90 }, "4": { "x": 300.72, "y": 311.52, "w": 219.3, - "h": 146.72 + "h": 146.72, + "rotation": 0 }, "5": { "x": 305.05, "y": 442.76, "w": 210.67, - "h": 127.13 + "h": 127.13, + "rotation": 0 }, "22": { "x": 281.48, "y": 1063.81, "w": 260.16, - "h": 59.34 + "h": 59.34, + "rotation": 0 }, "49": { "x": 87.78, "y": 826.98, "w": 50.54, - "h": 99.5 + "h": 99.5, + "rotation": 90 }, "50": { "x": 684.83, "y": 827.25, "w": 49.78, - "h": 99.71 + "h": 99.71, + "rotation": -90 }, "33": { "x": 284.24, "y": 1038.88, "w": 29.65, - "h": 22.7 + "h": 22.7, + "rotation": 0 }, "32": { "x": 507.74, "y": 1038.78, "w": 29.66, - "h": 22.69 + "h": 22.69, + "rotation": 0 }, "21": { "x": 303.29, "y": 841.39, "w": 215.88, - "h": 236.13 + "h": 236.13, + "rotation": 0 }, "38": { "x": 510.42, "y": 195.83, "w": 26.67, - "h": 821.6 + "h": 821.6, + "rotation": -90 }, "39": { "x": 281.9, "y": 195.94, "w": 26.67, - "h": 14.24 + "h": 14.24, + "rotation": 0 }, "23": { "x": 316.23, "y": 766.98, "w": 187.94, - "h": 89.25 + "h": 89.25, + "rotation": 0 }, "6": { "x": 78.13, "y": 314.95, "w": 91.98, - "h": 168.83 + "h": 168.83, + "rotation": 90 }, "7": { "x": 651.97, "y": 314.84, "w": 92.1, - "h": 168.93 + "h": 168.93, + "rotation": -90 }, "8": { "x": 81.05, "y": 734.43, "w": 104.0, - "h": 176.79 + "h": 176.79, + "rotation": 90 }, "9": { "x": 590.75, "y": 666.97, "w": 150.45, - "h": 244.25 + "h": 244.25, + "rotation": -90 }, "12": { "x": 81.53, "y": 628.12, "w": 99.38, - "h": 147.01 + "h": 147.01, + "rotation": 90 }, "13": { "x": 641.53, "y": 627.6, "w": 99.42, - "h": 147.14 + "h": 147.14, + "rotation": -90 }, "16": { "x": 325.94, "y": 563.11, "w": 168.93, - "h": 208.89 + "h": 208.89, + "rotation": 90 }, "14": { "x": 72.11, "y": 471.33, "w": 5.0, - "h": 161.47 + "h": 161.47, + "rotation": 90 }, "47": { "x": 72.83, "y": 615.64, "w": 5.6, - "h": 124.59 + "h": 124.59, + "rotation": 90 }, "15": { "x": 744.0, "y": 461.0, "w": 6.0, - "h": 171.5 + "h": 171.5, + "rotation": -90 }, "48": { "x": 744.0, "y": 632.5, "w": 6.0, - "h": 107.5 + "h": 107.5, + "rotation": -90 }, "27": { "x": 596.56, "y": 643.76, "w": 50.37, - "h": 84.35 + "h": 84.35, + "rotation": -90 }, "26": { "x": 175.44, "y": 643.75, "w": 50.57, - "h": 84.4 + "h": 84.4, + "rotation": 90 }, "24": { "x": 171.07, "y": 526.12, "w": 53.81, - "h": 105.33 + "h": 105.33, + "rotation": 90 }, "25": { "x": 597.45, "y": 526.06, "w": 53.57, - "h": 105.33 + "h": 105.33, + "rotation": -90 }, "18": { "x": 42.7, "y": 369.77, "w": 89.47, - "h": 88.82 + "h": 88.82, + "rotation": 0 }, "43": { "x": 56.05, "y": 382.97, "w": 63.97, - "h": 63.6 + "h": 63.6, + "rotation": 0 }, "17": { "x": 688.66, "y": 369.75, "w": 89.51, - "h": 88.83 + "h": 88.83, + "rotation": 0 }, "42": { "x": 702.04, "y": 382.95, "w": 63.97, - "h": 63.61 + "h": 63.61, + "rotation": 0 }, "20": { "x": 42.55, "y": 751.33, "w": 90.63, - "h": 88.87 + "h": 88.87, + "rotation": 0 }, "40": { "x": 56.71, "y": 764.4, "w": 61.8, - "h": 62.63 + "h": 62.63, + "rotation": 0 }, "19": { "x": 688.96, "y": 751.25, "w": 89.2, - "h": 88.92 + "h": 88.92, + "rotation": 0 }, "41": { "x": 701.6, "y": 763.49, "w": 64.65, - "h": 64.46 + "h": 64.46, + "rotation": 0 }, "44": { "x": 42.66, "y": 0.0, "w": 426.67, - "h": 512.0 + "h": 512.0, + "rotation": 90 }, "45": { "x": 42.66, "y": 0.0, "w": 426.67, - "h": 512.0 + "h": 512.0, + "rotation": 90 }, "46": { "x": 42.66, "y": 0.0, "w": 426.67, - "h": 512.0 + "h": 512.0, + "rotation": 90 }, "34": { "x": 0.0, "y": 76.0, "w": 512.0, - "h": 360.0 + "h": 360.0, + "rotation": 0 }, "35": { "x": 0.0, "y": 76.0, "w": 512.0, - "h": 360.0 + "h": 360.0, + "rotation": 0 }, "36": { "x": 0.0, "y": 76.0, "w": 512.0, - "h": 360.0 + "h": 360.0, + "rotation": 0 }, "37": { "x": 0.0, "y": 76.0, "w": 512.0, - "h": 360.0 + "h": 360.0, + "rotation": 0 }, "53": { "x": 185.83, "y": 719.15, "w": 42.97, - "h": 94.96 + "h": 94.96, + "rotation": 90 }, "54": { "x": 594.89, "y": 723.2, "w": 46.0, - "h": 101.37 + "h": 101.37, + "rotation": -90 }, "55": { "x": 170.0, "y": 450.0, "w": 60.0, - "h": 130.0 + "h": 130.0, + "rotation": 90 }, "56": { "x": 595.0, "y": 450.0, "w": 55.0, - "h": 130.0 + "h": 130.0, + "rotation": -90 } } \ No newline at end of file diff --git a/mechanic-pwa/frontend/src/components/damage-flow/ZoneDetailView.tsx b/mechanic-pwa/frontend/src/components/damage-flow/ZoneDetailView.tsx index c22244c..6619180 100644 --- a/mechanic-pwa/frontend/src/components/damage-flow/ZoneDetailView.tsx +++ b/mechanic-pwa/frontend/src/components/damage-flow/ZoneDetailView.tsx @@ -11,6 +11,7 @@ interface ZoneBBox { y: number; w: number; h: number; + rotation?: number; // degrees, 0 / 90 / -90 / 180 } interface Props { @@ -36,13 +37,30 @@ async function loadBBoxes(): Promise> { } function paddedViewBox(b: ZoneBBox): string { - const padX = b.w * PADDING; - const padY = b.h * PADDING; - const x = b.x - padX; - const y = b.y - padY; - const w = b.w + 2 * padX; - const h = b.h + 2 * padY; - return `${x} ${y} ${w} ${h}`; + const rotation = b.rotation ?? 0; + // For 90°/-90° rotation, after wrapping content in , + // the rotated bbox dims swap (w↔h). Center stays at (cx, cy). + const cx = b.x + b.w / 2; + const cy = b.y + b.h / 2; + let w = b.w; + let h = b.h; + if (rotation === 90 || rotation === -90) { + w = b.h; + h = b.w; + } + const padX = w * PADDING; + const padY = h * PADDING; + const minX = cx - w / 2 - padX; + const minY = cy - h / 2 - padY; + return `${minX} ${minY} ${w + 2 * padX} ${h + 2 * padY}`; +} + +function rotationTransform(b: ZoneBBox): string | null { + const rotation = b.rotation ?? 0; + if (rotation === 0) return null; + const cx = b.x + b.w / 2; + const cy = b.y + b.h / 2; + return `rotate(${rotation} ${cx} ${cy})`; } export function ZoneDetailView({ open, zoneId, zoneLabel, onConfirm, onCancel }: Props) { @@ -65,27 +83,40 @@ export function ZoneDetailView({ open, zoneId, zoneLabel, onConfirm, onCancel }: ]).then(([text, bboxes]) => { if (cancelled) return; const b = bboxes[String(zoneId)]; - if (b) setViewBox(paddedViewBox(b)); - else setViewBox(`0 0 ${COMPOSITE_W} ${COMPOSITE_H}`); // fallback to full view + const finalVB = b ? paddedViewBox(b) : `0 0 ${COMPOSITE_W} ${COMPOSITE_H}`; + const rotXform = b ? rotationTransform(b) : null; + setViewBox(finalVB); - // Rewrite the SVG root viewBox + strip script and set fills BEFORE mounting, - // so the SVG is correct from the very first paint (no flash of full unfolded view). - const finalVB = bboxes[String(zoneId)] ? paddedViewBox(bboxes[String(zoneId)]) : `0 0 ${COMPOSITE_W} ${COMPOSITE_H}`; + // Rewrite SVG BEFORE mounting so first paint is already zoomed + rotated correctly. let mutated = text; // strip mutated = mutated.replace(//gi, ""); - // replace viewBox attribute on the root opening tag + // replace viewBox on root mutated = mutated.replace(/(]*?)\sviewBox\s*=\s*"[^"]*"/i, `$1 viewBox="${finalVB}"`); - // ensure preserveAspectRatio is present - if (!/preserveAspectRatio/i.test(mutated.slice(0, 400))) { - mutated = mutated.replace(/]*?)\s(?:width|height)\s*=\s*"[^"]*"/gi, "$1" ); - mutated = mutated.replace(/ in a if rotation needed. + // This makes rotation visible AND keeps getScreenCTM correct for tap conversion. + if (rotXform) { + // Find the end of the root opening tag and the closing ; wrap everything between. + const openMatch = mutated.match(/]*?>/i); + const closeIdx = mutated.lastIndexOf(""); + if (openMatch && closeIdx > -1) { + const openEnd = (openMatch.index ?? 0) + openMatch[0].length; + const before = mutated.slice(0, openEnd); + const inner = mutated.slice(openEnd, closeIdx); + const after = mutated.slice(closeIdx); + mutated = `${before}${inner}${after}`; + } + } setSvgText(mutated); });