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
@@ -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,95 @@ 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
+ minimum_distance_threshold = OUTLINE_MINIMUM_DISTANCE_THRESHOLD + distance_tolerance
95
+ point_segment_distance(x, y, minimum_distance_threshold: minimum_distance_threshold) < minimum_distance_threshold
96
+ else
97
+ # We have a convex shape bounded by quad curve Pc(t)
98
+ # and ine Pl(t).
99
+ #
100
+ # P1 = (x1, y1) - start point of curve
101
+ # P2 = (x2, y2) - end point of curve
102
+ # Pc = (xc, yc) - control point
103
+ #
104
+ # Pq(t) = P1*(1 - t)^2 + 2*Pc*t*(1 - t) + P2*t^2 =
105
+ # = (P1 - 2*Pc + P2)*t^2 + 2*(Pc - P1)*t + P1
106
+ # Pl(t) = P1*(1 - t) + P2*t
107
+ # t = [0:1]
108
+ #
109
+ # P = (x, y) - point of interest
110
+ #
111
+ # Let's look at second derivative of quad curve equation:
112
+ #
113
+ # Pq''(t) = 2 * (P1 - 2 * Pc + P2) = Pq''
114
+ # It's constant vector.
115
+ #
116
+ # Let's draw a line through P to be parallel to this
117
+ # vector and find the intersection of the quad curve
118
+ # and the line.
119
+ #
120
+ # Pq(t) is point of intersection if system of equations
121
+ # below has the solution.
122
+ #
123
+ # L(s) = P + Pq''*s == Pq(t)
124
+ # Pq''*s + (P - Pq(t)) == 0
125
+ #
126
+ # | xq''*s + (x - xq(t)) == 0
127
+ # | yq''*s + (y - yq(t)) == 0
128
+ #
129
+ # This system has the solution if rank of its matrix equals to 1.
130
+ # That is, determinant of the matrix should be zero.
131
+ #
132
+ # (y - yq(t))*xq'' == (x - xq(t))*yq''
133
+ #
134
+ # Let's solve this equation with 't' variable.
135
+ # Also let kx = x1 - 2*xc + x2
136
+ # ky = y1 - 2*yc + y2
137
+ #
138
+ # t0q = (1/2)*((x - x1)*ky - (y - y1)*kx) /
139
+ # ((xc - x1)*ky - (yc - y1)*kx)
140
+ #
141
+ # Let's do the same for our line Pl(t):
142
+ #
143
+ # t0l = ((x - x1)*ky - (y - y1)*kx) /
144
+ # ((x2 - x1)*ky - (y2 - y1)*kx)
145
+ #
146
+ # It's easy to check that t0q == t0l. This fact means
147
+ # we can compute t0 only one time.
148
+ #
149
+ # In case t0 < 0 or t0 > 1, we have an intersections outside
150
+ # of shape bounds. So, P is definitely out of shape.
151
+ #
152
+ # In case t0 is inside [0:1], we should calculate Pq(t0)
153
+ # and Pl(t0). We have three points for now, and all of them
154
+ # lie on one line. So, we just need to detect, is our point
155
+ # of interest between points of intersections or not.
156
+ #
157
+ # If the denominator in the t0q and t0l equations is
158
+ # zero, then the points must be collinear and so the
159
+ # curve is degenerate and encloses no area. Thus the
160
+ # result is false.
161
+ kx = x1 - 2 * xc + x2;
162
+ ky = y1 - 2 * yc + y2;
163
+ dx = x - x1;
164
+ dy = y - y1;
165
+ dxl = x2 - x1;
166
+ dyl = y2 - y1;
167
+
168
+ t0 = (dx * ky - dy * kx) / (dxl * ky - dyl * kx)
169
+ return false if (t0 < 0 || t0 > 1 || t0 != t0)
170
+
171
+ xb = kx * t0 * t0 + 2 * (xc - x1) * t0 + x1;
172
+ yb = ky * t0 * t0 + 2 * (yc - y1) * t0 + y1;
173
+ xl = dxl * t0 + x1;
174
+ yl = dyl * t0 + y1;
175
+
176
+ (x >= xb && x < xl) ||
177
+ (x >= xl && x < xb) ||
178
+ (y >= yb && y < yl) ||
179
+ (y >= yl && y < yb)
180
+ end
174
181
  end
175
182
 
176
183
  # Calculates the number of times the quad
@@ -186,5 +193,84 @@ module PerfectShape
186
193
  return unless x && y
187
194
  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
195
  end
196
+
197
+
198
+ # The center point on the outline of the curve
199
+ def curve_center_point
200
+ subdivisions.last.points[0]
201
+ end
202
+
203
+ # The center point x on the outline of the curve
204
+ def curve_center_x
205
+ subdivisions.last.points[0][0]
206
+ end
207
+
208
+ # The center point y on the outline of the curve
209
+ def curve_center_y
210
+ subdivisions.last.points[0][1]
211
+ end
212
+
213
+ # Subdivides QuadraticBezierCurve exactly at its curve center
214
+ # returning 2 QuadraticBezierCurve's as a two-element Array by default
215
+ #
216
+ # Optional `level` parameter specifies the level of recursions to
217
+ # perform to get more subdivisions. The number of resulting
218
+ # subdivisions is 2 to the power of `level` (e.g. 2 subdivisions
219
+ # for level=1, 4 subdivisions for level=2, and 8 subdivisions for level=3)
220
+ def subdivisions(level = 1)
221
+ level -= 1 # consume 1 level
222
+
223
+ x1 = points[0][0]
224
+ y1 = points[0][1]
225
+ ctrlx = points[1][0]
226
+ ctrly = points[1][1]
227
+ x2 = points[2][0]
228
+ y2 = points[2][1]
229
+ ctrlx1 = (x1 + ctrlx) / 2.0
230
+ ctrly1 = (y1 + ctrly) / 2.0
231
+ ctrlx2 = (x2 + ctrlx) / 2.0
232
+ ctrly2 = (y2 + ctrly) / 2.0
233
+ centerx = (ctrlx1 + ctrlx2) / 2.0
234
+ centery = (ctrly1 + ctrly2) / 2.0
235
+
236
+ default_subdivisions = [
237
+ QuadraticBezierCurve.new(points: [x1, y1, ctrlx1, ctrly1, centerx, centery]),
238
+ QuadraticBezierCurve.new(points: [centerx, centery, ctrlx2, ctrly2, x2, y2])
239
+ ]
240
+
241
+ if level == 0
242
+ default_subdivisions
243
+ else
244
+ default_subdivisions.map { |curve| curve.subdivisions(level) }.flatten
245
+ end
246
+ end
247
+
248
+ def point_segment_distance(x_or_point, y = nil, minimum_distance_threshold: OUTLINE_MINIMUM_DISTANCE_THRESHOLD)
249
+ x, y = normalize_point(x_or_point, y)
250
+ return unless x && y
251
+
252
+ point = Point.new(x, y)
253
+ current_curve = self
254
+ minimum_distance = point.point_distance(curve_center_point)
255
+ last_minimum_distance = minimum_distance + 1 # start bigger to ensure going through loop once at least
256
+ while minimum_distance >= minimum_distance_threshold && minimum_distance < last_minimum_distance
257
+ curve1, curve2 = current_curve.subdivisions
258
+ distance1 = point.point_distance(curve1.curve_center_point)
259
+ distance2 = point.point_distance(curve2.curve_center_point)
260
+ last_minimum_distance = minimum_distance
261
+ if distance1 < distance2
262
+ minimum_distance = distance1
263
+ current_curve = curve1
264
+ else
265
+ minimum_distance = distance2
266
+ current_curve = curve2
267
+ end
268
+ end
269
+ if minimum_distance < minimum_distance_threshold
270
+ minimum_distance
271
+ else
272
+ last_minimum_distance
273
+ end
274
+ end
189
275
  end
190
276
  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,6 +21,7 @@
21
21
 
22
22
  require 'perfect_shape/shape'
23
23
  require 'perfect_shape/rectangular_shape'
24
+ require 'perfect_shape/line'
24
25
 
25
26
  module PerfectShape
26
27
  # Mostly ported from java.awt.geom: https://docs.oracle.com/javase/8/docs/api/java/awt/geom/Rectangle2D.html
@@ -36,10 +37,23 @@ module PerfectShape
36
37
  # @return {@code true} if the point lies within the bound of
37
38
  # the rectangle, {@code false} if the point lies outside of the
38
39
  # rectangle's bounds.
39
- def contain?(x_or_point, y = nil)
40
+ def contain?(x_or_point, y = nil, outline: false, distance_tolerance: 0)
40
41
  x, y = normalize_point(x_or_point, y)
41
42
  return unless x && y
42
- x.between?(self.x, self.x + self.width) && y.between?(self.y, self.y + self.height)
43
+ if outline
44
+ edges.any? { |edge| edge.contain?(x, y, distance_tolerance: distance_tolerance) }
45
+ else
46
+ x.between?(self.x, self.x + width) && y.between?(self.y, self.y + height)
47
+ end
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
+ ]
43
57
  end
44
58
  end
45
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
@@ -84,6 +84,10 @@ module PerfectShape
84
84
  [x, y]
85
85
  end
86
86
 
87
+ # Subclasses must implement
88
+ def contain?(x_or_point, y = nil, outline: false, distance_tolerance: 0)
89
+ end
90
+
87
91
  # Subclasses must implement
88
92
  def ==(other)
89
93
  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,17 +2,17 @@
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.2.0 ruby lib
5
+ # stub: perfect-shape 0.3.3 ruby lib
6
6
 
7
7
  Gem::Specification.new do |s|
8
8
  s.name = "perfect-shape".freeze
9
- s.version = "0.2.0"
9
+ s.version = "0.3.3"
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-07"
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 b\u00E9zier 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
14
+ s.date = "2022-01-10"
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 = [
18
18
  "CHANGELOG.md",
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.2.0
4
+ version: 0.3.3
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-07 00:00:00.000000000 Z
11
+ date: 2022-01-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: equalizer
@@ -98,7 +98,7 @@ description: Perfect Shape is a collection of pure Ruby geometric algorithms tha
98
98
  are mostly useful for GUI manipulation like checking containment of a mouse click
99
99
  point in popular geometry shapes such as rectangle, square, arc (open, chord, and
100
100
  pie), ellipse, circle, polygon, and paths containing lines, quadratic bézier curves,
101
- and cubic bézier curves (including both Ray Casting Algorithm, aka Even-odd Rule,
101
+ and cubic bezier curves (including both Ray Casting Algorithm, aka Even-odd Rule,
102
102
  and Winding Number Algorithm, aka Nonzero Rule). Additionally, it contains some
103
103
  purely mathematical algorithms like IEEEremainder (also known as IEEE-754 remainder).
104
104
  email: andy.am@gmail.com