perfect-shape 0.3.5 → 0.5.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -24,6 +24,19 @@ require 'perfect_shape/shape'
24
24
  module PerfectShape
25
25
  # Represents multi-point shapes like Line, Polygon, and Polyline
26
26
  module MultiPoint
27
+ class << self
28
+ def normalize_point_array(the_points)
29
+ if the_points.all? {|the_point| the_point.is_a?(Array)}
30
+ the_points
31
+ else
32
+ the_points = the_points.flatten
33
+ xs = the_points.each_with_index.select {|n, i| i.even?}.map(&:first)
34
+ ys = the_points.each_with_index.select {|n, i| i.odd?}.map(&:first)
35
+ xs.zip(ys)
36
+ end
37
+ end
38
+ end
39
+
27
40
  attr_reader :points
28
41
 
29
42
  def initialize(points: [])
@@ -32,11 +45,7 @@ module PerfectShape
32
45
 
33
46
  # Sets points, normalizing to an Array of Arrays of (x,y) pairs as BigDecimal
34
47
  def points=(the_points)
35
- unless the_points.first.is_a?(Array)
36
- xs = the_points.each_with_index.select {|n, i| i.even?}.map(&:first)
37
- ys = the_points.each_with_index.select {|n, i| i.odd?}.map(&:first)
38
- the_points = xs.zip(ys)
39
- end
48
+ the_points = MultiPoint.normalize_point_array(the_points)
40
49
  @points = the_points.map do |pair|
41
50
  [
42
51
  pair.first.is_a?(BigDecimal) ? pair.first : BigDecimal(pair.first.to_s),
@@ -27,7 +27,6 @@ require 'perfect_shape/cubic_bezier_curve'
27
27
  require 'perfect_shape/multi_point'
28
28
 
29
29
  module PerfectShape
30
- # Mostly ported from java.awt.geom: https://docs.oracle.com/javase/8/docs/api/java/awt/geom/Path2D.html
31
30
  class Path < Shape
32
31
  include MultiPoint
33
32
  include Equalizer.new(:shapes, :closed, :winding_rule)
@@ -115,7 +114,7 @@ module PerfectShape
115
114
  # the path or false if the point lies outside of the
116
115
  # path's bounds.
117
116
  def contain?(x_or_point, y = nil, outline: false, distance_tolerance: 0)
118
- x, y = normalize_point(x_or_point, y)
117
+ x, y = Point.normalize_point(x_or_point, y)
119
118
  return unless x && y
120
119
 
121
120
  if outline
@@ -149,7 +148,7 @@ module PerfectShape
149
148
  # The caller must check for NaN values.
150
149
  # The caller may also reject infinite values as well.
151
150
  def point_crossings(x_or_point, y = nil)
152
- x, y = normalize_point(x_or_point, y)
151
+ x, y = Point.normalize_point(x_or_point, y)
153
152
  return unless x && y
154
153
  return 0 if shapes.count == 0
155
154
  movx = movy = curx = cury = endx = endy = 0
@@ -33,6 +33,21 @@ module PerfectShape
33
33
  py = py.is_a?(BigDecimal) ? py : BigDecimal(py.to_s)
34
34
  BigDecimal(Math.sqrt((px - x)**2 + (py - y)**2).to_s)
35
35
  end
36
+
37
+ # Normalizes point args whether two-number Array or x, y args returning
38
+ # normalized point array of two BigDecimal's
39
+ #
40
+ # @param x_or_point The point or X coordinate of the point to test.
41
+ # @param y The Y coordinate of the point to test.
42
+ #
43
+ # @return Array of x and y BigDecimal's representing point
44
+ def normalize_point(x_or_point, y = nil)
45
+ x = x_or_point
46
+ x, y = x if y.nil? && x_or_point.is_a?(Array) && x_or_point.size == 2
47
+ x = x.is_a?(BigDecimal) ? x : BigDecimal(x.to_s)
48
+ y = y.is_a?(BigDecimal) ? y : BigDecimal(y.to_s)
49
+ [x, y]
50
+ end
36
51
  end
37
52
 
38
53
  include PointLocation
@@ -68,19 +83,23 @@ module PerfectShape
68
83
  # @return {@code true} if the point is close enough within distance tolerance,
69
84
  # {@code false} if the point is too far.
70
85
  def contain?(x_or_point, y = nil, outline: true, distance_tolerance: 0)
71
- x, y = normalize_point(x_or_point, y)
86
+ x, y = Point.normalize_point(x_or_point, y)
72
87
  return unless x && y
73
88
  distance_tolerance = BigDecimal(distance_tolerance.to_s)
74
89
  point_distance(x, y) <= distance_tolerance
75
90
  end
76
91
 
77
92
  def point_distance(x_or_point, y = nil)
78
- x, y = normalize_point(x_or_point, y)
93
+ x, y = Point.normalize_point(x_or_point, y)
79
94
  return unless x && y
80
95
 
81
96
  Point.point_distance(self.x, self.y, x, y)
82
97
  end
83
98
 
99
+ def intersect?(rectangle)
100
+ rectangle.contain?(self.to_a)
101
+ end
102
+
84
103
  # Convert to pair Array of x,y coordinates
85
104
  def to_a
86
105
  [self.x, self.y]
@@ -20,10 +20,10 @@
20
20
  # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21
21
 
22
22
  require 'perfect_shape/shape'
23
+ require 'perfect_shape/point'
23
24
  require 'perfect_shape/multi_point'
24
25
 
25
26
  module PerfectShape
26
- # Mostly ported from java.awt.geom: https://docs.oracle.com/javase/8/docs/api/java/awt/Polygon.html
27
27
  class Polygon < Shape
28
28
  include MultiPoint
29
29
  include Equalizer.new(:points)
@@ -38,7 +38,7 @@ module PerfectShape
38
38
  # the polygon, {@code 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
- x, y = normalize_point(x_or_point, y)
41
+ x, y = Point.normalize_point(x_or_point, y)
42
42
  return unless x && y
43
43
  if outline
44
44
  edges.any? { |edge| edge.contain?(x, y, distance_tolerance: distance_tolerance) }
@@ -20,10 +20,10 @@
20
20
  # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21
21
 
22
22
  require 'perfect_shape/shape'
23
+ require 'perfect_shape/point'
23
24
  require 'perfect_shape/multi_point'
24
25
 
25
26
  module PerfectShape
26
- # Mostly ported from java.awt.geom: https://docs.oracle.com/javase/8/docs/api/java/awt/geom/QuadCurve2D.html
27
27
  class QuadraticBezierCurve < Shape
28
28
  class << self
29
29
  # Calculates the number of times the quadratic bézier curve from (x1,y1) to (x2,y2)
@@ -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 {@code eqn}
98
+ # array and places the non-complex roots into the {@code 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 {@code -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 {@code -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)
@@ -80,7 +191,7 @@ module PerfectShape
80
191
  # the quadratic bézier curve, {@code 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
- x, y = normalize_point(x_or_point, y)
194
+ x, y = Point.normalize_point(x_or_point, y)
84
195
  return unless x && y
85
196
 
86
197
  x1 = points[0][0]
@@ -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) ||
@@ -190,7 +301,7 @@ module PerfectShape
190
301
  # +1 is added for each crossing where the Y coordinate is increasing
191
302
  # -1 is added for each crossing where the Y coordinate is decreasing
192
303
  def point_crossings(x_or_point, y = nil, level = 0)
193
- x, y = normalize_point(x_or_point, y)
304
+ x, y = Point.normalize_point(x_or_point, y)
194
305
  return unless x && y
195
306
  QuadraticBezierCurve.point_crossings(points[0][0], points[0][1], points[1][0], points[1][1], points[2][0], points[2][1], x, y, level)
196
307
  end
@@ -247,7 +358,7 @@ module PerfectShape
247
358
  end
248
359
 
249
360
  def point_distance(x_or_point, y = nil, minimum_distance_threshold: OUTLINE_MINIMUM_DISTANCE_THRESHOLD)
250
- x, y = normalize_point(x_or_point, y)
361
+ x, y = Point.normalize_point(x_or_point, y)
251
362
  return unless x && y
252
363
 
253
364
  point = Point.new(x, y)
@@ -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
@@ -21,13 +21,25 @@
21
21
 
22
22
  require 'perfect_shape/shape'
23
23
  require 'perfect_shape/rectangular_shape'
24
+ require 'perfect_shape/point'
24
25
  require 'perfect_shape/line'
25
26
 
26
27
  module PerfectShape
27
- # Mostly ported from java.awt.geom: https://docs.oracle.com/javase/8/docs/api/java/awt/geom/Rectangle2D.html
28
28
  class Rectangle < Shape
29
29
  include RectangularShape
30
30
  include Equalizer.new(:x, :y, :width, :height)
31
+
32
+ # bitmask indicating a point lies to the left
33
+ OUT_LEFT = 1
34
+
35
+ # bitmask indicating a point lies above
36
+ OUT_TOP = 2
37
+
38
+ # bitmask indicating a point lies to the right
39
+ OUT_RIGHT = 4
40
+
41
+ # bitmask indicating a point lies below
42
+ OUT_BOTTOM = 8
31
43
 
32
44
  # Checks if rectangle contains point (two-number Array or x, y args)
33
45
  #
@@ -38,7 +50,7 @@ module PerfectShape
38
50
  # the rectangle, {@code false} if the point lies outside of the
39
51
  # rectangle's bounds.
40
52
  def contain?(x_or_point, y = nil, outline: false, distance_tolerance: 0)
41
- x, y = normalize_point(x_or_point, y)
53
+ x, y = Point.normalize_point(x_or_point, y)
42
54
  return unless x && y
43
55
 
44
56
  if outline
@@ -56,5 +68,32 @@ module PerfectShape
56
68
  Line.new(points: [[self.x, self.y + height], [self.x, self.y]])
57
69
  ]
58
70
  end
71
+
72
+ # Returns out state for specified point (x,y): (left, right, top, bottom)
73
+ #
74
+ # It can be 0 meaning not outside the rectangle,
75
+ # or if outside the rectangle, then a bit mask
76
+ # combination of OUT_LEFT, OUT_RIGHT, OUT_TOP, or OUT_BOTTOM
77
+ def out_state(x_or_point, y = nil)
78
+ x, y = Point.normalize_point(x_or_point, y)
79
+ return unless x && y
80
+
81
+ out = 0
82
+ if self.width <= 0
83
+ out |= OUT_LEFT | OUT_RIGHT
84
+ elsif x < self.x
85
+ out |= OUT_LEFT
86
+ elsif x > self.x + self.width
87
+ out |= OUT_RIGHT
88
+ end
89
+ if self.height <= 0
90
+ out |= OUT_TOP | OUT_BOTTOM
91
+ elsif y < self.y
92
+ out |= OUT_TOP
93
+ elsif y > self.y + self.height
94
+ out |= OUT_BOTTOM
95
+ end
96
+ out
97
+ end
59
98
  end
60
99
  end
@@ -75,21 +75,6 @@ module PerfectShape
75
75
  Rectangle.new(x: min_x, y: min_y, width: width, height: height)
76
76
  end
77
77
 
78
- # Normalizes point args whether two-number Array or x, y args returning
79
- # normalized point array of two BigDecimal's
80
- #
81
- # @param x_or_point The point or X coordinate of the point to test.
82
- # @param y The Y coordinate of the point to test.
83
- #
84
- # @return Array of x and y BigDecimal's representing point
85
- def normalize_point(x_or_point, y = nil)
86
- x = x_or_point
87
- x, y = x if y.nil? && x_or_point.is_a?(Array) && x_or_point.size == 2
88
- x = x.is_a?(BigDecimal) ? x : BigDecimal(x.to_s)
89
- y = y.is_a?(BigDecimal) ? y : BigDecimal(y.to_s)
90
- [x, y]
91
- end
92
-
93
78
  # Subclasses must implement
94
79
  def contain?(x_or_point, y = nil, outline: false, distance_tolerance: 0)
95
80
  end
@@ -2,17 +2,17 @@
2
2
  # DO NOT EDIT THIS FILE DIRECTLY
3
3
  # Instead, edit Juwelier::Tasks in Rakefile, and run 'rake gemspec'
4
4
  # -*- encoding: utf-8 -*-
5
- # stub: perfect-shape 0.3.5 ruby lib
5
+ # stub: perfect-shape 0.5.2 ruby lib
6
6
 
7
7
  Gem::Specification.new do |s|
8
8
  s.name = "perfect-shape".freeze
9
- s.version = "0.3.5"
9
+ s.version = "0.5.2"
10
10
 
11
11
  s.required_rubygems_version = Gem::Requirement.new(">= 0".freeze) if s.respond_to? :required_rubygems_version=
12
12
  s.require_paths = ["lib".freeze]
13
13
  s.authors = ["Andy Maleh".freeze]
14
- s.date = "2022-01-11"
15
- s.description = "Perfect Shape is a collection of pure Ruby geometric algorithms that are mostly useful for GUI manipulation like checking 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 (including both Ray Casting Algorithm, aka Even-odd Rule, and Winding Number Algorithm, aka Nonzero Rule). Additionally, it contains some purely mathematical algorithms like IEEEremainder (also known as IEEE-754 remainder).".freeze
14
+ s.date = "2022-01-20"
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 = [
18
18
  "CHANGELOG.md",
@@ -25,6 +25,7 @@ Gem::Specification.new do |s|
25
25
  "README.md",
26
26
  "VERSION",
27
27
  "lib/perfect-shape.rb",
28
+ "lib/perfect_shape/affine_transform.rb",
28
29
  "lib/perfect_shape/arc.rb",
29
30
  "lib/perfect_shape/circle.rb",
30
31
  "lib/perfect_shape/composite_shape.rb",
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.3.5
4
+ version: 0.5.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andy Maleh
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-01-11 00:00:00.000000000 Z
11
+ date: 2022-01-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: equalizer
@@ -95,12 +95,14 @@ dependencies:
95
95
  - !ruby/object:Gem::Version
96
96
  version: '0'
97
97
  description: Perfect Shape is a collection of pure Ruby geometric algorithms that
98
- are mostly useful for GUI manipulation like checking containment of a mouse click
99
- point in popular geometry shapes such as rectangle, square, arc (open, chord, and
100
- pie), ellipse, circle, polygon, and paths containing lines, quadratic bézier curves,
101
- and cubic bezier curves (including both Ray Casting Algorithm, aka Even-odd Rule,
102
- and Winding Number Algorithm, aka Nonzero Rule). Additionally, it contains some
103
- purely mathematical algorithms like IEEEremainder (also known as IEEE-754 remainder).
98
+ are mostly useful for GUI manipulation like checking viewport rectangle intersection
99
+ or containment of a mouse click point in popular geometry shapes such as rectangle,
100
+ square, arc (open, chord, and pie), ellipse, circle, polygon, and paths containing
101
+ lines, quadratic bézier curves, and cubic bezier curves, potentially with affine
102
+ transforms applied like translation, scale, rotation, shear/skew, and inversion
103
+ (including both Ray Casting Algorithm, aka Even-odd Rule, and Winding Number Algorithm,
104
+ aka Nonzero Rule). Additionally, it contains some purely mathematical algorithms
105
+ like IEEEremainder (also known as IEEE-754 remainder).
104
106
  email: andy.am@gmail.com
105
107
  executables: []
106
108
  extensions: []
@@ -114,6 +116,7 @@ files:
114
116
  - README.md
115
117
  - VERSION
116
118
  - lib/perfect-shape.rb
119
+ - lib/perfect_shape/affine_transform.rb
117
120
  - lib/perfect_shape/arc.rb
118
121
  - lib/perfect_shape/circle.rb
119
122
  - lib/perfect_shape/composite_shape.rb