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