perfect-shape 0.1.0 → 0.3.0

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.
@@ -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
@@ -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)
@@ -26,8 +26,8 @@ module PerfectShape
26
26
  module MultiPoint
27
27
  attr_reader :points
28
28
 
29
- def initialize(points: nil)
30
- self.points = points || []
29
+ def initialize(points: [])
30
+ self.points = points
31
31
  end
32
32
 
33
33
  # Sets points, normalizing to an Array of Arrays of (x,y) pairs as BigDecimal
@@ -22,6 +22,8 @@
22
22
  require 'perfect_shape/shape'
23
23
  require 'perfect_shape/point'
24
24
  require 'perfect_shape/line'
25
+ require 'perfect_shape/quadratic_bezier_curve'
26
+ require 'perfect_shape/cubic_bezier_curve'
25
27
  require 'perfect_shape/multi_point'
26
28
 
27
29
  module PerfectShape
@@ -30,7 +32,10 @@ module PerfectShape
30
32
  include MultiPoint
31
33
  include Equalizer.new(:shapes, :closed, :winding_rule)
32
34
 
33
- 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
34
39
  WINDING_RULES = [:wind_non_zero, :wind_even_odd]
35
40
 
36
41
  attr_reader :winding_rule
@@ -38,8 +43,9 @@ module PerfectShape
38
43
  alias closed? closed
39
44
 
40
45
  # Constructs Path with winding rule, closed status, and shapes (must always start with PerfectShape::Point or Array of [x,y] coordinates)
41
- # 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
42
47
  # winding_rule can be any of WINDING_RULES: :wind_non_zero (default) or :wind_even_odd
48
+ # closed can be true or false
43
49
  def initialize(shapes: [], closed: false, winding_rule: :wind_non_zero)
44
50
  self.closed = closed
45
51
  self.winding_rule = winding_rule
@@ -47,20 +53,27 @@ module PerfectShape
47
53
  end
48
54
 
49
55
  def points
50
- @shapes.map do |shape|
56
+ the_points = []
57
+ @shapes.each do |shape|
51
58
  case shape
52
59
  when Point
53
- shape.to_a
60
+ the_points << shape.to_a
54
61
  when Array
55
- shape
62
+ the_points << shape.map {|n| BigDecimal(n.to_s)}
56
63
  when Line
57
- shape.points.last.to_a
58
- # when QuadraticBezierCurve # TODO
59
- # when CubicBezierCurve # TODO
64
+ the_points << shape.points.last.to_a
65
+ when QuadraticBezierCurve
66
+ shape.points.each do |point|
67
+ the_points << point.to_a
68
+ end
69
+ when CubicBezierCurve
70
+ shape.points.each do |point|
71
+ the_points << point.to_a
72
+ end
60
73
  end
61
- end.tap do |the_points|
62
- the_points << @shapes.first.to_a if closed?
63
74
  end
75
+ the_points << @shapes.first.to_a if closed?
76
+ the_points
64
77
  end
65
78
 
66
79
  def points=(some_points)
@@ -76,8 +89,10 @@ module PerfectShape
76
89
  :move_to
77
90
  when Line
78
91
  :line_to
79
- # when QuadraticBezierCurve # TODO
80
- # when CubicBezierCurve # TODO
92
+ when QuadraticBezierCurve
93
+ :quad_to
94
+ when CubicBezierCurve
95
+ :cubic_to
81
96
  end
82
97
  end
83
98
  the_drawing_shapes << :close if closed?
@@ -96,8 +111,8 @@ module PerfectShape
96
111
  # @param x The X coordinate of the point to test.
97
112
  # @param y The Y coordinate of the point to test.
98
113
  #
99
- # @return {@code true} if the point lies within the bound of
100
- # 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
101
116
  # path's bounds.
102
117
  def contain?(x_or_point, y = nil)
103
118
  x, y = normalize_point(x_or_point, y)
@@ -139,57 +154,63 @@ module PerfectShape
139
154
  crossings = 0
140
155
  ci = 2
141
156
  1.upto(shapes.count - 1).each do |i|
142
- case drawing_types[i]
143
- when :move_to
144
- if cury != movy
145
- line = PerfectShape::Line.new(points: [[curx, cury], [movx, movy]])
146
- crossings += line.point_crossings(x, y)
147
- end
148
- movx = curx = coords[ci]
149
- ci += 1
150
- movy = cury = coords[ci]
151
- ci += 1
152
- when :line_to
153
- endx = coords[ci]
154
- ci += 1
155
- endy = coords[ci]
156
- ci += 1
157
- 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]])
158
161
  crossings += line.point_crossings(x, y)
159
- curx = endx;
160
- cury = endy;
161
- # when :quad_to # TODO
162
- # crossings +=
163
- # Curve.point_crossings_for_quad(x, y,
164
- # curx, cury,
165
- # coords[ci++],
166
- # coords[ci++],
167
- # endx = coords[ci++],
168
- # endy = coords[ci++],
169
- # 0);
170
- # curx = endx;
171
- # cury = endy;
172
- # when :cubic_to # TODO
173
- # crossings +=
174
- # Curve.point_crossings_for_cubic(x, y,
175
- # curx, cury,
176
- # coords[ci++],
177
- # coords[ci++],
178
- # coords[ci++],
179
- # coords[ci++],
180
- # endx = coords[ci++],
181
- # endy = coords[ci++],
182
- # 0);
183
- # curx = endx;
184
- # cury = endy;
185
- when :close
186
- if cury != movy
187
- line = PerfectShape::Line.new(points: [[curx, cury], [movx, movy]])
188
- crossings += line.point_crossings(x, y)
189
- end
190
- curx = movx
191
- cury = movy
192
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
193
214
  end
194
215
  if cury != movy
195
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)
@@ -37,79 +37,85 @@ module PerfectShape
37
37
  # @return {@code true} if the point lies within the bound of
38
38
  # the polygon, {@code false} if the point lies outside of the
39
39
  # polygon's bounds.
40
- def contain?(x_or_point, y = nil)
40
+ def contain?(x_or_point, y = nil, outline: false, distance_tolerance: 0)
41
41
  x, y = normalize_point(x_or_point, y)
42
42
  return unless x && y
43
- npoints = points.count
44
- xpoints = points.map(&:first)
45
- ypoints = points.map(&:last)
46
- return false if npoints <= 2 || !bounding_box.contain?(x, y)
47
- hits = 0
48
-
49
- lastx = xpoints[npoints - 1]
50
- lasty = ypoints[npoints - 1]
51
-
52
- # Walk the edges of the polygon
53
- npoints.times do |i|
54
- curx = xpoints[i]
55
- cury = ypoints[i]
56
-
57
- if cury == lasty
58
- lastx = curx
59
- lasty = cury
60
- next
61
- end
62
-
63
- if curx < lastx
64
- if x >= lastx
65
- lastx = curx
66
- lasty = cury
67
- next
68
- end
69
- leftx = curx
70
- else
71
- if x >= curx
72
- lastx = curx
73
- lasty = cury
74
- next
75
- end
76
- leftx = lastx
43
+ if outline
44
+ points.zip(points.rotate(1)).any? do |point1, point2|
45
+ Line.new(points: [[point1.first, point1.last], [point2.first, point2.last]]).contain?(x, y, distance_tolerance: distance_tolerance)
77
46
  end
78
-
79
- if cury < lasty
80
- if y < cury || y >= lasty
47
+ else
48
+ npoints = points.count
49
+ xpoints = points.map(&:first)
50
+ ypoints = points.map(&:last)
51
+ return false if npoints <= 2 || !bounding_box.contain?(x, y)
52
+ hits = 0
53
+
54
+ lastx = xpoints[npoints - 1]
55
+ lasty = ypoints[npoints - 1]
56
+
57
+ # Walk the edges of the polygon
58
+ npoints.times do |i|
59
+ curx = xpoints[i]
60
+ cury = ypoints[i]
61
+
62
+ if cury == lasty
81
63
  lastx = curx
82
64
  lasty = cury
83
65
  next
84
66
  end
85
- if x < leftx
86
- hits += 1
87
- lastx = curx
88
- lasty = cury
89
- next
67
+
68
+ if curx < lastx
69
+ if x >= lastx
70
+ lastx = curx
71
+ lasty = cury
72
+ next
73
+ end
74
+ leftx = curx
75
+ else
76
+ if x >= curx
77
+ lastx = curx
78
+ lasty = cury
79
+ next
80
+ end
81
+ leftx = lastx
90
82
  end
91
- test1 = x - curx
92
- test2 = y - cury
93
- else
94
- if y < lasty || y >= cury
95
- lastx = curx
96
- lasty = cury
97
- next
83
+
84
+ if cury < lasty
85
+ if y < cury || y >= lasty
86
+ lastx = curx
87
+ lasty = cury
88
+ next
89
+ end
90
+ if x < leftx
91
+ hits += 1
92
+ lastx = curx
93
+ lasty = cury
94
+ next
95
+ end
96
+ test1 = x - curx
97
+ test2 = y - cury
98
+ else
99
+ if y < lasty || y >= cury
100
+ lastx = curx
101
+ lasty = cury
102
+ next
103
+ end
104
+ if x < leftx
105
+ hits += 1
106
+ lastx = curx
107
+ lasty = cury
108
+ next
109
+ end
110
+ test1 = x - lastx
111
+ test2 = y - lasty
98
112
  end
99
- if x < leftx
100
- hits += 1
101
- lastx = curx
102
- lasty = cury
103
- next
104
- end
105
- test1 = x - lastx
106
- test2 = y - lasty
113
+
114
+ hits += 1 if (test1 < (test2 / (lasty - cury) * (lastx - curx)))
107
115
  end
108
-
109
- hits += 1 if (test1 < (test2 / (lasty - cury) * (lastx - curx)))
116
+
117
+ (hits & 1) != 0
110
118
  end
111
-
112
- (hits & 1) != 0
113
119
  end
114
120
  end
115
121
  end