"""Luna wearable hydraulic teaching concept. Not the commercial kit CAD.
Python: numpy, trimesh, manifold3d. All dimensions mm.
Hydraulic seals/cylinders are purchased components, never printed pressure parts.
The bracket dimensions are placeholders pending measurement of chosen syringes.
"""
from pathlib import Path
import argparse, json, hashlib
import numpy as np
import trimesh
from manifold3d import Manifold as M

parser=argparse.ArgumentParser()
parser.add_argument('--output',type=Path)
args=parser.parse_args()
root=Path(__file__).resolve().parents[1]
out=args.output or (root/'pages/luna-tech/mechanical-hand/wearable' if Path(__file__).parent.name=='scripts' else Path.cwd()/'wearable-output')
out.mkdir(parents=True,exist_ok=True)
def box(s,at=(0,0,0)):return M.cube(s,center=True).translate(at)
def cyl(r,h,at=(0,0,0),axis='x'):
    s=M.cylinder(h,r,circular_segments=40,center=True)
    if axis=='x':s=s.rotate((0,90,0))
    if axis=='y':s=s.rotate((90,0,0))
    return s.translate(at)
def holes(s,points,axis='z',r=1.7):
    for p in points:s=s-cyl(r,40,p,axis)
    return s
def rounded(w,h,d):
    s=box((w-12,h,d))+box((w,h-12,d))
    for x in [-w/2+6,w/2-6]:
        for y in [-h/2+6,h/2-6]:s=s+cyl(6,d,(x,y,0),'z')
    return s
def save(s,name,label,note):
    m=s.to_mesh();t=trimesh.Trimesh(vertices=np.asarray(m.vert_properties)[:,:3],faces=np.asarray(m.tri_verts),process=True)
    assert t.is_watertight and t.is_winding_consistent and t.volume>0 and len(t.split())==1,name
    p=out/(name+'.stl');p.write_bytes(t.export(file_type='stl'))
    views=[]
    for i,(v,a) in enumerate([('앞 X/Y',s),('옆 Z/Y',s.rotate((0,90,0))),('위 X/Z',s.rotate((90,0,0)))]):
        polys=a.project().to_polygons();pts=np.vstack(polys);lo=pts.min(0);hi=pts.max(0);size=hi-lo;scale=min(2.5,180/max(size[0],1),230/max(size[1],1));cx=125+i*235;cy=215
        path=' '.join('M'+' L'.join(f'{cx+x:.2f},{cy-y:.2f}' for x,y in (poly-(lo+hi)/2)*scale)+' Z' for poly in polys)
        views.append(f'<text x="{cx}" y="95" text-anchor="middle">{v}</text><path d="{path}" fill="#e5edf4" stroke="#314b72"/><text x="{cx}" y="360" text-anchor="middle">{size[0]:.1f} × {size[1]:.1f} mm</text>')
    p.with_suffix('.svg').write_text(f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 720 465"><rect width="720" height="465" fill="white"/><g font-family="sans-serif" font-size="12" fill="#243d59"><text x="20" y="30" font-size="19">LUNA · {label}</text><text x="20" y="55">착용형 구조 시제품 / 실제 STL 투영 / 단위 mm / 도면 축척 없음</text>{"".join(views)}<text x="20" y="396">{note}</text><text x="20" y="424">실물·착용 시험 전. 교사가 탁상 지그에서 먼저 검증. Luna Whale · CC BY 4.0</text></g></svg>')
    return dict(file='wearable/'+p.name,drawing='wearable/'+p.with_suffix('.svg').name,label=label,note=note,bounds=t.bounds.round(3).tolist(),sha256=hashlib.sha256(p.read_bytes()).hexdigest(),watertight=True,volumeMm3=round(t.volume,2))

parts={}
# Open center exposes the real hand; slotted rails carry a sliding palm support.
frame=box((12,218,10),(-63,17,-15))+box((12,218,10),(63,17,-15))+box((126,18,10),(0,-83,-15))+box((126,20,10),(0,116,-15))
frame=frame-box((4.4,122,20),(-63,0,-15))-box((4.4,122,20),(63,0,-15))
frame=holes(frame,[(x,116,-15) for x in [-42,0,42]])
for x in [-42,42]:frame=frame-box((20,5,25),(x,-83,-15))
parts['frame']=save(frame,'frame','착용 프레임','손목 끈 슬롯 20 × 5 / 손바닥 받침 레일 4.4 mm')
palm=rounded(108,63,8)
palm=holes(palm,[(-46,-20,0),(-46,20,0),(46,-20,0),(46,20,0)],r=2.2)
parts['palm-rest']=save(palm,'palm-rest','슬라이딩 손바닥 받침','폭 108 / 길이 63 / M4 고정 슬롯에 장착')
# A common mount with fork cheeks; 7.2 mm gap around the 6 mm tongue.
mount=box((32,32,8),(0,-6,-6))+cyl(10,7,(-10,0,0))+cyl(10,7,(10,0,0))
mount=holes(mount,[(0,0,0)],'x')
mount=mount-box((7.2,26,26),(0,8,0))
mount=holes(mount,[(-11,-14,-6),(11,-14,-6)])
parts['mount']=save(mount,'mount','大손가락 관절 받침','축 Ø3.4 / 가운데 귀 6 / 양쪽 간격 7.2 mm')
def link(L,tip=False):
    # Two side ribs, lower crossbars, tongue at proximal pivot.
    s=box((7,L-10,15),(-10,(L+2)/2,0))+box((7,L-10,15),(10,(L+2)/2,0))
    for y in [12,L-16]:s=s+box((27,10,15),(0,y,0))
    s=s+cyl(9,6)+box((6,14,15),(0,7,0))
    if tip:s=s+rounded(30,20,13).translate((0,L-5,0))
    else:
        s=s+cyl(9,7,(-10,L,0))+cyl(9,7,(10,L,0))
        s=s-box((7.2,24,24),(0,L,0))
    s=holes(s,[(0,0,0)],'x')
    if not tip:s=holes(s,[(0,L,0)],'x')
    s=holes(s,[(-10,L/2,0),(10,L/2,0)],'z',r=2)
    return s
for L in [58,72,90]:parts['link-'+str(L)]=save(link(L),'link-'+str(L),f'바깥 첫 마디 {L}',f'관절 중심 간 {L} / 축 Ø3.4 mm')
for L in [52,62,78]:parts['tip-'+str(L)]=save(link(L,True),'tip-'+str(L),f'바깥 손끝 {L}',f'축부터 끝까지 약 {L+5} / 축 Ø3.4 mm')
ring=(cyl(11.7,8,(0,32,0),'z')-cyl(8.5,14,(0,32,0),'z'))+box((9,25,8),(0,13,0))+cyl(7,8,(0,0,0),'z')
ring=ring-cyl(1.7,20,(0,0,0),'z')
parts['control-ring']=save(ring,'control-ring','사람 손가락 조작 고리','참고 안쪽 지름 17 / 실제 손 크기에 맞게 재설계 및 이탈 검증')
bracket=box((32,28,6),(0,0,-13))+(cyl(12,16,(0,0,0),'y')-cyl(8.5,22,(0,0,0),'y'))
bracket=bracket-box((30,35,14),(0,0,14))
bracket=holes(bracket,[(-12,0,-13),(12,0,-13)])
parts['cylinder-cradle']=save(bracket,'cylinder-cradle','구매 피스톤 받침','참고 내경 17 / 선택 주사기 외경 실측 후 재설계')
pad=rounded(42,24,6)
pad=holes(pad,[(-12,0,0),(12,0,0)],r=2)
parts['wide-pad']=save(pad,'wide-pad','넓은 손끝 패드 받침','폭 42 / 실제 접촉면에 교사 검토한 부드러운 패드 부착')
config=dict(schema=1,units='mm',status='wearable-concept-physical-untested',license='CC BY 4.0',parts=parts,hydraulics='Four purchased cylinder pairs and tubes. No printable fluid seals. Cradle dimension pending measurement.',notCommercialReplica=True)
(out/'catalog.json').write_text(json.dumps(config,ensure_ascii=False,indent=2)+'\n')
print(json.dumps(dict(parts=len(parts),bytes=sum(p.stat().st_size for p in out.glob('*.stl')))))
