feat v1.35.0: glow fill — dark house with glowing light sources

- fill_mode 'glow': uniform darkness over every room; lit lamps render
  radial gradient pools (rgb_color -> color temp via kelvinToRgb ->
  configurable default; brightness scales alpha)
- pools clipped by the source's room + doorway sectors (doorSector rays
  to door edges; hasRoomBehind blocks entrance doors); windows and
  islands don't participate (no shadow casting, documented)
- glow radius in HA units (m/ft) in general settings, stored in cm;
  palette group glow_base/glow_light; backend schema updated
- +4 unit tests (98 total); smoke_glow.mjs (10 checks); docs same-commit
This commit is contained in:
Matysh
2026-07-23 12:51:48 +03:00
parent 1e20279adf
commit 7eaf513c9e
16 changed files with 765 additions and 342 deletions
+51
View File
@@ -4,6 +4,7 @@ import {
lqiColor, snapToGrid, segKey, samePoint, pointInPolygon, markerIdForBinding, averageLqi,
fitView, declump, safeUrl, resolveTapAction, floorsOf, subst, spaceDisplayOf, roomFillColor,
splitRoomPath, polyContainsPoly, islandsOf,
kelvinToRgb, glowColorOf, doorSector, hasRoomBehind,
segmentCm, formatLength, roomEdges, roomPoly, pointOnBoundary, pointStrictlyInside, roomsOverlap,
mergeRooms, splitRoom, polygonArea, closestPointOnBoundary, isActiveState, snapToWall, openingAmount, fillColorsOf, lerpColor, roomFillStyle, stateIcon, lightColorOf, isAlarmState, parseRoomRef, diffNewDevices,
} from '../test-build/logic.js';
@@ -598,3 +599,53 @@ test('splitRoomPath rejects bad paths', () => {
// менее двух точек
assert.equal(splitRoomPath(sq, [[4, 0]], 1e-6), null);
});
test('kelvinToRgb: warm is orange-ish, cool is blue-ish white', () => {
const warm = kelvinToRgb(2700);
assert.equal(warm[0], 255);
assert.ok(warm[2] < 200 && warm[1] < 230);
const cool = kelvinToRgb(6600);
assert.ok(cool[0] > 245 && cool[1] > 240 && cool[2] > 245);
// clamps
assert.ok(kelvinToRgb(100)[0] === 255);
});
test('glowColorOf: rgb wins, then color temp, then fallback; off = null', () => {
assert.equal(glowColorOf({ state: 'off', attributes: {} }, '#fff'), null);
assert.equal(glowColorOf(null, '#fff'), null);
const rgb = glowColorOf({ state: 'on', attributes: { rgb_color: [10, 20, 30], brightness: 128 } }, '#fff');
assert.equal(rgb.c, 'rgb(10, 20, 30)');
assert.ok(Math.abs(rgb.bri - 0.5) < 0.01);
const ct = glowColorOf({ state: 'on', attributes: { color_temp_kelvin: 2700 } }, '#fff');
assert.ok(ct.c.startsWith('rgb(255'));
const mireds = glowColorOf({ state: 'on', attributes: { color_temp: 370 } }, '#fff'); // ~2700K
assert.ok(mireds.c.startsWith('rgb(255'));
const fb = glowColorOf({ state: 'on', attributes: {} }, '#abcdef');
assert.equal(fb.c, '#abcdef');
assert.equal(fb.bri, 1);
});
test('doorSector: sector through a door, clamped and guarded', () => {
const s = [0, 0];
const sec = doorSector(s, [10, -2], [10, 2], 50);
assert.ok(sec && sec.length === 10 + 0); // вершина + 9 точек дуги
assert.deepEqual(sec[0], [0, 0]);
for (const p of sec.slice(1)) {
const d = Math.hypot(p[0], p[1]);
assert.ok(Math.abs(d - 50) < 1e-6);
assert.ok(p[0] > 0); // сектор смотрит в сторону двери
}
// дверь за радиусом
assert.equal(doorSector(s, [60, -2], [60, 2], 50), null);
// источник на краю двери
assert.equal(doorSector(s, [0, 0], [10, 2], 50), null);
});
test('hasRoomBehind: neighbour room yes, street no', () => {
const neighbour = [[10, -5], [20, -5], [20, 5], [10, 5]];
// дверь в стене x=10 (стена вертикальна: угол 90°), источник слева в (5,0)
assert.ok(hasRoomBehind([10, 0], 90, [5, 0], [neighbour], 1));
// за дверью пусто
assert.ok(!hasRoomBehind([10, 0], 90, [5, 0], [], 1));
assert.ok(!hasRoomBehind([10, 0], 90, [5, 0], [[[30, 30], [40, 30], [40, 40], [30, 40]]], 1));
});