contrek 1.1.3 → 1.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 60fae59ed14c24cedd7dadfdc33dea05188b1131ee6443b59591e5243cb4710b
4
- data.tar.gz: b565f7a0e9713a9389329cf1cc44f9c2da36908d75c538af625afdb6e1d9d71a
3
+ metadata.gz: 2b2690fa75c9a6671f9c8ea91522d3adaf8600bfc0739dad4c8eb4a14b80d97e
4
+ data.tar.gz: 38db9719cc44356ec32d33031e877edda131a970ec0fb5e385f9c31e19699858
5
5
  SHA512:
6
- metadata.gz: 86ece9b65041fae9aec64173f5e3bb719971852a6327953b78dc6567f94018db544cc8b7e062d57aad3a859f8142982406e1d9301716f2a49eaa8a05a586ddfc
7
- data.tar.gz: 87a22df67016a3249a069cec0bee1746ddb92c1103f9a98e0a8e85b2240ae7d4be735123cfa72443432b30a91be333c4d195b384b194f5703a075b4832e07bd6
6
+ metadata.gz: 4827a05d797400b0245c8225274dc070870e83186d51da4b4f16bb8368359597967012b65cedbff39f808fa9d1eafad5e3012e144f10afb801d564a2ff916dca
7
+ data.tar.gz: 690435e9531d0c8c47c703e41c4719333b91edbe60232a5183c30e21a2822c06b827498b49afbda4785cff404e3abdaf776f0a0b4aec9c9b58a0a9fbf29c8844
data/CHANGELOG.md CHANGED
@@ -52,4 +52,9 @@ All notable changes to this project will be documented in this file.
52
52
  ## [1.1.3] - 2026-02-21
53
53
  ### Changed
54
54
  - Added support for 8-way pixel connectivity (omnidirectional) in addition to the standard 4-way mode.
55
- - Optimized C++ and Ruby algorithms for initial spatial pixel tracking to improve performance.
55
+ - Optimized C++ and Ruby algorithms for initial spatial pixel tracking to improve performance.
56
+
57
+ ## [1.1.4] - 2026-02-28
58
+ ### Changed
59
+ - Fixed an infinite loop bug in multithreading during inner sequence joining in Omnidirectional mode.
60
+ - Optimized C++ and Ruby algorithms for initial spatial tangential sequence determination.
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- contrek (1.1.3)
4
+ contrek (1.1.4)
5
5
  chunky_png (~> 1.4)
6
6
  concurrent-ruby (~> 1.3.5)
7
7
  rice (= 4.5.0)
data/README.md CHANGED
@@ -112,7 +112,7 @@ Regarding multithreading:
112
112
 
113
113
  - The treemap option is currently ignored (multithreaded treemap support will be introduced in upcoming revisions).
114
114
 
115
- 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.53 secs with image load).
115
+ 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).
116
116
 
117
117
  ```ruby
118
118
  result = Contrek.contour!(
@@ -126,11 +126,11 @@ result = Contrek.contour!(
126
126
  )
127
127
  puts result.metadata[:benchmarks].inspect
128
128
 
129
- { compress: 13.0815,
130
- init: 453.245,
131
- inner: 27.0072,
132
- outer: 66.9162,
133
- total: 466.326
129
+ { compress: 26.37,
130
+ init: 354.339,
131
+ inner: 17.9427,
132
+ outer: 93.6252,
133
+ total: 380.709
134
134
  }
135
135
 
136
136
  ```
@@ -288,23 +288,24 @@ This process is applied recursively, merging bands until a single final band rem
288
288
  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).
289
289
  On pure ruby implementation kept time
290
290
  ```ruby
291
- { :scan=>801.494,
292
- :build_tangs_sequence=>160.491,
293
- :plot=>86.633,
291
+ { :scan=>775.435,
292
+ :build_tangs_sequence=>38.916,
293
+ :plot=>101.876,
294
294
  :compress=>0.002,
295
- :total=>1048.62}
295
+ :total=>916.229
296
+ }
296
297
  ```
297
298
  This the one for the native C++
298
299
  ```ruby
299
- { scan: 7.1146329999999995,
300
- build_tangs_sequence: 3.063812,
301
- plot: 4.474851999999999,
302
- compress: 0.0031999999999999997
303
- total: 14.656496999999998
300
+ { scan: 5.077878999999999,
301
+ build_tangs_sequence: 0.697222999999999,
302
+ plot: 2.00479,
303
+ compress: 0.00071,
304
+ total: 7.780602
304
305
  }
305
306
  ```
306
307
 
307
- About 75x faster. Times are in microseconds; system: AMD Ryzen 7 3700X 8-Core Processor (BogoMIPS: 7199,99) on Ubuntu distro.
308
+ About 130x faster. Times are in microseconds; system: AMD Ryzen 7 3700X 8-Core Processor (BogoMIPS: 7199,99) on Ubuntu distro.
308
309
 
309
310
  ## 🛠 C++ Standalone Library Usage
310
311
 
@@ -70,7 +70,7 @@ inline std::unique_ptr<ProcessResult> trace(const std::string& image_path, const
70
70
  if (m.flag) internal_args.emplace_back(m.arg);
71
71
  }
72
72
  internal_args.push_back("--number_of_tiles=" + std::to_string(cfg.tiles));
73
- if(cfg.connectivity_mode == Connectivity::OMNIDIRECTIONAL) {
73
+ if (cfg.connectivity_mode == Connectivity::OMNIDIRECTIONAL) {
74
74
  internal_args.push_back("--connectivity=" + std::to_string(8));
75
75
  }
76
76
 
@@ -44,6 +44,8 @@ FastPngBitmap::FastPngBitmap(std::string filename) : Bitmap("", 0) {
44
44
  spng_ctx_free(ctx);
45
45
  this->png_error = error;
46
46
  }
47
+ } else {
48
+ throw std::runtime_error("Unable open file: " + filename);
47
49
  }
48
50
  }
49
51
  }
@@ -17,9 +17,10 @@
17
17
  #include "Node.h"
18
18
  #include "NodeCluster.h"
19
19
 
20
- Node::Node(int min_x, int max_x, int y, char name)
20
+ Node::Node(int min_x, int max_x, int y, NodeCluster* cluster, char name)
21
21
  : start_point(min_x, y),
22
- end_point(max_x, y) {
22
+ end_point(max_x, y),
23
+ cluster(cluster) {
23
24
  this->name = name;
24
25
  this->min_x = min_x;
25
26
  this->max_x = max_x;
@@ -67,24 +68,14 @@ void Node::precalc_tangs_sequences(NodeCluster& cluster) {
67
68
  }
68
69
  // --- CLOCKWISE (UP) ---
69
70
  for (int upper_pos = this->upper_start; upper_pos <= this->upper_end; upper_pos++) {
70
- Node& t_node = cluster.vert_nodes[y + T_UP][upper_pos];
71
- tangs_sequence.emplace_back(NodeDescriptor{
72
- &t_node,
73
- Tangent{ &t_node.end_point, OMAX},
74
- Tangent{ &t_node.start_point, OMIN}
75
- });
71
+ tangs_sequence.push_back(-(upper_pos + 1));
76
72
  }
77
73
  if (this->lower_end >= 0) {
78
74
  this->down_indexer = (cluster.vert_nodes[y + T_DOWN][this->lower_start].abs_x_index + lower_size + upper_size - 1);
79
75
  }
80
76
  // --- COUNTER-CLOCKWISE (DOWN) ---
81
77
  for (int lower_pos = this->lower_end; lower_pos >= this->lower_start; lower_pos--) {
82
- Node& t_node = cluster.vert_nodes[y + T_DOWN][lower_pos];
83
- tangs_sequence.emplace_back(NodeDescriptor{
84
- &t_node,
85
- Tangent{&t_node.start_point, OMIN},
86
- Tangent{&t_node.end_point, OMAX}
87
- });
78
+ tangs_sequence.push_back(lower_pos);
88
79
  }
89
80
  this->tangs_count = this->tangs_sequence.size();
90
81
  }
@@ -95,7 +86,7 @@ Node* Node::my_next_inner(Node *last, int versus) {
95
86
  else last_node_index = this->down_indexer - last->abs_x_index;
96
87
  if (versus == Node::O) last_node_index == 0 ? last_node_index = this->tangs_sequence.size() - 1 : last_node_index--;
97
88
  else last_node_index == this->tangs_sequence.size() - 1 ? last_node_index = 0 : last_node_index++;
98
- return((this->tangs_sequence)[last_node_index].node);
89
+ return get_tangent_node_by_virtual_index(this->tangs_sequence[last_node_index]);
99
90
  }
100
91
 
101
92
  Node* Node::my_next_outer(Node *last, int versus) {
@@ -104,15 +95,42 @@ Node* Node::my_next_outer(Node *last, int versus) {
104
95
  else last_node_index = this->down_indexer - last->abs_x_index;
105
96
  if (versus == Node::O) last_node_index == this->tangs_sequence.size() - 1 ? last_node_index = 0 : last_node_index++;
106
97
  else last_node_index == 0 ? last_node_index = this->tangs_sequence.size() - 1 : last_node_index--;
107
- return((this->tangs_sequence)[last_node_index].node);
98
+ return get_tangent_node_by_virtual_index(this->tangs_sequence[last_node_index]);
108
99
  }
109
100
 
110
101
  Point* Node::coords_entering_to(Node *enter_to, int mode, int tracking) {
111
102
  int enter_to_index;
112
103
  if (enter_to->y < this->y) enter_to_index = enter_to->abs_x_index + this->up_indexer;
113
104
  else enter_to_index = this->down_indexer - enter_to->abs_x_index;
114
- NodeDescriptor ds = (this->tangs_sequence)[enter_to_index];
115
- Tangent t = (mode == Node::O ? ds.o : ds.a);
116
- enter_to->track |= TURNER[tracking][t.mode - 1];
117
- return(t.point);
105
+
106
+ int tg_index = this->tangs_sequence[enter_to_index];
107
+ Point* point;
108
+ if (tg_index < 0) {
109
+ Node& node_up = cluster->vert_nodes[y + T_UP][-(tg_index + 1)];
110
+ if (mode == Node::A) {
111
+ enter_to->track |= TURNER[tracking][OMAX - 1];
112
+ point = &node_up.end_point;
113
+ } else {
114
+ enter_to->track |= TURNER[tracking][OMIN - 1];
115
+ point = &node_up.start_point;
116
+ }
117
+ } else {
118
+ Node& node_down = cluster->vert_nodes[y + T_DOWN][tg_index];
119
+ if (mode == Node::A) {
120
+ enter_to->track |= TURNER[tracking][OMIN - 1];
121
+ point = &node_down.start_point;
122
+ } else {
123
+ enter_to->track |= TURNER[tracking][OMAX - 1];
124
+ point = &node_down.end_point;
125
+ }
126
+ }
127
+ return point;
128
+ }
129
+
130
+ Node* Node::get_tangent_node_by_virtual_index(int virtual_index) {
131
+ if (virtual_index < 0) {
132
+ return &(this->cluster->vert_nodes[y + T_UP][-(virtual_index + 1)]);
133
+ } else {
134
+ return &(this->cluster->vert_nodes[y + T_DOWN][virtual_index]);
135
+ }
118
136
  }
@@ -28,11 +28,6 @@ struct Point {
28
28
  }
29
29
  Point(int x_, int y_) : x(x_), y(y_) {}
30
30
  };
31
- struct Tangent {
32
- Point *point;
33
- int mode;
34
- };
35
- struct NodeDescriptor;
36
31
 
37
32
  class Node : public Listable {
38
33
  public:
@@ -63,24 +58,20 @@ class Node : public Listable {
63
58
  int lower_start = std::numeric_limits<int>::max();
64
59
  int lower_end = -1;
65
60
  Point start_point, end_point;
61
+ NodeCluster* cluster;
66
62
  void add_intersection(Node& other_node, int other_node_index);
67
- std::vector<NodeDescriptor> tangs_sequence;
63
+ std::vector<int> tangs_sequence;
68
64
  Point* coords_entering_to(Node *enter_to, int mode, int tracking);
69
65
  Node* my_next_outer(Node *last, int versus);
70
66
  Node* my_next_inner(Node *last, int versus);
67
+ Node* get_tangent_node_by_virtual_index(int vitual_index);
71
68
  bool track_uncomplete();
72
69
  bool track_complete();
73
70
  bool get_trackmax();
74
71
 
75
72
  public:
76
73
  int min_x, max_x;
77
- Node(int min_x, int max_x, int y, char name);
74
+ Node(int min_x, int max_x, int y, NodeCluster* cluster, char name);
78
75
  void precalc_tangs_sequences(NodeCluster& cluster);
79
76
  bool processed = false;
80
77
  };
81
-
82
- struct NodeDescriptor {
83
- Node *node;
84
- Tangent a;
85
- Tangent o;
86
- };
@@ -75,7 +75,7 @@ void NodeCluster::build_tangs_sequence() {
75
75
  }
76
76
 
77
77
  Node* NodeCluster::add_node(int min_x, int max_x, int y, char name, int offset) {
78
- vert_nodes[y].emplace_back(min_x, max_x, y, name);
78
+ vert_nodes[y].emplace_back(min_x, max_x, y, this, name);
79
79
 
80
80
  Node& node = vert_nodes[y].back();
81
81
  node.abs_x_index = vert_nodes[y].size() - 1;
@@ -115,8 +115,11 @@ void NodeCluster::plot(int versus) {
115
115
  this->plot_sequence.push_back(root_node);
116
116
  Polygon poly;
117
117
 
118
- if ((root_node)->tangs_sequence.size() > 0) // front() on empty list is undefined
119
- { versus == Node::A ? next_node = root_node->tangs_sequence.back().node : next_node = root_node->tangs_sequence.front().node;
118
+ if ((root_node)->tangs_sequence.size() > 0) // front() or back() on empty list is undefined
119
+ { versus == Node::A ?
120
+ next_node = root_node->get_tangent_node_by_virtual_index(root_node->tangs_sequence.back()) :
121
+ next_node = root_node->get_tangent_node_by_virtual_index(root_node->tangs_sequence.front());
122
+
120
123
  if (next_node != nullptr)
121
124
  { Point* p = next_node->coords_entering_to(root_node, versus_inverter[versus], Node::OUTER);
122
125
  poly.outer.push_back(p);
@@ -254,7 +257,11 @@ void NodeCluster::plot_inner_node(std::vector<Point*>& sequence_coords, Node *no
254
257
 
255
258
  bool plot = true;
256
259
  if (next_node->y == last_node->y) {
257
- Node *n = (versus == Node::A ? current_node->tangs_sequence.front().node : current_node->tangs_sequence.back().node);
260
+ Node *n;
261
+ int virtual_index = (versus == Node::A ?
262
+ current_node->tangs_sequence.front() :
263
+ current_node->tangs_sequence.back());
264
+ n = current_node->get_tangent_node_by_virtual_index(virtual_index);
258
265
  plot = (n == next_node);
259
266
  }
260
267
  if (plot) {
@@ -289,7 +296,10 @@ void NodeCluster::plot_node(std::vector<Point*>& sequence_coords, Node *node, No
289
296
 
290
297
  bool plot = true;
291
298
  if (next_node->y == last_node->y) {
292
- Node *n = (versus == Node::A ? current_node->tangs_sequence.back().node : current_node->tangs_sequence.front().node);
299
+ int virtual_index = (versus == Node::A ?
300
+ current_node->tangs_sequence.back() :
301
+ current_node->tangs_sequence.front());
302
+ Node *n = current_node->get_tangent_node_by_virtual_index(virtual_index);
293
303
  plot = (n == next_node);
294
304
  }
295
305
  if (plot) {
@@ -242,7 +242,7 @@ std::vector<Sequence*> Cursor::collect_inner_sequences(Sequence* outer_seq) {
242
242
  for (Part* part : all_parts)
243
243
  { part->touch();
244
244
  retme_sequence->move_from(*part, [&](QNode<Point>* pos) -> bool {
245
- Position *position = dynamic_cast<Position*>(pos);
245
+ Position *position = static_cast<Position*>(pos);
246
246
  if (part->is(Part::ADDED) &&
247
247
  !(position->payload->y >= bounds.min &&
248
248
  position->payload->y <= bounds.max)) {
@@ -283,6 +283,8 @@ void Cursor::traverse_inner(Part* act_part, std::vector<Part*>& all_parts, Bound
283
283
  for (Shape *shape : act_part->polyline()->next_tile_eligible_shapes()) {
284
284
  for (Part* dest_part : shape->outer_polyline->parts()) {
285
285
  if (dest_part->trasmuted || dest_part->is(Part::EXCLUSIVE)) continue;
286
+ int dest_part_versus = dest_part->versus();
287
+ if (dest_part_versus != 0 && dest_part_versus == act_part->versus()) continue;
286
288
  if (dest_part->intersect_part(act_part)) {
287
289
  std::vector<EndPoint*> link_seq = duplicates_intersection(*dest_part, *act_part);
288
290
  if (!link_seq.empty()) {
@@ -9,6 +9,7 @@
9
9
 
10
10
  #pragma once
11
11
  #include <vector>
12
+ #include <unordered_set>
12
13
  #include "Part.h"
13
14
 
14
15
  class Part;
@@ -19,9 +20,9 @@ class EndPoint {
19
20
 
20
21
  void set_point(Point* p) { point_ = p; }
21
22
  Point* get_point() const { return point_; }
22
- std::vector<Queueable<Point>*>& queues() { return queues_; }
23
+ std::unordered_set<Queueable<Point>*>& queues() { return queues_; }
23
24
  bool queues_include(Queueable<Point>* q) const;
24
25
  private:
25
26
  Point* point_;
26
- std::vector<Queueable<Point>*> queues_;
27
+ std::unordered_set<Queueable<Point>*> queues_;
27
28
  };
@@ -64,7 +64,7 @@ void Part::touch()
64
64
  bool Part::intersect_part(Part* other_part)
65
65
  { bool intersect = false;
66
66
  other_part->each([&](QNode<Point>* pos) -> bool {
67
- Position *position = dynamic_cast<Position*>(pos);
67
+ Position *position = static_cast<Position*>(pos);
68
68
  if (position->end_point()->queues_include(this))
69
69
  { intersect = true;
70
70
  return(false);
@@ -78,7 +78,7 @@ std::vector<EndPoint*> Part::to_endpoints() {
78
78
  std::vector<EndPoint*> out;
79
79
  QNode<Point>* current = head;
80
80
  while (current) {
81
- out.push_back((dynamic_cast<Position*>(current))->end_point());
81
+ out.push_back((static_cast<Position*>(current))->end_point());
82
82
  current = current->next;
83
83
  }
84
84
  return out;
@@ -48,7 +48,7 @@ void Partitionable::insert_after(Part* part, Part* new_part) {
48
48
 
49
49
  void Partitionable::partition()
50
50
  { this->parts_.clear();
51
- Polyline *polyline = dynamic_cast<Polyline*>(this);
51
+ Polyline *polyline = static_cast<Polyline*>(this);
52
52
  PartPool& pool = polyline->tile->cluster->parts_pool;
53
53
  Part *current_part = nullptr;
54
54
  int n = 0;
@@ -159,7 +159,7 @@ std::optional<SewReturnData> Partitionable::sew(std::vector<std::pair<int, int>>
159
159
  }
160
160
 
161
161
  std::vector<Part*> all_parts;
162
- Polyline* polyline = dynamic_cast<Polyline*>(this);
162
+ Polyline* polyline = static_cast<Polyline*>(this);
163
163
  all_parts.reserve(before_parts.size() + after_parts.size());
164
164
  all_parts.insert(all_parts.end(), before_parts.begin(), before_parts.end());
165
165
  all_parts.insert(all_parts.end(), after_parts.begin(), after_parts.end());
@@ -208,9 +208,9 @@ void Partitionable::trasmute_parts()
208
208
  if (inside == inside_compare || !inside_compare->is(Part::SEAM) ) continue;
209
209
  int count = 0;
210
210
  inside->each([&](QNode<Point>* pos) -> bool {
211
- Position *position = dynamic_cast<Position*>(pos);
211
+ Position *position = static_cast<Position*>(pos);
212
212
  if (position->end_point()->queues_include(inside_compare))
213
- { count ++;
213
+ { count++;
214
214
  return true;
215
215
  }
216
216
  return false;
@@ -9,6 +9,7 @@
9
9
 
10
10
  #include <vector>
11
11
  #include <limits>
12
+ #include <string>
12
13
  #include <unordered_set>
13
14
  #include <sstream>
14
15
  #include "Polyline.h"
@@ -77,7 +78,7 @@ std::vector<std::pair<int, int>> Polyline::intersection(const Polyline* other) c
77
78
  auto& part = parts_[i];
78
79
  if (!part->is(Part::SEAM) && part->trasmuted) continue;
79
80
  part->each([&](QNode<Point>* pos) -> bool {
80
- Position *position = dynamic_cast<Position*>(pos);
81
+ Position *position = static_cast<Position*>(pos);
81
82
  if (position->end_point() != nullptr)
82
83
  { this->tracked_endpoints[position->end_point()] = i;
83
84
  }
@@ -93,7 +94,7 @@ std::vector<std::pair<int, int>> Polyline::intersection(const Polyline* other) c
93
94
  continue;
94
95
  }
95
96
  other_part->each([&](QNode<Point>* pos) -> bool {
96
- Position *position = dynamic_cast<Position*>(pos);
97
+ Position *position = static_cast<Position*>(pos);
97
98
  auto it = this->tracked_endpoints.find(position->end_point());
98
99
  if (it != this->tracked_endpoints.end()) {
99
100
  int self_index = it->second;
@@ -30,12 +30,9 @@ Position::Position(EndPoint* end_point)
30
30
  }
31
31
 
32
32
  void Position::before_rem(Queueable<Point>* q) {
33
- if (this->end_point_ != nullptr) {
34
- auto& queues = this->end_point_->queues();
35
- queues.erase(std::remove(queues.begin(), queues.end(), q), queues.end());
36
- }
33
+ if (this->end_point_ != nullptr) this->end_point_->queues().erase(q);
37
34
  }
38
35
 
39
36
  void Position::after_add(Queueable<Point>* q) {
40
- if (this->end_point_ != nullptr) this->end_point_->queues().push_back(q);
37
+ if (this->end_point_ != nullptr) this->end_point_->queues().insert(q);
41
38
  }
@@ -189,6 +189,8 @@ module Contrek
189
189
  act_part.polyline.next_tile_eligible_shapes.each do |shape|
190
190
  shape.outer_polyline.parts.each do |dest_part|
191
191
  next if dest_part.trasmuted || dest_part.is?(Part::EXCLUSIVE)
192
+ dest_part_versus = dest_part.versus
193
+ next if dest_part_versus != 0 && dest_part_versus == act_part.versus
192
194
 
193
195
  if dest_part.intersect_part?(act_part)
194
196
  link_seq = duplicates_intersection(dest_part, act_part)
@@ -4,7 +4,7 @@ module Contrek
4
4
  include Listable
5
5
 
6
6
  attr_reader :min_x, :max_x, :y, :name, :tangs_sequence, :tangs_count, :data_pointer,
7
- :upper_start, :upper_end, :lower_start, :lower_end
7
+ :upper_start, :upper_end, :lower_start, :lower_end, :start_point, :end_point
8
8
  attr_accessor :track, :abs_x_index, :outer_index, :inner_index
9
9
 
10
10
  T_UP = -1
@@ -43,10 +43,21 @@ module Contrek
43
43
  @upper_end = -1
44
44
  @lower_start = Float::INFINITY
45
45
  @lower_end = -1
46
-
46
+ @start_point = {x: min_x, y: y}
47
+ @end_point = {x: max_x, y: y}
48
+ @cluster = cluster
47
49
  cluster.add_node(self, connectivity_offset)
48
50
  end
49
51
 
52
+ def get_tangent_node_by_virtual_index(virtual_index)
53
+ return nil if virtual_index.nil?
54
+ if virtual_index < 0
55
+ @cluster.vert_nodes[y + T_UP][-(virtual_index + 1)]
56
+ else
57
+ @cluster.vert_nodes[y + T_DOWN][virtual_index]
58
+ end
59
+ end
60
+
50
61
  def my_next(last, versus, mode)
51
62
  last_node_index = if last.y < y
52
63
  last.abs_x_index + @up_indexer
@@ -69,7 +80,7 @@ module Contrek
69
80
  (last_node_index == tangs_sequence.size - 1) ? last_node_index = 0 : last_node_index += 1
70
81
  end
71
82
  end
72
- tangs_sequence.at(last_node_index)
83
+ get_tangent_node_by_virtual_index(@tangs_sequence.at(last_node_index))
73
84
  end
74
85
 
75
86
  def coords_entering_to(enter_to, enter_mode, tracking)
@@ -78,10 +89,28 @@ module Contrek
78
89
  else
79
90
  @down_indexer - enter_to.abs_x_index
80
91
  end
81
- ds = tangs_sequence[enter_to_index]
82
- coords_source = ds.send(enter_mode)
83
- enter_to.track |= TURNER[tracking][coords_source[:m] - 1]
84
- coords_source[:point]
92
+
93
+ tg_index = @tangs_sequence[enter_to_index]
94
+ if tg_index < 0
95
+ node_up = @cluster.vert_nodes[y + T_UP][-(tg_index + 1)]
96
+ if enter_mode == :a
97
+ enter_to.track |= TURNER[tracking][OMAX - 1]
98
+ point = node_up.end_point
99
+ else
100
+ enter_to.track |= TURNER[tracking][OMIN - 1]
101
+ point = node_up.start_point
102
+ end
103
+ else
104
+ node_down = @cluster.vert_nodes[y + T_DOWN][tg_index]
105
+ if enter_mode == :a
106
+ enter_to.track |= TURNER[tracking][OMIN - 1]
107
+ point = node_down.start_point
108
+ else
109
+ enter_to.track |= TURNER[tracking][OMAX - 1]
110
+ point = node_down.end_point
111
+ end
112
+ end
113
+ point
85
114
  end
86
115
 
87
116
  def track_uncomplete
@@ -107,29 +136,17 @@ module Contrek
107
136
  @up_indexer = -cluster.vert_nodes[@y + T_UP][@upper_start].abs_x_index if @upper_end >= 0
108
137
  if @upper_end >= 0
109
138
  (@upper_start..@upper_end).each do |upper_pos|
110
- t_node = cluster.vert_nodes[@y + T_UP][upper_pos]
111
- @tangs_sequence << Contrek::Finder::PolygonFinder::NodeDescriptor.new(
112
- t_node,
113
- {point: {x: t_node.max_x, y: t_node.y}, m: OMAX},
114
- {point: {x: t_node.min_x, y: t_node.y}, m: OMIN}
115
- )
139
+ @tangs_sequence << -(upper_pos + 1)
116
140
  end
117
141
  end
118
-
119
142
  if @lower_end >= 0
120
143
  lower_size = (@lower_end >= 0) ? (@lower_end - @lower_start + 1) : 0
121
144
  upper_size = (@upper_end >= 0) ? (@upper_end - @upper_start + 1) : 0
122
145
  @down_indexer = (cluster.vert_nodes[@y + T_DOWN][@lower_start].abs_x_index + lower_size + upper_size - 1)
123
146
  end
124
-
125
147
  if @lower_end >= 0
126
148
  @lower_end.downto(@lower_start).each do |lower_pos|
127
- t_node = cluster.vert_nodes[@y + T_DOWN][lower_pos]
128
- @tangs_sequence << Contrek::Finder::PolygonFinder::NodeDescriptor.new(
129
- t_node,
130
- {point: {x: t_node.min_x, y: t_node.y}, m: OMIN},
131
- {point: {x: t_node.max_x, y: t_node.y}, m: OMAX}
132
- )
149
+ @tangs_sequence << lower_pos
133
150
  end
134
151
  end
135
152
  @tangs_count = tangs_sequence.size
@@ -62,21 +62,18 @@ module Contrek
62
62
  bounds = Bounds.empty
63
63
  # external polygon
64
64
  @plot_sequence << root_node
65
- next_node_nd = if versus == :a
66
- root_node.tangs_sequence.last
65
+
66
+ next_node = if versus == :a
67
+ root_node.get_tangent_node_by_virtual_index(root_node.tangs_sequence.last)
67
68
  else
68
- root_node.tangs_sequence.first
69
+ root_node.get_tangent_node_by_virtual_index(root_node.tangs_sequence.first)
69
70
  end
70
- #---------------
71
- if !next_node_nd.nil?
72
- next_node = next_node_nd.node
71
+ if !next_node.nil?
73
72
  coord = next_node.coords_entering_to(root_node, VERSUS_INVERTER[versus], Contrek::Finder::Node::OUTER)
74
73
  @sequence_coords << coord
75
74
  bounds.expand(x: coord[:x], y: coord[:y])
76
75
  end
77
- #---------------
78
-
79
- plot_node(next_node, root_node, bounds, versus) if @nodes > 0 && !next_node_nd.nil?
76
+ plot_node(next_node, root_node, bounds, versus) if @nodes > 0 && !next_node.nil?
80
77
 
81
78
  draw_sequence(bitmap, "X") unless bitmap.nil?
82
79
  @polygons << {outer: @sequence_coords, inner: [], bounds: (bounds.to_h if @options[:bounds])}.compact if @sequence_coords.size >= 2
@@ -198,13 +195,13 @@ module Contrek
198
195
  @root_nodes.delete(node)
199
196
  @inner_plot.delete(node)
200
197
  last_node = @plot_sequence.last
201
- next_node_nd = node.my_next(last_node, versus, :inner)
202
- next_node = next_node_nd.node
198
+ next_node = node.my_next(last_node, versus, :inner)
203
199
  @plot_sequence << node
204
200
 
205
201
  plot = true
206
202
  if next_node.y == last_node.y
207
- plot = (node.tangs_sequence.send((versus == :a) ? :first : :last).node == next_node)
203
+ virtual_index = node.tangs_sequence.send((versus == :a) ? :first : :last)
204
+ plot = (node.get_tangent_node_by_virtual_index(virtual_index) == next_node)
208
205
  end
209
206
  if plot
210
207
  @sequence_coords << last_node.coords_entering_to(node, VERSUS_INVERTER[versus], Contrek::Finder::Node::INNER)
@@ -231,14 +228,14 @@ module Contrek
231
228
 
232
229
  node.outer_index = start_node.outer_index
233
230
  last_node = @plot_sequence.last
234
- next_node_nd = node.my_next(last_node, versus, :outer)
235
- next_node = next_node_nd.node
231
+ next_node = node.my_next(last_node, versus, :outer)
236
232
 
237
233
  @plot_sequence << node
238
234
 
239
235
  plot = true
240
236
  if next_node.y == last_node.y
241
- plot = (node.tangs_sequence.send((versus == :a) ? :last : :first).node == next_node)
237
+ virtual_index = node.tangs_sequence.send((versus == :a) ? :last : :first)
238
+ plot = (node.get_tangent_node_by_virtual_index(virtual_index) == next_node)
242
239
  end
243
240
 
244
241
  # coord
@@ -3,7 +3,6 @@ require "benchmark"
3
3
  module Contrek
4
4
  module Finder
5
5
  class PolygonFinder
6
- NodeDescriptor = Struct.new(:node, :a, :o)
7
6
  def initialize(bitmap, matcher, test_bitmap = nil, options = {})
8
7
  @options = {versus: :a}.merge(options)
9
8
  sanitize_options
@@ -1,3 +1,3 @@
1
1
  module Contrek
2
- VERSION = "1.1.3"
2
+ VERSION = "1.1.4"
3
3
  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.1.3
4
+ version: 1.1.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Emanuele Cesaroni
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-02-21 00:00:00.000000000 Z
11
+ date: 2026-02-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rspec
@@ -279,7 +279,7 @@ metadata:
279
279
  homepage_uri: https://github.com/runout77/contrek
280
280
  documentation_uri: https://github.com/runout77/contrek#readme
281
281
  changelog_uri: https://github.com/runout77/contrek/blob/main/CHANGELOG.md
282
- post_install_message:
282
+ post_install_message:
283
283
  rdoc_options: []
284
284
  require_paths:
285
285
  - lib
@@ -295,7 +295,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
295
295
  version: '0'
296
296
  requirements: []
297
297
  rubygems_version: 3.5.22
298
- signing_key:
298
+ signing_key:
299
299
  specification_version: 4
300
300
  summary: Fast PNG contour tracing and shape detection for Ruby
301
301
  test_files: []