""" The station's exterior: an outer shell over the kit's rooms. The kit is built to be seen from inside only (its walls and ceilings are one-sided), so from outside the station was an open cage. add_exterior(b) adds (or replaces) the collection "08 EXTERIOR shell" in the open file, in place What is built, and why this way * ANCH_Shell_Rooms: the windowless rooms as ONE welded mesh, a union of boxes on a common grid. Only faces that end up outside are made, so there is nothing between two rooms and nothing inside the walk-through; no boolean cuts, all quads. The two junctions are wider, slightly taller blocks ("hubs"): every window gallery butts against a wall at least as wide as itself. * ANCH_Shell_Galleries: the window galleries (arrivals lock, mooring gallery, both quays). The kit finishes them outside only at the slanted window frames; below the sills, between the frames and above them it is open. So each gallery gets a base wall up to the sill, piers between the frames that stand 10 cm proud of them, a frieze above, a roof slab, and an end wall round its outer door. The frames are read from the file, so the envelope follows the kit pieces that are really there. * ANCH_Shell_Relay: Relay Control keeps the kit's round window drum; it gets a roof cap with a round opening over its skylight, walls round its square half and a footing ring under the drum. * ANCH_Decks: a landing deck with a ramp outside the arrivals door and an apron outside the maintenance door. * ANCH_Roof_Services: ducts over the corridors. * The relay: the satellite dish of the Sci-Fi Lite pack (/Game/SciFI_Lite_Assets, the user's choice of 2026-10-14: "use the relay from Sci-Fi Lite Sample pack"), standing on Relay Control's roof where a mast of my own first stood. Three kit pieces as the pack's own overview map stacks them: base, yoke, dish. They are separate, so the game can turn the yoke and tip the dish. Datums, the same on every wall: dark base band up to floor level (z 0), wall panels above with a joint at 4.40 (one storey of the kit), ribbed roofs. Walls stand 0.30 m outside the kit's outermost geometry and go 0.18 m into the ice. Materials are the pack's own tiling instances (ModularSciFiMats), so nothing new is needed in Unreal. The meshes are new geometry (not kit): build/export_custom.py writes them as FBX with collision hulls. build/check_exterior.py tests the result: nothing of a room may be visible from outside, nothing of the shell from inside. """ import math import bpy from mathutils import Matrix, Vector import own_materials from bl_kit import GAME, get_collection COLLECTION = "08 EXTERIOR shell" ZONE = "08 EXTERIOR shell" BASE_Z = -0.80 # bottom of walls (the ice top is at -0.62) ROOF_Z = 5.05 # roof of the windowless rooms (their kit ceilings end at 4.70 to 4.80) HUB_ROOF_Z = 5.45 # the two junction blocks HUB = 6.20 # half depth of a junction block, along the gallery that meets it STOREY = 4.40 # the kit's storey: door height, and the joint in the wall panels T = 0.30 # how far the shell stands outside the kit geometry TILE = 4.4 # one repeat of the pack's wall texture = one storey # The walls were the pack's white panels with an orange band at first. Since the outpost (2026-10-14, build/outpost.py) # they are the dark structure that shows between the armour plates standing in front of them. MAT_WALL_FIRST = GAME + "ModularSciFiMats/Materials/Instances/MI_Wall_B" MAT_WALL = own_materials.MAT["hull"] MAT_ROOF = GAME + "ModularSciFiMats/Materials/Instances/MI_Floor_E_Scr" MAT_BASE = GAME + "ModularSciFiMats/Materials/Instances/MI_Floor_A_Scr2" WALL, ROOF, BASE = 0, 1, 2 # The relay: the Sci-Fi Lite pack's satellite, on Relay Control's roof. Its four feet stand on the building's axes, # 3.55 to 5.03 m from its middle; the place keeps them 0.37 m inside the roof's east edge and 0.57 m inside its # north edge, and 2.4 m clear of the skylight. Its ladder comes down to the south-east, onto the open roof. LITE = "/Game/SciFI_Lite_Assets/StaticMeshes/" RELAY_DISH_AT = (-19.8, 8.9) RELAY_DISH_PARTS = ( # mesh, height of its origin above the base's (as in the pack's overview map), ("SM_Satelite_01", 0.0, "base", (0.0, 0.0, 0.0)), # and its rotation (XYZ, radians) ("SM_Satelite_02", 3.79, "yoke", (0.0, 0.0, 0.0)), ("SM_Satelite_03", 6.20, "dish", (-0.835947, 0.0, 0.0)), # tipped back 47.9 degrees: set by the user, by hand, ) # in the open file on 2026-10-14 (it stood level) RELAY_DISH_FEET = 0.03 # the feet's undersides are this far above the base's origin RELAY_DISH_MODULE = "exterior: relay dish (Sci-Fi Lite pack)" # The kit's window gallery in cross-section, measured on IN_SM_Win_Frame_A (w = distance from the gallery's axis). # The frames lean outwards: their outer face runs from the foot to the head given here. G_FOOT = (4.626, 0.603) G_HEAD = (6.929, 3.238) G_PROUD = 0.10 # the piers between the frames stand this far in front of them G_DOOR = 4.40 # half width of the kit's end doorway (it is one storey high) G_EAVE = 0.151 # the roof slab's overhang G_SOFFIT = 4.58 # underside of the roof slab (the kit's gallery ceiling ends at 4.56) G_ROOF = 4.85 WB, ZS = G_FOOT[0] + G_PROUD, G_FOOT[1] # base wall, and where the piers begin to lean WK, ZK = G_HEAD[0] + G_PROUD, G_HEAD[1] # the knee: from here up the envelope is vertical (the frieze) WS = WK + G_EAVE # half width of the roof slab: 7.18 SLOPE = (G_HEAD[0] - G_FOOT[0]) / (G_HEAD[1] - G_FOOT[1]) SLANT_LEN = math.sqrt(1.0 + SLOPE * SLOPE) # Relay Control (the command centre): centre of its round half, cap and wall heights CC = (-25.0, 0.0) CC_CAP_Z0, CC_CAP_Z1 = 6.70, 7.00 CC_BASE_Z = -2.20 # its walls go deeper: the ice dips into a moat round its sunken window ring CC_SKYLIGHT_R = 3.60 # the cap's opening (the kit's skylight: glass 3.55, frame 3.88) CC_SKYLIGHT_Z = 6.64 # the opening's wall comes down to the skylight frame's top CC_DRUM_R = 11.20 # the kit's window drum: sill and head rings CC_BAND_R = 10.77 # the drum's upper band, above the windows (kit: 4.42 to 6.10) CC_BAND_TOP = 6.10 CC_CORNICE_Z = 4.50 # top of the ring over the window heads; the kit leaves it open from above CC_CAP_R = 11.75 CC_PLINTH_R = 11.50 CC_STEP = 0.80 # the square half's west walls stand this far west of the drum's axis (kit: 0.50) QUAY_ROOM_END = 10.85 # where the quays' first window unit begins (its end posts), from the dock head's centre ARRIVALS_START = 32.91 # where the arrivals lock's end posts begin DECK_Z = 0.0 # top of the outside decks: level with the door thresholds # --------------------------------------------------------------------------------------------------------------- def _normal(pts): """Newell's method: works for quads that are not quite flat as well.""" nx = ny = nz = 0.0 for i in range(len(pts)): a, c = pts[i], pts[(i + 1) % len(pts)] nx += (a[1] - c[1]) * (a[2] + c[2]) ny += (a[2] - c[2]) * (a[0] + c[0]) nz += (a[0] - c[0]) * (a[1] + c[1]) return nx, ny, nz class MeshBuilder(object): """Collects quads with welded vertices, a material index and UVs per face.""" def __init__(self): self.verts, self.vmap, self.faces, self.mats, self.uvs = [], {}, [], [], [] self.no_inner = set() # faces that get no inner skin: cover strips lying on a wall def v(self, p): key = (round(p[0], 4), round(p[1], 4), round(p[2], 4)) i = self.vmap.get(key) if i is None: i = len(self.verts) self.vmap[key] = i self.verts.append(key) return i def face(self, pts, mat, uv): idx = [self.v(p) for p in pts] if len(set(idx)) < len(idx): return self.faces.append(idx) self.mats.append(mat) self.uvs.append(list(uv)) def poly(self, pts, uv, mat, outward, inner=True): """A face wound so that its normal points along `outward`, whatever order the points come in.""" n = _normal(pts) if n[0] * outward[0] + n[1] * outward[1] + n[2] * outward[2] < 0.0: pts, uv = list(pts)[::-1], list(uv)[::-1] before = len(self.faces) self.face(pts, mat, uv) if not inner and len(self.faces) > before: self.no_inner.add(before) # axis-aligned quads, wound so the normal points the way the name says def wall_x(self, x, y0, y1, z0, z1, outward, mat): uv = [(y0 / TILE, z0 / TILE), (y1 / TILE, z0 / TILE), (y1 / TILE, z1 / TILE), (y0 / TILE, z1 / TILE)] self.poly([(x, y0, z0), (x, y1, z0), (x, y1, z1), (x, y0, z1)], uv, mat, (outward, 0.0, 0.0)) def wall_y(self, y, x0, x1, z0, z1, outward, mat): uv = [(x0 / TILE, z0 / TILE), (x1 / TILE, z0 / TILE), (x1 / TILE, z1 / TILE), (x0 / TILE, z1 / TILE)] self.poly([(x0, y, z0), (x1, y, z0), (x1, y, z1), (x0, y, z1)], uv, mat, (0.0, outward, 0.0)) def flat(self, x0, x1, y0, y1, z, up, mat): uv = [(x0 / TILE, y0 / TILE), (x1 / TILE, y0 / TILE), (x1 / TILE, y1 / TILE), (x0 / TILE, y1 / TILE)] self.poly([(x0, y0, z), (x1, y0, z), (x1, y1, z), (x0, y1, z)], uv, mat, (0.0, 0.0, up)) def box(self, x0, x1, y0, y1, z0, z1, side_mat, top_mat, drop=()): """Closed box; `drop` lists faces that are hidden against something else: '+x', '-x', '+y', '-y', 'top', 'bottom'.""" if "top" not in drop: self.flat(x0, x1, y0, y1, z1, 1, top_mat) if "bottom" not in drop: self.flat(x0, x1, y0, y1, z0, -1, side_mat) if "+x" not in drop: self.wall_x(x1, y0, y1, z0, z1, 1, side_mat) if "-x" not in drop: self.wall_x(x0, y0, y1, z0, z1, -1, side_mat) if "+y" not in drop: self.wall_y(y1, x0, x1, z0, z1, 1, side_mat) if "-y" not in drop: self.wall_y(y0, x0, x1, z0, z1, -1, side_mat) def build(self, name, kit, mats=(MAT_WALL, MAT_ROOF, MAT_BASE), inner_skin=False): """inner_skin: every face once more, turned round and set 1 cm inwards, in the dark base material. The kit's rooms have hairline gaps between their pieces; through those one saw the sky, because the shell's faces are one-sided like everything else. With the inner skin one sees a dark wall instead, and no light gets in.""" verts, faces, fmats, fuvs = [tuple(v) for v in self.verts], list(self.faces), list(self.mats), list(self.uvs) n_outer = len(faces) if inner_skin: acc = [[0.0, 0.0, 0.0] for _ in verts] for f in faces: n = _normal([verts[i] for i in f]) ln = math.sqrt(n[0] ** 2 + n[1] ** 2 + n[2] ** 2) or 1.0 for i in f: for k in range(3): acc[i][k] += n[k] / ln base = len(verts) for v, a in zip(list(verts), acc): la = math.sqrt(a[0] ** 2 + a[1] ** 2 + a[2] ** 2) d = 0.01 / la if la > 1e-6 else 0.0 verts.append((v[0] - a[0] * d, v[1] - a[1] * d, v[2] - a[2] * d)) for k, (f, uvf) in enumerate(zip(list(faces), list(fuvs))): if k in self.no_inner: continue faces.append([i + base for i in f][::-1]) fmats.append(BASE) fuvs.append(uvf[::-1]) me = bpy.data.meshes.new(name) me.from_pydata(verts, [], faces) for path in mats: me.materials.append(own_materials.get(kit, path)) # the pack's, or one of the station's own me.polygons.foreach_set("material_index", fmats) uv = me.uv_layers.new(name="UVMap") k = 0 for f in fuvs: for c in f: uv.data[k].uv = c k += 1 me.polygons.foreach_set("use_smooth", [False] * len(me.polygons)) me.update() me.validate() if inner_skin: me["anchorage_inner_from"] = n_outer # polygons from this index on are the inner skin return me # The kit's big door leaf (6.8 x 4.0 m, one piece) can only lift. The frame's opening is 3.72 m high, but the leaf # has to lift 4.10 m: at 3.80 its foot still stands in the trim of the frame's head (build/check_doors.py found # that); from 4.10 up the kit's own frame has the slot for it. Over every leaf the roof has a slot too, and # build/outpost_more.py stands a housing on it that takes the whole lifted leaf. Found by the user on 2026-10-14: # "these doors are when opened (slide) they will clip through the walls because there is not enough coverage". DOOR_TRAVEL = 4.10 DOOR_SIDE = 0.20 # the slot is this much wider than the leaf at each end DOOR_PLAY = 0.18 # and this much deeper than the leaf on each face def door_closed_matrix(ob): """Where a leaf stands when it is shut. The doors are rigged (build/doors.py): a leaf left open in the file still has its "closed_location", and the slots, the housings and the export go by that.""" m = ob.matrix_world.copy() c = ob.get("closed_location") if c is not None and len(c) == 3: d = Vector(c) - ob.location if ob.parent is not None: d = (ob.parent.matrix_world @ ob.matrix_parent_inverse).to_3x3() @ d m.translation = m.translation + d return m def door_leaves(): """The door leaves that are in the file, shut: (object, lo, hi, axis the leaf is thin along).""" out = [] for ob in bpy.data.objects: if ob.type != "MESH" or ob.get("ue_role") != "door_leaf" or ob.get("anchorage_kit_source"): continue mw = door_closed_matrix(ob) pts = [mw @ Vector(c) for c in ob.bound_box] lo = Vector((min(p.x for p in pts), min(p.y for p in pts), min(p.z for p in pts))) hi = Vector((max(p.x for p in pts), max(p.y for p in pts), max(p.z for p in pts))) out.append((ob, lo, hi, "x" if hi.x - lo.x < hi.y - lo.y else "y")) return sorted(out, key=lambda t: t[0].name) def door_slots(): """The slots in the roofs, on the plan: (x0, x1, y0, y1) for every door leaf in the file.""" out = [] for ob, lo, hi, thin in door_leaves(): if thin == "x": out.append((round(lo.x - DOOR_PLAY, 3), round(hi.x + DOOR_PLAY, 3), round(lo.y - DOOR_SIDE, 3), round(hi.y + DOOR_SIDE, 3))) else: out.append((round(lo.x - DOOR_SIDE, 3), round(hi.x + DOOR_SIDE, 3), round(lo.y - DOOR_PLAY, 3), round(hi.y + DOOR_PLAY, 3))) return out def _split(a0, a1, step): n = max(1, int(math.ceil((a1 - a0) / step - 1e-6))) return [a0 + (a1 - a0) * i / n for i in range(n + 1)] # --------------------------------------------------------------------------------------------------------------- def union_shell(mb, rects, bays, openings, z_levels, base_z=BASE_Z, roof_slots=()): """Outer faces of a union of boxes. rects (x0, x1, y0, y1, ztop): the rooms to enclose bays (x0, x1, y0, y1, ztop): space that something else already closes up to ztop (Relay Control): no faces are made there, and a neighbouring room's wall starts at ztop openings ("x" or "y", plane, a0, a1, z0, z1): rectangular holes in the wall lying in that plane (door portals) roof_slots (x0, x1, y0, y1): rectangles left out of the roofs (a door leaf lifts through; a housing stands on it) All walls are cut at the same z levels and on the same grid lines, so the mesh has no T-junctions.""" inside = [s for s in roof_slots if any(r[0] - 1e-3 <= s[0] and s[1] <= r[1] + 1e-3 and r[2] - 1e-3 <= s[2] and s[3] <= r[3] + 1e-3 for r in rects)] xs = sorted(set(round(v, 3) for r in list(rects) + list(bays) for v in r[:2]) | set(round(v, 3) for o in openings if o[0] == "y" for v in o[2:4]) | set(round(v, 3) for s in inside for v in s[:2])) ys = sorted(set(round(v, 3) for r in list(rects) + list(bays) for v in r[2:4]) | set(round(v, 3) for o in openings if o[0] == "x" for v in o[2:4]) | set(round(v, 3) for s in inside for v in s[2:4])) nx, ny = len(xs) - 1, len(ys) - 1 def cover(i, j): """('full' | 'bay' | None, height) of grid cell i, j""" if i < 0 or j < 0 or i >= nx or j >= ny: return None, base_z cx, cy = (xs[i] + xs[i + 1]) / 2, (ys[j] + ys[j + 1]) / 2 h = [r[4] for r in rects if r[0] < cx < r[1] and r[2] < cy < r[3]] if h: return "full", max(h) b = [r[4] for r in bays if r[0] < cx < r[1] and r[2] < cy < r[3]] if b: return "bay", max(b) return None, base_z def is_open(axis, plane, a0, a1, z0, z1): for o in openings: if o[0] == axis and abs(o[1] - plane) < 2e-3 and a0 >= o[2] - 1e-3 and a1 <= o[3] + 1e-3 \ and z0 >= o[4] - 1e-3 and z1 <= o[5] + 1e-3: return True return False def bands(z0, z1): zs = [z0] + [z for z in z_levels if z0 + 1e-4 < z < z1 - 1e-4] + [z1] return list(zip(zs[:-1], zs[1:])) for i in range(nx): for j in range(ny): kind, h = cover(i, j) if kind != "full": continue x0, x1, y0, y1 = xs[i], xs[i + 1], ys[j], ys[j + 1] cx, cy = (x0 + x1) / 2.0, (y0 + y1) / 2.0 if not any(s[0] < cx < s[1] and s[2] < cy < s[3] for s in inside): mb.flat(x0, x1, y0, y1, h, 1, ROOF) for side, (di, dj) in (("+x", (1, 0)), ("-x", (-1, 0)), ("+y", (0, 1)), ("-y", (0, -1))): nk, nh = cover(i + di, j + dj) if nk == "full" and nh >= h - 1e-4: continue zf = nh if nk else base_z if zf >= h - 1e-4: continue for z0, z1 in bands(zf, h): mat = BASE if z1 <= 1e-3 else WALL if side in ("+x", "-x"): x = x1 if side == "+x" else x0 if not is_open("x", x, y0, y1, z0, z1): mb.wall_x(x, y0, y1, z0, z1, 1 if side == "+x" else -1, mat) else: y = y1 if side == "+y" else y0 if not is_open("y", y, x0, x1, z0, z1): mb.wall_y(y, x0, x1, z0, z1, 1 if side == "+y" else -1, mat) # --------------------------------------------------------------------------------------------------------------- def station_plan(b): """The station's outside in numbers, from the plan (build_anchorage.py) and the kit's measured extents. Returns (rooms, bays, openings, galleries).""" hy = b.DOCK_HEAD_Y qe = b.QUAY_END_X # 42.43: the outer face of the quays' end doorways by = b.BRIDGE_END_Y rooms = [ (-WS, WS, -HUB, HUB, HUB_ROOF_Z), # crossing hub (-14.40, -WS, -4.70, 4.70, ROOF_Z), # west hall, up to Relay Control's wall (WS, 20.43, -5.80, 5.80, ROOF_Z), # cargo hall; its end is the door's plane (-WS, WS, HUB, 14.33, ROOF_Z), # processing: supply hall (ceiling ribs 13.6 m wide) (-5.40, 5.40, 14.33, ARRIVALS_START, ROOF_Z), # processing: north hall and the central block (-9.20, -5.40, 13.63, 28.23, ROOF_Z), # processing: west (security) room (5.40, 9.20, 13.63, 18.63, ROOF_Z), # processing: south-east room (5.40, 12.40, 18.03, ARRIVALS_START, ROOF_Z), # processing: east rooms (-QUAY_ROOM_END, QUAY_ROOM_END, hy - HUB, hy + HUB, HUB_ROOF_Z), # dock hub: dock head and both quay links (-4.75, 4.75, by, hy - HUB, ROOF_Z), # boarding bridge; its end is open ] bays = [(-37.0, -14.40, -14.60, 14.60, CC_CAP_Z0)] # Relay Control closes the west hall's end d = G_DOOR openings = [ ("x", 20.43, -d, d, 0.0, STOREY), # maintenance door, east end of cargo ("y", by, -4.45, 4.35, 0.0, STOREY), # boarding bridge, open to the hull ("y", -HUB, -d, d, 0.0, STOREY), # crossing hub -> mooring gallery ("y", hy + HUB, -d, d, 0.0, STOREY), # dock hub -> mooring gallery ("x", QUAY_ROOM_END, hy - d, hy + d, 0.0, STOREY), # dock hub -> east quay ("x", -QUAY_ROOM_END, hy - d, hy + d, 0.0, STOREY), # dock hub -> west quay ("y", ARRIVALS_START, -d, d, 0.0, STOREY), # processing -> arrivals lock ] # name, axis it runs along, the other coordinate of its axis, from, to, what stands at each end: # ("room", half width of the wall it meets on the low side, on the high side) or ("door",) galleries = [ ("arrivals lock", "y", 0.0, ARRIVALS_START, 40.36, ("room", 5.40, 12.40), ("door",)), ("mooring gallery", "y", 0.0, hy + HUB, -HUB, ("room", 99.0, 99.0), ("room", 99.0, 99.0)), ("east quay", "x", hy, QUAY_ROOM_END, qe, ("room", HUB, HUB), ("door",)), ("west quay", "x", hy, -qe, -QUAY_ROOM_END, ("door",), ("room", HUB, HUB)), ] return rooms, bays, openings, galleries CASING = 0.03 # half width of the cover strip round an outer doorway CASING_PROUD = 0.006 # and how far it stands in front of the wall CASING_SEAL = 0.12 # at a door with a leaf: how far the head strip's underside reaches back behind the wall def door_casing(mb, point, w0, w1, z0, z1, outward, seal=0.0): """A dark cover strip round an outer doorway, 6 mm in front of the wall. The wall's opening and the kit's door frame meet edge to edge, as two separate meshes; without a strip over the joint a renderer may show the room behind as a line of stray pixels along it. `point(w, z)` gives a point of the wall plane (the strip's). seal: at a doorway whose leaf lifts, the strip over the head gets an underside this deep (2026-10-14). The kit's frame stands 2 to 5 cm behind the wall and is open at its top, so from under the doorway one saw up past the strip into the hollow over the frame, and the leaf going up through it (build/check_doors.py, from 1 m away).""" e = CASING for a0, a1, c0, c1 in ((w0 - e, w0 + e, z0 - e, z1 + e), (w1 - e, w1 + e, z0 - e, z1 + e), (w0 + e, w1 - e, z1 - e, z1 + e), (w0 + e, w1 - e, z0 - e, z0 + e)): q = [(a0, c0), (a1, c0), (a1, c1), (a0, c1)] mb.poly([point(w, z) for w, z in q], [(w / TILE, z / TILE) for w, z in q], BASE, outward, inner=False) if seal > 0.0: back = CASING_PROUD + seal front = [point(w0 - e, z1 - e), point(w1 + e, z1 - e)] rear = [tuple(c - o * back for c, o in zip(p, outward)) for p in reversed(front)] uv = [((w0 - e) / TILE, 0.0), ((w1 + e) / TILE, 0.0), ((w1 + e) / TILE, back / TILE), ((w0 - e) / TILE, back / TILE)] mb.poly(front + rear, uv, BASE, (0.0, 0.0, -1.0), inner=False) def build_rooms(mb, b): rooms, bays, openings, galleries = station_plan(b) union_shell(mb, rooms, bays, openings, z_levels=(0.0, STOREY, ROOF_Z), roof_slots=door_slots()) # the two openings of this mesh that are seen from outside: the maintenance door and the boarding bridge's end by = b.BRIDGE_END_Y door_casing(mb, lambda w, z: (20.43 + CASING_PROUD, w, z), -G_DOOR, G_DOOR, 0.0, STOREY, (1.0, 0.0, 0.0), seal=CASING_SEAL) door_casing(mb, lambda w, z: (w, by - CASING_PROUD, z), -4.45, 4.35, 0.0, STOREY, (0.0, -1.0, 0.0)) # --------------------------------------------------------------------------------------------------------------- class _Side(object): """One side of a gallery: s runs along its axis, w away from it, z up.""" def __init__(self, axis, c, side): self.axis, self.c, self.side = axis, c, side def p(self, s, w, z): return (self.c + self.side * w, s, z) if self.axis == "y" else (s, self.c + self.side * w, z) def lateral(self): return (float(self.side), 0.0, 0.0) if self.axis == "y" else (0.0, float(self.side), 0.0) def along(self, sign): return (0.0, float(sign), 0.0) if self.axis == "y" else (float(sign), 0.0, 0.0) def _w_out(z): """The envelope's outer line below the knee (the piers' face).""" return WB + (z - ZS) * SLOPE def _w_frame(z): """The window frames' outer face.""" return G_FOOT[0] + (z - ZS) * SLOPE def gallery_frames(axis, c, s0, s1): """Where the kit's window frames stand along a gallery, read from the file: {side: [(from, to), ...]}.""" out = {1: [], -1: []} for ob in bpy.data.objects: if ob.type != "MESH" or not str(ob.data.get("ue_mesh", "")).endswith("/SM_Win_Frame_A") \ or ob.get("anchorage_kit_source"): continue pts = [ob.matrix_world @ Vector(cn) for cn in ob.bound_box] lat = [p.x for p in pts] if axis == "y" else [p.y for p in pts] alo = [p.y for p in pts] if axis == "y" else [p.x for p in pts] lc = (min(lat) + max(lat)) / 2.0 - c if not 3.0 < abs(lc) < 7.5 or min(alo) < s0 - 0.05 or max(alo) > s1 + 0.05: continue if abs(max(abs(v - c) for v in lat) - G_HEAD[0]) > 0.02: print("[anchorage] WARNING: window frame %s is not where the envelope expects it" % ob.name) out[1 if lc > 0 else -1].append((round(min(alo), 3), round(max(alo), 3))) for k in out: out[k].sort() return out def _gallery_side(mb, sd, s0, s1, frames, end0, end1): lat, up, down = sd.lateral(), (0.0, 0.0, 1.0), (0.0, 0.0, -1.0) lean = (lat[0], lat[1], -SLOPE) # the leaning faces look outwards and down def add(pts, kind, mat, outward): if kind == "wall": uv = [(s / TILE, z / TILE) for s, w, z in pts] elif kind == "lean": uv = [(s / TILE, (ZS + (z - ZS) * SLANT_LEN) / TILE) for s, w, z in pts] elif kind == "flat": uv = [(s / TILE, w / TILE) for s, w, z in pts] else: # "end": a face across the gallery uv = [(w / TILE, z / TILE) for s, w, z in pts] mb.poly([sd.p(*q) for q in pts], uv, mat, outward) cuts = sorted(set([s0, s1] + [v for f in frames for v in f if s0 + 1e-3 < v < s1 - 1e-3])) spans = list(zip(cuts[:-1], cuts[1:])) kinds = ["frame" if any(f[0] - 1e-3 <= a and c <= f[1] + 1e-3 for f in frames) else "pier" for a, c in spans] last = len(spans) - 1 def cap_start(end): """Height at which the gallery becomes wider than the wall it meets, or None if it never does.""" if end[0] != "room": return None h = end[1 if sd.side < 0 else 2] if h >= WK - 1e-3: return None return ZS + (max(h, WB) - WB) / SLOPE for i, (a, c) in enumerate(spans): add([(a, WB, BASE_Z), (c, WB, BASE_Z), (c, WB, 0.0), (a, WB, 0.0)], "wall", BASE, lat) add([(a, WB, 0.0), (c, WB, 0.0), (c, WB, ZS), (a, WB, ZS)], "wall", WALL, lat) add([(a, WK, ZK), (c, WK, ZK), (c, WK, STOREY), (a, WK, STOREY)], "wall", WALL, lat) add([(a, WK, STOREY), (c, WK, STOREY), (c, WK, G_SOFFIT), (a, WK, G_SOFFIT)], "wall", WALL, lat) if kinds[i] == "frame": # a ledge under the frame and a soffit over it: both end exactly on the frame's own edges add([(a, G_FOOT[0], ZS), (c, G_FOOT[0], ZS), (c, WB, ZS), (a, WB, ZS)], "flat", WALL, up) add([(a, G_HEAD[0], ZK), (c, G_HEAD[0], ZK), (c, WK, ZK), (a, WK, ZK)], "flat", WALL, down) continue zs = {ZS, ZK} if i == 0 and cap_start(end0) is not None: zs.add(round(cap_start(end0), 4)) if i == last and cap_start(end1) is not None: zs.add(round(cap_start(end1), 4)) zs = sorted(z for z in zs if ZS - 1e-6 <= z <= ZK + 1e-6) for z0, z1 in zip(zs[:-1], zs[1:]): add([(a, _w_out(z0), z0), (c, _w_out(z0), z0), (c, _w_out(z1), z1), (a, _w_out(z1), z1)], "lean", WALL, lean) # the pier's cheeks, where a window frame stands next to it if i > 0 and kinds[i - 1] == "frame": add([(a, _w_frame(z0), z0), (a, _w_out(z0), z0), (a, _w_out(z1), z1), (a, _w_frame(z1), z1)], "end", WALL, sd.along(-1)) if i < last and kinds[i + 1] == "frame": add([(c, _w_frame(z0), z0), (c, _w_out(z0), z0), (c, _w_out(z1), z1), (c, _w_frame(z1), z1)], "end", WALL, sd.along(1)) for s_end, sign, end in ((s0, -1, end0), (s1, 1, end1)): n = sd.along(sign) if end[0] == "room": zst = cap_start(end) if zst is None: continue # the wall it meets is wider than the gallery hh = max(end[1 if sd.side < 0 else 2], WB) # the gallery is wider than that wall above zst: its end shows there add([(s_end, hh, zst), (s_end, WK, ZK), (s_end, WK, STOREY), (s_end, hh, STOREY)], "end", WALL, n) add([(s_end, hh, STOREY), (s_end, WK, STOREY), (s_end, WK, G_SOFFIT), (s_end, hh, G_SOFFIT)], "end", WALL, n) else: # end wall beside the kit's doorway, whose own front fills the opening d = G_DOOR add([(s_end, d, BASE_Z), (s_end, WB, BASE_Z), (s_end, WB, 0.0), (s_end, d, 0.0)], "end", BASE, n) add([(s_end, d, 0.0), (s_end, WB, 0.0), (s_end, WB, ZS), (s_end, d, ZS)], "end", WALL, n) add([(s_end, d, ZS), (s_end, WB, ZS), (s_end, WK, ZK), (s_end, d, ZK)], "end", WALL, n) add([(s_end, d, ZK), (s_end, WK, ZK), (s_end, WK, STOREY), (s_end, d, STOREY)], "end", WALL, n) add([(s_end, d, STOREY), (s_end, WK, STOREY), (s_end, WK, G_SOFFIT), (s_end, d, G_SOFFIT)], "end", WALL, n) if sd.side > 0: # under and over the doorway, once for both sides add([(s_end, -d, BASE_Z), (s_end, d, BASE_Z), (s_end, d, 0.0), (s_end, -d, 0.0)], "end", BASE, n) add([(s_end, -d, STOREY), (s_end, d, STOREY), (s_end, d, G_SOFFIT), (s_end, -d, G_SOFFIT)], "end", WALL, n) door_casing(mb, lambda w, z, s=s_end + sign * CASING_PROUD: sd.p(s, w, z), -d, d, 0.0, STOREY, n, seal=CASING_SEAL) return kinds, spans def build_galleries(mb, b): rooms, bays, openings, galleries = station_plan(b) for name, axis, c, s0, s1, end0, end1 in galleries: frames = gallery_frames(axis, c, s0, s1) for side in (1, -1): kinds, spans = _gallery_side(mb, _Side(axis, c, side), s0, s1, frames[side], end0, end1) rhythm = " ".join("%s%.2f" % ("W" if k == "frame" else "p", c2 - a2) for k, (a2, c2) in zip(kinds, spans)) print("[anchorage] gallery %-16s %d + %d window frames; along it: %s" % ( name, len(frames[1]), len(frames[-1]), rhythm)) # roof slab, with an eave over the outer door; where a door leaf lifts through it, it is laid round the slot a = s0 - (G_EAVE if end0[0] == "door" else 0.0) e = s1 + (G_EAVE if end1[0] == "door" else 0.0) def slab(sa, sb, wa, wb): if sb - sa < 1e-4 or wb - wa < 1e-4: return if axis == "y": mb.box(c + wa, c + wb, sa, sb, G_SOFFIT, G_ROOF, WALL, ROOF) else: mb.box(sa, sb, c + wa, c + wb, G_SOFFIT, G_ROOF, WALL, ROOF) cuts = [] for x0, x1, y0, y1 in door_slots(): sa, sb, wa, wb = (y0, y1, x0 - c, x1 - c) if axis == "y" else (x0, x1, y0 - c, y1 - c) if sa > a - 1e-3 and sb < e + 1e-3 and wa > -WS and wb < WS: cuts.append((sa, sb, wa, wb)) at = a for sa, sb, wa, wb in sorted(cuts): slab(at, sa, -WS, WS) # the roof up to the slot slab(sa, sb, -WS, wa) # beside it slab(sa, sb, wb, WS) at = sb slab(at, e, -WS, WS) # --------------------------------------------------------------------------------------------------------------- def relay_outline(): """Relay Control's wall line round its square half, counter-clockwise from the south step to the north step.""" sx = CC[0] - CC_STEP yd = math.sqrt(CC_BAND_R ** 2 - CC_STEP ** 2) # where the step walls meet the drum's upper band return [(sx, -yd), (sx, -14.50), (-18.60, -14.50), (-14.40, -10.30), (-14.40, 10.30), (-18.60, 14.50), (sx, 14.50), (sx, yd)] def build_relay(mb, west_hall_half_width=4.70): cx, cy = CC wall = relay_outline() centre = (-20.0, 0.0) # ---- walls round the square half, cut at the datums; the west hall's shell butts against the east wall zs = [CC_BASE_Z, 0.0, STOREY, ROOF_Z, CC_CAP_Z0] run = 0.0 for (ax, ay), (bx, by) in zip(wall[:-1], wall[1:]): length = math.hypot(bx - ax, by - ay) n = max(1, int(math.ceil(length / 4.4 - 1e-6))) cuts = [i / n for i in range(n + 1)] east = abs(ax + 14.40) < 1e-6 and abs(bx + 14.40) < 1e-6 if east: # cut where the hall meets it for yy in (-west_hall_half_width, west_hall_half_width): cuts.append((yy - ay) / (by - ay)) cuts = sorted(set(round(c, 6) for c in cuts)) mid = ((ax + bx) / 2.0, (ay + by) / 2.0) outward = (mid[0] - centre[0], mid[1] - centre[1], 0.0) # a wall's outward direction is its own normal, on the side away from the room's middle nrm = (by - ay, -(bx - ax), 0.0) if nrm[0] * outward[0] + nrm[1] * outward[1] < 0.0: nrm = (-nrm[0], -nrm[1], 0.0) for t0, t1 in zip(cuts[:-1], cuts[1:]): p0 = (ax + (bx - ax) * t0, ay + (by - ay) * t0) p1 = (ax + (bx - ax) * t1, ay + (by - ay) * t1) u0, u1 = (run + length * t0) / TILE, (run + length * t1) / TILE for z0, z1 in zip(zs[:-1], zs[1:]): if east and min(p0[1], p1[1]) >= -west_hall_half_width - 1e-6 and \ max(p0[1], p1[1]) <= west_hall_half_width + 1e-6 and z1 <= ROOF_Z + 1e-6: continue # the hall's cross-section: left open mat = BASE if z1 <= 1e-3 else WALL mb.poly([(p0[0], p0[1], z0), (p1[0], p1[1], z0), (p1[0], p1[1], z1), (p0[0], p0[1], z1)], [(u0, z0 / TILE), (u1, z0 / TILE), (u1, z1 / TILE), (u0, z1 / TILE)], mat, nrm) run += length # ---- roof cap: a slab over the whole room with a round opening over the kit's skylight ---------------------- a_end = math.atan2(math.sqrt(CC_CAP_R ** 2 - CC_STEP ** 2), -CC_STEP) # where the round edge meets the step outline = [(cx - CC_STEP, -math.sqrt(CC_CAP_R ** 2 - CC_STEP ** 2))] for (ax, ay), (bx, by) in zip(wall[1:-2], wall[2:-1]): for t in _split(0.0, 1.0, 3.2 / max(math.hypot(bx - ax, by - ay), 1e-6))[:-1]: outline.append((ax + (bx - ax) * t, ay + (by - ay) * t)) outline.append(wall[-2]) outline.append((cx - CC_STEP, math.sqrt(CC_CAP_R ** 2 - CC_STEP ** 2))) n_arc = 30 for i in range(1, n_arc): a = a_end + (2.0 * math.pi - 2.0 * a_end) * i / n_arc outline.append((cx + CC_CAP_R * math.cos(a), cy + CC_CAP_R * math.sin(a))) ring = [] for px, py in outline: a = math.atan2(py - cy, px - cx) r = math.hypot(px - cx, py - cy) inner = (cx + CC_SKYLIGHT_R * math.cos(a), cy + CC_SKYLIGHT_R * math.sin(a)) mid = (cx + (CC_SKYLIGHT_R + r) / 2 * math.cos(a), cy + (CC_SKYLIGHT_R + r) / 2 * math.sin(a)) ring.append((inner, mid, (px, py))) m = len(ring) def uvp(p): return (p[0] / TILE, p[1] / TILE) for k in range(m): a, c = ring[k], ring[(k + 1) % m] for lo, hi in ((0, 1), (1, 2)): q = [a[lo], a[hi], c[hi], c[lo]] mb.poly([(p[0], p[1], CC_CAP_Z1) for p in q], [uvp(p) for p in q], ROOF, (0.0, 0.0, 1.0)) mb.poly([(p[0], p[1], CC_CAP_Z0) for p in q], [uvp(p) for p in q], WALL, (0.0, 0.0, -1.0)) # outer edge and the edge round the opening edge_uv = [(0, CC_CAP_Z0 / TILE), (0.5, CC_CAP_Z0 / TILE), (0.5, CC_CAP_Z1 / TILE), (0, CC_CAP_Z1 / TILE)] o0, o1 = a[2], c[2] om = ((o0[0] + o1[0]) / 2.0 - cx, (o0[1] + o1[1]) / 2.0 - cy, 0.0) mb.poly([(o0[0], o0[1], CC_CAP_Z0), (o1[0], o1[1], CC_CAP_Z0), (o1[0], o1[1], CC_CAP_Z1), (o0[0], o0[1], CC_CAP_Z1)], edge_uv, WALL, om) # the opening's wall comes down past the soffit to the skylight frame, so nobody looks in under the cap i0, i1 = a[0], c[0] im = (cx - (i0[0] + i1[0]) / 2.0, cy - (i0[1] + i1[1]) / 2.0, 0.0) for z0, z1 in ((CC_SKYLIGHT_Z, CC_CAP_Z0), (CC_CAP_Z0, CC_CAP_Z1)): mb.poly([(i0[0], i0[1], z0), (i1[0], i1[1], z0), (i1[0], i1[1], z1), (i0[0], i0[1], z1)], [(0, z0 / TILE), (0.5, z0 / TILE), (0.5, z1 / TILE), (0, z1 / TILE)], WALL, im) # ---- the drum's upper band carried on up to the cap, and a top on the ring over the window heads ------------ af = math.atan2(math.sqrt(CC_BAND_R ** 2 - CC_STEP ** 2), -CC_STEP) n = 36 for i in range(n): aa = af + (2.0 * math.pi - 2.0 * af) * i / n ab = af + (2.0 * math.pi - 2.0 * af) * (i + 1) / n ca, sa, cb, sb = math.cos(aa), math.sin(aa), math.cos(ab), math.sin(ab) ba, bb = (cx + CC_BAND_R * ca, cy + CC_BAND_R * sa), (cx + CC_BAND_R * cb, cy + CC_BAND_R * sb) ra, rb = (cx + CC_DRUM_R * ca, cy + CC_DRUM_R * sa), (cx + CC_DRUM_R * cb, cy + CC_DRUM_R * sb) ua, ub = aa * CC_BAND_R / TILE, ab * CC_BAND_R / TILE om = ((ba[0] + bb[0]) / 2.0 - cx, (ba[1] + bb[1]) / 2.0 - cy, 0.0) mb.poly([(ba[0], ba[1], CC_BAND_TOP), (bb[0], bb[1], CC_BAND_TOP), (bb[0], bb[1], CC_CAP_Z0), (ba[0], ba[1], CC_CAP_Z0)], [(ua, CC_BAND_TOP / TILE), (ub, CC_BAND_TOP / TILE), (ub, CC_CAP_Z0 / TILE), (ua, CC_CAP_Z0 / TILE)], WALL, om) mb.poly([(ba[0], ba[1], CC_CORNICE_Z), (ra[0], ra[1], CC_CORNICE_Z), (rb[0], rb[1], CC_CORNICE_Z), (bb[0], bb[1], CC_CORNICE_Z)], [uvp(ba), uvp(ra), uvp(rb), uvp(bb)], WALL, (0.0, 0.0, 1.0)) # ---- footing ring under the window drum, with a ledge back to the drum's foot ------------------------------- a0 = math.atan2(math.sqrt(CC_PLINTH_R ** 2 - CC_STEP ** 2), -CC_STEP) n = 36 top = 0.12 for i in range(n): aa = a0 + (2.0 * math.pi - 2.0 * a0) * i / n ab = a0 + (2.0 * math.pi - 2.0 * a0) * (i + 1) / n pa = (cx + CC_PLINTH_R * math.cos(aa), cy + CC_PLINTH_R * math.sin(aa)) pb = (cx + CC_PLINTH_R * math.cos(ab), cy + CC_PLINTH_R * math.sin(ab)) da = (cx + CC_DRUM_R * math.cos(aa), cy + CC_DRUM_R * math.sin(aa)) db = (cx + CC_DRUM_R * math.cos(ab), cy + CC_DRUM_R * math.sin(ab)) ua, ub = aa * CC_PLINTH_R / TILE, ab * CC_PLINTH_R / TILE om = ((pa[0] + pb[0]) / 2.0 - cx, (pa[1] + pb[1]) / 2.0 - cy, 0.0) for z0, z1 in ((CC_BASE_Z, BASE_Z), (BASE_Z, top)): mb.poly([(pa[0], pa[1], z0), (pb[0], pb[1], z0), (pb[0], pb[1], z1), (pa[0], pa[1], z1)], [(ua, z0 / TILE), (ub, z0 / TILE), (ub, z1 / TILE), (ua, z1 / TILE)], BASE, om) mb.poly([(pa[0], pa[1], top), (pb[0], pb[1], top), (db[0], db[1], top), (da[0], da[1], top)], [uvp(pa), uvp(pb), uvp(db), uvp(da)], BASE, (0.0, 0.0, 1.0)) # --------------------------------------------------------------------------------------------------------------- def build_decks(mb, b): """Outside the two doors that open to the ice: a deck level with the threshold and a ramp down to the ice. Each deck keeps its face towards the building: the arrivals deck is wider than the gallery's base wall, so that face shows at both ends (left out at first, which opened the deck's hollow at two corners; the check found it). Only the bottom and the face under the ramp are left out.""" lo = BASE_Z # arrivals: where the convict is set down y0, y1, y2, hw = 40.36, 47.40, 51.60, 5.20 mb.box(-hw, hw, y0, y1, lo, DECK_Z, BASE, BASE, drop=("bottom", "+y")) mb.poly([(-hw, y1, DECK_Z), (hw, y1, DECK_Z), (hw, y2, lo), (-hw, y2, lo)], [(-hw / TILE, y1 / TILE), (hw / TILE, y1 / TILE), (hw / TILE, y2 / TILE), (-hw / TILE, y2 / TILE)], BASE, (0.0, 0.2, 1.0)) for sx in (-1.0, 1.0): mb.poly([(sx * hw, y1, lo), (sx * hw, y2, lo), (sx * hw, y1, DECK_Z)], [(y1 / TILE, lo / TILE), (y2 / TILE, lo / TILE), (y1 / TILE, DECK_Z / TILE)], BASE, (sx, 0.0, 0.0)) # maintenance door: the robots' way out, east end of the cargo hall x0, x1, x2, hw = 20.43, 25.00, 29.20, 5.00 mb.box(x0, x1, -hw, hw, lo, DECK_Z, BASE, BASE, drop=("bottom", "+x")) mb.poly([(x1, -hw, DECK_Z), (x1, hw, DECK_Z), (x2, hw, lo), (x2, -hw, lo)], [(x1 / TILE, -hw / TILE), (x1 / TILE, hw / TILE), (x2 / TILE, hw / TILE), (x2 / TILE, -hw / TILE)], BASE, (0.2, 0.0, 1.0)) for sy in (-1.0, 1.0): mb.poly([(x1, sy * hw, lo), (x2, sy * hw, lo), (x1, sy * hw, DECK_Z)], [(x1 / TILE, lo / TILE), (x2 / TILE, lo / TILE), (x1 / TILE, DECK_Z / TILE)], BASE, (0.0, sy, 0.0)) def build_roof_services(mb, b): """Ducts over the corridors, meeting in a plenum on each junction block. They rest on the roofs: no bottom faces, and no end face where a duct runs into a block's wall.""" hy = b.DOCK_HEAD_Y z0, z1 = ROOF_Z, ROOF_Z + 0.40 h0, h1 = HUB_ROOF_Z, HUB_ROOF_Z + 0.70 mb.box(-2.2, 2.2, -2.2, 2.2, h0, h1, WALL, ROOF, drop=("bottom",)) # crossing plenum mb.box(-0.8, 0.8, HUB, 32.4, z0, z1, WALL, ROOF, drop=("bottom", "-y")) # north, over processing mb.box(-13.9, -WS, -0.8, 0.8, z0, z1, WALL, ROOF, drop=("bottom", "+x")) # west hall mb.box(WS, 19.9, -0.8, 0.8, z0, z1, WALL, ROOF, drop=("bottom", "-x")) # cargo hall mb.box(-1.6, 1.6, hy - 1.6, hy + 1.6, h0, h1, WALL, ROOF, drop=("bottom",)) # dock plenum mb.box(-0.8, 0.8, b.BRIDGE_END_Y + 0.6, hy - HUB, z0, z1, WALL, ROOF, drop=("bottom", "+y")) for sx in (-1.0, 1.0): # along the dock hub xa, xb = sorted((sx * 1.6, sx * (QUAY_ROOM_END - 0.5))) mb.box(xa, xb, hy - 0.8, hy + 0.8, h0, h0 + 0.40, WALL, ROOF, drop=("bottom", "-x" if sx > 0 else "+x")) # two air handlers on the east rooms of processing, set out on the duct's line for y in (21.5, 28.5): mb.box(7.0, 10.6, y - 1.3, y + 1.3, z0, ROOF_Z + 1.10, WALL, ROOF, drop=("bottom",)) def add_relay_dish(b, col): """The relay on Relay Control's roof: the Sci-Fi Lite pack's satellite, three kit pieces stacked as the pack's own overview map stacks them. Puts them there, or puts them back where the recipe has them: pieces of it already in the file are replaced, and so is the mast of my own that stood there before (ANCH_Relay_Mast). A piece that is already there keeps the way it is turned: the yoke and the dish are meant to be posed by hand.""" gone, posed = [], {} for ob in list(bpy.data.objects): if ob.get("anchorage_module") == RELAY_DISH_MODULE or ob.name == "ANCH_Relay_Mast": me = ob.data gone.append(ob.name) if ob.get("ue_role"): posed[ob["ue_role"]] = ob.matrix_world.to_euler("XYZ") bpy.data.objects.remove(ob) if me is not None and me.users == 0: bpy.data.meshes.remove(me) x, y = RELAY_DISH_AT z = CC_CAP_Z1 - RELAY_DISH_FEET # the feet stand on the roof placed = [] for mesh, up, what, rot in RELAY_DISH_PARTS: rot = posed.get("relay_" + what, rot) mat = Matrix.Translation((x, y, z + up)) @ Matrix.Rotation(rot[2], 4, "Z") @ Matrix.Rotation(rot[1], 4, "Y") \ @ Matrix.Rotation(rot[0], 4, "X") ob = b.kit.place(LITE + mesh, mat, col, name="Relay dish | %s" % what) if ob is None: print("[anchorage] WARNING: %s is not in the kit export (kit_export_lite): the relay dish is incomplete" % mesh) continue ob["anchorage_zone"] = ZONE ob["anchorage_module"] = RELAY_DISH_MODULE ob["ue_role"] = "relay_" + what # base stands, the yoke turns, the dish tips: separate pieces placed.append(ob) if placed: top = max((o.matrix_world @ Vector(c)).z for o in placed for c in o.bound_box) print("[anchorage] relay dish: %d pieces of the Sci-Fi Lite pack at (%.1f, %.1f), from the roof at %.2f m up to " "%.2f m%s" % (len(placed), x, y, CC_CAP_Z1, top, ("; replaced: " + ", ".join(gone)) if gone else "")) return placed def add_skylight_frame(b, col): """The kit's skylight frame over Relay Control is one-sided, made to be seen from the room: from the roof one looked through its bars. A second instance of the same kit mesh, mirrored upwards, shows the frame from above. It is a kit piece like any other (the layout carries its mirrored matrix).""" src = None for ob in bpy.data.objects: if ob.type == "MESH" and str(ob.data.get("ue_mesh", "")).endswith("/SM_Ceiling_Window_A") \ and "relay" in str(ob.get("anchorage_module", "")): src = ob break if src is None: print("[anchorage] WARNING: Relay Control's skylight frame not found; none added on the roof side") return None top = max((src.matrix_world @ Vector(c)).z for c in src.bound_box) if abs(2.0 * top - min((src.matrix_world @ Vector(c)).z for c in src.bound_box) - CC_SKYLIGHT_Z) > 0.02: print("[anchorage] WARNING: the skylight frame is not at the height the roof cap expects") m = Matrix.Translation((0.0, 0.0, 2.0 * top)) @ Matrix.Scale(-1.0, 4, (0.0, 0.0, 1.0)) @ src.matrix_world over = {i: s.material["ue_material"] for i, s in enumerate(src.material_slots) if s.link == "OBJECT" and s.material is not None and s.material.get("ue_material")} ob = b.kit.place(src.data["ue_mesh"], m, col, name="Skylight frame, roof side (kit piece, mirrored)", overrides=over) ob["anchorage_zone"] = ZONE ob["anchorage_module"] = "exterior: skylight frame" return ob # --------------------------------------------------------------------------------------------------------------- PARTS = ( ("ANCH_Shell_Rooms", "shell of the windowless rooms: walls and roofs, one welded mesh"), ("ANCH_Shell_Galleries", "window galleries: base walls, piers between the kit's window frames, frieze, roof slabs, end walls"), ("ANCH_Shell_Relay", "Relay Control: roof cap with skylight opening, walls of its square half, footing ring"), ("ANCH_Decks", "landing deck outside the arrivals door and apron outside the maintenance door, each with a ramp"), ) # (ANCH_Roof_Services, the first exterior's box ducts, is gone: build/outpost_more.py roof_works stands there now) def refresh(b, names=("ANCH_Shell_Rooms", "ANCH_Shell_Galleries")): """New meshes for parts of the shell that are already in the open file, in place: the objects and everything else in the file stay as they are.""" own_materials.register(b.kit) done = [] for name in names: ob = bpy.data.objects.get(name) if ob is None: continue mb = MeshBuilder() if name == "ANCH_Shell_Rooms": build_rooms(mb, b) elif name == "ANCH_Shell_Galleries": build_galleries(mb, b) elif name == "ANCH_Shell_Relay": build_relay(mb) elif name == "ANCH_Decks": build_decks(mb, b) else: continue old = ob.data ob.data = mb.build(name, b.kit, inner_skin=name.startswith("ANCH_Shell_")) if old.users == 0: bpy.data.meshes.remove(old) ob.data.name = name done.append("%s (%d faces)" % (name, len(ob.data.polygons))) print("[anchorage] exterior: new meshes in place for %s" % ", ".join(done)) return done def add_exterior(b): """Adds or replaces the exterior in the open file. `b` is the Builder (it carries the kit and the plan's numbers).""" kit = b.kit own_materials.register(kit) # the walls' dark metal is an instance of the station's own station = get_collection("ANCHORAGE STATION") col = get_collection(COLLECTION, station) for ob in list(col.objects): me = ob.data bpy.data.objects.remove(ob) if me is not None and me.users == 0: bpy.data.meshes.remove(me) bpy.context.view_layer.update() builders = {} for name, note in PARTS: builders[name] = MeshBuilder() build_rooms(builders["ANCH_Shell_Rooms"], b) build_galleries(builders["ANCH_Shell_Galleries"], b) build_relay(builders["ANCH_Shell_Relay"]) build_decks(builders["ANCH_Decks"], b) report = {} for name, note in PARTS: me = builders[name].build(name, kit, inner_skin=name.startswith("ANCH_Shell_")) ob = bpy.data.objects.new(name, me) col.objects.link(ob) ob["anchorage_zone"] = ZONE ob["anchorage_module"] = "exterior" ob["anchorage_custom_mesh"] = name ob["anchorage_note"] = note # the decks are walked on and convex: they get hulls. Everything else is one-sided wall: the mesh itself. ob["anchorage_collision"] = "ucx" if name == "ANCH_Decks" else "complex" tris = sum(len(p.vertices) - 2 for p in me.polygons) report[name] = "%d faces, %d tris" % (len(me.polygons), tris) add_skylight_frame(b, col) print("[anchorage] exterior: %s" % report) add_relay_dish(b, col) return col