perfect-shape 0.0.7 → 0.0.11

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7fdd723154a24a57aa233988884800a33af7b6094bcb93c5d9a75778fe2259b2
4
- data.tar.gz: df02fc673c2d24490f7883282790c0f6135c7ed910679020d8cea8f1f969cfc3
3
+ metadata.gz: '097e027a1c0c026611e480d05bf6281c74db9228964f6f06c414637bb60af8b0'
4
+ data.tar.gz: e2bab91460ec298a74e4735a13a123bf7b5f0ae9d15f8b5544a77aa7733cd312
5
5
  SHA512:
6
- metadata.gz: cb680e84b8f0356378634d316ed06fff07944d1ab35fe1954359beccbe58e03e77256f4fbb008bc8ab448fa559d91e5a812e1b19b60cefc61161f44ae9047032
7
- data.tar.gz: 1a32bf4f4bc3f572cbab061628e0302e0b38d49774f54d94ba8fb35a90cb67373d22de0ad2642da620ba5a85d20f5a6587faa518dab798aa1a75c8ef3638af90
6
+ metadata.gz: a532c231809348346907d2f369df7380cbac384772c3fc6d9858bfa423f5da6f0fd15e9eda3f94404f5579ed3572cab87d3c8d99b19c50d776ae6e999a7ff5a1
7
+ data.tar.gz: 4d7b76430666115f21053b7417c290f6260fb99f278eb8397d53ddf1c5c4a84b48b1c87d49c977bdfc54cd513c801be49ca1e586d9c0d7501e4d2a5eb648f8e4
data/CHANGELOG.md CHANGED
@@ -1,5 +1,30 @@
1
1
  # Change Log
2
2
 
3
+ ## 0.0.11
4
+
5
+ - `PerfectShape::Polygon#==`
6
+ - `PerfectShape::Line#==`
7
+ - `PerfectShape::Point#==`
8
+
9
+ ## 0.0.10
10
+
11
+ - `PerfectShape::Point`
12
+ - `PerfectShape::Point#point_distance`
13
+ - `PerfectShape::Point#contain?(x_or_point, y=nil, distance: 0)`
14
+ - Refactor `PerfectShape::Point`,`PerfectShape::RectangularShape` to include shared `PerfectShape::PointLocation`
15
+
16
+ ## 0.0.9
17
+
18
+ - `PerfectShape::Line#contain?(x_or_point, y=nil, distance: 0)` (add a distance tolerance fuzz factor option)
19
+
20
+ ## 0.0.8
21
+
22
+ - `PerfectShape::Line`
23
+ - `PerfectShape::Line#contain?(x_or_point, y=nil)`
24
+ - `PerfectShape::Line#relative_counterclockwise`
25
+ - `PerfectShape::Line#point_segment_distance`
26
+ - Update `PerfectShape::Math::radians_to_degrees`, `PerfectShape::Math::degrees_to_radians`, and `PerfectShape::Math::normalize_degrees` to normalize numbers to `BigDecimal`
27
+
3
28
  ## 0.0.7
4
29
 
5
30
  - `PerfectShape::Shape#min_x`/`PerfectShape::Shape#min_y`/`PerfectShape::Shape#max_x`/`PerfectShape::Shape#max_y`/`PerfectShape::Shape#center_x`/`PerfectShape::Shape#center_y`/`PerfectShape::Shape#bounding_box`
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # Perfect Shape 0.0.7
1
+ # Perfect Shape 0.0.11
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
 
@@ -13,13 +13,13 @@ To ensure high accuracy, this library does all its mathematical operations with
13
13
  Run:
14
14
 
15
15
  ```
16
- gem install perfect-shape -v 0.0.7
16
+ gem install perfect-shape -v 0.0.11
17
17
  ```
18
18
 
19
19
  Or include in Bundler `Gemfile`:
20
20
 
21
21
  ```ruby
22
- gem 'perfect-shape', '~> 0.0.7'
22
+ gem 'perfect-shape', '~> 0.0.11'
23
23
  ```
24
24
 
25
25
  And, run:
@@ -43,6 +43,8 @@ Module
43
43
 
44
44
  Class
45
45
 
46
+ This is a base class for all shapes. It is not meant to be used directly. Subclasses implement/override its methods as needed.
47
+
46
48
  - `#min_x`: min x
47
49
  - `#min_y`: min y
48
50
  - `#max_x`: max x
@@ -53,11 +55,24 @@ Class
53
55
  - `#center_y`: center y
54
56
  - `#bounding_box`: bounding box is a rectangle with x = min x, y = min y, and width/height just as those of shape
55
57
  - `#normalize_point(x_or_point, y = nil)`: normalizes point into an `Array` of (x,y) coordinates
58
+ - `#==(other)`: Returns `true` if equal to `other` or `false` otherwise
59
+
60
+ ### `PerfectShape::PointLocation`
61
+
62
+ Module
63
+
64
+ - `#initialize(x: 0, y: 0)`: initializes a point location, usually representing the top-left point in a shape
65
+ - `#x`: top-left x
66
+ - `#y`: top-left y
67
+ - `#min_x`: min x (x by default)
68
+ - `#min_y`: min y (y by default)
56
69
 
57
70
  ### `PerfectShape::RectangularShape`
58
71
 
59
72
  Module
60
73
 
74
+ Includes `PerfectShape::PointLocation`
75
+
61
76
  - `#initialize(x: 0, y: 0, width: 1, height: 1)`: initializes a rectangular shape
62
77
  - `#x`: top-left x
63
78
  - `#y`: top-left y
@@ -71,20 +86,68 @@ Module
71
86
  - `#center_y`: center y
72
87
  - `#bounding_box`: bounding box is a rectangle with x = min x, y = min y, and width/height of shape
73
88
 
89
+ ### `PerfectShape::Point`
90
+
91
+ Class
92
+
93
+ Extends `PerfectShape::Shape`
94
+
95
+ Includes `PerfectShape::PointLocation`
96
+
97
+ ![point](https://raw.githubusercontent.com/AndyObtiva/perfect-shape/master/images/point.png)
98
+
99
+ 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
+
101
+ - `::point_distance(x, y, px, py)`: Returns the distance from a point to another point
102
+ - `::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
+ - `#min_x`: min x (always x)
104
+ - `#min_y`: min y (always y)
105
+ - `#max_x`: max x (always x)
106
+ - `#max_y`: max y (always y)
107
+ - `#width`: width (always 0)
108
+ - `#height`: height (always 0)
109
+ - `#center_x`: center x (always x)
110
+ - `#center_y`: center y (always y)
111
+ - `#bounding_box`: bounding box is a rectangle with x = min x, y = min y, and width/height of shape
112
+ - `#contain?(x_or_point, y=nil, distance: 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 in a GUI more successfully.
113
+ - `#point_distance(x_or_point, y=nil)`: Returns the distance from a point to another point
114
+ - `#==(other)`: Returns `true` if equal to `other` or `false` otherwise
115
+
74
116
  ### `PerfectShape::Line`
75
117
 
76
118
  Class
119
+
77
120
  Extends `PerfectShape::Shape`
78
121
 
79
- - `::relative_ccw(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 used by Java 2D, 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)”.
122
+ ![line](https://raw.githubusercontent.com/AndyObtiva/perfect-shape/master/images/line.png)
123
+
124
+ - `::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 used by Java 2D, 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)”.
125
+ - `::point_segment_distance_square(x1, y1, x2, y2, px, py)`: Returns the square of distance from a point to a line segment.
126
+ - `::point_segment_distance(x1, y1, x2, y2, px, py)`: Returns the distance from a point to a line segment.
127
+ - `::new(points: nil)`: constructs a polygon with `points` as `Array` of `Array`s of (x,y) pairs or flattened `Array` of alternating x and y values
128
+ - `#min_x`: min x
129
+ - `#min_y`: min y
130
+ - `#max_x`: max x
131
+ - `#max_y`: max y
132
+ - `#width`: width (from min x to max x)
133
+ - `#height`: height (from min y to max y)
134
+ - `#center_x`: center x
135
+ - `#center_y`: center y
136
+ - `#bounding_box`: bounding box is a rectangle with x = min x, y = min y, and width/height of shape
137
+ - `#contain?(x_or_point, y=nil, distance: 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 in a GUI more successfully.
138
+ - `#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 used by Java 2D, 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)”.
139
+ - `#point_segment_distance(x_or_point, y=nil)`: Returns the distance from a point to a line segment.
140
+ - `#==(other)`: Returns `true` if equal to `other` or `false` otherwise
80
141
 
81
142
  ### `PerfectShape::Rectangle`
82
143
 
83
144
  Class
145
+
84
146
  Extends `PerfectShape::Shape`
147
+
85
148
  Includes `PerfectShape::RectangularShape`
86
149
 
87
- ![rectangle](images/rectangle.png)
150
+ ![rectangle](https://raw.githubusercontent.com/AndyObtiva/perfect-shape/master/images/rectangle.png)
88
151
 
89
152
  - `::new(x: 0, y: 0, width: 1, height: 1)`: constructs a rectangle
90
153
  - `#x`: top-left x
@@ -99,13 +162,15 @@ Includes `PerfectShape::RectangularShape`
99
162
  - `#max_y`: max y
100
163
  - `#bounding_box`: bounding box is a rectangle with x = min x, y = min y, and width/height of shape
101
164
  - `#contain?(x_or_point, y=nil)`: checks if point is inside
165
+ - `#==(other)`: Returns `true` if equal to `other` or `false` otherwise
102
166
 
103
167
  ### `PerfectShape::Square`
104
168
 
105
169
  Class
170
+
106
171
  Extends `PerfectShape::Rectangle`
107
172
 
108
- ![square](images/square.png)
173
+ ![square](https://raw.githubusercontent.com/AndyObtiva/perfect-shape/master/images/square.png)
109
174
 
110
175
  - `::new(x: 0, y: 0, length: 1)`: constructs a square
111
176
  - `#x`: top-left x
@@ -121,18 +186,21 @@ Extends `PerfectShape::Rectangle`
121
186
  - `#max_y`: max y
122
187
  - `#bounding_box`: bounding box is a rectangle with x = min x, y = min y, and width/height of shape
123
188
  - `#contain?(x_or_point, y=nil)`: checks if point is inside
189
+ - `#==(other)`: Returns `true` if equal to `other` or `false` otherwise
124
190
 
125
191
  ### `PerfectShape::Arc`
126
192
 
127
193
  Class
194
+
128
195
  Extends `PerfectShape::Shape`
196
+
129
197
  Includes `PerfectShape::RectangularShape`
130
198
 
131
199
  Arcs can be of type `:open`, `:chord`, or `:pie`
132
200
 
133
201
  Open Arc | Chord Arc | Pie Arc
134
202
  ---------|-----------|--------
135
- ![arc-open](images/arc-open.png) | ![arc-chord](images/arc-chord.png) | ![arc-pie](images/arc-pie.png)
203
+ ![arc-open](https://raw.githubusercontent.com/AndyObtiva/perfect-shape/master/images/arc-open.png) | ![arc-chord](https://raw.githubusercontent.com/AndyObtiva/perfect-shape/master/images/arc-chord.png) | ![arc-pie](https://raw.githubusercontent.com/AndyObtiva/perfect-shape/master/images/arc-pie.png)
136
204
 
137
205
  - `::new(type: :open, x: 0, y: 0, width: 1, height: 1, start: 0, extent: 360, center_x: nil, center_y: nil, radius_x: nil, radius_y: nil)`: constructs an arc of type `:open` (default), `:chord`, or `:pie`
138
206
  - `#type`: `:open`, `:chord`, or `:pie`
@@ -152,13 +220,15 @@ Open Arc | Chord Arc | Pie Arc
152
220
  - `#max_y`: max y
153
221
  - `#bounding_box`: bounding box is a rectangle with x = min x, y = min y, and width/height of shape
154
222
  - `#contain?(x_or_point, y=nil)`: checks if point is inside
223
+ - `#==(other)`: Returns `true` if equal to `other` or `false` otherwise
155
224
 
156
225
  ### `PerfectShape::Ellipse`
157
226
 
158
227
  Class
228
+
159
229
  Extends `PerfectShape::Arc`
160
230
 
161
- ![ellipse](images/ellipse.png)
231
+ ![ellipse](https://raw.githubusercontent.com/AndyObtiva/perfect-shape/master/images/ellipse.png)
162
232
 
163
233
  - `::new(x: 0, y: 0, width: 1, height: 1, center_x: nil, center_y: nil, radius_x: nil, radius_y: nil)`: constructs an ellipse
164
234
  - `#x`: top-left x
@@ -178,13 +248,15 @@ Extends `PerfectShape::Arc`
178
248
  - `#max_y`: max y
179
249
  - `#bounding_box`: bounding box is a rectangle with x = min x, y = min y, and width/height of shape
180
250
  - `#contain?(x_or_point, y=nil)`: checks if point is inside
251
+ - `#==(other)`: Returns `true` if equal to `other` or `false` otherwise
181
252
 
182
253
  ### `PerfectShape::Circle`
183
254
 
184
255
  Class
256
+
185
257
  Extends `PerfectShape::Ellipse`
186
258
 
187
- ![circle](images/circle.png)
259
+ ![circle](https://raw.githubusercontent.com/AndyObtiva/perfect-shape/master/images/circle.png)
188
260
 
189
261
  - `::new(x: 0, y: 0, diameter: 1, width: 1, height: 1, center_x: nil, center_y: nil, radius: nil, radius_x: nil, radius_y: nil)`: constructs a circle
190
262
  - `#x`: top-left x
@@ -206,15 +278,17 @@ Extends `PerfectShape::Ellipse`
206
278
  - `#max_y`: max y
207
279
  - `#bounding_box`: bounding box is a rectangle with x = min x, y = min y, and width/height of shape
208
280
  - `#contain?(x_or_point, y=nil)`: checks if point is inside
281
+ - `#==(other)`: Returns `true` if equal to `other` or `false` otherwise
209
282
 
210
283
  ### `PerfectShape::Polygon`
211
284
 
212
285
  Class
286
+
213
287
  Extends `PerfectShape::Shape`
214
288
 
215
- ![polygon](images/polygon.png)
289
+ ![polygon](https://raw.githubusercontent.com/AndyObtiva/perfect-shape/master/images/polygon.png)
216
290
 
217
- - `::new(points: nil)`: constructs a polygon with `points` as `Array` of `Array`s of (x,y) pairs or flattened `Array` of alternating x and y coordinates
291
+ - `::new(points: nil)`: constructs a polygon with `points` as `Array` of `Array`s of (x,y) pairs or flattened `Array` of alternating x and y values
218
292
  - `#min_x`: min x
219
293
  - `#min_y`: min y
220
294
  - `#max_x`: max x
@@ -225,6 +299,7 @@ Extends `PerfectShape::Shape`
225
299
  - `#center_y`: center y
226
300
  - `#bounding_box`: bounding box is a rectangle with x = min x, y = min y, and width/height of shape
227
301
  - `#contain?(x_or_point, y=nil)`: 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))
302
+ - `#==(other)`: Returns `true` if equal to `other` or `false` otherwise
228
303
 
229
304
  ## Process
230
305
 
@@ -233,7 +308,7 @@ Extends `PerfectShape::Shape`
233
308
  ## Resources
234
309
 
235
310
  - Rubydoc: https://www.rubydoc.info/gems/perfect-shape
236
- - AWT Geom JavaDoc: https://docs.oracle.com/javase/8/docs/api/java/awt/geom/package-summary.html
311
+ - AWT Geom JavaDoc (inspiration): https://docs.oracle.com/javase/8/docs/api/java/awt/geom/package-summary.html
237
312
 
238
313
  ## TODO
239
314
 
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.0.7
1
+ 0.0.11
@@ -135,7 +135,7 @@ module PerfectShape
135
135
  @height = nil
136
136
  end
137
137
 
138
- # Checks if arc contains point denoted by point (two-number Array or x, y args)
138
+ # Checks if arc contains point (two-number Array or x, y args)
139
139
  #
140
140
  # @param x The X coordinate of the point to test.
141
141
  # @param y The Y coordinate of the point to test.
@@ -178,8 +178,8 @@ module PerfectShape
178
178
  angle += Math.degrees_to_radians(-extent)
179
179
  x2 = Math.cos(angle)
180
180
  y2 = Math.sin(angle)
181
- inside = (Line.relative_ccw(x1, y1, x2, y2, 2*normx, 2*normy) *
182
- Line.relative_ccw(x1, y1, x2, y2, 0, 0) >= 0)
181
+ inside = (Line.relative_counterclockwise(x1, y1, x2, y2, 2*normx, 2*normy) *
182
+ Line.relative_counterclockwise(x1, y1, x2, y2, 0, 0) >= 0)
183
183
  inarc ? !inside : inside
184
184
  end
185
185
 
@@ -55,7 +55,7 @@ module PerfectShape
55
55
  end
56
56
  end
57
57
 
58
- # Checks if ellipse contains point denoted by point (two-number Array or x, y args)
58
+ # Checks if ellipse contains point (two-number Array or x, y args)
59
59
  #
60
60
  # @param x The X coordinate of the point to test.
61
61
  # @param y The Y coordinate of the point to test.
@@ -20,6 +20,7 @@
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/multi_point'
23
24
 
24
25
  module PerfectShape
25
26
  # Mostly ported from java.awt.geom: https://docs.oracle.com/javase/8/docs/api/java/awt/geom/Line2D.html
@@ -52,7 +53,7 @@ module PerfectShape
52
53
  # @return an integer that indicates the position of the third specified
53
54
  # coordinates with respect to the line segment formed
54
55
  # by the first two specified coordinates.
55
- def relative_ccw(x1, y1, x2, y2, px, py)
56
+ def relative_counterclockwise(x1, y1, x2, y2, px, py)
56
57
  x2 -= x1;
57
58
  y2 -= y1;
58
59
  px -= x1;
@@ -82,6 +83,136 @@ module PerfectShape
82
83
  end
83
84
  (ccw < 0.0) ? -1 : ((ccw > 0.0) ? 1 : 0);
84
85
  end
86
+
87
+ # Returns the square of the distance from a point to a line segment.
88
+ # The distance measured is the distance between the specified
89
+ # point and the closest point between the specified end points.
90
+ # If the specified point intersects the line segment in between the
91
+ # end points, this method returns 0.0.
92
+ #
93
+ # @param x1 the X coordinate of the start point of the
94
+ # specified line segment
95
+ # @param y1 the Y coordinate of the start point of the
96
+ # specified line segment
97
+ # @param x2 the X coordinate of the end point of the
98
+ # specified line segment
99
+ # @param y2 the Y coordinate of the end point of the
100
+ # specified line segment
101
+ # @param px the X coordinate of the specified point being
102
+ # measured against the specified line segment
103
+ # @param py the Y coordinate of the specified point being
104
+ # measured against the specified line segment
105
+ # @return a double value that is the square of the distance from the
106
+ # specified point to the specified line segment.
107
+ def point_segment_distance_square(x1, y1,
108
+ x2, y2,
109
+ px, py)
110
+ x1 = BigDecimal(x1.to_s)
111
+ y1 = BigDecimal(y1.to_s)
112
+ x2 = BigDecimal(x2.to_s)
113
+ y2 = BigDecimal(y2.to_s)
114
+ px = BigDecimal(px.to_s)
115
+ py = BigDecimal(py.to_s)
116
+ # Adjust vectors relative to x1,y1
117
+ # x2,y2 becomes relative vector from x1,y1 to end of segment
118
+ x2 -= x1
119
+ y2 -= y1
120
+ # px,py becomes relative vector from x1,y1 to test point
121
+ px -= x1
122
+ py -= y1
123
+ dot_product = px * x2 + py * y2;
124
+ if dot_product <= 0.0
125
+ # px,py is on the side of x1,y1 away from x2,y2
126
+ # distance to segment is length of px,py vector
127
+ # "length of its (clipped) projection" is now 0.0
128
+ projected_length_square = BigDecimal('0.0');
129
+ else
130
+ # switch to backwards vectors relative to x2,y2
131
+ # x2,y2 are already the negative of x1,y1=>x2,y2
132
+ # to get px,py to be the negative of px,py=>x2,y2
133
+ # the dot product of two negated vectors is the same
134
+ # as the dot product of the two normal vectors
135
+ px = x2 - px
136
+ py = y2 - py
137
+ dot_product = px * x2 + py * y2
138
+ if dot_product <= 0.0
139
+ # px,py is on the side of x2,y2 away from x1,y1
140
+ # distance to segment is length of (backwards) px,py vector
141
+ # "length of its (clipped) projection" is now 0.0
142
+ projected_length_square = BigDecimal('0.0')
143
+ else
144
+ # px,py is between x1,y1 and x2,y2
145
+ # dot_product is the length of the px,py vector
146
+ # projected on the x2,y2=>x1,y1 vector times the
147
+ # length of the x2,y2=>x1,y1 vector
148
+ projected_length_square = dot_product * dot_product / (x2 * x2 + y2 * y2)
149
+ end
150
+ end
151
+ # Distance to line is now the length of the relative point
152
+ # vector minus the length of its projection onto the line
153
+ # (which is zero if the projection falls outside the range
154
+ # of the line segment).
155
+ length_square = px * px + py * py - projected_length_square
156
+ length_square = BigDecimal('0.0') if length_square < 0
157
+ length_square
158
+ end
159
+
160
+ # Returns the distance from a point to a line segment.
161
+ # The distance measured is the distance between the specified
162
+ # point and the closest point between the specified end points.
163
+ # If the specified point intersects the line segment in between the
164
+ # end points, this method returns 0.0.
165
+ #
166
+ # @param x1 the X coordinate of the start point of the
167
+ # specified line segment
168
+ # @param y1 the Y coordinate of the start point of the
169
+ # specified line segment
170
+ # @param x2 the X coordinate of the end point of the
171
+ # specified line segment
172
+ # @param y2 the Y coordinate of the end point of the
173
+ # specified line segment
174
+ # @param px the X coordinate of the specified point being
175
+ # measured against the specified line segment
176
+ # @param py the Y coordinate of the specified point being
177
+ # measured against the specified line segment
178
+ # @return a double value that is the distance from the specified point
179
+ # to the specified line segment.
180
+ def point_segment_distance(x1, y1,
181
+ x2, y2,
182
+ px, py)
183
+ BigDecimal(::Math.sqrt(point_segment_distance_square(x1, y1, x2, y2, px, py)).to_s)
184
+ end
185
+ end
186
+
187
+ include MultiPoint
188
+ include Equalizer.new(:points)
189
+
190
+ # Checks if line contains point (two-number Array or x, y args), with distance tolerance (0 by default)
191
+ #
192
+ # @param x The X coordinate of the point to test.
193
+ # @param y The Y coordinate of the point to test.
194
+ # @param distance The distance from line to tolerate (0 by default)
195
+ #
196
+ # @return {@code true} if the point lies within the bound of
197
+ # the line, {@code false} if the point lies outside of the
198
+ # line's bounds.
199
+ def contain?(x_or_point, y = nil, distance: 0)
200
+ x, y = normalize_point(x_or_point, y)
201
+ return unless x && y
202
+ distance = BigDecimal(distance.to_s)
203
+ point_segment_distance(x, y) <= distance
204
+ end
205
+
206
+ def point_segment_distance(x_or_point, y = nil)
207
+ x, y = normalize_point(x_or_point, y)
208
+ return unless x && y
209
+ Line.point_segment_distance(points[0][0], points[0][1], points[1][0], points[1][1], x, y)
210
+ end
211
+
212
+ def relative_counterclockwise(x_or_point, y = nil)
213
+ x, y = normalize_point(x_or_point, y)
214
+ return unless x && y
215
+ Line.relative_counterclockwise(points[0][0], points[0][1], points[1][0], points[1][1], x, y)
85
216
  end
86
217
  end
87
218
  end
@@ -6,33 +6,34 @@ module PerfectShape
6
6
  # Also includes standard Ruby ::Math utility methods
7
7
  module Math
8
8
  class << self
9
- # converts angle from radians to degrees
9
+ # converts angle from radians to degrees (normalizing to BigDecimal)
10
10
  def radians_to_degrees(radians)
11
- (180/Math::PI)*radians
11
+ (BigDecimal('180')/Math::PI)*BigDecimal(radians.to_s)
12
12
  end
13
13
 
14
- # converts angle from degrees to radians
14
+ # converts angle from degrees to radians (normalizing to BigDecimal)
15
15
  def degrees_to_radians(degrees)
16
- (Math::PI/180)*degrees
16
+ (Math::PI/BigDecimal('180'))*BigDecimal(degrees.to_s)
17
17
  end
18
18
 
19
19
  # Normalizes the specified angle into the range -180 to 180.
20
20
  def normalize_degrees(angle)
21
+ angle = BigDecimal(angle.to_s)
21
22
  if angle > 180.0
22
23
  if angle <= (180.0 + 360.0)
23
- angle = angle - 360.0
24
+ angle = angle - BigDecimal('360.0')
24
25
  else
25
26
  angle = Math.ieee_remainder(angle, 360.0)
26
27
  # IEEEremainder can return -180 here for some input values...
27
- angle = 180.0 if angle == -180.0
28
+ angle = BigDecimal('180.0') if angle == -180.0
28
29
  end
29
30
  elsif angle <= -180.0
30
31
  if angle > (-180.0 - 360.0)
31
- angle = angle + 360.0
32
+ angle = angle + BigDecimal('360.0')
32
33
  else
33
34
  angle = Math.ieee_remainder(angle, 360.0)
34
35
  # IEEEremainder can return -180 here for some input values...
35
- angle = 180.0 if angle == -180.0
36
+ angle = BigDecimal('180.0') if angle == -180.0
36
37
  end
37
38
  end
38
39
  angle
@@ -0,0 +1,59 @@
1
+ # Copyright (c) 2021 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/shape'
23
+
24
+ module PerfectShape
25
+ # Represents multi-point shapes like Line, Polygon, and Polyline
26
+ module MultiPoint
27
+ attr_reader :points
28
+
29
+ def initialize(points: nil)
30
+ self.points = points || []
31
+ end
32
+
33
+ # Sets points, normalizing to an Array of Arrays of (x,y) pairs as BigDecimal
34
+ def points=(the_points)
35
+ unless the_points.first.is_a?(Array)
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
40
+ @points = the_points.map {|pair| [BigDecimal(pair.first.to_s), BigDecimal(pair.last.to_s)]}
41
+ end
42
+
43
+ def min_x
44
+ points.map(&:first).min
45
+ end
46
+
47
+ def min_y
48
+ points.map(&:last).min
49
+ end
50
+
51
+ def max_x
52
+ points.map(&:first).max
53
+ end
54
+
55
+ def max_y
56
+ points.map(&:last).max
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,82 @@
1
+ # Copyright (c) 2021 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/shape'
23
+ require 'perfect_shape/point_location'
24
+
25
+ module PerfectShape
26
+ class Point < Shape
27
+ class << self
28
+ def point_distance(x, y, px, py)
29
+ x = BigDecimal(x.to_s)
30
+ y = BigDecimal(y.to_s)
31
+ px = BigDecimal(px.to_s)
32
+ py = BigDecimal(py.to_s)
33
+ BigDecimal(Math.sqrt((px - x)**2 + (py - y)**2).to_s)
34
+ end
35
+ end
36
+
37
+ include PointLocation
38
+ include Equalizer.new(:x, :y)
39
+
40
+ def initialize(x_or_point = nil, y_arg = nil, x: nil, y: nil)
41
+ if x_or_point.is_a?(Array)
42
+ x, y = x_or_point
43
+ super(x: x, y: y)
44
+ elsif x_or_point && y_arg
45
+ super(x: x_or_point, y: y_arg)
46
+ else
47
+ x ||= 0
48
+ y ||= 0
49
+ super(x: x, y: y)
50
+ end
51
+ end
52
+
53
+ def max_x
54
+ x
55
+ end
56
+
57
+ def max_y
58
+ y
59
+ end
60
+
61
+ # Checks if points match, with distance tolerance (0 by default)
62
+ #
63
+ # @param x The X coordinate of the point to test.
64
+ # @param y The Y coordinate of the point to test.
65
+ # @param distance The distance from point to tolerate (0 by default)
66
+ #
67
+ # @return {@code true} if the point is close enough within distance tolerance,
68
+ # {@code false} if the point is too far.
69
+ def contain?(x_or_point, y = nil, distance: 0)
70
+ x, y = normalize_point(x_or_point, y)
71
+ return unless x && y
72
+ distance = BigDecimal(distance.to_s)
73
+ point_distance(x, y) <= distance
74
+ end
75
+
76
+ def point_distance(x_or_point, y = nil)
77
+ x, y = normalize_point(x_or_point, y)
78
+ return unless x && y
79
+ Point.point_distance(self.x, self.y, x, y)
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,53 @@
1
+ # Copyright (c) 2021 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
+ module PerfectShape
23
+ module PointLocation
24
+ attr_reader :x, :y
25
+
26
+ # Calls super before setting x,y (default: 0,0)
27
+ def initialize(x: 0, y: 0)
28
+ super()
29
+ self.x = x
30
+ self.y = y
31
+ end
32
+
33
+ # Sets x, normalizing to BigDecimal
34
+ def x=(value)
35
+ @x = BigDecimal(value.to_s)
36
+ end
37
+
38
+ # Sets y, normalizing to BigDecimal
39
+ def y=(value)
40
+ @y = BigDecimal(value.to_s)
41
+ end
42
+
43
+ # Returns x by default. Subclasses may override.
44
+ def min_x
45
+ x
46
+ end
47
+
48
+ # Returns y by default. Subclasses may override.
49
+ def min_y
50
+ y
51
+ end
52
+ end
53
+ end
@@ -20,51 +20,15 @@
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/multi_point'
23
24
 
24
25
  module PerfectShape
25
26
  # Mostly ported from java.awt.geom: https://docs.oracle.com/javase/8/docs/api/java/awt/Polygon.html
26
27
  class Polygon < Shape
27
- attr_reader :points
28
+ include MultiPoint
29
+ include Equalizer.new(:points)
28
30
 
29
- def initialize(points: nil)
30
- self.points = points || []
31
- end
32
-
33
- # Sets points, normalizing to an Array of Arrays of (x,y) pairs as BigDecimal
34
- def points=(the_points)
35
- unless the_points.first.is_a?(Array)
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
40
- @points = the_points.map {|pair| [BigDecimal(pair.first.to_s), BigDecimal(pair.last.to_s)]}
41
- end
42
-
43
- def min_x
44
- points.map(&:first).min
45
- end
46
-
47
- def min_y
48
- points.map(&:last).min
49
- end
50
-
51
- def max_x
52
- points.map(&:first).max
53
- end
54
-
55
- def max_y
56
- points.map(&:last).max
57
- end
58
-
59
- def width
60
- max_x - min_x if min_x && max_x
61
- end
62
-
63
- def height
64
- max_y - min_y if min_y && max_y
65
- end
66
-
67
- # Checks if polygon contains point denoted by point (two-number Array or x, y args)
31
+ # Checks if polygon contains point (two-number Array or x, y args)
68
32
  # using the Ray Casting Algorithm (aka Even-Odd Rule): https://en.wikipedia.org/wiki/Point_in_polygon
69
33
  #
70
34
  # @param x The X coordinate of the point to test.
@@ -28,7 +28,7 @@ module PerfectShape
28
28
  include RectangularShape
29
29
  include Equalizer.new(:x, :y, :width, :height)
30
30
 
31
- # Checks if rectangle contains point denoted by point (two-number Array or x, y args)
31
+ # Checks if rectangle contains point (two-number Array or x, y args)
32
32
  #
33
33
  # @param x The X coordinate of the point to test.
34
34
  # @param y The Y coordinate of the point to test.
@@ -19,31 +19,23 @@
19
19
  # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
20
  # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21
21
 
22
+ require 'perfect_shape/point_location'
23
+
22
24
  module PerfectShape
23
25
  # Mixin Module for Rectangular Shapes (having x, y, width, height)
24
26
  # Can only be mixed into a class extending Shape or another module
25
27
  module RectangularShape
26
- attr_reader :x, :y, :width, :height
28
+ include PointLocation
29
+
30
+ attr_reader :width, :height
27
31
 
28
32
  # Calls super before setting x, y, width, height
29
33
  def initialize(x: 0, y: 0, width: 1, height: 1)
30
- super()
31
- self.x = x
32
- self.y = y
34
+ super(x: x, y: y)
33
35
  self.width = width
34
36
  self.height = height
35
37
  end
36
38
 
37
- # Sets x, normalizing to BigDecimal
38
- def x=(value)
39
- @x = BigDecimal(value.to_s)
40
- end
41
-
42
- # Sets y, normalizing to BigDecimal
43
- def y=(value)
44
- @y = BigDecimal(value.to_s)
45
- end
46
-
47
39
  # Sets width, normalizing to BigDecimal
48
40
  def width=(value)
49
41
  @width = BigDecimal(value.to_s)
@@ -54,14 +46,6 @@ module PerfectShape
54
46
  @height = BigDecimal(value.to_s)
55
47
  end
56
48
 
57
- def min_x
58
- @x
59
- end
60
-
61
- def min_y
62
- @y
63
- end
64
-
65
49
  def max_x
66
50
  @x + width if @x && width
67
51
  end
@@ -20,7 +20,8 @@
20
20
  # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21
21
 
22
22
  module PerfectShape
23
- # Superclass of all shapes
23
+ # Superclass of all shapes. Not meant to be used directly.
24
+ # Subclasses must implement/override methods as needed.
24
25
  class Shape
25
26
  # Subclasses must implement
26
27
  def min_x
@@ -38,12 +39,16 @@ module PerfectShape
38
39
  def max_y
39
40
  end
40
41
 
41
- # Subclasses must implement
42
+ # Default implementation is max_x - min_x
43
+ # Subclasses can override
42
44
  def width
45
+ max_x - min_x if max_x && min_x
43
46
  end
44
47
 
45
- # Subclasses must implement
48
+ # Default implementation is max_y - min_y
49
+ # Subclasses can override
46
50
  def height
51
+ max_y - min_y if max_y && min_y
47
52
  end
48
53
 
49
54
  # center_x is min_x + width/2.0 by default
@@ -78,5 +83,9 @@ module PerfectShape
78
83
  y = BigDecimal(y.to_s)
79
84
  [x, y]
80
85
  end
86
+
87
+ # Subclasses must implement
88
+ def ==(other)
89
+ end
81
90
  end
82
91
  end
@@ -2,16 +2,16 @@
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.0.7 ruby lib
5
+ # stub: perfect-shape 0.0.11 ruby lib
6
6
 
7
7
  Gem::Specification.new do |s|
8
8
  s.name = "perfect-shape".freeze
9
- s.version = "0.0.7"
9
+ s.version = "0.0.11"
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 = "2021-12-17"
14
+ s.date = "2021-12-20"
15
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 (Ray Casting Algorithm aka Even-Odd Rule), polyline, polyquad, polycubic, and paths containing lines, bezier curves, and quadratic curves. 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 = [
@@ -30,6 +30,9 @@ Gem::Specification.new do |s|
30
30
  "lib/perfect_shape/ellipse.rb",
31
31
  "lib/perfect_shape/line.rb",
32
32
  "lib/perfect_shape/math.rb",
33
+ "lib/perfect_shape/multi_point.rb",
34
+ "lib/perfect_shape/point.rb",
35
+ "lib/perfect_shape/point_location.rb",
33
36
  "lib/perfect_shape/polygon.rb",
34
37
  "lib/perfect_shape/rectangle.rb",
35
38
  "lib/perfect_shape/rectangular_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.0.7
4
+ version: 0.0.11
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andy Maleh
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-12-17 00:00:00.000000000 Z
11
+ date: 2021-12-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: equalizer
@@ -119,6 +119,9 @@ files:
119
119
  - lib/perfect_shape/ellipse.rb
120
120
  - lib/perfect_shape/line.rb
121
121
  - lib/perfect_shape/math.rb
122
+ - lib/perfect_shape/multi_point.rb
123
+ - lib/perfect_shape/point.rb
124
+ - lib/perfect_shape/point_location.rb
122
125
  - lib/perfect_shape/polygon.rb
123
126
  - lib/perfect_shape/rectangle.rb
124
127
  - lib/perfect_shape/rectangular_shape.rb