perfect-shape 0.3.1 → 0.3.5

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.
@@ -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
@@ -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
@@ -75,6 +75,8 @@ module PerfectShape
75
75
  include MultiPoint
76
76
  include Equalizer.new(:points)
77
77
 
78
+ OUTLINE_MINIMUM_DISTANCE_THRESHOLD = BigDecimal('0.001')
79
+
78
80
  # Checks if cubic bézier curve contains point (two-number Array or x, y args)
79
81
  #
80
82
  # @param x The X coordinate of the point to test.
@@ -83,24 +85,30 @@ module PerfectShape
83
85
  # @return {@code true} if the point lies within the bound of
84
86
  # the cubic bézier curve, {@code false} if the point lies outside of the
85
87
  # cubic bézier curve's bounds.
86
- def contain?(x_or_point, y = nil)
88
+ def contain?(x_or_point, y = nil, outline: false, distance_tolerance: 0)
87
89
  x, y = normalize_point(x_or_point, y)
88
90
  return unless x && y
89
91
 
90
- # Either x or y was infinite or NaN.
91
- # A NaN always produces a negative response to any test
92
- # and Infinity values cannot be "inside" any path so
93
- # they should return false as well.
94
- return false if (!(x * 0.0 + y * 0.0 == 0.0))
95
- # We count the "Y" crossings to determine if the point is
96
- # inside the curve bounded by its closing line.
97
- x1 = points[0][0]
98
- y1 = points[0][1]
99
- x2 = points[3][0]
100
- y2 = points[3][1]
101
- line = PerfectShape::Line.new(points: [[x1, y1], [x2, y2]])
102
- crossings = line.point_crossings(x, y) + point_crossings(x, y);
103
- (crossings & 1) == 1
92
+ if outline
93
+ distance_tolerance = BigDecimal(distance_tolerance.to_s)
94
+ minimum_distance_threshold = OUTLINE_MINIMUM_DISTANCE_THRESHOLD + distance_tolerance
95
+ point_distance(x, y, minimum_distance_threshold: minimum_distance_threshold) < minimum_distance_threshold
96
+ else
97
+ # Either x or y was infinite or NaN.
98
+ # A NaN always produces a negative response to any test
99
+ # and Infinity values cannot be "inside" any path so
100
+ # they should return false as well.
101
+ return false if (!(x * 0.0 + y * 0.0 == 0.0))
102
+ # We count the "Y" crossings to determine if the point is
103
+ # inside the curve bounded by its closing line.
104
+ x1 = points[0][0]
105
+ y1 = points[0][1]
106
+ x2 = points[3][0]
107
+ y2 = points[3][1]
108
+ line = PerfectShape::Line.new(points: [[x1, y1], [x2, y2]])
109
+ crossings = line.point_crossings(x, y) + point_crossings(x, y)
110
+ (crossings & 1) == 1
111
+ end
104
112
  end
105
113
 
106
114
  # Calculates the number of times the cubic bézier curve
@@ -116,5 +124,92 @@ module PerfectShape
116
124
  return unless x && y
117
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)
118
126
  end
127
+
128
+ # The center point on the outline of the curve
129
+ def curve_center_point
130
+ subdivisions.last.points[0]
131
+ end
132
+
133
+ # The center point x on the outline of the curve
134
+ def curve_center_x
135
+ subdivisions.last.points[0][0]
136
+ end
137
+
138
+ # The center point y on the outline of the curve
139
+ def curve_center_y
140
+ subdivisions.last.points[0][1]
141
+ end
142
+
143
+ # Subdivides CubicBezierCurve exactly at its curve center
144
+ # returning 2 CubicBezierCurve's as a two-element Array by default
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
+
153
+ x1 = points[0][0]
154
+ y1 = points[0][1]
155
+ ctrlx1 = points[1][0]
156
+ ctrly1 = points[1][1]
157
+ ctrlx2 = points[2][0]
158
+ ctrly2 = points[2][1]
159
+ x2 = points[3][0]
160
+ y2 = points[3][1]
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
179
+ default_subdivisions
180
+ else
181
+ default_subdivisions.map { |curve| curve.subdivisions(level) }.flatten
182
+ end
183
+ end
184
+
185
+ def point_distance(x_or_point, y = nil, minimum_distance_threshold: OUTLINE_MINIMUM_DISTANCE_THRESHOLD)
186
+ x, y = normalize_point(x_or_point, y)
187
+ return unless x && y
188
+
189
+ point = Point.new(x, y)
190
+ current_curve = self
191
+ minimum_distance = point.point_distance(curve_center_point)
192
+ last_minimum_distance = minimum_distance + 1 # start bigger to ensure going through loop once at least
193
+ while minimum_distance >= minimum_distance_threshold && minimum_distance < last_minimum_distance
194
+ curve1, curve2 = current_curve.subdivisions
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)
199
+ last_minimum_distance = minimum_distance
200
+ if distance1 < distance2
201
+ minimum_distance = distance1
202
+ current_curve = curve1
203
+ else
204
+ minimum_distance = distance2
205
+ current_curve = curve2
206
+ end
207
+ end
208
+ if minimum_distance < minimum_distance_threshold
209
+ minimum_distance
210
+ else
211
+ last_minimum_distance
212
+ end
213
+ end
119
214
  end
120
215
  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
@@ -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
@@ -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,17 +212,17 @@ 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)
215
+ def contain?(x_or_point, y = nil, outline: true, distance_tolerance: 0)
216
216
  x, y = 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)
222
+ def point_distance(x_or_point, y = nil)
223
223
  x, y = 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)
@@ -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
@@ -37,7 +37,13 @@ module PerfectShape
37
37
  ys = the_points.each_with_index.select {|n, i| i.odd?}.map(&:first)
38
38
  the_points = xs.zip(ys)
39
39
  end
40
- @points = the_points.map {|pair| [BigDecimal(pair.first.to_s), BigDecimal(pair.last.to_s)]}
40
+ @points = the_points.map do |pair|
41
+ [
42
+ pair.first.is_a?(BigDecimal) ? pair.first : BigDecimal(pair.first.to_s),
43
+ pair.last.is_a?(BigDecimal) ? pair.last : BigDecimal(pair.last.to_s)
44
+ ]
45
+ end
46
+ @points
41
47
  end
42
48
 
43
49
  def min_x
@@ -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
@@ -114,21 +114,26 @@ module PerfectShape
114
114
  # @return true if the point lies within the bound of
115
115
  # the path or false if the point lies outside of the
116
116
  # path's bounds.
117
- def contain?(x_or_point, y = nil)
117
+ def contain?(x_or_point, y = nil, outline: false, distance_tolerance: 0)
118
118
  x, y = normalize_point(x_or_point, y)
119
119
  return unless x && y
120
- if (x * 0.0 + y * 0.0) == 0.0
121
- # N * 0.0 is 0.0 only if N is finite.
122
- # Here we know that both x and y are finite.
123
- return false if shapes.count < 2
124
- mask = winding_rule == :wind_non_zero ? -1 : 1
125
- (point_crossings(x, y) & mask) != 0
120
+
121
+ if outline
122
+ disconnected_shapes.any? {|shape| shape.contain?(x, y, outline: true, distance_tolerance: distance_tolerance) }
126
123
  else
127
- # Either x or y was infinite or NaN.
128
- # A NaN always produces a negative response to any test
129
- # and Infinity values cannot be "inside" any path so
130
- # they should return false as well.
131
- false
124
+ if (x * 0.0 + y * 0.0) == 0.0
125
+ # N * 0.0 is 0.0 only if N is finite.
126
+ # Here we know that both x and y are finite.
127
+ return false if shapes.count < 2
128
+ mask = winding_rule == :wind_non_zero ? -1 : 1
129
+ (point_crossings(x, y) & mask) != 0
130
+ else
131
+ # Either x or y was infinite or NaN.
132
+ # A NaN always produces a negative response to any test
133
+ # and Infinity values cannot be "inside" any path so
134
+ # they should return false as well.
135
+ false
136
+ end
132
137
  end
133
138
  end
134
139
 
@@ -218,5 +223,50 @@ module PerfectShape
218
223
  end
219
224
  crossings
220
225
  end
226
+
227
+ # Disconnected shapes have their start point filled in
228
+ # so that each shape does not depend on the previous shape
229
+ # to determine its start point.
230
+ #
231
+ # Also, if a point is followed by a non-point shape, it is removed
232
+ # since it is augmented to the following shape as its start point.
233
+ #
234
+ # Lastly, if the path is closed, an extra shape is
235
+ # added to represent the line connecting the last point to the first
236
+ def disconnected_shapes
237
+ initial_point = start_point = @shapes.first.to_a.map {|n| BigDecimal(n.to_s)}
238
+ final_point = nil
239
+ the_disconnected_shapes = @shapes.drop(1).map do |shape|
240
+ case shape
241
+ when Point
242
+ disconnected_shape = Point.new(*shape.to_a)
243
+ start_point = shape.to_a
244
+ final_point = disconnected_shape.to_a
245
+ nil
246
+ when Array
247
+ disconnected_shape = Point.new(*shape.map {|n| BigDecimal(n.to_s)})
248
+ start_point = shape.map {|n| BigDecimal(n.to_s)}
249
+ final_point = disconnected_shape.to_a
250
+ nil
251
+ when Line
252
+ disconnected_shape = Line.new(points: [start_point.to_a, shape.points.last])
253
+ start_point = shape.points.last.to_a
254
+ final_point = disconnected_shape.points.last.to_a
255
+ disconnected_shape
256
+ when QuadraticBezierCurve
257
+ disconnected_shape = QuadraticBezierCurve.new(points: [start_point.to_a] + shape.points)
258
+ start_point = shape.points.last.to_a
259
+ final_point = disconnected_shape.points.last.to_a
260
+ disconnected_shape
261
+ when CubicBezierCurve
262
+ disconnected_shape = CubicBezierCurve.new(points: [start_point.to_a] + shape.points)
263
+ start_point = shape.points.last.to_a
264
+ final_point = disconnected_shape.points.last.to_a
265
+ disconnected_shape
266
+ end
267
+ end
268
+ the_disconnected_shapes << Line.new(points: [final_point, initial_point]) if closed?
269
+ the_disconnected_shapes.compact
270
+ end
221
271
  end
222
272
  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
@@ -27,10 +27,10 @@ module PerfectShape
27
27
  class Point < Shape
28
28
  class << self
29
29
  def point_distance(x, y, px, py)
30
- x = BigDecimal(x.to_s)
31
- y = BigDecimal(y.to_s)
32
- px = BigDecimal(px.to_s)
33
- py = BigDecimal(py.to_s)
30
+ x = x.is_a?(BigDecimal) ? x : BigDecimal(x.to_s)
31
+ y = y.is_a?(BigDecimal) ? y : BigDecimal(y.to_s)
32
+ px = px.is_a?(BigDecimal) ? px : BigDecimal(px.to_s)
33
+ py = py.is_a?(BigDecimal) ? py : BigDecimal(py.to_s)
34
34
  BigDecimal(Math.sqrt((px - x)**2 + (py - y)**2).to_s)
35
35
  end
36
36
  end
@@ -67,7 +67,7 @@ module PerfectShape
67
67
  #
68
68
  # @return {@code true} if the point is close enough within distance tolerance,
69
69
  # {@code false} if the point is too far.
70
- def contain?(x_or_point, y = nil, outline: false, distance_tolerance: 0)
70
+ def contain?(x_or_point, y = nil, outline: true, distance_tolerance: 0)
71
71
  x, y = normalize_point(x_or_point, y)
72
72
  return unless x && y
73
73
  distance_tolerance = BigDecimal(distance_tolerance.to_s)
@@ -77,6 +77,7 @@ module PerfectShape
77
77
  def point_distance(x_or_point, y = nil)
78
78
  x, y = normalize_point(x_or_point, y)
79
79
  return unless x && y
80
+
80
81
  Point.point_distance(self.x, self.y, x, y)
81
82
  end
82
83
 
@@ -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
@@ -41,9 +41,7 @@ module PerfectShape
41
41
  x, y = normalize_point(x_or_point, y)
42
42
  return unless x && y
43
43
  if outline
44
- points.zip(points.rotate(1)).any? do |point1, point2|
45
- Line.new(points: [[point1.first, point1.last], [point2.first, point2.last]]).contain?(x, y, distance_tolerance: distance_tolerance)
46
- end
44
+ edges.any? { |edge| edge.contain?(x, y, distance_tolerance: distance_tolerance) }
47
45
  else
48
46
  npoints = points.count
49
47
  xpoints = points.map(&:first)
@@ -117,5 +115,11 @@ module PerfectShape
117
115
  (hits & 1) != 0
118
116
  end
119
117
  end
118
+
119
+ def edges
120
+ points.zip(points.rotate(1)).map do |point1, point2|
121
+ Line.new(points: [[point1.first, point1.last], [point2.first, point2.last]])
122
+ end
123
+ end
120
124
  end
121
125
  end