v1.50.1: the v1.50.0 review (HP-1500-01..03)

- HP-1500-02: the stage budget was the absolute document coordinate, so any
  tall dashboard content before the card was billed as header and the stage
  collapsed to 0px. Measure our own chrome relative to the card plus a
  bounded (<=120px) allowance for what the viewport keeps above us; re-measure
  on window resize, remove the listener in disconnectedCallback.
- HP-1500-03, both layers: contentBounds opens a near-zero axis (< ~an icon)
  up to a 200-unit floor and ignores extra points outside a canvas envelope
  (-25%..125%) for FRAMING purposes only; the server bounds layout coordinates
  to +-4 — any finite float used to pass, and one 1e100 hid the plan from
  every viewer. A thin real room keeps its tight frame; the gate sensor past
  the edge still stretches it.
- HP-1500-01: no automatic double-transform — a correct layout and a stranded
  one are indistinguishable, and guessing wrong corrupts good data. Explicit
  admin command houseplan/geometry/repair: dry_run previews, the backup rides
  the same store write, undo restores, and routine layout writes now preserve
  unrelated store keys instead of eating the backup.

Tests: contentBounds guards (unit), layout coordinate bounds + repair
lifecycle (harness), card-below-content smoke. Inventory: 139 / 49 / 42 / 64.
This commit is contained in:
Matysh
2026-07-29 01:39:10 +03:00
parent 9282c28830
commit a8ce6020f4
19 changed files with 409 additions and 75 deletions
+21 -3
View File
@@ -86,10 +86,28 @@ export function contentBounds(
add(r.x + (r.w || 0), r.y + (r.h || 0));
}
}
// things that live outside any room still count as content — a gate sensor
// by the fence, a camera on a pole (the card passes device positions here)
for (const p of extra || []) add(p[0], p[1]);
// Things that live outside any room still count as content — a gate sensor
// by the fence, a camera on a pole (the card passes device positions here).
// But only within a bounded envelope around the canvas: a stored position is
// any finite number the layout schema took, and one absurd coordinate used
// to stretch the frame until the plan was a dot (HP-1500-03). A point past
// the envelope is still rendered wherever it is — it just does not command
// the opening view.
const lo = -NORM_W * 0.25, hi = NORM_W * 1.25;
for (const p of extra || []) {
if (p[0] >= lo && p[0] <= hi && p[1] >= lo && p[1] <= hi) add(p[0], p[1]);
}
if (minX > maxX || minY > maxY) return null;
// A single marker (or a collinear row of them) has no area, and an SVG
// viewBox with a zero axis draws nothing at all (HP-1500-03). An axis with
// essentially no span — nothing there but icons — opens up to a floor:
// enough canvas around a lone marker to see where it stands. A REAL thin
// shape (a 100-unit corridor) keeps its tight frame; only the degenerate
// case is padded, so the threshold sits at about an icon's size.
const FLOOR = NORM_W * 0.2;
const DEGENERATE = NORM_W * 0.03;
if (maxX - minX < DEGENERATE) { const c = (minX + maxX) / 2; minX = c - FLOOR / 2; maxX = c + FLOOR / 2; }
if (maxY - minY < DEGENERATE) { const c = (minY + maxY) / 2; minY = c - FLOOR / 2; maxY = c + FLOOR / 2; }
const m = Math.max(maxX - minX, maxY - minY) * pad;
const x = minX - m, y = minY - m;
return { x, y, w: (maxX - minX) + m * 2, h: (maxY - minY) + m * 2 };