# Worked example — cap emboss + 2-material (plane-split) colour, swapped into a composite (0603B104J500CT).
# ADAPT paths. Companion to build_insertion_glb-style board/animation assembly.

"""
v2 emboss + clean swap:
  - PART NUMBER  0603B104J500CT  → sunken into the TOP (+Z), brown region only
  - VALUE        100nF           → sunken into a long SIDE (+Y), brown region only, LARGE
  - silver/brown split at |x| >= 0.5 mm (the REAL termination boundary from the
    original chip mesh) → no triangular color artifacts
  - text kept within |x| <= 0.46 mm so it never touches the silver terminations
"""
import struct, json
import numpy as np
import cadquery as cq
import pygltflib

STEP_IN   = "/tmp/claude-1001/-home-adom-project/7572280e-1088-44ed-9829-35fccaaabecb/scratchpad/wiki-fresh-0603b104j500ct/0603B104J500CT.step"
BACKUP    = "/tmp/claude-1001/-home-adom-project/7572280e-1088-44ed-9829-35fccaaabecb/scratchpad/wiki-work-0603b104j500ct/_backup-working-composite.glb"
OUT       = "/tmp/claude-1001/-home-adom-project/7572280e-1088-44ed-9829-35fccaaabecb/scratchpad/wiki-work-0603b104j500ct/0603B104J500CT.glb"
INSERTION = "/tmp/claude-1001/-home-adom-project/7572280e-1088-44ed-9829-35fccaaabecb/scratchpad/wiki-work-0603b104j500ct/0603B104J500CT.insertion.glb"

DEPTH = 0.06
FONT, FONT_PATH = "DejaVu Sans", "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf"
ORIG_GLB  = "/tmp/claude-1001/-home-adom-project/7572280e-1088-44ed-9829-35fccaaabecb/scratchpad/wiki-fresh-0603b104j500ct/0603B104J500CT.glb"
TOL, ANG = 0.02, 0.35    # tessellation

# ---- Reference colours straight from the actual STEP (via its color-preserving GLB) ----
def load_ref():
    f=open(ORIG_GLB,'rb'); f.read(12); ln=struct.unpack('<I',f.read(4))[0]; f.read(4)
    j=json.loads(f.read(ln)); bl=struct.unpack('<I',f.read(4))[0]; f.read(4); bd=f.read(bl)
    CT={5123:'H',5125:'I',5126:'f'}; SZ={5123:2,5125:4,5126:4}; NC={'SCALAR':1,'VEC3':3}
    def acc(i):
        a=j['accessors'][i]; bv=j['bufferViews'][a['bufferView']]
        off=bv.get('byteOffset',0)+a.get('byteOffset',0); n=a['count']*NC[a['type']]
        v=struct.unpack(f'<{n}'+CT[a['componentType']], bd[off:off+n*SZ[a['componentType']]])
        return np.array(v).reshape(a['count'],NC[a['type']])
    cents=[]; silver=[]
    silver_mat = [m['name'] for m in j['materials']].index('mat_0')
    for pr in j['meshes'][0]['primitives']:
        pos=acc(pr['attributes']['POSITION']); idx=acc(pr['indices']).reshape(-1,3)
        c=pos[idx].mean(axis=1)
        cents.append(c); silver.append(np.full(len(c), pr.get('material')==silver_mat))
    return np.vstack(cents).astype(np.float32), np.concatenate(silver)
REF_C, REF_SILVER = load_ref()
print(f"ref mesh: {len(REF_C)} tris ({REF_SILVER.sum()} silver / {(~REF_SILVER).sum()} brown)")

body = cq.importers.importStep(STEP_IN)

# ---- PART NUMBER on TOP (+Z), reading world -X + letter-tops toward -Y so it
#      reads upright/consistent with the side value when the +Y face is toward you ----
pn = (cq.Workplane("XY").workplane(offset=0.8)
      .text("0603B104J500CT", 0.10, -DEPTH, font=FONT, fontPath=FONT_PATH,
            halign="center", valign="center", combine=False)
      .rotate((0,0,0),(0,0,1), 180))

# ---- VALUE on +Y SIDE, LARGE, reads correctly when viewed from +Y ----
# Plane on the +Y face: local +X = world -X (so a +Y viewer reads left→right),
# local +Y = world +Z (upright), normal +Y; extrude -DEPTH cuts into the body.
side_plane = cq.Plane(origin=(0, 0.40, 0.40), xDir=(-1, 0, 0), normal=(0, 1, 0))
val = (cq.Workplane(side_plane)
       .text("100nF", 0.28, -DEPTH, font=FONT, fontPath=FONT_PATH,
             halign="center", valign="center", combine=False))

for nm, t in (("PN(top)", pn), ("100nF(side)", val)):
    bb = t.val().BoundingBox()
    print(f"  {nm:12s} X[{bb.xmin:.3f},{bb.xmax:.3f}] Y[{bb.ymin:.3f},{bb.ymax:.3f}] Z[{bb.zmin:.3f},{bb.zmax:.3f}]")

embossed = body.cut(pn).cut(val)

# ---- tessellate + flat normals ----
vmm, tris = embossed.val().tessellate(TOL, ANG)
P = np.array([[v.x,v.y,v.z] for v in vmm]) * 0.001
tris = np.array(tris, dtype=np.int64)
tp = P[tris]
fn = np.cross(tp[:,1]-tp[:,0], tp[:,2]-tp[:,0]); L=np.linalg.norm(fn,axis=1,keepdims=True); L[L==0]=1; fn/=L
V = tp.reshape(-1,3).astype(np.float32); N = np.repeat(fn,3,axis=0).astype(np.float32)
# The actual STEP's silver/brown boundary is a clean plane at |x| = 0.5 mm (verified
# from the reference mesh: silver tris all |x|>=0.5, brown all <=0.5). Split there.
TERM_X = float(np.abs(REF_C[REF_SILVER][:,0]).min())   # inner edge of silver in the real STEP (m)
print(f"real silver inner edge |x| = {TERM_X*1000:.4f} mm  (brown reaches {np.abs(REF_C[~REF_SILVER][:,0]).max()*1000:.4f} mm)")
cx = tp.mean(axis=1)[:,0]
term = np.abs(cx) >= TERM_X - 1e-6
ids = np.arange(len(tris)*3).reshape(-1,3)
term_idx = ids[term].reshape(-1).astype(np.uint32)
body_idx = ids[~term].reshape(-1).astype(np.uint32)
print(f"tessellated {len(tris)} tris → silver {term.sum()} / brown {(~term).sum()} (clean split at STEP boundary)")

# ---- splice into composite (2 prims: silver mat_0, brown mat_1) ----
def swap(path_in, path_out):
    g = pygltflib.GLTF2().load(path_in); blob = bytearray(g.binary_blob())
    chip = next(n for n in g.nodes if n.name=="0603B104J500CT"); mesh = g.meshes[chip.mesh]
    names=[m.name for m in g.materials]; ms=names.index("mat_0"); mb=names.index("mat_1")
    def av(d,t):
        nonlocal blob
        while len(blob)%4: blob+=b"\x00"
        o=len(blob); blob+=d; g.bufferViews.append(pygltflib.BufferView(buffer=0,byteOffset=o,byteLength=len(d),target=t)); return len(g.bufferViews)-1
    pbv=av(V.tobytes(),34962); nbv=av(N.tobytes(),34962)
    pa=len(g.accessors); g.accessors.append(pygltflib.Accessor(bufferView=pbv,componentType=5126,count=len(V),type="VEC3",min=V.min(0).tolist(),max=V.max(0).tolist()))
    na=len(g.accessors); g.accessors.append(pygltflib.Accessor(bufferView=nbv,componentType=5126,count=len(N),type="VEC3"))
    def ia(a):
        bv=av(a.tobytes(),34963); i=len(g.accessors); g.accessors.append(pygltflib.Accessor(bufferView=bv,componentType=5125,count=len(a),type="SCALAR",min=[int(a.min())],max=[int(a.max())])); return i
    ta,ba=ia(term_idx),ia(body_idx)
    mesh.primitives=[
        pygltflib.Primitive(attributes=pygltflib.Attributes(POSITION=pa,NORMAL=na),indices=ta,material=ms,mode=4),
        pygltflib.Primitive(attributes=pygltflib.Attributes(POSITION=pa,NORMAL=na),indices=ba,material=mb,mode=4)]
    g.buffers[0].byteLength=len(blob); g.set_binary_blob(bytes(blob)); g.save(path_out)
    print(f"OK: {path_out} ({len(blob):,} B)")
swap(BACKUP, OUT)
import shutil; shutil.copy(OUT, INSERTION); print("mirrored → insertion.glb")
