perfect-shape 0.3.2 → 0.4.0
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 +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