"""Manta: a third Tender (the user's "sub tubs"), built after the user's reference picture (reference/user_reference.jpg): a pale grey-white glossy mini-sub, long pointed nose with a dark slot at the tip, a wide amber-lit cabin window low on the nose's flanks, two dorsal strakes, a dark M-shaped vent on top forward, swept wing fins with curled-down tips, lower fins swept down, a segmented tool boom out of the stern (starboard, the near side in the picture), a thin mast with a yellow tag. The user, 2026-10-14: "create me this small submarine as usable 3d model asset another version of sub tubs in blender using blender mcp. It can be bigger than usual subtub as soon as it fits the moon pool max limit 1 meter closure. you dont have to do the hatch on top you can just make holder clips so arms can hold the sub tub. try to make it similiar as this referance picture" Fixed by the ship and the carousel (same as the Remora, see models/tender-remora/build/build_remora.py): * moon pool deck well 12.3 x 6.3 m; the launch arm hangs the Tender's origin on its centre; the user's limit: 1 m from the well's edge all round (checked by build/sweep_check.py). * docking arm clamp (models/docking-arm/build/build_dockarm.py): four claws at 45/135/225/315 deg, wrist 1.94 m over the Tender origin, claw pads face r 0.555 at z 1.24-1.34, feet r 0.40-0.52 bottom z 1.405. The clamp's axis is at Tender (0, -0.09), as on the Kazah. No hatch (the user's word): a pedestal with four HOLDER CLIPS at the claws' azimuths, each clip an arc of the Kazah collar's outer profile (wall r 0.535 z 1.18-1.335, hook lip r 0.552, top z 1.375). Axes as the Kazah: 1 unit = 1 m, nose to -Y, port +X, Z up, origin 9 cm behind the clamp axis. No booleans: openings are left out of the hull grid (the grid is laid so the window outline is a grid line), parts are lathed or lofted quads. Runs inside the open Blender (exec through the MCP) or headless; it rebuilds only its own collections. """ import bpy, bmesh, math, os, sys, json, time from mathutils import Vector, Matrix, Euler HERE = r"D:\HADAL - The Descent\models\tender-manta\build" exec(open(os.path.join(HERE, "lib2.py")).read()) exec(open(os.path.join(HERE, "tender_lib.py")).read()) OUT = os.path.dirname(HERE) t_start = time.time() REPORT = {} # ------------------------------------------------------------------ clear the previous build (own collections only) for cname in ("MANTA | Tender (exported)", "MANTA | Collision (UCX)", "MANTA | Sockets", "_tmp"): c = bpy.data.collections.get(cname) if c: for o in list(c.all_objects): bpy.data.objects.remove(o) bpy.data.collections.remove(c) for me in list(bpy.data.meshes): if me.users == 0: bpy.data.meshes.remove(me) # ================================================================== materials (look only; surfaces_manta.py dresses them) PAINT = MAT("MANTA | Hull paint", (0.50, 0.54, 0.58), rough=0.26, coat=1.0) SEAM = MAT("MANTA | Panel seam", (0.05, 0.055, 0.06), rough=0.5) GRAPH = MAT("MANTA | Graphite", (0.030, 0.032, 0.036), rough=0.42) SENSOR = MAT("MANTA | Sensor glass", (0.006, 0.007, 0.009), rough=0.05, coat=1.0) CHROME = MAT("MANTA | Chrome", (0.86, 0.87, 0.89), metal=1.0, rough=0.10) STEEL = MAT("MANTA | Steel", (0.60, 0.61, 0.63), metal=1.0, rough=0.28) IMPEL = MAT("MANTA | Impeller alloy", (0.55, 0.56, 0.58), metal=1.0, rough=0.30) RUBBER = MAT("MANTA | Rubber", (0.017, 0.017, 0.018), rough=0.80) YELLOW = MAT("MANTA | Yellow marking", (0.85, 0.55, 0.04), rough=0.40, coat=0.5) GLASS = MAT("MANTA | Glass", (0.92, 0.96, 1.0), rough=0.02) LENS = MAT("MANTA | Lamp lens", (0.95, 0.97, 1.0), rough=0.03) LAMP = MAT("MANTA | Lamp light", (0.9, 0.9, 0.9), rough=0.3, emit=(1.0, 0.86, 0.66), emit_str=25.0) CAB = MAT("MANTA | Cabin panel", (0.36, 0.31, 0.25), rough=0.55) FLOOR = MAT("MANTA | Cabin floor", (0.07, 0.07, 0.08), rough=0.60) SEAT = MAT("MANTA | Seat", (0.05, 0.035, 0.026), rough=0.55) SCREEN = MAT("MANTA | Screen", (0.01, 0.02, 0.025), rough=0.15, emit=(1.0, 0.62, 0.18), emit_str=3.0) CABGLOW = MAT("MANTA | Cabin light", (0.08, 0.05, 0.02), rough=0.4, emit=(1.0, 0.55, 0.14), emit_str=30.0) for m in (GLASS, LENS): b = m.node_tree.nodes["Principled BSDF"] b.inputs["Transmission Weight"].default_value = 1.0 b.inputs["IOR"].default_value = 1.49 C_ROOT = get_coll("MANTA | Tender (exported)") C_COL = get_coll("MANTA | Collision (UCX)") C_SOCK = get_coll("MANTA | Sockets") C_TMP = get_coll("_tmp") root = bpy.data.objects.new("Manta_Root", None) C_ROOT.objects.link(root) root.empty_display_type = 'ARROWS' # ================================================================== 1. hull surface: superellipse sections along Y Y_NOSE, Y_TAIL = -4.75, 3.50 TAB = [ # y, half width W, top HT, bottom HB, centre line ZM (v03: pointed nose; the back rises to the clamp pedestal) (-4.75, 0.03, 0.03, 0.03, -0.16), (-4.55, 0.22, 0.13, 0.15, -0.17), (-4.15, 0.52, 0.30, 0.33, -0.15), (-3.50, 0.88, 0.50, 0.50, -0.12), (-2.70, 1.20, 0.68, 0.60, -0.08), (-1.70, 1.42, 0.86, 0.66, -0.05), (-0.60, 1.50, 0.94, 0.67, -0.03), (0.40, 1.48, 0.93, 0.66, -0.02), (1.40, 1.36, 0.84, 0.61, 0.0), (2.40, 1.06, 0.64, 0.52, 0.02), (3.05, 0.76, 0.52, 0.46, 0.03), (3.50, 0.45, 0.45, 0.45, 0.04)] Wf = pchip([(r[0], r[1]) for r in TAB]); HTf = pchip([(r[0], r[2]) for r in TAB]) HBf = pchip([(r[0], r[3]) for r in TAB]); ZMf = pchip([(r[0], r[4]) for r in TAB]) N_TOP, N_BOT = 2.3, 2.5 # chine: a crease along each flank (the reference's side line), a tent of 3 % of the half width centred on param TH_CH; # the thin seam column of the ring sits on its apex, so the crease carries a dark line TH_CH, CH_W, CH_A = 0.32, 0.20, 0.03 LINER = 0.07 # cabin lining inside the skin def sec(y, th, inset=0.0): c, s = math.cos(th), math.sin(th) n = N_TOP if s >= 0 else N_BOT W = Wf(y) - inset H = (HTf(y) if s >= 0 else HBf(y)) - inset d = min(abs(th - TH_CH), abs(th - (math.pi - TH_CH))) fade = smoothstep(Y_NOSE + 0.25, Y_NOSE + 1.10, y) * (1 - smoothstep(Y_TAIL - 0.70, Y_TAIL - 0.15, y)) k = 1.0 + CH_A * fade * max(0.0, 1.0 - d / CH_W) x = W * k * math.copysign(abs(c) ** (2.0 / n), c) z = ZMf(y) + H * k * math.copysign(abs(s) ** (2.0 / n), s) return Vector((x, y, z)) def hull_z_at(x, y, top=True): W = Wf(y); n = N_TOP if top else N_BOT; H = HTf(y) if top else HBf(y) q = max(0.0, 1.0 - min(1.0, abs(x) / W) ** n) return ZMf(y) + (1 if top else -1) * H * q ** (1.0 / n) def hull_x_at(z, y, inset=0.0): W = Wf(y) - inset; zm = ZMf(y) top = z >= zm; n = N_TOP if top else N_BOT; H = (HTf(y) if top else HBf(y)) - inset q = max(0.0, 1.0 - min(1.0, abs(z - zm) / H) ** n) return W * q ** (1.0 / n) def inside_depth(p): """> 0 inside the hull (approximate distance)""" y = p.y if y < Y_NOSE or y > Y_TAIL + 0.02: return -1.0 W = Wf(y); zm = ZMf(y) top = p.z >= zm; n = N_TOP if top else N_BOT; H = HTf(y) if top else HBf(y) f = (abs(p.x) / W) ** n + (abs(p.z - zm) / H) ** n return (1.0 - f ** (1.0 / n)) * min(W, H) # window outline (port; starboard mirrored): a lens between stations WY0..WY1, angles THB..THT WY0, WY1 = -4.10, -1.50 THT0, THB0, THC = 0.16, -0.95, -0.40 def win_scale(y): if WY0 <= y <= WY1: t = (y - WY0) / (WY1 - WY0) return max(0.10, math.sin(math.pi * t) ** 0.35) d = WY0 - y if y < WY0 else y - WY1 return 0.10 + 0.90 * smoothstep(0.0, 0.35, d) SEAM_A = (TH_CH - 0.01, TH_CH + 0.01) def ring_angles(y): s = win_scale(y) tt = THC + (THT0 - THC) * s tb = THC + (THB0 - THC) * s P = math.pi segs = [(-P / 2, tb, 14), (tb, tt, 16), (tt, SEAM_A[0], 8), (SEAM_A[0], SEAM_A[1], 1), (SEAM_A[1], P - SEAM_A[1], 50), (P - SEAM_A[1], P - SEAM_A[0], 1), (P - SEAM_A[0], P - tt, 8), (P - tt, P - tb, 16), (P - tb, 1.5 * P, 14)] out = [] for a, b, n in segs: for j in range(n): out.append(a + (b - a) * j / n) return out M = len(ring_angles(0.0)) assert M == 128 K_WIN = ((14, 30), (98, 114)) # face columns of the two windows K_SEAM = (38, 89) # face columns of the two longitudinal seams # stations: ~5.5 cm apart, exact at the window ends, thin seam bands at three frames SEAM_Y = (-1.30, 0.95, 2.30) ys = set() for a, b in ((Y_NOSE, WY0), (WY0, WY1), (WY1, Y_TAIL)): n = max(2, int(round((b - a) / 0.055))) for j in range(n + 1): ys.add(round(a + (b - a) * j / n, 5)) for sy in SEAM_Y: ys = {y for y in ys if not (sy - 0.03 < y < sy + 0.04)} ys |= {sy - 0.03, sy, sy + 0.012, sy + 0.04} YS = sorted(ys) I_WIN = [i for i in range(len(YS) - 1) if YS[i] >= WY0 - 1e-6 and YS[i + 1] <= WY1 + 1e-6] I_SEAM = [i for i in range(len(YS) - 1) if any(abs(YS[i] - sy) < 1e-6 for sy in SEAM_Y)] rings = [[sec(y, th) for th in ring_angles(y)] for y in YS] def uv_rings(rs): out = [] for r, y in zip(rs, YS): u = [0.0] for k in range(1, len(r)): u.append(u[-1] + (r[k] - r[k - 1]).length) out.append([(uu, y) for uu in u]) return out def is_win(i, k): return i in I_WIN and any(a <= k < b for a, b in K_WIN) def is_seam_y(y0, y1): return Y_NOSE + 0.6 < y0 < Y_TAIL - 0.3 and any(abs(y0 - sy) < 1e-6 for sy in SEAM_Y) hull = MB("SM_Manta_Hull", [PAINT, SEAM, SENSOR]) band_mats = [] for i in range(len(YS) - 1): band_mats.append(SEAM if is_seam_y(YS[i], YS[i + 1]) else PAINT) grid_rings(hull, rings, band_mats, cap0=Vector((0, Y_NOSE - 0.03, ZMf(Y_NOSE))), cap1=Vector((0, Y_TAIL, ZMf(Y_TAIL))), uv=uv_rings(rings), skip=lambda i, k: 0 <= i < len(YS) - 1 and is_win(i, k), expect=(len(YS) // 2, 0, (0, 0, -1))) REPORT["hull_stations"] = len(YS) # longitudinal seams: faces whose four corners lie on ring columns K_SEAM, K_SEAM+1 (between the nose cone and the tail) bm = hull.bm mi_seam = hull.mi(SEAM) vidx = {} for i, r in enumerate(rings): for k, p in enumerate(r): vidx[(round(p.x, 5), round(p.y, 5), round(p.z, 5))] = (i, k) mi_sens = hull.mi(SENSOR) K_MOUTH = (22, 106) # ring columns of the window lens's centre line: the mouth's upper edge for f in bm.faces: # the nose's dark mouth: the lower half of the tip (the tip fan by height) c = f.calc_center_median() if c.y < -4.30 and len(f.verts) == 3 and c.z < ZMf(c.y): f.material_index = mi_sens for f in bm.faces: ks = set() ok = True for v in f.verts: key = (round(v.co.x, 5), round(v.co.y, 5), round(v.co.z, 5)) if key not in vidx: ok = False; break ks.add(vidx[key][1]) if ok and len(f.verts) == 4 and any(ks == {kk, kk + 1} for kk in K_SEAM): if -3.95 < f.calc_center_median().y < 3.0: f.material_index = mi_seam if ok and f.calc_center_median().y < -4.30 and (max(ks) <= K_MOUTH[0] or min(ks) >= K_MOUTH[1] or ks == {M - 1, 0}): f.material_index = mi_sens # ================================================================== 2. cabin lining, window glass, frames and lips CY0, CY1 = -4.25, -1.20 ci = [i for i, y in enumerate(YS) if CY0 - 1e-6 <= y <= CY1 + 1e-6] c_ys = [YS[i] for i in ci] lin_rings = [[sec(y, th, LINER) for th in ring_angles(y)] for y in c_ys] cab = MB("SM_Manta_Cabin", [CAB, FLOOR, SEAT, SCREEN, CABGLOW, GRAPH, STEEL]) i_off = ci[0] grid_rings(cab, lin_rings, CAB, cap0=Vector((0, c_ys[0] - 0.02, ZMf(c_ys[0]))), cap1=Vector((0, c_ys[-1], ZMf(c_ys[-1]))), skip=lambda i, k: 0 <= i < len(c_ys) - 1 and is_win(i + i_off, k), expect=(len(c_ys) // 2, 0, (0, 0, 1))) frames = MB("SM_Manta_Frames", [GRAPH]) glass = MB("SM_Manta_Glass", [GLASS]) def hull_normal(i, k): r = rings[i] t1 = r[(k + 1) % M] - r[(k - 1) % M] t2 = rings[min(i + 1, len(YS) - 1)][k] - rings[max(i - 1, 0)][k] n = t2.cross(t1).normalized() return n for (k0, k1) in K_WIN: i0, i1 = I_WIN[0], I_WIN[-1] + 1 loop = [(i, k0) for i in range(i0, i1)] + [(i1, k) for k in range(k0, k1)] + [(i, k1) for i in range(i1, i0, -1)] + \ [(i0, k) for k in range(k1, k0, -1)] P = [rings[i][k] for i, k in loop] L = [lin_rings[i - i_off][k] for i, k in loop] Nn = [hull_normal(i, k) for i, k in loop] if Nn[len(Nn) // 4].dot(P[len(P) // 4] - Vector((0, P[len(P) // 4].y, ZMf(P[len(P) // 4].y)))) < 0: Nn = [-n for n in Nn] cen = sum(P, Vector()) / len(P) outs = [] for j in range(len(P)): t = (P[(j + 1) % len(P)] - P[j - 1]).normalized() o = Nn[j].cross(t).normalized() if o.dot(P[j] - cen) < 0: o = -o outs.append(o) prof = [(0.040, -0.006), (0.037, 0.008), (0.012, 0.016), (0.0, 0.006)] fr = [[P[j] + outs[j] * a + Nn[j] * h for j in range(len(P))] for a, h in prof] + [P, L] grid_rings(frames, fr, GRAPH, expect=(4, len(P) // 4, tuple(outs[len(P) // 4]))) # glass: the window's own grid, a quarter of the way from skin to lining g = [[rings[i][k].lerp(lin_rings[i - i_off][k], 0.25) for k in range(k0, k1 + 1)] for i in range(i0, i1 + 1)] grid_rings(glass, g, GLASS, closed_u=False, expect=(len(g) // 2, (k1 - k0) // 2, tuple(Nn[len(Nn) // 4]))) # ---- cabin interior ZF = -0.47 # deck def floor_hw(y): return hull_x_at(ZF, y, LINER) - 0.004 fl_rows = [] for y in [y for y in c_ys if -3.95 <= y <= CY1]: w = floor_hw(y) fl_rows.append([Vector((x, y, ZF)) for x in (-w, -w * 0.5, 0.0, w * 0.5, w)]) grid_rings(cab, fl_rows, FLOOR, closed_u=False, expect=(1, 1, (0, 0, 1))) # pilot seat (faces -Y), slightly reclined, on a pedestal SY = -2.40 cab.cyl(0.07, 0.07, 0.22, (0, SY + 0.05, ZF + 0.11), STEEL, segs=16) cab.box((0.50, 0.50, 0.10), (0, SY, ZF + 0.27), SEAT, bevel=0.03, seg=2) cab.box((0.50, 0.10, 0.62), (0, SY + 0.30, ZF + 0.60), SEAT, rot=(math.radians(-14), 0, 0), bevel=0.03, seg=2) cab.box((0.26, 0.08, 0.16), (0, SY + 0.40, ZF + 0.98), SEAT, rot=(math.radians(-14), 0, 0), bevel=0.03, seg=2) for s in (1, -1): cab.box((0.07, 0.40, 0.06), (s * 0.29, SY + 0.02, ZF + 0.47), SEAT, bevel=0.02) cab.cyl(0.018, 0.014, 0.12, (s * 0.29, SY - 0.12, ZF + 0.55), GRAPH, segs=10) # control sticks # console in front of the pilot, three amber screens CYc = -3.30 cab.box((0.86, 0.34, 0.30), (0, CYc, ZF + 0.32), GRAPH, rot=(math.radians(25), 0, 0), bevel=0.02) for j, x in enumerate((-0.27, 0.0, 0.27)): cab.box((0.24, 0.012, 0.16), (x, CYc + 0.05, ZF + 0.44), SCREEN, rot=(math.radians(25 + 90 - 90), 0, 0)) cab.box((0.40, 0.30, 0.42), (0, CYc - 0.12, ZF + 0.21), GRAPH, bevel=0.02) # amber light strips along both sides above the windows, and in the ceiling for s in (1, -1): pts = [] for y in [y for y in c_ys if -3.85 <= y <= -1.40]: z = 0.20 + ZMf(y) x = hull_x_at(z, y, LINER) - 0.012 pts.append(Vector((s * x, y, z))) cab.sweep(pts, 0.012, CABGLOW, sides=6) cab.sweep([Vector((0, y, hull_z_at(0, y) - LINER - 0.03)) for y in c_ys if -3.0 <= y <= -1.6], 0.016, CABGLOW, sides=6) # rear bulkhead: door leaf and frame, cabin side cab.box((0.62, 0.05, 1.02), (0, CY1 - 0.03, ZF + 0.51), GRAPH, bevel=0.02) cab.box((0.54, 0.02, 0.94), (0, CY1 - 0.06, ZF + 0.51), STEEL, bevel=0.01) cab.box((0.12, 0.02, 0.04), (0.18, CY1 - 0.075, ZF + 0.52), CABGLOW) # ================================================================== 3. holder clips (the docking clamp's interface; no hatch) clip = MB("_clamp", [PAINT, GRAPH, YELLOW, STEEL]) CL_C = Vector((0.0, -0.09, 0.0)) MC = 64 SK_R0, SK_R1, SK_Z1 = 0.80, 0.50, 1.15 skirt = [] for t in [j / 6 for j in range(7)]: 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.015 z = zb + (SK_Z1 - zb) * (1 - math.cos(t * math.pi / 2)) ring.append(Vector((r * math.cos(ph), r * math.sin(ph), z))) skirt.append(ring) top = [[Vector((r * math.cos(2 * math.pi * a / MC), r * math.sin(2 * math.pi * a / MC), z)) for a in range(MC)] for (r, z) in ((0.50, 1.15), (0.46, 1.15), (0.40, 1.15), (0.36, 1.15))] grid_rings(clip, skirt + top, [PAINT] * 6 + [PAINT, YELLOW, GRAPH, GRAPH], cap1=Vector((0, 0, 1.15)), expect=(8, 0, (0, 0, 1))) # four clips: an arc of the Kazah collar's outer profile each, at the claws' azimuths 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((r * math.cos(a0 + (a1 - a0) * j / segs), 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])) bm2.faces.new(list(reversed(cols[0]))) bm2.faces.new(cols[-1]) 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, 10, STEEL) clip.box((0.05, 0.10, 0.012), (0.47 * math.cos(a), 0.47 * math.sin(a), 1.381), YELLOW, rot=(0, 0, a)) # claw mark clip.transform(Matrix.Translation(CL_C)) REPORT["clips"] = {"centre": [CL_C.x, CL_C.y], "azimuths_deg": CLIP_AZ, "half_width_deg": 20.0, "wall_r": 0.535, "lip_r": 0.552, "top_z": 1.375, "pedestal_top_z": SK_Z1} # ================================================================== 4. attached parts (pruned where they sink into the hull) att = MB("_attached", [PAINT, GRAPH, SENSOR, CHROME, STEEL, RUBBER, YELLOW, LENS, LAMP, SEAM]) def strake(xc): secs = [] for y in [-1.10 + 0.10 * j for j in range(37)]: zt = hull_z_at(xc, y) e = smoothstep(-1.10, -0.70, y) * (1 - smoothstep(2.20, 2.60, y)) h = 0.02 + 0.075 * e secs.append((y, 0.075, zt - 0.05, zt + h, 0.03)) loft(att, secs, PAINT, xf=Matrix.Translation((xc, 0, 0)) @ Matrix.Rotation(math.pi / 2, 4, 'Z')) # the dark vent slot along its top s2 = [] for y in [-0.60 + 0.10 * j for j in range(28)]: zt = hull_z_at(xc, y) + 0.02 + 0.075 * smoothstep(-1.10, -0.70, y) * (1 - smoothstep(2.20, 2.60, y)) s2.append((y, 0.035, zt - 0.02, zt + 0.004, 0.008)) loft(att, s2, SENSOR, xf=Matrix.Translation((xc, 0, 0)) @ Matrix.Rotation(math.pi / 2, 4, 'Z')) for s in (1, -1): strake(s * 0.95) # dark M-shaped vent on top forward, with a raised lip in front of it vrows = [] for j in range(13): u = -1 + 2 * j / 12 x = 0.62 * u yf = -2.72 + 0.30 * abs(u) ** 1.3 yb = -2.18 - 0.10 * abs(u) + 0.16 * (1 - math.cos(math.pi * abs(u))) / 2 - 0.12 * math.exp(-(u * 4) ** 2) col = [] for q in range(5): y = yf + (yb - yf) * q / 4 col.append(Vector((x, y, hull_z_at(x, y) + 0.004))) vrows.append(col) grid_rings(att, vrows, SENSOR, closed_u=False, expect=(6, 2, (0, 0, 1))) lip = [] for j in range(13): u = -1 + 2 * j / 12 x = 0.64 * u y = -2.74 + 0.30 * abs(u) ** 1.3 lip.append(Vector((x, y, 0))) pts = [Vector((p.x, p.y, hull_z_at(p.x, p.y) + 0.005)) for p in lip] att.sweep(pts, 0.022, PAINT, sides=8) # wings: swept, curled down at the tips (manta), on the upper flanks amidships def wing(side): secs = [] for j in range(13): s = j / 12 bank = math.radians(65.0) * smoothstep(0.60, 1.0, s) x = 1.20 + 0.98 * s - 0.12 * smoothstep(0.7, 1.0, s) y = 0.20 + 1.30 * s ** 1.2 z = 0.22 - 0.10 * s - 0.34 * smoothstep(0.60, 1.0, s) ** 1.4 ch = 1.80 - 1.35 * s ** 0.85 th = 0.13 - 0.09 * s nd = Vector((side * math.sin(bank), 0, math.cos(bank))) secs.append((Vector((side * x, y, z)), Vector((0, 1, 0)), nd, ch, th)) foil(att, secs, PAINT) # lower fins: swept back and down from the belly, rubber skid pads on their tips (they land the sub on a deck) def lower_fin(side): secs = [] for j in range(9): s = j / 8 x = 0.78 + 0.95 * s y = -1.20 + 0.95 * s ** 1.2 z = -0.40 - 0.62 * s ch = 1.30 - 0.75 * s th = 0.11 - 0.05 * s nd = Vector((side * 0.60, 0, 0.80)).normalized() secs.append((Vector((side * x, y, z)), Vector((0, 1, 0)), nd, ch, th)) foil(att, secs, PAINT) att.box((0.16, 0.50, 0.05), (side * 1.70, -0.20, -1.045), RUBBER, bevel=0.015) for s in (1, -1): wing(s) lower_fin(s) # chin blade under the nose tip (the "lower jaw") and the chin lamps cb = [] for j in range(7): s = j / 6 y = -4.68 + 1.75 * s z = hull_z_at(0, y, top=False) - 0.11 * math.sin(math.pi * min(1, s * 1.3)) ** 0.7 + 0.03 cb.append((Vector((0, y, z)), Vector((0, 0, 1)), Vector((1, 0, 0)), 0.10 + 0.10 * math.sin(math.pi * s), 0.05)) foil(att, cb, PAINT) for s in (1, -1): y = -3.70 x = 0.46 * s z = hull_z_at(x, y, top=False) att.cyl(0.085, 0.085, 0.10, (x, y, z + 0.01), CHROME, rot=(math.radians(-60), 0, 0), segs=20) att.cyl(0.068, 0.068, 0.02, (x, y - 0.045, z - 0.015), LENS, rot=(math.radians(-60), 0, 0), segs=20) att.cyl(0.05, 0.05, 0.004, (x, y - 0.04, z - 0.01), LAMP, rot=(math.radians(-60), 0, 0), segs=16) # keel: a graphite machinery pod under the belly with vent slats ks = [] for y in [-1.20 + 0.15 * j for j in range(25)]: e = smoothstep(-1.20, -0.75, y) * (1 - smoothstep(2.10, 2.40, y)) zb = hull_z_at(0, y, top=False) ks.append((y, 0.20 + 0.25 * e, zb - 0.02 - 0.16 * e, zb + 0.10, 0.05)) loft(att, ks, GRAPH, xf=Matrix.Rotation(math.pi / 2, 4, 'Z')) for j in range(6): y = -0.40 + 0.30 * j zb = hull_z_at(0, y, top=False) - 0.18 att.box((0.40, 0.035, 0.02), (0, y, zb - 0.005), SENSOR) # machinery bay on each lower flank behind the cabin (the reference's dark recess amidships): a graphite panel laid on the # skin with louvres across it for s in (1, -1): TH0, TH1 = -0.62, -0.06 rows = [] for j in range(21): y = -1.12 + 1.55 * j / 20 ths = [TH0 + (TH1 - TH0) * q / 6 for q in range(7)] r = [sec(y, th, -0.006) for th in ths] rows.append([Vector((s * p.x, p.y, p.z)) for p in r]) grid_rings(att, rows, GRAPH, closed_u=False, expect=(10, 3, (s, 0, 0))) for j in range(7): y = -0.98 + 0.20 * j pts = [sec(y, TH0 + 0.04 + (TH1 - TH0 - 0.08) * q / 8, -0.016) for q in range(9)] att.sweep([Vector((s * p.x, p.y, p.z)) for p in pts], 0.012, SENSOR, sides=6) # side thrusters (two each side, flush grilles low on the flanks) for s in (1, -1): for y in (-0.95, 1.70): z = ZMf(y) - 0.30 x = hull_x_at(z, y) att.cyl(0.13, 0.13, 0.04, (s * (x - 0.005), y, z), GRAPH, rot=(0, math.pi / 2, 0), segs=24) att.cyl(0.10, 0.10, 0.012, (s * (x + 0.012), y, z), SENSOR, rot=(0, math.pi / 2, 0), segs=24) # mast with a ball and the yellow tag at its foot (port, aft) MX, MY = 0.32, 2.05 mz = hull_z_at(MX, MY) att.cyl(0.035, 0.022, 0.20, (MX, MY, mz + 0.08), PAINT, segs=12) att.cyl(0.011, 0.008, 0.62, (MX, MY, mz + 0.48), STEEL, segs=8) att.sphere(0.025, (MX, MY, mz + 0.80), STEEL, segs=10, rings=6) att.box((0.12, 0.08, 0.10), (MX, MY - 0.11, mz + 0.06), YELLOW, bevel=0.012) # tail: hub with stators inside a duct; the duct is fixed, the rotor moves TY0 = Y_TAIL - 0.05 # fixed hub (ends at TY0+0.25), rotor TY0+0.26..0.45, aft cone with the stators TY0+0.46..0.75, duct TY0+0.15..0.70 lathe(att, [(0.0, TY0 - 0.02), (0.46, TY0 - 0.02), (0.46, TY0 + 0.06), (0.38, TY0 + 0.18), (0.24, TY0 + 0.25), (0.0, TY0 + 0.25)], [GRAPH, PAINT, PAINT, GRAPH, GRAPH], segs=32, xf=YPOS) lathe(att, [(0.0, TY0 + 0.46), (0.15, TY0 + 0.47), (0.13, TY0 + 0.66), (0.0, TY0 + 0.75)], GRAPH, segs=24, xf=YPOS) DUCT_Y0, DUCT_Y1 = TY0 + 0.15, TY0 + 0.70 duct = [(0.53, DUCT_Y0), (0.585, DUCT_Y0 + 0.05), (0.595, DUCT_Y0 + 0.22), (0.57, DUCT_Y1), (0.535, DUCT_Y1 - 0.02), (0.515, DUCT_Y0 + 0.20)] revolve_loop(att, duct, [PAINT, PAINT, PAINT, GRAPH, GRAPH, GRAPH], segs=40, xf=YPOS) for j in range(4): # duct struts on the hub (ahead of the rotor) and stators (behind it) a = math.radians(45 + 90 * j) d = Vector((math.cos(a), 0, math.sin(a))) nrm = Vector((-math.sin(a), 0, math.cos(a))) foil(att, [(Vector((0, TY0 + 0.19, 0)) + d * r, Vector((0, 1, 0)), nrm, 0.07, 0.03) for r in (0.28, 0.40, 0.53)], GRAPH) foil(att, [(Vector((0, TY0 + 0.58, 0)) + d * r, Vector((0, 1, 0)), nrm, 0.14, 0.022) for r in (0.10, 0.31, 0.53)], GRAPH) # stern tool mount (starboard, upper quarter, clear of the duct): a housing with a fork; the boom's hinge barrel turns between # the fork's plates about Z (1 cm clear above and below), so the boom swings without touching the mount BX, BY, BZ = -0.72, 2.95, 0.28 att.sphere(0.20, (BX, BY, BZ), PAINT, segs=20, rings=12, scale=(1.0, 1.4, 0.9)) HINGE = Vector((BX, BY + 0.40, BZ)) for sz in (1, -1): att.box((0.22, 0.34, 0.025), (BX, BY + 0.33, BZ + sz * 0.1125), GRAPH, bevel=0.008) att.cyl(0.03, 0.03, 0.012, (BX, HINGE.y, BZ + sz * 0.131), STEEL, segs=12) # pin heads # prune what sinks into the hull, then measure the deepest part left inside REPORT["attached_faces_pruned_inside_hull"] = prune_inside(att, inside_depth, keep_depth=0.015) # ================================================================== 5. moving parts (own objects, origin on the axis) rot = MB("SM_Manta_Rotor", [IMPEL, GRAPH]) lathe(rot, [(0.0, -0.095), (0.17, -0.095), (0.17, 0.07), (0.12, 0.095), (0.0, 0.095)], GRAPH, segs=24, xf=YPOS) for j in range(5): a = 2 * math.pi * j / 5 secs = [] for q in range(5): r = 0.15 + 0.34 * q / 4 tw = math.radians(50 - 25 * q / 4) d = Vector((math.cos(a), 0, math.sin(a))) tang = Vector((-math.sin(a), 0, math.cos(a))) chord = (Vector((0, 1, 0)) * math.cos(tw) + tang * math.sin(tw)).normalized() nrm = chord.cross(d).normalized() secs.append((d * r, chord, nrm, 0.16 - 0.05 * q / 4, 0.022)) foil(rot, secs, IMPEL) ROTOR_AT = Vector((0, TY0 + 0.355, 0)) boom = MB("SM_Manta_ToolBoom", [STEEL, GRAPH, PAINT, YELLOW]) # boom along +Y from its pivot: rod with ring segments (the reference's segmented rod) L_B = 1.05 boom.cyl(0.055, 0.055, L_B, (0, L_B / 2, 0), STEEL, rot=(math.pi / 2, 0, 0), segs=16) for j in range(6): y = 0.22 + 0.13 * j boom.cyl(0.078, 0.078, 0.05, (0, y, 0), GRAPH, rot=(math.pi / 2, 0, 0), segs=16, bevel=0.008) boom.cyl(0.08, 0.08, 0.18, (0, 0, 0), PAINT, segs=20, bevel=0.01) # hinge barrel (axis Z) boom.cyl(0.075, 0.06, 0.10, (0, L_B - 0.02, 0), GRAPH, rot=(math.pi / 2, 0, 0), segs=16) BOOM_AT = HINGE.copy() bit = MB("SM_Manta_ToolBit", [STEEL, GRAPH, YELLOW]) bit.cyl(0.03, 0.03, 0.04, (0, 0.025, 0), STEEL, rot=(math.pi / 2, 0, 0), segs=12) # shaft, 5 mm off the boom's end bit.sphere(0.085, (0, 0.11, 0), GRAPH, segs=16, rings=10) # the knob: drill and suction head bit.cyl(0.05, 0.02, 0.14, (0, 0.22, 0), STEEL, rot=(-math.pi / 2, 0, 0), segs=12) bit.box((0.03, 0.05, 0.03), (0.07, 0.10, 0), YELLOW) BIT_AT = BOOM_AT + Vector((0, L_B + 0.03, 0)) # ================================================================== 6. build objects def finish(mb, name=None, origin=None, parent=root, coll=C_ROOT): o = mb.build(coll, origin=origin, parent=parent, recalc=False) if name: o.name = name return o o_hull = finish(hull) o_cab = finish(cab) o_frames = finish(frames) o_glass = finish(glass) # merge the clamp pedestal and the attached parts into the hull's static set by material: one static mesh o_att = att.build(C_ROOT, parent=root); o_att.name = "SM_Manta_Fittings" o_clip = clip.build(C_ROOT, parent=root); o_clip.name = "SM_Manta_ClampClips" o_rot = rot.build(C_ROOT, loc=ROTOR_AT, parent=root); o_rot.name = "SM_Manta_Rotor" o_boom = boom.build(C_ROOT, loc=BOOM_AT, parent=root); o_boom.name = "SM_Manta_ToolBoom" o_bit = bit.build(C_ROOT, parent=o_boom); o_bit.name = "SM_Manta_ToolBit" o_bit.matrix_parent_inverse.identity() # a plain child: its location is in the boom's frame, at the boom's end o_bit.location = (0, L_B + 0.03, 0) # rig properties (simple-expression drivers: this Blender has Python auto-run off) root["rotor_turns"] = 0.0 root["boom_swing_deg"] = 0.0 root["bit_turns"] = 0.0 for k, d in (("rotor_turns", "rotor turns (Y axis)"), ("boom_swing_deg", "boom swing about its hinge (Z): -10 (in, toward the duct) .. +45 (out) deg; checked by build/motion_check.py"), ("bit_turns", "drill bit turns (its Y axis)")): root.id_properties_ui(k).update(description=d) def drive(ob, idx, expr, prop): fc = ob.driver_add("rotation_euler", idx) dr = fc.driver; dr.type = 'SCRIPTED' v = dr.variables.new(); v.name = "p"; v.type = 'SINGLE_PROP' v.targets[0].id = root; v.targets[0].data_path = '["%s"]' % prop dr.expression = expr drive(o_rot, 1, "p*6.283185", "rotor_turns") drive(o_boom, 2, "p*0.0174533", "boom_swing_deg") drive(o_bit, 1, "p*6.283185", "bit_turns") # sockets for the game for nm, p in (("SOCKET_Clamp", CL_C + Vector((0, 0, 1.94))), ("SOCKET_Lamp_L", Vector((0.42, -3.62, -0.45))), ("SOCKET_Lamp_R", Vector((-0.42, -3.62, -0.45))), ("SOCKET_Pilot_Eye", Vector((0, -2.42, ZF + 1.12))), ("SOCKET_Tool", BIT_AT + Vector((0, 0.29, 0))), ("SOCKET_Thrust", Vector((0, DUCT_Y1, 0)))): e = bpy.data.objects.new(nm, None); C_SOCK.objects.link(e); e.parent = root; e.location = p e.empty_display_size = 0.15 # ================================================================== 7. collision: convex hulls in slices def ucx_from(obj, name, sel): bm2 = bmesh.new() pts = [obj.matrix_world @ v.co for v in obj.data.vertices if sel(obj.matrix_world @ v.co)] for p in pts: bm2.verts.new(p) bmesh.ops.convex_hull(bm2, input=bm2.verts[:]) me = bpy.data.meshes.new(name); bm2.to_mesh(me); bm2.free() o = bpy.data.objects.new(name, me); C_COL.objects.link(o); o.display_type = 'WIRE' return o cuts = [Y_NOSE - 0.1, -3.4, -2.2, -0.9, 0.6, 2.0, Y_TAIL + 0.1] for j in range(len(cuts) - 1): ucx_from(o_hull, "UCX_SM_Manta_Hull_%02d" % (j + 1), lambda p, a=cuts[j], b=cuts[j + 1]: a - 0.01 <= p.y <= b + 0.01) n = len(cuts) for nm, sel in (("wingL", lambda p: p.x > 1.05 and p.z > -0.2), ("wingR", lambda p: p.x < -1.05 and p.z > -0.2), ("finL", lambda p: p.x > 0.75 and p.z < -0.5), ("finR", lambda p: p.x < -0.75 and p.z < -0.5), ("tail", lambda p: p.y > Y_TAIL - 0.1 and abs(p.x) < 0.75)): n += 1 ucx_from(o_att, "UCX_SM_Manta_Hull_%02d" % n, sel) n += 1 ucx_from(o_clip, "UCX_SM_Manta_Hull_%02d" % n, lambda p: p.z > 0.95) # one static exterior mesh (skin, fittings, clips, window frames) with material slots; glass and cabin stay their own meshes with bpy.context.temp_override(active_object=o_hull, selected_editable_objects=[o_hull, o_att, o_clip, o_frames]): bpy.ops.object.join() o_hull.name = "SM_Manta_Hull" # ================================================================== 8. report dg = bpy.context.evaluated_depsgraph_get() allp = [] for o in C_ROOT.all_objects: if o.type == 'MESH': allp += [o.matrix_world @ v.co for v in o.data.vertices] mn = Vector((min(p.x for p in allp), min(p.y for p in allp), min(p.z for p in allp))) mx = Vector((max(p.x for p in allp), max(p.y for p in allp), max(p.z for p in allp))) REPORT["bounds_min"] = [round(c, 3) for c in mn]; REPORT["bounds_max"] = [round(c, 3) for c in mx] REPORT["dimensions"] = [round(c, 3) for c in (mx - mn)] REPORT["audit"] = [audit(o) for o in C_ROOT.all_objects if o.type == 'MESH'] REPORT["build_seconds"] = round(time.time() - t_start, 2) json.dump(REPORT, open(os.path.join(HERE, "build_report.json"), "w"), indent=1) print("BUILD OK", json.dumps({k: REPORT[k] for k in ("dimensions", "build_seconds")}))