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.
@@ -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
- # Normalize the coordinates compared to the ellipse
150
- # having a center at 0,0 and a radius of 0.5.
151
- ellw = width
152
- return false if (ellw <= 0.0)
153
- normx = (x - self.x) / ellw - 0.5
154
- ellh = height
155
- return false if (ellh <= 0.0)
156
- normy = (y - self.y) / ellh - 0.5
157
- dist_sq = (normx * normx) + (normy * normy)
158
- return false if (dist_sq >= 0.25)
159
- ang_ext = self.extent.abs
160
- return true if (ang_ext >= 360.0)
161
- inarc = contain_angle?(-1*Math.radians_to_degrees(Math.atan2(normy, normx)))
162
-
163
- return inarc if type == :pie
164
- # CHORD and OPEN behave the same way
165
- if inarc
166
- return true if ang_ext >= 180.0
167
- # point must be outside the "pie triangle"
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
- return false if ang_ext <= 180.0
170
- # point must be inside the "pie triangle"
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
- ellw = self.width
71
- return false if ellw <= 0.0
72
- normx = (x - self.x) / ellw - 0.5
73
- ellh = self.height
74
- return false if ellh <= 0.0
75
- normy = (y - self.y) / ellh - 0.5
76
- (normx * normx + normy * normy) < 0.25
70
+ if outline
71
+ super(x, y, outline: true, distance_tolerance: distance_tolerance)
72
+ else
73
+ ellw = self.width
74
+ return false if ellw <= 0.0
75
+ normx = (x - self.x) / ellw - 0.5
76
+ ellh = self.height
77
+ return false if ellh <= 0.0
78
+ normy = (y - self.y) / ellh - 0.5
79
+ (normx * normx + normy * normy) < 0.25
80
+ end
77
81
  end
78
82
  end
79
83
  end
@@ -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 BigDecimal('0') if (py < y1 && py < y2)
193
- return BigDecimal('0') if (py >= y1 && py >= y2)
192
+ return 0 if (py < y1 && py < y2)
193
+ return 0 if (py >= y1 && py >= y2)
194
194
  # assert(y1 != y2);
195
- return BigDecimal('0') if (px >= x1 && px >= x2)
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 BigDecimal('0') if (px >= xintercept)
199
- (y1 < y2) ? BigDecimal('1') : BigDecimal('-1')
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 distance The distance from line to tolerate (0 by default)
210
+ # @param distance_tolerance The distance from line to tolerate (0 by default)
211
211
  #
212
212
  # @return {@code true} if the point lies within the bound of
213
213
  # the line, {@code false} if the point lies outside of the
214
214
  # line's bounds.
215
- def contain?(x_or_point, y = nil, distance: 0)
215
+ def contain?(x_or_point, y = nil, outline: false, distance_tolerance: 0)
216
216
  x, y = normalize_point(x_or_point, y)
217
217
  return unless x && y
218
- distance = BigDecimal(distance.to_s)
219
- point_segment_distance(x, y) <= distance
218
+ distance_tolerance = BigDecimal(distance_tolerance.to_s)
219
+ point_segment_distance(x, y) <= distance_tolerance
220
220
  end
221
221
 
222
222
  def point_segment_distance(x_or_point, y = nil)
@@ -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
- SHAPE_TYPES = [Array, Point, Line]
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::Line
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
- # when CubicBezierCurve # TODO
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
- # when CubicBezierCurve # TODO
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 {@code true} if the point lies within the bound of
106
- # the path, {@code false} if the point lies outside of the
114
+ # @return true if the point lies within the bound of
115
+ # the path or false if the point lies outside of the
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).to_i & mask) != 0
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 BigDecimal('0') if shapes.count == 0
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 = BigDecimal('0')
146
- ci = BigDecimal('2')
154
+ crossings = 0
155
+ ci = 2
147
156
  1.upto(shapes.count - 1).each do |i|
148
- case drawing_types[i]
149
- when :move_to
150
- if cury != movy
151
- line = PerfectShape::Line.new(points: [[curx, cury], [movx, movy]])
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]])
@@ -63,15 +63,15 @@ module PerfectShape
63
63
  #
64
64
  # @param x The X coordinate of the point to test.
65
65
  # @param y The Y coordinate of the point to test.
66
- # @param distance The distance from point to tolerate (0 by default)
66
+ # @param distance_tolerance The distance from point to tolerate (0 by default)
67
67
  #
68
68
  # @return {@code true} if the point is close enough within distance tolerance,
69
69
  # {@code false} if the point is too far.
70
- def contain?(x_or_point, y = nil, distance: 0)
70
+ def contain?(x_or_point, y = nil, outline: false, distance_tolerance: 0)
71
71
  x, y = normalize_point(x_or_point, y)
72
72
  return unless x && y
73
- distance = BigDecimal(distance.to_s)
74
- point_distance(x, y) <= distance
73
+ distance_tolerance = BigDecimal(distance_tolerance.to_s)
74
+ point_distance(x, y) <= distance_tolerance
75
75
  end
76
76
 
77
77
  def point_distance(x_or_point, y = nil)