geom2d 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +15 -0
- data/CONTRIBUTERS +1 -1
- data/Rakefile +2 -0
- data/VERSION +1 -1
- data/lib/geom2d.rb +4 -4
- data/lib/geom2d/algorithms/polygon_operation.rb +12 -19
- data/lib/geom2d/polygon.rb +3 -5
- data/lib/geom2d/segment.rb +18 -13
- data/lib/geom2d/utils.rb +1 -1
- data/lib/geom2d/utils/sorted_list.rb +86 -0
- data/lib/geom2d/version.rb +1 -1
- data/test/geom2d/algorithms/test_polygon_operation.rb +2 -2
- data/test/geom2d/test_polygon.rb +2 -0
- data/test/geom2d/utils/{test_sorted_linked_list.rb → test_sorted_list.rb} +18 -22
- metadata +26 -5
- data/lib/geom2d/utils/sorted_linked_list.rb +0 -154
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ef94841dc1fefba33e2abbd4303cea33d3d1cfd4db5727a828300b12e7e405d6
|
4
|
+
data.tar.gz: d65cf9ae9017e07c0faf92cccd21cc918d323c31479bfc2f3a99b0a65162d33b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 18dfb48248517e32415185c17e1bc6bf5eab076a6ae43d5302dbc972c88c1da0bb00c70ac1889dcc7d9f8b3ed3596a485e89e4ca4b4b7168b348914ee16b9143
|
7
|
+
data.tar.gz: 93ed02cc3c1037a04bfbe97274c0acc65f2f1ada3417f3de8b47b517664069793757df8a7e6b1ea2cd455a6b569f36ef1aabfed67c39bd07140ace59901fc2b0
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
## 0.2.0 - Unreleased
|
2
|
+
|
3
|
+
### Changed
|
4
|
+
|
5
|
+
* Make Segment#intersect ~1.71x faster by avoiding unnecessary object creation
|
6
|
+
* Refactor and simplify the sorted list implementation used by the polygon
|
7
|
+
operations, making the latter ~1.15x faster
|
8
|
+
|
9
|
+
### Fixed
|
10
|
+
|
11
|
+
* Fix off-by-one error in Polygon#ccw? calculation
|
12
|
+
|
13
|
+
## 0.1.0 - 2018-03-28
|
14
|
+
|
15
|
+
* Initial release
|
data/CONTRIBUTERS
CHANGED
data/Rakefile
CHANGED
@@ -21,6 +21,7 @@ namespace :dev do
|
|
21
21
|
'lib/**/*.rb',
|
22
22
|
'test/**/*',
|
23
23
|
'Rakefile',
|
24
|
+
'CHANGELOG.md',
|
24
25
|
'LICENSE',
|
25
26
|
'VERSION',
|
26
27
|
'CONTRIBUTERS',
|
@@ -51,6 +52,7 @@ namespace :dev do
|
|
51
52
|
|
52
53
|
s.require_path = 'lib'
|
53
54
|
s.required_ruby_version = '>= 2.4'
|
55
|
+
s.add_development_dependency('rubocop', '~> 0.58', '>= 0.58.2')
|
54
56
|
|
55
57
|
s.author = 'Thomas Leitner'
|
56
58
|
s.email = 't_leitner@gmx.at'
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.
|
1
|
+
0.2.0
|
data/lib/geom2d.rb
CHANGED
@@ -29,7 +29,7 @@ module Geom2D
|
|
29
29
|
# Creates a new Point object from the given coordinates.
|
30
30
|
#
|
31
31
|
# See: Point.new
|
32
|
-
def self
|
32
|
+
def self.Point(x, y = nil)
|
33
33
|
if x.kind_of?(Point)
|
34
34
|
x
|
35
35
|
elsif y
|
@@ -43,7 +43,7 @@ module Geom2D
|
|
43
43
|
# +start_point+ to +start_point+ + +vector+.
|
44
44
|
#
|
45
45
|
# See: Segment.new
|
46
|
-
def self
|
46
|
+
def self.Segment(start_point, end_point = nil, vector: nil)
|
47
47
|
if end_point
|
48
48
|
Segment.new(start_point, end_point)
|
49
49
|
elsif vector
|
@@ -56,14 +56,14 @@ module Geom2D
|
|
56
56
|
# Creates a new Polygon object from the given vertices.
|
57
57
|
#
|
58
58
|
# See: Polygon.new
|
59
|
-
def self
|
59
|
+
def self.Polygon(*vertices)
|
60
60
|
Polygon.new(vertices)
|
61
61
|
end
|
62
62
|
|
63
63
|
# Creates a PolygonSet from the given array of Polygon instances.
|
64
64
|
#
|
65
65
|
# See: PolygonSet.new
|
66
|
-
def self
|
66
|
+
def self.PolygonSet(*polygons)
|
67
67
|
PolygonSet.new(polygons)
|
68
68
|
end
|
69
69
|
|
@@ -66,12 +66,12 @@ module Geom2D
|
|
66
66
|
attr_accessor :prev_in_result
|
67
67
|
|
68
68
|
# Creates a new SweepEvent.
|
69
|
-
def initialize(left, point, polygon_type, other_event
|
69
|
+
def initialize(left, point, polygon_type, other_event = nil)
|
70
70
|
@left = left
|
71
71
|
@point = point
|
72
72
|
@other_event = other_event
|
73
73
|
@polygon_type = polygon_type
|
74
|
-
@edge_type =
|
74
|
+
@edge_type = :normal
|
75
75
|
end
|
76
76
|
|
77
77
|
# Returns +true+ if this event's line #segment is below the point +p+.
|
@@ -184,9 +184,9 @@ module Geom2D
|
|
184
184
|
@operation = operation
|
185
185
|
|
186
186
|
@result = PolygonSet.new
|
187
|
-
@event_queue = Utils::
|
187
|
+
@event_queue = Utils::SortedList.new {|a, b| a.process_after?(b) }
|
188
188
|
# @sweep_line should really be a sorted data structure with O(log(n)) for insert/search!
|
189
|
-
@sweep_line = Utils::
|
189
|
+
@sweep_line = Utils::SortedList.new {|a, b| a.segment_below?(b) }
|
190
190
|
@sorted_events = []
|
191
191
|
end
|
192
192
|
|
@@ -212,9 +212,7 @@ module Geom2D
|
|
212
212
|
|
213
213
|
@event_queue.pop
|
214
214
|
if event.left # the segment hast to be inserted into status line
|
215
|
-
|
216
|
-
prev_event = (node.prev_node.anchor? ? nil : node.prev_node.value)
|
217
|
-
next_event = (node.next_node.anchor? ? nil : node.next_node.value)
|
215
|
+
prevprev_event, prev_event, next_event = @sweep_line.insert(event)
|
218
216
|
|
219
217
|
compute_event_fields(event, prev_event)
|
220
218
|
if next_event && possible_intersection(event, next_event) == 2
|
@@ -222,19 +220,14 @@ module Geom2D
|
|
222
220
|
compute_event_fields(next_event, event)
|
223
221
|
end
|
224
222
|
if prev_event && possible_intersection(prev_event, event) == 2
|
225
|
-
|
226
|
-
compute_event_fields(prev_event, prevprev_ev)
|
223
|
+
compute_event_fields(prev_event, prevprev_event)
|
227
224
|
compute_event_fields(event, prev_event)
|
228
225
|
end
|
229
226
|
else # the segment has to be removed from the status line
|
230
227
|
event = event.other_event # use left event
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
prev_node = node.prev_node
|
235
|
-
node.delete
|
236
|
-
unless prev_node.anchor? || next_node.anchor?
|
237
|
-
possible_intersection(prev_node.value, next_node.value)
|
228
|
+
prev_ev, next_ev = @sweep_line.delete(event)
|
229
|
+
if prev_ev && next_ev
|
230
|
+
possible_intersection(prev_ev, next_ev)
|
238
231
|
end
|
239
232
|
end
|
240
233
|
end
|
@@ -271,7 +264,7 @@ module Geom2D
|
|
271
264
|
return if segment.degenerate?
|
272
265
|
start_point_is_left = (segment.start_point == segment.min)
|
273
266
|
e1 = SweepEvent.new(start_point_is_left, segment.start_point, polygon_type)
|
274
|
-
e2 = SweepEvent.new(!start_point_is_left, segment.end_point, polygon_type,
|
267
|
+
e2 = SweepEvent.new(!start_point_is_left, segment.end_point, polygon_type, e1)
|
275
268
|
e1.other_event = e2
|
276
269
|
@event_queue.push(e1).push(e2)
|
277
270
|
end
|
@@ -363,8 +356,8 @@ module Geom2D
|
|
363
356
|
# Divides the event's segment at the given point (which has to be inside the segment) and adds
|
364
357
|
# the resulting events to the event queue.
|
365
358
|
def divide_segment(event, point)
|
366
|
-
right = SweepEvent.new(false, point, event.polygon_type,
|
367
|
-
left = SweepEvent.new(true, point, event.polygon_type,
|
359
|
+
right = SweepEvent.new(false, point, event.polygon_type, event)
|
360
|
+
left = SweepEvent.new(true, point, event.polygon_type, event.other_event)
|
368
361
|
event.other_event.other_event = left
|
369
362
|
event.other_event = right
|
370
363
|
@event_queue.push(left).push(right)
|
data/lib/geom2d/polygon.rb
CHANGED
@@ -64,10 +64,8 @@ module Geom2D
|
|
64
64
|
return to_enum(__method__) unless block_given?
|
65
65
|
return unless @vertices.size > 1
|
66
66
|
|
67
|
-
|
68
|
-
|
69
|
-
end
|
70
|
-
yield(Geom2D::Segment(@vertices[-1], @vertices[0]))
|
67
|
+
@vertices.each_cons(2) {|v1, v2| yield(Geom2D::Segment.new(v1, v2)) }
|
68
|
+
yield(Geom2D::Segment.new(@vertices[-1], @vertices[0]))
|
71
69
|
end
|
72
70
|
|
73
71
|
# Returns the BoundingBox of this polygon, or an empty BoundingBox if the polygon has no
|
@@ -83,7 +81,7 @@ module Geom2D
|
|
83
81
|
def ccw?
|
84
82
|
return true if @vertices.empty?
|
85
83
|
area = @vertices[-1].wedge(@vertices[0])
|
86
|
-
|
84
|
+
0.upto(@vertices.size - 2) {|i| area += @vertices[i].wedge(@vertices[i + 1]) }
|
87
85
|
area >= 0
|
88
86
|
end
|
89
87
|
|
data/lib/geom2d/segment.rb
CHANGED
@@ -110,27 +110,32 @@ module Geom2D
|
|
110
110
|
def intersect(segment)
|
111
111
|
p0 = start_point
|
112
112
|
p1 = segment.start_point
|
113
|
-
|
114
|
-
|
115
|
-
|
113
|
+
d0x = end_point.x - start_point.x
|
114
|
+
d0y = end_point.y - start_point.y
|
115
|
+
d1x = segment.end_point.x - segment.start_point.x
|
116
|
+
d1y = segment.end_point.y - segment.start_point.y
|
117
|
+
ex = p1.x - p0.x
|
118
|
+
ey = p1.y - p0.y
|
116
119
|
|
117
|
-
cross =
|
120
|
+
cross = (d0x * d1y - d1x * d0y).to_f # cross product of direction vectors
|
118
121
|
|
119
122
|
if cross.abs > Utils.precision # segments are not parallel
|
120
|
-
s =
|
123
|
+
s = (ex * d1y - d1x * ey) / cross
|
121
124
|
return nil if s < 0 || s > 1
|
122
|
-
t =
|
125
|
+
t = (ex * d0y - d0x * ey) / cross
|
123
126
|
return nil if t < 0 || t > 1
|
124
127
|
|
125
|
-
result = p0 +
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
128
|
+
result = p0 + Point.new(s * d0x, s * d0y)
|
129
|
+
return case result
|
130
|
+
when start_point then start_point
|
131
|
+
when end_point then end_point
|
132
|
+
when segment.start_point then segment.start_point
|
133
|
+
when segment.end_point then segment.end_point
|
134
|
+
else result
|
135
|
+
end
|
131
136
|
end
|
132
137
|
|
133
|
-
return nil if
|
138
|
+
return nil if (ex * d0y - d0x * ey).abs > Utils.precision # non-intersecting parallel segment lines
|
134
139
|
|
135
140
|
e0 = end_point
|
136
141
|
e1 = segment.end_point
|
data/lib/geom2d/utils.rb
CHANGED
@@ -13,7 +13,7 @@ module Geom2D
|
|
13
13
|
# Contains utility methods and classes.
|
14
14
|
module Utils
|
15
15
|
|
16
|
-
autoload(:
|
16
|
+
autoload(:SortedList, 'geom2d/utils/sorted_list')
|
17
17
|
|
18
18
|
# The precision when comparing two floats, defaults to 1e-10.
|
19
19
|
singleton_class.attr_accessor :precision
|
@@ -0,0 +1,86 @@
|
|
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 list that keeps its items sorted. Currently only used by
|
15
|
+
# Geom2D::Algorithms::PolygonOperation and therefore with special methods for the latter.
|
16
|
+
class SortedList
|
17
|
+
|
18
|
+
include Enumerable
|
19
|
+
|
20
|
+
# Creates a new SortedList using the +comparator+ or the given block as compare function.
|
21
|
+
#
|
22
|
+
# The comparator has to respond to +call(a, b)+ where +a+ is the value to be inserted and +b+
|
23
|
+
# is the value to which it is compared. The return value should be +true+ if the value +a+
|
24
|
+
# should be inserted before +b+, i.e. at the position of +b+.
|
25
|
+
def initialize(comparator = nil, &block)
|
26
|
+
@list = []
|
27
|
+
@comparator = comparator || block
|
28
|
+
end
|
29
|
+
|
30
|
+
# Returns +true+ if the list is empty?
|
31
|
+
def empty?
|
32
|
+
@list.empty?
|
33
|
+
end
|
34
|
+
|
35
|
+
# Returns the last value in the list.
|
36
|
+
def last
|
37
|
+
@list.last
|
38
|
+
end
|
39
|
+
|
40
|
+
# Yields each value in sorted order.
|
41
|
+
#
|
42
|
+
# If no block is given, an enumerator is returned.
|
43
|
+
def each(&block) #:yield: value
|
44
|
+
@list.each(&block)
|
45
|
+
end
|
46
|
+
|
47
|
+
# Inserts the value and returns self.
|
48
|
+
def push(value)
|
49
|
+
insert(value)
|
50
|
+
self
|
51
|
+
end
|
52
|
+
|
53
|
+
# Inserts a new value into the list (at a position decided by the compare function) and
|
54
|
+
# returns the previous-previous, previous and next values.
|
55
|
+
def insert(value)
|
56
|
+
i = @list.bsearch_index {|el| @comparator.call(value, el) } || @list.size
|
57
|
+
@list.insert(i, value)
|
58
|
+
[(i <= 1 ? nil : @list[i - 2]), (i == 0 ? nil : @list[i - 1]), @list[i + 1]]
|
59
|
+
end
|
60
|
+
|
61
|
+
# Deletes the given value and returns the previous and next values.
|
62
|
+
def delete(value)
|
63
|
+
i = @list.index(value)
|
64
|
+
result = [(i == 0 ? nil : @list[i - 1]), @list[i + 1]]
|
65
|
+
@list.delete_at(i)
|
66
|
+
result
|
67
|
+
end
|
68
|
+
|
69
|
+
# Clears the list.
|
70
|
+
def clear
|
71
|
+
@list.clear
|
72
|
+
end
|
73
|
+
|
74
|
+
# Removes the top value from the list and returns it.
|
75
|
+
def pop
|
76
|
+
@list.pop
|
77
|
+
end
|
78
|
+
|
79
|
+
def inspect #:nodoc:
|
80
|
+
"#<#{self.class.name}:0x#{object_id.to_s(16).rjust(0.size * 2, '0')} #{to_a}>"
|
81
|
+
end
|
82
|
+
|
83
|
+
end
|
84
|
+
|
85
|
+
end
|
86
|
+
end
|
data/lib/geom2d/version.rb
CHANGED
@@ -7,7 +7,7 @@ describe Geom2D::Algorithms::PolygonOperation::SweepEvent do
|
|
7
7
|
before do
|
8
8
|
@klass = Geom2D::Algorithms::PolygonOperation::SweepEvent
|
9
9
|
@left = @klass.new(true, Geom2D::Point(5, 3), :subject)
|
10
|
-
@right = @klass.new(false, Geom2D::Point(8, 5), :subject,
|
10
|
+
@right = @klass.new(false, Geom2D::Point(8, 5), :subject, @left)
|
11
11
|
@left.other_event = @right
|
12
12
|
end
|
13
13
|
|
@@ -29,7 +29,7 @@ describe Geom2D::Algorithms::PolygonOperation::SweepEvent do
|
|
29
29
|
end
|
30
30
|
|
31
31
|
it "detects vertical segments" do
|
32
|
-
assert(@klass.new(false, Geom2D::Point(5, 8), :subject,
|
32
|
+
assert(@klass.new(false, Geom2D::Point(5, 8), :subject, @left).vertical?)
|
33
33
|
end
|
34
34
|
end
|
35
35
|
|
data/test/geom2d/test_polygon.rb
CHANGED
@@ -56,6 +56,8 @@ describe Geom2D::Polygon do
|
|
56
56
|
assert(@polygon.ccw?)
|
57
57
|
@polygon.reverse!
|
58
58
|
refute(@polygon.ccw?)
|
59
|
+
refute(Geom2D::Polygon([2, 2], [20, 5], [10, 3.0]).ccw?)
|
60
|
+
assert(Geom2D::Polygon([2, 2], [20, 5], [10, 3.4]).ccw?)
|
59
61
|
end
|
60
62
|
|
61
63
|
it "reverses the vertex list" do
|
@@ -1,29 +1,38 @@
|
|
1
1
|
# -*- frozen_string_literal: true -*-
|
2
2
|
|
3
3
|
require 'test_helper'
|
4
|
-
require 'geom2d/utils/
|
4
|
+
require 'geom2d/utils/sorted_list'
|
5
5
|
|
6
|
-
describe Geom2D::Utils::
|
6
|
+
describe Geom2D::Utils::SortedList do
|
7
7
|
before do
|
8
|
-
@list = Geom2D::Utils::
|
8
|
+
@list = Geom2D::Utils::SortedList.new {|a, b| a < b }
|
9
9
|
end
|
10
10
|
|
11
11
|
describe "insert" do
|
12
|
-
it "inserts a value and returns
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
12
|
+
it "inserts a value and returns surrounding values" do
|
13
|
+
ppv, pv, nv = @list.insert(1)
|
14
|
+
assert_nil(ppv)
|
15
|
+
assert_nil(pv)
|
16
|
+
assert_nil(nv)
|
17
17
|
end
|
18
18
|
|
19
19
|
it "uses the comparator for choosing the place to insert the value" do
|
20
20
|
@list.insert(10)
|
21
21
|
@list.insert(5)
|
22
22
|
@list.insert(8)
|
23
|
-
|
23
|
+
ppv, pv, nv = @list.insert(9)
|
24
|
+
assert_equal([5, 8, 10], [ppv, pv, nv])
|
25
|
+
assert_equal([5, 8, 9, 10], @list.to_a)
|
24
26
|
end
|
25
27
|
end
|
26
28
|
|
29
|
+
it "deletes a value from the list and returns the neighbouring values" do
|
30
|
+
@list.push(8).push(5).push(3).push(4).push(6)
|
31
|
+
assert_equal([6, nil], @list.delete(8))
|
32
|
+
assert_equal([nil, 4], @list.delete(3))
|
33
|
+
assert_equal([4, 6], @list.delete(5))
|
34
|
+
end
|
35
|
+
|
27
36
|
it "returns whether it is empty" do
|
28
37
|
assert(@list.empty?)
|
29
38
|
@list.insert(5)
|
@@ -41,14 +50,6 @@ describe Geom2D::Utils::SortedLinkedList do
|
|
41
50
|
assert_equal(10, @list.last)
|
42
51
|
end
|
43
52
|
|
44
|
-
it "returns the first node found for a given value" do
|
45
|
-
@list.push(5).push(8)
|
46
|
-
node = @list.find_node(8)
|
47
|
-
assert_equal(8, node.value)
|
48
|
-
assert_equal(5, node.prev_node.value)
|
49
|
-
assert(node.next_node.anchor?)
|
50
|
-
end
|
51
|
-
|
52
53
|
it "pops the top value of the list" do
|
53
54
|
@list.push(8).push(5)
|
54
55
|
assert_equal(8, @list.pop)
|
@@ -60,11 +61,6 @@ describe Geom2D::Utils::SortedLinkedList do
|
|
60
61
|
assert(@list.empty?)
|
61
62
|
end
|
62
63
|
|
63
|
-
it "deletes a value from the list" do
|
64
|
-
@list.push(8).push(5)
|
65
|
-
assert_equal(8, @list.delete(8))
|
66
|
-
end
|
67
|
-
|
68
64
|
it "can be inspected" do
|
69
65
|
@list.push(8).push(5)
|
70
66
|
assert_match(/[5, 8]/, @list.inspect)
|
metadata
CHANGED
@@ -1,21 +1,42 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: geom2d
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Thomas Leitner
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2018-
|
12
|
-
dependencies:
|
11
|
+
date: 2018-12-16 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rubocop
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0.58'
|
20
|
+
- - ">="
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: 0.58.2
|
23
|
+
type: :development
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
26
|
+
requirements:
|
27
|
+
- - "~>"
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '0.58'
|
30
|
+
- - ">="
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: 0.58.2
|
13
33
|
description:
|
14
34
|
email: t_leitner@gmx.at
|
15
35
|
executables: []
|
16
36
|
extensions: []
|
17
37
|
extra_rdoc_files: []
|
18
38
|
files:
|
39
|
+
- CHANGELOG.md
|
19
40
|
- CONTRIBUTERS
|
20
41
|
- LICENSE
|
21
42
|
- README.md
|
@@ -30,7 +51,7 @@ files:
|
|
30
51
|
- lib/geom2d/polygon_set.rb
|
31
52
|
- lib/geom2d/segment.rb
|
32
53
|
- lib/geom2d/utils.rb
|
33
|
-
- lib/geom2d/utils/
|
54
|
+
- lib/geom2d/utils/sorted_list.rb
|
34
55
|
- lib/geom2d/version.rb
|
35
56
|
- test/geom2d/algorithms/test_polygon_operation.rb
|
36
57
|
- test/geom2d/test_algorithms.rb
|
@@ -39,7 +60,7 @@ files:
|
|
39
60
|
- test/geom2d/test_polygon.rb
|
40
61
|
- test/geom2d/test_polygon_set.rb
|
41
62
|
- test/geom2d/test_segment.rb
|
42
|
-
- test/geom2d/utils/
|
63
|
+
- test/geom2d/utils/test_sorted_list.rb
|
43
64
|
- test/test_helper.rb
|
44
65
|
homepage: https://geom2d.gettalong.org
|
45
66
|
licenses:
|
@@ -1,154 +0,0 @@
|
|
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
|