geom2d 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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