Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| eaedcb53dd | |||
| 11c8124674 |
+1
-1
@@ -94,7 +94,7 @@ On the same photo, keep the approved mask in view and draw the minimum-area boun
|
||||
User enters length `L` in inches (1/8 in steps) and thickness (1/8 in steps), then presses Calculate.
|
||||
|
||||
- `scale = longest_axis_px / L` where `longest_axis_px` is the mask/wood span along the approved length direction (slab pixels end-to-end on that axis), not the rectangle edge length. The rectangle sets direction only; user-entered `L` is wood end→end along that direction.
|
||||
- Widths: min, max, avg, sampled every 6 in along the approved length axis, perpendicular to it
|
||||
- Widths: min, max, avg from samples taken every 6 in along the approved length axis, perpendicular to it, after dropping the first and last 6 in of the span (bulk widths, not tips). If `L ≤ 12 in` there are no interior stations and all three are 0.
|
||||
- `sqft = (mask_pixels / scale²) / 144` (2 decimals)
|
||||
- `bdft = sqft × thickness_in` (2 decimals)
|
||||
- Crop: 3:4 portrait, slab centered, 80% fill at the widest and tallest points, transparent background
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { minAreaRect, orientedRect } from '../geometry/rect.ts';
|
||||
import { measure } from './measure.ts';
|
||||
|
||||
// Uniform slab, 10 px/in: full W×H canvas, axis +x via minAreaRect angle 0.
|
||||
function uniform(W: number, H: number) {
|
||||
const bitset = new Uint8Array(W * H).fill(1);
|
||||
const box = orientedRect(minAreaRect(bitset, W, H)!, 0);
|
||||
return { bitset, box, slabPx: W * H };
|
||||
}
|
||||
|
||||
// Tapered slab, 10 px/in: tips under 6 in and past L−6 in are `tipPx` wide
|
||||
// (centered), the bulk is full canvas height.
|
||||
function tapered(W: number, H: number, lengthEighths: number, tipPx: number) {
|
||||
const inIn = W / 10;
|
||||
const bitset = new Uint8Array(W * H);
|
||||
const half = tipPx / 2;
|
||||
for (let x = 0; x < W; x++) {
|
||||
const p = x / 10;
|
||||
const w = p < 6 || p > inIn - 6 ? tipPx : H;
|
||||
for (let y = (H - w) / 2; y < (H + w) / 2; y++) bitset[y * W + x] = 1;
|
||||
}
|
||||
const box = orientedRect(minAreaRect(bitset, W, H)!, 0);
|
||||
return { bitset, box, slabPx: bitset.reduce((a, c) => a + c, 0), lengthEighths };
|
||||
}
|
||||
|
||||
test('tapered 48in slab: min/max reflect the bulk, tips excluded', () => {
|
||||
const { bitset, box, slabPx, lengthEighths } = tapered(480, 100, 384, 20); // tips 2in, bulk 10in
|
||||
const m = measure(bitset, 480, 100, box, lengthEighths, 8, slabPx);
|
||||
assert.equal(m.widths.every6In.length, 7, `stations ${m.widths.every6In.length} (6..42in)`);
|
||||
for (const e of m.widths.every6In) {
|
||||
assert.ok(e >= 76 && e <= 84, `station ${e / 8}in must be bulk (~10in)`);
|
||||
}
|
||||
assert.ok(m.widths.min >= 76 && m.widths.min <= 84, `min ${m.widths.min / 8}in`);
|
||||
assert.ok(m.widths.max >= 76 && m.widths.max <= 84, `max ${m.widths.max / 8}in`);
|
||||
});
|
||||
|
||||
test('uniform slab: interior stations keep the full width', () => {
|
||||
const { bitset, box, slabPx } = uniform(480, 100);
|
||||
const m = measure(bitset, 480, 100, box, 384, 8, slabPx);
|
||||
assert.equal(m.widths.every6In.length, 7);
|
||||
for (const e of m.widths.every6In) {
|
||||
assert.ok(e >= 72 && e <= 104, `station ${e / 8}in`);
|
||||
}
|
||||
});
|
||||
|
||||
test('short slab L=12in: no interior stations, widths are empty/zero', () => {
|
||||
const { bitset, box, slabPx } = uniform(120, 100);
|
||||
const m = measure(bitset, 120, 100, box, 96, 8, slabPx);
|
||||
assert.deepEqual(m.widths, { min: 0, max: 0, avg: 0, every6In: [] });
|
||||
});
|
||||
|
||||
test('short slab L=10in: no interior stations, widths are empty/zero', () => {
|
||||
const { bitset, box, slabPx } = uniform(100, 100);
|
||||
const m = measure(bitset, 100, 100, box, 80, 8, slabPx);
|
||||
assert.deepEqual(m.widths, { min: 0, max: 0, avg: 0, every6In: [] });
|
||||
});
|
||||
@@ -39,8 +39,9 @@ function maskAxisSegment(
|
||||
};
|
||||
}
|
||||
|
||||
// Sampled every 6 in along the wood axis; each sample is a line probe
|
||||
// perpendicular to the axis, width = max run of slab px across the probe.
|
||||
// Interior stations only (6, 12, … ≤ L−6). Tips dominate min/max on tapered
|
||||
// boards, so the first and last 6 in are excluded. Each sample is a line
|
||||
// probe perpendicular to the axis; width = max run of slab px across it.
|
||||
function sampleWidths(
|
||||
bitset: Uint8Array, w: number, h: number,
|
||||
start: [number, number], ux: number, uy: number,
|
||||
@@ -48,11 +49,10 @@ function sampleWidths(
|
||||
): Eighths[] {
|
||||
const px = -uy, py = ux;
|
||||
const stepPx = 6 * scale;
|
||||
const count = Math.max(1, Math.floor(lengthIn / 6) + 1);
|
||||
const lastPx = lengthIn > 12 ? Math.floor((lengthIn - 6) / 6) * 6 * scale : 0;
|
||||
const out: Eighths[] = [];
|
||||
const half = Math.max(2, Math.round((w + h) / 4));
|
||||
for (let i = 0; i < count; i++) {
|
||||
const t = i * stepPx;
|
||||
for (let t = stepPx; t <= lastPx; t += stepPx) {
|
||||
const ox = start[0] + ux * t;
|
||||
const oy = start[1] + uy * t;
|
||||
let minP = Infinity, maxP = -Infinity;
|
||||
|
||||
Reference in New Issue
Block a user