perfect-shape 0.3.5 → 0.5.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ab1f48d0551ed07a6a2b5f2d51abba6c6596eef82e156e68df57489319104e4c
4
- data.tar.gz: bb67e137bbf23df2ed291a574ef45ae97113dd140d0a17269c9b60c27b7261e9
3
+ metadata.gz: cf595115a6a9344cedfce9e93801cb5aba1960f4e970d064043e5f813716a0be
4
+ data.tar.gz: bcb60359b840662739767882546e360ceb052e734f5d3d7836406e25ff7b72ae
5
5
  SHA512:
6
- metadata.gz: 6d365e47635c7daa094f02849df24696784b667c55bffa479bdbaacc41abf1b9ad1114084a4ed7bd0145e6dd18ad233f9b09e6bad8792828417aca49db2076c4
7
- data.tar.gz: d8440bb0d35e1539dcf5c8cad79e2930d74e10e32a408391085027d773c0ea7e4272a6011a7244677c035c8f6a7954606065611282ae8cd14aab71cbbe6b2bba
6
+ metadata.gz: c57fb7619dd930974af23d8004ad134bc3fefb51c6dd03f5ae2e7a46cccae3011d7dd74ca9a08e8055c41ad93c9a4ef92132f76d7349fd5cc20ffefc5c7d371c
7
+ data.tar.gz: ca7c2586198a35ae8e9dc1485a87298c11b4151e11aab16e54302775c4539ac94c7867a3b48598914dfb8367b87fa6ae166cdea5dbe9da32ee0161788692b6d2
data/CHANGELOG.md CHANGED
@@ -1,5 +1,36 @@
1
1
  # Change Log
2
2
 
3
+ ## 0.5.2
4
+
5
+ - `QuadraticBezierCurve#intersect?(rectangle)`
6
+
7
+ ## 0.5.1
8
+
9
+ - `Point#intersect?(rectangle)` (equivalent to `Rectangle#contain?(point)`)
10
+
11
+ ## 0.5.0
12
+
13
+ - `Line#intersect?(rectangle)`
14
+ - `Rectangle#out_state(x_or_point, y = nil)`
15
+
16
+ ## 0.4.0
17
+
18
+ - `PerfectShape::AffineTransform#new`
19
+ - `PerfectShape::AffineTransform#==`
20
+ - `PerfectShape::AffineTransform#transform_point`
21
+ - `PerfectShape::AffineTransform#transform_points`
22
+ - `PerfectShape::AffineTransform#identity!` (alias: `reset!`)
23
+ - `PerfectShape::AffineTransform#invert!`
24
+ - `PerfectShape::AffineTransform#invertible?`
25
+ - `PerfectShape::AffineTransform#multiply!`
26
+ - `PerfectShape::AffineTransform#translate!`
27
+ - `PerfectShape::AffineTransform#scale!`
28
+ - `PerfectShape::AffineTransform#rotate!`
29
+ - `PerfectShape::AffineTransform#shear!` (alias: `skew!`)
30
+ - `PerfectShape::AffineTransform#clone`
31
+ - `PerfectShape::AffineTransform#inverse_transform_point`
32
+ - `PerfectShape::AffineTransform#inverse_transform_points`
33
+
3
34
  ## 0.3.5
4
35
 
5
36
  - Check point containment in composite shape outline with distance tolerance (new method signature: `PerfectShape::CompositeShape#contain?(x_or_point, y = nil, outline: false, distance_tolerance: 0)`)
data/README.md CHANGED
@@ -1,9 +1,9 @@
1
- # Perfect Shape 0.3.5
1
+ # Perfect Shape 0.5.2
2
2
  ## Geometric Algorithms
3
3
  [![Gem Version](https://badge.fury.io/rb/perfect-shape.svg)](http://badge.fury.io/rb/perfect-shape)
4
4
  [![Test](https://github.com/AndyObtiva/perfect-shape/actions/workflows/ruby.yml/badge.svg)](https://github.com/AndyObtiva/perfect-shape/actions/workflows/ruby.yml)
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), and [paths](#perfectshapepath) containing [lines](#perfectshapeline), [quadratic bézier curves](#perfectshapequadraticbeziercurve), and [cubic bezier curves](#perfectshapecubicbeziercurve) (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 [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.3.5
17
+ gem install perfect-shape -v 0.5.2
18
18
  ```
19
19
 
20
20
  Or include in Bundler `Gemfile`:
21
21
 
22
22
  ```ruby
23
- gem 'perfect-shape', '~> 0.3.5'
23
+ gem 'perfect-shape', '~> 0.5.2'
24
24
  ```
25
25
 
26
26
  And, run:
@@ -57,7 +57,6 @@ This is a base class for all shapes. It is not meant to be used directly. Subcla
57
57
  - `#center_y`: center y
58
58
  - `#bounding_box`: bounding box is a rectangle with x = min x, y = min y, and width/height just as those of shape
59
59
  - `#==(other)`: Returns `true` if equal to `other` or `false` otherwise
60
- - `#normalize_point(x_or_point, y = nil)`: normalizes point into an `Array` of `[x,y]` coordinates
61
60
  - `#contain?(x_or_point, y=nil, outline: false, distance_tolerance: 0)`: checks if point is inside if `outline` is `false` or if point is on the outline if `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 shape from its outline more successfully
62
61
 
63
62
  ### `PerfectShape::PointLocation`
@@ -86,6 +85,71 @@ Includes `PerfectShape::PointLocation`
86
85
  - `#max_x`: max x
87
86
  - `#max_y`: max y
88
87
 
88
+ ### `PerfectShape::AffineTransform`
89
+
90
+ Class
91
+
92
+ Affine transforms have the following matrix:
93
+
94
+ [ xxp xyp xt ]<br>
95
+ [ yxp yyp yt ]
96
+
97
+ The matrix is used to transform (x,y) point coordinates as follows:
98
+
99
+ [ xxp xyp xt ] * [x] = [ xxp * x + xyp * y + xt ]<br>
100
+ [ yxp yyp yt ] * [y] = [ yxp * x + yyp * y + yt ]
101
+
102
+ `xxp` is the x coordinate x product (`m11`)<br>
103
+ `xyp` is the x coordinate y product (`m12`)<br>
104
+ `yxp` is the y coordinate x product (`m21`)<br>
105
+ `yyp` is the y coordinate y product (`m22`)<br>
106
+ `xt` is the x coordinate translation (`m13`)<br>
107
+ `yt` is the y coordinate translation (`m23`)
108
+
109
+ Affine transform mutation operations ending with `!` can be chained as they all return `self`.
110
+
111
+ - `::new(xxp_element = nil, xyp_element = nil, yxp_element = nil, yyp_element = nil, xt_element = nil, yt_element = nil,
112
+ xxp: nil, xyp: nil, yxp: nil, yyp: nil, xt: nil, yt: nil,
113
+ m11: nil, m12: nil, m21: nil, m22: nil, m13: nil, m23: nil)`:
114
+ The constructor accepts either the (x,y)-operation related argument/kwarg names or traditional matrix element kwarg names. If no arguments are supplied, it constructs an identity matrix (i.e. like calling `::new(xxp: 1, xyp: 0, yxp: 0, yyp: 1, xt: 0, yt: 0)`).
115
+ - `#matrix_3d`: Returns Ruby `Matrix` object representing affine transform in 3D (used internally for performing multiplication)
116
+ - `#==(other)`: Returns `true` if equal to `other` or `false` otherwise
117
+ - `#identity!` (alias: `reset!`): Resets to identity matrix (i.e. like calling `::new(xxp: 1, xyp: 0, yxp: 0, yyp: 1, xt: 0, yt: 0)`)
118
+ - `#invertible?` Returns `true` if matrix is invertible and `false` otherwise
119
+ - `#invert!`: Inverts affine transform matrix if invertible or raises an error otherwise
120
+ - `#multiply!(other)`: Multiplies affine transform with another affine transform, storing resulting changes in matrix elements
121
+ - `#translate!(x_or_point, y=nil)`: Translates affine transform with (x, y) translation values
122
+ - `#scale!(x_or_point, y=nil)`: Scales affine transform with (x, y) scale values
123
+ - `#rotate!(degrees)`: Rotates by angle degrees counter-clockwise if angle value is positive or clockwise if angle value is negative. Note that it returns very close approximate results for rotations that are 90/180/270 degrees (good enough for inverse-transform GUI point containment checks needed when checking if mouse-click-point is inside a transformed shape).
124
+ - `#shear!(x_or_point, y=nil)`: Shears by x and y factors
125
+ - `#clone`: Returns a new AffineTransform with the same matrix elements
126
+ - `#transform_point(x_or_point, y=nil)`: returns `[xxp * x + xyp * y + xt, yxp * x + yyp * y + yt]`. Note that result is a close approximation, but should be good enough for GUI mouse-click-point containment checks.
127
+ - `#transform_points(*xy_coordinates_or_points)`: returns `Array` of (x,y) pair `Array`s transformed with `#transform_point` method
128
+ - `#inverse_transform_point(x_or_point, y=nil)`: returns inverse transform of a point (x,y) coordinates (clones self and inverts clone, and then transforms point). Note that result is a close approximation, but should be good enough for GUI mouse-click-point containment checks.
129
+ - `#inverse_transform_points(*xy_coordinates_or_points)`: returns inverse transforms of a point `Array` of (x,y) coordinates
130
+
131
+ Example:
132
+
133
+ ```ruby
134
+ xxp = 2
135
+ xyp = 3
136
+ yxp = 4
137
+ yyp = 5
138
+ xt = 6
139
+ yt = 7
140
+ affine_transform1 = PerfectShape::AffineTransform.new(xxp: xxp, xyp: xyp, yxp: yxp, yyp: yyp, xt: xt, yt: yt) # (x,y)-operation kwarg names
141
+ affine_transform2 = PerfectShape::AffineTransform.new(m11: xxp, m12: xyp, m21: yxp, m22: yyp, m13: xt, m23: yt) # traditional matrix element kwarg names
142
+ affine_transform3 = PerfectShape::AffineTransform.new(xxp, xyp, yxp, yyp, xt, yt) # standard arguments
143
+
144
+ affine_transform2.matrix_3d == affine_transform1.matrix_3d # => true
145
+ affine_transform3.matrix_3d == affine_transform1.matrix_3d # => true
146
+
147
+ affine_transform = PerfectShape::AffineTransform.new.translate!(30, 20).scale!(2, 3)
148
+
149
+ affine_transform.transform_point(10, 10) # => approximately [50, 50]
150
+ affine_transform.inverse_transform_point(50, 50) # => approximately [10, 10]
151
+ ```
152
+
89
153
  ### `PerfectShape::Point`
90
154
 
91
155
  Class
@@ -99,6 +163,7 @@ Includes `PerfectShape::PointLocation`
99
163
  Points are simply represented by an `Array` of `[x,y]` coordinates when used within other shapes, but when needing point-specific operations like `point_distance`, the `PerfectShape::Point` class can come in handy.
100
164
 
101
165
  - `::point_distance(x, y, px, py)`: Returns the distance from a point to another point
166
+ - `::normalize_point(x_or_point, y = nil)`: Normalizes point args whether two-number `point` `Array` or `x`, `y` args, returning normalized point `Array` of two `BigDecimal`'s
102
167
  - `::new(x_or_point=nil, y_arg=nil, x: nil, y: nil)`: constructs a point with (x,y) pair (default: 0,0) whether specified as `Array` of (x,y) pair, flat `x,y` args, or `x:, y:` kwargs.
103
168
  - `#min_x`: min x (always x)
104
169
  - `#min_y`: min y (always y)
@@ -112,6 +177,7 @@ Points are simply represented by an `Array` of `[x,y]` coordinates when used wit
112
177
  - `#bounding_box`: bounding box is a rectangle with x = min x, y = min y, and width/height of shape
113
178
  - `#==(other)`: Returns `true` if equal to `other` or `false` otherwise
114
179
  - `#contain?(x_or_point, y=nil, outline: true, distance_tolerance: 0)`: checks if point matches self, with a distance tolerance (0 by default). Distance tolerance provides a fuzz factor that for example enables GUI users to mouse-click-select a point shape more successfully. `outline` option makes no difference on point
180
+ - `#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
115
181
  - `#point_distance(x_or_point, y=nil)`: Returns the distance from a point to another point
116
182
 
117
183
  Example:
@@ -139,7 +205,7 @@ Includes `PerfectShape::MultiPoint`
139
205
 
140
206
  ![line](https://raw.githubusercontent.com/AndyObtiva/perfect-shape/master/images/line.png)
141
207
 
142
- - `::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)”.
208
+ - `::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, 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)”.
143
209
  - `::point_distance_square(x1, y1, x2, y2, px, py)`: Returns the square of distance from a point to a line segment.
144
210
  - `::point_distance(x1, y1, x2, y2, px, py)`: Returns the distance from a point to a line segment.
145
211
  - `::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 coordinates
@@ -155,7 +221,8 @@ Includes `PerfectShape::MultiPoint`
155
221
  - `#bounding_box`: bounding box is a rectangle with x = min x, y = min y, and width/height of shape
156
222
  - `#==(other)`: Returns `true` if equal to `other` or `false` otherwise
157
223
  - `#contain?(x_or_point, y=nil, outline: true, distance_tolerance: 0)`: checks if point lies on line, with a distance tolerance (0 by default). Distance tolerance provides a fuzz factor that for example enables GUI users to mouse-click-select a line shape more successfully. `outline` option makes no difference on line
158
- - `#relative_counterclockwise(x_or_point, y=nil)`: 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)”.
224
+ - `#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
225
+ - `#relative_counterclockwise(x_or_point, y=nil)`: 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, 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)”.
159
226
  - `#point_distance(x_or_point, y=nil)`: Returns the distance from a point to a line segment.
160
227
 
161
228
  Example:
@@ -183,6 +250,10 @@ Includes `PerfectShape::MultiPoint`
183
250
 
184
251
  ![quadratic_bezier_curve](https://raw.githubusercontent.com/AndyObtiva/perfect-shape/master/images/quadratic_bezier_curve.png)
185
252
 
253
+ - `::tag(coord, low, high)`: Determine where coord lies with respect to the range from low to high. It is assumed that low < high. The return value is one of the 5 values BELOW, LOWEDGE, INSIDE, HIGHEDGE, or ABOVE.
254
+ - `::eqn(val, c1, cp, c2)`: Fill an array with the coefficients of the parametric equation in t, ready for solving against val with solve_quadratic. We currently have: val = Py(t) = C1*(1-t)^2 + 2*CP*t*(1-t) + C2*t^2 = C1 - 2*C1*t + C1*t^2 + 2*CP*t - 2*CP*t^2 + C2*t^2 = C1 + (2*CP - 2*C1)*t + (C1 - 2*CP + C2)*t^2; 0 = (C1 - val) + (2*CP - 2*C1)*t + (C1 - 2*CP + C2)*t^2; 0 = C + Bt + At^2; C = C1 - val; B = 2*CP - 2*C1; A = C1 - 2*CP + C2
255
+ - `::solve_quadratic(eqn)`: Solves the quadratic whose coefficients are in the eqn array and places the non-complex roots into the res array, returning the number of roots. The quadratic solved is represented by the equation: <pre>eqn = {C, B, A}; ax^2 + bx + c = 0</pre> A return value of {@code -1} is used to distinguish a constant equation, which might be always 0 or never 0, from an equation that has no zeroes.
256
+ - `::eval_quadratic(vals, num, include0, include1, inflect, c1, ctrl, c2)`: Evaluate the t values in the first num slots of the vals[] array and place the evaluated values back into the same array. Only evaluate t values that are within the range <, >, including the 0 and 1 ends of the range iff the include0 or include1 booleans are true. If an "inflection" equation is handed in, then any points which represent a point of inflection for that quadratic equation are also ignored.
186
257
  - `::new(points: [])`: constructs a quadratic bézier curve with three `points` (start point, control point, and end point) as `Array` of `Array`s of `[x,y]` pairs or flattened `Array` of alternating x and y coordinates
187
258
  - `#points`: points (start point, control point, and end point)
188
259
  - `#min_x`: min x
@@ -197,6 +268,7 @@ Includes `PerfectShape::MultiPoint`
197
268
  - `#bounding_box`: bounding box is a rectangle with x = min x, y = min y, and width/height of shape (bounding box only guarantees that the shape is within it, but it might be bigger than the shape)
198
269
  - `#==(other)`: Returns `true` if equal to `other` or `false` otherwise
199
270
  - `#contain?(x_or_point, y=nil, outline: false, distance_tolerance: 0)`: checks if point is inside when `outline` is `false` or if point is on the outline when `outline` is `true`. `distance_tolerance` can be used as a fuzz factor when `outline` is `true`, for example, to help GUI users mouse-click-select a quadratic bezier curve shape from its outline more successfully
271
+ - `#intersect?(rectangle)`: Returns `true` if intersecting with interior of rectangle or `false` otherwise. This is useful for GUI optimization checks of whether a shape appears in a GUI viewport rectangle and needs redrawing
200
272
  - `#curve_center_point`: point at the center of the curve outline (not the center of the bounding box area like `center_x` and `center_y`)
201
273
  - `#curve_center_x`: point x coordinate at the center of the curve outline (not the center of the bounding box area like `center_x` and `center_y`)
202
274
  - `#curve_center_y`: point y coordinate at the center of the curve outline (not the center of the bounding box area like `center_x` and `center_y`)
@@ -297,6 +369,7 @@ Includes `PerfectShape::RectangularShape`
297
369
  - `#==(other)`: Returns `true` if equal to `other` or `false` otherwise
298
370
  - `#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 rectangle shape from its outline more successfully
299
371
  - `#edges`: edges of rectangle as `PerfectShape::Line` objects
372
+ - `#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.
300
373
 
301
374
  Example:
302
375
 
@@ -735,7 +808,7 @@ A composite shape is simply an aggregate of multiple shapes (e.g. square and pol
735
808
  - `#center_y`: center y
736
809
  - `#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)
737
810
  - `#==(other)`: Returns `true` if equal to `other` or `false` otherwise
738
- - `#contain?(x_or_point, y=nil)`: When `outline` is `false`, it checks if point is inside any of the shapes owned by the composite shape. Otherwise, when `outline` is `true`, it checks if point is on the outline of any of the shapes owned by the composite shape. `distance_tolerance` can be used as a fuzz factor when `outline` is `true`, for example, to help GUI users mouse-click-select a composite shape from its outline more successfully
811
+ - `#contain?(x_or_point, y=nil, outline: false, distance_tolerance: 0)`: When `outline` is `false`, it checks if point is inside any of the shapes owned by the composite shape. Otherwise, when `outline` is `true`, it checks if point is on the outline of any of the shapes owned by the composite shape. `distance_tolerance` can be used as a fuzz factor when `outline` is `true`, for example, to help GUI users mouse-click-select a composite shape from its outline more successfully
739
812
 
740
813
  Example:
741
814
 
@@ -778,7 +851,10 @@ shape.contain?([145, 189], outline: true, distance_tolerance: 1) # => true
778
851
  ## Resources
779
852
 
780
853
  - Rubydoc: https://www.rubydoc.info/gems/perfect-shape
781
- - AWT Geom Javadoc (inspiration): https://docs.oracle.com/javase/8/docs/api/java/awt/geom/package-summary.html
854
+ - Point in Polygon: https://en.wikipedia.org/wiki/Point_in_polygon
855
+ - Even-Odd Rule: https://en.wikipedia.org/wiki/Even%E2%80%93odd_rule
856
+ - Nonzero Rule: https://en.wikipedia.org/wiki/Nonzero-rule
857
+ - IEEE 754-1985 Remainder: https://en.wikipedia.org/wiki/IEEE_754-1985
782
858
 
783
859
  ## TODO
784
860
 
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.3.5
1
+ 0.5.2
data/lib/perfect-shape.rb CHANGED
@@ -23,8 +23,8 @@ $LOAD_PATH.unshift File.expand_path('.', __dir__)
23
23
 
24
24
  require 'bigdecimal'
25
25
  require 'equalizer'
26
+ require 'matrix'
26
27
 
27
- # Perfect Shape algorithms are mostly ported from java.awt.geom: https://docs.oracle.com/javase/8/docs/api/java/awt/geom/package-summary.html
28
28
  module PerfectShape
29
29
  end
30
30
 
@@ -0,0 +1,235 @@
1
+ # Copyright (c) 2021-2022 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/math'
23
+ require 'perfect_shape/shape'
24
+ require 'perfect_shape/point'
25
+ require 'perfect_shape/multi_point'
26
+
27
+ module PerfectShape
28
+ # Represents an affine transform
29
+ class AffineTransform
30
+ include Equalizer.new(:xxp, :xyp, :yxp, :yyp, :xt, :yt)
31
+
32
+ attr_reader :xxp, :xyp, :yxp, :yyp, :xt, :yt
33
+ alias m11 xxp
34
+ alias m12 xyp
35
+ alias m21 yxp
36
+ alias m22 yyp
37
+ alias m13 xt
38
+ alias m23 yt
39
+
40
+ # Creates a new AffineTransform with the following Matrix:
41
+ #
42
+ # [ xxp xyp xt ]
43
+ # [ yxp yyp yt ]
44
+ #
45
+ # The Matrix is used to transform (x,y) point coordinates as follows:
46
+ #
47
+ # [ xxp xyp xt ] * [x] = [ xxp * x + xyp * y + xt ]
48
+ # [ yxp yyp yt ] * [y] = [ yxp * x + yyp * y + yt ]
49
+ #
50
+ # xxp is the x coordinate x product (m11)
51
+ # xyp is the x coordinate y product (m12)
52
+ # yxp is the y coordinate x product (m21)
53
+ # yyp is the y coordinate y product (m22)
54
+ # xt is the x coordinate translation (m13)
55
+ # yt is the y coordinate translation (m23)
56
+ #
57
+ # The constructor accepts either the (x,y)-operation related argument/kwarg names
58
+ # or traditional Matrix element kwarg names
59
+ #
60
+ # Example with (x,y)-operation kwarg names:
61
+ #
62
+ # AffineTransform.new(xxp: 2, xyp: 3, yxp: 4, yyp: 5, xt: 6, yt: 7)
63
+ #
64
+ # Example with traditional Matrix element kwarg names:
65
+ #
66
+ # AffineTransform.new(m11: 2, m12: 3, m21: 4, m22: 5, m13: 6, m23: 7)
67
+ #
68
+ # Example with standard arguments:
69
+ #
70
+ # AffineTransform.new(2, 3, 4, 5, 6, 7)
71
+ #
72
+ # If no arguments are supplied, it constructs an identity matrix
73
+ # (i.e. like calling `::new(xxp: 1, xyp: 0, yxp: 0, yyp: 1, xt: 0, yt: 0)`)
74
+ def initialize(xxp_element = nil, xyp_element = nil, yxp_element = nil, yyp_element = nil, xt_element = nil, yt_element = nil,
75
+ xxp: nil, xyp: nil, yxp: nil, yyp: nil, xt: nil, yt: nil,
76
+ m11: nil, m12: nil, m21: nil, m22: nil, m13: nil, m23: nil)
77
+ self.xxp = xxp_element || xxp || m11 || 1
78
+ self.xyp = xyp_element || xyp || m12 || 0
79
+ self.yxp = yxp_element || yxp || m21 || 0
80
+ self.yyp = yyp_element || yyp || m22 || 1
81
+ self.xt = xt_element || xt || m13 || 0
82
+ self.yt = yt_element || yt || m23 || 0
83
+ end
84
+
85
+ def xxp=(value)
86
+ @xxp = BigDecimal(value.to_s)
87
+ end
88
+ alias m11= xxp=
89
+
90
+ def xyp=(value)
91
+ @xyp = BigDecimal(value.to_s)
92
+ end
93
+ alias m12= xyp=
94
+
95
+ def yxp=(value)
96
+ @yxp = BigDecimal(value.to_s)
97
+ end
98
+ alias m21= yxp=
99
+
100
+ def yyp=(value)
101
+ @yyp = BigDecimal(value.to_s)
102
+ end
103
+ alias m22= yyp=
104
+
105
+ def xt=(value)
106
+ @xt = BigDecimal(value.to_s)
107
+ end
108
+ alias m13= xt=
109
+
110
+ def yt=(value)
111
+ @yt = BigDecimal(value.to_s)
112
+ end
113
+ alias m23= yt=
114
+
115
+ # Resets to identity matrix
116
+ # Returns self to support fluent interface chaining
117
+ def identity!
118
+ self.xxp = 1
119
+ self.xyp = 0
120
+ self.yxp = 0
121
+ self.yyp = 1
122
+ self.xt = 0
123
+ self.yt = 0
124
+
125
+ self
126
+ end
127
+ alias reset! identity!
128
+
129
+ # Inverts AffineTransform matrix if invertible
130
+ # Raises an error if affine transform matrix is not invertible
131
+ # Returns self to support fluent interface chaining
132
+ def invert!
133
+ raise 'Cannot invert (matrix is not invertible)!' if !invertible?
134
+
135
+ self.matrix_3d = matrix_3d.inverse
136
+
137
+ self
138
+ end
139
+
140
+ def invertible?
141
+ (m11 * m22 - m12 * m21) != 0
142
+ end
143
+
144
+ # Multiplies by other AffineTransform
145
+ def multiply!(other_affine_transform)
146
+ self.matrix_3d = matrix_3d*other_affine_transform.matrix_3d
147
+
148
+ self
149
+ end
150
+
151
+ # Translates AffineTransform
152
+ def translate!(x_or_point, y = nil)
153
+ x, y = Point.normalize_point(x_or_point, y)
154
+ return unless x && y
155
+
156
+ translation_affine_transform = AffineTransform.new(xxp: 1, xyp: 0, yxp: 0, yyp: 1, xt: x, yt: y)
157
+ multiply!(translation_affine_transform)
158
+
159
+ self
160
+ end
161
+
162
+ # Scales AffineTransform
163
+ def scale!(x_or_point, y = nil)
164
+ x, y = Point.normalize_point(x_or_point, y)
165
+ return unless x && y
166
+
167
+ scale_affine_transform = AffineTransform.new(xxp: x, xyp: 0, yxp: 0, yyp: y, xt: 0, yt: 0)
168
+ multiply!(scale_affine_transform)
169
+
170
+ self
171
+ end
172
+
173
+ # Rotates AffineTransform counter-clockwise for positive angle value in degrees
174
+ # or clockwise for negative angle value in degrees
175
+ def rotate!(degrees)
176
+ degrees = Math.normalize_degrees(degrees)
177
+ radians = Math.degrees_to_radians(degrees)
178
+
179
+ rotation_affine_transform = AffineTransform.new(xxp: Math.cos(radians), xyp: -Math.sin(radians), yxp: Math.sin(radians), yyp: Math.cos(radians), xt: 0, yt: 0)
180
+ multiply!(rotation_affine_transform)
181
+
182
+ self
183
+ end
184
+
185
+ # Shears AffineTransform by (x,y) amount
186
+ def shear!(x_or_point, y = nil)
187
+ x, y = Point.normalize_point(x_or_point, y)
188
+ return unless x && y
189
+
190
+ shear_affine_transform = AffineTransform.new(xxp: 1 + x*y, xyp: x, yxp: y, yyp: 1, xt: 0, yt: 0)
191
+ multiply!(shear_affine_transform)
192
+
193
+ self
194
+ end
195
+ alias skew! shear!
196
+
197
+ # Sets elements from a Ruby Matrix representing Affine Transform matrix elements in 3D
198
+ def matrix_3d=(the_matrix_3d)
199
+ self.xxp = the_matrix_3d[0, 0]
200
+ self.xyp = the_matrix_3d[0, 1]
201
+ self.xt = the_matrix_3d[0, 2]
202
+ self.yxp = the_matrix_3d[1, 0]
203
+ self.yyp = the_matrix_3d[1, 1]
204
+ self.yt = the_matrix_3d[1, 2]
205
+ end
206
+
207
+ # Returns Ruby Matrix representing Affine Transform matrix elements in 3D
208
+ def matrix_3d
209
+ Matrix[[xxp, xyp, xt], [yxp, yyp, yt], [0, 0, 1]]
210
+ end
211
+
212
+ def transform_point(x_or_point, y = nil)
213
+ x, y = Point.normalize_point(x_or_point, y)
214
+ return unless x && y
215
+
216
+ [xxp*x + xyp*y + xt, yxp*x + yyp*y + yt]
217
+ end
218
+
219
+ def inverse_transform_point(x_or_point, y = nil)
220
+ clone.invert!.transform_point(x_or_point, y)
221
+ end
222
+
223
+ def transform_points(*xy_coordinates_or_points)
224
+ points = xy_coordinates_or_points.first.is_a?(Array) ? xy_coordinates_or_points.first : xy_coordinates_or_points
225
+ points = MultiPoint.normalize_point_array(points)
226
+ points.map { |point| transform_point(point) }
227
+ end
228
+
229
+ def inverse_transform_points(*xy_coordinates_or_points)
230
+ points = xy_coordinates_or_points.first.is_a?(Array) ? xy_coordinates_or_points.first : xy_coordinates_or_points
231
+ points = MultiPoint.normalize_point_array(points)
232
+ points.map { |point| inverse_transform_point(point) }
233
+ end
234
+ end
235
+ end
@@ -21,10 +21,10 @@
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/Arc2D.html
28
28
  class Arc < Shape
29
29
  include RectangularShape
30
30
  include Equalizer.new(:type, :x, :y, :width, :height, :start, :extent)
@@ -145,7 +145,7 @@ module PerfectShape
145
145
  # the arc, {@code false} if the point lies outside of the
146
146
  # arc's bounds.
147
147
  def contain?(x_or_point, y = nil, outline: false, distance_tolerance: 0)
148
- x, y = normalize_point(x_or_point, y)
148
+ x, y = Point.normalize_point(x_or_point, y)
149
149
  return unless x && y
150
150
  if outline
151
151
  if type == :pie && x == center_x && y == center_y
@@ -64,7 +64,7 @@ module PerfectShape
64
64
  # the composite shape or false if the point lies outside of the
65
65
  # path's bounds.
66
66
  def contain?(x_or_point, y = nil, outline: false, distance_tolerance: 0)
67
- x, y = normalize_point(x_or_point, y)
67
+ x, y = Point.normalize_point(x_or_point, y)
68
68
  return unless x && y
69
69
 
70
70
  shapes.any? { |shape| shape.contain?(x, y, outline: outline, 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 CubicBezierCurve < Shape
28
28
  class << self
29
29
  # Calculates the number of times the cubic bézier curve from (x1,y1) to (x2,y2)
@@ -86,7 +86,7 @@ module PerfectShape
86
86
  # the cubic bézier curve, {@code false} if the point lies outside of the
87
87
  # cubic bézier curve's bounds.
88
88
  def contain?(x_or_point, y = nil, outline: false, distance_tolerance: 0)
89
- x, y = normalize_point(x_or_point, y)
89
+ x, y = Point.normalize_point(x_or_point, y)
90
90
  return unless x && y
91
91
 
92
92
  if outline
@@ -120,7 +120,7 @@ module PerfectShape
120
120
  # +1 is added for each crossing where the Y coordinate is increasing
121
121
  # -1 is added for each crossing where the Y coordinate is decreasing
122
122
  def point_crossings(x_or_point, y = nil, level = 0)
123
- x, y = normalize_point(x_or_point, y)
123
+ x, y = Point.normalize_point(x_or_point, y)
124
124
  return unless x && y
125
125
  CubicBezierCurve.point_crossings(points[0][0], points[0][1], points[1][0], points[1][1], points[2][0], points[2][1], points[3][0], points[3][1], x, y, level)
126
126
  end
@@ -183,7 +183,7 @@ module PerfectShape
183
183
  end
184
184
 
185
185
  def point_distance(x_or_point, y = nil, minimum_distance_threshold: OUTLINE_MINIMUM_DISTANCE_THRESHOLD)
186
- x, y = normalize_point(x_or_point, y)
186
+ x, y = Point.normalize_point(x_or_point, y)
187
187
  return unless x && y
188
188
 
189
189
  point = Point.new(x, y)
@@ -22,7 +22,6 @@
22
22
  require 'perfect_shape/arc'
23
23
 
24
24
  module PerfectShape
25
- # Mostly ported from java.awt.geom: https://docs.oracle.com/javase/8/docs/api/java/awt/geom/Ellipse2D.html
26
25
  class Ellipse < Arc
27
26
  MESSAGE_CANNOT_UPDATE_ATTRIUBTE = "Ellipse %s cannot be updated. If you want to update type, use Arc instead!"
28
27
 
@@ -65,7 +64,7 @@ module PerfectShape
65
64
  # ellipse's bounds.
66
65
  def contain?(x_or_point, y = nil, outline: false, distance_tolerance: 0)
67
66
  # This is implemented again even though super would have just worked to have an optimized algorithm for Ellipse.
68
- x, y = normalize_point(x_or_point, y)
67
+ x, y = Point.normalize_point(x_or_point, y)
69
68
  return unless x && y
70
69
  if outline
71
70
  super(x, y, outline: true, 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/Line2D.html
27
27
  class Line < Shape
28
28
  class << self
29
29
  # Returns an indicator of where the specified point (px,py) lies with respect to the line segment from
@@ -213,20 +213,20 @@ module PerfectShape
213
213
  # the line, {@code false} if the point lies outside of the
214
214
  # line's bounds.
215
215
  def contain?(x_or_point, y = nil, outline: true, distance_tolerance: 0)
216
- x, y = normalize_point(x_or_point, y)
216
+ x, y = Point.normalize_point(x_or_point, y)
217
217
  return unless x && y
218
218
  distance_tolerance = BigDecimal(distance_tolerance.to_s)
219
219
  point_distance(x, y) <= distance_tolerance
220
220
  end
221
221
 
222
222
  def point_distance(x_or_point, y = nil)
223
- x, y = normalize_point(x_or_point, y)
223
+ x, y = Point.normalize_point(x_or_point, y)
224
224
  return unless x && y
225
225
  Line.point_distance(points[0][0], points[0][1], points[1][0], points[1][1], x, y)
226
226
  end
227
227
 
228
228
  def relative_counterclockwise(x_or_point, y = nil)
229
- x, y = normalize_point(x_or_point, y)
229
+ x, y = Point.normalize_point(x_or_point, y)
230
230
  return unless x && y
231
231
  Line.relative_counterclockwise(points[0][0], points[0][1], points[1][0], points[1][1], x, y)
232
232
  end
@@ -237,9 +237,34 @@ module PerfectShape
237
237
  # +1 is returned for a crossing where the Y coordinate is increasing
238
238
  # -1 is returned for a crossing where the Y coordinate is decreasing
239
239
  def point_crossings(x_or_point, y = nil)
240
- x, y = normalize_point(x_or_point, y)
240
+ x, y = Point.normalize_point(x_or_point, y)
241
241
  return unless x && y
242
242
  Line.point_crossings(points[0][0], points[0][1], points[1][0], points[1][1], x, y)
243
243
  end
244
+
245
+ def intersect?(rectangle)
246
+ require 'perfect_shape/rectangle'
247
+ x1 = points[0][0]
248
+ y1 = points[0][1]
249
+ x2 = points[1][0]
250
+ y2 = points[1][1]
251
+ out1 = out2 = nil
252
+ return true if (out2 = rectangle.out_state(x2, y2)) == 0
253
+ while (out1 = rectangle.out_state(x1, y1)) != 0
254
+ return false if (out1 & out2) != 0
255
+ if (out1 & (Rectangle::OUT_LEFT | Rectangle::OUT_RIGHT)) != 0
256
+ x = rectangle.x
257
+ x += rectangle.width if (out1 & Rectangle::OUT_RIGHT) != 0
258
+ y1 = y1 + (x - x1) * (y2 - y1) / (x2 - x1)
259
+ x1 = x
260
+ else
261
+ y = rectangle.y
262
+ y += rectangle.height if (out1 & Rectangle::OUT_BOTTOM) != 0
263
+ x1 = x1 + (y - y1) * (x2 - x1) / (y2 - y1)
264
+ y1 = y
265
+ end
266
+ end
267
+ true
268
+ end
244
269
  end
245
270
  end