123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275(* 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 Fill.mli for the idea and the references *)typefill_rule=Nonzero|Even_odd(*****************************************************************************)(* Edges *)(*****************************************************************************)(* An edge of the polygon, from the point of view of the rows it
* crosses. Each row y is sampled at the height of its pixel centers,
* y + 0.5, so an edge from height top to height bottom crosses the rows
* whose center is in [top, bottom): from row ceil(top - 0.5) included
* to row ceil(bottom - 0.5) excluded. For example an edge from y = 1.2
* to y = 3.9 crosses rows 1, 2, 3 (centers 1.5, 2.5, 3.5).
*
* That the range is half-open matters at the corners: where two edges
* meet, at a height that happens to be a row's center, exactly one of
* the two counts the crossing. Counting it twice or zero times would
* flip inside/outside for the rest of that row. *)typeedge={first_row:int;end_row:int;(* excluded *)(* x where the edge crosses the current row (starting at first_row) *)mutablex:float;(* how much x moves from one row to the next: dx/dy *)slope:float;(* +1 if the edge goes down (y increasing), -1 if it goes up *)winding:int;}(* The first pixel, going right (or down), whose center is at or after
* the coordinate [v]: pixel n spans [n, n+1), its center is n + 0.5,
* so e.g. first_pixel 1.2 = 1 (center 1.5) and first_pixel 1.7 = 2 *)letfirst_pixel(v:float):int=int_of_float(Float.ceil(v-.0.5))(* The edge from (x0, y0) to (x1, y1), clipped to the rows [0, height);
* None if it crosses no row: horizontal edges, for instance, never
* cross a row, the edges before and after them do. *)letmake_edge~height(x0,y0)(x1,y1):edgeoption=letwinding=ify1>y0then1else-1in(* work from top to bottom whatever the edge's direction *)let(xt,yt),(_xb,yb)=ify0<y1then((x0,y0),(x1,y1))else((x1,y1),(x0,y0))inletfirst_row=max0(first_pixelyt)inletend_row=minheight(first_pixelyb)iniffirst_row>=end_rowthenNoneelseletslope=(x1-.x0)/.(y1-.y0)in(* x at the center of first_row, on the line through the two points *)letx=xt+.((floatfirst_row+.0.5-.yt)*.slope)inSome{first_row;end_row;x;slope;winding}(* The polygon's edges: from each point to the next, and from the last
* back to the first *)letedges_of_polygon~height(points:(float*float)list):edgelist=matchpointswith|[]->[]|first::_->letrecloopacc=function|p::(q::_asrest)->loop(make_edge~heightpq::acc)rest|[last]->make_edge~heightlastfirst::acc|[]->accinList.filter_mapFun.id(loop[]points)(*****************************************************************************)(* Scanlines *)(*****************************************************************************)letis_inside(rule:fill_rule)(winding:int):bool=matchrulewith|Nonzero->winding<>0(* the parity of the winding number is the parity of the number of
* crossings, since each crossing adds or removes 1 *)|Even_odd->windingland1=1(* The spans of one row, given the edges crossing it sorted by x: walk
* from left to right keeping the winding number of the current
* position; each time we go from outside to inside, a span starts, and
* it ends when we go back outside. For the "U" in Fill.mli with edges
* going down on the left and up on the right, the winding number goes
* 0 -> 1 -> 0 -> 1 -> 0 at x = 1, 3, 7, 9, giving the spans [1, 3) and
* [7, 9). *)letspans_of_row~rule(crossings:edgelist):(float*float)list=let_winding,_span_start,spans=List.fold_left(fun(winding,span_start,spans)(e:edge)->letwinding'=winding+e.windinginmatch(is_insiderulewinding,is_insiderulewinding')with|false,true->(winding',e.x,spans)|true,false->(winding',e.x,(span_start,e.x)::spans)|_->(winding',span_start,spans))(0,0.,[])crossingsinList.revspansletscan?(rule=Nonzero)~height(contours:(float*float)listlist)~on_span=(* the "edge table": all the edges, by the row where they start *)letedges=List.concat(List.map(edges_of_polygon~height)contours)|>List.sort(fun(e1:edge)e2->comparee1.first_rowe2.first_row)inmatchedgeswith|[]->()|first::_->letlast_row=List.fold_left(funacc(e:edge)->maxacce.end_row)0edgesin(* going down row by row, [active] is the "active edge list", the
* edges crossing the current row, and [pending] the edges below
* it, not reached yet *)letrecloopyactivepending=ify<last_rowthenbeginletstarting,pending=List.partition(fun(e:edge)->e.first_row=y)pendinginletactive=List.filter(fun(e:edge)->e.end_row>y)(starting@active)inletcrossings=List.sort(fun(e1:edge)e2->comparee1.xe2.x)activeinList.iter(fun(xa,xb)->on_span~yxaxb)(spans_of_row~rulecrossings);(* edge coherence: on the next row, each edge's crossing is
* [slope] further, no need to intersect lines again *)List.iter(fun(e:edge)->e.x<-e.x+.e.slope)active;loop(y+1)activependingendinloopfirst.first_row[]edges(*****************************************************************************)(* Filling: pixel centers *)(*****************************************************************************)(* A span from xa to xb covers the pixels whose center is in [xa, xb),
* the same rule as for rows *)letpolygons?rule(fb:Framebuffer.t)contours~rgb~alpha=scan?rule~height:fb.heightcontours~on_span:(fun~yxaxb->Framebuffer.fill_spanfb~y~x0:(first_pixelxa)~x1:(first_pixelxb)~rgb~alpha)letpolygon?rulefbpoints~rgb~alpha=polygons?rulefb[points]~rgb~alpha(*****************************************************************************)(* Filling with antialiasing: pixel coverage *)(*****************************************************************************)(* How much of pixel x the span [xa, xb) covers horizontally, from 0 to
* 1; e.g. [0.5, 2.5) covers half of pixel 0, all of pixel 1, and half
* of pixel 2 *)letoverlap(xa,xb)x=Float.max0.(Float.minxb(float(x+1))-.Float.maxxa(floatx))(* The original, simple version: a coverage array for the current pixel
* row, where each sub-row's span adds its overlap to every pixel it
* touches, one by one; then each pixel is plotted with its coverage.
* Easy to follow, but a span across a 1000-pixel-wide window costs
* 1000 additions per sub-row, and 1000 plots per row. *)letpolygons_aa_simple?rule?(subrows=4)(fb:Framebuffer.t)contours~rgb~alpha=letn=floatsubrowsinletcoverage=Array.makefb.width0.inletrow=ref(-1)inletflush()=if!row>=0thenforx=0tofb.width-1doifcoverage.(x)>0.thenbeginFramebuffer.plotfb~x~y:!row~rgb~alpha:(alpha*.Float.min1.coverage.(x));coverage.(x)<-0.enddoneinletstretched=List.map(List.map(fun(x,y)->(x,y*.n)))contoursinscan?rule~height:(fb.height*subrows)stretched~on_span:(fun~y:subrowxaxb->lety=subrow/subrowsinify<>!rowthenbeginflush();row:=yend;forx=max0(int_of_float(Float.floorxa))tomin(fb.width-1)(int_of_float(Float.ceilxb)-1)docoverage.(x)<-coverage.(x)+.(overlap(xa,xb)x/.n)done);flush()(* claude: optimization (Opti.enabled), what polygons_aa does instead.
*
* Coverage is accumulated per pixel row, over its sub-rows, as a list
* of "cells", so that adding a span costs the same whatever its length.
* A cell is a pixel x where something changes:
*
* - [partial]: coverage of pixel x alone, for the span's two end
* pixels, e.g. [0.5, 2.5) gives 0.5 to pixels 0 and 2 (divided by the
* number of sub-rows);
* - [step]: full coverage starting (+1) or stopping (-1) at x, for the
* pixels in between: [0.5, 2.5) gives +1 at x = 1 and -1 at x = 2.
*
* At the end of the row, walking the cells from left to right with a
* running sum of the steps gives every pixel's coverage: [partial] for
* the cells' pixels, plus the running sum, which stays the same between
* two cells -- so everything between two cells is one span of one
* coverage, e.g. the 800 fully covered pixels inside a big rectangle.
* A row costs a few cells per span, not one visit per pixel.
*
* (The "difference array" trick, kept sparse; the same idea as the cell
* lists of libart and Anti-Grain Geometry, and font-rs's accumulation
* buffer.) *)typecell={x:int;partial:float;step:float}letadd_span(cells:celllistref)~weight~widthxaxb=letxa=Float.max0.xaandxb=Float.min(floatwidth)xbinifxa<xbthenbeginletaddx~partial~step=cells:={x;partial;step}::!cellsinletia=int_of_float(Float.floorxa)andib=int_of_float(Float.floorxb)inifia=ibthen(* the whole span within one pixel *)addia~partial:((xb-.xa)*.weight)~step:0.elsebegin(* the left end pixel, from xa to its right side *)addia~partial:((float(ia+1)-.xa)*.weight)~step:0.;(* the right end pixel, from its left side to xb *)ifib<widththenaddib~partial:((xb-.floatib)*.weight)~step:0.;(* everything in between, fully *)add(ia+1)~partial:0.~step:weight;addib~partial:0.~step:(-.weight)endend(* Paint row y from its cells, left to right *)letpaint_row(fb:Framebuffer.t)(cells:celllist)~y~rgb~alpha=letpaintx0x1coverage=ifx0<x1&&coverage>0.thenFramebuffer.fill_spanfb~y~x0~x1~rgb~alpha:(alpha*.Float.min1.coverage)in(* [full]: the running sum of steps; pixels from [next] on haven't been
* painted yet *)letrecwalkfullnext=function|[]->()|{x;_}::_ascells->(* all the cells of pixel x together *)lethere,rest=List.partition(func->c.x=x)cellsinletpartial=List.fold_left(funaccc->acc+.c.partial)0.hereinletstep=List.fold_left(funaccc->acc+.c.step)0.herein(* up to x, nothing changed: one span *)paintnextxfull;letfull=full+.stepinifpartial<>0.thenbeginpaintx(x+1)(partial+.full);walkfull(x+1)restendelsewalkfullxrestinwalk0.0(List.sort(func1c2->comparec1.xc2.x)cells)letpolygons_aa_sparse?rule?(subrows=4)(fb:Framebuffer.t)contours~rgb~alpha=letcells=ref[]inletweight=1./.floatsubrowsinletrow=ref(-1)inletflush()=if!row>=0thenpaint_rowfb!cells~y:!row~rgb~alpha;cells:=[]in(* the polygon stretched [subrows] times vertically: its rows are our
* sub-rows, sampled at (k + 0.5) / subrows within each pixel row *)letstretched=List.map(List.map(fun(x,y)->(x,y*.floatsubrows)))contoursinscan?rule~height:(fb.height*subrows)stretched~on_span:(fun~y:subrowxaxb->lety=subrow/subrowsinify<>!rowthenbeginflush();row:=yend;add_spancells~weight~width:fb.widthxaxb);flush()letpolygons_aa?rule?subrowsfbcontours~rgb~alpha=if!Opti.enabledthenpolygons_aa_sparse?rule?subrowsfbcontours~rgb~alphaelsepolygons_aa_simple?rule?subrowsfbcontours~rgb~alpha