Skip to content

Commit acc4227

Browse files
squizzer73claude
andcommitted
v0.2.6 — auto-trim empty grid space
Canvas now sizes to the tight bounding box of placed rooms rather than the full floor cols×rows. Eliminates dead space when rooms don't span the full grid. Thermal heatmap viewBox and canvas ResizeObserver both follow the same bounding box. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent daa65ae commit acc4227

3 files changed

Lines changed: 52 additions & 18 deletions

File tree

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
# Changelog
22

3+
## [0.2.6] — 2026-05-13
4+
5+
### Fixed
6+
- **Auto-trim empty space** — the flat grid canvas now sizes itself to the tight bounding box of the rooms actually placed on the floor, eliminating dead space above/below/beside rooms. Rooms placed mid-grid no longer leave large empty areas. The heatmap thermal layer and canvas sizing follow the same bounding box automatically.
7+
8+
---
9+
310
## [0.2.5] — 2026-05-13
411

512
### Added

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "lovelace-house-card",
33
"type": "module",
4-
"version": "0.2.5",
4+
"version": "0.2.6",
55
"description": "Visual house floorplan card for Home Assistant",
66
"main": "dist/house-card.js",
77
"scripts": {

src/house-card.js

Lines changed: 44 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -501,6 +501,23 @@ class HouseCard extends LitElement {
501501
this._sizeGridCanvas();
502502
}
503503

504+
/**
505+
* Returns the tight bounding box of all rooms on a floor, in grid cells.
506+
* Falls back to the full floor dimensions if there are no rooms.
507+
* { minCol, minRow, usedCols, usedRows }
508+
*/
509+
_floorBounds(floor) {
510+
const rooms = floor?.rooms || [];
511+
if (!rooms.length) {
512+
return { minCol: 0, minRow: 0, usedCols: floor?.cols || 1, usedRows: floor?.rows || 1 };
513+
}
514+
const minCol = Math.min(...rooms.map(r => r.col));
515+
const minRow = Math.min(...rooms.map(r => r.row));
516+
const maxCol = Math.max(...rooms.map(r => r.col + r.width));
517+
const maxRow = Math.max(...rooms.map(r => r.row + r.height));
518+
return { minCol, minRow, usedCols: maxCol - minCol, usedRows: maxRow - minRow };
519+
}
520+
504521
_sizeGridCanvas() {
505522
const wrapper = this.shadowRoot?.querySelector('.grid-wrapper');
506523
const canvas = this.shadowRoot?.querySelector('.grid-canvas');
@@ -511,12 +528,13 @@ class HouseCard extends LitElement {
511528
const wW = wrapper.clientWidth;
512529
const wH = wrapper.clientHeight;
513530
if (!wW || !wH) return;
531+
// Use trimmed bounding box for sizing so empty rows/cols don't waste space
532+
const { usedCols, usedRows } = this._floorBounds(floor);
514533
// Visual height after rotateX(38deg) = DOM_height × cos(38°)
515-
// DOM_height = canvas_width × rows/cols
534+
// DOM_height = canvas_width × usedRows/usedCols
516535
// Constrain so visual height ≤ wrapper height:
517-
// canvas_width ≤ wH × cols / (rows × cos(38°))
518-
const cos38 = Math.cos(38 * Math.PI / 180); // ≈ 0.788
519-
const maxW = Math.floor(Math.min(wW, wH * floor.cols / (floor.rows * cos38)));
536+
const cos38 = Math.cos(38 * Math.PI / 180); // ≈ 0.788
537+
const maxW = Math.floor(Math.min(wW, wH * usedCols / (usedRows * cos38)));
520538
canvas.style.width = maxW + 'px';
521539
}
522540

@@ -774,7 +792,7 @@ class HouseCard extends LitElement {
774792
return out;
775793
}
776794

777-
_renderThermalLayer(floor) {
795+
_renderThermalLayer(floor, minCol = 0, minRow = 0, usedCols = null, usedRows = null) {
778796
const mode = this._getHeatmapMode();
779797
if (mode === 'off') return '';
780798

@@ -783,17 +801,23 @@ class HouseCard extends LitElement {
783801
const range = floor.temperature_range || this._config.temperature_range || [16, 26];
784802
const humFloor = this._config.humidity_floor ?? 50;
785803

804+
const vbCols = usedCols ?? floor.cols;
805+
const vbRows = usedRows ?? floor.rows;
806+
786807
const enriched = (floor.rooms || [])
787808
.filter(r => r.heatmap !== false)
788809
.map(r => {
789810
const t = parseFloat(this._getEntityState(r.entities?.temperature)?.state);
790811
const h = parseFloat(this._getEntityState(r.entities?.humidity)?.state);
791-
return { ...r, _temp: isNaN(t) ? null : t, _hum: isNaN(h) ? null : h };
812+
// Offset room coordinates to the trimmed origin
813+
return { ...r,
814+
col: r.col - minCol, row: r.row - minRow,
815+
_temp: isNaN(t) ? null : t, _hum: isNaN(h) ? null : h };
792816
});
793817

794818
return svg`
795819
<svg class="thermal-svg"
796-
viewBox="0 0 ${floor.cols * 100} ${floor.rows * 100}"
820+
viewBox="0 0 ${vbCols * 100} ${vbRows * 100}"
797821
preserveAspectRatio="none"
798822
xmlns="http://www.w3.org/2000/svg">
799823
@@ -1180,25 +1204,28 @@ class HouseCard extends LitElement {
11801204
if (!floor || !floor.cols || !floor.rows) {
11811205
return html`<div class="no-floor">Floor not configured</div>`;
11821206
}
1183-
const mode = this._getHeatmapMode();
1184-
const rooms = floor.rooms || [];
1185-
const cellWPct = 100 / floor.cols;
1186-
const cellHPct = 100 / floor.rows;
1207+
const mode = this._getHeatmapMode();
1208+
const rooms = floor.rooms || [];
11871209
const sunStyle = this._getSunStyle();
11881210

1211+
// Trim canvas to the tight bounding box of actual rooms
1212+
const { minCol, minRow, usedCols, usedRows } = this._floorBounds(floor);
1213+
const cellWPct = 100 / usedCols;
1214+
const cellHPct = 100 / usedRows;
1215+
11891216
return html`
11901217
<div class="grid-wrapper">
11911218
<div class="grid-canvas"
1192-
style="aspect-ratio:${floor.cols}/${floor.rows};${sunStyle}"
1219+
style="aspect-ratio:${usedCols}/${usedRows};${sunStyle}"
11931220
data-heatmap="${mode}">
1194-
${this._renderThermalLayer(floor)}
1195-
${rooms.map(room => this._renderRoom(room, cellWPct, cellHPct))}
1221+
${this._renderThermalLayer(floor, minCol, minRow, usedCols, usedRows)}
1222+
${rooms.map(room => this._renderRoom(room, cellWPct, cellHPct, minCol, minRow))}
11961223
</div>
11971224
</div>
11981225
`;
11991226
}
12001227

1201-
_renderRoom(room, cellWPct, cellHPct) {
1228+
_renderRoom(room, cellWPct, cellHPct, minCol = 0, minRow = 0) {
12021229
const lightOn = room.entities?.light ? this._isLightOn(room.entities.light) : false;
12031230
const occupied = room.entities?.occupancy ? this._isOccupied(room.entities.occupancy) : false;
12041231
const temp = room.entities?.temperature ? this._getTemperature(room.entities.temperature) : null;
@@ -1208,8 +1235,8 @@ class HouseCard extends LitElement {
12081235
const detections = this._getDetections(room.entities?.detections);
12091236

12101237
const gap = 0.6;
1211-
const left = `${room.col * cellWPct + gap}%`;
1212-
const top = `${room.row * cellHPct + gap}%`;
1238+
const left = `${(room.col - minCol) * cellWPct + gap}%`;
1239+
const top = `${(room.row - minRow) * cellHPct + gap}%`;
12131240
const width = `${room.width * cellWPct - gap * 2}%`;
12141241
const height = `${room.height * cellHPct - gap * 2}%`;
12151242

0 commit comments

Comments
 (0)