fix(touch): separate pan, edge swipe and double fit (#691)

Issue: #691
User-Visible: yes
This commit is contained in:
Matysh
2026-09-28 16:27:45 +03:00
parent 3f17a1497d
commit 37fbb9824d
17 changed files with 665 additions and 150 deletions
+21 -15
View File
@@ -263,7 +263,7 @@ produce a zero-sized SVG `viewBox`.
* **Zoom out** — `MIN_ZOOM = 1/3`: you can see three times the content
frame and no further. Empty space beyond that is not information.
* **Discrete camera motion** (#82) — wheel, `−`/`+`, Fit all, the home arrow
and the free-background double-click/tap in View or kiosk interpolate the
and the plan-surface double-click/tap in View or kiosk interpolate the
existing exact camera target for
160–220 ms with `cubic-bezier(0.2, 0.7, 0.2, 1)`. Zoom is logarithmic and
the world centre is linear; the final frame is the same clamped `viewBox`
@@ -291,10 +291,12 @@ produce a zero-sized SVG `viewBox`.
bails out on them), and a drawing tool that consumes the press —
decor line/rect/ellipse/text — bails out too. Two fingers are always
a pinch, never a pan. On a **kiosk** screen at swipe zoom (`≤ 1`,
more than one space) a *horizontal* drag belongs to the floor swipe:
the gesture is classified once, on the first movement past 8 px, and
keeps that role until the finger lifts (`_panLock`), so the plan
never slides under a swipe and a vertical drag still pans.
more than one space), a floor swipe may start only inside the inner
**48 CSS px** strip of the left/right stage edge that has a previous/next
space. The first movement past the 8 px Manhattan boundary becomes a swipe
only when it points inward and `|dx| > 1.5 × |dy|`; equality, the wrong
direction, a start outside the strip and an edge without a neighbour all
become ordinary pan. The decision remains final until release (`_panLock`).
* **Room fit (#152).** A clean primary click/tap on the browser-painted room
target fits that room's final floor plus its visible boundary wall body into
the middle 80% of the stage. Devices, openings/actions and the HA Area link
@@ -303,15 +305,19 @@ produce a zero-sized SVG `viewBox`.
does not write the per-space zoom preference. Its session-only room intent
is reapplied atomically on a stable stage resize, then cleared by manual
camera input, Fit all/home, mode/space/projection changes, structural
adoption, hidden state or disconnect. In kiosk, room-owned taps never enter
the free-background double-tap sequence.
* **Free-background fit (#449).** Two clean primary clicks/taps within 350 ms
on the stage background invoke the same Fit all command in View and kiosk.
Room, device, vacuum, opening, link and control paths disarm the sequence;
so do pan, pinch, swipe, long press, cancellation, editor/mode/space/projection
changes and lifecycle adoption. The new recognizer adds no timer or render on
the first tap. Mouse, touch and pen sequences are separate; editors do not
expose this shortcut.
adoption, hidden state or disconnect. A pointer tap waits up to 350 ms before
applying room fit: this lets a second clean plan tap select Fit all without
first animating into the room. Keyboard activation remains immediate.
* **Plan-surface fit (#449, #691).** Two clean primary clicks/taps within 350 ms
on stage background, room fill or a non-interactive room label invoke the
same Fit all command in View and kiosk. The two taps may use different plan
surfaces or rooms, but must use the same pointer modality and space. A single
room tap still fits that room once its 350 ms decision window expires; a
single background tap stays passive. Device, vacuum, opening, stair, link and
control paths cancel both the sequence and any pending room fit; so do pan,
pinch, swipe, long press, cancellation, editor/mode/space/projection changes
and lifecycle adoption. Mouse, touch and pen share this contract but do not
pair with one another; editors do not expose the shortcut.
* **The lock is final, at the release too** (audit DEV-1DA1-02). The
release used to ask `swipeTarget()` again from the raw start→end
vector, ignoring the lock — so a *curved* gesture (a short vertical
@@ -321,7 +327,7 @@ produce a zero-sized SVG `viewBox`.
'pan'` now means no floor change, whatever the overall vector ends up
looking like; only a gesture locked as `swipe` may reach
`swipeTarget()`, and it never pans on the way. A motionless tap locks
nothing, so a clean free-background double-tap remains available.
nothing, so a clean plan-surface double-tap remains available.
* **"Home is that way" arrow** — when the content frame is entirely
outside the current view, a small pointer appears at the view edge
in the frame's direction. Clicking it fits the content. Cheap
+7
View File
@@ -2,6 +2,13 @@
## Unreleased
- Touch navigation now keeps ordinary one-finger drags as pan and switches
kiosk spaces only from a 48 px inner edge with an available neighbour. Two
clean taps on background, a room fill or a passive room label fit the whole
plan without first jumping into the room; a single room tap still fits after
the 350 ms decision window, consistently for touch, mouse and pen
([#691](https://github.com/Matysh/houseplan-card/issues/691)).
- Stair outlines, treads, trapezoids and direction arrows now share one 3.6 cm
physical line weight. It scales with the plan instead of staying fixed in
screen pixels, remains uniform when a stair is resized or rotated, and uses
+8
View File
@@ -8,6 +8,14 @@
## Не выпущено
- Навигация на сенсорных экранах теперь сохраняет обычный drag одним пальцем
для панорамирования, а пространство в киоске переключается только жестом из
внутренней 48-пиксельной зоны края, за которым есть соседнее пространство.
Два чистых тапа по фону, заливке комнаты или пассивной подписи вписывают весь
план без промежуточного прыжка в комнату; одиночный тап по комнате по-прежнему
вписывает её после окна выбора 350 мс — одинаково для touch, мыши и пера
([#691](https://github.com/Matysh/houseplan-card/issues/691)).
- Контур, ступени, трапеция и стрелка направления лестницы теперь используют
единую физическую толщину 3,6 см. Она масштабируется вместе с планом, а не
остаётся постоянной в экранных пикселях, не искажается при изменении размера
+6 -2
View File
@@ -39,8 +39,12 @@ On phones, tablets, wall panels and HA Companion apps, the ordinary View must:
- support convenient pan, pinch zoom and space switching;
- treat a linked stair as a floor-navigation target only after a clean tap;
pan, pinch, long press, swipe and cancellation tails must stay inert;
- fit the whole plan after two clean taps on free scene background, while one
tap on a room keeps the immediate room-fit action;
- reserve floor switching for an inward horizontal gesture that starts in the
48 CSS px strip of an edge with a neighbouring space; a drag elsewhere and
an unavailable edge must pan the plan;
- fit the whole plan after two clean taps on background, room fill or a passive
room label. A single room tap fits only after the 350 ms second-tap window,
with no intermediate camera motion; keyboard room activation stays immediate;
- provide a touch path for essential information that desktop exposes through
hover;
- open and close View dialogs without clipping their essential content or
+10 -8
View File
@@ -433,9 +433,9 @@ precise drawing, Resize, keyboard modifiers and double-click properties.
| Scenario | Mouse | Touch View | Touch editors | Keyboard |
|---|---|---|---|---|
| Zoom and pan | Wheel; drag empty space; `−`/`+`; double-click free background to Fit all | Pinch; drag; double-tap free background to Fit all | Available but precision is not guaranteed; no Fit-all double-tap | — |
| Room | A clean click fits the room with 10% margins | One tap fits the room; repeated room taps never become Fit all | — | `Enter`/`Space` on a visible room label |
| Change space | Click a tab | Tap; kiosk swipe at 1:1 | Tap a tab | — |
| Zoom and pan | Wheel; drag empty space; `−`/`+`; double-click background/room to Fit all | Pinch; drag; double-tap background/room to Fit all | Available but precision is not guaranteed; no Fit-all double-tap | — |
| Room | One clean click fits the room after the 350 ms double-click window | One clean tap fits the room after the 350 ms double-tap window | — | Immediate `Enter`/`Space` on a visible room label |
| Change space | Click a tab | Tap; in kiosk at 1:1, swipe inward from the 48 px edge that has a neighbour | Tap a tab | — |
| Device | Click/double-click per mode | Tap; safe actions equal desktop | Drag/properties are best effort | `Esc` closes the top surface |
| Walls drawing or precise drag | Full contract | Not applicable | Best effort; use desktop for Resize and exact nodes | `Shift` changes magnet/angle; `Esc` finishes a Walls chain or cancels the current precise drag |
| Editor history | Undo/Redo controls | Not applicable | Controls may work; no gesture guarantee | `Ctrl/Cmd+Z`, `Ctrl/Cmd+Shift+Z`, `Ctrl+Y` |
@@ -443,7 +443,7 @@ precise drawing, Resize, keyboard modifiers and double-click properties.
Important details:
- wheel, `−`/`+`, **Fit all**, the return arrow and a free-background
- wheel, `−`/`+`, **Fit all**, the return arrow and a plan-surface
double-click/tap in View or kiosk use a short smooth camera transition;
rapid wheel events update one destination instead of building a queue;
- pinch and pan remain directly under the fingers; reduced motion makes every
@@ -454,8 +454,10 @@ Important details:
- each space keeps its own local View viewport. Editor pan/zoom is a working
view and does not replace it;
- a hint appears when objects lie far away from the main plan;
- above 1:1, a horizontal kiosk gesture pans instead of changing space. Any
manual kiosk operation pauses auto-cycle for 60 seconds;
- a kiosk space swipe starts only in the inner 48 CSS px of an edge with a
neighbouring space, points inward and stays strongly horizontal. Elsewhere,
at an unavailable edge and above 1:1 the gesture pans. Any manual kiosk
operation pauses auto-cycle for 60 seconds;
- `Shift` keeps positional grid snapping. For room walls it locks the current
segment to the nearest 45° direction; in Background it creates a square or
circle, allows independent axes for ordinary decor, unlocks furniture
@@ -1714,9 +1716,9 @@ cycle: 30
| Card header | Hidden |
| Editors | Unavailable |
| Height | `100dvh` |
| Swipe | Cycles spaces at 1:1 |
| Swipe | At 1:1, switches to an existing neighbour from the inner 48 px edge |
| Pinch/pan | Zooms and moves the plan |
| Double-tap free background | Fits all content |
| Double-tap background or room | Fits all content without an intermediate room jump |
| Hold empty space for 3 seconds | Opens per-display icon/text sizing |
| `cycle` | Automatically advances; any interaction pauses it for 60 seconds |
+4 -3
View File
@@ -95,13 +95,14 @@ support. User documentation recommends desktop for creation and maintenance.
## View — display and device interaction only
Allowed: pan/zoom (wheel, pinch, buttons, double-click/tap on free background
to Fit all), switching spaces, device tap
Allowed: pan/zoom (wheel, pinch, buttons, double-click/tap on background, room
fill or passive room label to Fit all), switching spaces, device tap
(info / more-info / toggle per settings), long-press → info card, lock-badge tap
→ door/lock info card (openings themselves are inert in View; the card carries an
explicit Unlock/Lock button — the only way to operate a lock from the card;
plan-icon taps never toggle locks),
room-card link icon → HA area, clean room click/tap → room fit, room hover
room-card link icon → HA area, clean single room click/tap → delayed room fit
(350 ms decision window; keyboard activation is immediate), room hover
highlight, hover tooltips (name, clean-floor area, temperature, signal).
Room, device, opening and shared-control hover is a mouse-only transient layer:
+12 -12
View File
@@ -3,7 +3,7 @@
"fixture": "synthetic-only",
"chromium": "151.0.7922.34",
"oxipng": null,
"sourceFingerprint": "fa82f6c7697f8d2ec7ab4fc2c255b9f6e5a16b3636f5823f85d93819a85d93d7",
"sourceFingerprint": "4955edd1dd8d30dad8ae5b4eb12ba23943124646e541ea491287d01550c94870",
"captureScriptSha256": "e2d14d89de793011615662af7c83c433768d4761f6cec7f0b3860c797494299e",
"command": "npm run build && node demo/docs/capture.mjs",
"scenarios": {
@@ -15,7 +15,7 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "fa82f6c7697f8d2ec7ab4fc2c255b9f6e5a16b3636f5823f85d93819a85d93d7",
"sourceSha256": "4955edd1dd8d30dad8ae5b4eb12ba23943124646e541ea491287d01550c94870",
"imageSha256": "de1ee062cfffa375f66ff74509ad9ddbf6aabc5c5440733fe1ff0ea5a40e96c7"
},
"view-touch": {
@@ -26,7 +26,7 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "fa82f6c7697f8d2ec7ab4fc2c255b9f6e5a16b3636f5823f85d93819a85d93d7",
"sourceSha256": "4955edd1dd8d30dad8ae5b4eb12ba23943124646e541ea491287d01550c94870",
"imageSha256": "f9dd5b54f86b814c00bc84149af84485f052b103cdcc9263b5c50bf5d32c69d6"
},
"space-create": {
@@ -37,7 +37,7 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "fa82f6c7697f8d2ec7ab4fc2c255b9f6e5a16b3636f5823f85d93819a85d93d7",
"sourceSha256": "4955edd1dd8d30dad8ae5b4eb12ba23943124646e541ea491287d01550c94870",
"imageSha256": "aab4811bba3afa6cc849cb9d3989d126b8c327b69a2f8f853e55fc077e0e9a1b"
},
"room-contour-close": {
@@ -48,7 +48,7 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "fa82f6c7697f8d2ec7ab4fc2c255b9f6e5a16b3636f5823f85d93819a85d93d7",
"sourceSha256": "4955edd1dd8d30dad8ae5b4eb12ba23943124646e541ea491287d01550c94870",
"imageSha256": "96a7e691ad8167103199a58b48b31ae2fc4f03e9616b65a5096dc8d0b9b51da0"
},
"plan-context-tray": {
@@ -59,7 +59,7 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "fa82f6c7697f8d2ec7ab4fc2c255b9f6e5a16b3636f5823f85d93819a85d93d7",
"sourceSha256": "4955edd1dd8d30dad8ae5b4eb12ba23943124646e541ea491287d01550c94870",
"imageSha256": "ebb89bacfecd16178534e1f02fed8bb7540072a691ec4d4b14407c0b8ec4455e"
},
"device-editor": {
@@ -70,7 +70,7 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "fa82f6c7697f8d2ec7ab4fc2c255b9f6e5a16b3636f5823f85d93819a85d93d7",
"sourceSha256": "4955edd1dd8d30dad8ae5b4eb12ba23943124646e541ea491287d01550c94870",
"imageSha256": "b40f843d7bcab902a6f697cc76735e6d3351a95443fe11ba761f0e39613120bc"
},
"device-display-preview": {
@@ -81,7 +81,7 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "fa82f6c7697f8d2ec7ab4fc2c255b9f6e5a16b3636f5823f85d93819a85d93d7",
"sourceSha256": "4955edd1dd8d30dad8ae5b4eb12ba23943124646e541ea491287d01550c94870",
"imageSha256": "835e9f1b9446072a2caa2421b06a669eac9e5c0e3e6cf94106dd6cc2dd0d8e2b"
},
"background-editor": {
@@ -92,7 +92,7 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "fa82f6c7697f8d2ec7ab4fc2c255b9f6e5a16b3636f5823f85d93819a85d93d7",
"sourceSha256": "4955edd1dd8d30dad8ae5b4eb12ba23943124646e541ea491287d01550c94870",
"imageSha256": "98a8c735165912b49b4254d2f975a61891e5737b0a9c16c627f08d60853fa8a7"
},
"room-card": {
@@ -103,7 +103,7 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "fa82f6c7697f8d2ec7ab4fc2c255b9f6e5a16b3636f5823f85d93819a85d93d7",
"sourceSha256": "4955edd1dd8d30dad8ae5b4eb12ba23943124646e541ea491287d01550c94870",
"imageSha256": "d47988b03e6a078508b27b5612b6eafe8ecc33c891b589ddda1cc7c71f43b7c8"
},
"device-info": {
@@ -114,7 +114,7 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "fa82f6c7697f8d2ec7ab4fc2c255b9f6e5a16b3636f5823f85d93819a85d93d7",
"sourceSha256": "4955edd1dd8d30dad8ae5b4eb12ba23943124646e541ea491287d01550c94870",
"imageSha256": "a10e6044376b087ba6800a31a229e4166a6797820104860f85a4659d89ee8045"
},
"pdf-export": {
@@ -125,7 +125,7 @@
},
"theme": "light",
"language": "en",
"sourceSha256": "fa82f6c7697f8d2ec7ab4fc2c255b9f6e5a16b3636f5823f85d93819a85d93d7",
"sourceSha256": "4955edd1dd8d30dad8ae5b4eb12ba23943124646e541ea491287d01550c94870",
"imageSha256": "788a1ff32d6ceb51c9fb47ea37922ab15aeddf3fdcf3686bfcb0709634c41f0e"
}
},