File Coverage

blib/lib/SVGPDF/Contrib/Bogen.pm
Criterion Covered Total %
statement 212 250 84.8
branch 73 104 70.1
condition 15 27 55.5
subroutine 8 8 100.0
pod 2 2 100.0
total 310 391 79.2


line stmt bran cond sub pod time code
1             #! perl
2              
3 2     2   1138 use v5.26;
  2         6  
4 2     2   6 use strict;
  2         4  
  2         38  
5 2     2   29 use warnings;
  2         2  
  2         181  
6              
7             package SVGPDF::Contrib::Bogen;
8              
9             =head1 NAME
10              
11             SVGPDF::Contrib::Bogen - Circular and elliptic curves
12              
13             =head1 SYNOPSIS
14              
15             $context->bogen( $x1,$y1, $x2,$y2, $r, @opts);
16             $context->bogen_ellip( $x1,$y1, $x2,$y2, $rx,$ry, @opts);
17              
18             =head1 DESCRIPTION
19              
20             This package contains functions to draw circular and elliptic curves.
21              
22             This code is developed by Phil Perry, based on old PDF::API2 code and
23             friendly contributed to the SVGPDF project.
24              
25             =cut
26              
27 2     2   582 use Math::Trig;
  2         19467  
  2         8366  
28              
29             =over
30              
31             =item $context->bogen_ellip($x1,$y1, $x2,$y2, $rx,$ry, @opts)
32              
33             This is a variant of the original C call from PDF::Builder, which
34             drew a segment (arc) of a circle, which was adapted here by Phil Perry to draw
35             an elliptical arc.
36              
37             (German for I, as in a segment (arc) of an ellipse), this is a
38             segment of an ellipse defined by the intersection of two ellipses of given x
39             and y radii, with the two intersection points as inputs. There are four
40             possible resulting arcs, which can be selected with opts C and C.
41              
42             This extends the path along an arc of an ellipse of the specified x and y radii
43             between C<[$x1,$y1]> to C<[$x2,$y2]>. The current position is then set
44             to the endpoint of the arc (C<[$x2,$y2]>).
45              
46             Options (C<@opts>)
47              
48             =over
49              
50             =item 'move' => move_flag
51              
52             Set C to a I value if this arc is the beginning of a new
53             path instead of the continuation of an existing path. Note that the default
54             (C => I) is
55             I a straight line to I and then the arc, but a blending into the curve
56             from the current point. It will often I pass through I! Set to
57             I, there will be a jump (move) from the current point to I, to where
58             the arc will start.
59              
60             =item 'large' => larger_arc_flag
61              
62             Set C to a I value to draw the larger ("outer") arc between the
63             two points, instead of the smaller one. Both arcs are
64             drawn I from I to I. The default value of I draws
65             the smaller arc.
66              
67             =item 'dir' => draw_direction
68              
69             Set C to a I value to draw the mirror image of the specified arc
70             (flip it over, so that its center point is on the other side of the line
71             connecting the two points). Both arcs (small or large) are drawn
72             I from I to I. The default (I) draws
73             clockwise arcs.
74              
75             =item 'rotate' => axis_rotation
76              
77             A non-zero value is the degrees to rotate the axes of the ellipse (in a
78             counter-clockwise manner). For example, C<'rotate'=E45> will have the
79             ellipse's +X axis pointing "northeast" and the +Y axis pointing "northwest".
80             The default value is 0 (no rotation).
81              
82             =item 'full' => color_spec
83              
84             If given (no default), draw the full ellipse (not just the arc)
85             in this color, with a dot at its center. This may be useful
86             for diagnostic and development purposes, to show the ellipse from which
87             the arc is obtained.
88              
89             =back
90              
91             B
92              
93             If the given radii C<$rx> and C<$ry> are too small for the points
94             I and I to fit on the specified ellipse, they will be proportionately
95             scaled up untilthe points fit on the ellipse.
96             This is a silent error, as due to rounding, given points (even if correct)
97             may not exactly fit on the ellipse. Further note that the algorithm only
98             enlarges the radii until a sweep of 180 degrees is obtained, so it is possible
99             that the ellipse will be smaller than your intended one!
100              
101             =back
102              
103             =cut
104              
105             sub bogen_ellip {
106 11     11 1 45 my ($self, $x1,$y1, $x2,$y2, $rx,$ry, %opts) = @_;
107              
108             # set default values for options
109 11         18 my $move = 0; # 0 = continue from present point, 1 = move to point 1
110 11         15 my $larc = 0; # 0 = choose smaller arc, 1 = choose larger
111 11         11 my $dir = 0; # 0 = CW, 1 = CCW
112 11         13 my $rotate = 0; # degrees rotated around center of ellipse (so rx isn't
113             # due left-right)
114 11 50       27 if (defined $opts{'move'}) { $move = $opts{'move'}; }
  11         18  
115 11 50       22 if (defined $opts{'large'}) { $larc = $opts{'large'}; }
  11         20  
116 11 50       20 if (defined $opts{'dir'}) { $dir = $opts{'dir'}; }
  11         13  
117 11 50       16 if (defined $opts{'rotate'}) { $rotate = $opts{'rotate'}; }
  11         36  
118              
119 11         41 my ($alpha,$beta);
120 11         0 my ($cosR, $sinR, $x1P,$y1P, $xcP,$ycP, $xc,$yc, $lambda, $d,$k);
121 11         0 my ($xm,$ym, $xM,$yM, $ux,$uy,$ulen, $vx,$vy,$vlen, $dp_uv);
122 11         0 my ($cosTheta1,$theta1, $cosDeltaTheta,$deltaTheta);
123 11         14 my $PI = 3.141593;
124              
125             # P1 and P2 need to be distinct
126 11 50 33     26 if ($x1 == $x2 && $y1 == $y2) {
127 0         0 print STDERR "bogen_ellip requires two distinct points. Skipping.\n";
128 0         0 return $self;
129             }
130              
131             # think of the SVG coordinates (where this algorithm comes from) as being
132             # like PDF's (conventional geometry), except mirrored about the x axis
133             # (horizontal line). y grows downwards, angles + = CW sweep, starting at
134             # angle 0 degrees points due east (axis rotation applied).
135             # just compute everything SVG's way, and when applied to PDF everything
136             # will be right side up and turning the right way.
137             # larc and dir need to be 0 or 1, not just false/true
138 11 100       18 if ($larc) { $larc = 1; } else { $larc = 0; }
  2         3  
  9         11  
139 11 100       14 if ($dir) { $dir = 1; } else { $dir = 0; }
  9         9  
  2         2  
140             # fS (from dir) 1 if sweep is increasing angle (CCW in PDF, CW in SVG)
141             # fA (larc) 1 is larger (> 180 degrees) arc
142              
143             # need to flip rotation direction, sweep direction to match
144             # SVG algorithm.
145             # $dir = !$dir if $larc != $dir;
146             # $dir = !$dir;
147             # $rotate = -$rotate;
148 11         23 $rotate = $rotate/180*$PI;
149              
150             # make both radii positive r = |r|
151 11 50       24 if ($rx < 0) { $rx = -$rx; }
  0         0  
152 11 50       17 if ($ry < 0) { $ry = -$ry; }
  0         0  
153              
154             # if either radius is 0, arc is a straight line from P1 to P2
155 11 50 33     39 if (!$rx || !$ry) {
156 0         0 $self->poly($x1,$y1, $x2,$y2); # degenerate case
157 0         0 return $self;
158             }
159              
160             # compute elliptical arc parameters per
161             # https://gitlab.gnome.org/GNOME/librsvg/-/blob/main/rsvg/src/path_builder.rs,
162             # based on https://www.w3.org/TR/SVG2/implnote.html#Introduction
163             # (code is more from the W3 math than the GNOME code, which it's not
164             # clear what the sign conventions are)
165             # if the radii are too small, they will be corrected below.
166              
167             # midpoint distance of line from P1 to P2
168 11         18 $xm = ($x1-$x2)/2.0;
169 11         13 $ym = ($y1-$y2)/2.0;
170             # actual midpoint of line from P1 to P2
171 11         58 $xM = ($x1+$x2)/2.0;
172 11         16 $yM = ($y1+$y2)/2.0;
173              
174             # P1'
175 11         19 $cosR = cos($rotate);
176 11         12 $sinR = sin($rotate);
177              
178 11         18 $x1P = $cosR*$xm + $sinR*$ym;
179 11         13 $y1P = -$sinR*$xm + $cosR*$ym;
180              
181             # increase radii if necessary
182 11         28 $lambda = ($x1P/$rx)**2 + ($y1P/$ry)**2;
183 11 100       20 if ($lambda > 1.0) {
184             # a radius cannot be too large, but if too small (lambda > 1),
185             # preserve aspect ratio while increasing rx and ry
186 6         8 $rx *= sqrt($lambda);
187 6         6 $ry *= sqrt($lambda);
188             }
189              
190             # C' (transformed center)
191 11         26 $d = ($rx * $ry)**2 - ($rx * $y1P)**2 - ($ry * $x1P)**2;
192 11         19 $d /= (($rx * $y1P)**2 + ($ry * $x1P)**2);
193             # deal with rounding issues
194 11 100 66     27 $d = 0 if $d < 0.0 && $d > -1.0e-10;
195 11 50       18 if ($d < 0.0) {
196             # failure, skip
197 0         0 print STDERR "Unable to compute elliptical arc (1) d=$d. Skipping.\n";
198 0         0 return $self;
199             }
200 11         12 $d = sqrt($d);
201             # negate if small arc CW or large arc CCW (per normal PDF coordinates)
202 11 100       15 $d = -$d if $larc == $dir;
203 11         16 $xcP = $d * $rx/$ry * $y1P;
204 11         16 $ycP = $d * -$ry/$rx * $x1P;
205              
206             # C (actual center)
207 11         22 $xc = $cosR * $xcP - $sinR * $ycP + $xM;
208 11         16 $yc = $sinR * $xcP + $cosR * $ycP + $yM;
209              
210             # theta1 (start angle 0, sweep to P1'). 0 is due East, CW +
211             # first, get unit vector for C'->P1'
212 11         14 $ux = ($x1P - $xcP)/$rx; # "unstretch" ellipse into circle
213 11         14 $uy = ($y1P - $ycP)/$ry;
214 11         16 $ulen = sqrt(($ux**2 + $uy**2));
215 11 50       16 if ($ulen == 0.0) {
216             # failure, skip (shouldn't see 0 length C' to P1' vector)
217 0         0 print STDERR "Unable to compute elliptical arc (2). Skipping.\n";
218 0         0 return $self;
219             }
220 11         13 $cosTheta1 = $ux/$ulen; # unit vector x component = cos(theta1)
221              
222             # as rx and ry have already been corrected, is this ever needed?
223             # better safe than sorry, especially if just past +/-1 due to rounding...
224 11 50       20 $cosTheta1 = -1 if $cosTheta1 < -1.0;
225 11 50       16 $cosTheta1 = 1 if $cosTheta1 > 1.0;
226 11         35 $theta1 = acos($cosTheta1);
227              
228             # negate (flip) if on other side (up, negative y territory)
229 11 100       102 $theta1 = -$theta1 if $uy < 0.0;
230              
231             # delta theta sweep P1 to P2. vector v is C' to P2'
232 11         17 $vx = (-$x1P - $xcP)/$rx; # again, squash ellipse to circle
233 11         14 $vy = (-$y1P - $ycP)/$ry;
234 11         16 $vlen = sqrt($vx**2 + $vy**2);
235 11 50       16 if ($vlen == 0.0) {
236             # failure, skip (P1 == P2? vector can't be 0 length)
237 0         0 print STDERR "Unable to compute elliptical arc (3). Skipping.\n";
238 0         0 return $self;
239             }
240 11         12 $vx /= $vlen; $vy /= $vlen;
  11         12  
241              
242             # acos( u dot v / 1*1 ) is sweep angle
243 11         29 $k = $ux*$vx + $uy*$vy;
244             # again, better safe than sorry...
245 11 100       18 $k = -1 if $k < -1.0;
246 11 50       17 $k = 1 if $k > 1.0;
247 11         15 $deltaTheta = acos($k);
248 11 100       55 $deltaTheta = -$deltaTheta if $ux*$vy-$uy*$vx < 0.0;
249              
250             # convert sweep angles to PDF coordinates in degrees
251 11         12 $alpha = $theta1*180/$PI;
252 11         17 $beta = $alpha + $deltaTheta*180/$PI;
253 11         20 while ($beta >= 360.0) { $beta -= 360.0; }
  0         0  
254 11         33 while ($beta < 0.0) { $beta += 360.0; }
  1         2  
255              
256             # -------------------------------------------------------------------
257             # if 'full' color ellipse requested, draw it now for angle 0 sweep 180
258             # and angle 180 sweep 180 (for full ellipse)
259 11 50       18 if (defined $opts{'full'}) {
260             # save current location to return to
261 0         0 my @saveloc = ($self->{' x'},$self->{' y'});
262 0         0 $self->save();
263            
264 0         0 $self->stroke_color($opts{'full'});
265             # move to P1, draw 180 arcs
266 0         0 $self->move($x1,$y1);
267 0         0 _arc2points($self, $rx,$ry, $alpha,$alpha+180, $x1,$y1, 2*$xc-$x1,2*$yc-$y1, 0, $rotate);
268 0         0 $self->move($x1,$y1);
269 0         0 _arc2points($self, $rx,$ry, $alpha,$alpha+180, $x1,$y1, 2*$xc-$x1,2*$yc-$y1, 1, $rotate);
270 0         0 $self->stroke();
271            
272 0         0 $self->restore();
273 0         0 $self->move(@saveloc);
274             }
275             # -------------------------------------------------------------------
276              
277             # move to starting point (if specified), then output arc
278 11 50       17 $self->move($x1,$y1) if $move;
279              
280             # PDF::Builder's arc() includes a 'dir' flag, but PDF::API doesn't.
281             # so, need to calculate points (for Bezier curves).
282 11         25 _arc2points($self, $rx,$ry, $alpha,$beta, $x1,$y1, $x2,$y2, $dir, $rotate);
283              
284 11         56 return $self;
285             }
286              
287             # calculate the Bezier control points for an elliptical arc, given
288             # self = graphics context
289             # rx and ry = radii
290             # alpha and beta = starting and ending sweeps (degrees)
291             # x' and y' = P1'
292             # x2 and y2 = last point (if needed)
293             # dir = 1 CW, 0 CCW
294             # rotate = axis rotation in radians
295             # returns nothing. curve called to output the curve to PDF
296             sub _arc2points {
297 11     11   28 my ($self, $rx,$ry, $alpha,$beta, $x1,$y1, $x2,$y2, $dir, $rotate) = @_;
298 11         15 my (@points, $x,$y, $p0_x,$p0_y, $p1_x,$p1_y, $p2_x,$p2_y, $p3_x,$p3_y);
299 11         29 $dir = !$dir;
300              
301             # @points is relative to starting point of arc
302 11         21 @points = _arctocurve($rx,$ry, $alpha,$beta, $dir,$rotate);
303              
304             # counterrotate all start/end/control points around P1 by -rotate degrees
305 11 100       20 if ($rotate) {
306 5         6 my $r = $rotate; # already in radians
307 5         5 my $cosR = cos($r);
308 5         5 my $sinR = sin($r);
309 5         6 my ($x,$y, $xr,$yr);
310 5         12 for (my $i=0; $i<@points; $i+=2) {
311 180         152 $x = $points[$i]; $y = $points[$i+1];
  180         152  
312 180         183 $xr = $x1 + $cosR*($x-$x1) - $sinR*($y-$y1);
313 180         157 $yr = $y1 + $sinR*($x-$x1) + $cosR*($y-$y1);
314 180         166 $points[$i] = $xr; $points[$i+1] = $yr;
  180         218  
315             }
316             }
317              
318 11         16 $p0_x = shift @points;
319 11         12 $p0_y = shift @points;
320 11         14 $x = $x1 - $p0_x;
321 11         14 $y = $y1 - $p0_y;
322              
323 11         16 while (scalar @points > 0) {
324 93         108 $p1_x = $x + shift @points;
325 93         90 $p1_y = $y + shift @points;
326 93         93 $p2_x = $x + shift @points;
327 93         91 $p2_y = $y + shift @points;
328             # if we run out of data points, use the end point instead
329 93 50       148 if (scalar @points == 0) {
330 0         0 $p3_x = $x2;
331 0         0 $p3_y = $y2;
332             } else {
333 93         112 $p3_x = $x + shift @points;
334 93         104 $p3_y = $y + shift @points;
335             }
336 93         215 $self->curve($p1_x,$p1_y, $p2_x,$p2_y, $p3_x,$p3_y);
337 93         107 shift @points;
338 93         165 shift @points;
339             }
340              
341 11         21 return $self;
342             }
343            
344             # input: x and y axis radii
345             # sweep start and end angles (degrees)
346             # sweep direction (0=CCW (default), or 1=CW)
347             # axis rotation (radians, + = CCW, default = 0)
348             # output: two endpoints and two control points for
349             # the Bezier curve describing the arc
350             # maximum 30 degrees of sweep: is broken up into smaller
351             # arc segments if necessary
352             # if crosses 0 degree angle in either sweep direction, split there at 0
353             # if alpha=beta (0 degree sweep) or either radius <= 0, fatal error
354             sub _arctocurve {
355 359     359   476 my ($rx,$ry, $alpha,$beta, $dir,$rot) = @_;
356              
357 359 100       434 if (!defined $rot) { $rot = 0; } # default is no rotation
  11         13  
358 359 50       423 if (!defined $dir) { $dir = 0; } # default is CCW sweep
  0         0  
359             # check for non-positive radius
360 359 50 33     654 if ($rx <= 0 || $ry <= 0) {
361 0         0 die "curve request with radius not > 0 ($rx, $ry)";
362             }
363             # check for zero degrees of sweep
364 359 50       410 if ($alpha == $beta) {
365 0         0 die "curve request with zero degrees of sweep ($alpha to $beta)";
366             }
367              
368             # constrain alpha and beta to 0..360 range so 0 crossing check works
369 359         434 while ($alpha < 0.0) { $alpha += 360.0; }
  9         14  
370 359         422 while ( $beta < 0.0) { $beta += 360.0; }
  6         12  
371 359         449 while ($alpha > 360.0) { $alpha -= 360.0; }
  0         0  
372 359         483 while ( $beta > 360.0) { $beta -= 360.0; }
  0         0  
373              
374             # Note that there is a problem with the original code, when the 0 degree
375             # angle is crossed. It especially shows up in arc() and pie(). Therefore,
376             # split the original sweep at 0 degrees, if it crosses that angle.
377 359 100 100     540 if (!$dir && $alpha > $beta) { # CCW pass over 0 degrees
378 7 50 33     26 if ($alpha == 360.0 && $beta == 0.0) { # oddball case
    50          
    100          
379 0         0 return (_arctocurve($rx,$ry, 0.0,360.0, 0,$rot));
380             } elsif ($alpha == 360.0) { # alpha to 360 would be null
381 0         0 return (_arctocurve($rx,$ry, 0.0,$beta, 0,$rot));
382             } elsif ($beta == 0.0) { # 0 to beta would be null
383 1         5 return (_arctocurve($rx,$ry, $alpha,360.0, 0,$rot));
384             } else {
385             return (
386 6         14 _arctocurve($rx,$ry, $alpha,360.0, 0,$rot),
387             _arctocurve($rx,$ry, 0.0,$beta, 0,$rot)
388             );
389             }
390             }
391 352 100 100     603 if ($dir && $alpha < $beta) { # CW pass over 0 degrees
392 5 50 66     50 if ($alpha == 0.0 && $beta == 360.0) { # oddball case
    100          
    50          
393 0         0 return (_arctocurve($rx,$ry, 360.0,0.0, 1,$rot));
394             } elsif ($alpha == 0.0) { # alpha to 0 would be null
395 2         6 return (_arctocurve($rx,$ry, 360.0,$beta, 1,$rot));
396             } elsif ($beta == 360.0) { # 360 to beta would be null
397 0         0 return (_arctocurve($rx,$ry, $alpha,0.0, 1,$rot));
398             } else {
399             return (
400 3         7 _arctocurve($rx,$ry, $alpha,0.0, 1,$rot),
401             _arctocurve($rx,$ry, 360.0,$beta, 1,$rot)
402             );
403             }
404             }
405              
406             # limit arc length to 30 degrees, for reasonable smoothness
407             # none of the long arcs or short resulting arcs cross 0 degrees
408 347 100       402 if (abs($beta-$alpha) > 30) {
409             return (
410 158         281 _arctocurve($rx,$ry, $alpha,($beta+$alpha)/2, $dir,$rot),
411             _arctocurve($rx,$ry, ($beta+$alpha)/2,$beta, $dir,$rot)
412             );
413             } else {
414             # calculate cubic Bezier points (start, two control, end)
415 189         211 my ($p0_x,$p0_y, $p1_x,$p1_y, $p2_x,$p2_y, $p3_x,$p3_y);
416             # Note that we can't use deg2rad(), because closed arcs (circle() and
417             # ellipse()) are 0-360 degrees, which deg2rad treats as 0-0 radians!
418 189         186 my $aa = $alpha * 3.141593 / 180;
419 189         180 my $bb = $beta * 3.141593 / 180;
420              
421 189         254 my $bcp = (4.0/3 * (1 - cos(($bb - $aa)/2)) / sin(($bb - $aa)/2));
422 189         200 my $sin_alpha = sin($aa);
423 189         192 my $sin_beta = sin($bb);
424 189         196 my $cos_alpha = cos($aa);
425 189         179 my $cos_beta = cos($bb);
426              
427 189         190 $p0_x = $rx * $cos_alpha;
428 189         166 $p0_y = $ry * $sin_alpha;
429 189         171 $p1_x = $rx * ($cos_alpha - $bcp * $sin_alpha);
430 189         187 $p1_y = $ry * ($sin_alpha + $bcp * $cos_alpha);
431 189         189 $p2_x = $rx * ($cos_beta + $bcp * $sin_beta);
432 189         177 $p2_y = $ry * ($sin_beta - $bcp * $cos_beta);
433 189         185 $p3_x = $rx * $cos_beta;
434 189         198 $p3_y = $ry * $sin_beta;
435              
436 189         537 return ($p0_x,$p0_y, $p1_x,$p1_y, $p2_x,$p2_y, $p3_x,$p3_y);
437             }
438             }
439              
440             # Circular arc ('bogen'), by PDF::API2 and anhanced by PDF::Builder.
441              
442             =over
443              
444             =item $content->bogen($x1,$y1, $x2,$y2, $radius, $move, $larger, $reverse)
445              
446             =item $content->bogen($x1,$y1, $x2,$y2, $radius, $move, $larger)
447              
448             =item $content->bogen($x1,$y1, $x2,$y2, $radius, $move)
449              
450             =item $content->bogen($x1,$y1, $x2,$y2, $radius)
451              
452             (I is German for I, as in a segment (arc) of a circle. This is a
453             segment of a circle defined by the intersection of two circles of a given
454             radius, with the two intersection points as inputs. There are B possible
455             resulting arcs, which can be selected with C<$larger> and C<$reverse>.)
456              
457             This extends the path along an arc of a circle of the specified radius
458             between C<[$x1,$y1]> to C<[$x2,$y2]>. The current position is then set
459             to the endpoint of the arc (C<[$x2,$y2]>).
460              
461             Set C<$move> to a I value if this arc is the beginning of a new
462             path instead of the continuation of an existing path. Note that the default
463             (C<$move> = I) is
464             I a straight line to I and then the arc, but a blending into the curve
465             from the current point. It will often I pass through I!
466              
467             Set C<$larger> to a I value to draw the larger ("outer") arc between the
468             two points, instead of the smaller one. Both arcs are drawn I from
469             I to I. The default value of I draws the smaller arc.
470             Note that the "other" circle's larger arc is used (the center point is
471             "flipped" across the line between I and I), rather than using the
472             "remainder" of the smaller arc's circle (which would necessitate reversing the
473             direction of travel along the arc -- see C<$reverse>).
474              
475             Set C<$reverse> to a I value to draw the mirror image of the
476             specified arc (flip it over, so that its center point is on the other
477             side of the line connecting the two points). Both arcs are drawn
478             I from I to I. The default (I) draws
479             clockwise arcs. An arc is B drawn from I to I; the direction
480             (clockwise or counter-clockwise) may be chosen.
481              
482             The C<$radius> value cannot be smaller than B the distance from
483             C<[$x1,$y1]> to C<[$x2,$y2]>. If it is too small, the radius will be set to
484             half the distance between the points (resulting in an arc that is a
485             semicircle). This is a silent error.
486              
487             =back
488              
489             =cut
490              
491             sub bogen {
492 11     11 1 27 my ($self, $x1,$y1, $x2,$y2, $r, $move, $larc, $spf) = @_;
493              
494 11         24 my ($p0_x,$p0_y, $p1_x,$p1_y, $p2_x,$p2_y, $p3_x,$p3_y);
495 11         0 my ($dx,$dy, $x,$y, $alpha,$beta, $alpha_rad, $d,$z, $dir, @points);
496              
497 11 50 33     25 if ($x1 == $x2 && $y1 == $y2) {
498 0         0 die "bogen requires two distinct points";
499             }
500 11 50       21 if ($r <= 0.0) {
501 0         0 die "bogen requires a positive radius";
502             }
503 11 50       17 $move = 0 if !defined $move;
504 11 50       18 $larc = 0 if !defined $larc;
505 11 50       16 $spf = 0 if !defined $spf;
506              
507 11         13 $dx = $x2 - $x1;
508 11         11 $dy = $y2 - $y1;
509 11         20 $z = sqrt($dx**2 + $dy**2);
510 11         32 $alpha_rad = asin($dy/$z); # |dy/z| guaranteed <= 1.0
511 11 100       76 $alpha_rad = pi - $alpha_rad if $dx < 0;
512              
513             # alpha is direction of vector P1 to P2
514 11         27 $alpha = rad2deg($alpha_rad);
515             # use the complementary angle for flipped arc (arc center on other side)
516             # effectively clockwise draw from P2 to P1
517 11 100       98 $alpha -= 180 if $spf;
518              
519 11         14 $d = 2*$r;
520             # z/d must be no greater than 1.0 (arcsine arg)
521 11 100       20 if ($z > $d) {
522 1         3 $d = $z; # SILENT error and fixup
523 1         2 $r = $d/2;
524             }
525              
526 11         20 $beta = rad2deg(2*asin($z/$d));
527             # beta is the sweep P1 to P2: ~0 (r very large) to 180 degrees (min r)
528 11 100       95 $beta = 360-$beta if $larc; # large arc is remainder of small arc
529             # for large arc, beta could approach 360 degrees if r is very large
530              
531             # always draw CW (dir=1)
532             # note that start and end could be well out of +/-360 degree range
533 11         18 my $sweep1 = 90 + $alpha + $beta/2;
534 11         17 my $sweep2 = 90 + $alpha - $beta/2;
535 11 100       21 if (abs($sweep1*$r) < 0.001) {
536             # tiny angle force to 0
537 2         4 $sweep1 = 0;
538             }
539 11         21 @points = _arctocurve($r,$r, $sweep1,$sweep2, 1);
540              
541 11 100       24 if ($spf) { # flip order of points for reverse arc
542 5         17 my @pts = @points;
543 5         12 @points = ();
544 5         10 while (@pts) {
545 160         139 $y = pop @pts;
546 160         143 $x = pop @pts;
547 160         192 push(@points, $x,$y);
548             }
549             }
550              
551 11         14 $p0_x = shift @points;
552 11         13 $p0_y = shift @points;
553 11         15 $x = $x1 - $p0_x;
554 11         13 $y = $y1 - $p0_y;
555              
556 11 50       17 $self->move($x1,$y1) if $move;
557              
558 11         19 while (scalar @points > 0) {
559 96         118 $p1_x = $x + shift @points;
560 96         98 $p1_y = $y + shift @points;
561 96         116 $p2_x = $x + shift @points;
562 96         103 $p2_y = $y + shift @points;
563             # if we run out of data points, use the end point instead
564 96 50       134 if (scalar @points == 0) {
565 0         0 $p3_x = $x2;
566 0         0 $p3_y = $y2;
567             } else {
568 96         97 $p3_x = $x + shift @points;
569 96         97 $p3_y = $y + shift @points;
570             }
571 96         212 $self->curve($p1_x,$p1_y, $p2_x,$p2_y, $p3_x,$p3_y);
572 96         119 shift @points;
573 96         168 shift @points;
574             }
575              
576 11         46 return $self;
577             }
578              
579             1;