perfect-shape 1.0.1 → 1.0.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +14 -0
- data/README.md +18 -9
- data/VERSION +1 -1
- data/lib/perfect_shape/arc.rb +151 -0
- data/lib/perfect_shape/cubic_bezier_curve.rb +0 -2
- data/lib/perfect_shape/path.rb +42 -11
- data/lib/perfect_shape/rectangle.rb +38 -0
- data/perfect-shape.gemspec +4 -4
- metadata +5 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d98187ae023d5501ed920790105a46340127a38a7a1b83143b9c69d4695dc60e
|
4
|
+
data.tar.gz: 4654765dbb6cf6f091677e95551f632cb65c2e739874d5cbe105c6dcd8a70645
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e8b1217796e01a1325934ab73f90ef6edeeb392eeb74c83b21ed468cde9658b2cb0ff1cff42d7c79a62f1c978829584815910b8d5cc31df2a35c06704bd92ff7
|
7
|
+
data.tar.gz: c0a7be7c454bd95cb2a520ec9cd6db231ec51ab1fb7d1b106c1c19526052d37988c8269d96b594c75fa14b523c1222e90d73f237e60627d23c005336368edffa
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,19 @@
|
|
1
1
|
# Change Log
|
2
2
|
|
3
|
+
## 1.0.4
|
4
|
+
|
5
|
+
- Add missing shapes (`Rectangle` and `Square`) to `Path::SHAPE_TYPES`
|
6
|
+
|
7
|
+
## 1.0.3
|
8
|
+
|
9
|
+
- `Rectangle` and `Square` `#to_path_shapes` method, which decomposes them into `Point`s and `Line`s to be added to a `Path`
|
10
|
+
- `Path` can contain a `Rectangle` or `Square` (to get affected by the winding algorithm as opposed to `CompositeShape`, which has no winding algorithm)
|
11
|
+
|
12
|
+
## 1.0.2
|
13
|
+
|
14
|
+
- `Arc`, `Ellipse`, and `Circle` `#to_path_shapes` method, which decomposes them into `Point`s, `Line`s, and `CubicBezierCurve`s to be added to a `Path`
|
15
|
+
- `Path` can contain an `Arc`, `Ellipse`, or `Circle` (to get affected by the winding algorithm as opposed to `CompositeShape`, which has no winding algorithm)
|
16
|
+
|
3
17
|
## 1.0.1
|
4
18
|
|
5
19
|
- Relax `equalizer` gem version to '>= 0.0.11', '< 1.1.0' to avoid conflicts in projects that might use future versions
|
data/README.md
CHANGED
@@ -1,26 +1,26 @@
|
|
1
|
-
# Perfect Shape 1.0.
|
1
|
+
# Perfect Shape 1.0.4
|
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)
|
5
5
|
|
6
|
-
[`PerfectShape`](https://rubygems.org/gems/perfect-shape) is a collection of pure Ruby geometric algorithms that are mostly useful for GUI (Graphical User Interface) manipulation like checking viewport rectangle intersection or containment of a mouse click [point](#perfectshapepoint) in popular geometry shapes such as [rectangle](#perfectshaperectangle), [square](#perfectshapesquare), [arc](#perfectshapearc) (open, chord, and pie), [ellipse](#perfectshapeellipse), [circle](#perfectshapecircle), [polygon](#perfectshapepolygon), and [paths](#perfectshapepath) containing [lines](#perfectshapeline), [quadratic bézier curves](#perfectshapequadraticbeziercurve), and [cubic bezier curves](#perfectshapecubicbeziercurve), potentially with [affine transforms](#perfectshapeaffinetransform) applied like translation, scale, rotation, shear/skew, and inversion (including both [Ray Casting Algorithm](https://en.wikipedia.org/wiki/Point_in_polygon#Ray_casting_algorithm), aka [Even-odd Rule](https://en.wikipedia.org/wiki/Even%E2%80%93odd_rule), and [Winding Number Algorithm](https://en.wikipedia.org/wiki/Point_in_polygon#Winding_number_algorithm), aka [Nonzero Rule](https://en.wikipedia.org/wiki/Nonzero-rule)).
|
6
|
+
[`PerfectShape`](https://rubygems.org/gems/perfect-shape) is a collection of pure Ruby geometric algorithms that are mostly useful for GUI (Graphical User Interface) manipulation like checking viewport rectangle intersection or containment of a mouse click [point](#perfectshapepoint) in popular geometry shapes such as [rectangle](#perfectshaperectangle), [square](#perfectshapesquare), [arc](#perfectshapearc) (open, chord, and pie), [ellipse](#perfectshapeellipse), [circle](#perfectshapecircle), [polygon](#perfectshapepolygon), and [paths](#perfectshapepath) containing [lines](#perfectshapeline), [quadratic bézier curves](#perfectshapequadraticbeziercurve), and [cubic bezier curves](#perfectshapecubicbeziercurve), potentially with [affine transforms](#perfectshapeaffinetransform) applied like translation, scale, rotation, shear/skew, and inversion (including both the [Ray Casting Algorithm](https://en.wikipedia.org/wiki/Point_in_polygon#Ray_casting_algorithm), aka [Even-odd Rule](https://en.wikipedia.org/wiki/Even%E2%80%93odd_rule), and the [Winding Number Algorithm](https://en.wikipedia.org/wiki/Point_in_polygon#Winding_number_algorithm), aka [Nonzero Rule](https://en.wikipedia.org/wiki/Nonzero-rule)).
|
7
7
|
|
8
8
|
Additionally, [`PerfectShape::Math`](#perfectshapemath) contains some purely mathematical algorithms, like [IEEE 754-1985 Remainder](https://en.wikipedia.org/wiki/IEEE_754-1985).
|
9
9
|
|
10
|
-
To ensure
|
10
|
+
To ensure accuracy and preciseness, this library does all its mathematical operations with [`BigDecimal`](https://ruby-doc.org/stdlib-3.0.2/libdoc/bigdecimal/rdoc/BigDecimal.html) numbers.
|
11
11
|
|
12
12
|
## Setup
|
13
13
|
|
14
14
|
Run:
|
15
15
|
|
16
16
|
```
|
17
|
-
gem install perfect-shape -v 1.0.
|
17
|
+
gem install perfect-shape -v 1.0.4
|
18
18
|
```
|
19
19
|
|
20
20
|
Or include in Bundler `Gemfile`:
|
21
21
|
|
22
22
|
```ruby
|
23
|
-
gem 'perfect-shape', '~> 1.0.
|
23
|
+
gem 'perfect-shape', '~> 1.0.4'
|
24
24
|
```
|
25
25
|
|
26
26
|
And, run:
|
@@ -377,6 +377,7 @@ Includes `PerfectShape::RectangularShape`
|
|
377
377
|
- `#edges`: edges of rectangle as `PerfectShape::Line` objects
|
378
378
|
- `#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.
|
379
379
|
- `#empty?`: Returns `true` if width or height are 0 (or negative) and `false` otherwise
|
380
|
+
- `#to_path_shapes`: Converts `Rectangle` into basic `Path` shapes made up of `Point`s and `Line`s. Used by `Path` when adding a `Rectangle` to `Path` `shapes`
|
380
381
|
|
381
382
|
Example:
|
382
383
|
|
@@ -424,6 +425,7 @@ Extends `PerfectShape::Rectangle`
|
|
424
425
|
- `#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
|
425
426
|
- `#edges`: edges of square as `PerfectShape::Line` objects
|
426
427
|
- `#empty?`: Returns `true` if length is 0 (or negative) and `false` otherwise
|
428
|
+
- `#to_path_shapes`: Converts `Square` into basic `Path` shapes made up of `Point`s and `Line`s. Used by `Path` when adding a `Square` to `Path` `shapes`
|
427
429
|
|
428
430
|
Example:
|
429
431
|
|
@@ -467,6 +469,8 @@ Open Arc | Chord Arc | Pie Arc
|
|
467
469
|
- `#start`: start angle in degrees
|
468
470
|
- `#extent`: extent angle in degrees
|
469
471
|
- `#center_point`: center point as `Array` of `[center_x, center_y]` coordinates
|
472
|
+
- `#start_point`: start point as `Array` of (x,y) coordinates
|
473
|
+
- `#end_point`: end point as `Array` of (x,y) coordinates
|
470
474
|
- `#center_x`: center x
|
471
475
|
- `#center_y`: center y
|
472
476
|
- `#radius_x`: radius along the x-axis
|
@@ -480,6 +484,8 @@ Open Arc | Chord Arc | Pie Arc
|
|
480
484
|
- `#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
|
481
485
|
- `#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
|
482
486
|
- `#contain_angle?(angle)`: returns `true` if the angle is within the angular extents of the arc and `false` otherwise
|
487
|
+
- `#to_path_shapes`: Converts `Arc` into basic `Path` shapes made up of `Point`s, `Line`s, and `CubicBezierCurve`s. Used by `Path` when adding an `Arc` to `Path` `shapes`
|
488
|
+
- `#btan(increment)`: btan computes the length (k) of the control segments at the beginning and end of a cubic bezier that approximates a segment of an arc with extent less than or equal to 90 degrees. This length (k) will be used to generate the 2 bezier control points for such a segment.
|
483
489
|
|
484
490
|
Example:
|
485
491
|
|
@@ -604,6 +610,7 @@ Extends `PerfectShape::Arc`
|
|
604
610
|
- `#==(other)`: Returns `true` if equal to `other` or `false` otherwise
|
605
611
|
- `#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
|
606
612
|
- `#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
|
613
|
+
- `#to_path_shapes`: Converts `Ellipse` into basic `Path` shapes made up of `Point`s, `Line`s, and `CubicBezierCurve`s. Used by `Path` when adding an `Ellipse` to `Path` `shapes`
|
607
614
|
|
608
615
|
Example:
|
609
616
|
|
@@ -666,6 +673,7 @@ Extends `PerfectShape::Ellipse`
|
|
666
673
|
- `#==(other)`: Returns `true` if equal to `other` or `false` otherwise
|
667
674
|
- `#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
|
668
675
|
- `#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
|
676
|
+
- `#to_path_shapes`: Converts `Circle` into basic `Path` shapes made up of `Point`s, `Line`s, and `CubicBezierCurve`s. Used by `Path` when adding a `Circle` to `Path` `shapes`
|
669
677
|
|
670
678
|
Example:
|
671
679
|
|
@@ -754,8 +762,9 @@ Includes `PerfectShape::MultiPoint`
|
|
754
762
|
|
755
763
|
![path](https://raw.githubusercontent.com/AndyObtiva/perfect-shape/master/images/path.png)
|
756
764
|
|
757
|
-
- `::new(shapes: [], closed: false, winding_rule: :
|
758
|
-
- `#shapes`: the shapes that the path is composed of (must always start with `PerfectShape::Point` or Array of [x,y] coordinates representing start point)
|
765
|
+
- `::new(shapes: [], closed: false, winding_rule: :wind_even_odd, line_to_complex_shapes: false)`: constructs a path with `shapes` as `Array` of shape objects, which can be `PerfectShape::Point` (or `Array` of `[x, y]` coordinates), `PerfectShape::Line`, `PerfectShape::QuadraticBezierCurve`, `PerfectShape::CubicBezierCurve`, or complex shapes that decompose into the aforementioned basic path shapes, like `PerfectShape::Arc`, `PerfectShape::Ellipse`, `PerfectShape::Circle`, `PerfectShape::Rectangle`, and `PerfectShape::Square`. If a path is closed, its last point is automatically connected to its first point with a line segment. The winding rule can be `:wind_non_zero` (default) or `:wind_even_odd`. `line_to_complex_shapes` can be `true` or `false` (default), indicating whether to connect to complex shapes, meaning `Arc`, `Ellipse`, `Circle`, `Rectangle`, and `Square`, with a line, or otherwise move to their start point instead.
|
766
|
+
- `#shapes`: the shapes that the path is composed of (must always start with `PerfectShape::Point` or Array of `[x,y]` coordinates representing start point)
|
767
|
+
- `#basic_shapes`: the basic shapes that the path is composed of, meaning only `Point`, `Line`, `QuadraticBezierCurve`, and `CubicBezierCurve` shapes (decomposing complex shapes like `Arc`, `Ellipse`, `Circle`, `Rectangle`, and `Square`, using their `#to_path_shapes` method)
|
759
768
|
- `#closed?`: returns `true` if closed and `false` otherwise
|
760
769
|
- `#winding_rule`: returns winding rule (`:wind_non_zero` or `:wind_even_odd`)
|
761
770
|
- `#points`: path points calculated (derived) from shapes
|
@@ -786,7 +795,7 @@ path_shapes << PerfectShape::Line.new(points: [250, 170]) # no need for start po
|
|
786
795
|
path_shapes << PerfectShape::QuadraticBezierCurve.new(points: [[300, 185], [350, 150]]) # no need for start point, just control point and end point
|
787
796
|
path_shapes << PerfectShape::CubicBezierCurve.new(points: [[370, 50], [430, 220], [480, 170]]) # no need for start point, just two control points and end point
|
788
797
|
|
789
|
-
shape = PerfectShape::Path.new(shapes: path_shapes, closed: false, winding_rule: :
|
798
|
+
shape = PerfectShape::Path.new(shapes: path_shapes, closed: false, winding_rule: :wind_non_zero)
|
790
799
|
|
791
800
|
shape.contain?(275, 165) # => true
|
792
801
|
shape.contain?([275, 165]) # => true
|
@@ -806,7 +815,7 @@ Class
|
|
806
815
|
|
807
816
|
Extends `PerfectShape::Shape`
|
808
817
|
|
809
|
-
A composite shape is simply an aggregate of multiple shapes (e.g. square and polygon)
|
818
|
+
A composite shape is simply an aggregate of multiple shapes (e.g. square and triangle polygon)
|
810
819
|
|
811
820
|
![composite shape](https://raw.githubusercontent.com/AndyObtiva/perfect-shape/master/images/composite-shape.png)
|
812
821
|
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
1.0.
|
1
|
+
1.0.4
|
data/lib/perfect_shape/arc.rb
CHANGED
@@ -312,5 +312,156 @@ module PerfectShape
|
|
312
312
|
y = self.y + (Math.sin(angle) * 0.5 + 0.5) * self.height
|
313
313
|
[x, y]
|
314
314
|
end
|
315
|
+
|
316
|
+
# Converts Arc into basic Path shapes made up of Points, Lines, and CubicBezierCurves
|
317
|
+
# Used by Path when adding an Arc to Path shapes
|
318
|
+
def to_path_shapes
|
319
|
+
w = BigDecimal(self.width.to_s) / 2
|
320
|
+
h = BigDecimal(self.height.to_s) / 2
|
321
|
+
x = self.x + w
|
322
|
+
y = self.x + h
|
323
|
+
ang_st_rad = -Math.degrees_to_radians(self.start)
|
324
|
+
ext = -self.extent
|
325
|
+
if ext >= 360.0 || ext <= -360
|
326
|
+
arc_segs = 4
|
327
|
+
increment = Math::PI / 2
|
328
|
+
cv = 0.5522847498307933
|
329
|
+
if ext < 0
|
330
|
+
increment = -increment
|
331
|
+
cv = -cv
|
332
|
+
end
|
333
|
+
else
|
334
|
+
arc_segs = (ext.abs / 90.0).ceil
|
335
|
+
increment = Math.degrees_to_radians(ext / arc_segs)
|
336
|
+
cv = btan(increment)
|
337
|
+
arc_segs = 0 if cv == 0
|
338
|
+
end
|
339
|
+
line_segs = nil
|
340
|
+
case self.type
|
341
|
+
when :open
|
342
|
+
line_segs = 0
|
343
|
+
when :chord
|
344
|
+
line_segs = 1
|
345
|
+
when :pie
|
346
|
+
line_segs = 2
|
347
|
+
end
|
348
|
+
arc_segs = line_segs = -1 if w < 0 || h < 0
|
349
|
+
|
350
|
+
first_point_x = first_point_y = nil
|
351
|
+
(arc_segs + line_segs + 1).to_i.times.map do |index|
|
352
|
+
coords = []
|
353
|
+
angle = ang_st_rad
|
354
|
+
if index == 0
|
355
|
+
first_point_x = coords[0] = x + Math.cos(angle) * w
|
356
|
+
first_point_y = coords[1] = y + Math.sin(angle) * h
|
357
|
+
Point.new(*coords)
|
358
|
+
elsif (index > arc_segs) && (extent - start) != 0 && ((extent - start)%360 == 0)
|
359
|
+
nil
|
360
|
+
elsif (index > arc_segs) && (index < arc_segs + line_segs) && (extent - start) == 0
|
361
|
+
nil
|
362
|
+
elsif (index > arc_segs) && (index == arc_segs + line_segs)
|
363
|
+
Line.new(points: [[first_point_x, first_point_y]])
|
364
|
+
elsif index > arc_segs
|
365
|
+
coords[0] = x
|
366
|
+
coords[1] = y
|
367
|
+
Line.new(points: coords)
|
368
|
+
else
|
369
|
+
angle += increment * (index - 1)
|
370
|
+
relx = Math.cos(angle)
|
371
|
+
rely = Math.sin(angle)
|
372
|
+
coords[0] = x + (relx - cv * rely) * w
|
373
|
+
coords[1] = y + (rely + cv * relx) * h
|
374
|
+
angle += increment
|
375
|
+
relx = Math.cos(angle)
|
376
|
+
rely = Math.sin(angle)
|
377
|
+
coords[2] = x + (relx + cv * rely) * w
|
378
|
+
coords[3] = y + (rely - cv * relx) * h
|
379
|
+
coords[4] = x + relx * w
|
380
|
+
coords[5] = y + rely * h
|
381
|
+
CubicBezierCurve.new(points: coords)
|
382
|
+
end
|
383
|
+
end.compact
|
384
|
+
end
|
385
|
+
|
386
|
+
# btan computes the length (k) of the control segments at
|
387
|
+
# the beginning and end of a cubic bezier that approximates
|
388
|
+
# a segment of an arc with extent less than or equal to
|
389
|
+
# 90 degrees. This length (k) will be used to generate the
|
390
|
+
# 2 bezier control points for such a segment.
|
391
|
+
#
|
392
|
+
# Assumptions:
|
393
|
+
# a) arc is centered on 0,0 with radius of 1.0
|
394
|
+
# b) arc extent is less than 90 degrees
|
395
|
+
# c) control points should preserve tangent
|
396
|
+
# d) control segments should have equal length
|
397
|
+
#
|
398
|
+
# Initial data:
|
399
|
+
# start angle: ang1
|
400
|
+
# end angle: ang2 = ang1 + extent
|
401
|
+
# start point: P1 = (x1, y1) = (cos(ang1), sin(ang1))
|
402
|
+
# end point: P4 = (x4, y4) = (cos(ang2), sin(ang2))
|
403
|
+
#
|
404
|
+
# Control points:
|
405
|
+
# P2 = (x2, y2)
|
406
|
+
# | x2 = x1 - k * sin(ang1) = cos(ang1) - k * sin(ang1)
|
407
|
+
# | y2 = y1 + k * cos(ang1) = sin(ang1) + k * cos(ang1)
|
408
|
+
#
|
409
|
+
# P3 = (x3, y3)
|
410
|
+
# | x3 = x4 + k * sin(ang2) = cos(ang2) + k * sin(ang2)
|
411
|
+
# | y3 = y4 - k * cos(ang2) = sin(ang2) - k * cos(ang2)
|
412
|
+
#
|
413
|
+
# The formula for this length (k) can be found using the
|
414
|
+
# following derivations:
|
415
|
+
#
|
416
|
+
# Midpoints:
|
417
|
+
# a) bezier (t = 1/2)
|
418
|
+
# bPm = P1 * (1-t)^3 +
|
419
|
+
# 3 * P2 * t * (1-t)^2 +
|
420
|
+
# 3 * P3 * t^2 * (1-t) +
|
421
|
+
# P4 * t^3 =
|
422
|
+
# = (P1 + 3P2 + 3P3 + P4)/8
|
423
|
+
#
|
424
|
+
# b) arc
|
425
|
+
# aPm = (cos((ang1 + ang2)/2), sin((ang1 + ang2)/2))
|
426
|
+
#
|
427
|
+
# Let angb = (ang2 - ang1)/2; angb is half of the angle
|
428
|
+
# between ang1 and ang2.
|
429
|
+
#
|
430
|
+
# Solve the equation bPm == aPm
|
431
|
+
#
|
432
|
+
# a) For xm coord:
|
433
|
+
# x1 + 3*x2 + 3*x3 + x4 = 8*cos((ang1 + ang2)/2)
|
434
|
+
#
|
435
|
+
# cos(ang1) + 3*cos(ang1) - 3*k*sin(ang1) +
|
436
|
+
# 3*cos(ang2) + 3*k*sin(ang2) + cos(ang2) =
|
437
|
+
# = 8*cos((ang1 + ang2)/2)
|
438
|
+
#
|
439
|
+
# 4*cos(ang1) + 4*cos(ang2) + 3*k*(sin(ang2) - sin(ang1)) =
|
440
|
+
# = 8*cos((ang1 + ang2)/2)
|
441
|
+
#
|
442
|
+
# 8*cos((ang1 + ang2)/2)*cos((ang2 - ang1)/2) +
|
443
|
+
# 6*k*sin((ang2 - ang1)/2)*cos((ang1 + ang2)/2) =
|
444
|
+
# = 8*cos((ang1 + ang2)/2)
|
445
|
+
#
|
446
|
+
# 4*cos(angb) + 3*k*sin(angb) = 4
|
447
|
+
#
|
448
|
+
# k = 4 / 3 * (1 - cos(angb)) / sin(angb)
|
449
|
+
#
|
450
|
+
# b) For ym coord we derive the same formula.
|
451
|
+
#
|
452
|
+
# Since this formula can generate "NaN" values for small
|
453
|
+
# angles, we will derive a safer form that does not involve
|
454
|
+
# dividing by very small values:
|
455
|
+
# (1 - cos(angb)) / sin(angb) =
|
456
|
+
# = (1 - cos(angb))*(1 + cos(angb)) / sin(angb)*(1 + cos(angb)) =
|
457
|
+
# = (1 - cos(angb)^2) / sin(angb)*(1 + cos(angb)) =
|
458
|
+
# = sin(angb)^2 / sin(angb)*(1 + cos(angb)) =
|
459
|
+
# = sin(angb) / (1 + cos(angb))
|
460
|
+
#
|
461
|
+
def btan(increment)
|
462
|
+
return 0 if increment.nan?
|
463
|
+
increment /= BigDecimal('2.0')
|
464
|
+
BigDecimal('4.0') / BigDecimal('3.0') * Math.sin(increment) / (BigDecimal('1.0') + Math.cos(increment))
|
465
|
+
end
|
315
466
|
end
|
316
467
|
end
|
data/lib/perfect_shape/path.rb
CHANGED
@@ -24,6 +24,11 @@ require 'perfect_shape/point'
|
|
24
24
|
require 'perfect_shape/line'
|
25
25
|
require 'perfect_shape/quadratic_bezier_curve'
|
26
26
|
require 'perfect_shape/cubic_bezier_curve'
|
27
|
+
require 'perfect_shape/arc'
|
28
|
+
require 'perfect_shape/ellipse'
|
29
|
+
require 'perfect_shape/circle'
|
30
|
+
require 'perfect_shape/rectangle'
|
31
|
+
require 'perfect_shape/square'
|
27
32
|
require 'perfect_shape/multi_point'
|
28
33
|
|
29
34
|
module PerfectShape
|
@@ -32,28 +37,34 @@ module PerfectShape
|
|
32
37
|
include Equalizer.new(:shapes, :closed, :winding_rule)
|
33
38
|
|
34
39
|
# Available class types for path shapes
|
35
|
-
SHAPE_TYPES = [Array, PerfectShape::Point, PerfectShape::Line, PerfectShape::QuadraticBezierCurve, PerfectShape::CubicBezierCurve]
|
40
|
+
SHAPE_TYPES = [Array, PerfectShape::Point, PerfectShape::Line, PerfectShape::QuadraticBezierCurve, PerfectShape::CubicBezierCurve, PerfectShape::Arc, PerfectShape::Ellipse, PerfectShape::Circle, PerfectShape::Rectangle, PerfectShape::Square]
|
36
41
|
|
37
42
|
# Available winding rules
|
38
43
|
WINDING_RULES = [:wind_even_odd, :wind_non_zero]
|
39
44
|
|
40
45
|
attr_reader :winding_rule
|
41
|
-
attr_accessor :shapes, :closed
|
46
|
+
attr_accessor :shapes, :closed, :line_to_complex_shapes
|
42
47
|
alias closed? closed
|
48
|
+
alias line_to_complex_shapes? line_to_complex_shapes
|
43
49
|
|
44
|
-
# Constructs Path with winding rule, closed status, and shapes (must always start with PerfectShape::Point or Array of [x,y] coordinates)
|
45
|
-
# Shape class types can be any of SHAPE_TYPES: Array (x,y coordinates), PerfectShape::Point, PerfectShape::Line, PerfectShape::QuadraticBezierCurve,
|
50
|
+
# Constructs Path with winding rule, closed status, line_to_complex_shapes option, and shapes (must always start with PerfectShape::Point or Array of [x,y] coordinates)
|
51
|
+
# Shape class types can be any of SHAPE_TYPES: Array (x,y coordinates), PerfectShape::Point, PerfectShape::Line, PerfectShape::QuadraticBezierCurve, PerfectShape::CubicBezierCurve
|
52
|
+
# PerfectShape::Arc, PerfectShape::Ellipse, or PerfectShape::Circle
|
53
|
+
# Complex shapes, meaning Arc, Ellipse, and Circle, are decomposed into basic path shapes, meaning Point, Line, QuadraticBezierCurve, and CubicBezierCurve.
|
46
54
|
# winding_rule can be any of WINDING_RULES: :wind_non_zero (default) or :wind_even_odd
|
47
|
-
# closed can be true or false
|
48
|
-
|
55
|
+
# closed can be true or false (default)
|
56
|
+
# line_to_complex_shapes can be true or false (default), indicating whether to connect to complex shapes,
|
57
|
+
# meaning Arc, Ellipse, and Circle, with a line, or otherwise move to their start point instead.
|
58
|
+
def initialize(shapes: [], closed: false, winding_rule: :wind_even_odd, line_to_complex_shapes: false)
|
49
59
|
self.closed = closed
|
50
60
|
self.winding_rule = winding_rule
|
51
61
|
self.shapes = shapes
|
62
|
+
self.line_to_complex_shapes = line_to_complex_shapes
|
52
63
|
end
|
53
64
|
|
54
65
|
def points
|
55
66
|
the_points = []
|
56
|
-
|
67
|
+
basic_shapes.each do |shape|
|
57
68
|
case shape
|
58
69
|
when Point
|
59
70
|
the_points << shape.to_a
|
@@ -71,7 +82,7 @@ module PerfectShape
|
|
71
82
|
end
|
72
83
|
end
|
73
84
|
end
|
74
|
-
the_points <<
|
85
|
+
the_points << basic_shapes.first.to_a if closed?
|
75
86
|
the_points
|
76
87
|
end
|
77
88
|
|
@@ -80,7 +91,7 @@ module PerfectShape
|
|
80
91
|
end
|
81
92
|
|
82
93
|
def drawing_types
|
83
|
-
the_drawing_shapes =
|
94
|
+
the_drawing_shapes = basic_shapes.map do |shape|
|
84
95
|
case shape
|
85
96
|
when Point
|
86
97
|
:move_to
|
@@ -233,9 +244,9 @@ module PerfectShape
|
|
233
244
|
# Lastly, if the path is closed, an extra shape is
|
234
245
|
# added to represent the line connecting the last point to the first
|
235
246
|
def disconnected_shapes
|
236
|
-
initial_point = start_point =
|
247
|
+
initial_point = start_point = basic_shapes.first.to_a.map {|n| BigDecimal(n.to_s)}
|
237
248
|
final_point = nil
|
238
|
-
the_disconnected_shapes =
|
249
|
+
the_disconnected_shapes = basic_shapes.drop(1).map do |shape|
|
239
250
|
case shape
|
240
251
|
when Point
|
241
252
|
disconnected_shape = Point.new(*shape.to_a)
|
@@ -372,5 +383,25 @@ module PerfectShape
|
|
372
383
|
# assert((crossings & 1) != 0)
|
373
384
|
crossings
|
374
385
|
end
|
386
|
+
|
387
|
+
# Returns basic shapes (i.e. Point, Line, QuadraticBezierCurve, and CubicBezierCurve),
|
388
|
+
# decomposed from complex shapes like Arc, Ellipse, and Circle by calling their `#to_path_shapes` method
|
389
|
+
def basic_shapes
|
390
|
+
the_shapes = []
|
391
|
+
@shapes.each do |shape|
|
392
|
+
if shape.respond_to?(:to_path_shapes)
|
393
|
+
shape_basic_shapes = shape.to_path_shapes
|
394
|
+
if @line_to_complex_shapes
|
395
|
+
first_basic_shape = shape_basic_shapes.shift
|
396
|
+
new_first_basic_shape = PerfectShape::Line.new(points: [first_basic_shape.to_a])
|
397
|
+
shape_basic_shapes.unshift(new_first_basic_shape)
|
398
|
+
end
|
399
|
+
the_shapes += shape_basic_shapes
|
400
|
+
else
|
401
|
+
the_shapes << shape
|
402
|
+
end
|
403
|
+
end
|
404
|
+
the_shapes
|
405
|
+
end
|
375
406
|
end
|
376
407
|
end
|
@@ -116,5 +116,43 @@ module PerfectShape
|
|
116
116
|
x < (x0 + self.width) &&
|
117
117
|
y < (y0 + self.height)
|
118
118
|
end
|
119
|
+
|
120
|
+
# Converts Rectangle into basic Path shapes made up of Points and Lines
|
121
|
+
# Used by Path when adding a Rectangle to Path shapes
|
122
|
+
def to_path_shapes
|
123
|
+
path_shapes = []
|
124
|
+
x = self.x
|
125
|
+
y = self.y
|
126
|
+
w = self.width
|
127
|
+
h = self.height
|
128
|
+
index = 0
|
129
|
+
index = 5 if (w < 0 || h < 0)
|
130
|
+
max_index = 4
|
131
|
+
max_index = 2 if (w == 0 && h > 0) || (w > 0 && h == 0)
|
132
|
+
max_index = 1 if (w == 0 && h == 0)
|
133
|
+
first_point_x = first_point_y = nil
|
134
|
+
|
135
|
+
until index > max_index
|
136
|
+
if index == max_index
|
137
|
+
path_shapes << Line.new(points: [[first_point_x, first_point_y]])
|
138
|
+
else
|
139
|
+
coords = []
|
140
|
+
coords[0] = x
|
141
|
+
coords[1] = y
|
142
|
+
coords[0] += w if ([2, 4].include?(max_index) && index == 1) || (max_index == 4 && index == 2)
|
143
|
+
coords[1] += h if (max_index == 2 && index == 1) || (max_index == 4 && [2, 3].include?(index))
|
144
|
+
if index == 0
|
145
|
+
first_point_x = coords[0]
|
146
|
+
first_point_y = coords[1]
|
147
|
+
path_shapes << Point.new(coords)
|
148
|
+
else
|
149
|
+
path_shapes << Line.new(points: coords)
|
150
|
+
end
|
151
|
+
end
|
152
|
+
index += 1
|
153
|
+
end
|
154
|
+
|
155
|
+
path_shapes
|
156
|
+
end
|
119
157
|
end
|
120
158
|
end
|
data/perfect-shape.gemspec
CHANGED
@@ -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 1.0.
|
5
|
+
# stub: perfect-shape 1.0.4 ruby lib
|
6
6
|
|
7
7
|
Gem::Specification.new do |s|
|
8
8
|
s.name = "perfect-shape".freeze
|
9
|
-
s.version = "1.0.
|
9
|
+
s.version = "1.0.4"
|
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-
|
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
|
14
|
+
s.date = "2022-05-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 the Ray Casting Algorithm, aka Even-odd Rule, and the 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",
|
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: 1.0.
|
4
|
+
version: 1.0.4
|
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-
|
11
|
+
date: 2022-05-15 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: equalizer
|
@@ -106,9 +106,9 @@ description: Perfect Shape is a collection of pure Ruby geometric algorithms tha
|
|
106
106
|
square, arc (open, chord, and pie), ellipse, circle, polygon, and paths containing
|
107
107
|
lines, quadratic bézier curves, and cubic bezier curves, potentially with affine
|
108
108
|
transforms applied like translation, scale, rotation, shear/skew, and inversion
|
109
|
-
(including both Ray Casting Algorithm, aka Even-odd Rule, and Winding Number
|
110
|
-
aka Nonzero Rule). Additionally, it contains some purely mathematical
|
111
|
-
like IEEEremainder (also known as IEEE-754 remainder).
|
109
|
+
(including both the Ray Casting Algorithm, aka Even-odd Rule, and the Winding Number
|
110
|
+
Algorithm, aka Nonzero Rule). Additionally, it contains some purely mathematical
|
111
|
+
algorithms like IEEEremainder (also known as IEEE-754 remainder).
|
112
112
|
email: andy.am@gmail.com
|
113
113
|
executables: []
|
114
114
|
extensions: []
|