# Grouper: helpers for the attached parts (exec'd after gr_hull.py, shares its namespace). Quads only: lofts through rings # with Coons-patch end caps, tubes with Coons caps, stadium and rounded-rectangle sections, bars that follow the hull's surface, # thin plates over outlines on the surface. Parts that sink into the hull are pruned (prune_inside) by the finish pass. import math from mathutils import Vector, Matrix def rrect2d(w, h, r, n=3): """a rounded rectangle (half width w, half height h, corner radius r) as 4 * (n + 1) points, counter-clockwise from the right side's middle: a multiple of 4 (Coons caps)""" r = min(r, 0.999 * min(w, h)) pts = [] for (cx, cy, a0) in ((w - r, h - r, 0), (-w + r, h - r, 90), (-w + r, -h + r, 180), (w - r, -h + r, 270)): for k in range(n + 1): a = math.radians(a0 + 90.0 * k / n) pts.append((cx + r * math.cos(a), cy + r * math.sin(a))) k = len(pts) // 8 # start at the right side's middle return pts[-k:] + pts[:-k] def stadium2d(L0, L1, h, n=4): """a stadium along u from L0 to L1 (round ends of radius h): 4 * (n + 1) points""" pts = [] for k in range(2 * n + 2): a = -math.pi / 2 + math.pi * k / (2 * n + 1) pts.append((L1 - h + h * math.cos(a), h * math.sin(a))) for k in range(2 * n + 2): a = math.pi / 2 + math.pi * k / (2 * n + 1) pts.append((L0 + h + h * math.cos(a), h * math.sin(a))) return pts def ladder_cap(bm2, ring, mi): """close a ring that is symmetric about its middle (points k and n-1-k are mirror pairs, e.g. a fin's or a ridge's section from one root over the top to the other root) with a ladder of quads between the pairs: no folds on non-convex sections""" n = len(ring) h = n // 2 for k in range(h - 1): f = bm2.faces.new((ring[k], ring[k + 1], ring[n - 2 - k], ring[n - 1 - k])) f.material_index = mi def loft_rings(mb, rings, mat, cap0=True, cap1=True, mats=None, centre=None, ladder=False): """quads through rings (lists of Vectors, equal counts, closed); Coons caps (counts % 4 == 0), or ladder caps for sections symmetric about their middle (ladder=True); faces turned outward from the rings' centres""" bm2 = bmesh.new() V = [[bm2.verts.new(p) for p in r] for r in rings] n = len(rings[0]) ml = mats if mats is not None else [mat] * (len(rings) - 1) for i in range(len(V) - 1): mi = mb.mi(ml[i]) for k in range(n): f = bm2.faces.new((V[i][k], V[i + 1][k], V[i + 1][(k + 1) % n], V[i][(k + 1) % n])) f.material_index = mi if cap0: (ladder_cap if ladder else coons_cap)(bm2, V[0][::-1] if not ladder else V[0], mb.mi(ml[0])) if cap1: (ladder_cap if ladder else coons_cap)(bm2, V[-1], mb.mi(ml[-1])) bm2.normal_update() bmesh.ops.recalc_face_normals(bm2, faces=bm2.faces[:]) bm2.normal_update() cs = [sum(r, Vector()) / len(r) for r in rings] vol = 0.0 for f in bm2.faces: # orientation: the side faces point away from the rings' centres c = f.calc_center_median() j = min(range(len(cs)), key=lambda q: (cs[q] - c).length) vol += f.normal.dot(c - cs[j]) * f.calc_area() if vol < 0: bmesh.ops.reverse_faces(bm2, faces=bm2.faces[:]) mb.merge(bm2, None) def frame_of(t, up=Vector((0, 0, 1))): t = t.normalized() if abs(t.dot(up)) > 0.95: up = Vector((1, 0, 0)) if abs(t.x) < 0.9 else Vector((0, 1, 0)) a = up.cross(t).normalized() b = t.cross(a).normalized() return a, b def tube(mb, pts, r, mat, sides=8, caps=True, up=Vector((0, 0, 1)), section=None): """a tube along a polyline (parallel-transport frames), radius r (or per point), quads, Coons caps; section: 2D points instead of a circle (counts % 4 == 0)""" P_ = [Vector(p) for p in pts] n = len(P_) rs = r if isinstance(r, (list, tuple)) else [r] * n t0 = (P_[1] - P_[0]).normalized() a, b = frame_of(t0, up) prev_t = t0 rings = [] sec = section if section is not None else [(math.cos(2 * math.pi * k / sides), math.sin(2 * math.pi * k / sides)) for k in range(sides)] for i in range(n): if i == 0: t = t0 elif i == n - 1: t = (P_[i] - P_[i - 1]).normalized() else: t = ((P_[i] - P_[i - 1]).normalized() + (P_[i + 1] - P_[i]).normalized()).normalized() ax = prev_t.cross(t) if ax.length > 1e-7: ang = math.acos(max(-1, min(1, prev_t.dot(t)))) R_ = Matrix.Rotation(ang, 3, ax.normalized()) a, b = R_ @ a, R_ @ b a = (a - a.dot(t) * t).normalized() b = t.cross(a).normalized() prev_t = t rings.append([P_[i] + rs[i] * (u * a + v * b) for u, v in sec]) loft_rings(mb, rings, mat, cap0=caps, cap1=caps) def rrect_bar(mb, p0, p1, w, h, r, mat, up=Vector((0, 0, 1)), n=2): """a straight bar of rounded-rectangle section (half width w along the side direction, half height h along up)""" t = (Vector(p1) - Vector(p0)).normalized() a, b = frame_of(t, up) sec = rrect2d(w, h, r, n) rings = [[Vector(p) + u * a + v * b for u, v in sec] for p in (p0, p1)] loft_rings(mb, rings, mat) def revolve_y(mb, prof, mats, centre, segs=32, cap0=True, cap1=True, ang0=0.0): """revolve a profile [(r, y)] about the axis through centre along +Y (r > 0), Coons caps at open ends""" rows = [] for r_, y_ in prof: rows.append([Vector((centre.x + r_ * math.cos(ang0 + 2 * math.pi * k / segs), y_, centre.z + r_ * math.sin(ang0 + 2 * math.pi * k / segs))) for k in range(segs)]) ml = mats if isinstance(mats, (list, tuple)) else [mats] * (len(prof) - 1) loft_rings(mb, rows, None, cap0=cap0, cap1=cap1, mats=ml) def surface_bar(mb, sg, path_yz, w, h, mat, level=0.0, sink=0.012): """a bar lying on a flank: path (Y, Z) points on the side chart, a rounded-rectangle section w (across the path, on the surface) x h (out of the surface), standing on the surface at `level` (sunk `sink` into it)""" pts, nrms = [], [] for y, z in path_yz: p, n = on_side(sg, y, z) pts.append(p + n * (level + h * 0.5 - sink * 0.5)) nrms.append(n) rings = [] sec = rrect2d(w, (h + sink) * 0.5, min(w, (h + sink) * 0.5) * 0.45, 2) for i, p in enumerate(pts): t = (pts[min(i + 1, len(pts) - 1)] - pts[max(i - 1, 0)]).normalized() n = nrms[i] a = n.cross(t).normalized() rings.append([p + a * u + n * v for u, v in sec]) loft_rings(mb, rings, mat) def chart_plate(mb, outline, to3d, mat, thick=0.004, lift=0.0, cell=0.03): """a thin plate over a closed chart outline (u, v): its top a ladder of quads over the outline's span (along u), its rim a band down `thick`; to3d(u, v) -> (point, normal) on the reference surface""" us = [p[0] for p in outline] u0, u1 = min(us) + 1e-4, max(us) - 1e-4 nt = max(2, int(math.ceil((u1 - u0) / cell))) poly = list(outline) spans = [] for q in range(nt + 1): u = u0 + (u1 - u0) * (0.5 - 0.5 * math.cos(math.pi * q / nt)) sp = span_at(poly, u) if sp is None: continue spans.append((u, sp)) na = max(2, int(math.ceil(max(s[1] - s[0] for u, s in spans) / cell))) bm2 = bmesh.new() top, bot = [], [] for u, (lo, hi) in spans: rt, rb = [], [] for c in range(na + 1): v = lo + (hi - lo) * c / na p, n = to3d(u, v) rt.append(bm2.verts.new(p + n * (lift + thick))) rb.append(bm2.verts.new(p + n * (lift - 0.002))) top.append(rt); bot.append(rb) for i in range(len(top) - 1): for c in range(na): bm2.faces.new((top[i][c], top[i + 1][c], top[i + 1][c + 1], top[i][c + 1])) loop_t = [r[0] for r in top] + top[-1][1:-1] + [r[-1] for r in reversed(top)] + top[0][1:-1][::-1] loop_b = [r[0] for r in bot] + bot[-1][1:-1] + [r[-1] for r in reversed(bot)] + bot[0][1:-1][::-1] m = len(loop_t) for k in range(m): bm2.faces.new((loop_t[k], loop_b[k], loop_b[(k + 1) % m], loop_t[(k + 1) % m])) loose = [v for v in bm2.verts if not v.link_faces] bmesh.ops.delete(bm2, geom=loose, context='VERTS') bm2.normal_update() bmesh.ops.recalc_face_normals(bm2, faces=bm2.faces[:]) bm2.normal_update() p, n = to3d(0.5 * (u0 + u1), sum(span_at(poly, 0.5 * (u0 + u1))) * 0.5) tf = max(bm2.faces, key=lambda f: f.calc_center_median().dot(n)) if tf.normal.dot(n) < 0: bmesh.ops.reverse_faces(bm2, faces=bm2.faces[:]) for f in bm2.faces: f.material_index = mb.mi(mat) mb.merge(bm2, None) def slot_trim(mb, outline_px, sg, level, mat_rim, mat_in, rim=0.012, proud=0.006, inner=0.002): """a vent or panel on a flank: a rim of one constant section round the SIDE outline (image px), standing `proud` on the surface at `level`, and a dark inner plate `inner` proud (so the inside reads recessed)""" poly = [(Yi(x), Zi(y)) for x, y in outline_px] prof = [(rim, -0.003), (rim, proud * 0.6), (rim * 0.6, proud), (0.0, proud), (0.0, inner)] frame_sweep(mb, poly, lambda u, v: on_side(sg, u, v, level), prof, mat_rim, None, max_step=0.03) chart_plate(mb, poly, lambda u, v: on_side(sg, u, v, level), mat_in, thick=inner, lift=0.0, cell=0.025) def rrect_px(x0, y0, x1, y1, r, n=4): return rounded_rect(x0, y0, x1, y1, min(r, 0.49 * (x1 - x0), 0.49 * (y1 - y0)), n)