laser-cutter 1.0.5 → 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 -5
- data/.plans/001.00-dry-cli-migration/plan.md +29 -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 +62 -0
- data/CONTRIBUTING.md +2 -4
- data/Gemfile +14 -0
- data/README.md +110 -87
- data/Rakefile +27 -13
- 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/box.rb +173 -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 +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 +23 -22
- data/lib/laser/cutter/notching/infinite_iterator.rb +27 -0
- data/lib/{laser-cutter → laser/cutter}/notching/path_generator.rb +76 -94
- 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 +35 -22
- data/spec/support/aruba.rb +10 -0
- metadata +202 -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.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
data/lib/laser-cutter/box.rb
DELETED
|
@@ -1,183 +0,0 @@
|
|
|
1
|
-
require 'forwardable'
|
|
2
|
-
module Laser
|
|
3
|
-
module Cutter
|
|
4
|
-
class Box
|
|
5
|
-
# Everything is in millimeters
|
|
6
|
-
extend Forwardable
|
|
7
|
-
def_delegators :@dim, :w, :h, :d
|
|
8
|
-
|
|
9
|
-
attr_accessor :dim, :thickness, :notch_width, :kerf
|
|
10
|
-
attr_accessor :padding, :units, :inside_box
|
|
11
|
-
|
|
12
|
-
attr_accessor :front, :back, :top, :bottom, :left, :right
|
|
13
|
-
attr_accessor :faces, :bounds, :conf, :corner_face
|
|
14
|
-
attr_accessor :metadata, :notches
|
|
15
|
-
|
|
16
|
-
def initialize(config = {})
|
|
17
|
-
self.dim = Geometry::Dimensions.new(config['width'], config['height'], config['depth'])
|
|
18
|
-
self.thickness = config['thickness']
|
|
19
|
-
|
|
20
|
-
self.notch_width = config['notch'] || (1.0 * self.longest / 5.0)
|
|
21
|
-
self.kerf = config['kerf'] || 0.0
|
|
22
|
-
self.padding = config['padding']
|
|
23
|
-
self.units = config['units']
|
|
24
|
-
self.inside_box = config['inside_box']
|
|
25
|
-
|
|
26
|
-
self.notches = []
|
|
27
|
-
|
|
28
|
-
self.metadata = Geometry::Point[config['metadata_width'] || 0, config['metadata_height'] || 0]
|
|
29
|
-
|
|
30
|
-
create_faces! # generates dimensions for each side
|
|
31
|
-
self.faces = [top, front, bottom, back, left, right]
|
|
32
|
-
|
|
33
|
-
self.conf = {
|
|
34
|
-
valign: [:out, :out, :out, :out, :in, :in],
|
|
35
|
-
halign: [:in, :out, :in, :out, :in, :in],
|
|
36
|
-
corners: {
|
|
37
|
-
top: [:yes, :no, :yes, :no, :no, :no], # 2nd choice, has to work if 1st doesn't
|
|
38
|
-
front: [:no, :yes, :no, :yes, :no, :no], # our default choice, but may not work
|
|
39
|
-
},
|
|
40
|
-
}
|
|
41
|
-
self
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
def generate_notches
|
|
45
|
-
position_faces!
|
|
46
|
-
self.corner_face = pick_corners_face
|
|
47
|
-
self.notches = []
|
|
48
|
-
faces.each_with_index do |face, face_index|
|
|
49
|
-
create_face_edges(face, face_index)
|
|
50
|
-
end
|
|
51
|
-
self.notches.flatten!
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
def enclosure
|
|
55
|
-
generate_notches if self.notches.empty?
|
|
56
|
-
p1 = notches.first.p1.to_a
|
|
57
|
-
p2 = notches.first.p2.to_a
|
|
58
|
-
|
|
59
|
-
notches.each do |notch|
|
|
60
|
-
n = notch.normalized
|
|
61
|
-
n.p1.to_a.each_with_index {|c, i| p1[i] = c if c < p1[i]}
|
|
62
|
-
n.p2.to_a.each_with_index {|c, i| p2[i] = c if c > p2[i]}
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
Geometry::Rect[Geometry::Point.new(p1), Geometry::Point.new(p2)]
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
def longest
|
|
69
|
-
[w, h, d].max
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
def to_s
|
|
73
|
-
"Box:\nH:#{dim.h} W:#{dim.w} D:#{dim.d}\nThickness:#{thickness}, Notch:#{notch_width}"
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
private
|
|
77
|
-
|
|
78
|
-
def create_face_edges(face, face_index)
|
|
79
|
-
bound = face_bounding_rect(face)
|
|
80
|
-
|
|
81
|
-
side_lines = []
|
|
82
|
-
edges = []
|
|
83
|
-
bound.sides.each_with_index do |bounding_side, side_index|
|
|
84
|
-
include_corners = (self.conf[:corners][corner_face][face_index] == :yes && side_index.odd?)
|
|
85
|
-
key = side_index.odd? ? :valign : :halign
|
|
86
|
-
center_out = (self.conf[key][face_index] == :out)
|
|
87
|
-
edges << Notching::Edge.new(bounding_side, face.sides[side_index],
|
|
88
|
-
{ :notch_width => notch_width,
|
|
89
|
-
:thickness => thickness,
|
|
90
|
-
:kerf => kerf,
|
|
91
|
-
:center_out => center_out,
|
|
92
|
-
:corners => include_corners
|
|
93
|
-
})
|
|
94
|
-
end
|
|
95
|
-
|
|
96
|
-
if edges.any? {|e| e.corners} && !edges.all? {|e| e.first_notch_out?}
|
|
97
|
-
edges.each {|e| e.adjust_corners = true}
|
|
98
|
-
end
|
|
99
|
-
|
|
100
|
-
edges.each do |edge|
|
|
101
|
-
side_lines << Notching::PathGenerator.new(edge).generate
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
aggregator = Aggregator.new(side_lines.flatten)
|
|
105
|
-
aggregator.dedup!.deoverlap!.dedup!
|
|
106
|
-
self.notches << aggregator.lines
|
|
107
|
-
end
|
|
108
|
-
|
|
109
|
-
def face_bounding_rect(face)
|
|
110
|
-
b = face.clone
|
|
111
|
-
b.move_to(b.position.plus(-thickness, -thickness))
|
|
112
|
-
b.p2 = b.p2.plus(2 * thickness, 2 * thickness)
|
|
113
|
-
b.relocate!
|
|
114
|
-
end
|
|
115
|
-
|
|
116
|
-
#___________________________________________________________________
|
|
117
|
-
#
|
|
118
|
-
# +-----------------+
|
|
119
|
-
# | |
|
|
120
|
-
# | back: W x H |
|
|
121
|
-
# | |
|
|
122
|
-
# +-----------------+
|
|
123
|
-
# +-----------------+
|
|
124
|
-
# | bottom: W x D |
|
|
125
|
-
# +-----------------+
|
|
126
|
-
# +--------+ +-----------------+ +--------+
|
|
127
|
-
# | | | | | |
|
|
128
|
-
# | left | | front: W x H | | right |
|
|
129
|
-
# | D x H | | | | D x H |
|
|
130
|
-
# +--------+ X-----------------+ +--------+
|
|
131
|
-
# +-----------------+
|
|
132
|
-
# | top : W x D |
|
|
133
|
-
# +-----------------+
|
|
134
|
-
#
|
|
135
|
-
# 0,0
|
|
136
|
-
#___________________________________________________________________
|
|
137
|
-
|
|
138
|
-
def position_faces!
|
|
139
|
-
offset_x = [padding + d + 3 * thickness, metadata.x + 2 * thickness + padding].max
|
|
140
|
-
offset_y = [padding + d + 3 * thickness, metadata.y + 2 * thickness + padding].max
|
|
141
|
-
|
|
142
|
-
# X Coordinate
|
|
143
|
-
left.x = offset_x - d - 2 * thickness - padding
|
|
144
|
-
right.x = offset_x + w + 2 * thickness + padding
|
|
145
|
-
|
|
146
|
-
[bottom, front, top, back].each {|s| s.x = offset_x}
|
|
147
|
-
|
|
148
|
-
# Y Coordinate
|
|
149
|
-
top.y = offset_y - d - 2 * thickness - padding
|
|
150
|
-
bottom.y = offset_y + h + 2 * thickness + padding
|
|
151
|
-
back.y = bottom.y + d + 2 * thickness + padding
|
|
152
|
-
|
|
153
|
-
[left, front, right].each {|s| s.y = offset_y}
|
|
154
|
-
|
|
155
|
-
faces.each(&:relocate!)
|
|
156
|
-
end
|
|
157
|
-
|
|
158
|
-
def create_faces!
|
|
159
|
-
zero = Geometry::Point.new(0, 0)
|
|
160
|
-
self.front = Geometry::Rect.create(zero, dim.w, dim.h, "front")
|
|
161
|
-
self.back = Geometry::Rect.create(zero, dim.w, dim.h, "back")
|
|
162
|
-
|
|
163
|
-
self.top = Geometry::Rect.create(zero, dim.w, dim.d, "top")
|
|
164
|
-
self.bottom = Geometry::Rect.create(zero, dim.w, dim.d, "bottom")
|
|
165
|
-
|
|
166
|
-
self.left = Geometry::Rect.create(zero, dim.d, dim.h, "left")
|
|
167
|
-
self.right = Geometry::Rect.create(zero, dim.d, dim.h, "right")
|
|
168
|
-
end
|
|
169
|
-
|
|
170
|
-
# Choose which face will be responsible for filling out the little square overlap
|
|
171
|
-
# in the corners. Only one of the 3 possible sides need to be picked.
|
|
172
|
-
def pick_corners_face
|
|
173
|
-
b = face_bounding_rect(front)
|
|
174
|
-
edges = []
|
|
175
|
-
front.sides[0..1].each_with_index do |face, index|
|
|
176
|
-
edges << Notching::Edge.new(b.sides[index], face, :notch_width => notch_width, :kerf => kerf)
|
|
177
|
-
end
|
|
178
|
-
edges.map(&:notch_count).all? {|c| c % 4 == 3} ? :top : :front
|
|
179
|
-
end
|
|
180
|
-
|
|
181
|
-
end
|
|
182
|
-
end
|
|
183
|
-
end
|
|
@@ -1,131 +0,0 @@
|
|
|
1
|
-
require 'optparse'
|
|
2
|
-
require 'colored'
|
|
3
|
-
require 'json'
|
|
4
|
-
require 'hashie/mash'
|
|
5
|
-
require 'laser-cutter'
|
|
6
|
-
require_relative 'serializer'
|
|
7
|
-
|
|
8
|
-
module Laser
|
|
9
|
-
module Cutter
|
|
10
|
-
module CLI
|
|
11
|
-
class OptParser
|
|
12
|
-
def self.puts_error(e)
|
|
13
|
-
STDERR.puts "Whoops, #{e}".red
|
|
14
|
-
STDERR.puts "Try --help or --examples for more info...".yellow
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
def self.parse(args)
|
|
18
|
-
banner_text = <<-EOF
|
|
19
|
-
#{('Laser-Cutter v'+ Laser::Cutter::VERSION).bold}
|
|
20
|
-
|
|
21
|
-
Usage: laser-cutter [options] -o filename.pdf
|
|
22
|
-
eg: laser-cutter -z 1x1.5x2/0.125 -O -o box.pdf
|
|
23
|
-
EOF
|
|
24
|
-
|
|
25
|
-
examples = <<-EOF
|
|
26
|
-
|
|
27
|
-
Examples:
|
|
28
|
-
1. Create a box defined in inches, set kerf to 0.008" and open PDF in preview right after:
|
|
29
|
-
|
|
30
|
-
laser-cutter -z 3x2x2/0.125 -k 0.008 -O -o box.pdf
|
|
31
|
-
|
|
32
|
-
2. Create a box defined in millimeters, print verbose info, and set
|
|
33
|
-
page size to A3, and layout to landscape, and stroke width to 1/2mm:
|
|
34
|
-
|
|
35
|
-
laser-cutter -u mm -w70 -h20 -d50 -t4.3 -n5 -iA3 -l landscape -s0.5 -v -O -o box.pdf
|
|
36
|
-
|
|
37
|
-
3. List all possible page sizes in metric systems:
|
|
38
|
-
|
|
39
|
-
laser-cutter -L -u mm
|
|
40
|
-
|
|
41
|
-
4. Create a box with provided dimensions, and save the config to a file
|
|
42
|
-
for later use:
|
|
43
|
-
|
|
44
|
-
laser-cutter -z 1.1x2.5x1.5/0.125/0.125 -p 0.1 -O -o box.pdf -W box-settings.json
|
|
45
|
-
|
|
46
|
-
5. Read settings from a previously saved file:
|
|
47
|
-
|
|
48
|
-
laser-cutter -O -o box.pdf -R box-settings.json
|
|
49
|
-
cat box-settings.json | laser-cutter -O -o box.pdf -R -
|
|
50
|
-
|
|
51
|
-
EOF
|
|
52
|
-
options = Hashie::Mash.new
|
|
53
|
-
options.verbose = false
|
|
54
|
-
options.units = 'in'
|
|
55
|
-
|
|
56
|
-
opt_parser = OptionParser.new do |opts|
|
|
57
|
-
opts.banner = banner_text.blue
|
|
58
|
-
opts.separator "Specific Options:"
|
|
59
|
-
opts.on("-w", "--width WIDTH", "Internal width of the box") { |value| options.width = value }
|
|
60
|
-
opts.on("-h", "--height HEIGHT", "Internal height of the box") { |value| options.height = value }
|
|
61
|
-
opts.on("-d", "--depth DEPTH", "Internal depth of the box") { |value| options.depth= value }
|
|
62
|
-
opts.on("-t", "--thickness THICKNESS", "Thickness of the box material") { |value| options.thickness = value }
|
|
63
|
-
opts.on("-n", "--notch NOTCH", "Optional notch length (aka \"tab width\"), guide only") { |value| options.notch = value }
|
|
64
|
-
opts.on("-k", "--kerf KERF", "Kerf - cut width (default is #{Laser::Cutter::Configuration::UNIT_SPECIFIC_DEFAULTS['in'][:kerf]}in)") { |value| options.kerf = value }
|
|
65
|
-
opts.separator ""
|
|
66
|
-
opts.on("-m", "--margin MARGIN", "Margins from the edge of the document") { |value| options.margin = value }
|
|
67
|
-
opts.on("-p", "--padding PADDING", "Space between the boxes on the page") { |value| options.padding = value }
|
|
68
|
-
opts.on("-s", "--stroke WIDTH", "Numeric stroke width of the line") { |value| options.stroke = value }
|
|
69
|
-
opts.on("-i", "--page_size LETTER", "Document page size, default is autofit the box.") { |value| options.page_size = value }
|
|
70
|
-
opts.on("-l", "--page_layout portrait", "Page layout, other option is 'landscape' ") { |value| options.page_layout = value }
|
|
71
|
-
opts.separator ""
|
|
72
|
-
opts.on("-O", "--open", "Open generated file with system viewer before exiting") { |v| options.open = v }
|
|
73
|
-
opts.on("-W", "--write CONFIG_FILE", "Save provided configuration to a file, use '-' for STDOUT") { |v| options.write_file = v }
|
|
74
|
-
opts.on("-R", "--read CONFIG_FILE", "Read configuration from a file, or use '-' for STDIN") { |v| options.read_file = v }
|
|
75
|
-
opts.separator ""
|
|
76
|
-
opts.on("-L", "--list-all-page-sizes", "Print all available page sizes with dimensions and exit") { |v| options.list_all_page_sizes = true }
|
|
77
|
-
opts.on("-M", "--no-metadata", "Do not print box metadata on the PDF") { |value| options.metadata = value }
|
|
78
|
-
opts.on("-v", "--[no-]verbose", "Run verbosely") { |v| options.verbose = v }
|
|
79
|
-
opts.on("-B", "--inside-box", "Draw the inside boxes (helpful to verify kerfing)") { |v| options.inside_box = v }
|
|
80
|
-
opts.on("-D", "--debug", "Show full exception stack trace on error") { |v| options.debug = v }
|
|
81
|
-
opts.separator ""
|
|
82
|
-
opts.on("--examples", "Show detailed usage examples") { puts opts; puts examples.yellow; exit }
|
|
83
|
-
opts.on("--help", "Show this message") { puts opts; exit }
|
|
84
|
-
opts.on("--version", "Show version") { puts Laser::Cutter::VERSION; exit }
|
|
85
|
-
opts.separator ""
|
|
86
|
-
opts.separator "Common Options:"
|
|
87
|
-
opts.on_tail("-o", "--file FILE", "Required output filename of the PDF") { |value| options.file = value }
|
|
88
|
-
opts.on_tail("-z", "--size WxHxD/T[/N]",
|
|
89
|
-
"Combined internal dimensions: W = width, H = height,\n#{" " * 37}D = depth, T = thickness, and optional N = notch length\n\n") do |size|
|
|
90
|
-
options.size = size
|
|
91
|
-
end
|
|
92
|
-
opts.on_tail("-u", "--units UNITS", "Either 'in' for inches (default) or 'mm'") { |value| options.units = value }
|
|
93
|
-
end
|
|
94
|
-
|
|
95
|
-
opt_parser.parse!(args)
|
|
96
|
-
|
|
97
|
-
if options.read_file
|
|
98
|
-
# these options are kept from the command line
|
|
99
|
-
override_with = %w(debug verbose read_file)
|
|
100
|
-
keep = options.reject{ |k,v| !override_with.include?(k)}
|
|
101
|
-
Serializer.new(options).deserialize
|
|
102
|
-
options.merge!(keep)
|
|
103
|
-
end
|
|
104
|
-
|
|
105
|
-
config = Laser::Cutter::Configuration.new(options.to_hash)
|
|
106
|
-
if config.list_all_page_sizes
|
|
107
|
-
puts PageManager.new(config.units).all_page_sizes
|
|
108
|
-
exit 0
|
|
109
|
-
end
|
|
110
|
-
|
|
111
|
-
if options.verbose
|
|
112
|
-
puts "Starting with the following configuration:"
|
|
113
|
-
puts JSON.pretty_generate(config.to_hash).green
|
|
114
|
-
end
|
|
115
|
-
|
|
116
|
-
config.validate!
|
|
117
|
-
|
|
118
|
-
if config.write_file
|
|
119
|
-
Serializer.new(config).serialize
|
|
120
|
-
end
|
|
121
|
-
|
|
122
|
-
config
|
|
123
|
-
rescue OptionParser::InvalidOption, OptionParser::MissingArgument, Laser::Cutter::MissingOption => e
|
|
124
|
-
puts opt_parser.banner.blue
|
|
125
|
-
puts_error(e)
|
|
126
|
-
exit 1
|
|
127
|
-
end
|
|
128
|
-
end
|
|
129
|
-
end
|
|
130
|
-
end
|
|
131
|
-
end
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
require 'json'
|
|
2
|
-
require 'laser-cutter'
|
|
3
|
-
|
|
4
|
-
module Laser
|
|
5
|
-
module Cutter
|
|
6
|
-
module CLI
|
|
7
|
-
class Serializer
|
|
8
|
-
attr_accessor :options
|
|
9
|
-
def initialize(options = {})
|
|
10
|
-
self.options = options
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
def deserialize
|
|
14
|
-
string = if options.read_file.eql?('-')
|
|
15
|
-
$stdin.read
|
|
16
|
-
elsif File.exist?(options.read_file)
|
|
17
|
-
File.read(options.read_file)
|
|
18
|
-
end
|
|
19
|
-
if string
|
|
20
|
-
options.replace(JSON.load(string))
|
|
21
|
-
end
|
|
22
|
-
rescue Exception => e
|
|
23
|
-
STDERR.puts "Error reading options from file #{options.read_file}, #{e.message}".red
|
|
24
|
-
if options.verbose
|
|
25
|
-
STDERR.puts e.backtrace.join("\n").red
|
|
26
|
-
end
|
|
27
|
-
exit 1
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
def serialize
|
|
31
|
-
output = if options.write_file.eql?('-')
|
|
32
|
-
$stdout
|
|
33
|
-
elsif options.write_file
|
|
34
|
-
File.open(options.write_file, 'w')
|
|
35
|
-
else
|
|
36
|
-
nil
|
|
37
|
-
end
|
|
38
|
-
output.puts(JSON.pretty_generate(options))
|
|
39
|
-
output.close if output != $stdout
|
|
40
|
-
rescue Exception => e
|
|
41
|
-
STDERR.puts "Error writing options to file #{options.write_file}, #{e.message}".red
|
|
42
|
-
if options.verbose
|
|
43
|
-
STDERR.puts e.backtrace.join("\n").red
|
|
44
|
-
end
|
|
45
|
-
exit 1
|
|
46
|
-
end
|
|
47
|
-
end
|
|
48
|
-
end
|
|
49
|
-
end
|
|
50
|
-
end
|
|
51
|
-
|
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
require 'hashie/mash'
|
|
2
|
-
require 'prawn/measurement_extensions'
|
|
3
|
-
require 'pdf/core/page_geometry'
|
|
4
|
-
|
|
5
|
-
module Laser
|
|
6
|
-
module Cutter
|
|
7
|
-
class MissingOption < RuntimeError; end
|
|
8
|
-
class ZeroValueNotAllowed < MissingOption; end
|
|
9
|
-
|
|
10
|
-
class UnitsConverter
|
|
11
|
-
def self.mm2in value
|
|
12
|
-
0.039370079 * value
|
|
13
|
-
end
|
|
14
|
-
def self.in2mm value
|
|
15
|
-
25.4 * value
|
|
16
|
-
end
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
class Configuration < Hashie::Mash
|
|
20
|
-
DEFAULTS = {
|
|
21
|
-
units: 'in',
|
|
22
|
-
page_layout: 'portrait',
|
|
23
|
-
metadata: true
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
UNIT_SPECIFIC_DEFAULTS = {
|
|
27
|
-
'in' => {
|
|
28
|
-
kerf: 0.0024, # smallest kerf for thin material, usually it's more than that.
|
|
29
|
-
margin: 0.125,
|
|
30
|
-
padding: 0.1,
|
|
31
|
-
stroke: 0.001,
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
UNIT_SPECIFIC_DEFAULTS['mm'] = UNIT_SPECIFIC_DEFAULTS['in'].map{|k, v| [k, UnitsConverter.in2mm(v)] }.to_h
|
|
36
|
-
|
|
37
|
-
SIZE_REGEXP = /[\d\.]+x[\d\.]+x[\d\.]+\/[\d\.]+(\/[\d\.]+)?/
|
|
38
|
-
|
|
39
|
-
FLOATS = %w(width height depth thickness notch margin padding stroke kerf)
|
|
40
|
-
NON_ZERO = %w(width height depth thickness stroke)
|
|
41
|
-
REQUIRED = %w(width height depth thickness notch file)
|
|
42
|
-
|
|
43
|
-
def initialize(options = {})
|
|
44
|
-
options.delete_if { |k, v| v.nil? }
|
|
45
|
-
if options['units'] && !UNIT_SPECIFIC_DEFAULTS.keys.include?(options['units'])
|
|
46
|
-
options.delete('units')
|
|
47
|
-
end
|
|
48
|
-
self.merge!(DEFAULTS)
|
|
49
|
-
self.merge!(options)
|
|
50
|
-
if self['size'] && self['size'] =~ SIZE_REGEXP
|
|
51
|
-
dim, self['thickness'], self['notch'] = self['size'].split('/')
|
|
52
|
-
self['width'], self['height'], self['depth'] = dim.split('x')
|
|
53
|
-
delete('size')
|
|
54
|
-
end
|
|
55
|
-
FLOATS.each do |k|
|
|
56
|
-
self[k] = self[k].to_f if (self[k] && self[k].is_a?(String))
|
|
57
|
-
end
|
|
58
|
-
self.merge!(UNIT_SPECIFIC_DEFAULTS[self['units']].merge(self))
|
|
59
|
-
self['notch'] = (self['thickness'] * 3.0).round(5) if self['thickness'] && self['notch'].nil?
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
def validate!
|
|
63
|
-
missing = []
|
|
64
|
-
REQUIRED.each { |k| missing << k if self[k].nil? }
|
|
65
|
-
raise MissingOption.new("#{missing.join(', ')} #{missing.size > 1 ? 'are' : 'is'} required, but missing.") unless missing.empty?
|
|
66
|
-
|
|
67
|
-
zeros = []
|
|
68
|
-
NON_ZERO.each { |k| zeros << k if self[k] == 0 }
|
|
69
|
-
raise ZeroValueNotAllowed.new("#{zeros.join(', ')} #{zeros.size > 1 ? 'are' : 'is'} required, but is zero.") unless zeros.empty?
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
def change_units(new_units)
|
|
73
|
-
return if (self.units.eql?(new_units) || !UNIT_SPECIFIC_DEFAULTS.keys.include?(new_units))
|
|
74
|
-
k = (self.units == 'in') ? UnitsConverter.in2mm(1.0) : UnitsConverter.mm2in(1.0)
|
|
75
|
-
FLOATS.each do |field|
|
|
76
|
-
next if self.send(field.to_sym).nil?
|
|
77
|
-
self.send("#{field}=".to_sym, (self.send(field.to_sym) * k).round(5))
|
|
78
|
-
end
|
|
79
|
-
self.units = new_units
|
|
80
|
-
end
|
|
81
|
-
end
|
|
82
|
-
end
|
|
83
|
-
end
|
data/lib/laser-cutter/face.rb
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
require 'forwardable'
|
|
2
|
-
module Laser
|
|
3
|
-
module Cutter
|
|
4
|
-
# Note: this class badly needs refactoring and tests. Both are coming.
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
class Face
|
|
8
|
-
attr_accessor :edges, :rect
|
|
9
|
-
extend Forwardable
|
|
10
|
-
def_delegators :@rect, *((Geometry::Rect.new(Geometry::Point.new(0,0), Geometry::Point.new(1,1))).methods - Object.methods)
|
|
11
|
-
def initialize(rect, edges = [])
|
|
12
|
-
self.rect = rect
|
|
13
|
-
self.edges = edges
|
|
14
|
-
end
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
end
|
|
18
|
-
end
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
require 'hashie/mash'
|
|
2
|
-
require 'prawn/measurement_extensions'
|
|
3
|
-
require 'pdf/core/page_geometry'
|
|
4
|
-
|
|
5
|
-
class Laser::Cutter::PageManager < Struct.new(:units)
|
|
6
|
-
SIZES = PDF::Core::PageGeometry::SIZES.clone.freeze
|
|
7
|
-
|
|
8
|
-
def all_page_sizes
|
|
9
|
-
output = ""
|
|
10
|
-
page_size_values.each do |k|
|
|
11
|
-
output << sprintf("\t%10s:\t%6.1f x %6.1f\n", *k)
|
|
12
|
-
end
|
|
13
|
-
output
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
# if from_units is nil, we expect it to be in dots per inch (default
|
|
17
|
-
# measurements for Prawn
|
|
18
|
-
def value_from_units value, from_units = nil
|
|
19
|
-
multiplier = if from_units.nil?
|
|
20
|
-
if units.eql?('in')
|
|
21
|
-
1.0 / 72.0 # PDF units per inch
|
|
22
|
-
else
|
|
23
|
-
25.4 * 1.0 / 72.0
|
|
24
|
-
end
|
|
25
|
-
elsif self.units.eql?(from_units)
|
|
26
|
-
1.0
|
|
27
|
-
elsif self.units.eql?('in') && from_units.eql?('mm')
|
|
28
|
-
(1.0 / 25.4)
|
|
29
|
-
else
|
|
30
|
-
25.4
|
|
31
|
-
end
|
|
32
|
-
value.to_f * multiplier
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
def page_size_values
|
|
36
|
-
h = SIZES
|
|
37
|
-
array = []
|
|
38
|
-
h.keys.sort.each do |k|
|
|
39
|
-
array << [k, value_from_units(h[k][0].to_f), value_from_units(h[k][1].to_f)]
|
|
40
|
-
end
|
|
41
|
-
array
|
|
42
|
-
end
|
|
43
|
-
end
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
# encoding: utf-8
|
|
2
|
-
require 'json'
|
|
3
|
-
module Laser
|
|
4
|
-
module Cutter
|
|
5
|
-
module Renderer
|
|
6
|
-
class BoxRenderer < Base
|
|
7
|
-
alias_method :box, :subject
|
|
8
|
-
|
|
9
|
-
def initialize(config)
|
|
10
|
-
super(config)
|
|
11
|
-
self.subject = Laser::Cutter::Box.new(config)
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
def ensure_space_for(rect)
|
|
15
|
-
coords = [ rect.p2.x, rect.p2.y ].map{|a| page_manager.value_from_units(a)}
|
|
16
|
-
box.metadata = Geometry::Point.new(coords)
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
def enclosure
|
|
20
|
-
box.enclosure
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
def render pdf = nil
|
|
24
|
-
renderer = self
|
|
25
|
-
pdf.instance_eval do
|
|
26
|
-
self.line_width = renderer.config.stroke.send(renderer.config.units.to_sym)
|
|
27
|
-
stroke_color renderer.config[:color] || BLACK
|
|
28
|
-
renderer.box.generate_notches.each do |notch|
|
|
29
|
-
LineRenderer.new(renderer.config, notch).render(self)
|
|
30
|
-
end
|
|
31
|
-
end
|
|
32
|
-
end
|
|
33
|
-
end
|
|
34
|
-
end
|
|
35
|
-
end
|
|
36
|
-
end
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
# encoding: utf-8
|
|
2
|
-
require 'json'
|
|
3
|
-
module Laser
|
|
4
|
-
module Cutter
|
|
5
|
-
module Renderer
|
|
6
|
-
class LayoutRenderer < Base
|
|
7
|
-
def initialize(config)
|
|
8
|
-
self.config = config
|
|
9
|
-
super(config)
|
|
10
|
-
end
|
|
11
|
-
|
|
12
|
-
def render
|
|
13
|
-
renderer = self
|
|
14
|
-
renderers = []
|
|
15
|
-
|
|
16
|
-
box_renderer = BoxRenderer.new(config)
|
|
17
|
-
renderers << box_renderer
|
|
18
|
-
|
|
19
|
-
if config.metadata
|
|
20
|
-
meta_renderer = MetaRenderer.new(config)
|
|
21
|
-
renderers << meta_renderer
|
|
22
|
-
box_renderer.ensure_space_for(meta_renderer.enclosure)
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
if config.debug
|
|
26
|
-
unkerfed_config = Laser::Cutter::Configuration.new(config.to_hash)
|
|
27
|
-
unkerfed_config.merge!(kerf: 0.0, color: 'DD2211')
|
|
28
|
-
unkerfed_box_renderer = BoxRenderer.new(unkerfed_config)
|
|
29
|
-
unkerfed_box_renderer.ensure_space_for(meta_renderer.enclosure) if meta_renderer
|
|
30
|
-
renderers << unkerfed_box_renderer
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
margin = config.margin.to_f.send(config.units.to_sym)
|
|
34
|
-
page_size = config.page_size || calculate_image_boundary(box_renderer, margin)
|
|
35
|
-
|
|
36
|
-
pdf = Prawn::Document.new(:margin => margin,
|
|
37
|
-
:page_size => page_size,
|
|
38
|
-
:page_layout => self.config.page_layout.to_sym)
|
|
39
|
-
|
|
40
|
-
pdf.instance_eval do
|
|
41
|
-
renderers.each {|r| r.render(self) }
|
|
42
|
-
render_file(renderer.config.file)
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
if config.verbose
|
|
46
|
-
puts "PDF saved to #{config.file}."
|
|
47
|
-
end
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
def calculate_image_boundary(box_renderer, margin)
|
|
51
|
-
box_renderer.enclosure.to_a[1].map do |c|
|
|
52
|
-
c.send(config.units.to_sym) + 2 * margin
|
|
53
|
-
end
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
end
|
|
57
|
-
end
|
|
58
|
-
end
|
|
59
|
-
end
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
module Laser
|
|
2
|
-
module Cutter
|
|
3
|
-
module Renderer
|
|
4
|
-
class LineRenderer < Base
|
|
5
|
-
alias_method :line, :subject
|
|
6
|
-
def render pdf = nil
|
|
7
|
-
pdf.stroke { pdf.line [line.p1.x, line.p1.y].map{ |p| p.send(units) },
|
|
8
|
-
[line.p2.x, line.p2.y].map{ |p| p.send(units) }}
|
|
9
|
-
end
|
|
10
|
-
|
|
11
|
-
def enclosure
|
|
12
|
-
self.line
|
|
13
|
-
end
|
|
14
|
-
end
|
|
15
|
-
end
|
|
16
|
-
end
|
|
17
|
-
end
|