"""Door robot arm: design.json for the scale-up and the door-centre attach (2026-10-18), made from design_base.json. The user: "make it bigger so those can withstand the pressure to open that door under the water" and "attach the main plate to the doors center and make it bigger". The layout (DESIGN.md addendum "2026-10-18 scale-up and door-centre attach", proved by dra_fit3d_proof.py -> build/fit3d_proof.json): - the whole machine 1.6x, anchored at the rail line under the deck (y -3.80, the deck's underside z -3.755); - the upper arm (J3 -> J4) 0.2312 m longer along its x (build units, x 1.6 in the ship: +20 % of J3 -> wrist centre with the forearm), the forearm (J4 -> wrist centre) +20 %; the lower arm and the tools only scaled; - the J3 cylinder's knob 14 % closer to J3 and turned 2 deg about it (window -30.25..+65.5 deg); J1 +-185 deg; - both versions at the door's centre (door_centre.json); version B's plate 1.2 m square on a 1.3 m conformal pad; - the carriage c = A + B * door x from c -3.871 (door shut) to c -5.50 (open), AFT; FORE mirrored about x 3.05. Everything the build reads is in metres in the ship (scaled); the part builders work in build units (x 1/S) and dra_rigdata scales their meshes. python build/dra_scaleup.py (writes design.json; design_base.json stays as it was) """ import copy import json import math import os import sys HERE = os.path.dirname(os.path.abspath(__file__)) ROOT = os.path.dirname(HERE) sys.path.insert(0, HERE) import design_drivers as DD # noqa: E402 S = 1.6 RAIL_Y = -3.80 DECK = -3.755 # the deck's underside MIRROR = 3.05 UPPER_DX = 0.2312 # build units: J4 moves this far along x (the housing's top stays level) K_FORE = 1.2 KNOB = (0.84, -4.5) # the knob: toward J3 by this factor, turned (deg) about J3 in the arm's plane (the # proof used 0.86 / -2; the build's sweeps moved it: the truss met the lower arm's # crotch 2 deg past version B's deepest elbow angle) J3_LIM = [-28.75, 66.0] # low: the cylinder's stroke (bore 0.535 .. 1.26 build units; the forearm's hose meets # the turret at -33.25); high: nothing touches up to 70 (fine sweep), 4 deg kept J2_LIM = [-72.0, 43.0] # the lower arm's root straps meet the turret's back at +44.0 (fine sweep); free to -72 J5_LIM = [-90.5, 120.0] # the tool hub meets the wrist block's core at -91.0 (J6 at rest) .. -92.0 (J6 on the path) TOOL_ROLL_A = {"AFT": 10.0, "FORE": -10.0} # version A grips the round knob turned this much about its axis (deg), # so the finger motors pass between the wrist block's cheeks at the # deepest wrist bends (J6 75-84 there instead of 65-74) J1_LIM = [-185.0, 185.0] PLATE_B = 1.2 # version B's plate (m, square) PAD_B = 1.3 # its conformal pad (m, square) RAIL_END_AFT = -3.12 # the beam's inner end: 30 mm short of the deck's edge at the beam's outer side (x -3.09) C_OPEN_AFT = -5.50 GAP = 0.0165 # the strike pad to its buffer at a travel end (10.3 mm x S, as before) BUF = 0.048 # rubber buffer thickness (0.030 x S) END_PLATE = 0.0192 # the end stop's plate on the beam's end face (0.012 x S) CHAIN_LINKS = 15 BANDS = {"upper arm": {"default": [0.817, 0.839]}, "forearm": {"default": [0.598, 0.598], "F07": [0.523, 0.535], "F02": [0.54, 0.58], "F08": [0.65, 0.71], "F08f": [0.70, 0.70]}} def r(v, n=6): if isinstance(v, (list, tuple)): return [r(x, n) for x in v] return round(float(v), n) def sub(a, b): return [a[i] - b[i] for i in range(3)] def add(a, b): return [a[i] + b[i] for i in range(3)] def mul(a, k): return [x * k for x in a] def zs(z): """a height scaled about the deck's underside""" return DECK + S * (z - DECK) def main(): base = json.load(open(os.path.join(ROOT, "design_base.json"))) dc = json.load(open(os.path.join(HERE, "door_centre.json"))) D = copy.deepcopy(base) K = D["kinematics"] J = {k: v["origin"] for k, v in base["kinematics"]["joints"].items()} J2, J3, J4, WC = J["J2"], J["J3"], J["J4"], J["J5"] # build-unit joints (x 1/S): lengths changed, the lower arm as it was J4n = add(J4, [UPPER_DX, 0.0, 0.0]) WCn = add(J4n, mul(sub(WC, J4), K_FORE)) kn0 = sub(base["kinematics"]["rod"]["knob_rest_A"], J3) f, rot = KNOB a = math.radians(rot) ka, kb = kn0[0] * f, kn0[2] * f knob_u = [ka * math.cos(a) - kb * math.sin(a), 0.0, ka * math.sin(a) + kb * math.cos(a)] KNOBn = add(J3, knob_u) TR = base["kinematics"]["rod"]["trunnion_rest_A"] unit = dict(J2=J2, J3=J3, J4=J4n, WC=WCn, TRUNNION=TR, KNOB=KNOBn) sc = {k: mul(v, S) for k, v in unit.items()} # kinematics (metres, arm frame A) for jn, p in (("J2", sc["J2"]), ("J3", sc["J3"]), ("J4", sc["J4"]), ("J5", sc["WC"]), ("J6", sc["WC"])): K["joints"][jn]["origin"] = r(p) K["joints"]["J1"]["limits"] = J1_LIM K["joints"]["J3"]["limits"] = J3_LIM K["joints"]["J2"]["limits"] = J2_LIM K["joints"]["J5"]["limits"] = J5_LIM L2v = sub(sc["J3"], sc["J2"]) L3v = sub(sc["WC"], sc["J3"]) K["L2"] = r(math.hypot(L2v[0], L2v[2])) K["L3"] = r(math.hypot(L3v[0], L3v[2])) K["phi_l0_rad"] = r(math.atan2(L2v[2], L2v[0])) K["beta_rad"] = r(math.atan2(L3v[2], L3v[0])) K["d4"] = r(L3v[0]) K["a3"] = r(L3v[2]) K["j2_r"] = r(-sc["J2"][0]) K["j2_h"] = r(sc["J2"][2]) K["wc_rest_A"] = r(sc["WC"]) K["j3_rest_A"] = r(sc["J3"]) K["flange_from_wc"] = r(0.132 * S) K["scale_m_per_px"] = 0.0022 K["scale_note"] = ("the part builders work in build units (ref1 px x 0.0022 m); the meshes are scaled %.2fx about the " "turntable face; every number in this file is in metres after the scale" % S) rod = K["rod"] rod["trunnion_rest_A"] = r(sc["TRUNNION"]) rod["knob_rest_A"] = r(sc["KNOB"]) rod["trunnion_in_lower"] = r(sub(sc["TRUNNION"], sc["J2"])) rod["knob_in_upper"] = r(sub(sc["KNOB"], sc["J3"])) d0 = sub(sc["KNOB"], sc["TRUNNION"]) rod["length_rest"] = r(math.hypot(d0[0], d0[2])) rod["rod_len"] = r(0.800 * S) rod["bore"] = r([-0.270 * S, 0.500 * S]) rod["stroke_window_build_units"] = [0.535, 1.26] rod["type"] = ("double-acting hydraulic cylinder on a middle trunnion (the knee pin); barrel %.3f m behind and %.3f m " "ahead of the trunnion; rod %.3f m from its eye to the piston; bore floor %.3f m behind the trunnion" % (0.30 * S, 0.50 * S, 0.80 * S, 0.27 * S)) # tools: wrist centre -> door face pad_top_B = round(0.012 + dc["AFT"]["sag_under_plate_m"]["1.3"][1], 4) dB = 0.132 * S + 0.130 * S + 0.044 * S + pad_top_B dA = 0.63 * S T = D["tools"] T["A"].update(d_t=r(dA), wc_to_flange=r(0.132 * S), hub_len=r(0.165 * S), hub_dia=r(0.275 * S), stud_head_centre_from_wc=r(0.44 * S), door_pad=r(0.020 * S), pad_size=r(0.32 * S), note="WC -> door face along the approach axis: version A as before, x %.1f" % S) T["B"].update(d_t=r(dB), wc_to_flange=r(0.132 * S), neck_len=r(0.130 * S), neck_dia=r([0.154 * S, 0.260 * S]), plate=r([PLATE_B, PLATE_B, 0.044 * S]), door_pad=pad_top_B, pad_size=PAD_B, note=("WC -> door face: flange %.4f + neck %.3f + plate %.4f + pad top %.4f (the pad is %.1f m square, 12 mm " "thick where the hollow face rises most, at its edges); B solves its own pose" % (0.132 * S, 0.130 * S, 0.044 * S, pad_top_B, PAD_B))) d_t = {"A": dA, "B": dB} # carriage, rail, schedule half = 0.429 * S c_closed = RAIL_END_AFT - BUF - GAP - half car = D["carriage"] car["size"] = dict(along_rail=r(0.858 * S), across=r(0.858 * S), height=r(0.407 * S), ref_px=[390, 390, 185]) car["underside_z"] = r(zs(base["carriage"]["underside_z"])) car["top_z"] = r(zs(base["carriage"]["top_z"])) car["centre_y"] = RAIL_Y cF_closed, cF_open = 2 * MIRROR - c_closed, 2 * MIRROR - C_OPEN_AFT car["travel_ends"] = {"AFT": r([C_OPEN_AFT, c_closed], 4), "FORE": r([cF_closed, cF_open], 4)} hw = (base["scene"]["rail"]["y"][1] - base["scene"]["rail"]["y"][0]) / 2 * S rail = D["scene"]["rail"] ew = base["scene"]["end_wall_faces_x"] rail.update(AFT=r([ew["AFT"], RAIL_END_AFT], 4), FORE=r([2 * MIRROR - RAIL_END_AFT, ew["FORE"]], 4), y=r([RAIL_Y - hw, RAIL_Y + hw], 5), z=r([zs(base["scene"]["rail"]["z"][0]), DECK], 4), usable_AFT=r([ew["AFT"], RAIL_END_AFT], 4), usable_FORE=r([2 * MIRROR - RAIL_END_AFT, ew["FORE"]], 4), beam=("the arm's own rail beam (DoorArm_ | rail beam), %.3f x %.3f m, from the pocket's end wall to " "%.2f m short of the deck's edge, its top on the deck's underside; the user's rail boxes are hidden" % (2 * hw, DECK - zs(base["scene"]["rail"]["z"][0]), 0.03)), stops=dict(buffer=BUF, gap=GAP, end_plate=END_PLATE, note="end stops at both travel ends: rubber buffer on a steel block; at the inner end the " "block is the beam's end plate")) for key in ("AFT", "FORE"): dr = D["doors"][key] att, n = dc[key]["attach_local"], dc[key]["normal_local"] xt = [-n[0], -n[1], -n[2]] yt = [-1.0, 0.0, 0.0] zt = [xt[1] * yt[2] - xt[2] * yt[1], xt[2] * yt[0] - xt[0] * yt[2], xt[0] * yt[1] - xt[1] * yt[0]] C = lambda v: [v[0], -v[1] + 0.0, -v[2] + 0.0] dr["attach_local"] = r(att) dr["normal_local"] = r(n, 9) dr["tool_axes_world"] = dict(x_t=r(xt, 9), y_t=yt, z_t=r(zt, 9)) dr["tool_axes_C"] = dict(x_t=r(C(xt), 9), y_t=C(yt), z_t=r(C(zt), 9)) dl = math.radians(TOOL_ROLL_A[key]) ytA = [yt[i] * math.cos(dl) + zt[i] * math.sin(dl) for i in range(3)] ztA = [-yt[i] * math.sin(dl) + zt[i] * math.cos(dl) for i in range(3)] dr["tool_roll_A_deg"] = TOOL_ROLL_A[key] dr["tool_axes_world_A"] = dict(x_t=r(xt, 9), y_t=r(ytA, 9), z_t=r(ztA, 9)) dr["tool_axes_C_A"] = dict(x_t=r(C(xt), 9), y_t=r(C(ytA), 9), z_t=r(C(ztA), 9)) dr["wc_local"] = {v: r([att[i] + d_t[v] * n[i] for i in range(3)]) for v in ("A", "B")} cc, co = (c_closed, C_OPEN_AFT) if key == "AFT" else (cF_closed, cF_open) B = (cc - co) / (dr["closed_x"] - dr["open_x"]) A = cc - B * dr["closed_x"] dr["carriage"] = dict(c_closed=r(cc, 4), c_open=r(co, 4), travel=r(abs(cc - co), 4), formula="c = c_open + (door_x - open_x) * (c_closed - c_open) / (closed_x - open_x)", A=r(A), B=r(B), note="c = A + B * door_x; the carriage runs at B of the door's speed") dr["rail"] = "DoorArm_%s | rail beam" % key dr["attach_note"] = ("the door's centre: the middle of its length (local x) and of its curved inner face by arc " "length (build/door_centre.json)") # energy chain (x S), the fixed end at mid-travel for key in ("AFT", "FORE"): ch = D["energy_chain"][key] cc, co = car["travel_ends"][key][1 if key == "AFT" else 0], car["travel_ends"][key][0 if key == "AFT" else 1] sgn = ch["bend_toward"] R = 0.15 * S p = 0.075 * S Lc = CHAIN_LINKS * p Fx = (cc + co) / 2 ym = RAIL_Y + (base["energy_chain"][key]["y_moving_run"] - base["carriage"]["centre_y"]) * S yf = RAIL_Y + (base["energy_chain"][key]["y_fixed_run"] - base["carriage"]["centre_y"]) * S ty = [RAIL_Y + (y - base["carriage"]["centre_y"]) * S for y in base["energy_chain"][key]["tray"]["y"]] # the loop's far point and the moving end's reach over the travel bx_min = min((m + Fx - Lc + math.pi * R) / 2 for m in (cc, co)) if sgn < 0 else None bx_max = max((m + Fx + Lc - math.pi * R) / 2 for m in (cc, co)) if sgn > 0 else None if key == "AFT": tx = [round(bx_min - R - 0.17, 3), round(max(cc, co) + 0.16, 3)] else: tx = [round(min(cc, co) - 0.16, 3), round(bx_max + R + 0.17, 3)] ch.update(fixed_x=r(Fx, 4), length=r(Lc, 4), bend_radius=r(R, 4), pitch=r(p, 4), links=CHAIN_LINKS, y_moving_run=r(ym, 4), y_fixed_run=r(yf, 4), moving_offset=0.0, z_bottom=r(zs(base["energy_chain"][key]["z_bottom"]), 4), z_top=r(zs(base["energy_chain"][key]["z_top"]), 4), link_size=r([0.075 * S, 0.06 * S, 0.1 * S], 4)) ch["tray"] = dict(x=tx, y=r(ty, 4), floor_top_z=r(zs(base["energy_chain"][key]["tray"]["floor_top_z"]), 4), wall_top_z=r(zs(base["energy_chain"][key]["tray"]["wall_top_z"]), 4), end_slant={"at_y": r(ty, 4), "x": [tx[1], tx[1]] if key == "AFT" else [tx[0], tx[0]]}, note="only where the chain runs (the travel is %.2f m); square ends, well under the deck" % abs(cc - co)) # the showroom (x S) sh = D["showroom"] RACK_PITCH = math.pi * 0.008 sh["arms"]["A"]["location"] = r([-1.9 * S, 0.0, 1.012 * S], 4) sh["arms"]["B"]["location"] = r([(-1.9 + 151 * RACK_PITCH) * S, 0.0, 1.012 * S], 4) # the driver plan (the rig builds its own from these numbers; the scene reads the chain's) D["drivers"] = {} for key in ("AFT", "FORE"): b = DD.build(D, key) csol, cdrv = DD.chain_drivers(D, key) D["drivers"][key] = dict(solver_empties=b["solvers"] + [csol], joint_and_solver_drivers=b["drivers"], chain_link_drivers=cdrv, piston_rest_location=r(b["piston_rest"]), rod_axis=r(b["rod_axis"]), rod_L0=r(b["rod_L0"]), cyl_beta0_rad=r(b["cyl_beta0"])) proof = json.load(open(os.path.join(HERE, "fit3d_proof.json"))) D["scale_up"] = dict( date="2026-10-18", S=S, rail_y=RAIL_Y, anchor_z=DECK, upper_dx_build=UPPER_DX, k_fore=K_FORE, knob=list(KNOB), build_units=dict((k, r(v)) for k, v in unit.items()), bands=BANDS, lengths_m=dict(lower_arm=r(K["L2"]), upper_arm=r(math.hypot(*[sc["J4"][i] - sc["J3"][i] for i in (0, 2)])), forearm=r(sc["WC"][0] - sc["J4"][0]), j3_to_wc=r(K["L3"])), length_changes_pct=dict(upper_arm=r(100 * (math.hypot(1.133 + UPPER_DX, 0.2354) / math.hypot(1.133, 0.2354) - 1), 2), forearm=r(100 * (K_FORE - 1), 2), lower_arm=0.0, j3_to_wc=r(100 * (math.hypot(2.167 + UPPER_DX + 0.2068, 0.2354) / 2.17975 - 1), 2)), plate_B=PLATE_B, pad_B=PAD_B, pad_top_B=pad_top_B, d_t={k: r(v) for k, v in d_t.items()}, c_closed_AFT=r(c_closed, 4), c_open_AFT=C_OPEN_AFT, rail_end_AFT=RAIL_END_AFT, j3_limits=J3_LIM, j1_limits=J1_LIM, j2_limits=J2_LIM, j5_limits=J5_LIM, tool_roll_A_deg=TOOL_ROLL_A, proof=dict(file="build/fit3d_proof.json", design=proof.get("design"), c_closed=proof.get("c_closed"), c_open=proof.get("c_open"), note="proved with the upper arm's J4 at 1.2 x (J4 - J3); built with J4 0.2312 along x (the same J3 -> " "wrist centre length, its angle 1.04 deg lower: q3 1.04 deg higher everywhere, still >= 6.2 deg " "inside the limits)"), why="DESIGN.md, addendum 2026-10-18 scale-up and door-centre attach") D["meta"]["date"] = "2026-10-18" D["meta"]["status"] = "scale-up 1.6x and door-centre attach (the Coder), from design_base.json by build/dra_scaleup.py" with open(os.path.join(ROOT, "design.json"), "w") as fh: json.dump(D, fh, indent=1) print("design.json written: S %.2f, c %.4f -> %.4f (AFT), B %.4f, d_t A %.4f B %.4f, L2 %.4f L3 %.4f beta %.5f" % (S, c_closed, C_OPEN_AFT, D["doors"]["AFT"]["carriage"]["B"], dA, dB, K["L2"], K["L3"], K["beta_rad"])) print("rail", rail["AFT"], rail["y"], rail["z"], "tray", D["energy_chain"]["AFT"]["tray"]["x"], "chain fixed", D["energy_chain"]["AFT"]["fixed_x"]) if __name__ == "__main__": main()