v1.59.0-rc.1: optimize plans and polish editor feedback
Validate / hacs (push) Failing after 7s
Validate / hassfest (push) Failing after 6s
Validate / frontend (push) Successful in 3m12s
Validate / backend (push) Failing after 8m53s
Validate / smoke (push) Failing after 13m51s

This commit is contained in:
Matysh
2026-08-06 10:14:52 +03:00
parent d2bec266ed
commit e0f6746d7f
43 changed files with 2052 additions and 568 deletions
+56
View File
@@ -0,0 +1,56 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { optimizePlans, PLAN_MODEL_VERSION } from '../test-build/plan-optimizer.js';
import { GRID_STEP_N as S } from '../test-build/space-geometry.js';
import { wallKey } from '../test-build/wall-thickness.js';
const room = (id, x0, x1, openTo) => ({
id,
name: id,
area: null,
poly: [[x0, 0], [x1, 0], [x1, 1], [x0, 1]],
...(openTo ? { open_to: [openTo] } : {}),
});
test('optimizePlans migrates, aligns and canonicalises idempotently', () => {
const config = {
spaces: [{
id: 'f1', title: 'Floor', view_box: [0, 0, 1, 1], cell_cm: 5,
segments: [[0, 0, 1, 0]],
rooms: [room('a', 0, 0.5, 'b'), room('b', 0.5, 1, 'a')],
walls: [
{ key: wallKey([0, 0], [0.25, 0], S), a: [0, 0], b: [0.25, 0], cm: 20 },
{ key: wallKey([0.25, 0], [0.5, 0], S), a: [0.25, 0], b: [0.5, 0], cm: 20 },
],
decor: [{
id: 't1', kind: 'text', x: S / 3, y: S / 3,
text: 'Now {}', entity: 'sensor.temp', attr: 'state', size: 'm',
}],
}],
markers: [{
id: 'm1', binding: 'virtual', display: 'ripple',
vacuum: { trail: false },
}],
settings: {},
};
const layout = { m1: { s: 'f1', x: S / 3, y: S / 3 } };
const first = optimizePlans(config, layout);
assert.equal(first.changed, true);
assert.equal(first.config.model_version, PLAN_MODEL_VERSION);
assert.equal('segments' in first.config.spaces[0], false);
assert.equal(first.config.markers[0].display, 'icon_ripple');
assert.deepEqual(first.config.markers[0].vacuum, { trail_mode: 'never' });
assert.equal(first.config.spaces[0].decor[0].text, 'Now {sensor.temp}');
assert.equal(first.config.spaces[0].decor[0].scale, 1);
assert.equal('entity' in first.config.spaces[0].decor[0], false);
assert.equal(first.config.spaces[0].open_spans.length, 1);
assert.equal(first.config.spaces[0].walls.length, 1);
assert.equal(first.layout.m1.x % S, 0);
const second = optimizePlans(first.config, first.layout);
assert.equal(second.changed, false);
assert.deepEqual(second.config, first.config);
assert.deepEqual(second.layout, first.layout);
});
+51 -1
View File
@@ -83,6 +83,15 @@ test('degradeWalls drops a key with no matching room edge', () => {
assert.equal(kept[0].key, live);
});
test('degradeWalls keeps an exact maximal run even when another breakpoint subdivides it', () => {
const rooms = [{ id: 'r1', poly: [[0, 0], [1, 0], [1, 1], [0, 1]] }];
const walls = setWallThickness([], [0, 0], [0.4, 0], 18, pitch);
const kept = degradeWalls(walls, rooms, pitch);
assert.equal(kept.length, 1);
assert.deepEqual(kept[0].a, [0, 0]);
assert.deepEqual(kept[0].b, [0.4, 0]);
});
test('rekeyWallsAfterMove rewrites the key when a span shifts by one cell', () => {
const oldA = [0.1, 0.2], oldB = [0.4, 0.2];
const newA = [0.1, 0.2 + pitch], newB = [0.4, 0.2 + pitch];
@@ -107,6 +116,16 @@ test('rekeyWallsAfterMove carries atomic remainders of a partially virtual wall'
]);
});
test('rekeyWallsAfterMove carries exact interval endpoints with the wall', () => {
const oldA = [0.2, 0.1], oldB = [0.2, 0.4];
const newA = [0.3, 0.1], newB = [0.3, 0.4];
const walls = setWallThickness([], oldA, oldB, 22, pitch);
const next = rekeyWallsAfterMove(walls, [[oldA, oldB]], [[newA, newB]], pitch);
assert.equal(next[0].key, wallKey(newA, newB, pitch));
assert.deepEqual(next[0].a, newA);
assert.deepEqual(next[0].b, newB);
});
test('setWallThickness upserts and removes', () => {
const a = [0, 0], b = [1, 0];
let walls = setWallThickness([], a, b, 12, pitch);
@@ -176,7 +195,11 @@ test('equal solid atomic pieces compact back to one whole-wall key', () => {
{ key: wallKey([5, 4], [5, 10], pitch), cm: 30 },
];
const next = normalizeWallIntervals(rooms, walls, [], pitch, cellCm, GRID_PITCH);
assert.deepEqual(next, [{ key: wallKey([5, 0], [5, 10], pitch), cm: 30 }]);
assert.equal(next.length, 1);
assert.equal(next[0].key, wallKey([5, 0], [5, 10], pitch));
assert.equal(next[0].cm, 30);
assert.deepEqual(next[0].a, [5, 0]);
assert.deepEqual(next[0].b, [5, 10]);
});
test('different solid thicknesses remain separate atomic keys', () => {
@@ -191,6 +214,33 @@ test('different solid thicknesses remain separate atomic keys', () => {
assert.ok(!next.some((w) => w.key === wallKey([5, 0], [5, 10], pitch)));
});
test('closing the sole geometric split preserves different thicknesses', () => {
const rooms = [
{ id: 'a', poly: [[0, 0], [5, 0], [5, 10], [0, 10]] },
{ id: 'b', poly: [[5, 0], [10, 0], [10, 10], [5, 10]] },
];
// No neighbour endpoint and no open cut remains at y=4. Exact endpoints in
// new wall entries are therefore the only record of this intentional break.
let walls = setWallThickness([], [5, 0], [5, 4], 20, pitch);
walls = setWallThickness(walls, [5, 4], [5, 10], 30, pitch);
const next = normalizeWallIntervals(rooms, walls, [], pitch, cellCm, GRID_PITCH);
assert.equal(next.length, 2);
assert.deepEqual(new Set(next.map((w) => w.cm)), new Set([20, 30]));
assert.ok(next.some((w) => w.a?.[1] === 4 || w.b?.[1] === 4));
});
test('exact endpoints do not prevent equal closed pieces from compacting', () => {
const rooms = [
{ id: 'a', poly: [[0, 0], [5, 0], [5, 10], [0, 10]] },
{ id: 'b', poly: [[5, 0], [10, 0], [10, 10], [5, 10]] },
];
let walls = setWallThickness([], [5, 0], [5, 4], 20, pitch);
walls = setWallThickness(walls, [5, 4], [5, 10], 20, pitch);
const next = normalizeWallIntervals(rooms, walls, [], pitch, cellCm, GRID_PITCH);
assert.equal(next.length, 1);
assert.equal(next[0].key, wallKey([5, 0], [5, 10], pitch));
});
// An open span that does NOT contain the parent edge's midpoint used to leave
// the key in place, so the wall body stayed solid straight across the passage.
test('open span away from the edge midpoint clears only its own interval', () => {