perfect-shape 0.1.0 → 0.1.1

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: 8bb743eb7a0d935839d671a33b58a30ea8eb65b945ee20e6016690fd0c6864f8
4
- data.tar.gz: cd5b26777efcf931ed81a84ec63fee3dcaed5dfa3b878dc39350e6eb206f937e
3
+ metadata.gz: a4eb7bc277a02c0795966623e05ee57514483d982bc0d80f264a525f6b3d5afa
4
+ data.tar.gz: f070a8292b835f6d2c68ff4ccddd3073b7a83d36e882d4e5c34c6e91a85d88ee
5
5
  SHA512:
6
- metadata.gz: 7adf8c29ae005e8f22c15e460dcf99befd28acef67a5387d950bef1d94781be6fce9cb25b725092892da83a6821b5b59bc699cf5c581af11133d90b4946ed29b
7
- data.tar.gz: 334ef034a6f1010f49ad2fa2edc9dfccae994d0f8524427104100b6f4ccc83a63b276f731d483c494625111ad5a668ac05300c9b589ae57b801645d393130c38
6
+ metadata.gz: 1add8519bdd16f38f8fb8431d8342c77df9f5d8d76f0dc6f1481bba5d9c0a15b653c56a9a0e372082f1590b55f5897ebac3f6a4d4505acd6ba017f257261e80d
7
+ data.tar.gz: fc3e2fce45f0a46ad7869fd5fec07d0dcf826e3bd36676439144eb3fd0198b8224d816a26db54858b43d0bb5332ff6b4108950efca3e71c1d5e42cebde4de63f
data/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Change Log
2
2
 
3
+ ## 0.1.1
4
+
5
+ - `PerfectShape::QuadraticBezierCurve` (two end points and one control point)
6
+ - `PerfectShape::QuadraticBezierCurve#contain?(x_or_point, y=nil)`
7
+ - `PerfectShape::QuadraticBezierCurve#==`
8
+ - `PerfectShape::Path` having quadratic bezier curves in addition to points and lines
9
+
3
10
  ## 0.1.0
4
11
 
5
12
  - `PerfectShape::Path` (having points or lines)
data/README.md CHANGED
@@ -1,9 +1,9 @@
1
- # Perfect Shape 0.1.0
1
+ # Perfect Shape 0.1.1
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 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), polyline, polyquad, polycubic, and [paths](#perfectshapepath) containing [lines](#perfectshapeline), quadratic bézier curves, and cubic bézier curves (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 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 bézier curves (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)).
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
 
@@ -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.1.0
17
+ gem install perfect-shape -v 0.1.1
18
18
  ```
19
19
 
20
20
  Or include in Bundler `Gemfile`:
21
21
 
22
22
  ```ruby
23
- gem 'perfect-shape', '~> 0.1.0'
23
+ gem 'perfect-shape', '~> 0.1.1'
24
24
  ```
25
25
 
26
26
  And, run:
@@ -114,6 +114,12 @@ Points are simply represented by an `Array` of `[x,y]` coordinates when used wit
114
114
  - `#point_distance(x_or_point, y=nil)`: Returns the distance from a point to another point
115
115
  - `#==(other)`: Returns `true` if equal to `other` or `false` otherwise
116
116
 
117
+ Example:
118
+
119
+ ```ruby
120
+ shape = PerfectShape::Point.new(x: 200, y: 150)
121
+ ```
122
+
117
123
  ### `PerfectShape::Line`
118
124
 
119
125
  Class
@@ -127,7 +133,7 @@ Includes `PerfectShape::MultiPoint`
127
133
  - `::relative_counterclockwise(x1, y1, x2, y2, px, py)`: Returns an indicator of where the specified point (px,py) lies with respect to the line segment from (x1,y1) to (x2,y2). The return value can be either 1, -1, or 0 and indicates in which direction the specified line must pivot around its first end point, (x1,y1), in order to point at the specified point (px,py). A return value of 1 indicates that the line segment must turn in the direction that takes the positive X axis towards the negative Y axis. In the default coordinate system used by Java 2D, this direction is counterclockwise. A return value of -1 indicates that the line segment must turn in the direction that takes the positive X axis towards the positive Y axis. In the default coordinate system, this direction is clockwise. A return value of 0 indicates that the point lies exactly on the line segment. Note that an indicator value of 0 is rare and not useful for determining collinearity because of floating point rounding issues. If the point is colinear with the line segment, but not between the end points, then the value will be -1 if the point lies “beyond (x1,y1)” or 1 if the point lies “beyond (x2,y2)”.
128
134
  - `::point_segment_distance_square(x1, y1, x2, y2, px, py)`: Returns the square of distance from a point to a line segment.
129
135
  - `::point_segment_distance(x1, y1, x2, y2, px, py)`: Returns the distance from a point to a line segment.
130
- - `::new(points: nil)`: constructs a polygon with `points` as `Array` of `Array`s of `[x,y]` pairs or flattened `Array` of alternating x and y values
136
+ - `::new(points: [])`: constructs a line with two `points` as `Array` of `Array`s of `[x,y]` pairs or flattened `Array` of alternating x and y values
131
137
  - `#min_x`: min x
132
138
  - `#min_y`: min y
133
139
  - `#max_x`: max x
@@ -142,6 +148,41 @@ Includes `PerfectShape::MultiPoint`
142
148
  - `#point_segment_distance(x_or_point, y=nil)`: Returns the distance from a point to a line segment.
143
149
  - `#==(other)`: Returns `true` if equal to `other` or `false` otherwise
144
150
 
151
+ Example:
152
+
153
+ ```ruby
154
+ shape = PerfectShape::Line.new(points: [[200, 150], [270, 220]]) # start point and end point
155
+ ```
156
+
157
+ ### `PerfectShape::QuadraticBezierCurve`
158
+
159
+ Class
160
+
161
+ Extends `PerfectShape::Shape`
162
+
163
+ Includes `PerfectShape::MultiPoint`
164
+
165
+ ![quadratic_bezier_curve](https://raw.githubusercontent.com/AndyObtiva/perfect-shape/master/images/quadratic_bezier_curve.png)
166
+
167
+ - `::new(points: [])`: constructs a quadratic bézier curve with three `points` (two end points and one control point) as `Array` of `Array`s of `[x,y]` pairs or flattened `Array` of alternating x and y values
168
+ - `#min_x`: min x
169
+ - `#min_y`: min y
170
+ - `#max_x`: max x
171
+ - `#max_y`: max y
172
+ - `#width`: width (from min x to max x)
173
+ - `#height`: height (from min y to max y)
174
+ - `#center_x`: center x
175
+ - `#center_y`: center y
176
+ - `#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)
177
+ - `#contain?(x_or_point, y=nil)`: checks if point is inside
178
+ - `#==(other)`: Returns `true` if equal to `other` or `false` otherwise
179
+
180
+ Example:
181
+
182
+ ```ruby
183
+ shape = PerfectShape::QuadraticBezierCurve.new(points: [[200, 150], [270, 220], [180, 170]]) # start point, control point, and end point
184
+ ```
185
+
145
186
  ### `PerfectShape::Rectangle`
146
187
 
147
188
  Class
@@ -167,6 +208,12 @@ Includes `PerfectShape::RectangularShape`
167
208
  - `#contain?(x_or_point, y=nil)`: checks if point is inside
168
209
  - `#==(other)`: Returns `true` if equal to `other` or `false` otherwise
169
210
 
211
+ Example:
212
+
213
+ ```ruby
214
+ shape = PerfectShape::Rectangle.new(x: 15, y: 30, width: 200, height: 100)
215
+ ```
216
+
170
217
  ### `PerfectShape::Square`
171
218
 
172
219
  Class
@@ -191,6 +238,12 @@ Extends `PerfectShape::Rectangle`
191
238
  - `#contain?(x_or_point, y=nil)`: checks if point is inside
192
239
  - `#==(other)`: Returns `true` if equal to `other` or `false` otherwise
193
240
 
241
+ Example:
242
+
243
+ ```ruby
244
+ shape = PerfectShape::Square.new(x: 15, y: 30, length: 200)
245
+ ```
246
+
194
247
  ### `PerfectShape::Arc`
195
248
 
196
249
  Class
@@ -225,6 +278,13 @@ Open Arc | Chord Arc | Pie Arc
225
278
  - `#contain?(x_or_point, y=nil)`: checks if point is inside
226
279
  - `#==(other)`: Returns `true` if equal to `other` or `false` otherwise
227
280
 
281
+ Example:
282
+
283
+ ```ruby
284
+ shape = PerfectShape::Arc.new(type: :chord, x: 2, y: 3, width: 50, height: 60, start: 30, extent: 90)
285
+ shape2 = PerfectShape::Arc.new(type: :chord, center_x: 2 + 25, center_y: 3 + 30, radius_x: 25, radius_y: 30, start: 30, extent: 90)
286
+ ```
287
+
228
288
  ### `PerfectShape::Ellipse`
229
289
 
230
290
  Class
@@ -253,6 +313,13 @@ Extends `PerfectShape::Arc`
253
313
  - `#contain?(x_or_point, y=nil)`: checks if point is inside
254
314
  - `#==(other)`: Returns `true` if equal to `other` or `false` otherwise
255
315
 
316
+ Example:
317
+
318
+ ```ruby
319
+ shape = PerfectShape::Ellipse.new(x: 2, y: 3, width: 50, height: 60)
320
+ shape2 = PerfectShape::Ellipse.new(center_x: 27, center_y: 33, radius_x: 25, radius_y: 30)
321
+ ```
322
+
256
323
  ### `PerfectShape::Circle`
257
324
 
258
325
  Class
@@ -283,6 +350,13 @@ Extends `PerfectShape::Ellipse`
283
350
  - `#contain?(x_or_point, y=nil)`: checks if point is inside
284
351
  - `#==(other)`: Returns `true` if equal to `other` or `false` otherwise
285
352
 
353
+ Example:
354
+
355
+ ```ruby
356
+ shape = PerfectShape::Circle.new(x: 2, y: 3, diameter: 60)
357
+ shape2 = PerfectShape::Circle.new(center_x: 2 + 30, center_y: 3 + 30, radius: 30)
358
+ ```
359
+
286
360
  ### `PerfectShape::Polygon`
287
361
 
288
362
  Class
@@ -291,9 +365,11 @@ Extends `PerfectShape::Shape`
291
365
 
292
366
  Includes `PerfectShape::MultiPoint`
293
367
 
368
+ A polygon can be thought of as a special case of [path](#perfectshapepath) that is closed, has the [Even-Odd](https://en.wikipedia.org/wiki/Even%E2%80%93odd_rule) winding rule, and consists of lines only.
369
+
294
370
  ![polygon](https://raw.githubusercontent.com/AndyObtiva/perfect-shape/master/images/polygon.png)
295
371
 
296
- - `::new(points: nil)`: constructs a polygon with `points` as `Array` of `Array`s of `[x,y]` pairs or flattened `Array` of alternating x and y values
372
+ - `::new(points: [])`: constructs a polygon with `points` as `Array` of `Array`s of `[x,y]` pairs or flattened `Array` of alternating x and y values
297
373
  - `#min_x`: min x
298
374
  - `#min_y`: min y
299
375
  - `#max_x`: max x
@@ -306,6 +382,12 @@ Includes `PerfectShape::MultiPoint`
306
382
  - `#contain?(x_or_point, y=nil)`: checks if point is inside using the [Ray Casting Algorithm](https://en.wikipedia.org/wiki/Point_in_polygon) (aka [Even-Odd Rule](https://en.wikipedia.org/wiki/Even%E2%80%93odd_rule))
307
383
  - `#==(other)`: Returns `true` if equal to `other` or `false` otherwise
308
384
 
385
+ Example:
386
+
387
+ ```ruby
388
+ shape = PerfectShape::Polygon.new(points: [[200, 150], [270, 170], [250, 220], [220, 190], [200, 200], [180, 170]])
389
+ ```
390
+
309
391
  ### `PerfectShape::Path`
310
392
 
311
393
  Class
@@ -316,8 +398,8 @@ Includes `PerfectShape::MultiPoint`
316
398
 
317
399
  ![path](https://raw.githubusercontent.com/AndyObtiva/perfect-shape/master/images/path.png)
318
400
 
319
- - `::new(shapes: nil, 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), or `PerfectShape::Line`. 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`.
320
- - `#shapes`: the shapes that the path is composed of
401
+ - `::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`, or `PerfectShape::QuadraticBezierCurve`. 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`.
402
+ - `#shapes`: the shapes that the path is composed of (must always start with `PerfectShape::Point` or Array of [x,y] coordinates representing start point)
321
403
  - `#closed?`: returns `true` if closed and `false` otherwise
322
404
  - `#winding_rule`: returns winding rule (`:wind_non_zero` or `:wind_even_odd`)
323
405
  - `#points`: path points calculated (derived) from shapes
@@ -329,11 +411,22 @@ Includes `PerfectShape::MultiPoint`
329
411
  - `#height`: height (from min y to max y)
330
412
  - `#center_x`: center x
331
413
  - `#center_y`: center y
332
- - `#bounding_box`: bounding box is a rectangle with x = min x, y = min y, and width/height of shape
414
+ - `#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)
333
415
  - `#contain?(x_or_point, y=nil)`: checks if point is inside path utilizing the configured winding rule, which can be the [Nonzero-Rule](https://en.wikipedia.org/wiki/Nonzero-rule) (aka [Winding Number Algorithm](https://en.wikipedia.org/wiki/Point_in_polygon#Winding_number_algorithm)) or the [Even-Odd Rule](https://en.wikipedia.org/wiki/Even%E2%80%93odd_rule) (aka [Ray Casting Algorithm](https://en.wikipedia.org/wiki/Point_in_polygon#Ray_casting_algorithm))
334
416
  - `#point_crossings(x_or_point, y=nil)`: calculates the number of times the given path crosses the ray extending to the right from (x,y)
335
417
  - `#==(other)`: Returns `true` if equal to `other` or `false` otherwise
336
418
 
419
+ Example:
420
+
421
+ ```ruby
422
+ path_shapes = []
423
+ path_shapes << PerfectShape::Point.new(x: 200, y: 150)
424
+ path_shapes << PerfectShape::Line.new(points: [250, 170]) # no need for start point, just end point
425
+ path_shapes << PerfectShape::QuadraticBezierCurve.new(points: [[300, 185], [350, 150]]) # no need for start point, just control point and end point
426
+
427
+ shape = PerfectShape::Path.new(shapes: path_shapes, closed: false, winding_rule: :wind_even_odd)
428
+ ```
429
+
337
430
  ## Process
338
431
 
339
432
  [Glimmer Process](https://github.com/AndyObtiva/glimmer/blob/master/PROCESS.md)
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.1.0
1
+ 0.1.1
@@ -189,14 +189,14 @@ module PerfectShape
189
189
  # +1 is returned for a crossing where the Y coordinate is increasing
190
190
  # -1 is returned for a crossing where the Y coordinate is decreasing
191
191
  def point_crossings(x1, y1, x2, y2, px, py)
192
- return 0 if (py < y1 && py < y2)
193
- return 0 if (py >= y1 && py >= y2)
192
+ return BigDecimal('0') if (py < y1 && py < y2)
193
+ return BigDecimal('0') if (py >= y1 && py >= y2)
194
194
  # assert(y1 != y2);
195
- return 0 if (px >= x1 && px >= x2)
195
+ return BigDecimal('0') if (px >= x1 && px >= x2)
196
196
  return ((y1 < y2) ? 1 : -1) if (px < x1 && px < x2)
197
197
  xintercept = x1 + (py - y1) * (x2 - x1) / (y2 - y1);
198
- return 0 if (px >= xintercept)
199
- (y1 < y2) ? 1 : -1
198
+ return BigDecimal('0') if (px >= xintercept)
199
+ (y1 < y2) ? BigDecimal('1') : BigDecimal('-1')
200
200
  end
201
201
  end
202
202
 
@@ -26,8 +26,8 @@ module PerfectShape
26
26
  module MultiPoint
27
27
  attr_reader :points
28
28
 
29
- def initialize(points: nil)
30
- self.points = points || []
29
+ def initialize(points: [])
30
+ self.points = points
31
31
  end
32
32
 
33
33
  # Sets points, normalizing to an Array of Arrays of (x,y) pairs as BigDecimal
@@ -22,6 +22,7 @@
22
22
  require 'perfect_shape/shape'
23
23
  require 'perfect_shape/point'
24
24
  require 'perfect_shape/line'
25
+ require 'perfect_shape/quadratic_bezier_curve'
25
26
  require 'perfect_shape/multi_point'
26
27
 
27
28
  module PerfectShape
@@ -47,20 +48,24 @@ module PerfectShape
47
48
  end
48
49
 
49
50
  def points
50
- @shapes.map do |shape|
51
+ the_points = []
52
+ @shapes.each do |shape|
51
53
  case shape
52
54
  when Point
53
- shape.to_a
55
+ the_points << shape.to_a
54
56
  when Array
55
- shape
57
+ the_points << shape.map {|n| BigDecimal(n.to_s)}
56
58
  when Line
57
- shape.points.last.to_a
58
- # when QuadraticBezierCurve # TODO
59
+ the_points << shape.points.last.to_a
60
+ when QuadraticBezierCurve
61
+ shape.points.each do |point|
62
+ the_points << point.to_a
63
+ end
59
64
  # when CubicBezierCurve # TODO
60
65
  end
61
- end.tap do |the_points|
62
- the_points << @shapes.first.to_a if closed?
63
66
  end
67
+ the_points << @shapes.first.to_a if closed?
68
+ the_points
64
69
  end
65
70
 
66
71
  def points=(some_points)
@@ -76,7 +81,8 @@ module PerfectShape
76
81
  :move_to
77
82
  when Line
78
83
  :line_to
79
- # when QuadraticBezierCurve # TODO
84
+ when QuadraticBezierCurve
85
+ :quad_to
80
86
  # when CubicBezierCurve # TODO
81
87
  end
82
88
  end
@@ -107,7 +113,7 @@ module PerfectShape
107
113
  # Here we know that both x and y are finite.
108
114
  return false if shapes.count < 2
109
115
  mask = winding_rule == :wind_non_zero ? -1 : 1
110
- (point_crossings(x, y) & mask) != 0
116
+ (point_crossings(x, y).to_i & mask) != 0
111
117
  else
112
118
  # Either x or y was infinite or NaN.
113
119
  # A NaN always produces a negative response to any test
@@ -131,13 +137,13 @@ module PerfectShape
131
137
  def point_crossings(x_or_point, y = nil)
132
138
  x, y = normalize_point(x_or_point, y)
133
139
  return unless x && y
134
- return 0 if shapes.count == 0
140
+ return BigDecimal('0') if shapes.count == 0
135
141
  movx = movy = curx = cury = endx = endy = 0
136
142
  coords = points.flatten
137
143
  curx = movx = coords[0]
138
144
  cury = movy = coords[1]
139
- crossings = 0
140
- ci = 2
145
+ crossings = BigDecimal('0')
146
+ ci = BigDecimal('2')
141
147
  1.upto(shapes.count - 1).each do |i|
142
148
  case drawing_types[i]
143
149
  when :move_to
@@ -158,17 +164,19 @@ module PerfectShape
158
164
  crossings += line.point_crossings(x, y)
159
165
  curx = endx;
160
166
  cury = endy;
161
- # when :quad_to # TODO
162
- # crossings +=
163
- # Curve.point_crossings_for_quad(x, y,
164
- # curx, cury,
165
- # coords[ci++],
166
- # coords[ci++],
167
- # endx = coords[ci++],
168
- # endy = coords[ci++],
169
- # 0);
170
- # curx = endx;
171
- # cury = endy;
167
+ when :quad_to
168
+ quad_ctrlx = coords[ci]
169
+ ci += 1
170
+ quad_ctrly = coords[ci]
171
+ ci += 1
172
+ endx = coords[ci]
173
+ ci += 1
174
+ endy = coords[ci]
175
+ ci += 1
176
+ quad = PerfectShape::QuadraticBezierCurve.new(points: [[curx, cury], [quad_ctrlx, quad_ctrly], [endx, endy]])
177
+ crossings += quad.point_crossings(x, y, 0)
178
+ curx = endx;
179
+ cury = endy;
172
180
  # when :cubic_to # TODO
173
181
  # crossings +=
174
182
  # Curve.point_crossings_for_cubic(x, y,
@@ -0,0 +1,182 @@
1
+ # Copyright (c) 2021 Andy Maleh
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining
4
+ # a copy of this software and associated documentation files (the
5
+ # "Software"), to deal in the Software without restriction, including
6
+ # without limitation the rights to use, copy, modify, merge, publish,
7
+ # distribute, sublicense, and/or sell copies of the Software, and to
8
+ # permit persons to whom the Software is furnished to do so, subject to
9
+ # the following conditions:
10
+ #
11
+ # The above copyright notice and this permission notice shall be
12
+ # included in all copies or substantial portions of the Software.
13
+ #
14
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21
+
22
+ require 'perfect_shape/shape'
23
+ require 'perfect_shape/multi_point'
24
+
25
+ module PerfectShape
26
+ # Mostly ported from java.awt.geom: https://docs.oracle.com/javase/8/docs/api/java/awt/geom/QuadCurve2D.html
27
+ class QuadraticBezierCurve < Shape
28
+ class << self
29
+ def point_crossings(x1, y1, xc, yc, x2, y2, px, py, level = 0)
30
+ return BigDecimal('0') if (py < y1 && py < yc && py < y2)
31
+ return BigDecimal('0') if (py >= y1 && py >= yc && py >= y2)
32
+ # Note y1 could equal y2...
33
+ return BigDecimal('0') if (px >= x1 && px >= xc && px >= x2)
34
+ if (px < x1 && px < xc && px < x2)
35
+ if (py >= y1)
36
+ return BigDecimal('1') if (py < y2)
37
+ else
38
+ # py < y1
39
+ return BigDecimal('-1') if (py >= y2)
40
+ end
41
+ # py outside of y11 range, and/or y1==y2
42
+ return BigDecimal('0')
43
+ end
44
+ # double precision only has 52 bits of mantissa
45
+ return PerfectShape::Line.point_crossings(x1, y1, x2, y2, px, py) if (level > 52)
46
+ x1c = (x1 + xc) / BigDecimal('2')
47
+ y1c = (y1 + yc) / BigDecimal('2')
48
+ xc1 = (xc + x2) / BigDecimal('2')
49
+ yc1 = (yc + y2) / BigDecimal('2')
50
+ xc = (x1c + xc1) / BigDecimal('2')
51
+ yc = (y1c + yc1) / BigDecimal('2')
52
+ # [xy]c are NaN if any of [xy]0c or [xy]c1 are NaN
53
+ # [xy]0c or [xy]c1 are NaN if any of [xy][0c1] are NaN
54
+ # These values are also NaN if opposing infinities are added
55
+ return BigDecimal('0') if (xc.nan? || yc.nan?)
56
+ point_crossings(x1, y1, x1c, y1c, xc, yc, px, py, level+1) +
57
+ point_crossings(xc, yc, xc1, yc1, x2, y2, px, py, level+1);
58
+ end
59
+ end
60
+
61
+ include MultiPoint
62
+ include Equalizer.new(:points)
63
+
64
+ # Checks if quadratic bézier curve contains point (two-number Array or x, y args)
65
+ #
66
+ # @param x The X coordinate of the point to test.
67
+ # @param y The Y coordinate of the point to test.
68
+ #
69
+ # @return {@code true} if the point lies within the bound of
70
+ # the quadratic bézier curve, {@code false} if the point lies outside of the
71
+ # quadratic bézier curve's bounds.
72
+ def contain?(x_or_point, y = nil, distance: 0)
73
+ x, y = normalize_point(x_or_point, y)
74
+ return unless x && y
75
+
76
+ x1 = points[0][0]
77
+ y1 = points[0][1]
78
+ xc = points[1][0]
79
+ yc = points[1][1]
80
+ x2 = points[2][0]
81
+ y2 = points[2][1]
82
+
83
+ # We have a convex shape bounded by quad curve Pc(t)
84
+ # and ine Pl(t).
85
+ #
86
+ # P1 = (x1, y1) - start point of curve
87
+ # P2 = (x2, y2) - end point of curve
88
+ # Pc = (xc, yc) - control point
89
+ #
90
+ # Pq(t) = P1*(1 - t)^2 + 2*Pc*t*(1 - t) + P2*t^2 =
91
+ # = (P1 - 2*Pc + P2)*t^2 + 2*(Pc - P1)*t + P1
92
+ # Pl(t) = P1*(1 - t) + P2*t
93
+ # t = [0:1]
94
+ #
95
+ # P = (x, y) - point of interest
96
+ #
97
+ # Let's look at second derivative of quad curve equation:
98
+ #
99
+ # Pq''(t) = 2 * (P1 - 2 * Pc + P2) = Pq''
100
+ # It's constant vector.
101
+ #
102
+ # Let's draw a line through P to be parallel to this
103
+ # vector and find the intersection of the quad curve
104
+ # and the line.
105
+ #
106
+ # Pq(t) is point of intersection if system of equations
107
+ # below has the solution.
108
+ #
109
+ # L(s) = P + Pq''*s == Pq(t)
110
+ # Pq''*s + (P - Pq(t)) == 0
111
+ #
112
+ # | xq''*s + (x - xq(t)) == 0
113
+ # | yq''*s + (y - yq(t)) == 0
114
+ #
115
+ # This system has the solution if rank of its matrix equals to 1.
116
+ # That is, determinant of the matrix should be zero.
117
+ #
118
+ # (y - yq(t))*xq'' == (x - xq(t))*yq''
119
+ #
120
+ # Let's solve this equation with 't' variable.
121
+ # Also let kx = x1 - 2*xc + x2
122
+ # ky = y1 - 2*yc + y2
123
+ #
124
+ # t0q = (1/2)*((x - x1)*ky - (y - y1)*kx) /
125
+ # ((xc - x1)*ky - (yc - y1)*kx)
126
+ #
127
+ # Let's do the same for our line Pl(t):
128
+ #
129
+ # t0l = ((x - x1)*ky - (y - y1)*kx) /
130
+ # ((x2 - x1)*ky - (y2 - y1)*kx)
131
+ #
132
+ # It's easy to check that t0q == t0l. This fact means
133
+ # we can compute t0 only one time.
134
+ #
135
+ # In case t0 < 0 or t0 > 1, we have an intersections outside
136
+ # of shape bounds. So, P is definitely out of shape.
137
+ #
138
+ # In case t0 is inside [0:1], we should calculate Pq(t0)
139
+ # and Pl(t0). We have three points for now, and all of them
140
+ # lie on one line. So, we just need to detect, is our point
141
+ # of interest between points of intersections or not.
142
+ #
143
+ # If the denominator in the t0q and t0l equations is
144
+ # zero, then the points must be collinear and so the
145
+ # curve is degenerate and encloses no area. Thus the
146
+ # result is false.
147
+ kx = x1 - 2 * xc + x2;
148
+ ky = y1 - 2 * yc + y2;
149
+ dx = x - x1;
150
+ dy = y - y1;
151
+ dxl = x2 - x1;
152
+ dyl = y2 - y1;
153
+
154
+ t0 = (dx * ky - dy * kx) / (dxl * ky - dyl * kx)
155
+ return false if (t0 < 0 || t0 > 1 || t0 != t0)
156
+
157
+ xb = kx * t0 * t0 + 2 * (xc - x1) * t0 + x1;
158
+ yb = ky * t0 * t0 + 2 * (yc - y1) * t0 + y1;
159
+ xl = dxl * t0 + x1;
160
+ yl = dyl * t0 + y1;
161
+
162
+ (x >= xb && x < xl) ||
163
+ (x >= xl && x < xb) ||
164
+ (y >= yb && y < yl) ||
165
+ (y >= yl && y < yb)
166
+ end
167
+
168
+ # Calculates the number of times the quad
169
+ # crosses the ray extending to the right from (x,y).
170
+ # If the point lies on a part of the curve,
171
+ # then no crossings are counted for that intersection.
172
+ # the level parameter should be 0 at the top-level call and will count
173
+ # up for each recursion level to prevent infinite recursion
174
+ # +1 is added for each crossing where the Y coordinate is increasing
175
+ # -1 is added for each crossing where the Y coordinate is decreasing
176
+ def point_crossings(x_or_point, y = nil, level = 0)
177
+ x, y = normalize_point(x_or_point, y)
178
+ return unless x && y
179
+ QuadraticBezierCurve.point_crossings(points[0][0], points[0][1], points[1][0], points[1][1], points[2][0], points[2][1], x, y, level)
180
+ end
181
+ end
182
+ 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.1.0 ruby lib
5
+ # stub: perfect-shape 0.1.1 ruby lib
6
6
 
7
7
  Gem::Specification.new do |s|
8
8
  s.name = "perfect-shape".freeze
9
- s.version = "0.1.0"
9
+ s.version = "0.1.1"
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
14
  s.date = "2021-12-22"
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, polyline, polyquad, polycubic, and paths containing lines, quadratic b\u00E9zier curves, and cubic b\u00E9zier 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
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 b\u00E9zier 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
16
16
  s.email = "andy.am@gmail.com".freeze
17
17
  s.extra_rdoc_files = [
18
18
  "CHANGELOG.md",
@@ -35,6 +35,7 @@ Gem::Specification.new do |s|
35
35
  "lib/perfect_shape/point.rb",
36
36
  "lib/perfect_shape/point_location.rb",
37
37
  "lib/perfect_shape/polygon.rb",
38
+ "lib/perfect_shape/quadratic_bezier_curve.rb",
38
39
  "lib/perfect_shape/rectangle.rb",
39
40
  "lib/perfect_shape/rectangular_shape.rb",
40
41
  "lib/perfect_shape/shape.rb",
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: perfect-shape
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andy Maleh
@@ -97,11 +97,10 @@ dependencies:
97
97
  description: Perfect Shape is a collection of pure Ruby geometric algorithms that
98
98
  are mostly useful for GUI manipulation like checking containment of a mouse click
99
99
  point in popular geometry shapes such as rectangle, square, arc (open, chord, and
100
- pie), ellipse, circle, polygon, polyline, polyquad, polycubic, and paths containing
101
- lines, quadratic bézier curves, and cubic bézier curves (including both Ray Casting
102
- Algorithm, aka Even-odd Rule, and Winding Number Algorithm, aka Nonzero Rule). Additionally,
103
- it contains some purely mathematical algorithms like IEEEremainder (also known as
104
- IEEE-754 remainder).
100
+ pie), ellipse, circle, polygon, and paths containing lines, quadratic bézier curves,
101
+ and cubic bézier 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).
105
104
  email: andy.am@gmail.com
106
105
  executables: []
107
106
  extensions: []
@@ -125,6 +124,7 @@ files:
125
124
  - lib/perfect_shape/point.rb
126
125
  - lib/perfect_shape/point_location.rb
127
126
  - lib/perfect_shape/polygon.rb
127
+ - lib/perfect_shape/quadratic_bezier_curve.rb
128
128
  - lib/perfect_shape/rectangle.rb
129
129
  - lib/perfect_shape/rectangular_shape.rb
130
130
  - lib/perfect_shape/shape.rb