perfect-shape 0.1.1 → 0.3.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 +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)
|