feat(capture): shared crop_fill + tape geometry for #99
- One crop_fill (default 0.9) replaces duplicated FILL in inventory/overlay - Thin localStorage settings store; crop % only moves long-axis end insets - Pure tapeGeometry: fixed stage line, stage↔image map, tick span → px - measure() prefers tape tick span for scale; maskSpanAlong demoted - rotateCalibration bakes cal tilt into inventory + overlay only - Fix stale 80%-fill comment on slabBounds
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { minAreaRect, orientedRect } from '../geometry/rect.ts';
|
||||
import { measure } from './measure.ts';
|
||||
|
||||
test('#99: tape span drives scale, not maskSpanAlong', () => {
|
||||
const W = 480, H = 100;
|
||||
const bitset = new Uint8Array(W * H).fill(1);
|
||||
const box = orientedRect(minAreaRect(bitset, W, H)!, 0);
|
||||
// Declared L = 48"; tape span = 240 px → 5 px/in (not the full 480 mask span).
|
||||
const m = measure(bitset, W, H, box, 384, 8, W * H, {
|
||||
start: [100, 50],
|
||||
ux: 1,
|
||||
uy: 0,
|
||||
spanPx: 240,
|
||||
});
|
||||
assert.ok(Math.abs(m.scale - 5) < 1e-9, `scale ${m.scale}`);
|
||||
});
|
||||
@@ -6,8 +6,19 @@ export function axisEndpoints(bbox: BBox): { start: [number, number]; end: [numb
|
||||
return { start: bbox.corners[3], end: bbox.corners[0] };
|
||||
}
|
||||
|
||||
export interface TapeAxis {
|
||||
/** Image-space tape start (tick A). */
|
||||
start: [number, number];
|
||||
/** Image-space unit direction tick A → tick B. */
|
||||
ux: number;
|
||||
uy: number;
|
||||
/** Image-space distance between ticks (owner-declared tape length in px). */
|
||||
spanPx: number;
|
||||
}
|
||||
|
||||
// Wood end→end along the approved direction: project the mask onto the axis
|
||||
// unit vector and return the segment on the bbox center line covering that span.
|
||||
// Used only as a fallback / sanity path when no tape ticks are present (#99).
|
||||
function maskAxisSegment(
|
||||
bitset: Uint8Array, w: number, h: number, bbox: BBox,
|
||||
): { start: [number, number]; end: [number, number]; span: number; ux: number; uy: number } {
|
||||
@@ -71,16 +82,38 @@ function sampleWidths(
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param tape When provided (#99), scale = tape.spanPx / L and width stations
|
||||
* walk from the tick-A endpoint. maskSpanAlong is not used for scale.
|
||||
*/
|
||||
export function measure(
|
||||
bitset: Uint8Array, w: number, h: number,
|
||||
bbox: BBox,
|
||||
lengthEighths: Eighths, thicknessEighths: Eighths,
|
||||
slabPx: number,
|
||||
tape?: TapeAxis | null,
|
||||
): Omit<Measures, 'inventoryPng'> & { sourceBelowFloor: false } {
|
||||
const lengthIn = lengthEighths / 8;
|
||||
const seg = maskAxisSegment(bitset, w, h, bbox);
|
||||
const scale = (seg.span || maskSpanAlong(bitset, w, h, seg.ux, seg.uy)) / lengthIn;
|
||||
const every6In = sampleWidths(bitset, w, h, seg.start, seg.ux, seg.uy, scale, lengthIn);
|
||||
let start: [number, number];
|
||||
let ux: number;
|
||||
let uy: number;
|
||||
let scale: number;
|
||||
if (tape && tape.spanPx > 0 && lengthIn > 0) {
|
||||
start = tape.start;
|
||||
ux = tape.ux;
|
||||
uy = tape.uy;
|
||||
scale = tape.spanPx / lengthIn;
|
||||
// Optional sanity: maskSpanAlong remains available for diagnostics only.
|
||||
void maskSpanAlong;
|
||||
} else {
|
||||
const seg = maskAxisSegment(bitset, w, h, bbox);
|
||||
// Legacy fallback when no tape ticks — keep prior behaviour for old drafts.
|
||||
scale = (seg.span || maskSpanAlong(bitset, w, h, seg.ux, seg.uy)) / lengthIn;
|
||||
start = seg.start;
|
||||
ux = seg.ux;
|
||||
uy = seg.uy;
|
||||
}
|
||||
const every6In = sampleWidths(bitset, w, h, start, ux, uy, scale, lengthIn);
|
||||
let widths: Widths;
|
||||
if (every6In.length > 0) {
|
||||
const min = Math.min(...every6In);
|
||||
|
||||
Reference in New Issue
Block a user