perfect-shape 0.3.2 → 0.3.3

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