"""Grouper, round 3: the before/after sheets of the topology round (plain Python 3, PIL, numpy; reads pictures and JSON only). python build/gr_compare_topo.py [OLD_TAG] [NEW_TAG] (defaults r2 r3) previews/compare/topology_before_after.png each wireframe shot of round 2 beside round 3's (previews/_topo_.png), then the zebra pairs (previews/_zebra_.png) where both exist previews/compare/lines_side.png the sheet's SIDE view with the fair design curves (build/fair_curves.json, red), their curvature combs (green) and the mesh's own design-line vertices (port side, cyan dots on their polylines, from build/checks/skin_struct.json); under it the turning per vertex of each mesh line along the hull (build/checks/lines.json) """ import json, math, os, sys import numpy as np from PIL import Image, ImageDraw, ImageFont HERE = os.path.dirname(os.path.abspath(__file__)) ROOT = os.path.dirname(HERE) PRE = os.path.join(ROOT, "previews") CMP = os.path.join(PRE, "compare") os.makedirs(CMP, exist_ok=True) OLD = sys.argv[1] if len(sys.argv) > 1 else "r2" NEW = sys.argv[2] if len(sys.argv) > 2 else "r3" SHOTS = ["q_front_stbd", "q_low_front", "q_rear_port", "q_top", "z_collar", "z_door", "z_nose", "z_stern"] ZEBRA = ["z_door", "z_stern", "q_low_front", "z_nose", "q_front_stbd", "q_rear_port", "z_collar", "q_top"] S, X0, Z0 = 0.0171, 748.14, 374.66 def font(sz): for f in ("arial.ttf", "DejaVuSans.ttf"): try: return ImageFont.truetype(f, sz) except OSError: pass return ImageFont.load_default() def content_box(im, bg_tol=10): a = np.asarray(im.convert("RGB")).astype(int) bg = a[2, 2] m = np.abs(a - bg).sum(2) > bg_tol ys, xs = np.nonzero(m) if not len(xs): return (0, 0, im.width, im.height) return (int(xs.min()), int(ys.min()), int(xs.max()) + 1, int(ys.max()) + 1) def pair_row(a_path, b_path, title, W=1000): A, B = Image.open(a_path).convert("RGB"), Image.open(b_path).convert("RGB") ba, bb = content_box(A), content_box(B) box = (max(0, min(ba[0], bb[0]) - 30), max(0, min(ba[1], bb[1]) - 30), min(A.width, max(ba[2], bb[2]) + 30), min(A.height, max(ba[3], bb[3]) + 30)) A, B = A.crop(box), B.crop(box) h = int(round(A.height * W / A.width)) A, B = A.resize((W, h), Image.LANCZOS), B.resize((W, h), Image.LANCZOS) row = Image.new("RGB", (2 * W + 30, h + 46), (24, 25, 28)) row.paste(A, (0, 46)); row.paste(B, (W + 30, 46)) d = ImageDraw.Draw(row) f = font(26) d.text((10, 8), "%s round 2 (before)" % title, fill=(235, 235, 235), font=f) d.text((W + 40, 8), "%s round 3 (after)" % title, fill=(235, 235, 235), font=f) return row def before_after(): rows = [] for nm in SHOTS: a, b = os.path.join(PRE, "%s_topo_%s.png" % (OLD, nm)), os.path.join(PRE, "%s_topo_%s.png" % (NEW, nm)) if os.path.exists(a) and os.path.exists(b): rows.append(pair_row(a, b, "wireframe " + nm)) for nm in ZEBRA: a, b = os.path.join(PRE, "%s_zebra_%s.png" % (OLD, nm)), os.path.join(PRE, "%s_zebra_%s.png" % (NEW, nm)) if os.path.exists(a) and os.path.exists(b): rows.append(pair_row(a, b, "zebra " + nm)) if not rows: print("COMPARE no pairs") return W = max(r.width for r in rows) H = sum(r.height for r in rows) + 12 * len(rows) sheet = Image.new("RGB", (W, H), (24, 25, 28)) y = 0 for r in rows: sheet.paste(r, (0, y)); y += r.height + 12 out = os.path.join(CMP, "topology_before_after.png") sheet.save(out, optimize=True) print("COMPARE", out, sheet.size, len(rows), "rows") def lines_side(): box = (520, 300, 925, 490) K = 4 x0, y0, x1, y1 = box sheet = Image.open(os.path.join(ROOT, "reference", "user_blueprint.jpg")).convert("RGB").crop(box) sheet = sheet.resize(((x1 - x0) * K, (y1 - y0) * K), Image.LANCZOS) sheet = Image.blend(sheet, Image.new("RGB", sheet.size, (0, 0, 0)), 0.5) d = ImageDraw.Draw(sheet) f = lambda x, y: ((x - x0) * K, (y - y0) * K) FC = json.load(open(os.path.join(HERE, "fair_curves.json"))) for nm, c in FC.items(): if not isinstance(c, dict) or "x" not in c: continue X, Y = np.array(c["x"]), np.array(c["y"]) if "k" in c and "tx" in c: k, tx, ty = np.array(c["k"]), np.array(c["tx"]), np.array(c["ty"]) for i in range(0, len(X), 8): # combs: 60 px per unit curvature (1/px) q = (X[i] + ty[i] * k[i] * 60.0, Y[i] - tx[i] * k[i] * 60.0) d.line([f(X[i], Y[i]), f(*q)], fill=(40, 190, 80), width=1) d.line([f(x, y) for x, y in zip(X, Y)], fill=(255, 50, 50), width=2) SK = json.load(open(os.path.join(HERE, "checks", "skin_struct.json"))) P = np.array(SK["base"]) for nm in ("UR0", "UR3", "LR0_L", "LR3_L", "FB0", "FB1"): ids = SK["lines"].get(nm) if not ids: continue Q = P[ids] Q = Q[Q[:, 0] > 1e-4] if nm[:2] in ("UR", "FB") else Q pts = [f(X0 - q[1] / S, Z0 - q[2] / S) for q in Q] d.line(pts, fill=(0, 220, 255), width=1) for u, v in pts: d.ellipse([u - 3, v - 3, u + 3, v + 3], outline=(0, 230, 255)) ft = font(22) d.text((14, 10), "SIDE (sheet, darkened): fair design curves (red) with curvature combs (green); the mesh's line vertices " "(cyan, port side, base surface)", fill=(240, 240, 240), font=ft) # turning per vertex along the hull, per mesh line (lines.json) L = json.load(open(os.path.join(HERE, "checks", "lines.json"))) pw, ph = sheet.width, 520 plot = Image.new("RGB", (pw, ph), (22, 23, 26)) dp = ImageDraw.Draw(plot) ymax = 12.0 gy = lambda t: ph - 40 - (min(t, ymax) / ymax) * (ph - 90) for t_ in (2, 4, 6, 8, 10, 12): dp.line([(60, gy(t_)), (pw - 20, gy(t_))], fill=(70, 70, 70) if t_ not in (4, 6) else (150, 110, 40), width=1) dp.text((10, gy(t_) - 12), "%d deg" % t_, fill=(200, 200, 200), font=font(20)) cols = {"UR0": (255, 80, 80), "UR3": (255, 160, 60), "LR0_L": (90, 200, 255), "LR3_L": (90, 255, 200), "FB0": (230, 230, 90), "FB1": (200, 120, 255)} for nm, col in cols.items(): if nm not in L or nm not in SK["lines"]: continue ids = SK["lines"][nm] if nm in ("FB0", "FB1"): ids = ids[1:-1] t = np.abs(np.array(L[nm]["turn_deg"])) Q = P[ids][1:-1] xs = X0 - Q[:, 1] / S keep = Q[:, 0] > -1e-4 pts = [((x - x0) * K, gy(v)) for x, v, kp in zip(xs, t, keep) if kp and x0 <= x <= x1] if len(pts) > 1: dp.line(pts, fill=col, width=2) dp.text((70, 8), "turning per vertex (deg) along the hull: UR0 red, UR3 orange, LR0 blue, LR3 green, FB0 yellow, FB1 violet " "(the limits 4 deg, and 6 deg in round corners, in amber)", fill=(230, 230, 230), font=font(22)) out_img = Image.new("RGB", (pw, sheet.height + ph), (22, 23, 26)) out_img.paste(sheet, (0, 0)); out_img.paste(plot, (0, sheet.height)) out = os.path.join(CMP, "lines_side.png") out_img.save(out, optimize=True) print("COMPARE", out, out_img.size) if __name__ == "__main__": before_after() lines_side()