""" Builds the vessel's interior from the Modular SciFi Season 2 Starter Bundle 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 # noqa: E402 from hall import BAY_H, DOOR_GAP_H, LENGTH_3, build_hall # 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 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)" 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)) 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 | ", "M_DockArm", "SI_", "EN_", "EP_")): continue if blocks is bpy.data.collections and not b.name.startswith(("10", "11", "12 CONTEXT", "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 (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) - 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, helm dais). 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 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) 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 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,)) for i, (x, y, z) in enumerate(((-6.0, -5.5, 4.6), (-6.0, 5.5, 4.6), (12.0, -5.5, 2.4), (11.0, 3.5, 6.2), (3.05, -7.6, 0.2), (3.05, 6.5, 0.8))): lamp(bay.lights, root, "Sub bay lamp %d" % (i + 1), (x, y, z), 900.0) # ============================================================================================ 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) P(crew(eng.car(finish, n, door_a=False, door_b=False, side_doors=[(d - b0, -s)], props="tidy")), fore, f) P(eng.conn, fore, frame(X_FORE + DOOR_GAP + (d + 0.5) * BAY, s * CAR_HALF, Z2, 90.0 * s)) # ============================================================================================ 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)) P(crew(eng.car("gold", N_CIC, props="tidy")), 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)) 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) 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)) 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, 0.0, Z3), "chart_table"), ("Helm", (45.4, 0.0, Z3 + 0.9), "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) # ---- 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)), "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.ops.wm.save_mainfile() log("saved " + bpy.data.filepath) main()