mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-03 13:18:58 +00:00
Icons are measured by the same plan the frame is
Audit dev@2c947f4, DEV-2C947-03 (P2). Three rooms in the core plus one dragged
90 canvases out: the frame rejected the stray exactly as §4.1 promises, and
then a perfectly ordinary marker on the main plan came out 90.89x too big and
covered the house. `contentFrame` voted; `iconUnit` did not — it took
`boxOf(every room)`, so the distance to the stray the frame had just thrown
away lived on in the numerator of `iconCqw`.
`iconUnit` now takes `contentFrame(roomItems, { pad: 0 }).core`: the same
main-mass vote, over the same rooms it always used (rooms only is what keeps
the full card and the static card bit-identical), with no padding, because
this is a UNIT and not a viewport. Below MIN_VOTERS nothing is declared an
outlier, so every ordinary plan — and every genuinely wide one, where the
majority veto applies — keeps exactly the unit it had. `defaultPositions`
takes its declump distance from the same call, so the auto-placement spacing
follows without a second rule.
Unit (test/canvas.test.mjs): a far room leaves both the frame and the icon
unit alone, `iconCqw` on the strayed plan equals `iconCqw` on the same plan
without the stray, and a plan that is honestly two canvases wide still scales.
smoke_canvas_frame.mjs measures the rendered badge in px with and without the
far room. Both are red on the parent commit.
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -8,6 +8,10 @@
|
||||
// DEV-2C947-02: inside an editor the frame only grows (a frame that shrank
|
||||
// mid-drag would move the ground under the pointer); that union was memoised
|
||||
// without the mode, so it survived the way back into View.
|
||||
//
|
||||
// DEV-2C947-03: a room-outlier is rejected from the frame, but iconUnit()
|
||||
// measured ALL rooms — so the stray it had just rejected still blew every
|
||||
// marker of the main plan up ~91x.
|
||||
import { launch, checkAll, finish } from './serve.mjs';
|
||||
|
||||
const { page, browser } = await launch({ width: 900, height: 820 }, 1);
|
||||
@@ -164,5 +168,45 @@ Object.assign(out, await page.evaluate(async () => {
|
||||
return o;
|
||||
}));
|
||||
|
||||
// ---------------------------------------------------- 03: icons and outliers
|
||||
// Three rooms in the core plus one dragged 90 canvases out. The frame rejects
|
||||
// the stray (that part always worked); the ICON UNIT used to measure all four
|
||||
// rooms, so every marker of the plan the user is looking at came out ~91x too
|
||||
// big and covered the house.
|
||||
const withFarRoom = (far) => ({
|
||||
spaces: [{
|
||||
id: 'i1', title: 'Icons', view_box: [0, 0, 1, 1], plan_url: null, plan_aspect: null,
|
||||
rooms: [
|
||||
{ id: 'r1', name: 'A', area: 'zx_a', poly: [[0.10, 0.10], [0.30, 0.10], [0.30, 0.30], [0.10, 0.30]] },
|
||||
{ id: 'r2', name: 'B', area: 'zx_b', poly: [[0.30, 0.10], [0.55, 0.10], [0.55, 0.35], [0.30, 0.35]] },
|
||||
{ id: 'r3', name: 'C', area: 'zx_c', poly: [[0.10, 0.30], [0.35, 0.30], [0.35, 0.55], [0.10, 0.55]] },
|
||||
...(far ? [{ id: 'r4', name: 'Far', area: 'zx_d', poly: [[90, 90], [91, 90], [91, 91], [90, 91]] }] : []),
|
||||
],
|
||||
}],
|
||||
markers: [{ id: 'm_one', binding: 'device:d_light1', area: 'zx_a' }],
|
||||
settings: { filter_seeded: true },
|
||||
});
|
||||
const ICON_POS = { m_one: { s: 'i1', x: 0.2, y: 0.2 } };
|
||||
|
||||
const iconPx = () => page.evaluate(() => {
|
||||
const c = window.__card;
|
||||
const sr = c.shadowRoot || c.renderRoot;
|
||||
const el = sr.querySelector('.devlayer .dev');
|
||||
// the badge is --dev-size plus a 1 px border on each side (smoke_icon_scale)
|
||||
return { px: el ? Math.round((el.getBoundingClientRect().width - 2) * 100) / 100 : null,
|
||||
outliers: c._outliers, frameW: Math.round(c._baseVb()[2]) };
|
||||
});
|
||||
|
||||
await load(withFarRoom(false), ICON_POS, 'i1');
|
||||
await raf();
|
||||
const plain = await iconPx();
|
||||
await load(withFarRoom(true), ICON_POS, 'i1');
|
||||
await raf();
|
||||
const strayed = await iconPx();
|
||||
out.strayRoomIsRejectedFromTheFrame = strayed.outliers === 1 && strayed.frameW < 1000;
|
||||
out.strayRoomDoesNotInflateTheIcons =
|
||||
plain.px > 4 && Math.abs(strayed.px - plain.px) < 0.5;
|
||||
if (!out.strayRoomDoesNotInflateTheIcons) console.log('icon px', plain, strayed);
|
||||
|
||||
checkAll(out);
|
||||
await finish(browser, out);
|
||||
|
||||
File diff suppressed because one or more lines are too long
Vendored
+1
-1
File diff suppressed because one or more lines are too long
+10
-2
@@ -139,6 +139,11 @@ When `outliers > 0` the card shows an unobtrusive inline hint (no
|
||||
modal) — "there are objects far from the plan" with a **Show** action
|
||||
that fits `all`.
|
||||
|
||||
Whatever the vote rejects is rejected **everywhere the plan is measured**,
|
||||
not only in the viewBox: §6's `iconUnit` runs the same vote over the rooms.
|
||||
One notion of "the plan", or a stray the frame had just thrown out came back
|
||||
as icons ninety times too big (audit DEV-2C947-03).
|
||||
|
||||
### §4.3 The frame in an editor
|
||||
|
||||
Inside an editor the frame only ever **grows** (`unionRect` with the previous
|
||||
@@ -229,7 +234,9 @@ there would have been worse than wrong: on a plan drawn 2 canvases
|
||||
wide the frame is ~2.2 canvases, so every marker would come out 2.2x
|
||||
smaller than on an ordinary plan — and 55x smaller on a plan 50
|
||||
canvases out, i.e. an invisible dot. `iconUnit(space) =
|
||||
max(NORM_W, roomsExtent)` is:
|
||||
max(NORM_W, mainMassOfTheRooms)` — the extent of the rooms **after the same
|
||||
outlier vote §4.1 applies to the frame** (rooms only, so the full card and the
|
||||
static card cannot drift apart), and it is:
|
||||
|
||||
* **exactly `NORM_W` for every plan that fits the old square**, and the
|
||||
editor has only ever stored `view_box: [0,0,1,1]`, so `iconUnit ===
|
||||
@@ -250,7 +257,8 @@ have made its markers shrink as the frame tightened.
|
||||
|
||||
**Auto-placement spacing** (`defaultPositions` -> `declump`) is measured
|
||||
in render units and uses the same `iconUnit`, so the icon's footprint
|
||||
and the distance markers are pushed apart by can never drift apart.
|
||||
and the distance markers are pushed apart by can never drift apart — and
|
||||
the outlier vote reaches the spacing through the very same call.
|
||||
|
||||
## §7 Adaptive grid
|
||||
|
||||
|
||||
@@ -1003,6 +1003,11 @@ require hands on real hardware — they remain for the human pass.
|
||||
there, deliberately), close the editor — View frames the room where it
|
||||
is NOW, not the union with where it was; re-entering the editor starts
|
||||
from the current geometry [auto: smoke_canvas_frame]
|
||||
- [ ] **A far stray does not inflate the icons either** (audit DEV-2C947-03):
|
||||
one ROOM dragged an order of magnitude away is rejected from the frame
|
||||
(as before) and the markers of the main plan keep the size they have
|
||||
without it; auto-placement spacing goes with them
|
||||
[auto: smoke_canvas_frame + unit canvas.test.mjs]
|
||||
- [ ] **A far stray does not break the view** (§4.1): a marker dragged an
|
||||
order of magnitude away leaves the opening view alone and raises the
|
||||
inline chip «Объектов далеко от плана: N» with «Показать». No modal.
|
||||
|
||||
+11
-1
@@ -292,11 +292,21 @@ export function spaceCenter(space: SpaceModel): Pt {
|
||||
* proportionally bigger unit, which is what keeps its markers from
|
||||
* degenerating into dots once the frame is the content (see `iconCqw`).
|
||||
* Rooms only — deterministic, so the full card and the static card agree.
|
||||
*
|
||||
* The MAIN MASS only, by the very same vote the frame uses (§4.1). A single
|
||||
* room dragged into the far corner is already rejected from `contentFrame.core`
|
||||
* — but it used to keep its distance in this numerator, so the plan the user
|
||||
* actually looks at grew icons ~91x too big while the frame stayed correct
|
||||
* (DEV-2C947-03). What is out of the frame is out of the icon unit: one notion
|
||||
* of "the plan", not two.
|
||||
*/
|
||||
export function iconUnit(space: SpaceModel): number {
|
||||
const items: ContentItem[] = [];
|
||||
for (const r of space.rooms || []) { const it = roomItem(r); if (it) items.push(it); }
|
||||
const b = boxOf(items);
|
||||
// pad 0: this is a UNIT, not a viewport — the frame's 5 % breathing room has
|
||||
// no business inflating the icons. Degenerate axes are still lifted off zero,
|
||||
// which cannot matter here (FLOOR < NORM_W).
|
||||
const b = contentFrame(items, { pad: 0 }).core;
|
||||
if (!b) return NORM_W;
|
||||
return Math.max(NORM_W, Math.min(SANE_LIMIT, Math.max(b.w, b.h)));
|
||||
}
|
||||
|
||||
@@ -106,6 +106,39 @@ test('icon size: no view yet is a plain percentage, never NaN', () => {
|
||||
assert.equal(iconCqw(2.5, m, 0, 1.5), 3.75);
|
||||
});
|
||||
|
||||
// DEV-2C947-03: what is out of the frame is out of the icon unit
|
||||
test('a room-outlier is excluded from the frame AND from the icon unit', () => {
|
||||
// three rooms in the core, a fourth one dragged 90 canvases out — MIN_VOTERS
|
||||
// is exactly met, so the vote runs and the frame rejects the stray.
|
||||
const rooms = [
|
||||
{ id: 'a', poly: [[0.10, 0.10], [0.30, 0.10], [0.30, 0.30], [0.10, 0.30]] },
|
||||
{ id: 'b', poly: [[0.30, 0.10], [0.55, 0.10], [0.55, 0.35], [0.30, 0.35]] },
|
||||
{ id: 'c', poly: [[0.10, 0.30], [0.35, 0.30], [0.35, 0.55], [0.10, 0.55]] },
|
||||
{ id: 'far', poly: [[90, 90], [91, 90], [91, 91], [90, 91]] },
|
||||
];
|
||||
const m = model({ id: 's', rooms });
|
||||
const f = contentFrame(contentItems(m));
|
||||
assert.equal(f.outliers, 1, 'the far room is rejected from the frame');
|
||||
assert.ok(f.core.w < 1000, 'the opening view is the house');
|
||||
// ...and the icon unit agrees: it used to be boxOf(ALL rooms) = 91000 units,
|
||||
// which made every marker of the ordinary plan ~91x too big while the frame
|
||||
// stayed correct (audit dev@2c947f4).
|
||||
assert.equal(iconUnit(m), NORM_W, 'the stray does not stretch the icon unit');
|
||||
const houseOnly = model({ id: 's', rooms: rooms.slice(0, 3) });
|
||||
assert.equal(iconUnit(m), iconUnit(houseOnly), 'same unit as the plan without it');
|
||||
// one shared notion of "the plan": the unit follows the frame's core
|
||||
assert.ok(iconCqw(2.5, m, 1000) < 3, 'the marker is a marker, not a wall');
|
||||
assert.equal(iconCqw(2.5, m, 1000), iconCqw(2.5, houseOnly, 1000));
|
||||
// a genuinely wide plan still scales — no vote, no rejection
|
||||
const wide = model({ id: 's', rooms: [
|
||||
{ id: 'a', poly: [[0, 0], [1, 0], [1, 1], [0, 1]] },
|
||||
{ id: 'b', poly: [[1, 0], [2, 0], [2, 1], [1, 1]] },
|
||||
{ id: 'c', poly: [[0, 1], [1, 1], [1, 2], [0, 2]] },
|
||||
{ id: 'd', poly: [[1, 1], [2, 1], [2, 2], [1, 2]] },
|
||||
] });
|
||||
assert.equal(iconUnit(wide), 2 * NORM_W);
|
||||
});
|
||||
|
||||
// ------------------------------------------------------------- the outlier
|
||||
test('an outlier does not command the frame, but "show all" reaches it', () => {
|
||||
const items = [
|
||||
|
||||
Reference in New Issue
Block a user