"""Remora: a second Tender (the user's "subtubs"), built after the user's reference picture (reference/user_reference.jpg): a sleek, glossy black mini-sub, wide and low, a big wrap-around nose window lit amber from inside, a bubble canopy on top, two ball-headed sensor masts, lamp pods at the front corners, round thruster nacelles at the rear, orange trim lines. 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", then "it can be bigger than usual subtub as soon as it fits the moon pool max limit 1 meter closure". Read as: bigger than the Kazah is allowed as long as it keeps 1 m from the moon pool's edge all round. Fixed by the ship and the carousel (measured 2026-10-14, see README): * moon pool deck well (interior fork, curve "Moonpool | inner black lip"): 12.3 x 6.3 m, rounded ends; the launch arm hangs the Tender's origin on its centre. With 1 m all round the plan may be 2.15 m half-width amidships, 1.27 m at 4.0 m forward, 1.22 m at 4.0 m aft (port side, the narrower one). * docking arm clamp (models/docking-arm/build/build_dockarm.py): wrist axis 1.94 m over the Tender origin, landing feet r 0.40-0.52 down to z 1.405, claw pads face r 0.555 at z 1.24-1.34, fingers end at z 1.215, head plate underside z 1.57. So the hatch collar keeps the Kazah's interface: wall r 0.535 (z 1.18-1.335), flange r 0.552 top z 1.375, nothing over z 1.40 inside r 0.52. Axes as the Kazah: 1 unit = 1 m, nose to -Y, port +X, Z up, origin on the hatch collar's axis. No booleans: openings are frames laid on the hull (the hull's cut edge lies under the frame), parts are lathed or lofted quads. """ import bpy, bmesh, math, os, sys, json, time from mathutils import Vector, Matrix, Euler HERE = os.path.dirname(os.path.abspath(__file__)) exec(open(os.path.join(HERE, "lib2.py")).read()) exec(open(os.path.join(HERE, "remora_lib.py")).read()) OUT = os.path.dirname(HERE) bpy.ops.wm.read_factory_settings(use_empty=True) t_start = time.time() REPORT = {} # ================================================================== materials (look only; surfaces.py dresses them) PAINT = MAT("REMORA | Hull paint", (0.010, 0.011, 0.013), rough=0.22, coat=1.0) GRAPH = MAT("REMORA | Graphite", (0.035, 0.037, 0.040), rough=0.45) CHROME = MAT("REMORA | Chrome", (0.86, 0.87, 0.89), metal=1.0, rough=0.10) RUBBER = MAT("REMORA | Rubber", (0.017, 0.017, 0.018), rough=0.80) ORANGE = MAT("REMORA | Orange paint", (0.80, 0.22, 0.02), rough=0.40, coat=0.6) AMBER = MAT("REMORA | Amber trim light", (0.06, 0.03, 0.01), rough=0.3, emit=(1.0, 0.40, 0.05), emit_str=6.0) GLASS = MAT("REMORA | Glass", (0.92, 0.96, 1.0), rough=0.02) LENS = MAT("REMORA | Lamp lens", (0.95, 0.97, 1.0), rough=0.03) LAMP = MAT("REMORA | Lamp light", (0.9, 0.9, 0.9), rough=0.3, emit=(1.0, 0.86, 0.66), emit_str=25.0) SENSOR = MAT("REMORA | Sensor glass", (0.008, 0.009, 0.011), rough=0.06, coat=1.0) IMPEL = MAT("REMORA | Impeller alloy", (0.55, 0.56, 0.58), metal=1.0, rough=0.30) STEEL = MAT("REMORA | Steel", (0.60, 0.61, 0.63), metal=1.0, rough=0.28) CAB = MAT("REMORA | Cabin panel", (0.55, 0.50, 0.44), rough=0.55) FLOOR = MAT("REMORA | Cabin floor", (0.09, 0.09, 0.10), rough=0.60) SEAT = MAT("REMORA | Seat", (0.045, 0.032, 0.026), rough=0.55) SCREEN = MAT("REMORA | Screen", (0.01, 0.02, 0.025), rough=0.15, emit=(0.15, 0.75, 1.0), emit_str=3.0) CABGLOW = MAT("REMORA | Cabin light", (0.08, 0.05, 0.02), rough=0.4, emit=(1.0, 0.48, 0.10), 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("REMORA | Tender (exported)") C_COL = get_coll("REMORA | Collision (UCX)") C_SOCK = get_coll("REMORA | Sockets") C_TMP = get_coll("_tmp") root = bpy.data.objects.new("Remora_Root", None) C_ROOT.objects.link(root) root.empty_display_type = 'ARROWS' # ================================================================== 1. hull surface Y_NOSE, Y_TAIL = -4.50, 4.20 NOSE_L, TAIL_L = 1.10, 0.50 Y_A, Y_B = Y_NOSE + NOSE_L, Y_TAIL - TAIL_L Wf = pchip([(-4.50, 1.06), (-3.40, 1.16), (-2.40, 1.28), (-1.20, 1.38), (0.0, 1.41), (0.8, 1.36), (1.45, 1.07), (2.0, 0.98), (3.0, 0.82), (3.7, 0.62), (4.2, 0.50)]) ZMf = pchip([(-4.50, -0.16), (-3.40, -0.12), (-2.0, -0.07), (0.0, -0.05), (2.0, -0.03), (3.4, 0.02), (4.2, 0.06)]) HTf = pchip([(-4.50, 0.62), (-3.40, 0.80), (-2.40, 0.97), (-1.20, 1.04), (0.0, 1.05), (1.20, 1.01), (2.40, 0.84), (3.40, 0.55), (4.20, 0.32)]) HBf = pchip([(-4.50, 0.58), (-3.40, 0.76), (-2.40, 0.88), (-1.20, 0.92), (0.0, 0.92), (1.20, 0.89), (2.40, 0.74), (3.40, 0.46), (4.20, 0.24)]) NTf = pchip([(-4.50, 2.15), (-3.40, 2.30), (-2.00, 2.60), (2.00, 2.60), (4.20, 2.20)]) NBf = pchip([(-4.50, 2.15), (-3.40, 2.40), (-2.00, 2.90), (2.00, 2.90), (4.20, 2.30)]) def env(y): if y < Y_A: return math.sqrt(max(0.0, 1 - ((Y_A - y) / NOSE_L) ** 2)) if y > Y_B: return math.sqrt(max(0.0, 1 - ((y - Y_B) / TAIL_L) ** 2)) return 1.0 def ridge(x, y): """the raised hood strip in front of the canopy (the reference's bonnet with its two creases)""" return 0.035 * smoothstep(-4.25, -3.80, y) * (1 - smoothstep(-3.05, -2.80, y)) * (1 - smoothstep(0.24, 0.38, abs(x))) def sec_xz(y, th): """port half of the section at y: th -pi/2 = bottom centre, 0 = waist, pi/2 = top centre""" E = env(y) W, Ht, Hb, zm = Wf(y) * E, HTf(y) * E, HBf(y) * E, ZMf(y) c, s = math.cos(th), math.sin(th) if s >= 0: n = NTf(y) x = W * abs(c) ** (2 / n) return x, zm + Ht * abs(s) ** (2 / n) + ridge(x, y) n = NBf(y) return W * abs(c) ** (2 / n), zm - Hb * abs(s) ** (2 / n) def hull_hw(y, z): """half-width of the hull at height z (analytic, without the hood strip); -1 outside its height""" E = env(y) if E <= 1e-6: return -1.0 W, Ht, Hb, zm = Wf(y) * E, HTf(y) * E, HBf(y) * E, ZMf(y) if z >= zm: if z > zm + Ht: return -1.0 n, u = NTf(y), (z - zm) / Ht else: if z < zm - Hb: return -1.0 n, u = NBf(y), (zm - z) / Hb return W * max(0.0, 1 - u ** n) ** (1.0 / n) def inside_depth(p): """how far a point lies inside the hull skin (true distance to the surface; negative outside)""" p = Vector(p) hw = hull_hw(p.y, p.z) q, n = SURF.near(p) d = (q - p).length return d if (hw > 0 and abs(p.x) < hw) else -d QB, QT = 30, 40 # quads per bottom / top quarter of a section NR = 2 * (QB + QT) # 140 around def half_ring(y): out = [] for (t0, t1, q, last) in ((-math.pi / 2, 0.0, QB, False), (0.0, math.pi / 2, QT, True)): S = 360 d = [sec_xz(y, t0 + (t1 - t0) * j / S) for j in range(S + 1)] acc = [0.0] for j in range(S): acc.append(acc[-1] + math.hypot(d[j + 1][0] - d[j][0], d[j + 1][1] - d[j][1])) L = acc[-1] jj = 0 for i in range(q + (1 if last else 0)): tg = L * i / q while jj < S - 1 and acc[jj + 1] < tg: jj += 1 seg = acc[jj + 1] - acc[jj] f = 0.0 if seg < 1e-12 else (tg - acc[jj]) / seg out.append((d[jj][0] + f * (d[jj + 1][0] - d[jj][0]), d[jj][1] + f * (d[jj + 1][1] - d[jj][1]))) return out def stations(): ys = [] JN, JT = 36, 14 for j in range(1, JN + 1): ys.append(Y_A - NOSE_L * math.cos(j * math.pi / 2 / JN)) y = Y_A while True: step = 0.05 if y < -0.70 else (0.10 if y > -0.30 else 0.05 + 0.05 * (y + 0.70) / 0.40) y += step if y >= Y_B - 0.04: break ys.append(y) for j in range(JT, 0, -1): ys.append(Y_B + TAIL_L * math.cos(j * math.pi / 2 / JT)) return ys YS = stations() RINGS, HUV = [], [] for y in YS: P = half_ring(y) ring = [] for k in range(NR): if k <= QB + QT: x, z = P[k] else: x, z = P[NR - k] x = -x if k in (0, QB + QT): x = 0.0 ring.append(Vector((x, y, z))) RINGS.append(ring) acc, uvr = 0.0, [] for k in range(NR): if k: acc += (ring[k] - ring[k - 1]).length uvr.append((acc, y)) HUV.append(uvr) TIP_N = Vector((0.0, Y_NOSE, ZMf(Y_NOSE))) TIP_T = Vector((0.0, Y_TAIL, ZMf(Y_TAIL))) ZTOP = lambda y: ZMf(y) + HTf(y) * env(y) + ridge(0.0, y) full = MB("_hull_full", []) grid_rings(full, RINGS, PAINT, cap0=TIP_N, cap1=TIP_T, expect=(len(YS) // 2, QB, (1, 0, 0))) HULL_FULL = full.build(C_TMP, weld=0.0) assert len(HULL_FULL.data.vertices) == len(YS) * NR + 2, len(HULL_FULL.data.vertices) SURF = Surface(HULL_FULL.data) HN = [[HULL_FULL.data.vertices[i * NR + k].normal.copy() for k in range(NR)] for i in range(len(YS))] REPORT["hull_stations"] = len(YS) def hull_z_at(x, y): q, n = SURF.ray((x, y, 5.0), (0, 0, -1)) return q.z def hull_z_below(x, y): q, n = SURF.ray((x, y, -5.0), (0, 0, 1)) return q.z # ================================================================== 2. openings: nose window and canopy D_LINER = 0.07 # cabin lining inside the hull skin DECK_Z = -0.58 Y_CAB = -0.85 # cabin bulkhead Y_FASCIA = -3.86 SHRINK = 0.030 # hull faces with a vertex this far inside an outline are removed (the cut edge stays under the frame) C_NOSE = Vector((0.0, Y_A, ZMf(Y_A))) class Opening: def __init__(s, name, to_param, ray_of, c, r, n, M): s.name, s.to_param, s.ray_of, s.c, s.r, s.n = name, to_param, ray_of, c, r, n D = 2400 dense = [] for j in range(D): a, b = s.par(2 * math.pi * j / D) q, nq = SURF.ray(*ray_of(a, b)) dense.append(q) acc = [0.0] for j in range(D): acc.append(acc[-1] + (dense[(j + 1) % D] - dense[j]).length) L = acc[-1] s.length = L s.P, s.N = [], [] jj = 0 for i in range(M): tg = L * i / M while acc[jj + 1] < tg: jj += 1 f = (tg - acc[jj]) / (acc[jj + 1] - acc[jj]) p = dense[jj].lerp(dense[(jj + 1) % D], f) q, nq = SURF.near(p) s.P.append(q); s.N.append(nq) s.kd = kdtree.KDTree(D) for j, p in enumerate(dense): s.kd.insert(p, j) s.kd.balance() cen = sum(s.P, Vector()) / M T0 = s.P[1] - s.P[-1] B0 = s.N[0].cross(T0) v = cen - s.P[0] v -= s.N[0] * v.dot(s.N[0]) s.bsign = -1.0 if B0.dot(v) > 0 else 1.0 # B points away from the opening def par(s, phi): cp, sp = math.cos(phi), math.sin(phi) return (s.c[0] + s.r[0] * math.copysign(abs(cp) ** (2 / s.n), cp), s.c[1] + s.r[1] * math.copysign(abs(sp) ** (2 / s.n), sp)) def inside(s, v): p = s.to_param(v) if p is None: return False return abs((p[0] - s.c[0]) / s.r[0]) ** s.n + abs((p[1] - s.c[1]) / s.r[1]) ** s.n < 1.0 def sdist(s, v): d = s.kd.find(v)[2] return -d if s.inside(v) else d def frame(s, i): P, N = s.P[i], s.N[i] M = len(s.P) T = s.P[(i + 1) % M] - s.P[i - 1] T = (T - N * T.dot(N)).normalized() return P, N, N.cross(T) * s.bsign def rows(s, prof, step=1): """rings of points for a profile [(o, h)]: o along B (outward, metres), h along the normal there""" out = [[] for _ in prof] cache = {} for i in range(0, len(s.P), step): P, N, B = s.frame(i) for r, (o, h) in enumerate(prof): if o not in cache: cache[o] = {} if i not in cache[o]: cache[o][i] = SURF.near(P + B * o) q, nq = cache[o][i] out[r].append(q + nq * h) return out def nose_param(v): dy = v.y - Y_A return (math.atan2(v.x, -dy), math.atan2(v.z - C_NOSE.z, math.hypot(v.x, dy))) def nose_ray(a, b): return C_NOSE, Vector((math.sin(a) * math.cos(b), -math.cos(a) * math.cos(b), math.sin(b))) CAN_C, CAN_R = (0.0, -1.975), (0.62, 0.875) WIN = Opening("nose window", nose_param, nose_ray, (0.0, math.radians(-12.0)), (math.radians(56.0), math.radians(22.0)), 4.0, 200) CAN = Opening("canopy", lambda v: (v.x, v.y) if v.z > 0.4 else None, lambda a, b: ((a, b, 5.0), (0, 0, -1)), CAN_C, CAN_R, 2.8, 180) REPORT["openings"] = {"nose_window_outline_m": round(WIN.length, 3), "canopy_outline_m": round(CAN.length, 3)} SD = [[min(WIN.sdist(RINGS[i][k]), CAN.sdist(RINGS[i][k])) for k in range(NR)] for i in range(len(YS))] SD_TIP_N = min(WIN.sdist(TIP_N), CAN.sdist(TIP_N)) def deep(i, k): return SD[i][k] < -SHRINK # ================================================================== 3. the hull (cut) and its frames hull = MB("SM_Remora_Hull", [PAINT]) def hull_skip(i, k): k2 = (k + 1) % NR if i == -1: return SD_TIP_N < -SHRINK or deep(0, k) or deep(0, k2) if i == len(YS) - 1: return False return deep(i, k) or deep(i, k2) or deep(i + 1, k) or deep(i + 1, k2) # panel seams of the skin, as signed distance fields (metres, stored + 5.0 so that 0 means "no seam" on other parts); the bake # turns them into grooves in the normal map and into the crevice mask SEAM_LONG = [(QB, -3.25, 3.70), (NR - QB, -3.25, 3.70), (QB + 18, -0.95, 3.55), (NR - QB - 18, -0.95, 3.55), (10, -2.90, 2.90), (NR - 10, -2.90, 2.90)] SEAM_RING = [-3.30, -0.95, 0.95, 2.55, 3.62] PANEL = (0.45, 0.85, -0.55, 0.60) # service panels above each waist: arc from the waist, y range def seam_fields(): fl, fr, fp = [], [], [] for i, y in enumerate(YS): a = [HUV[i][k][0] for k in range(NR)] L = a[-1] + (RINGS[i][0] - RINGS[i][-1]).length rl, rr, rp = [], [], [] for k in range(NR): best = 1.0 for (kl, y0, y1) in SEAM_LONG: if y0 <= y <= y1: d = a[k] - a[kl] if d > L / 2: d -= L if d < -L / 2: d += L if abs(d) < abs(best): best = d rl.append(best + 5.0) rr.append(min((y - t for t in SEAM_RING), key=abs) + 5.0) aw = (a[k] - a[QB]) if k <= NR // 2 else (a[NR - QB] - a[k]) ca, ha = (PANEL[0] + PANEL[1]) / 2, (PANEL[1] - PANEL[0]) / 2 cy, hy = (PANEL[2] + PANEL[3]) / 2, (PANEL[3] - PANEL[2]) / 2 rp.append(max(abs(aw - ca) - ha, abs(y - cy) - hy) + 5.0) fl.append(rl); fr.append(rr); fp.append(rp) return {"seam_long": fl, "seam_ring": fr, "seam_rect": fp} grid_rings(hull, RINGS, PAINT, cap0=TIP_N, cap1=TIP_T, uv=HUV, skip=hull_skip, expect=(len(YS) // 2, QB, (1, 0, 0)), vattr=seam_fields()) # check: every cut edge of the skin lies under its frame (signed distance from the outline within the frame's band) cover = {"nose window": [0, 1e9, -1e9], "canopy": [0, 1e9, -1e9]} bad = [] for i in range(len(YS)): for k in range(NR): adj = [(i - 1, k - 1), (i - 1, k), (i, k - 1), (i, k)] st = [hull_skip(a, b % NR) for a, b in adj if 0 <= a < len(YS) - 1] if not st or all(st) or not any(st): continue dw, dc = WIN.sdist(RINGS[i][k]), CAN.sdist(RINGS[i][k]) nm, d, lo, hi = ("nose window", dw, -0.035, 0.040) if abs(dw) < abs(dc) else ("canopy", dc, -0.050, 0.045) c = cover[nm] c[0] += 1; c[1] = min(c[1], d); c[2] = max(c[2], d) if not (lo + 0.002 <= d <= hi - 0.002): bad.append((nm, round(d, 4), i, k)) REPORT["cut_edges_under_frames"] = {k: {"vertices": v[0], "signed_dist_min": round(v[1], 4), "signed_dist_max": round(v[2], 4)} for k, v in cover.items()} REPORT["cut_edges_outside_frame_band"] = bad[:20] REPORT["cut_edges_outside_frame_band_count"] = len(bad) # nose window: frame (graphite with an amber strip), lip down past the cabin lining, glass 20 mm thick W_IN, W_OUT, DG1, DG2, D_LIP = 0.035, 0.040, 0.012, 0.024, D_LINER + 0.02 WA = [(W_OUT, -0.004), (0.033, 0.005), (0.024, 0.009), (-0.012, 0.009), (-0.026, 0.009), (-0.032, 0.006), (-W_IN, 0.0), (-W_IN, -DG1)] WB = [(-W_IN, -DG2), (-W_IN, -D_LIP), (-W_IN + 0.004, -D_LIP), (-W_IN + 0.004, -0.015)] frm = MB("_frames", [GRAPH]) ra = WIN.rows(WA) rb = WIN.rows(WB) grid_rings(frm, ra, [GRAPH, GRAPH, GRAPH, AMBER, GRAPH, GRAPH, GRAPH], expect=(2, 0, WIN.N[0])) grid_rings(frm, rb, GRAPH, expect=(0, 0, -WIN.frame(0)[2])) def glass_rings(op, ring0, depth, J=8): """polar grid from a boundary ring (on the lip) to the opening's centre, at a depth under the hull skin""" cen_par = op.c bpar = [op.to_param(SURF.near(p)[0]) for p in ring0] rings = [list(ring0)] for j in range(1, J): f = 1.0 - j / J f = math.sin(f * math.pi / 2) # closer rings near the frame r = [] for (a, b) in bpar: pa, pb = cen_par[0] + f * (a - cen_par[0]), cen_par[1] + f * (b - cen_par[1]) q, nq = SURF.ray(*op.ray_of(pa, pb)) r.append(q - nq * depth) rings.append(r) q, nq = SURF.ray(*op.ray_of(*cen_par)) return rings, q - nq * depth g_out, c_out = glass_rings(WIN, ra[-1], DG1) g_in, c_in = glass_rings(WIN, rb[0], DG2) mid = len(g_out) // 2 grid_rings(frm, g_out, GLASS, cap1=c_out, expect=(mid, 0, WIN.N[0])) grid_rings(frm, g_in, GLASS, cap1=c_in, expect=(mid, 0, -WIN.N[0])) grid_rings(frm, [ra[-1], rb[0]], GLASS, expect=(0, 0, WIN.frame(0)[2])) # the glass's edge, against the lip # canopy coaming: a raised sill with a rubber seal on top, lip down into the cabin C_IN, C_OUT = 0.050, 0.045 CA = [(C_OUT, -0.005), (0.036, 0.012), (0.026, 0.024), (0.012, 0.030), (-0.030, 0.030), (-0.042, 0.026), (-C_IN, 0.014), (-C_IN, -D_LIP), (-C_IN + 0.005, -D_LIP), (-C_IN + 0.005, -0.015)] rc = CAN.rows(CA) grid_rings(frm, rc, [GRAPH, GRAPH, GRAPH, RUBBER, GRAPH, GRAPH, GRAPH, GRAPH, GRAPH], expect=(3, 0, CAN.N[0])) COAM_TOP = 0.030 # ================================================================== 4. cabin: lining, bulkhead, deck, furniture, lights cab = MB("SM_Remora_Cabin", [CAB]) I_CAB = max(i for i, y in enumerate(YS) if y <= Y_CAB) LIN = [[RINGS[i][k] - HN[i][k] * D_LINER for k in range(NR)] for i in range(I_CAB + 1)] LUV = [[(HUV[i][k][0], HUV[i][k][1]) for k in range(NR)] for i in range(I_CAB + 1)] BULK_C = Vector((0.0, YS[I_CAB], 0.0)) def liner_skip(i, k): k2 = (k + 1) % NR if i == -1: return True if i == I_CAB: # bulkhead fan a, b = LIN[i][k], LIN[i][k2] return a.z < DECK_Z - 0.03 and b.z < DECK_Z - 0.03 ids = [(i, k), (i, k2), (i + 1, k), (i + 1, k2)] if any(deep(a, b) for a, b in ids): return True if YS[i] > Y_FASCIA and all(LIN[a][b].z < DECK_Z - 0.03 for a, b in ids): return True return False I_PROBE = min(range(I_CAB), key=lambda i: abs(YS[i] + 2.0)) grid_rings(cab, LIN, CAB, cap1=BULK_C, uv=LUV, skip=liner_skip, expect=(I_PROBE, QB, (-1, 0, 0))) def ring_x_at_z(ring, z, side=1): """x of the port (side 1) or starboard ring point at height z, along the bottom quarter and the top quarter""" best = None for k in range(QB + QT): a, b = ring[k], ring[k + 1] if (a.z - z) * (b.z - z) <= 0 and abs(b.z - a.z) > 1e-9: f = (z - a.z) / (b.z - a.z) x = a.x + f * (b.x - a.x) best = x if best is None else max(best, x) return best # deck: flat plate from the fascia to the bulkhead, its edges tucked 1 cm into the lining deck_rows, deck_uv = [], [] first = None for i in range(I_CAB + 1): if YS[i] < Y_FASCIA: continue w = ring_x_at_z(LIN[i], DECK_Z) if w is None: continue if first is None: first = i row = [Vector((-w - 0.01 + (2 * w + 0.02) * j / 12, YS[i], DECK_Z)) for j in range(13)] deck_rows.append(row) deck_uv.append([(p.x, p.y) for p in row]) grid_rings(cab, deck_rows, FLOOR, closed_u=False, uv=deck_uv, expect=(1, 6, (0, 0, 1))) fy = deck_rows[0][0].y fw = -deck_rows[0][0].x fascia = [] for t in range(8): zf = DECK_Z - 0.035 * t wf = ring_x_at_z(LIN[first], zf) if wf is None or wf < 0.12: break fascia.append([Vector((-(wf + 0.01) + 2 * (wf + 0.01) * j / 12, fy, zf)) for j in range(13)]) grid_rings(cab, fascia, CAB, closed_u=False, expect=(0, 6, (0, -1, 0))) REPORT["deck"] = {"from_y": round(fy, 3), "to_y": round(YS[I_CAB], 3), "half_width_at_front": round(fw, 3)} # pilot seat on a pedestal under the canopy, sticks on the armrests SEAT_Y = -1.78 cab.box((0.34, 0.38, 0.64), (0, SEAT_Y + 0.02, DECK_Z + 0.32), GRAPH, bevel=0.02) # pedestal cab.box((0.54, 0.52, 0.10), (0, SEAT_Y, 0.10), SEAT, bevel=0.035, seg=2) # cushion cab.box((0.52, 0.11, 0.64), (0, SEAT_Y + 0.29, 0.48), SEAT, rot=(math.radians(-12), 0, 0), bevel=0.035, seg=2) # backrest cab.box((0.30, 0.09, 0.17), (0, SEAT_Y + 0.37, 0.88), SEAT, rot=(math.radians(-12), 0, 0), bevel=0.03, seg=2) # headrest for s in (1, -1): cab.box((0.08, 0.40, 0.07), (s * 0.31, SEAT_Y - 0.02, 0.30), GRAPH, bevel=0.015) # armrest cab.cyl(0.012, 0.012, 0.12, (s * 0.31, SEAT_Y - 0.15, 0.39), CHROME, segs=12) # stick cab.sphere(0.028, (s * 0.31, SEAT_Y - 0.15, 0.46), GRAPH, segs=12, rings=8) # console under the canopy's front: sloped top with three screens facing the pilot CONS = [(-2.88, 0.05), (-2.88, 0.58), (-2.80, 0.66), (-2.42, 0.50), (-2.38, 0.44), (-2.38, 0.20), (-2.55, 0.05)] cab.prism([(y, z) for y, z in CONS], 1.10, (0.55, 0, 0), GRAPH, rot=(0, 0, math.pi / 2), bevel=0.008) sl = Vector((0, 0.38, -0.16)).normalized() sn = Vector((0, 0.16, 0.38)).normalized() ang = math.atan2(sl.z, sl.y) for sx in (-0.36, 0.0, 0.36): c = Vector((sx, -2.61, 0.58)) + sn * 0.004 cab.box((0.30, 0.24, 0.006), c, SCREEN, rot=(ang, 0, 0)) cab.box((1.0, 0.18, 0.40), (0, -2.47, -0.18), GRAPH, bevel=0.01) # console foot # observation bench in the nose, under the window cab.box((1.04, 0.36, 0.20), (0, -3.52, DECK_Z + 0.10), GRAPH, bevel=0.015) cab.box((1.00, 0.34, 0.07), (0, -3.52, DECK_Z + 0.235), SEAT, bevel=0.025, seg=2) # bulkhead door (aft, to the hatch trunk) cab.box((0.70, 0.03, 1.20), (0, YS[I_CAB] - 0.03, 0.02), GRAPH, bevel=0.02) cab.box((0.58, 0.02, 1.08), (0, YS[I_CAB] - 0.05, 0.02), CAB, bevel=0.01) cab.box((0.40, 0.012, 0.02), (0, YS[I_CAB] - 0.062, 0.50), CABGLOW) # light strips along both walls and along the nose ceiling for side in (1, -1): pts = [] for i in range(I_CAB + 1): if -3.30 <= YS[i] <= -1.05 and i % 2 == 0: x = ring_x_at_z(LIN[i], 0.30) if x is not None: pts.append((side * (x - 0.014), YS[i], 0.30)) cab.sweep(pts, 0.009, CABGLOW, sides=8) for side in (1, -1): pts = [(side * (ring_x_at_z(LIN[i], DECK_Z + 0.03) - 0.02), YS[i], DECK_Z + 0.012) for i in range(first, I_CAB + 1, 3) if ring_x_at_z(LIN[i], DECK_Z + 0.03) is not None and YS[i] <= -1.0] cab.sweep(pts, 0.008, CABGLOW, sides=6) top = [(0.0, LIN[i][QB + QT].y, LIN[i][QB + QT].z - 0.014) for i in range(I_CAB + 1) if -3.90 <= YS[i] <= -3.02 and i % 2 == 0] cab.sweep(top, 0.010, CABGLOW, sides=8) # ================================================================== 5. hatch collar (the docking clamp's interface) and its hinge col = MB("_collar", []) # The clamp's axis is NOT the Tender origin: on the Kazah the collar sits 9 cm ahead of it (0, -0.09), and the ship file, the arm # (build_dockarm.py COLLAR_X 0.09) and the Unreal blockout all hang the Tender by that. The collar is built round its own axis # and moved there, so the Remora drops into every placement made for the Kazah. COLLAR_C = Vector((0.0, -0.09, 0.0)) MC = 64 skirt = [] SK_R0, SK_R1, SK_Z1 = 0.88, 0.56, 1.12 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(COLLAR_C.x + SK_R0 * math.cos(ph), COLLAR_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) COLLAR_UP = [(0.560, 1.120), (0.540, 1.150), (0.535, 1.180), (0.535, 1.335), (0.548, 1.350), (0.552, 1.365), (0.545, 1.375), (0.470, 1.375), (0.435, 1.372), (0.422, 1.362), (0.422, 1.322), (0.395, 1.300), (0.385, 1.290), (0.385, 1.200), (0.375, 1.190)] up = [[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 COLLAR_UP] grid_rings(col, skirt + up[1:], [PAINT] * 6 + [PAINT, PAINT, GRAPH, GRAPH, GRAPH, GRAPH, ORANGE, GRAPH, GRAPH, GRAPH, GRAPH, GRAPH, GRAPH, GRAPH], cap1=Vector((0, 0, 1.190)), expect=(8, 0, (1, 0, 0))) HL = Vector((0.0, 0.43, 1.395)) # lid hinge in the collar's own frame for s in (1, -1): col.box((0.05, 0.05, 0.05), (s * 0.085, HL.y, 1.392), GRAPH, bevel=0.006) col.cyl(0.006, 0.006, 0.23, (0, HL.y, HL.z), STEEL, rot=(0, math.pi / 2, 0), segs=10) col.transform(Matrix.Translation(COLLAR_C)) HINGE_LID = COLLAR_C + HL REPORT["collar"] = {"centre": [COLLAR_C.x, COLLAR_C.y], "wall_r": 0.535, "flange_r": 0.552, "flange_top_z": 1.375, "skirt_base_r": SK_R0} # ================================================================== 6. attached parts (pruned where they sink into the hull) att = MB("_attached", []) # lamp pods at the front corners: a chrome bezel and a lens over a reflector dish POD_X, POD_Z = 1.13, -0.52 POD = [(0.0, -3.74), (0.040, -3.74), (0.150, -3.80), (0.158, -3.815), (0.182, -3.850), (0.205, -3.855), (0.222, -3.835), (0.232, -3.80), (0.262, -3.70), (0.284, -3.55), (0.292, -3.35), (0.280, -3.08), (0.240, -2.80), (0.165, -2.55), (0.070, -2.40), (0.0, -2.36)] POD_M = [LAMP, CHROME, CHROME, CHROME, CHROME, CHROME, CHROME] + [PAINT] * 8 POD_F, POD_FM = resample_profile(POD, POD_M, 0.035) LENS_P = [(0.0, -3.845), (0.080, -3.838), (0.150, -3.818), (0.152, -3.805), (0.0, -3.805)] for s in (1, -1): lathe(att, POD_F, POD_FM, 48, T(s * POD_X, 0, POD_Z) @ YPOS) lathe(att, LENS_P, LENS, 40, T(s * POD_X, 0, POD_Z) @ YPOS) # thruster nacelles at the rear flanks: shroud, bore, motor hub; the rotor is a moving part NAC_X, NAC_Z, ROTOR_Y = 1.49, -0.02, 1.85 NAC = [(0.0, -0.42), (0.10, -0.40), (0.20, -0.34), (0.29, -0.23), (0.36, -0.09), (0.41, 0.10), (0.420, 0.18), (0.428, 0.26), (0.435, 0.35), (0.44, 0.60), (0.44, 1.70), (0.432, 2.02), (0.418, 2.26), (0.405, 2.34), (0.388, 2.37), (0.372, 2.355), (0.370, 2.30), (0.370, 1.52), (0.362, 1.49), (0.150, 1.49), (0.130, 1.51), (0.130, 1.74), (0.095, 1.75), (0.095, 1.95), (0.130, 1.96), (0.130, 2.05), (0.105, 2.20), (0.055, 2.31), (0.0, 2.35)] NAC_M = [PAINT] * 6 + [ORANGE] + [PAINT] * 5 + [GRAPH] * 8 + [STEEL] * 5 + [GRAPH] * 3 for s in (1, -1): lathe(att, NAC, NAC_M, 48, T(s * NAC_X, 0, NAC_Z) @ YPOS) foil(att, [(Vector((s * 0.88, 1.70, NAC_Z)), Vector((0, 1, 0)), Vector((0, 0, 1)), 1.10, 0.070), (Vector((s * 1.09, 1.68, NAC_Z)), Vector((0, 1, 0)), Vector((0, 0, 1)), 1.00, 0.060)], PAINT) # web, ends 3 cm short of the bore for a in (45, 135, 225, 315): # stator vanes behind the rotor er = Vector((math.cos(math.radians(a)), 0, math.sin(math.radians(a)))) et = Vector((-math.sin(math.radians(a)), 0, math.cos(math.radians(a)))) c0 = Vector((s * NAC_X, 2.13, NAC_Z)) foil(att, [(c0 + er * 0.09, Vector((0, 1, 0)), et, 0.14, 0.014), (c0 + er * 0.378, Vector((0, 1, 0)), et, 0.14, 0.012)], GRAPH) # fins: a dorsal fin and two ventral fins at the stern yr = 3.45 foil(att, [(Vector((0, yr, ZTOP(yr) - 0.04)), Vector((0, 1, 0)), Vector((1, 0, 0)), 1.00, 0.050), (Vector((0, yr + 0.20, ZTOP(yr) + 0.22)), Vector((0, 1, 0)), Vector((1, 0, 0)), 0.70, 0.034), (Vector((0, yr + 0.38, ZTOP(yr) + 0.40)), Vector((0, 1, 0)), Vector((1, 0, 0)), 0.38, 0.020)], PAINT) for s in (1, -1): d = Vector((s * math.sin(math.radians(40)), 0, -math.cos(math.radians(40)))) q, nq = SURF.ray((0, yr, ZMf(yr)), d) nd = Vector((math.cos(math.radians(40)), 0, s * math.sin(math.radians(40)))).normalized() foil(att, [(q - d * 0.04, Vector((0, 1, 0)), nd, 0.90, 0.045), (q + d * 0.20 + Vector((0, 0.16, 0)), Vector((0, 1, 0)), nd, 0.60, 0.030), (q + d * 0.36 + Vector((0, 0.30, 0)), Vector((0, 1, 0)), nd, 0.34, 0.018)], PAINT) # skids under the belly: graphite runners on streamlined struts, rubber wear strips SKID_X = 0.62 RAIL = [(-2.70, 0.030, -0.99, -0.94, 0.012), (-2.55, 0.035, -1.07, -1.01, 0.015), (-2.35, 0.035, -1.12, -1.05, 0.015), (1.95, 0.035, -1.12, -1.05, 0.015), (2.15, 0.035, -1.08, -1.01, 0.015), (2.30, 0.030, -1.00, -0.95, 0.012)] for s in (1, -1): loft(att, RAIL, GRAPH, T(s * SKID_X, 0, 0) @ Matrix.Rotation(math.pi / 2, 4, 'Z')) att.box((0.05, 4.30, 0.016), (s * SKID_X, -0.20, -1.128), RUBBER, bevel=0.004) for ys_ in (-1.9, -0.1, 1.6): zt = hull_z_below(s * SKID_X, ys_) + 0.05 foil(att, [(Vector((s * SKID_X, ys_, -1.07)), Vector((0, 1, 0)), Vector((1, 0, 0)), 0.22, 0.05), (Vector((s * SKID_X, ys_ + 0.03, zt)), Vector((0, 1, 0)), Vector((1, 0, 0)), 0.30, 0.06)], GRAPH) # sensor masts behind the collar MAST_Y, MAST_X = 1.13, 0.30 for s in (1, -1): zh = hull_z_at(s * MAST_X, MAST_Y) lathe(att, [(0.0, zh - 0.03), (0.070, zh - 0.03), (0.070, zh + 0.025), (0.060, zh + 0.040), (0.030, zh + 0.045), (0.028, 1.56), (0.040, 1.565), (0.040, 1.585), (0.0, 1.585)], [GRAPH, GRAPH, GRAPH, GRAPH, CHROME, CHROME, CHROME, CHROME], 24, T(s * MAST_X, MAST_Y, 0)) att.sphere(0.085, (s * MAST_X, MAST_Y, 1.655), SENSOR, segs=24, rings=14) # grab rails beside the canopy for s in (1, -1): pts = [] for yy, h in ((-2.55, -0.03), (-2.53, 0.06), (-2.45, 0.09), (-1.55, 0.09), (-1.47, 0.06), (-1.45, -0.03)): pts.append((s * 0.86, yy, hull_z_at(s * 0.86, yy) + h)) att.sweep(pts, 0.016, CHROME, sides=10) # louvred vents on the rear deck def surface_plate(mb, x0, x1, y0, y1, h_top, h_bot, mat, nx=6, ny=6): """a slab lying on the hull: its top follows the skin at h_top, its sides go down to h_bot (under the skin)""" def at(x, y, h): q, n = SURF.ray((x, y, 5.0), (0, 0, -1)) return q + n * h bm = bmesh.new() G = [[bm.verts.new(at(x0 + (x1 - x0) * i / nx, y0 + (y1 - y0) * j / ny, h_top)) for j in range(ny + 1)] for i in range(nx + 1)] for i in range(nx): for j in range(ny): bm.faces.new((G[i][j], G[i + 1][j], G[i + 1][j + 1], G[i][j + 1])) loop = [(i, 0) for i in range(nx)] + [(nx, j) for j in range(ny)] + [(i, ny) for i in range(nx, 0, -1)] + [(0, j) for j in range(ny, 0, -1)] low = {} for (i, j) in loop: low[(i, j)] = bm.verts.new(at(x0 + (x1 - x0) * i / nx, y0 + (y1 - y0) * j / ny, h_bot)) for a in range(len(loop)): p, q = loop[a], loop[(a + 1) % len(loop)] bm.faces.new((G[p[0]][p[1]], low[p], low[q], G[q[0]][q[1]])) bm.normal_update() bmesh.ops.recalc_face_normals(bm, faces=bm.faces[:]) mb.merge(bm, mat) for s in (1, -1): # louvred vents on the rear deck vx0, vx1, vy0, vy1 = s * 0.34, s * 0.58, 1.70, 2.30 surface_plate(att, min(vx0, vx1), max(vx0, vx1), vy0, vy1, 0.012, -0.02, GRAPH, 6, 8) for j in range(8): yy = vy0 + 0.06 + j * (vy1 - vy0 - 0.12) / 7 surface_plate(att, min(vx0, vx1) + 0.03, max(vx0, vx1) - 0.03, yy - 0.012, yy + 0.012, 0.026, 0.005, STEEL, 4, 1) # stern light and tow eye lathe(att, [(0.0, Y_TAIL + 0.035), (0.035, Y_TAIL + 0.025), (0.052, Y_TAIL - 0.005), (0.052, Y_TAIL - 0.06), (0.0, Y_TAIL - 0.06)], [AMBER, AMBER, CHROME, CHROME], 20, T(0, 0, TIP_T.z) @ YPOS) zb = hull_z_below(0, 3.70) att.box((0.04, 0.16, 0.10), (0, 3.70, zb - 0.02), STEEL, bevel=0.008) att.torus(0.055, 0.014, (0, 3.70, zb - 0.10), STEEL, rot=(0, math.pi / 2, 0), segs=24, rsegs=8) # tool unit under the starboard pod: housing and the suction cowl round the drill (the drill bit is a moving part) TOOL = Vector((-POD_X, -3.64, -0.88)) loft(att, [(-3.62, 0.11, -0.99, -0.77, 0.04), (-3.50, 0.13, -1.02, -0.66, 0.05), (-3.00, 0.13, -1.02, -0.66, 0.05), (-2.78, 0.08, -0.93, -0.68, 0.04)], GRAPH, T(TOOL.x, 0, 0) @ Matrix.Rotation(math.pi / 2, 4, 'Z')) revolve_loop(att, [(0.082, 0.03), (0.130, 0.03), (0.140, -0.01), (0.125, -0.045), (0.088, -0.035)], [GRAPH, GRAPH, RUBBER, RUBBER, GRAPH], 32, T(*TOOL) @ YPOS) # canopy hinge blocks behind the coaming i_rear = max(range(len(CAN.P)), key=lambda i: CAN.P[i].y) P_REAR = CAN.P[i_rear] HINGE_CAN = Vector((0.0, P_REAR.y + 0.080, P_REAR.z + 0.050)) for s in (1, -1): for dx in (-0.075, 0.075): x = s * 0.30 + dx zb = hull_z_at(x, HINGE_CAN.y) att.box((0.05, 0.07, HINGE_CAN.z + 0.022 - zb + 0.02), (x, HINGE_CAN.y, (HINGE_CAN.z + 0.022 + zb - 0.02) / 2), GRAPH, bevel=0.008) att.cyl(0.008, 0.008, 0.20, (s * 0.30, HINGE_CAN.y, HINGE_CAN.z), STEEL, rot=(0, math.pi / 2, 0), segs=10) pruned = prune_inside(att, inside_depth, 0.015) REPORT["attached_faces_pruned_inside_hull"] = pruned dv = sorted(((inside_depth(v.co), tuple(round(c, 3) for c in v.co)) for v in att.bm.verts if v.co.y < Y_CAB + 0.02), reverse=True) deepest = dv[0][0] if dv else -1 REPORT["attached_parts_deepest_into_hull_in_cabin_zone_m"] = round(deepest, 4) assert deepest < D_LINER - 0.005, "an attached part reaches through the cabin lining: %s" % (dv[:6],) # interference among the static pieces (the lips are meant to reach 2 cm through the cabin lining; nothing else may touch) def pairs_report(a, b): pr = overlaps(a.bm, b.bm) b.bm.faces.ensure_lookup_table(); a.bm.faces.ensure_lookup_table() return pr REPORT["overlap_attached_vs_collar"] = len(pairs_report(att, col)) pa = pairs_report(att, frm) REPORT["overlap_attached_vs_frames_glass"] = len(pa) pc = pairs_report(att, cab) REPORT["overlap_attached_vs_cabin"] = len(pc) pl = pairs_report(frm, cab) lipdeep = 0 for fi, ci in pl: c = frm.bm.faces[fi].calc_center_median() if inside_depth(c) > 0.05: lipdeep += 1 REPORT["overlap_frames_vs_cabin"] = {"pairs": len(pl), "of_which_lip_bottoms_through_the_lining": lipdeep} if pa or pc: REPORT["overlap_examples"] = [tuple(round(x, 3) for x in att.bm.faces[i].calc_center_median()) for i, j in (pa + pc)[:12]] # ================================================================== 7. moving parts # canopy: frame with an amber edge light, a 12 mm bubble, a small orange-ringed sensor dome on top can = MB("SM_Remora_Canopy", []) RIM = [(0.030, 0.035), (0.030, 0.039), (0.030, 0.047), (0.030, 0.052), (0.024, 0.058), (-0.020, 0.058), (-0.040, 0.050), (-0.040, 0.035)] rr = CAN.rows(RIM, 2) grid_rings(can, rr + [rr[0]], [GRAPH, AMBER, GRAPH, GRAPH, GRAPH, GRAPH, GRAPH, GRAPH], expect=(4, 0, CAN.N[0])) Z_APEX = 1.53 bo = CAN.rows([(-0.025, 0.055)], 2)[0] bi = CAN.rows([(-0.037, 0.045)], 2)[0] def dome(base, apex_z, S=14): rings = [base] cen = Vector((CAN_C[0], CAN_C[1], 0)) for j in range(1, S): rho = math.cos(j / S * math.pi / 2) r = [] for p in base: xy = cen + (Vector((p.x, p.y, 0)) - cen) * rho z = p.z + (apex_z - p.z) * max(0.0, 1 - rho ** 2.4) ** 0.5 r.append(Vector((xy.x, xy.y, z))) rings.append(r) return rings, Vector((CAN_C[0], CAN_C[1], apex_z)) do, apo = dome(bo, Z_APEX) di, api = dome(bi, Z_APEX - 0.012) grid_rings(can, do, GLASS, cap1=apo, expect=(6, 0, (0, 0, 1))) grid_rings(can, di, GLASS, cap1=api, expect=(6, 0, (0, 0, -1))) grid_rings(can, [bo, bi], GLASS, expect=(0, 0, (0, 0, -1))) revolve_loop(can, [(0.105, -0.008), (0.150, -0.012), (0.152, 0.010), (0.140, 0.022), (0.110, 0.024)], ORANGE, 40, T(CAN_C[0], CAN_C[1], Z_APEX)) lathe(can, [(0.0, 0.085), (0.06, 0.07), (0.098, 0.035), (0.110, 0.012), (0.110, -0.004), (0.0, -0.004)], SENSOR, 32, T(CAN_C[0], CAN_C[1], Z_APEX)) for s in (1, -1): # hinge knuckles (tubes round the fixed pins) and their tabs revolve_loop(can, [(0.011, -0.04), (0.024, -0.04), (0.024, 0.04), (0.011, 0.04)], GRAPH, 16, T(s * 0.30, HINGE_CAN.y, HINGE_CAN.z) @ Matrix.Rotation(math.pi / 2, 4, 'Y')) can.box((0.07, 0.07, 0.016), (s * 0.30, HINGE_CAN.y - 0.050, HINGE_CAN.z - 0.004), GRAPH, bevel=0.004) # hatch lid in the collar: plate, handwheel, strap up to its hinge knuckle lid = MB("SM_Remora_HatchLid", []) lathe(lid, [(0.0, 1.318), (0.41, 1.318), (0.41, 1.350), (0.402, 1.358), (0.0, 1.358)], [GRAPH, GRAPH, GRAPH, PAINT], 48) lid.torus(0.12, 0.011, (0, 0, 1.375), CHROME, segs=40, rsegs=8) lathe(lid, [(0.0, 1.356), (0.032, 1.356), (0.032, 1.385), (0.0, 1.388)], CHROME, 16) for a in range(4): lid.box((0.10, 0.014, 0.010), (0.07 * math.cos(a * math.pi / 2), 0.07 * math.sin(a * math.pi / 2), 1.372), CHROME, rot=(0, 0, a * math.pi / 2)) lid.box((0.10, 0.06, 0.034), (0, 0.375, 1.373), GRAPH, bevel=0.004) lid.box((0.10, 0.033, 0.016), (0, 0.4015, 1.388), GRAPH, bevel=0.004) revolve_loop(lid, [(0.009, -0.05), (0.018, -0.05), (0.018, 0.05), (0.009, 0.05)], GRAPH, 16, T(*HL) @ Matrix.Rotation(math.pi / 2, 4, 'Y')) lid.transform(Matrix.Translation(COLLAR_C)) # rotor: six twisted blades on a hub ring (local axis = Y; one mesh, used on both sides) rot = MB("SM_Remora_Rotor", []) revolve_loop(rot, [(0.105, -0.065), (0.140, -0.065), (0.140, 0.065), (0.105, 0.065)], IMPEL, 32, YPOS) RB = [(0.132, 0.085, 42, 0.010), (0.20, 0.100, 36, 0.008), (0.27, 0.110, 30, 0.007), (0.33, 0.115, 26, 0.006), (0.352, 0.100, 25, 0.004)] for b in range(6): ps = 2 * math.pi * b / 6 er = Vector((math.cos(ps), math.sin(ps), 0)); et = Vector((-math.sin(ps), math.cos(ps), 0)); ea = Vector((0, 0, 1)) secs = [] for (r, ch, pit, th) in RB: bt = math.radians(pit) secs.append((er * r, et * math.cos(bt) + ea * math.sin(bt), -et * math.sin(bt) + ea * math.cos(bt), ch, th)) foil(rot, secs, IMPEL, YPOS) # drill bit with two flutes (local axis = Y, tip toward -Y) drl = MB("SM_Remora_Drill", []) lathe(drl, [(0.0, 0.0), (0.060, 0.0), (0.060, 0.085), (0.050, 0.095), (0.048, 0.10), (0.0, 0.42)], [STEEL, STEEL, STEEL, STEEL, STEEL], 24, Matrix.Rotation(math.pi / 2, 4, 'X')) for f in (0, 1): pts = [] for j in range(41): t = j / 40 a = 0.10 + 0.29 * t rad = 0.048 * (0.42 - a) / 0.32 + 0.006 ang = f * math.pi + t * 2 * math.pi * 2.5 pts.append((rad * math.cos(ang), -a, rad * math.sin(ang))) drl.sweep(pts, [0.012 * (1 - 0.6 * (j / 40)) for j in range(41)], STEEL, sides=8, caps=True) # ================================================================== 8. objects, pivots, sockets REPORT["build_seconds_geometry"] = round(time.time() - t_start, 1) for src in (col, att, frm): o = src.build(C_TMP) m2 = o.data tmp = bmesh.new(); tmp.from_mesh(m2) for f in tmp.faces: f.material_index = hull.mi(m2.materials[f.material_index]) hull.merge(tmp, None) HULL = hull.build(C_ROOT, parent=root, sharp_deg=40.0) CABIN = cab.build(C_ROOT, parent=root, sharp_deg=40.0) CANOPY = can.build(C_ROOT, parent=root, origin=HINGE_CAN, sharp_deg=40.0) LID = lid.build(C_ROOT, parent=root, origin=HINGE_LID, sharp_deg=40.0) ROTOR_L = rot.build(C_ROOT, parent=root, sharp_deg=40.0) ROTOR_L.name = "SM_Remora_Rotor" ROTOR_L.location = (NAC_X, ROTOR_Y, NAC_Z) ROTOR_R = bpy.data.objects.new("SM_Remora_Rotor.Stbd", ROTOR_L.data) C_ROOT.objects.link(ROTOR_R) ROTOR_R.location = (-NAC_X, ROTOR_Y, NAC_Z) ROTOR_R.parent = root ROTOR_L.name = "SM_Remora_Rotor.Port" DRILL = drl.build(C_ROOT, parent=root, sharp_deg=40.0) DRILL.location = TOOL for o in (HULL, CABIN, CANOPY, LID, ROTOR_L, ROTOR_R, DRILL): o["motion"] = "" CANOPY["motion"] = "rotate about local X: 0 closed .. -75 deg open (front lifts)" LID["motion"] = "rotate about local X: 0 closed .. -110 deg open (lifts toward the stern)" ROTOR_L["motion"] = ROTOR_R["motion"] = "spin about local Y" DRILL["motion"] = "spin about local Y" for nm, loc in (("SOCKET_Collar", (COLLAR_C.x, COLLAR_C.y, 1.375)), ("SOCKET_PilotEye", (0, SEAT_Y + 0.05, 1.02)), ("SOCKET_ToolTip", (TOOL.x, TOOL.y - 0.42, TOOL.z)), ("SOCKET_Lamp_Port", (POD_X, -3.86, POD_Z)), ("SOCKET_Lamp_Stbd", (-POD_X, -3.86, POD_Z)), ("SOCKET_Thrust_Port", (NAC_X, 2.37, NAC_Z)), ("SOCKET_Thrust_Stbd", (-NAC_X, 2.37, NAC_Z))): e = bpy.data.objects.new(nm, None) C_SOCK.objects.link(e) e.location = loc e.parent = root e.empty_display_size = 0.15 for o in list(C_TMP.objects): me = o.data bpy.data.objects.remove(o) if me and me.users == 0: bpy.data.meshes.remove(me) bpy.data.collections.remove(C_TMP) bpy.context.view_layer.update() parts = [HULL, CABIN, CANOPY, LID, ROTOR_L, DRILL] REPORT["audit"] = [audit(o) for o in parts] REPORT["dimensions"] = {} dg = bpy.context.evaluated_depsgraph_get() allv = [] for o in [HULL, CANOPY, LID, ROTOR_L, ROTOR_R, DRILL]: allv += [o.matrix_world @ v.co for v in o.data.vertices] REPORT["dimensions"] = {"x": [round(min(v.x for v in allv), 3), round(max(v.x for v in allv), 3)], "y": [round(min(v.y for v in allv), 3), round(max(v.y for v in allv), 3)], "z": [round(min(v.z for v in allv), 3), round(max(v.z for v in allv), 3)]} REPORT["pivots"] = {"canopy_hinge": [round(c, 4) for c in HINGE_CAN], "lid_hinge": [round(c, 4) for c in HINGE_LID], "rotor": [NAC_X, ROTOR_Y, NAC_Z], "drill": [round(c, 4) for c in TOOL]} sc = bpy.context.scene sc.unit_settings.system = 'METRIC' sc.render.engine = 'CYCLES' BLEND = os.path.join(OUT, "Tender_Remora.blend") bpy.ops.wm.save_as_mainfile(filepath=BLEND) REPORT["seconds"] = round(time.time() - t_start, 1) json.dump(REPORT, open(os.path.join(HERE, "build_report.json"), "w"), indent=1) print("BUILD OK", BLEND) print(json.dumps(REPORT, indent=1))