""" The reactor room. The reactor gallery was a corridor: three bays of the Engineer pack's "hallway B" (a glass floor 3.6 m wide with a gutter on each side, padded walls 4.4 m apart), between engine control aft and the works hall forward. The user, with the main-deck plan marked up (a rectangle to port and one to starboard of the corridor, arrows outwards): "extend this corridor and make a room instead of a corridor". apply() does that to the file as it is. It works on the pieces it finds in zone 10.3 (by mesh and place, not by name), so it is the same for the open, hand-edited file and for one built again by build_interior.py. width the two walls go 4.82 m out, to 7.00 m from the centre line: 14.0 m between them. That is what the hull allows at floor level at the aft end (its skin is 7.40 m out there, the wall pieces are 0.42 m thick). walls the pack's own wall pieces. Their lower 1.2 m is a base that stood in the gutter, 0.9 m below the floor; out here the hull and the two lower engine pods are in the way of it, so the pieces are cut copies (SM_RR_Wall_*): cut 0.30 m above the floor, in the plain plating of the base, 9 cm of which stays as a skirting. Four more of them, with two pilasters each, close the ends beside the two doors. dais along each wall a machinery base 1.6 m deep and 0.30 m high (SM_RR_Dais), with the edge profile and the materials of the pack's platform (EN_SM_Intersection_Floor). The walls stand on it. It is there because the engine pods' crowns rise up to 16 cm above floor level in the two aft outer corners: they end under it, 14 cm below its top. floor the glass floor in the middle stays. On each side one more floor module per bay: in the end bays the glass floor tile's own top 6 cm (SM_RR_Floor_Frame, a cut copy: the tile itself is 1.2 m deep, which the pods do not allow) with the pack's perforated panel in it; in the middle bay the pack's platform with a reactor vessel on it. The gutters are closed; what stood in them (strips, boxes, pipe stubs) is gone. ceiling two more rows of the pack's ceiling panel. Nothing of the ship model is touched (hull, pods). Everything that was removed or moved is written down in the text block RECORD inside the .blend, and remove() puts the corridor back from it. """ import json import math import os import sys import bmesh import bpy from mathutils import Matrix, Vector from mathutils.bvhtree import BVHTree sys.dont_write_bytecode = True HERE = os.path.dirname(os.path.abspath(__file__)) if HERE not in sys.path: sys.path.insert(0, HERE) import quarters # noqa: E402 TAG = "reactor_room" ZONE = "10.3 | Reactor gallery and engine control" RECORD = "REACTOR ROOM | what the corridor was" MADE_FOR = "M-Ship v2 reactor room (build/reactor_room.py)" PRE = "SM_RR_" # meshes made here: cut copies of pack pieces, and the few parts drawn here NAME = "Reactor room | " Z2 = -3.36 # the main deck's sill level HALF = 7.00 # half the room's width: the wall pieces' inner face SHIFT = 4.82 # how far each wall goes out (its inner face stood 2.18 m from the centre line) PLAT_H = 0.302 # height of the pack's platform, and so of the dais MODULE = 3.60 # a floor module of the pack JOINT = 0.002 # between a module and its neighbour (the glass floor, the dais) Y_IN = 1.80 + JOINT # where the new floor modules begin: the glass floor's edge Y_OUT = Y_IN + MODULE DAIS_Y = Y_OUT + JOINT # the dais' front FLOOR_KEEP = 0.06 # what is kept of a glass floor tile for a floor module: its top 6 cm SILL = 0.012 # the plate under a door-side brace and pilaster is this thick END_PANEL = 2.8025 # the end walls: brace 1.8-2.2 (the pack's), pilaster, wall piece from here 3.6 m, pilaster to 7.0 END_PROUD = 0.08 # the end walls' wall pieces stand this far into the room (the works hall's wall is behind the fore ones) PIER = 0.60 COURSE = 0.05 # the base course under an end wall stands this far out of the wall piece's face CELLS_OFF = 0.10 # the power cell banks stand this far off the wall (the padded panel on one wall is 8 cm proud) OFF = 0.001 # what stands on a surface keeps this far off it EPS = 0.0005 # two faces cross only if each reaches further than this through the other's plane WALLS = ("EN_SM_Wall_HB_A", "EN_SM_Wall_HB_A2", "EN_SM_Wall_HB_A3") END_WALL = "EN_SM_Wall_HB_A3" SHINY, DARK, RIM, BLACK = "EN_MI_Trim_A_Shiny2", "EN_MI_Trim_A_DarkGray", "EN_MI_Trim_Gray_Drt", "EN_MI_Trim_A_Black" # the platform's edge, measured on EN_SM_Intersection_Floor: (depth back, height) pairs and, per band, where it lies # on the pack's trim sheet (V from, V to, U at the start, U per metre); None = the sheet's plain patch PROFILE = [(0.0, 0.0), (0.0, 0.077), (0.044, 0.091), (0.044, 0.204), (0.0, 0.207), (0.0, 0.269), (0.049, PLAT_H)] BANDS = [(SHINY, (0.2233, 0.2545, 0.0446, 0.4065)), (DARK, None), (DARK, (0.0491, 0.0759, 0.1191, 0.2367)), (DARK, None), (SHINY, (0.2040, 0.2182, 0.5718, 0.2286)), (DARK, None)] PLAIN = (0.7514, 0.8020, 0.7741, 0.8206) # the plain patch of EN_MI_Trim_A_DarkGray (the platform's top) RIM_UV = (0.3864, 0.0743, 0.8253, 0.0902) # the glass floor's rim on EN_MI_Trim_Gray_Drt STRIP_UV = (0.2719, 0.4080, 0.7281, 0.4502) # the gutter's bottom strip on EN_MI_Trim_A_Black TUBE = "EP_SM_Light_B" # the pack's caged tube light, under the ceiling over each new floor module TUBE_Y = 4.80 # (the middle of the new ceiling panels) TUBE_WATTS = 220.0 TUBE_COLOR = (1.0, 0.80, 0.60) CEILING = 0.52 # the ceiling panels' underside (ship z) # ---- the corridor as it stands ------------------------------------------------------------------------------------- def _zone(): return [o for o in bpy.data.objects if str(o.get("zone", "")).startswith(ZONE)] def _kind(o): return o.data.name if o.type == "MESH" and o.data is not None else "" def _bb(o): if o.type != "MESH": t = o.matrix_world.translation return t.copy(), t.copy() mw = o.matrix_world cs = [mw @ v.co for v in o.data.vertices] if len(o.data.vertices) < 64 else [mw @ Vector(c) for c in o.bound_box] return (Vector((min(c.x for c in cs), min(c.y for c in cs), min(c.z for c in cs))), Vector((max(c.x for c in cs), max(c.y for c in cs), max(c.z for c in cs)))) def _mid(o): lo, hi = _bb(o) return (lo + hi) * 0.5 class Car: """The gallery's three bays: where they are and what belongs to them.""" def __init__(self): objs = _zone() ceil = sorted([o for o in objs if _kind(o) == "EN_SM_Ceiling_HB_A" and abs(_mid(o).y) < 0.2], key=lambda o: _mid(o).x) if not ceil: raise RuntimeError("no gallery in this file (zone %r has no ceiling panel on the centre line)" % ZONE) self.ceilings = ceil self.x0 = min(_bb(o)[0].x for o in ceil) self.x1 = max(_bb(o)[1].x for o in ceil) self.tiles = sorted([o for o in objs if _kind(o) == "EN_SM_GlassFloor_A" and self.x0 - 0.1 < _mid(o).x < self.x1 + 0.1 and abs(_mid(o).y) < 0.2], key=lambda o: _mid(o).x) self.bays = [(_bb(o)[0].x, _bb(o)[1].x) for o in self.tiles] self.objs = [o for o in objs if self.x0 - 0.35 <= _mid(o).x <= self.x1 + 0.35] self.coll = bpy.data.collections[ZONE] self.lights = bpy.data.collections.get(ZONE + " | lights") or self.coll self.root = bpy.data.objects.get(quarters.ROOT) self.length = self.x1 - self.x0 def plan(self): """What becomes of the corridor's pieces: lists by what is done with them.""" p = {k: [] for k in ("walls", "braces", "wire", "moved", "gone")} for o in self.objs: k, c = _kind(o), _mid(o) ay = abs(c.y) if k in WALLS: p["walls"].append(o) elif k == "EN_SM_Doorway_SideBrace": p["braces"].append(o) elif k == "EP_SM_Wire_Wall_B" and ay > 1.9: p["wire"].append(o) elif k == "EN_SM_GlassFloor_Side_A" or k == "EN_SM_Wall_HB_Edge": p["gone"].append(o) # the gutters' bottoms; the trims on the walls' open ends elif k == "EN_SM_ElectricDistribBox" and ay > 1.9 and c.z < Z2 + 0.4: p["gone"].append(o) # the boxes on the walls' bases, in the gutters elif k in ("EN_SM_Wall_Pipe_A", "EP_SM_CoatedPipe_A") and ay > 1.8: p["gone"].append(o) # the pipe stubs from the bases into the gutters elif k.startswith("DECAL_") and ay > 2.1: (p["gone"] if _bb(o)[0].z < Z2 + PLAT_H + 0.10 else p["moved"]).append(o) # labels on the bases elif k in ("SI_SM_Wall_Puffy_A", "EN_SM_Light_A") and ay > 2.0: p["moved"].append(o) elif o.type == "LIGHT" and o.data.type == "SPOT" and ay > 1.9: p["moved"].append(o) return p # ---- meshes made here ---------------------------------------------------------------------------------------------- def _forget(name): quarters._bounds_cache.pop(name, None) me = bpy.data.meshes.get(name) if me is not None: bpy.data.meshes.remove(me) def _slot(me, material): for i, m in enumerate(me.materials): if m is not None and m.name == material: return i me.materials.append(bpy.data.materials[material]) return len(me.materials) - 1 def cut_copy(kit_mesh, name, z_local, note): """A copy of a pack mesh with everything below local z cut away. It keeps the pack's materials, UVs and normals and is no longer an instance of the pack's asset (no "ue_mesh": it travels with the ship as its own part).""" src = bpy.data.meshes.get(kit_mesh) if src is None: raise RuntimeError("kit mesh not in this file: " + kit_mesh) _forget(name) me = src.copy() me.name = name me.use_fake_user = False bm = bmesh.new() bm.from_mesh(me) bmesh.ops.bisect_plane(bm, geom=bm.verts[:] + bm.edges[:] + bm.faces[:], dist=1e-5, plane_co=(0.0, 0.0, z_local), plane_no=(0.0, 0.0, 1.0), clear_inner=True, clear_outer=False) bm.to_mesh(me) bm.free() me.update() if "ue_mesh" in me: me["cut_from"] = me["ue_mesh"] del me["ue_mesh"] me["cut_below_local_z"] = round(z_local, 4) me["made_for"] = MADE_FOR me["note"] = note return me class Own: """A small mesh drawn here, on the pack's own materials and trim sheets.""" def __init__(self, materials): self.bm = bmesh.new() self.uv = self.bm.loops.layers.uv.verify() self.names = list(materials) def face(self, pts, material, uvs, toward): pts = [Vector(p) for p in pts] n = (pts[1] - pts[0]).cross(pts[2] - pts[0]) if n.dot(Vector(toward)) < 0.0: pts, uvs = pts[::-1], list(uvs)[::-1] f = self.bm.faces.new([self.bm.verts.new(p) for p in pts]) f.material_index = self.names.index(material) f.smooth = False for loop, u in zip(f.loops, uvs): loop[self.uv].uv = u return f def finish(self, name, note): bmesh.ops.remove_doubles(self.bm, verts=self.bm.verts[:], dist=1e-5) _forget(name) me = bpy.data.meshes.new(name) self.bm.to_mesh(me) self.bm.free() for m in self.names: me.materials.append(bpy.data.materials[m]) me["made_for"] = MADE_FOR me["note"] = note return me def _rect(box, a, b): """UVs of a quad in a rectangle of a trim sheet: a, b = (s, t) of two opposite corners, each 0..1.""" u0, v0, u1, v1 = box return [(u0 + (u1 - u0) * s, v0 + (v1 - v0) * t) for s, t in ((a[0], a[1]), (b[0], a[1]), (b[0], b[1]), (a[0], b[1]))] def _run(own, length, depth, joints=(), caps=False): """The platform's edge profile drawn along local +x from the origin: the front at y = 0 looks toward -y, the top reaches back to y = depth. Split at `joints` (distances along it).""" cuts = [0.0] + [j for j in joints if 0.0 < j < length] + [length] back, up = Vector((0.0, 1.0, 0.0)), Vector((0.0, 0.0, 1.0)) prof = PROFILE + [(depth, PLAT_H)] bands = BANDS + [(DARK, None)] for s0, s1 in zip(cuts, cuts[1:]): for (ya, za), (yb, zb), (mat, uvw) in zip(prof, prof[1:], bands): pts = [(s0, ya, za), (s1, ya, za), (s1, yb, zb), (s0, yb, zb)] if uvw is None: uvs = _rect(PLAIN, (s0 / length, 0.0), (s1 / length, 1.0)) else: va, vb, ua, per = uvw uvs = [(ua + per * s0, va), (ua + per * s1, va), (ua + per * s1, vb), (ua + per * s0, vb)] own.face(pts, mat, uvs, back * -(zb - za) + up * (yb - ya)) if caps: for s, d in ((0.0, -1.0), (length, 1.0)): for (ya, za), (yb, zb) in zip(PROFILE, PROFILE[1:]): own.face([(s, ya, za), (s, depth, za), (s, depth, zb), (s, yb, zb)], DARK, _rect(PLAIN, (0.0, 0.0), (1.0, 1.0)), (d, 0.0, 0.0)) def mesh_dais(length, joints): own = Own([SHINY, DARK]) _run(own, length, HALF + 0.06 - DAIS_Y, joints) return own.finish(PRE + "Dais", "machinery base along a wall of the reactor room: the edge profile and trim-sheet bands of the " "pack's platform (EN_SM_Intersection_Floor). No underside and no back: it stands on the floor against the wall") def mesh_course(): own = Own([SHINY, DARK]) _run(own, DAIS_Y - END_PANEL - JOINT, COURSE + 0.06, caps=True) # (6 cm of its top lie under the wall piece's skirting) return own.finish(PRE + "Base_Course", "base under an end wall of the reactor room, the dais' profile carried round") def mesh_pad(): own = Own([RIM]) a, b = 0.005, MODULE - 0.005 own.face([(a, a, 0.0), (b, a, 0.0), (b, b, 0.0), (a, b, 0.0)], RIM, _rect(RIM_UV, (0.0, 0.0), (1.0, 1.0)), (0, 0, 1)) return own.finish(PRE + "Platform_Pad", "plate under a vessel's platform (it shows only in the platform's cut corners)") def mesh_sill(): """The plate a door-side brace and the first pilaster stand on: it lies on the floor module's rim (the module's panel is 2 cm lower). Local frame: x from the end wall's plane into the room, y across, centred.""" own = Own([RIM]) xa, xb = 0.0, 0.62 hw = (END_PANEL - 1.805) / 2.0 one = _rect(RIM_UV, (0.0, 0.0), (0.3, 1.0)) own.face([(xa, -hw, SILL), (xb, -hw, SILL), (xb, hw, SILL), (xa, hw, SILL)], RIM, _rect(RIM_UV, (0.0, 0.0), (0.28, 1.0)), (0, 0, 1)) own.face([(xb, -hw, 0.0), (xb, hw, 0.0), (xb, hw, SILL), (xb, -hw, SILL)], RIM, one, (1, 0, 0)) for y in (-hw, hw): own.face([(xa, y, 0.0), (xb, y, 0.0), (xb, y, SILL), (xa, y, SILL)], RIM, one, (0, y, 0)) return own.finish(PRE + "Sill_Plate", "plate under a door-side brace and pilaster of the reactor room") def mesh_floor_frame(): """A floor module: the glass floor tile's own top (rim, seat for the panel, clips), closed underneath.""" src = bpy.data.meshes["EN_SM_GlassFloor_A"] zs = [v.co.z for v in src.vertices] xs = [v.co.x for v in src.vertices] ys = [v.co.y for v in src.vertices] rim_top = min(zs) + 1.20 # the tile stands 1.20 m below its rim (and the sill) cut = rim_top - FLOOR_KEEP me = cut_copy("EN_SM_GlassFloor_A", PRE + "Floor_Frame", cut, "floor module of the reactor room: the top %.0f cm of the pack's glass floor tile, closed underneath by a plate" % (FLOOR_KEEP * 100.0)) bm = bmesh.new() bm.from_mesh(me) uv = bm.loops.layers.uv.verify() a = 0.10 pts = [(min(xs) + a, min(ys) + a, cut), (max(xs) - a, min(ys) + a, cut), (max(xs) - a, max(ys) - a, cut), (min(xs) + a, max(ys) - a, cut)] f = bm.faces.new([bm.verts.new(p) for p in pts]) if f.normal.z < 0.0: f.normal_flip() f.material_index = _slot(me, BLACK) for loop, u in zip(f.loops, _rect(STRIP_UV, (0.0, 0.0), (1.0, 1.0))): loop[uv].uv = u bm.to_mesh(me) bm.free() me.update() return me # ---- the record ---------------------------------------------------------------------------------------------------- def _plain(v): if hasattr(v, "to_dict"): return v.to_dict() if hasattr(v, "to_list"): return v.to_list() return v def _snap(o): return {"name": o.name, "type": o.type, "data": o.data.name if o.data is not None else None, "matrix": [list(r) for r in o.matrix_world], "collections": [c.name for c in o.users_collection], "parent": o.parent.name if o.parent else None, "props": {k: _plain(o[k]) for k in o.keys()}, "slots": [[s.link, s.material.name if s.material else None] for s in o.material_slots] if o.type == "MESH" else []} def exists(): return RECORD in bpy.data.texts def record(): return json.loads(bpy.data.texts[RECORD].as_string()) if exists() else None def forget(): """Drop the record without touching any object. For a rebuild: build_interior.py throws the whole layout away and builds the gallery again as the pack's car, so a record left in the file describes objects that are gone.""" rec = record() if rec is None: return False bpy.data.texts.remove(bpy.data.texts[RECORD]) for n in rec["meshes"]: me = bpy.data.meshes.get(n) if me is not None and me.users == 0: _forget(n) return True def _tag(o, rec): o["made_by"] = TAG rec["added"].append(o.name) return o # ---- building ------------------------------------------------------------------------------------------------------ def apply(): """The corridor becomes the room. Returns what was done, in numbers.""" if exists(): raise RuntimeError("the reactor room is already there (text block %r); remove() puts the corridor back" % RECORD) car = Car() plan = car.plan() if len(plan["walls"]) != 2 * len(car.bays) or len(car.bays) < 3: raise RuntimeError("this is not the gallery as built: %d wall pieces for %d bays" % (len(plan["walls"]), len(car.bays))) rec = {"version": 1, "removed": [], "moved": [], "added": [], "meshes": [], "half_width": HALF, "x": [car.x0, car.x1]} coll, root, zone = car.coll, car.root, ZONE x0, x1 = car.x0, car.x1 zt = Z2 + PLAT_H put = quarters.put # 1. what stood in the gutters and on the walls' bases goes for o in plan["gone"]: rec["removed"].append(_snap(o)) if o.data is not None: o.data.use_fake_user = True # (so remove() finds the pack's mesh again) bpy.data.objects.remove(o, do_unlink=True) # 2. the walls and what hangs on them go out; the wall pieces, the wire on one of them and the door-side braces # become cut copies cuts = {} def cut_of(o, z_world, note): k = _kind(o) if k not in cuts: # (these pieces stand upright and unscaled: the cut's height in the mesh is its height over the origin) cuts[k] = cut_copy(k, PRE + k.split("_SM_", 1)[1], z_world - o.matrix_world.translation.z, note) rec["meshes"].append(cuts[k].name) return cuts[k] def swap(o, me): o.data = me if "ue_mesh" in o: o["cut_from"] = o["ue_mesh"] del o["ue_mesh"] for o in plan["walls"] + plan["wire"] + plan["moved"]: rec["moved"].append(_snap(o)) s = 1.0 if o.matrix_world.translation.y > 0.0 else -1.0 dx = 0.0 if o in plan["wire"]: # a wire up the wall keeps clear of the corner pilaster lo, hi = _bb(o) dx = min(0.0, x1 - PIER / 2.0 - 0.02 - hi.x) or max(0.0, x0 + PIER / 2.0 + 0.02 - lo.x) if o.type == "MESH" and _kind(o) in WALLS + ("EP_SM_Wire_Wall_B",): swap(o, cut_of(o, zt + OFF, "the pack's piece without the part that stood below the floor and behind the dais: " "the hull and the lower engine pods are in the way of it out here")) o.matrix_world = Matrix.Translation((dx, s * SHIFT, 0.0)) @ o.matrix_world for o in plan["braces"]: rec["moved"].append(_snap(o)) swap(o, cut_of(o, Z2 + SILL + OFF, "the pack's door-side brace without the foot that stood in the gutter")) end_cut = cuts[END_WALL] # (the gallery has all three wall pieces) pier_cut = cut_copy("SI_SM_Wall_Divider_A", PRE + "Wall_Divider_A", PLAT_H + OFF, "the pack's pilaster for standing on the dais") rec["meshes"].append(pier_cut.name) # 3. the parts drawn here joints = [b[1] - x0 for b in car.bays[:-1]] own = {"dais": mesh_dais(car.length, joints), "course": mesh_course(), "pad": mesh_pad(), "frame": mesh_floor_frame(), "sill": mesh_sill()} rec["meshes"] += [m.name for m in own.values()] def place(me, name, m): return _tag(quarters.place_mesh(me, NAME + name, coll, root, m, zone), rec) def kit(mesh, name, at, yaw=0.0, anchor="---", tilt=None): return _tag(put(mesh, coll, root, zone, at, yaw=yaw, anchor=anchor, name=NAME + name, tilt=tilt), rec) T, R = Matrix.Translation, lambda deg: Matrix.Rotation(math.radians(deg), 4, "Z") mid = len(car.bays) // 2 # a wall piece of the gallery with its light fitting and its lamp: the end walls get the same, in the same place ref = plan["walls"][0] top = Vector((_mid(ref).x, _mid(ref).y, CEILING)) fit = min((o for o in plan["moved"] if _kind(o) == "EN_SM_Light_A"), key=lambda o: (o.matrix_world.translation - top).length, default=None) spot = min((o for o in plan["moved"] if o.type == "LIGHT"), key=lambda o: (o.matrix_world.translation - top).length, default=None) inv = ref.matrix_world.inverted() for s, side in ((1.0, "port"), (-1.0, "starboard")): # the dais along the wall place(own["dais"], "dais " + side, T((x0, DAIS_Y, Z2)) if s > 0 else T((x1, -DAIS_Y, Z2)) @ R(180.0)) # ceiling: one more row of panels, the centre row's own pieces carried out for i, c in enumerate(car.ceilings): o = _tag(quarters.place_mesh(c.data, NAME + "ceiling %s %d" % (side, i + 1), coll, root, T((0.0, s * 4.8, 0.0)) @ c.matrix_world, zone), rec) if "ue_mesh" in c: o["ue_mesh"] = c["ue_mesh"] # floor: a module beside each glass floor tile for i, (t, (xa, xb)) in enumerate(zip(car.tiles, car.bays)): m = T((0.0, s * (MODULE + JOINT), 0.0)) @ t.matrix_world if i == mid: place(own["pad"], "platform pad " + side, T((xa, Y_IN if s > 0 else -Y_OUT, Z2 - OFF))) kit("EN_SM_Intersection_Floor", "vessel platform " + side, ((xa + xb) / 2.0, s * (Y_IN + MODULE / 2.0), Z2), anchor="00-") kit("SI_SM_WVC_Tank_A", "reactor vessel " + side, ((xa + xb) / 2.0, s * (Y_IN + MODULE / 2.0), zt + OFF), yaw=0.0 if s > 0 else 180.0, anchor="00-") else: place(own["frame"], "floor frame %s %d" % (side, i + 1), m) o = _tag(quarters.place_mesh(bpy.data.meshes["EN_SM_GlassFloor_Panel"], NAME + "floor panel %s %d" % (side, i + 1), coll, root, m, zone), rec) o["ue_mesh"] = o.data["ue_mesh"] # a bank of power cells on the dais behind the vessel xm = sum(car.bays[mid]) / 2.0 kit("SI_SM_PowerCells_Wall", "power cells " + side, (xm, s * (HALF - CELLS_OFF), zt + OFF), yaw=0.0 if s > 0 else 180.0, anchor="0+-" if s > 0 else "0--") # the two ends beside the doors for e, end, xe in ((1.0, "aft", x0), (-1.0, "fore", x1)): yaw_wall, yaw_pier = -90.0 * e, 90.0 * e # the wall piece's face and the pilaster's face look into the room ax = "+" if e > 0 else "-" # the wall piece lies behind the end plane lo, hi = sorted((s * END_PANEL, s * (END_PANEL + MODULE))) o = kit(end_cut.name, "end wall %s %s" % (end, side), (xe + e * END_PROUD, lo, zt + OFF), yaw=yaw_wall, anchor=ax + "--") o["cut_from"] = end_cut["cut_from"] if fit is not None: f = _tag(quarters.place_mesh(fit.data, NAME + "wall light %s %s" % (end, side), coll, root, o.matrix_world @ (inv @ fit.matrix_world), zone), rec) f["ue_mesh"] = fit.data["ue_mesh"] if spot is not None: lamp = bpy.data.objects.new(NAME + "wall lamp %s %s" % (end, side), spot.data) quarters._link(lamp, car.lights, root, o.matrix_world @ (inv @ spot.matrix_world), zone) for k in spot.keys(): if k != "zone": lamp[k] = spot[k] _tag(lamp, rec) for j, (ya, zb, me) in enumerate(((END_PANEL - PIER, Z2 + SILL + OFF, "SI_SM_Wall_Divider_A"), (END_PANEL + MODULE, zt + OFF, pier_cut.name))): lo, hi = sorted((s * ya, s * (ya + PIER))) o = kit(me, "pilaster %s %s %d" % (end, side, j + 1), (xe, lo, zb), yaw=yaw_pier, anchor="0--") if me == pier_cut.name: o["cut_from"] = pier_cut["cut_from"] # base under the wall piece, and the plate the brace and the first pilaster stand on xc, yc = xe + e * (END_PROUD + COURSE), END_PANEL + JOINT if e > 0: place(own["course"], "base course %s %s" % (end, side), T((xc, yc if s > 0 else -DAIS_Y, Z2)) @ R(90.0)) else: place(own["course"], "base course %s %s" % (end, side), T((xc, DAIS_Y if s > 0 else -yc, Z2)) @ R(-90.0)) place(own["sill"], "sill plate %s %s" % (end, side), T((xe, s * (1.805 + END_PANEL) / 2.0, Z2)) @ (R(0.0) if e > 0 else R(180.0))) # light: a tube under the ceiling over each new floor module, fore and aft along the room for i, (xa, xb) in enumerate(car.bays): at = ((xa + xb) / 2.0, s * TUBE_Y, CEILING - OFF) kit(TUBE, "ceiling light %s %d" % (side, i + 1), at, yaw=90.0, anchor="00+", tilt=("Y", 90.0)) lamp = quarters.area_lamp(car.lights, root, NAME + "ceiling lamp %s %d" % (side, i + 1), (at[0], at[1], CEILING - 0.13), TUBE_WATTS, (1.50, 0.14), color=TUBE_COLOR, zone=zone) _tag(lamp, rec) txt = bpy.data.texts.new(RECORD) txt.write(json.dumps(rec, indent=1)) bpy.context.view_layer.update() return {"removed": len(rec["removed"]), "moved": len(rec["moved"]), "added": len(rec["added"]), "meshes": rec["meshes"], "room": [round(car.length, 2), 2 * HALF, round(0.52 - Z2, 2)]} def remove(): """The corridor again, from the record.""" rec = record() if rec is None: return {"present": False} for n in rec["added"]: o = bpy.data.objects.get(n) if o is not None: d = o.data bpy.data.objects.remove(o, do_unlink=True) if isinstance(d, bpy.types.Light) and d.users == 0: bpy.data.lights.remove(d) for d in rec["moved"]: o = bpy.data.objects.get(d["name"]) if o is None: continue if d["type"] == "MESH" and o.data.name != d["data"]: o.data = bpy.data.meshes[d["data"]] for k in ("cut_from",): if k in o: del o[k] if "ue_mesh" in d["props"]: o["ue_mesh"] = d["props"]["ue_mesh"] o.matrix_world = Matrix(d["matrix"]) back = 0 for d in rec["removed"]: if d["name"] in bpy.data.objects or d["data"] not in bpy.data.meshes: continue o = bpy.data.objects.new(d["name"], bpy.data.meshes[d["data"]]) for c in d["collections"]: if c in bpy.data.collections: bpy.data.collections[c].objects.link(o) if d["parent"] and d["parent"] in bpy.data.objects: o.parent = bpy.data.objects[d["parent"]] o.matrix_world = Matrix(d["matrix"]) for k, v in d["props"].items(): o[k] = v for slot, (link, mat) in zip(o.material_slots, d["slots"]): if link == "OBJECT": slot.link = "OBJECT" slot.material = bpy.data.materials.get(mat) if mat else None back += 1 for n in rec["meshes"]: me = bpy.data.meshes.get(n) if me is not None and me.users == 0: _forget(n) bpy.data.texts.remove(bpy.data.texts[RECORD]) bpy.context.view_layer.update() return {"present": True, "taken_out": len(rec["added"]), "moved_back": len(rec["moved"]), "put_back": back} # ---- checking ------------------------------------------------------------------------------------------------------ def _geo(o, cache): g = cache.get(o.name) if g is None: me = o.data me.calc_loop_triangles() mw = o.matrix_world vs = [mw @ v.co for v in me.vertices] ts = [tuple(t.vertices) for t in me.loop_triangles] tree = BVHTree.FromPolygons([tuple(v) for v in vs], ts, epsilon=0.0) if ts else None lo = Vector((min(v.x for v in vs), min(v.y for v in vs), min(v.z for v in vs))) if vs else Vector() hi = Vector((max(v.x for v in vs), max(v.y for v in vs), max(v.z for v in vs))) if vs else Vector() g = cache[o.name] = (vs, ts, tree, lo, hi) return g def _crossing(a, b): """Pairs of faces of two meshes that cross: each reaches more than EPS through the other's plane.""" (va, ta, tra, loa, hia), (vb, tb, trb, lob, hib) = a, b if tra is None or trb is None or any(loa[i] > hib[i] or lob[i] > hia[i] for i in range(3)): return 0 def straddles(p, q): n = (q[1] - q[0]).cross(q[2] - q[0]) if n.length < 1e-12: return False n.normalize() d = [(x - q[0]).dot(n) for x in p] return max(d) > EPS and min(d) < -EPS k = 0 for i, j in tra.overlap(trb): p = [va[v] for v in ta[i]] q = [vb[v] for v in tb[j]] if straddles(p, q) and straddles(q, p): k += 1 return k def _label(o): k = _kind(o) return k if (k.startswith(PRE) or o.data.get("ue_mesh")) else "[ship] " + o.name def crossings(mine, others, cache=None): """{'A x B': crossing face pairs} between each of `mine` and each of `others`, and within `mine`.""" cache = {} if cache is None else cache out = {} ms = [o for o in mine if o.type == "MESH" and not _kind(o).startswith("DECAL_")] os_ = [o for o in others if o.type == "MESH" and not _kind(o).startswith("DECAL_")] for i, a in enumerate(ms): for b in ms[i + 1:] + os_: if a is b: continue k = _crossing(_geo(a, cache), _geo(b, cache)) if k: key = " x ".join(sorted((_label(a), _label(b)))) out[key] = out.get(key, 0) + k return out def _j(a, b): return " x ".join(sorted((a, b))) # Pieces that meet the way the pack joins them. The same pairs cross in the corridor as the pack's demo built it: a # wall's head (and a brace's, a wire's) stands 2 cm into the ceiling panel's lip, a light fitting and the padded panel # sit in their wall, the perforated panel in its seat. The pilasters are as high as the wall pieces and meet the # ceiling the same way. _W = [PRE + w.split("_SM_", 1)[1] for w in WALLS] KIT_JOINTS = {_j("EN_SM_Ceiling_HB_A", p) for p in _W + [PRE + "Doorway_SideBrace", PRE + "Wire_Wall_B", PRE + "Wall_Divider_A"]} \ | {_j(f, w) for w in _W for f in ("EN_SM_Light_A", "SI_SM_Wall_Puffy_A", PRE + "Wire_Wall_B")} \ | {_j("EN_SM_GlassFloor_Panel", PRE + "Floor_Frame")} def _is_kit(label): return not label.startswith("[ship]") def check(others=True): """For check_interior.py: is the room there, is every part of it inside the ship, does anything of it cross the ship's own structure (hull, engine pods, bulkheads) or another part of the layout.""" out = {"present": exists()} if not out["present"]: return out import shipfit rec = record() names = list(rec["added"]) + [d["name"] for d in rec["moved"]] mine = [bpy.data.objects[n] for n in names if n in bpy.data.objects] meshes = [o for o in mine if o.type == "MESH" and not _kind(o).startswith("DECAL_")] out["objects"] = {"added": len(rec["added"]), "moved": len(rec["moved"]), "removed": len(rec["removed"]), "missing": [n for n in names if n not in bpy.data.objects]} out["own_meshes"] = {n: len(bpy.data.meshes[n].polygons) for n in rec["meshes"] if n in bpy.data.meshes} walls = [o for o in meshes if _kind(o).startswith(PRE + "Wall_HB") and abs(_mid(o).y) > HALF - 0.3] out["width_between_walls_m"] = round(min(_bb(o)[0].y for o in walls if _mid(o).y > 0) - max(_bb(o)[1].y for o in walls if _mid(o).y < 0), 3) if walls else None env = shipfit.Envelope() worst, outside = (9.0, None, None), {} for o in meshes: r = shipfit.check_inside(o, env, spacing=0.10, clear_min=0.0) if r["outside"]: outside[o.name] = [r["outside"], r["furthest_outside_m"], r["outside_sample"][:2]] if r["least_clearance_m"] < worst[0]: worst = (r["least_clearance_m"], o.name, r["least_clearance_at"]) out["inside_the_ship"] = {"objects": len(meshes), "outside": outside, "least_clearance_m": worst[0], "at": worst[1], "point": worst[2]} if others: lo = Vector((rec["x"][0] - 1.5, -HALF - 1.5, Z2 - 2.5)) hi = Vector((rec["x"][1] + 1.5, HALF + 1.5, 2.0)) mine_set = {o.name for o in mine} near = [] # (the interior's own scene only: the file also holds an export scene with a merged ship body made from the # hull before its rebuild, with an inner skin 0.3 m inside that the hull no longer has) scene = (meshes[0].users_scene or [bpy.context.scene])[0] for o in scene.objects: if o.type != "MESH" or o.name in mine_set or o.name.startswith("KIT | "): continue a, b = _bb(o) if any(a[i] > hi[i] or lo[i] > b[i] for i in range(3)): continue near.append(o) got = crossings(meshes, near) bad = {k: v for k, v in got.items() if any(not _is_kit(p) or p.startswith(PRE) for p in k.split(" x ")) and k not in KIT_JOINTS} out["crossings"] = {"against": len(near), "with_the_ship_or_parts_made_here": bad, "pack_pieces_joined_the_packs_way": {k: v for k, v in got.items() if k not in bad}} return out