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
@@ -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
|