"""Tiger: the Tender interface (exec'd into tg_geom's namespace by tg_geom, plain numpy; the picture's frame). The docking arm (models/docking-arm) closes four claws round the clamp axis at 45/135/225/315 deg, the wrist 1.94 m over the Tender's origin, the axis 9 cm ahead of it (the Kazah's convention). Probed in the arm's file (the Mako's probe_head.py, this build's tg_probe_arm.py): the claws never come below z 1.213 (shut r 0.558-0.693, open r 0.61-0.79), the landing feet stay at z 1.405 (r 0.41-0.53). A clip is the Kazah collar's outer profile only where a claw closes, as on the Mako: a curved steel plate 2.9 cm thick (r 0.506-0.535), 22 deg long, a wall up to z 1.335 and a lip (r 0.552, top z 1.375), a yellow mark on top; each on a tapered orange fairing grown out of the structure under it. The hold is the collar's, as for the Kazah and the Mako: the shut claws stand 14 mm off the lip (r 0.558 against 0.552), they do not hook under it (the Critique, 2026-10-22); a real catch would change the arm and every Tender's interface. Where (tg_clamp_place.py against the arm's head in all 20 poses): the clamp axis at station y -0.25 of the picture's frame (the collar's back edge, just behind the glass). Aft of about -0.20 the dorsal fin comes within 0.1 m of the ore hose that hangs over the aft side (0.097 m at -0.20, 0.011 m at -0.05). Forward of -0.25 the fore clips would stand over the bare canopy glass. Raising the Tiger on the arm (shorter fairings) brought the claws down on the shell (0.012 m at +0.15). So the fore pair grows out of the shell's front edge behind the glass and rakes forward over the visor, the aft pair stands on the shell behind the collar; the built model is moved 0.16 m aft (TENDER_SHIFT_Y) so the axis lies at (0, -0.09) of the Tender's frame. The stern boom: the picture draws it pointing aft with a claw; that way it ends 0.63 m from the deck well's lip at BOARD (the user's limit is 1 m). It is a tool arm on a hinge at the hub ring's back, folding down for docking: its own part (SM_Tiger_ToolArm, origin on the hinge, 0 = aft as the picture, 90 deg = folded down), a knuckle on the hub. """ Y_CLAMP = -0.25 # the clamp axis, picture frame TENDER_SHIFT_Y = -0.09 - Y_CLAMP # the built model moves this far along +y CLIP_AZ = (45.0, 135.0, 225.0, 315.0) CLIP_RI, CLIP_RO, CLIP_LIP = 0.506, 0.535, 0.552 CLIP_Z0, CLIP_ZL, CLIP_ZT = 1.188, 1.335, 1.375 CLIP_HALF = math.radians(11.0) CLIP_R = 0.5205 HINGE = np.array([0.0, 4.64, 0.13]) # the tool arm's hinge (picture frame), axis along x ARM_R = 0.035 def cast_down(p, x, y, z0=4.0): """the highest point of a part under (x, y): a ray straight down""" T, _ = p.tris() o = np.array([x, y, z0]) d = np.array([0, 0, -1.0]) v0, e1, e2 = T[:, 0], T[:, 1] - T[:, 0], T[:, 2] - T[:, 0] pv = np.cross(d, e2) det = (e1 * pv).sum(1) ok = np.abs(det) > 1e-12 inv = np.where(ok, 1.0 / np.where(ok, det, 1), 0) tv = o - v0 a = (tv * pv).sum(1) * inv qv = np.cross(tv, e1) b = (d * qv).sum(1) * inv t = (e2 * qv).sum(1) * inv hit = ok & (a >= 0) & (b >= 0) & (a + b <= 1) & (t > 0) return float(z0 - t[hit].min()) if hit.any() else None def smoothstep(a, b, x): t = min(1.0, max(0.0, (x - a) / (b - a))) return t * t * (3 - 2 * t) def _ring_rows(rows, cap=True): """quads between rings of equal size (closed), ends closed by quads (8-point rings: three quads each)""" V = np.concatenate([np.asarray(r, float) for r in rows]) n = len(rows[0]) F = [] for i in range(len(rows) - 1): for j in range(n): a, b = i * n + j, i * n + (j + 1) % n F.append((a, b, b + n, a + n)) if cap and n == 8: for base, rev in ((0, True), ((len(rows) - 1) * n, False)): for q in ((7, 0, 1, 2), (7, 2, 3, 6), (6, 3, 4, 5)): f = tuple(base + k for k in q) F.append(f[::-1] if rev else f) return V, F def part_clips(): """the four clips (steel plates), their marks (yellow) and their fairings (orange), in the picture's frame""" top = merge([part_brow(), part_collar(), part_shell()], "top") plates, marks, fairings, info = [], [], [], {} for az in CLIP_AZ: a = math.radians(az) fore = math.sin(a) < 0 cx, cy = CLIP_R * math.cos(a), Y_CLAMP + CLIP_R * math.sin(a) tang = np.array([-math.sin(a), math.cos(a), 0.0]) t_dir = tang if tang[1] > 0 else -tang chord = 2 * CLIP_R * math.sin(CLIP_HALF) * 0.5 # the fore crests grow out of the shell's front edge behind the glass (0.16 m higher than the visor under the plate) and # rake forward to their plates; the aft fairings stand straight under theirs foot_hc = 0.08 if fore else 0.15 foot_y = Y_CLAMP - 0.12 if fore else cy zs = min(z for z in (cast_down(top, cx + dx, foot_y + dy) for dx in (-0.03, 0.03) for dy in (-foot_hc, -0.5 * foot_hc, 0.0, 0.5 * foot_hc, foot_hc)) if z is not None) z_foot, z_top = zs - 0.045, CLIP_Z0 + 0.014 rows = [] for k in range(9): t = k / 8 z = z_foot + (z_top - z_foot) * t e = smoothstep(0.0, 1.0, t) cd = np.array([0, 1.0, 0]) * (1 - smoothstep(0.55, 1.0, t)) + t_dir * smoothstep(0.55, 1.0, t) cd /= np.linalg.norm(cd) td = np.cross([0, 0, 1.0], cd) td /= np.linalg.norm(td) c = np.array([cx, foot_y + (cy - foot_y) * e, z]) hc = chord + (foot_hc - chord) * (1 - e) ** 1.2 ht = 0.0145 + 0.045 * (1 - e) ** 1.4 ch = 0.008 pts = [(-hc + ch, -ht), (hc - ch, -ht), (hc, -ht + ch), (hc, ht - ch), (hc - ch, ht), (-hc + ch, ht), (-hc, ht - ch), (-hc, -ht + ch)] rows.append([c + cd * u + td * v for u, v in pts]) V, F = _ring_rows(rows) fairings.append(part("clip_fairing", V, F, ORANGE, "paint_orange")) def prof_at(u): e = max(0.0, abs(2 * u - 1) - (1 - 2 * 0.18)) / (2 * 0.18) dz = 0.030 * e * e return [(CLIP_RI, CLIP_Z0), (CLIP_RO, CLIP_Z0), (CLIP_RO, CLIP_ZL - dz), (0.546, 1.348 - dz), (CLIP_LIP, 1.362 - dz), (0.546, CLIP_ZT - dz), (CLIP_RI, CLIP_ZT - dz), (CLIP_RI, 0.5 * (CLIP_Z0 + CLIP_ZT))] rows = [] for j in range(9): u = j / 8 ang = a - CLIP_HALF + 2 * CLIP_HALF * u rows.append([(r * math.cos(ang), Y_CLAMP + r * math.sin(ang), z) for r, z in prof_at(u)]) V, F = _ring_rows(rows) plates.append(part("clip", V, F, OTHER, "steel")) # the yellow mark on the plate's top: a thin plate following the arc's middle mrows = [] for j in range(5): ang = a - 0.045 + 0.09 * j / 4 mrows.append([(r * math.cos(ang), Y_CLAMP + r * math.sin(ang), CLIP_ZT + dzz) for r, dzz in ((0.512, -0.001), (0.530, -0.001), (0.530, 0.004), (0.512, 0.004))]) V, F = loft(mrows, closed=True, cap0=True, cap1=True) marks.append(part("clip_mark", V, F, OTHER, "yellow_mark", smooth=False)) info["%d" % az] = dict(fore=fore, centre=[round(cx, 3), round(cy + TENDER_SHIFT_Y, 3)], stands_on_z=round(zs, 3), fairing_m=round(CLIP_Z0 - zs, 3), height_m=round(CLIP_ZT - zs, 3), on="a raked crest from the shell's front edge, over the visor" if fore else "the shell behind the collar") return [merge(plates, "clips"), merge(marks, "clip_marks"), merge(fairings, "clip_fairings")], info def part_tool_arm(): """the stern's tool arm, in its own frame (origin on the hinge, pointing +y = aft as the picture draws it): a boom to y 0.36 with a wrist block and a two-finger claw; and the knuckle on the hub ring's back (static, picture frame)""" boom = cylinder_y(0.012, 0.36, ARM_R, 0.0, 0.0, n=10, name="tool_boom", cls=OTHER, mat="steel") eye = cylinder_x(0.0, 0.0, 0.042, -0.040, 0.040, n=16, name="tool_eye", cls=DARK, mat="dark_metal") # round: it turns wrist = box(-0.032, 0.032, 0.34, 0.40, -0.040, 0.040, "tool_wrist", DARK, "dark_metal") fingers = [] for sz in (1, -1): a, b, c = (0.0, 0.40, 0.025 * sz), (0.0, 0.48, 0.075 * sz), (0.0, 0.54, 0.050 * sz) fingers += [rod(a, b, 0.016, k=6, name="claw"), rod(b, c, 0.016, k=6, name="claw")] arm = merge([boom, eye, wrist] + fingers, "tool_arm") # the knuckle: two cheeks either side of the arm's eye, on a block fixed to the hub ring's back face h = HINGE # (the block ends 1 cm ahead of the eye's sweep, which reaches 4.598 folded; the cheeks leave 5 mm either side of the eye) knuckle = [box(-0.075, 0.075, 4.565, 4.588, h[2] - 0.06, h[2] + 0.06, "tool_knuckle", DARK, "dark_metal")] for x0, x1 in ((0.045, 0.075), (-0.075, -0.045)): knuckle.append(box(x0, x1, 4.580, h[1] + 0.045, h[2] - 0.045, h[2] + 0.045, "tool_knuckle", DARK, "dark_metal")) knuckle.append(cylinder_x(h[1], h[2], 0.012, -0.08, 0.08, name="tool_pin")) return arm, merge(knuckle, "tool_knuckle") def cylinder_x(yc, zc, r, x0, x1, n=10, name="pin", cls=OTHER, mat="steel"): rows = [[(x, yc + r * math.cos(a), zc + r * math.sin(a)) for a in np.linspace(0, 2 * math.pi, n, endpoint=False)] for x in (x0, x1)] V, F = loft(rows, closed=True, cap0=True, cap1=True) return part(name, V, F, cls, mat)