aboutsummaryrefslogtreecommitdiff
path: root/path/fixed.ml
blob: 1362ad320d96fa4348d81f29e47298af0dbc11ca (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
open StdLabels

(** Signature for points *)
module type P = sig
  type t

  val get_coord : t -> Gg.v2

  val id : t -> int

  val copy : t -> Gg.v2 -> t

end

module Make(Point:P) = struct

  type bezier =
    { ctrl0:Gg.v2   (* The control point *)
    ; ctrl1:Gg.v2   (* The control point *)
    ; p1:Point.t    (* The end point *)
    }

  module type BUILDER = sig
    type t

    val repr
      : t -> (module Repr.M with type point = Point.t and type t = 's) -> 's -> 's
  end

  type path =
    | Line of Point.t
    | Curve of bezier


  type step =
    { point : Point.t
    ; move : path
    }

  type t = step array

  module ToFixed = struct
    type point = Point.t

    type t = int * step list

    let create_path () = 0, []

    (* Start a new path. *)
    let start point t =
      let _ = point in
      t

    let line_to
      : point -> point -> t -> t
      = fun p1 p2 (i, t) ->
        ( i + 1
        , { point = p1
          ; move = Line p2
          }:: t )

    let quadratic_to
      : (point * Gg.v2 * Gg.v2 * point) -> t -> t
      = fun (p0,  ctrl0, ctrl1, p1) (i, t) ->
        let curve = Curve
            { ctrl0
            ; ctrl1
            ; p1} in
        ( i + 1
        , { point = p0
          ; move = curve
          } ::t)

    let stop t = t

    let get
      : int * step list -> step array
      = fun (n, t) ->

        (* The array is initialized with a magic number, and just after
           filled with the values from the list in reverse. All the elements are set.
        *)
        let res = Obj.magic (Array.make n 0) in
        List.iteri t
          ~f:(fun i elem -> Array.set res (n - i - 1) elem );
        res
  end

  let to_fixed
    : (module BUILDER with type t = 'a) -> 'a -> t
    = fun (type s) (module Builder: BUILDER with type t = s) t ->
      Builder.repr t (module ToFixed) (ToFixed.create_path ())
      |> ToFixed.get

  let repr
    : t -> (module Repr.M with type point = Point.t and type t = 's) -> 's -> 's
    = fun (type s) t (module Repr : Repr.M with type point = Point.t and type t = s) repr ->
      let repr_bezier p p0 bezier =
        Repr.quadratic_to
          ( p0
          , bezier.ctrl0
          , bezier.ctrl1
          , bezier.p1 )
          p in

      let _, repr = Array.fold_left t
          ~init:(true, repr)
          ~f:(fun (first, path) element ->
              let path =  if first then
                  Repr.start element.point path
                else path in
              match element.move with
              | Line p1 ->
                ( false
                , Repr.line_to element.point p1 path )
              | Curve bezier ->
                ( false
                , repr_bezier path element.point bezier )
            ) in
      Repr.stop repr


  type approx =
    { distance : float
    ; closest_point : Gg.v2
    ; ratio : float
    ; p0 : Point.t
    ; p1 : Point.t }

  (** Return the distance between a given point and the curve. May return
      None if the point is out of the curve *)
  let distance
    : Gg.v2 -> t -> approx option
    = fun point t ->

      Array.fold_left t
        ~init:None
        ~f:(fun res step ->
            match step.move with
            | Line p1 ->
              let box = Gg.Box2.of_pts (Point.get_coord step.point) (Point.get_coord p1) in
              begin match Gg.Box2.mem point box with
                | false -> res
                | true ->
                  (* TODO Evaluate the normal *)
                  res
              end
            | Curve bezier ->

              let bezier' = Shapes.Bezier.(

                  { p0 = Point.get_coord step.point
                  ; p1 = Point.get_coord bezier.p1
                  ; ctrl0 = bezier.ctrl0
                  ; ctrl1 = bezier.ctrl1 }
                ) in
              let ratio, point' = Shapes.Bezier.get_closest_point point bezier' in
              let distance' = Gg.V2.( norm (point - point') ) in
              match res with
              | Some {distance; _} when distance < distance' -> res
              | _ -> Some
                       { closest_point = point'
                       ; distance = distance'
                       ; p0 = step.point
                       ; p1 = bezier.p1
                       ; ratio }
          )

  let map
    : t -> (Point.t -> Point.t) -> t
    = fun t f ->
      Array.map t
        ~f:(fun step ->
            match step.move with
            | Line p2 ->
              { point = f step.point
              ; move = Line (f p2)
              }
            | Curve bezier ->
              let point = f step.point in
              { point
              ; move = Curve
                    { p1 = f bezier.p1
                    ; ctrl0 = Point.get_coord (f (Point.copy step.point bezier.ctrl0))
                    ; ctrl1 = Point.get_coord (f (Point.copy bezier.p1 bezier.ctrl1))
                    }
              }
          )

  let iter
    : t -> f:(Point.t -> unit) -> unit
    = fun t ~f ->
      Array.iter t
        ~f:(fun step ->
            match step.move with
            | Line p2 -> f step.point; f p2
            | Curve bezier -> f step.point ; f bezier.p1
          )

  let get_point'
    : step -> Point.t
    = fun { move ; _} ->
      match move with
      | Line p1 -> p1
      | Curve bezier -> bezier.p1

  (** Associate the return from the bezier point to an existing path *)
  let assoc_point
    : Shapes.Bezier.t -> step -> step
    = fun bezier step ->
      match step.move with
      | Line p1
      | Curve {p1; _} ->
        let p0' = Point.copy step.point bezier.Shapes.Bezier.p0
        and p1' = Point.copy p1 bezier.Shapes.Bezier.p1 in
        { point = p0'
        ; move = Curve
              { p1 = p1'
              ; ctrl0 = bezier.Shapes.Bezier.ctrl0
              ; ctrl1 = bezier.Shapes.Bezier.ctrl1
              }
        }


  let build_from_three_points p0 p1 p2 =
    let bezier =
      Shapes.Bezier.quadratic_to_cubic
      @@ Shapes.Bezier.three_points_quadratic
        (Point.get_coord p0)
        (Point.get_coord p1)
        (Point.get_coord p2) in

    (* The middle point is not exactly at the middle anymore (it can have been
       moved), we have the reevaluate it's position *)
    let ratio, _ = Shapes.Bezier.get_closest_point
        (Point.get_coord p1)
        bezier in

    let b0, b1 = Shapes.Bezier.slice ratio bezier in
    let p0' = Point.copy p0 b0.Shapes.Bezier.p0
    and p1' = Point.copy p1 b0.Shapes.Bezier.p1
    and p2' = Point.copy p2 b1.Shapes.Bezier.p1 in

    [| { point = p0'
       ; move =
           Curve { ctrl0 = b0.Shapes.Bezier.ctrl0
                 ; ctrl1 = b0.Shapes.Bezier.ctrl1
                 ; p1 = p1'
                 } }
     ; { point = p1'
       ; move = Curve { ctrl0 = b1.Shapes.Bezier.ctrl0
                      ; ctrl1 = b1.Shapes.Bezier.ctrl1
                      ; p1 = p2' }
       } |]

  (** Rebuild the whole curve by evaluating all the points *)
  let rebuild
    : t -> t option
    = fun t ->

      match Array.length t with
      | 0 -> None
      | 1 ->
        let step = Array.get t 0 in
        begin match step.move with
          | Curve {p1; _}
          | Line p1 ->
            Some
              [|
                { point = step.point
                ; move = Line p1 } |]
        end
      | 2 ->
        let p0 = (Array.get t 0).point
        and p1 = (Array.get t 1).point
        and p2 = get_point' @@ Array.get t 1 in
        Some (build_from_three_points p0 p1 p2)

      | _ ->

        (* Convert all the points in list *)
        let points = List.init
            ~len:((Array.length t) )
            ~f:(fun i -> Point.get_coord @@ get_point' (Array.get t i)) in
        let p0 = Point.get_coord @@ (Array.get t 0).point in

        let points = p0::points in

        (* We process the whole curve in a single block *)
        begin match Shapes.Bspline.to_bezier points with
          | Error `InvalidPath -> None
          | Ok beziers ->

            (* Now for each point, reassociate the same point information,
               We should have as many points as before *)
            let rebuilded = Array.map2 beziers t ~f:assoc_point in
            Some rebuilded
        end

  let find_pt_index
    : Point.t -> step array -> int option
    = fun point path ->
      (* First search the element to remove. The counter mark the position of
         the point to remove, not the segment itself. *)
      let idx = ref None
      and counter = ref 0  in

      let _ = Array.exists
          path
          ~f:(fun element ->
              let res =
                if (Point.id element.point) = (Point.id point) then (
                  idx := Some (!counter) ;
                  true
                ) else match element.move with
                  | Line p1
                  | Curve {p1;_} when (Point.id p1) = (Point.id point) ->
                    idx := Some (!counter+1) ;
                    true
                  | _ ->
                    false
              in
              incr counter;
              res) in
      !idx

  let remove_point
    : t -> Point.t -> t option
    = fun t point ->

      match Array.length t with
      | 0
      | 1 -> None
      | 2 ->
        (* Two segment, we get the points and transform this into a single line *)
        let p0 = (Array.get t 0).point
        and p1 = (Array.get t 1).point
        and p2 = get_point' @@ Array.get t 1 in
        let elms = List.filter [p0; p1; p2]
            ~f:(fun pt -> Point.id pt != Point.id point) in
        begin match elms with
          | p0::p1::[] ->
            Some
              [| { point = p0
                 ; move = Line p1 }|]
          | _ -> None
        end
      | l ->
        match find_pt_index point t with
        | None -> Some t
        | Some 0 ->
          (* Remove the first point *)
          let path = Array.init (l-1)
              ~f:( fun i -> Array.get t (i+1)) in
          Some path
        | Some n when n = (Array.length t) ->
          (* Remove the last point *)
          let path = Array.init (l-1)
              ~f:( fun i -> Array.get t i) in
          Some path
        | Some n ->
          let path' = Array.init (l-1)
              ~f:(fun i ->
                  if i < (n-1) then
                    Array.get t (i)
                  else if i = (n-1) then
                    (* We know that the point is not the first nor the last one.
                       So it is safe to call n-1 or n + 1 point

                       We have to rebuild the point and set that
                       point_(-1).id = point_(+1).id
                    *)
                    let p0 = (Array.get t i).point in

                    match (Array.get t (i+1)).move with
                    | Line p1 ->
                      { point = p0
                      ; move = Line p1 }
                    | Curve c ->
                      { point = p0
                      ; move = Curve c }

                  else
                    Array.get t (i+1)
                ) in
          rebuild path'

  let first_point
    : step -> Point.t
    = fun {point; _} -> point

  let replace_point
    : t -> Point.t -> t option
    = fun t p ->

      let add_path paths idx f points =
        if 0 <= idx && idx < Array.length paths then
          let path = Array.get t idx in
          Point.get_coord (f path)
          :: points
        else points in

      match Array.length t with
      | 0 -> None
      | 1 -> (* Only one point, easy ? *)
        let step = Array.get t 0 in
        begin match step.move with
          | Curve {p1; _}
          | Line p1 ->
            let p0 = if (Point.id step.point = Point.id p) then p else step.point
            and p1 = if (Point.id p1 = Point.id p) then p else p1 in
            Some [|
              { point = p0
              ; move = Line p1 }
            |]
        end

      | 2 ->
        let p0 = (Array.get t 0).point
        and p1 = (Array.get t 1).point
        and p2 = get_point' @@ Array.get t 1 in

        let p0 = if (Point.id p0 = Point.id p) then p else p0
        and p1 = if (Point.id p1 = Point.id p) then p else p1
        and p2 = if (Point.id p2 = Point.id p) then p else p2 in
        Some (build_from_three_points p0 p1 p2)

      (* More than two segmend, it is ok for a partial reevaluation *)
      | _ ->
        match find_pt_index p t with
        | None -> None
        | Some n ->
          let path = Array.copy t in

          let p0, p1 =

            if n < Array.length path then
              p, get_point' (Array.get path n)
            else
              (Array.get path (n -1)).point, p
          in

          let min_idx = max (n-3) 0 in

          let points =
            add_path path (n-3) first_point
            @@ add_path path (n-2) first_point
            @@ add_path path (n-1) first_point
            @@ (fun tl -> (Point.get_coord p)::tl)
            @@ add_path path n get_point'
            @@ add_path path (n+1) get_point'
            @@ add_path path (n+2) get_point'
            @@ [] in

          (* It is impressive how fast it is to evaluate the curve ! Maybe is the
             worker not required at all…
          *)
          let bezier_opt = Shapes.Bspline.to_bezier points in
          begin match bezier_opt with
            | Ok paths ->
              Array.iteri paths
                ~f:(fun i bezier ->
                    (* Only take two points before, and two after  *)
                    let idx = min_idx + i in
                    if (n-2 < idx) && (idx < n +2) && idx < Array.length path then
                      Array.set path idx (assoc_point bezier (Array.get path idx))
                  );
              Some path
            | Error _ ->
              let bezier', _ = Shapes.Bezier.three_points_quadratic
                  (Point.get_coord p)
                  (Point.get_coord @@ get_point' (Array.get path 0))
                  (Point.get_coord @@ get_point' (Array.get path 1))
                               |> Shapes.Bezier.quadratic_to_cubic
                               |> Shapes.Bezier.slice 0.5
              in
              Array.set path 0
                { point = p0
                ; move = (Curve
                            { ctrl0 = bezier'.Shapes.Bezier.ctrl0
                            ; ctrl1 = bezier'.Shapes.Bezier.ctrl1
                            ; p1
                            })
                };
              Some path
          end
end