""" The lower deck's bow bay as a raised floor with steps across the room, a kind of showroom stage. The user, 2026-10-20, after cutting the bow extension back to one bay by hand (fore_stairs.py had built two, its walls narrowing with the hull; the user deleted the second bay, set the side walls straight at the old width and the end wall at the first bay's end), with a side view: a red box over the bay's floor, an arrow up, three steps drawn at its aft edge: "walls are exceeding the hull so I want you to lift the floor just like the reference image and build a stair across the room so it will be some kind of showroom like lifted floor". floor the bay's three floor modules go up by RISERS x RISE: the fewest risers that bring the bottom of every wall of the bay inside the hull (at the bay's fore corners the hull's round bottom is 5.0 m from the centre line at the old floor, the walls' backs 5.8). The two side modules are whole again (they had been cut along the taper that the user took out). walls the user's side and end walls of the bay, cut at the new floor (copies of their meshes; the objects and where the user put them stay). steps across the whole room at the bay's aft edge, standing on the old floor: RISERS - 1 treads of TREAD, the last riser up to the raised modules. stair the deck-to-deck stair now starts on the raised floor, just forward of the steps. Its top stays where it was (the openings above do not change), so it is a little steeper: scaled along its run and its height separately, its width as before. The shaft's lining and the lower ceiling's opening are made again for it. gaps (the user's next look, the same night: "there are gaps fix those with wall adjustments") the side walls stand as the lower deck's own walls do: square to the ship, at full length (3.6 m, not stretched), their room face on the floor's edge; the end wall's two side pieces meet its centre piece; the side ceiling panels are whole (they had been cut along the taper too). fix_gaps() does that to a file where apply() ran before it; apply() does it as well. Everything is written down in the text block RECORD; remove() puts back what was there. """ import json import math import os import sys 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 lower_deck as ld # noqa: E402 import fore_stairs as fs # noqa: E402 import stairs_kit # noqa: E402 TAG = "lower_platform" RECORD = "LOWER PLATFORM | what it was" NAME = "Lower platform | " PRE = "SM_LP_" MADE_FOR = "M-Ship v2 lower deck bow bay raised as a stage (build/lower_platform.py)" RISE = 0.20 TREAD = 0.28 CLEAR = 0.03 # the walls' least clearance to the hull's skin TOE_IN = 0.20 # the stair's foot this far forward of the raised floor's aft edge STEP_MAT = "EN_MI_Trim_Gray_Drt" OFF = 0.001 FRAME_BELOW = 0.060 # a floor module's frame reaches this far below the sill PANEL_BELOW = 0.0175 # its perforated panel lies this far below the sill POCKET = ld.POCKET _kind, _bb, _mid = rr._kind, rr._bb, rr._mid 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 def _new(o, rec): o["made_by"] = TAG o["zone"] = o.get("zone", fs.ZONE) rec["added"].append(o.name) return o def _box(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(STEP_MAT), cell=0.6) ob.data["made_for"] = MADE_FOR rec["meshes"].append(ob.data.name) return _new(ob, rec) def _lining(name, coll, root, x0, x1, y0, y1, z0, z1, rec): ob = shipfit.box(fs.NAME + name, coll, root, x0, x1, y0, y1, z0, z1, material=bpy.data.materials.get(fs.LINING), cell=0.6) ob.data["made_for"] = fs.MADE_FOR rec["meshes"].append(ob.data.name) return _new(ob, rec) def _walls(x0): """The bay's wall pieces as they stand: whatever stands forward of the bay's aft edge on the lower deck's floor.""" out = [] for o in bpy.context.scene.objects: if o.type != "MESH": continue k = o.data.get("kit_kind") or _kind(o) if not (k.startswith(ld.PRE + "Wall_") or o.name.startswith((fs.NAME + "lower wall", fs.NAME + "lower end wall"))): continue a, b = _bb(o) if a.x > x0 - 0.2 and a.z < -6.0: out.append(o) return out def _cut_below(o, z, name, rec, note): return fs._cut_mesh(o, [((0, 0, z), (0, 0, 1))], lambda c: c.z >= z - 1e-5, name, note, rec) def _inside(o, z, env): """Every vertex of o above z inside the ship and at least CLEAR from the skin?""" mw = o.matrix_world me = o.data worst = 9.0 pts = [] for e in me.edges: # the vertices above z, and where the edges cross z p, q = mw @ me.vertices[e.vertices[0]].co, mw @ me.vertices[e.vertices[1]].co if (p.z - z) * (q.z - z) < 0.0: pts.append(p.lerp(q, (z - p.z) / (q.z - p.z))) pts += [mw @ v.co for v in me.vertices if (mw @ v.co).z >= z] for p in pts: if not env.inside(p): return False, 0.0 worst = min(worst, env.clearance(p)) return worst >= CLEAR, worst def apply(risers=None): if exists(): raise RuntimeError("the raised floor is already there (text block %r); remove() takes it out" % RECORD) if not fs.exists(): raise RuntimeError("fore_stairs.apply() first") frec = fs.record() rec = {"version": 1, "added": [], "removed": [], "swapped": [], "meshes": []} out = {} root = bpy.data.objects.get(quarters.ROOT) low = bpy.data.collections[fs.LOWER] T = Matrix.Translation site = fs.Site() s, x0 = site.s, site.lx1 walls = _walls(x0) if not walls: raise RuntimeError("no walls found in the bow bay") # ---- 1. how high: the fewest risers that bring every wall inside the hull -------------------------------------- env = shipfit.Envelope() n = risers if n is None: for k in range(2, 8): if all(_inside(o, s + k * RISE + OFF, env)[0] for o in walls): n = k break if n is None: raise RuntimeError("no height up to 1.4 m brings the walls inside the hull") R = round(n * RISE, 4) zp = s + R out["raised_by_m"] = R out["risers"] = n # ---- 2. the floor modules of the bay, whole and raised --------------------------------------------------------- old = [o for o in _objs_low() if (o.name.startswith(fs.NAME + "lower floor ")) and _bb(o)[0].x > x0 - 0.05] x_end = max(_bb(o)[1].x for o in old) if old else x0 + fs.BAY for o in old: fs._take(o, rec) dx = round(x_end - site.lx1, 3) for sgn, side in ((1.0, "port"), (-1.0, "starboard")): for src, what in ((site.modules[sgn], "module"), (site.panels[sgn], "panel")): _new(fs._copy(src, T((fs.BAY, 0, R)) @ src.matrix_world, {"added": []}, NAME + "floor %s %s" % (what, side)), rec) for src, what in ((site.modules[1.0], "module"), (site.panels[1.0], "panel")): _new(fs._copy(src, T((fs.BAY, -(ld.MODULE + fs.JOINT), R)) @ src.matrix_world, {"added": []}, NAME + "floor %s centre" % what), rec) out["floor"] = {"x": [x0, round(x0 + fs.BAY, 3)], "top_z": round(zp, 3), "old_modules_taken": len(old)} # ---- 3. the walls cut at the new floor ------------------------------------------------------------------------- for i, o in enumerate(walls): me = _cut_below(o, zp + OFF, "%sWall_%02d" % (PRE, i + 1), rec, "lower deck wall piece cut at the raised floor of the bow bay") rec["swapped"].append([o.name, o.data.name, me.name]) o.data = me out["walls_cut"] = [o.name for o in walls] # sill plates under the end walls where they stand over a module's perforated panel ends = [o for o in walls if (_bb(o)[1].x - _bb(o)[0].x) < 0.6] face = min(_bb(o)[0].x for o in ends) if ends else None rim = x0 + fs.BAY - POCKET if face is not None and face < rim: for ya, yb, side in ((-1.64, 1.64, "centre"), (1.96, 5.24, "port"), (-5.24, -1.96, "starboard")): _box("sill plate under the end wall, %s" % side, low, root, face, rim, ya, yb, zp - PANEL_BELOW + 0.0005, zp + OFF * 0.5, rec) # ---- 4. the steps across the room ------------------------------------------------------------------------------ half = ld.HALF z_floor = s - PANEL_BELOW + 0.001 for i in range(1, n): _box("step %d" % (n - i), low, root, x0 - i * TREAD, x0 - (i - 1) * TREAD, -half, half, z_floor, s + (n - i) * RISE, rec) _box("riser under the raised floor's edge", low, root, x0 - 0.02, x0, -half, half, s + (n - 1) * RISE, zp - FRAME_BELOW, rec) out["steps"] = {"from_x": round(x0 - (n - 1) * TREAD, 3), "to_x": x0, "rise_m": RISE, "tread_m": TREAD, "width_m": 2 * half} # ---- 5. the stair from the raised floor ---------------------------------------------------------------------- st = frec["stair"] e = bpy.data.objects[fs.NAME + "lower deck to sensor room"] rec["stair_before"] = {"location": list(e.location), "scale": list(e.scale)} x_top, zs = st["top_x"], round(st["z_foot"] + st["rise_m"], 4) z_bot = zp - PANEL_BELOW - 0.0002 toe = x0 + TOE_IN sy = (x_top - toe) / stairs_kit.RUN sz = (zs - z_bot) / stairs_kit.Z_DECK wx = st["scale"] e.scale = (wx, sy, sz) e.location = (x_top - sy * stairs_kit.Y_FACE, wx * stairs_kit.X_MID, z_bot) bpy.context.view_layer.update() slope = math.degrees(math.atan2(sz, sy)) rec["stair"] = {"top_x": x_top, "z_top": zs, "z_foot": round(z_bot, 4), "toe_x": round(toe, 3), "slope_deg": round(slope, 2), "scale_width_run_height": [round(wx, 4), round(sy, 4), round(sz, 4)]} out["stair"] = rec["stair"] # ---- 6. the shaft below the sensor room's floor, made again ------------------------------------------------------ op = frec["openings"] hx0, hx1 = op["upper"] W, P, g = fs.W, fs.PLATE, fs.GAP kids = [o for o in e.children] vs = [] for k in kids: mw = k.matrix_world vs += [mw @ v.co for v in k.data.vertices] cb, ct, tt = site.ceil_bottom, site.ceil_top, site.tile_top lx0 = op["lower"][0] lx1 = round(max(v.x for v in vs if v.z < ct + 0.03) + 0.06, 3) for nm in ("shaft side port, through both decks", "shaft side starboard, through both decks", "shaft side port, over the lower ceiling", "shaft side starboard, over the lower ceiling", "shaft fore, lower", "shaft floor over the lower ceiling"): o = bpy.data.objects.get(fs.NAME + nm) if o is not None: fs._take(o, rec) ceil = bpy.data.objects.get(fs.NAME + "lower ceiling 7 | GlassFloor_Side_A27.004") c_end = _bb(ceil)[1].x if ceil else x0 + fs.BAY if ceil is not None: lo = Vector((lx0, -W - P - g, cb - 0.05)) hi = Vector((min(lx1, c_end + 0.01) + g, W + P + g, ct + 0.05)) me = fs._cut_mesh(ceil, fs._box_planes(lo.x, hi.x, lo.y, hi.y), lambda c: not (lo.x < c.x < hi.x and lo.y < c.y < hi.y), PRE + "Ceiling_HB_A_hole", "lower ceiling opening for the stair, made longer for the raised floor's stair", rec) rec["swapped"].append([ceil.name, ceil.data.name, me.name]) ceil.data = me # where the end wall's head stands under the shaft, the lining starts on it head = max((_bb(o)[1].z for o in ends), default=cb) efx = face if face is not None else 1e9 seg = [(hx0 + P, min(efx, lx1), cb), (min(efx, lx1), lx1, max(cb, head + OFF))] for sgn, side in ((1.0, "port"), (-1.0, "starboard")): ya, yb = sorted((sgn * W, sgn * (W + P))) for j, (a, b, zb) in enumerate(seg): if b - a > 0.005: _lining("shaft side %s, through both decks %d" % (side, j + 1), low, root, a, b, ya, yb, zb, tt, rec) _lining("shaft side %s, over the lower ceiling" % side, low, root, lx1, hx1 + P + g, ya, yb, ct, tt, rec) _lining("shaft fore, lower", low, root, lx1 - P, lx1, -W, W, seg[-1][2] if lx1 > efx else cb, ct, rec) _lining("shaft floor over the lower ceiling", low, root, lx1, hx1, -W, W, ct, ct + P, rec) out["shaft"] = {"lower_opening": [lx0, lx1], "lower_ceiling_ends_x": round(c_end, 3), "end_wall_face_x": round(efx, 3) if face else None} out["gaps"] = _fix(rec, site, zp) rec["gaps_fixed"] = True bpy.data.texts.new(RECORD).write(json.dumps(rec)) bpy.context.view_layer.update() return out def _fix(rec, site, zp): """The side walls square and whole length, the end wall's pieces closed up, the side ceiling panels whole.""" out = {} root = bpy.data.objects.get(quarters.ROOT) low = bpy.data.collections[fs.LOWER] T = Matrix.Translation s, x0 = site.s, site.lx1 rec.setdefault("moved", []) orig = {name: old for name, old, new in rec["swapped"]} walls = _walls(x0) sides = [o for o in walls if (_bb(o)[1].x - _bb(o)[0].x) > 2.0] ends = [o for o in walls if (_bb(o)[1].x - _bb(o)[0].x) < 0.6] # 1. side walls: where lower_deck.apply() would have put a seventh wall piece, cut at the raised floor for o in sides: sgn = 1.0 if _mid(o).y > 0 else -1.0 me0 = bpy.data.meshes[orig.get(o.name, o.data.name)] tmp = quarters.put(me0.name, low, root, fs.ZONE, (x0 + fs.BAY / 2.0, sgn * (ld.HALF + 2 * ld.JOINT), s + OFF), yaw=180.0 if sgn > 0 else 0.0, anchor="0" + ("-" if sgn > 0 else "+") + "-", name="tmp") m = tmp.matrix_world.copy() bpy.data.objects.remove(tmp, do_unlink=True) rec["moved"].append([o.name, [list(r) for r in o.matrix_world]]) o.matrix_world = m old_cut = o.data o.data = me0 me = _cut_below(o, zp + OFF, old_cut.name + "_square", rec, "lower deck side wall, square and whole, cut at the raised floor") o.data = me for row in rec["swapped"]: if row[0] == o.name: row[2] = me.name if o.name not in orig: rec["swapped"].append([o.name, me0.name, me.name]) if old_cut.users == 0 and old_cut.name.startswith(PRE): rr._forget(old_cut.name) out["side_walls"] = [o.name for o in sides] # 2. the end wall's side pieces close up to its centre piece, all in one plane if ends: centre = min(ends, key=lambda o: abs(_mid(o).y)) ca, cb_ = _bb(centre) for o in ends: if o is centre: continue a, b = _bb(o) dy = (cb_.y - a.y) if _mid(o).y > 0 else (ca.y - b.y) dx = ca.x - a.x if abs(dy) > 1e-4 or abs(dx) > 1e-4: rec["moved"].append([o.name, [list(r) for r in o.matrix_world]]) o.matrix_world = T((dx, dy, 0.0)) @ o.matrix_world out["end_pieces_closed"] = [o.name for o in ends if o is not centre] # 3. the side ceiling panels whole taken = [] for o in list(fs._objs(fs.LOWER)): if o.name.startswith(fs.NAME + "lower ceiling 7 |") and abs(_mid(o).y) > 2.5 and o.data.name.startswith(fs.PRE): taken.append(o.name) fs._take(o, rec) for c in site.ceils: if abs(_mid(c).y) > 2.5: side = "port" if _mid(c).y > 0 else "starboard" _new(fs._copy(c, T((fs.BAY, 0, 0)) @ c.matrix_world, {"added": []}, NAME + "ceiling %s" % side), rec) out["ceiling_panels_made_whole"] = taken # 4. under the cut side walls' aft ends, beside the top step: the wall joint would show the hull there for sgn, side in ((1.0, "port"), (-1.0, "starboard")): ya, yb = sorted((sgn * (ld.HALF + 0.003), sgn * (ld.HALF + 0.415))) _lining_named("filler under the %s wall's aft end" % side, low, root, x0, x0 + 0.10, ya, yb, s - PANEL_BELOW + 0.001, zp, rec) bpy.context.view_layer.update() return out def _lining_named(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(fs.LINING), cell=0.6) ob.data["made_for"] = MADE_FOR rec["meshes"].append(ob.data.name) return _new(ob, rec) def fix_gaps(): """_fix() on a file where apply() ran without it.""" rec = record() if rec is None: raise RuntimeError("the raised floor is not in this file") if rec.get("gaps_fixed"): return {"already": True} site = fs.Site() zp = site.s + rec["stair"]["z_foot"] - site.s + PANEL_BELOW + 0.0002 out = _fix(rec, site, round(zp, 4)) rec["gaps_fixed"] = True bpy.data.texts[RECORD].clear() bpy.data.texts[RECORD].write(json.dumps(rec)) return out def _objs_low(): return fs._objs(fs.LOWER, fs.LOWER_LIGHTS) 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: bpy.data.objects.remove(o, do_unlink=True) for name, m in reversed(rec.get("moved", [])): o = bpy.data.objects.get(name) if o is not None: o.matrix_world = Matrix(m) 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] e = bpy.data.objects.get(fs.NAME + "lower deck to sensor room") if e is not None and "stair_before" in rec: e.location = rec["stair_before"]["location"] e.scale = rec["stair_before"]["scale"] 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 None if data is None: 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 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 headroom(rec=None): """Clear height over the stair's tread line (its new slope), centre and 0.4 m to each side.""" rec = rec or record() st = rec["stair"] x_top, zt_top, toe, z0 = st["top_x"], st["z_top"], st["toe_x"], st["z_foot"] k = (zt_top - z0) / (x_top - toe) e = bpy.data.objects[fs.NAME + "lower deck to sensor room"] skip = {e.name} | {c.name for c in e.children} lo, hi = Vector((toe - 0.5, -1.0, z0 - 0.5)), Vector((x_top + 1.0, 1.0, zt_top + 4.5)) vs, ts = [], [] for o in bpy.context.scene.objects: if o.type != "MESH" or o.name in skip or not o.visible_get() or not fs._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, n = (99.0, None), 0 x = toe + 0.3 while x <= x_top - 0.05: zt = zt_top - (x_top - x) * k 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]} ON_THE_FLOOR = {"SM_RR_Floor_Frame", "EN_SM_GlassFloor_A", "EN_SM_GlassFloor_Panel", "EN_SM_GlassFloor_Divider", "EN_SM_FloorPipes_End_A", "EN_SM_FloorPipes_End_B"} 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] e = bpy.data.objects[fs.NAME + "lower deck to sensor room"] stair = [c for c in e.children if c.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["headroom"] = headroom(rec) env = shipfit.Envelope() worst, outside = (9.0, None, None), {} for o in [m for m in mine if m.type == "MESH"] + swapped: 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"] = {"outside": outside, "least_clearance_m": worst[0], "at": worst[1], "point": worst[2]} if others: group = [m for m in mine if m.type == "MESH"] + swapped + stair 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)) near = [o for o in bpy.context.scene.objects if o.type == "MESH" and o.name not in names and not o.name.startswith(("KIT | ", "UCX_")) and o.visible_get() and fs._overlaps(o, lo, hi)] got = fs._crossings(group, near) sname = "[ship] " + fs.NAME + "lower deck to sensor room" steps = "[ship] " + NAME + "step" bad, floor = {}, {} for k, v in got.items(): a, b = k.split(" x ") if a.startswith(sname) and b.startswith(sname): continue if k in rr.KIT_JOINTS or ld._joint(k): continue pair = (a, b) if any(p.startswith((steps, "[ship] " + NAME + "riser")) for p in pair) and any(p in ON_THE_FLOOR for p in pair): floor[k] = v # the steps stand on the old floor's rims continue if any(p.startswith(("[ship]", PRE, fs.PRE)) for p in pair): bad[k] = v out["crossings"] = {"against": len(near), "bad": bad, "steps_on_the_floor_rims": floor} return out