fix(photos): length dim snaps to wood footprint (#69)
This commit is contained in:
+75
-11
@@ -83,7 +83,16 @@ function maskAxisSegment(
|
||||
w: number,
|
||||
h: number,
|
||||
bbox: BBox,
|
||||
): { start: [number, number]; end: [number, number]; ux: number; uy: number; span: number } {
|
||||
): {
|
||||
start: [number, number];
|
||||
end: [number, number];
|
||||
/** Actual mask pixels achieving min/max axial projection (wood tips). */
|
||||
tipStart: [number, number];
|
||||
tipEnd: [number, number];
|
||||
ux: number;
|
||||
uy: number;
|
||||
span: number;
|
||||
} {
|
||||
const { start: a, end: b } = axisEndpoints(bbox);
|
||||
const ex = b[0] - a[0];
|
||||
const ey = b[1] - a[1];
|
||||
@@ -93,21 +102,50 @@ function maskAxisSegment(
|
||||
const cx = (bbox.corners[0][0] + bbox.corners[1][0] + bbox.corners[2][0] + bbox.corners[3][0]) / 4;
|
||||
const cy = (bbox.corners[0][1] + bbox.corners[1][1] + bbox.corners[2][1] + bbox.corners[3][1]) / 4;
|
||||
const cProj = cx * ux + cy * uy;
|
||||
let minP = Infinity;
|
||||
let maxP = -Infinity;
|
||||
// Collect axial projections; absolute min/max tip pixels can be mask specks
|
||||
// past the visible wood footprint (Ty UAT #69). Use robust percentiles and
|
||||
// pick tip pixels near the centerline at those stations.
|
||||
const projs: number[] = [];
|
||||
for (let y = 0; y < h; y++) {
|
||||
const row = y * w;
|
||||
for (let x = 0; x < w; x++) {
|
||||
if (!bitset[row + x]) continue;
|
||||
projs.push(x * ux + y * uy);
|
||||
}
|
||||
}
|
||||
if (projs.length === 0) return { start: a, end: b, tipStart: a, tipEnd: b, ux, uy, span: 0 };
|
||||
projs.sort((p, q) => p - q);
|
||||
const lo = Math.min(Math.floor(projs.length * 0.002), projs.length - 1);
|
||||
const hi = Math.max(Math.floor(projs.length * 0.998), lo);
|
||||
const minP = projs[lo];
|
||||
const maxP = projs[hi];
|
||||
// Tip pixel at each robust station: closest to the axis centerline
|
||||
let tipStart: [number, number] = a;
|
||||
let tipEnd: [number, number] = b;
|
||||
let bestStart = Infinity;
|
||||
let bestEnd = Infinity;
|
||||
const stationTol = Math.max(1.5, (maxP - minP) * 0.001);
|
||||
for (let y = 0; y < h; y++) {
|
||||
const row = y * w;
|
||||
for (let x = 0; x < w; x++) {
|
||||
if (!bitset[row + x]) continue;
|
||||
const p = x * ux + y * uy;
|
||||
if (p < minP) minP = p;
|
||||
if (p > maxP) maxP = p;
|
||||
const across = Math.abs((x - cx) * -uy + (y - cy) * ux); // |(p - c) cross u| via r
|
||||
if (Math.abs(p - minP) <= stationTol && across < bestStart) {
|
||||
bestStart = across;
|
||||
tipStart = [x, y];
|
||||
}
|
||||
if (Math.abs(p - maxP) <= stationTol && across < bestEnd) {
|
||||
bestEnd = across;
|
||||
tipEnd = [x, y];
|
||||
}
|
||||
}
|
||||
}
|
||||
if (maxP < minP) return { start: a, end: b, ux, uy, span: 0 };
|
||||
return {
|
||||
start: [cx + ux * (minP - cProj), cy + uy * (minP - cProj)],
|
||||
end: [cx + ux * (maxP - cProj), cy + uy * (maxP - cProj)],
|
||||
tipStart,
|
||||
tipEnd,
|
||||
ux,
|
||||
uy,
|
||||
span: maxP - minP,
|
||||
@@ -174,7 +212,8 @@ export function buildOverlayPlan(input: OverlayPlanInput): OverlayPlan {
|
||||
const leftSrc: [number, number] = [ox + rx * extent.minP, oy + ry * extent.minP];
|
||||
const rightSrc: [number, number] = [ox + rx * extent.maxP, oy + ry * extent.maxP];
|
||||
// Width number sits axis-left outside the slab (past left tip).
|
||||
const labelOff = extent.minP - pad - scale * 0.4;
|
||||
// Extra clearance so glyph box (textAlign grows away from wood) stays off footprint.
|
||||
const labelOff = extent.minP - pad - scale * 0.4 - Math.max(22, Math.round(scale * 1.0)) * 0.35;
|
||||
const labelSrcW: [number, number] = [ox + rx * labelOff, oy + ry * labelOff];
|
||||
const widthIn = (extent.maxP - extent.minP + 1) / scale;
|
||||
widths.push({
|
||||
@@ -198,10 +237,31 @@ export function buildOverlayPlan(input: OverlayPlanInput): OverlayPlan {
|
||||
if (extent && extent.maxP > maxRight) maxRight = extent.maxP;
|
||||
}
|
||||
const outward = maxRight + pad;
|
||||
const aSrc: [number, number] = [bottom[0] + rx * outward, bottom[1] + ry * outward];
|
||||
const bSrc: [number, number] = [top[0] + rx * outward, top[1] + ry * outward];
|
||||
// Tip pixels = wood ends. Translate them by a single screen-axis offset so the
|
||||
// dim line stays || tip-to-tip (measured span = wood tip-to-tip) and arrowheads
|
||||
// share tip y (vertical-ish) / tip x (horizontal-ish) — no past-footprint
|
||||
// overshoot from (rx,ry)*outward (#69). Clearance magnitude matches classic
|
||||
// axis-right mid (centerline + r*outward).
|
||||
const tipOriented = pickBottomTowardTop(seg.tipStart, seg.tipEnd);
|
||||
const aClassic: [number, number] = [bottom[0] + rx * outward, bottom[1] + ry * outward];
|
||||
const bClassic: [number, number] = [top[0] + rx * outward, top[1] + ry * outward];
|
||||
const tipMidX = (tipOriented.bottom[0] + tipOriented.top[0]) / 2;
|
||||
const tipMidY = (tipOriented.bottom[1] + tipOriented.top[1]) / 2;
|
||||
const classicMidX = (aClassic[0] + bClassic[0]) / 2;
|
||||
const classicMidY = (aClassic[1] + bClassic[1]) / 2;
|
||||
let aSrc: [number, number];
|
||||
let bSrc: [number, number];
|
||||
if (Math.abs(uy) >= Math.abs(ux)) {
|
||||
const dx = classicMidX - tipMidX;
|
||||
aSrc = [tipOriented.bottom[0] + dx, tipOriented.bottom[1]];
|
||||
bSrc = [tipOriented.top[0] + dx, tipOriented.top[1]];
|
||||
} else {
|
||||
const dy = classicMidY - tipMidY;
|
||||
aSrc = [tipOriented.bottom[0], tipOriented.bottom[1] + dy];
|
||||
bSrc = [tipOriented.top[0], tipOriented.top[1] + dy];
|
||||
}
|
||||
const midSrc: [number, number] = [(aSrc[0] + bSrc[0]) / 2, (aSrc[1] + bSrc[1]) / 2];
|
||||
// Nudge label further outward a bit
|
||||
// Nudge label further outward a bit (axis-right of dim line)
|
||||
const labelSrc: [number, number] = [midSrc[0] + rx * (scale * 0.4), midSrc[1] + ry * (scale * 0.4)];
|
||||
|
||||
const fontPx = Math.max(22, Math.round(scale * 1.0));
|
||||
@@ -264,11 +324,15 @@ export function drawOverlayPlan(
|
||||
|
||||
// Length
|
||||
arrow(plan.length.a, plan.length.b);
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText(plan.length.label, plan.length.labelAt[0], plan.length.labelAt[1]);
|
||||
|
||||
// Widths
|
||||
// Widths — grow text away from wood (labelAt is axis-left of left tip)
|
||||
for (const w of plan.widths) {
|
||||
arrow(w.left, w.right);
|
||||
// labelAt is on the axis-left side of the station; align so glyphs extend further out
|
||||
const towardLeft = w.labelAt[0] <= w.center[0];
|
||||
ctx.textAlign = towardLeft ? 'right' : 'left';
|
||||
ctx.fillText(w.label, w.labelAt[0], w.labelAt[1]);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user