Files
houseplan-card/docs/RESIZE.md
T
Matysh 3e23ff58c3
Validate / hacs (push) Failing after 10s
Validate / hassfest (push) Failing after 8s
Validate / frontend (push) Successful in 1m22s
Validate / backend (push) Failing after 4m37s
Validate / smoke (push) Failing after 6m17s
ui(resize): wall handles — half-size wall-with-arrows glyph, grab cursor
Owner request: in the resize tool make the wall-drag handles twice
smaller, replace the circle with a 'wall + two opposite arrows' icon,
drag cursor.

- visible glyph: wall segment with two arrows perpendicular to the
  edge (the drag directions), rotated per wall orientation; accent ink
  over a --hp-bg halo, readable on any plan
- HIT area unchanged: an invisible circle of the original finger-sized
  radius keeps touch targets and the HP-1550-04 hit priority over
  openings (04_handle_wins_hit_test still passes)
- cursor: grab on hover, grabbing while dragging (:active)
- scale-frame corner handles untouched (classic filled circles,
  nwse-resize)
- docs/RESIZE.md: handle appearance updated
2026-08-02 11:50:49 +03:00

169 lines
8.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Room resize — the spec (source of truth)
Status: approved by the owner 2026-08-01. Dev-branch feature, **no
release**. Scope decisions final: a dedicated tool mode, wall-drag with
shared walls always moving together, a corner-scale frame for the
selected room, live numbers (wall lengths + room areas), grid snap,
Esc-cancel, one drag = one undo step.
## Principle
Until now room geometry could only be changed by split/merge or by
redrawing the outline — there was no vertex or wall dragging at all
(the `.rlhandle` corners belong to the room LABEL card, not to the
room). Resize adds exactly two mechanisms, both living ONLY inside a
dedicated Plan-editor tool «Изменение размера комнат» (`_tool ===
'resize'`). In every other tool the plan looks and behaves exactly as
before — no handles, no new hit areas.
## Mechanism A — wall drag
- Every visible room shows a small handle at the midpoint of every
wall (handles for all rooms at once — owner's UX pick). The visible
glyph is a compact icon — a wall segment with two arrows pointing
perpendicular to it (the directions the wall drags), rotated to the
wall's orientation; accent ink over a `--hp-bg` halo. It is half the
size of the old circle, but the HIT area is an invisible circle of
the original finger-sized radius (derived from `view.w` like
`.vacfithandle`), so touch targets did not shrink. Cursor: `grab`,
`grabbing` while dragging.
- Dragging a handle moves the wall along its outward normal; **both
ends of the edge translate together** (the wall stays parallel to
itself; adjacent walls stretch/shrink). Works for any polygon
(L-shaped included) and for legacy `x/y/w/h` rectangles — those are
converted through `roomPoly` and are **saved back as `poly`**.
- The moved wall position snaps to the drawing grid (`snapToGrid`,
same pitch as the draw tool).
- **Handles own the hit test** (HP-1550-04): inside the resize tool
openings are not editable — their transparent hit area is inert
(`pointer-events: none`) and the resize layer renders above the
openings, so a door sitting exactly at the midpoint of a wall can
never shadow that wall's handle. A drag of such a wall carries the
door along through the normal anchor pipeline; clicking over a door
falls through to room picking. Every other Plan tool keeps the
openings interactive exactly as before.
## Shared walls — ALWAYS together
If a stretch of the dragged wall coincides with a neighbour's boundary
(collinear overlap with an epsilon, the `sharedBoundary` notion), the
coinciding stretches of the neighbour move synchronously: your room
grows — the neighbour shrinks. Gaps and overlaps cannot appear by
construction.
Partial contact (T-junctions): only the coinciding stretch of the
neighbour moves. Where the stretch ends inside a neighbour wall, new
vertices are inserted into the neighbour outline, which may legally
become L-shaped. All of this is shown as a live preview during the
drag. On commit collinear leftovers are simplified away
(`simplifyPoly`), so geometry stays clean.
## Stops (the wall stops dead)
1. **Minimum size** — neither the own room nor a shrinking neighbour
may get thinner than ~30 cm (`MIN_ROOM_CM`, expressed in canvas
units through `cell_cm`). The measure is orientation-independent
(HP-1550-02): for a wall drag it is the smallest perpendicular
distance from the moved stretch to any part of the room boundary
inside the band the stretch sweeps along its normal — vertices and
crossing walls count whether parallel or not (a triangle's apex, a
slanted obstacle), while collinear remainders and the step corners
a T-junction inserts at the very ends of the stretch do not. For
the scale frame it is the TRUE minimum width of the outline
(rotating calipers over the convex hull) scaled by `k` — the
axis-aligned bbox is never consulted, so rotation cannot hide the
short side. Rooms that are ALREADY thinner keep their clearance
(the drag may improve it, never worsen it).
2. **Self-intersection** — a wall never passes through the opposite
side; the outline must stay a simple polygon with its orientation
and a positive area.
3. **Foreign rooms** — a growing wall stops when it would overlap a
room that is not a shared-wall neighbour (`roomsOverlap`; touching
walls are legal, crossing is not).
4. **Island rooms** — islands inside the room (`islandsOf`) must stay
fully inside; a wall shrinking onto an island stops.
5. **Openings are anchors** — a door/window ON the moving stretch
travels with the wall (its `openings[].x/y` centre is shifted, the
angle is unchanged). A wall that carries openings cannot get too
short for them: every opening previously sitting on a wall of an
affected room must still fit fully on some wall afterwards — for
the own room AND for the neighbour.
## Mechanism B — the scale frame
- In the resize tool a click inside a room SELECTS it: a dashed
bounding frame with 4 corner handles appears.
- Dragging a corner scales ALL vertices proportionally (uniform
similarity) about the opposite bbox corner — the same maths family
as the vacuum fit panel (`reanchorFit`), only without rotation.
- The same stops apply (minimum size, foreign overlap, islands,
openings; self-intersection is impossible under a similarity).
- **The one exception to «shared walls always together»:** a scale
breaks collinear coincidence (walls move apart at an angle-preserving
ratio, not along a normal), so neighbours are NOT dragged along.
Growing into a neighbour simply stops the scale (the neighbour is a
wall to hit); shrinking away from a neighbour legally opens a gap.
- Openings exclusive to the scaled room follow the transform
(position scales, physical length does not); openings on a wall
shared with an unchanged neighbour stay with the neighbour's wall.
## Live numbers
While a handle is being dragged:
- length badges (`.measurelabel` style, `segmentCm`/`formatLength`,
metric or imperial per the HA unit system) on the dragged wall and
its two adjacent walls;
- the room area in m² (`polygonArea` × scale²) at the room centre,
live; when a shared wall is dragged — the areas of BOTH rooms
(owner picked «стены + площадь»);
- Esc cancels the current drag and puts the original geometry back;
- releasing the handle (pointerup) commits: one write through the
standard debounced `_saveConfig` path;
- `pointercancel` / `lostpointercapture` (the system interrupted the
stream: app switch, palm rejection) takes the CANCEL path, never the
commit path — snapshot geometry back, no undo step, no write
(HP-1550-03).
## Preview vs commit (HP-1550-01)
The live drag preview never touches the shared `_serverCfg`: it lives
in a separate overlay (`_rszPreview`) that `_curSpaceCfg`/`_renderCfg`
substitute into every render. Config writes are serialized and read
`_serverCfg` at the moment they run (HP-1454-03), so a debounced write
still queued from a previous edit can fire mid-drag — with the overlay
it carries only committed geometry. The overlay moves into the real
config exactly once, on pointerup; a cancel (Esc, pointercancel) just
drops the overlay, leaving nothing to restore and nothing to write.
## Undo
One operation (handle release that changed something) = one undo step.
The plan editor has no committed-operation undo stack (Ctrl+Z/Esc only
walk back draw/split points), so the resize tool keeps its own stack
of pre-drag snapshots (rooms + openings of the space, capped at 30)
and Ctrl+Z/⌘Z pops it while the tool is active.
## Out of scope / invariants
- Device positions are not touched; the room settings button (pole of
inaccessibility) recomputes itself from the new outline.
- Saving goes through the standard config path (`houseplan/config/set`
with `expected_rev`); backend validation already covers polygons
(`_GEOM` ±4, `MAX_POLY_POINTS` 500) — inserted neighbour vertices are
just polygon points, openings keep their schema, nothing new to
validate server-side.
- Touch: handles are finger-sized, use pointer capture and swallow
`pointerdown`, so the stage pan/pinch never fights a handle drag.
- The label-card corners (`.rlhandle`, `_rlResizeDown`) are untouched.
## Geometry home
All pure geometry lives in `src/resize.ts` (edge normals, edge move,
shared-span search and vertex insertion, all stops — including the
orientation-independent `minSpanClearance` band measure and the
`minPolyWidth` calipers width — the scale clamp, area formatting)
under node:test units in `test/resize.test.mjs`;
`src/houseplan-card.ts` only wires pointers, the preview overlay,
badges and undo.