#!/home/sam/dev/perso/flsun/cad-env/bin/python
"""Ventouse Ø19,5 a fut d'emboitement, d'apres le croquis de Sam (2026-09-08).

    ./cad/ventouse.py            -> cad/ventouse.stl (+ apercu PNG avec coupe cotee)

Fonction : la coupelle colle sur la surface, le FUT Ø9 qui la surmonte s'emboite
dans le logement prevu pour la ventouse, et le teton Ø3 x 2 au sommet centre
l'ensemble. Trou borgne Ø3 par le dessous (allegement / demoulage).

Empilement donne par Sam, du bord de la levre au sommet du teton :
    corps 2,0 + fut 6,5 + teton 2,0 = 10,5 hors-tout.
Geometrie figee : Sam a valide la coupe le 2026-09-08, cotes du croquis comme
valeurs deduites (creux, levre). La profondeur du trou est comptee depuis son
entree reelle, c'est-a-dire le fond du creux.
Tout est en mm, piece modelisee levre en bas (plan de contact = z 0).
"""
import os, struct
import numpy as np
from manifold3d import Manifold, CrossSection

# ---- cotes du croquis (certaines) -------------------------------------------
D_EXT    = 19.5   # Ø hors-tout de la coupelle
H_CORPS  = 2.0    # hauteur du corps de ventouse, du bord de la levre au pied du fut
D_FUT    = 9.0    # Ø exterieur du fut d'emboitement
H_FUT    = 6.5    # hauteur du fut
D_TETON  = 3.0    # Ø du teton de centrage
H_TETON  = 2.0    # hauteur du teton de centrage
D_TROU   = 3.5    # Ø du trou borgne, par le dessous — different du teton Ø3
P_TROU   = 4.0    # profondeur du trou, comptee depuis son entree (fond du creux)
# ---- cotes deduites, validees par Sam sur la coupe le 2026-09-08 ------------
CREUX    = 1.0    # profondeur de la concavite au centre : c'est elle qui fait le vide
E_LEVRE  = 0.8    # epaisseur de la levre au bord
L_LEVRE  = 1.5    # largeur de la portee annulaire
BOMBE    = 1.8    # exposant du dessus du corps : 1 = cone, >1 = bombe
SEG      = 128    # segments de revolution

H = H_CORPS + H_FUT + H_TETON      # 10,5 hors-tout
R_EXT, R_FUT, R_TETON = D_EXT / 2, D_FUT / 2, D_TETON / 2
R_INT  = R_EXT - L_LEVRE
Z_FUT  = H_CORPS                   # pied du fut
Z_TET  = H_CORPS + H_FUT           # pied du teton de centrage


def profil(n=80):
    """Contour ferme (r, z) de la demi-coupe, sens antihoraire."""
    p = []
    r = np.linspace(0, R_INT, n)                       # dessous : concavite parabolique
    p += [(x, CREUX * (1 - (x / R_INT) ** 2)) for x in r]
    p += [(R_EXT, 0.0), (R_EXT, E_LEVRE)]              # levre : portee plate puis flanc
    r = np.linspace(R_EXT, R_FUT, n)                   # dessus de la coupelle, legerement bombe
    t = (r - R_FUT) / (R_EXT - R_FUT)
    p += [(x, E_LEVRE + (Z_FUT - E_LEVRE) * (1 - u) ** BOMBE) for x, u in zip(r, t)]
    p += [(R_FUT, Z_TET), (R_TETON, Z_TET), (R_TETON, H), (0.0, H)]   # fut, epaulement, teton
    return p


def build():
    v = Manifold.revolve(CrossSection([profil()]), SEG)
    h = CREUX + P_TROU                                  # sommet du trou = entree + profondeur
    trou = Manifold.cylinder(h + 1, D_TROU / 2, D_TROU / 2, 64)
    return v - trou.translate([0, 0, h - (h + 1)])      # borgne, debouche par le dessous


def write_stl(m, path):
    mesh = m.to_mesh()
    v = np.asarray(mesh.vert_properties)[:, :3]
    t = np.asarray(mesh.tri_verts)
    tri = v[t]
    n = np.cross(tri[:, 1] - tri[:, 0], tri[:, 2] - tri[:, 0])
    n /= np.maximum(np.linalg.norm(n, axis=1), 1e-12)[:, None]
    with open(path, "wb") as f:
        f.write(b"ventouse flsun".ljust(80, b"\0"))
        f.write(struct.pack("<I", len(t)))
        rec = np.zeros(len(t), dtype=[("n", "<3f4"), ("v", "<9f4"), ("a", "<u2")])
        rec["n"] = n; rec["v"] = tri.reshape(-1, 9)
        f.write(rec.tobytes())
    return len(t)


def cote(ax, p0, p1, txt, dec=0.0, coul="#c8141e"):
    """Ligne de cote entre deux points, texte au milieu."""
    (x0, y0), (x1, y1) = p0, p1
    ax.annotate("", (x1, y1), (x0, y0), arrowprops=dict(arrowstyle="<->", color=coul, lw=1.1))
    ax.text((x0 + x1) / 2 + dec, (y0 + y1) / 2, txt, color=coul, fontsize=8.5,
            ha="center", va="center", bbox=dict(fc="white", ec="none", pad=0.8))


def preview(m, path):
    import matplotlib; matplotlib.use("Agg")
    import matplotlib.pyplot as plt
    from mpl_toolkits.mplot3d.art3d import Poly3DCollection
    mesh = m.to_mesh(); v = np.asarray(mesh.vert_properties)[:, :3]; t = np.asarray(mesh.tri_verts)
    fig = plt.figure(figsize=(14, 5.4))

    for i, (el, az, titre) in enumerate(((20, -60, "3/4 dessus (teton)"), (-22, -60, "dessous (coupelle)"))):
        ax = fig.add_subplot(1, 3, i + 1, projection="3d")
        ax.add_collection3d(Poly3DCollection(v[t], facecolor="#c94b3f", edgecolor="#5a2620", linewidth=0.07))
        ax.set_xlim(-R_EXT, R_EXT); ax.set_ylim(-R_EXT, R_EXT); ax.set_zlim(0, H)
        ax.set_box_aspect((1, 1, H / D_EXT))
        ax.view_init(el, az); ax.set_title(titre, fontsize=11, pad=-2); ax.set_axis_off()

    ax = fig.add_subplot(1, 3, 3)
    p = np.array(profil())
    plein = np.vstack([p, [[0, H]]])
    dess = p[:80]                                       # la concavite seule
    for s_ in (1, -1):                                  # volume d'air emprisonne
        ax.fill(np.concatenate([s_ * dess[:, 0], [s_ * R_INT, 0]]),
                np.concatenate([dess[:, 1], [0.0, 0.0]]), fc="#8ddcea", ec="none", zorder=1)
    for s_ in (1, -1):                                  # coupe complete, symetrique
        ax.fill(s_ * plein[:, 0], plein[:, 1], fc="#e8b8b0", ec="#5a2620", lw=1.3, zorder=2)
    # le trou s'arrete exactement ou il commence et ou il finit : rien ne depasse
    ax.add_patch(plt.Rectangle((-D_TROU / 2, CREUX), D_TROU, P_TROU,
                               fc="white", ec="#5a2620", lw=1.3, zorder=3))
    ax.plot([0, 0], [-1.2, H + 2.6], "-.", color="#3a5fa0", lw=.8, zorder=4)
    cote(ax, (-R_EXT, -1.9), (R_EXT, -1.9), "Ø19,5")
    cote(ax, (R_EXT + 3.0, 0), (R_EXT + 3.0, H), "10,5")
    cote(ax, (-R_FUT, H + 2.1), (R_FUT, H + 2.1), "Ø9  fut")
    cote(ax, (-D_TETON / 2, H + 0.9), (D_TETON / 2, H + 0.9), "Ø3")
    cote(ax, (R_FUT + 1.6, Z_TET), (R_FUT + 1.6, H), "2")
    cote(ax, (R_FUT + 1.6, Z_FUT), (R_FUT + 1.6, Z_TET), "6,5")
    cote(ax, (R_EXT + 0.6, 0), (R_EXT + 0.6, Z_FUT), "2")
    cote(ax, (-R_EXT - 3.0, CREUX), (-R_EXT - 3.0, CREUX + P_TROU), "4")
    cote(ax, (-D_TROU / 2, CREUX + P_TROU + 1.1), (D_TROU / 2, CREUX + P_TROU + 1.1), "Ø3,5")
    ax.annotate("creux 1,0  (l'air emprisonne, en bleu)", (-4.5, CREUX / 2), (-16.5, -3.6), color="#1f8ea0", fontsize=9,
                arrowprops=dict(arrowstyle="->", color="#1f8ea0", lw=1))
    ax.annotate("levre 0,8", (R_EXT - 0.7, E_LEVRE / 2), (8.5, -3.6), color="#1f8ea0", fontsize=9,
                arrowprops=dict(arrowstyle="->", color="#1f8ea0", lw=1))
    ax.set_title("coupe A-A   —   rouge = croquis, cyan = deduit", fontsize=11)
    ax.set_aspect("equal"); ax.set_xlim(-17.5, 17.5); ax.set_ylim(-5.0, 14.0); ax.axis("off")

    fig.subplots_adjust(left=.01, right=.99, top=.96, bottom=.02, wspace=.05)
    fig.savefig(path, dpi=125); return True


if __name__ == "__main__":
    here = os.path.dirname(os.path.abspath(__file__))
    m = build()
    n = write_stl(m, os.path.join(here, "ventouse.stl"))
    print("ventouse Ø%.1f x %.1f mm, teton Ø%.1f, trou Ø%.1f p%.1f, %d triangles, volume %.2f cm3"
          % (D_EXT, H, D_TETON, D_TROU, P_TROU, n, m.volume() / 1000))
    print("apercu :", preview(m, os.path.join(here, "ventouse.png")))
