mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-01 04:09:17 +00:00
v1.59.0: release stable plan editing
This commit is contained in:
@@ -45,7 +45,7 @@ PLAN_ORPHAN_TTL_S = 3600
|
||||
SCHEDULED_GRACE_S = 30 * 24 * 3600
|
||||
FILES_DIR = "houseplan/files"
|
||||
CONF_ADMIN_ONLY = "admin_only"
|
||||
VERSION = "1.59.0-rc.2"
|
||||
VERSION = "1.59.0"
|
||||
|
||||
DEFAULT_CONFIG: dict = {
|
||||
"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",
|
||||
"requirements": [],
|
||||
"single_config_entry": true,
|
||||
"version": "1.59.0-rc.2"
|
||||
"version": "1.59.0"
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
Vendored
+19
-19
File diff suppressed because one or more lines are too long
@@ -2,6 +2,50 @@
|
||||
|
||||
## Unreleased (dev)
|
||||
|
||||
## v1.59.0 — 2026-08-06
|
||||
|
||||
Stable 1.59 turns House Plan into a substantially more complete floor-plan
|
||||
editor while keeping the card's live Home Assistant view coherent. It includes
|
||||
every beta/RC change below and closes the line with clean-floor hover geometry
|
||||
and robust wall unions.
|
||||
|
||||
- **Real walls, virtual boundaries and openings form one geometry system.** A
|
||||
room can mix wall thicknesses, a wall can contain real and virtual stretches,
|
||||
adjacent equal fragments compact again, and Resize preserves exact interval
|
||||
endpoints. Partial openings, virtual T-junctions, thick-wall door tunnels and
|
||||
sunlight from the inner corners of windows are handled consistently.
|
||||
- **Plan edits are explicit and reversible.** Room outline, Split, Close
|
||||
boundary, Merge rooms, Delete room, Resize, wall thickness and opening edits
|
||||
have one result each and share a named 50-command Undo/Redo stack. Delete
|
||||
removes only the selected object.
|
||||
- **One mandatory grid invariant.** Rooms, openings, markers, decor, furniture
|
||||
and backdrops always land on the grid or stay quantised along their wall.
|
||||
Optimize plans previews and applies all current lossless migrations, repairs
|
||||
legacy/off-grid data and compacts wall fragments with a safe one-step undo.
|
||||
- **Editors gained production workflows.** Background objects open their
|
||||
properties on double click; furniture has real dimensions and wall snapping;
|
||||
live labels accept arbitrary text mixed with manually typed or picker-inserted
|
||||
Home Assistant tokens; layer and object visibility controls are explicit.
|
||||
- **Device feedback uses one semantic language.** Yellow means actual work,
|
||||
orange means open/unlocked, alarms remain dominant, unavailable devices are
|
||||
muted, motion is a short event and presence lasts while detected. The marker
|
||||
plate, pulse/activity effect and source-light room fill now agree, including
|
||||
covers, climate, fans, relays, media players and other working devices.
|
||||
- **Rooms explain themselves in View.** Glow and toasts are pointer-transparent;
|
||||
room hover and tooltips work through light pools, the tooltip reports clean
|
||||
floor area, and device dialogs expose an explicit Hide/Show action. The hover
|
||||
accent now follows the same inner wall faces used for area; nested-room walls
|
||||
use the face exposed to the parent floor, while doors, windows and virtual
|
||||
spans remain gaps.
|
||||
- **Final wall-body correction.** Wall bodies are formed as the union of each
|
||||
room's own outset-minus-inset ring. A parent clean-floor contour can no longer
|
||||
erase half of a nested 15 cm wall or leave a white sliver at a complex
|
||||
multi-wall crossing.
|
||||
- **Documentation was rebuilt from the product.** The complete Russian user
|
||||
guide, setting/device behaviour tables and improvement plan describe the
|
||||
shipped code; obsolete audits and one-off debug material live under
|
||||
`legacy/`.
|
||||
|
||||
## v1.59.0-rc.2 — 2026-08-06
|
||||
|
||||
The second release candidate makes plan editing predictable and reversible,
|
||||
|
||||
@@ -8,6 +8,54 @@
|
||||
|
||||
## Не выпущено (dev)
|
||||
|
||||
## v1.59.0 — 2026-08-06
|
||||
|
||||
Стабильная 1.59 превращает House Plan в существенно более полный редактор
|
||||
планов, сохраняя цельное живое представление Home Assistant. В неё входят все
|
||||
изменения beta/RC ниже; линейку завершают hover по границе чистого пола и
|
||||
устойчивое объединение тел стен.
|
||||
|
||||
- **Реальные стены, виртуальные границы и проёмы работают как одна система.** В
|
||||
комнате можно смешивать толщины, одна стена может состоять из реальных и
|
||||
виртуальных участков, а соседние одинаковые фрагменты снова объединяются.
|
||||
Resize сохраняет точные концы интервалов; частичные открытия, виртуальные
|
||||
Т-стыки, дверные тоннели в толстых стенах и солнечные лучи из внутренних
|
||||
углов окон обрабатываются согласованно.
|
||||
- **Правки плана однозначны и обратимы.** «Контур комнаты», Split, «Закрыть
|
||||
границу», «Объединить комнаты», «Удалить комнату», Resize, толщина стен и
|
||||
операции с проёмами имеют по одному результату и используют общий
|
||||
именованный Undo/Redo на 50 команд. Delete удаляет только выбранный объект.
|
||||
- **Единый обязательный инвариант сетки.** Комнаты, проёмы, маркеры, декор,
|
||||
мебель и подложка всегда попадают в сетку либо квантуются вдоль своей стены.
|
||||
«Оптимизировать планы» показывает preview и применяет все актуальные
|
||||
миграции без потерь, исправляет старые/off-grid данные, уплотняет фрагменты
|
||||
стен и оставляет безопасную одношаговую отмену.
|
||||
- **Редакторы получили рабочие пользовательские сценарии.** Свойства объектов
|
||||
подложки открываются двойным кликом; мебель имеет реальные размеры и магнит
|
||||
к стенам; живые надписи принимают произвольный текст вперемешку с вручную
|
||||
введёнными или вставленными из picker токенами Home Assistant; управление
|
||||
видимостью слоёв и объектов стало явным.
|
||||
- **Индикация устройств говорит на одном семантическом языке.** Жёлтый означает
|
||||
фактическую работу, оранжевый — открыто/разблокировано, тревога имеет высший
|
||||
приоритет, недоступное устройство приглушается, движение является коротким
|
||||
событием, а присутствие длится весь детект. Подложка маркера, пульсация и
|
||||
заливка «Свет по источникам» согласованы для штор, климата, вентиляторов,
|
||||
реле, медиаплееров и других работающих устройств.
|
||||
- **Комната объясняет себя в Просмотре.** Glow и toast не перехватывают
|
||||
указатель; hover и тултип работают сквозь световое пятно, тултип показывает
|
||||
чистую площадь, а диалог устройства содержит явное действие
|
||||
«Скрыть»/«Показать». Акцент hover теперь идёт по тем же внутренним граням,
|
||||
что используются для площади; у вложенной комнаты выбирается грань,
|
||||
обращённая к родительскому полу, а двери, окна и виртуальные участки остаются
|
||||
разрывами.
|
||||
- **Финальная коррекция тел стен.** Тела формируются как объединение собственного
|
||||
кольца `внешний − внутренний` каждой комнаты. Чистый пол родительской комнаты
|
||||
больше не может вырезать половину вложенной стены толщиной 15 см либо оставить
|
||||
белую щель на сложном перекрёстке.
|
||||
- **Документация заново собрана по продукту.** Полное русское руководство,
|
||||
таблицы поведения настроек/устройств и план улучшений описывают выпущенный
|
||||
код; устаревшие аудиты и разовые debug-материалы перенесены в `legacy/`.
|
||||
|
||||
## v1.59.0-rc.2 — 2026-08-06
|
||||
|
||||
Второй release candidate делает редактирование плана предсказуемым и
|
||||
|
||||
+8
-3
@@ -15,11 +15,11 @@
|
||||
|
||||
| Item | State |
|
||||
|---|---|
|
||||
| Version | **v1.59.0-rc.2** everywhere (manifest, const.py, package.json, CARD_VERSION) — **pre-release**, tag `v1.59.0-rc.2` on **`dev`**, GitHub Release with `prerelease=true`; `main` is not touched. The rc line avoids the beta.9/beta.10 lexicographic discovery problem in HACS 2.0.x. Previous pre-release: v1.59.0-rc.1; previous stable: v1.58.0 |
|
||||
| Version | **v1.59.0** everywhere (manifest, const.py, package.json, CARD_VERSION) — stable release. Tag `v1.59.0`, `main` and `dev` point to the same tested release commit; GitHub Release uses `prerelease=false`. Previous pre-release: v1.59.0-rc.2; previous stable: v1.58.0 |
|
||||
|
||||
| Workflow | Owner's rule since 2026-08-05: ordinary fixes/features are made **locally, without tests and without commits**. Tests/build/smokes run only when the owner asks for a pre-release; then bump the version, commit/tag the tested `dev` state and publish a GitHub Release with `prerelease=true`. `main` is not touched and nothing is copied to the home instance by hand. The release workflow now reports a failed HACS-discovery check when GitHub does not return the new tag as its first prerelease |
|
||||
| Workflow | Owner's rule since 2026-08-05: ordinary fixes/features are made **locally, without tests and without commits**. A requested pre-release gets the full tests/build/smokes, one tested `dev` commit/tag and a GitHub Release with `prerelease=true`; `main` stays untouched. A requested stable release gets the same full gate, then `main` is fast-forwarded to the exact tested `dev` SHA and the GitHub Release uses `prerelease=false`. Nothing is copied to the home instance by hand |
|
||||
| GitHub | https://github.com/Matysh/houseplan-card — `main` carries stable releases; pre-release tags may point directly at `dev`. Work lands on `dev` and is merged into `main` for a stable release, so `dev` is normally equal to or ahead of `main`, never behind. Push via SSH key `ha_jb` (remote git@github.com:…); API releases via the fine-grained PAT in `~/.git-credentials` (Contents R/W, issued 2026-07-23) |
|
||||
| CI | rc.2 passes the local Node 22 typecheck/build, all 389 frontend tests, all 93 pure-backend tests and all 113 browser smokes. Native Windows cannot start the current HA pytest plugin (`fcntl` is Unix-only), so the exact-SHA Ubuntu Validate remains mandatory: `release.yml` withholds the asset until every matching run finishes green; GitHub Validate and the release asset are verified after the tag is pushed |
|
||||
| CI | v1.59.0 passes the local Node 22 typecheck/build, all 390 frontend tests, all 93 pure-backend tests and all 113 browser smokes. Native Windows cannot start the current HA pytest plugin (`fcntl` is Unix-only), so the exact-SHA Ubuntu Validate remains mandatory: `release.yml` withholds the asset until every matching run finishes green; GitHub Validate and the release asset are verified after the tag is pushed |
|
||||
| HACS | Custom repository works. **Inclusion PR: hacs/default#9004** — open, valid, labeled, mergeable clean, never drafted. Queue: 1212 open, 835 older than ours. Merge rate COLLAPSED: 75 in July but almost all in the first decade, 0 in the last week (checked 2026-07-29) — maintainers process in rare bursts; ETA unknowable, months at best. Nothing actionable on our side |
|
||||
| Home instance | ha.jbstudio.pro (SSH port **22222**, key `ha_jb`; HA config root is `/mnt/data/supervisor/homeassistant` — `/config` does NOT exist in this SSH environment), last direct copy was **v1.57.0**; from v1.58.0 on it updates itself through HACS by tag (no scp) |
|
||||
| Localization | UI en/ru (src/i18n/*.json), everything user-visible localized incl. kiosk popover |
|
||||
@@ -88,6 +88,11 @@
|
||||
Glow and toast overlays no longer steal room/tool pointers, View hover covers
|
||||
shared thick walls, device Hide/Show is explicit, the static no-op aspect
|
||||
field is gone, and the new user guide/product audit replaces archived docs.
|
||||
- **v1.59.0** (2026-08-06): The stable 1.59 line includes all beta/RC work.
|
||||
Room hover follows clean-floor wall faces, including nested contours and
|
||||
projected opening gaps. Thick-wall rendering unions each room's own wall
|
||||
ring, so one room's floor cannot erase another room's wall or leave white
|
||||
slivers at complex crossings.
|
||||
|
||||
## Recent milestones (details in CHANGELOG.md)
|
||||
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "houseplan-card",
|
||||
"version": "1.59.0-rc.2",
|
||||
"version": "1.59.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "houseplan-card",
|
||||
"version": "1.59.0-rc.2",
|
||||
"version": "1.59.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"lit": "^3.1.3",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "houseplan-card",
|
||||
"version": "1.59.0-rc.2",
|
||||
"version": "1.59.0",
|
||||
"description": "Interactive house plan Lovelace card for Home Assistant",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
|
||||
+86
-16
@@ -47,7 +47,8 @@ import {
|
||||
degradeWalls, rekeyWallsAfterMove,
|
||||
setWallThickness, setWallThicknessForRoom, cmToField, fieldToCm, wallCmToUnits,
|
||||
wallEdgeBodies, wallBodiesUnionPath, paperRoomShapesWithWalls,
|
||||
innerContourForRoom, openingInnerFaceOffset, applyWallThicknessToNewRoom,
|
||||
innerContourForRoom, roomWallProfile, outsetContour,
|
||||
openingInnerFaceOffset, applyWallThicknessToNewRoom,
|
||||
drawWallPreviewD, DRAW_WALL_DEFAULT_CM, wallIntervals, normalizeWallIntervals,
|
||||
intervalCmAt, wallBodyNeedsSolid, type WallEntry,
|
||||
} from './wall-thickness';
|
||||
@@ -90,7 +91,7 @@ import {
|
||||
type DeviceActivity, type DeviceVisualState, type EntityVisualSample,
|
||||
} from './device-visual';
|
||||
|
||||
const CARD_VERSION = '1.59.0-rc.2';
|
||||
const CARD_VERSION = '1.59.0';
|
||||
/** HP-1552 boot-veil timing (AUD-1552-02). The veil holds for at least
|
||||
* BOOT_MIN_MS; every stage-height change restarts a BOOT_QUIET_MS
|
||||
* trailing-quiescence requirement (chrome still settling near the cap
|
||||
@@ -5723,12 +5724,12 @@ class HouseplanCard extends LitElement {
|
||||
}
|
||||
|
||||
/**
|
||||
* View-only perimeter above real wall bodies.
|
||||
* View-only clean-floor perimeter above real wall bodies.
|
||||
*
|
||||
* The room shape lives below the unioned wall body, so its CSS :hover stroke
|
||||
* disappears precisely on thick/common walls. Draw the hovered room's real
|
||||
* boundary once more after the bodies. Open (virtual) spans stay dashed in
|
||||
* their own layer and are cut out here instead of being painted solid.
|
||||
* The hover follows the same inner face that defines the displayed room
|
||||
* area. A nested room is a hole in the hovered room, so its boundary uses the
|
||||
* opposite (outward) wall face. Open spans stay dashed in their own layer;
|
||||
* doors and windows remain gaps instead of being bridged by a solid accent.
|
||||
*/
|
||||
private _renderRoomHover(space: SpaceModel): TemplateResult {
|
||||
const hover = this._hoverRoom;
|
||||
@@ -5740,16 +5741,39 @@ class HouseplanCard extends LitElement {
|
||||
const poly = roomPoly(room);
|
||||
if (!poly) return svg`` as unknown as TemplateResult;
|
||||
|
||||
// A parent room also owns the walls around rooms nested inside it.
|
||||
// A parent room also owns the floor-facing side of walls around rooms
|
||||
// nested inside it. Keep the room together with its cached polygon so a
|
||||
// hole can use the OUTER face of that nested room's wall.
|
||||
const others = space.rooms
|
||||
.filter((r) => r !== room)
|
||||
.map((r) => roomPoly(r))
|
||||
.filter((p): p is number[][] => !!p);
|
||||
const contours = [poly, ...islandsOf(poly, others)];
|
||||
.map((r) => ({ room: r, poly: roomPoly(r) }))
|
||||
.filter((v): v is { room: RoomCfg; poly: number[][] } => !!v.poly);
|
||||
const islandPolys = islandsOf(poly, others.map((v) => v.poly));
|
||||
const pairs = this._openPairs();
|
||||
const allOpenCuts = pairs.flatMap((p) => p.segs);
|
||||
const openCuts = room.id
|
||||
? pairs.filter((p) => p.a.id === room.id || p.b.id === room.id).flatMap((p) => p.segs)
|
||||
: pairs.flatMap((p) => p.segs);
|
||||
const walls = this._spaceWalls;
|
||||
const floor = walls.length && room.id
|
||||
? (innerContourForRoom(
|
||||
space.rooms, room.id, walls, allOpenCuts,
|
||||
this._wallKeyPitch, this._cellCm, this._gridPitch, NORM_W,
|
||||
) || poly)
|
||||
: poly;
|
||||
const contours: { axis: number[][]; face: number[][] }[] = [{ axis: poly, face: floor }];
|
||||
for (const island of islandPolys) {
|
||||
const owner = others.find((v) => v.poly === island)?.room;
|
||||
let face = island;
|
||||
if (walls.length && owner?.id) {
|
||||
const profile = roomWallProfile(
|
||||
space.rooms, owner.id, walls, allOpenCuts,
|
||||
this._wallKeyPitch, this._cellCm, this._gridPitch, NORM_W,
|
||||
);
|
||||
if (profile) face = outsetContour(profile.poly, profile.offsets) || island;
|
||||
}
|
||||
contours.push({ axis: island, face });
|
||||
}
|
||||
// A hidden opening symbol is still a physical opening: never bridge a
|
||||
// doorway/window with the hover stroke merely because its symbol is off.
|
||||
const openingCuts = this._openingsR.map((o) => {
|
||||
@@ -5758,13 +5782,59 @@ class HouseplanCard extends LitElement {
|
||||
const dy = (Math.sin(rad) * o.rlen) / 2;
|
||||
return [o.rx - dx, o.ry - dy, o.rx + dx, o.ry + dy];
|
||||
});
|
||||
const cuts = openCuts.concat(openingCuts);
|
||||
const eps = this._gridPitch * 0.02;
|
||||
const d = contours.map((contour) => {
|
||||
if (!cuts.length) {
|
||||
return `M ${contour.map((p) => `${p[0]} ${p[1]}`).join(' L ')} Z`;
|
||||
|
||||
// Cuts are stored on wall centrelines. Project each relevant cut onto the
|
||||
// clean-floor face before trimming; otherwise a door on a thick wall would
|
||||
// no longer intersect the inset hover contour.
|
||||
const cutOnFace = (cut: number[], axis: number[][], face: number[][]): number[] | null => {
|
||||
const cx = cut[2] - cut[0], cy = cut[3] - cut[1];
|
||||
const cLen = Math.hypot(cx, cy);
|
||||
if (cLen < eps) return null;
|
||||
const cux = cx / cLen, cuy = cy / cLen;
|
||||
const mx = (cut[0] + cut[2]) / 2, my = (cut[1] + cut[3]) / 2;
|
||||
let belongs = false;
|
||||
for (let i = 0; i < axis.length; i++) {
|
||||
const a = axis[i], b = axis[(i + 1) % axis.length];
|
||||
const ex = b[0] - a[0], ey = b[1] - a[1];
|
||||
const eLen = Math.hypot(ex, ey);
|
||||
if (eLen < eps || Math.abs(cux * (ey / eLen) - cuy * (ex / eLen)) > 0.05) continue;
|
||||
if (distToSegment([mx, my], [a[0], a[1], b[0], b[1]]) <= eps * 4) {
|
||||
belongs = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return outlineWithout(contour, cuts, eps)
|
||||
if (!belongs) return null;
|
||||
|
||||
let best: { a: number[]; b: number[]; d: number } | null = null;
|
||||
for (let i = 0; i < face.length; i++) {
|
||||
const a = face[i], b = face[(i + 1) % face.length];
|
||||
const ex = b[0] - a[0], ey = b[1] - a[1];
|
||||
const eLen = Math.hypot(ex, ey);
|
||||
if (eLen < eps || Math.abs(cux * (ey / eLen) - cuy * (ex / eLen)) > 0.05) continue;
|
||||
const d = distToSegment([mx, my], [a[0], a[1], b[0], b[1]]);
|
||||
if (!best || d < best.d) best = { a, b, d };
|
||||
}
|
||||
if (!best) return null;
|
||||
const fx = best.b[0] - best.a[0], fy = best.b[1] - best.a[1];
|
||||
const fLen = Math.hypot(fx, fy) || 1;
|
||||
const nx = -fy / fLen, ny = fx / fLen;
|
||||
const shift = (best.a[0] - mx) * nx + (best.a[1] - my) * ny;
|
||||
return [
|
||||
cut[0] + nx * shift, cut[1] + ny * shift,
|
||||
cut[2] + nx * shift, cut[3] + ny * shift,
|
||||
];
|
||||
};
|
||||
|
||||
const rawCuts = openCuts.concat(openingCuts);
|
||||
const d = contours.map(({ axis, face }) => {
|
||||
const cuts = rawCuts
|
||||
.map((cut) => cutOnFace(cut, axis, face))
|
||||
.filter((cut): cut is number[] => !!cut);
|
||||
if (!cuts.length) {
|
||||
return `M ${face.map((p) => `${p[0]} ${p[1]}`).join(' L ')} Z`;
|
||||
}
|
||||
return outlineWithout(face, cuts, eps)
|
||||
.map((sg) => `M ${sg[0]} ${sg[1]} L ${sg[2]} ${sg[3]}`)
|
||||
.join(' ');
|
||||
}).filter(Boolean).join(' ');
|
||||
|
||||
+2
-2
@@ -1212,8 +1212,8 @@ export const cardStyles = css`
|
||||
stroke: rgba(62, 166, 255, 0.55);
|
||||
stroke-opacity: 1;
|
||||
}
|
||||
/* View hover is repeated above the unioned thick-wall body. The original
|
||||
room stroke remains responsible for the fill and thin-wall fallback. */
|
||||
/* View hover follows the clean-floor face above the unioned wall body. The
|
||||
original room stroke remains responsible for fill and thin-wall fallback. */
|
||||
.room-hover-outline {
|
||||
fill: none;
|
||||
stroke: var(--hp-accent);
|
||||
|
||||
+16
-18
@@ -1270,8 +1270,13 @@ export function wallBodyRings(
|
||||
}
|
||||
|
||||
/**
|
||||
* Seamless wall hatch: union of all half-outsets minus union of all half-insets,
|
||||
* Seamless wall hatch: union of each room's own outset-minus-inset wall ring,
|
||||
* with opening slots cut as holes. One continuous body across L and T joins.
|
||||
*
|
||||
* Do not rewrite this as `(union outsets) - (union insets)`: subtraction does
|
||||
* not distribute over union. In a nested/complex layout the clean floor of one
|
||||
* room would then erase a wall owned by another room, leaving half-depth strips
|
||||
* and tiny holes at junctions.
|
||||
*/
|
||||
export function wallBodiesUnionPath(
|
||||
rooms: any[],
|
||||
@@ -1284,8 +1289,7 @@ export function wallBodiesUnionPath(
|
||||
coordScale = 1,
|
||||
): { d: string; depthUnits: number } | null {
|
||||
if (!walls?.length) return null;
|
||||
const outsets: number[][][] = [];
|
||||
const insets: number[][][] = [];
|
||||
const roomRings: { outset: number[][]; inset: number[][] | null }[] = [];
|
||||
let maxDepth = 0;
|
||||
for (const room of rooms || []) {
|
||||
if (!room?.id) continue;
|
||||
@@ -1294,26 +1298,20 @@ export function wallBodiesUnionPath(
|
||||
for (const o of pr.offsets) if (o > 0) maxDepth = Math.max(maxDepth, o * 2);
|
||||
const outC = outsetContour(pr.poly, pr.offsets);
|
||||
const inC = insetContour(pr.poly, pr.offsets);
|
||||
if (outC) outsets.push(outC);
|
||||
if (inC) insets.push(inC);
|
||||
if (!outC) continue;
|
||||
roomRings.push({ outset: outC, inset: inC });
|
||||
}
|
||||
if (!outsets.length) return null;
|
||||
if (!roomRings.length) return null;
|
||||
const junctions = virtualJunctionPatches(
|
||||
rooms, walls, openCuts, pitch, cellCm, gridPitch, coordScale,
|
||||
);
|
||||
try {
|
||||
let Uout: any = closedRing(outsets[0]);
|
||||
for (let i = 1; i < outsets.length; i++) {
|
||||
Uout = union(Uout, closedRing(outsets[i]) as any);
|
||||
}
|
||||
let body: any = Uout;
|
||||
if (insets.length) {
|
||||
let Uin: any = closedRing(insets[0]);
|
||||
for (let i = 1; i < insets.length; i++) {
|
||||
Uin = union(Uin, closedRing(insets[i]) as any);
|
||||
}
|
||||
body = difference(Uout, Uin);
|
||||
}
|
||||
const bodyOf = (ring: typeof roomRings[number]): any => {
|
||||
const outset: any = closedRing(ring.outset);
|
||||
return ring.inset ? difference(outset, closedRing(ring.inset) as any) : outset;
|
||||
};
|
||||
let body: any = bodyOf(roomRings[0]);
|
||||
for (let i = 1; i < roomRings.length; i++) body = union(body, bodyOf(roomRings[i]));
|
||||
// The room-ring subtraction above cannot infer a mitre between real arms
|
||||
// owned by different contours at a virtual T. Add only those missing
|
||||
// junction pieces, then let physical openings cut through them as usual.
|
||||
|
||||
@@ -398,6 +398,24 @@ test('wallBodiesUnionPath: single fully-thick room keeps a floor hole', () => {
|
||||
assert.ok((united.d.match(/M/g) || []).length >= 2, united.d);
|
||||
});
|
||||
|
||||
test('wallBodiesUnionPath: a parent floor never erases a nested room wall', () => {
|
||||
const scale = 1000;
|
||||
const rooms = [
|
||||
{ id: 'parent', poly: [[100, 100], [900, 100], [900, 900], [100, 900]] },
|
||||
{ id: 'nested', poly: [[300, 300], [700, 300], [700, 700], [300, 700]] },
|
||||
];
|
||||
let walls = applyWallThicknessToNewRoom([], rooms, 'parent', 15, pitch, [], scale);
|
||||
walls = applyWallThicknessToNewRoom(walls, rooms, 'nested', 15, pitch, [], scale);
|
||||
const united = wallBodiesUnionPath(
|
||||
rooms, walls, [], [], pitch, cellCm, GRID_PITCH, scale,
|
||||
);
|
||||
assert.ok(united);
|
||||
// Parent ring (outer + floor hole) and nested ring (outer + floor hole).
|
||||
// The old `(union outsets) - (union insets)` formula returned only two
|
||||
// subpaths here because the parent floor swallowed the nested wall entirely.
|
||||
assert.ok((united.d.match(/M/g) || []).length >= 4, united.d);
|
||||
});
|
||||
|
||||
test('paper with walls covers shared centreline; without walls matches paperRoomShapes', () => {
|
||||
const rooms = [
|
||||
{ id: 'a', poly: [[0, 0], [5, 0], [5, 4], [0, 4]] },
|
||||
|
||||
Reference in New Issue
Block a user