perfect-shape 0.5.5 → 1.0.2

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: f32696a2e0ecd7c1aceb16d9211b38e977b4dd15c7ba57acb8ac99a769d75036
4
- data.tar.gz: 50c458cfcf134ce86bc11e0b22f1650b2faf2146c79c288d7b2501a2d7d9264c
3
+ metadata.gz: 73a1e3f4816ce8829788f6ad76e83a88429301000bb49aca7bab878148d90527
4
+ data.tar.gz: f2628350798a4f5285f3c82941154e21a52aa72b401f3a3fdd889e720554eade
5
5
  SHA512:
6
- metadata.gz: 1e15378328df2eaad3769ce087118f7990fcb834963cbc56b738ea1cc292a92df0f86d77506e1a2f94985bc03211b5436b9965e8b6b5c97c80f9d7ad59f6cb20
7
- data.tar.gz: cb64f0b3951ca4628f96e265070a88684060be6ffa38b5a45deea611bce0b2d20b72e43ef6c5d70b969925a31fe0017f4ec84407927f23a1b7824e0352bc9732
6
+ metadata.gz: d79de0f8201d530f55792faea84bb8e9d47608a06f234d305aeb5dcf8ca8fefc612c9eebda0b449ece028a3d281f6b5d8824c41c6b7703d9e575e9cb6cabaa55
7
+ data.tar.gz: 861950d0dd6107a7de5f5d5f59e192770eb6255024881d67a4e67b3ca229fdc1ff29f176f40223bdb0c8ef67e38cfc46a33313c1d9adb9b37c7fb84f688dc491
data/CHANGELOG.md CHANGED
@@ -1,5 +1,31 @@
1
1
  # Change Log
2
2
 
3
+ ## 1.0.2
4
+
5
+ - `Path` can contain an `Arc`, `Ellipse`, or `Circle` (to get affected by the winding algorithm as opposed to `CompositeShape` which has no winding algorithm)
6
+ - `Arc`, `Ellipse`, and `Circle` `#to_path_shapes` method, which decomposes them into `Point`s, `Line`s, and `CubicBezierCurve`s to be added to a `Path`
7
+
8
+ ## 1.0.1
9
+
10
+ - Relax `equalizer` gem version to '>= 0.0.11', '< 1.1.0' to avoid conflicts in projects that might use future versions
11
+ - Fix issue with infinite loop upon setting `Circle#radius=` to a number with very small decimals
12
+ - Fix issue with infinite loop upon setting `Circle#radius_x=` to a number with very small decimals
13
+ - Fix issue with infinite loop upon setting `Circle#radius_y=` to a number with very small decimals
14
+ - Fix issue with infinite loop upon setting `Circle#diameter=` to a number with very small decimals
15
+ - Fix issue with infinite loop upon setting `Circle#width=` to a number with very small decimals
16
+ - Fix issue with infinite loop upon setting `Circle#height=` to a number with very small decimals
17
+ - Fix issue with infinite loop upon setting `Square#length=` to a number with very small decimals
18
+ - Fix issue with infinite loop upon setting `Square#width=` to a number with very small decimals
19
+ - Fix issue with infinite loop upon setting `Square#height=` to a number with very small decimals
20
+
21
+ ## 1.0.0
22
+
23
+ - `PerfectShape::Path#intersect?(rectangle)`
24
+ - `PerfectShape::Polygon#intersect?(rectangle)`
25
+ - `PerfectShape::CompositeShape#intersect?(rectangle)`
26
+ - [API Breaking] Change `Path` default `winding_rule` to `:wind_even_odd`
27
+ - Make `PerfectShape::Polygon` support `:wind_non_zero` `winding_rule` (not just default `:wind_even_odd`)
28
+
3
29
  ## 0.5.5
4
30
 
5
31
  - `PerfectShape::Arc#intersect?(rectangle)`
data/README.md CHANGED
@@ -1,9 +1,9 @@
1
- # Perfect Shape 0.5.5
1
+ # Perfect Shape 1.0.2
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)
5
5
 
6
- [`PerfectShape`](https://rubygems.org/gems/perfect-shape) is a collection of pure Ruby geometric algorithms that are mostly useful for GUI (Graphical User Interface) manipulation like checking viewport rectangle intersection or containment of a mouse click [point](#perfectshapepoint) in popular geometry shapes such as [rectangle](#perfectshaperectangle), [square](#perfectshapesquare), [arc](#perfectshapearc) (open, chord, and pie), [ellipse](#perfectshapeellipse), [circle](#perfectshapecircle), [polygon](#perfectshapepolygon), and [paths](#perfectshapepath) containing [lines](#perfectshapeline), [quadratic bézier curves](#perfectshapequadraticbeziercurve), and [cubic bezier curves](#perfectshapecubicbeziercurve), potentially with [affine transforms](#perfectshapeaffinetransform) applied like translation, scale, rotation, shear/skew, and inversion (including both [Ray Casting Algorithm](https://en.wikipedia.org/wiki/Point_in_polygon#Ray_casting_algorithm), aka [Even-odd Rule](https://en.wikipedia.org/wiki/Even%E2%80%93odd_rule), and [Winding Number Algorithm](https://en.wikipedia.org/wiki/Point_in_polygon#Winding_number_algorithm), aka [Nonzero Rule](https://en.wikipedia.org/wiki/Nonzero-rule)).
6
+ [`PerfectShape`](https://rubygems.org/gems/perfect-shape) is a collection of pure Ruby geometric algorithms that are mostly useful for GUI (Graphical User Interface) manipulation like checking viewport rectangle intersection or containment of a mouse click [point](#perfectshapepoint) in popular geometry shapes such as [rectangle](#perfectshaperectangle), [square](#perfectshapesquare), [arc](#perfectshapearc) (open, chord, and pie), [ellipse](#perfectshapeellipse), [circle](#perfectshapecircle), [polygon](#perfectshapepolygon), and [paths](#perfectshapepath) containing [lines](#perfectshapeline), [quadratic bézier curves](#perfectshapequadraticbeziercurve), and [cubic bezier curves](#perfectshapecubicbeziercurve), potentially with [affine transforms](#perfectshapeaffinetransform) applied like translation, scale, rotation, shear/skew, and inversion (including both the [Ray Casting Algorithm](https://en.wikipedia.org/wiki/Point_in_polygon#Ray_casting_algorithm), aka [Even-odd Rule](https://en.wikipedia.org/wiki/Even%E2%80%93odd_rule), and the [Winding Number Algorithm](https://en.wikipedia.org/wiki/Point_in_polygon#Winding_number_algorithm), aka [Nonzero Rule](https://en.wikipedia.org/wiki/Nonzero-rule)).
7
7
 
8
8
  Additionally, [`PerfectShape::Math`](#perfectshapemath) contains some purely mathematical algorithms, like [IEEE 754-1985 Remainder](https://en.wikipedia.org/wiki/IEEE_754-1985).
9
9
 
@@ -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.5
17
+ gem install perfect-shape -v 1.0.2
18
18
  ```
19
19
 
20
20
  Or include in Bundler `Gemfile`:
21
21
 
22
22
  ```ruby
23
- gem 'perfect-shape', '~> 0.5.5'
23
+ gem 'perfect-shape', '~> 1.0.2'
24
24
  ```
25
25
 
26
26
  And, run:
@@ -275,6 +275,7 @@ Includes `PerfectShape::MultiPoint`
275
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`)
276
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`.
277
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)
278
279
 
279
280
  Example:
280
281
 
@@ -466,6 +467,8 @@ Open Arc | Chord Arc | Pie Arc
466
467
  - `#start`: start angle in degrees
467
468
  - `#extent`: extent angle in degrees
468
469
  - `#center_point`: center point as `Array` of `[center_x, center_y]` coordinates
470
+ - `#start_point`: start point as `Array` of (x,y) coordinates
471
+ - `#end_point`: end point as `Array` of (x,y) coordinates
469
472
  - `#center_x`: center x
470
473
  - `#center_y`: center y
471
474
  - `#radius_x`: radius along the x-axis
@@ -479,6 +482,8 @@ Open Arc | Chord Arc | Pie Arc
479
482
  - `#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
480
483
  - `#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
481
484
  - `#contain_angle?(angle)`: returns `true` if the angle is within the angular extents of the arc and `false` otherwise
485
+ - `#to_path_shapes`: Converts `Arc` into basic `Path` shapes made up of `Point`s, `Line`s, and `CubicBezierCurve`s. Used by `Path` when adding an `Arc` to `Path` `shapes`
486
+ - `#btan(increment)`: btan computes the length (k) of the control segments at the beginning and end of a cubic bezier that approximates a segment of an arc with extent less than or equal to 90 degrees. This length (k) will be used to generate the 2 bezier control points for such a segment.
482
487
 
483
488
  Example:
484
489
 
@@ -603,6 +608,7 @@ Extends `PerfectShape::Arc`
603
608
  - `#==(other)`: Returns `true` if equal to `other` or `false` otherwise
604
609
  - `#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
605
610
  - `#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
611
+ - `#to_path_shapes`: Converts `Ellipse` into basic `Path` shapes made up of `Point`s, `Line`s, and `CubicBezierCurve`s. Used by `Path` when adding an `Ellipse` to `Path` `shapes`
606
612
 
607
613
  Example:
608
614
 
@@ -665,6 +671,7 @@ Extends `PerfectShape::Ellipse`
665
671
  - `#==(other)`: Returns `true` if equal to `other` or `false` otherwise
666
672
  - `#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
667
673
  - `#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
674
+ - `#to_path_shapes`: Converts `Circle` into basic `Path` shapes made up of `Point`s, `Line`s, and `CubicBezierCurve`s. Used by `Path` when adding a `Circle` to `Path` `shapes`
668
675
 
669
676
  Example:
670
677
 
@@ -704,11 +711,11 @@ Extends `PerfectShape::Shape`
704
711
 
705
712
  Includes `PerfectShape::MultiPoint`
706
713
 
707
- 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.
714
+ 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.
708
715
 
709
716
  ![polygon](https://raw.githubusercontent.com/AndyObtiva/perfect-shape/master/images/polygon.png)
710
717
 
711
- - `::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
718
+ - `::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`)
712
719
  - `#min_x`: min x
713
720
  - `#min_y`: min y
714
721
  - `#max_x`: max x
@@ -720,7 +727,8 @@ A polygon can be thought of as a special case of [path](#perfectshapepath) that
720
727
  - `#center_y`: center y
721
728
  - `#bounding_box`: bounding box is a rectangle with x = min x, y = min y, and width/height of shape
722
729
  - `#==(other)`: Returns `true` if equal to `other` or `false` otherwise
723
- - `#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
730
+ - `#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
731
+ - `#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
724
732
  - `#edges`: edges of polygon as `PerfectShape::Line` objects
725
733
 
726
734
  Example:
@@ -752,8 +760,9 @@ Includes `PerfectShape::MultiPoint`
752
760
 
753
761
  ![path](https://raw.githubusercontent.com/AndyObtiva/perfect-shape/master/images/path.png)
754
762
 
755
- - `::new(shapes: [], closed: false, winding_rule: :wind_non_zero)`: constructs a path with `shapes` as `Array` of shape objects, which can be `PerfectShape::Point` (or `Array` of `[x, y]` coordinates), `PerfectShape::Line`, `PerfectShape::QuadraticBezierCurve`, or `PerfectShape::CubicBezierCurve`. If a path is closed, its last point is automatically connected to its first point with a line segment. The winding rule can be `:wind_non_zero` (default) or `:wind_even_odd`.
756
- - `#shapes`: the shapes that the path is composed of (must always start with `PerfectShape::Point` or Array of [x,y] coordinates representing start point)
763
+ - `::new(shapes: [], closed: false, winding_rule: :wind_even_odd, line_to_complex_shapes: false)`: constructs a path with `shapes` as `Array` of shape objects, which can be `PerfectShape::Point` (or `Array` of `[x, y]` coordinates), `PerfectShape::Line`, `PerfectShape::QuadraticBezierCurve`, `PerfectShape::CubicBezierCurve`, or complex shapes that decompose into the aforementioned basic path shapes, like `PerfectShape::Arc`, `PerfectShape::Ellipse`, and `PerfectShape::Circle`. If a path is closed, its last point is automatically connected to its first point with a line segment. The winding rule can be `:wind_non_zero` (default) or `:wind_even_odd`. `line_to_complex_shapes` can be `true` or `false` (default), indicating whether to connect to complex shapes, meaning `Arc`, `Ellipse`, and `Circle`, with a line, or otherwise move to their start point instead.
764
+ - `#shapes`: the shapes that the path is composed of (must always start with `PerfectShape::Point` or Array of `[x,y]` coordinates representing start point)
765
+ - `#basic_shapes`: the basic shapes that the path is composed of, meaning only `Point`, `Line`, `QuadraticBezierCurve`, and `CubicBezierCurve` shapes (decomposing complex shapes like `Arc`, `Ellipse`, and `Circle` using their `#to_path_shapes` method)
757
766
  - `#closed?`: returns `true` if closed and `false` otherwise
758
767
  - `#winding_rule`: returns winding rule (`:wind_non_zero` or `:wind_even_odd`)
759
768
  - `#points`: path points calculated (derived) from shapes
@@ -769,6 +778,7 @@ Includes `PerfectShape::MultiPoint`
769
778
  - `#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)
770
779
  - `#==(other)`: Returns `true` if equal to `other` or `false` otherwise
771
780
  - `#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
781
+ - `#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
772
782
  - `#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)
773
783
  - `#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
774
784
 
@@ -783,7 +793,7 @@ path_shapes << PerfectShape::Line.new(points: [250, 170]) # no need for start po
783
793
  path_shapes << PerfectShape::QuadraticBezierCurve.new(points: [[300, 185], [350, 150]]) # no need for start point, just control point and end point
784
794
  path_shapes << PerfectShape::CubicBezierCurve.new(points: [[370, 50], [430, 220], [480, 170]]) # no need for start point, just two control points and end point
785
795
 
786
- shape = PerfectShape::Path.new(shapes: path_shapes, closed: false, winding_rule: :wind_even_odd)
796
+ shape = PerfectShape::Path.new(shapes: path_shapes, closed: false, winding_rule: :wind_non_zero)
787
797
 
788
798
  shape.contain?(275, 165) # => true
789
799
  shape.contain?([275, 165]) # => true
@@ -821,6 +831,7 @@ A composite shape is simply an aggregate of multiple shapes (e.g. square and pol
821
831
  - `#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)
822
832
  - `#==(other)`: Returns `true` if equal to `other` or `false` otherwise
823
833
  - `#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
834
+ - `#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
824
835
 
825
836
  Example:
826
837
 
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.5.5
1
+ 1.0.2
@@ -312,5 +312,165 @@ module PerfectShape
312
312
  y = self.y + (Math.sin(angle) * 0.5 + 0.5) * self.height
313
313
  [x, y]
314
314
  end
315
+
316
+ # Converts Arc into basic Path shapes made up of Points, Lines, and CubicBezierCurves
317
+ # Used by Path when adding an Arc to Path shapes
318
+ def to_path_shapes
319
+ w = BigDecimal(self.width.to_s) / 2
320
+ h = BigDecimal(self.height.to_s) / 2
321
+ x = self.x + w
322
+ y = self.x + h
323
+ ang_st_rad = -Math.degrees_to_radians(self.start)
324
+ ext = -self.extent
325
+ if ext >= 360.0 || ext <= -360
326
+ arc_segs = 4
327
+ increment = Math::PI / 2
328
+ cv = 0.5522847498307933
329
+ if ext < 0
330
+ increment = -increment
331
+ cv = -cv
332
+ end
333
+ else
334
+ arc_segs = (ext.abs / 90.0).ceil
335
+ increment = Math.degrees_to_radians(ext / arc_segs)
336
+ cv = btan(increment)
337
+ arc_segs = 0 if cv == 0
338
+ end
339
+ line_segs = nil
340
+ case self.type
341
+ when :open
342
+ line_segs = 0
343
+ when :chord
344
+ line_segs = 1
345
+ when :pie
346
+ line_segs = 2
347
+ end
348
+ arc_segs = line_segs = -1 if w < 0 || h < 0
349
+
350
+ first_point_x = first_point_y = nil
351
+ last_point_x = last_point_y = nil
352
+ (arc_segs + line_segs + 1).to_i.times.map do |index|
353
+ coords = []
354
+ angle = ang_st_rad
355
+ if index == 0
356
+ first_point_x = coords[0] = x + Math.cos(angle) * w
357
+ first_point_y = coords[1] = y + Math.sin(angle) * h
358
+ Point.new(*coords)
359
+ elsif (index > arc_segs) && (extent - start) != 0 && ((extent - start)%360 == 0)
360
+ nil
361
+ elsif (index > arc_segs) && (index < arc_segs + line_segs) && (extent - start) == 0
362
+ nil
363
+ elsif (index > arc_segs) && (index == arc_segs + line_segs)
364
+ Line.new(points: [[first_point_x, first_point_y]])
365
+ elsif index > arc_segs
366
+ coords[0] = x
367
+ coords[1] = y
368
+ if line_segs == 2
369
+ last_point_x = coords[0]
370
+ last_point_y = coords[1]
371
+ end
372
+ Line.new(points: coords)
373
+ else
374
+ angle += increment * (index - 1)
375
+ relx = Math.cos(angle)
376
+ rely = Math.sin(angle)
377
+ coords[0] = x + (relx - cv * rely) * w
378
+ coords[1] = y + (rely + cv * relx) * h
379
+ angle += increment
380
+ relx = Math.cos(angle)
381
+ rely = Math.sin(angle)
382
+ coords[2] = x + (relx + cv * rely) * w
383
+ coords[3] = y + (rely - cv * relx) * h
384
+ coords[4] = x + relx * w
385
+ coords[5] = y + rely * h
386
+ if line_segs == 1
387
+ last_point_x = coords[4]
388
+ last_point_y = coords[5]
389
+ end
390
+ CubicBezierCurve.new(points: coords)
391
+ end
392
+ end.compact
393
+ end
394
+
395
+ # btan computes the length (k) of the control segments at
396
+ # the beginning and end of a cubic bezier that approximates
397
+ # a segment of an arc with extent less than or equal to
398
+ # 90 degrees. This length (k) will be used to generate the
399
+ # 2 bezier control points for such a segment.
400
+ #
401
+ # Assumptions:
402
+ # a) arc is centered on 0,0 with radius of 1.0
403
+ # b) arc extent is less than 90 degrees
404
+ # c) control points should preserve tangent
405
+ # d) control segments should have equal length
406
+ #
407
+ # Initial data:
408
+ # start angle: ang1
409
+ # end angle: ang2 = ang1 + extent
410
+ # start point: P1 = (x1, y1) = (cos(ang1), sin(ang1))
411
+ # end point: P4 = (x4, y4) = (cos(ang2), sin(ang2))
412
+ #
413
+ # Control points:
414
+ # P2 = (x2, y2)
415
+ # | x2 = x1 - k * sin(ang1) = cos(ang1) - k * sin(ang1)
416
+ # | y2 = y1 + k * cos(ang1) = sin(ang1) + k * cos(ang1)
417
+ #
418
+ # P3 = (x3, y3)
419
+ # | x3 = x4 + k * sin(ang2) = cos(ang2) + k * sin(ang2)
420
+ # | y3 = y4 - k * cos(ang2) = sin(ang2) - k * cos(ang2)
421
+ #
422
+ # The formula for this length (k) can be found using the
423
+ # following derivations:
424
+ #
425
+ # Midpoints:
426
+ # a) bezier (t = 1/2)
427
+ # bPm = P1 * (1-t)^3 +
428
+ # 3 * P2 * t * (1-t)^2 +
429
+ # 3 * P3 * t^2 * (1-t) +
430
+ # P4 * t^3 =
431
+ # = (P1 + 3P2 + 3P3 + P4)/8
432
+ #
433
+ # b) arc
434
+ # aPm = (cos((ang1 + ang2)/2), sin((ang1 + ang2)/2))
435
+ #
436
+ # Let angb = (ang2 - ang1)/2; angb is half of the angle
437
+ # between ang1 and ang2.
438
+ #
439
+ # Solve the equation bPm == aPm
440
+ #
441
+ # a) For xm coord:
442
+ # x1 + 3*x2 + 3*x3 + x4 = 8*cos((ang1 + ang2)/2)
443
+ #
444
+ # cos(ang1) + 3*cos(ang1) - 3*k*sin(ang1) +
445
+ # 3*cos(ang2) + 3*k*sin(ang2) + cos(ang2) =
446
+ # = 8*cos((ang1 + ang2)/2)
447
+ #
448
+ # 4*cos(ang1) + 4*cos(ang2) + 3*k*(sin(ang2) - sin(ang1)) =
449
+ # = 8*cos((ang1 + ang2)/2)
450
+ #
451
+ # 8*cos((ang1 + ang2)/2)*cos((ang2 - ang1)/2) +
452
+ # 6*k*sin((ang2 - ang1)/2)*cos((ang1 + ang2)/2) =
453
+ # = 8*cos((ang1 + ang2)/2)
454
+ #
455
+ # 4*cos(angb) + 3*k*sin(angb) = 4
456
+ #
457
+ # k = 4 / 3 * (1 - cos(angb)) / sin(angb)
458
+ #
459
+ # b) For ym coord we derive the same formula.
460
+ #
461
+ # Since this formula can generate "NaN" values for small
462
+ # angles, we will derive a safer form that does not involve
463
+ # dividing by very small values:
464
+ # (1 - cos(angb)) / sin(angb) =
465
+ # = (1 - cos(angb))*(1 + cos(angb)) / sin(angb)*(1 + cos(angb)) =
466
+ # = (1 - cos(angb)^2) / sin(angb)*(1 + cos(angb)) =
467
+ # = sin(angb)^2 / sin(angb)*(1 + cos(angb)) =
468
+ # = sin(angb) / (1 + cos(angb))
469
+ #
470
+ def btan(increment)
471
+ return 0 if increment.nan?
472
+ increment /= BigDecimal('2.0')
473
+ BigDecimal('4.0') / BigDecimal('3.0') * Math.sin(increment) / (BigDecimal('1.0') + Math.cos(increment))
474
+ end
315
475
  end
316
476
  end
@@ -52,40 +52,40 @@ module PerfectShape
52
52
  def diameter=(value)
53
53
  @diameter = BigDecimal(value.to_s)
54
54
  @radius = nil
55
- self.width = value unless width == value
56
- self.height = value unless height == value
55
+ self.width = @diameter unless width == @diameter
56
+ self.height = @diameter unless height == @diameter
57
57
  end
58
58
 
59
59
  # Sets radius, normalizing to BigDecimal
60
60
  def radius=(value)
61
61
  @radius = BigDecimal(value.to_s)
62
62
  @diameter = nil
63
- self.radius_x = value unless width == value
64
- self.radius_y = value unless height == value
63
+ self.radius_x = @radius unless @width == @radius
64
+ self.radius_y = @radius unless @height == @radius
65
65
  end
66
66
 
67
67
  def width=(value)
68
68
  super
69
- self.diameter = value unless diameter == value
70
- self.height = value unless height == value
69
+ self.diameter = @width unless diameter == @width
70
+ self.height = @width unless height == @width
71
71
  end
72
72
 
73
73
  def height=(value)
74
74
  super
75
- self.diameter = value unless diameter == value
76
- self.width = value unless width == value
75
+ self.diameter = @height unless diameter == @height
76
+ self.width = @height unless width == @height
77
77
  end
78
78
 
79
79
  def radius_x=(value)
80
80
  super
81
- self.radius = value unless radius == value
82
- self.radius_y = value unless radius_y == value
81
+ self.radius = @radius_x unless radius == @radius_x
82
+ self.radius_y = @radius_x unless radius_y == @radius_x
83
83
  end
84
84
 
85
85
  def radius_y=(value)
86
86
  super
87
- self.radius = value unless radius == value
88
- self.radius_x = value unless radius_x == value
87
+ self.radius = @radius_y unless radius == @radius_y
88
+ self.radius_x = @radius_y unless radius_x == @radius_y
89
89
  end
90
90
  end
91
91
  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
@@ -223,7 +223,7 @@ module PerfectShape
223
223
 
224
224
  num_crossings = rectangle_crossings(rectangle)
225
225
  # the intended return value is
226
- # num_crossings != 0 || num_crossings == Rectangle::RECT_INTERSECTS
226
+ # num_crossings != 0 || num_crossings == PerfectShape::Rectangle::RECT_INTERSECTS
227
227
  # but if (num_crossings != 0) num_crossings == INTERSECTS won't matter
228
228
  # and if !(num_crossings != 0) then num_crossings == 0, so
229
229
  # num_crossings != RECT_INTERSECT
@@ -244,7 +244,7 @@ module PerfectShape
244
244
  if !(x1 == x2 && y1 == y2)
245
245
  line = PerfectShape::Line.new(points: [[x1, y1], [x2, y2]])
246
246
  crossings = line.rect_crossings(x, y, x+w, y+h, crossings)
247
- return crossings if crossings == Rectangle::RECT_INTERSECTS
247
+ return crossings if crossings == PerfectShape::Rectangle::RECT_INTERSECTS
248
248
  end
249
249
  # we call this with the curve's direction reversed, because we wanted
250
250
  # to call rectCrossingsForLine first, because it's cheaper.
@@ -294,7 +294,7 @@ module PerfectShape
294
294
  # The intersection of ranges is more complicated
295
295
  # First do trivial INTERSECTS rejection of the cases
296
296
  # where one of the endpoints is inside the rectangle.
297
- return Rectangle::RECT_INTERSECTS if ((x0 > rxmin && x0 < rxmax && y0 > rymin && y0 < rymax) ||
297
+ return PerfectShape::Rectangle::RECT_INTERSECTS if ((x0 > rxmin && x0 < rxmax && y0 > rymin && y0 < rymax) ||
298
298
  (x1 > rxmin && x1 < rxmax && y1 > rymin && y1 < rymax))
299
299
 
300
300
  # Otherwise, subdivide and look for one of the cases above.
@@ -318,7 +318,7 @@ module PerfectShape
318
318
  return 0 if xmid.nan? || ymid.nan?
319
319
  cubic1 = CubicBezierCurve.new(points: [[x0, y0], [xc0, yc0], [xc0m, yc0m], [xmid, ymid]])
320
320
  crossings = cubic1.rect_crossings(rxmin, rymin, rxmax, rymax, level + 1, crossings)
321
- if crossings != Rectangle::RECT_INTERSECTS
321
+ if crossings != PerfectShape::Rectangle::RECT_INTERSECTS
322
322
  cubic2 = CubicBezierCurve.new(points: [[xmid, ymid], [xmc1, ymc1], [xc1, yc1], [x1, y1]])
323
323
  crossings = cubic2.rect_crossings(rxmin, rymin, rxmax, rymax, level + 1, crossings)
324
324
  end
@@ -244,7 +244,7 @@ module PerfectShape
244
244
 
245
245
  # Accumulate the number of times the line crosses the shadow
246
246
  # extending to the right of the rectangle. See the comment
247
- # for the Rectangle::RECT_INTERSECTS constant for more complete details.
247
+ # for the PerfectShape::Rectangle::RECT_INTERSECTS constant for more complete details.
248
248
  #
249
249
  # crossings arg is the initial crossings value to add to (useful
250
250
  # in cases where you want to accumulate crossings from multiple
@@ -280,7 +280,7 @@ module PerfectShape
280
280
  # Both x and y ranges overlap by a non-empty amount
281
281
  # First do trivial INTERSECTS rejection of the cases
282
282
  # where one of the endpoints is inside the rectangle.
283
- return Rectangle::RECT_INTERSECTS if ((x0 > rxmin && x0 < rxmax && y0 > rymin && y0 < rymax) ||
283
+ return PerfectShape::Rectangle::RECT_INTERSECTS if ((x0 > rxmin && x0 < rxmax && y0 > rymin && y0 < rymax) ||
284
284
  (x1 > rxmin && x1 < rxmax && y1 > rymin && y1 < rymax))
285
285
  # Otherwise calculate the y intercepts and see where
286
286
  # they fall with respect to the rectangle
@@ -311,7 +311,7 @@ module PerfectShape
311
311
  end
312
312
  return crossings
313
313
  end
314
- Rectangle::RECT_INTERSECTS
314
+ PerfectShape::Rectangle::RECT_INTERSECTS
315
315
  end
316
316
 
317
317
  def intersect?(rectangle)
@@ -32,28 +32,34 @@ module PerfectShape
32
32
  include Equalizer.new(:shapes, :closed, :winding_rule)
33
33
 
34
34
  # Available class types for path shapes
35
- SHAPE_TYPES = [Array, PerfectShape::Point, PerfectShape::Line, PerfectShape::QuadraticBezierCurve, PerfectShape::CubicBezierCurve]
35
+ SHAPE_TYPES = [Array, PerfectShape::Point, PerfectShape::Line, PerfectShape::QuadraticBezierCurve, PerfectShape::CubicBezierCurve, PerfectShape::Arc, PerfectShape::Ellipse, PerfectShape::Circle]
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
- attr_accessor :shapes, :closed
41
+ attr_accessor :shapes, :closed, :line_to_complex_shapes
42
42
  alias closed? closed
43
+ alias line_to_complex_shapes? line_to_complex_shapes
43
44
 
44
- # Constructs Path with winding rule, closed status, and shapes (must always start with PerfectShape::Point or Array of [x,y] coordinates)
45
- # Shape class types can be any of SHAPE_TYPES: Array (x,y coordinates), PerfectShape::Point, PerfectShape::Line, PerfectShape::QuadraticBezierCurve, or PerfectShape::CubicBezierCurve
45
+ # Constructs Path with winding rule, closed status, line_to_complex_shapes option, and shapes (must always start with PerfectShape::Point or Array of [x,y] coordinates)
46
+ # Shape class types can be any of SHAPE_TYPES: Array (x,y coordinates), PerfectShape::Point, PerfectShape::Line, PerfectShape::QuadraticBezierCurve, PerfectShape::CubicBezierCurve
47
+ # PerfectShape::Arc, PerfectShape::Ellipse, or PerfectShape::Circle
48
+ # Complex shapes, meaning Arc, Ellipse, and Circle, are decomposed into basic path shapes, meaning Point, Line, QuadraticBezierCurve, and CubicBezierCurve.
46
49
  # winding_rule can be any of WINDING_RULES: :wind_non_zero (default) or :wind_even_odd
47
- # closed can be true or false
48
- def initialize(shapes: [], closed: false, winding_rule: :wind_non_zero)
50
+ # closed can be true or false (default)
51
+ # line_to_complex_shapes can be true or false (default), indicating whether to connect to complex shapes,
52
+ # meaning Arc, Ellipse, and Circle, with a line, or otherwise move to their start point instead.
53
+ def initialize(shapes: [], closed: false, winding_rule: :wind_even_odd, line_to_complex_shapes: false)
49
54
  self.closed = closed
50
55
  self.winding_rule = winding_rule
51
56
  self.shapes = shapes
57
+ self.line_to_complex_shapes = line_to_complex_shapes
52
58
  end
53
59
 
54
60
  def points
55
61
  the_points = []
56
- @shapes.each do |shape|
62
+ basic_shapes.each do |shape|
57
63
  case shape
58
64
  when Point
59
65
  the_points << shape.to_a
@@ -71,7 +77,7 @@ module PerfectShape
71
77
  end
72
78
  end
73
79
  end
74
- the_points << @shapes.first.to_a if closed?
80
+ the_points << basic_shapes.first.to_a if closed?
75
81
  the_points
76
82
  end
77
83
 
@@ -80,7 +86,7 @@ module PerfectShape
80
86
  end
81
87
 
82
88
  def drawing_types
83
- the_drawing_shapes = @shapes.map do |shape|
89
+ the_drawing_shapes = basic_shapes.map do |shape|
84
90
  case shape
85
91
  when Point
86
92
  :move_to
@@ -99,7 +105,7 @@ module PerfectShape
99
105
  end
100
106
 
101
107
  def winding_rule=(value)
102
- raise "Invalid winding rule: #{value}" unless WINDING_RULES.include?(value.to_s.to_sym)
108
+ raise "Invalid winding rule: #{value} (must be one of #{WINDING_RULES})" unless WINDING_RULES.include?(value.to_s.to_sym)
103
109
  @winding_rule = value
104
110
  end
105
111
 
@@ -175,8 +181,8 @@ module PerfectShape
175
181
  ci += 1
176
182
  line = PerfectShape::Line.new(points: [[curx, cury], [endx, endy]])
177
183
  crossings += line.point_crossings(x, y)
178
- curx = endx;
179
- cury = endy;
184
+ curx = endx
185
+ cury = endy
180
186
  when :quad_to
181
187
  quad_ctrlx = coords[ci]
182
188
  ci += 1
@@ -188,8 +194,8 @@ module PerfectShape
188
194
  ci += 1
189
195
  quad = PerfectShape::QuadraticBezierCurve.new(points: [[curx, cury], [quad_ctrlx, quad_ctrly], [endx, endy]])
190
196
  crossings += quad.point_crossings(x, y)
191
- curx = endx;
192
- cury = endy;
197
+ curx = endx
198
+ cury = endy
193
199
  when :cubic_to
194
200
  cubic_ctrl1x = coords[ci]
195
201
  ci += 1
@@ -205,8 +211,8 @@ module PerfectShape
205
211
  ci += 1
206
212
  cubic = PerfectShape::CubicBezierCurve.new(points: [[curx, cury], [cubic_ctrl1x, cubic_ctrl1y], [cubic_ctrl2x, cubic_ctrl2y], [endx, endy]])
207
213
  crossings += cubic.point_crossings(x, y)
208
- curx = endx;
209
- cury = endy;
214
+ curx = endx
215
+ cury = endy
210
216
  when :close
211
217
  if cury != movy
212
218
  line = PerfectShape::Line.new(points: [[curx, cury], [movx, movy]])
@@ -233,9 +239,9 @@ module PerfectShape
233
239
  # Lastly, if the path is closed, an extra shape is
234
240
  # added to represent the line connecting the last point to the first
235
241
  def disconnected_shapes
236
- initial_point = start_point = @shapes.first.to_a.map {|n| BigDecimal(n.to_s)}
242
+ initial_point = start_point = basic_shapes.first.to_a.map {|n| BigDecimal(n.to_s)}
237
243
  final_point = nil
238
- the_disconnected_shapes = @shapes.drop(1).map do |shape|
244
+ the_disconnected_shapes = basic_shapes.drop(1).map do |shape|
239
245
  case shape
240
246
  when Point
241
247
  disconnected_shape = Point.new(*shape.to_a)
@@ -267,5 +273,130 @@ module PerfectShape
267
273
  the_disconnected_shapes << Line.new(points: [final_point, initial_point]) if closed?
268
274
  the_disconnected_shapes.compact
269
275
  end
276
+
277
+ def intersect?(rectangle)
278
+ x = rectangle.x
279
+ y = rectangle.y
280
+ w = rectangle.width
281
+ h = rectangle.height
282
+ # [xy]+[wh] is NaN if any of those values are NaN,
283
+ # or if adding the two together would produce NaN
284
+ # by virtue of adding opposing Infinte values.
285
+ # Since we need to add them below, their sum must
286
+ # not be NaN.
287
+ # We return false because NaN always produces a
288
+ # negative response to tests
289
+ return false if (x+w).nan? || (y+h).nan?
290
+ return false if w <= 0 || h <= 0
291
+ mask = winding_rule == :wind_non_zero ? -1 : 2
292
+ crossings = rect_crossings(x, y, x+w, y+h)
293
+ crossings == PerfectShape::Rectangle::RECT_INTERSECTS ||
294
+ (crossings & mask) != 0
295
+ end
296
+
297
+ def rect_crossings(rxmin, rymin, rxmax, rymax)
298
+ numTypes = drawing_types.count
299
+ return 0 if numTypes == 0
300
+ coords = points.flatten
301
+ curx = cury = movx = movy = endx = endy = nil
302
+ curx = movx = coords[0]
303
+ cury = movy = coords[1]
304
+ crossings = 0
305
+ ci = 2
306
+ i = 1
307
+
308
+ while crossings != PerfectShape::Rectangle::RECT_INTERSECTS && i < numTypes
309
+ case drawing_types[i]
310
+ when :move_to
311
+ if curx != movx || cury != movy
312
+ line = PerfectShape::Line.new(points: [curx, cury, movx, movy])
313
+ crossings = line.rect_crossings(rxmin, rymin, rxmax, rymax, crossings)
314
+ end
315
+ # Count should always be a multiple of 2 here.
316
+ # assert((crossings & 1) != 0)
317
+ movx = curx = coords[ci]
318
+ ci += 1
319
+ movy = cury = coords[ci]
320
+ ci += 1
321
+ when :line_to
322
+ endx = coords[ci]
323
+ ci += 1
324
+ endy = coords[ci]
325
+ ci += 1
326
+ line = PerfectShape::Line.new(points: [curx, cury, endx, endy])
327
+ crossings = line.rect_crossings(rxmin, rymin, rxmax, rymax, crossings)
328
+ curx = endx
329
+ cury = endy
330
+ when :quad_to
331
+ cx = coords[ci]
332
+ ci += 1
333
+ cy = coords[ci]
334
+ ci += 1
335
+ endx = coords[ci]
336
+ ci += 1
337
+ endy = coords[ci]
338
+ ci += 1
339
+ quadratic_bezier_curve = PerfectShape::QuadraticBezierCurve.new(points: [curx, cury, cx, cy, endx, endy])
340
+ crossings = quadratic_bezier_curve.rect_crossings(rxmin, rymin, rxmax, rymax, 0, crossings)
341
+ curx = endx
342
+ cury = endy
343
+ when :cubic_to
344
+ c1x = coords[ci]
345
+ ci += 1
346
+ c1y = coords[ci]
347
+ ci += 1
348
+ c2x = coords[ci]
349
+ ci += 1
350
+ c2y = coords[ci]
351
+ ci += 1
352
+ endx = coords[ci]
353
+ ci += 1
354
+ endy = coords[ci]
355
+ ci += 1
356
+ cubic_bezier_curve = PerfectShape::CubicBezierCurve.new(points: [curx, cury, c1x, c1y, c2x, c2y, endx, endy])
357
+ crossings = cubic_bezier_curve.rect_crossings(rxmin, rymin, rxmax, rymax, 0, crossings)
358
+ curx = endx
359
+ cury = endy
360
+ when :close
361
+ if curx != movx || cury != movy
362
+ line = PerfectShape::Line.new(points: [curx, cury, movx, movy])
363
+ crossings = line.rect_crossings(rxmin, rymin, rxmax, rymax, crossings)
364
+ end
365
+ curx = movx
366
+ cury = movy
367
+ # Count should always be a multiple of 2 here.
368
+ # assert((crossings & 1) != 0)
369
+ end
370
+ i += 1
371
+ end
372
+ if crossings != PerfectShape::Rectangle::RECT_INTERSECTS &&
373
+ (curx != movx || cury != movy)
374
+ line = PerfectShape::Line.new(points: [curx, cury, movx, movy])
375
+ crossings = line.rect_crossings(rxmin, rymin, rxmax, rymax, crossings)
376
+ end
377
+ # Count should always be a multiple of 2 here.
378
+ # assert((crossings & 1) != 0)
379
+ crossings
380
+ end
381
+
382
+ # Returns basic shapes (i.e. Point, Line, QuadraticBezierCurve, and CubicBezierCurve),
383
+ # decomposed from complex shapes like Arc, Ellipse, and Circle by calling their `#to_path_shapes` method
384
+ def basic_shapes
385
+ the_shapes = []
386
+ @shapes.each do |shape|
387
+ if shape.respond_to?(:to_path_shapes)
388
+ shape_basic_shapes = shape.to_path_shapes
389
+ if @line_to_complex_shapes
390
+ first_basic_shape = shape_basic_shapes.shift
391
+ new_first_basic_shape = PerfectShape::Line.new(points: [first_basic_shape.to_a])
392
+ shape_basic_shapes.unshift(new_first_basic_shape)
393
+ end
394
+ the_shapes += shape_basic_shapes
395
+ else
396
+ the_shapes << shape
397
+ end
398
+ end
399
+ the_shapes
400
+ end
270
401
  end
271
402
  end
@@ -22,14 +22,31 @@
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.
@@ -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
@@ -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
@@ -40,21 +40,21 @@ module PerfectShape
40
40
  # Sets length, normalizing to BigDecimal
41
41
  def length=(value)
42
42
  @length = BigDecimal(value.to_s)
43
- self.width = value unless width == value
44
- self.height = value unless height == value
43
+ self.width = @length unless width == @length
44
+ self.height = @length unless height == @length
45
45
  end
46
46
  alias size= length=
47
47
 
48
48
  def width=(value)
49
49
  super
50
- self.length = value unless length == value
51
- self.height = value unless height == value
50
+ self.length = @width unless length == @width
51
+ self.height = @width unless height == @width
52
52
  end
53
53
 
54
54
  def height=(value)
55
55
  super
56
- self.length = value unless length == value
57
- self.width = value unless width == value
56
+ self.length = @height unless length == @height
57
+ self.width = @height unless width == @height
58
58
  end
59
59
  end
60
60
  end
@@ -2,17 +2,17 @@
2
2
  # DO NOT EDIT THIS FILE DIRECTLY
3
3
  # Instead, edit Juwelier::Tasks in Rakefile, and run 'rake gemspec'
4
4
  # -*- encoding: utf-8 -*-
5
- # stub: perfect-shape 0.5.5 ruby lib
5
+ # stub: perfect-shape 1.0.2 ruby lib
6
6
 
7
7
  Gem::Specification.new do |s|
8
8
  s.name = "perfect-shape".freeze
9
- s.version = "0.5.5"
9
+ s.version = "1.0.2"
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-22"
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
14
+ s.date = "2022-04-03"
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 the Ray Casting Algorithm, aka Even-odd Rule, and the 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 = [
18
18
  "CHANGELOG.md",
@@ -55,14 +55,14 @@ Gem::Specification.new do |s|
55
55
  end
56
56
 
57
57
  if s.respond_to? :add_runtime_dependency then
58
- s.add_runtime_dependency(%q<equalizer>.freeze, ["= 0.0.11"])
58
+ s.add_runtime_dependency(%q<equalizer>.freeze, [">= 0.0.11", "< 1.1.0"])
59
59
  s.add_development_dependency(%q<rdoc>.freeze, ["~> 3.12"])
60
60
  s.add_development_dependency(%q<juwelier>.freeze, ["~> 2.1.0"])
61
61
  s.add_development_dependency(%q<minitest>.freeze, ["~> 5.14.4"])
62
62
  s.add_development_dependency(%q<puts_debuggerer>.freeze, ["~> 0.13.1"])
63
63
  s.add_development_dependency(%q<rake-tui>.freeze, ["> 0"])
64
64
  else
65
- s.add_dependency(%q<equalizer>.freeze, ["= 0.0.11"])
65
+ s.add_dependency(%q<equalizer>.freeze, [">= 0.0.11", "< 1.1.0"])
66
66
  s.add_dependency(%q<rdoc>.freeze, ["~> 3.12"])
67
67
  s.add_dependency(%q<juwelier>.freeze, ["~> 2.1.0"])
68
68
  s.add_dependency(%q<minitest>.freeze, ["~> 5.14.4"])
metadata CHANGED
@@ -1,29 +1,35 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: perfect-shape
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.5
4
+ version: 1.0.2
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-22 00:00:00.000000000 Z
11
+ date: 2022-04-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: equalizer
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - '='
17
+ - - ">="
18
18
  - !ruby/object:Gem::Version
19
19
  version: 0.0.11
20
+ - - "<"
21
+ - !ruby/object:Gem::Version
22
+ version: 1.1.0
20
23
  type: :runtime
21
24
  prerelease: false
22
25
  version_requirements: !ruby/object:Gem::Requirement
23
26
  requirements:
24
- - - '='
27
+ - - ">="
25
28
  - !ruby/object:Gem::Version
26
29
  version: 0.0.11
30
+ - - "<"
31
+ - !ruby/object:Gem::Version
32
+ version: 1.1.0
27
33
  - !ruby/object:Gem::Dependency
28
34
  name: rdoc
29
35
  requirement: !ruby/object:Gem::Requirement
@@ -100,9 +106,9 @@ description: Perfect Shape is a collection of pure Ruby geometric algorithms tha
100
106
  square, arc (open, chord, and pie), ellipse, circle, polygon, and paths containing
101
107
  lines, quadratic bézier curves, and cubic bezier curves, potentially with affine
102
108
  transforms applied like translation, scale, rotation, shear/skew, and inversion
103
- (including both Ray Casting Algorithm, aka Even-odd Rule, and Winding Number Algorithm,
104
- aka Nonzero Rule). Additionally, it contains some purely mathematical algorithms
105
- like IEEEremainder (also known as IEEE-754 remainder).
109
+ (including both the Ray Casting Algorithm, aka Even-odd Rule, and the Winding Number
110
+ Algorithm, aka Nonzero Rule). Additionally, it contains some purely mathematical
111
+ algorithms like IEEEremainder (also known as IEEE-754 remainder).
106
112
  email: andy.am@gmail.com
107
113
  executables: []
108
114
  extensions: []