fix(overlay): Photos-ready dims follow tape, not wood MAR

After rotateCalibration, map tape tick A→B through bakeTiltTransform into
baked cal coords, orient overlay bbox with orientedRect(..., tapeAngle), and
pass optional axis into buildOverlayPlan so length || tape and widths ⊥ tape.
This commit is contained in:
grokbot_ty
2026-09-26 08:08:21 -07:00
parent d7fc424b69
commit 4ea2618475
5 changed files with 257 additions and 18 deletions
+61 -13
View File
@@ -12,28 +12,43 @@ export interface RotatedCal {
source: Pick<Source, 'widthPx' | 'heightPx'>;
}
/** Pure math for the same clockwise-about-centre bake used by rotateCalibration. */
export interface BakeTiltTransform {
/** True when tilt is ~0° (identity; no resample). */
identity: boolean;
tiltDeg: number;
srcW: number;
srcH: number;
outW: number;
outH: number;
/** Map a point from pre-bake source coords into baked canvas coords. */
mapPoint: (x: number, y: number) => [number, number];
}
/**
* Rotate source clockwise by tiltDeg (same sign as CSS/canvas rotate).
* Returns a new bitmap the caller must close, plus recomputed mask geometry.
* Identity (≈0°) returns clones of inputs without resampling.
* Clockwise rotation about image centre; output canvas sized to AABB of rotated
* corners — identical sizing to rotateCalibration (canvas translate+rotate path).
*/
export async function rotateCalibration(
bitmap: ImageBitmap,
mask: Mask,
source: Pick<Source, 'widthPx' | 'heightPx'>,
export function bakeTiltTransform(
srcW: number,
srcH: number,
tiltDeg: number,
): Promise<RotatedCal> {
): BakeTiltTransform {
const a = ((tiltDeg % 360) + 360) % 360;
if (a < 0.05 || a > 359.95) {
// No meaningful rotation — hand back the same bitmap reference; caller closes once.
return { bitmap, mask, source };
return {
identity: true,
tiltDeg: a,
srcW,
srcH,
outW: srcW,
outH: srcH,
mapPoint: (x, y) => [x, y],
};
}
const srcW = source.widthPx;
const srcH = source.heightPx;
const rad = (a * Math.PI) / 180;
const cos = Math.cos(rad);
const sin = Math.sin(rad);
// Axis-aligned bounds of rotated rectangle corners.
const corners = [
[0, 0],
[srcW, 0],
@@ -53,6 +68,39 @@ export async function rotateCalibration(
}
const outW = Math.max(1, Math.ceil(maxX - minX));
const outH = Math.max(1, Math.ceil(maxY - minY));
return {
identity: false,
tiltDeg: a,
srcW,
srcH,
outW,
outH,
mapPoint: (x, y) => {
const cx = x - srcW / 2;
const cy = y - srcH / 2;
return [cx * cos - cy * sin + outW / 2, cx * sin + cy * cos + outH / 2];
},
};
}
/**
* Rotate source clockwise by tiltDeg (same sign as CSS/canvas rotate).
* Returns a new bitmap the caller must close, plus recomputed mask geometry.
* Identity (≈0°) returns clones of inputs without resampling.
*/
export async function rotateCalibration(
bitmap: ImageBitmap,
mask: Mask,
source: Pick<Source, 'widthPx' | 'heightPx'>,
tiltDeg: number,
): Promise<RotatedCal> {
const xform = bakeTiltTransform(source.widthPx, source.heightPx, tiltDeg);
if (xform.identity) {
// No meaningful rotation — hand back the same bitmap reference; caller closes once.
return { bitmap, mask, source };
}
const { srcW, srcH, outW, outH, tiltDeg: a } = xform;
const rad = (a * Math.PI) / 180;
const canvas = new OffscreenCanvas(outW, outH);
const ctx = canvas.getContext('2d')!;
ctx.translate(outW / 2, outH / 2);