"""Writes design.json: every number the builder needs. Run with plain Python (no bpy): python build/design_make_json.py Lengths in metres, angles in degrees unless a key says _rad. Ship world coordinates, Z up. Reference pixels (ref1 side view, 2000 px original) are kept beside the metres where a size was measured. """ import json, math, os, sys HERE = os.path.dirname(os.path.abspath(__file__)) ROOT = os.path.dirname(HERE) sys.path.insert(0, HERE) import design_kin as K import design_fit as F from design_parts import PARTS, MATERIALS, MARKINGS, TRI_BUDGET S = float(os.environ.get("DRA_S", 0.0022)) # metres per ref1 pixel (one scale for the whole arm) U_FACE = float(os.environ.get("DRA_U", 0.27)) # attach point: fraction down the door's inner face polyline PX_ATTACH = float(os.environ.get("DRA_PX", 2.10)) # door-local |x| of the attach point (toward the pocket: -x AFT, +x FORE) CLE = float(os.environ.get("DRA_CLE", 0.45)) # extra carriage stand-off at the end wall (on top of the 0.04 stop) OUT_JSON = os.environ.get("DRA_OUT", os.path.join(ROOT, "design.json")) def r(v, n=4): if isinstance(v, (list, tuple)): return [r(x, n) for x in v] return round(float(v), n) def px(v): return r(v * S) arm = K.Arm(S) (fy, fz), (ny, nz) = F.face_point(U_FACE) Lc, Wc, Hc = F.carriage(S) zb = -3.775 - Hc yb = (F.RAIL["y0"] + F.RAIL["y1"]) / 2 doors = {} for key, dn, side in (("AFT", "Moonpool_Door_Aft", +1), ("FORE", "Moonpool_Door_Fore", -1)): Sa = F.setup(S, U_FACE, -PX_ATTACH, CLE, "prop", dn, "A") tr = F.TRACK[dn] # tool roll about the normal: of the four square-symmetric choices, the one that keeps J6 nearest 0 over the track best = None for roll in (0.0, math.pi / 2, math.pi, -math.pi / 2): mm_ = F.assess(Sa, roll, -1) if mm_["fail"]: continue q6 = mm_["q"]["q6"] score = max(abs(q6[0]), abs(q6[1])) if best is None or score < best[0]: best = (score, roll) roll = best[1] xt, yt, zt = F.tool_axes(Sa, roll) doors[key] = dict( door=dn, side=side, closed_location=r(tr[0]), open_x=r(Sa["x_op"], 5), closed_x=r(Sa["x_cl"], 5), retracted_y=-4.88, closed_y=-6.1, z=-7.503, rail=("Pocket %s | upper rail" % key), end_wall=("Pocket %s | conformal closed end" % key), attach_local=r((-PX_ATTACH * side, fy, fz), 6), normal_local=r((0.0, ny, nz), 9), tool_axes_world=dict(x_t=r(xt, 9), y_t=r(yt, 9), z_t=r(zt, 9)), tool_roll_deg=r(math.degrees(roll), 2), tool_axes_C=dict(x_t=r(K.w2c(xt), 9), y_t=r(K.w2c(yt), 9), z_t=r(K.w2c(zt), 9)), carriage=dict(c_closed=r(Sa["c_hi"]), c_open=r(Sa["c_lo"]), travel=r(Sa["T"]), formula="c = c_open + (door_x - open_x) * (c_closed - c_open) / (closed_x - open_x)", A=r(Sa["c_lo"] - Sa["x_op"] * (Sa["c_hi"] - Sa["c_lo"]) / (Sa["x_cl"] - Sa["x_op"]), 6), B=r((Sa["c_hi"] - Sa["c_lo"]) / (Sa["x_cl"] - Sa["x_op"]), 6), note="c = A + B * door_x; the carriage runs at B of the door's speed"), wrist_branch=-1, ) tools = { "A": dict(name="gripper (grab limbs)", d_t=r(F.tool_len(S, "A")), wc_to_flange=px(60), hub_len=px(75), hub_dia=px(125), stud_head_centre_from_wc=0.44, door_pad=0.02, note="WC -> door face along the approach axis: flange 0.132 + hub 0.165 + finger reach to the stud head + stud 0.19 above the door face"), "B": dict(name="plate (red sketch, ref6)", d_t=r(F.tool_len(S, "B")), wc_to_flange=px(60), neck_len=r(F.tool_len(S, "B") - px(60) - px(20) - 0.02), neck_dia=[px(70), px(133)], plate=[px(250), px(250), px(20)], door_pad=0.02, note="WC -> door face: flange 0.132 + neck + plate 0.044 + conformal pad 0.02 = the same 0.63 m as version A, so both versions share one arm pose"), } for key in doors: for v in ("A", "B"): d = doors[key] P = d["attach_local"] n = d["normal_local"] dt = tools[v]["d_t"] d.setdefault("wc_local", {})[v] = r([P[0], P[1] + dt * n[1], P[2] + dt * n[2]], 5) J = { # joints: origin in the upright arm frame A at rest (= ref1 pose), axis, Blender channel, limits "J1": dict(name="turret yaw", parent="ArmRoot", origin=[0, 0, 0], axis="+Z", channel="rotation_euler[2]", sign=+1, limits=[-170, 170], ref="turntable (ref1 x_img 1000)"), "J2": dict(name="shoulder pitch", parent="Turret", origin=r([K.PX["J2"][0] * S, 0, K.PX["J2"][1] * S]), axis="-Y", channel="rotation_euler[1]", sign=-1, limits=[-95, 45], ref="perforated drum + orange-ring side caps (1155,1315)"), "J3": dict(name="elbow pitch", parent="LowerArm", origin=r([K.PX["J3"][0] * S, 0, K.PX["J3"][1] * S]), axis="-Y", channel="rotation_euler[1]", sign=-1, limits=[-40, 55], ref="black slotted elbow cap (1550,720); driven by the J3 cylinder"), "J4": dict(name="forearm roll", parent="UpperArm", origin=r([-35 * S, 0, K.PX["WC"][1] * S]), axis="+X", channel="rotation_euler[0]", sign=+1, limits=[-170, 170], ref="ringed roll section ahead of the upper arm's big cap (x_img 1025-1045)"), "J5": dict(name="wrist pitch", parent="Forearm", origin=r([K.PX["WC"][0] * S, 0, K.PX["WC"][1] * S]), axis="+Y", channel="rotation_euler[1]", sign=+1, limits=[-120, 120], ref="wrist round cap with concentric rings (565,613) = wrist centre WC"), "J6": dict(name="flange roll", parent="WristHead", origin=r([K.PX["WC"][0] * S, 0, K.PX["WC"][1] * S]), axis="+X", channel="rotation_euler[0]", sign=+1, limits=[-170, 170], ref="knurled neck ring / gripper hub turns about the tool axis"), } kin = dict( scale_m_per_px=S, frame_A="upright arm frame at rest: origin turntable face centre, +X forward (toward the tool), +Y the arm's left, +Z up (away from the carriage). " "In the ship the arm hangs: ArmRoot = carriage underside centre rotated 180 deg about world X (A's +Z = world -Z, A's +Y = world -y).", joints=J, L2=r(arm.L2), d4=r(arm.d4), a3=r(arm.a3), L3=r(arm.L3), beta_rad=r(K.BETA, 6), phi_l0_rad=r(K.PHI_L0, 6), j2_r=r(-arm.j2[0]), j2_h=r(arm.j2[1]), wc_rest_A=r([K.PX["WC"][0] * S, 0, K.PX["WC"][1] * S]), j3_rest_A=r([K.PX["J3"][0] * S, 0, K.PX["J3"][1] * S]), flange_from_wc=px(60), rod=dict(trunnion_rest_A=r([K.PX["knee"][0] * S, 0, K.PX["knee"][1] * S]), knob_rest_A=r([K.PX["knob"][0] * S, 0, K.PX["knob"][1] * S]), trunnion_in_lower=r([(K.PX["knee"][0] - K.PX["J2"][0]) * S, 0, (K.PX["knee"][1] - K.PX["J2"][1]) * S]), knob_in_upper=r([(K.PX["knob"][0] - K.PX["J3"][0]) * S, 0, (K.PX["knob"][1] - K.PX["J3"][1]) * S]), length_rest=r(math.hypot(K.PX["knob"][0] - K.PX["knee"][0], K.PX["knob"][1] - K.PX["knee"][1]) * S), type="double-acting hydraulic cylinder on a middle trunnion (the knee pin); barrel 0.30 m behind and 0.50 m ahead of the trunnion (gland 0.46..0.50); " "rod 0.80 m from its eye to the piston; gland collar at the barrel's front"), ) scene = dict( context_file="source/ship_context_2026-10-16.blend", context_json="source/ship_context_2026-10-16.json", note="objects in the context file are in no scene: link them into a scene before evaluating animation", deck_underside_z=-3.755, rail=dict(AFT=[-9.1035, -2.5453], FORE=[8.6453, 15.2035], y=[-6.0238, -5.5662], z=[-4.01, -3.6745], usable_AFT=[-9.0348, -2.5453], usable_FORE=[8.6453, 15.1348]), end_wall_faces_x=dict(AFT=-9.0348, FORE=15.1348), mirror_plane_x=3.05, door_frames=dict(closed=1, retracted_inboard=12, open=48, close_starts=72, retracted_again=108, closed_again=120, fps=30), lamps=dict(AFT_housing=[[-0.8231, -7.6437, -3.8468], [1.0069, -6.9727, -3.5723]], FORE_housing=[[5.0931, -7.6437, -3.8468], [6.9231, -6.9727, -3.5723]]), ) carriage = dict( size=dict(along_rail=r(Lc), across=r(Wc), height=r(Hc), ref_px=[390, 390, 185]), underside_z=r(zb), top_z=-3.775, centre_y=r(yb, 5), travel_ends=dict(AFT=[doors["AFT"]["carriage"]["c_open"], doors["AFT"]["carriage"]["c_closed"]], FORE=[doors["FORE"]["carriage"]["c_closed"], doors["FORE"]["carriage"]["c_open"]]), ) # energy chain (added rail kit): lies on its side in a tray hung under the deck, inboard of the rail chain = {} for key in ("AFT", "FORE"): side = doors[key]["side"] lo, hi = sorted(carriage["travel_ends"][key]) F_ = (lo + hi) / 2 chain[key] = dict(fixed_x=r(F_), length=3.30, bend_radius=0.15, pitch=0.075, links=44, y_moving_run=-5.23, y_fixed_run=-4.93, bend_toward=-side, moving_offset=0.0, z_bottom=-3.89, z_top=-3.79, link_size=[0.075, 0.06, 0.10], tray=dict(x=[r(-9.00), -2.95] if key == "AFT" else [9.05, 15.10], y=[-5.28, -4.88], floor_top_z=-3.895, wall_top_z=-3.78, end_slant=dict(at_y=[-5.28, -4.88], x=[-2.95, -3.08] if key == "AFT" else [9.05, 9.18]), note="the well-side end is cut on a slant (AFT x -2.95 at y -5.28 to x -3.08 at y -4.88) so it stays >= 0.05 m " "short of the moon pool well's rounded edge; the deck above it is solid"), check="the loop's far end stays inside the tray and both runs keep >= 0 length over the whole travel") design = dict( meta=dict(asset="DoorRobotArm", date="2026-10-16", status="design (Designer)", standalone=True, user_words=["3d model this in blender. Understand the picture references and rig it accordingly and mechanically.", "according to the open and close animation fit it to the scene ... also consider the rail accordingly.", "do 2 version with and without the grab limbs shown in the red lined picture ... dont miss details", "this will be standalone project dont add on the ship yet"]), scale=dict(m_per_ref1_px=S, how="chosen so the hanging arm reaches its door with good joint angles (design_search*.py); " "ref1 pixels x 0.0022 = metres; the whole arm uses this one scale"), scene=scene, kinematics=kin, doors=doors, tools=tools, carriage=carriage, energy_chain=chain, parts=PARTS, materials=MATERIALS, markings=MARKINGS, tri_budget=TRI_BUDGET, ) # --------------------------------------------------------------------------------------------------------------------- import design_fingers as DFG design["fingers"] = DFG.data() design["fingers"]["objects"] = { "top": dict(hinge_axis="+Y", proximal_channel="rotation_euler[1]", sign=+1), "bottom": dict(hinge_axis="+Y", proximal_channel="rotation_euler[1]", sign=-1), "left": dict(hinge_axis="+Z", proximal_channel="rotation_euler[2]", sign=-1), "right": dict(hinge_axis="+Z", proximal_channel="rotation_euler[2]", sign=+1), "note": "angle = sign * g * |turn| toward the tool axis (proximal 14.7 deg, distal 41.0 deg); the distal object is a child of the proximal, " "origin on the distal pin, same channel and sign; g = DoorArm | CONTROLS['grip'] (ship scene default 1.0 = closed on the knob)", } design["controls"] = dict( object="DoorArm | CONTROLS", props=dict(version=dict(default=0, values={"0": "A gripper (grab limbs)", "1": "B plate (red sketch)"}), grip=dict(default=1.0, range=[0.0, 1.0], note="version A fingers: 0 open, 1 closed on the knob")), version_drivers=dict(A_parts_expr="v > 0.5", B_parts_expr="v < 0.5", paths=["hide_viewport", "hide_render"], A_parts=["tool A hub", "finger * proximal", "finger * distal", "door fixture A"], B_parts=["tool B", "door fixture B"], note="v reads DoorArm | CONTROLS['version']; both arms switch together; the arm pose does not change (both tools share the wrist centre)"), door_fixture_drivers="fixture.location[i] = door.location[i] + fixture_offset[i] (three simple drivers per fixture); rotation constant " "(the door never turns); fixture_offset = attach_local; the fixture's local +Z is the door's inner-face normal", ) design["showroom"] = dict( scene="DoorArm | Showroom", arms=dict(A=dict(root="Showroom | arm A", location=[-1.9, 0.0, 1.012], tool="A", fingers="open (g = 0)"), B=dict(root="Showroom | arm B", location=[1.9, 0.0, 1.012], tool="B")), how="linked duplicates (shared mesh data) of every arm mesh at rest (all joints 0 = the ref1 pose), upright, no drivers; " "both stand on one floor rail like the references", floor_rail=dict(profile="the ship rail turned over: box 0.458 x 0.335 m with the K01 flanges and the K02 rack on its TOP", length=7.0, z=[0.25, 0.585], posts="square tubes 0.12 x 0.12, every 2.2 m, rail bottom 0.25 m above the floor (ref1 has short posts)", note="the carriage straddles the rail's top 0.255 m exactly as it hangs in the ship; its turntable face is at z 0.585 + 0.020 + 0.407 = 1.012"), studio=dict(floor="light grey cyclorama (floor + 6 m radius cove), grey 0.55 linear, rough 0.6", lights=["key: area 2 x 2 m, front-left high, 800 W", "fill: area 3 x 3 m, right, 300 W", "rim: area 1 x 4 m behind, 500 W"], world="flat grey 0.35, strength 0.4", note="matches the soft grey studio of ref1-3"), ) design["cables"] = dict( elbow_pair=dict(objects=["cable elbow 1", "cable elbow 2"], curve="Bezier, 5 points, bevel radius 0.013 (dia 0.026), 8 sides, the two cables 0.040 apart", route="from the twin gland U08 on the upper arm top, up and back over the elbow (0.22 m above the housing), down into the twin gland " "L17 on the lower arm's back just behind the elbow (ref1 shows the loop behind the elbow)", hooks="P0,P1 -> empty on the upper arm; P3,P4 -> empty on the lower arm; P2 -> empty with Copy Transforms to a mid empty " "on the upper arm (influence 1.0) then to one on the lower arm (influence 0.5): halfway blend, no Python", check="sample each curve every 2 cm at every frame: >= 15 mm from all link meshes except at its two glands", export="converted to mesh; weights upper_arm 1 -> 0 and lower_arm 0 -> 1 along the length (smoothstep)"), cylinder_hoses=dict(objects=["J3 hose rear", "J3 hose front"], hooks="one end on the cylinder, one on the lower arm channel wall", note="the cylinder only turns -5.2..+1.3 deg in the lower arm"), static="turret bundle (7), turret side pairs (2 x 2), forearm hose: plain meshes inside their link (no joint between their ends)", ) design["objects"] = dict( naming="'DoorArm_ | ' for each arm; 'DoorArm | ' for shared things; 'Showroom | ...' in the showroom", per_arm=[ dict(name="DoorArm_AFT", kind="EMPTY (rig root)", parent=None, location="(0, -5.795, -4.182) world", rotation="(180 deg, 0, 0)", note="static"), dict(name="| carriage", kind="MESH", parent="rig root", origin="turntable face centre", moves="location.x = c"), dict(name="| turret", kind="MESH", parent="carriage", origin="J1 (turntable face centre)", moves="rotation_euler.z = q1"), dict(name="| lower arm", kind="MESH", parent="turret", origin="J2", moves="rotation_euler.y = -q2"), dict(name="| J3 cylinder", kind="MESH", parent="lower arm", origin="trunnion (knee)", moves="rotation_euler.y = -cyl"), dict(name="| J3 piston", kind="MESH", parent="J3 cylinder", origin="rod eye centre", moves="location.x, location.z"), dict(name="| upper arm", kind="MESH", parent="lower arm", origin="J3", moves="rotation_euler.y = -q3"), dict(name="| forearm", kind="MESH", parent="upper arm", origin="J4 axis at the roll ring", moves="rotation_euler.x = q4"), dict(name="| wrist head", kind="MESH", parent="forearm", origin="WC (J5)", moves="rotation_euler.y = q5"), dict(name="| flange", kind="MESH", parent="wrist head", origin="WC", moves="rotation_euler.x = q6"), dict(name="| tool A hub", kind="MESH", parent="flange", origin="WC", moves="no (version A)"), dict(name="| finger proximal", kind="MESH", parent="tool A hub", origin="knuckle pin", moves="grip"), dict(name="| finger <...> distal", kind="MESH", parent="its proximal", origin="distal pin", moves="grip"), dict(name="| tool B", kind="MESH", parent="flange", origin="WC", moves="no (version B)"), dict(name="| cable elbow 1/2", kind="CURVE", parent="lower arm", moves="hooks"), dict(name="| J3 hose rear/front", kind="CURVE", parent="lower arm", moves="hooks"), dict(name="| door fixture A / B", kind="MESH", parent=None, origin="attach point on the door face, +Z = face normal", moves="location from the door"), dict(name="| rail kit", kind="MESH", parent=None, origin="world origin", moves="static (flanges, rack, stops, tray, hangers, chain fixed bracket)"), dict(name="| chain tray", kind="EMPTY", parent=None, origin="world origin", moves="static (parent of the links)"), dict(name="| chain link 00..43", kind="MESH (linked duplicates)", parent="| chain tray", origin="link centre", moves="location.x/y, rotation_euler.z"), dict(name="| IK1 target .. IK6 wrist angles, | chain solver", kind="EMPTY (solver, hidden)", parent=None, moves="custom properties driven"), ], shared=[dict(name="DoorArm | CONTROLS", kind="EMPTY", props=["version", "grip"])], rest_rule="every moving object: rotation 0 and scale 1 at rest (rest = the ref1 pose, all q = 0); meshes are modelled in that pose", merge_rule="each link's static parts are joined into the link's one mesh (several material slots); no booleans; quads; " "UV0 = atlas (bake), UV_Tile = metres (tiling scans)", ) design["collections"] = { "DoorArm | AFT": "rig root, all AFT moving meshes and curves", "DoorArm | FORE": "the same for the FORE door", "DoorArm | solver (hidden)": "the 14 solver empties (7 per arm), hidden in viewport and render", "DoorArm | rail kit": "both rail kits and both chains", "DoorArm | door fixtures": "fixtures A and B for both doors", "DoorArm | collision": "UCX_ hulls (eye off in the view layer, hide_render on)", "CONTEXT | ship (reference only)": "the ship context objects, linked into the fit scene, never exported", "Showroom | arms": "showroom scene only", "Showroom | studio": "floor rail, cyclorama, lights, cameras", } design["scenes"] = {"DoorArm | Ship fit": "frames 1-120, 30 fps, markers CLOSED 1, RETRACTED INBOARD 12, OPEN / PANELS IN POCKETS 48, CLOSE 108", "DoorArm | Showroom": "frame 1, both versions side by side, upright"} design["build"] = dict( entry="build/build.py -- (each stage one headless call under 9 minutes; renders and bakes in their own calls)", stages=[ dict(stage="context", does="new file; link the ship context objects into 'CONTEXT | ship'; scenes, frame range, fps, markers; save DoorRobotArm.blend (back up first)"), dict(stage="parts", does="model every part in the parts list per link (one module per link), join per link, origins on axes, two UV layers"), dict(stage="rig", does="hierarchy for both arms, solver empties and all drivers from design.json['drivers'], fingers, version switch, fixtures, " "cable hooks; assert every driver is_simple_expression and is_valid"), dict(stage="check", does="real-mesh check frames 1-120 both doors both versions (BVH overlap between every pair of moving meshes and " "against the context and the rail kit, like vessel-interior/build/door_arms.check); pose error; piston on knob; chain; " "writes build/check_report.json and fails on any crossing"), dict(stage="surfaces", does="materials (pressure-door/build/pd_look.py paint/steel/dark), decal atlas, masks bake (separate calls per atlas)"), dict(stage="showroom", does="showroom scene with linked duplicates, floor rail, studio, lights, cameras"), dict(stage="ucx", does="UCX_ convex hulls per link from design_parts.PROXIES (box and 16-sided prisms)"), dict(stage="export", does="FBX for Unreal (see export) + DoorRobotArm_layout.json"), dict(stage="renders", does="Cycles renders listed in renders (several calls)"), ], libraries=["models/pressure-door/build/pd_lib.py (lathe, rbox, finish)", "models/pressure-door/build/pd_look.py (paint, steel, dark)", "models/docking-arm/build/surfaces.py (masks atlas bake, tiling UV in metres)", "models/vessel-interior/build/boarding_gate.py (_crossing for BVH checks)", "models/machine-parts-kit, models/hardsurface-kit (part shapes to read)"], ) design["checks"] = [ "every driver: is_simple_expression and is_valid (the prototype has 172 per arm, all pass)", "frames 1-120 both doors both versions: tool contact point on the door target < 0.5 mm, axes < 0.1 deg", "joint angles inside kinematics.joints[*].limits; |q5| >= 30 deg; elbow inner angle 32..115 deg; WC >= 0.5 m from the J1 axis", "J3 piston eye on the knob < 0.5 mm; piston inside the barrel; barrel rear inside the lower arm", "no mesh crossing between any two moving parts or against the context, the rail kit, the doors (design clearance targets: >= 0.008 m " "for the tool against the wrist lobes, >= 0.10 m elsewhere; contact allowed only plate-pad-door and the hooks under the knob, 6 mm)", "carriage inside its travel; its end faces 10 mm short of the buffers at the travel ends", "chain: link spacing = pitch (<= 1 mm on the bend chord), first link at the carriage, all links inside the tray", "version switch: A parts hidden for v=1, B parts hidden for v=0, viewport and render", "game-ready audit: quads (n-gons only on flat caps, triangulated), no booleans, UV0 + UV_Tile on every mesh, rotation 0 / scale 1 at rest", ] design["renders"] = dict( showroom=[ dict(name="DoorArm_showroom_ref1_side", like="ref1", camera="arm A side on from its left (+Y), level with the elbow, 85 mm"), dict(name="DoorArm_showroom_ref2_34left", like="ref2", camera="front-left three-quarter, slightly below the forearm"), dict(name="DoorArm_showroom_ref3_34right", like="ref3", camera="right three-quarter from the back-right, truss and rod side"), dict(name="DoorArm_showroom_ref4_base", like="ref4", camera="close-up of carriage, turret, belt drive, cable bundle, left side"), dict(name="DoorArm_showroom_ref5_lowerarm", like="ref5", camera="close-up of the lower arm: knee cap, elbow cap, truss, rod, handle"), dict(name="DoorArm_showroom_ref6_wrist_A", like="ref6", camera="forearm and wrist from front-left-below, version A"), dict(name="DoorArm_showroom_ref6_wrist_B", like="ref6 red sketch", camera="the same camera, version B (neck + plate)"), dict(name="DoorArm_showroom_ref7_back", like="ref7", camera="back view of the base, low"), dict(name="DoorArm_showroom_side_by_side", like="side by side", camera="both arms, wide"), ], in_scene=dict(frames=[1, 12, 30, 48, 90], versions=["A", "B"], doors="both arms in every frame", cameras=["pocket AFT from inboard (y -1.5, z -5.0) looking outboard at the arm", "along the AFT rail from the well toward the end wall", "the same two for FORE"], light="the pocket is dark: two area lights (1 x 4 m, 600 W) under the deck inboard of each rail, a soft fill from the well; " "the docking lamps keep their own emission", engine="Cycles, GPU (HIP), 256 samples + denoise"), out="previews/", ) design["bake"] = dict( uv0="one atlas per arm (all link meshes, both tools, fixtures, chain link, rail kit): 4096 x 4096, about 512 px per metre on the large shells", maps=dict(T_DoorArm_Masks="R occlusion, G convex edges (wear), B cavities (grime); Cycles bake, 128 samples", T_DoorArm_BevelNormal="rounded-edge normal (bevel shader, radius 4 mm), tangent space OpenGL", T_DoorArm_Decals="1024 x 1024 RGBA, the marking shapes (no text), alpha-tested"), tiling="UV_Tile in metres: paint 1.25 m repeat, gunmetal 0.70, steel 0.50, rubber 0.40", calls="each bake in its own headless call (< 9 min); save images with relative paths (bpy.path.relpath)", ) design["export"] = dict( decision="Object hierarchy in Blender (each moving part its own object, pivot on its axis, drivers on object channels: what the user's " "Blender runs without Python and what the project's other rigs do). For Unreal the export stage builds a deform armature from it: one " "bone per moving object (head at the object's origin, same axes), every link mesh joined into one skinned mesh with weight 1.0 to its " "bone (rigid), the elbow cables weighted 1 -> 0 between upper and lower arm, the chain links one bone each; the 120 frames are baked " "from the object matrices to bone keys", files=dict(skeletal_A="fbx/SK_DoorRobotArm_A.fbx (mesh + skeleton)", skeletal_B="fbx/SK_DoorRobotArm_B.fbx (same skeleton; finger bones unweighted)", anims="fbx/A_DoorRobotArm_AFT_OpenClose.fbx, A_DoorRobotArm_FORE_OpenClose.fbx (frames 1-120, 30 fps), A_DoorRobotArm_Grip.fbx (g 0 -> 1, 15 frames)", fixtures="fbx/SM_DoorArm_FixtureA.fbx, SM_DoorArm_FixtureB.fbx (attach to the door actor)", rail_kit="fbx/SM_DoorArm_RailKit.fbx (static); the chain links ride in the skeletal mesh", static_parts="fbx/parts/SM_DoorArm_.fbx with UCX_ hulls (for a Blueprint build, if preferred)"), settings="FBX: Z up, -Y forward as the project's other exports; apply unit scale; add_leaf_bones False; bake_anim True; " "only the arm's collections (never CONTEXT); the rig root carries the hang (180 deg about X) so in Unreal it is the root bone's rotation", unreal="play the door's open/close timeline and the matching AnimSequence together (the solution is fixed by the door's 120 frames); " "switch versions by swapping SK_..._A / _B (one skeleton)", ) import design_parts_m as DPM design["parts_m"] = DPM.to_m(PARTS) import design_drivers as DD design["drivers"] = {} for key in ("AFT", "FORE"): b = DD.build(design, key) csol, cdrv = DD.chain_drivers(design, key) design["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"], 6), rod_axis=r(b["rod_axis"], 6), rod_L0=r(b["rod_L0"], 6), cyl_beta0_rad=r(b["cyl_beta0"], 6)) # verified numbers (from the proof runs, if present) ver = {} for k in ("AFT_A", "AFT_B", "FORE_A", "FORE_B"): pth = os.path.join(HERE, "check_%s.json" % k) if os.path.exists(pth): cj = json.load(open(pth)) ver["ik_check_" + k] = {kk: cj[kk] for kk in ("frames", "pose_err_mm", "pose_err_deg", "limits_ok", "carriage", "carriage_ok", "min_abs_q5_deg", "elbow_inner_deg", "wc_from_J1_axis_min", "reach_ratio", "joints", "max_step_deg", "rod")} ver["ik_check_" + k]["clearance_lowest"] = cj["clear_sorted"][:6] ver["ik_check_" + k]["self_lowest"] = cj["self_sorted"][:6] for k in ("AFT", "FORE"): pth = os.path.join(HERE, "proto_%s.json" % k) if os.path.exists(pth): ver["driver_proto_" + k] = json.load(open(pth)) design["verified"] = ver json.dump(design, open(OUT_JSON, "w"), indent=1) print("wrote design.json")