mirror of
https://github.com/spoopyghosty0/frameport.git
synced 2026-10-23 10:01:46 +02:00
The agent records Valve's passthrough depth mesh (IVRBlockQueue + IVRPaths, Background/Overlay OpenVR app), converts poses with the final raw->standing transform, shows pre-rendered prompts as a SteamVR overlay, then builds the room with fp_roomscan (room_capture / room_capture_status). PC: rooms.scan, Frame page progress, frameport frame room scan. FrameBridge reads the optional wall label (INVISIBLE_WALL_FACE). ROOM_FORMAT: outline orientation stated exactly.
171 lines
6.8 KiB
Python
171 lines
6.8 KiB
Python
"""RoomView capture (agent) and the PC's scan flow, without SteamVR: pose math, the final raw->STAGE rewrite of a
|
|
dump, prompt choice, the worker's status file, and rooms.scan's polling."""
|
|
import json
|
|
import math
|
|
import struct
|
|
from pathlib import Path
|
|
from types import SimpleNamespace
|
|
|
|
import pytest
|
|
from test_agent import load_agent
|
|
|
|
from frameport.install import rooms
|
|
|
|
|
|
def rot_y(deg, t=(0.0, 0.0, 0.0)):
|
|
c, s = math.cos(math.radians(deg)), math.sin(math.radians(deg))
|
|
return [c, 0, s, t[0], 0, 1, 0, t[1], -s, 0, c, t[2]]
|
|
|
|
|
|
def test_m34_mul_and_quat(monkeypatch, tmp_path):
|
|
a = load_agent(monkeypatch, tmp_path)
|
|
m = a._m34_mul(rot_y(90, (0, 1.6, 0)), rot_y(0, (1, 0, 0)))
|
|
assert m[3] == pytest.approx(0, abs=1e-6) and m[7] == pytest.approx(1.6) and m[11] == pytest.approx(-1)
|
|
x, y, z, w = a._m34_quat(rot_y(90))
|
|
assert (x, z) == (pytest.approx(0), pytest.approx(0))
|
|
assert y == pytest.approx(math.sin(math.radians(45))) and w == pytest.approx(math.cos(math.radians(45)))
|
|
for deg in (0, 179.9, -120): # every branch of the matrix -> quaternion conversion stays a unit rotation
|
|
q = a._m34_quat(rot_y(deg))
|
|
assert sum(v * v for v in q) == pytest.approx(1)
|
|
|
|
|
|
def test_prompt_choice(monkeypatch, tmp_path):
|
|
a = load_agent(monkeypatch, tmp_path)
|
|
assert a._prompt_for(None, set(), {"up": 0}, False) == "start"
|
|
assert a._prompt_for(1.0, {0, 1, 2}, {"up": 0}, True) == "look_03"
|
|
assert a._prompt_for(1.0, set(range(12)), {"up": 0}, True) == "ceiling"
|
|
assert a._prompt_for(1.0, set(range(12)), {"up": 9}, True) == "look_12"
|
|
|
|
|
|
def test_view_coverage_bins(monkeypatch, tmp_path):
|
|
a = load_agent(monkeypatch, tmp_path)
|
|
bins, pitch = set(), {"up": 0, "down": 0}
|
|
for deg in range(15, 360, 30): # mid-bin: exact multiples of 30 sit on bin edges
|
|
a._view_coverage(rot_y(deg), bins, pitch)
|
|
assert len(bins) == 12 and pitch == {"up": 0, "down": 0}
|
|
up = [1, 0, 0, 0, 0, 0, 1, 0, 0, -1, 0, 0] # mesh +Z (third column) pointing up
|
|
a._view_coverage(up, bins, pitch)
|
|
assert pitch["up"] == 1
|
|
|
|
|
|
class FakeReader:
|
|
"""Two fresh mesh blocks, then nothing; raw->standing moves the floor by +0.5 m at the end."""
|
|
|
|
def __init__(self, app_type=None):
|
|
import ctypes
|
|
|
|
self.ct = ctypes
|
|
self.blocks = [(1, rot_y(0, (0, 1.0, 0))), (2, rot_y(90, (0, 1.0, 0)))]
|
|
self.verts = ctypes.create_string_buffer(struct.pack("<9f", *range(9)))
|
|
self.idx = ctypes.create_string_buffer(struct.pack("<3I", 0, 1, 2))
|
|
self.data, self.layout = "data", "layout"
|
|
self.current = None
|
|
|
|
def up_to_date(self):
|
|
return True
|
|
|
|
def acquire(self, queue, read_type, timeout_ms):
|
|
if queue == "layout":
|
|
return SimpleNamespace(value="layout"), self.ct.addressof(self.idx)
|
|
if not self.blocks:
|
|
return None, None
|
|
self.current = self.blocks.pop(0)
|
|
return SimpleNamespace(value="data"), self.ct.addressof(self.verts)
|
|
|
|
def prop(self, root, path, fmt):
|
|
return {"/meshData/meshId": self.current[0] if self.current else None, "/meshData/layoutId": 5,
|
|
"/meshData/elementCount": 9, "/meshData/rawFromMeshPose": tuple(self.current[1]),
|
|
"/meshLayout/indexCount": 3, "/meshLayout/layoutId": 5}.get(path)
|
|
|
|
def release(self, queue, blk):
|
|
pass
|
|
|
|
def raw_to_standing(self):
|
|
return rot_y(0, (0, 0.5, 0))
|
|
|
|
def close(self):
|
|
pass
|
|
|
|
|
|
def test_capture_rewrites_poses_with_final_transform(monkeypatch, tmp_path):
|
|
a = load_agent(monkeypatch, tmp_path)
|
|
monkeypatch.setattr(a, "_RoomViewReader", FakeReader)
|
|
out = tmp_path / "scan.fprs"
|
|
clock = iter(i * 0.5 for i in range(1000))
|
|
monkeypatch.setattr(a.time, "time", lambda: next(clock))
|
|
res = a.capture_roomview(3, str(out), max_hz=100)
|
|
assert res["frames"] == 2
|
|
data = out.read_bytes()
|
|
assert data[:8] == b"FPRS\x01\0\0\0"
|
|
hdr = struct.calcsize(a.FPRS_FRAME)
|
|
t, px, py, pz, qx, qy, qz, qw, vc, ic = struct.unpack_from(a.FPRS_FRAME, data, 8)
|
|
assert (vc, ic) == (3, 3) and py == pytest.approx(1.5) # raw y 1.0 + the final floor shift 0.5
|
|
second = 8 + hdr + 3 * 12 + 3 * 4
|
|
_, _, py2, _, qx2, qy2, qz2, qw2, _, _ = struct.unpack_from(a.FPRS_FRAME, data, second)
|
|
assert py2 == pytest.approx(1.5) and qy2 == pytest.approx(math.sin(math.radians(45)))
|
|
assert "ceiling not seen: look up" in res["warnings"]
|
|
|
|
|
|
def test_capture_worker_status(monkeypatch, tmp_path):
|
|
a = load_agent(monkeypatch, tmp_path)
|
|
monkeypatch.setattr(a, "capture_roomview", lambda *args, **kw: {"frames": 0, "warnings": []})
|
|
a.room_capture_worker(json.dumps({"seconds": 10, "wait": 0}))
|
|
st = a.cmd_room_capture_status({})
|
|
assert st["state"] == "failed" and "passthrough" in st["error"]
|
|
|
|
built = {"report": {"walls": 4}, "room": {"universe": 7}}
|
|
monkeypatch.setattr(a, "capture_roomview", lambda *args, **kw: {"frames": 12, "warnings": []})
|
|
monkeypatch.setattr(a, "cmd_room_build", lambda args: built)
|
|
a.room_capture_worker(json.dumps({"seconds": 10, "wait": 0}))
|
|
st = a.cmd_room_capture_status({})
|
|
assert st["state"] == "done" and st["report"] == {"walls": 4}
|
|
|
|
|
|
class ScanFrame:
|
|
home = "/fake-frame"
|
|
|
|
def __init__(self, states):
|
|
self.states = list(states)
|
|
self.calls = []
|
|
self.puts = []
|
|
self.sums = ""
|
|
|
|
def agent(self, cmd, **kw):
|
|
self.calls.append((cmd, kw))
|
|
if cmd == "room_capture_status":
|
|
return self.states.pop(0)
|
|
return {"started": True}
|
|
|
|
def run(self, cmd):
|
|
return 0, self.sums, ""
|
|
|
|
def put(self, local, remote, resume=True):
|
|
self.puts.append((Path(local).name, remote))
|
|
|
|
|
|
def test_scan_polls_until_done(monkeypatch):
|
|
monkeypatch.setattr(rooms, "ensure_scanner", lambda frame: "x")
|
|
monkeypatch.setattr(rooms, "ensure_prompts", lambda frame: "x")
|
|
frame = ScanFrame([{"state": "waiting"}, {"state": "recording", "directions": 4}, {"state": "done", "room": {}}])
|
|
seen = []
|
|
st = rooms.scan(frame, seconds=30, wait=60, progress=seen.append, sleep=lambda s: None)
|
|
assert st["state"] == "done" and [s["state"] for s in seen] == ["waiting", "recording", "done"]
|
|
assert frame.calls[0] == ("room_capture", {"seconds": 30, "wait": 60})
|
|
|
|
frame = ScanFrame([{"state": "failed", "error": "No passthrough view arrived"}])
|
|
with pytest.raises(RuntimeError, match="No passthrough"):
|
|
rooms.scan(frame, sleep=lambda s: None)
|
|
|
|
|
|
def test_ensure_prompts_uploads_only_changed(monkeypatch, tmp_path):
|
|
import hashlib
|
|
|
|
(tmp_path / "roomscan").mkdir()
|
|
for name in ("a.png", "b.png"):
|
|
(tmp_path / "roomscan" / name).write_bytes(name.encode())
|
|
monkeypatch.setattr(rooms, "artifacts_dir", lambda: tmp_path)
|
|
frame = ScanFrame([])
|
|
frame.sums = hashlib.sha256(b"a.png").hexdigest() + " a.png\n" + "0" * 64 + " b.png\n"
|
|
rooms.ensure_prompts(frame)
|
|
assert [p[0] for p in frame.puts] == ["b.png"]
|