"""Tiger: the geometry, as plain numpy (no bpy), shared by the picture overlays (tg_try.py) and the Blender build. Frame (the Kazah's and the Mako's): 1 unit = 1 m, nose to -Y, port +X, Z up. The outlines are read off the user's picture (reference/user_picture.jpg, pixels (u, v), v down) and cast through the fitted camera (build/cam.json, tg_fit.py) onto the plane they lie in; what the one picture cannot show (widths, the far side) comes from the Mako sheet's TOP and FRONT readings of the same design (tender-mako/build/mk_measure.py), scaled to this picture's proportions. The canopy is fitted on its own (tg_fit_canopy.py -> canopy.json). build() -> list of Part(name, verts (n,3), faces [index tuples], cls per face, mat per face) """ import math from dataclasses import dataclass, field import numpy as np from tg_cam import load, on_plane import tg_canopy as tc CAM = load()["params"] BG, ORANGE, RED, BLUEGREY, DARK, BLACK, PALE, NAVY, GLASS, OTHER = range(10) # ----------------------------------------------------------------------------------------------------------- helpers def pchip(xs, ys): """monotone cubic through (xs, ys); f(x) clamps at the ends""" xs, ys = np.asarray(xs, float), np.asarray(ys, float) o = np.argsort(xs) xs, ys = xs[o], ys[o] h = np.diff(xs) d = np.diff(ys) / h m = np.zeros_like(ys) for i in range(1, len(xs) - 1): if d[i - 1] * d[i] > 0: w1, w2 = 2 * h[i] + h[i - 1], h[i] + 2 * h[i - 1] m[i] = (w1 + w2) / (w1 / d[i - 1] + w2 / d[i]) m[0], m[-1] = d[0], d[-1] def f(x): x = np.clip(np.asarray(x, float), xs[0], xs[-1]) i = np.clip(np.searchsorted(xs, x) - 1, 0, len(xs) - 2) 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 def bp(uv, n=(1, 0, 0), d0=0.0): """picture pixels cast onto the plane n . X = d0""" return np.array([on_plane(CAM, u, v, n, d0) for u, v in uv]) def cos_space(n, a, b): return a + (b - a) * (1 - np.cos(np.linspace(0, math.pi, n))) / 2 def resample(P, n): """n points evenly along the polyline P by arc length""" P = np.asarray(P, float) s = np.concatenate([[0], np.cumsum(np.linalg.norm(np.diff(P, axis=0), axis=1))]) t = np.linspace(0, s[-1], n) return np.stack([np.interp(t, s, P[:, k]) for k in range(P.shape[1])], 1) @dataclass class Part: name: str verts: np.ndarray faces: list cls: list mat: list smooth: bool = True extra: dict = field(default_factory=dict) def tris(self): out, c = [], [] for f, k in zip(self.faces, self.cls): for i in range(1, len(f) - 1): out.append(self.verts[[f[0], f[i], f[i + 1]]]) c.append(k) return (np.array(out), np.array(c)) if out else (np.zeros((0, 3, 3)), np.zeros(0, int)) def loft(rows, closed=True, cap0=False, cap1=False): """quads between consecutive rows (each an (m,3) array, same m); closed: each row is a loop; caps as one n-gon each""" rows = [np.asarray(r, float) for r in rows] m = len(rows[0]) V = np.concatenate(rows) F = [] for i in range(len(rows) - 1): for j in range(m if closed else m - 1): a, b = i * m + j, i * m + (j + 1) % m F.append((a, b, b + m, a + m)) if cap0: F.append(tuple(range(m - 1, -1, -1))) if cap1: n0 = (len(rows) - 1) * m F.append(tuple(range(n0, n0 + m))) return V, F def part(name, V, F, cls, mat, smooth=True, **extra): return Part(name, np.asarray(V, float), [tuple(f) for f in F], [cls] * len(F), [mat] * len(F), smooth, extra) def mirrored(p, name=None): """the part and its mirror across the centre plane, merged""" n = len(p.verts) V = np.concatenate([p.verts, p.verts * np.array([-1, 1, 1])]) F = list(p.faces) + [tuple(i + n for i in reversed(f)) for f in p.faces] return Part(name or p.name, V, F, p.cls * 2, p.mat * 2, p.smooth, dict(p.extra)) def blade(le, te, root_t, tip_t, n_span=9, n_chord=6, normal=(1, 0, 0), sink=0.0, max_at=0.35): """a fin from its outline in a plane: le and te are polylines (k,3) from the root to the tip (they meet at the tip). Each span level is a chord LE -> TE with a lens section (thickness root_t .. tip_t, thickest at max_at of the chord); the root level is pushed `sink` back along the span (into the hull). Returns V, F (closed loop rows).""" L, T = resample(le, n_span), resample(te, n_span) nrm = np.asarray(normal, float) nrm /= np.linalg.norm(nrm) rows = [] for i in range(n_span): a, b = L[i], T[i] th = root_t + (tip_t - root_t) * (i / (n_span - 1)) ** 0.9 us = cos_space(n_chord + 2, 0, 1)[1:-1] top, bot = [], [] for u in us: p = a + (b - a) * u k = math.sin(0.5 * math.pi * u / max_at) if u < max_at else math.cos(0.5 * math.pi * (u - max_at) / (1 - max_at)) k = max(k, 0.0) ** 0.8 top.append(p + nrm * th * 0.5 * k) bot.append(p - nrm * th * 0.5 * k) rows.append([a] + top + [b] + bot[::-1]) if sink: d = (L[0] + T[0]) / 2 - (L[1] + T[1]) / 2 d /= np.linalg.norm(d) rows[0] = [np.asarray(q) + d * sink for q in rows[0]] return loft(rows, closed=True, cap0=True) def ring(y, w, zt, zw, zk, n=28, ex_s=0.7, ex_t=0.9, ex_b=0.55, w_top=None): """closed section: top zt, widest w at zw, keel zk; w_top narrows the upper half""" pts = [] for a in np.linspace(0, 2 * math.pi, n, endpoint=False): s, c = math.sin(a), math.cos(a) ww = w if (c < 0 or w_top is None) else w + (w_top - w) * c x = ww * math.copysign(abs(s) ** ex_s, s) z = zw + (zt - zw) * c ** ex_t if c >= 0 else zw - (zw - zk) * (-c) ** ex_b pts.append((x, y, z)) return pts def box(x0, x1, y0, y1, z0, z1, name, cls, mat, slope_front=0.0): V = [(x0, y0 + slope_front, z0), (x1, y0 + slope_front, z0), (x1, y1, z0), (x0, y1, z0), (x0, y0, z1), (x1, y0, z1), (x1, y1, z1), (x0, y1, z1)] F = [(0, 3, 2, 1), (4, 5, 6, 7), (0, 1, 5, 4), (1, 2, 6, 5), (2, 3, 7, 6), (3, 0, 4, 7)] return part(name, V, F, cls, mat, smooth=False) def cylinder_y(y0, y1, r, xc, zc, n=16, name="cyl", cls=DARK, mat="dark_metal"): rows = [[(xc + r * math.cos(a), y, zc + r * math.sin(a)) for a in np.linspace(0, 2 * math.pi, n, endpoint=False)] for y in (y0, y1)] V, F = loft(rows, closed=True, cap0=True, cap1=True) return part(name, V, F, cls, mat) # ----------------------------------------------------------------------------------------------------------- the canopy CP = tc.load() CREST, GBOT, GMID, HALFW, SILL = tc.profiles(CP) Y_NOSE, Y_G1 = CP["y_nose"], CP["y_rear"] def part_canopy(): V, F, path, info = tc.canopy(CP, n_st=36, n_around=48) fv, ff = tc.frame_band(path, CP) g = part("glass", V, F, GLASS, "glass") # milky glass as the picture paints it: a band over the top (the middle 40 % of each station's ring, so its edges follow # the grid's lines) and the thick lower lip under the nose (the ring's lowest third over the first ten stations) na = 48 nfan = na - 1 for k in range(len(g.faces)): if k < nfan: g.mat[k] = "glass_pale" continue i, j = divmod(k - nfan, na - 1) u = abs(2.0 * (j + 0.5) / (na - 1) - 1.0) # 0 at the crest, 1 at the sills / under the nose if u < 0.40 or (i < 10 and u > 0.70): g.mat[k] = "glass_pale" return [g, part("frame", fv, ff, NAVY, "frame")] # ----------------------------------------------------------------------------------------------------------- centre plane def centre(uv): P = bp(uv) return P[:, 1], P[:, 2] # the shell's ridge: the picture's top of the orange per column, over the canopy (the brow) to the dorsal fin's leading edge, # and from its trailing edge to the tail fin (centre plane) RIDGE_UV = [(150, 312), (160, 310), (170, 301), (180, 295), (190, 292), (200, 287), (210, 283), (220, 279), (230, 276), (240, 271), (250, 268), (260, 263), (270, 259), (280, 253), (290, 248), (300, 245), (310, 241), (320, 237), (330, 232), (340, 227), (352, 223), (440, 219), (460, 222), (470, 225), (480, 228), (490, 231), (500, 233), (510, 237), (520, 241), (530, 244), (540, 246), (550, 249), (560, 252), (570, 256), (580, 260), (590, 263), (600, 267), (610, 271), (620, 275), (630, 279), (640, 282), (650, 287), (660, 291), (670, 293), (680, 297), (690, 300), (700, 303), (720, 309), (740, 316), (752, 322)] DORSAL_UV = [(341, 226), (350, 208), (360, 186), (370, 163), (380, 139), (390, 123), (400, 110), (410, 101), (420, 94), (427, 90), (430, 112), (440, 168), (450, 203), (462, 224)] TAIL_UV = [(690, 306), (700, 262), (705, 230), (709, 205), (713, 200), (720, 213), (730, 237), (740, 266), (747, 295), (752, 322)] VENTRAL_UV = [(706, 418), (746, 523), (753, 418)] RY, RZ = centre(RIDGE_UV) RIDGE = pchip(RY, RZ) Y_BROW0 = float(RY[0]) Y_END = 4.40 # the shell's last station (over the stern ring) # the shell's lower edge on the port flank (the picture's lowest orange below the ridge per column), cast onto the shell's # flank X = SHELL_W(y); the widths from the sheet's TOP (its orange cape), wider where the picture shows more flank SHELL_W = pchip([-0.6, 0.0, 0.6, 0.9, 1.2, 1.6, 2.1, 2.6, 3.1, 3.6, 4.0, 4.4], [0.66, 0.66, 0.62, 0.64, 0.66, 0.62, 0.52, 0.42, 0.33, 0.25, 0.20, 0.16]) EDGE_UV = [(350, 284), (380, 285), (410, 289), (440, 295), (450, 300), (460, 312), (475, 324), (490, 329), (510, 331), (530, 322), (550, 310), (570, 306), (590, 304), (610, 303), (630, 305), (650, 309), (670, 312), (690, 316), (710, 320), (730, 324), (750, 327)] def _edge_table(): ys, zs = [], [] for u, v in EDGE_UV: y = 0.0 for _ in range(4): # the plane depends on y: iterate P = on_plane(CAM, u, v, (1, 0, 0), float(SHELL_W(y))) y = P[1] ys.append(P[1]) zs.append(P[2]) return pchip(ys, zs), ys[0], ys[-1] SHELL_Z, _ey0, _ey1 = _edge_table() # ----------------------------------------------------------------------------------------------------------- brow & collar def part_brow(): """the orange visor over the glass's crest: its top on the ridge, its sides lying on the glass, from its point back under the shell's front edge (it ends inside the shell, 8 mm under its skin there, closed)""" y1 = Y_G1 + 0.16 ys = list(cos_space(16, Y_BROW0, Y_G1)) + [Y_G1 + 0.08, y1] rows = [] n = 9 for y in ys: t = min(1.0, (y - Y_BROW0) / (Y_G1 - Y_BROW0)) ab = 0.03 + 0.95 * t ** 0.7 # half angle on the glass (rad) yy = min(y, Y_G1) zr = float(RIDGE(y)) - (0.008 if y > Y_G1 + 0.05 else 0.0) top, bot = [], [] for a in np.linspace(ab, -ab, n): x, z = tc.ring_point(CP, yy, a, CREST, GBOT, GMID, HALFW) nx, nz = x, z - float(GMID(yy)) nn = math.hypot(nx, nz) or 1 nx, nz = nx / nn, nz / nn sq = 1 - (a / ab) ** 2 lift = max(0.0, zr - float(CREST(yy)) - 0.02) * sq ** 0.6 bot.append((x - nx * 0.004, y, z - nz * 0.004)) top.append((x + nx * 0.02, y, z + nz * 0.02 + lift)) rows.append(top + bot[::-1]) V, F = loft(rows, closed=True, cap0=True, cap1=True) return part("brow", V, F, ORANGE, "paint_orange") def part_collar(): """a thick orange ring round the glass's rear rim and on down the flanks to the jaw, flush with the shell on top""" n = 41 z_foot = -0.16 a_foot = tc.angle_of_z(CP, Y_G1, z_foot, CREST, GBOT, GMID) prof = [(-0.03, -0.005), (0.03, -0.0), (0.075, 0.015), (0.09, 0.04), (0.095, 0.17), (0.085, 0.28), (0.05, 0.31), (-0.03, 0.31)] # (out from the glass's rear ring, y from Y_G1) rows = [] for a in np.linspace(a_foot, -a_foot, n): x, z = tc.ring_point(CP, Y_G1, a, CREST, GBOT, GMID, HALFW) nx, nz = x, (z - float(GMID(Y_G1))) nn = math.hypot(nx, nz) or 1 nx, nz = nx / nn, nz / nn rows.append([(x + nx * o, Y_G1 + dy, z + nz * o) for o, dy in prof]) V, F = loft(rows, closed=True, cap0=True, cap1=True) return part("collar", V, F, ORANGE, "paint_orange") # ----------------------------------------------------------------------------------------------------------- shell def shell_arch(y, n, inset=0.0): """the shell's outer section at y: from the port lower edge over the ridge to the starboard edge""" zr = float(RIDGE(y)) - inset ze = float(SHELL_Z(y)) + inset * 0.3 we = float(SHELL_W(y)) - inset pts = [] for s in np.linspace(1, -1, n): q = max(0.0, 1 - abs(s) ** 2.4) ** (1 / 1.35) pts.append((we * s, y, ze + (zr - ze) * q)) return pts def part_shell(): ys = list(cos_space(40, Y_G1 + 0.08, Y_END)) n = 25 rows = [shell_arch(y, n) + shell_arch(y, n, inset=0.035)[::-1] for y in ys] V, F = loft(rows, closed=True, cap0=True, cap1=True) return part("shell", V, F, ORANGE, "paint_orange") def fin_from(uv, i_tip, root_t, tip_t, name, cls, mat, sink=0.12, n_span=9): P = bp(uv) le = P[:i_tip + 1] # root -> tip te = P[i_tip:][::-1] # root -> tip V, F = blade(le, te, root_t, tip_t, n_span=n_span, sink=sink) return part(name, V, F, cls, mat) # ----------------------------------------------------------------------------------------------------------- core body # the dark body behind the canopy: the lower body under the side bodies (its keel the picture's lower outline cast onto X 0.35), # then the capsule (its own part, a rounded front at y 0.93); widths from the sheet's WAIST CORE_Y = [-0.40, -0.20, 0.00, 0.20, 0.40, 0.55, 0.70, 0.85, 1.00, 1.20] CORE_W = [0.30, 0.42, 0.56, 0.60, 0.58, 0.54, 0.50, 0.46, 0.44, 0.42] CORE_ZW = [-0.30, -0.28, -0.25, -0.22, -0.20, -0.18, -0.15, -0.10, -0.05, 0.00] CORE_ZK = [-0.90, -0.88, -0.88, -0.90, -0.84, -0.66, -0.50, -0.40, -0.34, -0.30] CW, CZW, CZK = pchip(CORE_Y, CORE_W), pchip(CORE_Y, CORE_ZW), pchip(CORE_Y, CORE_ZK) def part_core(): ys = list(cos_space(16, -0.40, 1.20)) rows = [ring(y, float(CW(y)), float(RIDGE(y)) - 0.12, float(CZW(y)), float(CZK(y)), w_top=0.42) for y in ys] V, F = loft(rows, closed=True, cap0=True, cap1=True) p = part("core", V, F, DARK, "dark_body") # the upper flank behind the collar, under the shell, is the picture's lit grey recess; below it the dark lower body for k, f in enumerate(p.faces): c = p.verts[list(f)].mean(0) if c[2] > 0.12 and c[1] < 0.95: p.cls[k], p.mat[k] = BLUEGREY, "recess_grey" return p CAP_Y = [0.93, 0.96, 1.02, 1.12, 1.30, 1.70, 2.10, 2.60, 3.10, 3.60, 4.00, 4.30, 4.45] CAP_W = [0.05, 0.28, 0.40, 0.47, 0.52, 0.54, 0.53, 0.50, 0.46, 0.41, 0.35, 0.28, 0.22] CAP_ZW = [0.12, 0.12, 0.12, 0.12, 0.12, 0.12, 0.12, 0.12, 0.12, 0.12, 0.12, 0.13, 0.14] CAP_ZK = [0.05, -0.06, -0.12, -0.15, -0.17, -0.20, -0.23, -0.26, -0.28, -0.29, -0.29, -0.25, -0.18] CAP_ZT = [0.20, 0.32, 0.40, 0.44, 0.47, 0.50, 0.52, 0.52, 0.51, 0.48, 0.45, 0.42, 0.40] KW, KZW, KZK, KZT = pchip(CAP_Y, CAP_W), pchip(CAP_Y, CAP_ZW), pchip(CAP_Y, CAP_ZK), pchip(CAP_Y, CAP_ZT) def part_capsule(): ys = [0.93, 0.935, 0.95, 0.98] + list(cos_space(22, 1.02, 4.45)) rows = [ring(y, float(KW(y)), float(KZT(y)), float(KZW(y)), float(KZK(y)), n=32, ex_s=0.6, ex_t=0.8, ex_b=0.7) for y in ys] V, F = loft(rows, closed=True, cap0=True, cap1=True) p = part("capsule", V, F, DARK, "dark_body") # its upper flank lighter (the picture's lit blue grey under the shell), the belly dark for k, f in enumerate(p.faces): if p.verts[list(f)].mean(0)[2] > 0.10: p.mat[k] = "capsule_grey" return p # ----------------------------------------------------------------------------------------------------------- jaw # the jaw: a wedge under the glass, its flank from the sill down to a narrow keel; the keel is the picture's lower outline of the # red cast onto X 0.08 (it slopes down 35 deg from the front corner); the prongs carry it on Y_JAW0 = CP["y_jaw"] JAW_Y = [Y_JAW0, -1.20, -1.00, -0.80, -0.60, -0.40, -0.20, 0.00, 0.10] JAW_ZK = [-0.28, -0.40, -0.55, -0.69, -0.79, -0.83, -0.85, -0.85, -0.85] JAW_WK = [0.06, 0.08, 0.09, 0.10, 0.11, 0.12, 0.12, 0.12, 0.12] JZK, JWK = pchip(JAW_Y, JAW_ZK), pchip(JAW_Y, JAW_WK) exec(open(__import__("os").path.join(__import__("os").path.dirname(__file__), "tg_jaw.py"), encoding="utf-8").read()) # ----------------------------------------------------------------------------------------------------------- side body, big fins # The big fins, from the near and the far fin together (a mirrored pair is a stereo pair: each picture edge is a plane through # the camera, the far one mirrored meets the near one in the 3D edge). The spike: its leading edge from E (0.947, 0.125, -0.570) # and its trailing edge from C (0.899, 0.673, -0.616), meeting at the tip D (1.81, 0.232, -1.542), the near (450, 498) -> # (594, 659), (512, 498) -> (594, 659) with the far (240, 565) -> (205, 677), (285, 572) -> (205, 677). Above the kink E-C the # root panel stands almost upright to the side body: its root A (440, 410) .. B (510, 333) cast onto X 0.80. FIN_E = np.array([0.947, 0.125, -0.570]) FIN_C = np.array([0.899, 0.673, -0.616]) FIN_TIP = np.array([1.810, 0.232, -1.542]) # The root runs the whole way along the flank, from under the collar's foot (front, low) to the capsule's front (rear, high): the # picture's broad pale wing, its lit upper face a strake. Root: A0 (300, 408) on X 0.60 .. B (510, 333) on X 0.80, through # (330, 398), (420, 365), (470, 345); its lower (leading) edge from A0 through (360, 440) on X 0.72, (400, 452) on X 0.80, # (440, 488) on X 0.90 to E; the trailing edge drops from B to C. Faceted (flat faces, a crease along the chord). FIN_ROOT = [on_plane(CAM, u, v, (1, 0, 0), x) for (u, v), x in (((300, 408), 0.60), ((330, 398), 0.62), ((420, 365), 0.72), ((470, 345), 0.76), ((510, 333), 0.80))] FIN_LE = [FIN_ROOT[0]] + [on_plane(CAM, u, v, (1, 0, 0), x) for (u, v), x in (((360, 440), 0.72), ((400, 452), 0.80), ((440, 488), 0.90))] + [FIN_E] def facet_blade(le_segs, te_segs, counts, thick, sink=0.0, crease=(0.28, 0.72)): """a faceted fin: le_segs / te_segs are lists of polylines (one per panel, root .. tip, consecutive panels sharing their ends); each panel is resampled into counts[k] levels; each level's section is a flat-sided hexagon (creases at the `crease` fractions of the chord), thickness from thick (per panel end)""" L, T, H = [], [], [] for k in range(len(le_segs)): n = counts[k] + 1 lk, tk = resample(le_segs[k], n), resample(te_segs[k], n) for i, t in enumerate(np.linspace(0, 1, n)): if k > 0 and i == 0: continue L.append(lk[i]) T.append(tk[i]) H.append(thick[k] + (thick[k + 1] - thick[k]) * t) L, T = np.array(L), np.array(T) rows = [] for i in range(len(L)): j0, j1 = max(0, i - 1), min(len(L) - 1, i + 1) span = (L[j1] + T[j1]) / 2 - (L[j0] + T[j0]) / 2 chord = T[i] - L[i] nrm = np.cross(chord, span) if np.linalg.norm(nrm) < 1e-9: # the tip: no chord left, keep the last level's normal nrm = prev_n nrm = nrm / np.linalg.norm(nrm) prev_n = nrm c0, c1 = L[i] + chord * crease[0], L[i] + chord * crease[1] h = H[i] * 0.5 rows.append([L[i], c0 + nrm * h, c1 + nrm * h * 0.75, T[i], c1 - nrm * h * 0.75, c0 - nrm * h]) if sink: for k, q in enumerate(rows[0]): d = rows[0][k] - rows[1][k] rows[0][k] = q + d / (np.linalg.norm(d) + 1e-9) * sink return loft(rows, closed=True, cap0=True) def part_fin(): V, F = facet_blade([FIN_LE, [FIN_E, FIN_TIP]], [[FIN_ROOT[-1], FIN_C], [FIN_C, FIN_TIP]], [6, 9], [0.22, 0.07, 0.012]) return part("fin", V, F, BLUEGREY, "fin_grey") def part_fin_root(): """the thick strake along the root: a faceted beam from A0 to B whose top face is the picture's lit strip""" R = resample(np.array(FIN_ROOT), 10) rows = [] for i, q in enumerate(R): t = R[min(i + 1, 9)] - R[max(i - 1, 0)] t /= np.linalg.norm(t) out = np.array([1.0, 0, 0]) up = np.cross(t, out) up /= np.linalg.norm(up) if up[2] < 0: up = -up k = min(1.0, i / 1.5, (9 - i) / 1.0) ** 0.5 if 0 < i < 9 else 0.35 w, h = 0.13 * (0.4 + 0.6 * k), 0.10 * (0.4 + 0.6 * k) sink = 0.05 + 0.15 * min(1.0, i / 4) rows.append([q - out * sink + up * h * 0.6, q + out * w * 0.2 + up * h, q + out * w, q + out * w * 0.6 - up * h, q - out * sink - up * h]) V, F = loft(rows, closed=True, cap0=True, cap1=True) return part("fin_root", V, F, BLUEGREY, "fin_grey") def part_belly(): return box(-0.30, 0.30, 1.27, 2.22, -0.62, -0.05, "belly", BLACK, "black", slope_front=0.18) def part_stern(): """the stern as the picture draws it (cast onto the centre plane): a grey ring round the capsule's end, a thin raked plate over it (y 4.62, z 0.29 .. 0.62), a boom aft to y 5.0 with a claw, a dark box under the hub (y 4.0 .. 4.55, z -0.31 .. 0.04)""" V, F = blade(np.array([[0, 4.56, 0.30], [0, 4.62, 0.63]]), np.array([[0, 4.76, 0.30], [0, 4.66, 0.63]]), 0.05, 0.03, n_span=3, n_chord=3) return [cylinder_y(4.36, 4.58, 0.19, 0.0, 0.25, name="hub_ring", cls=OTHER, mat="steel"), box(-0.20, 0.20, 4.00, 4.55, -0.31, 0.04, "stern_box", DARK, "dark_metal"), part("stern_plate", V, F, DARK, "dark_metal", smooth=False)] # the boom: tg_clips.part_tool_arm def part_prongs(): """two flat bars under the jaw's rear, side by side across the centre plane (the near one higher), sloping down aft; the picture's red runs per row give their edges (the slit between them from (290, 572)), cast onto the centre plane""" out = [] for corners, x in ((((300, 552), (334, 586), (328, 597), (288, 566)), 0.035), (((280, 556), (316, 607), (309, 619), (250, 568)), -0.035)): P = bp(corners) rows = [[(x + 0.03, q[1], q[2]) for q in P], [(x - 0.03, q[1], q[2]) for q in P]] V, F = loft(rows, closed=True, cap0=True, cap1=True) out.append(part("prong", V, F, RED, "paint_red", smooth=False)) return out # ----------------------------------------------------------------------------------------------------------- casting onto parts def cast(p, uv): """picture pixels cast onto a part's surface (the nearest hit along the camera ray); None where the ray misses""" from tg_cam import ray T, _ = p.tris() v0, e1, e2 = T[:, 0], T[:, 1] - T[:, 0], T[:, 2] - T[:, 0] out = [] for u, v in uv: o, d = ray(CAM, u, v) pv = np.cross(d, e2) det = (e1 * pv).sum(1) ok = np.abs(det) > 1e-12 inv = np.where(ok, 1.0 / np.where(ok, det, 1), 0) tv = o - v0 a = (tv * pv).sum(1) * inv qv = np.cross(tv, e1) b = (d * qv).sum(1) * inv t = (e2 * qv).sum(1) * inv hit = ok & (a >= 0) & (b >= 0) & (a + b <= 1) & (t > 0) if not hit.any(): out.append(None) continue i = np.argmin(np.where(hit, t, np.inf)) out.append(o + d * t[i]) return out exec(open(__import__("os").path.join(__import__("os").path.dirname(__file__), "tg_details.py"), encoding="utf-8").read()) if __name__ == "__main__": for p in build(): print(p.name, len(p.verts), len(p.faces))