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 +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
|
[![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.
|
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]
|