mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-29 03:09:36 +00:00
@@ -0,0 +1,69 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import test from 'node:test';
|
||||
|
||||
import {
|
||||
COORDINATE_DECIMALS,
|
||||
canonicalizeConfigGeometry,
|
||||
canonicalizeLayoutGeometry,
|
||||
canonicalizeNumber,
|
||||
canonicalizePosition,
|
||||
} from '../test-build/coordinate-canonicalization.js';
|
||||
|
||||
const fixture = JSON.parse(readFileSync(
|
||||
new URL('./fixtures/coordinate-canonicalization.json', import.meta.url),
|
||||
'utf8',
|
||||
));
|
||||
|
||||
test('frontend and backend share the nine-decimal fixture contract (#224)', () => {
|
||||
assert.equal(COORDINATE_DECIMALS, fixture.decimals);
|
||||
const configBefore = structuredClone(fixture.configInput);
|
||||
const layoutBefore = structuredClone(fixture.layoutInput);
|
||||
|
||||
const config = canonicalizeConfigGeometry(fixture.configInput);
|
||||
const layout = canonicalizeLayoutGeometry(fixture.layoutInput);
|
||||
|
||||
assert.deepEqual(config, fixture.configExpected);
|
||||
assert.deepEqual(layout, fixture.layoutExpected);
|
||||
assert.deepEqual(fixture.configInput, configBefore, 'config input is immutable');
|
||||
assert.deepEqual(fixture.layoutInput, layoutBefore, 'layout input is immutable');
|
||||
assert.deepEqual(canonicalizeConfigGeometry(config), config, 'config is idempotent');
|
||||
assert.deepEqual(canonicalizeLayoutGeometry(layout), layout, 'layout is idempotent');
|
||||
assert.equal(Object.is(layout['rl:poly'].x, -0), false, 'negative zero becomes positive');
|
||||
});
|
||||
|
||||
test('scalar canonicalization is symmetric and never snaps off-grid geometry (#224)', () => {
|
||||
assert.equal(canonicalizeNumber(1.2345678905), 1.234567891);
|
||||
assert.equal(canonicalizeNumber(-1.2345678905), -1.234567891);
|
||||
assert.equal(canonicalizeNumber(0.20833333333333334), 0.208333333);
|
||||
assert.ok(Math.abs(canonicalizeNumber(0.20833333333333334) - 0.20833333333333334) <= 5e-10);
|
||||
assert.equal(canonicalizeNumber(Number.NaN), Number.NaN);
|
||||
assert.equal(canonicalizeNumber(Number.POSITIVE_INFINITY), Number.POSITIVE_INFINITY);
|
||||
});
|
||||
|
||||
test('one position changes only x/y and preserves future metadata (#224)', () => {
|
||||
const input = {
|
||||
s: 'floor', x: 0.1000000006, y: -0.0000000004,
|
||||
k: 1.0000000004, nested: { numeric: 0.1234567896 },
|
||||
};
|
||||
assert.deepEqual(canonicalizePosition(input), {
|
||||
s: 'floor', x: 0.100000001, y: 0,
|
||||
k: 1.0000000004, nested: { numeric: 0.1234567896 },
|
||||
});
|
||||
});
|
||||
|
||||
test('frontend write paths adopt canonical candidates before persistence (#224)', () => {
|
||||
const source = readFileSync(new URL('../src/houseplan-card.ts', import.meta.url), 'utf8');
|
||||
assert.match(
|
||||
source,
|
||||
/const candidate = canonicalizeConfigGeometry\(this\._serverCfg\);[\s\S]*config: candidate/,
|
||||
);
|
||||
assert.match(
|
||||
source,
|
||||
/const pos = canonicalizePosition\(this\._layout\[id\]\);[\s\S]*device_id: id, pos/,
|
||||
);
|
||||
assert.match(
|
||||
source,
|
||||
/this\._layout = canonicalizeLayoutGeometry\(this\._layout\);[\s\S]*localStorage\.setItem/,
|
||||
);
|
||||
});
|
||||
+435
@@ -0,0 +1,435 @@
|
||||
{
|
||||
"decimals": 9,
|
||||
"configInput": {
|
||||
"spaces": [
|
||||
{
|
||||
"id": "floor",
|
||||
"title": "Floor",
|
||||
"cell_cm": 5.0000000004,
|
||||
"plan_aspect": 1.5000000004,
|
||||
"plan_x": 0.1234567894,
|
||||
"plan_y": -0.1234567896,
|
||||
"plan_scale": 1.0000000001,
|
||||
"plan_scale_x": 1.2345678906,
|
||||
"plan_scale_y": 0.9876543216,
|
||||
"plan_angle": 44.1234567896,
|
||||
"view_box": [0.0000000004, -0.0000000004, 1.0000000004, 1.0000000004],
|
||||
"settings": {
|
||||
"room_opacity": 0.5000000004,
|
||||
"temp_min": 18.1234567896,
|
||||
"temp_max": 28.1234567896,
|
||||
"card_font_scale": 1.0000000004
|
||||
},
|
||||
"rooms": [
|
||||
{
|
||||
"id": "poly",
|
||||
"name": "Poly",
|
||||
"area": null,
|
||||
"poly": [
|
||||
[0.0000000004, -0.0000000004],
|
||||
[0.3000000004, 0.0000000004],
|
||||
[0.0000000004, 0.3000000004]
|
||||
],
|
||||
"settings": {
|
||||
"name_scale": 1.0000000004,
|
||||
"label_scale": 1.2000000004
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "rect",
|
||||
"name": "Rect",
|
||||
"area": null,
|
||||
"x": 0.4000000004,
|
||||
"y": 0.4000000006,
|
||||
"w": 0.2000000004,
|
||||
"h": 0.2000000006
|
||||
}
|
||||
],
|
||||
"walls": [
|
||||
{
|
||||
"key": "wall",
|
||||
"cm": 15.0000000004,
|
||||
"a": [0.0000000004, 0.3000000004],
|
||||
"b": [0.3000000006, 0.3000000006]
|
||||
}
|
||||
],
|
||||
"openings": [
|
||||
{
|
||||
"id": "opening",
|
||||
"type": "door",
|
||||
"x": 0.5000000004,
|
||||
"y": 0.0000000004,
|
||||
"angle": -0.0000000004,
|
||||
"length": 0.2000000006,
|
||||
"host": {
|
||||
"kind": "partition",
|
||||
"id": "partition",
|
||||
"t": 0.5000000004
|
||||
}
|
||||
}
|
||||
],
|
||||
"decor": [
|
||||
{
|
||||
"id": "line",
|
||||
"kind": "line",
|
||||
"x1": 0.1000000004,
|
||||
"y1": 0.1000000006,
|
||||
"x2": 0.2000000004,
|
||||
"y2": 0.2000000006,
|
||||
"width_cm": 2.0000000004,
|
||||
"opacity": 0.7000000004
|
||||
},
|
||||
{
|
||||
"id": "rect-decor",
|
||||
"kind": "rect",
|
||||
"x": 0.1000000004,
|
||||
"y": 0.2000000006,
|
||||
"w": 0.3000000004,
|
||||
"h": 0.4000000006,
|
||||
"angle": 12.1234567896,
|
||||
"fill_opacity": 0.6000000004
|
||||
},
|
||||
{
|
||||
"id": "ellipse",
|
||||
"kind": "ellipse",
|
||||
"x": -0.1000000004,
|
||||
"y": -0.2000000006,
|
||||
"w": 0.3000000006,
|
||||
"h": 0.4000000004,
|
||||
"angle": -12.1234567896
|
||||
},
|
||||
{
|
||||
"id": "text",
|
||||
"kind": "text",
|
||||
"x": 0.7000000004,
|
||||
"y": 0.8000000006,
|
||||
"text": "Text",
|
||||
"scale": 1.2345678906,
|
||||
"angle": 2.1234567896,
|
||||
"size_cm": 25.0000000004
|
||||
},
|
||||
{
|
||||
"id": "chair",
|
||||
"kind": "furniture",
|
||||
"symbol": "chair",
|
||||
"x": 0.6000000004,
|
||||
"y": 0.6000000006,
|
||||
"w": 0.1000000004,
|
||||
"h": 0.1000000006,
|
||||
"angle": 90.1234567896
|
||||
}
|
||||
],
|
||||
"room_drafts": [
|
||||
{
|
||||
"id": "draft",
|
||||
"points": [
|
||||
[0.1000000004, 0.9000000006],
|
||||
[0.2000000006, 0.9000000004],
|
||||
[0.3000000004, 0.9000000006]
|
||||
],
|
||||
"segments": [
|
||||
{"cm": 10.0000000004},
|
||||
{"cm": 20.0000000004}
|
||||
]
|
||||
}
|
||||
],
|
||||
"partitions": [
|
||||
{
|
||||
"id": "partition",
|
||||
"a": [0.0000000004, 0.0000000004],
|
||||
"b": [1.0000000004, 0.0000000004],
|
||||
"cm": 12.0000000004
|
||||
}
|
||||
],
|
||||
"wall_columns": [
|
||||
{
|
||||
"id": "column",
|
||||
"shape": "square",
|
||||
"center": [0.7500000004, 0.7500000006],
|
||||
"cm": 30.0000000004,
|
||||
"angle": 45.1234567896
|
||||
}
|
||||
],
|
||||
"open_spans": [
|
||||
{
|
||||
"a": [0.3000000004, 0.3000000006],
|
||||
"b": [0.6000000006, 0.3000000004]
|
||||
}
|
||||
],
|
||||
"future": {
|
||||
"numeric": 0.1234567896
|
||||
}
|
||||
}
|
||||
],
|
||||
"markers": [
|
||||
{
|
||||
"id": "virtual",
|
||||
"binding": "virtual",
|
||||
"angle": -30.1234567896,
|
||||
"size": 1.1000000004,
|
||||
"ripple_size": 1.5000000004,
|
||||
"glow_radius_cm": 120.0000000004,
|
||||
"glow_color": {
|
||||
"c": "#112233",
|
||||
"bri": 0.5000000004
|
||||
},
|
||||
"vacuum": {
|
||||
"calibration": {
|
||||
"map": [
|
||||
-0.0017921234567,
|
||||
0.0000000004,
|
||||
1.0000000004,
|
||||
0.0017921234567,
|
||||
-0.0000000004,
|
||||
2.0000000004
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"settings": {
|
||||
"glow_radius_cm": 150.0000000004,
|
||||
"fill_colors": {
|
||||
"on": {
|
||||
"c": "#abcdef",
|
||||
"a": 0.5000000004
|
||||
}
|
||||
},
|
||||
"future_numeric": 0.9876543216
|
||||
},
|
||||
"future_root": {
|
||||
"numeric": 0.1234567896
|
||||
}
|
||||
},
|
||||
"configExpected": {
|
||||
"spaces": [
|
||||
{
|
||||
"id": "floor",
|
||||
"title": "Floor",
|
||||
"cell_cm": 5.0000000004,
|
||||
"plan_aspect": 1.5000000004,
|
||||
"plan_x": 0.123456789,
|
||||
"plan_y": -0.12345679,
|
||||
"plan_scale": 1,
|
||||
"plan_scale_x": 1.234567891,
|
||||
"plan_scale_y": 0.987654322,
|
||||
"plan_angle": 44.12345679,
|
||||
"view_box": [0.0000000004, -0.0000000004, 1.0000000004, 1.0000000004],
|
||||
"settings": {
|
||||
"room_opacity": 0.5000000004,
|
||||
"temp_min": 18.1234567896,
|
||||
"temp_max": 28.1234567896,
|
||||
"card_font_scale": 1.0000000004
|
||||
},
|
||||
"rooms": [
|
||||
{
|
||||
"id": "poly",
|
||||
"name": "Poly",
|
||||
"area": null,
|
||||
"poly": [
|
||||
[0, 0],
|
||||
[0.3, 0],
|
||||
[0, 0.3]
|
||||
],
|
||||
"settings": {
|
||||
"name_scale": 1.0000000004,
|
||||
"label_scale": 1.2000000004
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "rect",
|
||||
"name": "Rect",
|
||||
"area": null,
|
||||
"x": 0.4,
|
||||
"y": 0.400000001,
|
||||
"w": 0.2,
|
||||
"h": 0.200000001
|
||||
}
|
||||
],
|
||||
"walls": [
|
||||
{
|
||||
"key": "wall",
|
||||
"cm": 15.0000000004,
|
||||
"a": [0, 0.3],
|
||||
"b": [0.300000001, 0.300000001]
|
||||
}
|
||||
],
|
||||
"openings": [
|
||||
{
|
||||
"id": "opening",
|
||||
"type": "door",
|
||||
"x": 0.5,
|
||||
"y": 0,
|
||||
"angle": 0,
|
||||
"length": 0.200000001,
|
||||
"host": {
|
||||
"kind": "partition",
|
||||
"id": "partition",
|
||||
"t": 0.5
|
||||
}
|
||||
}
|
||||
],
|
||||
"decor": [
|
||||
{
|
||||
"id": "line",
|
||||
"kind": "line",
|
||||
"x1": 0.1,
|
||||
"y1": 0.100000001,
|
||||
"x2": 0.2,
|
||||
"y2": 0.200000001,
|
||||
"width_cm": 2.0000000004,
|
||||
"opacity": 0.7000000004
|
||||
},
|
||||
{
|
||||
"id": "rect-decor",
|
||||
"kind": "rect",
|
||||
"x": 0.1,
|
||||
"y": 0.200000001,
|
||||
"w": 0.3,
|
||||
"h": 0.400000001,
|
||||
"angle": 12.12345679,
|
||||
"fill_opacity": 0.6000000004
|
||||
},
|
||||
{
|
||||
"id": "ellipse",
|
||||
"kind": "ellipse",
|
||||
"x": -0.1,
|
||||
"y": -0.200000001,
|
||||
"w": 0.300000001,
|
||||
"h": 0.4,
|
||||
"angle": -12.12345679
|
||||
},
|
||||
{
|
||||
"id": "text",
|
||||
"kind": "text",
|
||||
"x": 0.7,
|
||||
"y": 0.800000001,
|
||||
"text": "Text",
|
||||
"scale": 1.234567891,
|
||||
"angle": 2.12345679,
|
||||
"size_cm": 25.0000000004
|
||||
},
|
||||
{
|
||||
"id": "chair",
|
||||
"kind": "furniture",
|
||||
"symbol": "chair",
|
||||
"x": 0.6,
|
||||
"y": 0.600000001,
|
||||
"w": 0.1,
|
||||
"h": 0.100000001,
|
||||
"angle": 90.12345679
|
||||
}
|
||||
],
|
||||
"room_drafts": [
|
||||
{
|
||||
"id": "draft",
|
||||
"points": [
|
||||
[0.1, 0.900000001],
|
||||
[0.200000001, 0.9],
|
||||
[0.3, 0.900000001]
|
||||
],
|
||||
"segments": [
|
||||
{"cm": 10.0000000004},
|
||||
{"cm": 20.0000000004}
|
||||
]
|
||||
}
|
||||
],
|
||||
"partitions": [
|
||||
{
|
||||
"id": "partition",
|
||||
"a": [0, 0],
|
||||
"b": [1, 0],
|
||||
"cm": 12.0000000004
|
||||
}
|
||||
],
|
||||
"wall_columns": [
|
||||
{
|
||||
"id": "column",
|
||||
"shape": "square",
|
||||
"center": [0.75, 0.750000001],
|
||||
"cm": 30.0000000004,
|
||||
"angle": 45.12345679
|
||||
}
|
||||
],
|
||||
"open_spans": [
|
||||
{
|
||||
"a": [0.3, 0.300000001],
|
||||
"b": [0.600000001, 0.3]
|
||||
}
|
||||
],
|
||||
"future": {
|
||||
"numeric": 0.1234567896
|
||||
}
|
||||
}
|
||||
],
|
||||
"markers": [
|
||||
{
|
||||
"id": "virtual",
|
||||
"binding": "virtual",
|
||||
"angle": -30.12345679,
|
||||
"size": 1.1000000004,
|
||||
"ripple_size": 1.5000000004,
|
||||
"glow_radius_cm": 120.0000000004,
|
||||
"glow_color": {
|
||||
"c": "#112233",
|
||||
"bri": 0.5000000004
|
||||
},
|
||||
"vacuum": {
|
||||
"calibration": {
|
||||
"map": [
|
||||
-0.0017921234567,
|
||||
0.0000000004,
|
||||
1.0000000004,
|
||||
0.0017921234567,
|
||||
-0.0000000004,
|
||||
2.0000000004
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"settings": {
|
||||
"glow_radius_cm": 150.0000000004,
|
||||
"fill_colors": {
|
||||
"on": {
|
||||
"c": "#abcdef",
|
||||
"a": 0.5000000004
|
||||
}
|
||||
},
|
||||
"future_numeric": 0.9876543216
|
||||
},
|
||||
"future_root": {
|
||||
"numeric": 0.1234567896
|
||||
}
|
||||
},
|
||||
"layoutInput": {
|
||||
"virtual": {
|
||||
"s": "floor",
|
||||
"x": 0.1234567894,
|
||||
"y": -0.1234567896,
|
||||
"k": 1.2000000004,
|
||||
"future_numeric": 0.9876543216
|
||||
},
|
||||
"rl:poly": {
|
||||
"s": "floor",
|
||||
"x": -0.0000000004,
|
||||
"y": 0.7500000006,
|
||||
"k": 1.5000000004
|
||||
}
|
||||
},
|
||||
"layoutExpected": {
|
||||
"virtual": {
|
||||
"s": "floor",
|
||||
"x": 0.123456789,
|
||||
"y": -0.12345679,
|
||||
"k": 1.2000000004,
|
||||
"future_numeric": 0.9876543216
|
||||
},
|
||||
"rl:poly": {
|
||||
"s": "floor",
|
||||
"x": 0,
|
||||
"y": 0.750000001,
|
||||
"k": 1.5000000004
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
import {
|
||||
polygonSegments, splitAtIntersections, visibilityPolygon,
|
||||
} from '../test-build/light-visibility.js';
|
||||
import { canonicalizeConfigGeometry } from '../test-build/coordinate-canonicalization.js';
|
||||
|
||||
const closeTo = (got, want, tol = 1e-6) =>
|
||||
assert.ok(Math.abs(got - want) <= tol, `expected ${want}, got ${got}`);
|
||||
@@ -162,6 +163,28 @@ test('six-room ULP topology keeps a complete visible floor and is permutation-st
|
||||
closeTo(geometryArea(direct), 0.31835083680549986, 1e-9);
|
||||
});
|
||||
|
||||
test('write canonicalization cleans the six-room #218 topology before union', () => {
|
||||
const config = {
|
||||
spaces: [{
|
||||
rooms: noisySixRoomFloor.map((poly, index) => ({
|
||||
id: `r${index}`, name: `Room ${index}`, area: null, poly,
|
||||
})),
|
||||
}],
|
||||
markers: [],
|
||||
settings: {},
|
||||
};
|
||||
const clean = canonicalizeConfigGeometry(config);
|
||||
const outlines = clean.spaces[0].rooms.map((room) => room.poly);
|
||||
|
||||
assert.equal(outlines[0][0][0], outlines[0][2][0]);
|
||||
assert.equal(outlines[2][2][1], outlines[3][0][1]);
|
||||
assert.ok(unionBodies(outlines), 'canonical persisted rooms have a valid union');
|
||||
const fan = [[-0.1, -0.1], [1.1, -0.1], [1.1, 1.1], [-0.1, 1.1]];
|
||||
assert.ok(intersectionPaths([fan], outlines).length > 0, 'Glow clip remains non-empty');
|
||||
assert.deepEqual(config.spaces[0].rooms.map((room) => room.poly), noisySixRoomFloor,
|
||||
'write canonicalization never mutates the editor candidate');
|
||||
});
|
||||
|
||||
test('one malformed room is diagnosed and cannot erase healthy lit floor', () => {
|
||||
const fan = [[-1, -1], [6, -1], [6, 3], [-1, 3]];
|
||||
const healthy = [[3, 0], [5, 0], [5, 2], [3, 2]];
|
||||
|
||||
Reference in New Issue
Block a user