""" Builds the vessel's interior from the Modular SciFi Season 2 Starter Bundle, with the Season 1 bundle mixed in (the sub bay's walls and lettering, the bridge's command floor, furniture), inside the fork of the ship file. blender -b --factory-startup models/M_Ship_v2_interior_season2.blend --python build/build_interior.py [-- --no-save] What it does to the file it is run in: * removes the earlier layout (collections "04 INTERIOR | Main vessel layout", "05 KIT | Sci-Fi Panels ...") and anything a previous run of this script made; the ship, the dock room and the stairs kit are left alone; * builds the interior described in ../README.md under "10 INTERIOR | Season 2 layout"; * saves the file. Ship frame: +X bow, +Y port, Z up, metres. Every kit piece is an instance of the pack's own mesh and carries its Unreal asset path (["ue_mesh"]). """ 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__)) sys.path.insert(0, HERE) import bl_kit # noqa: E402 from bl_kit import Kit, exclude_collection, get_collection # noqa: E402 from cars import BAY, CAR_HALF, DOOR_GAP, Engineer, is_struct # noqa: E402 from hall import BAY_H, DOOR_GAP_H, LENGTH_3, build_hall # noqa: E402 from liner import PANEL_DEPTH, PANEL_HEIGHT, wall_liner # noqa: E402 import lettering # noqa: E402 import s1 # noqa: E402 import shipfit # noqa: E402 import stairs_kit # noqa: E402 from units import Demo, Unit, frame, place # noqa: E402 ARGS = sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else [] MODELS = os.path.dirname(os.path.dirname(HERE)) DOCKARM = os.path.join(MODELS, "docking-arm", "DockArm_Carousel.blend") KAZAH = os.path.join(MODELS, "Kazah-stern-cavity-fixedv4.blend") KAZAH_LIST = os.path.join(HERE, "kazah_objects.json") # ---- the plan's fixed numbers ----------------------------------------------------------------------------------- Z2 = -3.36 # main deck: the dock room's deck, also every door threshold on this deck Z3 = 3.14 # command deck threshold (its door frames end just under the dock room's roof) X_AFT, X_FORE = -9.99, 15.51 # the dock room's two ends = the door planes into it BULK_T = 0.30 # the two bulkheads stand inside the dock room's ends HUB = (3.05, 0.0, Z2 + 7.5) # carousel hub: over the well's middle (x), on the centre line, 7.5 m above the deck GALLERY = (12.3, X_FORE - BULK_T, -3.6, 7.2) # x0, x1, y0, y1 of the command-deck gallery in the dock room STAIR_Y = 6.0 # the sub bay's straight lining (the user's note of 2026-10-12: a rectangular lining is fine where the hull's shape # is hard to build around): a storey of the pack's wall panels inside the curved shell, under the Tender swing LINE_BACK = 8.95 # back plane of the two long walls (the shell is 9.00 to 9.36 from the centre line there) LINE_PANELS = 6 # whole panels a side, centred on the carousel hub FLANK = (3.37, 6.97) # the one panel each side of a bulkhead door, centred in its half of the bulkhead # Season 1 in the mix (the user, 2026-10-12: "use this assets too ... especially moon pool room suits better with this") BAY_LINING = "season1" # the sub bay's walls: "season1" (Hallways walls on the sides, Interiors walls on the ends) # or "season2" (the Engineer pack's panels) S1_SIDE_BAYS = 5 # 4.4 m bays a side; the middle panel is centred on the carousel hub BRIDGE_SET = "season1" # "season1": the command centre's round floor and terminal; "season2": the hall pad as a dais FURNISH = True # Season 1 furniture rows in the crew rooms and the CIC # lamp strength of the Season 1 demo lights in this file (x their Unreal value x 0.02 W), per demo map S1_GAIN = {s1.HALL_MAP: 1.0, s1.INT_MAP: 1.6, s1.CC_MAP: 4.0} PLATE = 0.03 # thickness of the kit's white floor plates ROOT_NAME = "M_SHIP_INTERIOR_S2" TOP = "10 INTERIOR | Season 2 layout" KIT_COLL = "11 KIT | Modular SciFi Season 2 (source, one of each)" KIT1_COLL = "13 KIT | Modular SciFi Season 1 (source, one of each)" OLD = ("04 INTERIOR | Main vessel layout", "05 KIT | Sci-Fi Panels Low Poly vol.1 (source)") TIDY_HALL = {"SM_Wire_Floor_A", "SM_Wire_Floor_B", "SM_RubberMat_Flat", "SM_RubberMat_Rolled", "SM_Wrench", "SM_Screwdriver", "SM_Pipe_Bar", "SM_Pipe_90", "SM_Pipe_90_b", "SM_Pipe_ASV", "SM_PortableLight", "SM_OxygenTank", "SM_NitrogenTank", "SM_NitrogenTank_Covered"} log = print # --------------------------------------------------------------------------------------------------------------- def remove_tree(name): col = bpy.data.collections.get(name) if col is None: return 0 n = 0 for c in list(col.children_recursive) + [col]: for ob in list(c.objects): bpy.data.objects.remove(ob, do_unlink=True) n += 1 for c in list(col.children_recursive) + [col]: bpy.data.collections.remove(c) return n def clean(): n = sum(remove_tree(c) for c in OLD + (TOP, KIT_COLL, KIT1_COLL)) for ob in [o for o in bpy.data.objects if o.name == ROOT_NAME or o.name.startswith("_Tender")]: bpy.data.objects.remove(ob, do_unlink=True) for t in ("LAYOUT README", "INTERIOR README"): if t in bpy.data.texts: bpy.data.texts.remove(bpy.data.texts[t]) for blocks in (bpy.data.meshes, bpy.data.lights, bpy.data.materials, bpy.data.images, bpy.data.collections): for b in [b for b in blocks if b.users == 0 and not b.use_fake_user]: if blocks is bpy.data.materials and not b.name.startswith(("PANEL | ", "LIGHT | ", "LINING | ", "M_DockArm", "SI_", "EN_", "EP_", "HW_", "IN_", "CC_", "PR_", "MT_")): continue if blocks is bpy.data.collections and not b.name.startswith(("10", "11", "12 CONTEXT", "13", "DockArm")): continue if blocks is bpy.data.images and not (b.get("ue_texture") or "sci_fi_panels_1k" in b.filepath.replace("\\", "/")): continue blocks.remove(b) log("removed %d objects of earlier layouts" % n) SKIRT = "S2 mask skirt inside the dock room" def mask_pod_skirt(fit): """The roof pod's lower skirt is a shell fragment that hangs through the dock room's roof lining (two white strips at |y| 5.6). Hide that part without deleting anything: vertex group + Mask modifier on the pod. Faces that also reach outside the hull are left alone, so the outside of the ship does not change.""" pod = bpy.data.objects.get("M_SHIP_Dorsal_Roof") if pod is None: return 0 for m in [m for m in pod.modifiers if m.name == SKIRT]: pod.modifiers.remove(m) if pod.vertex_groups.get(SKIRT): pod.vertex_groups.remove(pod.vertex_groups[SKIRT]) me, mw = pod.data, pod.matrix_world state = {} for v in me.vertices: p = mw @ v.co if not (X_AFT + 0.05 < p.x < X_FORE - 0.05): state[v.index] = 0 elif fit.ray(p, (0, 0, 1), ("Moonpool | hull lining",)) is not None: state[v.index] = 2 # under the lining: inside the dock room else: state[v.index] = 1 if fit.ray(p, (0, 0, 1), ("M_SHIP_Hull",)) is not None else 3 # 3 = outside the hull keep = set() for poly in me.polygons: st = [state[i] for i in poly.vertices] if 3 in st and 2 in st: keep.update(poly.vertices) hide = [i for i, s in state.items() if s == 2 and i not in keep] if not hide: return 0 g = pod.vertex_groups.new(name=SKIRT) g.add(hide, 1.0, "REPLACE") m = pod.modifiers.new(SKIRT, "MASK") m.vertex_group = SKIRT m.invert_vertex_group = True return len(hide) README_TEXT = """M-Ship v2 | interior built from the Modular Scifi Season 2 Starter Bundle with Season 1 mixed in (Fab), 2026-10-12 Full description, plan, checks and rebuild commands: models/vessel-interior/README.md Ship frame: +X bow, +Y port, Z up, metres. Main deck threshold z -3.36 (the dock room's deck), command deck z 3.14. MAIN DECK, aft to bow Engine control (cube) - Reactor gallery (red-lit pipe gallery) - WORKS HALL (the pack's hall, 3 bays: cargo, ore processing, fabrication; the port middle bay is the boarding gate at the hull's service hatch) - SUB BAY (the dock room: well, three-armed Tender carousel, stair to the gallery; its lower 4.5 m is lined with straight walls of the SEASON 1 bundle inside the curved shell) - fore passage with Galley and Quarters A to port, Habitation and Quarters B to starboard - Sensor room (cube). COMMAND DECK Gallery in the sub bay - CIC (gold-walled room, sonar table) - Bridge (deck under the canopy with the Season 1 command floor and its terminal). Two bundles are mixed: Season 2 (ModSciInteriors, ModSci_Engineer, ModSci_EngiProps) for the engineering spaces, the passages and the rooms' structure; Season 1 (StarterBundle: Hallways, Interiors, Command centre, Props, decals) for the sub bay's walls and lettering, the bridge's command floor, and the furniture in the crew rooms and the CIC. Everything of this layout is under "10 INTERIOR | Season 2 layout" and parented to M_SHIP_INTERIOR_S2. Kit pieces are instances of the pack's own meshes: mesh["ue_mesh"] = Unreal asset path, object-linked material slots = per-instance overrides, object["zone"] = which room. "MARKER | ..." empties are game hooks (fixtures the game places). "11 KIT | ..." (excluded from the view layer) holds every kit mesh once, for changes by hand. The Tenders are linked from Kazah-stern-cavity-fixedv4.blend for the picture only. Non-destructive edits to the ship's own meshes (Mask modifiers + vertex groups, nothing deleted): Moonpool | hull lining : 'LAYOUT mask end caps' (the two bulkheads with doors stand there instead) M_SHIP_Hull : 'LAYOUT mask pod opening' (the command deck reaches up into the roof pod) M_SHIP_Dorsal_Roof : 'S2 mask skirt inside the dock room' The carousel hub is on the centre line: there it turns clear of the hull, but its launch arm is 2.3 m outboard of the well's middle (see the README). """ class Zone(object): def __init__(self, name, parent): self.mesh = get_collection(name, parent) self.lights = get_collection(name + " | lights", self.mesh) self.decals = get_collection(name + " | lettering", self.mesh) self.name = name def marker(coll, root, name, loc, role, size=0.5, kind="PLAIN_AXES"): e = bpy.data.objects.new("MARKER | " + name, None) e.empty_display_type = kind e.empty_display_size = size coll.objects.link(e) e.parent = root e.location = loc e["ue_role"] = role return e def lamp(coll, root, name, loc, watts, color=(0.80, 0.90, 1.0), radius=0.25): d = bpy.data.lights.new(name, "POINT") d.energy = watts d.color = color d.shadow_soft_size = radius ob = bpy.data.objects.new(name, d) coll.objects.link(ob) ob.parent = root ob.location = loc ob["ue_light_class"] = "PointLightComponent" return ob def dark_material(): """Plain near-black satin metal for the parts made here that stand among the Season 1 walls (piers, cap ledge).""" m = bpy.data.materials.get("LINING | dark structure") if m is None: m = bpy.data.materials.new("LINING | dark structure") m.use_nodes = True b = next(n for n in m.node_tree.nodes if n.bl_idname == "ShaderNodeBsdfPrincipled") b.inputs["Base Color"].default_value = (0.022, 0.024, 0.027, 1.0) b.inputs["Metallic"].default_value = 0.6 b.inputs["Roughness"].default_value = 0.42 m.diffuse_color = (0.05, 0.05, 0.055, 1.0) return m def template(unit, name): """A mesh item of that kit name out of a unit: its mesh path and material overrides are reused for loose pieces.""" return next(it for it in unit.items if it.kind == "mesh" and it.name == name) def put(kit, tpl, coll, root, m, zone): ob = kit.place(tpl.key, m, coll, overrides=tpl.data) ob.parent = root ob.matrix_world = m ob["zone"] = zone return ob # --------------------------------------------------------------------------------------------------------------- def main(): scene = bpy.data.scenes.get("M-Ship v2 | REFERENCE ASSET") or bpy.context.scene if bpy.context.window is not None: bpy.context.window.scene = scene ship_root = bpy.data.objects["M_SHIP_ROOT"] clean() top = get_collection(TOP, scene.collection) root = bpy.data.objects.new(ROOT_NAME, None) root.empty_display_type = "PLAIN_AXES" top.objects.link(root) root.parent = ship_root bpy.context.view_layer.update() kit = Kit(log=log) k1 = s1.kit(log) # Season 1, read from models/anchorage demo = Demo("ModSci_Engineer__Example_Dynamic_White") eng = Engineer(demo) fit = shipfit.Fit(["M_SHIP_Hull", "Moonpool | hull lining"]) log("pod skirt: %d vertices hidden inside the dock room" % mask_pod_skirt(fit)) bpy.data.texts.new("INTERIOR README").write(README_TEXT) LINING = ("Moonpool | hull lining",) HULL = ("M_SHIP_Hull",) stats = {} mat_lining = bpy.data.materials.get("PBR | Moonpool interior") mat_deck = bpy.data.materials.get("PBR | Graphite lower hull") L = Engineer.length def P(unit, zone, f, **kw): return place(kit, unit.items, zone.mesh, f, zone=zone.name, lights=zone.lights, decals=zone.decals, parent=root, stats=stats, **kw) def P1(unit, zone, f, gain=1.0, **kw): """A Season 1 unit.""" return place(k1, unit.items, zone.mesh, f, zone=zone.name, lights=zone.lights, decals=zone.decals, parent=root, stats=stats, light_scale=gain, **kw) ROW_LEN, ROW_BACK = 5.1, 1.66 # a furniture row: its length, and how far its back stands from the car's axis def cleared(unit, x0, side): """A car unit without what would stand under a furniture row: loose things and floor fittings within the row's footprint (x0..x0 + ROW_LEN along the car, from the row's back to 2.1 m in front of it on `side`).""" if not FURNISH: return unit def keep(it): if it.kind != "mesh" or is_struct(it): return True c = it.center under = x0 - 0.05 <= c.x <= x0 + ROW_LEN + 0.05 and -0.45 <= c.y * side <= ROW_BACK + 0.06 and c.z < 0.45 return not under return unit.filtered(keep) def crew(unit): """The pack's hanging signs read CENTRAL ENGINEERING: right aft, wrong in the crew spaces and on the command deck.""" return unit.filtered(lambda it: not (it.kind == "mesh" and it.name == "SM_CeilingSign")) # ============================================================================================ 1. SUB BAY bay = Zone("10.1 | Sub bay", top) door2 = (-1.8, 1.8, Z2, Z2 + 3.9) door3 = (-1.8, 1.8, Z3, Z3 + 3.8) # (the hall's end wall reaches 5 cm past the door plane, so the aft bulkhead starts 6 cm inside it) shipfit.bulkhead_x("Bulkhead | sub bay aft", bay.mesh, root, fit, LINING, X_AFT + 0.2, X_AFT + 0.06, X_AFT + 0.06 + BULK_T, Z2, 7.2, openings=[door2], material=mat_lining) shipfit.bulkhead_x("Bulkhead | sub bay fore", bay.mesh, root, fit, LINING, X_FORE - 0.15, X_FORE, X_FORE - BULK_T, Z2, 7.2, openings=[door2, door3], material=mat_lining) # gallery at command-deck level along the fore bulkhead, outside the carousel's swing gx0, gx1, gy0, gy1 = GALLERY shipfit.slab("Gallery | command deck landing", bay.mesh, root, gx0, gx1, gy0, gy1, Z3 - PLATE, 0.22, material=mat_deck) white = eng._src_car("white") plate = template(white, "SM_Floor_A1") # 3.6 x 2.4 white plate, mesh spans x -3.6..0, y 0..2.4, top at z 0 rail = next(it for it in demo.items if it.kind == "mesh" and it.name == "SM_Rail_A" and it.key.split("/")[2] == "ModSci_Engineer") for k in range(3): # plates laid with their 3.6 m side along the gallery m = Matrix.Translation((gx0 + 0.25 + 2.4, gy0 + 3.6 * (k + 1), Z3)) @ Matrix.Rotation(math.radians(90.0), 4, "Z") put(kit, plate, bay.mesh, root, m, bay.name) def rail_run(x0, y0, x1, y1, skip=()): """Rail sections of 1.64 m from (x0, y0) toward (x1, y1); skip = indices left out (a gap).""" d = Vector((x1 - x0, y1 - y0, 0.0)) n = int(d.length // 1.64) yaw = math.atan2(d.y, d.x) u = d.normalized() for i in range(n): if i in skip: continue c = Vector((x0, y0, 0)) + u * (1.64 * (i + 0.5)) put(kit, rail, bay.mesh, root, Matrix.Translation((c.x, c.y, rail_run.z)) @ Matrix.Rotation(yaw, 4, "Z"), bay.name) return n rail_run.z = Z3 rail_run(gx0 + 0.08, gy0 + 0.1, gx0 + 0.08, STAIR_Y - 0.62) # along the gallery's edge up to the stair rail_run(gx0 + 0.3, gy0 + 0.06, gx1 - 0.1, gy0 + 0.06) # its starboard end rail_run(gx0 + 0.3, gy1 - 0.06, gx1 - 0.1, gy1 - 0.06) # its port end # rails round the well: both ends and the inboard side, with a boarding gap in the middle rail_run.z = Z2 rail_run(-3.45, -6.0, -3.45, -0.9) rail_run(9.55, -6.0, 9.55, -0.9) rail_run(3.05 - 3.5 * 1.64, -0.18, 3.05 + 3.5 * 1.64, -0.18, skip=(3,)) bay_lamps = [(-6.0, -5.5, 4.6), (-6.0, 5.5, 4.6), (12.0, -5.5, 2.4), (11.0, 3.5, 6.2)] if BAY_LINING != "season1": # (the Season 1 walls bring their own lights for the lower storey) bay_lamps += [(3.05, -7.6, 0.2), (3.05, 6.5, 0.8)] for i, (x, y, z) in enumerate(bay_lamps): lamp(bay.lights, root, "Sub bay lamp %d" % (i + 1), (x, y, z), 900.0) # ---- the straight lining: flat walls of whole kit bays inside the curved shell, under the Tender swing ---------- lin = Zone("10.1 | Sub bay | straight lining", bay.mesh) s_stair_run = (Z3 - Z2) / (stairs_kit.Z_DECK) * stairs_kit.RUN if hasattr(stairs_kit, "Z_DECK") else 6.53 if BAY_LINING == "season1": # Season 1: the Hallways pack's hall wall along both sides (locker band, two grey panels, black pilasters with # bronze fins), the Interiors pack's white wall on the two ends, each end with its middle bay left open for # the Season 2 doorway that stands in the bulkhead behind it. mat_pier = dark_material() z_cap = Z2 + s1.WHITE_WALL_H # 1.14: top of the pilasters; the Tenders swing 1.0 m above y_face = LINE_BACK - s1.HALL_WALL_DEPTH # 8.35: the side pilasters' room face xa_in, xf_in = X_AFT + 0.06 + BULK_T, X_FORE - BULK_T # the two bulkheads' room faces xa_face, xf_face = xa_in + s1.WHITE_WALL_DEPTH, xf_in - s1.WHITE_WALL_DEPTH ls = s1.run_length(S1_SIDE_BAYS) x_run0, x_run1 = HUB[0] - ls / 2, HUB[0] + ls / 2 side = s1.hall_wall(S1_SIDE_BAYS) P1(side, lin, frame(x_run0, -LINE_BACK, Z2, 0.0), S1_GAIN[s1.HALL_MAP]) # starboard (its room side is +y) P1(side, lin, frame(x_run1, LINE_BACK, Z2, 180.0), S1_GAIN[s1.HALL_MAP]) # port: the same run turned round le = s1.run_length(3) y_end = le / 2 end = s1.white_wall(3, kinds=("wall", "open", "wall")) P1(end, lin, frame(xa_in, y_end, Z2, -90.0), S1_GAIN[s1.INT_MAP]) # aft end, room side +x P1(end, lin, frame(xf_in, -y_end, Z2, 90.0), S1_GAIN[s1.INT_MAP]) # fore end, room side -x def pier(tag, x0, x1, y0, y1, z0=Z2, z1=None): return shipfit.box("Pier | " + tag, lin.mesh, root, x0, x1, y0, y1, z0, z_cap if z1 is None else z1, material=mat_pier) for s, side_name in ((1, "port"), (-1, "starboard")): pier("%s wall, aft end" % side_name, xa_in, x_run0, s * y_face, s * LINE_BACK) pier("%s wall, fore end" % side_name, x_run1, xf_in, s * y_face, s * LINE_BACK) pier("aft end wall, %s corner" % side_name, xa_in, xa_face, s * y_end, s * y_face) pier("fore end wall, %s corner" % side_name, xf_face, xf_in, s * y_end, s * y_face) # the cap: a ledge from the walls' face back to the shell, so nothing is open behind them shipfit.ledge_fitted("Ledge | %s wall" % side_name, lin.mesh, root, fit, LINING, xa_in, xf_in, s * (y_face - 0.04), s, z_cap + 0.06, 0.06, material=mat_pier) pier("aft door lintel", xa_in, xa_face, -1.79, 1.79, Z2 + 3.9) pier("fore door lintel", xf_face, xf_in, -1.79, 1.79, Z2 + 3.9) shipfit.box("Ledge | aft end wall", lin.mesh, root, xa_in, xa_face + 0.04, -(y_face - 0.04), y_face - 0.04, z_cap, z_cap + 0.06, material=mat_pier) shipfit.box("Ledge | fore end wall", lin.mesh, root, xf_face - 0.04, xf_in, -(y_face - 0.04), y_face - 0.04, z_cap, z_cap + 0.06, material=mat_pier) # floor lettering, the Season 1 bundle's own decals, by Anchorage's rules (lettering.py) zd = Z2 + 0.005 # the deck's top marks = (lettering.word(k1, "CARGO", xa_face + 2.1, 0.0, zd, 180.0) # before the door to the works hall + lettering.word(k1, "DOCK", xf_face - 2.1, 0.0, zd, 180.0) # entering from the fore passage + lettering.word(k1, "CONTROL", gx0 - s_stair_run - 2.1, STAIR_Y, zd, 0.0) # before the stair to the command deck + lettering.word(k1, "STAND CLEAR", HUB[0], 1.75, zd, -90.0, chevrons=False) # at the boarding gap of the well + lettering.hazard_band(k1, HUB[0], 0.35, zd, -90.0, width=10.0)) P1(Unit(marks), lin, Matrix.Identity(4)) else: # Season 2: six whole panels of the Engineer pack a side, one each side of the bulkhead doors y_face = LINE_BACK - PANEL_DEPTH # 8.53: the walls' room face z_cap = Z2 + PANEL_HEIGHT # 1.44: top of the panels; the Tenders swing 0.7 m above it xa_in, xf_in = X_AFT + 0.06 + BULK_T, X_FORE - BULK_T # the two bulkheads' room faces xa_face, xf_face = xa_in + PANEL_DEPTH, xf_in - PANEL_DEPTH x_run0, x_run1 = HUB[0] - LINE_PANELS / 2 * BAY, HUB[0] + LINE_PANELS / 2 * BAY wall = wall_liner(eng, seq=tuple(k % 3 for k in range(LINE_PANELS))) P(wall, lin, frame(x_run0, -LINE_BACK, Z2, 0.0)) # starboard wall (its room side is +y) P(wall, lin, frame(x_run1, LINE_BACK, Z2, 180.0)) # port wall: the same run turned round one = wall_liner(eng, seq=(1,)) for s in (1, -1): P(one, lin, frame(xa_in, FLANK[1] if s > 0 else -FLANK[0], Z2, -90.0)) # aft bulkhead, room side +x P(one, lin, frame(xf_in, FLANK[0] if s > 0 else -FLANK[1], Z2, 90.0)) # fore bulkhead, room side -x def pier(tag, x0, x1, y0, y1, z0=Z2, z1=None): return shipfit.box("Pier | " + tag, lin.mesh, root, x0, x1, y0, y1, z0, z_cap if z1 is None else z1, material=mat_deck) for s, side in ((1, "port"), (-1, "starboard")): pier("%s wall, aft end" % side, xa_in, x_run0, s * y_face, s * LINE_BACK) pier("%s wall, fore end" % side, x_run1, xf_in, s * y_face, s * LINE_BACK) pier("aft bulkhead, %s of the door" % side, xa_in, xa_face, s * 1.8, s * FLANK[0]) pier("aft bulkhead, %s corner" % side, xa_in, xa_face, s * FLANK[1], s * y_face) pier("fore bulkhead, %s of the door" % side, xf_face, xf_in, s * 1.8, s * FLANK[0]) pier("fore bulkhead, %s corner" % side, xf_face, xf_in, s * FLANK[1], s * y_face) # the cap: a ledge from the wall's face back to the shell, so nothing is open behind the panels shipfit.ledge_fitted("Ledge | %s wall" % side, lin.mesh, root, fit, LINING, xa_in, xf_in, s * (y_face - 0.04), s, z_cap + 0.06, 0.06, material=mat_deck) pier("aft door lintel", xa_in, xa_face, -1.8, 1.8, Z2 + 3.9) pier("fore door lintel", xf_face, xf_in, -1.8, 1.8, Z2 + 3.9) shipfit.box("Ledge | aft bulkhead", lin.mesh, root, xa_in, xa_face + 0.04, -(y_face - 0.04), y_face - 0.04, z_cap, z_cap + 0.06, material=mat_deck) shipfit.box("Ledge | fore bulkhead", lin.mesh, root, xf_face - 0.04, xf_in, -(y_face - 0.04), y_face - 0.04, z_cap, z_cap + 0.06, material=mat_deck) # ============================================================================================ 2. WORKS HALL works = Zone("10.2 | Works hall", top) hall = build_hall(demo, props="tidy", loose=TIDY_HALL) # the outermost base strip of the aft bay would stand in the hull's taper; it lies under the wall, out of sight hall = hall.filtered(lambda it: not (it.kind == "mesh" and it.name == "SM_Floor_Main_Side_D" and it.center.x < -DOOR_GAP_H - 2 * BAY_H)) f_hall = frame(X_AFT, 0.0, Z2, 0.0) P(hall, works, f_hall) x_hall_w = X_AFT - LENGTH_3 # the hall's aft door plane # ============================================================================================ 3. REACTOR GALLERY eng_zone = Zone("10.3 | Reactor gallery and engine control", top) P(eng.car("red", 3, props="tidy"), eng_zone, frame(x_hall_w, 0.0, Z2, 180.0)) x_ec = x_hall_w - L(3) P(eng.conn, eng_zone, frame(x_ec, 0.0, Z2, 180.0)) P(eng.cube("W", props="tidy"), eng_zone, frame(x_ec, 0.0, Z2, 180.0)) # ============================================================================================ 4. FORE MAIN DECK fore = Zone("10.4 | Fore main deck", top) N_SPINE = 6 rooms = [ # name, first spine bay, bays, side (+1 port), door at spine bay, wall finish ("Galley", 0, 3, +1, 1, "white"), ("Quarters A", 3, 2, +1, 3, "white"), ("Habitation", 0, 3, -1, 1, "gold"), ("Quarters B", 3, 2, -1, 3, "white")] P(crew(eng.car("blue", N_SPINE, side_doors=[(r[4], r[3]) for r in rooms], props="tidy")), fore, frame(X_FORE, 0.0, Z2, 0.0)) P(eng.conn, fore, frame(X_FORE, 0.0, Z2, 0.0)) P(eng.conn, fore, frame(X_FORE + L(N_SPINE), 0.0, Z2, 0.0)) P(eng.cube("W", props="tidy"), fore, frame(X_FORE + L(N_SPINE), 0.0, Z2, 0.0)) room_frames = {} for name, b0, n, s, d, finish in rooms: f = frame(X_FORE + b0 * BAY, s * 2 * CAR_HALF, Z2, 0.0) room_frames[name] = (f, n, s) room = crew(eng.car(finish, n, door_a=False, door_b=False, side_doors=[(d - b0, -s)], props="tidy")) P(cleared(room, DOOR_GAP + n * BAY / 2 - ROW_LEN / 2, s), fore, f) P(eng.conn, fore, frame(X_FORE + DOOR_GAP + (d + 0.5) * BAY, s * CAR_HALF, Z2, 90.0 * s)) if FURNISH: # Season 1 furniture rows (the Props pack's office demo) along each room's outer wall, in front of its rail ROW = {"Galley": "bench", "Habitation": "desks", "Quarters A": "study", "Quarters B": "bench"} for name, (f, n, s) in room_frames.items(): xc = f.translation.x + DOOR_GAP + n * BAY / 2 yc = f.translation.y fr = frame(xc + ROW_LEN / 2, yc + ROW_BACK, Z2 - 0.9, 180.0) if s > 0 else frame(xc - ROW_LEN / 2, yc - ROW_BACK, Z2 - 0.9, 0.0) P1(s1.furniture_row(ROW[name]), fore, fr) # ============================================================================================ 5. COMMAND DECK cmd = Zone("10.5 | Command deck", top) N_CIC = 4 P(eng.conn, cmd, frame(X_FORE, 0.0, Z3, 0.0)) cic = crew(eng.car("gold", N_CIC, props="tidy")) x_mid_car = DOOR_GAP + N_CIC * BAY / 2 cic = cleared(cleared(cic, x_mid_car - 0.5 - ROW_LEN, +1), x_mid_car + 0.5, -1) # under its two plotting stations P(cic, cmd, frame(X_FORE, 0.0, Z3, 0.0)) x_br = X_FORE + L(N_CIC) P(eng.conn, cmd, frame(x_br, 0.0, Z3, 0.0)) # the bridge's aft bulkhead: closes the hull round the CIC's door (the hull, and the roof pod where the hull opens into it) shell = shipfit.FitFirst(shipfit.Fit(["M_SHIP_Hull", "M_SHIP_Dorsal_Roof"]), ("M_SHIP_Hull", "M_SHIP_Dorsal_Roof")) shipfit.bulkhead_x("Bulkhead | bridge aft", cmd.mesh, root, shell, None, x_br + 0.25, x_br + 0.4, x_br + 0.1, Z3 - PLATE - 0.25, 10.6, openings=[(-1.8, 1.8, Z3, Z3 + 3.9)], material=mat_lining) fit_hull = shipfit.Fit(["M_SHIP_Hull"]) shipfit.deck_fitted("Deck | bridge", cmd.mesh, root, fit_hull, HULL, x_br + 0.4, 57.0, Z3 - PLATE, 0.25, material=mat_deck, inset=0.08) x_cc = x_br + 0.4 + s1.CC_DOOR_X # 40.61: the command floor's centre, its own door distance from the bulkhead if BRIDGE_SET == "season1": # Season 1: the middle of the pack's command centre (round floor, walkway from the door, terminal with its # display, the curved rail), turned so the operator at the terminal looks at the bow P1(s1.command_centre(), cmd, frame(x_cc, 0.0, Z3 - PLATE, 180.0), S1_GAIN[s1.CC_MAP]) helm = (x_cc - 0.45, 0.0, Z3) else: for i in range(2): # a white walkway from the door to the helm dais for j in range(3): m = Matrix.Translation((x_br + 0.9 + 3.6 * (i + 1), -3.6 + 2.4 * j, Z3)) put(kit, plate, cmd.mesh, root, m, cmd.name) # the helm dais: the hall's raised end pad (plates, rails, the five steps) set on the bridge deck under the canopy DAIS_X = 46.4 # its forward edge dais = Unit([it for it in hall.items if it.kind == "mesh" and it.center.x > -7.6 and it.name in ("SM_Floor_B", "SM_Floor_B2", "SM_Floor_B3", "SM_Rail_Pad", "SM_Stairs_A")]) P(dais, cmd, frame(DAIS_X + DOOR_GAP_H, 0.0, Z3 + 0.9, 0.0)) helm = (45.4, 0.0, Z3 + 0.9) if FURNISH: # two plotting stations in the CIC, one each side, staggered x_mid = X_FORE + DOOR_GAP + N_CIC * BAY / 2 P1(s1.furniture_row("desks"), cmd, frame(x_mid - 0.5, ROW_BACK, Z3 - 0.9, 180.0)) P1(s1.furniture_row("study"), cmd, frame(x_mid + 0.5, -ROW_BACK, Z3 - 0.9, 0.0)) for i, (x, y) in enumerate(((36.0, 0.0), (42.0, -3.0), (42.0, 3.0), (48.0, 0.0))): lamp(cmd.lights, root, "Bridge lamp %d" % (i + 1), (x, y, Z3 + 3.0), 500.0, color=(0.56, 0.78, 1.0)) # ============================================================================================ 6. STAIRS st = get_collection("10.6 | Stairs", top) e, kids, s = stairs_kit.kit_stair("Stairs | sub bay to command deck", st, root, gx0, Z3, Z2, +1, STAIR_Y) log("stair scale %.4f, foot at x %.2f" % (s, gx0 - s * stairs_kit.RUN)) # ============================================================================================ 7. CAROUSEL car_zone = get_collection("10.7 | Tender carousel", top) with bpy.data.libraries.load(DOCKARM, link=False) as (src, dst): dst.collections = [c for c in src.collections if c == "DockArm_Assembly"] asm = dst.collections[0] asm.name = "10.7 | Tender carousel | docking arm (models/docking-arm)" car_zone.children.link(asm) arm_root = next(o for o in asm.objects if o.name.startswith("DockArm_Root")) arm_root.parent = root arm_root.location = HUB arms = sorted([o for o in asm.objects if o.type == "EMPTY" and o.name.startswith("Arm")], key=lambda o: o.name) lower = [5.3, 0.0, 0.0] # arm 1 rests at BOARD over the launch slot, the other two stowed for a, lo in zip(arms, lower): a["lower_m"] = lo a["claw_open"] = 0.0 paint = bpy.data.materials.get("M_DockArm_Paint") steel = bpy.data.materials.get("M_DockArm_Steel") graphite = bpy.data.materials.get("M_DockArm_Graphite") # the bay's roof is 1.76 m above where the arm's mount flange has to be: a pylon carries it roof = fit.ceil_z(HUB[0], 0.0, HUB[2] + 1.3, LINING) or (HUB[2] + 2.96) z_fl = 1.20 top_z = roof - HUB[2] + 0.12 prof = [(1.05, z_fl), (1.05, z_fl + 0.10), (0.80, z_fl + 0.16), (0.80, top_z - 0.34), (1.20, top_z - 0.26), (1.20, top_z)] pyl = shipfit.turned("Carousel pylon (bay roof to mount flange)", car_zone, arm_root, prof, segments=32, materials=[m for m in (paint, graphite, steel) if m], mat_of=lambda i: (2 if i in (0, 4) else 1 if i == 1 else 0)) # the Tenders (the Kazah), linked for the picture only tenders = get_collection("10.7 | Tender carousel | Tenders (linked preview, not exported)", car_zone) kz = None if os.path.isfile(KAZAH) and os.path.isfile(KAZAH_LIST): with open(KAZAH_LIST, "r", encoding="utf-8") as fh: names = set(json.load(fh)) with bpy.data.libraries.load(KAZAH, link=True, relative=True) as (src, dst): dst.objects = [n for n in src.objects if n in names] kz = bpy.data.collections.new("12 CONTEXT | Tender (Kazah, linked)") for o in dst.objects: if o is not None: kz.objects.link(o) for k in range(3): th = math.radians(-90.0 - 120.0 * k) t = bpy.data.objects.new("_Tender%d" % (k + 1), None) t.instance_type = "COLLECTION" t.instance_collection = kz t.empty_display_size = 0.3 tenders.objects.link(t) t.parent = arm_root t.rotation_euler = (0, 0, math.radians(90.0 - 120.0 * k)) # Kazah file: nose -Y -> nose tangential t.location = (6.0 * math.cos(th), 6.0 * math.sin(th), -1.0 - lower[k]) t["preview_only"] = True # ============================================================================================ 8. MARKERS mk = get_collection("10.8 | Markers (game hooks)", top) def hall_pt(dx, dy, dz=0.0): return (X_AFT + dx, dy, Z2 + dz) for name, loc, role in ( ("Boarding gate (port, Anchorage bridge)", hall_pt(-10.4, 7.6), "boarding_gate"), ("Service gate (starboard)", hall_pt(-10.4, -7.6), "service_gate"), ("Packer", hall_pt(-8.6, -3.0, -0.9), "packer"), ("Storage wall", hall_pt(-12.2, -3.0, -0.9), "storage_wall"), ("Refinery", hall_pt(-8.6, 3.0, -0.9), "refinery"), ("Fabricator", hall_pt(-12.2, 3.0, -0.9), "fabricator"), ("Tethys terminal", hall_pt(-3.2, 2.9), "tethys_terminal"), ("Reactor", (x_hall_w - 0.35 - 1.5 * BAY, 0.0, Z2), "reactor"), ("Engine control", (x_ec - 2.15, 0.0, Z2), "engine_control"), ("Carousel console", (-6.5, 2.2, Z2), "carousel_console"), ("Pool ladder", (3.05, -0.45, Z2), "pool_ladder"), ("Ore pipe riser", (-9.0, -7.0, Z2), "ore_pipe_riser"), ("Launch slot (Tender at BOARD)", (HUB[0], HUB[1] - 6.0, HUB[2] - 6.3), "launch_slot"), ("Sonar table", (X_FORE + DOOR_GAP + 2 * BAY, 0.0, Z3 - 0.9), "sonar_table"), ("Chart table", (36.5, 4.2, Z3), "chart_table"), ("Helm", helm, "helm"), ("Sensor module", (X_FORE + L(N_SPINE) + 2.15, 0.0, Z2), "sensor_module"), ): marker(mk, root, name, loc, role) for name, (f, n, s) in room_frames.items(): xc = f.translation.x + DOOR_GAP + n * BAY / 2 yc = f.translation.y if name.startswith("Quarters"): for i, dx in enumerate((-1.8, 1.8)): marker(mk, root, "%s bed %d" % (name, i + 1), (xc + dx, yc + s * 1.2, Z2 - 0.9), "bed") elif name == "Galley": marker(mk, root, "Nutrient vat", (xc - 3.6, yc + s * 1.2, Z2 - 0.9), "nutrient_vat") for i in range(3): marker(mk, root, "Nutrient port %d" % (i + 1), (xc + 3.6 * (i - 1), yc + s * 2.0, Z2 + 0.55), "nutrient_port") else: marker(mk, root, "Habitation (couch, table, shelf)", (xc, yc, Z2 - 0.9), "habitation") # ============================================================================================ 9. KIT, one of each kc = get_collection(KIT_COLL, scene.collection) x = 0.0 row = 0 for i, path in enumerate(kit.all_mesh_paths()): me = kit.mesh(path) if me is None: continue ob = bpy.data.objects.new("KIT | " + me.name, me) kc.objects.link(ob) ob["ue_mesh"] = path ob.location = (x, 200.0 + row * 14.0, 0.0) x += max(2.0, ob.dimensions.x + 1.5) if x > 150.0: x, row = 0.0, row + 1 exclude_collection(KIT_COLL, True) kc1 = get_collection(KIT1_COLL, scene.collection) x = 0.0 row = 0 for path in k1.all_mesh_paths(): me = k1.mesh(path) if me is None: continue ob = bpy.data.objects.new("KIT | " + me.name, me) kc1.objects.link(ob) ob["ue_mesh"] = path ob.location = (x, 330.0 + row * 30.0, 0.0) x += max(2.0, ob.dimensions.x + 1.5) if x > 150.0: x, row = 0.0, row + 1 exclude_collection(KIT1_COLL, True) # ---- file hygiene --------------------------------------------------------------------------------------------- for img in bpy.data.images: if img.get("ue_texture") and img.filepath and not img.filepath.startswith("//"): img.filepath = bpy.path.relpath(img.filepath) bpy.context.view_layer.update() kit_objs = [o for o in top.all_objects if o.type == "MESH" and o.get("ue_mesh")] own = [o for o in top.all_objects if o.type == "MESH" and not o.get("ue_mesh") and not o.data.name.startswith(("SM_wlombbzdw", "SM_DockArm", "DECAL_"))] summary = { "placed": stats, "kit_instances": len(kit_objs), "kit_meshes_used": len(set(o.data.name for o in kit_objs)), "season1_instances": sum(1 for o in kit_objs if str(o.get("ue_mesh", "")).startswith(s1.S1_GAME)), "season1_meshes_used": len(set(o.data.name for o in kit_objs if str(o.get("ue_mesh", "")).startswith(s1.S1_GAME))), "kit_triangles": sum(len(o.data.polygons) for o in kit_objs), "lights": sum(1 for o in top.all_objects if o.type == "LIGHT"), "markers": sum(1 for o in top.all_objects if o.name.startswith("MARKER | ")), "own_meshes": [o.name for o in own], "own_audit": shipfit.audit(own), "tenders_linked": kz is not None, } log("SUMMARY " + json.dumps(summary)) with open(os.path.join(HERE, "build_report.json"), "w", encoding="utf-8") as fh: json.dump(summary, fh, indent=1) if "--no-save" not in ARGS: bpy.context.preferences.filepaths.save_version = 0 # no .blend1 next to the fork from a rebuild bpy.ops.wm.save_mainfile() log("saved " + bpy.data.filepath) main()