② Code changes
Room.py diffseed (blank shell) → authored
+578 −0 lines · seed → authored Room.py
@@ -1199,8 +1199,586 @@
# ============================================================================
+# HARNESS LAYER (agent-authored): ceiling + surface materials + wall fixtures
+# ============================================================================
+# Runs AFTER the blank build (local mesh coords == world metres). Every fixture
+# is a multi-part assembly bundled into ONE hide-as-a-unit group via
+# group_fixture(); part meshes are Wall<N>_-prefixed so the surface-ortho
+# renderer keeps them with their own wall's montage.
+
+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 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 plasterboard + a faint seam line every 1.2 m (large-board look)."""
+ name = "Ceiling_Boards"
+ if bpy.data.materials.get(name):
+ return bpy.data.materials[name]
+ mat = _pbr_mat(name, "ceiling_tile", scale=1.0, rough_fallback=0.9)
+ nt = mat.node_tree
+ b = next(n for n in nt.nodes if n.type == "BSDF_PRINCIPLED")
+ tc = next(n for n in nt.nodes if n.type == "TEX_COORD")
+ sep = nt.nodes.new("ShaderNodeSeparateXYZ")
+ nt.links.new(tc.outputs["Object"], 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 = 1.2
+ nt.links.new(add.outputs[0], mod.inputs[0])
+ lt = nt.nodes.new("ShaderNodeMath")
+ lt.operation = "LESS_THAN"
+ lt.inputs[1].default_value = 0.010
+ 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.66, 0.66, 0.65, 1.0)
+ diff_link = next(
+ (l for l in nt.links if l.to_node == b and l.to_socket.name == "Base Color"), None
+ )
+ if diff_link is not None:
+ nt.links.new(diff_link.from_socket, colA)
+ else:
+ colA.default_value = (0.92, 0.92, 0.90, 1.0)
+ nt.links.new(mx.outputs[0], fac)
+ nt.links.new(colout, b.inputs["Base Color"])
+ return mat
+
+
+def _harness_mats():
+ return {
+ # wall paints (per-wall assignment in apply_surface_materials)
+ "cream": _flat("Paint_Cream", (0.905, 0.878, 0.820), rough=0.92),
+ "grey": _flat("Paint_Grey", (0.868, 0.866, 0.848), rough=0.92),
+ "grey2": _flat("Paint_Grey2", (0.845, 0.845, 0.838), rough=0.92),
+ "sage": _flat("Paint_Sage", (0.868, 0.888, 0.842), rough=0.92),
+ "offwhite": _flat("Paint_OffWhite", (0.885, 0.885, 0.868), rough=0.92),
+ # fixture stock
+ "skirt": _flat("Skirting_BlueGrey", (0.44, 0.46, 0.50), rough=0.6),
+ "trim": _flat("Trim_White", (0.93, 0.93, 0.91), rough=0.55),
+ "white": _flat("White_Plastic", (0.96, 0.96, 0.95), rough=0.45),
+ "alu": _flat("Aluminium", (0.78, 0.79, 0.80), rough=0.35, metal=1.0),
+ "wmetal": _flat("White_Metal", (0.95, 0.95, 0.94), rough=0.4),
+ "face": _flat("Whiteboard_Face", (0.965, 0.965, 0.955), rough=0.2),
+ "dark": _flat("Dark_Grey", (0.24, 0.24, 0.26), rough=0.6),
+ "black": _flat("Matte_Black", (0.05, 0.05, 0.05), rough=0.6),
+ "red": _flat("Signal_Red", (0.70, 0.09, 0.07), rough=0.5),
+ "green": _flat("Sign_Green", (0.02, 0.32, 0.18), rough=0.5),
+ "bin": _flat("Bin_Green", (0.13, 0.52, 0.20), rough=0.4),
+ "bin_lt": _flat("Bin_Liner", (0.72, 0.85, 0.70), rough=0.3),
+ "teal": _flat("Poster_Teal", (0.10, 0.35, 0.40), rough=0.8),
+ "glass": _flat("Door_Glass", (0.62, 0.67, 0.70), rough=0.08, metal=0.1),
+ "blue": _flat("Sticker_Blue", (0.12, 0.25, 0.62), rough=0.5),
+ "oak": _pbr_mat("Door_Oak", "door_oak", scale=1.6, rough_fallback=0.45),
+ }
+
+
+# ---------------------------------------------------------------- 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, 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 _free_spans2(wall, margin=0.04, z_below=0.95):
+ """Along-wall [(u0,u1)] NOT covered by a floor-reaching opening (door)."""
+ w = SHELL["walls"][wall]
+ L = math.hypot(w["end"][0] - w["start"][0], w["end"][1] - w["start"][1])
+ blocks = sorted(
+ (op["offset"] - op["width"] / 2 - margin, op["offset"] + op["width"] / 2 + margin)
+ for op in SHELL["openings"].values()
+ if op["wall"] == wall and op["sill"] < z_below
+ )
+ spans, cur = [], 0.005
+ for b0, b1 in blocks:
+ if b0 > cur + 0.02:
+ spans.append((cur, b0))
+ cur = max(cur, b1)
+ if cur < L - 0.02:
+ spans.append((cur, L - 0.005))
+ return spans
+
+
+def _part(name, sx, sy, sz, mat):
+ """A metric box mesh (size baked into vertices)."""
+ 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."""
+ s, d, n, L = _wall_frame(wall)
+ ob = _part(f"{wall}_{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 _spin(ob, deg):
+ """Rotate a _wall_box part about the wall normal (its local Y) — e.g. 45°
+ turns a square window into the diamond porthole on the Wall6 door."""
+ ob.matrix_world = ob.matrix_world @ Matrix.Rotation(math.radians(deg), 4, "Y")
+ return ob
+
+
+def _free_box(name, cx, cy, z_abs, ang_deg, sx, sy, sz, mat):
+ """World-placed box (for ceiling fixtures): centre (cx,cy,z_abs), yaw deg."""
+ ob = _part(name, sx, sy, sz, mat)
+ r = math.radians(ang_deg)
+ c, s_ = math.cos(r), math.sin(r)
+ ob.matrix_world = Matrix(
+ (
+ (c, -s_, 0.0, cx),
+ (s_, c, 0.0, cy),
+ (0.0, 0.0, 1.0, z_abs),
+ (0.0, 0.0, 0.0, 1.0),
+ )
+ )
+ return ob
+
+
+# ---------------------------------------------------------------- ceiling
+# corner loop in wall-adjacency order: W3end->W7->W6->W0->W4->W2->W5->W1
+_CEIL_LOOP = [
+ (-0.4511, -2.0386), (-1.3002, -1.8567), (-1.1228, -0.8012), (-3.4218, 2.5598),
+ (-2.0599, 3.4914), (-0.5941, 1.3484), (-0.0796, 1.7003), (1.5443, -0.6737),
+]
+
+
+def build_ceiling():
+ if bpy.data.objects.get("Ceiling0") is not None:
+ return
+ cz = SHELL["ceiling_z"]
+ me = bpy.data.meshes.new("Ceiling0")
+ me.from_pydata([(x, y, cz) for x, y in _CEIL_LOOP], [], [tuple(range(len(_CEIL_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):
+ paint = {
+ "Wall0": M["cream"], "Wall1": M["grey"], "Wall2": M["grey2"],
+ "Wall3": M["sage"], "Wall4": M["offwhite"], "Wall5": M["cream"],
+ "Wall6": M["cream"], "Wall7": M["cream"],
+ }
+ for wn, mat in paint.items():
+ o = bpy.data.objects.get(wn)
+ if o is not None:
+ o.data.materials.clear()
+ o.data.materials.append(mat)
+ fl = bpy.data.objects.get("Floor0")
+ if fl is not None:
+ fl.data.materials.clear()
+ fl.data.materials.append(_pbr_mat("Floor_Carpet", "floor_carpet", scale=1.0))
+
+
+# ---------------------------------------------------------------- fixtures
+def _skirting(wall, idx, M):
+ """Blue-grey skirting board + slightly proud top lip; breaks at doors."""
+ parts = []
+ for u0, u1 in _free_spans2(wall):
+ um, ln = (u0 + u1) / 2.0, u1 - u0
+ parts.append(_wall_box(wall, um, 0.050, ln, 0.100, 0.015, M["skirt"], name="skirt"))
+ parts.append(_wall_box(wall, um, 0.108, ln, 0.016, 0.021, M["skirt"], name="skirt_lip"))
+ if parts:
+ group_fixture(f"Skirting{idx}", "skirting", parts)
+
+
+def _whiteboard(name, wall, uc, zc, W, H, M):
+ """Aluminium frame (4 bars) + gloss face + pen tray + corner caps."""
+ parts = []
+ parts.append(_wall_box(wall, uc, zc + H / 2 - 0.0125, W, 0.025, 0.018, M["alu"], name="wb_frame_t"))
+ parts.append(_wall_box(wall, uc, zc - H / 2 + 0.0125, W, 0.025, 0.018, M["alu"], name="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="wb_frame_s"))
+ parts.append(_wall_box(wall, uc, zc, W - 0.05, H - 0.05, 0.010, M["face"], out=0.004, name="wb_face"))
+ parts.append(_wall_box(wall, uc, zc - H / 2 - 0.012, 0.40, 0.014, 0.055, M["alu"], name="wb_tray"))
+ parts.append(_wall_box(wall, uc, zc - H / 2 - 0.017, 0.40, 0.012, 0.006, M["alu"], out=0.049, name="wb_tray_lip"))
+ for du in (-(W / 2 - 0.015), W / 2 - 0.015):
+ for dz in (-(H / 2 - 0.015), H / 2 - 0.015):
+ parts.append(_wall_box(wall, uc + du, zc + dz, 0.032, 0.032, 0.022, M["dark"], name="wb_cap"))
+ group_fixture(name, "whiteboard", parts)
+
+
+def _thermostat(name, wall, u, z, M):
+ """Wall thermostat: backplate + raised body + dark display slot."""
+ parts = [
+ _wall_box(wall, u, z, 0.080, 0.080, 0.010, M["white"], name="th_plate"),
+ _wall_box(wall, u, z, 0.062, 0.048, 0.014, M["white"], out=0.010, name="th_body"),
+ _wall_box(wall, u, z + 0.008, 0.036, 0.014, 0.004, M["dark"], out=0.024, name="th_lcd"),
+ ]
+ group_fixture(name, "thermostat", parts)
+
+
+def _exit_sign(name, wall, u, z, M):
+ """Green fire-exit plate + white pictogram inset + top wall bracket. Stood
+ ~0.10 m off the wall: the door GLBs protrude past the wall plane and would
+ otherwise bury a flush sign."""
+ parts = [
+ _wall_box(wall, u, z + 0.085, 0.030, 0.020, 0.100, M["wmetal"], name="ex_bracket"),
+ _wall_box(wall, u, z, 0.360, 0.150, 0.020, M["green"], out=0.095, name="ex_plate"),
+ _wall_box(wall, u - 0.06, z, 0.150, 0.095, 0.005, M["white"], out=0.115, name="ex_pictogram"),
+ _wall_box(wall, u + 0.11, z, 0.070, 0.028, 0.005, M["white"], out=0.115, name="ex_arrow"),
+ ]
+ group_fixture(name, "exit_sign", parts)
+
+
+def _wall_panels(name, wall, M, u0=0.10, u1=2.50, z0=0.80, z1=2.24, zj=1.55):
+ """Wall2's flush light-grey panel wall (whiteboard-style boards screwed to
+ the wall): two stacked boards + joint strip + screw-pair fixings."""
+ pan = _flat("Panel_Grey", (0.72, 0.73, 0.73), rough=0.38)
+ um, ln = (u0 + u1) / 2.0, u1 - u0
+ parts = [
+ _wall_box(wall, um, (zj + z1) / 2, ln, z1 - zj, 0.014, pan, name="pan_upper"),
+ _wall_box(wall, um, (z0 + zj) / 2, ln, zj - z0, 0.014, pan, name="pan_lower"),
+ _wall_box(wall, um, zj, ln, 0.010, 0.004, M["dark"], out=0.014, name="pan_joint"),
+ _wall_box(wall, 1.35, (zj + z1) / 2, 0.008, z1 - zj, 0.004, M["dark"],
+ out=0.014, name="pan_vjoint"),
+ ]
+ for u in (0.55, 1.30, 2.05): # screw pairs along the top edge
+ for du in (-0.025, 0.025):
+ parts.append(_wall_box(wall, u + du, z1 - 0.08, 0.014, 0.014, 0.006,
+ M["dark"], out=0.014, name="pan_screw"))
+ group_fixture(name, "wall_panel", parts)
+
+
+def _poster(name, wall, u, z, W, H, M, face="teal"):
+ """Silver snap frame + inset artwork + red accent stripe."""
+ parts = []
+ parts.append(_wall_box(wall, u, z + H / 2 - 0.011, W, 0.022, 0.016, M["alu"], name="po_frame_t"))
+ parts.append(_wall_box(wall, u, z - H / 2 + 0.011, W, 0.022, 0.016, M["alu"], name="po_frame_b"))
+ for du in (-(W / 2 - 0.011), W / 2 - 0.011):
+ parts.append(_wall_box(wall, u + du, z, 0.022, H - 0.044, 0.016, M["alu"], name="po_frame_s"))
+ parts.append(_wall_box(wall, u, z, W - 0.044, H - 0.044, 0.006, M[face], out=0.004, name="po_face"))
+ parts.append(_wall_box(wall, u - W / 2 + 0.06, z, 0.030, H - 0.05, 0.004, M["red"],
+ out=0.011, name="po_stripe"))
+ group_fixture(name, "poster", parts)
+
+
+def _vsign(name, wall, u, z, M):
+ """Small vertical red extinguisher-ID sign: red plate + white icon field."""
+ parts = [
+ _wall_box(wall, u, z, 0.100, 0.220, 0.006, M["red"], name="vs_plate"),
+ _wall_box(wall, u, z + 0.045, 0.062, 0.085, 0.004, M["white"], out=0.006, name="vs_icon"),
+ ]
+ group_fixture(name, "sign", parts)
+
+
+def _notice(name, wall, u, z, M):
+ """Fire-action notice: white sheet + red header band + holder lip."""
+ parts = [
+ _wall_box(wall, u, z, 0.140, 0.195, 0.006, M["white"], name="no_sheet"),
+ _wall_box(wall, u, z + 0.075, 0.128, 0.038, 0.004, M["red"], out=0.006, name="no_header"),
+ _wall_box(wall, u, z - 0.093, 0.140, 0.010, 0.010, M["white"], name="no_lip"),
+ ]
+ group_fixture(name, "notice", parts)
+
+
+def _fan(name, wall, u, M):
+ """Extractor fan high on Wall3: body + louvred face + duct up to ceiling."""
+ parts = [
+ _wall_box(wall, u, 2.02, 0.26, 0.26, 0.13, M["white"], name="fan_body"),
+ _wall_box(wall, u, 2.02, 0.20, 0.20, 0.015, M["white"], out=0.13, name="fan_face"),
+ ]
+ for i in range(4): # louvre slats on the face
+ parts.append(_wall_box(wall, u, 1.955 + i * 0.045, 0.16, 0.012, 0.008,
+ M["dark"], out=0.138, name="fan_louvre"))
+ parts.append(_wall_box(wall, u, 2.245, 0.095, 0.17, 0.095, M["white"], name="fan_duct"))
+ group_fixture(name, "extractor_fan", parts)
+
+
+def _extinguishers(name, wall, u, M):
+ """Double extinguisher stand: plinth + back + 2 red bottles + valves + hoses."""
+ parts = [_wall_box(wall, u, 0.05, 0.64, 0.10, 0.32, M["dark"], out=0.02, name="ext_base")]
+ for du in (-0.155, 0.155):
+ parts.append(_wall_box(wall, u + du, 0.33, 0.145, 0.46, 0.145, M["red"],
+ out=0.10, name="ext_bottle"))
+ parts.append(_wall_box(wall, u + du, 0.60, 0.05, 0.09, 0.05, M["black"],
+ out=0.145, name="ext_valve"))
+ parts.append(_wall_box(wall, u + du + 0.085, 0.42, 0.022, 0.28, 0.022, M["black"],
+ out=0.16, name="ext_hose"))
+ group_fixture(name, "extinguisher", parts)
+
+
+def _bin(name, wall, u, M):
+ """Green recycling bin: body + pale liner lip + white label."""
+ parts = [
+ _wall_box(wall, u, 0.235, 0.38, 0.47, 0.34, M["bin"], out=0.03, name="bin_body"),
+ _wall_box(wall, u, 0.485, 0.40, 0.055, 0.36, M["bin_lt"], out=0.02, name="bin_liner"),
+ _wall_box(wall, u, 0.28, 0.11, 0.075, 0.006, M["white"], out=0.372, name="bin_label"),
+ ]
+ group_fixture(name, "bin", parts)
+
+
+
+def _socket_plugs(name, wall, u, z, M):
+ """UK double socket + 2 plugged-in plugs with cable drops."""
+ parts = [_wall_box(wall, u, z, 0.146, 0.086, 0.012, M["white"], name="sock_plate")]
+ for du in (-0.034, 0.034):
+ parts.append(_wall_box(wall, u + du, z - 0.004, 0.036, 0.040, 0.007, M["white"],
+ out=0.012, name="sock_outlet"))
+ parts.append(_wall_box(wall, u + du, z + 0.030, 0.013, 0.016, 0.005, M["red"],
+ out=0.012, name="sock_rocker"))
+ parts.append(_wall_box(wall, u + du, z - 0.006, 0.048, 0.044, 0.038, M["white"],
+ out=0.019, name="sock_plug"))
+ parts.append(_wall_box(wall, u + du, z - 0.19, 0.012, 0.33, 0.012, M["black"],
+ out=0.045, name="sock_cable"))
+ group_fixture(name, "socket", parts)
+
+
+
+def _wall6_door(name, wall, M):
+ """Scan-missed closed door on Wall6, built as a wall fixture: white
+ architrave + oak leaf + diamond porthole + sign + glazed panel + handle."""
+ uc = 0.535
+ parts = []
+ for du in (-0.455, 0.455): # architrave uprights
+ parts.append(_wall_box(wall, uc + du, 1.0675, 0.090, 2.135, 0.060, M["trim"], name="dr_arch"))
+ parts.append(_wall_box(wall, uc, 2.175, 1.000, 0.080, 0.060, M["trim"], name="dr_arch_head"))
+ parts.append(_wall_box(wall, uc, 1.015, 0.820, 2.030, 0.040, M["oak"], out=0.004, name="dr_leaf"))
+ # diamond porthole (spun 45°): white frame square + glass square on top
+ parts.append(_spin(_wall_box(wall, 0.475, 1.70, 0.260, 0.260, 0.018, M["trim"],
+ out=0.045, name="dr_dia_frame"), 45))
+ parts.append(_spin(_wall_box(wall, 0.475, 1.70, 0.185, 0.185, 0.008, M["glass"],
+ out=0.064, name="dr_dia_glass"), 45))
+ # "Meeting Room" sign plate: white base + green + red bands
+ parts.append(_wall_box(wall, 0.58, 1.44, 0.300, 0.110, 0.008, M["white"], out=0.045, name="dr_sign"))
+ parts.append(_wall_box(wall, 0.58, 1.472, 0.264, 0.034, 0.004, M["green"], out=0.053, name="dr_sign_g"))
+ parts.append(_wall_box(wall, 0.58, 1.402, 0.264, 0.020, 0.004, M["red"], out=0.053, name="dr_sign_r"))
+ # glazed panel: white surround + grey glass inset
+ parts.append(_wall_box(wall, 0.56, 1.10, 0.300, 0.280, 0.020, M["trim"], out=0.045, name="dr_panel"))
+ parts.append(_wall_box(wall, 0.56, 1.10, 0.230, 0.210, 0.008, M["glass"], out=0.066, name="dr_panel_glass"))
+ # blue "fire door" disc + lever handle on rose (handle side)
+ parts.append(_wall_box(wall, 0.75, 1.55, 0.085, 0.085, 0.005, M["blue"], out=0.045, name="dr_sticker"))
+ parts.append(_wall_box(wall, 0.86, 1.00, 0.045, 0.045, 0.014, M["alu"], out=0.045, name="dr_rose"))
+ parts.append(_wall_box(wall, 0.805, 1.00, 0.130, 0.020, 0.020, M["alu"], out=0.059, name="dr_lever"))
+ group_fixture(name, "door", parts)
+
+
+# ---------------------------------------------------------------- ceiling kit
+def _ceiling_vent(name, cx, cy, ang, M):
+ """4-way diffuser: stack of shrinking square plates -> concentric pyramid."""
+ cz = SHELL["ceiling_z"]
+ parts = []
+ for i, s in enumerate((0.60, 0.47, 0.34, 0.22, 0.11)):
+ parts.append(_free_box(f"Ceiling0_vent{i}", cx, cy, cz - 0.004 - 0.013 * i,
+ ang, s, s, 0.012, M["wmetal"]))
+ group_fixture(name, "vent", parts)
+
+
+def _smoke(name, cx, cy, M):
+ parts = [
+ _free_box("Ceiling0_smoke_base", cx, cy, SHELL["ceiling_z"] - 0.010, 0,
+ 0.105, 0.105, 0.020, M["white"]),
+ _free_box("Ceiling0_smoke_dome", cx, cy, SHELL["ceiling_z"] - 0.028, 0,
+ 0.060, 0.060, 0.018, M["white"]),
+ ]
+ group_fixture(name, "smoke_detector", parts)
+
+
+def _power_rail(name, M):
+ """Suspended white power rail over the window desk: rail + socket bumps +
+ 2 black plugs with cable drops (seen in the capture frames)."""
+ cz = SHELL["ceiling_z"]
+ s, d, n, L = _wall_frame("Wall0")
+ c = s + d * 3.25 + n * 0.42
+ ang = math.degrees(math.atan2(d.y, d.x))
+ parts = [_free_box("Ceiling0_rail", c.x, c.y, cz - 0.045, ang, 1.15, 0.075, 0.065, M["white"])]
+ for t in (-0.42, -0.05, 0.33):
+ p = s + d * (3.25 + t) + n * 0.42
+ parts.append(_free_box("Ceiling0_rail_sock", p.x, p.y, cz - 0.085, ang,
+ 0.075, 0.055, 0.018, M["white"]))
+ for t in (-0.28, 0.18):
+ p = s + d * (3.25 + t) + n * 0.42
+ parts.append(_free_box("Ceiling0_rail_plug", p.x, p.y, cz - 0.105, ang,
+ 0.045, 0.040, 0.045, M["black"]))
+ parts.append(_free_box("Ceiling0_rail_cable", p.x, p.y, cz - 0.30, ang,
+ 0.012, 0.012, 0.35, M["black"]))
+ group_fixture(name, "power_rail", parts)
+
+
+def build_fixtures(M):
+ # skirting everywhere except Wall6 (its closed-door fixture fills the wall)
+ for i, wn in enumerate(sorted(SHELL["walls"])):
+ if wn == "Wall6":
+ continue
+ _skirting(wn, i, M)
+
+ # ---- Wall0 (long cream wall): whiteboard over the tape shelf + thermostat
+ _whiteboard("Whiteboard0", "Wall0", 1.90, 1.55, 1.30, 0.92, M)
+ _thermostat("Thermostat0", "Wall0", 3.22, 1.42, M)
+
+ # ---- Wall1 (grey wall, Door1 at far end): exit sign above the door
+ _exit_sign("ExitSign0", "Wall1", SHELL["openings"]["Door1"]["offset"], 2.19, M)
+
+ # ---- Wall2: flush panel wall (the white cupboards seen in frame 40 are
+ # freestanding furniture beside the window, not on this wall) + desk sockets
+ _wall_panels("Panel0", "Wall2", M)
+ _socket_plugs("Socket1", "Wall2", 1.55, 0.40, M)
+ _socket_plugs("Socket2", "Wall2", 2.15, 0.40, M)
+
+ # ---- Wall3 (sage wall, Door0 at start end): poster, signs, fan, stand, bin
+ # (stitch is MIRRORED — placements below are un-mirrored reality, cross-
+ # checked vs raw frames 5/15/70: fan + red ID sign near the Wall7 corner,
+ # extinguisher stand + green bin right beside the door)
+ _poster("Poster0", "Wall3", 1.84, 1.66, 0.46, 0.36, M)
+ _vsign("FireSign0", "Wall3", 2.12, 1.42, M)
+ _notice("Notice0", "Wall3", 1.45, 1.35, M)
+ _fan("Fan0", "Wall3", 2.18, M)
+ _extinguishers("Extinguishers0", "Wall3", 1.18, M)
+ _bin("Bin0", "Wall3", 1.68, M)
+
+ # ---- Wall4 (window wall): socket below the sill (the window GLB already
+ # carries its own dark frame + glazing bars, so no add-on grille/blind)
+ _socket_plugs("Socket0", "Wall4", 0.42, 0.86, M)
+
+ # ---- Wall6: the scan-missed closed door, as a grouped fixture
+ _wall6_door("Door3", "Wall6", M)
+
+ # ---- Wall7 (open Door2 to the stairwell): exit sign above the door
+ _exit_sign("ExitSign1", "Wall7", SHELL["openings"]["Door2"]["offset"], 2.20, M)
+
+ # ---- ceiling: two 4-way diffusers + smoke detector + power rail
+ _ceiling_vent("Vent0", -1.05, -1.15, -12.0, M)
+ _ceiling_vent("Vent1", -2.35, 2.30, 34.0, M)
+ _smoke("Smoke0", -1.40, 0.55, M)
+ _power_rail("PowerRail0", M)
+
+
+def harness_stage(scene):
+ """Agent-authored pass over the blank build: ceiling, 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 Room.md and Room.py in full, then compare against the stitches and references.
⌘ Bash🔎 Glob📄 Read /scratch2/code/LiteReality_to_share/agen📄 Read /scratch2/code/LiteReality_to_share/agen📄 Read /scratch2/code/LiteReality_to_share/agen📄 Read /home/zelda/zh340/.claude/projects/-scra⌘ Bash
•STEP #2
The QC recipe from the previous scene applies directly. Let me read the window and door object.py files to check glass + articulation.
📄 Read /scratch2/code/LiteReality_to_share/agen⌘ 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📄 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⌘ Bash
✏️EDIT #3
No room_preview exists yet — the render tool will drive the rebuild. Let me load the render MCP tool schema.
🔧 ToolSearch select:mcp__cap__render,mcp__cap__critic✏️ Edit /scratch2/code/LiteReality_to_share/agen✏️ Edit /scratch2/code/LiteReality_to_share/agen
✏️EDIT #4
Now the window blinds articulation in `Wall4_Window_0/object.py` — separate the static headrail, join the vanes with origin at the left end, and add a
✏️ Edit /scratch2/code/LiteReality_to_share/agen⌘ Bash
👁INSPECT #5
Both files compile. Now let me render the room to verify the build still compiles with my edits and get an overview:
scene · scene_frame_00010
scene · scene_frame_00040
scene · scene_frame_00060
scene · scene_frame_00068
scene · scene_frame_00072🖼 render room
•STEP #6
The MCP render tool can't pass `scan=` — per my memory, I need the render engine CLI directly. Let me check its interface:
⌘ Bash⌘ Bash⌘ Bash📄 Read /home/zelda/zh340/.claude/projects/-scra
✏️EDIT #7
Frames 0–73 available. Let me render the wall refs for the two surfaces I touched (Wall4 window, Wall6 fixture door) — this also drives the compact-ro
⌘ Bash📄 Read /scratch2/code/LiteReality_to_share/agen📄 Read /scratch2/code/LiteReality_to_share/agen⌘ Bash⌘ Bash⌘ Bash⌘ Bash⌘ Bash📄 Read /scratch2/code/LiteReality_to_share/agen✏️ Edit /scratch2/code/LiteReality_to_share/agen⌘ Bash📄 Read /scratch2/code/LiteReality_to_share/agen
•STEP #8
Possibly a stale cached Room.glb. Let me check timestamps:
⌘ Bash⌘ Bash⌘ Bash⌘ Bash⌘ Bash
•STEP #9
You're out of usage credits. Run /usage-credits to keep using Fable 5 or /model to switch models.
opus · qc_opus_20260720_082943.log
•STEP #1
⌘ 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 Objects directory and the window/door object.py files to check articulation and glass.
⌘ Bash📄 Read /scratch2/code/LiteReality_to_share/agen⌘ Bash⌘ Bash
•STEP #3
The window and doors are well-authored (articulated, transparent glass). Let me look at the object references vs their builders, and set up rendering.
📄 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 #4
Now let me set up the render tool to check the actual assembled room.
scene · scene_frame_00010
scene · scene_frame_00040
scene · scene_frame_00060
scene · scene_frame_00068
scene · scene_frame_00072🔧 ToolSearch select:mcp__cap__render🖼 render room⌘ Bash⌘ Bash⌘ Bash⌘ Bash⌘ Bash⌘ Bash⌘ Bash⌘ Bash📄 Read /scratch2/code/LiteReality_to_share/agen📄 Read /scratch2/code/LiteReality_to_share/agen📄 Read /scratch2/code/LiteReality_to_share/agen⌘ Bash📄 Read /scratch2/code/LiteReality_to_share/agen📄 Read /scratch2/code/LiteReality_to_share/agen
•STEP #5
Let me verify the door openings and glass on Wall1 (Door1) and the window on Wall4.
⌘ Bash📄 Read /scratch2/code/LiteReality_to_share/agen📄 Read /scratch2/code/LiteReality_to_share/agen⌘ Bash
•STEP #6
All 4 openings cut cleanly, 10 assets placed. Let me confirm the window and a glazed door leaf render correctly in-context.
⌘ Bash📄 Read /scratch2/code/LiteReality_to_share/agen📄 Read /scratch2/code/LiteReality_to_share/agen
•STEP #7
The room is in good shape. Let me do a final verification of the two remaining glazed doors (Door0, Door1) to be complete, then clean up.
⌘ Bash📄 Read /scratch2/code/LiteReality_to_share/agen⌘ Bash
•STEP #8
I completed a full QC pass on `Room.py` (read Room.md + Room.py in full, checked object.py builders for the window and all three doors, and render-che
I completed a full QC pass on `Room.py` (read Room.md + Room.py in full, checked object.py builders for the window and all three doors, and render-checked the assembled scene against the capture frames and object references). All four openings cut cleanly (`cut 4/4 openings`, 10 assets placed — no collapses, nothing missing).
**Result: no edits were needed — the authoring already satisfies every QC criterion.** Per-surface verification:
- **Openings (Door0/Wall3, Door1/Wall1, Door2/Wall7, Window0/Wall4)** — all are true cut holes with articulated leaves/panes sitting in them (revolute doors 0→90°, window right-sash prismatic slide). Confirmed in-context: Door0 oak leaf in a cut lining (frame 8), Door2 in a cut opening (frame 10), Window0 in a cut hole with daylight behind (frame 40). No solid slabs blocking.
- **Glass** — every pane is transmissive: window glass `transmission 0.94`, door vision panels `0.92`, Wall6-door fixture glass `_glassmat` (Transmission 1.0). No opaque "glass".
- **Window0** — articulated (sliding sash + traversing vertical blinds, blinds anchored to a headrail and gathering clear of the glass when open). Only one window on the wall, so no duplicates to unify.
- **Wall fixtures** — no fixture overlaps an opening: skirting breaks at doors (`_free_spans2`), Wall3 fixtures all sit at u≥1.18 (clear of Door0's 0.15–0.97 span), the Wall4 socket sits *below* the window sill, exit signs sit *above* the door heads. Nothing on the windows.
- **Ceiling0** — materialed (`_ceiling_mat`: plasterboard PBR + seam lines).
- **Objects vs reference** — Storage0 (tall oak bookcase), Storage1 (low oak bookcase), Storage2 (swing bin), Table0 (oak-top pedestal desk) all match their reference type/proportions at a glance.
- **Lighting** — the only emitter is the base-build daylight card behind the window (represents real daylight through the glazing); no fake in-room lights added by the harness.
The one cosmetic nitpick (the window frame's normal map reuses `metal_plate`, a tread-plate texture) is existing, good authoring at `normal_strength 0.20` — not something I added, so per the "don't restyle existing good surfaces" rule I left it untouched. Temp render helpers were cleaned up; `Room.py` is unchanged and remains valid Python.
④ Reference images used
Object references (object refs)real crop → reference · 4








Object references (opening refs)real crop → reference · 4








Surface references (per-wall stitches)authoring ground truth · 10
Final — render | photo

















