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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +43 -3
- data/LICENSE.txt +1 -1
- data/README.md +152 -32
- data/VERSION +1 -1
- data/lib/perfect-shape.rb +2 -2
- data/lib/perfect_shape/affine_transform.rb +235 -0
- data/lib/perfect_shape/arc.rb +5 -5
- data/lib/perfect_shape/circle.rb +1 -1
- data/lib/perfect_shape/composite_shape.rb +5 -4
- data/lib/perfect_shape/cubic_bezier_curve.rb +53 -46
- data/lib/perfect_shape/ellipse.rb +2 -3
- data/lib/perfect_shape/line.rb +13 -13
- data/lib/perfect_shape/math.rb +21 -0
- data/lib/perfect_shape/multi_point.rb +21 -6
- data/lib/perfect_shape/path.rb +65 -16
- data/lib/perfect_shape/point.rb +24 -8
- data/lib/perfect_shape/point_location.rb +1 -1
- data/lib/perfect_shape/polygon.rb +10 -6
- data/lib/perfect_shape/quadratic_bezier_curve.rb +176 -89
- data/lib/perfect_shape/rectangle.rb +14 -7
- data/lib/perfect_shape/rectangular_shape.rb +1 -1
- data/lib/perfect_shape/shape.rb +7 -16
- data/lib/perfect_shape/square.rb +1 -1
- data/perfect-shape.gemspec +5 -4
- metadata +8 -5
data/lib/perfect_shape/path.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
|
@@ -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
|
-
|
121
|
-
|
122
|
-
|
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
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
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
|
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
|
@@ -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:
|
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
|
@@ -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
|
-
|
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
|
-
|
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
|
@@ -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
|
-
|
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
|
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
|
@@ -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
|
data/lib/perfect_shape/square.rb
CHANGED