"""Scale for models (e.g. H0 = 1:87): floor plan and measurements in the original's size, and the original's size in Gaussian XR (worldScale). The model is the synthetic 4 x 3 m room built 87 times smaller, so the expected numbers are simply those of the full-size room.""" import json import subprocess import sys import time import pytest from conftest import BASE, ROOT, RUNS, server import synth H0 = 87 def plan(*runs, ratio=None): out = runs[0].parent / "plan.png" cmd = [sys.executable, str(ROOT / "scripts" / "floorplan.py"), str(out), *[f"{r}={r.name}" for r in runs]] if ratio: cmd += ["--ratio", str(ratio)] r = subprocess.run(cmd, capture_output=True, text=True) assert r.returncode == 0, r.stderr return json.loads(r.stdout.strip().splitlines()[-1]) def test_a_model_has_the_floor_plan_of_the_original(): full = plan(synth.make_room(BASE / "full", "Room", "A", error=(0.0, (0, 0, 0)))) model = plan(synth.make_room(BASE / "model", "Room", "A", error=(0.0, (0, 0, 0)), scale=1 / H0), ratio=H0) assert model["scenes"][0]["size"] == pytest.approx(full["scenes"][0]["size"], abs=0.05) # 4 x 3 m assert model["pixels_per_metre"] == pytest.approx(full["pixels_per_metre"], rel=0.01) # measuring in metres assert model["ratio"] == H0 def test_a_model_without_its_scale_still_draws(): info = plan(synth.make_room(BASE / "model", "Room", "A", error=(0.0, (0, 0, 0)), scale=1 / H0)) assert info["ratio"] == 1 @pytest.mark.parametrize("scale", ["0.5", "2000", "abc", "nan"]) def test_scales_out_of_range_are_refused(api, scale): synth.make_room(RUNS, "Layout", "A") status, r = api.post("/api/scale", {"slug": "Layout", "scale": scale}) assert status == 400 and r["error"]["code"] in ("err.range", "err.bad_value") def test_setting_and_clearing_the_scale_of_a_scene(api): synth.make_room(RUNS, "Layout", "A") assert api.post("/api/scale", {"slug": "Layout", "scale": "87", "xr": True})[0] == 200 s = {x["slug"]: x for x in api.get("/api/scenes")[1]}["Layout"] assert (s["scale"], s["scale_xr"]) == (87, True) assert api.post("/api/scale", {"slug": "Layout", "scale": "1", "xr": True})[0] == 200 s = {x["slug"]: x for x in api.get("/api/scenes")[1]}["Layout"] assert (s["scale"], s["scale_xr"]) == (1, False) # original size: nothing to enlarge in XR assert api.post("/api/scale", {"slug": "Nope", "scale": "87"})[0] == 400 def test_a_group_passes_its_scale_on(api): synth.make_room(RUNS, "Station", "A", recording="2026-10-12_16-40-00_stereo.mp4") synth.make_room(RUNS, "Yard", "B", recording="2026-10-12_16-43-00_stereo.mp4") synth.make_room(RUNS, "Other", "A", recording="2026-10-03_19-00-00_stereo.mp4") server.save_group("", "Layout", "Station,Yard") assert api.post("/api/scale", {"slug": "Layout", "scale": "160", "with_scenes": True})[0] == 200 scales = {x["slug"]: x["scale"] for x in api.get("/api/scenes")[1]} assert scales == {"Station": 160, "Yard": 160, "Other": 1} assert api.get("/api/groups")[1]["groups"][0]["scale"] == 160 api.post("/api/scale", {"slug": "Layout", "scale": "87"}) # without its scenes this time assert {x["slug"]: x["scale"] for x in api.get("/api/scenes")[1]}["Station"] == 160 def test_the_floor_plan_endpoint_measures_in_the_originals_metres(api): synth.make_room(RUNS, "Layout", "A", error=(0.0, (0, 0, 0)), scale=1 / H0) api.post("/api/scale", {"slug": "Layout", "scale": str(H0)}) status, png = api.get("/api/floorplan?slugs=Layout&style=classic") assert status == 200 and png[:4] == b"\x89PNG" full = plan(synth.make_room(BASE / "full", "Room", "A", error=(0.0, (0, 0, 0)))) info = server.floorplan(["Layout"], "classic", "en", "")[1] assert info["pixels_per_metre"] == pytest.approx(full["pixels_per_metre"], rel=0.01) def wait_upload(slug): end = time.time() + 120 while time.time() < end: u = server.upload_status(slug) if u and not u["running"]: return u time.sleep(0.2) raise TimeoutError @pytest.mark.parametrize("xr,expected", [(True, H0), (False, 1.0)]) def test_world_scale_on_the_frame(api, frame, xr, expected): run = synth.make_room(RUNS, "Layout", "A") api.post("/api/scale", {"slug": "Layout", "scale": str(H0), "xr": xr}) api.post("/api/upload", {"slug": "Layout"}) assert wait_upload("Layout")["ok"] sent = json.loads((frame / server.FRAME_SCENES / "20_Layout" / "layout.xrSceneTransform.json").read_text()) assert sent["worldScale"] == expected assert sent["transform"] == json.loads((run / "scene.xrSceneTransform.json").read_text())["transform"] assert json.loads((run / "scene.xrSceneTransform.json").read_text())["worldScale"] == 1.0 # the original stays def test_changing_the_scale_updates_the_frame(api, frame): synth.make_room(RUNS, "Layout", "A") api.post("/api/upload", {"slug": "Layout"}) wait_upload("Layout") assert api.post("/api/scale", {"slug": "Layout", "scale": "45", "xr": True, "update_frame": True})[0] == 200 assert wait_upload("Layout")["ok"] sent = json.loads((frame / server.FRAME_SCENES / "20_Layout" / "layout.xrSceneTransform.json").read_text()) assert sent["worldScale"] == 45 def test_a_tour_of_a_model_is_walkable_in_both_versions(): synth.make_room(RUNS, "Station", "A", recording="2026-10-12_16-40-00_stereo.mp4") synth.make_room(RUNS, "Yard", "B", recording="2026-10-12_16-43-00_stereo.mp4") server.save_group("", "Layout", "Station,Yard") server.set_scale("Layout", H0, xr=True, members=True, update_frame=False) assert server.start_combine({"slug": "Layout", "full": True}) is None from conftest import wait_job assert wait_job()["result"] == "ok" tour = RUNS / "Layout" assert json.loads((tour / "layout.xrSceneTransform.json").read_text())["worldScale"] == H0 assert json.loads((tour / "full" / "layout-full.xrSceneTransform.json").read_text())["worldScale"] == H0 def test_the_floor_plan_of_a_group_in_the_groups_scale(api): synth.make_room(RUNS, "Station", "A", error=(0.0, (0, 0, 0)), scale=1 / H0) server.save_group("", "Layout", "Station") api.post("/api/scale", {"slug": "Layout", "scale": str(H0)}) # only on the group, not passed on assert {x["slug"]: x["scale"] for x in api.get("/api/scenes")[1]}["Station"] == 1 full = plan(synth.make_room(BASE / "full", "Room", "A", error=(0.0, (0, 0, 0)))) info = server.floorplan(["Station"], "classic", "en", "", "Layout")[1] assert info["ratio"] == H0 assert info["scenes"][0]["size"] == pytest.approx(full["scenes"][0]["size"], abs=0.05) # 4 x 3 m status, png = api.get("/api/floorplan?slugs=Station&style=classic&group=Layout") assert status == 200 and png[:4] == b"\x89PNG" assert server.floorplan(["Station"], "classic", "en", "", "Station")["code"] == "err.bad_request" # not a group def test_a_floor_plan_of_scenes_in_different_scales_is_refused(api): synth.make_room(RUNS, "Station", "A", recording="2026-10-12_16-40-00_stereo.mp4") synth.make_room(RUNS, "Yard", "B", recording="2026-10-12_16-43-00_stereo.mp4") server.save_group("", "Layout", "Station,Yard") api.post("/api/scale", {"slug": "Station", "scale": "87"}) api.post("/api/scale", {"slug": "Yard", "scale": "160"}) status, r = api.get("/api/floorplan?slugs=Station,Yard&group=Layout") assert status == 400 and r["error"]["code"] == "err.mixed_scale" api.post("/api/scale", {"slug": "Layout", "scale": "87"}) # one scale for the group decides assert api.get("/api/floorplan?slugs=Station,Yard&style=classic&group=Layout")[0] == 200