room settings button: room-centred, icon-sized, zooms with the plan

Owner's spec: the button is no longer glued to the room NAME (which the user
can drag anywhere) — it anchors to the geometric centre of the ROOM
(interiorPoint for polygons, so an L-shaped room gets a point actually
inside it), one button-height below centre so it never covers the name,
whose default position is that same centre. Height is 70% of a device icon
box, and since --icon-size already rescales with the view, the button zooms
with the plan instead of keeping a constant screen size (verified: x2.2 zoom
-> x2.20 button). The small metric rows under the room name (temperature,
humidity, signal, lights) now render in the plan editor too — they used to
be view-mode only.

smoke_room_cards updated: plainInPlan now asserts metrics ARE present in the
editor (the old assertion pinned the old behaviour), plus gearDetached.
This commit is contained in:
Matysh
2026-07-29 13:39:46 +03:00
parent 2551b4ea0e
commit 09f4dc4115
6 changed files with 103 additions and 51 deletions
+8 -2
View File
@@ -36,8 +36,14 @@ const res = await page.evaluate(async () => {
c._setMode('plan'); await c.updateComplete; c._setMode('plan'); await c.updateComplete;
const planLbl = sr().querySelector('.roomlabel'); const planLbl = sr().querySelector('.roomlabel');
out.handlesInPlan = planLbl?.querySelectorAll('.rlhandle').length === 4; out.handlesInPlan = planLbl?.querySelectorAll('.rlhandle').length === 4;
// метрики скрыты в редакторе плана (карточка = имя, чтобы не мешать разметке) // метрики видны и в редакторе плана (решение владельца, 2026-07-29)
out.plainInPlan = sr().querySelectorAll('.roomlabel.card').length === 0; out.plainInPlan = sr().querySelectorAll('.roomlabel.card').length > 0;
// кнопка настроек — отдельно от подписи, в центре комнаты, с размером от иконки
out.gearDetached = (() => {
const g = sr().querySelector('.rlgearbtn');
if (!g || g.closest('.roomlabel')) return false;
return getComputedStyle(g).height !== 'auto';
})();
// масштаб: сымитируем resize через прямой вызов // масштаб: сымитируем resize через прямой вызов
const room = c._spaceModel().rooms.find((r) => r.name); const room = c._spaceModel().rooms.find((r) => r.name);
c._rlResize = { id: 'rl_' + room.id, space: spId, k0: 1, cx: 100, cy: 100, d0: 50 }; c._rlResize = { id: 'rl_' + room.id, space: spId, k0: 1, cx: 100, cy: 100, d0: 50 };
File diff suppressed because one or more lines are too long
+25 -16
View File
File diff suppressed because one or more lines are too long
+5
View File
@@ -239,6 +239,11 @@ Run the *core flows* (marked ★ below) in each environment at least once per mi
on the plan after a reload. Same for each tap action and each fill mode on the plan after a reload. Same for each tap action and each fill mode
[auto: backend test_every_display_mode_the_editor_offers_is_accepted and [auto: backend test_every_display_mode_the_editor_offers_is_accepted and
neighbours, test_a_marker_showing_its_value_can_be_saved] neighbours, test_a_marker_showing_its_value_can_be_saved]
- [ ] Room settings button (dev): detached from the (movable) name label —
always at the room's geometric centre, one button-height below it; sized
at 70% of a device icon and zooming WITH the plan; the small metric rows
under the room name now show in the plan editor too
[auto: smoke_room_cards gearDetached/plainInPlan]
- [ ] Hide-from-plan flag (dev, docs/FILTERING.md): every device dialog has - [ ] Hide-from-plan flag (dev, docs/FILTERING.md): every device dialog has
the checkbox, incl. virtual; hidden devices vanish from every mode and the checkbox, incl. virtual; hidden devices vanish from every mode and
the count, still count toward room LQI, cast no glow/light fill; the the count, still count toward room LQI, cast no glow/light fill; the
+25 -10
View File
@@ -15,7 +15,7 @@ import {
lqiColor, snapToGrid, samePoint, pointInPolygon, markerIdForBinding, lqiColor, snapToGrid, samePoint, pointInPolygon, markerIdForBinding,
segmentCm, formatLength, roomEdges, roomPoly, pointStrictlyInside, roomsOverlap, segmentCm, formatLength, roomEdges, roomPoly, pointStrictlyInside, roomsOverlap,
pointOnBoundary, mergeRooms, splitRoomPath, polygonArea, closestPointOnBoundary, pointStrictlyInside as ptInside, islandsOf, sharedBoundary, openZoneOf, distToSegment, outlineWithout, cutSegments, alignGuides, segmentAngle, is45, type AlignGuide, swipeTarget, clampScale, migratePdfUrls, roomFillModeOf, contentUrl, pointOnBoundary, mergeRooms, splitRoomPath, polygonArea, closestPointOnBoundary, pointStrictlyInside as ptInside, islandsOf, sharedBoundary, openZoneOf, distToSegment, outlineWithout, cutSegments, alignGuides, segmentAngle, is45, type AlignGuide, swipeTarget, clampScale, migratePdfUrls, roomFillModeOf, contentUrl,
snapToWall, openingAmount, snapToWall, openingAmount, interiorPoint,
averageLqi, fitView, declump, safeUrl, resolveTapAction, floorsOf, type FloorInfo, averageLqi, fitView, declump, safeUrl, resolveTapAction, floorsOf, type FloorInfo,
stateIcon, lightColorOf, isAlarmState, parseRoomRef, diffNewDevices, glowColorOf, doorSector, hasRoomBehind, controlsAction, isControllable, stateIcon, lightColorOf, isAlarmState, parseRoomRef, diffNewDevices, glowColorOf, doorSector, hasRoomBehind, controlsAction, isControllable,
spaceDisplayOf, roomFillStyle, fillColorsOf, DEFAULT_FILL_COLORS, type FillColors, spaceDisplayOf, roomFillStyle, fillColorsOf, DEFAULT_FILL_COLORS, type FillColors,
@@ -4157,6 +4157,7 @@ class HouseplanCard extends LitElement {
${disp.showNames || this._markup ${disp.showNames || this._markup
? space.rooms.map((r) => this._renderRoomLabel(r, space, view, disp)) ? space.rooms.map((r) => this._renderRoomLabel(r, space, view, disp))
: nothing} : nothing}
${this._markup ? space.rooms.map((r) => this._renderRoomGear(r, space, view)) : nothing}
</div> </div>
${this._measureAnchor ${this._measureAnchor
? html`<div class="measurelayer">${this._renderMeasureLabel(view)}</div>` ? html`<div class="measurelayer">${this._renderMeasureLabel(view)}</div>`
@@ -4501,6 +4502,28 @@ class HouseplanCard extends LitElement {
this._persistLayout(); this._persistLayout();
} }
/** The room-settings button: detached from the (movable) label, always at
* the geometric CENTRE of the room, sized at 70% of a device icon and
* therefore zooming with the plan (owner's spec, 2026-07-29). */
private _renderRoomGear(
r: RoomCfg, space: SpaceModel, view: { x: number; y: number; w: number; h: number },
): TemplateResult | typeof nothing {
if (!r.id) return nothing;
const c = r.poly
? (interiorPoint(r.poly) || [r.poly[0][0], r.poly[0][1]])
: r.x != null && r.y != null ? [r.x + (r.w || 0) / 2, r.y + (r.h || 0) / 2] : null;
if (!c) return nothing;
const left = ((c[0] - view.x) / view.w) * 100;
const top = ((c[1] - view.y) / view.h) * 100;
return html`<button class="rlgearbtn" style="left:${left}%;top:${top}%"
title=${this._t('room.settings_title')}
@pointerdown=${(e: Event) => e.stopPropagation()}
@click=${(e: Event) => { e.stopPropagation(); this._openRoomEdit(r); }}>
<ha-icon icon="mdi:cog-outline"></ha-icon>
<span class="rlgeartext">${this._t('room.settings_short')}</span>
</button>`;
}
private _renderRoomLabel( private _renderRoomLabel(
r: RoomCfg, space: SpaceModel, view: { x: number; y: number; w: number; h: number }, disp: SpaceDisplay, r: RoomCfg, space: SpaceModel, view: { x: number; y: number; w: number; h: number }, disp: SpaceDisplay,
): TemplateResult | typeof nothing { ): TemplateResult | typeof nothing {
@@ -4514,7 +4537,7 @@ class HouseplanCard extends LitElement {
const k = this._labelScale(r); const k = this._labelScale(r);
// optional metrics row (needs an HA area; sub-area rooms show the name only) // optional metrics row (needs an HA area; sub-area rooms show the name only)
const rows: TemplateResult[] = []; const rows: TemplateResult[] = [];
if ((r.area || r.settings?.temp_source || r.settings?.hum_source) && !this._markup) { if (r.area || r.settings?.temp_source || r.settings?.hum_source) {
if (disp.labelTemp) { if (disp.labelTemp) {
const t = this._roomTemp(r); const t = this._roomTemp(r);
if (t != null) rows.push(html`<span class="rlm"><ha-icon icon="mdi:thermometer"></ha-icon>${t}°</span>`); if (t != null) rows.push(html`<span class="rlm"><ha-icon icon="mdi:thermometer"></ha-icon>${t}°</span>`);
@@ -4552,14 +4575,6 @@ class HouseplanCard extends LitElement {
@pointerdown=${(e: Event) => e.stopPropagation()}></ha-icon>` @pointerdown=${(e: Event) => e.stopPropagation()}></ha-icon>`
: nothing}</span> : nothing}</span>
${rows.length ? html`<span class="rlmetrics">${rows}</span>` : nothing} ${rows.length ? html`<span class="rlmetrics">${rows}</span>` : nothing}
${this._markup && r.id
? html`<button class="rlgearbtn" title=${this._t('room.settings_title')}
@pointerdown=${(e: Event) => e.stopPropagation()}
@click=${(e: Event) => { e.stopPropagation(); this._openRoomEdit(r); }}>
<ha-icon icon="mdi:cog-outline"></ha-icon>
<span class="rlgeartext">${this._t('room.settings_short')}</span>
</button>`
: nothing}
${this._mode === 'plan' ${this._mode === 'plan'
? ['tl', 'tr', 'bl', 'br'].map( ? ['tl', 'tr', 'bl', 'br'].map(
(c) => html`<span class="rlhandle ${c}" (c) => html`<span class="rlhandle ${c}"
+15 -7
View File
@@ -403,31 +403,39 @@ export const cardStyles = css`
and the metrics hang below as absolutes the name renders in exactly and the metrics hang below as absolutes the name renders in exactly
the same place in view mode and in the plan editor (owner's request), the same place in view mode and in the plan editor (owner's request),
and the button sits at the very bottom of the card. */ and the button sits at the very bottom of the card. */
/* Standalone, centred on the room, sized from the device icon: icon-size
already rescales with the view, so the button zooms WITH the plan
instead of keeping a constant screen size (owner's spec). */
.rlgearbtn { .rlgearbtn {
--gear-h: calc(var(--icon-size, 2.5cqw) * 0.7 * 2.2); /* 70% of the icon BOX (icon + padding) */
position: absolute; position: absolute;
top: calc(100% + 0.35em); /* anchored to the room centre, sitting one button-height below it: the
left: 50%; room NAME defaults to the same centre, and dead-centred the button
transform: translateX(-50%); covered it. The offset is in button units, so it zooms along. */
transform: translate(-50%, calc(-50% + var(--gear-h) * 0.95));
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
gap: 4px; gap: 0.35em;
padding: 3px 8px; height: var(--gear-h);
padding: 0 calc(var(--gear-h) * 0.38);
border: 0; border: 0;
border-radius: 999px; border-radius: 999px;
background: var(--hp-accent); background: var(--hp-accent);
color: var(--text-primary-color, #fff); color: var(--text-primary-color, #fff);
font: inherit; font: inherit;
font-size: 11px; font-size: calc(var(--gear-h) * 0.42);
font-weight: 600; font-weight: 600;
line-height: 1; line-height: 1;
white-space: nowrap;
cursor: pointer; cursor: pointer;
pointer-events: auto; pointer-events: auto;
opacity: 0.92; opacity: 0.92;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.35); box-shadow: 0 1px 4px rgba(0, 0, 0, 0.35);
z-index: 2;
} }
.rlgearbtn { transition: opacity 0.15s, filter 0.15s; } .rlgearbtn { transition: opacity 0.15s, filter 0.15s; }
.rlgearbtn:hover { opacity: 1; filter: brightness(1.18); } .rlgearbtn:hover { opacity: 1; filter: brightness(1.18); }
.rlgearbtn ha-icon { --mdc-icon-size: 14px; display: inline-flex; } .rlgearbtn ha-icon { --mdc-icon-size: calc(var(--gear-h) * 0.55); display: inline-flex; }
.rlgear { .rlgear {
--mdc-icon-size: 0.9em; --mdc-icon-size: 0.9em;
display: inline-flex; display: inline-flex;