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.
- 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 -4
- data/.plans/001.00-dry-cli-migration/plan.md +29 -0
- data/.relaxed_rubocop.yml +153 -0
- data/.rspec +1 -1
- data/.rubocop.yml +31 -0
- data/.rubocop_todo.yml +90 -0
- data/.ruby-version +1 -0
- data/CLAUDE.md +62 -0
- data/CONTRIBUTING.md +2 -4
- data/Gemfile +14 -0
- data/README.md +83 -111
- data/Rakefile +41 -0
- data/docs/badges/coverage_badge.svg +21 -0
- data/exe/laser-cutter +8 -0
- data/exe/lc +1 -0
- data/justfile +97 -0
- data/laser-cutter.gemspec +26 -14
- data/lib/laser/cutter/aggregator.rb +101 -0
- data/lib/{laser-cutter → laser/cutter}/box.rb +44 -52
- 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 +42 -0
- data/lib/laser/cutter/cli/generate.rb +90 -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 +101 -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/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 +21 -20
- data/lib/laser/cutter/notching/infinite_iterator.rb +27 -0
- data/lib/{laser-cutter → laser/cutter}/notching/path_generator.rb +27 -60
- data/lib/laser/cutter/notching/shift.rb +17 -0
- data/lib/laser/cutter/page_manager.rb +50 -0
- data/lib/{laser-cutter → laser/cutter}/renderer/base.rb +3 -2
- 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/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 +29 -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_spec.rb +61 -0
- data/spec/laser/cutter/cli/config_file_spec.rb +31 -0
- data/spec/laser/cutter/cli_spec.rb +186 -0
- data/spec/laser/cutter/configuration_spec.rb +101 -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/{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 +102 -0
- data/spec/laser/cutter/renderer/svg_renderer_spec.rb +51 -0
- data/spec/laser/cutter/renderer_spec.rb +19 -0
- data/spec/spec_helper.rb +34 -25
- data/spec/support/aruba.rb +10 -0
- metadata +206 -80
- data/.travis.yml +0 -11
- data/LICENSE +0 -22
- data/bin/laser-cutter +0 -19
- data/lib/laser-cutter/aggregator.rb +0 -57
- 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/geometry.rb +0 -11
- data/lib/laser-cutter/notching/base.rb +0 -13
- data/lib/laser-cutter/notching.rb +0 -10
- 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 -40
- data/spec/configuration_spec.rb +0 -81
- data/spec/edge_spec.rb +0 -43
- data/spec/renderer_spec.rb +0 -72
- /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
|
-
|
|
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
|