perfect-shape 0.5.2 → 0.5.3

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: d107db62b52ceb89ecb570f5bb1b77a8adde17cd6d2000e55d0ede5c88c37170
4
+ data.tar.gz: a785b27dd723cb869b64b37eb3bceef4e159f78e37b0a95816c05c340b7def8a
5
5
  SHA512:
6
- metadata.gz: c57fb7619dd930974af23d8004ad134bc3fefb51c6dd03f5ae2e7a46cccae3011d7dd74ca9a08e8055c41ad93c9a4ef92132f76d7349fd5cc20ffefc5c7d371c
7
- data.tar.gz: ca7c2586198a35ae8e9dc1485a87298c11b4151e11aab16e54302775c4539ac94c7867a3b48598914dfb8367b87fa6ae166cdea5dbe9da32ee0161788692b6d2
6
+ metadata.gz: aef22594d36b2dec69b4685b3040530b838f7eb37749e74ec9ad58a9586b55a302298c68a9ef7f11639975d5284899975059aa3e33b76506ef5e749be7ea11a7
7
+ data.tar.gz: 9a59946ad112068bd8e74ff49f843c58b2e73b07216d5ac6d06e5a3911ce006587cc895ba961b5e1eacb26bd997c43aebbba1a3640efee753fab093e4131087d
data/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # Change Log
2
2
 
3
+ ## 0.5.3
4
+
5
+ - `CubicBezierCurve#intersect?(rectangle)`
6
+
3
7
  ## 0.5.2
4
8
 
5
9
  - `QuadraticBezierCurve#intersect?(rectangle)`
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # Perfect Shape 0.5.2
1
+ # Perfect Shape 0.5.3
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 0.5.3
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', '~> 0.5.3'
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
 
@@ -318,11 +319,14 @@ Includes `PerfectShape::MultiPoint`
318
319
  - `#bounding_box`: bounding box is a rectangle with x = min x, y = min y, and width/height of shape (bounding box only guarantees that the shape is within it, but it might be bigger than the shape)
319
320
  - `#==(other)`: Returns `true` if equal to `other` or `false` otherwise
320
321
  - `#contain?(x_or_point, y=nil, outline: false, distance_tolerance: 0)`: checks if point is inside when `outline` is `false` or if point is on the outline when `outline` is `true`. `distance_tolerance` can be used as a fuzz factor when `outline` is `true`, for example, to help GUI users mouse-click-select a cubic bezier curve shape from its outline more successfully
322
+ - `#intersect?(rectangle)`: Returns `true` if intersecting with interior of rectangle or `false` otherwise. This is useful for GUI optimization checks of whether a shape appears in a GUI viewport rectangle and needs redrawing
321
323
  - `#curve_center_point`: point at the center of the curve outline (not the center of the bounding box area like `center_x` and `center_y`)
322
324
  - `#curve_center_x`: point x coordinate at the center of the curve outline (not the center of the bounding box area like `center_x` and `center_y`)
323
325
  - `#curve_center_y`: point y coordinate at the center of the curve outline (not the center of the bounding box area like `center_x` and `center_y`)
324
326
  - `#subdivisions(level=1)`: subdivides cubic bezier curve at its center into into 2 cubic bezier curves by default, or more if `level` of recursion is specified. The resulting number of subdivisions is `2` to the power of `level`.
325
327
  - `#point_distance(x_or_point, y=nil, minimum_distance_threshold: OUTLINE_MINIMUM_DISTANCE_THRESHOLD)`: calculates distance from point to curve segment. It does so by subdividing curve into smaller curves and checking against the curve center points until the distance is less than `minimum_distance_threshold`, to avoid being an overly costly operation.
328
+ - `#rectangle_crossings(rectangle)`: rectangle crossings (used to determine rectangle interior intersection), optimized to check if line represented by cubic bezier curve crosses the rectangle first, and if not then perform expensive check with `#rect_crossings`
329
+ - `#rect_crossings(rxmin, rymin, rxmax, rymax, level, crossings = 0)`: rectangle crossings (adds to crossings arg)
326
330
 
327
331
  Example:
328
332
 
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.5.2
1
+ 0.5.3
@@ -211,5 +211,118 @@ module PerfectShape
211
211
  last_minimum_distance
212
212
  end
213
213
  end
214
+
215
+ def intersect?(rectangle)
216
+ x = rectangle.x
217
+ y = rectangle.y
218
+ w = rectangle.width
219
+ h = rectangle.height
220
+
221
+ # Trivially reject non-existant rectangles
222
+ return false if w <= 0 || h <= 0
223
+
224
+ num_crossings = rectangle_crossings(rectangle)
225
+ # the intended return value is
226
+ # num_crossings != 0 || num_crossings == Rectangle::RECT_INTERSECTS
227
+ # but if (num_crossings != 0) num_crossings == INTERSECTS won't matter
228
+ # and if !(num_crossings != 0) then num_crossings == 0, so
229
+ # num_crossings != RECT_INTERSECT
230
+ num_crossings != 0
231
+ end
232
+
233
+ def rectangle_crossings(rectangle)
234
+ x = rectangle.x
235
+ y = rectangle.y
236
+ w = rectangle.width
237
+ h = rectangle.height
238
+ x1 = points[0][0]
239
+ y1 = points[0][1]
240
+ x2 = points[3][0]
241
+ y2 = points[3][1]
242
+
243
+ crossings = 0
244
+ if !(x1 == x2 && y1 == y2)
245
+ line = PerfectShape::Line.new(points: [[x1, y1], [x2, y2]])
246
+ crossings = line.rect_crossings(x, y, x+w, y+h, crossings)
247
+ return crossings if crossings == Rectangle::RECT_INTERSECTS
248
+ end
249
+ # we call this with the curve's direction reversed, because we wanted
250
+ # to call rectCrossingsForLine first, because it's cheaper.
251
+ rect_crossings(x, y, x+w, y+h, 0, crossings)
252
+ end
253
+
254
+ # Accumulate the number of times the cubic crosses the shadow
255
+ # extending to the right of the rectangle. See the comment
256
+ # for the RECT_INTERSECTS constant for more complete details.
257
+ #
258
+ # crossings arg is the initial crossings value to add to (useful
259
+ # in cases where you want to accumulate crossings from multiple
260
+ # shapes)
261
+ def rect_crossings(rxmin, rymin, rxmax, rymax, level, crossings = 0)
262
+ x0 = points[0][0]
263
+ y0 = points[0][1]
264
+ xc0 = points[1][0]
265
+ yc0 = points[1][1]
266
+ xc1 = points[2][0]
267
+ yc1 = points[2][1]
268
+ x1 = points[3][0]
269
+ y1 = points[3][1]
270
+
271
+ return crossings if y0 >= rymax && yc0 >= rymax && yc1 >= rymax && y1 >= rymax
272
+ return crossings if y0 <= rymin && yc0 <= rymin && yc1 <= rymin && y1 <= rymin
273
+ return crossings if x0 <= rxmin && xc0 <= rxmin && xc1 <= rxmin && x1 <= rxmin
274
+ if x0 >= rxmax && xc0 >= rxmax && xc1 >= rxmax && x1 >= rxmax
275
+ # Cubic is entirely to the right of the rect
276
+ # and the vertical range of the 4 Y coordinates of the cubic
277
+ # overlaps the vertical range of the rect by a non-empty amount
278
+ # We now judge the crossings solely based on the line segment
279
+ # connecting the endpoints of the cubic.
280
+ # Note that we may have 0, 1, or 2 crossings as the control
281
+ # points may be causing the Y range intersection while the
282
+ # two endpoints are entirely above or below.
283
+ if y0 < y1
284
+ # y-increasing line segment...
285
+ crossings += 1 if (y0 <= rymin && y1 > rymin)
286
+ crossings += 1 if (y0 < rymax && y1 >= rymax)
287
+ elsif y1 < y0
288
+ # y-decreasing line segment...
289
+ crossings -= 1 if (y1 <= rymin && y0 > rymin)
290
+ crossings -= 1 if (y1 < rymax && y0 >= rymax)
291
+ end
292
+ return crossings
293
+ end
294
+ # The intersection of ranges is more complicated
295
+ # First do trivial INTERSECTS rejection of the cases
296
+ # where one of the endpoints is inside the rectangle.
297
+ return Rectangle::RECT_INTERSECTS if ((x0 > rxmin && x0 < rxmax && y0 > rymin && y0 < rymax) ||
298
+ (x1 > rxmin && x1 < rxmax && y1 > rymin && y1 < rymax))
299
+
300
+ # Otherwise, subdivide and look for one of the cases above.
301
+ # double precision only has 52 bits of mantissa
302
+ return PerfectShape::Line.new(points: [[x0, y0], [x1, y1]]).rect_crossings(rxmin, rymin, rxmax, rymax, crossings) if (level > 52)
303
+ xmid = BigDecimal((xc0 + xc1).to_s) / 2
304
+ ymid = BigDecimal((yc0 + yc1).to_s) / 2
305
+ xc0 = BigDecimal((x0 + xc0).to_s) / 2
306
+ yc0 = BigDecimal((y0 + yc0).to_s) / 2
307
+ xc1 = BigDecimal((xc1 + x1).to_s) / 2
308
+ yc1 = BigDecimal((yc1 + y1).to_s) / 2
309
+ xc0m = BigDecimal((xc0 + xmid).to_s) / 2
310
+ yc0m = BigDecimal((yc0 + ymid).to_s) / 2
311
+ xmc1 = BigDecimal((xmid + xc1).to_s) / 2
312
+ ymc1 = BigDecimal((ymid + yc1).to_s) / 2
313
+ xmid = BigDecimal((xc0m + xmc1).to_s) / 2
314
+ ymid = BigDecimal((yc0m + ymc1).to_s) / 2
315
+ # [xy]mid are NaN if any of [xy]c0m or [xy]mc1 are NaN
316
+ # [xy]c0m or [xy]mc1 are NaN if any of [xy][c][01] are NaN
317
+ # These values are also NaN if opposing infinities are added
318
+ return 0 if xmid.nan? || ymid.nan?
319
+ cubic1 = CubicBezierCurve.new(points: [[x0, y0], [xc0, yc0], [xc0m, yc0m], [xmid, ymid]])
320
+ crossings = cubic1.rect_crossings(rxmin, rymin, rxmax, rymax, level + 1, crossings)
321
+ if crossings != Rectangle::RECT_INTERSECTS
322
+ cubic2 = CubicBezierCurve.new(points: [[xmid, ymid], [xmc1, ymc1], [xc1, yc1], [x1, y1]])
323
+ crossings = cubic2.rect_crossings(rxmin, rymin, rxmax, rymax, level + 1, crossings)
324
+ end
325
+ crossings
326
+ end
214
327
  end
215
328
  end
@@ -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
@@ -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 Rectangle::RECT_INTERSECTS constant for more complete details.
248
+ #
249
+ # crossings arg is the initial crossings value to add to (useful
250
+ # in cases where you want to accumulate crossings from multiple
251
+ # shapes)
252
+ def rect_crossings(rxmin, rymin, rxmax, rymax, crossings = 0)
253
+ x0 = points[0][0]
254
+ y0 = points[0][1]
255
+ x1 = points[1][0]
256
+ y1 = points[1][1]
257
+ return crossings if y0 >= rymax && y1 >= rymax
258
+ return crossings if y0 <= rymin && y1 <= rymin
259
+ return crossings if x0 <= rxmin && x1 <= rxmin
260
+ if x0 >= rxmax && x1 >= rxmax
261
+ # Line is entirely to the right of the rect
262
+ # and the vertical ranges of the two overlap by a non-empty amount
263
+ # Thus, this line segment is partially in the "right-shadow"
264
+ # Path may have done a complete crossing
265
+ # Or path may have entered or exited the right-shadow
266
+ if y0 < y1
267
+ # y-increasing line segment...
268
+ # We know that y0 < rymax and y1 > rymin
269
+ crossings += 1 if (y0 <= rymin)
270
+ crossings += 1 if (y1 >= rymax)
271
+ elsif y1 < y0
272
+ # y-decreasing line segment...
273
+ # We know that y1 < rymax and y0 > rymin
274
+ crossings -= 1 if (y1 <= rymin)
275
+ crossings -= 1 if (y0 >= rymax)
276
+ end
277
+ return crossings
278
+ end
279
+ # Remaining case:
280
+ # Both x and y ranges overlap by a non-empty amount
281
+ # First do trivial INTERSECTS rejection of the cases
282
+ # where one of the endpoints is inside the rectangle.
283
+ return Rectangle::RECT_INTERSECTS if ((x0 > rxmin && x0 < rxmax && y0 > rymin && y0 < rymax) ||
284
+ (x1 > rxmin && x1 < rxmax && y1 > rymin && y1 < rymax))
285
+ # Otherwise calculate the y intercepts and see where
286
+ # they fall with respect to the rectangle
287
+ xi0 = x0
288
+ if y0 < rymin
289
+ xi0 += ((rymin - y0) * (x1 - x0) / (y1 - y0))
290
+ elsif y0 > rymax
291
+ xi0 += ((rymax - y0) * (x1 - x0) / (y1 - y0))
292
+ end
293
+ xi1 = x1
294
+ if y1 < rymin
295
+ xi1 += ((rymin - y1) * (x0 - x1) / (y0 - y1))
296
+ elsif y1 > rymax
297
+ xi1 += ((rymax - y1) * (x0 - x1) / (y0 - y1))
298
+ end
299
+ return crossings if xi0 <= rxmin && xi1 <= rxmin
300
+ if xi0 >= rxmax && xi1 >= rxmax
301
+ if y0 < y1
302
+ # y-increasing line segment...
303
+ # We know that y0 < rymax and y1 > rymin
304
+ crossings += 1 if (y0 <= rymin)
305
+ crossings += 1 if (y1 >= rymax)
306
+ elsif y1 < y0
307
+ # y-decreasing line segment...
308
+ # We know that y1 < rymax and y0 > rymin
309
+ crossings -= 1 if (y1 <= rymin)
310
+ crossings -= 1 if (y0 >= rymax)
311
+ end
312
+ return crossings
313
+ end
314
+ Rectangle::RECT_INTERSECTS
315
+ end
316
+
245
317
  def intersect?(rectangle)
246
318
  require 'perfect_shape/rectangle'
247
319
  x1 = points[0][0]
@@ -41,6 +41,8 @@ module PerfectShape
41
41
  # bitmask indicating a point lies below
42
42
  OUT_BOTTOM = 8
43
43
 
44
+ RECT_INTERSECTS = 0x80000000
45
+
44
46
  # Checks if rectangle contains point (two-number Array or x, y args)
45
47
  #
46
48
  # @param x The X coordinate of the point to test.
@@ -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 0.5.3 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 = "0.5.3"
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-21"
15
15
  s.description = "Perfect Shape is a collection of pure Ruby geometric algorithms that are mostly useful for GUI manipulation like checking viewport rectangle intersection or containment of a mouse click point in popular geometry shapes such as rectangle, square, arc (open, chord, and pie), ellipse, circle, polygon, and paths containing lines, quadratic b\u00E9zier curves, and cubic bezier curves, potentially with affine transforms applied like translation, scale, rotation, shear/skew, and inversion (including both Ray Casting Algorithm, aka Even-odd Rule, and Winding Number Algorithm, aka Nonzero Rule). Additionally, it contains some purely mathematical algorithms like IEEEremainder (also known as IEEE-754 remainder).".freeze
16
16
  s.email = "andy.am@gmail.com".freeze
17
17
  s.extra_rdoc_files = [
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: perfect-shape
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.2
4
+ version: 0.5.3
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-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: equalizer