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
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Laser
4
+ module Cutter
5
+ RSpec.describe Aggregator do
6
+ def line(x1, y1, x2, y2)
7
+ Geometry::Line.new(Geometry::Point[x1, y1], Geometry::Point[x2, y2])
8
+ end
9
+
10
+ def outline(*lines)
11
+ described_class.new(lines).lines.map(&:to_s)
12
+ end
13
+
14
+ let(:short) { line(0, 0, 5, 0) }
15
+ let(:long) { line(2, 0, 10, 0) }
16
+ let(:vertical) { line(2, 0, 2, 12) }
17
+
18
+ it 'keeps lines that share nothing, sorted' do
19
+ expect(outline(vertical, short)).to eq([short, vertical].map(&:to_s))
20
+ end
21
+
22
+ it 'cancels two identical lines, whichever way each one points' do
23
+ expect(outline(short, line(5, 0, 0, 0), vertical)).to eq([vertical.to_s])
24
+ end
25
+
26
+ it 'keeps a line drawn three times' do
27
+ expect(outline(short, short, short)).to eq([short.to_s])
28
+ end
29
+
30
+ it 'keeps only the parts two overlapping lines do not share' do
31
+ expect(outline(short, long, vertical)).to eq([line(0, 0, 2, 0), line(2, 0, 2, 12), line(5, 0, 10, 0)].map(&:to_s))
32
+ end
33
+
34
+ it 'joins lines that meet end to end' do
35
+ expect(outline(line(0, 0, 2, 0), line(2, 0, 5, 0))).to eq([short.to_s])
36
+ end
37
+
38
+ it 'drops a line one other line covers whole, leaving the rest of it' do
39
+ expect(outline(line(0, 0, 10, 0), line(2, 0, 5, 0))).to eq([line(0, 0, 2, 0), line(5, 0, 10, 0)].map(&:to_s))
40
+ end
41
+
42
+ it 'treats coordinates within the tolerance as the same' do
43
+ expect(outline(line(0, 0, 5, 0), line(5, 0.0004, 10, 0.0004))).to eq([line(0, 0, 10, 0).to_s])
44
+ end
45
+
46
+ it 'keeps horizontal and vertical lines apart' do
47
+ expect(outline(line(0, 0, 5, 0), line(0, 0, 0, 5)).size).to eq(2)
48
+ end
49
+
50
+ it 'passes a line on neither axis through' do
51
+ skewed = line(0, 0, 3, 4)
52
+ expect(outline(skewed, skewed)).to eq([skewed, skewed].map(&:to_s))
53
+ end
54
+
55
+ # A relative bound, since a shared CI runner can be several times slower
56
+ # than a laptop: four times the lines take about four and a half times as
57
+ # long when swept, and sixteen times as long when compared pairwise.
58
+ it 'scales with n log n rather than the square of the line count' do
59
+ seconds_for = lambda do |count|
60
+ lines = Array.new(count) { |i| line(i, 0, i + 1.5, 0) }
61
+ started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
62
+ described_class.new(lines)
63
+ Process.clock_gettime(Process::CLOCK_MONOTONIC) - started
64
+ end
65
+
66
+ expect(seconds_for.call(16_000) / seconds_for.call(4_000)).to be < 10
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ module Laser
6
+ module Cutter
7
+ RSpec.describe Box do
8
+ let(:config) { { 'width' => 50, 'height' => 60, 'depth' => 70, 'margin' => 5, 'padding' => 3, 'units' => 'mm' } }
9
+ let(:box1) { Box.new(config.merge('thickness' => 6, 'notch' => 10)) }
10
+ let(:box2) { Box.new(config.merge('thickness' => 6, )) }
11
+
12
+ describe '#initialize' do
13
+ it 'initializes with passed in parameters' do
14
+ expect(box1.w).to eq(50.0)
15
+ expect(box1.thickness).to eq(6.0)
16
+ expect(box1.notch_width).to eq(10.0)
17
+ end
18
+
19
+ it 'initializes with default notch' do
20
+ expect(box2.notch_width).to eq(70.0 / 5.0)
21
+ end
22
+ end
23
+
24
+ describe 'notch counts across joints' do
25
+ # Sides meeting at a joint share a nominal length, and must get the
26
+ # same number of notches or the two panels do not fit together.
27
+ [0.0, 0.0024].each do |kerf|
28
+ it "agree on a 4x3x2 box with a 0.5 notch and a #{kerf} kerf" do
29
+ edges = []
30
+ allow(Notching::Edge).to receive(:new).and_wrap_original do |original, *args|
31
+ original.call(*args).tap { |edge| edges << edge }
32
+ end
33
+ Box.new(Configuration.new(width: 4, height: 3, depth: 2, thickness: 0.125,
34
+ notch: 0.5, kerf: kerf, file: 'box.pdf')).generate_notches
35
+
36
+ counts = edges.group_by { |edge| edge.inside.length.round(1) }
37
+ .transform_values { |group| group.map(&:notch_count).uniq }
38
+ expect(counts).to eq(4.0 => [9], 3.0 => [7], 2.0 => [5])
39
+ end
40
+ end
41
+ end
42
+
43
+ describe '#notches' do
44
+ before do
45
+ box1.generate_notches
46
+ box2.generate_notches
47
+ end
48
+
49
+ it 'generates notches' do
50
+ expect(box1.notches).not_to be_nil
51
+ expect(box1.notches.size).to eql(344)
52
+ end
53
+
54
+ it 'properlies calculate enclosure' do
55
+ expect(box1.enclosure.to_a.flatten.map(&:round)).to eql([0, 0, 232, 317])
56
+ expect(box2.enclosure.to_a.flatten.map(&:round)).to eql([0, 0, 232, 317])
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'stringio'
4
+ require 'tmpdir'
5
+
6
+ RSpec.describe Laser::Cutter::CLI::ConfigFile do
7
+ describe '#read' do
8
+ it 'reads STDIN for -' do
9
+ stdin = $stdin
10
+ $stdin = StringIO.new('{"width": 4}')
11
+ expect(described_class.new('-').read).to eq('width' => 4)
12
+ ensure
13
+ $stdin = stdin
14
+ end
15
+
16
+ it 'raises on a file that is not JSON' do
17
+ Dir.mktmpdir do |dir|
18
+ path = File.join(dir, 'broken.json')
19
+ File.write(path, 'not json')
20
+ expect { described_class.new(path).read }.to raise_error(Laser::Cutter::Error, /cannot read/)
21
+ end
22
+ end
23
+ end
24
+
25
+ describe '#write' do
26
+ it 'raises when the file cannot be written' do
27
+ expect { described_class.new('/nowhere/at/all.json').write({}, StringIO.new) }
28
+ .to raise_error(Laser::Cutter::Error, /cannot write/)
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,186 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe 'laser-cutter', type: :aruba do
4
+ let(:box) { '-b 4x3x2/0.125/0.5' }
5
+
6
+ describe 'without arguments' do
7
+ before { run_command_and_stop('laser-cutter') }
8
+
9
+ it 'lists the commands and exits 0' do
10
+ expect(last_command_started).to have_output(/generate.*page-sizes.*examples.*help/m)
11
+ end
12
+
13
+ it 'wraps help at 90 columns or fewer' do
14
+ expect(last_command_started.output.lines.map { |line| line.chomp.length }.max).to be <= 90
15
+ end
16
+ end
17
+
18
+ describe 'the width of help' do
19
+ { 200 => 90, 80 => 74, 0 => 74, nil => 74 }.each do |columns, width|
20
+ it "is #{width} on a terminal #{columns.inspect} columns wide" do
21
+ expect(Laser::Cutter::CLI.help_width(columns)).to eq(width)
22
+ end
23
+ end
24
+ end
25
+
26
+ describe 'help' do
27
+ it 'prints the top-level help' do
28
+ run_command_and_stop('laser-cutter help')
29
+ expect(last_command_started).to have_output(/COMMANDS/)
30
+ end
31
+
32
+ it 'describes the completion command' do
33
+ run_command_and_stop('laser-cutter help')
34
+ expect(last_command_started).to have_output(/completion\s+Generates auto-complete for BASH or ZSH/)
35
+ end
36
+
37
+ # In-process, dry-cli names the program after the process: rspec.
38
+ it 'shows the shell the completion command takes' do
39
+ run_command_and_stop('laser-cutter help completion')
40
+ expect(last_command_started).to have_output(/completion SHELL \[OPTIONS\]/)
41
+ end
42
+
43
+ it 'explains one command' do
44
+ run_command_and_stop('laser-cutter help generate')
45
+ expect(last_command_started).to have_output(/-H, --height=VALUE/)
46
+ end
47
+ end
48
+
49
+ describe 'version' do
50
+ %w[version -V --version].each do |argument|
51
+ it "answers to #{argument}" do
52
+ run_command_and_stop("laser-cutter #{argument}")
53
+ expect(last_command_started).to have_output(Laser::Cutter::VERSION)
54
+ end
55
+ end
56
+ end
57
+
58
+ describe 'examples' do
59
+ it 'shows how to generate a box' do
60
+ run_command_and_stop('laser-cutter examples')
61
+ expect(last_command_started).to have_output(/laser-cutter generate -b 3x2x2/)
62
+ end
63
+ end
64
+
65
+ describe 'page-sizes' do
66
+ it 'lists the sizes in inches' do
67
+ run_command_and_stop('laser-cutter page-sizes')
68
+ expect(last_command_started).to have_output(/B10:\s+1\.2\s+x\s+1\.7/)
69
+ end
70
+
71
+ it 'lists the sizes in millimeters' do
72
+ run_command_and_stop('laser-cutter page-sizes -u mm')
73
+ expect(last_command_started).to have_output(/B10:\s+31\.0\s+x\s+44\.0/)
74
+ end
75
+ end
76
+
77
+ # dry-cli-autocomplete writes to $stdout rather than the stream Aruba hands
78
+ # the command, so this reads the process's stdout instead.
79
+ describe 'completion' do
80
+ it 'prints a zsh completion script' do
81
+ expect { run_command_and_stop('laser-cutter completion zsh') }.to output(/laser-cutter/).to_stdout
82
+ end
83
+ end
84
+
85
+ describe 'generate' do
86
+ it 'writes a PDF by default' do
87
+ run_command_and_stop("laser-cutter generate #{box} -o box.pdf")
88
+ expect(read('box.pdf').first).to start_with('%PDF')
89
+ end
90
+
91
+ it 'reports one unit of progress per line drawn' do
92
+ run_command_and_stop("laser-cutter generate #{box} -o box.pdf")
93
+ expect(last_command_started).to have_output(%r{Drawing box\.pdf 328/328})
94
+ end
95
+
96
+ it 'draws the info box as wide as help' do
97
+ run_command_and_stop("laser-cutter generate #{box} -o box.pdf")
98
+ info = last_command_started.stdout.split('┌─ Success').first
99
+ expect(info.lines.map { |line| line.chomp.length }.max).to eq(Laser::Cutter::CLI.help_width)
100
+ end
101
+
102
+ it 'keeps the path of the file on one line' do
103
+ run_command_and_stop("laser-cutter generate #{box} -o box.pdf")
104
+ expect(last_command_started.stdout).to include(expand_path('box.pdf'))
105
+ end
106
+
107
+ it 'opens with an info box that names the gem and the dimensions' do
108
+ run_command_and_stop("laser-cutter generate #{box} -o box.pdf")
109
+ expect(last_command_started.stdout).to match(/─ Info ─.*Laser-Cutter \(ruby gem\) Version #{Laser::Cutter::VERSION}, ©/m)
110
+ .and match(/Width:\s+4\.0 in.*Height:\s+3\.0 in.*Depth:\s+2\.0 in.*Notch:\s+0\.5 in.*Format: PDF/m)
111
+ end
112
+
113
+ it 'closes with a success box that gives the path of the file' do
114
+ run_command_and_stop("laser-cutter generate #{box} -f svg -o box.svg")
115
+ expect(last_command_started.stdout).to match(/─ Info ─.*─ Success ─.*Generated SVG file:.*box\.svg/m)
116
+ end
117
+
118
+ it 'writes an SVG, whatever the case of the format' do
119
+ run_command_and_stop("laser-cutter generate #{box} -f SVG -o box.svg")
120
+ expect(read('box.svg').join).to include('<svg', '<line').and include('viewBox="0 0 9.2012 11.5512"')
121
+ end
122
+
123
+ it 'draws the box without kerf as well with --inside-box' do
124
+ run_command_and_stop("laser-cutter generate #{box} --inside-box -o box.pdf")
125
+ expect(last_command_started).to have_output(%r{Drawing box\.pdf 656/656})
126
+ end
127
+
128
+ it 'takes each dimension as its own option, in millimeters' do
129
+ run_command_and_stop('laser-cutter generate -u mm -w 70 -H 20 -d 50 -t 4.3 -n 5 -f svg -o box.svg')
130
+ expect(read('box.svg').first).to match(/width="[\d.]+mm"/)
131
+ end
132
+
133
+ it 'prints the configuration with --verbose' do
134
+ run_command_and_stop("laser-cutter generate #{box} -o box.pdf -v")
135
+ expect(last_command_started).to have_output(/"thickness": 0\.125/)
136
+ end
137
+
138
+ it 'saves the configuration, and reads it back' do
139
+ run_command_and_stop("laser-cutter generate #{box} -o box.pdf -W settings.json")
140
+ expect(JSON.parse(read('settings.json').join)).to include('width' => 4.0, 'notch' => 0.5)
141
+
142
+ run_command_and_stop('laser-cutter generate -R settings.json -f svg -o again.svg')
143
+ expect(read('again.svg').join).to include('viewBox="0 0 9.2012 11.5512"')
144
+ end
145
+
146
+ it 'prints the configuration to STDOUT when saving to -' do
147
+ run_command_and_stop("laser-cutter generate #{box} -o box.pdf -W -")
148
+ expect(last_command_started).to have_output(/"depth": 2\.0/)
149
+ end
150
+
151
+ it 'opens the file with --open' do
152
+ allow_any_instance_of(Laser::Cutter::CLI::Generate).to receive(:system)
153
+ run_command_and_stop("laser-cutter generate #{box} -o box.pdf --open")
154
+ expect(last_command_started).to have_exit_status(0)
155
+ end
156
+
157
+ context 'when it cannot run' do
158
+ it 'fails without a file' do
159
+ run_command_and_stop("laser-cutter generate #{box}", fail_on_error: false)
160
+ expect(last_command_started).to have_exit_status(1)
161
+ expect(last_command_started.stderr).to match(/─ Error ─.*file is required/m)
162
+ expect(last_command_started.stdout).to be_empty
163
+ end
164
+
165
+ it 'draws the error box as wide as help' do
166
+ run_command_and_stop("laser-cutter generate #{box}", fail_on_error: false)
167
+ expect(last_command_started.stderr.lines.map { |line| line.chomp.length }.max).to eq(Laser::Cutter::CLI.help_width)
168
+ end
169
+
170
+ it 'fails on an unknown format' do
171
+ run_command_and_stop("laser-cutter generate #{box} -f dxf -o box.dxf", fail_on_error: false)
172
+ expect(last_command_started).to have_output(/unknown format "dxf", expected one of: pdf, svg/)
173
+ end
174
+
175
+ it 'fails on a missing configuration file' do
176
+ run_command_and_stop('laser-cutter generate -R nothing.json -o box.pdf', fail_on_error: false)
177
+ expect(last_command_started).to have_output(/cannot read the configuration from nothing\.json/)
178
+ end
179
+
180
+ it 'prints the backtrace with --verbose' do
181
+ run_command_and_stop("laser-cutter generate #{box} -f dxf -o box.dxf -v", fail_on_error: false)
182
+ expect(last_command_started).to have_output(/renderer\.rb/)
183
+ end
184
+ end
185
+ end
186
+ end
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ module Laser
6
+ module Cutter
7
+ RSpec.describe Configuration do
8
+ subject(:config) { described_class.new(opts) }
9
+
10
+ context 'when a box is 2x3x2/0.125/0.5' do
11
+ let(:opts) { { 'box' => '2x3x2/0.125/0.5' } }
12
+
13
+ its(:width) { is_expected.to eql(2.0) }
14
+ its(:height) { is_expected.to eql(3.0) }
15
+ its(:depth) { is_expected.to eql(2.0) }
16
+ its(:thickness) { is_expected.to eql(0.125) }
17
+ its(:page_layout) { is_expected.to eql('portrait') }
18
+ its(:notch) { is_expected.to eql(0.5) }
19
+ end
20
+
21
+ describe '#validate' do
22
+ context 'when required options are missing' do
23
+ let(:opts) { { 'height' => '23' } }
24
+
25
+ its(:height) { is_expected.to eql(23.0) }
26
+
27
+ it 'raises MissingOption' do
28
+ expect { config.validate! }.to raise_error(Laser::Cutter::MissingOption)
29
+ end
30
+ end
31
+
32
+ context 'when a required option is zero' do
33
+ let(:opts) { { 'box' => '2.0x0.0x2/0.125/0.5', 'file' => '/tmp/a' } }
34
+
35
+ its(:height) { is_expected.to eql(0.0) }
36
+
37
+ it 'raises ZeroValueNotAllowed' do
38
+ expect { config.validate! }.to raise_error(Laser::Cutter::ZeroValueNotAllowed)
39
+ end
40
+ end
41
+ end
42
+
43
+ context 'when notch is omitted' do
44
+ let(:opts) { { 'box' => '2.0x1.0x2/0.125', 'file' => '/tmp/a' } }
45
+
46
+ before { config.validate! }
47
+
48
+ its(:thickness) { is_expected.to eql(0.125) }
49
+ its(:notch) { is_expected.to eql(0.375) }
50
+ end
51
+
52
+ context 'when invalid units are provided' do
53
+ let(:opts) { { 'box' => '2x3x2/0.125/0.5', 'units' => 'xx' } }
54
+
55
+ its(:units) { is_expected.to eql(:in) }
56
+ end
57
+
58
+ context 'when converting between units' do
59
+ context 'from inches to mm' do
60
+ let(:opts) { { 'box' => '2.0x3x2/0.125/0.5', 'padding' => '4.2', 'units' => 'in' } }
61
+
62
+ its(:width) { is_expected.to eql(2.0) }
63
+
64
+ context 'after change_units(:in)' do
65
+ subject(:config) { described_class.new(opts).tap { |c| c.change_units(:in) } }
66
+
67
+ its(:width) { is_expected.to eql(2.0) }
68
+ end
69
+
70
+ context 'after change_units(:mm)' do
71
+ subject(:config) { described_class.new(opts).tap { |c| c.change_units(:mm) } }
72
+
73
+ its(:units) { is_expected.to eql(:mm) }
74
+ its(:width) { is_expected.to eql(50.8) }
75
+ its(:padding) { is_expected.to eql(106.68) }
76
+ end
77
+ end
78
+
79
+ context 'from mm to inches' do
80
+ let(:opts) { { 'box' => '20.0x30.0x40.0/5/5', 'margin' => '10.0', 'units' => 'mm' } }
81
+
82
+ its(:width) { is_expected.to eql(20.0) }
83
+
84
+ context 'after change_units(:mm)' do
85
+ subject(:config) { described_class.new(opts).tap { |c| c.change_units(:mm) } }
86
+
87
+ its(:width) { is_expected.to eql(20.0) }
88
+ end
89
+
90
+ context 'after change_units(:in)' do
91
+ subject(:config) { described_class.new(opts).tap { |c| c.change_units(:in) } }
92
+
93
+ its(:width) { is_expected.to be_within(0.00001).of(0.787401575) }
94
+ its(:margin) { is_expected.to be_within(0.00001).of(0.393700787) }
95
+ its(:units) { is_expected.to eql(:in) }
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end
@@ -1,22 +1,24 @@
1
- require_relative 'spec_helper'
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
2
4
  module Laser
3
5
  module Cutter
4
6
  module Geometry
5
-
6
- describe Dimensions do
7
+ RSpec.describe Dimensions do
7
8
  let(:dim1) { Dimensions.new(20, 10, 50) }
8
9
 
9
10
  context 'creation' do
10
11
  context 'from string' do
11
12
  let(:dim2) { Dimensions.new "20x10x50" }
12
- it 'should instantiate correctly from a string' do
13
- expect(dim2).to_not be_nil
13
+
14
+ it 'instantiates correctly from a string' do
15
+ expect(dim2).not_to be_nil
14
16
  expect(dim2).to eql(dim1)
15
17
  end
16
18
  end
17
19
 
18
20
  context 'from hash' do
19
- it 'should instantiate correctly from a hash' do
21
+ it 'instantiates correctly from a hash' do
20
22
  expect(Dimensions.new(h: 10, w: 20, d: 50)).to eql(dim1)
21
23
  end
22
24
  end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ module Laser
6
+ module Cutter
7
+ module Notching
8
+ RSpec.describe Edge do
9
+ # When the notch divides the side exactly, length / notch lands on a
10
+ # whole number, and float noise in the edge coordinates used to tip
11
+ # #ceil either way, so panels meeting at one joint disagreed.
12
+ context 'when the notch divides the side exactly' do
13
+ let(:outside) { Geometry::Line.new(Geometry::Point[0, 0], Geometry::Point[0, 3.25]) }
14
+
15
+ [2.9999999999999991, 3.0, 3.0000000000000013].each do |length|
16
+ it "gives a #{length.inspect}-long side 7 notches" do
17
+ inside = Geometry::Line.new(Geometry::Point[0.125, 0.125], Geometry::Point[0.125, 0.125 + length])
18
+ edge = described_class.new(outside, inside, notch_width: 0.5, thickness: 0.125)
19
+ expect(edge.notch_count).to eq(7)
20
+ end
21
+ end
22
+ end
23
+
24
+ context 'left vertical side' do
25
+ let(:notch_width) { 2 }
26
+ let(:inside) { Geometry::Line.new(Geometry::Point[1, 1], Geometry::Point[1, 9]) }
27
+ let(:outside) { Geometry::Line.new(Geometry::Point[0, 0], Geometry::Point[0, 10]) }
28
+ let(:edge) {
29
+ Notching::Edge.new(inside,
30
+ outside,
31
+ center_out: true,
32
+ fill_corners: true,
33
+ notch_width: notch_width,
34
+ kerf: 0.02,
35
+ thickness: 1)
36
+ }
37
+
38
+ it 'creates a node correctly' do
39
+ expect(inside.length).to eql(8.0)
40
+ expect(outside.length).to eql(10.0)
41
+ expect(edge.center_out).to be_truthy
42
+ expect(edge.kerf).to be_within(0.0001).of(0.02)
43
+ expect(edge.thickness).to be_within(0.0001).of(1)
44
+ expect(edge.notch_width).to be_within(notch_width / 3.0).of(notch_width)
45
+ end
46
+
47
+ it 'calculates notch width correctly' do
48
+ expect(inside.length).to eql(8.0)
49
+ expect(outside.length).to eql(10.0)
50
+ expect(edge.center_out).to be_truthy
51
+
52
+ expect(edge.kerf).to be_within(0.0001).of(0.02)
53
+ expect(edge.thickness).to be_within(0.0001).of(1)
54
+ end
55
+
56
+ it 'correctlies calculate v1 and v2' do
57
+ expect(edge.v1.to_a).to eql([1.0, 1.0])
58
+ expect(edge.v2.to_a).to eql([1.0, -1.0])
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
@@ -1,39 +1,43 @@
1
- require_relative 'spec_helper'
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
2
4
 
3
5
  module Laser
4
6
  module Cutter
5
7
  module Geometry
6
- describe Line do
8
+ RSpec.describe Line do
7
9
  let(:p1) { Point.new(1, 1) }
8
10
  let(:p2) { Point.new(7, 11) }
9
11
  let(:center) { Point.new((7 + 1) / 2, (11 + 1) / 2) }
10
12
  let(:line) { Line.new(p1, p2) }
11
13
 
12
- context '#center' do
13
- it 'should calculate' do
14
+ describe '#center' do
15
+ it 'calculates' do
14
16
  expect(line.center).to eql(center)
15
17
  end
16
18
  end
17
19
 
18
- context '#initialize' do
20
+ describe '#initialize' do
19
21
  let(:line2) { Line.new(from: [1, 1], to: [7, 11]) }
20
22
  let(:line3) { Line.new(from: Point.new(1, 1), to: Point.new(7, 11)) }
21
- it 'should create' do
23
+
24
+ it 'creates' do
22
25
  expect(line2.p1).to eql(Point.new(1, 1))
23
26
  expect(line2.p2).to eql(Point.new(7, 11))
24
27
  end
25
- it 'should properly equal identical line' do
28
+
29
+ it 'properlies equal identical line' do
26
30
  expect(line).to eql(line2)
27
31
  expect(line).to eql(line3)
28
32
  end
29
-
30
33
  end
31
34
 
32
- context '#length' do
35
+ describe '#length' do
33
36
  let(:line1) { Line.new(Point.new(0, 0), Point.new(0, 10)) }
34
37
  let(:line2) { Line.new(Point.new(0, 0), Point.new(-10, 0)) }
35
38
  let(:line3) { Line.new(Point.new(0, 0), Point.new(3, 4)) }
36
- it 'should calculate' do
39
+
40
+ it 'calculates' do
37
41
  expect(line1.length).to be_within(0.001).of(10)
38
42
  expect(line2.length).to be_within(0.001).of(10)
39
43
  expect(line3.length).to be_within(0.001).of(5)
@@ -46,25 +50,27 @@ module Laser
46
50
  let(:l3) { Line.new(Point[0, 0], Point[11, 10]) }
47
51
  let(:l4) { Line.new(Point[20, 20], Point[1, 1]) }
48
52
  let(:l5) { Line.new(Point[11, 10], Point[0, 0]) }
49
- it 'should detect equality' do
53
+
54
+ it 'detects equality' do
50
55
  expect(l1).to eql(Line.new(l1.p1, l1.p2))
51
- expect(l1).to_not eql(Line.new(l1.p1, Point[2, 4]))
56
+ expect(l1).not_to eql(Line.new(l1.p1, Point[2, 4]))
52
57
  expect(l5).to eql(l3)
53
58
  expect(l5.hash).to eql(l3.hash)
54
59
  end
55
60
 
56
- it 'should properly compare' do
61
+ it 'properlies compare' do
57
62
  list = [l4, l3, l1, l2]
58
63
  list.sort!
59
64
  expect(list).to eql([l1, l3, l2, l4])
60
65
  end
61
66
 
62
- it 'should properly uniq' do
67
+ it 'properlies uniq' do
63
68
  list = [l4, l1, l4, l2, l3, l3, l2, l1]
64
69
  list.sort!.uniq!
65
70
  expect(list).to eql([l1, l3, l2, l4])
66
71
  end
67
72
  end
73
+
68
74
  context 'overlaps and such' do
69
75
  let(:l1) { Line.new(Point[0, 0], Point[10, 0]) }
70
76
  let(:l2) { Line.new(Point[7, 0], Point[15, 0]) }
@@ -72,9 +78,8 @@ module Laser
72
78
  let(:l4) { Line.new(Point[0, 1], Point[0, 3]) }
73
79
  let(:l5) { Line.new(Point[0, -1], Point[0, 2]) }
74
80
 
75
-
76
- context '#overlaps?' do
77
- it 'should detect overlap' do
81
+ describe '#overlaps?' do
82
+ it 'detects overlap' do
78
83
  expect(l1.overlaps?(l2)).to be_truthy
79
84
  expect(l1.overlaps?(l3)).to be_falsey
80
85
  expect(l2.overlaps?(l3)).to be_falsey
@@ -84,9 +89,10 @@ module Laser
84
89
  end
85
90
  end
86
91
 
87
- context '#xor' do
88
- let(:xor) { [ Line.new(Point[0, 0], Point[7, 0]), Line.new(Point[10, 0], Point[15, 0])]}
89
- it 'should subtract lines' do
92
+ describe '#xor' do
93
+ let(:xor) { [Line.new(Point[0, 0], Point[7, 0]), Line.new(Point[10, 0], Point[15, 0])] }
94
+
95
+ it 'subtracts lines' do
90
96
  expect(l1.xor(l2)).to eql(xor)
91
97
  expect(l1.overlaps?(l3)).to be_falsey
92
98
  expect(l2.overlaps?(l3)).to be_falsey