fix(damage-flow): rotation round-trip (store original-frame coords, display in rotated view)
This commit is contained in:
@@ -29,6 +29,47 @@ const COMPOSITE_H = 1209;
|
|||||||
// Module-level cache for the bbox map
|
// Module-level cache for the bbox map
|
||||||
let bboxCache: Record<string, ZoneBBox> | null = null;
|
let bboxCache: Record<string, ZoneBBox> | null = null;
|
||||||
|
|
||||||
|
/** Convert tap point from rotated/visible viewBox frame back to original composite frame. */
|
||||||
|
function inverseRotateAroundBboxCenter(
|
||||||
|
pt: { x: number; y: number },
|
||||||
|
b: ZoneBBox
|
||||||
|
): { x: number; y: number } {
|
||||||
|
const rot = b.rotation ?? 0;
|
||||||
|
if (rot === 0) return pt;
|
||||||
|
const cx = b.x + b.w / 2;
|
||||||
|
const cy = b.y + b.h / 2;
|
||||||
|
const dx = pt.x - cx;
|
||||||
|
const dy = pt.y - cy;
|
||||||
|
// Inverse rotation (CCW by `rot` degrees, or CW by `-rot`)
|
||||||
|
const rad = (-rot * Math.PI) / 180;
|
||||||
|
const cos = Math.cos(rad);
|
||||||
|
const sin = Math.sin(rad);
|
||||||
|
return {
|
||||||
|
x: cx + dx * cos - dy * sin,
|
||||||
|
y: cy + dx * sin + dy * cos,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Apply forward rotation to a stored composite point so it appears at the correct visual position in the rotated detail view. */
|
||||||
|
function forwardRotateAroundBboxCenter(
|
||||||
|
pt: { x: number; y: number },
|
||||||
|
b: ZoneBBox
|
||||||
|
): { x: number; y: number } {
|
||||||
|
const rot = b.rotation ?? 0;
|
||||||
|
if (rot === 0) return pt;
|
||||||
|
const cx = b.x + b.w / 2;
|
||||||
|
const cy = b.y + b.h / 2;
|
||||||
|
const dx = pt.x - cx;
|
||||||
|
const dy = pt.y - cy;
|
||||||
|
const rad = (rot * Math.PI) / 180;
|
||||||
|
const cos = Math.cos(rad);
|
||||||
|
const sin = Math.sin(rad);
|
||||||
|
return {
|
||||||
|
x: cx + dx * cos - dy * sin,
|
||||||
|
y: cy + dx * sin + dy * cos,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
async function loadBBoxes(): Promise<Record<string, ZoneBBox>> {
|
async function loadBBoxes(): Promise<Record<string, ZoneBBox>> {
|
||||||
if (bboxCache) return bboxCache;
|
if (bboxCache) return bboxCache;
|
||||||
const res = await fetch("/data/zone_bboxes.json");
|
const res = await fetch("/data/zone_bboxes.json");
|
||||||
@@ -68,6 +109,7 @@ export function ZoneDetailView({ open, zoneId, zoneLabel, onConfirm, onCancel }:
|
|||||||
const [svgText, setSvgText] = useState<string | null>(null);
|
const [svgText, setSvgText] = useState<string | null>(null);
|
||||||
const [points, setPoints] = useState<Point[]>([]);
|
const [points, setPoints] = useState<Point[]>([]);
|
||||||
const [viewBox, setViewBox] = useState<string | null>(null);
|
const [viewBox, setViewBox] = useState<string | null>(null);
|
||||||
|
const [zoneBBox, setZoneBBox] = useState<ZoneBBox | null>(null);
|
||||||
|
|
||||||
// Load SVG + precomputed bboxes when opening, and pick this zone's viewBox up-front
|
// Load SVG + precomputed bboxes when opening, and pick this zone's viewBox up-front
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -86,6 +128,7 @@ export function ZoneDetailView({ open, zoneId, zoneLabel, onConfirm, onCancel }:
|
|||||||
const finalVB = b ? paddedViewBox(b) : `0 0 ${COMPOSITE_W} ${COMPOSITE_H}`;
|
const finalVB = b ? paddedViewBox(b) : `0 0 ${COMPOSITE_W} ${COMPOSITE_H}`;
|
||||||
const rotXform = b ? rotationTransform(b) : null;
|
const rotXform = b ? rotationTransform(b) : null;
|
||||||
setViewBox(finalVB);
|
setViewBox(finalVB);
|
||||||
|
setZoneBBox(b ?? null);
|
||||||
|
|
||||||
// Rewrite SVG BEFORE mounting so first paint is already zoomed + rotated correctly.
|
// Rewrite SVG BEFORE mounting so first paint is already zoomed + rotated correctly.
|
||||||
let mutated = text;
|
let mutated = text;
|
||||||
@@ -164,10 +207,17 @@ export function ZoneDetailView({ open, zoneId, zoneLabel, onConfirm, onCancel }:
|
|||||||
pt.y = e.clientY;
|
pt.y = e.clientY;
|
||||||
const ctm = svg.getScreenCTM();
|
const ctm = svg.getScreenCTM();
|
||||||
if (!ctm) return;
|
if (!ctm) return;
|
||||||
|
// svgPt is in the SVG root user-space (viewBox space), which is the rotated frame.
|
||||||
const svgPt = pt.matrixTransform(ctm.inverse());
|
const svgPt = pt.matrixTransform(ctm.inverse());
|
||||||
|
|
||||||
const x = svgPt.x / COMPOSITE_W;
|
// Convert back to original composite frame (un-rotate) so stored coord lines up
|
||||||
const y = svgPt.y / COMPOSITE_H;
|
// with the position on the unrotated overview SVG.
|
||||||
|
const original = zoneBBox
|
||||||
|
? inverseRotateAroundBboxCenter({ x: svgPt.x, y: svgPt.y }, zoneBBox)
|
||||||
|
: { x: svgPt.x, y: svgPt.y };
|
||||||
|
|
||||||
|
const x = original.x / COMPOSITE_W;
|
||||||
|
const y = original.y / COMPOSITE_H;
|
||||||
const cx = Math.max(0, Math.min(1, x));
|
const cx = Math.max(0, Math.min(1, x));
|
||||||
const cy = Math.max(0, Math.min(1, y));
|
const cy = Math.max(0, Math.min(1, y));
|
||||||
setPoints((prev) => [...prev, { x: cx, y: cy }]);
|
setPoints((prev) => [...prev, { x: cx, y: cy }]);
|
||||||
@@ -199,6 +249,7 @@ export function ZoneDetailView({ open, zoneId, zoneLabel, onConfirm, onCancel }:
|
|||||||
<PointOverlay
|
<PointOverlay
|
||||||
points={points}
|
points={points}
|
||||||
viewBox={viewBox}
|
viewBox={viewBox}
|
||||||
|
zoneBBox={zoneBBox}
|
||||||
onRemoveLast={() => setPoints((p) => p.slice(0, -1))}
|
onRemoveLast={() => setPoints((p) => p.slice(0, -1))}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -240,18 +291,24 @@ export function ZoneDetailView({ open, zoneId, zoneLabel, onConfirm, onCancel }:
|
|||||||
interface PointOverlayProps {
|
interface PointOverlayProps {
|
||||||
points: Point[];
|
points: Point[];
|
||||||
viewBox: string;
|
viewBox: string;
|
||||||
|
zoneBBox: ZoneBBox | null;
|
||||||
onRemoveLast: () => void;
|
onRemoveLast: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function PointOverlay({ points, viewBox, onRemoveLast }: PointOverlayProps) {
|
function PointOverlay({ points, viewBox, zoneBBox, onRemoveLast }: PointOverlayProps) {
|
||||||
const [minX, minY, w, h] = viewBox.split(/\s+/).map(Number);
|
const [minX, minY, w, h] = viewBox.split(/\s+/).map(Number);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{points.map((p, i) => {
|
{points.map((p, i) => {
|
||||||
const xVB = p.x * COMPOSITE_W;
|
// Stored point is in ORIGINAL composite frame (un-rotated).
|
||||||
const yVB = p.y * COMPOSITE_H;
|
// Apply forward rotation to map it back into the visible rotated frame so the dot
|
||||||
const xPct = ((xVB - minX) / w) * 100;
|
// appears at the position the user tapped.
|
||||||
const yPct = ((yVB - minY) / h) * 100;
|
const original = { x: p.x * COMPOSITE_W, y: p.y * COMPOSITE_H };
|
||||||
|
const rotated = zoneBBox
|
||||||
|
? forwardRotateAroundBboxCenter(original, zoneBBox)
|
||||||
|
: original;
|
||||||
|
const xPct = ((rotated.x - minX) / w) * 100;
|
||||||
|
const yPct = ((rotated.y - minY) / h) * 100;
|
||||||
if (xPct < 0 || xPct > 100 || yPct < 0 || yPct > 100) return null;
|
if (xPct < 0 || xPct > 100 || yPct < 0 || yPct > 100) return null;
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
|
|||||||
Reference in New Issue
Block a user