perfect-shape 0.1.0 → 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +7 -0
- data/README.md +102 -9
- data/VERSION +1 -1
- data/lib/perfect_shape/line.rb +5 -5
- data/lib/perfect_shape/multi_point.rb +2 -2
- data/lib/perfect_shape/path.rb +31 -23
- data/lib/perfect_shape/quadratic_bezier_curve.rb +182 -0
- data/perfect-shape.gemspec +4 -3
- metadata +6 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a4eb7bc277a02c0795966623e05ee57514483d982bc0d80f264a525f6b3d5afa
|
4
|
+
data.tar.gz: f070a8292b835f6d2c68ff4ccddd3073b7a83d36e882d4e5c34c6e91a85d88ee
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1add8519bdd16f38f8fb8431d8342c77df9f5d8d76f0dc6f1481bba5d9c0a15b653c56a9a0e372082f1590b55f5897ebac3f6a4d4505acd6ba017f257261e80d
|
7
|
+
data.tar.gz: fc3e2fce45f0a46ad7869fd5fec07d0dcf826e3bd36676439144eb3fd0198b8224d816a26db54858b43d0bb5332ff6b4108950efca3e71c1d5e42cebde4de63f
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,12 @@
|
|
1
1
|
# Change Log
|
2
2
|
|
3
|
+
## 0.1.1
|
4
|
+
|
5
|
+
- `PerfectShape::QuadraticBezierCurve` (two end points and one control point)
|
6
|
+
- `PerfectShape::QuadraticBezierCurve#contain?(x_or_point, y=nil)`
|
7
|
+
- `PerfectShape::QuadraticBezierCurve#==`
|
8
|
+
- `PerfectShape::Path` having quadratic bezier curves in addition to points and lines
|
9
|
+
|
3
10
|
## 0.1.0
|
4
11
|
|
5
12
|
- `PerfectShape::Path` (having points or lines)
|
data/README.md
CHANGED
@@ -1,9 +1,9 @@
|
|
1
|
-
# Perfect Shape 0.1.
|
1
|
+
# Perfect Shape 0.1.1
|
2
2
|
## Geometric Algorithms
|
3
3
|
[![Gem Version](https://badge.fury.io/rb/perfect-shape.svg)](http://badge.fury.io/rb/perfect-shape)
|
4
4
|
[![Test](https://github.com/AndyObtiva/perfect-shape/actions/workflows/ruby.yml/badge.svg)](https://github.com/AndyObtiva/perfect-shape/actions/workflows/ruby.yml)
|
5
5
|
|
6
|
-
[`PerfectShape`](https://rubygems.org/gems/perfect-shape) is a collection of pure Ruby geometric algorithms that are mostly useful for GUI (Graphical User Interface) manipulation like checking containment of a mouse click [point](#perfectshapepoint) in popular geometry shapes such as [rectangle](#perfectshaperectangle), [square](#perfectshapesquare), [arc](#perfectshapearc) (open, chord, and pie), [ellipse](#perfectshapeellipse), [circle](#perfectshapecircle), [polygon](#perfectshapepolygon),
|
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 (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.
|
17
|
+
gem install perfect-shape -v 0.1.1
|
18
18
|
```
|
19
19
|
|
20
20
|
Or include in Bundler `Gemfile`:
|
21
21
|
|
22
22
|
```ruby
|
23
|
-
gem 'perfect-shape', '~> 0.1.
|
23
|
+
gem 'perfect-shape', '~> 0.1.1'
|
24
24
|
```
|
25
25
|
|
26
26
|
And, run:
|
@@ -114,6 +114,12 @@ Points are simply represented by an `Array` of `[x,y]` coordinates when used wit
|
|
114
114
|
- `#point_distance(x_or_point, y=nil)`: Returns the distance from a point to another point
|
115
115
|
- `#==(other)`: Returns `true` if equal to `other` or `false` otherwise
|
116
116
|
|
117
|
+
Example:
|
118
|
+
|
119
|
+
```ruby
|
120
|
+
shape = PerfectShape::Point.new(x: 200, y: 150)
|
121
|
+
```
|
122
|
+
|
117
123
|
### `PerfectShape::Line`
|
118
124
|
|
119
125
|
Class
|
@@ -127,7 +133,7 @@ Includes `PerfectShape::MultiPoint`
|
|
127
133
|
- `::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)”.
|
128
134
|
- `::point_segment_distance_square(x1, y1, x2, y2, px, py)`: Returns the square of distance from a point to a line segment.
|
129
135
|
- `::point_segment_distance(x1, y1, x2, y2, px, py)`: Returns the distance from a point to a line segment.
|
130
|
-
- `::new(points:
|
136
|
+
- `::new(points: [])`: constructs a line with two `points` as `Array` of `Array`s of `[x,y]` pairs or flattened `Array` of alternating x and y values
|
131
137
|
- `#min_x`: min x
|
132
138
|
- `#min_y`: min y
|
133
139
|
- `#max_x`: max x
|
@@ -142,6 +148,41 @@ Includes `PerfectShape::MultiPoint`
|
|
142
148
|
- `#point_segment_distance(x_or_point, y=nil)`: Returns the distance from a point to a line segment.
|
143
149
|
- `#==(other)`: Returns `true` if equal to `other` or `false` otherwise
|
144
150
|
|
151
|
+
Example:
|
152
|
+
|
153
|
+
```ruby
|
154
|
+
shape = PerfectShape::Line.new(points: [[200, 150], [270, 220]]) # start point and end point
|
155
|
+
```
|
156
|
+
|
157
|
+
### `PerfectShape::QuadraticBezierCurve`
|
158
|
+
|
159
|
+
Class
|
160
|
+
|
161
|
+
Extends `PerfectShape::Shape`
|
162
|
+
|
163
|
+
Includes `PerfectShape::MultiPoint`
|
164
|
+
|
165
|
+
![quadratic_bezier_curve](https://raw.githubusercontent.com/AndyObtiva/perfect-shape/master/images/quadratic_bezier_curve.png)
|
166
|
+
|
167
|
+
- `::new(points: [])`: constructs a quadratic bézier curve with three `points` (two end points and one control point) as `Array` of `Array`s of `[x,y]` pairs or flattened `Array` of alternating x and y values
|
168
|
+
- `#min_x`: min x
|
169
|
+
- `#min_y`: min y
|
170
|
+
- `#max_x`: max x
|
171
|
+
- `#max_y`: max y
|
172
|
+
- `#width`: width (from min x to max x)
|
173
|
+
- `#height`: height (from min y to max y)
|
174
|
+
- `#center_x`: center x
|
175
|
+
- `#center_y`: center y
|
176
|
+
- `#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)
|
177
|
+
- `#contain?(x_or_point, y=nil)`: checks if point is inside
|
178
|
+
- `#==(other)`: Returns `true` if equal to `other` or `false` otherwise
|
179
|
+
|
180
|
+
Example:
|
181
|
+
|
182
|
+
```ruby
|
183
|
+
shape = PerfectShape::QuadraticBezierCurve.new(points: [[200, 150], [270, 220], [180, 170]]) # start point, control point, and end point
|
184
|
+
```
|
185
|
+
|
145
186
|
### `PerfectShape::Rectangle`
|
146
187
|
|
147
188
|
Class
|
@@ -167,6 +208,12 @@ Includes `PerfectShape::RectangularShape`
|
|
167
208
|
- `#contain?(x_or_point, y=nil)`: checks if point is inside
|
168
209
|
- `#==(other)`: Returns `true` if equal to `other` or `false` otherwise
|
169
210
|
|
211
|
+
Example:
|
212
|
+
|
213
|
+
```ruby
|
214
|
+
shape = PerfectShape::Rectangle.new(x: 15, y: 30, width: 200, height: 100)
|
215
|
+
```
|
216
|
+
|
170
217
|
### `PerfectShape::Square`
|
171
218
|
|
172
219
|
Class
|
@@ -191,6 +238,12 @@ Extends `PerfectShape::Rectangle`
|
|
191
238
|
- `#contain?(x_or_point, y=nil)`: checks if point is inside
|
192
239
|
- `#==(other)`: Returns `true` if equal to `other` or `false` otherwise
|
193
240
|
|
241
|
+
Example:
|
242
|
+
|
243
|
+
```ruby
|
244
|
+
shape = PerfectShape::Square.new(x: 15, y: 30, length: 200)
|
245
|
+
```
|
246
|
+
|
194
247
|
### `PerfectShape::Arc`
|
195
248
|
|
196
249
|
Class
|
@@ -225,6 +278,13 @@ Open Arc | Chord Arc | Pie Arc
|
|
225
278
|
- `#contain?(x_or_point, y=nil)`: checks if point is inside
|
226
279
|
- `#==(other)`: Returns `true` if equal to `other` or `false` otherwise
|
227
280
|
|
281
|
+
Example:
|
282
|
+
|
283
|
+
```ruby
|
284
|
+
shape = PerfectShape::Arc.new(type: :chord, x: 2, y: 3, width: 50, height: 60, start: 30, extent: 90)
|
285
|
+
shape2 = PerfectShape::Arc.new(type: :chord, center_x: 2 + 25, center_y: 3 + 30, radius_x: 25, radius_y: 30, start: 30, extent: 90)
|
286
|
+
```
|
287
|
+
|
228
288
|
### `PerfectShape::Ellipse`
|
229
289
|
|
230
290
|
Class
|
@@ -253,6 +313,13 @@ Extends `PerfectShape::Arc`
|
|
253
313
|
- `#contain?(x_or_point, y=nil)`: checks if point is inside
|
254
314
|
- `#==(other)`: Returns `true` if equal to `other` or `false` otherwise
|
255
315
|
|
316
|
+
Example:
|
317
|
+
|
318
|
+
```ruby
|
319
|
+
shape = PerfectShape::Ellipse.new(x: 2, y: 3, width: 50, height: 60)
|
320
|
+
shape2 = PerfectShape::Ellipse.new(center_x: 27, center_y: 33, radius_x: 25, radius_y: 30)
|
321
|
+
```
|
322
|
+
|
256
323
|
### `PerfectShape::Circle`
|
257
324
|
|
258
325
|
Class
|
@@ -283,6 +350,13 @@ Extends `PerfectShape::Ellipse`
|
|
283
350
|
- `#contain?(x_or_point, y=nil)`: checks if point is inside
|
284
351
|
- `#==(other)`: Returns `true` if equal to `other` or `false` otherwise
|
285
352
|
|
353
|
+
Example:
|
354
|
+
|
355
|
+
```ruby
|
356
|
+
shape = PerfectShape::Circle.new(x: 2, y: 3, diameter: 60)
|
357
|
+
shape2 = PerfectShape::Circle.new(center_x: 2 + 30, center_y: 3 + 30, radius: 30)
|
358
|
+
```
|
359
|
+
|
286
360
|
### `PerfectShape::Polygon`
|
287
361
|
|
288
362
|
Class
|
@@ -291,9 +365,11 @@ Extends `PerfectShape::Shape`
|
|
291
365
|
|
292
366
|
Includes `PerfectShape::MultiPoint`
|
293
367
|
|
368
|
+
A polygon can be thought of as a special case of [path](#perfectshapepath) that is closed, has the [Even-Odd](https://en.wikipedia.org/wiki/Even%E2%80%93odd_rule) winding rule, and consists of lines only.
|
369
|
+
|
294
370
|
![polygon](https://raw.githubusercontent.com/AndyObtiva/perfect-shape/master/images/polygon.png)
|
295
371
|
|
296
|
-
- `::new(points:
|
372
|
+
- `::new(points: [])`: constructs a polygon with `points` as `Array` of `Array`s of `[x,y]` pairs or flattened `Array` of alternating x and y values
|
297
373
|
- `#min_x`: min x
|
298
374
|
- `#min_y`: min y
|
299
375
|
- `#max_x`: max x
|
@@ -306,6 +382,12 @@ Includes `PerfectShape::MultiPoint`
|
|
306
382
|
- `#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))
|
307
383
|
- `#==(other)`: Returns `true` if equal to `other` or `false` otherwise
|
308
384
|
|
385
|
+
Example:
|
386
|
+
|
387
|
+
```ruby
|
388
|
+
shape = PerfectShape::Polygon.new(points: [[200, 150], [270, 170], [250, 220], [220, 190], [200, 200], [180, 170]])
|
389
|
+
```
|
390
|
+
|
309
391
|
### `PerfectShape::Path`
|
310
392
|
|
311
393
|
Class
|
@@ -316,8 +398,8 @@ Includes `PerfectShape::MultiPoint`
|
|
316
398
|
|
317
399
|
![path](https://raw.githubusercontent.com/AndyObtiva/perfect-shape/master/images/path.png)
|
318
400
|
|
319
|
-
- `::new(shapes:
|
320
|
-
- `#shapes`: the shapes that the path is composed of
|
401
|
+
- `::new(shapes: [], closed: false, winding_rule: :wind_non_zero)`: constructs a path with `shapes` as `Array` of shape objects, which can be `PerfectShape::Point` (or `Array` of `[x, y]` coordinates), `PerfectShape::Line`, or `PerfectShape::QuadraticBezierCurve`. If a path is closed, its last point is automatically connected to its first point with a line segment. The winding rule can be `:wind_non_zero` (default) or `:wind_even_odd`.
|
402
|
+
- `#shapes`: the shapes that the path is composed of (must always start with `PerfectShape::Point` or Array of [x,y] coordinates representing start point)
|
321
403
|
- `#closed?`: returns `true` if closed and `false` otherwise
|
322
404
|
- `#winding_rule`: returns winding rule (`:wind_non_zero` or `:wind_even_odd`)
|
323
405
|
- `#points`: path points calculated (derived) from shapes
|
@@ -329,11 +411,22 @@ Includes `PerfectShape::MultiPoint`
|
|
329
411
|
- `#height`: height (from min y to max y)
|
330
412
|
- `#center_x`: center x
|
331
413
|
- `#center_y`: center y
|
332
|
-
- `#bounding_box`: bounding box is a rectangle with x = min x, y = min y, and width/height of shape
|
414
|
+
- `#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)
|
333
415
|
- `#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))
|
334
416
|
- `#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)
|
335
417
|
- `#==(other)`: Returns `true` if equal to `other` or `false` otherwise
|
336
418
|
|
419
|
+
Example:
|
420
|
+
|
421
|
+
```ruby
|
422
|
+
path_shapes = []
|
423
|
+
path_shapes << PerfectShape::Point.new(x: 200, y: 150)
|
424
|
+
path_shapes << PerfectShape::Line.new(points: [250, 170]) # no need for start point, just end point
|
425
|
+
path_shapes << PerfectShape::QuadraticBezierCurve.new(points: [[300, 185], [350, 150]]) # no need for start point, just control point and end point
|
426
|
+
|
427
|
+
shape = PerfectShape::Path.new(shapes: path_shapes, closed: false, winding_rule: :wind_even_odd)
|
428
|
+
```
|
429
|
+
|
337
430
|
## Process
|
338
431
|
|
339
432
|
[Glimmer Process](https://github.com/AndyObtiva/glimmer/blob/master/PROCESS.md)
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.1.
|
1
|
+
0.1.1
|
data/lib/perfect_shape/line.rb
CHANGED
@@ -189,14 +189,14 @@ module PerfectShape
|
|
189
189
|
# +1 is returned for a crossing where the Y coordinate is increasing
|
190
190
|
# -1 is returned for a crossing where the Y coordinate is decreasing
|
191
191
|
def point_crossings(x1, y1, x2, y2, px, py)
|
192
|
-
return 0 if (py < y1 && py < y2)
|
193
|
-
return 0 if (py >= y1 && py >= y2)
|
192
|
+
return BigDecimal('0') if (py < y1 && py < y2)
|
193
|
+
return BigDecimal('0') if (py >= y1 && py >= y2)
|
194
194
|
# assert(y1 != y2);
|
195
|
-
return 0 if (px >= x1 && px >= x2)
|
195
|
+
return BigDecimal('0') if (px >= x1 && px >= x2)
|
196
196
|
return ((y1 < y2) ? 1 : -1) if (px < x1 && px < x2)
|
197
197
|
xintercept = x1 + (py - y1) * (x2 - x1) / (y2 - y1);
|
198
|
-
return 0 if (px >= xintercept)
|
199
|
-
(y1 < y2) ? 1 : -1
|
198
|
+
return BigDecimal('0') if (px >= xintercept)
|
199
|
+
(y1 < y2) ? BigDecimal('1') : BigDecimal('-1')
|
200
200
|
end
|
201
201
|
end
|
202
202
|
|
@@ -26,8 +26,8 @@ module PerfectShape
|
|
26
26
|
module MultiPoint
|
27
27
|
attr_reader :points
|
28
28
|
|
29
|
-
def initialize(points:
|
30
|
-
self.points = points
|
29
|
+
def initialize(points: [])
|
30
|
+
self.points = points
|
31
31
|
end
|
32
32
|
|
33
33
|
# Sets points, normalizing to an Array of Arrays of (x,y) pairs as BigDecimal
|
data/lib/perfect_shape/path.rb
CHANGED
@@ -22,6 +22,7 @@
|
|
22
22
|
require 'perfect_shape/shape'
|
23
23
|
require 'perfect_shape/point'
|
24
24
|
require 'perfect_shape/line'
|
25
|
+
require 'perfect_shape/quadratic_bezier_curve'
|
25
26
|
require 'perfect_shape/multi_point'
|
26
27
|
|
27
28
|
module PerfectShape
|
@@ -47,20 +48,24 @@ module PerfectShape
|
|
47
48
|
end
|
48
49
|
|
49
50
|
def points
|
50
|
-
|
51
|
+
the_points = []
|
52
|
+
@shapes.each do |shape|
|
51
53
|
case shape
|
52
54
|
when Point
|
53
|
-
shape.to_a
|
55
|
+
the_points << shape.to_a
|
54
56
|
when Array
|
55
|
-
shape
|
57
|
+
the_points << shape.map {|n| BigDecimal(n.to_s)}
|
56
58
|
when Line
|
57
|
-
shape.points.last.to_a
|
58
|
-
|
59
|
+
the_points << shape.points.last.to_a
|
60
|
+
when QuadraticBezierCurve
|
61
|
+
shape.points.each do |point|
|
62
|
+
the_points << point.to_a
|
63
|
+
end
|
59
64
|
# when CubicBezierCurve # TODO
|
60
65
|
end
|
61
|
-
end.tap do |the_points|
|
62
|
-
the_points << @shapes.first.to_a if closed?
|
63
66
|
end
|
67
|
+
the_points << @shapes.first.to_a if closed?
|
68
|
+
the_points
|
64
69
|
end
|
65
70
|
|
66
71
|
def points=(some_points)
|
@@ -76,7 +81,8 @@ module PerfectShape
|
|
76
81
|
:move_to
|
77
82
|
when Line
|
78
83
|
:line_to
|
79
|
-
|
84
|
+
when QuadraticBezierCurve
|
85
|
+
:quad_to
|
80
86
|
# when CubicBezierCurve # TODO
|
81
87
|
end
|
82
88
|
end
|
@@ -107,7 +113,7 @@ module PerfectShape
|
|
107
113
|
# Here we know that both x and y are finite.
|
108
114
|
return false if shapes.count < 2
|
109
115
|
mask = winding_rule == :wind_non_zero ? -1 : 1
|
110
|
-
(point_crossings(x, y) & mask) != 0
|
116
|
+
(point_crossings(x, y).to_i & mask) != 0
|
111
117
|
else
|
112
118
|
# Either x or y was infinite or NaN.
|
113
119
|
# A NaN always produces a negative response to any test
|
@@ -131,13 +137,13 @@ module PerfectShape
|
|
131
137
|
def point_crossings(x_or_point, y = nil)
|
132
138
|
x, y = normalize_point(x_or_point, y)
|
133
139
|
return unless x && y
|
134
|
-
return 0 if shapes.count == 0
|
140
|
+
return BigDecimal('0') if shapes.count == 0
|
135
141
|
movx = movy = curx = cury = endx = endy = 0
|
136
142
|
coords = points.flatten
|
137
143
|
curx = movx = coords[0]
|
138
144
|
cury = movy = coords[1]
|
139
|
-
crossings = 0
|
140
|
-
ci = 2
|
145
|
+
crossings = BigDecimal('0')
|
146
|
+
ci = BigDecimal('2')
|
141
147
|
1.upto(shapes.count - 1).each do |i|
|
142
148
|
case drawing_types[i]
|
143
149
|
when :move_to
|
@@ -158,17 +164,19 @@ module PerfectShape
|
|
158
164
|
crossings += line.point_crossings(x, y)
|
159
165
|
curx = endx;
|
160
166
|
cury = endy;
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
167
|
+
when :quad_to
|
168
|
+
quad_ctrlx = coords[ci]
|
169
|
+
ci += 1
|
170
|
+
quad_ctrly = coords[ci]
|
171
|
+
ci += 1
|
172
|
+
endx = coords[ci]
|
173
|
+
ci += 1
|
174
|
+
endy = coords[ci]
|
175
|
+
ci += 1
|
176
|
+
quad = PerfectShape::QuadraticBezierCurve.new(points: [[curx, cury], [quad_ctrlx, quad_ctrly], [endx, endy]])
|
177
|
+
crossings += quad.point_crossings(x, y, 0)
|
178
|
+
curx = endx;
|
179
|
+
cury = endy;
|
172
180
|
# when :cubic_to # TODO
|
173
181
|
# crossings +=
|
174
182
|
# Curve.point_crossings_for_cubic(x, y,
|
@@ -0,0 +1,182 @@
|
|
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/multi_point'
|
24
|
+
|
25
|
+
module PerfectShape
|
26
|
+
# Mostly ported from java.awt.geom: https://docs.oracle.com/javase/8/docs/api/java/awt/geom/QuadCurve2D.html
|
27
|
+
class QuadraticBezierCurve < Shape
|
28
|
+
class << self
|
29
|
+
def point_crossings(x1, y1, xc, yc, x2, y2, px, py, level = 0)
|
30
|
+
return BigDecimal('0') if (py < y1 && py < yc && py < y2)
|
31
|
+
return BigDecimal('0') if (py >= y1 && py >= yc && py >= y2)
|
32
|
+
# Note y1 could equal y2...
|
33
|
+
return BigDecimal('0') if (px >= x1 && px >= xc && px >= x2)
|
34
|
+
if (px < x1 && px < xc && px < x2)
|
35
|
+
if (py >= y1)
|
36
|
+
return BigDecimal('1') if (py < y2)
|
37
|
+
else
|
38
|
+
# py < y1
|
39
|
+
return BigDecimal('-1') if (py >= y2)
|
40
|
+
end
|
41
|
+
# py outside of y11 range, and/or y1==y2
|
42
|
+
return BigDecimal('0')
|
43
|
+
end
|
44
|
+
# double precision only has 52 bits of mantissa
|
45
|
+
return PerfectShape::Line.point_crossings(x1, y1, x2, y2, px, py) if (level > 52)
|
46
|
+
x1c = (x1 + xc) / BigDecimal('2')
|
47
|
+
y1c = (y1 + yc) / BigDecimal('2')
|
48
|
+
xc1 = (xc + x2) / BigDecimal('2')
|
49
|
+
yc1 = (yc + y2) / BigDecimal('2')
|
50
|
+
xc = (x1c + xc1) / BigDecimal('2')
|
51
|
+
yc = (y1c + yc1) / BigDecimal('2')
|
52
|
+
# [xy]c are NaN if any of [xy]0c or [xy]c1 are NaN
|
53
|
+
# [xy]0c or [xy]c1 are NaN if any of [xy][0c1] are NaN
|
54
|
+
# These values are also NaN if opposing infinities are added
|
55
|
+
return BigDecimal('0') if (xc.nan? || yc.nan?)
|
56
|
+
point_crossings(x1, y1, x1c, y1c, xc, yc, px, py, level+1) +
|
57
|
+
point_crossings(xc, yc, xc1, yc1, x2, y2, px, py, level+1);
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
include MultiPoint
|
62
|
+
include Equalizer.new(:points)
|
63
|
+
|
64
|
+
# Checks if quadratic bézier curve contains point (two-number Array or x, y args)
|
65
|
+
#
|
66
|
+
# @param x The X coordinate of the point to test.
|
67
|
+
# @param y The Y coordinate of the point to test.
|
68
|
+
#
|
69
|
+
# @return {@code true} if the point lies within the bound of
|
70
|
+
# the quadratic bézier curve, {@code false} if the point lies outside of the
|
71
|
+
# quadratic bézier curve's bounds.
|
72
|
+
def contain?(x_or_point, y = nil, distance: 0)
|
73
|
+
x, y = normalize_point(x_or_point, y)
|
74
|
+
return unless x && y
|
75
|
+
|
76
|
+
x1 = points[0][0]
|
77
|
+
y1 = points[0][1]
|
78
|
+
xc = points[1][0]
|
79
|
+
yc = points[1][1]
|
80
|
+
x2 = points[2][0]
|
81
|
+
y2 = points[2][1]
|
82
|
+
|
83
|
+
# We have a convex shape bounded by quad curve Pc(t)
|
84
|
+
# and ine Pl(t).
|
85
|
+
#
|
86
|
+
# P1 = (x1, y1) - start point of curve
|
87
|
+
# P2 = (x2, y2) - end point of curve
|
88
|
+
# Pc = (xc, yc) - control point
|
89
|
+
#
|
90
|
+
# Pq(t) = P1*(1 - t)^2 + 2*Pc*t*(1 - t) + P2*t^2 =
|
91
|
+
# = (P1 - 2*Pc + P2)*t^2 + 2*(Pc - P1)*t + P1
|
92
|
+
# Pl(t) = P1*(1 - t) + P2*t
|
93
|
+
# t = [0:1]
|
94
|
+
#
|
95
|
+
# P = (x, y) - point of interest
|
96
|
+
#
|
97
|
+
# Let's look at second derivative of quad curve equation:
|
98
|
+
#
|
99
|
+
# Pq''(t) = 2 * (P1 - 2 * Pc + P2) = Pq''
|
100
|
+
# It's constant vector.
|
101
|
+
#
|
102
|
+
# Let's draw a line through P to be parallel to this
|
103
|
+
# vector and find the intersection of the quad curve
|
104
|
+
# and the line.
|
105
|
+
#
|
106
|
+
# Pq(t) is point of intersection if system of equations
|
107
|
+
# below has the solution.
|
108
|
+
#
|
109
|
+
# L(s) = P + Pq''*s == Pq(t)
|
110
|
+
# Pq''*s + (P - Pq(t)) == 0
|
111
|
+
#
|
112
|
+
# | xq''*s + (x - xq(t)) == 0
|
113
|
+
# | yq''*s + (y - yq(t)) == 0
|
114
|
+
#
|
115
|
+
# This system has the solution if rank of its matrix equals to 1.
|
116
|
+
# That is, determinant of the matrix should be zero.
|
117
|
+
#
|
118
|
+
# (y - yq(t))*xq'' == (x - xq(t))*yq''
|
119
|
+
#
|
120
|
+
# Let's solve this equation with 't' variable.
|
121
|
+
# Also let kx = x1 - 2*xc + x2
|
122
|
+
# ky = y1 - 2*yc + y2
|
123
|
+
#
|
124
|
+
# t0q = (1/2)*((x - x1)*ky - (y - y1)*kx) /
|
125
|
+
# ((xc - x1)*ky - (yc - y1)*kx)
|
126
|
+
#
|
127
|
+
# Let's do the same for our line Pl(t):
|
128
|
+
#
|
129
|
+
# t0l = ((x - x1)*ky - (y - y1)*kx) /
|
130
|
+
# ((x2 - x1)*ky - (y2 - y1)*kx)
|
131
|
+
#
|
132
|
+
# It's easy to check that t0q == t0l. This fact means
|
133
|
+
# we can compute t0 only one time.
|
134
|
+
#
|
135
|
+
# In case t0 < 0 or t0 > 1, we have an intersections outside
|
136
|
+
# of shape bounds. So, P is definitely out of shape.
|
137
|
+
#
|
138
|
+
# In case t0 is inside [0:1], we should calculate Pq(t0)
|
139
|
+
# and Pl(t0). We have three points for now, and all of them
|
140
|
+
# lie on one line. So, we just need to detect, is our point
|
141
|
+
# of interest between points of intersections or not.
|
142
|
+
#
|
143
|
+
# If the denominator in the t0q and t0l equations is
|
144
|
+
# zero, then the points must be collinear and so the
|
145
|
+
# curve is degenerate and encloses no area. Thus the
|
146
|
+
# result is false.
|
147
|
+
kx = x1 - 2 * xc + x2;
|
148
|
+
ky = y1 - 2 * yc + y2;
|
149
|
+
dx = x - x1;
|
150
|
+
dy = y - y1;
|
151
|
+
dxl = x2 - x1;
|
152
|
+
dyl = y2 - y1;
|
153
|
+
|
154
|
+
t0 = (dx * ky - dy * kx) / (dxl * ky - dyl * kx)
|
155
|
+
return false if (t0 < 0 || t0 > 1 || t0 != t0)
|
156
|
+
|
157
|
+
xb = kx * t0 * t0 + 2 * (xc - x1) * t0 + x1;
|
158
|
+
yb = ky * t0 * t0 + 2 * (yc - y1) * t0 + y1;
|
159
|
+
xl = dxl * t0 + x1;
|
160
|
+
yl = dyl * t0 + y1;
|
161
|
+
|
162
|
+
(x >= xb && x < xl) ||
|
163
|
+
(x >= xl && x < xb) ||
|
164
|
+
(y >= yb && y < yl) ||
|
165
|
+
(y >= yl && y < yb)
|
166
|
+
end
|
167
|
+
|
168
|
+
# Calculates the number of times the quad
|
169
|
+
# crosses the ray extending to the right from (x,y).
|
170
|
+
# If the point lies on a part of the curve,
|
171
|
+
# then no crossings are counted for that intersection.
|
172
|
+
# the level parameter should be 0 at the top-level call and will count
|
173
|
+
# up for each recursion level to prevent infinite recursion
|
174
|
+
# +1 is added for each crossing where the Y coordinate is increasing
|
175
|
+
# -1 is added for each crossing where the Y coordinate is decreasing
|
176
|
+
def point_crossings(x_or_point, y = nil, level = 0)
|
177
|
+
x, y = normalize_point(x_or_point, y)
|
178
|
+
return unless x && y
|
179
|
+
QuadraticBezierCurve.point_crossings(points[0][0], points[0][1], points[1][0], points[1][1], points[2][0], points[2][1], x, y, level)
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end
|
data/perfect-shape.gemspec
CHANGED
@@ -2,17 +2,17 @@
|
|
2
2
|
# DO NOT EDIT THIS FILE DIRECTLY
|
3
3
|
# Instead, edit Juwelier::Tasks in Rakefile, and run 'rake gemspec'
|
4
4
|
# -*- encoding: utf-8 -*-
|
5
|
-
# stub: perfect-shape 0.1.
|
5
|
+
# stub: perfect-shape 0.1.1 ruby lib
|
6
6
|
|
7
7
|
Gem::Specification.new do |s|
|
8
8
|
s.name = "perfect-shape".freeze
|
9
|
-
s.version = "0.1.
|
9
|
+
s.version = "0.1.1"
|
10
10
|
|
11
11
|
s.required_rubygems_version = Gem::Requirement.new(">= 0".freeze) if s.respond_to? :required_rubygems_version=
|
12
12
|
s.require_paths = ["lib".freeze]
|
13
13
|
s.authors = ["Andy Maleh".freeze]
|
14
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,
|
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
|
16
16
|
s.email = "andy.am@gmail.com".freeze
|
17
17
|
s.extra_rdoc_files = [
|
18
18
|
"CHANGELOG.md",
|
@@ -35,6 +35,7 @@ Gem::Specification.new do |s|
|
|
35
35
|
"lib/perfect_shape/point.rb",
|
36
36
|
"lib/perfect_shape/point_location.rb",
|
37
37
|
"lib/perfect_shape/polygon.rb",
|
38
|
+
"lib/perfect_shape/quadratic_bezier_curve.rb",
|
38
39
|
"lib/perfect_shape/rectangle.rb",
|
39
40
|
"lib/perfect_shape/rectangular_shape.rb",
|
40
41
|
"lib/perfect_shape/shape.rb",
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: perfect-shape
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Andy Maleh
|
@@ -97,11 +97,10 @@ dependencies:
|
|
97
97
|
description: Perfect Shape is a collection of pure Ruby geometric algorithms that
|
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
|
-
pie), ellipse, circle, polygon,
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
IEEE-754 remainder).
|
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,
|
102
|
+
and Winding Number Algorithm, aka Nonzero Rule). Additionally, it contains some
|
103
|
+
purely mathematical algorithms like IEEEremainder (also known as IEEE-754 remainder).
|
105
104
|
email: andy.am@gmail.com
|
106
105
|
executables: []
|
107
106
|
extensions: []
|
@@ -125,6 +124,7 @@ files:
|
|
125
124
|
- lib/perfect_shape/point.rb
|
126
125
|
- lib/perfect_shape/point_location.rb
|
127
126
|
- lib/perfect_shape/polygon.rb
|
127
|
+
- lib/perfect_shape/quadratic_bezier_curve.rb
|
128
128
|
- lib/perfect_shape/rectangle.rb
|
129
129
|
- lib/perfect_shape/rectangular_shape.rb
|
130
130
|
- lib/perfect_shape/shape.rb
|