"""Aethel-9 parts, built in world metres from the sheet's pixel numbers in ae_lib. Each function returns Mesh objects; moving parts come back with their pivot (world) so the Blender build can set origins. Port parts are built at +X and mirrored for starboard.""" import numpy as np import ae_lib as L from ae_geo import Mesh, loft, revolve, rrect, ring3, circle, frame, sweep, box, grid, coons_cap, arclen, quad_ring S = L.S EX, EY, EZ = np.array([1.0, 0, 0]), np.array([0, 1.0, 0]), np.array([0, 0, 1.0]) def W(X, v, y): return L.to_world(X, v, y) def ring_rr(X, v, y, w, h, r, n_side=2, n_corner=4): """Rounded rectangle section (px sizes) in the plane v = const, centred at (X, y): ex = +X, ey = up.""" rr = rrect(0, 0, w * S, h * S, r * S, n_side, n_corner) return ring3(rr, W(X, v, y), EX, EZ) def ring_circ(X, v, y, r, n): """Circle section matching ring_rr's start (the bottom-right 45 deg point) and direction.""" th = np.deg2rad(-45) + np.linspace(0, 2 * np.pi, n, endpoint=False) c = W(X, v, y) return c + (r * S) * (np.cos(th)[:, None] * EX + np.sin(th)[:, None] * EZ) def lerp_ring(a, b, t): return (1 - t) * a + t * b # --------------------------------------------------------------------------------------------------------------------- def nacelle(): """Lower thruster nacelle (port): ducted fan intake at the front, the box body, a tail under the upper pod.""" n = L.NACELLE m = Mesh() X, yc, r = n['X'], n['y'], n['r'] ax = EY # duct (open revolve; the bore ends on a back plate) prof = [(31.0, 340.0), (31.2, 327.0), (31.3, 324.0), (31.5, 318.0), (32.5, 314.0), (34.5, 312.2), (37.0, 312.0), (39.0, 313.2), (40.0, 316.0), (40.0, 318.5), (40.4, 319.0), (40.4, 321.5), (40.0, 322.0), (40.0, 325.5), (40.4, 326.0), (40.4, 328.5), (40.0, 329.0), (40.0, 345.0)] rings = [circle(W(X, vv, yc), ax, rr * S, 48, np.deg2rad(-45), EX, EZ) for rr, vv in prof] # material per band: the lip and two rings glow (FRONT: purple rims; SIDE: two purple rings round the intake) P = np.array(rings) rowm = {0: 'Metal_Dark', 1: 'GlowFanHot', 2: 'Metal_Dark', 3: 'GlowFan', 4: 'GlowFan', 5: 'GlowFan', 10: 'GlowStrip', 14: 'GlowStrip'} grid(m, P, 'Paint_Graphite', closed_u=True, uv_scale=2.0, row_mats=[rowm.get(i, 'Paint_Graphite') for i in range(len(P) - 1)]) # concept: a hot ring deep in the bore, # a blue lip, two blue-violet rings round the outside # the module's outboard flank (SIDE x 375-455, y 298-348): a small control panel and a slatted grille box(m, W(X + 40.4, 386.0, 326.0), (18 * S, 26 * S, 2.4 * S), 'Frame', r=2 * S, ex=EY, ey=EZ, ez=EX) for kx in (-1, 1): for ky in (-1, 1): box(m, W(X + 41.7, 386.0 + kx * 4.2, 326.0 + ky * 6.0), (5.5 * S, 7.5 * S, 0.5 * S), 'Metal_Light', r=1.2 * S, ex=EY, ey=EZ, ez=EX) box(m, W(X + 40.0, 428.0, 322.0), (48 * S, 42 * S, 3.0 * S), 'Paint_Graphite', r=3 * S, ex=EY, ey=EZ, ez=EX) for k in range(9): box(m, W(X + 41.8, 409.0 + k * 4.8, 322.0), (2.2 * S, 34 * S, 1.4 * S), 'Metal_Dark', r=0.6 * S, ex=EY, ey=EZ, ez=EX) coons_cap(m, P[0], 'Metal_Dark', flip=True, ring_idx=None) # back plate of the bore # stator: six spokes and the hub behind the rotor hub = Mesh() revolve(hub, [(0.0, 0), (8.0 * S, 0.01), (8.5 * S, 0.05), (8.5 * S, 0.25)], W(X, 330.0, yc), EY, 16, 'Metal_Dark', cap1='Metal_Dark') m.merge(hub) for k in range(6): a = np.deg2rad(90 + 60 * k) d = np.cos(a) * EX + np.sin(a) * EZ cc = W(X, 332.0, yc) + d * (20.0 * S) box(m, cc, (2.2 * S, 23.5 * S, 3.0 * S), 'Metal_Dark', ex=np.cross(EY, d), ey=d, ez=EY) # body: the duct's circle, then the circle joined to an inboard box (SIDE's box top at y 262 lies inside the hull's # outline seen from FRONT, which shows only the fan discs), then a tail under the upper pod nr = 48 yc_c = 316.0 def union_ring(v, cx, cy, rr, bx0, bx1, by0, by1, rb=8.0): """Outline of a circle (cx, cy, rr) joined to a rounded box [bx0, bx1] x [by0, by1] (px, y down): for each of nr angles round (100, yc_c) (from -45 deg, counter-clockwise in (X, up)) the farthest point along the ray inside either shape. One point per angle, so no coincident vertices.""" def inside(px, py): ins = (px - cx) ** 2 + (py - cy) ** 2 <= rr ** 2 if bx1 > bx0: hx, hy = (bx1 - bx0) / 2 - rb, (by1 - by0) / 2 - rb qx = np.abs(px - (bx0 + bx1) / 2) - hx; qy = np.abs(py - (by0 + by1) / 2) - hy sd = np.hypot(np.maximum(qx, 0), np.maximum(qy, 0)) + np.minimum(np.maximum(qx, qy), 0) - rb ins |= sd <= 0 return ins rs = np.arange(0.0, 120.0, 0.02) out = [] for t in np.deg2rad(-45) + np.linspace(0, 2 * np.pi, nr, endpoint=False): px = 100.0 + rs * np.cos(t); py = yc_c - rs * np.sin(t) k = np.nonzero(inside(px, py))[0] r_ = rs[k.max()] if len(k) else 0.5 out.append(W(100.0 + r_ * np.cos(t), v, yc_c - r_ * np.sin(t))) return np.array(out) rings = [union_ring(345.0, X, yc, 40.0, 0, 0, 0, 0), union_ring(365.0, X, yc, 40.0, 70.0, 96.0, 276.0, 365.0), union_ring(395.0, X, yc, 40.0, 62.0, 104.0, 262.0, 366.0, 10.0), union_ring(455.0, X, yc, 40.0, 62.0, 104.0, 262.0, 366.0, 10.0), union_ring(485.0, X - 6, yc + 2, 34.0, 62.0, 104.0, 262.0, 366.0, 10.0), union_ring(545.0, X - 8, yc + 4, 32.0, 62.0, 104.0, 263.0, 366.0, 10.0), union_ring(572.0, 100.0, 334.0, 26.0, 70.0, 95.0, 290.0, 362.0), union_ring(596.0, 98.0, 336.0, 18.0, 0, 0, 0, 0), union_ring(604.0, 98.0, 336.0, 12.0, 0, 0, 0, 0), union_ring(608.0, 98.0, 336.0, 5.0, 0, 0, 0, 0)] loft(m, rings, 'Paint_Graphite', cap1='Metal_Dark', uv_scale=1.0) # fan rotor (moving): spinner and 12 blades rot = Mesh() piv = W(X, 319.0, yc) revolve(rot, [(0.0, -0.075), (3.5 * S, -0.06), (7.0 * S, -0.025), (8.0 * S, 0.0), (8.0 * S, 0.05)], piv, EY, 16, 'GlowHub', cap1='Metal_Light') for k in range(12): a = np.deg2rad(30 * k) d = np.cos(a) * EX + np.sin(a) * EZ t = np.cross(EY, d) tw = np.deg2rad(32) ez_ = np.cos(tw) * EY + np.sin(tw) * t ex_ = np.cross(d, ez_) box(rot, piv + d * (19.5 * S) + EY * 0.012, (6.0 * S, 22.0 * S, 0.6 * S), 'Metal_Light', ex=ex_, ey=d, ez=ez_) return m, rot, piv def upper_pod(): """Upper thruster (port), after the concept (2026-10-22): a closed nacelle, its duct and fan at the front (FRONT +-126 px; the concept's dark-blue fan face), hung under a short swept wing from the flank (SIDE's trapezoid 536-595 x 219-236 is the wing seen edge-on; TOP's pod block is wing and nacelle seen from above).""" p = L.POD m = Mesh() X, yc = p['X'], 274.0 ax = EY # bore (capped at its back by the hub plate), lip, body, rounded tail to a pole: one closed piece prof = [(26.0, 590.0), (26.5, 552.0), (27.5, 549.0), (29.5, 548.0), (32.0, 548.6), (33.0, 551.0), (33.0, 556.0), (33.6, 557.0), (33.6, 559.0), (33.0, 560.0), (33.0, 598.0), (31.5, 606.0), (27.0, 611.5), (19.0, 615.5), (9.0, 617.5), (0.0, 618.0)] rings = [circle(W(X, vv, yc), ax, rr * S, 32, np.deg2rad(-45), EX, EZ) for rr, vv in prof[:-1]] P = np.array(rings) base = grid(m, P, 'Paint_Graphite', closed_u=True, uv_scale=2.0, row_mats=['GlowFanDim' if 1 <= i <= 3 else ('GlowStrip' if i == 6 else ('Metal_Dark' if i == 0 else 'Paint_Graphite')) for i in range(len(P) - 1)]) coons_cap(m, P[0], 'Metal_Dark', flip=False, ring_idx=[base + j for j in range(32)]) pole = m.add_verts(W(X, prof[-1][1], yc)[None]) last = len(P) - 1 for j in range(32): a_, b_ = base + last * 32 + j, base + last * 32 + (j + 1) % 32 m.face([pole, b_, a_], [(0.5, 0.5), (1, 0), (0, 0)], 'Paint_Graphite') hub = Mesh() revolve(hub, [(0.0, 0), (6.5 * S, 0.01), (7.0 * S, 0.04), (7.0 * S, 0.30), (7.0 * S, 0.34)], W(X, 562.0, yc), EY, 16, 'Metal_Dark', cap1='Metal_Dark') m.merge(hub) for k in range(5): a = np.deg2rad(90 + 72 * k) d = np.cos(a) * EX + np.sin(a) * EZ box(m, W(X, 572.0, yc) + d * (16.5 * S), (2.0 * S, 19.5 * S, 3.0 * S), 'Metal_Dark', ex=np.cross(EY, d), ey=d, ez=EY) # the wing: from inside the flank (SIDE y 220) out and down to the nacelle's top (y 239), chord 59 -> 30 px w = L.WING root = W(w['root_X'], w['root_v'], w['root_y']); tip = W(X, w['tip_v'], w['tip_y']) span = tip - root e_th = np.cross(span, EY); e_th /= np.linalg.norm(e_th) if e_th[2] < 0: e_th = -e_th rings = [] for f in np.linspace(0, 1, 6): c = root + span * f chord = (w['root_chord'] + (w['tip_chord'] - w['root_chord']) * f) * S th = (w['root_th'] + (w['tip_th'] - w['root_th']) * f) * S rings.append(ring3(rrect(0, 0, chord, th, th * 0.45, n_side=3, n_corner=4), c, EY, e_th)) loft(m, rings, 'Paint_Hull', cap0='Paint_Hull', cap1='Paint_Hull') rot = Mesh() piv = W(X, 556.0, yc) revolve(rot, [(0.0, -0.06), (3.0 * S, -0.045), (6.0 * S, -0.015), (6.5 * S, 0.0), (6.5 * S, 0.06)], piv, EY, 16, 'GlowHub', cap1='Metal_Light') for k in range(5): a = np.deg2rad(72 * k) d = np.cos(a) * EX + np.sin(a) * EZ t = np.cross(EY, d) tw = np.deg2rad(32) ez_ = np.cos(tw) * EY + np.sin(tw) * t ex_ = np.cross(d, ez_) box(rot, piv + d * (16.0 * S) + EY * 0.015, (8.5 * S, 18.5 * S, 0.8 * S), 'Metal_Dark', ex=ex_, ey=d, ez=ez_) return m, rot, piv def side_lamp(): """The pill-shaped lamp housing on each flank under the strip (SIDE 325-378 x 238-266), its lamp facing forward.""" s = L.SIDE_LAMP m = Mesh() Xc, yc = 101.0, 252.0 rings = [] for vv, k in ((324.5, 0.25), (325.5, 0.6), (327.5, 0.85), (331.0, 1.0), (372.0, 1.0), (378.0, 0.7), (380.0, 0.35)): rings.append(ring_rr(Xc - (1 - k) * 4, vv, yc, 22.0 * k, 28.0 * (0.4 + 0.6 * k), 9.0 * k, n_side=2, n_corner=6)) loft(m, rings, 'Paint_Hull', cap0='Paint_Hull', cap1='Paint_Hull') # the lamp (the concept: a short barrel standing forward out of the housing, a big lens in a bezel) c = W(Xc, 328.0, yc) revolve(m, [(10.6 * S, 0.0), (10.6 * S, 0.085), (11.4 * S, 0.092), (11.4 * S, 0.108), (9.4 * S, 0.110), (9.4 * S, 0.100)], c, -EY, 24, 'Metal_Dark', cap0='Metal_Dark') lens = Mesh() coons_cap(lens, circle(c - EY * 0.100, -EY, 9.4 * S, 24), 'Lamp', flip=False) m.merge(lens) return m def nose_belt(): """The lidar belt round the nose (SIDE 232-265 x 226-250; FRONT +-62): a band 4.5 cm proud of the skin, with the two lamp blocks, the centre display and the bars between them on its front.""" b = L.BELT m = Mesh() ys = np.linspace(b['y0'], b['y1'], 7) vs = np.concatenate([np.linspace(b['v1'], L.V_TIP + 0.3, 14)]) port = [] for vv in vs: col = [] for yy in ys: v_, ph = L.side_to_param(np.array([vv]), np.array([yy])) col.append(L.surf_world(v_, ph)[0]) port.append(col) port = np.array(port) # (nv, ny, 3) aft -> fore on the port side # across the nose cap: the cap plane at the tip; X from the port column to the starboard one xr = port[-1, :, 0] front = [] for t in np.linspace(1, -1, 9)[1:-1]: col = port[-1].copy(); col[:, 0] = xr * t col[:, 1] = L.to_world(0, L.V_TIP, 0)[1] front.append(col) front = np.array(front) stbd = port[::-1].copy(); stbd[..., 0] *= -1 inner = np.concatenate([port, front, stbd], 0) # path port-aft -> nose -> starboard-aft # outward horizontal normal along the path P = inner[:, inner.shape[1] // 2] T = np.gradient(P, axis=0); T /= np.linalg.norm(T, axis=1, keepdims=True) Nh = np.cross(EZ, T); Nh[:, 2] = 0; Nh /= np.linalg.norm(Nh, axis=1, keepdims=True) if np.dot(Nh[len(Nh) // 2], -EY) < 0: Nh = -Nh D = 0.045 outer = inner.copy() out_face = P[:, None, :] + Nh[:, None, :] * D outer[..., 0] = out_face[..., 0]; outer[..., 1] = out_face[..., 1] inner_t = inner - Nh[:, None, :] * 0.006 # the inner face tucked 6 mm under the skin # one ring per path point: up the outer face, down the inner face (8 + 8 points), lofted and capped as one solid def resamp(col, n=8): t = np.linspace(0, len(col) - 1, n) return np.array([np.interp(t, np.arange(len(col)), col[:, d]) for d in range(3)]).T rings = np.array([np.vstack([resamp(outer[k][::-1]), resamp(inner_t[k])]) for k in range(len(P))]) # ring order: outer bottom -> outer top, inner top -> inner bottom; roll so the Coons sides fall on the faces rings = np.roll(rings, 2, axis=1) base = grid(m, rings, 'Frame', closed_u=True, uv_scale=2.0) ni, nk = rings.shape[:2] half = nk // 2 for i, rev in ((0, False), (ni - 1, True)): # ends: a ladder across the thin band (no Coons slivers) idx = [base + i * nk + ((j + 2) % nk) for j in range(nk)] # undo the roll: outer bottom..top, inner top..bottom for j in range(half - 1): q = [idx[j], idx[j + 1], idx[nk - 2 - j], idx[nk - 1 - j]] if rev: q = q[::-1] m.face(q, [(0, 0), (1, 0), (1, 1), (0, 1)], 'Frame') # front fittings: two lamp blocks and the display, on the band's front face yfront = L.to_world(0, L.V_TIP, 0)[1] - D lamps = Mesh() nl = L.NOSE_LAMPS # the band's front face position at a given X: interpolate along the path def face_at(Xw): k = np.argmin(np.abs(P[:, 0] + Nh[:, 0] * D - Xw) + (np.dot(Nh, -EY) < 0.2) * 9) return P[k] + Nh[k] * D, Nh[k] for sgn in (1, -1): Xw = sgn * nl['X'] * S c, nn = face_at(Xw) ex_ = np.cross(EZ, nn); ex_ /= np.linalg.norm(ex_) cz = (L.Y0 - nl['y']) * S cc = np.array([c[0], c[1], cz]) + nn * 0.02 box(lamps, cc, (nl['w'] * S, nl['h'] * S, 0.05), 'Frame', r=4 * S, ex=ex_, ey=EZ, ez=nn) for ly in nl['lamps']: lc = np.array([c[0], c[1], (L.Y0 - ly) * S]) + nn * 0.046 ring = circle(lc, nn, nl['r'] * S, 16) coons_cap(lamps, ring, 'Lamp', flip=False) grid(lamps, np.array([circle(lc - nn * 0.004, nn, nl['r'] * 1.35 * S, 16), ring]), 'Metal_Dark', closed_u=True) nd = L.NOSE_DISPLAY c = np.array([0, yfront - 0.012, (L.Y0 - nd['y']) * S]) box(lamps, c, (nd['w'] * 2 * S, nd['h'] * S, 0.026), 'Frame', r=3 * S, ex=EX, ey=EZ, ez=-EY) scr = Mesh() w, h = (nd['w'] * 2 - 4) * S, (nd['h'] - 4) * S q = np.array([[-w / 2, -h / 2], [w / 2, -h / 2], [w / 2, h / 2], [-w / 2, h / 2]]) base = scr.add_verts(c + q[:, :1] * EX + q[:, 1:] * EZ - EY * 0.0135) scr.face([base, base + 3, base + 2, base + 1], [(0, 0), (0, 1), (1, 1), (1, 0)], 'Screen') lamps.merge(scr) # bars between the lamp blocks and the display for sgn in (1, -1): x0, x1 = 20.0, 39.0 cxw = sgn * (x0 + x1) / 2 * S cc, nn = face_at(cxw) ex_ = np.cross(EZ, nn) box(lamps, np.array([cxw, cc[1], (L.Y0 - 238.0) * S]) + nn * 0.008, ((x1 - x0) * S, 6 * S, 0.02), 'Metal_Dark', r=2.5 * S, ex=ex_, ey=EZ, ez=nn) m.merge(lamps) return m def chin(): c = L.CHIN m = Mesh() rings = [ring_rr(0, vv, (c['y0'] + c['y1']) / 2, 2 * c['w'] * k, (c['y1'] - c['y0']) * k, 7.0 * k, n_side=2, n_corner=6) for vv, k in ((264.5, 0.92), (266.0, 1.0), (300.0, 1.0))] loft(m, rings, 'Paint_Graphite', cap0='Paint_Graphite', cap1='Paint_Graphite') for lx in c['lamps']: lc = W(lx, 264.3, 342.0) ring = circle(lc, -EY, c['r'] * S, 20) coons_cap(m, ring, 'Lamp', flip=False) grid(m, np.array([circle(lc + EY * 0.004, -EY, c['r'] * 1.3 * S, 20), ring]), 'Metal_Dark', closed_u=True) return m def _roof_trapezoid_loft(m, stations, mat, k=3): """Loft of trapezoid sections standing on the flat roof: stations (v, half-width at the base, height, top inset px). Section corners (X, up from the roof line): base out, top out, top in, base in; capped both ends (closed).""" rings = [] for v, hw, h, ins in stations: yb = L.interp(L.TOPLINE, v) + 1.5 # 1.5 px into the roof base = W(0, v, yb) h = max(h, 0.6) q = [((hw) * S, 0.0), ((hw - ins) * S, (h + 1.5) * S), (-(hw - ins) * S, (h + 1.5) * S), (-hw * S, 0.0)] rings.append(ring3(quad_ring(q, k), base, EX, EZ)) loft(m, rings, mat, cap0=mat, cap1=mat) def roof_hood(): """The concept's vented hood over the canopy, where the blueprint's TOP draws the roof window (v 316-401, +-21 -> +-33 px): a flat-topped block with chamfered sides, its front a slope with a dark vent slot.""" m = Mesh() st = [(313.0, 20.0, 0.0, 0.0), (316.0, 21.0, 1.5, 1.5), (323.0, 22.5, 4.0, 3.0), (396.0, 32.0, 4.5, 3.0), (400.0, 32.5, 3.8, 3.0), (402.0, 32.0, 2.0, 2.0)] _roof_trapezoid_loft(m, st, 'Paint_Hull') # the vent: a dark slot on the front slope, with three slats v0, v1 = 316.3, 322.7 y0 = L.interp(L.TOPLINE, v0) + 1.5 - 1.5 - 1.5; y1 = L.interp(L.TOPLINE, v1) + 1.5 - 4.0 - 1.5 p0, p1 = W(0, v0, y0), W(0, v1, y1) ey_ = (p1 - p0) / np.linalg.norm(p1 - p0); ez_ = np.cross(EX, ey_) if ez_[2] < 0: ez_ = -ez_ c = (p0 + p1) / 2 + ez_ * 0.002 box(m, c, (30.0 * S, np.linalg.norm(p1 - p0) * 0.62, 0.008), 'Frame', ex=EX, ey=ey_, ez=ez_) for k in (-1, 0, 1): box(m, c + ez_ * 0.005 + EX * k * 9.0 * S, (1.2 * S, np.linalg.norm(p1 - p0) * 0.55, 0.006), 'Metal_Dark', ex=EX, ey=ey_, ez=ez_) return m def roof_block(): """The thin step plate behind the hood (the blueprint's block at v 401-434; the concept's plate runs on to the hatch).""" m = Mesh() st = [(402.0, 29.0, 1.0, 1.0), (404.0, 30.0, 3.0, 2.0), (440.0, 26.0, 3.0, 2.0), (446.0, 22.0, 1.0, 1.0)] _roof_trapezoid_loft(m, st, 'Paint_Hull') return m def fin(): f = L.FIN m = Mesh() (le0, y0), (te0, _) = f['root']; (le1, y1), (te1, _) = f['tip'] ys = np.linspace(141.0, y1, 9) rings = [] for yy in ys: t = (yy - y0) / (y1 - y0) le = le0 + (le1 - le0) * t if yy <= y0 else le0 - (yy - y0) * (le1 - le0) / (y0 - y1) te = te0 + (te1 - te0) * t if yy <= y0 else te0 th = f['t_root'] + (f['t_tip'] - f['t_root']) * max(t, 0) rr = rrect(0, 0, (te - le) * S, th * S, th / 2 * S * 0.98, n_side=3, n_corner=6) # ex = along the hull (+Y), ey = +X: ring in the horizontal plane at height yy rings.append(ring3(rr, W(0, (le + te) / 2, yy), EY, EX)) loft(m, rings, 'Paint_Hull', cap1='Paint_Hull') return m def fin_logo(): """The fin's triangle mark (SIDE x 550-580, y 101-126) on both faces of the fin: a triangular ring 1 mm proud of the fin's flat side (the sheet's small lettering under it is not readable and is left out).""" f = L.FIN; lg = L.FIN_LOGO (le0, y0), _ = f['root']; (le1, y1), _ = f['tip'] def half_th(y): t = (y - y0) / (y1 - y0) return (f['t_root'] + (f['t_tip'] - f['t_root']) * t) / 2 ap = np.array(lg['apex']); b0, b1 = np.array(lg['base'][0]), np.array(lg['base'][1]) outer = np.array([b0, b1, ap]) cen = outer.mean(0) + np.array([0, 2.5]) inner = cen + (outer - cen) * lg['inner'] m = Mesh() for sgn in (1, -1): def P(p): return W(sgn * (half_th(p[1]) + 0.12), p[0], p[1]) for k in range(3): k2 = (k + 1) % 3 q = [P(outer[k]), P(outer[k2]), P(inner[k2]), P(inner[k])] b = m.add_verts(q) idx = [b, b + 1, b + 2, b + 3] if sgn > 0 else [b + 3, b + 2, b + 1, b] m.face(idx, [(0, 0), (1, 0), (1, 1), (0, 1)], 'Logo') return m def hatch_lid(): """The hatch's lid as the concept draws the top turret: a loaf (rounded section, rounded ends) over the hatch ring (TOP r 20 px), a dark lens across its front; hinged at its aft edge.""" v0, x0, r = L.HATCH m = Mesh() base_y = L.interp(L.TOPLINE, v0) - 2.4 st = [(v0 - 20.0, 13.0, 7.0), (v0 - 19.0, 16.5, 11.0), (v0 - 17.0, 18.5, 13.5), (v0 - 14.0, 19.5, 15.0), (v0 + 15.0, 19.5, 15.0), (v0 + 18.0, 18.5, 13.5), (v0 + 20.0, 16.5, 11.0), (v0 + 21.0, 13.0, 7.0)] rings = [] for v, hw, h in st: rr = rrect(0, h / 2 * S, 2 * hw * S, h * S, min(9.0, h * 0.62) * S, n_side=2, n_corner=8) rings.append(ring3(rr, W(0, v, base_y), EX, EZ)) loft(m, rings, 'Paint_Hull', cap0='Paint_Hull', cap1='Paint_Hull') # the lens: a dark curved band on the front end (two thirds of its height), 3 mm proud th = np.deg2rad(np.linspace(-62, 62, 13)) lens = Mesh() rows = [] for zz in np.linspace(3.0, 11.5, 4): row = [] for a_ in th: hw_ = 17.6 * np.cos(a_) ** 0.35 row.append(W(hw_ * np.sin(a_) * 1.08, v0 - 17.6 - 2.6 * np.cos(a_) ** 2 + 0.25, base_y - zz)) rows.append(row) grid(lens, np.array(rows), 'GlassDark', uv_scale=4.0, flip=True) m.merge(lens) # the coloured lamp beside the lens (the concept's small light) box(m, W(9.5, v0 - 17.0, base_y - 7.5), (2.2 * S, 1.4 * S, 2.2 * S), 'LampAmber', r=0.4 * S) # hinge knuckle at the aft edge box(m, W(0, v0 + 21.0, base_y - 1.5), (4 * S, 3.5 * S, 16 * S), 'Metal_Dark', r=1.5 * S, ex=EY, ey=EZ, ez=EX) pivot = W(0, v0 + 21.0, base_y - 1.5) return m, pivot def stern_cap(): s = L.STERN_CAP m = Mesh() secs = [(651.0, 68.0, 156.0, 218.0, 16.0), (658.0, 66.5, 156.5, 214.0, 15.0), (690.0, 60.0, 160.0, 210.0, 14.0), (696.0, 57.0, 162.5, 207.5, 12.0), (698.0, 54.0, 165.0, 205.0, 10.0)] rings = [ring_rr(0, vv, (y0 + y1) / 2, 2 * hw, y1 - y0, rr, n_side=3, n_corner=6) for vv, hw, y0, y1, rr in secs] loft(m, rings, 'Paint_Hull', cap1='Metal_Dark') # exhaust frame and glow on the rear face g = Mesh() rr0 = ring_rr(0, 698.4, 185.0, 70.0, 22.0, 9.0, n_side=3, n_corner=6) rr1 = ring_rr(0, 698.4, 185.0, 62.0, 15.0, 6.0, n_side=3, n_corner=6) coons_cap(g, rr1, 'GlowStrip', flip=True) grid(g, np.array([rr0, rr1]), 'Frame', closed_u=True) m.merge(g) # side vents (SIDE 667-680 x 168-195): two dark slots per side, slightly proud for sgn in (1, -1): for vv in (668.0, 678.0): hw = 62.5 - (vv - 658) * 0.2 box(m, W(sgn * (hw + 0.3), vv, 182.0), (0.5 * S, 7.0 * S, 24.0 * S), 'Metal_Dark', r=1.5 * S, ex=EX, ey=EY, ez=EZ) return m def flank_plate(): """The concept's lower flank (port; mirrored for starboard): an armour plate under the lower strip (S2), from just aft of the strip's front leg (v 314) back past the wing root (v 598), down to y 258 over the nacelle. Its top edge, 2.5 px under the strip, slopes out 5 cm within 9 px (the concept's lit shelf), then stands 5 cm off the skin. A closed solid whose back lies 12 mm inside the skin.""" import ae_hullmesh as HM pts = [(v, y) for v, y in L.STRIP_S2 if v >= 317] + [(600.0, 210.0)] def ytop(v): return np.interp(v, [p[0] for p in pts], [p[1] for p in pts]) + 2.5 vs = np.concatenate([np.linspace(314.0, 330.0, 5)[:-1], np.linspace(330.0, 598.0, 68)]) ws = np.array([0.0, 0.025, 0.055, 0.09, 0.13, 0.18, 0.25, 0.34, 0.46, 0.6, 0.78, 1.0]) nw = len(ws) O, I = [], [] for v in vs: y0, y1 = ytop(v), 258.0 ys = y0 + (y1 - y0) * ws vv, ph = L.side_to_param(np.full(nw, v), ys) P = L.surf_world(vv, ph); N = HM.surf_normal(vv, ph) N = np.where((N[:, :1] < 0), -N, N) d = np.clip((ys - y0) / 9.0, 0, 1) off = 0.004 + 0.046 * (d * d * (3 - 2 * d)) O.append(P + N * off[:, None]); I.append(P - N * 0.012) O, I = np.array(O), np.array(I) m = Mesh() ring = np.concatenate([O, I[:, ::-1]], 1) # per station: outer down, inner up ni, nk = ring.shape[:2] base = m.add_verts(ring.reshape(-1, 3)) along = np.concatenate([[0], np.cumsum(np.linalg.norm(np.diff(ring[:, 0], axis=0), axis=1))]) for i in range(ni - 1): for k in range(nk): k2 = (k + 1) % nk q = [base + i * nk + k, base + (i + 1) * nk + k, base + (i + 1) * nk + k2, base + i * nk + k2] uv = [(k * 0.05, along[i] * 1.2), (k * 0.05, along[i + 1] * 1.2), ((k + 1) * 0.05, along[i + 1] * 1.2), ((k + 1) * 0.05, along[i] * 1.2)] m.face(q, uv, 'Paint_Hull') for i, rev in ((0, False), (ni - 1, True)): # end faces: a ladder between the outer and inner rows for j in range(nw - 1): q = [base + i * nk + j, base + i * nk + j + 1, base + i * nk + (nk - 2 - j), base + i * nk + (nk - 1 - j)] if rev: q = q[::-1] m.face(q, [(0, 0), (1, 0), (1, 1), (0, 1)], 'Paint_Hull') return m def strip_path(points_side=None, points_top=None): """Project SIDE (v, y) and TOP (v, X) points onto the port skin; return dense 3D path and normals.""" import ae_hullmesh as HM params = [] if points_top: for v, X in points_top: params.append(L.view_to_param('top', X, v)) if points_side: for v, y in points_side: vv, ph = L.side_to_param(np.array([v]), np.array([y])) params.append((float(vv[0]), float(ph[0]))) par = np.array(params) # densify in param space d = np.linalg.norm(np.diff(L.surf_world(par[:, 0], par[:, 1]), axis=0), axis=1) s = np.concatenate([[0], np.cumsum(d)]) t = np.arange(0, s[-1], 0.012) pv = np.interp(t, s, par[:, 0]); pp = np.interp(t, s, par[:, 1]) # light smoothing of the corners for _ in range(3): pv[1:-1] = 0.25 * pv[:-2] + 0.5 * pv[1:-1] + 0.25 * pv[2:] pp[1:-1] = 0.25 * pp[:-2] + 0.5 * pp[1:-1] + 0.25 * pp[2:] P = L.surf_world(pv, pp) N = HM.surf_normal(pv, pp) if np.mean(np.sum(N * np.stack([P[:, 0], np.zeros(len(P)), np.zeros(len(P))], 1), 1)) < 0: N = -N return P, N STRIP_PROF = [(-0.048, -0.004), (-0.048, 0.006), (-0.024, 0.014), (0.024, 0.014), (0.048, 0.006), (0.048, -0.004)] STRIP_MATS = ['Frame', 'GlowStripSide', 'GlowStrip', 'GlowStripSide', 'Frame'] def strips(): m = Mesh() P, N = strip_path(points_top=L.STRIP_S1_ROOF, points_side=L.STRIP_S1_SIDE) sweep(m, P, N, STRIP_PROF, 'Frame', mats=STRIP_MATS) P, N = strip_path(points_side=L.STRIP_S2) sweep(m, P, N, STRIP_PROF, 'Frame', mats=STRIP_MATS) return m # ---- manipulator arm (port): chain of moving parts, each with its pivot ----------------------------------------------- def _link(m, p0, p1, h, w, mat, side_axis=EX, chamfer=0.25): """A box link from p0 to p1 (centre line), section h (in the arm plane) x w (along side_axis), chamfered edges.""" d = p1 - p0; L_ = np.linalg.norm(d); dn = d / L_ up = np.cross(side_axis, dn); up /= np.linalg.norm(up) box(m, (p0 + p1) / 2, (w, h, L_), mat, r=min(w, h) * chamfer, ex=side_axis, ey=up, ez=dn, n_corner=2) return dn, up def arm(): """Manipulator arm (port; the build mirrors it), shaped after the concept (2026-10-22): a cylinder shoulder joint on the hull's lower front flank, a boxy upper arm with a piston on its top, an elbow joint, a boxy forearm, a wrist roll joint and a parallel two-jaw gripper. Pose as both pictures draw it (back-projected through the concept's fitted camera it lands within a few px of SIDE's): shoulder (297, 336), elbow (233, 355), wrist (205, 361), jaw tips at x 180.""" a = L.ARM parts = {} import ae_hullmesh as HM # the shoulder joint (axis lateral) just in front of the lower nose, at FRONT's |X| ~ 47 px; a bracket block carries it # from inside the hull (the concept's small block under the nose with the joint on its side) c = W(a['joint'][0], a['joint'][1], a['joint'][2]) base = Mesh() box(base, c + np.array([-12.0 * S, 4.0 * S, 12.0 * S]), (14.0 * S, 26.0 * S, 40.0 * S), 'Paint_Graphite', r=3.0 * S, n_corner=2) parts['Arm_Mount'] = (base, None) up = Mesh() revolve(up, [(14.0 * S, -5.0 * S), (14.0 * S, 5.0 * S)], c, EX, 28, 'Metal_Dark', cap0='Metal_Dark', cap1='Metal_Dark') revolve(up, [(9.5 * S, 5.0 * S), (9.5 * S, 6.2 * S)], c, EX, 24, 'GlowStrip', cap1='Metal_Light') plane_x = c[0] + 10.0 * S # the links run beside the joint, outboard elw = W(0, *a['elbow']); elw[0] = plane_x s0 = c.copy(); s0[0] = plane_x dn, upv = _link(up, s0 + (elw - s0) / np.linalg.norm(elw - s0) * 0.07, elw, 18.0 * S, 13.0 * S, 'Paint_Hull') # piston on the upper arm's top: cylinder + rod p_a = s0 + dn * 0.18 + upv * (12.0 * S); p_b = elw - dn * 0.12 + upv * (10.5 * S) revolve(up, [(3.0 * S, 0.0), (3.0 * S, np.linalg.norm(p_b - p_a) * 0.6)], p_a, (p_b - p_a), 12, 'Metal_Dark', cap0='Metal_Dark', cap1='Metal_Dark') revolve(up, [(1.6 * S, 0.0), (1.6 * S, np.linalg.norm(p_b - p_a))], p_a, (p_b - p_a), 12, 'Metal_Light', cap0='Metal_Light', cap1='Metal_Light') parts['Arm_Upper'] = (up, c) # elbow joint + forearm (pivot: the elbow) fo = Mesh() ce = elw revolve(fo, [(10.5 * S, -7.5 * S), (10.5 * S, 7.5 * S)], ce, EX, 24, 'Metal_Dark', cap0='Metal_Light', cap1='Metal_Light') wrw = W(0, *a['wrist']); wrw[0] = plane_x dn2 = (wrw - ce) / np.linalg.norm(wrw - ce) _link(fo, ce + dn2 * 0.09, wrw - dn2 * 0.01, 14.0 * S, 11.0 * S, 'Paint_Hull') parts['Arm_Fore'] = (fo, ce) # wrist (roll about the forearm axis): a short drum and the palm wm = Mesh() cw = wrw revolve(wm, [(8.0 * S, -0.012), (8.0 * S, 0.060)], cw, dn2, 20, 'Metal_Dark', cap0='Metal_Dark', cap1='Metal_Light') upw = np.cross(EX, dn2); upw /= np.linalg.norm(upw) box(wm, cw + dn2 * 0.092, (13.0 * S, 16.0 * S, 0.065), 'Paint_Hull', r=1.8 * S, ex=EX, ey=upw, ez=dn2, n_corner=2) parts['Arm_Wrist'] = (wm, cw) # two jaws (pivot each at its knuckle on the palm, closing about +X) claw_root = cw + dn2 * 0.125 for name, sgn in (('Arm_Finger_A', 1), ('Arm_Finger_B', -1)): fm = Mesh() k = claw_root + upw * sgn * (5.5 * S) tip = k + dn2 * (22.0 * S) + upw * sgn * (1.5 * S) dd = (tip - k) / np.linalg.norm(tip - k) ey_ = np.cross(EX, dd) box(fm, (k + tip) / 2, (11.0 * S, 4.0 * S, np.linalg.norm(tip - k)), 'Metal_Dark', r=0.9 * S, ex=EX, ey=ey_, ez=dd, n_corner=2) box(fm, tip - dd * (2.8 * S) - ey_ * sgn * (2.0 * S), (10.0 * S, 1.8 * S, 5.5 * S), 'Metal_Light', ex=EX, ey=ey_, ez=dd) revolve(fm, [(3.0 * S, -6.2 * S), (3.0 * S, 6.2 * S)], k, EX, 12, 'Metal_Light', cap0='Metal_Light', cap1='Metal_Light') parts[name] = (fm, k) return parts # ---- cabin ------------------------------------------------------------------------------------------------------------ def cabin(grid_out): """Floor, two seats side by side (FRONT draws one pilot in each upper side window, SIDE the head at x 355), the dashboard under the front glass, the console between the seats and the ladder under the hatch. No figures.""" import ae_hullmesh as HM m = Mesh() yf = 322.0 zf = (L.Y0 - yf) * S # floor: per station the lining's half-width at the floor's height X = grid_out['X']; V = grid_out['V']; N = grid_out['N'] Xin = X - HM.LINING * N rows = [] for i in range(V.shape[0]): if V[i].max() > HM.CABIN_END - 2: break ring = Xin[i] zz = ring[:, 2]; xx = ring[:, 0] # crossing of the lining ring with z = zf on the port side cand = [] for j in range(len(ring)): j2 = (j + 1) % len(ring) if (zz[j] - zf) * (zz[j2] - zf) < 0: t = (zf - zz[j]) / (zz[j2] - zz[j]); cand.append(xx[j] + t * (xx[j2] - xx[j])) if len(cand) >= 2 and max(cand) > 0.25: rows.append((ring[:, 1].mean(), max(cand) - 0.01)) rows = np.array(rows) sel = rows[::2] G = np.array([[[x * f, y, zf] for f in np.linspace(-1, 1, 9)] for y, x in sel]) grid(m, G, 'Floor', uv_scale=2.0, flip=True) # seats for sgn in (1, -1): Xs = sgn * 0.60 # pedestal box(m, [Xs, (378.0 - L.V0) * S, zf + 0.30], (0.30, 0.34, 0.60), 'Metal_Dark', r=0.04) # pan box(m, [Xs, (372.0 - L.V0) * S, (L.Y0 - 266.0) * S], (0.54, 0.56, 0.12), 'Seat', r=0.06) # back (tilted 18 deg back) tilt = np.deg2rad(18) ez_ = np.array([0, np.sin(tilt), np.cos(tilt)]); ey_ = np.array([0, np.cos(tilt), -np.sin(tilt)]) box(m, [Xs, (395.0 - L.V0) * S, (L.Y0 - 226.0) * S], (0.54, 0.12, 0.78), 'Seat', r=0.05, ex=EX, ey=ey_, ez=ez_) box(m, [Xs, (402.0 - L.V0) * S, (L.Y0 - 186.0) * S], (0.30, 0.10, 0.22), 'Seat', r=0.04, ex=EX, ey=ey_, ez=ez_) # armrests for k in (-1, 1): box(m, [Xs + k * 0.30, (378.0 - L.V0) * S, (L.Y0 - 252.0) * S], (0.07, 0.42, 0.07), 'Metal_Dark', r=0.02) # footrest in the nose box(m, [Xs * 0.75, (300.0 - L.V0) * S, zf + 0.10], (0.36, 0.22, 0.05), 'Metal_Dark', r=0.02, ex=EX, ey=np.array([0, np.cos(0.5), np.sin(0.5)]), ez=np.array([0, -np.sin(0.5), np.cos(0.5)])) # dashboard: a low curved desk under the front windows dash = [] for t in np.linspace(-1, 1, 13): ang = t * 0.9 cx, cy = 0.0, (335.0 - L.V0) * S R = 0.95 dash.append((cx + R * np.sin(ang), cy - R * np.cos(ang) * 0.55)) dash = np.array(dash) for k in range(len(dash) - 1): p0, p1 = dash[k], dash[k + 1] c = np.array([(p0[0] + p1[0]) / 2, (p0[1] + p1[1]) / 2, (L.Y0 - 232.0) * S]) dx = np.array([p1[0] - p0[0], p1[1] - p0[1], 0]); ln = np.linalg.norm(dx); dx /= ln ny = np.cross(EZ, dx) tilt = np.deg2rad(35) ez_ = np.cos(tilt) * EZ - np.sin(tilt) * ny ey_ = np.cross(ez_, dx) box(m, c, (ln + 0.004, 0.26, 0.05), 'Console', ex=dx, ey=ey_, ez=ez_) box(m, c + ez_ * 0.026, (ln * 0.86, 0.18, 0.004), 'Screen', ex=dx, ey=ey_, ez=ez_) # centre console between the seats box(m, [0, (370.0 - L.V0) * S, zf + 0.45], (0.30, 0.60, 0.90), 'Console', r=0.04) box(m, [0, (352.0 - L.V0) * S, zf + 0.92], (0.26, 0.20, 0.004), 'Screen', ex=EX, ey=np.array([0, np.cos(0.6), np.sin(0.6)]), ez=np.array([0, -np.sin(0.6), np.cos(0.6)])) # ladder under the hatch (to the hatch coaming) v0 = L.HATCH[0] ztop = (L.Y0 - 140.0) * S for k in (-1, 1): box(m, [k * 0.20, (v0 - L.V0) * S + 0.12, (zf + ztop) / 2], (0.035, 0.035, ztop - zf), 'Metal_Dark', r=0.01) for z in np.arange(zf + 0.30, ztop - 0.1, 0.30): box(m, [0, (v0 - L.V0) * S + 0.12, z], (0.40, 0.03, 0.03), 'Metal_Light', r=0.01) # ceiling light bars (the concept's cabin glows blue through the glass) for k in (-1, 1): box(m, [k * 0.35, (420.0 - L.V0) * S, (L.Y0 - 141.5) * S], (0.06, 0.9, 0.02), 'LampSoft', r=0.008) box(m, [k * 0.40, (345.0 - L.V0) * S, (L.Y0 - 148.5) * S], (0.06, 0.55, 0.02), 'LampSoft', r=0.008) return m