mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-02 21:01:21 +00:00
Icons scale with the plan again, and a 5 degree angle step
Two owner corrections after the infinite canvas. - --icon-size goes back to being a percentage of the PLAN: a marker grows and shrinks with the zoom, like everything else drawn on the plan. The infinite canvas had made it a percentage of the viewport (fixed pixel size) — the owner looked at it and asked for the original contract back. What survives from the canvas work is the NUMERATOR. The old expression divided by `vb.w`, the stored view_box, which is not a frame any more; a fixed NORM_W in its place would have shrunk every marker on a plan drawn past the old square by exactly the factor the plan is outsized (an invisible dot 50 canvases out). So it is now `iconCqw() = iconPct * iconUnit(space) * kioskScale / view.w`, one pure helper both renderers call. `iconUnit` is exactly NORM_W for any plan that fits the old square — and the editor has never written anything but `view_box: [0,0,1,1]` — so the rendered size is bit-identical to the pre-canvas card: measured against the v1.56.0 bundle at a fixed view, both give 3.400 / 3.091 / 6.182 / 12.364 cqw = 28.52 / 26.11 / 50.22 / 98.44 px. On a plan drawn at 1.5..3.8 the marker is 26.1 px, the same as on an ordinary plan, instead of the ~11 px a fixed numerator would have given. The static space-card uses the same helper: it has no zoom, but its frame is the content now, so a bare iconPct shrank its markers as the frame tightened. marker.size, the kiosk scales and every satellite still ride on --dev-size, untouched. - the icon angle in the device dialog steps by 5 degrees, not 10 (0..355): a marker often has to line up with a wall that is not on a 10-degree grid. Tests: three unit tests on iconCqw (the legacy expression reproduced digit for digit, the runaway plan, the no-view fallback); the infinite canvas smoke's "same pixel size at zoom 1/4/1/3" assert is turned back into "scales 4x / 1/3 with the zoom" plus a new one that the marker on the far plan measures the same as on an ordinary one; the angle step is pinned in smoke_size_angle_parity. docs/CANVAS.md §6 rewritten.
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -1,7 +1,7 @@
|
||||
// Infinite canvas (docs/CANVAS.md): a plan drawn far past the old unit square
|
||||
// renders whole, devices can be placed and saved out there, the opening view
|
||||
// follows the content, one far stray neither breaks the view nor hides itself,
|
||||
// zoom-out stops at 3x the content and icons keep their pixel size.
|
||||
// zoom-out stops at 3x the content and the icons keep their proportion to it.
|
||||
import { writeFileSync, mkdirSync } from 'node:fs';
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import { launch, check, checkAll, finish } from './serve.mjs';
|
||||
@@ -223,15 +223,25 @@ Object.assign(out, await page.evaluate(async () => {
|
||||
return o;
|
||||
}));
|
||||
|
||||
// ---- icons keep their PIXEL size across zooms (docs/CANVAS.md §6) --------
|
||||
out.iconSizeIsViewportRelative = await page.evaluate(async () => {
|
||||
// ---- icons scale WITH THE PLAN across zooms (docs/CANVAS.md §6) ----------
|
||||
// The infinite canvas briefly made --icon-size a percentage of the viewport,
|
||||
// so a marker kept its pixel size at every zoom. The owner looked at it and
|
||||
// asked for the original contract back (2026-08-03): an icon is a percentage
|
||||
// of the PLAN, it grows when you zoom in and shrinks when you zoom out, like
|
||||
// everything else drawn on the plan. What the infinite canvas contributes is
|
||||
// only the base unit the percentage is taken OF: `iconUnit` instead of the
|
||||
// stored `vb.w`, checked right below on this very plan — which is drawn
|
||||
// entirely outside the old square.
|
||||
const farIcon = await page.evaluate(async () => {
|
||||
const c = window.__card;
|
||||
const sr = c.shadowRoot || c.renderRoot;
|
||||
// the badge is --dev-size PLUS a 1px border on each side, and the border
|
||||
// is not part of the scaled geometry (same correction as smoke_icon_scale)
|
||||
const size = () => {
|
||||
const el = sr.querySelector('.devlayer .dev');
|
||||
const r = el.getBoundingClientRect();
|
||||
return Math.round(r.width * 10) / 10;
|
||||
return Math.round((el.getBoundingClientRect().width - 2) * 100) / 100;
|
||||
};
|
||||
const o = {};
|
||||
c._resetZoom(); await c.updateComplete;
|
||||
const at1 = size();
|
||||
c._applyView(4); await c.updateComplete;
|
||||
@@ -239,8 +249,12 @@ out.iconSizeIsViewportRelative = await page.evaluate(async () => {
|
||||
c._applyView(1 / 3); await c.updateComplete;
|
||||
const atOut = size();
|
||||
c._resetZoom(); await c.updateComplete;
|
||||
return at1 > 4 && Math.abs(at4 - at1) <= 1 && Math.abs(atOut - at1) <= 1;
|
||||
o.iconGrowsWithZoomIn = at1 > 4 && Math.abs(at4 / at1 - 4) < 0.05;
|
||||
o.iconShrinksWithZoomOut = Math.abs(atOut / at1 - 1 / 3) < 0.02;
|
||||
if (!o.iconGrowsWithZoomIn || !o.iconShrinksWithZoomOut) console.log('icon px', at1, at4, atOut);
|
||||
return { o, at1 };
|
||||
});
|
||||
Object.assign(out, farIcon.o);
|
||||
|
||||
// ---- an OLD plan (small, with a view_box) opens exactly as before ---------
|
||||
Object.assign(out, await page.evaluate(async () => {
|
||||
@@ -278,6 +292,35 @@ Object.assign(out, await page.evaluate(async () => {
|
||||
return o;
|
||||
}));
|
||||
|
||||
// ---- and the icon does not DEGENERATE on the runaway plan ----------------
|
||||
// The catch of "a percentage of the plan" on an unbounded canvas: keep the
|
||||
// old fixed numerator (vb.w = NORM_W) and a plan drawn 2.3 canvases wide
|
||||
// divides it by a 2.5-canvas-wide frame — the marker shrinks by that factor
|
||||
// and keeps shrinking the further out the plan is drawn. `iconUnit` grows
|
||||
// with the plan, so the marker on the far plan measured above is the same
|
||||
// number of pixels as on this ordinary 0..1 one.
|
||||
out.farIconNotDegenerate = await page.evaluate(async (farPx) => {
|
||||
const c = window.__card;
|
||||
const sr = c.shadowRoot || c.renderRoot;
|
||||
// the small plan from the block above, with its devices back
|
||||
c._serverCfg = { spaces: [{ id: 'f1', title: 'Old', view_box: [0, 0, 1, 1], plan_url: null, plan_aspect: null,
|
||||
rooms: [{ id: 'r1', name: 'Living', area: 'living_room',
|
||||
poly: [[0.04, 0.14], [0.55, 0.14], [0.55, 0.58], [0.04, 0.58]] },
|
||||
{ id: 'r2', name: 'Kitchen', area: 'kitchen',
|
||||
poly: [[0.55, 0.14], [0.96, 0.14], [0.96, 0.46], [0.55, 0.46]] }] }],
|
||||
markers: [], settings: {} };
|
||||
c._layout = { d_light1: { s: 'f1', x: 0.22, y: 0.22 } };
|
||||
c._modelCache = null; c._frame = null; c._showFar = false; c._view = null;
|
||||
c._maybeRebuildDevices?.();
|
||||
c._defPos = c._defaultPositions();
|
||||
c.requestUpdate(); await c.updateComplete;
|
||||
c._resetZoom(); await c.updateComplete;
|
||||
const el = sr.querySelector('.devlayer .dev');
|
||||
const px = el.getBoundingClientRect().width - 2;
|
||||
if (!(px > 4 && Math.abs(px / farPx - 1) < 0.3)) console.log('icon px near/far', px, farPx);
|
||||
return px > 4 && Math.abs(px / farPx - 1) < 0.3;
|
||||
}, farIcon.at1);
|
||||
|
||||
// ---- screenshots ---------------------------------------------------------
|
||||
if (SHOTS) {
|
||||
mkdirSync(SHOTS, { recursive: true });
|
||||
|
||||
@@ -58,4 +58,33 @@ const out = await page.evaluate(async () => {
|
||||
c._cfgEpoch++; c._regSignature = ''; c._maybeRebuildDevices();
|
||||
return o;
|
||||
});
|
||||
await finish(browser, checkAll(out));
|
||||
checkAll(out);
|
||||
|
||||
// Шаг угла в диалоге устройства — 5°, а не 10° (владелец, 2026-08-03):
|
||||
// значок часто надо выровнять по стене, которая не лежит на сетке в 10°.
|
||||
const out3 = await page.evaluate(async () => {
|
||||
const o = {};
|
||||
const c = window.__card;
|
||||
const sr = () => c.shadowRoot || c.renderRoot;
|
||||
c._setMode('devices'); await c.updateComplete;
|
||||
const dev = c._devices.find((d) => d.space === 'f1');
|
||||
c._openMarkerDialog(dev); await c.updateComplete;
|
||||
// строка «размер · угол»: два ползунка, второй — угол
|
||||
const rows = [...sr().querySelectorAll('.dialog .colorrow')];
|
||||
const row = rows.find((r) => r.textContent.includes('°'));
|
||||
const ctl = [...row.querySelectorAll('ha-slider, input[type=range]')];
|
||||
const stepOf = (el) => Number(el.step ?? el.getAttribute('step'));
|
||||
const maxOf = (el) => Number(el.max ?? el.getAttribute('max'));
|
||||
o.angleStepIs5 = ctl.length === 2 && stepOf(ctl[1]) === 5;
|
||||
o.angleReaches355 = maxOf(ctl[1]) === 355;
|
||||
o.sizeStepUnchanged = stepOf(ctl[0]) === 0.1;
|
||||
// и значение реально ложится на 5°
|
||||
ctl[1].value = 35;
|
||||
ctl[1].dispatchEvent(new Event('input', { bubbles: true, composed: true }));
|
||||
await c.updateComplete;
|
||||
o.angle35Applied = c._markerDialog.angle === 35;
|
||||
c._markerDialog = null; await c.updateComplete;
|
||||
c._setMode('view'); await c.updateComplete;
|
||||
return o;
|
||||
});
|
||||
await finish(browser, checkAll(out3));
|
||||
|
||||
File diff suppressed because one or more lines are too long
Vendored
+21
-19
File diff suppressed because one or more lines are too long
+44
-23
@@ -20,7 +20,7 @@ coordinate system.
|
||||
device positions are still stored normalised. `1.0` is still the
|
||||
same distance it always was; `cell_cm` still ties a grid cell to
|
||||
real centimetres. **No data migration.** An existing plan opens as
|
||||
before (the one deliberate exception is icon size — see §6).
|
||||
before — including the size of the markers (§6).
|
||||
2. **`0..1` is not a boundary, it is an origin.** Any finite
|
||||
coordinate is legal. `2.7` simply means "2.7 canvas widths to the
|
||||
right of the origin".
|
||||
@@ -39,7 +39,7 @@ coordinate system.
|
||||
| "fit" rectangle | `view_box` (or the content bbox in view mode) | always the **content frame** (§4) |
|
||||
| Zoom out floor | `ZOOM_MIN = 0.4` (fraction of `view_box`) | 3x the content frame (`MIN_ZOOM = 1/3`) |
|
||||
| Pan bounds | content must cover the scene | content frame + one screen of slack in each direction |
|
||||
| Icon size | % of `view_box`, i.e. grew with zoom | % of the visible viewport (§6) |
|
||||
| Icon size | % of `view_box`, i.e. grew with zoom | % of `iconUnit` — still grows with zoom (§6) |
|
||||
| Validation range | `+/-4` | `+/-5000` (§3) |
|
||||
|
||||
### Render frame vs. view
|
||||
@@ -162,39 +162,60 @@ was silently excluded from the frame).
|
||||
in the frame's direction. Clicking it fits the content. Cheap
|
||||
insurance against getting lost in the empty plane.
|
||||
|
||||
## §6 Icon size — from the viewport
|
||||
## §6 Icon size — a percentage of the plan
|
||||
|
||||
**Changed behaviour, owner is aware.** Before:
|
||||
**Unchanged behaviour** — an icon scales with the plan, exactly as it
|
||||
always did. (A first cut of the infinite canvas made it a fixed
|
||||
percentage of the viewport; the owner looked at it on 2026-08-03 and
|
||||
asked for the original back. The history is kept here because the
|
||||
reasoning for the *numerator* below is the whole point.)
|
||||
|
||||
Before the infinite canvas:
|
||||
|
||||
```
|
||||
--icon-size: iconPct * vb.w / view.w (cqw)
|
||||
```
|
||||
|
||||
so an icon grew as you zoomed in — at 8x zoom a marker covered a whole
|
||||
room. Now:
|
||||
Now (`iconCqw()` in `src/space-geometry.ts`, pure and unit tested):
|
||||
|
||||
```
|
||||
--icon-size: iconPct * kioskScale (cqw)
|
||||
--icon-size: iconPct * iconUnit(space) * kioskScale / view.w (cqw)
|
||||
```
|
||||
|
||||
An icon is a fixed percentage of the **visible viewport**: zooming
|
||||
changes how much plan you see, not how big the markers are. Side
|
||||
effects, all intended:
|
||||
Read it in render units: a marker always occupies
|
||||
`iconPct/100 * iconUnit` **render units** of the plan, whatever the
|
||||
frame and whatever the zoom. Dividing by the width of the visible view
|
||||
turns that into the percentage of the container `cqw` means. Zoom in
|
||||
2x and the marker is 2x bigger, together with the walls it sits on.
|
||||
|
||||
* the full card and the static `houseplan-space-card` now use the
|
||||
identical expression — the two renderers finally agree at every zoom;
|
||||
* the per-device multiplier `marker.size` and the kiosk icon/font
|
||||
scales are untouched: they still feed `--dev-size`, and every
|
||||
satellite (badges, LQI chips, presence rings, ripples) still derives
|
||||
from `--dev-size` exactly as before.
|
||||
**Why the numerator changed.** `vb.w` was the stored `view_box`, and
|
||||
`view_box` is not a frame any more (§4). Keeping a fixed `NORM_W`
|
||||
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:
|
||||
|
||||
* **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 ===
|
||||
vb.w` there and the rendered pixel size is bit-identical to the
|
||||
pre-canvas card (verified against the v1.56.0 bundle at a fixed view:
|
||||
`3.400 / 3.091 / 6.182 / 12.364 cqw`, i.e. `28.52 / 26.11 / 50.22 /
|
||||
98.44 px`, both bundles);
|
||||
* **proportional to an outsized plan**, so a runaway plan gets markers
|
||||
of the same apparent size as an ordinary one.
|
||||
|
||||
Everything else is untouched: the per-device multiplier `marker.size`
|
||||
and the kiosk icon/font scales still feed `--dev-size`, and every
|
||||
satellite (badges, LQI chips, presence rings, ripples) still derives
|
||||
from `--dev-size`. The full card and the static
|
||||
`houseplan-space-card` call the same `iconCqw()` — the static card has
|
||||
no zoom, but its frame is the content now, so a bare `iconPct` would
|
||||
have made its markers shrink as the frame tightened.
|
||||
|
||||
**Auto-placement spacing** (`defaultPositions` -> `declump`) is measured
|
||||
in render units, so it needs the icon's render-unit footprint, which is
|
||||
now frame-relative. `iconUnit(space) = max(NORM_W, roomsExtent)` is
|
||||
used instead of a bare `NORM_W`: for any plan that fits the old square
|
||||
this is exactly `NORM_W` (bit-identical placement, no churn), and for a
|
||||
plan three canvases wide the spacing grows with it. Both renderers call
|
||||
the same helper, so the static card and the full card stay in step.
|
||||
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.
|
||||
|
||||
## §7 Adaptive grid
|
||||
|
||||
@@ -232,7 +253,7 @@ fits `all`.
|
||||
| `contentBounds` envelope `-25 %..125 %` | content outside the square does not count | **removed** — replaced by §4.1 outlier rejection |
|
||||
| `_baseVb()` `if (mode !== 'view') return m.vb` | editors need the whole square to have room to draw | **removed** — the content frame plus §5 pan slack and 3x zoom-out gives more room than the square ever did |
|
||||
| `_baseVb()` `if (m.bg) return m.vb` | image plans frame on the square | image rect is now just one content item (§4) |
|
||||
| `--icon-size` scaled by `vb.w / view.w` | icon is a fraction of the canvas | §6 |
|
||||
| `--icon-size` scaled by `vb.w / view.w` | the canvas is what an icon is a fraction OF | numerator becomes `iconUnit()`; the icon still scales with the plan (§6) |
|
||||
| `defaultPositions` `minDist` from `NORM_W` | one canvas = one plan | `iconUnit()` (§6) |
|
||||
| `markerPos` / `_pos` fallback = `view_box` centre | a device with no position belongs in the middle of the square | `spaceCenter()` — the middle of the content |
|
||||
| grid `<rect>` over `vb` | the grid ends with the square | rect follows the view (§7) |
|
||||
|
||||
+14
-9
@@ -51,7 +51,7 @@ import './space-card';
|
||||
import { cardStyles } from './styles';
|
||||
import {
|
||||
fitInSquare, contentBounds, spaceModels, contentFrame, contentItems, spaceFrame,
|
||||
spaceCenter, iconUnit, gridLevels, itemOf,
|
||||
spaceCenter, iconUnit, iconCqw, gridLevels, itemOf,
|
||||
MIN_ZOOM, PAN_SLACK, CANVAS_LIMIT, type ContentItem, type Rect,
|
||||
} from './space-geometry';
|
||||
import { langOf, t, type I18nKey } from './i18n';
|
||||
@@ -5663,14 +5663,17 @@ class HouseplanCard extends LitElement {
|
||||
${this._renderOpenings(disp)}
|
||||
${this._markup && this._tool === 'resize' ? this._renderResizeLayer(view) : nothing}
|
||||
</svg>
|
||||
${''/* docs/CANVAS.md §6: --icon-size is a percentage of the VISIBLE
|
||||
viewport (cqw), no longer of the canvas — the old
|
||||
`* vb.w / view.w` made a marker grow with the zoom until it
|
||||
covered a room, and on an unbounded canvas "a percent of the
|
||||
canvas" has no meaning at all. Same expression as the static
|
||||
space-card, so the two renderers finally agree. The per-device
|
||||
${''/* docs/CANVAS.md §6: an icon is a percentage of the PLAN and
|
||||
scales with it when you zoom — the behaviour the card always
|
||||
had, restored by the owner. `iconCqw` is `iconPct * iconUnit
|
||||
/ view.w`: the old expression with the stored `vb.w` replaced
|
||||
by the plan's own base unit, which is the same NORM_W for an
|
||||
ordinary plan (pixel-identical) but grows with a plan drawn
|
||||
past the old square, where a fixed 1000 would have shrunk
|
||||
every marker to a dot. Same expression as the static
|
||||
space-card, so the two renderers agree. The per-device
|
||||
multiplier and the kiosk scales still feed --dev-size. */}
|
||||
<div class="devlayer" style="--icon-size:${(iconPct * (this._kiosk ? this._kioskScale.icon : 1)).toFixed(3)}cqw;--rl-font:${this._kiosk ? this._kioskScale.font : 1}">
|
||||
<div class="devlayer" style="--icon-size:${iconCqw(iconPct, space, view.w, this._kiosk ? this._kioskScale.icon : 1).toFixed(3)}cqw;--rl-font:${this._kiosk ? this._kioskScale.font : 1}">
|
||||
${devs.map((d) => this._renderDevice(d, view, showLqi, disp.fill === 'glow' && !this._markup))}
|
||||
${this._renderVacuums(devs, view)}
|
||||
${this._renderVacFit(view)}
|
||||
@@ -7553,7 +7556,9 @@ class HouseplanCard extends LitElement {
|
||||
${this._rangeInput(0.5, 3, 0.1, d.size, (n) => (this._markerDialog = { ...d, size: n }))}
|
||||
<span class="opv">×${d.size.toFixed(1)}</span>
|
||||
<span class="opl">${this._t('marker.angle_label')}</span>
|
||||
${this._rangeInput(0, 350, 10, d.angle, (n) => (this._markerDialog = { ...d, angle: n }))}
|
||||
${''/* 5 degrees, not 10 (owner 2026-08-03): a marker often has to
|
||||
line up with a wall that is not on a 10-degree grid. */}
|
||||
${this._rangeInput(0, 355, 5, d.angle, (n) => (this._markerDialog = { ...d, angle: n }))}
|
||||
<span class="opv">${d.angle}°</span>
|
||||
</div>
|
||||
|
||||
|
||||
+38
-4
@@ -281,10 +281,16 @@ export function spaceCenter(space: SpaceModel): Pt {
|
||||
}
|
||||
|
||||
/**
|
||||
* Render units one icon-percent is measured against for AUTO-PLACEMENT
|
||||
* spacing (docs/CANVAS.md §6). For any plan that fits the old square this
|
||||
* is exactly NORM_W, so existing layouts do not move by a hair; a plan
|
||||
* three canvases wide gets proportionally wider spacing.
|
||||
* The BASE UNIT one icon-percent is measured against, in render units
|
||||
* (docs/CANVAS.md §6). It is both the icon's own footprint and the
|
||||
* auto-placement spacing, so the two can never drift apart.
|
||||
*
|
||||
* For any plan that fits the old square this is exactly NORM_W — which is
|
||||
* what `vb.w` was for every plan the card itself ever wrote (the editor only
|
||||
* ever stored `view_box: [0,0,1,1]`), so sizes and layouts are bit-identical
|
||||
* to the pre-infinite-canvas card. A plan drawn three canvases wide gets a
|
||||
* 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.
|
||||
*/
|
||||
export function iconUnit(space: SpaceModel): number {
|
||||
@@ -295,6 +301,34 @@ export function iconUnit(space: SpaceModel): number {
|
||||
return Math.max(NORM_W, Math.min(SANE_LIMIT, Math.max(b.w, b.h)));
|
||||
}
|
||||
|
||||
/**
|
||||
* `--icon-size` in cqw (docs/CANVAS.md §6) — the ONE expression both
|
||||
* renderers use.
|
||||
*
|
||||
* An icon is a percentage of the PLAN, not of the viewport: it scales with
|
||||
* the plan as you zoom, exactly as it did before the infinite canvas (owner,
|
||||
* 2026-08-03). In render units the marker always occupies
|
||||
* `iconPct/100 * iconUnit`, whatever the frame or the zoom happens to be;
|
||||
* dividing by the width of the visible view turns that into a percentage of
|
||||
* the container, which is what `cqw` means.
|
||||
*
|
||||
* The only thing the infinite canvas changed here is the numerator: it used
|
||||
* to be `vb.w`, the stored view_box, and a plan drawn past the old square
|
||||
* kept a 1000-unit numerator while its frame grew to tens of thousands —
|
||||
* markers would shrink to invisible dots. `iconUnit` is that same 1000 for
|
||||
* an ordinary plan and grows with an outsized one.
|
||||
*/
|
||||
export function iconCqw(
|
||||
iconPct: number, space: SpaceModel, viewW: number, kioskIcon = 1,
|
||||
): number {
|
||||
const w = Number(viewW);
|
||||
const k = Number.isFinite(kioskIcon) && kioskIcon > 0 ? kioskIcon : 1;
|
||||
// No view yet (first paint, zero-width stage): fall back to the plain
|
||||
// percentage rather than to Infinity/NaN — the frame arrives a tick later.
|
||||
if (!Number.isFinite(w) || w <= 0) return iconPct * k;
|
||||
return (iconPct * iconUnit(space) * k) / w;
|
||||
}
|
||||
|
||||
/** Grid step multipliers offered to the adaptive grid (docs/CANVAS.md §7). */
|
||||
export const GRID_STEPS = [1, 2, 5, 10, 20, 50, 100, 200, 500, 1000];
|
||||
|
||||
|
||||
+8
-2
@@ -14,7 +14,7 @@ import { t, type Lang } from './i18n';
|
||||
import { bgModeOf, northDegOf, sunStateOf, dayPhase } from './sun';
|
||||
import type { ServerConfig } from './types';
|
||||
import {
|
||||
spaceModels, roomCenter, defaultPositions, markerPos, labelPos, spaceFrame, NORM_W,
|
||||
spaceModels, roomCenter, defaultPositions, markerPos, labelPos, spaceFrame, iconCqw, NORM_W,
|
||||
type Layout, type ContentItem,
|
||||
} from './space-geometry';
|
||||
|
||||
@@ -198,7 +198,13 @@ export function renderSpaceStatic(o: StaticRenderOpts): TemplateResult | null {
|
||||
: nothing}
|
||||
${roomShapes}
|
||||
</svg>
|
||||
<div class="devlayer" style="--icon-size:${iconPct}cqw">${markers}${labels}</div>
|
||||
${''/* docs/CANVAS.md §6: the same expression as the full card. The
|
||||
static card has no zoom, but its frame is the CONTENT now, so a
|
||||
bare `iconPct` would make markers shrink relative to the plan the
|
||||
tighter the frame got. `iconCqw` keeps the marker's footprint at
|
||||
iconPct% of the plan's base unit, which is what it was when the
|
||||
frame was the stored view_box. */}
|
||||
<div class="devlayer" style="--icon-size:${iconCqw(iconPct, space, vb[2]).toFixed(3)}cqw">${markers}${labels}</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
+45
-1
@@ -4,7 +4,7 @@ import assert from 'node:assert/strict';
|
||||
import {
|
||||
NORM_W, CANVAS_LIMIT, SANE_LIMIT, MIN_ZOOM, OUTLIER_K, MIN_VOTERS,
|
||||
spaceModels, contentItems, contentFrame, contentBounds, spaceFrame, spaceCenter,
|
||||
iconUnit, gridLevels, itemOf, roomItem, defaultPositions,
|
||||
iconUnit, iconCqw, gridLevels, itemOf, roomItem, defaultPositions,
|
||||
} from '../test-build/space-geometry.js';
|
||||
|
||||
const model = (space) => spaceModels({ spaces: [{ view_box: [0, 0, 1, 1], rooms: [], ...space }], markers: [] })[0];
|
||||
@@ -62,6 +62,50 @@ test('a plan past the square keeps its icon spacing in proportion', () => {
|
||||
}
|
||||
});
|
||||
|
||||
// ------------------------------------------------------------- icon size
|
||||
test('icon size: a percentage of the PLAN, exactly as before the canvas', () => {
|
||||
// The pre-infinite-canvas card rendered `--icon-size: iconPct * vb.w / view.w`
|
||||
// and the editor only ever wrote view_box [0,0,1,1], i.e. vb.w === NORM_W.
|
||||
// iconCqw must reproduce that number to the last digit on such a plan.
|
||||
const small = model({ id: 's', rooms: [{ id: 'r', poly: [[0.04, 0.14], [0.96, 0.14], [0.96, 0.86], [0.04, 0.86]] }] });
|
||||
const legacy = (pct, viewW) => (pct * NORM_W) / viewW;
|
||||
for (const viewW of [1000, 1100, 1093.969144460028, 550, 275, 137.5]) {
|
||||
assert.equal(iconCqw(3.4, small, viewW), legacy(3.4, viewW));
|
||||
}
|
||||
// and that number IS "scales with the plan": half the view, twice the icon
|
||||
assert.equal(iconCqw(2.5, small, 500) / iconCqw(2.5, small, 1000), 2);
|
||||
assert.equal(iconCqw(2.5, small, 8000) / iconCqw(2.5, small, 1000), 0.125);
|
||||
// the kiosk icon multiplier is a plain factor, as it was
|
||||
assert.equal(iconCqw(2.5, small, 1000, 2), 2 * iconCqw(2.5, small, 1000));
|
||||
});
|
||||
|
||||
test('icon size: a plan drawn past the old square keeps its markers', () => {
|
||||
// rooms at 1.0..3.0 — 2 canvases wide, framed by ~2.2 canvases once the
|
||||
// frame is the content. With the old fixed NORM_W numerator the marker
|
||||
// would be 2.2x smaller than on an ordinary plan, and 55x smaller on a
|
||||
// plan 50 canvases wide: the reason the numerator is iconUnit.
|
||||
const big = model({ id: 's', rooms: [{ id: 'r', poly: [[1, 1], [3, 1], [3, 3], [1, 3]] }] });
|
||||
const small = model({ id: 's', rooms: [{ id: 'r', poly: [[0, 0], [1, 0], [1, 1], [0, 1]] }] });
|
||||
const frame = (m) => { const f = spaceFrame(m); return f.w; }; // content + 5%
|
||||
assert.equal(frame(small), NORM_W * 1.1);
|
||||
assert.equal(frame(big), 2 * NORM_W * 1.1);
|
||||
// same percentage of the frame on both plans — the marker did not degenerate
|
||||
assert.equal(iconCqw(2.5, big, frame(big)), iconCqw(2.5, small, frame(small)));
|
||||
assert.ok(iconCqw(2.5, big, frame(big)) > 2, 'and it is a real size, not a dot');
|
||||
// the degenerate alternative, for the record
|
||||
assert.ok((2.5 * NORM_W) / frame(big) < iconCqw(2.5, big, frame(big)) / 1.9);
|
||||
// the size unit and the auto-placement spacing are the same unit
|
||||
assert.equal(iconUnit(big), 2 * NORM_W);
|
||||
});
|
||||
|
||||
test('icon size: no view yet is a plain percentage, never NaN', () => {
|
||||
const m = model({ id: 's', rooms: [{ id: 'r', poly: [[0.1, 0.1], [0.5, 0.5]] }] });
|
||||
for (const bad of [0, -1, NaN, Infinity, null, undefined]) {
|
||||
assert.equal(iconCqw(2.5, m, bad), 2.5);
|
||||
}
|
||||
assert.equal(iconCqw(2.5, m, 0, 1.5), 3.75);
|
||||
});
|
||||
|
||||
// ------------------------------------------------------------- the outlier
|
||||
test('an outlier does not command the frame, but "show all" reaches it', () => {
|
||||
const items = [
|
||||
|
||||
Reference in New Issue
Block a user