mirror of
https://github.com/Matysh/houseplan-card
synced 2026-07-31 08:28:31 +00:00
feat v1.23.0: doors & windows with live open/lock state
New markup tool 'Opening': click a wall — the opening snaps onto the nearest DERIVED room wall (v1.19.0 model: openings keep absolute coords, so room edits never break them) and takes its angle. Dialog: type, length in real cm (door 90 / window 120 — per-space scale makes it honest), contact sensor (invertible), lock (doors). Rendering after easy-floorplan (MIT): hinged leaf + swing arc drawing on via stroke-dashoffset; window = two casements. No sensor → static plan defaults (door open, window closed); unavailable freezes the default (pure openingAmount, tested). Lock: padlock badge (green locked / orange unlocked / grey unknown); the lock is never toggled from the plan — clicking shows an info card with both states. snapToWall + openingAmount in logic.ts; space.openings validated server-side. +2 tests (76). Verified live on the dacha (real TTLock + TRV window sensor).
This commit is contained in:
@@ -74,6 +74,50 @@ export function roomEdges(rooms: any[]): number[][] {
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Snap a point onto the nearest wall of any room, returning the snapped point and
|
||||
* the wall's angle (degrees), or null when no wall is within maxDist. Walls are the
|
||||
* DERIVED room edges (a line has no independent existence on the plan), so an opening
|
||||
* placed with this stays valid however rooms are later edited — it keeps absolute
|
||||
* coordinates and is not tied to a room id or edge index.
|
||||
*/
|
||||
export function snapToWall(
|
||||
p: number[], rooms: any[], maxDist: number,
|
||||
): { x: number; y: number; angle: number } | null {
|
||||
let best: { x: number; y: number; angle: number } | null = null;
|
||||
let bestD = maxDist;
|
||||
for (const e of roomEdges(rooms)) {
|
||||
const [x1, y1, x2, y2] = e;
|
||||
const dx = x2 - x1, dy = y2 - y1;
|
||||
const len2 = dx * dx + dy * dy;
|
||||
if (!len2) continue;
|
||||
let t = ((p[0] - x1) * dx + (p[1] - y1) * dy) / len2;
|
||||
t = Math.max(0, Math.min(1, t));
|
||||
const q = [x1 + t * dx, y1 + t * dy];
|
||||
const d = Math.hypot(p[0] - q[0], p[1] - q[1]);
|
||||
if (d < bestD) {
|
||||
bestD = d;
|
||||
best = { x: q[0], y: q[1], angle: (Math.atan2(dy, dx) * 180) / Math.PI };
|
||||
}
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
/**
|
||||
* How far open an opening is drawn, 0..1, from its contact sensor state.
|
||||
* No sensor bound → doors default to open (the familiar swing symbol), windows to
|
||||
* closed (intact glass) — same convention as a static architectural plan.
|
||||
* `unavailable`/`unknown` freeze the default too: an outage must not fake motion.
|
||||
*/
|
||||
export function openingAmount(
|
||||
type: 'door' | 'window', state: string | null | undefined, invert = false,
|
||||
): number {
|
||||
if (state == null || state === 'unavailable' || state === 'unknown')
|
||||
return type === 'door' ? 1 : 0;
|
||||
const open = isActiveState(state) !== !!invert;
|
||||
return open ? 1 : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is an entity "active / detected"? Used by presence ripples, which are opted into per
|
||||
* device and therefore must not depend on the card-wide live_states toggle.
|
||||
|
||||
Reference in New Issue
Block a user