"""Direct room/list -> bbox handoff, without requiring an asset resolver. GLB tests decode the published binary convention independently of the exporter. They intentionally do not use trimesh or treat a proxy box as a furniture mesh. """ from copy import deepcopy import hashlib import itertools import json import math from pathlib import Path import struct import xml.etree.ElementTree as ET import pytest from fastfill.v2.direct_layout import ( export_handoff, layout_to_roomgenbench, layout_to_scene, request_to_condition, write_bbox_glb, ) from fastfill.v2.schema import validate_condition def request(): return {"room_type": "living_room", "room_size_m": [5., 4., 2.8], "furniture_list": ["sofa", {"category": "chair", "count": 2}, {"id": "lamp", "category": "lamp", "description": "reading lamp"}]} def layout(condition, *, yaw=0.): return {"schema_version": "fastfill.v2", "objects": [ {"id": obj["id"], "target_size_local_m": [1.4, .7, .5 + index], "bottom_center_m": [2., 1.5, .2 * index], "yaw_rad": yaw} for index, obj in enumerate(condition["objects"])]} def corners(position, size, yaw): """Independent full-extent, bottom-center OBB reference.""" c, s = math.cos(yaw), math.sin(yaw) w, d, h = size return [[position[0] + c * x - s * y, position[1] + s * x + c * y, position[2] + z] for x, y, z in itertools.product((-w / 2, w / 2), (-d / 2, d / 2), (0, h))] def corner_set(values): return sorted(tuple(round(float(coordinate), 6) for coordinate in point) for point in values) def read_glb(path): data = Path(path).read_bytes() assert len(data) >= 28 magic, version, length = struct.unpack_from(" .02) assert all(obj["support_status"] != "declared" for obj in downstream["objects"]) @pytest.mark.parametrize("yaw", [0., math.pi / 2, math.radians(97), -math.pi]) def test_glb_is_self_contained_and_applies_full_extent_bottom_center_z_up_to_y_up(tmp_path, yaw): condition = request_to_condition(request()) prediction = layout(condition, yaw=yaw) scene = layout_to_scene(condition, prediction) output = tmp_path / "layout.glb" write_bbox_glb(output, scene) document, binary = read_glb(output) assert not document.get("images") assert not document.get("textures") for obj in prediction["objects"]: actual = node_vertices(document, binary, obj["id"]) expected = [[x, z, -y] for x, y, z in corners(obj["bottom_center_m"], obj["target_size_local_m"], yaw)] # Vertices may be duplicated for flat face normals; compare unique corners. assert sorted(set(corner_set(actual))) == sorted(set(corner_set(expected))) saved = output.read_bytes() with pytest.raises(FileExistsError): write_bbox_glb(output, scene) assert output.read_bytes() == saved def test_handoff_exports_without_catalog_or_support_validation_and_preserves_raw_prediction(tmp_path): condition = request_to_condition(request()) prediction = layout(condition) # Deliberately outside the room: diagnostics may reject it, but never shrink/delete it. prediction["objects"][0]["bottom_center_m"] = [-4., 1., .4] saved_condition, saved_prediction = deepcopy(condition), deepcopy(prediction) directory = tmp_path / "handoff" export_handoff(directory, condition, prediction) assert {"scene.json", "roomgenbench_scene.json", "layout.glb", "preview.svg", "diagnostics.json"} <= {p.name for p in directory.iterdir()} scene = json.loads((directory / "scene.json").read_text()) assert scene == layout_to_scene(condition, prediction) assert scene["objects"][0]["bottom_center_m"] == [-4., 1., .4] assert len(scene["objects"]) == len(prediction["objects"]) json.loads((directory / "diagnostics.json").read_text()) assert ET.parse(directory / "preview.svg").getroot().tag == "{http://www.w3.org/2000/svg}svg" read_glb(directory / "layout.glb") assert condition == saved_condition and prediction == saved_prediction snapshots = {p.name: p.read_bytes() for p in directory.iterdir()} with pytest.raises(FileExistsError): export_handoff(directory, condition, prediction) assert {p.name: p.read_bytes() for p in directory.iterdir()} == snapshots def test_invalid_handoff_is_rejected_before_any_directory_is_created(tmp_path): condition = request_to_condition(request()) prediction = layout(condition) prediction["objects"][0]["target_size_local_m"] = [0., 1., 1.] directory = tmp_path / "invalid" with pytest.raises(ValueError): export_handoff(directory, condition, prediction) assert not directory.exists() def test_source_data_paths_are_protected_for_direct_exports(): condition = request_to_condition(request()) prediction = layout(condition) with pytest.raises(ValueError): export_handoff(Path("/Volumes/harddisk/3D_Room_Collections/new-handoff"), condition, prediction) with pytest.raises(ValueError): write_bbox_glb(Path("/Volumes/harddisk/3D_Room_Collections/new-layout.glb"), layout_to_scene(condition, prediction)) def test_prediction_cli_accepts_minimal_request_and_direct_handoff_without_assets(tmp_path): from fastfill.v2.batch import TinyTokenizer from fastfill.v2.model import ModelConfig, build_model from fastfill.v2.predict import main checkpoint = tmp_path / "model" build_model(ModelConfig(backbone="tiny", lora_rank=0, decoder_dim=16, decoder_heads=2, decoder_layers=1, tiny_hidden_size=16)).save_pretrained(checkpoint) TinyTokenizer().save_pretrained(tmp_path / "tokenizer") request_path = tmp_path / "request.json" request_path.write_text(json.dumps(request())) output = tmp_path / "prediction.json" handoff = tmp_path / "handoff" main(["--checkpoint", str(checkpoint), "--request", str(request_path), "--output", str(output), "--export-dir", str(handoff), "--device", "cpu"]) prediction = json.loads(output.read_text()) assert {obj["id"] for obj in prediction["objects"]} == {"obj_0000", "obj_0001", "obj_0002", "lamp"} scene = json.loads((handoff / "scene.json").read_text()) assert scene == layout_to_scene(request_to_condition(request()), prediction) read_glb(handoff / "layout.glb") def test_svg_escapes_markup_and_filters_xml_controls_without_changing_json_semantics(tmp_path): source = {"room_type": "living_room\u0001 ", "category": "chair\u0001 <>&", "description": "oak <>& chair"}]} condition = request_to_condition(source) prediction = layout(condition) handoff = tmp_path / "xml-safe" export_handoff(handoff, condition, prediction) scene = json.loads((handoff / "scene.json").read_text()) assert scene["room"]["room_type"] == source["room_type"] assert scene["objects"][0]["id"] == source["furniture_list"][0]["id"] assert scene["objects"][0]["category"] == source["furniture_list"][0]["category"] assert scene["objects"][0]["description"] == source["furniture_list"][0]["description"] svg = (handoff / "preview.svg").read_text() tree = ET.fromstring(svg) assert "\u0001" not in svg assert {element.tag.rsplit("}", 1)[-1] for element in tree.iter()} <= {"svg", "rect", "text", "polygon", "title"} text = " ".join(element.text or "" for element in tree.iter()) assert "