""" The sub bay's bare places in the wall buffer's smooth finish. The user, 2026-10-23, with three pictures of the moon pool room (the fore end wall beside the command deck doorway over the gallery, the gallery's front edge, the port wall's ledge seen from above, a close look at the band beside the doorway): "what to do those places without texture? try to add that black marble texture there". A black marble made from their Fab set was sent back the same hour, with a picture of the dark wall buffer beside the lower fore door (WallBuffer_A16.004, material IN_MI_WallBuffer_A): "that was not what I meant ... I meant this texture replace with that texture smooth forget about marble". What had no texture, and what it is now: bands two bands of the fore end wall beside the command deck doorway, 0.81 m wide, from the gallery up to the vault: the perforated plates were cut there by mistake and the bulkhead showed (PBR | Moonpool interior, a flat colour). The ceiling keeps them open on purpose now (moonpool_ceiling.DOOR_BANDS); panels fill them, flush with the plates and as thick (2 cm), 1 mm clear of the plates' cut edges, jointed where the plates have their reveals (2 cm, at z 4.80 and 6.60), cut to the same outline under the end trim. lining the room's dark structure: the ledges round the walls under the vault (z 1.14 to 1.20), the piers in the corners and at the wall ends, the door lintels and jambs: 17 objects in LINING | dark structure, a flat colour, with no UVs to speak of. Their meshes are swapped for copies in the finish, UVs in metres. gallery the command deck landing's edges (front, ends, back): its slab material, PBR | Graphite lower hull, lies at 4 m a repeat, which reads as flat on a 22 cm edge. A copy of its mesh carries the finish on the edges; its top and underside keep the graphite. pieces then, with a picture of the carousel's pylon (cylinder and both flanges, selected): "do the same with this piece". PIECES: a copy of the mesh with every face in the finish (the docking arm's own materials, shared with its other parts, stay as they are), UVs in metres round the pylon's axis. The finish, IN_MI_WallBuffer_A | smooth: a copy of the kit's wall buffer material. That material reads an atlas made for the buffer's own mesh (T_Hall_Wall_Buffer_Diff, _M, _N), so the copy reads its three atlas maps at one point of the buffer's plain front (PIN_UV) and keeps the kit's scratch map on the roughness, tiled over the faces' UVs. The faces' UVs: each face projected on the axis plane it faces, metres times K (about the buffer front's own density), so the scratches run on across neighbouring faces. Not done (seen while looking, not in the pictures): under the ledges, between the wall tops (z about 0.7) and the ledge (1.14), the room is open to the bare hull lining and the end bulkheads (PBR | Moonpool interior). import sub_bay_bare_places as sbp sbp.apply() # RECORD "SUB BAY BARE PLACES | what it was" sbp.check() sbp.remove() # the meshes swapped back, the panels and the material copy taken out """ import json import math import os import sys import bmesh import bpy from mathutils import Vector from mathutils.bvhtree import BVHTree sys.dont_write_bytecode = True HERE = os.path.dirname(os.path.abspath(__file__)) if HERE not in sys.path: sys.path.insert(0, HERE) import moonpool_ceiling as mc # noqa: E402 import quarters # noqa: E402 import reactor_room as rr # noqa: E402 import shipfit # noqa: E402 TAG = "sub_bay_bare_places" ZONE = "10.1 | Sub bay" COLL = "10.1 | Sub bay | bare places" RECORD = "SUB BAY BARE PLACES | what it was" MADE_FOR = "M-Ship v2 sub bay (moon pool room) bare places in the wall buffer's finish (build/sub_bay_bare_places.py)" PRE = "SM_SBP_" NAME = "Sub bay bare places | " SUFFIX = " | wall buffer finish" SOURCE = "IN_MI_WallBuffer_A" MAT = SOURCE + " | smooth" PIN_UV = (0.19, 0.33) # a point of the buffer's plain front in its atlas (front: u 0.154-0.291, v 0.010-0.635) LINING = "LINING | dark structure" GALLERY = "Gallery | command deck landing" PIECES = ("Carousel pylon (bay roof to mount flange)",) # whole pieces in the finish (round ones: UVs round the axis) K = 0.2 # UV per metre (the buffer's front: 0.167 across, 0.25 up; its scratch map tiles twice per UV) GAP = 0.001 # the band panels stand this far from the plates' cut edges GRID = 0.3 def exists(): return RECORD in bpy.data.texts def record(): return json.loads(bpy.data.texts[RECORD].as_string()) if exists() else None _bb = rr._bb # ---- the material ----------------------------------------------------------------------------------------------- def material(): """IN_MI_WallBuffer_A | smooth (made once): the kit material with its atlas maps pinned to the buffer's front.""" m = bpy.data.materials.get(MAT) if m is not None: return m, False m = bpy.data.materials[SOURCE].copy() m.name = MAT nt = m.node_tree pin = nt.nodes.new("ShaderNodeCombineXYZ") pin.name = pin.label = "Pinned to the buffer's front" pin.inputs["X"].default_value, pin.inputs["Y"].default_value = PIN_UV pin.location = (-1100, 200) pinned = [] for n in nt.nodes: if n.type == "TEX_IMAGE" and n.image is not None and not n.inputs["Vector"].is_linked: nt.links.new(pin.outputs["Vector"], n.inputs["Vector"]) pinned.append(n.image.name) m["made_for"] = MADE_FOR m["source"] = "copy of %s; its atlas maps read at %s, the scratch map on the faces' UVs" % (SOURCE, PIN_UV) m["pinned"] = ", ".join(pinned) return m, True # ---- UVs ------------------------------------------------------------------------------------------------------------ def _uv(p, n): """A world point on a face with world normal n: the axis plane the face looks along, metres times K, read as someone facing the face reads it (not mirrored).""" ax = max(range(3), key=lambda i: abs(n[i])) if ax == 0: return (-p.y * K if n.x < 0 else p.y * K), p.z * K if ax == 1: return (p.x * K if n.y < 0 else -p.x * K), p.z * K return p.x * K, (p.y * K if n.z > 0 else -p.y * K) def _render_uv(me): """The UV layer the renderer reads (the materials' image nodes take the default UVs).""" return next((l for l in me.uv_layers if l.active_render), None) or me.uv_layers.active or me.uv_layers.new(name="UVMap") def _lay_uvs(o, me, faces): uvl = _render_uv(me) mw = o.matrix_world m3 = mw.to_3x3() for p in faces: n = (m3 @ p.normal).normalized() for li in p.loop_indices: co = mw @ me.vertices[me.loops[li].vertex_index].co uvl.data[li].uv = _uv(co, n) def _lay_uvs_round(o, me, faces): """A round piece: its sides unrolled round the axis through its vertices' mean (arc length times K, up times K), its caps as _uv does them.""" uvl = _render_uv(me) mw = o.matrix_world m3 = mw.to_3x3() cs = [mw @ v.co for v in me.vertices] cx, cy = sum(c.x for c in cs) / len(cs), sum(c.y for c in cs) / len(cs) for p in faces: n = (m3 @ p.normal).normalized() lis = list(p.loop_indices) pts = [mw @ me.vertices[me.loops[li].vertex_index].co for li in lis] if abs(n.z) > 0.7: for li, co in zip(lis, pts): uvl.data[li].uv = _uv(co, n) continue c = sum(pts, Vector()) / len(pts) ac = math.atan2(c.y - cy, c.x - cx) for li, co in zip(lis, pts): a = math.atan2(co.y - cy, co.x - cx) a += 2.0 * math.pi * round((ac - a) / (2.0 * math.pi)) # (no jump across the seam) r = math.hypot(co.x - cx, co.y - cy) uvl.data[li].uv = (a * r * K, co.z * K) def _smooth_by_angle(me, angle=30.0): """Smooth shading with the edges between faces more than `angle` apart kept sharp: a faceted round piece in a glossy finish shows every facet as a stripe (the pylon's 32 sides did).""" lim = math.cos(math.radians(angle)) faces_of = {} for p in me.polygons: p.use_smooth = True for ek in p.edge_keys: faces_of.setdefault(ek, []).append(p.index) n = 0 for e in me.edges: fs = faces_of.get(tuple(sorted(e.vertices)), []) sharp = len(fs) != 2 or me.polygons[fs[0]].normal.dot(me.polygons[fs[1]].normal) < lim e.use_edge_sharp = sharp n += sharp return n def _swap_copy(o, rec, note): src = o.data me = src.copy() me.name = src.name + SUFFIX me["made_for"] = MADE_FOR me["note"] = note src.use_fake_user = True o.data = me rec["swapped"].append([o.name, src.name, me.name]) rec["meshes"].append(me.name) return me # ---- the bands beside the command deck doorway --------------------------------------------------------------------- def _shell(room): """The vault's sections, from the lining as it was before the ceiling cut it (the ceiling keeps that mesh).""" lo = bpy.data.objects[mc.SHELL] me = lo.data crec = mc.record() for name, old, new in (crec or {}).get("swapped", []): if name == mc.SHELL and old in bpy.data.meshes: me = bpy.data.meshes[old] me.calc_loop_triangles() mw = lo.matrix_world lining = ([tuple(mw @ v.co) for v in me.vertices], [tuple(t.vertices) for t in me.loop_triangles]) return mc.Shell(lining, room.z0, room.ledge_out) def _band_panels(rec, coll, root): room = mc.Room() sh = _shell(room) xf = room.x1 - mc.END_FACE back = mc.PANEL_T sec = sh.section(xf) ss = [sec["s"][i] for i in range(len(sec["theta"])) if 0.0 <= sec["theta"][i] <= 180.0] outline = [] for s_ in ss: # the outline the plates are cut to (5 mm short of the end trim) p = sh.at(xf, s_, -mc.CORNER - 0.005)[0] outline.append((p.y, max(p.z, room.z0))) z0 = room.z0 + 0.001 poly = outline + [(outline[-1][0], z0 - 0.01), (outline[0][0], z0 - 0.01)] glo, ghi = room.gallery z_lo = max(mc.DOOR_BANDS_Z[0], ghi.z + 0.03) # (the plates beside the bands stop there, cut round the gallery) h = mc.END_PLATE[1] rows = [z0 + h * k for k in range(int((mc.DOOR_BANDS_Z[1] - z0) / h) + 2)] out = [] for bi, (ya0, yb0) in enumerate(mc.DOOR_BANDS): ya, yb = ya0 + GAP, yb0 - GAP z_top = min(mc.DOOR_BANDS_Z[1], max(z for y, z in outline if ya - 0.3 <= y <= yb + 0.3) + 0.05) joints = [z for z in rows if z_lo < z < z_top] slabs, za = [], z_lo for zj in joints: # panels jointed in line with the plates' reveals slabs.append((za, zj - mc.PLATE_GAP / 2)) za = zj + mc.PLATE_GAP / 2 slabs.append((za, z_top)) mb = mc.MB([MAT]) for za, zb in slabs: gy = mc._lines(ya, yb, GRID) gz = mc._lines(za, zb, GRID) for i in range(len(gy) - 1): for j in range(len(gz) - 1): q = [(xf, gy[i], gz[j]), (xf, gy[i + 1], gz[j]), (xf, gy[i + 1], gz[j + 1]), (xf, gy[i], gz[j + 1])] mb.quad(q, [_uv(Vector(c), Vector((-1, 0, 0))) for c in q], MAT, toward=(-1, 0, 0)) qb = [(xf + back, c[1], c[2]) for c in q] mb.quad(qb, [_uv(Vector(c), Vector((1, 0, 0))) for c in qb], MAT, toward=(1, 0, 0)) for (y0_, z0_), (y1_, z1_), d in (((ya, za), (yb, za), (0, 0, -1)), ((yb, zb), (ya, zb), (0, 0, 1)), ((ya, zb), (ya, za), (0, -1, 0)), ((yb, za), (yb, zb), (0, 1, 0))): q = [(xf, y0_, z0_), (xf, y1_, z1_), (xf + back, y1_, z1_), (xf + back, y0_, z0_)] mb.quad(q, [_uv(Vector(c), Vector(d)) for c in q], MAT, toward=d) me = mb.finish(PRE + "Band_%d" % (bi + 1), rec, "panel beside the command deck doorway in the wall buffer's finish (band %d)" % (bi + 1)) me["made_for"] = MADE_FOR # cut to the outline, as the plates are (only the outline's segments over the band matter) bm = bmesh.new() bm.from_mesh(me) for i in range(len(poly) - 1): (y1, z1), (y2, z2) = poly[i], poly[i + 1] if max(y1, y2) < ya - 0.05 or min(y1, y2) > yb + 0.05: continue d = Vector((0.0, y2 - y1, z2 - z1)) if d.length < 1e-6: continue no = Vector((0.0, -d.z, d.y)).normalized() bmesh.ops.bisect_plane(bm, geom=bm.verts[:] + bm.edges[:] + bm.faces[:], dist=1e-6, plane_co=(xf, y1, z1), plane_no=no) gone = [f for f in bm.faces if not mc._point_in_poly(f.calc_center_median().y, f.calc_center_median().z, poly)] bmesh.ops.delete(bm, geom=gone, context="FACES") bm.to_mesh(me) bm.free() o = bpy.data.objects.new(NAME + "band %s of the command deck doorway" % ("starboard" if ya0 < 0 else "port"), me) coll.objects.link(o) if root is not None: o.parent = root o.matrix_parent_inverse = root.matrix_world.inverted() o["made_by"] = TAG o["zone"] = ZONE rec["added"].append(o.name) out.append({"y": [round(ya, 4), round(yb, 4)], "z": [round(z_lo, 3), round(z_top, 3)], "panels": len(slabs)}) return out # ---- apply / remove --------------------------------------------------------------------------------------------- def _lining_objects(): return [o for o in bpy.context.scene.objects if o.type == "MESH" and any(s.material is not None and s.material.name == LINING for s in o.material_slots)] def apply(): if exists(): raise RuntimeError("the finish is already there (text block %r); remove() takes it out" % RECORD) if not mc.exists(): raise RuntimeError("the moon pool ceiling is not in this file: the bands it leaves open are where the panels go") mat, new = material() rec = {"version": 1, "added": [], "meshes": [], "swapped": [], "object_slots": [], "material": MAT, "material_new": new} out = {"material": MAT, "made": new} root = bpy.data.objects.get(quarters.ROOT) coll = quarters.get_collection(COLL, bpy.data.collections[ZONE]) # 1. the bands beside the command deck doorway out["bands"] = _band_panels(rec, coll, root) # 2. the dark structure: ledges, piers, lintels and jambs lin = _lining_objects() for o in lin: me = _swap_copy(o, rec, "the room's dark structure in the wall buffer's finish (UVs in metres)") for i, m in enumerate(me.materials): if m is not None and m.name == LINING: me.materials[i] = mat for i, s in enumerate(o.material_slots): if s.link == "OBJECT" and s.material is not None and s.material.name == LINING: rec["object_slots"].append([o.name, i, LINING]) s.material = mat _lay_uvs(o, me, list(me.polygons)) out["lining"] = len(lin) # 3. the gallery's edges g = bpy.data.objects.get(GALLERY) if g is not None: me = _swap_copy(g, rec, "the command deck landing with its edges in the wall buffer's finish") me.materials.append(mat) idx = len(me.materials) - 1 m3 = g.matrix_world.to_3x3() edges = [p for p in me.polygons if abs((m3 @ p.normal).normalized().z) < 0.5] for p in edges: p.material_index = idx _lay_uvs(g, me, edges) out["gallery_edge_faces"] = len(edges) # 4. the pieces the user pointed at: every face in the finish out["pieces"] = {} for name in PIECES: o = bpy.data.objects.get(name) if o is None or o.type != "MESH": out["pieces"][name] = "not in this file" continue me = _swap_copy(o, rec, "%s in the wall buffer's finish (UVs in metres round its axis)" % name) for i in range(len(me.materials)): me.materials[i] = mat for i, s in enumerate(o.material_slots): if s.link == "OBJECT" and s.material is not None and s.material != mat: rec["object_slots"].append([o.name, i, s.material.name]) s.material = mat _lay_uvs_round(o, me, list(me.polygons)) out["pieces"][name] = {"faces": len(me.polygons), "sharp_edges": _smooth_by_angle(me)} bpy.data.texts.new(RECORD).write(json.dumps(rec)) bpy.context.view_layer.update() out["objects"] = len(rec["added"]) out["swapped"] = len(rec["swapped"]) return out def remove(): rec = record() if rec is None: return {"present": False} for n in rec["added"]: o = bpy.data.objects.get(n) if o is not None: bpy.data.objects.remove(o, do_unlink=True) for name, i, old in rec.get("object_slots", []): o = bpy.data.objects.get(name) if o is not None and old in bpy.data.materials: o.material_slots[i].material = bpy.data.materials[old] for name, old, new in reversed(rec["swapped"]): o = bpy.data.objects.get(name) if o is not None and old in bpy.data.meshes: o.data = bpy.data.meshes[old] o.data.use_fake_user = False for n in rec["meshes"]: me = bpy.data.meshes.get(n) if me is not None and me.users == 0: rr._forget(n) m = bpy.data.materials.get(rec["material"]) if rec.get("material_new") and m is not None and m.users == 0: bpy.data.materials.remove(m) c = bpy.data.collections.get(COLL) if c is not None and not c.objects and not c.children: bpy.data.collections.remove(c) bpy.data.texts.remove(bpy.data.texts[RECORD]) bpy.context.view_layer.update() return {"present": True, "taken_out": len(rec["added"]), "unswapped": len(rec["swapped"])} # ---- checking ------------------------------------------------------------------------------------------------------ def _uv_scale(o, me, faces): """UV length over world length along the faces' edges: K when the UVs are metres times K.""" uvl = _render_uv(me).data mw = o.matrix_world rs = [] for p in faces[:400]: li = list(p.loop_indices) for a, b in zip(li, li[1:] + li[:1]): w = (mw @ me.vertices[me.loops[a].vertex_index].co - mw @ me.vertices[me.loops[b].vertex_index].co).length if w > 0.02: rs.append((uvl[a].uv - uvl[b].uv).length / w) return (round(min(rs), 4), round(max(rs), 4)) if rs else None def _tree(objs): vs, ts = [], [] for o in objs: me = o.data me.calc_loop_triangles() mw = o.matrix_world b = len(vs) vs += [tuple(mw @ v.co) for v in me.vertices] ts += [tuple(b + i for i in t.vertices) for t in me.loop_triangles] return BVHTree.FromPolygons(vs, ts, epsilon=0.0) def _rays_at_bands(): """Rays from the gallery toward the fore end wall across the bands: what they meet first.""" dg = bpy.context.evaluated_depsgraph_get() sc = bpy.context.scene seen = {} for ya0, yb0 in mc.DOOR_BANDS: for k in range(1, 8): y = ya0 + (yb0 - ya0) * k / 8 for zi in range(0, 36): z = 3.2 + zi * 0.1 ok, loc, no, fi, ob, mx = sc.ray_cast(dg, Vector((13.0, y, z)), Vector((1, 0, 0)), distance=4.0) if not ok: continue try: mi = ob.data.polygons[fi].material_index mn = ob.material_slots[mi].material.name except Exception: # noqa: BLE001 mn = "?" key = "%s / %s" % ((ob.original if hasattr(ob, "original") else ob).name, mn) seen[key] = seen.get(key, 0) + 1 return seen def check(others=True): out = {"present": exists()} if not out["present"]: return out rec = record() mat = bpy.data.materials.get(rec["material"]) out["missing"] = [n for n in rec["added"] if n not in bpy.data.objects] + [s[0] for s in rec["swapped"] if s[0] not in bpy.data.objects] faces, scales, wrong = 0, [], [] for name, old, new in rec["swapped"]: o = bpy.data.objects.get(name) if o is None or o.data.name != new: wrong.append(name) continue me = o.data idx = [i for i, s in enumerate(o.material_slots) if s.material == mat] mf = [p for p in me.polygons if p.material_index in idx] if name == GALLERY: m3 = o.matrix_world.to_3x3() bad = [p for p in me.polygons if (abs((m3 @ p.normal).normalized().z) < 0.5) != (p.material_index in idx)] if bad: wrong.append("%s: %d faces" % (name, len(bad))) elif len(mf) != len(me.polygons): wrong.append("%s: %d of %d" % (name, len(mf), len(me.polygons))) faces += len(mf) s = _uv_scale(o, me, mf) if s: scales.append(s) out["finish_faces"] = faces out["not_in_the_finish"] = wrong out["uv_per_metre"] = [min(s[0] for s in scales), max(s[1] for s in scales)] if scales else None out["uv_per_metre_wanted"] = K out["lining_left_flat"] = [o.name for o in _lining_objects()] out["pinned_maps"] = mat.get("pinned") if mat else None mine = [bpy.data.objects[n] for n in rec["added"] if n in bpy.data.objects] env = shipfit.Envelope() outside = {} for o in mine: r = shipfit.check_inside(o, env, spacing=0.3, clear_min=0.0) if r["outside"]: outside[o.name] = r["outside"] out["bands_outside_the_ship"] = outside if others and mine: names = {o.name for o in mine} lo = Vector((min(_bb(o)[0].x for o in mine) - 0.1, min(_bb(o)[0].y for o in mine) - 0.1, min(_bb(o)[0].z for o in mine) - 0.1)) hi = Vector((max(_bb(o)[1].x for o in mine) + 0.1, max(_bb(o)[1].y for o in mine) + 0.1, max(_bb(o)[1].z for o in mine) + 0.1)) near = [o for o in bpy.context.scene.objects if o.type == "MESH" and o.name not in names and not o.name.startswith(("KIT | ", "UCX_")) and o.visible_get() and all(_bb(o)[0][i] < hi[i] and _bb(o)[1][i] > lo[i] for i in range(3)) and o.name not in ("M_SHIP_Hull", "M_SHIP_Dorsal_Roof", "M_SHIP_Canopy")] T = _tree(mine) hits = {} for o in near: n = len(T.overlap(_tree([o]))) if n: hits[o.name] = n least = 9.0 for o in near: me = o.data mw = o.matrix_world for v in me.vertices[::max(1, len(me.vertices) // 4000)]: loc, nrm, idx, d = T.find_nearest(mw @ v.co, 0.2) if loc is not None and d < least: least = d out["bands_against"] = {"objects": len(near), "crossing_triangles": hits, "least_gap_m": round(least, 4)} out["rays_across_the_bands"] = _rays_at_bands() return out def state(): """A digest of what apply() and remove() touch, to prove remove() gives the file back as it was.""" objs = {o.name: [o.data.name if o.data else None, [s.material.name if s.material else None for s in o.material_slots]] for o in bpy.data.objects if o.type == "MESH"} meshes = {m.name: [len(m.vertices), len(m.polygons), m.use_fake_user, [x.name if x else None for x in m.materials]] for m in bpy.data.meshes} targets = _lining_objects() + [bpy.data.objects[n] for n in (GALLERY,) + PIECES if n in bpy.data.objects] uvs = {} for o in targets: me = o.data for l in me.uv_layers: uvs["%s / %s" % (o.name, l.name)] = round(sum(abs(d.uv.x) + abs(d.uv.y) for d in l.data), 4) return {"objects": objs, "meshes": meshes, "materials": sorted(m.name for m in bpy.data.materials), "images": sorted(i.name for i in bpy.data.images), "texts": sorted(t.name for t in bpy.data.texts), "collections": sorted(c.name for c in bpy.data.collections), "uvs": uvs}