laser-cutter 1.0.5 → 2.0.1
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 +5 -5
- data/.envrc +2 -0
- data/.github/workflows/lint.yml +22 -0
- data/.github/workflows/rspec.yml +22 -0
- data/.gitignore +5 -5
- data/.plans/001.00-dry-cli-migration/plan.md +29 -0
- data/.plans/002.00-lid-options/plan.md +45 -0
- data/.plans/003.00-ruby-api/plan.md +37 -0
- data/.relaxed_rubocop.yml +153 -0
- data/.rspec +1 -0
- data/.rubocop.yml +31 -0
- data/.rubocop_todo.yml +90 -0
- data/.ruby-version +1 -0
- data/CLAUDE.md +75 -0
- data/CONTRIBUTING.md +2 -4
- data/Gemfile +14 -0
- data/README.md +207 -79
- data/Rakefile +27 -13
- data/docs/badges/coverage_badge.svg +21 -0
- data/docs/images/box-lid-back.avif +0 -0
- data/docs/images/box-lid-full.avif +0 -0
- data/docs/images/box-lid-plain.avif +0 -0
- data/docs/images/box-metric.avif +0 -0
- data/docs/images/box-tray.avif +0 -0
- data/exe/laser-cutter +8 -0
- data/exe/lc +1 -0
- data/justfile +97 -0
- data/laser-cutter.gemspec +27 -14
- data/lib/laser/cutter/aggregator.rb +101 -0
- data/lib/laser/cutter/box.rb +251 -0
- data/lib/laser/cutter/cli/command.rb +46 -0
- data/lib/laser/cutter/cli/completion.rb +21 -0
- data/lib/laser/cutter/cli/config_file.rb +36 -0
- data/lib/laser/cutter/cli/examples.rb +47 -0
- data/lib/laser/cutter/cli/generate.rb +95 -0
- data/lib/laser/cutter/cli/help.rb +20 -0
- data/lib/laser/cutter/cli/page_sizes.rb +19 -0
- data/lib/laser/cutter/cli/version.rb +15 -0
- data/lib/laser/cutter/cli.rb +49 -0
- data/lib/laser/cutter/configuration.rb +107 -0
- data/lib/{laser-cutter → laser/cutter}/geometry/dimensions.rb +2 -3
- data/lib/{laser-cutter/geometry/shape → laser/cutter/geometry}/line.rb +19 -18
- data/lib/{laser-cutter → laser/cutter}/geometry/point.rb +4 -2
- data/lib/{laser-cutter/geometry/shape → laser/cutter/geometry}/rect.rb +6 -7
- data/lib/{laser-cutter → laser/cutter}/geometry/shape.rb +7 -7
- data/lib/{laser-cutter → laser/cutter}/geometry/tuple.rb +40 -36
- data/lib/laser/cutter/invalid_option.rb +8 -0
- data/lib/laser/cutter/launcher.rb +40 -0
- data/lib/laser/cutter/missing_option.rb +7 -0
- data/lib/laser/cutter/notching/base.rb +19 -0
- data/lib/{laser-cutter → laser/cutter}/notching/edge.rb +36 -20
- data/lib/laser/cutter/notching/infinite_iterator.rb +27 -0
- data/lib/laser/cutter/notching/path_generator.rb +187 -0
- data/lib/laser/cutter/notching/shift.rb +17 -0
- data/lib/laser/cutter/options.rb +154 -0
- data/lib/laser/cutter/page_manager.rb +50 -0
- data/lib/{laser-cutter → laser/cutter}/renderer/base.rb +10 -3
- data/lib/laser/cutter/renderer/box_renderer.rb +40 -0
- data/lib/laser/cutter/renderer/layout_renderer.rb +61 -0
- data/lib/laser/cutter/renderer/line_renderer.rb +21 -0
- data/lib/laser/cutter/renderer/meta_renderer.rb +83 -0
- data/lib/{laser-cutter → laser/cutter}/renderer/rect_renderer.rb +3 -1
- data/lib/laser/cutter/renderer/svg_renderer.rb +65 -0
- data/lib/laser/cutter/renderer.rb +24 -0
- data/lib/laser/cutter/types.rb +57 -0
- data/lib/laser/cutter/units_converter.rb +15 -0
- data/lib/laser/cutter/version.rb +7 -0
- data/lib/laser/cutter/zero_value_not_allowed.rb +7 -0
- data/lib/laser/cutter.rb +67 -0
- data/lib/laser-cutter.rb +2 -14
- data/lib/laser_cutter.rb +3 -0
- data/spec/laser/cutter/aggregator_spec.rb +70 -0
- data/spec/laser/cutter/box_lid_spec.rb +142 -0
- data/spec/laser/cutter/box_spec.rb +61 -0
- data/spec/laser/cutter/cli/config_file_spec.rb +31 -0
- data/spec/laser/cutter/cli_spec.rb +213 -0
- data/spec/laser/cutter/configuration_spec.rb +118 -0
- data/spec/{dimensions_spec.rb → laser/cutter/dimensions_spec.rb} +8 -6
- data/spec/laser/cutter/edge_spec.rb +64 -0
- data/spec/{line_spec.rb → laser/cutter/line_spec.rb} +26 -20
- data/spec/laser/cutter/options_spec.rb +120 -0
- data/spec/{page_manager_spec.rb → laser/cutter/page_manager_spec.rb} +27 -11
- data/spec/{path_generator_spec.rb → laser/cutter/path_generator_spec.rb} +18 -14
- data/spec/{point_spec.rb → laser/cutter/point_spec.rb} +27 -22
- data/spec/{rect_spec.rb → laser/cutter/rect_spec.rb} +9 -5
- data/spec/laser/cutter/renderer/layout_renderer_spec.rb +85 -0
- data/spec/laser/cutter/renderer/svg_renderer_spec.rb +51 -0
- data/spec/laser/cutter/renderer_spec.rb +19 -0
- data/spec/laser/cutter_spec.rb +114 -0
- data/spec/spec_helper.rb +35 -22
- data/spec/support/aruba.rb +10 -0
- data/spec/support/outline.rb +53 -0
- metadata +230 -76
- data/.travis.yml +0 -26
- data/BOXMAKER.md +0 -51
- data/LICENSE +0 -22
- data/bin/laser-cutter +0 -19
- data/lib/laser-cutter/aggregator.rb +0 -57
- data/lib/laser-cutter/box.rb +0 -183
- data/lib/laser-cutter/cli/opt_parser.rb +0 -131
- data/lib/laser-cutter/cli/serializer.rb +0 -51
- data/lib/laser-cutter/configuration.rb +0 -83
- data/lib/laser-cutter/face.rb +0 -18
- data/lib/laser-cutter/geometry.rb +0 -11
- data/lib/laser-cutter/notching/path_generator.rb +0 -234
- data/lib/laser-cutter/notching.rb +0 -9
- data/lib/laser-cutter/page_manager.rb +0 -43
- data/lib/laser-cutter/renderer/box_renderer.rb +0 -36
- data/lib/laser-cutter/renderer/layout_renderer.rb +0 -59
- data/lib/laser-cutter/renderer/line_renderer.rb +0 -17
- data/lib/laser-cutter/renderer/meta_renderer.rb +0 -77
- data/lib/laser-cutter/renderer.rb +0 -14
- data/lib/laser-cutter/version.rb +0 -5
- data/spec/aggregator_spec.rb +0 -65
- data/spec/box_spec.rb +0 -39
- data/spec/configuration_spec.rb +0 -81
- data/spec/edge_spec.rb +0 -43
- data/spec/renderer_spec.rb +0 -72
- /data/docs/{comparison.jpg → images/comparison.jpg} +0 -0
|
@@ -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,213 @@
|
|
|
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
|
+
{ 'plain' => 224, 'back' => 260, 'full' => 328 }.each do |lid, lines|
|
|
152
|
+
it "draws #{lines} lines for a #{lid} lid" do
|
|
153
|
+
run_command_and_stop("laser-cutter generate #{box} --lid #{lid} -o box.pdf")
|
|
154
|
+
expect(last_command_started).to have_output(%r{Lid:\s+#{lid}.*Drawing box\.pdf #{lines}/#{lines}}m)
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
it 'keeps the lid a saved configuration asks for' do
|
|
159
|
+
run_command_and_stop("laser-cutter generate #{box} -L plain -o box.pdf -W settings.json")
|
|
160
|
+
expect(JSON.parse(read('settings.json').join)).to include('lid' => 'plain')
|
|
161
|
+
|
|
162
|
+
run_command_and_stop('laser-cutter generate -R settings.json -o again.pdf')
|
|
163
|
+
expect(last_command_started).to have_output(%r{Drawing again\.pdf 224/224})
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
it 'opens the file with --open' do
|
|
167
|
+
allow_any_instance_of(Laser::Cutter::CLI::Generate).to receive(:system)
|
|
168
|
+
run_command_and_stop("laser-cutter generate #{box} -o box.pdf --open")
|
|
169
|
+
expect(last_command_started).to have_exit_status(0)
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
context 'when it cannot run' do
|
|
173
|
+
it 'fails without a file' do
|
|
174
|
+
run_command_and_stop("laser-cutter generate #{box}", fail_on_error: false)
|
|
175
|
+
expect(last_command_started).to have_exit_status(1)
|
|
176
|
+
expect(last_command_started.stderr).to match(/─ Error ─.*file is required/m)
|
|
177
|
+
expect(last_command_started.stdout).to be_empty
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
it 'draws the error box as wide as help' do
|
|
181
|
+
run_command_and_stop("laser-cutter generate #{box}", fail_on_error: false)
|
|
182
|
+
expect(last_command_started.stderr.lines.map { |line| line.chomp.length }.max).to eq(Laser::Cutter::CLI.help_width)
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
it 'fails on an unknown format' do
|
|
186
|
+
run_command_and_stop("laser-cutter generate #{box} -f dxf -o box.dxf", fail_on_error: false)
|
|
187
|
+
expect(last_command_started).to have_output(/unknown format "dxf", expected one of: pdf, svg/)
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
it 'fails on an unknown lid' do
|
|
191
|
+
run_command_and_stop("laser-cutter generate #{box} --lid sliding -o box.pdf", fail_on_error: false)
|
|
192
|
+
expect(last_command_started).to have_exit_status(1)
|
|
193
|
+
expect(last_command_started.stderr).to match(/was called with arguments.*--lid sliding/)
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
it 'fails on an unknown lid in a saved configuration' do
|
|
197
|
+
write_file('settings.json', '{"box": "4x3x2/0.125/0.5", "lid": "sliding"}')
|
|
198
|
+
run_command_and_stop('laser-cutter generate -R settings.json -o box.pdf', fail_on_error: false)
|
|
199
|
+
expect(last_command_started.stderr).to match(/─ Error ─.*lid is "sliding", but must be one of: full, back, plain/m)
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
it 'fails on a missing configuration file' do
|
|
203
|
+
run_command_and_stop('laser-cutter generate -R nothing.json -o box.pdf', fail_on_error: false)
|
|
204
|
+
expect(last_command_started).to have_output(/cannot read the configuration from nothing\.json/)
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
it 'prints the backtrace with --verbose' do
|
|
208
|
+
run_command_and_stop("laser-cutter generate #{box} -f dxf -o box.dxf -v", fail_on_error: false)
|
|
209
|
+
expect(last_command_started).to have_output(/renderer\.rb/)
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
end
|
|
@@ -0,0 +1,118 @@
|
|
|
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
|
+
describe 'lid' do
|
|
44
|
+
let(:opts) { { 'box' => '2x3x2/0.125/0.5', 'file' => '/tmp/a' } }
|
|
45
|
+
|
|
46
|
+
its(:lid) { is_expected.to eql('full') }
|
|
47
|
+
|
|
48
|
+
%w[full back plain].each do |lid|
|
|
49
|
+
it "accepts #{lid}" do
|
|
50
|
+
expect { described_class.new(opts.merge('lid' => lid)).validate! }.not_to raise_error
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
it 'rejects anything else' do
|
|
55
|
+
expect { described_class.new(opts.merge('lid' => 'sliding')).validate! }
|
|
56
|
+
.to raise_error(Laser::Cutter::InvalidOption, /lid is "sliding"/)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
context 'when notch is omitted' do
|
|
61
|
+
let(:opts) { { 'box' => '2.0x1.0x2/0.125', 'file' => '/tmp/a' } }
|
|
62
|
+
|
|
63
|
+
before { config.validate! }
|
|
64
|
+
|
|
65
|
+
its(:thickness) { is_expected.to eql(0.125) }
|
|
66
|
+
its(:notch) { is_expected.to eql(0.375) }
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
context 'when invalid units are provided' do
|
|
70
|
+
let(:opts) { { 'box' => '2x3x2/0.125/0.5', 'units' => 'xx' } }
|
|
71
|
+
|
|
72
|
+
its(:units) { is_expected.to eql(:in) }
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
context 'when converting between units' do
|
|
76
|
+
context 'from inches to mm' do
|
|
77
|
+
let(:opts) { { 'box' => '2.0x3x2/0.125/0.5', 'padding' => '4.2', 'units' => 'in' } }
|
|
78
|
+
|
|
79
|
+
its(:width) { is_expected.to eql(2.0) }
|
|
80
|
+
|
|
81
|
+
context 'after change_units(:in)' do
|
|
82
|
+
subject(:config) { described_class.new(opts).tap { |c| c.change_units(:in) } }
|
|
83
|
+
|
|
84
|
+
its(:width) { is_expected.to eql(2.0) }
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
context 'after change_units(:mm)' do
|
|
88
|
+
subject(:config) { described_class.new(opts).tap { |c| c.change_units(:mm) } }
|
|
89
|
+
|
|
90
|
+
its(:units) { is_expected.to eql(:mm) }
|
|
91
|
+
its(:width) { is_expected.to eql(50.8) }
|
|
92
|
+
its(:padding) { is_expected.to eql(106.68) }
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
context 'from mm to inches' do
|
|
97
|
+
let(:opts) { { 'box' => '20.0x30.0x40.0/5/5', 'margin' => '10.0', 'units' => 'mm' } }
|
|
98
|
+
|
|
99
|
+
its(:width) { is_expected.to eql(20.0) }
|
|
100
|
+
|
|
101
|
+
context 'after change_units(:mm)' do
|
|
102
|
+
subject(:config) { described_class.new(opts).tap { |c| c.change_units(:mm) } }
|
|
103
|
+
|
|
104
|
+
its(:width) { is_expected.to eql(20.0) }
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
context 'after change_units(:in)' do
|
|
108
|
+
subject(:config) { described_class.new(opts).tap { |c| c.change_units(:in) } }
|
|
109
|
+
|
|
110
|
+
its(:width) { is_expected.to be_within(0.00001).of(0.787401575) }
|
|
111
|
+
its(:margin) { is_expected.to be_within(0.00001).of(0.393700787) }
|
|
112
|
+
its(:units) { is_expected.to eql(:in) }
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
@@ -1,22 +1,24 @@
|
|
|
1
|
-
|
|
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
|
-
|
|
13
|
-
|
|
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 '
|
|
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
|
-
|
|
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
|
-
|
|
13
|
-
it '
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
53
|
+
|
|
54
|
+
it 'detects equality' do
|
|
50
55
|
expect(l1).to eql(Line.new(l1.p1, l1.p2))
|
|
51
|
-
expect(l1).
|
|
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 '
|
|
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 '
|
|
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
|
-
|
|
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
|
-
|
|
88
|
-
let(:xor) { [
|
|
89
|
-
|
|
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
|