perfect-shape 0.3.3 → 0.5.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 +37 -3
- data/README.md +138 -32
- data/VERSION +1 -1
- data/lib/perfect-shape.rb +1 -1
- data/lib/perfect_shape/affine_transform.rb +235 -0
- data/lib/perfect_shape/arc.rb +4 -4
- data/lib/perfect_shape/composite_shape.rb +4 -3
- data/lib/perfect_shape/cubic_bezier_curve.rb +42 -37
- data/lib/perfect_shape/ellipse.rb +1 -2
- data/lib/perfect_shape/line.rb +37 -12
- data/lib/perfect_shape/multi_point.rb +20 -5
- data/lib/perfect_shape/path.rb +64 -15
- data/lib/perfect_shape/point.rb +23 -7
- data/lib/perfect_shape/polygon.rb +2 -2
- data/lib/perfect_shape/quadratic_bezier_curve.rb +14 -13
- data/lib/perfect_shape/rectangle.rb +42 -2
- data/lib/perfect_shape/shape.rb +6 -15
- data/perfect-shape.gemspec +5 -4
- metadata +11 -8
@@ -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
@@ -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
|
@@ -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
|
@@ -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
|
@@ -148,6 +149,7 @@ module PerfectShape
|
|
148
149
|
# for level=1, 4 subdivisions for level=2, and 8 subdivisions for level=3)
|
149
150
|
def subdivisions(level = 1)
|
150
151
|
level -= 1 # consume 1 level
|
152
|
+
|
151
153
|
x1 = points[0][0]
|
152
154
|
y1 = points[0][1]
|
153
155
|
ctrlx1 = points[1][0]
|
@@ -156,22 +158,23 @@ module PerfectShape
|
|
156
158
|
ctrly2 = points[2][1]
|
157
159
|
x2 = points[3][0]
|
158
160
|
y2 = points[3][1]
|
159
|
-
centerx = (ctrlx1 + ctrlx2) / 2
|
160
|
-
centery = (ctrly1 + ctrly2) / 2
|
161
|
-
ctrlx1 = (x1 + ctrlx1) / 2
|
162
|
-
ctrly1 = (y1 + ctrly1) / 2
|
163
|
-
ctrlx2 = (x2 + ctrlx2) / 2
|
164
|
-
ctrly2 = (y2 + ctrly2) / 2
|
165
|
-
ctrlx12 = (ctrlx1 + centerx) / 2
|
166
|
-
ctrly12 = (ctrly1 + centery) / 2
|
167
|
-
ctrlx21 = (ctrlx2 + centerx) / 2
|
168
|
-
ctrly21 = (ctrly2 + centery) / 2
|
169
|
-
centerx = (ctrlx12 + ctrlx21) / 2
|
170
|
-
centery = (ctrly12 + ctrly21) / 2
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
]
|
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
|
+
|
175
178
|
if level == 0
|
176
179
|
default_subdivisions
|
177
180
|
else
|
@@ -179,8 +182,8 @@ module PerfectShape
|
|
179
182
|
end
|
180
183
|
end
|
181
184
|
|
182
|
-
def
|
183
|
-
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)
|
184
187
|
return unless x && y
|
185
188
|
|
186
189
|
point = Point.new(x, y)
|
@@ -189,8 +192,10 @@ module PerfectShape
|
|
189
192
|
last_minimum_distance = minimum_distance + 1 # start bigger to ensure going through loop once at least
|
190
193
|
while minimum_distance >= minimum_distance_threshold && minimum_distance < last_minimum_distance
|
191
194
|
curve1, curve2 = current_curve.subdivisions
|
192
|
-
|
193
|
-
|
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)
|
194
199
|
last_minimum_distance = minimum_distance
|
195
200
|
if distance1 < distance2
|
196
201
|
minimum_distance = distance1
|
@@ -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
@@ -20,10 +20,11 @@
|
|
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'
|
25
|
+
require 'perfect_shape/rectangle'
|
24
26
|
|
25
27
|
module PerfectShape
|
26
|
-
# Mostly ported from java.awt.geom: https://docs.oracle.com/javase/8/docs/api/java/awt/geom/Line2D.html
|
27
28
|
class Line < Shape
|
28
29
|
class << self
|
29
30
|
# Returns an indicator of where the specified point (px,py) lies with respect to the line segment from
|
@@ -104,7 +105,7 @@ module PerfectShape
|
|
104
105
|
# measured against the specified line segment
|
105
106
|
# @return a double value that is the square of the distance from the
|
106
107
|
# specified point to the specified line segment.
|
107
|
-
def
|
108
|
+
def point_distance_square(x1, y1,
|
108
109
|
x2, y2,
|
109
110
|
px, py)
|
110
111
|
x1 = BigDecimal(x1.to_s)
|
@@ -177,10 +178,10 @@ module PerfectShape
|
|
177
178
|
# measured against the specified line segment
|
178
179
|
# @return a double value that is the distance from the specified point
|
179
180
|
# to the specified line segment.
|
180
|
-
def
|
181
|
+
def point_distance(x1, y1,
|
181
182
|
x2, y2,
|
182
183
|
px, py)
|
183
|
-
BigDecimal(::Math.sqrt(
|
184
|
+
BigDecimal(::Math.sqrt(point_distance_square(x1, y1, x2, y2, px, py)).to_s)
|
184
185
|
end
|
185
186
|
|
186
187
|
# Calculates the number of times the line from (x1,y1) to (x2,y2)
|
@@ -212,21 +213,21 @@ module PerfectShape
|
|
212
213
|
# @return {@code true} if the point lies within the bound of
|
213
214
|
# the line, {@code false} if the point lies outside of the
|
214
215
|
# line's bounds.
|
215
|
-
def contain?(x_or_point, y = nil, outline:
|
216
|
-
x, y = normalize_point(x_or_point, y)
|
216
|
+
def contain?(x_or_point, y = nil, outline: true, distance_tolerance: 0)
|
217
|
+
x, y = Point.normalize_point(x_or_point, y)
|
217
218
|
return unless x && y
|
218
219
|
distance_tolerance = BigDecimal(distance_tolerance.to_s)
|
219
|
-
|
220
|
+
point_distance(x, y) <= distance_tolerance
|
220
221
|
end
|
221
222
|
|
222
|
-
def
|
223
|
-
x, y = normalize_point(x_or_point, y)
|
223
|
+
def point_distance(x_or_point, y = nil)
|
224
|
+
x, y = Point.normalize_point(x_or_point, y)
|
224
225
|
return unless x && y
|
225
|
-
Line.
|
226
|
+
Line.point_distance(points[0][0], points[0][1], points[1][0], points[1][1], x, y)
|
226
227
|
end
|
227
228
|
|
228
229
|
def relative_counterclockwise(x_or_point, y = nil)
|
229
|
-
x, y = normalize_point(x_or_point, y)
|
230
|
+
x, y = Point.normalize_point(x_or_point, y)
|
230
231
|
return unless x && y
|
231
232
|
Line.relative_counterclockwise(points[0][0], points[0][1], points[1][0], points[1][1], x, y)
|
232
233
|
end
|
@@ -237,9 +238,33 @@ module PerfectShape
|
|
237
238
|
# +1 is returned for a crossing where the Y coordinate is increasing
|
238
239
|
# -1 is returned for a crossing where the Y coordinate is decreasing
|
239
240
|
def point_crossings(x_or_point, y = nil)
|
240
|
-
x, y = normalize_point(x_or_point, y)
|
241
|
+
x, y = Point.normalize_point(x_or_point, y)
|
241
242
|
return unless x && y
|
242
243
|
Line.point_crossings(points[0][0], points[0][1], points[1][0], points[1][1], x, y)
|
243
244
|
end
|
245
|
+
|
246
|
+
def intersect?(rectangle)
|
247
|
+
x1 = points[0][0]
|
248
|
+
y1 = points[0][1]
|
249
|
+
x2 = points[1][0]
|
250
|
+
y2 = points[1][1]
|
251
|
+
out1 = out2 = nil
|
252
|
+
return true if (out2 = rectangle.out_state(x2, y2)) == 0
|
253
|
+
while (out1 = rectangle.out_state(x1, y1)) != 0
|
254
|
+
return false if (out1 & out2) != 0
|
255
|
+
if (out1 & (Rectangle::OUT_LEFT | Rectangle::OUT_RIGHT)) != 0
|
256
|
+
x = rectangle.x
|
257
|
+
x += rectangle.width if (out1 & Rectangle::OUT_RIGHT) != 0
|
258
|
+
y1 = y1 + (x - x1) * (y2 - y1) / (x2 - x1)
|
259
|
+
x1 = x
|
260
|
+
else
|
261
|
+
y = rectangle.y
|
262
|
+
y += rectangle.height if (out1 & Rectangle::OUT_BOTTOM) != 0
|
263
|
+
x1 = x1 + (y - y1) * (x2 - x1) / (y2 - y1)
|
264
|
+
y1 = y
|
265
|
+
end
|
266
|
+
end
|
267
|
+
true
|
268
|
+
end
|
244
269
|
end
|
245
270
|
end
|
@@ -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
|