perfect-shape 0.3.2 → 0.3.3

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8f9bd6a8a2c7c3d3ec4ec3fd3e5b311a9eec08fa63e35666547534a2e19273c6
4
- data.tar.gz: 4dbcd6b139e118c555d50681efe4e770cf796774a904459994a64c2bb19d5140
3
+ metadata.gz: 42bff3742b2697349882d865b7c5da9c06ce4a259850996bb92f291e8a74130e
4
+ data.tar.gz: 5a2f007129f1e0f483a48ea607a7ee3e44cbdf01b400a656f406b412dc8e9524
5
5
  SHA512:
6
- metadata.gz: e30baa5bb362e5e540af6f67ba9c99ff0f5f0469f48a724823a76522111d6ea86e1bce6cb6b273b056cc9bf7e8f92bd667103aa348abbc237e4ffec082154434
7
- data.tar.gz: 7627cabd6ee3ef671ed802d6846ad1c192065cc4a5bf6d00062a7d8da7d3376c3d84b836664413a5033644dfa839d19a8634badf851e4aa050e6f03cdef031bc
6
+ metadata.gz: 9bf87bebe682dfbab8a4b082caa321ac0b7051510d86e946e3f214337e818a18da7cee51bc12432510567deaa68f65a69cb3c7da10b7c9153045803e1d25f0b7
7
+ data.tar.gz: fa32aa79d500055b81b572e17ded38429503a3757430bba1c0fea021853e4377d7e025071cf52ed8c2f965c8bc8ecd35601276f2a1bcda847202c6f49c8422a0
data/CHANGELOG.md CHANGED
@@ -1,10 +1,21 @@
1
1
  # Change Log
2
2
 
3
+ ## 0.3.3
4
+
5
+ - Check point containment in quadratic bezier curve outline with distance tolerance (new method signature: `PerfectShape::QuadraticBezierCurve#contain?(x_or_point, y = nil, outline: false, distance_tolerance: 0)`)
6
+ - `PerfectShape::QuadraticBezierCurve#curve_center_point`, `PerfectShape::QuadraticBezierCurve#curve_center_x`, `PerfectShape::QuadraticBezierCurve#curve_center_y`
7
+ - `PerfectShape::QuadraticBezierCurve#subdivisions(level=1)`
8
+ - `PerfectShape::QuadraticBezierCurve#point_segment_distance(x_or_point, y = nil, minimum_distance_threshold: OUTLINE_MINIMUM_DISTANCE_THRESHOLD)`
9
+ - `PerfectShape::Polygon#edges` returns edges of polygon as `PerfectShape::Line` objects
10
+ - `PerfectShape::Rectangle#edges` returns edges of rectangle as `PerfectShape::Line` objects
11
+ - `PerfectShape::Square#edges` returns edges of square as `PerfectShape::Line` objects
12
+ - Rename `number` arg to `level` in `CubicBezierCurve#subdivisions(level=1)`, making it signify the level of subdivision recursion to perform.
13
+
3
14
  ## 0.3.2
4
15
 
5
16
  - Check point containment in cubic bezier curve outline with distance tolerance (new method signature: `PerfectShape::CubicBezierCurve#contain?(x_or_point, y = nil, outline: false, distance_tolerance: 0)`)
6
17
  - `PerfectShape::CubicBezierCurve#curve_center_point`, `PerfectShape::CubicBezierCurve#curve_center_x`, `PerfectShape::CubicBezierCurve#curve_center_y`
7
- - `PerfectShape::CubicBezierCurve#subdivisions(number=2)`
18
+ - `PerfectShape::CubicBezierCurve#subdivisions(level=1)`
8
19
  - `PerfectShape::CubicBezierCurve#point_segment_distance(x_or_point, y = nil, minimum_distance_threshold: OUTLINE_MINIMUM_DISTANCE_THRESHOLD)`
9
20
 
10
21
  ## 0.3.1
data/LICENSE.txt CHANGED
@@ -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
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # Perfect Shape 0.3.2
1
+ # Perfect Shape 0.3.3
2
2
  ## Geometric Algorithms
3
3
  [![Gem Version](https://badge.fury.io/rb/perfect-shape.svg)](http://badge.fury.io/rb/perfect-shape)
4
4
  [![Test](https://github.com/AndyObtiva/perfect-shape/actions/workflows/ruby.yml/badge.svg)](https://github.com/AndyObtiva/perfect-shape/actions/workflows/ruby.yml)
@@ -14,13 +14,13 @@ To ensure high accuracy, this library does all its mathematical operations with
14
14
  Run:
15
15
 
16
16
  ```
17
- gem install perfect-shape -v 0.3.2
17
+ gem install perfect-shape -v 0.3.3
18
18
  ```
19
19
 
20
20
  Or include in Bundler `Gemfile`:
21
21
 
22
22
  ```ruby
23
- gem 'perfect-shape', '~> 0.3.2'
23
+ gem 'perfect-shape', '~> 0.3.3'
24
24
  ```
25
25
 
26
26
  And, run:
@@ -38,7 +38,7 @@ Module
38
38
  - `::degrees_to_radians(angle)`: converts degrees to radians
39
39
  - `::radians_to_degrees(angle)`: converts radians to degrees
40
40
  - `::normalize_degrees(angle)`: normalizes the specified angle into the range -180 to 180.
41
- - `::ieee_remainder(x, y)` (alias: `ieee754_remainder`): [IEEE 754-1985 Remainder](https://en.wikipedia.org/wiki/IEEE_754-1985) (different from standard % modulo operator as it operates on floats and could return a negative result)
41
+ - `::ieee_remainder(x, y)` (alias: `ieee754_remainder`): [IEEE 754-1985 Remainder](https://en.wikipedia.org/wiki/IEEE_754-1985) (different from standard `%` modulo operator as it operates on floats and could return a negative result)
42
42
 
43
43
  ### `PerfectShape::Shape`
44
44
 
@@ -195,7 +195,12 @@ Includes `PerfectShape::MultiPoint`
195
195
  - `#center_y`: center y
196
196
  - `#bounding_box`: bounding box is a rectangle with x = min x, y = min y, and width/height of shape (bounding box only guarantees that the shape is within it, but it might be bigger than the shape)
197
197
  - `#==(other)`: Returns `true` if equal to `other` or `false` otherwise
198
- - `#contain?(x_or_point, y=nil)`: checks if point is inside
198
+ - `#contain?(x_or_point, y=nil, outline: false, distance_tolerance: 0)`: checks if point is inside when `outline` is `false` or if point is on the outline when `outline` is `true`. `distance_tolerance` can be used as a fuzz factor when `outline` is `true`, for example, to help GUI users mouse-click-select a quadratic bezier curve shape from its outline more successfully
199
+ - `#curve_center_point`: point at the center of the curve (not the center of the bounding box area like `center_x` and `center_y`)
200
+ - `#curve_center_x`: point x coordinate at the center of the curve (not the center of the bounding box area like `center_x` and `center_y`)
201
+ - `#curve_center_y`: point y coordinate at the center of the curve (not the center of the bounding box area like `center_x` and `center_y`)
202
+ - `#subdivisions(level=1)`: subdivides quadratic bezier curve at its center into into 2 quadratic bezier curves by default, or more if `level` of recursion is specified. The resulting number of subdivisions is `2` to the power of `level`.
203
+ - `#point_segment_distance(x_or_point, y=nil, minimum_distance_threshold: OUTLINE_MINIMUM_DISTANCE_THRESHOLD)`: calculates distance from point to curve segment. It does so by subdividing curve into smaller curves and checking against the curve center points until the distance is less than `minimum_distance_threshold`, to avoid being an overly costly operation.
199
204
 
200
205
  Example:
201
206
 
@@ -206,6 +211,14 @@ shape = PerfectShape::QuadraticBezierCurve.new(points: [[200, 150], [270, 320],
206
211
 
207
212
  shape.contain?(270, 220) # => true
208
213
  shape.contain?([270, 220]) # => true
214
+ shape.contain?(270, 220, outline: true) # => false
215
+ shape.contain?([270, 220], outline: true) # => false
216
+ shape.contain?(280, 235, outline: true) # => true
217
+ shape.contain?([280, 235], outline: true) # => true
218
+ shape.contain?(281, 235, outline: true) # => false
219
+ shape.contain?([281, 235], outline: true) # => false
220
+ shape.contain?(281, 235, outline: true, distance_tolerance: 1) # => true
221
+ shape.contain?([281, 235], outline: true, distance_tolerance: 1) # => true
209
222
  ```
210
223
 
211
224
  ### `PerfectShape::CubicBezierCurve`
@@ -234,7 +247,7 @@ Includes `PerfectShape::MultiPoint`
234
247
  - `#curve_center_point`: point at the center of the curve (not the center of the bounding box area like `center_x` and `center_y`)
235
248
  - `#curve_center_x`: point x coordinate at the center of the curve (not the center of the bounding box area like `center_x` and `center_y`)
236
249
  - `#curve_center_y`: point y coordinate at the center of the curve (not the center of the bounding box area like `center_x` and `center_y`)
237
- - `#subdivisions(number=2)`: subdivides cubic bezier curve at its center into into 2 cubic bezier curves by default, or more if `number` is specified. `number` must be an even number, or it will be rounded up to the closest even number.
250
+ - `#subdivisions(level=1)`: subdivides cubic bezier curve at its center into into 2 cubic bezier curves by default, or more if `level` of recursion is specified. The resulting number of subdivisions is `2` to the power of `level`.
238
251
  - `#point_segment_distance(x_or_point, y=nil, minimum_distance_threshold: OUTLINE_MINIMUM_DISTANCE_THRESHOLD)`: calculates distance from point to curve segment. It does so by subdividing curve into smaller curves and checking against the curve center points until the distance is less than `minimum_distance_threshold`, to avoid being an overly costly operation.
239
252
 
240
253
  Example:
@@ -280,6 +293,7 @@ Includes `PerfectShape::RectangularShape`
280
293
  - `#bounding_box`: bounding box is a rectangle with x = min x, y = min y, and width/height of shape
281
294
  - `#==(other)`: Returns `true` if equal to `other` or `false` otherwise
282
295
  - `#contain?(x_or_point, y=nil, outline: false, distance_tolerance: 0)`: checks if point is inside when `outline` is `false` or if point is on the outline when `outline` is `true`. `distance_tolerance` can be used as a fuzz factor when `outline` is `true`, for example, to help GUI users mouse-click-select a rectangle shape from its outline more successfully
296
+ - `#edges`: edges of rectangle as `PerfectShape::Line` objects
283
297
 
284
298
  Example:
285
299
 
@@ -323,6 +337,7 @@ Extends `PerfectShape::Rectangle`
323
337
  - `#bounding_box`: bounding box is a rectangle with x = min x, y = min y, and width/height of shape
324
338
  - `#==(other)`: Returns `true` if equal to `other` or `false` otherwise
325
339
  - `#contain?(x_or_point, y=nil, outline: false, distance_tolerance: 0)`: checks if point is inside when `outline` is `false` or if point is on the outline when `outline` is `true`. `distance_tolerance` can be used as a fuzz factor when `outline` is `true`, for example, to help GUI users mouse-click-select a square shape from its outline more successfully
340
+ - `#edges`: edges of square as `PerfectShape::Line` objects
326
341
 
327
342
  Example:
328
343
 
@@ -613,6 +628,7 @@ A polygon can be thought of as a special case of [path](#perfectshapepath) that
613
628
  - `#bounding_box`: bounding box is a rectangle with x = min x, y = min y, and width/height of shape
614
629
  - `#==(other)`: Returns `true` if equal to `other` or `false` otherwise
615
630
  - `#contain?(x_or_point, y=nil, outline: false, distance_tolerance: 0)`: When `outline` is `false`, it checks if point is inside using the [Ray Casting Algorithm](https://en.wikipedia.org/wiki/Point_in_polygon) (aka [Even-Odd Rule](https://en.wikipedia.org/wiki/Even%E2%80%93odd_rule)). Otherwise, when `outline` is `true`, it checks if point is on the outline. `distance_tolerance` can be used as a fuzz factor when `outline` is `true`, for example, to help GUI users mouse-click-select a polygon shape from its outline more successfully
631
+ - `#edges`: edges of polygon as `PerfectShape::Line` objects
616
632
 
617
633
  Example:
618
634
 
@@ -756,5 +772,5 @@ shape.contain?([170, 190]) # => true
756
772
 
757
773
  [MIT](LICENSE.txt)
758
774
 
759
- Copyright (c) 2021 Andy Maleh. See
775
+ Copyright (c) 2021-2022 Andy Maleh. See
760
776
  [LICENSE.txt](LICENSE.txt) for further details.
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.3.2
1
+ 0.3.3
data/lib/perfect-shape.rb CHANGED
@@ -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
@@ -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
@@ -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
@@ -141,11 +141,13 @@ module PerfectShape
141
141
 
142
142
  # Subdivides CubicBezierCurve exactly at its curve center
143
143
  # 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
144
+ #
145
+ # Optional `level` parameter specifies the level of recursions to
146
+ # perform to get more subdivisions. The number of resulting
147
+ # subdivisions is 2 to the power of `level` (e.g. 2 subdivisions
148
+ # for level=1, 4 subdivisions for level=2, and 8 subdivisions for level=3)
149
+ def subdivisions(level = 1)
150
+ level -= 1 # consume 1 level
149
151
  x1 = points[0][0]
150
152
  y1 = points[0][1]
151
153
  ctrlx1 = points[1][0]
@@ -170,10 +172,10 @@ module PerfectShape
170
172
  CubicBezierCurve.new(points: [x1, y1, ctrlx1, ctrly1, ctrlx12, ctrly12, centerx, centery]),
171
173
  CubicBezierCurve.new(points: [centerx, centery, ctrlx21, ctrly21, ctrlx2, ctrly2, x2, y2])
172
174
  ]
173
- if number > 2
174
- default_subdivisions.map { |curve| curve.subdivisions(number - 2) }.flatten
175
- else
175
+ if level == 0
176
176
  default_subdivisions
177
+ else
178
+ default_subdivisions.map { |curve| curve.subdivisions(level) }.flatten
177
179
  end
178
180
  end
179
181
 
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -69,6 +69,8 @@ module PerfectShape
69
69
  include MultiPoint
70
70
  include Equalizer.new(:points)
71
71
 
72
+ OUTLINE_MINIMUM_DISTANCE_THRESHOLD = BigDecimal('0.001')
73
+
72
74
  # Checks if quadratic bézier curve contains point (two-number Array or x, y args)
73
75
  #
74
76
  # @param x The X coordinate of the point to test.
@@ -77,7 +79,7 @@ module PerfectShape
77
79
  # @return {@code true} if the point lies within the bound of
78
80
  # the quadratic bézier curve, {@code false} if the point lies outside of the
79
81
  # quadratic bézier curve's bounds.
80
- def contain?(x_or_point, y = nil)
82
+ def contain?(x_or_point, y = nil, outline: false, distance_tolerance: 0)
81
83
  x, y = normalize_point(x_or_point, y)
82
84
  return unless x && y
83
85
 
@@ -87,90 +89,95 @@ module PerfectShape
87
89
  yc = points[1][1]
88
90
  x2 = points[2][0]
89
91
  y2 = points[2][1]
90
-
91
- # We have a convex shape bounded by quad curve Pc(t)
92
- # and ine Pl(t).
93
- #
94
- # P1 = (x1, y1) - start point of curve
95
- # P2 = (x2, y2) - end point of curve
96
- # Pc = (xc, yc) - control point
97
- #
98
- # Pq(t) = P1*(1 - t)^2 + 2*Pc*t*(1 - t) + P2*t^2 =
99
- # = (P1 - 2*Pc + P2)*t^2 + 2*(Pc - P1)*t + P1
100
- # Pl(t) = P1*(1 - t) + P2*t
101
- # t = [0:1]
102
- #
103
- # P = (x, y) - point of interest
104
- #
105
- # Let's look at second derivative of quad curve equation:
106
- #
107
- # Pq''(t) = 2 * (P1 - 2 * Pc + P2) = Pq''
108
- # It's constant vector.
109
- #
110
- # Let's draw a line through P to be parallel to this
111
- # vector and find the intersection of the quad curve
112
- # and the line.
113
- #
114
- # Pq(t) is point of intersection if system of equations
115
- # below has the solution.
116
- #
117
- # L(s) = P + Pq''*s == Pq(t)
118
- # Pq''*s + (P - Pq(t)) == 0
119
- #
120
- # | xq''*s + (x - xq(t)) == 0
121
- # | yq''*s + (y - yq(t)) == 0
122
- #
123
- # This system has the solution if rank of its matrix equals to 1.
124
- # That is, determinant of the matrix should be zero.
125
- #
126
- # (y - yq(t))*xq'' == (x - xq(t))*yq''
127
- #
128
- # Let's solve this equation with 't' variable.
129
- # Also let kx = x1 - 2*xc + x2
130
- # ky = y1 - 2*yc + y2
131
- #
132
- # t0q = (1/2)*((x - x1)*ky - (y - y1)*kx) /
133
- # ((xc - x1)*ky - (yc - y1)*kx)
134
- #
135
- # Let's do the same for our line Pl(t):
136
- #
137
- # t0l = ((x - x1)*ky - (y - y1)*kx) /
138
- # ((x2 - x1)*ky - (y2 - y1)*kx)
139
- #
140
- # It's easy to check that t0q == t0l. This fact means
141
- # we can compute t0 only one time.
142
- #
143
- # In case t0 < 0 or t0 > 1, we have an intersections outside
144
- # of shape bounds. So, P is definitely out of shape.
145
- #
146
- # In case t0 is inside [0:1], we should calculate Pq(t0)
147
- # and Pl(t0). We have three points for now, and all of them
148
- # lie on one line. So, we just need to detect, is our point
149
- # of interest between points of intersections or not.
150
- #
151
- # If the denominator in the t0q and t0l equations is
152
- # zero, then the points must be collinear and so the
153
- # curve is degenerate and encloses no area. Thus the
154
- # result is false.
155
- kx = x1 - 2 * xc + x2;
156
- ky = y1 - 2 * yc + y2;
157
- dx = x - x1;
158
- dy = y - y1;
159
- dxl = x2 - x1;
160
- dyl = y2 - y1;
161
-
162
- t0 = (dx * ky - dy * kx) / (dxl * ky - dyl * kx)
163
- return false if (t0 < 0 || t0 > 1 || t0 != t0)
164
-
165
- xb = kx * t0 * t0 + 2 * (xc - x1) * t0 + x1;
166
- yb = ky * t0 * t0 + 2 * (yc - y1) * t0 + y1;
167
- xl = dxl * t0 + x1;
168
- yl = dyl * t0 + y1;
169
-
170
- (x >= xb && x < xl) ||
171
- (x >= xl && x < xb) ||
172
- (y >= yb && y < yl) ||
173
- (y >= yl && y < yb)
92
+
93
+ if outline
94
+ minimum_distance_threshold = OUTLINE_MINIMUM_DISTANCE_THRESHOLD + distance_tolerance
95
+ point_segment_distance(x, y, minimum_distance_threshold: minimum_distance_threshold) < minimum_distance_threshold
96
+ else
97
+ # We have a convex shape bounded by quad curve Pc(t)
98
+ # and ine Pl(t).
99
+ #
100
+ # P1 = (x1, y1) - start point of curve
101
+ # P2 = (x2, y2) - end point of curve
102
+ # Pc = (xc, yc) - control point
103
+ #
104
+ # Pq(t) = P1*(1 - t)^2 + 2*Pc*t*(1 - t) + P2*t^2 =
105
+ # = (P1 - 2*Pc + P2)*t^2 + 2*(Pc - P1)*t + P1
106
+ # Pl(t) = P1*(1 - t) + P2*t
107
+ # t = [0:1]
108
+ #
109
+ # P = (x, y) - point of interest
110
+ #
111
+ # Let's look at second derivative of quad curve equation:
112
+ #
113
+ # Pq''(t) = 2 * (P1 - 2 * Pc + P2) = Pq''
114
+ # It's constant vector.
115
+ #
116
+ # Let's draw a line through P to be parallel to this
117
+ # vector and find the intersection of the quad curve
118
+ # and the line.
119
+ #
120
+ # Pq(t) is point of intersection if system of equations
121
+ # below has the solution.
122
+ #
123
+ # L(s) = P + Pq''*s == Pq(t)
124
+ # Pq''*s + (P - Pq(t)) == 0
125
+ #
126
+ # | xq''*s + (x - xq(t)) == 0
127
+ # | yq''*s + (y - yq(t)) == 0
128
+ #
129
+ # This system has the solution if rank of its matrix equals to 1.
130
+ # That is, determinant of the matrix should be zero.
131
+ #
132
+ # (y - yq(t))*xq'' == (x - xq(t))*yq''
133
+ #
134
+ # Let's solve this equation with 't' variable.
135
+ # Also let kx = x1 - 2*xc + x2
136
+ # ky = y1 - 2*yc + y2
137
+ #
138
+ # t0q = (1/2)*((x - x1)*ky - (y - y1)*kx) /
139
+ # ((xc - x1)*ky - (yc - y1)*kx)
140
+ #
141
+ # Let's do the same for our line Pl(t):
142
+ #
143
+ # t0l = ((x - x1)*ky - (y - y1)*kx) /
144
+ # ((x2 - x1)*ky - (y2 - y1)*kx)
145
+ #
146
+ # It's easy to check that t0q == t0l. This fact means
147
+ # we can compute t0 only one time.
148
+ #
149
+ # In case t0 < 0 or t0 > 1, we have an intersections outside
150
+ # of shape bounds. So, P is definitely out of shape.
151
+ #
152
+ # In case t0 is inside [0:1], we should calculate Pq(t0)
153
+ # and Pl(t0). We have three points for now, and all of them
154
+ # lie on one line. So, we just need to detect, is our point
155
+ # of interest between points of intersections or not.
156
+ #
157
+ # If the denominator in the t0q and t0l equations is
158
+ # zero, then the points must be collinear and so the
159
+ # curve is degenerate and encloses no area. Thus the
160
+ # result is false.
161
+ kx = x1 - 2 * xc + x2;
162
+ ky = y1 - 2 * yc + y2;
163
+ dx = x - x1;
164
+ dy = y - y1;
165
+ dxl = x2 - x1;
166
+ dyl = y2 - y1;
167
+
168
+ t0 = (dx * ky - dy * kx) / (dxl * ky - dyl * kx)
169
+ return false if (t0 < 0 || t0 > 1 || t0 != t0)
170
+
171
+ xb = kx * t0 * t0 + 2 * (xc - x1) * t0 + x1;
172
+ yb = ky * t0 * t0 + 2 * (yc - y1) * t0 + y1;
173
+ xl = dxl * t0 + x1;
174
+ yl = dyl * t0 + y1;
175
+
176
+ (x >= xb && x < xl) ||
177
+ (x >= xl && x < xb) ||
178
+ (y >= yb && y < yl) ||
179
+ (y >= yl && y < yb)
180
+ end
174
181
  end
175
182
 
176
183
  # Calculates the number of times the quad
@@ -186,5 +193,84 @@ module PerfectShape
186
193
  return unless x && y
187
194
  QuadraticBezierCurve.point_crossings(points[0][0], points[0][1], points[1][0], points[1][1], points[2][0], points[2][1], x, y, level)
188
195
  end
196
+
197
+
198
+ # The center point on the outline of the curve
199
+ def curve_center_point
200
+ subdivisions.last.points[0]
201
+ end
202
+
203
+ # The center point x on the outline of the curve
204
+ def curve_center_x
205
+ subdivisions.last.points[0][0]
206
+ end
207
+
208
+ # The center point y on the outline of the curve
209
+ def curve_center_y
210
+ subdivisions.last.points[0][1]
211
+ end
212
+
213
+ # Subdivides QuadraticBezierCurve exactly at its curve center
214
+ # returning 2 QuadraticBezierCurve's as a two-element Array by default
215
+ #
216
+ # Optional `level` parameter specifies the level of recursions to
217
+ # perform to get more subdivisions. The number of resulting
218
+ # subdivisions is 2 to the power of `level` (e.g. 2 subdivisions
219
+ # for level=1, 4 subdivisions for level=2, and 8 subdivisions for level=3)
220
+ def subdivisions(level = 1)
221
+ level -= 1 # consume 1 level
222
+
223
+ x1 = points[0][0]
224
+ y1 = points[0][1]
225
+ ctrlx = points[1][0]
226
+ ctrly = points[1][1]
227
+ x2 = points[2][0]
228
+ y2 = points[2][1]
229
+ ctrlx1 = (x1 + ctrlx) / 2.0
230
+ ctrly1 = (y1 + ctrly) / 2.0
231
+ ctrlx2 = (x2 + ctrlx) / 2.0
232
+ ctrly2 = (y2 + ctrly) / 2.0
233
+ centerx = (ctrlx1 + ctrlx2) / 2.0
234
+ centery = (ctrly1 + ctrly2) / 2.0
235
+
236
+ default_subdivisions = [
237
+ QuadraticBezierCurve.new(points: [x1, y1, ctrlx1, ctrly1, centerx, centery]),
238
+ QuadraticBezierCurve.new(points: [centerx, centery, ctrlx2, ctrly2, x2, y2])
239
+ ]
240
+
241
+ if level == 0
242
+ default_subdivisions
243
+ else
244
+ default_subdivisions.map { |curve| curve.subdivisions(level) }.flatten
245
+ end
246
+ end
247
+
248
+ def point_segment_distance(x_or_point, y = nil, minimum_distance_threshold: OUTLINE_MINIMUM_DISTANCE_THRESHOLD)
249
+ x, y = normalize_point(x_or_point, y)
250
+ return unless x && y
251
+
252
+ point = Point.new(x, y)
253
+ current_curve = self
254
+ minimum_distance = point.point_distance(curve_center_point)
255
+ last_minimum_distance = minimum_distance + 1 # start bigger to ensure going through loop once at least
256
+ while minimum_distance >= minimum_distance_threshold && minimum_distance < last_minimum_distance
257
+ curve1, curve2 = current_curve.subdivisions
258
+ distance1 = point.point_distance(curve1.curve_center_point)
259
+ distance2 = point.point_distance(curve2.curve_center_point)
260
+ last_minimum_distance = minimum_distance
261
+ if distance1 < distance2
262
+ minimum_distance = distance1
263
+ current_curve = curve1
264
+ else
265
+ minimum_distance = distance2
266
+ current_curve = curve2
267
+ end
268
+ end
269
+ if minimum_distance < minimum_distance_threshold
270
+ minimum_distance
271
+ else
272
+ last_minimum_distance
273
+ end
274
+ end
189
275
  end
190
276
  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
@@ -41,13 +41,19 @@ module PerfectShape
41
41
  x, y = normalize_point(x_or_point, y)
42
42
  return unless x && y
43
43
  if outline
44
- Line.new(points: [[self.x, self.y], [self.x + width, self.y]]).contain?(x, y, distance_tolerance: distance_tolerance) or
45
- Line.new(points: [[self.x + width, self.y], [self.x + width, self.y + height]]).contain?(x, y, distance_tolerance: distance_tolerance) or
46
- Line.new(points: [[self.x + width, self.y + height], [self.x, self.y + height]]).contain?(x, y, distance_tolerance: distance_tolerance) or
47
- Line.new(points: [[self.x, self.y + height], [self.x, self.y]]).contain?(x, y, distance_tolerance: distance_tolerance)
44
+ edges.any? { |edge| edge.contain?(x, y, distance_tolerance: distance_tolerance) }
48
45
  else
49
46
  x.between?(self.x, self.x + width) && y.between?(self.y, self.y + height)
50
47
  end
51
48
  end
49
+
50
+ def edges
51
+ [
52
+ Line.new(points: [[self.x, self.y], [self.x + width, self.y]]),
53
+ Line.new(points: [[self.x + width, self.y], [self.x + width, self.y + height]]),
54
+ Line.new(points: [[self.x + width, self.y + height], [self.x, self.y + height]]),
55
+ Line.new(points: [[self.x, self.y + height], [self.x, self.y]])
56
+ ]
57
+ end
52
58
  end
53
59
  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
@@ -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
@@ -2,11 +2,11 @@
2
2
  # DO NOT EDIT THIS FILE DIRECTLY
3
3
  # Instead, edit Juwelier::Tasks in Rakefile, and run 'rake gemspec'
4
4
  # -*- encoding: utf-8 -*-
5
- # stub: perfect-shape 0.3.2 ruby lib
5
+ # stub: perfect-shape 0.3.3 ruby lib
6
6
 
7
7
  Gem::Specification.new do |s|
8
8
  s.name = "perfect-shape".freeze
9
- s.version = "0.3.2"
9
+ s.version = "0.3.3"
10
10
 
11
11
  s.required_rubygems_version = Gem::Requirement.new(">= 0".freeze) if s.respond_to? :required_rubygems_version=
12
12
  s.require_paths = ["lib".freeze]
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: perfect-shape
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.2
4
+ version: 0.3.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andy Maleh