perfect-shape 0.5.5 → 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: f32696a2e0ecd7c1aceb16d9211b38e977b4dd15c7ba57acb8ac99a769d75036
4
- data.tar.gz: 50c458cfcf134ce86bc11e0b22f1650b2faf2146c79c288d7b2501a2d7d9264c
3
+ metadata.gz: c200dfc203347661cd329e24cebc75ae038b0b69f6874832cfe8793ae2cbf6e5
4
+ data.tar.gz: 55cac9b991125bfcf15493eb5e05fecaea618d5d415e9c2b2c9d593efba98be4
5
5
  SHA512:
6
- metadata.gz: 1e15378328df2eaad3769ce087118f7990fcb834963cbc56b738ea1cc292a92df0f86d77506e1a2f94985bc03211b5436b9965e8b6b5c97c80f9d7ad59f6cb20
7
- data.tar.gz: cb64f0b3951ca4628f96e265070a88684060be6ffa38b5a45deea611bce0b2d20b72e43ef6c5d70b969925a31fe0017f4ec84407927f23a1b7824e0352bc9732
6
+ metadata.gz: 6cd01a66d882fa6af01e3e52e5c37bdc05c8217ae550e1b2e212bce588c7812059af7ff476ede2bd7c873f9e55738aace0386d46d6f06839ee9e50f52bd29d1c
7
+ data.tar.gz: f69e3ea514b1cf4f071e04fc943ddb814fef3e7d4594e2ffde34d3d570868feb9f642d4705e0639100b23b81202b67cc5c88eab8ef769df91c7acddf60da46e9
data/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
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
+
3
11
  ## 0.5.5
4
12
 
5
13
  - `PerfectShape::Arc#intersect?(rectangle)`
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # Perfect Shape 0.5.5
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.5
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.5'
23
+ gem 'perfect-shape', '~> 1.0.0'
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
 
@@ -704,11 +705,11 @@ Extends `PerfectShape::Shape`
704
705
 
705
706
  Includes `PerfectShape::MultiPoint`
706
707
 
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.
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.
708
709
 
709
710
  ![polygon](https://raw.githubusercontent.com/AndyObtiva/perfect-shape/master/images/polygon.png)
710
711
 
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
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`)
712
713
  - `#min_x`: min x
713
714
  - `#min_y`: min y
714
715
  - `#max_x`: max x
@@ -720,7 +721,8 @@ A polygon can be thought of as a special case of [path](#perfectshapepath) that
720
721
  - `#center_y`: center y
721
722
  - `#bounding_box`: bounding box is a rectangle with x = min x, y = min y, and width/height of shape
722
723
  - `#==(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
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
724
726
  - `#edges`: edges of polygon as `PerfectShape::Line` objects
725
727
 
726
728
  Example:
@@ -769,6 +771,7 @@ Includes `PerfectShape::MultiPoint`
769
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)
770
772
  - `#==(other)`: Returns `true` if equal to `other` or `false` otherwise
771
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
772
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)
773
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
774
777
 
@@ -821,6 +824,7 @@ A composite shape is simply an aggregate of multiple shapes (e.g. square and pol
821
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)
822
825
  - `#==(other)`: Returns `true` if equal to `other` or `false` otherwise
823
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
824
828
 
825
829
  Example:
826
830
 
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.5.5
1
+ 1.0.0
@@ -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)
@@ -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
@@ -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
@@ -2,16 +2,16 @@
2
2
  # DO NOT EDIT THIS FILE DIRECTLY
3
3
  # Instead, edit Juwelier::Tasks in Rakefile, and run 'rake gemspec'
4
4
  # -*- encoding: utf-8 -*-
5
- # stub: perfect-shape 0.5.5 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.5"
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-22"
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.5
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-22 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