contrek 1.3.7 → 1.3.8

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 60aa8f5e3cfb43291e3dcce9921d77e84ce289bffd974dca7a4b2ed4c07124c0
4
- data.tar.gz: 98ccefa7a9de3fa2ea0922d09c18fa45470ccb6ad0072c0b85d6203f025a7f11
3
+ metadata.gz: 68bd09217262e7b3495d05110af5a6b9c0c97bd493be54138706e3ec298847a7
4
+ data.tar.gz: a5dbd685e38170bc830fbbbf1e17c1725eaca367c53c229e6d233564a12337a6
5
5
  SHA512:
6
- metadata.gz: 57731f61f8afc63f459b2fd349cd34e151406943c51628f80d0ac06dd29c360323adead860da8e2f9b9fb4579b6f29987c5a681175db7315d999d78039f4811f
7
- data.tar.gz: 8420ba829bd714492b95249c567bdc1be27c8559bd11fbcdaf48ee9d975dc719891db23fd5d2f54d832e4f79f3dc335b1da557a53382f713d3c478094004f3d6
6
+ metadata.gz: d6179fce1a4e54c1fbcc14a25939ff769d74f9804ea6bc09f84fac511f0b3f7be435817e7467f2a2556d215ddc4aec36edd0e4ea2b4791ba01d09e8f9d748c3b
7
+ data.tar.gz: e0694addda28d20ca8d57108555a902b18ad0fb1765662ebad035c7637b47ae1d55fe6e87715e05d21a8793cfbb7a9a86156ef13f0333e9c2fc316eef6f379b5
data/CHANGELOG.md CHANGED
@@ -144,3 +144,6 @@ All notable changes to this project will be documented in this file.
144
144
  - Added polygon compression to the progressive streaming pipeline (StreamingMerger).
145
145
  - Added strict validation in progressive streaming. Results passed to add_tile() are now validated to ensure they contain compatible settings (such as versus conventions and compression options).
146
146
  - Added unsafe_mode flag to streaming mergers. This allows bypassing compatibility checks when using custom or arbitrary input data.
147
+
148
+ ## [1.3.8] - 2026-08-08
149
+ - Fixed issue found on treemap determination.
data/README.md CHANGED
@@ -1,31 +1,60 @@
1
1
  # Contrek
2
- **Contrek** is a standalone <u>[C++17 topology-preserving streaming polygonization engine](#-c-standalone-library-usage)</u> for raster-to-vector conversion. It extracts polygonal contours from PNG images and raw memory streams using programmable pixel matchers, representing shapes as connected graphs of shared vertices to guarantee topological consistency. Its streaming architecture supports gigapixel-scale datasets, multithreaded execution, and incremental vector serialization to multiple formats such as SVG while keeping memory consumption bounded. A fully featured Ruby wrapper is also available, exposing the complete C++ API through an idiomatic Ruby interface.
3
2
 
4
- ## About Contrek library
3
+ **Contrek** is a standalone **C++17 contour tracing and polygonization library** for raster images.
5
4
 
6
- Contrek (**con**tour **trek**king) simply scans your png/raw 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, splitting the image into vertical strips and recombining the coordinates in pairs.
5
+ It was originally developed to solve a practical problem: extracting polygons from very large raster images without loading the entire image into memory. Over time the implementation evolved into a reusable library that can process PNG images as well as raw memory buffers, while preserving polygon topology during the entire tracing process.
7
6
 
8
- 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
7
+ The engine is based on programmable pixel matchers, allowing the caller to decide which pixels belong to the regions being traced. Polygon coordinates can be streamed as they are produced, making the library suitable for datasets that would otherwise require a large amount of RAM.
8
+
9
+ Although the tracing engine is written in C++, Contrek is also distributed as a Ruby gem exposing almost the complete native API through an idiomatic Ruby interface.
10
+
11
+ ## About Contrek
12
+
13
+ **Contrek** (**CON**tour **TREK**king) at its simplest, the library scans a bitmap and returns the contours of all regions matching a user-defined criterion. Each region is represented by one outer polygon and, when necessary, one or more inner polygons describing holes.
14
+
15
+ Besides extracting contours, Contrek can also determine the nesting relationship between polygons and build a tree describing which polygons contain others. This is useful when reconstructing the topology of complex shapes.
16
+
17
+ Several coordinate-compression algorithms are available to reduce polygon size after tracing. The library can also split the work across multiple threads by processing independent vertical stripes and merging the results afterwards into a single topologically consistent representation.
18
+
19
+ The image below shows a simple example. Every non-white pixel is considered part of the traced region. The resulting outer contour is shown in red, while inner contours are shown in green.
9
20
 
10
21
  ![alt text](contrek.png "Contour tracing")
11
22
 
12
- ## 🚀 Why Contrek?
13
23
 
14
- Contrek is a high-performance vectorization engine designed for massive datasets where memory efficiency and multi-core utilization are critical. Unlike traditional sequential contour tracers, Contrek is built on a **Stripe-Merging Architecture** that allows it to process images in independent segments and reconstruct the resulting polygons with perfect topological integrity.
24
+ # Why Contrek?
25
+
26
+ Most contour tracing libraries process an image sequentially. That approach is perfectly adequate for many workloads, but it becomes less practical once images become very large or when memory usage starts to matter.
27
+
28
+ Contrek follows a different strategy.
29
+
30
+ Instead of processing the whole image at once, it can split it into independent vertical stripes. Each stripe is traced separately and the resulting polygons are merged afterwards. Most of the implementation complexity lies in this merge phase, whose purpose is to reconstruct polygons crossing stripe boundaries without breaking their topology.
31
+
32
+ The same design makes it possible to use the library in two different ways:
33
+
34
+ - **Parallel processing**, where several stripes are traced simultaneously on different CPU cores.
35
+ - **Streaming processing**, where only a small portion of the image is kept in memory at any given time.
36
+
37
+ The tracing algorithm itself is identical in both cases; only the execution strategy changes.
38
+
39
+ ## Parallel execution
15
40
 
16
- ### 1. Parallel Processing Power
17
- Contrek is designed to saturate all available CPU cores. By partitioning a single image into stripes, it can assign each segment to a different thread.
18
- * **The Result:** On multi-core systems, Contrek achieves unmatched extraction speeds by parallelizing the workload.
19
- * **Scalability:** Performance scales directly with your hardware, making it the ideal choice for high-throughput processing servers.
41
+ When multiple CPU cores are available, independent stripes can be processed concurrently. The actual speedup depends on the image content and on how much work is required during the merge stage, but large datasets generally benefit from additional cores.
20
42
 
21
- ### 2. Streamed Memory Management
22
- Standard vectorization requires loading entire high-resolution images into RAM, which often leads to memory saturation.
23
- * **The Solution:** Contrek enables **streamed processing** (see the C++ example in the repo). You can define a specific stripe height (buffer) and process the image incrementally, consuming only the memory allocated for that buffer.
24
- * **The Benefit:** Vectorize massive, gigapixel-scale images or raw datasets directly from memory buffers. This allows for high-performance processing even on systems with limited memory that would otherwise fail.
43
+ This approach allows Contrek to make effective use of modern multicore processors without changing the resulting geometry.
25
44
 
26
- ### 3. Intelligent "Stitching" Technology
27
- The core strength of Contrek is its **Topologically Consistent Merging** algorithm, which maintains a deep understanding of polygon connectivity across stripe boundaries.
28
- * Whether you are optimizing for **speed** (parallel cores) or **memory** (sequential streaming), the output is always a seamless, continuous set of vector data, free from gaps or duplicates at the seams.
45
+ ## Streaming large datasets
46
+
47
+ Loading an entire gigapixel image into memory is often unnecessary.
48
+
49
+ Contrek can instead process the image incrementally by reading one stripe at a time. Only the current working buffer needs to be allocated, making memory consumption predictable regardless of the final image height.
50
+
51
+ This execution mode is particularly useful when processing images that are too large to fit comfortably into RAM.
52
+
53
+ ## Merging polygons
54
+
55
+ Whenever a polygon crosses the boundary between two adjacent stripes, the library reconstructs it during the merge phase.
56
+
57
+ This reconstruction preserves polygon connectivity across stripe boundaries, so the final output is equivalent to tracing the entire image in a single pass.
29
58
 
30
59
  <table>
31
60
  <tr>
@@ -49,47 +78,67 @@ The core strength of Contrek is its **Topologically Consistent Merging** algorit
49
78
  </tr>
50
79
  </table>
51
80
 
52
- ## 📊 Benchmarking & Performance
53
- The **Stripe-Merging** algorithm has been validated through a dedicated testing suite comparing **Contrek** against **OpenCV** (industry-standard contour extraction).
81
+ ## Benchmarking
82
+
83
+ Contrek has been benchmarked mainly against OpenCV, which is probably the most widely used library for contour extraction.
54
84
 
55
- ### Key Metrics:
56
- * **Execution Latency:** Single-threaded OpenCV vs. Contrek's parallel thread management.
57
- * **Memory Footprint:** RAM consumption during ultra-high-resolution processing.
58
- * **Extraction Fidelity:** Verifying polygon precision across both engines.
85
+ The goal was not simply to compare execution times, but also to verify that the reconstructed polygons remain topologically correct after stripe merging, even when processing very large images.
86
+
87
+ The benchmark suite measures three aspects:
88
+
89
+ - execution time;
90
+ - memory usage;
91
+ - consistency of the generated polygons.
92
+
93
+ The benchmark source code and the datasets used for testing are available here:
59
94
 
60
- The complete testing suite, source code, and raw benchmarks are available here:
61
95
  👉 **[test_contrek](https://github.com/runout77/test_contrek)**
62
96
 
97
+
63
98
  ## Prerequisites
64
99
 
65
- For optimal performance and efficient memory management with large images (20k+), it is highly recommended to install **tcmalloc**.
100
+ For small and medium-sized images no special configuration is required.
101
+
102
+ If you expect to process very large images (roughly 20k pixels or more), installing **tcmalloc** is recommended. It generally provides more stable memory behaviour than the default allocator during long-running processing.
103
+
104
+ ### Ubuntu / Debian
66
105
 
67
- **Ubuntu / Debian:**
68
106
  ```bash
69
107
  sudo apt-get install libgoogle-perftools-dev
70
108
  ```
71
- > For advanced performance tuning (zlib-ng, tcmalloc, thread configuration) see [PERFORMANCE.md](PERFORMANCE.md).
72
109
 
73
- > ⚠️ **Platform support:** Contrek native extensions are supported on **Linux** and **macOS** only.
74
- > Windows is not supported due to the use of POSIX threading primitives and platform-specific
75
- > memory management. On Windows, consider using WSL2 (Windows Subsystem for Linux).
110
+ For additional tuning options (zlib-ng, thread configuration, memory allocator and other performance-related settings), see **[PERFORMANCE.md](PERFORMANCE.md)**.
76
111
 
77
- ## Install
112
+ > **Platform support**
113
+ >
114
+ > Native extensions are currently supported on **Linux** and **macOS**.
115
+ >
116
+ > Windows is not supported because the implementation relies on POSIX threading primitives and platform-specific memory management. Running under **WSL2** is currently the recommended approach for Windows users.
78
117
 
79
- Add this line to your application's Gemfile:
118
+
119
+ # Installation
120
+
121
+ Add the gem to your application's Gemfile:
80
122
 
81
123
  ```ruby
82
- gem 'contrek'
124
+ gem "contrek"
83
125
  ```
84
126
 
85
- And then execute:
127
+ and install it normally:
128
+
129
+ ```bash
130
+ bundle install
131
+ ```
132
+
133
+ During installation the native C++ extension is compiled automatically.
134
+
135
+ Once installed, the C++ implementation is used by default. A pure Ruby implementation is also included and can be selected explicitly whenever native extensions are not desired.
86
136
 
87
- bundle install
137
+ # Usage
88
138
 
89
- This will install the gem and compile the native extensions.
139
+ The simplest way to use Contrek is through the `Contrek.contour!` helper.
90
140
 
91
- ## Usage
92
- In this example we are asking to examine any pixel that does not have the red color.
141
+ In the example below, every pixel except pure red is considered part of the region to be traced.
93
142
 
94
143
  ```ruby
95
144
  result = Contrek.contour!(
@@ -101,65 +150,200 @@ result = Contrek.contour!(
101
150
  }
102
151
  )
103
152
  ```
104
- The resulting metadata information contains the execution times (microseconds), the count of polygons found and the nesting tree map. You can access polygons coordinates too (see later).
105
153
 
106
- ```ruby
107
- {:benchmarks=>{"build_tangs_sequence"=>0.129, "compress"=>0.037, "plot"=>0.198, "scan"=>0.114, "total"=>0.478}, :groups=>2, :named_sequence=>"", :treemap=>[]}
154
+ The returned object contains both the extracted polygons and some metadata describing the execution.
155
+
156
+ Metadata includes timing information (expressed in milliseconds), the number of detected polygon groups and, if requested, the nesting tree.
108
157
 
158
+ ```ruby
159
+ {
160
+ :benchmarks=>{
161
+ "build_tangs_sequence"=>0.129,
162
+ "compress"=>0.037,
163
+ "plot"=>0.198,
164
+ "scan"=>0.114,
165
+ "total"=>0.478
166
+ },
167
+ :groups=>2,
168
+ :named_sequence=>"",
169
+ :treemap=>[]
170
+ }
109
171
  ```
110
172
 
111
- By default the C++ version is used. If you want run pure but slower ruby implementation
173
+ The actual polygon coordinates are discussed later in the **Result** section.
174
+
175
+
176
+ ## Pure Ruby implementation
177
+
178
+ By default Contrek uses the native C++ implementation.
179
+
180
+ A pure Ruby version is included mainly for portability, debugging and environments where compiling native extensions is not possible.
181
+
182
+ It exposes the same public API, although execution is considerably slower.
183
+
112
184
  ```ruby
113
185
  result = Contrek.contour!(
114
186
  png_file_path: "labyrinth3.png",
115
187
  options: {
116
- native: false, # force ruby pure
188
+ native: false,
117
189
  class: "value_not_matcher",
118
190
  color: {r: 241, g: 156, b: 156, a: 255}
119
191
  }
192
+ )
193
+ ```
120
194
 
195
+ ## Performance Native vs Pure
196
+
197
+ One of the largest examples included in the test suite processes the image `sample_1200x800.png`.
198
+
199
+ Pure Ruby:
200
+
201
+ ```ruby
202
+ {
203
+ scan: 775.435,
204
+ build_tangs_sequence: 38.916,
205
+ plot: 101.876,
206
+ compress: 0.002,
207
+ total: 916.229
208
+ }
121
209
  ```
122
210
 
123
- You can bypass the helper and access low level (here the CPP classes)
211
+ Native C++:
212
+
213
+ ```ruby
214
+ {
215
+ scan: 5.077,
216
+ build_tangs_sequence: 0.697,
217
+ plot: 2.004,
218
+ compress: 0.000,
219
+ total: 7.781
220
+ }
221
+ ```
222
+
223
+ On the reference machine used for development (AMD Ryzen 7 3700X, Ubuntu, 64 GB RAM), the native implementation is roughly 130× faster for this workload.
224
+
225
+ As always with benchmarks, these numbers should be considered indicative rather than absolute. Actual performance depends on image content, compression settings and hardware.
226
+
227
+
228
+ ## Working with the C++ API
229
+
230
+ The helper method is convenient for most applications, but it is also possible to access the native classes directly.
231
+
232
+ This gives more control over bitmap creation, matcher selection and polygon finder configuration.
124
233
 
125
234
  ```ruby
126
235
  png_bitmap = CPPPngBitMap.new("labyrinth3.png")
127
- rgb_matcher = CPPRGBNotMatcher.new(png_bitmap.rgb_value_at(0, 0))
128
- polygonfinder = CPPPolygonFinder.new(png_bitmap,
236
+
237
+ rgb_matcher = CPPRGBNotMatcher.new(
238
+ png_bitmap.rgb_value_at(0, 0)
239
+ )
240
+
241
+ polygonfinder = CPPPolygonFinder.new(
242
+ png_bitmap,
129
243
  rgb_matcher,
130
244
  nil,
131
- {versus: :a, compress: {visvalingam: true, visvalingam_tolerance: 1.5}})
245
+ {
246
+ versus: :a,
247
+ compress: {
248
+ visvalingam: true,
249
+ visvalingam_tolerance: 1.5
250
+ }
251
+ }
252
+ )
253
+
132
254
  result = polygonfinder.process_info
133
- # draws the polygons found
134
- Contrek::Bitmaps::Painting.direct_draw_polygons(result.points, png_image)
135
- png_image.save('result.png') # => inspect the image to feedback the result
255
+
256
+ Contrek::Bitmaps::Painting.direct_draw_polygons(
257
+ result.points,
258
+ png_image
259
+ )
260
+
261
+ png_image.save("result.png")
136
262
  ```
137
263
 
138
- You can also read base64 png images
264
+ Using the low-level API is useful when building custom processing pipelines or when more control over the tracing process is required.
265
+
266
+
267
+ ## Reading PNG images from Base64
268
+
269
+ When image data is already available as a Base64 string there is no need to write it to disk first.
270
+
139
271
  ```ruby
140
- png_bitmap = CPPRemotePngBitMap.new("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+P+/HgAFhAJ/wlseKgAAAABJRU5ErkJggg==")
272
+ png_bitmap = CPPRemotePngBitMap.new(
273
+ "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+P+/HgAFhAJ/wlseKgAAAABJRU5ErkJggg=="
274
+ )
141
275
  ```
142
276
 
143
- You can process from a raw stream
277
+
278
+ ## Processing raw memory buffers
279
+
280
+ Contrek is not limited to PNG files.
281
+
282
+ Images can also be created directly in memory, making the library suitable for integration with image-processing pipelines that never touch the filesystem.
283
+
284
+ The following example creates a small bitmap, draws a rectangle and extracts its contour.
285
+
144
286
  ```ruby
145
- raw_bitmap = CPPRawBitMap.new
146
- # set 20 as width, 30 as height, 4 as bytes per pixel and clears the content (true)
147
- raw_bitmap.define(20,30,4,true)
148
- # draws a polygon
149
- 4.upto(5) do |y|
150
- 5.upto(8) do |x|
151
- raw_bitmap.draw_pixel(x, y, 1, 0, 0, 0);
152
- end
287
+ raw_bitmap = CPPRawBitMap.new
288
+
289
+ raw_bitmap.define(20,30,4,true)
290
+
291
+ 4.upto(5) do |y|
292
+ 5.upto(8) do |x|
293
+ raw_bitmap.draw_pixel(x, y, 1, 0, 0, 0)
153
294
  end
154
- not_matcher = CPPRGBNotMatcher.new(raw_bitmap.rgb_value_at(0, 0))
155
- result = CPPPolygonFinder.new(raw_bitmap, not_matcher, nil,{compress: {uniq: true, linear: true}}).process_info
156
- puts result.points.inspect
157
- =>
158
- [{:outer=>[{x: 5, y: 4}, {x: 5, y: 6}, {x: 9, y: 6}, {x: 9, y: 4}], :inner=>[]}]
295
+ end
296
+
297
+ not_matcher = CPPRGBNotMatcher.new(
298
+ raw_bitmap.rgb_value_at(0,0)
299
+ )
300
+
301
+ result = CPPPolygonFinder.new(
302
+ raw_bitmap,
303
+ not_matcher,
304
+ nil,
305
+ {
306
+ compress:{
307
+ uniq:true,
308
+ linear:true
309
+ }
310
+ }
311
+ ).process_info
312
+
313
+ puts result.points.inspect
159
314
  ```
160
315
 
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.
316
+ which produces
317
+
318
+ ```ruby
319
+ [
320
+ {
321
+ :outer=>[
322
+ {x:5,y:4},
323
+ {x:5,y:6},
324
+ {x:9,y:6},
325
+ {x:9,y:4}
326
+ ],
327
+ :inner=>[]
328
+ }
329
+ ]
330
+ ```
331
+
332
+ This interface is particularly useful when image data comes from another application, a network stream or an in-memory processing stage rather than from a PNG file.
333
+
334
+ # Multithreading
335
+
336
+ Both the native C++ implementation and the pure Ruby implementation expose the same multithreading interface.
337
+
338
+ There is, however, an important difference between the two.
339
+
340
+ The native implementation performs contour extraction in parallel and can take advantage of all available CPU cores.
341
+
342
+ The pure Ruby implementation, on the other hand, is limited by MRI's Global Interpreter Lock (GIL). Although multiple threads can be created, they do not execute Ruby code simultaneously, so CPU-bound workloads remain effectively sequential.
343
+
344
+ Alternative runtimes such as JRuby or TruffleRuby do not have this limitation, although they have not been tested with Contrek.
345
+
346
+ A simple multithreaded example:
163
347
 
164
348
  ```ruby
165
349
  result = Contrek.contour!(
@@ -169,21 +353,60 @@ result = Contrek.contour!(
169
353
  native: false,
170
354
  class: "value_not_matcher",
171
355
  color: {r: 255, g: 255, b: 255, a: 255},
172
- finder: {number_of_tiles: 2, compress: {uniq: true, linear: true}}
356
+ finder: {
357
+ number_of_tiles: 2,
358
+ compress: {
359
+ uniq: true,
360
+ linear: true
361
+ }
362
+ }
173
363
  }
174
364
  )
175
365
  ```
176
- 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.
366
+
367
+ In this example the image is divided into two vertical stripes and each stripe is assigned to a worker thread.
368
+
369
+ Instead of specifying the number of threads explicitly, you can also let Contrek choose a suitable value automatically.
177
370
 
178
371
  ```ruby
179
- number_of_threads: nil
372
+ number_of_threads: nil
180
373
  ```
181
374
 
182
- Regarding multithreading:
375
+ The native implementation uses the number of available CPU cores reported by the operating system.
183
376
 
184
- - 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.
377
+ ## A note about determinism
185
378
 
186
- 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 0.328 secs with image load).
379
+ The tracing stage itself is deterministic.
380
+
381
+ The merge stage is not.
382
+
383
+ Each stripe is processed independently and adjacent stripes are merged as soon as they become available. Depending on thread scheduling, the merge order may differ from one execution to another.
384
+
385
+ For example, given three stripes:
386
+
387
+ ```
388
+ B1 B2 B3
389
+ ```
390
+
391
+ one execution may merge them like this:
392
+
393
+ ```
394
+ (B1 + B2) + B3
395
+ ```
396
+
397
+ while another execution may produce:
398
+
399
+ ```
400
+ B1 + (B2 + B3)
401
+ ```
402
+
403
+ Both executions generate equivalent polygons, but the order of intermediate merge operations is differentso the final coordinate sequence is not guaranteed to be byte-for-byte identical across executions.
404
+
405
+ ## Native execution
406
+
407
+ When the `native` option is omitted, Contrek automatically uses the C++ implementation.
408
+
409
+ For example, the following configuration processes a 105 MP image using four worker threads and four processing tiles.
187
410
 
188
411
  ```ruby
189
412
  result = Contrek.contour!(
@@ -192,135 +415,205 @@ result = Contrek.contour!(
192
415
  number_of_threads: 4,
193
416
  class: "value_not_matcher",
194
417
  color: {r: 255, g: 255, b: 255, a: 255},
195
- finder: {number_of_tiles: 4, compress: {uniq: true}}
418
+ finder: {
419
+ number_of_tiles: 4,
420
+ compress: {
421
+ uniq: true
422
+ }
423
+ }
196
424
  }
197
425
  )
198
- puts result.metadata[:benchmarks].inspect
199
426
 
200
- { compress: 5.3933,
427
+ puts result.metadata[:benchmarks]
428
+ ```
429
+
430
+ Example timings:
431
+
432
+ ```ruby
433
+ {
434
+ compress: 5.3933,
201
435
  init: 322.749,
202
436
  inner: 4.795,
203
437
  outer: 81.546,
204
438
  total: 328.142
205
439
  }
206
-
207
440
  ```
208
441
 
209
- ## Tracking & Topology Schema
442
+ The reported timings include tracing, polygon reconstruction and coordinate compression.
443
+
444
+ # Tracking model
210
445
 
211
- Contrek utilizes a **Dual-Grid / Cell-Based Topology** instead of a classic point-pixel model. Each pixel is treated as a solid 2D cell defined by **4 distinct vertices** (nodes).
446
+ Contrek works on a cell-based representation. Each pixel is treated as a square with four explicit vertices. During tracing, polygons are built by walking along the edges separating matching and non-matching cells.
212
447
 
448
+ This representation makes it easier to preserve topology and simplifies the reconstruction of polygons when different image stripes are merged together.
213
449
 
214
- ### Pixel-Cell Geometry
450
+ ### Pixel geometry
215
451
 
216
452
  ```text
217
453
  (x, y) Top Edge (x+1, y)
218
454
  O--------------------------------O
219
455
  | |
220
456
  | |
221
- Left | PIXEL | Right
222
- Edge | (x, y) | Edge
457
+ Left | PIXEL | Right
458
+ Edge | (x, y) | Edge
223
459
  | |
224
460
  | |
225
461
  O--------------------------------O
226
- (x, y+1) Bottom Edge (x+1, y+1)
227
-
462
+ (x, y+1) Bottom Edge (x+1, y+1)
228
463
  ```
229
464
 
230
- ### Connectivity Modes
231
465
 
232
- Contrek supports different pixel connectivity rules to define how polygons are traced and merged. This is controlled by the `:connectivity` option.
466
+ ## Connectivity
467
+
468
+ Neighbouring pixels can be connected using either **4-connectivity** or **8-connectivity**.
233
469
 
234
- | Value | Name | Type | Description |
235
- | :--- | :--- | :--- | :--- |
236
- | `4` | **Orthogonal** | Default | Only pixels sharing an edge (N, S, E, W) are connected. |
237
- | `8` | **Omnidirectional** | Extended | Includes diagonal neighbors (sharing a corner). |
470
+ With **4-connectivity**, only pixels sharing an edge belong to the same region.
238
471
 
472
+ With **8-connectivity**, diagonal neighbours are also considered connected.
239
473
 
240
- To enable **Omnidirectional** (8-way) connectivity, pass `connectivity: 8` in the options hash:
474
+ | Value | Description |
475
+ |------:|-------------|
476
+ | `4` | Orthogonal connectivity (default) |
477
+ | `8` | Includes diagonal neighbours |
478
+
479
+ To enable 8-connectivity:
241
480
 
242
481
  ```ruby
243
482
  result = Contrek.contour!(
244
- png_file_path: "./spec/files/images/sample_10240x10240.png",
483
+ png_file_path: "...",
245
484
  options: {
246
- number_of_threads: 4,
247
- class: "value_not_matcher",
248
- color: {r: 255, g: 255, b: 255, a: 255},
249
485
  finder: {
250
- number_of_tiles: 4,
251
- connectivity: 8, # 8 for Omnidirectional, 4 for Orthogonal
252
- compress: {uniq: true}}
486
+ connectivity: 8
487
+ }
253
488
  }
254
489
  )
255
490
  ```
256
491
 
257
- ## Result
492
+ The choice mainly affects how diagonally touching regions are interpreted.
493
+
494
+ # Results
495
+
496
+ The tracing result contains two kinds of information:
258
497
 
259
- The result structure contains polygon coordinates and a set of metadata. Polygon coordinates can be accessed via:
498
+ - the extracted polygons;
499
+ - metadata describing how they were produced.
500
+
501
+ Polygon coordinates are available through
260
502
 
261
503
  ```ruby
262
504
  result.polygons
263
-
264
- [{:outer=>[11, 2, 11, 5, 6, 5, 6, 2],
265
- :inner=>[[10, 3, 7, 3, 7, 4, 10, 4]]}]
266
505
  ```
267
506
 
268
- For native classes (C/C++), coordinates are represented as an interleaved NumPy array (`ndarray`) in the form:
507
+ The native implementation stores coordinates as an interleaved array
269
508
 
270
- ```
509
+ ```text
271
510
  [x0, y0, x1, y1, ...]
272
511
  ```
273
512
 
274
- A ruby side helper method could converts this data structure into point-based representations:
513
+ This representation is compact and avoids allocating thousands of small point objects.
514
+
515
+ If you prefer a more Ruby-friendly representation you can use
275
516
 
276
517
  ```ruby
277
518
  result.points
519
+ ```
520
+
521
+ which converts the same data into hashes containing `x` and `y`.
278
522
 
279
- [{:outer=>[{:x=>11, :y=>2}, {:x=>11, :y=>5}, {:x=>6, :y=>5}, {:x=>6, :y=>2}],
280
- :inner=>[[{:x=>10, :y=>3}, {:x=>7, :y=>3}, {:x=>7, :y=>4}, {:x=>10, :y=>4}]]}]
523
+ ```ruby
524
+ [
525
+ {
526
+ outer: [
527
+ {x:11,y:2},
528
+ {x:11,y:5},
529
+ {x:6,y:5},
530
+ {x:6,y:2}
531
+ ],
532
+ inner: [
533
+ [
534
+ {x:10,y:3},
535
+ {x:7,y:3},
536
+ {x:7,y:4},
537
+ {x:10,y:4}
538
+ ]
539
+ ]
540
+ }
541
+ ]
281
542
  ```
282
543
 
283
- - `outer` represents a single polygonal sequence
284
- - `inner` is represented as an array of polygonal sequences (holes)
544
+ The conversion is performed only on the Ruby side. Internally, the native implementation continues to use the compact coordinate array.
545
+
546
+ For pure Ruby (`native: false`) both methods return the same point-based representation.
285
547
 
286
- For pure Ruby implementations (`{ native: false }`), coordinates are always expressed as points.
287
- In this case, both `polygons` and `points` return the same data, represented as hashes with `x` and `y` keys.
288
548
 
289
- For each returned polygon, it is possible to get its bounding box. You just need to request it as an option (`bounds`), for example:
549
+ ## Bounding boxes
550
+
551
+ If bounding boxes are required they can be generated during tracing instead of computing them afterwards.
552
+
553
+ Enable them with
554
+
290
555
  ```ruby
291
- {named_sequences: true, bounds: true, compress: {uniq: true, linear: true}}
556
+ {
557
+ bounds: true
558
+ }
292
559
  ```
293
- Each entry will contain the precalculated bounds key:
560
+
561
+ Each polygon will then include an additional `bounds` entry.
562
+
294
563
  ```ruby
295
- [{:bounds=>{:min_x=>3, :max_x=>11, :min_y=>1, :max_y=>3},
296
- :outer=>[{:x=>3, :y=>1}, {:x=>3, :y=>3}, {:x=>11, :y=>3}, {:x=>11, :y=>1}],
297
- :inner=>[]}]
564
+ {
565
+ bounds:{
566
+ min_x:3,
567
+ max_x:11,
568
+ min_y:1,
569
+ max_y:3
570
+ },
571
+ outer:[...],
572
+ inner:[...]
573
+ }
298
574
  ```
299
575
 
576
+ Since bounds are computed while polygons are being built, requesting them has almost no additional cost.
577
+
300
578
 
301
- ## Metadata
579
+ # Metadata
302
580
 
303
- Metadata associated with the result can be accessed via:
581
+ Execution metadata is available through
304
582
 
305
583
  ```ruby
306
584
  result.metadata
585
+ ```
586
+
587
+ Typical information includes
307
588
 
308
- {:benchmarks=>{"build_tangs_sequence"=>0.00577,
309
- "compress"=>0.01239,
310
- "plot"=>0.01919,
311
- "scan"=>0.01473,
312
- "total"=>0.05208},
313
- :groups=>1,
314
- :named_sequence=>"AFEDCBA",
315
- :treemap=>[],
316
- :options=>{versus: :a, compress: {visvalingam: true, visvalingam_tolerance: 1.5}}
589
+ - execution times;
590
+ - tracing options;
591
+ - number of polygon groups;
592
+ - nesting information.
593
+
594
+ ```ruby
595
+ {
596
+ benchmarks:{...},
597
+ groups:1,
598
+ treemap:[],
599
+ options:{...}
317
600
  }
318
601
  ```
319
- The options are the original options passed by user.
320
602
 
321
- ## Treemap
603
+ The content of `options` is simply the configuration originally passed to the tracing engine.
604
+
322
605
 
323
- 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).
606
+ # Treemap
607
+
608
+ Besides returning polygon coordinates, Contrek can also describe how polygons are nested inside one another.
609
+
610
+ This information is stored in the **treemap**, which records the parent-child relationship between every detected polygon.
611
+
612
+ Rather than rebuilding the hierarchy afterwards through geometric containment tests, the information is collected during tracing, making it immediately available once processing completes.
613
+
614
+ Each entry in the treemap refers to the corresponding polygon returned by `result.polygons`.
615
+
616
+ For example consider the above image traced clockwise (o).
324
617
 
325
618
  ```ruby
326
619
  "AAAAAAAAAAAAAAAAAAAAAA" \
@@ -339,19 +632,18 @@ The treemap is a data structure that represents the containment hierarchy of pol
339
632
  ```ruby
340
633
  result.metadata[:treemap]
341
634
 
342
- [[-1, -1], # A
343
- [0, 0], # B
344
- [1, 1], # C
345
- [1, 0]] # D
635
+ [
636
+ [-1, -1], # A
637
+ [0, 0], # B
638
+ [1, 1], # C
639
+ [1, 0] # D
640
+ ]
346
641
  ```
347
642
 
348
- There are four polygons (`A`, `B`, `C`, and `D`).
349
- The order matches the one provided in `result.polygons`.
350
-
351
- Each entry has the structure:
643
+ Each element has the form
352
644
 
353
- ```
354
- [parent_index, index_of_parent_inner_sequence]
645
+ ```text
646
+ [parent_polygon_index, parent_inner_sequence_index]
355
647
  ```
356
648
 
357
649
  - Polygon **A** (index `0`) has no parent and is represented as `[-1, -1]`
@@ -388,11 +680,14 @@ result.metadata[:treemap]
388
680
  C, D and E will get the same pair (**[1, 0]**) because are all placed inside the first (0) inner sequence of B.
389
681
 
390
682
 
391
- ## Multithreaded approach
683
+ # How stripe merging works
392
684
 
393
- The multithreaded contour-tracing implementation operates as follows:
685
+ The contour tracing itself is relatively straightforward.
686
+
687
+ The more challenging part is reconstructing polygons that cross the boundary between two independently processed image stripes.
688
+
689
+ This reconstruction step is what allows Contrek to support both streaming and parallel execution without changing the resulting geometry.
394
690
 
395
- 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.
396
691
  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).
397
692
  The vertical column at x=9 is therefore the shared region.
398
693
 
@@ -401,62 +696,74 @@ The vertical column at x=9 is therefore the shared region.
401
696
  ---------*----------
402
697
  ```
403
698
 
404
- Each band is processed independently as if it were a standalone image, with its contour-tracing assigned to a dedicated thread.
405
- Whenever two adjacent bands have completed processing, they are assigned to an available thread that performs the merge of their coordinates.
699
+ Each stripe is processed independently, exactly as if it were a separate image.
406
700
 
407
- For example, with three bands
408
- B1 – B2 – B3
409
- the merging sequence might be:
701
+ The shared column guarantees that contours intersecting the boundary are visible from both sides.
410
702
 
411
- B1 + B2 (B1+B2) + B3
412
- or
703
+ Once tracing has completed, adjacent stripes are merged.
413
704
 
414
- B2 + B3 B1 + (B2+B3)
705
+ The merge algorithm identifies polygons that continue across the shared boundary, splits them into matching segments and reconnects them into a single continuous contour.
415
706
 
416
- The order is therefore nondeterministic, constrained only by the possible combinations derived from the initial number of bands.
707
+ The same operation is then repeated recursively until only one stripe remains.
417
708
 
418
- 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.
419
- 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.
709
+ In other words, Contrek never attempts to trace the whole image at once.
420
710
 
421
- This process is applied recursively, merging bands until a single final band remains, upon which the coordinate-compression algorithms are executed.
711
+ Instead, it repeatedly combines smaller topologically consistent pieces into a larger one.
712
+
713
+ This strategy makes it possible to process images whose full size would otherwise exceed the available memory.
714
+
715
+
716
+ ## Merge order
717
+
718
+ The merge order is intentionally left to the thread scheduler.
719
+
720
+ For three stripes, both of the following execution orders are valid:
422
721
 
423
- ## Performances native vs pure
424
- 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).
425
- On pure ruby implementation kept time
426
- ```ruby
427
- { :scan=>775.435,
428
- :build_tangs_sequence=>38.916,
429
- :plot=>101.876,
430
- :compress=>0.002,
431
- :total=>916.229
432
- }
433
722
  ```
434
- This the one for the native C++
435
- ```ruby
436
- { scan: 5.077878999999999,
437
- build_tangs_sequence: 0.697222999999999,
438
- plot: 2.00479,
439
- compress: 0.00071,
440
- total: 7.780602
441
- }
723
+ (B1 + B2) + B3
724
+ ```
725
+
726
+ or
727
+
442
728
  ```
729
+ B1 + (B2 + B3)
730
+ ```
731
+
732
+ Although the intermediate merge sequence changes, the reconstructed polygons remain geometrically equivalent.
733
+
734
+ The exact ordering of coordinates may differ because polygon simplification is applied after merging.
735
+
736
+
737
+ ## Polygon reconstruction
738
+
739
+ Internally, polygons crossing a stripe boundary are temporarily divided into smaller pieces.
443
740
 
444
- About 130x faster. Times are in microseconds; system: AMD Ryzen 7 3700X 8-Core Processor (BogoMIPS: 7199,99) on Ubuntu distro.
741
+ The merge stage reconnects these pieces by matching their endpoints inside the shared region.
445
742
 
446
- **Note:** Benchmarks were measured inside a VMware virtual machine.
743
+ Outer boundaries are reconstructed first.
447
744
 
448
- ## 🛠 C++ Standalone Library Usage
745
+ Inner boundaries (holes) are processed afterwards and inserted back into their corresponding outer polygon.
449
746
 
450
- The core of **Contrek** is a high-performance `C++17` library. It is designed to be **standalone**, meaning it has zero dependencies on Ruby and can be integrated into any `C++` project.
747
+ This separation considerably simplifies the reconstruction logic while preserving the topology of the original image.
451
748
 
749
+ Once every stripe has been merged into a single dataset, the optional coordinate-compression algorithms are applied.
452
750
 
453
- ### Prerequisites
454
- * **CMake** (3.10+)
455
- * **ZLIB** (Required for PNG processing)
456
- * **C++17 Compiler**
751
+ At this point the resulting polygons are identical to those that would have been obtained by processing the image in a single pass, while requiring substantially less memory.
457
752
 
458
- ### 1. Build & Run Examples
459
- If you want to test the performances or see the library in action:
753
+
754
+ # Standalone C++ library
755
+
756
+ Although Contrek is distributed as a Ruby gem, the tracing engine itself is a standalone C++17 library.
757
+
758
+ The C++ code has no dependency on Ruby and can be integrated into other projects directly.
759
+
760
+ ## Requirements
761
+
762
+ - CMake 3.10 or newer
763
+ - A C++17 compiler
764
+ - ZLIB (required for PNG support)
765
+
766
+ ## Building the examples
460
767
 
461
768
  ```bash
462
769
  # Navigate to the core folder
@@ -473,12 +780,12 @@ make
473
780
  ./contrek_test
474
781
  ```
475
782
 
476
- ### 2. How to integrate it into your project
783
+ The examples are a good starting point for understanding the native API and evaluating the library without involving Ruby.
477
784
 
478
- To use Contrek's engine in your own C++ application without Ruby:
479
785
 
480
- 1. **Copy the core folder**: Take the `ext/cpp_polygon_finder/PolygonFinder` directory and place it inside your project (e.g., in a `libs/` or `third_party/` folder).
481
- 2. **Update your CMakeLists.txt**: Add these lines to link the library:
786
+ ## Integrating the library
787
+
788
+ The easiest way to embed Contrek into an existing C++ project is to copy the `PolygonFinder` directory into your source tree and add it as a CMake subdirectory.
482
789
 
483
790
  ```cmake
484
791
  # Tell CMake to include Contrek
@@ -489,9 +796,12 @@ add_executable(my_app main.cpp)
489
796
  target_link_libraries(my_app PRIVATE ContrekLib)
490
797
  ```
491
798
 
492
- ### 3. C++ API Quick Start
799
+ No Ruby components are required.
800
+
801
+
802
+ ## Basic example
493
803
 
494
- Contrek provides a high-level C++ API. Here is how you can use it in your standalone projects:
804
+ A minimal program looks like this:
495
805
 
496
806
  ```cpp
497
807
  #include "ContrekApi.h"
@@ -515,7 +825,10 @@ int main() {
515
825
  }
516
826
  ```
517
827
 
518
- ## License
828
+ The API intentionally stays close to the concepts used by the Ruby wrapper, making it easy to switch between the two interfaces.
829
+
830
+
831
+ # License
519
832
 
520
833
  Contrek uses a dual-license model:
521
834
 
@@ -524,11 +837,12 @@ Contrek uses a dual-license model:
524
837
 
525
838
  See [LICENSE.md](LICENSE.md) for full details.
526
839
 
527
- ## Changelog
840
+
841
+ # Changelog
528
842
 
529
843
  See [CHANGELOG.md](CHANGELOG.md) for a complete list of changes.
530
844
 
531
- ## History
845
+ # History
532
846
  The algorithm was originally developed by me in 2018 when I was commissioned to create a Rails web application whose main objective was to census buildings from GoogleMAPS; the end user had to be able to select their home building by clicking its roof on the map which had to be identified as a clickable polygon. The solution was to configure GoogleMAPS to render buildings of a well-defined color (red), and at each refresh of the same to transform the div into an image (html2canvas) then process it server side returning the polygons to be superimposed again on the map. This required very fast polygons determination. Searching for a library for tracing the contours I was not able to find anything better except OpenCV which however seemed to me a very heavy dependency. So I decided to write my algorithm directly in the context of the ROR application. Once perfected, it was already usable but a bit slow in the higher image resolutions. So I decided to write the counterpart in C++, which came out much faster and which I then used as an extension on Ruby by means of Rice.
533
847
 
534
848
  I thought that the algorithm had excellent qualities but I never had the time to develop it further. A few months ago I decided to publish it as a gem, freely usable. The gem includes the C++ extension. There is also a [Rails 7.1 demo project](https://github.com/runout77/contrek_rails) that uses the gem and proposes the same scheme I described above. Starting from a GoogleMAPS map, the server receives the image and returns the outlines to be drawn again on the same. It is a great way to test an applicative use of this gem. Enjoy!.
@@ -132,3 +132,12 @@ Node* Node::get_tangent_node_by_virtual_index(int virtual_index) {
132
132
  return &(this->cluster->vert_nodes[y + T_DOWN][virtual_index]);
133
133
  }
134
134
  }
135
+
136
+ void Node::assign_lateral_inner_index(Node* my_prev_node, Node* end_node, int versus) {
137
+ bool first_is_max = ((this->y > my_prev_node->y) == (versus == Node::A));
138
+ if (first_is_max) {
139
+ this->inner_right_index = end_node->inner_index;
140
+ } else {
141
+ this->inner_left_index = end_node->inner_index;
142
+ }
143
+ }
@@ -113,6 +113,7 @@ class Node : public Listable {
113
113
  int min_x, max_x;
114
114
  Node(int min_x, int max_x, int y, NodeCluster* cluster, char name);
115
115
  void precalc_tangs_sequences(NodeCluster& cluster);
116
+ void assign_lateral_inner_index(Node* my_prev_node, Node* end_node, int versus);
116
117
  bool processed = false;
117
118
  char name;
118
119
  int inner_left_index = -1;
@@ -292,13 +292,6 @@ Node* NodeCluster::plot_inner_node(std::vector<Point>& sequence_coords, Node *no
292
292
  Node *next_node = current_node->my_next_inner(last_node, versus);
293
293
  plot_sequence.push_back(current_node);
294
294
 
295
- bool first_is_max = ((current_node->y > last_node->y) == (versus == Node::A));
296
- if (first_is_max) {
297
- if (current_node->inner_right_index == -1) current_node->inner_right_index = stop_at->inner_index;
298
- } else {
299
- if (current_node->inner_left_index == -1) current_node->inner_left_index = stop_at->inner_index;
300
- }
301
-
302
295
  bool plot = true;
303
296
  if (next_node->y == last_node->y) {
304
297
  Node *n;
@@ -309,6 +302,7 @@ Node* NodeCluster::plot_inner_node(std::vector<Point>& sequence_coords, Node *no
309
302
  plot = (n == next_node);
310
303
  }
311
304
  if (plot) {
305
+ current_node->assign_lateral_inner_index(last_node, stop_at, versus);
312
306
  Point first_point = last_node->coords_entering_to(current_node, versus_inverter[versus], Node::INNER);
313
307
  if (sequence_coords.back() != first_point) {
314
308
  sequence_coords.push_back(first_point);
@@ -338,7 +332,12 @@ Node* NodeCluster::plot_inner_node(std::vector<Point>& sequence_coords, Node *no
338
332
  } else {
339
333
  inner_new->remove(current_node);
340
334
  }
341
- if (next_node == stop_at) break;
335
+ if (next_node == stop_at) {
336
+ if (plot) {
337
+ next_node->assign_lateral_inner_index(current_node, stop_at, versus);
338
+ }
339
+ break;
340
+ }
342
341
  current_node = next_node;
343
342
  }
344
343
  return current_node;
@@ -105,6 +105,17 @@ module Contrek
105
105
  get_tangent_node_by_virtual_index(@tangs_sequence.at(last_node_index))
106
106
  end
107
107
 
108
+ def assign_lateral_inner_index(my_prev_node, end_node, versus)
109
+ is_moving_up = y > my_prev_node.y
110
+ is_forward = (versus == :a)
111
+ first_is_max = (is_moving_up == is_forward)
112
+ if first_is_max
113
+ self.inner_right_index = end_node.inner_index
114
+ else
115
+ self.inner_left_index = end_node.inner_index
116
+ end
117
+ end
118
+
108
119
  def coords_entering_to(enter_to, enter_mode, tracking)
109
120
  enter_to_index = if enter_to.y < y
110
121
  enter_to.abs_x_index + @up_indexer
@@ -233,13 +233,6 @@ module Contrek
233
233
  next_node = node.my_next(last_node, versus, :inner)
234
234
  @plot_sequence << node
235
235
 
236
- first_is_max = ((node.y > last_node.y) == (versus == :a))
237
- if first_is_max
238
- node.inner_right_index = stop_at.inner_index if node.inner_right_index == -1
239
- elsif node.inner_left_index == -1
240
- node.inner_left_index = stop_at.inner_index
241
- end
242
-
243
236
  plot = true
244
237
  if next_node.y == last_node.y
245
238
  virtual_index = node.tangs_sequence.send((versus == :a) ? :first : :last)
@@ -247,6 +240,7 @@ module Contrek
247
240
  end
248
241
 
249
242
  if plot
243
+ node.assign_lateral_inner_index(last_node, stop_at, versus)
250
244
  first_point = last_node.coords_entering_to(node, VERSUS_INVERTER[versus], Contrek::Finder::Node::INNER)
251
245
  @sequence_coords << first_point if @sequence_coords.last != first_point
252
246
  if next_node.y == last_node.y
@@ -271,8 +265,10 @@ module Contrek
271
265
  else
272
266
  @inner_new.delete(node)
273
267
  end
274
-
275
- return node if next_node == stop_at
268
+ if next_node == stop_at
269
+ next_node.assign_lateral_inner_index(node, stop_at, versus) if plot
270
+ return node
271
+ end
276
272
  plot_inner_node(next_node, versus, stop_at, start_node)
277
273
  end
278
274
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Contrek
4
- VERSION = "1.3.7"
4
+ VERSION = "1.3.8"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: contrek
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.3.7
4
+ version: 1.3.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - Emanuele Cesaroni
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-07-29 00:00:00.000000000 Z
11
+ date: 2026-08-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rspec