# Spearfish: the hull, its openings, glass, cabin lining and cabin (exec'd by build_spearfish.py after section 1). # # Topology (rebuilt 2026-10-20 after the user: "the topology is terrible fix the topology and make this as a game ready asset"): # * one quad grid: 64 columns round, placed by arc length on each section (even faces, no bunching at the shoulders), and # stations spaced by need (about 13 cm amidships, 4 cm at the nose tip, 5 cm at the stern, 6 cm along the openings); # * the canopy, the side windows and the hatch are rectangles of that grid: on the stations along an opening, the band's # columns are pinned to the opening's outline and relax back to their even places within 0.35 m past its ends. No # snapped vertices, no slivers; # * each opening has a frame in the hull's own topology: the ring of faces round the hole, its inner edge raised 10 mm # (6 mm at the hatch), and a sleeve straight down to the cabin lining, whose hole meets it vertex for vertex; # * the glass is the hole's own grid sunk 22 mm; the lining is the grid sunk 60 mm (the same columns, one shell); # * ends: the stern closes with a recessed panel filled by a quad grid; the nose ends in a fan at its point (a cone tip); # * panel seams are not geometry: two per-vertex fields (seam_ring, seam_long: 5 + signed distance in metres, 0 where # there is none) that the surfaces pass draws into the paint and bakes into the bevel normal. exec(open(os.path.join(HERE, "sf_topo.py")).read()) M = 64 HALF = M // 2 # port columns j = 0 (keel) .. 32 (top), starboard mirrored: k = 64 - j J_B, J_T = 7, 15 # window band (the hole between them), frames on j 6..7 and 15..16; eight faces across the # glass since the canopy grew to the user's drawing (2026-10-20): 1.1 m high, 14 cm a face J_H = 29 # hatch: hole over j 29 .. 35 across the top, frame 28..29 FR_W, FR_H = 0.045, 0.040 # frame widths (arc on the skin) RIM_W, RIM_H = 0.010, 0.006 # how far each frame's inner edge stands proud GLASS_D, LINER = 0.022, 0.060 RELAX = 0.35 # columns relax to their even places this far past an opening HMIN = 0.03 # an opening's ends are cut where its outline is lower than this (the frame covers the rest) NSEC = 360 def half_table(y): th = [-math.pi / 2 + math.pi * i / NSEC for i in range(NSEC + 1)] pts = [sec(y, t) for t in th] arc = [0.0] for i in range(1, len(pts)): arc.append(arc[-1] + (pts[i] - pts[i - 1]).length) return th, arc def interp(xs, ys, x): if x <= xs[0]: return ys[0] if x >= xs[-1]: return ys[-1] lo, hi = 0, len(xs) - 1 while hi - lo > 1: mid = (lo + hi) // 2 if xs[mid] <= x: lo = mid else: hi = mid t = (x - xs[lo]) / (xs[hi] - xs[lo]) return ys[lo] + (ys[hi] - ys[lo]) * t def theta_of_z(y, z): """port flank angle of the point at height z""" zm = ZMf(y); top = z >= zm n = NT(y) if top else NB(y); H = HTf(y) if top else HBf(y) q = min(1.0, abs(z - zm) / H) ** (n / 2.0) return math.copysign(math.asin(min(1.0, q)), z - zm) def theta_of_lat(y, lat): """angle of the point on the top at lateral offset lat""" n = NT(y); W = Wf(y) c = min(1.0, abs(lat) / W) ** (n / 2.0) return math.acos(min(1.0, c)) def side_span(poly, y): zs = [] n = len(poly) for i in range(n): (y1, z1), (y2, z2) = poly[i], poly[(i + 1) % n] if (y1 - y) * (y2 - y) <= 0 and y1 != y2: zs.append(z1 + (z2 - z1) * (y - y1) / (y2 - y1)) return (min(zs), max(zs)) if len(zs) >= 2 else None def opening_range(poly): ys = [p[0] for p in poly] a, b = min(ys), max(ys) ok = [] for i in range(801): y = a + (b - a) * i / 800 s = side_span(poly, y) if s and s[1] - s[0] >= HMIN: ok.append(y) return min(ok), max(ok) WINDOWS = [] for poly in (P_CANOPY, P_SIDEWIN): ya, yb = opening_range(poly) WINDOWS.append(dict(poly=poly, ya=ya, yb=yb)) (hxa, hwa), (hxf, hwf) = HATCH H_IN = 0.045 # the hole is the lid's trapezoid less 4.5 cm all round HATCH_YA, HATCH_YB = Yi(hxf) + H_IN, Yi(hxa) - H_IN def hatch_half(y): t = (y - Yi(hxf)) / (Yi(hxa) - Yi(hxf)) return (hwf + (hwa - hwf) * t) * S - H_IN def weight(y, a, b, pad): if a <= y <= b: return 1.0 d = (a - y) if y < a else (y - b) return 1.0 - smoothstep(pad, pad + RELAX, d) # ------------------------------------------------------------------ stations def spacing(y): x = ximg(y) h = 0.13 dn = y - Y_NOSE # distance from the nose tip h = min(h, 0.035 + 0.095 * smoothstep(0.0, 1.1, dn)) ds = Y_TAIL - y # distance from the stern face h = min(h, 0.05 + 0.08 * smoothstep(0.0, 0.5, ds)) for w in WINDOWS: if w["ya"] - 0.1 <= y <= w["yb"] + 0.1: h = min(h, 0.065) for e in (w["ya"], w["yb"]): # closer at a window's ends, where its edge turns: the hole follows it if abs(y - e) < 0.18: h = min(h, 0.025 + 0.04 * smoothstep(0.05, 0.18, abs(y - e))) if HATCH_YA - 0.1 <= y <= HATCH_YB + 0.1: h = min(h, 0.07) return h Y0 = Yi(X_TIP - 2.5) # first ring, 4 cm behind the point special = [] for w in WINDOWS: special += [w["ya"], w["yb"]] special += [HATCH_YA - FR_H, HATCH_YA, HATCH_YB, HATCH_YB + FR_H] ys = [Y0] while ys[-1] < Y_TAIL - 1e-6: ys.append(min(Y_TAIL, ys[-1] + spacing(ys[-1] + 0.5 * spacing(ys[-1])))) for s in special: ys = [y for y in ys if abs(y - s) > 0.45 * spacing(s)] + [s] ys = sorted(set(round(y, 6) for y in ys)) if Y_TAIL - ys[-1] > 1e-4: ys.append(Y_TAIL) YS = ys NR = len(YS) def st(y): return min(range(NR), key=lambda i: abs(YS[i] - y)) for w in WINDOWS: w["ia"], w["ib"] = st(w["ya"]), st(w["yb"]) H_IA, H_IB = st(HATCH_YA), st(HATCH_YB) H_FA, H_FB = st(HATCH_YA - FR_H), st(HATCH_YB + FR_H) # ------------------------------------------------------------------ columns per station (arc length, pinned at the openings) def columns(y): th, arc = half_table(y) L = arc[-1] def a_of(t): return interp(th, arc, t) ctrl = {0: 0.0, HALF: L} dflt = lambda j: L * j / HALF ww, wo = 0.0, None for w in WINDOWS: g = weight(y, w["ya"], w["yb"], FR_W) if g > ww: ww, wo = g, w if wo is not None and ww > 0: yc = min(max(y, wo["ya"]), wo["yb"]) zlo, zhi = side_span(wo["poly"], yc) aB, aT = a_of(theta_of_z(y, zlo)), a_of(theta_of_z(y, zhi)) for j, tgt in ((J_B - 1, aB - FR_W), (J_B, aB), (J_T, aT), (J_T + 1, aT + FR_W)): ctrl[j] = dflt(j) + (tgt - dflt(j)) * ww wh = weight(y, HATCH_YA, HATCH_YB, FR_H) if wh > 0: yc = min(max(y, HATCH_YA), HATCH_YB) hw = hatch_half(yc) aE, aF = a_of(theta_of_lat(y, hw)), a_of(theta_of_lat(y, hw + FR_H)) for j, tgt in ((J_H - 1, aF), (J_H, aE)): ctrl[j] = dflt(j) + (tgt - dflt(j)) * wh keys = sorted(ctrl) vals = [ctrl[k] for k in keys] for i in range(1, len(vals)): # keep them in order vals[i] = max(vals[i], vals[i - 1] + 0.004) a_j = [interp(keys, vals, j) for j in range(HALF + 1)] t_j = [interp(arc, th, a) for a in a_j] t_j[0], t_j[HALF] = -math.pi / 2, math.pi / 2 # full ring: port 0..32, starboard 33..63 (mirror of 31..1) return t_j + [math.pi - t_j[HALF - k] for k in range(1, HALF)] TH = [columns(y) for y in YS] BASE = [[sec(YS[i], t) for t in TH[i]] for i in range(NR)] NRM = [[normal_at(p) for p in row] for row in BASE] def kband(j0, j1): """the ring indices of a port band j0..j1 and its starboard mirror""" return list(range(j0, j1)), [M - j for j in range(j1, j0, -1)] # roles of the grid faces (i, k): i between station i and i+1, k between column k and k+1 ROLE = {} for i in range(NR - 1): for k in range(M): ROLE[(i, k)] = "skin" for w in WINDOWS: # the windows' frames are swept trims over the hole's edge (below) for ks in kband(J_B, J_T): for i in range(w["ia"], w["ib"]): for k in ks: ROLE[(i, k)] = ("hole", w) for i in range(H_IA, H_IB): for k in range(J_H, M - J_H): ROLE[(i, k)] = ("hole", None) for i in list(range(H_FA, H_IA)) + list(range(H_IB, H_FB)): for k in range(J_H, M - J_H): ROLE[(i, k)] = "frame" for i in range(H_FA, H_FB): for k in (J_H - 1, M - J_H): ROLE[(i, k)] = "frame" def hole_loop(ia, ib, k0, k1): """the hole's boundary as grid vertices, in order round it""" return ([(i, k0) for i in range(ia, ib)] + [(ib, k) for k in range(k0, k1)] + [(i, k1) for i in range(ib, ia, -1)] + [(ia, k) for k in range(k1, k0, -1)]) HOLES = [] for w in WINDOWS: HOLES.append((w["ia"], w["ib"], J_B, J_T, RIM_W, True)) HOLES.append((w["ia"], w["ib"], M - J_T, M - J_B, RIM_W, True)) HOLES.append((H_IA, H_IB, J_H, M - J_H, RIM_H, False)) RAISE = {} for ia, ib, k0, k1, rim, glazed in HOLES: if glazed: continue for ik in hole_loop(ia, ib, k0, k1): RAISE[ik] = rim def P(ik): i, k = ik k %= M p = BASE[i][k] return p + NRM[i][k] * RAISE.get((i, k), 0.0) # seam fields (5 + signed distance; 0 where a vertex has none) SEAM_YS = [Yi(x) for x in SEAMS_X] def seam_ring(p): d = min((p.y - sy for sy in SEAM_YS), key=abs) return 5.0 + d if abs(d) < 0.30 else 0.0 dx0, dx1, dlat = DECK_SEAM def seam_long(p): if not (Yi(dx1) <= p.y <= Yi(dx0)) or p.z < ZMf(p.y): return 0.0 d = abs(p.x) - dlat * S return 5.0 + d if abs(d) < 0.30 else 0.0 # ------------------------------------------------------------------ the hull mesh: skin, frames, sleeves, nose tip, stern panel hull = MB("SM_Spearfish_Hull", [PAINT, SEAM, GRAPH]) MI_P, MI_G = hull.mi(PAINT), hull.mi(GRAPH) bm = bmesh.new() lay_r = bm.verts.layers.float.new("seam_ring") lay_l = bm.verts.layers.float.new("seam_long") V = {} def hv(ik): i, k = ik[0], ik[1] % M if (i, k) not in V: p = P((i, k)) v = bm.verts.new(p) v[lay_r] = seam_ring(p); v[lay_l] = seam_long(p) V[(i, k)] = v return V[(i, k)] for (i, k), role in ROLE.items(): if isinstance(role, tuple): continue f = bm.faces.new([hv(a) for a in ((i, k), (i, (k + 1) % M), (i + 1, (k + 1) % M), (i + 1, k))]) f.material_index = MI_G if role == "frame" else MI_P # sleeves: from each raised hole edge down to the lining (through the glass's edge) SLEEVE = {} for ia, ib, k0, k1, rim, glazed in HOLES: if glazed: continue # the windows' swept frames carry their own sleeves loop = hole_loop(ia, ib, k0, k1) rows = [[hv(ik) for ik in loop]] for d in (GLASS_D, LINER): row = [] for ik in loop: i, k = ik[0], ik[1] % M row.append(bm.verts.new(BASE[i][k] - NRM[i][k] * d)) rows.append(row) ring_rows(bm, rows, closed=True, mat_index=MI_G) SLEEVE[(ia, ib, k0, k1)] = loop # nose: a fan to the point (a cone tip) tipv = bm.verts.new((0, Y_NOSE, Zi(419.0))) for k in range(M): bm.faces.new((hv((0, (k + 1) % M)), hv((0, k)), tipv)) # stern: the last ring steps in and down into a recessed panel, filled by a quad grid iE = NR - 1 cen = Vector((0, YS[iE], ZMf(YS[iE]))) rE = [hv((iE, k)) for k in range(M)] rows = [rE] for sc_, dy in ((0.94, 0.0), (0.90, -0.045), (0.80, -0.045)): rows.append([bm.verts.new(cen + (v.co - cen) * sc_ + Vector((0, dy, 0))) for v in rE]) ring_rows(bm, rows[:3], closed=True, mat_index=MI_P) ring_rows(bm, rows[2:], closed=True, mat_index=MI_G) coons_cap(bm, rows[-1][::-1], MI_G) bm.normal_update() bmesh.ops.recalc_face_normals(bm, faces=bm.faces[:]) # one connected surface: consistent winding top_f = max(bm.faces, key=lambda f: f.calc_center_median().z) if top_f.normal.z < 0: bmesh.ops.reverse_faces(bm, faces=bm.faces[:]) bm.normal_update() hull.merge(bm, None) REPORT["hull_stations"] = NR REPORT["hull_columns"] = M # ------------------------------------------------------------------ glass: each window's smooth outline, sunk 22 mm # (its own ladder of quads over the outline, so its edge is the frame's sleeve exactly; the hull's hole, which follows the # same outline station by station, lies under the frame's lip) glass = MB("SM_Spearfish_Glass", [GLASS]) GLASS_POLYS = [P_CANOPY, P_SIDEWIN] def glass_patch(poly, sg, step=0.03, rows=8): ys_ = [p[0] for p in poly] y0, y1 = min(ys_) + 0.002, max(ys_) - 0.002 n = max(2, int(math.ceil((y1 - y0) / step))) cols = [] for j in range(n + 1): t = j / n t = 0.5 - 0.5 * math.cos(math.pi * t) # closer together at the ends, where the outline turns y = y0 + (y1 - y0) * t sp = _span(poly, y) if sp is None: continue lo, hi = sp col = [] for r in range(rows + 1): z = lo + (hi - lo) * r / rows p = on_side(sg, y, z) col.append(p - normal_at(p) * GLASS_D) cols.append(col) bmg = bmesh.new() V_ = [[bmg.verts.new(p) for p in col] for col in cols] for j in range(len(V_) - 1): for r in range(rows): bmg.faces.new((V_[j][r], V_[j + 1][r], V_[j + 1][r + 1], V_[j][r + 1])) bmg.normal_update() out = sum(f.normal.dot(Vector((f.calc_center_median().x, 0, 0))) for f in bmg.faces) if out < 0: bmesh.ops.reverse_faces(bmg, faces=bmg.faces[:]) glass.merge(bmg, GLASS) for sg in (1, -1): for poly in GLASS_POLYS: glass_patch(poly, sg) # ------------------------------------------------------------------ cabin lining: the grid sunk 60 mm, from the nose to the rear bulkhead CAB_Y0, CAB_Y1 = Yi(1104.0), Yi(669.0) I_CAB = [i for i, y in enumerate(YS) if CAB_Y0 - 1e-6 <= y <= CAB_Y1 + 1e-6] i_c0, i_c1 = I_CAB[0], I_CAB[-1] cab = MB("SM_Spearfish_Cabin", [CAB, FLOOR, SEAT, SCREEN, CABGLOW, GRAPH, STEEL]) bml = bmesh.new() LV = {} def lv(i, k): k %= M if (i, k) not in LV: LV[(i, k)] = bml.verts.new(BASE[i][k] - NRM[i][k] * LINER) return LV[(i, k)] for i in range(i_c0, i_c1): for k in range(M): if isinstance(ROLE[(i, k)], tuple): continue bml.faces.new((lv(i, k), lv(i + 1, k), lv(i + 1, (k + 1) % M), lv(i, (k + 1) % M))) coons_cap(bml, [lv(i_c1, k) for k in range(M)]) coons_cap(bml, [lv(i_c0, k) for k in range(M)][::-1]) bml.normal_update() for f in bml.faces: # seen from inside: toward the axis line c = f.calc_center_median() ax = Vector((0, c.y, ZMf(min(max(c.y, Y_NOSE), Y_TAIL)))) if abs(f.normal.y) < 0.9 and f.normal.dot(ax - c) < 0: f.normal_flip() elif abs(f.normal.y) >= 0.9 and ((c.y > (CAB_Y0 + CAB_Y1) / 2) == (f.normal.y > 0)): f.normal_flip() cab.merge(bml, CAB) # ------------------------------------------------------------------ window frames: swept trims of one constant section # Along each window's smooth outline (the glass's edge): an outer flange 40 mm out on the skin (its edge sunk 4 mm into it), # a rounded top 12 mm proud, an inner lip 12 mm over the glass, and a sleeve straight down from the outline to the cabin # lining (60 mm), against which the glass's edge sits. Quads only, one loop per profile point. It covers the hull's hole # edge (which follows the outline station by station) and the lining's. FR_PROF = [(0.040, -0.004), (0.036, 0.007), (0.022, 0.012), (0.002, 0.012), (-0.012, 0.007), (-0.012, -0.002), (0.0, -0.006), (0.0, -0.060)] def adaptive(poly, max_step=0.06, max_turn_deg=6.0): """points of a smooth closed outline: one every max_step, closer where it turns (one per max_turn_deg of turning)""" pts = resample(poly, 0.005) n = len(pts) keep = [0] acc_d = acc_t = 0.0 for i in range(1, n): (y0, z0), (y1, z1), (y2, z2) = pts[i - 1], pts[i], pts[(i + 1) % n] a1 = math.atan2(z1 - z0, y1 - y0); a2 = math.atan2(z2 - z1, y2 - y1) acc_t += abs((a2 - a1 + math.pi) % (2 * math.pi) - math.pi) acc_d += math.hypot(y1 - y0, z1 - z0) if acc_d >= max_step or acc_t >= math.radians(max_turn_deg): keep.append(i); acc_d = acc_t = 0.0 return [pts[i] for i in keep] def frame_sweep(poly, sg): pts2 = adaptive(poly) n = len(pts2) area = sum(pts2[i][0] * pts2[(i + 1) % n][1] - pts2[(i + 1) % n][0] * pts2[i][1] for i in range(n)) ccw = area > 0 P3, N3, O3 = [], [], [] for j in range(n): (ym, zm_), (y, z), (yp, zp) = pts2[j - 1], pts2[j], pts2[(j + 1) % n] ty, tz = yp - ym, zp - zm_ L = math.hypot(ty, tz) or 1.0 oy, oz = ((tz / L, -ty / L) if ccw else (-tz / L, ty / L)) # outward, in the side view p = on_side(sg, y, z) q = on_side(sg, y + oy * 0.01, z + oz * 0.01) nrm = normal_at(p) o = q - p o = (o - o.dot(nrm) * nrm).normalized() P3.append(p); N3.append(nrm); O3.append(o) loops = [[P3[j] + O3[j] * a + N3[j] * h for j in range(n)] for a, h in FR_PROF] grid_rings(frames, loops, GRAPH, expect=(2, n // 4, tuple(N3[n // 4]))) # canopy mullions: low bars across the glass. Each keeps the blueprint's place and slant (MULLIONS), runs across the whole # glass and ends against the frame's sleeve; it sits 2 mm over the glass. def mullion_span(a, b): A = Vector((Yi(a[0]), Zi(a[1]))); B = Vector((Yi(b[0]), Zi(b[1]))) d = (B - A).normalized() M_ = (A + B) / 2 ends = [] for sgn in (-1, 1): t = 0.0 while pip(tuple(M_ + d * sgn * (t + 0.002)), P_CANOPY) and t < 3.0: t += 0.002 ends.append(M_ + d * sgn * t) return tuple(ends[0]), tuple(ends[1]) def mullion(a, b, sg): pts2 = resample([a, b], 0.06, closed=False) Pp = [on_side(sg, y, z) for y, z in pts2] rows = [] for (w, h) in ((-0.014, -0.020), (-0.014, 0.006), (0.014, 0.006), (0.014, -0.020)): row = [] for j, p in enumerate(Pp): t = (Pp[min(j + 1, len(Pp) - 1)] - Pp[max(j - 1, 0)]).normalized() nrm = normal_at(p) bn = nrm.cross(t).normalized() row.append(p + bn * w + nrm * h) rows.append(row) grid_rings(frames, [list(r) for r in zip(*rows)], GRAPH, closed_u=False, expect=(len(Pp) // 2, 1, tuple(normal_at(Pp[len(Pp) // 2])))) frames = MB("SM_Spearfish_Frames", [GRAPH]) for sg in (1, -1): for poly in GLASS_POLYS: frame_sweep(poly, sg) MULLION_SPANS = [mullion_span(a, b) for a, b in MULLIONS] for sg in (1, -1): for a, b in MULLION_SPANS: mullion(a, b, sg) # ------------------------------------------------------------------ cabin interior (seen through the canopy, the side windows, the hatch) ZF = -0.47 fl_rows = [] for i in I_CAB: y = YS[i] if y < Yi(1040.0): continue w = hull_x_at(ZF, y, LINER) - 0.004 fl_rows.append([Vector((x, y, ZF)) for x in (-w, -w * 0.5, 0.0, w * 0.5, w)]) grid_rings(cab, fl_rows, FLOOR, closed_u=False, expect=(1, 1, (0, 0, 1))) # ---- two seats side by side (the user, 2026-10-20: "two seater next to each other when two player can seat each should can # see the sea from the window front"). They stand beside the canopy's aft part, low and reclined like a racing cockpit, as # far out as the lining allows (the drawn rectangles are at +-0.70; the hull is 1.6 m wide inside at the pan's height here). # Seen from ahead, each eye lies outside the nose's opaque spine, so a look straight ahead leaves through that seat's own # glass (build/view_check.py casts the rays). Hatch above, between the seats; a centre console between them. # Where: a search over seat place, height and recline (rays from the eye against the built hull, glass and lining): further # forward the hull is too narrow to set the seats wide enough, and from an eye nearer the centre line a look straight ahead # meets the nose's opaque top strip (between the two drawn windows). With bucket seats (a narrower base under the cushion) # the pans fit at y -0.9 with the eyes 0.50 m out: straight ahead and two thirds of a +-30 deg cone leave through the glass. SEAT_Y = -0.90 # the pan's centre; the eyes 0.36 m behind the canopy's aft end, the hatch just ahead SEAT_W, SEAT_D = 0.42, 0.46 SEAT_PAN_Z = -0.24 # the cushion's top 0.27 m over the deck; the backs' tops near z 0.25, as drawn SEAT_RECLINE = math.radians(40) def _seat_room(): """how far out a seat's centre may go: the cushion (full width), its narrower base and the back's foot stay 2.5 cm inside the lining""" lim = 9.0 for dy in (-SEAT_D / 2, SEAT_D / 2): lim = min(lim, hull_x_at(SEAT_PAN_Z - 0.005, SEAT_Y + dy, LINER) - SEAT_W / 2, hull_x_at(SEAT_PAN_Z - 0.06, SEAT_Y + dy, LINER) - (SEAT_W - 0.12) / 2) lim = min(lim, hull_x_at(SEAT_PAN_Z + 0.04, SEAT_Y + SEAT_D / 2, LINER) - SEAT_W / 2) return lim - 0.025 SEAT_X = round(min(0.70, _seat_room()), 3) REPORT["seat_x"] = SEAT_X UPV = Vector((0, math.sin(SEAT_RECLINE), math.cos(SEAT_RECLINE))) # the back's direction (reclined aft) SEAT_EYES = {} FLOOR_HALF = hull_x_at(ZF, SEAT_Y, LINER) for s in (1, -1): cx = s * SEAT_X px_ = s * min(SEAT_X, FLOOR_HALF - 0.09) # the pedestal stands on the deck, inboard if need be hp = SEAT_PAN_Z - 0.06 - ZF qcyl(cab, 0.06, hp, (px_, SEAT_Y + 0.04, ZF + hp / 2), STEEL) qbox(cab, (SEAT_W, SEAT_D, 0.05), (cx, SEAT_Y, SEAT_PAN_Z + 0.02), SEAT, bevel=0.02) # cushion qbox(cab, (SEAT_W - 0.12, SEAT_D - 0.06, 0.06), (cx, SEAT_Y + 0.02, SEAT_PAN_Z - 0.03), GRAPH, bevel=0.01) # bucket base base = Vector((cx, SEAT_Y + SEAT_D / 2 - 0.04, SEAT_PAN_Z + 0.04)) qbox(cab, (SEAT_W, 0.09, 0.62), base + UPV * 0.31, SEAT, rot=(-SEAT_RECLINE, 0, 0), bevel=0.03) qbox(cab, (0.24, 0.08, 0.16), base + UPV * 0.70, SEAT, rot=(-SEAT_RECLINE, 0, 0), bevel=0.03) for side in (1, -1): # armrests on the side bolsters ax = cx + side * (SEAT_W / 2 - 0.035) qbox(cab, (0.07, 0.34, 0.06), (ax, SEAT_Y + 0.02, SEAT_PAN_Z + 0.17), SEAT, bevel=0.015) qcyl(cab, 0.018, 0.12, (cx + s * (SEAT_W / 2 - 0.035), SEAT_Y - SEAT_D / 2 + 0.07, SEAT_PAN_Z + 0.26), GRAPH, segs=8, r2=0.014) # the side stick, on the outboard armrest fy = SEAT_Y - SEAT_D / 2 - 0.75 # footrest: the legs run in toward the narrowing nose qbox(cab, (0.20, 0.05, 0.16), (s * 0.30, fy, ZF + 0.10), GRAPH, rot=(math.radians(-35), 0, 0), bevel=0.01) SEAT_EYES[s] = base + UPV * 0.62 + Vector((0, -0.12, -0.02)) # centre console between the seats, and a low instrument pod ahead of them (two screens, one for each seat) qbox(cab, (0.22, 0.62, 0.40), (0, SEAT_Y - 0.04, ZF + 0.20), GRAPH, bevel=0.02) qbox(cab, (0.18, 0.30, 0.02), (0, SEAT_Y + 0.02, ZF + 0.41), STEEL, bevel=0.005) CYc = Yi(935.0) # ahead of the hatch's opening (y -1.05 .. -1.58), clear of the drop in qbox(cab, (0.44, 0.26, 0.22), (0, CYc, ZF + 0.30), GRAPH, rot=(math.radians(30), 0, 0), bevel=0.02) for x in (-0.11, 0.11): qbox(cab, (0.17, 0.012, 0.12), (x, CYc + 0.07, ZF + 0.41), SCREEN, rot=(math.radians(30), 0, 0)) qbox(cab, (0.26, 0.24, 0.24), (0, CYc - 0.10, ZF + 0.12), GRAPH, bevel=0.02) # the aft part of the cabin, by the side windows, is the equipment bay: two racks against the rear bulkhead for s in (1, -1): y0, y1 = Yi(722.0), Yi(677.0) w = hull_x_at(ZF + 0.30, (y0 + y1) / 2, LINER) qbox(cab, (0.22, abs(y1 - y0), 0.55), (s * (w - 0.16), (y0 + y1) / 2, ZF + 0.30), GRAPH, bevel=0.02) for s in (1, -1): pts = [] for i in I_CAB: y = YS[i] if Yi(1000.0) <= y <= Yi(690.0): x = 0.25 * s pts.append(Vector((x, y, hull_z_at(x, y, True, LINER) - 0.015))) cab.sweep(pts, 0.012, CABGLOW, sides=6) qbox(cab, (0.62, 0.05, 1.00), (0, CAB_Y1 - 0.03, ZF + 0.52), GRAPH, bevel=0.02) qbox(cab, (0.54, 0.02, 0.92), (0, CAB_Y1 - 0.06, ZF + 0.52), STEEL, bevel=0.01) SIDE_OPENINGS = [P_CANOPY, P_SIDEWIN] OPENINGS = SIDE_OPENINGS REPORT["stage"] = "hull+glass+cabin"