perfect-shape 0.1.1 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +29 -3
- data/README.md +300 -21
- data/VERSION +1 -1
- data/lib/perfect_shape/arc.rb +48 -34
- data/lib/perfect_shape/composite_shape.rb +72 -0
- data/lib/perfect_shape/cubic_bezier_curve.rb +120 -0
- data/lib/perfect_shape/ellipse.rb +12 -8
- data/lib/perfect_shape/line.rb +9 -9
- data/lib/perfect_shape/path.rb +74 -61
- data/lib/perfect_shape/point.rb +4 -4
- data/lib/perfect_shape/polygon.rb +68 -62
- data/lib/perfect_shape/quadratic_bezier_curve.rb +22 -14
- data/lib/perfect_shape/rectangle.rb +10 -2
- data/perfect-shape.gemspec +7 -5
- metadata +6 -4
data/lib/perfect_shape/arc.rb
CHANGED
@@ -143,44 +143,58 @@ module PerfectShape
|
|
143
143
|
# @return {@code true} if the point lies within the bound of
|
144
144
|
# the arc, {@code false} if the point lies outside of the
|
145
145
|
# arc's bounds.
|
146
|
-
def contain?(x_or_point, y = nil)
|
146
|
+
def contain?(x_or_point, y = nil, outline: false, distance_tolerance: 0)
|
147
147
|
x, y = normalize_point(x_or_point, y)
|
148
148
|
return unless x && y
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
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"
|
149
|
+
if outline
|
150
|
+
if type == :pie && x == center_x && y == center_y
|
151
|
+
true
|
152
|
+
else
|
153
|
+
distance_tolerance = BigDecimal(distance_tolerance.to_s)
|
154
|
+
outside_inside_radius_difference = BigDecimal('0.001') + distance_tolerance * 2.0
|
155
|
+
outside_radius_difference = inside_radius_difference = outside_inside_radius_difference / 2.0
|
156
|
+
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)
|
157
|
+
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)
|
158
|
+
outside_shape.contain?(x, y, outline: false) and
|
159
|
+
!inside_shape.contain?(x, y, outline: false)
|
160
|
+
end
|
168
161
|
else
|
169
|
-
|
170
|
-
#
|
162
|
+
# Normalize the coordinates compared to the ellipse
|
163
|
+
# having a center at 0,0 and a radius of 0.5.
|
164
|
+
ellw = width
|
165
|
+
return false if (ellw <= 0.0)
|
166
|
+
normx = (x - self.x) / ellw - 0.5
|
167
|
+
ellh = height
|
168
|
+
return false if (ellh <= 0.0)
|
169
|
+
normy = (y - self.y) / ellh - 0.5
|
170
|
+
dist_sq = (normx * normx) + (normy * normy)
|
171
|
+
return false if (dist_sq >= 0.25)
|
172
|
+
ang_ext = self.extent.abs
|
173
|
+
return true if (ang_ext >= 360.0)
|
174
|
+
inarc = contain_angle?(-1*Math.radians_to_degrees(Math.atan2(normy, normx)))
|
175
|
+
|
176
|
+
return inarc if type == :pie
|
177
|
+
# CHORD and OPEN behave the same way
|
178
|
+
if inarc
|
179
|
+
return true if ang_ext >= 180.0
|
180
|
+
# point must be outside the "pie triangle"
|
181
|
+
else
|
182
|
+
return false if ang_ext <= 180.0
|
183
|
+
# point must be inside the "pie triangle"
|
184
|
+
end
|
185
|
+
|
186
|
+
# The point is inside the pie triangle iff it is on the same
|
187
|
+
# side of the line connecting the ends of the arc as the center.
|
188
|
+
angle = Math.degrees_to_radians(-start)
|
189
|
+
x1 = Math.cos(angle)
|
190
|
+
y1 = Math.sin(angle)
|
191
|
+
angle += Math.degrees_to_radians(-extent)
|
192
|
+
x2 = Math.cos(angle)
|
193
|
+
y2 = Math.sin(angle)
|
194
|
+
inside = (Line.relative_counterclockwise(x1, y1, x2, y2, 2*normx, 2*normy) *
|
195
|
+
Line.relative_counterclockwise(x1, y1, x2, y2, 0, 0) >= 0)
|
196
|
+
inarc ? !inside : inside
|
171
197
|
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
198
|
end
|
185
199
|
|
186
200
|
# 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
|
@@ -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
|
@@ -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
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
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
|
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
|
|
@@ -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
|
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,
|
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
|
-
|
219
|
-
point_segment_distance(x, y) <=
|
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)
|
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
|
@@ -39,8 +43,9 @@ module PerfectShape
|
|
39
43
|
alias closed? closed
|
40
44
|
|
41
45
|
# Constructs Path with winding rule, closed status, and shapes (must always start with PerfectShape::Point or Array of [x,y] coordinates)
|
42
|
-
# Shape class types can be any of SHAPE_TYPES: Array (x,y coordinates), PerfectShape::Point, or PerfectShape::
|
46
|
+
# Shape class types can be any of SHAPE_TYPES: Array (x,y coordinates), PerfectShape::Point, PerfectShape::Line, PerfectShape::QuadraticBezierCurve, or PerfectShape::CubicBezierCurve
|
43
47
|
# winding_rule can be any of WINDING_RULES: :wind_non_zero (default) or :wind_even_odd
|
48
|
+
# closed can be true or false
|
44
49
|
def initialize(shapes: [], closed: false, winding_rule: :wind_non_zero)
|
45
50
|
self.closed = closed
|
46
51
|
self.winding_rule = winding_rule
|
@@ -61,7 +66,10 @@ module PerfectShape
|
|
61
66
|
shape.points.each do |point|
|
62
67
|
the_points << point.to_a
|
63
68
|
end
|
64
|
-
|
69
|
+
when CubicBezierCurve
|
70
|
+
shape.points.each do |point|
|
71
|
+
the_points << point.to_a
|
72
|
+
end
|
65
73
|
end
|
66
74
|
end
|
67
75
|
the_points << @shapes.first.to_a if closed?
|
@@ -83,7 +91,8 @@ module PerfectShape
|
|
83
91
|
:line_to
|
84
92
|
when QuadraticBezierCurve
|
85
93
|
:quad_to
|
86
|
-
|
94
|
+
when CubicBezierCurve
|
95
|
+
:cubic_to
|
87
96
|
end
|
88
97
|
end
|
89
98
|
the_drawing_shapes << :close if closed?
|
@@ -102,8 +111,8 @@ module PerfectShape
|
|
102
111
|
# @param x The X coordinate of the point to test.
|
103
112
|
# @param y The Y coordinate of the point to test.
|
104
113
|
#
|
105
|
-
# @return
|
106
|
-
# the path
|
114
|
+
# @return true if the point lies within the bound of
|
115
|
+
# the path or false if the point lies outside of the
|
107
116
|
# path's bounds.
|
108
117
|
def contain?(x_or_point, y = nil)
|
109
118
|
x, y = normalize_point(x_or_point, y)
|
@@ -113,7 +122,7 @@ module PerfectShape
|
|
113
122
|
# Here we know that both x and y are finite.
|
114
123
|
return false if shapes.count < 2
|
115
124
|
mask = winding_rule == :wind_non_zero ? -1 : 1
|
116
|
-
(point_crossings(x, y)
|
125
|
+
(point_crossings(x, y) & mask) != 0
|
117
126
|
else
|
118
127
|
# Either x or y was infinite or NaN.
|
119
128
|
# A NaN always produces a negative response to any test
|
@@ -137,67 +146,71 @@ module PerfectShape
|
|
137
146
|
def point_crossings(x_or_point, y = nil)
|
138
147
|
x, y = normalize_point(x_or_point, y)
|
139
148
|
return unless x && y
|
140
|
-
return
|
149
|
+
return 0 if shapes.count == 0
|
141
150
|
movx = movy = curx = cury = endx = endy = 0
|
142
151
|
coords = points.flatten
|
143
152
|
curx = movx = coords[0]
|
144
153
|
cury = movy = coords[1]
|
145
|
-
crossings =
|
146
|
-
ci =
|
154
|
+
crossings = 0
|
155
|
+
ci = 2
|
147
156
|
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]])
|
157
|
+
case drawing_types[i]
|
158
|
+
when :move_to
|
159
|
+
if cury != movy
|
160
|
+
line = PerfectShape::Line.new(points: [[curx, cury], [movx, movy]])
|
164
161
|
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
162
|
end
|
163
|
+
movx = curx = coords[ci]
|
164
|
+
ci += 1
|
165
|
+
movy = cury = coords[ci]
|
166
|
+
ci += 1
|
167
|
+
when :line_to
|
168
|
+
endx = coords[ci]
|
169
|
+
ci += 1
|
170
|
+
endy = coords[ci]
|
171
|
+
ci += 1
|
172
|
+
line = PerfectShape::Line.new(points: [[curx, cury], [endx, endy]])
|
173
|
+
crossings += line.point_crossings(x, y)
|
174
|
+
curx = endx;
|
175
|
+
cury = endy;
|
176
|
+
when :quad_to
|
177
|
+
quad_ctrlx = coords[ci]
|
178
|
+
ci += 1
|
179
|
+
quad_ctrly = coords[ci]
|
180
|
+
ci += 1
|
181
|
+
endx = coords[ci]
|
182
|
+
ci += 1
|
183
|
+
endy = coords[ci]
|
184
|
+
ci += 1
|
185
|
+
quad = PerfectShape::QuadraticBezierCurve.new(points: [[curx, cury], [quad_ctrlx, quad_ctrly], [endx, endy]])
|
186
|
+
crossings += quad.point_crossings(x, y)
|
187
|
+
curx = endx;
|
188
|
+
cury = endy;
|
189
|
+
when :cubic_to
|
190
|
+
cubic_ctrl1x = coords[ci]
|
191
|
+
ci += 1
|
192
|
+
cubic_ctrl1y = coords[ci]
|
193
|
+
ci += 1
|
194
|
+
cubic_ctrl2x = coords[ci]
|
195
|
+
ci += 1
|
196
|
+
cubic_ctrl2y = coords[ci]
|
197
|
+
ci += 1
|
198
|
+
endx = coords[ci]
|
199
|
+
ci += 1
|
200
|
+
endy = coords[ci]
|
201
|
+
ci += 1
|
202
|
+
cubic = PerfectShape::CubicBezierCurve.new(points: [[curx, cury], [cubic_ctrl1x, cubic_ctrl1y], [cubic_ctrl2x, cubic_ctrl2y], [endx, endy]])
|
203
|
+
crossings += cubic.point_crossings(x, y)
|
204
|
+
curx = endx;
|
205
|
+
cury = endy;
|
206
|
+
when :close
|
207
|
+
if cury != movy
|
208
|
+
line = PerfectShape::Line.new(points: [[curx, cury], [movx, movy]])
|
209
|
+
crossings += line.point_crossings(x, y)
|
210
|
+
end
|
211
|
+
curx = movx
|
212
|
+
cury = movy
|
213
|
+
end
|
201
214
|
end
|
202
215
|
if cury != movy
|
203
216
|
line = PerfectShape::Line.new(points: [[curx, cury], [movx, movy]])
|
data/lib/perfect_shape/point.rb
CHANGED
@@ -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
|
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,
|
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
|
-
|
74
|
-
point_distance(x, y) <=
|
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)
|