laser-cutter 1.0.3 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. checksums.yaml +5 -5
  2. data/.envrc +2 -0
  3. data/.github/workflows/lint.yml +22 -0
  4. data/.github/workflows/rspec.yml +22 -0
  5. data/.gitignore +5 -4
  6. data/.plans/001.00-dry-cli-migration/plan.md +29 -0
  7. data/.relaxed_rubocop.yml +153 -0
  8. data/.rspec +1 -1
  9. data/.rubocop.yml +31 -0
  10. data/.rubocop_todo.yml +90 -0
  11. data/.ruby-version +1 -0
  12. data/CLAUDE.md +62 -0
  13. data/CONTRIBUTING.md +2 -4
  14. data/Gemfile +14 -0
  15. data/README.md +83 -111
  16. data/Rakefile +41 -0
  17. data/docs/badges/coverage_badge.svg +21 -0
  18. data/exe/laser-cutter +8 -0
  19. data/exe/lc +1 -0
  20. data/justfile +97 -0
  21. data/laser-cutter.gemspec +26 -14
  22. data/lib/laser/cutter/aggregator.rb +101 -0
  23. data/lib/{laser-cutter → laser/cutter}/box.rb +44 -52
  24. data/lib/laser/cutter/cli/command.rb +46 -0
  25. data/lib/laser/cutter/cli/completion.rb +21 -0
  26. data/lib/laser/cutter/cli/config_file.rb +36 -0
  27. data/lib/laser/cutter/cli/examples.rb +42 -0
  28. data/lib/laser/cutter/cli/generate.rb +90 -0
  29. data/lib/laser/cutter/cli/help.rb +20 -0
  30. data/lib/laser/cutter/cli/page_sizes.rb +19 -0
  31. data/lib/laser/cutter/cli/version.rb +15 -0
  32. data/lib/laser/cutter/cli.rb +49 -0
  33. data/lib/laser/cutter/configuration.rb +101 -0
  34. data/lib/{laser-cutter → laser/cutter}/geometry/dimensions.rb +2 -3
  35. data/lib/{laser-cutter/geometry/shape → laser/cutter/geometry}/line.rb +19 -18
  36. data/lib/{laser-cutter → laser/cutter}/geometry/point.rb +4 -2
  37. data/lib/{laser-cutter/geometry/shape → laser/cutter/geometry}/rect.rb +6 -7
  38. data/lib/{laser-cutter → laser/cutter}/geometry/shape.rb +7 -7
  39. data/lib/{laser-cutter → laser/cutter}/geometry/tuple.rb +40 -36
  40. data/lib/laser/cutter/launcher.rb +40 -0
  41. data/lib/laser/cutter/missing_option.rb +7 -0
  42. data/lib/laser/cutter/notching/base.rb +19 -0
  43. data/lib/{laser-cutter → laser/cutter}/notching/edge.rb +21 -20
  44. data/lib/laser/cutter/notching/infinite_iterator.rb +27 -0
  45. data/lib/{laser-cutter → laser/cutter}/notching/path_generator.rb +27 -60
  46. data/lib/laser/cutter/notching/shift.rb +17 -0
  47. data/lib/laser/cutter/page_manager.rb +50 -0
  48. data/lib/{laser-cutter → laser/cutter}/renderer/base.rb +3 -2
  49. data/lib/laser/cutter/renderer/box_renderer.rb +40 -0
  50. data/lib/laser/cutter/renderer/layout_renderer.rb +61 -0
  51. data/lib/laser/cutter/renderer/line_renderer.rb +21 -0
  52. data/lib/laser/cutter/renderer/meta_renderer.rb +83 -0
  53. data/lib/{laser-cutter → laser/cutter}/renderer/rect_renderer.rb +3 -1
  54. data/lib/laser/cutter/renderer/svg_renderer.rb +65 -0
  55. data/lib/laser/cutter/renderer.rb +24 -0
  56. data/lib/laser/cutter/units_converter.rb +15 -0
  57. data/lib/laser/cutter/version.rb +7 -0
  58. data/lib/laser/cutter/zero_value_not_allowed.rb +7 -0
  59. data/lib/laser/cutter.rb +29 -0
  60. data/lib/laser-cutter.rb +2 -14
  61. data/lib/laser_cutter.rb +3 -0
  62. data/spec/laser/cutter/aggregator_spec.rb +70 -0
  63. data/spec/laser/cutter/box_spec.rb +61 -0
  64. data/spec/laser/cutter/cli/config_file_spec.rb +31 -0
  65. data/spec/laser/cutter/cli_spec.rb +186 -0
  66. data/spec/laser/cutter/configuration_spec.rb +101 -0
  67. data/spec/{dimensions_spec.rb → laser/cutter/dimensions_spec.rb} +8 -6
  68. data/spec/laser/cutter/edge_spec.rb +64 -0
  69. data/spec/{line_spec.rb → laser/cutter/line_spec.rb} +26 -20
  70. data/spec/{page_manager_spec.rb → laser/cutter/page_manager_spec.rb} +27 -11
  71. data/spec/{path_generator_spec.rb → laser/cutter/path_generator_spec.rb} +18 -14
  72. data/spec/{point_spec.rb → laser/cutter/point_spec.rb} +27 -22
  73. data/spec/{rect_spec.rb → laser/cutter/rect_spec.rb} +9 -5
  74. data/spec/laser/cutter/renderer/layout_renderer_spec.rb +102 -0
  75. data/spec/laser/cutter/renderer/svg_renderer_spec.rb +51 -0
  76. data/spec/laser/cutter/renderer_spec.rb +19 -0
  77. data/spec/spec_helper.rb +34 -25
  78. data/spec/support/aruba.rb +10 -0
  79. metadata +206 -80
  80. data/.travis.yml +0 -11
  81. data/LICENSE +0 -22
  82. data/bin/laser-cutter +0 -19
  83. data/lib/laser-cutter/aggregator.rb +0 -57
  84. data/lib/laser-cutter/cli/opt_parser.rb +0 -131
  85. data/lib/laser-cutter/cli/serializer.rb +0 -51
  86. data/lib/laser-cutter/configuration.rb +0 -83
  87. data/lib/laser-cutter/geometry.rb +0 -11
  88. data/lib/laser-cutter/notching/base.rb +0 -13
  89. data/lib/laser-cutter/notching.rb +0 -10
  90. data/lib/laser-cutter/page_manager.rb +0 -43
  91. data/lib/laser-cutter/renderer/box_renderer.rb +0 -36
  92. data/lib/laser-cutter/renderer/layout_renderer.rb +0 -59
  93. data/lib/laser-cutter/renderer/line_renderer.rb +0 -17
  94. data/lib/laser-cutter/renderer/meta_renderer.rb +0 -77
  95. data/lib/laser-cutter/renderer.rb +0 -14
  96. data/lib/laser-cutter/version.rb +0 -5
  97. data/spec/aggregator_spec.rb +0 -65
  98. data/spec/box_spec.rb +0 -40
  99. data/spec/configuration_spec.rb +0 -81
  100. data/spec/edge_spec.rb +0 -43
  101. data/spec/renderer_spec.rb +0 -72
  102. /data/{doc → docs/images}/comparison.jpg +0 -0
@@ -1,10 +1,11 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Laser
2
4
  module Cutter
3
5
  module Geometry
4
6
  class Line < Shape
5
-
6
- def self.[] *array
7
- self.new *array
7
+ def self.[](*array)
8
+ new(*array)
8
9
  end
9
10
 
10
11
  attr_accessor :p1, :p2
@@ -19,31 +20,33 @@ module Laser
19
20
  self.p2 = point2.clone
20
21
  end
21
22
  self.position = p1.clone
22
- raise 'Both points are required for line definition' unless (p1 && p2)
23
+ raise 'Both points are required for line definition' unless p1 && p2
23
24
  end
24
25
 
25
26
  def overlaps?(another)
26
27
  xs, ys = sorted_coords(another)
27
- return false unless xs.all?{|x| x == xs[0] } || ys.all?{|y| y == ys[0] }
28
+ return false unless xs.all?{ |x| x == xs[0] } || ys.all?{ |y| y == ys[0] }
28
29
  return false if xs.first[1] < xs.last[0] || xs.first[0] > xs.last[1]
29
30
  return false if ys.first[1] < ys.last[0] || ys.first[0] > ys.last[1]
31
+
30
32
  true
31
33
  end
32
34
 
33
35
  def sorted_coords(another)
34
36
  xs = [[p1.x, p2.x].sort, [another.p1.x, another.p2.x].sort]
35
37
  ys = [[p1.y, p2.y].sort, [another.p1.y, another.p2.y].sort]
36
- return xs, ys
38
+ [xs, ys]
37
39
  end
38
40
 
39
41
  def xor(another)
40
42
  return nil unless overlaps?(another)
43
+
41
44
  xs, ys = sorted_coords(another)
42
45
  xs.flatten!.sort!
43
46
  ys.flatten!.sort!
44
47
 
45
- [ Line.new(Point[xs[0], ys[0]], Point[xs[1], ys[1]]),
46
- Line.new(Point[xs[2], ys[2]], Point[xs[3], ys[3]])]
48
+ [Line.new(Point[xs[0], ys[0]], Point[xs[1], ys[1]]),
49
+ Line.new(Point[xs[2], ys[2]], Point[xs[3], ys[3]])]
47
50
  end
48
51
 
49
52
  def relocate!
@@ -60,16 +63,16 @@ module Laser
60
63
  end
61
64
 
62
65
  def length
63
- Math.sqrt((p2.x - p1.x)**2 + (p2.y - p1.y)**2)
66
+ Math.sqrt(((p2.x - p1.x)**2) + ((p2.y - p1.y)**2))
64
67
  end
65
68
 
66
69
  def to_s
67
- "#{self.class.name.gsub(/.*::/,'').downcase} #{p1}=>#{p2}"
70
+ "#{self.class.name.gsub(/.*::/, '').downcase} #{p1}=>#{p2}"
68
71
  end
69
72
 
70
73
  def eql?(other)
71
74
  (other.p1.eql?(p1) && other.p2.eql?(p2)) ||
72
- (other.p2.eql?(p1) && other.p1.eql?(p2))
75
+ (other.p2.eql?(p1) && other.p1.eql?(p2))
73
76
  end
74
77
 
75
78
  def normalized
@@ -77,17 +80,17 @@ module Laser
77
80
  end
78
81
 
79
82
  def <=>(other)
80
- n1 = self.normalized
83
+ n1 = normalized
81
84
  n2 = other.normalized
82
85
  n1.p1.eql?(n2.p1) ? n1.p2 <=> n2.p2 : n1.p1 <=> n2.p1
83
86
  end
84
87
 
85
- def < (other)
86
- self.p1 == other.p1 ? self.p2 < other.p2 : self.p1 < other.p1
88
+ def <(other)
89
+ p1 == other.p1 ? p2 < other.p2 : p1 < other.p1
87
90
  end
88
91
 
89
- def > (other)
90
- self.p1 == other.p1 ? self.p2 > other.p2 : self.p1 > other.p1
92
+ def >(other)
93
+ p1 == other.p1 ? p2 > other.p2 : p1 > other.p1
91
94
  end
92
95
 
93
96
  def hash
@@ -97,9 +100,7 @@ module Laser
97
100
  def clone
98
101
  self.class.new(p1, p2)
99
102
  end
100
-
101
103
  end
102
-
103
104
  end
104
105
  end
105
106
  end
@@ -1,9 +1,11 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Laser
2
4
  module Cutter
3
5
  module Geometry
4
6
  class Point < Tuple
5
- def self.[] *array
6
- Point.new *array
7
+ def self.[](*array)
8
+ Point.new(*array)
7
9
  end
8
10
  end
9
11
  end
@@ -1,11 +1,12 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Laser
2
4
  module Cutter
3
5
  module Geometry
4
6
  class Rect < Line
5
-
6
7
  attr_accessor :sides, :vertices
7
8
 
8
- def self.[] p1, p2
9
+ def self.[](p1, p2)
9
10
  Rect.new(p1, p2)
10
11
  end
11
12
 
@@ -19,8 +20,8 @@ module Laser
19
20
  Rect.new(line.p1, line.p2)
20
21
  end
21
22
 
22
- def initialize(*args)
23
- super(*args)
23
+ def initialize(*)
24
+ super
24
25
  relocate!
25
26
  end
26
27
 
@@ -47,15 +48,13 @@ module Laser
47
48
  end
48
49
 
49
50
  def to_s
50
- "#{sprintf "%3d", w}(w)x#{sprintf "%3d", h}(h) @ #{position.to_s}"
51
+ "#{sprintf '%3d', w}(w)x#{sprintf '%3d', h}(h) @ #{position}"
51
52
  end
52
53
 
53
54
  def to_a
54
55
  [[p1.x, p1.y], [p2.x, p2.y]]
55
56
  end
56
57
  end
57
-
58
58
  end
59
-
60
59
  end
61
60
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Laser
2
4
  module Cutter
3
5
  module Geometry
@@ -8,27 +10,28 @@ module Laser
8
10
  @position ||= Point.new(0, 0)
9
11
  end
10
12
 
11
- def x= value
13
+ def x=(value)
12
14
  position.x = value
13
15
  end
16
+
14
17
  def x
15
18
  position.x
16
19
  end
17
20
 
18
- def y= value
21
+ def y=(value)
19
22
  position.y = value
20
23
  end
24
+
21
25
  def y
22
26
  position.y
23
27
  end
24
28
 
25
- def move_to new_point
29
+ def move_to(new_point)
26
30
  self.position = new_point
27
31
  relocate!
28
32
  self
29
33
  end
30
34
 
31
-
32
35
  # Implement in each shape to move to the new pointd
33
36
  def relocate!
34
37
  raise 'Abstract method'
@@ -37,6 +40,3 @@ module Laser
37
40
  end
38
41
  end
39
42
  end
40
-
41
- require_relative 'shape/line'
42
- require_relative 'shape/rect'
@@ -1,60 +1,63 @@
1
- require 'matrix'
1
+ # frozen_string_literal: true
2
+
2
3
  module Laser
3
4
  module Cutter
4
5
  module Geometry
5
6
  class Tuple
6
7
  attr_accessor :coords
8
+
7
9
  PRECISION = 0.000001
8
10
 
9
11
  def initialize(*args)
10
12
  x = args.first
11
- coordinates = if x.is_a?(String)
12
- parse_string(x)
13
- elsif x.is_a?(Hash)
14
- parse_hash(x)
15
- elsif x.is_a?(Array)
16
- x.clone
17
- elsif x.is_a?(Tuple) or x.is_a?(Vector)
18
- x.to_a
19
- else
20
- args.clone
21
- end
13
+ coordinates = case x
14
+ when String
15
+ parse_string(x)
16
+ when Hash
17
+ parse_hash(x)
18
+ when Array
19
+ x.clone
20
+ when Tuple, Vector
21
+ x.to_a
22
+ else
23
+ args.clone
24
+ end
22
25
  coordinates.map!(&:to_f)
23
26
  self.coords = Vector.[](*coordinates)
24
27
  end
25
28
 
26
- def + x, y = nil
29
+ def +(x, y = nil)
27
30
  shift = if x.is_a?(Vector)
28
31
  x
29
32
  elsif x.is_a?(Tuple)
30
33
  x.coords
31
34
  elsif y
32
- Vector.[](x,y)
35
+ Vector.[](x, y)
33
36
  end
34
- self.class.new(self.coords + shift)
37
+ self.class.new(coords + shift)
35
38
  end
36
39
 
37
-
38
40
  alias_method :plus, :+
39
41
 
40
42
  def to_a
41
- self.coords.to_a
43
+ coords.to_a
42
44
  end
43
45
 
44
46
  def to_s
45
- "{#{coords.to_a.map { |a| sprintf("%.5f", a) }.join(separator)}}"
47
+ "{#{coords.to_a.map { |a| sprintf('%.5f', a) }.join(separator)}}"
46
48
  end
47
49
 
48
50
  def valid?
49
- raise "Have nil value: #{self.inspect}" if coords.to_a.any? { |c| c.nil? }
51
+ raise "Have nil value: #{inspect}" if coords.to_a.any?(&:nil?)
52
+
50
53
  true
51
54
  end
52
55
 
53
- def x= value
56
+ def x=(value)
54
57
  self.coords = Vector.[](value, coords.[](1))
55
58
  end
56
59
 
57
- def y= value
60
+ def y=(value)
58
61
  self.coords = Vector.[](coords.[](0), value)
59
62
  end
60
63
 
@@ -70,11 +73,10 @@ module Laser
70
73
  ','
71
74
  end
72
75
 
73
- def [] value
76
+ def [](value)
74
77
  coords.[](value)
75
78
  end
76
79
 
77
-
78
80
  # Override in subclasses, eg:
79
81
  # def separator
80
82
  # ';'
@@ -90,8 +92,9 @@ module Laser
90
92
  # Identity, cloning and sorting/ordering
91
93
  def eql?(other)
92
94
  return false unless other.respond_to?(:coords)
95
+
93
96
  equal = true
94
- self.coords.each_with_index do |c, i|
97
+ coords.each_with_index do |c, i|
95
98
  if (c - other.coords.to_a[i])**2 > PRECISION
96
99
  equal = false
97
100
  break
@@ -99,18 +102,22 @@ module Laser
99
102
  end
100
103
  equal
101
104
  end
105
+
102
106
  def <=>(other)
103
- self.x == other.x ? self.y <=> other.y : self.x <=> other.x
107
+ x == other.x ? y <=> other.y : x <=> other.x
104
108
  end
105
- def < (other)
106
- self.x == other.x ? self.y < other.y : self.x < other.x
109
+
110
+ def <(other)
111
+ x == other.x ? y < other.y : x < other.x
107
112
  end
108
- def > (other)
109
- self.x == other.x ? self.y > other.y : self.x > other.x
113
+
114
+ def >(other)
115
+ x == other.x ? y > other.y : x > other.x
110
116
  end
117
+
111
118
  def clone
112
119
  clone = super
113
- clone.coords = self.coords.clone
120
+ clone.coords = coords.clone
114
121
  clone
115
122
  end
116
123
 
@@ -118,18 +125,15 @@ module Laser
118
125
 
119
126
  #
120
127
  # Convert from, eg "100,50" to [100.0, 50.0],
121
- def parse_string string
128
+ def parse_string(string)
122
129
  string.split(separator).map(&:to_f)
123
130
  end
124
131
 
125
132
  # Return array of coordinates
126
- def parse_hash hash
127
- hash_keys.map{ |k,v| hash[k] }
133
+ def parse_hash(hash)
134
+ hash_keys.map{ |k, _v| hash[k] }
128
135
  end
129
-
130
136
  end
131
-
132
-
133
137
  end
134
138
  end
135
139
  end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Laser
4
+ module Cutter
5
+ # Starts the CLI. The executable and the Aruba suite both build one, each
6
+ # with its own argv and streams, and it exits only through kernel.exit.
7
+ class Launcher
8
+ def initialize(argv, stdin = $stdin, stdout = $stdout, stderr = $stderr, kernel = Kernel)
9
+ @argv = argv
10
+ @stdin = stdin
11
+ @stdout = stdout
12
+ @stderr = stderr
13
+ @kernel = kernel
14
+ end
15
+
16
+ def execute!
17
+ @kernel.exit(run)
18
+ end
19
+
20
+ private
21
+
22
+ # dry-cli exits by itself after help or a usage error; its status is kept.
23
+ def run
24
+ Dry::CLI.new(CLI).call(arguments: @argv, out: @stdout, err: @stderr)
25
+ 0
26
+ rescue SystemExit => e
27
+ e.status
28
+ rescue StandardError => e
29
+ ui.error(e.message)
30
+ @stderr.puts(e.backtrace) if @argv.intersect?(%w[-v --verbose])
31
+ 1
32
+ end
33
+
34
+ # The same boxes the commands draw, on this run's own streams.
35
+ def ui
36
+ Dry::CLI::UI::Console.new(out: @stdout, err: @stderr, box_width: CLI.help_width)
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Laser
4
+ module Cutter
5
+ class MissingOption < Error; end
6
+ end
7
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Laser
4
+ module Cutter
5
+ module Notching
6
+ class Base
7
+ attr_accessor :edge
8
+
9
+ def initialize(edge)
10
+ @edge = edge
11
+ end
12
+
13
+ def notches
14
+ raise 'Abstract method'
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -1,29 +1,27 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Laser
2
4
  module Cutter
3
5
  module Notching
4
6
  MINIMUM_NOTCHES_PER_SIDE = 3
7
+ # Decimal places kept of length / notch width before counting notches.
8
+ RATIO_DIGITS = 6
5
9
  # This class represents a single edge of one side: both inside
6
10
  # and outside edge of the material. It's also responsible
7
11
  # for calculating the "perfect" notch width.
8
12
  class Edge
9
-
10
- attr_accessor :outside, :inside
11
- attr_accessor :notch_width
12
- attr_accessor :thickness, :kerf
13
- attr_accessor :center_out, :corners, :adjust_corners
14
- attr_accessor :notch_count, :v1, :v2
15
-
13
+ attr_accessor :outside, :inside, :notch_width, :thickness, :kerf, :center_out, :corners, :adjust_corners, :notch_count, :v1, :v2
16
14
 
17
15
  def initialize(outside, inside, options = {})
18
16
  self.outside = outside.clone
19
17
  self.inside = inside.clone
20
18
 
21
19
  # two vectors representing directions going from beginning of each inside line to the outside
22
- self.v1 = [inside.p1.x - outside.p1.x, inside.p1.y - outside.p1.y].map{|e| -(e / e.abs).to_f }
23
- self.v2 = [inside.p2.x - outside.p2.x, inside.p2.y - outside.p2.y].map{|e| -(e / e.abs).to_f }
20
+ self.v1 = [inside.p1.x - outside.p1.x, inside.p1.y - outside.p1.y].map{ |e| -(e / e.abs).to_f }
21
+ self.v2 = [inside.p2.x - outside.p2.x, inside.p2.y - outside.p2.y].map{ |e| -(e / e.abs).to_f }
24
22
 
25
- self.v1 = Vector.[](*self.v1)
26
- self.v2 = Vector.[](*self.v2)
23
+ self.v1 = Vector.[](*v1)
24
+ self.v2 = Vector.[](*v2)
27
25
 
28
26
  self.center_out = options[:center_out] || false
29
27
  self.thickness = options[:thickness]
@@ -37,13 +35,13 @@ module Laser
37
35
  end
38
36
 
39
37
  def adjust_for_kerf!
40
- if kerf?
41
- self.inside = move_line_for_kerf(inside)
42
- self.outside = move_line_for_kerf(outside)
43
- end
38
+ return unless kerf?
39
+
40
+ self.inside = move_line_for_kerf(inside)
41
+ self.outside = move_line_for_kerf(outside)
44
42
  end
45
43
 
46
- def move_line_for_kerf line
44
+ def move_line_for_kerf(line)
47
45
  k = kerf / 2.0
48
46
  p1 = line.p1.plus(v1 * k)
49
47
  p2 = line.p2.plus(v2 * k)
@@ -51,7 +49,7 @@ module Laser
51
49
  end
52
50
 
53
51
  def kerf?
54
- self.kerf > 0.0
52
+ kerf > 0.0
55
53
  end
56
54
 
57
55
  # face_setting determines if we want that face to have center notch
@@ -65,17 +63,20 @@ module Laser
65
63
  def first_notch_out?
66
64
  add_across_line?(center_out)
67
65
  end
66
+
68
67
  private
69
68
 
70
69
  def calculate_notch_width!
71
- length = kerf? ? self.inside.length - kerf : self.inside.length
72
- count = (length / notch_width).to_f.ceil + 1
70
+ length = kerf? ? inside.length - kerf : inside.length
71
+ # Rounded before #ceil: when the notch divides the side exactly,
72
+ # float noise in the coordinates would otherwise decide the count,
73
+ # and the two panels meeting at this joint would disagree.
74
+ count = (length / notch_width).to_f.round(RATIO_DIGITS).ceil + 1
73
75
  count = (count / 2 * 2) + 1 # make count always an odd number
74
76
  count = [MINIMUM_NOTCHES_PER_SIDE, count].max
75
77
  self.notch_width = 1.0 * length / count
76
78
  self.notch_count = count
77
79
  end
78
-
79
80
  end
80
81
  end
81
82
  end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Laser
4
+ module Cutter
5
+ module Notching
6
+ # Alternating iterator
7
+ InfiniteIterator = Struct.new(:array) do
8
+ attr_accessor :array, :next_index, :calls
9
+
10
+ def initialize(array)
11
+ self.array = array
12
+ self.calls = 0
13
+ self.next_index = 0
14
+ end
15
+
16
+ def next
17
+ item = array[next_index].clone
18
+ self.next_index += 1
19
+ self.next_index %= array.size
20
+ self.calls += 1
21
+ item = yield item, calls if block_given?
22
+ item
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end