laser-cutter 2.0.0 → 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 +4 -4
- data/.plans/002.00-lid-options/plan.md +45 -0
- data/.plans/003.00-ruby-api/plan.md +37 -0
- data/CLAUDE.md +15 -2
- data/README.md +106 -1
- 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/laser-cutter.gemspec +1 -0
- data/lib/laser/cutter/box.rb +97 -19
- data/lib/laser/cutter/cli/examples.rb +5 -0
- data/lib/laser/cutter/cli/generate.rb +7 -2
- data/lib/laser/cutter/configuration.rb +6 -0
- data/lib/laser/cutter/invalid_option.rb +8 -0
- data/lib/laser/cutter/notching/edge.rb +15 -0
- data/lib/laser/cutter/notching/path_generator.rb +46 -75
- data/lib/laser/cutter/options.rb +154 -0
- data/lib/laser/cutter/renderer/base.rb +7 -1
- data/lib/laser/cutter/renderer/layout_renderer.rb +3 -3
- data/lib/laser/cutter/renderer/meta_renderer.rb +1 -1
- data/lib/laser/cutter/renderer/svg_renderer.rb +3 -3
- data/lib/laser/cutter/types.rb +57 -0
- data/lib/laser/cutter/version.rb +1 -1
- data/lib/laser/cutter.rb +38 -0
- data/spec/laser/cutter/box_lid_spec.rb +142 -0
- data/spec/laser/cutter/cli_spec.rb +27 -0
- data/spec/laser/cutter/configuration_spec.rb +17 -0
- data/spec/laser/cutter/options_spec.rb +120 -0
- data/spec/laser/cutter/renderer/layout_renderer_spec.rb +6 -23
- data/spec/laser/cutter_spec.rb +114 -0
- data/spec/support/outline.rb +53 -0
- metadata +29 -1
|
@@ -148,6 +148,21 @@ RSpec.describe 'laser-cutter', type: :aruba do
|
|
|
148
148
|
expect(last_command_started).to have_output(/"depth": 2\.0/)
|
|
149
149
|
end
|
|
150
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
|
+
|
|
151
166
|
it 'opens the file with --open' do
|
|
152
167
|
allow_any_instance_of(Laser::Cutter::CLI::Generate).to receive(:system)
|
|
153
168
|
run_command_and_stop("laser-cutter generate #{box} -o box.pdf --open")
|
|
@@ -172,6 +187,18 @@ RSpec.describe 'laser-cutter', type: :aruba do
|
|
|
172
187
|
expect(last_command_started).to have_output(/unknown format "dxf", expected one of: pdf, svg/)
|
|
173
188
|
end
|
|
174
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
|
+
|
|
175
202
|
it 'fails on a missing configuration file' do
|
|
176
203
|
run_command_and_stop('laser-cutter generate -R nothing.json -o box.pdf', fail_on_error: false)
|
|
177
204
|
expect(last_command_started).to have_output(/cannot read the configuration from nothing\.json/)
|
|
@@ -40,6 +40,23 @@ module Laser
|
|
|
40
40
|
end
|
|
41
41
|
end
|
|
42
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
|
+
|
|
43
60
|
context 'when notch is omitted' do
|
|
44
61
|
let(:opts) { { 'box' => '2.0x1.0x2/0.125', 'file' => '/tmp/a' } }
|
|
45
62
|
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
module Laser
|
|
6
|
+
module Cutter
|
|
7
|
+
RSpec.describe Options do
|
|
8
|
+
subject(:options) { described_class.new(settings) }
|
|
9
|
+
|
|
10
|
+
let(:dimensions) { { width: 40, height: 30, depth: 20, thickness: 3 } }
|
|
11
|
+
let(:settings) { dimensions }
|
|
12
|
+
|
|
13
|
+
describe 'with the dimensions alone' do
|
|
14
|
+
its(:width) { is_expected.to eql(40.0) }
|
|
15
|
+
its(:thickness) { is_expected.to eql(3.0) }
|
|
16
|
+
its(:units) { is_expected.to eq('in') }
|
|
17
|
+
its(:lid) { is_expected.to eq('full') }
|
|
18
|
+
its(:page_layout) { is_expected.to eq('portrait') }
|
|
19
|
+
its(:metadata) { is_expected.to be(true) }
|
|
20
|
+
its(:inside_box) { is_expected.to be(false) }
|
|
21
|
+
its(:notch) { is_expected.to be_nil }
|
|
22
|
+
its(:kerf) { is_expected.to be_nil }
|
|
23
|
+
its(:format) { is_expected.to be_nil }
|
|
24
|
+
its(:file) { is_expected.to be_nil }
|
|
25
|
+
its(:page_size) { is_expected.to be_nil }
|
|
26
|
+
|
|
27
|
+
it { is_expected.not_to respond_to(:width=) }
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
describe 'with what a form sends: Strings, some of them blank' do
|
|
31
|
+
let(:settings) do
|
|
32
|
+
{ 'width' => '70', 'height' => '20', 'depth' => '50', 'thickness' => '4.3', 'notch' => '', 'kerf' => '0.1',
|
|
33
|
+
'units' => 'mm', 'lid' => 'plain', 'format' => 'SVG', 'page_size' => 'a4', 'page_layout' => '',
|
|
34
|
+
'metadata' => '0', 'inside_box' => 'true', 'margin' => '', 'padding' => '2', 'stroke' => '0.2' }
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
its(:width) { is_expected.to eql(70.0) }
|
|
38
|
+
its(:thickness) { is_expected.to eql(4.3) }
|
|
39
|
+
its(:notch) { is_expected.to be_nil }
|
|
40
|
+
its(:kerf) { is_expected.to eql(0.1) }
|
|
41
|
+
its(:units) { is_expected.to eq('mm') }
|
|
42
|
+
its(:lid) { is_expected.to eq('plain') }
|
|
43
|
+
its(:format) { is_expected.to eq('svg') }
|
|
44
|
+
its(:page_size) { is_expected.to eq('A4') }
|
|
45
|
+
its(:page_layout) { is_expected.to eq('portrait') }
|
|
46
|
+
its(:metadata) { is_expected.to be(false) }
|
|
47
|
+
its(:inside_box) { is_expected.to be(true) }
|
|
48
|
+
its(:margin) { is_expected.to be_nil }
|
|
49
|
+
its(:padding) { is_expected.to eql(2.0) }
|
|
50
|
+
its(:stroke) { is_expected.to eql(0.2) }
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
describe 'with Symbols' do
|
|
54
|
+
let(:settings) { dimensions.merge(units: :mm, lid: :back, format: :pdf, page_layout: :landscape) }
|
|
55
|
+
|
|
56
|
+
its(:units) { is_expected.to eq('mm') }
|
|
57
|
+
its(:lid) { is_expected.to eq('back') }
|
|
58
|
+
its(:format) { is_expected.to eq('pdf') }
|
|
59
|
+
its(:page_layout) { is_expected.to eq('landscape') }
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
describe 'given an Options' do
|
|
63
|
+
it 'returns it' do
|
|
64
|
+
expect(described_class.new(options)).to be(options)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
describe '#new' do
|
|
69
|
+
subject(:changed) { options.new(lid: :plain) }
|
|
70
|
+
|
|
71
|
+
its(:lid) { is_expected.to eq('plain') }
|
|
72
|
+
its(:width) { is_expected.to eql(40.0) }
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
describe 'what it refuses' do
|
|
76
|
+
{
|
|
77
|
+
{ lid: 'sliding' } => 'lid cannot be "sliding", but must be one of: full, back, plain.',
|
|
78
|
+
{ units: 'cm' } => 'units cannot be "cm", but must be one of: in, mm.',
|
|
79
|
+
{ format: 'dxf' } => 'format cannot be "dxf", but must be one of: pdf, svg.',
|
|
80
|
+
{ page_layout: 'sideways' } => 'page_layout cannot be "sideways", but must be one of: portrait, landscape.',
|
|
81
|
+
{ page_size: 'A99' } => 'page_size cannot be "A99".',
|
|
82
|
+
{ width: 'wide' } => 'width cannot be "wide".',
|
|
83
|
+
{ depth: 0 } => 'depth cannot be 0.',
|
|
84
|
+
{ kerf: -0.1 } => 'kerf cannot be -0.1.',
|
|
85
|
+
{ metadata: 'maybe' } => 'metadata cannot be "maybe".'
|
|
86
|
+
}.each do |bad, message|
|
|
87
|
+
it "raises InvalidOption when #{bad.keys.first} is #{bad.values.first.inspect}" do
|
|
88
|
+
expect { described_class.new(dimensions.merge(bad)) }.to raise_error(InvalidOption, message)
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
it 'raises InvalidOption for a key that is not an option' do
|
|
93
|
+
expect { described_class.new(dimensions.merge(lids: :plain)) }.to raise_error(InvalidOption, /lids/)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
it 'raises MissingOption naming every dimension left out' do
|
|
97
|
+
expect { described_class.new(width: 40, depth: '') }
|
|
98
|
+
.to raise_error(MissingOption, 'height, depth, thickness are required, but missing.')
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
it 'raises MissingOption naming the one dimension left out' do
|
|
102
|
+
expect { described_class.new(dimensions.except(:width)) }.to raise_error(MissingOption, 'width is required, but missing.')
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
describe '#to_configuration' do
|
|
107
|
+
subject(:config) { described_class.new(dimensions.merge(units: :mm, lid: :back, inside_box: true, format: :svg)).to_configuration }
|
|
108
|
+
|
|
109
|
+
it { is_expected.to be_a(Configuration) }
|
|
110
|
+
|
|
111
|
+
its(:width) { is_expected.to eql(40.0) }
|
|
112
|
+
its(:lid) { is_expected.to eq('back') }
|
|
113
|
+
its(:debug) { is_expected.to be(true) }
|
|
114
|
+
its(:notch) { is_expected.to eql(9.0) }
|
|
115
|
+
its(:kerf) { is_expected.to be_within(1e-6).of(0.06096) }
|
|
116
|
+
its(:format) { is_expected.to be_nil }
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'spec_helper'
|
|
4
|
+
require 'tmpdir'
|
|
4
5
|
|
|
5
6
|
module Laser
|
|
6
7
|
module Cutter
|
|
@@ -8,33 +9,15 @@ module Laser
|
|
|
8
9
|
RSpec.describe LayoutRenderer do
|
|
9
10
|
describe '#render' do
|
|
10
11
|
let(:renderer) { LayoutRenderer.new(config) }
|
|
11
|
-
let(:
|
|
12
|
+
let(:dir) { Dir.mktmpdir }
|
|
13
|
+
let(:file) { File.join(dir, 'box.pdf') }
|
|
14
|
+
|
|
15
|
+
after { FileUtils.remove_entry(dir) }
|
|
12
16
|
|
|
13
17
|
def render_file(filename)
|
|
14
|
-
real_file = ENV['RSPEC_SAVE_PDF'] ? true : false
|
|
15
18
|
config.validate!
|
|
16
|
-
expect(!File.exist?(filename)) if real_file
|
|
17
19
|
renderer.render
|
|
18
|
-
expect(File.
|
|
19
|
-
expect(File.size(filename) > 0) if real_file
|
|
20
|
-
rescue Exception => e
|
|
21
|
-
warn e.backtrace.join("\n")
|
|
22
|
-
fail e.message
|
|
23
|
-
ensure
|
|
24
|
-
if real_file
|
|
25
|
-
begin
|
|
26
|
-
File.delete(filename)
|
|
27
|
-
rescue StandardError
|
|
28
|
-
nil
|
|
29
|
-
end
|
|
30
|
-
expect(!File.exist?(filename))
|
|
31
|
-
end
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
before do
|
|
35
|
-
unless ENV['RSPEC_SAVE_PDF']
|
|
36
|
-
expect_any_instance_of(Prawn::Document).to receive(:render_file).once
|
|
37
|
-
end
|
|
20
|
+
expect(File.binread(filename)).to start_with('%PDF')
|
|
38
21
|
end
|
|
39
22
|
|
|
40
23
|
context 'metric' do
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
require 'tmpdir'
|
|
5
|
+
|
|
6
|
+
# What a Ruby program, such as the MakeABox.io website, calls instead of the command line.
|
|
7
|
+
RSpec.describe Laser::Cutter do
|
|
8
|
+
let(:settings) { { width: 40, height: 30, depth: 20, thickness: 3, units: 'mm' } }
|
|
9
|
+
let(:options) { Laser::Cutter::Options.new(settings) }
|
|
10
|
+
let(:dir) { Dir.mktmpdir }
|
|
11
|
+
|
|
12
|
+
after { FileUtils.remove_entry(dir) }
|
|
13
|
+
|
|
14
|
+
describe '.render' do
|
|
15
|
+
subject(:document) { described_class.render(options) }
|
|
16
|
+
|
|
17
|
+
it { is_expected.to start_with('%PDF') }
|
|
18
|
+
|
|
19
|
+
it 'writes no file' do
|
|
20
|
+
expect { Dir.chdir(dir) { document } }.not_to(change { Dir.children(dir) })
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
it 'draws an SVG when the options ask for one' do
|
|
24
|
+
expect(described_class.render(options.new(format: :svg))).to include('<svg', 'width="109.46048mm"')
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
it 'takes a Hash, the way a form sends it' do
|
|
28
|
+
expect(described_class.render(settings.transform_keys(&:to_s).transform_values(&:to_s))).to start_with('%PDF')
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
it 'yields each line it draws' do
|
|
32
|
+
lines = []
|
|
33
|
+
described_class.render(options.new(format: 'svg')) { |line| lines << line }
|
|
34
|
+
expect(lines.size).to eq(280)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
{ nil => 280, '' => 280, 'full' => 280, :back => 208, 'back' => 208, :plain => 184, 'plain' => 184 }.each do |lid, lines|
|
|
38
|
+
it "draws #{lines} lines for the lid #{lid.inspect}" do
|
|
39
|
+
svg = described_class.render(settings.merge(lid: lid, format: :svg))
|
|
40
|
+
expect(svg.scan('<line').size).to eq(lines)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
it 'names the lid in the description of an SVG' do
|
|
45
|
+
expect(described_class.render(settings.merge(lid: :plain, format: :svg))).to match(/<desc>.*lid: plain/m)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
it 'leaves the description out of an SVG without metadata' do
|
|
49
|
+
expect(described_class.render(settings.merge(format: :svg, metadata: false))).not_to include('<desc>')
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
it 'draws the box without kerf as well in a PDF, when asked' do
|
|
53
|
+
lines = 0
|
|
54
|
+
described_class.render(options.new(inside_box: true)) { lines += 1 }
|
|
55
|
+
expect(lines).to eq(560)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
it 'raises InvalidOption for a lid it does not know' do
|
|
59
|
+
expect { described_class.render(settings.merge(lid: 'sliding')) }.to raise_error(Laser::Cutter::InvalidOption, /lid/)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
it 'raises MissingOption for a dimension left out' do
|
|
63
|
+
expect { described_class.render(settings.except(:depth)) }.to raise_error(Laser::Cutter::MissingOption, /depth is required/)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
describe '.write' do
|
|
68
|
+
let(:path) { File.join(dir, 'box.svg') }
|
|
69
|
+
|
|
70
|
+
it 'takes the format from the extension, and returns the path' do
|
|
71
|
+
expect(described_class.write(options.new(file: path, lid: :back))).to eq(path)
|
|
72
|
+
expect(File.read(path).scan('<line').size).to eq(208)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
it 'writes the format it is given, whatever the extension' do
|
|
76
|
+
described_class.write(settings.merge(file: path, format: :pdf))
|
|
77
|
+
expect(File.binread(path)).to start_with('%PDF')
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
it 'writes a PDF when the extension says nothing' do
|
|
81
|
+
described_class.write(settings.merge(file: File.join(dir, 'box')))
|
|
82
|
+
expect(File.binread(File.join(dir, 'box'))).to start_with('%PDF')
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
it 'raises MissingOption without a file' do
|
|
86
|
+
expect { described_class.write(options) }.to raise_error(Laser::Cutter::MissingOption, /file is required/)
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# app/helpers/home_helper.rb and app/controllers/home_controller.rb, written against 1.0.3.
|
|
91
|
+
describe 'the calls MakeABox.io makes today' do
|
|
92
|
+
let(:form) { { 'width' => '40', 'height' => '30', 'depth' => '20', 'thickness' => '3', 'units' => 'mm', 'lid' => '' } }
|
|
93
|
+
let(:config) { Laser::Cutter::Configuration.new(form.merge(metadata: true)) }
|
|
94
|
+
let(:path) { File.join(dir, 'box.pdf') }
|
|
95
|
+
|
|
96
|
+
before { config['file'] = path }
|
|
97
|
+
|
|
98
|
+
it 'still draw a box' do
|
|
99
|
+
config.validate!
|
|
100
|
+
Laser::Cutter::Renderer::LayoutRenderer.new(config).render
|
|
101
|
+
expect(File.binread(path)).to start_with('%PDF')
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
it 'still list the page sizes' do
|
|
105
|
+
expect(Laser::Cutter::PageManager.new(config.units).page_size_values)
|
|
106
|
+
.to include(['A4', be_within(0.1).of(210), be_within(0.1).of(297)])
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
it 'still fail on a missing dimension with an error the website rescues' do
|
|
110
|
+
config.delete(:width)
|
|
111
|
+
expect { config.validate! }.to raise_error(Laser::Cutter::MissingOption)
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Reads the lines of one panel as the region they enclose, without using any
|
|
4
|
+
# of the code that drew them.
|
|
5
|
+
module Outline
|
|
6
|
+
module_function
|
|
7
|
+
|
|
8
|
+
# Whether a point is inside the outline, by the even-odd rule: a ray from
|
|
9
|
+
# the point crosses the outline an odd number of times.
|
|
10
|
+
#
|
|
11
|
+
# @param lines [Array<Laser::Cutter::Geometry::Line>] an outline of horizontal and vertical lines
|
|
12
|
+
# @return [Boolean]
|
|
13
|
+
def inside?(lines, x, y)
|
|
14
|
+
lines.count do |line|
|
|
15
|
+
next false unless line.p1.x == line.p2.x && line.p1.x > x
|
|
16
|
+
|
|
17
|
+
low, high = [line.p1.y, line.p2.y].sort
|
|
18
|
+
low < y && y < high
|
|
19
|
+
end.odd?
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Whether the outline grown by a margin on every side covers the point.
|
|
23
|
+
#
|
|
24
|
+
# @return [Boolean]
|
|
25
|
+
def within?(lines, x, y, margin)
|
|
26
|
+
offsets = [-margin, 0, margin]
|
|
27
|
+
offsets.product(offsets).any? { |dx, dy| inside?(lines, x + dx, y + dy) }
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Whether every line ends where another begins, so the outline has no gap.
|
|
31
|
+
#
|
|
32
|
+
# @return [Boolean]
|
|
33
|
+
def closed?(lines)
|
|
34
|
+
ends = lines.flat_map { |line| [line.p1, line.p2] }.map { |point| point.to_a.map { |c| c.round(6) + 0.0 } }
|
|
35
|
+
ends.tally.values.all?(&:even?)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Points spread over a rectangle, off any round coordinate a line could sit on.
|
|
39
|
+
#
|
|
40
|
+
# @param rect [Laser::Cutter::Geometry::Rect]
|
|
41
|
+
# @param margin [Float] how far past the rectangle to reach
|
|
42
|
+
# @param steps [Integer] points per axis
|
|
43
|
+
# @return [Array<Array(Float, Float)>]
|
|
44
|
+
def samples(rect, margin:, steps: 61)
|
|
45
|
+
xs = spread(rect.p1.x - margin, rect.p2.x + margin, steps)
|
|
46
|
+
ys = spread(rect.p1.y - margin, rect.p2.y + margin, steps)
|
|
47
|
+
xs.product(ys)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def spread(from, to, steps)
|
|
51
|
+
Array.new(steps) { |i| from + ((to - from) * (i + 0.4142) / steps) }
|
|
52
|
+
end
|
|
53
|
+
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: laser-cutter
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.0.
|
|
4
|
+
version: 2.0.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Konstantin Gredeskoul
|
|
@@ -65,6 +65,20 @@ dependencies:
|
|
|
65
65
|
- - ">="
|
|
66
66
|
- !ruby/object:Gem::Version
|
|
67
67
|
version: '0'
|
|
68
|
+
- !ruby/object:Gem::Dependency
|
|
69
|
+
name: dry-struct
|
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
|
71
|
+
requirements:
|
|
72
|
+
- - "~>"
|
|
73
|
+
- !ruby/object:Gem::Version
|
|
74
|
+
version: '1.6'
|
|
75
|
+
type: :runtime
|
|
76
|
+
prerelease: false
|
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
78
|
+
requirements:
|
|
79
|
+
- - "~>"
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: '1.6'
|
|
68
82
|
- !ruby/object:Gem::Dependency
|
|
69
83
|
name: hashie
|
|
70
84
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -250,6 +264,8 @@ files:
|
|
|
250
264
|
- ".github/workflows/rspec.yml"
|
|
251
265
|
- ".gitignore"
|
|
252
266
|
- ".plans/001.00-dry-cli-migration/plan.md"
|
|
267
|
+
- ".plans/002.00-lid-options/plan.md"
|
|
268
|
+
- ".plans/003.00-ruby-api/plan.md"
|
|
253
269
|
- ".relaxed_rubocop.yml"
|
|
254
270
|
- ".rspec"
|
|
255
271
|
- ".rubocop.yml"
|
|
@@ -262,6 +278,11 @@ files:
|
|
|
262
278
|
- README.md
|
|
263
279
|
- Rakefile
|
|
264
280
|
- docs/badges/coverage_badge.svg
|
|
281
|
+
- docs/images/box-lid-back.avif
|
|
282
|
+
- docs/images/box-lid-full.avif
|
|
283
|
+
- docs/images/box-lid-plain.avif
|
|
284
|
+
- docs/images/box-metric.avif
|
|
285
|
+
- docs/images/box-tray.avif
|
|
265
286
|
- docs/images/comparison.jpg
|
|
266
287
|
- exe/laser-cutter
|
|
267
288
|
- exe/lc
|
|
@@ -287,6 +308,7 @@ files:
|
|
|
287
308
|
- lib/laser/cutter/geometry/rect.rb
|
|
288
309
|
- lib/laser/cutter/geometry/shape.rb
|
|
289
310
|
- lib/laser/cutter/geometry/tuple.rb
|
|
311
|
+
- lib/laser/cutter/invalid_option.rb
|
|
290
312
|
- lib/laser/cutter/launcher.rb
|
|
291
313
|
- lib/laser/cutter/missing_option.rb
|
|
292
314
|
- lib/laser/cutter/notching/base.rb
|
|
@@ -294,6 +316,7 @@ files:
|
|
|
294
316
|
- lib/laser/cutter/notching/infinite_iterator.rb
|
|
295
317
|
- lib/laser/cutter/notching/path_generator.rb
|
|
296
318
|
- lib/laser/cutter/notching/shift.rb
|
|
319
|
+
- lib/laser/cutter/options.rb
|
|
297
320
|
- lib/laser/cutter/page_manager.rb
|
|
298
321
|
- lib/laser/cutter/renderer.rb
|
|
299
322
|
- lib/laser/cutter/renderer/base.rb
|
|
@@ -303,11 +326,13 @@ files:
|
|
|
303
326
|
- lib/laser/cutter/renderer/meta_renderer.rb
|
|
304
327
|
- lib/laser/cutter/renderer/rect_renderer.rb
|
|
305
328
|
- lib/laser/cutter/renderer/svg_renderer.rb
|
|
329
|
+
- lib/laser/cutter/types.rb
|
|
306
330
|
- lib/laser/cutter/units_converter.rb
|
|
307
331
|
- lib/laser/cutter/version.rb
|
|
308
332
|
- lib/laser/cutter/zero_value_not_allowed.rb
|
|
309
333
|
- lib/laser_cutter.rb
|
|
310
334
|
- spec/laser/cutter/aggregator_spec.rb
|
|
335
|
+
- spec/laser/cutter/box_lid_spec.rb
|
|
311
336
|
- spec/laser/cutter/box_spec.rb
|
|
312
337
|
- spec/laser/cutter/cli/config_file_spec.rb
|
|
313
338
|
- spec/laser/cutter/cli_spec.rb
|
|
@@ -315,6 +340,7 @@ files:
|
|
|
315
340
|
- spec/laser/cutter/dimensions_spec.rb
|
|
316
341
|
- spec/laser/cutter/edge_spec.rb
|
|
317
342
|
- spec/laser/cutter/line_spec.rb
|
|
343
|
+
- spec/laser/cutter/options_spec.rb
|
|
318
344
|
- spec/laser/cutter/page_manager_spec.rb
|
|
319
345
|
- spec/laser/cutter/path_generator_spec.rb
|
|
320
346
|
- spec/laser/cutter/point_spec.rb
|
|
@@ -322,8 +348,10 @@ files:
|
|
|
322
348
|
- spec/laser/cutter/renderer/layout_renderer_spec.rb
|
|
323
349
|
- spec/laser/cutter/renderer/svg_renderer_spec.rb
|
|
324
350
|
- spec/laser/cutter/renderer_spec.rb
|
|
351
|
+
- spec/laser/cutter_spec.rb
|
|
325
352
|
- spec/spec_helper.rb
|
|
326
353
|
- spec/support/aruba.rb
|
|
354
|
+
- spec/support/outline.rb
|
|
327
355
|
homepage: https://github.com/kigster/laser-cutter
|
|
328
356
|
licenses:
|
|
329
357
|
- MIT
|