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.
@@ -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
@@ -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.0
156
- outside_radius_difference = inside_radius_difference = outside_inside_radius_difference / 2.0
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
@@ -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
@@ -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
- shapes.any? {|shape| shape.contain?(x, y) }
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
- point_segment_distance(x, y, minimum_distance_threshold: minimum_distance_threshold) < minimum_distance_threshold
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
- # `number` parameter may be specified as an even number in case more
145
- # subdivisions are needed. If an odd number is given, it is rounded
146
- # up to the closest even number above it (e.g. 3 becomes 4).
147
- def subdivisions(number = 2)
148
- number = (number.to_i / 2.0).ceil*2
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.0
158
- centery = (ctrly1 + ctrly2) / 2.0
159
- ctrlx1 = (x1 + ctrlx1) / 2.0
160
- ctrly1 = (y1 + ctrly1) / 2.0
161
- ctrlx2 = (x2 + ctrlx2) / 2.0
162
- ctrly2 = (y2 + ctrly2) / 2.0
163
- ctrlx12 = (ctrlx1 + centerx) / 2.0
164
- ctrly12 = (ctrly1 + centery) / 2.0
165
- ctrlx21 = (ctrlx2 + centerx) / 2.0
166
- ctrly21 = (ctrly2 + centery) / 2.0
167
- centerx = (ctrlx12 + ctrlx21) / 2.0
168
- centery = (ctrly12 + ctrly21) / 2.0
169
- default_subdivisions = [
170
- CubicBezierCurve.new(points: [x1, y1, ctrlx1, ctrly1, ctrlx12, ctrly12, centerx, centery]),
171
- CubicBezierCurve.new(points: [centerx, centery, ctrlx21, ctrly21, ctrlx2, ctrly2, x2, y2])
172
- ]
173
- if number > 2
174
- default_subdivisions.map { |curve| curve.subdivisions(number - 2) }.flatten
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 point_segment_distance(x_or_point, y = nil, minimum_distance_threshold: OUTLINE_MINIMUM_DISTANCE_THRESHOLD)
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
- distance1 = point.point_distance(curve1.curve_center_point)
191
- distance2 = point.point_distance(curve2.curve_center_point)
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)
@@ -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 point_segment_distance_square(x1, y1,
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 point_segment_distance(x1, y1,
180
+ def point_distance(x1, y1,
181
181
  x2, y2,
182
182
  px, py)
183
- BigDecimal(::Math.sqrt(point_segment_distance_square(x1, y1, x2, y2, px, py)).to_s)
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: false, distance_tolerance: 0)
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
- point_segment_distance(x, y) <= distance_tolerance
219
+ point_distance(x, y) <= distance_tolerance
220
220
  end
221
221
 
222
- def point_segment_distance(x_or_point, y = nil)
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.point_segment_distance(points[0][0], points[0][1], points[1][0], points[1][1], x, y)
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
@@ -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
- unless the_points.first.is_a?(Array)
36
- xs = the_points.each_with_index.select {|n, i| i.even?}.map(&:first)
37
- ys = the_points.each_with_index.select {|n, i| i.odd?}.map(&:first)
38
- the_points = xs.zip(ys)
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 = the_points.map {|pair| [BigDecimal(pair.first.to_s), BigDecimal(pair.last.to_s)]}
55
+ @points
41
56
  end
42
57
 
43
58
  def min_x