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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e83221df061b944acb69abfec2536a0c0bc3c8176d6f5b4929c68bb6283e7f82
4
- data.tar.gz: 5f778729c9b39a259708a0e6995b370f040442b4864f7594418fafdfd1208d12
3
+ metadata.gz: d98187ae023d5501ed920790105a46340127a38a7a1b83143b9c69d4695dc60e
4
+ data.tar.gz: 4654765dbb6cf6f091677e95551f632cb65c2e739874d5cbe105c6dcd8a70645
5
5
  SHA512:
6
- metadata.gz: 9e18a4d841f70f938c220c21e1d928273d5a24a6f114a4a2b8c0f2c0c6ca020a80510f9dd698a092582607d5bd8a224e5c6c6aea875a443a740d64982ae4f2d4
7
- data.tar.gz: 5712f83712d505f3c7156dfd9e194735b73215542c9012bda5359544f72eac4743f00d9e39f97c98a2bee22c43dd45f808554abad3a5ea6f2b983e925f3cadb2
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
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 high accuracy, this library does all its mathematical operations with [`BigDecimal`](https://ruby-doc.org/stdlib-3.0.2/libdoc/bigdecimal/rdoc/BigDecimal.html) numbers.
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.1
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.1'
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: :wind_non_zero)`: 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`, or `PerfectShape::CubicBezierCurve`. 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`.
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: :wind_even_odd)
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
+ 1.0.4
@@ -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
@@ -213,8 +213,6 @@ module PerfectShape
213
213
  end
214
214
 
215
215
  def intersect?(rectangle)
216
- x = rectangle.x
217
- y = rectangle.y
218
216
  w = rectangle.width
219
217
  h = rectangle.height
220
218
 
@@ -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, or PerfectShape::CubicBezierCurve
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
- def initialize(shapes: [], closed: false, winding_rule: :wind_even_odd)
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
- @shapes.each do |shape|
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 << @shapes.first.to_a if closed?
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 = @shapes.map do |shape|
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 = @shapes.first.to_a.map {|n| BigDecimal(n.to_s)}
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 = @shapes.drop(1).map do |shape|
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
@@ -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.1 ruby lib
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.1"
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-02-16"
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.1
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-02-16 00:00:00.000000000 Z
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 Algorithm,
110
- aka Nonzero Rule). Additionally, it contains some purely mathematical algorithms
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: []