perfect-shape 0.2.0 → 0.3.3

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.
@@ -1,4 +1,4 @@
1
- # Copyright (c) 2021 Andy Maleh
1
+ # Copyright (c) 2021-2022 Andy Maleh
2
2
  #
3
3
  # Permission is hereby granted, free of charge, to any person obtaining
4
4
  # a copy of this software and associated documentation files (the
@@ -1,4 +1,4 @@
1
- # Copyright (c) 2021 Andy Maleh
1
+ # Copyright (c) 2021-2022 Andy Maleh
2
2
  #
3
3
  # Permission is hereby granted, free of charge, to any person obtaining
4
4
  # a copy of this software and associated documentation files (the
@@ -1,4 +1,4 @@
1
- # Copyright (c) 2021 Andy Maleh
1
+ # Copyright (c) 2021-2022 Andy Maleh
2
2
  #
3
3
  # Permission is hereby granted, free of charge, to any person obtaining
4
4
  # a copy of this software and associated documentation files (the
@@ -75,6 +75,8 @@ module PerfectShape
75
75
  include MultiPoint
76
76
  include Equalizer.new(:points)
77
77
 
78
+ OUTLINE_MINIMUM_DISTANCE_THRESHOLD = BigDecimal('0.001')
79
+
78
80
  # Checks if cubic bézier curve contains point (two-number Array or x, y args)
79
81
  #
80
82
  # @param x The X coordinate of the point to test.
@@ -83,24 +85,29 @@ module PerfectShape
83
85
  # @return {@code true} if the point lies within the bound of
84
86
  # the cubic bézier curve, {@code false} if the point lies outside of the
85
87
  # cubic bézier curve's bounds.
86
- def contain?(x_or_point, y = nil)
88
+ def contain?(x_or_point, y = nil, outline: false, distance_tolerance: 0)
87
89
  x, y = normalize_point(x_or_point, y)
88
90
  return unless x && y
89
91
 
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
92
+ if outline
93
+ minimum_distance_threshold = OUTLINE_MINIMUM_DISTANCE_THRESHOLD + distance_tolerance
94
+ point_segment_distance(x, y, minimum_distance_threshold: minimum_distance_threshold) < minimum_distance_threshold
95
+ else
96
+ # Either x or y was infinite or NaN.
97
+ # A NaN always produces a negative response to any test
98
+ # and Infinity values cannot be "inside" any path so
99
+ # they should return false as well.
100
+ return false if (!(x * 0.0 + y * 0.0 == 0.0))
101
+ # We count the "Y" crossings to determine if the point is
102
+ # inside the curve bounded by its closing line.
103
+ x1 = points[0][0]
104
+ y1 = points[0][1]
105
+ x2 = points[3][0]
106
+ y2 = points[3][1]
107
+ line = PerfectShape::Line.new(points: [[x1, y1], [x2, y2]])
108
+ crossings = line.point_crossings(x, y) + point_crossings(x, y);
109
+ (crossings & 1) == 1
110
+ end
104
111
  end
105
112
 
106
113
  # Calculates the number of times the cubic bézier curve
@@ -116,5 +123,88 @@ module PerfectShape
116
123
  return unless x && y
117
124
  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
125
  end
126
+
127
+ # The center point on the outline of the curve
128
+ def curve_center_point
129
+ subdivisions.last.points[0]
130
+ end
131
+
132
+ # The center point x on the outline of the curve
133
+ def curve_center_x
134
+ subdivisions.last.points[0][0]
135
+ end
136
+
137
+ # The center point y on the outline of the curve
138
+ def curve_center_y
139
+ subdivisions.last.points[0][1]
140
+ end
141
+
142
+ # Subdivides CubicBezierCurve exactly at its curve center
143
+ # returning 2 CubicBezierCurve's as a two-element Array by default
144
+ #
145
+ # Optional `level` parameter specifies the level of recursions to
146
+ # perform to get more subdivisions. The number of resulting
147
+ # subdivisions is 2 to the power of `level` (e.g. 2 subdivisions
148
+ # for level=1, 4 subdivisions for level=2, and 8 subdivisions for level=3)
149
+ def subdivisions(level = 1)
150
+ level -= 1 # consume 1 level
151
+ x1 = points[0][0]
152
+ y1 = points[0][1]
153
+ ctrlx1 = points[1][0]
154
+ ctrly1 = points[1][1]
155
+ ctrlx2 = points[2][0]
156
+ ctrly2 = points[2][1]
157
+ x2 = points[3][0]
158
+ y2 = points[3][1]
159
+ centerx = (ctrlx1 + ctrlx2) / 2.0
160
+ centery = (ctrly1 + ctrly2) / 2.0
161
+ ctrlx1 = (x1 + ctrlx1) / 2.0
162
+ ctrly1 = (y1 + ctrly1) / 2.0
163
+ ctrlx2 = (x2 + ctrlx2) / 2.0
164
+ ctrly2 = (y2 + ctrly2) / 2.0
165
+ ctrlx12 = (ctrlx1 + centerx) / 2.0
166
+ ctrly12 = (ctrly1 + centery) / 2.0
167
+ ctrlx21 = (ctrlx2 + centerx) / 2.0
168
+ ctrly21 = (ctrly2 + centery) / 2.0
169
+ centerx = (ctrlx12 + ctrlx21) / 2.0
170
+ centery = (ctrly12 + ctrly21) / 2.0
171
+ default_subdivisions = [
172
+ CubicBezierCurve.new(points: [x1, y1, ctrlx1, ctrly1, ctrlx12, ctrly12, centerx, centery]),
173
+ CubicBezierCurve.new(points: [centerx, centery, ctrlx21, ctrly21, ctrlx2, ctrly2, x2, y2])
174
+ ]
175
+ if level == 0
176
+ default_subdivisions
177
+ else
178
+ default_subdivisions.map { |curve| curve.subdivisions(level) }.flatten
179
+ end
180
+ end
181
+
182
+ def point_segment_distance(x_or_point, y = nil, minimum_distance_threshold: OUTLINE_MINIMUM_DISTANCE_THRESHOLD)
183
+ x, y = normalize_point(x_or_point, y)
184
+ return unless x && y
185
+
186
+ point = Point.new(x, y)
187
+ current_curve = self
188
+ minimum_distance = point.point_distance(curve_center_point)
189
+ last_minimum_distance = minimum_distance + 1 # start bigger to ensure going through loop once at least
190
+ while minimum_distance >= minimum_distance_threshold && minimum_distance < last_minimum_distance
191
+ curve1, curve2 = current_curve.subdivisions
192
+ distance1 = point.point_distance(curve1.curve_center_point)
193
+ distance2 = point.point_distance(curve2.curve_center_point)
194
+ last_minimum_distance = minimum_distance
195
+ if distance1 < distance2
196
+ minimum_distance = distance1
197
+ current_curve = curve1
198
+ else
199
+ minimum_distance = distance2
200
+ current_curve = curve2
201
+ end
202
+ end
203
+ if minimum_distance < minimum_distance_threshold
204
+ minimum_distance
205
+ else
206
+ last_minimum_distance
207
+ end
208
+ end
119
209
  end
120
210
  end
@@ -1,4 +1,4 @@
1
- # Copyright (c) 2021 Andy Maleh
1
+ # Copyright (c) 2021-2022 Andy Maleh
2
2
  #
3
3
  # Permission is hereby granted, free of charge, to any person obtaining
4
4
  # a copy of this software and associated documentation files (the
@@ -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
@@ -1,4 +1,4 @@
1
- # Copyright (c) 2021 Andy Maleh
1
+ # Copyright (c) 2021-2022 Andy Maleh
2
2
  #
3
3
  # Permission is hereby granted, free of charge, to any person obtaining
4
4
  # a copy of this software and associated documentation files (the
@@ -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)
@@ -1,3 +1,24 @@
1
+ # Copyright (c) 2021-2022 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
+
1
22
  module PerfectShape
2
23
  # Perfect Shape Math utility methods
3
24
  #
@@ -1,4 +1,4 @@
1
- # Copyright (c) 2021 Andy Maleh
1
+ # Copyright (c) 2021-2022 Andy Maleh
2
2
  #
3
3
  # Permission is hereby granted, free of charge, to any person obtaining
4
4
  # a copy of this software and associated documentation files (the
@@ -1,4 +1,4 @@
1
- # Copyright (c) 2021 Andy Maleh
1
+ # Copyright (c) 2021-2022 Andy Maleh
2
2
  #
3
3
  # Permission is hereby granted, free of charge, to any person obtaining
4
4
  # a copy of this software and associated documentation files (the
@@ -1,4 +1,4 @@
1
- # Copyright (c) 2021 Andy Maleh
1
+ # Copyright (c) 2021-2022 Andy Maleh
2
2
  #
3
3
  # Permission is hereby granted, free of charge, to any person obtaining
4
4
  # a copy of this software and associated documentation files (the
@@ -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)
@@ -1,4 +1,4 @@
1
- # Copyright (c) 2021 Andy Maleh
1
+ # Copyright (c) 2021-2022 Andy Maleh
2
2
  #
3
3
  # Permission is hereby granted, free of charge, to any person obtaining
4
4
  # a copy of this software and associated documentation files (the
@@ -1,4 +1,4 @@
1
- # Copyright (c) 2021 Andy Maleh
1
+ # Copyright (c) 2021-2022 Andy Maleh
2
2
  #
3
3
  # Permission is hereby granted, free of charge, to any person obtaining
4
4
  # a copy of this software and associated documentation files (the
@@ -37,79 +37,89 @@ 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
77
- end
78
-
79
- if cury < lasty
80
- if y < cury || y >= lasty
43
+ if outline
44
+ edges.any? { |edge| edge.contain?(x, y, distance_tolerance: distance_tolerance) }
45
+ else
46
+ npoints = points.count
47
+ xpoints = points.map(&:first)
48
+ ypoints = points.map(&:last)
49
+ return false if npoints <= 2 || !bounding_box.contain?(x, y)
50
+ hits = 0
51
+
52
+ lastx = xpoints[npoints - 1]
53
+ lasty = ypoints[npoints - 1]
54
+
55
+ # Walk the edges of the polygon
56
+ npoints.times do |i|
57
+ curx = xpoints[i]
58
+ cury = ypoints[i]
59
+
60
+ if cury == lasty
81
61
  lastx = curx
82
62
  lasty = cury
83
63
  next
84
64
  end
85
- if x < leftx
86
- hits += 1
87
- lastx = curx
88
- lasty = cury
89
- next
65
+
66
+ if curx < lastx
67
+ if x >= lastx
68
+ lastx = curx
69
+ lasty = cury
70
+ next
71
+ end
72
+ leftx = curx
73
+ else
74
+ if x >= curx
75
+ lastx = curx
76
+ lasty = cury
77
+ next
78
+ end
79
+ leftx = lastx
90
80
  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
81
+
82
+ if cury < lasty
83
+ if y < cury || y >= lasty
84
+ lastx = curx
85
+ lasty = cury
86
+ next
87
+ end
88
+ if x < leftx
89
+ hits += 1
90
+ lastx = curx
91
+ lasty = cury
92
+ next
93
+ end
94
+ test1 = x - curx
95
+ test2 = y - cury
96
+ else
97
+ if y < lasty || y >= cury
98
+ lastx = curx
99
+ lasty = cury
100
+ next
101
+ end
102
+ if x < leftx
103
+ hits += 1
104
+ lastx = curx
105
+ lasty = cury
106
+ next
107
+ end
108
+ test1 = x - lastx
109
+ test2 = y - lasty
98
110
  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
111
+
112
+ hits += 1 if (test1 < (test2 / (lasty - cury) * (lastx - curx)))
107
113
  end
108
-
109
- hits += 1 if (test1 < (test2 / (lasty - cury) * (lastx - curx)))
114
+
115
+ (hits & 1) != 0
116
+ end
117
+ end
118
+
119
+ def edges
120
+ points.zip(points.rotate(1)).map do |point1, point2|
121
+ Line.new(points: [[point1.first, point1.last], [point2.first, point2.last]])
110
122
  end
111
-
112
- (hits & 1) != 0
113
123
  end
114
124
  end
115
125
  end