""" The Tender carousel's height under the dock room's roof. (The user, 2026-10-13, with a marked-up picture of the pylon: "shorten this "Carousel pylon (bay roof to mount flange)" and adjust the whole docking arm according to that".) The docking arm (models/docking-arm) was designed for a flat ceiling 1.2 m above its origin (ArmsRoot). This bay's roof is a vault: 2.91 m above the origin at the hub, but only 1.66 m at 6 m from it, where the arms' ends turn. So the arm hangs low on a pylon, and its ends still pass close under the vault's shoulders. set_raise(delta) moves the mount, the turret and its booms up by `delta` (the pylon gets that much shorter) and lets every hoist down by the same `delta`, so that the heads and the Tenders stay exactly where they were: the stowed ones at their height, the launch Tender at BOARD. Only what is near the hub goes up, where the vault is high. It is stored on DockArm_Root["raise_m"] and can be set again to any other value (0 puts everything back). clearances() turns the stowed carousel through a full circle and gives, part by part, the least distance to the dock room's lining. The lining is measured as Blender draws it (loop triangles). STATE, 2026-10-13: a study, not applied. Asked how the pylon should be shortened (a flat arm can go up 0.3 m at most; more needs the booms to slope down from the hub), the user chose to put the pylon back and decide later, and had the carousel swapped for the rigged model (carousel_rigged.py). set_raise() was written for the UNRIGGED assembly (it sets lower_m on the Arm empties) and refuses to run on the rigged one, whose hoist handles stop at 5.3 m. rebuild_pylon() and clearances() work with either. """ import math import bmesh import bpy import numpy as np from mathutils import Vector from mathutils.bvhtree import BVHTree import shipfit ROOT = "DockArm_Root" PYLON = "Carousel pylon (bay roof to mount flange)" LINING = "Moonpool | hull lining" Z_FLANGE = 1.20 # the mount flange's top in the arm's own frame (the ceiling the arm was designed for) STOWED, BOARD = 1.0, 6.3 # how far a Tender hangs under the arm's origin when stowed / at BOARD, as designed (raise 0) ARM_REACH = 6.0 # the two links of a hoist, 3 m each: the most it can be let down def pylon_profile(top_z): """(radius, z) in the arm's frame: collar on the mount flange, shaft, flare into the roof.""" z = Z_FLANGE return [(1.05, z), (1.05, z + 0.10), (0.80, z + 0.16), (0.80, top_z - 0.34), (1.20, top_z - 0.26), (1.20, top_z)] def rebuild_pylon(ob, top_z, segments=32): """The pylon's mesh again for a new top (arm frame), in place: same object, materials, shape of its two ends.""" prof = pylon_profile(top_z) if prof[3][1] - prof[2][1] < 0.05: raise ValueError("the pylon would be shorter than its collar and its flare together (%.2f m)" % (top_z - Z_FLANGE)) bm = bmesh.new() rings = [[bm.verts.new((r * math.cos(2 * math.pi * k / segments), r * math.sin(2 * math.pi * k / segments), z)) for k in range(segments)] for r, z in prof] uv = bm.loops.layers.uv.verify() lens = [0.0] for (r0, z0), (r1, z1) in zip(prof[:-1], prof[1:]): lens.append(lens[-1] + math.hypot(r1 - r0, z1 - z0)) total = max(lens[-1], 1e-6) mat_of = lambda i: (2 if i in (0, 4) else 1 if i == 1 else 0) # as build_interior.py gave them for i in range(len(rings) - 1): for k in range(segments): k2 = (k + 1) % segments f = bm.faces.new((rings[i][k], rings[i][k2], rings[i + 1][k2], rings[i + 1][k])) f.material_index = mat_of(i) for l, u, v in zip(f.loops, (k / segments, (k + 1) / segments, (k + 1) / segments, k / segments), (lens[i] / total, lens[i] / total, lens[i + 1] / total, lens[i + 1] / total)): l[uv].uv = (u, v) for ring, z, flip in ((rings[0], prof[0][1], True), (rings[-1], prof[-1][1], False)): c = bm.verts.new((0, 0, z)) for k in range(segments): k2 = (k + 1) % segments f = bm.faces.new((c, ring[k2], ring[k]) if flip else (c, ring[k], ring[k2])) for l in f.loops: l[uv].uv = (0.5 + l.vert.co.x * 0.2, 0.5 + l.vert.co.y * 0.2) bmesh.ops.recalc_face_normals(bm, faces=bm.faces[:]) me = ob.data smooth = [p.use_smooth for p in me.polygons][:1] bm.to_mesh(me) bm.free() if smooth: for p in me.polygons: p.use_smooth = smooth[0] me.update() return {"top_z_arm_frame": round(top_z, 3), "height_m": round(top_z - Z_FLANGE, 3), "faces": len(me.polygons)} def set_raise(delta): """Mount, turret and booms `delta` higher than the arm's designed place under this roof; hoists let down by the same, so heads and Tenders do not move. Returns what was set.""" root = bpy.data.objects[ROOT] if bpy.data.objects.get("DockArm_Rig") is not None: raise RuntimeError("the carousel is the rigged model: set_raise() only knows the unrigged assembly (see the note at the top)") old = float(root.get("raise_m", 0.0)) arms = sorted([o for o in bpy.data.objects if o.type == "EMPTY" and o.name in ("Arm1", "Arm2", "Arm3")], key=lambda o: o.name) tenders = sorted([o for o in bpy.data.objects if o.name in ("_Tender1", "_Tender2", "_Tender3")], key=lambda o: o.name) # what each hoist is let down by, as designed (raise 0): taken from the file once and kept for a in arms: if "lower_designed_m" not in a: a["lower_designed_m"] = float(a["lower_m"]) - old worst = max(float(a["lower_designed_m"]) for a in arms) + delta if worst > ARM_REACH - 0.05: raise ValueError("a hoist would have to reach %.2f m, it has %.2f" % (worst, ARM_REACH)) root.location.z += delta - old for a in arms: a["lower_m"] = float(a["lower_designed_m"]) + delta a.update_tag() for t in tenders: t.location.z -= delta - old # children of the root: back down, so they stay where they were root["raise_m"] = float(delta) root["board_lower_m"] = BOARD - STOWED + delta # read by check_interior.py: how far the launch hoist is let down # the pylon: its top stays in the roof py = bpy.data.objects.get(PYLON) info = {"raise_m": delta, "was_m": old} if py is not None: if "top_designed" not in py: py["top_designed"] = max(v.co.z for v in py.data.vertices) + old info["pylon"] = rebuild_pylon(py, float(py["top_designed"]) - delta) bpy.context.view_layer.update() info["root_z"] = round(root.matrix_world.translation.z, 3) info["hoists_let_down_m"] = {a.name: round(float(a["lower_m"]), 3) for a in arms} info["tenders_z"] = {t.name: round(t.matrix_world.translation.z, 3) for t in tenders} return info def clearances(extra=0.0, margin_list=(0.25,)): """The stowed carousel turned through a full circle: least distance to the dock room's lining, part by part. `extra` adds a what-if height to everything measured (Tenders included).""" dg = bpy.context.evaluated_depsgraph_get() lin = bpy.data.objects[LINING] tree = shipfit.drawn_tree(lin, evaluated=True) hub = bpy.data.objects[ROOT].matrix_world.translation.copy() rs, zs = np.arange(0.0, 8.61, 0.1), np.arange(1.0, 7.41, 0.1) ang = [math.radians(a) for a in range(0, 360, 6)] dist = np.zeros((len(rs), len(zs))) roof = np.zeros(len(rs)) for i, r in enumerate(rs): lo = 1e9 for a in ang: hit = tree.ray_cast(Vector((hub.x + r * math.cos(a), hub.y + r * math.sin(a), 1.5)), Vector((0, 0, 1)), 20.0) if hit[0] is not None: lo = min(lo, hit[0].z) roof[i] = lo for j, z in enumerate(zs): best = 1e9 for a in ang: loc, nrm, idx, dd = tree.find_nearest(Vector((hub.x + r * math.cos(a), hub.y + r * math.sin(a), z))) if loc is not None and dd < best: best = dd dist[i, j] = best def clear(r, z): i = min(len(rs) - 1, max(0, int(round(r / 0.1)))) if z >= roof[i] - 1e-6: return -(z - roof[i]) j = int(round((z - 1.0) / 0.1)) return 9.0 if j < 0 else float(dist[i, min(len(zs) - 1, j)]) def rz(ob, mw): ev = ob.evaluated_get(dg) m = ev.to_mesh() n = len(m.vertices) out = [] for k in range(0, n, max(1, n // 2500)): q = mw @ m.vertices[k].co out.append((math.hypot(q.x - hub.x, q.y - hub.y), q.z)) ev.to_mesh_clear() return out rows = {} rig = bpy.data.objects.get("DockArm_Rig") for ob in bpy.data.objects: if ob.type != "MESH": continue if rig is not None: # the rigged model: turret, mount and the parts of every arm whose hoist handle is at 0 (stowed) if not ob.name.startswith("SK_DockArm_"): continue part = ob.name[len("SK_DockArm_"):] if part.startswith("Arm"): h = rig.pose.bones.get("CTRL_%s_Hoist" % part[:4]) if h is None or abs(h.location.y) > 1e-6: continue else: stowed_arm = ob.name.startswith(("Arm2_", "Arm3_")) and abs(float(bpy.data.objects[ob.name[:4]].get("lower_m", 0.0)) - float(bpy.data.objects[ROOT].get("raise_m", 0.0))) < 1e-6 if not (ob.name in ("SM_DockArm_Turret", "SM_DockArm_Mount") or stowed_arm): continue pts = rz(ob, ob.matrix_world) rows[ob.name] = round(min(clear(r, z + extra) for r, z in pts), 3) kz = None for t in (bpy.data.objects.get("_Tender2"), bpy.data.objects.get("_Tender3")): if t is None or t.instance_collection is None: continue kz = t.instance_collection pts = [] for o in kz.objects: if o.type == "MESH": pts += rz(o, t.matrix_world @ o.matrix_world) if pts: rows[t.name + " (stowed Tender)"] = round(min(clear(r, z + extra) for r, z in pts), 3) least = min(rows.items(), key=lambda kv: kv[1]) if rows else (None, None) return {"least_m": least[1], "at": least[0], "by_part": dict(sorted(rows.items(), key=lambda kv: kv[1])), "roof_over_the_hub_z": round(float(roof[0]), 3), "roof_at_6_m_z": round(float(roof[60]), 3)}