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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +34 -3
- data/LICENSE.txt +1 -1
- data/README.md +178 -31
- 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 +106 -16
- data/lib/perfect_shape/ellipse.rb +13 -9
- data/lib/perfect_shape/line.rb +5 -5
- data/lib/perfect_shape/math.rb +21 -0
- data/lib/perfect_shape/multi_point.rb +1 -1
- data/lib/perfect_shape/path.rb +1 -1
- data/lib/perfect_shape/point.rb +5 -5
- data/lib/perfect_shape/point_location.rb +1 -1
- data/lib/perfect_shape/polygon.rb +74 -64
- data/lib/perfect_shape/quadratic_bezier_curve.rb +172 -86
- data/lib/perfect_shape/rectangle.rb +17 -3
- data/lib/perfect_shape/rectangular_shape.rb +1 -1
- data/lib/perfect_shape/shape.rb +5 -1
- data/lib/perfect_shape/square.rb +1 -1
- data/perfect-shape.gemspec +4 -4
- metadata +3 -3
data/lib/perfect_shape/circle.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
|
@@ -75,6 +75,8 @@ module PerfectShape
|
|
75
75
|
include MultiPoint
|
76
76
|
include Equalizer.new(:points)
|
77
77
|
|
78
|
+
OUTLINE_MINIMUM_DISTANCE_THRESHOLD = BigDecimal('0.001')
|
79
|
+
|
78
80
|
# Checks if cubic bézier curve contains point (two-number Array or x, y args)
|
79
81
|
#
|
80
82
|
# @param x The X coordinate of the point to test.
|
@@ -83,24 +85,29 @@ module PerfectShape
|
|
83
85
|
# @return {@code true} if the point lies within the bound of
|
84
86
|
# the cubic bézier curve, {@code false} if the point lies outside of the
|
85
87
|
# cubic bézier curve's bounds.
|
86
|
-
def contain?(x_or_point, y = nil)
|
88
|
+
def contain?(x_or_point, y = nil, outline: false, distance_tolerance: 0)
|
87
89
|
x, y = normalize_point(x_or_point, y)
|
88
90
|
return unless x && y
|
89
91
|
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
92
|
+
if outline
|
93
|
+
minimum_distance_threshold = OUTLINE_MINIMUM_DISTANCE_THRESHOLD + distance_tolerance
|
94
|
+
point_segment_distance(x, y, minimum_distance_threshold: minimum_distance_threshold) < minimum_distance_threshold
|
95
|
+
else
|
96
|
+
# Either x or y was infinite or NaN.
|
97
|
+
# A NaN always produces a negative response to any test
|
98
|
+
# and Infinity values cannot be "inside" any path so
|
99
|
+
# they should return false as well.
|
100
|
+
return false if (!(x * 0.0 + y * 0.0 == 0.0))
|
101
|
+
# We count the "Y" crossings to determine if the point is
|
102
|
+
# inside the curve bounded by its closing line.
|
103
|
+
x1 = points[0][0]
|
104
|
+
y1 = points[0][1]
|
105
|
+
x2 = points[3][0]
|
106
|
+
y2 = points[3][1]
|
107
|
+
line = PerfectShape::Line.new(points: [[x1, y1], [x2, y2]])
|
108
|
+
crossings = line.point_crossings(x, y) + point_crossings(x, y);
|
109
|
+
(crossings & 1) == 1
|
110
|
+
end
|
104
111
|
end
|
105
112
|
|
106
113
|
# Calculates the number of times the cubic bézier curve
|
@@ -116,5 +123,88 @@ module PerfectShape
|
|
116
123
|
return unless x && y
|
117
124
|
CubicBezierCurve.point_crossings(points[0][0], points[0][1], points[1][0], points[1][1], points[2][0], points[2][1], points[3][0], points[3][1], x, y, level)
|
118
125
|
end
|
126
|
+
|
127
|
+
# The center point on the outline of the curve
|
128
|
+
def curve_center_point
|
129
|
+
subdivisions.last.points[0]
|
130
|
+
end
|
131
|
+
|
132
|
+
# The center point x on the outline of the curve
|
133
|
+
def curve_center_x
|
134
|
+
subdivisions.last.points[0][0]
|
135
|
+
end
|
136
|
+
|
137
|
+
# The center point y on the outline of the curve
|
138
|
+
def curve_center_y
|
139
|
+
subdivisions.last.points[0][1]
|
140
|
+
end
|
141
|
+
|
142
|
+
# Subdivides CubicBezierCurve exactly at its curve center
|
143
|
+
# returning 2 CubicBezierCurve's as a two-element Array by default
|
144
|
+
#
|
145
|
+
# Optional `level` parameter specifies the level of recursions to
|
146
|
+
# perform to get more subdivisions. The number of resulting
|
147
|
+
# subdivisions is 2 to the power of `level` (e.g. 2 subdivisions
|
148
|
+
# for level=1, 4 subdivisions for level=2, and 8 subdivisions for level=3)
|
149
|
+
def subdivisions(level = 1)
|
150
|
+
level -= 1 # consume 1 level
|
151
|
+
x1 = points[0][0]
|
152
|
+
y1 = points[0][1]
|
153
|
+
ctrlx1 = points[1][0]
|
154
|
+
ctrly1 = points[1][1]
|
155
|
+
ctrlx2 = points[2][0]
|
156
|
+
ctrly2 = points[2][1]
|
157
|
+
x2 = points[3][0]
|
158
|
+
y2 = points[3][1]
|
159
|
+
centerx = (ctrlx1 + ctrlx2) / 2.0
|
160
|
+
centery = (ctrly1 + ctrly2) / 2.0
|
161
|
+
ctrlx1 = (x1 + ctrlx1) / 2.0
|
162
|
+
ctrly1 = (y1 + ctrly1) / 2.0
|
163
|
+
ctrlx2 = (x2 + ctrlx2) / 2.0
|
164
|
+
ctrly2 = (y2 + ctrly2) / 2.0
|
165
|
+
ctrlx12 = (ctrlx1 + centerx) / 2.0
|
166
|
+
ctrly12 = (ctrly1 + centery) / 2.0
|
167
|
+
ctrlx21 = (ctrlx2 + centerx) / 2.0
|
168
|
+
ctrly21 = (ctrly2 + centery) / 2.0
|
169
|
+
centerx = (ctrlx12 + ctrlx21) / 2.0
|
170
|
+
centery = (ctrly12 + ctrly21) / 2.0
|
171
|
+
default_subdivisions = [
|
172
|
+
CubicBezierCurve.new(points: [x1, y1, ctrlx1, ctrly1, ctrlx12, ctrly12, centerx, centery]),
|
173
|
+
CubicBezierCurve.new(points: [centerx, centery, ctrlx21, ctrly21, ctrlx2, ctrly2, x2, y2])
|
174
|
+
]
|
175
|
+
if level == 0
|
176
|
+
default_subdivisions
|
177
|
+
else
|
178
|
+
default_subdivisions.map { |curve| curve.subdivisions(level) }.flatten
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
def point_segment_distance(x_or_point, y = nil, minimum_distance_threshold: OUTLINE_MINIMUM_DISTANCE_THRESHOLD)
|
183
|
+
x, y = normalize_point(x_or_point, y)
|
184
|
+
return unless x && y
|
185
|
+
|
186
|
+
point = Point.new(x, y)
|
187
|
+
current_curve = self
|
188
|
+
minimum_distance = point.point_distance(curve_center_point)
|
189
|
+
last_minimum_distance = minimum_distance + 1 # start bigger to ensure going through loop once at least
|
190
|
+
while minimum_distance >= minimum_distance_threshold && minimum_distance < last_minimum_distance
|
191
|
+
curve1, curve2 = current_curve.subdivisions
|
192
|
+
distance1 = point.point_distance(curve1.curve_center_point)
|
193
|
+
distance2 = point.point_distance(curve2.curve_center_point)
|
194
|
+
last_minimum_distance = minimum_distance
|
195
|
+
if distance1 < distance2
|
196
|
+
minimum_distance = distance1
|
197
|
+
current_curve = curve1
|
198
|
+
else
|
199
|
+
minimum_distance = distance2
|
200
|
+
current_curve = curve2
|
201
|
+
end
|
202
|
+
end
|
203
|
+
if minimum_distance < minimum_distance_threshold
|
204
|
+
minimum_distance
|
205
|
+
else
|
206
|
+
last_minimum_distance
|
207
|
+
end
|
208
|
+
end
|
119
209
|
end
|
120
210
|
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
|
@@ -63,17 +63,21 @@ module PerfectShape
|
|
63
63
|
# @return {@code true} if the point lies within the bound of
|
64
64
|
# the ellipse, {@code false} if the point lies outside of the
|
65
65
|
# ellipse's bounds.
|
66
|
-
def contain?(x_or_point, y = nil)
|
66
|
+
def contain?(x_or_point, y = nil, outline: false, distance_tolerance: 0)
|
67
67
|
# This is implemented again even though super would have just worked to have an optimized algorithm for Ellipse.
|
68
68
|
x, y = normalize_point(x_or_point, y)
|
69
69
|
return unless x && y
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
70
|
+
if outline
|
71
|
+
super(x, y, outline: true, distance_tolerance: distance_tolerance)
|
72
|
+
else
|
73
|
+
ellw = self.width
|
74
|
+
return false if ellw <= 0.0
|
75
|
+
normx = (x - self.x) / ellw - 0.5
|
76
|
+
ellh = self.height
|
77
|
+
return false if ellh <= 0.0
|
78
|
+
normy = (y - self.y) / ellh - 0.5
|
79
|
+
(normx * normx + normy * normy) < 0.25
|
80
|
+
end
|
77
81
|
end
|
78
82
|
end
|
79
83
|
end
|
data/lib/perfect_shape/line.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
|
@@ -207,16 +207,16 @@ module PerfectShape
|
|
207
207
|
#
|
208
208
|
# @param x The X coordinate of the point to test.
|
209
209
|
# @param y The Y coordinate of the point to test.
|
210
|
-
# @param
|
210
|
+
# @param distance_tolerance The distance from line to tolerate (0 by default)
|
211
211
|
#
|
212
212
|
# @return {@code true} if the point lies within the bound of
|
213
213
|
# the line, {@code false} if the point lies outside of the
|
214
214
|
# line's bounds.
|
215
|
-
def contain?(x_or_point, y = nil,
|
215
|
+
def contain?(x_or_point, y = nil, outline: false, distance_tolerance: 0)
|
216
216
|
x, y = normalize_point(x_or_point, y)
|
217
217
|
return unless x && y
|
218
|
-
|
219
|
-
point_segment_distance(x, y) <=
|
218
|
+
distance_tolerance = BigDecimal(distance_tolerance.to_s)
|
219
|
+
point_segment_distance(x, y) <= distance_tolerance
|
220
220
|
end
|
221
221
|
|
222
222
|
def point_segment_distance(x_or_point, y = nil)
|
data/lib/perfect_shape/math.rb
CHANGED
@@ -1,3 +1,24 @@
|
|
1
|
+
# Copyright (c) 2021-2022 Andy Maleh
|
2
|
+
#
|
3
|
+
# Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
# a copy of this software and associated documentation files (the
|
5
|
+
# "Software"), to deal in the Software without restriction, including
|
6
|
+
# without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
# distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
# permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
# the following conditions:
|
10
|
+
#
|
11
|
+
# The above copyright notice and this permission notice shall be
|
12
|
+
# included in all copies or substantial portions of the Software.
|
13
|
+
#
|
14
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
21
|
+
|
1
22
|
module PerfectShape
|
2
23
|
# Perfect Shape Math utility methods
|
3
24
|
#
|
data/lib/perfect_shape/path.rb
CHANGED
data/lib/perfect_shape/point.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
|
@@ -63,15 +63,15 @@ module PerfectShape
|
|
63
63
|
#
|
64
64
|
# @param x The X coordinate of the point to test.
|
65
65
|
# @param y The Y coordinate of the point to test.
|
66
|
-
# @param
|
66
|
+
# @param distance_tolerance The distance from point to tolerate (0 by default)
|
67
67
|
#
|
68
68
|
# @return {@code true} if the point is close enough within distance tolerance,
|
69
69
|
# {@code false} if the point is too far.
|
70
|
-
def contain?(x_or_point, y = nil,
|
70
|
+
def contain?(x_or_point, y = nil, outline: false, distance_tolerance: 0)
|
71
71
|
x, y = normalize_point(x_or_point, y)
|
72
72
|
return unless x && y
|
73
|
-
|
74
|
-
point_distance(x, y) <=
|
73
|
+
distance_tolerance = BigDecimal(distance_tolerance.to_s)
|
74
|
+
point_distance(x, y) <= distance_tolerance
|
75
75
|
end
|
76
76
|
|
77
77
|
def point_distance(x_or_point, y = nil)
|
@@ -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
|
@@ -37,79 +37,89 @@ module PerfectShape
|
|
37
37
|
# @return {@code true} if the point lies within the bound of
|
38
38
|
# the polygon, {@code false} if the point lies outside of the
|
39
39
|
# polygon's bounds.
|
40
|
-
def contain?(x_or_point, y = nil)
|
40
|
+
def contain?(x_or_point, y = nil, outline: false, distance_tolerance: 0)
|
41
41
|
x, y = normalize_point(x_or_point, y)
|
42
42
|
return unless x && y
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
end
|
62
|
-
|
63
|
-
if curx < lastx
|
64
|
-
if x >= lastx
|
65
|
-
lastx = curx
|
66
|
-
lasty = cury
|
67
|
-
next
|
68
|
-
end
|
69
|
-
leftx = curx
|
70
|
-
else
|
71
|
-
if x >= curx
|
72
|
-
lastx = curx
|
73
|
-
lasty = cury
|
74
|
-
next
|
75
|
-
end
|
76
|
-
leftx = lastx
|
77
|
-
end
|
78
|
-
|
79
|
-
if cury < lasty
|
80
|
-
if y < cury || y >= lasty
|
43
|
+
if outline
|
44
|
+
edges.any? { |edge| edge.contain?(x, y, distance_tolerance: distance_tolerance) }
|
45
|
+
else
|
46
|
+
npoints = points.count
|
47
|
+
xpoints = points.map(&:first)
|
48
|
+
ypoints = points.map(&:last)
|
49
|
+
return false if npoints <= 2 || !bounding_box.contain?(x, y)
|
50
|
+
hits = 0
|
51
|
+
|
52
|
+
lastx = xpoints[npoints - 1]
|
53
|
+
lasty = ypoints[npoints - 1]
|
54
|
+
|
55
|
+
# Walk the edges of the polygon
|
56
|
+
npoints.times do |i|
|
57
|
+
curx = xpoints[i]
|
58
|
+
cury = ypoints[i]
|
59
|
+
|
60
|
+
if cury == lasty
|
81
61
|
lastx = curx
|
82
62
|
lasty = cury
|
83
63
|
next
|
84
64
|
end
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
65
|
+
|
66
|
+
if curx < lastx
|
67
|
+
if x >= lastx
|
68
|
+
lastx = curx
|
69
|
+
lasty = cury
|
70
|
+
next
|
71
|
+
end
|
72
|
+
leftx = curx
|
73
|
+
else
|
74
|
+
if x >= curx
|
75
|
+
lastx = curx
|
76
|
+
lasty = cury
|
77
|
+
next
|
78
|
+
end
|
79
|
+
leftx = lastx
|
90
80
|
end
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
81
|
+
|
82
|
+
if cury < lasty
|
83
|
+
if y < cury || y >= lasty
|
84
|
+
lastx = curx
|
85
|
+
lasty = cury
|
86
|
+
next
|
87
|
+
end
|
88
|
+
if x < leftx
|
89
|
+
hits += 1
|
90
|
+
lastx = curx
|
91
|
+
lasty = cury
|
92
|
+
next
|
93
|
+
end
|
94
|
+
test1 = x - curx
|
95
|
+
test2 = y - cury
|
96
|
+
else
|
97
|
+
if y < lasty || y >= cury
|
98
|
+
lastx = curx
|
99
|
+
lasty = cury
|
100
|
+
next
|
101
|
+
end
|
102
|
+
if x < leftx
|
103
|
+
hits += 1
|
104
|
+
lastx = curx
|
105
|
+
lasty = cury
|
106
|
+
next
|
107
|
+
end
|
108
|
+
test1 = x - lastx
|
109
|
+
test2 = y - lasty
|
98
110
|
end
|
99
|
-
|
100
|
-
|
101
|
-
lastx = curx
|
102
|
-
lasty = cury
|
103
|
-
next
|
104
|
-
end
|
105
|
-
test1 = x - lastx
|
106
|
-
test2 = y - lasty
|
111
|
+
|
112
|
+
hits += 1 if (test1 < (test2 / (lasty - cury) * (lastx - curx)))
|
107
113
|
end
|
108
|
-
|
109
|
-
hits
|
114
|
+
|
115
|
+
(hits & 1) != 0
|
116
|
+
end
|
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]])
|
110
122
|
end
|
111
|
-
|
112
|
-
(hits & 1) != 0
|
113
123
|
end
|
114
124
|
end
|
115
125
|
end
|