Compare commits
2 Commits
9bc35f7edd
...
05447c92fc
| Author | SHA1 | Date | |
|---|---|---|---|
| 05447c92fc | |||
| 415158d4d3 |
@@ -5,9 +5,11 @@ import {
|
|||||||
pickBottomTowardTop,
|
pickBottomTowardTop,
|
||||||
axisRightUnit,
|
axisRightUnit,
|
||||||
buildOverlayPlan,
|
buildOverlayPlan,
|
||||||
|
inventorySourceMap,
|
||||||
type OverlayPlan,
|
type OverlayPlan,
|
||||||
} from './overlay.ts';
|
} from './overlay.ts';
|
||||||
import { orientedRect, minAreaRect } from '../geometry/rect.ts';
|
import { orientedRect, minAreaRect } from '../geometry/rect.ts';
|
||||||
|
import { axisEndpoints } from '../measure/measure.ts';
|
||||||
|
|
||||||
// --- Station math (#63) ---
|
// --- Station math (#63) ---
|
||||||
|
|
||||||
@@ -292,3 +294,206 @@ test('buildOverlayPlan: font one step smaller than prior 28 / scale*1.2 (#67)',
|
|||||||
// Concrete contract: max(22, round(scale*1.0))
|
// Concrete contract: max(22, round(scale*1.0))
|
||||||
assert.equal(plan.fontPx, Math.max(22, Math.round(scale * 1.0)));
|
assert.equal(plan.fontPx, Math.max(22, Math.round(scale * 1.0)));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// --- Length dim must not overshoot wood footprint (#69) ---
|
||||||
|
|
||||||
|
/** Tilted-axis slab: irregular mask + approved bbox long side ~10° from vertical. */
|
||||||
|
function tiltedSlabFixture() {
|
||||||
|
const W = 300, H = 600;
|
||||||
|
const bitset = new Uint8Array(W * H);
|
||||||
|
for (let y = 50; y <= 550; y++) {
|
||||||
|
const t = (y - 50) / 500;
|
||||||
|
const x0 = Math.round(80 + t * 20);
|
||||||
|
const x1 = Math.round(180 + t * 40);
|
||||||
|
for (let x = x0; x <= x1; x++) bitset[y * W + x] = 1;
|
||||||
|
}
|
||||||
|
const ang = (10 * Math.PI) / 180;
|
||||||
|
const ux0 = Math.sin(ang);
|
||||||
|
const uy0 = Math.cos(ang);
|
||||||
|
const rx0 = -uy0;
|
||||||
|
const ry0 = ux0;
|
||||||
|
const cx = 140, cy = 300, halfL = 280, halfW = 90;
|
||||||
|
const corners: [number, number][] = [
|
||||||
|
[cx + ux0 * halfL + rx0 * halfW, cy + uy0 * halfL + ry0 * halfW],
|
||||||
|
[cx + ux0 * halfL - rx0 * halfW, cy + uy0 * halfL - ry0 * halfW],
|
||||||
|
[cx - ux0 * halfL - rx0 * halfW, cy - uy0 * halfL - ry0 * halfW],
|
||||||
|
[cx - ux0 * halfL + rx0 * halfW, cy - uy0 * halfL + ry0 * halfW],
|
||||||
|
];
|
||||||
|
const bbox = { corners, longSidePx: halfL * 2, angleDeg: 80 };
|
||||||
|
return { W, H, bitset, bbox };
|
||||||
|
}
|
||||||
|
|
||||||
|
function maskWoodAabb(bitset: Uint8Array, W: number, H: number) {
|
||||||
|
let minX = W, maxX = 0, minY = H, maxY = 0;
|
||||||
|
for (let y = 0; y < H; y++) {
|
||||||
|
for (let x = 0; x < W; x++) {
|
||||||
|
if (!bitset[y * W + x]) continue;
|
||||||
|
if (x < minX) minX = x;
|
||||||
|
if (x > maxX) maxX = x;
|
||||||
|
if (y < minY) minY = y;
|
||||||
|
if (y > maxY) maxY = y;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { minX, maxX, minY, maxY };
|
||||||
|
}
|
||||||
|
|
||||||
|
test('buildOverlayPlan: length axial span = tip span; inventory y stays within wood AABB (#69)', () => {
|
||||||
|
const { W, H, bitset, bbox } = tiltedSlabFixture();
|
||||||
|
const scale = 10;
|
||||||
|
const wood = maskWoodAabb(bitset, W, H);
|
||||||
|
const slabBounds = {
|
||||||
|
x: wood.minX - 8,
|
||||||
|
y: wood.minY - 8,
|
||||||
|
w: wood.maxX - wood.minX + 16,
|
||||||
|
h: wood.maxY - wood.minY + 16,
|
||||||
|
};
|
||||||
|
const inv = inventorySourceMap(slabBounds, { widthPx: W, heightPx: H }, 900, 1200);
|
||||||
|
const plan = buildOverlayPlan({
|
||||||
|
bitset,
|
||||||
|
widthPx: W,
|
||||||
|
heightPx: H,
|
||||||
|
bbox,
|
||||||
|
scale,
|
||||||
|
lengthIn: 50,
|
||||||
|
lengthLabel: '50"',
|
||||||
|
mapToInventory: inv.mapToInventory,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Wood AABB in inventory space
|
||||||
|
let mMinY = Infinity, mMaxY = -Infinity;
|
||||||
|
let mMinX = Infinity, mMaxX = -Infinity;
|
||||||
|
for (let y = 0; y < H; y++) {
|
||||||
|
for (let x = 0; x < W; x++) {
|
||||||
|
if (!bitset[y * W + x]) continue;
|
||||||
|
const [mx, my] = inv.mapToInventory(x, y);
|
||||||
|
if (my < mMinY) mMinY = my;
|
||||||
|
if (my > mMaxY) mMaxY = my;
|
||||||
|
if (mx < mMinX) mMinX = mx;
|
||||||
|
if (mx > mMaxX) mMaxX = mx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const lenMinY = Math.min(plan.length.a[1], plan.length.b[1]);
|
||||||
|
const lenMaxY = Math.max(plan.length.a[1], plan.length.b[1]);
|
||||||
|
// Current bug: (rx,ry)*outward pushes length ends past wood tip y when axis is tilted
|
||||||
|
assert.ok(
|
||||||
|
lenMinY >= mMinY - 1.5,
|
||||||
|
`length top y=${lenMinY.toFixed(1)} must not extend past wood AABB minY=${mMinY.toFixed(1)}`,
|
||||||
|
);
|
||||||
|
assert.ok(
|
||||||
|
lenMaxY <= mMaxY + 1.5,
|
||||||
|
`length bottom y=${lenMaxY.toFixed(1)} must not extend past wood AABB maxY=${mMaxY.toFixed(1)}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Length endpoints = robust tip pixels + common screen offset → tip-to-tip span
|
||||||
|
const { start: aa, end: bb } = axisEndpoints(bbox);
|
||||||
|
const ex = bb[0] - aa[0], ey = bb[1] - aa[1], elen = Math.hypot(ex, ey) || 1;
|
||||||
|
const ux = ex / elen, uy = ey / elen;
|
||||||
|
const cx = bbox.corners.reduce((s, c) => s + c[0], 0) / 4;
|
||||||
|
const cy = bbox.corners.reduce((s, c) => s + c[1], 0) / 4;
|
||||||
|
const projs: number[] = [];
|
||||||
|
for (let y = 0; y < H; y++) {
|
||||||
|
for (let x = 0; x < W; x++) {
|
||||||
|
if (!bitset[y * W + x]) continue;
|
||||||
|
projs.push(x * ux + y * uy);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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 tipMin = projs[lo];
|
||||||
|
const tipMax = projs[hi];
|
||||||
|
const stationTol = Math.max(1.5, (tipMax - tipMin) * 0.001);
|
||||||
|
let tipMinPt: [number, number] = [0, 0];
|
||||||
|
let tipMaxPt: [number, number] = [0, 0];
|
||||||
|
let bestStart = Infinity, bestEnd = Infinity;
|
||||||
|
for (let y = 0; y < H; y++) {
|
||||||
|
for (let x = 0; x < W; x++) {
|
||||||
|
if (!bitset[y * W + x]) continue;
|
||||||
|
const p = x * ux + y * uy;
|
||||||
|
const across = Math.abs((x - cx) * -uy + (y - cy) * ux);
|
||||||
|
if (Math.abs(p - tipMin) <= stationTol && across < bestStart) {
|
||||||
|
bestStart = across;
|
||||||
|
tipMinPt = [x, y];
|
||||||
|
}
|
||||||
|
if (Math.abs(p - tipMax) <= stationTol && across < bestEnd) {
|
||||||
|
bestEnd = across;
|
||||||
|
tipMaxPt = [x, y];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const tipOr = pickBottomTowardTop(tipMinPt, tipMaxPt);
|
||||||
|
const mapTipBot = inv.mapToInventory(tipOr.bottom[0], tipOr.bottom[1]);
|
||||||
|
const mapTipTop = inv.mapToInventory(tipOr.top[0], tipOr.top[1]);
|
||||||
|
const tipEuclid = Math.hypot(mapTipTop[0] - mapTipBot[0], mapTipTop[1] - mapTipBot[1]);
|
||||||
|
const lenEuclid = Math.hypot(plan.length.b[0] - plan.length.a[0], plan.length.b[1] - plan.length.a[1]);
|
||||||
|
assert.ok(
|
||||||
|
Math.abs(lenEuclid - tipEuclid) <= 2,
|
||||||
|
`length span ${lenEuclid.toFixed(1)} must equal tip-to-tip ${tipEuclid.toFixed(1)}`,
|
||||||
|
);
|
||||||
|
// Arrowhead stations share tip screen-y (vertical-ish fixture)
|
||||||
|
assert.ok(Math.abs(plan.length.a[1] - mapTipBot[1]) <= 1.5 || Math.abs(plan.length.a[1] - mapTipTop[1]) <= 1.5);
|
||||||
|
assert.ok(Math.abs(plan.length.b[1] - mapTipBot[1]) <= 1.5 || Math.abs(plan.length.b[1] - mapTipTop[1]) <= 1.5);
|
||||||
|
|
||||||
|
// Length stays axis-right of wood (clearance to the right in inventory x for this fixture)
|
||||||
|
const lenMidX = (plan.length.a[0] + plan.length.b[0]) / 2;
|
||||||
|
assert.ok(lenMidX > mMaxX + 4, `length dim must sit outside wood on axis-right (x=${lenMidX} vs maxX=${mMaxX})`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('buildOverlayPlan: width labels fully left of wood AABB after inventory map (#69)', () => {
|
||||||
|
const { W, H, bitset, bbox } = tiltedSlabFixture();
|
||||||
|
const scale = 10;
|
||||||
|
const wood = maskWoodAabb(bitset, W, H);
|
||||||
|
const slabBounds = {
|
||||||
|
x: wood.minX - 8,
|
||||||
|
y: wood.minY - 8,
|
||||||
|
w: wood.maxX - wood.minX + 16,
|
||||||
|
h: wood.maxY - wood.minY + 16,
|
||||||
|
};
|
||||||
|
const inv = inventorySourceMap(slabBounds, { widthPx: W, heightPx: H }, 900, 1200);
|
||||||
|
const plan = buildOverlayPlan({
|
||||||
|
bitset,
|
||||||
|
widthPx: W,
|
||||||
|
heightPx: H,
|
||||||
|
bbox,
|
||||||
|
scale,
|
||||||
|
lengthIn: 50,
|
||||||
|
lengthLabel: '50"',
|
||||||
|
mapToInventory: inv.mapToInventory,
|
||||||
|
});
|
||||||
|
assert.ok(plan.widths.length > 0, 'expected width stations');
|
||||||
|
|
||||||
|
let mMinX = Infinity, mMaxX = -Infinity, mMinY = Infinity, mMaxY = -Infinity;
|
||||||
|
for (let y = 0; y < H; y++) {
|
||||||
|
for (let x = 0; x < W; x++) {
|
||||||
|
if (!bitset[y * W + x]) continue;
|
||||||
|
const [mx, my] = inv.mapToInventory(x, y);
|
||||||
|
if (mx < mMinX) mMinX = mx;
|
||||||
|
if (mx > mMaxX) mMaxX = mx;
|
||||||
|
if (my < mMinY) mMinY = my;
|
||||||
|
if (my > mMaxY) mMaxY = my;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const w of plan.widths) {
|
||||||
|
// Label anchor fully off the wood footprint (axis-left), with margin for glyph half-width
|
||||||
|
const margin = plan.fontPx * 0.55; // textAlign will grow away from wood; still keep anchor clear
|
||||||
|
const onWood =
|
||||||
|
w.labelAt[0] >= mMinX - 1 &&
|
||||||
|
w.labelAt[0] <= mMaxX + 1 &&
|
||||||
|
w.labelAt[1] >= mMinY - 1 &&
|
||||||
|
w.labelAt[1] <= mMaxY + 1;
|
||||||
|
assert.ok(
|
||||||
|
!onWood,
|
||||||
|
`width labelAt (${w.labelAt[0].toFixed(0)},${w.labelAt[1].toFixed(0)}) must be outside wood AABB`,
|
||||||
|
);
|
||||||
|
// Axis-left = past the left tip (smaller across); for this nearly-vertical slab, left of wood in x
|
||||||
|
assert.ok(
|
||||||
|
w.labelAt[0] < mMinX - 2,
|
||||||
|
`width label ${w.label} x=${w.labelAt[0].toFixed(0)} must be left of wood minX=${mMinX.toFixed(0)} (margin~${margin.toFixed(0)})`,
|
||||||
|
);
|
||||||
|
// Width arrows still touch wood edges (left/right tips inside/on AABB)
|
||||||
|
assert.ok(w.left[0] >= mMinX - 3 && w.left[0] <= mMaxX + 3);
|
||||||
|
assert.ok(w.right[0] >= mMinX - 3 && w.right[0] <= mMaxX + 3);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|||||||
+75
-11
@@ -83,7 +83,16 @@ function maskAxisSegment(
|
|||||||
w: number,
|
w: number,
|
||||||
h: number,
|
h: number,
|
||||||
bbox: BBox,
|
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 { start: a, end: b } = axisEndpoints(bbox);
|
||||||
const ex = b[0] - a[0];
|
const ex = b[0] - a[0];
|
||||||
const ey = b[1] - a[1];
|
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 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 cy = (bbox.corners[0][1] + bbox.corners[1][1] + bbox.corners[2][1] + bbox.corners[3][1]) / 4;
|
||||||
const cProj = cx * ux + cy * uy;
|
const cProj = cx * ux + cy * uy;
|
||||||
let minP = Infinity;
|
// Collect axial projections; absolute min/max tip pixels can be mask specks
|
||||||
let maxP = -Infinity;
|
// 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++) {
|
for (let y = 0; y < h; y++) {
|
||||||
const row = y * w;
|
const row = y * w;
|
||||||
for (let x = 0; x < w; x++) {
|
for (let x = 0; x < w; x++) {
|
||||||
if (!bitset[row + x]) continue;
|
if (!bitset[row + x]) continue;
|
||||||
const p = x * ux + y * uy;
|
const p = x * ux + y * uy;
|
||||||
if (p < minP) minP = p;
|
const across = Math.abs((x - cx) * -uy + (y - cy) * ux); // |(p - c) cross u| via r
|
||||||
if (p > maxP) maxP = p;
|
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 {
|
return {
|
||||||
start: [cx + ux * (minP - cProj), cy + uy * (minP - cProj)],
|
start: [cx + ux * (minP - cProj), cy + uy * (minP - cProj)],
|
||||||
end: [cx + ux * (maxP - cProj), cy + uy * (maxP - cProj)],
|
end: [cx + ux * (maxP - cProj), cy + uy * (maxP - cProj)],
|
||||||
|
tipStart,
|
||||||
|
tipEnd,
|
||||||
ux,
|
ux,
|
||||||
uy,
|
uy,
|
||||||
span: maxP - minP,
|
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 leftSrc: [number, number] = [ox + rx * extent.minP, oy + ry * extent.minP];
|
||||||
const rightSrc: [number, number] = [ox + rx * extent.maxP, oy + ry * extent.maxP];
|
const rightSrc: [number, number] = [ox + rx * extent.maxP, oy + ry * extent.maxP];
|
||||||
// Width number sits axis-left outside the slab (past left tip).
|
// 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 labelSrcW: [number, number] = [ox + rx * labelOff, oy + ry * labelOff];
|
||||||
const widthIn = (extent.maxP - extent.minP + 1) / scale;
|
const widthIn = (extent.maxP - extent.minP + 1) / scale;
|
||||||
widths.push({
|
widths.push({
|
||||||
@@ -198,10 +237,31 @@ export function buildOverlayPlan(input: OverlayPlanInput): OverlayPlan {
|
|||||||
if (extent && extent.maxP > maxRight) maxRight = extent.maxP;
|
if (extent && extent.maxP > maxRight) maxRight = extent.maxP;
|
||||||
}
|
}
|
||||||
const outward = maxRight + pad;
|
const outward = maxRight + pad;
|
||||||
const aSrc: [number, number] = [bottom[0] + rx * outward, bottom[1] + ry * outward];
|
// Tip pixels = wood ends. Translate them by a single screen-axis offset so the
|
||||||
const bSrc: [number, number] = [top[0] + rx * outward, top[1] + ry * outward];
|
// 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];
|
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 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));
|
const fontPx = Math.max(22, Math.round(scale * 1.0));
|
||||||
@@ -264,11 +324,15 @@ export function drawOverlayPlan(
|
|||||||
|
|
||||||
// Length
|
// Length
|
||||||
arrow(plan.length.a, plan.length.b);
|
arrow(plan.length.a, plan.length.b);
|
||||||
|
ctx.textAlign = 'center';
|
||||||
ctx.fillText(plan.length.label, plan.length.labelAt[0], plan.length.labelAt[1]);
|
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) {
|
for (const w of plan.widths) {
|
||||||
arrow(w.left, w.right);
|
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]);
|
ctx.fillText(w.label, w.labelAt[0], w.labelAt[1]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user