geometry 6.5 → 6.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +5 -13
- data/.github/workflows/ruby.yml +21 -0
- data/Gemfile +3 -0
- data/README.markdown +1 -2
- data/geometry.gemspec +5 -1
- data/lib/geometry/annulus.rb +2 -2
- data/lib/geometry/arc.rb +72 -1
- data/lib/geometry/edge.rb +21 -4
- data/lib/geometry/line.rb +2 -2
- data/lib/geometry/obround.rb +1 -2
- data/lib/geometry/path.rb +27 -0
- data/lib/geometry/point.rb +21 -6
- data/lib/geometry/point_iso.rb +0 -2
- data/lib/geometry/point_one.rb +0 -2
- data/lib/geometry/point_zero.rb +0 -1
- data/lib/geometry/polygon.rb +13 -12
- data/lib/geometry/polyline.rb +6 -6
- data/lib/geometry/rectangle.rb +12 -12
- data/lib/geometry/rotation.rb +5 -3
- data/lib/geometry/size.rb +1 -3
- data/lib/geometry/square.rb +13 -11
- data/lib/geometry/transformation/composition.rb +1 -1
- data/lib/geometry/triangle.rb +1 -1
- data/test/geometry/arc.rb +98 -0
- data/test/geometry/circle.rb +3 -3
- data/test/geometry/edge.rb +16 -2
- data/test/geometry/line.rb +3 -3
- data/test/geometry/path.rb +16 -0
- data/test/geometry/point.rb +18 -0
- data/test/geometry/point_iso.rb +21 -21
- data/test/geometry/point_one.rb +1 -1
- data/test/geometry/point_zero.rb +1 -1
- data/test/geometry/polygon.rb +1 -1
- data/test/geometry/rectangle.rb +6 -1
- data/test/geometry/rotation.rb +1 -1
- data/test/geometry/size_one.rb +1 -1
- data/test/geometry/size_zero.rb +1 -1
- data/test/geometry/square.rb +15 -10
- data/test/geometry/transformation.rb +1 -1
- data/test/geometry/vector.rb +1 -1
- metadata +63 -10
- data/.travis.yml +0 -12
checksums.yaml
CHANGED
@@ -1,15 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
|
5
|
-
data.tar.gz: !binary |-
|
6
|
-
N2QyODkzMTkwYzhkZjhmYjg2MjliOTViYTM4ZjkxN2JkMDA4NDAyOQ==
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 18aa25c45e739b77377ac376b9575f9828e2eb0566b4349a073be5b4deeea20e
|
4
|
+
data.tar.gz: 1ddbf89f0ad3854c222ffd344c0eaedd129d315ec9b1488f503f519dc8b5ca5b
|
7
5
|
SHA512:
|
8
|
-
metadata.gz:
|
9
|
-
|
10
|
-
Y2M2ZWNmYTc1NzU2ODFkN2Y2NGI3NTYzYzE2NTcwY2IwMWVlODYzM2U1NmIx
|
11
|
-
NzNmYjM3OTEyMDY3NGVhZjU3NTZiNzY1YmQ1ZDVmODEzODE3N2I=
|
12
|
-
data.tar.gz: !binary |-
|
13
|
-
ZDU2NTJmODQ1ZTAyMzc2NGJiNjFhM2E4NTBhNWIxNjY5M2JjNThhNWJjN2Rm
|
14
|
-
MjAwNTYwMjlkMWE1N2UyNTMwZTMxNWZkNDc5NTU3NzhiNDlhOTdjMmY0NzBk
|
15
|
-
ZWRiNmVlMDgwZjVjNzY5YjM0ZTMyM2U4M2UzOGU0MzIzYmVkMjM=
|
6
|
+
metadata.gz: 69645a43932bb7b4c36a6401daabf49d2bbcbf3450f750f85d1c2f60f8d1e7944cb0f1c5110569912bc10d656f3ef8a1f5ffaa80ee9eb228a8a8f7855aaed0c8
|
7
|
+
data.tar.gz: 8e68aa085655950d9f499a887cf9ec0aeaac2c183e53a242675105f093652fdcb141aae4cc91226bedadf97e0dd94651b25956026c78c4a98e60b57d7b232dfa
|
@@ -0,0 +1,21 @@
|
|
1
|
+
name: Ruby
|
2
|
+
|
3
|
+
on: [push]
|
4
|
+
|
5
|
+
jobs:
|
6
|
+
test:
|
7
|
+
strategy:
|
8
|
+
fail-fast: false
|
9
|
+
matrix:
|
10
|
+
ruby-version: ['3.0', '3.1', '3.2', '3.3']
|
11
|
+
os: [ubuntu-latest]
|
12
|
+
|
13
|
+
runs-on: ${{ matrix.os }}
|
14
|
+
|
15
|
+
steps:
|
16
|
+
- uses: actions/checkout@v4
|
17
|
+
- uses: ruby/setup-ruby@v1
|
18
|
+
with:
|
19
|
+
ruby-version: ${{ matrix.ruby-version }}
|
20
|
+
bundler-cache: true
|
21
|
+
- run: bundle exec rake
|
data/Gemfile
CHANGED
data/README.markdown
CHANGED
@@ -1,7 +1,6 @@
|
|
1
1
|
Geometry for Ruby
|
2
2
|
=================
|
3
3
|
|
4
|
-
[](https://travis-ci.org/bfoz/geometry)
|
5
4
|
[](http://badge.fury.io/rb/geometry)
|
6
5
|
|
7
6
|
Classes and methods for the handling of all of the basic geometry that you
|
@@ -15,7 +14,7 @@ that don't work in higher dimensions and I'll do my best to fix them.
|
|
15
14
|
License
|
16
15
|
-------
|
17
16
|
|
18
|
-
Copyright 2012-
|
17
|
+
Copyright 2012-2024 Brandon Fosdick <bfoz@bfoz.net> and released under the BSD license.
|
19
18
|
|
20
19
|
Primitives
|
21
20
|
----------
|
data/geometry.gemspec
CHANGED
@@ -2,8 +2,9 @@
|
|
2
2
|
$:.push File.expand_path("../lib", __FILE__)
|
3
3
|
|
4
4
|
Gem::Specification.new do |s|
|
5
|
+
spec = s
|
5
6
|
s.name = "geometry"
|
6
|
-
|
7
|
+
spec.version = '6.6'
|
7
8
|
s.authors = ["Brandon Fosdick"]
|
8
9
|
s.email = ["bfoz@bfoz.net"]
|
9
10
|
s.homepage = "http://github.com/bfoz/geometry"
|
@@ -18,4 +19,7 @@ Gem::Specification.new do |s|
|
|
18
19
|
s.require_paths = ["lib"]
|
19
20
|
|
20
21
|
s.required_ruby_version = '>= 2.0'
|
22
|
+
|
23
|
+
spec.add_development_dependency "bundler", "~> 2"
|
24
|
+
spec.add_development_dependency "rake", "~> 13"
|
21
25
|
end
|
data/lib/geometry/annulus.rb
CHANGED
@@ -22,7 +22,7 @@ known as a Ring, is a circle that ate another circle.
|
|
22
22
|
# @!attribute inner_radius
|
23
23
|
# @return [Number] the radius of the inside of the {Annulus}
|
24
24
|
def inner_radius
|
25
|
-
@inner_radius || (@inner_diameter && @inner_diameter/2)
|
25
|
+
@inner_radius || (@inner_diameter && @inner_diameter.to_r/2)
|
26
26
|
end
|
27
27
|
|
28
28
|
# @!attribute outer_diameter
|
@@ -34,7 +34,7 @@ known as a Ring, is a circle that ate another circle.
|
|
34
34
|
# @!attribute outer_radius
|
35
35
|
# @return [Number] the outer radius
|
36
36
|
def outer_radius
|
37
|
-
@outer_radius || (@outer_diameter && @outer_diameter/2)
|
37
|
+
@outer_radius || ((@outer_diameter && @outer_diameter).to_r/2)
|
38
38
|
end
|
39
39
|
|
40
40
|
# @!attribute diameter
|
data/lib/geometry/arc.rb
CHANGED
@@ -17,8 +17,15 @@ An {Arc} with its center at [1,1] and a radius of 2 that starts at the X-axis an
|
|
17
17
|
include ClusterFactory
|
18
18
|
|
19
19
|
attr_reader :center
|
20
|
+
|
21
|
+
# @return [Number] the radius of the {Arc}
|
20
22
|
attr_reader :radius
|
21
|
-
|
23
|
+
|
24
|
+
# @return [Number] the starting angle of the {Arc} as radians from the x-axis
|
25
|
+
attr_reader :start_angle
|
26
|
+
|
27
|
+
# @return [Number] the ending angle of the {Arc} as radians from the x-axis
|
28
|
+
attr_reader :end_angle
|
22
29
|
|
23
30
|
# @overload new(center, start, end)
|
24
31
|
# Create a new {Arc} given center, start and end {Point}s
|
@@ -90,5 +97,69 @@ An {Arc} with its center at [1,1] and a radius of 2 that starts at the X-axis an
|
|
90
97
|
# The end point of the {Arc}
|
91
98
|
# @return [Point]
|
92
99
|
alias :last :end
|
100
|
+
|
101
|
+
def ==(other)
|
102
|
+
if other.is_a?(ThreePointArc)
|
103
|
+
(self.center == other.center) && (self.end == other.end) && (self.start == other.start)
|
104
|
+
else
|
105
|
+
super other
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
# @group Attributes
|
110
|
+
|
111
|
+
# @return [Point] The upper-right corner of the bounding rectangle that encloses the {Path}
|
112
|
+
def max
|
113
|
+
minmax.last
|
114
|
+
end
|
115
|
+
|
116
|
+
# @return [Point] The lower-left corner of the bounding rectangle that encloses the {Path}
|
117
|
+
def min
|
118
|
+
minmax.first
|
119
|
+
end
|
120
|
+
|
121
|
+
# @return [Array<Point>] The lower-left and upper-right corners of the enclosing bounding rectangle
|
122
|
+
def minmax
|
123
|
+
a = [self.start, self.end]
|
124
|
+
quadrants = a.map(&:quadrant)
|
125
|
+
|
126
|
+
# If the Arc spans more than one quadrant, then it must cross at
|
127
|
+
# least one axis. Each axis-crossing is a potential extrema.
|
128
|
+
if quadrants.first != quadrants.last
|
129
|
+
range = (quadrants.first...quadrants.last)
|
130
|
+
# If the Arc crosses the X axis...
|
131
|
+
if quadrants.first > quadrants.last
|
132
|
+
range = (quadrants.first..4).to_a + (1...quadrants.last).to_a
|
133
|
+
end
|
134
|
+
|
135
|
+
a = range.map do |q|
|
136
|
+
case q
|
137
|
+
when 1 then self.center + Point[0,radius]
|
138
|
+
when 2 then self.center + Point[-radius, 0]
|
139
|
+
when 3 then self.center + Point[0,-radius]
|
140
|
+
when 4 then self.center + Point[radius,0]
|
141
|
+
end
|
142
|
+
end.push(*a)
|
143
|
+
a.reduce([a.first, a.first]) {|memo, e| [memo.first.min(e), memo.last.max(e)] }
|
144
|
+
else
|
145
|
+
[a.first.min(a.last), a.first.max(a.last)]
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
def end_angle
|
150
|
+
a = (self.end - self.center)
|
151
|
+
Math.atan2(a.y, a.x)
|
152
|
+
end
|
153
|
+
|
154
|
+
def radius
|
155
|
+
(self.start - self.center).magnitude
|
156
|
+
end
|
157
|
+
|
158
|
+
def start_angle
|
159
|
+
a = (self.start - self.center)
|
160
|
+
Math.atan2(a.y, a.x)
|
161
|
+
end
|
162
|
+
|
163
|
+
# @endgroup
|
93
164
|
end
|
94
165
|
end
|
data/lib/geometry/edge.rb
CHANGED
@@ -1,5 +1,3 @@
|
|
1
|
-
require 'mathn'
|
2
|
-
|
3
1
|
require_relative 'point'
|
4
2
|
|
5
3
|
module Geometry
|
@@ -42,6 +40,25 @@ An edge. It's a line segment between 2 points. Generally part of a {Polygon}.
|
|
42
40
|
end
|
43
41
|
end
|
44
42
|
|
43
|
+
# @group Attributes
|
44
|
+
|
45
|
+
# @return [Point] The upper-right corner of the bounding rectangle that encloses the {Edge}
|
46
|
+
def max
|
47
|
+
first.max(last)
|
48
|
+
end
|
49
|
+
|
50
|
+
# @return [Point] The lower-left corner of the bounding rectangle that encloses the {Edge}
|
51
|
+
def min
|
52
|
+
first.min(last)
|
53
|
+
end
|
54
|
+
|
55
|
+
# @return [Array<Point>] The lower-left and upper-right corners of the enclosing bounding rectangle
|
56
|
+
def minmax
|
57
|
+
first.minmax(last)
|
58
|
+
end
|
59
|
+
|
60
|
+
# @endgroup
|
61
|
+
|
45
62
|
# Return a new {Edge} with swapped endpoints
|
46
63
|
def reverse
|
47
64
|
self.class.new(@last, @first)
|
@@ -128,8 +145,8 @@ An edge. It's a line segment between 2 points. Generally part of a {Polygon}.
|
|
128
145
|
nil
|
129
146
|
end
|
130
147
|
else
|
131
|
-
s = (-v1[1] * p.x + v1[0] * p.y) / denominator # v1 x (p0 - p2) / denominator
|
132
|
-
t = ( v2[0] * p.y - v2[1] * p.x) / denominator # v2 x (p0 - p2) / denominator
|
148
|
+
s = (-v1[1] * p.x + v1[0] * p.y).to_r / denominator # v1 x (p0 - p2) / denominator
|
149
|
+
t = ( v2[0] * p.y - v2[1] * p.x).to_r / denominator # v2 x (p0 - p2) / denominator
|
133
150
|
|
134
151
|
p0 + v1 * t if ((0..1) === s) && ((0..1) === t)
|
135
152
|
end
|
data/lib/geometry/line.rb
CHANGED
@@ -190,7 +190,7 @@ Supports two-point, slope-intercept, and point-slope initializer forms
|
|
190
190
|
((other.first.y == slope * other.first.x + intercept)) && (other.last.y == (slope * other.last.x + intercept))
|
191
191
|
else
|
192
192
|
self.eql? other
|
193
|
-
|
193
|
+
end
|
194
194
|
end
|
195
195
|
|
196
196
|
# Two {SlopeInterceptLine}s are equal if both have equal slopes and intercepts
|
@@ -251,7 +251,7 @@ Supports two-point, slope-intercept, and point-slope initializer forms
|
|
251
251
|
((first.y == other.slope * first.x + other.intercept)) && (last.y == (other.slope * last.x + other.intercept))
|
252
252
|
else
|
253
253
|
self.eql?(other) || ((first == other.last) && (last == other.first))
|
254
|
-
|
254
|
+
end
|
255
255
|
end
|
256
256
|
|
257
257
|
# Two {TwoPointLine}s are equal if both have equal endpoints
|
data/lib/geometry/obround.rb
CHANGED
@@ -180,8 +180,6 @@ The {Obround} class cluster represents a rectangle with semicircular end caps
|
|
180
180
|
end
|
181
181
|
|
182
182
|
class SizedObround < Obround
|
183
|
-
# @return [Point] The {Obround}'s center
|
184
|
-
attr_reader :center
|
185
183
|
# @return [Point] The {Obround}'s origin
|
186
184
|
attr_accessor :origin
|
187
185
|
# @return [Size] The {Size} of the {Obround}
|
@@ -219,6 +217,7 @@ The {Obround} class cluster represents a rectangle with semicircular end caps
|
|
219
217
|
alias :== :eql?
|
220
218
|
|
221
219
|
# @group Accessors
|
220
|
+
# @return [Point] The {Obround}'s center
|
222
221
|
def center
|
223
222
|
@origin + @size/2
|
224
223
|
end
|
data/lib/geometry/path.rb
CHANGED
@@ -52,11 +52,38 @@ An object representing a set of connected elements, each of which could be an
|
|
52
52
|
end
|
53
53
|
end
|
54
54
|
|
55
|
+
def ==(other)
|
56
|
+
if other.is_a?(Path)
|
57
|
+
@elements == other.elements
|
58
|
+
else
|
59
|
+
super other
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
# @group Attributes
|
64
|
+
|
65
|
+
# @return [Point] The upper-right corner of the bounding rectangle that encloses the {Path}
|
66
|
+
def max
|
67
|
+
elements.reduce(elements.first.max) {|memo, e| memo.max(e.max) }
|
68
|
+
end
|
69
|
+
|
70
|
+
# @return [Point] The lower-left corner of the bounding rectangle that encloses the {Path}
|
71
|
+
def min
|
72
|
+
elements.reduce(elements.first.min) {|memo, e| memo.min(e.max) }
|
73
|
+
end
|
74
|
+
|
75
|
+
# @return [Array<Point>] The lower-left and upper-right corners of the enclosing bounding rectangle
|
76
|
+
def minmax
|
77
|
+
elements.reduce(elements.first.minmax) {|memo, e| [memo.first.min(e.min), memo.last.max(e.max)] }
|
78
|
+
end
|
79
|
+
|
55
80
|
# @return [Geometry] The last element in the {Path}
|
56
81
|
def last
|
57
82
|
@elements.last
|
58
83
|
end
|
59
84
|
|
85
|
+
# @endgroup
|
86
|
+
|
60
87
|
# Append a new geometry element to the {Path}
|
61
88
|
# @return [Path]
|
62
89
|
def push(arg)
|
data/lib/geometry/point.rb
CHANGED
@@ -21,8 +21,6 @@ geometry class (x, y, z).
|
|
21
21
|
point = Geometry::Point[x,y]
|
22
22
|
=end
|
23
23
|
class Point < Vector
|
24
|
-
attr_reader :x, :y, :z
|
25
|
-
|
26
24
|
# Allow vector-style initialization, but override to support copy-init
|
27
25
|
# from Vector or another Point
|
28
26
|
#
|
@@ -34,15 +32,15 @@ geometry class (x, y, z).
|
|
34
32
|
return array[0] if array[0].is_a?(Point)
|
35
33
|
array = array[0] if array[0].is_a?(Array)
|
36
34
|
array = array[0].to_a if array[0].is_a?(Vector)
|
37
|
-
super
|
35
|
+
super(*array)
|
38
36
|
end
|
39
37
|
|
40
|
-
# Creates and returns a new {PointIso} instance. Or, a {Point} full of
|
38
|
+
# Creates and returns a new {PointIso} instance. Or, a {Point} full of the given value if the size argument is given.
|
41
39
|
# @param value [Number] the value of the elements
|
42
40
|
# @param size [Number] the size of the new {Point} full of ones
|
43
41
|
# @return [PointIso] A new {PointIso} instance
|
44
42
|
def self.iso(value, size=nil)
|
45
|
-
size ? Point[Array.new(size,
|
43
|
+
size ? Point[Array.new(size, value)] : PointIso.new(value)
|
46
44
|
end
|
47
45
|
|
48
46
|
# Creates and returns a new {PointOne} instance. Or, a {Point} full of ones if the size argument is given.
|
@@ -119,6 +117,8 @@ geometry class (x, y, z).
|
|
119
117
|
'Point' + @elements.to_s
|
120
118
|
end
|
121
119
|
|
120
|
+
# @group Attributes
|
121
|
+
|
122
122
|
# @override max()
|
123
123
|
# @return [Number] The maximum value of the {Point}'s elements
|
124
124
|
# @override max(point)
|
@@ -169,6 +169,22 @@ geometry class (x, y, z).
|
|
169
169
|
end
|
170
170
|
end
|
171
171
|
|
172
|
+
# Return the {Point}'s quadrant in the 2D Cartesian Euclidean Plane
|
173
|
+
# https://en.wikipedia.org/wiki/Quadrant_(plane_geometry)
|
174
|
+
# @note Undefined for all points on the axes, and for dimensionalities other than 2
|
175
|
+
# @todo Define the results for points on the axes
|
176
|
+
# @return [Bool] The {Point}'s quadrant in the 2D Cartesian Euclidean Plane
|
177
|
+
def quadrant
|
178
|
+
return nil unless elements[1]
|
179
|
+
if elements.first > 0
|
180
|
+
(elements[1] > 0) ? 1 : 4
|
181
|
+
else
|
182
|
+
(elements[1] > 0) ? 2 : 3
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
# @endgroup
|
187
|
+
|
172
188
|
# Returns a new {Point} with the given number of elements removed from the end
|
173
189
|
# @return [Point] the popped elements
|
174
190
|
def pop(count=1)
|
@@ -298,4 +314,3 @@ geometry class (x, y, z).
|
|
298
314
|
|
299
315
|
end
|
300
316
|
end
|
301
|
-
|
data/lib/geometry/point_iso.rb
CHANGED
data/lib/geometry/point_one.rb
CHANGED
data/lib/geometry/point_zero.rb
CHANGED
data/lib/geometry/polygon.rb
CHANGED
@@ -46,7 +46,7 @@ but there's currently nothing that enforces simplicity.
|
|
46
46
|
|
47
47
|
# @return [Polygon] A new {Polygon} with orientation that's the opposite of the receiver
|
48
48
|
def reverse
|
49
|
-
self.class.new
|
49
|
+
self.class.new(*(self.vertices.reverse))
|
50
50
|
end
|
51
51
|
|
52
52
|
# Reverse the receiver and return it
|
@@ -68,7 +68,7 @@ but there's currently nothing that enforces simplicity.
|
|
68
68
|
# @param [Point] point The {Point} to test
|
69
69
|
# @return [Number] 1 if the {Point} is inside the {Polygon}, -1 if it's outside, and 0 if it's on an {Edge}
|
70
70
|
def <=>(point)
|
71
|
-
|
71
|
+
winding = edges.reduce(0) do |sum, e|
|
72
72
|
direction = e.last.y <=> e.first.y
|
73
73
|
# Ignore edges that don't cross the point's x coordinate
|
74
74
|
next sum unless ((point.y <=> e.last.y) + (point.y <=> e.first.y)).abs <= 1
|
@@ -83,7 +83,7 @@ but there's currently nothing that enforces simplicity.
|
|
83
83
|
sum += 0 <=> (direction + is_left)
|
84
84
|
end
|
85
85
|
end
|
86
|
-
(0 ==
|
86
|
+
(0 == winding) ? -1 : 1
|
87
87
|
end
|
88
88
|
|
89
89
|
# Create a new {Polygon} that's the union of the receiver and a passed {Polygon}
|
@@ -152,11 +152,11 @@ but there's currently nothing that enforces simplicity.
|
|
152
152
|
edgeFragments = edgeFragments.reject {|f| edgeFragments.find {|f2| (f[:first] == f2[:last]) and (f[:last] == f2[:first])} }
|
153
153
|
|
154
154
|
# Construct the output polygons
|
155
|
-
|
155
|
+
output_polygons = edgeFragments.reduce([Array.new]) do |output, fragment|
|
156
156
|
next output if fragment.empty?
|
157
157
|
polygon = output.last
|
158
158
|
polygon.push fragment[:first], fragment[:last] if polygon.empty?
|
159
|
-
|
159
|
+
loop do
|
160
160
|
adjacent_fragment = edgeFragments.find {|f| fragment[:last] == f[:first]}
|
161
161
|
break unless adjacent_fragment
|
162
162
|
|
@@ -170,13 +170,13 @@ but there's currently nothing that enforces simplicity.
|
|
170
170
|
end
|
171
171
|
|
172
172
|
# If everything worked properly there should be only one output Polygon
|
173
|
-
|
174
|
-
|
173
|
+
output_polygons.reject! {|a| a.empty?}
|
174
|
+
output_polygons = Polygon.new(*(output_polygons[0]))
|
175
175
|
|
176
176
|
# Table 4: Both input polygons are "island" type and the operation
|
177
177
|
# is union, so the output polygon's orientation should be the same
|
178
178
|
# as the input polygon's orientation
|
179
|
-
(self.clockwise? !=
|
179
|
+
(self.clockwise? != output_polygons.clockwise?) ? output_polygons.reverse : output_polygons
|
180
180
|
end
|
181
181
|
alias :+ :union
|
182
182
|
|
@@ -213,7 +213,7 @@ but there's currently nothing that enforces simplicity.
|
|
213
213
|
break if current_point == hull_points.first
|
214
214
|
hull_points << min_point
|
215
215
|
end
|
216
|
-
Polygon.new
|
216
|
+
Polygon.new(*hull_points)
|
217
217
|
end
|
218
218
|
|
219
219
|
# @endgroup
|
@@ -278,7 +278,7 @@ but there's currently nothing that enforces simplicity.
|
|
278
278
|
redo # Recheck the modified edges
|
279
279
|
end
|
280
280
|
end
|
281
|
-
Polygon.new
|
281
|
+
Polygon.new(*(active_edges.map {|e| e[:edge]}.compact.map {|e| [e.first, e.last]}.flatten))
|
282
282
|
end
|
283
283
|
|
284
284
|
# Vertex bisectors suitable for outsetting
|
@@ -332,9 +332,10 @@ but there's currently nothing that enforces simplicity.
|
|
332
332
|
# @param [Integer] index The index to insert the new {Point} before
|
333
333
|
# @param [Point] point The {Point} to insert
|
334
334
|
# @param [Integer] type The vertex type: 1 is inside, 0 is boundary, -1 is outside
|
335
|
+
# @return [Bool] true if the {Point} was inserted, false otherwise
|
335
336
|
def insert(index, point, type)
|
336
|
-
if
|
337
|
-
|
337
|
+
if vertex = @vertices.find {|v| v[:vertex] == point }
|
338
|
+
vertex[:type] = type
|
338
339
|
false
|
339
340
|
else
|
340
341
|
@vertices.insert(index, {:vertex => point, :type => type})
|
data/lib/geometry/polyline.rb
CHANGED
@@ -41,7 +41,7 @@ also like a {Path} in that it isn't necessarily closed.
|
|
41
41
|
@vertices.push first
|
42
42
|
elsif first.is_a?(Edge)
|
43
43
|
@edges.push first
|
44
|
-
@vertices.push
|
44
|
+
@vertices.push(*(first.to_a))
|
45
45
|
end
|
46
46
|
|
47
47
|
args.reduce(@vertices.last) do |previous,n|
|
@@ -82,7 +82,7 @@ also like a {Path} in that it isn't necessarily closed.
|
|
82
82
|
else
|
83
83
|
e = Edge.new(previous, n.first)
|
84
84
|
push_edge e, n
|
85
|
-
push_vertex
|
85
|
+
push_vertex(*(e.to_a), *(n.to_a))
|
86
86
|
end
|
87
87
|
n.last
|
88
88
|
end
|
@@ -167,7 +167,7 @@ also like a {Path} in that it isn't necessarily closed.
|
|
167
167
|
# Clone the receiver, reverse it, then return it
|
168
168
|
# @return [Polyline] the reversed clone
|
169
169
|
def reverse
|
170
|
-
self.class.new
|
170
|
+
self.class.new(*(edges.reverse.map! {|edge| edge.reverse! }))
|
171
171
|
end
|
172
172
|
|
173
173
|
# Reverse the receiver and return it
|
@@ -261,7 +261,7 @@ also like a {Path} in that it isn't necessarily closed.
|
|
261
261
|
redo # Recheck the modified edges
|
262
262
|
end
|
263
263
|
end
|
264
|
-
Polyline.new
|
264
|
+
Polyline.new(*(active_edges.map {|e| e[:edge]}.compact.map {|e| [e.first, e.last]}.flatten))
|
265
265
|
end
|
266
266
|
alias :leftset :offset
|
267
267
|
|
@@ -369,12 +369,12 @@ also like a {Path} in that it isn't necessarily closed.
|
|
369
369
|
end
|
370
370
|
|
371
371
|
def push_edge(*e)
|
372
|
-
@edges.push
|
372
|
+
@edges.push(*e)
|
373
373
|
@edges.uniq!
|
374
374
|
end
|
375
375
|
|
376
376
|
def push_vertex(*v)
|
377
|
-
@vertices.push
|
377
|
+
@vertices.push(*v)
|
378
378
|
@vertices.uniq!
|
379
379
|
end
|
380
380
|
|
data/lib/geometry/rectangle.rb
CHANGED
@@ -25,16 +25,8 @@ The {Rectangle} class cluster represents your typical arrangement of 4 corners a
|
|
25
25
|
class Rectangle
|
26
26
|
include ClusterFactory
|
27
27
|
|
28
|
-
# @return [Point] The {Rectangle}'s center
|
29
|
-
attr_reader :center
|
30
|
-
# @return [Number] Height of the {Rectangle}
|
31
|
-
attr_reader :height
|
32
|
-
# @return [Point] The {Rectangle}'s origin
|
33
|
-
attr_reader :origin
|
34
28
|
# @return [Size] The {Size} of the {Rectangle}
|
35
29
|
attr_reader :size
|
36
|
-
# @return [Number] Width of the {Rectangle}
|
37
|
-
attr_reader :width
|
38
30
|
|
39
31
|
# @overload new(width, height)
|
40
32
|
# Creates a {Rectangle} of the given width and height, centered on the origin
|
@@ -120,7 +112,7 @@ The {Rectangle} class cluster represents your typical arrangement of 4 corners a
|
|
120
112
|
# @return [Point] The {Rectangle}'s center
|
121
113
|
def center
|
122
114
|
min, max = @points.minmax {|a,b| a.y <=> b.y}
|
123
|
-
Point[(max.x+min.x)/2, (max.y+min.y)/2]
|
115
|
+
Point[(max.x+min.x).to_r/2, (max.y+min.y).to_r/2]
|
124
116
|
end
|
125
117
|
|
126
118
|
# @!attribute closed?
|
@@ -155,25 +147,28 @@ The {Rectangle} class cluster represents your typical arrangement of 4 corners a
|
|
155
147
|
[self.min, self.max]
|
156
148
|
end
|
157
149
|
|
158
|
-
# @return [Array<Point>] The {Rectangle}'s four points (
|
150
|
+
# @return [Array<Point>] The {Rectangle}'s four points (clockwise)
|
159
151
|
def points
|
160
152
|
point0, point2 = *@points
|
161
|
-
point1 = Point[
|
162
|
-
point3 = Point[
|
153
|
+
point1 = Point[point0.x, point2.y]
|
154
|
+
point3 = Point[point2.x, point0.y]
|
163
155
|
[point0, point1, point2, point3]
|
164
156
|
end
|
165
157
|
|
158
|
+
# @return [Point] The {Rectangle}'s origin
|
166
159
|
def origin
|
167
160
|
minx = @points.min {|a,b| a.x <=> b.x}
|
168
161
|
miny = @points.min {|a,b| a.y <=> b.y}
|
169
162
|
Point[minx.x, miny.y]
|
170
163
|
end
|
171
164
|
|
165
|
+
# @return [Number] Height of the {Rectangle}
|
172
166
|
def height
|
173
167
|
min, max = @points.minmax {|a,b| a.y <=> b.y}
|
174
168
|
max.y - min.y
|
175
169
|
end
|
176
170
|
|
171
|
+
# @return [Number] Width of the {Rectangle}
|
177
172
|
def width
|
178
173
|
min, max = @points.minmax {|a,b| a.x <=> b.x}
|
179
174
|
max.x - min.x
|
@@ -212,6 +207,11 @@ The {Rectangle} class cluster represents your typical arrangement of 4 corners a
|
|
212
207
|
Rectangle.new from:(min + Point[options[:left], options[:bottom]]), to:(max - Point[options[:right], options[:top]])
|
213
208
|
end
|
214
209
|
end
|
210
|
+
|
211
|
+
# @return [Path] A closed {Path} that traces the boundary of the {Rectangle} clockwise, starting from the lower-left
|
212
|
+
def path
|
213
|
+
Path.new(*self.points, self.points.first)
|
214
|
+
end
|
215
215
|
end
|
216
216
|
|
217
217
|
class CenteredRectangle < Rectangle
|
data/lib/geometry/rotation.rb
CHANGED
@@ -72,16 +72,18 @@ A generalized representation of a rotation transformation.
|
|
72
72
|
alias :== :eql?
|
73
73
|
|
74
74
|
def identity?
|
75
|
-
|
75
|
+
x, y, z = self.x, self.y, self.z
|
76
|
+
(!x && !y && !z) || ([x, y, z].select {|a| a}.all? {|a| a.respond_to?(:magnitude) ? (1 == a.magnitude) : (1 == a.size)})
|
76
77
|
end
|
77
78
|
|
78
79
|
# @attribute [r] matrix
|
79
80
|
# @return [Matrix] the transformation {Matrix} representing the {Rotation}
|
80
81
|
def matrix
|
81
|
-
|
82
|
+
x, y, z = self.x, self.y, self.z
|
83
|
+
return nil unless [x, y, z].compact.size >= 2
|
82
84
|
|
83
85
|
# Force all axes to be Vectors
|
84
|
-
x,y,z = [
|
86
|
+
x,y,z = [x, y, z].map {|a| a.is_a?(Array) ? Vector[*a] : a}
|
85
87
|
|
86
88
|
# Force all axes to exist
|
87
89
|
if x and y
|
data/lib/geometry/size.rb
CHANGED
@@ -17,8 +17,6 @@ methods (width, height and depth).
|
|
17
17
|
=end
|
18
18
|
|
19
19
|
class Size < Vector
|
20
|
-
attr_reader :x, :y, :z
|
21
|
-
|
22
20
|
# Allow vector-style initialization, but override to support copy-init
|
23
21
|
# from Vector, Point or another Size
|
24
22
|
#
|
@@ -30,7 +28,7 @@ methods (width, height and depth).
|
|
30
28
|
def self.[](*array)
|
31
29
|
array.map! {|a| a.respond_to?(:to_a) ? a.to_a : a }
|
32
30
|
array.flatten!
|
33
|
-
super
|
31
|
+
super(*array)
|
34
32
|
end
|
35
33
|
|
36
34
|
# Creates and returns a new {SizeOne} instance. Or, a {Size} full of ones if the size argument is given.
|