contrek 1.1.7 → 1.1.9

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 (50) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +13 -1
  3. data/Gemfile.lock +1 -1
  4. data/README.md +50 -23
  5. data/ext/cpp_polygon_finder/PolygonFinder/CMakeLists.txt +19 -11
  6. data/ext/cpp_polygon_finder/PolygonFinder/clean.sh +28 -0
  7. data/ext/cpp_polygon_finder/PolygonFinder/examples/example.cpp +4 -2
  8. data/ext/cpp_polygon_finder/PolygonFinder/src/Tests.cpp +1 -1
  9. data/ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/Node.h +2 -0
  10. data/ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/NodeCluster.cpp +66 -61
  11. data/ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/Primitives.h +14 -0
  12. data/ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/concurrent/ClippedPolygonFinder.h +1 -1
  13. data/ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/concurrent/Cluster.cpp +86 -23
  14. data/ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/concurrent/Cluster.h +3 -0
  15. data/ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/concurrent/Cursor.cpp +24 -57
  16. data/ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/concurrent/Cursor.h +7 -13
  17. data/ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/concurrent/Finder.cpp +12 -9
  18. data/ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/concurrent/Finder.h +2 -1
  19. data/ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/concurrent/InnerPolyline.cpp +55 -0
  20. data/ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/concurrent/InnerPolyline.h +32 -0
  21. data/ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/concurrent/Merger.cpp +1 -1
  22. data/ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/concurrent/Part.cpp +38 -23
  23. data/ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/concurrent/Part.h +1 -2
  24. data/ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/concurrent/Polyline.cpp +21 -0
  25. data/ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/concurrent/Polyline.h +4 -0
  26. data/ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/concurrent/Queueable.h +1 -0
  27. data/ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/concurrent/Sequence.cpp +14 -0
  28. data/ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/concurrent/Sequence.h +14 -0
  29. data/ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/concurrent/Shape.cpp +15 -6
  30. data/ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/concurrent/Shape.h +11 -3
  31. data/ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/concurrent/ShapePool.cpp +42 -0
  32. data/ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/concurrent/ShapePool.h +34 -0
  33. data/ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/concurrent/Tile.cpp +72 -11
  34. data/ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/concurrent/Tile.h +9 -1
  35. data/ext/cpp_polygon_finder/cpp_polygon_finder.cpp +4 -0
  36. data/lib/contrek/finder/concurrent/cluster.rb +58 -9
  37. data/lib/contrek/finder/concurrent/cursor.rb +11 -19
  38. data/lib/contrek/finder/concurrent/finder.rb +8 -2
  39. data/lib/contrek/finder/concurrent/inner_polyline.rb +49 -0
  40. data/lib/contrek/finder/concurrent/part.rb +26 -12
  41. data/lib/contrek/finder/concurrent/polyline.rb +46 -0
  42. data/lib/contrek/finder/concurrent/queueable.rb +0 -17
  43. data/lib/contrek/finder/concurrent/sequence.rb +21 -0
  44. data/lib/contrek/finder/concurrent/shape.rb +29 -2
  45. data/lib/contrek/finder/concurrent/tile.rb +36 -7
  46. data/lib/contrek/finder/node.rb +3 -1
  47. data/lib/contrek/finder/node_cluster.rb +56 -48
  48. data/lib/contrek/version.rb +1 -1
  49. data/lib/contrek.rb +1 -0
  50. metadata +9 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d1025551133df8cf6974f60a7376965a4d0f05aa67e3056d7fdc80d9238c0b15
4
- data.tar.gz: 340f67ea51692d68c66c8c50d95c85b424dfa3da556e886a778139b9e15163c6
3
+ metadata.gz: 87e8665985a5c9b3818f5f405609c8bf0f9d81dfaa197054e0801acdc60dc56c
4
+ data.tar.gz: 450c56eda5bd65ab936fcd52b37e3f8edbc2ec0ac852aa0cc4c052ee6d81ab7e
5
5
  SHA512:
6
- metadata.gz: '08df976898407e0db4decf010da33a337b6a405232154676ae4d740c7872812e48c5f9f5943b3040b2d4dd0015e8d77b729c4c3ba332497a2c3aba4f642ab0d0'
7
- data.tar.gz: dac48e9cf23557b132301481a262f4a536301aa3bcc5c86796ff7b7061a629e7fd4588ceb19b6bc962bfbee4fbd2e2414c7620bafd424b770ca1c265fc077d64
6
+ metadata.gz: e4cc2ea0d61819436e922eacdcad551658deceac1b5a907cea03d306de65fa2f71b96018acf6fcc6f88163775cabf0ba9b8d6ded3e9067dfcf3cc3e70d3e3e6d
7
+ data.tar.gz: ca933647465baca8a294176a1fbbc336bf1956c8eebab2cf48dd45ac4e55ba55ea84b00d38cbdd9ee0cf012b332684b8902c5e98a8cbaaafbfc846b374a12801
data/CHANGELOG.md CHANGED
@@ -18,7 +18,7 @@ All notable changes to this project will be documented in this file.
18
18
  ### Added
19
19
  - Added C++ multithreading supports
20
20
  - Optimized old just present C++ code
21
- - Removed Png++ dependency in place of libspn
21
+ - Removed Png++ dependency in place of libspng
22
22
 
23
23
  ## [1.0.7] - 2026-01-10
24
24
  ### Added
@@ -68,3 +68,15 @@ All notable changes to this project will be documented in this file.
68
68
  ### Changed
69
69
  - Added strict_bounds tracing mode: enables more accurate shape tracing by strictly adhering to pixel boundaries.
70
70
  - Topological Consistency Fixes: improved the Topologically Consistent Merging algorithm to support progressive polygon tracing during sequential data streaming.
71
+
72
+ ## [1.1.7] - 2026-03-23
73
+ ### Changed
74
+ - Removed docs directory from gem.
75
+
76
+ ## [1.1.8] - 2026-04-19
77
+ ### Changed
78
+ - Treemap now available on multiprocessing side too.
79
+
80
+ ## [1.1.9] - 2026-04-24
81
+ ### Changed
82
+ - Improved the internal parts joining algorithm, which was imprecise in some circumstances.
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- contrek (1.1.6)
4
+ contrek (1.1.9)
5
5
  chunky_png (~> 1.4)
6
6
  concurrent-ruby (~> 1.3.5)
7
7
  rice (= 4.5.0)
data/README.md CHANGED
@@ -29,24 +29,24 @@ The core strength of Contrek is its **Topologically Consistent Merging** algorit
29
29
 
30
30
  <table>
31
31
  <tr>
32
- <td><img src="docs/images/stripes/whole_0.png" alt="Contour tracing stripe 0" width="50%"></td>
33
- <td rowspan="4" align="center">
34
- <img src="docs/images/stripes/whole.png" alt="Contour tracing stripe 0" width="50%">
32
+ <td width="50%" style="padding: 0; background-color: white;">
33
+ <img src="docs/images/stripes/whole_0.png" width="100%"><br>
34
+ <img src="docs/images/stripes/whole_256.png" width="100%"><br>
35
+ <img src="docs/images/stripes/whole_512.png" width="100%"><br>
36
+ <img src="docs/images/stripes/whole_768.png" width="100%">
37
+ </td>
38
+ <td width="50%" align="center" style="vertical-align: middle; background-color: white;">
39
+ <strong>Full Topological Reconstruction</strong><br><br>
40
+ <img src="docs/images/stripes/whole.png" width="90%">
35
41
  </td>
36
42
  </tr>
37
43
  <tr>
38
- <td><img src="docs/images/stripes/whole_256.png" alt="Contour tracing stripe 0" width="50%"></td>
39
- </tr>
40
- <tr>
41
- <td><img src="docs/images/stripes/whole_512.png" alt="Contour tracing stripe 0" width="50%"></td>
42
- </tr>
43
- <tr>
44
- <td><img src="docs/images/stripes/whole_768.png" alt="Contour tracing stripe 0" width="50%"></td>
44
+ <td colspan="2" align="center" style="background-color: white;">
45
+ <em><b>Left:</b> Image split into 4 independent memory buffers (stripes).</em><br>
46
+ <em><b>Right:</b> Contrek ensures <b>perfect topological continuity</b> during merging.</em><br>
47
+ 🔴 <b>Red:</b> Outer contours &nbsp;&nbsp; | &nbsp;&nbsp; 🟢 <b>Green:</b> Inner zones
48
+ </td>
45
49
  </tr>
46
- <tr><td colspan="2"><em><b>Left:</b> The image is split into 4 independent memory buffers (stripes).
47
- <br><b>Right:</b> Contrek ensures <b>perfect topological continuity</b> during merging.</em>
48
- <br><span style="color: #d32f2f;">■</span> <b>Red:</b> Outer contours |
49
- <span style="color: #2e7d32;">■</span> <b>Dark Green:</b> Inner zones</td></tr>
50
50
  </table>
51
51
 
52
52
  ## Prerequisites
@@ -167,8 +167,6 @@ Regarding multithreading:
167
167
 
168
168
  - 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.
169
169
 
170
- - The treemap option is currently ignored (multithreaded treemap support will be introduced in upcoming revisions).
171
-
172
170
  By not declaring native option CPP Multithreading optimized code is used. In the above example a [105 MP image](spec/files/images/sample_10240x10240.png) is examined by 4 threads working on 4 tiles (total compute time about 1.2 secs with image load).
173
171
 
174
172
  ```ruby
@@ -290,7 +288,7 @@ result.metadata
290
288
 
291
289
  ## Treemap
292
290
 
293
- The treemap is a data structure that represents the containment hierarchy of polygons. For each polygon, it defines the parent polygon in which it is contained and its relative position among siblings. This structure allows reconstruction of the inclusion tree and determination of nesting relationships between geometries. Consider the above image
291
+ The treemap is a data structure that represents the containment hierarchy of polygons. For each polygon, it defines the parent polygon in which it is contained and the index of the parent inner sequence in which is placed. This structure allows reconstruction of the inclusion tree and determination of nesting relationships between geometries. The treemap indexes are referred to positions inside the array of polygons returned by result.points. Consider the above image traced clockwise (o).
294
292
 
295
293
  ```ruby
296
294
  "AAAAAAAAAAAAAAAAAAAAAA" \
@@ -311,8 +309,8 @@ result.metadata[:treemap]
311
309
 
312
310
  [[-1, -1], # A
313
311
  [0, 0], # B
314
- [1, 0], # C
315
- [1, 1]] # D
312
+ [1, 1], # C
313
+ [1, 0]] # D
316
314
  ```
317
315
 
318
316
  There are four polygons (`A`, `B`, `C`, and `D`).
@@ -321,13 +319,42 @@ The order matches the one provided in `result.polygons`.
321
319
  Each entry has the structure:
322
320
 
323
321
  ```
324
- [parent_index, position]
322
+ [parent_index, index_of_parent_inner_sequence]
325
323
  ```
326
324
 
327
325
  - Polygon **A** (index `0`) has no parent and is represented as `[-1, -1]`
328
- - Polygon **B** is contained in **A** and is its first child → `[0, 0]`
329
- - Polygon **C** is contained in **B** and is its first child → `[1, 0]`
330
- - Polygon **D** is also contained in **B** and is its second child → `[1, 1]`
326
+ - Polygon **B** is contained in **A** in the first (0) and unique inner sequence of A → `[0, 0]`
327
+ - Polygon **C** is contained in **B** and inside its second (1) inner sequence → `[1, 1]`
328
+ - Polygon **D** is also contained in **B** but in the first (0) of its inner sequences → `[1, 0]`
329
+
330
+ **C** is inside the **second** B inner sequence and **D** is inside the **first** B inner sequence because when the contour is traced clockwise the inner sequences are listed from right to left (left to right when anti-clockwise).
331
+
332
+ Consider this sequence
333
+ ```ruby
334
+ "AAAAAAAAAAAAAAA " \
335
+ "A A " \
336
+ "A BBBBBBBBBBB A " \
337
+ "A B B A " \
338
+ "A B CC DD E B A " \
339
+ "A B B A " \
340
+ "A BBBBBBBBBBB A " \
341
+ "A A " \
342
+ "AAAAAAAAAAAAAAA "
343
+ ```
344
+
345
+ will get
346
+
347
+ ```ruby
348
+ result.metadata[:treemap]
349
+
350
+ [[-1, -1], # A
351
+ [0, 0], # B
352
+ [1, 0], # C
353
+ [1, 0], # D
354
+ [1, 0]] # E
355
+ ```
356
+ C, D and E will get the same pair (**[1, 0]**) because are all placed inside the first (0) inner sequence of B.
357
+
331
358
 
332
359
  ## Multithreaded approach
333
360
 
@@ -3,20 +3,28 @@ project(ContrekCore C CXX)
3
3
 
4
4
  set(CMAKE_CXX_STANDARD 17)
5
5
  set(CMAKE_C_STANDARD 11)
6
-
7
- set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra -pthread -march=native -DNDEBUG -Ofast -flto")
8
- set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -O3 -march=native -fPIC -DNDEBUG")
9
- set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -pthread -flto=auto -Wl,--no-as-needed")
10
-
11
- find_package(ZLIB REQUIRED)
12
- find_library(TCMALLOC_LIB tcmalloc) # <--- Cerca tcmalloc nel sistema
13
-
14
- if(TCMALLOC_LIB)
15
- message(STATUS "Contrek: tcmalloc found in ${TCMALLOC_LIB}")
6
+ if(CMAKE_BUILD_TYPE STREQUAL "Debug")
7
+ message(STATUS "Contrek: debuginning mode (ASan + GDB)")
8
+ set(CMAKE_CXX_FLAGS_DEBUG "-g -O0 -fsanitize=address -fno-omit-frame-pointer")
9
+ set(CMAKE_C_FLAGS_DEBUG "-g -O0 -fsanitize=address -fno-omit-frame-pointer")
10
+ set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -fsanitize=address")
11
+ set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -fsanitize=address")
12
+ list(FILTER CMAKE_CXX_FLAGS EXCLUDE REGEX "-DNDEBUG")
13
+ list(FILTER CMAKE_C_FLAGS EXCLUDE REGEX "-DNDEBUG")
16
14
  else()
17
- message(WARNING "Contrek: tcmalloc not found; standard one will be used.")
15
+ set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra -pthread -march=native -DNDEBUG -Ofast -flto")
16
+ set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -O3 -march=native -fPIC -DNDEBUG")
17
+ set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -pthread -flto=auto -Wl,--no-as-needed")
18
+ find_library(TCMALLOC_LIB tcmalloc)
19
+ if(TCMALLOC_LIB)
20
+ message(STATUS "Contrek: tcmalloc found in ${TCMALLOC_LIB}")
21
+ else()
22
+ message(WARNING "Contrek: tcmalloc not found; standard one will be used.")
23
+ endif()
18
24
  endif()
19
25
 
26
+ find_package(ZLIB REQUIRED)
27
+
20
28
  file(GLOB_RECURSE CPP_SOURCES "*.cpp")
21
29
  file(GLOB_RECURSE C_SOURCES "*.c")
22
30
 
@@ -0,0 +1,28 @@
1
+ #!/bin/bash
2
+
3
+ # Get the script's directory (the project root)
4
+ ROOT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
5
+ BUILD_DIR="$ROOT_DIR/build"
6
+
7
+ echo "--- Surgical cleanup initiated in: $BUILD_DIR ---"
8
+
9
+ # Check if the build directory exists
10
+ if [ ! -d "$BUILD_DIR" ]; then
11
+ echo "Build directory does not exist. Nothing to clean."
12
+ exit 0
13
+ fi
14
+
15
+ # Enter the build directory to operate safely
16
+ cd "$BUILD_DIR" || exit
17
+
18
+ # Remove critical CMake files and object files without deleting the build folder itself
19
+ echo "Removing cache files and build artifacts..."
20
+ rm -rf CMakeCache.txt CMakeFiles/ cmake_install.cmake Makefile bin/ lib/ examples/
21
+
22
+ # Optional: Recursively remove all .o, .a, and .so files if present
23
+ find . -name "*.o" -delete
24
+ find . -name "*.a" -delete
25
+ find . -name "*.so" -delete
26
+
27
+ echo "--- Cleanup complete. The build directory is now pristine. ---"
28
+ echo "You can now run: cmake -DBUILD_EXAMPLES=ON .."
@@ -24,7 +24,7 @@ void run_test() {
24
24
  // test_suite.test_f();
25
25
  // test_suite.test_g();
26
26
  // test_suite.test_h();
27
- test_suite.test_i();
27
+ // test_suite.test_i();
28
28
  std::cout << "compute time =" << cpu_timer.stop() << std::endl;
29
29
  }
30
30
 
@@ -33,11 +33,13 @@ int main() {
33
33
  cfg.threads = 4;
34
34
  cfg.tiles = 4;
35
35
  cfg.compress_unique = true;
36
- cfg.connectivity_mode = Contrek::Connectivity::OMNIDIRECTIONAL;
36
+ // cfg.treemap = true;
37
+ // cfg.connectivity_mode = Contrek::Connectivity::OMNIDIRECTIONAL;
37
38
 
38
39
  CpuTimer cpu_timer;
39
40
  cpu_timer.start();
40
41
  std::cout << "--- Start Native Benchmark ---" << std::endl;
42
+ // auto result = Contrek::trace("../images/graphs_1024x1024.png", cfg);
41
43
  auto result = Contrek::trace("../images/sample_10240x10240.png", cfg);
42
44
  result->print_info();
43
45
  std::cout << "Found polygons: " << result->groups << std::endl;
@@ -155,7 +155,7 @@ void Tests::test_d()
155
155
  std::cout << "color =" << color << std::endl;
156
156
  RGBNotMatcher not_matcher(color);
157
157
 
158
- std::vector<std::string> arguments = {"--versus=a", "--compress_uniq"};
158
+ std::vector<std::string> arguments = {"--versus=a", "--compress_uniq", "--treemap"};
159
159
  PolygonFinder pl(&png_bitmap, &not_matcher, nullptr, &arguments);
160
160
  ProcessResult *o = pl.process_info();
161
161
  o->print_info();
@@ -74,4 +74,6 @@ class Node : public Listable {
74
74
  Node(int min_x, int max_x, int y, NodeCluster* cluster, char name);
75
75
  void precalc_tangs_sequences(NodeCluster& cluster);
76
76
  bool processed = false;
77
+ int inner_left_index = -1;
78
+ int inner_right_index = -1;
77
79
  };
@@ -129,86 +129,89 @@ void NodeCluster::plot(int versus) {
129
129
  if ((this->nodes > 0) && (next_node != nullptr))
130
130
  { plot_node(poly.outer, next_node, root_node, versus, poly.bounds);
131
131
  }
132
- this->sequences.push_back(std::move(this->plot_sequence));
133
- this->plot_sequence.clear();
134
132
 
135
- if (poly.outer.size() > 2)
133
+ if (poly.outer.size() >= 2)
136
134
  { this->polygons.push_back(poly);
137
- }
138
-
139
- int index_inner = 0;
140
- while (inner_plot->size() > 0)
141
- { this->plot_sequence.clear();
142
- std::vector<Point*> inner_sequence;
143
- std::list<Node*>::iterator first_i;
144
- Node *first = nullptr;
145
-
146
- Listable *act = inner_plot->first();
147
- do
148
- { if ((reinterpret_cast<Node*>(act))->tangs_count <= 2)
149
- { first = reinterpret_cast<Node*>(act);
150
- break;
151
- }
152
- }while((act = act->data_pointer[inner_plot->get_id()].next) != nullptr);
153
-
154
- if (first == nullptr) first = reinterpret_cast<Node*>(inner_plot->first());
155
-
156
- plot_sequence.push_back(first);
157
- inner_plot->remove(first);
158
- root_nodes->remove(first);
159
-
160
- first->inner_index = index_inner;
161
-
162
- Node* next_node = nullptr;
163
-
164
- if (first->get_trackmax()) {
165
- if (inner_v == Node::A) {
166
- if (first->upper_end >= 0) next_node = &this->vert_nodes[first->y + Node::T_UP][first->upper_start];
135
+ this->sequences.push_back(std::move(this->plot_sequence));
136
+
137
+ int index_inner = 0;
138
+ while (inner_plot->size() > 0)
139
+ { this->plot_sequence.clear();
140
+ std::vector<Point*> inner_sequence;
141
+ std::list<Node*>::iterator first_i;
142
+ Node *first = nullptr;
143
+
144
+ Listable *act = inner_plot->first();
145
+ do
146
+ { if ((reinterpret_cast<Node*>(act))->tangs_count <= 2)
147
+ { first = reinterpret_cast<Node*>(act);
148
+ break;
149
+ }
150
+ }while((act = act->data_pointer[inner_plot->get_id()].next) != nullptr);
151
+
152
+ if (first == nullptr) first = reinterpret_cast<Node*>(inner_plot->first());
153
+
154
+ plot_sequence.push_back(first);
155
+ inner_plot->remove(first);
156
+ root_nodes->remove(first);
157
+
158
+ first->inner_index = index_inner;
159
+
160
+ Node* next_node = nullptr;
161
+
162
+ if (first->get_trackmax()) {
163
+ if (inner_v == Node::A) {
164
+ if (first->upper_end >= 0) next_node = &this->vert_nodes[first->y + Node::T_UP][first->upper_start];
165
+ } else {
166
+ if (first->lower_end >= 0) next_node = &this->vert_nodes[first->y + Node::T_DOWN][first->lower_start];
167
+ }
167
168
  } else {
168
- if (first->lower_end >= 0) next_node = &this->vert_nodes[first->y + Node::T_DOWN][first->lower_start];
169
+ if (inner_v == Node::A) {
170
+ if (first->lower_end >= 0) next_node = &this->vert_nodes[first->y + Node::T_DOWN][first->lower_end];
171
+ } else {
172
+ if (first->upper_end >= 0) next_node = &this->vert_nodes[first->y + Node::T_UP][first->upper_end];
173
+ }
169
174
  }
170
- } else {
171
- if (inner_v == Node::A) {
172
- if (first->lower_end >= 0) next_node = &this->vert_nodes[first->y + Node::T_DOWN][first->lower_end];
173
- } else {
174
- if (first->upper_end >= 0) next_node = &this->vert_nodes[first->y + Node::T_UP][first->upper_end];
175
- }
176
- }
177
175
 
178
- if (next_node != nullptr)
179
- { inner_sequence.push_back(next_node->coords_entering_to(first, inner_v, Node::INNER));
180
- plot_inner_node(inner_sequence, next_node, inner_v, first, root_node);
176
+ if (next_node != nullptr)
177
+ { inner_sequence.push_back(next_node->coords_entering_to(first, inner_v, Node::INNER));
178
+ plot_inner_node(inner_sequence, next_node, inner_v, first, root_node);
179
+ }
180
+ this->polygons.back().inner.push_back(inner_sequence);
181
+ this->inner_plot->grab(this->inner_new);
182
+ index_inner++;
181
183
  }
182
- this->polygons.back().inner.push_back(inner_sequence);
183
- this->inner_plot->grab(this->inner_new);
184
- index_inner++;
184
+ } else {
185
+ this->plot_sequence.clear();
186
+ }
187
+ if (this->options->treemap)
188
+ { this->treemap.push_back(versus == Node::A ? this->test_in_hole_a(root_node) : this->test_in_hole_o(root_node));
185
189
  }
190
+ index_order++;
186
191
  } else {
187
- this->sequences.push_back(std::move(this->plot_sequence));
188
192
  this->plot_sequence.clear();
189
193
  }
190
- // tree
191
- if (this->options->treemap)
192
- { this->treemap.push_back(versus == Node::A ? this->test_in_hole_a(root_node) : this->test_in_hole_o(root_node));
193
- }
194
- index_order++;
195
194
  }
196
195
  }
197
196
 
198
197
  std::pair<int, int> NodeCluster::test_in_hole_a(Node* node)
199
198
  { if (node->outer_index > 0)
200
199
  { int start_left = node->abs_x_index - 1;
200
+ auto& row = this->vert_nodes[node->y];
201
+ int row_size = static_cast<int>(row.size());
202
+ if (start_left < 0 && row_size > 0) {
203
+ start_left = row_size - 1;
204
+ }
201
205
  do {
202
206
  Node* prev = &this->vert_nodes[node->y][start_left];
203
207
  int cindex = prev->outer_index;
204
-
205
208
  if ((cindex < node->outer_index) && (prev->track & Node::IMAX))
206
209
  { unsigned int start_right = node->abs_x_index;
207
210
  unsigned int line_size = this->vert_nodes[node->y].size();
208
211
  while (++start_right != line_size)
209
212
  { Node* tnext = &this->vert_nodes[node->y][start_right];
210
213
  if (tnext->outer_index == cindex) {
211
- if (tnext->track & Node::IMIN) return {cindex, prev->inner_index};
214
+ if (tnext->track & Node::IMIN) return {cindex, prev->inner_right_index == -1 ? prev->inner_left_index : prev->inner_right_index};
212
215
  else return {-1, -1};
213
216
  }
214
217
  }
@@ -222,18 +225,16 @@ std::pair<int, int> NodeCluster::test_in_hole_o(Node* node)
222
225
  { auto& line = this->vert_nodes[node->y];
223
226
  const unsigned int line_size = line.size();
224
227
  if (node->outer_index == 0 || &line.back() == node) return {-1, -1};
225
-
226
228
  unsigned int start_left = node->abs_x_index + 1;
227
229
  do {
228
230
  Node* prev = &line[start_left];
229
231
  int cindex = prev->outer_index;
230
-
231
232
  if (cindex < node->outer_index && (prev->track & Node::IMIN))
232
233
  { int start_right = node->abs_x_index;
233
234
  while (--start_right >= 0) {
234
235
  Node* tnext = &line[start_right];
235
236
  if (tnext->outer_index == cindex) {
236
- if (tnext->track & Node::IMAX) return {cindex, prev->inner_index};
237
+ if (tnext->track & Node::IMAX) return {cindex, prev->inner_left_index == -1 ? prev->inner_right_index : prev->inner_left_index};
237
238
  else return {-1, -1};
238
239
  }
239
240
  }
@@ -247,15 +248,20 @@ void NodeCluster::plot_inner_node(std::vector<Point*>& sequence_coords, Node *no
247
248
  bool strict_bounds = this->options->strict_bounds;
248
249
  while (current_node != nullptr) {
249
250
  current_node->outer_index = start_node->outer_index;
250
- current_node->inner_index = stop_at->inner_index;
251
251
  root_nodes->remove(current_node);
252
252
  inner_plot->remove(current_node);
253
253
 
254
254
  Node *last_node = plot_sequence.back();
255
255
  Node *next_node = current_node->my_next_inner(last_node, versus);
256
-
257
256
  plot_sequence.push_back(current_node);
258
257
 
258
+ bool first_is_max = ((current_node->y > last_node->y) == (versus == Node::A));
259
+ if (first_is_max) {
260
+ if (current_node->inner_right_index == -1) current_node->inner_right_index = stop_at->inner_index;
261
+ } else {
262
+ if (current_node->inner_left_index == -1) current_node->inner_left_index = stop_at->inner_index;
263
+ }
264
+
259
265
  bool plot = true;
260
266
  if (next_node->y == last_node->y) {
261
267
  Node *n;
@@ -273,7 +279,6 @@ void NodeCluster::plot_inner_node(std::vector<Point*>& sequence_coords, Node *no
273
279
  }
274
280
  }
275
281
  } else if (strict_bounds) {
276
- bool first_is_max = ((current_node->y > last_node->y) == (versus == Node::A));
277
282
  sequence_coords.push_back(this->points_pool.acquire((first_is_max ? last_node->max_x : last_node->min_x), current_node->y));
278
283
  sequence_coords.push_back(this->points_pool.acquire((first_is_max ? next_node->min_x : next_node->max_x), current_node->y));
279
284
  }
@@ -0,0 +1,14 @@
1
+ /*
2
+ * Primitives.h
3
+ *
4
+ * Copyright (c) 2025-2026 Emanuele Cesaroni
5
+ *
6
+ * Licensed under the GNU Affero General Public License v3 (AGPLv3).
7
+ * See the LICENSE file in this directory for the full license text.
8
+ */
9
+ #pragma once
10
+
11
+ struct Bounds {
12
+ int min;
13
+ int max;
14
+ };
@@ -14,5 +14,5 @@
14
14
 
15
15
  class ClippedPolygonFinder : public PolygonFinder {
16
16
  public:
17
- ClippedPolygonFinder(Bitmap *bitmap, Matcher *matcher, int bm_start_x, int bm_end_x, std::vector<std::string> *options = nullptr);
17
+ ClippedPolygonFinder(Bitmap *bitmap, Matcher *matcher, int bm_start_x, int bm_end_x, std::vector<std::string>* options = nullptr);
18
18
  };
@@ -49,17 +49,20 @@ void Cluster::list_to_string(std::vector<Point*> list)
49
49
  }
50
50
 
51
51
  Tile* Cluster::merge_tiles() {
52
+ bool treemap = this->finder->options().treemap;
52
53
  double tot_inner = 0;
53
54
  double tot_outer = 0;
54
55
  CpuTimer timer;
55
- std::list<Shape*> shapes;
56
+
57
+ std::list<Shape*> new_shapes;
58
+ std::vector<InnerPolyline*> all_new_inner_polylines;
56
59
 
57
60
  timer.start();
58
61
  for (Tile* tile : tiles_) {
59
62
  for (Shape *shape : tile->shapes()) {
60
63
  if (shape->outer_polyline->is_on(Polyline::TRACKED_OUTER) || shape->outer_polyline->width() == 0) continue;
61
- if (shape->outer_polyline->boundary())
62
- { shape->outer_polyline->partition();
64
+ if (shape->outer_polyline->boundary()) {
65
+ shape->outer_polyline->partition();
63
66
  shape->outer_polyline->precalc();
64
67
  }
65
68
  }
@@ -68,51 +71,111 @@ Tile* Cluster::merge_tiles() {
68
71
 
69
72
  for (Tile* tile : tiles_) {
70
73
  std::list<Shape*>& src = tile->shapes();
71
- for (auto it = src.begin(); it != src.end(); )
72
- { Shape* shape = *it;
73
- if (shape->outer_polyline->is_on(Polyline::TRACKED_OUTER) || shape->outer_polyline->width() == 0)
74
- { it++;
74
+
75
+ for (Shape* shape : src) {
76
+ if (shape->outer_polyline->is_on(Polyline::TRACKED_OUTER) || shape->outer_polyline->width() == 0) {
75
77
  continue;
76
78
  }
77
79
 
78
80
  if (shape->outer_polyline->boundary() && !shape->outer_polyline->next_tile_eligible_shapes().empty()) {
79
- Sequence* new_outer = nullptr;
80
- std::list<std::vector<Point*>> new_inners = shape->inner_polylines;
81
81
  Cursor cursor(*this, shape);
82
+ Sequence* new_outer = nullptr;
83
+
82
84
  timer.start();
83
85
  new_outer = cursor.join_outers();
84
86
  tot_outer += timer.stop();
85
87
 
86
88
  timer.start();
87
- std::vector<Sequence*> new_inner_sequences = cursor.join_inners(new_outer);
89
+ std::vector<InnerPolyline*> new_inners = shape->inner_polylines;
90
+ std::vector<InnerPolyline*> new_inner_polylines = cursor.join_inners(new_outer);
88
91
  tot_inner += timer.stop();
89
92
 
90
- for (Sequence* s : new_inner_sequences) {
91
- new_inners.push_back(s->to_vector());
92
- delete s;
93
+ for (InnerPolyline* inner_polyline : new_inner_polylines) {
94
+ new_inners.push_back(inner_polyline);
95
+ if (treemap) {
96
+ inner_polyline->sequence()->compute_vertical_bounds();
97
+ all_new_inner_polylines.push_back(inner_polyline);
98
+ for (const auto orphan_inner : cursor.orphan_inners()) {
99
+ if (orphan_inner->recombined()) {
100
+ all_new_inner_polylines.push_back(orphan_inner);
101
+ }
102
+ }
103
+ }
93
104
  }
105
+
94
106
  for (auto s : cursor.orphan_inners()) {
95
107
  new_inners.push_back(s);
96
108
  }
97
- Polyline* polyline = new Polyline(tile, new_outer->to_vector());
98
- Shape* shape = new Shape(polyline, new_inners);
99
- shapes.push_back(shape);
100
- polyline->shape = shape;
101
- it++;
109
+
110
+ Polyline* polyline = tile->shapes_pool->acquire_polyline(tile, new_outer->to_vector(), std::nullopt);
111
+ Shape* inserting_new_shape = tile->shapes_pool->acquire_shape(polyline, new_inners);
112
+
113
+ new_shapes.push_back(inserting_new_shape);
114
+ polyline->shape = inserting_new_shape;
115
+ inserting_new_shape->set_parent_shape(shape->parent_shape());
116
+
117
+ for (InnerPolyline* inner_polyline : new_inner_polylines) {
118
+ inner_polyline->sequence()->shape = inserting_new_shape;
119
+ }
120
+
121
+ if (treemap) {
122
+ for (const auto merged_shape : cursor.shapes_sequence()) {
123
+ merged_shape->merged_to_shape = inserting_new_shape;
124
+ }
125
+ this->assign_ancestry(inserting_new_shape, all_new_inner_polylines);
126
+ }
127
+
102
128
  } else {
103
- shapes.push_back(shape);
104
- it = src.erase(it);
129
+ if (treemap && !shape->reassociation_skip && shape->parent_shape() == nullptr) {
130
+ this->assign_ancestry(shape, all_new_inner_polylines);
131
+ }
132
+ new_shapes.push_back(shape);
133
+ }
134
+ }
135
+ }
136
+
137
+ if (treemap) {
138
+ for (Tile* tile : tiles_) {
139
+ for (Shape* shape : tile->shapes()) {
140
+ Shape* parent = shape->parent_shape();
141
+ while (parent && parent->merged_to_shape != nullptr) {
142
+ parent = parent->merged_to_shape;
143
+ }
144
+ if (parent != shape->parent_shape()) {
145
+ shape->set_parent_shape(parent);
146
+ }
105
147
  }
106
148
  }
107
149
  }
150
+
108
151
  double past_tot_outer = tiles_.front()->benchmarks.outer + tiles_.back()->benchmarks.outer;
109
152
  double past_tot_inner = tiles_.front()->benchmarks.inner + tiles_.back()->benchmarks.inner;
153
+
110
154
  Benchmarks b{
111
155
  tot_outer + past_tot_outer,
112
156
  tot_inner + past_tot_inner
113
157
  };
114
- Tile* tile = new Tile(this->finder, tiles_.front()->start_x(), tiles_.back()->end_x(), tiles_.front()->name() + tiles_.back()->name(), b);
115
- tile->assign_shapes(shapes);
116
- return(tile);
158
+
159
+ Tile* tile = new Tile(
160
+ this->finder, tiles_.front()->start_x(), tiles_.back()->end_x(), tiles_.front()->name() + tiles_.back()->name(), b);
161
+
162
+ tile->assign_shapes(new_shapes);
163
+ for (Tile* old_tile : tiles_) {
164
+ tile->adopt(old_tile);
165
+ }
166
+ return tile;
117
167
  }
118
168
 
169
+ void Cluster::assign_ancestry(Shape *shape, std::vector<InnerPolyline*>& inner_polylines)
170
+ { for (auto* inner_polyline : inner_polylines) {
171
+ if (shape->outer_polyline->vert_bounds_intersect(inner_polyline->vertical_bounds())) {
172
+ if (shape->outer_polyline->within(inner_polyline->raw())) {
173
+ shape->set_parent_shape(inner_polyline->shape());
174
+ shape->parent_inner_polyline = inner_polyline;
175
+ for (auto* children_shape : shape->children_shapes) {
176
+ children_shape->reassociation_skip = true;
177
+ }
178
+ }
179
+ }
180
+ }
181
+ }