perfect-shape 0.5.1 → 0.5.5

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: 11b0b3324d2d9a6ea74cf92529f4657899021ef9b515e9bac45ce0538927b519
4
- data.tar.gz: b89aaf29076f074800f8934e3df56111254f5f162c5efbcd26ee1afda5386734
3
+ metadata.gz: f32696a2e0ecd7c1aceb16d9211b38e977b4dd15c7ba57acb8ac99a769d75036
4
+ data.tar.gz: 50c458cfcf134ce86bc11e0b22f1650b2faf2146c79c288d7b2501a2d7d9264c
5
5
  SHA512:
6
- metadata.gz: bb359cd9182b49f0d345c66d17a3cfda427a9b366b615d8bd8b63fcf75953747da9273802295135fec146ad25bb0eb6875d83626224acdf159111d23ab61213b
7
- data.tar.gz: a2c318a07294e002af53c80421f388f662045f390ec9be24437eb9c7aeeafb9af50db36899fbb90e738b67e1a21a85cb0762b238891fb2b93ce238893c5514c6
6
+ metadata.gz: 1e15378328df2eaad3769ce087118f7990fcb834963cbc56b738ea1cc292a92df0f86d77506e1a2f94985bc03211b5436b9965e8b6b5c97c80f9d7ad59f6cb20
7
+ data.tar.gz: cb64f0b3951ca4628f96e265070a88684060be6ffa38b5a45deea611bce0b2d20b72e43ef6c5d70b969925a31fe0017f4ec84407927f23a1b7824e0352bc9732
data/CHANGELOG.md CHANGED
@@ -1,13 +1,42 @@
1
1
  # Change Log
2
2
 
3
+ ## 0.5.5
4
+
5
+ - `PerfectShape::Arc#intersect?(rectangle)`
6
+ - `PerfectShape::Ellipse#intersect?(rectangle)`
7
+ - `PerfectShape::Circle#intersect?(rectangle)`
8
+
9
+ ## 0.5.4
10
+
11
+ - `PerfectShape::Rectangle#intersect?(rectangle)`
12
+ - `PerfectShape::Square#intersect?(rectangle)`
13
+ - `PerfectShape::Rectangle#empty?`
14
+ - `PerfectShape::Square#empty?`
15
+ - `PerfectShape::Square#size`/`PerfectShape::Square#size=` aliases for `#length`/`#length=`
16
+
17
+ ## 0.5.3
18
+
19
+ - `PerfectShape::CubicBezierCurve#intersect?(rectangle)`
20
+ - `PerfectShape::CubicBezierCurve#rectangle_crossings(rectangle)`
21
+ - `PerfectShape::CubicBezierCurve#rect_crossings(rxmin, rymin, rxmax, rymax, level, crossings = 0)`
22
+ - `PerfectShape::Line#rect_crossings(rxmin, rymin, rxmax, rymax, crossings = 0)`
23
+
24
+ ## 0.5.2
25
+
26
+ - `PerfectShape::QuadraticBezierCurve#intersect?(rectangle)`
27
+ - `PerfectShape::QuadraticBezierCurve::tag(coord, low, high)`
28
+ - `PerfectShape::QuadraticBezierCurve::eqn(val, c1, cp, c2)`
29
+ - `PerfectShape::QuadraticBezierCurve::solve_quadratic(eqn)`
30
+ - `PerfectShape::QuadraticBezierCurve::eval_quadratic(vals, num, include0, include1, inflect, c1, ctrl, c2)`
31
+
3
32
  ## 0.5.1
4
33
 
5
- - `Point#intersect?(rectangle)` (equivalent to `Rectangle#contain?(point)`)
34
+ - `PerfectShape::Point#intersect?(rectangle)` (equivalent to `PerfectShape::Rectangle#contain?(point)`)
6
35
 
7
36
  ## 0.5.0
8
37
 
9
- - `Line#intersect?(rectangle)`
10
- - `Rectangle#out_state(x_or_point, y = nil)`
38
+ - `PerfectShape::Line#intersect?(rectangle)`
39
+ - `PerfectShape::Rectangle#out_state(x_or_point, y = nil)`
11
40
 
12
41
  ## 0.4.0
13
42
 
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # Perfect Shape 0.5.1
1
+ # Perfect Shape 0.5.5
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.1
17
+ gem install perfect-shape -v 0.5.5
18
18
  ```
19
19
 
20
20
  Or include in Bundler `Gemfile`:
21
21
 
22
22
  ```ruby
23
- gem 'perfect-shape', '~> 0.5.1'
23
+ gem 'perfect-shape', '~> 0.5.5'
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
 
@@ -250,6 +251,10 @@ Includes `PerfectShape::MultiPoint`
250
251
 
251
252
  ![quadratic_bezier_curve](https://raw.githubusercontent.com/AndyObtiva/perfect-shape/master/images/quadratic_bezier_curve.png)
252
253
 
254
+ - `::tag(coord, low, high)`: Determine where coord lies with respect to the range from low to high. It is assumed that low < high. The return value is one of the 5 values BELOW, LOWEDGE, INSIDE, HIGHEDGE, or ABOVE.
255
+ - `::eqn(val, c1, cp, c2)`: Fill an array with the coefficients of the parametric equation in t, ready for solving against val with solve_quadratic. We currently have: val = Py(t) = C1*(1-t)^2 + 2*CP*t*(1-t) + C2*t^2 = C1 - 2*C1*t + C1*t^2 + 2*CP*t - 2*CP*t^2 + C2*t^2 = C1 + (2*CP - 2*C1)*t + (C1 - 2*CP + C2)*t^2; 0 = (C1 - val) + (2*CP - 2*C1)*t + (C1 - 2*CP + C2)*t^2; 0 = C + Bt + At^2; C = C1 - val; B = 2*CP - 2*C1; A = C1 - 2*CP + C2
256
+ - `::solve_quadratic(eqn)`: Solves the quadratic whose coefficients are in the eqn array and places the non-complex roots into the res array, returning the number of roots. The quadratic solved is represented by the equation: <pre>eqn = {C, B, A}; ax^2 + bx + c = 0</pre> A return value of `-1` is used to distinguish a constant equation, which might be always 0 or never 0, from an equation that has no zeroes.
257
+ - `::eval_quadratic(vals, num, include0, include1, inflect, c1, ctrl, c2)`: Evaluate the t values in the first num slots of the vals[] array and place the evaluated values back into the same array. Only evaluate t values that are within the range <, >, including the 0 and 1 ends of the range iff the include0 or include1 booleans are true. If an "inflection" equation is handed in, then any points which represent a point of inflection for that quadratic equation are also ignored.
253
258
  - `::new(points: [])`: constructs a quadratic bézier curve with three `points` (start point, control point, and end point) as `Array` of `Array`s of `[x,y]` pairs or flattened `Array` of alternating x and y coordinates
254
259
  - `#points`: points (start point, control point, and end point)
255
260
  - `#min_x`: min x
@@ -264,6 +269,7 @@ Includes `PerfectShape::MultiPoint`
264
269
  - `#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)
265
270
  - `#==(other)`: Returns `true` if equal to `other` or `false` otherwise
266
271
  - `#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 quadratic bezier curve shape from its outline more successfully
272
+ - `#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
267
273
  - `#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`)
268
274
  - `#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`)
269
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`)
@@ -313,11 +319,14 @@ Includes `PerfectShape::MultiPoint`
313
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)
314
320
  - `#==(other)`: Returns `true` if equal to `other` or `false` otherwise
315
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
316
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`)
317
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`)
318
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`)
319
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`.
320
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)
321
330
 
322
331
  Example:
323
332
 
@@ -363,8 +372,10 @@ Includes `PerfectShape::RectangularShape`
363
372
  - `#bounding_box`: bounding box is a rectangle with x = min x, y = min y, and width/height of shape
364
373
  - `#==(other)`: Returns `true` if equal to `other` or `false` otherwise
365
374
  - `#contain?(x_or_point, y=nil, outline: false, distance_tolerance: 0)`: checks if point is inside when `outline` is `false` or if point is on the outline when `outline` is `true`. `distance_tolerance` can be used as a fuzz factor when `outline` is `true`, for example, to help GUI users mouse-click-select a rectangle shape from its outline more successfully
375
+ - `#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
366
376
  - `#edges`: edges of rectangle as `PerfectShape::Line` objects
367
377
  - `#out_state(x_or_point, y = nil)`: Returns "out state" of specified point (x,y) (whether it lies to the left, right, top, bottom of rectangle). If point is outside rectangle, it returns a bit mask combination of `Rectangle::OUT_LEFT`, `Rectangle::OUT_RIGHT`, `Rectangle::OUT_TOP`, or `Rectangle::OUT_BOTTOM`. Otherwise, it returns `0` if point is inside the rectangle.
378
+ - `#empty?`: Returns `true` if width or height are 0 (or negative) and `false` otherwise
368
379
 
369
380
  Example:
370
381
 
@@ -393,7 +404,7 @@ Extends `PerfectShape::Rectangle`
393
404
 
394
405
  ![square](https://raw.githubusercontent.com/AndyObtiva/perfect-shape/master/images/square.png)
395
406
 
396
- - `::new(x: 0, y: 0, length: 1)`: constructs a square
407
+ - `::new(x: 0, y: 0, length: 1)` (`length` alias: `size`): constructs a square
397
408
  - `#x`: top-left x
398
409
  - `#y`: top-left y
399
410
  - `#length`: length
@@ -409,7 +420,9 @@ Extends `PerfectShape::Rectangle`
409
420
  - `#bounding_box`: bounding box is a rectangle with x = min x, y = min y, and width/height of shape
410
421
  - `#==(other)`: Returns `true` if equal to `other` or `false` otherwise
411
422
  - `#contain?(x_or_point, y=nil, outline: false, distance_tolerance: 0)`: checks if point is inside when `outline` is `false` or if point is on the outline when `outline` is `true`. `distance_tolerance` can be used as a fuzz factor when `outline` is `true`, for example, to help GUI users mouse-click-select a square shape from its outline more successfully
423
+ - `#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
412
424
  - `#edges`: edges of square as `PerfectShape::Line` objects
425
+ - `#empty?`: Returns `true` if length is 0 (or negative) and `false` otherwise
413
426
 
414
427
  Example:
415
428
 
@@ -464,6 +477,8 @@ Open Arc | Chord Arc | Pie Arc
464
477
  - `#bounding_box`: bounding box is a rectangle with x = min x, y = min y, and width/height of shape
465
478
  - `#==(other)`: Returns `true` if equal to `other` or `false` otherwise
466
479
  - `#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
+ - `#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
+ - `#contain_angle?(angle)`: returns `true` if the angle is within the angular extents of the arc and `false` otherwise
467
482
 
468
483
  Example:
469
484
 
@@ -587,6 +602,7 @@ Extends `PerfectShape::Arc`
587
602
  - `#bounding_box`: bounding box is a rectangle with x = min x, y = min y, and width/height of shape
588
603
  - `#==(other)`: Returns `true` if equal to `other` or `false` otherwise
589
604
  - `#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
+ - `#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
590
606
 
591
607
  Example:
592
608
 
@@ -648,6 +664,7 @@ Extends `PerfectShape::Ellipse`
648
664
  - `#bounding_box`: bounding box is a rectangle with x = min x, y = min y, and width/height of shape
649
665
  - `#==(other)`: Returns `true` if equal to `other` or `false` otherwise
650
666
  - `#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
+ - `#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
651
668
 
652
669
  Example:
653
670
 
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.5.1
1
+ 0.5.5
@@ -141,8 +141,8 @@ module PerfectShape
141
141
  # @param x The X coordinate of the point to test.
142
142
  # @param y The Y coordinate of the point to test.
143
143
  #
144
- # @return {@code true} if the point lies within the bound of
145
- # the arc, {@code false} if the point lies outside of the
144
+ # @return true if the point lies within the bound of
145
+ # the arc, false if the point lies outside of the
146
146
  # arc's bounds.
147
147
  def contain?(x_or_point, y = nil, outline: false, distance_tolerance: 0)
148
148
  x, y = Point.normalize_point(x_or_point, y)
@@ -212,5 +212,105 @@ module PerfectShape
212
212
 
213
213
  (angle >= 0.0) && (angle < ang_ext)
214
214
  end
215
+
216
+ def intersect?(rectangle)
217
+ x = rectangle.x
218
+ y = rectangle.y
219
+ w = rectangle.width
220
+ h = rectangle.height
221
+ aw = self.width
222
+ ah = self.height
223
+
224
+ return false if w <= 0 || h <= 0 || aw <= 0 || ah <= 0
225
+ ext = self.extent
226
+ return false if ext == 0
227
+
228
+ ax = self.x
229
+ ay = self.y
230
+ axw = ax + aw
231
+ ayh = ay + ah
232
+ xw = x + w
233
+ yh = y + h
234
+
235
+ # check bbox
236
+ return false if x >= axw || y >= ayh || xw <= ax || yh <= ay
237
+
238
+ # extract necessary data
239
+ axc = self.center_x
240
+ ayc = self.center_y
241
+ sx, sy = self.start_point
242
+ ex, ey = self.end_point
243
+
244
+ # Try to catch rectangles that intersect arc in areas
245
+ # outside of rectagle with left top corner coordinates
246
+ # (min(center x, start point x, end point x),
247
+ # min(center y, start point y, end point y))
248
+ # and rigth bottom corner coordinates
249
+ # (max(center x, start point x, end point x),
250
+ # max(center y, start point y, end point y)).
251
+ # So we'll check axis segments outside of rectangle above.
252
+ if ayc >= y && ayc <= yh # 0 and 180
253
+ return true if (sx < xw && ex < xw && axc < xw &&
254
+ axw > x && contain_angle?(0)) ||
255
+ (sx > x && ex > x && axc > x &&
256
+ ax < xw && contain_angle?(180))
257
+ end
258
+ if axc >= x && axc <= xw # 90 and 270
259
+ return true if (sy > y && ey > y && ayc > y &&
260
+ ay < yh && contain_angle?(90)) ||
261
+ (sy < yh && ey < yh && ayc < yh &&
262
+ ayh > y && contain_angle?(270))
263
+ end
264
+
265
+ # For PIE we should check intersection with pie slices
266
+ # also we should do the same for arcs with extent is greater
267
+ # than 180, because we should cover case of rectangle, which
268
+ # situated between center of arc and chord, but does not
269
+ # intersect the chord.
270
+ rect = PerfectShape::Rectangle.new(x: x, y: y, width: w, height: h)
271
+ if type == :pie || ext.abs > 180
272
+ # for PIE: try to find intersections with pie slices
273
+ line1 = PerfectShape::Line.new(points: [[axc, ayc], [sx, sy]])
274
+ line2 = PerfectShape::Line.new(points: [[axc, ayc], [ex, ey]])
275
+ return true if line1.intersect?(rect) || line2.intersect?(rect)
276
+ else
277
+ # for CHORD and OPEN: try to find intersections with chord
278
+ line = PerfectShape::Line.new(points: [[sx, sy], [ex, ey]])
279
+ return true if line.intersect?(rect)
280
+ end
281
+
282
+ # finally check the rectangle corners inside the arc
283
+ return true if contain?(x, y) || contain?(x + w, y) ||
284
+ contain?(x, y + h) || contain?(x + w, y + h)
285
+
286
+ false
287
+ end
288
+
289
+ # Returns the starting point of the arc. This point is the
290
+ # intersection of the ray from the center defined by the
291
+ # starting angle and the elliptical boundary of the arc.
292
+ #
293
+ # @return An (x,y) pair Array object representing the
294
+ # x,y coordinates of the starting point of the arc.
295
+ def start_point
296
+ angle = Math.degrees_to_radians(-self.start)
297
+ x = self.x + (Math.cos(angle) * 0.5 + 0.5) * self.width
298
+ y = self.y + (Math.sin(angle) * 0.5 + 0.5) * self.height
299
+ [x, y]
300
+ end
301
+
302
+ # Returns the ending point of the arc. This point is the
303
+ # intersection of the ray from the center defined by the
304
+ # starting angle plus the angular extent of the arc and the
305
+ # elliptical boundary of the arc.
306
+ #
307
+ # @return An (x,y) pair Array object representing the
308
+ # x,y coordinates of the ending point of the arc.
309
+ def end_point
310
+ angle = Math.degrees_to_radians(-self.start - self.extent)
311
+ x = self.x + (Math.cos(angle) * 0.5 + 0.5) * self.width
312
+ y = self.y + (Math.sin(angle) * 0.5 + 0.5) * self.height
313
+ [x, y]
314
+ end
215
315
  end
216
316
  end
@@ -82,8 +82,8 @@ module PerfectShape
82
82
  # @param x The X coordinate of the point to test.
83
83
  # @param y The Y coordinate of the point to test.
84
84
  #
85
- # @return {@code true} if the point lies within the bound of
86
- # the cubic bézier curve, {@code false} if the point lies outside of the
85
+ # @return true if the point lies within the bound of
86
+ # the cubic bézier curve, false if the point lies outside of the
87
87
  # cubic bézier curve's bounds.
88
88
  def contain?(x_or_point, y = nil, outline: false, distance_tolerance: 0)
89
89
  x, y = Point.normalize_point(x_or_point, y)
@@ -211,5 +211,118 @@ module PerfectShape
211
211
  last_minimum_distance
212
212
  end
213
213
  end
214
+
215
+ def intersect?(rectangle)
216
+ x = rectangle.x
217
+ y = rectangle.y
218
+ w = rectangle.width
219
+ h = rectangle.height
220
+
221
+ # Trivially reject non-existant rectangles
222
+ return false if w <= 0 || h <= 0
223
+
224
+ num_crossings = rectangle_crossings(rectangle)
225
+ # the intended return value is
226
+ # num_crossings != 0 || num_crossings == 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
@@ -59,8 +59,8 @@ module PerfectShape
59
59
  # @param x The X coordinate of the point to test.
60
60
  # @param y The Y coordinate of the point to test.
61
61
  #
62
- # @return {@code true} if the point lies within the bound of
63
- # the ellipse, {@code false} if the point lies outside of the
62
+ # @return true if the point lies within the bound of
63
+ # the ellipse, false if the point lies outside of the
64
64
  # ellipse's bounds.
65
65
  def contain?(x_or_point, y = nil, outline: false, distance_tolerance: 0)
66
66
  # This is implemented again even though super would have just worked to have an optimized algorithm for Ellipse.
@@ -22,7 +22,6 @@
22
22
  require 'perfect_shape/shape'
23
23
  require 'perfect_shape/point'
24
24
  require 'perfect_shape/multi_point'
25
- require 'perfect_shape/rectangle'
26
25
 
27
26
  module PerfectShape
28
27
  class Line < Shape
@@ -55,11 +54,11 @@ module PerfectShape
55
54
  # coordinates with respect to the line segment formed
56
55
  # by the first two specified coordinates.
57
56
  def relative_counterclockwise(x1, y1, x2, y2, px, py)
58
- x2 -= x1;
59
- y2 -= y1;
60
- px -= x1;
61
- py -= y1;
62
- ccw = px * y2 - py * x2;
57
+ x2 -= x1
58
+ y2 -= y1
59
+ px -= x1
60
+ py -= y1
61
+ ccw = px * y2 - py * x2
63
62
  if ccw == 0.0
64
63
  # The point is colinear, classify based on which side of
65
64
  # the segment the point falls on. We can calculate a
@@ -67,7 +66,7 @@ module PerfectShape
67
66
  # segment - a negative value indicates the point projects
68
67
  # outside of the segment in the direction of the particular
69
68
  # endpoint used as the origin for the projection.
70
- ccw = px * x2 + py * y2;
69
+ ccw = px * x2 + py * y2
71
70
  if ccw > 0.0
72
71
  # Reverse the projection to be relative to the original x2,y2
73
72
  # x2 and y2 are simply negated.
@@ -76,13 +75,13 @@ module PerfectShape
76
75
  # Since we really want to get a positive answer when the
77
76
  # point is "beyond (x2,y2)", then we want to calculate
78
77
  # the inverse anyway - thus we leave x2 & y2 negated.
79
- px -= x2;
80
- py -= y2;
81
- ccw = px * x2 + py * y2;
78
+ px -= x2
79
+ py -= y2
80
+ ccw = px * x2 + py * y2
82
81
  ccw = 0.0 if ccw < 0.0
83
82
  end
84
83
  end
85
- (ccw < 0.0) ? -1 : ((ccw > 0.0) ? 1 : 0);
84
+ (ccw < 0.0) ? -1 : ((ccw > 0.0) ? 1 : 0)
86
85
  end
87
86
 
88
87
  # Returns the square of the distance from a point to a line segment.
@@ -121,12 +120,12 @@ module PerfectShape
121
120
  # px,py becomes relative vector from x1,y1 to test point
122
121
  px -= x1
123
122
  py -= y1
124
- dot_product = px * x2 + py * y2;
123
+ dot_product = px * x2 + py * y2
125
124
  if dot_product <= 0.0
126
125
  # px,py is on the side of x1,y1 away from x2,y2
127
126
  # distance to segment is length of px,py vector
128
127
  # "length of its (clipped) projection" is now 0.0
129
- projected_length_square = BigDecimal('0.0');
128
+ projected_length_square = BigDecimal('0.0')
130
129
  else
131
130
  # switch to backwards vectors relative to x2,y2
132
131
  # x2,y2 are already the negative of x1,y1=>x2,y2
@@ -192,10 +191,10 @@ module PerfectShape
192
191
  def point_crossings(x1, y1, x2, y2, px, py)
193
192
  return 0 if (py < y1 && py < y2)
194
193
  return 0 if (py >= y1 && py >= y2)
195
- # assert(y1 != y2);
194
+ # assert(y1 != y2)
196
195
  return 0 if (px >= x1 && px >= x2)
197
196
  return ((y1 < y2) ? 1 : -1) if (px < x1 && px < x2)
198
- xintercept = x1 + (py - y1) * (x2 - x1) / (y2 - y1);
197
+ xintercept = x1 + (py - y1) * (x2 - x1) / (y2 - y1)
199
198
  return 0 if (px >= xintercept)
200
199
  (y1 < y2) ? 1 : -1
201
200
  end
@@ -210,8 +209,8 @@ module PerfectShape
210
209
  # @param y The Y coordinate of the point to test.
211
210
  # @param distance_tolerance The distance from line to tolerate (0 by default)
212
211
  #
213
- # @return {@code true} if the point lies within the bound of
214
- # the line, {@code false} if the point lies outside of the
212
+ # @return true if the point lies within the bound of
213
+ # the line, false if the point lies outside of the
215
214
  # line's bounds.
216
215
  def contain?(x_or_point, y = nil, outline: true, distance_tolerance: 0)
217
216
  x, y = Point.normalize_point(x_or_point, y)
@@ -243,7 +242,80 @@ module PerfectShape
243
242
  Line.point_crossings(points[0][0], points[0][1], points[1][0], points[1][1], x, y)
244
243
  end
245
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
+
246
317
  def intersect?(rectangle)
318
+ require 'perfect_shape/rectangle'
247
319
  x1 = points[0][0]
248
320
  y1 = points[0][1]
249
321
  x2 = points[1][0]
@@ -80,8 +80,8 @@ module PerfectShape
80
80
  # @param y The Y coordinate of the point to test.
81
81
  # @param distance_tolerance The distance from point to tolerate (0 by default)
82
82
  #
83
- # @return {@code true} if the point is close enough within distance tolerance,
84
- # {@code false} if the point is too far.
83
+ # @return true if the point is close enough within distance tolerance,
84
+ # false if the point is too far.
85
85
  def contain?(x_or_point, y = nil, outline: true, distance_tolerance: 0)
86
86
  x, y = Point.normalize_point(x_or_point, y)
87
87
  return unless x && y
@@ -34,8 +34,8 @@ module PerfectShape
34
34
  # @param x The X coordinate of the point to test.
35
35
  # @param y The Y coordinate of the point to test.
36
36
  #
37
- # @return {@code true} if the point lies within the bound of
38
- # the polygon, {@code false} if the point lies outside of the
37
+ # @return true if the point lies within the bound of
38
+ # the polygon, false if the point lies outside of the
39
39
  # polygon's bounds.
40
40
  def contain?(x_or_point, y = nil, outline: false, distance_tolerance: 0)
41
41
  x, y = Point.normalize_point(x_or_point, y)
@@ -62,13 +62,124 @@ module PerfectShape
62
62
  # These values are also NaN if opposing infinities are added
63
63
  return 0 if (xc.nan? || yc.nan?)
64
64
  point_crossings(x1, y1, x1c, y1c, xc, yc, px, py, level+1) +
65
- point_crossings(xc, yc, xc1, yc1, x2, y2, px, py, level+1);
65
+ point_crossings(xc, yc, xc1, yc1, x2, y2, px, py, level+1)
66
+ end
67
+
68
+ # Determine where coord lies with respect to the range from
69
+ # low to high. It is assumed that low < high. The return
70
+ # value is one of the 5 values BELOW, LOWEDGE, INSIDE, HIGHEDGE,
71
+ # or ABOVE.
72
+ def tag(coord, low, high)
73
+ return (coord < low ? BELOW : LOWEDGE) if coord <= low
74
+ return (coord > high ? ABOVE : HIGHEDGE) if coord >= high
75
+ INSIDE
76
+ end
77
+
78
+ # Fill an array with the coefficients of the parametric equation
79
+ # in t, ready for solving against val with solve_quadratic.
80
+ # We currently have:
81
+ # val = Py(t) = C1*(1-t)^2 + 2*CP*t*(1-t) + C2*t^2
82
+ # = C1 - 2*C1*t + C1*t^2 + 2*CP*t - 2*CP*t^2 + C2*t^2
83
+ # = C1 + (2*CP - 2*C1)*t + (C1 - 2*CP + C2)*t^2
84
+ # 0 = (C1 - val) + (2*CP - 2*C1)*t + (C1 - 2*CP + C2)*t^2
85
+ # 0 = C + Bt + At^2
86
+ # C = C1 - val
87
+ # B = 2*CP - 2*C1
88
+ # A = C1 - 2*CP + C2
89
+ def eqn(val, c1, cp, c2)
90
+ [
91
+ c1 - val,
92
+ cp + cp - c1 - c1,
93
+ c1 - cp - cp + c2,
94
+ ]
95
+ end
96
+
97
+ # Solves the quadratic whose coefficients are in the eqn
98
+ # array and places the non-complex roots into the res
99
+ # array, returning the number of roots.
100
+ # The quadratic solved is represented by the equation:
101
+ # <pre>
102
+ # eqn = {C, B, A}
103
+ # ax^2 + bx + c = 0
104
+ # </pre>
105
+ # A return value of -1 is used to distinguish a constant
106
+ # equation, which might be always 0 or never 0, from an equation that
107
+ # has no zeroes.
108
+ # @param eqn the specified array of coefficients to use to solve
109
+ # the quadratic equation
110
+ # @param res the array that contains the non-complex roots
111
+ # resulting from the solution of the quadratic equation
112
+ # @return the number of roots, or -1 if the equation is
113
+ # a constant.
114
+ def solve_quadratic(eqn, res)
115
+ a = eqn[2]
116
+ b = eqn[1]
117
+ c = eqn[0]
118
+ roots = -1
119
+ if a == 0.0
120
+ # The quadratic parabola has degenerated to a line.
121
+ # The line has degenerated to a constant.
122
+ return -1 if b == 0.0
123
+ res[roots += 1] = -c / b
124
+ else
125
+ # From Numerical Recipes, 5.6, Quadratic and Cubic Equations
126
+ d = b * b - 4.0 * a * c
127
+ # If d < 0.0, then there are no roots
128
+ return 0 if d < 0.0
129
+ d = BigDecimal(Math.sqrt(d).to_a)
130
+ # For accuracy, calculate one root using:
131
+ # (-b +/- d) / 2a
132
+ # and the other using:
133
+ # 2c / (-b +/- d)
134
+ # Choose the sign of the +/- so that b+d gets larger in magnitude
135
+ d = -d if b < 0.0
136
+ q = (b + d) / -2.0
137
+ # We already tested a for being 0 above
138
+ res[roots += 1] = q / a
139
+ res[roots += 1] = c / q if q != 0.0
140
+ end
141
+ roots
142
+ end
143
+
144
+ # Evaluate the t values in the first num slots of the vals[] array
145
+ # and place the evaluated values back into the same array. Only
146
+ # evaluate t values that are within the range <, >, including
147
+ # the 0 and 1 ends of the range iff the include0 or include1
148
+ # booleans are true. If an "inflection" equation is handed in,
149
+ # then any points which represent a point of inflection for that
150
+ # quadratic equation are also ignored.
151
+ def eval_quadratic(vals, num,
152
+ include0,
153
+ include1,
154
+ inflect,
155
+ c1, ctrl, c2)
156
+ j = -1
157
+ i = 0
158
+ while i < num
159
+ t = vals[i]
160
+
161
+ if ((include0 ? t >= 0 : t > 0) &&
162
+ (include1 ? t <= 1 : t < 1) &&
163
+ (inflect.nil? ||
164
+ inflect[1] + 2*inflect[2]*t != 0))
165
+ u = 1 - t
166
+ vals[j+=1] = c1*u*u + 2*ctrl*t*u + c2*t*t
167
+ end
168
+ i+=1
169
+ end
170
+ j
66
171
  end
67
172
  end
68
173
 
69
174
  include MultiPoint
70
175
  include Equalizer.new(:points)
71
176
 
177
+ BELOW = -2
178
+ LOWEDGE = -1
179
+ INSIDE = 0
180
+ HIGHEDGE = 1
181
+ ABOVE = 2
182
+
72
183
  OUTLINE_MINIMUM_DISTANCE_THRESHOLD = BigDecimal('0.001')
73
184
 
74
185
  # Checks if quadratic bézier curve contains point (two-number Array or x, y args)
@@ -76,8 +187,8 @@ module PerfectShape
76
187
  # @param x The X coordinate of the point to test.
77
188
  # @param y The Y coordinate of the point to test.
78
189
  #
79
- # @return {@code true} if the point lies within the bound of
80
- # the quadratic bézier curve, {@code false} if the point lies outside of the
190
+ # @return true if the point lies within the bound of
191
+ # the quadratic bézier curve, false if the point lies outside of the
81
192
  # quadratic bézier curve's bounds.
82
193
  def contain?(x_or_point, y = nil, outline: false, distance_tolerance: 0)
83
194
  x, y = Point.normalize_point(x_or_point, y)
@@ -159,20 +270,20 @@ module PerfectShape
159
270
  # zero, then the points must be collinear and so the
160
271
  # curve is degenerate and encloses no area. Thus the
161
272
  # result is false.
162
- kx = x1 - 2 * xc + x2;
163
- ky = y1 - 2 * yc + y2;
164
- dx = x - x1;
165
- dy = y - y1;
166
- dxl = x2 - x1;
167
- dyl = y2 - y1;
273
+ kx = x1 - 2 * xc + x2
274
+ ky = y1 - 2 * yc + y2
275
+ dx = x - x1
276
+ dy = y - y1
277
+ dxl = x2 - x1
278
+ dyl = y2 - y1
168
279
 
169
280
  t0 = (dx * ky - dy * kx) / (dxl * ky - dyl * kx)
170
281
  return false if (t0 < 0 || t0 > 1 || t0 != t0)
171
282
 
172
- xb = kx * t0 * t0 + 2 * (xc - x1) * t0 + x1;
173
- yb = ky * t0 * t0 + 2 * (yc - y1) * t0 + y1;
174
- xl = dxl * t0 + x1;
175
- yl = dyl * t0 + y1;
283
+ xb = kx * t0 * t0 + 2 * (xc - x1) * t0 + x1
284
+ yb = ky * t0 * t0 + 2 * (yc - y1) * t0 + y1
285
+ xl = dxl * t0 + x1
286
+ yl = dyl * t0 + y1
176
287
 
177
288
  (x >= xb && x < xl) ||
178
289
  (x >= xl && x < xb) ||
@@ -273,5 +384,166 @@ module PerfectShape
273
384
  last_minimum_distance
274
385
  end
275
386
  end
387
+
388
+ def intersect?(rectangle)
389
+ x = rectangle.x
390
+ y = rectangle.y
391
+ w = rectangle.width
392
+ h = rectangle.height
393
+
394
+ # Trivially reject non-existant rectangles
395
+ return false if w <= 0 || h <= 0
396
+
397
+ # Trivially accept if either endpoint is inside the rectangle
398
+ # (not on its border since it may end there and not go inside)
399
+ # Record where they lie with respect to the rectangle.
400
+ # -1 => left, 0 => inside, 1 => right
401
+ x1 = points[0][0]
402
+ y1 = points[0][1]
403
+ x1tag = QuadraticBezierCurve.tag(x1, x, x+w)
404
+ y1tag = QuadraticBezierCurve.tag(y1, y, y+h)
405
+ return true if x1tag == INSIDE && y1tag == INSIDE
406
+ x2 = points[2][0]
407
+ y2 = points[2][1]
408
+ x2tag = QuadraticBezierCurve.tag(x2, x, x+w)
409
+ y2tag = QuadraticBezierCurve.tag(y2, y, y+h)
410
+ return true if x2tag == INSIDE && y2tag == INSIDE
411
+ ctrlx = points[1][0]
412
+ ctrly = points[1][1]
413
+ ctrlxtag = QuadraticBezierCurve.tag(ctrlx, x, x+w)
414
+ ctrlytag = QuadraticBezierCurve.tag(ctrly, y, y+h)
415
+
416
+ # Trivially reject if all points are entirely to one side of
417
+ # the rectangle.
418
+ # Returning false means All points left
419
+ return false if x1tag < INSIDE && x2tag < INSIDE && ctrlxtag < INSIDE
420
+ # Returning false means All points above
421
+ return false if y1tag < INSIDE && y2tag < INSIDE && ctrlytag < INSIDE
422
+ # Returning false means All points right
423
+ return false if x1tag > INSIDE && x2tag > INSIDE && ctrlxtag > INSIDE
424
+ # Returning false means All points below
425
+ return false if y1tag > INSIDE && y2tag > INSIDE && ctrlytag > INSIDE
426
+
427
+ # Test for endpoints on the edge where either the segment
428
+ # or the curve is headed "inwards" from them
429
+ # Note: These tests are a superset of the fast endpoint tests
430
+ # above and thus repeat those tests, but take more time
431
+ # and cover more cases
432
+ # First endpoint on border with either edge moving inside
433
+ return true if inwards(x1tag, x2tag, ctrlxtag) && inwards(y1tag, y2tag, ctrlytag)
434
+ # Second endpoint on border with either edge moving inside
435
+ return true if inwards(x2tag, x1tag, ctrlxtag) && inwards(y2tag, y1tag, ctrlytag)
436
+
437
+ # Trivially accept if endpoints span directly across the rectangle
438
+ xoverlap = (x1tag * x2tag <= 0)
439
+ yoverlap = (y1tag * y2tag <= 0)
440
+ return true if x1tag == INSIDE && x2tag == INSIDE && yoverlap
441
+ return true if y1tag == INSIDE && y2tag == INSIDE && xoverlap
442
+
443
+ # We now know that both endpoints are outside the rectangle
444
+ # but the 3 points are not all on one side of the rectangle.
445
+ # Therefore the curve cannot be contained inside the rectangle,
446
+ # but the rectangle might be contained inside the curve, or
447
+ # the curve might intersect the boundary of the rectangle.
448
+
449
+ eqn = nil
450
+ res = []
451
+ if !yoverlap
452
+ # Both Y coordinates for the closing segment are above or
453
+ # below the rectangle which means that we can only intersect
454
+ # if the curve crosses the top (or bottom) of the rectangle
455
+ # in more than one place and if those crossing locations
456
+ # span the horizontal range of the rectangle.
457
+ eqn = QuadraticBezierCurve.eqn((y1tag < INSIDE ? y : y+h), y1, ctrly, y2)
458
+ return (QuadraticBezierCurve.solve_quadratic(eqn, res) == 2 &&
459
+ QuadraticBezierCurve.eval_quadratic(res, 2, true, true, nil,
460
+ x1, ctrlx, x2) == 2 &&
461
+ QuadraticBezierCurve.tag(res[0], x, x+w) * QuadraticBezierCurve.tag(res[1], x, x+w) <= 0)
462
+ end
463
+
464
+ # Y ranges overlap. Now we examine the X ranges
465
+ if !xoverlap
466
+ # Both X coordinates for the closing segment are left of
467
+ # or right of the rectangle which means that we can only
468
+ # intersect if the curve crosses the left (or right) edge
469
+ # of the rectangle in more than one place and if those
470
+ # crossing locations span the vertical range of the rectangle.
471
+ eqn = QuadraticBezierCurve.eqn((x1tag < INSIDE ? x : x+w), x1, ctrlx, x2)
472
+ return (QuadraticBezierCurve.solve_quadratic(eqn, res) == 2 &&
473
+ QuadraticBezierCurve.eval_quadratic(res, 2, true, true, nil,
474
+ y1, ctrly, y2) == 2 &&
475
+ QuadraticBezierCurve.tag(res[0], y, y+h) * QuadraticBezierCurve.tag(res[1], y, y+h) <= 0)
476
+ end
477
+
478
+ # The X and Y ranges of the endpoints overlap the X and Y
479
+ # ranges of the rectangle, now find out how the endpoint
480
+ # line segment intersects the Y range of the rectangle
481
+ dx = x2 - x1
482
+ dy = y2 - y1
483
+ k = y2 * x1 - x2 * y1
484
+ c1tag = c2tag = nil
485
+ if y1tag == INSIDE
486
+ c1tag = x1tag
487
+ else
488
+ c1tag = QuadraticBezierCurve.tag((k + dx * (y1tag < INSIDE ? y : y+h)) / dy, x, x+w)
489
+ end
490
+ if y2tag == INSIDE
491
+ c2tag = x2tag
492
+ else
493
+ c2tag = QuadraticBezierCurve.tag((k + dx * (y2tag < INSIDE ? y : y+h)) / dy, x, x+w)
494
+ end
495
+ # If the part of the line segment that intersects the Y range
496
+ # of the rectangle crosses it horizontally - trivially accept
497
+ return true if c1tag * c2tag <= 0
498
+
499
+ # Now we know that both the X and Y ranges intersect and that
500
+ # the endpoint line segment does not directly cross the rectangle.
501
+ #
502
+ # We can almost treat this case like one of the cases above
503
+ # where both endpoints are to one side, except that we will
504
+ # only get one intersection of the curve with the vertical
505
+ # side of the rectangle. This is because the endpoint segment
506
+ # accounts for the other intersection.
507
+ #
508
+ # (Remember there is overlap in both the X and Y ranges which
509
+ # means that the segment must cross at least one vertical edge
510
+ # of the rectangle - in particular, the "near vertical side" -
511
+ # leaving only one intersection for the curve.)
512
+ #
513
+ # Now we calculate the y tags of the two intersections on the
514
+ # "near vertical side" of the rectangle. We will have one with
515
+ # the endpoint segment, and one with the curve. If those two
516
+ # vertical intersections overlap the Y range of the rectangle,
517
+ # we have an intersection. Otherwise, we don't.
518
+
519
+ # c1tag = vertical intersection class of the endpoint segment
520
+ #
521
+ # Choose the y tag of the endpoint that was not on the same
522
+ # side of the rectangle as the subsegment calculated above.
523
+ # Note that we can "steal" the existing Y tag of that endpoint
524
+ # since it will be provably the same as the vertical intersection.
525
+ c1tag = ((c1tag * x1tag <= 0) ? y1tag : y2tag)
526
+
527
+ # c2tag = vertical intersection class of the curve
528
+ #
529
+ # We have to calculate this one the straightforward way.
530
+ # Note that the c2tag can still tell us which vertical edge
531
+ # to test against.
532
+ eqn = QuadraticBezierCurve.eqn((c2tag < INSIDE ? x : x+w), x1, ctrlx, x2)
533
+ num = QuadraticBezierCurve.solve_quadratic(eqn, res)
534
+
535
+ # Note: We should be able to assert(num == 2) since the
536
+ # X range "crosses" (not touches) the vertical boundary,
537
+ # but we pass num to QuadraticBezierCurve.eval_quadratic for completeness.
538
+ QuadraticBezierCurve.eval_quadratic(res, num, true, true, nil, y1, ctrly, y2)
539
+
540
+ # Note: We can assert(num evals == 1) since one of the
541
+ # 2 crossings will be out of the [0,1] range.
542
+ c2tag = QuadraticBezierCurve.tag(res[0], y, y+h)
543
+
544
+ # Finally, we have an intersection if the two crossings
545
+ # overlap the Y range of the rectangle.
546
+ c1tag * c2tag <= 0
547
+ end
276
548
  end
277
549
  end
@@ -41,13 +41,15 @@ module PerfectShape
41
41
  # bitmask indicating a point lies below
42
42
  OUT_BOTTOM = 8
43
43
 
44
+ RECT_INTERSECTS = 0x80000000
45
+
44
46
  # Checks if rectangle contains point (two-number Array or x, y args)
45
47
  #
46
48
  # @param x The X coordinate of the point to test.
47
49
  # @param y The Y coordinate of the point to test.
48
50
  #
49
- # @return {@code true} if the point lies within the bound of
50
- # the rectangle, {@code false} if the point lies outside of the
51
+ # @return true if the point lies within the bound of
52
+ # the rectangle, false if the point lies outside of the
51
53
  # rectangle's bounds.
52
54
  def contain?(x_or_point, y = nil, outline: false, distance_tolerance: 0)
53
55
  x, y = Point.normalize_point(x_or_point, y)
@@ -95,5 +97,24 @@ module PerfectShape
95
97
  end
96
98
  out
97
99
  end
100
+
101
+ # A rectangle is empty if its width or height is 0 (or less)
102
+ def empty?
103
+ width <= 0.0 || height <= 0.0
104
+ end
105
+
106
+ def intersect?(rectangle)
107
+ x = rectangle.x
108
+ y = rectangle.y
109
+ w = rectangle.width
110
+ h = rectangle.height
111
+ return false if empty? || w <= 0 || h <= 0
112
+ x0 = self.x
113
+ y0 = self.y
114
+ (x + w) > x0 &&
115
+ (y + h) > y0 &&
116
+ x < (x0 + self.width) &&
117
+ y < (y0 + self.height)
118
+ end
98
119
  end
99
120
  end
@@ -23,12 +23,15 @@ require 'perfect_shape/rectangle'
23
23
 
24
24
  module PerfectShape
25
25
  class Square < Rectangle
26
- MESSAGE_WIDTH_AND_HEIGHT_AND_LENGTH_NOT_EQUAL = 'Square width, height, and length must all be equal if more than one is specified; or otherwise keep only one of them in arguments!'
26
+ MESSAGE_WIDTH_AND_HEIGHT_AND_LENGTH_NOT_EQUAL = 'Square width, height, and length must all be equal if more than one is specified; or otherwise keep only one of them in constructor arguments!'
27
27
 
28
28
  attr_reader :length
29
+ alias size length
29
30
 
30
31
  # Constructs with x, y, length (optionally width or height can be passed as alias for length)
31
- def initialize(x: 0, y: 0, length: nil, width: nil, height: nil)
32
+ def initialize(x: 0, y: 0, length: nil, size: nil, width: nil, height: nil)
33
+ raise MESSAGE_WIDTH_AND_HEIGHT_AND_LENGTH_NOT_EQUAL if (length && size && length != size)
34
+ length ||= size
32
35
  raise MESSAGE_WIDTH_AND_HEIGHT_AND_LENGTH_NOT_EQUAL if (length && width && length != width) || (length && height && length != height) || (width && height && width != height)
33
36
  length ||= width || height || 1
34
37
  super(x: x, y: y, width: length, height: length)
@@ -40,6 +43,7 @@ module PerfectShape
40
43
  self.width = value unless width == value
41
44
  self.height = value unless height == value
42
45
  end
46
+ alias size= length=
43
47
 
44
48
  def width=(value)
45
49
  super
@@ -2,16 +2,16 @@
2
2
  # DO NOT EDIT THIS FILE DIRECTLY
3
3
  # Instead, edit Juwelier::Tasks in Rakefile, and run 'rake gemspec'
4
4
  # -*- encoding: utf-8 -*-
5
- # stub: perfect-shape 0.5.1 ruby lib
5
+ # stub: perfect-shape 0.5.5 ruby lib
6
6
 
7
7
  Gem::Specification.new do |s|
8
8
  s.name = "perfect-shape".freeze
9
- s.version = "0.5.1"
9
+ s.version = "0.5.5"
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-19"
14
+ s.date = "2022-01-22"
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.1
4
+ version: 0.5.5
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-19 00:00:00.000000000 Z
11
+ date: 2022-01-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: equalizer