# Spearfish: the hull's shape and the 2D outline helpers. Pure functions and constants, no scene changes: exec'd by # build_spearfish.py, and by sf_collision.py on its own (after lib2.py, tender_lib.py and sf_measure.py). import math from mathutils import Vector # ================================================================== 1. the hull's shape: superellipse sections along Y def _f(tab, conv): return pchip(sorted((Yi(x), conv(v)) for x, v in tab)) TOPf, BOTf, Wf = _f(TOPY, Zi), _f(BOTY, Zi), _f(WPX, Lp) Y_NOSE, Y_TAIL = Yi(X_TIP), Yi(X_STERN) ZM_FRAC = 0.45 # widest line 45 % of the height down (REAR view: 49 %, FRONT view: 30 %) def ZMf(y): return TOPf(y) - ZM_FRAC * (TOPf(y) - BOTf(y)) def HTf(y): return max(1e-3, TOPf(y) - ZMf(y)) def HBf(y): return max(1e-3, ZMf(y) - BOTf(y)) def ximg(y): return X0 - y / S def NT(y): x = ximg(y) return 2.8 - 0.9 * smoothstep(990, 1125, x) - 0.2 * smoothstep(600, 530, x) # REAR view: a full top (n 3.5-4) def NB(y): return 2.3 - 0.7 * smoothstep(990, 1125, ximg(y)) # REAR: n 2.3-2.5; nose: a keel def sec(y, th, inset=0.0): c, s = math.cos(th), math.sin(th) n = NT(y) if s >= 0 else NB(y) W = max(0.004, Wf(y) - inset) H = max(0.004, (HTf(y) if s >= 0 else HBf(y)) - inset) x = W * math.copysign(abs(c) ** (2.0 / n), c) z = ZMf(y) + H * math.copysign(abs(s) ** (2.0 / n), s) return Vector((x, y, z)) def hull_z_at(x, y, top=True, inset=0.0): W = max(0.004, Wf(y) - inset); n = NT(y) if top else NB(y); H = max(0.004, (HTf(y) if top else HBf(y)) - inset) q = max(0.0, 1.0 - min(1.0, abs(x) / W) ** n) return ZMf(y) + (1 if top else -1) * H * q ** (1.0 / n) def hull_x_at(z, y, inset=0.0): W = max(0.004, Wf(y) - inset); zm = ZMf(y) top = z >= zm; n = NT(y) if top else NB(y); H = max(0.004, (HTf(y) if top else HBf(y)) - inset) q = max(0.0, 1.0 - min(1.0, abs(z - zm) / H) ** n) return W * q ** (1.0 / n) def _F(p): y = min(max(p.y, Y_NOSE + 1e-3), Y_TAIL) zm = ZMf(y); top = p.z >= zm n = NT(y) if top else NB(y); W = Wf(y); H = HTf(y) if top else HBf(y) return (abs(p.x) / W) ** n + (abs(p.z - zm) / H) ** n def normal_at(p, h=1e-4): g = Vector(((_F(p + Vector((h, 0, 0))) - _F(p - Vector((h, 0, 0)))), (_F(p + Vector((0, h, 0))) - _F(p - Vector((0, h, 0)))), (_F(p + Vector((0, 0, h))) - _F(p - Vector((0, 0, h)))))) return g.normalized() if g.length > 1e-12 else Vector((0, 0, 1)) def inside_depth(p): y = p.y if y < Y_NOSE or y > Y_TAIL + 0.005: return -1.0 W = Wf(y); zm = ZMf(y); top = p.z >= zm n = NT(y) if top else NB(y); H = HTf(y) if top else HBf(y) f = (abs(p.x) / W) ** n + (abs(p.z - zm) / H) ** n return (1.0 - f ** (1.0 / n)) * min(W, H) # projections: a point of a view's 2D outline onto the hull def on_top(lat, y, lift=0.0): p = Vector((lat, y, hull_z_at(lat, y, True))) return p + normal_at(p) * lift if lift else p def on_bot(lat, y, lift=0.0): p = Vector((lat, y, hull_z_at(lat, y, False))) return p + normal_at(p) * lift if lift else p def on_side(sg, y, z, lift=0.0): p = Vector((sg * hull_x_at(z, y), y, z)) return p + normal_at(p) * lift if lift else p # 2D outlines in model units def side_poly(img): return [(Yi(x), Zi(v)) for x, v in img] def pip(pt, poly): x, y = pt; inside = False; n = len(poly) for i in range(n): x1, y1 = poly[i]; x2, y2 = poly[(i + 1) % n] if (y1 > y) != (y2 > y) and x < (x2 - x1) * (y - y1) / (y2 - y1) + x1: inside = not inside return inside def nearest_on_poly(pt, poly): best, bd = None, 1e9 px, py = pt for i in range(len(poly)): ax, ay = poly[i]; bx, by = poly[(i + 1) % len(poly)] dx, dy = bx - ax, by - ay t = max(0.0, min(1.0, ((px - ax) * dx + (py - ay) * dy) / max(1e-12, dx * dx + dy * dy))) qx, qy = ax + dx * t, ay + dy * t d = (qx - px) ** 2 + (qy - py) ** 2 if d < bd: bd, best = d, (qx, qy) return best def resample(poly, step, closed=True): pts = list(poly) + ([poly[0]] if closed else []) out = [] for i in range(len(pts) - 1): (ax, ay), (bx, by) = pts[i], pts[i + 1] n = max(1, int(math.ceil(math.hypot(bx - ax, by - ay) / step))) for j in range(n): out.append((ax + (bx - ax) * j / n, ay + (by - ay) * j / n)) if not closed: out.append(pts[-1]) return out def centroid(poly): return (sum(p[0] for p in poly) / len(poly), sum(p[1] for p in poly) / len(poly)) def _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 inset_vertical(poly, d, step=0.01): """the outline's top lowered and bottom raised by d at every y (robust at the pointed ends, which simply get shorter)""" ys = [p[0] for p in poly] top, bot = [], [] y = min(ys) + 1e-4 while y < max(ys): sp = _span(poly, y) if sp and sp[1] - sp[0] > 2 * d + 0.004: top.append((y, sp[1] - d)); bot.append((y, sp[0] + d)) y += step return top + bot[::-1] def gauss_closed(poly, sigma, step=0.005): """a closed outline resampled every `step` and smoothed along itself with a Gaussian of `sigma` (metres): takes the hand-drawn tremor out and rounds corners, without moving the outline more than a few millimetres elsewhere""" pts = resample(poly, step) n = len(pts) k = max(1, int(3 * sigma / step)) w = [math.exp(-0.5 * (i * step / sigma) ** 2) for i in range(-k, k + 1)] sw = sum(w) out = [] for i in range(n): y = z = 0.0 for j in range(-k, k + 1): q = pts[(i + j) % n] y += w[j + k] * q[0]; z += w[j + k] * q[1] out.append((y / sw, z / sw)) return out[::4] # the canopy: the user's drawn outline of 2026-10-20 (sf_measure.CANOPY_YZ) smoothed (6 cm), its glass edge 22 mm inside # (smoothed again, 2 cm), else the blueprint's. After the user's "looks terrible those frames": the first version followed # the drawing's tremor and the frames were part of the hull grid, so they wobbled and changed width (2026-10-20) if "CANOPY_YZ" in globals(): P_CANOPY_DRAWN = gauss_closed(CANOPY_YZ, 0.06) P_CANOPY = gauss_closed(inset_vertical(P_CANOPY_DRAWN, CANOPY_INSET), 0.02) else: P_CANOPY = side_poly(CANOPY) P_SIDEWIN = gauss_closed(side_poly(SIDEWIN), 0.025) # the blueprint's corners rounded (2.5 cm)