File Coverage

blib/lib/SVGPDF/Contrib/Bogen.pm
Criterion Covered Total %
statement 207 246 84.1
branch 70 102 68.6
condition 14 27 51.8
subroutine 8 8 100.0
pod 2 2 100.0
total 301 385 78.1


line stmt bran cond sub pod time code
1             #! perl
2              
3 2     2   1223 use v5.26;
  2         8  
4 2     2   10 use strict;
  2         4  
  2         55  
5 2     2   22 use warnings;
  2         3  
  2         199  
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   474 use Math::Trig;
  2         18286  
  2         9130  
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 69 my ($self, $x1,$y1, $x2,$y2, $rx,$ry, %opts) = @_;
107              
108             # set default values for options
109 11         27 my $move = 0; # 0 = continue from present point, 1 = move to point 1
110 11         19 my $larc = 0; # 0 = choose smaller arc, 1 = choose larger
111 11         24 my $dir = 0; # 0 = CW, 1 = CCW
112 11         16 my $rotate = 0; # degrees rotated around center of ellipse (so rx isn't
113             # due left-right)
114 11 50       42 if (defined $opts{'move'}) { $move = $opts{'move'}; }
  11         23  
115 11 50       35 if (defined $opts{'large'}) { $larc = $opts{'large'}; }
  11         26  
116 11 50       29 if (defined $opts{'dir'}) { $dir = $opts{'dir'}; }
  11         23  
117 11 50       27 if (defined $opts{'rotate'}) { $rotate = $opts{'rotate'}; }
  11         66  
118              
119 11         60 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         20 my $PI = 3.141593;
124              
125             # P1 and P2 need to be distinct
126 11 50 33     39 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       27 if ($larc) { $larc = 1; } else { $larc = 0; }
  2         5  
  9         19  
139 11 100       27 if ($dir) { $dir = 1; } else { $dir = 0; }
  9         16  
  2         4  
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         31 $rotate = $rotate/180*$PI;
149              
150             # make both radii positive r = |r|
151 11 50       27 if ($rx < 0) { $rx = -$rx; }
  0         0  
152 11 50       29 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     60 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         24 $xm = ($x1-$x2)/2.0;
169 11         24 $ym = ($y1-$y2)/2.0;
170             # actual midpoint of line from P1 to P2
171 11         26 $xM = ($x1+$x2)/2.0;
172 11         89 $yM = ($y1+$y2)/2.0;
173              
174             # P1'
175 11         24 $cosR = cos($rotate);
176 11         18 $sinR = sin($rotate);
177              
178 11         25 $x1P = $cosR*$xm + $sinR*$ym;
179 11         28 $y1P = -$sinR*$xm + $cosR*$ym;
180              
181             # increase radii if necessary
182 11         40 $lambda = ($x1P/$rx)**2 + ($y1P/$ry)**2;
183 11 100       30 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         15 $rx *= sqrt($lambda);
187 6         9 $ry *= sqrt($lambda);
188             }
189              
190             # C' (transformed center)
191 11         38 $d = ($rx * $ry)**2 - ($rx * $y1P)**2 - ($ry * $x1P)**2;
192 11         28 $d /= (($rx * $y1P)**2 + ($ry * $x1P)**2);
193             # deal with rounding issues
194 11 100 66     43 $d = 0 if $d < 0.0 && $d > -1.0e-10;
195 11 50       34 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         20 $d = sqrt($d);
201             # negate if small arc CW or large arc CCW (per normal PDF coordinates)
202 11 100       48 $d = -$d if $larc == $dir;
203 11         23 $xcP = $d * $rx/$ry * $y1P;
204 11         25 $ycP = $d * -$ry/$rx * $x1P;
205              
206             # C (actual center)
207 11         27 $xc = $cosR * $xcP - $sinR * $ycP + $xM;
208 11         24 $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         20 $ux = ($x1P - $xcP)/$rx; # "unstretch" ellipse into circle
213 11         19 $uy = ($y1P - $ycP)/$ry;
214 11         24 $ulen = sqrt(($ux**2 + $uy**2));
215 11 50       29 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         21 $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       31 $cosTheta1 = -1 if $cosTheta1 < -1.0;
225 11 50       25 $cosTheta1 = 1 if $cosTheta1 > 1.0;
226 11         54 $theta1 = acos($cosTheta1);
227              
228             # negate (flip) if on other side (up, negative y territory)
229 11 100       152 $theta1 = -$theta1 if $uy < 0.0;
230              
231             # delta theta sweep P1 to P2. vector v is C' to P2'
232 11         22 $vx = (-$x1P - $xcP)/$rx; # again, squash ellipse to circle
233 11         50 $vy = (-$y1P - $ycP)/$ry;
234 11         27 $vlen = sqrt($vx**2 + $vy**2);
235 11 50       32 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         20 $vx /= $vlen; $vy /= $vlen;
  11         16  
241              
242             # acos( u dot v / 1*1 ) is sweep angle
243 11         22 $k = $ux*$vx + $uy*$vy;
244             # again, better safe than sorry...
245 11 100       28 $k = -1 if $k < -1.0;
246 11 50       25 $k = 1 if $k > 1.0;
247 11         28 $deltaTheta = acos($k);
248 11 100       108 $deltaTheta = -$deltaTheta if $ux*$vy-$uy*$vx < 0.0;
249              
250             # convert sweep angles to PDF coordinates in degrees
251 11         26 $alpha = $theta1*180/$PI;
252 11         23 $beta = $alpha + $deltaTheta*180/$PI;
253 11         31 while ($beta >= 360.0) { $beta -= 360.0; }
  0         0  
254 11         30 while ($beta < 0.0) { $beta += 360.0; }
  1         4  
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       33 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       28 $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         38 _arc2points($self, $rx,$ry, $alpha,$beta, $x1,$y1, $x2,$y2, $dir, $rotate);
283              
284 11         77 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   46 my ($self, $rx,$ry, $alpha,$beta, $x1,$y1, $x2,$y2, $dir, $rotate) = @_;
298 11         27 my (@points, $x,$y, $p0_x,$p0_y, $p1_x,$p1_y, $p2_x,$p2_y, $p3_x,$p3_y);
299 11         19 $dir = !$dir;
300              
301             # @points is relative to starting point of arc
302 11         34 @points = _arctocurve($rx,$ry, $alpha,$beta, $dir,$rotate);
303              
304             # counterrotate all start/end/control points around P1 by -rotate degrees
305 11 100       32 if ($rotate) {
306 5         11 my $r = $rotate; # already in radians
307 5         11 my $cosR = cos($r);
308 5         12 my $sinR = sin($r);
309 5         9 my ($x,$y, $xr,$yr);
310 5         20 for (my $i=0; $i<@points; $i+=2) {
311 180         312 $x = $points[$i]; $y = $points[$i+1];
  180         291  
312 180         380 $xr = $x1 + $cosR*($x-$x1) - $sinR*($y-$y1);
313 180         410 $yr = $y1 + $sinR*($x-$x1) + $cosR*($y-$y1);
314 180         281 $points[$i] = $xr; $points[$i+1] = $yr;
  180         455  
315             }
316             }
317              
318 11         28 $p0_x = shift @points;
319 11         17 $p0_y = shift @points;
320 11         20 $x = $x1 - $p0_x;
321 11         34 $y = $y1 - $p0_y;
322              
323 11         32 while (scalar @points > 0) {
324 93         150 $p1_x = $x + shift @points;
325 93         170 $p1_y = $y + shift @points;
326 93         133 $p2_x = $x + shift @points;
327 93         136 $p2_y = $y + shift @points;
328             # if we run out of data points, use the end point instead
329 93 50       223 if (scalar @points == 0) {
330 0         0 $p3_x = $x2;
331 0         0 $p3_y = $y2;
332             } else {
333 93         160 $p3_x = $x + shift @points;
334 93         159 $p3_y = $y + shift @points;
335             }
336 93         316 $self->curve($p1_x,$p1_y, $p2_x,$p2_y, $p3_x,$p3_y);
337 93         156 shift @points;
338 93         238 shift @points;
339             }
340              
341 11         30 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 282     282   776 my ($rx,$ry, $alpha,$beta, $dir,$rot) = @_;
356              
357 282 100       545 if (!defined $rot) { $rot = 0; } # default is no rotation
  6         12  
358 282 50       551 if (!defined $dir) { $dir = 0; } # default is CCW sweep
  0         0  
359             # check for non-positive radius
360 282 50 33     912 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 282 50       557 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 282         569 while ($alpha < 0.0) { $alpha += 360.0; }
  9         25  
370 282         607 while ( $beta < 0.0) { $beta += 360.0; }
  3         9  
371 282         577 while ($alpha > 360.0) { $alpha -= 360.0; }
  0         0  
372 282         574 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 282 100 100     741 if (!$dir && $alpha > $beta) { # CCW pass over 0 degrees
378 7 50 33     41 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         18 _arctocurve($rx,$ry, $alpha,360.0, 0,$rot),
387             _arctocurve($rx,$ry, 0.0,$beta, 0,$rot)
388             );
389             }
390             }
391 275 100 100     711 if ($dir && $alpha < $beta) { # CW pass over 0 degrees
392 2 50 33     15 if ($alpha == 0.0 && $beta == 360.0) { # oddball case
    50          
    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 0         0 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 2         6 _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 273 100       536 if (abs($beta-$alpha) > 30) {
409             return (
410 124         361 _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 149         239 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 149         230 my $aa = $alpha * 3.141593 / 180;
419 149         239 my $bb = $beta * 3.141593 / 180;
420              
421 149         318 my $bcp = (4.0/3 * (1 - cos(($bb - $aa)/2)) / sin(($bb - $aa)/2));
422 149         231 my $sin_alpha = sin($aa);
423 149         243 my $sin_beta = sin($bb);
424 149         274 my $cos_alpha = cos($aa);
425 149         240 my $cos_beta = cos($bb);
426              
427 149         226 $p0_x = $rx * $cos_alpha;
428 149         204 $p0_y = $ry * $sin_alpha;
429 149         240 $p1_x = $rx * ($cos_alpha - $bcp * $sin_alpha);
430 149         237 $p1_y = $ry * ($sin_alpha + $bcp * $cos_alpha);
431 149         228 $p2_x = $rx * ($cos_beta + $bcp * $sin_beta);
432 149         231 $p2_y = $ry * ($sin_beta - $bcp * $cos_beta);
433 149         213 $p3_x = $rx * $cos_beta;
434 149         241 $p3_y = $ry * $sin_beta;
435              
436 149         674 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 6     6 1 43 my ($self, $x1,$y1, $x2,$y2, $r, $move, $larc, $spf) = @_;
493              
494 6         27 my ($p0_x,$p0_y, $p1_x,$p1_y, $p2_x,$p2_y, $p3_x,$p3_y);
495 6         0 my ($dx,$dy, $x,$y, $alpha,$beta, $alpha_rad, $d,$z, $dir, @points);
496              
497 6 50 33     21 if ($x1 == $x2 && $y1 == $y2) {
498 0         0 die "bogen requires two distinct points";
499             }
500 6 50       19 if ($r <= 0.0) {
501 0         0 die "bogen requires a positive radius";
502             }
503 6 50       16 $move = 0 if !defined $move;
504 6 50       17 $larc = 0 if !defined $larc;
505 6 50       16 $spf = 0 if !defined $spf;
506              
507 6         9 $dx = $x2 - $x1;
508 6         10 $dy = $y2 - $y1;
509 6         21 $z = sqrt($dx**2 + $dy**2);
510 6         30 $alpha_rad = asin($dy/$z); # |dy/z| guaranteed <= 1.0
511 6 100       64 $alpha_rad = pi - $alpha_rad if $dx < 0;
512              
513             # alpha is direction of vector P1 to P2
514 6         22 $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 6 100       86 $alpha -= 180 if $spf;
518              
519 6         13 $d = 2*$r;
520             # z/d must be no greater than 1.0 (arcsine arg)
521 6 100       18 if ($z > $d) {
522 1         3 $d = $z; # SILENT error and fixup
523 1         3 $r = $d/2;
524             }
525              
526 6         18 $beta = rad2deg(2*asin($z/$d));
527             # beta is the sweep P1 to P2: ~0 (r very large) to 180 degrees (min r)
528 6 100       85 $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 6         30 @points = _arctocurve($r,$r, 90+$alpha+$beta/2,90+$alpha-$beta/2, 1);
534              
535 6 100       47 if ($spf) { # flip order of points for reverse arc
536 2         16 my @pts = @points;
537 2         9 @points = ();
538 2         7 while (@pts) {
539 80         149 $y = pop @pts;
540 80         124 $x = pop @pts;
541 80         200 push(@points, $x,$y);
542             }
543             }
544              
545 6         11 $p0_x = shift @points;
546 6         12 $p0_y = shift @points;
547 6         16 $x = $x1 - $p0_x;
548 6         11 $y = $y1 - $p0_y;
549              
550 6 50       15 $self->move($x1,$y1) if $move;
551              
552 6         16 while (scalar @points > 0) {
553 56         109 $p1_x = $x + shift @points;
554 56         99 $p1_y = $y + shift @points;
555 56         98 $p2_x = $x + shift @points;
556 56         89 $p2_y = $y + shift @points;
557             # if we run out of data points, use the end point instead
558 56 50       126 if (scalar @points == 0) {
559 0         0 $p3_x = $x2;
560 0         0 $p3_y = $y2;
561             } else {
562 56         111 $p3_x = $x + shift @points;
563 56         103 $p3_y = $y + shift @points;
564             }
565 56         208 $self->curve($p1_x,$p1_y, $p2_x,$p2_y, $p3_x,$p3_y);
566 56         125 shift @points;
567 56         161 shift @points;
568             }
569              
570 6         36 return $self;
571             }
572              
573             1;