# Grouper: the side doors (exec'd by build_grouper.py after gr_fittings.py). SIDE: a rounded rectangle in the waist band, # x 738.5-796.5, y 348.5-418.5 (0.99 x 1.20 m), corner r 9 px, a porthole (glass r 12.2 px, frame to 15.4) and two latches on # its forward side; both flanks (the port door the mirror). # # Each leaf (SM_Grouper_Door_L / _R, moving) is a polar grid round the porthole out to the opening's outline drawn in 8 mm # (the gap), its outer face on the band's surface (it follows the hull's curvature both ways), its edge a HORIZONTAL extrusion # 50 mm in, as the doorway's sleeve is (gr_hull.py, DOOR_TRIM): hinged on a vertical axis at its aft edge just outside the skin, # it swings out 0..95 deg and every point of it moves out or forward, never into the jambs. The porthole: a raised frame ring # round a bore through the leaf, the glass 25 mm in. Two latches on the forward side. Hinges: two knuckle pairs, the leaf's # knuckles on short arms, the hull's on arms back onto the band. DOOR_GAP = 0.008 LEAF_T = 0.05 PH_C = (Yi(DOOR["porthole_c"][0]), Zi(DOOR["porthole_c"][1])) PH_R_GLASS, PH_R_FRAME = Lp(DOOR["porthole_r"][0]), Lp(DOOR["porthole_r"][1]) N_ANG = 32 gpx = DOOR_GAP / S LEAF_PX = door_outline_px(gpx, 12) # (round 3) the doorway's G2 outline drawn in by the gap P_LEAF = side_poly(LEAF_PX) def ray_poly(c, d, poly): """distance from c along d to the closed polygon""" best = None n = len(poly) for i in range(n): (y1, z1), (y2, z2) = poly[i], poly[(i + 1) % n] ex, ez = y2 - y1, z2 - z1 den = d[0] * ez - d[1] * ex if abs(den) < 1e-12: continue t = ((y1 - c[0]) * ez - (z1 - c[1]) * ex) / den u = ((y1 - c[0]) * d[1] - (z1 - c[1]) * d[0]) / den if t > 0 and -1e-9 <= u <= 1 + 1e-9: best = t if best is None else min(best, t) return best def leaf_pt(sg, y, z, h=0.0): p, n = on_side(sg, y, z, LV_BAND) return p + n * h, n DOORS = {} HINGES = {} door_static = MB("_door_static", [FRAME, STEEL]) for sg in (1, -1): tag = "L" if sg > 0 else "R" mb = MB("SM_Grouper_Door_" + tag, [BAND, FRAME, ACRYLD, STEEL]) bmd = bmesh.new() angs = [2 * math.pi * k / N_ANG for k in range(N_ANG)] R_out = [ray_poly(PH_C, (math.cos(a), math.sin(a)), P_LEAF) for a in angs] r_in = PH_R_GLASS - 0.002 fr = [0.0, 0.12, 0.30, 0.55, 0.80, 1.0] outer = [] for f_ in fr: row = [] for k, a in enumerate(angs): rr = r_in + (R_out[k] - r_in) * f_ p, n = leaf_pt(sg, PH_C[0] + rr * math.cos(a), PH_C[1] + rr * math.sin(a)) row.append(p) outer.append(row) sx = Vector((sg, 0, 0)) VO = [[bmd.verts.new(p) for p in row] for row in outer] VI = [[bmd.verts.new(p - sx * LEAF_T) for p in row] for row in outer] for i in range(len(fr) - 1): for k in range(N_ANG): k2 = (k + 1) % N_ANG bmd.faces.new((VO[i][k], VO[i + 1][k], VO[i + 1][k2], VO[i][k2])).material_index = 0 bmd.faces.new((VI[i][k2], VI[i + 1][k2], VI[i + 1][k], VI[i][k])).material_index = 1 for k in range(N_ANG): # the leaf's edge and the porthole's bore k2 = (k + 1) % N_ANG bmd.faces.new((VO[-1][k], VI[-1][k], VI[-1][k2], VO[-1][k2])).material_index = 1 bmd.faces.new((VO[0][k2], VI[0][k2], VI[0][k], VO[0][k])).material_index = 1 bmd.normal_update() mid = VO[3][N_ANG // 4] p_, n_ = leaf_pt(sg, PH_C[0] + 0.4, PH_C[1]) if sum((f.normal.dot(n_) for f in bmd.faces if f.material_index == 0)) < 0: bmesh.ops.reverse_faces(bmd, faces=bmd.faces[:]) mb.merge(bmd, None) # the porthole's frame ring and glass ring_prof = [(PH_R_FRAME + 0.004, -0.004), (PH_R_FRAME, 0.010), (PH_R_FRAME - 0.012, 0.017), (r_in + 0.012, 0.017), (r_in, 0.009), (r_in - 0.004, -0.004), (r_in - 0.004, -0.020)] loops = [] for rr, h in ring_prof: loops.append([leaf_pt(sg, PH_C[0] + rr * math.cos(a), PH_C[1] + rr * math.sin(a), h)[0] for a in angs]) grid_rings(mb, loops, FRAME, expect=(1, N_ANG // 4, tuple(n_))) bmg = bmesh.new() gl = [bmg.verts.new(leaf_pt(sg, PH_C[0] + (r_in - 0.004) * math.cos(a), PH_C[1] + (r_in - 0.004) * math.sin(a), -0.020)[0]) for a in angs] coons_cap(bmg, gl, 0) bmg.normal_update() if sum(f.normal.dot(n_) for f in bmg.faces) < 0: bmesh.ops.reverse_faces(bmg, faces=bmg.faces[:]) mb.merge(bmg, ACRYLD) # smoked: the porthole reads as a dark, deep recess (m3) # (critique round 2, m2) the pane's inner face, 10 mm in, facing the cabin: the acrylic is a slab, seen from both sides bmg = bmesh.new() gl = [bmg.verts.new(leaf_pt(sg, PH_C[0] + (r_in - 0.004) * math.cos(a), PH_C[1] + (r_in - 0.004) * math.sin(a), -0.030)[0]) for a in angs] coons_cap(bmg, gl, 0) bmg.normal_update() if sum(f.normal.dot(n_) for f in bmg.faces) > 0: bmesh.ops.reverse_faces(bmg, faces=bmg.faces[:]) mb.merge(bmg, ACRYLD) # two latches on the forward side (SIDE (788, 367), (788, 397), 6 x 4 px): OVAL bosses on the leaf, as drawn (critique m9: # they were square blocks) for xl, yl in DOOR["latches"]: c, n = leaf_pt(sg, Yi(xl), Zi(yl), 0.0) qsphere(mb, c + n * 0.002, (0.014, Lp(DOOR["latch_size"][0]) * 0.5, Lp(DOOR["latch_size"][1]) * 0.5), STEEL, cuts=2) # the hinge axis: vertical, at the leaf's aft edge, 4 mm outside its outer face at its widest (the leaf swings out past the # aft jamb: an axis inside the skin would drive its aft edge into the jamb). The hinges are FLUSH (critique m9: the sheet # draws none; they stood out 3-4 cm on arms): slim knuckles r 11 mm on the axis, the leaf's on a strap lying on its outer # face, the hull's on straps lying on the band aft of the doorway y_aft = max(p[0] for p in P_LEAF) xs_aft = [abs(v.co.x) for v in mb.bm.verts if abs(v.co.y - y_aft) < 0.03] x_h = max(xs_aft) + 0.004 zc = 0.5 * (Zi(DOOR["y"][0]) + Zi(DOOR["y"][1])) AXIS = Vector((sg * x_h, y_aft, zc)) for zh in (zc + 0.38, zc - 0.40): pl, nl = leaf_pt(sg, y_aft - 0.05, zh, 0.003) qcyl(mb, 0.011, 0.07, Vector((AXIS.x, AXIS.y, zh)), STEEL, segs=12) rrect_bar(mb, pl, Vector((AXIS.x, AXIS.y - 0.004, zh)), 0.030, 0.004, 0.003, FRAME, up=Vector((sg, 0, 0))) for dz in (-0.058, 0.058): kz = zh + dz qcyl(door_static, 0.011, 0.040, Vector((AXIS.x, AXIS.y, kz)), STEEL, segs=12) pb, nb = on_side(sg, y_aft + 0.060, kz, LV_BAND) rrect_bar(door_static, Vector((AXIS.x, AXIS.y + 0.006, kz)), pb + nb * 0.002, 0.016, 0.004, 0.003, FRAME, up=Vector((sg, 0, 0))) DOORS[sg] = (mb, AXIS) # the door stop (finishing step, 2026-10-22): a ring inside the doorway's sleeve, 8 mm behind the leaf's inner face, from # 3 mm in the sleeve's wall to 35 mm in over the leaf's edge, so no line of sight runs through the 8 mm gap round the leaf # into the cabin (the side render saw through both doors' gaps). The leaf only moves outboard as it opens: the stop never # meets it (gr_motion_check.py). rs = [] for i in range(len(P_DOOR)): a, b = Vector(P_DOOR[i]), Vector(P_DOOR[(i + 1) % len(P_DOOR)]) n_seg = max(1, int(math.ceil((b - a).length / 0.06))) rs += [a.lerp(b, t / n_seg) for t in range(n_seg)] cy = sum((v for v in rs), Vector((0, 0))) / len(rs) loops = [[], [], [], []] sx = Vector((sg, 0, 0)) for i, q in enumerate(rs): t = rs[(i + 1) % len(rs)] - rs[i - 1] nin = Vector((-t.y, t.x)).normalized() if nin.dot(cy - q) < 0: nin = -nin p, n = on_side(sg, q.x, q.y, LV_BAND) for j, (dep, ins) in enumerate(((LEAF_T + 0.008, -0.003), (LEAF_T + 0.008, 0.035), (LEAF_T + 0.020, 0.035), (LEAF_T + 0.020, -0.003))): loops[j].append(p - sx * dep + Vector((0.0, nin.x, nin.y)) * ins) grid_rings(door_static, loops, FRAME, expect=(0, 0, (sg, 0.0, 0.0))) REPORT["doors"] = {("L" if sg > 0 else "R"): {"hinge_axis": [round(c, 4) for c in a], "open_deg": list(DOOR["open_deg"])} for sg, (m, a) in DOORS.items()}