v1.59.0-beta.10: unify device visuals and wall refinements
Validate / hacs (push) Failing after 6s
Validate / hassfest (push) Failing after 6s
Validate / frontend (push) Successful in 3m19s
Validate / smoke (push) Failing after 1m12s
Validate / backend (push) Failing after 7m45s

This commit is contained in:
Matysh
2026-08-05 23:38:01 +03:00
parent 4868cc0786
commit d2bec266ed
50 changed files with 2749 additions and 1462 deletions
+17 -7
View File
@@ -87,11 +87,16 @@ Built from the registries (`_buildDevices`), rules carried over 1-to-1 from the
## Device markers (v1.6.0+)
Per-marker appearance (v1.22.0): `display: badge|ripple|icon_ripple` (+`ripple_color`,
`ripple_size`) draws presence-style pulsing rings gated by the pure `isActiveState` —
deliberately independent of the card-wide `live_states` toggle, with `unavailable` counting as
idle. `size` (icon multiplier via the `--dev-size` CSS var — value badges scale along) and
`angle` rotate/scale a single icon. Room drawing shows a live **ruler** (`segmentCm` +
Per-marker appearance: `display: badge|icon_ripple|value`. All three use one semantic
resolver (`src/device-visual.ts`) for availability, steady status and activity. `badge`
shows the icon/morph and status plate; `icon_ripple` additionally shows a finite event,
static presence, mechanical transition or actual-work ring; `value` replaces the icon
with the HA-formatted numeric value. A critical alarm is red in every presentation.
Legacy `display: ripple` is read as `icon_ripple` and rewritten on the next config save;
the backend accepts it only for compatibility. `ripple_color` and `ripple_size` remain the
stored names for the ordinary activity effect. `size` (icon multiplier via the
`--dev-size` CSS var — value badges scale along) and `angle` rotate/scale a single icon.
Room drawing shows a live **ruler** (`segmentCm` +
`formatLength`, metres or feet+inches by `hass.config.unit_system`); the scale is per-space
`cell_cm` (default 5 cm per grid cell).
@@ -386,12 +391,17 @@ hash falls back to the default.
a per-light `clipPath` = zone polygons + doorway sectors, each contour a
SEPARATE clipPath child (children union; subpaths of one nonzero path
cancel on opposite windings — field bug v1.36.3). Radius: global
`settings.glow_radius_cm` + per-marker `glow_radius_cm`.
`settings.glow_radius_cm` + per-marker `glow_radius_cm`. On a thick wall the
doorway sector is the angular intersection of its near- and far-face clear
spans, so the jamb returns clip the spill as a real opening tunnel.
- **Open boundaries** (v1.37, revised 2026-08): `space.open_spans` hold
geometric virtual stretches; `room.open_to` remains the light-zone index
derived from spans (legacy `open_to`-only configs expand to full
`sharedBoundary` on read). Shared stretches drawn as a TRUE dash; outlines
trimmed (`outlineWithout`/`cutSegments`). Geometry mutations clip one stored
trimmed (`outlineWithout`/`cutSegments`). In View the dash group is painted
before thick wall bodies, letting real jambs mask its centreline ends; in all
editors it is painted after the bodies so saved spans and previews remain
fully visible. Geometry mutations clip one stored
span to **every** surviving shared segment. Adjacent pieces owned by different
room pairs stay separate: their midpoints are the source of the corresponding
`open_to` links after Split (`AUD-159B7-01`).
+38
View File
@@ -2,6 +2,44 @@
## Unreleased (dev)
## v1.59.0-beta.10 — 2026-08-05
Tenth pre-release of the 1.59 line: one coherent device-state language and a
focused set of wall, doorway-light and background-label refinements.
- **One device-state language.** Marker plates and effects now come from one
semantic resolver: yellow means actual work, orange means open/unlocked,
unavailable is faded, and alarms are always red. The display list is reduced
to Icon, Icon + activity and Value; the removed Ripple-only value migrates to
Icon + activity on the next save. Activity distinguishes short events,
persistent presence, mechanical travel and actual running, with
reduced-motion fallbacks and no false event on first load or reconnect.
Short effects also reset when a marker's effective source changes, while a
cover's real opening/closing state takes precedence over the tap fallback.
- **Clearer wall-drawing toolbar.** The Plan editor's former “Add” tool is now
labelled “Walls”, and its new-wall thickness field sits immediately to the
right of that button instead of after the rest of the toolbar.
- **View-mode virtual walls sit behind thick wall bodies.** Their stored
geometry still reaches the physical centreline, but in View the hatch masks
the dash ends inside adjoining thick walls. All three editors keep saved
dashes and live previews above the wall body so the complete span remains
visible while editing.
- **Door light respects thick-wall reveals.** Light now crosses a doorway only
through the clear width of its physical wall tunnel. The near and far inner
face spans jointly clip the spill, so the two jamb returns cast the expected
cut-offs for an off-centre source; zero-thickness walls retain the previous
doorway sector.
- **Inline HA variables in decor text.** A label can now mix ordinary copy and
multiple `{entity}` / `{entity:attribute}` references. Choosing a state or
attribute inserts its token at the textarea caret. The separate unit field,
one-slot hint, and preview are removed; old linked labels remain readable and
migrate to inline tokens when edited.
- **Canonical wall fragments.** Touching or overlapping virtual stretches on
the same boundary and room pair now collapse into one `open_span`; Split
pieces belonging to different room pairs remain separate. When removing the
last virtual stretches leaves an original wall solid and uniformly thick,
its atomic thickness entries collapse back to one whole-wall key.
## v1.59.0-beta.9 — 2026-08-05
Ninth pre-release of the 1.59 line: mixed virtual/thick resize integrity and
+42
View File
@@ -8,6 +8,48 @@
## Не выпущено (dev)
## v1.59.0-beta.10 — 2026-08-05
Десятый pre-release линейки 1.59: единая система состояния устройств и набор
точечных улучшений стен, света через дверные проёмы и надписей подложки.
- **Единый язык состояния устройств.** Подложка и эффекты маркера теперь
вычисляются одним семантическим resolver: жёлтый означает фактическую работу,
оранжевый — открыто/разблокировано, недоступное устройство приглушается, а
тревога всегда остаётся красной. В списке отображения остались «Значок»,
«Значок + активность» и «Значение вместо иконки»; удалённый режим «Только
пульсация» при следующем сохранении переходит в «Значок + активность».
Активность различает короткое событие, постоянное присутствие, механическое
движение и фактическую работу, не создавая ложного события при загрузке или
восстановлении связи. Короткие эффекты также сбрасываются при смене
фактического источника маркера, а реальное открытие/закрытие шторы имеет
приоритет над временной реакцией на нажатие.
- **Более понятная панель рисования стен.** Бывшая кнопка «Добавить» в
редакторе плана теперь называется «Стены», а поле толщины новых стен стоит
сразу справа от неё, а не после остальных инструментов панели.
- **В режиме просмотра виртуальные стены находятся под телом толстой стены.**
Их сохранённая геометрия по-прежнему доходит до физической осевой линии, но в
View штриховка скрывает концы пунктира внутри примыкающих толстых стен. Во
всех трёх редакторах сохранённый пунктир и живой preview остаются поверх тела
стены, чтобы весь отрезок был виден во время правки.
- **Свет из двери учитывает откосы толстой стены.** Теперь свет проходит через
дверной проём только по свободной ширине его физического тоннеля. Ближний и
дальний внутренние срезы совместно ограничивают пучок, поэтому оба стеновых
откоса корректно отсекают лучи смещённого источника; для стены без толщины
сохраняется прежний сектор проёма.
- **Inline-переменные HA в тексте подложки.** Одна надпись теперь может
сочетать обычный текст и несколько ссылок `{entity}` / `{entity:attribute}`.
Выбор состояния или атрибута вставляет токен в позицию курсора textarea.
Отдельное поле единицы, подсказка про один слот и предпросмотр удалены;
старые связанные надписи продолжают читаться и при редактировании переходят
на inline-токены.
- **Канонические фрагменты стен.** Соседние или перекрывающиеся виртуальные
отрезки на одной границе одной пары комнат теперь объединяются в один
`open_span`; части Split, принадлежащие разным парам комнат, остаются
раздельными. Если после удаления последних виртуальных участков исходная
стена снова целиком реальная и имеет одну толщину, её атомарные ключи
схлопываются обратно в один ключ цельной стены.
## v1.59.0-beta.9 — 2026-08-05
Девятый pre-release линейки 1.59: целостный resize смешанных
+8 -8
View File
@@ -71,16 +71,16 @@ the old behaviour until an editing client materialises it.
## What a marker SHOWS
A marker's live indication — the yellow «on» plate, the «open» frame (never
on a cover, see below), the breathing `covermove` ring, the state-morphed
icon, the ripple — speaks for ONE entity of the device, resolved in this
order (`_stateClass` / `_actEntity`):
A marker's live indication — status plate, state-morphed icon and semantic
activity — is derived by one resolver from one effective source set. Its
source precedence is (`_visualSamples` / `_actEntity`):
1. the device's **cover**, when the marker's tap action is explicitly
«Открыть/закрыть» (`tap_action: 'cover'` — `coverEntityOf`, the same helper
and the same entity the tap drives). It wins over EVERYTHING below;
2. the marker's bound **controls**, if it has any (a stateless remote or a
virtual wall switch mirrors what it drives, not itself);
virtual wall switch aggregates what it drives, not itself; any working
target drives both the yellow plate and running activity);
3. a **lit light** among its entities (owner's principle 2026-07-29: the glow
spot and the badge may never disagree);
4. otherwise the **primary** entity (`primaryEntity`).
@@ -125,14 +125,14 @@ dialog away, and the honest reading of what the marker was told it is.
«У штор не должно быть жёлтой подложки НИКОГДА, индикация открыто/закрыто за
счёт морфинга иконки.» For the `cover` domain — and for the cover an
«Открыть/закрыть» marker indicates, rule 3 above — `_stateClass` returns no
plate class in any state:
«Открыть/закрыть» marker indicates, rule 1 above — the visual resolver returns
no working/open plate in any state:
| cover state | plate | ring | icon |
|---|---|---|---|
| `closed` | neutral | — | closed glyph |
| `open`, ajar (`open` + position) | neutral | — | open glyph |
| `opening`, `closing` | neutral | `.covermove` breathes | open glyph |
| `opening`, `closing` | neutral | `.activity-transition` breathes in Icon + activity | open glyph |
| `unknown` / no state | neutral | — | base icon, no morph |
| `unavailable` | neutral, faded (`.unavail`) | — | base icon |
+45 -64
View File
@@ -1,17 +1,18 @@
# The text block — a decor label that can show an entity's state
Status: **implemented (dev, unreleased).** Code: `src/logic.ts`
(`liveText`, `liveTextValue`, `decorTextScale`, `decorTextLines` — pure,
unit-tested), `src/houseplan-card.ts` (`_renderDecorLayer`,
(`liveText`, `liveTextReference`, `liveTextToken`, `liveTextValue`,
`decorTextScale`, `decorTextLines` — pure, unit-tested),
`src/houseplan-card.ts` (`_renderDecorLayer`,
`_renderTextFrame`, `_renderDecorTextDialog`, the `_dt*` gestures),
`custom_components/houseplan/validation.py` (`DECOR_SCHEMA`, text branch).
Smokes: `demo/smoke_live_text.mjs`, `demo/smoke_decor_text.mjs`;
`demo/smoke_decor.mjs` keeps the surrounding decor contract.
The shape: `{kind:'text', x, y, text, color, scale?, angle?, entity?, attr?,
unit?}` — plus the legacy `size?` that older plans still carry. Everything
after `color` is optional, so **every existing plan validates and renders
unchanged and no migration runs**.
The shape: `{kind:'text', x, y, text, color, scale?, angle?}` — plus legacy
`size?`, `entity?`, `attr?`, and `unit?` fields that older plans may still
carry. New live references are stored only inside `text`; existing linked
labels render unchanged and migrate to inline references when edited.
## 1. Why a live label
@@ -27,28 +28,26 @@ happens to have a live number in it.
The competing card (ha-floorplan) covers this with `text_set` and it is one of
its most used features; our decor text was one field away from it.
## 2. The live value
## 2. Inline HA variables
A decor text shape has three optional fields:
`text` is both the visible copy and the complete template. It may contain any
number of HA references mixed with ordinary text and line breaks:
- `entity` — an entity id whose state is substituted; absent = today's plain
static label, byte-for-byte unchanged;
- `attr` — an attribute name to read instead of `state` (battery level,
current temperature of a climate, position of a cover); absent = the state;
- `unit` — a suffix; absent = the entity's own `unit_of_measurement`, so
`sensor.tank` needs no configuration to read *68 %*.
- `{sensor.water_tank}` — the entity state;
- `{climate.hall:current_temperature}` — one attribute;
- `Бак {sensor.water_tank}, зал {climate.hall:current_temperature}` — several
independent values in one label.
The `text` field keeps its present meaning and becomes the **template**: the
placeholder `{}` is where the value lands. `Бак {}` → *Бак 68 %*. A template
without a placeholder gets the value appended after a space, so a user who
only picks an entity and types nothing sensible still sees something useful.
**Only the first `{}` is replaced** — one label, one value.
The editor writes the colon form because the boundary between entity and
attribute is unambiguous. Hand-written `{climate.hall.current_temperature}` is
accepted too: the first two dot-separated parts form the entity id and the
rest is the attribute. Invalid brace contents stay literal, while a valid but
missing entity or attribute renders as a dash.
`{}` was chosen over `{{ }}` deliberately: this is a substitution, not a
template language. There is no expression, no condition, no arithmetic — the
moment we accept `{{ states('x') | round(1) }}` we have signed up for the
class of support load that fills the competitor's thread (138 posts of "my
CSS/template does not work"). One value, one place, no syntax to get wrong.
This remains substitution, not a template language: no expressions,
conditions, arithmetic, Jinja, or nested braces. The 200-character limit is
the limit of the saved template; each resolved value is still clipped to 60
characters.
### 2.1. Rendering rules
@@ -77,17 +76,10 @@ CSS/template does not work"). One value, one place, no syntax to get wrong.
formatter falls back to the raw state, byte-for-byte the pre-2026-08-05
behaviour. Imperial/metric is not our business either — the value and the
unit come from HA (docs/STYLING-HOOKS.md §6).
- **The unit is inherited only for the STATE.** With an `attr` the entity's
`unit_of_measurement` is not applied: it describes the state, and a
`battery_level` read off a °C sensor must not come out as «73 °C». An
explicit `unit` always wins; an empty one inherits.
- **The unit appears exactly once.** HA's formatter normally appends the
entity's unit itself, so the inherited one is not added a second time — and
where it does not append it, ours still is, so the unit never silently
disappears either. The rule: strip the entity's own `unit_of_measurement`
if it is already the tail (exact trailing match, nothing else in the string
is touched), then append the unit actually wanted — the user's explicit one,
or the entity's own.
- **Units belong to Home Assistant.** State variables use HA's formatted state,
including its unit. Attribute variables use HA's attribute formatter and do
not inherit the entity state's unit. There is no separate unit override in
the new editor; a literal suffix can be typed immediately after the token.
- The value is clipped to `LIVE_TEXT_VALUE_MAX` (60) characters: a caption is
a caption, and an attribute that turns out to be a 4 KB string must not
become the plan's wallpaper.
@@ -119,7 +111,7 @@ size is not one of three opinions; it is whatever fits the place it is put in.
newline is stored and rendered as a newline (one `<tspan>` per line, line
height 1.2 em). The label **never wraps by itself** — a caption that reflows
on every state change is a caption that jumps around the plan. A
300-character line stays one line.
200-character line stays one line.
- **Multi-line blocks are centred**, horizontally (the decor layer's
`text-anchor: middle`, which single-line labels already used) and
vertically: the anchor `x/y` sits in the middle of the block, so adding a
@@ -159,34 +151,24 @@ click opens its editor, and the corner/rotate handles appear.
## 5. The dialog
Under the text field:
- a **textarea** (line breaks are content now), saved with the button or
Ctrl/⌘+Enter — plain Enter is a new line;
- a hint that mentions `{}` **only when an entity is chosen** — an unlinked
label must not be burdened with syntax it does not need;
- an **entity picker** with a datalist of all entities — the same control
style the vacuum source and the weather field use;
- an **attribute field**, shown only once an entity is chosen, with a datalist
of that entity's actual attribute names — the user should not have to know
that a climate keeps `current_temperature`;
- a **unit field** whose placeholder is the entity's own unit, so leaving it
empty is the obvious right answer (the placeholder disappears once an
attribute is chosen, because an attribute does not inherit it);
- a **live preview** of the resulting label, rendered through the same
`liveText` the plan uses: one substitution, one truth.
Clearing the entity clears the attribute and the unit with it — a save never
leaves orphan fields in the config.
- The textarea is the sole source of the label. It accepts ordinary copy,
line breaks, and manually typed references in any order; Ctrl/⌘+Enter saves.
- «Insert HA variable» contains an entity picker. After an entity is selected,
the second control offers its state and actual attribute names.
- Choosing the state or an attribute immediately inserts the complete token at
the textarea's current selection/caret, then returns focus after the token.
The user can continue typing or insert another variable, including one from
another entity, until the 200-character field limit is reached.
- There is no unit field, single-slot hint, or separate preview. The label on
the plan is already the live preview and uses the same text template.
## 6. Backend
`DECOR_SCHEMA`, text branch: `entity` optional, `None` or an entity id
(`^[a-z0-9_]+\.[a-z0-9_]+$`, ≤ 255); `attr` optional, `None` or a flat name
≤ 64; `unit` optional, `None` or ≤ 16 characters; `scale` optional, finite,
`0.15…20`; `angle` optional, finite, `-360…360`; `text` ≤ 200 characters,
newlines included. Everything optional, so every existing plan validates
unchanged. Tests: `tests_backend/test_validation.py`
`DECOR_SCHEMA`, text branch: `text` ≤ 200 characters, newlines and inline
references included; `scale` optional, finite, `0.15…20`; `angle` optional,
finite, `-360…360`. Legacy `entity`/`attr`/`unit` remain accepted and bounded
so old saved plans continue to validate and render. The frontend writes none
of them after the label has been edited. Tests: `tests_backend/test_validation.py`
(`test_decor_text_live_fields`, `test_decor_text_block_scale_and_angle`).
## 7. What this is not
@@ -194,8 +176,7 @@ unchanged. Tests: `tests_backend/test_validation.py`
- **Not a second device marker.** No tap action, no icon, no state class, no
participation in room aggregation (LQI, climate averages) — it is a caption,
not a device. A user who wants an interactive thing puts a marker.
- **Not a template engine** (see §2).
- **Not a multi-entity widget.** One label, one value. Two values are two
labels; that stays honest and costs the user one drag.
- **Not a template engine** (see §2). Multiple substitutions do not introduce
expressions, conditions, or formatting rules.
- **Not an auto-layout.** No wrapping, no shrink-to-fit: the size is set with
the corners and the lines with the Enter key.
+8 -3
View File
@@ -15,11 +15,11 @@
| Item | State |
|---|---|
| Version | **v1.59.0-beta.9** everywhere (manifest, const.py, package.json, CARD_VERSION) — **pre-release**, tag `v1.59.0-beta.9` on **`dev`**, GitHub Release with `prerelease=true`; `main` is not touched and nothing is copied to the home instance by hand — HACS delivers it on the beta channel. On top of beta.8: resize keeps partial `open_spans` and the atomic thickness keys of their solid remainders together in live preview, commit and Undo; real arms at virtual T-junctions receive the missing mitre, and the virtual dash/preview renders above thick real walls. Previous pre-release: v1.59.0-beta.8; previous stable: v1.58.0 |
| Version | **v1.59.0-beta.10** everywhere (manifest, const.py, package.json, CARD_VERSION) — **pre-release**, tag `v1.59.0-beta.10` on **`dev`**, GitHub Release with `prerelease=true`; `main` is not touched and nothing is copied to the home instance by hand — HACS delivers it on the beta channel. On top of beta.9: unified device status/activity visuals; inline HA variables in decor text; doorway-light clipping by thick-wall reveals; view-only virtual-wall masking; a clearer wall toolbar; and canonical merging/compaction of wall fragments. Previous pre-release: v1.59.0-beta.9; previous stable: v1.58.0 |
| Workflow | Since 2026-07-22: minor changes go to branch **`dev`** (build + smokes → deploy home → commit → push, NO release); releases are batched on the owner's command. **Since 2026-08-04 there is also a pre-release track:** bump to `X.Y.Z-beta.N`, tag the `dev` commit, publish a GitHub Release with `prerelease=true` — `main` is not touched and nothing is copied to the home instance by hand; HACS delivers it on the beta channel |
| Workflow | Owner's rule since 2026-08-05: ordinary fixes/features are made **locally, without tests and without commits**. Tests/build/smokes run only when the owner asks for a pre-release; then bump to `X.Y.Z-beta.N`, commit/tag the tested `dev` state and publish a GitHub Release with `prerelease=true`. `main` is not touched and nothing is copied to the home instance by hand; HACS delivers it on the beta channel |
| GitHub | https://github.com/Matysh/houseplan-card — `main` carries stable releases; pre-release tags may point directly at `dev`. Work lands on `dev` and is merged into `main` for a stable release, so `dev` is normally equal to or ahead of `main`, never behind. Push via SSH key `ha_jb` (remote git@github.com:…); API releases via the fine-grained PAT in `~/.git-credentials` (Contents R/W, issued 2026-07-23) |
| CI | beta.8 repairs the stale general-settings smoke that left beta.7 Validate red (`111/112`) and adds exact-SHA release gating: `release.yml` withholds the asset until every matching Validate run finishes green. The full local gate is green; GitHub Validate and the release asset are verified after the tag is pushed |
| CI | beta.10 passes the full local frontend, pure-backend and browser-smoke gate. Exact-SHA release gating remains active: `release.yml` withholds the asset until every matching Validate run finishes green; GitHub Validate and the release asset are verified after the tag is pushed |
| HACS | Custom repository works. **Inclusion PR: hacs/default#9004** — open, valid, labeled, mergeable clean, never drafted. Queue: 1212 open, 835 older than ours. Merge rate COLLAPSED: 75 in July but almost all in the first decade, 0 in the last week (checked 2026-07-29) — maintainers process in rare bursts; ETA unknowable, months at best. Nothing actionable on our side |
| Home instance | ha.jbstudio.pro (SSH port **22222**, key `ha_jb`; HA config root is `/mnt/data/supervisor/homeassistant` — `/config` does NOT exist in this SSH environment), last direct copy was **v1.57.0**; from v1.58.0 on it updates itself through HACS by tag (no scp) |
| Localization | UI en/ru (src/i18n/*.json), everything user-visible localized incl. kiosk popover |
@@ -69,6 +69,11 @@
- **Yellow = working right now** (v1.51.0): climate by hvac_action, service
switches can no longer become primary, glow pool and icon share one
condition. Editor gestures on touch (pinch/pan) landed the same release.
- **Unified device status/activity** (v1.59.0-beta.10, 2026-08-05): three display
modes (Icon / Icon + activity / Value), one semantic resolver for yellow
actual work, orange open/unlocked, unavailable and always-red alarms;
activity distinguishes a short event, presence, mechanical travel and
running. Legacy Ripple-only migrates to Icon + activity on the next save.
## Recent milestones (details in CHANGELOG.md)
+14 -10
View File
@@ -136,10 +136,10 @@
- Drag иконок, снап к сетке, позиция per-space — Стенд: редактор устройств: перетащить иконку, F5 — позиция жива; у «Двора» своя раскладка.
- ↺ сброс автолейаута — Стенд: не проверяется: кнопка Reset убрана в v1.33.2 (пункт в TESTING.md исторический).
- Бейдж температуры, LQI с цветом — Стенд: под «Zigbee»-термометрами цифра LQI (цвет от красного к зелёному: Кабинет красный, Гостиная зелёный), температура на бейдже.
- Живые состояния: свет жёлтый, открытая штора оранжевая, unavailable блёклый — Стенд: пощёлкать свет; открыть штору («Открыть шторы» или карточка); unavailable в демо нет — эмулировать нечем, но у робота после паузы всё живое.
- Живые состояния: фактическая работа жёлтая, дверь/разблокированный замок/клапан оранжевые, штора нейтральная и меняет иконку, unavailable блёклый — Стенд: пощёлкать свет; открыть штору («Открыть шторы» или карточка); unavailable в демо нет — smoke.
- Морфинг иконок по состоянию — Стенд: дверь/замок: открыть lock.front_door из карточки двери — иконка меняется; гаражные ворота во «Дворе».
- «Значение вместо иконки» — Стенд: у термометра в диалоге выбрать display «значение» — на плане цифра.
- RGB только в glow/ripple — Стенд: включить RGBWW-лампу в Гостиной цветом: бейдж жёлтый стандартный, цвет только в пятне glow.
- RGB только в glow/activity — Стенд: включить RGBWW-лампу в Гостиной цветом: бейдж жёлтый стандартный, цвет только в пятне glow и activity-эффекте.
- Красная пульсация тревоги — Стенд: включить `input_boolean.demo_leak` (Настройки → Устройства и службы → Помощники) — датчик на Кухне пульсирует красным; дым в Спальне сам вспыхивает каждые 10 минут на 30 с.
- Стоимость рендера (geometry once) — Стенд: не проверяется руками — smoke_render_perf.
- Тап vs drag открывания — Стенд: редактор плана, тап по двери = свойства, drag на место = ничего не записано.
@@ -155,7 +155,7 @@
- Tap запускает автоматизацию — Стенд: тап по «Открыть шторы» (Гостиная) → диалог подтверждения → штора едет; в диалоге маркера видно «Run» + поиск по automation/script/scene (есть скрипт «Открыть шторы», автоматизация «Вечерний сценарий», сцены «Кино», «Вечер в гостиной»); Esc = не выполнять.
- Бейджи light-source в glow — Стенд: заливка glow (по умолчанию): у горящей лампы бейдж стандартный (индикатор — пятно), у розетки-«источника света» жёлтый.
- Множитель размера иконки растит глиф — Стенд: size 3 у любого маркера.
- Auto-grid parity, ripple ghost, hidden LQI parity, ghost без цифр — Стенд: скрыть «Zigbee»-термометр: LQI комнаты не меняется, «Схема» совпадает с планом; «Show hidden» в редакторе устройств показывает синий пунктирный призрак без значений.
- Auto-grid parity, activity ghost, hidden LQI parity, ghost без цифр — Стенд: скрыть «Zigbee»-термометр: LQI комнаты не меняется, «Схема» совпадает с планом; «Show hidden» в редакторе устройств показывает синий пунктирный призрак без значений и эффектов.
- Флаг «скрыть с плана» — Стенд: галка в любом диалоге устройства; скрытый исчезает из всех режимов и счётчика, но LQI комнаты держит.
- «Жёлтый = работает» — Стенд: «Тёплый пол» в Кабинете: hvac_action=heating → жёлтая подложка. (Выключить нагрев можно, поставив target ниже 20° в карточке — бейдж гаснет.)
- Жесты редактора на touch — Стенд: с телефона в редакторе плана: pinch = zoom, палец = pan, отпускание не ставит точку.
@@ -316,15 +316,19 @@
- show_button: false — Стенд: правкой YAML вида.
- #space= диплинк — Стенд: открыть `…/plan#space=yard`.
## Presence ripples / per-device иконка
## Состояние и активность устройства
- «Только пульсация»: бейдж исчезает, кольца при on — Стенд: у датчика движения «Movement Backyard» (Кухня) поставить display «пульсация» — он периодически on/off сам.
- «Иконка + пульсация» — Стенд: там же.
- Цвет и размер ×2..×8 — Стенд: там же.
- unavailable останавливает пульс — Стенд: не проверяется (нет unavailable-сущностей в демо) — smoke.
- В списке отображения ровно три режима: «Значок», «Значок + активность», «Значение вместо иконки»; «Только пульсация» отсутствует.
- Motion в «Значок + активность» даёт три волны только при `off → on` и затихает примерно через 3,3 с, даже если датчик ещё `on`.
- Occupancy/presence показывает спокойное статичное кольцо, пока присутствие активно.
- Штора в `opening/closing` показывает дыхание только в «Значок + активность»; при прямом `closed ↔ open` без промежуточного состояния эффект держится примерно 3,3 с.
- Работающий свет, реле, вентилятор, climate с активным `hvac_action`, плеер и пылесос получают жёлтую подложку; `automation = on` (включена) остаётся нейтральной.
- Дверь/окно, разблокированный замок и открытый клапан — оранжевые, а не жёлтые.
- Тревога всегда красная и пульсирует во всех трёх режимах; unavailable приглушён и не получает обычной активности.
- Цвет и размер ×2..×8 применяются к обычной активности; тревога остаётся красной.
- Размер ×0.5..×3 и поворот; бейджи масштабируются — Стенд: любой маркер.
- Риплы при выключенных live-состояниях — Стенд: выключить «живые состояния» в конфиге карточки (правка вида) — риплы живут.
- reduce motion — Стенд: включить в ОС тестера.
- При выключенных live-состояниях обычные статус и активность выключены, критическая тревога остаётся.
- reduce motion — Стенд: включить в ОС тестера; вместо анимации остаётся статичное кольцо.
## Двери и окна
+94 -45
View File
@@ -232,17 +232,23 @@ Run the *core flows* (marked ★ below) in each environment at least once per mi
- [ ] Drag anywhere (no edit mode), snaps to grid, persists after reload, per space
- [ ] ↺ reset restores auto layout after confirm
- [ ] Temperature badge on thermometers; LQI value under zigbee icons with red→green color
- [ ] Live states: light on = yellow, open cover/lock/door = orange, unavailable = faded
- [ ] Unified live states (dev, owner 2026-08-05): actual work is yellow;
open door/window, unlocked lock and open valve are orange; covers stay
neutral and morph their icon; unavailable is faded. The plate and the
activity effect come from the same semantic resolver
- [ ] State icons (v1.26.0): auto icons morph with state — door/window/garage open↔closed,
lock locked↔unlocked, bulb on; custom icons and unavailable states never morph [manual]
- [ ] display "Value instead of an icon": the marker shows the measurement (°/%/unit)
as its body, small badges hidden; non-numeric fallback keeps the icon [manual]
- [ ] RGB lights (v1.27.0, contract changed in v1.52.0): a lamp's colour lives
in its glow spot and the ripple fallback ONLY — the icon/badge/border get
no RGB tint; explicit ripple color still wins; off lights unchanged
in its glow spot and the activity-effect fallback ONLY — the icon/badge/border get
no RGB tint; explicit activity color still wins; off lights unchanged
[auto: smoke_light_badges + smoke_rgb_alarm]
- [ ] Alarm pulse (v1.27.0): leak/smoke/gas/CO/siren in 'on' pulse a red ring over any
display mode; clears on 'off'; unavailable never alarms [manual]; reduced-motion static
- [ ] Alarm pulse (v1.27.0, unified dev): leak/smoke/gas/CO/siren in `on`
and an alarm control panel in `triggered`
get a red plate and red pulse over every display mode and even with
ordinary live-state dressing off; clears on 'off'; unavailable never
alarms [manual]; reduced-motion is static
- [ ] Render cost (v1.43.1, audit L1): geometry (space model, open pairs) is
computed once per config change, not per HA state push — smoke asserts
zero recomputations across 10 state pushes and recomputation after an
@@ -299,9 +305,10 @@ Run the *core flows* (marked ★ below) in each environment at least once per mi
closed -> cover.open_cover, open (incl. ajar) -> cover.close_cover,
opening/closing -> cover.stop_cover, no readable state -> cover.toggle;
the «ask for confirmation» checkbox guards it like toggle/run.
Indication: while travelling the icon breathes a soft yellow ring
(.covermove, 2.2s, static under prefers-reduced-motion) and the plate
stays NEUTRAL in EVERY state (2026-08-04, see the next item); the icon
Indication: while travelling the icon breathes a soft activity ring
(`.activity-transition`, 2.2s, static under prefers-reduced-motion) in
display «Icon + activity» and the plate stays NEUTRAL in EVERY state
(2026-08-04, see the next item); the icon
morphs by state + device_class (blinds/shutter/curtain…), unknown state
morphs nothing; no position percentages anywhere
[auto: smoke_cover_tap + units resolveTapAction/coverService/stateIcon +
@@ -312,7 +319,8 @@ Run the *core flows* (marked ★ below) in each environment at least once per mi
closing: the plate is the plain neutral badge every time — never the
yellow «включено» one, never the orange «открыто» frame it used to wear
while open — the icon is the only open/closed signal, and the breathing
.covermove ring appears in the two travelling states and nowhere else.
`.activity-transition` ring appears in the two travelling states and
nowhere else when «Icon + activity» is selected.
The morph is exhaustive: every device class gives two DIFFERENT glyphs
(awning included), a cover with no device_class morphs within its own
auto-icon family (mdi:roller-shade, mdi:garage-variant), a hand-picked
@@ -334,7 +342,7 @@ Run the *core flows* (marked ★ below) in each environment at least once per mi
- [ ] Curtain INDICATION follows the same cover (dev, owner 2026-08-04): with
«Open/close» chosen on that same Aqara marker, the plan shows the cover
and not the service switch — the breathing ring while it travels
(`covermove`, opening AND closing), the `mdi:curtains-closed` /
(`activity-transition`, opening AND closing), the `mdi:curtains-closed` /
`mdi:curtains` morph, a neutral plate throughout (the «открыто» frame
was retired for covers later the same day), and no yellow plate when
`switch.*_reverse_direction` happens to be on. The rule is exactly
@@ -348,8 +356,8 @@ Run the *core flows* (marked ★ below) in each environment at least once per mi
device whose own `light.*` is ON, one whose bound `controls` switch is
ON. In every cover state (closed / open / opening / closing) the plate
stays neutral — never the yellow «включено», never the orange «открыто»
— the travelling ring breathes, the icon morphs with the cover, and in
glow fill the ring is still there (that is where a yellow-plated curtain
— with «Icon + activity» the travelling ring breathes, the icon morphs
with the cover, and in glow fill the ring is still there (that is where a yellow-plated curtain
used to lose BOTH indicators). Untouched: the same mixed device without
the explicit action is yellow again, a wall switch still mirrors its
controls, and an «Открыть/закрыть» marker whose device has no `cover.*`
@@ -357,7 +365,7 @@ Run the *core flows* (marked ★ below) in each environment at least once per mi
- [ ] Light-source badges (v1.52.0): in glow fill a lit lamp's badge stays
standard (the spot is the indicator) and a lit socket stays yellow; in
other fills a lit lamp is plain yellow with no RGB tint; morphing and
the ripple colour fallback survive [auto: smoke_light_badges +
the activity colour fallback survive [auto: smoke_light_badges +
smoke_rgb_alarm]
- [ ] Icon size multiplier scales the glyph (dev): set a marker's size to 3 —
the icon inside grows with the badge instead of staying default
@@ -365,8 +373,8 @@ Run the *core flows* (marked ★ below) in each environment at least once per mi
- [ ] Auto-grid parity (v1.51.2, HP-1511-01): with an empty layout, a visible
device among hidden ones sits at the same spot on both cards
[auto: smoke_hidden_flag autoGridParity]
- [ ] Ripple ghost (v1.51.2, HP-1511-02): a hidden ripple-display marker shows
its base icon, no pulse [auto: smoke_hidden_flag rippleGhost*]
- [ ] Activity ghost (v1.51.2, unified dev): a hidden icon+activity marker
shows its base icon and no effect [auto: smoke_hidden_flag rippleGhost*]
- [ ] Hidden LQI parity (v1.51.1, HP-1510-01): a room whose only Zigbee
devices are hidden paints the same lqi fill on the full and the static
card [auto: smoke_hidden_flag lqiParity]
@@ -378,7 +386,7 @@ Run the *core flows* (marked ★ below) in each environment at least once per mi
the count, still count toward room LQI, cast no glow/light fill; the
device editor's "Show hidden" (local, per tab) shows them as BLUE
dashed ghosts — distinct from a grey unavailable icon — with NO live
state paint (no yellow, no alarm, no ripple);
state paint (no yellow, no alarm, no activity);
unticking keeps a hidden:false marker (re-seed protection); an old
config materialises on first load by an editing client and legacy
clients keep the old behaviour until then
@@ -679,7 +687,7 @@ Run the *core flows* (marked ★ below) in each environment at least once per mi
tap action Toggle flips all bound lights/switches at once (any on → all
off, all off → all on, one service call); the icon state (yellow badge)
mirrors the targets, not the marker's own entity — the RGB tint is gone
since v1.52.0, target colours reach only the glow/ripple; without explicit Toggle
since v1.52.0, target colours reach only the glow/activity effect; without explicit Toggle
the click opens info as usual; the info card lists targets with states;
locks/other domains are filtered out of controls [auto: smoke_controls]
- [ ] Glow fill (v1.35.0): fill mode "Light sources" — every room painted with
@@ -869,17 +877,34 @@ require hands on real hardware — they remain for the human pass.
- [ ] `show_button: false` hides the footer
- [ ] Full card honours `#space=<id>` on load and on hashchange; invalid id ignored [manual]
## Presence ripples / per-device icon (v1.22.0)
## Unified device status and activity (dev, owner 2026-08-05)
- [ ] Marker dialog → Display = "Ripple only": the icon badge disappears, rings pulse while the
entity is on, and collapse to a faint dot when it goes off
- [ ] Display = "Icon + ripple": both the icon and the rings are drawn
- [ ] Ripple colour and size (×2..×8) apply per device
- [ ] An entity going `unavailable` stops the pulsing (idle dot), never leaves it running
- [ ] Icon size ×0.5..×3 and rotation 0..350° apply per device; the temp/humidity badges
scale with the icon
- [ ] Ripples still work with the card-wide "live states" toggle OFF (they are opt-in per device)
- [ ] With OS "reduce motion" enabled, rings do not animate
- [ ] The Display list contains exactly Icon, Icon + activity, Value instead
of an icon. Legacy `display: ripple` reads and saves back as `icon_ripple`
- [ ] Icon shows state plate/morph but no ordinary activity effect; Icon +
activity adds the semantic effect; Value keeps the state-coloured plate
and hides ordinary activity
- [ ] Motion/vibration/sound/contact rising edges render exactly three waves
for about 3.3 s; initial load and recovery from unknown/unavailable do
not fake an event; a rapid retrigger restarts it
- [ ] Occupancy/presence is one calm static ring for the whole active state
- [ ] Cover/lock/valve movement breathes until the travelling state ends;
direct terminal `closed ↔ open` / `locked ↔ unlocked` without an
intermediate state breathes for about 3.3 s
- [ ] Actual work (light/switch/fan/humidifier on, active climate action,
media playing, vacuum cleaning, script running) is yellow and slowly
breathes in Icon + activity; `automation = on` is merely enabled and
remains neutral
- [ ] Controls aggregate their targets: any working target drives both the
yellow plate and the running effect
- [ ] Open contact/unlocked lock/open valve are orange; an open cover stays
neutral because its icon morph carries that state
- [ ] Unavailable suppresses ordinary activity. Alarm outranks all of it,
remains red in every display mode and with ordinary live states off
- [ ] Activity colour and size (×2..×8) apply per device; alarm ignores them
- [ ] Icon size ×0.5..×3 and rotation 0..355° apply per device; the
temp/humidity badges scale with the icon
- [ ] With OS "reduce motion" enabled, activity/alarm rings are static
## Doors & windows (v1.23.0)
@@ -1132,25 +1157,30 @@ require hands on real hardware — they remain for the human pass.
## The text block on the plan (docs/LIVE-TEXT.md, dev, unreleased)
- [ ] **A label can show a live value**: in the Background editor place a text,
pick an entity in the dialog and write `Бак {}` — the plan shows
«Бак 68 %». Change the sensor: the label follows without any reload. A
label with no entity is untouched, byte for byte
[auto: smoke_live_text + unit logic.test]
- [ ] **The unit comes from the entity**: leave the unit field empty and the
entity's own `unit_of_measurement` is used (its placeholder shows which);
type your own and it wins. Choose an ATTRIBUTE (say `battery_level` on a
°C sensor) and the entity's unit is NOT inherited — an attribute is not
the state [auto: smoke_live_text]
- [ ] **One text, many HA variables**: write `Бак {sensor.tank}, зал
{climate.hall:current_temperature}` — both values render and update
independently. The hand-written dotted attribute form
`{climate.hall.current_temperature}` works too; ordinary/invalid braces
stay literal [auto: smoke_live_text + unit logic.test]
- [ ] **Picker inserts at the caret**: put the cursor between two words, choose
an entity and then its state/attribute — the full token appears exactly
at that selection and focus returns immediately after it. Continue typing
and insert another variable until the textarea's 200-character limit
[auto: smoke_live_text]
- [ ] **No separate unit/preview/single-slot UI**: the dialog has none of the
old unit field, `{}` hint, or preview block. State units come from HA;
attributes do not inherit the state unit, and a custom suffix is ordinary
text after the token [manual]
- [ ] **A dead sensor says so**: make the entity unavailable (or delete it) —
the value becomes «—» and the rest of the caption stays. The dash carries
no unit [auto: smoke_live_text]
- [ ] **Nothing is rounded**: a sensor reporting `23.94781` shows `23.94781`.
Rounding is the sensor's `display_precision`, not ours [auto:
smoke_live_text]
- [ ] **The preview is the truth**: the line under the dialog's fields is
rendered by the same substitution the plan uses, so what it shows is what
the plan will show [manual]
- [ ] **Old links migrate on edit**: a stored beta.9 `text + entity/attr/unit`
label still renders unchanged. Open it: the inline token is visible in
the textarea; save it and only the new text template remains
[auto: smoke_live_text]
- [ ] **The same everywhere**: the label reads identically in View, in the
editors and on a kiosk screen [auto: smoke_live_text]
- [ ] **No font-size choice any more**: the dialog has no Small/Medium/Large.
@@ -1427,9 +1457,9 @@ require hands on real hardware — they remain for the human pass.
attribute goes through the attribute formatter (a climate's
`current_temperature` is a number, not the climate's state)
[auto: smoke_value_format + unit logic.test]
- [ ] **Your own unit replaces the entity's**: type `проц.` in a label's unit
field — the result is «68,4 проц.», never «68,4 % проц.»
[auto: smoke_value_format]
- [ ] **A literal suffix stays literal**: write an attribute token followed by
` проц.` — the suffix is part of the text, with no hidden unit override
and no duplicate appended by the label renderer [auto: smoke_live_text]
- [ ] **An older Home Assistant is unchanged**: on an HA without
`formatEntityState` the badge and the label print the raw state with the
entity's unit appended, exactly as before — nothing is blank and nothing
@@ -1451,6 +1481,11 @@ require hands on real hardware — they remain for the human pass.
## Wall thickness (docs/WALL-THICKNESS.md, Unreleased)
- [ ] **Walls + adjacent draw thickness**: the first Plan-editor tool is named
“Walls” / «Стены» rather than “Add”; while it is active, its thickness
field is the immediately following toolbar element, before Merge. The
field still defaults to 15 cm and disappears when another tool is selected
[auto: smoke_draw_wall_thickness]
- [ ] **Tool + hover + input**: Plan editor → Wall thickness. Hover highlights
the whole wall; click opens the cm/in field; empty/0 clears; Esc closes
without applying; «Apply to all walls of this room» fills every allowed
@@ -1462,6 +1497,11 @@ require hands on real hardware — they remain for the human pass.
the body; the door swing is offset toward the inner face; with
`hide_openings` the symbols hide but the cut remains
[auto: smoke_wall_thickness]
- [ ] **Door light uses the clear tunnel**: place an off-centre light beside a
door in a thick wall. In the neighbouring room the glow is limited by
sight lines through both the near and far inner-face corners; neither
side crosses a solid jamb return. Clearing wall thickness restores the
wider centreline-based sector [auto: test/logic.test.mjs; manual visual]
- [ ] **Shared once / clear → line / resize re-keys**: one body for a shared
wall; clearing thickness restores the centreline; resizing a thick wall
keeps the thickness on the moved stretch, including both atomic solid
@@ -1469,9 +1509,18 @@ require hands on real hardware — they remain for the human pass.
[auto: smoke_wall_thickness + smoke_resize_virtual_thick]
- [ ] **Virtual T-junction**: when two real thick arms from different room
contours meet at an `open_span` endpoint, the outside corner is a clean
mitre with no stair-step; the saved dash and the two-click rubber band
paint above the real hatch right up to the centreline
mitre with no stair-step. In every editor the saved dash and the two-click
rubber band paint above the real hatch right up to the centreline; in
View the same saved dash paints below the body, so each thick jamb masks
its centreline end without shortening the stored span
[auto: test/wall-thickness.test.mjs + smoke_resize_virtual_thick]
- [ ] **Fragment normalisation**: draw adjacent/overlapping virtual stretches
along the same pair of rooms — they persist and select as one span. Close
the last virtual stretch on a uniformly thick wall — its saved `walls`
data returns to one whole-edge key. Repeat across a Split boundary: spans
owned by different room pairs must stay separate
[auto: test/open-spans.test.mjs + test/wall-thickness.test.mjs +
smoke_resize_virtual_thick]
- [ ] **Unit + backend**: inset/mitre/bevel, key from either end, degrade,
rekey, cm↔inches; `walls` schema bounds
[auto: test/wall-thickness.test.mjs + tests_backend/test_validation.py]
+12 -6
View File
@@ -48,8 +48,9 @@ Header in View: space tabs, device count, zoom cluster. Nothing else.
## Plan — geometry and appearance of the space
- Toolbar tools: Outline room (with a session wall-thickness field, default
15 cm — docs/WALL-THICKNESS.md §6), Delete room, Merge, Split, Resize,
- Toolbar tools: **Walls** / room outline (with its session wall-thickness field
immediately on the right, default 15 cm — docs/WALL-THICKNESS.md §6), Delete
room, Merge, Split, Resize,
Opening (place / drag along walls / properties), Open boundary, Wall thickness
(docs/WALL-THICKNESS.md — click a wall, set cm/inches from HA's unit system;
empty/0 clears), Room labels (drag positions — labels are part of the plan).
@@ -74,6 +75,10 @@ layer you cannot see is a layer you cannot edit.
- **Virtual walls follow `show_borders`** (owner, 2026-08-05). They are walls —
dashed ones. Drawing them on a space with no borders left a plan whose only
walls were a few floating dashed stretches.
- **Their geometry and their presentation are separate.** Every virtual span
still ends on the real wall centreline. In View the thick wall body is
painted over the dash ends, so they visually stop at its faces; editors paint
the full dash (and live preview) over the body for unambiguous editing.
- **`hide_openings` hides the symbol, not the opening.** Light still spills
through it, the sun still enters at a window, a contact sensor still opens
it, and the resize tool still anchors to it. Anything else would be a second
@@ -85,7 +90,8 @@ layer you cannot see is a layer you cannot edit.
## Devices — placement and marker configuration
- Icon dragging (ONLY here). Click on a device opens the **edit dialog directly**
(binding, name, icon, size/angle, display badge/ripple + colors, tap override,
(binding, name, icon, size/angle, display as icon / icon + activity / value,
activity color and size, tap override,
model/link/description/PDFs, room).
- + add device/entity/virtual, the "Hide device from plan" checkbox (since
v1.51.0 the one hiding mechanism, docs/FILTERING.md), ↺ reset layout,
@@ -117,12 +123,12 @@ layer you cannot see is a layer you cannot edit.
1. State-reflecting icons (open/closed door variants etc., like core HA).
2. `display: value` — show the measurement instead of an icon.
3. Light color in the icon/ripple (RGB lights). *(Shipped in v1.27.0;
superseded in v1.52.0: the colour lives in the glow spot and the ripple
3. Light color in the activity effect (RGB lights). *(Shipped in v1.27.0;
superseded in v1.52.0: the colour lives in the glow spot and the activity
fallback only, the icon tint was removed by the owner's rule.)*
4. Alarm visual (leak/smoke/doorbell): red pulse overlay.
5. Rooms as sub-areas without an HA area + manual device placement by room id.
6. Backlog (not planned): music notes for players, directional TV ripples.
6. Backlog (not planned): music notes for players, directional TV effects.
## Implementation iterations
+17 -4
View File
@@ -18,7 +18,11 @@ Code: `src/wall-thickness.ts`, render in `src/houseplan-card.ts` /
Per space: `walls: [{ key, cm }]`. Key = quantised midpoint + direction
(modulo 180°). Config always stores centimetres. Open boundaries refuse
thickness. One physical stretch has one thickness (atomic collinear spans when
neighbours overlap only partially).
neighbours overlap only partially). Atomic keys are storage only while a real
geometric break requires them: once an original edge is entirely solid with
one thickness, normalisation writes its single whole-edge key again. Likewise,
touching/overlapping `open_spans` of the same room pair are stored as one span;
pair ownership remains a hard boundary so Split can derive exact `open_to` links.
Degrade unmatched keys silently on write. Resize / undo / scale re-key all
touched spans in the same transaction and keep `walls` in the resize snapshot.
@@ -47,12 +51,19 @@ angle (mod 180°), then nearest span — never a perpendicular neighbour at a T.
At an `open_span` endpoint, real arms owned by different room contours receive
the same bounded mitre patch as arms from one contour; a virtual T therefore
has one clean outer corner rather than two butt caps forming a step.
The virtual segment itself remains centreline-based. View paints it before the
wall body, which masks the part inside each adjoining thick jamb; editors paint
it after the body so its full stored extent and live preview stay visible.
## 4. Floor, fills, light, area
- **Inner contour** = `inset(poly, half[])`.
- Room fills and glow are clipped to the inner contour (not painted into the
wall hatch).
- Glow leaving through a door uses the clear rectangular opening tunnel: its
sector is the intersection of the doorway spans at the near and far inner
faces. The two jamb returns therefore clip off-axis light; a zero-depth wall
keeps the original centreline sector.
- Displayed **m²** = area of the inner contour (clean floor). Wall-length
rulers and opening anchors stay on the centreline.
- With no thickness, inner = poly (parity with pre-thickness behaviour).
@@ -70,8 +81,9 @@ centreline/full-span wedge. `hide_openings` hides the symbol only.
Plan-editor tool «Wall thickness»; hover whole wall; cm/in from HA; empty/0
clears; apply-to-room. Hooks: `data-hp="wall"`. i18n en/ru.
**Draw with thickness.** The Draw toolbar carries a session thickness field
(default **15 cm**, or inches when HA is imperial). Closing a new room outline
**Draw with thickness.** The Plan toolbar's **Walls** button carries its session
thickness field immediately on the right (default **15 cm**, or inches when HA
is imperial). Closing a new room outline
writes that cm onto every new edge that does not already have one; shared
stretches that already carry a neighbour's thickness are left alone. Empty / 0
leaves the room thin. Live thick preview follows the rubber-band while drawing.
@@ -84,7 +96,8 @@ Decor-line thickness, per-side finish, auto-from-backdrop, plan-wide default.
## 8. Testing
Unit: ring closed at corners; half-out; inner area; atomic partial shared;
virtual-T mitre; angle-aware opening; whole and atomic rekey after edge/scale.
virtual-T mitre; angle-aware opening; thick-door tunnel clipping; whole and
atomic rekey after edge/scale.
Browser: seamless frame; fill not in hatch; m² drops with thickness; a partial
virtual stretch, its solid thick remainders and Undo move as one real resize;
the virtual rubber band paints above the real body; sun starts at the room-side
@@ -0,0 +1,155 @@
# Единая индикация состояния и активности устройств
Статус: **утверждено владельцем и реализовано локально 2026-08-05**.
## Цель
Заменить четыре пересекающиеся механики — цвет подложки, пользовательскую
пульсацию, motion/presence-эффекты и движение штор — одной семантической
системой. Визуал должен отвечать на три разных вопроса:
1. Доступно ли устройство.
2. В каком устойчивом состоянии оно находится.
3. Что оно делает прямо сейчас или что только что произошло.
## Режимы отображения
В UI остаются:
1. **Значок** — иконка, морфинг и статусная подложка без обычных эффектов
активности.
2. **Значок + активность** — то же плюс семантический эффект активности.
3. **Значение вместо значка** — числовая плашка со статусным цветом; обычная
активность не рисуется.
Опция **«Только пульсация» удаляется**. Legacy `display: ripple` читается как
`icon_ripple`, а при следующем сохранении конфигурации переписывается в
`icon_ripple`. Backend временно продолжает принимать старое значение.
## Модель визуального состояния
Для маркера вычисляется один результат:
```ts
availability: 'available' | 'unavailable'
status: 'neutral' | 'open' | 'working' | 'alarm'
activity: 'none' | 'event' | 'presence' | 'transition' | 'running'
```
- `availability` отвечает за приглушение недоступного устройства;
- `status` управляет подложкой и морфингом;
- `activity` управляет кольцом или волнами;
- тревога имеет высший приоритет;
- недоступность подавляет обычную активность.
## Визуальный язык
| Смысл | Подложка | Эффект в «Значок + активность» |
|---|---|---|
| Доступно, простаивает | нейтральная | нет |
| Выполняет основную функцию | жёлтая | медленное дыхание |
| Открыто / разблокировано | оранжевая | только событие/переход, не постоянный эффект |
| Разовое срабатывание | текущая статусная | три расходящиеся волны, 3,3 с |
| Присутствие | нейтральная | спокойное статичное кольцо |
| Механическое движение | текущая статусная | дыхание до конца движения |
| Недоступно | приглушённая | нет |
| Тревога (протечка, дым, газ, CO, safety/tamper/problem, сирена, сработавшая охрана) | красная | быстрая красная пульсация при любом display |
`prefers-reduced-motion` заменяет анимацию статичным кольцом.
## Значение жёлтой подложки
Жёлтый означает только **фактическое выполнение основной функции**:
- `light/switch/fan/humidifier = on`;
- climate: `hvac_action = heating/cooling/drying/fan`, но не просто выбранный
режим `heat/cool/auto`;
- media player: `playing`;
- vacuum: `cleaning` (и работающий механизм при `returning`);
- бытовая техника: `running/washing/rinsing/spinning/drying/heating/cooking`;
- маркер с `controls`: работает хотя бы одна управляемая цель.
Не являются работой: открытая дверь, разблокированный замок, открытые шторы,
presence/motion, `enabled`, `home`, `unknown`, `unavailable`, climate `idle`.
В glow-режиме у реального источника света жёлтая подложка может быть скрыта:
его устойчивым индикатором служит световое пятно. Эффект активности остаётся.
## Семантика активности
### Event, 3,3 секунды
- motion: только засвидетельствованный `off -> on`, не весь cooldown;
- vibration/shock/sound;
- открытие двери/окна (`off -> on`), но не всё время открытого состояния;
- button/event;
- успешный запуск script/scene/automation без устойчивого состояния.
Первое состояние после загрузки и восстановление из `unknown/unavailable` не
создают ложного события. Повторный детект перезапускает окно и анимацию.
### Presence
`occupancy/presence = on` даёт статичное кольцо до исчезновения присутствия.
### Transition
- cover: `opening/closing`;
- lock: `locking/unlocking`;
- valve: `opening/closing`;
- vacuum: `returning`.
Если промежуточного состояния нет, прямой конечный переход
`closed <-> open` / `locked <-> unlocked` показывает transition 3,3 секунды.
### Running
Пока устройство фактически работает, его подложка жёлтая, а в режиме
«Значок + активность» вокруг неё медленно дышит кольцо.
## Источник состояния
Приоритет:
1. Явный специализированный источник (в будущей расширенной настройке).
2. Cover, когда tap action явно задан как «Открыть/закрыть».
3. `controls` — агрегированное состояние всех управляемых целей.
4. Функциональная сущность устройства / включённый light.
5. Primary entity.
Критические сущности устройства проверяются независимо и не могут быть
скрыты менее важным primary state.
## Приоритет вывода
1. alarm;
2. unavailable;
3. event;
4. transition;
5. presence;
6. running;
7. open;
8. neutral.
## Инварианты
- Одна функция вычисляет подложку и activity; они не смотрят на разные
сущности.
- Обычная активность появляется только в `icon_ripple`.
- Alarm отображается при любом display и не зависит от пользовательского цвета.
- Hidden/ghost не получает live-чисел, морфинга, подложки или активности.
- Static `houseplan-space-card` остаётся статической и не рисует live effects.
- Пользовательские цвет и размер применяются к обычной активности, но не к
красной тревоге.
## Реализация
- Чистая классификация сущностей, агрегация и распознавание переходов:
`src/device-visual.ts`.
- Выбор эффективных источников маркера, runtime-окно 3,3 с, миграция legacy
display и рендер: `src/houseplan-card.ts`.
- Общий слой колец и reduced-motion варианты: `src/styles.ts`.
- UI оставляет три режима; backend сохраняет `ripple` только как входное
legacy-значение до завершения миграционного периода.
- Unit-спецификация классификатора подготовлена в
`test/device-visual.test.mjs`; браузерные smokes переведены на новые классы.
@@ -0,0 +1,465 @@
# Свободные цепочки стен и разная толщина при рисовании — ТЗ
Статус: **согласовано и отложено** (2026-08-05).
> Реализацию не начинать до отдельного прямого запроса владельца проекта.
> Этот документ фиксирует требования и принятые продуктовые решения для
> будущей реализации.
## 1. Цель
Добавить возможность:
1. Рисовать одну цепочку стен, задавая отдельную толщину каждому следующему
сегменту.
2. Оставлять на плане незамкнутые стены, не принадлежащие комнате или контуру.
3. Переключаться на другие инструменты без удаления уже нарисованной
незамкнутой цепочки.
4. После возвращения в инструмент или перезагрузки продолжать сохранённую
цепочку с одного из её концов.
5. Замыкать цепочку с выбором результата:
- создать комнату;
- оставить замкнутую цепочку самостоятельными стенами.
## 2. Текущее состояние и архитектурная причина изменения
Сейчас комната является единственным первичным источником геометрии стен:
- контур хранится в `rooms[].poly`;
- стены вычисляются из рёбер комнат;
- `walls[]` хранит толщину рёбер, но не самостоятельную геометрию;
- незаконченный `_path` является временным состоянием редактора;
- смена инструмента очищает `_path`;
- сервер удаляет старое legacy-поле `segments`;
- статическая карточка, проёмы, привязки и расчёт видимой области получают
геометрию стен преимущественно из комнат.
Поэтому функция должна вводить новую сохраняемую сущность. Простое сохранение
текущего `_path` не решает рендер, восстановление после перезагрузки,
посегментную толщину и интеграцию с остальными инструментами.
## 3. Термины
- **Свободная цепочка стен** — упорядоченная открытая или замкнутая ломаная,
которая не принадлежит комнате.
- **Сегмент** — отрезок между двумя соседними вершинами; у замкнутой цепочки
последний сегмент соединяет последнюю вершину с первой.
- **Активная цепочка** — цепочка, которую пользователь сейчас продолжает.
- **Линейная стена** — существующий вариант стены без объёмного тела.
- **Толстая стена** — стена с положительной толщиной в сантиметрах.
- **Виртуальный участок** — существующий `open_span`. Отсутствие толщины само
по себе не превращает стену в виртуальную.
## 4. Модель данных
В конфигурацию пространства добавить необязательное поле `wall_paths`:
```ts
interface WallPathCfg {
id: string;
points: Array<[number, number]>;
closed?: boolean;
segments: Array<{
cm?: number;
}>;
}
```
Пример открытой цепочки:
```json
{
"id": "wp_01",
"points": [[0.10, 0.20], [0.40, 0.20], [0.40, 0.55]],
"segments": [{ "cm": 15 }, { "cm": 25 }]
}
```
Пример замкнутой самостоятельной цепочки:
```json
{
"id": "wp_02",
"closed": true,
"points": [[0.10, 0.20], [0.40, 0.20], [0.40, 0.55]],
"segments": [{ "cm": 15 }, { "cm": 20 }, { "cm": 25 }]
}
```
Инварианты:
- открытая цепочка: `points.length >= 2` и
`segments.length === points.length - 1`;
- замкнутая цепочка: минимум три пригодные вершины и
`segments.length === points.length`;
- у замкнутой цепочки последний сегмент идёт от последней вершины к первой;
- первая точка не дублируется в конце массива;
- `cm` отсутствует для линейной стены без объёмного тела;
- отсутствие `cm` не означает `open_span`;
- соседние точки не совпадают;
- `id` уникален в пределах пространства;
- координаты нормализованы по тем же правилам, что и `rooms[].poly`;
- старое корневое поле `segments` не возвращается и продолжает считаться
legacy.
Толщина хранится внутри `wall_paths`, потому что существующий `walls[]`
привязан к геометрии комнат и удаляет осиротевшие ключи при нормализации.
## 5. Рисование и посегментная толщина
### 5.1 Начало цепочки
1. Пользователь выбирает инструмент «Стены».
2. Первый клик создаёт временную начальную точку.
3. Второй клик создаёт первый сегмент и постоянную запись в `wall_paths`.
4. Одиночная точка без сегмента не сохраняется и исчезает при смене
инструмента или перезагрузке.
### 5.2 Значение поля толщины
Поле толщины означает **толщину следующего сегмента**:
- значение фиксируется при клике второй точки сегмента;
- изменение поля не меняет уже созданные сегменты;
- live-preview использует текущее значение;
- пустое значение создаёт линейную стену;
- конфиг всегда хранит сантиметры независимо от выбранной системы единиц;
- допустимая толщина — 1–100 см;
- нечисловое значение и значение вне диапазона показывают ошибку и не дают
создать сегмент.
Пример: пользователь может последовательно создать сегменты 15 см, 25 см,
линейный сегмент и снова 15 см в пределах одной цепочки.
## 6. Сохранение при смене инструмента
Как только создан первый сегмент:
- цепочка становится частью конфигурации пространства;
- каждый следующий законченный сегмент сразу добавляется в локальную
конфигурацию;
- запись на сервер может оставаться debounce-записью;
- перед сменой этажа, режима, пространства или размонтированием карточки
ожидающая запись принудительно завершается;
- переключение на другой инструмент прекращает активное рисование, но не
удаляет цепочку;
- цепочка остаётся видимой во всех предусмотренных режимах.
Активное состояние редактора не требуется хранить на сервере.
## 7. Продолжение цепочки
Принятое поведение:
- в той же сессии после возвращения в инструмент «Стены» автоматически
продолжается последняя активная цепочка;
- после перезагрузки пользователь выбирает конечную точку кликом;
- конечные точки доступных цепочек подсвечиваются при наведении;
- клик по последней точке продолжает цепочку в текущем направлении;
- клик по первой точке разворачивает порядок точек и сегментных параметров,
после чего продолжает цепочку с этого конца;
- при продолжении поле толщины принимает значение крайнего сегмента выбранного
конца, после чего пользователь может его изменить;
- из середины сегмента и из внутренней вершины нельзя создавать ответвление.
### 7.1 Соединение двух цепочек
Разрешается соединение **конец с концом**:
- при необходимости одна или обе цепочки разворачиваются;
- точки и параметры сегментов объединяются без потери соответствия;
- одна исходная запись сохраняет `id`, вторая удаляется;
- нулевой соединительный сегмент не создаётся;
- соединение через середину сегмента не поддерживается;
- если соединение приводит к замыканию, открывается диалог завершения,
описанный ниже.
## 8. Замыкание цепочки
При клике по начальной точке активной цепочки с минимум тремя пригодными
вершинами:
1. Проверяется замыкающий сегмент.
2. Проверяются нулевая длина, самопересечения и недопустимые наложения.
3. Открывается существующий диалог создания комнаты.
4. Внизу диалога добавляется второе действие
**«Оставить замкнутыми стенами»**.
До выбора результата исходная открытая цепочка остаётся в конфигурации, а
замыкающий сегмент хранится как ожидающее локальное изменение. Это исключает
потерю стен при отмене диалога или ошибке сохранения.
### 8.1 Основное действие: создать комнату
При обычном сохранении комнаты одной транзакцией:
- создаётся `room.poly`;
- свободная цепочка удаляется из `wall_paths`;
- толщина каждого сегмента переносится в существующую модель `walls[]`;
- замыкающий сегмент получает толщину, выбранную перед замыканием;
- если новый контур разделяет физическую стену с существующей комнатой,
побеждает толщина уже существующей стены.
Одна физическая стена не должна иметь две конкурирующие толщины.
### 8.2 Второе действие: оставить замкнутыми стенами
При выборе «Оставить замкнутыми стенами»:
- комната не создаётся;
- заливка, название и площадь не появляются;
- замыкающий сегмент добавляется в `segments`;
- цепочка сохраняется с `closed: true`;
- активное рисование этой цепочки завершается.
Для продолжения замкнутой цепочки сначала потребуется удалить один из её
сегментов. Перемещение вершин в первой версии не предусмотрено.
### 8.3 Отмена и ошибки
- отмена диалога возвращает пользователя к открытой сохранённой цепочке;
- ошибка записи не удаляет исходный `wall_path`;
- удаление `wall_path` и добавление комнаты выполняются атомарно в одном
изменении локальной конфигурации.
## 9. Рендер
Свободные стены отображаются:
- в редакторе плана — всегда;
- в остальных редакторах — по действующим правилам отображения границ;
- в режиме просмотра — при включённом `show_borders`;
- в основной и статической карточке.
### 9.1 Толстые стены
Для сегментов с толщиной необходимо:
- строить тело относительно центральной линии;
- объединять тела свободных и комнатных стен перед отрисовкой;
- использовать существующие заливку и штриховку стен;
- корректно формировать L- и T-образные стыки;
- объединять сегменты разной толщины без щелей;
- ограничивать miter на острых углах и переходить к bevel;
- завершать действительно свободный конец ровным поперечным срезом;
- прятать окончание линейного сегмента под телом примыкающей толстой стены.
### 9.2 Виртуальные участки
Действующее правило сохраняется:
- в режиме просмотра виртуальный пунктир рисуется под телом стены;
- в редакторах пунктир рисуется поверх тела полностью от центральной линии;
- свободная линейная стена не является виртуальным участком.
### 9.3 Границы содержимого
`wall_paths` участвуют в расчёте видимой области с учётом половины толщины.
План, состоящий только из свободных стен, должен корректно центрироваться и
масштабироваться.
## 10. Влияние на комнаты, площадь и свет
До преобразования в комнату свободная цепочка:
- не создаёт заливку пола;
- не создаёт площадь и подпись м²;
- не создаёт название комнаты;
- не связывается с HA area;
- не изменяет внутренний контур существующей комнаты;
- не вычитает площадь из комнаты;
- не разделяет комнату на световые зоны;
- не участвует в распространении света между комнатами;
- не создаёт солнечные лучи или дверной тоннель.
Свободная стена является отображаемой физической геометрией, но не
топологической границей помещения.
Автоматическое распознавание комнат из произвольного графа пересекающихся стен
не входит в задачу. Комната создаётся только явным замыканием цепочки и выбором
основного действия в диалоге.
## 11. Работа инструментов
### 11.1 Толщина стены
Инструмент позволяет выбрать сегмент свободной цепочки и изменить только его
толщину. Действие «Применить ко всей цепочке» можно добавить позднее как
необязательное улучшение.
### 11.2 Удаление
- единственный сегмент: удалить всю цепочку;
- крайний сегмент: укоротить цепочку;
- средний сегмент: разбить цепочку на две независимые цепочки;
- одна часть сохраняет старый `id`, вторая получает новый;
- часть без сегментов не сохраняется;
- удаление сегмента замкнутой цепочки превращает её в открытую цепочку с
`closed: false`.
### 11.3 Resize и перемещение вершин
Не входят в первую версию. Resize продолжает работать только с комнатами.
Перемещение вершин свободных цепочек вынесено в следующий этап.
### 11.4 Merge и Split
Не считают свободные цепочки комнатами и не изменяют их автоматически.
### 11.5 Выравнивание
Глобальное выравнивание плана по сетке должно включать точки `wall_paths`, не
нарушая соответствие сегментов и их толщин.
### 11.6 Привязка мебели
Свободные физические стены рекомендуется включить в привязку мебели, поскольку
для пользователя они являются обычными стенами.
### 11.7 Двери и окна
Двери и окна устанавливаются **только в стены комнат**.
- свободные открытые и замкнутые цепочки не являются целью инструмента
проёмов;
- при наведении на них инструмент не показывает доступную привязку;
- проёмы становятся доступны только после преобразования цепочки в комнату.
## 12. Undo, Escape и Reset
Рекомендуемая безопасная модель:
- `Ctrl+Z` или `Escape` удаляет последний сегмент, добавленный в текущей
сессии редактирования;
- при продолжении старой цепочки Undo не удаляет части, существовавшие до её
выбора;
- когда добавленных в сессии сегментов больше нет, следующее действие снимает
активность с цепочки;
- для новой цепочки отмена первого сегмента удаляет `wall_path`, оставляя
временную стартовую точку;
- следующая отмена удаляет стартовую точку.
Reset следует трактовать как «Отменить текущие изменения цепочки»:
- новая цепочка возвращается к состоянию до текущего рисования;
- у продолженной цепочки удаляются только добавленные в этой сессии сегменты;
- ранее сохранённая часть не удаляется;
- полное удаление выполняется инструментом удаления.
## 13. Сохранение и конкурентные изменения
- каждый завершённый сегмент сразу попадает в локальную конфигурацию;
- запись на сервер выполняется существующей сериализованной очередью;
- ожидающий debounce принудительно завершается при уходе из контекста;
- `activeWallPathId` является только состоянием редактора;
- после перезагрузки продолжение начинается выбором конечной точки;
- преобразование в комнату не должно сначала удалить цепочку и только потом
пытаться сохранить комнату;
- конфликты ревизий обрабатываются действующим механизмом конфигурации.
## 14. Серверная валидация и ограничения
Предлагаемые пределы:
- не более 500 цепочек на пространство;
- не более 500 точек в одной цепочке;
- не более 2000 свободных сегментов суммарно;
- только конечные числовые координаты в допустимых границах холста;
- толщина 1–100 см;
- уникальные идентификаторы;
- отсутствие совпадающих соседних точек;
- точное соответствие количества сегментов типу цепочки;
- минимум три пригодные вершины у замкнутой цепочки;
- лишние или повреждённые поля отклоняются либо нормализуются единообразно.
Существующие конфигурации миграции не требуют: `wall_paths` является
необязательным полем.
## 15. Геометрические и UX edge cases
Обязательно учесть:
- смену инструмента после единственной точки;
- двойной клик в одной координате;
- нулевой и очень короткий сегмент;
- возврат в предыдущую точку;
- попытку замкнуть цепочку с недостаточным количеством вершин;
- самопересечение замыкающего или обычного сегмента;
- прохождение свободной стены через существующую комнату;
- полное или частичное совпадение с комнатной стеной;
- полное или частичное совпадение двух свободных цепочек;
- пересечение двух цепочек без общей вершины;
- соединение двух цепочек с необходимостью развернуть обе;
- попытку соединиться с серединой сегмента;
- T-образный стык стен разной толщины;
- переход толстая → линейная → толстая;
- острый угол и ограничение miter;
- удаление среднего сегмента открытой и замкнутой цепочки;
- отмену диалога комнаты;
- ошибку сервера при завершении комнаты;
- изменение единиц измерения во время рисования;
- координаты вне сетки;
- достижение лимита точек или общего размера конфигурации;
- план без комнат, содержащий только свободные стены;
- скрытые границы в режиме просмотра;
- конкурентное редактирование в другой вкладке.
## 16. Критерии готовности первой версии
1. Незамкнутая цепочка с хотя бы одним сегментом не исчезает при смене
инструмента.
2. Она сохраняется после перезагрузки карточки и Home Assistant.
3. В той же сессии рисование продолжается автоматически.
4. После перезагрузки цепочку можно продолжить с любого конца.
5. Две цепочки можно соединить конец с концом.
6. Ответвление из середины сегмента невозможно.
7. Толщина задаётся отдельно каждому следующему сегменту.
8. Сегменты разной толщины образуют корректное тело без щелей.
9. При замыкании доступны «Создать комнату» и
«Оставить замкнутыми стенами».
10. Создание комнаты переносит толщины и атомарно удаляет свободную цепочку.
11. При общей стене сохраняется ранее существовавшая толщина.
12. Отмена диалога не удаляет свободную цепочку.
13. Замкнутая самостоятельная цепочка сохраняется без комнаты, заливки и
площади.
14. Свободные стены видны в основной и статической карточке.
15. Они учитываются при масштабировании и центрировании.
16. Удаление среднего сегмента корректно разделяет цепочку.
17. Двери и окна нельзя устанавливать в свободные стены.
18. Старые конфигурации продолжают работать без миграции.
## 17. Не входит в первую версию
- перемещение вершин свободных стен;
- Resize свободных цепочек;
- ответвление из середины сегмента;
- автоматическое распознавание комнат из сети стен;
- установка дверей и окон в свободные стены;
- участие свободных стен в комнатной топологии, площади и распространении
света;
- отдельные материалы, стили и свойства сторон свободной стены.
## 18. Предлагаемый порядок будущей реализации
1. Схема `wall_paths`, серверная валидация и чистые операции над цепочками.
2. Посегментная толщина в инструменте «Стены».
3. Сохранение, автоматическое продолжение и выбор конца после перезагрузки.
4. Соединение цепочек конец с концом.
5. Диалог замыкания и два варианта завершения.
6. Геометрия тел и стыков стен разной толщины.
7. Основная и статическая карточки, расчёт видимой области.
8. Инструменты толщины, удаления, привязки и выравнивания.
9. Документация и тестовый чек-лист.
10. Прогон тестов только перед пре-релизом согласно принятому процессу.
## 19. Зафиксированные ответы владельца
| Вопрос | Решение |
|---|---|
| Что делать при замыкании? | Открывать диалог комнаты; внизу сразу добавить второе действие «Оставить замкнутыми стенами». |
| Как продолжать цепочку? | В той же сессии продолжать автоматически; после перезагрузки выбирать конец кликом. |
| Как соединять цепочки? | Соединять конец с концом; ответвление из середины сегмента не поддерживать. |
| Разрешать двери и окна? | Нет, проёмы устанавливаются только в стены комнат. |
| Нужны ли перемещение вершин и Resize? | Вынести в следующий этап; в первой версии оставить продолжение, изменение толщины и удаление сегментов. |
| Какая толщина побеждает у общей стены? | Уже существующая физическая стена определяет толщину. |
| Сохранять ли одиночную точку? | Нет, точка без сегмента остаётся временной. |