mirror of
https://github.com/thunderheronight/splat-workshop.git
synced 2026-10-17 14:01:33 +02:00
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
152 lines
7.6 KiB
Python
152 lines
7.6 KiB
Python
"""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
|