perfect-shape 0.1.1 → 0.1.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +7 -0
- data/README.md +40 -8
- data/VERSION +1 -1
- data/lib/perfect_shape/cubic_bezier_curve.rb +120 -0
- data/lib/perfect_shape/line.rb +5 -5
- data/lib/perfect_shape/path.rb +70 -58
- data/lib/perfect_shape/quadratic_bezier_curve.rb +22 -14
- data/perfect-shape.gemspec +3 -2
- metadata +2 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: cfa0c67a9d8a4b23b53bdd7895121fa51ee3da17e1d22fa9bb56c0890a47844b
|
4
|
+
data.tar.gz: 81cd3fe5766f023fc250951cab6541142d80ff70b8af3d8cbbfcefcb56154cba
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ea4539cd6122363483988ef9de8b712ebd981c2e7102d5510e94f9d093bad1c02d17cd0e1c4eebe5ebc3e4211a67d22ca7ee2630cdf561404a9adc36031de220
|
7
|
+
data.tar.gz: 4a9fcfa3accf7ca519c403e24076bfc76b1bd9e2b47fda886e0012af610d3e142257d09c6508a04deccf665b536c41b60cc68047ba2c76f06a667be2625b0c40
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,12 @@
|
|
1
1
|
# Change Log
|
2
2
|
|
3
|
+
## 0.1.2
|
4
|
+
|
5
|
+
- `PerfectShape::CubicBezierCurve` (two end points and two control points)
|
6
|
+
- `PerfectShape::CubicBezierCurve#contain?(x_or_point, y=nil)`
|
7
|
+
- `PerfectShape::CubicBezierCurve#==`
|
8
|
+
- `PerfectShape::Path` having cubic bezier curves in addition to points, lines, and quadratic bezier curves
|
9
|
+
|
3
10
|
## 0.1.1
|
4
11
|
|
5
12
|
- `PerfectShape::QuadraticBezierCurve` (two end points and one control point)
|
data/README.md
CHANGED
@@ -1,9 +1,9 @@
|
|
1
|
-
# Perfect Shape 0.1.
|
1
|
+
# Perfect Shape 0.1.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 (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 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)).
|
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.2
|
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.2'
|
24
24
|
```
|
25
25
|
|
26
26
|
And, run:
|
@@ -133,7 +133,7 @@ Includes `PerfectShape::MultiPoint`
|
|
133
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)”.
|
134
134
|
- `::point_segment_distance_square(x1, y1, x2, y2, px, py)`: Returns the square of distance from a point to a line segment.
|
135
135
|
- `::point_segment_distance(x1, y1, x2, y2, px, py)`: Returns the distance from a point to a line segment.
|
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
|
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 coordinates
|
137
137
|
- `#min_x`: min x
|
138
138
|
- `#min_y`: min y
|
139
139
|
- `#max_x`: max x
|
@@ -164,7 +164,8 @@ Includes `PerfectShape::MultiPoint`
|
|
164
164
|
|
165
165
|
![quadratic_bezier_curve](https://raw.githubusercontent.com/AndyObtiva/perfect-shape/master/images/quadratic_bezier_curve.png)
|
166
166
|
|
167
|
-
- `::new(points: [])`: constructs a quadratic bézier curve with three `points` (
|
167
|
+
- `::new(points: [])`: constructs a quadratic bézier curve with three `points` (start point, control point, and end point) as `Array` of `Array`s of `[x,y]` pairs or flattened `Array` of alternating x and y coordinates
|
168
|
+
- `#points`: points (start point, control point, and end point)
|
168
169
|
- `#min_x`: min x
|
169
170
|
- `#min_y`: min y
|
170
171
|
- `#max_x`: max x
|
@@ -183,6 +184,36 @@ Example:
|
|
183
184
|
shape = PerfectShape::QuadraticBezierCurve.new(points: [[200, 150], [270, 220], [180, 170]]) # start point, control point, and end point
|
184
185
|
```
|
185
186
|
|
187
|
+
### `PerfectShape::CubicBezierCurve`
|
188
|
+
|
189
|
+
Class
|
190
|
+
|
191
|
+
Extends `PerfectShape::Shape`
|
192
|
+
|
193
|
+
Includes `PerfectShape::MultiPoint`
|
194
|
+
|
195
|
+
![cubic_bezier_curve](https://raw.githubusercontent.com/AndyObtiva/perfect-shape/master/images/cubic_bezier_curve.png)
|
196
|
+
|
197
|
+
- `::new(points: [])`: constructs a cubic bézier curve with four `points` (start point, two control points, and end point) as `Array` of `Array`s of `[x,y]` pairs or flattened `Array` of alternating x and y coordinates
|
198
|
+
- `#points`: points (start point, two control points, and end point)
|
199
|
+
- `#min_x`: min x
|
200
|
+
- `#min_y`: min y
|
201
|
+
- `#max_x`: max x
|
202
|
+
- `#max_y`: max y
|
203
|
+
- `#width`: width (from min x to max x)
|
204
|
+
- `#height`: height (from min y to max y)
|
205
|
+
- `#center_x`: center x
|
206
|
+
- `#center_y`: center y
|
207
|
+
- `#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
|
+
- `#==(other)`: Returns `true` if equal to `other` or `false` otherwise
|
210
|
+
|
211
|
+
Example:
|
212
|
+
|
213
|
+
```ruby
|
214
|
+
shape = PerfectShape::CubicBezierCurve.new(points: [[200, 150], [230, 50], [270, 220], [180, 170]]) # start point, two control points, and end point
|
215
|
+
```
|
216
|
+
|
186
217
|
### `PerfectShape::Rectangle`
|
187
218
|
|
188
219
|
Class
|
@@ -369,7 +400,7 @@ A polygon can be thought of as a special case of [path](#perfectshapepath) that
|
|
369
400
|
|
370
401
|
![polygon](https://raw.githubusercontent.com/AndyObtiva/perfect-shape/master/images/polygon.png)
|
371
402
|
|
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
|
403
|
+
- `::new(points: [])`: constructs a polygon with `points` as `Array` of `Array`s of `[x,y]` pairs or flattened `Array` of alternating x and y coordinates
|
373
404
|
- `#min_x`: min x
|
374
405
|
- `#min_y`: min y
|
375
406
|
- `#max_x`: max x
|
@@ -398,7 +429,7 @@ Includes `PerfectShape::MultiPoint`
|
|
398
429
|
|
399
430
|
![path](https://raw.githubusercontent.com/AndyObtiva/perfect-shape/master/images/path.png)
|
400
431
|
|
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::
|
432
|
+
- `::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`, `PerfectShape::QuadraticBezierCurve`, or `PerfectShape::CubicBezierCurve`. 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
433
|
- `#shapes`: the shapes that the path is composed of (must always start with `PerfectShape::Point` or Array of [x,y] coordinates representing start point)
|
403
434
|
- `#closed?`: returns `true` if closed and `false` otherwise
|
404
435
|
- `#winding_rule`: returns winding rule (`:wind_non_zero` or `:wind_even_odd`)
|
@@ -423,6 +454,7 @@ path_shapes = []
|
|
423
454
|
path_shapes << PerfectShape::Point.new(x: 200, y: 150)
|
424
455
|
path_shapes << PerfectShape::Line.new(points: [250, 170]) # no need for start point, just end point
|
425
456
|
path_shapes << PerfectShape::QuadraticBezierCurve.new(points: [[300, 185], [350, 150]]) # no need for start point, just control point and end point
|
457
|
+
path_shapes << PerfectShape::CubicBezierCurve.new(points: [[370, 50], [430, 220], [480, 170]]) # no need for start point, just two control points and end point
|
426
458
|
|
427
459
|
shape = PerfectShape::Path.new(shapes: path_shapes, closed: false, winding_rule: :wind_even_odd)
|
428
460
|
```
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.1.
|
1
|
+
0.1.2
|
@@ -0,0 +1,120 @@
|
|
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 CubicBezierCurve < Shape
|
28
|
+
class << self
|
29
|
+
# Calculates the number of times the cubic bézier curve from (x1,y1) to (x2,y2)
|
30
|
+
# crosses the ray extending to the right from (x,y).
|
31
|
+
# If the point lies on a part of the curve,
|
32
|
+
# then no crossings are counted for that intersection.
|
33
|
+
# the level parameter should be 0 at the top-level call and will count
|
34
|
+
# up for each recursion level to prevent infinite recursion
|
35
|
+
# +1 is added for each crossing where the Y coordinate is increasing
|
36
|
+
# -1 is added for each crossing where the Y coordinate is decreasing
|
37
|
+
def point_crossings(x1, y1, xc1, yc1, xc2, yc2, x2, y2, px, py, level = 0)
|
38
|
+
return 0 if (py < y1 && py < yc1 && py < yc2 && py < y2)
|
39
|
+
return 0 if (py >= y1 && py >= yc1 && py >= yc2 && py >= y2)
|
40
|
+
# Note y1 could equal yc1...
|
41
|
+
return 0 if (px >= x1 && px >= xc1 && px >= xc2 && px >= x2)
|
42
|
+
if (px < x1 && px < xc1 && px < xc2 && px < x2)
|
43
|
+
if (py >= y1)
|
44
|
+
return 1 if (py < y2)
|
45
|
+
else
|
46
|
+
# py < y1
|
47
|
+
return -1 if (py >= y2)
|
48
|
+
end
|
49
|
+
# py outside of y12 range, and/or y1==yc1
|
50
|
+
return 0
|
51
|
+
end
|
52
|
+
# double precision only has 52 bits of mantissa
|
53
|
+
return PerfectShape::Line.point_crossings(x1, y1, x2, y2, px, py) if (level > 52)
|
54
|
+
xmid = BigDecimal((xc1 + xc2).to_s) / 2;
|
55
|
+
ymid = BigDecimal((yc1 + yc2).to_s) / 2;
|
56
|
+
xc1 = BigDecimal((x1 + xc1).to_s) / 2;
|
57
|
+
yc1 = BigDecimal((y1 + yc1).to_s) / 2;
|
58
|
+
xc2 = BigDecimal((xc2 + x2).to_s) / 2;
|
59
|
+
yc2 = BigDecimal((yc2 + y2).to_s) / 2;
|
60
|
+
xc1m = BigDecimal((xc1 + xmid).to_s) / 2;
|
61
|
+
yc1m = BigDecimal((yc1 + ymid).to_s) / 2;
|
62
|
+
xmc1 = BigDecimal((xmid + xc2).to_s) / 2;
|
63
|
+
ymc1 = BigDecimal((ymid + yc2).to_s) / 2;
|
64
|
+
xmid = BigDecimal((xc1m + xmc1).to_s) / 2;
|
65
|
+
ymid = BigDecimal((yc1m + ymc1).to_s) / 2;
|
66
|
+
# [xy]mid are NaN if any of [xy]c0m or [xy]mc1 are NaN
|
67
|
+
# [xy]c0m or [xy]mc1 are NaN if any of [xy][c][01] are NaN
|
68
|
+
# These values are also NaN if opposing infinities are added
|
69
|
+
return 0 if (xmid.nan? || ymid.nan?)
|
70
|
+
point_crossings(x1, y1, xc1, yc1, xc1m, yc1m, xmid, ymid, px, py, level+1) +
|
71
|
+
point_crossings(xmid, ymid, xmc1, ymc1, xc2, yc2, x2, y2, px, py, level+1)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
include MultiPoint
|
76
|
+
include Equalizer.new(:points)
|
77
|
+
|
78
|
+
# Checks if cubic bézier curve contains point (two-number Array or x, y args)
|
79
|
+
#
|
80
|
+
# @param x The X coordinate of the point to test.
|
81
|
+
# @param y The Y coordinate of the point to test.
|
82
|
+
#
|
83
|
+
# @return {@code true} if the point lies within the bound of
|
84
|
+
# the cubic bézier curve, {@code false} if the point lies outside of the
|
85
|
+
# cubic bézier curve's bounds.
|
86
|
+
def contain?(x_or_point, y = nil)
|
87
|
+
x, y = normalize_point(x_or_point, y)
|
88
|
+
return unless x && y
|
89
|
+
|
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
|
104
|
+
end
|
105
|
+
|
106
|
+
# Calculates the number of times the cubic bézier curve
|
107
|
+
# crosses the ray extending to the right from (x,y).
|
108
|
+
# If the point lies on a part of the curve,
|
109
|
+
# then no crossings are counted for that intersection.
|
110
|
+
# the level parameter should be 0 at the top-level call and will count
|
111
|
+
# up for each recursion level to prevent infinite recursion
|
112
|
+
# +1 is added for each crossing where the Y coordinate is increasing
|
113
|
+
# -1 is added for each crossing where the Y coordinate is decreasing
|
114
|
+
def point_crossings(x_or_point, y = nil, level = 0)
|
115
|
+
x, y = normalize_point(x_or_point, y)
|
116
|
+
return unless x && y
|
117
|
+
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
|
+
end
|
119
|
+
end
|
120
|
+
end
|
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
|
193
|
-
return
|
192
|
+
return 0 if (py < y1 && py < y2)
|
193
|
+
return 0 if (py >= y1 && py >= y2)
|
194
194
|
# assert(y1 != y2);
|
195
|
-
return
|
195
|
+
return 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
|
199
|
-
(y1 < y2) ?
|
198
|
+
return 0 if (px >= xintercept)
|
199
|
+
(y1 < y2) ? 1 : -1
|
200
200
|
end
|
201
201
|
end
|
202
202
|
|
data/lib/perfect_shape/path.rb
CHANGED
@@ -23,6 +23,7 @@ require 'perfect_shape/shape'
|
|
23
23
|
require 'perfect_shape/point'
|
24
24
|
require 'perfect_shape/line'
|
25
25
|
require 'perfect_shape/quadratic_bezier_curve'
|
26
|
+
require 'perfect_shape/cubic_bezier_curve'
|
26
27
|
require 'perfect_shape/multi_point'
|
27
28
|
|
28
29
|
module PerfectShape
|
@@ -31,7 +32,10 @@ module PerfectShape
|
|
31
32
|
include MultiPoint
|
32
33
|
include Equalizer.new(:shapes, :closed, :winding_rule)
|
33
34
|
|
34
|
-
|
35
|
+
# Available class types for path shapes
|
36
|
+
SHAPE_TYPES = [Array, PerfectShape::Point, PerfectShape::Line, PerfectShape::QuadraticBezierCurve, PerfectShape::CubicBezierCurve]
|
37
|
+
|
38
|
+
# Available winding rules
|
35
39
|
WINDING_RULES = [:wind_non_zero, :wind_even_odd]
|
36
40
|
|
37
41
|
attr_reader :winding_rule
|
@@ -61,7 +65,10 @@ module PerfectShape
|
|
61
65
|
shape.points.each do |point|
|
62
66
|
the_points << point.to_a
|
63
67
|
end
|
64
|
-
|
68
|
+
when CubicBezierCurve
|
69
|
+
shape.points.each do |point|
|
70
|
+
the_points << point.to_a
|
71
|
+
end
|
65
72
|
end
|
66
73
|
end
|
67
74
|
the_points << @shapes.first.to_a if closed?
|
@@ -83,7 +90,8 @@ module PerfectShape
|
|
83
90
|
:line_to
|
84
91
|
when QuadraticBezierCurve
|
85
92
|
:quad_to
|
86
|
-
|
93
|
+
when CubicBezierCurve
|
94
|
+
:cubic_to
|
87
95
|
end
|
88
96
|
end
|
89
97
|
the_drawing_shapes << :close if closed?
|
@@ -113,7 +121,7 @@ module PerfectShape
|
|
113
121
|
# Here we know that both x and y are finite.
|
114
122
|
return false if shapes.count < 2
|
115
123
|
mask = winding_rule == :wind_non_zero ? -1 : 1
|
116
|
-
(point_crossings(x, y)
|
124
|
+
(point_crossings(x, y) & mask) != 0
|
117
125
|
else
|
118
126
|
# Either x or y was infinite or NaN.
|
119
127
|
# A NaN always produces a negative response to any test
|
@@ -137,67 +145,71 @@ module PerfectShape
|
|
137
145
|
def point_crossings(x_or_point, y = nil)
|
138
146
|
x, y = normalize_point(x_or_point, y)
|
139
147
|
return unless x && y
|
140
|
-
return
|
148
|
+
return 0 if shapes.count == 0
|
141
149
|
movx = movy = curx = cury = endx = endy = 0
|
142
150
|
coords = points.flatten
|
143
151
|
curx = movx = coords[0]
|
144
152
|
cury = movy = coords[1]
|
145
|
-
crossings =
|
146
|
-
ci =
|
153
|
+
crossings = 0
|
154
|
+
ci = 2
|
147
155
|
1.upto(shapes.count - 1).each do |i|
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
crossings += line.point_crossings(x, y)
|
153
|
-
end
|
154
|
-
movx = curx = coords[ci]
|
155
|
-
ci += 1
|
156
|
-
movy = cury = coords[ci]
|
157
|
-
ci += 1
|
158
|
-
when :line_to
|
159
|
-
endx = coords[ci]
|
160
|
-
ci += 1
|
161
|
-
endy = coords[ci]
|
162
|
-
ci += 1
|
163
|
-
line = PerfectShape::Line.new(points: [[curx, cury], [endx, endy]])
|
156
|
+
case drawing_types[i]
|
157
|
+
when :move_to
|
158
|
+
if cury != movy
|
159
|
+
line = PerfectShape::Line.new(points: [[curx, cury], [movx, movy]])
|
164
160
|
crossings += line.point_crossings(x, y)
|
165
|
-
curx = endx;
|
166
|
-
cury = endy;
|
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;
|
180
|
-
# when :cubic_to # TODO
|
181
|
-
# crossings +=
|
182
|
-
# Curve.point_crossings_for_cubic(x, y,
|
183
|
-
# curx, cury,
|
184
|
-
# coords[ci++],
|
185
|
-
# coords[ci++],
|
186
|
-
# coords[ci++],
|
187
|
-
# coords[ci++],
|
188
|
-
# endx = coords[ci++],
|
189
|
-
# endy = coords[ci++],
|
190
|
-
# 0);
|
191
|
-
# curx = endx;
|
192
|
-
# cury = endy;
|
193
|
-
when :close
|
194
|
-
if cury != movy
|
195
|
-
line = PerfectShape::Line.new(points: [[curx, cury], [movx, movy]])
|
196
|
-
crossings += line.point_crossings(x, y)
|
197
|
-
end
|
198
|
-
curx = movx
|
199
|
-
cury = movy
|
200
161
|
end
|
162
|
+
movx = curx = coords[ci]
|
163
|
+
ci += 1
|
164
|
+
movy = cury = coords[ci]
|
165
|
+
ci += 1
|
166
|
+
when :line_to
|
167
|
+
endx = coords[ci]
|
168
|
+
ci += 1
|
169
|
+
endy = coords[ci]
|
170
|
+
ci += 1
|
171
|
+
line = PerfectShape::Line.new(points: [[curx, cury], [endx, endy]])
|
172
|
+
crossings += line.point_crossings(x, y)
|
173
|
+
curx = endx;
|
174
|
+
cury = endy;
|
175
|
+
when :quad_to
|
176
|
+
quad_ctrlx = coords[ci]
|
177
|
+
ci += 1
|
178
|
+
quad_ctrly = coords[ci]
|
179
|
+
ci += 1
|
180
|
+
endx = coords[ci]
|
181
|
+
ci += 1
|
182
|
+
endy = coords[ci]
|
183
|
+
ci += 1
|
184
|
+
quad = PerfectShape::QuadraticBezierCurve.new(points: [[curx, cury], [quad_ctrlx, quad_ctrly], [endx, endy]])
|
185
|
+
crossings += quad.point_crossings(x, y)
|
186
|
+
curx = endx;
|
187
|
+
cury = endy;
|
188
|
+
when :cubic_to
|
189
|
+
cubic_ctrl1x = coords[ci]
|
190
|
+
ci += 1
|
191
|
+
cubic_ctrl1y = coords[ci]
|
192
|
+
ci += 1
|
193
|
+
cubic_ctrl2x = coords[ci]
|
194
|
+
ci += 1
|
195
|
+
cubic_ctrl2y = coords[ci]
|
196
|
+
ci += 1
|
197
|
+
endx = coords[ci]
|
198
|
+
ci += 1
|
199
|
+
endy = coords[ci]
|
200
|
+
ci += 1
|
201
|
+
cubic = PerfectShape::CubicBezierCurve.new(points: [[curx, cury], [cubic_ctrl1x, cubic_ctrl1y], [cubic_ctrl2x, cubic_ctrl2y], [endx, endy]])
|
202
|
+
crossings += cubic.point_crossings(x, y)
|
203
|
+
curx = endx;
|
204
|
+
cury = endy;
|
205
|
+
when :close
|
206
|
+
if cury != movy
|
207
|
+
line = PerfectShape::Line.new(points: [[curx, cury], [movx, movy]])
|
208
|
+
crossings += line.point_crossings(x, y)
|
209
|
+
end
|
210
|
+
curx = movx
|
211
|
+
cury = movy
|
212
|
+
end
|
201
213
|
end
|
202
214
|
if cury != movy
|
203
215
|
line = PerfectShape::Line.new(points: [[curx, cury], [movx, movy]])
|
@@ -26,33 +26,41 @@ module PerfectShape
|
|
26
26
|
# Mostly ported from java.awt.geom: https://docs.oracle.com/javase/8/docs/api/java/awt/geom/QuadCurve2D.html
|
27
27
|
class QuadraticBezierCurve < Shape
|
28
28
|
class << self
|
29
|
+
# Calculates the number of times the quadratic bézier curve from (x1,y1) to (x2,y2)
|
30
|
+
# crosses the ray extending to the right from (x,y).
|
31
|
+
# If the point lies on a part of the curve,
|
32
|
+
# then no crossings are counted for that intersection.
|
33
|
+
# the level parameter should be 0 at the top-level call and will count
|
34
|
+
# up for each recursion level to prevent infinite recursion
|
35
|
+
# +1 is added for each crossing where the Y coordinate is increasing
|
36
|
+
# -1 is added for each crossing where the Y coordinate is decreasing
|
29
37
|
def point_crossings(x1, y1, xc, yc, x2, y2, px, py, level = 0)
|
30
|
-
return
|
31
|
-
return
|
38
|
+
return 0 if (py < y1 && py < yc && py < y2)
|
39
|
+
return 0 if (py >= y1 && py >= yc && py >= y2)
|
32
40
|
# Note y1 could equal y2...
|
33
|
-
return
|
41
|
+
return 0 if (px >= x1 && px >= xc && px >= x2)
|
34
42
|
if (px < x1 && px < xc && px < x2)
|
35
43
|
if (py >= y1)
|
36
|
-
return
|
44
|
+
return 1 if (py < y2)
|
37
45
|
else
|
38
46
|
# py < y1
|
39
|
-
return
|
47
|
+
return -1 if (py >= y2)
|
40
48
|
end
|
41
49
|
# py outside of y11 range, and/or y1==y2
|
42
|
-
return
|
50
|
+
return 0
|
43
51
|
end
|
44
52
|
# double precision only has 52 bits of mantissa
|
45
53
|
return PerfectShape::Line.point_crossings(x1, y1, x2, y2, px, py) if (level > 52)
|
46
|
-
x1c = (x1 + xc) /
|
47
|
-
y1c = (y1 + yc) /
|
48
|
-
xc1 = (xc + x2) /
|
49
|
-
yc1 = (yc + y2) /
|
50
|
-
xc = (x1c + xc1) /
|
51
|
-
yc = (y1c + yc1) /
|
54
|
+
x1c = BigDecimal((x1 + xc).to_s) / 2
|
55
|
+
y1c = BigDecimal((y1 + yc).to_s) / 2
|
56
|
+
xc1 = BigDecimal((xc + x2).to_s) / 2
|
57
|
+
yc1 = BigDecimal((yc + y2).to_s) / 2
|
58
|
+
xc = BigDecimal((x1c + xc1).to_s) / 2
|
59
|
+
yc = BigDecimal((y1c + yc1).to_s) / 2
|
52
60
|
# [xy]c are NaN if any of [xy]0c or [xy]c1 are NaN
|
53
61
|
# [xy]0c or [xy]c1 are NaN if any of [xy][0c1] are NaN
|
54
62
|
# These values are also NaN if opposing infinities are added
|
55
|
-
return
|
63
|
+
return 0 if (xc.nan? || yc.nan?)
|
56
64
|
point_crossings(x1, y1, x1c, y1c, xc, yc, px, py, level+1) +
|
57
65
|
point_crossings(xc, yc, xc1, yc1, x2, y2, px, py, level+1);
|
58
66
|
end
|
@@ -69,7 +77,7 @@ module PerfectShape
|
|
69
77
|
# @return {@code true} if the point lies within the bound of
|
70
78
|
# the quadratic bézier curve, {@code false} if the point lies outside of the
|
71
79
|
# quadratic bézier curve's bounds.
|
72
|
-
def contain?(x_or_point, y = nil
|
80
|
+
def contain?(x_or_point, y = nil)
|
73
81
|
x, y = normalize_point(x_or_point, y)
|
74
82
|
return unless x && y
|
75
83
|
|
data/perfect-shape.gemspec
CHANGED
@@ -2,11 +2,11 @@
|
|
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.2 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.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]
|
@@ -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/cubic_bezier_curve.rb",
|
30
31
|
"lib/perfect_shape/ellipse.rb",
|
31
32
|
"lib/perfect_shape/line.rb",
|
32
33
|
"lib/perfect_shape/math.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.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Andy Maleh
|
@@ -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/cubic_bezier_curve.rb
|
119
120
|
- lib/perfect_shape/ellipse.rb
|
120
121
|
- lib/perfect_shape/line.rb
|
121
122
|
- lib/perfect_shape/math.rb
|