123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130(* Claude Code
*
* Copyright (C) 2026 Yoann Padioleau
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Library General Public License
* (LGPL) as published by the Free Software Foundation; either version
* 2 of the License, or (at your option) any later version.
*)typepoint=float*floattypebox={x0:float;y0:float;x1:float;y1:float}letbox(ax,ay)(bx,by)={x0=Float.minaxbx;y0=Float.minayby;x1=Float.maxaxbx;y1=Float.maxayby}typestyle={fill:floatoption;pen:float}typet=|Lineofpoint*point*style|Rectofbox*style|Ovalofbox*style|Textofbox*string*float|Groupoftlistletunionab={x0=Float.mina.x0b.x0;y0=Float.mina.y0b.y0;x1=Float.maxa.x1b.x1;y1=Float.maxa.y1b.y1}letrecbounds=function|Line(a,b,_)->boxab|Rect(b,_)|Oval(b,_)|Text(b,_,_)->b|Group[]->{x0=0.;y0=0.;x1=0.;y1=0.}|Group(f::rest)->List.fold_left(funaccf->unionacc(boundsf))(boundsf)rest(*****************************************************************************)(* Hit testing *)(*****************************************************************************)(* how far a point is from the segment a-b: from its nearest point *)letdistance_to_segment(px,py)(ax,ay)(bx,by)=letdx=bx-.axanddy=by-.ayinletlen2=(dx*.dx)+.(dy*.dy)inlett=iflen2=0.then0.elseFloat.max0.(Float.min1.((((px-.ax)*.dx)+.((py-.ay)*.dy))/.len2))inletnx=ax+.(t*.dx)andny=ay+.(t*.dy)inFloat.sqrt(((px-.nx)**2.)+.((py-.ny)**2.))letinsideb(x,y)=x>=b.x0&&x<=b.x1&&y>=b.y0&&y<=b.y1letrechit~tolerancef((x,y)asp)=matchfwith|Line(a,b,s)->distance_to_segmentpab<=tolerance+.(s.pen/.2.)|Text(b,_,_)->insidebp|Rect(b,s)->(matchs.fillwith|Some_->insidebp|None->(* near one of the four sides *)letcorners=[(b.x0,b.y0);(b.x1,b.y0);(b.x1,b.y1);(b.x0,b.y1)]inletsides=List.combinecorners(List.tlcorners@[List.hdcorners])inList.exists(fun(a,c)->distance_to_segmentpac<=tolerance+.(s.pen/.2.))sides)|Oval(b,s)->(leta=(b.x1-.b.x0)/.2.andr=(b.y1-.b.y0)/.2.inifa<=0.||r<=0.thenfalseelse(* how far out the point is, in radii: 1 on the ellipse *)letu=(x-.(b.x0+.a))/.aandv=(y-.(b.y0+.r))/.rinletk=Float.sqrt((u*.u)+.(v*.v))inmatchs.fillwith|Some_->k<=1.|None->Float.abs(k-.1.)*.Float.minar<=tolerance+.(s.pen/.2.))|Groupfs->List.exists(funf->hit~tolerancefp)fs(*****************************************************************************)(* Moving and resizing: maps of the points *)(*****************************************************************************)letrecmap_pointsm=function|Line(a,b,s)->Line(ma,mb,s)|Rect(b,s)->Rect(box(m(b.x0,b.y0))(m(b.x1,b.y1)),s)|Oval(b,s)->Oval(box(m(b.x0,b.y0))(m(b.x1,b.y1)),s)|Text(b,text,size)->(* text keeps its size: only its place moves *)letx,y=m(b.x0,b.y1)inText({x0=x;y1=y;x1=x+.(b.x1-.b.x0);y0=y-.(b.y1-.b.y0)},text,size)|Groupfs->Group(List.map(map_pointsm)fs)lettranslatedxdy=map_points(fun(x,y)->(x+.dx,y+.dy))letfitnbf=letb=boundsfinletscalelohinlonhiv=ifhi=lothennloelsenlo+.((v-.lo)*.(nhi-.nlo)/.(hi-.lo))inmap_points(fun(x,y)->(scaleb.x0b.x1nb.x0nb.x1x,scaleb.y0b.y1nb.y0nb.y1y))f(*****************************************************************************)(* Handles *)(*****************************************************************************)lethandles=function|Line(a,b,_)->[a;b]|f->letb=boundsfinletmx=(b.x0+.b.x1)/.2.andmy=(b.y0+.b.y1)/.2.in[(b.x0,b.y1);(b.x1,b.y1);(b.x1,b.y0);(b.x0,b.y0);(mx,b.y1);(b.x1,my);(mx,b.y0);(b.x0,my)]letdrag_handlefi(x,y)=matchfwith|Line(a,b,s)->ifi=0thenLine((x,y),b,s)elseLine(a,(x,y),s)|f->letb=boundsfin(* which sides the handle carries: left/right, top/bottom *)letl,r,t,bo=matchiwith|0->(Somex,None,Somey,None)|1->(None,Somex,Somey,None)|2->(None,Somex,None,Somey)|3->(Somex,None,None,Somey)|4->(None,None,Somey,None)|5->(None,Somex,None,None)|6->(None,None,None,Somey)|_->(Somex,None,None,None)inletvod=Option.valueo~default:din(* the corners it ends with, in whichever order: [box] turns a
box dragged past its opposite side over *)fit(box(vlb.x0,vbob.y0)(vrb.x1,vtb.y1))fletrecrestyleg=function|Line(a,b,s)->Line(a,b,gs)|Rect(b,s)->Rect(b,gs)|Oval(b,s)->Oval(b,gs)|Text_ast->t|Groupfs->Group(List.map(restyleg)fs)