perfect-shape 0.3.3 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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)
@@ -114,21 +113,26 @@ module PerfectShape
114
113
  # @return true if the point lies within the bound of
115
114
  # the path or false if the point lies outside of the
116
115
  # path's bounds.
117
- def contain?(x_or_point, y = nil)
118
- x, y = normalize_point(x_or_point, y)
116
+ def contain?(x_or_point, y = nil, outline: false, distance_tolerance: 0)
117
+ x, y = Point.normalize_point(x_or_point, y)
119
118
  return unless x && y
120
- if (x * 0.0 + y * 0.0) == 0.0
121
- # N * 0.0 is 0.0 only if N is finite.
122
- # Here we know that both x and y are finite.
123
- return false if shapes.count < 2
124
- mask = winding_rule == :wind_non_zero ? -1 : 1
125
- (point_crossings(x, y) & mask) != 0
119
+
120
+ if outline
121
+ disconnected_shapes.any? {|shape| shape.contain?(x, y, outline: true, distance_tolerance: distance_tolerance) }
126
122
  else
127
- # Either x or y was infinite or NaN.
128
- # A NaN always produces a negative response to any test
129
- # and Infinity values cannot be "inside" any path so
130
- # they should return false as well.
131
- false
123
+ if (x * 0.0 + y * 0.0) == 0.0
124
+ # N * 0.0 is 0.0 only if N is finite.
125
+ # Here we know that both x and y are finite.
126
+ return false if shapes.count < 2
127
+ mask = winding_rule == :wind_non_zero ? -1 : 1
128
+ (point_crossings(x, y) & mask) != 0
129
+ else
130
+ # Either x or y was infinite or NaN.
131
+ # A NaN always produces a negative response to any test
132
+ # and Infinity values cannot be "inside" any path so
133
+ # they should return false as well.
134
+ false
135
+ end
132
136
  end
133
137
  end
134
138
 
@@ -144,7 +148,7 @@ module PerfectShape
144
148
  # The caller must check for NaN values.
145
149
  # The caller may also reject infinite values as well.
146
150
  def point_crossings(x_or_point, y = nil)
147
- x, y = normalize_point(x_or_point, y)
151
+ x, y = Point.normalize_point(x_or_point, y)
148
152
  return unless x && y
149
153
  return 0 if shapes.count == 0
150
154
  movx = movy = curx = cury = endx = endy = 0
@@ -218,5 +222,50 @@ module PerfectShape
218
222
  end
219
223
  crossings
220
224
  end
225
+
226
+ # Disconnected shapes have their start point filled in
227
+ # so that each shape does not depend on the previous shape
228
+ # to determine its start point.
229
+ #
230
+ # Also, if a point is followed by a non-point shape, it is removed
231
+ # since it is augmented to the following shape as its start point.
232
+ #
233
+ # Lastly, if the path is closed, an extra shape is
234
+ # added to represent the line connecting the last point to the first
235
+ def disconnected_shapes
236
+ initial_point = start_point = @shapes.first.to_a.map {|n| BigDecimal(n.to_s)}
237
+ final_point = nil
238
+ the_disconnected_shapes = @shapes.drop(1).map do |shape|
239
+ case shape
240
+ when Point
241
+ disconnected_shape = Point.new(*shape.to_a)
242
+ start_point = shape.to_a
243
+ final_point = disconnected_shape.to_a
244
+ nil
245
+ when Array
246
+ disconnected_shape = Point.new(*shape.map {|n| BigDecimal(n.to_s)})
247
+ start_point = shape.map {|n| BigDecimal(n.to_s)}
248
+ final_point = disconnected_shape.to_a
249
+ nil
250
+ when Line
251
+ disconnected_shape = Line.new(points: [start_point.to_a, shape.points.last])
252
+ start_point = shape.points.last.to_a
253
+ final_point = disconnected_shape.points.last.to_a
254
+ disconnected_shape
255
+ when QuadraticBezierCurve
256
+ disconnected_shape = QuadraticBezierCurve.new(points: [start_point.to_a] + shape.points)
257
+ start_point = shape.points.last.to_a
258
+ final_point = disconnected_shape.points.last.to_a
259
+ disconnected_shape
260
+ when CubicBezierCurve
261
+ disconnected_shape = CubicBezierCurve.new(points: [start_point.to_a] + shape.points)
262
+ start_point = shape.points.last.to_a
263
+ final_point = disconnected_shape.points.last.to_a
264
+ disconnected_shape
265
+ end
266
+ end
267
+ the_disconnected_shapes << Line.new(points: [final_point, initial_point]) if closed?
268
+ the_disconnected_shapes.compact
269
+ end
221
270
  end
222
271
  end
@@ -27,12 +27,27 @@ module PerfectShape
27
27
  class Point < Shape
28
28
  class << self
29
29
  def point_distance(x, y, px, py)
30
- x = BigDecimal(x.to_s)
31
- y = BigDecimal(y.to_s)
32
- px = BigDecimal(px.to_s)
33
- py = BigDecimal(py.to_s)
30
+ x = x.is_a?(BigDecimal) ? x : BigDecimal(x.to_s)
31
+ y = y.is_a?(BigDecimal) ? y : BigDecimal(y.to_s)
32
+ px = px.is_a?(BigDecimal) ? px : BigDecimal(px.to_s)
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
@@ -67,16 +82,17 @@ module PerfectShape
67
82
  #
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
- def contain?(x_or_point, y = nil, outline: false, distance_tolerance: 0)
71
- x, y = normalize_point(x_or_point, y)
85
+ def contain?(x_or_point, y = nil, outline: true, distance_tolerance: 0)
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
95
+
80
96
  Point.point_distance(self.x, self.y, x, y)
81
97
  end
82
98
 
@@ -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)
@@ -80,7 +80,7 @@ module PerfectShape
80
80
  # the quadratic bézier curve, {@code false} if the point lies outside of the
81
81
  # quadratic bézier curve's bounds.
82
82
  def contain?(x_or_point, y = nil, outline: false, distance_tolerance: 0)
83
- x, y = normalize_point(x_or_point, y)
83
+ x, y = Point.normalize_point(x_or_point, y)
84
84
  return unless x && y
85
85
 
86
86
  x1 = points[0][0]
@@ -91,8 +91,9 @@ module PerfectShape
91
91
  y2 = points[2][1]
92
92
 
93
93
  if outline
94
+ distance_tolerance = BigDecimal(distance_tolerance.to_s)
94
95
  minimum_distance_threshold = OUTLINE_MINIMUM_DISTANCE_THRESHOLD + distance_tolerance
95
- point_segment_distance(x, y, minimum_distance_threshold: minimum_distance_threshold) < minimum_distance_threshold
96
+ point_distance(x, y, minimum_distance_threshold: minimum_distance_threshold) < minimum_distance_threshold
96
97
  else
97
98
  # We have a convex shape bounded by quad curve Pc(t)
98
99
  # and ine Pl(t).
@@ -189,13 +190,13 @@ module PerfectShape
189
190
  # +1 is added for each crossing where the Y coordinate is increasing
190
191
  # -1 is added for each crossing where the Y coordinate is decreasing
191
192
  def point_crossings(x_or_point, y = nil, level = 0)
192
- x, y = normalize_point(x_or_point, y)
193
+ x, y = Point.normalize_point(x_or_point, y)
193
194
  return unless x && y
194
195
  QuadraticBezierCurve.point_crossings(points[0][0], points[0][1], points[1][0], points[1][1], points[2][0], points[2][1], x, y, level)
195
196
  end
196
197
 
197
-
198
198
  # The center point on the outline of the curve
199
+ # in Array format as pair of (x, y) coordinates
199
200
  def curve_center_point
200
201
  subdivisions.last.points[0]
201
202
  end
@@ -226,12 +227,12 @@ module PerfectShape
226
227
  ctrly = points[1][1]
227
228
  x2 = points[2][0]
228
229
  y2 = points[2][1]
229
- ctrlx1 = (x1 + ctrlx) / 2.0
230
- ctrly1 = (y1 + ctrly) / 2.0
231
- ctrlx2 = (x2 + ctrlx) / 2.0
232
- ctrly2 = (y2 + ctrly) / 2.0
233
- centerx = (ctrlx1 + ctrlx2) / 2.0
234
- centery = (ctrly1 + ctrly2) / 2.0
230
+ ctrlx1 = BigDecimal((x1 + ctrlx).to_s) / 2
231
+ ctrly1 = BigDecimal((y1 + ctrly).to_s) / 2
232
+ ctrlx2 = BigDecimal((x2 + ctrlx).to_s) / 2
233
+ ctrly2 = BigDecimal((y2 + ctrly).to_s) / 2
234
+ centerx = BigDecimal((ctrlx1 + ctrlx2).to_s) / 2
235
+ centery = BigDecimal((ctrly1 + ctrly2).to_s) / 2
235
236
 
236
237
  default_subdivisions = [
237
238
  QuadraticBezierCurve.new(points: [x1, y1, ctrlx1, ctrly1, centerx, centery]),
@@ -245,8 +246,8 @@ module PerfectShape
245
246
  end
246
247
  end
247
248
 
248
- def point_segment_distance(x_or_point, y = nil, minimum_distance_threshold: OUTLINE_MINIMUM_DISTANCE_THRESHOLD)
249
- x, y = normalize_point(x_or_point, y)
249
+ def point_distance(x_or_point, y = nil, minimum_distance_threshold: OUTLINE_MINIMUM_DISTANCE_THRESHOLD)
250
+ x, y = Point.normalize_point(x_or_point, y)
250
251
  return unless x && y
251
252
 
252
253
  point = Point.new(x, y)
@@ -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,8 +50,9 @@ 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
55
+
43
56
  if outline
44
57
  edges.any? { |edge| edge.contain?(x, y, distance_tolerance: distance_tolerance) }
45
58
  else
@@ -55,5 +68,32 @@ module PerfectShape
55
68
  Line.new(points: [[self.x, self.y + height], [self.x, self.y]])
56
69
  ]
57
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
58
98
  end
59
99
  end
@@ -51,6 +51,12 @@ module PerfectShape
51
51
  max_y - min_y if max_y && min_y
52
52
  end
53
53
 
54
+ # Center point is `[center_x, center_y]`
55
+ # Returns `nil` if either center_x or center_y are `nil`
56
+ def center_point
57
+ [center_x, center_y] unless center_x.nil? || center_y.nil?
58
+ end
59
+
54
60
  # center_x is min_x + width/2.0 by default
55
61
  # Returns nil if min_x or width are nil
56
62
  def center_x
@@ -69,21 +75,6 @@ module PerfectShape
69
75
  Rectangle.new(x: min_x, y: min_y, width: width, height: height)
70
76
  end
71
77
 
72
- # Normalizes point args whether two-number Array or x, y args returning
73
- # normalized point array of two BigDecimal's
74
- #
75
- # @param x_or_point The point or X coordinate of the point to test.
76
- # @param y The Y coordinate of the point to test.
77
- #
78
- # @return Array of x and y BigDecimal's representing point
79
- def normalize_point(x_or_point, y = nil)
80
- x = x_or_point
81
- x, y = x if y.nil? && x_or_point.is_a?(Array) && x_or_point.size == 2
82
- x = BigDecimal(x.to_s)
83
- y = BigDecimal(y.to_s)
84
- [x, y]
85
- end
86
-
87
78
  # Subclasses must implement
88
79
  def contain?(x_or_point, y = nil, outline: false, distance_tolerance: 0)
89
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.3 ruby lib
5
+ # stub: perfect-shape 0.5.0 ruby lib
6
6
 
7
7
  Gem::Specification.new do |s|
8
8
  s.name = "perfect-shape".freeze
9
- s.version = "0.3.3"
9
+ s.version = "0.5.0"
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-10"
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-18"
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.3
4
+ version: 0.5.0
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-10 00:00:00.000000000 Z
11
+ date: 2022-01-18 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