# Spearfish: everything on the hull (exec'd by build_spearfish.py after sf_hull.py). Quads only: lofts and foils with # three-quad end caps, revolved parts and cylinders with Coons caps, cube spheres, ladder patches (sf_topo.py). # Numbers are blueprint pixels from sf_measure.py unless they are in metres. # 2026-10-20, the user: "delete the red lined artifact only wings should be": the ribbed actuator at the fin root, its # dark recess and the dark block at the fin's fore root are gone; the fins and their root fairings stay. from mathutils.bvhtree import BVHTree att = MB("_attached", [PAINT, GRAPH, SENSOR, CHROME, STEEL, RUBBER, YELLOW, LENS, LAMP, SEAM]) hull.bm.normal_update() HBVH = BVHTree.FromBMesh(hull.bm) VIEW = {"top": (lambda u, v: Vector((u, v, 5.0)), Vector((0, 0, -1))), "bot": (lambda u, v: Vector((u, v, -5.0)), Vector((0, 0, 1))), "sideP": (lambda u, v: Vector((5.0, u, v)), Vector((-1, 0, 0))), "sideS": (lambda u, v: Vector((-5.0, u, v)), Vector((1, 0, 0))), "rear": (lambda u, v: Vector((u, 12.0, v)), Vector((0, -1, 0)))} def proj(view, u, v): """(u, v) of a view's 2D coordinates (metres) -> (point on the hull, outward normal)""" of, d = VIEW[view] hit, nrm, fi, dist = HBVH.ray_cast(of(u, v), d) if hit is None: return None, None n = nrm.normalized() if (hit.y > Y_TAIL - 0.06 or abs(nrm.dot(normal_at(hit))) < 0.5) else normal_at(hit) if n.dot(-d) < 0: n = -n return hit, n def region(outline, axis): """a closed 2D outline -> (lo_hi(t), t0, t1) scanned along coordinate `axis` (0 or 1)""" o = 1 - axis ts = [p[axis] for p in outline] t0, t1 = min(ts), max(ts) pad = (t1 - t0) * 0.02 def lo_hi(t): xs = [] n = len(outline) for i in range(n): a, b = outline[i], outline[(i + 1) % n] if (a[axis] - t) * (b[axis] - t) <= 0 and a[axis] != b[axis]: xs.append(a[o] + (b[o] - a[o]) * (t - a[axis]) / (b[axis] - a[axis])) return (min(xs), max(xs)) if xs else (0.0, 0.0) return lo_hi, t0 + pad, t1 - pad def patch(mb, outline, view, mat, axis=0, lift=0.004, bulge=0.0, rim=None, rim_r=0.008, nt=8, nacross=3, cell=0.035): """a thin plate on the hull over a 2D outline, as a ladder of quads (no fan); optional rim tube round it. Its faces are kept near `cell` in size so the flat plate follows the hull's facets (4 mm proud).""" lo_hi, t0, t1 = region(outline, axis) wmax = max(lo_hi(t0 + (t1 - t0) * i / 8)[1] - lo_hi(t0 + (t1 - t0) * i / 8)[0] for i in range(9)) nt = max(nt, int(math.ceil((t1 - t0) / cell))) nacross = max(nacross, int(math.ceil(wmax / cell))) def to3d(t, s): uv = (t, s) if axis == 0 else (s, t) p, n = proj(view, *uv) return p def nrm(p): _, n = proj(view, *((p.x, p.y) if view in ("top", "bot") else (p.y, p.z) if view.startswith("side") else (p.x, p.z))) return n if n is not None else normal_at(p) ladder_patch(mb, lo_hi, t0, t1, to3d, mat, nt=nt, nacross=nacross, lift=lift, bulge=bulge, normal=nrm) if rim is not None: chain = [(t0 + (t1 - t0) * i / nt, lo_hi(t0 + (t1 - t0) * i / nt)) for i in range(nt + 1)] loop2 = [(t, lh[0]) for t, lh in chain] + [(t, lh[1]) for t, lh in reversed(chain)] pts = [] for (t, s) in loop2 + [loop2[0]]: uv = (t, s) if axis == 0 else (s, t) p, n = proj(view, *uv) pts.append(p + n * rim_r * 0.35) mb.sweep(pts, rim_r, rim, sides=6, caps=False) def capsule(u0, u1, v, r, n=6): """a stadium outline from (u0, v) to (u1, v), radius r""" out = [] for j in range(n + 1): a = -math.pi / 2 + math.pi * j / n out.append((u1 + r * math.cos(a), v + r * math.sin(a))) for j in range(n + 1): a = math.pi / 2 + math.pi * j / n out.append((u0 + r * math.cos(a), v + r * math.sin(a))) return out # ------------------------------------------------------------------ fins (TOP planforms), low on the flank def scan(poly, L): xs = [] n = len(poly) for i in range(n): (x1, l1), (x2, l2) = poly[i], poly[(i + 1) % n] if l1 == l2: continue if (l1 - L) * (l2 - L) <= 0: xs.append(x1 + (x2 - x1) * (L - l1) / (l2 - l1)) return min(xs), max(xs) def fin(poly, Ls, z_img, anh, th0, th1, side, droop_from=None, droop_deg=0.0, mat=PAINT): lmin = min(l for _, l in poly) + 1e-6 secs = [] lat0 = Ls[0] * S for j, L in enumerate(Ls): xa, xf = scan(poly, max(L, lmin)) t = j / (len(Ls) - 1) lat = L * S z = Zi(z_img) - math.tan(math.radians(anh)) * (lat - lat0) bank = math.radians(anh) if droop_from is not None: dd = smoothstep(droop_from, 1.0, t) z -= 0.10 * dd ** 1.5 bank += math.radians(droop_deg) * dd nd = Vector((side * math.sin(bank), 0, math.cos(bank))) secs.append((Vector((side * lat, Yi((xa + xf) / 2), z)), Vector((0, 1, 0)), nd, (xf - xa) * S, th0 + (th1 - th0) * t)) foil(att, secs, mat) return secs BIG_LS = [56, 62, 68, 74, 80, 86, 92, 98, 104, 109, 113.5, 117, 119.6, 121.4, 122.6] CAN_LS = [52, 58, 64, 69, 73.5, 77.5, 80.5, 82.4, 83.2] for side in (1, -1): fin(BIGFIN, BIG_LS, BIGFIN_Y, BIGFIN_ANHEDRAL, 0.13, 0.035, side) fin(CANARD, CAN_LS, CANARD_Y, 0.0, 0.075, 0.028, side, droop_from=0.55, droop_deg=38.0) # lamp pods on the big fins' outer fore corners (FRONT: a round disc at the end of each fin) POD_R, POD_X0, POD_X1, POD_L = 0.070, 771.0, 727.0, 117.0 # inside the TOP planform (L 116.5-120.5 there) for side in (1, -1): zt = Zi(BIGFIN_Y) - math.tan(math.radians(BIGFIN_ANHEDRAL)) * (POD_L - BIG_LS[0]) * S front = Vector((side * POD_L * S, Yi(POD_X0), zt)) ln = (POD_X0 - POD_X1) * S prof = [(0.045, 0.0), (0.054, 0.004), (0.066, 0.012), (POD_R, 0.04), (POD_R, ln - 0.20), (0.050, ln - 0.03), (0.016, ln)] revolve(att, prof, [CHROME, CHROME, PAINT, PAINT, PAINT, PAINT], segs=16, xf=Matrix.Translation(front) @ YPOS, cap0_mat=LENS, cap1_mat=PAINT) qcyl(att, 0.038, 0.004, front + Vector((0, 0.014, 0)), LAMP, rot=(math.pi / 2, 0, 0), segs=16) # fin root fairing along the flank (SIDE: the swept blade from aft-low to fore-high); three-quad end caps def surf_loft(mb, Pp, ws, hs, mat): bm = bmesh.new() R = [] n = len(Pp) for j in range(n): t = (Pp[min(j + 1, n - 1)] - Pp[max(j - 1, 0)]).normalized() N = normal_at(Pp[j]); N = (N - N.dot(t) * t).normalized() b = N.cross(t) w, h = ws[j], hs[j] sec_pts = [(-w, -0.025), (-0.75 * w, 0.55 * h), (-0.3 * w, h), (0.3 * w, h), (0.75 * w, 0.55 * h), (w, -0.025), (0.4 * w, -0.035), (-0.4 * w, -0.035)] R.append([bm.verts.new(Pp[j] + b * u + N * v) for u, v in sec_pts]) ring_rows(bm, R) for r in (R[0], R[-1]): bm.faces.new((r[0], r[1], r[2], r[7])); bm.faces.new((r[7], r[2], r[3], r[6])); bm.faces.new((r[6], r[3], r[4], r[5])) bm.normal_update() bmesh.ops.recalc_face_normals(bm, faces=bm.faces[:]) mb.merge(bm, mat) for sg in (1, -1): pts2 = resample([(Yi(x), Zi(v)) for x, v in STRAKE], 0.09, closed=False) Pp = [on_side(sg, y, z) for y, z in pts2] n = len(Pp) ws = [0.030 + 0.075 * smoothstep(0.0, 0.62, j / (n - 1)) - 0.06 * smoothstep(0.88, 1.0, j / (n - 1)) for j in range(n)] hs = [0.020 + 0.050 * smoothstep(0.0, 0.5, j / (n - 1)) - 0.04 * smoothstep(0.85, 1.0, j / (n - 1)) for j in range(n)] surf_loft(att, Pp, ws, hs, PAINT) # small dark root blocks (TOP: at the big fin's aft root and the canard's fore root) for side in (1, -1): for (xa, xb, la, lb, zi) in ((697, 706, 66, 70, BIGFIN_Y), (882, 890, 60, 66, CANARD_Y)): c = Vector((side * (la + lb) / 2 * S, Yi((xa + xb) / 2), Zi(zi) + 0.01)) qbox(att, ((lb - la) * S + 0.02, (xb - xa) * S, 0.07), c, GRAPH, bevel=0.012) # ------------------------------------------------------------------ dorsal rails (TOP: two long capsules; SIDE: raised 0.12 m) rx0, rx1, rlat, rhw, rtop = RAILS for side in (1, -1): lat = side * rlat * S ztop = Zi(rtop) xs = [rx0 + (rx1 - rx0) * t for t in (0, 0.02, 0.05, 0.09, 0.14, 0.3, 0.5, 0.7, 0.86, 0.91, 0.95, 0.98, 1.0)] def rail_sec(x, inset=0.0): y = Yi(x) zh = hull_z_at(lat, y) ramp = smoothstep(rx0, rx0 + 16, x) * (1 - smoothstep(rx1 - 14, rx1, x)) z1 = zh + 0.012 + (ztop - zh - 0.012) * ramp ** 0.7 w = rhw * S * (0.55 + 0.45 * ramp ** 0.5) return y, w, zh, z1, ramp secs = [] for x in xs: y, w, zh, z1, ramp = rail_sec(x) secs.append((y, w, zh - 0.06, z1, min(w * 0.55, 0.05))) secs.sort(key=lambda s: s[0]) loft(att, secs, PAINT, xf=Matrix.Translation((lat, 0, 0)) @ Matrix.Rotation(math.pi / 2, 4, 'Z')) gs = [] for x in [rx0 + 8 + (rx1 - rx0 - 16) * t for t in (0, 0.04, 0.1, 0.3, 0.5, 0.7, 0.9, 0.96, 1.0)]: y, w, zh, z1, ramp = rail_sec(x) gs.append((y, 0.025 * (0.3 + 0.7 * ramp), z1 - 0.012, z1 + 0.002, 0.008)) gs.sort(key=lambda s: s[0]) loft(att, gs, GRAPH, xf=Matrix.Translation((lat, 0, 0)) @ Matrix.Rotation(math.pi / 2, 4, 'Z')) # ------------------------------------------------------------------ dorsal scoop (TOP: dark horseshoe; SIDE: a hump; FRONT: a dark arch) hx0, hx1, hhw, htop = SCOOP_HUMP H_SC = 0.10 rows = [] nu = 8 for x in [hx0 + (hx1 - hx0) * j / 8 for j in range(9)]: y = Yi(x) row = [] for q in range(nu + 1): u = -1 + 2 * q / nu lat = u * hhw * S * (0.55 + 0.45 * smoothstep(hx0, hx1, x) ** 0.5) h = H_SC * smoothstep(hx0, hx1 - 4, x) ** 0.8 * max(0.0, 1 - abs(u) ** 3) ** 0.5 row.append(Vector((lat, y, hull_z_at(lat, y) + h - 0.01 * (abs(u) > 0.999)))) rows.append(row) last = rows[-1] mouth = [Vector((p.x, p.y, hull_z_at(p.x, p.y) - 0.01)) for p in last] grid_rings(att, [list(r) for r in zip(*rows)], PAINT, closed_u=False, expect=(nu // 2, 4, (0, 0, 1))) grid_rings(att, [last, mouth], GRAPH, closed_u=False, expect=(0, nu // 2, (0, -1, 0))) recess = [(938, 27), (950, 14), (952, 0), (950, -14), (938, -27), (960, -18), (993, -11), (993, 11), (960, 18)] patch(att, [(l * S, Yi(x)) for x, l in recess], "top", SENSOR, axis=0, nt=8, nacross=4) rx_a, rx_b, rhw2 = SCOOP_RAMP patch(att, [(-rhw2 * S, Yi(rx_a)), (rhw2 * S, Yi(rx_a)), (rhw2 * S, Yi(rx_b)), (-rhw2 * S, Yi(rx_b))], "top", PAINT, axis=1, lift=0.007, rim=GRAPH, rim_r=0.006, nt=4, nacross=3) # over the recess plate (4 mm) # ------------------------------------------------------------------ nose sensors (TOP and UNDERSIDE: dark capsules near the tip) def sensor_outline(spec, x_end): up = [(spec[0][0] - 4, 0.0)] + [(x, l) for x, l in spec] + [(x_end, 0.0)] dn = [(x, -l) for x, l in reversed(spec)] return [(l * S, Yi(x)) for x, l in up + dn] patch(att, sensor_outline(NOSE_SENSOR_TOP, 1101), "top", SENSOR, axis=1, bulge=0.012, rim=GRAPH, rim_r=0.006, nt=8) patch(att, sensor_outline(NOSE_SENSOR_BOT, 1098), "bot", SENSOR, axis=1, bulge=0.010, rim=GRAPH, rim_r=0.006, nt=8) # ------------------------------------------------------------------ stern lip = [] for j in range(5): x = 520 + (536 - 520) * j / 4 y = Yi(x) zt = min(TOPf(min(y, Y_TAIL)), Zi(STERN_LIP[1]) + (j / 4) * 0.06) if y <= Y_TAIL else Zi(STERN_LIP[1]) + (j / 4) * 0.06 lip.append((y, 18 * S * (0.85 + 0.15 * j / 4), zt - 0.05, zt, 0.02)) lip.sort(key=lambda s: s[0]) loft(att, lip, PAINT, xf=Matrix.Rotation(math.pi / 2, 4, 'Z')) lx0, lx1, llat, lz = STERN_LUGS for side in (1, -1): qbox(att, (0.10, (lx1 - lx0) * S, 0.13), (side * llat * S, Yi((lx0 + lx1) / 2), Zi(lz)), PAINT, bevel=0.03) # the stern hatch and its vent slots (REAR view), on the end panel and the steep aft underside # (below the recessed end panel, which starts at z 0.25: a plate across the recess's lip would step in and out of it) hatch_out = [(-0.30, 0.20), (0.30, 0.20), (0.40, 0.12), (0.42, 0.0), (0.38, -0.20), (0.26, -0.27), (-0.26, -0.27), (-0.38, -0.20), (-0.42, 0.0), (-0.40, 0.12)] patch(att, hatch_out, "rear", PAINT, axis=0, lift=0.006, rim=GRAPH, rim_r=0.010, nt=8, nacross=6) for sx in (-0.33, -0.25, 0.25, 0.33): patch(att, [(sx + v, u) for u, v in capsule(-0.12, 0.10, 0.0, 0.022)], "rear", SENSOR, axis=1, lift=0.010, nt=4, nacross=1) tx0, tx1, tlat = TOP_AFT_VENT for x in (549.5, 556.0, 562.5): patch(att, capsule(-tlat * S, tlat * S, Yi(x), 2.0 * S), "top", SENSOR, axis=0, nt=6, nacross=1) for vw, sg in (("sideP", 1), ("sideS", -1)): (xa, va), (xb, vb) = STERN_VENT_SIDE patch(att, capsule(Yi(xb), Yi(xa), Zi(va), 5 * S), vw, SENSOR, axis=0, rim=PAINT, rim_r=0.008, nt=6, nacross=2) # pods with whip antennas (TOP: two small capsules with antennas raked aft and out; SIDE: the mast) MASTS = {} px0, px1, plat = STERN_PODS ax_, alat, av = ANTENNA_TIP for side in (1, -1): yc = Yi((px0 + px1) / 2) zc = hull_z_at(plat * S, yc) c = Vector((side * plat * S, yc, zc)) qsphere(att, c + Vector((0, 0, 0.005)), (0.075, 0.12, 0.055), GRAPH) qsphere(att, c + Vector((0, -0.02, 0.025)), (0.045, 0.07, 0.04), LENS) base = c + Vector((0, 0.07, 0.03)) tipp = Vector((side * alat * S, Yi(ax_), Zi(av))) # the whip is its own object: raised as drawn, folded aft along the stern slope for the carousel (the raised whips # reach 0.52 m into the bay's curved wall in the carousel sweep; folded they clear it, build/sweep_check.py) mb_m = MB("SM_Spearfish_Mast_%s" % ("L" if side > 0 else "R"), [STEEL, GRAPH]) pts = [base.lerp(tipp, t) for t in (0, 0.08, 0.3, 0.6, 1.0)] mb_m.sweep(pts, [0.016, 0.010, 0.008, 0.006, 0.004], STEEL, sides=6, caps=False) qsphere(mb_m, base, (0.022, 0.022, 0.022), GRAPH, cuts=2) qsphere(mb_m, tipp, (0.0045, 0.0045, 0.0045), STEEL, cuts=1) MASTS[side] = (mb_m, base.copy(), (tipp - base).normalized(), (tipp - base).length) qcyl(att, 0.028, 0.07, base + Vector((0, 0, -0.01)), GRAPH, r2=0.024) bx = Vector((side * plat * S, Yi(584.0), Zi(338.0))) qcyl(att, 0.012, 0.12, bx + Vector((0, 0, -0.08)), STEEL, segs=8) qbox(att, (0.05, 0.11, 0.10), bx, GRAPH, bevel=0.015) # folded pose: pointing straight aft and down the stern slope, clear of the skin (checked here along its length) MAST_FOLD = {} for side, (mb_m, base, d_r, ln) in MASTS.items(): best = None for k in range(40): slope = -0.20 - 0.01 * k d_f = Vector((0, 1.0, slope)).normalized() clear = min(base.z + d_f.z * ln * t - hull_z_at(base.x, base.y + d_f.y * ln * t) for t in [j / 20 for j in range(2, 21)] if base.y + d_f.y * ln * t <= Y_TAIL) if clear >= 0.025: best = (d_f, clear) if best is None: best = (Vector((0, 1.0, -0.20)).normalized(), None) d_f = best[0] axis = d_r.cross(d_f).normalized() ang = math.acos(max(-1.0, min(1.0, d_r.dot(d_f)))) MAST_FOLD[side] = (axis, ang, best[1]) REPORT["mast_fold"] = {("L" if s > 0 else "R"): dict(axis=[round(c, 4) for c in v[0]], deg=round(math.degrees(v[1]), 2), least_clear_m=None if v[2] is None else round(v[2], 3)) for s, v in MAST_FOLD.items()} # ------------------------------------------------------------------ underside (UNDERSIDE, inferred) bx0, bx1, bhw, (sl0, sl1) = VENTRAL_BLISTER bout = [(bhw * S * math.cos(a), Yi(bx0 + bhw) + bhw * S * math.sin(a)) for a in [math.pi * j / 8 for j in range(9)]] bout = bout + [(-bhw * S, Yi(bx1)), (bhw * S, Yi(bx1))] patch(att, bout, "bot", PAINT, axis=1, lift=0.004, bulge=0.020, rim=GRAPH, rim_r=0.007, nt=8, nacross=3) for x in (893, 898, 903, 908): p, n = proj("bot", 0.0, Yi(x)) qbox(att, (0.17, 0.030, 0.012), p + n * 0.020, SENSOR) ux0, ux1, ulat = UNDER_SLOTS for side in (1, -1): patch(att, [(side * ulat * S + v, u) for u, v in capsule(Yi(ux1), Yi(ux0), 0.0, 2.5 * S)], "bot", SENSOR, axis=1, rim=PAINT, rim_r=0.007, nt=6, nacross=1) sx0, sx1, shw = UNDER_STERN_VENT for l in (-8, 0, 8): patch(att, [(l * S + v, u) for u, v in capsule(Yi(sx1), Yi(sx0), 0.0, 2.2 * S)], "bot", SENSOR, axis=1, nt=4, nacross=1) # ------------------------------------------------------------------ docking interface: holder clips for the docking arm # The docking arm (models/docking-arm) closes four claws at 45/135/225/315 deg round the clamp axis (0, -0.09), wrist # 1.94 m over the origin, claw pads r 0.555 at z 1.24-1.34. As on the Manta: an arc of the Kazah collar's outer profile # at each claw (wall r 0.535 z 1.18-1.335, hook lip r 0.552, top z 1.375). They stand on a slim column between the # dorsal rails (the rails' inner edges 0.403 m off the centre line, their tops at z 1.094, 3 cm under the disc). clip = MB("_clamp", [PAINT, GRAPH, YELLOW, STEEL]) CL_C = Vector((0.0, Yi(CL_IMG), 0.0)) MC = 32 SK_R0, SK_R1, SK_Z1 = 0.37, 0.32, 1.12 bmc2 = bmesh.new() crow = [] for t in [j / 4 for j in range(5)]: ring = [] for a in range(MC): ph = 2 * math.pi * a / MC r = SK_R1 + (SK_R0 - SK_R1) * (1 - math.sin(t * math.pi / 2)) zb = hull_z_at(CL_C.x + SK_R0 * math.cos(ph), CL_C.y + SK_R0 * math.sin(ph)) - 0.02 z = zb + (SK_Z1 - zb) * (1 - math.cos(t * math.pi / 2)) ring.append(bmc2.verts.new((CL_C.x + r * math.cos(ph), CL_C.y + r * math.sin(ph), z))) crow.append((ring, clip.mi(PAINT))) for (r, z), m_ in (((0.50, 1.122), PAINT), ((0.53, 1.130), PAINT), ((0.53, 1.150), PAINT), ((0.46, 1.150), PAINT), ((0.40, 1.150), YELLOW), ((0.36, 1.150), GRAPH)): crow.append(([bmc2.verts.new((CL_C.x + r * math.cos(2 * math.pi * a / MC), CL_C.y + r * math.sin(2 * math.pi * a / MC), z)) for a in range(MC)], clip.mi(m_))) for (ra, ma), (rb, mb_) in zip(crow, crow[1:]): ring_rows(bmc2, [ra, rb], mat_index=mb_) coons_cap(bmc2, crow[-1][0], clip.mi(GRAPH)) bmc2.normal_update() bmesh.ops.recalc_face_normals(bmc2, faces=bmc2.faces[:]) topf = max(bmc2.faces, key=lambda f: f.calc_center_median().z) if topf.normal.z < 0: bmesh.ops.reverse_faces(bmc2, faces=bmc2.faces[:]) clip.merge(bmc2, None) CLIP_LOOP = [(0.400, 1.150), (0.520, 1.150), (0.535, 1.180), (0.535, 1.335), (0.548, 1.350), (0.552, 1.365), (0.545, 1.375), (0.400, 1.375)] CLIP_HALF = math.radians(20.0) def revolve_arc(mb, loop, a0, a1, segs, mat): bm2 = bmesh.new() cols = [[bm2.verts.new((CL_C.x + r * math.cos(a0 + (a1 - a0) * j / segs), CL_C.y + r * math.sin(a0 + (a1 - a0) * j / segs), z)) for (r, z) in loop] for j in range(segs + 1)] n = len(loop) for j in range(segs): for q in range(n): bm2.faces.new((cols[j][q], cols[j + 1][q], cols[j + 1][(q + 1) % n], cols[j][(q + 1) % n])) for c in (cols[0], cols[-1]): # the 8-point profile closed by three quads bm2.faces.new((c[0], c[1], c[2], c[7])); bm2.faces.new((c[7], c[2], c[3], c[6])); bm2.faces.new((c[6], c[3], c[4], c[5])) bm2.normal_update() bmesh.ops.recalc_face_normals(bm2, faces=bm2.faces[:]) mb.merge(bm2, mat) CLIP_AZ = (45.0, 135.0, 225.0, 315.0) for az in CLIP_AZ: a = math.radians(az) revolve_arc(clip, CLIP_LOOP, a - CLIP_HALF, a + CLIP_HALF, 6, STEEL) qbox(clip, (0.05, 0.05, 0.006), (CL_C.x + 0.47 * math.cos(a), CL_C.y + 0.47 * math.sin(a), 1.378), YELLOW, rot=(0, 0, a)) # ------------------------------------------------------------------ top hatch lid (TOP: the trapezoid panel ahead of the rails); moves (hxa, hwa), (hxf, hwf) = HATCH hatch = MB("SM_Spearfish_Hatch", [PAINT, GRAPH, YELLOW]) LID_Z0, LID_Z1 = 0.012, 0.030 # the lid's underside and top over the skin (the frame's rim reaches 0.006) NU_L, NV_L = 8, 5 def lid_pt(i, j, lift): v = j / NV_L y = Yi(hxa) + (Yi(hxf) - Yi(hxa)) * v hw = (hwa + (hwf - hwa) * v) * S x = -hw + 2 * hw * i / NU_L q = on_top(x, y) return q + normal_at(q) * lift bml2 = bmesh.new() TOPV = [[bml2.verts.new(lid_pt(i, j, LID_Z1)) for i in range(NU_L + 1)] for j in range(NV_L + 1)] BOTV = [[bml2.verts.new(lid_pt(i, j, LID_Z0)) for i in range(NU_L + 1)] for j in range(NV_L + 1)] ring_rows(bml2, TOPV, closed=False) ring_rows(bml2, BOTV, closed=False) def edge_loop(G): return ([G[0][i] for i in range(NU_L + 1)] + [G[j][NU_L] for j in range(1, NV_L + 1)] + [G[NV_L][i] for i in range(NU_L - 1, -1, -1)] + [G[j][0] for j in range(NV_L - 1, 0, -1)]) ring_rows(bml2, [edge_loop(TOPV), edge_loop(BOTV)], closed=True) bml2.normal_update() bmesh.ops.recalc_face_normals(bml2, faces=bml2.faces[:]) hatch.merge(bml2, PAINT) cu, cvv = 0.0, 0.5 * (Yi(hxa) + Yi(hxf)) for (i, j) in ((1, 1), (NU_L - 1, 1), (1, NV_L - 1), (NU_L - 1, NV_L - 1)): q = lid_pt(i, j, LID_Z1 + 0.003) qcyl(hatch, 0.014, 0.010, q, GRAPH, segs=8) gy = Yi(hxf) + 0.07 hatch.sweep([on_top(x, gy) + Vector((0, 0, LID_Z1 + 0.03 * (abs(x) < 0.10))) for x in (-0.14, -0.14, -0.09, 0.09, 0.14, 0.14)][1:-1], 0.012, YELLOW, sides=8) HATCH_HINGE = Vector((0.0, Yi(hxa), hull_z_at(0.0, Yi(hxa)) + 0.5 * (LID_Z0 + LID_Z1))) # ------------------------------------------------------------------ prune what sank into the hull REPORT["pruned_faces"] = prune_inside(att, inside_depth, keep_depth=0.02) REPORT["stage"] = "fittings"