UAT-8b: axis UI — mask carry-forward, remove axis line, slider

1. drawOverlay: checkerboard mask overlay now drawn in axis + measure
   states (mask carry-forward — slab is isolated)
2. Removed the separate red axis line + arrowhead — the rect's long
   side IS the axis
3. Rect rotates: uses axisState.rect.angle (from UAT-8a)
4. Replace three rotate buttons with slider (-180..180, step 1) +
   minus/plus nudge buttons (same pattern as Sensitivity slider)

587 lines, 109/109 tests, build OK.
This commit is contained in:
Ty
2026-09-14 11:45:12 -07:00
parent f00c4a7460
commit 2bc7eecd7c
+26 -35
View File
@@ -154,6 +154,15 @@
axisState = reduceAxisState(axisState, { type: 'rotate', angle: rad });
}
function onRotateSlider(deg) {
if (!axisState) return;
const currentDeg = axisState.rect.angle * 180 / Math.PI;
const deltaDeg = deg - currentDeg;
if (deltaDeg === 0) return;
const rad = (deltaDeg * Math.PI) / 180;
axisState = reduceAxisState(axisState, { type: 'rotate', angle: rad });
}
function approveAxis() {
if (!axisState) return;
axisState = reduceAxisState(axisState, { type: 'approve' });
@@ -239,7 +248,7 @@
// Draw photo
ctx.drawImage(photo, 0, 0, w, h);
if (state === 'mask' && maskResult) {
if ((state === 'mask' || state === 'axis' || state === 'measure') && maskResult) {
// Checkerboard on removed pixels
const weights = maskResult.weights;
const mask = maskResult.mask;
@@ -263,42 +272,20 @@
}
}
if ((state === 'axis' || state === 'measure') && rect) {
// Draw bounding rectangle
const cx = (rect.center.x / photoW) * w;
const cy = (rect.center.y / photoH) * h;
const rw = (rect.width / photoW) * w;
const rh = (rect.height / photoH) * h;
const angle = rect.angle;
if ((state === 'axis' || state === 'measure') && axisState) {
// Draw bounding rectangle — uses the ROTATED rect from axisState.
// The rect's long side IS the length axis (no separate line).
const r = axisState.rect;
const cx = (r.center.x / photoW) * w;
const cy = (r.center.y / photoH) * h;
const rw = (r.width / photoW) * w;
const rh = (r.height / photoH) * h;
ctx.save();
ctx.translate(cx, cy);
ctx.rotate(angle);
ctx.rotate(r.angle);
ctx.strokeStyle = '#00ff88';
ctx.lineWidth = 2;
ctx.strokeRect(-rw / 2, -rh / 2, rw, rh);
// Draw axis line on long side
const axis = axisState?.axis;
if (axis) {
const axLen = (Math.max(rect.width, rect.height) / photoW) * w;
const axScreenX = axis.x * axLen;
const axScreenY = axis.y * axLen;
ctx.strokeStyle = '#ff4444';
ctx.lineWidth = 3;
ctx.beginPath();
ctx.moveTo(-axScreenX / 2, -axScreenY / 2);
ctx.lineTo(axScreenX / 2, axScreenY / 2);
ctx.stroke();
// Arrowhead
const tipX = axScreenX / 2;
const tipY = axScreenY / 2;
const aAng = Math.atan2(axScreenY, axScreenX);
ctx.beginPath();
ctx.moveTo(tipX, tipY);
ctx.lineTo(tipX - 12 * Math.cos(aAng - 0.4), tipY - 12 * Math.sin(aAng - 0.4));
ctx.moveTo(tipX, tipY);
ctx.lineTo(tipX - 12 * Math.cos(aAng + 0.4), tipY - 12 * Math.sin(aAng + 0.4));
ctx.stroke();
}
ctx.restore();
}
}
@@ -404,9 +391,13 @@
<div class="controls">
<div class="control-row">
<label>Rotate</label>
<button class="nudge" onclick={() => rotateAxisBy(-5)}>−5°</button>
<button class="nudge" onclick={() => rotateAxisBy(5)}>+5°</button>
<button class="nudge" onclick={() => rotateAxisBy(90)}>90°</button>
<button class="nudge" onclick={() => rotateAxisBy(-5)}>−</button>
<input type="range" min="-180" max="180" step="1"
value={Math.round(axisState.rect.angle * 180 / Math.PI)}
oninput={(e) => onRotateSlider(Number(e.target.value))}
aria-label="Rotate axis" />
<span class="val">{Math.round(axisState.rect.angle * 180 / Math.PI)}°</span>
<button class="nudge" onclick={() => rotateAxisBy(5)}>+</button>
</div>
</div>
<div class="actions">