123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144(* 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.
*)(* See Cad_edit.mli *)moduleG=Cad_geommoduleD=Cad_drawingtypept=G.ptletcurve=function|D.Line(a,b)->Some(G.Segment(a,b))|D.Circle(c,r)->Some(G.Circle(c,r))|D.Arc(c,r,a0,a1)->Some(G.Arc(c,r,a0,a1))|D.Insert_|D.Dimension_->Noneleteps=1e-6(*****************************************************************************)(* OFFSET *)(*****************************************************************************)letoffsetde~side=matchewith|D.Line(a,b)->letn=G.unit(letx,y=G.subbain(-.y,x))inletn=ifG.dot(G.subsidea)n>=0.thennelseG.scale(-1.)ninOk(D.Line(G.adda(G.scaledn),G.addb(G.scaledn)))|D.Circle(c,r)|D.Arc(c,r,_,_)->letr'=ifG.distcside<rthenr-.delser+.dinifr'<=0.thenError"Cannot offset that object."elseOk(matchewithD.Arc(_,_,a0,a1)->D.Arc(c,r',a0,a1)|_->D.Circle(c,r'))|D.Insert_|D.Dimension_->Error"Cannot offset that object."(*****************************************************************************)(* TRIM *)(*****************************************************************************)(* the cuts before and after [x] among [cuts] (all in (0, top)) *)letaroundcutsx=letbefore=List.fold_left(funmc->ifc<xthenFloat.maxmcelsem)neg_infinitycutsinletafter=List.fold_left(funmc->ifc>xthenFloat.minmcelsem)infinitycutsin(before,after)lettrimedgese~at=matchcurveewith|None->Error"Cannot trim this object."|Somec->(letcrossings=List.concat_map(funedge->G.intersectionscedge)edgesinmatch(e,c)with|D.Line(a,b),_->letcuts=List.filter(funt->t>eps&&t<1.-.eps)(List.map(G.paramab)crossings)inifcuts=[]thenError"Object does not intersect an edge."elseletlo,hi=aroundcuts(G.paramabat)inletpointt=G.adda(G.scalet(G.subba))inOk((iflo>neg_infinitythen[D.Line(a,pointlo)]else[])@ifhi<infinitythen[D.Line(pointhi,b)]else[])|D.Circle(center,r),_->letcuts=List.sort_uniqcompare(List.map(G.anglecenter)crossings)inifList.lengthcuts<2thenError"Circle must intersect twice."else(* the angles turned to start just past the click *)letfrom=G.anglecenteratinletrela=G.norm_angle(a-.from)inletnext=List.fold_left(funma->ifrela<relmthenaelsem)(List.hdcuts)cutsinletprev=List.fold_left(funma->ifrela>relmthenaelsem)(List.hdcuts)cutsin(* what stays goes on from the cut after the click round to
the one before it *)Ok[D.Arc(center,r,next,prev)]|D.Arc(center,r,a0,a1),_->letspan=G.norm_angle(a1-.a0)inletoffa=G.norm_angle(a-.a0)inletcuts=List.filter(funx->x>eps&&x<span-.eps)(List.map(funp->off(G.anglecenterp))crossings)inifcuts=[]thenError"Object does not intersect an edge."elseletlo,hi=aroundcuts(off(G.anglecenterat))inOk((iflo>neg_infinitythen[D.Arc(center,r,a0,G.norm_angle(a0+.lo))]else[])@ifhi<infinitythen[D.Arc(center,r,G.norm_angle(a0+.hi),a1)]else[])|_->Error"Cannot trim this object.")(*****************************************************************************)(* EXTEND *)(*****************************************************************************)letextendedgese~at=matchewith|D.Line(a,b)->((* the end nearer the click moves: from the other one, along *)letfixed,moving=ifG.distata<G.distatbthen(b,a)else(a,b)inletcarrier=G.Segment(fixed,moving)inlethits=List.concat_map(funedge->List.filter(funp->G.on_pieceedgep)(G.carrier_intersectionscarrieredge))edges|>List.map(G.paramfixedmoving)|>List.filter(funt->t>1.+.eps)inmatchhitswith|[]->Error"No edge in that direction."|t::ts->lett=List.fold_leftFloat.minttsinletp=G.addfixed(G.scalet(G.submovingfixed))inOk(ifmoving=athenD.Line(p,b)elseD.Line(a,p)))|_->Error"Cannot extend this object."(*****************************************************************************)(* FILLET *)(*****************************************************************************)letfilletr(e1,q1)(e2,q2)=match(e1,e2)with|D.Line(a1,b1),D.Line(a2,b2)->(matchG.carrier_intersections(G.Segment(a1,b1))(G.Segment(a2,b2))with|[]->Error"Lines are parallel."|p::_->(* along each line, towards the part clicked, and that part's
far end *)letsideabq=letu=G.unit(G.sub(G.foot(G.Segment(a,b))q)p)inletfar=ifG.dot(G.subap)u>=G.dot(G.subbp)uthenaelsebin(u,far)inletu1,f1=sidea1b1q1andu2,f2=sidea2b2q2inifr<=0.thenOk(D.Line(f1,p),D.Line(f2,p),None)elselettheta=Float.acos(Float.max(-1.)(Float.min1.(G.dotu1u2)))inletd=r/.Float.tan(theta/.2.)inifd>G.distpf1||d>G.distpf2thenError"Radius is too large."elselett1=G.addp(G.scaledu1)andt2=G.addp(G.scaledu2)inletc=G.addp(G.scale(r/.Float.sin(theta/.2.))(G.unit(G.addu1u2)))inleta1=G.anglect1anda2=G.anglect2in(* the short way round, counterclockwise *)letarc=ifG.norm_angle(a2-.a1)<=180.thenD.Arc(c,r,a1,a2)elseD.Arc(c,r,a2,a1)inOk(D.Line(f1,t1),D.Line(f2,t2),Somearc))|_->Error"Can only fillet lines."