"""Door arms for the moon pool's starboard pocket doors (Moonpool_Door_Aft, Moonpool_Door_Fore). The user, 2026-10-16, with a picture of a marine flush hinge sheet (MFH 100 / MFH 200: a frame block, a long main arm, a short link, two turnbuckles as parallel links, a door-side bracket with a disc, set and cap screws) and a picture of the two doors in their pockets with an arm drawn from each pocket's upper rail down to a plate on the door: "make a copy. Create a door arm that looks like in the referance understand the picture and rig it accordingly and mechanically. according to the open and close animation fit it to the scene. "Moonpool_Door_Fore", "Moonpool_Door_Aft" are the door" ... "you can attach the door arms according to the picture as well". The doors' own animation is kept as it is (frames 1-12 each door moves 1.22 m inboard, +y; 12-48 it slides 6.14 m into its pocket along x; back again 72-120). The arm follows it, so it is mechanical: - a TROLLEY runs on the pocket's upper rail (the user's `Pocket AFT/FORE | upper rail`); it follows the door's slide. The rail ends at the moon pool's deck opening, so the trolley sits at the rail's inner end with the door shut and at the pocket's end wall with the door in its pocket (6.14 m of the rail's 6.49 m free length; the trolley is 0.33 long). - under it a FRAME BLOCK on a vertical yaw bearing; - the arm itself, a double parallelogram as on the sheet: the MAIN ARM and the first TURNBUCKLE (a parallel link) from the frame block to an ELBOW KNUCKLE, then the LINK and the second TURNBUCKLE from the knuckle to the DOOR BRACKET. The parallelograms keep the knuckle and the bracket level, so the bracket only turns about the vertical, like the frame block; its DISC turns on a bearing on the DOOR MOUNT, a wedge bolted to the door's inner face. The door does not turn (its animation has no rotation): the two vertical bearings take the arm's swing. - When the door moves inboard the arm folds (two-link inverse kinematics in the arm's vertical plane, elbow down, the first link hanging from the rail as in the user's drawing); when it slides the whole arm rides along on the trolley. Everything is driven from the door's location by simple-expression drivers (this Blender runs no scripts in files), so scrubbing or playing the doors' timeline moves the arms. The user's four placeholder carriages on the rails (`Pocket ... | rail carriage`, cubes) stood in the trolley's path: they are hidden, not deleted; remove() shows them again. apply(), remove(), check() as the other build scripts; the record is the text block RECORD. """ import json import math import os import sys import bpy import bmesh from mathutils import Matrix, Vector from mathutils.bvhtree import BVHTree HERE = os.path.dirname(os.path.abspath(__file__)) if HERE not in sys.path: sys.path.insert(0, HERE) MODELS = os.path.normpath(os.path.join(HERE, "..", "..")) PD_BUILD = os.path.join(MODELS, "pressure-door", "build") if PD_BUILD not in sys.path: sys.path.insert(0, PD_BUILD) TAG = "door_arms" RECORD = "DOOR ARMS | what was added" PARENT_COLL = "02 MECHANISM | Starboard pocket doors" COLL = "02 MECHANISM | Starboard pocket doors | door arms" SHIP_ROOT = "M_SHIP_ROOT" DOORS = { # side: +1 = the pocket lies toward -x (the aft door), -1 = toward +x (the fore door) "AFT": dict(door="Moonpool_Door_Aft", rail="Pocket AFT | upper rail", wall="Pocket AFT | conformal closed end", side=+1, carriages=("Pocket AFT | rail carriage", "Pocket AFT | rail carriage.001")), "FORE": dict(door="Moonpool_Door_Fore", rail="Pocket FORE | upper rail", wall="Pocket FORE | conformal closed end", side=-1, carriages=("Pocket FORE | rail carriage", "Pocket FORE | rail carriage.001")), } # ---- dimensions (metres) --------------------------------------------------------------------------------------------- TL = 0.33 # trolley length along the rail (its end bumpers stand BUMP further out) BUMP = 0.012 STOP = 0.004 # the bumpers' stop off the pocket's end wall (open); shut, the trolley stays 2 mm inside the rail's end Z_TROLLEY = -3.96 # trolley origin height Z_PLATE = -4.15 # trolley bottom plate's underside Z_CHEEK_TOP = -3.775 # the deck's underside is at -3.755 Z_A = -4.47 # the frame block's main pin (the arm's root, A) PLATE_X = 1.30 # the door mount's centre from the door's centre, toward its pocket PLATE_Y = -1.625 # ... and outboard of the door's origin (door-local y), on the door's inner face WEDGE_HALF = (0.27, 0.22) G_ABOVE_TOP = 0.302 # the bracket's pin G above the mount's top (lower race 0.05, 2 mm, upper race 0.05, G 0.20 over it) U = 0.36 # the parallelograms' pin spacing (stage 1 horizontal, stage 2 vertical) BODY_T = 0.06 # half the thickness of the frame block, the knuckle and the bracket (along the pins) EYE_T = 0.05 # half the thickness of the arms' eyes ROD_T = 0.035 # ... and of the turnbuckles' rod-end eyes W = 0.12 # the two stages' planes either side of the arm's centre plane (along the pins) PIN_R, BORE_R = 0.045, 0.047 PIN_IN, PIN_OUT = BODY_T - 0.03, W + EYE_T + 0.006 # a pin runs from inside its body to its cap screw CAP_R, CAP_H = 0.07, 0.04 N = 32 # segments round pins and eyes TAU = 2.0 * math.pi def _pd(): g = {"__name__": "pd_lib_for_door_arms"} with open(os.path.join(PD_BUILD, "pd_lib.py"), encoding="utf-8") as f: exec(f.read(), g) return g # ---- small geometry ---------------------------------------------------------------------------------------------------- def ring(g, bm, center, axis, ri, ro, h0, h1, mat, n=N, ch=0.004): """an annulus round `axis` through `center`, from h0 to h1 along the axis; chamfered outer edges""" c = min(ch, (h1 - h0) * 0.3, (ro - ri) * 0.3) prof = [(ri, h0), (ri, h1), (ro - c, h1), (ro, h1 - c), (ro, h0 + c), (ro - c, h0), (ri, h0)] g["lathe"](bm, prof, n, center=center, axis=axis, mat=mat) def disc(g, bm, center, axis, r, h0, h1, mat, n=N, ch=0.004): """a solid cylinder round `axis` through `center`, from h0 to h1; chamfered edges; fans on the axis""" c = min(ch, (h1 - h0) * 0.3, r * 0.3) prof = [(0.0, h1), (r - c, h1), (r, h1 - c), (r, h0 + c), (r - c, h0), (0.0, h0)] g["lathe"](bm, prof, n, center=center, axis=axis, mat=mat) def hexbar(g, bm, center, axis, r, h0, h1, mat, ch=0.003): disc(g, bm, center, axis, r, h0, h1, mat, n=6, ch=ch) def cap_screw(g, bm, center, axis, h0, sign, mat, r=CAP_R, hh=CAP_H): """a socket cap screw head on `axis`, its underside at h0, rising in `sign` direction; a hex socket in its face""" a, b = (h0, h0 + hh) if sign > 0 else (h0 - hh, h0) top, bot = (b, a) if sign > 0 else (a, b) s = 1.0 if sign > 0 else -1.0 k = r * 0.36 # the hex socket dd = min(hh * 0.4, 0.016) prof = [(0.0, top - s * dd), (k, top - s * dd), (k, top), (r - 0.004, top), (r, top - s * 0.004), (r, bot), (0.0, bot)] if sign < 0: prof = [(p[0], p[1]) for p in prof] # orientation: lathe wants the outside on the left of travel; for sign < 0 the profile runs the other way round if sign > 0: g["lathe"](bm, prof, 24, center=center, axis=axis, mat=mat) else: g["lathe"](bm, list(reversed(prof)), 24, center=center, axis=axis, mat=mat) def pin(g, bm, center, axis, y0, y1, mat, cap_at=None): """a pin of PIN_R from y0 to y1 along `axis`; a cap screw head at y1 (or y0) facing away""" disc(g, bm, center, axis, PIN_R, y0, y1, mat, n=24, ch=0.002) if cap_at == "hi": cap_screw(g, bm, center, axis, y1, +1, mat) elif cap_at == "lo": cap_screw(g, bm, center, axis, y0, -1, mat) def turnbuckle(g, bm, L, steel, dark, ro=0.075, shank=0.034, nut=0.055, thread=0.027, body=0.07, body_len=0.40): """a turnbuckle link along +X from 0 to L: rod-end eyes, shanks, jam nuts, threaded rods, the hex body""" for x0, sgn in ((0.0, 1.0), (L, -1.0)): ring(g, bm, (x0, 0, 0), 'Y', BORE_R, ro, -ROD_T, ROD_T, steel, ch=0.006) a, b = (x0 + sgn * (ro - 0.01), x0 + sgn * (ro + 0.13)) disc(g, bm, (0, 0, 0), 'X', shank, min(a, b), max(a, b), steel, n=20, ch=0.003) a, b = (x0 + sgn * (ro + 0.13), x0 + sgn * (ro + 0.18)) hexbar(g, bm, (0, 0, 0), 'X', nut, min(a, b), max(a, b), steel, ch=0.004) a, b = (x0 + sgn * (ro + 0.18), L / 2 - sgn * body_len / 2) disc(g, bm, (0, 0, 0), 'X', thread, min(a, b), max(a, b), steel, n=20, ch=0.001) hexbar(g, bm, (0, 0, 0), 'X', body, L / 2 - body_len / 2, L / 2 + body_len / 2, dark, ch=0.008) for xm in (L / 2 - body_len / 2 + 0.03, L / 2 + body_len / 2 - 0.03): # its two collars hexbar(g, bm, (0, 0, 0), 'X', body + 0.006, xm - 0.02, xm + 0.02, steel, ch=0.003) def arm_beam(g, bm, L, ro, h, t, black, steel, boss=True): """an arm from 0 to L along +X with eyes at both ends (bore BORE_R), a forged beam between them""" for x0 in (0.0, L): ring(g, bm, (x0, 0, 0), 'Y', BORE_R + 0.012, ro, -EYE_T, EYE_T, black, ch=0.012) ring(g, bm, (x0, 0, 0), 'Y', BORE_R, BORE_R + 0.0115, -EYE_T - 0.002, EYE_T + 0.002, steel, ch=0.002) # the bush x0, x1 = ro * 0.7, L - ro * 0.7 g["rbox"](bm, (x1 - x0, t, h), min(0.035, h * 0.2), 3, M=Matrix.Translation(((x0 + x1) / 2, 0, 0)), mat=black) # the forging's pockets either side (a thinner web between thick edges, as on the sheet) for sy in (-1, 1): g["rbox"](bm, ((x1 - x0) * 0.62, 0.012, h * 0.52), 0.005, 1, M=Matrix.Translation(((x0 + x1) / 2, sy * (t / 2 - 0.004), 0)), mat=black) if boss: # the raised boss in the middle of the main arm, with its hole ring(g, bm, (L / 2, 0, 0), 'Y', 0.03, 0.085, t / 2 - 0.002, t / 2 + 0.02, black, ch=0.006) disc(g, bm, (L / 2, 0, 0), 'Y', 0.03, t / 2 - 0.004, t / 2 + 0.008, steel, n=20, ch=0.002) def wedge(bm, door_tree, cx, cy, z_top, hx, hy, mat, nx=6, ny=6, lift=0.001): """a block with a flat top at z_top over the footprint cx +- hx, cy +- hy (door-local), its underside following the door's inner face (rays down onto it, `lift` above it); all quads""" def bottom(x, y): hit = door_tree.ray_cast(Vector((x, y, z_top + 0.5)), Vector((0, 0, -1)), 5.0) if hit[0] is None: raise RuntimeError("the door mount's footprint runs off the door at (%.3f, %.3f)" % (x, y)) return hit[0].z + lift xs = [cx - hx + 2 * hx * i / nx for i in range(nx + 1)] ys = [cy - hy + 2 * hy * j / ny for j in range(ny + 1)] T = [[bm.verts.new((x, y, z_top)) for y in ys] for x in xs] B = [[bm.verts.new((x, y, bottom(x, y))) for y in ys] for x in xs] for i in range(nx): for j in range(ny): f = bm.faces.new((T[i][j], T[i + 1][j], T[i + 1][j + 1], T[i][j + 1])) f.material_index = mat f = bm.faces.new((B[i][j], B[i][j + 1], B[i + 1][j + 1], B[i + 1][j])) f.material_index = mat ring_idx = [(i, 0) for i in range(nx)] + [(nx, j) for j in range(ny)] + [(i, ny) for i in range(nx, 0, -1)] + [(0, j) for j in range(ny, 0, -1)] for k in range(len(ring_idx)): i0, j0 = ring_idx[k] i1, j1 = ring_idx[(k + 1) % len(ring_idx)] f = bm.faces.new((B[i0][j0], B[i1][j1], T[i1][j1], T[i0][j0])) f.material_index = mat bmesh.ops.recalc_face_normals(bm, faces=bm.faces[:]) return min(B[i][j].co.z for i in range(nx + 1) for j in range(ny + 1)) # ---- materials ----------------------------------------------------------------------------------------------------------- def materials(): import pd_look out = [] for name, kind in (("M_DA_Black", "black"), ("M_DA_Steel", "steel"), ("M_DA_Gunmetal", "gun")): m = bpy.data.materials.get(name) or bpy.data.materials.new(name) if kind == "steel": pd_look.steel(m, None) elif kind == "black": # black anodised forging, satin (the sheet's MFH 100) pd_look.dark(m, None, tint=(0.022, 0.023, 0.026), rough=(0.32, 0.50), dust=0.08) b = next(n for n in m.node_tree.nodes if n.type == 'BSDF_PRINCIPLED') b.inputs["Metallic"].default_value = 0.35 b.inputs["Coat Weight"].default_value = 0.25 b.inputs["Coat Roughness"].default_value = 0.3 else: pd_look.paint(m, None, **pd_look.STYLES["gunmetal"]) m["made_by"] = TAG out.append(m) return out def uv_box(me, size=1.0): """UV_Tile (and UVMap, the same): metres, box-projected""" for nm in ("UVMap", "UV_Tile"): if nm not in me.uv_layers: me.uv_layers.new(name=nm) for nm in ("UVMap", "UV_Tile"): uv = me.uv_layers[nm] for p in me.polygons: n = p.normal ax = max(range(3), key=lambda i: abs(n[i])) for li in p.loop_indices: co = me.vertices[me.loops[li].vertex_index].co uv.data[li].uv = ((co.y, co.z) if ax == 0 else ((co.x, co.z) if ax == 1 else (co.x, co.y))) # ---- the geometry of one arm --------------------------------------------------------------------------------------------- def layout(key): """where everything stands for one door, worked out from the scene: the trolley's travel on the rail, the arm's root, the bracket's pin with the door shut, the arm's length""" D = DOORS[key] door = bpy.data.objects[D["door"]] rail = bpy.data.objects[D["rail"]] wall = bpy.data.objects[D["wall"]] d0 = Vector(door["closed_location"]) travel = float(door["pocket_travel_m"]) inboard = float(door["clearance_inboard_m"]) s = D["side"] def bb(o): cs = [o.matrix_world @ Vector(c) for c in o.bound_box] return Vector((min(c.x for c in cs), min(c.y for c in cs), min(c.z for c in cs))), Vector((max(c.x for c in cs), max(c.y for c in cs), max(c.z for c in cs))) rlo, rhi = bb(rail) wlo, whi = bb(wall) rail_y = (rlo.y + rhi.y) / 2 if s > 0: wall_face, rail_inner = whi.x, rhi.x open_x = wall_face + STOP + BUMP + TL / 2 shut_x = open_x + travel ok = shut_x + TL / 2 <= rail_inner - 0.002 else: wall_face, rail_inner = wlo.x, rlo.x open_x = wall_face - STOP - BUMP - TL / 2 shut_x = open_x - travel ok = shut_x - TL / 2 >= rail_inner + 0.002 if not ok: raise RuntimeError("%s: the trolley does not fit its travel on the rail" % key) # the door mount: on the door's inner face, PLATE_X toward the pocket, its top level and a little over the face's # highest point under the footprint dl = door.data tree = BVHTree.FromPolygons([tuple(v.co) for v in dl.vertices], [tuple(p.vertices) for p in dl.polygons], epsilon=0.0) cx, cy = -s * PLATE_X, PLATE_Y hx, hy = WEDGE_HALF zmax = -99.0 for x in (cx - hx, cx, cx + hx): for y in (cy - hy, cy, cy + hy): hit = tree.ray_cast(Vector((x, y, 3.0)), Vector((0, 0, -1)), 10.0) if hit[0] is None: raise RuntimeError("%s: the door mount's footprint runs off the door" % key) zmax = max(zmax, hit[0].z) top_local = zmax + 0.06 G = d0 + Vector((cx, cy, top_local + G_ABOVE_TOP)) # world, door shut A = Vector((shut_x, rail_y, Z_A)) dX = G.x - A.x V = G.z - A.z r_shut = math.sqrt(dX * dX + (G.y - A.y) ** 2 + V * V) r_in = math.sqrt(dX * dX + (G.y + inboard - A.y) ** 2 + V * V) L = math.ceil(max(r_shut, r_in) / 1.88 * 100.0) / 100.0 # cos(alpha) never above 0.94 return dict(key=key, door=door, d0=d0, travel=travel, inboard=inboard, side=s, rail_y=rail_y, shut_x=shut_x, open_x=open_x, A=A, G=G, dX=dX, V=V, KY=G.y - d0.y - A.y, KZ=G.z - d0.z - A.z, L=L, mount=(cx, cy, top_local), tree=tree, r=(r_shut, r_in)) def ik(lay, dy, dz): """psi (yaw), phi1 (main arm), phi2 (link) for the door at (.., dy, dz): elbow down""" Y = dy + lay["KY"] V = dz + lay["KZ"] h = math.sqrt(lay["dX"] ** 2 + Y * Y) r = math.sqrt(h * h + V * V) th = math.atan2(V, h) al = math.acos(min(1.0, r / (2 * lay["L"]))) return math.atan2(Y, lay["dX"]), th - al, th + al # ---- building --------------------------------------------------------------------------------------------------------------- def exists(): return RECORD in bpy.data.texts def record(): return json.loads(bpy.data.texts[RECORD].as_string()) if exists() else None def _link(ob, coll): for c in list(ob.users_collection): c.objects.unlink(ob) coll.objects.link(ob) def _obj(g, bm, name, coll, mats, parent, loc, rec, mirror_y=False): ob = g["finish"](bm, name, coll, mats) me = ob.data if mirror_y: # the stack of the arm's two stages the other way round (the fore arm: away from its pocket's end) me.transform(Matrix.Scale(-1.0, 4, (0.0, 1.0, 0.0))) me.flip_normals() me.update() uv_box(me) me["made_for"] = "M-Ship v2 moon pool door arms (build/door_arms.py)" ob.parent = parent ob.matrix_parent_inverse = Matrix.Identity(4) ob.location = loc ob.rotation_mode = 'XYZ' ob["made_by"] = TAG rec["added"].append(ob.name) return ob def _drive(ob, path, index, expr, door): fc = ob.driver_add(path, index) d = fc.driver d.type = 'SCRIPTED' for v in list(d.variables): d.variables.remove(v) for nm, i in (("dx", 0), ("dy", 1), ("dz", 2)): if nm in expr: v = d.variables.new() v.name = nm v.type = 'SINGLE_PROP' v.targets[0].id_type = 'OBJECT' v.targets[0].id = door v.targets[0].data_path = "location[%d]" % i d.expression = expr return fc def build(key, rec, mats): g = _pd() lay = layout(key) BLACK, STEEL, GUN = 0, 1, 2 door = lay["door"] s = lay["side"] L = lay["L"] coll = bpy.data.collections[COLL] root = bpy.data.objects.get(SHIP_ROOT) pre = "DoorArm %s | " % key d0 = lay["d0"] # the arm's two stages stand either side of its centre plane; the main arm's side faces away from the pocket's end # wall (its local +Y turns toward the wall for the fore door), so the fore arm is built the other way round st = 1.0 if s > 0 else -1.0 mir = st < 0 # trolley ---------------------------------------------------------------------------------------------------------- bm = bmesh.new() zl = lambda z: z - Z_TROLLEY g["rbox"](bm, (TL, 0.64, 0.09), 0.012, 2, M=Matrix.Translation((0, 0, zl(Z_PLATE + 0.045))), mat=GUN) for sy in (-1, 1): g["rbox"](bm, (TL, 0.05, Z_CHEEK_TOP - Z_PLATE), 0.012, 2, M=Matrix.Translation((0, sy * 0.32, zl((Z_PLATE + Z_CHEEK_TOP) / 2))), mat=GUN) for sx in (-1, 1): # the guide rollers' axles disc(g, bm, (sx * 0.10, sy * 0.2615, 0), 'Z', 0.008, zl(Z_PLATE + 0.09), zl(-3.86), STEEL, n=12, ch=0.001) cap_screw(g, bm, (sx * 0.10, sy * 0.2615, 0), 'Z', zl(-3.86), +1, STEEL, r=0.014, hh=0.008) for sx in (-1, 1): # end bumpers g["rbox"](bm, (BUMP + 0.006, 0.05, 0.08), 0.005, 2, M=Matrix.Translation((sx * (TL / 2 + (BUMP - 0.006) / 2), sy * 0.32, zl(-3.86))), mat=BLACK) ring(g, bm, (0, 0, 0), 'Z', 0.104, 0.135, zl(-4.20), zl(Z_PLATE) + 0.002, STEEL, ch=0.005) # the yaw bearing's housing for k in range(8): a = TAU * (k + 0.5) / 8 cap_screw(g, bm, (0.12 * math.cos(a), 0.12 * math.sin(a), 0), 'Z', zl(-4.20), -1, STEEL, r=0.012, hh=0.008) trolley = _obj(g, bm, pre + "trolley", coll, mats, root, (lay["shut_x"], lay["rail_y"], Z_TROLLEY), rec) _drive(trolley, "location", 0, "dx + %.6f" % (lay["shut_x"] - d0.x), door) for sy in (-1, 1): for sx in (-1, 1): bm = bmesh.new() ring(g, bm, (0, 0, 0), 'Z', 0.0095, 0.03, -0.03, 0.03, BLACK, n=24, ch=0.004) ring(g, bm, (0, 0, 0), 'Z', 0.0095, 0.018, -0.032, 0.032, STEEL, n=16, ch=0.001) rl = _obj(g, bm, pre + "guide roller %s%s" % ("LR"[(sx + 1) // 2], "IO"[(sy + 1) // 2]), coll, mats, trolley, (sx * 0.10, sy * 0.2615, zl(-3.90)), rec) # rolling along the rail's side: one turn per 2*pi*R of travel, the two sides opposite _drive(rl, "rotation_euler", 2, "%s(dx - %.6f) / 0.03" % ("" if sy > 0 else "-", d0.x), door) # frame block (yaw) ----------------------------------------------------------------------------------------------- bm = bmesh.new() disc(g, bm, (0, 0, 0), 'Z', 0.10, -4.199 - Z_A, -4.152 - Z_A, STEEL, n=40, ch=0.004) # the turntable in the housing disc(g, bm, (0, 0, 0), 'Z', 0.09, 0.22, -4.19 - Z_A, BLACK, n=32, ch=0.008) # neck top_b = -4.215 - Z_A # 1.5 cm under the housing's screws g["rbox"](bm, (U + 0.30, 2 * BODY_T, top_b + 0.19), 0.04, 3, M=Matrix.Translation((U / 2, 0, (top_b - 0.19) / 2)), mat=BLACK) for x in (0.0, U): disc(g, bm, (x, 0, 0), 'Y', 0.10, 0.0, BODY_T + 0.006, BLACK, n=32, ch=0.008) # knuckle boss pin(g, bm, (x, 0, 0), 'Y', PIN_IN, PIN_OUT, STEEL, cap_at="hi") for x in (U * 0.25, U * 0.75): # cap screws into the block cap_screw(g, bm, (x, 0, -0.12), 'Y', -BODY_T, -1, STEEL, r=0.024, hh=0.016) cap_screw(g, bm, (U / 2, 0, 0.12), 'Y', BODY_T, +1, STEEL, r=0.02, hh=0.012) # set screw (as on the sheet) block = _obj(g, bm, pre + "frame block", coll, mats, trolley, (0, 0, Z_A - Z_TROLLEY), rec, mir) # main arm, link, turnbuckles ------------------------------------------------------------------------------------- bm = bmesh.new() arm_beam(g, bm, L, 0.13, 0.24, 2 * EYE_T, BLACK, STEEL, boss=True) main = _obj(g, bm, pre + "main arm", coll, mats, block, (0, st * W, 0), rec, mir) bm = bmesh.new() turnbuckle(g, bm, L, STEEL, BLACK) rod1 = _obj(g, bm, pre + "turnbuckle 1", coll, mats, block, (U, st * W, 0), rec, mir) bm = bmesh.new() g["rbox"](bm, (U + 0.24, 2 * BODY_T, 0.20), 0.035, 3, M=Matrix.Translation((U / 2, 0, 0)), mat=BLACK) g["rbox"](bm, (0.20, 2 * BODY_T, U + 0.24), 0.035, 3, M=Matrix.Translation((0, 0, U / 2)), mat=BLACK) for (x, z, cap) in ((0, 0, "both"), (U, 0, "hi"), (0, U, "lo")): if cap == "both": pin(g, bm, (x, 0, z), 'Y', -PIN_OUT, PIN_OUT, STEEL, cap_at="hi") cap_screw(g, bm, (x, 0, z), 'Y', -PIN_OUT, -1, STEEL) elif cap == "hi": pin(g, bm, (x, 0, z), 'Y', PIN_IN, PIN_OUT, STEEL, cap_at="hi") else: pin(g, bm, (x, 0, z), 'Y', -PIN_OUT, -PIN_IN, STEEL, cap_at="lo") disc(g, bm, (x, 0, z), 'Y', 0.10, -BODY_T - 0.006, BODY_T + 0.006, BLACK, n=32, ch=0.008) knuckle = _obj(g, bm, pre + "elbow knuckle", coll, mats, main, (L, -st * W, 0), rec, mir) bm = bmesh.new() arm_beam(g, bm, L, 0.11, 0.18, 2 * EYE_T - 0.01, BLACK, STEEL, boss=False) link = _obj(g, bm, pre + "link", coll, mats, knuckle, (0, -st * W, 0), rec, mir) bm = bmesh.new() turnbuckle(g, bm, L, STEEL, BLACK) rod2 = _obj(g, bm, pre + "turnbuckle 2", coll, mats, knuckle, (0, -st * W, U), rec, mir) # door bracket and its disc ---------------------------------------------------------------------------------------- bm = bmesh.new() g["rbox"](bm, (0.22, 2 * BODY_T, U + 0.32), 0.035, 3, M=Matrix.Translation((0, 0, -0.21 + (U + 0.32) / 2)), mat=BLACK) for z in (0.0, U): pin(g, bm, (0, 0, z), 'Y', -PIN_OUT, -PIN_IN, STEEL, cap_at="lo") disc(g, bm, (0, 0, z), 'Y', 0.10, -BODY_T - 0.006, BODY_T + 0.006, BLACK, n=32, ch=0.008) disc(g, bm, (0, 0, 0), 'Z', 0.20, -0.25, -0.20, STEEL, n=48, ch=0.006) # the disc (upper race) for k in range(8): a = TAU * (k + 0.5) / 8 cap_screw(g, bm, (0.165 * math.cos(a), 0.165 * math.sin(a), 0), 'Z', -0.20, +1, STEEL, r=0.016, hh=0.01) bracket = _obj(g, bm, pre + "door bracket", coll, mats, link, (L, st * W, 0), rec, mir) # the door mount (on the door) ------------------------------------------------------------------------------------- cx, cy, top = lay["mount"] # (the wedge is built in the mount's own frame: origin at the top centre) tree_local = lay["tree"] class Shifted: def ray_cast(self, o, d, dist): hit = tree_local.ray_cast(o + Vector((cx, cy, top)), d, dist) return (hit[0] - Vector((cx, cy, top)) if hit[0] is not None else None,) + tuple(hit[1:]) bm = bmesh.new() wedge(bm, Shifted(), 0.0, 0.0, 0.0, WEDGE_HALF[0], WEDGE_HALF[1], GUN) disc(g, bm, (0, 0, 0), 'Z', 0.21, -0.001, 0.05, STEEL, n=48, ch=0.006) # the lower race for sx in (-1, 1): for sy in (-1, 1): cap_screw(g, bm, (sx * (WEDGE_HALF[0] - 0.045), sy * (WEDGE_HALF[1] - 0.045), 0), 'Z', 0.0, +1, STEEL, r=0.024, hh=0.015) mount = _obj(g, bm, pre + "door mount", coll, mats, door, (cx, cy, top), rec) # drivers ---------------------------------------------------------------------------------------------------------- dX, KY, KZ = lay["dX"], lay["KY"], lay["KZ"] Ys = "(dy + %.6f)" % KY Vs = "(dz + %.6f)" % KZ hs = "sqrt(%.6f + %s*%s)" % (dX * dX, Ys, Ys) rs = "sqrt(%.6f + %s*%s + %s*%s)" % (dX * dX, Ys, Ys, Vs, Vs) th = "atan2(%s, %s)" % (Vs, hs) al = "acos(min(1.0, %s / %.6f))" % (rs, 2 * L) _drive(block, "rotation_euler", 2, "atan2(%s, %.6f)" % (Ys, dX), door) # pitch about +Y turns +X downward: a link at angle phi (up positive) has rotation_y = -phi _drive(main, "rotation_euler", 1, "%s - %s" % (al, th), door) # -(th - al) _drive(rod1, "rotation_euler", 1, "%s - %s" % (al, th), door) _drive(knuckle, "rotation_euler", 1, "%s - %s" % (th, al), door) # back to level _drive(link, "rotation_euler", 1, "-(%s + %s)" % (th, al), door) # -(th + al) _drive(rod2, "rotation_euler", 1, "-(%s + %s)" % (th, al), door) _drive(bracket, "rotation_euler", 1, "%s + %s" % (th, al), door) for ob in (trolley, block, main, rod1, knuckle, link, rod2, bracket, mount): ob["closed_location"] = list(ob.location) return dict(layout={k: (list(v) if isinstance(v, Vector) else v) for k, v in lay.items() if k not in ("door", "tree")}, objects=[o.name for o in (trolley, block, main, rod1, knuckle, link, rod2, bracket, mount)]) def apply(): if exists(): raise RuntimeError("the door arms are already there (text block %r); remove() takes them out" % RECORD) for D in DOORS.values(): for n in (D["door"], D["rail"], D["wall"]) + D["carriages"]: if bpy.data.objects.get(n) is None: raise RuntimeError("not in this file: " + n) parent = bpy.data.collections[PARENT_COLL] coll = bpy.data.collections.get(COLL) if coll is None: coll = bpy.data.collections.new(COLL) parent.children.link(coll) rec = {"added": [], "hidden": [], "materials": [], "collection": COLL} try: mats = materials() rec["materials"] = [m.name for m in mats] out = {} for key in DOORS: out[key] = build(key, rec, mats) for D in DOORS.values(): for n in D["carriages"]: o = bpy.data.objects[n] rec["hidden"].append([n, o.hide_viewport, o.hide_render, o.hide_get()]) o.hide_viewport = True o.hide_render = True except Exception: _remove(rec) raise txt = bpy.data.texts.new(RECORD) txt.write(json.dumps(rec, indent=1)) bpy.context.view_layer.update() drivers = [(o.name, fc.data_path, fc.array_index, fc.driver.is_simple_expression, fc.driver.is_valid) for o in (bpy.data.objects[n] for n in rec["added"]) if o.animation_data for fc in o.animation_data.drivers] return {"added": len(rec["added"]), "hidden": [h[0] for h in rec["hidden"]], "doors": out, "drivers_not_simple": [d for d in drivers if not d[3] or not d[4]], "drivers": len(drivers)} def remove(): rec = record() if rec is None: return {"present": False} out = _remove(rec) bpy.data.texts.remove(bpy.data.texts[RECORD]) return out def _remove(rec): n = 0 for name in reversed(rec["added"]): o = bpy.data.objects.get(name) if o is None: continue me = o.data bpy.data.objects.remove(o, do_unlink=True) if me is not None and me.users == 0: bpy.data.meshes.remove(me) n += 1 for name, hv, hr, hs in rec.get("hidden", []): o = bpy.data.objects.get(name) if o is not None: o.hide_viewport, o.hide_render = hv, hr c = bpy.data.collections.get(COLL) if c is not None and not c.all_objects: bpy.data.collections.remove(c) for m in rec.get("materials", []): mat = bpy.data.materials.get(m) if mat is not None and mat.users == 0: bpy.data.materials.remove(mat) return {"taken_out": n, "shown_again": [h[0] for h in rec.get("hidden", [])]} # ---- checking --------------------------------------------------------------------------------------------------------------- def _geo(o, dg): ev = o.evaluated_get(dg) me = ev.to_mesh() mw = ev.matrix_world me.calc_loop_triangles() vs = [mw @ v.co for v in me.vertices] ts = [tuple(t.vertices) for t in me.loop_triangles] ev.to_mesh_clear() if not ts: return None tree = BVHTree.FromPolygons([tuple(v) for v in vs], ts, epsilon=0.0) lo = Vector((min(v.x for v in vs), min(v.y for v in vs), min(v.z for v in vs))) hi = Vector((max(v.x for v in vs), max(v.y for v in vs), max(v.z for v in vs))) return (vs, ts, tree, lo, hi) def check(step=2): """every part of both arms through the doors' whole timeline against the ship round them, the doors, each other; the bracket's pin on the door mount (how far the arm's end leaves the door, at most)""" import boarding_gate as bgate rec = record() if rec is None: return {"present": False} scene = bpy.data.scenes.get("M-Ship v2 | REFERENCE ASSET") or bpy.context.scene mine = [bpy.data.objects[n] for n in rec["added"] if n in bpy.data.objects and bpy.data.objects[n].type == 'MESH'] box = (-9.6, 15.8, -10.0, 0.5, -11.0, -3.3) others = [] for o in scene.objects: if o.type != 'MESH' or o in mine or o.name.startswith(("UCX_", "KIT | ")) or not o.visible_get(): continue if any(c.name.startswith(("11 KIT", "13 KIT")) for c in o.users_collection): continue lo, hi = Vector((1e9,) * 3), Vector((-1e9,) * 3) for c in o.bound_box: w = o.matrix_world @ Vector(c) lo = Vector(map(min, lo, w)); hi = Vector(map(max, hi, w)) if hi.x < box[0] or lo.x > box[1] or hi.y < box[2] or lo.y > box[3] or hi.z < box[4] or lo.z > box[5]: continue others.append(o) was = scene.frame_current clashes, gap_max, near = {}, {}, {} fr = list(range(scene.frame_start, scene.frame_end + 1, step)) if scene.frame_end not in fr: fr.append(scene.frame_end) static_cache = {} dg = bpy.context.evaluated_depsgraph_get() for f in fr: scene.frame_set(f) dg = bpy.context.evaluated_depsgraph_get() geo = {o.name: _geo(o, dg) for o in mine} ogeo = {} for o in others: if o.animation_data or (o.parent and o.parent.animation_data) or o.name.startswith("Moonpool_Door"): ogeo[o.name] = _geo(o, dg) else: if o.name not in static_cache: static_cache[o.name] = _geo(o, dg) ogeo[o.name] = static_cache[o.name] names = list(geo) for i, a in enumerate(names): ga = geo[a] if ga is None: continue for b in names[i + 1:]: gb = geo[b] if gb is None: continue k = bgate._crossing(ga, gb) if k: key = a + " x " + b c = clashes.setdefault(key, {"first": f, "last": f, "pairs": 0}) c["last"] = f c["pairs"] = max(c["pairs"], k) for b, gb in ogeo.items(): if gb is None: continue if " | door mount" in a and bpy.data.objects[a].parent and bpy.data.objects[a].parent.name == b: continue # the mount sits on its own door (1 mm off its face) k = bgate._crossing(ga, gb) if k: key = a + " x " + b c = clashes.setdefault(key, {"first": f, "last": f, "pairs": 0}) c["last"] = f c["pairs"] = max(c["pairs"], k) # the bracket's disc stays on its mount: distance from the bracket's pin to where it should be for key in DOORS: br = bpy.data.objects.get("DoorArm %s | door bracket" % key) mt = bpy.data.objects.get("DoorArm %s | door mount" % key) if br is None or mt is None: continue want = mt.matrix_world @ Vector((0, 0, G_ABOVE_TOP)) got = br.matrix_world.translation e = (want - got).length tilt = (br.matrix_world.to_3x3() @ Vector((0, 0, 1))).angle(Vector((0, 0, 1))) gm = gap_max.setdefault(key, {"pin_mm": 0.0, "tilt_deg": 0.0}) gm["pin_mm"] = max(gm["pin_mm"], round(e * 1000, 3)) gm["tilt_deg"] = max(gm["tilt_deg"], round(math.degrees(tilt), 4)) scene.frame_set(was) return {"present": True, "frames": len(fr), "others": len(others), "clashes": clashes, "arm_end_on_mount": gap_max}