compat_geom2d 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,84 @@
1
+ # -*- frozen_string_literal: true -*-
2
+ #
3
+ #--
4
+ # geom2d - 2D Geometric Objects and Algorithms
5
+ # Copyright (C) 2018 Thomas Leitner <t_leitner@gmx.at>
6
+ #
7
+ # This software may be modified and distributed under the terms
8
+ # of the MIT license. See the LICENSE file for details.
9
+ #++
10
+
11
+ module Geom2D
12
+
13
+ # Represents an axis aligned bounding box.
14
+ #
15
+ # An empty bounding box contains just the point at origin.
16
+ class BoundingBox
17
+
18
+ # The minimum x-coordinate.
19
+ attr_reader :min_x
20
+
21
+ # The minimum y-coordinate.
22
+ attr_reader :min_y
23
+
24
+ # The maximum x-coordinate.
25
+ attr_reader :max_x
26
+
27
+ # The maximum y-coordinate.
28
+ attr_reader :max_y
29
+
30
+ # Creates a new BoundingBox.
31
+ def initialize(min_x = 0, min_y = 0, max_x = 0, max_y = 0)
32
+ @min_x = min_x
33
+ @min_y = min_y
34
+ @max_x = max_x
35
+ @max_y = max_y
36
+ end
37
+
38
+ # Updates this bounding box to also contain the given bounding box or point.
39
+ def add!(other)
40
+ case other
41
+ when BoundingBox
42
+ @min_x = [min_x, other.min_x].min
43
+ @min_y = [min_y, other.min_y].min
44
+ @max_x = [max_x, other.max_x].max
45
+ @max_y = [max_y, other.max_y].max
46
+ when Point
47
+ @min_x = [min_x, other.x].min
48
+ @min_y = [min_y, other.y].min
49
+ @max_x = [max_x, other.x].max
50
+ @max_y = [max_y, other.y].max
51
+ else
52
+ raise ArgumentError, "Can only use another BoundingBox or Point"
53
+ end
54
+ self
55
+ end
56
+
57
+ # Returns the width of the bounding box.
58
+ def width
59
+ @max_x - @min_x
60
+ end
61
+
62
+ # Returns the height of the bounding box.
63
+ def height
64
+ @max_y - @min_y
65
+ end
66
+
67
+ # Returns a bounding box containing this bounding box and the argument.
68
+ def add(other)
69
+ dup.add!(other)
70
+ end
71
+ alias + add
72
+
73
+ # Returns the bounding box as an array of the form [min_x, min_y, max_x, max_y].
74
+ def to_a
75
+ [@min_x, @min_y, @max_x, @max_y]
76
+ end
77
+
78
+ def inspect #:nodoc:
79
+ "BBox[#{min_x}, #{min_y}, #{max_x}, #{max_y}]"
80
+ end
81
+
82
+ end
83
+
84
+ end
@@ -0,0 +1,145 @@
1
+ # -*- frozen_string_literal: true -*-
2
+ #
3
+ #--
4
+ # geom2d - 2D Geometric Objects and Algorithms
5
+ # Copyright (C) 2018 Thomas Leitner <t_leitner@gmx.at>
6
+ #
7
+ # This software may be modified and distributed under the terms
8
+ # of the MIT license. See the LICENSE file for details.
9
+ #++
10
+
11
+ require 'geom2d'
12
+
13
+ module Geom2D
14
+
15
+ # Represents a point.
16
+ class Point
17
+
18
+ include Utils
19
+
20
+ # The x-coordinate.
21
+ attr_reader :x
22
+
23
+ # The y-coordinate.
24
+ attr_reader :y
25
+
26
+ # Creates a new Point from the given coordinates.
27
+ def initialize(x, y)
28
+ @x = x
29
+ @y = y
30
+ end
31
+
32
+ # Returns the point's bounding box (i.e. a bounding box containing only the point itself).
33
+ def bbox
34
+ BoundingBox.new(x, y, x, y)
35
+ end
36
+
37
+ # Returns the distance from this point to the given point.
38
+ def distance(point)
39
+ Math.hypot(point.x - x, point.y - y)
40
+ end
41
+
42
+ # Returns self.
43
+ def +@
44
+ self
45
+ end
46
+
47
+ # Returns the point mirrored in the origin.
48
+ def -@
49
+ Point.new(-x, -y)
50
+ end
51
+
52
+ # Depending on the type of the argument, either adds a number to each coordinate or adds two
53
+ # points.
54
+ def +(other)
55
+ case other
56
+ when Point
57
+ Point.new(x + other.x, y + other.y)
58
+ when Numeric
59
+ Point.new(x + other, y + other)
60
+ when Array
61
+ self + Geom2D::Point(other)
62
+ else
63
+ raise ArgumentError, "Invalid argument class, must be Numeric or Point"
64
+ end
65
+ end
66
+
67
+ # Depending on the type of the argument, either subtracts a number from each coordinate or
68
+ # subtracts the other point from this one.
69
+ def -(other)
70
+ case other
71
+ when Point
72
+ Point.new(x - other.x, y - other.y)
73
+ when Numeric
74
+ Point.new(x - other, y - other)
75
+ when Array
76
+ self - Geom2D::Point(other)
77
+ else
78
+ raise ArgumentError, "Invalid argument class, must be Numeric or Point"
79
+ end
80
+ end
81
+
82
+ # Depending on the type of the argument, either multiplies this point with the other point (dot
83
+ # product) or multiplies each coordinate with the given number.
84
+ def *(other)
85
+ case other
86
+ when Point
87
+ x * other.x + y * other.y
88
+ when Numeric
89
+ Point.new(x * other, y * other)
90
+ when Array
91
+ self * Geom2D::Point(other)
92
+ else
93
+ raise ArgumentError, "Invalid argument class, must be Numeric or Point"
94
+ end
95
+ end
96
+
97
+ # Multiplies this point with the other point using the dot product.
98
+ def dot(other)
99
+ self * other
100
+ end
101
+
102
+ # Performs the wedge product of this point with the other point.
103
+ def wedge(other)
104
+ other = Geom2D::Point(other)
105
+ x * other.y - other.x * y
106
+ end
107
+
108
+ # Divides each coordinate by the given number.
109
+ def /(other)
110
+ case other
111
+ when Numeric
112
+ Point.new(x / other.to_f, y / other.to_f)
113
+ else
114
+ raise ArgumentError, "Invalid argument class, must be Numeric"
115
+ end
116
+ end
117
+
118
+ # Compares this point to the other point, using floating point equality.
119
+ #
120
+ # See Utils#float_equal.
121
+ def ==(other)
122
+ case other
123
+ when Point
124
+ float_equal(x, other.x) && float_equal(y, other.y)
125
+ when Array
126
+ self == Geom2D::Point(other)
127
+ else
128
+ false
129
+ end
130
+ end
131
+
132
+ # Allows destructuring of a point into an array.
133
+ def to_ary
134
+ [x, y]
135
+ end
136
+ alias to_a to_ary
137
+
138
+ def inspect #:nodoc:
139
+ "(#{x}, #{y})"
140
+ end
141
+ alias to_s inspect
142
+
143
+ end
144
+
145
+ end
@@ -0,0 +1,108 @@
1
+ # -*- frozen_string_literal: true -*-
2
+ #
3
+ #--
4
+ # geom2d - 2D Geometric Objects and Algorithms
5
+ # Copyright (C) 2018 Thomas Leitner <t_leitner@gmx.at>
6
+ #
7
+ # This software may be modified and distributed under the terms
8
+ # of the MIT license. See the LICENSE file for details.
9
+ #++
10
+
11
+ require 'geom2d'
12
+
13
+ module Geom2D
14
+
15
+ # Represents a polygon.
16
+ class Polygon
17
+
18
+ # Creates a new Polygon object. The +vertices+ argument has to be an array of point-like
19
+ # objects.
20
+ def initialize(vertices = [])
21
+ @vertices = []
22
+ vertices.each {|value| @vertices << Geom2D::Point(value) }
23
+ end
24
+
25
+ # Returns one since a polygon object represents a single polygon.
26
+ def nr_of_contours
27
+ 1
28
+ end
29
+
30
+ # Returns the number of vertices in the polygon.
31
+ def nr_of_vertices
32
+ @vertices.size
33
+ end
34
+
35
+ # Returns the i-th vertex of the polygon.
36
+ def [](i)
37
+ @vertices[i]
38
+ end
39
+
40
+ # Adds a new vertex to the end of the polygon.
41
+ def add(x, y = nil)
42
+ @vertices << Geom2D::Point(x, y)
43
+ self
44
+ end
45
+ alias << add
46
+
47
+ # Removes the last vertex of the polygon.
48
+ def pop
49
+ @vertices.pop
50
+ end
51
+
52
+ # Calls the given block once for each vertex of the polygon.
53
+ #
54
+ # If no block is given, an Enumerator is returned.
55
+ def each_vertex(&block)
56
+ return to_enum(__method__) unless block_given?
57
+ @vertices.each(&block)
58
+ end
59
+
60
+ # Calls the given block once for each segment in the polygon.
61
+ #
62
+ # If no block is given, an Enumerator is returned.
63
+ def each_segment
64
+ return to_enum(__method__) unless block_given?
65
+ return unless @vertices.size > 1
66
+
67
+ 0.upto(@vertices.size - 2) do |i|
68
+ yield(Geom2D::Segment(@vertices[i], @vertices[i + 1]))
69
+ end
70
+ yield(Geom2D::Segment(@vertices[-1], @vertices[0]))
71
+ end
72
+
73
+ # Returns the BoundingBox of this polygon, or an empty BoundingBox if the polygon has no
74
+ # vertices.
75
+ def bbox
76
+ return BoundingBox.new if @vertices.empty?
77
+ result = @vertices.first.bbox
78
+ @vertices[1..-1].each {|v| result.add!(v) }
79
+ result
80
+ end
81
+
82
+ # Returns +true+ if the vertices of the polygon are ordered in a counterclockwise fashion.
83
+ def ccw?
84
+ return true if @vertices.empty?
85
+ area = @vertices[-1].wedge(@vertices[0])
86
+ 1.upto(@vertices.size - 2) {|i| area += @vertices[i].wedge(@vertices[i + 1]) }
87
+ area >= 0
88
+ end
89
+
90
+ # Reverses the direction of the vertices (and therefore the segments).
91
+ def reverse!
92
+ @vertices.reverse!
93
+ end
94
+
95
+ # Returns an array with the vertices of the polygon.
96
+ def to_ary
97
+ @vertices.dup
98
+ end
99
+ alias to_a to_ary
100
+
101
+ def inspect #:nodoc:
102
+ "Polygon#{@vertices}"
103
+ end
104
+ alias to_s inspect
105
+
106
+ end
107
+
108
+ end
@@ -0,0 +1,67 @@
1
+ # -*- frozen_string_literal: true -*-
2
+ #
3
+ #--
4
+ # geom2d - 2D Geometric Objects and Algorithms
5
+ # Copyright (C) 2018 Thomas Leitner <t_leitner@gmx.at>
6
+ #
7
+ # This software may be modified and distributed under the terms
8
+ # of the MIT license. See the LICENSE file for details.
9
+ #++
10
+
11
+ require 'geom2d/polygon'
12
+
13
+ module Geom2D
14
+
15
+ # Represents a set of polygons.
16
+ class PolygonSet
17
+
18
+ # The array of polygons.
19
+ attr_reader :polygons
20
+
21
+ # Creates a new PolygonSet with the given polygons.
22
+ def initialize(polygons = [])
23
+ @polygons = polygons
24
+ end
25
+
26
+ # Adds a polygon to this set.
27
+ def add(polygon)
28
+ @polygons << polygon
29
+ self
30
+ end
31
+ alias << add
32
+
33
+ # Creates a new polygon set by combining the polygons from this set and the other one.
34
+ def join(other)
35
+ PolygonSet.new(@polygons + other.polygons)
36
+ end
37
+ alias + join
38
+
39
+ # Calls the given block once for each segment of each polygon in the set.
40
+ #
41
+ # If no block is given, an Enumerator is returned.
42
+ def each_segment(&block)
43
+ return to_enum(__method__) unless block_given?
44
+ @polygons.each {|polygon| polygon.each_segment(&block) }
45
+ end
46
+
47
+ # Returns the number of polygons in this set.
48
+ def nr_of_contours
49
+ @polygons.size
50
+ end
51
+
52
+ # Returns the BoundingBox of all polygons in the set, or +nil+ if it contains no polygon.
53
+ def bbox
54
+ return BoundingBox.new if @polygons.empty?
55
+ result = @polygons.first.bbox
56
+ @polygons[1..-1].each {|v| result.add!(v.bbox) }
57
+ result
58
+ end
59
+
60
+ def inspect #:nodoc:
61
+ "PolygonSet#{@polygons}"
62
+ end
63
+ alias to_s inspect
64
+
65
+ end
66
+
67
+ end
@@ -0,0 +1,202 @@
1
+ # -*- frozen_string_literal: true -*-
2
+ #
3
+ #--
4
+ # geom2d - 2D Geometric Objects and Algorithms
5
+ # Copyright (C) 2018 Thomas Leitner <t_leitner@gmx.at>
6
+ #
7
+ # This software may be modified and distributed under the terms
8
+ # of the MIT license. See the LICENSE file for details.
9
+ #++
10
+
11
+ require 'geom2d'
12
+
13
+ module Geom2D
14
+
15
+ # Represents a line segment.
16
+ class Segment
17
+
18
+ include Utils
19
+
20
+ # The start point of the segment.
21
+ attr_reader :start_point
22
+
23
+ # The end point of the segment.
24
+ attr_reader :end_point
25
+
26
+ # Creates a new Segment from the start to the end point. The arguments are converted to proper
27
+ # Geom2D::Point objects if needed.
28
+ def initialize(start_point, end_point)
29
+ @start_point = Geom2D::Point(start_point)
30
+ @end_point = Geom2D::Point(end_point)
31
+ end
32
+
33
+ # Returns +true+ if the segment is degenerate, i.e. if it consists only of a point.
34
+ def degenerate?
35
+ @start_point == @end_point
36
+ end
37
+
38
+ # Returns +true+ if the segment is vertical.
39
+ def vertical?
40
+ float_equal(start_point.x, end_point.x)
41
+ end
42
+
43
+ # Returns +true+ if the segment is horizontal.
44
+ def horizontal?
45
+ float_equal(start_point.y, end_point.y)
46
+ end
47
+
48
+ # Returns the left-most bottom-most point of the segment (either the start or the end point).
49
+ def min
50
+ if start_point.x < end_point.x ||
51
+ (float_equal(start_point.x, end_point.x) && start_point.y < end_point.y)
52
+ start_point
53
+ else
54
+ end_point
55
+ end
56
+ end
57
+
58
+ # Returns the right-most top-most point of the segment (either the start or the end point).
59
+ def max
60
+ if start_point.x > end_point.x ||
61
+ (float_equal(start_point.x, end_point.x) && start_point.y > end_point.y)
62
+ start_point
63
+ else
64
+ end_point
65
+ end
66
+ end
67
+
68
+ # Returns the length of the segment.
69
+ def length
70
+ start_point.distance(end_point)
71
+ end
72
+
73
+ # Returns the direction vector of the segment as Geom2D::Point object.
74
+ def direction
75
+ end_point - start_point
76
+ end
77
+
78
+ # Returns the slope of the segment.
79
+ #
80
+ # If the segment is vertical, Float::INFINITY is returned.
81
+ def slope
82
+ if float_equal(start_point.x, end_point.x)
83
+ Float::INFINITY
84
+ else
85
+ (end_point.y - start_point.y).to_f / (end_point.x - start_point.x)
86
+ end
87
+ end
88
+
89
+ # Returns the y-intercept, i.e. the point on the y-axis where the segment does/would intercept
90
+ # it.
91
+ def y_intercept
92
+ slope = self.slope
93
+ if slope == Float::INFINITY
94
+ nil
95
+ else
96
+ -start_point.x * slope + start_point.y
97
+ end
98
+ end
99
+
100
+ # Reverses the start and end point.
101
+ def reverse!
102
+ @start_point, @end_point = @end_point, @start_point
103
+ end
104
+
105
+ # Returns the intersection of this segment with the given one:
106
+ #
107
+ # +nil+:: No intersections
108
+ # Geom2D::Point:: Exactly one point
109
+ # Geom2D::Segment:: The segment overlapping both other segments.
110
+ def intersect(segment)
111
+ p0 = start_point
112
+ p1 = segment.start_point
113
+ d0 = direction
114
+ d1 = segment.direction
115
+ e = p1 - p0
116
+
117
+ cross = d0.wedge(d1).to_f # cross product of direction vectors
118
+
119
+ if cross.abs > Utils.precision # segments are not parallel
120
+ s = e.wedge(d1) / cross
121
+ return nil if s < 0 || s > 1
122
+ t = e.wedge(d0) / cross
123
+ return nil if t < 0 || t > 1
124
+
125
+ result = p0 + [s * d0.x, s * d0.y]
126
+ result = start_point if result == start_point
127
+ result = end_point if result == end_point
128
+ result = segment.start_point if result == segment.start_point
129
+ result = segment.end_point if result == segment.end_point
130
+ return result
131
+ end
132
+
133
+ return nil if e.wedge(d0).abs > Utils.precision # non-intersecting parallel segment lines
134
+
135
+ e0 = end_point
136
+ e1 = segment.end_point
137
+
138
+ # sort segment points by x-value
139
+ p0, e0 = e0, p0 if float_compare(p0.x, e0.x) > 0
140
+ p1, e1 = e1, p1 if float_compare(p1.x, e1.x) > 0
141
+ if float_compare(p0.x, p1.x) > 0
142
+ _p0, p1, e0, e1 = p1, p0, e1, e0
143
+ end
144
+
145
+ # p0 before or equal to p1
146
+ if float_compare(e0.x, p1.x) < 0 # e0 before p1
147
+ nil # no common point
148
+ elsif float_compare(e1.x, e0.x) <= 0 # e1 before or equal to e0
149
+ self.class.new(p1, e1) # p1-e1 inside p0-e0
150
+ elsif float_compare(p1.x, e0.x) == 0 # common endpoint p1=e0
151
+ p1
152
+ else
153
+ self.class.new(p1, e0) # s1 overlaps end of s0
154
+ end
155
+ end
156
+
157
+ # Returns self.
158
+ def +@
159
+ self
160
+ end
161
+
162
+ # Returns the segment mirrored in the origin.
163
+ def -@
164
+ Segment.new(-start_point, -end_point)
165
+ end
166
+
167
+ # Adds the given vector (given as array or Geom2D::Point) to the segment, i.e. performs a
168
+ # translation.
169
+ def +(other)
170
+ case other
171
+ when Point, Array
172
+ Segment.new(start_point + other, end_point + other)
173
+ else
174
+ raise ArgumentError, "Invalid argument class, must be Point"
175
+ end
176
+ end
177
+
178
+ # Subtracts the given vector (given as array or Geom2D::Point) from the segment, i.e. performs a
179
+ # translation.
180
+ def -(other)
181
+ case other
182
+ when Point, Array
183
+ Segment.new(start_point - other, end_point - other)
184
+ else
185
+ raise ArgumentError, "Invalid argument class, must be Point"
186
+ end
187
+ end
188
+
189
+ # Compares this segment to the other, returning true if the end points match.
190
+ def ==(other)
191
+ return false unless other.kind_of?(Segment)
192
+ start_point == other.start_point && end_point == other.end_point
193
+ end
194
+
195
+ def inspect #:nodoc:
196
+ "Segment[#{start_point}-#{end_point}]"
197
+ end
198
+ alias to_s inspect
199
+
200
+ end
201
+
202
+ end