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.
@@ -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)