perfect-shape 0.5.0 → 0.5.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +29 -2
- data/README.md +18 -4
- data/VERSION +1 -1
- data/lib/perfect_shape/cubic_bezier_curve.rb +113 -0
- data/lib/perfect_shape/line.rb +87 -15
- data/lib/perfect_shape/point.rb +4 -0
- data/lib/perfect_shape/quadratic_bezier_curve.rb +283 -11
- data/lib/perfect_shape/rectangle.rb +21 -0
- data/lib/perfect_shape/square.rb +6 -2
- data/perfect-shape.gemspec +3 -3
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 98226e1aac4fbac06b162630c2dbaa5ca2f749d24590cc0bd0cca9924f760635
|
4
|
+
data.tar.gz: 3d7d5be2905838db9ff1b3f2876e8437e0df90d9ac6b2316c1347aa4f58db626
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 686a5054b13bd3adf1176e779e80d2ef758c4cf9cc25625633151628a724c1b802c03847301a447fb14155b570ee0fe0fdf8fe4d357b92186bdd7df83d6de240
|
7
|
+
data.tar.gz: e05c4a2eb8d2efa7feef615dc5dd3d522778d8aa98b60845726de0d3a0e68bf84ffa173984d0a907401838a16432b725ed1e0ac5ac1d4239ed66596181dac324
|
data/CHANGELOG.md
CHANGED
@@ -1,9 +1,36 @@
|
|
1
1
|
# Change Log
|
2
2
|
|
3
|
+
## 0.5.4
|
4
|
+
|
5
|
+
- `PerfectShape::Rectangle#intersect?(rectangle)`
|
6
|
+
- `PerfectShape::Square#intersect?(rectangle)`
|
7
|
+
- `PerfectShape::Rectangle#empty?`
|
8
|
+
- `PerfectShape::Square#empty?`
|
9
|
+
- `PerfectShape::Square#size`/`PerfectShape::Square#size=` aliases for `#length`/`#length=`
|
10
|
+
|
11
|
+
## 0.5.3
|
12
|
+
|
13
|
+
- `PerfectShape::CubicBezierCurve#intersect?(rectangle)`
|
14
|
+
- `PerfectShape::CubicBezierCurve#rectangle_crossings(rectangle)`
|
15
|
+
- `PerfectShape::CubicBezierCurve#rect_crossings(rxmin, rymin, rxmax, rymax, level, crossings = 0)`
|
16
|
+
- `PerfectShape::Line#rect_crossings(rxmin, rymin, rxmax, rymax, crossings = 0)`
|
17
|
+
|
18
|
+
## 0.5.2
|
19
|
+
|
20
|
+
- `PerfectShape::QuadraticBezierCurve#intersect?(rectangle)`
|
21
|
+
- `PerfectShape::QuadraticBezierCurve::tag(coord, low, high)`
|
22
|
+
- `PerfectShape::QuadraticBezierCurve::eqn(val, c1, cp, c2)`
|
23
|
+
- `PerfectShape::QuadraticBezierCurve::solve_quadratic(eqn)`
|
24
|
+
- `PerfectShape::QuadraticBezierCurve::eval_quadratic(vals, num, include0, include1, inflect, c1, ctrl, c2)`
|
25
|
+
|
26
|
+
## 0.5.1
|
27
|
+
|
28
|
+
- `PerfectShape::Point#intersect?(rectangle)` (equivalent to `PerfectShape::Rectangle#contain?(point)`)
|
29
|
+
|
3
30
|
## 0.5.0
|
4
31
|
|
5
|
-
- `Line#intersect?(rectangle)`
|
6
|
-
- `Rectangle#out_state(x_or_point, y = nil)`
|
32
|
+
- `PerfectShape::Line#intersect?(rectangle)`
|
33
|
+
- `PerfectShape::Rectangle#out_state(x_or_point, y = nil)`
|
7
34
|
|
8
35
|
## 0.4.0
|
9
36
|
|
data/README.md
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
# Perfect Shape 0.5.
|
1
|
+
# Perfect Shape 0.5.4
|
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.5.
|
17
|
+
gem install perfect-shape -v 0.5.4
|
18
18
|
```
|
19
19
|
|
20
20
|
Or include in Bundler `Gemfile`:
|
21
21
|
|
22
22
|
```ruby
|
23
|
-
gem 'perfect-shape', '~> 0.5.
|
23
|
+
gem 'perfect-shape', '~> 0.5.4'
|
24
24
|
```
|
25
25
|
|
26
26
|
And, run:
|
@@ -177,6 +177,7 @@ Points are simply represented by an `Array` of `[x,y]` coordinates when used wit
|
|
177
177
|
- `#bounding_box`: bounding box is a rectangle with x = min x, y = min y, and width/height of shape
|
178
178
|
- `#==(other)`: Returns `true` if equal to `other` or `false` otherwise
|
179
179
|
- `#contain?(x_or_point, y=nil, outline: true, distance_tolerance: 0)`: checks if point matches self, with a distance tolerance (0 by default). Distance tolerance provides a fuzz factor that for example enables GUI users to mouse-click-select a point shape more successfully. `outline` option makes no difference on point
|
180
|
+
- `#intersect?(rectangle)`: Returns `true` if intersecting with interior of rectangle or `false` otherwise. This is useful for GUI optimization checks of whether a shape appears in a GUI viewport rectangle and needs redrawing
|
180
181
|
- `#point_distance(x_or_point, y=nil)`: Returns the distance from a point to another point
|
181
182
|
|
182
183
|
Example:
|
@@ -223,6 +224,7 @@ Includes `PerfectShape::MultiPoint`
|
|
223
224
|
- `#intersect?(rectangle)`: Returns `true` if intersecting with interior of rectangle or `false` otherwise. This is useful for GUI optimization checks of whether a shape appears in a GUI viewport rectangle and needs redrawing
|
224
225
|
- `#relative_counterclockwise(x_or_point, y=nil)`: Returns an indicator of where the specified point (px,py) lies with respect to the line segment from (x1,y1) to (x2,y2). The return value can be either 1, -1, or 0 and indicates in which direction the specified line must pivot around its first end point, (x1,y1), in order to point at the specified point (px,py). A return value of 1 indicates that the line segment must turn in the direction that takes the positive X axis towards the negative Y axis. In the default coordinate system, this direction is counterclockwise. A return value of -1 indicates that the line segment must turn in the direction that takes the positive X axis towards the positive Y axis. In the default coordinate system, this direction is clockwise. A return value of 0 indicates that the point lies exactly on the line segment. Note that an indicator value of 0 is rare and not useful for determining collinearity because of floating point rounding issues. If the point is colinear with the line segment, but not between the end points, then the value will be -1 if the point lies “beyond (x1,y1)” or 1 if the point lies “beyond (x2,y2)”.
|
225
226
|
- `#point_distance(x_or_point, y=nil)`: Returns the distance from a point to a line segment.
|
227
|
+
- `#rect_crossings(rxmin, rymin, rxmax, rymax, crossings = 0)`: rectangle crossings (adds to crossings arg)
|
226
228
|
|
227
229
|
Example:
|
228
230
|
|
@@ -249,6 +251,10 @@ Includes `PerfectShape::MultiPoint`
|
|
249
251
|
|
250
252
|
![quadratic_bezier_curve](https://raw.githubusercontent.com/AndyObtiva/perfect-shape/master/images/quadratic_bezier_curve.png)
|
251
253
|
|
254
|
+
- `::tag(coord, low, high)`: Determine where coord lies with respect to the range from low to high. It is assumed that low < high. The return value is one of the 5 values BELOW, LOWEDGE, INSIDE, HIGHEDGE, or ABOVE.
|
255
|
+
- `::eqn(val, c1, cp, c2)`: Fill an array with the coefficients of the parametric equation in t, ready for solving against val with solve_quadratic. We currently have: val = Py(t) = C1*(1-t)^2 + 2*CP*t*(1-t) + C2*t^2 = C1 - 2*C1*t + C1*t^2 + 2*CP*t - 2*CP*t^2 + C2*t^2 = C1 + (2*CP - 2*C1)*t + (C1 - 2*CP + C2)*t^2; 0 = (C1 - val) + (2*CP - 2*C1)*t + (C1 - 2*CP + C2)*t^2; 0 = C + Bt + At^2; C = C1 - val; B = 2*CP - 2*C1; A = C1 - 2*CP + C2
|
256
|
+
- `::solve_quadratic(eqn)`: Solves the quadratic whose coefficients are in the eqn array and places the non-complex roots into the res array, returning the number of roots. The quadratic solved is represented by the equation: <pre>eqn = {C, B, A}; ax^2 + bx + c = 0</pre> A return value of {@code -1} is used to distinguish a constant equation, which might be always 0 or never 0, from an equation that has no zeroes.
|
257
|
+
- `::eval_quadratic(vals, num, include0, include1, inflect, c1, ctrl, c2)`: Evaluate the t values in the first num slots of the vals[] array and place the evaluated values back into the same array. Only evaluate t values that are within the range <, >, including the 0 and 1 ends of the range iff the include0 or include1 booleans are true. If an "inflection" equation is handed in, then any points which represent a point of inflection for that quadratic equation are also ignored.
|
252
258
|
- `::new(points: [])`: constructs a quadratic bézier curve with three `points` (start point, control point, and end point) as `Array` of `Array`s of `[x,y]` pairs or flattened `Array` of alternating x and y coordinates
|
253
259
|
- `#points`: points (start point, control point, and end point)
|
254
260
|
- `#min_x`: min x
|
@@ -263,6 +269,7 @@ Includes `PerfectShape::MultiPoint`
|
|
263
269
|
- `#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)
|
264
270
|
- `#==(other)`: Returns `true` if equal to `other` or `false` otherwise
|
265
271
|
- `#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
|
272
|
+
- `#intersect?(rectangle)`: Returns `true` if intersecting with interior of rectangle or `false` otherwise. This is useful for GUI optimization checks of whether a shape appears in a GUI viewport rectangle and needs redrawing
|
266
273
|
- `#curve_center_point`: point at the center of the curve outline (not the center of the bounding box area like `center_x` and `center_y`)
|
267
274
|
- `#curve_center_x`: point x coordinate at the center of the curve outline (not the center of the bounding box area like `center_x` and `center_y`)
|
268
275
|
- `#curve_center_y`: point y coordinate at the center of the curve outline (not the center of the bounding box area like `center_x` and `center_y`)
|
@@ -312,11 +319,14 @@ Includes `PerfectShape::MultiPoint`
|
|
312
319
|
- `#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)
|
313
320
|
- `#==(other)`: Returns `true` if equal to `other` or `false` otherwise
|
314
321
|
- `#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 cubic bezier curve shape from its outline more successfully
|
322
|
+
- `#intersect?(rectangle)`: Returns `true` if intersecting with interior of rectangle or `false` otherwise. This is useful for GUI optimization checks of whether a shape appears in a GUI viewport rectangle and needs redrawing
|
315
323
|
- `#curve_center_point`: point at the center of the curve outline (not the center of the bounding box area like `center_x` and `center_y`)
|
316
324
|
- `#curve_center_x`: point x coordinate at the center of the curve outline (not the center of the bounding box area like `center_x` and `center_y`)
|
317
325
|
- `#curve_center_y`: point y coordinate at the center of the curve outline (not the center of the bounding box area like `center_x` and `center_y`)
|
318
326
|
- `#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`.
|
319
327
|
- `#point_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.
|
328
|
+
- `#rectangle_crossings(rectangle)`: rectangle crossings (used to determine rectangle interior intersection), optimized to check if line represented by cubic bezier curve crosses the rectangle first, and if not then perform expensive check with `#rect_crossings`
|
329
|
+
- `#rect_crossings(rxmin, rymin, rxmax, rymax, level, crossings = 0)`: rectangle crossings (adds to crossings arg)
|
320
330
|
|
321
331
|
Example:
|
322
332
|
|
@@ -362,8 +372,10 @@ Includes `PerfectShape::RectangularShape`
|
|
362
372
|
- `#bounding_box`: bounding box is a rectangle with x = min x, y = min y, and width/height of shape
|
363
373
|
- `#==(other)`: Returns `true` if equal to `other` or `false` otherwise
|
364
374
|
- `#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
|
375
|
+
- `#intersect?(rectangle)`: Returns `true` if intersecting with interior of rectangle or `false` otherwise. This is useful for GUI optimization checks of whether a shape appears in a GUI viewport rectangle and needs redrawing
|
365
376
|
- `#edges`: edges of rectangle as `PerfectShape::Line` objects
|
366
377
|
- `#out_state(x_or_point, y = nil)`: Returns "out state" of specified point (x,y) (whether it lies to the left, right, top, bottom of rectangle). If point is outside rectangle, it returns a bit mask combination of `Rectangle::OUT_LEFT`, `Rectangle::OUT_RIGHT`, `Rectangle::OUT_TOP`, or `Rectangle::OUT_BOTTOM`. Otherwise, it returns `0` if point is inside the rectangle.
|
378
|
+
- `#empty?`: Returns `true` if width or height are 0 (or negative) and `false` otherwise
|
367
379
|
|
368
380
|
Example:
|
369
381
|
|
@@ -392,7 +404,7 @@ Extends `PerfectShape::Rectangle`
|
|
392
404
|
|
393
405
|
![square](https://raw.githubusercontent.com/AndyObtiva/perfect-shape/master/images/square.png)
|
394
406
|
|
395
|
-
- `::new(x: 0, y: 0, length: 1)
|
407
|
+
- `::new(x: 0, y: 0, length: 1)` (`length` alias: `size`): constructs a square
|
396
408
|
- `#x`: top-left x
|
397
409
|
- `#y`: top-left y
|
398
410
|
- `#length`: length
|
@@ -408,7 +420,9 @@ Extends `PerfectShape::Rectangle`
|
|
408
420
|
- `#bounding_box`: bounding box is a rectangle with x = min x, y = min y, and width/height of shape
|
409
421
|
- `#==(other)`: Returns `true` if equal to `other` or `false` otherwise
|
410
422
|
- `#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
|
423
|
+
- `#intersect?(rectangle)`: Returns `true` if intersecting with interior of rectangle or `false` otherwise. This is useful for GUI optimization checks of whether a shape appears in a GUI viewport rectangle and needs redrawing
|
411
424
|
- `#edges`: edges of square as `PerfectShape::Line` objects
|
425
|
+
- `#empty?`: Returns `true` if length is 0 (or negative) and `false` otherwise
|
412
426
|
|
413
427
|
Example:
|
414
428
|
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.5.
|
1
|
+
0.5.4
|
@@ -211,5 +211,118 @@ module PerfectShape
|
|
211
211
|
last_minimum_distance
|
212
212
|
end
|
213
213
|
end
|
214
|
+
|
215
|
+
def intersect?(rectangle)
|
216
|
+
x = rectangle.x
|
217
|
+
y = rectangle.y
|
218
|
+
w = rectangle.width
|
219
|
+
h = rectangle.height
|
220
|
+
|
221
|
+
# Trivially reject non-existant rectangles
|
222
|
+
return false if w <= 0 || h <= 0
|
223
|
+
|
224
|
+
num_crossings = rectangle_crossings(rectangle)
|
225
|
+
# the intended return value is
|
226
|
+
# num_crossings != 0 || num_crossings == Rectangle::RECT_INTERSECTS
|
227
|
+
# but if (num_crossings != 0) num_crossings == INTERSECTS won't matter
|
228
|
+
# and if !(num_crossings != 0) then num_crossings == 0, so
|
229
|
+
# num_crossings != RECT_INTERSECT
|
230
|
+
num_crossings != 0
|
231
|
+
end
|
232
|
+
|
233
|
+
def rectangle_crossings(rectangle)
|
234
|
+
x = rectangle.x
|
235
|
+
y = rectangle.y
|
236
|
+
w = rectangle.width
|
237
|
+
h = rectangle.height
|
238
|
+
x1 = points[0][0]
|
239
|
+
y1 = points[0][1]
|
240
|
+
x2 = points[3][0]
|
241
|
+
y2 = points[3][1]
|
242
|
+
|
243
|
+
crossings = 0
|
244
|
+
if !(x1 == x2 && y1 == y2)
|
245
|
+
line = PerfectShape::Line.new(points: [[x1, y1], [x2, y2]])
|
246
|
+
crossings = line.rect_crossings(x, y, x+w, y+h, crossings)
|
247
|
+
return crossings if crossings == Rectangle::RECT_INTERSECTS
|
248
|
+
end
|
249
|
+
# we call this with the curve's direction reversed, because we wanted
|
250
|
+
# to call rectCrossingsForLine first, because it's cheaper.
|
251
|
+
rect_crossings(x, y, x+w, y+h, 0, crossings)
|
252
|
+
end
|
253
|
+
|
254
|
+
# Accumulate the number of times the cubic crosses the shadow
|
255
|
+
# extending to the right of the rectangle. See the comment
|
256
|
+
# for the RECT_INTERSECTS constant for more complete details.
|
257
|
+
#
|
258
|
+
# crossings arg is the initial crossings value to add to (useful
|
259
|
+
# in cases where you want to accumulate crossings from multiple
|
260
|
+
# shapes)
|
261
|
+
def rect_crossings(rxmin, rymin, rxmax, rymax, level, crossings = 0)
|
262
|
+
x0 = points[0][0]
|
263
|
+
y0 = points[0][1]
|
264
|
+
xc0 = points[1][0]
|
265
|
+
yc0 = points[1][1]
|
266
|
+
xc1 = points[2][0]
|
267
|
+
yc1 = points[2][1]
|
268
|
+
x1 = points[3][0]
|
269
|
+
y1 = points[3][1]
|
270
|
+
|
271
|
+
return crossings if y0 >= rymax && yc0 >= rymax && yc1 >= rymax && y1 >= rymax
|
272
|
+
return crossings if y0 <= rymin && yc0 <= rymin && yc1 <= rymin && y1 <= rymin
|
273
|
+
return crossings if x0 <= rxmin && xc0 <= rxmin && xc1 <= rxmin && x1 <= rxmin
|
274
|
+
if x0 >= rxmax && xc0 >= rxmax && xc1 >= rxmax && x1 >= rxmax
|
275
|
+
# Cubic is entirely to the right of the rect
|
276
|
+
# and the vertical range of the 4 Y coordinates of the cubic
|
277
|
+
# overlaps the vertical range of the rect by a non-empty amount
|
278
|
+
# We now judge the crossings solely based on the line segment
|
279
|
+
# connecting the endpoints of the cubic.
|
280
|
+
# Note that we may have 0, 1, or 2 crossings as the control
|
281
|
+
# points may be causing the Y range intersection while the
|
282
|
+
# two endpoints are entirely above or below.
|
283
|
+
if y0 < y1
|
284
|
+
# y-increasing line segment...
|
285
|
+
crossings += 1 if (y0 <= rymin && y1 > rymin)
|
286
|
+
crossings += 1 if (y0 < rymax && y1 >= rymax)
|
287
|
+
elsif y1 < y0
|
288
|
+
# y-decreasing line segment...
|
289
|
+
crossings -= 1 if (y1 <= rymin && y0 > rymin)
|
290
|
+
crossings -= 1 if (y1 < rymax && y0 >= rymax)
|
291
|
+
end
|
292
|
+
return crossings
|
293
|
+
end
|
294
|
+
# The intersection of ranges is more complicated
|
295
|
+
# First do trivial INTERSECTS rejection of the cases
|
296
|
+
# where one of the endpoints is inside the rectangle.
|
297
|
+
return Rectangle::RECT_INTERSECTS if ((x0 > rxmin && x0 < rxmax && y0 > rymin && y0 < rymax) ||
|
298
|
+
(x1 > rxmin && x1 < rxmax && y1 > rymin && y1 < rymax))
|
299
|
+
|
300
|
+
# Otherwise, subdivide and look for one of the cases above.
|
301
|
+
# double precision only has 52 bits of mantissa
|
302
|
+
return PerfectShape::Line.new(points: [[x0, y0], [x1, y1]]).rect_crossings(rxmin, rymin, rxmax, rymax, crossings) if (level > 52)
|
303
|
+
xmid = BigDecimal((xc0 + xc1).to_s) / 2
|
304
|
+
ymid = BigDecimal((yc0 + yc1).to_s) / 2
|
305
|
+
xc0 = BigDecimal((x0 + xc0).to_s) / 2
|
306
|
+
yc0 = BigDecimal((y0 + yc0).to_s) / 2
|
307
|
+
xc1 = BigDecimal((xc1 + x1).to_s) / 2
|
308
|
+
yc1 = BigDecimal((yc1 + y1).to_s) / 2
|
309
|
+
xc0m = BigDecimal((xc0 + xmid).to_s) / 2
|
310
|
+
yc0m = BigDecimal((yc0 + ymid).to_s) / 2
|
311
|
+
xmc1 = BigDecimal((xmid + xc1).to_s) / 2
|
312
|
+
ymc1 = BigDecimal((ymid + yc1).to_s) / 2
|
313
|
+
xmid = BigDecimal((xc0m + xmc1).to_s) / 2
|
314
|
+
ymid = BigDecimal((yc0m + ymc1).to_s) / 2
|
315
|
+
# [xy]mid are NaN if any of [xy]c0m or [xy]mc1 are NaN
|
316
|
+
# [xy]c0m or [xy]mc1 are NaN if any of [xy][c][01] are NaN
|
317
|
+
# These values are also NaN if opposing infinities are added
|
318
|
+
return 0 if xmid.nan? || ymid.nan?
|
319
|
+
cubic1 = CubicBezierCurve.new(points: [[x0, y0], [xc0, yc0], [xc0m, yc0m], [xmid, ymid]])
|
320
|
+
crossings = cubic1.rect_crossings(rxmin, rymin, rxmax, rymax, level + 1, crossings)
|
321
|
+
if crossings != Rectangle::RECT_INTERSECTS
|
322
|
+
cubic2 = CubicBezierCurve.new(points: [[xmid, ymid], [xmc1, ymc1], [xc1, yc1], [x1, y1]])
|
323
|
+
crossings = cubic2.rect_crossings(rxmin, rymin, rxmax, rymax, level + 1, crossings)
|
324
|
+
end
|
325
|
+
crossings
|
326
|
+
end
|
214
327
|
end
|
215
328
|
end
|
data/lib/perfect_shape/line.rb
CHANGED
@@ -22,7 +22,6 @@
|
|
22
22
|
require 'perfect_shape/shape'
|
23
23
|
require 'perfect_shape/point'
|
24
24
|
require 'perfect_shape/multi_point'
|
25
|
-
require 'perfect_shape/rectangle'
|
26
25
|
|
27
26
|
module PerfectShape
|
28
27
|
class Line < Shape
|
@@ -55,11 +54,11 @@ module PerfectShape
|
|
55
54
|
# coordinates with respect to the line segment formed
|
56
55
|
# by the first two specified coordinates.
|
57
56
|
def relative_counterclockwise(x1, y1, x2, y2, px, py)
|
58
|
-
x2 -= x1
|
59
|
-
y2 -= y1
|
60
|
-
px -= x1
|
61
|
-
py -= y1
|
62
|
-
ccw = px * y2 - py * x2
|
57
|
+
x2 -= x1
|
58
|
+
y2 -= y1
|
59
|
+
px -= x1
|
60
|
+
py -= y1
|
61
|
+
ccw = px * y2 - py * x2
|
63
62
|
if ccw == 0.0
|
64
63
|
# The point is colinear, classify based on which side of
|
65
64
|
# the segment the point falls on. We can calculate a
|
@@ -67,7 +66,7 @@ module PerfectShape
|
|
67
66
|
# segment - a negative value indicates the point projects
|
68
67
|
# outside of the segment in the direction of the particular
|
69
68
|
# endpoint used as the origin for the projection.
|
70
|
-
ccw = px * x2 + py * y2
|
69
|
+
ccw = px * x2 + py * y2
|
71
70
|
if ccw > 0.0
|
72
71
|
# Reverse the projection to be relative to the original x2,y2
|
73
72
|
# x2 and y2 are simply negated.
|
@@ -76,13 +75,13 @@ module PerfectShape
|
|
76
75
|
# Since we really want to get a positive answer when the
|
77
76
|
# point is "beyond (x2,y2)", then we want to calculate
|
78
77
|
# the inverse anyway - thus we leave x2 & y2 negated.
|
79
|
-
px -= x2
|
80
|
-
py -= y2
|
81
|
-
ccw = px * x2 + py * y2
|
78
|
+
px -= x2
|
79
|
+
py -= y2
|
80
|
+
ccw = px * x2 + py * y2
|
82
81
|
ccw = 0.0 if ccw < 0.0
|
83
82
|
end
|
84
83
|
end
|
85
|
-
(ccw < 0.0) ? -1 : ((ccw > 0.0) ? 1 : 0)
|
84
|
+
(ccw < 0.0) ? -1 : ((ccw > 0.0) ? 1 : 0)
|
86
85
|
end
|
87
86
|
|
88
87
|
# Returns the square of the distance from a point to a line segment.
|
@@ -121,12 +120,12 @@ module PerfectShape
|
|
121
120
|
# px,py becomes relative vector from x1,y1 to test point
|
122
121
|
px -= x1
|
123
122
|
py -= y1
|
124
|
-
dot_product = px * x2 + py * y2
|
123
|
+
dot_product = px * x2 + py * y2
|
125
124
|
if dot_product <= 0.0
|
126
125
|
# px,py is on the side of x1,y1 away from x2,y2
|
127
126
|
# distance to segment is length of px,py vector
|
128
127
|
# "length of its (clipped) projection" is now 0.0
|
129
|
-
projected_length_square = BigDecimal('0.0')
|
128
|
+
projected_length_square = BigDecimal('0.0')
|
130
129
|
else
|
131
130
|
# switch to backwards vectors relative to x2,y2
|
132
131
|
# x2,y2 are already the negative of x1,y1=>x2,y2
|
@@ -192,10 +191,10 @@ module PerfectShape
|
|
192
191
|
def point_crossings(x1, y1, x2, y2, px, py)
|
193
192
|
return 0 if (py < y1 && py < y2)
|
194
193
|
return 0 if (py >= y1 && py >= y2)
|
195
|
-
# assert(y1 != y2)
|
194
|
+
# assert(y1 != y2)
|
196
195
|
return 0 if (px >= x1 && px >= x2)
|
197
196
|
return ((y1 < y2) ? 1 : -1) if (px < x1 && px < x2)
|
198
|
-
xintercept = x1 + (py - y1) * (x2 - x1) / (y2 - y1)
|
197
|
+
xintercept = x1 + (py - y1) * (x2 - x1) / (y2 - y1)
|
199
198
|
return 0 if (px >= xintercept)
|
200
199
|
(y1 < y2) ? 1 : -1
|
201
200
|
end
|
@@ -243,7 +242,80 @@ module PerfectShape
|
|
243
242
|
Line.point_crossings(points[0][0], points[0][1], points[1][0], points[1][1], x, y)
|
244
243
|
end
|
245
244
|
|
245
|
+
# Accumulate the number of times the line crosses the shadow
|
246
|
+
# extending to the right of the rectangle. See the comment
|
247
|
+
# for the Rectangle::RECT_INTERSECTS constant for more complete details.
|
248
|
+
#
|
249
|
+
# crossings arg is the initial crossings value to add to (useful
|
250
|
+
# in cases where you want to accumulate crossings from multiple
|
251
|
+
# shapes)
|
252
|
+
def rect_crossings(rxmin, rymin, rxmax, rymax, crossings = 0)
|
253
|
+
x0 = points[0][0]
|
254
|
+
y0 = points[0][1]
|
255
|
+
x1 = points[1][0]
|
256
|
+
y1 = points[1][1]
|
257
|
+
return crossings if y0 >= rymax && y1 >= rymax
|
258
|
+
return crossings if y0 <= rymin && y1 <= rymin
|
259
|
+
return crossings if x0 <= rxmin && x1 <= rxmin
|
260
|
+
if x0 >= rxmax && x1 >= rxmax
|
261
|
+
# Line is entirely to the right of the rect
|
262
|
+
# and the vertical ranges of the two overlap by a non-empty amount
|
263
|
+
# Thus, this line segment is partially in the "right-shadow"
|
264
|
+
# Path may have done a complete crossing
|
265
|
+
# Or path may have entered or exited the right-shadow
|
266
|
+
if y0 < y1
|
267
|
+
# y-increasing line segment...
|
268
|
+
# We know that y0 < rymax and y1 > rymin
|
269
|
+
crossings += 1 if (y0 <= rymin)
|
270
|
+
crossings += 1 if (y1 >= rymax)
|
271
|
+
elsif y1 < y0
|
272
|
+
# y-decreasing line segment...
|
273
|
+
# We know that y1 < rymax and y0 > rymin
|
274
|
+
crossings -= 1 if (y1 <= rymin)
|
275
|
+
crossings -= 1 if (y0 >= rymax)
|
276
|
+
end
|
277
|
+
return crossings
|
278
|
+
end
|
279
|
+
# Remaining case:
|
280
|
+
# Both x and y ranges overlap by a non-empty amount
|
281
|
+
# First do trivial INTERSECTS rejection of the cases
|
282
|
+
# where one of the endpoints is inside the rectangle.
|
283
|
+
return Rectangle::RECT_INTERSECTS if ((x0 > rxmin && x0 < rxmax && y0 > rymin && y0 < rymax) ||
|
284
|
+
(x1 > rxmin && x1 < rxmax && y1 > rymin && y1 < rymax))
|
285
|
+
# Otherwise calculate the y intercepts and see where
|
286
|
+
# they fall with respect to the rectangle
|
287
|
+
xi0 = x0
|
288
|
+
if y0 < rymin
|
289
|
+
xi0 += ((rymin - y0) * (x1 - x0) / (y1 - y0))
|
290
|
+
elsif y0 > rymax
|
291
|
+
xi0 += ((rymax - y0) * (x1 - x0) / (y1 - y0))
|
292
|
+
end
|
293
|
+
xi1 = x1
|
294
|
+
if y1 < rymin
|
295
|
+
xi1 += ((rymin - y1) * (x0 - x1) / (y0 - y1))
|
296
|
+
elsif y1 > rymax
|
297
|
+
xi1 += ((rymax - y1) * (x0 - x1) / (y0 - y1))
|
298
|
+
end
|
299
|
+
return crossings if xi0 <= rxmin && xi1 <= rxmin
|
300
|
+
if xi0 >= rxmax && xi1 >= rxmax
|
301
|
+
if y0 < y1
|
302
|
+
# y-increasing line segment...
|
303
|
+
# We know that y0 < rymax and y1 > rymin
|
304
|
+
crossings += 1 if (y0 <= rymin)
|
305
|
+
crossings += 1 if (y1 >= rymax)
|
306
|
+
elsif y1 < y0
|
307
|
+
# y-decreasing line segment...
|
308
|
+
# We know that y1 < rymax and y0 > rymin
|
309
|
+
crossings -= 1 if (y1 <= rymin)
|
310
|
+
crossings -= 1 if (y0 >= rymax)
|
311
|
+
end
|
312
|
+
return crossings
|
313
|
+
end
|
314
|
+
Rectangle::RECT_INTERSECTS
|
315
|
+
end
|
316
|
+
|
246
317
|
def intersect?(rectangle)
|
318
|
+
require 'perfect_shape/rectangle'
|
247
319
|
x1 = points[0][0]
|
248
320
|
y1 = points[0][1]
|
249
321
|
x2 = points[1][0]
|
data/lib/perfect_shape/point.rb
CHANGED
@@ -62,13 +62,124 @@ module PerfectShape
|
|
62
62
|
# These values are also NaN if opposing infinities are added
|
63
63
|
return 0 if (xc.nan? || yc.nan?)
|
64
64
|
point_crossings(x1, y1, x1c, y1c, xc, yc, px, py, level+1) +
|
65
|
-
point_crossings(xc, yc, xc1, yc1, x2, y2, px, py, level+1)
|
65
|
+
point_crossings(xc, yc, xc1, yc1, x2, y2, px, py, level+1)
|
66
|
+
end
|
67
|
+
|
68
|
+
# Determine where coord lies with respect to the range from
|
69
|
+
# low to high. It is assumed that low < high. The return
|
70
|
+
# value is one of the 5 values BELOW, LOWEDGE, INSIDE, HIGHEDGE,
|
71
|
+
# or ABOVE.
|
72
|
+
def tag(coord, low, high)
|
73
|
+
return (coord < low ? BELOW : LOWEDGE) if coord <= low
|
74
|
+
return (coord > high ? ABOVE : HIGHEDGE) if coord >= high
|
75
|
+
INSIDE
|
76
|
+
end
|
77
|
+
|
78
|
+
# Fill an array with the coefficients of the parametric equation
|
79
|
+
# in t, ready for solving against val with solve_quadratic.
|
80
|
+
# We currently have:
|
81
|
+
# val = Py(t) = C1*(1-t)^2 + 2*CP*t*(1-t) + C2*t^2
|
82
|
+
# = C1 - 2*C1*t + C1*t^2 + 2*CP*t - 2*CP*t^2 + C2*t^2
|
83
|
+
# = C1 + (2*CP - 2*C1)*t + (C1 - 2*CP + C2)*t^2
|
84
|
+
# 0 = (C1 - val) + (2*CP - 2*C1)*t + (C1 - 2*CP + C2)*t^2
|
85
|
+
# 0 = C + Bt + At^2
|
86
|
+
# C = C1 - val
|
87
|
+
# B = 2*CP - 2*C1
|
88
|
+
# A = C1 - 2*CP + C2
|
89
|
+
def eqn(val, c1, cp, c2)
|
90
|
+
[
|
91
|
+
c1 - val,
|
92
|
+
cp + cp - c1 - c1,
|
93
|
+
c1 - cp - cp + c2,
|
94
|
+
]
|
95
|
+
end
|
96
|
+
|
97
|
+
# Solves the quadratic whose coefficients are in the {@code eqn}
|
98
|
+
# array and places the non-complex roots into the {@code res}
|
99
|
+
# array, returning the number of roots.
|
100
|
+
# The quadratic solved is represented by the equation:
|
101
|
+
# <pre>
|
102
|
+
# eqn = {C, B, A}
|
103
|
+
# ax^2 + bx + c = 0
|
104
|
+
# </pre>
|
105
|
+
# A return value of {@code -1} is used to distinguish a constant
|
106
|
+
# equation, which might be always 0 or never 0, from an equation that
|
107
|
+
# has no zeroes.
|
108
|
+
# @param eqn the specified array of coefficients to use to solve
|
109
|
+
# the quadratic equation
|
110
|
+
# @param res the array that contains the non-complex roots
|
111
|
+
# resulting from the solution of the quadratic equation
|
112
|
+
# @return the number of roots, or {@code -1} if the equation is
|
113
|
+
# a constant.
|
114
|
+
def solve_quadratic(eqn, res)
|
115
|
+
a = eqn[2]
|
116
|
+
b = eqn[1]
|
117
|
+
c = eqn[0]
|
118
|
+
roots = -1
|
119
|
+
if a == 0.0
|
120
|
+
# The quadratic parabola has degenerated to a line.
|
121
|
+
# The line has degenerated to a constant.
|
122
|
+
return -1 if b == 0.0
|
123
|
+
res[roots += 1] = -c / b
|
124
|
+
else
|
125
|
+
# From Numerical Recipes, 5.6, Quadratic and Cubic Equations
|
126
|
+
d = b * b - 4.0 * a * c
|
127
|
+
# If d < 0.0, then there are no roots
|
128
|
+
return 0 if d < 0.0
|
129
|
+
d = BigDecimal(Math.sqrt(d).to_a)
|
130
|
+
# For accuracy, calculate one root using:
|
131
|
+
# (-b +/- d) / 2a
|
132
|
+
# and the other using:
|
133
|
+
# 2c / (-b +/- d)
|
134
|
+
# Choose the sign of the +/- so that b+d gets larger in magnitude
|
135
|
+
d = -d if b < 0.0
|
136
|
+
q = (b + d) / -2.0
|
137
|
+
# We already tested a for being 0 above
|
138
|
+
res[roots += 1] = q / a
|
139
|
+
res[roots += 1] = c / q if q != 0.0
|
140
|
+
end
|
141
|
+
roots
|
142
|
+
end
|
143
|
+
|
144
|
+
# Evaluate the t values in the first num slots of the vals[] array
|
145
|
+
# and place the evaluated values back into the same array. Only
|
146
|
+
# evaluate t values that are within the range <, >, including
|
147
|
+
# the 0 and 1 ends of the range iff the include0 or include1
|
148
|
+
# booleans are true. If an "inflection" equation is handed in,
|
149
|
+
# then any points which represent a point of inflection for that
|
150
|
+
# quadratic equation are also ignored.
|
151
|
+
def eval_quadratic(vals, num,
|
152
|
+
include0,
|
153
|
+
include1,
|
154
|
+
inflect,
|
155
|
+
c1, ctrl, c2)
|
156
|
+
j = -1
|
157
|
+
i = 0
|
158
|
+
while i < num
|
159
|
+
t = vals[i]
|
160
|
+
|
161
|
+
if ((include0 ? t >= 0 : t > 0) &&
|
162
|
+
(include1 ? t <= 1 : t < 1) &&
|
163
|
+
(inflect.nil? ||
|
164
|
+
inflect[1] + 2*inflect[2]*t != 0))
|
165
|
+
u = 1 - t
|
166
|
+
vals[j+=1] = c1*u*u + 2*ctrl*t*u + c2*t*t
|
167
|
+
end
|
168
|
+
i+=1
|
169
|
+
end
|
170
|
+
j
|
66
171
|
end
|
67
172
|
end
|
68
173
|
|
69
174
|
include MultiPoint
|
70
175
|
include Equalizer.new(:points)
|
71
176
|
|
177
|
+
BELOW = -2
|
178
|
+
LOWEDGE = -1
|
179
|
+
INSIDE = 0
|
180
|
+
HIGHEDGE = 1
|
181
|
+
ABOVE = 2
|
182
|
+
|
72
183
|
OUTLINE_MINIMUM_DISTANCE_THRESHOLD = BigDecimal('0.001')
|
73
184
|
|
74
185
|
# Checks if quadratic bézier curve contains point (two-number Array or x, y args)
|
@@ -159,20 +270,20 @@ module PerfectShape
|
|
159
270
|
# zero, then the points must be collinear and so the
|
160
271
|
# curve is degenerate and encloses no area. Thus the
|
161
272
|
# result is false.
|
162
|
-
kx = x1 - 2 * xc + x2
|
163
|
-
ky = y1 - 2 * yc + y2
|
164
|
-
dx = x - x1
|
165
|
-
dy = y - y1
|
166
|
-
dxl = x2 - x1
|
167
|
-
dyl = y2 - y1
|
273
|
+
kx = x1 - 2 * xc + x2
|
274
|
+
ky = y1 - 2 * yc + y2
|
275
|
+
dx = x - x1
|
276
|
+
dy = y - y1
|
277
|
+
dxl = x2 - x1
|
278
|
+
dyl = y2 - y1
|
168
279
|
|
169
280
|
t0 = (dx * ky - dy * kx) / (dxl * ky - dyl * kx)
|
170
281
|
return false if (t0 < 0 || t0 > 1 || t0 != t0)
|
171
282
|
|
172
|
-
xb = kx * t0 * t0 + 2 * (xc - x1) * t0 + x1
|
173
|
-
yb = ky * t0 * t0 + 2 * (yc - y1) * t0 + y1
|
174
|
-
xl = dxl * t0 + x1
|
175
|
-
yl = dyl * t0 + y1
|
283
|
+
xb = kx * t0 * t0 + 2 * (xc - x1) * t0 + x1
|
284
|
+
yb = ky * t0 * t0 + 2 * (yc - y1) * t0 + y1
|
285
|
+
xl = dxl * t0 + x1
|
286
|
+
yl = dyl * t0 + y1
|
176
287
|
|
177
288
|
(x >= xb && x < xl) ||
|
178
289
|
(x >= xl && x < xb) ||
|
@@ -273,5 +384,166 @@ module PerfectShape
|
|
273
384
|
last_minimum_distance
|
274
385
|
end
|
275
386
|
end
|
387
|
+
|
388
|
+
def intersect?(rectangle)
|
389
|
+
x = rectangle.x
|
390
|
+
y = rectangle.y
|
391
|
+
w = rectangle.width
|
392
|
+
h = rectangle.height
|
393
|
+
|
394
|
+
# Trivially reject non-existant rectangles
|
395
|
+
return false if w <= 0 || h <= 0
|
396
|
+
|
397
|
+
# Trivially accept if either endpoint is inside the rectangle
|
398
|
+
# (not on its border since it may end there and not go inside)
|
399
|
+
# Record where they lie with respect to the rectangle.
|
400
|
+
# -1 => left, 0 => inside, 1 => right
|
401
|
+
x1 = points[0][0]
|
402
|
+
y1 = points[0][1]
|
403
|
+
x1tag = QuadraticBezierCurve.tag(x1, x, x+w)
|
404
|
+
y1tag = QuadraticBezierCurve.tag(y1, y, y+h)
|
405
|
+
return true if x1tag == INSIDE && y1tag == INSIDE
|
406
|
+
x2 = points[2][0]
|
407
|
+
y2 = points[2][1]
|
408
|
+
x2tag = QuadraticBezierCurve.tag(x2, x, x+w)
|
409
|
+
y2tag = QuadraticBezierCurve.tag(y2, y, y+h)
|
410
|
+
return true if x2tag == INSIDE && y2tag == INSIDE
|
411
|
+
ctrlx = points[1][0]
|
412
|
+
ctrly = points[1][1]
|
413
|
+
ctrlxtag = QuadraticBezierCurve.tag(ctrlx, x, x+w)
|
414
|
+
ctrlytag = QuadraticBezierCurve.tag(ctrly, y, y+h)
|
415
|
+
|
416
|
+
# Trivially reject if all points are entirely to one side of
|
417
|
+
# the rectangle.
|
418
|
+
# Returning false means All points left
|
419
|
+
return false if x1tag < INSIDE && x2tag < INSIDE && ctrlxtag < INSIDE
|
420
|
+
# Returning false means All points above
|
421
|
+
return false if y1tag < INSIDE && y2tag < INSIDE && ctrlytag < INSIDE
|
422
|
+
# Returning false means All points right
|
423
|
+
return false if x1tag > INSIDE && x2tag > INSIDE && ctrlxtag > INSIDE
|
424
|
+
# Returning false means All points below
|
425
|
+
return false if y1tag > INSIDE && y2tag > INSIDE && ctrlytag > INSIDE
|
426
|
+
|
427
|
+
# Test for endpoints on the edge where either the segment
|
428
|
+
# or the curve is headed "inwards" from them
|
429
|
+
# Note: These tests are a superset of the fast endpoint tests
|
430
|
+
# above and thus repeat those tests, but take more time
|
431
|
+
# and cover more cases
|
432
|
+
# First endpoint on border with either edge moving inside
|
433
|
+
return true if inwards(x1tag, x2tag, ctrlxtag) && inwards(y1tag, y2tag, ctrlytag)
|
434
|
+
# Second endpoint on border with either edge moving inside
|
435
|
+
return true if inwards(x2tag, x1tag, ctrlxtag) && inwards(y2tag, y1tag, ctrlytag)
|
436
|
+
|
437
|
+
# Trivially accept if endpoints span directly across the rectangle
|
438
|
+
xoverlap = (x1tag * x2tag <= 0)
|
439
|
+
yoverlap = (y1tag * y2tag <= 0)
|
440
|
+
return true if x1tag == INSIDE && x2tag == INSIDE && yoverlap
|
441
|
+
return true if y1tag == INSIDE && y2tag == INSIDE && xoverlap
|
442
|
+
|
443
|
+
# We now know that both endpoints are outside the rectangle
|
444
|
+
# but the 3 points are not all on one side of the rectangle.
|
445
|
+
# Therefore the curve cannot be contained inside the rectangle,
|
446
|
+
# but the rectangle might be contained inside the curve, or
|
447
|
+
# the curve might intersect the boundary of the rectangle.
|
448
|
+
|
449
|
+
eqn = nil
|
450
|
+
res = []
|
451
|
+
if !yoverlap
|
452
|
+
# Both Y coordinates for the closing segment are above or
|
453
|
+
# below the rectangle which means that we can only intersect
|
454
|
+
# if the curve crosses the top (or bottom) of the rectangle
|
455
|
+
# in more than one place and if those crossing locations
|
456
|
+
# span the horizontal range of the rectangle.
|
457
|
+
eqn = QuadraticBezierCurve.eqn((y1tag < INSIDE ? y : y+h), y1, ctrly, y2)
|
458
|
+
return (QuadraticBezierCurve.solve_quadratic(eqn, res) == 2 &&
|
459
|
+
QuadraticBezierCurve.eval_quadratic(res, 2, true, true, nil,
|
460
|
+
x1, ctrlx, x2) == 2 &&
|
461
|
+
QuadraticBezierCurve.tag(res[0], x, x+w) * QuadraticBezierCurve.tag(res[1], x, x+w) <= 0)
|
462
|
+
end
|
463
|
+
|
464
|
+
# Y ranges overlap. Now we examine the X ranges
|
465
|
+
if !xoverlap
|
466
|
+
# Both X coordinates for the closing segment are left of
|
467
|
+
# or right of the rectangle which means that we can only
|
468
|
+
# intersect if the curve crosses the left (or right) edge
|
469
|
+
# of the rectangle in more than one place and if those
|
470
|
+
# crossing locations span the vertical range of the rectangle.
|
471
|
+
eqn = QuadraticBezierCurve.eqn((x1tag < INSIDE ? x : x+w), x1, ctrlx, x2)
|
472
|
+
return (QuadraticBezierCurve.solve_quadratic(eqn, res) == 2 &&
|
473
|
+
QuadraticBezierCurve.eval_quadratic(res, 2, true, true, nil,
|
474
|
+
y1, ctrly, y2) == 2 &&
|
475
|
+
QuadraticBezierCurve.tag(res[0], y, y+h) * QuadraticBezierCurve.tag(res[1], y, y+h) <= 0)
|
476
|
+
end
|
477
|
+
|
478
|
+
# The X and Y ranges of the endpoints overlap the X and Y
|
479
|
+
# ranges of the rectangle, now find out how the endpoint
|
480
|
+
# line segment intersects the Y range of the rectangle
|
481
|
+
dx = x2 - x1
|
482
|
+
dy = y2 - y1
|
483
|
+
k = y2 * x1 - x2 * y1
|
484
|
+
c1tag = c2tag = nil
|
485
|
+
if y1tag == INSIDE
|
486
|
+
c1tag = x1tag
|
487
|
+
else
|
488
|
+
c1tag = QuadraticBezierCurve.tag((k + dx * (y1tag < INSIDE ? y : y+h)) / dy, x, x+w)
|
489
|
+
end
|
490
|
+
if y2tag == INSIDE
|
491
|
+
c2tag = x2tag
|
492
|
+
else
|
493
|
+
c2tag = QuadraticBezierCurve.tag((k + dx * (y2tag < INSIDE ? y : y+h)) / dy, x, x+w)
|
494
|
+
end
|
495
|
+
# If the part of the line segment that intersects the Y range
|
496
|
+
# of the rectangle crosses it horizontally - trivially accept
|
497
|
+
return true if c1tag * c2tag <= 0
|
498
|
+
|
499
|
+
# Now we know that both the X and Y ranges intersect and that
|
500
|
+
# the endpoint line segment does not directly cross the rectangle.
|
501
|
+
#
|
502
|
+
# We can almost treat this case like one of the cases above
|
503
|
+
# where both endpoints are to one side, except that we will
|
504
|
+
# only get one intersection of the curve with the vertical
|
505
|
+
# side of the rectangle. This is because the endpoint segment
|
506
|
+
# accounts for the other intersection.
|
507
|
+
#
|
508
|
+
# (Remember there is overlap in both the X and Y ranges which
|
509
|
+
# means that the segment must cross at least one vertical edge
|
510
|
+
# of the rectangle - in particular, the "near vertical side" -
|
511
|
+
# leaving only one intersection for the curve.)
|
512
|
+
#
|
513
|
+
# Now we calculate the y tags of the two intersections on the
|
514
|
+
# "near vertical side" of the rectangle. We will have one with
|
515
|
+
# the endpoint segment, and one with the curve. If those two
|
516
|
+
# vertical intersections overlap the Y range of the rectangle,
|
517
|
+
# we have an intersection. Otherwise, we don't.
|
518
|
+
|
519
|
+
# c1tag = vertical intersection class of the endpoint segment
|
520
|
+
#
|
521
|
+
# Choose the y tag of the endpoint that was not on the same
|
522
|
+
# side of the rectangle as the subsegment calculated above.
|
523
|
+
# Note that we can "steal" the existing Y tag of that endpoint
|
524
|
+
# since it will be provably the same as the vertical intersection.
|
525
|
+
c1tag = ((c1tag * x1tag <= 0) ? y1tag : y2tag)
|
526
|
+
|
527
|
+
# c2tag = vertical intersection class of the curve
|
528
|
+
#
|
529
|
+
# We have to calculate this one the straightforward way.
|
530
|
+
# Note that the c2tag can still tell us which vertical edge
|
531
|
+
# to test against.
|
532
|
+
eqn = QuadraticBezierCurve.eqn((c2tag < INSIDE ? x : x+w), x1, ctrlx, x2)
|
533
|
+
num = QuadraticBezierCurve.solve_quadratic(eqn, res)
|
534
|
+
|
535
|
+
# Note: We should be able to assert(num == 2) since the
|
536
|
+
# X range "crosses" (not touches) the vertical boundary,
|
537
|
+
# but we pass num to QuadraticBezierCurve.eval_quadratic for completeness.
|
538
|
+
QuadraticBezierCurve.eval_quadratic(res, num, true, true, nil, y1, ctrly, y2)
|
539
|
+
|
540
|
+
# Note: We can assert(num evals == 1) since one of the
|
541
|
+
# 2 crossings will be out of the [0,1] range.
|
542
|
+
c2tag = QuadraticBezierCurve.tag(res[0], y, y+h)
|
543
|
+
|
544
|
+
# Finally, we have an intersection if the two crossings
|
545
|
+
# overlap the Y range of the rectangle.
|
546
|
+
c1tag * c2tag <= 0
|
547
|
+
end
|
276
548
|
end
|
277
549
|
end
|
@@ -41,6 +41,8 @@ module PerfectShape
|
|
41
41
|
# bitmask indicating a point lies below
|
42
42
|
OUT_BOTTOM = 8
|
43
43
|
|
44
|
+
RECT_INTERSECTS = 0x80000000
|
45
|
+
|
44
46
|
# Checks if rectangle contains point (two-number Array or x, y args)
|
45
47
|
#
|
46
48
|
# @param x The X coordinate of the point to test.
|
@@ -95,5 +97,24 @@ module PerfectShape
|
|
95
97
|
end
|
96
98
|
out
|
97
99
|
end
|
100
|
+
|
101
|
+
# A rectangle is empty if its width or height is 0 (or less)
|
102
|
+
def empty?
|
103
|
+
width <= 0.0 || height <= 0.0
|
104
|
+
end
|
105
|
+
|
106
|
+
def intersect?(rectangle)
|
107
|
+
x = rectangle.x
|
108
|
+
y = rectangle.y
|
109
|
+
w = rectangle.width
|
110
|
+
h = rectangle.height
|
111
|
+
return false if empty? || w <= 0 || h <= 0
|
112
|
+
x0 = self.x
|
113
|
+
y0 = self.y
|
114
|
+
(x + w) > x0 &&
|
115
|
+
(y + h) > y0 &&
|
116
|
+
x < (x0 + self.width) &&
|
117
|
+
y < (y0 + self.height)
|
118
|
+
end
|
98
119
|
end
|
99
120
|
end
|
data/lib/perfect_shape/square.rb
CHANGED
@@ -23,12 +23,15 @@ require 'perfect_shape/rectangle'
|
|
23
23
|
|
24
24
|
module PerfectShape
|
25
25
|
class Square < Rectangle
|
26
|
-
MESSAGE_WIDTH_AND_HEIGHT_AND_LENGTH_NOT_EQUAL = 'Square width, height, and length must all be equal if more than one is specified; or otherwise keep only one of them in arguments!'
|
26
|
+
MESSAGE_WIDTH_AND_HEIGHT_AND_LENGTH_NOT_EQUAL = 'Square width, height, and length must all be equal if more than one is specified; or otherwise keep only one of them in constructor arguments!'
|
27
27
|
|
28
28
|
attr_reader :length
|
29
|
+
alias size length
|
29
30
|
|
30
31
|
# Constructs with x, y, length (optionally width or height can be passed as alias for length)
|
31
|
-
def initialize(x: 0, y: 0, length: nil, width: nil, height: nil)
|
32
|
+
def initialize(x: 0, y: 0, length: nil, size: nil, width: nil, height: nil)
|
33
|
+
raise MESSAGE_WIDTH_AND_HEIGHT_AND_LENGTH_NOT_EQUAL if (length && size && length != size)
|
34
|
+
length ||= size
|
32
35
|
raise MESSAGE_WIDTH_AND_HEIGHT_AND_LENGTH_NOT_EQUAL if (length && width && length != width) || (length && height && length != height) || (width && height && width != height)
|
33
36
|
length ||= width || height || 1
|
34
37
|
super(x: x, y: y, width: length, height: length)
|
@@ -40,6 +43,7 @@ module PerfectShape
|
|
40
43
|
self.width = value unless width == value
|
41
44
|
self.height = value unless height == value
|
42
45
|
end
|
46
|
+
alias size= length=
|
43
47
|
|
44
48
|
def width=(value)
|
45
49
|
super
|
data/perfect-shape.gemspec
CHANGED
@@ -2,16 +2,16 @@
|
|
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.5.
|
5
|
+
# stub: perfect-shape 0.5.4 ruby lib
|
6
6
|
|
7
7
|
Gem::Specification.new do |s|
|
8
8
|
s.name = "perfect-shape".freeze
|
9
|
-
s.version = "0.5.
|
9
|
+
s.version = "0.5.4"
|
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]
|
13
13
|
s.authors = ["Andy Maleh".freeze]
|
14
|
-
s.date = "2022-01-
|
14
|
+
s.date = "2022-01-21"
|
15
15
|
s.description = "Perfect Shape is a collection of pure Ruby geometric algorithms that are mostly useful for GUI manipulation like checking viewport rectangle intersection or containment of a mouse click point in popular geometry shapes such as rectangle, square, arc (open, chord, and pie), ellipse, circle, polygon, and paths containing lines, quadratic b\u00E9zier curves, and cubic bezier curves, potentially with affine transforms applied like translation, scale, rotation, shear/skew, and inversion (including both Ray Casting Algorithm, aka Even-odd Rule, and Winding Number Algorithm, aka Nonzero Rule). Additionally, it contains some purely mathematical algorithms like IEEEremainder (also known as IEEE-754 remainder).".freeze
|
16
16
|
s.email = "andy.am@gmail.com".freeze
|
17
17
|
s.extra_rdoc_files = [
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: perfect-shape
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.5.
|
4
|
+
version: 0.5.4
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Andy Maleh
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2022-01-
|
11
|
+
date: 2022-01-21 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: equalizer
|