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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cf595115a6a9344cedfce9e93801cb5aba1960f4e970d064043e5f813716a0be
4
- data.tar.gz: bcb60359b840662739767882546e360ceb052e734f5d3d7836406e25ff7b72ae
3
+ metadata.gz: c200dfc203347661cd329e24cebc75ae038b0b69f6874832cfe8793ae2cbf6e5
4
+ data.tar.gz: 55cac9b991125bfcf15493eb5e05fecaea618d5d415e9c2b2c9d593efba98be4
5
5
  SHA512:
6
- metadata.gz: c57fb7619dd930974af23d8004ad134bc3fefb51c6dd03f5ae2e7a46cccae3011d7dd74ca9a08e8055c41ad93c9a4ef92132f76d7349fd5cc20ffefc5c7d371c
7
- data.tar.gz: ca7c2586198a35ae8e9dc1485a87298c11b4151e11aab16e54302775c4539ac94c7867a3b48598914dfb8367b87fa6ae166cdea5dbe9da32ee0161788692b6d2
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.5.2
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.5.2
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.5.2'
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 {@code -1} is used to distinguish a constant equation, which might be always 0 or never 0, from an equation that has no zeroes.
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)`: constructs a square
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) that is closed, has the [Even-Odd](https://en.wikipedia.org/wiki/Even%E2%80%93odd_rule) winding rule, and consists of lines only.
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.5.2
1
+ 1.0.0
@@ -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 {@code true} if the point lies within the bound of
145
- # the arc, {@code false} if the point lies outside of the
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
@@ -69,5 +69,9 @@ module PerfectShape
69
69
 
70
70
  shapes.any? { |shape| shape.contain?(x, y, outline: outline, distance_tolerance: distance_tolerance) }
71
71
  end
72
+
73
+ def intersect?(rectangle)
74
+ shapes.any? { |shape| shape.intersect?(rectangle) }
75
+ end
72
76
  end
73
77
  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 {@code true} if the point lies within the bound of
86
- # the cubic bézier curve, {@code false} if the point lies outside of the
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 {@code true} if the point lies within the bound of
63
- # the ellipse, {@code false} if the point lies outside of the
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.
@@ -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 {@code true} if the point lies within the bound of
213
- # the line, {@code false} if the point lies outside of the
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]
@@ -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 = [:wind_non_zero, :wind_even_odd]
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: :wind_non_zero)
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
@@ -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 {@code true} if the point is close enough within distance tolerance,
84
- # {@code false} if the point is too far.
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): https://en.wikipedia.org/wiki/Point_in_polygon
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 {@code true} if the point lies within the bound of
38
- # the polygon, {@code false} if the point lies outside of the
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
- npoints = points.count
47
- xpoints = points.map(&:first)
48
- ypoints = points.map(&:last)
49
- return false if npoints <= 2 || !bounding_box.contain?(x, y)
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 {@code eqn}
98
- # array and places the non-complex roots into the {@code res}
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 {@code -1} is used to distinguish a constant
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 {@code -1} if the equation is
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 {@code true} if the point lies within the bound of
191
- # the quadratic bézier curve, {@code false} if the point lies outside of the
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 {@code true} if the point lies within the bound of
50
- # the rectangle, {@code false} if the point lies outside of the
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
@@ -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
@@ -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.2 ruby lib
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.5.2"
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-20"
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.5.2
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-20 00:00:00.000000000 Z
11
+ date: 2022-01-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: equalizer