geom2d 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,38 @@
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
+ # Contains utility methods and classes.
14
+ module Utils
15
+
16
+ autoload(:SortedLinkedList, 'geom2d/utils/sorted_linked_list')
17
+
18
+ # The precision when comparing two floats, defaults to 1e-10.
19
+ singleton_class.attr_accessor :precision
20
+ self.precision = 1e-10
21
+
22
+ private
23
+
24
+ # Compares two float whether they are equal using the set precision.
25
+ def float_equal(a, b)
26
+ (a - b).abs < Utils.precision
27
+ end
28
+
29
+ # Compares two floats like the <=> operator but using the set precision for detecting whether
30
+ # they are equal.
31
+ def float_compare(a, b)
32
+ result = a - b
33
+ (result.abs < Utils.precision ? 0 : a <=> b)
34
+ end
35
+
36
+ end
37
+
38
+ end
@@ -0,0 +1,154 @@
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
+ module Utils
13
+
14
+ # A doubly linked list that keeps its items sorted.
15
+ class SortedLinkedList
16
+
17
+ include Enumerable
18
+
19
+ # A node of the double linked list.
20
+ class Node
21
+
22
+ AnchorValue = Object.new #:nodoc:
23
+
24
+ # Creates a Node object that can be used as the anchor of the doubly linked list.
25
+ def self.create_anchor
26
+ Node.new(AnchorValue).tap {|anchor| anchor.next_node = anchor.prev_node = anchor }
27
+ end
28
+
29
+ # The previous node in the list. The first node points to the anchor node.
30
+ attr_accessor :prev_node
31
+
32
+ # The next node in the list. The last node points to the anchor node.
33
+ attr_accessor :next_node
34
+
35
+ # The value of the node.
36
+ attr_accessor :value
37
+
38
+ # Creates a new Node for the given value, with optional previous and next nodes to point to.
39
+ def initialize(value, prev_node = nil, next_node = nil)
40
+ @prev_node = prev_node
41
+ @next_node = next_node
42
+ @value = value
43
+ end
44
+
45
+ # Returns +true+ if this node is an anchor node, i.e. the start and end of this list.
46
+ def anchor?
47
+ @value == AnchorValue
48
+ end
49
+
50
+ # Inserts this node before the given node.
51
+ def insert_before(node)
52
+ @prev_node = node.prev_node
53
+ @next_node = node
54
+ node.prev_node.next_node = self
55
+ node.prev_node = self
56
+ self
57
+ end
58
+
59
+ # Deletes this node from the linked list.
60
+ def delete
61
+ @prev_node.next_node = @next_node
62
+ @next_node.prev_node = @prev_node
63
+ @prev_node = @next_node = nil
64
+ @value
65
+ end
66
+
67
+ end
68
+
69
+ # Creates a new SortedLinkedList using the +comparator+ or the given block as compare
70
+ # function.
71
+ #
72
+ # The comparator has to respond to +call(a, b)+ where +a+ is the value to be inserted and +b+
73
+ # is the value to which it is compared. The return value should be +true+ if the value +a+
74
+ # should be inserted before +b+, i.e. at the position of +b+.
75
+ def initialize(comparator = nil, &block)
76
+ @anchor = Node.create_anchor
77
+ @comparator = comparator || block
78
+ end
79
+
80
+ # Returns +true+ if the list is empty?
81
+ def empty?
82
+ @anchor.next_node == @anchor
83
+ end
84
+
85
+ # Returns the last value in the list.
86
+ def last
87
+ empty? ? nil : @anchor.prev_node.value
88
+ end
89
+
90
+ # Yields each value in sorted order.
91
+ #
92
+ # If no block is given, an enumerator is returned.
93
+ def each #:yield: value
94
+ return to_enum(__method__) unless block_given?
95
+ current = @anchor.next_node
96
+ while current != @anchor
97
+ yield(current.value)
98
+ current = current.next_node
99
+ end
100
+ end
101
+
102
+ # Returns the node with the given value, or +nil+ if no such value is found.
103
+ def find_node(value)
104
+ current = @anchor.next_node
105
+ while current != @anchor
106
+ return current if current.value == value
107
+ current = current.next_node
108
+ end
109
+ end
110
+
111
+ # Inserts the value and returns self.
112
+ def push(value)
113
+ insert(value)
114
+ self
115
+ end
116
+
117
+ # Inserts a new node with the given value into the list (at a position decided by the compare
118
+ # function) and returns it.
119
+ def insert(value)
120
+ node = Node.new(value)
121
+ current = @anchor.next_node
122
+ while current != @anchor
123
+ if @comparator.call(node.value, current.value)
124
+ return node.insert_before(current)
125
+ end
126
+ current = current.next_node
127
+ end
128
+ node.insert_before(@anchor)
129
+ end
130
+
131
+ # Clears the list.
132
+ def clear
133
+ @anchor = Node.create_anchor
134
+ end
135
+
136
+ # Removes the top node from the list and returns its value.
137
+ def pop
138
+ return nil if empty?
139
+ @anchor.prev_node.delete
140
+ end
141
+
142
+ # Deletes the node with the given value from the list and returns its value.
143
+ def delete(value)
144
+ find_node(value)&.delete
145
+ end
146
+
147
+ def inspect #:nodoc:
148
+ "#<#{self.class.name}:0x#{object_id.to_s(16).rjust(0.size * 2, '0')} #{to_a}>"
149
+ end
150
+
151
+ end
152
+
153
+ end
154
+ end
@@ -0,0 +1,16 @@
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
+ # The version of Geom2D
14
+ VERSION = '0.1.0'
15
+
16
+ end
@@ -0,0 +1,229 @@
1
+ # -*- frozen_string_literal: true -*-
2
+
3
+ require 'test_helper'
4
+ require 'geom2d/algorithms/polygon_operation'
5
+
6
+ describe Geom2D::Algorithms::PolygonOperation::SweepEvent do
7
+ before do
8
+ @klass = Geom2D::Algorithms::PolygonOperation::SweepEvent
9
+ @left = @klass.new(true, Geom2D::Point(5, 3), :subject)
10
+ @right = @klass.new(false, Geom2D::Point(8, 5), :subject, other_event: @left)
11
+ @left.other_event = @right
12
+ end
13
+
14
+ describe "below?/above?" do
15
+ before do
16
+ @point_above = Geom2D::Point(6, 5)
17
+ @point_below = Geom2D::Point(6, 3)
18
+ end
19
+
20
+ it "works for the left endpoint event" do
21
+ assert(@left.below?(@point_above))
22
+ assert(@left.above?(@point_below))
23
+ end
24
+
25
+ it "works for the right endpoint event" do
26
+ assert(@right.below?(@point_above))
27
+ assert(@right.above?(@point_below))
28
+ end
29
+ end
30
+
31
+ it "detects vertical segments" do
32
+ assert(@klass.new(false, Geom2D::Point(5, 8), :subject, other_event: @left).vertical?)
33
+ end
34
+ end
35
+
36
+ describe Geom2D::Algorithms::PolygonOperation do
37
+ def assert_op(result, op, subject = @ps, clipping = @qs)
38
+ alg_result = Geom2D::Algorithms::PolygonOperation.run(subject, clipping, op)
39
+ result_segments = result.each_segment.to_a
40
+ alg_result_segments = alg_result.each_segment.to_a
41
+ assert_equal(result_segments.size, alg_result_segments.size)
42
+ 0.upto(result_segments.size - 1) do |i|
43
+ assert_equal(result_segments[i], alg_result_segments[i])
44
+ end
45
+ end
46
+
47
+ it "doesn't work if edges of the same polygon overlap" do
48
+ ps = Geom2D::PolygonSet.new([Geom2D::Polygon([0, 0], [5, 0], [5, 5], [0, 5], [0, 0], [2, 0])])
49
+ assert_raises(RuntimeError) { assert_op(ps, :union, ps, ps) }
50
+ end
51
+
52
+ describe "trivial operation" do
53
+ before do
54
+ @p = Geom2D::Polygon([0, 0], [10, 0], [10, 10], [0, 10])
55
+ @set = Geom2D::PolygonSet.new([@p])
56
+ @empty = Geom2D::PolygonSet.new
57
+ end
58
+
59
+ it "subject is empty" do
60
+ assert_op(@set, :union, @empty, @set)
61
+ assert_op(@empty, :intersection, @empty, @set)
62
+ assert_op(@set, :xor, @empty, @set)
63
+ assert_op(@empty, :difference, @empty, @set)
64
+ end
65
+
66
+ it "clipping is empty" do
67
+ assert_op(@set, :union, @set, @empty)
68
+ assert_op(@empty, :intersection, @set, @empty)
69
+ assert_op(@set, :xor, @set, @empty)
70
+ assert_op(@set, :difference, @set, @empty)
71
+ end
72
+
73
+ it "bounding boxes are not intersecting" do
74
+ set2 = Geom2D::PolygonSet(Geom2D::Polygon([15, 15], [20, 15], [20, 20], [15, 20]))
75
+ assert_op(@set + set2, :union, @set, set2)
76
+ assert_op(@empty, :intersection, @set, set2)
77
+ assert_op(@set + set2, :xor, @set, set2)
78
+ assert_op(@set, :difference, @set, set2)
79
+ end
80
+ end
81
+
82
+ # +-----+
83
+ # |Qout |
84
+ # | |
85
+ # +----+-----+-----+
86
+ # | |Qin,P| |
87
+ # | | | |
88
+ # | +-----+ |
89
+ # | |
90
+ # | P |
91
+ # +----------------+
92
+ describe "complete overlapping edges" do
93
+ before do
94
+ @p = Geom2D::Polygon([0, 0], [10, 0], [10, 10], [0, 10], [0, 0])
95
+ @ps = Geom2D::PolygonSet.new([@p])
96
+ @q_in = Geom2D::Polygon([3, 10], [3, 5], [8, 5], [8, 10])
97
+ @qs_in = Geom2D::PolygonSet.new([@q_in])
98
+ @q_out = Geom2D::Polygon([3, 10], [3, 15], [8, 15], [8, 10])
99
+ @qs_out = Geom2D::PolygonSet.new([@q_out])
100
+ end
101
+
102
+ it "union" do
103
+ @qs = @qs_in
104
+ result = Geom2D::Polygon([0, 0], [10, 0], [10, 10], [0, 10])
105
+ assert_op(result, :union)
106
+
107
+ @qs = @qs_out
108
+ result = Geom2D::Polygon([0, 0], [10, 0], [10, 10], [8, 10], [8, 15], [3, 15], [3, 10], [0, 10])
109
+ assert_op(result, :union)
110
+ end
111
+
112
+ it "intersection" do
113
+ @qs = @qs_in
114
+ result = Geom2D::Polygon([3, 5], [8, 5], [8, 10], [3, 10])
115
+ assert_op(result, :intersection)
116
+
117
+ @qs = @qs_out
118
+ result = Geom2D::Polygon()
119
+ assert_op(result, :intersection)
120
+ end
121
+
122
+ it "xor" do
123
+ @qs = @qs_in
124
+ result = Geom2D::Polygon([0, 0], [10, 0], [10, 10], [8, 10], [8, 5], [3, 5], [3, 10], [0, 10])
125
+ assert_op(result, :xor)
126
+
127
+ @qs = @qs_out
128
+ result = Geom2D::Polygon([0, 0], [10, 0], [10, 10], [8, 10], [8, 15], [3, 15], [3, 10], [0, 10])
129
+ assert_op(result, :xor)
130
+ end
131
+
132
+ it "difference" do
133
+ @qs = @qs_in
134
+ result = Geom2D::Polygon([0, 0], [10, 0], [10, 10], [8, 10], [8, 5], [3, 5], [3, 10], [0, 10])
135
+ assert_op(result, :difference)
136
+
137
+ @qs = @qs_out
138
+ result = Geom2D::Polygon([0, 0], [10, 0], [10, 10], [0, 10])
139
+ assert_op(result, :difference)
140
+ end
141
+ end
142
+
143
+ # +-----+--------------+-----+
144
+ # \ Q / \ P /
145
+ # \ / \ /
146
+ # X P,Q X
147
+ # / \ / \
148
+ # / P \ / Q \
149
+ # +-----+--------------+-----+
150
+ describe "partial overlapping edges" do
151
+ before do
152
+ @p = Geom2D::Polygon([0, 0], [10, 0], [15, 10], [5, 10])
153
+ @ps = Geom2D::PolygonSet.new([@p])
154
+ @q = Geom2D::Polygon([0, 10], [5, 0], [15, 0], [10, 10])
155
+ @qs = Geom2D::PolygonSet.new([@q])
156
+ end
157
+
158
+ it "union" do
159
+ result = Geom2D::Polygon([0, 0], [15, 0], [12.5, 5], [15, 10], [0, 10], [2.5, 5])
160
+ assert_op(result, :union)
161
+ end
162
+
163
+ it "intersection" do
164
+ result = Geom2D::Polygon([2.5, 5], [5, 0], [10, 0], [12.5, 5], [10, 10], [5, 10])
165
+ assert_op(result, :intersection)
166
+ end
167
+
168
+ it "xor" do
169
+ left = Geom2D::Polygon([0, 0], [5, 0], [2.5, 5], [5, 10], [0, 10], [2.5, 5])
170
+ right = Geom2D::Polygon([10, 0], [15, 0], [12.5, 5], [15, 10], [10, 10], [12.5, 5])
171
+ assert_op(Geom2D::PolygonSet(left, right), :xor)
172
+ end
173
+
174
+ it "difference" do
175
+ left = Geom2D::Polygon([0, 0], [5, 0], [2.5, 5])
176
+ right = Geom2D::Polygon([10, 10], [12.5, 5], [15, 10])
177
+ assert_op(Geom2D::PolygonSet(left, right), :difference)
178
+ end
179
+ end
180
+
181
+ # +------+------+
182
+ # | Q | |
183
+ # | | |
184
+ # +------+ |
185
+ # | |
186
+ # | P |
187
+ # +-------------+
188
+ describe "overlapping corner" do
189
+ before do
190
+ @p = Geom2D::Polygon([0, 0], [10, 0], [10, 10], [0, 10])
191
+ @ps = Geom2D::PolygonSet.new([@p])
192
+ @q = Geom2D::Polygon([0, 10], [5, 10], [5, 5], [0, 5])
193
+ @qs = Geom2D::PolygonSet.new([@q])
194
+ end
195
+
196
+ it "union" do
197
+ result = Geom2D::Polygon([0, 0], [10, 0], [10, 10], [0, 10])
198
+ assert_op(result, :union)
199
+ end
200
+
201
+ it "intersection" do
202
+ result = Geom2D::Polygon([0, 5], [5, 5], [5, 10], [0, 10])
203
+ assert_op(result, :intersection)
204
+ end
205
+
206
+ it "difference" do
207
+ result = Geom2D::Polygon([0, 0], [10, 0], [10, 10], [5, 10], [5, 5], [0, 5])
208
+ assert_op(result, :difference)
209
+ end
210
+ end
211
+
212
+ describe "self-intersecting polygon" do
213
+ it "intersection" do
214
+ p1 = Geom2D::Polygon([0, 20], [0, 2], [15, -1], [15, 18])
215
+ @ps = Geom2D::PolygonSet.new([p1])
216
+ q1 = Geom2D::Polygon([1, 11], [13, 17], [13, 11], [1, 17])
217
+ q2 = Geom2D::Polygon([2, 3], [11, 3], [11, 9])
218
+ q3 = Geom2D::Polygon([3, 8], [3, 5], [8, 5])
219
+ @qs = Geom2D::PolygonSet.new([q1, q2, q3])
220
+ result = Geom2D::PolygonSet(
221
+ Geom2D::Polygon([1, 11], [7, 14], [1, 17]),
222
+ Geom2D::Polygon([2, 3], [11, 3], [11, 9], [6.421052631578, 5.947368421052], [8, 5], [5, 5],
223
+ [6.421052631578, 5.947368421052], [3, 8], [3, 5], [5, 5]),
224
+ Geom2D::Polygon([7, 14], [13, 11], [13, 17])
225
+ )
226
+ assert_op(result, :intersection)
227
+ end
228
+ end
229
+ end
@@ -0,0 +1,26 @@
1
+ # -*- frozen_string_literal: true -*-
2
+
3
+ require 'test_helper'
4
+ require 'geom2d/algorithms'
5
+ require 'geom2d/point'
6
+
7
+ describe Geom2D::Algorithms do
8
+ describe "ccw" do
9
+ before do
10
+ @p1 = Geom2D::Point(1, 2)
11
+ @p2 = Geom2D::Point(3, 4)
12
+ end
13
+
14
+ it "returns 1 for counterclockwise turn" do
15
+ assert_equal(1, Geom2D::Algorithms.ccw(@p1, @p2, Geom2D::Point(0, 3)))
16
+ end
17
+
18
+ it "returns -1 for clockwise turn" do
19
+ assert_equal(-1, Geom2D::Algorithms.ccw(@p1, @p2, Geom2D::Point(2, 0)))
20
+ end
21
+
22
+ it "returns 0 for collinear points" do
23
+ assert_equal(0, Geom2D::Algorithms.ccw(@p1, @p2, @p2 + (@p2 - @p1)))
24
+ end
25
+ end
26
+ end