euclidean 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/.rspec +2 -0
- data/Gemfile +8 -0
- data/Guardfile +8 -0
- data/LICENSE.txt +22 -0
- data/README.md +90 -0
- data/Rakefile +1 -0
- data/euclidean.gemspec +24 -0
- data/lib/euclidean.rb +12 -0
- data/lib/euclidean/circle.rb +118 -0
- data/lib/euclidean/cluster_factory.rb +15 -0
- data/lib/euclidean/edge.rb +143 -0
- data/lib/euclidean/point.rb +160 -0
- data/lib/euclidean/point_zero.rb +104 -0
- data/lib/euclidean/rectangle.rb +378 -0
- data/lib/euclidean/size.rb +78 -0
- data/lib/euclidean/vector.rb +34 -0
- data/lib/euclidean/version.rb +3 -0
- data/spec/euclidean/circle_spec.rb +115 -0
- data/spec/euclidean/edge_spec.rb +129 -0
- data/spec/euclidean/point_spec.rb +264 -0
- data/spec/euclidean/rectangle_spec.rb +155 -0
- data/spec/euclidean/size_spec.rb +98 -0
- data/spec/euclidean/vector_spec.rb +41 -0
- data/spec/spec_helper.rb +15 -0
- metadata +118 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 9a73c24a00cc5b70d91928313b88a92fd4b7b911
|
4
|
+
data.tar.gz: 90fc4f8ec1974af5a8e7663e89d99564791ad673
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 5ec8e7d0cc9e81d4d974cd7b3d0a0bef4f636d2e0c17981937ab8fbeb48c8814763af0d61ec426536ffe2e0ffd300b497b12bc437d2aeca68c3f9f06e2db83c8
|
7
|
+
data.tar.gz: b31be403bbaaa9510a7efe67d3a40427fd7440488445955cc32ec938c18b5c3315f4e2ce3c809c94a4b19ad5d9d26332d4026cd8eb6c9a1c98ea4ed199091d81
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/Gemfile
ADDED
data/Guardfile
ADDED
@@ -0,0 +1,8 @@
|
|
1
|
+
# A sample Guardfile
|
2
|
+
# More info at https://github.com/guard/guard#readme
|
3
|
+
|
4
|
+
guard 'rspec', cmd: 'bundle exec rspec', all_on_start: false, all_after_pass: false do
|
5
|
+
watch(%r{^spec/.+_spec\.rb$})
|
6
|
+
watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" }
|
7
|
+
watch('spec/spec_helper.rb') { "spec" }
|
8
|
+
end
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2014 Jeremy Ward
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,90 @@
|
|
1
|
+
# Euclidean
|
2
|
+
|
3
|
+
Euclidean adds classes and methods to handle basic geometry in Ruby.
|
4
|
+
|
5
|
+
The classes in this library are based on the Vector class provided in the Ruby Standard Library.
|
6
|
+
Geometric primitives are generally assumed to lie in 2-Dimensional space but are not necessarily
|
7
|
+
restricted to it.
|
8
|
+
|
9
|
+
Like Euclid himself, the work in this gem builds upon the work of others. Most notably:
|
10
|
+
* [geometry](https://github.com/bfoz/geometry)
|
11
|
+
* [ruby-geometry](https://github.com/DanielVartanov/ruby-geometry)
|
12
|
+
|
13
|
+
## Installation
|
14
|
+
|
15
|
+
Add this line to your application's Gemfile:
|
16
|
+
|
17
|
+
gem 'euclidean'
|
18
|
+
|
19
|
+
And then execute:
|
20
|
+
|
21
|
+
$ bundle
|
22
|
+
|
23
|
+
Or install it yourself as:
|
24
|
+
|
25
|
+
$ gem install euclidean
|
26
|
+
|
27
|
+
## Defined Geometric Primitives
|
28
|
+
* Circle
|
29
|
+
* Edge
|
30
|
+
* Point
|
31
|
+
* Rectangle
|
32
|
+
* Size
|
33
|
+
* Vector
|
34
|
+
|
35
|
+
## Usage
|
36
|
+
|
37
|
+
### Point
|
38
|
+
```ruby
|
39
|
+
point = Euclidean::Point[3,4] # 2D Point at coordinate 3, 4
|
40
|
+
|
41
|
+
# Copy constructors
|
42
|
+
point2 = Euclidean::Point[point]
|
43
|
+
point2 = Euclidean::Point[Vector[5,6]]
|
44
|
+
|
45
|
+
# Accessors
|
46
|
+
point.x
|
47
|
+
point.y
|
48
|
+
point[2] # Same as point.z
|
49
|
+
|
50
|
+
# Zero
|
51
|
+
Euclidean::PointZero.new # A Point full of zeros of unspecified length
|
52
|
+
Euclidean::Point.zero # Another way to do the same thing
|
53
|
+
Euclidean::Point.zero(3) # => Point[0,0,0]
|
54
|
+
```
|
55
|
+
|
56
|
+
### Circle
|
57
|
+
```ruby
|
58
|
+
# A circle at Point[1,2] with a radius of 3
|
59
|
+
circle = Euclidean::Circle.new [1,2], 3
|
60
|
+
circle = Euclidean::Circle.new center:[1,2], radius: 3
|
61
|
+
|
62
|
+
# A circle at Point[1,2] with a diameter of 3
|
63
|
+
circle = Euclidean::Circle.new center:[1,2], diameter: 3
|
64
|
+
|
65
|
+
# A circle at Point[0,0] with a diameter of 4
|
66
|
+
circle = Euclidean::Circle.new diameter: 4
|
67
|
+
```
|
68
|
+
|
69
|
+
### Rectangle
|
70
|
+
```ruby
|
71
|
+
# A Rectangle made from two corner points
|
72
|
+
rectangle = Euclidean::Rectangle.new [1,2], [2,3]
|
73
|
+
rectangle = Euclidean::Rectangle.new from:[1,2], to:[2,3]
|
74
|
+
|
75
|
+
rectangle = Euclidean::Rectangle.new center:[1,2], size:[1,1] # Using a center point and a size
|
76
|
+
rectangle = Euclidean::Rectangle.new origin:[1,2], size:[1,1] # Using an origin point and a size
|
77
|
+
|
78
|
+
# A Rectangle with its origin at [0, 0] and a size of [10, 20]
|
79
|
+
rectangle = Euclidean::Rectangle.new size: [10, 20]
|
80
|
+
rectangle = Euclidean::Rectangle.new size: Euclidean::Size[10, 20]
|
81
|
+
rectangle = Euclidean::Rectangle.new width: 10, height: 20
|
82
|
+
```
|
83
|
+
|
84
|
+
## Contributing
|
85
|
+
|
86
|
+
1. Fork it ( http://github.com/<my-github-username>/euclidean/fork )
|
87
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
88
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
89
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
90
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
data/euclidean.gemspec
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'euclidean/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "euclidean"
|
8
|
+
spec.version = Euclidean::VERSION
|
9
|
+
spec.authors = ["Jeremy Ward"]
|
10
|
+
spec.email = ["jeremy.ward@digital-ocd.com"]
|
11
|
+
spec.summary = %q{Basic Geometric primitives and algoritms for Ruby}
|
12
|
+
spec.description = %q{Basic Geometric primitives and algoritms for Ruby}
|
13
|
+
spec.homepage = "https://github.com/jrmyward/euclidean"
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0")
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_development_dependency "bundler", "~> 1.5"
|
22
|
+
spec.add_development_dependency "rake"
|
23
|
+
spec.add_development_dependency 'rspec'
|
24
|
+
end
|
data/lib/euclidean.rb
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
require 'euclidean/cluster_factory'
|
2
|
+
require 'euclidean/circle'
|
3
|
+
require 'euclidean/point'
|
4
|
+
require 'euclidean/point_zero'
|
5
|
+
require 'euclidean/rectangle'
|
6
|
+
require 'euclidean/size'
|
7
|
+
require "euclidean/vector"
|
8
|
+
require "euclidean/version"
|
9
|
+
|
10
|
+
module Euclidean
|
11
|
+
|
12
|
+
end
|
@@ -0,0 +1,118 @@
|
|
1
|
+
module Euclidean
|
2
|
+
=begin rdoc
|
3
|
+
Circles come in all shapes and sizes, but they're usually round.
|
4
|
+
|
5
|
+
== Usage
|
6
|
+
circle = Euclidean::Circle.new [1,2], 3
|
7
|
+
circle = Euclidean::Circle.new center:[1,2], radius:3
|
8
|
+
circle = Euclidean::Circle.new center:[1,2], diameter:6
|
9
|
+
circle = Euclidean::Circle.new diameter:6
|
10
|
+
=end
|
11
|
+
|
12
|
+
class Circle
|
13
|
+
include ClusterFactory
|
14
|
+
|
15
|
+
# @return [Point] The {Circle}'s center point
|
16
|
+
attr_reader :center
|
17
|
+
# @return [Number] The {Circle}'s radius
|
18
|
+
attr_reader :radius
|
19
|
+
|
20
|
+
# @overload new(center, radius)
|
21
|
+
# Construct a {Circle} using a centerpoint and radius
|
22
|
+
# @param [Point] center The center point of the {Circle}
|
23
|
+
# @param [Number] radius The radius of the {Circle}
|
24
|
+
# @overload new(center, radius)
|
25
|
+
# Construct a circle using named center and radius parameters
|
26
|
+
# @option options [Point] :center (PointZero)
|
27
|
+
# @option options [Number] :radius
|
28
|
+
# @overload new(center, diameter)
|
29
|
+
# Construct a circle using named center and diameter parameters
|
30
|
+
# @option options [Point] :center (PointZero)
|
31
|
+
# @option options [Number] :diameter
|
32
|
+
def self.new(*args, &block)
|
33
|
+
options, args = args.partition {|a| a.is_a? Hash}
|
34
|
+
options = options.reduce({}, :merge)
|
35
|
+
center, radius = args[0..1]
|
36
|
+
|
37
|
+
center ||= (options[:center] || PointZero.new)
|
38
|
+
radius ||= options[:radius]
|
39
|
+
|
40
|
+
if radius
|
41
|
+
self.allocate.tap {|circle| circle.send :initialize, center, radius, &block }
|
42
|
+
elsif options.has_key?(:diameter)
|
43
|
+
CenterDiameterCircle.new center, options[:diameter], &block
|
44
|
+
else
|
45
|
+
raise ArgumentError, "Circle.new requires a radius or a diameter"
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
# Construct a new {Circle} from a centerpoint and radius
|
50
|
+
# @param [Point] center The center point of the {Circle}
|
51
|
+
# @param [Number] radius The radius of the {Circle}
|
52
|
+
# @return [Circle] A new {Circle} object
|
53
|
+
def initialize(center, radius)
|
54
|
+
@center = Point[center]
|
55
|
+
@radius = radius
|
56
|
+
end
|
57
|
+
|
58
|
+
def eql?(other)
|
59
|
+
(self.center == other.center) && (self.radius == other.radius)
|
60
|
+
end
|
61
|
+
alias :== :eql?
|
62
|
+
|
63
|
+
# @!group Accessors
|
64
|
+
# @return [Rectangle] The smallest axis-aligned {Rectangle} that bounds the receiver
|
65
|
+
def bounds
|
66
|
+
return Euclidean::Rectangle.new(self.min, self.max)
|
67
|
+
end
|
68
|
+
|
69
|
+
# @!attribute [r] diameter
|
70
|
+
# @return [Numeric] The diameter of the {Circle}
|
71
|
+
def diameter
|
72
|
+
@radius*2
|
73
|
+
end
|
74
|
+
|
75
|
+
# @return [Point] The upper right corner of the bounding {Rectangle}
|
76
|
+
def max
|
77
|
+
@center+radius
|
78
|
+
end
|
79
|
+
|
80
|
+
# @return [Point] The lower left corner of the bounding {Rectangle}
|
81
|
+
def min
|
82
|
+
@center-radius
|
83
|
+
end
|
84
|
+
|
85
|
+
# @return [Array<Point>] The lower left and upper right corners of the bounding {Rectangle}
|
86
|
+
def minmax
|
87
|
+
[self.min, self.max]
|
88
|
+
end
|
89
|
+
# @!endgroup
|
90
|
+
end
|
91
|
+
|
92
|
+
class CenterDiameterCircle < Circle
|
93
|
+
# @return [Number] The {Circle}'s diameter
|
94
|
+
attr_reader :diameter
|
95
|
+
|
96
|
+
# Construct a new {Circle} from a centerpoint and a diameter
|
97
|
+
# @param [Point] center The center point of the {Circle}
|
98
|
+
# @param [Number] diameter The radius of the {Circle}
|
99
|
+
# @return [Circle] A new {Circle} object
|
100
|
+
def initialize(center, diameter)
|
101
|
+
@center = Point[center]
|
102
|
+
@diameter = diameter
|
103
|
+
end
|
104
|
+
|
105
|
+
def eql?(other)
|
106
|
+
(self.center == other.center) && (self.diameter == other.diameter)
|
107
|
+
end
|
108
|
+
alias :== :eql?
|
109
|
+
|
110
|
+
# @!group Accessors
|
111
|
+
# @return [Number] The {Circle}'s radius
|
112
|
+
def radius
|
113
|
+
@diameter/2
|
114
|
+
end
|
115
|
+
# @!endgroup
|
116
|
+
end
|
117
|
+
|
118
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# Include this module in the base class of a class cluster to handle swizzling
|
2
|
+
# of ::new
|
3
|
+
module ClusterFactory
|
4
|
+
def self.included(parent)
|
5
|
+
class << parent
|
6
|
+
alias :original_new :new
|
7
|
+
|
8
|
+
def inherited(subclass)
|
9
|
+
class << subclass
|
10
|
+
alias :new :original_new
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,143 @@
|
|
1
|
+
require 'mathn'
|
2
|
+
require_relative 'point'
|
3
|
+
|
4
|
+
module Euclidean
|
5
|
+
=begin rdoc
|
6
|
+
An edge. It's a line segment between 2 points. Generally part of a {Polygon}.
|
7
|
+
|
8
|
+
== Usage
|
9
|
+
edge = Geometry::Edge([1,1], [2,2])
|
10
|
+
|
11
|
+
=end
|
12
|
+
class Edge
|
13
|
+
attr_reader :first, :last
|
14
|
+
|
15
|
+
# Construct a new {Edge} object from any two things that can be converted
|
16
|
+
# to a {Point}.
|
17
|
+
def initialize(point0, point1)
|
18
|
+
@first, @last = [Point[point0], Point[point1]]
|
19
|
+
end
|
20
|
+
|
21
|
+
# Two Edges are equal if both have equal {Point}s in the same order
|
22
|
+
def ==(other)
|
23
|
+
(@first == other.first) && (@last == other.last)
|
24
|
+
end
|
25
|
+
|
26
|
+
# @param [Point] point A {Point} to spaceship with
|
27
|
+
# @return [Boolean] Returns 1 if the {Point} is strictly to the left of the receiver, -1 to the right, and 0 if the point is on the receiver
|
28
|
+
def <=>(point)
|
29
|
+
case point
|
30
|
+
when Point
|
31
|
+
k = (@last.x - @first.x) * (point.y - @first.y) - (point.x - @first.x) * (@last.y - @first.y)
|
32
|
+
if 0 == k
|
33
|
+
(((@first.x <=> point.x) + (@last.x <=> point.x)).abs <= 1) && (((@first.y <=> point.y) + (@last.y <=> point.y)).abs <= 1) ? 0 : nil
|
34
|
+
else
|
35
|
+
k <=> 0
|
36
|
+
end
|
37
|
+
else
|
38
|
+
raise ArgumentError, "Can't spaceship with #{point.class}"
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# @param [Edge] other The other {Edge} to check
|
43
|
+
# @return [Bool] Returns true if the receiver and the passed {Edge} share an endpoint
|
44
|
+
def connected?(other)
|
45
|
+
(@first == other.last) || (@last == other.first) || (@first == other.first) || (@last == other.last)
|
46
|
+
end
|
47
|
+
|
48
|
+
# @return [Vector] A unit {Vector} pointing from first to last
|
49
|
+
def direction
|
50
|
+
self.vector.normalize
|
51
|
+
end
|
52
|
+
|
53
|
+
# Return the {Edge}'s length along the Y axis
|
54
|
+
def height
|
55
|
+
(@first.y - @last.y).abs
|
56
|
+
end
|
57
|
+
|
58
|
+
def inspect
|
59
|
+
'Edge(' + @first.inspect + ', ' + @last.inspect + ')'
|
60
|
+
end
|
61
|
+
alias :to_s :inspect
|
62
|
+
|
63
|
+
# Find the intersection of two {Edge}s (http://bloggingmath.wordpress.com/2009/05/29/line-segment-intersection/)
|
64
|
+
# @param [Edge] other The other {Edge}
|
65
|
+
# @return [Point] The intersection of the two {Edge}s, nil if they don't intersect, true if they're collinear and overlapping, and false if they're collinear and non-overlapping
|
66
|
+
def intersection(other)
|
67
|
+
return self.first if (self.first == other.first) or (self.first == other.last)
|
68
|
+
return self.last if (self.last == other.first) or (self.last == other.last)
|
69
|
+
|
70
|
+
p0, p1 = self.first, self.last
|
71
|
+
p2, p3 = other.first, other.last
|
72
|
+
v1, v2 = self.vector, other.vector
|
73
|
+
|
74
|
+
denominator = v1[0] * v2[1] - v2[0] * v1[1] # v1 x v2
|
75
|
+
p = p0 - p2
|
76
|
+
|
77
|
+
if denominator == 0 # collinear, so check for overlap
|
78
|
+
if 0 == (-v1[1] * p.x + v1[0] * p.y) # collinear?
|
79
|
+
# The edges are collinear, but do they overlap?
|
80
|
+
# Project them onto the x and y axes to find out
|
81
|
+
left1, right1 = [self.first[0], self.last[0]].sort
|
82
|
+
bottom1, top1 = [self.first[1], self.last[1]].sort
|
83
|
+
left2, right2 = [other.first[0], other.last[0]].sort
|
84
|
+
bottom2, top2 = [other.first[1], other.last[1]].sort
|
85
|
+
|
86
|
+
!((left2 > right1) || (right2 < left1) || (top2 < bottom1) || (bottom2 > top1))
|
87
|
+
else
|
88
|
+
nil
|
89
|
+
end
|
90
|
+
else
|
91
|
+
s = (-v1[1] * p.x + v1[0] * p.y) / denominator # v1 x (p0 - p2) / denominator
|
92
|
+
t = ( v2[0] * p.y - v2[1] * p.x) / denominator # v2 x (p0 - p2) / denominator
|
93
|
+
|
94
|
+
p0 + v1 * t if ((0..1) === s) && ((0..1) === t)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
# @return [Bool] Returns true if the passed {Edge} is parallel to the receiver
|
99
|
+
def parallel?(edge)
|
100
|
+
v1, v2 = self.direction, edge.direction
|
101
|
+
winding = v1[0]*v2[1] - v1[1]*v2[0]
|
102
|
+
if 0 == winding # collinear?
|
103
|
+
if v1 == v2
|
104
|
+
true # 1 # same direction
|
105
|
+
else
|
106
|
+
true # -1 # opposite direction
|
107
|
+
end
|
108
|
+
else
|
109
|
+
false
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
# Return a new {Edge} with swapped endpoints
|
114
|
+
def reverse
|
115
|
+
Edge.new(@last, @first)
|
116
|
+
end
|
117
|
+
|
118
|
+
# In-place swap the endpoints
|
119
|
+
def reverse!
|
120
|
+
@first, @last = @last, @first
|
121
|
+
self
|
122
|
+
end
|
123
|
+
|
124
|
+
def to_a
|
125
|
+
[@first, @last]
|
126
|
+
end
|
127
|
+
|
128
|
+
# @return [Vector] A {Vector} pointing from first to last
|
129
|
+
def vector
|
130
|
+
Vector[*((last-first).to_a)]
|
131
|
+
end
|
132
|
+
|
133
|
+
# Return the {Edge}'s length along the X axis
|
134
|
+
def width
|
135
|
+
(@first.x - @last.x).abs
|
136
|
+
end
|
137
|
+
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
def Edge(*args)
|
142
|
+
Euclidean::Edge.new(*args)
|
143
|
+
end
|