""" A stair from the lower deck up to the sensor room, and both rooms longer toward the bow. The user, 2026-10-20, with a side view of the fore section (bow to the right): the stair drawn in green from the lower deck's floor under the aft end of the raised bow rooms up to their floor at its fore end, and two boxes in red forward of the rooms, one at the raised deck's level, one at the lower deck's: "I want you to make a stairs (green lines), and extend the rooms (red lines) accoring to the reference picture". What the hull allows decides the shapes (measured from M_SHIP_Hull): upper the three cars of the raised bow already reach the hull at their outer top corners (y 7.8 against 7.95 at x 41.8). Only the centre car can go on: it gets a third bay, 3.6 m, to x 45.36. Its end (end wall, braces, trims, the ends of ducts and pipes) moves to the new bay; the bay that was its end is dressed as a middle bay; the new bay has a wall on its port side too (the port car, the sensor room's other half, ends where it ended). lower at the lower deck's floor the round bottom narrows fast: 5.9 m from the centre line at x 37.5, 5.1 at 41, 3.6 at 44.7. Two bays more (to x 44.66), the side walls running straight from the room's corner at y 5.4 to y 2.9 at the new end: the room narrows the way the bow does. Floor modules, ceiling panels and the walls are the room's own, cut along that line; the walls between the two corners are its wall pieces turned 19 degrees and stretched 6 %. stair the Modular Stairs Kit's flight, as on the other deck-to-deck stair, on the centre line: its foot on the lower deck's glass floor in the room's last old bay, its top at the raised deck's floor 0.64 m into the new bay, so that one steps off forward onto the new bay (the old end wall stood right there). It goes up through a shaft lined with plates: an opening in the lower deck's new ceiling and one in the centre car's floor, each just as long as headroom (2.05 m) needs, with rails round the upper one. Everything taken out, swapped or added is written down in the text block RECORD; remove() puts it back. """ 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 import reactor_room as rr # noqa: E402 import shipfit # noqa: E402 import fore_deck as fd # noqa: E402 import lower_deck as ld # noqa: E402 import stairs_kit # noqa: E402 TAG = "fore_stairs" ZONE = "10.4 | Fore main deck" LIGHTS = ZONE + " | lights" LOWER = ld.COLL LOWER_LIGHTS = ld.COLL + " | lights" STAIRS = "10.6 | Stairs" RECORD = "FORE STAIRS | what it was" MADE_FOR = "M-Ship v2 stair from the lower deck to the sensor room, rooms extended (build/fore_stairs.py)" PRE = "SM_FS_" NAME = "Fore stairs | " BAY = 3.60 OFF = 0.001 JOINT = 0.002 W = 0.62 # half the shaft's clear width (the stair is 0.9 m wide at its scale) PLATE = 0.02 # the shaft's lining plates GAP = 0.001 # between a lining plate and the cut pack piece behind it HEAD = 2.05 # headroom over the stair's treads INTO_NEW_BAY = 0.64 # the stair arrives this far forward of the centre car's old end TAPER_END = 2.90 # the lower deck's side walls run from y 5.4 at its old end to this at the new end WALL_D = 0.42 # a wall piece's depth N_LOW = 2 # bays added to the lower deck LINING = "EN_MI_Trim_A_DarkGray" WALL_PIECES = ("EN_SM_Wall_HB_A", "EN_SM_Wall_HB_A2", "EN_SM_Wall_HB_A3") END_ONLY = ("EN_SM_Doorway_SideBrace", "EN_SM_Wall_HB_Edge", "EN_SM_Truss_A_Wires_End") TO_MID = {"EN_SM_AirDuct_End": "EN_SM_AirDuct_Mid", "EN_SM_FloorPipes_End_A": "EN_SM_FloorPipes_Mid_A", "EN_SM_FloorPipes_End_B": "EN_SM_FloorPipes_Mid_B"} 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(): if not exists(): return False bpy.data.texts.remove(bpy.data.texts[RECORD]) return True _kind, _bb, _mid = rr._kind, rr._bb, rr._mid def _kk(o): """The pack piece an object is, or was cut from.""" return (o.data.get("kit_kind") if o.type == "MESH" and o.data is not None else None) or _kind(o) def _objs(*names): out = [] for n in names: c = bpy.data.collections.get(n) if c is not None: out += [o for o in c.objects if o not in out] return out def _take(o, rec): rec["removed"].append(rr._snap(o)) if o.data is not None: o.data.use_fake_user = True bpy.data.objects.remove(o, do_unlink=True) def _swap(o, me, rec): rec["swapped"].append([o.name, o.data.name, me.name]) o.data = me def _copy(o, m, rec, name=None): c = o.copy() c.name = name or (NAME + o.name) for col in o.users_collection: col.objects.link(c) c.matrix_world = m c["made_by"] = TAG rec["added"].append(c.name) return c def _new(o, rec): o["made_by"] = TAG o["zone"] = o.get("zone", ZONE) rec["added"].append(o.name) return o def _cut_mesh(o, planes, keep, name, note, rec): """A copy of o's mesh split by the world planes (co, no) and without the faces whose centre keep() rejects. The pack's materials, UVs and normals stay; it is no longer an instance of the pack's asset.""" src = o.data rr._forget(name) me = src.copy() me.name = name me.use_fake_user = False mw = o.matrix_world inv = mw.inverted() nrm = mw.to_3x3().transposed() bm = bmesh.new() bm.from_mesh(me) for co, no in planes: bmesh.ops.bisect_plane(bm, geom=bm.verts[:] + bm.edges[:] + bm.faces[:], dist=1e-6, plane_co=inv @ Vector(co), plane_no=(nrm @ Vector(no)).normalized()) gone = [f for f in bm.faces if not keep(mw @ f.calc_center_median())] bmesh.ops.delete(bm, geom=gone, context="FACES") bm.to_mesh(me) bm.free() me.update() if "ue_mesh" in me: me["cut_from"] = me["ue_mesh"] del me["ue_mesh"] if "ue_mesh" in o: del o["ue_mesh"] me["kit_kind"] = src.get("kit_kind", src.name) me["made_for"] = MADE_FOR me["note"] = note rec["meshes"].append(me.name) return me def _box_planes(x0, x1, y0, y1): return [((x0, 0, 0), (1, 0, 0)), ((x1, 0, 0), (1, 0, 0)), ((0, y0, 0), (0, 1, 0)), ((0, y1, 0), (0, 1, 0))] def _overlaps(o, lo, hi): a, b = _bb(o) return all(a[i] < hi[i] and b[i] > lo[i] for i in range(3)) def _plate(name, coll, root, x0, x1, y0, y1, z0, z1, rec): ob = shipfit.box(NAME + name, coll, root, x0, x1, y0, y1, z0, z1, material=bpy.data.materials.get(LINING), cell=0.6) ob.data["made_for"] = MADE_FOR rec["meshes"].append(ob.data.name) return _new(ob, rec) class Site: """Where things are, found in the file as it stands.""" def __init__(self): up = _objs(ZONE) low = _objs(LOWER) tiles = [o for o in up if _kk(o) == "EN_SM_GlassFloor_A" and abs(_mid(o).y) < 0.1 and _mid(o).z > -3.5] if not tiles: raise RuntimeError("no centre car on the raised fore deck") self.tile2 = max(tiles, key=lambda o: _mid(o).x) # the centre car's fore bay a, b = _bb(self.tile2) self.xa, self.xb = round(a.x, 3), round(b.x, 3) self.tile_bottom, self.tile_top = a.z, b.z self.zs = round(fd.Z2 + fd.record()["raised_by"], 4) if fd.exists() else 0.0 if not fd.exists(): raise RuntimeError("the fore main deck is not raised in this file") if not ld.exists(): raise RuntimeError("there is no lower deck in this file") self.s = ld.record()["sill"] ltiles = [o for o in low if _kk(o) == "EN_SM_GlassFloor_A" and abs(_mid(o).y) < 0.1] self.ltile = max(ltiles, key=lambda o: _mid(o).x) # the lower deck's last bay, centre a, b = _bb(self.ltile) self.lx0, self.lx1 = round(a.x, 3), round(b.x, 3) cs = [o for o in low if _kk(o) == "EN_SM_Ceiling_HB_A" and self.lx0 - 0.05 < _bb(o)[0].x and _bb(o)[1].x < self.lx1 + 0.05] self.ceil_bottom = min(_bb(o)[0].z for o in cs) self.ceil_top = max(_bb(o)[1].z for o in cs) self.ceils = cs self.glass = next(o for o in low if _kk(o) == "EN_SM_GlassFloor_Panel" and abs(_mid(o).y) < 0.1 and self.lx0 - 0.05 < _bb(o)[0].x and _bb(o)[1].x < self.lx1 + 0.05) self.modules = {sgn: next(o for o in low if _kk(o) == rr.PRE + "Floor_Frame" and _mid(o).y * sgn > 1.0 and self.lx0 - 0.05 < _bb(o)[0].x and _bb(o)[1].x < self.lx1 + 0.05) for sgn in (1.0, -1.0)} self.panels = {sgn: next(o for o in low if _kk(o) == "EN_SM_GlassFloor_Panel" and _mid(o).y * sgn > 1.0 and self.lx0 - 0.05 < _bb(o)[0].x and _bb(o)[1].x < self.lx1 + 0.05) for sgn in (1.0, -1.0)} self.glass_up_top = max(_bb(o)[1].z for o in up if _kk(o) == "EN_SM_GlassFloor_Panel" and abs(_mid(o).y) < 0.1 and self.xa < _mid(o).x < self.xb) self.up, self.low = up, low def bay2(self): """The centre car's fore bay: what belongs to it (not the port car's side, not the lift room's).""" out = [] for o in _objs(ZONE, LIGHTS): a, b = _bb(o) c = (a + b) * 0.5 if not (self.xa + 0.15 < c.x < self.xb + 0.15 and a.x > self.xa - 0.12 and b.x < self.xb + 0.12): continue if a.z < -3.5 or b.z > 2.6 or a.y < -3.05 or b.y > 2.25: continue if c.y < -2.62 and _kind(o) not in WALL_PIECES: continue out.append(o) return out def taper(sgn, x0, x1): """The lower deck's side wall line on side sgn: its room face, and its outward normal.""" p0 = Vector((x0, sgn * ld.HALF, 0.0)) p1 = Vector((x1, sgn * TAPER_END, 0.0)) d = (p1 - p0).normalized() n = Vector((-d.y, d.x, 0.0)) if sgn > 0 else Vector((d.y, -d.x, 0.0)) if n.y * sgn < 0: n = -n return p0, p1, d, n def apply(): if exists(): raise RuntimeError("the fore stairs are already there (text block %r); remove() takes them out" % RECORD) site = Site() rec = {"version": 1, "added": [], "removed": [], "swapped": [], "meshes": []} out = {"sill_raised": site.zs, "sill_lower": site.s} root = bpy.data.objects.get(quarters.ROOT) T = Matrix.Translation # ---- 1. the centre car's third bay ------------------------------------------------------------------------------ bay2 = site.bay2() xe = site.xb new3 = [_copy(o, T((BAY, 0.0, 0.0)) @ o.matrix_world, rec, NAME + "bay 3 | " + o.name) for o in bay2] # its port side: what stands on the starboard wall, turned round to the port side port = [] for o in new3: a, b = _bb(o) if b.y > -1.75 or a.y < -3.05 or _kind(o) == "EN_SM_Doorway_SideBrace" or o.type != "MESH": continue c = (a + b) * 0.5 m = T((c.x, -c.y, c.z)) @ Matrix.Rotation(math.pi, 4, "Z") @ T(-c) @ o.matrix_world c2 = _copy(o, m, rec, NAME + "bay 3 port | " + o.name) if _kind(o) in WALL_PIECES: c2.data = _cut_mesh(c2, [((xe + GAP, 0, 0), (1, 0, 0))], lambda c: c.x >= xe + GAP - 1e-5, "%s%s_port%02d" % (PRE, _kind(o).replace("EN_SM_", ""), len(port)), "bay 3's port wall piece, its aft end 1 mm clear of the sensor room's beam", rec) port.append(c2) # the old end becomes a middle bay xe = site.xb taken = [] for o in bay2: a, b = _bb(o) k = _kind(o) end = b.x > xe - 0.75 and (k in END_ONLY or (k in WALL_PIECES and abs(_mid(o).y) < 0.2)) if end: taken.append(o.name) _take(o, rec) elif k in TO_MID: _swap(o, bpy.data.meshes[TO_MID[k]], rec) out["upper"] = {"bay": [xe, round(xe + BAY, 3)], "copied": len(new3), "port_side": len(port), "end_pieces_moved": taken} bpy.context.view_layer.update() # ---- 2. the lower deck's two bays ---------------------------------------------------------------------------- low = bpy.data.collections[LOWER] low_l = quarters.get_collection(LOWER_LIGHTS, low) x0, x1 = site.lx1, site.lx1 + N_LOW * BAY s = site.s for o in list(_objs(LOWER)): a, b = _bb(o) k = _kind(o) if b.x > x0 - 0.5 and a.x > x0 - 0.5 and (k.startswith(ld.PRE + "Wall_") or k == "EN_SM_Wall_HB_A2" or k == ld.PRE + "Sill_Strip"): _take(o, rec) # the old fore end walls and their sill strips cut_n = [0] def cutname(k): cut_n[0] += 1 return "%s%s_cut%02d" % (PRE, k.replace("EN_SM_", "").replace("SM_", ""), cut_n[0]) floor = [] for j in range(N_LOW): dx = BAY * (j + 1) for sgn in (1.0, -1.0): floor.append(_copy(site.modules[sgn], T((dx, 0, 0)) @ site.modules[sgn].matrix_world, rec, NAME + "lower floor module %s %d" % ("port" if sgn > 0 else "starboard", 7 + j))) floor.append(_copy(site.panels[sgn], T((dx, 0, 0)) @ site.panels[sgn].matrix_world, rec, NAME + "lower floor panel %s %d" % ("port" if sgn > 0 else "starboard", 7 + j))) cm = T((dx, -(ld.MODULE + JOINT), 0)) @ site.modules[1.0].matrix_world floor.append(_copy(site.modules[1.0], cm, rec, NAME + "lower floor module centre %d" % (7 + j))) floor.append(_copy(site.panels[1.0], T((dx, -(ld.MODULE + JOINT), 0)) @ site.panels[1.0].matrix_world, rec, NAME + "lower floor panel centre %d" % (7 + j))) ceil = [] for j in range(N_LOW): for c in site.ceils: ceil.append(_copy(c, T((BAY * (j + 1), 0, 0)) @ c.matrix_world, rec, NAME + "lower ceiling %d | %s" % (7 + j, c.name))) for j, (sgn, side) in enumerate(((1.0, "port"), (-1.0, "starboard"))): p0, p1, d, n = taper(sgn, x0, x1) plane = [((p0.x, p0.y, 0.0), tuple(n))] back = [((p0.x + n.x * WALL_D, p0.y + n.y * WALL_D, 0.0), tuple(n))] for o in floor: if _mid(o).y * sgn > 1.0: o.data = _cut_mesh(o, plane, lambda c, p0=p0, n=n: (c - Vector((p0.x, p0.y, c.z))).dot(n) <= 1e-4, cutname(_kind(o)), "lower deck floor module cut along the bow's taper", rec) for o in ceil: if _mid(o).y * sgn > 2.5: o.data = _cut_mesh(o, back, lambda c, p0=p0, n=n: (c - Vector((p0.x, p0.y, c.z))).dot(n) <= WALL_D + 1e-4, cutname(_kind(o)), "lower deck ceiling panel cut behind the bow's tapered wall", rec) # the tapered side walls: two pieces a side, their room face on the line, cut at the end wall's face walls = [] x_face = x1 - WALL_D for sgn, side in ((1.0, "port"), (-1.0, "starboard")): p0, p1, d, n = taper(sgn, x0, x1) L = (p1 - p0).length sx = L / 2.0 / ld.MODULE for i, kname in enumerate((ld.PRE + "Wall_HB_A2", ld.PRE + "Wall_HB_A3")): me = bpy.data.meshes[kname] zmin = min(v.co.z for v in me.vertices) ymax = max(v.co.y for v in me.vertices) ey, ez = -n, Vector((0, 0, 1)) ex = ey.cross(ez) R = Matrix(((ex.x, ey.x, ez.x, 0), (ex.y, ey.y, ez.y, 0), (ex.z, ey.z, ez.z, 0), (0, 0, 0, 1))) # local x = 0 is the end the piece's +x points away from a_end = p0 + d * (L / 2.0) * i if ex.dot(d) < 0 else p0 + d * (L / 2.0) * (i + 1) m = T((a_end.x, a_end.y, s - 0.019)) @ R @ Matrix.Diagonal((sx, 1.0, 1.0, 1.0)) @ T((0.0, -ymax, -zmin)) o = bpy.data.objects.new(NAME + "lower wall %s %d" % (side, i + 1), me) low.objects.link(o) o.parent = root o.matrix_world = m o["cut_from"] = me.get("cut_from", "") _new(o, rec) if i == 1: o.data = _cut_mesh(o, [((x_face, 0, 0), (1, 0, 0))], lambda c: c.x <= x_face + 1e-4, cutname(kname), "lower deck tapered wall piece cut at the new end wall's face", rec) walls.append(o) # the end wall: a whole piece across the centre, a cut piece each side out to the tapered walls y_end = TAPER_END + (x1 - x_face) * (ld.HALF - TAPER_END) / (x1 - x0) # the taper's room face at the end wall's face reach = round(y_end + 0.25, 3) endm = bpy.data.meshes[ld.PRE + "Wall_HB_A3"] o = quarters.put(endm.name, low, root, ZONE, (x1, -1.80, s + OFF), yaw=90.0, anchor="+--", name=NAME + "lower end wall centre") _new(o, rec) for sgn, side in ((1.0, "port"), (-1.0, "starboard")): o = quarters.put(endm.name, low, root, ZONE, (x1, 1.80 if sgn > 0 else -ld.HALF, s + OFF), yaw=90.0, anchor="+--", name=NAME + "lower end wall %s" % side) o.data = _cut_mesh(o, [((0, sgn * reach, 0), (0, 1, 0))], lambda c, sgn=sgn: c.y * sgn <= reach + 1e-4, PRE + "Wall_HB_A3_end_%s" % side, "lower deck end wall piece cut behind the tapered wall", rec) _new(o, rec) # the sill strips under the end wall, cut with the floor strip = bpy.data.meshes.get(ld.PRE + "Sill_Strip") for sgn, side in ((1.0, "port"), (-1.0, "starboard"), (0.0, "centre")): yb = {1.0: ld.HALF + JOINT, -1.0: -(1.80 + JOINT), 0.0: 1.80}[sgn] # (the strip runs from here toward -y) mx = T((x1, yb, s)) @ Matrix.Rotation(math.pi, 4, "Z") o = quarters.place_mesh(strip, NAME + "lower sill strip %s" % side, low, root, mx, ZONE) _new(o, rec) if sgn != 0: p0, p1, d, n = taper(sgn, x0, x1) o.data = _cut_mesh(o, [((p0.x, p0.y, 0.0), tuple(n))], lambda c, p0=p0, n=n: (c - Vector((p0.x, p0.y, c.z))).dot(n) <= 1e-4, cutname("Sill_Strip"), "sill strip cut along the bow's taper", rec) # light: the pack's caged tube under the ceiling, a lamp under it for j, xs in enumerate((x0 + 0.5 * BAY, x0 + 1.5 * BAY)): for sgn, side in ((1.0, "port"), (-1.0, "starboard")): y = sgn * (2.9 if j == 0 else 1.8) at = (xs, y, s + ld.ROOF - ld.CEIL_T - OFF) _new(quarters.put(ld.TUBE, low, root, ZONE, at, yaw=90.0, anchor="00+", tilt=("Y", 90.0), name=NAME + "lower ceiling light %s %d" % (side, 7 + j)), rec) _new(quarters.area_lamp(low_l, root, NAME + "lower ceiling lamp %s %d" % (side, 7 + j), (at[0], at[1], at[2] - 0.13), ld.TUBE_WATTS, (1.50, 0.14), color=ld.TUBE_COLOR, zone=ZONE), rec) out["lower"] = {"bays": [x0, x1], "room_end_face_x": round(x_face, 3), "half_width_at_old_end": ld.HALF, "half_width_at_new_end": round(y_end, 3)} bpy.context.view_layer.update() # ---- 3. the stair ----------------------------------------------------------------------------------------------- st = bpy.data.collections[STAIRS] x_top = round(xe + INTO_NEW_BAY, 3) z_bot = _bb(site.glass)[1].z e, kids, sc = stairs_kit.kit_stair(NAME + "lower deck to sensor room", st, root, x_top, site.zs, z_bot, +1, 0.0) for k in [e] + kids: _new(k, rec) bpy.context.view_layer.update() toe = x_top - sc * stairs_kit.RUN out["stair"] = {"scale": round(sc, 4), "foot_toe_x": round(toe, 3), "top_x": x_top, "rise_m": round(site.zs - z_bot, 3), "z_foot": round(z_bot, 4)} # ---- 4. the openings and the shaft ------------------------------------------------------------------------------ # upper: aft edge where a tread is HEAD under the floor's underside (with the soffit plate) hx1 = x_top hx0 = round(hx1 - (site.zs - (site.tile_bottom - PLATE - HEAD)), 3) # lower: the fore edge where the stair's underside has come up through the lower deck's ceiling vs = [] for k in kids: mw = k.matrix_world vs += [mw @ v.co for v in k.data.vertices] lx1 = round(max(v.x for v in vs if v.z < site.ceil_top + 0.03) + 0.06, 3) lx0 = site.lx1 g = GAP # (the cuts stand this far behind the lining plates) hole_up = (Vector((hx0 - g, -W - PLATE - g, site.tile_bottom - 0.2)), Vector((hx1 + PLATE + 2 * g, W + PLATE + g, site.tile_top + 0.05))) hole_lo = (Vector((lx0, -W - PLATE - g, site.ceil_bottom - 0.05)), Vector((lx1 + g, W + PLATE + g, site.ceil_top + 0.05))) cut_up, cut_lo = [], [] for o in _objs(ZONE): if o.type == "MESH" and _overlaps(o, *hole_up): lo, hi = hole_up me = _cut_mesh(o, _box_planes(lo.x, hi.x, lo.y, hi.y), lambda c, lo=lo, hi=hi: not (lo.x < c.x < hi.x and lo.y < c.y < hi.y), cutname(_kind(o)), "opening for the stair from the lower deck", rec) if o.get("made_by") == TAG: o.data = me else: _swap(o, me, rec) cut_up.append(o.name) for o in _objs(LOWER): if o.type == "MESH" and _overlaps(o, *hole_lo): lo, hi = hole_lo me = _cut_mesh(o, _box_planes(lo.x, hi.x, lo.y, hi.y), lambda c, lo=lo, hi=hi: not (lo.x < c.x < hi.x and lo.y < c.y < hi.y), cutname(_kind(o)), "opening for the stair to the sensor room", rec) if o.get("made_by") == TAG: o.data = me else: _swap(o, me, rec) cut_lo.append(o.name) up = bpy.data.collections[ZONE] tb, tt, cb, ct = site.tile_bottom, site.tile_top, site.ceil_bottom, site.ceil_top for sgn, side in ((1.0, "port"), (-1.0, "starboard")): ya, yb = sorted((sgn * W, sgn * (W + PLATE))) _plate("shaft side %s, under the floor" % side, low, root, lx0, hx0 + PLATE, ya, yb, cb, tb, rec) _plate("shaft side %s, through both decks" % side, low, root, hx0 + PLATE, lx1, ya, yb, cb, tt, rec) _plate("shaft side %s, over the lower ceiling" % side, low, root, lx1, hx1 + PLATE + g, ya, yb, ct, tt, rec) _plate("shaft aft, lower", low, root, lx0, lx0 + PLATE, -W, W, cb, tb - PLATE, rec) _plate("shaft soffit under the sensor room floor", low, root, lx0 + PLATE, hx0 + PLATE, -W, W, tb - PLATE, tb, rec) _plate("shaft aft, upper", up, root, hx0, hx0 + PLATE, -W, W, tb, tt, rec) _plate("shaft fore, lower", low, root, lx1 - PLATE, lx1, -W, W, cb, ct, rec) _plate("shaft floor over the lower ceiling", low, root, lx1, hx1, -W, W, ct, ct + PLATE, rec) _plate("shaft fore, upper (the stair's top plates bolt to it)", up, root, hx1 + g, hx1 + PLATE + g, -W, W, ct, tt, rec) # rails round the upper opening, on the glass zg = site.glass_up_top ry = W + PLATE + 0.07 run = hx1 - (hx0 + PLATE) k = 2 sxr = run / (k * 1.64) for sgn, side in ((1.0, "port"), (-1.0, "starboard")): for i in range(k): xc = hx0 + PLATE + run * (i + 0.5) / k o = quarters.put(fd.RAIL, up, root, ZONE, (xc, sgn * ry, zg), yaw=0.0, anchor="00-", name=NAME + "rail %s %d" % (side, i + 1)) o.scale = (sxr, 1.0, 1.0) _new(o, rec) o = quarters.put(fd.RAIL, up, root, ZONE, (hx0 - 0.07, 0.0, zg), yaw=90.0, anchor="00-", name=NAME + "rail aft") o.scale = ((2 * ry) / 1.64, 1.0, 1.0) _new(o, rec) out["openings"] = {"upper": [hx0, hx1], "lower": [lx0, lx1], "clear_width_m": 2 * W, "cut_upper": len(cut_up), "cut_lower": len(cut_lo)} rec["stair"] = out["stair"] rec["openings"] = out["openings"] bpy.data.texts.new(RECORD).write(json.dumps(rec)) bpy.context.view_layer.update() return out def remove(): 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 name, old, new in reversed(rec["swapped"]): o = bpy.data.objects.get(name) if o is not None and old in bpy.data.meshes: o.data = bpy.data.meshes[old] back = 0 for d in rec["removed"]: if d["name"] in bpy.data.objects: continue data = bpy.data.meshes.get(d["data"]) if d["type"] == "MESH" else bpy.data.lights.get(d["data"]) if d["type"] == "LIGHT" else None if data is None and d["type"] != "EMPTY": continue o = bpy.data.objects.new(d["name"], 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 if data is not None: data.use_fake_user = False back += 1 for n in rec["meshes"]: me = bpy.data.meshes.get(n) if me is not None and me.users == 0: rr._forget(n) bpy.data.texts.remove(bpy.data.texts[RECORD]) bpy.context.view_layer.update() return {"present": True, "taken_out": len(rec["added"]), "put_back": back, "unswapped": len(rec["swapped"])} # ---- checking ------------------------------------------------------------------------------------------------------ def _label(o): """A pack piece (or a cut copy of one: by the piece it was cut from) by its mesh; anything else by its name.""" k = o.data.get("kit_kind") or _kind(o) if k.startswith((rr.PRE, ld.PRE)) or o.data.get("ue_mesh") or o.data.get("kit_kind"): return k return "[ship] " + o.name def _crossings(mine, others): 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_: k = rr._crossing(rr._geo(a, cache), rr._geo(b, cache)) if k: key = " x ".join(sorted((_label(a), _label(b)))) out[key] = out.get(key, 0) + k return out def headroom(rec=None): """Clear height over the stair's tread line, at its centre and 0.4 m to each side, from 0.3 m above the toe up.""" rec = rec or record() st = rec["stair"] x_top, zs, toe = st["top_x"], st["z_foot"] + st["rise_m"], st["foot_toe_x"] mine_stair = {n for n in rec["added"] if n.startswith(NAME + "lower deck to sensor room")} lo, hi = Vector((toe - 0.5, -1.0, st["z_foot"] - 0.5)), Vector((x_top + 1.0, 1.0, zs + 4.5)) vs, ts = [], [] for o in bpy.context.scene.objects: if o.type != "MESH" or o.name in mine_stair or not o.visible_get() or not _overlaps(o, lo, hi): continue me = o.data me.calc_loop_triangles() mw = o.matrix_world b = len(vs) vs += [tuple(mw @ v.co) for v in me.vertices] ts += [tuple(b + i for i in t.vertices) for t in me.loop_triangles] tree = BVHTree.FromPolygons(vs, ts, epsilon=0.0) worst = (99.0, None) n = 0 x = toe + 0.3 while x <= x_top - 0.05: zt = zs - (x_top - x) for y in (-0.4, 0.0, 0.4): hit = tree.ray_cast(Vector((x, y, zt + 0.02)), Vector((0, 0, 1)), 10.0) h = (hit[3] + 0.02) if hit[0] is not None else 10.0 n += 1 if h < worst[0]: worst = (round(h, 3), (round(x, 2), y, round(zt, 3))) x += 0.05 return {"samples": n, "least_headroom_m": worst[0], "at": worst[1]} def check(others=True): out = {"present": exists()} if not out["present"]: return out rec = record() mine = [bpy.data.objects[n] for n in rec["added"] if n in bpy.data.objects] swapped = [bpy.data.objects[s[0]] for s in rec["swapped"] if s[0] in bpy.data.objects] meshes = [o for o in mine if o.type == "MESH"] out["objects"] = {"added": len(rec["added"]), "missing": [n for n in rec["added"] if n not in bpy.data.objects][:10], "removed": len(rec["removed"]), "swapped": len(rec["swapped"])} out["stair"] = rec["stair"] out["openings"] = rec["openings"] out["headroom"] = headroom(rec) env = shipfit.Envelope() worst, outside = (9.0, None, None), {} for o in meshes: r = shipfit.check_inside(o, env, spacing=0.25, clear_min=0.0) if r["outside"]: outside[o.name] = [_kind(o), r["outside"], r["furthest_outside_m"], r["outside_sample"][:1]] 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: group = meshes + [o for o in swapped if o not in meshes] names = {o.name for o in group} lo = Vector((min(_bb(o)[0].x for o in group) - 0.3, min(_bb(o)[0].y for o in group) - 0.3, min(_bb(o)[0].z for o in group) - 0.3)) hi = Vector((max(_bb(o)[1].x for o in group) + 0.3, max(_bb(o)[1].y for o in group) + 0.3, max(_bb(o)[1].z for o in group) + 0.3)) scene = bpy.data.scenes.get("M-Ship v2 | REFERENCE ASSET") or bpy.context.scene near = [o for o in scene.objects if o.type == "MESH" and o.name not in names and not o.name.startswith(("KIT | ", "UCX_")) and o.visible_get() and _overlaps(o, lo, hi)] got = _crossings(group, near) own = set(_objs(ZONE, LIGHTS, LOWER, LOWER_LIGHTS, STAIRS)) cache, other = {}, {} for x in [o for o in near if o not in own]: for m in group: k = rr._crossing(rr._geo(m, cache), rr._geo(x, cache)) if k: key = "%s x %s" % (_label(m), _label(x)) other[key] = other.get(key, 0) + k out["crossings_with_other_zones"] = other # (the command deck above, the hull: should be none) stair = "[ship] " + NAME + "lower deck to sensor room" bad = {k: v for k, v in got.items() if any(p.startswith(("[ship]", PRE)) for p in k.split(" x ")) and not all(p.startswith(stair) for p in k.split(" x ")) and k not in rr.KIT_JOINTS and not ld._joint(k)} out["crossings"] = {"against": len(near), "with_the_ship_or_parts_made_here": bad, "pack_pairs": dict(sorted(((k, v) for k, v in got.items() if k not in bad), key=lambda kv: -kv[1])[:24])} return out