"""Door robot arm: the showroom (both versions upright side by side on a floor rail) and the parts the design adds in the ship scene: the rail kits (running flanges, rack, end stops, energy-chain tray), the chain links and the door fixtures. Ship coordinates (metres); FORE is the AFT kit mirrored in the plane x = 3.05. Showroom height: the design put the rail at z 0.25..0.585 with the carriage on top of it; but the carriage wraps the rail's top 0.255 m (as it wraps the rail's lower 0.255 m in the ship), so the rail is at z 0.505..0.840 here (turntable face stays at z 1.012). Arm B stands a whole number of rack teeth from arm A (x +1.8946, not +1.9), so both pinions mesh with the same rack. """ import math import json import os import bmesh import bpy from mathutils import Vector, Matrix from mathutils.bvhtree import BVHTree from dra_lib import (Link, lathe, slab, rbox, tube, merge, xform, frame, basis, mirror_y, W, G, S, R, O, MAT_NAMES, uv_tile) from dra_shapes import rrect, xz_slab, yz_slab, xy_slab, screw, dome, hexbolt, cyl, ring import dra_rigdata as rd from dra_parts_base import RACK_PITCH, PIN_MOD, PIN_Z HERE = os.path.dirname(os.path.abspath(__file__)) ROOT = os.path.dirname(HERE) SC = rd.S # the scale-up (2026-10-18): the showroom and the kit are scaled with the arm ROOT_Z1 = 1.012 # showroom turntable face height, build units ARM_X1 = {"A": -1.9, "B": -1.9 + 151 * RACK_PITCH} ROOT_Z = ROOT_Z1 * SC ARM_X = {k: v * SC for k, v in ARM_X1.items()} MIRROR_X = 3.05 def scaled_mesh(L, name, M): """a Link made in build units -> a mesh moved by M (the scale), with its tiling UVs in metres""" import dra_lib me = dra_lib.finish(L, name) me.transform(M) uv_tile(me) if "UVMap" not in me.uv_layers: uv = me.uv_layers.new(name="UVMap") for li, l in enumerate(me.loops): co = me.vertices[l.vertex_index].co uv.data[li].uv = (co.x * 0.25 + 0.5, co.y * 0.25 + 0.5) me.uv_layers.active = uv uv.active_render = True return me def _design(): with open(os.path.join(ROOT, "design.json")) as f: return json.load(f) def link_collection(name, parent): c = bpy.data.collections.get(name) or bpy.data.collections.new(name) if c.name not in [x.name for x in parent.children]: parent.children.link(c) return c def mesh_from_link(L, name, origin=(0, 0, 0)): import dra_lib me = dra_lib.finish(L, name, origin=origin) uv_tile(me) if "UVMap" not in me.uv_layers: uv = me.uv_layers.new(name="UVMap") # a plain planar map; the bake stage repacks every mesh into the atlas for li, l in enumerate(me.loops): co = me.vertices[l.vertex_index].co uv.data[li].uv = (co.x * 0.25 + 0.5, co.y * 0.25 + 0.5) me.uv_layers.active = uv uv.active_render = True return me # ------------------------------------------------------------------ rack (teeth along +local Z) def rack_piece(x0, x1, phase, zb, zt, zr, half_w=0.030, mat=S): """a gear rack from x0 to x1 (local), body from zb to zr (root line), teeth from zr to zt, tooth centres at phase + k * pitch. Teeth point along +Z.""" L = Link("rack") L.put(rbox((x1 - x0, 2 * half_w, zr - zb), 0.002), Matrix.Translation(((x0 + x1) / 2, 0, (zb + zr) / 2)), mat, 'rack') p = RACK_PITCH k0 = math.ceil((x0 - phase) / p + 0.5) k1 = math.floor((x1 - phase) / p - 0.5) for k in range(k0, k1 + 1): cx = phase + k * p prof = [(cx - 0.0068, zr - 0.001, 0.0), (cx + 0.0068, zr - 0.001, 0.0), (cx + 0.0028, zt, 0.0), (cx - 0.0028, zt, 0.0)] L.put(xz_slab(prof, -half_w + 0.001, half_w - 0.001, 0.0, 0.0, n=1), None, mat, 'rack t', seats=('rack',)) return L # ------------------------------------------------------------------ showroom def build_showroom(sc, meshes): top = sc.collection c_arms = link_collection("Showroom | arms", top) c_studio = link_collection("Showroom | studio", top) for v in ("A", "B"): c = link_collection("Showroom | arm " + v, c_arms) M = Matrix.Translation((ARM_X[v], 0.0, ROOT_Z)) root, objs = rd.instantiate(c, "Showroom | arm " + v, M, meshes, tools=(v,)) root["version"] = v for ob in objs.values(): ob["version"] = v # the cables over the elbow and the cylinder hoses (curves at rest) import dra_parts_arm dra_parts_arm.add_curves(c, "Showroom | arm " + v, objs) for ob in c.objects: ob["version"] = v _floor_rail(c_studio) _studio(sc, c_studio) def _floor_rail(c): """the ship's rail turned over on short posts: box 0.458 x 0.335, running flanges and the rack on its top""" L = Link("floor rail") x0, x1 = -3.6, 3.6 zt = ROOT_Z1 - 0.172 # rail top (= its bottom face in the ship), build units zb = zt - 0.335 L.put(rbox((x1 - x0, 0.458, 0.335), 0.004), Matrix.Translation((0, 0, (zt + zb) / 2)), G, 'rail') for s in (1, -1): L.put(rbox((x1 - x0, 0.050, 0.030), 0.0025), Matrix.Translation((0, s * 0.254, zt - 0.015)), S, 'K01', seats=('rail',)) for k in range(int((x1 - x0) / 0.25)): x = x0 + 0.125 + k * 0.25 L.put(dome(0.009, 0.004, S), frame((x, s * 0.279, zt - 0.015), (0, s, 0)), None, 'K01b', seats=('K01',)) # rack: in carriage terms body Z -0.172..-0.150, teeth to -0.132; teeth between the two arms' pinion teeth rk = rack_piece(x0 + 0.05, x1 - 0.05, ARM_X1["A"] + RACK_PITCH / 2, zt, ROOT_Z1 - 0.132, ROOT_Z1 - 0.150) merge(L.bm, rk.bm) for p in rk.parts: L.pid(p['id'], p['seats']) rk.bm.free() # posts: square tubes 0.12 x 0.12 every 2.2 m for x in (-3.3, -1.1, 1.1, 3.3): if abs(x - ARM_X1["A"]) < 0.5 or abs(x - ARM_X1["B"]) < 0.5: pass L.put(rbox((0.12, 0.12, zb), 0.006), Matrix.Translation((x, 0, zb / 2)), G, 'post', seats=('rail',)) L.put(xy_slab(rrect(0.22, 0.22, 0.01, x, 0), 0.0, 0.012, 0.0, 0.003), None, G, 'post', seats=('post',)) me = scaled_mesh(L, "Showroom | floor rail", Matrix.Scale(SC, 4)) ob = bpy.data.objects.new("Showroom | floor rail", me) c.objects.link(ob) return ob def _studio(sc, c): m = bpy.data.materials.get("M_Studio_Floor") or bpy.data.materials.new("M_Studio_Floor") m.diffuse_color = (0.55, 0.55, 0.55, 1) m.use_nodes = True m.node_tree.nodes["Principled BSDF"].inputs["Base Color"].default_value = (0.55, 0.55, 0.55, 1) m.node_tree.nodes["Principled BSDF"].inputs["Roughness"].default_value = 0.6 # cyclorama: floor from y +12 to -4, a 6 m cove up to z 9 behind the arms (-y), seen from the left side (+y) bm = bmesh.new() prof = [(12.0, 0.0), (-4.0, 0.0)] for k in range(1, 13): a = (math.pi / 2) * k / 12 prof.append((-4.0 - 6.0 * math.sin(a), 6.0 - 6.0 * math.cos(a))) prof.append((-10.0, 9.0)) xs = [-14.0 + 2.0 * i for i in range(15)] grid = [[bm.verts.new((x * SC, y * SC, z * SC)) for x in xs] for (y, z) in prof] for j in range(len(prof) - 1): for i in range(len(xs) - 1): f = bm.faces.new((grid[j][i], grid[j][i + 1], grid[j + 1][i + 1], grid[j + 1][i])) bmesh.ops.recalc_face_normals(bm, faces=bm.faces[:]) for f in bm.faces: if f.normal.z < -0.1 or (f.normal.y < -0.1): f.normal_flip() me = bpy.data.meshes.new("Showroom | cyclorama") bm.to_mesh(me) bm.free() me.materials.append(m) me.shade_smooth() uv_tile(me, 4.0 * SC) ob = bpy.data.objects.new("Showroom | cyclorama", me) c.objects.link(ob) for nm, loc, size, energy, rot in (("key", (-5.0, 6.0, 7.0), (2.0, 2.0), 800, None), ("fill", (6.0, 5.0, 3.0), (3.0, 3.0), 300, None), ("rim", (0.0, -7.0, 5.0), (1.0, 4.0), 500, None)): ld = bpy.data.lights.new("Showroom | " + nm, 'AREA') ld.energy = energy * SC * SC ld.shape = 'RECTANGLE' ld.size, ld.size_y = size[0] * SC, size[1] * SC lo = bpy.data.objects.new("Showroom | light " + nm, ld) lo.location = Vector(loc) * SC d = (Vector((0, 0, 1.6 * SC)) - lo.location).normalized() lo.rotation_euler = d.to_track_quat('-Z', 'Y').to_euler() c.objects.link(lo) w = bpy.data.worlds.get("Showroom | world") or bpy.data.worlds.new("Showroom | world") w.color = (0.35, 0.35, 0.35) w.use_nodes = True bg = w.node_tree.nodes.get("Background") if bg: bg.inputs[0].default_value = (0.35, 0.35, 0.35, 1) bg.inputs[1].default_value = 0.4 sc.world = w # ------------------------------------------------------------------ ship fit: rail kits, chain, fixtures def _mx(side): """AFT: identity; FORE: mirror in the plane x = 3.05""" if side == "AFT": return Matrix.Identity(4) return Matrix.Translation((2 * MIRROR_X, 0, 0)) @ Matrix.Scale(-1, 4, Vector((1, 0, 0))) def rail_kit_link(d): """the AFT rail kit in ship coordinates (scale-up 2026-10-18: on the new rail line, every size x S): running flanges, the rack, the two end stops at the carriage's travel ends, the energy-chain tray""" sc = d["scene"] xr0, xr1 = sc["rail"]["usable_AFT"] y0, y1 = sc["rail"]["y"] zb, zt = sc["rail"]["z"] yc = (y0 + y1) / 2 k = SC L = Link("rail kit") # K01 running flanges on both side faces at the rail's bottom, low dome bolt heads on their outer faces for side, yy in ((-1, y0 - 0.025 * k - 0.0003), (1, y1 + 0.025 * k + 0.0003)): L.put(rbox((xr1 - xr0 - 0.004, 0.050 * k, 0.030 * k), 0.0025 * k), Matrix.Translation(((xr0 + xr1) / 2, yy, zb + 0.015 * k)), S, 'K01') x = xr0 + 0.125 * k while x < xr1 - 0.05 * k: L.put(dome(0.009 * k, 0.004 * k, S), frame((x, yy + side * 0.025 * k, zb + 0.015 * k), (0, side, 0)), None, 'K01b', seats=('K01',)) x += 0.25 * k # K02 rack under the rail, teeth down: made in build units about the closed-door carriage position, then scaled, so # the pinion (scaled with the carriage) meshes with it as before c_open, c_closed = d["carriage"]["travel_ends"]["AFT"] rk = rack_piece((c_open - 0.11 * k - c_closed) / k, 0.11, RACK_PITCH / 2, 0.0, 0.040, 0.022, 0.030) xform(rk.bm, Matrix.Translation((c_closed, yc, zb)) @ Matrix.Scale(k, 4) @ Matrix.Scale(-1, 4, Vector((0, 0, 1)))) lay_r = rk.bm.faces.layers.int.get('part') remap = {i: L.pid(p['id'], p['seats']) for i, p in enumerate(rk.parts)} for f in rk.bm.faces: f[lay_r] = remap.get(f[lay_r], f[lay_r]) merge(L.bm, rk.bm) rk.bm.free() # K03 end stops at the carriage's travel ends (its strike pads stop 'gap' short of the rubber). End-wall side: a # steel block with a rubber buffer under the rail. Inner end: the rubber on a plate across the rail's end face; the # rail ends 30 mm short of the deck's edge, so the stop stays under the deck st = sc["rail"]["stops"] half = d["carriage"]["size"]["along_rail"] / 2 zs_c, zs_h = zb - 0.06 * k, 0.12 * k # the stops' height band under the rail (the strike pads') buf = st["buffer"] rf = c_open - half - st["gap"] # end-wall side: the rubber's face bx = rf - buf - 0.08 * k # the block's middle (0.10 x S long) L.put(rbox((0.10 * k, 0.30 * k, zs_h), 0.004 * k), Matrix.Translation((bx, yc, zs_c)), G, 'K03') L.put(rbox((buf + 0.002, 0.26 * k, 0.10 * k), 0.006 * k), Matrix.Translation((rf - (buf + 0.002) / 2, yc, zs_c)), R, 'K03r', seats=('K03',)) for yy in (-0.1 * k, 0.1 * k): L.put(hexbolt(0.018 * k, 0.008 * k), frame((bx, yc + yy, zs_c - zs_h / 2), '-Z'), None, 'K03b', seats=('K03',)) ri = c_closed + half + st["gap"] # inner end: the rubber's face xe = xr1 # the rail's end face pz0, pz1 = zs_c - zs_h / 2, zt - 0.065 * k # the end plate: from under the rubber up the rail's end face L.put(rbox((st["end_plate"], 0.30 * k, pz1 - pz0), 0.003 * k), Matrix.Translation((xe + st["end_plate"] / 2, yc, (pz0 + pz1) / 2)), G, 'K03') L.put(rbox((xe - ri + 0.001, 0.26 * k, 0.10 * k), 0.006 * k), Matrix.Translation(((ri + xe + 0.001) / 2, yc, zs_c)), R, 'K03r', seats=('K03',)) for zz in (zs_c, (zb + pz1) / 2): for yy in (-0.075 * k, 0.075 * k): L.put(hexbolt(0.018 * k, 0.008 * k), frame((xe + st["end_plate"], yc + yy, zz), 'X'), None, 'K03b', seats=('K03',)) # K04 energy-chain tray: floor, two walls, both end walls; hangers from the far wall up to the deck t = d["energy_chain"]["AFT"]["tray"] tx0, tx1s = t["x"] ty0, ty1 = t["y"] fz = t["floor_top_z"] wz = t["wall_top_z"] xa, xb = t["end_slant"]["x"] # well-side end x at y0 and at y1 w6 = 0.006 * k floor = [(tx0, ty0), (xa, ty0), (xb, ty1), (tx0, ty1)] L.put(xy_slab(floor, fz - w6, fz, 0.0015 * k, 0.0015 * k), None, G, 'K04') for yy in (ty0, ty1): xe_ = xa if yy == ty0 else xb L.put(rbox((xe_ - tx0, w6, wz - fz + w6), 0.0015 * k), Matrix.Translation(((tx0 + xe_) / 2, yy + (w6 / 2 if yy == ty0 else -w6 / 2), (wz + fz - w6) / 2)), G, 'K04', seats=('K04',)) ang = math.atan2(ty1 - ty0, xb - xa) ln = math.hypot(xb - xa, ty1 - ty0) Mend = Matrix.Translation(((xa + xb) / 2 - w6 / 2 - 0.0005, (ty0 + ty1) / 2, (wz + fz - w6) / 2)) @ Matrix.Rotation(ang, 4, 'Z') L.put(rbox((ln - 2 * w6 - 0.002, w6, wz - fz + w6 - 0.001), 0.0015 * k), Mend, G, 'K04e', seats=('K04',)) L.put(rbox((w6, ty1 - ty0 - 2 * w6 - 0.002, wz - fz + w6 - 0.001), 0.0015 * k), Matrix.Translation((tx0 + w6 / 2 + 0.0005, (ty0 + ty1) / 2, (wz + fz - w6) / 2 + 0.0005)), G, 'K04e', seats=('K04',)) x = tx0 + 0.3 * k deck = sc["deck_underside_z"] while x < xb - 0.2 * k: L.put(rbox((0.04 * k, w6, deck - wz - 0.0035 * k), 0.001 * k), Matrix.Translation((x, ty1 + w6 / 2, (deck + wz) / 2 - 0.0028 * k)), G, 'K04h', seats=('K04',)) L.put(rbox((0.06 * k, 0.06 * k, w6), 0.001 * k), Matrix.Translation((x, ty1 + 0.03 * k, deck - 0.0035 * k)), G, 'K04h', seats=('K04h',)) x += 0.8 * k # the chain's fixed end bracket on the tray floor at the fixed x fy = d["energy_chain"]["AFT"]["y_fixed_run"] c1 = d["carriage"]["travel_ends"]["AFT"][1] pos = chain_positions(d, "AFT", c1) (x43, y43, r43), (x42, y42, r42) = pos[-1], pos[-2] sg = 1.0 if x43 > x42 else -1.0 xe2 = x43 + sg * 0.0505 * k x0b, x1b = (xe2, xe2 + 0.012 * k) if sg > 0 else (xe2 - 0.012 * k, xe2) L.put(yz_slab(rrect(0.07 * k, 0.10 * k, 0.006 * k, fy, (fz + wz) / 2 - 0.005 * k), x0b, x1b, 0.0015 * k, 0.0015 * k), None, S, 'K05f', seats=('K04',)) return L def rail_beam_link(d): """the AFT rail beam (scale-up 2026-10-18: the rail line moved 2.0 m inboard, so the arm brings its own rail): the user's rail section x S where it shows under the deck, its top on the deck's underside, from the pocket's end wall to 30 mm short of the deck's edge at the moon pool; thin plates along both top edges bolt it to the deck""" sc = d["scene"] xr0, xr1 = sc["rail"]["usable_AFT"] y0, y1 = sc["rail"]["y"] zb, zt = sc["rail"]["z"] k = SC L = Link("rail beam") L.put(rbox((xr1 - xr0, y1 - y0, zt - zb), 0.004 * k), Matrix.Translation(((xr0 + xr1) / 2, (y0 + y1) / 2, (zb + zt) / 2)), G, 'K00') x = xr0 + 0.40 * k tp = 0.008 * k while x < xr1 - 0.30 * k: for yy, sd in ((y0, -1), (y1, 1)): L.put(rbox((0.10 * k, 0.040 * k, tp), 0.0015 * k), Matrix.Translation((x, yy + sd * 0.020 * k, zt - tp / 2)), G, 'K00p', seats=('K00',)) L.put(hexbolt(0.016 * k, 0.006 * k), frame((x, yy + sd * 0.022 * k, zt - tp), '-Z'), None, 'K00b', seats=('K00p',)) x += 0.80 * k return L def chain_link_mesh(): """one energy-chain link lying on its side (bend axis vertical), centre at the origin, x along the chain (pitch 0.075). Outer plates (top and bottom) on the back half, inner plates one step further in on the front half, so a link's inner plates slide under the next link's outer plates; the bars stand outside the inner plates' width. Kept light (it is repeated 88 times): plain chamfered boxes.""" L = Link("chain link") for sz in (1, -1): M = Matrix.Scale(sz, 4, Vector((0, 0, 1))) L.put(rbox((0.054, 0.068, 0.006), 0.0012, segs=1), M @ Matrix.Translation((-0.023, 0, 0.047)), R, 'outer') L.put(rbox((0.050, 0.054, 0.006), 0.0012, segs=1), M @ Matrix.Translation((0.021, 0, 0.039)), R, 'inner', seats=('step',)) L.put(rbox((0.008, 0.054, 0.0145), 0.001, segs=1), M @ Matrix.Translation((0.0, 0, 0.0428)), R, 'step', seats=('outer', 'inner')) for yy in (-0.031, 0.031): L.put(rbox((0.016, 0.006, 0.0875), 0.0012, segs=1), Matrix.Translation((-0.006, yy, 0)), R, 'bar', seats=('outer',)) me = scaled_mesh(L, "DoorArm | chain link", Matrix.Scale(SC, 4)) return me def chain_positions(d, side, c): """the designer's chain driver expressions evaluated for carriage position c -> [(x, y, rot_z)] (ship, AFT or FORE)""" dr = d["drivers"][side]["chain_link_drivers"] env = {"sin": math.sin, "cos": math.cos, "clamp": lambda v, a, b: max(a, min(b, v)), "pi": math.pi, "min": min, "max": max} solver = {} vals = {} for e in dr: if e["target"].endswith("chain solver"): loc = dict(env) loc.update({v[0]: c for v in e["vars"]}) solver[e["path"].strip('[]"')] = eval(e["expr"], {"__builtins__": {}}, loc) for e in dr: if e["target"].endswith("chain solver"): continue loc = dict(env) for v in e["vars"]: key = v[2].strip('[]"') loc[v[0]] = solver[key] val = eval(e["expr"], {"__builtins__": {}}, loc) vals.setdefault(e["target"], {})[(e["path"], e["index"])] = val out = [] for i in range(d["energy_chain"][side]["links"]): nm = "DoorArm_%s | chain link %02d" % (side, i) v = vals.get(nm, {}) out.append((v.get(("location", 0), 0.0), v.get(("location", 1), 0.0), v.get(("rotation_euler", 2), 0.0))) return out KNOB_NECK_R = 0.0603 # version A's knob neck: the closed hooks' surfaces reach 0.0608 from its axis def fixture_links(d, door_ob, side): """door fixtures A (pad, base plate, stem, mushroom knob) and B (conformal pad) for one door, in ship coordinates relative to the attach point (rotation 0: the mesh is built in the ship's axes)""" dd = d["doors"][side] loc = Vector(dd["closed_location"]) att = loc + Vector(dd["attach_local"]) n = Vector(dd["normal_local"]).normalized() xa = Vector((1, 0, 0)) ya = n.cross(xa).normalized() Mf = Matrix((xa, ya, n)).transposed().to_4x4() # fixture local -> ship axes me = door_ob.data verts = [loc + v.co for v in me.vertices] bvh = BVHTree.FromPolygons(verts, [tuple(p.vertices) for p in me.polygons]) out = {} ks = SC tB = d["tools"]["B"] for ver, size, top in (("A", 0.32 * ks, 0.020 * ks), ("B", tB.get("pad_size", 0.60), tB.get("door_pad", 0.020))): L = Link("door fixture " + ver) N = 8 if ver == "A" else 20 bm = bmesh.new() topv, botv = [], [] for j in range(N + 1): rt, rb = [], [] for i in range(N + 1): u = -size / 2 + size * i / N v = -size / 2 + size * j / N p = att + xa * u + ya * v hit = bvh.ray_cast(p + n * 0.3, -n, 1.0) hp = hit[0] if hit[0] is not None else p lp = Mf.inverted() @ (hp - att).to_4d() q = Vector((u, v, lp.z + 0.0005)) rb.append(bm.verts.new(Mf.to_3x3() @ q)) rt.append(bm.verts.new(Mf.to_3x3() @ Vector((u, v, top)))) topv.append(rt) botv.append(rb) from dra_lib import face as _face for j in range(N): for i in range(N): _face(bm, [topv[j][i], topv[j][i + 1], topv[j + 1][i + 1], topv[j + 1][i]], G, out=n) _face(bm, [botv[j][i], botv[j][i + 1], botv[j + 1][i + 1], botv[j + 1][i]], G, out=-n) per = [(i, 0) for i in range(N)] + [(N, j) for j in range(N)] + [(i, N) for i in range(N, 0, -1)] + [(0, j) for j in range(N, 0, -1)] for k in range(len(per)): (i0, j0), (i1, j1) = per[k], per[(k + 1) % len(per)] a, b = topv[j0][i0], topv[j1][i1] c_, e = botv[j1][i1], botv[j0][i0] mid = (a.co + b.co) / 2 _face(bm, [a, b, c_, e], G, out=mid - n * mid.dot(n)) L.put(bm, None, G, 'D%s1' % ver) R3 = Mf if ver == "A": L.put(rbox((0.26 * ks, 0.26 * ks, 0.030 * ks), 0.004 * ks, rv=0.02 * ks, vax=2), R3 @ Matrix.Translation((0, 0, top + 0.015 * ks)), G, 'DA2', seats=('DA1',)) for (u, v) in ((0.095, 0.095), (-0.095, 0.095), (0.095, -0.095), (-0.095, -0.095)): L.put(hexbolt(0.024 * ks, 0.010 * ks), R3 @ Matrix.Translation((u * ks, v * ks, top + 0.030 * ks)), None, 'DA2b', seats=('DA2',)) # fix round (CRITIQUE_2 should 7): the neck sized to the closed hooks of version A (grip 1: they close to # 0.0608 from the knob's axis, under the head), so they hold it 0.5-0.7 mm off; the head widened to R 0.085 so it # still overhangs the neck (the hooks sit 23 mm inside its rim) # scale-up: the stem and the head x S about the pad's face (the hooks are x S about the wrist centre) L.put(cyl(KNOB_NECK_R * ks, 0.115 * ks, 40, 0.003 * ks, S, z0=top + 0.030 * ks), R3, S, 'DA3', seats=('DA2', 'DA4')) z1, z2 = top + 0.145 * ks, top + 0.195 * ks L.put(lathe([(0.0, z1, 0, S), (0.085 * ks, z1, 0.004 * ks, S), (0.085 * ks, z2, 0.006 * ks, S), (0.0, z2, 0, S)], 56), R3, S, 'DA4', seats=('DA3',)) out[ver] = (L, att) return out def build_ship_fit(sc, meshes): d = _design() top = sc.collection c_rail = link_collection("DoorArm | rail kit", top) c_fix = link_collection("DoorArm | door fixtures", top) L = rail_kit_link(d) me_aft = mesh_from_link(L, "DoorArm | rail kit AFT") me_beam = mesh_from_link(rail_beam_link(d), "DoorArm | rail beam AFT") chain_me = chain_link_mesh() for side in ("AFT", "FORE"): if side == "AFT": me = me_aft else: me = me_aft.copy() me.name = "DoorArm | rail kit FORE" me.transform(_mx("FORE")) me.flip_normals() ob = bpy.data.objects.new("DoorArm_%s | rail kit" % side, me) c_rail.objects.link(ob) if side == "AFT": mb = me_beam else: mb = me_beam.copy() mb.name = "DoorArm | rail beam FORE" mb.transform(_mx("FORE")) mb.flip_normals() ob_b = bpy.data.objects.new("DoorArm_%s | rail beam" % side, mb) ob_b["replaces"] = "Pocket %s | upper rail" % side c_rail.objects.link(ob_b) tray = bpy.data.objects.new("DoorArm_%s | chain tray" % side, None) tray.empty_display_size = 0.2 c_rail.objects.link(tray) c_frame1 = d["carriage"]["travel_ends"][side][1 if side == "AFT" else 0] for i, (x, y, rz) in enumerate(chain_positions(d, side, c_frame1)): lo = bpy.data.objects.new("DoorArm_%s | chain link %02d" % (side, i), chain_me) lo.parent = tray lo.location = (x, y, (d["energy_chain"][side]["z_bottom"] + d["energy_chain"][side]["z_top"]) / 2) lo.rotation_euler = (0, 0, rz) c_rail.objects.link(lo) doors = {"AFT": bpy.data.objects.get("Moonpool_Door_Aft"), "FORE": bpy.data.objects.get("Moonpool_Door_Fore")} for side, door in doors.items(): if door is None: continue for ver, (L, att) in fixture_links(d, door, side).items(): me = mesh_from_link(L, "DoorArm_%s | door fixture %s" % (side, ver)) ob = bpy.data.objects.new("DoorArm_%s | door fixture %s" % (side, ver), me) ob.location = att ob["version"] = ver ob["attach_local"] = d["doors"][side]["attach_local"] c_fix.objects.link(ob)