""" The outpost: armour, structure and equipment over the station's plain shell. The user, 2026-10-14, with three reference pictures: "The exterior buildings and structures are too flat, too basic, looks like 95 game do it more versatile, stylized, try to do like an actual outpost." The pictures show white armour plates with clipped corners standing off a dark frame, orange as bands and edges, machinery in the gaps, aerials in clusters, glowing strips, pads with orange rings. add_outpost(b) adds (or replaces) the collection "10 OUTPOST armour and works" in the open file, in place How it is put together * The shell of build/exterior.py stays as the closed hull (it is what keeps anyone from looking into the rooms) and turns dark. Everything here stands OUTSIDE it and only adds: nothing of the hull is cut or opened. * Every outer wall is read from the plan as a run with the outside to its right; the runs are linked at their corners. On that one description hang, the same on every building: plinth a battered dark base, drawn along the linked runs armour white plates in two rows (three on Relay Control), 12 to 16 cm proud, 7 cm apart, corners clipped; a row's end plates are orange with a long clip; long walls have a louvred plate service band between the rows the dark hull shows, with two pipes on brackets parapet a white fascia with an orange underside and a dark coping, drawn along the roof's edge pylons an eight-sided tower on every outer corner, above the roofline, with light strips, an orange cap and an aerial; a pier where a wall ends against a gallery * The window galleries get fins on their piers, a visor over the windows and a base course; the outer doors get a portal; Relay Control's drum gets a crown of canted armour. See the functions. * Set out by rule from the plan, not by chance: the same input gives the same outpost. The only variety that is not from the plan is the aerials' lengths, which follow a fixed list. Materials: the Sci-Fi Lite pack's painted metals and three instances of them (build/own_materials.py). All of it is new geometry in quads with chamfered edges (build/hs.py); build/export_custom.py writes it as FBX. """ import math import bpy from mathutils import Matrix, Vector import exterior as ext import own_materials from bl_kit import get_collection from hs import Frame, Part, box, clipped, cylinder, loft, prism, rect, ring, sweep, tube COLLECTION = "10 OUTPOST armour and works" ZONE = "10 OUTPOST" BASE = ext.BASE_Z # -0.80: where the walls go into the ice PLINTH_TOP = 0.35 ROW_LOW = (0.47, 2.32) # the lower row of plates ROW_UP0 = 2.98 # the upper row begins here; between the rows is the service band PARAPET_DROP = 0.42 # the fascia hangs this far below the roof's edge PARAPET_UP = 0.44 # and the coping stands this far above it PLATE = 2.2 # the plates' module: half a bay of the kit GAP = 0.07 KEEP_SOUTH = -35.20 # south of this line is the vessel: nothing may reach past it DRUM_CLEAR = 12.70 # Relay Control's step walls are armoured from here outwards (the crown's rim: 12.5) AERIALS = (3.4, 4.6, 2.6, 5.2, 3.0, 4.0, 2.8, 3.8) # lengths of the aerials on the pylons, in turn # --------------------------------------------------------------------------------------------------------------- # the plan as runs class Run(object): """A stretch of outer wall on the plan, from p0 to p1 with the outside to the right; from z0 up to z1.""" def __init__(self, p0, p1, z0, z1, what): self.p0, self.p1 = Vector((p0[0], p0[1])), Vector((p1[0], p1[1])) self.z0, self.z1, self.what = z0, z1, what d = self.p1 - self.p0 self.len = d.length self.dir = d / self.len self.out = Vector((self.dir.y, -self.dir.x)) self.start = self.end = "free" # how it ends: "convex", "concave", "straight", "free", "keep" self.prev = self.next = None self.door = None # (a0, a1, top) along the run: a doorway in it self.keepout = False # nothing at all on this run (the boarding bridge's end, at the hull) self.base = BASE @property def ground(self): return self.z0 <= BASE + 1e-3 def frame(self, proud=0.0): o = self.p0 + self.out * proud return Frame((o.x, o.y, 0.0), (self.dir.x, self.dir.y, 0.0), (0.0, 0.0, 1.0), (self.out.x, self.out.y, 0.0)) def at(self, a, n=0.0): p = self.p0 + self.dir * a + self.out * n return (p.x, p.y) def __repr__(self): return "<%s (%.2f, %.2f) -> (%.2f, %.2f) %.2f m, z %.2f..%.2f, %s|%s>" % ( self.what, self.p0.x, self.p0.y, self.p1.x, self.p1.y, self.len, self.z0, self.z1, self.start, self.end) def _key(p): return (round(p[0], 2), round(p[1], 2)) def plan_runs(b): """Every exposed stretch of the outer walls of the rooms and of Relay Control's square half.""" rooms, bays, openings, galleries = ext.station_plan(b) block = [tuple(r) for r in bays] for name, axis, c, s0, s1, e0, e1 in galleries: block.append((c - ext.WS, c + ext.WS, s0, s1, ext.G_ROOF) if axis == "y" else (s0, s1, c - ext.WS, c + ext.WS, ext.G_ROOF)) xs = sorted(set(round(v, 3) for r in list(rooms) + block for v in r[:2])) ys = sorted(set(round(v, 3) for r in list(rooms) + block for v in r[2:4])) def height(boxes, cx, cy): h = [r[4] for r in boxes if r[0] < cx < r[1] and r[2] < cy < r[3]] return max(h) if h else None pieces = [] for i in range(len(xs) - 1): for j in range(len(ys) - 1): cx, cy = (xs[i] + xs[i + 1]) / 2.0, (ys[j] + ys[j + 1]) / 2.0 h = height(rooms, cx, cy) if h is None: continue for axis, plane, a0, a1, out, (nx, ny) in ( ("x", xs[i + 1], ys[j], ys[j + 1], 1, (cx + (xs[i + 1] - xs[i]) / 2.0 + 0.01, cy)), ("x", xs[i], ys[j], ys[j + 1], -1, (cx - (xs[i + 1] - xs[i]) / 2.0 - 0.01, cy)), ("y", ys[j + 1], xs[i], xs[i + 1], 1, (cx, cy + (ys[j + 1] - ys[j]) / 2.0 + 0.01)), ("y", ys[j], xs[i], xs[i + 1], -1, (cx, cy - (ys[j + 1] - ys[j]) / 2.0 - 0.01))): nh, nb = height(rooms, nx, ny), height(block, nx, ny) if nh is not None and nh >= h - 1e-4: continue z0 = max(BASE, nh if nh is not None else BASE, nb if nb is not None else BASE) if z0 >= h - 1e-4: continue pieces.append([axis, round(plane, 3), a0, a1, round(z0, 3), h, out]) pieces.sort(key=lambda p: (p[0], p[1], p[6], p[4], p[5], p[2])) merged = [] for p in pieces: m = merged[-1] if merged else None if m and m[0] == p[0] and m[1] == p[1] and m[6] == p[6] and m[4] == p[4] and m[5] == p[5] and abs(m[3] - p[2]) < 1e-3: m[3] = p[3] else: merged.append(list(p)) runs = [] for axis, plane, a0, a1, z0, z1, out in merged: if axis == "x": p0, p1 = ((plane, a0), (plane, a1)) if out > 0 else ((plane, a1), (plane, a0)) else: p0, p1 = ((a1, plane), (a0, plane)) if out > 0 else ((a0, plane), (a1, plane)) r = Run(p0, p1, z0, z1, "room") for o in openings: if o[0] == axis and abs(o[1] - plane) < 2e-3 and o[2] >= a0 - 1e-3 and o[3] <= a1 + 1e-3 and r.ground: # along the run: its own coordinate runs from p0 pa = Vector((plane, o[2])) if axis == "x" else Vector((o[2], plane)) pb = Vector((plane, o[3])) if axis == "x" else Vector((o[3], plane)) lo, hi = sorted(((pa - r.p0).dot(r.dir), (pb - r.p0).dot(r.dir))) r.door = (lo, hi, o[5]) if axis == "y" and abs(plane - b.BRIDGE_END_Y) < 1e-3: r.keepout = True runs.append(r) # Relay Control's square half: its wall line, counter-clockwise from the south step to the north step wall = ext.relay_outline() top = ext.CC_CAP_Z1 hw = 4.70 # the west hall's half width: it covers the east wall up to its roof step = ext.CC[0] - ext.CC_STEP for (ax, ay), (bx, by) in zip(wall[:-1], wall[1:]): if abs(ax + 14.40) < 1e-6 and abs(bx + 14.40) < 1e-6: for (y0, y1, z0) in ((ay, -hw, BASE), (-hw, hw, ext.ROOF_Z), (hw, by, BASE)): r = Run((ax, y0), (ax, y1), z0, top, "relay") runs.append(r) elif abs(ax - step) < 1e-6 and abs(bx - step) < 1e-6: # the step walls beside the drum: armoured only up to where the drum's crown comes round (DRUM_CLEAR) if abs(ay) < abs(by): runs.append(Run((ax, math.copysign(DRUM_CLEAR, ay)), (bx, by), BASE, top, "relay")) else: runs.append(Run((ax, ay), (bx, math.copysign(DRUM_CLEAR, by)), BASE, top, "relay")) else: runs.append(Run((ax, ay), (bx, by), BASE, top, "relay")) for r in runs: if r.what == "relay": r.base = ext.CC_BASE_Z if r.p0.x < -24.0 or r.p1.x < -24.0 else BASE # link the ground runs at their corners starts = {} for r in runs: if r.ground and not r.keepout: starts[_key(r.p0)] = r for r in runs: if not r.ground or r.keepout: continue n = starts.get(_key(r.p1)) if n is None or n is r: continue turn = r.dir.x * n.dir.y - r.dir.y * n.dir.x kind = "convex" if turn > 0.1 else ("concave" if turn < -0.1 else "straight") r.end, n.start = kind, kind r.next, n.prev = n, r # an end at the vessel's side, or at the drum's crown: nothing may be built beyond it for r in runs: if r.ground and not r.keepout: if r.end == "free" and (r.p1.y < KEEP_SOUTH or (r.what == "relay" and abs(r.p1.x - step) < 1e-6)): r.end = "keep" if r.start == "free" and (r.p0.y < KEEP_SOUTH or (r.what == "relay" and abs(r.p0.x - step) < 1e-6)): r.start = "keep" return runs def chains(runs, ok, same=None): """The runs for which ok(run) holds, in the order they follow each other: lists of runs that share end points. same(a, b): may b follow a in one chain (default: always).""" pool = [r for r in runs if ok(r)] starts = {_key(r.p0): r for r in pool} ends = {_key(r.p1): r for r in pool} out, used = [], set() def follows(a): n = starts.get(_key(a.p1)) if n is None or n is a or id(n) in used: return None return n if (same is None or same(a, n)) else None for r in pool: p = ends.get(_key(r.p0)) if p is not None and p is not r and (same is None or same(p, r)): continue # not a chain's first run chain = [r] used.add(id(r)) n = follows(r) while n is not None: chain.append(n) used.add(id(n)) n = follows(n) out.append((chain, False)) for r in pool: # what is left goes round in a ring if id(r) in used: continue chain = [r] used.add(id(r)) n = follows(r) while n is not None: chain.append(n) used.add(id(n)) n = follows(n) out.append((chain, True)) return out def chain_path(chain, closed): pts = [(r.p0.x, r.p0.y) for r in chain] if not closed: pts.append((chain[-1].p1.x, chain[-1].p1.y)) # drop points on a straight line keep = [] n = len(pts) for k, p in enumerate(pts): if not closed and k in (0, n - 1): keep.append(p) continue a, c = pts[k - 1], pts[(k + 1) % n] if abs((p[0] - a[0]) * (c[1] - p[1]) - (p[1] - a[1]) * (c[0] - p[0])) > 1e-6: keep.append(p) return keep # --------------------------------------------------------------------------------------------------------------- # what hangs on a run def plinth_profile(base): """(n, z) and one material per edge: bottom, face, slope, top, top, back.""" return ([(-0.02, base - 0.02), (0.30, base - 0.02), (0.30, 0.10), (0.17, PLINTH_TOP), (0.07, PLINTH_TOP), (-0.02, PLINTH_TOP)], ["concrete"] * 6) def parapet_profiles(drop): fascia = ([(-0.02, -drop), (0.24, -drop), (0.36, -drop + 0.12), (0.36, 0.0), (0.18, 0.0), (-0.02, 0.0)], ["orange", "orange", "white", "white", "white", "hull"]) coping = ([(-0.22, 0.0), (0.36, 0.0), (0.36, PARAPET_UP - 0.08), (0.28, PARAPET_UP), (-0.14, PARAPET_UP), (-0.22, PARAPET_UP - 0.08)], ["white", "white", "hull", "hull", "hull", "white"]) return fascia, coping def lift(profile, dz): return [(n, z + dz) for n, z in profile] def octagon(c, across, start=22.5): """Eight corners round c, its flats `across` apart and facing along the axes and the diagonals.""" r = across / 2.0 / math.cos(math.radians(22.5)) return [(c[0] + r * math.cos(math.radians(start + 45.0 * k)), c[1] + r * math.sin(math.radians(start + 45.0 * k))) for k in range(8)] def pylon(part, c, base, top, faces, aerial, across=0.96): """An eight-sided tower on a corner: dark foot, white shaft, orange cap, a light strip on the faces that look out (`faces`: directions on the plan), an aerial with a red lamp.""" prism(part, Frame.ground(0.0, 0.0, base), octagon(c, across + 0.14), 0.0, PLINTH_TOP - base, "concrete", cham=0.05) prism(part, Frame.ground(0.0, 0.0, PLINTH_TOP), octagon(c, across), 0.0, 3.50 - PLINTH_TOP, "white", cham=0.04) prism(part, Frame.ground(0.0, 0.0, 3.50), octagon(c, across - 0.10), -0.02, 0.14, "black") # a dark collar prism(part, Frame.ground(0.0, 0.0, 3.62), octagon(c, across), 0.0, top - 3.62, "white", cham=0.05) prism(part, Frame.ground(0.0, 0.0, top), octagon(c, across - 0.16), 0.0, 0.26, "orange", cham=0.05) for f in faces: f = Vector((f[0], f[1])).normalized() o = (c[0] + f.x * across / 2.0, c[1] + f.y * across / 2.0, 0.0) fr = Frame.facing(o, (f.x, f.y, 0.0)) prism(part, fr, rect(-0.12, 0.12, 1.10, 3.30), -0.02, 0.012, "black", back=False) prism(part, fr, rect(-0.045, 0.045, 1.25, 3.15), 0.0, 0.03, "light", cham=0.008, back=False) if aerial > 0.0: z = top + 0.26 cylinder(part, (c[0], c[1], z - 0.02), (c[0], c[1], z + 0.20), 0.07, "black", sides=8, cham=0.015) cylinder(part, (c[0], c[1], z + 0.18), (c[0], c[1], z + aerial), 0.018, "grey", sides=6) cylinder(part, (c[0], c[1], z + aerial), (c[0], c[1], z + aerial + 0.08), 0.035, "red", sides=6, smooth=False) def pier(part, run, a0, a1, top, proud=0.62): """A pier where a run ends against something else: a block along the wall with a foot and an orange head.""" prism(part, Frame.ground(0.0, 0.0, run.base), _quad(run, a0 - 0.05, a1 + 0.05, -0.02, proud + 0.07), 0.0, PLINTH_TOP - run.base, "concrete", cham=0.05) prism(part, Frame.ground(0.0, 0.0, PLINTH_TOP), _quad(run, a0, a1, -0.02, proud, clip=0.10), 0.0, top - PLINTH_TOP, "white", cham=0.05) prism(part, Frame.ground(0.0, 0.0, top), _quad(run, a0 + 0.08, a1 - 0.08, 0.02, proud - 0.08, clip=0.06), 0.0, 0.26, "orange", cham=0.05) if a1 - a0 > 0.45: fr = run.frame(proud) m = (a0 + a1) / 2.0 prism(part, fr, rect(m - 0.12, m + 0.12, 1.10, 3.30), -0.02, 0.012, "black", back=False) prism(part, fr, rect(m - 0.045, m + 0.045, 1.25, 3.15), 0.0, 0.03, "light", cham=0.008, back=False) def _quad(run, a0, a1, n0, n1, clip=0.0): """A footprint on the plan: from a0 to a1 along the run, from n0 to n1 out of it, counter-clockwise; the two outer corners clipped.""" pts = clipped(a0, a1, n0, n1, 0.0, 0.0, clip, clip) out = [run.at(a, n) for a, n in pts] # along x out: (dir, out) is a left-handed pair on the plan (out is to the right of dir), so turn it round return out[::-1] def rows_of(run): """The rows of plates on a run: (z from, z to, how far proud, name).""" top = run.z1 - PARAPET_DROP - 0.10 if not run.ground: lo = run.z0 + 0.50 return [(lo, top, 0.14, "top")] if top - lo > 0.8 else [] if run.z1 > 6.5: return [ROW_LOW + (0.16, "low"), (ROW_UP0, 4.55, 0.12, "up"), (4.95, top, 0.14, "top")] return [ROW_LOW + (0.16, "low"), (ROW_UP0, top, 0.12, "up")] def vent_plate(part, fr, a0, a1, z0, z1, t): """A plate that is a louvre: a white frame, a dark back, slats.""" w = 0.16 for o in (rect(a0, a0 + w, z0, z1), rect(a1 - w, a1, z0, z1), rect(a0 + w, a1 - w, z1 - w, z1), rect(a0 + w, a1 - w, z0, z0 + w)): prism(part, fr, o, -0.03, t, "white", cham=0.02, back=False) prism(part, fr, rect(a0 + w, a1 - w, z0 + w, z1 - w), -0.03, 0.02, "black", back=False) n = max(3, int(round((z1 - z0 - 2 * w) / 0.17))) step = (z1 - z0 - 2 * w) / n for k in range(n): zc = z0 + w + step * (k + 0.5) prism(part, fr, rect(a0 + w - 0.01, a1 - w + 0.01, zc - step * 0.30, zc + step * 0.30), 0.0, t * 0.62, "grey", cham=min(0.03, step * 0.2), back=False) def clad_run(part, run): """Plates, service band and its pipes on one run.""" if run.keepout or run.door is not None: return 0 clear = {"convex": 0.46, "concave": 0.42, "straight": 0.035, "free": 0.90, "keep": 0.25} cs, ce = clear[run.start], clear[run.end] if not run.ground: cs, ce = 0.25, 0.25 span = run.len - cs - ce if span < 0.55: return 0 n = max(1, int(round(span / PLATE))) w = span / n rows = rows_of(run) count = 0 for z0, z1, t, name in rows: fr = run.frame() for k in range(n): a0, a1 = cs + k * w + GAP / 2.0, cs + (k + 1) * w - GAP / 2.0 first, last = k == 0, k == n - 1 pressed = (0.09, 0.014) if a1 - a0 > 0.9 else None # a pressed field in the plate's face if name == "low": c = min(0.14, (a1 - a0) * 0.2) prism(part, fr, clipped(a0, a1, z0, z1, c, c, 0.0, 0.0), -0.03, t, "white", cham=0.025, back=False, panel=pressed) elif name == "up" and n >= 5 and k == n // 2 and n % 2 == 1: vent_plate(part, fr, a0, a1, z0, z1, t) elif name in ("up", "top") and n >= 3 and (first or last) and a1 - a0 > 1.2 and (name == "top" or run.z1 <= 6.5): big = min(0.55, (z1 - z0) * 0.4) prism(part, fr, clipped(a0, a1, z0, z1, 0.0, 0.0, big if last else 0.0, big if first else 0.0), -0.03, t + 0.03, "orange", cham=0.025, back=False, panel=pressed) else: c = min(0.16, (a1 - a0) * 0.2) prism(part, fr, clipped(a0, a1, z0, z1, 0.0, 0.0, c, c), -0.03, t, "white", cham=0.025, back=False, panel=pressed) count += 1 if run.ground: # the service band: two pipes on brackets, from pylon to pylon over = {"convex": 0.30, "concave": 0.10, "straight": 0.0, "free": 0.10, "keep": -0.05} a0, a1 = cs - over[run.start], run.len - ce + over[run.end] if a1 - a0 > 1.0: for z, r, n_out, mat in ((2.50, 0.06, 0.12, "grey"), (2.76, 0.04, 0.10, "orange")): p0, p1 = run.at(a0, n_out), run.at(a1, n_out) tube(part, [(p0[0], p0[1], z), (p1[0], p1[1], z)], r, mat, sides=8) fr = run.frame() for k in range(n + 1): a = cs + k * w prism(part, fr, rect(a - 0.05, a + 0.05, 2.39, 2.88), -0.02, 0.19, "black", cham=0.015, back=False) if run.z1 > 6.5 and len(rows) == 3: fr = run.frame() for k in range(n): a0p, a1p = cs + k * w + 0.30, cs + (k + 1) * w - 0.30 if a1p - a0p > 0.3: prism(part, fr, rect(a0p, a1p, 4.70, 4.80), -0.02, 0.03, "light", cham=0.01, back=False) return count def clad_rooms(part, runs): """Everything that hangs on the runs: plinths, parapets, pylons and piers, plates.""" info = {"plates": 0, "pylons": 0, "piers": 0, "plinth": 0.0, "parapet": 0.0} # plinth: along the linked ground runs for chain, closed in chains(runs, lambda r: r.ground and not r.keepout and r.door is None, same=lambda a, c: abs(a.base - c.base) < 1e-3): prof, mats = plinth_profile(chain[0].base) path = chain_path(chain, closed) sweep(part, prof, path, mats, closed=closed) info["plinth"] += sum(r.len for r in chain) # parapet: along the roof's edge, level by level for chain, closed in chains(runs, lambda r: not r.keepout, same=lambda a, c: abs(a.z1 - c.z1) < 1e-3 and abs((a.z1 - a.z0 < 0.5) - (c.z1 - c.z0 < 0.5)) == 0): z1 = chain[0].z1 drop = min(PARAPET_DROP, max(0.18, z1 - chain[0].z0 - 0.02)) fascia, coping = parapet_profiles(drop) path = [(x, y, z1) for x, y in chain_path(chain, closed)] sweep(part, fascia[0], path, fascia[1], closed=closed) sweep(part, coping[0], path, coping[1], closed=closed) info["parapet"] += sum(r.len for r in chain) # pylons on the outer corners, piers at loose ends k = 0 for r in runs: if not r.ground or r.keepout: continue if r.end == "convex" and r.next is not None: n = r.next bis = (r.out + n.out).normalized() c = (r.p1.x + bis.x * 0.22, r.p1.y + bis.y * 0.22) top = max(r.z1, n.z1) + 0.62 pylon(part, c, min(r.base, n.base), top, [r.out, n.out], AERIALS[k % len(AERIALS)]) k += 1 info["pylons"] += 1 for end, a0, a1 in (("start", 0.0, min(0.84, r.len)), ("end", max(0.0, r.len - 0.84), r.len)): if getattr(r, end) == "free" and r.door is None: pier(part, r, a0, a1, r.z1 + 0.30) info["piers"] += 1 for r in runs: info["plates"] += clad_run(part, r) return info # --------------------------------------------------------------------------------------------------------------- def make_object(part, kit, col, module, note, collision="complex"): me = part.to_mesh(lambda key: own_materials.get(kit, key)) ob = bpy.data.objects.new(part.name, me) col.objects.link(ob) ob["anchorage_zone"] = ZONE ob["anchorage_module"] = module ob["anchorage_custom_mesh"] = part.name ob["anchorage_collision"] = collision ob["anchorage_note"] = note return ob def clear(col): for ob in list(col.all_objects): me = ob.data if ob.type == "MESH" else None bpy.data.objects.remove(ob) if me is not None and me.users == 0: bpy.data.meshes.remove(me) def recolour_hull(kit): """The shell's white wall panels turn into the dark structure behind the armour.""" hull = own_materials.get(kit, "hull") n = 0 for name in ("ANCH_Shell_Rooms", "ANCH_Shell_Galleries", "ANCH_Shell_Relay"): ob = bpy.data.objects.get(name) if ob is None: continue for i, m in enumerate(ob.data.materials): if m is not None and m.get("ue_material") == ext.MAT_WALL_FIRST: ob.data.materials[i] = hull n += 1 return n def refresh(b, names=("ANCH_Armour_Galleries", "ANCH_Roof_Works")): """New meshes for parts of the outpost that are already in the open file, in place: the objects, what was done to them by hand and everything else in the file stay. (The roof works bring the pack's ducts with them: those are laid anew.)""" import outpost_more as more kit = b.kit own_materials.register(kit) col = get_collection(COLLECTION, get_collection("ANCHORAGE STATION")) done = [] def tag(ob, module): ob["anchorage_zone"] = ZONE ob["anchorage_module"] = module return ob for name in names: ob = bpy.data.objects.get(name) if ob is None: print("[anchorage] outpost: no %s in this file" % name) continue part = Part(name) if name == "ANCH_Armour_Galleries": more.clad_galleries(part, b) more.portals(part, b) elif name == "ANCH_Armour_Rooms": clad_rooms(part, plan_runs(b)) elif name == "ANCH_Armour_Relay": more.relay_crown(part) elif name == "ANCH_Roof_Works": for d in [o for o in col.all_objects if o.get("anchorage_module") == "outpost: roof ducts (Sci-Fi Lite pack)"]: bpy.data.objects.remove(d) more.roof_works(part, b, kit, col, tag) elif name == "ANCH_Site_Works": # the site's own mesh comes with everything that is set out on the site: pods, fences, gate, terminals, # props and lamps are laid anew where the recipe has them import outpost_site as site import ph for d in [o for o in col.all_objects if o is not ob and o.get("anchorage_surroundings") and str(o.get("anchorage_module", "")).startswith("outpost")]: data = d.data if d.type == "LIGHT" else None bpy.data.objects.remove(d) if data is not None and data.users == 0: bpy.data.lights.remove(data) def tag_site(o, module): tag(o, module) o["anchorage_surroundings"] = True return o site.add_site(part, b, kit, ph.Haven(), col, tag_site) else: continue old = ob.data ob.data = part.to_mesh(lambda key: own_materials.get(kit, key)) 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] outpost: new meshes in place for %s" % ", ".join(done)) return done def add_outpost(b, only=None): """Adds or replaces the outpost in the open file. `b` is the Builder (the kit and the plan's numbers).""" import outpost_more as more import outpost_site as site import ph kit = b.kit own_materials.register(kit) haven = ph.Haven() station = get_collection("ANCHORAGE STATION") col = get_collection(COLLECTION, station) clear(col) old = bpy.data.objects.get("ANCH_Roof_Services") # the first exterior's box ducts: the roof works replace them if old is not None: me = old.data bpy.data.objects.remove(old) if me.users == 0: bpy.data.meshes.remove(me) def tag(ob, module): ob["anchorage_zone"] = ZONE ob["anchorage_module"] = module return ob def report(part, text): st = part.stats() print("[anchorage] outpost, %s; %d faces (%d tris, %d of them three-cornered), %d solids, %d turned round" % ( text, st["faces"], st["tris"], st["3-gons"], st["solids"], part.turned)) runs = plan_runs(b) part = Part("ANCH_Armour_Rooms") info = clad_rooms(part, runs) make_object(part, kit, col, "outpost: armour of the rooms", "plinth, plates, service pipes, parapet, corner pylons and piers: new geometry, outside the shell") report(part, "rooms and Relay Control's square half: %d runs (%.0f m of plinth, %.0f m of parapet), %d pylons, %d piers, " "%d plates" % (len(runs), info["plinth"], info["parapet"], info["pylons"], info["piers"], info["plates"])) part = Part("ANCH_Armour_Galleries") gi = more.clad_galleries(part, b) n_portals, n_housings = more.portals(part, b) make_object(part, kit, col, "outpost: armour of the galleries, portals", "base course, fins on the piers, plates and lights over the windows, visor, roof spine; portals at the outer doors; " "the housings the door leaves lift into") report(part, "galleries: %d fins, %.0f m of visor, %d portals, %d door housings" % (gi["fins"], gi["visor"], n_portals, n_housings)) part = Part("ANCH_Armour_Relay") n_crown = more.relay_crown(part) make_object(part, kit, col, "outpost: Relay Control's crown", "canted armour round the drum's head, rail round the skylight, aerials") report(part, "Relay Control's crown: %d plates" % n_crown) part = Part("ANCH_Roof_Works") ri = more.roof_works(part, b, kit, col, tag) make_object(part, kit, col, "outpost: roof works", "plant units, duct stands, condensers, hatches, the crossing's mast") report(part, "roofs: %d plant units, %d pieces of the pack's duct" % (ri["units"] + 2, ri["ducts"])) def tag_site(ob, module): tag(ob, module) ob["anchorage_surroundings"] = True # it stands on the ice, round the station: not part of the shell return ob part = Part("ANCH_Site_Works") si = site.add_site(part, b, kit, haven, col, tag_site) ob = make_object(part, kit, col, "outpost: site works", "landing pad, walkways, tank farm, comms mast, lighting masts, mooring bitts, cradles: on the ice") ob["anchorage_surroundings"] = True report(part, "site: %d cargo pods, %d fences, %d props from polyhaven.com, %d lamps" % (si["pods"], si["fences"], si["props"], si["lights"])) n = recolour_hull(kit) print("[anchorage] outpost: the shell's wall panels are the dark concrete structure now (%d material slots)" % n) if site.set_sky(haven) is not None: print("[anchorage] outpost: the scene is lit by Poly Haven's overcast snow field") return runs