"""Tiger: the smaller parts and build() (exec'd at the end of tg_geom.py, in its namespace; plain numpy). Each detail is placed where the picture shows it: picture pixels cast onto the part it sits on (cast()), or onto that part's flank at a station and height (cast_xz()). """ def cast_xz(p, y, z): """the point of a part's port flank at height z on station y (the outermost hit of a ray along -X)""" T, _ = p.tris() o = np.array([5.0, y, z]) d = np.array([-1.0, 0, 0]) v0, e1, e2 = T[:, 0], T[:, 1] - T[:, 0], T[:, 2] - T[:, 0] 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) return o + d * t[hit].min() if hit.any() else np.array([0.0, y, z]) def frame_of(n): """two unit vectors across the direction n""" n = np.asarray(n, float) / np.linalg.norm(n) a = np.array([0, 0, 1.0]) if abs(n[2]) < 0.9 else np.array([0, 1.0, 0]) u = np.cross(a, n) u /= np.linalg.norm(u) return u, np.cross(n, u) def surface_normal(p, q): """the part's outward (+X side) normal near the point q, from its nearest faces""" T, _ = p.tris() d = np.linalg.norm(T.mean(1) - q, axis=1) i = np.argsort(d)[:6] n = np.cross(T[i, 1] - T[i, 0], T[i, 2] - T[i, 0]) n /= np.linalg.norm(n, axis=1, keepdims=True) + 1e-12 n[n[:, 0] < 0] *= -1 m = n.mean(0) return m / np.linalg.norm(m) def disc(c, n, ru, rv, h, k=20, name="disc", cls=OTHER, mat="steel", bevel=0.3): """an oval plate on a surface: centre c, normal n, half axes ru (across, horizontal) and rv, height h, its edge rounded""" u, v = frame_of(n) n = np.asarray(n, float) / np.linalg.norm(n) rows = [] for f, z in ((1.0, -0.4 * h), (1.0, h * (1 - bevel)), (1.0 - bevel * h / max(min(ru, rv), 1e-3), h)): rows.append([c + u * ru * f * math.cos(a) + v * rv * f * math.sin(a) + n * z for a in np.linspace(0, 2 * math.pi, k, endpoint=False)]) V, F = loft(rows, closed=True, cap1=True) return part(name, V, F, cls, mat) def rod(a, b, r, k=8, name="rod", cls=OTHER, mat="steel"): a, b = np.asarray(a, float), np.asarray(b, float) d = (b - a) / np.linalg.norm(b - a) u, v = frame_of(d) rows = [[q + u * r * math.cos(t) + v * r * math.sin(t) for t in np.linspace(0, 2 * math.pi, k, endpoint=False)] for q in (a, b)] V, F = loft(rows, closed=True, cap0=True, cap1=True) return part(name, V, F, cls, mat) def surface_strip(p, pts, width, height, name, cls, mat, n_seg=24): """a trim rail along a curve on a part's surface (pts: 3D points on it), standing `height` proud, `width` wide""" P = resample(np.array(pts, float), n_seg) rows = [] for i in range(n_seg): q = P[i] n = surface_normal(p, q) t = P[min(i + 1, n_seg - 1)] - P[max(i - 1, 0)] t /= np.linalg.norm(t) w = np.cross(n, t) w /= np.linalg.norm(w) rows.append([q + w * width / 2 - n * 0.01, q + w * width / 2 + n * height, q - w * width / 2 + n * height, q - w * width / 2 - n * 0.01]) V, F = loft(rows, closed=True, cap0=True, cap1=True) return part(name, V, F, cls, mat, smooth=False) def part_cockpit(): """inside the glass, as far as the picture shows it: a dark bulkhead at the glass's rear with a wide cyan display across it (the picture's cyan through the glass, cast onto y -0.65 .. -0.9), the seat in front of it, a low console forward, the two struts of the seat's frame""" out = [] zs = float(SILL(Y_G1)) a_max = tc.angle_of_z(CP, Y_G1, zs - 0.05, CREST, GBOT, GMID) zm = float(GMID(Y_G1)) rows = [] for y in (Y_G1 - 0.02, Y_G1 - 0.05): pts = [] for a in np.linspace(a_max, -a_max, 24): x, z = tc.ring_point(CP, Y_G1, a, CREST, GBOT, GMID, HALFW) pts.append((x * 0.93, y, zm + (z - zm) * 0.93)) # down to the cockpit's floor, 15 mm inside the jaw's inner walls at every height (they run from the sill, x_s z_s, # to the floor, x_f z_f, and close in toward the floor as the jaw narrows) x_s, z_s = jaw_top(Y_G1) z_f = COCKPIT_FLOOR_Z x_f = flank_x(Y_G1, z_f) - 0.03 z1 = pts[-1][2] def wall(z): return x_s + (x_f - x_s) * (z_s - z) / (z_s - z_f) - 0.015 zs_ = [z1 - 0.02, 0.5 * (z1 + z_f), z_f + 0.005] pts += [(-min(abs(pts[-1][0]), wall(z)), y, z) for z in zs_] + [(min(pts[0][0], wall(z)), y, z) for z in zs_[::-1]] rows.append(pts) V, F = loft(rows, closed=True, cap0=True, cap1=True) out.append(part("bulkhead", V, F, DARK, "cockpit_dark")) rows = [[(x, Y_G1 - 0.075 - 0.10 * (x / 0.42) ** 2, z) for x in np.linspace(0.42, -0.42, 9)] for z in (0.10, 0.44)] V, F = loft(rows, closed=False) out.append(part("display", V, F, DARK, "display_cyan")) out.append(box(-0.21, 0.21, -1.02, -0.78, -0.12, -0.03, "seat_pan", DARK, "seat")) back = np.array([[0, -0.80, -0.03], [0, -0.70, 0.40]]) rows = [] for q in back: rows.append([q + np.array([sx * 0.19, sy * 0.045, 0]) for sx, sy in ((1, -1), (-1, -1), (-1, 1), (1, 1))]) V, F = loft(rows, closed=True, cap0=True, cap1=True) out.append(part("seat_back", V, F, DARK, "seat", smooth=False)) out.append(box(-0.13, 0.13, -0.74, -0.66, 0.42, 0.55, "headrest", DARK, "seat")) out.append(box(-0.17, 0.17, -0.99, -0.81, -0.03, 0.00, "cushion", RED, "seat_red")) out.append(box(-0.22, 0.22, -1.40, -1.20, -0.10, 0.00, "console", DARK, "cockpit_dark")) for xc in (-0.11, 0.11): out.append(box(xc - 0.09, xc + 0.09, -1.36, -1.24, 0.00, 0.012, "console_screen", DARK, "screen_cyan")) for sx in (1, -1): out.append(rod((0.33 * sx, -1.05, -0.10), (0.22 * sx, -0.68, 0.50), 0.022, name="strut", cls=DARK, mat="cockpit_dark")) return out def part_recess_details(): """on the grey recess behind the collar (cast from the picture): the big round cover (y 0.10 .. 0.63, z 0.37 .. 0.73), the dark port at its lower front, a short white lamp bar above it""" core = part_core() c = np.array([0.43, 0.345, 0.55]) n = surface_normal(core, c) out = [disc(c, n, 0.26, 0.18, 0.035, k=28, name="recess_cover", cls=BLUEGREY, mat="recess_grey")] out.append(disc(np.array([0.445, 0.07, 0.43]) + n * 0.03, n, 0.045, 0.045, 0.02, k=12, name="recess_port", cls=BLACK, mat="black")) a, b = np.array([0.40, -0.02, 0.575]) + n * 0.02, np.array([0.39, 0.06, 0.61]) + n * 0.02 out.append(rod(a, b, 0.012, k=6, name="recess_lamp", cls=PALE, mat="lamp_white")) return out def part_capsule_details(): cap = part_capsule() out = [surface_strip(cap, [(0.444, 1.104, -0.004), (0.51, 1.266, 0.10), (0.527, 1.50, 0.222), (0.49, 1.80, 0.346), (0.40, 2.05, 0.43), (0.294, 2.255, 0.495)], 0.045, 0.012, "stripe", BLACK, "trim_dark")] # the rear window: a low glass blister over a bay on the flank (its outline cast from the picture), a dark frame outline = [(2.34, 0.13), (2.45, 0.27), (2.64, 0.347), (2.90, 0.39), (3.166, 0.395), (3.40, 0.38), (3.631, 0.346), (3.73, 0.27), (3.768, 0.153), (3.75, 0.02), (3.69, -0.098), (3.40, -0.12), (3.12, -0.121), (2.80, -0.11), (2.532, -0.093), (2.40, 0.0)] base = np.array([cast_xz(cap, y, z) for y, z in outline]) cen = base.mean(0) nrm = surface_normal(cap, cen) # each ring of the blister lifted from the capsule's own skin under it (the outline's centroid lies inside the curved skin) def lifted(p, lift): s = cast_xz(cap, p[1], p[2]) return s + nrm * lift rows = [[lifted(cen + (q - cen) * f, lift) for q in base] for f, lift in ((1.0, -0.01), (1.0, 0.03), (0.96, 0.07), (0.82, 0.10), (0.55, 0.115), (0.25, 0.12))] V, F = loft(rows, closed=True) m = len(base) tip = len(V) V = np.concatenate([V, [lifted(cen, 0.121)]]) F += [(tip - m + k, tip - m + (k + 1) % m, tip) for k in range(m)] # a fan to the centre, not a zigzag cap out.append(part("window", V, F, GLASS, "window_glass")) rows = [[cen + (q - cen) * f + nrm * lift for q in base] for f, lift in ((1.06, -0.01), (1.06, 0.035), (1.0, 0.035), (1.0, -0.01))] V, F = loft(rows, closed=True) out.append(part("window_frame", V, F, DARK, "trim_dark", smooth=False)) # in the bay, as the picture shows through the glass: a white box and an orange canister, standing on the capsule's skin # (5 mm off it) under the blister (which rises 0.10-0.12 m over the skin there) xb = max(cast_xz(cap, y, z)[0] for y in (3.20, 3.45) for z in (0.08, 0.22)) out.append(box(xb + 0.005, xb + 0.06, 3.20, 3.45, 0.08, 0.22, "bay_box", PALE, "bay_white")) xc = max(cast_xz(cap, y, z)[0] for y in (2.85, 3.10) for z in (0.14, 0.22)) out.append(cylinder_y(2.85, 3.10, 0.038, xc + 0.043, 0.18, n=12, name="bay_canister", cls=ORANGE, mat="paint_orange")) return out def conform_plate(surf, u0, v0, u1, v1, nu, nv, lift, sink=0.012, name="plate", cls=PALE, mat="white_plate"): """a thin plate lying on a part's surface over the picture's rectangle (u0, v0) .. (u1, v1): a grid of camera rays cast onto the surface, its top `lift` proud along the surface normal, its underside `sink` below; mirrored by the caller""" us, vs = np.linspace(u0, u1, nu), np.linspace(v0, v1, nv) hits = cast(surf, [(u, v) for v in vs for u in us]) P = np.array(hits, float).reshape(nv, nu, 3) N = np.array([surface_normal(surf, q) for q in P.reshape(-1, 3)]).reshape(nv, nu, 3) top, bot = P + N * lift, P - N * sink V = np.concatenate([top.reshape(-1, 3), bot.reshape(-1, 3)]) F = [] n = nu * nv for j in range(nv - 1): for i in range(nu - 1): a, b_, c, d = j * nu + i, j * nu + i + 1, (j + 1) * nu + i + 1, (j + 1) * nu + i F.append((a, b_, c, d)) F.append((n + d, n + c, n + b_, n + a)) ring = [j * 0 + i for i in range(nu)] + [(j + 1) * nu - 1 for j in range(1, nv)] + \ [(nv - 1) * nu + i for i in range(nu - 2, -1, -1)] + [j * nu for j in range(nv - 2, 0, -1)] for k in range(len(ring)): a, b_ = ring[k], ring[(k + 1) % len(ring)] F.append((b_, a, n + a, n + b_)) return part(name, V, F, cls, mat, smooth=False) def part_white_patch(): """the white plate on the shell's front behind the glass (the picture's (291, 266) .. (331, 281)), a dark mark on it at (313, 268) .. (323, 274); both lie on the surface (cast grids), 12 and 20 mm proud""" top = merge([part_collar(), part_brow(), part_shell()], "top") return [conform_plate(top, 289, 265, 333, 282, 5, 3, 0.012, name="white_patch"), conform_plate(top, 312, 268, 324, 275, 2, 2, 0.020, name="white_patch_mark", cls=BLACK, mat="black")] def part_stern_details(): out = [disc(np.array([0.20, 4.06, 0.0]), np.array([0.75, 0.66, 0.0]), 0.06, 0.06, 0.04, k=14, name="stern_lens", cls=RED, mat="lens_orange")] return out def merge(parts, name, smooth=True): V, F, C, M = [], [], [], [] off = 0 for p in parts: V.append(p.verts) F += [tuple(i + off for i in f) for f in p.faces] C += p.cls M += p.mat off += len(p.verts) return Part(name, np.concatenate(V), F, C, M, smooth) # ----------------------------------------------------------------------------------------------------------- build def build(posed=True): parts = part_canopy() + [ part_brow(), part_collar(), part_shell(), part_core(), part_capsule(), part_jaw(), fin_from(DORSAL_UV, 9, 0.12, 0.02, "dorsal", ORANGE, "paint_orange"), fin_from(TAIL_UV, 4, 0.10, 0.02, "tail", ORANGE, "paint_orange"), fin_from(VENTRAL_UV, 1, 0.08, 0.015, "ventral", DARK, "dark_metal", sink=0.10), mirrored(part_fin(), "fins"), mirrored(part_fin_root(), "fin_roots"), part_belly()] parts += part_stern() + part_prongs() + part_cockpit() parts += [mirrored(q) for q in part_recess_details() + part_capsule_details() + part_white_patch()] parts += [mirrored(q) for q in part_stern_details()] clips, _ = part_clips() parts += clips arm, knuckle = part_tool_arm() parts.append(knuckle) arm.extra["origin"] = HINGE.copy() # its own object, origin on the hinge (tg_build) if posed: # the picture's pose for the overlays: as drawn, pointing aft arm = Part(arm.name, arm.verts + HINGE, arm.faces, arm.cls, arm.mat, arm.smooth, dict(arm.extra)) parts.append(arm) return parts exec(open(__import__("os").path.join(__import__("os").path.dirname(__file__), "tg_clips.py"), encoding="utf-8").read())