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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +35 -4
- data/LICENSE.txt +1 -1
- data/README.md +172 -32
- data/VERSION +1 -1
- data/lib/perfect-shape.rb +1 -1
- data/lib/perfect_shape/arc.rb +50 -35
- data/lib/perfect_shape/circle.rb +1 -1
- data/lib/perfect_shape/composite_shape.rb +1 -1
- data/lib/perfect_shape/cubic_bezier_curve.rb +123 -28
- data/lib/perfect_shape/ellipse.rb +13 -9
- data/lib/perfect_shape/line.rb +8 -8
- data/lib/perfect_shape/math.rb +21 -0
- data/lib/perfect_shape/multi_point.rb +8 -2
- data/lib/perfect_shape/path.rb +63 -13
- data/lib/perfect_shape/point.rb +7 -6
- data/lib/perfect_shape/point_location.rb +1 -1
- data/lib/perfect_shape/polygon.rb +8 -4
- data/lib/perfect_shape/quadratic_bezier_curve.rb +173 -86
- data/lib/perfect_shape/rectangle.rb +11 -5
- data/lib/perfect_shape/rectangular_shape.rb +1 -1
- data/lib/perfect_shape/shape.rb +13 -3
- data/lib/perfect_shape/square.rb +1 -1
- data/perfect-shape.gemspec +3 -3
- metadata +2 -2
@@ -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
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
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
|
-
|
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
|
data/lib/perfect_shape/shape.rb
CHANGED
@@ -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
|
data/lib/perfect_shape/square.rb
CHANGED
data/perfect-shape.gemspec
CHANGED
@@ -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.
|
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.
|
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-
|
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.
|
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-
|
11
|
+
date: 2022-01-11 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: equalizer
|