contrek 1.2.1 → 1.2.3

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 (40) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +16 -1
  3. data/Gemfile.lock +1 -1
  4. data/PERFORMANCE.md +177 -0
  5. data/README.md +2 -1
  6. data/contrek.gemspec +5 -1
  7. data/ext/cpp_polygon_finder/PolygonFinder/src/ContrekApi.h +39 -16
  8. data/ext/cpp_polygon_finder/PolygonFinder/src/Tests.cpp +4 -3
  9. data/ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/PolygonFinder.h +74 -3
  10. data/ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/concurrent/Cluster.cpp +32 -41
  11. data/ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/concurrent/Cluster.h +2 -1
  12. data/ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/concurrent/Cursor.cpp +33 -5
  13. data/ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/concurrent/Cursor.h +3 -2
  14. data/ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/concurrent/InnerPolyline.cpp +1 -2
  15. data/ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/concurrent/InnerPolyline.h +1 -3
  16. data/ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/concurrent/Polyline.cpp +16 -20
  17. data/ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/concurrent/Polyline.h +8 -3
  18. data/ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/concurrent/Shape.cpp +5 -0
  19. data/ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/concurrent/Shape.h +3 -1
  20. data/ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/concurrent/ShapePool.cpp +2 -2
  21. data/ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/concurrent/ShapePool.h +1 -1
  22. data/ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/concurrent/Tile.cpp +5 -3
  23. data/ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/concurrent/Tile.h +4 -4
  24. data/ext/cpp_polygon_finder/cpp_polygon_finder.cpp +2 -3
  25. data/lib/contrek/cpp/cpp_result.rb +2 -0
  26. data/lib/contrek/finder/concurrent/cluster.rb +30 -29
  27. data/lib/contrek/finder/concurrent/cursor.rb +35 -12
  28. data/lib/contrek/finder/concurrent/inner_polyline.rb +2 -3
  29. data/lib/contrek/finder/concurrent/polyline.rb +5 -7
  30. data/lib/contrek/finder/concurrent/shape.rb +7 -3
  31. data/lib/contrek/finder/concurrent/tile.rb +5 -4
  32. data/lib/contrek/finder/result.rb +2 -0
  33. data/lib/contrek/shared/result.rb +23 -0
  34. data/lib/contrek/version.rb +1 -1
  35. data/lib/contrek.rb +2 -1
  36. metadata +4 -6
  37. data/ext/cpp_polygon_finder/PolygonFinder/examples/example.cpp +0 -56
  38. data/ext/cpp_polygon_finder/PolygonFinder/images/graphs_1024x1024.png +0 -0
  39. data/ext/cpp_polygon_finder/PolygonFinder/images/labyrinth.png +0 -0
  40. data/ext/cpp_polygon_finder/PolygonFinder/images/sample_10240x10240.png +0 -0
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3e7576fef74116c62a018f66a13d9e29d215f7b505637b40bcb16631250fed0d
4
- data.tar.gz: 7ba2f39e97a44eaddd13361b4dc5a8f8cb1790a93dab327c1794d61e0ffc45a0
3
+ metadata.gz: 07f6097c256b6d44373aa941b969b12dd68630e168c72b29d1ae478bb5f48186
4
+ data.tar.gz: 7ec31b1ef4413e679e31a123f367d0a86ed332160455c8024ea185ca67c27020
5
5
  SHA512:
6
- metadata.gz: 748e4750b607bc2a9e9e916fc0a00a96db530e94658822d1d6eb97508fa9e302e5def958a63e621a2f97be1396bb0acad4971aea936cbca700c1ea93ac5acdaf
7
- data.tar.gz: 804a329359ea6648e4bcbf4babdb8af53a12205851d669b562fa37d5aa69a49f8b6988a143f2783c85b2681432f65ff52b82db8f207718bd29b16c46882f518d
6
+ metadata.gz: c03213b7499e5741bf3c90044251a5ba6fc606905f3c72ff93462a62ffc116ebae7ce211fb2a201862d874ae865df048e45cf85beed094bc60967d2b4dd6fc34
7
+ data.tar.gz: ae891e55d9d9ef5503f654d79c7187713579c6fa1c699736d446d6b19497ddb9d30723a26e98a77261481f364466b7d891962e9937433a65e6e341f240129faa
data/CHANGELOG.md CHANGED
@@ -83,4 +83,19 @@ All notable changes to this project will be documented in this file.
83
83
 
84
84
  ## [1.2.0] - 2026-05-02
85
85
  ### Changed
86
- - Further improvements have been applied to the internal parts joining algorithm using a new structural approach. This update is faster and resolves edge cases where inner parts were mistakenly classified as outer perimeters, ensuring precise contour hierarchy. The simplified logic has led to a significant reduction in codebase complexity and the removal of substantial redundant code.
86
+ - Further improvements have been applied to the internal parts joining algorithm using a new structural approach. This update is faster and resolves edge cases where inner parts were mistakenly classified as outer perimeters, ensuring precise contour hierarchy. The simplified logic has led to a significant reduction in codebase complexity and the removal of substantial redundant code.
87
+
88
+ ## [1.2.1] - 2026-05-09
89
+ ### Changed
90
+ - Some c++ optimizations.
91
+
92
+ ## [1.2.2] - 2026-05-20
93
+ ### Changed
94
+ - The treemap determination algorithm has been heavily optimized. Calls to the geometric routine that checks whether a newly generated inner polyline encloses other already-existing ones have been reduced to the minimum. Polylines adjacent to the shared overlap stripe are now excluded from these checks, as they are already identified during the initial polygon detection phase. The geometric approach remains unavoidable in this context and is still a performance bottleneck. It will certainly be the subject of future optimizations.
95
+
96
+ ## [1.2.3] - 2026-05-23
97
+ ### Changed
98
+ ### Changed
99
+ * **SVG Conversion:** Added utility methods to convert point coordinates directly into SVG paths.
100
+ * **Contrek API & RAII Architecture:** Refactored the Contrek API to utilize an RAII (Resource Acquisition Is Initialization) pattern, safely wrapping both the trace engine and the processing results within a unified context lifecycle shell.
101
+ * **ProcessResult Memory Management:** Updated `ProcessResult` to properly manage resource deallocation during cloning operations, ensuring deep-copied or moved internal points are automatically and safely freed when the context scope ends.
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- contrek (1.2.0)
4
+ contrek (1.2.3)
5
5
  chunky_png (~> 1.4)
6
6
  concurrent-ruby (~> 1.3.5)
7
7
  rice (= 4.5.0)
data/PERFORMANCE.md ADDED
@@ -0,0 +1,177 @@
1
+ # ⚡ Contrek Performance Tuning
2
+
3
+ This document describes optional dependencies and configuration tips to get the best performance out of Contrek on large images.
4
+
5
+ All optimizations are **optional** — Contrek works correctly without any of them. However, on high-resolution images (10k×10k and above), the combined effect is significant.
6
+
7
+ ---
8
+
9
+ ## Benchmark Reference
10
+
11
+ > System: AMD Ryzen 7 3700X 8-Core Processor (BogoMIPS: 7199,99) on Ubuntu distro
12
+ > Image: 20480×20480 pixels — 8 threads / 8 tiles
13
+ >
14
+ > **Note:** Benchmarks were measured inside a VMware virtual machine.
15
+
16
+ | Configuration | Time |
17
+ |---|---|
18
+ | Baseline (no tuning) | 5316 ms |
19
+ | **Fully tuned** | **2938.05 ms** |
20
+
21
+ ---
22
+
23
+ ## 1. zlib-ng — Faster PNG Decoding
24
+
25
+ **Impact: ~57% reduction in PNG decode time**
26
+
27
+ Contrek uses [libspng](https://libspng.org/) for PNG decoding, which internally relies on zlib for decompression. [zlib-ng](https://github.com/zlib-ng/zlib-ng) is a high-performance, drop-in replacement for zlib that uses modern CPU instructions (AVX2, SSE4) to significantly accelerate deflate decompression.
28
+
29
+ If zlib-ng is not installed, standard zlib is used automatically — no errors, just slower PNG decoding.
30
+
31
+ ### Installation
32
+
33
+ **Ubuntu / Debian** — not available in standard repos, build from source:
34
+
35
+ ```bash
36
+ git clone https://github.com/zlib-ng/zlib-ng.git
37
+ cd zlib-ng && mkdir build && cd build
38
+ cmake .. -DZLIB_COMPAT=ON -DCMAKE_BUILD_TYPE=Release
39
+ make -j$(nproc)
40
+ sudo make install
41
+ sudo ldconfig
42
+ ```
43
+
44
+ > ⚠️ The `-DZLIB_COMPAT=ON` flag is mandatory. Without it, zlib-ng uses a different ABI and CMake's `find_package(ZLIB)` won't detect it.
45
+
46
+ **macOS:**
47
+ ```bash
48
+ brew install zlib-ng
49
+ ```
50
+
51
+ **Arch Linux:**
52
+ ```bash
53
+ sudo pacman -S zlib-ng
54
+ ```
55
+
56
+ After installation, rebuild Contrek — CMake will automatically detect zlib-ng in `/usr/local` and use it instead of system zlib.
57
+
58
+ ---
59
+
60
+ ## 2. tcmalloc — Faster Memory Allocation
61
+
62
+ **Impact: significant reduction in allocator contention under multithreaded load**
63
+
64
+ Contrek creates and destroys large numbers of small objects during processing. Under multithreaded workloads, the standard glibc allocator serializes many of these operations, causing thread contention. [tcmalloc](https://github.com/google/tcmalloc) (Thread-Caching Malloc) is Google's high-performance allocator that maintains per-thread caches, dramatically reducing lock contention.
65
+
66
+ ### Installation
67
+
68
+ **Ubuntu / Debian:**
69
+ ```bash
70
+ sudo apt-get install libgoogle-perftools-dev
71
+ ```
72
+
73
+ **macOS:**
74
+ ```bash
75
+ brew install gperftools
76
+ ```
77
+
78
+ CMake will detect tcmalloc automatically. You will see this confirmation during the build:
79
+ ```
80
+ -- Contrek: tcmalloc found in /usr/lib/x86_64-linux-gnu/libtcmalloc.so
81
+ ```
82
+
83
+ ### Tuning tcmalloc cache size
84
+
85
+ For large images with many threads, increasing the per-thread cache size reduces requests to the central allocator. Add this at the very beginning of your `main()`:
86
+
87
+ ```cpp
88
+ #include <gperftools/malloc_extension.h>
89
+
90
+ int main() {
91
+ MallocExtension::instance()->SetNumericProperty(
92
+ "tcmalloc.max_total_thread_cache_bytes",
93
+ 1024 * 1024 * 1024 // 1GB total thread cache
94
+ );
95
+ // ...
96
+ }
97
+ ```
98
+
99
+ The default is 32MB total. On systems with 16GB+ RAM, 1GB is a safe value that virtually eliminates allocator contention.
100
+
101
+ ---
102
+
103
+ ## 3. Thread and Tile Configuration
104
+
105
+ **Impact: up to ~35% reduction in processing time on multi-core systems**
106
+
107
+ Contrek splits the image into vertical tiles processed in parallel. The optimal configuration depends on your hardware.
108
+
109
+ ### General rule
110
+
111
+ Set both `threads` and `tiles` to the number of **physical cores** on your machine.
112
+
113
+ ```cpp
114
+ Contrek::Config cfg;
115
+ cfg.threads = 8; // match your physical core count
116
+ cfg.tiles = 8; // same as threads for best results
117
+ ```
118
+
119
+ ```ruby
120
+ result = Contrek.contour!(
121
+ png_file_path: "image.png",
122
+ options: {
123
+ number_of_threads: 8,
124
+ finder: { number_of_tiles: 8 }
125
+ }
126
+ )
127
+ ```
128
+
129
+ ### Why threads == tiles?
130
+
131
+ - **Fewer tiles than threads**: some cores sit idle waiting for others to finish
132
+ - **More tiles than threads**: merge overhead increases without adding parallelism
133
+ - **threads == tiles**: optimal balance between parallel scan and merge cost
134
+
135
+ Consider this depends your system. Probably is better not to saturate all cores leaving one ot two to the system and the others to Contrek. So on 8 cpu core 6 thread/tiles at maximum.
136
+
137
+ ---
138
+
139
+ ## 4. Combining All Optimizations
140
+
141
+ Install zlib-ng and tcmalloc, then configure:
142
+
143
+ ```ruby
144
+ # Ruby
145
+ result = Contrek.contour!(
146
+ png_file_path: "large_image.png",
147
+ options: {
148
+ number_of_threads: 8, # match your core count (or 1-2 less)
149
+ class: "value_not_matcher",
150
+ color: { r: 255, g: 255, b: 255, a: 255 },
151
+ finder: {
152
+ number_of_tiles: 8, # same as threads
153
+ compress: { uniq: true }
154
+ }
155
+ }
156
+ )
157
+ ```
158
+
159
+ ```cpp
160
+ // C++ standalone
161
+ #include <gperftools/malloc_extension.h>
162
+ #include "ContrekApi.h"
163
+
164
+ int main() {
165
+ MallocExtension::instance()->SetNumericProperty(
166
+ "tcmalloc.max_total_thread_cache_bytes",
167
+ 1024 * 1024 * 1024
168
+ );
169
+
170
+ Contrek::Config cfg;
171
+ cfg.threads = 8;
172
+ cfg.tiles = 8;
173
+
174
+ auto result = Contrek::trace("large_image.png", cfg);
175
+ std::cout << "Time: " << result->total_time << " ms" << std::endl;
176
+ }
177
+ ```
data/README.md CHANGED
@@ -158,7 +158,8 @@ You can process from a raw stream
158
158
  [{:outer=>[{:x=>5, :y=>4}, {:x=>5, :y=>5}, {:x=>8, :y=>5}, {:x=>8, :y=>4}], :inner=>[]}]
159
159
  ```
160
160
 
161
- 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).
161
+ Multithreaded contour processing is supported by both the native C++ and pure Ruby implementations. When using the C++ engine (default), multithreading works as expected and fully utilizes all available cores.
162
+ When running the pure Ruby implementation, however, the Global Interpreter Lock (GIL) in Ruby MRI (the standard Ruby interpreter, up to at least version 3.x) prevents true parallel execution — threads are serialized even on multicore systems. Switching to JRuby or TruffleRuby would bypass this limitation, though these runtimes have not been tested with Contrek.
162
163
 
163
164
  ```ruby
164
165
  result = Contrek.contour!(
data/contrek.gemspec CHANGED
@@ -11,7 +11,11 @@ Gem::Specification.new do |s|
11
11
  s.homepage = "https://github.com/runout77/contrek"
12
12
  s.licenses = ["MIT", "AGPL-3.0-only"]
13
13
  s.files = Dir.chdir(File.expand_path("..", __FILE__)) do
14
- `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(docs|pkg|spec)/}) }
14
+ `git ls-files -z`.split("\x0").reject do |f|
15
+ f.match(%r{^(docs|pkg|spec)/}) ||
16
+ f.include?("PolygonFinder/images/") ||
17
+ f.include?("PolygonFinder/examples/")
18
+ end
15
19
  end
16
20
  s.metadata = {
17
21
  "homepage_uri" => "https://github.com/runout77/contrek",
@@ -13,6 +13,7 @@
13
13
  #include <vector>
14
14
  #include <memory>
15
15
  #include <cstdint>
16
+ #include <string_view>
16
17
 
17
18
  #include "Finder.h"
18
19
  #include "FastPngBitmap.h"
@@ -43,21 +44,44 @@ struct Config {
43
44
  Connectivity connectivity_mode = Connectivity::ORTHOGONAL;
44
45
  };
45
46
 
46
- inline std::unique_ptr<ProcessResult> trace(const std::string& image_path, const Config& cfg = Config()) {
47
- auto bitmap = std::make_unique<FastPngBitmap>(image_path);
47
+ struct TraceContext {
48
+ std::unique_ptr<FastPngBitmap> bitmap;
49
+ std::unique_ptr<Matcher> matcher;
50
+ std::vector<std::string> internal_args;
51
+ std::unique_ptr<Finder> finder;
52
+ std::unique_ptr<ProcessResult> result;
53
+
54
+ // this allows result direct access
55
+ const ProcessResult* operator->() const { return result.get(); }
56
+ ProcessResult* operator->() { return result.get(); }
57
+ const ProcessResult& operator*() const { return *result; }
58
+ ProcessResult& operator*() { return *result; }
59
+
60
+ // context can be moved not copied
61
+ TraceContext() = default;
62
+ TraceContext(const TraceContext&) = delete;
63
+ TraceContext& operator=(const TraceContext&) = delete;
64
+ TraceContext(TraceContext&&) = default;
65
+ TraceContext& operator=(TraceContext&&) = default;
66
+ };
67
+
68
+ inline TraceContext trace(const std::string& image_path, const Config& cfg = Config()) {
69
+ TraceContext ctx;
70
+
71
+ ctx.bitmap = std::make_unique<FastPngBitmap>(image_path);
48
72
 
49
73
  int32_t color_to_match = (cfg.target_color == -1)
50
- ? bitmap->rgb_value_at(0, 0)
51
- : cfg.target_color;
74
+ ? ctx.bitmap->rgb_value_at(0, 0)
75
+ : cfg.target_color;
52
76
 
53
- std::unique_ptr<Matcher> matcher;
54
77
  if (cfg.mode == MatchMode::NOT_COLOR) {
55
- matcher = std::make_unique<RGBNotMatcher>(color_to_match);
78
+ ctx.matcher = std::make_unique<RGBNotMatcher>(color_to_match);
56
79
  } else {
57
- matcher = std::make_unique<RGBMatcher>(color_to_match);
80
+ ctx.matcher = std::make_unique<RGBMatcher>(color_to_match);
58
81
  }
59
82
 
60
- std::vector<std::string> internal_args = {"--versus=a"};
83
+ ctx.internal_args = {"--versus=a"};
84
+
61
85
  struct Mapping { bool flag; std::string_view arg; };
62
86
  const Mapping mappings[] = {
63
87
  {cfg.compress_unique, "--compress_uniq"},
@@ -65,18 +89,17 @@ inline std::unique_ptr<ProcessResult> trace(const std::string& image_path, const
65
89
  {cfg.compress_visvalingam, "--compress_visvalingam"},
66
90
  {cfg.treemap, "--treemap"}
67
91
  };
68
-
69
- for (auto& m : mappings) {
70
- if (m.flag) internal_args.emplace_back(m.arg);
92
+ for (const auto& m : mappings) {
93
+ if (m.flag) ctx.internal_args.emplace_back(m.arg);
71
94
  }
72
- internal_args.push_back("--number_of_tiles=" + std::to_string(cfg.tiles));
95
+ ctx.internal_args.push_back("--number_of_tiles=" + std::to_string(cfg.tiles));
73
96
  if (cfg.connectivity_mode == Connectivity::OMNIDIRECTIONAL) {
74
- internal_args.push_back("--connectivity=" + std::to_string(8));
97
+ ctx.internal_args.push_back("--connectivity=" + std::to_string(8));
75
98
  }
99
+ ctx.finder = std::make_unique<Finder>(cfg.threads, ctx.bitmap.get(), ctx.matcher.get(), &ctx.internal_args);
100
+ ctx.result = std::unique_ptr<ProcessResult>(ctx.finder->process_info());
76
101
 
77
- Finder finder(cfg.threads, bitmap.get(), matcher.get(), &internal_args);
78
-
79
- return std::unique_ptr<ProcessResult>(finder.process_info());
102
+ return ctx;
80
103
  }
81
104
 
82
105
  } // namespace Contrek
@@ -342,17 +342,18 @@ void stream_png_image(const std::string& filepath, uint32_t stripe_height) {
342
342
 
343
343
  std::cout << "Merging polygons ..." << std::endl;
344
344
  ProcessResult *merged_result = vmerger.process_info();
345
- std::cout << "Founds total polygons: " << merged_result->groups << std::endl;
346
345
 
347
346
  if (merged_result) {
348
- RawBitmap full_bitmap;
347
+ std::cout << "Founds total polygons: " << merged_result->groups << std::endl;
348
+ /*RawBitmap full_bitmap;
349
349
  full_bitmap.define(total_width, total_height, 4, true);
350
350
  full_bitmap.fill(255, 255, 255);
351
351
  merged_result->draw_on_bitmap(full_bitmap);
352
352
  std::cout << "Saving whole png ..." << std::endl;
353
353
  if (full_bitmap.save_to_png("whole.png")) {
354
354
  std::cout << "Png saved!" << std::endl;
355
- }
355
+ }*/
356
+ merged_result->save_svg("whole.svg");
356
357
  }
357
358
  delete merged_result;
358
359
  // frees memory
@@ -14,7 +14,12 @@
14
14
  #include <string>
15
15
  #include <map>
16
16
  #include <iostream>
17
+ #include <memory>
17
18
  #include <utility>
19
+ #include <fstream>
20
+ #include <sstream>
21
+ #include <stdexcept>
22
+ #include <limits>
18
23
  #include "../bitmaps/Bitmap.h"
19
24
  #include "NodeCluster.h"
20
25
  #include "Node.h"
@@ -58,6 +63,8 @@ struct ProcessResult {
58
63
  std::list<Polygon> polygons;
59
64
  std::string named_sequence;
60
65
  std::vector<std::pair<int, int>> treemap;
66
+ std::vector<std::unique_ptr<Point>> cloned_points_storage;
67
+
61
68
  void draw_on_bitmap(RawBitmap& canvas) const;
62
69
 
63
70
  void print_polygons() {
@@ -103,14 +110,22 @@ struct ProcessResult {
103
110
  new_res->named_sequence = this->named_sequence;
104
111
  new_res->treemap = this->treemap;
105
112
 
113
+ size_t estimated_points = 0;
114
+ for (const auto& poly : this->polygons) {
115
+ estimated_points += poly.outer.size();
116
+ for (const auto& seq : poly.inner) estimated_points += seq.size();
117
+ }
118
+ new_res->cloned_points_storage.reserve(estimated_points);
119
+
106
120
  for (const auto& poly : this->polygons) {
107
121
  Polygon new_poly;
108
- // Bounds
122
+ // bounds
109
123
  new_poly.bounds = poly.bounds;
110
124
  // outer
111
125
  for (const Point* p : poly.outer) {
112
126
  if (p) {
113
- new_poly.outer.push_back(new Point(p->x, p->y));
127
+ new_res->cloned_points_storage.push_back(std::make_unique<Point>(p->x, p->y));
128
+ new_poly.outer.push_back(new_res->cloned_points_storage.back().get());
114
129
  }
115
130
  }
116
131
  // inner
@@ -118,7 +133,8 @@ struct ProcessResult {
118
133
  std::vector<Point*> new_seq;
119
134
  for (const Point* p : seq) {
120
135
  if (p) {
121
- new_seq.push_back(new Point(p->x, p->y));
136
+ new_res->cloned_points_storage.push_back(std::make_unique<Point>(p->x, p->y));
137
+ new_seq.push_back(new_res->cloned_points_storage.back().get());
122
138
  }
123
139
  }
124
140
  new_poly.inner.push_back(new_seq);
@@ -127,6 +143,61 @@ struct ProcessResult {
127
143
  }
128
144
  return new_res;
129
145
  }
146
+
147
+ std::string to_svg() const {
148
+ std::vector<std::string> lines;
149
+ lines.push_back(
150
+ "<svg xmlns=\"http://www.w3.org/2000/svg\" "
151
+ "width=\"" + std::to_string(width) +
152
+ "\" height=\"" + std::to_string(height) + "\">");
153
+ for (const auto& poly : polygons) {
154
+ { // outer
155
+ std::ostringstream pts;
156
+ bool first = true;
157
+ for (const Point* p : poly.outer) {
158
+ if (!p) continue;
159
+ if (!first)
160
+ pts << " ";
161
+ first = false;
162
+ pts << p->x << "," << p->y;
163
+ }
164
+ lines.push_back(
165
+ "<polygon points=\"" + pts.str() +
166
+ "\" fill=\"none\" stroke=\"red\" stroke-width=\"1\"/>");
167
+ }
168
+ // inner
169
+ for (const auto& sequence : poly.inner) {
170
+ if (sequence.empty()) continue;
171
+ std::ostringstream pts;
172
+ bool first = true;
173
+ for (const Point* p : sequence) {
174
+ if (!p) continue;
175
+ if (!first) pts << " ";
176
+ first = false;
177
+ pts << p->x << "," << p->y;
178
+ }
179
+ lines.push_back(
180
+ "<polygon points=\"" + pts.str() +
181
+ "\" fill=\"none\" stroke=\"green\" stroke-width=\"1\"/>");
182
+ }
183
+ }
184
+ lines.push_back("</svg>");
185
+ std::ostringstream result;
186
+ for (size_t i = 0; i < lines.size(); ++i) {
187
+ result << lines[i];
188
+ if (i + 1 < lines.size()) result << "\n";
189
+ }
190
+ return result.str();
191
+ }
192
+
193
+ void save_svg(const std::string& filename) const {
194
+ std::ofstream file(filename);
195
+ if (!file.is_open()) {
196
+ throw std::runtime_error("Unable to open SVG file: " + filename);
197
+ }
198
+ file << to_svg();
199
+ file.close();
200
+ }
130
201
  };
131
202
 
132
203
  class PolygonFinder {
@@ -54,7 +54,7 @@ Tile* Cluster::merge_tiles() {
54
54
  double tot_outer = 0;
55
55
  CpuTimer timer;
56
56
 
57
- std::list<Shape*> new_shapes;
57
+ std::vector<Shape*> new_shapes;
58
58
  std::vector<InnerPolyline*> all_new_inner_polylines;
59
59
 
60
60
  timer.start();
@@ -69,7 +69,7 @@ Tile* Cluster::merge_tiles() {
69
69
  tot_outer += timer.stop();
70
70
 
71
71
  for (Tile* tile : tiles_) {
72
- std::list<Shape*>& src = tile->shapes();
72
+ std::vector<Shape*>& src = tile->shapes();
73
73
 
74
74
  for (Shape* shape : src) {
75
75
  if (shape->outer_polyline->is_on(Polyline::TRACKED_OUTER) || shape->outer_polyline->width() == 0) {
@@ -86,7 +86,7 @@ Tile* Cluster::merge_tiles() {
86
86
 
87
87
  timer.start();
88
88
  std::vector<InnerPolyline*> new_inners = shape->inner_polylines;
89
- std::vector<InnerPolyline*> new_inner_polylines = cursor.join_inners(new_outer);
89
+ std::vector<InnerPolyline*> new_inner_polylines = cursor.join_inners(new_outer, treemap);
90
90
  tot_inner += timer.stop();
91
91
 
92
92
  for (InnerPolyline* inner_polyline : new_inner_polylines) {
@@ -94,59 +94,44 @@ Tile* Cluster::merge_tiles() {
94
94
  if (treemap) {
95
95
  inner_polyline->sequence()->compute_vertical_bounds();
96
96
  all_new_inner_polylines.push_back(inner_polyline);
97
- for (const auto orphan_inner : cursor.orphan_inners()) {
98
- if (orphan_inner->recombined()) {
99
- all_new_inner_polylines.push_back(orphan_inner);
100
- }
101
- }
102
97
  }
103
98
  }
104
-
105
99
  for (auto s : cursor.orphan_inners()) {
106
100
  new_inners.push_back(s);
107
101
  }
108
-
109
102
  Polyline* polyline = tile->shapes_pool->acquire_polyline(tile, new_outer->to_vector(), std::nullopt);
110
103
  Shape* inserting_new_shape = tile->shapes_pool->acquire_shape(polyline, new_inners);
111
-
112
104
  new_shapes.push_back(inserting_new_shape);
113
105
  polyline->shape = inserting_new_shape;
114
- inserting_new_shape->set_parent_shape(shape->parent_shape());
115
106
 
116
107
  for (InnerPolyline* inner_polyline : new_inner_polylines) {
117
108
  inner_polyline->sequence()->shape = inserting_new_shape;
118
109
  }
119
-
120
110
  if (treemap) {
121
111
  for (const auto merged_shape : cursor.shapes_sequence()) {
122
112
  merged_shape->merged_to_shape = inserting_new_shape;
123
113
  }
124
- this->assign_ancestry(inserting_new_shape, all_new_inner_polylines);
114
+ InnerPolyline* inside_inner_polyline = shape->outer_polyline->inside_inner_polyline;
115
+ if (inside_inner_polyline) {
116
+ assign_ancestry(inserting_new_shape, inside_inner_polyline);
117
+ }
125
118
  }
126
-
127
119
  } else {
128
- if (treemap && !shape->reassociation_skip && shape->parent_shape() == nullptr) {
129
- this->assign_ancestry(shape, all_new_inner_polylines);
120
+ if (treemap) {
121
+ if (shape->fixed) {
122
+ Shape* ms = shape->parent_shape()->merged_to_shape;
123
+ if (ms) {
124
+ shape->set_parent_shape(ms);
125
+ }
126
+ } else {
127
+ is_children(shape, all_new_inner_polylines);
128
+ }
130
129
  }
131
130
  new_shapes.push_back(shape);
132
131
  }
133
132
  }
134
133
  }
135
134
 
136
- if (treemap) {
137
- for (Tile* tile : tiles_) {
138
- for (Shape* shape : tile->shapes()) {
139
- Shape* parent = shape->parent_shape();
140
- while (parent && parent->merged_to_shape != nullptr) {
141
- parent = parent->merged_to_shape;
142
- }
143
- if (parent != shape->parent_shape()) {
144
- shape->set_parent_shape(parent);
145
- }
146
- }
147
- }
148
- }
149
-
150
135
  double past_tot_outer = tiles_.front()->benchmarks.outer + tiles_.back()->benchmarks.outer;
151
136
  double past_tot_inner = tiles_.front()->benchmarks.inner + tiles_.back()->benchmarks.inner;
152
137
 
@@ -165,16 +150,22 @@ Tile* Cluster::merge_tiles() {
165
150
  return tile;
166
151
  }
167
152
 
168
- void Cluster::assign_ancestry(Shape *shape, std::vector<InnerPolyline*>& inner_polylines)
169
- { for (auto* inner_polyline : inner_polylines) {
170
- if (shape->outer_polyline->vert_bounds_intersect(inner_polyline->vertical_bounds())) {
171
- if (shape->outer_polyline->within(inner_polyline->raw())) {
172
- shape->set_parent_shape(inner_polyline->shape());
173
- shape->parent_inner_polyline = inner_polyline;
174
- for (auto* children_shape : shape->children_shapes) {
175
- children_shape->reassociation_skip = true;
176
- }
177
- }
153
+ void Cluster::assign_ancestry(Shape *shape, InnerPolyline* inner_polyline)
154
+ { shape->set_parent_shape(inner_polyline->sequence()->shape);
155
+ shape->parent_inner_polyline = inner_polyline;
156
+ shape->fixed = true;
157
+ }
158
+
159
+ void Cluster::is_children(Shape* shape, std::vector<InnerPolyline*> inner_polylines) {
160
+ int shape_max_y = shape->outer_polyline->max_y();
161
+ int shape_min_y = shape->outer_polyline->min_y();
162
+ for (InnerPolyline* inner_polyline : inner_polylines) {
163
+ Bounds bounds = inner_polyline->vertical_bounds();
164
+ int min_y = bounds.min;
165
+ int max_y = bounds.max;
166
+ if (shape_max_y < min_y || shape_min_y > max_y ) continue;
167
+ if (shape->outer_polyline->within(inner_polyline->raw())) {
168
+ assign_ancestry(shape, inner_polyline);
178
169
  }
179
170
  }
180
171
  }
@@ -21,7 +21,8 @@ class Cluster {
21
21
  Finder *finder;
22
22
  std::vector<Tile*> tiles_;
23
23
  Hub *hub_ = nullptr;
24
- void assign_ancestry(Shape *shape, std::vector<InnerPolyline*>& inner_polylines);
24
+ void assign_ancestry(Shape *shape, InnerPolyline* inner_polyline);
25
+ void is_children(Shape* shape, std::vector<InnerPolyline*> inner_polylines);
25
26
 
26
27
  public:
27
28
  Cluster(Finder *finder, int height, int start_x, int end_x);