#!/usr/bin/env python3
"""Manually extend a user-owned BoneShape v1.0.10 TTF to 82 glyphs.
Dependency: fontTools and shapely, installed offline or available locally.
Usage: python build-shape-font.py path/to/BoneShape-Regular.ttf output-directory
Does not install fonts into Windows. No font binaries are redistributed.
"""
from pathlib import Path
import sys, json, re, math
from fontTools.ttLib import TTFont
from fontTools.pens.ttGlyphPen import TTGlyphPen
from fontTools.pens.recordingPen import RecordingPen
from fontTools.svgLib.path import parse_path
from shapely.geometry import Polygon, LineString
from shapely.ops import unary_union
if len(sys.argv)!=3:
    raise SystemExit('Usage: python build-shape-font.py <existing BoneShape-Regular.ttf> <output-folder>')
registry_path=Path(__file__).resolve().parent.parent/'data/bone-shapes.json'
reg=json.loads(registry_path.read_text(encoding='utf-8'))
ui=[x for x in reg['shapes'] if x['category']=='ui-parts']
assert len(ui)==42 and len(reg['shapes'])==82
# Font generation from previous full TTF: keep old glyph table/metric/cmap for old 40 identical.
basefont=Path(sys.argv[1]);assert basefont.is_file(), f'Existing v1.0.10 TTF not found: {basefont}'
font=TTFont(basefont)
prev_map=font.getBestCmap().copy();assert len(prev_map)==40 and set(prev_map)==set(range(0xE001,0xE029))

# svg path to shapely coordinates via recording, contour flatten

def path_coords(d):
 p=RecordingPen()
 try: parse_path(d,p)
 except Exception:return []
 figures=[];curr=[];here=(0,0);start=(0,0)
 for op,args in p.value:
  if op=='moveTo':
   if curr: figures.append((curr,False))
   here=tuple(args[0]);start=here;curr=[here]
  elif op=='lineTo':
   for xy in args:
    here=tuple(xy);curr.append(here)
  elif op=='curveTo':
   points=[here]+[tuple(a) for a in args]
   if len(points)==4:
    for k in range(1,13):
     t=k/12.;q=1-t
     curr.append((q**3*points[0][0]+3*q*q*t*points[1][0]+3*q*t*t*points[2][0]+t**3*points[3][0],q**3*points[0][1]+3*q*q*t*points[1][1]+3*q*t*t*points[2][1]+t**3*points[3][1]))
    here=points[-1]
  elif op=='qCurveTo':
   pts=[here]+[tuple(a) for a in args if a is not None]
   if len(pts)>=3:
    a,b,c=pts[:3]
    for k in range(1,13):
     t=k/12;q=1-t
     curr.append((q*q*a[0]+2*q*t*b[0]+t*t*c[0],q*q*a[1]+2*q*t*b[1]+t*t*c[1]))
    here=c
  elif op=='closePath':
   if curr:figures.append((curr,True));curr=[];here=start
  elif op=='endPath':
   if curr:figures.append((curr,False));curr=[]
 if curr:figures.append((curr,False))
 return figures

def ellipse(cx,cy,rx,ry,n=32):return [(cx+rx*math.cos(2*math.pi*i/n),cy+ry*math.sin(2*math.pi*i/n)) for i in range(n)]
def make_geo(el):
 tag=el['tag'];attrs=el.get('attrs',{});klass=el.get('class','');
 if tag=='text': return [] # TTF is monochrome pictogram; editable text stays in SVG/html
 outline=klass in ('ui-body','ui-surface','ui-header','ui-line','ui-strong','ui-faint','ui-success','ui-accent-line','ui-inverse-line','ui-danger','ui-selected-outline')
 stroke=float(attrs.get('stroke-width', 2));
 if klass in ('ui-body','ui-surface','ui-header'): stroke=2.4
 if klass in ('ui-line','ui-faint'):stroke=2.5
 if klass in ('ui-strong','ui-success','ui-danger'):stroke=4.0
 figs=[]
 try:
  if tag=='rect':
   x=float(attrs.get('x',0));y=float(attrs.get('y',0));w=float(attrs.get('width',0));h=float(attrs.get('height',0))
   if w>0 and h>0:figs=[([(x,y),(x+w,y),(x+w,y+h),(x,y+h)],True)]
  elif tag=='circle':figs=[(ellipse(float(attrs.get('cx',0)),float(attrs.get('cy',0)),float(attrs['r']),float(attrs['r'])),True)]
  elif tag=='ellipse':figs=[(ellipse(float(attrs.get('cx',0)),float(attrs.get('cy',0)),float(attrs['rx']),float(attrs['ry'])),True)]
  elif tag=='line':figs=[([(float(attrs['x1']),float(attrs['y1'])),(float(attrs['x2']),float(attrs['y2']))],False)]
  elif tag in ('polyline','polygon'):
   arr=re.findall(r'[-+]?(?:\d*\.\d+|\d+)',str(attrs.get('points','')))
   figs=[([(float(arr[i]),float(arr[i+1])) for i in range(0,len(arr)-1,2)],tag=='polygon')]
  elif tag=='path':figs=path_coords(attrs.get('d',''))
 except Exception:pass
 outgeo=[]
 for pts,closed in figs:
  if len(pts)<2:continue
  try:
   geom=Polygon(pts) if closed and len(pts)>2 else LineString(pts)
   if not geom.is_valid:geom=geom.buffer(0)
   if geom.is_empty:continue
   if outline or not closed or tag=='line':
    geom=geom.boundary.buffer(stroke/2,cap_style='round',join_style='round') if isinstance(geom,Polygon) else geom.buffer(stroke/2,cap_style='round',join_style='round')
   outgeo.append(geom)
  except Exception:pass
 return outgeo

def contours(poly):
 if poly.geom_type=='Polygon':yield poly.exterior.coords
 elif poly.geom_type=='MultiPolygon':
  for item in poly.geoms:yield item.exterior.coords
 elif poly.geom_type=='GeometryCollection':
  for item in poly.geoms:yield from contours(item)
# Map glyphs with 110 units left/right, 700 units height, orientation flip
existing_order=list(font.getGlyphOrder());new_codes={}
for i,sh in enumerate(ui):
 code=0xE029+i;name='bf_'+sh['type'].lower();new_codes[code]=name
 assert code not in prev_map and name not in existing_order
 geos=[]
 for e in sh.get('elements',[]):geos+=make_geo(e)
 shape=unary_union(geos) if geos else None
 if shape is None or shape.is_empty: raise RuntimeError('Empty font glyph '+sh['type'])
 vb=sh['viewBox']; left,top,width,height=map(float,vb);scale=min(800/width,690/height);dx=500-width*scale/2;dy=(700-height*scale)/2
 pen=TTGlyphPen(None)
 for ring in contours(shape):
  pts=list(ring)
  if len(pts)<4:continue
  coords=[(int(round(dx+(x-left)*scale)),int(round(dy+(height-(y-top))*scale))) for x,y in pts[:-1]]
  cleaned=[]
  for pt in coords:
   if not cleaned or pt!=cleaned[-1]:cleaned.append(pt)
  if len(set(cleaned))<3:continue
  # glyph fill orientation (clockwise)
  pen.moveTo(cleaned[0])
  for pt in cleaned[1:]:pen.lineTo(pt)
  pen.closePath()
 font['glyf'][name]=pen.glyph()
 font['hmtx'].metrics[name]=(1000,0)
 existing_order.append(name)
font.setGlyphOrder(existing_order)
for table in font['cmap'].tables:
 if table.isUnicode():table.cmap.update(new_codes)
font['maxp'].numGlyphs=len(existing_order)
font['name'].setName('Bone Shape',1,3,1,0x409)
font['name'].setName('Regular',2,3,1,0x409)
font['name'].setName('BoneShape-Regular',6,3,1,0x409)
font['head'].modified=font['head'].modified+1
font['OS/2'].usFirstCharIndex=min(prev_map)
font['OS/2'].usLastCharIndex=max(new_codes)
out_dir=Path(sys.argv[2]);out_dir.mkdir(parents=True,exist_ok=True)
font_path=out_dir/'BoneShape-Regular.ttf';font.save(font_path)
font.flavor='woff2'; font.save(out_dir/'bone-shape.woff2');font.flavor=None
newfont=TTFont(font_path)
assert len(newfont.getBestCmap())==82 and all(newfont['glyf'][n].numberOfContours>0 for n in new_codes.values())
oldfont=TTFont(basefont)
assert all(newfont['glyf'][name].compile(newfont['glyf'])==oldfont['glyf'][name].compile(oldfont['glyf']) for name in prev_map.values())

print('FONT BUILD PASS: 82 glyphs; existing 40 glyph contours preserved.')
