Files
houseplan-card/tests_backend/test_coordinate_canonicalization_pure.py
T
Sergey Matyunin d03a68b88a feat: добавить лестницы между этажами (#663)
Прямые и винтовые лестницы получили отдельную модель, инструменты редактора, безопасную межэтажную навигацию, вычитание из чистой площади и плоское отображение в 2.5D.

Issue: #663
User-Visible: yes
2026-09-26 21:00:43 +03:00

125 lines
4.4 KiB
Python

"""HA-independent coordinate canonicalisation contracts (#224/#431/#440)."""
from __future__ import annotations
import copy
import json
import math
from pathlib import Path
from custom_components.houseplan.coordinate_canonicalization import (
COORDINATE_DECIMALS,
DECOR_BOX_KINDS,
LATTICE_GRID_N,
LATTICE_NOISE_STEPS,
canonicalize_config_geometry,
canonicalize_lattice_coordinate,
canonicalize_layout_geometry,
canonicalize_number,
)
FIXTURE = (
Path(__file__).parents[1]
/ "test"
/ "fixtures"
/ "coordinate-canonicalization.json"
)
def _fixture() -> dict:
return json.loads(FIXTURE.read_text(encoding="utf-8"))
def test_python_and_frontend_share_the_scalar_lattice_fixture_contract() -> None:
fixture = _fixture()
config_input = fixture["configInput"]
layout_input = fixture["layoutInput"]
config_before = copy.deepcopy(config_input)
layout_before = copy.deepcopy(layout_input)
assert COORDINATE_DECIMALS == fixture["decimals"]
config = canonicalize_config_geometry(config_input)
layout = canonicalize_layout_geometry(layout_input)
assert config == fixture["configExpected"]
assert layout == fixture["layoutExpected"]
assert config_input == config_before
assert layout_input == layout_before
assert canonicalize_config_geometry(config) == config
assert canonicalize_layout_geometry(layout) == layout
assert math.copysign(1.0, layout["rl:poly"]["x"]) == 1.0
def test_decor_box_catalog_matches_shared_contract() -> None:
fixture = _fixture()
assert list(DECOR_BOX_KINDS) == fixture["boxKinds"]
input_decor = fixture["configInput"]["spaces"][0]["decor"]
input_boxes = [item for item in input_decor if item["kind"] in DECOR_BOX_KINDS]
assert sorted(item["kind"] for item in input_boxes) == sorted(fixture["boxKinds"])
result = canonicalize_config_geometry(fixture["configInput"])
output_by_id = {item["id"]: item for item in result["spaces"][0]["decor"]}
expected_by_id = {
item["id"]: item for item in fixture["configExpected"]["spaces"][0]["decor"]
}
for source in input_boxes:
actual = output_by_id[source["id"]]
expected = expected_by_id[source["id"]]
for field in ("x", "y", "w", "h", "angle"):
assert actual[field] == expected[field]
assert actual[field] != source[field]
source_image = next(item for item in input_boxes if item["kind"] == "image")
image = output_by_id["image"]
for field in ("asset_id", "opacity", "flip_h", "flip_v", "future"):
assert image[field] == source_image[field]
def test_scalar_contract_is_symmetric_and_keeps_off_grid_geometry() -> None:
assert canonicalize_number(1.2345678905) == 1.234567891
assert canonicalize_number(-1.2345678905) == -1.234567891
value = 0.20833333333333334
assert canonicalize_number(value) == 0.208333333
assert abs(canonicalize_number(value) - value) <= 5e-10
assert math.isnan(canonicalize_number(float("nan")))
assert canonicalize_number(float("inf")) == float("inf")
def test_stair_transform_keeps_continuous_wall_face_magnet_position() -> None:
stair = {
"id": "stair",
"kind": "straight",
"direction": "forward",
"target_space_id": "upper",
"x": 0.5000001,
"y": 0.3999999,
"length": 0.2000001,
"width": 0.0999999,
"angle": 17.1234567896,
}
output = canonicalize_config_geometry(
{"spaces": [{"id": "ground", "stairs": [stair]}]}
)["spaces"][0]["stairs"][0]
assert output == {
**stair,
"angle": 17.12345679,
}
def test_all_4801_lattice_nodes_and_nine_decimal_forms_share_exact_bits() -> None:
assert LATTICE_GRID_N == 240
assert LATTICE_NOISE_STEPS == 1e-4
for index in range(-2400, 2401):
node = index / LATTICE_GRID_N
nine_decimal = float(f"{node:.9f}")
assert canonicalize_lattice_coordinate(node) == node
assert canonicalize_lattice_coordinate(nine_decimal) == node
assert canonicalize_lattice_coordinate(
canonicalize_lattice_coordinate(nine_decimal)
) == node
assert math.copysign(1.0, canonicalize_lattice_coordinate(-0.0)) == 1.0
assert canonicalize_lattice_coordinate(0.06) == 0.06
assert canonicalize_lattice_coordinate(0.2875) == 0.2875
assert canonicalize_lattice_coordinate((1 + 0.999e-4) / 240) == 1 / 240
assert canonicalize_lattice_coordinate((1 + 1.001e-4) / 240) == 0.004167084