geom2d 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
checksums.yaml
ADDED
@@ -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
|
data/CONTRIBUTERS
ADDED
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.
|
data/README.md
ADDED
@@ -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
|
+
~~~
|
data/Rakefile
ADDED
@@ -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
|
data/lib/geom2d.rb
ADDED
@@ -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
|