geometry 6.5 → 6.6
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
-
[![Build Status](https://travis-ci.org/bfoz/geometry.png)](https://travis-ci.org/bfoz/geometry)
|
5
4
|
[![Gem Version](https://badge.fury.io/rb/geometry.svg)](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.
|