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 +4 -4
- data/CHANGELOG.md +12 -1
- data/LICENSE.txt +1 -1
- data/README.md +23 -7
- data/VERSION +1 -1
- data/lib/perfect-shape.rb +1 -1
- data/lib/perfect_shape/arc.rb +1 -1
- data/lib/perfect_shape/circle.rb +1 -1
- data/lib/perfect_shape/composite_shape.rb +1 -1
- data/lib/perfect_shape/cubic_bezier_curve.rb +11 -9
- data/lib/perfect_shape/ellipse.rb +1 -1
- data/lib/perfect_shape/line.rb +1 -1
- data/lib/perfect_shape/math.rb +21 -0
- data/lib/perfect_shape/multi_point.rb +1 -1
- data/lib/perfect_shape/path.rb +1 -1
- data/lib/perfect_shape/point.rb +1 -1
- data/lib/perfect_shape/point_location.rb +1 -1
- data/lib/perfect_shape/polygon.rb +8 -4
- data/lib/perfect_shape/quadratic_bezier_curve.rb +172 -86
- data/lib/perfect_shape/rectangle.rb +11 -5
- data/lib/perfect_shape/rectangular_shape.rb +1 -1
- data/lib/perfect_shape/shape.rb +1 -1
- data/lib/perfect_shape/square.rb +1 -1
- data/perfect-shape.gemspec +2 -2
- metadata +1 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 42bff3742b2697349882d865b7c5da9c06ce4a259850996bb92f291e8a74130e
|
4
|
+
data.tar.gz: 5a2f007129f1e0f483a48ea607a7ee3e44cbdf01b400a656f406b412dc8e9524
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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(
|
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
data/README.md
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
# Perfect Shape 0.3.
|
1
|
+
# Perfect Shape 0.3.3
|
2
2
|
## Geometric Algorithms
|
3
3
|
[](http://badge.fury.io/rb/perfect-shape)
|
4
4
|
[](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.
|
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.
|
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
|
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(
|
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.
|
1
|
+
0.3.3
|
data/lib/perfect-shape.rb
CHANGED
data/lib/perfect_shape/arc.rb
CHANGED
data/lib/perfect_shape/circle.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
|
@@ -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
|
-
#
|
145
|
-
#
|
146
|
-
#
|
147
|
-
|
148
|
-
|
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
|
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
|
|
data/lib/perfect_shape/line.rb
CHANGED
data/lib/perfect_shape/math.rb
CHANGED
@@ -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
|
#
|
data/lib/perfect_shape/path.rb
CHANGED
data/lib/perfect_shape/point.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
|
@@ -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
|
-
|
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
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
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
|
-
|
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
|
data/lib/perfect_shape/shape.rb
CHANGED
data/lib/perfect_shape/square.rb
CHANGED
data/perfect-shape.gemspec
CHANGED
@@ -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.
|
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.
|
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]
|