feat(card): the LED strip tool in the Devices editor (#780)

Stage 4 of #780 (ТЗ §4–§5). «LED strip» next to «Add» draws a chain with
clean clicks only (pan, pinch, a second finger, cancel and the synthetic
click after navigation add nothing), snaps to the grid and to physical wall
faces / zero-wall axes, stops at the first face of masonry, partitions,
columns and windows (doors, gates and passages are cut by geometry), Shift
gives 45°. Ctrl+Z removes the chain's own point first, Esc finishes, a
double click or a click on the first point (≥3 vertices) closes; fewer than
two distinct points write nothing. A finished strip opens the device picker
(lights first, taken markers explained, «New device…» binds in that
dialog's own write, «Later» keeps unbound geometry).

A selected strip shows its vertex handles (a drag re-checks both neighbours
and the whole path) and a new Devices tray branch: device settings,
bind/change, unbind, show as icon, delete. The device dialog gets the
representation section: show as strip (restores a hidden shape at once or
draws one for this marker, behind the dialog's own save/discard guard),
show as icon, unbind/delete for a hidden shape. Every geometry or
representation change is one optimistic write and one LED command of the
device history; Undo/Redo restores only its own strip record and refuses
when a newer change sits on it. A rebinding renames the link in the marker
save, a deleted marker leaves an unbound strip, a bound marker may not move
to another space. Plan/Background show strips as a passive translucent mark.

The tool, its `led` dictionary (en static, ru/de/fr lazy) and placement
geometry are a new lazy `led-strip-editor` chunk (9.5 KB gzip), loaded only
on the tool, an editable strip in the shown space or a strip device's
dialog. The initial graph gets the loader and delegation only
(src/led-strip-card.ts); the lazy editor graph +122 B, inside its ceiling.
Mutant anchors follow the moved code (marker dialog guard, static LED layer).

Issue: #780
User-Visible: no
This commit is contained in:
Codex
2026-10-02 12:13:23 +03:00
parent c7c67830f6
commit fa89d655ed
28 changed files with 1479 additions and 49 deletions
+7 -6
View File
@@ -6084,9 +6084,9 @@ const MUTANT_DEFINITIONS = [
patches: [{
file: 'custom_components/houseplan/validation.py',
find: ' extra=vol.ALLOW_EXTRA, # unknown (legacy) keys do not break loading\n'
+ ' ),\n canonicalize_config_geometry,\n _config_wall_segment_invariants,\n)',
+ ' ),\n canonicalize_config_geometry,\n _config_wall_segment_invariants,\n',
replace: ' extra=vol.ALLOW_EXTRA, # unknown (legacy) keys do not break loading\n'
+ ' ),\n lambda value: value,\n _config_wall_segment_invariants,\n)',
+ ' ),\n lambda value: value,\n _config_wall_segment_invariants,\n',
}],
},
{
@@ -7387,7 +7387,7 @@ const MUTANT_DEFINITIONS = [
find: ' ${passageGlowTunnels}\n'
+ ' <g class="decorlayer" pointer-events="none">${decorImages}</g>\n'
+ ' <g class="hp-stairs-layer" pointer-events="none">${stairShapes}</g>\n'
+ ' ${glowPools}\n ${wallUnion',
+ ' ${glowPools}\n ${leds.size',
replace: ' ${passageGlowTunnels}\n'
+ ' ${!space.bg && !disp.showNames ? svg`<g class="room-svg-labels" pointer-events="none">${space.rooms.map((room) => {\n'
+ ' const center = roomCenter(room);\n'
@@ -7397,7 +7397,7 @@ const MUTANT_DEFINITIONS = [
+ ' <g class="decorlayer" pointer-events="none">${decorImages}</g>\n'
+ ' <g class="hp-stairs-layer" pointer-events="none">${stairShapes}</g>\n'
+ ' ${glowPools}\n'
+ ' ${wallUnion',
+ ' ${leds.size',
}],
},
{
@@ -13741,8 +13741,9 @@ const MUTANT_DEFINITIONS = [
+ 'Перевёрнутое условие закрывает «грязный» диалог молча и спрашивает у чистого',
patches: [{
file: 'src/editors/marker-dialog.ts',
find: " if (!edit || !dirty) { forgetMarkerBaseline(this.host); this._closeMarkerDialog(); return; }",
replace: " if (!edit || dirty) { forgetMarkerBaseline(this.host); this._closeMarkerDialog(); return; }",
// #780: the guard is the dialog's shared `leave` (close and the LED representation switch).
find: " if (edit && dirty && !await this.host._confirmDanger({",
replace: " if ((!edit || !dirty) && !await this.host._confirmDanger({",
}],
},
{