#!/usr/bin/env python3 """ Generate vendor/plane3d.js - the low-poly airliner used by the flight layer. The mesh is built here from primitives (no source model, no textures), so the asset is CC0 by construction and reproducible: python3 tools/plane_gen.py # writes vendor/plane3d.js Output format (exactly what clouds3d.js expects): window.__planeVerts = '' of non-indexed triangles, interleaved per vertex: pos(3), normal(3), colIdx(1), float32 little endian. colIdx: 0 = body (light albedo), 1 = glazing (dark albedo). Convention: forward = +Z, up = +Y, nose-to-tail ~2.0 (the renderer scales the model with uS and assumes that ~2.0 span, see PLANE_VERT_SRC). """ import base64 import math import os import struct ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) BODY, GLASS = 0.0, 1.0 SIDES = 10 TRIS = [] def add_tri(a, b, c, col, ref): """Store one triangle; its flat normal is oriented away from ref.""" ux, uy, uz = b[0] - a[0], b[1] - a[1], b[2] - a[2] vx, vy, vz = c[0] - a[0], c[1] - a[1], c[2] - a[2] nx, ny, nz = uy * vz - uz * vy, uz * vx - ux * vz, ux * vy - uy * vx ln = math.sqrt(nx * nx + ny * ny + nz * nz) if ln < 1e-9: return nx, ny, nz = nx / ln, ny / ln, nz / ln mx = (a[0] + b[0] + c[0]) / 3.0 - ref[0] my = (a[1] + b[1] + c[1]) / 3.0 - ref[1] mz = (a[2] + b[2] + c[2]) / 3.0 - ref[2] if nx * mx + ny * my + nz * mz < 0: nx, ny, nz = -nx, -ny, -nz TRIS.append((a, b, c, col, (nx, ny, nz))) def add_quad(a, b, c, d, col, ref): add_tri(a, b, c, col, ref) add_tri(a, c, d, col, ref) def ring(z, r, cy, cx=0.0, phase=0.0): pts = [] for i in range(SIDES): ang = phase + 2.0 * math.pi * i / SIDES pts.append((cx + r * math.sin(ang), cy + r * math.cos(ang), z)) return pts def add_cap(r, ref, col=BODY): cx = sum(p[0] for p in r) / len(r) cy = sum(p[1] for p in r) / len(r) cz = sum(p[2] for p in r) / len(r) for i in range(len(r)): add_tri((cx, cy, cz), r[i], r[(i + 1) % len(r)], col, ref) def tube(stations, cy, ref, cx=0.0, col=BODY): """stations: [(z, radius), ...] from one end to the other, caps closed.""" rings = [ring(z, r, cy, cx) for (z, r) in stations] for k in range(len(rings) - 1): a, b = rings[k], rings[k + 1] for i in range(SIDES): j = (i + 1) % SIDES add_quad(a[i], a[j], b[j], b[i], col, ref) add_cap(rings[0], ref, col) add_cap(rings[-1], ref, col) def slab(root_le, root_te, tip_le, tip_te, half_t): """Tapered wing/stabilizer: planform corners extruded by +-half_t in y.""" def off(p, dy): return (p[0], p[1] + dy, p[2]) tlr, ttr = off(root_le, half_t), off(root_te, half_t) tlt, ttt = off(tip_le, half_t), off(tip_te, half_t) blr, btr = off(root_le, -half_t), off(root_te, -half_t) blt, btt = off(tip_le, -half_t), off(tip_te, -half_t) ref = ((root_le[0] + tip_le[0]) / 2.0, root_le[1] - 0.25, (root_le[2] + tip_le[2]) / 2.0) add_quad(tlr, tlt, ttt, ttr, BODY, ref) # top skin add_quad(blr, btr, btt, blt, BODY, ref) # bottom skin add_quad(tlr, blr, blt, tlt, BODY, ref) # leading edge add_quad(ttr, ttt, btt, btr, BODY, ref) # trailing edge add_quad(tlt, ttt, btt, blt, BODY, ref) # tip def plate(outline, x, thickness, col=BODY): """Thin plate: outline is a list of (z, y), extruded around x.""" a = [(x - thickness / 2.0, p[1], p[0]) for p in outline] b = [(x + thickness / 2.0, p[1], p[0]) for p in outline] ym = sum(p[1] for p in outline) / len(outline) zm = sum(p[0] for p in outline) / len(outline) for i in range(1, len(a) - 1): add_tri(a[0], a[i], a[i + 1], col, (x - 1.0, ym, zm)) add_tri(b[0], b[i + 1], b[i], col, (x + 1.0, ym, zm)) for i in range(len(a)): j = (i + 1) % len(a) add_quad(a[i], a[j], b[j], b[i], col, (x, ym, zm)) def window_band(ang_mid, cy, r, z0, z1, step, h, col=GLASS): """Glazing quads on the fuselage skin around the section angle ang_mid.""" x = r * 1.02 * math.sin(ang_mid) ym = cy + r * 1.02 * math.cos(ang_mid) y_lo, y_hi = ym - h / 2.0, ym + h / 2.0 ref = (0.0, cy, 0.5 * (z0 + z1)) z = z0 while z + step <= z1 + 1e-9: za, zb = z + step * 0.18, z + step * 0.82 add_quad((x, y_hi, za), (x, y_hi, zb), (x, y_lo, zb), (x, y_lo, za), col, ref) z += step def build(): cy = 0.115 # fuselage: nose cone, parallel section, tail cone tube([(0.97, 0.020), (0.88, 0.075), (0.78, 0.105), (0.70, 0.115), (-0.55, 0.115), (-0.75, 0.095), (-0.90, 0.055), (-1.00, 0.015)], cy, (0.0, cy, 0.0)) # wings: swept and tapered, slight dihedral (root chord 0.56, tip 0.18) for s in (1.0, -1.0): slab((s * 0.09, 0.055, 0.30), (s * 0.09, 0.055, -0.26), (s * 0.91, 0.125, -0.28), (s * 0.91, 0.125, -0.46), 0.016) # winglet: small plate standing up at the tip plate([(-0.28, 0.125), (-0.46, 0.125), (-0.46, 0.235), (-0.28, 0.215)], s * 0.912, 0.014) # horizontal stabilizers for s in (1.0, -1.0): slab((s * 0.07, 0.135, -0.66), (s * 0.07, 0.135, -0.94), (s * 0.42, 0.165, -0.86), (s * 0.42, 0.165, -1.00), 0.011) # vertical fin (outline in the (z, y) plane) plate([(-0.48, 0.20), (-0.60, 0.60), (-0.86, 0.60), (-1.00, 0.26)], 0.0, 0.026) # engine nacelles under the wings, with a thin pylon each for s in (1.0, -1.0): ex, ey = s * 0.36, -0.055 tube([(0.24, 0.030), (0.18, 0.072), (-0.06, 0.075), (-0.14, 0.045)], ey, (ex, ey, 0.05), cx=ex) plate([(0.16, 0.015), (-0.02, 0.015), (-0.02, 0.075), (0.16, 0.075)], ex, 0.030) # window bands on both flanks, cockpit glazing on the upper flanks d = math.radians window_band(d(90), cy, 0.115, -0.42, 0.58, 0.075, 0.048) window_band(d(270), cy, 0.115, -0.42, 0.58, 0.075, 0.048) window_band(d(54), cy, 0.115, 0.64, 0.82, 0.09, 0.055) window_band(d(306), cy, 0.115, 0.64, 0.82, 0.09, 0.055) def emit(): out = bytearray() for (a, b, c, col, n) in TRIS: for p in (a, b, c): out += struct.pack('