""" 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 cic as corridor_unit # noqa: E402 import s1 # noqa: E402 import shipfit # noqa: E402 import bridge_shell # noqa: E402 import captains_cabin # noqa: E402 import captains_look # noqa: E402 import captains_suite # noqa: E402 import carousel_rigged # noqa: E402 import corridor_level # noqa: E402 import fore_deck # noqa: E402 import lower_deck # noqa: E402 import boarding_gate # noqa: E402 import door_arms # noqa: E402 import wall_props # noqa: E402 import quarters # noqa: E402 import reactor_room # 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_Rigged.blend") # the rigged model (the user, 2026-10-13) 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 LAUNCH_REACH = 2.34 # the launch arm's head moves this far in toward the hub as it is let down to BOARD, so the # Tender hangs on the centre line of the deck well (straight down it hung 0.88 m over the # deck); the user, 2026-10-13: "put the tender1 more center to the moon pool", "Its still not centered?" WRIST_RANGE = 100.0 # degrees; the model's rig stops the wrists at 92, the reach above needs 98.8 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 # the control room corridor (the former CIC), as the user marked it up on 2026-10-12: see cic.py CORRIDOR_SWAP = "gold" # "gold": only the gold foil panels became the ticked wall; "all": the silver padded ones too # 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) def pod_skirt(fit): """Since 2026-10-12 the roof pod is trimmed to the hull's skin for real (build/roof_trim.py; its mesh carries the property "trim"): then there is nothing to hide, and a mask left by an earlier build comes off. A pod that is not trimmed still gets the mask. That hides the part inside the dock room but leaves a stepped edge with spikes, which the user rejected: run roof_trim.py with --apply on such a file.""" pod = bpy.data.objects.get("M_SHIP_Dorsal_Roof") if pod is not None and pod.data.get("trim"): 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]) return "trimmed to the hull's skin (roof_trim.py), no mask" return "NOT TRIMMED: %d vertices hidden with a mask inside the dock room; run build/roof_trim.py --apply" % mask_pod_skirt(fit) 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 ROOM (the red-lit pipe gallery widened to 14 m: two reactor vessels beside the glass floor) - 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 - CONTROL ROOM CORRIDOR (the former CIC: a clean corridor, its white floor level with the door sills since 2026-10-13, no stairs, no tables, no rails; two side doors facing each other in its third bay from aft) - Bridge (deck under the canopy with the Season 1 command floor and its terminal). Beside the corridor, through its two side doors (since 2026-10-13): CREW QUARTERS to port (six berths with lockers under the hull's slope, a storage wall, a mess bench): 14.4 x 4.9 m with a folded lining that follows the hull (knee wall 0.9 m, three folds, ceiling strip 3.46 m up along the corridor's wall). Lining and berths are made for this hull (meshes "Quarters | ..."); floor panels, furniture and fittings are the Props pack's (build/quarters.py; quarters.remove() takes both rooms out). CAPTAIN'S QUARTERS to starboard, the whole room (x 15.86..30.21, 14.35 m): three bays in the look of the user's reference picture (dark panels, padded walls, light strips). MIDDLE: the cabin of models/captains-quarters (another session's model: bunk alcove with a padded back wall, work station, bench with the chart), set in by build/captains_cabin.py as instances of the meshes of the scene "Captains Quarters" in this file, turned half a turn about Z, origin at (23.06, -5.303, 2.94). Its window wall stands 3.55 m further forward (at the bridge end), its locker wall with the three doors 3.6 m further aft. FORWARD (lounge) and AFT (mess): made in the ship by build/captains_suite.py with the model's own library (meshes SM_CQS_*). They are deeper than the cabin: floor 4.4 m from the corridor's wall, a knee wall 1.6 m high with a padded bench and a light cove, a canted panel up to the ceiling. Lounge: low table, chair, sideboard under a lit shelf. Mess: table, two chairs, sideboard under a status display. ENTRANCE: the corridor's own side door "Door_A_BP4.230" (the user: "the entrance should be via the "Door_A_BP4.230" build according to that"). The model's sliding door and its door wall are taken out; the wall is drawn again (SM_CQS_Wall_Door) with a doorway made to that door, 1.64 m wide. Inside the door a landing at the corridor's level with one lit step (0.20 m) down to the cabin's floor (SM_CQS_Entry); over the doorway, facing the corridor, a panel with the cabin's number closes what the door's opening has above the cabin's ceiling. The floor is 0.20 m below the door sills because of the hull (the bunk alcove's top corner is 9 cm inside the skin). 13 lamps, markers "Captain's bed", "Captain's desk", "Captain's mess table", "Captain's lounge". captains_suite.remove() goes back to the cabin alone; quarters.CAPTAIN = "lined" builds the first, white room. SURFACES (the user: "do it more realistic with good textures"): the room's materials M_CQ_* carry texture sets instead of flat colours (build/captains_look.py). Panels, metal and floor: the packs' own ScratchedMetal, BrushedMetal, StainedSteel and FloorPanel_A sets with a smudge mask, on the meshes' UVs (metres). Padded walls: scanned leather; cushions, mattress, blanket, pillow: scanned cloth (Poly Haven, CC0, in models/vessel-interior/source_textures), laid on by position. The colours are still the black cabin's. The flat versions stay in the file as M_CQ_*__flat; captains_look.revert() puts them back. Look at the room in Rendered view with Cycles: reflections and light bounces are what show the surfaces. Collections "10.5 | Command deck | Crew quarters" and "10.5 | Command deck | Captain's quarters" (each with "| lights"). 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; the hidden strip ends at x 30.86 inside the bridge's aft bulkhead, on an edge cut for it, so the bridge's ceiling is whole. Hull mesh from before: '... (before the ceiling slot fix)') Nine of the hull's trim curves (the transverse seam at x 32.2, the two cowl belt seams, the four longitudinal deck seams, two grab slots) carry the modifier 'Seat on the skin (nothing of it inside the hull)': their inner half stood 1 to 4 cm inside the hull and showed as dark lines in the bridge. Remove the modifier to undo. The roof pod (M_SHIP_Dorsal_Roof) is cut off 5 mm under the hull's skin (build/roof_trim.py): nothing of it hangs inside the ship. Its mesh from before is kept as 'Dorsal roof | regular rounded loft.006'; assign it to undo. The bridge's aft bulkhead closes the section of hull and pod and stays 2 cm inside their skin; its mesh from before is kept as 'Bulkhead | bridge aft (before the fit fix)'. The carousel hub is on the centre line: there it turns clear of the hull. The launch arm's head is 2.3 m outboard of the well's middle when it hangs straight down; it reaches in 2.34 m instead (build/carousel_rigged.py; see the README). The control room corridor's level floor and the two quarters beside it come from build/corridor_level.py and build/quarters.py (2026-10-13, on the user's marked-up plan and picture). The reactor gallery was widened into a room on 2026-10-13 (build/reactor_room.py), at the user's word on the marked-up plan: "extend this corridor and make a room instead of a corridor". 14.0 m between the walls (the hull's skin at floor level allows no more at the aft end). The walls are the gallery's own wall pieces, as cut copies (SM_RR_Wall_*: their base stood 0.9 m below the floor, where the hull and the lower engine pods are out here); they stand on a machinery base 1.6 m deep and 0.30 m high (SM_RR_Dais), under which the pods' crowns end. The gutters are closed. What the corridor was is written down in the text block "REACTOR ROOM | what the corridor was"; reactor_room.remove() puts it back from that. The fore main deck was raised the same evening (build/fore_deck.py), at the user's word on a side view and the plan: "this whole section should be moved up and stick to the [command deck] so that I can utilize the space underneath better ... sensor room needs to be extended". The fore passage and its four rooms stand 1.40 m higher (sill z -1.96), their roof against the control room corridor's wall pieces; the passage is five bays long; forward of its door the SENSOR ROOM (two cars joined, 9.6 x 7.2 m) and to starboard of that the LIFT ROOM (empty: the lift is to come). The door from the sub bay is 1.40 m above the bay's deck, with a landing and a flight of steps to each side. Under the raised deck the hull is free for a third floor. fore_deck.remove() puts it all back. The room under the raised deck was made on 2026-10-14 (build/lower_deck.py): "create the room underneath". A hall 10.8 x 20.8 m inside, 3.9 m high, floor at z -7.95, its roof 2 cm under the raised deck; no way in until the lift the user means to add (its last bay lies under the lift room). lower_deck.remove() takes it out. """ 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"] old_root = bpy.data.objects.get(ROOT_NAME) if old_root is not None and old_root.get("hand_edited") and "--overwrite-hand-edits" not in ARGS: raise SystemExit("This file has been changed by hand since the last build (%s). A rebuild would undo that.\n" "Change it in place, or run with -- --overwrite-hand-edits if that is really wanted." % old_root["hand_edited"]) 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: " + 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 = corridor_unit.N_BAYS P(eng.conn, cmd, frame(X_FORE, 0.0, Z3, 0.0)) # the control room corridor: a clean gold car with two side doors facing each other and the ticked wall (cic.py) P(corridor_unit.corridor(eng, CORRIDOR_SWAP), cmd, frame(X_FORE, 0.0, Z3, 0.0)) for fr in corridor_unit.door_frames(X_FORE, Z3): P(eng.conn, cmd, fr) 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 whole section round the corridor's door, the hull and the roof pod above # it taken as one outer skin. Its edge stays 2 cm inside that skin at both faces, with row lines where the shape # needs them. (bulkhead_x with its rows 1.2 m apart cut straight across the hollow corner between hull and pod # and stood 20 cm outside the ship: the user's "is clipping outside needs fixing", 2026-10-12.) shipfit.section_wall("Bulkhead | bridge aft", cmd.mesh, root, shipfit.Envelope(), x_br + 0.4, x_br + 0.1, Z3 - PLATE - 0.25, openings=[(-1.8, 1.8, Z3, Z3 + 3.9)], material=mat_lining) # the bridge's own shell seen from inside (bridge_shell.py; the user: "fix the bridge ceiling slot and seam lines # too", 2026-10-12). The strip of hull that the mask hides under the pod ends inside this bulkhead's thickness, on # an edge cut for it, so that the bridge's ceiling is whole; and the hull's trim curves that stood through the # skin into the bridge are laid on it. Both do nothing on a file where it has been done. ship_hull = bpy.data.objects.get("M_SHIP_Hull") if ship_hull is not None and ship_hull.modifiers.get("LAYOUT mask pod opening"): log("bridge ceiling slot: " + json.dumps(bridge_shell.end_hull_opening_at(ship_hull, x_br + 0.25))) if ship_hull is not None: log("bridge trims: " + json.dumps(bridge_shell.fix_bridge_trims(ship_hull))) 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) 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) # the rigged carousel: the armature DockArm_Rig with its skinned parts, under an empty at the hub that also carries # the pylon and the preview Tenders (carousel_rigged.py describes it; until 2026-10-13 the unrigged assembly # DockArm_Carousel.blend stood here, posed through the property lower_m on its Arm empties) mats_before = set(m.name for m in bpy.data.materials) imgs_before = set(i.name for i in bpy.data.images) with bpy.data.libraries.load(DOCKARM, link=False) as (src, dst): dst.collections = [c for c in src.collections if c in ("DockArm_Rigged", "DockArm_Widgets")] asm = next(c for c in dst.collections if c is not None and c.name.startswith("DockArm_Rigged")) wgt = next((c for c in dst.collections if c is not None and c.name.startswith("DockArm_Widgets")), None) carousel_rigged._drop_duplicates(bpy.data.materials, mats_before) carousel_rigged._drop_duplicates(bpy.data.images, imgs_before) asm.name = carousel_rigged.NEW_COLL car_zone.children.link(asm) if wgt is not None: wgt.name = carousel_rigged.WGT_COLL car_zone.children.link(wgt) exclude_collection(carousel_rigged.WGT_COLL, True) arm_root = bpy.data.objects.new("DockArm_Root", None) arm_root.empty_display_type = "PLAIN_AXES" asm.objects.link(arm_root) arm_root.parent = root arm_root.location = HUB arm_root["carousel"] = "rigged: DockArm_Rig (Pose Mode handles CTRL_ArmN_Hoist 0..5.3 m, CTRL_ArmN_Claws)" rig = next(o for o in asm.objects if o.type == "ARMATURE") rig.parent = arm_root rig.location = (0.0, 0.0, 0.0) lower = [5.3, 0.0, 0.0] # arm 1 rests at BOARD over the launch slot, the other two stowed for k, lo in enumerate(lower): rig.pose.bones["CTRL_Arm%d_Hoist" % (k + 1)].location = (0.0, lo, 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 # the launch arm reaches in, and the preview Tenders hang on the head bones so that they go with the arms # (carousel_rigged.py: the Tenders are hung first, where the plain zigzag holds them, then the reach is set) bpy.context.view_layer.update() carousel_rigged.add_reach(rig) carousel_rigged.hang_tenders(rig) carousel_rigged.set_wrist_range(rig, WRIST_RANGE) carousel_rigged.set_reach(rig, 1, LAUNCH_REACH) launch_obj = bpy.data.objects.get("_Tender1") launch_at = tuple(launch_obj.matrix_world.translation) if launch_obj is not None else (HUB[0], HUB[1] - 6.0 + LAUNCH_REACH, HUB[2] - 6.3) # ============================================================================================ 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)", launch_at, "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) # ============================================================== 10. THE CORRIDOR'S LEVEL FLOOR, THE TWO QUARTERS # The user, 2026-10-13, on a marked-up plan and a marked-up picture of the corridor: "I want rooms here create me # rooms for Crew quarters and captains quarters, also corridor needs to be level, delete stairs and level the # floor". Both were done in the open file with these two modules. They stand here, after the kit sections, # because the rooms place Props pack meshes by name and the corridor is first built as the pack's sunken car. log("corridor floor: " + json.dumps(corridor_level.apply())) log("quarters: " + json.dumps(quarters.build())) # the captain's quarters are the cabin another session modelled from the user's reference picture # (models/captains-quarters); the user, the same afternoon: "Port Captains quarters design from my other session # into the room make it fit. delete anything inside, you can lower down floor ... to make it fit" if quarters.CAPTAIN == "cabin": log("captain's cabin: " + json.dumps(captains_cabin.port())) # ... and the user on seeing it in half of the room: "what is this? this is a small room. you said it wont fit # thats why I said try to fit. make it use all the space previously spared just generate if necessary" log("captain's quarters, the whole room: " + json.dumps(captains_suite.extend())) # ... and on its flat black surfaces: "do it more realistic with good textures" if not captains_look.applied(): log("captain's quarters, texture sets: " + json.dumps(captains_look.apply())) # The user, 2026-10-13, on the main-deck plan with a rectangle marked to port and to starboard of the reactor # gallery: "extend this corridor and make a room instead of a corridor". It is first built as the pack's car # (section 3) and widened here, the way the open file was changed. reactor_room.forget() # (a record in the file is of the room this rebuild has just thrown away) log("reactor room: " + json.dumps(reactor_room.apply())) # The user, the same evening, on a side view and the plan of the fore main deck: "this whole section should be moved # up and stick to the [command deck] so that I can utilize the space underneath better ... sensor room needs to be # extended" (with a lift room drawn beside it). Section 4 builds the deck as it was; this changes it the way the # open file was changed. fore_deck.forget() log("fore deck: " + json.dumps(fore_deck.apply(kit=kit, eng=eng))) # ... and the next day: "create the room underneath" (the third floor under the raised deck) lower_deck.forget() log("lower deck: " + json.dumps(lower_deck.apply(kit=kit, eng=eng))) # The user, 2026-10-15, on a picture of the kit's lattice gate in the works hall's port wall: "replace this part of # the vessel change it with [models/pressure-door] ... open a hole on the main hull of the vessel all the way so it # is going to be a pressure door". The door is appended from its own file and the hull is cut, as in the open file. boarding_gate.forget() log("boarding gate: " + json.dumps(boarding_gate.apply())) # The user, 2026-10-16, with a hinge sheet (MFH 100/200) and the doors in their pockets with an arm drawn from each # pocket's rail: "Create a door arm that looks like in the referance ... rig it accordingly and mechanically". if door_arms.exists(): door_arms.remove() log("door arms: " + json.dumps(door_arms.apply(), default=str)[:2000]) # The user, 2026-10-19: the pack's Storage Area Network boxes replaced by wall props made from their two kits wall_props.forget() log("wall props: " + json.dumps(wall_props.apply(), default=str)[:2000]) # ---- 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", "SK_DockArm", "WGT_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()