(* \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

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
(* \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
    | [p; q; r; s] -> Bezier(p, q, r, s)
    | _ -> raise (Pdf.PDFError ("Shapes.quarter: inconsistency"))

(* The anticlockwise variant. *)
let quarter_anticlockwise s c r =
  match quarter s c r with
  | Bezier(p, q, r, s) -> Bezier(s, r, q, p)
  | _ -> 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]
  | Bezier(_, _, _, d) as s::Bezier(_, b', c', d')::rest ->
      s::joinsegs (Bezier(d, b', c', d')::rest)
  | _ -> 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
  | [] -> []
  | [Bezier(a, b, c, d)] -> [Bezier(p1, b, c, p2)]
  | segs ->
    match extremes_and_middle segs with
    | Bezier(_, b, c, d), m, Bezier(a', b', c', _) ->
        Bezier(p1, b, c, d)::m @ [Bezier(a', b', c', p2)]
    | _ -> 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 =
  NonZero,
    [(Not_hole,
      Closed,
     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 =
  (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))])]))
