perfect-shape 0.3.0 → 0.3.4

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
@@ -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,7 +79,7 @@ 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)
82
+ def contain?(x_or_point, y = nil, outline: false, distance_tolerance: 0)
81
83
  x, y = normalize_point(x_or_point, y)
82
84
  return unless x && y
83
85
 
@@ -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
@@ -186,5 +194,84 @@ module PerfectShape
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 = 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
@@ -41,13 +41,19 @@ module PerfectShape
41
41
  x, y = normalize_point(x_or_point, y)
42
42
  return unless x && y
43
43
  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)
44
+ edges.any? { |edge| edge.contain?(x, y, distance_tolerance: distance_tolerance) }
48
45
  else
49
46
  x.between?(self.x, self.x + width) && y.between?(self.y, self.y + height)
50
47
  end
51
48
  end
49
+
50
+ def edges
51
+ [
52
+ Line.new(points: [[self.x, self.y], [self.x + width, self.y]]),
53
+ Line.new(points: [[self.x + width, self.y], [self.x + width, self.y + height]]),
54
+ Line.new(points: [[self.x + width, self.y + height], [self.x, self.y + height]]),
55
+ Line.new(points: [[self.x, self.y + height], [self.x, self.y]])
56
+ ]
57
+ end
52
58
  end
53
59
  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
@@ -79,11 +85,15 @@ module PerfectShape
79
85
  def normalize_point(x_or_point, y = nil)
80
86
  x = x_or_point
81
87
  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)
88
+ x = x.is_a?(BigDecimal) ? x : BigDecimal(x.to_s)
89
+ y = y.is_a?(BigDecimal) ? y : BigDecimal(y.to_s)
84
90
  [x, y]
85
91
  end
86
92
 
93
+ # Subclasses must implement
94
+ def contain?(x_or_point, y = nil, outline: false, distance_tolerance: 0)
95
+ end
96
+
87
97
  # Subclasses must implement
88
98
  def ==(other)
89
99
  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
@@ -2,16 +2,16 @@
2
2
  # DO NOT EDIT THIS FILE DIRECTLY
3
3
  # Instead, edit Juwelier::Tasks in Rakefile, and run 'rake gemspec'
4
4
  # -*- encoding: utf-8 -*-
5
- # stub: perfect-shape 0.3.0 ruby lib
5
+ # stub: perfect-shape 0.3.4 ruby lib
6
6
 
7
7
  Gem::Specification.new do |s|
8
8
  s.name = "perfect-shape".freeze
9
- s.version = "0.3.0"
9
+ s.version = "0.3.4"
10
10
 
11
11
  s.required_rubygems_version = Gem::Requirement.new(">= 0".freeze) if s.respond_to? :required_rubygems_version=
12
12
  s.require_paths = ["lib".freeze]
13
13
  s.authors = ["Andy Maleh".freeze]
14
- s.date = "2022-01-08"
14
+ s.date = "2022-01-11"
15
15
  s.description = "Perfect Shape is a collection of pure Ruby geometric algorithms that are mostly useful for GUI manipulation like checking containment of a mouse click point in popular geometry shapes such as rectangle, square, arc (open, chord, and pie), ellipse, circle, polygon, and paths containing lines, quadratic b\u00E9zier curves, and cubic bezier curves (including both Ray Casting Algorithm, aka Even-odd Rule, and Winding Number Algorithm, aka Nonzero Rule). Additionally, it contains some purely mathematical algorithms like IEEEremainder (also known as IEEE-754 remainder).".freeze
16
16
  s.email = "andy.am@gmail.com".freeze
17
17
  s.extra_rdoc_files = [
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: perfect-shape
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.3.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andy Maleh
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-01-08 00:00:00.000000000 Z
11
+ date: 2022-01-11 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: equalizer