perfect-shape 0.3.2 → 0.4.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.
@@ -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
@@ -27,7 +27,6 @@ require 'perfect_shape/cubic_bezier_curve'
27
27
  require 'perfect_shape/multi_point'
28
28
 
29
29
  module PerfectShape
30
- # Mostly ported from java.awt.geom: https://docs.oracle.com/javase/8/docs/api/java/awt/geom/Path2D.html
31
30
  class Path < Shape
32
31
  include MultiPoint
33
32
  include Equalizer.new(:shapes, :closed, :winding_rule)
@@ -114,21 +113,26 @@ module PerfectShape
114
113
  # @return true if the point lies within the bound of
115
114
  # the path or false if the point lies outside of the
116
115
  # path's bounds.
117
- def contain?(x_or_point, y = nil)
118
- x, y = normalize_point(x_or_point, y)
116
+ def contain?(x_or_point, y = nil, outline: false, distance_tolerance: 0)
117
+ x, y = Point.normalize_point(x_or_point, y)
119
118
  return unless x && y
120
- if (x * 0.0 + y * 0.0) == 0.0
121
- # N * 0.0 is 0.0 only if N is finite.
122
- # Here we know that both x and y are finite.
123
- return false if shapes.count < 2
124
- mask = winding_rule == :wind_non_zero ? -1 : 1
125
- (point_crossings(x, y) & mask) != 0
119
+
120
+ if outline
121
+ disconnected_shapes.any? {|shape| shape.contain?(x, y, outline: true, distance_tolerance: distance_tolerance) }
126
122
  else
127
- # Either x or y was infinite or NaN.
128
- # A NaN always produces a negative response to any test
129
- # and Infinity values cannot be "inside" any path so
130
- # they should return false as well.
131
- false
123
+ if (x * 0.0 + y * 0.0) == 0.0
124
+ # N * 0.0 is 0.0 only if N is finite.
125
+ # Here we know that both x and y are finite.
126
+ return false if shapes.count < 2
127
+ mask = winding_rule == :wind_non_zero ? -1 : 1
128
+ (point_crossings(x, y) & mask) != 0
129
+ else
130
+ # Either x or y was infinite or NaN.
131
+ # A NaN always produces a negative response to any test
132
+ # and Infinity values cannot be "inside" any path so
133
+ # they should return false as well.
134
+ false
135
+ end
132
136
  end
133
137
  end
134
138
 
@@ -144,7 +148,7 @@ module PerfectShape
144
148
  # The caller must check for NaN values.
145
149
  # The caller may also reject infinite values as well.
146
150
  def point_crossings(x_or_point, y = nil)
147
- x, y = normalize_point(x_or_point, y)
151
+ x, y = Point.normalize_point(x_or_point, y)
148
152
  return unless x && y
149
153
  return 0 if shapes.count == 0
150
154
  movx = movy = curx = cury = endx = endy = 0
@@ -218,5 +222,50 @@ module PerfectShape
218
222
  end
219
223
  crossings
220
224
  end
225
+
226
+ # Disconnected shapes have their start point filled in
227
+ # so that each shape does not depend on the previous shape
228
+ # to determine its start point.
229
+ #
230
+ # Also, if a point is followed by a non-point shape, it is removed
231
+ # since it is augmented to the following shape as its start point.
232
+ #
233
+ # Lastly, if the path is closed, an extra shape is
234
+ # added to represent the line connecting the last point to the first
235
+ def disconnected_shapes
236
+ initial_point = start_point = @shapes.first.to_a.map {|n| BigDecimal(n.to_s)}
237
+ final_point = nil
238
+ the_disconnected_shapes = @shapes.drop(1).map do |shape|
239
+ case shape
240
+ when Point
241
+ disconnected_shape = Point.new(*shape.to_a)
242
+ start_point = shape.to_a
243
+ final_point = disconnected_shape.to_a
244
+ nil
245
+ when Array
246
+ disconnected_shape = Point.new(*shape.map {|n| BigDecimal(n.to_s)})
247
+ start_point = shape.map {|n| BigDecimal(n.to_s)}
248
+ final_point = disconnected_shape.to_a
249
+ nil
250
+ when Line
251
+ disconnected_shape = Line.new(points: [start_point.to_a, shape.points.last])
252
+ start_point = shape.points.last.to_a
253
+ final_point = disconnected_shape.points.last.to_a
254
+ disconnected_shape
255
+ when QuadraticBezierCurve
256
+ disconnected_shape = QuadraticBezierCurve.new(points: [start_point.to_a] + shape.points)
257
+ start_point = shape.points.last.to_a
258
+ final_point = disconnected_shape.points.last.to_a
259
+ disconnected_shape
260
+ when CubicBezierCurve
261
+ disconnected_shape = CubicBezierCurve.new(points: [start_point.to_a] + shape.points)
262
+ start_point = shape.points.last.to_a
263
+ final_point = disconnected_shape.points.last.to_a
264
+ disconnected_shape
265
+ end
266
+ end
267
+ the_disconnected_shapes << Line.new(points: [final_point, initial_point]) if closed?
268
+ the_disconnected_shapes.compact
269
+ end
221
270
  end
222
271
  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
@@ -27,12 +27,27 @@ module PerfectShape
27
27
  class Point < Shape
28
28
  class << self
29
29
  def point_distance(x, y, px, py)
30
- x = BigDecimal(x.to_s)
31
- y = BigDecimal(y.to_s)
32
- px = BigDecimal(px.to_s)
33
- py = BigDecimal(py.to_s)
30
+ x = x.is_a?(BigDecimal) ? x : BigDecimal(x.to_s)
31
+ y = y.is_a?(BigDecimal) ? y : BigDecimal(y.to_s)
32
+ px = px.is_a?(BigDecimal) ? px : BigDecimal(px.to_s)
33
+ py = py.is_a?(BigDecimal) ? py : BigDecimal(py.to_s)
34
34
  BigDecimal(Math.sqrt((px - x)**2 + (py - y)**2).to_s)
35
35
  end
36
+
37
+ # Normalizes point args whether two-number Array or x, y args returning
38
+ # normalized point array of two BigDecimal's
39
+ #
40
+ # @param x_or_point The point or X coordinate of the point to test.
41
+ # @param y The Y coordinate of the point to test.
42
+ #
43
+ # @return Array of x and y BigDecimal's representing point
44
+ def normalize_point(x_or_point, y = nil)
45
+ x = x_or_point
46
+ x, y = x if y.nil? && x_or_point.is_a?(Array) && x_or_point.size == 2
47
+ x = x.is_a?(BigDecimal) ? x : BigDecimal(x.to_s)
48
+ y = y.is_a?(BigDecimal) ? y : BigDecimal(y.to_s)
49
+ [x, y]
50
+ end
36
51
  end
37
52
 
38
53
  include PointLocation
@@ -67,16 +82,17 @@ module PerfectShape
67
82
  #
68
83
  # @return {@code true} if the point is close enough within distance tolerance,
69
84
  # {@code false} if the point is too far.
70
- def contain?(x_or_point, y = nil, outline: false, distance_tolerance: 0)
71
- x, y = normalize_point(x_or_point, y)
85
+ def contain?(x_or_point, y = nil, outline: true, distance_tolerance: 0)
86
+ x, y = Point.normalize_point(x_or_point, y)
72
87
  return unless x && y
73
88
  distance_tolerance = BigDecimal(distance_tolerance.to_s)
74
89
  point_distance(x, y) <= distance_tolerance
75
90
  end
76
91
 
77
92
  def point_distance(x_or_point, y = nil)
78
- x, y = normalize_point(x_or_point, y)
93
+ x, y = Point.normalize_point(x_or_point, y)
79
94
  return unless x && y
95
+
80
96
  Point.point_distance(self.x, self.y, x, y)
81
97
  end
82
98
 
@@ -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
@@ -20,10 +20,10 @@
20
20
  # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21
21
 
22
22
  require 'perfect_shape/shape'
23
+ require 'perfect_shape/point'
23
24
  require 'perfect_shape/multi_point'
24
25
 
25
26
  module PerfectShape
26
- # Mostly ported from java.awt.geom: https://docs.oracle.com/javase/8/docs/api/java/awt/Polygon.html
27
27
  class Polygon < Shape
28
28
  include MultiPoint
29
29
  include Equalizer.new(:points)
@@ -38,12 +38,10 @@ module PerfectShape
38
38
  # the polygon, {@code false} if the point lies outside of the
39
39
  # polygon's bounds.
40
40
  def contain?(x_or_point, y = nil, outline: false, distance_tolerance: 0)
41
- x, y = normalize_point(x_or_point, y)
41
+ x, y = Point.normalize_point(x_or_point, y)
42
42
  return unless x && y
43
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)
46
- end
44
+ edges.any? { |edge| edge.contain?(x, y, distance_tolerance: distance_tolerance) }
47
45
  else
48
46
  npoints = points.count
49
47
  xpoints = points.map(&:first)
@@ -117,5 +115,11 @@ module PerfectShape
117
115
  (hits & 1) != 0
118
116
  end
119
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]])
122
+ end
123
+ end
120
124
  end
121
125
  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
@@ -20,10 +20,10 @@
20
20
  # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21
21
 
22
22
  require 'perfect_shape/shape'
23
+ require 'perfect_shape/point'
23
24
  require 'perfect_shape/multi_point'
24
25
 
25
26
  module PerfectShape
26
- # Mostly ported from java.awt.geom: https://docs.oracle.com/javase/8/docs/api/java/awt/geom/QuadCurve2D.html
27
27
  class QuadraticBezierCurve < Shape
28
28
  class << self
29
29
  # Calculates the number of times the quadratic bézier curve from (x1,y1) to (x2,y2)
@@ -69,6 +69,8 @@ module PerfectShape
69
69
  include MultiPoint
70
70
  include Equalizer.new(:points)
71
71
 
72
+ OUTLINE_MINIMUM_DISTANCE_THRESHOLD = BigDecimal('0.001')
73
+
72
74
  # Checks if quadratic bézier curve contains point (two-number Array or x, y args)
73
75
  #
74
76
  # @param x The X coordinate of the point to test.
@@ -77,8 +79,8 @@ module PerfectShape
77
79
  # @return {@code true} if the point lies within the bound of
78
80
  # the quadratic bézier curve, {@code false} if the point lies outside of the
79
81
  # quadratic bézier curve's bounds.
80
- def contain?(x_or_point, y = nil)
81
- x, y = normalize_point(x_or_point, y)
82
+ def contain?(x_or_point, y = nil, outline: false, distance_tolerance: 0)
83
+ x, y = Point.normalize_point(x_or_point, y)
82
84
  return unless x && y
83
85
 
84
86
  x1 = points[0][0]
@@ -87,90 +89,96 @@ module PerfectShape
87
89
  yc = points[1][1]
88
90
  x2 = points[2][0]
89
91
  y2 = points[2][1]
90
-
91
- # We have a convex shape bounded by quad curve Pc(t)
92
- # and ine Pl(t).
93
- #
94
- # P1 = (x1, y1) - start point of curve
95
- # P2 = (x2, y2) - end point of curve
96
- # Pc = (xc, yc) - control point
97
- #
98
- # Pq(t) = P1*(1 - t)^2 + 2*Pc*t*(1 - t) + P2*t^2 =
99
- # = (P1 - 2*Pc + P2)*t^2 + 2*(Pc - P1)*t + P1
100
- # Pl(t) = P1*(1 - t) + P2*t
101
- # t = [0:1]
102
- #
103
- # P = (x, y) - point of interest
104
- #
105
- # Let's look at second derivative of quad curve equation:
106
- #
107
- # Pq''(t) = 2 * (P1 - 2 * Pc + P2) = Pq''
108
- # It's constant vector.
109
- #
110
- # Let's draw a line through P to be parallel to this
111
- # vector and find the intersection of the quad curve
112
- # and the line.
113
- #
114
- # Pq(t) is point of intersection if system of equations
115
- # below has the solution.
116
- #
117
- # L(s) = P + Pq''*s == Pq(t)
118
- # Pq''*s + (P - Pq(t)) == 0
119
- #
120
- # | xq''*s + (x - xq(t)) == 0
121
- # | yq''*s + (y - yq(t)) == 0
122
- #
123
- # This system has the solution if rank of its matrix equals to 1.
124
- # That is, determinant of the matrix should be zero.
125
- #
126
- # (y - yq(t))*xq'' == (x - xq(t))*yq''
127
- #
128
- # Let's solve this equation with 't' variable.
129
- # Also let kx = x1 - 2*xc + x2
130
- # ky = y1 - 2*yc + y2
131
- #
132
- # t0q = (1/2)*((x - x1)*ky - (y - y1)*kx) /
133
- # ((xc - x1)*ky - (yc - y1)*kx)
134
- #
135
- # Let's do the same for our line Pl(t):
136
- #
137
- # t0l = ((x - x1)*ky - (y - y1)*kx) /
138
- # ((x2 - x1)*ky - (y2 - y1)*kx)
139
- #
140
- # It's easy to check that t0q == t0l. This fact means
141
- # we can compute t0 only one time.
142
- #
143
- # In case t0 < 0 or t0 > 1, we have an intersections outside
144
- # of shape bounds. So, P is definitely out of shape.
145
- #
146
- # In case t0 is inside [0:1], we should calculate Pq(t0)
147
- # and Pl(t0). We have three points for now, and all of them
148
- # lie on one line. So, we just need to detect, is our point
149
- # of interest between points of intersections or not.
150
- #
151
- # If the denominator in the t0q and t0l equations is
152
- # zero, then the points must be collinear and so the
153
- # curve is degenerate and encloses no area. Thus the
154
- # result is false.
155
- kx = x1 - 2 * xc + x2;
156
- ky = y1 - 2 * yc + y2;
157
- dx = x - x1;
158
- dy = y - y1;
159
- dxl = x2 - x1;
160
- dyl = y2 - y1;
161
-
162
- t0 = (dx * ky - dy * kx) / (dxl * ky - dyl * kx)
163
- return false if (t0 < 0 || t0 > 1 || t0 != t0)
164
-
165
- xb = kx * t0 * t0 + 2 * (xc - x1) * t0 + x1;
166
- yb = ky * t0 * t0 + 2 * (yc - y1) * t0 + y1;
167
- xl = dxl * t0 + x1;
168
- yl = dyl * t0 + y1;
169
-
170
- (x >= xb && x < xl) ||
171
- (x >= xl && x < xb) ||
172
- (y >= yb && y < yl) ||
173
- (y >= yl && y < yb)
92
+
93
+ if outline
94
+ distance_tolerance = BigDecimal(distance_tolerance.to_s)
95
+ minimum_distance_threshold = OUTLINE_MINIMUM_DISTANCE_THRESHOLD + distance_tolerance
96
+ point_distance(x, y, minimum_distance_threshold: minimum_distance_threshold) < minimum_distance_threshold
97
+ else
98
+ # We have a convex shape bounded by quad curve Pc(t)
99
+ # and ine Pl(t).
100
+ #
101
+ # P1 = (x1, y1) - start point of curve
102
+ # P2 = (x2, y2) - end point of curve
103
+ # Pc = (xc, yc) - control point
104
+ #
105
+ # Pq(t) = P1*(1 - t)^2 + 2*Pc*t*(1 - t) + P2*t^2 =
106
+ # = (P1 - 2*Pc + P2)*t^2 + 2*(Pc - P1)*t + P1
107
+ # Pl(t) = P1*(1 - t) + P2*t
108
+ # t = [0:1]
109
+ #
110
+ # P = (x, y) - point of interest
111
+ #
112
+ # Let's look at second derivative of quad curve equation:
113
+ #
114
+ # Pq''(t) = 2 * (P1 - 2 * Pc + P2) = Pq''
115
+ # It's constant vector.
116
+ #
117
+ # Let's draw a line through P to be parallel to this
118
+ # vector and find the intersection of the quad curve
119
+ # and the line.
120
+ #
121
+ # Pq(t) is point of intersection if system of equations
122
+ # below has the solution.
123
+ #
124
+ # L(s) = P + Pq''*s == Pq(t)
125
+ # Pq''*s + (P - Pq(t)) == 0
126
+ #
127
+ # | xq''*s + (x - xq(t)) == 0
128
+ # | yq''*s + (y - yq(t)) == 0
129
+ #
130
+ # This system has the solution if rank of its matrix equals to 1.
131
+ # That is, determinant of the matrix should be zero.
132
+ #
133
+ # (y - yq(t))*xq'' == (x - xq(t))*yq''
134
+ #
135
+ # Let's solve this equation with 't' variable.
136
+ # Also let kx = x1 - 2*xc + x2
137
+ # ky = y1 - 2*yc + y2
138
+ #
139
+ # t0q = (1/2)*((x - x1)*ky - (y - y1)*kx) /
140
+ # ((xc - x1)*ky - (yc - y1)*kx)
141
+ #
142
+ # Let's do the same for our line Pl(t):
143
+ #
144
+ # t0l = ((x - x1)*ky - (y - y1)*kx) /
145
+ # ((x2 - x1)*ky - (y2 - y1)*kx)
146
+ #
147
+ # It's easy to check that t0q == t0l. This fact means
148
+ # we can compute t0 only one time.
149
+ #
150
+ # In case t0 < 0 or t0 > 1, we have an intersections outside
151
+ # of shape bounds. So, P is definitely out of shape.
152
+ #
153
+ # In case t0 is inside [0:1], we should calculate Pq(t0)
154
+ # and Pl(t0). We have three points for now, and all of them
155
+ # lie on one line. So, we just need to detect, is our point
156
+ # of interest between points of intersections or not.
157
+ #
158
+ # If the denominator in the t0q and t0l equations is
159
+ # zero, then the points must be collinear and so the
160
+ # curve is degenerate and encloses no area. Thus the
161
+ # result is false.
162
+ kx = x1 - 2 * xc + x2;
163
+ ky = y1 - 2 * yc + y2;
164
+ dx = x - x1;
165
+ dy = y - y1;
166
+ dxl = x2 - x1;
167
+ dyl = y2 - y1;
168
+
169
+ t0 = (dx * ky - dy * kx) / (dxl * ky - dyl * kx)
170
+ return false if (t0 < 0 || t0 > 1 || t0 != t0)
171
+
172
+ xb = kx * t0 * t0 + 2 * (xc - x1) * t0 + x1;
173
+ yb = ky * t0 * t0 + 2 * (yc - y1) * t0 + y1;
174
+ xl = dxl * t0 + x1;
175
+ yl = dyl * t0 + y1;
176
+
177
+ (x >= xb && x < xl) ||
178
+ (x >= xl && x < xb) ||
179
+ (y >= yb && y < yl) ||
180
+ (y >= yl && y < yb)
181
+ end
174
182
  end
175
183
 
176
184
  # Calculates the number of times the quad
@@ -182,9 +190,88 @@ module PerfectShape
182
190
  # +1 is added for each crossing where the Y coordinate is increasing
183
191
  # -1 is added for each crossing where the Y coordinate is decreasing
184
192
  def point_crossings(x_or_point, y = nil, level = 0)
185
- x, y = normalize_point(x_or_point, y)
193
+ x, y = Point.normalize_point(x_or_point, y)
186
194
  return unless x && y
187
195
  QuadraticBezierCurve.point_crossings(points[0][0], points[0][1], points[1][0], points[1][1], points[2][0], points[2][1], x, y, level)
188
196
  end
197
+
198
+ # The center point on the outline of the curve
199
+ # in Array format as pair of (x, y) coordinates
200
+ def curve_center_point
201
+ subdivisions.last.points[0]
202
+ end
203
+
204
+ # The center point x on the outline of the curve
205
+ def curve_center_x
206
+ subdivisions.last.points[0][0]
207
+ end
208
+
209
+ # The center point y on the outline of the curve
210
+ def curve_center_y
211
+ subdivisions.last.points[0][1]
212
+ end
213
+
214
+ # Subdivides QuadraticBezierCurve exactly at its curve center
215
+ # returning 2 QuadraticBezierCurve's as a two-element Array by default
216
+ #
217
+ # Optional `level` parameter specifies the level of recursions to
218
+ # perform to get more subdivisions. The number of resulting
219
+ # subdivisions is 2 to the power of `level` (e.g. 2 subdivisions
220
+ # for level=1, 4 subdivisions for level=2, and 8 subdivisions for level=3)
221
+ def subdivisions(level = 1)
222
+ level -= 1 # consume 1 level
223
+
224
+ x1 = points[0][0]
225
+ y1 = points[0][1]
226
+ ctrlx = points[1][0]
227
+ ctrly = points[1][1]
228
+ x2 = points[2][0]
229
+ y2 = points[2][1]
230
+ ctrlx1 = BigDecimal((x1 + ctrlx).to_s) / 2
231
+ ctrly1 = BigDecimal((y1 + ctrly).to_s) / 2
232
+ ctrlx2 = BigDecimal((x2 + ctrlx).to_s) / 2
233
+ ctrly2 = BigDecimal((y2 + ctrly).to_s) / 2
234
+ centerx = BigDecimal((ctrlx1 + ctrlx2).to_s) / 2
235
+ centery = BigDecimal((ctrly1 + ctrly2).to_s) / 2
236
+
237
+ default_subdivisions = [
238
+ QuadraticBezierCurve.new(points: [x1, y1, ctrlx1, ctrly1, centerx, centery]),
239
+ QuadraticBezierCurve.new(points: [centerx, centery, ctrlx2, ctrly2, x2, y2])
240
+ ]
241
+
242
+ if level == 0
243
+ default_subdivisions
244
+ else
245
+ default_subdivisions.map { |curve| curve.subdivisions(level) }.flatten
246
+ end
247
+ end
248
+
249
+ def point_distance(x_or_point, y = nil, minimum_distance_threshold: OUTLINE_MINIMUM_DISTANCE_THRESHOLD)
250
+ x, y = Point.normalize_point(x_or_point, y)
251
+ return unless x && y
252
+
253
+ point = Point.new(x, y)
254
+ current_curve = self
255
+ minimum_distance = point.point_distance(curve_center_point)
256
+ last_minimum_distance = minimum_distance + 1 # start bigger to ensure going through loop once at least
257
+ while minimum_distance >= minimum_distance_threshold && minimum_distance < last_minimum_distance
258
+ curve1, curve2 = current_curve.subdivisions
259
+ distance1 = point.point_distance(curve1.curve_center_point)
260
+ distance2 = point.point_distance(curve2.curve_center_point)
261
+ last_minimum_distance = minimum_distance
262
+ if distance1 < distance2
263
+ minimum_distance = distance1
264
+ current_curve = curve1
265
+ else
266
+ minimum_distance = distance2
267
+ current_curve = curve2
268
+ end
269
+ end
270
+ if minimum_distance < minimum_distance_threshold
271
+ minimum_distance
272
+ else
273
+ last_minimum_distance
274
+ end
275
+ end
189
276
  end
190
277
  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
@@ -21,10 +21,10 @@
21
21
 
22
22
  require 'perfect_shape/shape'
23
23
  require 'perfect_shape/rectangular_shape'
24
+ require 'perfect_shape/point'
24
25
  require 'perfect_shape/line'
25
26
 
26
27
  module PerfectShape
27
- # Mostly ported from java.awt.geom: https://docs.oracle.com/javase/8/docs/api/java/awt/geom/Rectangle2D.html
28
28
  class Rectangle < Shape
29
29
  include RectangularShape
30
30
  include Equalizer.new(:x, :y, :width, :height)
@@ -38,16 +38,23 @@ module PerfectShape
38
38
  # the rectangle, {@code false} if the point lies outside of the
39
39
  # rectangle's bounds.
40
40
  def contain?(x_or_point, y = nil, outline: false, distance_tolerance: 0)
41
- x, y = normalize_point(x_or_point, y)
41
+ x, y = Point.normalize_point(x_or_point, y)
42
42
  return unless x && y
43
+
43
44
  if outline
44
- Line.new(points: [[self.x, self.y], [self.x + width, self.y]]).contain?(x, y, distance_tolerance: distance_tolerance) or
45
- Line.new(points: [[self.x + width, self.y], [self.x + width, self.y + height]]).contain?(x, y, distance_tolerance: distance_tolerance) or
46
- Line.new(points: [[self.x + width, self.y + height], [self.x, self.y + height]]).contain?(x, y, distance_tolerance: distance_tolerance) or
47
- Line.new(points: [[self.x, self.y + height], [self.x, self.y]]).contain?(x, y, distance_tolerance: distance_tolerance)
45
+ edges.any? { |edge| edge.contain?(x, y, distance_tolerance: distance_tolerance) }
48
46
  else
49
47
  x.between?(self.x, self.x + width) && y.between?(self.y, self.y + height)
50
48
  end
51
49
  end
50
+
51
+ def edges
52
+ [
53
+ Line.new(points: [[self.x, self.y], [self.x + width, self.y]]),
54
+ Line.new(points: [[self.x + width, self.y], [self.x + width, self.y + height]]),
55
+ Line.new(points: [[self.x + width, self.y + height], [self.x, self.y + height]]),
56
+ Line.new(points: [[self.x, self.y + height], [self.x, self.y]])
57
+ ]
58
+ end
52
59
  end
53
60
  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
@@ -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
@@ -51,6 +51,12 @@ module PerfectShape
51
51
  max_y - min_y if max_y && min_y
52
52
  end
53
53
 
54
+ # Center point is `[center_x, center_y]`
55
+ # Returns `nil` if either center_x or center_y are `nil`
56
+ def center_point
57
+ [center_x, center_y] unless center_x.nil? || center_y.nil?
58
+ end
59
+
54
60
  # center_x is min_x + width/2.0 by default
55
61
  # Returns nil if min_x or width are nil
56
62
  def center_x
@@ -69,21 +75,6 @@ module PerfectShape
69
75
  Rectangle.new(x: min_x, y: min_y, width: width, height: height)
70
76
  end
71
77
 
72
- # Normalizes point args whether two-number Array or x, y args returning
73
- # normalized point array of two BigDecimal's
74
- #
75
- # @param x_or_point The point or X coordinate of the point to test.
76
- # @param y The Y coordinate of the point to test.
77
- #
78
- # @return Array of x and y BigDecimal's representing point
79
- def normalize_point(x_or_point, y = nil)
80
- x = x_or_point
81
- x, y = x if y.nil? && x_or_point.is_a?(Array) && x_or_point.size == 2
82
- x = BigDecimal(x.to_s)
83
- y = BigDecimal(y.to_s)
84
- [x, y]
85
- end
86
-
87
78
  # Subclasses must implement
88
79
  def contain?(x_or_point, y = nil, outline: false, distance_tolerance: 0)
89
80
  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