#!/usr/bin/env python3
"""Quadruped build guide: real 3D, 4 views per step, from kit_v3_1 geometry."""
import os, math
os.environ["PYOPENGL_PLATFORM"] = "osmesa"
import numpy as np, trimesh, pyrender
from PIL import Image, ImageDraw, ImageFont
import kit_v3_1 as K

T = 3.7; TH = T/2
COL = dict(fork=(0.86,0.72,0.48,1), cheek=(0.93,0.83,0.62,1), lolli=(0.94,0.64,0.35,1),
           bar=(0.70,0.54,0.33,1), cookie=(0.88,0.76,0.55,1), sole=(0.60,0.48,0.32,1),
           fin=(0.60,0.48,0.32,1), pin=(0.55,0.42,0.28,1), head=(0.94,0.64,0.35,1))

def mesh2d(poly, color, R, origin, thick=T):
    """Extrude shapely poly (its plane -> columns of R: u,v in-plane, w normal), center on w."""
    if poly.geom_type == "MultiPolygon":
        poly = max(poly.geoms, key=lambda g: g.area)
    m = trimesh.creation.extrude_polygon(poly, thick)
    m.apply_translation([0,0,-thick/2])
    M = np.eye(4); M[:3,0], M[:3,1], M[:3,2] = R[:,0], R[:,1], R[:,2]; M[:3,3] = origin
    m.apply_transform(M)
    m.visual.face_colors = [int(c*255) for c in color]
    return m

# plane bases (columns: u,v,normal)
YZ = np.array([[0,0,1],[1,0,0],[0,1,0]]).T      # u->y, v->z, n->x
XZ = np.array([[1,0,0],[0,0,1],[0,1,0]]).T      # u->x, v->z, n->y
XY = np.eye(3)                                   # horizontal

# ---- frame constants (mm) ----
SOLE_TOP = T
FIN_TOP = SOLE_TOP + 22.0
BAR_BOT = FIN_TOP - 9.0
BAR_TOP = BAR_BOT + 50.0
ZP = BAR_TOP + 29.0                 # clevis pocket center height
COOKIE_MID = ZP + (57.0 - 9.0)      # fork tail length 57, notch overlap 9
ZS = COOKIE_MID - 8.0               # spine bottom edge
XC_R, XC_F = 13.0, 97.0             # cookie centers (rear, front) on 110 spine
HIP_Y = 17.5                        # tail centered in notch at r 13..22

def clevis(x, y, splay_deg=0.0, with_pins=True):
    """One assembled clevis hanging from a cookie notch at (x, +/-y)."""
    parts = []
    sgn = 1 if y >= 0 else -1
    base = np.array([x, y, COOKIE_MID])
    # fork: local +u(tail)->+z, so use YZ base with fork rotated: fork local x->z, y->y*sgn
    Rf = np.array([[0,0,sgn*0],[sgn*1,0,0],[0,0,0]])  # build explicitly below
    Rf = np.column_stack(([0,sgn*1,0],[0,0,1],[sgn*1,0,0]))  # u->y*sgn? we want fork local x(tail)->+z, local y->y
    Rf = np.column_stack(([0,0,1],[0,1,0],[1,0,0]))   # u(localx)->z, v(localy)->y, n->x
    org = np.array([x, y, ZP])
    parts.append(mesh2d(K.fork_plate(), COL["fork"], Rf, org))
    for dx in (-T, T):
        parts.append(mesh2d(K.cheek(), COL["cheek"], Rf, org + [dx,0,0]))
    # lollipop: local y(stem) -> -z... lolli stem is -v; with Rf, v->y. Need stem -z: use base u->? 
    Rl = np.column_stack(([0,1,0],[0,0,1],[1,0,0]))   # u(localx)->y, v(localy)->z: stem(-v)->-z
    lol = mesh2d(K.lolli(), COL["lolli"], Rl, org)
    if splay_deg:
        rot = trimesh.transformations.rotation_matrix(math.radians(sgn*splay_deg), [1,0,0], org)
        lol.apply_transform(rot)
    parts.append(lol)
    if with_pins:
        for a in K.PIN_ANGS + [None]:
            if a is None:
                pu, pv = K.TAIL_PIN_X, 0
            else:
                pu, pv = K.PIN_R*math.cos(math.radians(a)), K.PIN_R*math.sin(math.radians(a))
            ppos = org + Rf[:,0]*pu + Rf[:,1]*pv
            pin = trimesh.creation.box([3.6, 3.4, 12.0]) if abs(Rf[2,0])<0.5 else trimesh.creation.box([12.0,3.4,3.6])
            pin = trimesh.creation.box([12.0, 3.4, 3.6])
            pin.apply_transform(trimesh.transformations.rotation_matrix(math.pi/2,[0,1,0]))
            pin.apply_translation(ppos)
            pin.visual.face_colors = [int(c*255) for c in COL["pin"]]
            parts.append(pin)
    return parts, org

def leg(x, y):
    Rb = np.column_stack(([0,0,-1],[1,0,0],[0,1,0]))  # bar local x -> -z (hangs), local y -> x
    parts = [mesh2d(K.strut(50), COL["bar"], Rb, [x, y, ZP-29.0])]
    return parts

def foot(x, y):
    Rfin = np.column_stack(([0,1,0],[0,0,1],[1,0,0]))  # fin local x->y, y->z
    parts = [mesh2d(K.sole(), COL["sole"], XY, [x-22, y-13, SOLE_TOP-T/2+ -0.0], thick=T),
             mesh2d(K.fin(), COL["fin"], Rfin, [x, y-13, SOLE_TOP])]
    # fix sole centering: sole() local origin at corner; translate handled above via x-22,y-13
    return parts

def chassis():
    Rs = np.column_stack(([1,0,0],[0,0,1],[0,1,0]))   # spine local x->x, y->z (on edge)
    parts = [mesh2d(K.strut(110), COL["bar"], Rs, [0, 0, ZS+8.0-8.0+8.0])]
    # place: spine local y in [-8,8] -> z ZS..ZS+16: origin z = ZS+8
    parts = [mesh2d(K.strut(110), COL["bar"], Rs, [0, 0, ZS+8.0])]
    parts.append(mesh2d(K.hub(4), COL["cookie"], XY, [XC_F, 0, COOKIE_MID]))
    parts.append(mesh2d(K.hub(5), COL["cookie"], XY, [XC_R, 0, COOKIE_MID]))
    return parts

def neck_head():
    Rn = np.column_stack(([0,0,1],[1,0,0],[0,1,0]))   # neck bar local x->z (up), y->x
    parts = [mesh2d(K.strut(50), COL["bar"], Rn, [XC_F+17.5, 0, COOKIE_MID-9+0.0])]
    # neck bottom slit engages notch: bar local x0 at z=COOKIE_MID-9? origin maps local x0->z origin; set origin z so bottom= COOKIE_MID-9... local x in 0..50 -> z org..org+50: org = COOKIE_MID-9
    Rh = np.column_stack(([0,1,0],[0,0,1],[1,0,0]))   # head local x->y (faces +y), y->z
    hz = COOKIE_MID - 9 + 50 - 9 + 12.5
    parts.append(mesh2d(K.head(), COL["head"], Rh, [XC_F+17.5, -41, hz]))
    return parts

# ---------- steps ----------
def step_parts(n):
    P = []
    if n == 1:   # one clevis at origin-ish, exploded along x
        org = np.array([0,0,60])
        Rf = np.column_stack(([0,0,1],[0,1,0],[1,0,0]))
        Rl = np.column_stack(([0,1,0],[0,0,1],[1,0,0]))
        P.append(mesh2d(K.cheek(), COL["cheek"], Rf, org + [-80,0,0]))
        P.append(mesh2d(K.fork_plate(), COL["fork"], Rf, org + [-27,0,0]))
        P.append(mesh2d(K.lolli(), COL["lolli"], Rl, org + [27,0,0]))
        P.append(mesh2d(K.cheek(), COL["cheek"], Rf, org + [80,0,0]))
        for i,dx in enumerate((-30,)):
            for a in K.PIN_ANGS + [999]:
                pu,pv = (K.TAIL_PIN_X,0) if a==999 else (K.PIN_R*math.cos(math.radians(a)), K.PIN_R*math.sin(math.radians(a)))
                pin = trimesh.creation.box([10,3.4,3.6])
                pin.apply_transform(trimesh.transformations.rotation_matrix(math.pi/2,[0,1,0]))
                pin.apply_translation(org + [-103, pv, pu])
                pin.visual.face_colors=[int(c*255) for c in COL["pin"]]
                P.append(pin)
        return P, "STEP 1  BUILD 4 CLEVISES  (exploded: cheek+pins / fork / lollipop / cheek; stack along the pins)"
    if n == 2:
        c,_ = clevis(0, 0.01); P += c; P += leg(0, 0.01)
        return P, "STEP 2  LEG ONTO STEM  (S50 end slit onto the lollipop stem slit, x4)"
    if n == 3:
        c,_ = clevis(0, 0.01); P += c; P += leg(0, 0.01); P += foot(0, 0.01)
        return P, "STEP 3  FOOT  (fin tab through sole, leg bar into fin slit, x4)"
    if n == 4:
        P += chassis()
        return P, "STEP 4  CHASSIS  (S110 on edge; H4 cookie on front end, H5 on rear; cookies flat)"
    if n == 5:
        P += chassis()
        for x in (XC_F, XC_R):
            for y in (HIP_Y, -HIP_Y):
                c,_ = clevis(x, y); P += c
        return P, "STEP 5  HIPS  (each clevis tail slit UP into a cookie side notch, mouth down)"
    if n == 6:
        P += chassis()
        for x in (XC_F, XC_R):
            for y in (HIP_Y, -HIP_Y):
                c,_ = clevis(x, y); P += c; P += leg(x, y); P += foot(x, y)
        return P, "STEP 6  LEGS + FEET ON ALL FOUR  (it stands; set equal tick angles, then splay to pose)"
    if n == 7:
        P += chassis()
        for x in (XC_F, XC_R):
            for y in (HIP_Y, -HIP_Y):
                c,_ = clevis(x, y); P += c; P += leg(x, y); P += foot(x, y)
        P += neck_head()
        return P, "STEP 7  NECK + HEAD  (spare S50 upright in the front notch; head slit onto its top; faces sideways, that's the cat's choice)"
    return P, ""

# ---------- rendering ----------
def render_views(meshes, size=560):
    scene_all = trimesh.util.concatenate(meshes)
    b = scene_all.bounds; center = b.mean(axis=0); ext = (b[1]-b[0])
    radius = float(np.linalg.norm(ext))/2
    imgs = []
    views = [("iso", None), ("front", "front"), ("side", "side"), ("top", "top")]
    for name, kind in views:
        scene = pyrender.Scene(bg_color=[245,246,248,255], ambient_light=[0.45]*3)
        for m in meshes:
            scene.add(pyrender.Mesh.from_trimesh(m, smooth=False))
        if kind is None:
            cam = pyrender.PerspectiveCamera(yfov=math.radians(30), znear=1.0, zfar=8000.0)
            el, az = 24, -55
            dist = radius/math.sin(math.radians(15))*1.12
            eye = center + dist*np.array([math.cos(math.radians(el))*math.cos(math.radians(az)),
                                          math.cos(math.radians(el))*math.sin(math.radians(az)),
                                          math.sin(math.radians(el))])
        else:
            mag = radius*1.12
            cam = pyrender.OrthographicCamera(xmag=mag, ymag=mag, znear=1.0, zfar=8000.0)
            eye = center + {"front": np.array([0,-4*radius,0]),
                            "side":  np.array([4*radius,0,0]),
                            "top":   np.array([0,0.001,4*radius])}[kind]
        f = center-eye; f/=np.linalg.norm(f)
        up = np.array([0,0,1.0]) if kind!="top" else np.array([0,1.0,0])
        s = np.cross(f,up); s/=np.linalg.norm(s); u = np.cross(s,f)
        M = np.eye(4); M[:3,0],M[:3,1],M[:3,2],M[:3,3] = s,u,-f,eye
        scene.add(cam, pose=M)
        scene.add(pyrender.DirectionalLight(intensity=3.0), pose=M)
        M2 = M.copy(); scene.add(pyrender.DirectionalLight(intensity=1.2),
                                 pose=trimesh.transformations.rotation_matrix(2.2,[0,0,1],center) @ M)
        r = pyrender.OffscreenRenderer(size, size)
        color,_ = r.render(scene); r.delete()
        im = Image.fromarray(color)
        d = ImageDraw.Draw(im); d.text((10,8), name, fill=(60,60,70))
        imgs.append(im)
    return imgs

CELL = 560
rows = []
for n in range(1, 8):
    meshes, caption = step_parts(n)
    imgs = render_views(meshes, CELL)
    row = Image.new("RGB", (CELL*4, CELL+46), (19,26,38))
    for i, im in enumerate(imgs):
        row.paste(im, (i*CELL, 46))
    d = ImageDraw.Draw(row)
    try:
        font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 24)
    except Exception:
        font = ImageFont.load_default()
    d.text((14, 10), caption, fill=(240,163,92), font=font)
    rows.append(row)
W = CELL*4; Hs = sum(r.height for r in rows)
sheet = Image.new("RGB", (W, Hs+60), (19,26,38))
d = ImageDraw.Draw(sheet)
try:
    tfont = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 32)
except Exception:
    tfont = ImageFont.load_default()
d.text((14, 12), "QUADRUPED — kit v3.1 — every step, four views (iso / front / side / top)", fill=(232,220,200), font=tfont)
y = 60
for r in rows:
    sheet.paste(r, (0, y)); y += r.height
sheet.save("quad_steps.png")
print("quad_steps.png", sheet.size)
