2023-05-11 23:03:47 +02:00
|
|
|
(* \chaptertitle{Shapes}{Stroking lines and making shapes} *)
|
|
|
|
|
|
|
|
(* This module provides for the stroking of lines, and production of shape
|
|
|
|
primitives (circles, regular polygons etc). *)
|
|
|
|
open Pdfutil
|
|
|
|
|
2023-06-13 15:07:34 +02:00
|
|
|
type fpoint = float * float
|
|
|
|
|
|
|
|
type winding_rule = EvenOdd | NonZero
|
|
|
|
|
|
|
|
type segment =
|
|
|
|
| Straight of fpoint * fpoint
|
|
|
|
| Bezier of fpoint * fpoint * fpoint * fpoint
|
|
|
|
|
|
|
|
(* Each segment list may be marked as a hole or not. *)
|
|
|
|
type hole = Hole | Not_hole
|
|
|
|
|
|
|
|
(* A [subpath] is either closed or open. *)
|
|
|
|
type closure = Closed | Open
|
|
|
|
|
|
|
|
(* A [subpath] is the pair of a hole and a list of segments. *)
|
|
|
|
type subpath = hole * closure * segment list
|
|
|
|
|
|
|
|
(* A path is made from a number of subpaths. *)
|
|
|
|
type path = winding_rule * subpath list
|
2023-05-11 23:03:47 +02:00
|
|
|
(* \section{Common geometric functions} *)
|
|
|
|
|
|
|
|
(* The factor by which we multiply the radius to find the length of the bezier
|
|
|
|
control lines when approximating quarter arcs to make semicircles and circles.
|
|
|
|
*)
|
|
|
|
let kappa = ((sqrt 2. -. 1.) /. 3.) *. 4.
|
|
|
|
|
|
|
|
(* Calculate rotation from [p] to [p'] about [c] with the shorter arc-length.
|
|
|
|
When arc-lengths are equal, the result may be either. *)
|
|
|
|
let rotation (cx, cy) (px, py) (px', py') =
|
|
|
|
let px = px -. cx and py = py -. cy
|
|
|
|
and px' = px' -. cx and py' = py' -. cy in
|
|
|
|
let a = px *. py' -. py *. px'
|
|
|
|
and b = px *. px' +. py *. py' in
|
|
|
|
atan2 a b
|
|
|
|
|
|
|
|
(* The absolute angle to a point [p] from a centre [c]. The angle is the
|
|
|
|
rotation clockwise (i.e the first quadrant encountered has positive [x] and [y]
|
|
|
|
values) from East. When the point is [(0, 0)], the result is [0].*)
|
|
|
|
let angle_to (cx, cy) (px, py) =
|
|
|
|
let r = atan2 (py -. cy) (px -. cx) in
|
|
|
|
if r < 0. then r +. 2. *. pi else r
|
|
|
|
|
|
|
|
(* Restrict an angle [a] to one of those at $s, 2s, 3s\ldots$. We find the two
|
|
|
|
candidate angles, and see which [a] is numerically closer to. The candidate
|
|
|
|
points are taken modulo $2\pi$ for this to work. *)
|
|
|
|
let restrict_angle s a =
|
|
|
|
let p = mod_float (floor (a /. s) *. s) (2. *. pi) in
|
|
|
|
let p' = mod_float (p +. s) (2. *. pi) in
|
|
|
|
if abs_float (p -. a) < abs_float (p' -. a) then p else p'
|
|
|
|
|
|
|
|
(* \section{Some Useful Shapes} *)
|
|
|
|
|
|
|
|
(* Make a quarter-circle from a single bezier curve from [s] to $(s + \pi / 2)
|
|
|
|
\bmod 2\pi$ with centre [c] and radius [r]. We cheat by making the standard
|
|
|
|
quarter from [(1, 0)] to [(0, 1)] and rotating using the [Transform] module.
|
|
|
|
*)
|
|
|
|
let quarter s (cx, cy) r =
|
|
|
|
let standard_quarter_points =
|
|
|
|
[(1., 0.); (1., kappa); (kappa, 1.); (0., 1.)]
|
|
|
|
and transform =
|
|
|
|
[Pdftransform.Translate(cx, cy);
|
|
|
|
Pdftransform.Scale((0., 0.), r, r);
|
|
|
|
Pdftransform.Rotate((0., 0.), s)]
|
|
|
|
in
|
|
|
|
match
|
|
|
|
map (Pdftransform.transform transform) standard_quarter_points
|
|
|
|
with
|
2023-06-13 15:07:34 +02:00
|
|
|
| [p; q; r; s] -> Bezier(p, q, r, s)
|
2023-05-11 23:03:47 +02:00
|
|
|
| _ -> raise (Pdf.PDFError ("Shapes.quarter: inconsistency"))
|
|
|
|
|
|
|
|
(* The anticlockwise variant. *)
|
|
|
|
let quarter_anticlockwise s c r =
|
|
|
|
match quarter s c r with
|
2023-06-13 15:07:34 +02:00
|
|
|
| Bezier(p, q, r, s) -> Bezier(s, r, q, p)
|
2023-05-11 23:03:47 +02:00
|
|
|
| _ -> raise (Pdf.PDFError "Shapes.quarter_anticlockwise: inconsistency")
|
|
|
|
|
|
|
|
(* Some of the following functions generate what is supposed to be a connected
|
|
|
|
list of segments. However, since they operate by calculating each segment
|
|
|
|
seperately, floating point inaccuracies can arise, making the end of one
|
|
|
|
segment misalign with the start of the next. This function corrects the defect
|
|
|
|
by copying the end of one segment to the beginning of the next. We only need to
|
|
|
|
deal with bezier segments for now. *)
|
|
|
|
let rec joinsegs segments =
|
|
|
|
match segments with
|
|
|
|
| [] -> []
|
|
|
|
| [x] -> [x]
|
2023-06-13 15:07:34 +02:00
|
|
|
| Bezier(_, _, _, d) as s::Bezier(_, b', c', d')::rest ->
|
|
|
|
s::joinsegs (Bezier(d, b', c', d')::rest)
|
2023-05-11 23:03:47 +02:00
|
|
|
| _ -> raise (Pdf.PDFError "PDFShapes.joinsegs: Segment not supported")
|
|
|
|
|
|
|
|
(* This version sets the start and end points to p1 and p2 respectively. Used
|
|
|
|
for ensuring round joins join correctly to the rails they connect *)
|
|
|
|
let joinsegs_ends p1 p2 segments =
|
|
|
|
match joinsegs segments with
|
|
|
|
| [] -> []
|
2023-06-13 15:07:34 +02:00
|
|
|
| [Bezier(a, b, c, d)] -> [Bezier(p1, b, c, p2)]
|
2023-05-11 23:03:47 +02:00
|
|
|
| segs ->
|
|
|
|
match extremes_and_middle segs with
|
2023-06-13 15:07:34 +02:00
|
|
|
| Bezier(_, b, c, d), m, Bezier(a', b', c', _) ->
|
|
|
|
Bezier(p1, b, c, d)::m @ [Bezier(a', b', c', p2)]
|
2023-05-11 23:03:47 +02:00
|
|
|
| _ -> raise (Pdf.PDFError "PDFShapes.joinsegs_ends: Segment not supported")
|
|
|
|
|
|
|
|
(* The shorter arc made from bezier curves from [p1] to [p2] with centre [c].
|
|
|
|
The arc is formed from zero or more quarter arcs rotated accordingly, and at
|
|
|
|
most one partial arc produced by truncating a quarter arc, again rotated. If
|
|
|
|
[p1=p2], no segments are produced. If the two curves defined by the arguments
|
|
|
|
are of equal length, the one chosen is undefined. *)
|
|
|
|
(*i let arc p1 p2 c =
|
|
|
|
let ninety = pi /. 2.
|
|
|
|
and angletogo = rotation c p1 p2 (*r signed angle to turn through *)
|
|
|
|
and abs_angle = angle_to c p1 (*r absolute angle to the first point *)
|
|
|
|
and r = distance_between p1 c in (*r radius of the resultant arc *)
|
|
|
|
let quarter, ninety_abs =
|
|
|
|
if angletogo > 0.
|
|
|
|
then quarter, ninety
|
|
|
|
else quarter_anticlockwise, ~-.ninety
|
|
|
|
in
|
|
|
|
let segments = ref []
|
|
|
|
and angletogo = ref (abs_float angletogo) (*r Have dealt with sign. *)
|
|
|
|
and abs_angle = ref abs_angle in
|
|
|
|
while !angletogo > 0. do
|
|
|
|
if !angletogo >= ninety then
|
|
|
|
begin
|
|
|
|
angletogo := !angletogo -. ninety;
|
|
|
|
segments := (quarter !abs_angle c r)::!segments;
|
|
|
|
abs_angle := mod_float (!abs_angle +. ninety_abs) (2. *. pi)
|
|
|
|
end
|
|
|
|
else
|
|
|
|
(* Calculate a partial arc to finish, if required. *)
|
|
|
|
if !angletogo > 0. then
|
|
|
|
begin
|
|
|
|
let q = quarter !abs_angle c r in
|
|
|
|
let portion_needed = !angletogo /. ninety in
|
|
|
|
let portion, _ = Polygon.bezier_split portion_needed q in
|
|
|
|
segments := portion::!segments;
|
|
|
|
angletogo := 0.
|
|
|
|
end;
|
|
|
|
done;
|
|
|
|
joinsegs_ends p1 p2 (rev !segments) i*)
|
|
|
|
|
|
|
|
(* Approximate a circle using four bezier curves.*)
|
|
|
|
let circle x y r =
|
2023-06-13 15:07:34 +02:00
|
|
|
NonZero,
|
|
|
|
[(Not_hole,
|
|
|
|
Closed,
|
2023-05-11 23:03:47 +02:00
|
|
|
joinsegs
|
|
|
|
[quarter 0. (x, y) r;
|
|
|
|
quarter (pi /. 2.) (x, y) r;
|
|
|
|
quarter pi (x, y) r;
|
|
|
|
quarter (3. *. pi /. 2.) (x, y) r ])]
|
|
|
|
|
|
|
|
let rectangle x y w h =
|
2023-06-13 15:07:34 +02:00
|
|
|
(EvenOdd,
|
|
|
|
([(Not_hole,
|
|
|
|
Closed,
|
|
|
|
[Straight ((x, y), (x +. w, y));
|
|
|
|
Straight ((x +. w, y), (x +. w, y +. h));
|
|
|
|
Straight ((x +. w, y +. h), (x, y +. h));
|
|
|
|
Straight ((x, y +. h), (x, y))])]))
|