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
@@ -0,0 +1,235 @@
|
|
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
|
+
|
22
|
+
require 'perfect_shape/math'
|
23
|
+
require 'perfect_shape/shape'
|
24
|
+
require 'perfect_shape/point'
|
25
|
+
require 'perfect_shape/multi_point'
|
26
|
+
|
27
|
+
module PerfectShape
|
28
|
+
# Represents an affine transform
|
29
|
+
class AffineTransform
|
30
|
+
include Equalizer.new(:xxp, :xyp, :yxp, :yyp, :xt, :yt)
|
31
|
+
|
32
|
+
attr_reader :xxp, :xyp, :yxp, :yyp, :xt, :yt
|
33
|
+
alias m11 xxp
|
34
|
+
alias m12 xyp
|
35
|
+
alias m21 yxp
|
36
|
+
alias m22 yyp
|
37
|
+
alias m13 xt
|
38
|
+
alias m23 yt
|
39
|
+
|
40
|
+
# Creates a new AffineTransform with the following Matrix:
|
41
|
+
#
|
42
|
+
# [ xxp xyp xt ]
|
43
|
+
# [ yxp yyp yt ]
|
44
|
+
#
|
45
|
+
# The Matrix is used to transform (x,y) point coordinates as follows:
|
46
|
+
#
|
47
|
+
# [ xxp xyp xt ] * [x] = [ xxp * x + xyp * y + xt ]
|
48
|
+
# [ yxp yyp yt ] * [y] = [ yxp * x + yyp * y + yt ]
|
49
|
+
#
|
50
|
+
# xxp is the x coordinate x product (m11)
|
51
|
+
# xyp is the x coordinate y product (m12)
|
52
|
+
# yxp is the y coordinate x product (m21)
|
53
|
+
# yyp is the y coordinate y product (m22)
|
54
|
+
# xt is the x coordinate translation (m13)
|
55
|
+
# yt is the y coordinate translation (m23)
|
56
|
+
#
|
57
|
+
# The constructor accepts either the (x,y)-operation related argument/kwarg names
|
58
|
+
# or traditional Matrix element kwarg names
|
59
|
+
#
|
60
|
+
# Example with (x,y)-operation kwarg names:
|
61
|
+
#
|
62
|
+
# AffineTransform.new(xxp: 2, xyp: 3, yxp: 4, yyp: 5, xt: 6, yt: 7)
|
63
|
+
#
|
64
|
+
# Example with traditional Matrix element kwarg names:
|
65
|
+
#
|
66
|
+
# AffineTransform.new(m11: 2, m12: 3, m21: 4, m22: 5, m13: 6, m23: 7)
|
67
|
+
#
|
68
|
+
# Example with standard arguments:
|
69
|
+
#
|
70
|
+
# AffineTransform.new(2, 3, 4, 5, 6, 7)
|
71
|
+
#
|
72
|
+
# If no arguments are supplied, it constructs an identity matrix
|
73
|
+
# (i.e. like calling `::new(xxp: 1, xyp: 0, yxp: 0, yyp: 1, xt: 0, yt: 0)`)
|
74
|
+
def initialize(xxp_element = nil, xyp_element = nil, yxp_element = nil, yyp_element = nil, xt_element = nil, yt_element = nil,
|
75
|
+
xxp: nil, xyp: nil, yxp: nil, yyp: nil, xt: nil, yt: nil,
|
76
|
+
m11: nil, m12: nil, m21: nil, m22: nil, m13: nil, m23: nil)
|
77
|
+
self.xxp = xxp_element || xxp || m11 || 1
|
78
|
+
self.xyp = xyp_element || xyp || m12 || 0
|
79
|
+
self.yxp = yxp_element || yxp || m21 || 0
|
80
|
+
self.yyp = yyp_element || yyp || m22 || 1
|
81
|
+
self.xt = xt_element || xt || m13 || 0
|
82
|
+
self.yt = yt_element || yt || m23 || 0
|
83
|
+
end
|
84
|
+
|
85
|
+
def xxp=(value)
|
86
|
+
@xxp = BigDecimal(value.to_s)
|
87
|
+
end
|
88
|
+
alias m11= xxp=
|
89
|
+
|
90
|
+
def xyp=(value)
|
91
|
+
@xyp = BigDecimal(value.to_s)
|
92
|
+
end
|
93
|
+
alias m12= xyp=
|
94
|
+
|
95
|
+
def yxp=(value)
|
96
|
+
@yxp = BigDecimal(value.to_s)
|
97
|
+
end
|
98
|
+
alias m21= yxp=
|
99
|
+
|
100
|
+
def yyp=(value)
|
101
|
+
@yyp = BigDecimal(value.to_s)
|
102
|
+
end
|
103
|
+
alias m22= yyp=
|
104
|
+
|
105
|
+
def xt=(value)
|
106
|
+
@xt = BigDecimal(value.to_s)
|
107
|
+
end
|
108
|
+
alias m13= xt=
|
109
|
+
|
110
|
+
def yt=(value)
|
111
|
+
@yt = BigDecimal(value.to_s)
|
112
|
+
end
|
113
|
+
alias m23= yt=
|
114
|
+
|
115
|
+
# Resets to identity matrix
|
116
|
+
# Returns self to support fluent interface chaining
|
117
|
+
def identity!
|
118
|
+
self.xxp = 1
|
119
|
+
self.xyp = 0
|
120
|
+
self.yxp = 0
|
121
|
+
self.yyp = 1
|
122
|
+
self.xt = 0
|
123
|
+
self.yt = 0
|
124
|
+
|
125
|
+
self
|
126
|
+
end
|
127
|
+
alias reset! identity!
|
128
|
+
|
129
|
+
# Inverts AffineTransform matrix if invertible
|
130
|
+
# Raises an error if affine transform matrix is not invertible
|
131
|
+
# Returns self to support fluent interface chaining
|
132
|
+
def invert!
|
133
|
+
raise 'Cannot invert (matrix is not invertible)!' if !invertible?
|
134
|
+
|
135
|
+
self.matrix_3d = matrix_3d.inverse
|
136
|
+
|
137
|
+
self
|
138
|
+
end
|
139
|
+
|
140
|
+
def invertible?
|
141
|
+
(m11 * m22 - m12 * m21) != 0
|
142
|
+
end
|
143
|
+
|
144
|
+
# Multiplies by other AffineTransform
|
145
|
+
def multiply!(other_affine_transform)
|
146
|
+
self.matrix_3d = matrix_3d*other_affine_transform.matrix_3d
|
147
|
+
|
148
|
+
self
|
149
|
+
end
|
150
|
+
|
151
|
+
# Translates AffineTransform
|
152
|
+
def translate!(x_or_point, y = nil)
|
153
|
+
x, y = Point.normalize_point(x_or_point, y)
|
154
|
+
return unless x && y
|
155
|
+
|
156
|
+
translation_affine_transform = AffineTransform.new(xxp: 1, xyp: 0, yxp: 0, yyp: 1, xt: x, yt: y)
|
157
|
+
multiply!(translation_affine_transform)
|
158
|
+
|
159
|
+
self
|
160
|
+
end
|
161
|
+
|
162
|
+
# Scales AffineTransform
|
163
|
+
def scale!(x_or_point, y = nil)
|
164
|
+
x, y = Point.normalize_point(x_or_point, y)
|
165
|
+
return unless x && y
|
166
|
+
|
167
|
+
scale_affine_transform = AffineTransform.new(xxp: x, xyp: 0, yxp: 0, yyp: y, xt: 0, yt: 0)
|
168
|
+
multiply!(scale_affine_transform)
|
169
|
+
|
170
|
+
self
|
171
|
+
end
|
172
|
+
|
173
|
+
# Rotates AffineTransform counter-clockwise for positive angle value in degrees
|
174
|
+
# or clockwise for negative angle value in degrees
|
175
|
+
def rotate!(degrees)
|
176
|
+
degrees = Math.normalize_degrees(degrees)
|
177
|
+
radians = Math.degrees_to_radians(degrees)
|
178
|
+
|
179
|
+
rotation_affine_transform = AffineTransform.new(xxp: Math.cos(radians), xyp: -Math.sin(radians), yxp: Math.sin(radians), yyp: Math.cos(radians), xt: 0, yt: 0)
|
180
|
+
multiply!(rotation_affine_transform)
|
181
|
+
|
182
|
+
self
|
183
|
+
end
|
184
|
+
|
185
|
+
# Shears AffineTransform by (x,y) amount
|
186
|
+
def shear!(x_or_point, y = nil)
|
187
|
+
x, y = Point.normalize_point(x_or_point, y)
|
188
|
+
return unless x && y
|
189
|
+
|
190
|
+
shear_affine_transform = AffineTransform.new(xxp: 1 + x*y, xyp: x, yxp: y, yyp: 1, xt: 0, yt: 0)
|
191
|
+
multiply!(shear_affine_transform)
|
192
|
+
|
193
|
+
self
|
194
|
+
end
|
195
|
+
alias skew! shear!
|
196
|
+
|
197
|
+
# Sets elements from a Ruby Matrix representing Affine Transform matrix elements in 3D
|
198
|
+
def matrix_3d=(the_matrix_3d)
|
199
|
+
self.xxp = the_matrix_3d[0, 0]
|
200
|
+
self.xyp = the_matrix_3d[0, 1]
|
201
|
+
self.xt = the_matrix_3d[0, 2]
|
202
|
+
self.yxp = the_matrix_3d[1, 0]
|
203
|
+
self.yyp = the_matrix_3d[1, 1]
|
204
|
+
self.yt = the_matrix_3d[1, 2]
|
205
|
+
end
|
206
|
+
|
207
|
+
# Returns Ruby Matrix representing Affine Transform matrix elements in 3D
|
208
|
+
def matrix_3d
|
209
|
+
Matrix[[xxp, xyp, xt], [yxp, yyp, yt], [0, 0, 1]]
|
210
|
+
end
|
211
|
+
|
212
|
+
def transform_point(x_or_point, y = nil)
|
213
|
+
x, y = Point.normalize_point(x_or_point, y)
|
214
|
+
return unless x && y
|
215
|
+
|
216
|
+
[xxp*x + xyp*y + xt, yxp*x + yyp*y + yt]
|
217
|
+
end
|
218
|
+
|
219
|
+
def inverse_transform_point(x_or_point, y = nil)
|
220
|
+
clone.invert!.transform_point(x_or_point, y)
|
221
|
+
end
|
222
|
+
|
223
|
+
def transform_points(*xy_coordinates_or_points)
|
224
|
+
points = xy_coordinates_or_points.first.is_a?(Array) ? xy_coordinates_or_points.first : xy_coordinates_or_points
|
225
|
+
points = MultiPoint.normalize_point_array(points)
|
226
|
+
points.map { |point| transform_point(point) }
|
227
|
+
end
|
228
|
+
|
229
|
+
def inverse_transform_points(*xy_coordinates_or_points)
|
230
|
+
points = xy_coordinates_or_points.first.is_a?(Array) ? xy_coordinates_or_points.first : xy_coordinates_or_points
|
231
|
+
points = MultiPoint.normalize_point_array(points)
|
232
|
+
points.map { |point| inverse_transform_point(point) }
|
233
|
+
end
|
234
|
+
end
|
235
|
+
end
|
data/lib/perfect_shape/arc.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
|
@@ -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/Arc2D.html
|
28
28
|
class Arc < Shape
|
29
29
|
include RectangularShape
|
30
30
|
include Equalizer.new(:type, :x, :y, :width, :height, :start, :extent)
|
@@ -145,15 +145,15 @@ module PerfectShape
|
|
145
145
|
# the arc, {@code false} if the point lies outside of the
|
146
146
|
# arc's bounds.
|
147
147
|
def contain?(x_or_point, y = nil, outline: false, distance_tolerance: 0)
|
148
|
-
x, y = normalize_point(x_or_point, y)
|
148
|
+
x, y = Point.normalize_point(x_or_point, y)
|
149
149
|
return unless x && y
|
150
150
|
if outline
|
151
151
|
if type == :pie && x == center_x && y == center_y
|
152
152
|
true
|
153
153
|
else
|
154
154
|
distance_tolerance = BigDecimal(distance_tolerance.to_s)
|
155
|
-
outside_inside_radius_difference = DEFAULT_OUTLINE_RADIUS + distance_tolerance * 2
|
156
|
-
outside_radius_difference = inside_radius_difference = outside_inside_radius_difference / 2
|
155
|
+
outside_inside_radius_difference = DEFAULT_OUTLINE_RADIUS + distance_tolerance * 2
|
156
|
+
outside_radius_difference = inside_radius_difference = outside_inside_radius_difference / 2
|
157
157
|
outside_shape = Arc.new(type: type, center_x: center_x, center_y: center_y, radius_x: radius_x + outside_radius_difference, radius_y: radius_y + outside_radius_difference, start: start, extent: extent)
|
158
158
|
inside_shape = Arc.new(type: type, center_x: center_x, center_y: center_y, radius_x: radius_x - inside_radius_difference, radius_y: radius_y - inside_radius_difference, start: start, extent: extent)
|
159
159
|
outside_shape.contain?(x, y, outline: false) and
|
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
|
@@ -63,10 +63,11 @@ module PerfectShape
|
|
63
63
|
# @return true if the point lies within the bound of
|
64
64
|
# the composite shape or false if the point lies outside of the
|
65
65
|
# path's bounds.
|
66
|
-
def contain?(x_or_point, y = nil)
|
67
|
-
x, y = normalize_point(x_or_point, y)
|
66
|
+
def contain?(x_or_point, y = nil, outline: false, distance_tolerance: 0)
|
67
|
+
x, y = Point.normalize_point(x_or_point, y)
|
68
68
|
return unless x && y
|
69
|
-
|
69
|
+
|
70
|
+
shapes.any? { |shape| shape.contain?(x, y, outline: outline, distance_tolerance: distance_tolerance) }
|
70
71
|
end
|
71
72
|
end
|
72
73
|
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 CubicBezierCurve < Shape
|
28
28
|
class << self
|
29
29
|
# Calculates the number of times the cubic bézier curve from (x1,y1) to (x2,y2)
|
@@ -51,18 +51,18 @@ module PerfectShape
|
|
51
51
|
end
|
52
52
|
# double precision only has 52 bits of mantissa
|
53
53
|
return PerfectShape::Line.point_crossings(x1, y1, x2, y2, px, py) if (level > 52)
|
54
|
-
xmid = BigDecimal((xc1 + xc2).to_s) / 2
|
55
|
-
ymid = BigDecimal((yc1 + yc2).to_s) / 2
|
56
|
-
xc1 = BigDecimal((x1 + xc1).to_s) / 2
|
57
|
-
yc1 = BigDecimal((y1 + yc1).to_s) / 2
|
58
|
-
xc2 = BigDecimal((xc2 + x2).to_s) / 2
|
59
|
-
yc2 = BigDecimal((yc2 + y2).to_s) / 2
|
60
|
-
xc1m = BigDecimal((xc1 + xmid).to_s) / 2
|
61
|
-
yc1m = BigDecimal((yc1 + ymid).to_s) / 2
|
62
|
-
xmc1 = BigDecimal((xmid + xc2).to_s) / 2
|
63
|
-
ymc1 = BigDecimal((ymid + yc2).to_s) / 2
|
64
|
-
xmid = BigDecimal((xc1m + xmc1).to_s) / 2
|
65
|
-
ymid = BigDecimal((yc1m + ymc1).to_s) / 2
|
54
|
+
xmid = BigDecimal((xc1 + xc2).to_s) / 2
|
55
|
+
ymid = BigDecimal((yc1 + yc2).to_s) / 2
|
56
|
+
xc1 = BigDecimal((x1 + xc1).to_s) / 2
|
57
|
+
yc1 = BigDecimal((y1 + yc1).to_s) / 2
|
58
|
+
xc2 = BigDecimal((xc2 + x2).to_s) / 2
|
59
|
+
yc2 = BigDecimal((yc2 + y2).to_s) / 2
|
60
|
+
xc1m = BigDecimal((xc1 + xmid).to_s) / 2
|
61
|
+
yc1m = BigDecimal((yc1 + ymid).to_s) / 2
|
62
|
+
xmc1 = BigDecimal((xmid + xc2).to_s) / 2
|
63
|
+
ymc1 = BigDecimal((ymid + yc2).to_s) / 2
|
64
|
+
xmid = BigDecimal((xc1m + xmc1).to_s) / 2
|
65
|
+
ymid = BigDecimal((yc1m + ymc1).to_s) / 2
|
66
66
|
# [xy]mid are NaN if any of [xy]c0m or [xy]mc1 are NaN
|
67
67
|
# [xy]c0m or [xy]mc1 are NaN if any of [xy][c][01] are NaN
|
68
68
|
# These values are also NaN if opposing infinities are added
|
@@ -86,12 +86,13 @@ module PerfectShape
|
|
86
86
|
# the cubic bézier curve, {@code false} if the point lies outside of the
|
87
87
|
# cubic bézier curve's bounds.
|
88
88
|
def contain?(x_or_point, y = nil, outline: false, distance_tolerance: 0)
|
89
|
-
x, y = normalize_point(x_or_point, y)
|
89
|
+
x, y = Point.normalize_point(x_or_point, y)
|
90
90
|
return unless x && y
|
91
91
|
|
92
92
|
if outline
|
93
|
+
distance_tolerance = BigDecimal(distance_tolerance.to_s)
|
93
94
|
minimum_distance_threshold = OUTLINE_MINIMUM_DISTANCE_THRESHOLD + distance_tolerance
|
94
|
-
|
95
|
+
point_distance(x, y, minimum_distance_threshold: minimum_distance_threshold) < minimum_distance_threshold
|
95
96
|
else
|
96
97
|
# Either x or y was infinite or NaN.
|
97
98
|
# A NaN always produces a negative response to any test
|
@@ -105,7 +106,7 @@ module PerfectShape
|
|
105
106
|
x2 = points[3][0]
|
106
107
|
y2 = points[3][1]
|
107
108
|
line = PerfectShape::Line.new(points: [[x1, y1], [x2, y2]])
|
108
|
-
crossings = line.point_crossings(x, y) + point_crossings(x, y)
|
109
|
+
crossings = line.point_crossings(x, y) + point_crossings(x, y)
|
109
110
|
(crossings & 1) == 1
|
110
111
|
end
|
111
112
|
end
|
@@ -119,7 +120,7 @@ module PerfectShape
|
|
119
120
|
# +1 is added for each crossing where the Y coordinate is increasing
|
120
121
|
# -1 is added for each crossing where the Y coordinate is decreasing
|
121
122
|
def point_crossings(x_or_point, y = nil, level = 0)
|
122
|
-
x, y = normalize_point(x_or_point, y)
|
123
|
+
x, y = Point.normalize_point(x_or_point, y)
|
123
124
|
return unless x && y
|
124
125
|
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)
|
125
126
|
end
|
@@ -141,11 +142,14 @@ module PerfectShape
|
|
141
142
|
|
142
143
|
# Subdivides CubicBezierCurve exactly at its curve center
|
143
144
|
# returning 2 CubicBezierCurve's as a two-element Array by default
|
144
|
-
#
|
145
|
-
#
|
146
|
-
#
|
147
|
-
|
148
|
-
|
145
|
+
#
|
146
|
+
# Optional `level` parameter specifies the level of recursions to
|
147
|
+
# perform to get more subdivisions. The number of resulting
|
148
|
+
# subdivisions is 2 to the power of `level` (e.g. 2 subdivisions
|
149
|
+
# for level=1, 4 subdivisions for level=2, and 8 subdivisions for level=3)
|
150
|
+
def subdivisions(level = 1)
|
151
|
+
level -= 1 # consume 1 level
|
152
|
+
|
149
153
|
x1 = points[0][0]
|
150
154
|
y1 = points[0][1]
|
151
155
|
ctrlx1 = points[1][0]
|
@@ -154,31 +158,32 @@ module PerfectShape
|
|
154
158
|
ctrly2 = points[2][1]
|
155
159
|
x2 = points[3][0]
|
156
160
|
y2 = points[3][1]
|
157
|
-
centerx = (ctrlx1 + ctrlx2) / 2
|
158
|
-
centery = (ctrly1 + ctrly2) / 2
|
159
|
-
ctrlx1 = (x1 + ctrlx1) / 2
|
160
|
-
ctrly1 = (y1 + ctrly1) / 2
|
161
|
-
ctrlx2 = (x2 + ctrlx2) / 2
|
162
|
-
ctrly2 = (y2 + ctrly2) / 2
|
163
|
-
ctrlx12 = (ctrlx1 + centerx) / 2
|
164
|
-
ctrly12 = (ctrly1 + centery) / 2
|
165
|
-
ctrlx21 = (ctrlx2 + centerx) / 2
|
166
|
-
ctrly21 = (ctrly2 + centery) / 2
|
167
|
-
centerx = (ctrlx12 + ctrlx21) / 2
|
168
|
-
centery = (ctrly12 + ctrly21) / 2
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
]
|
173
|
-
|
174
|
-
|
175
|
-
else
|
161
|
+
centerx = BigDecimal((ctrlx1 + ctrlx2).to_s) / 2
|
162
|
+
centery = BigDecimal((ctrly1 + ctrly2).to_s) / 2
|
163
|
+
ctrlx1 = BigDecimal((x1 + ctrlx1).to_s) / 2
|
164
|
+
ctrly1 = BigDecimal((y1 + ctrly1).to_s) / 2
|
165
|
+
ctrlx2 = BigDecimal((x2 + ctrlx2).to_s) / 2
|
166
|
+
ctrly2 = BigDecimal((y2 + ctrly2).to_s) / 2
|
167
|
+
ctrlx12 = BigDecimal((ctrlx1 + centerx).to_s) / 2
|
168
|
+
ctrly12 = BigDecimal((ctrly1 + centery).to_s) / 2
|
169
|
+
ctrlx21 = BigDecimal((ctrlx2 + centerx).to_s) / 2
|
170
|
+
ctrly21 = BigDecimal((ctrly2 + centery).to_s) / 2
|
171
|
+
centerx = BigDecimal((ctrlx12 + ctrlx21).to_s) / 2
|
172
|
+
centery = BigDecimal((ctrly12 + ctrly21).to_s) / 2
|
173
|
+
|
174
|
+
first_curve = CubicBezierCurve.new(points: [x1, y1, ctrlx1, ctrly1, ctrlx12, ctrly12, centerx, centery])
|
175
|
+
second_curve = CubicBezierCurve.new(points: [centerx, centery, ctrlx21, ctrly21, ctrlx2, ctrly2, x2, y2])
|
176
|
+
default_subdivisions = [first_curve, second_curve]
|
177
|
+
|
178
|
+
if level == 0
|
176
179
|
default_subdivisions
|
180
|
+
else
|
181
|
+
default_subdivisions.map { |curve| curve.subdivisions(level) }.flatten
|
177
182
|
end
|
178
183
|
end
|
179
184
|
|
180
|
-
def
|
181
|
-
x, y = normalize_point(x_or_point, y)
|
185
|
+
def point_distance(x_or_point, y = nil, minimum_distance_threshold: OUTLINE_MINIMUM_DISTANCE_THRESHOLD)
|
186
|
+
x, y = Point.normalize_point(x_or_point, y)
|
182
187
|
return unless x && y
|
183
188
|
|
184
189
|
point = Point.new(x, y)
|
@@ -187,8 +192,10 @@ module PerfectShape
|
|
187
192
|
last_minimum_distance = minimum_distance + 1 # start bigger to ensure going through loop once at least
|
188
193
|
while minimum_distance >= minimum_distance_threshold && minimum_distance < last_minimum_distance
|
189
194
|
curve1, curve2 = current_curve.subdivisions
|
190
|
-
|
191
|
-
|
195
|
+
curve1_center_point = curve1.curve_center_point
|
196
|
+
distance1 = point.point_distance(curve1_center_point)
|
197
|
+
curve2_center_point = curve2.curve_center_point
|
198
|
+
distance2 = point.point_distance(curve2_center_point)
|
192
199
|
last_minimum_distance = minimum_distance
|
193
200
|
if distance1 < distance2
|
194
201
|
minimum_distance = distance1
|
@@ -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
|
@@ -22,7 +22,6 @@
|
|
22
22
|
require 'perfect_shape/arc'
|
23
23
|
|
24
24
|
module PerfectShape
|
25
|
-
# Mostly ported from java.awt.geom: https://docs.oracle.com/javase/8/docs/api/java/awt/geom/Ellipse2D.html
|
26
25
|
class Ellipse < Arc
|
27
26
|
MESSAGE_CANNOT_UPDATE_ATTRIUBTE = "Ellipse %s cannot be updated. If you want to update type, use Arc instead!"
|
28
27
|
|
@@ -65,7 +64,7 @@ module PerfectShape
|
|
65
64
|
# ellipse's bounds.
|
66
65
|
def contain?(x_or_point, y = nil, outline: false, distance_tolerance: 0)
|
67
66
|
# This is implemented again even though super would have just worked to have an optimized algorithm for Ellipse.
|
68
|
-
x, y = normalize_point(x_or_point, y)
|
67
|
+
x, y = Point.normalize_point(x_or_point, y)
|
69
68
|
return unless x && y
|
70
69
|
if outline
|
71
70
|
super(x, y, outline: true, distance_tolerance: distance_tolerance)
|
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
|
@@ -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/Line2D.html
|
27
27
|
class Line < Shape
|
28
28
|
class << self
|
29
29
|
# Returns an indicator of where the specified point (px,py) lies with respect to the line segment from
|
@@ -104,7 +104,7 @@ module PerfectShape
|
|
104
104
|
# measured against the specified line segment
|
105
105
|
# @return a double value that is the square of the distance from the
|
106
106
|
# specified point to the specified line segment.
|
107
|
-
def
|
107
|
+
def point_distance_square(x1, y1,
|
108
108
|
x2, y2,
|
109
109
|
px, py)
|
110
110
|
x1 = BigDecimal(x1.to_s)
|
@@ -177,10 +177,10 @@ module PerfectShape
|
|
177
177
|
# measured against the specified line segment
|
178
178
|
# @return a double value that is the distance from the specified point
|
179
179
|
# to the specified line segment.
|
180
|
-
def
|
180
|
+
def point_distance(x1, y1,
|
181
181
|
x2, y2,
|
182
182
|
px, py)
|
183
|
-
BigDecimal(::Math.sqrt(
|
183
|
+
BigDecimal(::Math.sqrt(point_distance_square(x1, y1, x2, y2, px, py)).to_s)
|
184
184
|
end
|
185
185
|
|
186
186
|
# Calculates the number of times the line from (x1,y1) to (x2,y2)
|
@@ -212,21 +212,21 @@ module PerfectShape
|
|
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, outline:
|
216
|
-
x, y = normalize_point(x_or_point, y)
|
215
|
+
def contain?(x_or_point, y = nil, outline: true, distance_tolerance: 0)
|
216
|
+
x, y = Point.normalize_point(x_or_point, y)
|
217
217
|
return unless x && y
|
218
218
|
distance_tolerance = BigDecimal(distance_tolerance.to_s)
|
219
|
-
|
219
|
+
point_distance(x, y) <= distance_tolerance
|
220
220
|
end
|
221
221
|
|
222
|
-
def
|
223
|
-
x, y = normalize_point(x_or_point, y)
|
222
|
+
def point_distance(x_or_point, y = nil)
|
223
|
+
x, y = Point.normalize_point(x_or_point, y)
|
224
224
|
return unless x && y
|
225
|
-
Line.
|
225
|
+
Line.point_distance(points[0][0], points[0][1], points[1][0], points[1][1], x, y)
|
226
226
|
end
|
227
227
|
|
228
228
|
def relative_counterclockwise(x_or_point, y = nil)
|
229
|
-
x, y = normalize_point(x_or_point, y)
|
229
|
+
x, y = Point.normalize_point(x_or_point, y)
|
230
230
|
return unless x && y
|
231
231
|
Line.relative_counterclockwise(points[0][0], points[0][1], points[1][0], points[1][1], x, y)
|
232
232
|
end
|
@@ -237,7 +237,7 @@ module PerfectShape
|
|
237
237
|
# +1 is returned for a crossing where the Y coordinate is increasing
|
238
238
|
# -1 is returned for a crossing where the Y coordinate is decreasing
|
239
239
|
def point_crossings(x_or_point, y = nil)
|
240
|
-
x, y = normalize_point(x_or_point, y)
|
240
|
+
x, y = Point.normalize_point(x_or_point, y)
|
241
241
|
return unless x && y
|
242
242
|
Line.point_crossings(points[0][0], points[0][1], points[1][0], points[1][1], x, y)
|
243
243
|
end
|
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
|
#
|
@@ -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
|
@@ -24,6 +24,19 @@ require 'perfect_shape/shape'
|
|
24
24
|
module PerfectShape
|
25
25
|
# Represents multi-point shapes like Line, Polygon, and Polyline
|
26
26
|
module MultiPoint
|
27
|
+
class << self
|
28
|
+
def normalize_point_array(the_points)
|
29
|
+
if the_points.all? {|the_point| the_point.is_a?(Array)}
|
30
|
+
the_points
|
31
|
+
else
|
32
|
+
the_points = the_points.flatten
|
33
|
+
xs = the_points.each_with_index.select {|n, i| i.even?}.map(&:first)
|
34
|
+
ys = the_points.each_with_index.select {|n, i| i.odd?}.map(&:first)
|
35
|
+
xs.zip(ys)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
27
40
|
attr_reader :points
|
28
41
|
|
29
42
|
def initialize(points: [])
|
@@ -32,12 +45,14 @@ module PerfectShape
|
|
32
45
|
|
33
46
|
# Sets points, normalizing to an Array of Arrays of (x,y) pairs as BigDecimal
|
34
47
|
def points=(the_points)
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
48
|
+
the_points = MultiPoint.normalize_point_array(the_points)
|
49
|
+
@points = the_points.map do |pair|
|
50
|
+
[
|
51
|
+
pair.first.is_a?(BigDecimal) ? pair.first : BigDecimal(pair.first.to_s),
|
52
|
+
pair.last.is_a?(BigDecimal) ? pair.last : BigDecimal(pair.last.to_s)
|
53
|
+
]
|
39
54
|
end
|
40
|
-
@points
|
55
|
+
@points
|
41
56
|
end
|
42
57
|
|
43
58
|
def min_x
|