mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-04 21:58:56 +00:00
2fd46493db98eeb867b9d70d785f11536c4cf278
11
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
2fd46493db | v1.58.0-beta.1: backdrop transform, paper by rooms, decor tool fix | ||
|
|
df233905c5 |
DEV-B58: one bound, one grid — the canvas border and the snap contract
Two owner reports after v1.57.0, both about coordinates.
=== DEV-B58-01: nothing stops at the old canvas border any more ===
The infinite canvas freed the FRAME and the DRAWING; it did not free the
drag handlers, and both the owner and a user hit that within a day:
"названия комнат и устройства не перетаскиваются дальше старых границ
холста".
Two clamps survived v1.57.0, and the second is the worse one:
* `_pointerMove` (device marker) clamped into `_baseVb()` — the CONTENT
FRAME, with a 0.8 % inset. A marker could never be dragged past the
outline of what was already drawn, so a plan could not be extended by
putting a device where the next room was going to be.
* `_labelMove` (room label) clamped into `_spaceModel().vb` — the
space's STORED `view_box`, which is `[0,0,1,1]` for every plan the
card has ever written. Literally the old square: a room drawn at 2.5
had a name that could not reach its own room.
And one asymmetry: `_decorCommitDraft` and the decor text anchor had no
guard at all, while `_decorMoveUpdate` did — a draft could be born
outside the range the mover then refused to leave.
The rule now is one line: an editor gesture has exactly ONE bound,
`+/-CANVAS_LIMIT`, the same number `validation.py` enforces, and it is a
garbage limit rather than a frame. `clampCanvasR` / `clampCanvasN` in
space-geometry.ts are the only two functions allowed to impose it, and
`_snap()` applies it on the way out, so every gesture that goes through
the snap is bounded by construction.
demo/smoke_drag_bounds.mjs starts from an ORDINARY plan (rooms inside
0..1, so the old clamps really were in the way), drags a marker, a room
name, a decor shape and an opening far past the old square, checks each
arrives, is stored, survives a rebuild and takes the frame with it — and
that a wild drag still parks at exactly 5000 rather than 1e12. Seven of
its eleven facts fail by name on
|
||
|
|
aa01eaef01 | v1.57.0 | ||
|
|
93b60c5b8e |
Editor grid dots are a hint: mute both levels (0.35 / 0.5)
The adaptive grid drew at full strength — on the white paper of a drawn plan the dots argued with the walls instead of guiding them. Both levels are dimmed, the CAD hierarchy kept: fine dots 0.75 -> 0.35, coarse nodes 1 -> 0.5, so the accents still carry the scale reference on the dark scene background. Editors only; View draws no grid (smoke_grid_fade). |
||
|
|
0af50a74ae |
A kiosk pan stays a pan all the way to the release
The gesture is classified once, on the first movement past 8 px (`_panLock`), but `_stagePointerUp` ignored that decision and asked `swipeTarget()` again from the raw start→end vector — audit DEV-1DA1-02. So a CURVED gesture could be both: a small vertical lead-in locked `pan`, the plan started following the finger, the trajectory then swept far sideways, and lifting the finger landed the user on another storey. On a wall tablet that is the worst kind of surprise — you watch the plan drag along and end up on a different floor. The lock is now final: with `_panLock === 'pan'` the floor never changes, whatever the overall vector looks like, and only a gesture locked as `swipe` may reach `swipeTarget()`. A motionless tap locks nothing, so the double-tap zoom reset is untouched. Regression: demo/smoke_kiosk_pan_lock.mjs — the auditor's curved pan (both directions and a long diagonal), the mirror case of a swipe that bends vertically (it never pans, and if it stops qualifying it simply does nothing), plus the straight swipe / straight pan / double tap / zoomed-in cases. docs/CANVAS.md §5 and docs/TESTING.md updated. |
||
|
|
ade8daab16 |
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.
|
||
|
|
05a2a838d6 |
The editor's grown frame stays in the editor
Audit dev@2c947f4, DEV-2C947-02 (P2). Move the only room from 0.1..0.9 to 5.1..5.9 inside the Plan editor and go back to View: the frame stayed 5880 units wide instead of the room's 880, and only a manual `_frame = null` put it right. Anything that moves, deletes or heavily resizes geometry in an editor left View looking at ground the plan no longer occupies — until some unrelated model/layout/device change happened to invalidate the memo. The growth itself is deliberate and stays (docs/CANVAS.md §4.3): inside an editor the frame bounds pan and defines what zoom 1 means, and one that shrank the instant a room was deleted would move the ground under a live gesture. The bug was that the growth was invisible to the memo — `_frame`'s key carried the space, the model, the layout, the devices and the show-far flag, but not the mode, so the accumulated union was handed straight back in View. `grow` (`_mode !== 'view'`) is now part of the key, and the union is only ever taken against a frame the same editor session produced. Leaving an editor recomputes from the content; entering one starts from the current geometry instead of resurrecting the union of a previous session. smoke_canvas_frame.mjs grows the auditor's scenario: the frame before, the union inside the editor (asserted, so the growth cannot be "fixed" by deleting it), the frame after exit — 5060..5940 — and re-entry. Two checks are red on the parent commit. |
||
|
|
f4ad843619 |
A hidden device no longer stretches the plan's frame
Audit dev@2c947f4, DEV-2C947-01 (P2). One visible room and one marker with a saved position 90 canvases out, then the marker is hidden: the auditor's probe measured a frame 112.375x wider than the room it drew — the house opened as a dot in the corner of empty canvas. The same on `houseplan-space-card`. Both cards filtered the devices for RENDERING and framed the unfiltered list. The full card's `_contentItems` walked `_devices` without looking at `hidden`, while the renderer a few lines later drew `!d.hidden`; `space-render.ts` said it out loud — `devs = spaceDevs.filter(d => !d.hidden)` for the markers, `spaceDevs` for the frame. The frame is PRESENTATION (docs/CANVAS.md §4), so it follows what is drawn. Hidden devices keep everything the filtering contract gives them: they are still built, still counted by room LQI, still hold their cell in the auto-grid roster (so hiding one does not move a visible neighbour) — they are simply not content items. The device editor's ghosts are not items either: reaching a ghost is what the §5 pan slack is for, and making the frame follow a local, ephemeral editor toggle would have made the opening view depend on which tab had it switched on. demo/smoke_canvas_frame.mjs is the auditor's probe, both cards: with the marker visible the frame holds it (2 items is below MIN_VOTERS, so the outlier vote cannot quietly rescue the test); hidden, the marker is gone from the DOM, the frame is exactly the room's 60..940 and the room fills the stage. Three of its checks are red on the parent commit. |
||
|
|
fd72330549 |
Drag the plan at any zoom, not only above 100%
Owner's report 2026-08-04: «добавь возможность таскать план при любом масштабе, а не только при более 100%, как сейчас (и в редакторах, и в просмотре)». _stagePointerMove moved the view only while `_zoom > 1`. That gate is older than the infinite canvas and made sense under the old rule — the content had to cover the scene, so at 100% or below there was literally nowhere to go and a drag could only jitter. The infinite canvas removed the edge and gave panning a slack of one screen past the content in every direction (CANVAS.md §5), and from that moment the gate was not a guard but a missing feature: at 100% you could see the arrow «home is that way» light up from a wheel-zoom, and still not drag the plan an inch. The zoom no longer takes part in the decision — `_clampView` alone says how far you may walk, at 400% and at 33% alike. The drag also stopped depending on `_view` being materialised: it reads `_viewOr(baseVb)`, so the very first drag on a freshly opened space pans instead of doing nothing. Gesture ownership is unchanged, and that is the point of most of the new smoke: `_stagePointerDown` still bails out on the room-resize handles, device badges, openings, room labels and decor shapes, and on a decor drawing tool that consumes the press; two fingers are still a pinch. The one place where a drag had a rival is the kiosk, where a horizontal swipe changes floors. It is now classified once per gesture, on the first movement past 8px (`_panLock`): horizontal in the swipe zone (kiosk, zoom <= 1, more than one space) = swipe and no pan, everything else = pan. So the plan never slides out from under a swipe, a vertical drag on a wall tablet pans as it does everywhere else, and zoomed in — where swipeTarget already refuses — a horizontal drag pans. demo/smoke_pan_any_zoom.mjs: a drag on empty scene moves the view at 100%, 50% and 1/3 in View and in every editor (all seven plan tools, Devices, Background), and at 400% as before; the walk stops at the PAN_SLACK limit and the home arrow appears; a resize handle resizes, a device badge moves the device and an opening slides along its wall, none of them panning a pixel; two fingers still zoom; the kiosk still swipes floors through a gesture that has real pointermove events in it (smoke_kiosk only ever sent down+up), a vertical drag there pans, and a zoomed-in horizontal drag pans without changing the floor. Before the fix 25 of its checks are red, including every editor at every zoom. |
||
|
|
2c947f4f7a |
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. |
||
|
|
47ab60cddd |
Infinite canvas: the spec, the pure geometry and the ±5000 limits
docs/CANVAS.md is the source of truth (owner-approved 2026-08-03): the normalised square was never a sheet of paper, only a coordinate system, and users who drew past its edge could not place devices there. Storage does not change and there is no migration. What changes is what the renderers DERIVE from it: - space-geometry.ts gains contentFrame() — one item per drawn object, a rank-based outlier vote (median centre, 75th-percentile spread, 10x threshold, majority veto) and a fit-everything box beside the opening view. contentBounds() is now a thin wrapper over it; the old -25%..125% envelope is gone — it WAS the bug that made a plan drawn at 1.5..3.0 frame empty canvas. - spaceFrame()/spaceCenter() make view_box an optional first-frame hint used only when there is nothing to frame; iconUnit() keeps auto placement spacing in proportion (NORM_W for anything inside the old square, so no layout moves); gridLevels() picks a legible grid step. - validation.py: coordinates ±4 -> ±5000, sizes 0.001..5000, decor -1..2 -> ±5000, opening length <= 5000. Garbage insurance, not a frame — a stored 1e100 is still refused. Units: test/canvas.test.mjs covers the plan past the square, the outlier (and the three ways NOT to declare one), corruption, empty space, a lone marker, image plans and the adaptive grid. Backend: the limits, and that a config from any released version validates untouched. |