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:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user