""" The moon pool room's ceiling: a lined vault over the sub bay, in the works hall's materials. The user, 2026-10-22, with two pictures of the bay's bare roof (the hull's dark inside above the wall lining), one of the works hall's ceiling cove (the Season 2 kit's quilted grid between red and black trim ribs) and one of its perforated plate: "try to create those empty ceiling of moon pool room with those materials". The room is the sub bay, x -9.63 to 15.21 between its bulkheads, its walls the Hallways kit's hall walls up to a dark ledge at z 1.2. Above the ledge there was only the M-Ship model's own lining of the hull ("Moonpool | hull lining"): a near-round barrel vault, 18.5 m wide at the ledge, 7.0 to 7.1 m high at the crown, 0.28 m inside the hull's skin. vault 6 cm outside that lining (the lining's part above the ledge is cut away; remove() puts it back). It has to be: the carousel's ore hoses pass 8.6 cm under the lining when an arm points to a side (x 3, y +-6.4, z 5.47); outside it they keep 15 cm. Measured as rays from the room's axis (x, 0, ledge), so every section follows the lining exactly. coves from the ledge 6.0 m up the arc: plates of the crown's perforated metal, three to a bay along the room and three up the cove, 2 cm reveals between them. (The user, the same day: first the works hall cove's quilted grid, SI_MI_FloorPadding5; then "can you replace that aluminum ceiling with this floor material", the floor modules EN_SM_Floor_A1 / A2 bent onto the coves; then, with a picture of the crown plates: "I dont like it cover it with this material not the last one that wanted". The floor-module version is kept beside the session's scratch files, moonpool_ceiling_v2_floor.py.) crown the rest: plates of the works hall's perforated metal, SI_MI_Floor_A (1.057 tiles a metre), three to a bay along the room and six across, with 2 cm reveals between them. ribs one over each pilaster of the side walls (x -7.95 ... 14.05, every 4.4 m), the works hall cove's arch: red sides (SI_MI_Trim_A_Red) and a black soffit (SI_MI_Trim_A_Black), on the same bands of the trim sheet the kit's arch uses. A dark trim (SI_MI_Trim_A_DarkGray) along each cove's top and at both ends. pylon the carousel's pylon goes through the crown: a round dark rose round it, 5 cm clear of its top flange. ends both end walls above the ledge in the same perforated plates (2.4 x 1.8 m, 2 cm reveals), cut to the vault's outline 12 cm in (the end trim covers the joint), the fore one round the gallery, the command deck doorway and the raised door's lintel, and open in two bands beside the doorway (DOOR_BANDS) where sub_bay_bare_places.py puts panels in the wall buffer's finish. Nothing of the room is moved. The lining's upper part is the one thing taken out (as a cut copy of its mesh). Everything is written down in the text block RECORD; remove() undoes it. """ import json import math import os import sys import bmesh import bpy from mathutils import Matrix, 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 quarters # noqa: E402 import reactor_room as rr # noqa: E402 import shipfit # noqa: E402 TAG = "moonpool_ceiling" ZONE = "10.1 | Sub bay" LINING_COLL = "10.1 | Sub bay | straight lining" COLL = "10.1 | Sub bay | ceiling" RECORD = "MOONPOOL CEILING | what it was" MADE_FOR = "M-Ship v2 sub bay (moon pool room) ceiling (build/moonpool_ceiling.py)" PRE = "SM_MC_" NAME = "Moon pool ceiling | " SHELL = "Moonpool | hull lining" AFT_BULKHEAD = "Bulkhead | sub bay aft" FORE_BULKHEAD = "Bulkhead | sub bay fore" HUB = (3.05, 0.0) # the carousel's axis (DockArm_Root) OUT = 0.06 # the vault's face outside the old lining PANEL_T = 0.02 # panel thickness (toward the hull) THETA_C = 45.0 # cove / crown boundary, degrees from the springing (seen from the room's axis at the ledge) RIB_W = 0.40 RIB_D = 0.16 PURLIN_W = 0.24 PURLIN_D = 0.08 CORNER = 0.12 PLATE_GAP = 0.02 LIFT = 0.004 # trims lying on the curved panels stand this far off them (a flat chord sags under a curve) PLATES_ACROSS = 6 PLATE_X = 1.5 # crown plates along the room: as many as fit at about this length STEP_X = 0.6 STEP_S = 0.22 ROSE_IN = 1.25 # the pylon's top flange is r 1.20 ROSE_OUT = 1.95 ROSE_D = 0.10 COVE_LEN = 6.0 # the coves' arc from the springing; the cove trim covers the joint at its top COVE_PLATES = 3 # plates up each cove END_FACE = 0.022 # the end walls' face this far inside the end planes END_PLATE = (2.4, 1.8) # end wall plates: across, up # The fore end wall leaves two bands beside the command deck doorway open (y spans, and z from the gallery up to the # vault): panels in the wall buffer's smooth finish fill them (sub_bay_bare_places.py). Until 2026-10-23 they were cut # by mistake, the doorway list also taking two doorways of the command deck's rooms at x 23 (|y| 2.20 to 3.00), and the # bare bulkhead showed there; the user marked them as places without texture. Same spans as that cut. DOOR_BANDS = ((-3.003, -2.197), (2.197, 3.003)) DOOR_BANDS_Z = (3.03, 7.24) PAD = "SI_MI_FloorPadding5" PERF = "SI_MI_Floor_A" RED = "SI_MI_Trim_A_Red" BLACK = "SI_MI_Trim_A_Black" DARK = "SI_MI_Trim_A_DarkGray" K_PAD = 1.1656 # texture tiles a metre on the kit's cove (SI_SM_Ceiling_Main_E) K_PERF = 1.0565 # on the kit's perforated ceiling panel (SI_SM_Ceiling_Main_B2) K_RED_U, RED_V = 0.666, (0.07, 0.09) # the kit cove's red arch: along, and its band of the trim sheet K_BLK_U, BLK_V = 0.44, (0.22, 0.26) THETAS = [i * 2.5 for i in range(73)] # 0 .. 180 def exists(): return RECORD in bpy.data.texts def record(): return json.loads(bpy.data.texts[RECORD].as_string()) if exists() else None def forget(): if not exists(): return False bpy.data.texts.remove(bpy.data.texts[RECORD]) return True _bb = rr._bb # ---- the room's shape ------------------------------------------------------------------------------------------- class Shell: """Sections of the old lining as rays from the room's axis at the ledge; the vault OUT outside them. Each section starts and ends with a foot on the ledge: a short flat strip from the ledge's outer edge to the vault.""" def __init__(self, lining_mesh_world, z_spring, ledge_out): vs, ts = lining_mesh_world self.tree = BVHTree.FromPolygons(vs, ts, epsilon=0.0) self.z0 = z_spring self.foot = ledge_out - 0.01 self.cache = {} def section(self, x): k = round(x, 4) if k in self.cache: return self.cache[k] o = Vector((x, 0.0, self.z0)) pts = [] for t in THETAS: r = math.radians(t) h = self.tree.ray_cast(o, Vector((0.0, math.cos(r), math.sin(r))), 40.0) if h[0] is None: raise RuntimeError("no lining at x %.2f, %.1f degrees" % (x, t)) pts.append(Vector((h[0].y, h[0].z))) outn = [] for i in range(len(pts)): a, b = pts[max(i - 1, 0)], pts[min(i + 1, len(pts) - 1)] t = (b - a).normalized() outn.append(Vector((t.y, -t.x))) outn[0] = Vector((1.0, 0.0)) # (at the springing the vault meets the ledge square) outn[-1] = Vector((-1.0, 0.0)) vault = [p + n * OUT for p, n in zip(pts, outn)] zf = self.z0 + PANEL_T + 0.001 # (the panels' backs 1 mm over the ledge) vault[0] = Vector((pts[0].x + OUT, zf)) vault[-1] = Vector((pts[-1].x - OUT, zf)) # the feet on the ledge (their back faces down) p_all = [Vector((self.foot, zf))] + vault + [Vector((-self.foot, zf))] n_all = [Vector((0.0, -1.0))] + outn + [Vector((0.0, -1.0))] th_all = [-1.0] + list(THETAS) + [181.0] s = [0.0] for i in range(1, len(p_all)): s.append(s[-1] + (p_all[i] - p_all[i - 1]).length) sec = {"p": p_all, "n": n_all, "s": s, "theta": th_all} self.cache[k] = sec return sec def at(self, x, s, depth=0.0): """Point of the vault at x and arc length s (from the port foot over the crown to the starboard foot), moved `depth` along its normal (outward positive, into the room negative); and that normal.""" sec = self.section(x) ss = sec["s"] s = min(max(s, 0.0), ss[-1]) i = 1 while i < len(ss) - 1 and ss[i] < s: i += 1 f = (s - ss[i - 1]) / max(ss[i] - ss[i - 1], 1e-9) p = sec["p"][i - 1].lerp(sec["p"][i], f) n = sec["n"][i - 1].lerp(sec["n"][i], f) n = n.normalized() if n.length > 1e-9 else sec["n"][i] q = p + n * depth return Vector((x, q.x, q.y)), Vector((0.0, n.x, n.y)) def s_total(self, x): return self.section(x)["s"][-1] STATION = 0.1 def at_fast(self, x, s, depth=0.0): """at(), its sections taken every STATION along x and blended between (the lining changes by a millimetre or two over a metre along the room).""" i = math.floor(x / self.STATION) x0, x1 = round(i * self.STATION, 4), round((i + 1) * self.STATION, 4) f = (x - x0) / self.STATION p0, n0 = self.at(x0, s, depth) p1, n1 = self.at(x1, s, depth) p = p0.lerp(p1, f) p.x = x return p, n0.lerp(n1, f).normalized() def frame(self, x, s): """The vault's tangent along the arc and its outward normal at (x, s).""" a = self.at_fast(x, s - 0.01)[0] b = self.at_fast(x, s + 0.01)[0] return (b - a).normalized(), self.at_fast(x, s)[1] def s_at_theta(self, x, theta): sec = self.section(x) th, ss = sec["theta"], sec["s"] for i in range(1, len(th)): if th[i - 1] <= theta <= th[i]: f = (theta - th[i - 1]) / max(th[i] - th[i - 1], 1e-9) return ss[i - 1] + (ss[i] - ss[i - 1]) * f raise RuntimeError("theta %.1f out of range" % theta) def s_at_y(self, x, y): """Arc length where the vault's face passes y on the crown.""" sec = self.section(x) p, ss = sec["p"], sec["s"] for i in range(1, len(p)): a, b = p[i - 1].x, p[i].x if (a - y) * (b - y) <= 0.0 and abs(a - b) > 1e-9 and p[i].y > self.z0 + 3.0: return ss[i - 1] + (ss[i] - ss[i - 1]) * (y - a) / (b - a) raise RuntimeError("y %.2f not on the crown at x %.2f" % (y, x)) def _lines(a, b, step, extra=()): n = max(1, int(math.ceil((b - a) / step - 1e-9))) vals = {round(a + (b - a) * i / n, 5) for i in range(n + 1)} for e in extra: if a + 1e-6 < e < b - 1e-6: vals.add(round(e, 5)) return sorted(vals) # ---- meshes --------------------------------------------------------------------------------------------------------- class MB: """A mesh drawn here: quads with their own UVs, on named materials.""" def __init__(self, mats): self.bm = bmesh.new() self.uv = self.bm.loops.layers.uv.verify() self.mats = list(mats) self.vmap = {} def v(self, co): k = (round(co[0], 5), round(co[1], 5), round(co[2], 5)) if k not in self.vmap: self.vmap[k] = self.bm.verts.new(co) return self.vmap[k] def quad(self, pts, uvs, mat, toward=None): vs = [self.v(p) for p in pts] if len({id(x) for x in vs}) < 3: return None if toward is not None: n = (Vector(pts[1]) - Vector(pts[0])).cross(Vector(pts[2]) - Vector(pts[0])) if n.length > 1e-12 and n.dot(Vector(toward)) < 0.0: vs, uvs = vs[::-1], list(uvs)[::-1] try: f = self.bm.faces.new(vs) except ValueError: return None f.material_index = self.mats.index(mat) f.smooth = False for loop, u in zip(f.loops, uvs): loop[self.uv].uv = u return f def finish(self, name, rec, note): rr._forget(name) me = bpy.data.meshes.new(name) self.bm.normal_update() self.bm.to_mesh(me) self.bm.free() for m in self.mats: me.materials.append(bpy.data.materials[m]) me["made_for"] = MADE_FOR me["note"] = note rec["meshes"].append(me.name) return me def _place(me, name, coll, root, rec): o = bpy.data.objects.new(NAME + name, 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) return o def _sheet(mb, sh, xs, ss, mat, k, depth_front, depth_back, sides=True, side_mat=None): """A curved panel over the grid xs x ss (x positions, arc lengths), its face at depth_front (into the room negative), its back at depth_back; with its edge faces. UV: metres times k along x and along the arc.""" side_mat = side_mat or mat F = [[sh.at(x, s, depth_front)[0] for s in ss] for x in xs] B = [[sh.at(x, s, depth_back)[0] for s in ss] for x in xs] for i in range(len(xs) - 1): for j in range(len(ss) - 1): cen = (F[i][j] + F[i + 1][j + 1]) * 0.5 inward = sh.at((xs[i] + xs[i + 1]) / 2, (ss[j] + ss[j + 1]) / 2)[1] * -1.0 uv = [(xs[i] * k, ss[j] * k), (xs[i + 1] * k, ss[j] * k), (xs[i + 1] * k, ss[j + 1] * k), (xs[i] * k, ss[j + 1] * k)] mb.quad([F[i][j], F[i + 1][j], F[i + 1][j + 1], F[i][j + 1]], uv, mat, toward=inward) mb.quad([B[i][j], B[i + 1][j], B[i + 1][j + 1], B[i][j + 1]], uv, mat, toward=-inward) if not sides: return t = abs(depth_back - depth_front) for i in range(len(xs) - 1): # along the two arc ends for j, sgn in ((0, -1.0), (len(ss) - 1, 1.0)): out_dir = (F[i][j] - F[i][max(0, j - 1)] if j > 0 else F[i][0] - F[i][1]).normalized() mb.quad([F[i][j], F[i + 1][j], B[i + 1][j], B[i][j]], [(xs[i] * k, 0), (xs[i + 1] * k, 0), (xs[i + 1] * k, t * k), (xs[i] * k, t * k)], side_mat, toward=out_dir) for i, sx in ((0, -1.0), (len(xs) - 1, 1.0)): # across the two x ends for j in range(len(ss) - 1): mb.quad([F[i][j], F[i][j + 1], B[i][j + 1], B[i][j]], [(ss[j] * k, 0), (ss[j + 1] * k, 0), (ss[j + 1] * k, t * k), (ss[j] * k, t * k)], side_mat, toward=(sx, 0, 0)) def _sweep_box(mb, sh, xa, xb, ss, d_in, d_out, mats, ku): """A box swept along the vault between xa and xb over the arc lengths ss, from d_in (into the room, negative) to d_out. mats = (soffit, sides, back) with their (u per metre, v band).""" (m_sof, k_sof, v_sof), (m_side, k_side, v_side), (m_back, k_back, v_back) = mats P = {} for x in (xa, xb): for d in (d_in, d_out): P[(x, d)] = [sh.at(x, s, d)[0] for s in ss] for j in range(len(ss) - 1): inward = sh.at((xa + xb) / 2, (ss[j] + ss[j + 1]) / 2)[1] * -1.0 u0, u1 = ss[j], ss[j + 1] # soffit a, b = P[(xa, d_in)], P[(xb, d_in)] mb.quad([a[j], b[j], b[j + 1], a[j + 1]], [(u0 * k_sof, v_sof[0]), (u0 * k_sof, v_sof[1]), (u1 * k_sof, v_sof[1]), (u1 * k_sof, v_sof[0])], m_sof, toward=inward) # back a, b = P[(xa, d_out)], P[(xb, d_out)] mb.quad([a[j], b[j], b[j + 1], a[j + 1]], [(u0 * k_back, v_back[0]), (u0 * k_back, v_back[1]), (u1 * k_back, v_back[1]), (u1 * k_back, v_back[0])], m_back, toward=-inward) # the two sides for x, sx in ((xa, -1.0), (xb, 1.0)): a, b = P[(x, d_in)], P[(x, d_out)] mb.quad([a[j], b[j], b[j + 1], a[j + 1]], [(u0 * k_side, v_side[0]), (u0 * k_side, v_side[1]), (u1 * k_side, v_side[1]), (u1 * k_side, v_side[0])], m_side, toward=(sx, 0, 0)) # end caps for j, sgn in ((0, -1.0), (len(ss) - 1, 1.0)): q = [P[(xa, d_in)][j], P[(xb, d_in)][j], P[(xb, d_out)][j], P[(xa, d_out)][j]] tdir = (P[(xa, d_in)][j] - P[(xa, d_in)][j - 1 if j > 0 else 1]) * (1.0 if j > 0 else 1.0) mb.quad(q, [(0, v_side[0]), (1 * k_side * (xb - xa), v_side[0]), (k_side * (xb - xa), v_side[1]), (0, v_side[1])], m_side, toward=tdir) def _arc_steps(sa, sb, step=STEP_S, extra=()): return _lines(sa, sb, step, extra) def _x_steps(xa, xb, extra=()): return _lines(xa, xb, STEP_X, extra) # ---- the build -------------------------------------------------------------------------------------------------- class Room: def __init__(self): aft = bpy.data.objects[AFT_BULKHEAD] fore = bpy.data.objects[FORE_BULKHEAD] self.x0 = round(_bb(aft)[1].x + 0.02, 4) # end walls' faces, 2 cm in front of the bulkheads self.x1 = round(_bb(fore)[0].x - 0.02, 4) ledges = [o for o in bpy.data.collections[LINING_COLL].objects if o.name.startswith("Ledge | ")] self.z0 = round(max(_bb(o)[1].z for o in ledges), 4) port = next(o for o in ledges if o.name == "Ledge | port wall") self.ledge_out = round(_bb(port)[1].y, 4) # the ledge's outer edge (port side) bufs = [o for o in bpy.data.collections[LINING_COLL].objects if o.type == "MESH" and o.data.name == "HW_SM_Hall_WallBuffer" and rr._mid(o).y > 0] self.ribs = sorted(round(rr._mid(o).x, 4) for o in bufs) sh = bpy.data.objects[SHELL] me = sh.data me.calc_loop_triangles() mw = sh.matrix_world self.lining = ([tuple(mw @ v.co) for v in me.vertices], [tuple(t.vertices) for t in me.loop_triangles]) self.gallery = self._box_of("Gallery | command deck landing") doors = [o for o in bpy.context.scene.objects if o.type == "MESH" and o.data.name == "EN_SM_Doorway_Small_A" and self.x1 - 0.2 < _bb(o)[0].x < self.x1 + 0.3] # (only the doorways in the fore end wall) self.doorways = [(_bb(o)[0], _bb(o)[1]) for o in doors] lin = bpy.data.objects.get("Pier | fore door lintel (raised door)") self.lintel = (_bb(lin)[0], _bb(lin)[1]) if lin else None def _box_of(self, name): o = bpy.data.objects.get(name) return (_bb(o)[0], _bb(o)[1]) if o else None def apply(): if exists(): raise RuntimeError("the ceiling is already there (text block %r); remove() takes it out" % RECORD) room = Room() rec = {"version": 1, "added": [], "meshes": [], "swapped": []} out = {"x": [room.x0, room.x1], "springing_z": room.z0, "ribs_x": room.ribs} sh = Shell(room.lining, room.z0, room.ledge_out) root = bpy.data.objects.get(quarters.ROOT) zone = bpy.data.collections[ZONE] coll = quarters.get_collection(COLL, zone) # 1. the old lining above the ledge goes (a cut copy of its mesh: the part below the ledge stays) lo = bpy.data.objects[SHELL] z0 = room.z0 src = lo.data me = src.copy() me.name = PRE + "Hull_Lining_below_ledge" bm = bmesh.new() bm.from_mesh(me) inv = lo.matrix_world.inverted() bmesh.ops.bisect_plane(bm, geom=bm.verts[:] + bm.edges[:] + bm.faces[:], dist=1e-6, plane_co=inv @ Vector((0, 0, z0)), plane_no=(lo.matrix_world.to_3x3().transposed() @ Vector((0, 0, 1))).normalized()) gone = [f for f in bm.faces if (lo.matrix_world @ f.calc_center_median()).z > z0 + 1e-4] bmesh.ops.delete(bm, geom=gone, context="FACES") bm.to_mesh(me) bm.free() me["made_for"] = MADE_FOR me["note"] = "the M-Ship model's lining of the hull, without its part above the sub bay's wall ledge (the ceiling took its place)" rec["meshes"].append(me.name) rec["swapped"].append([lo.name, src.name, me.name]) src.use_fake_user = True lo.data = me out["lining_faces_taken"] = len(gone) ribs = room.ribs xe0, xe1 = room.x0 + END_FACE, room.x1 - END_FACE edges_x = [xe0] + [v for r in ribs for v in (r - RIB_W / 2, r + RIB_W / 2)] + [xe1] bays = [(edges_x[i], edges_x[i + 1]) for i in range(0, len(edges_x), 2)] S_mid = sh.s_total((room.x0 + room.x1) / 2) out["vault_arc_m"] = round(S_mid, 3) # 2. the coves and the crown: perforated plates, bay by bay s_spring = sh.s_at_theta(xe0, 0.0) # the foot: ledge edge to the springing out["cove"] = {"arc_m": COVE_LEN, "plates_up": COVE_PLATES} for bi, (xa, xb) in enumerate(bays): st = sh.s_total(xa) sc = s_spring + COVE_LEN # the feet on the ledge: a dark strip mb = MB([DARK]) xs = _x_steps(xa, xb) _sheet(mb, sh, xs, [0.0, s_spring], DARK, K_BLK_U, 0.0, PANEL_T) _sheet(mb, sh, xs, [st - s_spring, st], DARK, K_BLK_U, 0.0, PANEL_T) _place(mb.finish(PRE + "Foot_%d" % (bi + 1), rec, "dark strip where the vault stands on the wall ledge"), "feet, bay %d" % (bi + 1), coll, root, rec) n_x = max(1, int(round((xb - xa) / PLATE_X))) ca = xa + (CORNER if bi == 0 else 0.0) # (in the end bays the end trim stands over the cove's end) cb = xb - (CORNER if bi == len(bays) - 1 else 0.0) for side in ("port", "starboard"): mb = MB([PERF]) g = PLATE_GAP / 2 # (joints at the foot and under the cove trim) s_lo, s_hi = (s_spring + g, sc - g) if side == "port" else (st - sc + g, st - s_spring - g) _plates(mb, sh, ca, cb, s_lo, s_hi, n_x, COVE_PLATES) _place(mb.finish(PRE + "Cove_%s_%d" % (side, bi + 1), rec, "perforated cove plates of the moon pool ceiling (works hall's SI_MI_Floor_A)"), "cove plates %s, bay %d" % (side, bi + 1), coll, root, rec) # crown plates sa, sb = sc + PLATE_GAP / 2, st - sc - PLATE_GAP / 2 # (a joint under each cove trim) mb = MB([PERF]) hole = None if xa < HUB[0] < xb: a = ROSE_IN + 0.02 hole = (HUB[0] - a, HUB[0] + a, -a, a) _plates(mb, sh, xa, xb, sa, sb, n_x, PLATES_ACROSS, hole) _place(mb.finish(PRE + "Crown_%d" % (bi + 1), rec, "perforated crown plates of the moon pool ceiling (works hall's SI_MI_Floor_A)"), "crown plates, bay %d" % (bi + 1), coll, root, rec) # dark trims along the coves' tops (short of the corner trims at the two ends) mb = MB([DARK]) pa = xa + (CORNER if bi == 0 else 0.0) pb = xb - (CORNER if bi == len(bays) - 1 else 0.0) for side in ("port", "starboard"): s_c = sc if side == "port" else st - sc _sweep_box_long(mb, sh, pa, pb, s_c, PURLIN_W, -PURLIN_D, -LIFT, DARK) _place(mb.finish(PRE + "CoveTrim_%d" % (bi + 1), rec, "dark trim along the coves' tops"), "cove trims, bay %d" % (bi + 1), coll, root, rec) # 3. the ribs over the pilasters, and the dark corner trims at the end walls for i, xr in enumerate(ribs): mb = MB([BLACK, RED]) st = sh.s_total(xr) ss = _arc_steps(0.0, st, step=STEP_S) _sweep_box(mb, sh, xr - RIB_W / 2, xr + RIB_W / 2, ss, -RIB_D, PANEL_T, ((BLACK, K_BLK_U, BLK_V), (RED, K_RED_U, RED_V), (BLACK, K_BLK_U, BLK_V)), K_RED_U) _place(mb.finish(PRE + "Rib_%d" % (i + 1), rec, "rib over a pilaster: the works hall cove's arch (red sides, black soffit)"), "rib %d (x %.2f)" % (i + 1, xr), coll, root, rec) for end, xa, xb in (("aft", xe0, xe0 + CORNER), ("fore", xe1 - CORNER, xe1)): mb = MB([DARK]) st = sh.s_total(xa) spans = [(0.0, st)] head = [d for d in room.doorways if d[0].z > room.z0] if end == "fore" else [] if head: # the command deck doorway's frame reaches up to the crown lo, hi = head[0] spans = [(0.0, sh.s_at_y(xa, hi.y + 0.05)), (sh.s_at_y(xa, lo.y - 0.05), st)] for sa_, sb_ in spans: ss = _arc_steps(sa_, sb_, step=STEP_S) _sweep_box(mb, sh, xa, xb, ss, -CORNER, -LIFT, ((DARK, K_BLK_U, BLK_V), (DARK, K_BLK_U, BLK_V), (DARK, K_BLK_U, BLK_V)), K_BLK_U) _place(mb.finish(PRE + "EndTrim_%s" % end, rec, "dark trim where the vault meets the end wall"), "end trim %s" % end, coll, root, rec) # 4. the rose round the pylon _place(_rose(sh, rec), "rose round the carousel pylon", coll, root, rec) # 5. the end walls above the ledge, in the same plates out["end_walls"] = {} for end in ("aft", "fore"): me, n = _end_wall_plates(sh, room, end, rec) _place(me, "end wall %s" % end, coll, root, rec) out["end_walls"][end] = n # above the command deck doorway the end wall leaves a slit to the vault: a dark plate closes it head = [d for d in room.doorways if d[0].z > room.z0] if head: lo_, hi_ = head[0] y0_, y1_ = lo_.y - 0.05, hi_.y + 0.05 ztop = min(sh.at_fast(xe1, sh.s_at_y(xe1, y))[0].z for y in (y0_, 0.0, y1_)) - 0.005 mb = MB([DARK]) q = [(xe1, y0_, hi_.z + 0.001), (xe1, y1_, hi_.z + 0.001), (xe1, y1_, ztop), (xe1, y0_, ztop)] uv = [(0, BLK_V[0]), (1, BLK_V[0]), (1, BLK_V[1]), (0, BLK_V[1])] mb.quad(q, uv, DARK, toward=(-1, 0, 0)) mb.quad([(xe1 + 0.02, p[1], p[2]) for p in q], uv, DARK, toward=(1, 0, 0)) _place(mb.finish(PRE + "DoorHeadPlate", rec, "plate between the command deck doorway's head and the vault"), "plate over the command deck doorway", coll, root, rec) bpy.data.texts.new(RECORD).write(json.dumps(rec)) bpy.context.view_layer.update() out["objects"] = len(rec["added"]) return out def _plates(mb, sh, xa, xb, sa, sb, n_x, n_s, hole=None): """Perforated plates over x in [xa, xb] and arc lengths [sa, sb]: n_x along the room, n_s across, 2 cm reveals between them (none at the outer edges); without what falls in the hole (x0, x1, y0, y1).""" for ix in range(n_x): pxa = xa + (xb - xa) * ix / n_x + (PLATE_GAP / 2 if ix > 0 else 0.0) pxb = xa + (xb - xa) * (ix + 1) / n_x - (PLATE_GAP / 2 if ix < n_x - 1 else 0.0) for iy in range(n_s): psa = sa + (sb - sa) * iy / n_s + (PLATE_GAP / 2 if iy > 0 else 0.0) psb = sa + (sb - sa) * (iy + 1) / n_s - (PLATE_GAP / 2 if iy < n_s - 1 else 0.0) extra_x = (hole[0], hole[1]) if hole else () xs_p = _x_steps(pxa, pxb, extra_x) extra_s = () if hole: try: extra_s = (sh.s_at_y(HUB[0], hole[3]), sh.s_at_y(HUB[0], hole[2])) except RuntimeError: extra_s = () ss_p = _arc_steps(psa, psb, extra=extra_s) if hole: _sheet_holed(mb, sh, xs_p, ss_p, PERF, K_PERF, hole) else: _sheet(mb, sh, xs_p, ss_p, PERF, K_PERF, 0.0, PANEL_T) def _point_in_poly(y, z, poly): inside = False n = len(poly) for i in range(n): y1, z1 = poly[i] y2, z2 = poly[(i + 1) % n] if (z1 > z) != (z2 > z): yi = y1 + (z - z1) * (y2 - y1) / (z2 - z1) if y < yi: inside = not inside return inside def _end_wall_plates(sh, room, end, rec): """The end wall above the ledge in perforated plates (END_PLATE, 2 cm reveals), 2 cm thick, cut to the vault's outline CORNER inside it and round what stands at the wall. One mesh; returns it and the number of plates.""" xf = room.x0 + END_FACE if end == "aft" else room.x1 - END_FACE back = -PANEL_T if end == "aft" else PANEL_T # (the plates' backs toward the bulkhead) face_dir = (1.0, 0.0, 0.0) if end == "aft" else (-1.0, 0.0, 0.0) sec = sh.section(xf) th = sec["theta"] ss = [sec["s"][i] for i in range(len(th)) if 0.0 <= th[i] <= 180.0] outline = [] for s_ in ss: p = sh.at(xf, s_, -CORNER - 0.005)[0] # (5 mm short of the end trim's inner face) outline.append((p.y, max(p.z, room.z0))) z0 = room.z0 + 0.001 poly = [(y, z) for y, z in outline] + [(outline[-1][0], z0 - 0.01), (outline[0][0], z0 - 0.01)] cuts = [] if end == "fore": if room.gallery: lo, hi = room.gallery cuts.append((lo.y - 0.01, hi.y + 0.01, lo.z - 0.01, hi.z + 0.03)) for lo, hi in room.doorways: if lo.z > room.z0: cuts.append((lo.y - 0.005, hi.y + 0.005, lo.z - 0.11, hi.z + 0.2)) if room.lintel: lo, hi = room.lintel cuts.append((lo.y - 0.005, hi.y + 0.005, room.z0, hi.z + 0.005)) for ya, yb in DOOR_BANDS: cuts.append((ya, yb, DOOR_BANDS_Z[0], DOOR_BANDS_Z[1])) ymax = max(abs(p[0]) for p in outline) zmax = max(p[1] for p in outline) w, h = END_PLATE ys = [-w / 2 - w * k for k in range(int(ymax / w) + 2)][::-1] + [w / 2 + w * k for k in range(int(ymax / w) + 2)] ys = sorted(set([round(v, 4) for v in ys])) zs = [z0 + h * k for k in range(int((zmax - z0) / h) + 2)] mb = MB([PERF]) k = K_PERF sgn_u = 1.0 if end == "aft" else -1.0 n = 0 for ci in range(len(ys) - 1): for ri in range(len(zs) - 1): ya, yb = ys[ci] + PLATE_GAP / 2, ys[ci + 1] - PLATE_GAP / 2 za, zb = zs[ri] + (PLATE_GAP / 2 if ri > 0 else 0.0), zs[ri + 1] - PLATE_GAP / 2 corners = [(ya, za), (yb, za), (yb, zb), (ya, zb)] if not any(_point_in_poly(cy, cz, poly) for cy, cz in corners) and not any(ya <= p[0] <= yb and za <= p[1] <= zb for p in outline): continue if any(c[0] <= ya and yb <= c[1] and c[2] <= za and zb <= c[3] for c in cuts): continue n += 1 gy = _lines(ya, yb, 0.3) gz = _lines(za, zb, 0.3) 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])] uv = [(sgn_u * p[1] * k, p[2] * k) for p in q] mb.quad(q, uv, PERF, toward=face_dir) mb.quad([(xf + back, p[1], p[2]) for p in q], uv, PERF, toward=(-face_dir[0], 0, 0)) for (y0_, z0_), (y1_, z1_), out_dir 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_)] L = ((y1_ - y0_) ** 2 + (z1_ - z0_) ** 2) ** 0.5 mb.quad(q, [(0, 0), (L * k, 0), (L * k, PANEL_T * k), (0, PANEL_T * k)], PERF, toward=out_dir) me = mb.finish(PRE + "EndWall_%s" % end, rec, "perforated plates of the moon pool room's %s end wall" % end) # cut to the outline and round what stands at the wall: by every outline segment and cut edge (planes across the wall) bm = bmesh.new() bm.from_mesh(me) lines = [(poly[i], poly[(i + 1) % len(poly)]) for i in range(len(poly))] for c in cuts: lines += [((c[0], c[2]), (c[0], c[3])), ((c[1], c[2]), (c[1], c[3])), ((c[0], c[2]), (c[1], c[2])), ((c[0], c[3]), (c[1], c[3]))] for (y1, z1), (y2, z2) in lines: 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 = [] for f in bm.faces: c = f.calc_center_median() if not _point_in_poly(c.y, c.z, poly) or c.z < z0 - 1e-4: gone.append(f) elif any(cc[0] < c.y < cc[1] and cc[2] < c.z < cc[3] for cc in cuts): gone.append(f) bmesh.ops.delete(bm, geom=gone, context="FACES") bm.to_mesh(me) bm.free() return me, n def _sheet_holed(mb, sh, xs, ss, mat, k, hole): """_sheet without the quads whose centre falls in the hole (x0, x1, y0, y1), and with edge faces round it.""" x0, x1, y0, y1 = hole F = [[sh.at(x, s, 0.0)[0] for s in ss] for x in xs] B = [[sh.at(x, s, PANEL_T)[0] for s in ss] for x in xs] keep = {} for i in range(len(xs) - 1): for j in range(len(ss) - 1): c = (F[i][j] + F[i + 1][j + 1]) * 0.5 keep[(i, j)] = not (x0 - 1e-6 <= c.x <= x1 + 1e-6 and y0 - 1e-6 <= c.y <= y1 + 1e-6) for (i, j), k_ in keep.items(): if not k_: continue inward = sh.at((xs[i] + xs[i + 1]) / 2, (ss[j] + ss[j + 1]) / 2)[1] * -1.0 uv = [(xs[i] * k, ss[j] * k), (xs[i + 1] * k, ss[j] * k), (xs[i + 1] * k, ss[j + 1] * k), (xs[i] * k, ss[j + 1] * k)] mb.quad([F[i][j], F[i + 1][j], F[i + 1][j + 1], F[i][j + 1]], uv, mat, toward=inward) mb.quad([B[i][j], B[i + 1][j], B[i + 1][j + 1], B[i][j + 1]], uv, mat, toward=-inward) t = PANEL_T ni, nj = len(xs) - 1, len(ss) - 1 for (i, j), k_ in keep.items(): # edge faces wherever a kept cell borders a dropped one or the border if not k_: continue for di, dj in ((-1, 0), (1, 0), (0, -1), (0, 1)): nb = (i + di, j + dj) if 0 <= nb[0] < ni and 0 <= nb[1] < nj and keep[nb]: continue if di == -1: a, b = (i, j), (i, j + 1) elif di == 1: a, b = (i + 1, j), (i + 1, j + 1) elif dj == -1: a, b = (i, j), (i + 1, j) else: a, b = (i, j + 1), (i + 1, j + 1) fa, fb, ba, bb_ = F[a[0]][a[1]], F[b[0]][b[1]], B[a[0]][a[1]], B[b[0]][b[1]] cen = (F[i][j] + F[i + 1][j + 1]) * 0.5 mid = (fa + fb) * 0.5 mb.quad([fa, fb, bb_, ba], [(0, 0), ((fb - fa).length * k, 0), ((fb - fa).length * k, t * k), (0, t * k)], mat, toward=mid - cen) def _sweep_box_long(mb, sh, xa, xb, s_c, w, d_in, d_out, mat): """A trim along the room (x from xa to xb) centred on arc length s_c, w wide across, from d_in to d_out.""" xs = _x_steps(xa, xb) sa, sb = s_c - w / 2, s_c + w / 2 for i in range(len(xs) - 1): x0, x1 = xs[i], xs[i + 1] A = {(x, s, d): sh.at(x, s, d)[0] for x in (x0, x1) for s in (sa, sb) for d in (d_in, d_out)} inward = sh.at((x0 + x1) / 2, s_c)[1] * -1.0 u0, u1 = x0 * K_BLK_U, x1 * K_BLK_U v0, v1 = BLK_V mb.quad([A[(x0, sa, d_in)], A[(x1, sa, d_in)], A[(x1, sb, d_in)], A[(x0, sb, d_in)]], [(u0, v0), (u1, v0), (u1, v1), (u0, v1)], mat, toward=inward) mb.quad([A[(x0, sa, d_out)], A[(x1, sa, d_out)], A[(x1, sb, d_out)], A[(x0, sb, d_out)]], [(u0, v0), (u1, v0), (u1, v1), (u0, v1)], mat, toward=-inward) for s, sg in ((sa, -1.0), (sb, 1.0)): p_in, p_in2 = A[(x0, s, d_in)], A[(x1, s, d_in)] o_dir = (sh.at((x0 + x1) / 2, s + 0.01 * sg)[0] - sh.at((x0 + x1) / 2, s)[0]) mb.quad([A[(x0, s, d_in)], A[(x1, s, d_in)], A[(x1, s, d_out)], A[(x0, s, d_out)]], [(u0, v0), (u1, v0), (u1, v1), (u0, v1)], mat, toward=o_dir) for x, sx in ((xs[0], -1.0), (xs[-1], 1.0)): mb.quad([sh.at(x, sa, d_in)[0], sh.at(x, sb, d_in)[0], sh.at(x, sb, d_out)[0], sh.at(x, sa, d_out)[0]], [(0, BLK_V[0]), (w * K_BLK_U, BLK_V[0]), (w * K_BLK_U, BLK_V[1]), (0, BLK_V[1])], mat, toward=(sx, 0, 0)) def _crown_z(sh, x, y, depth): """The vault's face over (x, y) on the crown, moved by depth along its normal.""" s = sh.s_at_y(x, y) return sh.at(x, s, depth) def _rose(sh, rec): """A dark ring round the pylon, lying on the crown plates: from ROSE_IN to ROSE_OUT, ROSE_D deep.""" mb = MB([BLACK, RED]) n_a = 64 radii = [ROSE_IN, (ROSE_IN + ROSE_OUT) / 2, ROSE_OUT] def P(r, a, d): x, y = HUB[0] + r * math.cos(a), HUB[1] + r * math.sin(a) return _crown_z(sh, x, y, d)[0] angs = [2 * math.pi * i / n_a for i in range(n_a + 1)] d_face, d_back = -ROSE_D, -LIFT for i in range(n_a): a0, a1 = angs[i], angs[i + 1] for j in range(len(radii) - 1): r0, r1 = radii[j], radii[j + 1] uv = [(a0 * r0 * K_BLK_U, BLK_V[0] + 0.04 * j / 2), (a1 * r0 * K_BLK_U, BLK_V[0] + 0.04 * j / 2), (a1 * r1 * K_BLK_U, BLK_V[0] + 0.04 * (j + 1) / 2), (a0 * r1 * K_BLK_U, BLK_V[0] + 0.04 * (j + 1) / 2)] mb.quad([P(r0, a0, d_face), P(r0, a1, d_face), P(r1, a1, d_face), P(r1, a0, d_face)], uv, BLACK, toward=(0, 0, -1)) mb.quad([P(r0, a0, d_back), P(r0, a1, d_back), P(r1, a1, d_back), P(r1, a0, d_back)], uv, BLACK, toward=(0, 0, 1)) # inner lip (red, up into the opening to the plates' back) and outer lip for r, sg, top in ((ROSE_IN, -1.0, PANEL_T + 0.02), (ROSE_OUT, 1.0, d_back)): q = [P(r, a0, d_face), P(r, a1, d_face), P(r, a1, top), P(r, a0, top)] mid = (q[0] + q[1]) * 0.5 hub = Vector((HUB[0], HUB[1], mid.z)) mb.quad(q, [(a0 * r * K_RED_U, RED_V[0]), (a1 * r * K_RED_U, RED_V[0]), (a1 * r * K_RED_U, RED_V[1]), (a0 * r * K_RED_U, RED_V[1])], RED if sg < 0 else BLACK, toward=(mid - hub) * sg) return mb.finish(PRE + "PylonRose", rec, "dark rose round the carousel pylon where it goes through the crown") 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, 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) 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 _label(o): k = rr._kind(o) return k if (k.startswith((PRE,)) or o.data.get("ue_mesh")) else "[ship] " + o.name def _crossings(mine, others): cache, out = {}, {} for i, a in enumerate(mine): for b in mine[i + 1:] + others: k = rr._crossing(rr._geo(a, cache), rr._geo(b, cache)) if k: key = " x ".join(sorted((_label(a), _label(b)))) out[key] = out.get(key, 0) + k return out def _tree_of(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 carousel_sweep(step=10, hoists=(0.0, 2.65, 5.3), stride=2): """The carousel turned through a whole turn at each hoist (all three arms), its parts and the Tenders on it against the ceiling: triangles that cross (BVH overlap) and the least distance of their vertices.""" rec = record() mine = [bpy.data.objects[n] for n in rec["added"] if n in bpy.data.objects and bpy.data.objects[n].type == "MESH"] T = _tree_of(mine) rig = bpy.data.objects["DockArm_Rig"] pb = rig.pose.bones keep = {"yaw": pb["Turret"].rotation_euler[1], "h": [pb["CTRL_Arm%d_Hoist" % i].location[1] for i in (1, 2, 3)]} parts = [o for c in ("10.7 | Tender carousel | docking arm, rigged (models/docking-arm)", "10.7 | Tender carousel") for o in bpy.data.collections[c].objects if o.type == "MESH"] worst = (99.0, None) cross = {} poses = 0 try: for h in hoists: for i in (1, 2, 3): pb["CTRL_Arm%d_Hoist" % i].location[1] = h for yaw in range(0, 360, step): pb["Turret"].rotation_euler[1] = math.radians(yaw) bpy.context.view_layer.update() dg = bpy.context.evaluated_depsgraph_get() poses += 1 meshes = [] for o in parts: e = o.evaluated_get(dg) m = e.to_mesh() m.calc_loop_triangles() w = o.matrix_world meshes.append((o.name, [w @ v.co for v in m.vertices], [tuple(t.vertices) for t in m.loop_triangles])) e.to_mesh_clear() for inst in dg.object_instances: ob = inst.object if not inst.is_instance or ob.type != "MESH" or ob.original.library is None: continue w = inst.matrix_world.copy() me = ob.data me.calc_loop_triangles() meshes.append(("Tender:" + ob.name, [w @ v.co for v in me.vertices], [tuple(t.vertices) for t in me.loop_triangles])) for name, vs, ts in meshes: if not ts: continue t2 = BVHTree.FromPolygons([tuple(v) for v in vs], ts, epsilon=0.0) hits = T.overlap(t2) if hits: key = "%s (hoist %.2f, yaw %d)" % (name, h, yaw) cross[key] = len(hits) for v in vs[::stride]: loc, nrm, idx, d = T.find_nearest(v, 0.6) if loc is not None and d < worst[0]: worst = (d, [name, round(h, 2), yaw, [round(c, 3) for c in v]]) finally: pb["Turret"].rotation_euler[1] = keep["yaw"] for i, h in zip((1, 2, 3), keep["h"]): pb["CTRL_Arm%d_Hoist" % i].location[1] = h bpy.context.view_layer.update() return {"poses": poses, "crossings": dict(list(cross.items())[:20]), "n_crossing_poses": len(cross), "least_m": round(worst[0], 4), "at": worst[1]} def check(others=True, sweep=True): out = {"present": exists()} if not out["present"]: return out rec = record() mine = [bpy.data.objects[n] for n in rec["added"] if n in bpy.data.objects and bpy.data.objects[n].type == "MESH"] out["objects"] = {"added": len(rec["added"]), "missing": [n for n in rec["added"] if n not in bpy.data.objects][:10]} env = shipfit.Envelope() worst, outside = (9.0, None, None), {} for o in mine: r = shipfit.check_inside(o, env, spacing=0.4, clear_min=0.0) if r["outside"]: outside[o.name] = [r["outside"], r["furthest_outside_m"], r["outside_sample"][:1]] if r["least_clearance_m"] < worst[0]: worst = (r["least_clearance_m"], o.name, r["least_clearance_at"]) out["inside_the_ship"] = {"outside": outside, "least_clearance_m": worst[0], "at": worst[1], "point": worst[2]} if others: names = {o.name for o in mine} lo = Vector((min(_bb(o)[0].x for o in mine) - 0.2, min(_bb(o)[0].y for o in mine) - 0.2, min(_bb(o)[0].z for o in mine) - 0.2)) hi = Vector((max(_bb(o)[1].x for o in mine) + 0.2, max(_bb(o)[1].y for o in mine) + 0.2, max(_bb(o)[1].z for o in mine) + 0.2)) 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 not rr._kind(o).startswith("DECAL_")] near = [o for o in near if o.name not in ("M_SHIP_Hull", "M_SHIP_Dorsal_Roof", "M_SHIP_Canopy")] out["crossings"] = {"against": len(near), "found": _crossings(mine, near)} if sweep: out["carousel"] = carousel_sweep() return out