perfect-shape 0.3.4 → 0.5.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +31 -0
- data/README.md +105 -17
- data/VERSION +1 -1
- data/lib/perfect-shape.rb +1 -1
- data/lib/perfect_shape/affine_transform.rb +235 -0
- data/lib/perfect_shape/arc.rb +2 -2
- data/lib/perfect_shape/composite_shape.rb +4 -3
- data/lib/perfect_shape/cubic_bezier_curve.rb +4 -4
- data/lib/perfect_shape/ellipse.rb +1 -2
- data/lib/perfect_shape/line.rb +30 -5
- data/lib/perfect_shape/multi_point.rb +14 -5
- data/lib/perfect_shape/path.rb +2 -3
- data/lib/perfect_shape/point.rb +21 -2
- data/lib/perfect_shape/polygon.rb +2 -2
- data/lib/perfect_shape/quadratic_bezier_curve.rb +4 -4
- data/lib/perfect_shape/rectangle.rb +42 -2
- data/lib/perfect_shape/shape.rb +0 -15
- data/perfect-shape.gemspec +5 -4
- metadata +11 -8
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 11b0b3324d2d9a6ea74cf92529f4657899021ef9b515e9bac45ce0538927b519
|
4
|
+
data.tar.gz: b89aaf29076f074800f8934e3df56111254f5f162c5efbcd26ee1afda5386734
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: bb359cd9182b49f0d345c66d17a3cfda427a9b366b615d8bd8b63fcf75953747da9273802295135fec146ad25bb0eb6875d83626224acdf159111d23ab61213b
|
7
|
+
data.tar.gz: a2c318a07294e002af53c80421f388f662045f390ec9be24437eb9c7aeeafb9af50db36899fbb90e738b67e1a21a85cb0762b238891fb2b93ce238893c5514c6
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,36 @@
|
|
1
1
|
# Change Log
|
2
2
|
|
3
|
+
## 0.5.1
|
4
|
+
|
5
|
+
- `Point#intersect?(rectangle)` (equivalent to `Rectangle#contain?(point)`)
|
6
|
+
|
7
|
+
## 0.5.0
|
8
|
+
|
9
|
+
- `Line#intersect?(rectangle)`
|
10
|
+
- `Rectangle#out_state(x_or_point, y = nil)`
|
11
|
+
|
12
|
+
## 0.4.0
|
13
|
+
|
14
|
+
- `PerfectShape::AffineTransform#new`
|
15
|
+
- `PerfectShape::AffineTransform#==`
|
16
|
+
- `PerfectShape::AffineTransform#transform_point`
|
17
|
+
- `PerfectShape::AffineTransform#transform_points`
|
18
|
+
- `PerfectShape::AffineTransform#identity!` (alias: `reset!`)
|
19
|
+
- `PerfectShape::AffineTransform#invert!`
|
20
|
+
- `PerfectShape::AffineTransform#invertible?`
|
21
|
+
- `PerfectShape::AffineTransform#multiply!`
|
22
|
+
- `PerfectShape::AffineTransform#translate!`
|
23
|
+
- `PerfectShape::AffineTransform#scale!`
|
24
|
+
- `PerfectShape::AffineTransform#rotate!`
|
25
|
+
- `PerfectShape::AffineTransform#shear!` (alias: `skew!`)
|
26
|
+
- `PerfectShape::AffineTransform#clone`
|
27
|
+
- `PerfectShape::AffineTransform#inverse_transform_point`
|
28
|
+
- `PerfectShape::AffineTransform#inverse_transform_points`
|
29
|
+
|
30
|
+
## 0.3.5
|
31
|
+
|
32
|
+
- Check point containment in composite shape outline with distance tolerance (new method signature: `PerfectShape::CompositeShape#contain?(x_or_point, y = nil, outline: false, distance_tolerance: 0)`)
|
33
|
+
|
3
34
|
## 0.3.4
|
4
35
|
|
5
36
|
- Check point containment in path outline with distance tolerance (new method signature: `PerfectShape::Path#contain?(x_or_point, y = nil, outline: false, distance_tolerance: 0)`)
|
data/README.md
CHANGED
@@ -1,9 +1,9 @@
|
|
1
|
-
# Perfect Shape 0.
|
1
|
+
# Perfect Shape 0.5.1
|
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 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) (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 [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)).
|
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 0.5.1
|
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', '~> 0.5.1'
|
24
24
|
```
|
25
25
|
|
26
26
|
And, run:
|
@@ -57,7 +57,6 @@ This is a base class for all shapes. It is not meant to be used directly. Subcla
|
|
57
57
|
- `#center_y`: center y
|
58
58
|
- `#bounding_box`: bounding box is a rectangle with x = min x, y = min y, and width/height just as those of shape
|
59
59
|
- `#==(other)`: Returns `true` if equal to `other` or `false` otherwise
|
60
|
-
- `#normalize_point(x_or_point, y = nil)`: normalizes point into an `Array` of `[x,y]` coordinates
|
61
60
|
- `#contain?(x_or_point, y=nil, outline: false, distance_tolerance: 0)`: checks if point is inside if `outline` is `false` or if point is on the outline if `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 shape from its outline more successfully
|
62
61
|
|
63
62
|
### `PerfectShape::PointLocation`
|
@@ -86,6 +85,71 @@ Includes `PerfectShape::PointLocation`
|
|
86
85
|
- `#max_x`: max x
|
87
86
|
- `#max_y`: max y
|
88
87
|
|
88
|
+
### `PerfectShape::AffineTransform`
|
89
|
+
|
90
|
+
Class
|
91
|
+
|
92
|
+
Affine transforms have the following matrix:
|
93
|
+
|
94
|
+
[ xxp xyp xt ]<br>
|
95
|
+
[ yxp yyp yt ]
|
96
|
+
|
97
|
+
The matrix is used to transform (x,y) point coordinates as follows:
|
98
|
+
|
99
|
+
[ xxp xyp xt ] * [x] = [ xxp * x + xyp * y + xt ]<br>
|
100
|
+
[ yxp yyp yt ] * [y] = [ yxp * x + yyp * y + yt ]
|
101
|
+
|
102
|
+
`xxp` is the x coordinate x product (`m11`)<br>
|
103
|
+
`xyp` is the x coordinate y product (`m12`)<br>
|
104
|
+
`yxp` is the y coordinate x product (`m21`)<br>
|
105
|
+
`yyp` is the y coordinate y product (`m22`)<br>
|
106
|
+
`xt` is the x coordinate translation (`m13`)<br>
|
107
|
+
`yt` is the y coordinate translation (`m23`)
|
108
|
+
|
109
|
+
Affine transform mutation operations ending with `!` can be chained as they all return `self`.
|
110
|
+
|
111
|
+
- `::new(xxp_element = nil, xyp_element = nil, yxp_element = nil, yyp_element = nil, xt_element = nil, yt_element = nil,
|
112
|
+
xxp: nil, xyp: nil, yxp: nil, yyp: nil, xt: nil, yt: nil,
|
113
|
+
m11: nil, m12: nil, m21: nil, m22: nil, m13: nil, m23: nil)`:
|
114
|
+
The constructor accepts either the (x,y)-operation related argument/kwarg names or traditional matrix element kwarg names. If no arguments are supplied, it constructs an identity matrix (i.e. like calling `::new(xxp: 1, xyp: 0, yxp: 0, yyp: 1, xt: 0, yt: 0)`).
|
115
|
+
- `#matrix_3d`: Returns Ruby `Matrix` object representing affine transform in 3D (used internally for performing multiplication)
|
116
|
+
- `#==(other)`: Returns `true` if equal to `other` or `false` otherwise
|
117
|
+
- `#identity!` (alias: `reset!`): Resets to identity matrix (i.e. like calling `::new(xxp: 1, xyp: 0, yxp: 0, yyp: 1, xt: 0, yt: 0)`)
|
118
|
+
- `#invertible?` Returns `true` if matrix is invertible and `false` otherwise
|
119
|
+
- `#invert!`: Inverts affine transform matrix if invertible or raises an error otherwise
|
120
|
+
- `#multiply!(other)`: Multiplies affine transform with another affine transform, storing resulting changes in matrix elements
|
121
|
+
- `#translate!(x_or_point, y=nil)`: Translates affine transform with (x, y) translation values
|
122
|
+
- `#scale!(x_or_point, y=nil)`: Scales affine transform with (x, y) scale values
|
123
|
+
- `#rotate!(degrees)`: Rotates by angle degrees counter-clockwise if angle value is positive or clockwise if angle value is negative. Note that it returns very close approximate results for rotations that are 90/180/270 degrees (good enough for inverse-transform GUI point containment checks needed when checking if mouse-click-point is inside a transformed shape).
|
124
|
+
- `#shear!(x_or_point, y=nil)`: Shears by x and y factors
|
125
|
+
- `#clone`: Returns a new AffineTransform with the same matrix elements
|
126
|
+
- `#transform_point(x_or_point, y=nil)`: returns `[xxp * x + xyp * y + xt, yxp * x + yyp * y + yt]`. Note that result is a close approximation, but should be good enough for GUI mouse-click-point containment checks.
|
127
|
+
- `#transform_points(*xy_coordinates_or_points)`: returns `Array` of (x,y) pair `Array`s transformed with `#transform_point` method
|
128
|
+
- `#inverse_transform_point(x_or_point, y=nil)`: returns inverse transform of a point (x,y) coordinates (clones self and inverts clone, and then transforms point). Note that result is a close approximation, but should be good enough for GUI mouse-click-point containment checks.
|
129
|
+
- `#inverse_transform_points(*xy_coordinates_or_points)`: returns inverse transforms of a point `Array` of (x,y) coordinates
|
130
|
+
|
131
|
+
Example:
|
132
|
+
|
133
|
+
```ruby
|
134
|
+
xxp = 2
|
135
|
+
xyp = 3
|
136
|
+
yxp = 4
|
137
|
+
yyp = 5
|
138
|
+
xt = 6
|
139
|
+
yt = 7
|
140
|
+
affine_transform1 = PerfectShape::AffineTransform.new(xxp: xxp, xyp: xyp, yxp: yxp, yyp: yyp, xt: xt, yt: yt) # (x,y)-operation kwarg names
|
141
|
+
affine_transform2 = PerfectShape::AffineTransform.new(m11: xxp, m12: xyp, m21: yxp, m22: yyp, m13: xt, m23: yt) # traditional matrix element kwarg names
|
142
|
+
affine_transform3 = PerfectShape::AffineTransform.new(xxp, xyp, yxp, yyp, xt, yt) # standard arguments
|
143
|
+
|
144
|
+
affine_transform2.matrix_3d == affine_transform1.matrix_3d # => true
|
145
|
+
affine_transform3.matrix_3d == affine_transform1.matrix_3d # => true
|
146
|
+
|
147
|
+
affine_transform = PerfectShape::AffineTransform.new.translate!(30, 20).scale!(2, 3)
|
148
|
+
|
149
|
+
affine_transform.transform_point(10, 10) # => approximately [50, 50]
|
150
|
+
affine_transform.inverse_transform_point(50, 50) # => approximately [10, 10]
|
151
|
+
```
|
152
|
+
|
89
153
|
### `PerfectShape::Point`
|
90
154
|
|
91
155
|
Class
|
@@ -99,6 +163,7 @@ Includes `PerfectShape::PointLocation`
|
|
99
163
|
Points are simply represented by an `Array` of `[x,y]` coordinates when used within other shapes, but when needing point-specific operations like `point_distance`, the `PerfectShape::Point` class can come in handy.
|
100
164
|
|
101
165
|
- `::point_distance(x, y, px, py)`: Returns the distance from a point to another point
|
166
|
+
- `::normalize_point(x_or_point, y = nil)`: Normalizes point args whether two-number `point` `Array` or `x`, `y` args, returning normalized point `Array` of two `BigDecimal`'s
|
102
167
|
- `::new(x_or_point=nil, y_arg=nil, x: nil, y: nil)`: constructs a point with (x,y) pair (default: 0,0) whether specified as `Array` of (x,y) pair, flat `x,y` args, or `x:, y:` kwargs.
|
103
168
|
- `#min_x`: min x (always x)
|
104
169
|
- `#min_y`: min y (always y)
|
@@ -112,6 +177,7 @@ Points are simply represented by an `Array` of `[x,y]` coordinates when used wit
|
|
112
177
|
- `#bounding_box`: bounding box is a rectangle with x = min x, y = min y, and width/height of shape
|
113
178
|
- `#==(other)`: Returns `true` if equal to `other` or `false` otherwise
|
114
179
|
- `#contain?(x_or_point, y=nil, outline: true, distance_tolerance: 0)`: checks if point matches self, with a distance tolerance (0 by default). Distance tolerance provides a fuzz factor that for example enables GUI users to mouse-click-select a point shape more successfully. `outline` option makes no difference on point
|
180
|
+
- `#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
|
115
181
|
- `#point_distance(x_or_point, y=nil)`: Returns the distance from a point to another point
|
116
182
|
|
117
183
|
Example:
|
@@ -139,7 +205,7 @@ Includes `PerfectShape::MultiPoint`
|
|
139
205
|
|
140
206
|
![line](https://raw.githubusercontent.com/AndyObtiva/perfect-shape/master/images/line.png)
|
141
207
|
|
142
|
-
- `::relative_counterclockwise(x1, y1, x2, y2, px, py)`: Returns an indicator of where the specified point (px,py) lies with respect to the line segment from (x1,y1) to (x2,y2). The return value can be either 1, -1, or 0 and indicates in which direction the specified line must pivot around its first end point, (x1,y1), in order to point at the specified point (px,py). A return value of 1 indicates that the line segment must turn in the direction that takes the positive X axis towards the negative Y axis. In the default coordinate system
|
208
|
+
- `::relative_counterclockwise(x1, y1, x2, y2, px, py)`: Returns an indicator of where the specified point (px,py) lies with respect to the line segment from (x1,y1) to (x2,y2). The return value can be either 1, -1, or 0 and indicates in which direction the specified line must pivot around its first end point, (x1,y1), in order to point at the specified point (px,py). A return value of 1 indicates that the line segment must turn in the direction that takes the positive X axis towards the negative Y axis. In the default coordinate system, this direction is counterclockwise. A return value of -1 indicates that the line segment must turn in the direction that takes the positive X axis towards the positive Y axis. In the default coordinate system, this direction is clockwise. A return value of 0 indicates that the point lies exactly on the line segment. Note that an indicator value of 0 is rare and not useful for determining collinearity because of floating point rounding issues. If the point is colinear with the line segment, but not between the end points, then the value will be -1 if the point lies “beyond (x1,y1)” or 1 if the point lies “beyond (x2,y2)”.
|
143
209
|
- `::point_distance_square(x1, y1, x2, y2, px, py)`: Returns the square of distance from a point to a line segment.
|
144
210
|
- `::point_distance(x1, y1, x2, y2, px, py)`: Returns the distance from a point to a line segment.
|
145
211
|
- `::new(points: [])`: constructs a line with two `points` as `Array` of `Array`s of `[x,y]` pairs or flattened `Array` of alternating x and y coordinates
|
@@ -155,7 +221,8 @@ Includes `PerfectShape::MultiPoint`
|
|
155
221
|
- `#bounding_box`: bounding box is a rectangle with x = min x, y = min y, and width/height of shape
|
156
222
|
- `#==(other)`: Returns `true` if equal to `other` or `false` otherwise
|
157
223
|
- `#contain?(x_or_point, y=nil, outline: true, distance_tolerance: 0)`: checks if point lies on line, with a distance tolerance (0 by default). Distance tolerance provides a fuzz factor that for example enables GUI users to mouse-click-select a line shape more successfully. `outline` option makes no difference on line
|
158
|
-
- `#
|
224
|
+
- `#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
|
225
|
+
- `#relative_counterclockwise(x_or_point, y=nil)`: Returns an indicator of where the specified point (px,py) lies with respect to the line segment from (x1,y1) to (x2,y2). The return value can be either 1, -1, or 0 and indicates in which direction the specified line must pivot around its first end point, (x1,y1), in order to point at the specified point (px,py). A return value of 1 indicates that the line segment must turn in the direction that takes the positive X axis towards the negative Y axis. In the default coordinate system, this direction is counterclockwise. A return value of -1 indicates that the line segment must turn in the direction that takes the positive X axis towards the positive Y axis. In the default coordinate system, this direction is clockwise. A return value of 0 indicates that the point lies exactly on the line segment. Note that an indicator value of 0 is rare and not useful for determining collinearity because of floating point rounding issues. If the point is colinear with the line segment, but not between the end points, then the value will be -1 if the point lies “beyond (x1,y1)” or 1 if the point lies “beyond (x2,y2)”.
|
159
226
|
- `#point_distance(x_or_point, y=nil)`: Returns the distance from a point to a line segment.
|
160
227
|
|
161
228
|
Example:
|
@@ -297,6 +364,7 @@ Includes `PerfectShape::RectangularShape`
|
|
297
364
|
- `#==(other)`: Returns `true` if equal to `other` or `false` otherwise
|
298
365
|
- `#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 rectangle shape from its outline more successfully
|
299
366
|
- `#edges`: edges of rectangle as `PerfectShape::Line` objects
|
367
|
+
- `#out_state(x_or_point, y = nil)`: Returns "out state" of specified point (x,y) (whether it lies to the left, right, top, bottom of rectangle). If point is outside rectangle, it returns a bit mask combination of `Rectangle::OUT_LEFT`, `Rectangle::OUT_RIGHT`, `Rectangle::OUT_TOP`, or `Rectangle::OUT_BOTTOM`. Otherwise, it returns `0` if point is inside the rectangle.
|
300
368
|
|
301
369
|
Example:
|
302
370
|
|
@@ -700,10 +768,10 @@ path_shapes << PerfectShape::CubicBezierCurve.new(points: [[370, 50], [430, 220]
|
|
700
768
|
|
701
769
|
shape = PerfectShape::Path.new(shapes: path_shapes, closed: false, winding_rule: :wind_even_odd)
|
702
770
|
|
703
|
-
shape.contain?(
|
704
|
-
shape.contain?([
|
705
|
-
shape.contain?(
|
706
|
-
shape.contain?([
|
771
|
+
shape.contain?(275, 165) # => true
|
772
|
+
shape.contain?([275, 165]) # => true
|
773
|
+
shape.contain?(275, 165, outline: true) # => false
|
774
|
+
shape.contain?([275, 165], outline: true) # => false
|
707
775
|
shape.contain?(shape.disconnected_shapes[1].curve_center_x, shape.disconnected_shapes[1].curve_center_y, outline: true) # => true
|
708
776
|
shape.contain?([shape.disconnected_shapes[1].curve_center_x, shape.disconnected_shapes[1].curve_center_y], outline: true) # => true
|
709
777
|
shape.contain?(shape.disconnected_shapes[1].curve_center_x + 1, shape.disconnected_shapes[1].curve_center_y, outline: true) # => false
|
@@ -735,7 +803,7 @@ A composite shape is simply an aggregate of multiple shapes (e.g. square and pol
|
|
735
803
|
- `#center_y`: center y
|
736
804
|
- `#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)
|
737
805
|
- `#==(other)`: Returns `true` if equal to `other` or `false` otherwise
|
738
|
-
- `#contain?(x_or_point, y=nil)`: checks if point is inside any of the shapes owned by the composite shape
|
806
|
+
- `#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
|
739
807
|
|
740
808
|
Example:
|
741
809
|
|
@@ -748,10 +816,27 @@ shapes << PerfectShape::Polygon.new(points: [[120, 215], [170, 165], [220, 215]]
|
|
748
816
|
|
749
817
|
shape = PerfectShape::CompositeShape.new(shapes: shapes)
|
750
818
|
|
751
|
-
shape.contain?(170, 265) # => true
|
752
|
-
shape.contain?([170, 265]) # => true
|
753
|
-
shape.contain?(170,
|
754
|
-
shape.contain?([170,
|
819
|
+
shape.contain?(170, 265) # => true inside square
|
820
|
+
shape.contain?([170, 265]) # => true inside square
|
821
|
+
shape.contain?(170, 265, outline: true) # => false
|
822
|
+
shape.contain?([170, 265], outline: true) # => false
|
823
|
+
shape.contain?(170, 315, outline: true) # => true
|
824
|
+
shape.contain?([170, 315], outline: true) # => true
|
825
|
+
shape.contain?(170, 316, outline: true) # => false
|
826
|
+
shape.contain?([170, 316], outline: true) # => false
|
827
|
+
shape.contain?(170, 316, outline: true, distance_tolerance: 1) # => true
|
828
|
+
shape.contain?([170, 316], outline: true, distance_tolerance: 1) # => true
|
829
|
+
|
830
|
+
shape.contain?(170, 190) # => true inside polygon
|
831
|
+
shape.contain?([170, 190]) # => true inside polygon
|
832
|
+
shape.contain?(170, 190, outline: true) # => false
|
833
|
+
shape.contain?([170, 190], outline: true) # => false
|
834
|
+
shape.contain?(145, 190, outline: true) # => true
|
835
|
+
shape.contain?([145, 190], outline: true) # => true
|
836
|
+
shape.contain?(145, 189, outline: true) # => false
|
837
|
+
shape.contain?([145, 189], outline: true) # => false
|
838
|
+
shape.contain?(145, 189, outline: true, distance_tolerance: 1) # => true
|
839
|
+
shape.contain?([145, 189], outline: true, distance_tolerance: 1) # => true
|
755
840
|
```
|
756
841
|
|
757
842
|
## Process
|
@@ -761,7 +846,10 @@ shape.contain?([170, 190]) # => true
|
|
761
846
|
## Resources
|
762
847
|
|
763
848
|
- Rubydoc: https://www.rubydoc.info/gems/perfect-shape
|
764
|
-
-
|
849
|
+
- Point in Polygon: https://en.wikipedia.org/wiki/Point_in_polygon
|
850
|
+
- Even-Odd Rule: https://en.wikipedia.org/wiki/Even%E2%80%93odd_rule
|
851
|
+
- Nonzero Rule: https://en.wikipedia.org/wiki/Nonzero-rule
|
852
|
+
- IEEE 754-1985 Remainder: https://en.wikipedia.org/wiki/IEEE_754-1985
|
765
853
|
|
766
854
|
## TODO
|
767
855
|
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.
|
1
|
+
0.5.1
|
data/lib/perfect-shape.rb
CHANGED
@@ -23,8 +23,8 @@ $LOAD_PATH.unshift File.expand_path('.', __dir__)
|
|
23
23
|
|
24
24
|
require 'bigdecimal'
|
25
25
|
require 'equalizer'
|
26
|
+
require 'matrix'
|
26
27
|
|
27
|
-
# Perfect Shape algorithms are mostly ported from java.awt.geom: https://docs.oracle.com/javase/8/docs/api/java/awt/geom/package-summary.html
|
28
28
|
module PerfectShape
|
29
29
|
end
|
30
30
|
|
@@ -0,0 +1,235 @@
|
|
1
|
+
# Copyright (c) 2021-2022 Andy Maleh
|
2
|
+
#
|
3
|
+
# Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
# a copy of this software and associated documentation files (the
|
5
|
+
# "Software"), to deal in the Software without restriction, including
|
6
|
+
# without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
# distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
# permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
# the following conditions:
|
10
|
+
#
|
11
|
+
# The above copyright notice and this permission notice shall be
|
12
|
+
# included in all copies or substantial portions of the Software.
|
13
|
+
#
|
14
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
21
|
+
|
22
|
+
require 'perfect_shape/math'
|
23
|
+
require 'perfect_shape/shape'
|
24
|
+
require 'perfect_shape/point'
|
25
|
+
require 'perfect_shape/multi_point'
|
26
|
+
|
27
|
+
module PerfectShape
|
28
|
+
# Represents an affine transform
|
29
|
+
class AffineTransform
|
30
|
+
include Equalizer.new(:xxp, :xyp, :yxp, :yyp, :xt, :yt)
|
31
|
+
|
32
|
+
attr_reader :xxp, :xyp, :yxp, :yyp, :xt, :yt
|
33
|
+
alias m11 xxp
|
34
|
+
alias m12 xyp
|
35
|
+
alias m21 yxp
|
36
|
+
alias m22 yyp
|
37
|
+
alias m13 xt
|
38
|
+
alias m23 yt
|
39
|
+
|
40
|
+
# Creates a new AffineTransform with the following Matrix:
|
41
|
+
#
|
42
|
+
# [ xxp xyp xt ]
|
43
|
+
# [ yxp yyp yt ]
|
44
|
+
#
|
45
|
+
# The Matrix is used to transform (x,y) point coordinates as follows:
|
46
|
+
#
|
47
|
+
# [ xxp xyp xt ] * [x] = [ xxp * x + xyp * y + xt ]
|
48
|
+
# [ yxp yyp yt ] * [y] = [ yxp * x + yyp * y + yt ]
|
49
|
+
#
|
50
|
+
# xxp is the x coordinate x product (m11)
|
51
|
+
# xyp is the x coordinate y product (m12)
|
52
|
+
# yxp is the y coordinate x product (m21)
|
53
|
+
# yyp is the y coordinate y product (m22)
|
54
|
+
# xt is the x coordinate translation (m13)
|
55
|
+
# yt is the y coordinate translation (m23)
|
56
|
+
#
|
57
|
+
# The constructor accepts either the (x,y)-operation related argument/kwarg names
|
58
|
+
# or traditional Matrix element kwarg names
|
59
|
+
#
|
60
|
+
# Example with (x,y)-operation kwarg names:
|
61
|
+
#
|
62
|
+
# AffineTransform.new(xxp: 2, xyp: 3, yxp: 4, yyp: 5, xt: 6, yt: 7)
|
63
|
+
#
|
64
|
+
# Example with traditional Matrix element kwarg names:
|
65
|
+
#
|
66
|
+
# AffineTransform.new(m11: 2, m12: 3, m21: 4, m22: 5, m13: 6, m23: 7)
|
67
|
+
#
|
68
|
+
# Example with standard arguments:
|
69
|
+
#
|
70
|
+
# AffineTransform.new(2, 3, 4, 5, 6, 7)
|
71
|
+
#
|
72
|
+
# If no arguments are supplied, it constructs an identity matrix
|
73
|
+
# (i.e. like calling `::new(xxp: 1, xyp: 0, yxp: 0, yyp: 1, xt: 0, yt: 0)`)
|
74
|
+
def initialize(xxp_element = nil, xyp_element = nil, yxp_element = nil, yyp_element = nil, xt_element = nil, yt_element = nil,
|
75
|
+
xxp: nil, xyp: nil, yxp: nil, yyp: nil, xt: nil, yt: nil,
|
76
|
+
m11: nil, m12: nil, m21: nil, m22: nil, m13: nil, m23: nil)
|
77
|
+
self.xxp = xxp_element || xxp || m11 || 1
|
78
|
+
self.xyp = xyp_element || xyp || m12 || 0
|
79
|
+
self.yxp = yxp_element || yxp || m21 || 0
|
80
|
+
self.yyp = yyp_element || yyp || m22 || 1
|
81
|
+
self.xt = xt_element || xt || m13 || 0
|
82
|
+
self.yt = yt_element || yt || m23 || 0
|
83
|
+
end
|
84
|
+
|
85
|
+
def xxp=(value)
|
86
|
+
@xxp = BigDecimal(value.to_s)
|
87
|
+
end
|
88
|
+
alias m11= xxp=
|
89
|
+
|
90
|
+
def xyp=(value)
|
91
|
+
@xyp = BigDecimal(value.to_s)
|
92
|
+
end
|
93
|
+
alias m12= xyp=
|
94
|
+
|
95
|
+
def yxp=(value)
|
96
|
+
@yxp = BigDecimal(value.to_s)
|
97
|
+
end
|
98
|
+
alias m21= yxp=
|
99
|
+
|
100
|
+
def yyp=(value)
|
101
|
+
@yyp = BigDecimal(value.to_s)
|
102
|
+
end
|
103
|
+
alias m22= yyp=
|
104
|
+
|
105
|
+
def xt=(value)
|
106
|
+
@xt = BigDecimal(value.to_s)
|
107
|
+
end
|
108
|
+
alias m13= xt=
|
109
|
+
|
110
|
+
def yt=(value)
|
111
|
+
@yt = BigDecimal(value.to_s)
|
112
|
+
end
|
113
|
+
alias m23= yt=
|
114
|
+
|
115
|
+
# Resets to identity matrix
|
116
|
+
# Returns self to support fluent interface chaining
|
117
|
+
def identity!
|
118
|
+
self.xxp = 1
|
119
|
+
self.xyp = 0
|
120
|
+
self.yxp = 0
|
121
|
+
self.yyp = 1
|
122
|
+
self.xt = 0
|
123
|
+
self.yt = 0
|
124
|
+
|
125
|
+
self
|
126
|
+
end
|
127
|
+
alias reset! identity!
|
128
|
+
|
129
|
+
# Inverts AffineTransform matrix if invertible
|
130
|
+
# Raises an error if affine transform matrix is not invertible
|
131
|
+
# Returns self to support fluent interface chaining
|
132
|
+
def invert!
|
133
|
+
raise 'Cannot invert (matrix is not invertible)!' if !invertible?
|
134
|
+
|
135
|
+
self.matrix_3d = matrix_3d.inverse
|
136
|
+
|
137
|
+
self
|
138
|
+
end
|
139
|
+
|
140
|
+
def invertible?
|
141
|
+
(m11 * m22 - m12 * m21) != 0
|
142
|
+
end
|
143
|
+
|
144
|
+
# Multiplies by other AffineTransform
|
145
|
+
def multiply!(other_affine_transform)
|
146
|
+
self.matrix_3d = matrix_3d*other_affine_transform.matrix_3d
|
147
|
+
|
148
|
+
self
|
149
|
+
end
|
150
|
+
|
151
|
+
# Translates AffineTransform
|
152
|
+
def translate!(x_or_point, y = nil)
|
153
|
+
x, y = Point.normalize_point(x_or_point, y)
|
154
|
+
return unless x && y
|
155
|
+
|
156
|
+
translation_affine_transform = AffineTransform.new(xxp: 1, xyp: 0, yxp: 0, yyp: 1, xt: x, yt: y)
|
157
|
+
multiply!(translation_affine_transform)
|
158
|
+
|
159
|
+
self
|
160
|
+
end
|
161
|
+
|
162
|
+
# Scales AffineTransform
|
163
|
+
def scale!(x_or_point, y = nil)
|
164
|
+
x, y = Point.normalize_point(x_or_point, y)
|
165
|
+
return unless x && y
|
166
|
+
|
167
|
+
scale_affine_transform = AffineTransform.new(xxp: x, xyp: 0, yxp: 0, yyp: y, xt: 0, yt: 0)
|
168
|
+
multiply!(scale_affine_transform)
|
169
|
+
|
170
|
+
self
|
171
|
+
end
|
172
|
+
|
173
|
+
# Rotates AffineTransform counter-clockwise for positive angle value in degrees
|
174
|
+
# or clockwise for negative angle value in degrees
|
175
|
+
def rotate!(degrees)
|
176
|
+
degrees = Math.normalize_degrees(degrees)
|
177
|
+
radians = Math.degrees_to_radians(degrees)
|
178
|
+
|
179
|
+
rotation_affine_transform = AffineTransform.new(xxp: Math.cos(radians), xyp: -Math.sin(radians), yxp: Math.sin(radians), yyp: Math.cos(radians), xt: 0, yt: 0)
|
180
|
+
multiply!(rotation_affine_transform)
|
181
|
+
|
182
|
+
self
|
183
|
+
end
|
184
|
+
|
185
|
+
# Shears AffineTransform by (x,y) amount
|
186
|
+
def shear!(x_or_point, y = nil)
|
187
|
+
x, y = Point.normalize_point(x_or_point, y)
|
188
|
+
return unless x && y
|
189
|
+
|
190
|
+
shear_affine_transform = AffineTransform.new(xxp: 1 + x*y, xyp: x, yxp: y, yyp: 1, xt: 0, yt: 0)
|
191
|
+
multiply!(shear_affine_transform)
|
192
|
+
|
193
|
+
self
|
194
|
+
end
|
195
|
+
alias skew! shear!
|
196
|
+
|
197
|
+
# Sets elements from a Ruby Matrix representing Affine Transform matrix elements in 3D
|
198
|
+
def matrix_3d=(the_matrix_3d)
|
199
|
+
self.xxp = the_matrix_3d[0, 0]
|
200
|
+
self.xyp = the_matrix_3d[0, 1]
|
201
|
+
self.xt = the_matrix_3d[0, 2]
|
202
|
+
self.yxp = the_matrix_3d[1, 0]
|
203
|
+
self.yyp = the_matrix_3d[1, 1]
|
204
|
+
self.yt = the_matrix_3d[1, 2]
|
205
|
+
end
|
206
|
+
|
207
|
+
# Returns Ruby Matrix representing Affine Transform matrix elements in 3D
|
208
|
+
def matrix_3d
|
209
|
+
Matrix[[xxp, xyp, xt], [yxp, yyp, yt], [0, 0, 1]]
|
210
|
+
end
|
211
|
+
|
212
|
+
def transform_point(x_or_point, y = nil)
|
213
|
+
x, y = Point.normalize_point(x_or_point, y)
|
214
|
+
return unless x && y
|
215
|
+
|
216
|
+
[xxp*x + xyp*y + xt, yxp*x + yyp*y + yt]
|
217
|
+
end
|
218
|
+
|
219
|
+
def inverse_transform_point(x_or_point, y = nil)
|
220
|
+
clone.invert!.transform_point(x_or_point, y)
|
221
|
+
end
|
222
|
+
|
223
|
+
def transform_points(*xy_coordinates_or_points)
|
224
|
+
points = xy_coordinates_or_points.first.is_a?(Array) ? xy_coordinates_or_points.first : xy_coordinates_or_points
|
225
|
+
points = MultiPoint.normalize_point_array(points)
|
226
|
+
points.map { |point| transform_point(point) }
|
227
|
+
end
|
228
|
+
|
229
|
+
def inverse_transform_points(*xy_coordinates_or_points)
|
230
|
+
points = xy_coordinates_or_points.first.is_a?(Array) ? xy_coordinates_or_points.first : xy_coordinates_or_points
|
231
|
+
points = MultiPoint.normalize_point_array(points)
|
232
|
+
points.map { |point| inverse_transform_point(point) }
|
233
|
+
end
|
234
|
+
end
|
235
|
+
end
|
data/lib/perfect_shape/arc.rb
CHANGED
@@ -21,10 +21,10 @@
|
|
21
21
|
|
22
22
|
require 'perfect_shape/shape'
|
23
23
|
require 'perfect_shape/rectangular_shape'
|
24
|
+
require 'perfect_shape/point'
|
24
25
|
require 'perfect_shape/line'
|
25
26
|
|
26
27
|
module PerfectShape
|
27
|
-
# Mostly ported from java.awt.geom: https://docs.oracle.com/javase/8/docs/api/java/awt/geom/Arc2D.html
|
28
28
|
class Arc < Shape
|
29
29
|
include RectangularShape
|
30
30
|
include Equalizer.new(:type, :x, :y, :width, :height, :start, :extent)
|
@@ -145,7 +145,7 @@ module PerfectShape
|
|
145
145
|
# the arc, {@code false} if the point lies outside of the
|
146
146
|
# arc's bounds.
|
147
147
|
def contain?(x_or_point, y = nil, outline: false, distance_tolerance: 0)
|
148
|
-
x, y = normalize_point(x_or_point, y)
|
148
|
+
x, y = Point.normalize_point(x_or_point, y)
|
149
149
|
return unless x && y
|
150
150
|
if outline
|
151
151
|
if type == :pie && x == center_x && y == center_y
|
@@ -63,10 +63,11 @@ module PerfectShape
|
|
63
63
|
# @return true if the point lies within the bound of
|
64
64
|
# the composite shape or false if the point lies outside of the
|
65
65
|
# path's bounds.
|
66
|
-
def contain?(x_or_point, y = nil)
|
67
|
-
x, y = normalize_point(x_or_point, y)
|
66
|
+
def contain?(x_or_point, y = nil, outline: false, distance_tolerance: 0)
|
67
|
+
x, y = Point.normalize_point(x_or_point, y)
|
68
68
|
return unless x && y
|
69
|
-
|
69
|
+
|
70
|
+
shapes.any? { |shape| shape.contain?(x, y, outline: outline, distance_tolerance: distance_tolerance) }
|
70
71
|
end
|
71
72
|
end
|
72
73
|
end
|
@@ -20,10 +20,10 @@
|
|
20
20
|
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
21
21
|
|
22
22
|
require 'perfect_shape/shape'
|
23
|
+
require 'perfect_shape/point'
|
23
24
|
require 'perfect_shape/multi_point'
|
24
25
|
|
25
26
|
module PerfectShape
|
26
|
-
# Mostly ported from java.awt.geom: https://docs.oracle.com/javase/8/docs/api/java/awt/geom/QuadCurve2D.html
|
27
27
|
class CubicBezierCurve < Shape
|
28
28
|
class << self
|
29
29
|
# Calculates the number of times the cubic bézier curve from (x1,y1) to (x2,y2)
|
@@ -86,7 +86,7 @@ module PerfectShape
|
|
86
86
|
# the cubic bézier curve, {@code false} if the point lies outside of the
|
87
87
|
# cubic bézier curve's bounds.
|
88
88
|
def contain?(x_or_point, y = nil, outline: false, distance_tolerance: 0)
|
89
|
-
x, y = normalize_point(x_or_point, y)
|
89
|
+
x, y = Point.normalize_point(x_or_point, y)
|
90
90
|
return unless x && y
|
91
91
|
|
92
92
|
if outline
|
@@ -120,7 +120,7 @@ module PerfectShape
|
|
120
120
|
# +1 is added for each crossing where the Y coordinate is increasing
|
121
121
|
# -1 is added for each crossing where the Y coordinate is decreasing
|
122
122
|
def point_crossings(x_or_point, y = nil, level = 0)
|
123
|
-
x, y = normalize_point(x_or_point, y)
|
123
|
+
x, y = Point.normalize_point(x_or_point, y)
|
124
124
|
return unless x && y
|
125
125
|
CubicBezierCurve.point_crossings(points[0][0], points[0][1], points[1][0], points[1][1], points[2][0], points[2][1], points[3][0], points[3][1], x, y, level)
|
126
126
|
end
|
@@ -183,7 +183,7 @@ module PerfectShape
|
|
183
183
|
end
|
184
184
|
|
185
185
|
def point_distance(x_or_point, y = nil, minimum_distance_threshold: OUTLINE_MINIMUM_DISTANCE_THRESHOLD)
|
186
|
-
x, y = normalize_point(x_or_point, y)
|
186
|
+
x, y = Point.normalize_point(x_or_point, y)
|
187
187
|
return unless x && y
|
188
188
|
|
189
189
|
point = Point.new(x, y)
|
@@ -22,7 +22,6 @@
|
|
22
22
|
require 'perfect_shape/arc'
|
23
23
|
|
24
24
|
module PerfectShape
|
25
|
-
# Mostly ported from java.awt.geom: https://docs.oracle.com/javase/8/docs/api/java/awt/geom/Ellipse2D.html
|
26
25
|
class Ellipse < Arc
|
27
26
|
MESSAGE_CANNOT_UPDATE_ATTRIUBTE = "Ellipse %s cannot be updated. If you want to update type, use Arc instead!"
|
28
27
|
|
@@ -65,7 +64,7 @@ module PerfectShape
|
|
65
64
|
# ellipse's bounds.
|
66
65
|
def contain?(x_or_point, y = nil, outline: false, distance_tolerance: 0)
|
67
66
|
# This is implemented again even though super would have just worked to have an optimized algorithm for Ellipse.
|
68
|
-
x, y = normalize_point(x_or_point, y)
|
67
|
+
x, y = Point.normalize_point(x_or_point, y)
|
69
68
|
return unless x && y
|
70
69
|
if outline
|
71
70
|
super(x, y, outline: true, distance_tolerance: distance_tolerance)
|
data/lib/perfect_shape/line.rb
CHANGED
@@ -20,10 +20,11 @@
|
|
20
20
|
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
21
21
|
|
22
22
|
require 'perfect_shape/shape'
|
23
|
+
require 'perfect_shape/point'
|
23
24
|
require 'perfect_shape/multi_point'
|
25
|
+
require 'perfect_shape/rectangle'
|
24
26
|
|
25
27
|
module PerfectShape
|
26
|
-
# Mostly ported from java.awt.geom: https://docs.oracle.com/javase/8/docs/api/java/awt/geom/Line2D.html
|
27
28
|
class Line < Shape
|
28
29
|
class << self
|
29
30
|
# Returns an indicator of where the specified point (px,py) lies with respect to the line segment from
|
@@ -213,20 +214,20 @@ module PerfectShape
|
|
213
214
|
# the line, {@code false} if the point lies outside of the
|
214
215
|
# line's bounds.
|
215
216
|
def contain?(x_or_point, y = nil, outline: true, distance_tolerance: 0)
|
216
|
-
x, y = normalize_point(x_or_point, y)
|
217
|
+
x, y = Point.normalize_point(x_or_point, y)
|
217
218
|
return unless x && y
|
218
219
|
distance_tolerance = BigDecimal(distance_tolerance.to_s)
|
219
220
|
point_distance(x, y) <= distance_tolerance
|
220
221
|
end
|
221
222
|
|
222
223
|
def point_distance(x_or_point, y = nil)
|
223
|
-
x, y = normalize_point(x_or_point, y)
|
224
|
+
x, y = Point.normalize_point(x_or_point, y)
|
224
225
|
return unless x && y
|
225
226
|
Line.point_distance(points[0][0], points[0][1], points[1][0], points[1][1], x, y)
|
226
227
|
end
|
227
228
|
|
228
229
|
def relative_counterclockwise(x_or_point, y = nil)
|
229
|
-
x, y = normalize_point(x_or_point, y)
|
230
|
+
x, y = Point.normalize_point(x_or_point, y)
|
230
231
|
return unless x && y
|
231
232
|
Line.relative_counterclockwise(points[0][0], points[0][1], points[1][0], points[1][1], x, y)
|
232
233
|
end
|
@@ -237,9 +238,33 @@ module PerfectShape
|
|
237
238
|
# +1 is returned for a crossing where the Y coordinate is increasing
|
238
239
|
# -1 is returned for a crossing where the Y coordinate is decreasing
|
239
240
|
def point_crossings(x_or_point, y = nil)
|
240
|
-
x, y = normalize_point(x_or_point, y)
|
241
|
+
x, y = Point.normalize_point(x_or_point, y)
|
241
242
|
return unless x && y
|
242
243
|
Line.point_crossings(points[0][0], points[0][1], points[1][0], points[1][1], x, y)
|
243
244
|
end
|
245
|
+
|
246
|
+
def intersect?(rectangle)
|
247
|
+
x1 = points[0][0]
|
248
|
+
y1 = points[0][1]
|
249
|
+
x2 = points[1][0]
|
250
|
+
y2 = points[1][1]
|
251
|
+
out1 = out2 = nil
|
252
|
+
return true if (out2 = rectangle.out_state(x2, y2)) == 0
|
253
|
+
while (out1 = rectangle.out_state(x1, y1)) != 0
|
254
|
+
return false if (out1 & out2) != 0
|
255
|
+
if (out1 & (Rectangle::OUT_LEFT | Rectangle::OUT_RIGHT)) != 0
|
256
|
+
x = rectangle.x
|
257
|
+
x += rectangle.width if (out1 & Rectangle::OUT_RIGHT) != 0
|
258
|
+
y1 = y1 + (x - x1) * (y2 - y1) / (x2 - x1)
|
259
|
+
x1 = x
|
260
|
+
else
|
261
|
+
y = rectangle.y
|
262
|
+
y += rectangle.height if (out1 & Rectangle::OUT_BOTTOM) != 0
|
263
|
+
x1 = x1 + (y - y1) * (x2 - x1) / (y2 - y1)
|
264
|
+
y1 = y
|
265
|
+
end
|
266
|
+
end
|
267
|
+
true
|
268
|
+
end
|
244
269
|
end
|
245
270
|
end
|
@@ -24,6 +24,19 @@ require 'perfect_shape/shape'
|
|
24
24
|
module PerfectShape
|
25
25
|
# Represents multi-point shapes like Line, Polygon, and Polyline
|
26
26
|
module MultiPoint
|
27
|
+
class << self
|
28
|
+
def normalize_point_array(the_points)
|
29
|
+
if the_points.all? {|the_point| the_point.is_a?(Array)}
|
30
|
+
the_points
|
31
|
+
else
|
32
|
+
the_points = the_points.flatten
|
33
|
+
xs = the_points.each_with_index.select {|n, i| i.even?}.map(&:first)
|
34
|
+
ys = the_points.each_with_index.select {|n, i| i.odd?}.map(&:first)
|
35
|
+
xs.zip(ys)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
27
40
|
attr_reader :points
|
28
41
|
|
29
42
|
def initialize(points: [])
|
@@ -32,11 +45,7 @@ module PerfectShape
|
|
32
45
|
|
33
46
|
# Sets points, normalizing to an Array of Arrays of (x,y) pairs as BigDecimal
|
34
47
|
def points=(the_points)
|
35
|
-
|
36
|
-
xs = the_points.each_with_index.select {|n, i| i.even?}.map(&:first)
|
37
|
-
ys = the_points.each_with_index.select {|n, i| i.odd?}.map(&:first)
|
38
|
-
the_points = xs.zip(ys)
|
39
|
-
end
|
48
|
+
the_points = MultiPoint.normalize_point_array(the_points)
|
40
49
|
@points = the_points.map do |pair|
|
41
50
|
[
|
42
51
|
pair.first.is_a?(BigDecimal) ? pair.first : BigDecimal(pair.first.to_s),
|
data/lib/perfect_shape/path.rb
CHANGED
@@ -27,7 +27,6 @@ require 'perfect_shape/cubic_bezier_curve'
|
|
27
27
|
require 'perfect_shape/multi_point'
|
28
28
|
|
29
29
|
module PerfectShape
|
30
|
-
# Mostly ported from java.awt.geom: https://docs.oracle.com/javase/8/docs/api/java/awt/geom/Path2D.html
|
31
30
|
class Path < Shape
|
32
31
|
include MultiPoint
|
33
32
|
include Equalizer.new(:shapes, :closed, :winding_rule)
|
@@ -115,7 +114,7 @@ module PerfectShape
|
|
115
114
|
# the path or false if the point lies outside of the
|
116
115
|
# path's bounds.
|
117
116
|
def contain?(x_or_point, y = nil, outline: false, distance_tolerance: 0)
|
118
|
-
x, y = normalize_point(x_or_point, y)
|
117
|
+
x, y = Point.normalize_point(x_or_point, y)
|
119
118
|
return unless x && y
|
120
119
|
|
121
120
|
if outline
|
@@ -149,7 +148,7 @@ module PerfectShape
|
|
149
148
|
# The caller must check for NaN values.
|
150
149
|
# The caller may also reject infinite values as well.
|
151
150
|
def point_crossings(x_or_point, y = nil)
|
152
|
-
x, y = normalize_point(x_or_point, y)
|
151
|
+
x, y = Point.normalize_point(x_or_point, y)
|
153
152
|
return unless x && y
|
154
153
|
return 0 if shapes.count == 0
|
155
154
|
movx = movy = curx = cury = endx = endy = 0
|
data/lib/perfect_shape/point.rb
CHANGED
@@ -33,6 +33,21 @@ module PerfectShape
|
|
33
33
|
py = py.is_a?(BigDecimal) ? py : BigDecimal(py.to_s)
|
34
34
|
BigDecimal(Math.sqrt((px - x)**2 + (py - y)**2).to_s)
|
35
35
|
end
|
36
|
+
|
37
|
+
# Normalizes point args whether two-number Array or x, y args returning
|
38
|
+
# normalized point array of two BigDecimal's
|
39
|
+
#
|
40
|
+
# @param x_or_point The point or X coordinate of the point to test.
|
41
|
+
# @param y The Y coordinate of the point to test.
|
42
|
+
#
|
43
|
+
# @return Array of x and y BigDecimal's representing point
|
44
|
+
def normalize_point(x_or_point, y = nil)
|
45
|
+
x = x_or_point
|
46
|
+
x, y = x if y.nil? && x_or_point.is_a?(Array) && x_or_point.size == 2
|
47
|
+
x = x.is_a?(BigDecimal) ? x : BigDecimal(x.to_s)
|
48
|
+
y = y.is_a?(BigDecimal) ? y : BigDecimal(y.to_s)
|
49
|
+
[x, y]
|
50
|
+
end
|
36
51
|
end
|
37
52
|
|
38
53
|
include PointLocation
|
@@ -68,19 +83,23 @@ module PerfectShape
|
|
68
83
|
# @return {@code true} if the point is close enough within distance tolerance,
|
69
84
|
# {@code false} if the point is too far.
|
70
85
|
def contain?(x_or_point, y = nil, outline: true, distance_tolerance: 0)
|
71
|
-
x, y = normalize_point(x_or_point, y)
|
86
|
+
x, y = Point.normalize_point(x_or_point, y)
|
72
87
|
return unless x && y
|
73
88
|
distance_tolerance = BigDecimal(distance_tolerance.to_s)
|
74
89
|
point_distance(x, y) <= distance_tolerance
|
75
90
|
end
|
76
91
|
|
77
92
|
def point_distance(x_or_point, y = nil)
|
78
|
-
x, y = normalize_point(x_or_point, y)
|
93
|
+
x, y = Point.normalize_point(x_or_point, y)
|
79
94
|
return unless x && y
|
80
95
|
|
81
96
|
Point.point_distance(self.x, self.y, x, y)
|
82
97
|
end
|
83
98
|
|
99
|
+
def intersect?(rectangle)
|
100
|
+
rectangle.contain?(self.to_a)
|
101
|
+
end
|
102
|
+
|
84
103
|
# Convert to pair Array of x,y coordinates
|
85
104
|
def to_a
|
86
105
|
[self.x, self.y]
|
@@ -20,10 +20,10 @@
|
|
20
20
|
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
21
21
|
|
22
22
|
require 'perfect_shape/shape'
|
23
|
+
require 'perfect_shape/point'
|
23
24
|
require 'perfect_shape/multi_point'
|
24
25
|
|
25
26
|
module PerfectShape
|
26
|
-
# Mostly ported from java.awt.geom: https://docs.oracle.com/javase/8/docs/api/java/awt/Polygon.html
|
27
27
|
class Polygon < Shape
|
28
28
|
include MultiPoint
|
29
29
|
include Equalizer.new(:points)
|
@@ -38,7 +38,7 @@ module PerfectShape
|
|
38
38
|
# the polygon, {@code false} if the point lies outside of the
|
39
39
|
# polygon's bounds.
|
40
40
|
def contain?(x_or_point, y = nil, outline: false, distance_tolerance: 0)
|
41
|
-
x, y = normalize_point(x_or_point, y)
|
41
|
+
x, y = Point.normalize_point(x_or_point, y)
|
42
42
|
return unless x && y
|
43
43
|
if outline
|
44
44
|
edges.any? { |edge| edge.contain?(x, y, distance_tolerance: distance_tolerance) }
|
@@ -20,10 +20,10 @@
|
|
20
20
|
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
21
21
|
|
22
22
|
require 'perfect_shape/shape'
|
23
|
+
require 'perfect_shape/point'
|
23
24
|
require 'perfect_shape/multi_point'
|
24
25
|
|
25
26
|
module PerfectShape
|
26
|
-
# Mostly ported from java.awt.geom: https://docs.oracle.com/javase/8/docs/api/java/awt/geom/QuadCurve2D.html
|
27
27
|
class QuadraticBezierCurve < Shape
|
28
28
|
class << self
|
29
29
|
# Calculates the number of times the quadratic bézier curve from (x1,y1) to (x2,y2)
|
@@ -80,7 +80,7 @@ module PerfectShape
|
|
80
80
|
# the quadratic bézier curve, {@code false} if the point lies outside of the
|
81
81
|
# quadratic bézier curve's bounds.
|
82
82
|
def contain?(x_or_point, y = nil, outline: false, distance_tolerance: 0)
|
83
|
-
x, y = normalize_point(x_or_point, y)
|
83
|
+
x, y = Point.normalize_point(x_or_point, y)
|
84
84
|
return unless x && y
|
85
85
|
|
86
86
|
x1 = points[0][0]
|
@@ -190,7 +190,7 @@ module PerfectShape
|
|
190
190
|
# +1 is added for each crossing where the Y coordinate is increasing
|
191
191
|
# -1 is added for each crossing where the Y coordinate is decreasing
|
192
192
|
def point_crossings(x_or_point, y = nil, level = 0)
|
193
|
-
x, y = normalize_point(x_or_point, y)
|
193
|
+
x, y = Point.normalize_point(x_or_point, y)
|
194
194
|
return unless x && y
|
195
195
|
QuadraticBezierCurve.point_crossings(points[0][0], points[0][1], points[1][0], points[1][1], points[2][0], points[2][1], x, y, level)
|
196
196
|
end
|
@@ -247,7 +247,7 @@ module PerfectShape
|
|
247
247
|
end
|
248
248
|
|
249
249
|
def point_distance(x_or_point, y = nil, minimum_distance_threshold: OUTLINE_MINIMUM_DISTANCE_THRESHOLD)
|
250
|
-
x, y = normalize_point(x_or_point, y)
|
250
|
+
x, y = Point.normalize_point(x_or_point, y)
|
251
251
|
return unless x && y
|
252
252
|
|
253
253
|
point = Point.new(x, y)
|
@@ -21,13 +21,25 @@
|
|
21
21
|
|
22
22
|
require 'perfect_shape/shape'
|
23
23
|
require 'perfect_shape/rectangular_shape'
|
24
|
+
require 'perfect_shape/point'
|
24
25
|
require 'perfect_shape/line'
|
25
26
|
|
26
27
|
module PerfectShape
|
27
|
-
# Mostly ported from java.awt.geom: https://docs.oracle.com/javase/8/docs/api/java/awt/geom/Rectangle2D.html
|
28
28
|
class Rectangle < Shape
|
29
29
|
include RectangularShape
|
30
30
|
include Equalizer.new(:x, :y, :width, :height)
|
31
|
+
|
32
|
+
# bitmask indicating a point lies to the left
|
33
|
+
OUT_LEFT = 1
|
34
|
+
|
35
|
+
# bitmask indicating a point lies above
|
36
|
+
OUT_TOP = 2
|
37
|
+
|
38
|
+
# bitmask indicating a point lies to the right
|
39
|
+
OUT_RIGHT = 4
|
40
|
+
|
41
|
+
# bitmask indicating a point lies below
|
42
|
+
OUT_BOTTOM = 8
|
31
43
|
|
32
44
|
# Checks if rectangle contains point (two-number Array or x, y args)
|
33
45
|
#
|
@@ -38,8 +50,9 @@ module PerfectShape
|
|
38
50
|
# the rectangle, {@code false} if the point lies outside of the
|
39
51
|
# rectangle's bounds.
|
40
52
|
def contain?(x_or_point, y = nil, outline: false, distance_tolerance: 0)
|
41
|
-
x, y = normalize_point(x_or_point, y)
|
53
|
+
x, y = Point.normalize_point(x_or_point, y)
|
42
54
|
return unless x && y
|
55
|
+
|
43
56
|
if outline
|
44
57
|
edges.any? { |edge| edge.contain?(x, y, distance_tolerance: distance_tolerance) }
|
45
58
|
else
|
@@ -55,5 +68,32 @@ module PerfectShape
|
|
55
68
|
Line.new(points: [[self.x, self.y + height], [self.x, self.y]])
|
56
69
|
]
|
57
70
|
end
|
71
|
+
|
72
|
+
# Returns out state for specified point (x,y): (left, right, top, bottom)
|
73
|
+
#
|
74
|
+
# It can be 0 meaning not outside the rectangle,
|
75
|
+
# or if outside the rectangle, then a bit mask
|
76
|
+
# combination of OUT_LEFT, OUT_RIGHT, OUT_TOP, or OUT_BOTTOM
|
77
|
+
def out_state(x_or_point, y = nil)
|
78
|
+
x, y = Point.normalize_point(x_or_point, y)
|
79
|
+
return unless x && y
|
80
|
+
|
81
|
+
out = 0
|
82
|
+
if self.width <= 0
|
83
|
+
out |= OUT_LEFT | OUT_RIGHT
|
84
|
+
elsif x < self.x
|
85
|
+
out |= OUT_LEFT
|
86
|
+
elsif x > self.x + self.width
|
87
|
+
out |= OUT_RIGHT
|
88
|
+
end
|
89
|
+
if self.height <= 0
|
90
|
+
out |= OUT_TOP | OUT_BOTTOM
|
91
|
+
elsif y < self.y
|
92
|
+
out |= OUT_TOP
|
93
|
+
elsif y > self.y + self.height
|
94
|
+
out |= OUT_BOTTOM
|
95
|
+
end
|
96
|
+
out
|
97
|
+
end
|
58
98
|
end
|
59
99
|
end
|
data/lib/perfect_shape/shape.rb
CHANGED
@@ -75,21 +75,6 @@ module PerfectShape
|
|
75
75
|
Rectangle.new(x: min_x, y: min_y, width: width, height: height)
|
76
76
|
end
|
77
77
|
|
78
|
-
# Normalizes point args whether two-number Array or x, y args returning
|
79
|
-
# normalized point array of two BigDecimal's
|
80
|
-
#
|
81
|
-
# @param x_or_point The point or X coordinate of the point to test.
|
82
|
-
# @param y The Y coordinate of the point to test.
|
83
|
-
#
|
84
|
-
# @return Array of x and y BigDecimal's representing point
|
85
|
-
def normalize_point(x_or_point, y = nil)
|
86
|
-
x = x_or_point
|
87
|
-
x, y = x if y.nil? && x_or_point.is_a?(Array) && x_or_point.size == 2
|
88
|
-
x = x.is_a?(BigDecimal) ? x : BigDecimal(x.to_s)
|
89
|
-
y = y.is_a?(BigDecimal) ? y : BigDecimal(y.to_s)
|
90
|
-
[x, y]
|
91
|
-
end
|
92
|
-
|
93
78
|
# Subclasses must implement
|
94
79
|
def contain?(x_or_point, y = nil, outline: false, distance_tolerance: 0)
|
95
80
|
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 0.5.1 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 = "0.5.1"
|
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-01-
|
15
|
-
s.description = "Perfect Shape is a collection of pure Ruby geometric algorithms that are mostly useful for GUI manipulation like checking 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 (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-01-19"
|
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
|
16
16
|
s.email = "andy.am@gmail.com".freeze
|
17
17
|
s.extra_rdoc_files = [
|
18
18
|
"CHANGELOG.md",
|
@@ -25,6 +25,7 @@ Gem::Specification.new do |s|
|
|
25
25
|
"README.md",
|
26
26
|
"VERSION",
|
27
27
|
"lib/perfect-shape.rb",
|
28
|
+
"lib/perfect_shape/affine_transform.rb",
|
28
29
|
"lib/perfect_shape/arc.rb",
|
29
30
|
"lib/perfect_shape/circle.rb",
|
30
31
|
"lib/perfect_shape/composite_shape.rb",
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: perfect-shape
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.5.1
|
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-01-
|
11
|
+
date: 2022-01-19 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: equalizer
|
@@ -95,12 +95,14 @@ dependencies:
|
|
95
95
|
- !ruby/object:Gem::Version
|
96
96
|
version: '0'
|
97
97
|
description: Perfect Shape is a collection of pure Ruby geometric algorithms that
|
98
|
-
are mostly useful for GUI manipulation like checking
|
99
|
-
point in popular geometry shapes such as rectangle,
|
100
|
-
pie), ellipse, circle, polygon, and paths containing
|
101
|
-
|
102
|
-
|
103
|
-
|
98
|
+
are mostly useful for GUI manipulation like checking viewport rectangle intersection
|
99
|
+
or containment of a mouse click point in popular geometry shapes such as rectangle,
|
100
|
+
square, arc (open, chord, and pie), ellipse, circle, polygon, and paths containing
|
101
|
+
lines, quadratic bézier curves, and cubic bezier curves, potentially with affine
|
102
|
+
transforms applied like translation, scale, rotation, shear/skew, and inversion
|
103
|
+
(including both Ray Casting Algorithm, aka Even-odd Rule, and Winding Number Algorithm,
|
104
|
+
aka Nonzero Rule). Additionally, it contains some purely mathematical algorithms
|
105
|
+
like IEEEremainder (also known as IEEE-754 remainder).
|
104
106
|
email: andy.am@gmail.com
|
105
107
|
executables: []
|
106
108
|
extensions: []
|
@@ -114,6 +116,7 @@ files:
|
|
114
116
|
- README.md
|
115
117
|
- VERSION
|
116
118
|
- lib/perfect-shape.rb
|
119
|
+
- lib/perfect_shape/affine_transform.rb
|
117
120
|
- lib/perfect_shape/arc.rb
|
118
121
|
- lib/perfect_shape/circle.rb
|
119
122
|
- lib/perfect_shape/composite_shape.rb
|