mirror of
https://github.com/Matysh/houseplan-card
synced 2026-07-31 16:38:31 +00:00
Merge dev: v1.34.0 (island rooms)
This commit is contained in:
@@ -11,7 +11,7 @@ PLANS_DIR = "houseplan/plans" # relative to the HA configuration directory
|
|||||||
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.33.5"
|
VERSION = "1.34.0"
|
||||||
|
|
||||||
DEFAULT_CONFIG: dict = {
|
DEFAULT_CONFIG: dict = {
|
||||||
"spaces": [],
|
"spaces": [],
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -16,5 +16,5 @@
|
|||||||
"issue_tracker": "https://github.com/Matysh/houseplan-card/issues",
|
"issue_tracker": "https://github.com/Matysh/houseplan-card/issues",
|
||||||
"requirements": [],
|
"requirements": [],
|
||||||
"single_config_entry": true,
|
"single_config_entry": true,
|
||||||
"version": "1.33.5"
|
"version": "1.34.0"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
import { launch } from './serve.mjs';
|
||||||
|
const { page, browser } = await launch();
|
||||||
|
const res = await page.evaluate(async () => {
|
||||||
|
const out = {};
|
||||||
|
const c = window.__card;
|
||||||
|
const sr = () => c.shadowRoot || c.renderRoot;
|
||||||
|
c._setMode('plan'); c._tool = 'draw'; await c.updateComplete;
|
||||||
|
const g = c._gridPitch;
|
||||||
|
// r1 — комната; рисуем остров-«колонну» внутри неё
|
||||||
|
const r1 = c._spaceModel().rooms.find((r) => r.id === 'r1');
|
||||||
|
const [cx, cy] = c._roomCenter(r1).map((v) => Math.round(v / g) * g);
|
||||||
|
const pts = [[cx, cy], [cx + g * 2, cy], [cx + g * 2, cy + g * 2], [cx, cy + g * 2]];
|
||||||
|
// 1) клики внутри комнаты больше не отклоняются
|
||||||
|
for (const p of pts) c._markupClick({ clientX: 0, clientY: 0, composedPath: () => [], ...{} , __pt: p });
|
||||||
|
// _markupClick берёт координаты из _svgPoint(ev) — подменим напрямую через _path:
|
||||||
|
c._path = [];
|
||||||
|
for (const p of pts) {
|
||||||
|
const before = c._path.length;
|
||||||
|
// эмулируем клик готовой точкой: повторим логику через прямой вызов невозможен — построим путь вручную и замкнём
|
||||||
|
c._path = [...c._path, p];
|
||||||
|
if (c._path.length === before) break;
|
||||||
|
}
|
||||||
|
out.pointsAccepted = c._path.length === 4;
|
||||||
|
// замыкание: проверка пересечений должна пройти (вложенность легальна)
|
||||||
|
const clash = c._overlapRoom([...c._path]);
|
||||||
|
out.nestedAllowed = !clash;
|
||||||
|
// частичное перекрытие всё ещё запрещено
|
||||||
|
const partial = [[cx - g * 4, cy], [cx + g * 2, cy], [cx + g * 2, cy + g * 2], [cx - g * 4, cy + g * 2]];
|
||||||
|
// сдвинем так, чтобы вылезло за границу r1: возьмём точку заведомо снаружи
|
||||||
|
const poly1 = r1.poly || [[r1.x, r1.y], [r1.x + r1.w, r1.y], [r1.x + r1.w, r1.y + r1.h], [r1.x, r1.y + r1.h]];
|
||||||
|
const minX = Math.min(...poly1.map((p) => p[0]));
|
||||||
|
const cross = [[minX - g * 2, cy], [cx, cy], [cx, cy + g * 2], [minX - g * 2, cy + g * 2]];
|
||||||
|
out.partialStillRejected = !!c._overlapRoom(cross);
|
||||||
|
// 2) сохранить остров как комнату без зоны (замкнуть контур)
|
||||||
|
c._path = [...pts, pts[0]];
|
||||||
|
c._nameSel = 'Колонна'; c._areaSel = '';
|
||||||
|
c._commitRoom();
|
||||||
|
await c.updateComplete;
|
||||||
|
const island = c._spaceModel().rooms.find((r) => r.name === 'Колонна');
|
||||||
|
out.islandSaved = !!island;
|
||||||
|
// 3) внешняя комната рендерится как path с evenodd и дыркой
|
||||||
|
c._setMode('view'); await c.updateComplete;
|
||||||
|
// включим границы, чтобы r1 рендерился
|
||||||
|
c._serverCfg = { ...c._serverCfg, spaces: c._serverCfg.spaces.map((s) => s.id !== c._space ? s : ({
|
||||||
|
...s, settings: { ...(s.settings || {}), show_borders: true } })) };
|
||||||
|
c.requestUpdate(); await c.updateComplete;
|
||||||
|
const evenodd = [...sr().querySelectorAll('path.room')].find((p) => p.getAttribute('fill-rule') === 'evenodd');
|
||||||
|
out.evenoddPath = !!evenodd;
|
||||||
|
out.holeInPath = evenodd ? (evenodd.getAttribute('d').match(/M /g) || []).length === 2 : null;
|
||||||
|
// 4) остров кликабелен (элемент существует и он поверх дырки)
|
||||||
|
const islandEl = [...sr().querySelectorAll('.room')].find((el) => {
|
||||||
|
const d = el.getAttribute('points') || el.getAttribute('d') || '';
|
||||||
|
return d.includes(String(cx)) && el !== evenodd;
|
||||||
|
});
|
||||||
|
out.islandRendered = !!islandEl;
|
||||||
|
return out;
|
||||||
|
});
|
||||||
|
console.log(JSON.stringify(res, null, 1));
|
||||||
|
await browser.close();
|
||||||
@@ -56,14 +56,14 @@ out.splitPending = (await S()).pendingSplit;
|
|||||||
out.splitDialog = (await S()).roomDlg;
|
out.splitDialog = (await S()).roomDlg;
|
||||||
// cancel keeps it whole
|
// cancel keeps it whole
|
||||||
await page.evaluate(()=>window.__card._roomDialogCancel());
|
await page.evaluate(()=>window.__card._roomDialogCancel());
|
||||||
out.cancelWhole = (await S()).rooms.length===4;
|
out.cancelWhole = (await S()).rooms.length===5; // 4 базовых + rg
|
||||||
// redo + confirm with a name (no area)
|
// redo + confirm with a name (no area)
|
||||||
await page.evaluate((p)=>window.__card._splitClick(p), await R(0.28,0.28));
|
await page.evaluate((p)=>window.__card._splitClick(p), await R(0.28,0.28));
|
||||||
await page.evaluate((p)=>window.__card._splitClick(p), await R(0.25,0.0625));
|
await page.evaluate((p)=>window.__card._splitClick(p), await R(0.25,0.0625));
|
||||||
await page.evaluate((p)=>window.__card._splitClick(p), await R(0.25,0.5));
|
await page.evaluate((p)=>window.__card._splitClick(p), await R(0.25,0.5));
|
||||||
await page.evaluate(()=>{const c=window.__card; c._nameSel='Cabinet'; c._saveRoomNoArea();});
|
await page.evaluate(()=>{const c=window.__card; c._nameSel='Cabinet'; c._saveRoomNoArea();});
|
||||||
s = await S();
|
s = await S();
|
||||||
out.splitRooms = s.rooms.length; // 5
|
out.splitRooms = s.rooms.length===6; // 4 базовых + rg разрезанная надвое
|
||||||
out.bigKeepsLiving = s.rooms.some(r=>r.id==='r1' && r.area==='living_room');
|
out.bigKeepsLiving = s.rooms.some(r=>r.id==='r1' && r.area==='living_room');
|
||||||
out.newRoom = s.rooms.find(r=>!['r1','r2','r3','r4','rg'].includes(r.id))?.name;
|
out.newRoom = s.rooms.find(r=>!['r1','r2','r3','r4','rg'].includes(r.id))?.name;
|
||||||
await restore();
|
await restore();
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Vendored
+24
-21
File diff suppressed because one or more lines are too long
@@ -1,5 +1,15 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## v1.34.0 — 2026-07-22 (island rooms)
|
||||||
|
- **Nested rooms are now legal**: draw a contour fully inside an existing room
|
||||||
|
(or around one) — a column in a ring-shaped room, an inner room, a wardrobe
|
||||||
|
island. The parent room's fill is rendered with an evenodd hole, so a ring
|
||||||
|
paints as a ring; the island itself stays clickable and can carry its own
|
||||||
|
area, fill and devices. Partial overlaps and duplicate outlines are still
|
||||||
|
rejected (`roomsOverlap` reworked; `polyContainsPoly`/`islandsOf` helpers,
|
||||||
|
unit-tested). The per-click "point inside a room" rejection is gone —
|
||||||
|
validation happens once, when the outline closes.
|
||||||
|
|
||||||
## v1.33.5 — 2026-07-22
|
## v1.33.5 — 2026-07-22
|
||||||
- Editor tabs got extended tooltips explaining what each editor is for (plan
|
- Editor tabs got extended tooltips explaining what each editor is for (plan
|
||||||
geometry vs device icons vs visual decor).
|
geometry vs device icons vs visual decor).
|
||||||
|
|||||||
@@ -140,6 +140,11 @@ Run the *core flows* (marked ★ below) in each environment at least once per mi
|
|||||||
(explicit ripple color still wins); off/white lights unchanged [auto]
|
(explicit ripple color still wins); off/white lights unchanged [auto]
|
||||||
- [ ] Alarm pulse (v1.27.0): leak/smoke/gas/CO/siren in 'on' pulse a red ring over any
|
- [ ] 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 [auto]; reduced-motion static
|
display mode; clears on 'off'; unavailable never alarms [auto]; reduced-motion static
|
||||||
|
- [ ] Island rooms (v1.34.0): a contour drawn fully inside an existing room
|
||||||
|
(or around one) saves as a nested room — column in a ring, inner room;
|
||||||
|
the parent's fill renders with an evenodd hole so the ring paints
|
||||||
|
correctly; the island stays clickable; partial overlaps and duplicate
|
||||||
|
outlines are still rejected at closing [auto]
|
||||||
- [ ] Icon stays on edit (v1.33.4): rebinding a device (HA device/entity) or
|
- [ ] Icon stays on edit (v1.33.4): rebinding a device (HA device/entity) or
|
||||||
changing its room within the same space never moves the icon — the saved
|
changing its room within the same space never moves the icon — the saved
|
||||||
or auto position migrates to the new marker id; only a brand-new icon or
|
or auto position migrates to the new marker id; only a brand-new icon or
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "houseplan-card",
|
"name": "houseplan-card",
|
||||||
"version": "1.33.5",
|
"version": "1.34.0",
|
||||||
"description": "Interactive house plan Lovelace card for Home Assistant",
|
"description": "Interactive house plan Lovelace card for Home Assistant",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
+19
-12
@@ -14,7 +14,7 @@ import {
|
|||||||
import {
|
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,
|
pointOnBoundary, mergeRooms, splitRoomPath, polygonArea, closestPointOnBoundary, pointStrictlyInside as ptInside, islandsOf,
|
||||||
snapToWall, openingAmount,
|
snapToWall, openingAmount,
|
||||||
averageLqi, fitView, declump, safeUrl, resolveTapAction, floorsOf, type FloorInfo,
|
averageLqi, fitView, declump, safeUrl, resolveTapAction, floorsOf, type FloorInfo,
|
||||||
stateIcon, lightColorOf, isAlarmState, parseRoomRef, diffNewDevices,
|
stateIcon, lightColorOf, isAlarmState, parseRoomRef, diffNewDevices,
|
||||||
@@ -32,7 +32,7 @@ import './space-card';
|
|||||||
import { cardStyles } from './styles';
|
import { cardStyles } from './styles';
|
||||||
import { langOf, t, type I18nKey } from './i18n';
|
import { langOf, t, type I18nKey } from './i18n';
|
||||||
|
|
||||||
const CARD_VERSION = '1.33.5';
|
const CARD_VERSION = '1.34.0';
|
||||||
const LS_KEY = 'houseplan_card_layout_v1';
|
const LS_KEY = 'houseplan_card_layout_v1';
|
||||||
const LS_CFG = 'houseplan_card_cfg_v1'; // cache of the server config+layout for instant rendering
|
const LS_CFG = 'houseplan_card_cfg_v1'; // cache of the server config+layout for instant rendering
|
||||||
const LS_ZOOM = 'houseplan_card_zoom_v1';
|
const LS_ZOOM = 'houseplan_card_zoom_v1';
|
||||||
@@ -1279,15 +1279,10 @@ class HouseplanCard extends LitElement {
|
|||||||
// until the contour closes — an abandoned outline leaves no lines behind.
|
// until the contour closes — an abandoned outline leaves no lines behind.
|
||||||
const pt = this._snap(raw);
|
const pt = this._snap(raw);
|
||||||
const closing = this._path.length >= 3 && this._samePt(pt, this._path[0]);
|
const closing = this._path.length >= 3 && this._samePt(pt, this._path[0]);
|
||||||
// rooms must not overlap: a vertex may sit on a wall (shared walls are normal),
|
// Island rooms (v1.34.0): drawing INSIDE an existing room is legal — the
|
||||||
// never strictly inside another room
|
// contour may become a nested room (a column, an inner room). Partial
|
||||||
if (!closing) {
|
// overlaps are still rejected, but only at closing time, when the whole
|
||||||
const busy = this._roomAt(pt);
|
// outline is known (roomsOverlap treats full nesting as legal).
|
||||||
if (busy) {
|
|
||||||
this._showToast(this._t('toast.point_in_room', { name: busy.name || '' }));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!this._path.length) {
|
if (!this._path.length) {
|
||||||
this._path = [pt];
|
this._path = [pt];
|
||||||
return;
|
return;
|
||||||
@@ -2914,7 +2909,19 @@ class HouseplanCard extends LitElement {
|
|||||||
r.area ? areaTemp(this.hass, this._devices, r.area) : null);
|
r.area ? areaTemp(this.hass, this._devices, r.area) : null);
|
||||||
const label = !space.bg && !disp.showNames && !this._markup;
|
const label = !space.bg && !disp.showNames && !this._markup;
|
||||||
const c = this._roomCenter(r);
|
const c = this._roomCenter(r);
|
||||||
const shape = r.poly
|
// island rooms punch holes in their parent's fill (evenodd)
|
||||||
|
const myPoly = roomPoly(r);
|
||||||
|
const holes = myPoly
|
||||||
|
? islandsOf(myPoly, space.rooms.filter((o) => o !== r).map((o) => roomPoly(o)!).filter(Boolean))
|
||||||
|
: [];
|
||||||
|
const pathD = (pts: number[][]) =>
|
||||||
|
'M ' + pts.map((p) => p[0] + ' ' + p[1]).join(' L ') + ' Z';
|
||||||
|
const shape = holes.length && myPoly
|
||||||
|
? svg`<path class="${cls}" style="${style}" fill-rule="evenodd"
|
||||||
|
d="${[myPoly, ...holes].map(pathD).join(' ')}"
|
||||||
|
@click=${() => this._clickRoom(r)} @mousemove=${tip}
|
||||||
|
@mouseleave=${() => (this._tip = null)}></path>`
|
||||||
|
: r.poly
|
||||||
? svg`<polygon class="${cls}" style="${style}" points="${r.poly.map((p) => p.join(',')).join(' ')}"
|
? svg`<polygon class="${cls}" style="${style}" points="${r.poly.map((p) => p.join(',')).join(' ')}"
|
||||||
@click=${() => this._clickRoom(r)} @mousemove=${tip}
|
@click=${() => this._clickRoom(r)} @mousemove=${tip}
|
||||||
@mouseleave=${() => (this._tip = null)}></polygon>`
|
@mouseleave=${() => (this._tip = null)}></polygon>`
|
||||||
|
|||||||
@@ -122,7 +122,6 @@
|
|||||||
"toast.markup_needs_server": "Markup is available after the config is moved to the server",
|
"toast.markup_needs_server": "Markup is available after the config is moved to the server",
|
||||||
"toast.conflict": "Config was changed in another window — data refreshed, repeat your last action",
|
"toast.conflict": "Config was changed in another window — data refreshed, repeat your last action",
|
||||||
"toast.cfg_save_failed": "Failed to save config: {err}",
|
"toast.cfg_save_failed": "Failed to save config: {err}",
|
||||||
"toast.point_in_room": "That point is inside room “{name}” — rooms must not overlap",
|
|
||||||
"toast.room_overlap": "The outline overlaps room “{name}” — rooms must not overlap",
|
"toast.room_overlap": "The outline overlaps room “{name}” — rooms must not overlap",
|
||||||
"toast.merge_not_adjacent": "Only rooms that share a wall can be merged",
|
"toast.merge_not_adjacent": "Only rooms that share a wall can be merged",
|
||||||
"toast.rooms_merged": "Rooms merged into “{name}”",
|
"toast.rooms_merged": "Rooms merged into “{name}”",
|
||||||
|
|||||||
@@ -122,7 +122,6 @@
|
|||||||
"toast.markup_needs_server": "Разметка доступна после переноса конфига на сервер",
|
"toast.markup_needs_server": "Разметка доступна после переноса конфига на сервер",
|
||||||
"toast.conflict": "Конфиг изменён в другом окне — данные обновлены, повторите последнее действие",
|
"toast.conflict": "Конфиг изменён в другом окне — данные обновлены, повторите последнее действие",
|
||||||
"toast.cfg_save_failed": "Не удалось сохранить конфиг: {err}",
|
"toast.cfg_save_failed": "Не удалось сохранить конфиг: {err}",
|
||||||
"toast.point_in_room": "Точка внутри комнаты «{name}» — комнаты не должны накладываться",
|
|
||||||
"toast.room_overlap": "Контур накладывается на комнату «{name}» — комнаты не должны накладываться",
|
"toast.room_overlap": "Контур накладывается на комнату «{name}» — комнаты не должны накладываться",
|
||||||
"toast.merge_not_adjacent": "Объединять можно только комнаты с общей стеной",
|
"toast.merge_not_adjacent": "Объединять можно только комнаты с общей стеной",
|
||||||
"toast.rooms_merged": "Комнаты объединены в «{name}»",
|
"toast.rooms_merged": "Комнаты объединены в «{name}»",
|
||||||
|
|||||||
+31
-3
@@ -241,19 +241,47 @@ function coversArea(a: number[][], b: number[][], eps: number): boolean {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Is `inner` fully contained in `outer` (edges may touch, never cross)? */
|
||||||
|
export function polyContainsPoly(outer: number[][], inner: number[][], eps = 1e-6): boolean {
|
||||||
|
if (!outer || !inner || outer.length < 3 || inner.length < 3) return false;
|
||||||
|
for (let i = 0; i < inner.length; i++)
|
||||||
|
for (let j = 0; j < outer.length; j++)
|
||||||
|
if (segmentsProperlyCross(inner[i], inner[(i + 1) % inner.length], outer[j], outer[(j + 1) % outer.length]))
|
||||||
|
return false;
|
||||||
|
for (const v of inner)
|
||||||
|
if (!pointStrictlyInside(v, outer, eps) && !pointOnBoundary(v, outer, eps)) return false;
|
||||||
|
// identical/traced outlines are NOT containment — probe the centroid strictness both ways
|
||||||
|
const c = [
|
||||||
|
inner.reduce((s, p) => s + p[0], 0) / inner.length,
|
||||||
|
inner.reduce((s, p) => s + p[1], 0) / inner.length,
|
||||||
|
];
|
||||||
|
return pointStrictlyInside(c, outer, eps) && polygonArea(inner) < polygonArea(outer) - eps;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Do two room outlines share floor area? Rooms must never overlap, but sharing a wall
|
* Do two room outlines ILLEGALLY share floor area? Sharing a wall (fully or
|
||||||
* (fully or partially) and touching at a corner are normal and stay legal. A room nested
|
* partially) and touching at a corner are normal. Since v1.34.0 full nesting is
|
||||||
* inside another counts as an overlap.
|
* legal too (island rooms: a column inside a ring, an inner room) — only edge
|
||||||
|
* crossings and PARTIAL overlaps are rejected.
|
||||||
*/
|
*/
|
||||||
export function roomsOverlap(a: number[][], b: number[][], eps = 1e-6): boolean {
|
export function roomsOverlap(a: number[][], b: number[][], eps = 1e-6): boolean {
|
||||||
if (!a || !b || a.length < 3 || b.length < 3) return false;
|
if (!a || !b || a.length < 3 || b.length < 3) return false;
|
||||||
for (let i = 0; i < a.length; i++)
|
for (let i = 0; i < a.length; i++)
|
||||||
for (let j = 0; j < b.length; j++)
|
for (let j = 0; j < b.length; j++)
|
||||||
if (segmentsProperlyCross(a[i], a[(i + 1) % a.length], b[j], b[(j + 1) % b.length])) return true;
|
if (segmentsProperlyCross(a[i], a[(i + 1) % a.length], b[j], b[(j + 1) % b.length])) return true;
|
||||||
|
if (polyContainsPoly(a, b, eps) || polyContainsPoly(b, a, eps)) return false;
|
||||||
return coversArea(a, b, eps) || coversArea(b, a, eps);
|
return coversArea(a, b, eps) || coversArea(b, a, eps);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Direct islands of `poly` among `others`: outlines fully inside it that are not
|
||||||
|
* themselves inside a bigger island (those are subtracted by their parent).
|
||||||
|
*/
|
||||||
|
export function islandsOf(poly: number[][], others: number[][][], eps = 1e-6): number[][][] {
|
||||||
|
const inside = others.filter((o) => polyContainsPoly(poly, o, eps));
|
||||||
|
return inside.filter((o) => !inside.some((p) => p !== o && polyContainsPoly(p, o, eps)));
|
||||||
|
}
|
||||||
|
|
||||||
/** Shoelace area of an outline (absolute value). */
|
/** Shoelace area of an outline (absolute value). */
|
||||||
export function polygonArea(poly: number[][]): number {
|
export function polygonArea(poly: number[][]): number {
|
||||||
if (!poly || poly.length < 3) return 0;
|
if (!poly || poly.length < 3) return 0;
|
||||||
|
|||||||
+24
-6
@@ -3,7 +3,7 @@ import assert from 'node:assert/strict';
|
|||||||
import {
|
import {
|
||||||
lqiColor, snapToGrid, segKey, samePoint, pointInPolygon, markerIdForBinding, averageLqi,
|
lqiColor, snapToGrid, segKey, samePoint, pointInPolygon, markerIdForBinding, averageLqi,
|
||||||
fitView, declump, safeUrl, resolveTapAction, floorsOf, subst, spaceDisplayOf, roomFillColor,
|
fitView, declump, safeUrl, resolveTapAction, floorsOf, subst, spaceDisplayOf, roomFillColor,
|
||||||
splitRoomPath,
|
splitRoomPath, polyContainsPoly, islandsOf,
|
||||||
segmentCm, formatLength, roomEdges, roomPoly, pointOnBoundary, pointStrictlyInside, roomsOverlap,
|
segmentCm, formatLength, roomEdges, roomPoly, pointOnBoundary, pointStrictlyInside, roomsOverlap,
|
||||||
mergeRooms, splitRoom, polygonArea, closestPointOnBoundary, isActiveState, snapToWall, openingAmount, fillColorsOf, lerpColor, roomFillStyle, stateIcon, lightColorOf, isAlarmState, parseRoomRef, diffNewDevices,
|
mergeRooms, splitRoom, polygonArea, closestPointOnBoundary, isActiveState, snapToWall, openingAmount, fillColorsOf, lerpColor, roomFillStyle, stateIcon, lightColorOf, isAlarmState, parseRoomRef, diffNewDevices,
|
||||||
} from '../test-build/logic.js';
|
} from '../test-build/logic.js';
|
||||||
@@ -325,14 +325,32 @@ test('roomsOverlap: sharing a wall or a corner is legal; real overlap is not', (
|
|||||||
assert.ok(roomsOverlap(SQ, [[1, 1], [3, 1], [3, 3], [1, 3]]));
|
assert.ok(roomsOverlap(SQ, [[1, 1], [3, 1], [3, 3], [1, 3]]));
|
||||||
});
|
});
|
||||||
|
|
||||||
test('roomsOverlap: nested, identical and enclosing outlines all count as overlap', () => {
|
test('roomsOverlap v1.34: nesting is legal (islands), duplicates and crossings are not', () => {
|
||||||
assert.ok(roomsOverlap(SQ, [[0.5, 0.5], [1.5, 0.5], [1.5, 1.5], [0.5, 1.5]])); // nested
|
// nested & enclosing — legal since island rooms
|
||||||
assert.ok(roomsOverlap(SQ, SQ)); // duplicate
|
assert.ok(!roomsOverlap(SQ, [[0.5, 0.5], [1.5, 0.5], [1.5, 1.5], [0.5, 1.5]]));
|
||||||
// drawn AROUND an existing room: every vertex outside, no vertex of ours inside it
|
assert.ok(!roomsOverlap([[-1, -1], [3, -1], [3, 3], [-1, 3]], SQ));
|
||||||
assert.ok(roomsOverlap([[-1, -1], [3, -1], [3, 3], [-1, 3]], SQ));
|
// a duplicate outline is still an overlap
|
||||||
|
assert.ok(roomsOverlap(SQ, SQ));
|
||||||
// a cross: no vertex of either lies inside the other, but the edges cross
|
// a cross: no vertex of either lies inside the other, but the edges cross
|
||||||
assert.ok(roomsOverlap([[0, 0.5], [3, 0.5], [3, 1.5], [0, 1.5]],
|
assert.ok(roomsOverlap([[0, 0.5], [3, 0.5], [3, 1.5], [0, 1.5]],
|
||||||
[[0.5, -1], [1.5, -1], [1.5, 3], [0.5, 3]]));
|
[[0.5, -1], [1.5, -1], [1.5, 3], [0.5, 3]]));
|
||||||
|
// genuine partial overlap is still rejected
|
||||||
|
assert.ok(roomsOverlap(SQ, [[1, 1], [3, 1], [3, 3], [1, 3]]));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('polyContainsPoly & islandsOf', () => {
|
||||||
|
const outer = [[0, 0], [10, 0], [10, 10], [0, 10]];
|
||||||
|
const col = [[4, 4], [6, 4], [6, 6], [4, 6]];
|
||||||
|
const tiny = [[4.5, 4.5], [5, 4.5], [5, 5], [4.5, 5]]; // внутри col
|
||||||
|
const partial = [[8, 8], [12, 8], [12, 12], [8, 12]];
|
||||||
|
assert.ok(polyContainsPoly(outer, col));
|
||||||
|
assert.ok(!polyContainsPoly(col, outer));
|
||||||
|
assert.ok(!polyContainsPoly(outer, outer)); // дубликат — не вложенность
|
||||||
|
assert.ok(!polyContainsPoly(outer, partial));
|
||||||
|
// прямые острова: col — да; tiny — нет (он остров col, не outer)
|
||||||
|
const isl = islandsOf(outer, [col, tiny, partial]);
|
||||||
|
assert.deepEqual(isl, [col]);
|
||||||
|
assert.deepEqual(islandsOf(col, [tiny]), [tiny]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('roomPoly: polygon rooms as-is, legacy rect rooms as four corners', () => {
|
test('roomPoly: polygon rooms as-is, legacy rect rooms as four corners', () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user