perfect-shape 0.5.2 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +37 -4
- data/README.md +24 -8
- data/VERSION +1 -1
- data/lib/perfect_shape/arc.rb +102 -2
- data/lib/perfect_shape/composite_shape.rb +4 -0
- data/lib/perfect_shape/cubic_bezier_curve.rb +115 -2
- data/lib/perfect_shape/ellipse.rb +2 -2
- data/lib/perfect_shape/line.rb +88 -16
- data/lib/perfect_shape/path.rb +114 -9
- data/lib/perfect_shape/point.rb +2 -2
- data/lib/perfect_shape/polygon.rb +111 -72
- data/lib/perfect_shape/quadratic_bezier_curve.rb +73 -6
- data/lib/perfect_shape/rectangle.rb +23 -2
- 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: c200dfc203347661cd329e24cebc75ae038b0b69f6874832cfe8793ae2cbf6e5
|
4
|
+
data.tar.gz: 55cac9b991125bfcf15493eb5e05fecaea618d5d415e9c2b2c9d593efba98be4
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6cd01a66d882fa6af01e3e52e5c37bdc05c8217ae550e1b2e212bce588c7812059af7ff476ede2bd7c873f9e55738aace0386d46d6f06839ee9e50f52bd29d1c
|
7
|
+
data.tar.gz: f69e3ea514b1cf4f071e04fc943ddb814fef3e7d4594e2ffde34d3d570868feb9f642d4705e0639100b23b81202b67cc5c88eab8ef769df91c7acddf60da46e9
|
data/CHANGELOG.md
CHANGED
@@ -1,17 +1,50 @@
|
|
1
1
|
# Change Log
|
2
2
|
|
3
|
+
## 1.0.0
|
4
|
+
|
5
|
+
- `PerfectShape::Path#intersect?(rectangle)`
|
6
|
+
- `PerfectShape::Polygon#intersect?(rectangle)`
|
7
|
+
- `PerfectShape::CompositeShape#intersect?(rectangle)`
|
8
|
+
- [API Breaking] Change `Path` default `winding_rule` to `:wind_even_odd`
|
9
|
+
- Make `PerfectShape::Polygon` support `:wind_non_zero` `winding_rule` (not just default `:wind_even_odd`)
|
10
|
+
|
11
|
+
## 0.5.5
|
12
|
+
|
13
|
+
- `PerfectShape::Arc#intersect?(rectangle)`
|
14
|
+
- `PerfectShape::Ellipse#intersect?(rectangle)`
|
15
|
+
- `PerfectShape::Circle#intersect?(rectangle)`
|
16
|
+
|
17
|
+
## 0.5.4
|
18
|
+
|
19
|
+
- `PerfectShape::Rectangle#intersect?(rectangle)`
|
20
|
+
- `PerfectShape::Square#intersect?(rectangle)`
|
21
|
+
- `PerfectShape::Rectangle#empty?`
|
22
|
+
- `PerfectShape::Square#empty?`
|
23
|
+
- `PerfectShape::Square#size`/`PerfectShape::Square#size=` aliases for `#length`/`#length=`
|
24
|
+
|
25
|
+
## 0.5.3
|
26
|
+
|
27
|
+
- `PerfectShape::CubicBezierCurve#intersect?(rectangle)`
|
28
|
+
- `PerfectShape::CubicBezierCurve#rectangle_crossings(rectangle)`
|
29
|
+
- `PerfectShape::CubicBezierCurve#rect_crossings(rxmin, rymin, rxmax, rymax, level, crossings = 0)`
|
30
|
+
- `PerfectShape::Line#rect_crossings(rxmin, rymin, rxmax, rymax, crossings = 0)`
|
31
|
+
|
3
32
|
## 0.5.2
|
4
33
|
|
5
|
-
- `QuadraticBezierCurve#intersect?(rectangle)`
|
34
|
+
- `PerfectShape::QuadraticBezierCurve#intersect?(rectangle)`
|
35
|
+
- `PerfectShape::QuadraticBezierCurve::tag(coord, low, high)`
|
36
|
+
- `PerfectShape::QuadraticBezierCurve::eqn(val, c1, cp, c2)`
|
37
|
+
- `PerfectShape::QuadraticBezierCurve::solve_quadratic(eqn)`
|
38
|
+
- `PerfectShape::QuadraticBezierCurve::eval_quadratic(vals, num, include0, include1, inflect, c1, ctrl, c2)`
|
6
39
|
|
7
40
|
## 0.5.1
|
8
41
|
|
9
|
-
- `Point#intersect?(rectangle)` (equivalent to `Rectangle#contain?(point)`)
|
42
|
+
- `PerfectShape::Point#intersect?(rectangle)` (equivalent to `PerfectShape::Rectangle#contain?(point)`)
|
10
43
|
|
11
44
|
## 0.5.0
|
12
45
|
|
13
|
-
- `Line#intersect?(rectangle)`
|
14
|
-
- `Rectangle#out_state(x_or_point, y = nil)`
|
46
|
+
- `PerfectShape::Line#intersect?(rectangle)`
|
47
|
+
- `PerfectShape::Rectangle#out_state(x_or_point, y = nil)`
|
15
48
|
|
16
49
|
## 0.4.0
|
17
50
|
|
data/README.md
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
# Perfect Shape 0.
|
1
|
+
# Perfect Shape 1.0.0
|
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.
|
17
|
+
gem install perfect-shape -v 1.0.0
|
18
18
|
```
|
19
19
|
|
20
20
|
Or include in Bundler `Gemfile`:
|
21
21
|
|
22
22
|
```ruby
|
23
|
-
gem 'perfect-shape', '~> 0.
|
23
|
+
gem 'perfect-shape', '~> 1.0.0'
|
24
24
|
```
|
25
25
|
|
26
26
|
And, run:
|
@@ -224,6 +224,7 @@ Includes `PerfectShape::MultiPoint`
|
|
224
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
|
225
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)”.
|
226
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)
|
227
228
|
|
228
229
|
Example:
|
229
230
|
|
@@ -252,7 +253,7 @@ Includes `PerfectShape::MultiPoint`
|
|
252
253
|
|
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.
|
254
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
|
255
|
-
- `::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
|
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 `-1` is used to distinguish a constant equation, which might be always 0 or never 0, from an equation that has no zeroes.
|
256
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.
|
257
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
|
258
259
|
- `#points`: points (start point, control point, and end point)
|
@@ -274,6 +275,7 @@ Includes `PerfectShape::MultiPoint`
|
|
274
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`)
|
275
276
|
- `#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`.
|
276
277
|
- `#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.
|
278
|
+
- `#rect_crossings(rxmin, rymin, rxmax, rymax, level, crossings = 0)`: rectangle crossings (adds to crossings arg)
|
277
279
|
|
278
280
|
Example:
|
279
281
|
|
@@ -318,11 +320,14 @@ Includes `PerfectShape::MultiPoint`
|
|
318
320
|
- `#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)
|
319
321
|
- `#==(other)`: Returns `true` if equal to `other` or `false` otherwise
|
320
322
|
- `#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
|
323
|
+
- `#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
|
321
324
|
- `#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`)
|
322
325
|
- `#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`)
|
323
326
|
- `#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`)
|
324
327
|
- `#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`.
|
325
328
|
- `#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.
|
329
|
+
- `#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`
|
330
|
+
- `#rect_crossings(rxmin, rymin, rxmax, rymax, level, crossings = 0)`: rectangle crossings (adds to crossings arg)
|
326
331
|
|
327
332
|
Example:
|
328
333
|
|
@@ -368,8 +373,10 @@ Includes `PerfectShape::RectangularShape`
|
|
368
373
|
- `#bounding_box`: bounding box is a rectangle with x = min x, y = min y, and width/height of shape
|
369
374
|
- `#==(other)`: Returns `true` if equal to `other` or `false` otherwise
|
370
375
|
- `#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
|
376
|
+
- `#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
|
371
377
|
- `#edges`: edges of rectangle as `PerfectShape::Line` objects
|
372
378
|
- `#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.
|
379
|
+
- `#empty?`: Returns `true` if width or height are 0 (or negative) and `false` otherwise
|
373
380
|
|
374
381
|
Example:
|
375
382
|
|
@@ -398,7 +405,7 @@ Extends `PerfectShape::Rectangle`
|
|
398
405
|
|
399
406
|
![square](https://raw.githubusercontent.com/AndyObtiva/perfect-shape/master/images/square.png)
|
400
407
|
|
401
|
-
- `::new(x: 0, y: 0, length: 1)
|
408
|
+
- `::new(x: 0, y: 0, length: 1)` (`length` alias: `size`): constructs a square
|
402
409
|
- `#x`: top-left x
|
403
410
|
- `#y`: top-left y
|
404
411
|
- `#length`: length
|
@@ -414,7 +421,9 @@ Extends `PerfectShape::Rectangle`
|
|
414
421
|
- `#bounding_box`: bounding box is a rectangle with x = min x, y = min y, and width/height of shape
|
415
422
|
- `#==(other)`: Returns `true` if equal to `other` or `false` otherwise
|
416
423
|
- `#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
|
424
|
+
- `#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
|
417
425
|
- `#edges`: edges of square as `PerfectShape::Line` objects
|
426
|
+
- `#empty?`: Returns `true` if length is 0 (or negative) and `false` otherwise
|
418
427
|
|
419
428
|
Example:
|
420
429
|
|
@@ -469,6 +478,8 @@ Open Arc | Chord Arc | Pie Arc
|
|
469
478
|
- `#bounding_box`: bounding box is a rectangle with x = min x, y = min y, and width/height of shape
|
470
479
|
- `#==(other)`: Returns `true` if equal to `other` or `false` otherwise
|
471
480
|
- `#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 an arc shape from its outline more successfully
|
481
|
+
- `#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
|
482
|
+
- `#contain_angle?(angle)`: returns `true` if the angle is within the angular extents of the arc and `false` otherwise
|
472
483
|
|
473
484
|
Example:
|
474
485
|
|
@@ -592,6 +603,7 @@ Extends `PerfectShape::Arc`
|
|
592
603
|
- `#bounding_box`: bounding box is a rectangle with x = min x, y = min y, and width/height of shape
|
593
604
|
- `#==(other)`: Returns `true` if equal to `other` or `false` otherwise
|
594
605
|
- `#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 an ellipse shape from its outline more successfully
|
606
|
+
- `#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
|
595
607
|
|
596
608
|
Example:
|
597
609
|
|
@@ -653,6 +665,7 @@ Extends `PerfectShape::Ellipse`
|
|
653
665
|
- `#bounding_box`: bounding box is a rectangle with x = min x, y = min y, and width/height of shape
|
654
666
|
- `#==(other)`: Returns `true` if equal to `other` or `false` otherwise
|
655
667
|
- `#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 circle shape from its outline more successfully
|
668
|
+
- `#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
|
656
669
|
|
657
670
|
Example:
|
658
671
|
|
@@ -692,11 +705,11 @@ Extends `PerfectShape::Shape`
|
|
692
705
|
|
693
706
|
Includes `PerfectShape::MultiPoint`
|
694
707
|
|
695
|
-
A polygon can be thought of as a special case of [path](#perfectshapepath)
|
708
|
+
A polygon can be thought of as a special case of [path](#perfectshapepath), consisting of lines only, is closed, and has the [Even-Odd](https://en.wikipedia.org/wiki/Even%E2%80%93odd_rule) winding rule by default.
|
696
709
|
|
697
710
|
![polygon](https://raw.githubusercontent.com/AndyObtiva/perfect-shape/master/images/polygon.png)
|
698
711
|
|
699
|
-
- `::new(points: [])`: constructs a polygon with `points` as `Array` of `Array`s of `[x,y]` pairs or flattened `Array` of alternating x and y coordinates
|
712
|
+
- `::new(points: [], winding_rule: :wind_even_odd)`: constructs a polygon with `points` as `Array` of `Array`s of `[x,y]` pairs or flattened `Array` of alternating x and y coordinates and specified winding rule (`:wind_even_odd` or `:wind_non_zero`)
|
700
713
|
- `#min_x`: min x
|
701
714
|
- `#min_y`: min y
|
702
715
|
- `#max_x`: max x
|
@@ -708,7 +721,8 @@ A polygon can be thought of as a special case of [path](#perfectshapepath) that
|
|
708
721
|
- `#center_y`: center y
|
709
722
|
- `#bounding_box`: bounding box is a rectangle with x = min x, y = min y, and width/height of shape
|
710
723
|
- `#==(other)`: Returns `true` if equal to `other` or `false` otherwise
|
711
|
-
- `#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
|
724
|
+
- `#contain?(x_or_point, y=nil, outline: false, distance_tolerance: 0)`: When `outline` is `false`, it checks if point is inside using either 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)) or [Winding Number Algorithm](https://en.wikipedia.org/wiki/Point_in_polygon#Winding_number_algorithm) (aka [Nonzero-Rule](https://en.wikipedia.org/wiki/Nonzero-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
|
725
|
+
- `#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
|
712
726
|
- `#edges`: edges of polygon as `PerfectShape::Line` objects
|
713
727
|
|
714
728
|
Example:
|
@@ -757,6 +771,7 @@ Includes `PerfectShape::MultiPoint`
|
|
757
771
|
- `#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)
|
758
772
|
- `#==(other)`: Returns `true` if equal to `other` or `false` otherwise
|
759
773
|
- `#contain?(x_or_point, y=nil, outline: false, distance_tolerance: 0)`: When `outline` is `false`, it checks if point is inside path utilizing the configured winding rule, which can be the [Nonzero-Rule](https://en.wikipedia.org/wiki/Nonzero-rule) (aka [Winding Number Algorithm](https://en.wikipedia.org/wiki/Point_in_polygon#Winding_number_algorithm)) or the [Even-Odd Rule](https://en.wikipedia.org/wiki/Even%E2%80%93odd_rule) (aka [Ray Casting Algorithm](https://en.wikipedia.org/wiki/Point_in_polygon#Ray_casting_algorithm)). 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 path shape from its outline more successfully
|
774
|
+
- `#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
|
760
775
|
- `#point_crossings(x_or_point, y=nil)`: calculates the number of times the given path crosses the ray extending to the right from (x,y)
|
761
776
|
- `#disconnected_shapes`: Disconnected shapes have their start point filled in so that each shape does not depend on the previous shape to determine its start point. Also, if a point is followed by a non-point shape, it is removed since it is augmented to the following shape as its start point. Lastly, if the path is closed, an extra shape is added to represent the line connecting the last point to the first
|
762
777
|
|
@@ -809,6 +824,7 @@ A composite shape is simply an aggregate of multiple shapes (e.g. square and pol
|
|
809
824
|
- `#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)
|
810
825
|
- `#==(other)`: Returns `true` if equal to `other` or `false` otherwise
|
811
826
|
- `#contain?(x_or_point, y=nil, outline: false, distance_tolerance: 0)`: When `outline` is `false`, it checks if point is inside any of the shapes owned by the composite shape. Otherwise, when `outline` is `true`, it checks if point is on the outline of any of the shapes owned by the composite shape. `distance_tolerance` can be used as a fuzz factor when `outline` is `true`, for example, to help GUI users mouse-click-select a composite shape from its outline more successfully
|
827
|
+
- `#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
|
812
828
|
|
813
829
|
Example:
|
814
830
|
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.
|
1
|
+
1.0.0
|
data/lib/perfect_shape/arc.rb
CHANGED
@@ -141,8 +141,8 @@ module PerfectShape
|
|
141
141
|
# @param x The X coordinate of the point to test.
|
142
142
|
# @param y The Y coordinate of the point to test.
|
143
143
|
#
|
144
|
-
# @return
|
145
|
-
# the arc,
|
144
|
+
# @return true if the point lies within the bound of
|
145
|
+
# the arc, false if the point lies outside of the
|
146
146
|
# arc's bounds.
|
147
147
|
def contain?(x_or_point, y = nil, outline: false, distance_tolerance: 0)
|
148
148
|
x, y = Point.normalize_point(x_or_point, y)
|
@@ -212,5 +212,105 @@ module PerfectShape
|
|
212
212
|
|
213
213
|
(angle >= 0.0) && (angle < ang_ext)
|
214
214
|
end
|
215
|
+
|
216
|
+
def intersect?(rectangle)
|
217
|
+
x = rectangle.x
|
218
|
+
y = rectangle.y
|
219
|
+
w = rectangle.width
|
220
|
+
h = rectangle.height
|
221
|
+
aw = self.width
|
222
|
+
ah = self.height
|
223
|
+
|
224
|
+
return false if w <= 0 || h <= 0 || aw <= 0 || ah <= 0
|
225
|
+
ext = self.extent
|
226
|
+
return false if ext == 0
|
227
|
+
|
228
|
+
ax = self.x
|
229
|
+
ay = self.y
|
230
|
+
axw = ax + aw
|
231
|
+
ayh = ay + ah
|
232
|
+
xw = x + w
|
233
|
+
yh = y + h
|
234
|
+
|
235
|
+
# check bbox
|
236
|
+
return false if x >= axw || y >= ayh || xw <= ax || yh <= ay
|
237
|
+
|
238
|
+
# extract necessary data
|
239
|
+
axc = self.center_x
|
240
|
+
ayc = self.center_y
|
241
|
+
sx, sy = self.start_point
|
242
|
+
ex, ey = self.end_point
|
243
|
+
|
244
|
+
# Try to catch rectangles that intersect arc in areas
|
245
|
+
# outside of rectagle with left top corner coordinates
|
246
|
+
# (min(center x, start point x, end point x),
|
247
|
+
# min(center y, start point y, end point y))
|
248
|
+
# and rigth bottom corner coordinates
|
249
|
+
# (max(center x, start point x, end point x),
|
250
|
+
# max(center y, start point y, end point y)).
|
251
|
+
# So we'll check axis segments outside of rectangle above.
|
252
|
+
if ayc >= y && ayc <= yh # 0 and 180
|
253
|
+
return true if (sx < xw && ex < xw && axc < xw &&
|
254
|
+
axw > x && contain_angle?(0)) ||
|
255
|
+
(sx > x && ex > x && axc > x &&
|
256
|
+
ax < xw && contain_angle?(180))
|
257
|
+
end
|
258
|
+
if axc >= x && axc <= xw # 90 and 270
|
259
|
+
return true if (sy > y && ey > y && ayc > y &&
|
260
|
+
ay < yh && contain_angle?(90)) ||
|
261
|
+
(sy < yh && ey < yh && ayc < yh &&
|
262
|
+
ayh > y && contain_angle?(270))
|
263
|
+
end
|
264
|
+
|
265
|
+
# For PIE we should check intersection with pie slices
|
266
|
+
# also we should do the same for arcs with extent is greater
|
267
|
+
# than 180, because we should cover case of rectangle, which
|
268
|
+
# situated between center of arc and chord, but does not
|
269
|
+
# intersect the chord.
|
270
|
+
rect = PerfectShape::Rectangle.new(x: x, y: y, width: w, height: h)
|
271
|
+
if type == :pie || ext.abs > 180
|
272
|
+
# for PIE: try to find intersections with pie slices
|
273
|
+
line1 = PerfectShape::Line.new(points: [[axc, ayc], [sx, sy]])
|
274
|
+
line2 = PerfectShape::Line.new(points: [[axc, ayc], [ex, ey]])
|
275
|
+
return true if line1.intersect?(rect) || line2.intersect?(rect)
|
276
|
+
else
|
277
|
+
# for CHORD and OPEN: try to find intersections with chord
|
278
|
+
line = PerfectShape::Line.new(points: [[sx, sy], [ex, ey]])
|
279
|
+
return true if line.intersect?(rect)
|
280
|
+
end
|
281
|
+
|
282
|
+
# finally check the rectangle corners inside the arc
|
283
|
+
return true if contain?(x, y) || contain?(x + w, y) ||
|
284
|
+
contain?(x, y + h) || contain?(x + w, y + h)
|
285
|
+
|
286
|
+
false
|
287
|
+
end
|
288
|
+
|
289
|
+
# Returns the starting point of the arc. This point is the
|
290
|
+
# intersection of the ray from the center defined by the
|
291
|
+
# starting angle and the elliptical boundary of the arc.
|
292
|
+
#
|
293
|
+
# @return An (x,y) pair Array object representing the
|
294
|
+
# x,y coordinates of the starting point of the arc.
|
295
|
+
def start_point
|
296
|
+
angle = Math.degrees_to_radians(-self.start)
|
297
|
+
x = self.x + (Math.cos(angle) * 0.5 + 0.5) * self.width
|
298
|
+
y = self.y + (Math.sin(angle) * 0.5 + 0.5) * self.height
|
299
|
+
[x, y]
|
300
|
+
end
|
301
|
+
|
302
|
+
# Returns the ending point of the arc. This point is the
|
303
|
+
# intersection of the ray from the center defined by the
|
304
|
+
# starting angle plus the angular extent of the arc and the
|
305
|
+
# elliptical boundary of the arc.
|
306
|
+
#
|
307
|
+
# @return An (x,y) pair Array object representing the
|
308
|
+
# x,y coordinates of the ending point of the arc.
|
309
|
+
def end_point
|
310
|
+
angle = Math.degrees_to_radians(-self.start - self.extent)
|
311
|
+
x = self.x + (Math.cos(angle) * 0.5 + 0.5) * self.width
|
312
|
+
y = self.y + (Math.sin(angle) * 0.5 + 0.5) * self.height
|
313
|
+
[x, y]
|
314
|
+
end
|
215
315
|
end
|
216
316
|
end
|
@@ -82,8 +82,8 @@ module PerfectShape
|
|
82
82
|
# @param x The X coordinate of the point to test.
|
83
83
|
# @param y The Y coordinate of the point to test.
|
84
84
|
#
|
85
|
-
# @return
|
86
|
-
# the cubic bézier curve,
|
85
|
+
# @return true if the point lies within the bound of
|
86
|
+
# the cubic bézier curve, false if the point lies outside of the
|
87
87
|
# cubic bézier curve's bounds.
|
88
88
|
def contain?(x_or_point, y = nil, outline: false, distance_tolerance: 0)
|
89
89
|
x, y = Point.normalize_point(x_or_point, y)
|
@@ -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 == PerfectShape::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 == PerfectShape::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 PerfectShape::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 != PerfectShape::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
|
@@ -59,8 +59,8 @@ module PerfectShape
|
|
59
59
|
# @param x The X coordinate of the point to test.
|
60
60
|
# @param y The Y coordinate of the point to test.
|
61
61
|
#
|
62
|
-
# @return
|
63
|
-
# the ellipse,
|
62
|
+
# @return true if the point lies within the bound of
|
63
|
+
# the ellipse, false if the point lies outside of the
|
64
64
|
# ellipse's bounds.
|
65
65
|
def contain?(x_or_point, y = nil, outline: false, distance_tolerance: 0)
|
66
66
|
# This is implemented again even though super would have just worked to have an optimized algorithm for Ellipse.
|
data/lib/perfect_shape/line.rb
CHANGED
@@ -54,11 +54,11 @@ module PerfectShape
|
|
54
54
|
# coordinates with respect to the line segment formed
|
55
55
|
# by the first two specified coordinates.
|
56
56
|
def relative_counterclockwise(x1, y1, x2, y2, px, py)
|
57
|
-
x2 -= x1
|
58
|
-
y2 -= y1
|
59
|
-
px -= x1
|
60
|
-
py -= y1
|
61
|
-
ccw = px * y2 - py * x2
|
57
|
+
x2 -= x1
|
58
|
+
y2 -= y1
|
59
|
+
px -= x1
|
60
|
+
py -= y1
|
61
|
+
ccw = px * y2 - py * x2
|
62
62
|
if ccw == 0.0
|
63
63
|
# The point is colinear, classify based on which side of
|
64
64
|
# the segment the point falls on. We can calculate a
|
@@ -66,7 +66,7 @@ module PerfectShape
|
|
66
66
|
# segment - a negative value indicates the point projects
|
67
67
|
# outside of the segment in the direction of the particular
|
68
68
|
# endpoint used as the origin for the projection.
|
69
|
-
ccw = px * x2 + py * y2
|
69
|
+
ccw = px * x2 + py * y2
|
70
70
|
if ccw > 0.0
|
71
71
|
# Reverse the projection to be relative to the original x2,y2
|
72
72
|
# x2 and y2 are simply negated.
|
@@ -75,13 +75,13 @@ module PerfectShape
|
|
75
75
|
# Since we really want to get a positive answer when the
|
76
76
|
# point is "beyond (x2,y2)", then we want to calculate
|
77
77
|
# the inverse anyway - thus we leave x2 & y2 negated.
|
78
|
-
px -= x2
|
79
|
-
py -= y2
|
80
|
-
ccw = px * x2 + py * y2
|
78
|
+
px -= x2
|
79
|
+
py -= y2
|
80
|
+
ccw = px * x2 + py * y2
|
81
81
|
ccw = 0.0 if ccw < 0.0
|
82
82
|
end
|
83
83
|
end
|
84
|
-
(ccw < 0.0) ? -1 : ((ccw > 0.0) ? 1 : 0)
|
84
|
+
(ccw < 0.0) ? -1 : ((ccw > 0.0) ? 1 : 0)
|
85
85
|
end
|
86
86
|
|
87
87
|
# Returns the square of the distance from a point to a line segment.
|
@@ -120,12 +120,12 @@ module PerfectShape
|
|
120
120
|
# px,py becomes relative vector from x1,y1 to test point
|
121
121
|
px -= x1
|
122
122
|
py -= y1
|
123
|
-
dot_product = px * x2 + py * y2
|
123
|
+
dot_product = px * x2 + py * y2
|
124
124
|
if dot_product <= 0.0
|
125
125
|
# px,py is on the side of x1,y1 away from x2,y2
|
126
126
|
# distance to segment is length of px,py vector
|
127
127
|
# "length of its (clipped) projection" is now 0.0
|
128
|
-
projected_length_square = BigDecimal('0.0')
|
128
|
+
projected_length_square = BigDecimal('0.0')
|
129
129
|
else
|
130
130
|
# switch to backwards vectors relative to x2,y2
|
131
131
|
# x2,y2 are already the negative of x1,y1=>x2,y2
|
@@ -191,10 +191,10 @@ module PerfectShape
|
|
191
191
|
def point_crossings(x1, y1, x2, y2, px, py)
|
192
192
|
return 0 if (py < y1 && py < y2)
|
193
193
|
return 0 if (py >= y1 && py >= y2)
|
194
|
-
# assert(y1 != y2)
|
194
|
+
# assert(y1 != y2)
|
195
195
|
return 0 if (px >= x1 && px >= x2)
|
196
196
|
return ((y1 < y2) ? 1 : -1) if (px < x1 && px < x2)
|
197
|
-
xintercept = x1 + (py - y1) * (x2 - x1) / (y2 - y1)
|
197
|
+
xintercept = x1 + (py - y1) * (x2 - x1) / (y2 - y1)
|
198
198
|
return 0 if (px >= xintercept)
|
199
199
|
(y1 < y2) ? 1 : -1
|
200
200
|
end
|
@@ -209,8 +209,8 @@ module PerfectShape
|
|
209
209
|
# @param y The Y coordinate of the point to test.
|
210
210
|
# @param distance_tolerance The distance from line to tolerate (0 by default)
|
211
211
|
#
|
212
|
-
# @return
|
213
|
-
# the line,
|
212
|
+
# @return true if the point lies within the bound of
|
213
|
+
# the line, false if the point lies outside of the
|
214
214
|
# line's bounds.
|
215
215
|
def contain?(x_or_point, y = nil, outline: true, distance_tolerance: 0)
|
216
216
|
x, y = Point.normalize_point(x_or_point, y)
|
@@ -242,6 +242,78 @@ module PerfectShape
|
|
242
242
|
Line.point_crossings(points[0][0], points[0][1], points[1][0], points[1][1], x, y)
|
243
243
|
end
|
244
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 PerfectShape::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 PerfectShape::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
|
+
PerfectShape::Rectangle::RECT_INTERSECTS
|
315
|
+
end
|
316
|
+
|
245
317
|
def intersect?(rectangle)
|
246
318
|
require 'perfect_shape/rectangle'
|
247
319
|
x1 = points[0][0]
|
data/lib/perfect_shape/path.rb
CHANGED
@@ -35,7 +35,7 @@ module PerfectShape
|
|
35
35
|
SHAPE_TYPES = [Array, PerfectShape::Point, PerfectShape::Line, PerfectShape::QuadraticBezierCurve, PerfectShape::CubicBezierCurve]
|
36
36
|
|
37
37
|
# Available winding rules
|
38
|
-
WINDING_RULES = [:
|
38
|
+
WINDING_RULES = [:wind_even_odd, :wind_non_zero]
|
39
39
|
|
40
40
|
attr_reader :winding_rule
|
41
41
|
attr_accessor :shapes, :closed
|
@@ -45,7 +45,7 @@ module PerfectShape
|
|
45
45
|
# Shape class types can be any of SHAPE_TYPES: Array (x,y coordinates), PerfectShape::Point, PerfectShape::Line, PerfectShape::QuadraticBezierCurve, or PerfectShape::CubicBezierCurve
|
46
46
|
# winding_rule can be any of WINDING_RULES: :wind_non_zero (default) or :wind_even_odd
|
47
47
|
# closed can be true or false
|
48
|
-
def initialize(shapes: [], closed: false, winding_rule: :
|
48
|
+
def initialize(shapes: [], closed: false, winding_rule: :wind_even_odd)
|
49
49
|
self.closed = closed
|
50
50
|
self.winding_rule = winding_rule
|
51
51
|
self.shapes = shapes
|
@@ -99,7 +99,7 @@ module PerfectShape
|
|
99
99
|
end
|
100
100
|
|
101
101
|
def winding_rule=(value)
|
102
|
-
raise "Invalid winding rule: #{value}" unless WINDING_RULES.include?(value.to_s.to_sym)
|
102
|
+
raise "Invalid winding rule: #{value} (must be one of #{WINDING_RULES})" unless WINDING_RULES.include?(value.to_s.to_sym)
|
103
103
|
@winding_rule = value
|
104
104
|
end
|
105
105
|
|
@@ -175,8 +175,8 @@ module PerfectShape
|
|
175
175
|
ci += 1
|
176
176
|
line = PerfectShape::Line.new(points: [[curx, cury], [endx, endy]])
|
177
177
|
crossings += line.point_crossings(x, y)
|
178
|
-
curx = endx
|
179
|
-
cury = endy
|
178
|
+
curx = endx
|
179
|
+
cury = endy
|
180
180
|
when :quad_to
|
181
181
|
quad_ctrlx = coords[ci]
|
182
182
|
ci += 1
|
@@ -188,8 +188,8 @@ module PerfectShape
|
|
188
188
|
ci += 1
|
189
189
|
quad = PerfectShape::QuadraticBezierCurve.new(points: [[curx, cury], [quad_ctrlx, quad_ctrly], [endx, endy]])
|
190
190
|
crossings += quad.point_crossings(x, y)
|
191
|
-
curx = endx
|
192
|
-
cury = endy
|
191
|
+
curx = endx
|
192
|
+
cury = endy
|
193
193
|
when :cubic_to
|
194
194
|
cubic_ctrl1x = coords[ci]
|
195
195
|
ci += 1
|
@@ -205,8 +205,8 @@ module PerfectShape
|
|
205
205
|
ci += 1
|
206
206
|
cubic = PerfectShape::CubicBezierCurve.new(points: [[curx, cury], [cubic_ctrl1x, cubic_ctrl1y], [cubic_ctrl2x, cubic_ctrl2y], [endx, endy]])
|
207
207
|
crossings += cubic.point_crossings(x, y)
|
208
|
-
curx = endx
|
209
|
-
cury = endy
|
208
|
+
curx = endx
|
209
|
+
cury = endy
|
210
210
|
when :close
|
211
211
|
if cury != movy
|
212
212
|
line = PerfectShape::Line.new(points: [[curx, cury], [movx, movy]])
|
@@ -267,5 +267,110 @@ module PerfectShape
|
|
267
267
|
the_disconnected_shapes << Line.new(points: [final_point, initial_point]) if closed?
|
268
268
|
the_disconnected_shapes.compact
|
269
269
|
end
|
270
|
+
|
271
|
+
def intersect?(rectangle)
|
272
|
+
x = rectangle.x
|
273
|
+
y = rectangle.y
|
274
|
+
w = rectangle.width
|
275
|
+
h = rectangle.height
|
276
|
+
# [xy]+[wh] is NaN if any of those values are NaN,
|
277
|
+
# or if adding the two together would produce NaN
|
278
|
+
# by virtue of adding opposing Infinte values.
|
279
|
+
# Since we need to add them below, their sum must
|
280
|
+
# not be NaN.
|
281
|
+
# We return false because NaN always produces a
|
282
|
+
# negative response to tests
|
283
|
+
return false if (x+w).nan? || (y+h).nan?
|
284
|
+
return false if w <= 0 || h <= 0
|
285
|
+
mask = winding_rule == :wind_non_zero ? -1 : 2
|
286
|
+
crossings = rect_crossings(x, y, x+w, y+h)
|
287
|
+
crossings == PerfectShape::Rectangle::RECT_INTERSECTS ||
|
288
|
+
(crossings & mask) != 0
|
289
|
+
end
|
290
|
+
|
291
|
+
def rect_crossings(rxmin, rymin, rxmax, rymax)
|
292
|
+
numTypes = drawing_types.count
|
293
|
+
return 0 if numTypes == 0
|
294
|
+
coords = points.flatten
|
295
|
+
curx = cury = movx = movy = endx = endy = nil
|
296
|
+
curx = movx = coords[0]
|
297
|
+
cury = movy = coords[1]
|
298
|
+
crossings = 0
|
299
|
+
ci = 2
|
300
|
+
i = 1
|
301
|
+
|
302
|
+
while crossings != PerfectShape::Rectangle::RECT_INTERSECTS && i < numTypes
|
303
|
+
case drawing_types[i]
|
304
|
+
when :move_to
|
305
|
+
if curx != movx || cury != movy
|
306
|
+
line = PerfectShape::Line.new(points: [curx, cury, movx, movy])
|
307
|
+
crossings = line.rect_crossings(rxmin, rymin, rxmax, rymax, crossings)
|
308
|
+
end
|
309
|
+
# Count should always be a multiple of 2 here.
|
310
|
+
# assert((crossings & 1) != 0)
|
311
|
+
movx = curx = coords[ci]
|
312
|
+
ci += 1
|
313
|
+
movy = cury = coords[ci]
|
314
|
+
ci += 1
|
315
|
+
when :line_to
|
316
|
+
endx = coords[ci]
|
317
|
+
ci += 1
|
318
|
+
endy = coords[ci]
|
319
|
+
ci += 1
|
320
|
+
line = PerfectShape::Line.new(points: [curx, cury, endx, endy])
|
321
|
+
crossings = line.rect_crossings(rxmin, rymin, rxmax, rymax, crossings)
|
322
|
+
curx = endx
|
323
|
+
cury = endy
|
324
|
+
when :quad_to
|
325
|
+
cx = coords[ci]
|
326
|
+
ci += 1
|
327
|
+
cy = coords[ci]
|
328
|
+
ci += 1
|
329
|
+
endx = coords[ci]
|
330
|
+
ci += 1
|
331
|
+
endy = coords[ci]
|
332
|
+
ci += 1
|
333
|
+
quadratic_bezier_curve = PerfectShape::QuadraticBezierCurve.new(points: [curx, cury, cx, cy, endx, endy])
|
334
|
+
crossings = quadratic_bezier_curve.rect_crossings(rxmin, rymin, rxmax, rymax, 0, crossings)
|
335
|
+
curx = endx
|
336
|
+
cury = endy
|
337
|
+
when :cubic_to
|
338
|
+
c1x = coords[ci]
|
339
|
+
ci += 1
|
340
|
+
c1y = coords[ci]
|
341
|
+
ci += 1
|
342
|
+
c2x = coords[ci]
|
343
|
+
ci += 1
|
344
|
+
c2y = coords[ci]
|
345
|
+
ci += 1
|
346
|
+
endx = coords[ci]
|
347
|
+
ci += 1
|
348
|
+
endy = coords[ci]
|
349
|
+
ci += 1
|
350
|
+
cubic_bezier_curve = PerfectShape::CubicBezierCurve.new(points: [curx, cury, c1x, c1y, c2x, c2y, endx, endy])
|
351
|
+
crossings = cubic_bezier_curve.rect_crossings(rxmin, rymin, rxmax, rymax, 0, crossings)
|
352
|
+
curx = endx
|
353
|
+
cury = endy
|
354
|
+
when :close
|
355
|
+
if curx != movx || cury != movy
|
356
|
+
line = PerfectShape::Line.new(points: [curx, cury, movx, movy])
|
357
|
+
crossings = line.rect_crossings(rxmin, rymin, rxmax, rymax, crossings)
|
358
|
+
end
|
359
|
+
curx = movx
|
360
|
+
cury = movy
|
361
|
+
# Count should always be a multiple of 2 here.
|
362
|
+
# assert((crossings & 1) != 0)
|
363
|
+
end
|
364
|
+
i += 1
|
365
|
+
end
|
366
|
+
if crossings != PerfectShape::Rectangle::RECT_INTERSECTS &&
|
367
|
+
(curx != movx || cury != movy)
|
368
|
+
line = PerfectShape::Line.new(points: [curx, cury, movx, movy])
|
369
|
+
crossings = line.rect_crossings(rxmin, rymin, rxmax, rymax, crossings)
|
370
|
+
end
|
371
|
+
# Count should always be a multiple of 2 here.
|
372
|
+
# assert((crossings & 1) != 0)
|
373
|
+
crossings
|
374
|
+
end
|
270
375
|
end
|
271
376
|
end
|
data/lib/perfect_shape/point.rb
CHANGED
@@ -80,8 +80,8 @@ module PerfectShape
|
|
80
80
|
# @param y The Y coordinate of the point to test.
|
81
81
|
# @param distance_tolerance The distance from point to tolerate (0 by default)
|
82
82
|
#
|
83
|
-
# @return
|
84
|
-
#
|
83
|
+
# @return true if the point is close enough within distance tolerance,
|
84
|
+
# false if the point is too far.
|
85
85
|
def contain?(x_or_point, y = nil, outline: true, distance_tolerance: 0)
|
86
86
|
x, y = Point.normalize_point(x_or_point, y)
|
87
87
|
return unless x && y
|
@@ -22,20 +22,37 @@
|
|
22
22
|
require 'perfect_shape/shape'
|
23
23
|
require 'perfect_shape/point'
|
24
24
|
require 'perfect_shape/multi_point'
|
25
|
+
require 'perfect_shape/path'
|
25
26
|
|
26
27
|
module PerfectShape
|
27
28
|
class Polygon < Shape
|
28
29
|
include MultiPoint
|
29
30
|
include Equalizer.new(:points)
|
30
31
|
|
32
|
+
WINDING_RULES = PerfectShape::Path::WINDING_RULES
|
33
|
+
|
34
|
+
attr_reader :winding_rule
|
35
|
+
|
36
|
+
def initialize(points: [], winding_rule: :wind_even_odd)
|
37
|
+
super(points: points)
|
38
|
+
self.winding_rule = winding_rule
|
39
|
+
end
|
40
|
+
|
41
|
+
def winding_rule=(value)
|
42
|
+
raise "Invalid winding rule: #{value} (must be one of #{WINDING_RULES})" unless WINDING_RULES.include?(value.to_s.to_sym)
|
43
|
+
@winding_rule = value
|
44
|
+
end
|
45
|
+
|
31
46
|
# Checks if polygon contains point (two-number Array or x, y args)
|
32
|
-
# using the Ray Casting Algorithm (aka Even-Odd Rule)
|
47
|
+
# using the Ray Casting Algorithm (aka Even-Odd Rule)
|
48
|
+
# or Winding Number Algorithm (aka Nonzero Rule)
|
49
|
+
# Details: https://en.wikipedia.org/wiki/Point_in_polygon
|
33
50
|
#
|
34
51
|
# @param x The X coordinate of the point to test.
|
35
52
|
# @param y The Y coordinate of the point to test.
|
36
53
|
#
|
37
|
-
# @return
|
38
|
-
# the polygon,
|
54
|
+
# @return true if the point lies within the bound of
|
55
|
+
# the polygon, false if the point lies outside of the
|
39
56
|
# polygon's bounds.
|
40
57
|
def contain?(x_or_point, y = nil, outline: false, distance_tolerance: 0)
|
41
58
|
x, y = Point.normalize_point(x_or_point, y)
|
@@ -43,76 +60,11 @@ module PerfectShape
|
|
43
60
|
if outline
|
44
61
|
edges.any? { |edge| edge.contain?(x, y, distance_tolerance: distance_tolerance) }
|
45
62
|
else
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
hits = 0
|
51
|
-
|
52
|
-
lastx = xpoints[npoints - 1]
|
53
|
-
lasty = ypoints[npoints - 1]
|
54
|
-
|
55
|
-
# Walk the edges of the polygon
|
56
|
-
npoints.times do |i|
|
57
|
-
curx = xpoints[i]
|
58
|
-
cury = ypoints[i]
|
59
|
-
|
60
|
-
if cury == lasty
|
61
|
-
lastx = curx
|
62
|
-
lasty = cury
|
63
|
-
next
|
64
|
-
end
|
65
|
-
|
66
|
-
if curx < lastx
|
67
|
-
if x >= lastx
|
68
|
-
lastx = curx
|
69
|
-
lasty = cury
|
70
|
-
next
|
71
|
-
end
|
72
|
-
leftx = curx
|
73
|
-
else
|
74
|
-
if x >= curx
|
75
|
-
lastx = curx
|
76
|
-
lasty = cury
|
77
|
-
next
|
78
|
-
end
|
79
|
-
leftx = lastx
|
80
|
-
end
|
81
|
-
|
82
|
-
if cury < lasty
|
83
|
-
if y < cury || y >= lasty
|
84
|
-
lastx = curx
|
85
|
-
lasty = cury
|
86
|
-
next
|
87
|
-
end
|
88
|
-
if x < leftx
|
89
|
-
hits += 1
|
90
|
-
lastx = curx
|
91
|
-
lasty = cury
|
92
|
-
next
|
93
|
-
end
|
94
|
-
test1 = x - curx
|
95
|
-
test2 = y - cury
|
96
|
-
else
|
97
|
-
if y < lasty || y >= cury
|
98
|
-
lastx = curx
|
99
|
-
lasty = cury
|
100
|
-
next
|
101
|
-
end
|
102
|
-
if x < leftx
|
103
|
-
hits += 1
|
104
|
-
lastx = curx
|
105
|
-
lasty = cury
|
106
|
-
next
|
107
|
-
end
|
108
|
-
test1 = x - lastx
|
109
|
-
test2 = y - lasty
|
110
|
-
end
|
111
|
-
|
112
|
-
hits += 1 if (test1 < (test2 / (lasty - cury) * (lastx - curx)))
|
63
|
+
if winding_rule == :wind_even_odd
|
64
|
+
wind_even_odd_contain?(x, y)
|
65
|
+
else
|
66
|
+
path.contain?(x, y)
|
113
67
|
end
|
114
|
-
|
115
|
-
(hits & 1) != 0
|
116
68
|
end
|
117
69
|
end
|
118
70
|
|
@@ -121,5 +73,92 @@ module PerfectShape
|
|
121
73
|
Line.new(points: [[point1.first, point1.last], [point2.first, point2.last]])
|
122
74
|
end
|
123
75
|
end
|
76
|
+
|
77
|
+
def path
|
78
|
+
path_shapes = []
|
79
|
+
path_shapes << PerfectShape::Point.new(points[0])
|
80
|
+
path_shapes += points[1..-1].map { |point| PerfectShape::Line.new(points: [point]) }
|
81
|
+
PerfectShape::Path.new(shapes: path_shapes, closed: true, winding_rule: winding_rule)
|
82
|
+
end
|
83
|
+
|
84
|
+
def intersect?(rectangle)
|
85
|
+
path.intersect?(rectangle)
|
86
|
+
end
|
87
|
+
|
88
|
+
private
|
89
|
+
|
90
|
+
# optimized even-odd rule point in polygon algorithm (uses less memory than PerfectShape::Path algorithms)
|
91
|
+
def wind_even_odd_contain?(x, y)
|
92
|
+
npoints = points.count
|
93
|
+
xpoints = points.map(&:first)
|
94
|
+
ypoints = points.map(&:last)
|
95
|
+
return false if npoints <= 2 || !bounding_box.contain?(x, y)
|
96
|
+
hits = 0
|
97
|
+
|
98
|
+
lastx = xpoints[npoints - 1]
|
99
|
+
lasty = ypoints[npoints - 1]
|
100
|
+
|
101
|
+
# Walk the edges of the polygon
|
102
|
+
npoints.times do |i|
|
103
|
+
curx = xpoints[i]
|
104
|
+
cury = ypoints[i]
|
105
|
+
|
106
|
+
if cury == lasty
|
107
|
+
lastx = curx
|
108
|
+
lasty = cury
|
109
|
+
next
|
110
|
+
end
|
111
|
+
|
112
|
+
if curx < lastx
|
113
|
+
if x >= lastx
|
114
|
+
lastx = curx
|
115
|
+
lasty = cury
|
116
|
+
next
|
117
|
+
end
|
118
|
+
leftx = curx
|
119
|
+
else
|
120
|
+
if x >= curx
|
121
|
+
lastx = curx
|
122
|
+
lasty = cury
|
123
|
+
next
|
124
|
+
end
|
125
|
+
leftx = lastx
|
126
|
+
end
|
127
|
+
|
128
|
+
if cury < lasty
|
129
|
+
if y < cury || y >= lasty
|
130
|
+
lastx = curx
|
131
|
+
lasty = cury
|
132
|
+
next
|
133
|
+
end
|
134
|
+
if x < leftx
|
135
|
+
hits += 1
|
136
|
+
lastx = curx
|
137
|
+
lasty = cury
|
138
|
+
next
|
139
|
+
end
|
140
|
+
test1 = x - curx
|
141
|
+
test2 = y - cury
|
142
|
+
else
|
143
|
+
if y < lasty || y >= cury
|
144
|
+
lastx = curx
|
145
|
+
lasty = cury
|
146
|
+
next
|
147
|
+
end
|
148
|
+
if x < leftx
|
149
|
+
hits += 1
|
150
|
+
lastx = curx
|
151
|
+
lasty = cury
|
152
|
+
next
|
153
|
+
end
|
154
|
+
test1 = x - lastx
|
155
|
+
test2 = y - lasty
|
156
|
+
end
|
157
|
+
|
158
|
+
hits += 1 if (test1 < (test2 / (lasty - cury) * (lastx - curx)))
|
159
|
+
end
|
160
|
+
|
161
|
+
(hits & 1) != 0
|
162
|
+
end
|
124
163
|
end
|
125
164
|
end
|
@@ -94,22 +94,22 @@ module PerfectShape
|
|
94
94
|
]
|
95
95
|
end
|
96
96
|
|
97
|
-
# Solves the quadratic whose coefficients are in the
|
98
|
-
# array and places the non-complex roots into the
|
97
|
+
# Solves the quadratic whose coefficients are in the eqn
|
98
|
+
# array and places the non-complex roots into the res
|
99
99
|
# array, returning the number of roots.
|
100
100
|
# The quadratic solved is represented by the equation:
|
101
101
|
# <pre>
|
102
102
|
# eqn = {C, B, A}
|
103
103
|
# ax^2 + bx + c = 0
|
104
104
|
# </pre>
|
105
|
-
# A return value of
|
105
|
+
# A return value of -1 is used to distinguish a constant
|
106
106
|
# equation, which might be always 0 or never 0, from an equation that
|
107
107
|
# has no zeroes.
|
108
108
|
# @param eqn the specified array of coefficients to use to solve
|
109
109
|
# the quadratic equation
|
110
110
|
# @param res the array that contains the non-complex roots
|
111
111
|
# resulting from the solution of the quadratic equation
|
112
|
-
# @return the number of roots, or
|
112
|
+
# @return the number of roots, or -1 if the equation is
|
113
113
|
# a constant.
|
114
114
|
def solve_quadratic(eqn, res)
|
115
115
|
a = eqn[2]
|
@@ -187,8 +187,8 @@ module PerfectShape
|
|
187
187
|
# @param x The X coordinate of the point to test.
|
188
188
|
# @param y The Y coordinate of the point to test.
|
189
189
|
#
|
190
|
-
# @return
|
191
|
-
# the quadratic bézier curve,
|
190
|
+
# @return true if the point lies within the bound of
|
191
|
+
# the quadratic bézier curve, false if the point lies outside of the
|
192
192
|
# quadratic bézier curve's bounds.
|
193
193
|
def contain?(x_or_point, y = nil, outline: false, distance_tolerance: 0)
|
194
194
|
x, y = Point.normalize_point(x_or_point, y)
|
@@ -545,5 +545,72 @@ module PerfectShape
|
|
545
545
|
# overlap the Y range of the rectangle.
|
546
546
|
c1tag * c2tag <= 0
|
547
547
|
end
|
548
|
+
|
549
|
+
# Accumulate the number of times the quad crosses the shadow
|
550
|
+
# extending to the right of the rectangle. See the comment
|
551
|
+
# for the RECT_INTERSECTS constant for more complete details.
|
552
|
+
#
|
553
|
+
# crossings arg is the initial crossings value to add to (useful
|
554
|
+
# in cases where you want to accumulate crossings from multiple
|
555
|
+
# shapes)
|
556
|
+
def rect_crossings(rxmin, rymin, rxmax, rymax, level, crossings = 0)
|
557
|
+
x0 = points[0][0]
|
558
|
+
y0 = points[0][1]
|
559
|
+
xc = points[1][0]
|
560
|
+
yc = points[1][1]
|
561
|
+
x1 = points[2][0]
|
562
|
+
y1 = points[2][1]
|
563
|
+
return crossings if y0 >= rymax && yc >= rymax && y1 >= rymax
|
564
|
+
return crossings if y0 <= rymin && yc <= rymin && y1 <= rymin
|
565
|
+
return crossings if x0 <= rxmin && xc <= rxmin && x1 <= rxmin
|
566
|
+
if x0 >= rxmax && xc >= rxmax && x1 >= rxmax
|
567
|
+
# Quad is entirely to the right of the rect
|
568
|
+
# and the vertical range of the 3 Y coordinates of the quad
|
569
|
+
# overlaps the vertical range of the rect by a non-empty amount
|
570
|
+
# We now judge the crossings solely based on the line segment
|
571
|
+
# connecting the endpoints of the quad.
|
572
|
+
# Note that we may have 0, 1, or 2 crossings as the control
|
573
|
+
# point may be causing the Y range intersection while the
|
574
|
+
# two endpoints are entirely above or below.
|
575
|
+
if y0 < y1
|
576
|
+
# y-increasing line segment...
|
577
|
+
crossings += 1 if y0 <= rymin && y1 > rymin
|
578
|
+
crossings += 1 if y0 < rymax && y1 >= rymax
|
579
|
+
elsif y1 < y0
|
580
|
+
# y-decreasing line segment...
|
581
|
+
crossings -= 1 if y1 <= rymin && y0 > rymin
|
582
|
+
crossings -= 1 if y1 < rymax && y0 >= rymax
|
583
|
+
end
|
584
|
+
return crossings
|
585
|
+
end
|
586
|
+
# The intersection of ranges is more complicated
|
587
|
+
# First do trivial INTERSECTS rejection of the cases
|
588
|
+
# where one of the endpoints is inside the rectangle.
|
589
|
+
return PerfectShape::Rectangle::RECT_INTERSECTS if (x0 < rxmax && x0 > rxmin && y0 < rymax && y0 > rymin) ||
|
590
|
+
(x1 < rxmax && x1 > rxmin && y1 < rymax && y1 > rymin)
|
591
|
+
# Otherwise, subdivide and look for one of the cases above.
|
592
|
+
# double precision only has 52 bits of mantissa
|
593
|
+
if level > 52
|
594
|
+
line = PerfectShape::Line.new(points: [x0, y0, x1, y1])
|
595
|
+
return line.rect_crossings(rxmin, rymin, rxmax, rymax, crossings)
|
596
|
+
end
|
597
|
+
x0c = BigDecimal((x0 + xc).to_s) / 2
|
598
|
+
y0c = BigDecimal((y0 + yc).to_s) / 2
|
599
|
+
xc1 = BigDecimal((xc + x1).to_s) / 2
|
600
|
+
yc1 = BigDecimal((yc + y1).to_s) / 2
|
601
|
+
xc = BigDecimal((x0c + xc1).to_s) / 2
|
602
|
+
yc = BigDecimal((y0c + yc1).to_s) / 2
|
603
|
+
# [xy]c are NaN if any of [xy]0c or [xy]c1 are NaN
|
604
|
+
# [xy]0c or [xy]c1 are NaN if any of [xy][0c1] are NaN
|
605
|
+
# These values are also NaN if opposing infinities are added
|
606
|
+
return 0 if xc.nan? || yc.nan?
|
607
|
+
quad1 = QuadraticBezierCurve.new(points: [x0, y0, x0c, y0c, xc, yc])
|
608
|
+
crossings = quad1.rect_crossings(rxmin, rymin, rxmax, rymax, level+1, crossings)
|
609
|
+
if crossings != PerfectShape::Rectangle::RECT_INTERSECTS
|
610
|
+
quad2 = QuadraticBezierCurve.new(points: [xc, yc, xc1, yc1, x1, y1])
|
611
|
+
crossings = quad2.rect_crossings(rxmin, rymin, rxmax, rymax, level+1, crossings)
|
612
|
+
end
|
613
|
+
crossings
|
614
|
+
end
|
548
615
|
end
|
549
616
|
end
|
@@ -41,13 +41,15 @@ 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.
|
47
49
|
# @param y The Y coordinate of the point to test.
|
48
50
|
#
|
49
|
-
# @return
|
50
|
-
# the rectangle,
|
51
|
+
# @return true if the point lies within the bound of
|
52
|
+
# the rectangle, false if the point lies outside of the
|
51
53
|
# rectangle's bounds.
|
52
54
|
def contain?(x_or_point, y = nil, outline: false, distance_tolerance: 0)
|
53
55
|
x, y = Point.normalize_point(x_or_point, y)
|
@@ -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
|
+
# stub: perfect-shape 1.0.0 ruby lib
|
6
6
|
|
7
7
|
Gem::Specification.new do |s|
|
8
8
|
s.name = "perfect-shape".freeze
|
9
|
-
s.version = "0.
|
9
|
+
s.version = "1.0.0"
|
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-23"
|
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.
|
4
|
+
version: 1.0.0
|
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-23 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: equalizer
|