perfect-shape 0.5.1 → 0.5.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 11b0b3324d2d9a6ea74cf92529f4657899021ef9b515e9bac45ce0538927b519
4
- data.tar.gz: b89aaf29076f074800f8934e3df56111254f5f162c5efbcd26ee1afda5386734
3
+ metadata.gz: cf595115a6a9344cedfce9e93801cb5aba1960f4e970d064043e5f813716a0be
4
+ data.tar.gz: bcb60359b840662739767882546e360ceb052e734f5d3d7836406e25ff7b72ae
5
5
  SHA512:
6
- metadata.gz: bb359cd9182b49f0d345c66d17a3cfda427a9b366b615d8bd8b63fcf75953747da9273802295135fec146ad25bb0eb6875d83626224acdf159111d23ab61213b
7
- data.tar.gz: a2c318a07294e002af53c80421f388f662045f390ec9be24437eb9c7aeeafb9af50db36899fbb90e738b67e1a21a85cb0762b238891fb2b93ce238893c5514c6
6
+ metadata.gz: c57fb7619dd930974af23d8004ad134bc3fefb51c6dd03f5ae2e7a46cccae3011d7dd74ca9a08e8055c41ad93c9a4ef92132f76d7349fd5cc20ffefc5c7d371c
7
+ data.tar.gz: ca7c2586198a35ae8e9dc1485a87298c11b4151e11aab16e54302775c4539ac94c7867a3b48598914dfb8367b87fa6ae166cdea5dbe9da32ee0161788692b6d2
data/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # Change Log
2
2
 
3
+ ## 0.5.2
4
+
5
+ - `QuadraticBezierCurve#intersect?(rectangle)`
6
+
3
7
  ## 0.5.1
4
8
 
5
9
  - `Point#intersect?(rectangle)` (equivalent to `Rectangle#contain?(point)`)
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # Perfect Shape 0.5.1
1
+ # Perfect Shape 0.5.2
2
2
  ## Geometric Algorithms
3
3
  [![Gem Version](https://badge.fury.io/rb/perfect-shape.svg)](http://badge.fury.io/rb/perfect-shape)
4
4
  [![Test](https://github.com/AndyObtiva/perfect-shape/actions/workflows/ruby.yml/badge.svg)](https://github.com/AndyObtiva/perfect-shape/actions/workflows/ruby.yml)
@@ -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.2
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.2'
24
24
  ```
25
25
 
26
26
  And, run:
@@ -250,6 +250,10 @@ Includes `PerfectShape::MultiPoint`
250
250
 
251
251
  ![quadratic_bezier_curve](https://raw.githubusercontent.com/AndyObtiva/perfect-shape/master/images/quadratic_bezier_curve.png)
252
252
 
253
+ - `::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.
254
+ - `::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
255
+ - `::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 {@code -1} is used to distinguish a constant equation, which might be always 0 or never 0, from an equation that has no zeroes.
256
+ - `::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
257
  - `::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
258
  - `#points`: points (start point, control point, and end point)
255
259
  - `#min_x`: min x
@@ -264,6 +268,7 @@ Includes `PerfectShape::MultiPoint`
264
268
  - `#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
269
  - `#==(other)`: Returns `true` if equal to `other` or `false` otherwise
266
270
  - `#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
271
+ - `#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
272
  - `#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
273
  - `#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
274
  - `#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`)
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.5.1
1
+ 0.5.2
@@ -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
@@ -244,6 +243,7 @@ module PerfectShape
244
243
  end
245
244
 
246
245
  def intersect?(rectangle)
246
+ require 'perfect_shape/rectangle'
247
247
  x1 = points[0][0]
248
248
  y1 = points[0][1]
249
249
  x2 = points[1][0]
@@ -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)
@@ -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
@@ -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.2 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.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-19"
14
+ s.date = "2022-01-20"
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.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-19 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