euclidean 0.1.0
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 +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
|