"""Aethel-9 hull meshes from the planned grid (ae_hull.plan): the skin with its openings and end caps, the cabin lining (the skin offset inward, the openings joined to it by the frames' sleeves), the window frames (one constant section swept round each hole), the glass panes (slabs) and the hatch coaming. World metres.""" import numpy as np import ae_lib as L import ae_hull as H from ae_geo import Mesh, coons_cap, arclen LINING = 0.085 # cabin lining depth under the skin (m) CABIN_END = 492.0 # SIDE x of the cabin's rear bulkhead (aft of the hatch) # frame section (s across the hole edge, + away from the opening; n along the skin normal, + out) FRAME = [(0.006, -0.004), (0.006, 0.010), (-0.002, 0.020), (-0.030, 0.020), (-0.040, 0.011), (-0.040, -LINING - 0.006), (0.012, -LINING - 0.006), (0.012, -LINING + 0.004)] GLASS_N, GLASS_T, GLASS_S = -0.030, 0.016, -0.040 def surf_normal(v, phi, dv=0.05, dp=0.0005): a = L.surf_world(v + dv, phi) - L.surf_world(v - dv, phi) b = L.surf_world(v, phi + dp) - L.surf_world(v, phi - dp) n = np.cross(b, a) return n / np.linalg.norm(n, axis=-1, keepdims=True) def build(g=None): g = H.plan() if g is None else g V, P = g['V'], g['P'] NI, NJ = V.shape X = L.surf_world(V, P) # (NI, NJ, 3) Nn = surf_normal(V, P) # outward check: normals point away from the hull's centre line c = np.stack([np.zeros_like(V), (V - L.V0) * L.S, (L.Y0 - (L.interp(L.TOPLINE, V) + L.interp(L.BOTLINE, V)) / 2) * L.S], -1) if np.mean(np.sum((X - c) * Nn, -1) > 0) < 0.5: Nn = -Nn hole = H.hole_mask(g) used = np.zeros((NI, NJ), bool) for i in range(NI - 1): for j in range(NJ): if not hole[i, j]: used[i, j] = used[i + 1, j] = used[i, (j + 1) % NJ] = used[i + 1, (j + 1) % NJ] = True used[0] = used[-1] = True out = {} # ---- skin --------------------------------------------------------------------------------------------------- skin = Mesh() idx = -np.ones((NI, NJ), int) pts = X[used] base = skin.add_verts(pts) idx[used] = base + np.arange(len(pts)) U = np.mod(P, 1.0) Vv = (V - L.V_TIP) / (L.V_END - L.V_TIP) for i in range(NI - 1): for j in range(NJ): if hole[i, j]: continue jn = (j + 1) % NJ # winding: (aft) then (round), so the normal points out of the hull q = [idx[i, j], idx[i + 1, j], idx[i + 1, jn], idx[i, jn]] uu = [P[i, j], P[i + 1, j], P[i + 1, jn], P[i, jn]] uu = np.array(uu); uu = uu - np.floor(uu[0]) uu = np.where(uu - uu[0] > 0.5, uu - 1, np.where(uu - uu[0] < -0.5, uu + 1, uu)) uv = [(uu[0], Vv[i, j]), (uu[1], Vv[i + 1, j]), (uu[2], Vv[i + 1, jn]), (uu[3], Vv[i, jn])] skin.face(q, uv, 'Hull') # end caps (planar rings: the nose cap under the lidar belt, the stern end under the stern cap) for row, flip in ((0, False), (NI - 1, True)): ring = X[row] # roll so the Coons sides are the bottom, port, top, starboard quarters (start at phi -1/8) k = NJ // 8 ring = np.roll(ring, k, 0) m = Mesh(); coons_cap(m, ring, 'HullDark', flip=flip, uv_scale=1.0) # the cap re-adds its ring vertices; weld them to the skin's mv, mf, muv, mm = m.arrays() ringidx = np.roll(idx[row], k) remap = {} for vi in range(len(mv)): d = np.linalg.norm(ring - mv[vi], axis=1) if d.min() < 1e-7: remap[vi] = int(ringidx[np.argmin(d)]) newb = len(skin.v) keep = [vi for vi in range(len(mv)) if vi not in remap] for n_, vi in enumerate(keep): remap[vi] = newb + n_ skin.add_verts(mv[keep]) for f, uv in zip(mf, muv): skin.face([remap[a] for a in f], [(u * 0.2 + 0.0, w * 0.2) for u, w in uv], 'HullDark') out['skin'] = skin # ---- lining (cabin): rows up to CABIN_END, offset inward ------------------------------------------------------ rows_cab = [i for i in range(NI) if V[i].max() <= CABIN_END + 1e-6] ie = rows_cab[-1] i0 = 3 # the first rows at the nose are too small to offset; cap there Xin = X - LINING * Nn lin = Mesh() lidx = -np.ones((NI, NJ), int) usedL = used.copy(); usedL[:i0] = False; usedL[ie + 1:] = False usedL[i0] = True; usedL[ie] = True pts = Xin[usedL] base = lin.add_verts(pts) lidx[usedL] = base + np.arange(len(pts)) for i in range(i0, ie): for j in range(NJ): if hole[i, j]: continue jn = (j + 1) % NJ q = [lidx[i, j], lidx[i, jn], lidx[i + 1, jn], lidx[i + 1, j]] # facing into the cabin uv = [(V[i, j] * L.S, P[i, j] * 8), (V[i, jn] * L.S, P[i, jn] * 8), (V[i + 1, jn] * L.S, P[i + 1, jn] * 8), (V[i + 1, j] * L.S, P[i + 1, j] * 8)] lin.face(q, uv, 'Lining') k = NJ // 8 for row, flip in ((i0, True), (ie, False)): ring = np.roll(Xin[row], k, 0) m = Mesh(); coons_cap(m, ring, 'Lining', flip=flip, uv_scale=1.0) mv, mf, muv, mm = m.arrays() ringidx = np.roll(lidx[row], k) remap = {} for vi in range(len(mv)): d = np.linalg.norm(ring - mv[vi], axis=1) if d.min() < 1e-7: remap[vi] = int(ringidx[np.argmin(d)]) newb = len(lin.v); keep = [vi for vi in range(len(mv)) if vi not in remap] for n_, vi in enumerate(keep): remap[vi] = newb + n_ lin.add_verts(mv[keep]) for f, uv in zip(mf, muv): lin.face([remap[a] for a in f], uv, 'Lining') out['lining'] = lin out['cabin_rows'] = (i0, ie) # ---- openings: boundary loops, frames, glass ----------------------------------------------------------------- frames, glass = Mesh(), Mesh() loops = {} for name, (r0, r1, c0, c1) in g['rects'].items(): loop = [] for j in range(c0, c1): loop.append((r0, j % NJ)) for i in range(r0, r1): loop.append((i, c1 % NJ)) for j in range(c1, c0, -1): loop.append((r1, j % NJ)) for i in range(r1, r0, -1): loop.append((i, c0 % NJ)) Pl = np.array([X[i, j] for i, j in loop]); Nl = np.array([Nn[i, j] for i, j in loop]) loops[name] = (loop, Pl, Nl) # side direction: tangent x normal, pointing away from the opening's centre T = np.roll(Pl, -1, 0) - np.roll(Pl, 1, 0); T /= np.linalg.norm(T, axis=1, keepdims=True) Sd = np.cross(T, Nl); Sd /= np.linalg.norm(Sd, axis=1, keepdims=True) cen = Pl.mean(0) rev = np.mean(np.sum((Pl - cen) * Sd, 1)) < 0 if rev: Sd = -Sd prof = FRAME if name != 'hatch' else [(0.006, -0.004), (0.006, 0.012), (-0.004, 0.030), (-0.030, 0.030), (-0.034, 0.022), (-0.034, -LINING - 0.006), (0.012, -LINING - 0.006), (0.012, -LINING + 0.004)] pr = np.array(prof) G = Pl[:, None, :] + pr[None, :, :1] * Sd[:, None, :] + pr[None, :, 1:2] * Nl[:, None, :] nb = len(Pl); npf = len(pr) b = frames.add_verts(G.reshape(-1, 3)) along = arclen(np.vstack([Pl, Pl[:1]])) across = arclen(pr) mat = 'Frame' for a in range(nb): an = (a + 1) % nb for p in range(npf - 1): q = [b + a * npf + p, b + an * npf + p, b + an * npf + p + 1, b + a * npf + p + 1] if rev: q = q[::-1] # outward by construction for a counter-clockwise loop; fixed below by a centroid test uv = [(along[a] * 2, across[p] * 2), (along[a + 1] * 2, across[p] * 2), (along[a + 1] * 2, across[p + 1] * 2), (along[a] * 2, across[p + 1] * 2)] frames.face(q, uv[::-1] if rev else uv, mat) if name == 'hatch': continue # glass: Coons from the ring at the sleeve wall, at GLASS_N under the skin; a slab GLASS_T thick ringG = Pl + GLASS_S * Sd + GLASS_N * Nl nloop = len(ringG) # the loop has 2*(r1-r0) + 2*(c1-c0) points; Coons needs equal sides -> resample to 4k kk = 8 sides = [(0, c1 - c0), (c1 - c0, c1 - c0 + r1 - r0), (c1 - c0 + r1 - r0, 2 * (c1 - c0) + r1 - r0), (2 * (c1 - c0) + r1 - r0, nloop)] ring4 = [] for s0, s1 in sides: seg = np.vstack([ringG[s0:s1], ringG[s1 % nloop][None]]) sl = arclen(seg); t = np.linspace(0, sl[-1], kk + 1)[:-1] ring4 += [np.array([np.interp(tt, sl, seg[:, d]) for d in range(3)]) for tt in t] ring4 = np.array(ring4) nrm = Nl.mean(0); nrm /= np.linalg.norm(nrm) m1 = Mesh(); coons_cap(m1, ring4, 'Glass', flip=False, uv_scale=1.0) m2 = Mesh(); coons_cap(m2, ring4 - GLASS_T * nrm, 'Glass', flip=True, uv_scale=1.0) # orient: the outer pane faces +nrm mv, mf, muv, _ = m1.arrays() f0 = mf[len(mf) // 2]; pq = mv[list(f0)] fn = np.cross(pq[1] - pq[0], pq[2] - pq[0]) if np.dot(fn, nrm) < 0: m1.f = [tuple(reversed(f)) for f in m1.f]; m1.uv = [list(reversed(u)) for u in m1.uv] m2.f = [tuple(reversed(f)) for f in m2.f]; m2.uv = [list(reversed(u)) for u in m2.uv] gb = len(glass.v) glass.merge(m1); glass.merge(m2) # rim between the two panes (their rings are the first 4k vertices added by coons_cap after the interior) r1i = [i for i, p in enumerate(m1.v) if np.min(np.linalg.norm(ring4 - np.array(p), axis=1)) < 1e-7] r2i = [i for i, p in enumerate(m2.v) if np.min(np.linalg.norm(ring4 - GLASS_T * nrm - np.array(p), axis=1)) < 1e-7] order1 = [r1i[int(np.argmin([np.linalg.norm(np.array(m1.v[i]) - q) for i in r1i]))] for q in ring4] order2 = [r2i[int(np.argmin([np.linalg.norm(np.array(m2.v[i]) - (q - GLASS_T * nrm)) for i in r2i]))] for q in ring4] n4 = len(ring4) for a in range(n4): an = (a + 1) % n4 glass.face([gb + order1[an], gb + len(m1.v) + order2[an], gb + len(m1.v) + order2[a], gb + order1[a]], [(1, 0), (1, 1), (0, 1), (0, 0)], 'Glass') out['frames'] = frames out['glass'] = glass out['loops'] = loops out['grid'] = dict(X=X, N=Nn, V=V, P=P, idx=idx) return out def face_normals(mesh): V, F, _, _ = mesh.arrays() C, N = [], [] for f in F: p = V[list(f)] n = np.zeros(3) for a in range(len(p)): b = (a + 1) % len(p) n += np.cross(p[a], p[b]) C.append(p.mean(0)); N.append(n / (np.linalg.norm(n) + 1e-12)) return np.array(C), np.array(N) def check_orientation(o): """Fraction of skin faces facing out of the hull and of lining faces facing into the cabin (ends: along -Y / +Y).""" out = {} for key, want in (('skin', 1), ('lining', -1)): C, N = face_normals(o[key]) v = C[:, 1] / L.S + L.V0 zc = (L.Y0 - (L.interp(L.TOPLINE, v) + L.interp(L.BOTLINE, v)) / 2) * L.S ref = np.stack([C[:, 0], np.zeros(len(C)), C[:, 2] - zc], 1) tip = v < L.V_TIP + 0.2 if key == 'skin' else np.zeros(len(C), bool) end = v > L.V_END - 0.2 if key == 'skin' else np.zeros(len(C), bool) ref[tip] = [0, -1, 0]; ref[end] = [0, 1, 0] good = np.sum(N * ref, 1) * want > 0 out[key] = round(float(good.mean()), 4) return out if __name__ == '__main__': o = build() print('orientation', check_orientation(o)) for k in ('skin', 'lining', 'frames', 'glass'): v, f, uv, m = o[k].arrays() print(k, len(v), 'verts', len(f), 'faces', {n: sum(1 for x in f if len(x) == n) for n in (3, 4)})