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,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 3edb20280c7781756b3e384e9882aea7c3a98ef0803a33c801cf6580a9d4e3ef
4
+ data.tar.gz: f1f8dc432c32de394ef313536b1faac5e7719f88ef68d1138c2abc7aec56309a
5
+ SHA512:
6
+ metadata.gz: 429eae46087970b5624c6cefad3f907a44d812093be921f831675cb92cb17901f7efc1c825d844137684156b16e7d9479957d4edd9310242394a448415f21e66
7
+ data.tar.gz: 3302aa1ebe48790c07c72d6c8882bd6fae780596bf641e0ad4b82852ed76370f6f0599d93ab92155033ed657548fa837539a3d74a3df4a28ccca8cbd314f9ae8
@@ -0,0 +1,3 @@
1
+ Count Name
2
+ ======= ====
3
+ 8 Thomas Leitner <t_leitner@gmx.at>
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ geom2d - 2D Geometry Objects and Algorithms
2
+ Copyright (C) 2018 Thomas Leitner <t_leitner@gmx.at>
3
+
4
+ Permission is hereby granted, free of charge, to any person obtaining a
5
+ copy of this software and associated documentation files (the
6
+ "Software"), to deal in the Software without restriction, including
7
+ without limitation the rights to use, copy, modify, merge, publish,
8
+ distribute, sublicense, and/or sell copies of the Software, and to
9
+ permit persons to whom the Software is furnished to do so, subject to
10
+ the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included
13
+ in all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
16
+ OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
18
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
19
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
20
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
21
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,49 @@
1
+ # Geom2D - Objects and Algorithms for 2D Geometry in Ruby
2
+
3
+ This library implements objects for 2D geometry, like points, lines, line segments, arcs, curves and
4
+ so on, as well as algorithms for these objects, like line-line intersections and arc approximation
5
+ by Bézier curves.
6
+
7
+
8
+ ## License
9
+
10
+ Copyright (C) 2018 Thomas Leitner <t_leitner@gmx.at>, licensed under the MIT - see the **LICENSE**
11
+ file.
12
+
13
+
14
+ ## Features
15
+
16
+ * Objects
17
+ * Point
18
+ * Segment
19
+ * Polygon
20
+ * PolygonSet
21
+ * Polyline (TODO)
22
+ * Rectangle (TODO)
23
+ * QuadraticCurve (TODO)
24
+ * QubicCurve (TODO)
25
+ * Arc (TODO)
26
+ * Circle (TODO)
27
+ * Path (TODO)
28
+ * Algorithms
29
+ * Segment-Segment Intersection
30
+ * Boolean Operations on PolygonSets
31
+
32
+ ## Usage
33
+
34
+ ~~~ ruby
35
+ require 'geom2d'
36
+
37
+ # Point, can also be interpreted as vector
38
+ point1 = Geom2D::Point(2, 2)
39
+ point2 = Geom2D::Point([2, 2]) # arrays are fine but not as efficient
40
+ point3 = Geom2D::Point(point2) # copy constructor
41
+
42
+ # Segment defined by two points or a point and a vector
43
+ line1 = Geom2D::Segment(point1, point2)
44
+ line2 = Geom2D::Segment(point1, vector: point2)
45
+ line3 = Geom2D::Segment([3, 4], [9, 6]) # arrays are also possible
46
+
47
+ # Segment intersection
48
+ line1.intersect(line3) # => intersection_point
49
+ ~~~
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rake/testtask'
4
+ require 'rake/clean'
5
+ require 'rubygems/package_task'
6
+
7
+ $:.unshift('lib')
8
+ require 'geom2d/version'
9
+
10
+ Rake::TestTask.new do |t|
11
+ t.libs << 'test'
12
+ t.test_files = FileList['test/**/*.rb']
13
+ t.verbose = false
14
+ t.warning = true
15
+ end
16
+
17
+ namespace :dev do
18
+ PKG_FILES = FileList.new(
19
+ [
20
+ 'README.md',
21
+ 'lib/**/*.rb',
22
+ 'test/**/*',
23
+ 'Rakefile',
24
+ 'LICENSE',
25
+ 'VERSION',
26
+ 'CONTRIBUTERS',
27
+ ]
28
+ )
29
+
30
+ CLOBBER << "VERSION"
31
+ file 'VERSION' do
32
+ puts "Generating VERSION file"
33
+ File.open('VERSION', 'w+') {|file| file.write(Geom2D::VERSION + "\n") }
34
+ end
35
+
36
+ CLOBBER << 'CONTRIBUTERS'
37
+ file 'CONTRIBUTERS' do
38
+ puts "Generating CONTRIBUTERS file"
39
+ `echo " Count Name" > CONTRIBUTERS`
40
+ `echo "======= ====" >> CONTRIBUTERS`
41
+ `git log | grep ^Author: | sed 's/^Author: //' | sort | uniq -c | sort -nr >> CONTRIBUTERS`
42
+ end
43
+
44
+ spec = Gem::Specification.new do |s|
45
+ s.name = 'geom2d'
46
+ s.version = Geom2D::VERSION
47
+ s.summary = "Objects and Algorithms for 2D Geometry"
48
+ s.license = 'MIT'
49
+
50
+ s.files = PKG_FILES.to_a
51
+
52
+ s.require_path = 'lib'
53
+ s.required_ruby_version = '>= 2.4'
54
+
55
+ s.author = 'Thomas Leitner'
56
+ s.email = 't_leitner@gmx.at'
57
+ s.homepage = "https://geom2d.gettalong.org"
58
+ end
59
+
60
+ Gem::PackageTask.new(spec) do |pkg|
61
+ pkg.need_zip = true
62
+ pkg.need_tar = true
63
+ end
64
+
65
+ desc "Upload the release to Rubygems"
66
+ task publish_files: [:package] do
67
+ sh "gem push pkg/geom2d-#{Geom2D::VERSION}.gem"
68
+ puts 'done'
69
+ end
70
+
71
+ desc 'Release Geom2D version ' + Geom2D::VERSION
72
+ task release: [:clobber, :package, :publish_files]
73
+
74
+ desc "Insert/Update copyright notice"
75
+ task :update_copyright do
76
+ statement = <<~STATEMENT
77
+ #--
78
+ # geom2d - 2D Geometric Objects and Algorithms
79
+ # Copyright (C) 2018 Thomas Leitner <t_leitner@gmx.at>
80
+ #
81
+ # This software may be modified and distributed under the terms
82
+ # of the MIT license. See the LICENSE file for details.
83
+ #++
84
+ STATEMENT
85
+ state_re = /\A(#.*\n)*#{Regexp.escape(statement)}/
86
+ inserted = false
87
+ Dir["lib/**/*.rb"].each do |file|
88
+ unless File.read(file).match?(state_re)
89
+ inserted = true
90
+ puts "Updating file #{file}"
91
+ old = File.read(file)
92
+ old.sub!(/^#--.*?\n#\+\+\n|\A/m, statement)
93
+ File.write(file, old)
94
+ end
95
+ end
96
+ puts "Look through the above mentioned files and correct all problems" if inserted
97
+ end
98
+ end
99
+
100
+ task clobber: 'dev:clobber'
101
+ task default: 'test'
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.1.0
@@ -0,0 +1,70 @@
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
+ # = Geom2D - Objects and Algorithms for 2D Geometry in Ruby
12
+ #
13
+ # This library implements objects for 2D geometry, like points, line segments, arcs, curves and so
14
+ # on, as well as algorithms for these objects, like line-line intersections and arc approximation by
15
+ # Bezier curves.
16
+ module Geom2D
17
+
18
+ autoload(:Point, 'geom2d/point')
19
+ autoload(:Segment, 'geom2d/segment')
20
+ autoload(:Polygon, 'geom2d/polygon')
21
+ autoload(:PolygonSet, 'geom2d/polygon_set')
22
+
23
+ autoload(:BoundingBox, 'geom2d/bounding_box')
24
+ autoload(:Algorithms, 'geom2d/algorithms')
25
+
26
+ autoload(:Utils, 'geom2d/utils')
27
+ autoload(:VERSION, 'geom2d/version')
28
+
29
+ # Creates a new Point object from the given coordinates.
30
+ #
31
+ # See: Point.new
32
+ def self::Point(x, y = nil)
33
+ if x.kind_of?(Point)
34
+ x
35
+ elsif y
36
+ Point.new(x, y)
37
+ else
38
+ Point.new(*x)
39
+ end
40
+ end
41
+
42
+ # Creates a new Segment from +start_point+ to +end_point+ or, if +vector+ is given, from
43
+ # +start_point+ to +start_point+ + +vector+.
44
+ #
45
+ # See: Segment.new
46
+ def self::Segment(start_point, end_point = nil, vector: nil)
47
+ if end_point
48
+ Segment.new(start_point, end_point)
49
+ elsif vector
50
+ Segment.new(start_point, start_point + vector)
51
+ else
52
+ raise ArgumentError, "Either end_point or a vector must be given"
53
+ end
54
+ end
55
+
56
+ # Creates a new Polygon object from the given vertices.
57
+ #
58
+ # See: Polygon.new
59
+ def self::Polygon(*vertices)
60
+ Polygon.new(vertices)
61
+ end
62
+
63
+ # Creates a PolygonSet from the given array of Polygon instances.
64
+ #
65
+ # See: PolygonSet.new
66
+ def self::PolygonSet(*polygons)
67
+ PolygonSet.new(polygons)
68
+ end
69
+
70
+ end
@@ -0,0 +1,35 @@
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/utils'
12
+
13
+ module Geom2D
14
+
15
+ # This module contains helper functions as well as classes implementing algorithms.
16
+ module Algorithms
17
+
18
+ autoload(:PolygonOperation, 'geom2d/algorithms/polygon_operation')
19
+
20
+ extend Utils
21
+
22
+ # Determines whether the three points form a counterclockwise turn.
23
+ #
24
+ # Returns
25
+ #
26
+ # * +1 if the points a -> b -> c form a counterclockwise angle,
27
+ # * -1 if the points a -> b -> c from a clockwise angle, and
28
+ # * 0 if the points are collinear.
29
+ def self.ccw(a, b, c)
30
+ float_compare((b.x - a.x) * (c.y - a.y), (c.x - a.x) * (b.y - a.y))
31
+ end
32
+
33
+ end
34
+
35
+ end
@@ -0,0 +1,435 @@
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/algorithms'
12
+ require 'geom2d/utils'
13
+ require 'geom2d/polygon_set'
14
+
15
+ module Geom2D
16
+ module Algorithms
17
+
18
+ # Performs intersection, union, difference and xor operations on Geom2D::PolygonSet objects.
19
+ #
20
+ # The entry method is PolygonOperation.run.
21
+ #
22
+ # The algorithm is described in the paper "A simple algorithm for Boolean operations on
23
+ # polygons" by Martinez et al (see http://dl.acm.org/citation.cfm?id=2494701). This
24
+ # implementation is based on the public domain code from
25
+ # http://www4.ujaen.es/~fmartin/bool_op.html, which is the original implementation from the
26
+ # authors of the paper.
27
+ class PolygonOperation
28
+
29
+ include Utils
30
+
31
+ # Represents one event of the sweep line phase, i.e. a (left or right) endpoint of a segment
32
+ # together with processing information.
33
+ class SweepEvent
34
+
35
+ include Utils
36
+
37
+ # +True+ if the #point is the left endpoint of the segment.
38
+ attr_accessor :left
39
+
40
+ # The point of this event, a Geom2D::Point instance.
41
+ attr_reader :point
42
+
43
+ # The type of polygon, either :clipping or :subject.
44
+ attr_reader :polygon_type
45
+
46
+ # The other event. This event together with the other event represents a segment.
47
+ attr_accessor :other_event
48
+
49
+ # The edge type of the event's segment, either :normal, :non_contributing, :same_transition
50
+ # or :different_transition.
51
+ attr_accessor :edge_type
52
+
53
+ # +True+ if the segment represents an inside-outside transition from (point.x, -infinity)
54
+ # into the polygon set to which the segment belongs.
55
+ attr_accessor :in_out
56
+
57
+ # +True+ if the closest segment downwards from this segment that belongs to the other
58
+ # polygon set represents an inside-outside transition from (point.x, -infinity).
59
+ attr_accessor :other_in_out
60
+
61
+ # +True+ if this event's segment is part of the result polygon set.
62
+ attr_accessor :in_result
63
+
64
+ # The previous event/segment downwards from this segment that is part of the result polygon
65
+ # set.
66
+ attr_accessor :prev_in_result
67
+
68
+ # Creates a new SweepEvent.
69
+ def initialize(left, point, polygon_type, other_event: nil, edge_type: :normal)
70
+ @left = left
71
+ @point = point
72
+ @other_event = other_event
73
+ @polygon_type = polygon_type
74
+ @edge_type = edge_type
75
+ end
76
+
77
+ # Returns +true+ if this event's line #segment is below the point +p+.
78
+ def below?(p)
79
+ if left
80
+ Algorithms.ccw(@point, @other_event.point, p) > 0
81
+ else
82
+ Algorithms.ccw(@other_event.point, @point, p) > 0
83
+ end
84
+ end
85
+
86
+ # Returns +true+ if this event's line #segment is above the point +p+.
87
+ def above?(point)
88
+ !below?(point)
89
+ end
90
+
91
+ # Returns +true+ if this event's line segment is vertical.
92
+ def vertical?
93
+ float_equal(@point.x, other_event.point.x)
94
+ end
95
+
96
+ # Returns +true+ if this event should be *processed after the given event*.
97
+ #
98
+ # This method is used for sorting events in the event queue of the main algorithm.
99
+ def process_after?(event)
100
+ if (cmp = float_compare(point.x, event.point.x)) != 0
101
+ cmp > 0 # different x-coordinates, true if point.x is greater
102
+ elsif (cmp = float_compare(point.y, event.point.y)) != 0
103
+ cmp > 0 # same x-, different y-coordinates, true if point.y is greater
104
+ elsif left != event.left
105
+ left # same point; one is left, one is right endpoint; true if left endpoint
106
+ elsif Algorithms.ccw(point, other_event.point, event.other_event.point) != 0
107
+ above?(event.other_event.point) # both left or right; not collinear; true if top segment
108
+ else
109
+ polygon_type < event.polygon_type # true if clipping polygon
110
+ end
111
+ end
112
+
113
+ # Returns +true+ it this event's segment is below the segment of the other event.
114
+ #
115
+ # This method is used for sorting events in the sweep line status data structure of the main
116
+ # algorithm.
117
+ #
118
+ # This method is intended to be used only on left events!
119
+ def segment_below?(event)
120
+ if self == event
121
+ false
122
+ elsif Algorithms.ccw(point, other_event.point, event.point) != 0 ||
123
+ Algorithms.ccw(point, other_event.point, event.other_event.point) != 0
124
+ # segments are not collinear
125
+ if point == event.point
126
+ below?(event.other_event.point)
127
+ elsif float_compare(point.x, event.point.x) == 0
128
+ float_compare(point.y, event.point.y) < 0
129
+ elsif process_after?(event)
130
+ event.above?(point)
131
+ else
132
+ below?(event.point)
133
+ end
134
+ elsif polygon_type != event.polygon_type
135
+ polygon_type > event.polygon_type
136
+ elsif point == event.point
137
+ object_id < event.object_id # just need any consistency criterion
138
+ else
139
+ process_after?(event)
140
+ end
141
+ end
142
+
143
+ # Returns +true+ if this event's segment should be in the result based on the boolean
144
+ # operation.
145
+ def in_result?(operation)
146
+ case edge_type
147
+ when :normal
148
+ case operation
149
+ when :intersection then !other_in_out
150
+ when :union then other_in_out
151
+ when :difference then polygon_type == :subject ? other_in_out : !other_in_out
152
+ when :xor then true
153
+ end
154
+ when :same_transition
155
+ operation == :intersection || operation == :union
156
+ when :different_transition
157
+ operation == :difference
158
+ when :non_contributing
159
+ false
160
+ end
161
+ end
162
+
163
+ # Returns this event's line segment (point, other_event.point).
164
+ def segment
165
+ Geom2D::Segment(point, other_event.point)
166
+ end
167
+
168
+ end
169
+
170
+ # Performs the given operation (:union, :intersection, :difference, :xor) on the subject and
171
+ # clipping polygon sets.
172
+ def self.run(subject, clipping, operation)
173
+ new(subject, clipping, operation).run.result
174
+ end
175
+
176
+ # The result of the operation, a Geom2D::PolygonSet.
177
+ attr_reader :result
178
+
179
+ # Creates a new boolean operation object, performing the +operation+ (either :intersection,
180
+ # :union, :difference or :xor) on the subject and clipping Geom2D::PolygonSet objects.
181
+ def initialize(subject, clipping, operation)
182
+ @subject = subject
183
+ @clipping = clipping
184
+ @operation = operation
185
+
186
+ @result = PolygonSet.new
187
+ @event_queue = Utils::SortedLinkedList.new {|a, b| a.process_after?(b) }
188
+ # @sweep_line should really be a sorted data structure with O(log(n)) for insert/search!
189
+ @sweep_line = Utils::SortedLinkedList.new {|a, b| a.segment_below?(b) }
190
+ @sorted_events = []
191
+ end
192
+
193
+ # Performs the boolean polygon operation.
194
+ def run
195
+ subject_bb = @subject.bbox
196
+ clipping_bb = @clipping.bbox
197
+ min_of_max_x = [subject_bb.max_x, clipping_bb.max_x].min
198
+
199
+ return self if trivial_operation(subject_bb, clipping_bb)
200
+
201
+ @subject.each_segment {|segment| process_segment(segment, :subject) }
202
+ @clipping.each_segment {|segment| process_segment(segment, :clipping) }
203
+
204
+ until @event_queue.empty?
205
+ event = @event_queue.last
206
+ if (@operation == :intersection && event.point.x > min_of_max_x) ||
207
+ (@operation == :difference && event.point.x > subject_bb.max_x)
208
+ connect_edges
209
+ return self
210
+ end
211
+ @sorted_events.push(event)
212
+
213
+ @event_queue.pop
214
+ if event.left # the segment hast to be inserted into status line
215
+ node = @sweep_line.insert(event)
216
+ prev_event = (node.prev_node.anchor? ? nil : node.prev_node.value)
217
+ next_event = (node.next_node.anchor? ? nil : node.next_node.value)
218
+
219
+ compute_event_fields(event, prev_event)
220
+ if next_event && possible_intersection(event, next_event) == 2
221
+ compute_event_fields(event, prev_event)
222
+ compute_event_fields(next_event, event)
223
+ end
224
+ if prev_event && possible_intersection(prev_event, event) == 2
225
+ prevprev_ev = (node.prev_node.prev_node.anchor? ? nil : node.prev_node.prev_node.value)
226
+ compute_event_fields(prev_event, prevprev_ev)
227
+ compute_event_fields(event, prev_event)
228
+ end
229
+ else # the segment has to be removed from the status line
230
+ event = event.other_event # use left event
231
+ node = @sweep_line.find_node(event)
232
+
233
+ next_node = node.next_node
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)
238
+ end
239
+ end
240
+ end
241
+ connect_edges
242
+ self
243
+ end
244
+
245
+ private
246
+
247
+ # Returns +true+ if the operation is a trivial one, e.g. if one polygon set is empty.
248
+ def trivial_operation(subject_bb, clipping_bb)
249
+ if @subject.nr_of_contours * @clipping.nr_of_contours == 0
250
+ if @operation == :difference
251
+ @result = @subject
252
+ elsif @operation == :union || @operation == :xor
253
+ @result = (@subject.nr_of_contours == 0 ? @clipping : @subject)
254
+ end
255
+ true
256
+ elsif subject_bb.min_x > clipping_bb.max_x || clipping_bb.min_x > subject_bb.max_x ||
257
+ subject_bb.min_y > clipping_bb.max_y || clipping_bb.min_y > subject_bb.max_y
258
+ if @operation == :difference
259
+ @result = @subject
260
+ elsif @operation == :union || @operation == :xor
261
+ @result = @subject + @clipping
262
+ end
263
+ true
264
+ else
265
+ false
266
+ end
267
+ end
268
+
269
+ # Processes the segment by adding the needed SweepEvent objects into the event queue.
270
+ def process_segment(segment, polygon_type)
271
+ return if segment.degenerate?
272
+ start_point_is_left = (segment.start_point == segment.min)
273
+ 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, other_event: e1)
275
+ e1.other_event = e2
276
+ @event_queue.push(e1).push(e2)
277
+ end
278
+
279
+ # Computes the fields of the sweep event, using information from the previous event.
280
+ #
281
+ # The argument +prev+ is either the previous event or +nil+ if there is no previous event.
282
+ def compute_event_fields(event, prev)
283
+ if prev.nil?
284
+ event.in_out = false
285
+ event.other_in_out = true
286
+ elsif event.polygon_type == prev.polygon_type
287
+ event.in_out = !prev.in_out
288
+ event.other_in_out = prev.other_in_out
289
+ else
290
+ event.in_out = !prev.other_in_out
291
+ event.other_in_out = (prev.vertical? ? !prev.in_out : prev.in_out)
292
+ end
293
+
294
+ if prev
295
+ event.prev_in_result = if !prev.in_result?(@operation) || prev.vertical?
296
+ prev.prev_in_result
297
+ else
298
+ prev
299
+ end
300
+ end
301
+ event.in_result = event.in_result?(@operation)
302
+ end
303
+
304
+ # Checks for possible intersections of the segments of the two events and returns 0 for no
305
+ # intersections, 1 for intersection in one point, 2 if the segments are equal or have the same
306
+ # left endpoint, and 3 for all other cases.
307
+ def possible_intersection(ev1, ev2)
308
+ result = ev1.segment.intersect(ev2.segment)
309
+
310
+ result_is_point = result.kind_of?(Geom2D::Point)
311
+ if result.nil? ||
312
+ (result_is_point &&
313
+ (ev1.point == ev2.point || ev1.other_event.point == ev2.other_event.point))
314
+ return 0
315
+ elsif !result_is_point && ev1.polygon_type == ev2.polygon_type
316
+ raise "Edges of the same polygon overlap - not supported"
317
+ end
318
+
319
+ if result_is_point
320
+ divide_segment(ev1, result) if ev1.point != result && ev1.other_event.point != result
321
+ divide_segment(ev2, result) if ev2.point != result && ev2.other_event.point != result
322
+ return 1
323
+ end
324
+
325
+ events = []
326
+ if ev1.point == ev2.point
327
+ events.push(nil)
328
+ elsif ev1.process_after?(ev2)
329
+ events.push(ev2, ev1)
330
+ else
331
+ events.push(ev1, ev2)
332
+ end
333
+ if ev1.other_event.point == ev2.other_event.point
334
+ events.push(nil)
335
+ elsif ev1.other_event.process_after?(ev2.other_event)
336
+ events.push(ev2.other_event, ev1.other_event)
337
+ else
338
+ events.push(ev1.other_event, ev2.other_event)
339
+ end
340
+
341
+ if events.size == 2 || (events.size == 3 && events[2])
342
+ # segments are equal or have the same left endpoint
343
+ ev1.edge_type = :non_contributing
344
+ ev2.edge_type = (ev1.in_out == ev2.in_out ? :same_transition : :different_transition)
345
+ if events.size == 3
346
+ divide_segment(events[2].other_event, events[1].point)
347
+ end
348
+ 2
349
+ elsif events.size == 3 # segments have the same right endpoint
350
+ divide_segment(events[0], events[1].point)
351
+ 3
352
+ elsif events[0] != events[3].other_event # partial segment overlap
353
+ divide_segment(events[0], events[1].point)
354
+ divide_segment(events[1], events[2].point)
355
+ 3
356
+ else # one segments includes the other
357
+ divide_segment(events[0], events[1].point)
358
+ divide_segment(events[3].other_event, events[2].point)
359
+ 3
360
+ end
361
+ end
362
+
363
+ # Divides the event's segment at the given point (which has to be inside the segment) and adds
364
+ # the resulting events to the event queue.
365
+ def divide_segment(event, point)
366
+ right = SweepEvent.new(false, point, event.polygon_type, other_event: event)
367
+ left = SweepEvent.new(true, point, event.polygon_type, other_event: event.other_event)
368
+ event.other_event.other_event = left
369
+ event.other_event = right
370
+ @event_queue.push(left).push(right)
371
+ end
372
+
373
+ # Connects the edges of the segments that are in the result.
374
+ def connect_edges
375
+ events = @sorted_events.select do |ev|
376
+ (ev.left && ev.in_result) || (!ev.left && ev.other_event.in_result)
377
+ end
378
+
379
+ # events may not be fully sorted due to overlapping edges
380
+ events.sort! {|a, b| a.process_after?(b) ? 1 : -1 }
381
+ event_pos = {}
382
+ events.each_with_index do |event, index|
383
+ event_pos[event] = index
384
+ unless event.left
385
+ event_pos[event], event_pos[event.other_event] =
386
+ event_pos[event.other_event], event_pos[event]
387
+ end
388
+ end
389
+
390
+ processed = {}
391
+ events.each do |event|
392
+ next if processed[event]
393
+
394
+ initial_point = event.point
395
+ polygon = Geom2D::Polygon.new
396
+ @result << polygon
397
+ polygon << initial_point
398
+ while event.other_event.point != initial_point
399
+ processed[event] = true
400
+ processed[event.other_event] = true
401
+ if polygon.nr_of_vertices > 1 &&
402
+ Algorithms.ccw(polygon[-2], polygon[-1], event.other_event.point) == 0
403
+ polygon.pop
404
+ end
405
+ polygon << event.other_event.point
406
+ event = next_event(events, event_pos, processed, event)
407
+ end
408
+
409
+ if Algorithms.ccw(polygon[-2], polygon[-1], polygon[0]) == 0
410
+ polygon.pop
411
+ end
412
+ processed[event] = processed[event.other_event] = true
413
+ end
414
+ end
415
+
416
+ # Chooses the next event based on the argument.
417
+ def next_event(events, event_pos, processed, event)
418
+ pos = event_pos[event] + 1
419
+ while pos < events.size && events[pos].point == event.other_event.point
420
+ if processed[events[pos]]
421
+ pos += 1
422
+ else
423
+ return events[pos]
424
+ end
425
+ end
426
+
427
+ pos = event_pos[event] - 1
428
+ pos -= 1 while processed[events[pos]]
429
+ events[pos]
430
+ end
431
+
432
+ end
433
+
434
+ end
435
+ end