fallside-office-Zhening — how it was made

← back to reconstruction

① One-shot authoring

model claude-fable-5 · 111 tool calls · 29m33s · cost $20.653031
👁INSPECT #1
I'll start by reading `Room.md` and `Room.py` in full, then the head-on stitches.
📄 Read /scratch2/code/LiteReality_to_share/agen📄 Read /scratch2/code/LiteReality_to_share/agen📄 Read /scratch2/code/LiteReality_to_share/agen📄 Read /scratch2/code/LiteReality_to_share/agen📄 Read /scratch2/code/LiteReality_to_share/agen📄 Read /scratch2/code/LiteReality_to_share/agen📄 Read /scratch2/code/LiteReality_to_share/agen📄 Read /scratch2/code/LiteReality_to_share/agen🔧 ToolSearch select:mcp__cap__render,mcp__cap__fetch_⌘ Bash⌘ Bash📄 Read /scratch2/code/LiteReality_to_share/agen⌘ Bash🎞 select_views Wall2🎞 select_views Wall3⌘ Bash⌘ Bash📄 Read /tmp/contact_office.jpg⌘ Bash📄 Read /tmp/f05.jpg📄 Read /tmp/f10.jpg📄 Read /tmp/f17.jpg📄 Read /tmp/f30.jpg📄 Read /tmp/f37.jpg📄 Read /tmp/f44.jpg⌘ Bash📄 Read /tmp/f44_part.jpg
✏️EDIT #2
Two glass columns between door and corner with a central mullion, three frosted rows over a solid wood base, and a clear transom at top. Checking fram
📄 Read /tmp/f43.jpg⌘ Bash📄 Read /tmp/f19.jpg📄 Read /tmp/f21.jpg🎨 fetch_material blue office carpet tiles🎨 fetch_material white mineral fiber office ceiling tile🎨 fetch_material golden oak wood veneer fine grain🎨 fetch_material light beech wood board📄 Read /scratch2/code/LiteReality_to_share/agen⌘ Bash⌘ Bash⌘ Bash✏️ Edit /scratch2/code/LiteReality_to_share/agen⌘ Bash
STEP #3
Compiles. Now the first render pass — ortho-vs-stitch for every surface:
⌘ Bash📄 Read /tmp/ref1/Wall2_compare.png📄 Read /tmp/ref1/Wall1_compare.png⌘ Bash📄 Read /tmp/ref1/Wall2_compare.png⌘ Bash📄 Read /tmp/ref1/Wall2_render_big.png📄 Read /tmp/ref1/Wall2_real_big.png📄 Read /tmp/ref1/Wall1_render_big.png📄 Read /tmp/ref1/Wall1_real_big.png
STEP #4
Wall1 matches well — monitor left-of-center, fan and lamp at the right, boards and standards aligned. Checking Wall0 and Wall3:
📄 Read /tmp/ref1/Wall0_render_big.png📄 Read /tmp/ref1/Wall0_real_big.png📄 Read /tmp/ref1/Wall0_frame_00046.png
STEP #5
Frame 46 shows good agreement — whiteboard, coat rack, door, partition, carpet all read right. Checking the left end of Wall0 (Storage0 area):
📄 Read /tmp/ref1/Wall0_frame_00016.png
✏️EDIT #6
The left shelving renders via Storage0's GLB and reads close to real. Now Wall3, floor, and ceiling:
📄 Read /tmp/ref1/Wall3_render_big.png📄 Read /tmp/ref1/Wall3_real_big.png📄 Read /tmp/ref1/Floor0_compare.png📄 Read /tmp/ref1/Ceiling0_compare.png📄 Read /scratch2/code/LiteReality_to_share/agen✏️ Edit /scratch2/code/LiteReality_to_share/agen✏️ Edit /scratch2/code/LiteReality_to_share/agen✏️ Edit /scratch2/code/LiteReality_to_share/agen✏️ Edit /scratch2/code/LiteReality_to_share/agen✏️ Edit /scratch2/code/LiteReality_to_share/agen✏️ Edit /scratch2/code/LiteReality_to_share/agen⌘ Bash⌘ Bash📄 Read /tmp/ref2/Wall2_render_big.png📄 Read /tmp/ref2/Ceiling0_compare.png
✏️EDIT #7
Checking whether my edits actually took effect (whiteboard width changed?) — suspicious that the glass/grid look identical:
📄 Read /tmp/ref2/Wall0_render_big.png✏️ Edit /scratch2/code/LiteReality_to_share/agen✏️ Edit /scratch2/code/LiteReality_to_share/agen📄 Read /tmp/ref2/Wall2_frame_00014.png📄 Read /tmp/ref2/Wall1_frame_00030.png✏️ Edit /scratch2/code/LiteReality_to_share/agen✏️ Edit /scratch2/code/LiteReality_to_share/agen⌘ Bash⌘ Bash📄 Read /tmp/ref3/Ceiling0_render_big.png📄 Read /tmp/ref3/Floor0_render_big.png
👁INSPECT #8
Grids now align with the walls and the LED panel reads. Final per-wall verification pass on the round-3 renders:
⌘ Bash📄 Read /tmp/ref3/Wall0_render_big.png📄 Read /tmp/ref3/Wall1_render_big.png📄 Read /tmp/ref3/Wall2_render_big.png📄 Read /tmp/ref3/Wall3_render_big.png⌘ Bash📄 Read /tmp/scene1/scene_frame_00021.png📄 Read /tmp/scene1/scene_frame_00046.png⚖️ critic🎨 fetch_material golden oak wood veneer fine grain🎨 fetch_material blue office carpet tiles
✏️EDIT #9
Now the coat rack and note-cluster tweaks:
✏️ Edit /scratch2/code/LiteReality_to_share/agen✏️ Edit /scratch2/code/LiteReality_to_share/agen⌘ Bash📄 Read /tmp/scene2/scene_frame_00044.png📄 Read /tmp/scene2/scene_frame_00019.png⌘ Bash📄 Read /tmp/f44_render_big.png📄 Read /tmp/w0f46_big.png⚖️ critic✏️ Edit /scratch2/code/LiteReality_to_share/agen✏️ Edit /scratch2/code/LiteReality_to_share/agen✏️ Edit /scratch2/code/LiteReality_to_share/agen⌘ Bash⌘ Bash📄 Read /tmp/ref5/Wall0_frame_00046.png📄 Read /tmp/ref5/Wall3_frame_00020.png✏️ Write /home/zelda/zh340/.claude/projects/-scra⌘ Bash⌘ Bash
STEP #10
Done. `Room.py` now carries the full agent-authored harness layer for fallside-office-Zhening, verified per wall against the rectified photo reference

Authoring — model summary

Done. `Room.py` now carries the full agent-authored harness layer for fallside-office-Zhening, verified per wall against the rectified photo references. The file compiles and rebuilt successfully in every render pass (6 headless Blender builds). ## Shell structure + materials (job 1) The scanned SHELL was **correct** — 4-wall room, door on Wall2 at offset 0.438 (I cross-checked against raw frames because the pre-made Wall1/Wall2 stitches turned out to be **mirrored**; the coat-rack-next-to-door in frame 5 settled it). No wall/opening edits were needed, and no furniture boxes were touched. Materials: - **Floor0** — recoloured blue carpet-tile PBR + a 0.5 m two-tone checker, rotated −26.29° so tiles align with the walls, matte roughness. - **Ceiling0** — new mesh (the blank build has none) with white suspended-grid material (fetched tile relief, brown diffuse unlinked, 0.6 m seam grid, wall-aligned). - **Walls** — white paint; partition glass gets bright frosted (mild emission) and glowing clear transom, since the corridor beyond is lit in every photo. ## Fixtures per wall (job 2) — all built via `group_fixture`, each hides as one unit - **Wall0** — `Whiteboard0` (4 alu frame bars + recessed gloss face + pen tray + tray lip + pink eraser + marker + 4 sticky notes), `CoatRack0` (beech rail + 3 alu hooks, each plate+peg+upturned tip). Verified on ortho + frame 46: board size/position match, hooks read as pegs not a slab. - **Wall1** — `Shelf1` (6 slotted standards + brackets per board + two 3.5 m beech boards + ~17 items: desk fan with cage/blades, articulated lamp, telephone, binders, tins, cup, boxes), `Monitor0` (base+neck+screen+bezel standing on the upper board), `Trunking1` (channel base + proud lid) + `Sockets1` (6 UK doubles, each plate+outlets+rockers). Verified on ortho + frame 30. - **Wall2** — `Partition0` (3 full-height oak posts, head rail, door head rail, solid oak base panel, 3 horizontal rails, 6 frosted panes in 2 columns, clear transom panes incl. over the door, thermometer strip on the mullion), `Heater0` (body + 13-fin loop + top grille + feet on the floor). Verified on ortho + frames 14/44 — the frame-44 side-by-side is a near match. - **Wall3** — `Blinds0` (headrail + two banks of 14/13 cream slats with the photographed open middle), `Sill0` (beech board + lip), `Radiator0` (back panel + 14-fin loop + top grille + end caps + valves/pipes), `Trunking3` + `Sockets3`. Verified on ortho + frames 19/20. - **Ceiling** — `CeilingLight0` (rece ✓ [3/6] one-shot authoring · 29m34s ⊘ [4/6] per-object refinement — skipped (SKIP_REFINE=1) ⊘ [5/6] per-wall fixture refinement — skipped (REFINE_WALLS=0) ▶ [6/6] export three.js viewer (0 stages left) === RoomScene.build (blank) === shell <- SHELL(compact): 4 walls, 2 openings, 6 objects(无 usdz) thickened 4 walls outward to ~0.1 m cut 2/2 openings {'Door0': 'Wall2', 'Window0': 'Wall3'} 13:14:31 | INFO: Data are loaded, start creating Blender stuff 13:14:31 | INFO: Blender create Mesh node Cube 13:14:31 | INFO: Blender create Mesh node Cube.006 13:14:31 | INFO: glTF import finished in 0.00s 13:14:31 | INFO: Data are loaded, start creating Blender stuff 13:14:31 | INFO: Blender create Mesh node Cube 13:14:31 | INFO: glTF import finished in 0.00s 13:14:31 | INFO: Data are loaded, start creating Blender stuff 13:14:31 | INFO: Blender create Mesh node Cube 13:14:31 | INFO: Blender create Mesh node Cube.010 13:14:31 | INFO: Blender create Mesh node Cube.015 13:14:31 | INFO: Blender create Mesh node Cube.001 13:14:31 | INFO: Blender create Mesh node Cube.002 13:14:31 | INFO: Blender create Mesh node Cube.006 13:14:31 | INFO: glTF import finished in 0.01s 13:14:31 | INFO: Data are loaded, start creating Blender stuff 13:14:31 | INFO: Blender create Mesh node Cube 13:14:31 | INFO: Blender create Mesh node Cube.017 13:14:31 | INFO: Blender create Mesh node Cube.007 13:14:31 | INFO: Blender create Mesh node Cube.008 13:14:31 | INFO: glTF import finished in 0.00s 13:14:31 | INFO: Data are loaded, start creating Blender stuff 13:14:31 | INFO: Blender create Mesh node Cube 13:14:31 | INFO: Blender create Mesh node Cube.005 13:14:31 | INFO: Blender create Mesh node Cube.006 13:14:31 | INFO: Blender create Mesh node Cube.013 13:14:31 | INFO: Blender create Mesh node Cube.004 13:14:31 | INFO: Blender create Mesh node Cube.003 13:14:31 | INFO: Blender create Mesh node Cube.020 13:14:31 | INFO: glTF import finished in 0.01s 13:14:31 | INFO: Data are loaded, start creating Blender stuff 13:14:31 | INFO: Blender create Mesh node geometry_0 13:14:31 | INFO: glTF import finished in 0.01s 13:14:31 | INFO: Data are loaded, start creating Blender stuff 13:14:31 | INFO: Blender create Mesh node geometry_0 13:14:31 | INFO: glTF import finished in 0.01s 13:14:31 | INFO: Data are loaded, start creating Blender stuff 13:14:31 | INFO: Blender create Mesh node geometry_0 13:14:31 | INFO: glTF import finished in 0.01s placed 8 asset instances built 0 capture cameras indexed 13 objects, 0 cameras floor-grounded 0 visible piece(s) harness: ceiling + materials + fixtures built === ROOM SCENE (blank) === blinds : Blinds0 ceiling : Ceiling0 ceiling_light: CeilingLight0 chair : Chair0, Chair1, Chair2 coat_rack: CoatRack0 door : Door0 floor : Floor0 heater : Heater0 monitor : Monitor0 partition: Partition0 radiator : Radiator0 shelf : Shelf1 sill : Sill0 socket : Sockets1, Sockets3 storage : Storage0 table : Table0, Table1 trunking : Trunking1, Trunking3 wall : Wall0, Wall1, Wall2, Wall3 whiteboard: Whiteboard0 window : Window0 placeable surfaces (top z): Shelf1 z=1.083 center=[-1.4965, -0.4129, 0.3006] Storage0 z=0.453 center=[0.4869, 1.6528, 0.1891] Table0 z=-0.693 center=[-1.2042, -0.3552, -1.0623] Table1 z=-0.700 center=[0.2825, 1.5681, -1.0655] cameras: none INFO Draco mesh compression is available, use library at /scratch2/zh340/blender-4.5.1-linux-x64/4.5/scripts/addons_core/io_scene_gltf2/libextern_draco.so 13:14:31 | INFO: Starting glTF 2.0 export 13:14:31 | INFO: Extracting primitive: Floor0 13:14:31 | INFO: Primitives created: 1 13:14:31 | INFO: Extracting primitive: Wall0 13:14:31 | INFO: Primitives created: 1 13:14:31 | INFO: Extracting primitive: Wall1 13:14:31 | INFO: Primitives created: 1 13:14:31 | INFO: Extracting primitive: Wall2 13:14:31 | INFO: Primitives created: 1 13:14:31 | INFO: Extracting primitive: Wall3 13:14:31 | INFO: Primitives created: 1 13:14:31 | INFO: Extracting primitive: Ceiling0 13:14:31 | INFO: Primitives created: 1 13:14:31 | INFO: Extracting primitive: Cube 13:14:31 | WARNING: More than one shader node tex image used for a texture. The resulting glTF sampler will behave like the first shader node tex image. 13:14:31 | INFO: Primitives created: 1 13:14:31 | INFO: Extracting primitive: Cube.006 13:14:31 | INFO: Primitives created: 1 13:14:31 | INFO: Extracting primitive: Cube.001 13:14:31 | WARNING: More than one shader node tex image used for a texture. The resulting glTF sampler will behave like the first shader node tex image. 13:14:31 | INFO: Primitives created: 2 13:14:31 | INFO: Extracting primitive: Cube.002 13:14:32 | WARNING: More than one shader node tex image used for a texture. The resulting glTF sampler will behave like the first shader node tex image. 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Cube.010 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Cube.015 13:14:32 | INFO: Primitives created: 2 13:14:32 | INFO: Extracting primitive: Cube.003 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Cube.004 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Cube.005 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: geometry_0 13:14:32 | WARNING: More than one shader node tex image used for a texture. The resulting glTF sampler will behave like the first shader node tex image. 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: geometry_0.001 13:14:32 | WARNING: More than one shader node tex image used for a texture. The resulting glTF sampler will behave like the first shader node tex image. 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: geometry_0.002 13:14:32 | WARNING: More than one shader node tex image used for a texture. The resulting glTF sampler will behave like the first shader node tex image. 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Cube.007 13:14:32 | WARNING: More than one shader node tex image used for a texture. The resulting glTF sampler will behave like the first shader node tex image. 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Cube.017 13:14:32 | INFO: Primitives created: 4 13:14:32 | INFO: Extracting primitive: Cube.008 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Cube.009 13:14:32 | INFO: Primitives created: 2 13:14:32 | INFO: Extracting primitive: Plane 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Cube.011 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Cube.012 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Cube.013 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Cube.014 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Cube.016 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Cube.018 13:14:32 | WARNING: More than one shader node tex image used for a texture. The resulting glTF sampler will behave like the first shader node tex image. 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Cube.020 13:14:32 | INFO: Primitives created: 3 13:14:32 | INFO: Extracting primitive: Wall1_trunk_base 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall1_trunk_lid 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall3_trunk_base 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall3_trunk_lid 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall1_sock_outlet 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall1_sock_outlet.001 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall1_sock_outlet.002 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall1_sock_outlet.003 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall1_sock_outlet.004 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall1_sock_outlet.005 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall1_sock_outlet.006 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall1_sock_outlet.007 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall1_sock_outlet.008 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall1_sock_outlet.009 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall1_sock_outlet.010 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall1_sock_outlet.011 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall1_sock_plate 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall1_sock_plate.001 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall1_sock_plate.002 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall1_sock_plate.003 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall1_sock_plate.004 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall1_sock_plate.005 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall1_sock_rocker 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall1_sock_rocker.001 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall1_sock_rocker.002 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall1_sock_rocker.003 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall1_sock_rocker.004 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall1_sock_rocker.005 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall1_sock_rocker.006 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall1_sock_rocker.007 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall1_sock_rocker.008 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall1_sock_rocker.009 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall1_sock_rocker.010 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall1_sock_rocker.011 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall3_sock_outlet 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall3_sock_outlet.001 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall3_sock_plate 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall3_sock_rocker 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall3_sock_rocker.001 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall0_wb_eraser 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall0_wb_face 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall0_wb_frame_b 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall0_wb_frame_s 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall0_wb_frame_s.001 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall0_wb_frame_t 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall0_wb_marker 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall0_wb_note 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall0_wb_note.001 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall0_wb_note.002 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall0_wb_note.003 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall0_wb_tray 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall0_wb_tray_lip 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall0_hook_peg 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall0_hook_peg.001 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall0_hook_peg.002 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall0_hook_plate 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall0_hook_plate.001 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall0_hook_plate.002 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall0_hook_tip 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall0_hook_tip.001 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall0_hook_tip.002 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall0_rack_rail 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall1_board 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall1_board.001 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall1_bracket 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall1_bracket.001 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall1_bracket.002 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall1_bracket.003 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall1_bracket.004 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall1_bracket.005 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall1_bracket.006 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall1_bracket.007 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall1_bracket.008 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall1_bracket.009 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall1_bracket.010 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall1_bracket.011 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall1_item 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall1_item.001 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall1_item.002 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall1_item.003 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall1_item.004 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall1_item.005 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall1_item.006 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall1_item.007 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall1_item.008 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall1_item.009 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall1_item.010 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall1_item.011 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall1_item.012 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall1_item.013 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall1_item.014 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall1_item.015 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall1_item.016 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall1_item.017 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall1_item.018 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall1_item.019 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall1_item.020 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall1_std 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall1_std.001 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall1_std.002 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall1_std.003 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall1_std.004 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall1_std.005 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall1_mon_base 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall1_mon_bezel 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall1_mon_neck 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall1_mon_screen 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall2_base_panel 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall2_clear 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall2_clear.001 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall2_door_head 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall2_frost 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall2_frost.001 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall2_frost.002 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall2_frost.003 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall2_frost.004 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall2_frost.005 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall2_glass_door_t 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall2_head_rail 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall2_post 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall2_post.001 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall2_post.002 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall2_rail 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall2_rail.001 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall2_rail.002 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall2_thermometer 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall2_htr_body 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall2_htr_fin 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall2_htr_fin.001 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall2_htr_fin.002 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall2_htr_fin.003 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall2_htr_fin.004 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall2_htr_fin.005 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall2_htr_fin.006 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall2_htr_fin.007 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall2_htr_fin.008 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall2_htr_fin.009 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall2_htr_fin.010 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall2_htr_fin.011 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall2_htr_fin.012 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall2_htr_foot 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall2_htr_foot.001 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall2_htr_grille 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall3_sill_board 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall3_sill_lip 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall3_blind_rail 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall3_slat 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall3_slat.001 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall3_slat.002 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall3_slat.003 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall3_slat.004 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall3_slat.005 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall3_slat.006 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall3_slat.007 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall3_slat.008 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall3_slat.009 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall3_slat.010 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall3_slat.011 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall3_slat.012 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall3_slat.013 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall3_slat.014 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall3_slat.015 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall3_slat.016 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall3_slat.017 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall3_slat.018 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall3_slat.019 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall3_slat.020 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall3_slat.021 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall3_slat.022 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall3_slat.023 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall3_slat.024 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall3_slat.025 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall3_slat.026 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall3_rad_back 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall3_rad_cap 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall3_rad_cap.001 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall3_rad_fin 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall3_rad_fin.001 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall3_rad_fin.002 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall3_rad_fin.003 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall3_rad_fin.004 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall3_rad_fin.005 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall3_rad_fin.006 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall3_rad_fin.007 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall3_rad_fin.008 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall3_rad_fin.009 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall3_rad_fin.010 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall3_rad_fin.011 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall3_rad_fin.012 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall3_rad_fin.013 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall3_rad_grille 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall3_rad_pipe 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall3_rad_pipe.001 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall3_rad_valve 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: Wall3_rad_valve.001 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: CeilLight_face 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: CeilLight_trim 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: CeilLight_trim.001 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: CeilLight_trim.002 13:14:32 | INFO: Primitives created: 1 13:14:32 | INFO: Extracting primitive: CeilLight_trim.003 13:14:32 | INFO: Primitives created: 1 13:14:33 | INFO: Finished glTF 2.0 export in 1.2181146144866943 s EXPORTED -> /scratch2/code/LiteReality_to_share/agentic_litereality/output/fallside-office-Zhening/scene_stage/_oneshot/room_preview/Room.glb (19226 KB) LAYOUT -> /scratch2/code/LiteReality_to_share/agentic_litereality/output/fallside-office-Zhening/scene_stage/_oneshot/room_preview/room_layout.json (28 objects, 0 cameras) Info: Saved as "Room.blend" BLEND -> /scratch2/code/LiteReality_to_share/agentic_litereality/output/fallside-office-Zhening/scene_stage/_oneshot/room_preview/Room.blend (30667 KB) COPIED -> /scratch2/code/LiteReality_to_share/agentic_litereality/output/fallside-office-Zhening/scene_stage/_oneshot/room_preview/build_room.py DONE Blender 4.5.1 LTS (hash b0a72b245dcf built 2025-07-29 06:24:35) Blender quit === 物化 6 个物体 === tex Storage0: 重建 3 张贴图 regen Storage0 (object.py) tex Table0: 重建 3 张贴图 regen Table0 (object.py) tex Table1: 重建 4 张贴图 regen Table1 (object.py) tex Wall2_Door_0: 重建 4 张贴图 regen Wall2_Door_0 (object.py) tex Wall3_Window_0: 重建 3 张贴图 regen Wall3_Window_0 (object.py) wrap ChairCluster0 (object.py) === 装配(无 frames(仅几何),外壳来自 Room.py 的 SHELL)-> Room.glb + Room.blend === 物化 6/6 个物体 Room_preview -> /scratch2/code/LiteReality_to_share/agentic_litereality/output/fallside-office-Zhening/scene_stage/_oneshot/room_preview viewer → /scratch2/code/LiteReality_to_share/agentic_litereality/final_results/fallside-office-Zhening/fallside-office-Zhening.html (12.0 MB, self-contained) final_results → /scratch2/code/LiteReality_to_share/agentic_litereality/final_results/fallside-office-Zhening ✓ [6/6] export three.js viewer · 7s ╭────────── fallside-office-Zhening ──────────╮ ✓ init (skipped) 0s ✓ surface stitches 32s ✓ one-shot authoring 29m34s ✓ obj refine (skipped) 0s ✓ wall refine (skipped) 0s ✓ export three.js viewer 7s total 30m14s viewer /scratch2/code/LiteReality_to_share/agentic_litereality/final_results/fallside-office-Zhening/fallside-office-Zhening.html room /scratch2/code/LiteReality_to_share/agentic_litereality/output/fallside-office-Zhening/scene_stage/_oneshot/room/Room.py ╰────────────────────────────────────────────────╯

② Code changes

Room.py diffseed (blank shell) → authored
+639 −0 lines · seed → authored Room.py
@@ -1159,8 +1159,647 @@
# ============================================================================
+# HARNESS LAYER (agent-authored): ceiling + surface materials + wall fixtures
+# ============================================================================
+# Everything below reads SHELL (defined at the bottom) and runs AFTER the blank
+# build, so local mesh coords == world metres. All fixture boxes are metric
+# meshes (size baked into vertices) placed with rotation+translation only, so
+# Object-space texture coords stay metric for box-projected PBR textures.
+
+MATERIALS_DIR = os.path.join(HERE, "materials")
+
+
+def _flat(name, srgb, rough=0.5, metal=0.0):
+ mat = bpy.data.materials.get(name)
+ if mat is not None:
+ return mat
+ mat = bpy.data.materials.new(name)
+ mat.use_nodes = True
+ nt = mat.node_tree
+ nt.nodes.clear()
+ out = nt.nodes.new("ShaderNodeOutputMaterial")
+ b = nt.nodes.new("ShaderNodeBsdfPrincipled")
+ nt.links.new(b.outputs["BSDF"], out.inputs["Surface"])
+ b.inputs["Base Color"].default_value = (*srgb_to_linear(srgb), 1.0)
+ b.inputs["Roughness"].default_value = rough
+ if "Metallic" in b.inputs:
+ b.inputs["Metallic"].default_value = metal
+ return mat
+
+
+def _pbr_mat(name, base, scale=1.0, rough_fallback=0.8, normal_strength=1.0):
+ """PBR from materials/<base>_{diff,rough,normal}.jpg, BOX-projected in
+ Object space (meshes here carry metric local coords, so scale = tiles/m)."""
+ mat = bpy.data.materials.get(name)
+ if mat is not None:
+ return mat
+ mat = bpy.data.materials.new(name)
+ mat.use_nodes = True
+ nt = mat.node_tree
+ nt.nodes.clear()
+ out = nt.nodes.new("ShaderNodeOutputMaterial")
+ b = nt.nodes.new("ShaderNodeBsdfPrincipled")
+ nt.links.new(b.outputs["BSDF"], out.inputs["Surface"])
+ b.inputs["Roughness"].default_value = rough_fallback
+ tc = nt.nodes.new("ShaderNodeTexCoord")
+ mp = nt.nodes.new("ShaderNodeMapping")
+ mp.inputs["Scale"].default_value = (scale, scale, scale)
+ nt.links.new(tc.outputs["Object"], mp.inputs["Vector"])
+
+ def img(suffix, noncolor):
+ p = os.path.join(MATERIALS_DIR, f"{base}_{suffix}.jpg")
+ if not os.path.exists(p):
+ return None
+ node = nt.nodes.new("ShaderNodeTexImage")
+ node.image = bpy.data.images.load(p, check_existing=True)
+ node.projection = "BOX"
+ node.projection_blend = 0.2
+ if noncolor:
+ node.image.colorspace_settings.name = "Non-Color"
+ nt.links.new(mp.outputs["Vector"], node.inputs["Vector"])
+ return node
+
+ d = img("diff", False)
+ if d:
+ nt.links.new(d.outputs["Color"], b.inputs["Base Color"])
+ r = img("rough", True)
+ if r:
+ nt.links.new(r.outputs["Color"], b.inputs["Roughness"])
+ nm = img("normal", True)
+ if nm:
+ nmap = nt.nodes.new("ShaderNodeNormalMap")
+ nmap.inputs["Strength"].default_value = normal_strength
+ nt.links.new(nm.outputs["Color"], nmap.inputs["Color"])
+ nt.links.new(nmap.outputs["Normal"], b.inputs["Normal"])
+ return mat
+
+
+def _mix_rgb(nt):
+ """(node, fac_in, colA_in, colB_in, col_out) across Blender 3.x/4.x."""
+ try:
+ n = nt.nodes.new("ShaderNodeMix")
+ n.data_type = "RGBA"
+ return n, n.inputs["Factor"], n.inputs[6], n.inputs[7], n.outputs[2]
+ except Exception:
+ n = nt.nodes.new("ShaderNodeMixRGB")
+ return n, n.inputs["Fac"], n.inputs["Color1"], n.inputs["Color2"], n.outputs["Color"]
+
+
+def _ceiling_mat():
+ """White mineral tile + a dark seam line every 0.6 m (suspended T-grid look)."""
+ name = "Ceiling_Tiles"
+ if bpy.data.materials.get(name):
+ return bpy.data.materials[name]
+ mat = _pbr_mat(name, "ceiling_tile", scale=1.2, rough_fallback=0.9)
+ nt = mat.node_tree
+ b = next(n for n in nt.nodes if n.type == "BSDF_PRINCIPLED")
+ # the fetched diffuse is brown plaster — keep only its relief; tiles are WHITE
+ dl = next((l for l in nt.links if l.to_node == b and l.to_socket.name == "Base Color"), None)
+ if dl is not None:
+ nt.links.remove(dl)
+ tc = next(n for n in nt.nodes if n.type == "TEX_COORD")
+ rot = nt.nodes.new("ShaderNodeMapping") # align the T-grid with the walls
+ rot.inputs["Rotation"].default_value = (0.0, 0.0, math.radians(-26.29))
+ nt.links.new(tc.outputs["Object"], rot.inputs["Vector"])
+ sep = nt.nodes.new("ShaderNodeSeparateXYZ")
+ nt.links.new(rot.outputs["Vector"], sep.inputs["Vector"])
+
+ def seam(axis_out):
+ add = nt.nodes.new("ShaderNodeMath")
+ add.operation = "ADD" # shift positive: MODULO is fmod (sign-keeping)
+ add.inputs[1].default_value = 30.0
+ nt.links.new(axis_out, add.inputs[0])
+ mod = nt.nodes.new("ShaderNodeMath")
+ mod.operation = "MODULO"
+ mod.inputs[1].default_value = 0.6
+ nt.links.new(add.outputs[0], mod.inputs[0])
+ lt = nt.nodes.new("ShaderNodeMath")
+ lt.operation = "LESS_THAN"
+ lt.inputs[1].default_value = 0.014
+ nt.links.new(mod.outputs[0], lt.inputs[0])
+ return lt
+
+ mx = nt.nodes.new("ShaderNodeMath")
+ mx.operation = "MAXIMUM"
+ nt.links.new(seam(sep.outputs["X"]).outputs[0], mx.inputs[0])
+ nt.links.new(seam(sep.outputs["Y"]).outputs[0], mx.inputs[1])
+ mixn, fac, colA, colB, colout = _mix_rgb(nt)
+ colB.default_value = (0.55, 0.55, 0.54, 1.0)
+ colA.default_value = (0.93, 0.93, 0.91, 1.0)
+ nt.links.new(mx.outputs[0], fac)
+ nt.links.new(colout, b.inputs["Base Color"])
+ return mat
+
+
+def _floor_mat():
+ """Blue carpet tiles: recoloured tile PBR + a 0.5 m two-tone checker (the real
+ floor alternates lighter/darker blue tiles) + matte carpet roughness."""
+ name = "Floor_Carpet"
+ if bpy.data.materials.get(name):
+ return bpy.data.materials[name]
+ mat = _pbr_mat(name, "floor_carpet", scale=0.333, rough_fallback=0.95)
+ nt = mat.node_tree
+ b = next(n for n in nt.nodes if n.type == "BSDF_PRINCIPLED")
+ rl = next((l for l in nt.links if l.to_node == b and l.to_socket.name == "Roughness"), None)
+ if rl is not None: # carpet is matte — drop the glossy tile roughness map
+ nt.links.remove(rl)
+ b.inputs["Roughness"].default_value = 0.95
+ tc = next(n for n in nt.nodes if n.type == "TEX_COORD")
+ mp = next(n for n in nt.nodes if n.type == "MAPPING")
+ mp.inputs["Rotation"].default_value = (0.0, 0.0, math.radians(-26.29)) # align w/ walls
+ rot = nt.nodes.new("ShaderNodeMapping") # rotation-only coords for the checker
+ rot.inputs["Rotation"].default_value = (0.0, 0.0, math.radians(-26.29))
+ nt.links.new(tc.outputs["Object"], rot.inputs["Vector"])
+ ck = nt.nodes.new("ShaderNodeTexChecker")
+ ck.inputs["Scale"].default_value = 2.0 # object coords -> 0.5 m cells
+ ck.inputs["Color1"].default_value = (1.0, 1.0, 1.0, 1.0)
+ ck.inputs["Color2"].default_value = (0.62, 0.67, 0.78, 1.0)
+ nt.links.new(rot.outputs["Vector"], ck.inputs["Vector"])
+ diff_link = next(
+ (l for l in nt.links if l.to_node == b and l.to_socket.name == "Base Color"), None
+ )
+ mixn, fac, colA, colB, colout = _mix_rgb(nt)
+ try:
+ mixn.blend_type = "MULTIPLY"
+ except AttributeError:
+ pass
+ fac.default_value = 1.0
+ if diff_link is not None:
+ nt.links.new(diff_link.from_socket, colA)
+ else:
+ colA.default_value = (0.29, 0.39, 0.50, 1.0)
+ nt.links.new(ck.outputs["Color"], colB)
+ nt.links.new(colout, b.inputs["Base Color"])
+ return mat
+
+
+def _harness_mats():
+ clear = _flat("Glass_Clear", (0.93, 0.95, 0.97), rough=0.06)
+ frost = _flat("Glass_Frosted", (0.94, 0.95, 0.94), rough=0.07)
+ for mat, glow in ((clear, 0.7), (frost, 0.22)):
+ try: # glow: the corridor beyond the partition is LIT in every photo
+ cb = next(n for n in mat.node_tree.nodes if n.type == "BSDF_PRINCIPLED")
+ cb.inputs["Emission Color"].default_value = (0.95, 0.96, 1.0, 1.0)
+ cb.inputs["Emission Strength"].default_value = glow
+ except (StopIteration, KeyError):
+ pass
+ return {
+ "paint": _flat("Wall_Paint", (0.93, 0.92, 0.885), rough=0.92),
+ "trunk": _flat("Trunking_White", (0.94, 0.94, 0.91), rough=0.5),
+ "white": _flat("White_Plastic", (0.96, 0.96, 0.95), rough=0.45),
+ "wmetal": _flat("White_Metal", (0.95, 0.95, 0.94), rough=0.4),
+ "alu": _flat("Aluminium", (0.78, 0.79, 0.80), rough=0.35, metal=1.0),
+ "dark": _flat("Dark_Grey", (0.22, 0.22, 0.23), rough=0.6),
+ "black": _flat("Matte_Black", (0.05, 0.05, 0.05), rough=0.55),
+ "face": _flat("Whiteboard_Face", (0.965, 0.965, 0.958), rough=0.15),
+ "frost": frost,
+ "clear": clear,
+ "blind": _flat("Blind_Cream", (0.91, 0.88, 0.74), rough=0.85),
+ "pink": _flat("Eraser_Pink", (0.91, 0.45, 0.55), rough=0.6),
+ "note_y": _flat("Note_Yellow", (0.95, 0.83, 0.30), rough=0.9),
+ "note_b": _flat("Note_Blue", (0.45, 0.65, 0.88), rough=0.9),
+ "orange": _flat("Tin_Orange", (0.82, 0.38, 0.12), rough=0.6),
+ "book_t": _flat("Book_Tan", (0.78, 0.72, 0.58), rough=0.8),
+ "book_b": _flat("Book_Blue", (0.25, 0.35, 0.60), rough=0.8),
+ "oak": _pbr_mat("Partition_Oak", "partition_oak", scale=1.5, rough_fallback=0.45),
+ "beech": _pbr_mat("Shelf_Beech", "shelf_beech", scale=1.8, rough_fallback=0.5),
+ }
+
+
+# ---------------------------------------------------------------- wall frames
+def _walls_centroid():
+ pts = []
+ for w in SHELL["walls"].values():
+ pts.append(Vector(((w["start"][0] + w["end"][0]) / 2, (w["start"][1] + w["end"][1]) / 2, 0)))
+ return sum(pts, Vector()) / len(pts)
+
+
+def _wall_frame(wall):
+ """(start, along-dir, INTERIOR normal, length) of a SHELL wall, on the floor plane."""
+ w = SHELL["walls"][wall]
+ s = Vector((w["start"][0], w["start"][1], 0.0))
+ e = Vector((w["end"][0], w["end"][1], 0.0))
+ d = e - s
+ L = d.length
+ d = d / L
+ n = Vector((-d.y, d.x, 0.0))
+ if n.dot(_walls_centroid() - (s + e) / 2) < 0:
+ n = -n
+ return s, d, n, L
+
+
+def _part(name, sx, sy, sz, mat):
+ """A metric box mesh (size baked into vertices), unlinked transform-wise."""
+ me = _unit_cube_mesh(name)
+ for v in me.vertices:
+ v.co.x *= sx
+ v.co.y *= sy
+ v.co.z *= sz
+ ob = bpy.data.objects.new(name, me)
+ if mat is not None:
+ ob.data.materials.append(mat)
+ bpy.context.scene.collection.objects.link(ob)
+ return ob
+
+
+def _wall_box(wall, u, z, length, height, depth, mat, out=0.0, name="part"):
+ """Box on a wall: u = centre along wall from start (m), z = centre height above
+ floor, out = gap between wall face and the box's BACK face (box grows inward)."""
+ s, d, n, L = _wall_frame(wall)
+ ob = _part(name, length, depth, height, mat)
+ pos = s + d * u + n * (out + depth / 2.0)
+ ob.matrix_world = Matrix(
+ (
+ (d.x, n.x, 0.0, pos.x),
+ (d.y, n.y, 0.0, pos.y),
+ (0.0, 0.0, 1.0, SHELL["floor_z"] + z),
+ (0.0, 0.0, 0.0, 1.0),
+ )
+ )
+ return ob
+
+
+def group_fixture(name, category, parts):
+ """Bundle a fixture's parts under ONE named empty handle (its own collection),
+ so it selects/hides/toggles as a unit exactly like Table0/Chair0."""
+ coll = get_or_make_collection(name)
+ handle = bpy.data.objects.new(name, None)
+ handle.empty_display_size = 0.1
+ handle["room_id"] = name
+ handle["category"] = category
+ coll.objects.link(handle)
+ for p in parts:
+ if p is None:
+ continue
+ p.parent = handle
+ p.matrix_parent_inverse = handle.matrix_world.inverted()
+ move_to_collection(p, coll)
+ return handle
+
+
+# ---------------------------------------------------------------- ceiling
+def build_ceiling():
+ if bpy.data.objects.get("Ceiling0") is not None:
+ return
+ cz = SHELL["ceiling_z"]
+ loop = [
+ SHELL["walls"]["Wall0"]["start"], SHELL["walls"]["Wall2"]["start"],
+ SHELL["walls"]["Wall1"]["start"], SHELL["walls"]["Wall3"]["start"],
+ ]
+ me = bpy.data.meshes.new("Ceiling0")
+ me.from_pydata([(x, y, cz) for x, y in loop], [], [tuple(range(len(loop)))])
+ me.update()
+ ob = bpy.data.objects.new("Ceiling0", me)
+ bpy.context.scene.collection.objects.link(ob)
+ move_to_collection(ob, get_or_make_collection(COLL_SHELL))
+ ob.data.materials.append(_ceiling_mat())
+
+
+def apply_surface_materials(M):
+ for wn in SHELL["walls"]:
+ o = bpy.data.objects.get(wn)
+ if o is not None:
+ o.data.materials.clear()
+ o.data.materials.append(M["paint"])
+ fl = bpy.data.objects.get("Floor0")
+ if fl is not None:
+ fl.data.materials.clear()
+ fl.data.materials.append(_floor_mat())
+
+
+# ---------------------------------------------------------------- fixture kit
+BAND_Z0, BAND_H = 0.78, 0.15 # dado trunking band (bottom z, height)
+SOCK_Z = BAND_Z0 + BAND_H / 2.0 # sockets sit mid-band
+SHELF_ZB1, SHELF_ZB2 = 1.355, 1.845 # Wall1 shelf board centre heights
+
+
+# NOTE part names below are Wall<N>_-prefixed on purpose: the surface-ortho renderer
+# assigns un-prefixed fixture meshes to walls by min signed distance, which lets a
+# neighbour wall "steal" corner fixtures and hide them from their own montage.
+# Group names stay Whiteboard0/Radiator0/... as required.
+def _trunking(wall, idx, M):
+ """Cable trunking = channel base + a proud lid (two stacked boxes), full wall."""
+ S = globals().get("SHELL")
+ w = S["walls"][wall]
+ L = math.hypot(w["end"][0] - w["start"][0], w["end"][1] - w["start"][1])
+ parts = [
+ _wall_box(wall, L / 2, BAND_Z0 + BAND_H / 2, L - 0.02, BAND_H, 0.028,
+ M["trunk"], name=f"{wall}_trunk_base"),
+ _wall_box(wall, L / 2, BAND_Z0 + BAND_H / 2, L - 0.02, BAND_H - 0.022, 0.018,
+ M["trunk"], out=0.028, name=f"{wall}_trunk_lid"),
+ ]
+ group_fixture(f"Trunking{idx}", "trunking", parts)
+
+
+def _double_socket(wall, u, M, z=SOCK_Z, out=0.047):
+ """UK double socket ON the trunking lid: faceplate + 2 outlet bumps + 2 rockers."""
+ p = [_wall_box(wall, u, z, 0.146, 0.086, 0.012, M["white"], out=out,
+ name=f"{wall}_sock_plate")]
+ for du in (-0.034, 0.034):
+ p.append(_wall_box(wall, u + du, z - 0.004, 0.036, 0.040, 0.007, M["white"],
+ out=out + 0.012, name=f"{wall}_sock_outlet"))
+ p.append(_wall_box(wall, u + du, z + 0.028, 0.013, 0.018, 0.005, M["white"],
+ out=out + 0.012, name=f"{wall}_sock_rocker"))
+ return p
+
+
+def _sockets(wall, idx, us, M):
+ parts = []
+ for u in us:
+ parts += _double_socket(wall, u, M)
+ group_fixture(f"Sockets{idx}", "socket", parts)
+
+
+def _whiteboard(name, wall, uc, zc, W, H, M):
+ """Aluminium frame + recessed gloss face + pen tray, with the eraser, a marker
+ and the sticky notes the photo shows."""
+ parts = []
+ parts.append(_wall_box(wall, uc, zc + H / 2 - 0.0125, W, 0.025, 0.018, M["alu"],
+ name=f"{wall}_wb_frame_t"))
+ parts.append(_wall_box(wall, uc, zc - H / 2 + 0.0125, W, 0.025, 0.018, M["alu"],
+ name=f"{wall}_wb_frame_b"))
+ for du in (-(W / 2 - 0.0125), W / 2 - 0.0125):
+ parts.append(_wall_box(wall, uc + du, zc, 0.025, H - 0.05, 0.018, M["alu"],
+ name=f"{wall}_wb_frame_s"))
+ parts.append(_wall_box(wall, uc, zc, W - 0.05, H - 0.05, 0.010, M["face"],
+ out=0.004, name=f"{wall}_wb_face"))
+ parts.append(_wall_box(wall, uc, zc - H / 2 - 0.014, 0.42, 0.016, 0.055, M["alu"],
+ name=f"{wall}_wb_tray"))
+ parts.append(_wall_box(wall, uc, zc - H / 2 - 0.008, 0.42, 0.014, 0.006, M["alu"],
+ out=0.049, name=f"{wall}_wb_tray_lip"))
+ parts.append(_wall_box(wall, uc + 0.09, zc - H / 2 + 0.012, 0.10, 0.032, 0.055,
+ M["pink"], out=0.0, name=f"{wall}_wb_eraser"))
+ parts.append(_wall_box(wall, uc - 0.10, zc - H / 2 - 0.002, 0.12, 0.016, 0.018,
+ M["black"], out=0.012, name=f"{wall}_wb_marker"))
+ for du, dz, mk in ((-0.44, 0.30, "note_y"), (-0.33, 0.32, "note_y"),
+ (-0.21, 0.30, "note_b"), (0.34, -0.02, "pink")):
+ parts.append(_wall_box(wall, uc + du, zc + dz, 0.076, 0.076, 0.004, M[mk],
+ out=0.015, name=f"{wall}_wb_note"))
+ group_fixture(name, "whiteboard", parts)
+
+
+def _coat_rack(name, wall, uc, zc, W, M):
+ """Beech peg rail + 3 aluminium double hooks (plate + peg + upturned tip)."""
+ parts = [_wall_box(wall, uc, zc, W, 0.095, 0.026, M["beech"], name=f"{wall}_rack_rail")]
+ for du in (-W / 2 + 0.09, 0.0, W / 2 - 0.09):
+ u = uc + du
+ parts.append(_wall_box(wall, u, zc, 0.026, 0.105, 0.010, M["alu"],
+ out=0.026, name=f"{wall}_hook_plate"))
+ parts.append(_wall_box(wall, u, zc - 0.030, 0.020, 0.020, 0.070, M["alu"],
+ out=0.036, name=f"{wall}_hook_peg"))
+ parts.append(_wall_box(wall, u, zc - 0.008, 0.020, 0.042, 0.014, M["alu"],
+ out=0.092, name=f"{wall}_hook_tip"))
+ group_fixture(name, "coat_rack", parts)
+
+
+def _shelf_wall1(M):
+ """Twin-slot shelving across Wall1: slotted standards + brackets + two long
+ beech boards + the items the photos show sitting on them."""
+ wall = "Wall1"
+ parts = []
+ ups = [0.22, 0.85, 1.48, 2.20, 2.90, 3.50]
+ for u in ups: # slotted standards
+ parts.append(_wall_box(wall, u, 1.575, 0.025, 1.25, 0.012, M["wmetal"],
+ name=f"{wall}_std"))
+ for bz in (SHELF_ZB1, SHELF_ZB2):
+ for u in ups: # bracket arm under each board at each standard
+ parts.append(_wall_box(wall, u, bz - 0.026, 0.006, 0.032, 0.21,
+ M["wmetal"], name=f"{wall}_bracket"))
+ parts.append(_wall_box(wall, 1.86, bz, 3.52, 0.018, 0.24, M["beech"],
+ out=0.004, name=f"{wall}_board"))
+
+ def item(u, ztop, w, h, d, mk, out=0.05):
+ parts.append(_wall_box(wall, u, ztop + h / 2, w, h, d, M[mk], out=out,
+ name=f"{wall}_item"))
+
+ top1, top2 = SHELF_ZB1 + 0.009, SHELF_ZB2 + 0.009
+ # ---- upper board: desk fan + articulated lamp + router
+ item(3.32, top2, 0.26, 0.025, 0.20, "white") # fan base
+ item(3.32, top2 + 0.025, 0.05, 0.26, 0.05, "white", 0.10) # fan pole
+ item(3.32, top2 + 0.28, 0.38, 0.38, 0.10, "wmetal", 0.06) # fan cage
+ item(3.32, top2 + 0.28, 0.29, 0.29, 0.045, "dark", 0.145) # fan blades disc
+ item(2.86, top2, 0.15, 0.022, 0.15, "black") # lamp base
+ item(2.86, top2 + 0.02, 0.025, 0.30, 0.025, "black", 0.09) # lamp lower arm
+ item(2.76, top2 + 0.30, 0.24, 0.025, 0.025, "black", 0.09) # lamp upper arm
+ item(2.64, top2 + 0.24, 0.16, 0.10, 0.09, "black", 0.07) # lamp head
+ item(3.02, top2, 0.20, 0.045, 0.15, "white") # router flat box
+ # ---- lower board: boxes / tins / cup / dock / rolls / books
+ item(0.55, top1, 0.055, 0.24, 0.22, "book_b") # upright binder
+ item(0.62, top1, 0.055, 0.24, 0.22, "white") # upright binder
+ item(1.05, top1, 0.34, 0.16, 0.22, "white") # big white box
+ item(1.32, top1, 0.12, 0.12, 0.12, "note_b") # blue tin
+ item(1.50, top1, 0.16, 0.10, 0.12, "dark") # grey box
+ item(2.05, top1, 0.12, 0.14, 0.08, "white") # phone dock
+ item(2.22, top1, 0.08, 0.11, 0.08, "white") # paper cup
+ item(2.38, top1, 0.10, 0.12, 0.10, "orange") # orange tin
+ item(2.56, top1, 0.13, 0.12, 0.13, "white") # tape roll
+ item(2.76, top1, 0.20, 0.045, 0.16, "black") # telephone base
+ item(2.76, top1 + 0.045, 0.05, 0.035, 0.18, "black", 0.06) # handset
+ item(3.10, top1, 0.28, 0.06, 0.20, "book_t") # flat magazine stack
+ group_fixture("Shelf1", "shelf", parts)
+
+
+def _monitor(name, wall, u, M):
+ """Flat-screen standing on the upper Wall1 board: base + neck + screen slab."""
+ ztop = SHELF_ZB2 + 0.009
+ parts = [
+ _wall_box(wall, u, ztop + 0.008, 0.24, 0.016, 0.17, M["black"], out=0.05,
+ name=f"{wall}_mon_base"),
+ _wall_box(wall, u, ztop + 0.06, 0.06, 0.10, 0.04, M["black"], out=0.10,
+ name=f"{wall}_mon_neck"),
+ _wall_box(wall, u, ztop + 0.016 + 0.20, 0.63, 0.40, 0.045, M["black"], out=0.07,
+ name=f"{wall}_mon_screen"),
+ _wall_box(wall, u, ztop + 0.045, 0.63, 0.018, 0.006, M["dark"], out=0.115,
+ name=f"{wall}_mon_bezel"),
+ ]
+ group_fixture(name, "monitor", parts)
+
+
+def _partition(name, wall, M):
+ """Wall2 glazed oak partition: posts + rails + solid base panel + frosted panes
+ + a clear transom row (lit corridor beyond) + the thermometer on the mullion."""
+ oak = M["oak"]
+ H = SHELL["ceiling_z"] - SHELL["floor_z"] # 2.5575
+ L = _wall_frame(wall)[3]
+ parts = []
+ for u, w in ((0.90, 0.085), (1.955, 0.085), (2.98, 0.075)): # full-height posts
+ parts.append(_wall_box(wall, u, H / 2, w, H, 0.075, oak, name=f"{wall}_post"))
+ parts.append(_wall_box(wall, L / 2, 2.525, L, 0.065, 0.075, oak,
+ name=f"{wall}_head_rail"))
+ parts.append(_wall_box(wall, 0.44, 2.25, 0.88, 0.08, 0.075, oak,
+ name=f"{wall}_door_head")) # frame rail over the door
+ parts.append(_wall_box(wall, 0.44, 2.39, 0.80, 0.20, 0.010, M["clear"],
+ out=0.030, name=f"{wall}_glass_door_t"))
+ zone0, zone1 = 0.92, 2.945
+ zm, zl = (zone0 + zone1) / 2, zone1 - zone0
+ parts.append(_wall_box(wall, zm, 0.315, zl, 0.63, 0.055, oak,
+ name=f"{wall}_base_panel")) # solid wood bottom
+ for z0, z1 in ((0.96, 1.05), (1.44, 1.53), (1.94, 2.07)): # horizontal rails
+ parts.append(_wall_box(wall, zm, (z0 + z1) / 2, zl, z1 - z0, 0.065, oak,
+ name=f"{wall}_rail"))
+ for c0, c1 in ((0.9425, 1.9125), (1.9975, 2.9425)): # 2 glass columns
+ cm, cl = (c0 + c1) / 2, c1 - c0
+ for r0, r1 in ((0.63, 0.96), (1.05, 1.44), (1.53, 1.94)): # frosted rows
+ parts.append(_wall_box(wall, cm, (r0 + r1) / 2, cl, r1 - r0, 0.010,
+ M["frost"], out=0.025, name=f"{wall}_frost"))
+ parts.append(_wall_box(wall, cm, 2.285, cl, 0.43, 0.010, M["clear"],
+ out=0.025, name=f"{wall}_clear")) # transom
+ parts.append(_wall_box(wall, 1.955, 1.62, 0.028, 0.21, 0.008, M["white"],
+ out=0.075, name=f"{wall}_thermometer"))
+ group_fixture(name, "partition", parts)
+
+
+def _heater(name, wall, uc, M):
+ """White convector on the floor against the partition base panel: body + a loop
+ of front fins + top grille + feet."""
+ parts = [
+ _wall_box(wall, uc, 0.27, 0.75, 0.40, 0.11, M["white"], out=0.06,
+ name=f"{wall}_htr_body"),
+ _wall_box(wall, uc, 0.475, 0.76, 0.045, 0.13, M["white"], out=0.05,
+ name=f"{wall}_htr_grille"),
+ ]
+ n_fin, span = 13, 0.62
+ for i in range(n_fin):
+ u = uc - span / 2 + i * span / (n_fin - 1)
+ parts.append(_wall_box(wall, u, 0.25, 0.028, 0.26, 0.012, M["white"],
+ out=0.17, name=f"{wall}_htr_fin"))
+ for du in (-0.30, 0.30):
+ parts.append(_wall_box(wall, uc + du, 0.03, 0.06, 0.06, 0.06, M["wmetal"],
+ out=0.10, name=f"{wall}_htr_foot"))
+ group_fixture(name, "heater", parts)
+
+
+def _radiator(name, wall, uc, M, W=1.10, z0=0.12, z1=0.57):
+ """Panel radiator under the window: back panel + vertical fin columns + top
+ grille + end caps + pipe drops."""
+ parts = [_wall_box(wall, uc, (z0 + z1) / 2, W, z1 - z0, 0.055, M["wmetal"],
+ out=0.02, name=f"{wall}_rad_back")]
+ n_fin, span = 14, W - 0.12
+ for i in range(n_fin):
+ u = uc - span / 2 + i * span / (n_fin - 1)
+ parts.append(_wall_box(wall, u, (z0 + z1) / 2 - 0.02, 0.042, z1 - z0 - 0.10,
+ 0.014, M["wmetal"], out=0.075, name=f"{wall}_rad_fin"))
+ parts.append(_wall_box(wall, uc, z1 - 0.014, W, 0.028, 0.075, M["wmetal"],
+ out=0.02, name=f"{wall}_rad_grille"))
+ for du in (-(W / 2 - 0.015), W / 2 - 0.015):
+ parts.append(_wall_box(wall, uc + du, (z0 + z1) / 2, 0.030, z1 - z0, 0.075,
+ M["wmetal"], out=0.02, name=f"{wall}_rad_cap"))
+ for du in (-(W / 2 + 0.04), W / 2 + 0.04):
+ parts.append(_wall_box(wall, uc + du, z0 - 0.02, 0.045, 0.07, 0.045,
+ M["wmetal"], out=0.04, name=f"{wall}_rad_valve"))
+ parts.append(_wall_box(wall, uc + du, (z0 - 0.05) / 2, 0.02, z0 - 0.05, 0.02,
+ M["wmetal"], out=0.05, name=f"{wall}_rad_pipe"))
+ group_fixture(name, "radiator", parts)
+
+
+def _sill(name, wall, M):
+ """Beech window board over the Window0 opening + a proud front lip."""
+ op = SHELL["openings"]["Window0"]
+ u0, u1 = op["offset"] - op["width"] / 2, op["offset"] + op["width"] / 2
+ um, ln = (u0 + u1) / 2, (u1 - u0) + 0.12
+ parts = [
+ _wall_box(wall, um, op["sill"] - 0.008, ln, 0.030, 0.19, M["beech"],
+ name=f"{wall}_sill_board"),
+ _wall_box(wall, um, op["sill"] - 0.030, ln, 0.016, 0.022, M["beech"],
+ out=0.168, name=f"{wall}_sill_lip"),
+ ]
+ group_fixture(name, "sill", parts)
+
+
+def _blinds(name, wall, M):
+ """Vertical blinds: white headrail + two drawn-back banks of cream slats with
+ the middle of the window left open (as photographed)."""
+ op = SHELL["openings"]["Window0"]
+ u0, u1 = op["offset"] - op["width"] / 2, op["offset"] + op["width"] / 2
+ parts = [_wall_box(wall, (u0 + u1) / 2, 2.34, (u1 - u0) + 0.10, 0.06, 0.08,
+ M["white"], out=0.02, name=f"{wall}_blind_rail")]
+ for i in range(14): # left bank
+ parts.append(_wall_box(wall, 0.14 + i * 0.0855, 1.65, 0.055, 1.32, 0.038,
+ M["blind"], out=0.045, name=f"{wall}_slat"))
+ for i in range(13): # right bank
+ parts.append(_wall_box(wall, 1.87 + i * 0.0855, 1.65, 0.055, 1.32, 0.038,
+ M["blind"], out=0.045, name=f"{wall}_slat"))
+ group_fixture(name, "blinds", parts)
+
+
+def _ceiling_light(name, cx, cy, M, size=0.593):
+ """Recessed square LED panel in the suspended grid (frame 14 shows it near
+ Wall2): emissive face + white trim bars, dropped just under the ceiling."""
+ cz = SHELL["ceiling_z"]
+ ang = math.radians(26.29) # tile-grid axes (parallel to the walls)
+ dx = Vector((math.cos(ang), math.sin(ang), 0.0))
+ dy = Vector((-math.sin(ang), math.cos(ang), 0.0))
+ emis = _flat("Light_Panel", (1.0, 1.0, 1.0), rough=0.3)
+ try:
+ eb = next(n for n in emis.node_tree.nodes if n.type == "BSDF_PRINCIPLED")
+ eb.inputs["Emission Color"].default_value = (1.0, 0.99, 0.95, 1.0)
+ eb.inputs["Emission Strength"].default_value = 6.0
+ except (StopIteration, KeyError):
+ pass
+
+ def at(ob, ox, oy, z):
+ pos = Vector((cx, cy, 0.0)) + dx * ox + dy * oy
+ ob.matrix_world = Matrix(
+ (
+ (dx.x, dy.x, 0.0, pos.x),
+ (dx.y, dy.y, 0.0, pos.y),
+ (0.0, 0.0, 1.0, z),
+ (0.0, 0.0, 0.0, 1.0),
+ )
+ )
+ return ob
+
+ parts = [at(_part("CeilLight_face", size, size, 0.012, emis), 0, 0, cz - 0.008)]
+ t = size / 2 + 0.015
+ for ox, oy, sx, sy in ((0, t, size + 0.06, 0.03), (0, -t, size + 0.06, 0.03),
+ (t, 0, 0.03, size), (-t, 0, 0.03, size)):
+ parts.append(at(_part("CeilLight_trim", sx, sy, 0.015, M["white"]), ox, oy, cz - 0.006))
+ group_fixture(name, "ceiling_light", parts)
+
+
+def build_fixtures(M):
+ # dado trunking with sockets runs along Wall1 (shelving wall) + Wall3 (window)
+ _trunking("Wall1", 1, M)
+ _trunking("Wall3", 3, M)
+ _sockets("Wall1", 1, [0.45, 0.70, 1.90, 2.30, 3.20, 3.48], M)
+ _sockets("Wall3", 3, [0.35], M)
+
+ # ---- Wall0: whiteboard (with notes/eraser/marker) + beech coat rack
+ _whiteboard("Whiteboard0", "Wall0", 2.42, 1.48, 1.30, 1.00, M)
+ _coat_rack("CoatRack0", "Wall0", 3.30, 1.765, 0.42, M)
+
+ # ---- Wall1: twin-slot shelving (fan/lamp/boxes) + flat screen on upper board
+ _shelf_wall1(M)
+ _monitor("Monitor0", "Wall1", 1.62, M)
+
+ # ---- Wall2: glazed oak partition around the door + floor convector heater
+ _partition("Partition0", "Wall2", M)
+ _heater("Heater0", "Wall2", 1.45, M)
+
+ # ---- Wall3: window board + vertical blinds + panel radiator under the window
+ _sill("Sill0", "Wall3", M)
+ _blinds("Blinds0", "Wall3", M)
+ _radiator("Radiator0", "Wall3", 1.48, M)
+
+ # ---- ceiling: recessed LED panel in the tile grid near Wall2
+ _ceiling_light("CeilingLight0", 0.69, -0.57, M)
+
+
+def harness_stage(scene):
+ """Agent-authored pass over the blank build: ceiling, real materials, fixtures."""
+ M = _harness_mats()
+ build_ceiling()
+ apply_surface_materials(M)
+ build_fixtures(M)
+ bpy.context.view_layer.update()
+ scene.index()
+ print(" harness: ceiling + materials + fixtures built")
+
+
+# ============================================================================
# main
# ============================================================================
def main():
scene = RoomScene().build()
+ harness_stage(scene)
scene.summary()
scene.export_glb()

③ QC pass(es)

qc · qc_20260720_074632.log

STEP #1
I'll start by reading the room definition and the ground-truth stitches.
⌘ Bash📄 Read /scratch2/code/LiteReality_to_share/agen⌘ Bash📄 Read /scratch2/code/LiteReality_to_share/agen📄 Read /scratch2/code/LiteReality_to_share/agen
👁INSPECT #2
Now let me look at the objects, the qc_pass helper (for canonical articulation/glass recipes), and the ground-truth images.
⌘ Bash📄 Read /scratch2/code/LiteReality_to_share/agen📄 Read /scratch2/code/LiteReality_to_share/agen📄 Read /scratch2/code/LiteReality_to_share/agen📄 Read /scratch2/code/LiteReality_to_share/agen⌘ Bash🔧 ToolSearch select:mcp__cap__render,mcp__cap__critic🖼 render Wall2
STEP #3
The MCP render can't infer the scan; per my notes I need the engine CLI with `scan=` explicitly. Let me check its interface.
⌘ Bash⌘ Bash⌘ Bash⌘ Bash⌘ Bash
STEP #4
You're out of usage credits. Run /usage-credits to keep using Fable 5 or /model to switch models.

directed · qc_directed_20260720_115314.log

STEP #1
I'll start by reading the Room.md and Room.py in full to understand the current authoring.
⌘ Bash📄 Read /scratch2/code/LiteReality_to_share/agen📄 Read /scratch2/code/LiteReality_to_share/agen📄 Read /scratch2/code/LiteReality_to_share/agen
STEP #2
Now let me look at the key stitches and references to confirm which wall is the partition and what's really on the shelves.
📄 Read /scratch2/code/LiteReality_to_share/agen
STEP #3
Wall2 is confirmed a glazed oak partition (with wooden door) — not a solid wall. Now the shelf wall (Wall1) and references:
📄 Read /scratch2/code/LiteReality_to_share/agen
STEP #4
Wall1 shelving: upper board has a real fan + articulated lamp + the monitor (separate fixture); the lower board holds indistinct clutter. Let me check
📄 Read /scratch2/code/LiteReality_to_share/agen
✏️EDIT #5
Both fixes are clear. Let me make the edits.
✏️ Edit /scratch2/code/LiteReality_to_share/agen
✏️EDIT #6
**Fix 2 — remove placeholder/filler items from the Wall1 shelving** (keep the shelving, the clearly-real fan and articulated lamp; drop the router gue
✏️ Edit /scratch2/code/LiteReality_to_share/agen
👁INSPECT #7
Now let me render to verify. Let me load the render tool.
🔧 ToolSearch select:mcp__cap__render🖼 render Wall2
👁INSPECT #8
The MCP render can't infer scan. Let me find the engine CLI recipe used for these final_results rooms.
⌘ Bash⌘ Bash⌘ Bash⌘ Bash🔧 ToolSearch select:mcp__cap__select_views🎞 select_views Wall2⌘ Bash⌘ Bash⌘ Bash⌘ Bash📄 Read /home/zelda/zh340/.claude/projects/-scra
✏️EDIT #9
The recipe: monkeypatch `_scan_from_room`, run from repo root. Let me write a render driver.
✏️ Write /scratch2/code/LiteReality_to_share/agen⌘ Bash
STEP #10
Rendered successfully (auto-recompiled, so the edits are valid). Let me view the results.
📄 Read /scratch2/code/LiteReality_to_share/agen📄 Read /scratch2/code/LiteReality_to_share/agen
STEP #11
Wall2 partition reads correctly as the sole structure (no solid wall stacked behind). Now Wall1 shelf:
📄 Read /scratch2/code/LiteReality_to_share/agen📄 Read /scratch2/code/LiteReality_to_share/agen
STEP #12
Both fixes verified. Let me clean up the temporary render driver.
⌘ Bash
STEP #13
Both mandatory fixes are done and render-verified. Summary of changes to `Room.py` (only file touched):
Both mandatory fixes are done and render-verified. Summary of changes to `Room.py` (only file touched): **Wall2 — partition, not a wall (fix 1)** - Wall2 is a glazed oak partition with a wooden door (confirmed from the stitch), so the redundant solid wall was removed. In `build_fixtures`, right after `_partition("Partition0", "Wall2", …)` and `_heater(…)`, I now delete the `Wall2` solid mesh (`bpy.data.objects.remove`). The oak/glass partition already fills that whole plane, so nothing is stacked behind it anymore. `SHELL["walls"]["Wall2"]` is deliberately kept — the partition, heater, door opening and floor all derive their frame from it — only the built solid mesh is dropped. - Render confirms: the partition (posts, rails, frosted panes, door + head transom) now *is* the wall, with no grey solid wall behind it. **Wall1 shelving — no placeholder objects (fix 2)** - In `_shelf_wall1` I kept the shelving structure (standards, brackets, both beech boards) and the two objects the reference clearly resolves — the caged desk fan and the articulated task lamp on the upper board (plus the separate `Monitor0`). I removed the "router flat box" guess and the entire lower-board row of generic filler (binders, white/grey boxes, blue/orange tins, phone dock, paper cup, tape roll, telephone + handset, magazine stack) that the blurry stitch does not actually show. - Render confirms: the lower shelf is now clear of props; only the fan, lamp and monitor remain up top. Nothing else was changed — walls, openings, Table/Chair/Storage, materials, ceiling, window/blind articulation and all other fixtures are untouched. The room auto-recompiled cleanly during rendering, so `Room.py` is valid.

opus · qc_opus_20260720_082943.log

STEP #1
I'll start by reading the Room.md and Room.py files in full to understand what I'm working with.
⌘ Bash📄 Read /scratch2/code/LiteReality_to_share/agen📄 Read /scratch2/code/LiteReality_to_share/agen
STEP #2
Let me read the rest of the file, focusing on the harness layer (fixtures) and SHELL.
📄 Read /scratch2/code/LiteReality_to_share/agen
STEP #3
Now let me look at the Objects directory and the procedural door/window objects, which are the key items for the glass/articulation checks.
⌘ Bash📄 Read /scratch2/code/LiteReality_to_share/agen📄 Read /scratch2/code/LiteReality_to_share/agen
STEP #4
The door and window objects both look well-authored already: glass is transmissive (transmission 0.90/0.94), both leaf and vent articulate. Now let me
🔧 ToolSearch select:mcp__cap__render,mcp__cap__fetch_
STEP #5
Let me view the key reference images and the Wall2 stitch, and kick off a room render in parallel.
📄 Read /scratch2/code/LiteReality_to_share/agen📄 Read /scratch2/code/LiteReality_to_share/agen📄 Read /scratch2/code/LiteReality_to_share/agen📄 Read /scratch2/code/LiteReality_to_share/agen
👁INSPECT #6
Now let me render the current room to see the actual state. Let me start with the room overview and the two key walls (Wall2 partition/door, Wall3 win
🖼 render room
STEP #7
The MCP render needs an explicit scan. Let me find the render engine CLI.
⌘ Bash⌘ Bash⌘ Bash⌘ Bash⌘ Bash⌘ Bash⌘ Bash⌘ Bash⌘ Bash⌘ Bash
STEP #8
The room dir resolves into `final_results`, breaking scan inference. I'll write a small driver that monkeypatches the scan resolver and reuses the ful
⌘ Bash⌘ Bash
STEP #9
Render works now. Let me view the room renders.
📄 Read /scratch2/code/LiteReality_to_share/agen📄 Read /scratch2/code/LiteReality_to_share/agen📄 Read /scratch2/code/LiteReality_to_share/agen📄 Read /scratch2/code/LiteReality_to_share/agen
STEP #10
The room looks broadly correct. Now the critical walls — Wall2 (partition + glazed door) and Wall3 (window). Let me render both.
⌘ Bash
STEP #11
The head-on ortho compares are the best ground-truth check. Let me view both.
📄 Read /scratch2/code/LiteReality_to_share/agen📄 Read /scratch2/code/LiteReality_to_share/agen📄 Read /scratch2/code/LiteReality_to_share/agen
STEP #12
Let me view the door and window reference images to judge the openings against intent.
📄 Read /scratch2/code/LiteReality_to_share/agen⌘ Bash
STEP #13
The door/window have no separate refs (only the 3 objects). Let me render the three procedural objects (Storage0, Table0, Table1) to QC against their
⌘ Bash📄 Read /scratch2/code/LiteReality_to_share/agen📄 Read /scratch2/code/LiteReality_to_share/agen
STEP #14
Storage0 auto-selected no frames. Let me force frames that view its location and check the Storage0 object.py.
📄 Read /scratch2/code/LiteReality_to_share/agen
STEP #15
Storage0's object.py faithfully builds the wall-shelf from its reference. Let me force-render it to confirm placement in-scene.
⌘ Bash📄 Read /scratch2/code/LiteReality_to_share/agen📄 Read /scratch2/code/LiteReality_to_share/agen
✏️EDIT #16
Storage0, Table0, Table1 all match their references well. The scene is in good shape. My QC findings:
📄 Read /scratch2/code/LiteReality_to_share/agen✏️ Edit /scratch2/code/LiteReality_to_share/agen
✏️EDIT #17
Now remove the redundant static `Blinds0` wall fixture in Room.py so the visible blinds are the articulated GLB set:
✏️ Edit /scratch2/code/LiteReality_to_share/agen
STEP #18
Now let me render Wall3 to verify the GLB vanes alone read correctly and the window still looks right (the render sync will rebuild the window GLB and
⌘ Bash⌘ Bash
STEP #19
The render is running in the background; I'll be notified. Let me check its current output.
📄 Read /scratch2/code/LiteReality_to_share/agen⌘ Bash
STEP #20
The render (rebuild GLB + recompile + render) takes a few minutes. Let me wait for it to finish.
⌘ Bash
STEP #21
The window GLB rebuilt and recompiled cleanly (exit 0). Let me view the updated Wall3 to confirm the blinds still read correctly with only the articul
📄 Read /scratch2/code/LiteReality_to_share/agen
STEP #22
The blinds still read correctly from the articulated GLB set alone — drawn to the sides, center vent visible, radiator below. Let me confirm with a pe
📄 Read /scratch2/code/LiteReality_to_share/agen
STEP #23
The blinds render correctly and the room recompiled + exported cleanly. Let me explicitly rebuild the window object standalone to confirm the new arti
⌘ Bash
STEP #24
Clean build (exit 0). Let me confirm the blind slide animations are actually present in the exported GLB.
⌘ Bash
STEP #25
All three parts articulate: `Blind_Left_slide` + `Blind_Right_slide` (prismatic) and the existing `Window_Vent_swing` (revolute). Let me clean up my s
⌘ Bash
✏️EDIT #26
Let me record this QC pass in memory.
✏️ Write /home/zelda/zh340/.claude/projects/-scra📄 Read /home/zelda/zh340/.claude/projects/-scra📄 Read /home/zelda/zh340/.claude/projects/-scra✏️ Edit /home/zelda/zh340/.claude/projects/-scra
STEP #27
QC pass complete. Here's what I did, per surface:
QC pass complete. Here's what I did, per surface: ## Findings & changes The room was already in strong shape from scene_stage authoring, so I made **two minimal, verified edits** and left the good authoring untouched. **Wall3 (window) — the one real gap:** - **Blinds now articulate.** The vertical blinds live inside the `Window0` GLB (`Wall3_Window_0/object.py`). I added prismatic slide articulation + closed→open clips to both vane banks (left bank gathers toward −X, right toward +X, travel = 0.7·span). The rest pose is unchanged — drawn to the sides with the centre vent clear, exactly as photographed — so the static render looks identical, but the vanes now slide open without covering the glass. Verified: the GLB now exports `Blind_Left_slide`, `Blind_Right_slide`, and the existing `Window_Vent_swing`. - **Removed a duplicate blind fixture.** `Room.py` also built a *static* `Blinds0` wall fixture that duplicated the GLB's vanes and sat more proud (out=0.045), so it would have frozen in front of the moving set. Deleted that one call; kept the sill and radiator. Re-rendered Wall3 — the blinds still read correctly from the single articulated GLB set. **Everything else checked out, no change needed:** - **Wall2 (door + partition):** door glazed lights and window pane use transmissive glass; both openings are cut and filled (leaf/pane in the hole, not a card). The partition's frosted/clear panes are intentionally opaque `_flat` + faint emission — the validated obscured-glass recipe (real transmissive glass renders as a dark void in this pipeline), so I left it. - **Windows articulate:** the top-hung vent (revolute) already worked. - **No fixture over an opening:** Wall3 trunking/sockets sit below the sill, radiator is under the window, sill/blinds above — all clear; Wall2 partition and heater clear the door. - **Ceiling** is materialed (mineral-tile grid); **lights** are only the real LED panel + daylight card (no invented emitters). - **Objects vs reference:** Table0 (double-pedestal oak desk), Table1 (oak desk/black frame), Storage0 (wall shelf on Wall3) all match their references at a glance. Both edits were confirmed by a standalone GLB rebuild (exit 0) and a full room recompile + Wall3 re-render.

opus · qc_opus_20260720_113531.log

STEP #1
I'll start by reading the Room.md and Room.py files in full to understand what's already authored.
⌘ Bash📄 Read /scratch2/code/LiteReality_to_share/agen📄 Read /scratch2/code/LiteReality_to_share/agen
STEP #2
Let me read the rest of the file, and look at the relevant memories.
📄 Read /scratch2/code/LiteReality_to_share/agen
STEP #3
Let me read the two most relevant memories for this scene.
📄 Read /home/zelda/zh340/.claude/projects/-scra📄 Read /home/zelda/zh340/.claude/projects/-scra
STEP #4
The prior QC pass already handled several items (blinds articulation, duplicate blind removal). Let me verify the current state of the object GLBs and
⌘ Bash
STEP #5
Let me read the Door and Window object.py files, and view the object reference images and wall stitches.
📄 Read /scratch2/code/LiteReality_to_share/agen
STEP #6
Window0 looks complete (transmissive glass + articulated vent + articulated blinds). Now let me check the Door object and view the ground-truth images
📄 Read /scratch2/code/LiteReality_to_share/agen
STEP #7
Let me view the ground-truth images — door reference, wall stitches, and object references.
⌘ Bash⌘ Bash
STEP #8
Now let me view the key ground-truth images: door reference, window reference, and the wall stitches.
📄 Read /scratch2/code/LiteReality_to_share/agen📄 Read /scratch2/code/LiteReality_to_share/agen
STEP #9
Door and window match their references well (transmissive glass, articulated, correct layout). Now let me view the object references and wall stitches
📄 Read /scratch2/code/LiteReality_to_share/agen📄 Read /scratch2/code/LiteReality_to_share/agen📄 Read /scratch2/code/LiteReality_to_share/agen
STEP #10
Storage0 is a clean wall-shelf unit (no props on it). Now let me view the wall stitches to check fixtures against ground truth.
📄 Read /scratch2/code/LiteReality_to_share/agen📄 Read /scratch2/code/LiteReality_to_share/agen
STEP #11
Wall1's shelf clutter (fan, lamp, monitor, boxes, phone) is genuinely in the photo — not invented filler, so it stays. Let me view the remaining surfa
📄 Read /scratch2/code/LiteReality_to_share/agen📄 Read /scratch2/code/LiteReality_to_share/agen
👁INSPECT #12
All fixtures match the stitches: Wall2 = frosted oak partition + door (frosted glass is genuinely opaque in the photo), Wall3 = window with side-drawn
🔧 ToolSearch select:mcp__cap__render🖼 render room
STEP #13
The MCP render can't pass `scan=`. Per my notes, the engine CLI accepts `--scan`. Let me use that from the repo root.
⌘ Bash⌘ Bash⌘ Bash⌘ Bash📄 Read /tmp/qc_scene/scene_frame_00008.png📄 Read /tmp/qc_scene/scene_frame_00020.png📄 Read /tmp/qc_scene/scene_frame_00032.png📄 Read /tmp/qc_scene/scene_frame_00042.png
STEP #14
I've now verified all four walls, both openings, the ceiling, floor, and all three procedural objects against ground truth. Let me summarize my findin
I've now verified all four walls, both openings, the ceiling, floor, and all three procedural objects against ground truth. Let me summarize my findings. ## QC pass result — fallside-office-Zhening I read `Room.md` + `Room.py` in full, inspected both opening GLBs (`Wall2_Door_0/object.py`, `Wall3_Window_0/object.py`), and render-checked frames 8, 20, 32, 42 (all four walls) against the stitches and per-object references. **The scene already satisfies every QC criterion — no edits were needed.** The prior QC edits (blind articulation + duplicate-fixture removal) are present and correct in this directory. What I verified per surface: - **Wall2 — Door0 + oak partition.** Opening is boolean-cut; the door leaf/sidelight sit in the hole (no slab). Leaf = top vision light + 4-light column, sidelight = 4 lights over a solid panel, handle on the right, hinges on the left — matches the reference. Leaf is a `revolute` articulated swing; all panes use a transmissive Principled BSDF (transmission 0.90). Partition frosted panels are intentionally opaque-white — that matches the *real* obscured privacy glass in the stitch, so it's correct, not a defect. - **Wall3 — Window0 + radiator + sill.** Opening cut; the fixed light + centre vent sit in it. Vent is a top-hung `revolute` (transmissive glass rides the moving sash). Vertical blinds are `prismatic` (both banks gather to the jambs, centre left clear) — matches the stitch. Blinds live only in the GLB; the redundant static `Blinds0` fixture is correctly absent. Radiator/sill sit below the sill, not over the opening. - **Wall1 — twin-slot shelf + monitor.** The shelf clutter (fan, articulated lamp, monitor, boxes, tins, phone, books, router) is genuinely visible in the stitch — these are faithful, not invented filler, so they stay. Trunking/sockets sit below at z≈0.78. - **Wall0 — whiteboard + coat rack + Storage0.** Whiteboard with notes/eraser/marker and the 3-hook rack match the stitch; Storage0 (a separate wall-shelf on Wall0, distinct from Shelf1) matches its clean reference with nothing added on it. - **Ceiling0** is materialed (white mineral tile + T-grid seams); the only emitter is the real recessed LED panel (CeilingLight0) — no invented lights. - **Floor0 / Table0 / Table1 / Chairs** untouched; Table0 (twin-pedestal desk) and Table1 (black-frame desk) match their references. All glass is glass, all openings open, both openings articulate, nothing overlaps an opening, ceiling is materialed, and every object matches its reference. Room compiles and renders. No changes made.

④ Reference images used

Object references (object refs)real crop → reference · 3
captured crop
reference used
captured crop
reference used
captured crop
reference used
Object references (opening refs)real crop → reference · 2
captured crop
reference used
captured crop
reference used
Surface references (per-wall stitches)authoring ground truth · 6

Final — render | photo

photo
render
photo
render
photo
render
photo
render