├── .github └── workflows │ └── ruby.yml ├── .gitignore ├── Gemfile ├── LICENSE ├── README.markdown ├── Rakefile ├── geometry.gemspec ├── lib ├── geometry.rb └── geometry │ ├── annulus.rb │ ├── arc.rb │ ├── bezier.rb │ ├── circle.rb │ ├── cluster_factory.rb │ ├── edge.rb │ ├── line.rb │ ├── obround.rb │ ├── path.rb │ ├── point.rb │ ├── point_iso.rb │ ├── point_one.rb │ ├── point_zero.rb │ ├── polygon.rb │ ├── polyline.rb │ ├── rectangle.rb │ ├── regular_polygon.rb │ ├── rotation.rb │ ├── size.rb │ ├── size_one.rb │ ├── size_zero.rb │ ├── square.rb │ ├── transformation.rb │ ├── transformation │ └── composition.rb │ ├── triangle.rb │ └── vector.rb └── test ├── geometry.rb └── geometry ├── annulus.rb ├── arc.rb ├── bezier.rb ├── circle.rb ├── edge.rb ├── line.rb ├── obround.rb ├── path.rb ├── point.rb ├── point_iso.rb ├── point_one.rb ├── point_zero.rb ├── polygon.rb ├── polyline.rb ├── rectangle.rb ├── regular_polygon.rb ├── rotation.rb ├── size.rb ├── size_one.rb ├── size_zero.rb ├── square.rb ├── transformation.rb ├── transformation └── composition.rb ├── triangle.rb └── vector.rb /.github/workflows/ruby.yml: -------------------------------------------------------------------------------- 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 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | .bundle 3 | .yardoc 4 | Gemfile.lock 5 | pkg/* 6 | doc 7 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | 3 | gemspec 4 | 5 | group :test do 6 | gem 'minitest' 7 | gem 'rake' 8 | end 9 | 10 | gem "matrix", "~> 0.4.2" 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012, Brandon Fosdick 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, are permitted provided 5 | that the following conditions are met: 6 | 7 | Redistributions of source code must retain the above copyright notice, this list of conditions and the 8 | following disclaimer. 9 | Redistributions in binary form must reproduce the above copyright notice, this list of conditions and 10 | the following disclaimer in the documentation and/or other materials provided with the distribution. 11 | 12 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 13 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 14 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 15 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 16 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 17 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 18 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 19 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR 20 | TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 21 | THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 22 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | Geometry for Ruby 2 | ================= 3 | 4 | [![Gem Version](https://badge.fury.io/rb/geometry.svg)](http://badge.fury.io/rb/geometry) 5 | 6 | Classes and methods for the handling of all of the basic geometry that you 7 | learned in high school (and then forgot). 8 | 9 | The classes in this libary are based on the Vector class provided by the Ruby 10 | standard library. Geometric primitives are generally assumed to lie in 2D space, 11 | but aren't necessarily restricted to it. Please let me know if you find cases 12 | that don't work in higher dimensions and I'll do my best to fix them. 13 | 14 | License 15 | ------- 16 | 17 | Copyright 2012-2024 Brandon Fosdick and released under the BSD license. 18 | 19 | Primitives 20 | ---------- 21 | 22 | - Point 23 | - Size 24 | - Line 25 | - Edge 26 | - [Annulus](http://en.wikipedia.org/wiki/Annulus_(mathematics)) 27 | - [Arc](http://en.wikipedia.org/wiki/Arc_(geometry)), Circle 28 | - [Bézier Curves](http://en.wikipedia.org/wiki/Bézier_curve) 29 | - Rectangle, Square 30 | - Path, [Polyline](http://en.wikipedia.org/wiki/Polyline), [Polygon](http://en.wikipedia.org/wiki/Polygon), [RegularPolygon](http://en.wikipedia.org/wiki/Regular_polygon) 31 | - Transformation 32 | - [Triangle](http://en.wikipedia.org/wiki/Triangle) 33 | - [Obround](http://en.wiktionary.org/wiki/obround) 34 | 35 | Examples 36 | -------- 37 | 38 | ### Point 39 | ```ruby 40 | point = Geometry::Point[3,4] # 2D Point at coordinate 3, 4 41 | 42 | # Copy constructors 43 | point2 = Geometry::Point[point] 44 | point2 = Geometry::Point[Vector[5,6]] 45 | 46 | # Accessors 47 | point.x 48 | point.y 49 | point[2] # Same as point.z 50 | 51 | # Zero 52 | PointZero.new # A Point full of zeros of unspecified length 53 | Point.zero # Another way to do the same thing 54 | Point.zero(3) # => Point[0,0,0] 55 | ``` 56 | 57 | ### Line 58 | ```ruby 59 | # Two-point constructors 60 | line = Geometry::Line[[0,0], [10,10]] 61 | line = Geometry::Line[Geometry::Point[0,0], Geometry::Point[10,10]] 62 | line = Geometry::Line[Vector[0,0], Vector[10,10]] 63 | 64 | # Slope-intercept constructors 65 | Geometry::Line[Rational(3,4), 5] # Slope = 3/4, Intercept = 5 66 | Geometry::Line[0.75, 5] 67 | 68 | # Point-slope constructors 69 | Geometry::Line(Geometry::Point[0,0], 0.75) 70 | Geometry::Line(Vector[0,0], Rational(3,4)) 71 | 72 | # Special constructors (2D only) 73 | Geometry::Line.horizontal(y=0) 74 | Geometry::Line.vertical(x=0) 75 | ``` 76 | 77 | ### Rectangle 78 | ```ruby 79 | # A Rectangle made from two corner points 80 | Geometry::Rectangle.new [1,2], [2,3] 81 | Geometry::Rectangle.new from:[1,2], to:[2,3] 82 | 83 | Geometry::Rectangle.new center:[1,2], size:[1,1] # Using a center point and a size 84 | Geometry::Rectangle.new origin:[1,2], size:[1,1] # Using an origin point and a size 85 | 86 | # A Rectangle with its origin at [0, 0] and a size of [10, 20] 87 | Geometry::Rectangle.new size: [10, 20] 88 | Geometry::Rectangle.new size: Size[10, 20] 89 | Geometry::Rectangle.new width: 10, height: 20 90 | ``` 91 | 92 | ### Circle 93 | ```ruby 94 | # A circle at Point[1,2] with a radius of 3 95 | circle = Geometry::Circle.new center:[1,2], radius:3 96 | ``` 97 | 98 | ### Polygon 99 | ```ruby 100 | # A polygon that looks a lot like a square 101 | polygon = Geometry::Polygon.new [0,0], [1,0], [1,1], [0,1] 102 | ``` 103 | ### Regular Polygon 104 | ```ruby 105 | # Everyone loves a good hexagon 106 | hexagon = Geometry::RegularPolygon.new 6, :diameter => 3 107 | ``` 108 | 109 | ### Zeros and Ones 110 | ```ruby 111 | # For when you know you need a zero, but you don't know how big it should be 112 | zero = Point.zero # Returns a Point of indeterminate length that always compares equal to zero 113 | 114 | # Oh, you wanted ones instead? No problem. 115 | ones = Point.one # => Point[1,1,1...1] 116 | 117 | # Looking for something more exotic that a mere 1? 118 | iso = Point.iso(5) # => Point[5,5,5...5] 119 | ``` 120 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require 'rake/testtask' 3 | 4 | task :default => :test 5 | 6 | Rake::TestTask.new do |t| 7 | t.libs.push "lib" 8 | t.test_files = FileList['test/**/*.rb'] 9 | t.verbose = true 10 | end 11 | 12 | task :fixdates do 13 | branch = `git branch --no-color -r --merged`.strip 14 | `git fix-dates #{branch}..HEAD` 15 | end 16 | 17 | task :fixdates_f do 18 | branch = `git branch --no-color -r --merged`.strip 19 | `git fix-dates -f #{branch}..HEAD` 20 | end 21 | 22 | task :trim_whitespace do 23 | system(%Q[git status --short | awk '{if ($1 != "D" && $1 != "R") print $2}' | grep -e '.*\.rb$' | xargs sed -i '' -e 's/[ \t]*$//g;']) 24 | end 25 | -------------------------------------------------------------------------------- /geometry.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | $:.push File.expand_path("../lib", __FILE__) 3 | 4 | Gem::Specification.new do |s| 5 | spec = s 6 | s.name = "geometry" 7 | spec.version = '6.6' 8 | s.authors = ["Brandon Fosdick"] 9 | s.email = ["bfoz@bfoz.net"] 10 | s.homepage = "http://github.com/bfoz/geometry" 11 | s.summary = %q{Geometric primitives and algoritms} 12 | s.description = %q{Geometric primitives and algorithms for Ruby} 13 | 14 | s.rubyforge_project = "geometry" 15 | 16 | s.files = `git ls-files`.split("\n") 17 | s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") 18 | s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } 19 | s.require_paths = ["lib"] 20 | 21 | s.required_ruby_version = '>= 2.0' 22 | 23 | spec.add_development_dependency "bundler", "~> 2" 24 | spec.add_development_dependency "rake", "~> 13" 25 | end 26 | -------------------------------------------------------------------------------- /lib/geometry.rb: -------------------------------------------------------------------------------- 1 | require_relative 'geometry/annulus' 2 | require_relative 'geometry/arc' 3 | require_relative 'geometry/circle' 4 | require_relative 'geometry/line' 5 | require_relative 'geometry/obround' 6 | require_relative 'geometry/path' 7 | require_relative 'geometry/point' 8 | require_relative 'geometry/point_zero' 9 | require_relative 'geometry/polygon' 10 | require_relative 'geometry/polyline' 11 | require_relative 'geometry/rectangle' 12 | require_relative 'geometry/regular_polygon' 13 | require_relative 'geometry/rotation' 14 | require_relative 'geometry/size' 15 | require_relative 'geometry/size_zero' 16 | require_relative 'geometry/square' 17 | require_relative 'geometry/transformation' 18 | require_relative 'geometry/triangle' 19 | require_relative 'geometry/vector' 20 | 21 | module Geometry 22 | end 23 | -------------------------------------------------------------------------------- /lib/geometry/annulus.rb: -------------------------------------------------------------------------------- 1 | module Geometry 2 | 3 | =begin rdoc 4 | An {http://en.wikipedia.org/wiki/Annulus_(mathematics) Annulus}, more commonly 5 | known as a Ring, is a circle that ate another circle. 6 | 7 | == Usage 8 | ring = Geometry::Annulus.new center:[1,2], inner_radius:5, radius:10 9 | ring = Geometry::Ring.new center:[1,2], inner_radius:5, radius:10 10 | =end 11 | class Annulus 12 | # @!attribute center 13 | # @return [Point] The center point of the {Annulus} 14 | attr_accessor :center 15 | 16 | # @!attribute inner_diameter 17 | # @return [Number] the diameter of the inside of the {Annulus} 18 | def inner_diameter 19 | @inner_diameter || (@inner_radius && 2*@inner_radius) 20 | end 21 | 22 | # @!attribute inner_radius 23 | # @return [Number] the radius of the inside of the {Annulus} 24 | def inner_radius 25 | @inner_radius || (@inner_diameter && @inner_diameter.to_r/2) 26 | end 27 | 28 | # @!attribute outer_diameter 29 | # @return [Number] the out diameter 30 | def outer_diameter 31 | @outer_diameter || (@outer_radius && 2*@outer_radius) 32 | end 33 | 34 | # @!attribute outer_radius 35 | # @return [Number] the outer radius 36 | def outer_radius 37 | @outer_radius || ((@outer_diameter && @outer_diameter).to_r/2) 38 | end 39 | 40 | # @!attribute diameter 41 | # @return [Number] the outer diameter 42 | alias :diameter :outer_diameter 43 | 44 | # @!attribute radius 45 | # @return [Number] the outer radius 46 | alias :radius :outer_radius 47 | 48 | # @note 49 | # The 'center' argument can also be passed as a named argument of the same name 50 | # @overload initialize(center, :inner_radius, :outer_radius) 51 | # @param center [Point] The center {Point}, defaults to the origin 52 | # @param :inner_radius [Number] The radius of the hole that's in the center 53 | # @param :outer_radius [Number] The overall radius of the whole thing 54 | # @overload initialize(center, :inner_diameter, :outer_diameter) 55 | # @param center [Point] The center {Point}, defaults to the origin 56 | # @param :inner_diameter [Number] The radius of the hole that's in the center 57 | # @param :outer_diameter [Number] The overall radius of the whole thing 58 | def initialize(center = Point.zero, **options) 59 | @center = Point[options.fetch(:center, center)] 60 | 61 | options.delete :center 62 | raise ArgumentError, 'Annulus requires more than a center' if options.empty? 63 | 64 | @inner_diameter = options[:inner_diameter] 65 | @inner_radius = options[:inner_radius] 66 | @outer_diameter = options[:outer_diameter] || options[:diameter] 67 | @outer_radius = options[:outer_radius] || options[:radius] 68 | end 69 | 70 | # @!attribute max 71 | # @return [Point] The upper right corner of the bounding {Rectangle} 72 | def max 73 | @center+radius 74 | end 75 | 76 | # @!attribute min 77 | # @return [Point] The lower left corner of the bounding {Rectangle} 78 | def min 79 | @center-radius 80 | end 81 | 82 | # @!attribute minmax 83 | # @return [Array] The lower left and upper right corners of the bounding {Rectangle} 84 | def minmax 85 | [self.min, self.max] 86 | end 87 | end 88 | 89 | # Ring is an alias of Annulus because that's the word that most people use, 90 | # despite the proclivities of mathmeticians. 91 | Ring = Annulus 92 | end 93 | -------------------------------------------------------------------------------- /lib/geometry/arc.rb: -------------------------------------------------------------------------------- 1 | require_relative 'point' 2 | 3 | require_relative 'cluster_factory' 4 | require_relative 'point' 5 | 6 | module Geometry 7 | 8 | =begin rdoc 9 | {http://en.wikipedia.org/wiki/Arc_(geometry) Arcs} are Circles that don't quite go all the way around 10 | 11 | == Usage 12 | An {Arc} with its center at [1,1] and a radius of 2 that starts at the X-axis and goes to the Y-axis (counter-clockwise) 13 | arc = Geometry::Arc.new center:[1,1], radius:2, start:0, end:90 14 | =end 15 | 16 | class Arc 17 | include ClusterFactory 18 | 19 | attr_reader :center 20 | 21 | # @return [Number] the radius of the {Arc} 22 | attr_reader :radius 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 29 | 30 | # @overload new(center, start, end) 31 | # Create a new {Arc} given center, start and end {Point}s 32 | # @option options [Point] :center (PointZero) The {Point} at the center 33 | # @option options [Point] :start The {Arc} starts at the start {Point} 34 | # @option options [Point] :end The {Point} where it all ends 35 | # @return [Arc] 36 | # @overload new(center, radius, start, end) 37 | # Create a new {Arc} given a center {Point}, a radius and start and end angles 38 | # @option options [Point] :center (PointZero) The {Point} at the center of it all 39 | # @option options [Numeric] :radius Radius 40 | # @option options [Numeric] :start Starting angle 41 | # @option options [Numeric] :end Ending angle 42 | # @return [ThreePointArc] 43 | def self.new(options={}) 44 | center = options.delete(:center) || PointZero.new 45 | 46 | if options.has_key?(:radius) 47 | original_new(center, options[:radius], options[:start], options[:end]) 48 | else 49 | ThreePointArc.new(center, options[:start], options[:end]) 50 | end 51 | end 52 | 53 | # Construct a new {Arc} 54 | # @overload initialize(center, radius, start_angle, end_angle) 55 | # @param [Point] center The {Point} at the center of it all 56 | # @param [Numeric] radius Radius 57 | # @param [Numeric] start_angle Starting angle 58 | # @param [Numeric] end_angle Ending angle 59 | def initialize(center, radius, start_angle, end_angle) 60 | @center = Point[center] 61 | @radius = radius 62 | @start_angle = start_angle 63 | @end_angle = end_angle 64 | end 65 | 66 | # @return [Point] The starting point of the {Arc} 67 | def first 68 | @center + @radius * Vector[Math.cos(@start_angle), Math.sin(@start_angle)] 69 | end 70 | 71 | # @return [Point] The end point of the {Arc} 72 | def last 73 | @center + @radius * Vector[Math.cos(@end_angle), Math.sin(@end_angle)] 74 | end 75 | end 76 | 77 | class ThreePointArc < Arc 78 | attr_reader :center 79 | attr_reader :start, :end 80 | 81 | # Contruct a new {Arc} given center, start and end {Point}s 82 | # Always assumes that the {Arc} is counter-clockwise. Reverse the order 83 | # of the start and end points to get an {Arc} that goes around the other way. 84 | # @overload initialize(center_point, start_point, end_point) 85 | # @param [Point] center_point The {Point} at the center 86 | # @param [Point] start_point The {Arc} starts at the start {Point} 87 | # @param [Point] end_point The {Point} where it all ends 88 | def initialize(center_point, start_point, end_point) 89 | @center, @start, @end = [center_point, start_point, end_point].map {|p| Point[p]} 90 | raise ArgumentError unless [@center, @start, @end].all? {|p| p.is_a?(Point)} 91 | end 92 | 93 | # The starting point of the {Arc} 94 | # @return [Point] 95 | alias :first :start 96 | 97 | # The end point of the {Arc} 98 | # @return [Point] 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] 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 164 | end 165 | end 166 | -------------------------------------------------------------------------------- /lib/geometry/bezier.rb: -------------------------------------------------------------------------------- 1 | module Geometry 2 | =begin 3 | Bézier curves are like lines, but curvier. 4 | 5 | http://en.wikipedia.org/wiki/Bézier_curve 6 | 7 | == Constructors 8 | 9 | Bezier.new [0,0], [1,1], [2,2] # From control points 10 | 11 | == Usage 12 | 13 | To get a point on the curve for a particular value of t, you can use the subscript operator 14 | 15 | bezier[0.5] # => [1,1] 16 | =end 17 | class Bezier 18 | # @!attribute degree 19 | # @return [Number] The degree of the curve 20 | def degree 21 | points.length - 1 22 | end 23 | 24 | # @!attribute points 25 | # @return [Array] The control points for the Bézier curve 26 | attr_reader :points 27 | 28 | def initialize(*points) 29 | @points = points.map {|v| Point[v]} 30 | end 31 | 32 | # http://en.wikipedia.org/wiki/Binomial_coefficient 33 | # http://rosettacode.org/wiki/Evaluate_binomial_coefficients#Ruby 34 | def binomial_coefficient(k) 35 | (0...k).inject(1) {|m,i| (m * (degree - i)) / (i + 1) } 36 | end 37 | 38 | # @param t [Float] the input parameter 39 | def [](t) 40 | return nil unless (0..1).include?(t) 41 | result = Point.zero(points.first.size) 42 | points.each_with_index do |v, i| 43 | result += v * binomial_coefficient(i) * ((1 - t) ** (degree - i)) * (t ** i) 44 | end 45 | result 46 | end 47 | end 48 | end -------------------------------------------------------------------------------- /lib/geometry/circle.rb: -------------------------------------------------------------------------------- 1 | require_relative 'cluster_factory' 2 | require_relative 'point' 3 | 4 | module Geometry 5 | 6 | =begin rdoc 7 | Circles come in all shapes and sizes, but they're usually round. 8 | 9 | == Usage 10 | circle = Geometry::Circle.new [1,2], 3 11 | circle = Geometry::Circle.new center:[1,2], radius:3 12 | circle = Geometry::Circle.new center:[1,2], diameter:6 13 | circle = Geometry::Circle.new diameter:6 14 | =end 15 | 16 | class Circle 17 | include ClusterFactory 18 | 19 | # @return [Point] The {Circle}'s center point 20 | attr_reader :center 21 | 22 | # @return [Number] The {Circle}'s radius 23 | attr_reader :radius 24 | 25 | # @overload new(center, radius) 26 | # Construct a {Circle} using a centerpoint and radius 27 | # @param [Point] center The center point of the {Circle} 28 | # @param [Number] radius The radius of the {Circle} 29 | # @overload new(center, radius) 30 | # Construct a circle using named center and radius parameters 31 | # @option options [Point] :center (PointZero) 32 | # @option options [Number] :radius 33 | # @overload new(center, diameter) 34 | # Construct a circle using named center and diameter parameters 35 | # @option options [Point] :center (PointZero) 36 | # @option options [Number] :diameter 37 | def self.new(*args, &block) 38 | options, args = args.partition {|a| a.is_a? Hash} 39 | options = options.reduce({}, :merge) 40 | center, radius = args[0..1] 41 | 42 | center ||= (options[:center] || PointZero.new) 43 | radius ||= options[:radius] 44 | 45 | if radius 46 | self.allocate.tap {|circle| circle.send :initialize, center, radius, &block } 47 | elsif options.has_key?(:diameter) 48 | CenterDiameterCircle.new center, options[:diameter], &block 49 | else 50 | raise ArgumentError, "Circle.new requires a radius or a diameter" 51 | end 52 | end 53 | 54 | # Construct a new {Circle} from a centerpoint and radius 55 | # @param [Point] center The center point of the {Circle} 56 | # @param [Number] radius The radius of the {Circle} 57 | # @return [Circle] A new {Circle} object 58 | def initialize(center, radius) 59 | @center = Point[center] 60 | @radius = radius 61 | end 62 | 63 | def eql?(other) 64 | (self.center == other.center) && (self.radius == other.radius) 65 | end 66 | alias :== :eql? 67 | 68 | # @!group Accessors 69 | # @return [Rectangle] The smallest axis-aligned {Rectangle} that bounds the receiver 70 | def bounds 71 | return Rectangle.new(self.min, self.max) 72 | end 73 | 74 | # @!attribute closed? 75 | # @return [Bool] always true 76 | def closed? 77 | true 78 | end 79 | 80 | # @!attribute [r] diameter 81 | # @return [Numeric] The diameter of the {Circle} 82 | def diameter 83 | @radius*2 84 | end 85 | 86 | # @return [Point] The upper right corner of the bounding {Rectangle} 87 | def max 88 | @center+radius 89 | end 90 | 91 | # @return [Point] The lower left corner of the bounding {Rectangle} 92 | def min 93 | @center-radius 94 | end 95 | 96 | # @return [Array] The lower left and upper right corners of the bounding {Rectangle} 97 | def minmax 98 | [self.min, self.max] 99 | end 100 | # @!endgroup 101 | end 102 | 103 | class CenterDiameterCircle < Circle 104 | # @return [Number] The {Circle}'s diameter 105 | attr_reader :diameter 106 | 107 | # Construct a new {Circle} from a centerpoint and a diameter 108 | # @param [Point] center The center point of the {Circle} 109 | # @param [Number] diameter The radius of the {Circle} 110 | # @return [Circle] A new {Circle} object 111 | def initialize(center, diameter) 112 | @center = Point[center] 113 | @diameter = diameter 114 | end 115 | 116 | def eql?(other) 117 | (self.center == other.center) && (self.diameter == other.diameter) 118 | end 119 | alias :== :eql? 120 | 121 | # @!group Accessors 122 | # @return [Number] The {Circle}'s radius 123 | def radius 124 | @diameter/2 125 | end 126 | # @!endgroup 127 | end 128 | end 129 | -------------------------------------------------------------------------------- /lib/geometry/cluster_factory.rb: -------------------------------------------------------------------------------- 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 16 | -------------------------------------------------------------------------------- /lib/geometry/edge.rb: -------------------------------------------------------------------------------- 1 | require_relative 'point' 2 | 3 | module Geometry 4 | 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.new([1,1], [2,2]) 10 | edge = Geometry::Edge([1,1], [2,2]) 11 | =end 12 | 13 | class Edge 14 | attr_reader :first, :last 15 | 16 | # Construct a new {Edge} object from any two things that can be converted 17 | # to a {Point}. 18 | def initialize(point0, point1) 19 | @first, @last = [Point[point0], Point[point1]] 20 | end 21 | 22 | # Two Edges are equal if both have equal {Point}s in the same order 23 | def ==(other) 24 | (@first == other.first) && (@last == other.last) 25 | end 26 | 27 | # @param [Point] point A {Point} to spaceship with 28 | # @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 29 | def <=>(point) 30 | case point 31 | when Point 32 | k = (@last.x - @first.x) * (point.y - @first.y) - (point.x - @first.x) * (@last.y - @first.y) 33 | if 0 == k 34 | (((@first.x <=> point.x) + (@last.x <=> point.x)).abs <= 1) && (((@first.y <=> point.y) + (@last.y <=> point.y)).abs <= 1) ? 0 : nil 35 | else 36 | k <=> 0 37 | end 38 | else 39 | raise ArgumentError, "Can't spaceship with #{point.class}" 40 | end 41 | end 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] 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 | 62 | # Return a new {Edge} with swapped endpoints 63 | def reverse 64 | self.class.new(@last, @first) 65 | end 66 | 67 | # In-place swap the endpoints 68 | def reverse! 69 | @first, @last = @last, @first 70 | self 71 | end 72 | 73 | # @return [Number] the length of the {Edge} 74 | def length 75 | @length ||= vector.magnitude 76 | end 77 | 78 | # Return the {Edge}'s length along the Y axis 79 | def height 80 | (@first.y - @last.y).abs 81 | end 82 | 83 | # Return the {Edge}'s length along the X axis 84 | def width 85 | (@first.x - @last.x).abs 86 | end 87 | 88 | def inspect 89 | 'Edge(' + @first.inspect + ', ' + @last.inspect + ')' 90 | end 91 | alias :to_s :inspect 92 | 93 | # @return [Bool] Returns true if the passed {Edge} is parallel to the receiver 94 | def parallel?(edge) 95 | v1 = self.direction 96 | v2 = edge.direction 97 | winding = v1[0]*v2[1] - v1[1]*v2[0] 98 | if 0 == winding # collinear? 99 | if v1 == v2 100 | 1 # same direction 101 | else 102 | -1 # opposite direction 103 | end 104 | else 105 | false 106 | end 107 | end 108 | 109 | # @param [Edge] other The other {Edge} to check 110 | # @return [Bool] Returns true if the receiver and the passed {Edge} share an endpoint 111 | def connected?(other) 112 | (@first == other.last) || (@last == other.first) || (@first == other.first) || (@last == other.last) 113 | end 114 | 115 | # @!attribute [r] direction 116 | # @return [Vector] A unit {Vector} pointing from first to last 117 | def direction 118 | @direction ||= self.vector.normalize 119 | end 120 | 121 | # Find the intersection of two {Edge}s (http://bloggingmath.wordpress.com/2009/05/29/line-segment-intersection/) 122 | # @param [Edge] other The other {Edge} 123 | # @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 124 | def intersection(other) 125 | return self.first if (self.first == other.first) or (self.first == other.last) 126 | return self.last if (self.last == other.first) or (self.last == other.last) 127 | 128 | p0, p1 = self.first, self.last 129 | p2, p3 = other.first, other.last 130 | v1, v2 = self.vector, other.vector 131 | 132 | denominator = v1[0] * v2[1] - v2[0] * v1[1] # v1 x v2 133 | p = p0 - p2 134 | if denominator == 0 # collinear, so check for overlap 135 | if 0 == (-v1[1] * p.x + v1[0] * p.y) # collinear? 136 | # The edges are collinear, but do they overlap? 137 | # Project them onto the x and y axes to find out 138 | left1, right1 = [self.first[0], self.last[0]].sort 139 | bottom1, top1 = [self.first[1], self.last[1]].sort 140 | left2, right2 = [other.first[0], other.last[0]].sort 141 | bottom2, top2 = [other.first[1], other.last[1]].sort 142 | 143 | !((left2 > right1) || (right2 < left1) || (top2 < bottom1) || (bottom2 > top1)) 144 | else 145 | nil 146 | end 147 | else 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 150 | 151 | p0 + v1 * t if ((0..1) === s) && ((0..1) === t) 152 | end 153 | end 154 | 155 | # @!attribute [r] vector 156 | # @return [Vector] A {Vector} pointing from first to last 157 | def vector 158 | @vector ||= last - first 159 | end 160 | 161 | def to_a 162 | [@first, @last] 163 | end 164 | end 165 | 166 | # Convenience initializer for {Edge} that tries to coerce its arguments into 167 | # something useful 168 | # @param first [Point, Array] the starting point of the {Edge} 169 | # @param last [Point, Array] the endpoint of the {Edge} 170 | def Edge(first, last) 171 | Edge.new(first, last) 172 | end 173 | end 174 | -------------------------------------------------------------------------------- /lib/geometry/line.rb: -------------------------------------------------------------------------------- 1 | require_relative 'cluster_factory' 2 | require_relative 'point' 3 | 4 | module Geometry 5 | 6 | =begin rdoc 7 | A cluster of objects representing a Line of infinite length 8 | 9 | Supports two-point, slope-intercept, and point-slope initializer forms 10 | 11 | == Usage 12 | 13 | === Two-point constructors 14 | line = Geometry::Line[[0,0], [10,10]] 15 | line = Geometry::Line[Geometry::Point[0,0], Geometry::Point[10,10]] 16 | line = Geometry::Line[Vector[0,0], Vector[10,10]] 17 | 18 | === Slope-intercept constructors 19 | Geometry::Line[Rational(3,4), 5] # Slope = 3/4, Intercept = 5 20 | Geometry::Line[0.75, 5] 21 | 22 | === Point-slope constructors 23 | Geometry::Line(Geometry::Point[0,0], 0.75) 24 | Geometry::Line(Vector[0,0], Rational(3,4)) 25 | 26 | === Special constructors (2D only) 27 | Geometry::Line.horizontal(y=0) 28 | Geometry::Line.vertical(x=0) 29 | =end 30 | 31 | class Line 32 | include ClusterFactory 33 | 34 | # @!attribute [r] horizontal? 35 | # @return [Boolean] true if the slope is zero 36 | 37 | # @!attribute [r] slope 38 | # @return [Number] the slope of the {Line} 39 | 40 | # @!attribute [r] vertical? 41 | # @return [Boolean] true if the slope is infinite 42 | 43 | # @overload [](Array, Array) 44 | # @return [TwoPointLine] 45 | # @overload [](Point, Point) 46 | # @return [TwoPointLine] 47 | # @overload [](Vector, Vector) 48 | # @return [TwoPointLine] 49 | # @overload [](y-intercept, slope) 50 | # @return [SlopeInterceptLine] 51 | # @overload [](point, slope) 52 | # @return [PointSlopeLine] 53 | def self.[](*args) 54 | if( 2 == args.size ) 55 | args.map! {|x| x.is_a?(Array) ? Point[*x] : x} 56 | 57 | # If both args are Points, create a TwoPointLine 58 | return TwoPointLine.new(*args) if args.all? {|x| x.is_a?(Vector)} 59 | 60 | # If only the first arg is a Point, create a PointSlopeLine 61 | return PointSlopeLine.new(*args) if args.first.is_a?(Vector) 62 | 63 | # Otherise, create a SlopeInterceptLine 64 | return SlopeInterceptLine.new(*args) 65 | else 66 | nil 67 | end 68 | end 69 | 70 | # @overload new(from, to) 71 | # @option options [Point] :from A starting {Point} 72 | # @option options [Point] :to An end {Point} 73 | # @return [TwoPointLine] 74 | # @overload new(start, end) 75 | # @option options [Point] :start A starting {Point} 76 | # @option options [Point] :end An end {Point} 77 | # @return [TwoPointLine] 78 | def self.new(options={}) 79 | from = options[:from] || options[:start] 80 | to = options[:end] || options[:to] 81 | 82 | if from and to 83 | TwoPointLine.new(from, to) 84 | else 85 | raise ArgumentError, "Start and end Points must be provided" 86 | end 87 | end 88 | 89 | def self.horizontal(y_intercept=0) 90 | SlopeInterceptLine.new(0, y_intercept) 91 | end 92 | def self.vertical(x_intercept=0) 93 | SlopeInterceptLine.new(1/0.0, x_intercept) 94 | end 95 | end 96 | 97 | module SlopedLine 98 | # @!attribute slope 99 | # @return [Number] the slope of the {Line} 100 | attr_reader :slope 101 | 102 | # @!attribute horizontal? 103 | # @return [Boolean] true if the slope is zero 104 | def horizontal? 105 | slope.zero? 106 | end 107 | 108 | # @!attribute vertical? 109 | # @return [Boolean] true if the slope is infinite 110 | def vertical? 111 | slope.infinite? != nil 112 | rescue # Non-Float's don't have an infinite? method 113 | false 114 | end 115 | end 116 | 117 | # @private 118 | class PointSlopeLine < Line 119 | include SlopedLine 120 | 121 | # @!attribute point 122 | # @return [Point] the stating point 123 | attr_reader :point 124 | 125 | # @param point [Point] a {Point} that lies on the {Line} 126 | # @param slope [Number] the slope of the {Line} 127 | def initialize(point, slope) 128 | @point = Point[point] 129 | @slope = slope 130 | end 131 | 132 | # Two {PointSlopeLine}s are equal if both have equal slope and origin 133 | def ==(other) 134 | case other 135 | when SlopeInterceptLine 136 | # Check that the slopes are equal and that the starting point will solve the slope-intercept equation 137 | (slope == other.slope) && (point.y == other.slope * point.x + other.intercept) 138 | when TwoPointLine 139 | # Plug both of other's endpoints into the line equation and check that they solve it 140 | first_diff = other.first - point 141 | last_diff = other.last - point 142 | (first_diff.y == slope*first_diff.x) && (last_diff.y == slope*last_diff.x) 143 | else 144 | self.eql? other 145 | end 146 | end 147 | 148 | # Two {PointSlopeLine}s are equal if both have equal slopes and origins 149 | # @note eql? does not check for equivalence between cluster subclases 150 | def eql?(other) 151 | (point == other.point) && (slope == other.slope) 152 | end 153 | 154 | def to_s 155 | 'Line(' + @slope.to_s + ',' + @point.to_s + ')' 156 | end 157 | 158 | # Find the requested axis intercept 159 | # @param axis [Symbol] the axis to intercept (either :x or :y) 160 | # @return [Number] the location of the intercept 161 | def intercept(axis=:y) 162 | case axis 163 | when :x 164 | vertical? ? point.x : (horizontal? ? nil : (slope * point.x - point.y)) 165 | when :y 166 | vertical? ? nil : (horizontal? ? point.y : (point.y - slope * point.x)) 167 | end 168 | end 169 | end 170 | 171 | # @private 172 | class SlopeInterceptLine < Line 173 | include SlopedLine 174 | 175 | # @param slope [Number] the slope 176 | # @param intercept [Number] the location of the y-axis intercept 177 | def initialize(slope, intercept) 178 | @slope = slope 179 | @intercept = intercept 180 | end 181 | 182 | # Two {SlopeInterceptLine}s are equal if both have equal slope and intercept 183 | def ==(other) 184 | case other 185 | when PointSlopeLine 186 | # Check that the slopes are equal and that the starting point will solve the slope-intercept equation 187 | (slope == other.slope) && (other.point.y == slope * other.point.x + intercept) 188 | when TwoPointLine 189 | # Check that both endpoints solve the line equation 190 | ((other.first.y == slope * other.first.x + intercept)) && (other.last.y == (slope * other.last.x + intercept)) 191 | else 192 | self.eql? other 193 | end 194 | end 195 | 196 | # Two {SlopeInterceptLine}s are equal if both have equal slopes and intercepts 197 | # @note eql? does not check for equivalence between cluster subclases 198 | def eql?(other) 199 | (intercept == other.intercept) && (slope == other.slope) 200 | end 201 | 202 | # Find the requested axis intercept 203 | # @param axis [Symbol] the axis to intercept (either :x or :y) 204 | # @return [Number] the location of the intercept 205 | def intercept(axis=:y) 206 | case axis 207 | when :x 208 | vertical? ? @intercept : (horizontal? ? nil : (-@intercept/@slope)) 209 | when :y 210 | vertical? ? nil : @intercept 211 | end 212 | end 213 | 214 | def to_s 215 | 'Line(' + @slope.to_s + ',' + @intercept.to_s + ')' 216 | end 217 | end 218 | 219 | # @private 220 | class TwoPointLine < Line 221 | # @!attribute first 222 | # @return [Point] the {Line}'s starting point 223 | attr_reader :first 224 | 225 | # @!attribute last 226 | # @return [Point] the {Line}'s end point 227 | attr_reader :last 228 | 229 | # @param first [Point] the starting point 230 | # @param last [Point] the end point 231 | def initialize(first, last) 232 | @first = Point[first] 233 | @last = Point[last] 234 | end 235 | 236 | def inspect 237 | 'Line(' + @first.inspect + ', ' + @last.inspect + ')' 238 | end 239 | alias :to_s :inspect 240 | 241 | # Two {TwoPointLine}s are equal if both have equal {Point}s in the same order 242 | def ==(other) 243 | case other 244 | when PointSlopeLine 245 | # Plug both endpoints into the line equation and check that they solve it 246 | first_diff = first - other.point 247 | last_diff = last - other.point 248 | (first_diff.y == other.slope*first_diff.x) && (last_diff.y == other.slope*last_diff.x) 249 | when SlopeInterceptLine 250 | # Check that both endpoints solve the line equation 251 | ((first.y == other.slope * first.x + other.intercept)) && (last.y == (other.slope * last.x + other.intercept)) 252 | else 253 | self.eql?(other) || ((first == other.last) && (last == other.first)) 254 | end 255 | end 256 | 257 | # Two {TwoPointLine}s are equal if both have equal endpoints 258 | # @note eql? does not check for equivalence between cluster subclases 259 | def eql?(other) 260 | (first == other.first) && (last == other.last) 261 | end 262 | 263 | # @group Accessors 264 | # !@attribute [r[ slope 265 | # @return [Number] the slope of the {Line} 266 | def slope 267 | (last.y - first.y)/(last.x - first.x) 268 | end 269 | 270 | def horizontal? 271 | first.y == last.y 272 | end 273 | 274 | def vertical? 275 | first.x == last.x 276 | end 277 | 278 | # Find the requested axis intercept 279 | # @param axis [Symbol] the axis to intercept (either :x or :y) 280 | # @return [Number] the location of the intercept 281 | def intercept(axis=:y) 282 | case axis 283 | when :x 284 | vertical? ? first.x : (horizontal? ? nil : (first.x - first.y/slope)) 285 | when :y 286 | vertical? ? nil : (horizontal? ? first.y : (first.y - slope * first.x)) 287 | end 288 | end 289 | 290 | # @endgroup 291 | end 292 | end 293 | 294 | -------------------------------------------------------------------------------- /lib/geometry/obround.rb: -------------------------------------------------------------------------------- 1 | require_relative 'cluster_factory' 2 | require_relative 'point' 3 | 4 | module Geometry 5 | 6 | =begin 7 | The {Obround} class cluster represents a rectangle with semicircular end caps 8 | 9 | {http://en.wiktionary.org/wiki/obround} 10 | =end 11 | 12 | class Obround 13 | include ClusterFactory 14 | 15 | # @overload new(width, height) 16 | # Creates a {Obround} of the given width and height, centered on the origin 17 | # @param [Number] height Height 18 | # @param [Number] width Width 19 | # @return [CenteredObround] 20 | # @overload new(size) 21 | # Creates a {Obround} of the given {Size} centered on the origin 22 | # @param [Size] size Width and height 23 | # @return [CenteredObround] 24 | # @overload new(point0, point1) 25 | # Creates a {Obround} using the given {Point}s 26 | # @param [Point] point0 A corner 27 | # @param [Point] point1 The other corner 28 | # @overload new(origin, size) 29 | # Creates a {Obround} from the given origin and size 30 | # @param [Point] origin Lower-left corner 31 | # @param [Size] size Width and height 32 | # @return [SizedObround] 33 | # @overload new(left, bottom, right, top) 34 | # Creates a {Obround} from the locations of each side 35 | # @param [Number] left X-coordinate of the left side 36 | # @param [Number] bottom Y-coordinate of the bottom edge 37 | # @param [Number] right X-coordinate of the right side 38 | # @param [Number] top Y-coordinate of the top edge 39 | # @return [Obround] 40 | def self.new(*args) 41 | case args.size 42 | when 1 43 | CenteredObround.new(args[0]) 44 | when 2 45 | if args.all? {|a| a.is_a?(Numeric) } 46 | CenteredObround.new(Size[*args]) 47 | elsif args.all? {|a| a.is_a?(Array) || a.is_a?(Point) } 48 | original_new(*args) 49 | elsif (args[0].is_a?(Point) or args[0].is_a?(Array))and args[1].is_a?(Size) 50 | SizedObround.new(*args) 51 | else 52 | raise ArgumentError, "Invalid arguments #{args}" 53 | end 54 | when 4 55 | raise ArgumentError unless args.all? {|a| a.is_a?(Numeric)} 56 | left, bottom, right, top = *args 57 | original_new(Point[left, bottom], Point[right, top]) 58 | end 59 | end 60 | 61 | # Create a {Obround} using the given {Point}s 62 | # @param [Point0] point0 The bottom-left corner (closest to the origin) 63 | # @param [Point1] point1 The top-right corner (farthest from the origin) 64 | def initialize(point0, point1) 65 | point0, point1 = Point[point0], Point[point1] 66 | raise(ArgumentError, "Point sizes must match") unless point0.size == point1.size 67 | 68 | # Reorder the points to get lower-left and upper-right 69 | if (point0.x > point1.x) && (point0.y > point1.y) 70 | point0, point1 = point1, point0 71 | else 72 | p0x, p1x = [point0.x, point1.x].minmax 73 | p0y, p1y = [point0.y, point1.y].minmax 74 | point0 = Point[p0x, p0y] 75 | point1 = Point[p1x, p1y] 76 | end 77 | @points = [point0, point1] 78 | end 79 | 80 | def eql?(other) 81 | self.points == other.points 82 | end 83 | alias :== :eql? 84 | 85 | # @group Accessors 86 | 87 | # @return [Point] The {Obround}'s center 88 | def center 89 | min, max = @points.minmax {|a,b| a.y <=> b.y} 90 | Point[(max.x+min.x)/2, (max.y+min.y)/2] 91 | end 92 | 93 | # @!attribute closed? 94 | # @return [Bool] always true 95 | def closed? 96 | true 97 | end 98 | 99 | # @return [Array] The {Obround}'s four points (counterclockwise) 100 | def points 101 | point0, point2 = *@points 102 | point1 = Point[point2.x, point0.y] 103 | point3 = Point[point0.x, point2.y] 104 | [point0, point1, point2, point3] 105 | end 106 | 107 | def origin 108 | minx = @points.min {|a,b| a.x <=> b.x} 109 | miny = @points.min {|a,b| a.y <=> b.y} 110 | Point[minx.x, miny.y] 111 | end 112 | 113 | def height 114 | min, max = @points.minmax {|a,b| a.y <=> b.y} 115 | max.y - min.y 116 | end 117 | 118 | def width 119 | min, max = @points.minmax {|a,b| a.x <=> b.x} 120 | max.x - min.x 121 | end 122 | # @endgroup 123 | end 124 | 125 | class CenteredObround < Obround 126 | # @return [Point] The {Obround}'s center 127 | attr_accessor :center 128 | attr_reader :origin 129 | # @return [Size] The {Size} of the {Obround} 130 | attr_accessor :size 131 | 132 | # @overload new(width, height) 133 | # Creates a {Obround} of the given width and height, centered on the origin 134 | # @param [Number] height Height 135 | # @param [Number] width Width 136 | # @return [CenteredObround] 137 | # @overload new(size) 138 | # Creates a {Obround} of the given {Size} centered on the origin 139 | # @param [Size] size Width and height 140 | # @return [CenteredObround] 141 | # @overload new(center, size) 142 | # Creates a {Obround} with the given center point and size 143 | # @param [Point] center 144 | # @param [Size] size 145 | def initialize(*args) 146 | if args[0].is_a?(Size) 147 | @center = Point[0,0] 148 | @size = args[0] 149 | elsif args[0].is_a?(Geometry::Point) and args[1].is_a?(Geometry::Size) 150 | @center, @size = args[0,1] 151 | elsif (2 == args.size) and args.all? {|a| a.is_a?(Numeric)} 152 | @center = Point[0,0] 153 | @size = Geometry::Size[*args] 154 | end 155 | end 156 | 157 | def eql?(other) 158 | (self.center == other.center) && (self.size == other.size) 159 | end 160 | alias :== :eql? 161 | 162 | # @group Accessors 163 | # @return [Array] The {Obround}'s four points (clockwise) 164 | def points 165 | point0 = @center - @size/2.0 166 | point2 = @center + @size/2.0 167 | point1 = Point[point0.x,point2.y] 168 | point3 = Point[point2.x, point0.y] 169 | [point0, point1, point2, point3] 170 | end 171 | 172 | def height 173 | @size.height 174 | end 175 | 176 | def width 177 | @size.width 178 | end 179 | # @endgroup 180 | end 181 | 182 | class SizedObround < Obround 183 | # @return [Point] The {Obround}'s origin 184 | attr_accessor :origin 185 | # @return [Size] The {Size} of the {Obround} 186 | attr_accessor :size 187 | 188 | # @overload new(width, height) 189 | # Creates an {Obround} of the given width and height with its origin at [0,0] 190 | # @param [Number] height Height 191 | # @param [Number] width Width 192 | # @return SizedObround 193 | # @overload new(size) 194 | # Creates an {Obround} of the given {Size} with its origin at [0,0] 195 | # @param [Size] size Width and height 196 | # @return SizedObround 197 | # @overload new(origin, size) 198 | # Creates an {Obround} with the given origin point and size 199 | # @param [Point] origin 200 | # @param [Size] size 201 | # @return SizedObround 202 | def initialize(*args) 203 | if args[0].is_a?(Size) 204 | @origin = Point[0,0] 205 | @size = args[0] 206 | elsif (args[0].is_a?(Point) or args[0].is_a?(Array)) and args[1].is_a?(Geometry::Size) 207 | @origin, @size = Point[args[0]], args[1] 208 | elsif (2 == args.size) and args.all? {|a| a.is_a?(Numeric)} 209 | @origin = Point[0,0] 210 | @size = Geometry::Size[*args] 211 | end 212 | end 213 | 214 | def eql?(other) 215 | (self.origin == other.origin) && (self.size == other.size) 216 | end 217 | alias :== :eql? 218 | 219 | # @group Accessors 220 | # @return [Point] The {Obround}'s center 221 | def center 222 | @origin + @size/2 223 | end 224 | 225 | # @return [Array] The {Obround}'s four points (clockwise) 226 | def points 227 | point0 = @origin 228 | point2 = @origin + @size 229 | point1 = Point[point0.x,point2.y] 230 | point3 = Point[point2.x, point0.y] 231 | [point0, point1, point2, point3] 232 | end 233 | 234 | def height 235 | @size.height 236 | end 237 | 238 | def width 239 | @size.width 240 | end 241 | # @endgroup 242 | end 243 | end 244 | -------------------------------------------------------------------------------- /lib/geometry/path.rb: -------------------------------------------------------------------------------- 1 | require 'geometry/arc' 2 | require 'geometry/edge' 3 | 4 | module Geometry 5 | =begin 6 | An object representing a set of connected elements, each of which could be an 7 | {Edge} or an {Arc}. Unlike a {Polygon}, a {Path} is not guaranteed to be closed. 8 | =end 9 | class Path 10 | attr_reader :elements 11 | 12 | # Construct a new Path from {Point}s, {Edge}s, and {Arc}s 13 | # Successive {Point}s will be converted to {Edge}s. 14 | def initialize(*args) 15 | args.map! {|a| (a.is_a?(Array) or a.is_a?(Vector)) ? Point[a] : a} 16 | args.each {|a| raise ArgumentError, "Unknown argument type #{a.class}" unless a.is_a?(Point) or a.is_a?(Edge) or a.is_a?(Arc) } 17 | 18 | @elements = [] 19 | 20 | first = args.shift 21 | push first if first.is_a?(Edge) or first.is_a?(Arc) 22 | 23 | args.reduce(first) do |previous, n| 24 | case n 25 | when Point 26 | case previous 27 | when Point then push Edge.new(previous, n) 28 | when Arc, Edge then push Edge.new(previous.last, n) unless previous.last == n 29 | end 30 | last 31 | when Edge 32 | case previous 33 | when Point then push Edge.new(previous, n.first) 34 | when Arc, Edge then push Edge.new(previous.last, n.first) unless previous.last == n.first 35 | end 36 | push(n).last 37 | when Arc 38 | case previous 39 | when Point 40 | if previous == n.first 41 | raise ArgumentError, "Duplicated point before an Arc" 42 | else 43 | push Edge.new(previous, n.first) 44 | end 45 | when Arc, Edge 46 | push Edge.new(previous.last, n.first) unless previous.last == n.first 47 | end 48 | push(n).last 49 | else 50 | raise ArgumentError, "Unsupported argument type: #{n}" 51 | end 52 | end 53 | end 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] 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 | 80 | # @return [Geometry] The last element in the {Path} 81 | def last 82 | @elements.last 83 | end 84 | 85 | # @endgroup 86 | 87 | # Append a new geometry element to the {Path} 88 | # @return [Path] 89 | def push(arg) 90 | @elements.push arg 91 | self 92 | end 93 | end 94 | end 95 | -------------------------------------------------------------------------------- /lib/geometry/point.rb: -------------------------------------------------------------------------------- 1 | require 'matrix' 2 | 3 | require_relative 'point_iso' 4 | require_relative 'point_one' 5 | require_relative 'point_zero' 6 | 7 | module Geometry 8 | DimensionMismatch = Class.new(StandardError) 9 | OperationNotDefined = Class.new(StandardError) 10 | 11 | =begin rdoc 12 | An object repesenting a Point in N-dimensional space 13 | 14 | Supports all of the familiar Vector methods and adds convenience 15 | accessors for those variables you learned to hate in your high school 16 | geometry class (x, y, z). 17 | 18 | == Usage 19 | 20 | === Constructor 21 | point = Geometry::Point[x,y] 22 | =end 23 | class Point < Vector 24 | # Allow vector-style initialization, but override to support copy-init 25 | # from Vector or another Point 26 | # 27 | # @overload [](x,y,z,...) 28 | # @overload [](Array) 29 | # @overload [](Point) 30 | # @overload [](Vector) 31 | def self.[](*array) 32 | return array[0] if array[0].is_a?(Point) 33 | array = array[0] if array[0].is_a?(Array) 34 | array = array[0].to_a if array[0].is_a?(Vector) 35 | super(*array) 36 | end 37 | 38 | # Creates and returns a new {PointIso} instance. Or, a {Point} full of the given value if the size argument is given. 39 | # @param value [Number] the value of the elements 40 | # @param size [Number] the size of the new {Point} full of ones 41 | # @return [PointIso] A new {PointIso} instance 42 | def self.iso(value, size=nil) 43 | size ? Point[Array.new(size, value)] : PointIso.new(value) 44 | end 45 | 46 | # Creates and returns a new {PointOne} instance. Or, a {Point} full of ones if the size argument is given. 47 | # @param size [Number] the size of the new {Point} full of ones 48 | # @return [PointOne] A new {PointOne} instance 49 | def self.one(size=nil) 50 | size ? Point[Array.new(size, 1)] : PointOne.new 51 | end 52 | 53 | # Creates and returns a new {PointZero} instance. Or, a {Point} full of zeros if the size argument is given. 54 | # @param size [Number] the size of the new {Point} full of zeros 55 | # @return [PointZero] A new {PointZero} instance 56 | def self.zero(size=nil) 57 | size ? Point[Array.new(size, 0)] : PointZero.new 58 | end 59 | 60 | # Return a copy of the {Point} 61 | def clone 62 | Point[@elements.clone] 63 | end 64 | 65 | # Allow comparison with an Array, otherwise do the normal thing 66 | def eql?(other) 67 | if other.is_a?(Array) 68 | @elements.eql? other 69 | elsif other.is_a?(PointIso) 70 | value = other.value 71 | @elements.all? {|e| e.eql? value } 72 | elsif other.is_a?(PointOne) 73 | @elements.all? {|e| e.eql? 1 } 74 | elsif other.is_a?(PointZero) 75 | @elements.all? {|e| e.eql? 0 } 76 | else 77 | super other 78 | end 79 | end 80 | 81 | # Allow comparison with an Array, otherwise do the normal thing 82 | def ==(other) 83 | if other.is_a?(Array) 84 | @elements.eql? other 85 | elsif other.is_a?(PointIso) 86 | value = other.value 87 | @elements.all? {|e| e.eql? value } 88 | elsif other.is_a?(PointOne) 89 | @elements.all? {|e| e.eql? 1 } 90 | elsif other.is_a?(PointZero) 91 | @elements.all? {|e| e.eql? 0 } 92 | else 93 | super other 94 | end 95 | end 96 | 97 | # Combined comparison operator 98 | # @return [Point] The <=> operator is applied to the elements of the arguments pairwise and the results are returned in a Point 99 | def <=>(other) 100 | Point[self.to_a.zip(other.to_a).map {|a,b| a <=> b}.compact] 101 | end 102 | 103 | def coerce(other) 104 | case other 105 | when Array then [Point[*other], self] 106 | when Numeric then [Point[Array.new(self.size, other)], self] 107 | when Vector then [Point[*other], self] 108 | else 109 | raise TypeError, "#{self.class} can't be coerced into #{other.class}" 110 | end 111 | end 112 | 113 | def inspect 114 | 'Point' + @elements.inspect 115 | end 116 | def to_s 117 | 'Point' + @elements.to_s 118 | end 119 | 120 | # @group Attributes 121 | 122 | # @override max() 123 | # @return [Number] The maximum value of the {Point}'s elements 124 | # @override max(point) 125 | # @return [Point] The element-wise maximum values of the receiver and the given {Point} 126 | def max(*args) 127 | if args.empty? 128 | @elements.max 129 | else 130 | args = args.first if 1 == args.size 131 | case args 132 | when PointIso then self.class[@elements.map {|e| [e, args.value].max }] 133 | when PointOne then self.class[@elements.map {|e| [e, 1].max }] 134 | when PointZero then self.class[@elements.map {|e| [e, 0].max }] 135 | else 136 | self.class[@elements.zip(args).map(&:max)] 137 | end 138 | end 139 | end 140 | 141 | # @override min() 142 | # @return [Number] The minimum value of the {Point}'s elements 143 | # @override min(point) 144 | # @return [Point] The element-wise minimum values of the receiver and the given {Point} 145 | def min(*args) 146 | if args.empty? 147 | @elements.min 148 | else 149 | args = args.first if 1 == args.size 150 | case args 151 | when PointIso then self.class[@elements.map {|e| [e, args.value].min }] 152 | when PointOne then self.class[@elements.map {|e| [e, 1].min }] 153 | when PointZero then self.class[@elements.map {|e| [e, 0].min }] 154 | else 155 | self.class[@elements.zip(args).map(&:min)] 156 | end 157 | end 158 | end 159 | 160 | # @override minmax() 161 | # @return [Array] The minimum value of the {Point}'s elements 162 | # @override min(point) 163 | # @return [Array] The element-wise minimum values of the receiver and the given {Point} 164 | def minmax(*args) 165 | if args.empty? 166 | @elements.minmax 167 | else 168 | [min(*args), max(*args)] 169 | end 170 | end 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 | 188 | # Returns a new {Point} with the given number of elements removed from the end 189 | # @return [Point] the popped elements 190 | def pop(count=1) 191 | self.class[to_a.pop(count)] 192 | end 193 | 194 | # Returns a new {Point} with the given elements appended 195 | # @return [Point] 196 | def push(*args) 197 | self.class[to_a.push(*args)] 198 | end 199 | 200 | # Removes the first element and returns it 201 | # @return [Point] the shifted elements 202 | def shift(count=1) 203 | self.class[to_a.shift(count)] 204 | end 205 | 206 | # Prepend the given objects and return a new {Point} 207 | # @return [Point] 208 | def unshift(*args) 209 | self.class[to_a.unshift(*args)] 210 | end 211 | 212 | # @group Accessors 213 | # @param [Integer] i Index into the {Point}'s elements 214 | # @return [Numeric] Element i (starting at 0) 215 | def [](*args) 216 | @elements[*args] 217 | end 218 | 219 | # @attribute [r] x 220 | # @return [Numeric] X-component 221 | def x 222 | @elements[0] 223 | end 224 | 225 | # @attribute [r] y 226 | # @return [Numeric] Y-component 227 | def y 228 | @elements[1] 229 | end 230 | 231 | # @attribute [r] z 232 | # @return [Numeric] Z-component 233 | def z 234 | @elements[2] 235 | end 236 | # @endgroup 237 | 238 | # @group Arithmetic 239 | 240 | # @group Unary operators 241 | def +@ 242 | self 243 | end 244 | 245 | def -@ 246 | Point[@elements.map {|e| -e }] 247 | end 248 | # @endgroup 249 | 250 | def +(other) 251 | case other 252 | when Numeric 253 | Point[@elements.map {|e| e + other}] 254 | when PointIso 255 | value = other.value 256 | Point[@elements.map {|e| e + value}] 257 | when PointOne 258 | Point[@elements.map {|e| e + 1}] 259 | when PointZero, NilClass 260 | self.dup 261 | else 262 | raise OperationNotDefined, "#{other.class} must respond to :size and :[]" unless other.respond_to?(:size) && other.respond_to?(:[]) 263 | raise DimensionMismatch, "Can't add #{other} to #{self}" if size != other.size 264 | Point[Array.new(size) {|i| @elements[i] + other[i] }] 265 | end 266 | end 267 | 268 | def -(other) 269 | case other 270 | when Numeric 271 | Point[@elements.map {|e| e - other}] 272 | when PointIso 273 | value = other.value 274 | Point[@elements.map {|e| e - value}] 275 | when PointOne 276 | Point[@elements.map {|e| e - 1}] 277 | when PointZero, NilClass 278 | self.dup 279 | else 280 | raise OperationNotDefined, "#{other.class} must respond to :size and :[]" unless other.respond_to?(:size) && other.respond_to?(:[]) 281 | raise DimensionMismatch, "Can't subtract #{other} from #{self}" if size != other.size 282 | Point[Array.new(size) {|i| @elements[i] - other[i] }] 283 | end 284 | end 285 | 286 | def *(other) 287 | case other 288 | when NilClass 289 | nil 290 | when Numeric 291 | Point[@elements.map {|e| e * other}] 292 | when PointZero 293 | Point.zero 294 | else 295 | if other.respond_to?(:[]) 296 | raise OperationNotDefined, "#{other.class} must respond to :size" unless other.respond_to?(:size) 297 | raise DimensionMismatch, "Can't multiply #{self} by #{other}" if size != other.size 298 | Point[Array.new(size) {|i| @elements[i] * other[i] }] 299 | else 300 | Point[@elements.map {|e| e * other}] 301 | end 302 | end 303 | end 304 | 305 | def /(other) 306 | case other 307 | when Matrix, Vector, Point, Size, NilClass, PointZero, SizeZero 308 | raise OperationNotDefined, "Can't divide #{self} by #{other}" 309 | else 310 | Point[@elements.map {|e| e / other}] 311 | end 312 | end 313 | # @endgroup 314 | 315 | end 316 | end 317 | -------------------------------------------------------------------------------- /lib/geometry/point_iso.rb: -------------------------------------------------------------------------------- 1 | module Geometry 2 | =begin rdoc 3 | An object repesenting a N-dimensional {Point} with identical elements. 4 | =end 5 | class PointIso 6 | # @!attribute value 7 | # @return [Number] the value for every element 8 | attr_accessor :value 9 | 10 | # Initialize to the given value 11 | # @param value [Number] the value for every element of the new {PointIso} 12 | def initialize(value) 13 | @value = value 14 | end 15 | 16 | def eql?(other) 17 | if other.respond_to? :all? 18 | other.all? {|e| e.eql? @value} 19 | else 20 | other == @value 21 | end 22 | end 23 | alias == eql? 24 | 25 | def coerce(other) 26 | if other.is_a? Numeric 27 | [other, @value] 28 | elsif other.is_a? Array 29 | [other, Array.new(other.size, @value)] 30 | elsif other.is_a? Vector 31 | [other, Vector[*Array.new(other.size, @value)]] 32 | else 33 | [Point[other], Point[Array.new(other.size, @value)]] 34 | end 35 | end 36 | 37 | def inspect 38 | 'PointIso<' + @value.inspect + '>' 39 | end 40 | def to_s 41 | 'PointIso<' + @value.to_s + '>' 42 | end 43 | 44 | def is_a?(klass) 45 | (klass == Point) || super 46 | end 47 | alias :kind_of? :is_a? 48 | 49 | # This is a hack to get Array#== to work properly. It works on ruby 2.0 and 1.9.3. 50 | def to_ary 51 | [] 52 | end 53 | 54 | # @override max() 55 | # @return [Number] The maximum value of the {Point}'s elements 56 | # @override max(point) 57 | # @return [Point] The element-wise maximum values of the receiver and the given {Point} 58 | def max(*args) 59 | if args.empty? 60 | @value 61 | else 62 | args = args.first if 1 == args.size 63 | Point[Array.new(args.size, @value).zip(args).map(&:max)] 64 | end 65 | end 66 | 67 | # @override min() 68 | # @return [Number] The minimum value of the {Point}'s elements 69 | # @override min(point) 70 | # @return [Point] The element-wise minimum values of the receiver and the given {Point} 71 | def min(*args) 72 | if args.empty? 73 | @value 74 | else 75 | args = args.first if 1 == args.size 76 | Point[Array.new(args.size, @value).zip(args).map(&:min)] 77 | end 78 | end 79 | 80 | # @override minmax() 81 | # @return [Array] The minimum value of the {Point}'s elements 82 | # @override min(point) 83 | # @return [Array] The element-wise minimum values of the receiver and the given {Point} 84 | def minmax(*args) 85 | if args.empty? 86 | [@value, @value] 87 | else 88 | [min(*args), max(*args)] 89 | end 90 | end 91 | 92 | # Returns a new {Point} with the given number of elements removed from the end 93 | # @return [Point] the popped elements 94 | def pop(count=1) 95 | Point[Array.new(count, @value)] 96 | end 97 | 98 | # Removes the first element and returns it 99 | # @return [Point] the shifted elements 100 | def shift(count=1) 101 | Point[Array.new(count, @value)] 102 | end 103 | 104 | # @group Accessors 105 | # @param i [Integer] Index into the {Point}'s elements 106 | # @return [Numeric] Element i (starting at 0) 107 | def [](i) 108 | @value 109 | end 110 | 111 | # @attribute [r] x 112 | # @return [Numeric] X-component 113 | def x 114 | @value 115 | end 116 | 117 | # @attribute [r] y 118 | # @return [Numeric] Y-component 119 | def y 120 | @value 121 | end 122 | 123 | # @attribute [r] z 124 | # @return [Numeric] Z-component 125 | def z 126 | @value 127 | end 128 | # @endgroup 129 | 130 | # @group Arithmetic 131 | 132 | # @group Unary operators 133 | def +@ 134 | self 135 | end 136 | 137 | def -@ 138 | self.class.new(-@value) 139 | end 140 | # @endgroup 141 | 142 | def +(other) 143 | case other 144 | when Numeric 145 | other + @value 146 | when Size 147 | Point[other.map {|a| a + @value }] 148 | else 149 | if other.respond_to?(:map) 150 | other.map {|a| a + @value } 151 | else 152 | Point[other + @value] 153 | end 154 | end 155 | end 156 | 157 | def -(other) 158 | if other.is_a? Size 159 | Point[other.map {|a| @value - a }] 160 | elsif other.respond_to? :map 161 | other.map {|a| @value - a } 162 | else 163 | @value - other 164 | end 165 | end 166 | 167 | def *(other) 168 | raise OperationNotDefined unless other.is_a? Numeric 169 | self.class.new(other * @value) 170 | end 171 | 172 | def /(other) 173 | raise OperationNotDefined unless other.is_a? Numeric 174 | raise ZeroDivisionError if 0 == other 175 | self.class.new(@value / other) 176 | end 177 | # @endgroup 178 | 179 | end 180 | end 181 | 182 | -------------------------------------------------------------------------------- /lib/geometry/point_one.rb: -------------------------------------------------------------------------------- 1 | module Geometry 2 | =begin rdoc 3 | An object repesenting a {Point} that is one unit away from the origin, along each 4 | axis, in N-dimensional space 5 | 6 | A {PointOne} object is a {Point} that will always compare equal to one and unequal to 7 | everything else, regardless of size. It's similar to the 8 | {http://en.wikipedia.org/wiki/Null_Object_pattern Null Object Pattern}, but for ones. 9 | =end 10 | class PointOne 11 | def eql?(other) 12 | if other.respond_to? :all? 13 | other.all? {|e| e.eql? 1} 14 | else 15 | other == 1 16 | end 17 | end 18 | alias == eql? 19 | 20 | def coerce(other) 21 | if other.is_a? Numeric 22 | [other, 1] 23 | elsif other.is_a? Array 24 | [other, Array.new(other.size, 1)] 25 | elsif other.is_a? Vector 26 | [other, Vector[*Array.new(other.size, 1)]] 27 | else 28 | [Point[other], Point[Array.new(other.size, 1)]] 29 | end 30 | end 31 | 32 | def is_a?(klass) 33 | (klass == Point) || super 34 | end 35 | alias :kind_of? :is_a? 36 | 37 | # This is a hack to get Array#== to work properly. It works on ruby 2.0 and 1.9.3. 38 | def to_ary 39 | [] 40 | end 41 | 42 | # @group Accessors 43 | # @param [Integer] i Index into the {Point}'s elements 44 | # @return [Numeric] Element i (starting at 0) 45 | def [](i) 46 | 1 47 | end 48 | 49 | # @attribute [r] x 50 | # @return [Numeric] X-component 51 | def x 52 | 1 53 | end 54 | 55 | # @attribute [r] y 56 | # @return [Numeric] Y-component 57 | def y 58 | 1 59 | end 60 | 61 | # @attribute [r] z 62 | # @return [Numeric] Z-component 63 | def z 64 | 1 65 | end 66 | # @endgroup 67 | 68 | # @override max() 69 | # @return [Number] The maximum value of the {Point}'s elements 70 | # @override max(point) 71 | # @return [Point] The element-wise maximum values of the receiver and the given {Point} 72 | def max(*args) 73 | if args.empty? 74 | 1 75 | else 76 | args = args.first if 1 == args.size 77 | Point[Array.new(args.size, 1).zip(args).map(&:max)] 78 | end 79 | end 80 | 81 | # @override min() 82 | # @return [Number] The minimum value of the {Point}'s elements 83 | # @override min(point) 84 | # @return [Point] The element-wise minimum values of the receiver and the given {Point} 85 | def min(*args) 86 | if args.empty? 87 | 1 88 | else 89 | args = args.first if 1 == args.size 90 | Point[Array.new(args.size, 1).zip(args).map(&:min)] 91 | end 92 | end 93 | 94 | # @override minmax() 95 | # @return [Array] The minimum value of the {Point}'s elements 96 | # @override min(point) 97 | # @return [Array] The element-wise minimum values of the receiver and the given {Point} 98 | def minmax(*args) 99 | if args.empty? 100 | [1, 1] 101 | else 102 | [min(*args), max(*args)] 103 | end 104 | end 105 | 106 | # Returns a new {Point} with the given number of elements removed from the end 107 | # @return [Point] the popped elements 108 | def pop(count=1) 109 | Point[Array.new(count, 1)] 110 | end 111 | 112 | # Removes the first element and returns it 113 | # @return [Point] the shifted elements 114 | def shift(count=1) 115 | Point[Array.new(count, 1)] 116 | end 117 | 118 | # @group Arithmetic 119 | 120 | # @group Unary operators 121 | def +@ 122 | self 123 | end 124 | 125 | def -@ 126 | -1 127 | end 128 | # @endgroup 129 | 130 | def +(other) 131 | case other 132 | when Numeric 133 | Point.iso(other + 1) 134 | when Size 135 | Point[other.map {|a| a + 1 }] 136 | else 137 | if other.respond_to?(:map) 138 | other.map {|a| a + 1 } 139 | else 140 | Point[other + 1] 141 | end 142 | end 143 | end 144 | 145 | def -(other) 146 | if other.is_a? Size 147 | Point[other.map {|a| 1 - a }] 148 | elsif other.respond_to? :map 149 | other.map {|a| 1 - a } 150 | elsif other == 1 151 | Point.zero 152 | else 153 | Point.iso(1 - other) 154 | end 155 | end 156 | 157 | def *(other) 158 | raise OperationNotDefined unless other.is_a? Numeric 159 | other 160 | end 161 | 162 | def /(other) 163 | raise OperationNotDefined unless other.is_a? Numeric 164 | raise ZeroDivisionError if 0 == other 165 | 1 / other 166 | end 167 | # @endgroup 168 | 169 | # @group Enumerable 170 | 171 | # Return the first, or first n, elements (always 0) 172 | # @param n [Number] the number of elements to return 173 | def first(n=nil) 174 | Array.new(n, 1) rescue 1 175 | end 176 | # @endgroup 177 | end 178 | end 179 | 180 | -------------------------------------------------------------------------------- /lib/geometry/point_zero.rb: -------------------------------------------------------------------------------- 1 | require_relative 'point_iso' 2 | 3 | module Geometry 4 | =begin rdoc 5 | An object repesenting a {Point} at the origin in N-dimensional space 6 | 7 | A {PointZero} object is a {Point} that will always compare equal to zero and unequal to 8 | everything else, regardless of size. You can think of it as an application of the 9 | {http://en.wikipedia.org/wiki/Null_Object_pattern Null Object Pattern}. 10 | =end 11 | class PointZero 12 | def eql?(other) 13 | if other.respond_to? :all? 14 | other.all? {|e| e.eql? 0} 15 | else 16 | other == 0 17 | end 18 | end 19 | alias == eql? 20 | 21 | def coerce(other) 22 | if other.is_a? Numeric 23 | [other, 0] 24 | elsif other.is_a? Array 25 | [other, Array.new(other.size,0)] 26 | elsif other.is_a? Vector 27 | [other, Vector[*Array.new(other.size,0)]] 28 | else 29 | [Point[other], Point[Array.new(other.size,0)]] 30 | end 31 | end 32 | 33 | def is_a?(klass) 34 | (klass == Point) || super 35 | end 36 | alias :kind_of? :is_a? 37 | 38 | # This is a hack to get Array#== to work properly. It works on ruby 2.0 and 1.9.3. 39 | def to_ary 40 | [] 41 | end 42 | 43 | # @group Accessors 44 | # @param [Integer] i Index into the {Point}'s elements 45 | # @return [Numeric] Element i (starting at 0) 46 | def [](i) 47 | 0 48 | end 49 | 50 | # @attribute [r] x 51 | # @return [Numeric] X-component 52 | def x 53 | 0 54 | end 55 | 56 | # @attribute [r] y 57 | # @return [Numeric] Y-component 58 | def y 59 | 0 60 | end 61 | 62 | # @attribute [r] z 63 | # @return [Numeric] Z-component 64 | def z 65 | 0 66 | end 67 | # @endgroup 68 | 69 | # @override max() 70 | # @return [Number] The maximum value of the {Point}'s elements 71 | # @override max(point) 72 | # @return [Point] The element-wise maximum values of the receiver and the given {Point} 73 | def max(*args) 74 | if args.empty? 75 | 0 76 | else 77 | args = args.first if 1 == args.size 78 | Array.new(args.size, 0).zip(args).map(&:max) 79 | end 80 | end 81 | 82 | # @override min() 83 | # @return [Number] The minimum value of the {Point}'s elements 84 | # @override min(point) 85 | # @return [Point] The element-wise minimum values of the receiver and the given {Point} 86 | def min(*args) 87 | if args.empty? 88 | 0 89 | else 90 | args = args.first if 1 == args.size 91 | Array.new(args.size, 0).zip(args).map(&:min) 92 | end 93 | end 94 | 95 | # @override minmax() 96 | # @return [Array] The minimum value of the {Point}'s elements 97 | # @override min(point) 98 | # @return [Array] The element-wise minimum values of the receiver and the given {Point} 99 | def minmax(*args) 100 | if args.empty? 101 | [0, 0] 102 | else 103 | [min(*args), max(*args)] 104 | end 105 | end 106 | 107 | # Returns a new {Point} with the given number of elements removed from the end 108 | # @return [Point] the popped elements 109 | def pop(count=1) 110 | Point[Array.new(count, 0)] 111 | end 112 | 113 | # Removes the first element and returns it 114 | # @return [Point] the shifted elements 115 | def shift(count=1) 116 | Point[Array.new(count, 0)] 117 | end 118 | 119 | # @group Arithmetic 120 | 121 | # @group Unary operators 122 | def +@ 123 | self 124 | end 125 | 126 | def -@ 127 | self 128 | end 129 | # @endgroup 130 | 131 | def +(other) 132 | case other 133 | when Array then other 134 | when Numeric 135 | Point.iso(other) 136 | else 137 | Point[other] 138 | end 139 | end 140 | 141 | def -(other) 142 | if other.is_a? Size 143 | -Point[other] 144 | elsif other.is_a? Numeric 145 | Point.iso(-other) 146 | elsif other.respond_to? :-@ 147 | -other 148 | elsif other.respond_to? :map 149 | other.map {|a| -a } 150 | end 151 | end 152 | 153 | def *(other) 154 | self 155 | end 156 | 157 | def /(other) 158 | raise OperationNotDefined unless other.is_a? Numeric 159 | raise ZeroDivisionError if 0 == other 160 | self 161 | end 162 | # @endgroup 163 | 164 | # @group Enumerable 165 | 166 | # Return the first, or first n, elements (always 0) 167 | # @param n [Number] the number of elements to return 168 | def first(n=nil) 169 | Array.new(n, 0) rescue 0 170 | end 171 | # @endgroup 172 | end 173 | end 174 | 175 | -------------------------------------------------------------------------------- /lib/geometry/regular_polygon.rb: -------------------------------------------------------------------------------- 1 | require_relative 'polygon' 2 | 3 | module Geometry 4 | =begin rdoc 5 | A {RegularPolygon} is a lot like a {Polygon}, but more regular. 6 | 7 | {http://en.wikipedia.org/wiki/Regular_polygon} 8 | 9 | == Usage 10 | polygon = Geometry::RegularPolygon.new sides:4, center:[1,2], radius:3 11 | polygon = Geometry::RegularPolygon.new sides:6, center:[1,2], diameter:6 12 | 13 | polygon = Geometry::RegularPolygon.new sides:4, center:[1,2], inradius:3 14 | polygon = Geometry::RegularPolygon.new sides:6, center:[1,2], indiameter:6 15 | =end 16 | 17 | class RegularPolygon < Polygon 18 | # @return [Point] The {RegularPolygon}'s center point 19 | attr_reader :center 20 | 21 | # @return [Number] The {RegularPolygon}'s number of sides 22 | attr_reader :edge_count 23 | 24 | # @overload new(sides, center, radius) 25 | # Construct a {RegularPolygon} using a center point and radius 26 | # @option options [Number] :sides The number of edges 27 | # @option options [Point] :center (PointZero) The center point of the {RegularPolygon} 28 | # @option options [Number] :radius The circumradius of the {RegularPolygon} 29 | # @overload new(sides, center, inradius) 30 | # Construct a {RegularPolygon} using a center point and radius 31 | # @option options [Number] :sides The number of edges 32 | # @option options [Point] :center (PointZero) The center point of the {RegularPolygon} 33 | # @option options [Number] :inradius The inradius of the {RegularPolygon} 34 | # @overload new(sides, center, diameter) 35 | # Construct a {RegularPolygon} using a center point and diameter 36 | # @option options [Number] :sides The number of edges 37 | # @option options [Point] :center (PointZero) The center point of the {RegularPolygon} 38 | # @option options [Number] :diameter The circumdiameter of the {RegularPolygon} 39 | # @overload new(sides, center, indiameter) 40 | # Construct a {RegularPolygon} using a center point and diameter 41 | # @option options [Number] :sides The number of edges 42 | # @option options [Point] :center (PointZero) The center point of the {RegularPolygon} 43 | # @option options [Number] :indiameter The circumdiameter of the {RegularPolygon} 44 | # @return [RegularPolygon] A new {RegularPolygon} object 45 | def initialize(edge_count:nil, sides:nil, center:nil, radius:nil, diameter:nil, indiameter:nil, inradius:nil) 46 | @edge_count = edge_count || sides 47 | raise ArgumentError, "RegularPolygon requires an edge count" unless @edge_count 48 | 49 | raise ArgumentError, "RegularPolygon.new requires a radius or a diameter" unless diameter || indiameter || inradius || radius 50 | 51 | @center = center ? Point[center] : Point.zero 52 | @diameter = diameter 53 | @indiameter = indiameter 54 | @inradius = inradius 55 | @radius = radius 56 | end 57 | 58 | def eql?(other) 59 | (self.center == other.center) && (self.edge_count == other.edge_count) && (self.radius == other.radius) 60 | end 61 | alias :== :eql? 62 | 63 | # Check to see if the {Polygon} is closed (always true) 64 | # @return [True] Always true because a {Polygon} is always closed 65 | def closed? 66 | true 67 | end 68 | 69 | # @!group Accessors 70 | # @return [Rectangle] The smallest axis-aligned {Rectangle} that bounds the receiver 71 | def bounds 72 | return Rectangle.new(self.min, self.max) 73 | end 74 | 75 | # @!attribute [r] diameter 76 | # @return [Numeric] The diameter of the {RegularPolygon} 77 | def diameter 78 | @diameter || (@radius && 2*@radius) || (@indiameter && @indiameter/cosine_half_angle) 79 | end 80 | alias :circumdiameter :diameter 81 | 82 | # !@attribute [r] edges 83 | def edges 84 | points = self.vertices 85 | points.each_cons(2).map {|p1,p2| Edge.new(p1,p2) } + [Edge.new(points.last, points.first)] 86 | end 87 | 88 | # !@attribute [r] vertices 89 | # @return [Array] 90 | def vertices 91 | (0...2*Math::PI).step(2*Math::PI/edge_count).map {|angle| center + Point[Math::cos(angle), Math::sin(angle)]*radius } 92 | end 93 | alias :points :vertices 94 | 95 | # @return [Point] The upper right corner of the bounding {Rectangle} 96 | def max 97 | @center+Point[radius, radius] 98 | end 99 | 100 | # @return [Point] The lower left corner of the bounding {Rectangle} 101 | def min 102 | @center-Point[radius, radius] 103 | end 104 | 105 | # @return [Array] The lower left and upper right corners of the bounding {Rectangle} 106 | def minmax 107 | [self.min, self.max] 108 | end 109 | 110 | # @!attribute indiameter 111 | # @return [Number] the indiameter 112 | def indiameter 113 | @indiameter || (@inradius && 2*@inradius) || (@diameter && (@diameter * cosine_half_angle)) || (@radius && (2 * @radius * cosine_half_angle)) 114 | end 115 | 116 | # @!attribute inradius 117 | # @return [Number] The inradius 118 | def inradius 119 | @inradius || (@indiameter && @indiameter/2) || (@radius && (@radius * cosine_half_angle)) 120 | end 121 | alias :apothem :inradius 122 | 123 | # @!attribute [r] radius 124 | # @return [Number] The {RegularPolygon}'s radius 125 | def radius 126 | @radius || (@diameter && @diameter/2) || (@inradius && (@inradius / cosine_half_angle)) || (@indiameter && @indiameter/cosine_half_angle/2) 127 | end 128 | alias :circumradius :radius 129 | 130 | # @!attribute [r] side_length 131 | # @return [Number] The length of each side 132 | def side_length 133 | 2 * circumradius * Math.sin(Math::PI/edge_count) 134 | end 135 | 136 | private 137 | def cosine_half_angle 138 | Math.cos(Math::PI/edge_count) 139 | end 140 | 141 | # @!endgroup 142 | end 143 | end 144 | -------------------------------------------------------------------------------- /lib/geometry/rotation.rb: -------------------------------------------------------------------------------- 1 | require 'matrix' 2 | 3 | require_relative 'cluster_factory' 4 | require_relative 'point' 5 | 6 | module Geometry 7 | =begin 8 | A generalized representation of a rotation transformation. 9 | 10 | == Usage 11 | Rotation.new angle:45*Math.PI/180 # Rotate 45 degrees counterclockwise 12 | Rotation.new x:[0,1] # Rotate 90 degrees counterclockwise 13 | =end 14 | class Rotation 15 | include ClusterFactory 16 | 17 | # @return [Integer] dimensions 18 | attr_reader :dimensions 19 | attr_reader :x, :y, :z 20 | 21 | # @overload new(angle) 22 | # Create a planar {Rotation} with an angle 23 | def self.new(*args) 24 | options = args.select {|a| a.is_a? Hash}.reduce({}, :merge) 25 | 26 | if options.has_key? :angle 27 | RotationAngle.new options[:angle] 28 | elsif options.has_key?(:x) && [:x, :y, :z].one? {|k| options.has_key? k } 29 | RotationAngle.new x:options[:x] 30 | else 31 | self.allocate.tap {|rotation| rotation.send :initialize, *args } 32 | end 33 | end 34 | 35 | # @overload initialize(options={}) 36 | # @option options [Radians] :angle Planar rotation angle 37 | # @option options [Integer] :dimensions Dimensionality of the rotation 38 | # @option options [Vector] :x X-axis 39 | # @option options [Vector] :y Y-axis 40 | # @option options [Vector] :z Z-axis 41 | def initialize(*args) 42 | options, args = args.partition {|a| a.is_a? Hash} 43 | options = options.reduce({}, :merge) 44 | 45 | @dimensions = options[:dimensions] || nil 46 | 47 | axis_options = [options[:x], options[:y], options[:z]] 48 | all_axes_options = [options[:x], options[:y], options[:z]].select {|a| a} 49 | if all_axes_options.count != 0 50 | @x = options[:x] || nil 51 | @y = options[:y] || nil 52 | @z = options[:z] || nil 53 | 54 | raise ArgumentError, "All axis options must be Vectors" unless all_axes_options.all? {|a| a.is_a?(Vector) or a.is_a?(Array) } 55 | 56 | raise ArgumentError, "All provided axes must be the same size" unless all_axes_options.all? {|a| a.size == all_axes_options.first.size} 57 | 58 | @dimensions ||= all_axes_options.first.size 59 | 60 | raise ArgumentError, "Dimensionality mismatch" unless all_axes_options.first.size <= @dimensions 61 | if all_axes_options.first.size < @dimensions 62 | @x, @y, @z = [@x, @y, @z].map {|a| (a && (a.size != 0) && (a.size < @dimensions)) ? Array.new(@dimensions) {|i| a[i] || 0 } : a } 63 | end 64 | 65 | raise ArgumentError, "Too many axes specified (expected #{@dimensions - 1} but got #{all_axes_options.size}" unless all_axes_options.size == (@dimensions - 1) 66 | end 67 | end 68 | 69 | def eql?(other) 70 | (self.x.eql? other.x) && (self.y.eql? other.y) && (self.z.eql? other.z) 71 | end 72 | alias :== :eql? 73 | 74 | def identity? 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)}) 77 | end 78 | 79 | # @attribute [r] matrix 80 | # @return [Matrix] the transformation {Matrix} representing the {Rotation} 81 | def matrix 82 | x, y, z = self.x, self.y, self.z 83 | return nil unless [x, y, z].compact.size >= 2 84 | 85 | # Force all axes to be Vectors 86 | x,y,z = [x, y, z].map {|a| a.is_a?(Array) ? Vector[*a] : a} 87 | 88 | # Force all axes to exist 89 | if x and y 90 | z = x ** y 91 | elsif x and z 92 | y = x ** z 93 | elsif y and z 94 | x = y ** z 95 | end 96 | 97 | rows = [] 98 | [x, y, z].each_with_index {|a, i| rows.push(a.to_a) if i < @dimensions } 99 | 100 | raise ArgumentError, "Number of axes must match the dimensions of each axis" unless @dimensions == rows.size 101 | 102 | Matrix[*rows] 103 | end 104 | 105 | 106 | # Transform and return a new {Point} 107 | # @param [Point] point the {Point} to rotate into the parent coordinate frame 108 | # @return [Point] the rotated {Point} 109 | def transform(point) 110 | return point if point.is_a?(PointZero) 111 | m = matrix 112 | m ? Point[m * Point[point]] : point 113 | end 114 | end 115 | 116 | class RotationAngle < Rotation 117 | # @return [Radians] the planar rotation angle 118 | attr_accessor :angle 119 | 120 | # @option options [Radians] :angle the rotation angle from the parent coordinate frame 121 | # @option options [Point] :x the X-axis expressed in the parent coordinate frame 122 | def initialize(*args) 123 | options, args = args.partition {|a| a.is_a? Hash} 124 | options = options.reduce({}, :merge) 125 | 126 | angle = options[:angle] || args[0] 127 | 128 | if angle 129 | @angle = angle 130 | elsif options.has_key? :x 131 | @angle = Math.atan2(*options[:x].to_a.reverse) 132 | else 133 | @angle = 0 134 | end 135 | end 136 | 137 | def eql?(other) 138 | case other 139 | when RotationAngle then angle.eql? other.angle 140 | else 141 | false 142 | end 143 | end 144 | alias :== :eql? 145 | 146 | # @group Accessors 147 | # !@attribute [r] matrix 148 | # @return [Matrix] the transformation {Matrix} representing the {Rotation} 149 | def matrix 150 | return nil unless angle 151 | 152 | c, s = Math.cos(angle), Math.sin(angle) 153 | Matrix[[c, -s], [s, c]] 154 | end 155 | 156 | # !@attribute [r] x 157 | # @return [Point] the X-axis expressed in the parent coordinate frame 158 | def x 159 | Point[Math.cos(angle), Math.sin(angle)] 160 | end 161 | 162 | # !@attribute [r] y 163 | # @return [Point] the Y-axis expressed in the parent coordinate frame 164 | def y 165 | Point[-Math.sin(angle), Math.cos(angle)] 166 | end 167 | # @endgroup 168 | 169 | # @group Composition 170 | def -@ 171 | RotationAngle.new(-angle) 172 | end 173 | 174 | def +(other) 175 | case other 176 | when RotationAngle 177 | RotationAngle.new(angle + other.angle) 178 | else 179 | raise TypeError, "Can't compose a #{self.class} with a #{other.class}" 180 | end 181 | end 182 | 183 | def -(other) 184 | case other 185 | when RotationAngle 186 | RotationAngle.new(angle - other.angle) 187 | else 188 | raise TypeError, "Can't subtract #{other.class} from #{self.class}" 189 | end 190 | end 191 | # @endgroup 192 | end 193 | end 194 | -------------------------------------------------------------------------------- /lib/geometry/size.rb: -------------------------------------------------------------------------------- 1 | require 'matrix' 2 | 3 | require_relative 'size_one' 4 | require_relative 'size_zero' 5 | 6 | module Geometry 7 | =begin 8 | An object representing the size of something. 9 | 10 | Supports all of the familiar {Vector} methods as well as a few convenience 11 | methods (width, height and depth). 12 | 13 | == Usage 14 | 15 | === Constructor 16 | size = Geometry::Size[x,y,z] 17 | =end 18 | 19 | class Size < Vector 20 | # Allow vector-style initialization, but override to support copy-init 21 | # from Vector, Point or another Size 22 | # 23 | # @overload [](x,y,z,...) 24 | # @overload [](Point) 25 | # @overload [](Size) 26 | # @overload [](Vector) 27 | # @return [Size] A new {Size} object 28 | def self.[](*array) 29 | array.map! {|a| a.respond_to?(:to_a) ? a.to_a : a } 30 | array.flatten! 31 | super(*array) 32 | end 33 | 34 | # Creates and returns a new {SizeOne} instance. Or, a {Size} full of ones if the size argument is given. 35 | # @param size [Number] the size of the new {Size} full of ones 36 | # @return [SizeOne] A new {SizeOne} instance 37 | def self.one(size=nil) 38 | size ? Size[Array.new(size, 1)] : SizeOne.new 39 | end 40 | 41 | # Creates and returns a new {SizeOne} instance. Or, a {Size} full of zeros if the size argument is given. 42 | # @param size [Number] the size of the new {Size} full of zeros 43 | # @return [SizeOne] A new {SizeOne} instance 44 | def self.zero(size=nil) 45 | size ? Size[Array.new(size, 0)] : SizeOne.new 46 | end 47 | 48 | # Allow comparison with an Array, otherwise do the normal thing 49 | def ==(other) 50 | return @elements == other if other.is_a?(Array) 51 | super other 52 | end 53 | 54 | # Override Vector#[] to allow for regular array slicing 55 | def [](*args) 56 | @elements[*args] 57 | end 58 | 59 | def coerce(other) 60 | case other 61 | when Array then [Size[*other], self] 62 | when Numeric then [Size[Array.new(self.size, other)], self] 63 | when Vector then [Size[*other], self] 64 | else 65 | raise TypeError, "#{self.class} can't be coerced into #{other.class}" 66 | end 67 | end 68 | 69 | def inspect 70 | 'Size' + @elements.inspect 71 | end 72 | def to_s 73 | 'Size' + @elements.to_s 74 | end 75 | 76 | # @return [Number] The size along the Z axis 77 | def depth 78 | z 79 | end 80 | 81 | # @return [Number] The size along the Y axis 82 | def height 83 | y 84 | end 85 | 86 | # @return [Number] The size along the X axis 87 | def width 88 | x 89 | end 90 | 91 | # @return [Number] X-component (width) 92 | def x 93 | @elements[0] 94 | end 95 | 96 | # @return [Number] Y-component (height) 97 | def y 98 | @elements[1] 99 | end 100 | 101 | # @return [Number] Z-component (depth) 102 | def z 103 | @elements[2] 104 | end 105 | 106 | # Create a new {Size} that is smaller than the receiver by the specified amounts 107 | # @overload inset(x,y) 108 | # @param x [Number] the horizontal inset 109 | # @param y [Number] the vertical inset 110 | # @overload inset(options) 111 | # @option options [Number] :left the left inset 112 | # @option options [Number] :right the right inset 113 | # @option options [Number] :top the top inset 114 | # @option options [Number] :bottom the bottom inset 115 | def inset(*args) 116 | options, args = args.partition {|a| a.is_a? Hash} 117 | options = options.reduce({}, :merge) 118 | 119 | left = right = top = bottom = 0 120 | if 1 == args.size 121 | left = top = -args.shift 122 | right = bottom = 0 123 | elsif 2 == args.size 124 | left = right = -args.shift 125 | top = bottom = -args.shift 126 | end 127 | 128 | left = right = -options[:x] if options[:x] 129 | top = bottom = -options[:y] if options[:y] 130 | 131 | top = -options[:top] if options[:top] 132 | left = -options[:left] if options[:left] 133 | bottom = -options[:bottom] if options[:bottom] 134 | right = -options[:right] if options[:right] 135 | 136 | self.class[left + width + right, top + height + bottom] 137 | end 138 | 139 | # Create a new {Size} that is larger than the receiver by the specified amounts 140 | # @overload outset(x,y) 141 | # @param x [Number] the horizontal inset 142 | # @param y [Number] the vertical inset 143 | # @overload outset(options) 144 | # @option options [Number] :left the left inset 145 | # @option options [Number] :right the right inset 146 | # @option options [Number] :top the top inset 147 | # @option options [Number] :bottom the bottom inset 148 | def outset(*args) 149 | options, args = args.partition {|a| a.is_a? Hash} 150 | options = options.reduce({}, :merge) 151 | 152 | left = right = top = bottom = 0 153 | if 1 == args.size 154 | left = top = args.shift 155 | right = bottom = 0 156 | elsif 2 == args.size 157 | left = right = args.shift 158 | top = bottom = args.shift 159 | end 160 | 161 | left = right = options[:x] if options[:x] 162 | top = bottom = options[:y] if options[:y] 163 | 164 | top = options[:top] if options[:top] 165 | left = options[:left] if options[:left] 166 | bottom = options[:bottom] if options[:bottom] 167 | right = options[:right] if options[:right] 168 | 169 | self.class[left + width + right, top + height + bottom] 170 | end 171 | end 172 | end 173 | -------------------------------------------------------------------------------- /lib/geometry/size_one.rb: -------------------------------------------------------------------------------- 1 | require_relative 'point' 2 | 3 | module Geometry 4 | =begin rdoc 5 | An object repesenting a {Size} of 1, in N-dimensional space 6 | 7 | A {SizeOne} object is a {Size} that will always compare equal to one and unequal to 8 | everything else, regardless of dimensionality. It's similar to the 9 | {http://en.wikipedia.org/wiki/Null_Object_pattern Null Object Pattern}, but for ones. 10 | =end 11 | class SizeOne 12 | def eql?(other) 13 | if other.respond_to? :all? 14 | other.all? {|e| e.eql? 1} 15 | else 16 | other == 1 17 | end 18 | end 19 | alias == eql? 20 | 21 | def coerce(other) 22 | if other.is_a? Numeric 23 | [other, 1] 24 | elsif other.is_a? Array 25 | [other, Array.new(other.size,1)] 26 | elsif other.is_a? Vector 27 | [other, Vector[*Array.new(other.size,1)]] 28 | else 29 | [Size[other], Size[Array.new(other.size,1)]] 30 | end 31 | end 32 | 33 | # @group Arithmetic 34 | 35 | # @group Unary operators 36 | def +@ 37 | self 38 | end 39 | 40 | def -@ 41 | -1 42 | end 43 | # @endgroup 44 | 45 | def +(other) 46 | if other.respond_to?(:map) 47 | other.map {|a| a + 1 } 48 | else 49 | other + 1 50 | end 51 | end 52 | 53 | def -(other) 54 | if other.is_a? Numeric 55 | 1 - other 56 | elsif other.respond_to? :map 57 | other.map {|a| 1 - a } 58 | else 59 | 1 - other 60 | end 61 | end 62 | 63 | def *(other) 64 | raise OperationNotDefined unless other.is_a? Numeric 65 | other 66 | end 67 | 68 | def /(other) 69 | raise OperationNotDefined unless other.is_a? Numeric 70 | raise ZeroDivisionError if 0 == other 71 | 1 / other 72 | end 73 | # @endgroup 74 | 75 | # @group Enumerable 76 | 77 | # Return the first, or first n, elements (always 0) 78 | # @param n [Number] the number of elements to return 79 | def first(n=nil) 80 | Array.new(n, 1) rescue 1 81 | end 82 | # @endgroup 83 | end 84 | end 85 | 86 | -------------------------------------------------------------------------------- /lib/geometry/size_zero.rb: -------------------------------------------------------------------------------- 1 | require_relative 'point' 2 | 3 | module Geometry 4 | =begin rdoc 5 | An object repesenting a zero {Size} in N-dimensional space 6 | 7 | A {SizeZero} object is a {Size} that will always compare equal to zero and unequal to 8 | everything else, regardless of dimensionality. You can think of it as an application of the 9 | {http://en.wikipedia.org/wiki/Null_Object_pattern Null Object Pattern}. 10 | =end 11 | class SizeZero 12 | def eql?(other) 13 | if other.respond_to? :all? 14 | other.all? {|e| e.eql? 0} 15 | else 16 | other == 0 17 | end 18 | end 19 | alias == eql? 20 | 21 | def coerce(other) 22 | if other.is_a? Numeric 23 | [other, 0] 24 | elsif other.is_a? Array 25 | [other, Array.new(other.size,0)] 26 | elsif other.is_a? Vector 27 | [other, Vector[*Array.new(other.size,0)]] 28 | else 29 | [Size[other], Size[Array.new(other.size,0)]] 30 | end 31 | end 32 | 33 | # @group Arithmetic 34 | 35 | # @group Unary operators 36 | def +@ 37 | self 38 | end 39 | 40 | def -@ 41 | self 42 | end 43 | # @endgroup 44 | 45 | def +(other) 46 | other 47 | end 48 | 49 | def -(other) 50 | if other.respond_to? :-@ 51 | -other 52 | elsif other.respond_to? :map 53 | other.map {|a| -a } 54 | end 55 | end 56 | 57 | def *(other) 58 | self 59 | end 60 | 61 | def /(other) 62 | raise OperationNotDefined unless other.is_a? Numeric 63 | raise ZeroDivisionError if 0 == other 64 | self 65 | end 66 | # @endgroup 67 | 68 | # @group Enumerable 69 | 70 | # Return the first, or first n, elements (always 0) 71 | # @param n [Number] the number of elements to return 72 | def first(n=nil) 73 | Array.new(n, 0) rescue 0 74 | end 75 | # @endgroup 76 | end 77 | end 78 | 79 | -------------------------------------------------------------------------------- /lib/geometry/square.rb: -------------------------------------------------------------------------------- 1 | require_relative 'point' 2 | 3 | module Geometry 4 | NotSquareError = Class.new(ArgumentError) 5 | 6 | =begin 7 | The {Square} class cluster is like the {Rectangle} class cluster, but not longer in one direction. 8 | 9 | == Constructors 10 | 11 | Square.new from:[1,2], to:[2,3] # Using two corners 12 | Square.new origin:[3,4], size:5 # Using an origin point and a size 13 | Square.new center:[5,5], size:5 # Using a center point and a size 14 | Square.new size:5 # Centered on the origin 15 | =end 16 | class Square 17 | include ClusterFactory 18 | 19 | # @!attribute points 20 | # @return [Array] the corner {Point}s of the {Square} in counter-clockwise order 21 | attr_reader :points 22 | alias :vertices :points 23 | 24 | # @overload new(:origin, :size) 25 | # Creates a {Square} with the given origin and size 26 | # @option [Point] :origin The lower-left corner 27 | # @option [Number] :size Bigness 28 | # @return [CenteredSquare] 29 | def self.new(*args) 30 | options, args = args.partition {|a| a.is_a? Hash} 31 | options = options.reduce({}, :merge) 32 | 33 | if options.key?(:size) 34 | unless options[:size].is_a? Numeric 35 | raise NotSquareError, 'Size must be a square' unless options[:size].all? {|a| a == options[:size].first} 36 | options[:size] = options[:size].first 37 | end 38 | 39 | if options.key? :origin 40 | SizedSquare.new(options[:origin], options[:size]) 41 | else 42 | CenteredSquare.new(options[:center] || PointZero.new, options[:size]) 43 | end 44 | elsif options.key?(:from) and options.key?(:to) 45 | original_new(from: options[:from], to: options[:to]) 46 | end 47 | end 48 | 49 | # Creates a {Square} given two {Point}s 50 | # @option options [Point] :from A corner (ie. bottom-left) 51 | # @option options [Point] :to The other corner (ie. top-right) 52 | # @option options [Point] :origin The lower left corner 53 | def initialize(options={}) 54 | origin = options[:from] || options[:origin] 55 | origin = origin ? Point[origin] : PointZero.new 56 | 57 | if options.has_key? :to 58 | point1 = options[:to] 59 | end 60 | 61 | point1 = Point[point1] 62 | raise(ArgumentError, "Point sizes must match (#{origin.size} != #{point1.size})") unless origin.is_a?(PointZero) || (origin.size == point1.size) 63 | 64 | # Reorder the points to get lower-left and upper-right 65 | minx, maxx = [origin.x, point1.x].minmax 66 | miny, maxy = [origin.y, point1.y].minmax 67 | @points = [Point[minx, miny], Point[maxx, maxy]] 68 | 69 | raise(NotSquareError) if height != width 70 | end 71 | 72 | # !@group Accessors 73 | # @!attribute closed? 74 | # @return [Bool] always true 75 | def closed? 76 | true 77 | end 78 | 79 | # @!attribute [r] edges 80 | # @return [Array] An array of {Edge}s corresponding to the sides of the {Square} 81 | def edges 82 | (points + [points.first]).each_cons(2).map {|v1,v2| Edge.new v1, v2} 83 | end 84 | 85 | # @return [Point] The upper right corner of the bounding {Rectangle} 86 | def max 87 | @points.last 88 | end 89 | 90 | # @return [Point] The lower left corner of the bounding {Rectangle} 91 | def min 92 | @points.first 93 | end 94 | 95 | # @return [Array] The lower left and upper right corners of the bounding {Rectangle} 96 | def minmax 97 | [self.min, self.max] 98 | end 99 | 100 | # @!attribute origin 101 | # @return [Point] The {Square}'s origin (lower-left corner) 102 | def origin 103 | @points.first 104 | end 105 | 106 | # @return [Array] The {Square}'s four points (clockwise) 107 | def points 108 | p0, p1 = *@points 109 | [p0, Point[p0.x, p1.y], p1, Point[p1.x, p0.y]] 110 | end 111 | 112 | def height 113 | min, max = @points.minmax {|a,b| a.y <=> b.y} 114 | max.y - min.y 115 | end 116 | 117 | def width 118 | min, max = @points.minmax {|a,b| a.x <=> b.x} 119 | max.x - min.x 120 | end 121 | # @endgroup 122 | 123 | # @return [Path] A closed {Path} that traces the boundary of the {Square} clockwise, starting from the lower-left 124 | def path 125 | Path.new(*self.points, self.points.first) 126 | end 127 | end 128 | 129 | # A {Square} created with a center point and a size 130 | class CenteredSquare < Square 131 | # @attribute [r] center 132 | # @return [Point] The center of the {Square} 133 | attr_reader :center 134 | 135 | # @!attribute size 136 | # @return [Size] The {Size} of the {Square} 137 | attr_accessor :size 138 | 139 | # @param [Point] center The center point 140 | # @param [Numeric] size The length of each side 141 | def initialize(center, size) 142 | @center = Point[center] 143 | @size = size 144 | end 145 | 146 | # @group Accessors 147 | # @return [Point] The upper right corner of the bounding {Rectangle} 148 | def max 149 | half_size = @size/2 150 | Point[@center.x + half_size, @center.y + half_size] 151 | end 152 | 153 | # @return [Point] The lower left corner of the bounding {Rectangle} 154 | def min 155 | half_size = @size/2 156 | Point[@center.x - half_size, @center.y - half_size] 157 | end 158 | 159 | # @return [Array] The lower left and upper right corners of the bounding {Rectangle} 160 | def minmax 161 | [self.min, self.max] 162 | end 163 | 164 | # @attribute [r] origin 165 | # @return [Point] The lower left corner 166 | def origin 167 | Point[@center.x - size/2, @center.y - size/2] 168 | end 169 | 170 | # @attribute [r] points 171 | # @return [Array] The {Square}'s four points (clockwise) 172 | def points 173 | half_size = @size/2 174 | minx = @center.x - half_size 175 | maxx = @center.x + half_size 176 | miny = @center.y - half_size 177 | maxy = @center.y + half_size 178 | 179 | [Point[minx,miny], Point[minx, maxy], Point[maxx, maxy], Point[maxx,miny]] 180 | end 181 | 182 | def height 183 | @size 184 | end 185 | 186 | def width 187 | @size 188 | end 189 | # @endgroup 190 | end 191 | 192 | # A {Square} created with an origin point and a size 193 | class SizedSquare < Square 194 | # @!attribute size 195 | # @return [Size] The {Size} of the {Square} 196 | attr_accessor :size 197 | 198 | # @param [Point] origin The origin point (bottom-left corner) 199 | # @param [Numeric] size The length of each side 200 | def initialize(origin, size) 201 | @origin = Point[origin] 202 | @size = size 203 | end 204 | 205 | # @group Accessors 206 | # @!attribute center 207 | # @return [Point] The center of it all 208 | def center 209 | origin + size/2 210 | end 211 | 212 | # @return [Point] The upper right corner of the bounding {Rectangle} 213 | def max 214 | origin + size 215 | end 216 | 217 | # @return [Point] The lower left corner of the bounding {Rectangle} 218 | def min 219 | origin 220 | end 221 | 222 | # @attribute [r] origin 223 | # @return [Point] The lower left corner 224 | def origin 225 | @origin 226 | end 227 | 228 | # @attribute [r] points 229 | # @return [Array] The {Square}'s four points (clockwise) 230 | def points 231 | minx = origin.x 232 | maxx = origin.x + size 233 | miny = origin.y 234 | maxy = origin.y + size 235 | 236 | [origin, Point[minx, maxy], Point[maxx, maxy], Point[maxx,miny]] 237 | end 238 | 239 | # @return [Number] The size of the {Square} along the y-axis 240 | def height 241 | @size 242 | end 243 | 244 | # @return [Number] The size of the {Square} along the x-axis 245 | def width 246 | @size 247 | end 248 | # @endgroup 249 | end 250 | end 251 | -------------------------------------------------------------------------------- /lib/geometry/transformation.rb: -------------------------------------------------------------------------------- 1 | require 'geometry/point' 2 | require 'geometry/rotation' 3 | 4 | require_relative 'transformation/composition' 5 | 6 | module Geometry 7 | =begin 8 | {Transformation} represents a relationship between two coordinate frames. 9 | 10 | To create a pure translation relationship: 11 | 12 | translate = Geometry::Transformation.new(:translate => Point[4, 2]) 13 | 14 | To create a transformation with an origin and an X-axis aligned with the parent 15 | coordinate system's Y-axis (the Y and Z axes will be chosen arbitrarily): 16 | 17 | translate = Geometry::Transformation.new(:origin => [4, 2], :x => [0,1,0]) 18 | 19 | To create a transformation with an origin, an X-axis aligned with the parent 20 | coordinate system's Y-axis, and a Y-axis aligned with the parent coordinate 21 | system's X-axis: 22 | 23 | translate = Geometry::Transformation.new(:origin => [4, 2], :x => [0,1,0], :y => [1,0,0]) 24 | =end 25 | class Transformation 26 | attr_reader :dimensions 27 | attr_reader :rotation 28 | attr_reader :scale 29 | attr_reader :translation 30 | 31 | attr_reader :x_axis, :y_axis, :z_axis 32 | 33 | # @overload new(translate, rotate, scale) 34 | # @param [Point] translate Linear displacement 35 | # @param [Rotation] rotate Rotation 36 | # @param [Vector] scale Scaling 37 | # @overload new(options) 38 | # @param [Hash] options 39 | # @option options [Integer] :dimensions Dimensionality of the transformation 40 | # @option options [Point] :origin Same as :translate 41 | # @option options [Point] :move Same as :translate 42 | # @option options [Point] :translate Linear displacement 43 | # @option options [Angle] :angle Rotation angle (assumes planar geometry) 44 | # @option options [Rotation] :rotate Rotation 45 | # @option options [Vector] :scale Scaling 46 | # @option options [Vector] :x X-axis 47 | # @option options [Vector] :y Y-axis 48 | # @option options [Vector] :z Z-axis 49 | def initialize(*args) 50 | options, args = args.partition {|a| a.is_a? Hash} 51 | translate, rotate, scale = args 52 | options = options.reduce({}, :merge) 53 | 54 | @dimensions = options[:dimensions] || nil 55 | 56 | rotation_options = options.select {|key, value| [:angle, :x, :y, :z].include? key } 57 | @rotation = options[:rotate] || rotate || ((rotation_options.size > 0) ? Geometry::Rotation.new(rotation_options) : nil) 58 | @scale = options[:scale] || scale 59 | 60 | case options.count {|k,v| [:move, :origin, :translate].include? k } 61 | when 0 62 | @translation = translate 63 | when 1 64 | @translation = (options[:translate] ||= options.delete(:move) || options.delete(:origin)) 65 | else 66 | raise ArgumentError, "Too many translation parameters in #{options}" 67 | end 68 | 69 | raise ArgumentError, "Bad translation" if @translation.is_a? Hash 70 | @translation = Point[*@translation] 71 | if @translation 72 | @translation = nil if @translation.all? {|v| v == 0} 73 | raise ArgumentError, ":translate must be a Point or a Vector" if @translation and not @translation.is_a?(Vector) 74 | end 75 | 76 | if @dimensions 77 | biggest = [@translation, @scale].select {|a| a}.map {|a| a.size}.max 78 | 79 | if biggest and (biggest != 0) and (((biggest != @dimensions)) or (@rotation and (@rotation.dimensions != biggest))) 80 | raise ArgumentError, "Dimensionality mismatch" 81 | end 82 | end 83 | end 84 | 85 | def initialize_clone(source) 86 | super 87 | @rotation = @rotation.clone if @rotation 88 | @scale = @scale.clone if @scale 89 | @translation = @translation.clone if @translation 90 | end 91 | 92 | # !@attribute [r] has_rotation? 93 | # @return [Bool] true if the transformation has any rotation components 94 | def has_rotation? 95 | !!@rotation 96 | end 97 | 98 | # Returns true if the {Transformation} is the identity transformation 99 | def identity? 100 | !(@rotation || @scale || @translation) 101 | end 102 | 103 | def eql?(other) 104 | return false unless other 105 | return true if !self.dimensions && !other.dimensions && !self.rotation && !other.rotation && !self.translation && !other.translation && !self.scale && !other.scale 106 | return false if !(self.dimensions && other.dimensions) && !(self.rotation && other.rotation) && !(self.translation && other.translation) && !(self.scale && other.scale) 107 | 108 | ((self.dimensions && other.dimensions && self.dimensions.eql?(other.dimensions)) || !(self.dimensions && other.dimensions)) && 109 | ((self.rotation && other.rotation && self.rotation.eql?(other.rotation)) || !(self.rotation && other.rotation)) && 110 | ((self.scale && other.scale && self.scale.eql?(other.scale)) || !(self.scale && other.rotation)) && 111 | ((self.translation && other.translation && self.translation.eql?(other.translation)) || !(self.scale && other.rotation)) 112 | end 113 | alias :== :eql? 114 | 115 | # Update the translation property with the given {Point} 116 | # @param point [Point] the distance to translate by 117 | # @return [Transformation] 118 | def translate(point) 119 | if translation 120 | @translation += Point[point] 121 | else 122 | @translation = Point[point] 123 | end 124 | self 125 | end 126 | 127 | # Compose the current {Transformation} with another one 128 | def +(other) 129 | return self.clone unless other 130 | 131 | case other 132 | when Array, Vector 133 | if @translation 134 | Transformation.new(@translation+other, @rotation, @scale) 135 | else 136 | Transformation.new(other, @rotation, @scale) 137 | end 138 | when Composition 139 | Composition.new(self, *other.transformations) 140 | when Transformation 141 | if @rotation || other.rotation 142 | Composition.new(self, other) 143 | else 144 | translation = @translation ? (@translation + other.translation) : other.translation 145 | Transformation.new(translation, @rotation, @scale) 146 | end 147 | end 148 | end 149 | 150 | def -(other) 151 | return self.clone unless other 152 | 153 | case other 154 | when Array, Vector 155 | if @translation 156 | Transformation.new(@translation-other, @rotation, @scale) 157 | else 158 | Transformation.new(other.map {|e| -e}, @rotation, @scale) 159 | end 160 | when Transformation 161 | if @rotation 162 | rotation = other.rotation ? (@rotation - other.rotation) : @rotation 163 | elsif other.rotation 164 | rotation = -other.rotation 165 | else 166 | rotation = nil 167 | end 168 | 169 | translation = @translation ? (@translation - other.translation) : -other.translation 170 | 171 | Transformation.new(translation, rotation, @scale) 172 | end 173 | end 174 | 175 | # Transform and return a new {Point}. Rotation is applied before translation. 176 | # @param [Point] point the {Point} to transform into the parent coordinate frame 177 | # @return [Point] The transformed {Point} 178 | def transform(point) 179 | point = @rotation.transform(point) if @rotation 180 | @translation ? (@translation + point) : point 181 | end 182 | end 183 | 184 | # @override translation(x, y, z) 185 | # Construct a new translation from the given values 186 | # @param x [Number] The distance to translate along the X-axis 187 | # @param y [Number] The distance to translate along the Y-axis 188 | # @param z [Number] The distance to translate along the Z-axis 189 | # @override translation([x, y, z]) 190 | # Construct a new translation from an array of values 191 | # @override translation(point) 192 | # Construct a new translation from a {Point} 193 | # @param point [Point] A {Point} 194 | def self.translation(*args) 195 | args = *args if args[0].is_a? Array 196 | Transformation.new translate:args 197 | end 198 | end 199 | -------------------------------------------------------------------------------- /lib/geometry/transformation/composition.rb: -------------------------------------------------------------------------------- 1 | module Geometry 2 | class Transformation 3 | class Composition 4 | attr_reader :transformations 5 | 6 | def initialize(*args) 7 | raise TypeError unless args.all? {|a| a.is_a? Transformation } 8 | @transformations = *args 9 | end 10 | 11 | def +(other) 12 | case other 13 | when Transformation 14 | Composition.new(*transformations, other) 15 | when Composition 16 | Composition.new(*transformations, *other.transformations) 17 | end 18 | end 19 | 20 | # @group Accessors 21 | # !@attribute [r] has_rotation? 22 | # @return [Bool] true if the transformation has any rotation components 23 | def has_rotation? 24 | transformations.any? {|t| t.is_a?(Rotation) || t.has_rotation? } 25 | end 26 | 27 | # !@attribute [r] size 28 | # @return [Number] the number of composed {Transformation}s 29 | def size 30 | transformations.size 31 | end 32 | # @endgroup 33 | 34 | def transform(point) 35 | transformations.reverse.reduce(point) {|_point, transformation| transformation.transform(_point) } 36 | end 37 | end 38 | end 39 | end -------------------------------------------------------------------------------- /lib/geometry/triangle.rb: -------------------------------------------------------------------------------- 1 | require_relative 'cluster_factory' 2 | require_relative 'point' 3 | 4 | module Geometry 5 | =begin rdoc 6 | A {http://en.wikipedia.org/wiki/Triangle Triangle} is not a square. 7 | 8 | == Usage 9 | A right {Triangle} with its right angle at the origin and sides of length 1 10 | triangle = Geometry::Triangle.new [0,0], [1,0], [0,1] 11 | 12 | An isoscoles right {Triangle} created with an origin and leg length 13 | triangle = Geometry::Triangle.new [0,0], 1 14 | =end 15 | 16 | # @abstract Factory class that instantiates the appropriate subclasses 17 | class Triangle 18 | 19 | include ClusterFactory 20 | 21 | # @overload new(point0, point1, point2) 22 | # Contruct a {ScaleneTriangle} using three {Point}s 23 | # @param [Point] point0 The first vertex of the {Triangle} 24 | # @param [Point] point1 Another vertex of the {Triangle} 25 | # @param [Point] point2 The final vertex of the {Triangle} 26 | # @overload new(point, length) 27 | # Construct a {RightTriangle} using a {Point} and the lengths of the sides 28 | # @param [Point] point The location of the vertex at {Triangle}'s right angle 29 | # @param [Number] base The length of the {Triangle}'s base leg 30 | # @param [Number] height The length of the {Triangle}'s vertical leg 31 | def self.new(*args) 32 | if args.size == 3 33 | ScaleneTriangle.new(*args) 34 | elsif args.size == 2 35 | RightTriangle.new args[0], args[1], args[1] 36 | end 37 | end 38 | 39 | # @!attribute closed? 40 | # @return [Bool] always true 41 | def closed? 42 | true 43 | end 44 | 45 | # @return [Point] The upper-right corner of the bounding rectangle that encloses the {Polyline} 46 | def max 47 | points.reduce {|memo, vertex| Point[[memo.x, vertex.x].max, [memo.y, vertex.y].max] } 48 | end 49 | 50 | # @return [Point] The lower-left corner of the bounding rectangle that encloses the {Polyline} 51 | def min 52 | points.reduce {|memo, vertex| Point[[memo.x, vertex.x].min, [memo.y, vertex.y].min] } 53 | end 54 | 55 | # @return [Array] The lower-left and upper-right corners of the enclosing bounding rectangle 56 | def minmax 57 | points.reduce([points.first, points.first]) {|memo, vertex| [Point[[memo.first.x, vertex.x].min, [memo.first.y, vertex.y].min], Point[[memo.last.x, vertex.x].max, [memo.last.y, vertex.y].max]] } 58 | end 59 | end 60 | 61 | # {http://en.wikipedia.org/wiki/Equilateral_triangle Equilateral Triangle} 62 | class EquilateralTriangle < Triangle 63 | def self.new(*args) 64 | original_new(*args) 65 | end 66 | end 67 | 68 | class IsoscelesTriangle < Triangle 69 | def self.new(*args) 70 | original_new(*args) 71 | end 72 | end 73 | 74 | # {http://en.wikipedia.org/wiki/Right_triangle Right Triangle} 75 | class RightTriangle < Triangle 76 | attr_reader :origin, :base, :height 77 | 78 | # Construct a Right Triangle given a {Point} and the leg lengths 79 | def initialize(origin, base, height) 80 | @origin = Point[origin] 81 | @base, @height = base, height 82 | end 83 | 84 | # An array of points corresponding to the vertices of the {Triangle} (clockwise) 85 | # @return [Array] Vertices 86 | def points 87 | [@origin, @origin + Point[0,@height], @origin + Point[@base,0]] 88 | end 89 | end 90 | 91 | class ScaleneTriangle < Triangle 92 | attr_reader :points 93 | 94 | # Construct a scalene {Triangle} 95 | def initialize(point0, point1, point2) 96 | @points = [point0, point1, point2].map {|p| Point[p] } 97 | end 98 | end 99 | end 100 | -------------------------------------------------------------------------------- /lib/geometry/vector.rb: -------------------------------------------------------------------------------- 1 | require 'matrix' 2 | 3 | # Monkeypatch Vector to overcome some deficiencies 4 | class Vector 5 | X = Vector[1,0,0] 6 | Y = Vector[0,1,0] 7 | Z = Vector[0,0,1] 8 | 9 | # @group Unary operators 10 | def +@ 11 | self 12 | end 13 | 14 | def -@ 15 | Vector[*(@elements.map {|e| -e })] 16 | end 17 | # @endgroup 18 | 19 | # Cross-product of two {Vector}s 20 | # @return [Vector] 21 | def cross(other) 22 | Vector.Raise ErrDimensionMismatch unless @elements.size == other.size 23 | 24 | case @elements.size 25 | when 0 then raise ArgumentError, "Can't multply zero-length Vectors" 26 | when 1 then @elements.first * other.first 27 | when 2 then @elements.first * other[1] - @elements.last * other.first 28 | when 3 then Vector[ @elements[1]*other[2] - @elements[2]*other[1], 29 | @elements[2]*other[0] - @elements[0]*other[2], 30 | @elements[0]*other[1] - @elements[1]*other[0]] 31 | end 32 | end 33 | alias ** cross 34 | end 35 | -------------------------------------------------------------------------------- /test/geometry.rb: -------------------------------------------------------------------------------- 1 | require 'minitest/autorun' 2 | require 'geometry' 3 | 4 | describe Geometry do 5 | end 6 | -------------------------------------------------------------------------------- /test/geometry/annulus.rb: -------------------------------------------------------------------------------- 1 | require 'minitest/autorun' 2 | require 'geometry/annulus' 3 | 4 | describe Geometry::Annulus do 5 | it 'must complain when constructed with only a center' do 6 | -> { Geometry::Annulus.new center:Point[1,2] }.must_raise ArgumentError 7 | end 8 | 9 | it 'must also be known as a Ring' do 10 | Geometry::Ring.new(Point[1,2], inner_radius:5, radius:10).must_be_instance_of Geometry::Annulus 11 | end 12 | 13 | describe 'when constructed with a named center' do 14 | subject { Geometry::Annulus.new center:Point[1,2], inner_radius:5, radius:10 } 15 | 16 | it 'must have a center' do 17 | subject.center.must_equal Point[1,2] 18 | end 19 | 20 | it 'must have a max' do 21 | subject.max.must_equal Point[11, 12] 22 | end 23 | 24 | it 'must have a min' do 25 | subject.min.must_equal Point[-9, -8] 26 | end 27 | 28 | it 'must have a min and a max' do 29 | subject.minmax.must_equal [subject.min, subject.max] 30 | end 31 | end 32 | 33 | describe 'when constructed with a center, inner_radius and radius' do 34 | subject { Geometry::Annulus.new Point[1,2], inner_radius:5, radius:10 } 35 | 36 | it 'must have a center' do 37 | subject.center.must_equal Point[1,2] 38 | end 39 | 40 | it 'must have an inner diameter' do 41 | subject.inner_diameter.must_equal 10 42 | end 43 | 44 | it 'must have an inner radius' do 45 | subject.inner_radius.must_equal 5 46 | end 47 | 48 | it 'must have an outer diameter' do 49 | subject.outer_diameter.must_equal 20 50 | end 51 | 52 | it 'must have a radius' do 53 | subject.radius.must_equal 10 54 | subject.outer_radius.must_equal 10 55 | end 56 | end 57 | 58 | describe 'when constructed with a center, inner_diameter and diameter' do 59 | subject { Geometry::Annulus.new Point[1,2], inner_diameter:5, diameter:10 } 60 | 61 | it 'must have a center' do 62 | subject.center.must_equal Point[1,2] 63 | end 64 | 65 | it 'must have an inner diameter' do 66 | subject.inner_diameter.must_equal 5 67 | end 68 | 69 | it 'must have an inner radius' do 70 | subject.inner_radius.must_equal 2.5 71 | end 72 | 73 | it 'must have an outer diameter' do 74 | subject.outer_diameter.must_equal 10 75 | end 76 | 77 | it 'must have a radius' do 78 | subject.radius.must_equal 5 79 | end 80 | end 81 | end 82 | -------------------------------------------------------------------------------- /test/geometry/arc.rb: -------------------------------------------------------------------------------- 1 | require 'minitest/autorun' 2 | require 'geometry/arc' 3 | 4 | describe Geometry::Arc do 5 | Arc = Geometry::Arc 6 | 7 | describe "when constructed" do 8 | it "must accept a center point, radius, start and end angles" do 9 | arc = Geometry::Arc.new center:[1,2], radius:3, start:0, end:90 10 | arc.must_be_kind_of Geometry::Arc 11 | arc.center.must_equal Point[1,2] 12 | arc.radius.must_equal 3 13 | arc.start_angle.must_equal 0 14 | arc.end_angle.must_equal 90 15 | end 16 | 17 | it "must create an Arc from center, start and end points" do 18 | arc = Geometry::Arc.new center:[1,2], start:[3,4], end:[5,6] 19 | arc.must_be_kind_of Geometry::Arc 20 | arc.center.must_equal Point[1,2] 21 | arc.first.must_equal Point[3,4] 22 | arc.last.must_equal Point[5,6] 23 | end 24 | end 25 | end 26 | 27 | describe Geometry::ThreePointArc do 28 | it 'must have an ending angle' do 29 | arc = Geometry::ThreePointArc.new([0,0], [1,0], [0,1]) 30 | arc.end_angle.must_equal Math::PI/2 31 | end 32 | 33 | it 'must have a radius' do 34 | arc = Geometry::ThreePointArc.new([0,0], [1,0], [0,1]) 35 | arc.radius.must_equal 1 36 | end 37 | 38 | it 'must have an starting angle' do 39 | arc = Geometry::ThreePointArc.new([0,0], [1,0], [0,1]) 40 | arc.start_angle.must_equal 0 41 | end 42 | 43 | describe 'max' do 44 | # Cosine and sine of a 22.5 degree angle 45 | let(:cos) { 0.9239556995 } 46 | let(:sin) { 0.3824994973 } 47 | let(:radius) { 1.0000000000366434 } 48 | 49 | it 'must handle an Arc entirely in quadrant 1' do 50 | arc = Geometry::ThreePointArc.new([0,0], [cos,sin], [sin,cos]) 51 | arc.max.must_equal Point[cos,cos] 52 | end 53 | 54 | it 'must handle a counterclockwise Arc from quadrant 1 to quadrant 2' do 55 | arc = Geometry::ThreePointArc.new([0,0], [cos,sin], [-cos,sin]) 56 | arc.max.must_equal Point[cos,radius] 57 | end 58 | 59 | it 'must handle a counterclockwise Arc from quadrant 4 to quadrant 1' do 60 | arc = Geometry::ThreePointArc.new([0,0], [sin,-cos], [sin,cos]) 61 | arc.max.must_equal Point[radius,cos] 62 | end 63 | 64 | it 'must handle a counterclockwise Arc from quadrant 3 to quadrant 2' do 65 | arc = Geometry::ThreePointArc.new([0,0], [-cos,-sin], [-cos,sin]) 66 | arc.max.must_equal Point[radius,radius] 67 | end 68 | end 69 | 70 | describe 'min' do 71 | # Cosine and sine of a 22.5 degree angle 72 | let(:cos) { 0.9239556995 } 73 | let(:sin) { 0.3824994973 } 74 | let(:radius) { 1.0000000000366434 } 75 | 76 | it 'must handle an Arc entirely in quadrant 1' do 77 | arc = Geometry::ThreePointArc.new([0,0], [cos,sin], [sin,cos]) 78 | arc.min.must_equal Point[sin,sin] 79 | end 80 | 81 | it 'must handle a counterclockwise Arc from quadrant 1 to quadrant 2' do 82 | arc = Geometry::ThreePointArc.new([0,0], [cos,sin], [-cos,sin]) 83 | arc.min.must_equal Point[-cos,sin] 84 | end 85 | 86 | it 'must handle a counterclockwise Arc from quadrant 4 to quadrant 1' do 87 | arc = Geometry::ThreePointArc.new([0,0], [sin,-cos], [sin,cos]) 88 | arc.min.must_equal Point[sin,-cos] 89 | end 90 | 91 | it 'must handle a counterclockwise Arc from quadrant 3 to quadrant 2' do 92 | arc = Geometry::ThreePointArc.new([0,0], [-cos,-sin], [-cos,sin]) 93 | arc.min.must_equal Point[-cos,-radius] 94 | end 95 | end 96 | 97 | describe 'minmax' do 98 | # Cosine and sine of a 22.5 degree angle 99 | let(:cos) { 0.9239556995 } 100 | let(:sin) { 0.3824994973 } 101 | let(:radius) { 1.0000000000366434 } 102 | 103 | it 'must handle an Arc entirely in quadrant 1' do 104 | arc = Geometry::ThreePointArc.new([0,0], [cos,sin], [sin,cos]) 105 | arc.minmax.must_equal [Point[sin,sin], Point[cos,cos]] 106 | end 107 | 108 | it 'must handle a counterclockwise Arc from quadrant 1 to quadrant 2' do 109 | arc = Geometry::ThreePointArc.new([0,0], [cos,sin], [-cos,sin]) 110 | arc.minmax.must_equal [Point[-cos,sin], Point[cos,radius]] 111 | end 112 | 113 | it 'must handle a counterclockwise Arc from quadrant 4 to quadrant 1' do 114 | arc = Geometry::ThreePointArc.new([0,0], [sin,-cos], [sin,cos]) 115 | arc.minmax.must_equal [Point[sin,-cos], Point[radius,cos]] 116 | end 117 | 118 | it 'must handle a counterclockwise Arc from quadrant 3 to quadrant 2' do 119 | arc = Geometry::ThreePointArc.new([0,0], [-cos,-sin], [-cos,sin]) 120 | arc.minmax.must_equal [Point[-cos,-radius], Point[radius,radius]] 121 | end 122 | end 123 | end -------------------------------------------------------------------------------- /test/geometry/bezier.rb: -------------------------------------------------------------------------------- 1 | require 'minitest/autorun' 2 | require 'geometry/bezier' 3 | 4 | describe Geometry::Bezier do 5 | subject { Geometry::Bezier.new [0,0], [1,1], [2,2], [3,3] } 6 | 7 | it 'must have control points' do 8 | subject.points.length.must_equal 4 9 | end 10 | 11 | it 'must generate Pascals Triangle' do 12 | subject.binomial_coefficient(0).must_equal 1 13 | subject.binomial_coefficient(1).must_equal 3 14 | subject.binomial_coefficient(2).must_equal 3 15 | subject.binomial_coefficient(3).must_equal 1 16 | end 17 | 18 | it 'must return nil when t is out of range' do 19 | subject[2].must_equal nil 20 | end 21 | 22 | it 'must subscript on the parameter' do 23 | subject[0.5].must_equal Point[1.5, 1.5] 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /test/geometry/circle.rb: -------------------------------------------------------------------------------- 1 | require 'minitest/autorun' 2 | require 'geometry/circle' 3 | 4 | describe Geometry::Circle do 5 | Circle = Geometry::Circle 6 | 7 | describe "when constructed with center and radius arguments" do 8 | let(:circle) { Circle.new [1,2], 3 } 9 | 10 | it "must create a Circle" do 11 | circle.must_be_instance_of(Circle) 12 | end 13 | 14 | it "must have a center point accessor" do 15 | circle.center.must_equal Point[1,2] 16 | end 17 | 18 | it "must have a radius accessor" do 19 | circle.radius.must_equal 3 20 | end 21 | 22 | it "must compare equal" do 23 | circle.must_equal Circle.new([1,2], 3) 24 | end 25 | end 26 | 27 | describe "when constructed with named center and radius arguments" do 28 | let(:circle) { Circle.new :center => [1,2], :radius => 3 } 29 | 30 | it "must create a Circle" do 31 | circle.must_be_instance_of(Circle) 32 | end 33 | 34 | it "must have a center point accessor" do 35 | circle.center.must_equal Point[1,2] 36 | end 37 | 38 | it "must have a radius accessor" do 39 | circle.radius.must_equal 3 40 | end 41 | 42 | it "must compare equal" do 43 | (circle == Circle.new(:center => [1,2], :radius => 3)).must_equal true 44 | end 45 | end 46 | 47 | describe "when constructed with named center and diameter arguments" do 48 | let(:circle) { Circle.new center:[1,2], diameter:4 } 49 | 50 | it "must be a CenterDiameterCircle" do 51 | circle.must_be_instance_of(Geometry::CenterDiameterCircle) 52 | circle.must_be_kind_of(Circle) 53 | end 54 | 55 | it "must have a center" do 56 | circle.center.must_equal Point[1,2] 57 | end 58 | 59 | it "must have a diameter" do 60 | circle.diameter.must_equal 4 61 | end 62 | 63 | it "must calculate the correct radius" do 64 | circle.radius.must_equal 2 65 | end 66 | 67 | it "must compare equal" do 68 | circle.must_equal Circle.new([1,2], :diameter => 4) 69 | end 70 | end 71 | 72 | describe "when constructed with a diameter and no center" do 73 | let(:circle) { Circle.new :diameter => 4 } 74 | 75 | it "must be a CenterDiameterCircle" do 76 | circle.must_be_instance_of(Geometry::CenterDiameterCircle) 77 | circle.must_be_kind_of(Circle) 78 | end 79 | 80 | it "must have a nil center" do 81 | circle.center.must_be_kind_of Geometry::PointZero 82 | end 83 | 84 | it "must have a diameter" do 85 | circle.diameter.must_equal 4 86 | end 87 | 88 | it "must calculate the correct radius" do 89 | circle.radius.must_equal 2 90 | end 91 | 92 | it 'must have the correct min values' do 93 | circle.min.must_equal Point[-2, -2] 94 | circle.min.must_be_instance_of Geometry::PointIso 95 | end 96 | 97 | it 'must have the correct max values' do 98 | circle.max.must_equal Point[2, 2] 99 | circle.max.must_be_instance_of Geometry::PointIso 100 | end 101 | 102 | it 'must have the correct minmax values' do 103 | circle.minmax.must_equal [Point[-2, -2], Point[2,2]] 104 | end 105 | end 106 | 107 | describe 'when constructed with a Rational diameter and no center' do 108 | let(:circle) { Circle.new :diameter => Rational(5,3) } 109 | 110 | it 'must have the correct min values' do 111 | circle.min.must_equal Point[-5.to_r/6, -5.to_r/6] 112 | circle.min.must_be_instance_of Geometry::PointIso 113 | end 114 | 115 | it 'must have the correct max values' do 116 | circle.max.must_equal Point[5.to_r/6, 5.to_r/6] 117 | circle.max.must_be_instance_of Geometry::PointIso 118 | end 119 | 120 | it 'must have the correct minmax values' do 121 | circle.minmax.must_equal [Point[-5.to_r/6, -5.to_r/6], Point[5.to_r/6,5.to_r/6]] 122 | end 123 | end 124 | 125 | describe "properties" do 126 | subject { Circle.new center:[1,2], :diameter => 4 } 127 | 128 | it "must have a bounds property that returns a Rectangle" do 129 | subject.bounds.must_equal Rectangle.new([-1,0], [3,4]) 130 | end 131 | 132 | it 'must always be closed' do 133 | subject.closed?.must_equal true 134 | end 135 | 136 | it "must have a minmax property that returns the corners of the bounding rectangle" do 137 | subject.minmax.must_equal [Point[-1,0], Point[3,4]] 138 | end 139 | 140 | it "must have a max property that returns the upper right corner of the bounding rectangle" do 141 | subject.max.must_equal Point[3,4] 142 | end 143 | 144 | it "must have a min property that returns the lower left corner of the bounding rectangle" do 145 | subject.min.must_equal Point[-1,0] 146 | end 147 | end 148 | end 149 | -------------------------------------------------------------------------------- /test/geometry/edge.rb: -------------------------------------------------------------------------------- 1 | require 'minitest/autorun' 2 | require 'geometry/edge' 3 | 4 | def Edge(*args) 5 | Geometry::Edge.new(*args) 6 | end 7 | 8 | describe Geometry::Edge do 9 | Edge = Geometry::Edge 10 | subject { Geometry::Edge.new [0,0], [1,1] } 11 | 12 | it "must create an Edge object" do 13 | edge = Edge.new([0,0], [1,0]) 14 | assert_kind_of(Geometry::Edge, edge) 15 | assert_equal(Geometry::Point[0,0], edge.first) 16 | assert_equal(Geometry::Point[1,0], edge.last) 17 | end 18 | 19 | it 'must have a convenience initializer' do 20 | edge = Edge([0,0], [1,0]) 21 | edge.must_be_kind_of Geometry::Edge 22 | edge.must_equal Edge.new([0,0], [1,0]) 23 | end 24 | 25 | it "must handle equality" do 26 | edge1 = Edge.new([1,0], [0,1]) 27 | edge2 = Edge.new([1,0], [0,1]) 28 | edge3 = Edge.new([1,1], [5,5]) 29 | assert_equal(edge1, edge2) 30 | edge1.wont_equal edge3 31 | end 32 | 33 | it 'must have a length' do 34 | Edge.new([0,0], [1,0]).length.must_equal 1 35 | end 36 | 37 | it "must return the height of the edge" do 38 | edge = Edge([0,0], [1,1]) 39 | assert_equal(1, edge.height) 40 | end 41 | 42 | it "must return the width of the edge" do 43 | edge = Edge([0,0], [1,1]) 44 | assert_equal(1, edge.width) 45 | end 46 | 47 | it "must convert an Edge to a Vector" do 48 | Edge.new([0,0], [1,0]).vector.must_equal Vector[1,0] 49 | end 50 | 51 | it "must return the normalized direction of a vector" do 52 | Edge.new([0,0], [1,0]).direction.must_equal Vector[1,0] 53 | end 54 | 55 | it "must return true for parallel edges" do 56 | Edge.new([0,0], [1,0]).parallel?(Edge.new([0,0], [1,0])).must_equal 1 57 | Edge.new([0,0], [1,0]).parallel?(Edge.new([1,0], [2,0])).must_equal 1 58 | Edge.new([0,0], [1,0]).parallel?(Edge.new([3,0], [4,0])).must_equal 1 59 | Edge.new([0,0], [1,0]).parallel?(Edge.new([3,1], [4,1])).must_equal 1 60 | end 61 | 62 | it "must return false for non-parallel edges" do 63 | Edge.new([0,0], [2,0]).parallel?(Edge.new([1,-1], [1,1])).must_equal false 64 | end 65 | 66 | it "must clone and reverse" do 67 | reversed = subject.reverse 68 | reversed.to_a.must_equal subject.to_a.reverse 69 | reversed.wont_be_same_as subject 70 | end 71 | 72 | it "must reverse itself" do 73 | original = subject.to_a 74 | subject.reverse! 75 | subject.to_a.must_equal original.reverse 76 | end 77 | 78 | describe 'attributes' do 79 | it 'must have a maximum' do 80 | Edge([0,0], [1,1]).max.must_equal Point[1,1] 81 | end 82 | 83 | it 'must have a minimum' do 84 | Edge([0,0], [1,1]).min.must_equal Point[0,0] 85 | end 86 | 87 | it 'must have a minmax' do 88 | Edge([0,0], [1,1]).minmax.must_equal [Point[0,0], Point[1,1]] 89 | end 90 | end 91 | 92 | describe "spaceship" do 93 | it "ascending with a Point" do 94 | edge = Edge.new [0,0], [1,1] 95 | (edge <=> Point[0,0]).must_equal 0 96 | (edge <=> Point[1,0]).must_equal(-1) 97 | (edge <=> Point[0,1]).must_equal 1 98 | (edge <=> Point[2,2]).must_equal nil 99 | end 100 | 101 | it "descending with a Point" do 102 | edge = Edge.new [1,1], [0,0] 103 | (edge <=> Point[0,0]).must_equal 0 104 | (edge <=> Point[1,0]).must_equal 1 105 | (edge <=> Point[0,1]).must_equal(-1) 106 | (edge <=> Point[2,2]).must_equal nil 107 | end 108 | end 109 | 110 | describe "when finding an intersection" do 111 | it "must find the intersection of two end-intersecting Edges" do 112 | intersection = Edge.new([0,0],[1,1]).intersection(Edge.new([0,1],[1,1])) 113 | intersection.must_be_kind_of Geometry::Point 114 | intersection.must_equal Geometry::Point[1,1] 115 | end 116 | 117 | it "must find the intersection of two collinear end-intersecting Edges" do 118 | intersection = Edge.new([2,2], [0,2]).intersection(Edge.new([3,2], [2,2])) 119 | intersection.must_be_kind_of Geometry::Point 120 | intersection.must_equal Geometry::Point[2,2] 121 | 122 | intersection = Edge.new([0,2], [2,2]).intersection(Edge.new([2,2], [3,2])) 123 | intersection.must_be_kind_of Geometry::Point 124 | intersection.must_equal Geometry::Point[2,2] 125 | end 126 | 127 | it "must find the itersection of two crossed Edges" do 128 | edge1 = Edge.new [0.0, 0], [2.0, 2.0] 129 | edge2 = Edge.new [2.0, 0], [0.0, 2.0] 130 | intersection = edge1.intersection edge2 131 | intersection.must_be_kind_of Geometry::Point 132 | intersection.must_equal Geometry::Point[1,1] 133 | end 134 | 135 | it "must return nil for two edges that do not intersect" do 136 | Edge.new([0,0],[1,0]).intersection(Edge.new([0,1],[1,1])).must_equal nil 137 | end 138 | 139 | it "must return true for two collinear and overlapping edges" do 140 | Edge.new([0,0],[2,0]).intersection(Edge.new([1,0],[3,0])).must_equal true 141 | end 142 | 143 | it "must return false for collinear but non-overlapping edges" do 144 | Edge.new([0,0],[2,0]).intersection(Edge.new([3,0],[4,0])).must_equal false 145 | Edge.new([0,0],[0,2]).intersection(Edge.new([0,3],[0,4])).must_equal false 146 | end 147 | 148 | it "must return nil for two parallel but not collinear edges" do 149 | Edge.new([0,0],[2,0]).intersection(Edge.new([1,1],[3,1])).must_equal nil 150 | end 151 | 152 | it "must return nil for two perpendicular but not interseting edges" do 153 | Edge.new([0, 0], [2, 0]).intersection(Edge.new([3, 3], [3, 1])).must_equal nil 154 | end 155 | end 156 | end 157 | -------------------------------------------------------------------------------- /test/geometry/line.rb: -------------------------------------------------------------------------------- 1 | require 'minitest/autorun' 2 | require 'geometry/line' 3 | 4 | describe Geometry::Line do 5 | Line = Geometry::Line 6 | Point = Geometry::Point 7 | 8 | describe "when initializing" do 9 | it "must accept two named points" do 10 | line = Line.new(from:Point[0,0], to:Point[10,10]) 11 | line.must_be_kind_of(Line) 12 | line.must_be_instance_of(Geometry::TwoPointLine) 13 | line.first.must_equal Point[0,0] 14 | line.last.must_equal Point[10,10] 15 | end 16 | 17 | it "must accept named start and end points" do 18 | line = Line.new(start:Point[0,0], end:Point[10,10]) 19 | line.must_be_kind_of(Line) 20 | line.must_be_instance_of(Geometry::TwoPointLine) 21 | line.first.must_equal Point[0,0] 22 | line.last.must_equal Point[10,10] 23 | end 24 | 25 | it "must raise an exception when no arguments are given" do 26 | -> { Line.new }.must_raise ArgumentError 27 | end 28 | end 29 | 30 | it "create a Line object from 2 Points" do 31 | line = Geometry::Line[Geometry::Point[0,0], Geometry::Point[10,10]] 32 | assert_kind_of(Geometry::Line, line) 33 | assert_kind_of(Geometry::TwoPointLine, line) 34 | end 35 | it "create a Line object from two arrays" do 36 | line = Geometry::Line[[0,0], [10,10]] 37 | assert(line.is_a?(Geometry::Line)) 38 | assert_kind_of(Geometry::TwoPointLine, line) 39 | assert_kind_of(Geometry::Point, line.first) 40 | assert_kind_of(Geometry::Point, line.last) 41 | end 42 | it "create a Line object from two Vectors" do 43 | line = Geometry::Line[Vector[0,0], Vector[10,10]] 44 | assert(line.is_a?(Geometry::Line)) 45 | assert_kind_of(Geometry::TwoPointLine, line) 46 | end 47 | 48 | it "create a Line from a slope and y-intercept" do 49 | line = Geometry::Line[0.75, 5] 50 | assert(line.is_a?(Geometry::Line)) 51 | assert_kind_of(Geometry::SlopeInterceptLine, line) 52 | assert_equal(5, line.intercept) 53 | assert_equal(0.75, line.slope) 54 | end 55 | 56 | it "create a Line from a Rational slope and y-intercept" do 57 | line = Geometry::Line[Rational(3,4), 5] 58 | assert_kind_of(Geometry::SlopeInterceptLine, line) 59 | assert(line.is_a?(Geometry::Line)) 60 | assert_equal(Rational(3,4), line.slope) 61 | end 62 | 63 | it "have a special constructor for horizontal lines" do 64 | line = Geometry::Line.horizontal 65 | assert(line.horizontal?) 66 | end 67 | it "have a special constructor for vertical lines" do 68 | line = Geometry::Line.vertical 69 | assert(line.vertical?) 70 | end 71 | 72 | it "have accessor for y-intercept" do 73 | line = Geometry::Line[0.75, 5] 74 | assert_equal(5, line.intercept) 75 | assert_equal(5, line.intercept(:y)) 76 | end 77 | it "have accessor for x-intercept" do 78 | line = Geometry::Line.vertical(7) 79 | assert_equal(7, line.intercept(:x)) 80 | end 81 | 82 | it "return the correct x-intercept for vertical lines" do 83 | line = Geometry::Line.vertical(7) 84 | assert_equal(7, line.intercept(:x)) 85 | end 86 | it "return the correct y-intercept for horizontal lines" do 87 | line = Geometry::Line.horizontal(4) 88 | assert_equal(4, line.intercept(:y)) 89 | end 90 | 91 | it "return nil x-intercept for horizontal lines" do 92 | line = Geometry::Line.horizontal 93 | assert_nil(line.intercept(:x)) 94 | end 95 | it "return nil y-intercept for vertical lines" do 96 | line = Geometry::Line.vertical 97 | assert_nil(line.intercept(:y)) 98 | end 99 | 100 | it "implement inspect" do 101 | line = Geometry::Line[[0,0], [10,10]] 102 | assert_equal('Line(Point[0, 0], Point[10, 10])', line.inspect) 103 | end 104 | it "implement to_s" do 105 | line = Geometry::Line[[0,0], [10,10]] 106 | assert_equal('Line(Point[0, 0], Point[10, 10])', line.to_s) 107 | end 108 | end 109 | 110 | describe Geometry::PointSlopeLine do 111 | subject { Geometry::PointSlopeLine.new [1,2], 3 } 112 | 113 | it "must have a slope attribute" do 114 | subject.slope.must_equal 3 115 | end 116 | 117 | it "must handle equality" do 118 | line2 = Geometry::PointSlopeLine.new([1,2], 3) 119 | line3 = Geometry::PointSlopeLine.new([1,1], 4) 120 | subject.must_equal line2 121 | subject.wont_equal line3 122 | end 123 | 124 | it 'must handle equality with a SlopeInterceptLine' do 125 | line2 = Geometry::SlopeInterceptLine.new(3, -1) 126 | line3 = Geometry::SlopeInterceptLine.new(4, -1) 127 | line2.must_equal subject 128 | line3.wont_equal subject 129 | end 130 | 131 | it 'must handle equality with a TwoPointLine' do 132 | line2 = Geometry::TwoPointLine.new([1,2], [2,5]) 133 | line3 = Geometry::TwoPointLine.new([1,2], [2,4]) 134 | line2.must_equal subject 135 | line3.wont_equal subject 136 | end 137 | 138 | it 'must know how to be horizontal' do 139 | Geometry::PointSlopeLine.new([1,2],0).horizontal?.must_equal true 140 | Geometry::PointSlopeLine.new([1,2],1).horizontal?.must_equal false 141 | end 142 | 143 | it 'must know how to be vertical' do 144 | Geometry::PointSlopeLine.new([1,2], Float::INFINITY).vertical?.must_equal true 145 | Geometry::PointSlopeLine.new([1,2], -Float::INFINITY).vertical?.must_equal true 146 | Geometry::PointSlopeLine.new([1,2],1).vertical?.must_equal false 147 | end 148 | 149 | it 'must have an x-intercept' do 150 | subject.intercept(:x).must_equal 1 151 | end 152 | 153 | it 'must not have an x-intercept for horizontal lines' do 154 | Geometry::PointSlopeLine.new([1,2], 0).intercept(:x).must_equal nil 155 | end 156 | 157 | it 'must have a y-intercept' do 158 | subject.intercept.must_equal(-1) 159 | end 160 | end 161 | 162 | describe Geometry::SlopeInterceptLine do 163 | subject { Geometry::SlopeInterceptLine.new 3, 2 } 164 | 165 | it "must have a slope attribute" do 166 | subject.slope.must_equal 3 167 | end 168 | 169 | it "must handle equality" do 170 | line2 = Geometry::SlopeInterceptLine.new(3, 2) 171 | line3 = Geometry::SlopeInterceptLine.new(4, 3) 172 | subject.must_equal line2 173 | subject.wont_equal line3 174 | end 175 | 176 | it 'must handle equality with a PointSlopeLine' do 177 | line2 = Geometry::PointSlopeLine.new([0,2], 3) 178 | line3 = Geometry::PointSlopeLine.new([0,2], 2) 179 | line2.must_equal subject 180 | line3.wont_equal subject 181 | end 182 | 183 | it 'must handle equality with a TwoPointLine' do 184 | line2 = Geometry::TwoPointLine.new([0,2], [1,5]) 185 | line3 = Geometry::TwoPointLine.new([0,2], [1,4]) 186 | line2.must_equal subject 187 | line3.wont_equal subject 188 | end 189 | 190 | it 'must know how to be horizontal' do 191 | Geometry::SlopeInterceptLine.new(0, 2).horizontal?.must_equal true 192 | Geometry::SlopeInterceptLine.new(1, 2).horizontal?.must_equal false 193 | end 194 | 195 | it 'must know how to be vertical' do 196 | Geometry::SlopeInterceptLine.new(Float::INFINITY, 2).vertical?.must_equal true 197 | Geometry::SlopeInterceptLine.new(-Float::INFINITY, 2).vertical?.must_equal true 198 | Geometry::SlopeInterceptLine.new(1, 2).vertical?.must_equal false 199 | end 200 | 201 | it 'must have an x-intercept' do 202 | subject.intercept(:x).must_equal(-2/3) 203 | end 204 | 205 | it 'must not have an x-intercept for horizontal lines' do 206 | Geometry::SlopeInterceptLine.new(0, 2).intercept(:x).must_equal nil 207 | end 208 | 209 | it 'must have a y-intercept' do 210 | subject.intercept.must_equal 2 211 | end 212 | end 213 | 214 | describe Geometry::TwoPointLine do 215 | subject { Geometry::TwoPointLine.new [1,2], [3,4] } 216 | 217 | it "must have a slope attribute" do 218 | subject.slope.must_equal 1 219 | end 220 | 221 | it "must handle equality" do 222 | line2 = Geometry::TwoPointLine.new([1,2], [3,4]) 223 | line3 = Geometry::TwoPointLine.new([1,1], [5,5]) 224 | subject.must_equal line2 225 | subject.wont_equal line3 226 | end 227 | 228 | it 'must handle equality with a PointSlopeLine' do 229 | line2 = Geometry::PointSlopeLine.new([1,2], 1) 230 | line3 = Geometry::PointSlopeLine.new([1,2], 2) 231 | line2.must_equal subject 232 | line3.wont_equal subject 233 | end 234 | 235 | it 'must handle equality with a SlopeInterceptLine' do 236 | line2 = Geometry::SlopeInterceptLine.new(1, 1) 237 | line3 = Geometry::SlopeInterceptLine.new(1, -1) 238 | line2.must_equal subject 239 | line3.wont_equal subject 240 | end 241 | 242 | it 'must know how to be horizontal' do 243 | Geometry::TwoPointLine.new([1,2],[2,2]).horizontal?.must_equal true 244 | Geometry::TwoPointLine.new([1,2],[3,4]).horizontal?.must_equal false 245 | end 246 | 247 | it 'must know how to be vertical' do 248 | Geometry::TwoPointLine.new([1,2], [1,3]).vertical?.must_equal true 249 | Geometry::TwoPointLine.new([1,2], [1,-3]).vertical?.must_equal true 250 | Geometry::TwoPointLine.new([1,2],[3,4]).vertical?.must_equal false 251 | end 252 | 253 | it 'must have an x-intercept' do 254 | subject.intercept(:x).must_equal(-1) 255 | end 256 | 257 | it 'must not have an x-intercept for horizontal lines' do 258 | Geometry::TwoPointLine.new([1,2],[2,2]).intercept(:x).must_equal nil 259 | end 260 | 261 | it 'must have an x-intercept for vertical lines' do 262 | Geometry::TwoPointLine.new([1,2], [1,3]).intercept(:x).must_equal 1 263 | end 264 | 265 | it 'must have a y-intercept' do 266 | subject.intercept.must_equal 1 267 | end 268 | end 269 | -------------------------------------------------------------------------------- /test/geometry/obround.rb: -------------------------------------------------------------------------------- 1 | require 'minitest/autorun' 2 | require 'geometry/obround' 3 | 4 | describe Geometry::Obround do 5 | Obround = Geometry::Obround 6 | 7 | describe "when constructed" do 8 | it "must accept two Points" do 9 | obround = Geometry::Obround.new [1,2], [3,4] 10 | obround.must_be_kind_of Geometry::Obround 11 | end 12 | 13 | it "must accept a width and height" do 14 | obround = Geometry::Obround.new 2, 3 15 | obround.must_be_kind_of Geometry::Obround 16 | obround.height.must_equal 3 17 | obround.width.must_equal 2 18 | end 19 | 20 | it "must compare equal" do 21 | obround = Geometry::Obround.new [1,2], [3,4] 22 | obround.must_equal Obround.new([1,2], [3,4]) 23 | end 24 | end 25 | 26 | it 'must always be closed' do 27 | obround = Geometry::Obround.new 2, 3 28 | obround.closed?.must_equal true 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /test/geometry/path.rb: -------------------------------------------------------------------------------- 1 | require 'minitest/autorun' 2 | require 'geometry/path' 3 | 4 | describe Geometry::Path do 5 | describe "construction" do 6 | it "must create a Path with no arguments" do 7 | path = Geometry::Path.new 8 | path.must_be_kind_of Geometry::Path 9 | path.elements.wont_be_nil 10 | path.elements.size.must_equal 0 11 | end 12 | 13 | it "must create a Path from Points" do 14 | path = Geometry::Path.new Point[1,1], Point[2,2], Point[3,3] 15 | path.elements.size.must_equal 2 16 | path.elements.each {|a| a.must_be_kind_of Geometry::Edge } 17 | end 18 | 19 | it "with connected Edges" do 20 | path = Geometry::Path.new Edge.new([1,1], [2,2]), Edge.new([2,2], [3,3]) 21 | path.elements.size.must_equal 2 22 | path.elements.each {|a| a.must_be_kind_of Geometry::Edge } 23 | end 24 | 25 | it "with disjoint Edges" do 26 | path = Geometry::Path.new Edge.new([1,1], [2,2]), Edge.new([3,3], [4,4]) 27 | path.elements.size.must_equal 3 28 | path.elements.each {|a| a.must_be_kind_of Geometry::Edge } 29 | end 30 | 31 | it "with Points and Arcs" do 32 | path = Geometry::Path.new [0,0], [1.0,0.0], Arc.new(center:[1,1], radius:1, start:-90*Math::PI/180, end:0), [2.0,1.0], [1,2] 33 | path.elements.size.must_equal 3 34 | path.elements[0].must_be_kind_of Geometry::Edge 35 | path.elements[1].must_be_kind_of Geometry::Arc 36 | path.elements[2].must_be_kind_of Geometry::Edge 37 | end 38 | 39 | it "with Edges and Arcs" do 40 | path = Geometry::Path.new Edge.new([0,0], [1.0,0.0]), Arc.new(center:[1,1], radius:1, start:-90*Math::PI/180, end:0), Edge.new([2.0,1.0], [1,2]) 41 | path.elements.size.must_equal 3 42 | path.elements[0].must_be_kind_of Geometry::Edge 43 | path.elements[1].must_be_kind_of Geometry::Arc 44 | path.elements[2].must_be_kind_of Geometry::Edge 45 | end 46 | 47 | it "with disjoint Edges and Arcs" do 48 | path = Geometry::Path.new Edge.new([0,0], [1,0]), Arc.new(center:[2,1], radius:1, start:-90*Math::PI/180, end:0), Edge.new([3,1], [1,2]) 49 | path.elements.size.must_equal 4 50 | path.elements[0].must_be_kind_of Geometry::Edge 51 | path.elements[1].must_be_kind_of Geometry::Edge 52 | path.elements[2].must_be_kind_of Geometry::Arc 53 | path.elements[3].must_be_kind_of Geometry::Edge 54 | end 55 | 56 | it "with disjoint Arcs" do 57 | path = Geometry::Path.new Arc.new(center:[2,1], radius:1, start:-90*Math::PI/180, end:0), Arc.new(center:[3,1], radius:1, start:-90*Math::PI/180, end:0) 58 | path.elements.size.must_equal 3 59 | path.elements[0].must_be_kind_of Geometry::Arc 60 | path.elements[1].must_be_kind_of Geometry::Edge 61 | path.elements[2].must_be_kind_of Geometry::Arc 62 | 63 | path.elements[0].last.must_equal path.elements[1].first 64 | end 65 | end 66 | 67 | describe 'attributes' do 68 | let(:unit_square) { Geometry::Path.new [0,0], [1,0], [1,1], [0,1] } 69 | 70 | it 'must know the max' do 71 | unit_square.max.must_equal Point[1,1] 72 | end 73 | 74 | it 'must know the min' do 75 | unit_square.min.must_equal Point[0,0] 76 | end 77 | 78 | it 'must know the min and the max' do 79 | unit_square.minmax.must_equal [Point[0,0], Point[1,1]] 80 | end 81 | end 82 | end 83 | -------------------------------------------------------------------------------- /test/geometry/point_iso.rb: -------------------------------------------------------------------------------- 1 | require 'minitest/autorun' 2 | require 'geometry/point_iso' 3 | 4 | describe Geometry::PointIso do 5 | let(:iso_value) { 5 } 6 | subject { Geometry::PointIso.new(5) } 7 | 8 | it 'must pop' do 9 | subject.pop.must_equal Point[5] 10 | subject.pop(2).must_equal Point[5, 5] 11 | end 12 | 13 | it 'must shift' do 14 | subject.shift.must_equal Point[5] 15 | subject.shift(2).must_equal Point[5, 5] 16 | end 17 | 18 | describe 'minmax' do 19 | it 'must have a minimum' do 20 | subject.min.must_equal 5 21 | end 22 | 23 | it 'must minimum with another Point' do 24 | subject.min(Point[4,7]).must_equal Point[4,5] 25 | subject.min(Point[4,7]).must_be_kind_of Point 26 | end 27 | 28 | it 'must minimum with an Array' do 29 | subject.min([4,7]).must_equal Point[4,5] 30 | end 31 | 32 | it 'must minimum with a multiple arguments' do 33 | subject.min(4,7).must_equal Point[4,5] 34 | end 35 | 36 | it 'must have a maximum' do 37 | subject.max.must_equal 5 38 | end 39 | 40 | it 'must maximum with another Point' do 41 | subject.max(Point[7,2]).must_equal Point[7,5] 42 | subject.max(Point[7,2]).must_be_kind_of Point 43 | end 44 | 45 | it 'must maximum with an Array' do 46 | subject.max([7,2]).must_equal Point[7,5] 47 | end 48 | 49 | it 'must maximum with multiple arguments' do 50 | subject.max(7,2).must_equal Point[7,5] 51 | end 52 | 53 | it 'must have a minmax' do 54 | subject.minmax.must_equal [5,5] 55 | end 56 | 57 | it 'must minmax with another Point' do 58 | subject.minmax(Point[7,2]).must_equal [Point[5,2], Point[7,5]] 59 | end 60 | 61 | it 'must minmax with an Array' do 62 | subject.minmax([7,2]).must_equal [Point[5,2], Point[7,5]] 63 | end 64 | 65 | it 'must maximum with multiple arguments' do 66 | subject.minmax(7,2).must_equal [Point[5,2], Point[7,5]] 67 | end 68 | end 69 | 70 | describe 'arithmetic' do 71 | let(:left) { Point[1,2] } 72 | let(:right) { Point[3,4] } 73 | 74 | it 'must pretend to be a Point' do 75 | subject.is_a?(Point).must_equal true 76 | subject.kind_of?(Point).must_equal true 77 | 78 | subject.is_a?(Geometry::PointIso).must_equal true 79 | subject.kind_of?(Geometry::PointIso).must_equal true 80 | 81 | subject.instance_of?(Point).must_equal false 82 | subject.instance_of?(Geometry::PointIso).must_equal true 83 | end 84 | 85 | it 'must have +@' do 86 | (+subject).must_be :eql?, iso_value 87 | (+subject).must_be_instance_of(Geometry::PointIso) 88 | end 89 | 90 | it 'must have unary negation' do 91 | (-subject).must_be :eql?, -iso_value 92 | (-subject).must_be_instance_of(Geometry::PointIso) 93 | end 94 | 95 | describe 'Accessors' do 96 | it 'must return 1 for array access' do 97 | subject[3].must_equal iso_value 98 | end 99 | 100 | it 'must return 1 for named element access' do 101 | subject.x.must_equal iso_value 102 | subject.y.must_equal iso_value 103 | subject.z.must_equal iso_value 104 | end 105 | end 106 | 107 | it 'must add a number' do 108 | (subject + 3).must_equal (iso_value + 3) 109 | (3 + subject).must_equal (3 + iso_value) 110 | end 111 | 112 | it 'return a Point when adding two Points' do 113 | (subject + right).must_be_kind_of Point 114 | (left + subject).must_be_kind_of Point 115 | end 116 | 117 | it 'must return an Array when adding an array' do 118 | (subject + [5,6]).must_equal [iso_value+5, iso_value+6] 119 | # ([5,6] + subject).must_equal [10, 11] 120 | end 121 | 122 | it 'must return a Point when adding a Size' do 123 | (subject + Size[5,6]).must_be_instance_of(Point) 124 | (subject + Size[5,6]).must_equal Point[iso_value+5, iso_value+6] 125 | end 126 | 127 | describe 'when subtracting' do 128 | it 'must subtract a number' do 129 | (subject - 3).must_equal (iso_value - 3) 130 | (3 - subject).must_equal(-2) 131 | end 132 | 133 | it 'return a Point when subtracting two Points' do 134 | (subject - right).must_equal Point[iso_value - right.x, iso_value - right.y] 135 | (left - subject).must_equal Point[left.x - iso_value, left.y - iso_value] 136 | end 137 | 138 | it 'must return a Point when subtracting an array' do 139 | (subject - [5,6]).must_equal [0, -1] 140 | # ([5,6] - subject).must_equal [4,5] 141 | end 142 | 143 | it 'must return a Point when subtracting a Size' do 144 | (subject - Size[5,6]).must_be_instance_of(Point) 145 | (subject - Size[5,6]).must_equal Point[0,-1] 146 | end 147 | end 148 | 149 | it 'must multiply by a scalar' do 150 | (subject * 3).must_equal 15 151 | (subject * 3.0).must_equal 15.0 152 | end 153 | 154 | it 'must refuse to multiply by a Point' do 155 | -> { subject * Point[1, 2] }.must_raise Geometry::OperationNotDefined 156 | end 157 | 158 | it 'must refuse to multiply by a Vector' do 159 | -> { subject * Vector[2, 3] }.must_raise Geometry::OperationNotDefined 160 | end 161 | 162 | it 'must divide by a scalar' do 163 | (subject / 3).must_equal 5/3 164 | (subject / 4.0).must_equal 5/4.0 165 | end 166 | 167 | it 'must raise an exception when divided by 0' do 168 | -> { subject / 0 }.must_raise ZeroDivisionError 169 | end 170 | 171 | describe 'division' do 172 | it 'must raise an exception for Points' do 173 | lambda { subject / Point[1,2] }.must_raise Geometry::OperationNotDefined 174 | end 175 | 176 | it 'must raise an exception for Vectors' do 177 | lambda { subject / Vector[1,2] }.must_raise Geometry::OperationNotDefined 178 | end 179 | end 180 | end 181 | 182 | describe 'coercion' do 183 | it 'must coerce Arrays into Points' do 184 | subject.coerce([3,4]).must_equal [Point[3,4], Point[5, 5]] 185 | end 186 | 187 | it 'must coerce Vectors into Vectors' do 188 | subject.coerce(Vector[3,4]).must_equal [Vector[3,4], Vector[5, 5]] 189 | end 190 | 191 | it 'must coerce Points into Points' do 192 | subject.coerce(Point[5,6]).must_equal [Point[5,6], Point[5, 5]] 193 | end 194 | end 195 | 196 | describe 'comparison' do 197 | it 'must be equal to the same value' do 198 | subject.must_be :eql?, 5 199 | subject.must_be :eql?, 5.0 200 | end 201 | 202 | it 'must not be equal to a number of a different value' do 203 | 0.wont_equal subject 204 | 3.14.wont_equal subject 205 | end 206 | 207 | it 'must be equal to an Array of the same value' do 208 | subject.must_be :==, [5,5] 209 | subject.must_be :eql?, [5,5] 210 | subject.must_be :===, [5,5] 211 | [5,5].must_equal subject 212 | subject.must_equal [5,5] 213 | end 214 | 215 | it 'must not be equal to an Array of other values' do 216 | subject.wont_equal [3, 2, 1] 217 | [3, 2, 1].wont_equal subject 218 | end 219 | 220 | it 'must not be equal to a Point at the origin' do 221 | subject.wont_be :==, Point[0,0] 222 | subject.wont_be :eql?, Point[0,0] 223 | subject.wont_be :===, Point[0,0] 224 | Point[0,0].wont_equal subject 225 | subject.wont_equal Point[0,0] 226 | end 227 | 228 | it 'must not be equal to a Point not at the origin' do 229 | subject.wont_equal Point[3,2] 230 | Point[3,2].wont_equal subject 231 | end 232 | 233 | it 'must be equal to a Point of subjects' do 234 | subject.must_be :==, Point[iso_value, iso_value] 235 | subject.must_be :eql?, Point[iso_value, iso_value] 236 | subject.must_be :===, Point[iso_value, iso_value] 237 | Point[iso_value, iso_value].must_equal subject 238 | subject.must_equal Point[iso_value, iso_value] 239 | end 240 | 241 | it 'must be equal to an Vector of the same value' do 242 | subject.must_be :eql?, Vector[iso_value, iso_value] 243 | Vector[5, 5].must_equal subject 244 | end 245 | 246 | it 'must not be equal to a Vector of other values' do 247 | subject.wont_equal Vector[3,2] 248 | Vector[3,2].wont_equal subject 249 | end 250 | end 251 | end 252 | -------------------------------------------------------------------------------- /test/geometry/point_one.rb: -------------------------------------------------------------------------------- 1 | require 'minitest/autorun' 2 | require 'geometry/point_one' 3 | 4 | describe Geometry::PointOne do 5 | subject { Geometry::PointOne.new } 6 | let(:one) { Geometry::PointOne.new } 7 | 8 | it 'must pop' do 9 | subject.pop.must_equal Point[1] 10 | subject.pop(2).must_equal Point[1, 1] 11 | end 12 | 13 | it 'must shift' do 14 | subject.shift.must_equal Point[1] 15 | subject.shift(2).must_equal Point[1, 1] 16 | end 17 | 18 | describe 'minmax' do 19 | it 'must have a minimum' do 20 | subject.min.must_equal 1 21 | end 22 | 23 | it 'must minimum with another Point' do 24 | subject.min(Point[0,7]).must_equal Point[0,1] 25 | subject.min(Point[0,7]).must_be_kind_of Point 26 | end 27 | 28 | it 'must minimum with an Array' do 29 | subject.min([0,7]).must_equal Point[0,1] 30 | end 31 | 32 | it 'must minimum with a multiple arguments' do 33 | subject.min(0,7).must_equal Point[0,1] 34 | end 35 | 36 | it 'must have a maximum' do 37 | subject.max.must_equal 1 38 | end 39 | 40 | it 'must maximum with another Point' do 41 | subject.max(Point[7,0]).must_equal Point[7,1] 42 | subject.max(Point[7,0]).must_be_kind_of Point 43 | end 44 | 45 | it 'must maximum with an Array' do 46 | subject.max([7,0]).must_equal Point[7,1] 47 | end 48 | 49 | it 'must maximum with multiple arguments' do 50 | subject.max(7,0).must_equal Point[7,1] 51 | end 52 | 53 | it 'must have a minmax' do 54 | subject.minmax.must_equal [1,1] 55 | end 56 | 57 | it 'must minmax with another Point' do 58 | subject.minmax(Point[7,0]).must_equal [Point[1,0], Point[7,1]] 59 | end 60 | 61 | it 'must minmax with an Array' do 62 | subject.minmax([7,0]).must_equal [Point[1,0], Point[7,1]] 63 | end 64 | 65 | it 'must maximum with multiple arguments' do 66 | subject.minmax(7,0).must_equal [Point[1,0], Point[7,1]] 67 | end 68 | end 69 | 70 | describe 'arithmetic' do 71 | let(:left) { Point[1,2] } 72 | let(:right) { Point[3,4] } 73 | 74 | it 'must pretend to be a Point' do 75 | subject.is_a?(Point).must_equal true 76 | subject.kind_of?(Point).must_equal true 77 | 78 | subject.is_a?(Geometry::PointOne).must_equal true 79 | subject.kind_of?(Geometry::PointOne).must_equal true 80 | 81 | subject.instance_of?(Point).must_equal false 82 | subject.instance_of?(Geometry::PointOne).must_equal true 83 | end 84 | 85 | it 'must have +@' do 86 | (+one).must_be :eql?, 1 87 | (+one).must_be_instance_of(Geometry::PointOne) 88 | end 89 | 90 | it 'must have unary negation' do 91 | (-one).must_be :eql?, -1 92 | # (-one).must_be_instance_of(Geometry::PointOne) 93 | end 94 | 95 | describe 'Accessors' do 96 | it 'must return 1 for array access' do 97 | one[3].must_equal 1 98 | end 99 | 100 | it 'must return 1 for named element access' do 101 | one.x.must_equal 1 102 | one.y.must_equal 1 103 | one.z.must_equal 1 104 | end 105 | end 106 | 107 | it 'must add a number' do 108 | (one + 3).must_equal 4 109 | (3 + one).must_equal 4 110 | end 111 | 112 | it 'return a Point when adding two Points' do 113 | (one + right).must_be_kind_of Point 114 | (left + one).must_be_kind_of Point 115 | end 116 | 117 | it 'must return an Array when adding an array' do 118 | (one + [5,6]).must_equal [6, 7] 119 | ([5,6] + one).must_equal [5,6] 120 | end 121 | 122 | it 'must return a Point when adding a Size' do 123 | (one + Size[5,6]).must_be_instance_of(Point) 124 | (one + Size[5,6]).must_equal Point[6,7] 125 | end 126 | 127 | describe 'when subtracting' do 128 | it 'must subtract a number' do 129 | (one - 3).must_equal(-2) 130 | (3 - one).must_equal 2 131 | end 132 | 133 | it 'return a Point when subtracting two Points' do 134 | (one - right).must_equal Point[-2, -3] 135 | (left - one).must_equal Point[0, 1] 136 | end 137 | 138 | it 'must return a Point when subtracting an array' do 139 | (one - [5,6]).must_equal [-4, -5] 140 | # ([5,6] - one).must_equal [4,5] 141 | end 142 | 143 | it 'must return a Point when subtracting a Size' do 144 | (one - Size[5,6]).must_be_instance_of(Point) 145 | (one - Size[5,6]).must_equal Point[-4,-5] 146 | end 147 | end 148 | 149 | it 'must multiply by a scalar' do 150 | (one * 3).must_equal 3 151 | (one * 3.0).must_equal 3.0 152 | end 153 | 154 | it 'must refuse to multiply by a Point' do 155 | -> { one * Point[1, 2] }.must_raise Geometry::OperationNotDefined 156 | end 157 | 158 | it 'must refuse to multiply by a Vector' do 159 | -> { one * Vector[2, 3] }.must_raise Geometry::OperationNotDefined 160 | end 161 | 162 | it 'must divide by a scalar' do 163 | (one / 3).must_equal 1/3 164 | (one / 4.0).must_equal 1/4.0 165 | end 166 | 167 | it 'must raise an exception when divided by 0' do 168 | -> { one / 0 }.must_raise ZeroDivisionError 169 | end 170 | 171 | describe 'division' do 172 | it 'must raise an exception for Points' do 173 | lambda { one / Point[1,2] }.must_raise Geometry::OperationNotDefined 174 | end 175 | 176 | it 'must raise an exception for Vectors' do 177 | lambda { one / Vector[1,2] }.must_raise Geometry::OperationNotDefined 178 | end 179 | end 180 | end 181 | 182 | describe 'coercion' do 183 | it 'must coerce Arrays into Points' do 184 | one.coerce([3,4]).must_equal [Point[3,4], Point[1, 1]] 185 | end 186 | 187 | it 'must coerce Vectors into Vectors' do 188 | one.coerce(Vector[3,4]).must_equal [Vector[3,4], Vector[1, 1]] 189 | end 190 | 191 | it 'must coerce Points into Points' do 192 | one.coerce(Point[5,6]).must_equal [Point[5,6], Point[1, 1]] 193 | end 194 | end 195 | 196 | describe 'comparison' do 197 | subject { Geometry::PointOne.new } 198 | 199 | it 'must be equal to 1 and 1.0' do 200 | one.must_be :eql?, 1 201 | one.must_be :eql?, 1.0 202 | end 203 | 204 | it 'must not be equal to a non-one number' do 205 | 0.wont_equal one 206 | 3.14.wont_equal one 207 | end 208 | 209 | it 'must be equal to an Array of ones' do 210 | one.must_be :==, [1,1] 211 | one.must_be :eql?, [1,1] 212 | one.must_be :===, [1,1] 213 | [1, 1].must_equal one 214 | one.must_equal [1, 1] 215 | end 216 | 217 | it 'must not be equal to a non-one Array' do 218 | one.wont_equal [3, 2, 1] 219 | [3, 2, 1].wont_equal one 220 | end 221 | 222 | it 'must not be equal to a Point at the origin' do 223 | one.wont_be :==, Point[0,0] 224 | one.wont_be :eql?, Point[0,0] 225 | one.wont_be :===, Point[0,0] 226 | Point[0,0].wont_equal one 227 | subject.wont_equal Point[0,0] 228 | end 229 | 230 | it 'must not be equal to a Point not at the origin' do 231 | one.wont_equal Point[3,2] 232 | Point[3,2].wont_equal one 233 | end 234 | 235 | it 'must be equal to a Point of ones' do 236 | one.must_be :==, Point[1,1] 237 | one.must_be :eql?, Point[1,1] 238 | one.must_be :===, Point[1,1] 239 | Point[1,1].must_equal one 240 | one.must_equal Point[1,1] 241 | end 242 | 243 | it 'must be equal to an Vector of ones' do 244 | one.must_be :eql?, Vector[1, 1] 245 | Vector[1, 1].must_equal one 246 | end 247 | 248 | it 'must not be equal to a non-one Vector' do 249 | one.wont_equal Vector[3,2] 250 | Vector[3,2].wont_equal one 251 | end 252 | end 253 | 254 | describe 'when enumerating' do 255 | it 'must have a first method' do 256 | one.first.must_equal 1 257 | one.first(1).must_equal [1] 258 | one.first(5).must_equal [1,1,1,1,1] 259 | end 260 | end 261 | end 262 | -------------------------------------------------------------------------------- /test/geometry/point_zero.rb: -------------------------------------------------------------------------------- 1 | require 'minitest/autorun' 2 | require 'geometry/point_zero' 3 | 4 | describe Geometry::PointZero do 5 | subject { Geometry::PointZero.new } 6 | let(:zero) { Geometry::PointZero.new } 7 | 8 | it 'must pop' do 9 | subject.pop.must_equal Point[0] 10 | subject.pop(2).must_equal Point[0, 0] 11 | end 12 | 13 | it 'must shift' do 14 | subject.shift.must_equal Point[0] 15 | subject.shift(2).must_equal Point[0, 0] 16 | end 17 | 18 | describe 'minmax' do 19 | it 'must have a minimum' do 20 | subject.min.must_equal 0 21 | end 22 | 23 | it 'must minimum with another Point' do 24 | subject.min(Point[-1,7]).must_equal Point[-1,0] 25 | end 26 | 27 | it 'must minimum with an Array' do 28 | subject.min([-1,7]).must_equal Point[-1,0] 29 | end 30 | 31 | it 'must minimum with a multiple arguments' do 32 | subject.min(-1,7).must_equal Point[-1,0] 33 | end 34 | 35 | it 'must have a maximum' do 36 | subject.max.must_equal 0 37 | end 38 | 39 | it 'must maximum with another Point' do 40 | subject.max(Point[7,-1]).must_equal Point[7,0] 41 | end 42 | 43 | it 'must maximum with an Array' do 44 | subject.max([7,-1]).must_equal Point[7,0] 45 | end 46 | 47 | it 'must maximum with multiple arguments' do 48 | subject.max(7,-1).must_equal Point[7,0] 49 | end 50 | 51 | it 'must have a minmax' do 52 | subject.minmax.must_equal [0, 0] 53 | end 54 | 55 | it 'must minmax with another Point' do 56 | subject.minmax(Point[7,-1]).must_equal [Point[0,-1], Point[7,0]] 57 | end 58 | 59 | it 'must minmax with an Array' do 60 | subject.minmax([7,-1]).must_equal [Point[0,-1], Point[7,0]] 61 | end 62 | 63 | it 'must maximum with multiple arguments' do 64 | subject.minmax(7,-1).must_equal [Point[0,-1], Point[7,0]] 65 | end 66 | end 67 | 68 | describe "arithmetic" do 69 | let(:left) { Point[1,2] } 70 | let(:right) { Point[3,4] } 71 | 72 | it 'must pretend to be a Point' do 73 | subject.is_a?(Point).must_equal true 74 | subject.kind_of?(Point).must_equal true 75 | 76 | subject.is_a?(PointZero).must_equal true 77 | subject.kind_of?(PointZero).must_equal true 78 | 79 | subject.instance_of?(Point).must_equal false 80 | subject.instance_of?(PointZero).must_equal true 81 | end 82 | 83 | it "must have +@" do 84 | (+zero).must_be :eql?, 0 85 | (+zero).must_be_instance_of(Geometry::PointZero) 86 | end 87 | 88 | it "must have unary negation" do 89 | (-zero).must_be :eql?, 0 90 | (-zero).must_be_instance_of(Geometry::PointZero) 91 | end 92 | 93 | describe "Accessors" do 94 | it "must return 0 for array access" do 95 | zero[3].must_equal 0 96 | end 97 | 98 | it "must return 0 for named element access" do 99 | zero.x.must_equal 0 100 | zero.y.must_equal 0 101 | zero.z.must_equal 0 102 | end 103 | end 104 | 105 | describe "when adding" do 106 | it "must return a PointIso when adding a number" do 107 | (zero + 3).must_equal 3 108 | (zero + 3).must_be_instance_of Geometry::PointIso 109 | (3 + zero).must_equal 3 110 | end 111 | 112 | it "return a Point when adding two Points" do 113 | (zero + right).must_be_kind_of Point 114 | (left + zero).must_be_kind_of Point 115 | end 116 | 117 | it "must return an Array when adding an array" do 118 | (zero + [5,6]).must_equal [5,6] 119 | # ([5,6] + zero).must_equal [5,6] 120 | end 121 | 122 | it "must return a Point when adding a Size" do 123 | (zero + Size[5,6]).must_be_instance_of(Point) 124 | (zero + Size[5,6]).must_equal Point[5,6] 125 | end 126 | end 127 | 128 | describe "when subtracting" do 129 | it "must return a number" do 130 | (zero - 3).must_equal(-3) 131 | (3 - zero).must_equal 3 132 | end 133 | 134 | it "return a Point when subtracting two Points" do 135 | (zero - right).must_equal Point[-3,-4] 136 | (left - zero).must_equal Point[1,2] 137 | end 138 | 139 | it "must return a Point when subtracting an array" do 140 | (zero - [5,6]).must_equal [-5, -6] 141 | # ([5,6] - zero).must_equal [5,6] 142 | end 143 | 144 | it "must return a Point when subtracting a Size" do 145 | (zero - Size[5,6]).must_be_instance_of(Point) 146 | (zero - Size[5,6]).must_equal Point[-5,-6] 147 | end 148 | end 149 | 150 | describe "multiplication" do 151 | it "must return 0 for scalars" do 152 | (zero * 3).must_equal 0 153 | (zero * 3.0).must_equal 0.0 154 | end 155 | 156 | it "must return 0 for Points" do 157 | (zero * Point[1,2]).must_equal 0 158 | end 159 | 160 | it "must return 0 for Vectors" do 161 | (zero * Vector[2,3]).must_equal 0 162 | end 163 | end 164 | 165 | describe "division" do 166 | it "must return 0 for non-zero scalars" do 167 | (zero / 3).must_equal 0 168 | (zero / 4.0).must_equal 0 169 | end 170 | 171 | it "must raise an exception when divided by 0" do 172 | lambda { zero / 0 }.must_raise ZeroDivisionError 173 | end 174 | 175 | it "must raise an exception for Points" do 176 | lambda { zero / Point[1,2] }.must_raise Geometry::OperationNotDefined 177 | end 178 | 179 | it "must raise an exception for Vectors" do 180 | lambda { zero / Vector[1,2] }.must_raise Geometry::OperationNotDefined 181 | end 182 | 183 | end 184 | 185 | end 186 | 187 | describe "coercion" do 188 | it "must coerce Arrays into Points" do 189 | zero.coerce([3,4]).must_equal [Point[3,4], Point[0,0]] 190 | end 191 | 192 | it "must coerce Vectors into Vectors" do 193 | zero.coerce(Vector[3,4]).must_equal [Vector[3,4], Vector[0,0]] 194 | end 195 | 196 | it "must coerce Points into Points" do 197 | zero.coerce(Point[5,6]).must_equal [Point[5,6], Point[0,0]] 198 | end 199 | end 200 | 201 | describe "comparison" do 202 | subject { Geometry::PointZero.new } 203 | 204 | it "must be equal to 0 and 0.0" do 205 | zero.must_be :eql?, 0 206 | zero.must_be :eql?, 0.0 207 | end 208 | 209 | it "must not be equal to a non-zero number" do 210 | 1.wont_equal zero 211 | 3.14.wont_equal zero 212 | end 213 | 214 | it "must be equal to an Array of zeros" do 215 | zero.must_be :==, [0,0] 216 | zero.must_be :eql?, [0,0] 217 | zero.must_be :===, [0,0] 218 | [0,0].must_equal zero 219 | subject.must_equal [0,0] 220 | end 221 | 222 | it "must not be equal to a non-zero Array" do 223 | zero.wont_equal [3,2] 224 | [3,2].wont_equal zero 225 | end 226 | 227 | it "must be equal to a Point at the origin" do 228 | zero.must_be :==, Point[0,0] 229 | zero.must_be :eql?, Point[0,0] 230 | zero.must_be :===, Point[0,0] 231 | Point[0,0].must_equal zero 232 | subject.must_equal Point[0,0] 233 | end 234 | 235 | it "must not be equal to a Point not at the origin" do 236 | zero.wont_equal Point[3,2] 237 | Point[3,2].wont_equal zero 238 | end 239 | 240 | it "must be equal to an Vector of zeroes" do 241 | zero.must_be :eql?, Vector[0,0] 242 | Vector[0,0].must_equal zero 243 | end 244 | 245 | it "must not be equal to a non-zero Vector" do 246 | zero.wont_equal Vector[3,2] 247 | Vector[3,2].wont_equal zero 248 | end 249 | end 250 | 251 | describe 'when enumerating' do 252 | it 'must have a first method' do 253 | subject.first.must_equal 0 254 | subject.first(1).must_equal [0] 255 | subject.first(5).must_equal [0,0,0,0,0] 256 | end 257 | end 258 | end 259 | -------------------------------------------------------------------------------- /test/geometry/polygon.rb: -------------------------------------------------------------------------------- 1 | require 'minitest/autorun' 2 | require 'geometry/polygon' 3 | 4 | describe Geometry::Polygon do 5 | Polygon = Geometry::Polygon 6 | 7 | let(:cw_unit_square) { Polygon.new [0,0], [0,1], [1,1], [1,0] } 8 | let(:unit_square) { Polygon.new [0,0], [1,0], [1,1], [0,1] } 9 | let(:simple_concave) { Polygon.new [0,0], [4,0], [4,2], [3,2], [3,1], [1,1], [1,2], [0,2] } 10 | subject { unit_square } 11 | 12 | it "must create a Polygon object with no arguments" do 13 | polygon = Geometry::Polygon.new 14 | assert_kind_of(Geometry::Polygon, polygon) 15 | assert_equal(0, polygon.edges.size) 16 | assert_equal(0, polygon.vertices.size) 17 | end 18 | 19 | it "must create a Polygon object from array arguments" do 20 | polygon = Geometry::Polygon.new([0,0], [1,0], [1,1], [0,1]) 21 | assert_kind_of(Geometry::Polygon, polygon) 22 | assert_equal(4, polygon.edges.size) 23 | assert_equal(4, polygon.vertices.size) 24 | end 25 | 26 | describe "when creating a Polygon from an array of Points" do 27 | it "must ignore repeated Points" do 28 | polygon = Geometry::Polygon.new([0,0], [1,0], [1,1], [1,1], [0,1]) 29 | polygon.must_be_kind_of Geometry::Polygon 30 | polygon.edges.size.must_equal 4 31 | polygon.vertices.size.must_equal 4 32 | polygon.must_equal Geometry::Polygon.new([0,0], [1,0], [1,1], [0,1]) 33 | end 34 | 35 | it "must collapse collinear Edges" do 36 | polygon = Geometry::Polygon.new([0,0], [1,0], [1,1], [0.5,1], [0,1]) 37 | polygon.must_equal Geometry::Polygon.new([0,0], [1,0], [1,1], [0,1]) 38 | end 39 | 40 | it "must collapse backtracking Edges" do 41 | polygon = Geometry::Polygon.new [0,0], [2,0], [2,2], [1,2], [1,1], [1,2], [0,2] 42 | polygon.must_equal Geometry::Polygon.new([0,0], [2,0], [2,2], [0,2]) 43 | end 44 | end 45 | 46 | it "must compare identical polygons as equal" do 47 | (unit_square.eql? unit_square).must_equal true 48 | end 49 | 50 | it "must create closed polygons" do 51 | subject.closed?.must_equal true 52 | end 53 | 54 | it "must handle already closed polygons" do 55 | polygon = Geometry::Polygon.new([0,0], [1,0], [1,1], [0,1], [0,0]) 56 | assert_kind_of(Geometry::Polygon, polygon) 57 | assert_equal(4, polygon.edges.size) 58 | assert_equal(4, polygon.vertices.size) 59 | assert_equal(polygon.edges.first.first, polygon.edges.last.last) 60 | end 61 | 62 | it "must return itself on close" do 63 | closed = subject.close 64 | closed.closed?.must_equal true 65 | closed.must_equal subject 66 | closed.must_be_same_as subject 67 | end 68 | 69 | describe "orientation" do 70 | it "must return true for clockwise" do 71 | Polygon.new([0,0], [0,1], [1,1], [1,0]).clockwise?.must_equal true 72 | Polygon.new([1,1], [1,3], [3,3], [3,1]).clockwise?.must_equal true 73 | end 74 | 75 | it "must return false for counterclockwise" do 76 | Polygon.new([0,0], [1,0], [1,1], [0,1]).clockwise?.must_equal false 77 | Polygon.new([1,1], [3,1], [3,3], [1,3]).clockwise?.must_equal false 78 | end 79 | end 80 | 81 | it "must gift wrap a square polygon" do 82 | polygon = Polygon.new [0,0], [1,0], [1,1], [0,1] 83 | convex_hull = polygon.wrap 84 | convex_hull.must_be_kind_of Geometry::Polygon 85 | convex_hull.edges.size.must_equal 4 86 | convex_hull.vertices.must_equal [[0,0], [0,1], [1,1], [1,0]].map {|a| Point[*a]} 87 | end 88 | 89 | it "must gift wrap another square polygon" do 90 | polygon = Polygon.new [0,1], [0,0], [1,0], [1,1] 91 | convex_hull = polygon.wrap 92 | convex_hull.must_be_kind_of Geometry::Polygon 93 | convex_hull.edges.size.must_equal 4 94 | convex_hull.vertices.must_equal [[0,0], [0,1], [1,1], [1,0]].map {|a| Point[*a]} 95 | end 96 | 97 | it "must gift wrap a concave polygon" do 98 | polygon = Polygon.new [0,0], [1,-1], [2,0], [1,1], [2,2], [0,1] 99 | convex_hull = polygon.wrap 100 | convex_hull.must_be_kind_of Geometry::Polygon 101 | convex_hull.edges.size.must_equal 5 102 | convex_hull.vertices.must_equal [Point[0, 0], Point[0, 1], Point[2, 2], Point[2, 0], Point[1, -1]] 103 | end 104 | 105 | it "must gift wrap a polygon" do 106 | polygon = Polygon.new [0,0], [1,-1], [2,0], [2,1], [0,1] 107 | convex_hull = polygon.wrap 108 | convex_hull.must_be_kind_of Geometry::Polygon 109 | convex_hull.edges.size.must_equal 5 110 | convex_hull.vertices.must_equal [[0,0], [0,1], [2,1], [2,0], [1,-1]].map {|a| Point[*a]} 111 | end 112 | 113 | it "must generate spokes" do 114 | unit_square.spokes.must_equal [Vector[-1,-1], Vector[1,-1], Vector[1,1], Vector[-1,1]] 115 | cw_unit_square.spokes.must_equal [Vector[-1,-1], Vector[-1,1], Vector[1,1], Vector[1,-1]] 116 | simple_concave.spokes.must_equal [Vector[-1,-1], Vector[1,-1], Vector[1,1], Vector[-1,1], Vector[-1,1], Vector[1,1], Vector[1,1], Vector[-1,1]] 117 | end 118 | 119 | describe "spaceship" do 120 | it "with a Point" do 121 | (unit_square <=> Point[2,0]).must_equal(-1) 122 | (unit_square <=> Point[1,0]).must_equal 0 123 | (unit_square <=> Point[0.5,0.5]).must_equal 1 124 | end 125 | 126 | it "with a Point that lies on a horizontal edge" do 127 | (unit_square <=> Point[0.5,0]).must_equal 0 128 | end 129 | end 130 | 131 | describe "when outsetting" do 132 | it "must outset a unit square" do 133 | outset_polygon = unit_square.outset(1) 134 | expected_polygon = Polygon.new [-1.0,-1.0], [2.0,-1.0], [2.0,2.0], [-1.0,2.0] 135 | outset_polygon.must_equal expected_polygon 136 | end 137 | 138 | it "must outset a simple concave polygon" do 139 | concave_polygon = Polygon.new [0,0], [4,0], [4,2], [3,2], [3,1], [1,1], [1,2], [0,2] 140 | outset_polygon = concave_polygon.outset(1) 141 | outset_polygon.must_equal Polygon.new [-1,-1], [5,-1], [5,3], [-1,3] 142 | end 143 | 144 | it "must outset a concave polygon" do 145 | concave_polygon = Polygon.new [0,0], [4,0], [4,2], [3,2], [3,1], [1,1], [1,2], [0,2] 146 | outset_polygon = concave_polygon.outset(2) 147 | outset_polygon.must_equal Polygon.new [-2,-2], [6,-2], [6,4], [-2,4] 148 | end 149 | 150 | it "must outset an asymetric concave polygon" do 151 | concave_polygon = Polygon.new [0,0], [4,0], [4,3], [3,3], [3,1], [1,1], [1,2], [0,2] 152 | outset_polygon = concave_polygon.outset(2) 153 | outset_polygon.must_equal Polygon.new [-2,-2], [6,-2], [6,5], [1,5], [1,4], [-2,4] 154 | end 155 | 156 | it "must outset a concave polygon with multiply-intersecting edges" do 157 | concave_polygon = Polygon.new [0,0], [5,0], [5,2], [4,2], [4,1], [3,1], [3,2], [2,2], [2,1], [1,1], [1,2], [0,2] 158 | outset_polygon = concave_polygon.outset(1) 159 | outset_polygon.must_equal Polygon.new [-1,-1], [6,-1], [6,3], [-1,3] 160 | end 161 | 162 | it "must outset a concave polygon where the first outset edge intersects with the last outset edge" do 163 | polygon = Polygon.new [0,0], [0,1], [2,1], [2,2], [-1,2], [-1,-1], [2,-1], [2,0] 164 | polygon.edges.count.must_equal 8 165 | polygon.outset(1).must_equal Polygon.new [3, 0], [3, 3], [-2, 3], [-2, -2], [3, -2] 166 | end 167 | 168 | # Naturally, this test is very sensitive to the input coordinate values. This is a painfully contrived example that 169 | # checks for sensitivity to edges that are very close to horizontal, but not quite. 170 | # When the test fails, the first point of the offset polygon is at [0,-1] 171 | it "must not be sensitive to floating point rounding errors" do 172 | polygon = Polygon.new [0, 0], [0, -2], [10, -2], [10, 10], [-100, 10], [-100, -22], [-69, -22], [-69, 3.552713678800501e-15], [0,0] 173 | outset = polygon.outset(1) 174 | outset.edges.first.first.must_equal Geometry::Point[-1,-1] 175 | end 176 | end 177 | 178 | describe "set operations" do 179 | describe "union" do 180 | it "must union two adjacent squares" do 181 | polygonA = Polygon.new [0,0], [1,0], [1,1], [0,1] 182 | polygonB = Polygon.new [1,0], [2,0], [2,1], [1,1] 183 | (polygonA.union polygonB).must_equal Polygon.new [0,0], [2,0], [2,1], [0,1] 184 | (polygonA + polygonB).must_equal Polygon.new [0,0], [2,0], [2,1], [0,1] 185 | end 186 | 187 | it "must union two overlapping squares" do 188 | polygonA = Polygon.new [0,0], [2,0], [2,2], [0,2] 189 | polygonB = Polygon.new [1,1], [3,1], [3,3], [1,3] 190 | expected_polygon = Polygon.new [0,0], [2,0], [2,1], [3,1], [3,3], [1,3], [1,2], [0,2] 191 | union = polygonA.union polygonB 192 | union.must_be_kind_of Polygon 193 | union.must_equal expected_polygon 194 | end 195 | 196 | it "must union two overlapping clockwise squares" do 197 | polygonA = Polygon.new [0,0], [0,2], [2,2], [2,0] 198 | polygonB = Polygon.new [1,1], [1,3], [3,3], [3,1] 199 | expected_polygon = Polygon.new [0, 0], [0, 2], [1, 2], [1, 3], [3, 3], [3, 1], [2, 1], [2, 0] 200 | union = polygonA.union polygonB 201 | union.must_be_kind_of Polygon 202 | union.must_equal expected_polygon 203 | end 204 | 205 | it "must union two overlapping squares with collinear edges" do 206 | polygonA = Polygon.new [0,0], [2,0], [2,2], [0,2] 207 | polygonB = Polygon.new [1,0], [3,0], [3,2], [1,2] 208 | union = polygonA + polygonB 209 | union.must_be_kind_of Polygon 210 | union.must_equal Polygon.new [0,0], [3,0], [3,2], [0,2] 211 | end 212 | end 213 | end 214 | end 215 | -------------------------------------------------------------------------------- /test/geometry/rectangle.rb: -------------------------------------------------------------------------------- 1 | require 'minitest/autorun' 2 | require 'geometry/rectangle' 3 | 4 | def Rectangle(*args) 5 | Geometry::Rectangle.new(*args) 6 | end 7 | 8 | describe Geometry::Rectangle do 9 | Rectangle = Geometry::Rectangle 10 | 11 | describe "when initializing" do 12 | it "must accept two corners as Arrays" do 13 | rectangle = Rectangle.new [1,2], [2,3] 14 | rectangle.must_be_kind_of Geometry::Rectangle 15 | rectangle.height.must_equal 1 16 | rectangle.width.must_equal 1 17 | rectangle.origin.must_equal Point[1,2] 18 | end 19 | 20 | it "must accept two named corners as Arrays" do 21 | rectangle = Rectangle.new from:[1,2], to:[2,3] 22 | rectangle.must_be_kind_of Geometry::Rectangle 23 | rectangle.height.must_equal 1 24 | rectangle.width.must_equal 1 25 | rectangle.origin.must_equal Point[1,2] 26 | end 27 | 28 | it "must accept named center point and size arguments" do 29 | rectangle = Rectangle.new center:[1,2], size:[3,4] 30 | rectangle.must_be_kind_of Geometry::Rectangle 31 | rectangle.height.must_equal 4 32 | rectangle.width.must_equal 3 33 | rectangle.center.must_equal Point[1,2] 34 | end 35 | 36 | it "must reject a named center argument with no size argument" do 37 | -> { Rectangle.new center:[1,2] }.must_raise ArgumentError 38 | end 39 | 40 | it "must accept named origin point and size arguments" do 41 | rectangle = Rectangle.new origin:[1,2], size:[3,4] 42 | rectangle.must_be_kind_of Geometry::Rectangle 43 | rectangle.height.must_equal 4 44 | rectangle.width.must_equal 3 45 | rectangle.origin.must_equal Point[1,2] 46 | end 47 | 48 | it "must reject a named origin argument with no size argument" do 49 | -> { Rectangle.new origin:[1,2] }.must_raise ArgumentError 50 | end 51 | 52 | it "must accept a sole named size argument that is an Array" do 53 | rectangle = Rectangle.new size:[1,2] 54 | rectangle.must_be_kind_of Geometry::Rectangle 55 | rectangle.origin.must_equal Point[0,0] 56 | rectangle.height.must_equal 2 57 | rectangle.width.must_equal 1 58 | end 59 | 60 | it "must accept a sole named size argument that is a Size" do 61 | rectangle = Rectangle.new size:Size[1,2] 62 | rectangle.must_be_kind_of Geometry::Rectangle 63 | rectangle.origin.must_equal Point[0,0] 64 | rectangle.height.must_equal 2 65 | rectangle.width.must_equal 1 66 | end 67 | 68 | it "must accept named width and height arguments" do 69 | rectangle = Rectangle.new width:1, height:3 70 | rectangle.must_be_kind_of Geometry::Rectangle 71 | rectangle.height.must_equal 3 72 | rectangle.width.must_equal 1 73 | end 74 | 75 | it "must reject width or height by themselves" do 76 | -> { Rectangle.new height:1 }.must_raise ArgumentError 77 | -> { Rectangle.new width:1 }.must_raise ArgumentError 78 | end 79 | end 80 | 81 | describe "comparison" do 82 | it "must compare equal" do 83 | rectangle = Rectangle [1,2], [3,4] 84 | rectangle.must_equal Rectangle([1,2], [3, 4]) 85 | end 86 | end 87 | 88 | describe "inset" do 89 | subject { Rectangle.new [0,0], [10,10] } 90 | 91 | it "must inset equally" do 92 | subject.inset(1).must_equal Rectangle.new [1,1], [9,9] 93 | end 94 | 95 | it "must inset vertically and horizontally" do 96 | subject.inset(1,2).must_equal Rectangle.new [1,2], [9,8] 97 | subject.inset(x:1, y:2).must_equal Rectangle.new [1,2], [9,8] 98 | end 99 | 100 | it "must inset from individual sides" do 101 | subject.inset(1,2,3,4).must_equal Rectangle.new [2,3], [6,9] 102 | subject.inset(top:1, left:2, bottom:3, right:4).must_equal Rectangle.new [2,3], [6,9] 103 | end 104 | end 105 | 106 | describe "properties" do 107 | subject { Rectangle.new [1,2], [3,4] } 108 | let(:rectangle) { Rectangle [1,2], [3,4] } 109 | 110 | it "have a center point property" do 111 | rectangle.center.must_equal Point[2,3] 112 | end 113 | 114 | it 'must always be closed' do 115 | subject.closed?.must_equal true 116 | end 117 | 118 | it "have a width property" do 119 | assert_equal(2, rectangle.width) 120 | end 121 | 122 | it "have a height property" do 123 | assert_equal(2, rectangle.height) 124 | end 125 | 126 | it "have an origin property" do 127 | assert_equal(Point[1,2], rectangle.origin) 128 | end 129 | 130 | it "have an edges property that returns 4 edges" do 131 | edges = rectangle.edges 132 | assert_equal(4, edges.size) 133 | edges.each {|edge| assert_kind_of(Geometry::Edge, edge)} 134 | end 135 | 136 | it "have a points property that returns 4 points in clockwise order starting from the lower-left" do 137 | points = rectangle.points 138 | assert_equal(4, points.size) 139 | points.each {|point| assert_kind_of(Geometry::Point, point)} 140 | points.must_equal [Point[1,2], Point[1,4], Point[3,4], Point[3,2]] 141 | end 142 | 143 | it "must have a bounds property that returns a Rectangle" do 144 | subject.bounds.must_equal Rectangle.new([1,2], [3,4]) 145 | end 146 | 147 | it "must have a minmax property that returns the corners of the bounding rectangle" do 148 | subject.minmax.must_equal [Point[1,2], Point[3,4]] 149 | end 150 | 151 | it "must have a max property that returns the upper right corner of the bounding rectangle" do 152 | subject.max.must_equal Point[3,4] 153 | end 154 | 155 | it "must have a min property that returns the lower left corner of the bounding rectangle" do 156 | subject.min.must_equal Point[1,2] 157 | end 158 | 159 | it 'must have a path property that returns a closed Path' do 160 | rectangle.path.must_equal Geometry::Path.new([1,2], [1,4], [3,4], [3,2], [1,2]) 161 | end 162 | end 163 | end 164 | -------------------------------------------------------------------------------- /test/geometry/regular_polygon.rb: -------------------------------------------------------------------------------- 1 | require 'minitest/autorun' 2 | require 'geometry/regular_polygon' 3 | 4 | describe Geometry::RegularPolygon do 5 | RegularPolygon = Geometry::RegularPolygon 6 | 7 | subject { RegularPolygon.new sides:4, center:[1,2], radius:3 } 8 | 9 | it 'must always be closed' do 10 | subject.closed?.must_equal true 11 | end 12 | 13 | describe 'when constructed with a center and circumradius' do 14 | let(:polygon) { RegularPolygon.new sides:4, center:[1,2], radius:3 } 15 | subject { RegularPolygon.new sides:4, center:[1,2], radius:3 } 16 | 17 | it "must create a RegularPolygon" do 18 | polygon.must_be_instance_of(RegularPolygon) 19 | end 20 | 21 | it "must have the correct number of sides" do 22 | polygon.edge_count.must_equal 4 23 | end 24 | 25 | it "must have a center point accessor" do 26 | polygon.center.must_equal Point[1,2] 27 | end 28 | 29 | it "must have a radius accessor" do 30 | polygon.radius.must_equal 3 31 | end 32 | 33 | it "must compare equal" do 34 | polygon.must_equal RegularPolygon.new(sides:4, center:[1,2], radius:3) 35 | end 36 | 37 | describe "properties" do 38 | it "must have vertices" do 39 | subject.vertices.must_equal [Point[4.0, 2.0], Point[1.0000000000000002, 5.0], Point[-2.0, 2.0000000000000004], Point[0.9999999999999994, -1.0]] 40 | end 41 | end 42 | 43 | it 'must have an indiameter' do 44 | subject.indiameter.must_be_close_to 4.242 45 | end 46 | 47 | it 'must have an inradius' do 48 | subject.inradius.must_be_close_to 2.121 49 | end 50 | end 51 | 52 | describe "when constructed with a center and diameter" do 53 | let(:polygon) { RegularPolygon.new sides:4, center:[1,2], diameter:4 } 54 | 55 | it "must have the correct number of sides" do 56 | polygon.edge_count.must_equal 4 57 | end 58 | 59 | it "must have a center" do 60 | polygon.center.must_equal Point[1,2] 61 | end 62 | 63 | it "must have a diameter" do 64 | polygon.diameter.must_equal 4 65 | end 66 | 67 | it "must calculate the correct radius" do 68 | polygon.radius.must_equal 2 69 | end 70 | 71 | it "must compare equal" do 72 | polygon.must_equal RegularPolygon.new(sides:4, center:[1,2], diameter:4) 73 | end 74 | end 75 | 76 | describe "when constructed with a diameter and no center" do 77 | let(:polygon) { RegularPolygon.new sides:4, diameter:4 } 78 | 79 | it "must have the correct number of sides" do 80 | polygon.edge_count.must_equal 4 81 | end 82 | 83 | it "must be at the origin" do 84 | polygon.center.must_equal Point.zero 85 | end 86 | 87 | it "must have a diameter" do 88 | polygon.diameter.must_equal 4 89 | end 90 | 91 | it "must calculate the correct radius" do 92 | polygon.radius.must_equal 2 93 | end 94 | end 95 | 96 | describe 'when constructed with an indiameter and center' do 97 | subject { RegularPolygon.new sides:6, indiameter:4 } 98 | 99 | it 'must have a circumdiameter' do 100 | subject.diameter.must_be_close_to 4.618 101 | end 102 | 103 | it 'must have a circumradius' do 104 | subject.circumradius.must_be_close_to 2.309 105 | end 106 | 107 | it 'must have an indiameter' do 108 | subject.indiameter.must_be_close_to 4 109 | end 110 | 111 | it 'must have an inradius' do 112 | subject.inradius.must_be_close_to 2 113 | end 114 | end 115 | 116 | describe 'when constructed with an inradius and center' do 117 | subject { RegularPolygon.new sides:6, inradius:4 } 118 | 119 | it 'must have a circumradius' do 120 | subject.circumradius.must_be_close_to 4.618 121 | end 122 | 123 | it 'must have points' do 124 | expected_points = [Point[4.618, 0], Point[2.309, 4], Point[-2.309, 4], Point[-4.618, 0], Point[-2.309, -4], Point[2.309, -4]] 125 | subject.points.zip(expected_points) do |point0, point1| 126 | point0.to_a.zip(point1.to_a) {|a, b| a.must_be_close_to b } 127 | end 128 | end 129 | end 130 | 131 | describe "properties" do 132 | subject { RegularPolygon.new sides:6, diameter:4 } 133 | 134 | it "must have edges" do 135 | expected_edges = [Edge(Point[2, 0], Point[1, 1.732]), Edge(Point[1, 1.732], Point[-1, 1.732]), Edge(Point[-1, 1.732], Point[-2, 0]), Edge(Point[-2, 0], Point[-1, -1.732]), Edge(Point[-1, -1.732], Point[1, -1.732]), Edge(Point[1, -1.732], Point[2, 0])] 136 | subject.edges.zip(expected_edges) do |edge1, edge2| 137 | edge1.to_a.zip(edge2.to_a) do |point1, point2| 138 | point1.to_a.zip(point2.to_a) {|a, b| a.must_be_close_to b } 139 | end 140 | end 141 | end 142 | 143 | it 'must have points' do 144 | expected_points = [Point[2, 0], Point[1, 1.732], Point[-1, 1.732], Point[-2, 0], Point[-1, -1.732], Point[1, -1.732]] 145 | subject.points.zip(expected_points) do |point0, point1| 146 | point0.to_a.zip(point1.to_a) {|a, b| a.must_be_close_to b } 147 | end 148 | end 149 | 150 | it "must have a bounds property that returns a Rectangle" do 151 | subject.bounds.must_equal Rectangle.new([-2,-2], [2,2]) 152 | end 153 | 154 | it "must have a minmax property that returns the corners of the bounding rectangle" do 155 | subject.minmax.must_equal [Point[-2,-2], Point[2,2]] 156 | end 157 | 158 | it "must have a max property that returns the upper right corner of the bounding rectangle" do 159 | subject.max.must_equal Point[2,2] 160 | end 161 | 162 | it "must have a min property that returns the lower left corner of the bounding rectangle" do 163 | subject.min.must_equal Point[-2,-2] 164 | end 165 | end 166 | end 167 | -------------------------------------------------------------------------------- /test/geometry/rotation.rb: -------------------------------------------------------------------------------- 1 | require 'minitest/autorun' 2 | require 'geometry/rotation' 3 | 4 | describe Geometry::Rotation do 5 | Point = Geometry::Point 6 | Rotation = Geometry::Rotation 7 | RotationAngle = Geometry::RotationAngle 8 | 9 | describe "when constructed" do 10 | it "must accept a rotation angle" do 11 | rotation = Rotation.new angle:Math::PI/2 12 | rotation.must_be_instance_of(RotationAngle) 13 | rotation.angle.must_equal Math::PI/2 14 | rotation.x.x.must_be_close_to 0 15 | rotation.x.y.must_be_close_to 1 16 | rotation.y.x.must_be_close_to(-1) 17 | rotation.y.y.must_be_close_to 0 18 | end 19 | 20 | it "must accept an X axis" do 21 | rotation = Rotation.new x:[1,0] 22 | rotation.must_be_instance_of(RotationAngle) 23 | rotation.angle.must_equal 0 24 | rotation.x.must_equal Point[1,0] 25 | rotation.y.must_equal Point[0,1] 26 | end 27 | end 28 | 29 | it "must accept x and y axes" do 30 | rotation = Geometry::Rotation.new :x => [1,2,3], :y => [4,5,6] 31 | rotation.x.must_equal [1,2,3] 32 | rotation.y.must_equal [4,5,6] 33 | end 34 | 35 | it "must accept 3-element x and y axes and a dimensionality of 3" do 36 | rotation = Geometry::Rotation.new(:dimensions => 3, :x => [1,2,3], :y => [4,5,6]) 37 | rotation.dimensions.must_equal 3 38 | end 39 | 40 | it "must reject 3-element x and y axes and a dimensionality of 2" do 41 | lambda { Geometry::Rotation.new(:dimensions => 2, :x => [1,2,3], :y => [4,5,6]) }.must_raise ArgumentError 42 | end 43 | 44 | it "must promote 2-element Vectors to dimensionality of 3" do 45 | rotation = Geometry::Rotation.new(:dimensions => 3, :x => [1,2], :y => [4,5]) 46 | rotation.dimensions.must_equal 3 47 | rotation.x.must_equal [1,2,0] 48 | rotation.y.must_equal [4,5,0] 49 | end 50 | 51 | it "must be the identity rotation if no axes are given" do 52 | Geometry::Rotation.new.identity?.must_equal true 53 | Geometry::Rotation.new(:dimensions => 3).identity?.must_equal true 54 | end 55 | 56 | it "must be the identity rotation when identity axes are given" do 57 | Geometry::Rotation.new(:x => [1,0,0], :y => [0,1,0]) 58 | end 59 | 60 | it "must have a matrix accessor" do 61 | r = Geometry::Rotation.new(:x => [1,0,0], :y => [0,1,0]) 62 | r.matrix.must_equal Matrix[[1,0,0],[0,1,0],[0,0,1]] 63 | end 64 | 65 | describe "when comparing" do 66 | it "must equate equal objects" do 67 | Rotation.new(x:[1,2,3], y:[4,5,6]).must_equal Rotation.new(x:[1,2,3], y:[4,5,6]) 68 | end 69 | end 70 | 71 | describe "comparison" do 72 | it "must equate equal angles" do 73 | Rotation.new(angle:45).must_equal Rotation.new(angle:45) 74 | end 75 | 76 | it "must not equate unequal angles" do 77 | Rotation.new(angle:10).wont_equal Rotation.new(angle:45) 78 | end 79 | end 80 | 81 | describe "composition" do 82 | it "must add angles" do 83 | (Rotation.new(angle:45) + Rotation.new(angle:45)).must_equal Rotation.new(angle:90) 84 | end 85 | 86 | it "must subtract angles" do 87 | (Rotation.new(angle:45) - Rotation.new(angle:45)).must_equal Rotation.new(angle:0) 88 | end 89 | 90 | it "must negate angles" do 91 | (-Rotation.new(angle:45)).must_equal Rotation.new(angle:-45) 92 | end 93 | end 94 | 95 | describe "when transforming a Point" do 96 | subject { Rotation.new(angle:Math::PI/2) } 97 | 98 | describe "when no rotation is set" do 99 | it "must return the Point" do 100 | Rotation.new.transform(Point[1,0]).must_equal Point[1,0] 101 | end 102 | end 103 | 104 | it "must rotate" do 105 | rotated_point = Rotation.new(angle:Math::PI/2).transform(Point[1,0]) 106 | rotated_point.x.must_be_close_to 0 107 | rotated_point.y.must_be_close_to 1 108 | end 109 | 110 | it 'must transform a PointZero' do 111 | subject.transform(Point.zero).must_equal Point.zero 112 | end 113 | end 114 | end 115 | -------------------------------------------------------------------------------- /test/geometry/size.rb: -------------------------------------------------------------------------------- 1 | require 'minitest/autorun' 2 | require 'geometry/size' 3 | 4 | describe Geometry::Size do 5 | subject { Geometry::Size[10,10] } 6 | 7 | describe "when constructed" do 8 | it "create a Size object using list syntax" do 9 | size = Geometry::Size[2,1] 10 | assert_equal(2, size.size) 11 | assert_equal(2, size.x) 12 | assert_equal(1, size.y) 13 | end 14 | 15 | it "create a Size object from an array" do 16 | size = Geometry::Size[[3,4]] 17 | assert_equal(2, size.size) 18 | assert_equal(3, size.x) 19 | assert_equal(4, size.y) 20 | end 21 | 22 | it "create a Size object from individual parameters" do 23 | size = Geometry::Size[3,4] 24 | assert_equal(2, size.size) 25 | assert_equal(3, size.x) 26 | assert_equal(4, size.y) 27 | end 28 | 29 | it "create a Size object from a Size" do 30 | size = Geometry::Size[Geometry::Size[3,4]] 31 | assert_equal(2, size.size) 32 | assert_equal(3, size.x) 33 | assert_equal(4, size.y) 34 | end 35 | 36 | it "create a Size object from a Vector" do 37 | size = Geometry::Size[Vector[3,4]] 38 | assert_equal(2, size.size) 39 | assert_equal(3, size.x) 40 | assert_equal(4, size.y) 41 | end 42 | end 43 | 44 | describe 'when array access' do 45 | it 'must allow indexed access' do 46 | size = Geometry::Size[5,6] 47 | size.size.must_equal 2 48 | size[0].must_equal 5 49 | size[1].must_equal 6 50 | end 51 | 52 | it 'must slize with a start index and a length' do 53 | size = Geometry::Size[5, 6, 7] 54 | slice = size[1,2] 55 | slice.length.must_equal 2 56 | end 57 | end 58 | 59 | it "allow named element access" do 60 | size = Geometry::Size[5,6,7] 61 | assert_equal(3, size.size) 62 | assert_equal(5, size.x) 63 | assert_equal(6, size.y) 64 | assert_equal(7, size.z) 65 | end 66 | 67 | it "have a width accessor" do 68 | size = Geometry::Size[5,6,7] 69 | assert_equal(5, size.width) 70 | end 71 | 72 | it "have a height accessor" do 73 | size = Geometry::Size[5,6,7] 74 | assert_equal(6, size.height) 75 | end 76 | 77 | it "have a depth accessor" do 78 | size = Geometry::Size[5,6,7] 79 | assert_equal(7, size.depth) 80 | end 81 | 82 | it "compare equal" do 83 | size1 = Geometry::Size[1,2] 84 | size2 = Geometry::Size[1,2] 85 | size3 = Geometry::Size[3,4] 86 | assert_equal(size1, size2) 87 | size2.wont_equal size3 88 | end 89 | 90 | it "compare equal to an array with equal elements" do 91 | size1 = Size[1,2] 92 | assert_equal(size1, [1,2]) 93 | end 94 | 95 | it "not compare equal to an array with unequal elements" do 96 | size1 = Size[1,2] 97 | size1.wont_equal [3,2] 98 | end 99 | 100 | it "implement inspect" do 101 | size = Geometry::Size[8,9] 102 | assert_equal('Size[8, 9]', size.inspect) 103 | end 104 | it "implement to_s" do 105 | size = Geometry::Size[10,11] 106 | assert_equal('Size[10, 11]', size.to_s) 107 | end 108 | 109 | it 'must inset with horizontal and vertical insets' do 110 | subject.inset(4).must_equal Geometry::Size[6, 6] 111 | subject.inset(2,3).must_equal Geometry::Size[6, 4] 112 | subject.inset(x:2, y:3).must_equal Geometry::Size[6, 4] 113 | end 114 | 115 | it 'must inset with left and top' do 116 | subject.inset(left:2, top:3).must_equal Geometry::Size[8, 7] 117 | end 118 | 119 | it 'must inset with right and bottom' do 120 | subject.inset(right:2, bottom:3).must_equal Geometry::Size[8, 7] 121 | end 122 | 123 | it 'must inset with insets for top, left, bottom, right' do 124 | subject.inset(top:1, left:2, bottom:3, right:4).must_equal Geometry::Size[4, 6] 125 | end 126 | 127 | it 'must outset' do 128 | subject.outset(4).must_equal Geometry::Size[14, 14] 129 | subject.outset(2,3).must_equal Geometry::Size[14, 16] 130 | subject.outset(x:2, y:3).must_equal Geometry::Size[14, 16] 131 | end 132 | 133 | it 'must outset with left and top' do 134 | subject.outset(left:2, top:3).must_equal Geometry::Size[12, 13] 135 | end 136 | 137 | it 'must outset with right and bottom' do 138 | subject.outset(right:2, bottom:3).must_equal Geometry::Size[12, 13] 139 | end 140 | 141 | it 'must outset with insets for top, left, bottom, right' do 142 | subject.outset(top:1, left:2, bottom:3, right:4).must_equal Geometry::Size[16, 14] 143 | end 144 | end 145 | -------------------------------------------------------------------------------- /test/geometry/size_one.rb: -------------------------------------------------------------------------------- 1 | require 'minitest/autorun' 2 | require 'geometry/size_one' 3 | 4 | describe Geometry::SizeOne do 5 | Size = Geometry::Size 6 | 7 | let(:one) { Geometry::SizeOne.new } 8 | 9 | describe 'arithmetic' do 10 | let(:left) { Size[1,2] } 11 | let(:right) { Size[3,4] } 12 | 13 | it 'must have +@' do 14 | (+one).must_be :eql?, 1 15 | (+one).must_be_instance_of(Geometry::SizeOne) 16 | end 17 | 18 | it 'must have unary negation' do 19 | (-one).must_be :eql?, -1 20 | # (-one).must_be_instance_of(Geometry::SizeOne) 21 | end 22 | 23 | it 'must add a number' do 24 | (one + 3).must_equal 4 25 | (3 + one).must_equal 4 26 | end 27 | 28 | it 'return a Size when adding two Sizes' do 29 | (one + right).must_be_kind_of Size 30 | (left + one).must_be_kind_of Size 31 | end 32 | 33 | it 'must return a Size when adding an array' do 34 | (one + [5,6]).must_equal [6,7] 35 | # ([5,6] + one).must_equal [6,7] 36 | end 37 | 38 | describe 'when subtracting' do 39 | it 'must subtract a number' do 40 | (one - 3).must_equal(-2) 41 | (3 - one).must_equal 2 42 | end 43 | 44 | it 'return a Size when subtracting two Size' do 45 | (one - right).must_equal Size[-2,-3] 46 | (left - one).must_equal Size[0,1] 47 | end 48 | 49 | it 'must return a Size when subtracting an array' do 50 | (one - [5,6]).must_equal [-4, -5] 51 | # ([5,6] - one).must_equal [6,7] 52 | end 53 | end 54 | 55 | it 'must multiply by a scalar' do 56 | (one * 3).must_equal 3 57 | (one * 3.0).must_equal 3.0 58 | end 59 | 60 | it 'must refuse to multiply by a Size' do 61 | -> { one * Size[1,2] }.must_raise Geometry::OperationNotDefined 62 | end 63 | 64 | it 'must refuse to multiply by a Vector' do 65 | -> { one * Vector[1, 2] }.must_raise Geometry::OperationNotDefined 66 | end 67 | 68 | describe 'division' do 69 | it 'must divide by a scalar' do 70 | (one / 3).must_equal 1/3 71 | (one / 4.0).must_equal 1/4.0 72 | end 73 | 74 | it 'must raise an exception when divided by 0' do 75 | -> { one / 0 }.must_raise ZeroDivisionError 76 | end 77 | 78 | it 'must raise an exception for Sizes' do 79 | -> { one / Size[1,2] }.must_raise Geometry::OperationNotDefined 80 | end 81 | 82 | it 'must raise an exception for Vectors' do 83 | -> { one / Vector[1,2] }.must_raise Geometry::OperationNotDefined 84 | end 85 | end 86 | end 87 | 88 | describe 'coercion' do 89 | it 'must coerce Arrays into Sizes' do 90 | one.coerce([3,4]).must_equal [Size[3,4], Size[1,1]] 91 | end 92 | 93 | it 'must coerce Vectors into Vectors' do 94 | one.coerce(Vector[3,4]).must_equal [Vector[3,4], Vector[1,1]] 95 | end 96 | 97 | it 'must coerce Size into Size' do 98 | one.coerce(Size[5,6]).must_equal [Size[5,6], Size[1,1]] 99 | end 100 | end 101 | 102 | describe 'comparison' do 103 | let(:one) { Geometry::SizeOne.new } 104 | 105 | it 'must be equal to 1 and 1.0' do 106 | one.must_be :eql?, 1 107 | one.must_be :eql?, 1.0 108 | end 109 | 110 | it 'must not be equal to a non-one number' do 111 | 0.wont_equal one 112 | 3.14.wont_equal one 113 | end 114 | 115 | it 'must be equal to an Array of ones' do 116 | one.must_be :==, [1,1] 117 | one.must_be :eql?, [1,1] 118 | [1,1].must_equal one 119 | end 120 | 121 | it 'must not be equal to a non-one Array' do 122 | one.wont_equal [3,2] 123 | [3,2].wont_equal one 124 | end 125 | 126 | it 'must be equal to a Size of ones' do 127 | one.must_be :==, Size[1,1] 128 | one.must_be :eql?, Size[1,1] 129 | Size[1,1].must_equal one 130 | end 131 | 132 | it 'must not be equal to a non-one Size' do 133 | one.wont_equal Size[3,2] 134 | Size[3,2].wont_equal one 135 | end 136 | 137 | it 'must be equal to an Vector of onees' do 138 | one.must_be :eql?, Vector[1,1] 139 | Vector[1,1].must_equal one 140 | end 141 | 142 | it 'must not be equal to a non-one Vector' do 143 | one.wont_equal Vector[3,2] 144 | Vector[3,2].wont_equal one 145 | end 146 | end 147 | 148 | describe 'when enumerating' do 149 | it 'must have a first method' do 150 | one.first.must_equal 1 151 | one.first(1).must_equal [1] 152 | one.first(5).must_equal [1,1,1,1,1] 153 | end 154 | end 155 | end 156 | -------------------------------------------------------------------------------- /test/geometry/size_zero.rb: -------------------------------------------------------------------------------- 1 | require 'minitest/autorun' 2 | require 'geometry/size_zero' 3 | 4 | describe Geometry::SizeZero do 5 | Size = Geometry::Size 6 | 7 | let(:zero) { Geometry::SizeZero.new } 8 | 9 | describe "arithmetic" do 10 | let(:left) { Size[1,2] } 11 | let(:right) { Size[3,4] } 12 | 13 | it "must have +@" do 14 | (+zero).must_be :eql?, 0 15 | (+zero).must_be_instance_of(Geometry::SizeZero) 16 | end 17 | 18 | it "must have unary negation" do 19 | (-zero).must_be :eql?, 0 20 | (-zero).must_be_instance_of(Geometry::SizeZero) 21 | end 22 | 23 | describe "when adding" do 24 | it "must return a number" do 25 | (zero + 3).must_equal 3 26 | (3 + zero).must_equal 3 27 | end 28 | 29 | it "return a Size when adding two Sizes" do 30 | (zero + right).must_be_kind_of Size 31 | # (left + zero).must_be_kind_of Size 32 | end 33 | 34 | it "must return a Size when adding an array" do 35 | (zero + [5,6]).must_equal [5,6] 36 | # ([5,6] + zero).must_equal [5,6] 37 | end 38 | end 39 | 40 | describe "when subtracting" do 41 | it "must return a number" do 42 | (zero - 3).must_equal(-3) 43 | (3 - zero).must_equal 3 44 | end 45 | 46 | it "return a Size when subtracting two Size" do 47 | (zero - right).must_equal Size[-3,-4] 48 | (left - zero).must_equal Size[1,2] 49 | end 50 | 51 | it "must return a Size when subtracting an array" do 52 | (zero - [5,6]).must_equal [-5, -6] 53 | # ([5,6] - zero).must_equal [5,6] 54 | end 55 | end 56 | 57 | describe "multiplication" do 58 | it "must return 0 for scalars" do 59 | (zero * 3).must_equal 0 60 | (zero * 3.0).must_equal 0.0 61 | end 62 | 63 | it "must return 0 for Sizes" do 64 | (zero * Size[1,2]).must_equal 0 65 | end 66 | 67 | it "must return 0 for Vectors" do 68 | (zero * Vector[2,3]).must_equal 0 69 | end 70 | end 71 | 72 | describe "division" do 73 | it "must return 0 for non-zero scalars" do 74 | (zero / 3).must_equal 0 75 | (zero / 4.0).must_equal 0 76 | end 77 | 78 | it "must raise an exception when divided by 0" do 79 | lambda { zero / 0 }.must_raise ZeroDivisionError 80 | end 81 | 82 | it "must raise an exception for Sizes" do 83 | lambda { zero / Size[1,2] }.must_raise Geometry::OperationNotDefined 84 | end 85 | 86 | it "must raise an exception for Vectors" do 87 | lambda { zero / Vector[1,2] }.must_raise Geometry::OperationNotDefined 88 | end 89 | 90 | end 91 | 92 | end 93 | 94 | describe "coercion" do 95 | it "must coerce Arrays into Sizes" do 96 | zero.coerce([3,4]).must_equal [Size[3,4], Size[0,0]] 97 | end 98 | 99 | it "must coerce Vectors into Vectors" do 100 | zero.coerce(Vector[3,4]).must_equal [Vector[3,4], Vector[0,0]] 101 | end 102 | 103 | it "must coerce Size into Size" do 104 | zero.coerce(Size[5,6]).must_equal [Size[5,6], Size[0,0]] 105 | end 106 | end 107 | 108 | describe "comparison" do 109 | let(:zero) { Geometry::PointZero.new } 110 | 111 | it "must be equal to 0 and 0.0" do 112 | zero.must_be :eql?, 0 113 | zero.must_be :eql?, 0.0 114 | end 115 | 116 | it "must not be equal to a non-zero number" do 117 | 1.wont_equal zero 118 | 3.14.wont_equal zero 119 | end 120 | 121 | it "must be equal to an Array of zeros" do 122 | zero.must_be :==, [0,0] 123 | zero.must_be :eql?, [0,0] 124 | [0,0].must_equal zero 125 | end 126 | 127 | it "must not be equal to a non-zero Array" do 128 | zero.wont_equal [3,2] 129 | [3,2].wont_equal zero 130 | end 131 | 132 | it "must be equal to a zero Size" do 133 | zero.must_be :==, Size[0,0] 134 | zero.must_be :eql?, Size[0,0] 135 | Size[0,0].must_equal zero 136 | end 137 | 138 | it "must not be equal to a non-zero Size" do 139 | zero.wont_equal Size[3,2] 140 | Size[3,2].wont_equal zero 141 | end 142 | 143 | it "must be equal to an Vector of zeroes" do 144 | zero.must_be :eql?, Vector[0,0] 145 | Vector[0,0].must_equal zero 146 | end 147 | 148 | it "must not be equal to a non-zero Vector" do 149 | zero.wont_equal Vector[3,2] 150 | Vector[3,2].wont_equal zero 151 | end 152 | end 153 | 154 | describe 'when enumerating' do 155 | it 'must have a first method' do 156 | zero.first.must_equal 0 157 | zero.first(1).must_equal [0] 158 | zero.first(5).must_equal [0,0,0,0,0] 159 | end 160 | end 161 | end 162 | -------------------------------------------------------------------------------- /test/geometry/square.rb: -------------------------------------------------------------------------------- 1 | require 'minitest/autorun' 2 | require 'geometry/square' 3 | 4 | describe Geometry::Square do 5 | Square = Geometry::Square 6 | 7 | describe "when constructed" do 8 | it "must create a Square from two Points" do 9 | square = Square.new from:[1,2], to:[3,4] 10 | square.must_be_kind_of Geometry::Square 11 | end 12 | 13 | it "must reorder swapped points when constructed from two Points" do 14 | square = Geometry::Square.new from:[3,4], to:[1,2] 15 | square.must_be_kind_of Geometry::Square 16 | square.instance_eval('@points[0]').must_equal Point[1,2] 17 | square.instance_eval('@points[1]').must_equal Point[3,4] 18 | end 19 | 20 | it "must accept an origin Point and a size" do 21 | square = Square.new origin:[1,2], size:5 22 | square.must_be_kind_of Geometry::SizedSquare 23 | square.origin.must_equal Point[1,2] 24 | square.height.must_equal 5 25 | square.width.must_equal 5 26 | end 27 | 28 | it 'must accept a center and a size' do 29 | square = Square.new center:[1,2], size:5 30 | square.must_be_kind_of Geometry::CenteredSquare 31 | square.center.must_equal Point[1,2] 32 | square.size.must_equal 5 33 | end 34 | 35 | it 'must work with only a size parameter' do 36 | square = Square.new size:5 37 | square.must_be_kind_of Geometry::CenteredSquare 38 | square.center.must_equal Point[0,0] 39 | square.size.must_equal 5 40 | end 41 | 42 | it 'must deal with a non-numeric size' do 43 | square = Square.new size:[5,5] 44 | square.must_be_kind_of Geometry::CenteredSquare 45 | square.center.must_equal Point[0,0] 46 | square.size.must_equal 5 47 | end 48 | 49 | it 'must accept a Size size' do 50 | square = Square.new size:Size[5,5] 51 | square.must_be_kind_of Geometry::CenteredSquare 52 | square.center.must_equal Point[0,0] 53 | square.size.must_equal 5 54 | end 55 | 56 | it 'must reject a size parameter that is not square' do 57 | -> { Square.new size:[1,2] }.must_raise Geometry::NotSquareError 58 | end 59 | end 60 | 61 | describe "properties" do 62 | subject { Square.new from:[2,3], to:[3,4] } 63 | 64 | it 'must always be closed' do 65 | subject.closed?.must_equal true 66 | end 67 | 68 | it 'must have edges' do 69 | subject.edges.must_equal [Edge([2,3], [2,4]), Edge.new([2,4], [3,4]), Edge.new([3,4], [3,3]), Edge.new([3,3], [2,3])] 70 | end 71 | 72 | it "must have an origin accessor" do 73 | subject.origin.must_equal Point[2,3] 74 | end 75 | 76 | it "must have a minmax property that returns the corners of the bounding rectangle" do 77 | subject.minmax.must_equal [Point[2, 3], Point[3, 4]] 78 | end 79 | 80 | it "must have a max property that returns the upper right corner of the bounding rectangle" do 81 | subject.max.must_equal Point[3, 4] 82 | end 83 | 84 | it "must have a min property that returns the lower left corner of the bounding rectangle" do 85 | subject.min.must_equal Point[2, 3] 86 | end 87 | 88 | it 'have a points property that returns 4 points in clockwise order starting from the lower-left' do 89 | subject.points.must_equal [Point[2,3], Point[2,4], Point[3,4], Point[3,3]] 90 | end 91 | 92 | it 'must have a path property that returns a closed Path' do 93 | square = Geometry::Square.new origin:[0,0], size:5 94 | square.path.must_equal Geometry::Path.new([0,0], [0,5], [5,5], [5,0], [0,0]) 95 | end 96 | end 97 | end 98 | 99 | describe Geometry::CenteredSquare do 100 | describe "when constructed" do 101 | it "must create a CenteredSquare from a center point and a size" do 102 | square = Geometry::CenteredSquare.new [2,3], 5 103 | square.must_be_instance_of Geometry::CenteredSquare 104 | square.must_be_kind_of Geometry::Square 105 | end 106 | end 107 | 108 | describe "properties" do 109 | let(:square) { Geometry::CenteredSquare.new [2,3], 4 } 110 | 111 | it "must have a center property" do 112 | square.center.must_equal Point[2,3] 113 | end 114 | 115 | it 'have a points property that returns 4 points in clockwise order starting from the lower-left' do 116 | square.points.must_equal [Point[0,1], Point[0,5], Point[4,5], Point[4,1]] 117 | end 118 | 119 | it "must have a height property" do 120 | square.height.must_equal 4 121 | end 122 | 123 | it "must have a width property" do 124 | square.width.must_equal 4 125 | end 126 | 127 | it "must have a minmax property that returns the corners of the bounding rectangle" do 128 | square.minmax.must_equal [Point[0, 1], Point[4, 5]] 129 | end 130 | 131 | it "must have a max property that returns the upper right corner of the bounding rectangle" do 132 | square.max.must_equal Point[4, 5] 133 | end 134 | 135 | it "must have a min property that returns the lower left corner of the bounding rectangle" do 136 | square.min.must_equal Point[0, 1] 137 | end 138 | end 139 | end 140 | 141 | describe Geometry::SizedSquare do 142 | describe "when constructed" do 143 | it "must create a SizedSquare from a point and a size" do 144 | square = Geometry::SizedSquare.new [2,3], 5 145 | square.must_be_instance_of Geometry::SizedSquare 146 | square.must_be_kind_of Geometry::Square 147 | end 148 | end 149 | 150 | describe "properties" do 151 | let(:square) { Geometry::SizedSquare.new [2,3], 4 } 152 | 153 | it "must have a center property" do 154 | square.center.must_equal Point[4,5] 155 | end 156 | 157 | it 'have a points property that returns 4 points in clockwise order starting from the lower-left' do 158 | square.points.must_equal [Point[2,3], Point[2,7], Point[6,7], Point[6,3]] 159 | end 160 | 161 | it "must have a height property" do 162 | square.height.must_equal 4 163 | end 164 | 165 | it "must have a width property" do 166 | square.width.must_equal 4 167 | end 168 | 169 | it "must have a minmax property that returns the corners of the bounding rectangle" do 170 | square.minmax.must_equal [Point[2, 3], Point[6, 7]] 171 | end 172 | 173 | it "must have a max property that returns the upper right corner of the bounding rectangle" do 174 | square.max.must_equal Point[6, 7] 175 | end 176 | 177 | it "must have a min property that returns the lower left corner of the bounding rectangle" do 178 | square.min.must_equal Point[2, 3] 179 | end 180 | end 181 | end 182 | -------------------------------------------------------------------------------- /test/geometry/transformation.rb: -------------------------------------------------------------------------------- 1 | require 'minitest/autorun' 2 | require 'geometry/point' 3 | require 'geometry/transformation' 4 | 5 | describe Geometry::Transformation do 6 | Transformation = Geometry::Transformation 7 | 8 | describe "when constructed" do 9 | it "must accept nothing and become an identity transformation" do 10 | Transformation.new.identity?.must_equal true 11 | end 12 | 13 | it "must accept a translate parameter" do 14 | Transformation.new([4,2]).translation.must_equal Point[4,2] 15 | end 16 | 17 | it "must accept a translate Array" do 18 | translate = Transformation.new(:translate => [4,2]) 19 | translate.translation.must_equal Point[4,2] 20 | end 21 | 22 | it "must accept a translate Point" do 23 | translate = Transformation.new(:translate => Point[4,2]) 24 | translate.translation.must_equal Point[4,2] 25 | end 26 | 27 | it "must accept a translate Point equal to zero" do 28 | translate = Transformation.new(:translate => [0,0]) 29 | translate.translation.must_equal nil 30 | end 31 | 32 | it "must accept a translate Vector" do 33 | translate = Transformation.new(:translate => Vector[4,2]) 34 | translate.translation.must_equal Point[4,2] 35 | end 36 | 37 | it "must accept an origin option" do 38 | translate = Transformation.new(:origin => [4,2]) 39 | translate.translation.must_equal Point[4,2] 40 | end 41 | 42 | it "must raise an exception when given too many translation options" do 43 | lambda { Transformation.new :translate => [1,2], :origin => [3,4] }.must_raise ArgumentError 44 | end 45 | 46 | describe "when given a dimensions option" do 47 | it "must raise an exception if the other arguments are too big" do 48 | lambda { Transformation.new :dimensions => 2, :origin => [1,2,3] }.must_raise ArgumentError 49 | end 50 | 51 | it "must raise an exception if the other arguments are too small" do 52 | lambda { Transformation.new :dimensions => 3, :origin => [1,2] }.must_raise ArgumentError 53 | end 54 | 55 | it "must not complain when given only a dimensions option" do 56 | Transformation.new(:dimensions => 3).dimensions.must_equal 3 57 | end 58 | end 59 | 60 | describe "rotation" do 61 | it "must accept a y axis option" do 62 | t = Transformation.new :y => [1,0] 63 | t.rotation.y.must_equal [1,0] 64 | t.identity?.wont_equal true 65 | end 66 | 67 | it "must accept a rotation angle" do 68 | transformation = Transformation.new angle:90 69 | transformation.identity?.wont_equal true 70 | transformation.rotation.wont_be_nil 71 | transformation.rotation.angle.must_equal 90 72 | end 73 | 74 | it "must accept a rotation angle specified by an X-axis" do 75 | transformation = Transformation.new x:[0,1] 76 | rotation = transformation.rotation 77 | rotation.must_be_instance_of(RotationAngle) 78 | rotation.angle.must_equal Math::PI/2 79 | rotation.x.x.must_be_close_to 0 80 | rotation.x.y.must_be_close_to 1 81 | rotation.y.x.must_be_close_to(-1) 82 | rotation.y.y.must_be_close_to 0 83 | end 84 | end 85 | end 86 | 87 | describe 'convenience constructors' do 88 | it 'must create a translation from coordinates' do 89 | Geometry.translation(1, 2, 3).translation.must_equal Point[1,2,3] 90 | end 91 | 92 | it 'must create a translation from an Array' do 93 | Geometry.translation([1, 2, 3]).translation.must_equal Point[1,2,3] 94 | end 95 | 96 | it 'must create a translation from a Point' do 97 | Geometry.translation(Point[1, 2, 3]).translation.must_equal Point[1,2,3] 98 | end 99 | end 100 | 101 | it 'must translate with a Point' do 102 | Transformation.new(translate:[1,2]).translate(Point[3,4]).translation.must_equal Point[4,6] 103 | end 104 | 105 | it 'must translate with an Array' do 106 | Transformation.new(translate:[1,2]).translate([3,4]).translation.must_equal Point[4,6] 107 | end 108 | 109 | describe "comparison" do 110 | subject { Transformation.new(origin:[1,2]) } 111 | 112 | it "must equate equal transformations" do 113 | subject.must_equal Transformation.new(origin:[1,2]) 114 | end 115 | 116 | it "must not equal nil" do 117 | subject.eql?(nil).wont_equal true 118 | end 119 | 120 | it "must not equate a translation with a rotation" do 121 | subject.wont_equal Transformation.new(x:[0,1,0], y:[1,0,0]) 122 | end 123 | 124 | it "must equate empty transformations" do 125 | Transformation.new.must_equal Transformation.new 126 | end 127 | end 128 | 129 | describe "attributes" do 130 | describe "has_rotation?" do 131 | it "must properly be true" do 132 | Transformation.new(angle:90).has_rotation?.must_equal true 133 | end 134 | 135 | it "must properly be false" do 136 | Transformation.new.has_rotation?.must_equal false 137 | end 138 | end 139 | end 140 | 141 | describe "composition" do 142 | let(:translate_left) { Geometry::Transformation.new origin:[-2,-2] } 143 | let(:translate_right) { Geometry::Transformation.new origin:[1,1] } 144 | let(:transformation) { Geometry::Transformation.new } 145 | 146 | it "must add pure translation" do 147 | (translate_left + translate_right).must_equal Geometry::Transformation.new origin:[-1,-1] 148 | end 149 | 150 | it "must add translation and no translation" do 151 | (transformation + translate_left).must_equal translate_left 152 | (translate_left + transformation).must_equal translate_left 153 | end 154 | 155 | it "array addition" do 156 | (transformation + [1,2]).translation.must_equal Point[1,2] 157 | ((transformation + [1,2]) + [2,3]).translation.must_equal Point[3,5] 158 | (transformation + [1,2]).rotation.must_be_nil 159 | end 160 | 161 | it "must update the translation when an array is subtracted" do 162 | (transformation - [1,2]).translation.must_equal Point[-1,-2] 163 | ((transformation - [1,2]) - [2,3]).translation.must_equal Point[-3,-5] 164 | (transformation - [1,2,3]).rotation.must_be_nil 165 | end 166 | 167 | it "must subtract translation and no translation" do 168 | (transformation - translate_left).must_equal translate_left 169 | (translate_left - transformation).must_equal translate_left 170 | end 171 | end 172 | 173 | describe "when transforming a Point" do 174 | describe "when no transformation is set" do 175 | it "must return the Point" do 176 | Transformation.new.transform(Point[1,2]).must_equal Point[1,2]; 177 | end 178 | end 179 | 180 | it "must translate" do 181 | Geometry::Transformation.new(origin:[0,1]).transform([1,0]).must_equal Point[1,1] 182 | end 183 | 184 | it "must rotate" do 185 | rotated_point = Transformation.new(angle:Math::PI/2).transform([1,0]) 186 | rotated_point.x.must_be_close_to 0 187 | rotated_point.y.must_be_close_to 1 188 | end 189 | 190 | end 191 | end 192 | -------------------------------------------------------------------------------- /test/geometry/transformation/composition.rb: -------------------------------------------------------------------------------- 1 | require 'minitest/autorun' 2 | require 'geometry/transformation/composition' 3 | 4 | describe Geometry::Transformation::Composition do 5 | Composition = Geometry::Transformation::Composition 6 | Transformation = Geometry::Transformation 7 | 8 | subject { Composition.new } 9 | 10 | describe "when constructed" do 11 | it "must accept multiple transformations" do 12 | composition = Composition.new(Transformation.new, Transformation.new) 13 | composition.size.must_equal 2 14 | end 15 | 16 | it "must reject anything that isn't a Transformation" do 17 | -> { Composition.new :foo }.must_raise TypeError 18 | end 19 | end 20 | 21 | describe "attributes" do 22 | describe "has_rotation?" do 23 | it "must properly be true" do 24 | Composition.new(Transformation.new angle:90).has_rotation?.must_equal true 25 | end 26 | 27 | it "must properly be false" do 28 | subject.has_rotation?.must_equal false 29 | end 30 | end 31 | end 32 | 33 | describe "when composing" do 34 | it "must append a Transformation" do 35 | (Composition.new(Transformation.new) + Transformation.new).size.must_equal 2 36 | end 37 | 38 | it "must merge with a Composition" do 39 | (Composition.new(Transformation.new) + Composition.new(Transformation.new)).size.must_equal 2 40 | end 41 | end 42 | 43 | describe "when transforming a Point" do 44 | it "must handle composed translations" do 45 | composition = Composition.new(Transformation.new origin:[1,2]) + Composition.new(Transformation.new [3,4]) 46 | composition.transform(Point[5,6]).must_equal Point[9, 12] 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /test/geometry/triangle.rb: -------------------------------------------------------------------------------- 1 | require 'minitest/autorun' 2 | require 'geometry/triangle' 3 | 4 | describe Geometry::Triangle do 5 | Triangle = Geometry::Triangle 6 | 7 | it 'must always be closed' do 8 | Triangle.new([0,0], [0,1], [1,0]).closed?.must_equal true 9 | end 10 | 11 | describe "when constructed with 3 points" do 12 | let(:triangle) { Triangle.new [0,0], [0,1], [1,0] } 13 | 14 | it "must create a scalene Triangle" do 15 | triangle.must_be_instance_of Geometry::ScaleneTriangle 16 | triangle.must_be_kind_of Triangle 17 | end 18 | 19 | it "must have a points accessor" do 20 | triangle.points.must_equal [Point[0,0], Point[0,1], Point[1,0]] 21 | end 22 | 23 | it 'must know the max' do 24 | triangle.max.must_equal Point[1,1] 25 | end 26 | 27 | it 'must know the min' do 28 | triangle.min.must_equal Point[0,0] 29 | end 30 | 31 | it 'must know the min and the max' do 32 | triangle.minmax.must_equal [Point[0,0], Point[1,1]] 33 | end 34 | end 35 | 36 | describe "when constructed with a point and a leg length" do 37 | let(:triangle) { Triangle.new [0,0], 1 } 38 | 39 | it "must create a right Triangle" do 40 | triangle.must_be_instance_of Geometry::RightTriangle 41 | triangle.must_be_kind_of(Triangle) 42 | end 43 | 44 | it "must have a points accessor" do 45 | triangle.points.must_equal [Point[0,0], Point[0,1], Point[1,0]] 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /test/geometry/vector.rb: -------------------------------------------------------------------------------- 1 | require 'minitest/autorun' 2 | require 'geometry/vector' 3 | 4 | describe Vector do 5 | describe "when monkeypatching Vector" do 6 | let(:left) { Vector[1,2] } 7 | let(:right) { Vector[3,4] } 8 | 9 | it "must have +@" do 10 | (+left).must_equal Vector[1,2] 11 | end 12 | 13 | it "must have unary negation" do 14 | (-left).must_equal Vector[-1,-2] 15 | end 16 | 17 | it "must cross product" do 18 | left.cross(right).must_equal(-2) 19 | Vector[1,2,3].cross(Vector[3,4,5]).must_equal Vector[-2, 4, -2] 20 | (Vector[1,2,3] ** Vector[3,4,5]).must_equal Vector[-2, 4, -2] 21 | end 22 | 23 | it "must have a constant representing the X axis" do 24 | Vector::X.must_equal Vector[1,0,0] 25 | end 26 | 27 | it "must have a constant representing the Y axis" do 28 | Vector::Y.must_equal Vector[0,1,0] 29 | end 30 | 31 | it "must have a constant representing the Z axis" do 32 | Vector::Z.must_equal Vector[0,0,1] 33 | end 34 | 35 | it "must not create global axis constants" do 36 | -> { X }.must_raise NameError 37 | -> { Y }.must_raise NameError 38 | -> { Z }.must_raise NameError 39 | end 40 | end 41 | end 42 | --------------------------------------------------------------------------------