contrek 1.2.2 → 1.2.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: 0d4e8a9a3c94ae345edb0ecbb6087020141ad20cd2661e2b58578323a721f66e
4
- data.tar.gz: ec99c9629d41d589e90ff39d8305c7e0355743b3f51caeb1e0a9da4702007fde
3
+ metadata.gz: 0df584b1fe92f70cb27f479a6d7705545f662b8758a4843bf966fe92d90c4c9d
4
+ data.tar.gz: 2dfe6f8b0cc96298858f5055158ba439644d24a9921166c572e91cd5df17780b
5
5
  SHA512:
6
- metadata.gz: 9180029576fc846f3cbc8adcd68e5f68374b49fb734db8352e9ced637a447cfad93beb37cb5206084b411f4cb5cc26de9a1ac075b09f8ce1b39bb6e33fadd018
7
- data.tar.gz: 163a00611440eb83d538b4dcd3f3c36745350c1d34e657fcdae069dd8c8020f36eec005264bd581e2024fca772e7f53dc0f856d35eca4717b741b0bcbaa91e5a
6
+ metadata.gz: d97742b1bc4f3688a8d083d25873da8db709b69c42c8068d02cfe6fe3569acac159f3d1a37c31128a7809b0e9851ba5e9da15c801f18c514189e53f4bc39440a
7
+ data.tar.gz: ee34393dc9713f62de0560b5d6538e7e9ffd6ff38358429f14f7ab7efb3cde201f2461d4ec209ed906a0a0e1e6aa0eb013040dddf290856400f632131e4c3622
data/CHANGELOG.md CHANGED
@@ -91,4 +91,11 @@ All notable changes to this project will be documented in this file.
91
91
 
92
92
  ## [1.2.2] - 2026-05-20
93
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.
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.2)
4
+ contrek (1.2.4)
5
5
  chunky_png (~> 1.4)
6
6
  concurrent-ruby (~> 1.3.5)
7
7
  rice (= 4.5.0)
data/contrek.gemspec CHANGED
@@ -11,10 +11,10 @@ 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 do |f|
15
- f.match(%r{^(docs|pkg|spec)/}) ||
16
- f.include?("PolygonFinder/images/") ||
17
- f.include?("PolygonFinder/examples/")
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
18
  end
19
19
  end
20
20
  s.metadata = {
@@ -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 {
@@ -86,9 +86,8 @@ void Cursor::traverse_outer(Part* act_part,
86
86
  auto& q_set = new_position->end_point()->queues();
87
87
  auto it = std::find_if(q_set.begin(), q_set.end(), [&](Queueable<Point>* q) {
88
88
  Part* p = static_cast<Part*>(q);
89
- return p->versus() == -versus && p->polyline()->tile != act_part->polyline()->tile;
89
+ return (p->mirror || act_part->mirror || p->versus() == -versus) && p->polyline()->tile != act_part->polyline()->tile;
90
90
  });
91
-
92
91
  Part* part = nullptr;
93
92
  if (it != q_set.end()) {
94
93
  part = static_cast<Part*>(*it);
@@ -97,24 +96,26 @@ void Cursor::traverse_outer(Part* act_part,
97
96
  const auto n = all_parts.size();
98
97
  Part *last_last_part = n >= 2 ? all_parts[n - 2] : nullptr;
99
98
  if (last_last_part != part) {
99
+ bool all_seam = false;
100
100
  if (n >= 2) {
101
- bool all_seam = true;
101
+ all_seam = true;
102
102
  for (std::size_t i = all_parts.size() - 2; i < all_parts.size(); ++i) {
103
103
  if (all_parts[i]->type != Part::SEAM) {
104
104
  all_seam = false;
105
105
  break;
106
106
  }
107
107
  }
108
- if (all_seam) break;
109
108
  }
110
- if (shapes_sequence_lookup.insert(part->polyline()->shape).second) {
111
- shapes_sequence.push_back(part->polyline()->shape);
109
+ if (!all_seam) {
110
+ if (shapes_sequence_lookup.insert(part->polyline()->shape).second) {
111
+ shapes_sequence.push_back(part->polyline()->shape);
112
+ }
113
+ part->next_position(new_position);
114
+ part->dead_end = true;
115
+ act_part = part;
116
+ jumped_to_new_part = true;
117
+ break;
112
118
  }
113
- part->next_position(new_position);
114
- part->dead_end = true;
115
- act_part = part;
116
- jumped_to_new_part = true;
117
- break;
118
119
  }
119
120
  }
120
121
  if (!jumped_to_new_part) {
@@ -65,7 +65,13 @@ void Part::orient()
65
65
  { if (this->size <= 1 || (this->size == 2 && this->inverts)) {
66
66
  this->versus_ = 0;
67
67
  } else {
68
- this->versus_ = (this->tail->payload->y - this->head->payload->y) > 0 ? 1 : -1;
68
+ int diff = this->tail->payload->y - this->head->payload->y;
69
+ if (diff == 0) {
70
+ this->mirror = true;
71
+ this->versus_ = 0;
72
+ } else {
73
+ this->versus_ = diff > 0 ? 1 : -1;
74
+ }
69
75
  }
70
76
  }
71
77
 
@@ -78,6 +84,7 @@ std::string Part::inspect() {
78
84
  std::stringstream ss;
79
85
  ss << "part " << part_index
80
86
  << " (versus=" << this->versus_
87
+ << " mirror=" << this->mirror
81
88
  << " inv=" << this->inverts
82
89
  << " trm=" << this->trasmuted
83
90
  << " touched=" << this->touched_
@@ -33,6 +33,7 @@ class Part : public Queueable<Point> {
33
33
  bool inverts = false;
34
34
  bool trasmuted = false;
35
35
  bool dead_end = false;
36
+ bool mirror = false;
36
37
  Part* next = nullptr;
37
38
  Part* prev = nullptr;
38
39
  Part* circular_next = nullptr;
@@ -87,10 +87,16 @@ void Partitionable::trasmute_parts()
87
87
  }
88
88
  return false;
89
89
  });
90
- if (count == inside->size && count < inside_compare->size) {
91
- inside->type = Part::EXCLUSIVE;
92
- inside->trasmuted = true;
93
- break;
90
+ if (count == inside->size) {
91
+ if (count < inside_compare->size) {
92
+ inside->type = Part::EXCLUSIVE;
93
+ inside->trasmuted = true;
94
+ break;
95
+ } else if ( count == inside_compare->size &&
96
+ inside->next == nullptr &&
97
+ inside_compare->prev == nullptr) {
98
+ inside->mirror = true;
99
+ }
94
100
  }
95
101
  }
96
102
  }
@@ -1,6 +1,8 @@
1
1
  module Contrek
2
2
  module Cpp
3
3
  class CPPResult
4
+ include Shared::Result
5
+
4
6
  def polygons=(list)
5
7
  @polygons_storage = list
6
8
  end
@@ -115,7 +115,7 @@ module Contrek
115
115
  new_position.end_point.tracked_outer = true
116
116
  versus = act_part.versus
117
117
  part = new_position.end_point.queues.find do |p|
118
- p.versus == -versus && p.polyline.tile != act_part.polyline.tile
118
+ (p.mirror || act_part.mirror || p.versus == -versus) && p.polyline.tile != act_part.polyline.tile
119
119
  end
120
120
  if part
121
121
  if all_parts[-2] != part
@@ -8,7 +8,7 @@ module Contrek
8
8
  ADDED = 2
9
9
 
10
10
  attr_reader :polyline, :touched
11
- attr_accessor :next, :circular_next, :prev, :type, :dead_end, :inverts, :trasmuted, :versus
11
+ attr_accessor :next, :circular_next, :prev, :type, :dead_end, :inverts, :trasmuted, :versus, :mirror
12
12
  def initialize(type, polyline)
13
13
  @type = type
14
14
  @polyline = polyline
@@ -20,6 +20,7 @@ module Contrek
20
20
  @inverts = false
21
21
  @trasmuted = false
22
22
  @versus = 0
23
+ @mirror = false
23
24
  end
24
25
 
25
26
  def is?(type)
@@ -56,7 +57,7 @@ module Contrek
56
57
  end
57
58
 
58
59
  def inspect
59
- "part #{polyline.parts.index(self)} (versus=#{@versus} inv=#{@inverts} trm=#{@trasmuted} touched=#{@touched} dead_end =#{@dead_end}, #{size}x) of #{polyline.info} (#{name}) (#{to_a.map { |e| "[#{e[:x]},#{e[:y]}]" }.join})"
60
+ "part #{polyline.parts.index(self)} (mir=#{@mirror} versus=#{@versus} inv=#{@inverts} trm=#{@trasmuted} touched=#{@touched} dead_end =#{@dead_end}, #{size}x) of #{polyline.named} (#{name}) (#{to_a.map { |e| "[#{e[:x]},#{e[:y]}]" }.join})"
60
61
  end
61
62
 
62
63
  def innerable?
@@ -67,7 +68,13 @@ module Contrek
67
68
  @versus = if size <= 1 || (size == 2 && @inverts)
68
69
  0
69
70
  else
70
- (tail.payload[:y] - head.payload[:y]).positive? ? 1 : -1
71
+ diff = tail.payload[:y] - head.payload[:y]
72
+ if diff == 0
73
+ @mirror = true
74
+ 0
75
+ else
76
+ diff.positive? ? 1 : -1
77
+ end
71
78
  end
72
79
  end
73
80
 
@@ -73,6 +73,10 @@ module Contrek
73
73
  inside.trasmuted = true
74
74
  break
75
75
  end
76
+ if count == inside.size && count == inside_compare.size &&
77
+ inside.next.nil? && inside_compare.prev.nil?
78
+ inside.mirror = true
79
+ end
76
80
  end
77
81
  end
78
82
  end
@@ -1,6 +1,8 @@
1
1
  module Contrek
2
2
  module Finder
3
3
  Result = Struct.new(:polygons, :metadata) do
4
+ include Shared::Result
5
+
4
6
  def points
5
7
  polygons
6
8
  end
@@ -0,0 +1,23 @@
1
+ module Contrek
2
+ module Shared
3
+ module Result
4
+ def to_svg
5
+ width = metadata[:width]
6
+ height = metadata[:height]
7
+ lines = []
8
+ lines << %(<svg xmlns="http://www.w3.org/2000/svg" width="#{width}" height="#{height}">)
9
+ points.each do |poly|
10
+ pts = poly[:outer].map { |p| "#{p[:x]},#{p[:y]}" }.join(" ")
11
+ lines << %(<polygon points="#{pts}" fill="none" stroke="red" stroke-width="1"/>)
12
+ poly[:inner].each do |sequence|
13
+ next if sequence.empty?
14
+ pts = sequence.map { |p| "#{p[:x]},#{p[:y]}" }.join(" ")
15
+ lines << %(<polygon points="#{pts}" fill="none" stroke="green" stroke-width="1"/>)
16
+ end
17
+ end
18
+ lines << "</svg>"
19
+ lines.join("\n")
20
+ end
21
+ end
22
+ end
23
+ end
@@ -1,3 +1,3 @@
1
1
  module Contrek
2
- VERSION = "1.2.2"
2
+ VERSION = "1.2.4"
3
3
  end
data/lib/contrek.rb CHANGED
@@ -6,8 +6,9 @@ require "contrek/bitmaps/png_bitmap"
6
6
  require "contrek/bitmaps/raw_bitmap"
7
7
  require "contrek/bitmaps/rgb_color"
8
8
  require "contrek/bitmaps/rgb_cpp_color"
9
- require "contrek/finder/bounds"
10
9
  require "contrek/bitmaps/sample_generator"
10
+ require "contrek/shared/result"
11
+ require "contrek/finder/bounds"
11
12
  require "contrek/finder/list"
12
13
  require "contrek/finder/list_entry"
13
14
  require "contrek/finder/listable"
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.2.2
4
+ version: 1.2.4
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-05-20 00:00:00.000000000 Z
11
+ date: 2026-05-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rspec
@@ -291,6 +291,7 @@ files:
291
291
  - lib/contrek/reducers/reducer.rb
292
292
  - lib/contrek/reducers/uniq_reducer.rb
293
293
  - lib/contrek/reducers/visvalingam_reducer.rb
294
+ - lib/contrek/shared/result.rb
294
295
  - lib/contrek/version.rb
295
296
  homepage: https://github.com/runout77/contrek
296
297
  licenses: