""" The room under the raised fore main deck: the third floor. The user, 2026-10-13, asking for the fore main deck to be raised: "so that I can utilize the space underneath better. this way we can make another room beneath so its gonna be 3 floor I will add a lift later on"; the next day: "create the room underneath". apply() does it to the file as it is (after fore_deck.apply(): it needs the raised deck above it), the same for the open file and for one built again by build_interior.py: level its roof just under the lowest part of the raised deck (z -3.73), so its floor is 4.2 m lower (about z -7.96): the pack's room height, 3.9 m clear. width at that depth the hull's round bottom is what limits it: 5.88 m from the centre line at floor level at the forward end. The room is 10.8 m wide inside (5.4 m each side), the same all along: a car of the pack's glass floor on the centre line and an aisle of floor modules on each side. length six bays (x 15.86 to 37.46), under the passage and its four rooms and under the aft half of the sensor room and the lift room: the lift the user means to add can come down into its last bay. walls the pack's wall pieces as cut copies (SM_LD_Wall_HB_*): their base would stand 0.9 m below the floor, through the hull's bottom; they are cut at the floor and stand on it, the base's top 0.39 m showing as a skirting, as in the passage. Three whole pieces close each end. ceiling the centre car's own panels and two more rows; the pack's caged tube light under the ceiling over each of the twelve aisle modules, with a lamp under it. The aisles' floor modules are the reactor room's (SM_RR_Floor_Frame: the glass floor tile's top 6 cm, with the pack's perforated panel); the hull's bottom is too close under them for the whole tile. Nothing is moved or removed: everything is added, written down in the text block RECORD, and remove() takes it out again. """ import json import math import os import sys import bpy from mathutils import Matrix, Vector 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 import fore_deck as fd # noqa: E402 TAG = "lower_deck" ZONE = "10.4 | Fore main deck" COLL = "10.4 | Fore main deck | lower deck" RECORD = "LOWER DECK | what was added" MADE_FOR = "M-Ship v2 room under the raised fore main deck (build/lower_deck.py)" PRE = "SM_LD_" NAME = "Lower deck | " BAY = 3.60 DOOR_GAP = 0.35 N_BAYS = 6 X_DOOR = 15.51 # the sub bay's fore door plane: the cars of the fore main deck count their bays from it ROOF = 4.20 # from a car's sill to the top of its ceiling panels CEIL_T = 0.318 # the ceiling panel's depth GAP = 0.02 # between the raised deck's lowest part and this room's roof HALF = 5.40 # half the room's width inside: the aisles' outer edge, where the side walls stand MODULE = 3.60 JOINT = 0.002 OFF = 0.001 WALLS = ("EN_SM_Wall_HB_A", "EN_SM_Wall_HB_A2", "EN_SM_Wall_HB_A3") SEQUENCE = ("EN_SM_Wall_HB_A3", "EN_SM_Wall_HB_A", "EN_SM_Wall_HB_A2", "EN_SM_Wall_HB_A2", "EN_SM_Wall_HB_A", "EN_SM_Wall_HB_A3") END_WALL = "EN_SM_Wall_HB_A3" TUBE = "EP_SM_Light_B" TUBE_WATTS = 200.0 TUBE_COLOR = (0.86, 0.93, 1.0) # the passage's cool white _kind, _bb, _mid = rr._kind, rr._bb, rr._mid 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 def sill_level(): """The floor 4.2 m under the lowest part of the raised fore deck (zone 10.4 as it stands).""" low = min(_bb(o)[0].z for o in bpy.data.objects if o.type == "MESH" and str(o.get("zone", "")).startswith(ZONE) and o.get("made_by") != TAG and not _kind(o).startswith("DECAL_")) return round(low - GAP - ROOF, 3) def _cut(kit_mesh): """The pack's wall piece without its base below the sill (cut 1 mm above the sill line).""" name = PRE + kit_mesh.split("_SM_", 1)[1] me = bpy.data.meshes.get(name) if me is not None: return me return rr.cut_copy(kit_mesh, name, 0.90 + OFF, "the pack's wall piece cut at the floor: under the raised fore deck the hull's bottom " "is too close for the 0.9 m of base the piece has below its sill") POCKET = 0.16 # a floor module's rim and its chamfer reach this far in from the module's edge PANEL_TOP = -0.0175 # the module's perforated panel lies this far below the sill (its top) def mesh_sill_strip(): """Under an end wall piece standing on an aisle's floor module: it fills the 2 cm between the module's panel and the module's rim, inside the rim, so the wall stands on a plate and not over the perforated panel. Local frame: x from the module's end into the room, y from the module's side edge.""" own = rr.Own([rr.RIM]) fd._box(own, (POCKET, POCKET, PANEL_TOP), (0.42, MODULE - POCKET, -OFF), rr.RIM, faces="XyYZ", uv=rr._rect(rr.RIM_UV, (0, 0), (1, 1))) return own.finish(PRE + "Sill_Strip", "plate under an end wall piece of the lower deck, on an aisle's floor module") def apply(kit=None, eng=None): if exists(): raise RuntimeError("the lower deck is already there (text block %r); remove() takes it out" % RECORD) if not fd.exists(): raise RuntimeError("the fore main deck is not raised in this file (fore_deck.apply() first)") import bl_kit import cars import units kit = kit or bl_kit.Kit(log=lambda *a: None) eng = eng or cars.Engineer(units.Demo(cars.WHITE_MAP)) zone_coll = bpy.data.collections[ZONE] coll = quarters.get_collection(COLL, zone_coll) lights = quarters.get_collection(COLL + " | lights", coll) decals = quarters.get_collection(COLL + " | lettering", coll) root = bpy.data.objects.get(quarters.ROOT) s = sill_level() rec = {"version": 1, "added": [], "meshes": [], "sill": s, "collections": [COLL + " | lettering", COLL + " | lights", COLL]} out = {"sill_z": s, "roof_z": round(s + ROOF, 3)} before = set(bpy.data.objects) stats = {} x0 = X_DOOR + DOOR_GAP x1 = x0 + N_BAYS * BAY # 1. the centre car: the pack's glass floor and ceiling, closed at both ends, without its side walls and what # stands in its gutters (the aisles come there), its corner braces and wall-end trims side = [(k, sgn) for k in range(N_BAYS) for sgn in (1, -1)] unit = eng.car("blue", N_BAYS, door_a=False, door_b=False, side_doors=side, props="tidy") unit = unit.filtered(lambda it: not (it.kind == "mesh" and it.name in ("SM_GlassFloor_Side_A", "SM_Wall_HB_Edge", "SM_Doorway_SideBrace", "SM_GlassFloor_Wire_A", "SM_GlassFloor_Wire_B"))) units.place(kit, unit.items, coll, units.frame(X_DOOR, 0.0, s, 0.0), zone=ZONE, lights=lights, decals=decals, parent=root, stats=stats) out["centre_car"] = dict(stats) car = [o for o in bpy.data.objects if o not in before] tiles = sorted([o for o in car if _kind(o) == "EN_SM_GlassFloor_A"], key=lambda o: _mid(o).x) ceils = sorted([o for o in car if _kind(o) == "EN_SM_Ceiling_HB_A"], key=lambda o: _mid(o).x) # 2. the aisles: floor modules, ceiling rows, side walls, the ends, light frame_me = bpy.data.meshes.get(rr.PRE + "Floor_Frame") or rr.mesh_floor_frame() panel = bpy.data.meshes["EN_SM_GlassFloor_Panel"] strip = mesh_sill_strip() rec["meshes"].append(strip.name) if frame_me.name not in rec["meshes"] and frame_me.users == 0: rec["meshes"].append(frame_me.name) cuts = {k: _cut(k) for k in WALLS} for me in cuts.values(): if me.name not in rec["meshes"]: rec["meshes"].append(me.name) T = Matrix.Translation def place(me, name, m, c=coll): o = quarters.place_mesh(me, NAME + name, c, root, m, ZONE) if "ue_mesh" in me: o["ue_mesh"] = me["ue_mesh"] return o zt = s + 3.90 # the wall pieces' heads for sgn, sname in ((1.0, "port"), (-1.0, "starboard")): for i, t in enumerate(tiles): m = T((0.0, sgn * (MODULE + JOINT), 0.0)) @ t.matrix_world place(frame_me, "floor module %s %d" % (sname, i + 1), m) place(panel, "floor panel %s %d" % (sname, i + 1), m) for i, c in enumerate(ceils): place(c.data, "ceiling %s %d" % (sname, i + 1), T((0.0, sgn * 4.8, 0.0)) @ c.matrix_world) # side walls: the piece's room face at the aisle's edge, cut at the floor for i in range(N_BAYS): xa = x0 + i * BAY me = cuts[SEQUENCE[i]] yaw = 180.0 if sgn > 0 else 0.0 # (the piece's room face looks along its local +y) quarters._bounds_cache.pop(me.name, None) o = quarters.put(me.name, coll, root, ZONE, (xa + BAY / 2.0, sgn * (HALF + 2 * JOINT), s + OFF), yaw=yaw, anchor="0" + ("-" if sgn > 0 else "+") + "-", name=NAME + "wall %s %d" % (sname, i + 1)) o["cut_from"] = me["cut_from"] # the ends: a whole piece across each aisle, on a plate over the module's panel for e, end, xe in ((1.0, "aft", x0), (-1.0, "fore", x1)): ya, yb = sorted((sgn * (1.80 + JOINT), sgn * (HALF + JOINT))) me = cuts[END_WALL] o = quarters.put(me.name, coll, root, ZONE, (xe, ya, s + OFF), yaw=-90.0 * e, anchor=("-" if e > 0 else "+") + "--", name=NAME + "end wall %s %s" % (end, sname)) o["cut_from"] = me["cut_from"] mx = T((xe, ya, s)) if e > 0 else T((xe, yb, s)) @ Matrix.Rotation(math.pi, 4, "Z") place(strip, "sill strip %s %s" % (end, sname), mx) out["aisle_objects"] = len([o for o in bpy.data.objects if o not in before]) - len(car) # 3. light: the pack's caged tube under the ceiling over each floor module, with a lamp under it n_lamps = 0 for sgn, sname in ((1.0, "port"), (-1.0, "starboard")): for i in range(N_BAYS): at = (x0 + (i + 0.5) * BAY, sgn * 3.6, s + ROOF - CEIL_T - OFF) # (under the ceiling panel) quarters.put(TUBE, coll, root, ZONE, at, yaw=90.0, anchor="00+", tilt=("Y", 90.0), name=NAME + "ceiling light %s %d" % (sname, i + 1)) quarters.area_lamp(lights, root, NAME + "ceiling lamp %s %d" % (sname, i + 1), (at[0], at[1], at[2] - 0.13), TUBE_WATTS, (1.50, 0.14), color=TUBE_COLOR, zone=ZONE) n_lamps += 1 new = [o for o in bpy.data.objects if o not in before] for o in new: o["made_by"] = TAG rec["added"].append(o.name) out["lamps"] = n_lamps out["objects"] = len(new) out["room"] = {"x": [round(x0 + 0.42, 2), round(x1 - 0.42, 2)], "y": [-HALF, HALF], "floor_z": s, "clear_height_m": 3.90} bpy.data.texts.new(RECORD).write(json.dumps(rec)) bpy.context.view_layer.update() return out def remove(): rec = record() if rec is None: return {"present": False} n = 0 for name in rec["added"]: o = bpy.data.objects.get(name) if o is not None: d = o.data bpy.data.objects.remove(o, do_unlink=True) n += 1 if isinstance(d, bpy.types.Light) and d.users == 0: bpy.data.lights.remove(d) for name in rec["meshes"]: me = bpy.data.meshes.get(name) if me is not None and me.users == 0: rr._forget(name) for name in rec["collections"]: c = bpy.data.collections.get(name) if c is not None and not c.all_objects: bpy.data.collections.remove(c) bpy.data.texts.remove(bpy.data.texts[RECORD]) bpy.context.view_layer.update() return {"present": True, "taken_out": n} def check(others=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] meshes = [o for o in mine if o.type == "MESH" and not _kind(o).startswith("DECAL_")] out["objects"] = {"added": len(rec["added"]), "missing": [n for n in rec["added"] if n not in bpy.data.objects][:10]} out["sill_z"] = rec["sill"] top = max(_bb(o)[1].z for o in meshes) above = min(_bb(o)[0].z for o in bpy.data.objects if o.type == "MESH" and str(o.get("zone", "")).startswith(ZONE) and o.get("made_by") != TAG and not _kind(o).startswith("DECAL_")) out["roof_z"] = round(top, 3) out["gap_under_the_raised_deck_m"] = round(above - top, 3) env = shipfit.Envelope() worst, outside = (9.0, None, None), {} for o in meshes: r = shipfit.check_inside(o, env, spacing=0.15, clear_min=0.0) if r["outside"]: outside[o.name] = [_kind(o), 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"] = {"objects": len(meshes), "outside": outside, "least_clearance_m": worst[0], "at": worst[1], "point": worst[2]} if others: scene = (meshes[0].users_scene or [bpy.context.scene])[0] names = {o.name for o in mine} lo = Vector((min(_bb(o)[0].x for o in meshes) - 0.5, min(_bb(o)[0].y for o in meshes) - 0.5, min(_bb(o)[0].z for o in meshes) - 0.5)) hi = Vector((max(_bb(o)[1].x for o in meshes) + 0.5, max(_bb(o)[1].y for o in meshes) + 0.5, max(_bb(o)[1].z for o in meshes) + 0.5)) near = [] for o in scene.objects: if o.type != "MESH" or o.name in names or o.name.startswith("KIT | "): continue a, b = _bb(o) if any(a[i] > hi[i] or lo[i] > b[i] for i in range(3)): continue near.append(o) got = _crossings(meshes, near) bad = {k: v for k, v in got.items() if any(p.startswith(("[ship]", PRE, rr.PRE)) for p in k.split(" x ")) and k not in rr.KIT_JOINTS and not _joint(k)} out["crossings"] = {"against": len(near), "with_the_ship_or_parts_made_here": bad, "pack_pairs": dict(sorted(((k, v) for k, v in got.items() if k not in bad), key=lambda kv: -kv[1])[:24])} return out def _label(o): k = _kind(o) return k if (k.startswith((PRE, rr.PRE)) or o.data.get("ue_mesh")) else "[ship] " + o.name def _crossings(mine, others): cache, out = {}, {} ms = [o for o in mine if o.type == "MESH" and not _kind(o).startswith("DECAL_")] os_ = [o for o in others if o.type == "MESH" and not _kind(o).startswith("DECAL_")] for i, a in enumerate(ms): for b in ms[i + 1:] + os_: 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 _joint(key): """The pack's own joints with this room's cut wall pieces (as in the passage: the head 2 cm in the ceiling's lip, the light fitting in its wall).""" a, b = key.split(" x ") pair = {a, b} cut = {PRE + w.split("_SM_", 1)[1] for w in WALLS} return bool(pair & cut) and bool(pair & {"EN_SM_Ceiling_HB_A", "EN_SM_Light_A"}) or pair == {"EN_SM_GlassFloor_Panel", rr.PRE + "Floor_Frame"}