# Mako: everything on the hull (exec'd by build_mako.py after mk_hull.py). Quads only: lofts and foils with three-quad end # caps, revolved parts with Coons caps, cube spheres, ladder patches (mk_topo.py); parts that sink into the hull are pruned. # Numbers are reference pixels from mk_measure.py unless they are in metres. from mathutils.bvhtree import BVHTree att = MB("_attached", [PAINT, DARK, GRAPH, FIN, SENSOR, STEEL, RUBBER, YELLOW, LENS, LAMP, JAW]) hull.bm.normal_update() HBVH = BVHTree.FromBMesh(hull.bm) VIEW = {"top": (lambda u, v: Vector((u, v, 5.0)), Vector((0, 0, -1))), "bot": (lambda u, v: Vector((u, v, -5.0)), Vector((0, 0, 1))), "sideP": (lambda u, v: Vector((5.0, u, v)), Vector((-1, 0, 0))), "sideS": (lambda u, v: Vector((-5.0, u, v)), Vector((1, 0, 0)))} def proj(view, u, v): """(u, v) of a view's 2D coordinates (metres) -> (point on the hull, outward normal)""" of, d = VIEW[view] hit, nrm, fi, dist = HBVH.ray_cast(of(u, v), d) if hit is None: return None, None n = normal_at(hit) if abs(nrm.dot(normal_at(hit))) > 0.5 else nrm.normalized() if n.dot(-d) < 0: n = -n return hit, n def region(outline, axis): o = 1 - axis ts = [p[axis] for p in outline] t0, t1 = min(ts), max(ts) pad = (t1 - t0) * 0.02 def lo_hi(t): xs = [] n = len(outline) for i in range(n): a, b = outline[i], outline[(i + 1) % n] if (a[axis] - t) * (b[axis] - t) <= 0 and a[axis] != b[axis]: xs.append(a[o] + (b[o] - a[o]) * (t - a[axis]) / (b[axis] - a[axis])) return (min(xs), max(xs)) if xs else (0.0, 0.0) return lo_hi, t0 + pad, t1 - pad def patch(mb, outline, view, mat, axis=0, lift=0.004, bulge=0.0, rim=None, rim_r=0.008, nt=8, nacross=3, cell=0.035): """a thin plate on the hull over a 2D outline, as a ladder of quads (no fan); optional rim tube round it""" lo_hi, t0, t1 = region(outline, axis) wmax = max(lo_hi(t0 + (t1 - t0) * i / 8)[1] - lo_hi(t0 + (t1 - t0) * i / 8)[0] for i in range(9)) nt = max(nt, int(math.ceil((t1 - t0) / cell))) nacross = max(nacross, int(math.ceil(wmax / cell))) def to3d(t, s): uv = (t, s) if axis == 0 else (s, t) p, n = proj(view, *uv) return p def nrm(p): _, n = proj(view, *((p.x, p.y) if view in ("top", "bot") else (p.y, p.z))) return n if n is not None else normal_at(p) ladder_patch(mb, lo_hi, t0, t1, to3d, mat, nt=nt, nacross=nacross, lift=lift, bulge=bulge, normal=nrm) if rim is not None: chain = [(t0 + (t1 - t0) * i / nt, lo_hi(t0 + (t1 - t0) * i / nt)) for i in range(nt + 1)] loop2 = [(t, lh[0]) for t, lh in chain] + [(t, lh[1]) for t, lh in reversed(chain)] pts = [] for (t, s) in loop2 + [loop2[0]]: uv = (t, s) if axis == 0 else (s, t) p, n = proj(view, *uv) pts.append(p + n * rim_r * 0.35) mb.sweep(pts, rim_r, rim, sides=6, caps=False) def band_outline(pts2, half): """a polyline (2D) thickened to a closed band of half width `half` with round ends""" n = len(pts2) L, R = [], [] for j in range(n): a = Vector(pts2[max(j - 1, 0)]); b = Vector(pts2[min(j + 1, n - 1)]) t = (b - a).normalized() nn = Vector((-t.y, t.x)) p = Vector(pts2[j]) L.append(p + nn * half); R.append(p - nn * half) def cap(c, t, sgn): nn = Vector((-t.y, t.x)) return [c + (nn * math.cos(a) + t * math.sin(a) * sgn) * half for a in [math.pi * q / 4 for q in (1, 2, 3)]] t_end = (Vector(pts2[-1]) - Vector(pts2[-2])).normalized() t_beg = (Vector(pts2[1]) - Vector(pts2[0])).normalized() out = L + cap(Vector(pts2[-1]), t_end, 1) + R[::-1] + cap(Vector(pts2[0]), -t_beg, 1) return [(p.x, p.y) for p in out] def fin_from(le, te, tip, th0, th1, mat, n=14, s0=-0.06, s1=0.975, side_normal=None): """a triangular fin: root chord le..te, both edges running to the tip; lens sections, quads, three-quad end caps""" pl = (te - le).cross(tip - le).normalized() if side_normal is not None and pl.dot(side_normal) < 0: pl = -pl secs = [] for j in range(n + 1): s = s0 + (s1 - s0) * j / n a = le + (tip - le) * s b = te + (tip - te) * s c = (a + b) * 0.5 ch = (b - a).length secs.append((c, (b - a).normalized(), pl, ch, th0 + (th1 - th0) * max(0.0, s))) foil(att, secs, mat) return secs def ysec(le_tab, te_tab, z): """the chord's ends at height z (side view) from a leading edge and a trailing edge polyline [(img x, img y)]""" def at(tab): pts = sorted(tab, key=lambda p: -p[1]) # by height for (xa, va), (xb, vb) in zip(pts, pts[1:]): za, zb = Zi(va), Zi(vb) if min(za, zb) - 1e-9 <= z <= max(za, zb) + 1e-9 and za != zb: return Yi(xa + (xb - xa) * (z - za) / (zb - za)) p = pts[0] if abs(z - Zi(pts[0][1])) < abs(z - Zi(pts[-1][1])) else pts[-1] return Yi(p[0]) return at(le_tab), at(te_tab) def vfin(le_tab, te_tab, z0, z1, th0, th1, mat, n=12, root_extra=0.12, down=False): """a fin in the centre plane: chords at heights z0 (root, extended into the hull) .. z1 (tip)""" secs = [] for j in range(n + 1): t = j / n z = z0 + (z1 - z0) * t a, b = ysec(le_tab, te_tab, z) if j == 0: # carry the root's chord down into the hull zr = z0 + (root_extra if down else -root_extra) secs.append((Vector((0, (a + b) / 2, zr)), Vector((0, 1, 0)), Vector((1, 0, 0)), abs(b - a), th0)) if j == n: # a short rounded tip, not a needle ch = max(abs(b - a), 0.035) else: ch = abs(b - a) secs.append((Vector((0, (a + b) / 2, z)), Vector((0, 1, 0)), Vector((1, 0, 0)), ch, th0 + (th1 - th0) * t)) foil(att, secs, mat) # ------------------------------------------------------------------ side pods (v03): big water-jet bodies standing off the narrowed hull # SIDE 318-488 x 705-795, TOP out to 140 px with a black slot along the top-inner side, REAR big round nozzles under the roof's # shoulders, FRONT dark rounded fronts with ribs flanking the dome. Rounded-rectangle sections (superellipse) easing to a circle # at the nozzle; an intake throat with a ribbed grille in front; the slot as a recessed black channel along the top-inner side. PODS = {} def pod(side): x0, x1, lat, yc, hw, hh = POD["x0"], POD["x1"], POD["lat"], POD["y"], POD["hw"], POD["hh"] cx, cz = side * lat * S, Zi(yc) HW, HH = hw * S, hh * S NP = 24 sy0, sy1 = Yi(POD_SLOT[0]), Yi(POD_SLOT[1]) def slot_w(c, s_): """1 on the pod's top and inner side (where TOP draws the black slot: the inner 60 % of its width), 0 outboard and below""" return smoothstep(0.62, 0.30, side * c) * smoothstep(-0.45, -0.10, s_) def ring(x, k_w, k_h, dy=0.0, n=2.6, slot=0.0): y = Yi(x) + dy out = [] for q in range(NP): a = 2 * math.pi * (q + 0.5) / NP c, s_ = math.cos(a), math.sin(a) px = HW * k_w * math.copysign(abs(c) ** (2 / n), c) pz = HH * k_h * math.copysign(abs(s_) ** (2 / n), s_) sl = slot * slot_w(c, s_) out.append(Vector((cx + px * (1 - sl * 0.9), y, cz + pz * (1 - sl * 0.6)))) return out def slot_at(x): y = Yi(x) return smoothstep(sy0 - 0.04, sy0 + 0.06, y) * (1 - smoothstep(sy1 - 0.06, sy1 + 0.04, y)) * 0.10 xs_body = [x0 + 3, x0 + 8, 332, 345, 365, 390, 415, 436, 450, 465, 476, x1 - 4, x1] # TOP: the pod's outer edge bulges to 140 px at x 380-400 and draws in toward both ends kw = {x0 + 3: 0.80, x0 + 8: 0.86, 332: 0.92, 345: 0.96, 365: 0.99, 436: 0.97, 450: 0.95} rings, xs_used = [], [] for x in xs_body: t = (x - 440) / (x1 - 440) if x > 440 else 0.0 k_w = kw.get(x, 1.0) * (1.0 - 0.12 * t) k_h = (0.5 + 0.5 * kw.get(x, 1.0)) * (1.0 - 0.25 * t) n = 2.6 - 0.6 * t rings.append(ring(x, k_w, k_h, n=n, slot=slot_at(x))) xs_used.append(x) # intake: the lip turns in, a short throat, then the grille face; nozzle: the lip turns in to a round throat lip = [ring(x0, 0.80, 0.78, 0.010), ring(x0, 0.72, 0.70, 0.05), ring(x0, 0.70, 0.68, 0.17)] noz = [ring(x1, 0.80, 0.68, -0.012, n=2.0), ring(x1, 0.74, 0.62, -0.18, n=2.0)] mats = [DARK, DARK, GRAPH] for j in range(len(rings) - 1): xm = 0.5 * (xs_used[j] + xs_used[j + 1]) mats.append(DARK) mats += [GRAPH, DARK] allr = lip[::-1] + rings + noz # the slot: the faces of the top and inner side between the slot's ends are black (TOP's black intake slot), the rest # dark grey; one grid, drawn in two passes that share its vertices (welded when the mesh is built) band_x = [None] * 3 + xs_used + [None, None] def is_slot(i, k): if i < 3 or i >= 3 + len(xs_used) - 1: return False xa, xb = band_x[i], band_x[i + 1] if not (POD_SLOT[0] <= xa and xb <= POD_SLOT[1] + 1e-6): return False am = 2 * math.pi * (k + 1.0) / NP return slot_w(math.cos(am), math.sin(am)) > 0.5 grid_rings(att, allr, mats, closed_u=True, skip=is_slot, expect=(5, 18, (0, 0, -1))) jb = next(i for i in range(3, 3 + len(xs_used) - 1) if POD_SLOT[0] <= band_x[i] and band_x[i + 1] <= POD_SLOT[1]) grid_rings(att, allr, SENSOR, closed_u=True, skip=lambda i, k: not is_slot(i, k), expect=(jb, 5, (0, 0, 1))) # grille in the intake: a dark face with three horizontal ribs; turbine face in the nozzle: hub, two rings, six vanes for r_, outward, mat in ((lip[-1], -1, SENSOR), (noz[-1], 1, SENSOR)): bm2 = bmesh.new() vs = [bm2.verts.new(p) for p in r_] coons_cap(bm2, vs if outward < 0 else vs[::-1]) bm2.normal_update() if sum(f.normal.y for f in bm2.faces) * outward < 0: bmesh.ops.reverse_faces(bm2, faces=bm2.faces[:]) att.merge(bm2, mat) yg = Yi(x0) + 0.14 for dz in (-0.12, 0.0, 0.12): qbox(att, (2 * HW * 0.70, 0.025, 0.022), (cx, yg, cz + dz), GRAPH, bevel=0.006) yn = Yi(x1) - 0.15 qsphere(att, Vector((cx, yn - 0.01, cz)), (0.07, 0.06, 0.07), GRAPH, cuts=2) for rr in (0.10, 0.155): att.sweep([Vector((cx + rr * math.cos(2 * math.pi * q / 24), yn, cz + rr * math.sin(2 * math.pi * q / 24))) for q in range(25)], 0.010, STEEL, sides=6, caps=False) for q in range(6): a = 2 * math.pi * q / 6 + 0.3 qbox(att, (0.016, 0.03, 0.15), Vector((cx + 0.125 * math.cos(a), yn + 0.01, cz + 0.125 * math.sin(a))), GRAPH, rot=(0, -a + math.pi / 2, 0)) # the dark oval the SIDE view draws on the pod's outer side behind the collar (x 340-400, y 740-790): a recessed intake # as a ladder plate on the pod's own surface, with a graphite rim a_out = 0.0 if side > 0 else math.pi def pod_pt(y, a, lift=0.0): c, s_ = math.cos(a), math.sin(a) px = HW * math.copysign(abs(c) ** (2 / 2.6), c) pz = HH * math.copysign(abs(s_) ** (2 / 2.6), s_) nrm = Vector((px / HW ** 2, 0.0, pz / HH ** 2)).normalized() return Vector((cx + px, y, cz + pz)) + nrm * lift oy0, oy1 = Yi(342.0), Yi(402.0) oz_c, oa = Zi(764.0) - cz, 0.42 # the oval's centre height over the pod's, its half span in angle a_c = math.asin(max(-0.9, min(0.9, oz_c / HH))) * (1 if side > 0 else -1) def lo_hi(t): u = (t - oy0) / (oy1 - oy0) * 2 - 1 h = oa * math.sqrt(max(0.0, 1 - u * u)) h = max(h, 0.15 * oa) return (a_out + a_c - h, a_out + a_c + h) ladder_patch(att, lo_hi, oy0 + 0.01, oy1 - 0.01, lambda t, s_: pod_pt(t, s_), SENSOR, nt=8, nacross=4, lift=0.004, normal=lambda p: Vector((p.x - cx, 0, (p.z - cz) * (HW / HH) ** 2)).normalized()) rim = [] for k in range(25): th = 2 * math.pi * k / 24 y = 0.5 * (oy0 + oy1) + 0.5 * (oy1 - oy0) * math.cos(th) rim.append(pod_pt(y, a_out + a_c + oa * math.sin(th) * (1 if side > 0 else -1), 0.006)) att.sweep(rim, 0.008, GRAPH, sides=6, caps=False) PODS[side] = (cx, cz) for side in (1, -1): pod(side) # ------------------------------------------------------------------ big fins (v03): rooted along the pods, rising aft, drooped about 60 deg # The plane through the root's two ends and the tip; the trailing edge drops from the root's rear end to a kink, then runs to # the tip, as SIDE draws it; lens sections from inside the pod and hull out to a short rounded tip. FIN_GEOM = {} def fin_v3(side): A = Vector((side * FIN_A[1], Yi(FIN_A[0]), Zi(FIN_A[2]))) B = Vector((side * FIN_B[1], Yi(FIN_B[0]), Zi(FIN_B[2]))) T = Vector((side * FIN_TIP[1] * S, Yi(FIN_TIP[0]), Zi(FIN_TIP[2]))) nrm = (B - A).cross(T - A).normalized() if nrm.dot(Vector((side, 0, 1))) < 0: nrm = -nrm ky, kz = Yi(FIN_K[0]), Zi(FIN_K[1]) kx = A.x - (nrm.y * (ky - A.y) + nrm.z * (kz - A.z)) / nrm.x K = Vector((kx, ky, kz)) te = [B, K, T] tl = [0.0, (K - B).length, (K - B).length + (T - K).length] def te_at(u): d = u * tl[-1] if d <= tl[1]: return B.lerp(K, d / tl[1]) return K.lerp(T, (d - tl[1]) / (tl[2] - tl[1])) secs = [] N = 18 for j in range(N + 1): u = -0.06 + 1.035 * j / N # from inside the pod and the hull to just short of the tip le = A + (T - A) * u tr = te_at(u) if u >= 0 else B + (B - T) * (-u) ch = (tr - le).length th = 0.16 * (1 - max(0.0, u)) + 0.026 * max(0.0, u) cdir = (tr - le).normalized() nd = (nrm - nrm.dot(cdir) * cdir).normalized() if j == N: ch = max(ch, 0.05) secs.append(((le + tr) * 0.5, cdir, nd, ch, th)) foil(att, secs, FIN) FIN_GEOM[side] = dict(A=A, B=B, K=K, T=T, n=nrm) for side in (1, -1): fin_v3(side) # ------------------------------------------------------------------ dorsal fin, tail fin (orange) and the ventral stern fin (grey) vfin(DORSAL_LE, DORSAL_TE, Zi(670.0), Zi(549.0), 0.085, 0.018, PAINT, n=12) vfin(TAIL_LE, TAIL_TE, Zi(760.0), Zi(659.0), 0.060, 0.015, PAINT, n=9) VLE = [VENTRAL[0], VENTRAL[1], VENTRAL[2], VENTRAL[3]] VTE = [VENTRAL[3], VENTRAL[4], VENTRAL[5], VENTRAL[6]] vfin(VLE, VTE, Zi(850.0), Zi(940.0), 0.055, 0.014, FIN, n=9, down=True) # ------------------------------------------------------------------ the jaw: two curved grooves between its slats, two prongs per side for sg, vw in ((1, "sideP"), (-1, "sideS")): for g in GROOVES: pts2 = [(Yi(x), Zi(v)) for x, v in g] patch(att, band_outline(pts2, 0.020), vw, SENSOR, axis=0, lift=0.003, rim=GRAPH, rim_r=0.007, nt=10, nacross=2) for side in (1, -1): # v03: one fang a side (FRONT: 1163/1210), rooted in the jaw (rx, ry), (tx, ty), lat = PRONG p_side = Vector((side * lat, Yi(rx), Zi(ry))) # where SIDE shows it leave the jaw tip = Vector((side * (lat - 0.015), Yi(tx), Zi(ty))) ax = (tip - p_side).normalized() p_in = p_side + Vector((side * 0.02, -0.10, 0.22)) # well inside the jaw def frame_(a): cd = Vector((0, 1, 0)); cd = (cd - cd.dot(a) * a).normalized() nd = Vector((side, 0, 0)); nd = (nd - nd.dot(a) * a - nd.dot(cd) * cd).normalized() return cd, nd cd0, nd0 = frame_((p_side - p_in).normalized()) cd, nd = frame_(ax) secs = [(p_in, cd0, nd0, 0.13, 0.10)] for j in range(10): t = j / 9 c = p_side.lerp(tip, t) ch = 0.13 - 0.10 * smoothstep(0.0, 1.0, t) # a tooth, about as wide seen from the front as from the side secs.append((c, cd, nd, ch if j < 9 else 0.03, (0.10 - 0.075 * smoothstep(0.0, 1.0, t)) if j < 9 else 0.025)) foil(att, secs, JAW) # the dark keel tooth under the jaw's centre (FRONT 1180-1195, 445-485): a short blade in the centre plane kx0, kx1, kdep = KEEL_TOOTH qsecs = [] for j in range(6): # sections across the blade's height, chord along y t = j / 5 yb, ye = Yi(kx0) + 0.02 * t, Yi(kx1) - 0.25 * t zc = hull_z_at(0.0, 0.5 * (yb + ye), top=False) + 0.05 - (kdep + 0.05) * t qsecs.append((Vector((0, 0.5 * (yb + ye), zc)), Vector((0, 1, 0)), Vector((1, 0, 0)), max(0.04, ye - yb), 0.045 - 0.02 * t)) foil(att, qsecs, GRAPH) # ------------------------------------------------------------------ belly pack (SIDE: the dark box under the rear bay), ribbed, on two skids bx0, bx1, bhw, bbot = BELLY byc, blen = Yi((bx0 + bx1) / 2), (bx1 - bx0) * S bz0 = Zi(bbot) bz1 = hull_z_at(0.0, byc, top=False) + 0.10 qbox(att, (2 * bhw * S, blen, bz1 - bz0), (0, byc, (bz0 + bz1) / 2), DARK, bevel=0.04) for t in (0.18, 0.5, 0.82): qbox(att, (2 * bhw * S + 0.03, 0.035, 0.08), (0, Yi(bx0) + blen * t, bz0 + 0.12), GRAPH, bevel=0.01) for side in (1, -1): qbox(att, (0.05, blen + 0.10, 0.045), (side * (bhw * S - 0.06), byc, bz0 - 0.02), RUBBER, bevel=0.015) # ------------------------------------------------------------------ stern housing: the tail's last section carried on, with the thruster duct sx0, sx1 = STERN_HOUSING yE = Y_TAIL hrows = [] for t, k in ((0.0, 1.02), (0.25, 1.04), (0.7, 1.0), (1.0, 0.94)): y = Yi(sx0) + (Yi(sx1) - Yi(sx0)) * t yy = min(y, Y_TAIL) row = [] zm = ZMf(Y_TAIL) for q in range(16): a = 2 * math.pi * (q + 0.5) / 16 p = sec(Y_TAIL, a if a <= math.pi / 2 or a >= 3 * math.pi / 2 else a) row.append(Vector((p.x * k, y, zm + (p.z - zm) * k))) hrows.append(row) grid_rings(att, hrows, DARK, closed_u=True, expect=(1, 4, (0, 0, 1))) bm2 = bmesh.new() vs = [bm2.verts.new(p) for p in hrows[-1]] coons_cap(bm2, vs[::-1]) bm2.normal_update() if sum(f.normal.y for f in bm2.faces) < 0: bmesh.ops.reverse_faces(bm2, faces=bm2.faces[:]) att.merge(bm2, GRAPH) TH_C = Vector((0, Yi(sx1), ZMf(Y_TAIL) - 0.02)) # thruster axis on the housing's aft face def duct_ring(r, dy, n=24): return [TH_C + Vector((r * math.cos(2 * math.pi * q / n), dy, r * math.sin(2 * math.pi * q / n))) for q in range(n)] # one profile, outer wall -> lip -> inner wall, skinned in order so the inner wall faces the axis grid_rings(att, [duct_ring(0.150, -0.01), duct_ring(0.150, 0.11), duct_ring(0.138, 0.125), duct_ring(0.122, 0.11), duct_ring(0.122, -0.03)], [GRAPH, GRAPH, STEEL, DARK], closed_u=True, expect=(0, 6, (0, 0, 1))) qcyl(att, 0.045, 0.05, TH_C + Vector((0, 0.005, 0)), GRAPH, rot=(math.pi / 2, 0, 0), segs=12) # hub stator, ends 3 cm before the rotor for a in range(4): ang = math.pi / 4 + a * math.pi / 2 qbox(att, (0.012, 0.03, 0.115), TH_C + Vector((0.07 * math.cos(ang), 0.02, 0.07 * math.sin(ang))), GRAPH, rot=(0, -ang + math.pi / 2, 0)) # small fittings on the housing: a sensor lens above, a light below qsphere(att, Vector((0, Yi(sx1) - 0.02, ZMf(Y_TAIL) + 0.22)), (0.045, 0.03, 0.045), SENSOR, cuts=2) qcyl(att, 0.035, 0.02, Vector((0, Yi(sx1) + 0.005, ZMf(Y_TAIL) - 0.22)), LAMP, rot=(math.pi / 2, 0, 0), segs=12) # ------------------------------------------------------------------ canopy frames (v03, as TOP and SIDE draw them): an arc across the front # (a circle in plan), a belt along each side at the widest line, two orange rails over the top that rise toward the collar and # carry the fore holder clips, a thin seam along the nose's top. Bars sit in the glass (which is sunk 22 mm) and stand proud. frames = MB("SM_Mako_Frames", [GRAPH, PAINT]) def bar(pts3, half_w, hb, ht, mat): """a bar along points on the hull's surface: half width half_w, from hb (into the glass) to ht (above the skin); hb/ht may be lists (one per point)""" nP = len(pts3) HB = hb if isinstance(hb, (list, tuple)) else [hb] * nP HT = ht if isinstance(ht, (list, tuple)) else [ht] * nP rows = [] for (w, which) in ((-half_w, 0), (-half_w * 0.8, 1), (half_w * 0.8, 1), (half_w, 0)): row = [] for j, p in enumerate(pts3): t = (pts3[min(j + 1, nP - 1)] - pts3[max(j - 1, 0)]).normalized() nrm = normal_at(p) bn = nrm.cross(t).normalized() row.append(p + bn * w + nrm * (HT[j] if which else HB[j])) rows.append(row) grid_rings(frames, [list(r) for r in zip(*rows)], mat, closed_u=False, expect=(nP // 2, 1, tuple(normal_at(pts3[nP // 2])))) Y_FR_END = Y_C0 + 0.004 # the frames run into the collar's front face # belts along the widest line for sg in (1, -1): ys_ = [Yi(BELT_X0) + (Y_FR_END - Yi(BELT_X0)) * t for t in [j / 30 for j in range(31)]] bar([sec(y, 0.0 if sg > 0 else math.pi) for y in ys_], 0.018, -0.024, 0.010, GRAPH) # the arc across the front: a circle in plan from belt to belt over the top HCY, HR = Yi(HOOP["cx"]), HOOP["r"] * S def hoop_pt(phi): lat, y = HR * math.sin(phi), HCY - HR * math.cos(phi) W = Wf(y) lat = max(-W * 0.999, min(W * 0.999, lat)) return Vector((lat, y, hull_z_at(lat, y, True))) phis = [] for j in range(181): phi = -math.pi / 2 + math.pi * j / 180 lat, y = HR * math.sin(phi), HCY - HR * math.cos(phi) if abs(lat) < Wf(y) * 0.985: phis.append(phi) hp = [hoop_pt(phis[0] + (phis[-1] - phis[0]) * t) for t in [j / 40 for j in range(41)]] bar(hp, 0.020, -0.024, 0.012, GRAPH) # two rails over the top: orange, from the arc to the collar, rising from 12 mm to 60 mm proud RAIL_LAT = MERIDIAN_LAT * S RAILS = {} for sg in (1, -1): yh = HCY - math.sqrt(max(0.0, HR * HR - RAIL_LAT * RAIL_LAT)) ys_ = [yh + (Y_FR_END - yh) * t for t in [j / 30 for j in range(31)]] pts = [Vector((sg * RAIL_LAT, y, hull_z_at(RAIL_LAT, y, True))) for y in ys_] hts = [0.012 + 0.048 * smoothstep(0.0, 1.0, (y - yh) / (Y_FR_END - yh)) ** 1.3 for y in ys_] bar(pts, 0.028, -0.024, hts, PAINT) RAILS[sg] = (ys_, pts, hts) # the nose's top seam ys_ = [Yi(NOSE_SEAM[0]) + (Yi(NOSE_SEAM[1]) - Yi(NOSE_SEAM[0])) * t for t in [j / 16 for j in range(17)]] bar([sec(y, math.pi / 2) for y in ys_], 0.007, -0.024, 0.003, GRAPH) # the rear bay window's two frame posts for sg, vw in ((1, "sideP"), (-1, "sideS")): for xm in WIN_MULLIONS: y = Yi(xm) zlo, zhi = side_span(WIN["poly"], y) zs = [zlo - 0.01 + (zhi - zlo + 0.02) * t for t in [j / 8 for j in range(9)]] bar([on_side(sg, y, z) for z in zs], 0.012, -0.024, 0.006, GRAPH) # ------------------------------------------------------------------ the dorsal spine (TOP: the strip from the collar to its point), raised rx0, rx1, rhw, rht = DORSAL_RIDGE SP_W = table_f(SPINE, Yi, lambda v: v * S) secs = [] for t in [j / 24 for j in range(25)]: x_ = rx0 + (rx1 - rx0) * t y = Yi(x_) w = max(0.012, SP_W(y) + 0.02) z0 = hull_z_at(0.0, y) - 0.03 h = rht * smoothstep(0.0, 0.06, t) * (1 - smoothstep(0.85, 1.0, t)) z1 = hull_z_at(0.0, y) + H_SHELL + max(0.004, h) secs.append((y, w, z0, z1, min(0.015, w * 0.4))) loft(att, secs, PAINT, xf=Matrix.Rotation(math.pi / 2, 4, 'Z')) # ------------------------------------------------------------------ lamps: a pair low on the jaw's front, looking ahead and down LAMPS = [] for side in (1, -1): y = Yi(178.0) z = CHINz(y) - 0.20 p = on_side(side, y, z) n = normal_at(p) aim = n # flush with the jaw: the bezel sits as a ring round the lens rot = Vector((0, 0, 1)).rotation_difference(aim).to_euler() revolve(att, [(0.062, -0.025), (0.062, 0.010), (0.055, 0.018), (0.046, 0.010)], [GRAPH, GRAPH, GRAPH], segs=16, xf=Matrix.Translation(p) @ rot.to_matrix().to_4x4(), cap0=False, cap1=True, cap1_mat=LENS) qcyl(att, 0.040, 0.004, p + aim * 0.006, LAMP, rot=tuple(rot), segs=16) LAMPS.append((p + aim * 0.03, (n + Vector((0, -1.6, -0.3))).normalized())) # the beam looks ahead and down REPORT["stage"] = "fittings"