perfect-shape 0.5.5 → 1.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +26 -0
- data/README.md +21 -10
- data/VERSION +1 -1
- data/lib/perfect_shape/arc.rb +160 -0
- data/lib/perfect_shape/circle.rb +12 -12
- data/lib/perfect_shape/composite_shape.rb +4 -0
- data/lib/perfect_shape/cubic_bezier_curve.rb +4 -4
- data/lib/perfect_shape/line.rb +3 -3
- data/lib/perfect_shape/path.rb +150 -19
- data/lib/perfect_shape/polygon.rb +109 -70
- data/lib/perfect_shape/quadratic_bezier_curve.rb +67 -0
- data/lib/perfect_shape/square.rb +6 -6
- data/perfect-shape.gemspec +6 -6
- metadata +13 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 73a1e3f4816ce8829788f6ad76e83a88429301000bb49aca7bab878148d90527
|
4
|
+
data.tar.gz: f2628350798a4f5285f3c82941154e21a52aa72b401f3a3fdd889e720554eade
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d79de0f8201d530f55792faea84bb8e9d47608a06f234d305aeb5dcf8ca8fefc612c9eebda0b449ece028a3d281f6b5d8824c41c6b7703d9e575e9cb6cabaa55
|
7
|
+
data.tar.gz: 861950d0dd6107a7de5f5d5f59e192770eb6255024881d67a4e67b3ca229fdc1ff29f176f40223bdb0c8ef67e38cfc46a33313c1d9adb9b37c7fb84f688dc491
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,31 @@
|
|
1
1
|
# Change Log
|
2
2
|
|
3
|
+
## 1.0.2
|
4
|
+
|
5
|
+
- `Path` can contain an `Arc`, `Ellipse`, or `Circle` (to get affected by the winding algorithm as opposed to `CompositeShape` which has no winding algorithm)
|
6
|
+
- `Arc`, `Ellipse`, and `Circle` `#to_path_shapes` method, which decomposes them into `Point`s, `Line`s, and `CubicBezierCurve`s to be added to a `Path`
|
7
|
+
|
8
|
+
## 1.0.1
|
9
|
+
|
10
|
+
- Relax `equalizer` gem version to '>= 0.0.11', '< 1.1.0' to avoid conflicts in projects that might use future versions
|
11
|
+
- Fix issue with infinite loop upon setting `Circle#radius=` to a number with very small decimals
|
12
|
+
- Fix issue with infinite loop upon setting `Circle#radius_x=` to a number with very small decimals
|
13
|
+
- Fix issue with infinite loop upon setting `Circle#radius_y=` to a number with very small decimals
|
14
|
+
- Fix issue with infinite loop upon setting `Circle#diameter=` to a number with very small decimals
|
15
|
+
- Fix issue with infinite loop upon setting `Circle#width=` to a number with very small decimals
|
16
|
+
- Fix issue with infinite loop upon setting `Circle#height=` to a number with very small decimals
|
17
|
+
- Fix issue with infinite loop upon setting `Square#length=` to a number with very small decimals
|
18
|
+
- Fix issue with infinite loop upon setting `Square#width=` to a number with very small decimals
|
19
|
+
- Fix issue with infinite loop upon setting `Square#height=` to a number with very small decimals
|
20
|
+
|
21
|
+
## 1.0.0
|
22
|
+
|
23
|
+
- `PerfectShape::Path#intersect?(rectangle)`
|
24
|
+
- `PerfectShape::Polygon#intersect?(rectangle)`
|
25
|
+
- `PerfectShape::CompositeShape#intersect?(rectangle)`
|
26
|
+
- [API Breaking] Change `Path` default `winding_rule` to `:wind_even_odd`
|
27
|
+
- Make `PerfectShape::Polygon` support `:wind_non_zero` `winding_rule` (not just default `:wind_even_odd`)
|
28
|
+
|
3
29
|
## 0.5.5
|
4
30
|
|
5
31
|
- `PerfectShape::Arc#intersect?(rectangle)`
|
data/README.md
CHANGED
@@ -1,9 +1,9 @@
|
|
1
|
-
# Perfect Shape 0.
|
1
|
+
# Perfect Shape 1.0.2
|
2
2
|
## Geometric Algorithms
|
3
3
|
[![Gem Version](https://badge.fury.io/rb/perfect-shape.svg)](http://badge.fury.io/rb/perfect-shape)
|
4
4
|
[![Test](https://github.com/AndyObtiva/perfect-shape/actions/workflows/ruby.yml/badge.svg)](https://github.com/AndyObtiva/perfect-shape/actions/workflows/ruby.yml)
|
5
5
|
|
6
|
-
[`PerfectShape`](https://rubygems.org/gems/perfect-shape) is a collection of pure Ruby geometric algorithms that are mostly useful for GUI (Graphical User Interface) manipulation like checking viewport rectangle intersection or containment of a mouse click [point](#perfectshapepoint) in popular geometry shapes such as [rectangle](#perfectshaperectangle), [square](#perfectshapesquare), [arc](#perfectshapearc) (open, chord, and pie), [ellipse](#perfectshapeellipse), [circle](#perfectshapecircle), [polygon](#perfectshapepolygon), and [paths](#perfectshapepath) containing [lines](#perfectshapeline), [quadratic bézier curves](#perfectshapequadraticbeziercurve), and [cubic bezier curves](#perfectshapecubicbeziercurve), potentially with [affine transforms](#perfectshapeaffinetransform) applied like translation, scale, rotation, shear/skew, and inversion (including both [Ray Casting Algorithm](https://en.wikipedia.org/wiki/Point_in_polygon#Ray_casting_algorithm), aka [Even-odd Rule](https://en.wikipedia.org/wiki/Even%E2%80%93odd_rule), and [Winding Number Algorithm](https://en.wikipedia.org/wiki/Point_in_polygon#Winding_number_algorithm), aka [Nonzero Rule](https://en.wikipedia.org/wiki/Nonzero-rule)).
|
6
|
+
[`PerfectShape`](https://rubygems.org/gems/perfect-shape) is a collection of pure Ruby geometric algorithms that are mostly useful for GUI (Graphical User Interface) manipulation like checking viewport rectangle intersection or containment of a mouse click [point](#perfectshapepoint) in popular geometry shapes such as [rectangle](#perfectshaperectangle), [square](#perfectshapesquare), [arc](#perfectshapearc) (open, chord, and pie), [ellipse](#perfectshapeellipse), [circle](#perfectshapecircle), [polygon](#perfectshapepolygon), and [paths](#perfectshapepath) containing [lines](#perfectshapeline), [quadratic bézier curves](#perfectshapequadraticbeziercurve), and [cubic bezier curves](#perfectshapecubicbeziercurve), potentially with [affine transforms](#perfectshapeaffinetransform) applied like translation, scale, rotation, shear/skew, and inversion (including both the [Ray Casting Algorithm](https://en.wikipedia.org/wiki/Point_in_polygon#Ray_casting_algorithm), aka [Even-odd Rule](https://en.wikipedia.org/wiki/Even%E2%80%93odd_rule), and the [Winding Number Algorithm](https://en.wikipedia.org/wiki/Point_in_polygon#Winding_number_algorithm), aka [Nonzero Rule](https://en.wikipedia.org/wiki/Nonzero-rule)).
|
7
7
|
|
8
8
|
Additionally, [`PerfectShape::Math`](#perfectshapemath) contains some purely mathematical algorithms, like [IEEE 754-1985 Remainder](https://en.wikipedia.org/wiki/IEEE_754-1985).
|
9
9
|
|
@@ -14,13 +14,13 @@ To ensure high accuracy, this library does all its mathematical operations with
|
|
14
14
|
Run:
|
15
15
|
|
16
16
|
```
|
17
|
-
gem install perfect-shape -v 0.
|
17
|
+
gem install perfect-shape -v 1.0.2
|
18
18
|
```
|
19
19
|
|
20
20
|
Or include in Bundler `Gemfile`:
|
21
21
|
|
22
22
|
```ruby
|
23
|
-
gem 'perfect-shape', '~> 0.
|
23
|
+
gem 'perfect-shape', '~> 1.0.2'
|
24
24
|
```
|
25
25
|
|
26
26
|
And, run:
|
@@ -275,6 +275,7 @@ Includes `PerfectShape::MultiPoint`
|
|
275
275
|
- `#curve_center_y`: point y coordinate at the center of the curve outline (not the center of the bounding box area like `center_x` and `center_y`)
|
276
276
|
- `#subdivisions(level=1)`: subdivides quadratic bezier curve at its center into into 2 quadratic bezier curves by default, or more if `level` of recursion is specified. The resulting number of subdivisions is `2` to the power of `level`.
|
277
277
|
- `#point_distance(x_or_point, y=nil, minimum_distance_threshold: OUTLINE_MINIMUM_DISTANCE_THRESHOLD)`: calculates distance from point to curve segment. It does so by subdividing curve into smaller curves and checking against the curve center points until the distance is less than `minimum_distance_threshold`, to avoid being an overly costly operation.
|
278
|
+
- `#rect_crossings(rxmin, rymin, rxmax, rymax, level, crossings = 0)`: rectangle crossings (adds to crossings arg)
|
278
279
|
|
279
280
|
Example:
|
280
281
|
|
@@ -466,6 +467,8 @@ Open Arc | Chord Arc | Pie Arc
|
|
466
467
|
- `#start`: start angle in degrees
|
467
468
|
- `#extent`: extent angle in degrees
|
468
469
|
- `#center_point`: center point as `Array` of `[center_x, center_y]` coordinates
|
470
|
+
- `#start_point`: start point as `Array` of (x,y) coordinates
|
471
|
+
- `#end_point`: end point as `Array` of (x,y) coordinates
|
469
472
|
- `#center_x`: center x
|
470
473
|
- `#center_y`: center y
|
471
474
|
- `#radius_x`: radius along the x-axis
|
@@ -479,6 +482,8 @@ Open Arc | Chord Arc | Pie Arc
|
|
479
482
|
- `#contain?(x_or_point, y=nil, outline: false, distance_tolerance: 0)`: checks if point is inside when `outline` is `false` or if point is on the outline when `outline` is `true`. `distance_tolerance` can be used as a fuzz factor when `outline` is `true`, for example, to help GUI users mouse-click-select an arc shape from its outline more successfully
|
480
483
|
- `#intersect?(rectangle)`: Returns `true` if intersecting with interior of rectangle or `false` otherwise. This is useful for GUI optimization checks of whether a shape appears in a GUI viewport rectangle and needs redrawing
|
481
484
|
- `#contain_angle?(angle)`: returns `true` if the angle is within the angular extents of the arc and `false` otherwise
|
485
|
+
- `#to_path_shapes`: Converts `Arc` into basic `Path` shapes made up of `Point`s, `Line`s, and `CubicBezierCurve`s. Used by `Path` when adding an `Arc` to `Path` `shapes`
|
486
|
+
- `#btan(increment)`: btan computes the length (k) of the control segments at the beginning and end of a cubic bezier that approximates a segment of an arc with extent less than or equal to 90 degrees. This length (k) will be used to generate the 2 bezier control points for such a segment.
|
482
487
|
|
483
488
|
Example:
|
484
489
|
|
@@ -603,6 +608,7 @@ Extends `PerfectShape::Arc`
|
|
603
608
|
- `#==(other)`: Returns `true` if equal to `other` or `false` otherwise
|
604
609
|
- `#contain?(x_or_point, y=nil, outline: false, distance_tolerance: 0)`: checks if point is inside when `outline` is `false` or if point is on the outline when `outline` is `true`. `distance_tolerance` can be used as a fuzz factor when `outline` is `true`, for example, to help GUI users mouse-click-select an ellipse shape from its outline more successfully
|
605
610
|
- `#intersect?(rectangle)`: Returns `true` if intersecting with interior of rectangle or `false` otherwise. This is useful for GUI optimization checks of whether a shape appears in a GUI viewport rectangle and needs redrawing
|
611
|
+
- `#to_path_shapes`: Converts `Ellipse` into basic `Path` shapes made up of `Point`s, `Line`s, and `CubicBezierCurve`s. Used by `Path` when adding an `Ellipse` to `Path` `shapes`
|
606
612
|
|
607
613
|
Example:
|
608
614
|
|
@@ -665,6 +671,7 @@ Extends `PerfectShape::Ellipse`
|
|
665
671
|
- `#==(other)`: Returns `true` if equal to `other` or `false` otherwise
|
666
672
|
- `#contain?(x_or_point, y=nil, outline: false, distance_tolerance: 0)`: checks if point is inside when `outline` is `false` or if point is on the outline when `outline` is `true`. `distance_tolerance` can be used as a fuzz factor when `outline` is `true`, for example, to help GUI users mouse-click-select a circle shape from its outline more successfully
|
667
673
|
- `#intersect?(rectangle)`: Returns `true` if intersecting with interior of rectangle or `false` otherwise. This is useful for GUI optimization checks of whether a shape appears in a GUI viewport rectangle and needs redrawing
|
674
|
+
- `#to_path_shapes`: Converts `Circle` into basic `Path` shapes made up of `Point`s, `Line`s, and `CubicBezierCurve`s. Used by `Path` when adding a `Circle` to `Path` `shapes`
|
668
675
|
|
669
676
|
Example:
|
670
677
|
|
@@ -704,11 +711,11 @@ Extends `PerfectShape::Shape`
|
|
704
711
|
|
705
712
|
Includes `PerfectShape::MultiPoint`
|
706
713
|
|
707
|
-
A polygon can be thought of as a special case of [path](#perfectshapepath)
|
714
|
+
A polygon can be thought of as a special case of [path](#perfectshapepath), consisting of lines only, is closed, and has the [Even-Odd](https://en.wikipedia.org/wiki/Even%E2%80%93odd_rule) winding rule by default.
|
708
715
|
|
709
716
|
![polygon](https://raw.githubusercontent.com/AndyObtiva/perfect-shape/master/images/polygon.png)
|
710
717
|
|
711
|
-
- `::new(points: [])`: constructs a polygon with `points` as `Array` of `Array`s of `[x,y]` pairs or flattened `Array` of alternating x and y coordinates
|
718
|
+
- `::new(points: [], winding_rule: :wind_even_odd)`: constructs a polygon with `points` as `Array` of `Array`s of `[x,y]` pairs or flattened `Array` of alternating x and y coordinates and specified winding rule (`:wind_even_odd` or `:wind_non_zero`)
|
712
719
|
- `#min_x`: min x
|
713
720
|
- `#min_y`: min y
|
714
721
|
- `#max_x`: max x
|
@@ -720,7 +727,8 @@ A polygon can be thought of as a special case of [path](#perfectshapepath) that
|
|
720
727
|
- `#center_y`: center y
|
721
728
|
- `#bounding_box`: bounding box is a rectangle with x = min x, y = min y, and width/height of shape
|
722
729
|
- `#==(other)`: Returns `true` if equal to `other` or `false` otherwise
|
723
|
-
- `#contain?(x_or_point, y=nil, outline: false, distance_tolerance: 0)`: When `outline` is `false`, it checks if point is inside using the [Ray Casting Algorithm](https://en.wikipedia.org/wiki/Point_in_polygon) (aka [Even-Odd Rule](https://en.wikipedia.org/wiki/Even%E2%80%93odd_rule)). Otherwise, when `outline` is `true`, it checks if point is on the outline. `distance_tolerance` can be used as a fuzz factor when `outline` is `true`, for example, to help GUI users mouse-click-select a polygon shape from its outline more successfully
|
730
|
+
- `#contain?(x_or_point, y=nil, outline: false, distance_tolerance: 0)`: When `outline` is `false`, it checks if point is inside using either the [Ray Casting Algorithm](https://en.wikipedia.org/wiki/Point_in_polygon) (aka [Even-Odd Rule](https://en.wikipedia.org/wiki/Even%E2%80%93odd_rule)) or [Winding Number Algorithm](https://en.wikipedia.org/wiki/Point_in_polygon#Winding_number_algorithm) (aka [Nonzero-Rule](https://en.wikipedia.org/wiki/Nonzero-rule)). Otherwise, when `outline` is `true`, it checks if point is on the outline. `distance_tolerance` can be used as a fuzz factor when `outline` is `true`, for example, to help GUI users mouse-click-select a polygon shape from its outline more successfully
|
731
|
+
- `#intersect?(rectangle)`: Returns `true` if intersecting with interior of rectangle or `false` otherwise. This is useful for GUI optimization checks of whether a shape appears in a GUI viewport rectangle and needs redrawing
|
724
732
|
- `#edges`: edges of polygon as `PerfectShape::Line` objects
|
725
733
|
|
726
734
|
Example:
|
@@ -752,8 +760,9 @@ Includes `PerfectShape::MultiPoint`
|
|
752
760
|
|
753
761
|
![path](https://raw.githubusercontent.com/AndyObtiva/perfect-shape/master/images/path.png)
|
754
762
|
|
755
|
-
- `::new(shapes: [], closed: false, winding_rule: :
|
756
|
-
- `#shapes`: the shapes that the path is composed of (must always start with `PerfectShape::Point` or Array of [x,y] coordinates representing start point)
|
763
|
+
- `::new(shapes: [], closed: false, winding_rule: :wind_even_odd, line_to_complex_shapes: false)`: constructs a path with `shapes` as `Array` of shape objects, which can be `PerfectShape::Point` (or `Array` of `[x, y]` coordinates), `PerfectShape::Line`, `PerfectShape::QuadraticBezierCurve`, `PerfectShape::CubicBezierCurve`, or complex shapes that decompose into the aforementioned basic path shapes, like `PerfectShape::Arc`, `PerfectShape::Ellipse`, and `PerfectShape::Circle`. If a path is closed, its last point is automatically connected to its first point with a line segment. The winding rule can be `:wind_non_zero` (default) or `:wind_even_odd`. `line_to_complex_shapes` can be `true` or `false` (default), indicating whether to connect to complex shapes, meaning `Arc`, `Ellipse`, and `Circle`, with a line, or otherwise move to their start point instead.
|
764
|
+
- `#shapes`: the shapes that the path is composed of (must always start with `PerfectShape::Point` or Array of `[x,y]` coordinates representing start point)
|
765
|
+
- `#basic_shapes`: the basic shapes that the path is composed of, meaning only `Point`, `Line`, `QuadraticBezierCurve`, and `CubicBezierCurve` shapes (decomposing complex shapes like `Arc`, `Ellipse`, and `Circle` using their `#to_path_shapes` method)
|
757
766
|
- `#closed?`: returns `true` if closed and `false` otherwise
|
758
767
|
- `#winding_rule`: returns winding rule (`:wind_non_zero` or `:wind_even_odd`)
|
759
768
|
- `#points`: path points calculated (derived) from shapes
|
@@ -769,6 +778,7 @@ Includes `PerfectShape::MultiPoint`
|
|
769
778
|
- `#bounding_box`: bounding box is a rectangle with x = min x, y = min y, and width/height of shape (bounding box only guarantees that the shape is within it, but it might be bigger than the shape)
|
770
779
|
- `#==(other)`: Returns `true` if equal to `other` or `false` otherwise
|
771
780
|
- `#contain?(x_or_point, y=nil, outline: false, distance_tolerance: 0)`: When `outline` is `false`, it checks if point is inside path utilizing the configured winding rule, which can be the [Nonzero-Rule](https://en.wikipedia.org/wiki/Nonzero-rule) (aka [Winding Number Algorithm](https://en.wikipedia.org/wiki/Point_in_polygon#Winding_number_algorithm)) or the [Even-Odd Rule](https://en.wikipedia.org/wiki/Even%E2%80%93odd_rule) (aka [Ray Casting Algorithm](https://en.wikipedia.org/wiki/Point_in_polygon#Ray_casting_algorithm)). Otherwise, when `outline` is `true`, it checks if point is on the outline. `distance_tolerance` can be used as a fuzz factor when `outline` is `true`, for example, to help GUI users mouse-click-select a path shape from its outline more successfully
|
781
|
+
- `#intersect?(rectangle)`: Returns `true` if intersecting with interior of rectangle or `false` otherwise. This is useful for GUI optimization checks of whether a shape appears in a GUI viewport rectangle and needs redrawing
|
772
782
|
- `#point_crossings(x_or_point, y=nil)`: calculates the number of times the given path crosses the ray extending to the right from (x,y)
|
773
783
|
- `#disconnected_shapes`: Disconnected shapes have their start point filled in so that each shape does not depend on the previous shape to determine its start point. Also, if a point is followed by a non-point shape, it is removed since it is augmented to the following shape as its start point. Lastly, if the path is closed, an extra shape is added to represent the line connecting the last point to the first
|
774
784
|
|
@@ -783,7 +793,7 @@ path_shapes << PerfectShape::Line.new(points: [250, 170]) # no need for start po
|
|
783
793
|
path_shapes << PerfectShape::QuadraticBezierCurve.new(points: [[300, 185], [350, 150]]) # no need for start point, just control point and end point
|
784
794
|
path_shapes << PerfectShape::CubicBezierCurve.new(points: [[370, 50], [430, 220], [480, 170]]) # no need for start point, just two control points and end point
|
785
795
|
|
786
|
-
shape = PerfectShape::Path.new(shapes: path_shapes, closed: false, winding_rule: :
|
796
|
+
shape = PerfectShape::Path.new(shapes: path_shapes, closed: false, winding_rule: :wind_non_zero)
|
787
797
|
|
788
798
|
shape.contain?(275, 165) # => true
|
789
799
|
shape.contain?([275, 165]) # => true
|
@@ -821,6 +831,7 @@ A composite shape is simply an aggregate of multiple shapes (e.g. square and pol
|
|
821
831
|
- `#bounding_box`: bounding box is a rectangle with x = min x, y = min y, and width/height of shape (bounding box only guarantees that the shape is within it, but it might be bigger than the shape)
|
822
832
|
- `#==(other)`: Returns `true` if equal to `other` or `false` otherwise
|
823
833
|
- `#contain?(x_or_point, y=nil, outline: false, distance_tolerance: 0)`: When `outline` is `false`, it checks if point is inside any of the shapes owned by the composite shape. Otherwise, when `outline` is `true`, it checks if point is on the outline of any of the shapes owned by the composite shape. `distance_tolerance` can be used as a fuzz factor when `outline` is `true`, for example, to help GUI users mouse-click-select a composite shape from its outline more successfully
|
834
|
+
- `#intersect?(rectangle)`: Returns `true` if intersecting with interior of rectangle or `false` otherwise. This is useful for GUI optimization checks of whether a shape appears in a GUI viewport rectangle and needs redrawing
|
824
835
|
|
825
836
|
Example:
|
826
837
|
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.
|
1
|
+
1.0.2
|
data/lib/perfect_shape/arc.rb
CHANGED
@@ -312,5 +312,165 @@ module PerfectShape
|
|
312
312
|
y = self.y + (Math.sin(angle) * 0.5 + 0.5) * self.height
|
313
313
|
[x, y]
|
314
314
|
end
|
315
|
+
|
316
|
+
# Converts Arc into basic Path shapes made up of Points, Lines, and CubicBezierCurves
|
317
|
+
# Used by Path when adding an Arc to Path shapes
|
318
|
+
def to_path_shapes
|
319
|
+
w = BigDecimal(self.width.to_s) / 2
|
320
|
+
h = BigDecimal(self.height.to_s) / 2
|
321
|
+
x = self.x + w
|
322
|
+
y = self.x + h
|
323
|
+
ang_st_rad = -Math.degrees_to_radians(self.start)
|
324
|
+
ext = -self.extent
|
325
|
+
if ext >= 360.0 || ext <= -360
|
326
|
+
arc_segs = 4
|
327
|
+
increment = Math::PI / 2
|
328
|
+
cv = 0.5522847498307933
|
329
|
+
if ext < 0
|
330
|
+
increment = -increment
|
331
|
+
cv = -cv
|
332
|
+
end
|
333
|
+
else
|
334
|
+
arc_segs = (ext.abs / 90.0).ceil
|
335
|
+
increment = Math.degrees_to_radians(ext / arc_segs)
|
336
|
+
cv = btan(increment)
|
337
|
+
arc_segs = 0 if cv == 0
|
338
|
+
end
|
339
|
+
line_segs = nil
|
340
|
+
case self.type
|
341
|
+
when :open
|
342
|
+
line_segs = 0
|
343
|
+
when :chord
|
344
|
+
line_segs = 1
|
345
|
+
when :pie
|
346
|
+
line_segs = 2
|
347
|
+
end
|
348
|
+
arc_segs = line_segs = -1 if w < 0 || h < 0
|
349
|
+
|
350
|
+
first_point_x = first_point_y = nil
|
351
|
+
last_point_x = last_point_y = nil
|
352
|
+
(arc_segs + line_segs + 1).to_i.times.map do |index|
|
353
|
+
coords = []
|
354
|
+
angle = ang_st_rad
|
355
|
+
if index == 0
|
356
|
+
first_point_x = coords[0] = x + Math.cos(angle) * w
|
357
|
+
first_point_y = coords[1] = y + Math.sin(angle) * h
|
358
|
+
Point.new(*coords)
|
359
|
+
elsif (index > arc_segs) && (extent - start) != 0 && ((extent - start)%360 == 0)
|
360
|
+
nil
|
361
|
+
elsif (index > arc_segs) && (index < arc_segs + line_segs) && (extent - start) == 0
|
362
|
+
nil
|
363
|
+
elsif (index > arc_segs) && (index == arc_segs + line_segs)
|
364
|
+
Line.new(points: [[first_point_x, first_point_y]])
|
365
|
+
elsif index > arc_segs
|
366
|
+
coords[0] = x
|
367
|
+
coords[1] = y
|
368
|
+
if line_segs == 2
|
369
|
+
last_point_x = coords[0]
|
370
|
+
last_point_y = coords[1]
|
371
|
+
end
|
372
|
+
Line.new(points: coords)
|
373
|
+
else
|
374
|
+
angle += increment * (index - 1)
|
375
|
+
relx = Math.cos(angle)
|
376
|
+
rely = Math.sin(angle)
|
377
|
+
coords[0] = x + (relx - cv * rely) * w
|
378
|
+
coords[1] = y + (rely + cv * relx) * h
|
379
|
+
angle += increment
|
380
|
+
relx = Math.cos(angle)
|
381
|
+
rely = Math.sin(angle)
|
382
|
+
coords[2] = x + (relx + cv * rely) * w
|
383
|
+
coords[3] = y + (rely - cv * relx) * h
|
384
|
+
coords[4] = x + relx * w
|
385
|
+
coords[5] = y + rely * h
|
386
|
+
if line_segs == 1
|
387
|
+
last_point_x = coords[4]
|
388
|
+
last_point_y = coords[5]
|
389
|
+
end
|
390
|
+
CubicBezierCurve.new(points: coords)
|
391
|
+
end
|
392
|
+
end.compact
|
393
|
+
end
|
394
|
+
|
395
|
+
# btan computes the length (k) of the control segments at
|
396
|
+
# the beginning and end of a cubic bezier that approximates
|
397
|
+
# a segment of an arc with extent less than or equal to
|
398
|
+
# 90 degrees. This length (k) will be used to generate the
|
399
|
+
# 2 bezier control points for such a segment.
|
400
|
+
#
|
401
|
+
# Assumptions:
|
402
|
+
# a) arc is centered on 0,0 with radius of 1.0
|
403
|
+
# b) arc extent is less than 90 degrees
|
404
|
+
# c) control points should preserve tangent
|
405
|
+
# d) control segments should have equal length
|
406
|
+
#
|
407
|
+
# Initial data:
|
408
|
+
# start angle: ang1
|
409
|
+
# end angle: ang2 = ang1 + extent
|
410
|
+
# start point: P1 = (x1, y1) = (cos(ang1), sin(ang1))
|
411
|
+
# end point: P4 = (x4, y4) = (cos(ang2), sin(ang2))
|
412
|
+
#
|
413
|
+
# Control points:
|
414
|
+
# P2 = (x2, y2)
|
415
|
+
# | x2 = x1 - k * sin(ang1) = cos(ang1) - k * sin(ang1)
|
416
|
+
# | y2 = y1 + k * cos(ang1) = sin(ang1) + k * cos(ang1)
|
417
|
+
#
|
418
|
+
# P3 = (x3, y3)
|
419
|
+
# | x3 = x4 + k * sin(ang2) = cos(ang2) + k * sin(ang2)
|
420
|
+
# | y3 = y4 - k * cos(ang2) = sin(ang2) - k * cos(ang2)
|
421
|
+
#
|
422
|
+
# The formula for this length (k) can be found using the
|
423
|
+
# following derivations:
|
424
|
+
#
|
425
|
+
# Midpoints:
|
426
|
+
# a) bezier (t = 1/2)
|
427
|
+
# bPm = P1 * (1-t)^3 +
|
428
|
+
# 3 * P2 * t * (1-t)^2 +
|
429
|
+
# 3 * P3 * t^2 * (1-t) +
|
430
|
+
# P4 * t^3 =
|
431
|
+
# = (P1 + 3P2 + 3P3 + P4)/8
|
432
|
+
#
|
433
|
+
# b) arc
|
434
|
+
# aPm = (cos((ang1 + ang2)/2), sin((ang1 + ang2)/2))
|
435
|
+
#
|
436
|
+
# Let angb = (ang2 - ang1)/2; angb is half of the angle
|
437
|
+
# between ang1 and ang2.
|
438
|
+
#
|
439
|
+
# Solve the equation bPm == aPm
|
440
|
+
#
|
441
|
+
# a) For xm coord:
|
442
|
+
# x1 + 3*x2 + 3*x3 + x4 = 8*cos((ang1 + ang2)/2)
|
443
|
+
#
|
444
|
+
# cos(ang1) + 3*cos(ang1) - 3*k*sin(ang1) +
|
445
|
+
# 3*cos(ang2) + 3*k*sin(ang2) + cos(ang2) =
|
446
|
+
# = 8*cos((ang1 + ang2)/2)
|
447
|
+
#
|
448
|
+
# 4*cos(ang1) + 4*cos(ang2) + 3*k*(sin(ang2) - sin(ang1)) =
|
449
|
+
# = 8*cos((ang1 + ang2)/2)
|
450
|
+
#
|
451
|
+
# 8*cos((ang1 + ang2)/2)*cos((ang2 - ang1)/2) +
|
452
|
+
# 6*k*sin((ang2 - ang1)/2)*cos((ang1 + ang2)/2) =
|
453
|
+
# = 8*cos((ang1 + ang2)/2)
|
454
|
+
#
|
455
|
+
# 4*cos(angb) + 3*k*sin(angb) = 4
|
456
|
+
#
|
457
|
+
# k = 4 / 3 * (1 - cos(angb)) / sin(angb)
|
458
|
+
#
|
459
|
+
# b) For ym coord we derive the same formula.
|
460
|
+
#
|
461
|
+
# Since this formula can generate "NaN" values for small
|
462
|
+
# angles, we will derive a safer form that does not involve
|
463
|
+
# dividing by very small values:
|
464
|
+
# (1 - cos(angb)) / sin(angb) =
|
465
|
+
# = (1 - cos(angb))*(1 + cos(angb)) / sin(angb)*(1 + cos(angb)) =
|
466
|
+
# = (1 - cos(angb)^2) / sin(angb)*(1 + cos(angb)) =
|
467
|
+
# = sin(angb)^2 / sin(angb)*(1 + cos(angb)) =
|
468
|
+
# = sin(angb) / (1 + cos(angb))
|
469
|
+
#
|
470
|
+
def btan(increment)
|
471
|
+
return 0 if increment.nan?
|
472
|
+
increment /= BigDecimal('2.0')
|
473
|
+
BigDecimal('4.0') / BigDecimal('3.0') * Math.sin(increment) / (BigDecimal('1.0') + Math.cos(increment))
|
474
|
+
end
|
315
475
|
end
|
316
476
|
end
|
data/lib/perfect_shape/circle.rb
CHANGED
@@ -52,40 +52,40 @@ module PerfectShape
|
|
52
52
|
def diameter=(value)
|
53
53
|
@diameter = BigDecimal(value.to_s)
|
54
54
|
@radius = nil
|
55
|
-
self.width =
|
56
|
-
self.height =
|
55
|
+
self.width = @diameter unless width == @diameter
|
56
|
+
self.height = @diameter unless height == @diameter
|
57
57
|
end
|
58
58
|
|
59
59
|
# Sets radius, normalizing to BigDecimal
|
60
60
|
def radius=(value)
|
61
61
|
@radius = BigDecimal(value.to_s)
|
62
62
|
@diameter = nil
|
63
|
-
self.radius_x =
|
64
|
-
self.radius_y =
|
63
|
+
self.radius_x = @radius unless @width == @radius
|
64
|
+
self.radius_y = @radius unless @height == @radius
|
65
65
|
end
|
66
66
|
|
67
67
|
def width=(value)
|
68
68
|
super
|
69
|
-
self.diameter =
|
70
|
-
self.height =
|
69
|
+
self.diameter = @width unless diameter == @width
|
70
|
+
self.height = @width unless height == @width
|
71
71
|
end
|
72
72
|
|
73
73
|
def height=(value)
|
74
74
|
super
|
75
|
-
self.diameter =
|
76
|
-
self.width =
|
75
|
+
self.diameter = @height unless diameter == @height
|
76
|
+
self.width = @height unless width == @height
|
77
77
|
end
|
78
78
|
|
79
79
|
def radius_x=(value)
|
80
80
|
super
|
81
|
-
self.radius =
|
82
|
-
self.radius_y =
|
81
|
+
self.radius = @radius_x unless radius == @radius_x
|
82
|
+
self.radius_y = @radius_x unless radius_y == @radius_x
|
83
83
|
end
|
84
84
|
|
85
85
|
def radius_y=(value)
|
86
86
|
super
|
87
|
-
self.radius =
|
88
|
-
self.radius_x =
|
87
|
+
self.radius = @radius_y unless radius == @radius_y
|
88
|
+
self.radius_x = @radius_y unless radius_x == @radius_y
|
89
89
|
end
|
90
90
|
end
|
91
91
|
end
|
@@ -223,7 +223,7 @@ module PerfectShape
|
|
223
223
|
|
224
224
|
num_crossings = rectangle_crossings(rectangle)
|
225
225
|
# the intended return value is
|
226
|
-
# num_crossings != 0 || num_crossings == Rectangle::RECT_INTERSECTS
|
226
|
+
# num_crossings != 0 || num_crossings == PerfectShape::Rectangle::RECT_INTERSECTS
|
227
227
|
# but if (num_crossings != 0) num_crossings == INTERSECTS won't matter
|
228
228
|
# and if !(num_crossings != 0) then num_crossings == 0, so
|
229
229
|
# num_crossings != RECT_INTERSECT
|
@@ -244,7 +244,7 @@ module PerfectShape
|
|
244
244
|
if !(x1 == x2 && y1 == y2)
|
245
245
|
line = PerfectShape::Line.new(points: [[x1, y1], [x2, y2]])
|
246
246
|
crossings = line.rect_crossings(x, y, x+w, y+h, crossings)
|
247
|
-
return crossings if crossings == Rectangle::RECT_INTERSECTS
|
247
|
+
return crossings if crossings == PerfectShape::Rectangle::RECT_INTERSECTS
|
248
248
|
end
|
249
249
|
# we call this with the curve's direction reversed, because we wanted
|
250
250
|
# to call rectCrossingsForLine first, because it's cheaper.
|
@@ -294,7 +294,7 @@ module PerfectShape
|
|
294
294
|
# The intersection of ranges is more complicated
|
295
295
|
# First do trivial INTERSECTS rejection of the cases
|
296
296
|
# where one of the endpoints is inside the rectangle.
|
297
|
-
return Rectangle::RECT_INTERSECTS if ((x0 > rxmin && x0 < rxmax && y0 > rymin && y0 < rymax) ||
|
297
|
+
return PerfectShape::Rectangle::RECT_INTERSECTS if ((x0 > rxmin && x0 < rxmax && y0 > rymin && y0 < rymax) ||
|
298
298
|
(x1 > rxmin && x1 < rxmax && y1 > rymin && y1 < rymax))
|
299
299
|
|
300
300
|
# Otherwise, subdivide and look for one of the cases above.
|
@@ -318,7 +318,7 @@ module PerfectShape
|
|
318
318
|
return 0 if xmid.nan? || ymid.nan?
|
319
319
|
cubic1 = CubicBezierCurve.new(points: [[x0, y0], [xc0, yc0], [xc0m, yc0m], [xmid, ymid]])
|
320
320
|
crossings = cubic1.rect_crossings(rxmin, rymin, rxmax, rymax, level + 1, crossings)
|
321
|
-
if crossings != Rectangle::RECT_INTERSECTS
|
321
|
+
if crossings != PerfectShape::Rectangle::RECT_INTERSECTS
|
322
322
|
cubic2 = CubicBezierCurve.new(points: [[xmid, ymid], [xmc1, ymc1], [xc1, yc1], [x1, y1]])
|
323
323
|
crossings = cubic2.rect_crossings(rxmin, rymin, rxmax, rymax, level + 1, crossings)
|
324
324
|
end
|
data/lib/perfect_shape/line.rb
CHANGED
@@ -244,7 +244,7 @@ module PerfectShape
|
|
244
244
|
|
245
245
|
# Accumulate the number of times the line crosses the shadow
|
246
246
|
# extending to the right of the rectangle. See the comment
|
247
|
-
# for the Rectangle::RECT_INTERSECTS constant for more complete details.
|
247
|
+
# for the PerfectShape::Rectangle::RECT_INTERSECTS constant for more complete details.
|
248
248
|
#
|
249
249
|
# crossings arg is the initial crossings value to add to (useful
|
250
250
|
# in cases where you want to accumulate crossings from multiple
|
@@ -280,7 +280,7 @@ module PerfectShape
|
|
280
280
|
# Both x and y ranges overlap by a non-empty amount
|
281
281
|
# First do trivial INTERSECTS rejection of the cases
|
282
282
|
# where one of the endpoints is inside the rectangle.
|
283
|
-
return Rectangle::RECT_INTERSECTS if ((x0 > rxmin && x0 < rxmax && y0 > rymin && y0 < rymax) ||
|
283
|
+
return PerfectShape::Rectangle::RECT_INTERSECTS if ((x0 > rxmin && x0 < rxmax && y0 > rymin && y0 < rymax) ||
|
284
284
|
(x1 > rxmin && x1 < rxmax && y1 > rymin && y1 < rymax))
|
285
285
|
# Otherwise calculate the y intercepts and see where
|
286
286
|
# they fall with respect to the rectangle
|
@@ -311,7 +311,7 @@ module PerfectShape
|
|
311
311
|
end
|
312
312
|
return crossings
|
313
313
|
end
|
314
|
-
Rectangle::RECT_INTERSECTS
|
314
|
+
PerfectShape::Rectangle::RECT_INTERSECTS
|
315
315
|
end
|
316
316
|
|
317
317
|
def intersect?(rectangle)
|
data/lib/perfect_shape/path.rb
CHANGED
@@ -32,28 +32,34 @@ module PerfectShape
|
|
32
32
|
include Equalizer.new(:shapes, :closed, :winding_rule)
|
33
33
|
|
34
34
|
# Available class types for path shapes
|
35
|
-
SHAPE_TYPES = [Array, PerfectShape::Point, PerfectShape::Line, PerfectShape::QuadraticBezierCurve, PerfectShape::CubicBezierCurve]
|
35
|
+
SHAPE_TYPES = [Array, PerfectShape::Point, PerfectShape::Line, PerfectShape::QuadraticBezierCurve, PerfectShape::CubicBezierCurve, PerfectShape::Arc, PerfectShape::Ellipse, PerfectShape::Circle]
|
36
36
|
|
37
37
|
# Available winding rules
|
38
|
-
WINDING_RULES = [:
|
38
|
+
WINDING_RULES = [:wind_even_odd, :wind_non_zero]
|
39
39
|
|
40
40
|
attr_reader :winding_rule
|
41
|
-
attr_accessor :shapes, :closed
|
41
|
+
attr_accessor :shapes, :closed, :line_to_complex_shapes
|
42
42
|
alias closed? closed
|
43
|
+
alias line_to_complex_shapes? line_to_complex_shapes
|
43
44
|
|
44
|
-
# Constructs Path with winding rule, closed status, and shapes (must always start with PerfectShape::Point or Array of [x,y] coordinates)
|
45
|
-
# Shape class types can be any of SHAPE_TYPES: Array (x,y coordinates), PerfectShape::Point, PerfectShape::Line, PerfectShape::QuadraticBezierCurve,
|
45
|
+
# Constructs Path with winding rule, closed status, line_to_complex_shapes option, and shapes (must always start with PerfectShape::Point or Array of [x,y] coordinates)
|
46
|
+
# Shape class types can be any of SHAPE_TYPES: Array (x,y coordinates), PerfectShape::Point, PerfectShape::Line, PerfectShape::QuadraticBezierCurve, PerfectShape::CubicBezierCurve
|
47
|
+
# PerfectShape::Arc, PerfectShape::Ellipse, or PerfectShape::Circle
|
48
|
+
# Complex shapes, meaning Arc, Ellipse, and Circle, are decomposed into basic path shapes, meaning Point, Line, QuadraticBezierCurve, and CubicBezierCurve.
|
46
49
|
# winding_rule can be any of WINDING_RULES: :wind_non_zero (default) or :wind_even_odd
|
47
|
-
# closed can be true or false
|
48
|
-
|
50
|
+
# closed can be true or false (default)
|
51
|
+
# line_to_complex_shapes can be true or false (default), indicating whether to connect to complex shapes,
|
52
|
+
# meaning Arc, Ellipse, and Circle, with a line, or otherwise move to their start point instead.
|
53
|
+
def initialize(shapes: [], closed: false, winding_rule: :wind_even_odd, line_to_complex_shapes: false)
|
49
54
|
self.closed = closed
|
50
55
|
self.winding_rule = winding_rule
|
51
56
|
self.shapes = shapes
|
57
|
+
self.line_to_complex_shapes = line_to_complex_shapes
|
52
58
|
end
|
53
59
|
|
54
60
|
def points
|
55
61
|
the_points = []
|
56
|
-
|
62
|
+
basic_shapes.each do |shape|
|
57
63
|
case shape
|
58
64
|
when Point
|
59
65
|
the_points << shape.to_a
|
@@ -71,7 +77,7 @@ module PerfectShape
|
|
71
77
|
end
|
72
78
|
end
|
73
79
|
end
|
74
|
-
the_points <<
|
80
|
+
the_points << basic_shapes.first.to_a if closed?
|
75
81
|
the_points
|
76
82
|
end
|
77
83
|
|
@@ -80,7 +86,7 @@ module PerfectShape
|
|
80
86
|
end
|
81
87
|
|
82
88
|
def drawing_types
|
83
|
-
the_drawing_shapes =
|
89
|
+
the_drawing_shapes = basic_shapes.map do |shape|
|
84
90
|
case shape
|
85
91
|
when Point
|
86
92
|
:move_to
|
@@ -99,7 +105,7 @@ module PerfectShape
|
|
99
105
|
end
|
100
106
|
|
101
107
|
def winding_rule=(value)
|
102
|
-
raise "Invalid winding rule: #{value}" unless WINDING_RULES.include?(value.to_s.to_sym)
|
108
|
+
raise "Invalid winding rule: #{value} (must be one of #{WINDING_RULES})" unless WINDING_RULES.include?(value.to_s.to_sym)
|
103
109
|
@winding_rule = value
|
104
110
|
end
|
105
111
|
|
@@ -175,8 +181,8 @@ module PerfectShape
|
|
175
181
|
ci += 1
|
176
182
|
line = PerfectShape::Line.new(points: [[curx, cury], [endx, endy]])
|
177
183
|
crossings += line.point_crossings(x, y)
|
178
|
-
curx = endx
|
179
|
-
cury = endy
|
184
|
+
curx = endx
|
185
|
+
cury = endy
|
180
186
|
when :quad_to
|
181
187
|
quad_ctrlx = coords[ci]
|
182
188
|
ci += 1
|
@@ -188,8 +194,8 @@ module PerfectShape
|
|
188
194
|
ci += 1
|
189
195
|
quad = PerfectShape::QuadraticBezierCurve.new(points: [[curx, cury], [quad_ctrlx, quad_ctrly], [endx, endy]])
|
190
196
|
crossings += quad.point_crossings(x, y)
|
191
|
-
curx = endx
|
192
|
-
cury = endy
|
197
|
+
curx = endx
|
198
|
+
cury = endy
|
193
199
|
when :cubic_to
|
194
200
|
cubic_ctrl1x = coords[ci]
|
195
201
|
ci += 1
|
@@ -205,8 +211,8 @@ module PerfectShape
|
|
205
211
|
ci += 1
|
206
212
|
cubic = PerfectShape::CubicBezierCurve.new(points: [[curx, cury], [cubic_ctrl1x, cubic_ctrl1y], [cubic_ctrl2x, cubic_ctrl2y], [endx, endy]])
|
207
213
|
crossings += cubic.point_crossings(x, y)
|
208
|
-
curx = endx
|
209
|
-
cury = endy
|
214
|
+
curx = endx
|
215
|
+
cury = endy
|
210
216
|
when :close
|
211
217
|
if cury != movy
|
212
218
|
line = PerfectShape::Line.new(points: [[curx, cury], [movx, movy]])
|
@@ -233,9 +239,9 @@ module PerfectShape
|
|
233
239
|
# Lastly, if the path is closed, an extra shape is
|
234
240
|
# added to represent the line connecting the last point to the first
|
235
241
|
def disconnected_shapes
|
236
|
-
initial_point = start_point =
|
242
|
+
initial_point = start_point = basic_shapes.first.to_a.map {|n| BigDecimal(n.to_s)}
|
237
243
|
final_point = nil
|
238
|
-
the_disconnected_shapes =
|
244
|
+
the_disconnected_shapes = basic_shapes.drop(1).map do |shape|
|
239
245
|
case shape
|
240
246
|
when Point
|
241
247
|
disconnected_shape = Point.new(*shape.to_a)
|
@@ -267,5 +273,130 @@ module PerfectShape
|
|
267
273
|
the_disconnected_shapes << Line.new(points: [final_point, initial_point]) if closed?
|
268
274
|
the_disconnected_shapes.compact
|
269
275
|
end
|
276
|
+
|
277
|
+
def intersect?(rectangle)
|
278
|
+
x = rectangle.x
|
279
|
+
y = rectangle.y
|
280
|
+
w = rectangle.width
|
281
|
+
h = rectangle.height
|
282
|
+
# [xy]+[wh] is NaN if any of those values are NaN,
|
283
|
+
# or if adding the two together would produce NaN
|
284
|
+
# by virtue of adding opposing Infinte values.
|
285
|
+
# Since we need to add them below, their sum must
|
286
|
+
# not be NaN.
|
287
|
+
# We return false because NaN always produces a
|
288
|
+
# negative response to tests
|
289
|
+
return false if (x+w).nan? || (y+h).nan?
|
290
|
+
return false if w <= 0 || h <= 0
|
291
|
+
mask = winding_rule == :wind_non_zero ? -1 : 2
|
292
|
+
crossings = rect_crossings(x, y, x+w, y+h)
|
293
|
+
crossings == PerfectShape::Rectangle::RECT_INTERSECTS ||
|
294
|
+
(crossings & mask) != 0
|
295
|
+
end
|
296
|
+
|
297
|
+
def rect_crossings(rxmin, rymin, rxmax, rymax)
|
298
|
+
numTypes = drawing_types.count
|
299
|
+
return 0 if numTypes == 0
|
300
|
+
coords = points.flatten
|
301
|
+
curx = cury = movx = movy = endx = endy = nil
|
302
|
+
curx = movx = coords[0]
|
303
|
+
cury = movy = coords[1]
|
304
|
+
crossings = 0
|
305
|
+
ci = 2
|
306
|
+
i = 1
|
307
|
+
|
308
|
+
while crossings != PerfectShape::Rectangle::RECT_INTERSECTS && i < numTypes
|
309
|
+
case drawing_types[i]
|
310
|
+
when :move_to
|
311
|
+
if curx != movx || cury != movy
|
312
|
+
line = PerfectShape::Line.new(points: [curx, cury, movx, movy])
|
313
|
+
crossings = line.rect_crossings(rxmin, rymin, rxmax, rymax, crossings)
|
314
|
+
end
|
315
|
+
# Count should always be a multiple of 2 here.
|
316
|
+
# assert((crossings & 1) != 0)
|
317
|
+
movx = curx = coords[ci]
|
318
|
+
ci += 1
|
319
|
+
movy = cury = coords[ci]
|
320
|
+
ci += 1
|
321
|
+
when :line_to
|
322
|
+
endx = coords[ci]
|
323
|
+
ci += 1
|
324
|
+
endy = coords[ci]
|
325
|
+
ci += 1
|
326
|
+
line = PerfectShape::Line.new(points: [curx, cury, endx, endy])
|
327
|
+
crossings = line.rect_crossings(rxmin, rymin, rxmax, rymax, crossings)
|
328
|
+
curx = endx
|
329
|
+
cury = endy
|
330
|
+
when :quad_to
|
331
|
+
cx = coords[ci]
|
332
|
+
ci += 1
|
333
|
+
cy = coords[ci]
|
334
|
+
ci += 1
|
335
|
+
endx = coords[ci]
|
336
|
+
ci += 1
|
337
|
+
endy = coords[ci]
|
338
|
+
ci += 1
|
339
|
+
quadratic_bezier_curve = PerfectShape::QuadraticBezierCurve.new(points: [curx, cury, cx, cy, endx, endy])
|
340
|
+
crossings = quadratic_bezier_curve.rect_crossings(rxmin, rymin, rxmax, rymax, 0, crossings)
|
341
|
+
curx = endx
|
342
|
+
cury = endy
|
343
|
+
when :cubic_to
|
344
|
+
c1x = coords[ci]
|
345
|
+
ci += 1
|
346
|
+
c1y = coords[ci]
|
347
|
+
ci += 1
|
348
|
+
c2x = coords[ci]
|
349
|
+
ci += 1
|
350
|
+
c2y = coords[ci]
|
351
|
+
ci += 1
|
352
|
+
endx = coords[ci]
|
353
|
+
ci += 1
|
354
|
+
endy = coords[ci]
|
355
|
+
ci += 1
|
356
|
+
cubic_bezier_curve = PerfectShape::CubicBezierCurve.new(points: [curx, cury, c1x, c1y, c2x, c2y, endx, endy])
|
357
|
+
crossings = cubic_bezier_curve.rect_crossings(rxmin, rymin, rxmax, rymax, 0, crossings)
|
358
|
+
curx = endx
|
359
|
+
cury = endy
|
360
|
+
when :close
|
361
|
+
if curx != movx || cury != movy
|
362
|
+
line = PerfectShape::Line.new(points: [curx, cury, movx, movy])
|
363
|
+
crossings = line.rect_crossings(rxmin, rymin, rxmax, rymax, crossings)
|
364
|
+
end
|
365
|
+
curx = movx
|
366
|
+
cury = movy
|
367
|
+
# Count should always be a multiple of 2 here.
|
368
|
+
# assert((crossings & 1) != 0)
|
369
|
+
end
|
370
|
+
i += 1
|
371
|
+
end
|
372
|
+
if crossings != PerfectShape::Rectangle::RECT_INTERSECTS &&
|
373
|
+
(curx != movx || cury != movy)
|
374
|
+
line = PerfectShape::Line.new(points: [curx, cury, movx, movy])
|
375
|
+
crossings = line.rect_crossings(rxmin, rymin, rxmax, rymax, crossings)
|
376
|
+
end
|
377
|
+
# Count should always be a multiple of 2 here.
|
378
|
+
# assert((crossings & 1) != 0)
|
379
|
+
crossings
|
380
|
+
end
|
381
|
+
|
382
|
+
# Returns basic shapes (i.e. Point, Line, QuadraticBezierCurve, and CubicBezierCurve),
|
383
|
+
# decomposed from complex shapes like Arc, Ellipse, and Circle by calling their `#to_path_shapes` method
|
384
|
+
def basic_shapes
|
385
|
+
the_shapes = []
|
386
|
+
@shapes.each do |shape|
|
387
|
+
if shape.respond_to?(:to_path_shapes)
|
388
|
+
shape_basic_shapes = shape.to_path_shapes
|
389
|
+
if @line_to_complex_shapes
|
390
|
+
first_basic_shape = shape_basic_shapes.shift
|
391
|
+
new_first_basic_shape = PerfectShape::Line.new(points: [first_basic_shape.to_a])
|
392
|
+
shape_basic_shapes.unshift(new_first_basic_shape)
|
393
|
+
end
|
394
|
+
the_shapes += shape_basic_shapes
|
395
|
+
else
|
396
|
+
the_shapes << shape
|
397
|
+
end
|
398
|
+
end
|
399
|
+
the_shapes
|
400
|
+
end
|
270
401
|
end
|
271
402
|
end
|
@@ -22,14 +22,31 @@
|
|
22
22
|
require 'perfect_shape/shape'
|
23
23
|
require 'perfect_shape/point'
|
24
24
|
require 'perfect_shape/multi_point'
|
25
|
+
require 'perfect_shape/path'
|
25
26
|
|
26
27
|
module PerfectShape
|
27
28
|
class Polygon < Shape
|
28
29
|
include MultiPoint
|
29
30
|
include Equalizer.new(:points)
|
30
31
|
|
32
|
+
WINDING_RULES = PerfectShape::Path::WINDING_RULES
|
33
|
+
|
34
|
+
attr_reader :winding_rule
|
35
|
+
|
36
|
+
def initialize(points: [], winding_rule: :wind_even_odd)
|
37
|
+
super(points: points)
|
38
|
+
self.winding_rule = winding_rule
|
39
|
+
end
|
40
|
+
|
41
|
+
def winding_rule=(value)
|
42
|
+
raise "Invalid winding rule: #{value} (must be one of #{WINDING_RULES})" unless WINDING_RULES.include?(value.to_s.to_sym)
|
43
|
+
@winding_rule = value
|
44
|
+
end
|
45
|
+
|
31
46
|
# Checks if polygon contains point (two-number Array or x, y args)
|
32
|
-
# using the Ray Casting Algorithm (aka Even-Odd Rule)
|
47
|
+
# using the Ray Casting Algorithm (aka Even-Odd Rule)
|
48
|
+
# or Winding Number Algorithm (aka Nonzero Rule)
|
49
|
+
# Details: https://en.wikipedia.org/wiki/Point_in_polygon
|
33
50
|
#
|
34
51
|
# @param x The X coordinate of the point to test.
|
35
52
|
# @param y The Y coordinate of the point to test.
|
@@ -43,76 +60,11 @@ module PerfectShape
|
|
43
60
|
if outline
|
44
61
|
edges.any? { |edge| edge.contain?(x, y, distance_tolerance: distance_tolerance) }
|
45
62
|
else
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
hits = 0
|
51
|
-
|
52
|
-
lastx = xpoints[npoints - 1]
|
53
|
-
lasty = ypoints[npoints - 1]
|
54
|
-
|
55
|
-
# Walk the edges of the polygon
|
56
|
-
npoints.times do |i|
|
57
|
-
curx = xpoints[i]
|
58
|
-
cury = ypoints[i]
|
59
|
-
|
60
|
-
if cury == lasty
|
61
|
-
lastx = curx
|
62
|
-
lasty = cury
|
63
|
-
next
|
64
|
-
end
|
65
|
-
|
66
|
-
if curx < lastx
|
67
|
-
if x >= lastx
|
68
|
-
lastx = curx
|
69
|
-
lasty = cury
|
70
|
-
next
|
71
|
-
end
|
72
|
-
leftx = curx
|
73
|
-
else
|
74
|
-
if x >= curx
|
75
|
-
lastx = curx
|
76
|
-
lasty = cury
|
77
|
-
next
|
78
|
-
end
|
79
|
-
leftx = lastx
|
80
|
-
end
|
81
|
-
|
82
|
-
if cury < lasty
|
83
|
-
if y < cury || y >= lasty
|
84
|
-
lastx = curx
|
85
|
-
lasty = cury
|
86
|
-
next
|
87
|
-
end
|
88
|
-
if x < leftx
|
89
|
-
hits += 1
|
90
|
-
lastx = curx
|
91
|
-
lasty = cury
|
92
|
-
next
|
93
|
-
end
|
94
|
-
test1 = x - curx
|
95
|
-
test2 = y - cury
|
96
|
-
else
|
97
|
-
if y < lasty || y >= cury
|
98
|
-
lastx = curx
|
99
|
-
lasty = cury
|
100
|
-
next
|
101
|
-
end
|
102
|
-
if x < leftx
|
103
|
-
hits += 1
|
104
|
-
lastx = curx
|
105
|
-
lasty = cury
|
106
|
-
next
|
107
|
-
end
|
108
|
-
test1 = x - lastx
|
109
|
-
test2 = y - lasty
|
110
|
-
end
|
111
|
-
|
112
|
-
hits += 1 if (test1 < (test2 / (lasty - cury) * (lastx - curx)))
|
63
|
+
if winding_rule == :wind_even_odd
|
64
|
+
wind_even_odd_contain?(x, y)
|
65
|
+
else
|
66
|
+
path.contain?(x, y)
|
113
67
|
end
|
114
|
-
|
115
|
-
(hits & 1) != 0
|
116
68
|
end
|
117
69
|
end
|
118
70
|
|
@@ -121,5 +73,92 @@ module PerfectShape
|
|
121
73
|
Line.new(points: [[point1.first, point1.last], [point2.first, point2.last]])
|
122
74
|
end
|
123
75
|
end
|
76
|
+
|
77
|
+
def path
|
78
|
+
path_shapes = []
|
79
|
+
path_shapes << PerfectShape::Point.new(points[0])
|
80
|
+
path_shapes += points[1..-1].map { |point| PerfectShape::Line.new(points: [point]) }
|
81
|
+
PerfectShape::Path.new(shapes: path_shapes, closed: true, winding_rule: winding_rule)
|
82
|
+
end
|
83
|
+
|
84
|
+
def intersect?(rectangle)
|
85
|
+
path.intersect?(rectangle)
|
86
|
+
end
|
87
|
+
|
88
|
+
private
|
89
|
+
|
90
|
+
# optimized even-odd rule point in polygon algorithm (uses less memory than PerfectShape::Path algorithms)
|
91
|
+
def wind_even_odd_contain?(x, y)
|
92
|
+
npoints = points.count
|
93
|
+
xpoints = points.map(&:first)
|
94
|
+
ypoints = points.map(&:last)
|
95
|
+
return false if npoints <= 2 || !bounding_box.contain?(x, y)
|
96
|
+
hits = 0
|
97
|
+
|
98
|
+
lastx = xpoints[npoints - 1]
|
99
|
+
lasty = ypoints[npoints - 1]
|
100
|
+
|
101
|
+
# Walk the edges of the polygon
|
102
|
+
npoints.times do |i|
|
103
|
+
curx = xpoints[i]
|
104
|
+
cury = ypoints[i]
|
105
|
+
|
106
|
+
if cury == lasty
|
107
|
+
lastx = curx
|
108
|
+
lasty = cury
|
109
|
+
next
|
110
|
+
end
|
111
|
+
|
112
|
+
if curx < lastx
|
113
|
+
if x >= lastx
|
114
|
+
lastx = curx
|
115
|
+
lasty = cury
|
116
|
+
next
|
117
|
+
end
|
118
|
+
leftx = curx
|
119
|
+
else
|
120
|
+
if x >= curx
|
121
|
+
lastx = curx
|
122
|
+
lasty = cury
|
123
|
+
next
|
124
|
+
end
|
125
|
+
leftx = lastx
|
126
|
+
end
|
127
|
+
|
128
|
+
if cury < lasty
|
129
|
+
if y < cury || y >= lasty
|
130
|
+
lastx = curx
|
131
|
+
lasty = cury
|
132
|
+
next
|
133
|
+
end
|
134
|
+
if x < leftx
|
135
|
+
hits += 1
|
136
|
+
lastx = curx
|
137
|
+
lasty = cury
|
138
|
+
next
|
139
|
+
end
|
140
|
+
test1 = x - curx
|
141
|
+
test2 = y - cury
|
142
|
+
else
|
143
|
+
if y < lasty || y >= cury
|
144
|
+
lastx = curx
|
145
|
+
lasty = cury
|
146
|
+
next
|
147
|
+
end
|
148
|
+
if x < leftx
|
149
|
+
hits += 1
|
150
|
+
lastx = curx
|
151
|
+
lasty = cury
|
152
|
+
next
|
153
|
+
end
|
154
|
+
test1 = x - lastx
|
155
|
+
test2 = y - lasty
|
156
|
+
end
|
157
|
+
|
158
|
+
hits += 1 if (test1 < (test2 / (lasty - cury) * (lastx - curx)))
|
159
|
+
end
|
160
|
+
|
161
|
+
(hits & 1) != 0
|
162
|
+
end
|
124
163
|
end
|
125
164
|
end
|
@@ -545,5 +545,72 @@ module PerfectShape
|
|
545
545
|
# overlap the Y range of the rectangle.
|
546
546
|
c1tag * c2tag <= 0
|
547
547
|
end
|
548
|
+
|
549
|
+
# Accumulate the number of times the quad crosses the shadow
|
550
|
+
# extending to the right of the rectangle. See the comment
|
551
|
+
# for the RECT_INTERSECTS constant for more complete details.
|
552
|
+
#
|
553
|
+
# crossings arg is the initial crossings value to add to (useful
|
554
|
+
# in cases where you want to accumulate crossings from multiple
|
555
|
+
# shapes)
|
556
|
+
def rect_crossings(rxmin, rymin, rxmax, rymax, level, crossings = 0)
|
557
|
+
x0 = points[0][0]
|
558
|
+
y0 = points[0][1]
|
559
|
+
xc = points[1][0]
|
560
|
+
yc = points[1][1]
|
561
|
+
x1 = points[2][0]
|
562
|
+
y1 = points[2][1]
|
563
|
+
return crossings if y0 >= rymax && yc >= rymax && y1 >= rymax
|
564
|
+
return crossings if y0 <= rymin && yc <= rymin && y1 <= rymin
|
565
|
+
return crossings if x0 <= rxmin && xc <= rxmin && x1 <= rxmin
|
566
|
+
if x0 >= rxmax && xc >= rxmax && x1 >= rxmax
|
567
|
+
# Quad is entirely to the right of the rect
|
568
|
+
# and the vertical range of the 3 Y coordinates of the quad
|
569
|
+
# overlaps the vertical range of the rect by a non-empty amount
|
570
|
+
# We now judge the crossings solely based on the line segment
|
571
|
+
# connecting the endpoints of the quad.
|
572
|
+
# Note that we may have 0, 1, or 2 crossings as the control
|
573
|
+
# point may be causing the Y range intersection while the
|
574
|
+
# two endpoints are entirely above or below.
|
575
|
+
if y0 < y1
|
576
|
+
# y-increasing line segment...
|
577
|
+
crossings += 1 if y0 <= rymin && y1 > rymin
|
578
|
+
crossings += 1 if y0 < rymax && y1 >= rymax
|
579
|
+
elsif y1 < y0
|
580
|
+
# y-decreasing line segment...
|
581
|
+
crossings -= 1 if y1 <= rymin && y0 > rymin
|
582
|
+
crossings -= 1 if y1 < rymax && y0 >= rymax
|
583
|
+
end
|
584
|
+
return crossings
|
585
|
+
end
|
586
|
+
# The intersection of ranges is more complicated
|
587
|
+
# First do trivial INTERSECTS rejection of the cases
|
588
|
+
# where one of the endpoints is inside the rectangle.
|
589
|
+
return PerfectShape::Rectangle::RECT_INTERSECTS if (x0 < rxmax && x0 > rxmin && y0 < rymax && y0 > rymin) ||
|
590
|
+
(x1 < rxmax && x1 > rxmin && y1 < rymax && y1 > rymin)
|
591
|
+
# Otherwise, subdivide and look for one of the cases above.
|
592
|
+
# double precision only has 52 bits of mantissa
|
593
|
+
if level > 52
|
594
|
+
line = PerfectShape::Line.new(points: [x0, y0, x1, y1])
|
595
|
+
return line.rect_crossings(rxmin, rymin, rxmax, rymax, crossings)
|
596
|
+
end
|
597
|
+
x0c = BigDecimal((x0 + xc).to_s) / 2
|
598
|
+
y0c = BigDecimal((y0 + yc).to_s) / 2
|
599
|
+
xc1 = BigDecimal((xc + x1).to_s) / 2
|
600
|
+
yc1 = BigDecimal((yc + y1).to_s) / 2
|
601
|
+
xc = BigDecimal((x0c + xc1).to_s) / 2
|
602
|
+
yc = BigDecimal((y0c + yc1).to_s) / 2
|
603
|
+
# [xy]c are NaN if any of [xy]0c or [xy]c1 are NaN
|
604
|
+
# [xy]0c or [xy]c1 are NaN if any of [xy][0c1] are NaN
|
605
|
+
# These values are also NaN if opposing infinities are added
|
606
|
+
return 0 if xc.nan? || yc.nan?
|
607
|
+
quad1 = QuadraticBezierCurve.new(points: [x0, y0, x0c, y0c, xc, yc])
|
608
|
+
crossings = quad1.rect_crossings(rxmin, rymin, rxmax, rymax, level+1, crossings)
|
609
|
+
if crossings != PerfectShape::Rectangle::RECT_INTERSECTS
|
610
|
+
quad2 = QuadraticBezierCurve.new(points: [xc, yc, xc1, yc1, x1, y1])
|
611
|
+
crossings = quad2.rect_crossings(rxmin, rymin, rxmax, rymax, level+1, crossings)
|
612
|
+
end
|
613
|
+
crossings
|
614
|
+
end
|
548
615
|
end
|
549
616
|
end
|
data/lib/perfect_shape/square.rb
CHANGED
@@ -40,21 +40,21 @@ module PerfectShape
|
|
40
40
|
# Sets length, normalizing to BigDecimal
|
41
41
|
def length=(value)
|
42
42
|
@length = BigDecimal(value.to_s)
|
43
|
-
self.width =
|
44
|
-
self.height =
|
43
|
+
self.width = @length unless width == @length
|
44
|
+
self.height = @length unless height == @length
|
45
45
|
end
|
46
46
|
alias size= length=
|
47
47
|
|
48
48
|
def width=(value)
|
49
49
|
super
|
50
|
-
self.length =
|
51
|
-
self.height =
|
50
|
+
self.length = @width unless length == @width
|
51
|
+
self.height = @width unless height == @width
|
52
52
|
end
|
53
53
|
|
54
54
|
def height=(value)
|
55
55
|
super
|
56
|
-
self.length =
|
57
|
-
self.width =
|
56
|
+
self.length = @height unless length == @height
|
57
|
+
self.width = @height unless width == @height
|
58
58
|
end
|
59
59
|
end
|
60
60
|
end
|
data/perfect-shape.gemspec
CHANGED
@@ -2,17 +2,17 @@
|
|
2
2
|
# DO NOT EDIT THIS FILE DIRECTLY
|
3
3
|
# Instead, edit Juwelier::Tasks in Rakefile, and run 'rake gemspec'
|
4
4
|
# -*- encoding: utf-8 -*-
|
5
|
-
# stub: perfect-shape 0.
|
5
|
+
# stub: perfect-shape 1.0.2 ruby lib
|
6
6
|
|
7
7
|
Gem::Specification.new do |s|
|
8
8
|
s.name = "perfect-shape".freeze
|
9
|
-
s.version = "0.
|
9
|
+
s.version = "1.0.2"
|
10
10
|
|
11
11
|
s.required_rubygems_version = Gem::Requirement.new(">= 0".freeze) if s.respond_to? :required_rubygems_version=
|
12
12
|
s.require_paths = ["lib".freeze]
|
13
13
|
s.authors = ["Andy Maleh".freeze]
|
14
|
-
s.date = "2022-
|
15
|
-
s.description = "Perfect Shape is a collection of pure Ruby geometric algorithms that are mostly useful for GUI manipulation like checking viewport rectangle intersection or containment of a mouse click point in popular geometry shapes such as rectangle, square, arc (open, chord, and pie), ellipse, circle, polygon, and paths containing lines, quadratic b\u00E9zier curves, and cubic bezier curves, potentially with affine transforms applied like translation, scale, rotation, shear/skew, and inversion (including both Ray Casting Algorithm, aka Even-odd Rule, and Winding Number Algorithm, aka Nonzero Rule). Additionally, it contains some purely mathematical algorithms like IEEEremainder (also known as IEEE-754 remainder).".freeze
|
14
|
+
s.date = "2022-04-03"
|
15
|
+
s.description = "Perfect Shape is a collection of pure Ruby geometric algorithms that are mostly useful for GUI manipulation like checking viewport rectangle intersection or containment of a mouse click point in popular geometry shapes such as rectangle, square, arc (open, chord, and pie), ellipse, circle, polygon, and paths containing lines, quadratic b\u00E9zier curves, and cubic bezier curves, potentially with affine transforms applied like translation, scale, rotation, shear/skew, and inversion (including both the Ray Casting Algorithm, aka Even-odd Rule, and the Winding Number Algorithm, aka Nonzero Rule). Additionally, it contains some purely mathematical algorithms like IEEEremainder (also known as IEEE-754 remainder).".freeze
|
16
16
|
s.email = "andy.am@gmail.com".freeze
|
17
17
|
s.extra_rdoc_files = [
|
18
18
|
"CHANGELOG.md",
|
@@ -55,14 +55,14 @@ Gem::Specification.new do |s|
|
|
55
55
|
end
|
56
56
|
|
57
57
|
if s.respond_to? :add_runtime_dependency then
|
58
|
-
s.add_runtime_dependency(%q<equalizer>.freeze, ["
|
58
|
+
s.add_runtime_dependency(%q<equalizer>.freeze, [">= 0.0.11", "< 1.1.0"])
|
59
59
|
s.add_development_dependency(%q<rdoc>.freeze, ["~> 3.12"])
|
60
60
|
s.add_development_dependency(%q<juwelier>.freeze, ["~> 2.1.0"])
|
61
61
|
s.add_development_dependency(%q<minitest>.freeze, ["~> 5.14.4"])
|
62
62
|
s.add_development_dependency(%q<puts_debuggerer>.freeze, ["~> 0.13.1"])
|
63
63
|
s.add_development_dependency(%q<rake-tui>.freeze, ["> 0"])
|
64
64
|
else
|
65
|
-
s.add_dependency(%q<equalizer>.freeze, ["
|
65
|
+
s.add_dependency(%q<equalizer>.freeze, [">= 0.0.11", "< 1.1.0"])
|
66
66
|
s.add_dependency(%q<rdoc>.freeze, ["~> 3.12"])
|
67
67
|
s.add_dependency(%q<juwelier>.freeze, ["~> 2.1.0"])
|
68
68
|
s.add_dependency(%q<minitest>.freeze, ["~> 5.14.4"])
|
metadata
CHANGED
@@ -1,29 +1,35 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: perfect-shape
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 1.0.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Andy Maleh
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2022-
|
11
|
+
date: 2022-04-03 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: equalizer
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
|
-
- -
|
17
|
+
- - ">="
|
18
18
|
- !ruby/object:Gem::Version
|
19
19
|
version: 0.0.11
|
20
|
+
- - "<"
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: 1.1.0
|
20
23
|
type: :runtime
|
21
24
|
prerelease: false
|
22
25
|
version_requirements: !ruby/object:Gem::Requirement
|
23
26
|
requirements:
|
24
|
-
- -
|
27
|
+
- - ">="
|
25
28
|
- !ruby/object:Gem::Version
|
26
29
|
version: 0.0.11
|
30
|
+
- - "<"
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: 1.1.0
|
27
33
|
- !ruby/object:Gem::Dependency
|
28
34
|
name: rdoc
|
29
35
|
requirement: !ruby/object:Gem::Requirement
|
@@ -100,9 +106,9 @@ description: Perfect Shape is a collection of pure Ruby geometric algorithms tha
|
|
100
106
|
square, arc (open, chord, and pie), ellipse, circle, polygon, and paths containing
|
101
107
|
lines, quadratic bézier curves, and cubic bezier curves, potentially with affine
|
102
108
|
transforms applied like translation, scale, rotation, shear/skew, and inversion
|
103
|
-
(including both Ray Casting Algorithm, aka Even-odd Rule, and Winding Number
|
104
|
-
aka Nonzero Rule). Additionally, it contains some purely mathematical
|
105
|
-
like IEEEremainder (also known as IEEE-754 remainder).
|
109
|
+
(including both the Ray Casting Algorithm, aka Even-odd Rule, and the Winding Number
|
110
|
+
Algorithm, aka Nonzero Rule). Additionally, it contains some purely mathematical
|
111
|
+
algorithms like IEEEremainder (also known as IEEE-754 remainder).
|
106
112
|
email: andy.am@gmail.com
|
107
113
|
executables: []
|
108
114
|
extensions: []
|