clipper2 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +165 -0
- data/examples/larb_transform.rb +17 -0
- data/examples/rugl_mesh.rb +33 -0
- data/examples/svg_boolean.rb +28 -0
- data/ext/clipper2/CMakeLists.txt +27 -0
- data/ext/clipper2/clipper2_native.cpp +2 -0
- data/ext/clipper2/extconf.rb +27 -0
- data/ext/clipper2/generate_bindings.rb +73 -0
- data/ext/clipper2/memory_check.cpp +48 -0
- data/ext/clipper2/vendor/LICENSE +23 -0
- data/ext/clipper2/vendor/UPSTREAM.md +11 -0
- data/ext/clipper2/vendor/include/clipper2/clipper.core.h +1159 -0
- data/ext/clipper2/vendor/include/clipper2/clipper.engine.h +635 -0
- data/ext/clipper2/vendor/include/clipper2/clipper.export.h +851 -0
- data/ext/clipper2/vendor/include/clipper2/clipper.h +796 -0
- data/ext/clipper2/vendor/include/clipper2/clipper.minkowski.h +117 -0
- data/ext/clipper2/vendor/include/clipper2/clipper.offset.h +125 -0
- data/ext/clipper2/vendor/include/clipper2/clipper.rectclip.h +80 -0
- data/ext/clipper2/vendor/include/clipper2/clipper.triangulation.h +27 -0
- data/ext/clipper2/vendor/include/clipper2/clipper.version.h +6 -0
- data/ext/clipper2/vendor/src/clipper.engine.cpp +3152 -0
- data/ext/clipper2/vendor/src/clipper.offset.cpp +661 -0
- data/ext/clipper2/vendor/src/clipper.rectclip.cpp +1027 -0
- data/ext/clipper2/vendor/src/clipper.triangulation.cpp +1221 -0
- data/lib/clipper2/api.rb +45 -0
- data/lib/clipper2/c_paths64.rb +80 -0
- data/lib/clipper2/earcut/engine.rb +191 -0
- data/lib/clipper2/earcut/geometry.rb +100 -0
- data/lib/clipper2/earcut/node.rb +18 -0
- data/lib/clipper2/earcut/ring.rb +94 -0
- data/lib/clipper2/earcut.rb +18 -0
- data/lib/clipper2/errors.rb +19 -0
- data/lib/clipper2/generated.rb +62 -0
- data/lib/clipper2/native.rb +38 -0
- data/lib/clipper2/native_memory.rb +64 -0
- data/lib/clipper2/operations.rb +206 -0
- data/lib/clipper2/path.rb +171 -0
- data/lib/clipper2/path_simplifier.rb +61 -0
- data/lib/clipper2/paths.rb +130 -0
- data/lib/clipper2/quantizer.rb +48 -0
- data/lib/clipper2/triangulation.rb +103 -0
- data/lib/clipper2/version.rb +5 -0
- data/lib/clipper2.rb +34 -0
- data/licenses/CLIPPER2-BSL-1.0.txt +23 -0
- data/licenses/EARCUT-ISC.txt +15 -0
- metadata +113 -0
data/lib/clipper2/api.rb
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Clipper2
|
|
4
|
+
class << self
|
|
5
|
+
def boolean(type, subject, clip = nil, **options)
|
|
6
|
+
Operations.boolean(type, subject, clip, **options)
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def union(subject, clip = nil, **options)
|
|
10
|
+
boolean(:union, subject, clip, **options).first
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def intersect(subject, clip, **options)
|
|
14
|
+
boolean(:intersection, subject, clip, **options).first
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def difference(subject, clip, **options)
|
|
18
|
+
boolean(:difference, subject, clip, **options).first
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def xor(subject, clip, **options)
|
|
22
|
+
boolean(:xor, subject, clip, **options).first
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def inflate(paths, delta, **options)
|
|
26
|
+
Operations.inflate(paths, delta, **options)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def rect_clip(paths, rect)
|
|
30
|
+
Operations.rect_clip(paths, rect)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def rect_clip_lines(paths, rect)
|
|
34
|
+
Operations.rect_clip(paths, rect, lines: true)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def minkowski_sum(pattern, path, **options)
|
|
38
|
+
Operations.minkowski_sum(pattern, path, **options)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def minkowski_difference(pattern, path, **options)
|
|
42
|
+
Operations.minkowski_difference(pattern, path, **options)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Clipper2
|
|
4
|
+
module CPaths64
|
|
5
|
+
MAX_ELEMENTS = 100_000_000
|
|
6
|
+
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
def pack(paths)
|
|
10
|
+
non_empty_paths = paths.reject(&:empty?)
|
|
11
|
+
values = [2 + non_empty_paths.sum { |path| 2 + (path.size * 2) }, non_empty_paths.size]
|
|
12
|
+
non_empty_paths.each do |path|
|
|
13
|
+
values << path.size << 0
|
|
14
|
+
values.concat(path.send(:quantized_coords))
|
|
15
|
+
end
|
|
16
|
+
values.pack("q<*")
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def pack_path(path)
|
|
20
|
+
[path.size, 0, *path.send(:quantized_coords)].pack("q<*")
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def unpack(bytes, scale:)
|
|
24
|
+
raise CodecError, "CPaths64 data length must be a multiple of 8" unless (bytes.bytesize % 8).zero?
|
|
25
|
+
|
|
26
|
+
decode(bytes.unpack("q<*"), scale: scale)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def read(pointer, scale:)
|
|
30
|
+
return Paths.new([], scale: scale) if pointer.nil? || pointer.null?
|
|
31
|
+
|
|
32
|
+
length = pointer.read_bytes(8).unpack1("q<")
|
|
33
|
+
validate_length(length)
|
|
34
|
+
unpack(pointer.read_bytes(length * 8), scale: scale)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def memory_pointer(bytes)
|
|
38
|
+
pointer = FFI::MemoryPointer.new(:char, bytes.bytesize)
|
|
39
|
+
pointer.put_bytes(0, bytes)
|
|
40
|
+
pointer
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def decode(values, scale:)
|
|
44
|
+
raise CodecError, "CPaths64 header is missing" if values.length < 2
|
|
45
|
+
|
|
46
|
+
total_length, path_count = values.first(2)
|
|
47
|
+
validate_length(total_length)
|
|
48
|
+
unless total_length == values.length && path_count.between?(0, total_length)
|
|
49
|
+
raise CodecError, "invalid CPaths64 header"
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
cursor = 2
|
|
53
|
+
paths_coords = Array.new(path_count) do
|
|
54
|
+
vertex_count, reserved = values[cursor, 2]
|
|
55
|
+
unless vertex_count && vertex_count >= 0 && reserved == 0
|
|
56
|
+
raise CodecError, "invalid CPath64 header at element #{cursor}"
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
cursor += 2
|
|
60
|
+
coordinate_count = vertex_count * 2
|
|
61
|
+
coords = values[cursor, coordinate_count]
|
|
62
|
+
raise CodecError, "truncated CPath64 coordinates" unless coords&.length == coordinate_count
|
|
63
|
+
|
|
64
|
+
cursor += coordinate_count
|
|
65
|
+
coords
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
raise CodecError, "trailing CPaths64 data" unless cursor == values.length
|
|
69
|
+
|
|
70
|
+
Paths.from_coords(paths_coords, scale: scale)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def validate_length(length)
|
|
74
|
+
return if length.is_a?(Integer) && length.between?(2, MAX_ELEMENTS)
|
|
75
|
+
|
|
76
|
+
raise CodecError, "invalid CPaths64 element count: #{length.inspect}"
|
|
77
|
+
end
|
|
78
|
+
private_class_method :validate_length
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Clipper2
|
|
4
|
+
module Earcut
|
|
5
|
+
class Engine
|
|
6
|
+
include Geometry
|
|
7
|
+
include Ring
|
|
8
|
+
|
|
9
|
+
def initialize(data, hole_indices)
|
|
10
|
+
@data = data
|
|
11
|
+
@hole_indices = hole_indices
|
|
12
|
+
@triangles = []
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def call
|
|
16
|
+
outer_end = @hole_indices.empty? ? @data.length / 2 : @hole_indices.first
|
|
17
|
+
outer = linked_list(0, outer_end, true)
|
|
18
|
+
return @triangles unless outer && !outer.next.equal?(outer.previous)
|
|
19
|
+
|
|
20
|
+
outer = eliminate_holes(outer) unless @hole_indices.empty?
|
|
21
|
+
earcut_linked(outer)
|
|
22
|
+
@triangles
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def earcut_linked(ear, pass = 0)
|
|
28
|
+
return unless ear
|
|
29
|
+
|
|
30
|
+
stop = ear
|
|
31
|
+
until ear.previous.equal?(ear.next)
|
|
32
|
+
previous = ear.previous
|
|
33
|
+
following = ear.next
|
|
34
|
+
if ear?(ear)
|
|
35
|
+
@triangles.concat([previous.index, ear.index, following.index])
|
|
36
|
+
remove_node(ear)
|
|
37
|
+
ear = following.next
|
|
38
|
+
stop = following.next
|
|
39
|
+
next
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
ear = following
|
|
43
|
+
next unless ear.equal?(stop)
|
|
44
|
+
|
|
45
|
+
if pass.zero?
|
|
46
|
+
earcut_linked(filter_points(ear), 1)
|
|
47
|
+
elsif pass == 1
|
|
48
|
+
earcut_linked(cure_local_intersections(filter_points(ear)), 2)
|
|
49
|
+
else
|
|
50
|
+
split_earcut(ear)
|
|
51
|
+
end
|
|
52
|
+
break
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def ear?(ear)
|
|
57
|
+
first = ear.previous
|
|
58
|
+
third = ear.next
|
|
59
|
+
return false if triangle_area(first, ear, third) >= 0
|
|
60
|
+
|
|
61
|
+
min_x, max_x = [first.x, ear.x, third.x].minmax
|
|
62
|
+
min_y, max_y = [first.y, ear.y, third.y].minmax
|
|
63
|
+
point = third.next
|
|
64
|
+
until point.equal?(first)
|
|
65
|
+
inside_bounds = point.x.between?(min_x, max_x) && point.y.between?(min_y, max_y)
|
|
66
|
+
if inside_bounds && point_in_triangle?(first.x, first.y, ear.x, ear.y, third.x, third.y, point.x, point.y) &&
|
|
67
|
+
triangle_area(point.previous, point, point.next) >= 0
|
|
68
|
+
return false
|
|
69
|
+
end
|
|
70
|
+
point = point.next
|
|
71
|
+
end
|
|
72
|
+
true
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def cure_local_intersections(start)
|
|
76
|
+
point = start
|
|
77
|
+
loop do
|
|
78
|
+
first = point.previous
|
|
79
|
+
fourth = point.next.next
|
|
80
|
+
if !same_point?(first, fourth) && intersects?(first, point, point.next, fourth) &&
|
|
81
|
+
locally_inside?(first, fourth) && locally_inside?(fourth, first)
|
|
82
|
+
@triangles.concat([first.index, point.index, fourth.index])
|
|
83
|
+
remove_node(point)
|
|
84
|
+
remove_node(point.next)
|
|
85
|
+
point = start = fourth
|
|
86
|
+
end
|
|
87
|
+
point = point.next
|
|
88
|
+
break if point.equal?(start)
|
|
89
|
+
end
|
|
90
|
+
filter_points(point)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def split_earcut(start)
|
|
94
|
+
first = start
|
|
95
|
+
loop do
|
|
96
|
+
second = first.next.next
|
|
97
|
+
until second.equal?(first.previous)
|
|
98
|
+
if first.index != second.index && valid_diagonal?(first, second)
|
|
99
|
+
other_half = split_polygon(first, second)
|
|
100
|
+
first = filter_points(first, first.next)
|
|
101
|
+
other_half = filter_points(other_half, other_half.next)
|
|
102
|
+
earcut_linked(first)
|
|
103
|
+
earcut_linked(other_half)
|
|
104
|
+
return
|
|
105
|
+
end
|
|
106
|
+
second = second.next
|
|
107
|
+
end
|
|
108
|
+
first = first.next
|
|
109
|
+
break if first.equal?(start)
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def eliminate_holes(outer)
|
|
114
|
+
holes = @hole_indices.each_with_index.map do |start_index, index|
|
|
115
|
+
end_index = @hole_indices[index + 1] || @data.length / 2
|
|
116
|
+
list = linked_list(start_index, end_index, false)
|
|
117
|
+
list.steiner = true if list.equal?(list.next)
|
|
118
|
+
leftmost(list)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
holes.sort_by!(&:x)
|
|
122
|
+
holes.each { |hole| outer = eliminate_hole(hole, outer) }
|
|
123
|
+
outer
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def eliminate_hole(hole, outer)
|
|
127
|
+
bridge = hole_bridge(hole, outer)
|
|
128
|
+
return outer unless bridge
|
|
129
|
+
|
|
130
|
+
reverse_bridge = split_polygon(bridge, hole)
|
|
131
|
+
filter_points(reverse_bridge, reverse_bridge.next)
|
|
132
|
+
filter_points(bridge, bridge.next)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def hole_bridge(hole, outer)
|
|
136
|
+
point = outer
|
|
137
|
+
intersection_x = -Float::INFINITY
|
|
138
|
+
bridge = nil
|
|
139
|
+
loop do
|
|
140
|
+
if hole.y <= point.y && hole.y >= point.next.y && point.next.y != point.y
|
|
141
|
+
x = point.x + ((hole.y - point.y) * (point.next.x - point.x)).fdiv(point.next.y - point.y)
|
|
142
|
+
if x <= hole.x && x > intersection_x
|
|
143
|
+
intersection_x = x
|
|
144
|
+
bridge = point.x < point.next.x ? point : point.next
|
|
145
|
+
return bridge if x == hole.x
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
point = point.next
|
|
149
|
+
break if point.equal?(outer)
|
|
150
|
+
end
|
|
151
|
+
return nil unless bridge
|
|
152
|
+
|
|
153
|
+
bridge_x = bridge.x
|
|
154
|
+
bridge_y = bridge.y
|
|
155
|
+
best_tangent = Float::INFINITY
|
|
156
|
+
start = bridge
|
|
157
|
+
point = bridge
|
|
158
|
+
loop do
|
|
159
|
+
if hole.x >= point.x && point.x >= bridge_x && hole.x != point.x &&
|
|
160
|
+
point_in_triangle?(hole.y < bridge_y ? hole.x : intersection_x, hole.y,
|
|
161
|
+
bridge_x, bridge_y,
|
|
162
|
+
hole.y < bridge_y ? intersection_x : hole.x, hole.y,
|
|
163
|
+
point.x, point.y)
|
|
164
|
+
tangent = (hole.y - point.y).abs.fdiv(hole.x - point.x)
|
|
165
|
+
better_tangent = tangent < best_tangent ||
|
|
166
|
+
(tangent == best_tangent && (point.x > bridge.x ||
|
|
167
|
+
(point.x == bridge.x && sector_contains_sector?(bridge, point))))
|
|
168
|
+
if locally_inside?(point, hole) && better_tangent
|
|
169
|
+
bridge = point
|
|
170
|
+
best_tangent = tangent
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
point = point.next
|
|
174
|
+
break if point.equal?(start)
|
|
175
|
+
end
|
|
176
|
+
bridge
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def leftmost(start)
|
|
180
|
+
point = start
|
|
181
|
+
result = start
|
|
182
|
+
loop do
|
|
183
|
+
result = point if point.x < result.x || (point.x == result.x && point.y < result.y)
|
|
184
|
+
point = point.next
|
|
185
|
+
break if point.equal?(start)
|
|
186
|
+
end
|
|
187
|
+
result
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
end
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Clipper2
|
|
4
|
+
module Earcut
|
|
5
|
+
module Geometry
|
|
6
|
+
private
|
|
7
|
+
|
|
8
|
+
def triangle_area(first, second, third)
|
|
9
|
+
((second.y - first.y) * (third.x - second.x)) -
|
|
10
|
+
((second.x - first.x) * (third.y - second.y))
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def same_point?(first, second)
|
|
14
|
+
first.x == second.x && first.y == second.y
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def point_in_triangle?(ax, ay, bx, by, cx, cy, px, py)
|
|
18
|
+
((cx - px) * (ay - py) >= (ax - px) * (cy - py)) &&
|
|
19
|
+
((ax - px) * (by - py) >= (bx - px) * (ay - py)) &&
|
|
20
|
+
((bx - px) * (cy - py) >= (cx - px) * (by - py))
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def valid_diagonal?(first, second)
|
|
24
|
+
return false if first.next.index == second.index || first.previous.index == second.index
|
|
25
|
+
return false if intersects_polygon?(first, second)
|
|
26
|
+
|
|
27
|
+
visible = locally_inside?(first, second) && locally_inside?(second, first) && middle_inside?(first, second)
|
|
28
|
+
sectors_face_same_way = !triangle_area(first.previous, first, second.previous).zero? ||
|
|
29
|
+
!triangle_area(first, second.previous, second).zero?
|
|
30
|
+
return true if visible && sectors_face_same_way
|
|
31
|
+
|
|
32
|
+
same_point?(first, second) && triangle_area(first.previous, first, first.next).positive? &&
|
|
33
|
+
triangle_area(second.previous, second, second.next).positive?
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def intersects?(first_start, first_end, second_start, second_end)
|
|
37
|
+
first_orientation = sign(triangle_area(first_start, first_end, second_start))
|
|
38
|
+
second_orientation = sign(triangle_area(first_start, first_end, second_end))
|
|
39
|
+
third_orientation = sign(triangle_area(second_start, second_end, first_start))
|
|
40
|
+
fourth_orientation = sign(triangle_area(second_start, second_end, first_end))
|
|
41
|
+
return true if first_orientation != second_orientation && third_orientation != fourth_orientation
|
|
42
|
+
return true if first_orientation.zero? && on_segment?(first_start, second_start, first_end)
|
|
43
|
+
return true if second_orientation.zero? && on_segment?(first_start, second_end, first_end)
|
|
44
|
+
return true if third_orientation.zero? && on_segment?(second_start, first_start, second_end)
|
|
45
|
+
|
|
46
|
+
fourth_orientation.zero? && on_segment?(second_start, first_end, second_end)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def on_segment?(start_point, point, end_point)
|
|
50
|
+
point.x.between?(*[start_point.x, end_point.x].minmax) &&
|
|
51
|
+
point.y.between?(*[start_point.y, end_point.y].minmax)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def sign(number)
|
|
55
|
+
number <=> 0
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def intersects_polygon?(first, second)
|
|
59
|
+
point = first
|
|
60
|
+
loop do
|
|
61
|
+
unrelated_edge = point.index != first.index && point.next.index != first.index &&
|
|
62
|
+
point.index != second.index && point.next.index != second.index
|
|
63
|
+
return true if unrelated_edge && intersects?(point, point.next, first, second)
|
|
64
|
+
|
|
65
|
+
point = point.next
|
|
66
|
+
break if point.equal?(first)
|
|
67
|
+
end
|
|
68
|
+
false
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def locally_inside?(first, second)
|
|
72
|
+
if triangle_area(first.previous, first, first.next).negative?
|
|
73
|
+
triangle_area(first, second, first.next) >= 0 && triangle_area(first, first.previous, second) >= 0
|
|
74
|
+
else
|
|
75
|
+
triangle_area(first, second, first.previous).negative? || triangle_area(first, first.next, second).negative?
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def middle_inside?(first, second)
|
|
80
|
+
point = first
|
|
81
|
+
inside = false
|
|
82
|
+
middle_x = (first.x + second.x).fdiv(2)
|
|
83
|
+
middle_y = (first.y + second.y).fdiv(2)
|
|
84
|
+
loop do
|
|
85
|
+
crosses = (point.y > middle_y) != (point.next.y > middle_y) && point.next.y != point.y
|
|
86
|
+
edge_x = ((point.next.x - point.x) * (middle_y - point.y)).fdiv(point.next.y - point.y) + point.x if crosses
|
|
87
|
+
inside = !inside if crosses && middle_x < edge_x
|
|
88
|
+
point = point.next
|
|
89
|
+
break if point.equal?(first)
|
|
90
|
+
end
|
|
91
|
+
inside
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def sector_contains_sector?(outer, inner)
|
|
95
|
+
triangle_area(outer.previous, outer, inner.previous).negative? &&
|
|
96
|
+
triangle_area(inner.next, outer, outer.next).negative?
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Clipper2
|
|
4
|
+
module Earcut
|
|
5
|
+
class Node
|
|
6
|
+
attr_accessor :index, :x, :y, :previous, :next, :steiner
|
|
7
|
+
|
|
8
|
+
def initialize(index, x, y)
|
|
9
|
+
@index = index
|
|
10
|
+
@x = x
|
|
11
|
+
@y = y
|
|
12
|
+
@previous = nil
|
|
13
|
+
@next = nil
|
|
14
|
+
@steiner = false
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Clipper2
|
|
4
|
+
module Earcut
|
|
5
|
+
module Ring
|
|
6
|
+
private
|
|
7
|
+
|
|
8
|
+
def linked_list(start_index, end_index, clockwise)
|
|
9
|
+
last = nil
|
|
10
|
+
indices = (start_index...end_index).to_a
|
|
11
|
+
indices.reverse! unless clockwise == (signed_area(start_index, end_index).positive?)
|
|
12
|
+
indices.each do |index|
|
|
13
|
+
coordinate_index = index * 2
|
|
14
|
+
last = insert_node(index, @data[coordinate_index], @data[coordinate_index + 1], last)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
if last && same_point?(last, last.next)
|
|
18
|
+
following = last.next
|
|
19
|
+
remove_node(last)
|
|
20
|
+
last = following
|
|
21
|
+
end
|
|
22
|
+
last
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def filter_points(start, finish = start)
|
|
26
|
+
return start unless start
|
|
27
|
+
|
|
28
|
+
point = start
|
|
29
|
+
loop do
|
|
30
|
+
again = false
|
|
31
|
+
if !point.steiner && (same_point?(point, point.next) || triangle_area(point.previous, point, point.next).zero?)
|
|
32
|
+
remove_node(point)
|
|
33
|
+
point = finish = point.previous
|
|
34
|
+
break if point.equal?(point.next)
|
|
35
|
+
|
|
36
|
+
again = true
|
|
37
|
+
else
|
|
38
|
+
point = point.next
|
|
39
|
+
end
|
|
40
|
+
break unless again || !point.equal?(finish)
|
|
41
|
+
end
|
|
42
|
+
finish
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def split_polygon(first, second)
|
|
46
|
+
first_copy = Node.new(first.index, first.x, first.y)
|
|
47
|
+
second_copy = Node.new(second.index, second.x, second.y)
|
|
48
|
+
first_next = first.next
|
|
49
|
+
second_previous = second.previous
|
|
50
|
+
|
|
51
|
+
first.next = second
|
|
52
|
+
second.previous = first
|
|
53
|
+
first_copy.next = first_next
|
|
54
|
+
first_next.previous = first_copy
|
|
55
|
+
second_copy.next = first_copy
|
|
56
|
+
first_copy.previous = second_copy
|
|
57
|
+
second_previous.next = second_copy
|
|
58
|
+
second_copy.previous = second_previous
|
|
59
|
+
second_copy
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def insert_node(index, x, y, last)
|
|
63
|
+
point = Node.new(index, x, y)
|
|
64
|
+
if last
|
|
65
|
+
point.next = last.next
|
|
66
|
+
point.previous = last
|
|
67
|
+
last.next.previous = point
|
|
68
|
+
last.next = point
|
|
69
|
+
else
|
|
70
|
+
point.previous = point
|
|
71
|
+
point.next = point
|
|
72
|
+
end
|
|
73
|
+
point
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def remove_node(point)
|
|
77
|
+
point.next.previous = point.previous
|
|
78
|
+
point.previous.next = point.next
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def signed_area(start_index, end_index)
|
|
82
|
+
sum = 0
|
|
83
|
+
previous_index = end_index - 1
|
|
84
|
+
(start_index...end_index).each do |index|
|
|
85
|
+
previous = previous_index * 2
|
|
86
|
+
current = index * 2
|
|
87
|
+
sum += (@data[previous] - @data[current]) * (@data[current + 1] + @data[previous + 1])
|
|
88
|
+
previous_index = index
|
|
89
|
+
end
|
|
90
|
+
sum
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# This is a dependency-free Ruby port of Mapbox Earcut 2.2.4's core algorithm.
|
|
4
|
+
# See licenses/EARCUT-ISC.txt for the upstream copyright and license.
|
|
5
|
+
require_relative "earcut/node"
|
|
6
|
+
require_relative "earcut/geometry"
|
|
7
|
+
require_relative "earcut/ring"
|
|
8
|
+
require_relative "earcut/engine"
|
|
9
|
+
|
|
10
|
+
module Clipper2
|
|
11
|
+
module Earcut
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
def call(vertices, hole_indices = [])
|
|
15
|
+
Engine.new(vertices, hole_indices).call
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Clipper2
|
|
4
|
+
class Error < StandardError; end
|
|
5
|
+
|
|
6
|
+
class RangeError < Error; end
|
|
7
|
+
class ScaleError < Error; end
|
|
8
|
+
class CodecError < Error; end
|
|
9
|
+
class TriangulationError < Error; end
|
|
10
|
+
|
|
11
|
+
class NativeError < Error
|
|
12
|
+
attr_reader :code
|
|
13
|
+
|
|
14
|
+
def initialize(message, code: nil)
|
|
15
|
+
@code = code
|
|
16
|
+
super(code ? "#{message} (code: #{code})" : message)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Generated by ext/clipper2/generate_bindings.rb. Do not edit by hand.
|
|
4
|
+
module Clipper2
|
|
5
|
+
module Generated
|
|
6
|
+
UPSTREAM_VERSION = "2.0.1"
|
|
7
|
+
|
|
8
|
+
CLIP_TYPES = {
|
|
9
|
+
:no_clip => 0,
|
|
10
|
+
:intersection => 1,
|
|
11
|
+
:union => 2,
|
|
12
|
+
:difference => 3,
|
|
13
|
+
:xor => 4
|
|
14
|
+
}.freeze
|
|
15
|
+
|
|
16
|
+
FILL_RULES = {
|
|
17
|
+
:even_odd => 0,
|
|
18
|
+
:non_zero => 1,
|
|
19
|
+
:positive => 2,
|
|
20
|
+
:negative => 3
|
|
21
|
+
}.freeze
|
|
22
|
+
|
|
23
|
+
JOIN_TYPES = {
|
|
24
|
+
:square => 0,
|
|
25
|
+
:bevel => 1,
|
|
26
|
+
:round => 2,
|
|
27
|
+
:miter => 3
|
|
28
|
+
}.freeze
|
|
29
|
+
|
|
30
|
+
END_TYPES = {
|
|
31
|
+
:polygon => 0,
|
|
32
|
+
:joined => 1,
|
|
33
|
+
:butt => 2,
|
|
34
|
+
:square => 3,
|
|
35
|
+
:round => 4
|
|
36
|
+
}.freeze
|
|
37
|
+
|
|
38
|
+
NATIVE_EXPORTS = [
|
|
39
|
+
:BooleanOp64,
|
|
40
|
+
:BooleanOpD,
|
|
41
|
+
:BooleanOp_PolyTree64,
|
|
42
|
+
:BooleanOp_PolyTreeD,
|
|
43
|
+
:DisposeArray64,
|
|
44
|
+
:DisposeArrayD,
|
|
45
|
+
:InflatePath64,
|
|
46
|
+
:InflatePathD,
|
|
47
|
+
:InflatePaths64,
|
|
48
|
+
:InflatePathsD,
|
|
49
|
+
:MinkowskiDiff64,
|
|
50
|
+
:MinkowskiSum64,
|
|
51
|
+
:RectClip64,
|
|
52
|
+
:RectClipD,
|
|
53
|
+
:RectClipLines64,
|
|
54
|
+
:RectClipLinesD,
|
|
55
|
+
:SetZCallback64,
|
|
56
|
+
:SetZCallbackD,
|
|
57
|
+
:Triangulate64,
|
|
58
|
+
:TriangulateD,
|
|
59
|
+
:Version
|
|
60
|
+
].freeze
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ffi"
|
|
4
|
+
|
|
5
|
+
module Clipper2
|
|
6
|
+
module Native
|
|
7
|
+
extend FFI::Library
|
|
8
|
+
|
|
9
|
+
class << self
|
|
10
|
+
def extension_path
|
|
11
|
+
candidates = [__dir__, *$LOAD_PATH.map { |path| File.join(path, "clipper2") }].flat_map do |directory|
|
|
12
|
+
%w[bundle dylib so dll].map { |extension| File.join(directory, "clipper2_native.#{extension}") }
|
|
13
|
+
end
|
|
14
|
+
path = candidates.find { |candidate| File.file?(candidate) }
|
|
15
|
+
return path if path
|
|
16
|
+
|
|
17
|
+
raise LoadError, "clipper2 native library is unavailable; reinstall the gem or run `rake native:build`"
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
ffi_lib extension_path
|
|
22
|
+
|
|
23
|
+
attach_function :version, :Version, [], :string
|
|
24
|
+
attach_function :BooleanOp64,
|
|
25
|
+
%i[uint8 uint8 pointer pointer pointer pointer pointer bool bool],
|
|
26
|
+
:int,
|
|
27
|
+
blocking: true
|
|
28
|
+
attach_function :InflatePaths64,
|
|
29
|
+
%i[pointer double uint8 uint8 double double bool],
|
|
30
|
+
:pointer,
|
|
31
|
+
blocking: true
|
|
32
|
+
attach_function :RectClip64, %i[pointer pointer], :pointer, blocking: true
|
|
33
|
+
attach_function :RectClipLines64, %i[pointer pointer], :pointer, blocking: true
|
|
34
|
+
attach_function :MinkowskiSum64, %i[pointer pointer bool], :pointer, blocking: true
|
|
35
|
+
attach_function :MinkowskiDiff64, %i[pointer pointer bool], :pointer, blocking: true
|
|
36
|
+
attach_function :DisposeArray64, [:pointer], :void
|
|
37
|
+
end
|
|
38
|
+
end
|