# S-7.5 Prospector v2 -- procedural build for Blender 4.2+ / 5.x, from Assets/allinone.png (one 5-view sheet). # exec(open(r"D:\HADAL - The Descent\models\build_prospector_v2.py").read()) -> rebuilds collection "S75_Prospector_v2". # # AXES bow = -Y, stern = +Y, up = +Z, port = +X, metres. "s" = metres aft of the bow tip (0..7.5), world Y = s-3.75. # z = 0 is the centre of the nose lens. # # --------------------------------------------------------------------------------------------------------- # BLUEPRINT SCALES (sheet pixel coordinates of allinone.png, 1086 x 1448, y down) # view x-scale px/m y-scale px/m origin crop box (x0,y0,x1,y1) # side 128.7 128.7 bow tip x=61, z=0 at y=304 (30,100,1056,417) # top 129.3 (length) 117.5 (beam) bow tip x=57, centreline y=621 (30,455,1056,776) image up = starboard # bottom 129.2 (length) 112.7 (beam) bow tip x=57, centreline y=949 (30,800,1056,1098) image up = port # front 146.2 146.2 centreline x=298, z=0 at y=1307 (lens) (90,1090,516,1442) image right = port # rear 158.8 158.8 centreline x=778, z=0 at y=1264 (560,1038,1010,1406) image right = starboard # (rear scale: duct outer faces x=572..985 taken as 2.6 m; z origin puts the rotor axis, z=0.13, on y=1243) # Length arrow: x=59.5..1025.5 (966 px = 7.5 m). Beam arrow (front): x=108..488 (380 px = 2.6 m). # # CONFLICT RULINGS (the sheet's views are not mutually consistent) # R1 Beam. At the length scale the plan views are only 2.36 m wide (ducts y=469..773). The sheet states 2.6 m, # so the plan views are read with a separate lateral scale (117.5 / 112.7 px/m) that makes the widest items # (duct outer faces, clamp lugs, guard hoops) 2.60 m apart. UPHELD by the Critique (round 1), with the # correction that the hull aft of the canopy collar is only +-0.66 wide (top/bottom views), see W_T. # R2 Heights. Front and rear views are the side-view heights squeezed to ~0.64x / 0.71x. Side view wins for # every height. Front/rear are rendered twice: isotropic (lens / rotor-axis aligned) and "normalised" # (front ppy 93.3 px/m, z=0 at y=1270.3; rear ppy 113.2 px/m, z=0 at y=1274.6) for lateral / ordering checks. # R3 Canopy rear edge from the side view (s=2.46 below z 0.8, raked forward to 2.36 at the roof). # R4 Canopy sill vs plan width: side view puts the sill low where the top view draws the glass narrow, so the # hull has a shelf ("cheek") between hull flank and frame forward of s=1.5. Side for sill z, top for width. # R5 No frame band along the roof (top view shows bare glass across the centreline). # R6 OVERTURNED in round 2: no centreline hinge bar. Two two-tier hinge arms at x=+-0.55 (top + side views). # R7 Hatch: centre s=3.66, flange r=0.535 (side). Posts fore/aft at s=3.25 / 4.06 (top + side). # R8 Propulsor: body of revolution, 0.93 m max diameter, s=6.10..7.50 (top view); axis z=0.13 (side view). # R9 Upper side pods: flattened capsule 0.38 wide (top view) x 0.816 tall (side view), centre x=0.855, z=0.354; # the end cones taper in width and height to flat caps 0.14 x 0.40. # R10 OVERTURNED in round 2: guard hoops built, two per side at the clamp stations (s=2.96, 4.475), outer face 1.30. # R11 Pylon: plan (top/bottom) for stations and widths, side view for its height band (z=-0.13..0.40). # R12 Chin dome: side view (centre s=0.70) wins over the bottom view (s=0.09..0.74). # R13 Tool housing (port side only): side view for stations/heights, bottom view for lateral position (x=0.63). # R14 Moving canopy (owner requirement): glass + inner frame band + hinge arms = Canopy_Lid assembly pivoting # about X at s=2.37, z=1.25; the outer coaming band, hinge pedestals and latch blocks stay on the hull. # --------------------------------------------------------------------------------------------------------- import bpy, bmesh, math from bisect import bisect_right from math import sin, cos, pi, radians, sqrt, asin, acos, copysign from mathutils import Vector, Matrix NAME = "S75_Prospector_v2" L_HALF = 3.75 # ============================================================================ measured tables (s, value) ZT_T = [(0.06, 0.105), (0.117, 0.194), (0.24, 0.32), (0.365, 0.404), (0.49, 0.474), (0.61, 0.536), (0.74, 0.66), (0.86, 0.78), (1.0, 0.875), (1.11, 0.948), (1.235, 1.018), (1.36, 1.07), (1.48, 1.12), (1.61, 1.158), (1.73, 1.197), (1.857, 1.22), (2.0, 1.228), (2.25, 1.222), (2.45, 1.20), (2.56, 1.30), (2.8, 1.30), (2.9, 1.26), (3.0, 1.18), (3.1, 1.10), (3.2, 1.065), (4.2, 1.065), (4.6, 1.065), (4.96, 1.02), (5.34, 0.97), (5.59, 0.91), (5.83, 0.81), (6.08, 0.70), (6.45, 0.58), (6.6, 0.45), (6.75, 0.27), (6.88, 0.255)] ZB_T = [(0.06, -0.105), (0.117, -0.194), (0.24, -0.27), (0.365, -0.326), (0.5, -0.36), (0.75, -0.385), (0.95, -0.40), (1.1, -0.50), (1.25, -0.565), (1.5, -0.63), (2.0, -0.71), (2.5, -0.77), (3.0, -0.79), (4.5, -0.79), (5.0, -0.76), (5.3, -0.71), (5.57, -0.645), (6.0, -0.52), (6.5, -0.35), (6.62, -0.20), (6.75, -0.02), (6.88, -0.01)] W_T = [(0.06, 0.105), (0.147, 0.26), (0.27, 0.38), (0.39, 0.47), (0.52, 0.55), (0.64, 0.62), (0.77, 0.68), (0.89, 0.73), (1.01, 0.77), (1.14, 0.81), (1.26, 0.84), (1.38, 0.87), (1.51, 0.885), (1.63, 0.90), (1.76, 0.906), (1.88, 0.915), (2.0, 0.923), (2.3, 0.93), # aft of the canopy collar (Critique round 1, top view half-width px / 117.5): shoulder, +-0.66 bay, tail (2.54, 0.92), (2.73, 0.834), (2.885, 0.757), (3.04, 0.69), (3.13, 0.664), (4.90, 0.66), (4.97, 0.647), (5.28, 0.545), (5.59, 0.477), (5.90, 0.35), (6.21, 0.22), (6.48, 0.20), (6.83, 0.20), (6.88, 0.13)] ZC_T = [(0, 0.0), (0.5, 0.05), (1.0, 0.10), (1.5, 0.14), (2.0, 0.17), (2.5, 0.20), (5.0, 0.20), (5.6, 0.15), (6.5, 0.12), (6.88, 0.1225)] N_T = [(0, 2.0), (0.5, 2.2), (1.5, 2.4), (2.5, 2.5), (5.0, 2.5), (6.0, 2.2), (6.88, 2.0)] # superellipse exponent S_BOW, S_STERN = 0.06, 6.88 # hull loft range (the lens button stands 0.06 proud of the bow face) # canopy glass bubble: base (virtual sill) height, half-width at the base, roof = ZT_T Z0_T = [(0.64, 0.55), (0.68, 0.49), (0.75, 0.40), (0.85, 0.31), (1.0, 0.20), (1.25, 0.10), (1.5, 0.06), (1.75, 0.05), (2.0, 0.05), (2.47, 0.06)] W0_T = [(0.64, 0.0), (0.645, 0.05), (0.67, 0.13), (0.72, 0.22), (0.8, 0.33), (1.0, 0.535), (1.25, 0.685), (1.5, 0.775), (1.75, 0.82), (2.0, 0.832), (2.3, 0.83), (2.47, 0.80)] SILLC_T = [(2.10, 0.05), (2.2, 0.075), (2.28, 0.115), (2.35, 0.18), (2.40, 0.27), (2.44, 0.39), (2.46, 0.50)] RAKE_T = [(0.80, 0.0), (0.96, 0.04), (1.21, 0.10)] # rear arch leans forward with height (side view) GLASS_M = 2.6 # bubble section exponent GS = [0.645, 0.655, 0.67, 0.69, 0.72, 0.76, 0.81, 0.87, 0.94, 1.02, 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8, 1.9, 2.0, 2.1, 2.18, 2.25, 2.31, 2.36, 2.40, 2.43, 2.45, 2.46] # glass / canopy-zone stations FRAME_W, FRAME_K, FRAME_K_ARCH = 0.165, 0.60, 0.25 # band width, outward flare at the sill / on the rear arch PIVOT_S, PIVOT_Z, HINGE_X = 2.37, 1.25, 0.55 # canopy hinge line (local X) and hinge arm stations HATCH_S, HATCH_R, LID_R = 3.66, 0.535, 0.37 POST_S = (3.25, 4.06) POD_X, POD_Z = 0.855, 0.354 # upper side pod axis TNK_X, TNK_Z, TNK_R = 0.97, -0.485, 0.255 # lower tank axis / radius CLAMP_S = (2.96, 4.475) HOOP_PATH = [(0.97, 0.74), (1.20, 0.62), (1.26, 0.25), (1.20, -0.05), (1.05, -0.20)] # (x, z) tube centreline, r 0.04 DUCT_X, DUCT_Z, DUCT_S0 = 0.829, 0.13, 6.10 ROTOR_S, NBLADES, NSTATORS = 6.60, 8, 8 TOOL_X = 0.63 SEAM_RINGS = [4.97, 5.60] # plain ring seams (nose cap and s=3.0 rings are curved, see build_seams) SEAM_LOWER = [1.70, 2.48] # ring seams that exist only below the canopy frame (s=0.80 stops at the chine) # ============================================================================ small maths def pchip(tbl): xs = [p[0] for p in tbl]; ys = [p[1] for p in tbl]; n = len(xs) h = [xs[i + 1] - xs[i] for i in range(n - 1)] d = [(ys[i + 1] - ys[i]) / h[i] for i in range(n - 1)] m = [0.0] * n m[0], m[-1] = d[0], d[-1] for i in range(1, n - 1): if d[i - 1] * d[i] > 0: w1 = 2 * h[i] + h[i - 1]; w2 = h[i] + 2 * h[i - 1] m[i] = (w1 + w2) / (w1 / d[i - 1] + w2 / d[i]) def f(x): if x <= xs[0]: return ys[0] if x >= xs[-1]: return ys[-1] i = bisect_right(xs, x) - 1 t = (x - xs[i]) / h[i] return ((2 * t ** 3 - 3 * t ** 2 + 1) * ys[i] + (t ** 3 - 2 * t ** 2 + t) * h[i] * m[i] + (-2 * t ** 3 + 3 * t ** 2) * ys[i + 1] + (t ** 3 - t ** 2) * h[i] * m[i + 1]) return f fZT, fZB, fW, fZC, fN = pchip(ZT_T), pchip(ZB_T), pchip(W_T), pchip(ZC_T), pchip(N_T) fZ0, fW0, fSILLC, fRAKE = pchip(Z0_T), pchip(W0_T), pchip(SILLC_T), pchip(RAKE_T) # glass roof = hull roof table up to s 2.45, then flat (the hull hoop behind rises to 1.30, the glass does not) fZTG = pchip([q for q in ZT_T if q[0] <= 2.45] + [(2.47, 1.195)]) def Y(s): return s - L_HALF def P(x, s, z): return Vector((x, s - L_HALF, z)) def spow(v, e): return copysign(abs(v) ** e, v) def hull_xz(s, th): """Half hull section (x >= 0); th = -pi/2 keel .. +pi/2 crown.""" zc = fZC(s); n = fN(s) H = (fZT(s) - zc) if th >= 0 else (zc - fZB(s)) return fW(s) * abs(cos(th)) ** (2 / n), zc + H * spow(sin(th), 2 / n) def hull_th_at_z(s, z): zc = fZC(s); n = fN(s) if z >= zc: return asin(min(1.0, (z - zc) / (fZT(s) - zc)) ** (n / 2)) return -asin(min(1.0, (zc - z) / (zc - fZB(s))) ** (n / 2)) def hull_th_at_x(s, x, upper=True): c = min(1.0, abs(x) / fW(s)) ** (fN(s) / 2) return acos(c) if upper else -acos(c) def hull_pt(s, th, sign=1, off=0.0): x, z = hull_xz(s, th) p = P(sign * x, s, z) if off: d = Vector((sign * x, 0, z - fZC(s))) if d.length > 1e-6: p += d.normalized() * off return p def hull_low_z(s, x): return hull_xz(s, hull_th_at_x(s, x, False))[1] def hull_up_z(s, x): return hull_xz(s, hull_th_at_x(s, x, True))[1] def sill(s): return max(fZ0(s), fSILLC(s)) if s > 2.1 else fZ0(s) def rake(s, z): g = min(1.0, max(0.0, (s - 2.05) / (GS[-1] - 2.05))); g = g * g * (3 - 2 * g) return fRAKE(z) * g def bub(s, t): """Glass bubble point; t = 0 port base .. pi/2 crown .. pi starboard base. s is the un-raked station.""" w = fW0(s); z0 = fZ0(s); h = max(fZTG(s) - z0, 1e-4) z = z0 + h * abs(sin(t)) ** (2 / GLASS_M) return P(w * spow(cos(t), 2 / GLASS_M), s - rake(s, z), z) def bub_t0(s): z0 = fZ0(s); h = max(fZTG(s) - z0, 1e-4) return asin(min(1.0, max(0.0, (sill(s) - z0) / h)) ** (GLASS_M / 2)) def bub_normal(s, t): ds, dt = 0.01, 0.02 s0, s1 = max(GS[0], s - ds), min(GS[-1], s + ds) a = bub(s1, t) - bub(s0, t); b = bub(s, t + dt) - bub(s, t - dt) n = b.cross(a) p = bub(s, t) c = P(0, min(max(s, 1.0), 2.1), fZ0(s) + 0.3) if n.length < 1e-9: n = p - c n.normalize() if n.dot(p - c) < 0: n = -n w = min(1.0, max(0.0, (0.74 - s) / 0.07)) # near the apex the bubble degenerates: blend to the nose-top normal if w > 0: n = (n * (1 - w) + Vector((0, -0.51, 0.86)) * w).normalized() return n # ============================================================================ scene setup coll = bpy.data.collections.get(NAME) if coll: for ob in list(coll.objects): me = ob.data bpy.data.objects.remove(ob, do_unlink=True) if isinstance(me, bpy.types.Mesh) and me.users == 0: bpy.data.meshes.remove(me) else: coll = bpy.data.collections.new(NAME) bpy.context.scene.collection.children.link(coll) root = bpy.data.objects.new(NAME, None) root.empty_display_size = 0.5 coll.objects.link(root) ORIGINS = {NAME: Vector((0, 0, 0))} def make_mat(name, color, metallic=0.0, rough=0.5, emit=0.0, alpha=1.0, double=False): m = bpy.data.materials.get(name) or bpy.data.materials.new(name) try: m.use_nodes = True except Exception: pass nt = m.node_tree if nt is not None: bsdf = next((n for n in nt.nodes if n.type == 'BSDF_PRINCIPLED'), None) if bsdf is None: nt.nodes.clear() bsdf = nt.nodes.new("ShaderNodeBsdfPrincipled") out = nt.nodes.new("ShaderNodeOutputMaterial") nt.links.new(bsdf.outputs[0], out.inputs[0]) def setin(key, val): if key in bsdf.inputs: bsdf.inputs[key].default_value = val setin("Base Color", (*color, 1.0)); setin("Metallic", metallic); setin("Roughness", rough); setin("Alpha", alpha) if emit > 0.0: setin("Emission Color", (*color, 1.0)); setin("Emission Strength", emit) m.diffuse_color = (*color, alpha); m.metallic = metallic; m.roughness = rough m.use_backface_culling = not (double or alpha < 1.0) if alpha < 1.0: for attr, val in (("surface_render_method", 'BLENDED'), ("blend_method", 'BLEND')): try: setattr(m, attr, val) except Exception: pass return m M_HULL = make_mat("S75v2_HullPaint", (0.62, 0.61, 0.55), 0.15, 0.55) M_SEAM = make_mat("S75v2_Seam", (0.16, 0.16, 0.16), 0.5, 0.6, double=True) M_DARK = make_mat("S75v2_DarkMetal", (0.08, 0.09, 0.10), 0.8, 0.45) M_STEEL = make_mat("S75v2_Steel", (0.45, 0.47, 0.49), 0.9, 0.32) M_POD = make_mat("S75v2_PodPaint", (0.22, 0.30, 0.36), 0.5, 0.45) M_TANK = make_mat("S75v2_TankPaint", (0.85, 0.45, 0.07), 0.3, 0.5) M_DUCT = make_mat("S75v2_DuctPaint", (0.14, 0.15, 0.17), 0.7, 0.4) M_RUBBER = make_mat("S75v2_Rubber", (0.025, 0.025, 0.025), 0.0, 0.85) M_GLASS = make_mat("S75v2_Glass", (0.55, 0.80, 0.92), 0.0, 0.05, alpha=0.30) M_LENS = make_mat("S75v2_LampLens", (0.95, 0.97, 1.0), 0.0, 0.1, emit=3.0) M_SENSOR = make_mat("S75v2_SensorGlass", (0.02, 0.03, 0.05), 0.2, 0.08) M_INT = make_mat("S75v2_Interior", (0.06, 0.065, 0.07), 0.1, 0.85, double=True) M_VANE = make_mat("S75v2_Vane", (0.55, 0.57, 0.60), 0.2, 0.6) # ============================================================================ mesh builder class B: def __init__(self): self.bm = bmesh.new(); self.mats = [] def mi(self, mat): if mat not in self.mats: self.mats.append(mat) return self.mats.index(mat) def grid(self, rows, mat, close_u=False, close_v=False): """rows: list of lists of Vector. A row of length 1 becomes a fan apex.""" mi = self.mi(mat) vr = [[self.bm.verts.new(p) for p in r] for r in rows] nr = len(vr) for i in range(nr if close_u else nr - 1): r0, r1 = vr[i], vr[(i + 1) % nr] n = max(len(r0), len(r1)) for j in range(n if close_v else n - 1): a = r0[j % len(r0)]; b = r0[(j + 1) % len(r0)] c = r1[(j + 1) % len(r1)]; d = r1[j % len(r1)] vs = [] for v in (a, b, c, d): if v not in vs: vs.append(v) if len(vs) >= 3: try: f = self.bm.faces.new(vs); f.material_index = mi; f.smooth = True except ValueError: pass return vr def lathe(self, o, axis, prof, segs, mat, sx=1.0, sz=1.0, u=None, arc=None): """prof = [(t along axis, r)], r == 0 gives an apex. sx/sz stretch the ring along u / v.""" axis = Vector(axis).normalized(); o = Vector(o) if u is None: u = Vector((1, 0, 0)) if abs(axis.x) < 0.9 else Vector((0, 1, 0)) u = (Vector(u) - axis * axis.dot(Vector(u))).normalized() v = axis.cross(u) rows = [] for t, r in prof: if r <= 1e-6: rows.append([o + axis * t]) else: rows.append([o + axis * t + (u * cos(2 * pi * k / segs) * sx + v * sin(2 * pi * k / segs) * sz) * r for k in range(segs)]) return self.grid(rows, mat, close_v=True) def cyl(self, p0, p1, r, segs, mat, cap=True): p0, p1 = Vector(p0), Vector(p1); L = (p1 - p0).length prof = [(0, r), (L, r)] if cap: prof = [(0, 0)] + prof + [(L, 0)] self.lathe(p0, p1 - p0, prof, segs, mat) def bolt(self, p, n, r=0.014, h=0.012, mat=None): self.lathe(Vector(p), n, [(-0.006, r), (h, r), (h, 0)], 6, mat or M_STEEL) def tube(self, pts, r, segs, mat, cap=True): pts = [Vector(p) for p in pts] rows = []; u = None for i, p in enumerate(pts): t = (pts[min(i + 1, len(pts) - 1)] - pts[max(i - 1, 0)]).normalized() if u is None: u = Vector((0, 0, 1)) if abs(t.z) < 0.9 else Vector((1, 0, 0)) u = (u - t * t.dot(u)).normalized(); v = t.cross(u) rows.append([p + (u * cos(2 * pi * k / segs) + v * sin(2 * pi * k / segs)) * r for k in range(segs)]) if cap: rows = [[pts[0]]] + rows + [[pts[-1]]] self.grid(rows, mat, close_v=True) def box(self, c, size, mat, bevel=0.0, rot=None, taper=None): """taper: optional function(Vector local unit-cube coord) -> Vector, applied before bevel.""" t = bmesh.new() bmesh.ops.create_cube(t, size=1.0) for v in t.verts: if taper: v.co = taper(v.co.copy()) v.co = Vector((v.co.x * size[0], v.co.y * size[1], v.co.z * size[2])) if bevel > 0: bmesh.ops.bevel(t, geom=t.edges[:], offset=bevel, segments=2, affect='EDGES', profile=0.5) M = Matrix.Translation(Vector(c)) @ (rot.to_4x4() if rot is not None else Matrix.Identity(4)) mi = self.mi(mat); vm = {} for v in t.verts: vm[v] = self.bm.verts.new(M @ v.co) for f in t.faces: nf = self.bm.faces.new([vm[v] for v in f.verts]); nf.material_index = mi; nf.smooth = True t.free() def finish(self, name, origin=(0, 0, 0), parent=None, sharp=38.0): origin = Vector(origin) bmesh.ops.remove_doubles(self.bm, verts=self.bm.verts, dist=1e-5) bmesh.ops.recalc_face_normals(self.bm, faces=self.bm.faces) for v in self.bm.verts: v.co -= origin me = bpy.data.meshes.new(name) self.bm.to_mesh(me); self.bm.free() for m in self.mats: me.materials.append(m) try: me.set_sharp_from_angle(angle=radians(sharp)) except Exception: pass ob = bpy.data.objects.new(name, me) coll.objects.link(ob) par = parent or root ob.parent = par ob.location = origin - ORIGINS[par.name] ORIGINS[name] = origin return ob def rotmat(axis_from, axis_to): return Vector(axis_from).rotation_difference(Vector(axis_to).normalized()).to_matrix() # ============================================================================ CANOPY PERIMETER / BAND FRAMES NV = 22 NA, NB = 17, 11 # points on the lower / upper half hull section def is_canopy(s): return GS[0] - 1e-6 <= s <= GS[-1] + 1e-6 def perimeter(): """Closed loop of (point, bubble normal, station, on_arch): port sill fwd->aft, rear arch port->stbd, stbd sill aft->fwd.""" loop = [] for s in GS: t0 = bub_t0(s); loop.append((bub(s, t0), bub_normal(s, t0), s, False)) s = GS[-1]; t0 = bub_t0(s) for k in range(1, NV): t = t0 + (pi - 2 * t0) * k / NV; loop.append((bub(s, t), bub_normal(s, t), s, True)) for s in reversed(GS): t = pi - bub_t0(s); loop.append((bub(s, t), bub_normal(s, t), s, False)) return loop def collar_frames(): """Per perimeter point: (p, band direction, height direction, flare weight 0..1 (0 at the apex), station).""" loop = perimeter(); n = len(loop) G = P(0, 1.7, 0.75) fr = [] for i, (p, nn, sp, arch) in enumerate(loop): t = (loop[(i + 1) % n][0] - loop[(i - 1) % n][0]) if t.length < 1e-6: t = Vector((0, 1, 0)) t.normalize() o = t.cross(nn) if o.length < 1e-6: o = p - G o.normalize() if o.dot(p - G) < 0: o = -o a = 1.0 - min(1.0, max(0.0, (0.80 - sp) / 0.12)) # no flare at the apex: the band lies on the nose kk = (FRAME_K_ARCH if arch else FRAME_K) * a fr.append([p, (o + kk * nn).normalized(), (nn - kk * o).normalized(), a, sp, rake(sp, p.z) if arch else 0.0]) for _ in range(3): # smooth the directions round the loop f2 = [] for i in range(n): bd = (fr[i - 1][1] + 2 * fr[i][1] + fr[(i + 1) % n][1]).normalized() hd = (fr[i - 1][2] + 2 * fr[i][2] + fr[(i + 1) % n][2]) hd = (hd - bd * hd.dot(bd)).normalized() f2.append([fr[i][0], bd, hd, fr[i][3], fr[i][4], fr[i][5]]) fr = f2 return fr FRAMES = collar_frames() # band profiles in (d along the band from the glass edge, h proud of the band plane) INNER_PROF = [(-0.012, -0.015), (-0.012, 0.022), (0.0, 0.030), (0.058, 0.030), (0.068, 0.022), (0.068, -0.015)] # moves COAM_PROF = [(0.078, -0.03), (0.078, 0.014), (0.086, 0.022), (0.153, 0.022), (0.165, 0.014), (0.165, -0.10)] # fixed FOOT, RIM = [], [] # hull skin points under the band, per canopy station (port side, x/z only) for i, s in enumerate(GS): p, bd, hd, a = FRAMES[i][:4] zs = p.z; wg = p.x ff = p + bd * 0.15 - hd * 0.035; rf = p - bd * 0.012 - hd * 0.045 FOOT.append((max(0.0, (wg + 0.10) * (1 - a) + ff.x * a), (zs - 0.035) * (1 - a) + ff.z * a)) RIM.append((max(0.0, max(wg - 0.012, 0.0) * (1 - a) + rf.x * a), (zs - 0.045) * (1 - a) + rf.z * a)) fFOOTZ = pchip([(s, FOOT[i][1]) for i, s in enumerate(GS)]) # ============================================================================ HULL def tub_floor(s): return max(fZ0(s) - min(0.55, 1.2 * (s - 0.64)) - 0.03, fZB(s) + 0.12) def _arc(s, th_e): return [hull_xz(s, -pi / 2 + (th_e + pi / 2) * k / (NA - 1)) for k in range(NA)] LAND_END = 2.60 def z_low(s): """Height of the hull skin under the outer edge of the band where it fans round the lower rear corner.""" return sill(min(max(s - 0.12, 2.0), GS[-1])) - 0.165 def land_pts(s): sl = GS[-1]; zc_ = 0.45 w = fW0(sl); z0 = fZ0(sl); h = fZTG(sl) - z0 ta = asin(min(1.0, max(0.0, (z_low(s) + 0.03 - z0) / h)) ** (GLASS_M / 2)) out = [] for k in range(NB): t = ta + (pi / 2 - ta) * k / (NB - 1) v = Vector((w * cos(t) ** (2 / GLASS_M), z0 + h * sin(t) ** (2 / GLASS_M) - zc_)) v *= 1 + (FRAME_K_ARCH * (s - sl) - 0.045) / v.length out.append((max(v.x, 0.0), v.y + zc_)) out[-1] = (0.0, out[-1][1]) return out def half_section(s): """NA points keel -> end of the outer skin, then NB points to the centreline. Canopy zone: skin runs up the hull superellipse until it meets the band foot (clipped to a shelf where the hull would be higher, ruling R4), then foot, tub rim, tub wall, tub floor. Bow / aft sections: plain superellipse, split at an angle that lines up with the nearest canopy section.""" if is_canopy(s): i = min(range(len(GS)), key=lambda k: abs(GS[k] - s)) xf, zf = FOOT[i]; rx, rz = RIM[i] if s > 2.2: # lower rear corner: the band fans aft, keep the skin under it w = min(1.0, (s - 2.2) / 0.1); w = w * w * (3 - 2 * w) zf = zf * (1 - w) + min(zf, z_low(s)) * w xf = xf * (1 - w) + (rx + 0.02) * w if FRAMES[i][3] < 1.0: # apex: the band runs forward over steeply rising sill, so w = 1.0 - FRAMES[i][3] # recess the skin under it and put a wall just outside the glass zf -= 0.07 * w; xf = xf * (1 - w) + (rx + 0.02) * w xf = min(xf, fW(s) - 0.004) if hull_up_z(s, xf) > zf: # hull would be above the band foot: clip at the shelf th_e = hull_th_at_z(s, zf) else: th_e = hull_th_at_x(s, xf, True); zf = hull_xz(s, th_e)[1] a = FRAMES[i][3] if a < 1.0: # towards the apex end the skin high up, near the band, so xa = min(FRAMES[i][0].x + 0.19, fW(s) - 0.004) # the nose keeps its shape and the loft does not twist th_e = th_e * a + hull_th_at_x(s, xa, True) * (1 - a) pts = _arc(s, th_e) xo = min(max(xf, FRAMES[i][0].x + 0.12 * (1 - a)), pts[-1][0]) pts.append((xo, zf)) # outer edge of the recess under the band (= foot when a = 1) pts.append((min(xf, xo), zf)) # band foot pts.append((min(rx, xf), rz)) # tub rim, under the glass edge fl = min(tub_floor(s), rz - 0.02); xw = max(rx - 0.06, 0.0) pts.append((xw, fl)) for k in range(1, NB - 3): pts.append((xw * (1 - k / (NB - 4)), fl)) return pts if s < GS[0]: th_e = hull_th_at_x(s, min(FRAMES[0][0].x + 0.19, 0.6 * fW(s)), True) pts = _arc(s, th_e) + [hull_xz(s, th_e + (pi / 2 - th_e) * k / NB) for k in range(1, NB + 1)] w = min(1.0, max(0.0, (s - 0.48) / (GS[0] - 0.48))); w = w * w * (3 - 2 * w) # recess under the band tip return [(x, z - 0.07 * w * max(0.0, 1 - (x / 0.45) ** 2)) for x, z in pts] else: if s <= LAND_END + 1e-6: # recessed land under the rear-arch band, up to the hoop return _arc(s, hull_th_at_z(s, z_low(s))) + land_pts(s) th_e = hull_th_at_z(LAND_END, z_low(LAND_END)) * max(0.0, 1 - (s - LAND_END) / 0.6) return _arc(s, th_e) + [hull_xz(s, th_e + (pi / 2 - th_e) * k / NB) for k in range(1, NB + 1)] def hull_stations(): st = [S_BOW, 0.075, 0.10, 0.14] s = 0.19 while s < S_STERN - 0.03: if not (GS[0] - 0.03 < s < GS[-1] + 0.03): st.append(round(s, 4)) s += 0.06 st += GS + [0.61, 2.48, 2.52, 2.56, 2.60, 2.62, 2.66, 2.73, 2.80, 2.885, 2.96, 3.04, 3.13, 6.48, 6.75, 6.83, S_STERN] st = [v for v in st if not (GS[-1] + 1e-6 < v < 2.62 and v not in (2.48, 2.52, 2.56, 2.60))] st = sorted(set(st)); out = [st[0]] for v in st[1:]: if v - out[-1] > 0.012 or v in GS: out.append(v) return out def build_hull(): b = B(); mh = b.mi(M_HULL); mint = b.mi(M_INT) st = hull_stations(); rings = [] for s in st: h = half_section(s) ring = [P(x, s, z) for x, z in h] + [P(-x, s, z) for x, z in reversed(h[1:-1])] rings.append([b.bm.verts.new(p) for p in ring]) nring = len(rings[0]) for i in range(len(st) - 1): can = is_canopy(st[i]) or is_canopy(st[i + 1]) for j in range(nring): j2 = (j + 1) % nring vs = [rings[i][j], rings[i][j2], rings[i + 1][j2], rings[i + 1][j]] f = b.bm.faces.new(vs); f.smooth = True jj = min(j, nring - 1 - j) # mirrored index 0..NA+NB-1 f.material_index = mint if (can and jj >= NA + 2) else mh for ring, s in ((rings[0], st[0]), (rings[-1], st[-1])): # bow / stern caps c = b.bm.verts.new(P(0, s, fZC(s))) for j in range(nring): f = b.bm.faces.new([ring[j], ring[(j + 1) % nring], c]); f.material_index = mh zc = fZC(S_STERN) # stern nozzle r 0.12 to s 6.91 b.lathe(P(0, S_STERN - 0.02, zc), (0, 1, 0), [(0, 0.125), (0.045, 0.125), (0.05, 0.115), (0.05, 0.09)], 20, M_DARK) b.lathe(P(0, S_STERN - 0.02, zc), (0, 1, 0), [(0.05, 0.09), (0.0, 0.085), (0.0, 0)], 20, M_RUBBER) return b.finish("Hull", sharp=42) def build_seams(): b = B(); off = 0.005; hw = 0.011 def ring(sf, th0, th1, n=40): f = sf if callable(sf) else (lambda th: sf) for sg in (1, -1): rows = [[hull_pt(f(th0 + (th1 - th0) * k / n) + d, th0 + (th1 - th0) * k / n, sg, off) for k in range(n + 1)] for d in (-hw, hw)] b.grid(rows, M_SEAM) for s in SEAM_RINGS: ring(s, -pi / 2, pi / 2, 48) # nose cap seam, C-shaped in the side view: s 0.40 at the keel, 0.24 at mid height, 0.30 at the crown ring(lambda th: 0.24 - 0.05 * (th / (pi / 2)) + 0.11 * (th / (pi / 2)) ** 2, -pi / 2, pi / 2, 48) ring(3.01, -pi / 2, pi / 2, 48) # kept straight: side view curls it forward at the top, top view bows it aft for s in SEAM_LOWER: th1 = hull_th_at_z(s, fFOOTZ(s) - 0.03) if is_canopy(s) else hull_th_at_z(s, -0.10) ring(s, -pi / 2, th1, 24) ring(0.80, hull_th_at_z(0.80, -0.27), hull_th_at_z(0.80, fFOOTZ(0.80) - 0.03), 12) # frame down to the chine only def long_z(s0, s1, zf, n=24): # seam along the flank at height zf(s) for sg in (1, -1): rows = [[], []] for k in range(n + 1): s = s0 + (s1 - s0) * k / n; z = zf(s) for r, dz in ((0, -hw), (1, hw)): rows[r].append(hull_pt(s, hull_th_at_z(s, z + dz), sg, off)) b.grid(rows, M_SEAM) def long_x(s0, s1, xf, n=24): # seam along the top deck at half-width xf(s) for sg in (1, -1): rows = [[], []] for k in range(n + 1): s = s0 + (s1 - s0) * k / n; x = xf(s) for r, dx in ((0, -hw), (1, hw)): rows[r].append(hull_pt(s, hull_th_at_x(s, x + dx, True), sg, off)) b.grid(rows, M_SEAM) long_z(4.97, 6.40, pchip([(4.97, 0.70), (5.60, 0.68), (6.2, 0.44), (6.4, 0.38)])) # side view upper tail line long_z(4.97, 6.40, pchip([(4.97, -0.12), (5.60, -0.12), (6.4, -0.10)])) # side view lower tail line long_z(3.01, 4.97, lambda s: 0.70, 30) # z=+0.70 line behind the pods long_z(2.52, 3.01, lambda s: 0.124, 10) # z=+0.124 line aft of the frame long_z(4.21, 4.90, lambda s: min(1.04, fZT(s) - 0.022), 14) # spine line (hidden under the collar forward of 4.2) long_z(0.60, 2.90, pchip([(0.60, -0.24), (0.78, -0.26), (1.47, -0.37), (1.70, -0.46), (2.90, -0.46)]), 40) # chine long_z(0.30, 0.60, pchip([(0.30, 0.19), (0.60, 0.35)]), 8) # nose creases (cheek edges) return b.finish("Hull_Seams") # ============================================================================ CANOPY PIVOT = P(0, PIVOT_S, PIVOT_Z) # canopy opens about local X through the hinge bases on the hoop def glass_rows(): rows = [] for s in GS: t0 = bub_t0(s) rows.append([bub(s, t0 + (pi - 2 * t0) * k / NV) for k in range(NV + 1)]) return rows def band_pt(fr, d, h, coam): """Point of the frame band. Near the apex (a < 1) the moving inner band shrinks to a thin bead that hugs the glass edge and the fixed coaming takes the rest; on the raked rear arch the coaming widens so its outer edge stays on the hoop (s ~2.62).""" p, bd, hd, a, sp, rk = fr sc = 0.35 + 0.65 * a if coam: d0 = 0.078 * sc dd = d0 + (d - 0.078) / 0.087 * (0.165 + rk - d0) if h < -0.05: h -= 0.09 * (1 - a) # deeper outer wall where the nose skin is recessed under it return p + bd * dd + hd * (h + 0.02 * (1 - a)) return p + bd * (d * sc if d > 0 else d) + hd * (h + 0.02 * (1 - a)) def band_rows(prof, coam=False): return [[band_pt(fr, d, h, coam) for (d, h) in prof] for fr in FRAMES] def glass_at_x(s, x): t = acos(min(1.0, x / fW0(s)) ** (GLASS_M / 2)) return bub(s, t) def build_canopy_lid(): """Moving assembly (R14): inner frame band + the two hinge arms. Canopy_Glass is its child.""" b = B() b.grid(band_rows(INNER_PROF[:3]), M_RUBBER, close_u=True) b.grid(band_rows(INNER_PROF[2:]), M_DARK, close_u=True) zg = glass_at_x(2.25, HINGE_X).z for sg in (1, -1): x = sg * HINGE_X b.box(P(x, 2.155, 1.25), (0.16, 0.43, 0.10), M_DARK, bevel=0.02) # upper bar s 1.94..2.37 b.box(P(x, 2.17, (zg - 0.05 + 1.21) / 2), (0.12, 0.34, 1.21 - zg + 0.05), M_DARK, bevel=0.015) # lower bracket b.cyl(P(x - 0.09, 1.99, 1.25), P(x + 0.09, 1.99, 1.25), 0.025, 12, M_STEEL) # pin dia 0.05 (side view) b.cyl(P(x - 0.082, PIVOT_S, PIVOT_Z), P(x + 0.082, PIVOT_S, PIVOT_Z), 0.03, 12, M_STEEL) # pivot knuckle return b.finish("Canopy_Lid", origin=PIVOT) def build_glass(parent): b = B() b.grid(glass_rows(), M_GLASS) return b.finish("Canopy_Glass", origin=PIVOT, parent=parent, sharp=60) def build_coaming(): """Fixed part: outer frame band with a hard outer step, bolts, hinge pedestals, side latch blocks.""" b = B() b.grid(band_rows(COAM_PROF, True), M_STEEL, close_u=True) last = None for fr in FRAMES: hd = fr[2]; q = band_pt(fr, 0.125, 0.022, True) if last is None or (q - last).length > 0.21: b.bolt(q, hd, 0.011, 0.008, M_DARK); last = q for sg in (1, -1): x = sg * HINGE_X b.box(P(x, 2.55, 1.19), (0.21, 0.10, 0.22), M_DARK, bevel=0.02) # pedestal on the hoop for e in (-1, 1): # cheek plates beside the bar b.box(P(x + e * 0.096, 2.43, 1.25), (0.018, 0.20, 0.10), M_DARK, bevel=0.004) # side latch blocks (plan views only): 0.31 x 0.12, outboard face 1.055, thin in z b.box(P(sg * 0.995, 2.205, -0.14), (0.12, 0.31, 0.05), M_DARK, bevel=0.012) b.bolt(P(sg * 1.055, 2.205, -0.14), (sg, 0, 0), 0.014, 0.008) return b.finish("Canopy_Coaming") def build_interior(): b = B() fl = tub_floor(1.85) b.box(P(0, 1.85, fl + 0.13), (0.50, 0.50, 0.26), M_RUBBER, bevel=0.04) # seat cushion Rb = Matrix.Rotation(radians(-14), 3, 'X') b.box(P(0, 2.17, fl + 0.55), (0.48, 0.13, 0.72), M_RUBBER, bevel=0.04, rot=Rb) # backrest b.box(P(0, 2.27, fl + 1.02), (0.26, 0.11, 0.22), M_RUBBER, bevel=0.04, rot=Rb) # headrest f2 = tub_floor(1.08) b.box(P(0, 1.08, (f2 + 0.10) / 2), (0.62, 0.26, 0.10 - f2), M_DARK, bevel=0.03) # console b.box(P(0, 1.16, 0.12), (0.46, 0.05, 0.12), M_SENSOR, bevel=0.01, rot=Matrix.Rotation(radians(25), 3, 'X')) b.cyl(P(0.22, 1.45, tub_floor(1.45)), P(0.22, 1.40, tub_floor(1.45) + 0.33), 0.018, 8, M_DARK) b.lathe(P(0.22, 1.40, tub_floor(1.45) + 0.33), (0, -0.15, 1), [(0, 0.0), (0.01, 0.03), (0.07, 0.03), (0.08, 0)], 8, M_RUBBER) return b.finish("Cockpit_Interior") # ============================================================================ NOSE / CHIN def build_nose_lens(): b = B() # bezel on the bow face, and the lens as a rounded button r 0.078 standing 0.06 proud b.lathe(P(0, 0, 0), (0, 1, 0), [(0.085, 0.108), (0.062, 0.106), (0.052, 0.096), (0.052, 0.078)], 24, M_DARK) b.lathe(P(0, 0, 0), (0, 1, 0), [(0.06, 0.078), (0.025, 0.078), (0.012, 0.072), (0.004, 0.055), (0.0, 0.03), (0.0, 0.0)], 24, M_LENS) return b.finish("Nose_Lens") def build_chin(): b = B() c = P(0, 0.70, 0) b.lathe(c, (0, 0, -1), [(0.18, 0.250), (0.345, 0.250), (0.37, 0.240), (0.375, 0.222)], 32, M_DARK, sx=1.08, sz=1.0) for k in range(8): a = 2 * pi * (k + 0.5) / 8 b.bolt(c + Vector((0.236 * 1.08 * cos(a), 0.236 * sin(a), -0.372)), (0, 0, -1), 0.010, 0.006) prof = [(0.372 + 0.222 * sin(a), 0.222 * cos(a)) for a in [radians(d) for d in range(0, 90, 10)]] + [(0.594, 0)] b.lathe(c, (0, 0, -1), prof, 32, M_SENSOR) b.lathe(P(0, 1.03, fZB(1.03) + 0.012), (0, 0, -1), [(0, 0.035), (0.02, 0.035), (0.024, 0.028), (0.024, 0)], 12, M_STEEL) return b.finish("Chin_Sensor") # ============================================================================ HATCH def build_hatch(): b = B(); c = P(0, HATCH_S, 0) R = HATCH_R # neck (narrower than the flange, reaches down into the narrow hull shoulder) + flange b.lathe(c, (0, 0, 1), [(0.80, R - 0.035), (1.085, R - 0.035), (1.10, R), (1.215, R), (1.225, R - 0.01), (1.225, LID_R + 0.012), (1.20, LID_R + 0.012), (1.20, 0)], 48, M_STEEL) for s in POST_S: # lifting posts, fore and aft (R7) p = P(0, s, 1.225) b.lathe(p, (0, 0, 1), [(-0.005, 0.032), (0.025, 0.032), (0.03, 0.022), (0.135, 0.022), (0.155, 0.017), (0.165, 0)], 12, M_STEEL) for k in range(12): # flange studs a = 2 * pi * (k + 0.5) / 12 if abs(cos(a)) < 0.3: continue b.bolt(c + Vector((0.475 * cos(a), 0.475 * sin(a), 1.225)), (0, 0, 1), 0.011, 0.008) # spine fairing: rounded-rectangle slab s 4.19..4.63, 0.255 wide, crisp edges hw_, r_ = 0.1275, 0.08 ss = [4.10, 4.30, 4.55] + [4.55 + r_ * sin(radians(a)) for a in (20, 40, 60, 75, 86)] rows = [] for s in ss: w = hw_ if s <= 4.55 else hw_ - r_ + sqrt(max(r_ * r_ - (s - 4.55) ** 2, 0.0)) zt = 1.105 - 0.02 * (s - 4.10) / 0.53 rows.append([P(-w, s, 0.98), P(-w, s, zt - 0.012), P(-w + 0.012, s, zt), P(w - 0.012, s, zt), P(w, s, zt - 0.012), P(w, s, 0.98)]) rows.append([P(-(hw_ - r_), 4.631, 0.98), P(-(hw_ - r_), 4.631, 1.07), P(-(hw_ - r_) + 0.01, 4.63, 1.075), P(hw_ - r_ - 0.01, 4.63, 1.075), P(hw_ - r_, 4.631, 1.07), P(hw_ - r_, 4.631, 0.98)]) b.grid(rows, M_HULL) return b.finish("Hatch_Collar", sharp=30) def build_lid(): b = B(); c = P(0, HATCH_S, 0); R = LID_R b.lathe(c, (0, 0, 1), [(1.20, R), (1.265, R), (1.277, R - 0.012), (1.280, 0.275), (1.270, 0.268), (1.270, 0.252), (1.284, 0.240), (1.295, 0.0)], 48, M_HULL) for k in range(4): # four bosses on the 45 deg diagonals (top view) a = pi / 4 + pi / 2 * k b.lathe(c + Vector((0.315 * cos(a), 0.315 * sin(a), 1.279)), (0, 0, 1), [(-0.004, 0.03), (0.012, 0.03), (0.016, 0.024), (0.016, 0)], 12, M_STEEL) for sg in (1, -1): # two small bolts on the lateral axis b.bolt(c + Vector((sg * 0.32, 0, 1.279)), (0, 0, 1), 0.013, 0.008) origin = c + Vector((0, R, 1.22)) # hinge line: local X through the aft rim for sg in (1, -1): # hinge knuckles either side of the aft post b.cyl(origin + Vector((sg * 0.12 - 0.035, -0.012, 0.03)), origin + Vector((sg * 0.12 + 0.035, -0.012, 0.03)), 0.02, 10, M_STEEL) return b.finish("Hatch_Lid", origin=origin) # ============================================================================ SIDE TANKS POD_HX, POD_HZ = 0.19, 0.408 POD_PROF = [ # s, half-width, half-height (side view: body 3.41..4.55, caps at 3.11 / 4.87, band rings) (3.11, 0.070, 0.200), (3.125, 0.076, 0.206), (3.36, 0.184, 0.396), (3.365, 0.198, 0.416), (3.425, 0.198, 0.416), (3.43, POD_HX, POD_HZ), (4.52, POD_HX, POD_HZ), (4.525, 0.198, 0.416), (4.575, 0.198, 0.416), (4.58, 0.184, 0.396), (4.855, 0.076, 0.206), (4.87, 0.070, 0.200)] POD_N = 3.2 def build_pod(sg, name): b = B(); segs = 28 def sec(s, hx, hz): return [P(sg * POD_X + hx * spow(cos(a), 2 / POD_N), s, POD_Z + hz * spow(sin(a), 2 / POD_N)) for a in [2 * pi * k / segs for k in range(segs)]] b.grid([sec(*p) for p in POD_PROF], M_POD, close_v=True) for s, d in ((3.11, -1), (4.87, 1)): # flat end caps, slightly proud b.grid([sec(s, 0.070, 0.200), sec(s + d * 0.012, 0.060, 0.188), [P(sg * POD_X, s + d * 0.012, POD_Z)]], M_STEEL, close_v=True) # seam strips on the outboard face (side view: z +0.36, +0.28 and faint ones near top and bottom) for z in (0.36, 0.28, 0.67, 0.03): rows = [] for dz in (-0.009, 0.009): zz = z + dz - POD_Z x = (POD_HX + 0.004) * (1 - min(0.999, abs(zz) / POD_HZ) ** POD_N) ** (1 / POD_N) rows.append([P(sg * (POD_X + x), s, z + dz) for s in (3.44, 3.98, 4.51)]) b.grid(rows, M_SEAM) for s in (3.70, 4.25): # standoff brackets to the hull b.box(P(sg * 0.62, s, POD_Z), (0.16, 0.12, 0.22), M_DARK, bevel=0.015) return b.finish(name) def build_tank(sg, name): b = B(); o = Vector((sg * TNK_X, 0, TNK_Z)); ax = (0, 1, 0); R = TNK_R def ys(s): return s - L_HALF body = [(2.62, 0.17), (2.72, R - 0.01), (2.72, R + 0.005), (2.78, R + 0.005), (2.78, R), (4.60, R), (4.66, R - 0.012), (4.70, 0.235), (4.74, 0.205), (4.77, 0.155), (4.78, 0.14), (4.80, 0.147), (4.82, 0.137), (4.84, 0.147), (4.86, 0.14), (4.86, 0.07), (4.89, 0.07), (4.89, 0)] b.lathe(o, ax, [(ys(s), r) for s, r in body], 28, M_TANK) # forward lamp housing (front view): bezel r 0.17, lens r 0.14 b.lathe(o, ax, [(ys(2.62), 0.17), (ys(2.565), 0.17), (ys(2.555), 0.162), (ys(2.555), 0.142), (ys(2.57), 0.14)], 28, M_DARK) b.lathe(o, ax, [(ys(2.57), 0.14), (ys(2.56), 0.09), (ys(2.556), 0)], 28, M_LENS) for k in range(8): # bolts on the forward ring a = 2 * pi * (k + 0.5) / 8 b.bolt(o + Vector((0.215 * cos(a), ys(2.69), 0.215 * sin(a))), (0, -1, 0), 0.012, 0.008) # clamps: strap all round + four lugs on the 45 deg diagonals, saddle bracket to the hull for cs in CLAMP_S: b.lathe(o, ax, [(ys(cs - 0.075), R - 0.005), (ys(cs - 0.075), R + 0.03), (ys(cs + 0.075), R + 0.03), (ys(cs + 0.075), R - 0.005)], 28, M_DARK) for up in (1, -1): zc_ = TNK_Z + up * 0.20 # outboard lug: face at x 1.29, bolt head to 1.30 b.box(Vector((sg * 1.195, Y(cs), zc_)), (0.19, 0.15, 0.13), M_DARK, bevel=0.018) b.bolt(Vector((sg * 1.29, Y(cs), zc_)), (sg, 0, 0), 0.026, 0.010) b.bolt(Vector((sg * (TNK_X + 0.19), Y(cs), zc_ + up * 0.065)), (0, 0, up), 0.024, 0.010) # inboard lug, bearing on the hull side b.box(Vector((sg * (TNK_X - 0.19), Y(cs), zc_)), (0.13, 0.15, 0.13), M_DARK, bevel=0.018) b.bolt(Vector((sg * (TNK_X - 0.19), Y(cs), zc_ + up * 0.065)), (0, 0, up), 0.024, 0.010) b.box(Vector((sg * 0.62, Y(cs), TNK_Z + 0.02)), (0.36, 0.10, 0.30), M_DARK, bevel=0.015) # saddle bracket # rails between the clamps s0, s1 = CLAMP_S[0] + 0.075, CLAMP_S[1] - 0.075 b.cyl(P(sg * TNK_X, s0, TNK_Z + R + 0.03), P(sg * TNK_X, s1, TNK_Z + R + 0.03), 0.016, 8, M_STEEL, cap=False) b.cyl(P(sg * (TNK_X + R + 0.03), s0, TNK_Z), P(sg * (TNK_X + R + 0.03), s1, TNK_Z), 0.016, 8, M_STEEL, cap=False) # pipe fitting from the forward cap back into the hull flank (plan views show fittings at s 2.45..2.7) b.tube([P(sg * (TNK_X - 0.10), 2.66, TNK_Z + 0.10), P(sg * (TNK_X - 0.16), 2.56, TNK_Z + 0.2), P(sg * (TNK_X - 0.18), 2.45, TNK_Z + 0.34), P(sg * (TNK_X - 0.25), 2.40, TNK_Z + 0.42)], 0.03, 8, M_STEEL) return b.finish(name) def build_hoops(sg, name): """Front-view guard hoops (R10): D-shaped tube from the pod's upper outboard corner to the upper outboard lug.""" b = B() pts = [Vector(p) for p in HOOP_PATH] + [Vector((1.12, -0.27))] fx = pchip([(i, p.x) for i, p in enumerate(pts)]); fz = pchip([(i, p.y) for i, p in enumerate(pts)]) n = len(pts) - 1 for cs in CLAMP_S: path = [P(sg * fx(n * k / 28), cs, fz(n * k / 28)) for k in range(29)] b.tube(path, 0.04, 10, M_STEEL) b.box(P(sg * 0.93, cs, 0.765), (0.16, 0.12, 0.07), M_DARK, bevel=0.012) # cap block (x 0.85..1.01) b.tube([P(sg * 0.93, cs, 0.74), P(sg * (fW(cs) - 0.12), cs, 0.70)], 0.035, 8, M_STEEL) # bracket to the hull shoulder return b.finish(name) # ============================================================================ TOOL HOUSING (port) def build_tool(): b = B(); x0 = TOOL_X b.box(P(x0, 1.76, -0.58), (0.46, 0.50, 0.32), M_DARK, bevel=0.03) b.box(P(x0 + 0.232, 1.76, -0.575), (0.014, 0.36, 0.23), M_STEEL, bevel=0.004) # outboard cover plate for ds in (-0.15, 0.15): for dz in (-0.085, 0.085): b.bolt(P(x0 + 0.239, 1.76 + ds, -0.575 + dz), (1, 0, 0), 0.012, 0.008) # oval bolted cover on the underside (bottom view: 0.58 x 0.50, centre s 1.99), kept thin c = P(x0 + 0.01, 1.99, -0.755) b.lathe(c, (0, 0, -1), [(-0.02, 1.0), (0.006, 1.0), (0.01, 0.97), (0.01, 0)], 28, M_STEEL, sx=0.25, sz=0.29) for k in range(10): a = 2 * pi * (k + 0.5) / 10 b.bolt(c + Vector((0.215 * cos(a), 0.25 * sin(a), -0.01)), (0, 0, -1), 0.012, 0.006) # feet (side view) with bolts b.box(P(x0 + 0.17, 1.76, -0.775), (0.10, 0.44, 0.05), M_DARK, bevel=0.012) for s in (1.60, 1.93): b.bolt(P(x0 + 0.22, s, -0.778), (1, 0, 0), 0.014, 0.008) # bolted collar + nose dome o = P(x0, 1.53, -0.59) b.lathe(o, (0, -1, 0), [(-0.02, 0.15), (0.05, 0.15), (0.055, 0.14), (0.055, 0.118)], 20, M_DARK) for k in range(6): a = 2 * pi * (k + 0.5) / 6 b.bolt(o + Vector((0.134 * cos(a), -0.055, 0.134 * sin(a))), (0, -1, 0), 0.009, 0.006) prof = [(0.05, 0.118)] + [(0.05 + 0.16 * sin(radians(d)), 0.118 * cos(radians(d))) for d in range(10, 90, 10)] + [(0.21, 0)] b.lathe(o, (0, -1, 0), prof, 24, M_STEEL) # motor block + round fitting facing outboard (r 0.075) b.box(P(x0 - 0.01, 2.13, -0.60), (0.44, 0.27, 0.32), M_DARK, bevel=0.05) o = P(x0 + 0.16, 2.125, -0.58) b.lathe(o, (1, 0, 0), [(0, 0.10), (0.068, 0.10), (0.075, 0.09), (0.075, 0.075), (0.10, 0.072), (0.10, 0.045), (0.088, 0.04), (0.088, 0)], 20, M_STEEL) # round bolted cap on the aft face (rear view: x 0.50, z -0.68 on the hull bottom line, r 0.11, bolts at 3 and 9) o = P(0.50, 2.265, -0.66) b.lathe(o, (0, 1, 0), [(-0.02, 0.11), (0.02, 0.11), (0.026, 0.10), (0.026, 0)], 20, M_STEEL) for sgn in (1, -1): b.bolt(o + Vector((sgn * 0.08, 0.026, 0)), (0, 1, 0), 0.012, 0.006) return b.finish("Tool_Housing", sharp=50) # ============================================================================ BELLY PANEL / SCOOP def build_belly(): b = B(); s0, s1, xw = 3.72, 4.52, 0.62 ss = [s0] + [s0 + (s1 - s0) * k / 8 for k in range(9)] + [s1] us = [-1.0] + [-1 + 2 * k / 14 for k in range(15)] + [1.0] rows = [] for i, s in enumerate(ss): thx = hull_th_at_x(s, xw, False); row = [] for j, u in enumerate(us): edge = i in (0, len(ss) - 1) or j in (0, len(us) - 1) row.append(hull_pt(s, -pi / 2 + (thx + pi / 2) * abs(u), 1 if u >= 0 else -1, -0.02 if edge else 0.014)) rows.append(row) b.grid(rows, M_STEEL) for s in (s0 + 0.05, s1 - 0.05): thx = hull_th_at_x(s, xw, False) for sg in (1, -1): p = hull_pt(s, -pi / 2 + (thx + pi / 2) * 0.88, sg, 0.014) nrm = Vector((p.x, 0, p.z - fZC(s))).normalized() b.bolt(p, nrm, 0.02, 0.01, M_DARK) return b.finish("Belly_Panel") def build_scoop(): """Raked mouth (side view): lower lip (s 5.84, z -0.71), upper lip (s 6.15, z -0.48); fairing blends into the belly at s 5.63..5.75. Rear view mouth 0.40 x 0.22, opening 0.27 x 0.10.""" b = B() lo = Vector((0, Y(5.84), -0.71)); up = Vector((0, Y(6.15), -0.48)) C = (lo + up) / 2; e = (up - lo); hh = e.length / 2; e.normalize() nrm = Vector((0, e.z, -e.y)) # mouth normal: aft and down X = Vector((1, 0, 0)); hw = 0.20 def ring(c, kx, kz, n_exp=0.5): return [c + X * (hw * kx * spow(cos(a), n_exp)) + e * (hh * kz * spow(sin(a), n_exp)) for a in [2 * pi * k / 28 for k in range(28)]] # fairing: from a small ring buried in the belly forward of the mouth to the mouth ring cf = Vector((0, Y(5.60), -0.50)) rows = [[cf]] for f in (0.25, 0.5, 0.75, 0.92, 1.0): c = cf.lerp(C, f) rows.append(ring(c, 0.45 + 0.55 * f, 0.20 + 0.80 * f)) b.grid(rows, M_HULL, close_v=True) # rounded lip ring b.grid([ring(C, 1.0, 1.0), ring(C + nrm * 0.018, 0.97, 0.955), ring(C + nrm * 0.02, 0.84, 0.80), ring(C + nrm * 0.008, 0.76, 0.70)], M_DARK, close_v=True) b.grid([ring(C + nrm * 0.008, 0.76, 0.70), ring(C - nrm * 0.09, 0.68, 0.45), [C - nrm * 0.09]], M_RUBBER, close_v=True) return b.finish("Tail_Scoop") # ============================================================================ PROPULSION DUCT_OUT = [(0.00, 0.355), (0.012, 0.378), (0.03, 0.388), (0.10, 0.390), (0.112, 0.398), (0.25, 0.425), (0.45, 0.450), (0.70, 0.464), (0.83, 0.465), (1.00, 0.455), (1.15, 0.435), (1.29, 0.400), (1.30, 0.385), (1.385, 0.378), (1.40, 0.360)] DUCT_IN = [(1.40, 0.330), (1.25, 0.350), (0.90, 0.380), (0.50, 0.375), (0.20, 0.350), (0.06, 0.332), (0.0, 0.340)] def duct_r_in(t): return pchip(sorted(DUCT_IN))(t) def build_propulsor(sg, name): b = B(); o = P(sg * DUCT_X, DUCT_S0, DUCT_Z); ax = Vector((0, 1, 0)); segs = 40 b.lathe(o, ax, DUCT_OUT[3:12], segs, M_DUCT) b.lathe(o, ax, DUCT_IN + DUCT_OUT[:4], segs, M_DARK) b.lathe(o, ax, DUCT_OUT[11:] + DUCT_IN[:1], segs, M_DARK) # band seam + 8 lengthwise seams at 45 deg (plan views: centre seam + bowed ones at 0.7 r) b.lathe(o, ax, [(0.82, 0.4675), (0.84, 0.4675)], segs, M_SEAM) f = pchip(DUCT_OUT[4:12]) for k in range(8): a = pi / 4 * k rows = [] for da in (-0.02, 0.02): rows.append([o + ax * t + Vector((cos(a + da), 0, sin(a + da))) * (f(t) + 0.0025) for t in [0.13 + (1.27 - 0.13) * i / 12 for i in range(13)]]) b.grid(rows, M_SEAM) # static centre body + flat hub cap (rear view) b.lathe(o, ax, [(0.63, 0.0), (0.63, 0.10), (1.22, 0.10), (1.24, 0.095), (1.24, 0.05), (1.27, 0.045), (1.27, 0)], 20, M_DARK) # stator vanes: wide at the hub, pointed at the ring (rear view star) for k in range(NSTATORS): a = 2 * pi * k / NSTATORS + pi / 2 rad = Vector((cos(a), 0, sin(a))); tan = Vector((-sin(a), 0, cos(a))) r0, r1 = 0.095, duct_r_in(1.1) + 0.01 c0 = o + ax * 1.08 + rad * r0; c1 = o + ax * 1.10 + rad * r1 rows = [] for (c, hl, ht) in ((c0, 0.13, 0.045), (c1, 0.10, 0.010)): rows.append([c - ax * hl, c + tan * ht, c + ax * hl, c - tan * ht]) b.grid(rows, M_VANE, close_v=True) return b.finish(name, origin=P(sg * DUCT_X, DUCT_S0 + 0.70, DUCT_Z)) def build_rotor(sg, name, parent): b = B(); org = P(sg * DUCT_X, ROTOR_S, DUCT_Z); ax = Vector((0, 1, 0)) o = P(sg * DUCT_X, DUCT_S0, DUCT_Z) b.lathe(o, ax, [(0.27, 0.0), (0.275, 0.03), (0.29, 0.045), (0.56, 0.098), (0.625, 0.10), (0.625, 0)], 20, M_STEEL) # spinner r_tip = duct_r_in(0.5) - 0.012 for k in range(NBLADES): # rest angle: blades sit behind the stator vanes in the rear view a = 2 * pi * k / NBLADES + pi / 2 rad = Vector((cos(a), 0, sin(a))); tan = Vector((-sin(a), 0, cos(a))) rows = [] for i in range(6): f = i / 5; r = 0.085 + (r_tip - 0.085) * f pitch = radians(58 - 30 * f) * sg # mirrored pitch port / starboard chord = 0.15 - 0.03 * f; th = 0.014 - 0.008 * f cdir = (ax * cos(pitch) + tan * sin(pitch)); ndir = (tan * cos(pitch) - ax * sin(pitch)) c = org + rad * r rows.append([c - cdir * chord / 2, c + ndir * th - cdir * chord * 0.1, c + cdir * chord / 2, c - ndir * th * 0.3]) rows = [[org + rad * 0.08]] + rows + [[org + rad * (r_tip + 0.001)]] b.grid(rows, M_DARK, close_v=True) return b.finish(name, origin=org, parent=parent) # spins about local Y def build_pylon(sg, name): b = B() def X(x): return sg * x # root fairing box against the tail flank (R11); inner face follows the narrower tail b.box(P(X(0.565), 5.735, 0.135), (0.47, 0.43, 0.53), M_HULL, bevel=0.07) b.box(P(X(0.805), 5.735, 0.135), (0.012, 0.30, 0.38), M_SEAM, bevel=0.003) # inset outline seen in plan/side # neck: tall at the box, curving out into a flat wedge that meets the duct lip secs = [(5.93, 0.62, 0.80, -0.10, 0.37), (5.98, 0.615, 0.805, -0.06, 0.33), (6.03, 0.595, 0.82, -0.01, 0.28), (6.08, 0.555, 0.85, 0.035, 0.225), (6.13, 0.50, 0.885, 0.07, 0.19), (6.20, 0.46, 0.90, 0.095, 0.165)] rows = [[P(X((secs[0][1] + secs[0][2]) / 2), secs[0][0] - 0.02, 0.135)]] for s, x0, x1, z0, z1 in secs: cx, cz, hx, hz = (x0 + x1) / 2, (z0 + z1) / 2, (x1 - x0) / 2, (z1 - z0) / 2 rows.append([P(X(cx + hx * spow(cos(a), 0.6)), s, cz + hz * spow(sin(a), 0.6)) for a in [2 * pi * k / 16 for k in range(16)]]) rows.append([P(X(0.68), 6.21, 0.13)]) b.grid(rows, M_HULL, close_v=True) b.box(P(X(0.29), 6.74, 0.13), (0.30, 0.11, 0.13), M_HULL, bevel=0.025) # aft strut tail stub -> duct # feed pipe from the lower tank's aft end to the pylon root (plan views) b.tube([P(X(TNK_X), 4.86, TNK_Z), P(X(0.96), 4.98, TNK_Z + 0.03), P(X(0.80), 5.40, -0.16), P(X(0.74), 5.56, -0.06)], 0.04, 10, M_STEEL) return b.finish(name) # ============================================================================ build everything objs = {} objs["Hull"] = build_hull() objs["Hull_Seams"] = build_seams() objs["Belly_Panel"] = build_belly() objs["Nose_Lens"] = build_nose_lens() objs["Chin_Sensor"] = build_chin() objs["Cockpit_Interior"] = build_interior() objs["Canopy_Coaming"] = build_coaming() objs["Canopy_Lid"] = build_canopy_lid() objs["Canopy_Glass"] = build_glass(objs["Canopy_Lid"]) objs["Hatch_Collar"] = build_hatch() objs["Hatch_Lid"] = build_lid() objs["Tool_Housing"] = build_tool() objs["Tail_Scoop"] = build_scoop() for sg, sn in ((1, "Port"), (-1, "Stbd")): objs["Tank_Upper_" + sn] = build_pod(sg, "Tank_Upper_" + sn) objs["Tank_Lower_" + sn] = build_tank(sg, "Tank_Lower_" + sn) objs["Guard_Hoops_" + sn] = build_hoops(sg, "Guard_Hoops_" + sn) objs["Pylon_" + sn] = build_pylon(sg, "Pylon_" + sn) pr = build_propulsor(sg, "Propulsor_" + sn) objs["Propulsor_" + sn] = pr objs["Rotor_" + sn] = build_rotor(sg, "Rotor_" + sn, pr) bpy.context.view_layer.update() def s75v2_stats(): out = {}; tot = 0 for ob in coll.objects: if ob.type == 'MESH': n = sum(len(p.vertices) - 2 for p in ob.data.polygons); out[ob.name] = n; tot += n out["TOTAL"] = tot return out def s75v2_bounds(names=None): lo = Vector((1e9,) * 3); hi = Vector((-1e9,) * 3) for ob in coll.objects: if ob.type == 'MESH' and (names is None or ob.name in names): for v in ob.data.vertices: w = ob.matrix_world @ v.co for i in range(3): lo[i] = min(lo[i], w[i]); hi[i] = max(hi[i], w[i]) return [round(v, 4) for v in lo], [round(v, 4) for v in hi] def s75v2_canopy_swing(angles=(0, 5, 15, 30, 45, 60), restore=True): """Rotate Canopy_Lid (+ child glass) about its local X and count triangle overlaps against the static parts.""" from mathutils.bvhtree import BVHTree lid = bpy.data.objects["Canopy_Lid"]; movers = [lid, bpy.data.objects["Canopy_Glass"]] static = [o for o in coll.objects if o.type == 'MESH' and o not in movers and not o.name.startswith(("Rotor", "Propulsor", "Pylon", "Tail", "Belly", "Chin", "Nose", "Tool"))] def tree(ob): me = ob.data; me.calc_loop_triangles() vs = [ob.matrix_world @ v.co for v in me.vertices] return BVHTree.FromPolygons(vs, [tuple(t.vertices) for t in me.loop_triangles], epsilon=0.0) st = {o.name: tree(o) for o in static} out = {} for a in angles: lid.rotation_euler = (-radians(a), 0, 0) # negative local-X rotation lifts the bow end of the lid bpy.context.view_layer.update() hits = {} for m in movers: tm = tree(m) for nm, ts in st.items(): n = len(tm.overlap(ts)) if n: hits[m.name + " x " + nm] = n zmin = min((m.matrix_world @ v.co).z for m in movers for v in m.data.vertices) ymax = max((m.matrix_world @ v.co).y for m in movers for v in m.data.vertices) out[a] = {"overlaps": hits, "aft_most_s": round(ymax + L_HALF, 3), "lowest_z": round(zmin, 3)} if restore: lid.rotation_euler = (0, 0, 0); bpy.context.view_layer.update() return out bpy.app.driver_namespace["s75v2_stats"] = s75v2_stats bpy.app.driver_namespace["s75v2_bounds"] = s75v2_bounds bpy.app.driver_namespace["s75v2_canopy_swing"] = s75v2_canopy_swing