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.
Files changed (48) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +21 -0
  3. data/README.md +165 -0
  4. data/examples/larb_transform.rb +17 -0
  5. data/examples/rugl_mesh.rb +33 -0
  6. data/examples/svg_boolean.rb +28 -0
  7. data/ext/clipper2/CMakeLists.txt +27 -0
  8. data/ext/clipper2/clipper2_native.cpp +2 -0
  9. data/ext/clipper2/extconf.rb +27 -0
  10. data/ext/clipper2/generate_bindings.rb +73 -0
  11. data/ext/clipper2/memory_check.cpp +48 -0
  12. data/ext/clipper2/vendor/LICENSE +23 -0
  13. data/ext/clipper2/vendor/UPSTREAM.md +11 -0
  14. data/ext/clipper2/vendor/include/clipper2/clipper.core.h +1159 -0
  15. data/ext/clipper2/vendor/include/clipper2/clipper.engine.h +635 -0
  16. data/ext/clipper2/vendor/include/clipper2/clipper.export.h +851 -0
  17. data/ext/clipper2/vendor/include/clipper2/clipper.h +796 -0
  18. data/ext/clipper2/vendor/include/clipper2/clipper.minkowski.h +117 -0
  19. data/ext/clipper2/vendor/include/clipper2/clipper.offset.h +125 -0
  20. data/ext/clipper2/vendor/include/clipper2/clipper.rectclip.h +80 -0
  21. data/ext/clipper2/vendor/include/clipper2/clipper.triangulation.h +27 -0
  22. data/ext/clipper2/vendor/include/clipper2/clipper.version.h +6 -0
  23. data/ext/clipper2/vendor/src/clipper.engine.cpp +3152 -0
  24. data/ext/clipper2/vendor/src/clipper.offset.cpp +661 -0
  25. data/ext/clipper2/vendor/src/clipper.rectclip.cpp +1027 -0
  26. data/ext/clipper2/vendor/src/clipper.triangulation.cpp +1221 -0
  27. data/lib/clipper2/api.rb +45 -0
  28. data/lib/clipper2/c_paths64.rb +80 -0
  29. data/lib/clipper2/earcut/engine.rb +191 -0
  30. data/lib/clipper2/earcut/geometry.rb +100 -0
  31. data/lib/clipper2/earcut/node.rb +18 -0
  32. data/lib/clipper2/earcut/ring.rb +94 -0
  33. data/lib/clipper2/earcut.rb +18 -0
  34. data/lib/clipper2/errors.rb +19 -0
  35. data/lib/clipper2/generated.rb +62 -0
  36. data/lib/clipper2/native.rb +38 -0
  37. data/lib/clipper2/native_memory.rb +64 -0
  38. data/lib/clipper2/operations.rb +206 -0
  39. data/lib/clipper2/path.rb +171 -0
  40. data/lib/clipper2/path_simplifier.rb +61 -0
  41. data/lib/clipper2/paths.rb +130 -0
  42. data/lib/clipper2/quantizer.rb +48 -0
  43. data/lib/clipper2/triangulation.rb +103 -0
  44. data/lib/clipper2/version.rb +5 -0
  45. data/lib/clipper2.rb +34 -0
  46. data/licenses/CLIPPER2-BSL-1.0.txt +23 -0
  47. data/licenses/EARCUT-ISC.txt +15 -0
  48. metadata +113 -0
@@ -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