perfect-shape 0.3.2 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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