geom2d 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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