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,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Laser
|
|
4
|
+
module Cutter
|
|
5
|
+
# Every command the executable answers to.
|
|
6
|
+
module CLI
|
|
7
|
+
extend Dry::CLI::Registry
|
|
8
|
+
|
|
9
|
+
PROGRAM = 'laser-cutter'
|
|
10
|
+
|
|
11
|
+
# Help and boxes are never wider than this, however wide the terminal is.
|
|
12
|
+
HELP_WIDTH = 90
|
|
13
|
+
|
|
14
|
+
# Columns assumed when there is no terminal, or it reports none.
|
|
15
|
+
DEFAULT_COLUMNS = 80
|
|
16
|
+
|
|
17
|
+
# @return [Integer] columns help wraps at, and a box takes: the terminal's less 6,
|
|
18
|
+
# capped at HELP_WIDTH
|
|
19
|
+
def self.help_width(columns = IO.console&.winsize&.last)
|
|
20
|
+
columns = DEFAULT_COLUMNS unless columns&.positive?
|
|
21
|
+
[columns - 6, HELP_WIDTH].min
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
Dry::CLI::Help.configure do
|
|
25
|
+
title "laser-cutter #{Laser::Cutter::VERSION}"
|
|
26
|
+
description 'Draws the notched panels of a box, ready to cut on a laser cutter, as a PDF or an SVG.'
|
|
27
|
+
epilogue 'Documentation: https://github.com/kigster/laser-cutter'
|
|
28
|
+
|
|
29
|
+
width CLI.help_width
|
|
30
|
+
exit_code_without_arguments 0
|
|
31
|
+
|
|
32
|
+
styles do
|
|
33
|
+
heading :bold, :cyan, case: :UPPERCASE
|
|
34
|
+
usage :yellow
|
|
35
|
+
example :yellow
|
|
36
|
+
option :green
|
|
37
|
+
example_comment :bright_black
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
register 'generate', Generate, aliases: ['g']
|
|
42
|
+
register 'page-sizes', PageSizes
|
|
43
|
+
register 'examples', Examples
|
|
44
|
+
register 'help', Help
|
|
45
|
+
register 'version', Version, aliases: ['-V', '--version']
|
|
46
|
+
register 'completion', Completion
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Laser
|
|
4
|
+
module Cutter
|
|
5
|
+
class Configuration < Hashie::Mash
|
|
6
|
+
include ::Hashie::Extensions::Mash::SymbolizeKeys
|
|
7
|
+
|
|
8
|
+
SIZE_REGEXP = %r{[\d.]+x[\d.]+x[\d.]+/[\d.]+(/[\d.]+)?}
|
|
9
|
+
|
|
10
|
+
FLOATS = %i(width height depth thickness notch margin padding stroke kerf).freeze
|
|
11
|
+
NON_ZERO = %i(width height depth thickness stroke).freeze
|
|
12
|
+
REQUIRED = %i(width height depth thickness notch file).freeze
|
|
13
|
+
|
|
14
|
+
@mutex = Mutex.new
|
|
15
|
+
|
|
16
|
+
class << self
|
|
17
|
+
attr_accessor :mutex, :defaults
|
|
18
|
+
|
|
19
|
+
def compute_defaults!
|
|
20
|
+
mutex.synchronize do
|
|
21
|
+
if defined?(@defaults)
|
|
22
|
+
@defaults
|
|
23
|
+
else
|
|
24
|
+
@defaults = compute_metric_units!
|
|
25
|
+
end
|
|
26
|
+
end.dup
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def compute_metric_units!
|
|
30
|
+
defaults = Hashie::Mash.new({
|
|
31
|
+
units: :in,
|
|
32
|
+
page_layout: 'portrait',
|
|
33
|
+
lid: 'full',
|
|
34
|
+
metadata: true,
|
|
35
|
+
in: {
|
|
36
|
+
kerf: 0.0024, # smallest kerf for thin material, usually it's more than that.
|
|
37
|
+
margin: 0.125,
|
|
38
|
+
padding: 0.1,
|
|
39
|
+
stroke: 0.001,
|
|
40
|
+
}
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
defaults[:mm] = defaults[:in].map { |k, v| [k, UnitsConverter.in2mm(v)] }.to_h
|
|
44
|
+
defaults
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
compute_defaults!
|
|
49
|
+
|
|
50
|
+
def initialize(options = {})
|
|
51
|
+
::Hashie::Extensions::SymbolizeKeys.symbolize_keys!(options)
|
|
52
|
+
|
|
53
|
+
options.delete_if { |_k, v| v.nil? }
|
|
54
|
+
options.delete(:lid) if options[:lid].to_s.empty? # a blank form field
|
|
55
|
+
if options[:units]
|
|
56
|
+
unit = options[:units].to_sym
|
|
57
|
+
unless self.class.defaults.key?(unit) || self.class.defaults.key?(unit.to_s)
|
|
58
|
+
options.delete(:units)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
super(self.class.defaults.merge(options))
|
|
63
|
+
|
|
64
|
+
if box =~ SIZE_REGEXP
|
|
65
|
+
dim, self[:thickness], self[:notch] = self[:box].split('/')
|
|
66
|
+
self[:width], self[:height], self[:depth] = dim.split('x')
|
|
67
|
+
delete(:box)
|
|
68
|
+
end
|
|
69
|
+
FLOATS.each do |k|
|
|
70
|
+
self[k] = self[k].to_f if self[k].is_a?(String)
|
|
71
|
+
end
|
|
72
|
+
merge!(self.class.defaults[self[:units].to_sym].merge(self))
|
|
73
|
+
self[:notch] = (self[:thickness] * 3.0).round(5) if self[:thickness] && self[:notch].nil?
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def validate!
|
|
77
|
+
missing = []
|
|
78
|
+
REQUIRED.each { |k| missing << k if self[k].nil? }
|
|
79
|
+
raise MissingOption, "#{missing.join(', ')} #{missing.size > 1 ? 'are' : 'is'} required, but missing." unless missing.empty?
|
|
80
|
+
|
|
81
|
+
zeros = []
|
|
82
|
+
NON_ZERO.each { |k| zeros << k if self[k] == 0 }
|
|
83
|
+
raise ZeroValueNotAllowed, "#{zeros.join(', ')} #{zeros.size > 1 ? 'are' : 'is'} required, but is zero." unless zeros.empty?
|
|
84
|
+
|
|
85
|
+
return if Box::LIDS.include?(lid.to_s.to_sym)
|
|
86
|
+
|
|
87
|
+
raise InvalidOption, "lid is #{lid.to_s.inspect}, but must be one of: #{Box::LIDS.join(', ')}."
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def change_units(new_units)
|
|
91
|
+
new_units = new_units.to_sym
|
|
92
|
+
return if new_units == units.to_sym
|
|
93
|
+
return unless self.class.defaults.key?(new_units)
|
|
94
|
+
|
|
95
|
+
k = units.to_sym == :in ? UnitsConverter.in2mm(1.0) : UnitsConverter.mm2in(1.0)
|
|
96
|
+
|
|
97
|
+
FLOATS.each do |field|
|
|
98
|
+
next if send(field).nil?
|
|
99
|
+
|
|
100
|
+
send("#{field}=", (send(field) * k).round(5))
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
self.units = new_units
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Laser
|
|
2
4
|
module Cutter
|
|
3
5
|
module Geometry
|
|
4
6
|
class Dimensions < Tuple
|
|
5
|
-
|
|
6
7
|
def w
|
|
7
8
|
coords.[](0)
|
|
8
9
|
end
|
|
@@ -22,9 +23,7 @@ module Laser
|
|
|
22
23
|
def hash_keys
|
|
23
24
|
[:w, :h, :d]
|
|
24
25
|
end
|
|
25
|
-
|
|
26
26
|
end
|
|
27
27
|
end
|
|
28
28
|
end
|
|
29
|
-
|
|
30
29
|
end
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Laser
|
|
2
4
|
module Cutter
|
|
3
5
|
module Geometry
|
|
4
6
|
class Line < Shape
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
self.new *array
|
|
7
|
+
def self.[](*array)
|
|
8
|
+
new(*array)
|
|
8
9
|
end
|
|
9
10
|
|
|
10
11
|
attr_accessor :p1, :p2
|
|
@@ -19,31 +20,33 @@ module Laser
|
|
|
19
20
|
self.p2 = point2.clone
|
|
20
21
|
end
|
|
21
22
|
self.position = p1.clone
|
|
22
|
-
raise 'Both points are required for line definition' unless
|
|
23
|
+
raise 'Both points are required for line definition' unless p1 && p2
|
|
23
24
|
end
|
|
24
25
|
|
|
25
26
|
def overlaps?(another)
|
|
26
27
|
xs, ys = sorted_coords(another)
|
|
27
|
-
return false unless xs.all?{|x| x == xs[0] } || ys.all?{|y| y == ys[0] }
|
|
28
|
+
return false unless xs.all?{ |x| x == xs[0] } || ys.all?{ |y| y == ys[0] }
|
|
28
29
|
return false if xs.first[1] < xs.last[0] || xs.first[0] > xs.last[1]
|
|
29
30
|
return false if ys.first[1] < ys.last[0] || ys.first[0] > ys.last[1]
|
|
31
|
+
|
|
30
32
|
true
|
|
31
33
|
end
|
|
32
34
|
|
|
33
35
|
def sorted_coords(another)
|
|
34
36
|
xs = [[p1.x, p2.x].sort, [another.p1.x, another.p2.x].sort]
|
|
35
37
|
ys = [[p1.y, p2.y].sort, [another.p1.y, another.p2.y].sort]
|
|
36
|
-
|
|
38
|
+
[xs, ys]
|
|
37
39
|
end
|
|
38
40
|
|
|
39
41
|
def xor(another)
|
|
40
42
|
return nil unless overlaps?(another)
|
|
43
|
+
|
|
41
44
|
xs, ys = sorted_coords(another)
|
|
42
45
|
xs.flatten!.sort!
|
|
43
46
|
ys.flatten!.sort!
|
|
44
47
|
|
|
45
|
-
[
|
|
46
|
-
|
|
48
|
+
[Line.new(Point[xs[0], ys[0]], Point[xs[1], ys[1]]),
|
|
49
|
+
Line.new(Point[xs[2], ys[2]], Point[xs[3], ys[3]])]
|
|
47
50
|
end
|
|
48
51
|
|
|
49
52
|
def relocate!
|
|
@@ -60,16 +63,16 @@ module Laser
|
|
|
60
63
|
end
|
|
61
64
|
|
|
62
65
|
def length
|
|
63
|
-
Math.sqrt((p2.x - p1.x)**2 + (p2.y - p1.y)**2)
|
|
66
|
+
Math.sqrt(((p2.x - p1.x)**2) + ((p2.y - p1.y)**2))
|
|
64
67
|
end
|
|
65
68
|
|
|
66
69
|
def to_s
|
|
67
|
-
"#{self.class.name.gsub(/.*::/,'').downcase} #{p1}=>#{p2}"
|
|
70
|
+
"#{self.class.name.gsub(/.*::/, '').downcase} #{p1}=>#{p2}"
|
|
68
71
|
end
|
|
69
72
|
|
|
70
73
|
def eql?(other)
|
|
71
74
|
(other.p1.eql?(p1) && other.p2.eql?(p2)) ||
|
|
72
|
-
|
|
75
|
+
(other.p2.eql?(p1) && other.p1.eql?(p2))
|
|
73
76
|
end
|
|
74
77
|
|
|
75
78
|
def normalized
|
|
@@ -77,17 +80,17 @@ module Laser
|
|
|
77
80
|
end
|
|
78
81
|
|
|
79
82
|
def <=>(other)
|
|
80
|
-
n1 =
|
|
83
|
+
n1 = normalized
|
|
81
84
|
n2 = other.normalized
|
|
82
85
|
n1.p1.eql?(n2.p1) ? n1.p2 <=> n2.p2 : n1.p1 <=> n2.p1
|
|
83
86
|
end
|
|
84
87
|
|
|
85
|
-
def <
|
|
86
|
-
|
|
88
|
+
def <(other)
|
|
89
|
+
p1 == other.p1 ? p2 < other.p2 : p1 < other.p1
|
|
87
90
|
end
|
|
88
91
|
|
|
89
|
-
def >
|
|
90
|
-
|
|
92
|
+
def >(other)
|
|
93
|
+
p1 == other.p1 ? p2 > other.p2 : p1 > other.p1
|
|
91
94
|
end
|
|
92
95
|
|
|
93
96
|
def hash
|
|
@@ -97,9 +100,7 @@ module Laser
|
|
|
97
100
|
def clone
|
|
98
101
|
self.class.new(p1, p2)
|
|
99
102
|
end
|
|
100
|
-
|
|
101
103
|
end
|
|
102
|
-
|
|
103
104
|
end
|
|
104
105
|
end
|
|
105
106
|
end
|
|
@@ -1,11 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Laser
|
|
2
4
|
module Cutter
|
|
3
5
|
module Geometry
|
|
4
6
|
class Rect < Line
|
|
5
|
-
|
|
6
7
|
attr_accessor :sides, :vertices
|
|
7
8
|
|
|
8
|
-
def self.[]
|
|
9
|
+
def self.[](p1, p2)
|
|
9
10
|
Rect.new(p1, p2)
|
|
10
11
|
end
|
|
11
12
|
|
|
@@ -19,8 +20,8 @@ module Laser
|
|
|
19
20
|
Rect.new(line.p1, line.p2)
|
|
20
21
|
end
|
|
21
22
|
|
|
22
|
-
def initialize(*
|
|
23
|
-
super
|
|
23
|
+
def initialize(*)
|
|
24
|
+
super
|
|
24
25
|
relocate!
|
|
25
26
|
end
|
|
26
27
|
|
|
@@ -47,15 +48,13 @@ module Laser
|
|
|
47
48
|
end
|
|
48
49
|
|
|
49
50
|
def to_s
|
|
50
|
-
"#{sprintf
|
|
51
|
+
"#{sprintf '%3d', w}(w)x#{sprintf '%3d', h}(h) @ #{position}"
|
|
51
52
|
end
|
|
52
53
|
|
|
53
54
|
def to_a
|
|
54
55
|
[[p1.x, p1.y], [p2.x, p2.y]]
|
|
55
56
|
end
|
|
56
57
|
end
|
|
57
|
-
|
|
58
58
|
end
|
|
59
|
-
|
|
60
59
|
end
|
|
61
60
|
end
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Laser
|
|
2
4
|
module Cutter
|
|
3
5
|
module Geometry
|
|
@@ -8,27 +10,28 @@ module Laser
|
|
|
8
10
|
@position ||= Point.new(0, 0)
|
|
9
11
|
end
|
|
10
12
|
|
|
11
|
-
def x=
|
|
13
|
+
def x=(value)
|
|
12
14
|
position.x = value
|
|
13
15
|
end
|
|
16
|
+
|
|
14
17
|
def x
|
|
15
18
|
position.x
|
|
16
19
|
end
|
|
17
20
|
|
|
18
|
-
def y=
|
|
21
|
+
def y=(value)
|
|
19
22
|
position.y = value
|
|
20
23
|
end
|
|
24
|
+
|
|
21
25
|
def y
|
|
22
26
|
position.y
|
|
23
27
|
end
|
|
24
28
|
|
|
25
|
-
def move_to
|
|
29
|
+
def move_to(new_point)
|
|
26
30
|
self.position = new_point
|
|
27
31
|
relocate!
|
|
28
32
|
self
|
|
29
33
|
end
|
|
30
34
|
|
|
31
|
-
|
|
32
35
|
# Implement in each shape to move to the new pointd
|
|
33
36
|
def relocate!
|
|
34
37
|
raise 'Abstract method'
|
|
@@ -37,6 +40,3 @@ module Laser
|
|
|
37
40
|
end
|
|
38
41
|
end
|
|
39
42
|
end
|
|
40
|
-
|
|
41
|
-
require_relative 'shape/line'
|
|
42
|
-
require_relative 'shape/rect'
|
|
@@ -1,60 +1,63 @@
|
|
|
1
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
2
3
|
module Laser
|
|
3
4
|
module Cutter
|
|
4
5
|
module Geometry
|
|
5
6
|
class Tuple
|
|
6
7
|
attr_accessor :coords
|
|
8
|
+
|
|
7
9
|
PRECISION = 0.000001
|
|
8
10
|
|
|
9
11
|
def initialize(*args)
|
|
10
12
|
x = args.first
|
|
11
|
-
coordinates =
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
13
|
+
coordinates = case x
|
|
14
|
+
when String
|
|
15
|
+
parse_string(x)
|
|
16
|
+
when Hash
|
|
17
|
+
parse_hash(x)
|
|
18
|
+
when Array
|
|
19
|
+
x.clone
|
|
20
|
+
when Tuple, Vector
|
|
21
|
+
x.to_a
|
|
22
|
+
else
|
|
23
|
+
args.clone
|
|
24
|
+
end
|
|
22
25
|
coordinates.map!(&:to_f)
|
|
23
26
|
self.coords = Vector.[](*coordinates)
|
|
24
27
|
end
|
|
25
28
|
|
|
26
|
-
def +
|
|
29
|
+
def +(x, y = nil)
|
|
27
30
|
shift = if x.is_a?(Vector)
|
|
28
31
|
x
|
|
29
32
|
elsif x.is_a?(Tuple)
|
|
30
33
|
x.coords
|
|
31
34
|
elsif y
|
|
32
|
-
Vector.[](x,y)
|
|
35
|
+
Vector.[](x, y)
|
|
33
36
|
end
|
|
34
|
-
self.class.new(
|
|
37
|
+
self.class.new(coords + shift)
|
|
35
38
|
end
|
|
36
39
|
|
|
37
|
-
|
|
38
40
|
alias_method :plus, :+
|
|
39
41
|
|
|
40
42
|
def to_a
|
|
41
|
-
|
|
43
|
+
coords.to_a
|
|
42
44
|
end
|
|
43
45
|
|
|
44
46
|
def to_s
|
|
45
|
-
"{#{coords.to_a.map { |a| sprintf(
|
|
47
|
+
"{#{coords.to_a.map { |a| sprintf('%.5f', a) }.join(separator)}}"
|
|
46
48
|
end
|
|
47
49
|
|
|
48
50
|
def valid?
|
|
49
|
-
raise "Have nil value: #{
|
|
51
|
+
raise "Have nil value: #{inspect}" if coords.to_a.any?(&:nil?)
|
|
52
|
+
|
|
50
53
|
true
|
|
51
54
|
end
|
|
52
55
|
|
|
53
|
-
def x=
|
|
56
|
+
def x=(value)
|
|
54
57
|
self.coords = Vector.[](value, coords.[](1))
|
|
55
58
|
end
|
|
56
59
|
|
|
57
|
-
def y=
|
|
60
|
+
def y=(value)
|
|
58
61
|
self.coords = Vector.[](coords.[](0), value)
|
|
59
62
|
end
|
|
60
63
|
|
|
@@ -70,11 +73,10 @@ module Laser
|
|
|
70
73
|
','
|
|
71
74
|
end
|
|
72
75
|
|
|
73
|
-
def []
|
|
76
|
+
def [](value)
|
|
74
77
|
coords.[](value)
|
|
75
78
|
end
|
|
76
79
|
|
|
77
|
-
|
|
78
80
|
# Override in subclasses, eg:
|
|
79
81
|
# def separator
|
|
80
82
|
# ';'
|
|
@@ -90,8 +92,9 @@ module Laser
|
|
|
90
92
|
# Identity, cloning and sorting/ordering
|
|
91
93
|
def eql?(other)
|
|
92
94
|
return false unless other.respond_to?(:coords)
|
|
95
|
+
|
|
93
96
|
equal = true
|
|
94
|
-
|
|
97
|
+
coords.each_with_index do |c, i|
|
|
95
98
|
if (c - other.coords.to_a[i])**2 > PRECISION
|
|
96
99
|
equal = false
|
|
97
100
|
break
|
|
@@ -99,18 +102,22 @@ module Laser
|
|
|
99
102
|
end
|
|
100
103
|
equal
|
|
101
104
|
end
|
|
105
|
+
|
|
102
106
|
def <=>(other)
|
|
103
|
-
|
|
107
|
+
x == other.x ? y <=> other.y : x <=> other.x
|
|
104
108
|
end
|
|
105
|
-
|
|
106
|
-
|
|
109
|
+
|
|
110
|
+
def <(other)
|
|
111
|
+
x == other.x ? y < other.y : x < other.x
|
|
107
112
|
end
|
|
108
|
-
|
|
109
|
-
|
|
113
|
+
|
|
114
|
+
def >(other)
|
|
115
|
+
x == other.x ? y > other.y : x > other.x
|
|
110
116
|
end
|
|
117
|
+
|
|
111
118
|
def clone
|
|
112
119
|
clone = super
|
|
113
|
-
clone.coords =
|
|
120
|
+
clone.coords = coords.clone
|
|
114
121
|
clone
|
|
115
122
|
end
|
|
116
123
|
|
|
@@ -118,18 +125,15 @@ module Laser
|
|
|
118
125
|
|
|
119
126
|
#
|
|
120
127
|
# Convert from, eg "100,50" to [100.0, 50.0],
|
|
121
|
-
def parse_string
|
|
128
|
+
def parse_string(string)
|
|
122
129
|
string.split(separator).map(&:to_f)
|
|
123
130
|
end
|
|
124
131
|
|
|
125
132
|
# Return array of coordinates
|
|
126
|
-
def parse_hash
|
|
127
|
-
hash_keys.map{ |k,
|
|
133
|
+
def parse_hash(hash)
|
|
134
|
+
hash_keys.map{ |k, _v| hash[k] }
|
|
128
135
|
end
|
|
129
|
-
|
|
130
136
|
end
|
|
131
|
-
|
|
132
|
-
|
|
133
137
|
end
|
|
134
138
|
end
|
|
135
139
|
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Laser
|
|
4
|
+
module Cutter
|
|
5
|
+
# Starts the CLI. The executable and the Aruba suite both build one, each
|
|
6
|
+
# with its own argv and streams, and it exits only through kernel.exit.
|
|
7
|
+
class Launcher
|
|
8
|
+
def initialize(argv, stdin = $stdin, stdout = $stdout, stderr = $stderr, kernel = Kernel)
|
|
9
|
+
@argv = argv
|
|
10
|
+
@stdin = stdin
|
|
11
|
+
@stdout = stdout
|
|
12
|
+
@stderr = stderr
|
|
13
|
+
@kernel = kernel
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def execute!
|
|
17
|
+
@kernel.exit(run)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
# dry-cli exits by itself after help or a usage error; its status is kept.
|
|
23
|
+
def run
|
|
24
|
+
Dry::CLI.new(CLI).call(arguments: @argv, out: @stdout, err: @stderr)
|
|
25
|
+
0
|
|
26
|
+
rescue SystemExit => e
|
|
27
|
+
e.status
|
|
28
|
+
rescue StandardError => e
|
|
29
|
+
ui.error(e.message)
|
|
30
|
+
@stderr.puts(e.backtrace) if @argv.intersect?(%w[-v --verbose])
|
|
31
|
+
1
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# The same boxes the commands draw, on this run's own streams.
|
|
35
|
+
def ui
|
|
36
|
+
Dry::CLI::UI::Console.new(out: @stdout, err: @stderr, box_width: CLI.help_width)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Laser
|
|
4
|
+
module Cutter
|
|
5
|
+
module Notching
|
|
6
|
+
class Base
|
|
7
|
+
attr_accessor :edge
|
|
8
|
+
|
|
9
|
+
def initialize(edge)
|
|
10
|
+
@edge = edge
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def notches
|
|
14
|
+
raise 'Abstract method'
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|