perfect-shape 0.3.1 → 0.3.5

Sign up to get free protection for your applications and to get access to all the features.
@@ -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