contrek 1.0.3 → 1.0.4
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 +4 -4
- data/Gemfile.lock +9 -1
- data/README.md +65 -3
- data/contrek.gemspec +4 -0
- data/lib/contrek/bitmaps/bitmap.rb +25 -4
- data/lib/contrek/bitmaps/chunky_bitmap.rb +29 -0
- data/lib/contrek/bitmaps/custom_bitmap.rb +9 -0
- data/lib/contrek/bitmaps/png_bitmap.rb +4 -0
- data/lib/contrek/finder/concurrent/clipped_polygon_finder.rb +13 -0
- data/lib/contrek/finder/concurrent/cluster.rb +86 -0
- data/lib/contrek/finder/concurrent/cursor.rb +219 -0
- data/lib/contrek/finder/concurrent/end_point.rb +14 -0
- data/lib/contrek/finder/concurrent/fake_cluster.rb +10 -0
- data/lib/contrek/finder/concurrent/finder.rb +119 -0
- data/lib/contrek/finder/concurrent/hub.rb +12 -0
- data/lib/contrek/finder/concurrent/listable.rb +23 -0
- data/lib/contrek/finder/concurrent/part.rb +72 -0
- data/lib/contrek/finder/concurrent/partitionable.rb +132 -0
- data/lib/contrek/finder/concurrent/polyline.rb +95 -0
- data/lib/contrek/finder/concurrent/poolable.rb +36 -0
- data/lib/contrek/finder/concurrent/position.rb +27 -0
- data/lib/contrek/finder/concurrent/queueable.rb +204 -0
- data/lib/contrek/finder/concurrent/sequence.rb +7 -0
- data/lib/contrek/finder/concurrent/shape.rb +11 -0
- data/lib/contrek/finder/concurrent/tile.rb +91 -0
- data/lib/contrek/finder/polygon_finder.rb +18 -5
- data/lib/contrek/version.rb +1 -1
- data/lib/contrek.rb +39 -8
- metadata +74 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d96b6c7ae60aefc5e7afed98c4619cf4e77cd0a226f2994d4dbc4f91952b315c
|
|
4
|
+
data.tar.gz: 336e0c2ca0f36f4608fd1547bdbcb27b5d78624a288d329a003e54412d6ce287
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 615ae9e725c5fa149bb3f9f30be365863d348962f26fbda8610285a8e096b80dc6d564438480a8b783dc607c5bc4b3c66bb9bc5ca808d2783c494b74e3b3b7be
|
|
7
|
+
data.tar.gz: 1d2fc4088a58c1ffe2dbe46c08b67a728bf3eacce02ead907aa7b871565982e3161681205930de777bc7af3e702310dd3030fb961c0cbd71fb15280704e37da3
|
data/Gemfile.lock
CHANGED
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
contrek (1.0.
|
|
4
|
+
contrek (1.0.4)
|
|
5
5
|
chunky_png (~> 1.4)
|
|
6
|
+
concurrent-ruby (~> 1.3.5)
|
|
6
7
|
rice (~> 4.5)
|
|
7
8
|
|
|
8
9
|
GEM
|
|
9
10
|
remote: https://rubygems.org/
|
|
10
11
|
specs:
|
|
11
12
|
ast (2.4.3)
|
|
13
|
+
base64 (0.3.0)
|
|
12
14
|
chunky_png (1.4.0)
|
|
15
|
+
concurrent-ruby (1.3.5)
|
|
16
|
+
curses (1.5.3)
|
|
13
17
|
diff-lcs (1.6.1)
|
|
14
18
|
json (2.11.3)
|
|
15
19
|
language_server-protocol (3.17.0.4)
|
|
@@ -54,6 +58,8 @@ GEM
|
|
|
54
58
|
lint_roller (~> 1.1)
|
|
55
59
|
rubocop (>= 1.75.0, < 2.0)
|
|
56
60
|
rubocop-ast (>= 1.38.0, < 2.0)
|
|
61
|
+
ruby-prof (1.7.2)
|
|
62
|
+
base64
|
|
57
63
|
ruby-progressbar (1.13.0)
|
|
58
64
|
standard (1.51.1)
|
|
59
65
|
language_server-protocol (~> 3.17.0.2)
|
|
@@ -77,7 +83,9 @@ PLATFORMS
|
|
|
77
83
|
|
|
78
84
|
DEPENDENCIES
|
|
79
85
|
contrek!
|
|
86
|
+
curses (>= 1.5.3)
|
|
80
87
|
rspec (~> 3.12)
|
|
88
|
+
ruby-prof (>= 1.7.2)
|
|
81
89
|
standard (~> 1.51)
|
|
82
90
|
|
|
83
91
|
BUNDLED WITH
|
data/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# Contrek
|
|
2
|
-
Contrek is a Ruby library (C++ powered) to trace png bitmap areas polygonal contours. Manages png images usign png++ and picoPNG (version 20101224) libraries.
|
|
2
|
+
Contrek is a Ruby library (C++ powered) to trace png bitmap areas polygonal contours. Manages png images usign png++ and picoPNG (version 20101224) libraries and may work multithreading.
|
|
3
3
|
|
|
4
4
|
## About Contrek library
|
|
5
|
-
Contrek (
|
|
5
|
+
Contrek (**con**tour **trek**king) simply scans your png bitmap and returns shape contour as close polygonal lines, both for the external and internal sides. It can compute the nesting level of the polygons found with a tree structure. It supports various levels and modes of compression and approximation of the found coordinates. It is capable of multithreaded processing (currently only on the Ruby side), splitting the image into vertical strips and recombining the coordinates in pairs.
|
|
6
6
|
|
|
7
7
|
In the following image all the non-white pixels have been examined and the result is the red polygon for the outer contour and the green one for the inner one
|
|
8
8
|

|
|
@@ -43,7 +43,8 @@ result = Contrek.contour!(
|
|
|
43
43
|
png_file_path: "labyrinth3.png",
|
|
44
44
|
options: {
|
|
45
45
|
class: "value_not_matcher",
|
|
46
|
-
color: {r: 255, g: 0, b: 0, a: 255}
|
|
46
|
+
color: {r: 255, g: 0, b: 0, a: 255},
|
|
47
|
+
finder: {treemap: true}
|
|
47
48
|
}
|
|
48
49
|
)
|
|
49
50
|
```
|
|
@@ -85,6 +86,67 @@ You can also read base64 png images
|
|
|
85
86
|
png_bitmap = CPPRemotePngBitMap.new("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+P+/HgAFhAJ/wlseKgAAAABJRU5ErkJggg==")
|
|
86
87
|
```
|
|
87
88
|
|
|
89
|
+
Multithreaded contour processing is supported. However, on Ruby MRI (the standard Ruby implementation, at least up to 3.x), the Global Interpreter Lock (GIL) prevents more than one thread from executing Ruby code simultaneously. As a consequence, execution remains effectively serialized even on multicore systems, unless the gem is used under JRuby or TruffleRuby (not tested).
|
|
90
|
+
|
|
91
|
+
The actual multithreaded implementation effort is therefore focused on the native C++ component, which is currently under development and is expected to provide substantially higher performance.
|
|
92
|
+
|
|
93
|
+
```ruby
|
|
94
|
+
result = Contrek.contour!(
|
|
95
|
+
png_file_path: "./spec/files/images/rectangle_8x8.png",
|
|
96
|
+
options: {
|
|
97
|
+
number_of_threads: 2,
|
|
98
|
+
native: false,
|
|
99
|
+
class: "value_not_matcher",
|
|
100
|
+
color: {r: 255, g: 255, b: 255, a: 255},
|
|
101
|
+
finder: {number_of_tiles: 2, compress: {uniq: true, linear: true}}
|
|
102
|
+
}
|
|
103
|
+
)
|
|
104
|
+
```
|
|
105
|
+
The example uses 2 threads, and the image has been divided into 2 vertical bands. It is also possible to rely on the system to determine the maximum number of threads supported by your CPU (cores) by passing the appropriate parameter.
|
|
106
|
+
|
|
107
|
+
```ruby
|
|
108
|
+
number_of_threads: nil
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Regarding multithreading:
|
|
112
|
+
|
|
113
|
+
- The algorithm splits the contour-detection workflow into multiple phases that can be executed in parallel. The initial contour extraction on each band and the subsequent merging of coordinates between adjacent bands—performed pairwise, recursively, and in a non-deterministic order—results in a final output that is not idempotent. Idempotence is guaranteed only when the exact same merging sequence is repeated.
|
|
114
|
+
|
|
115
|
+
- The treemap option is currently ignored (multithreaded treemap support will be introduced in upcoming revisions).
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
## Multithreaded approach
|
|
119
|
+
|
|
120
|
+
The multithreaded contour-tracing implementation operates as follows:
|
|
121
|
+
|
|
122
|
+
The input image is divided into multiple vertical bands—at least two—where each band shares a one-pixel-wide vertical strip with its adjacent bands.
|
|
123
|
+
For example, if the image is 20 pixels wide and split into two bands, the first band may span from x=0 to x=9 (10 pixels), and the second from x=9 to x=19 (11 pixels).
|
|
124
|
+
The vertical column at x=9 is therefore the shared region.
|
|
125
|
+
|
|
126
|
+
```md
|
|
127
|
+
01234567890123456789
|
|
128
|
+
---------*----------
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Each band is processed independently as if it were a standalone image, with its contour-tracing assigned to a dedicated thread.
|
|
132
|
+
Whenever two adjacent bands have completed processing, they are assigned to an available thread that performs the merge of their coordinates.
|
|
133
|
+
|
|
134
|
+
For example, with three bands
|
|
135
|
+
B1 – B2 – B3
|
|
136
|
+
the merging sequence might be:
|
|
137
|
+
|
|
138
|
+
B1 + B2 → (B1+B2) + B3
|
|
139
|
+
or
|
|
140
|
+
|
|
141
|
+
B2 + B3 → B1 + (B2+B3)
|
|
142
|
+
|
|
143
|
+
The order is therefore nondeterministic, constrained only by the possible combinations derived from the initial number of bands.
|
|
144
|
+
|
|
145
|
+
The merging algorithm operates by splitting polygons that intersect the shared band into sequential subsets of coordinates—called parts—distinguishing between segments inside the shared region and those outside it.
|
|
146
|
+
The first stage merges the outer boundary; the second stage merges the disconnected inner parts, which are inserted where needed into the outer sequences produced in the first stage.
|
|
147
|
+
|
|
148
|
+
This process is applied recursively, merging bands until a single final band remains, upon which the coordinate-compression algorithms are executed.
|
|
149
|
+
|
|
88
150
|
## Performances native vs pure
|
|
89
151
|
One of the most complex test you can find under the spec folder is named "scans poly 1200x800", scans this [image](spec/files/images/sample_1200x800.png) computing coordinates to draw polygons drawn in this [result](spec/files/stored_samples/sample_1200x800.png).
|
|
90
152
|
On pure ruby implementation kept time
|
data/contrek.gemspec
CHANGED
|
@@ -19,7 +19,11 @@ Gem::Specification.new do |s|
|
|
|
19
19
|
|
|
20
20
|
s.add_development_dependency "rspec", "~> 3.12"
|
|
21
21
|
s.add_development_dependency "standard", "~> 1.51"
|
|
22
|
+
s.add_development_dependency "curses", "~> 1.5", ">= 1.5.3"
|
|
23
|
+
s.add_development_dependency "ruby-prof", "~> 1.7", ">= 1.7.2"
|
|
24
|
+
|
|
22
25
|
s.add_dependency "chunky_png", "~> 1.4"
|
|
26
|
+
s.add_dependency "concurrent-ruby", "~> 1.3.5"
|
|
23
27
|
s.add_dependency "rice", "~> 4.5"
|
|
24
28
|
|
|
25
29
|
s.required_ruby_version = ">= 3.0.0"
|
|
@@ -3,19 +3,40 @@ module Contrek
|
|
|
3
3
|
class Bitmap
|
|
4
4
|
include Painting
|
|
5
5
|
|
|
6
|
-
def scan
|
|
7
|
-
x =
|
|
6
|
+
def scan(start_x: 0, end_x: w)
|
|
7
|
+
x = start_x
|
|
8
8
|
y = 0
|
|
9
9
|
loop do
|
|
10
10
|
yield x, y, value_at(x, y)
|
|
11
11
|
x += 1
|
|
12
|
-
if x ==
|
|
13
|
-
x =
|
|
12
|
+
if x == end_x
|
|
13
|
+
x = start_x
|
|
14
14
|
y += 1
|
|
15
15
|
break if y == h
|
|
16
16
|
end
|
|
17
17
|
end
|
|
18
18
|
end
|
|
19
|
+
|
|
20
|
+
def copy_rect(src_bitmap:, src_x:, src_y:, dest_x:, dest_y:, rect_w:, rect_h:)
|
|
21
|
+
rect_h.times do |hp|
|
|
22
|
+
rect_w.times do |wp|
|
|
23
|
+
src_pos_x = src_x + wp
|
|
24
|
+
src_pos_y = src_y + hp
|
|
25
|
+
src_color = src_bitmap.value_at(src_pos_x, src_pos_y)
|
|
26
|
+
dest_pos_x = dest_x + wp
|
|
27
|
+
dest_pos_y = dest_y + hp
|
|
28
|
+
value_set(dest_pos_x, dest_pos_y, src_color)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def clear!(color: " ")
|
|
34
|
+
w.times do |x|
|
|
35
|
+
h.times do |y|
|
|
36
|
+
value_set(x, y, color)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
19
40
|
end
|
|
20
41
|
end
|
|
21
42
|
end
|
|
@@ -28,6 +28,35 @@ module Contrek
|
|
|
28
28
|
|
|
29
29
|
@raw[y * @module + x] = value
|
|
30
30
|
end
|
|
31
|
+
|
|
32
|
+
def dup!
|
|
33
|
+
ChunkyBitmap.new(@raw, @module)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def draw_polygons(polygons)
|
|
37
|
+
polygons.each do |polygon|
|
|
38
|
+
[[:outer, "o"], [:inner, "i"]].each do |side, color|
|
|
39
|
+
sequences = polygon[side]
|
|
40
|
+
sequences = [sequences] if side == :outer
|
|
41
|
+
sequences.each do |sequence|
|
|
42
|
+
sequence.each do |position|
|
|
43
|
+
value_set(position[:x], position[:y], color)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def to_terminal
|
|
51
|
+
puts " " + (0...@module).map { |i| (i % 10).to_s }.join
|
|
52
|
+
n = 0
|
|
53
|
+
@raw.scan(/.{1,#{@module}}/).each do |line|
|
|
54
|
+
puts n.to_s + " " + line
|
|
55
|
+
n += 1
|
|
56
|
+
n = 0 if n >= 10
|
|
57
|
+
end
|
|
58
|
+
puts
|
|
59
|
+
end
|
|
31
60
|
end
|
|
32
61
|
end
|
|
33
62
|
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
module Contrek
|
|
2
|
+
module Concurrent
|
|
3
|
+
class ClippedPolygonFinder < Finder::PolygonFinder
|
|
4
|
+
attr_reader :start_x, :end_x
|
|
5
|
+
|
|
6
|
+
def initialize(bitmap:, matcher:, start_x:, end_x:, options: {})
|
|
7
|
+
@start_x = start_x
|
|
8
|
+
@end_x = end_x
|
|
9
|
+
super(bitmap, matcher, nil, options)
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
module Contrek
|
|
2
|
+
module Concurrent
|
|
3
|
+
class Cluster
|
|
4
|
+
attr_reader :tiles, :hub
|
|
5
|
+
|
|
6
|
+
def initialize(finder:, height:, width:)
|
|
7
|
+
@finder = finder
|
|
8
|
+
@tiles = []
|
|
9
|
+
@hub = Hub.new(height:, width:)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def add(tile)
|
|
13
|
+
last_tile = @tiles.last
|
|
14
|
+
last_tile.next = last_tile.circular_next = tile if last_tile
|
|
15
|
+
@tiles << tile
|
|
16
|
+
tile.prev = last_tile
|
|
17
|
+
tile.circular_next = last_tile
|
|
18
|
+
tile.cluster = self
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Here, two tiles are merged into a single one. The tile is assigned new shapes composed of the detected
|
|
22
|
+
# outer sequences and both the old and new inner ones. Shapes that are not in the contact area are directly
|
|
23
|
+
# reassigned unchanged to the new shape.
|
|
24
|
+
def merge_tiles!
|
|
25
|
+
tot_inner = 0
|
|
26
|
+
tot_outer = 0
|
|
27
|
+
|
|
28
|
+
new_shapes = []
|
|
29
|
+
tot_outer += Benchmark.measure do
|
|
30
|
+
@tiles.each do |tile|
|
|
31
|
+
tile.shapes.each do |shape|
|
|
32
|
+
next if shape.outer_polyline.on?(Polyline::TRACKED_OUTER) || shape.outer_polyline.width == 0
|
|
33
|
+
if shape.outer_polyline.boundary?
|
|
34
|
+
shape.outer_polyline.partition!
|
|
35
|
+
shape.outer_polyline.precalc!
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end.real
|
|
40
|
+
|
|
41
|
+
@tiles.each do |tile|
|
|
42
|
+
tile.shapes.each do |shape|
|
|
43
|
+
next if shape.outer_polyline.on?(Polyline::TRACKED_OUTER) || shape.outer_polyline.width == 0
|
|
44
|
+
|
|
45
|
+
if shape.outer_polyline.boundary? && shape.outer_polyline.next_tile_eligible_shapes.any?
|
|
46
|
+
new_outer = nil
|
|
47
|
+
new_inners = shape.inner_polylines
|
|
48
|
+
cursor = Cursor.new(cluster: self, shape: shape)
|
|
49
|
+
|
|
50
|
+
tot_outer += Benchmark.measure do
|
|
51
|
+
new_outer = cursor.join_outers!
|
|
52
|
+
end.real
|
|
53
|
+
tot_inner += Benchmark.measure do
|
|
54
|
+
new_inner_seq = cursor.join_inners!(new_outer)
|
|
55
|
+
new_inners += new_inner_seq if new_inner_seq.any?
|
|
56
|
+
new_inners += cursor.orphan_inners
|
|
57
|
+
end.real
|
|
58
|
+
|
|
59
|
+
new_shapes << Shape.new.tap do |shape|
|
|
60
|
+
polyline = Polyline.new(tile: tile, polygon: new_outer.to_a)
|
|
61
|
+
shape.outer_polyline = polyline
|
|
62
|
+
polyline.shape = shape
|
|
63
|
+
shape.inner_polylines = new_inners
|
|
64
|
+
end
|
|
65
|
+
else
|
|
66
|
+
new_shapes << shape
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
past_tot_outer = @tiles.first.benchmarks[:outer] + @tiles.last.benchmarks[:outer]
|
|
71
|
+
past_tot_inner = @tiles.first.benchmarks[:inner] + @tiles.last.benchmarks[:inner]
|
|
72
|
+
|
|
73
|
+
tile = Tile.new(
|
|
74
|
+
finder: @finder,
|
|
75
|
+
start_x: @tiles.first.start_x,
|
|
76
|
+
end_x: @tiles.last.end_x,
|
|
77
|
+
benchmarks: {outer: tot_outer + past_tot_outer, inner: tot_inner + past_tot_inner},
|
|
78
|
+
name: @tiles.first.name + @tiles.last.name
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
tile.assign_shapes!(new_shapes)
|
|
82
|
+
tile
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
module Contrek
|
|
2
|
+
module Concurrent
|
|
3
|
+
class Cursor
|
|
4
|
+
attr_reader :orphan_inners
|
|
5
|
+
def initialize(cluster:, shape:)
|
|
6
|
+
@polylines_sequence = []
|
|
7
|
+
@cluster = cluster
|
|
8
|
+
@outer_polyline = shape.outer_polyline
|
|
9
|
+
@orphan_inners = []
|
|
10
|
+
@shapes = [shape]
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def inspect
|
|
14
|
+
self.class
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Given the initial polyline, draw its outer boundary, possibly extending into
|
|
18
|
+
# adjacent polylines, and then connect them. At the end, @polylines_sequence
|
|
19
|
+
# contains the merged polylines. Returns a new resulting polyline.
|
|
20
|
+
def join_outers!
|
|
21
|
+
seq_log = []
|
|
22
|
+
@polylines_sequence << @outer_polyline
|
|
23
|
+
|
|
24
|
+
outer_joined_polyline = Sequence.new
|
|
25
|
+
|
|
26
|
+
traverse_outer(@outer_polyline.parts.first,
|
|
27
|
+
seq_log,
|
|
28
|
+
@polylines_sequence,
|
|
29
|
+
@shapes,
|
|
30
|
+
outer_joined_polyline)
|
|
31
|
+
outer_joined_polyline.pop! if outer_joined_polyline.head.payload == outer_joined_polyline.tail.payload &&
|
|
32
|
+
@cluster.tiles.first.left? && @cluster.tiles.last.right?
|
|
33
|
+
|
|
34
|
+
@polylines_sequence.uniq!
|
|
35
|
+
|
|
36
|
+
@polylines_sequence.each { |c| c.turn_on(Polyline::TRACKED_OUTER) }
|
|
37
|
+
(@shapes - [@outer_polyline.shape]).each do |shape|
|
|
38
|
+
@orphan_inners += shape.inner_polylines
|
|
39
|
+
shape.clear_inner!
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
outer_joined_polyline
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def join_inners!(outer_seq)
|
|
46
|
+
# search for missing sequence to sew
|
|
47
|
+
missing_shapes = []
|
|
48
|
+
@cluster.tiles.each do |tile|
|
|
49
|
+
tile.shapes.each do |shape|
|
|
50
|
+
next if shape.outer_polyline.on?(Polyline::TRACKED_OUTER) ||
|
|
51
|
+
shape.outer_polyline.on?(Polyline::TRACKED_INNER) ||
|
|
52
|
+
!shape.outer_polyline.boundary? ||
|
|
53
|
+
@polylines_sequence.include?(shape.outer_polyline)
|
|
54
|
+
missing_shapes << shape
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
@polylines_sequence.each do |polyline|
|
|
58
|
+
missing_shapes.each do |missing_shape|
|
|
59
|
+
outer_polyline = missing_shape.outer_polyline
|
|
60
|
+
if (intersection = polyline.intersection(outer_polyline)).any?
|
|
61
|
+
inject_sequences_left, inject_sequences_right = polyline.sew!(intersection, outer_polyline)
|
|
62
|
+
combine!(inject_sequences_right, inject_sequences_left).each do |sewn_sequence|
|
|
63
|
+
sewn_sequence.uniq!
|
|
64
|
+
@orphan_inners << sewn_sequence if sewn_sequence.size > 1 && sewn_sequence.map { |c| c[:x] }.uniq.size > 1 # segmenti non sono ammessi, solo aree
|
|
65
|
+
end
|
|
66
|
+
outer_polyline.clear!
|
|
67
|
+
outer_polyline.turn_on(Polyline::TRACKED_OUTER)
|
|
68
|
+
outer_polyline.turn_on(Polyline::TRACKED_INNER)
|
|
69
|
+
@orphan_inners += missing_shape.inner_polylines
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
retme = collect_inner_sequences(outer_seq)
|
|
75
|
+
|
|
76
|
+
@polylines_sequence.each do |polyline|
|
|
77
|
+
polyline.turn_on(Polyline::TRACKED_INNER)
|
|
78
|
+
end
|
|
79
|
+
retme
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
private
|
|
83
|
+
|
|
84
|
+
# rubocop:disable Lint/NonLocalExitFromIterator
|
|
85
|
+
def traverse_outer(act_part, all_parts, polylines, shapes, outer_joined_polyline)
|
|
86
|
+
all_parts << act_part if all_parts.last != act_part
|
|
87
|
+
shapes << act_part.polyline.shape if !shapes.include?(act_part.polyline.shape)
|
|
88
|
+
|
|
89
|
+
if act_part.is?(Part::EXCLUSIVE)
|
|
90
|
+
return if act_part.size == 0
|
|
91
|
+
while (position = act_part.next_position)
|
|
92
|
+
return if outer_joined_polyline.size > 1 &&
|
|
93
|
+
outer_joined_polyline.head.payload == position.payload &&
|
|
94
|
+
act_part == all_parts.first
|
|
95
|
+
outer_joined_polyline.add(position)
|
|
96
|
+
end
|
|
97
|
+
else
|
|
98
|
+
while (new_position = act_part.iterator)
|
|
99
|
+
return if outer_joined_polyline.size > 1 &&
|
|
100
|
+
outer_joined_polyline.head.payload == new_position.payload &&
|
|
101
|
+
act_part == all_parts.first
|
|
102
|
+
outer_joined_polyline.add(Position.new(position: new_position.payload, hub: @cluster.hub))
|
|
103
|
+
|
|
104
|
+
act_part.polyline.next_tile_eligible_shapes.each do |shape|
|
|
105
|
+
if (part = shape.outer_polyline.find_first_part_by_position(new_position))
|
|
106
|
+
if all_parts[-2] != part
|
|
107
|
+
if all_parts.size >= 2
|
|
108
|
+
map = all_parts[-2..].map(&:type).uniq
|
|
109
|
+
break if map.size == 1 && map.first == Part::SEAM
|
|
110
|
+
end
|
|
111
|
+
polylines << part.polyline.shape.outer_polyline
|
|
112
|
+
part.next_position(new_position.payload)
|
|
113
|
+
traverse_outer(part, all_parts, polylines, shapes, outer_joined_polyline)
|
|
114
|
+
return
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
act_part.passes += 1
|
|
119
|
+
act_part.next_position
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
next_part = act_part.circular_next
|
|
123
|
+
next_part.rewind!
|
|
124
|
+
traverse_outer(act_part.circular_next, all_parts, polylines, shapes, outer_joined_polyline)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def collect_inner_sequences(outer_seq)
|
|
128
|
+
return_sequences = []
|
|
129
|
+
@polylines_sequence.each do |polyline|
|
|
130
|
+
polyline.parts.each do |part|
|
|
131
|
+
if part.innerable?
|
|
132
|
+
all_parts = []
|
|
133
|
+
bounds = {min: polyline.max_y, max: 0}
|
|
134
|
+
traverse_inner(part, all_parts, bounds)
|
|
135
|
+
range_of_bounds = (bounds[:min]..bounds[:max])
|
|
136
|
+
|
|
137
|
+
retme_sequence = Sequence.new
|
|
138
|
+
all_parts.map do |part|
|
|
139
|
+
part.touch!
|
|
140
|
+
|
|
141
|
+
retme_sequence.move_from(part) do |pos|
|
|
142
|
+
next false if part.is?(Part::ADDED) && !(range_of_bounds === pos.payload[:y])
|
|
143
|
+
!(polyline.tile.tg_border?(pos.payload) && pos.end_point.queues.include?(outer_seq))
|
|
144
|
+
end
|
|
145
|
+
end.flatten
|
|
146
|
+
raw_sequence = retme_sequence.to_a
|
|
147
|
+
return_sequences << raw_sequence if raw_sequence.map { _1[:x] }.uniq.take(2).size > 1
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
return_sequences
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def traverse_inner(act_part, all_parts, bounds)
|
|
155
|
+
return if act_part == all_parts.first
|
|
156
|
+
|
|
157
|
+
if act_part.size > 0
|
|
158
|
+
min_y, max_y = act_part.to_a.minmax_by { |p| p[:y] }
|
|
159
|
+
bounds[:min] = [bounds[:min], min_y[:y]].min
|
|
160
|
+
bounds[:max] = [bounds[:max], max_y[:y]].max
|
|
161
|
+
end
|
|
162
|
+
if act_part.innerable?
|
|
163
|
+
all_parts << act_part
|
|
164
|
+
while (act_part = act_part.next)
|
|
165
|
+
if act_part.innerable?
|
|
166
|
+
all_parts << act_part
|
|
167
|
+
else
|
|
168
|
+
act_part.polyline.next_tile_eligible_shapes.each do |shape|
|
|
169
|
+
shape.outer_polyline.parts.each do |dest_part|
|
|
170
|
+
next if dest_part.trasmuted
|
|
171
|
+
if dest_part.intersect_part?(act_part)
|
|
172
|
+
link_seq = duplicates_intersection(dest_part, act_part)
|
|
173
|
+
if link_seq.any?
|
|
174
|
+
ins_part = Part.new(Part::ADDED, act_part.polyline)
|
|
175
|
+
link_seq.each { |pos| ins_part.add(Position.new(position: pos, hub: @cluster.hub)) }
|
|
176
|
+
all_parts << ins_part
|
|
177
|
+
end
|
|
178
|
+
traverse_inner(dest_part.circular_next, all_parts, bounds)
|
|
179
|
+
return
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
all_parts << act_part if act_part.is?(Part::SEAM)
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
elsif act_part.next
|
|
187
|
+
traverse_inner(act_part.next, all_parts, bounds)
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
# rubocop:enable Lint/NonLocalExitFromIterator
|
|
191
|
+
|
|
192
|
+
# The sequences should contain inverted parts, e.g., 4,5,6,6,5,4. These parts must be
|
|
193
|
+
# removed from the sequences, and the remaining elements are compared. Only the
|
|
194
|
+
# elements that appear once between the two sequences are kept. This represents
|
|
195
|
+
# a connection between parts inserted afterwards.
|
|
196
|
+
# TODO evaluate the adoption of remove_adjacent_pairs!
|
|
197
|
+
def duplicates_intersection(part_a, part_b)
|
|
198
|
+
a1 = part_a.inverts ? part_a.remove_adjacent_pairs : part_a.to_a
|
|
199
|
+
b1 = part_b.inverts ? part_b.remove_adjacent_pairs : part_b.to_a
|
|
200
|
+
|
|
201
|
+
(a1 - b1) + (b1 - a1)
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# example
|
|
205
|
+
# a = [[A],[B,C]]
|
|
206
|
+
# b = [[D],[E,F]]
|
|
207
|
+
# res = [[D,B,C],[E,F,A]]
|
|
208
|
+
def combine!(seqa, seqb)
|
|
209
|
+
rets = []
|
|
210
|
+
[seqa.count, seqb.count].min.times do
|
|
211
|
+
last = seqa.pop
|
|
212
|
+
first = seqb.shift
|
|
213
|
+
rets << first + last
|
|
214
|
+
end
|
|
215
|
+
rets
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
end
|