mirror of
https://github.com/Matysh/houseplan-card
synced 2026-07-31 16:38:31 +00:00
feat v1.8.1: fit-to-viewport (весь план виден по ширине и высоте) + сохранение зума по пространствам в localStorage
This commit is contained in:
@@ -10,7 +10,7 @@ PLANS_DIR = "houseplan/plans" # относительно каталога ко
|
|||||||
FILES_URL = "/houseplan_files/files"
|
FILES_URL = "/houseplan_files/files"
|
||||||
FILES_DIR = "houseplan/files"
|
FILES_DIR = "houseplan/files"
|
||||||
CONF_ADMIN_ONLY = "admin_only"
|
CONF_ADMIN_ONLY = "admin_only"
|
||||||
VERSION = "1.8.0"
|
VERSION = "1.8.1"
|
||||||
|
|
||||||
DEFAULT_CONFIG: dict = {
|
DEFAULT_CONFIG: dict = {
|
||||||
"spaces": [],
|
"spaces": [],
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -15,5 +15,5 @@
|
|||||||
"iot_class": "local_push",
|
"iot_class": "local_push",
|
||||||
"issue_tracker": "https://github.com/justbusiness/houseplan-card/issues",
|
"issue_tracker": "https://github.com/justbusiness/houseplan-card/issues",
|
||||||
"requirements": [],
|
"requirements": [],
|
||||||
"version": "1.8.0"
|
"version": "1.8.1"
|
||||||
}
|
}
|
||||||
Vendored
+17
-16
File diff suppressed because one or more lines are too long
@@ -1,5 +1,17 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## v1.8.1 — 2026-07-04 (fit-to-viewport + сохранение зума по пространствам)
|
||||||
|
- Уменьшение масштаба до полного вида: нижний предел зума = 100% совпадает с полностью
|
||||||
|
видимым планом. `.stage` теперь ограничена по высоте вьюпорта
|
||||||
|
(`width:min(100%, calc((100dvh − 118px) × aspect))`, `max-width:100%`, центрирование),
|
||||||
|
поэтому при сбросе/зум-ауте вся схема видна и по ширине, и по высоте без обрезки.
|
||||||
|
- Масштаб запоминается для каждого пространства отдельно и локально на устройстве
|
||||||
|
(`localStorage['houseplan_card_zoom_v1']`, `_zoomBySpace`): переключение этажей
|
||||||
|
восстанавливает свой зум; при возврате пан центрируется и ограничивается.
|
||||||
|
- Проверено на живом HA: f1→196%, f2→140%, двор нетронут — после переключений
|
||||||
|
значения восстанавливаются; сброс даёт 100% и полный план (874≤992 px по высоте).
|
||||||
|
|
||||||
|
|
||||||
## v1.8.0 — 2026-07-04 (зумирование плана)
|
## v1.8.0 — 2026-07-04 (зумирование плана)
|
||||||
- Зум и панорама схемы: колёсико мыши (к курсору), щипок двумя пальцами (pinch) на тач-экране,
|
- Зум и панорама схемы: колёсико мыши (к курсору), щипок двумя пальцами (pinch) на тач-экране,
|
||||||
перетаскивание фона одним пальцем при увеличении, кнопки −/сброс/+ в тулбаре, бейдж масштаба.
|
перетаскивание фона одним пальцем при увеличении, кнопки −/сброс/+ в тулбаре, бейдж масштаба.
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "houseplan-card",
|
"name": "houseplan-card",
|
||||||
"version": "1.8.0",
|
"version": "1.8.1",
|
||||||
"description": "Interactive house plan Lovelace card for Home Assistant (dacha Kirillovskoe)",
|
"description": "Interactive house plan Lovelace card for Home Assistant (dacha Kirillovskoe)",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
+46
-4
@@ -13,8 +13,9 @@ import {
|
|||||||
} from './logic';
|
} from './logic';
|
||||||
import './editor';
|
import './editor';
|
||||||
|
|
||||||
const CARD_VERSION = '1.8.0';
|
const CARD_VERSION = '1.8.1';
|
||||||
const LS_KEY = 'houseplan_card_layout_v1';
|
const LS_KEY = 'houseplan_card_layout_v1';
|
||||||
|
const LS_ZOOM = 'houseplan_card_zoom_v1';
|
||||||
const NORM_W = 1000; // ширина рендер-пространства для нормированных конфигов
|
const NORM_W = 1000; // ширина рендер-пространства для нормированных конфигов
|
||||||
|
|
||||||
interface RoomCfg {
|
interface RoomCfg {
|
||||||
@@ -144,9 +145,10 @@ class HouseplanCard extends LitElement {
|
|||||||
private _areaSel = '';
|
private _areaSel = '';
|
||||||
private _nameSel = '';
|
private _nameSel = '';
|
||||||
private _roomDialog = false;
|
private _roomDialog = false;
|
||||||
// зум/панорама плана
|
// зум/панорама плана (зум сохраняется по пространству локально)
|
||||||
private _zoom = 1;
|
private _zoom = 1;
|
||||||
private _pan = { x: 0, y: 0 };
|
private _pan = { x: 0, y: 0 };
|
||||||
|
private _zoomBySpace: Record<string, number> = {};
|
||||||
private _pointers = new Map<number, { x: number; y: number }>();
|
private _pointers = new Map<number, { x: number; y: number }>();
|
||||||
private _panStart: { sx: number; sy: number; ox: number; oy: number } | null = null;
|
private _panStart: { sx: number; sy: number; ox: number; oy: number } | null = null;
|
||||||
private _pinchStart: { dist: number; zoom: number; cx: number; cy: number; px: number; py: number } | null = null;
|
private _pinchStart: { dist: number; zoom: number; cx: number; cy: number; px: number; py: number } | null = null;
|
||||||
@@ -273,6 +275,11 @@ class HouseplanCard extends LitElement {
|
|||||||
public setConfig(config: CardConfig): void {
|
public setConfig(config: CardConfig): void {
|
||||||
this._config = { icon_size: 2.5, show_temperature: true, live_states: true, show_signal: true, ...config };
|
this._config = { icon_size: 2.5, show_temperature: true, live_states: true, show_signal: true, ...config };
|
||||||
if (config.default_floor) this._space = config.default_floor;
|
if (config.default_floor) this._space = config.default_floor;
|
||||||
|
try {
|
||||||
|
this._zoomBySpace = JSON.parse(localStorage.getItem(LS_ZOOM) || '{}') || {};
|
||||||
|
} catch {
|
||||||
|
this._zoomBySpace = {};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public getCardSize(): number {
|
public getCardSize(): number {
|
||||||
@@ -365,6 +372,7 @@ class HouseplanCard extends LitElement {
|
|||||||
if (this._norm && !this._model.find((s) => s.id === this._space)) {
|
if (this._norm && !this._model.find((s) => s.id === this._space)) {
|
||||||
this._space = this._model[0]?.id || this._space;
|
this._space = this._model[0]?.id || this._space;
|
||||||
}
|
}
|
||||||
|
this._restoreZoom();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// не последняя попытка — молча ждём следующего обновления hass (прогрев WS)
|
// не последняя попытка — молча ждём следующего обновления hass (прогрев WS)
|
||||||
if (this._loadTries >= 8) {
|
if (this._loadTries >= 8) {
|
||||||
@@ -866,17 +874,48 @@ class HouseplanCard extends LitElement {
|
|||||||
const r = stage.getBoundingClientRect();
|
const r = stage.getBoundingClientRect();
|
||||||
const factor = ev.deltaY < 0 ? 1.15 : 1 / 1.15;
|
const factor = ev.deltaY < 0 ? 1.15 : 1 / 1.15;
|
||||||
this._zoomAt(ev.clientX - r.left, ev.clientY - r.top, this._zoom * factor);
|
this._zoomAt(ev.clientX - r.left, ev.clientY - r.top, this._zoom * factor);
|
||||||
|
this._saveZoom();
|
||||||
}
|
}
|
||||||
|
|
||||||
private _stepZoom(delta: number): void {
|
private _stepZoom(delta: number): void {
|
||||||
const stage = this._stageEl;
|
const stage = this._stageEl;
|
||||||
if (!stage) return;
|
if (!stage) return;
|
||||||
this._zoomAt(stage.clientWidth / 2, stage.clientHeight / 2, this._zoom * (delta > 0 ? 1.4 : 1 / 1.4));
|
this._zoomAt(stage.clientWidth / 2, stage.clientHeight / 2, this._zoom * (delta > 0 ? 1.4 : 1 / 1.4));
|
||||||
|
this._saveZoom();
|
||||||
}
|
}
|
||||||
|
|
||||||
private _resetZoom(): void {
|
private _resetZoom(): void {
|
||||||
this._zoom = 1;
|
this._zoom = 1;
|
||||||
this._pan = { x: 0, y: 0 };
|
this._pan = { x: 0, y: 0 };
|
||||||
|
this._saveZoom();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Сохранить текущий зум пространства в localStorage. */
|
||||||
|
private _saveZoom(): void {
|
||||||
|
this._zoomBySpace = { ...this._zoomBySpace, [this._space]: this._zoom };
|
||||||
|
try {
|
||||||
|
localStorage.setItem(LS_ZOOM, JSON.stringify(this._zoomBySpace));
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Восстановить сохранённый зум пространства и центрировать. */
|
||||||
|
private _restoreZoom(): void {
|
||||||
|
this._zoom = this._zoomBySpace[this._space] || 1;
|
||||||
|
this._pan = { x: 0, y: 0 };
|
||||||
|
// центрировать после применения размеров
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
const stage = this._stageEl;
|
||||||
|
if (stage) {
|
||||||
|
this._pan = {
|
||||||
|
x: (stage.clientWidth * (1 - this._zoom)) / 2,
|
||||||
|
y: (stage.clientHeight * (1 - this._zoom)) / 2,
|
||||||
|
};
|
||||||
|
this._clampPan();
|
||||||
|
this.requestUpdate();
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private _stagePointerDown(ev: PointerEvent): void {
|
private _stagePointerDown(ev: PointerEvent): void {
|
||||||
@@ -921,6 +960,7 @@ class HouseplanCard extends LitElement {
|
|||||||
this._zoom = z;
|
this._zoom = z;
|
||||||
this._clampPan();
|
this._clampPan();
|
||||||
this._suppressClick = true;
|
this._suppressClick = true;
|
||||||
|
this._saveZoom();
|
||||||
} else if (this._panStart) {
|
} else if (this._panStart) {
|
||||||
const ddx = ev.clientX - this._panStart.sx;
|
const ddx = ev.clientX - this._panStart.sx;
|
||||||
const ddy = ev.clientY - this._panStart.sy;
|
const ddy = ev.clientY - this._panStart.sy;
|
||||||
@@ -1653,6 +1693,7 @@ class HouseplanCard extends LitElement {
|
|||||||
@click=${() => {
|
@click=${() => {
|
||||||
this._space = s.id;
|
this._space = s.id;
|
||||||
this._selId = null;
|
this._selId = null;
|
||||||
|
this._restoreZoom();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
${s.title}${this._norm
|
${s.title}${this._norm
|
||||||
@@ -1703,7 +1744,7 @@ class HouseplanCard extends LitElement {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="stage ${this._edit ? 'edit' : ''} ${this._markup ? 'markup' : ''}"
|
<div class="stage ${this._edit ? 'edit' : ''} ${this._markup ? 'markup' : ''}"
|
||||||
style="aspect-ratio:${vb[2]}/${vb[3]}"
|
style="aspect-ratio:${vb[2]}/${vb[3]};width:min(100%,calc((100dvh - 118px) * ${(vb[2] / vb[3]).toFixed(4)}))"
|
||||||
@click=${(e: MouseEvent) => this._markupClick(e)}
|
@click=${(e: MouseEvent) => this._markupClick(e)}
|
||||||
@wheel=${(e: WheelEvent) => this._onWheel(e)}
|
@wheel=${(e: WheelEvent) => this._onWheel(e)}
|
||||||
@pointerdown=${(e: PointerEvent) => this._stagePointerDown(e)}
|
@pointerdown=${(e: PointerEvent) => this._stagePointerDown(e)}
|
||||||
@@ -2231,7 +2272,8 @@ class HouseplanCard extends LitElement {
|
|||||||
}
|
}
|
||||||
.stage {
|
.stage {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
max-width: 100%;
|
||||||
|
margin: 0 auto; /* центрировать, когда ширина ограничена высотой экрана */
|
||||||
container-type: inline-size;
|
container-type: inline-size;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
touch-action: none; /* свои жесты pinch/pan */
|
touch-action: none; /* свои жесты pinch/pan */
|
||||||
|
|||||||
Reference in New Issue
Block a user