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.
- checksums.yaml +7 -0
- data/CONTRIBUTERS +3 -0
- data/LICENSE +21 -0
- data/README.md +49 -0
- data/Rakefile +101 -0
- data/VERSION +1 -0
- data/lib/geom2d.rb +70 -0
- data/lib/geom2d/algorithms.rb +35 -0
- data/lib/geom2d/algorithms/polygon_operation.rb +435 -0
- data/lib/geom2d/bounding_box.rb +84 -0
- data/lib/geom2d/point.rb +145 -0
- data/lib/geom2d/polygon.rb +108 -0
- data/lib/geom2d/polygon_set.rb +67 -0
- data/lib/geom2d/segment.rb +202 -0
- data/lib/geom2d/utils.rb +38 -0
- data/lib/geom2d/utils/sorted_linked_list.rb +154 -0
- data/lib/geom2d/version.rb +16 -0
- data/test/geom2d/algorithms/test_polygon_operation.rb +229 -0
- data/test/geom2d/test_algorithms.rb +26 -0
- data/test/geom2d/test_bounding_box.rb +37 -0
- data/test/geom2d/test_point.rb +148 -0
- data/test/geom2d/test_polygon.rb +69 -0
- data/test/geom2d/test_polygon_set.rb +41 -0
- data/test/geom2d/test_segment.rb +253 -0
- data/test/geom2d/utils/test_sorted_linked_list.rb +72 -0
- data/test/test_helper.rb +15 -0
- metadata +68 -0
data/lib/geom2d/utils.rb
ADDED
@@ -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
|