#!/usr/bin/env python3
"""Generate black-and-white vector OUTLINE icons of a chip STEP via OCCT HLR
(hidden-line removal), at the user's chosen orientation.

Usage:  occt-outline.py <step> <outdir> <mpn> [axis]

Writes <mpn>-3d-outline-<style>.svg for each style (thin/bold/blueprint/teal/dark),
rendered at the same camera as chip-thumbnailer's iso pose for <axis>
(z/asIs, y/yUp, x/xUp, -y/yDown, -x/xDown, -z/zDown). Requires cadquery-ocp (OCP).
"""
import sys, os, math
from OCP.STEPControl import STEPControl_Reader
from OCP.HLRBRep import HLRBRep_Algo, HLRBRep_HLRToShape
from OCP.HLRAlgo import HLRAlgo_Projector
from OCP.gp import gp_Pnt, gp_Dir, gp_Ax2, gp_Ax3, gp_Trsf
from OCP.Bnd import Bnd_Box
from OCP.BRepBndLib import BRepBndLib
from OCP.TopExp import TopExp_Explorer
from OCP.TopAbs import TopAbs_EDGE
from OCP.TopoDS import TopoDS
from OCP.BRepAdaptor import BRepAdaptor_Curve
from OCP.GeomAbs import GeomAbs_Line
from OCP.BRepBuilderAPI import BRepBuilderAPI_Transform

try:
    from hershey_simplex import FONT as HERSHEY
except Exception:
    import os as _os
    sys.path.insert(0, _os.path.dirname(_os.path.abspath(__file__)))
    from hershey_simplex import FONT as HERSHEY

# Single-stroke text: one line of Hershey glyphs → list of 2D polylines (font
# units; x advances, baseline y=0, left-origin). Returns (polylines, width).
def _hershey_line(text):
    polys = []
    cx = 0.0
    for ch in text:
        g = HERSHEY.get(ord(ch)) or HERSHEY.get(63)  # '?' fallback
        w = g[0]
        coords = g[1:]
        cur = []
        i = 0
        while i < len(coords):
            if coords[i] is None:
                if len(cur) >= 2:
                    polys.append(cur)
                cur = []
                i += 1
                continue
            cur.append((cx + coords[i], coords[i + 1]))
            i += 2
        if len(cur) >= 2:
            polys.append(cur)
        cx += w
    return polys, cx

CAP = 21.0  # Hershey cap height in font units

# chip-thumbnailer's _UP_AXIS_VIEW (proj, up) per orientation.
AXIS_VIEW = {
    "asis": ((1, 1, 1), (0, 0, 1)),
    "zdown": ((1, -1, -1), (0, 0, -1)),
    "yup":  ((1, 1, -1), (0, 1, 0)),
    "ydown": ((1, -1, 1), (0, -1, 0)),
    "xup":  ((1, 1, -1), (1, 0, 0)),
    "xdown": ((-1, 1, 1), (-1, 0, 0)),
}
ALIAS = {"z": "asis", "zup": "asis", "": "asis", "asis": "asis",
         "y": "yup", "yup": "yup", "-y": "ydown", "ydown": "ydown",
         "x": "xup", "xup": "xup", "-x": "xdown", "xdown": "xdown",
         "-z": "zdown", "zdown": "zdown"}

def nrm(v): n = math.sqrt(sum(c*c for c in v)) or 1; return tuple(c/n for c in v)
def dot(a, b): return sum(x*y for x, y in zip(a, b))
def cross(a, b): return (a[1]*b[2]-a[2]*b[1], a[2]*b[0]-a[0]*b[2], a[0]*b[1]-a[1]*b[0])

def view_trsf(proj, up):
    d = nrm((-proj[0], -proj[1], -proj[2]))
    u = nrm((up[0]-dot(up, d)*d[0], up[1]-dot(up, d)*d[1], up[2]-dot(up, d)*d[2]))
    r = nrm(cross(d, u))
    ax3 = gp_Ax3(gp_Pnt(0, 0, 0), gp_Dir(-d[0], -d[1], -d[2]), gp_Dir(*r))
    t = gp_Trsf(); t.SetTransformation(ax3)
    return t

def to_view(shape, proj, up):
    return BRepBuilderAPI_Transform(shape, view_trsf(proj, up), True).Shape()

# Build single-stroke name text, placed flat on the chip's TOP face (per up-axis)
# and projected through the SAME view transform as the chip → 2D polylines in the
# chip's projected space. `lines` is a list of strings (line 2 = variant suffix).
def name_polylines_2d(orig_shape, up, lines, proj, vproj):
    b = Bnd_Box(); BRepBndLib.Add_s(orig_shape, b)
    xmin, ymin, zmin, xmax, ymax, zmax = b.Get()
    if up == "y":
        width, depth, top = (xmax - xmin), (zmax - zmin), ymax
        cu, cv = (xmin + xmax) / 2, (zmin + zmax) / 2
    else:  # z-up / as-is
        width, depth, top = (xmax - xmin), (ymax - ymin), zmax
        cu, cv = (xmin + xmax) / 2, (ymin + ymax) / 2
    lines = [l for l in lines if l]
    n = len(lines) or 1
    # Fit: widest line spans ~74% of the face width; all lines stack within ~46%
    # of the depth. Scale = font-units → mm.
    raw = [(_hershey_line(s)) for s in lines]
    maxw = max((w for _, w in raw), default=1) or 1
    line_gap = CAP * 0.5
    block_h = n * CAP + (n - 1) * line_gap
    sc = min(width * 0.74 / maxw, depth * 0.46 / block_h)
    t = view_trsf(proj, vproj)
    out = []
    # stack lines top→down (first line highest)
    y_cursor = (block_h / 2.0)  # in font units, centered block
    for polys, w in raw:
        # center this line horizontally; baseline so the line's cap-center sits at y_cursor-CAP/2
        x_off = -w / 2.0
        y_base = (y_cursor - CAP)  # baseline of this line (font units)
        for poly in polys:
            pts3 = []
            for (fx, fy) in poly:
                lx = (fx + x_off) * sc
                ly = (fy + y_base) * sc
                if up == "y":
                    P = gp_Pnt(cu - lx, top + 0.002, cv + ly)
                else:
                    P = gp_Pnt(cu + lx, cv + ly, top + 0.002)
                P.Transform(t)
                pts3.append((P.X(), P.Y()))
            if len(pts3) >= 2:
                out.append(pts3)
        y_cursor -= (CAP + line_gap)
    return out

def polylines(comp, n=22):
    out = []
    if comp is None or comp.IsNull(): return out
    exp = TopExp_Explorer(comp, TopAbs_EDGE)
    while exp.More():
        try:
            ad = BRepAdaptor_Curve(TopoDS.Edge_s(exp.Current()))
            a, b = ad.FirstParameter(), ad.LastParameter()
            steps = 1 if ad.GetType() == GeomAbs_Line else n
            pts = [(ad.Value(a+(b-a)*i/steps).X(), ad.Value(a+(b-a)*i/steps).Y()) for i in range(steps+1)]
            if len(pts) >= 2: out.append(pts)
        except Exception: pass
        exp.Next()
    return out

def hlr(shape):
    ax2 = gp_Ax2(gp_Pnt(0, 0, 0), gp_Dir(0, 0, 1), gp_Dir(1, 0, 0))
    a = HLRBRep_Algo(); a.Add(shape); a.Projector(HLRAlgo_Projector(ax2)); a.Update(); a.Hide()
    hs = HLRBRep_HLRToShape(a)
    def g(nm):
        try: return getattr(hs, nm)()
        except Exception: return None
    return {"vis": polylines(g("VCompound")), "hid": polylines(g("HCompound"))}

def svg(groups, vb, size=320, pad=0.10, bg=None):
    minx, miny, maxx, maxy = vb; w = (maxx-minx) or 1; h = (maxy-miny) or 1
    s = size*(1-2*pad)/max(w, h)
    ox = size*pad - minx*s + (size*(1-2*pad)-w*s)/2
    oy = size*pad + maxy*s + (size*(1-2*pad)-h*s)/2
    def P(poly): return " ".join(f"{ox+x*s:.2f},{oy-y*s:.2f}" for x, y in poly)
    body = f'<rect width="{size}" height="{size}" fill="{bg}"/>' if bg else ''
    for polys, st in groups:
        for poly in polys: body += f'<polyline points="{P(poly)}" {st}/>'
    return f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {size} {size}">{body}</svg>'

def main():
    # Optional --suffix <s> appends to the output filenames (e.g. "-named") so a
    # name-marked variant can sit beside the plain outline.
    argv = sys.argv[1:]
    suffix = ""
    name = ""
    line2 = ""
    for flag, setter in (("--suffix", "suffix"), ("--name", "name"), ("--line2", "line2")):
        if flag in argv:
            i = argv.index(flag); val = argv[i + 1]; del argv[i:i + 2]
            if setter == "suffix": suffix = val
            elif setter == "name": name = val
            else: line2 = val
    step, outdir, mpn = argv[0], argv[1], argv[2]
    axis = ALIAS.get((argv[3] if len(argv) > 3 else "").lower(), "asis")
    proj, up = AXIS_VIEW[axis]
    r = STEPControl_Reader(); r.ReadFile(step); r.TransferRoots()
    orig = r.OneShape()
    shape = to_view(orig, proj, up)
    H = hlr(shape)
    # Single-stroke part-number on the top face (centerline text, not outlined
    # glyphs — reads cleanly even for long names).
    nm = name_polylines_2d(orig, "y" if axis == "yup" else "z", [name, line2], proj, up) if name else []
    xs = [x for g in (H["vis"], H["hid"]) for poly in g for x, y in poly]
    ys = [y for g in (H["vis"], H["hid"]) for poly in g for x, y in poly]
    vb = (min(xs), min(ys), max(xs), max(ys)) if xs else (0, 0, 1, 1)
    J = 'stroke-linejoin="round" stroke-linecap="round"'
    # (visible-edge stroke, name stroke). Name is a touch thinner so the
    # centerline text stays crisp against the chip's silhouette.
    styles = {
        "thin":  (("#0d1117", "1.1", "#0d1117", "0.9"), None),
        "bold":  (("#0d1117", "2.4", "#0d1117", "1.6"), None),
        "teal":  (("#00e6dc", "1.7", "#00e6dc", "1.2"), None),
        "dark":  (("#e6edf3", "1.5", "#e6edf3", "1.1"), None),
    }
    for k, ((ec, ew, nc, nw), bg) in styles.items():
        gs = [(H["vis"], f'fill="none" stroke="{ec}" stroke-width="{ew}" {J}')]
        if nm:
            gs.append((nm, f'fill="none" stroke="{nc}" stroke-width="{nw}" {J}'))
        open(os.path.join(outdir, f"{mpn}-3d-outline-{k}{suffix}.svg"), "w").write(svg(gs, vb, bg=bg))
    # blueprint = WHITE visible lines + faint blue dashed hidden lines + white name.
    bp = [(H["hid"], 'fill="none" stroke="#7fa8d8" stroke-width="0.6" stroke-dasharray="3,2"'),
          (H["vis"], f'fill="none" stroke="#e6edf3" stroke-width="1.3" {J}')]
    if nm:
        bp.append((nm, f'fill="none" stroke="#e6edf3" stroke-width="1.0" {J}'))
    open(os.path.join(outdir, f"{mpn}-3d-outline-blueprint{suffix}.svg"), "w").write(svg(bp, vb, bg=None))
    print(f"OK: wrote 5 outline styles for {mpn} at axis={axis} (vis edges={len(H['vis'])}, name strokes={len(nm)})")

if __name__ == "__main__":
    main()