perfect-shape 0.1.2 → 0.3.2

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: cfa0c67a9d8a4b23b53bdd7895121fa51ee3da17e1d22fa9bb56c0890a47844b
4
- data.tar.gz: 81cd3fe5766f023fc250951cab6541142d80ff70b8af3d8cbbfcefcb56154cba
3
+ metadata.gz: 8f9bd6a8a2c7c3d3ec4ec3fd3e5b311a9eec08fa63e35666547534a2e19273c6
4
+ data.tar.gz: 4dbcd6b139e118c555d50681efe4e770cf796774a904459994a64c2bb19d5140
5
5
  SHA512:
6
- metadata.gz: ea4539cd6122363483988ef9de8b712ebd981c2e7102d5510e94f9d093bad1c02d17cd0e1c4eebe5ebc3e4211a67d22ca7ee2630cdf561404a9adc36031de220
7
- data.tar.gz: 4a9fcfa3accf7ca519c403e24076bfc76b1bd9e2b47fda886e0012af610d3e142257d09c6508a04deccf665b536c41b60cc68047ba2c76f06a667be2625b0c40
6
+ metadata.gz: e30baa5bb362e5e540af6f67ba9c99ff0f5f0469f48a724823a76522111d6ea86e1bce6cb6b273b056cc9bf7e8f92bd667103aa348abbc237e4ffec082154434
7
+ data.tar.gz: 7627cabd6ee3ef671ed802d6846ad1c192065cc4a5bf6d00062a7d8da7d3376c3d84b836664413a5033644dfa839d19a8634badf851e4aa050e6f03cdef031bc
data/CHANGELOG.md CHANGED
@@ -1,5 +1,31 @@
1
1
  # Change Log
2
2
 
3
+ ## 0.3.2
4
+
5
+ - Check point containment in cubic bezier curve outline with distance tolerance (new method signature: `PerfectShape::CubicBezierCurve#contain?(x_or_point, y = nil, outline: false, distance_tolerance: 0)`)
6
+ - `PerfectShape::CubicBezierCurve#curve_center_point`, `PerfectShape::CubicBezierCurve#curve_center_x`, `PerfectShape::CubicBezierCurve#curve_center_y`
7
+ - `PerfectShape::CubicBezierCurve#subdivisions(number=2)`
8
+ - `PerfectShape::CubicBezierCurve#point_segment_distance(x_or_point, y = nil, minimum_distance_threshold: OUTLINE_MINIMUM_DISTANCE_THRESHOLD)`
9
+
10
+ ## 0.3.1
11
+
12
+ - Check point containment in arc outline with distance tolerance (new method signature: `PerfectShape::Arc#contain?(x_or_point, y = nil, outline: false, distance_tolerance: 0)`)
13
+ - Check point containment in ellipse outline with distance tolerance (new method signature: `PerfectShape::Ellipse#contain?(x_or_point, y = nil, outline: false, distance_tolerance: 0)`)
14
+ - Check point containment in circle outline with distance tolerance (new method signature: `PerfectShape::Circle#contain?(x_or_point, y = nil, outline: false, distance_tolerance: 0)`)
15
+
16
+ ## 0.3.0
17
+
18
+ - Refactoring: rename `distance` option for `#contain?` on `Point`/`Line` into `distance_tolerance`
19
+ - Check point containment in rectangle outline with distance tolerance (new method signature: `PerfectShape::Rectangle#contain?(x_or_point, y = nil, outline: false, distance_tolerance: 0)`)
20
+ - Check point containment in square outline with distance tolerance (new method signature: `PerfectShape::Square#contain?(x_or_point, y = nil, outline: false, distance_tolerance: 0)`)
21
+ - Check point containment in polygon outline with distance tolerance (new method signature: `PerfectShape::Polygon#contain?(x_or_point, y = nil, outline: false, distance_tolerance: 0)`)
22
+
23
+ ## 0.2.0
24
+
25
+ - `PerfectShape::CompositeShape`: aggregate of multiple shapes
26
+ - `PerfectShape::CompositeShape#contain?(x_or_point, y=nil)`
27
+ - `PerfectShape::CompositeShape#==`
28
+
3
29
  ## 0.1.2
4
30
 
5
31
  - `PerfectShape::CubicBezierCurve` (two end points and two control points)
@@ -17,7 +43,7 @@
17
43
  ## 0.1.0
18
44
 
19
45
  - `PerfectShape::Path` (having points or lines)
20
- - `PerfectShape::Path#contain?(x_or_point, y=nil, distance: 0)`
46
+ - `PerfectShape::Path#contain?(x_or_point, y=nil, distance_tolerance: 0)`
21
47
  - `PerfectShape::Path#point_crossings(x_or_point, y=nil)`
22
48
  - `PerfectShape::Path#==`
23
49
 
@@ -31,12 +57,12 @@
31
57
 
32
58
  - `PerfectShape::Point`
33
59
  - `PerfectShape::Point#point_distance`
34
- - `PerfectShape::Point#contain?(x_or_point, y=nil, distance: 0)`
60
+ - `PerfectShape::Point#contain?(x_or_point, y=nil, distance_tolerance: 0)`
35
61
  - Refactor `PerfectShape::Point`,`PerfectShape::RectangularShape` to include shared `PerfectShape::PointLocation`
36
62
 
37
63
  ## 0.0.9
38
64
 
39
- - `PerfectShape::Line#contain?(x_or_point, y=nil, distance: 0)` (add a distance tolerance fuzz factor option)
65
+ - `PerfectShape::Line#contain?(x_or_point, y=nil, distance_tolerance: 0)` (add a distance tolerance fuzz factor option)
40
66
 
41
67
  ## 0.0.8
42
68
 
data/README.md CHANGED
@@ -1,9 +1,9 @@
1
- # Perfect Shape 0.1.2
1
+ # Perfect Shape 0.3.2
2
2
  ## Geometric Algorithms
3
3
  [![Gem Version](https://badge.fury.io/rb/perfect-shape.svg)](http://badge.fury.io/rb/perfect-shape)
4
4
  [![Test](https://github.com/AndyObtiva/perfect-shape/actions/workflows/ruby.yml/badge.svg)](https://github.com/AndyObtiva/perfect-shape/actions/workflows/ruby.yml)
5
5
 
6
- [`PerfectShape`](https://rubygems.org/gems/perfect-shape) is a collection of pure Ruby geometric algorithms that are mostly useful for GUI (Graphical User Interface) manipulation like checking 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 bézier 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 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)).
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.1.2
17
+ gem install perfect-shape -v 0.3.2
18
18
  ```
19
19
 
20
20
  Or include in Bundler `Gemfile`:
21
21
 
22
22
  ```ruby
23
- gem 'perfect-shape', '~> 0.1.2'
23
+ gem 'perfect-shape', '~> 0.3.2'
24
24
  ```
25
25
 
26
26
  And, run:
@@ -55,8 +55,9 @@ This is a base class for all shapes. It is not meant to be used directly. Subcla
55
55
  - `#center_x`: center x
56
56
  - `#center_y`: center y
57
57
  - `#bounding_box`: bounding box is a rectangle with x = min x, y = min y, and width/height just as those of shape
58
- - `#normalize_point(x_or_point, y = nil)`: normalizes point into an `Array` of `[x,y]` coordinates
59
58
  - `#==(other)`: Returns `true` if equal to `other` or `false` otherwise
59
+ - `#normalize_point(x_or_point, y = nil)`: normalizes point into an `Array` of `[x,y]` coordinates
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
60
61
 
61
62
  ### `PerfectShape::PointLocation`
62
63
 
@@ -110,14 +111,23 @@ Points are simply represented by an `Array` of `[x,y]` coordinates when used wit
110
111
  - `#center_x`: center x (always x)
111
112
  - `#center_y`: center y (always y)
112
113
  - `#bounding_box`: bounding box is a rectangle with x = min x, y = min y, and width/height of shape
113
- - `#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.
114
- - `#point_distance(x_or_point, y=nil)`: Returns the distance from a point to another point
115
114
  - `#==(other)`: Returns `true` if equal to `other` or `false` otherwise
115
+ - `#contain?(x_or_point, y=nil, 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.
116
+ - `#point_distance(x_or_point, y=nil)`: Returns the distance from a point to another point
116
117
 
117
118
  Example:
118
119
 
119
120
  ```ruby
121
+ require 'perfect-shape'
122
+
120
123
  shape = PerfectShape::Point.new(x: 200, y: 150)
124
+
125
+ shape.contain?(200, 150) # => true
126
+ shape.contain?([200, 150]) # => true
127
+ shape.contain?(200, 151) # => false
128
+ shape.contain?([200, 151]) # => false
129
+ shape.contain?(200, 151, distance_tolerance: 5) # => true
130
+ shape.contain?([200, 151], distance_tolerance: 5) # => true
121
131
  ```
122
132
 
123
133
  ### `PerfectShape::Line`
@@ -143,15 +153,24 @@ Includes `PerfectShape::MultiPoint`
143
153
  - `#center_x`: center x
144
154
  - `#center_y`: center y
145
155
  - `#bounding_box`: bounding box is a rectangle with x = min x, y = min y, and width/height of shape
146
- - `#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.
156
+ - `#==(other)`: Returns `true` if equal to `other` or `false` otherwise
157
+ - `#contain?(x_or_point, y=nil, 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.
147
158
  - `#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)”.
148
159
  - `#point_segment_distance(x_or_point, y=nil)`: Returns the distance from a point to a line segment.
149
- - `#==(other)`: Returns `true` if equal to `other` or `false` otherwise
150
160
 
151
161
  Example:
152
162
 
153
163
  ```ruby
154
- shape = PerfectShape::Line.new(points: [[200, 150], [270, 220]]) # start point and end point
164
+ require 'perfect-shape'
165
+
166
+ shape = PerfectShape::Line.new(points: [[0, 0], [100, 100]]) # start point and end point
167
+
168
+ shape.contain?(50, 50) # => true
169
+ shape.contain?([50, 50]) # => true
170
+ shape.contain?(50, 51) # => false
171
+ shape.contain?([50, 51]) # => false
172
+ shape.contain?(50, 51, distance_tolerance: 5) # => true
173
+ shape.contain?([50, 51], distance_tolerance: 5) # => true
155
174
  ```
156
175
 
157
176
  ### `PerfectShape::QuadraticBezierCurve`
@@ -175,13 +194,18 @@ Includes `PerfectShape::MultiPoint`
175
194
  - `#center_x`: center x
176
195
  - `#center_y`: center y
177
196
  - `#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)
178
- - `#contain?(x_or_point, y=nil)`: checks if point is inside
179
197
  - `#==(other)`: Returns `true` if equal to `other` or `false` otherwise
198
+ - `#contain?(x_or_point, y=nil)`: checks if point is inside
180
199
 
181
200
  Example:
182
201
 
183
202
  ```ruby
184
- shape = PerfectShape::QuadraticBezierCurve.new(points: [[200, 150], [270, 220], [180, 170]]) # start point, control point, and end point
203
+ require 'perfect-shape'
204
+
205
+ shape = PerfectShape::QuadraticBezierCurve.new(points: [[200, 150], [270, 320], [380, 150]]) # start point, control point, and end point
206
+
207
+ shape.contain?(270, 220) # => true
208
+ shape.contain?([270, 220]) # => true
185
209
  ```
186
210
 
187
211
  ### `PerfectShape::CubicBezierCurve`
@@ -205,13 +229,31 @@ Includes `PerfectShape::MultiPoint`
205
229
  - `#center_x`: center x
206
230
  - `#center_y`: center y
207
231
  - `#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)
208
- - `#contain?(x_or_point, y=nil)`: checks if point is inside
209
232
  - `#==(other)`: Returns `true` if equal to `other` or `false` otherwise
233
+ - `#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 cubic bezier curve shape from its outline more successfully
234
+ - `#curve_center_point`: point at the center of the curve (not the center of the bounding box area like `center_x` and `center_y`)
235
+ - `#curve_center_x`: point x coordinate at the center of the curve (not the center of the bounding box area like `center_x` and `center_y`)
236
+ - `#curve_center_y`: point y coordinate at the center of the curve (not the center of the bounding box area like `center_x` and `center_y`)
237
+ - `#subdivisions(number=2)`: subdivides cubic bezier curve at its center into into 2 cubic bezier curves by default, or more if `number` is specified. `number` must be an even number, or it will be rounded up to the closest even number.
238
+ - `#point_segment_distance(x_or_point, y=nil, minimum_distance_threshold: OUTLINE_MINIMUM_DISTANCE_THRESHOLD)`: calculates distance from point to curve segment. It does so by subdividing curve into smaller curves and checking against the curve center points until the distance is less than `minimum_distance_threshold`, to avoid being an overly costly operation.
210
239
 
211
240
  Example:
212
241
 
213
242
  ```ruby
214
- shape = PerfectShape::CubicBezierCurve.new(points: [[200, 150], [230, 50], [270, 220], [180, 170]]) # start point, two control points, and end point
243
+ require 'perfect-shape'
244
+
245
+ shape = PerfectShape::CubicBezierCurve.new(points: [[200, 150], [235, 235], [270, 320], [380, 150]]) # start point, two control points, and end point
246
+
247
+ shape.contain?(270, 220) # => true
248
+ shape.contain?([270, 220]) # => true
249
+ shape.contain?(270, 220, outline: true) # => false
250
+ shape.contain?([270, 220], outline: true) # => false
251
+ shape.contain?(261.875, 245.625, outline: true) # => true
252
+ shape.contain?([261.875, 245.625], outline: true) # => true
253
+ shape.contain?(261.875, 246.625, outline: true) # => false
254
+ shape.contain?([261.875, 246.625], outline: true) # => false
255
+ shape.contain?(261.875, 246.625, outline: true, distance_tolerance: 1) # => true
256
+ shape.contain?([261.875, 246.625], outline: true, distance_tolerance: 1) # => true
215
257
  ```
216
258
 
217
259
  ### `PerfectShape::Rectangle`
@@ -236,13 +278,26 @@ Includes `PerfectShape::RectangularShape`
236
278
  - `#max_x`: max x
237
279
  - `#max_y`: max y
238
280
  - `#bounding_box`: bounding box is a rectangle with x = min x, y = min y, and width/height of shape
239
- - `#contain?(x_or_point, y=nil)`: checks if point is inside
240
281
  - `#==(other)`: Returns `true` if equal to `other` or `false` otherwise
282
+ - `#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
241
283
 
242
284
  Example:
243
285
 
244
286
  ```ruby
287
+ require 'perfect-shape'
288
+
245
289
  shape = PerfectShape::Rectangle.new(x: 15, y: 30, width: 200, height: 100)
290
+
291
+ shape.contain?(115, 80) # => true
292
+ shape.contain?([115, 80]) # => true
293
+ shape.contain?(115, 80, outline: true) # => false
294
+ shape.contain?([115, 80], outline: true) # => false
295
+ shape.contain?(115, 30, outline: true) # => true
296
+ shape.contain?([115, 30], outline: true) # => true
297
+ shape.contain?(115, 31, outline: true) # => false
298
+ shape.contain?([115, 31], outline: true) # => false
299
+ shape.contain?(115, 31, outline: true, distance_tolerance: 1) # => true
300
+ shape.contain?([115, 31], outline: true, distance_tolerance: 1) # => true
246
301
  ```
247
302
 
248
303
  ### `PerfectShape::Square`
@@ -266,13 +321,26 @@ Extends `PerfectShape::Rectangle`
266
321
  - `#max_x`: max x
267
322
  - `#max_y`: max y
268
323
  - `#bounding_box`: bounding box is a rectangle with x = min x, y = min y, and width/height of shape
269
- - `#contain?(x_or_point, y=nil)`: checks if point is inside
270
324
  - `#==(other)`: Returns `true` if equal to `other` or `false` otherwise
325
+ - `#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 square shape from its outline more successfully
271
326
 
272
327
  Example:
273
328
 
274
329
  ```ruby
330
+ require 'perfect-shape'
331
+
275
332
  shape = PerfectShape::Square.new(x: 15, y: 30, length: 200)
333
+
334
+ shape.contain?(115, 130) # => true
335
+ shape.contain?([115, 130]) # => true
336
+ shape.contain?(115, 130, outline: true) # => false
337
+ shape.contain?([115, 130], outline: true) # => false
338
+ shape.contain?(115, 30, outline: true) # => true
339
+ shape.contain?([115, 30], outline: true) # => true
340
+ shape.contain?(115, 31, outline: true) # => false
341
+ shape.contain?([115, 31], outline: true) # => false
342
+ shape.contain?(115, 31, outline: true, distance_tolerance: 1) # => true
343
+ shape.contain?([115, 31], outline: true, distance_tolerance: 1) # => true
276
344
  ```
277
345
 
278
346
  ### `PerfectShape::Arc`
@@ -306,14 +374,101 @@ Open Arc | Chord Arc | Pie Arc
306
374
  - `#max_x`: max x
307
375
  - `#max_y`: max y
308
376
  - `#bounding_box`: bounding box is a rectangle with x = min x, y = min y, and width/height of shape
309
- - `#contain?(x_or_point, y=nil)`: checks if point is inside
310
377
  - `#==(other)`: Returns `true` if equal to `other` or `false` otherwise
378
+ - `#contain?(x_or_point, y=nil, outline: false, distance_tolerance: 0)`: checks if point is inside when `outline` is `false` or if point is on the outline when `outline` is `true`. `distance_tolerance` can be used as a fuzz factor when `outline` is `true`, for example, to help GUI users mouse-click-select an arc shape from its outline more successfully
311
379
 
312
380
  Example:
313
381
 
314
382
  ```ruby
315
- shape = PerfectShape::Arc.new(type: :chord, x: 2, y: 3, width: 50, height: 60, start: 30, extent: 90)
316
- shape2 = PerfectShape::Arc.new(type: :chord, center_x: 2 + 25, center_y: 3 + 30, radius_x: 25, radius_y: 30, start: 30, extent: 90)
383
+ require 'perfect-shape'
384
+
385
+ shape = PerfectShape::Arc.new(type: :open, x: 2, y: 3, width: 50, height: 60, start: 45, extent: 270)
386
+ shape2 = PerfectShape::Arc.new(type: :open, center_x: 2 + 25, center_y: 3 + 30, radius_x: 25, radius_y: 30, start: 45, extent: 270)
387
+
388
+ shape.contain?(39.5, 33.0) # => true
389
+ shape.contain?([39.5, 33.0]) # => true
390
+ shape2.contain?(39.5, 33.0) # => true
391
+ shape2.contain?([39.5, 33.0]) # => true
392
+ shape.contain?(39.5, 33.0, outline: true) # => false
393
+ shape.contain?([39.5, 33.0], outline: true) # => false
394
+ shape2.contain?(39.5, 33.0, outline: true) # => false
395
+ shape2.contain?([39.5, 33.0], outline: true) # => false
396
+ shape.contain?(2.0, 33.0, outline: true) # => true
397
+ shape.contain?([2.0, 33.0], outline: true) # => true
398
+ shape2.contain?(2.0, 33.0, outline: true) # => true
399
+ shape2.contain?([2.0, 33.0], outline: true) # => true
400
+ shape.contain?(3.0, 33.0, outline: true) # => false
401
+ shape.contain?([3.0, 33.0], outline: true) # => false
402
+ shape2.contain?(3.0, 33.0, outline: true) # => false
403
+ shape2.contain?([3.0, 33.0], outline: true) # => false
404
+ shape.contain?(3.0, 33.0, outline: true, distance_tolerance: 1.0) # => true
405
+ shape.contain?([3.0, 33.0], outline: true, distance_tolerance: 1.0) # => true
406
+ shape2.contain?(3.0, 33.0, outline: true, distance_tolerance: 1.0) # => true
407
+ shape2.contain?([3.0, 33.0], outline: true, distance_tolerance: 1.0) # => true
408
+ shape.contain?(shape.center_x, shape.center_y, outline: true) # => false
409
+ shape.contain?([shape.center_x, shape.center_y], outline: true) # => false
410
+ shape2.contain?(shape2.center_x, shape2.center_y, outline: true) # => false
411
+ shape2.contain?([shape2.center_x, shape2.center_y], outline: true) # => false
412
+
413
+ shape3 = PerfectShape::Arc.new(type: :chord, x: 2, y: 3, width: 50, height: 60, start: 45, extent: 270)
414
+ shape4 = PerfectShape::Arc.new(type: :chord, center_x: 2 + 25, center_y: 3 + 30, radius_x: 25, radius_y: 30, start: 45, extent: 270)
415
+
416
+ shape3.contain?(39.5, 33.0) # => true
417
+ shape3.contain?([39.5, 33.0]) # => true
418
+ shape4.contain?(39.5, 33.0) # => true
419
+ shape4.contain?([39.5, 33.0]) # => true
420
+ shape3.contain?(39.5, 33.0, outline: true) # => false
421
+ shape3.contain?([39.5, 33.0], outline: true) # => false
422
+ shape4.contain?(39.5, 33.0, outline: true) # => false
423
+ shape4.contain?([39.5, 33.0], outline: true) # => false
424
+ shape3.contain?(2.0, 33.0, outline: true) # => true
425
+ shape3.contain?([2.0, 33.0], outline: true) # => true
426
+ shape4.contain?(2.0, 33.0, outline: true) # => true
427
+ shape4.contain?([2.0, 33.0], outline: true) # => true
428
+ shape3.contain?(3.0, 33.0, outline: true) # => false
429
+ shape3.contain?([3.0, 33.0], outline: true) # => false
430
+ shape4.contain?(3.0, 33.0, outline: true) # => false
431
+ shape4.contain?([3.0, 33.0], outline: true) # => false
432
+ shape3.contain?(3.0, 33.0, outline: true, distance_tolerance: 1.0) # => true
433
+ shape3.contain?([3.0, 33.0], outline: true, distance_tolerance: 1.0) # => true
434
+ shape4.contain?(3.0, 33.0, outline: true, distance_tolerance: 1.0) # => true
435
+ shape4.contain?([3.0, 33.0], outline: true, distance_tolerance: 1.0) # => true
436
+ shape3.contain?(shape3.center_x, shape3.center_y, outline: true) # => false
437
+ shape3.contain?([shape3.center_x, shape3.center_y], outline: true) # => false
438
+ shape4.contain?(shape4.center_x, shape4.center_y, outline: true) # => false
439
+ shape4.contain?([shape4.center_x, shape4.center_y], outline: true) # => false
440
+
441
+ shape5 = PerfectShape::Arc.new(type: :pie, x: 2, y: 3, width: 50, height: 60, start: 45, extent: 270)
442
+ shape6 = PerfectShape::Arc.new(type: :pie, center_x: 2 + 25, center_y: 3 + 30, radius_x: 25, radius_y: 30, start: 45, extent: 270)
443
+
444
+ shape5.contain?(39.5, 33.0) # => false
445
+ shape5.contain?([39.5, 33.0]) # => false
446
+ shape6.contain?(39.5, 33.0) # => false
447
+ shape6.contain?([39.5, 33.0]) # => false
448
+ shape5.contain?(9.5, 33.0) # => true
449
+ shape5.contain?([9.5, 33.0]) # => true
450
+ shape6.contain?(9.5, 33.0) # => true
451
+ shape6.contain?([9.5, 33.0]) # => true
452
+ shape5.contain?(39.5, 33.0, outline: true) # => false
453
+ shape5.contain?([39.5, 33.0], outline: true) # => false
454
+ shape6.contain?(39.5, 33.0, outline: true) # => false
455
+ shape6.contain?([39.5, 33.0], outline: true) # => false
456
+ shape5.contain?(2.0, 33.0, outline: true) # => true
457
+ shape5.contain?([2.0, 33.0], outline: true) # => true
458
+ shape6.contain?(2.0, 33.0, outline: true) # => true
459
+ shape6.contain?([2.0, 33.0], outline: true) # => true
460
+ shape5.contain?(3.0, 33.0, outline: true) # => false
461
+ shape5.contain?([3.0, 33.0], outline: true) # => false
462
+ shape6.contain?(3.0, 33.0, outline: true) # => false
463
+ shape6.contain?([3.0, 33.0], outline: true) # => false
464
+ shape5.contain?(3.0, 33.0, outline: true, distance_tolerance: 1.0) # => true
465
+ shape5.contain?([3.0, 33.0], outline: true, distance_tolerance: 1.0) # => true
466
+ shape6.contain?(3.0, 33.0, outline: true, distance_tolerance: 1.0) # => true
467
+ shape6.contain?([3.0, 33.0], outline: true, distance_tolerance: 1.0) # => true
468
+ shape5.contain?(shape5.center_x, shape5.center_y, outline: true) # => true
469
+ shape5.contain?([shape5.center_x, shape5.center_y], outline: true) # => true
470
+ shape6.contain?(shape6.center_x, shape6.center_y, outline: true) # => true
471
+ shape6.contain?([shape6.center_x, shape6.center_y], outline: true) # => true
317
472
  ```
318
473
 
319
474
  ### `PerfectShape::Ellipse`
@@ -341,14 +496,37 @@ Extends `PerfectShape::Arc`
341
496
  - `#max_x`: max x
342
497
  - `#max_y`: max y
343
498
  - `#bounding_box`: bounding box is a rectangle with x = min x, y = min y, and width/height of shape
344
- - `#contain?(x_or_point, y=nil)`: checks if point is inside
345
499
  - `#==(other)`: Returns `true` if equal to `other` or `false` otherwise
500
+ - `#contain?(x_or_point, y=nil, outline: false, distance_tolerance: 0)`: checks if point is inside when `outline` is `false` or if point is on the outline when `outline` is `true`. `distance_tolerance` can be used as a fuzz factor when `outline` is `true`, for example, to help GUI users mouse-click-select an ellipse shape from its outline more successfully
346
501
 
347
502
  Example:
348
503
 
349
504
  ```ruby
505
+ require 'perfect-shape'
506
+
350
507
  shape = PerfectShape::Ellipse.new(x: 2, y: 3, width: 50, height: 60)
351
508
  shape2 = PerfectShape::Ellipse.new(center_x: 27, center_y: 33, radius_x: 25, radius_y: 30)
509
+
510
+ shape.contain?(27, 33) # => true
511
+ shape.contain?([27, 33]) # => true
512
+ shape2.contain?(27, 33) # => true
513
+ shape2.contain?([27, 33]) # => true
514
+ shape.contain?(27, 33, outline: true) # => false
515
+ shape.contain?([27, 33], outline: true) # => false
516
+ shape2.contain?(27, 33, outline: true) # => false
517
+ shape2.contain?([27, 33], outline: true) # => false
518
+ shape.contain?(2, 33, outline: true) # => true
519
+ shape.contain?([2, 33], outline: true) # => true
520
+ shape2.contain?(2, 33, outline: true) # => true
521
+ shape2.contain?([2, 33], outline: true) # => true
522
+ shape.contain?(1, 33, outline: true) # => false
523
+ shape.contain?([1, 33], outline: true) # => false
524
+ shape2.contain?(1, 33, outline: true) # => false
525
+ shape2.contain?([1, 33], outline: true) # => false
526
+ shape.contain?(1, 33, outline: true, distance_tolerance: 1) # => true
527
+ shape.contain?([1, 33], outline: true, distance_tolerance: 1) # => true
528
+ shape2.contain?(1, 33, outline: true, distance_tolerance: 1) # => true
529
+ shape2.contain?([1, 33], outline: true, distance_tolerance: 1) # => true
352
530
  ```
353
531
 
354
532
  ### `PerfectShape::Circle`
@@ -378,14 +556,37 @@ Extends `PerfectShape::Ellipse`
378
556
  - `#max_x`: max x
379
557
  - `#max_y`: max y
380
558
  - `#bounding_box`: bounding box is a rectangle with x = min x, y = min y, and width/height of shape
381
- - `#contain?(x_or_point, y=nil)`: checks if point is inside
382
559
  - `#==(other)`: Returns `true` if equal to `other` or `false` otherwise
560
+ - `#contain?(x_or_point, y=nil, outline: false, distance_tolerance: 0)`: checks if point is inside when `outline` is `false` or if point is on the outline when `outline` is `true`. `distance_tolerance` can be used as a fuzz factor when `outline` is `true`, for example, to help GUI users mouse-click-select a circle shape from its outline more successfully
383
561
 
384
562
  Example:
385
563
 
386
564
  ```ruby
565
+ require 'perfect-shape'
566
+
387
567
  shape = PerfectShape::Circle.new(x: 2, y: 3, diameter: 60)
388
568
  shape2 = PerfectShape::Circle.new(center_x: 2 + 30, center_y: 3 + 30, radius: 30)
569
+
570
+ shape.contain?(32, 33) # => true
571
+ shape.contain?([32, 33]) # => true
572
+ shape2.contain?(32, 33) # => true
573
+ shape2.contain?([32, 33]) # => true
574
+ shape.contain?(32, 33, outline: true) # => false
575
+ shape.contain?([32, 33], outline: true) # => false
576
+ shape2.contain?(32, 33, outline: true) # => false
577
+ shape2.contain?([32, 33], outline: true) # => false
578
+ shape.contain?(2, 33, outline: true) # => true
579
+ shape.contain?([2, 33], outline: true) # => true
580
+ shape2.contain?(2, 33, outline: true) # => true
581
+ shape2.contain?([2, 33], outline: true) # => true
582
+ shape.contain?(1, 33, outline: true) # => false
583
+ shape.contain?([1, 33], outline: true) # => false
584
+ shape2.contain?(1, 33, outline: true) # => false
585
+ shape2.contain?([1, 33], outline: true) # => false
586
+ shape.contain?(1, 33, outline: true, distance_tolerance: 1) # => true
587
+ shape.contain?([1, 33], outline: true, distance_tolerance: 1) # => true
588
+ shape2.contain?(1, 33, outline: true, distance_tolerance: 1) # => true
589
+ shape2.contain?([1, 33], outline: true, distance_tolerance: 1) # => true
389
590
  ```
390
591
 
391
592
  ### `PerfectShape::Polygon`
@@ -410,13 +611,26 @@ A polygon can be thought of as a special case of [path](#perfectshapepath) that
410
611
  - `#center_x`: center x
411
612
  - `#center_y`: center y
412
613
  - `#bounding_box`: bounding box is a rectangle with x = min x, y = min y, and width/height of shape
413
- - `#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))
414
614
  - `#==(other)`: Returns `true` if equal to `other` or `false` otherwise
615
+ - `#contain?(x_or_point, y=nil, outline: false, distance_tolerance: 0)`: When `outline` is `false`, it checks if point is inside using the [Ray Casting Algorithm](https://en.wikipedia.org/wiki/Point_in_polygon) (aka [Even-Odd Rule](https://en.wikipedia.org/wiki/Even%E2%80%93odd_rule)). Otherwise, when `outline` is `true`, it checks if point is on the outline. `distance_tolerance` can be used as a fuzz factor when `outline` is `true`, for example, to help GUI users mouse-click-select a polygon shape from its outline more successfully
415
616
 
416
617
  Example:
417
618
 
418
619
  ```ruby
620
+ require 'perfect-shape'
621
+
419
622
  shape = PerfectShape::Polygon.new(points: [[200, 150], [270, 170], [250, 220], [220, 190], [200, 200], [180, 170]])
623
+
624
+ shape.contain?(225, 185) # => true
625
+ shape.contain?([225, 185]) # => true
626
+ shape.contain?(225, 185, outline: true) # => false
627
+ shape.contain?([225, 185], outline: true) # => false
628
+ shape.contain?(200, 150, outline: true) # => true
629
+ shape.contain?([200, 150], outline: true) # => true
630
+ shape.contain?(200, 151, outline: true) # => false
631
+ shape.contain?([200, 151], outline: true) # => false
632
+ shape.contain?(200, 151, outline: true, distance_tolerance: 1) # => true
633
+ shape.contain?([200, 151], outline: true, distance_tolerance: 1) # => true
420
634
  ```
421
635
 
422
636
  ### `PerfectShape::Path`
@@ -443,13 +657,15 @@ Includes `PerfectShape::MultiPoint`
443
657
  - `#center_x`: center x
444
658
  - `#center_y`: center y
445
659
  - `#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)
660
+ - `#==(other)`: Returns `true` if equal to `other` or `false` otherwise
446
661
  - `#contain?(x_or_point, y=nil)`: checks if point is inside path utilizing the configured winding rule, which can be the [Nonzero-Rule](https://en.wikipedia.org/wiki/Nonzero-rule) (aka [Winding Number Algorithm](https://en.wikipedia.org/wiki/Point_in_polygon#Winding_number_algorithm)) or the [Even-Odd Rule](https://en.wikipedia.org/wiki/Even%E2%80%93odd_rule) (aka [Ray Casting Algorithm](https://en.wikipedia.org/wiki/Point_in_polygon#Ray_casting_algorithm))
447
662
  - `#point_crossings(x_or_point, y=nil)`: calculates the number of times the given path crosses the ray extending to the right from (x,y)
448
- - `#==(other)`: Returns `true` if equal to `other` or `false` otherwise
449
663
 
450
664
  Example:
451
665
 
452
666
  ```ruby
667
+ require 'perfect-shape'
668
+
453
669
  path_shapes = []
454
670
  path_shapes << PerfectShape::Point.new(x: 200, y: 150)
455
671
  path_shapes << PerfectShape::Line.new(points: [250, 170]) # no need for start point, just end point
@@ -457,6 +673,50 @@ path_shapes << PerfectShape::QuadraticBezierCurve.new(points: [[300, 185], [350,
457
673
  path_shapes << PerfectShape::CubicBezierCurve.new(points: [[370, 50], [430, 220], [480, 170]]) # no need for start point, just two control points and end point
458
674
 
459
675
  shape = PerfectShape::Path.new(shapes: path_shapes, closed: false, winding_rule: :wind_even_odd)
676
+
677
+ shape.contain?(225, 160) # => true
678
+ shape.contain?([225, 160]) # => true
679
+ ```
680
+
681
+ ### `PerfectShape::CompositeShape`
682
+
683
+ Class
684
+
685
+ Extends `PerfectShape::Shape`
686
+
687
+ A composite shape is simply an aggregate of multiple shapes (e.g. square and polygon)
688
+
689
+ ![composite shape](https://raw.githubusercontent.com/AndyObtiva/perfect-shape/master/images/composite-shape.png)
690
+
691
+ - `::new(shapes: [])`: constructs a composite shape with `shapes` as `Array` of `PerfectShape::Shape` objects
692
+ - `#shapes`: the shapes that the composite shape is composed of
693
+ - `#min_x`: min x
694
+ - `#min_y`: min y
695
+ - `#max_x`: max x
696
+ - `#max_y`: max y
697
+ - `#width`: width (from min x to max x)
698
+ - `#height`: height (from min y to max y)
699
+ - `#center_x`: center x
700
+ - `#center_y`: center y
701
+ - `#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)
702
+ - `#==(other)`: Returns `true` if equal to `other` or `false` otherwise
703
+ - `#contain?(x_or_point, y=nil)`: checks if point is inside any of the shapes owned by the composite shape
704
+
705
+ Example:
706
+
707
+ ```ruby
708
+ require 'perfect-shape'
709
+
710
+ shapes = []
711
+ shapes << PerfectShape::Square.new(x: 120, y: 215, length: 100)
712
+ shapes << PerfectShape::Polygon.new(points: [[120, 215], [170, 165], [220, 215]])
713
+
714
+ shape = PerfectShape::CompositeShape.new(shapes: shapes)
715
+
716
+ shape.contain?(170, 265) # => true
717
+ shape.contain?([170, 265]) # => true
718
+ shape.contain?(170, 190) # => true
719
+ shape.contain?([170, 190]) # => true
460
720
  ```
461
721
 
462
722
  ## Process
@@ -466,7 +726,7 @@ shape = PerfectShape::Path.new(shapes: path_shapes, closed: false, winding_rule:
466
726
  ## Resources
467
727
 
468
728
  - Rubydoc: https://www.rubydoc.info/gems/perfect-shape
469
- - AWT Geom JavaDoc (inspiration): https://docs.oracle.com/javase/8/docs/api/java/awt/geom/package-summary.html
729
+ - AWT Geom Javadoc (inspiration): https://docs.oracle.com/javase/8/docs/api/java/awt/geom/package-summary.html
470
730
 
471
731
  ## TODO
472
732
 
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.1.2
1
+ 0.3.2
@@ -30,6 +30,7 @@ module PerfectShape
30
30
  include Equalizer.new(:type, :x, :y, :width, :height, :start, :extent)
31
31
 
32
32
  TYPES = [:open, :chord, :pie]
33
+ DEFAULT_OUTLINE_RADIUS = BigDecimal('0.001')
33
34
  attr_accessor :type
34
35
  attr_reader :start, :extent
35
36
 
@@ -143,44 +144,58 @@ module PerfectShape
143
144
  # @return {@code true} if the point lies within the bound of
144
145
  # the arc, {@code false} if the point lies outside of the
145
146
  # arc's bounds.
146
- def contain?(x_or_point, y = nil)
147
+ def contain?(x_or_point, y = nil, outline: false, distance_tolerance: 0)
147
148
  x, y = normalize_point(x_or_point, y)
148
149
  return unless x && y
149
- # Normalize the coordinates compared to the ellipse
150
- # having a center at 0,0 and a radius of 0.5.
151
- ellw = width
152
- return false if (ellw <= 0.0)
153
- normx = (x - self.x) / ellw - 0.5
154
- ellh = height
155
- return false if (ellh <= 0.0)
156
- normy = (y - self.y) / ellh - 0.5
157
- dist_sq = (normx * normx) + (normy * normy)
158
- return false if (dist_sq >= 0.25)
159
- ang_ext = self.extent.abs
160
- return true if (ang_ext >= 360.0)
161
- inarc = contain_angle?(-1*Math.radians_to_degrees(Math.atan2(normy, normx)))
162
-
163
- return inarc if type == :pie
164
- # CHORD and OPEN behave the same way
165
- if inarc
166
- return true if ang_ext >= 180.0
167
- # point must be outside the "pie triangle"
150
+ if outline
151
+ if type == :pie && x == center_x && y == center_y
152
+ true
153
+ else
154
+ distance_tolerance = BigDecimal(distance_tolerance.to_s)
155
+ outside_inside_radius_difference = DEFAULT_OUTLINE_RADIUS + distance_tolerance * 2.0
156
+ outside_radius_difference = inside_radius_difference = outside_inside_radius_difference / 2.0
157
+ outside_shape = Arc.new(type: type, center_x: center_x, center_y: center_y, radius_x: radius_x + outside_radius_difference, radius_y: radius_y + outside_radius_difference, start: start, extent: extent)
158
+ inside_shape = Arc.new(type: type, center_x: center_x, center_y: center_y, radius_x: radius_x - inside_radius_difference, radius_y: radius_y - inside_radius_difference, start: start, extent: extent)
159
+ outside_shape.contain?(x, y, outline: false) and
160
+ !inside_shape.contain?(x, y, outline: false)
161
+ end
168
162
  else
169
- return false if ang_ext <= 180.0
170
- # point must be inside the "pie triangle"
163
+ # Normalize the coordinates compared to the ellipse
164
+ # having a center at 0,0 and a radius of 0.5.
165
+ ellw = width
166
+ return false if (ellw <= 0.0)
167
+ normx = (x - self.x) / ellw - 0.5
168
+ ellh = height
169
+ return false if (ellh <= 0.0)
170
+ normy = (y - self.y) / ellh - 0.5
171
+ dist_sq = (normx * normx) + (normy * normy)
172
+ return false if (dist_sq >= 0.25)
173
+ ang_ext = self.extent.abs
174
+ return true if (ang_ext >= 360.0)
175
+ inarc = contain_angle?(-1*Math.radians_to_degrees(Math.atan2(normy, normx)))
176
+
177
+ return inarc if type == :pie
178
+ # CHORD and OPEN behave the same way
179
+ if inarc
180
+ return true if ang_ext >= 180.0
181
+ # point must be outside the "pie triangle"
182
+ else
183
+ return false if ang_ext <= 180.0
184
+ # point must be inside the "pie triangle"
185
+ end
186
+
187
+ # The point is inside the pie triangle iff it is on the same
188
+ # side of the line connecting the ends of the arc as the center.
189
+ angle = Math.degrees_to_radians(-start)
190
+ x1 = Math.cos(angle)
191
+ y1 = Math.sin(angle)
192
+ angle += Math.degrees_to_radians(-extent)
193
+ x2 = Math.cos(angle)
194
+ y2 = Math.sin(angle)
195
+ inside = (Line.relative_counterclockwise(x1, y1, x2, y2, 2*normx, 2*normy) *
196
+ Line.relative_counterclockwise(x1, y1, x2, y2, 0, 0) >= 0)
197
+ inarc ? !inside : inside
171
198
  end
172
-
173
- # The point is inside the pie triangle iff it is on the same
174
- # side of the line connecting the ends of the arc as the center.
175
- angle = Math.degrees_to_radians(-start)
176
- x1 = Math.cos(angle)
177
- y1 = Math.sin(angle)
178
- angle += Math.degrees_to_radians(-extent)
179
- x2 = Math.cos(angle)
180
- y2 = Math.sin(angle)
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
- inarc ? !inside : inside
184
199
  end
185
200
 
186
201
  # Determines whether or not the specified angle is within the
@@ -0,0 +1,72 @@
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'
24
+ require 'perfect_shape/line'
25
+ require 'perfect_shape/quadratic_bezier_curve'
26
+ require 'perfect_shape/cubic_bezier_curve'
27
+ require 'perfect_shape/multi_point'
28
+
29
+ module PerfectShape
30
+ # A composite of multiple shapes
31
+ class CompositeShape < Shape
32
+ include Equalizer.new(:shapes)
33
+
34
+ attr_accessor :shapes
35
+
36
+ # Constructs from multiple shapes
37
+ def initialize(shapes: [])
38
+ self.shapes = shapes
39
+ end
40
+
41
+ def min_x
42
+ shapes.map(&:min_x).min
43
+ end
44
+
45
+ def min_y
46
+ shapes.map(&:min_y).min
47
+ end
48
+
49
+ def max_x
50
+ shapes.map(&:max_x).max
51
+ end
52
+
53
+ def max_y
54
+ shapes.map(&:max_y).max
55
+ end
56
+
57
+ # Checks if composite shape contains point (two-number Array or x, y args)
58
+ # by comparing against all shapes it consists of
59
+ #
60
+ # @param x The X coordinate of the point to test.
61
+ # @param y The Y coordinate of the point to test.
62
+ #
63
+ # @return true if the point lies within the bound of
64
+ # the composite shape or false if the point lies outside of the
65
+ # path's bounds.
66
+ def contain?(x_or_point, y = nil)
67
+ x, y = normalize_point(x_or_point, y)
68
+ return unless x && y
69
+ shapes.any? {|shape| shape.contain?(x, y) }
70
+ end
71
+ end
72
+ end
@@ -75,6 +75,8 @@ module PerfectShape
75
75
  include MultiPoint
76
76
  include Equalizer.new(:points)
77
77
 
78
+ OUTLINE_MINIMUM_DISTANCE_THRESHOLD = BigDecimal('0.001')
79
+
78
80
  # Checks if cubic bézier curve contains point (two-number Array or x, y args)
79
81
  #
80
82
  # @param x The X coordinate of the point to test.
@@ -83,24 +85,29 @@ module PerfectShape
83
85
  # @return {@code true} if the point lies within the bound of
84
86
  # the cubic bézier curve, {@code false} if the point lies outside of the
85
87
  # cubic bézier curve's bounds.
86
- def contain?(x_or_point, y = nil)
88
+ def contain?(x_or_point, y = nil, outline: false, distance_tolerance: 0)
87
89
  x, y = normalize_point(x_or_point, y)
88
90
  return unless x && y
89
91
 
90
- # Either x or y was infinite or NaN.
91
- # A NaN always produces a negative response to any test
92
- # and Infinity values cannot be "inside" any path so
93
- # they should return false as well.
94
- return false if (!(x * 0.0 + y * 0.0 == 0.0))
95
- # We count the "Y" crossings to determine if the point is
96
- # inside the curve bounded by its closing line.
97
- x1 = points[0][0]
98
- y1 = points[0][1]
99
- x2 = points[3][0]
100
- y2 = points[3][1]
101
- line = PerfectShape::Line.new(points: [[x1, y1], [x2, y2]])
102
- crossings = line.point_crossings(x, y) + point_crossings(x, y);
103
- (crossings & 1) == 1
92
+ if outline
93
+ minimum_distance_threshold = OUTLINE_MINIMUM_DISTANCE_THRESHOLD + distance_tolerance
94
+ point_segment_distance(x, y, minimum_distance_threshold: minimum_distance_threshold) < minimum_distance_threshold
95
+ else
96
+ # Either x or y was infinite or NaN.
97
+ # A NaN always produces a negative response to any test
98
+ # and Infinity values cannot be "inside" any path so
99
+ # they should return false as well.
100
+ return false if (!(x * 0.0 + y * 0.0 == 0.0))
101
+ # We count the "Y" crossings to determine if the point is
102
+ # inside the curve bounded by its closing line.
103
+ x1 = points[0][0]
104
+ y1 = points[0][1]
105
+ x2 = points[3][0]
106
+ y2 = points[3][1]
107
+ line = PerfectShape::Line.new(points: [[x1, y1], [x2, y2]])
108
+ crossings = line.point_crossings(x, y) + point_crossings(x, y);
109
+ (crossings & 1) == 1
110
+ end
104
111
  end
105
112
 
106
113
  # Calculates the number of times the cubic bézier curve
@@ -116,5 +123,86 @@ module PerfectShape
116
123
  return unless x && y
117
124
  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)
118
125
  end
126
+
127
+ # The center point on the outline of the curve
128
+ def curve_center_point
129
+ subdivisions.last.points[0]
130
+ end
131
+
132
+ # The center point x on the outline of the curve
133
+ def curve_center_x
134
+ subdivisions.last.points[0][0]
135
+ end
136
+
137
+ # The center point y on the outline of the curve
138
+ def curve_center_y
139
+ subdivisions.last.points[0][1]
140
+ end
141
+
142
+ # Subdivides CubicBezierCurve exactly at its curve center
143
+ # returning 2 CubicBezierCurve's as a two-element Array by default
144
+ # `number` parameter may be specified as an even number in case more
145
+ # subdivisions are needed. If an odd number is given, it is rounded
146
+ # up to the closest even number above it (e.g. 3 becomes 4).
147
+ def subdivisions(number = 2)
148
+ number = (number.to_i / 2.0).ceil*2
149
+ x1 = points[0][0]
150
+ y1 = points[0][1]
151
+ ctrlx1 = points[1][0]
152
+ ctrly1 = points[1][1]
153
+ ctrlx2 = points[2][0]
154
+ ctrly2 = points[2][1]
155
+ x2 = points[3][0]
156
+ y2 = points[3][1]
157
+ centerx = (ctrlx1 + ctrlx2) / 2.0
158
+ centery = (ctrly1 + ctrly2) / 2.0
159
+ ctrlx1 = (x1 + ctrlx1) / 2.0
160
+ ctrly1 = (y1 + ctrly1) / 2.0
161
+ ctrlx2 = (x2 + ctrlx2) / 2.0
162
+ ctrly2 = (y2 + ctrly2) / 2.0
163
+ ctrlx12 = (ctrlx1 + centerx) / 2.0
164
+ ctrly12 = (ctrly1 + centery) / 2.0
165
+ ctrlx21 = (ctrlx2 + centerx) / 2.0
166
+ ctrly21 = (ctrly2 + centery) / 2.0
167
+ centerx = (ctrlx12 + ctrlx21) / 2.0
168
+ centery = (ctrly12 + ctrly21) / 2.0
169
+ default_subdivisions = [
170
+ CubicBezierCurve.new(points: [x1, y1, ctrlx1, ctrly1, ctrlx12, ctrly12, centerx, centery]),
171
+ CubicBezierCurve.new(points: [centerx, centery, ctrlx21, ctrly21, ctrlx2, ctrly2, x2, y2])
172
+ ]
173
+ if number > 2
174
+ default_subdivisions.map { |curve| curve.subdivisions(number - 2) }.flatten
175
+ else
176
+ default_subdivisions
177
+ end
178
+ end
179
+
180
+ def point_segment_distance(x_or_point, y = nil, minimum_distance_threshold: OUTLINE_MINIMUM_DISTANCE_THRESHOLD)
181
+ x, y = normalize_point(x_or_point, y)
182
+ return unless x && y
183
+
184
+ point = Point.new(x, y)
185
+ current_curve = self
186
+ minimum_distance = point.point_distance(curve_center_point)
187
+ last_minimum_distance = minimum_distance + 1 # start bigger to ensure going through loop once at least
188
+ while minimum_distance >= minimum_distance_threshold && minimum_distance < last_minimum_distance
189
+ curve1, curve2 = current_curve.subdivisions
190
+ distance1 = point.point_distance(curve1.curve_center_point)
191
+ distance2 = point.point_distance(curve2.curve_center_point)
192
+ last_minimum_distance = minimum_distance
193
+ if distance1 < distance2
194
+ minimum_distance = distance1
195
+ current_curve = curve1
196
+ else
197
+ minimum_distance = distance2
198
+ current_curve = curve2
199
+ end
200
+ end
201
+ if minimum_distance < minimum_distance_threshold
202
+ minimum_distance
203
+ else
204
+ last_minimum_distance
205
+ end
206
+ end
119
207
  end
120
208
  end
@@ -63,17 +63,21 @@ module PerfectShape
63
63
  # @return {@code true} if the point lies within the bound of
64
64
  # the ellipse, {@code false} if the point lies outside of the
65
65
  # ellipse's bounds.
66
- def contain?(x_or_point, y = nil)
66
+ def contain?(x_or_point, y = nil, outline: false, distance_tolerance: 0)
67
67
  # This is implemented again even though super would have just worked to have an optimized algorithm for Ellipse.
68
68
  x, y = normalize_point(x_or_point, y)
69
69
  return unless x && y
70
- ellw = self.width
71
- return false if ellw <= 0.0
72
- normx = (x - self.x) / ellw - 0.5
73
- ellh = self.height
74
- return false if ellh <= 0.0
75
- normy = (y - self.y) / ellh - 0.5
76
- (normx * normx + normy * normy) < 0.25
70
+ if outline
71
+ super(x, y, outline: true, distance_tolerance: distance_tolerance)
72
+ else
73
+ ellw = self.width
74
+ return false if ellw <= 0.0
75
+ normx = (x - self.x) / ellw - 0.5
76
+ ellh = self.height
77
+ return false if ellh <= 0.0
78
+ normy = (y - self.y) / ellh - 0.5
79
+ (normx * normx + normy * normy) < 0.25
80
+ end
77
81
  end
78
82
  end
79
83
  end
@@ -207,16 +207,16 @@ module PerfectShape
207
207
  #
208
208
  # @param x The X coordinate of the point to test.
209
209
  # @param y The Y coordinate of the point to test.
210
- # @param distance The distance from line to tolerate (0 by default)
210
+ # @param distance_tolerance The distance from line to tolerate (0 by default)
211
211
  #
212
212
  # @return {@code true} if the point lies within the bound of
213
213
  # the line, {@code false} if the point lies outside of the
214
214
  # line's bounds.
215
- def contain?(x_or_point, y = nil, distance: 0)
215
+ def contain?(x_or_point, y = nil, outline: false, distance_tolerance: 0)
216
216
  x, y = normalize_point(x_or_point, y)
217
217
  return unless x && y
218
- distance = BigDecimal(distance.to_s)
219
- point_segment_distance(x, y) <= distance
218
+ distance_tolerance = BigDecimal(distance_tolerance.to_s)
219
+ point_segment_distance(x, y) <= distance_tolerance
220
220
  end
221
221
 
222
222
  def point_segment_distance(x_or_point, y = nil)
@@ -43,8 +43,9 @@ module PerfectShape
43
43
  alias closed? closed
44
44
 
45
45
  # Constructs Path with winding rule, closed status, and shapes (must always start with PerfectShape::Point or Array of [x,y] coordinates)
46
- # Shape class types can be any of SHAPE_TYPES: Array (x,y coordinates), PerfectShape::Point, or PerfectShape::Line
46
+ # Shape class types can be any of SHAPE_TYPES: Array (x,y coordinates), PerfectShape::Point, PerfectShape::Line, PerfectShape::QuadraticBezierCurve, or PerfectShape::CubicBezierCurve
47
47
  # winding_rule can be any of WINDING_RULES: :wind_non_zero (default) or :wind_even_odd
48
+ # closed can be true or false
48
49
  def initialize(shapes: [], closed: false, winding_rule: :wind_non_zero)
49
50
  self.closed = closed
50
51
  self.winding_rule = winding_rule
@@ -110,8 +111,8 @@ module PerfectShape
110
111
  # @param x The X coordinate of the point to test.
111
112
  # @param y The Y coordinate of the point to test.
112
113
  #
113
- # @return {@code true} if the point lies within the bound of
114
- # the path, {@code false} if the point lies outside of the
114
+ # @return true if the point lies within the bound of
115
+ # the path or false if the point lies outside of the
115
116
  # path's bounds.
116
117
  def contain?(x_or_point, y = nil)
117
118
  x, y = normalize_point(x_or_point, y)
@@ -63,15 +63,15 @@ module PerfectShape
63
63
  #
64
64
  # @param x The X coordinate of the point to test.
65
65
  # @param y The Y coordinate of the point to test.
66
- # @param distance The distance from point to tolerate (0 by default)
66
+ # @param distance_tolerance The distance from point to tolerate (0 by default)
67
67
  #
68
68
  # @return {@code true} if the point is close enough within distance tolerance,
69
69
  # {@code false} if the point is too far.
70
- def contain?(x_or_point, y = nil, distance: 0)
70
+ def contain?(x_or_point, y = nil, outline: false, distance_tolerance: 0)
71
71
  x, y = normalize_point(x_or_point, y)
72
72
  return unless x && y
73
- distance = BigDecimal(distance.to_s)
74
- point_distance(x, y) <= distance
73
+ distance_tolerance = BigDecimal(distance_tolerance.to_s)
74
+ point_distance(x, y) <= distance_tolerance
75
75
  end
76
76
 
77
77
  def point_distance(x_or_point, y = nil)
@@ -37,79 +37,85 @@ module PerfectShape
37
37
  # @return {@code true} if the point lies within the bound of
38
38
  # the polygon, {@code false} if the point lies outside of the
39
39
  # polygon's bounds.
40
- def contain?(x_or_point, y = nil)
40
+ def contain?(x_or_point, y = nil, outline: false, distance_tolerance: 0)
41
41
  x, y = normalize_point(x_or_point, y)
42
42
  return unless x && y
43
- npoints = points.count
44
- xpoints = points.map(&:first)
45
- ypoints = points.map(&:last)
46
- return false if npoints <= 2 || !bounding_box.contain?(x, y)
47
- hits = 0
48
-
49
- lastx = xpoints[npoints - 1]
50
- lasty = ypoints[npoints - 1]
51
-
52
- # Walk the edges of the polygon
53
- npoints.times do |i|
54
- curx = xpoints[i]
55
- cury = ypoints[i]
56
-
57
- if cury == lasty
58
- lastx = curx
59
- lasty = cury
60
- next
61
- end
62
-
63
- if curx < lastx
64
- if x >= lastx
65
- lastx = curx
66
- lasty = cury
67
- next
68
- end
69
- leftx = curx
70
- else
71
- if x >= curx
72
- lastx = curx
73
- lasty = cury
74
- next
75
- end
76
- leftx = lastx
43
+ if outline
44
+ points.zip(points.rotate(1)).any? do |point1, point2|
45
+ Line.new(points: [[point1.first, point1.last], [point2.first, point2.last]]).contain?(x, y, distance_tolerance: distance_tolerance)
77
46
  end
78
-
79
- if cury < lasty
80
- if y < cury || y >= lasty
47
+ else
48
+ npoints = points.count
49
+ xpoints = points.map(&:first)
50
+ ypoints = points.map(&:last)
51
+ return false if npoints <= 2 || !bounding_box.contain?(x, y)
52
+ hits = 0
53
+
54
+ lastx = xpoints[npoints - 1]
55
+ lasty = ypoints[npoints - 1]
56
+
57
+ # Walk the edges of the polygon
58
+ npoints.times do |i|
59
+ curx = xpoints[i]
60
+ cury = ypoints[i]
61
+
62
+ if cury == lasty
81
63
  lastx = curx
82
64
  lasty = cury
83
65
  next
84
66
  end
85
- if x < leftx
86
- hits += 1
87
- lastx = curx
88
- lasty = cury
89
- next
67
+
68
+ if curx < lastx
69
+ if x >= lastx
70
+ lastx = curx
71
+ lasty = cury
72
+ next
73
+ end
74
+ leftx = curx
75
+ else
76
+ if x >= curx
77
+ lastx = curx
78
+ lasty = cury
79
+ next
80
+ end
81
+ leftx = lastx
90
82
  end
91
- test1 = x - curx
92
- test2 = y - cury
93
- else
94
- if y < lasty || y >= cury
95
- lastx = curx
96
- lasty = cury
97
- next
83
+
84
+ if cury < lasty
85
+ if y < cury || y >= lasty
86
+ lastx = curx
87
+ lasty = cury
88
+ next
89
+ end
90
+ if x < leftx
91
+ hits += 1
92
+ lastx = curx
93
+ lasty = cury
94
+ next
95
+ end
96
+ test1 = x - curx
97
+ test2 = y - cury
98
+ else
99
+ if y < lasty || y >= cury
100
+ lastx = curx
101
+ lasty = cury
102
+ next
103
+ end
104
+ if x < leftx
105
+ hits += 1
106
+ lastx = curx
107
+ lasty = cury
108
+ next
109
+ end
110
+ test1 = x - lastx
111
+ test2 = y - lasty
98
112
  end
99
- if x < leftx
100
- hits += 1
101
- lastx = curx
102
- lasty = cury
103
- next
104
- end
105
- test1 = x - lastx
106
- test2 = y - lasty
113
+
114
+ hits += 1 if (test1 < (test2 / (lasty - cury) * (lastx - curx)))
107
115
  end
108
-
109
- hits += 1 if (test1 < (test2 / (lasty - cury) * (lastx - curx)))
116
+
117
+ (hits & 1) != 0
110
118
  end
111
-
112
- (hits & 1) != 0
113
119
  end
114
120
  end
115
121
  end
@@ -21,6 +21,7 @@
21
21
 
22
22
  require 'perfect_shape/shape'
23
23
  require 'perfect_shape/rectangular_shape'
24
+ require 'perfect_shape/line'
24
25
 
25
26
  module PerfectShape
26
27
  # Mostly ported from java.awt.geom: https://docs.oracle.com/javase/8/docs/api/java/awt/geom/Rectangle2D.html
@@ -36,10 +37,17 @@ module PerfectShape
36
37
  # @return {@code true} if the point lies within the bound of
37
38
  # the rectangle, {@code false} if the point lies outside of the
38
39
  # rectangle's bounds.
39
- def contain?(x_or_point, y = nil)
40
+ def contain?(x_or_point, y = nil, outline: false, distance_tolerance: 0)
40
41
  x, y = normalize_point(x_or_point, y)
41
42
  return unless x && y
42
- x.between?(self.x, self.x + self.width) && y.between?(self.y, self.y + self.height)
43
+ if outline
44
+ Line.new(points: [[self.x, self.y], [self.x + width, self.y]]).contain?(x, y, distance_tolerance: distance_tolerance) or
45
+ Line.new(points: [[self.x + width, self.y], [self.x + width, self.y + height]]).contain?(x, y, distance_tolerance: distance_tolerance) or
46
+ Line.new(points: [[self.x + width, self.y + height], [self.x, self.y + height]]).contain?(x, y, distance_tolerance: distance_tolerance) or
47
+ Line.new(points: [[self.x, self.y + height], [self.x, self.y]]).contain?(x, y, distance_tolerance: distance_tolerance)
48
+ else
49
+ x.between?(self.x, self.x + width) && y.between?(self.y, self.y + height)
50
+ end
43
51
  end
44
52
  end
45
53
  end
@@ -84,6 +84,10 @@ module PerfectShape
84
84
  [x, y]
85
85
  end
86
86
 
87
+ # Subclasses must implement
88
+ def contain?(x_or_point, y = nil, outline: false, distance_tolerance: 0)
89
+ end
90
+
87
91
  # Subclasses must implement
88
92
  def ==(other)
89
93
  end
@@ -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.1.2 ruby lib
5
+ # stub: perfect-shape 0.3.2 ruby lib
6
6
 
7
7
  Gem::Specification.new do |s|
8
8
  s.name = "perfect-shape".freeze
9
- s.version = "0.1.2"
9
+ s.version = "0.3.2"
10
10
 
11
11
  s.required_rubygems_version = Gem::Requirement.new(">= 0".freeze) if s.respond_to? :required_rubygems_version=
12
12
  s.require_paths = ["lib".freeze]
13
13
  s.authors = ["Andy Maleh".freeze]
14
- s.date = "2021-12-22"
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 b\u00E9zier 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-10"
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
16
16
  s.email = "andy.am@gmail.com".freeze
17
17
  s.extra_rdoc_files = [
18
18
  "CHANGELOG.md",
@@ -27,6 +27,7 @@ Gem::Specification.new do |s|
27
27
  "lib/perfect-shape.rb",
28
28
  "lib/perfect_shape/arc.rb",
29
29
  "lib/perfect_shape/circle.rb",
30
+ "lib/perfect_shape/composite_shape.rb",
30
31
  "lib/perfect_shape/cubic_bezier_curve.rb",
31
32
  "lib/perfect_shape/ellipse.rb",
32
33
  "lib/perfect_shape/line.rb",
@@ -45,7 +46,7 @@ Gem::Specification.new do |s|
45
46
  ]
46
47
  s.homepage = "http://github.com/AndyObtiva/perfect-shape".freeze
47
48
  s.licenses = ["MIT".freeze]
48
- s.rubygems_version = "3.2.31".freeze
49
+ s.rubygems_version = "3.3.1".freeze
49
50
  s.summary = "Perfect Shape - Geometric Algorithms".freeze
50
51
 
51
52
  if s.respond_to? :specification_version then
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.1.2
4
+ version: 0.3.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andy Maleh
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-12-22 00:00:00.000000000 Z
11
+ date: 2022-01-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: equalizer
@@ -98,7 +98,7 @@ description: Perfect Shape is a collection of pure Ruby geometric algorithms tha
98
98
  are mostly useful for GUI manipulation like checking containment of a mouse click
99
99
  point in popular geometry shapes such as rectangle, square, arc (open, chord, and
100
100
  pie), ellipse, circle, polygon, and paths containing lines, quadratic bézier curves,
101
- and cubic bézier curves (including both Ray Casting Algorithm, aka Even-odd Rule,
101
+ and cubic bezier curves (including both Ray Casting Algorithm, aka Even-odd Rule,
102
102
  and Winding Number Algorithm, aka Nonzero Rule). Additionally, it contains some
103
103
  purely mathematical algorithms like IEEEremainder (also known as IEEE-754 remainder).
104
104
  email: andy.am@gmail.com
@@ -116,6 +116,7 @@ files:
116
116
  - lib/perfect-shape.rb
117
117
  - lib/perfect_shape/arc.rb
118
118
  - lib/perfect_shape/circle.rb
119
+ - lib/perfect_shape/composite_shape.rb
119
120
  - lib/perfect_shape/cubic_bezier_curve.rb
120
121
  - lib/perfect_shape/ellipse.rb
121
122
  - lib/perfect_shape/line.rb
@@ -150,7 +151,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
150
151
  - !ruby/object:Gem::Version
151
152
  version: '0'
152
153
  requirements: []
153
- rubygems_version: 3.2.31
154
+ rubygems_version: 3.3.1
154
155
  signing_key:
155
156
  specification_version: 4
156
157
  summary: Perfect Shape - Geometric Algorithms