laser-cutter 0.5.1 → 0.5.3
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/README.md +32 -29
- data/bin/laser-cutter +5 -5
- data/lib/laser-cutter/box.rb +24 -6
- data/lib/laser-cutter/configuration.rb +5 -4
- data/lib/laser-cutter/geometry/path_generator.rb +1 -4
- data/lib/laser-cutter/geometry/shape/rect.rb +3 -1
- data/lib/laser-cutter/geometry/tuple.rb +1 -1
- data/lib/laser-cutter/renderer/base.rb +3 -0
- data/lib/laser-cutter/renderer/box_renderer.rb +3 -0
- data/lib/laser-cutter/renderer/layout_renderer.rb +10 -2
- data/lib/laser-cutter/renderer/meta_renderer.rb +2 -1
- data/lib/laser-cutter/version.rb +1 -1
- data/spec/box_spec.rb +6 -1
- data/spec/configuration_spec.rb +10 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA1:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 869598bd65fbb14370dcce5b969a19748f246ce2
|
|
4
|
+
data.tar.gz: 4ce9cc14002201a598b5df25f1ae1a474b4269f6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c3b2164cedb8d7af10dfc1beb1482d902f5ce56ae916d32f35cdc289d42d87943567a0d8729f3e793d2844a40c99b27428b5a942b3ddb0b57e4055cc3a7e0266
|
|
7
|
+
data.tar.gz: 9983e63d47f490f0354159f0ba96ddb086d30c2dc44b62e76f0541529bd1b546693b01100366ac7b0f68c44359b6431c873a4b4ec7fa5a0821dfddc2965edc85
|
data/README.md
CHANGED
|
@@ -3,14 +3,23 @@
|
|
|
3
3
|
[](https://codeclimate.com/github/kigster/laser-cutter)
|
|
4
4
|
[](https://codeclimate.com/github/kigster/laser-cutter)
|
|
5
5
|
|
|
6
|
-
## LaserCutter
|
|
6
|
+
## LaserCutter and Make-A-Box.io
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
```laser-cutter``` is a ruby library for generating PDF designs for boxes of
|
|
9
|
+
custom dimensions that suit your project, that can be cut from wood or acrylic
|
|
10
|
+
using a laser-cutter. The sides of the box snap together using alternating notches,
|
|
11
|
+
that are deliberately layed out in a symmetric form.
|
|
11
12
|
|
|
12
|
-
|
|
13
|
-
|
|
13
|
+
To use ```laser-cutter``` you need to have a recent version of ruby interpreter,
|
|
14
|
+
install it as a gem, and use command line to generate PDFs.
|
|
15
|
+
|
|
16
|
+
[Make-A-Box](http://makeabox.io) is a online web application that uses ```laser-cutter``` library
|
|
17
|
+
and provides a straight-forward user interface for generating PDF designs without the need to install
|
|
18
|
+
the gem or use command line.
|
|
19
|
+
|
|
20
|
+
Use whatever suites you better.
|
|
21
|
+
|
|
22
|
+
### Design Goals
|
|
14
23
|
|
|
15
24
|
One of the design goals of this project is to provide a highly extensible platform for creating
|
|
16
25
|
laser-cut designs, where alternative strategies can be added over time, and supported by various
|
|
@@ -20,13 +29,6 @@ contributing to the project, please see [contributing](CONTRIBUTING.md) for more
|
|
|
20
29
|
```laser-cutter``` supports many flexible command line options that allow setting dimensions,
|
|
21
30
|
stroke width, page size, layout, margins, padding (spacing between the boxes), and many more.
|
|
22
31
|
|
|
23
|
-
## Web Front-End
|
|
24
|
-
|
|
25
|
-
There is a web online application that uses this gem and allows you to generate PDFs with
|
|
26
|
-
a friendly UI.
|
|
27
|
-
|
|
28
|
-
Please visit [http://makeabox.io](http://makeabox.io).
|
|
29
|
-
|
|
30
32
|
## Dependencies
|
|
31
33
|
|
|
32
34
|
The gem depends primarily on [Prawn](http://prawnpdf.org) – a fantastic PDF generation library.
|
|
@@ -118,31 +120,32 @@ Read settings from a previously saved file:
|
|
|
118
120
|
cat box-settings.json | laser-cutter -O -o box.pdf -R -
|
|
119
121
|
```
|
|
120
122
|
|
|
121
|
-
##
|
|
123
|
+
## Feature Wish List
|
|
122
124
|
|
|
125
|
+
* Create T-style joins, using various standard sizes of nuts and bolts (such as common #4-40 and M2 sizes)
|
|
123
126
|
* Extensibility with various layout strategies, notch drawing strategies, basically plug and play
|
|
124
127
|
model for adding new algorithms for path creation and box joining
|
|
125
|
-
* Support more shapes than just box
|
|
126
|
-
* Create T-style joins, using various standard sizes of nuts and bolts (such as common #4-40 and M2 sizes)
|
|
128
|
+
* Support more shapes than just box, such as prisms
|
|
127
129
|
* Supporting lids and front panels, that are larger than the box itself and have holes for notches.
|
|
128
130
|
* Your brilliant idea can be here too! Please see [contributing](CONTRIBUTING.md) for more info.
|
|
129
131
|
|
|
130
|
-
##
|
|
132
|
+
## LaserCutter vs BoxMaker
|
|
131
133
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
134
|
+
[Rahulbot](https://github.com/rahulbot/)-made [BoxMaker](https://github.com/rahulbot/boxmaker/) is a
|
|
135
|
+
functional generator of notched designs, similar to ```laser-cutter```, and generously open sourced
|
|
136
|
+
by the author, and so in no way this project disputes BoxMaker's viability. In fact BoxMaker was an
|
|
137
|
+
inspiration for this project.
|
|
138
|
+
|
|
139
|
+
Laser-Cutter library attempts to further advance the concept of programmatically creating
|
|
140
|
+
laser-cut box designs, provides additional fine tuning, many more options, strategies and most
|
|
141
|
+
importantly – extensibility.
|
|
138
142
|
|
|
139
|
-
Unlike ```BoxMaker```, this gem has a suit of automated tests (rspecs) around
|
|
140
|
-
In addition,
|
|
141
|
-
regard
|
|
143
|
+
Unlike ```BoxMaker```, this gem has a suit of automated tests (rspecs) around the core functionality.
|
|
144
|
+
In addition, new feature contributions are highly encouraged, and in that
|
|
145
|
+
regard having existing test suit offers confidence against regressions, and thus welcomes colaboration.
|
|
142
146
|
|
|
143
|
-
BoxMaker's algorithm
|
|
144
|
-
|
|
145
|
-
And that might be entirely OK with you :)
|
|
147
|
+
Finally, BoxMaker's notch-drawing algorithm generates non-symmetric and sometimes purely broken designs
|
|
148
|
+
(see picture below).
|
|
146
149
|
|
|
147
150
|
```laser-cutter```'s algorithm will create a _symmetric design for most panels_, but it might sacrifice
|
|
148
151
|
identical notch length. Depending on the box dimensions you may end up with a slightly different notch
|
data/bin/laser-cutter
CHANGED
|
@@ -18,7 +18,7 @@ module Laser
|
|
|
18
18
|
#{('Laser-Cutter v'+ Laser::Cutter::VERSION).bold}
|
|
19
19
|
|
|
20
20
|
Usage: laser-cutter [options] -o filename.pdf
|
|
21
|
-
eg: laser-cutter -s 1x1.5x2/0.125
|
|
21
|
+
eg: laser-cutter -s 1x1.5x2/0.125 -O -o box.pdf
|
|
22
22
|
EOF
|
|
23
23
|
|
|
24
24
|
examples = <<-EOF
|
|
@@ -26,7 +26,7 @@ Usage: laser-cutter [options] -o filename.pdf
|
|
|
26
26
|
Examples:
|
|
27
27
|
1. Create a box defined in inches, and open PDF in preview right after:
|
|
28
28
|
|
|
29
|
-
laser-cutter -s 3x2x2/0.125
|
|
29
|
+
laser-cutter -s 3x2x2/0.125 -O -o box.pdf
|
|
30
30
|
|
|
31
31
|
2. Create a box defined in millimeters, print verbose info, and set
|
|
32
32
|
page size to A3, and layout to landscape, and stroke width to 1/2mm:
|
|
@@ -59,7 +59,7 @@ Examples:
|
|
|
59
59
|
opts.on("-h", "--height HEIGHT", "Internal height of the box") { |value| options.height = value }
|
|
60
60
|
opts.on("-d", "--depth DEPTH", "Internal depth of the box") { |value| options.depth= value }
|
|
61
61
|
opts.on("-t", "--thickness THICKNESS", "Thickness of the box material") { |value| options.thickness = value }
|
|
62
|
-
opts.on("-n", "--notch NOTCH", "
|
|
62
|
+
opts.on("-n", "--notch NOTCH", "Optional notch length (used as a guide)") { |value| options.notch = value }
|
|
63
63
|
opts.separator ""
|
|
64
64
|
opts.on("-m", "--margin MARGIN", "Margins from the edge of the document") { |value| options.margin = value }
|
|
65
65
|
opts.on("-p", "--padding PADDING", "Space between the boxes on the page") { |value| options.padding = value }
|
|
@@ -81,8 +81,8 @@ Examples:
|
|
|
81
81
|
opts.separator ""
|
|
82
82
|
opts.separator "Common Options:"
|
|
83
83
|
opts.on_tail("-o", "--file FILE", "Required output filename of the PDF") { |value| options.file = value }
|
|
84
|
-
opts.on_tail("-s", "--size WxHxD/T/N",
|
|
85
|
-
"Combined internal dimensions: W = width, H = height,\n#{" " * 37}D = depth, T = thickness, N = notch length\n\n") do |size|
|
|
84
|
+
opts.on_tail("-s", "--size WxHxD/T[/N]",
|
|
85
|
+
"Combined internal dimensions: W = width, H = height,\n#{" " * 37}D = depth, T = thickness, and optional N = notch length\n\n") do |size|
|
|
86
86
|
options.size = size
|
|
87
87
|
end
|
|
88
88
|
opts.on_tail("-u", "--units UNITS", "Either 'in' for inches (default) or 'mm'") { |value| options.units = value }
|
data/lib/laser-cutter/box.rb
CHANGED
|
@@ -10,7 +10,7 @@ module Laser
|
|
|
10
10
|
|
|
11
11
|
attr_accessor :front, :back, :top, :bottom, :left, :right
|
|
12
12
|
attr_accessor :faces, :bounds, :conf, :corner_face
|
|
13
|
-
attr_accessor :metadata
|
|
13
|
+
attr_accessor :metadata, :notches
|
|
14
14
|
|
|
15
15
|
def initialize(config = {})
|
|
16
16
|
self.dim = Geometry::Dimensions.new(config['width'], config['height'], config['depth'])
|
|
@@ -20,6 +20,8 @@ module Laser
|
|
|
20
20
|
self.padding = config['padding']
|
|
21
21
|
self.units = config['units']
|
|
22
22
|
|
|
23
|
+
@notches = []
|
|
24
|
+
|
|
23
25
|
self.metadata = Geometry::Point[config['metadata_width'] || 0, config['metadata_height'] || 0]
|
|
24
26
|
|
|
25
27
|
create_faces! # generates dimensions for each side
|
|
@@ -36,15 +38,31 @@ module Laser
|
|
|
36
38
|
self
|
|
37
39
|
end
|
|
38
40
|
|
|
41
|
+
def enclosure
|
|
42
|
+
p1 = notches.first.p1.to_a
|
|
43
|
+
p2 = notches.first.p2.to_a
|
|
44
|
+
|
|
45
|
+
notches.each do |notch|
|
|
46
|
+
n = notch.normalized
|
|
47
|
+
n.p1.to_a.each_with_index {|c, i| p1[i] = c if c < p1[i] }
|
|
48
|
+
n.p2.to_a.each_with_index {|c, i| p2[i] = c if c > p2[i] }
|
|
49
|
+
end
|
|
39
50
|
|
|
40
|
-
|
|
41
|
-
|
|
51
|
+
Geometry::Rect[Geometry::Point.new(p1), Geometry::Point.new(p2)]
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Returns an flattened array of lines representing notched lines
|
|
42
55
|
def notches
|
|
56
|
+
generate_notches! if @notches.empty?
|
|
57
|
+
@notches
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def generate_notches!
|
|
43
61
|
position_faces!
|
|
44
62
|
|
|
45
63
|
corner_face = pick_corners_face
|
|
46
64
|
|
|
47
|
-
notches = []
|
|
65
|
+
@notches = []
|
|
48
66
|
faces.each_with_index do |face, face_index|
|
|
49
67
|
bound = face_bounding_rect(face)
|
|
50
68
|
bound.sides.each_with_index do |bounding_side, side_index |
|
|
@@ -54,11 +72,11 @@ module Laser
|
|
|
54
72
|
:fill_corners => (self.conf[:corners][corner_face][face_index] == :yes && side_index.odd?),
|
|
55
73
|
:thickness => thickness
|
|
56
74
|
).path(Geometry::Edge.new(bounding_side, face.sides[side_index], self.notch_width))
|
|
57
|
-
notches << path.create_lines
|
|
75
|
+
@notches << path.create_lines
|
|
58
76
|
end
|
|
59
77
|
end
|
|
60
78
|
|
|
61
|
-
Geometry::PathGenerator.deduplicate(notches.flatten.sort)
|
|
79
|
+
@notches = Geometry::PathGenerator.deduplicate(@notches.flatten.sort)
|
|
62
80
|
end
|
|
63
81
|
|
|
64
82
|
def w; dim.w; end
|
|
@@ -17,8 +17,8 @@ module Laser
|
|
|
17
17
|
|
|
18
18
|
UNIT_SPECIFIC_DEFAULTS = {
|
|
19
19
|
'mm' => {
|
|
20
|
-
margin:
|
|
21
|
-
padding:
|
|
20
|
+
margin: 3,
|
|
21
|
+
padding: 2,
|
|
22
22
|
stroke: 0.0254,
|
|
23
23
|
},
|
|
24
24
|
'in' => {
|
|
@@ -28,8 +28,7 @@ module Laser
|
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
|
|
32
|
-
SIZE_REGEXP = /[\d\.]+x[\d\.]+x[\d\.]+\/[\d\.]+\/[\d\.]+/
|
|
31
|
+
SIZE_REGEXP = /[\d\.]+x[\d\.]+x[\d\.]+\/[\d\.]+(\/[\d\.]+)?/
|
|
33
32
|
|
|
34
33
|
FLOATS = %w(width height depth thickness notch margin padding stroke)
|
|
35
34
|
NON_ZERO = %w(width height depth thickness stroke)
|
|
@@ -51,6 +50,7 @@ module Laser
|
|
|
51
50
|
self[k] = self[k].to_f if (self[k] && self[k].is_a?(String))
|
|
52
51
|
end
|
|
53
52
|
self.merge!(UNIT_SPECIFIC_DEFAULTS[self['units']].merge(self))
|
|
53
|
+
self['notch'] = self['thickness'] * 3.0 if self['thickness'] && self['notch'].nil?
|
|
54
54
|
end
|
|
55
55
|
|
|
56
56
|
def validate!
|
|
@@ -67,6 +67,7 @@ module Laser
|
|
|
67
67
|
return if (self.units.eql?(new_units) || !UNIT_SPECIFIC_DEFAULTS.keys.include?(new_units))
|
|
68
68
|
k = (self.units == 'in') ? 25.4 : 0.039370079
|
|
69
69
|
FLOATS.each do |field|
|
|
70
|
+
next if self.send(field.to_sym).nil?
|
|
70
71
|
self.send("#{field}=".to_sym, (self.send(field.to_sym) * k).round(5))
|
|
71
72
|
end
|
|
72
73
|
self.units = new_units
|
|
@@ -34,7 +34,7 @@ module Laser
|
|
|
34
34
|
# Removes the items from the list that appear more than once
|
|
35
35
|
# Unlike uniq-ing which keeps all elements, just ensures that are not
|
|
36
36
|
# repeated, here we remove elements completely if they are seen more than once.
|
|
37
|
-
# This is used to remove lines
|
|
37
|
+
# This is used to remove lines that join the same two points.
|
|
38
38
|
def self.deduplicate list
|
|
39
39
|
new_list = []
|
|
40
40
|
list.sort!
|
|
@@ -53,9 +53,6 @@ module Laser
|
|
|
53
53
|
@notch_width = options[:notch_width] # only desired, will be adapted for each line
|
|
54
54
|
@center_out = options[:center_out] # when true, the notch in the middle of the edge is out, not in.
|
|
55
55
|
@thickness = options[:thickness]
|
|
56
|
-
|
|
57
|
-
# 2D array of booleans. If true first or second end of the edge is
|
|
58
|
-
# created with a corner filled in.
|
|
59
56
|
@fill_corners = options[:fill_corners]
|
|
60
57
|
end
|
|
61
58
|
|
|
@@ -6,18 +6,20 @@ module Laser
|
|
|
6
6
|
class LayoutRenderer < Base
|
|
7
7
|
def initialize(config)
|
|
8
8
|
self.config = config
|
|
9
|
+
super(config)
|
|
9
10
|
end
|
|
10
11
|
|
|
11
12
|
def render
|
|
12
13
|
renderer = self
|
|
13
14
|
|
|
15
|
+
margin = config.margin.to_f.send(config.units.to_sym)
|
|
14
16
|
meta_renderer = MetaRenderer.new(config)
|
|
15
17
|
box_renderer = BoxRenderer.new(config)
|
|
16
18
|
box_renderer.ensure_space_for(meta_renderer.enclosure) if config.metadata
|
|
17
19
|
|
|
18
|
-
page_size = config.page_size ||
|
|
20
|
+
page_size = config.page_size || calculate_image_boundary(box_renderer, margin)
|
|
19
21
|
|
|
20
|
-
pdf = Prawn::Document.new(:margin =>
|
|
22
|
+
pdf = Prawn::Document.new(:margin => margin,
|
|
21
23
|
:page_size => page_size,
|
|
22
24
|
:page_layout => config.page_layout.to_sym)
|
|
23
25
|
|
|
@@ -34,6 +36,12 @@ module Laser
|
|
|
34
36
|
end
|
|
35
37
|
end
|
|
36
38
|
|
|
39
|
+
def calculate_image_boundary(box_renderer, margin)
|
|
40
|
+
box_renderer.enclosure.to_a[1].map do |c|
|
|
41
|
+
c.send(config.units.to_sym) + 2 * margin
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
37
45
|
end
|
|
38
46
|
end
|
|
39
47
|
end
|
|
@@ -23,11 +23,12 @@ class Laser::Cutter::Renderer::MetaRenderer < Laser::Cutter::Renderer::Base
|
|
|
23
23
|
|
|
24
24
|
metadata = config.to_hash
|
|
25
25
|
metadata.delete_if { |k| %w(verbose metadata open file).include?(k) }
|
|
26
|
+
metadata['page_size'] ||= 'custom'
|
|
26
27
|
|
|
27
28
|
rect = self.enclosure
|
|
28
29
|
|
|
29
30
|
pdf.instance_eval do
|
|
30
|
-
self.line_width = 0.
|
|
31
|
+
self.line_width = 0.2.mm
|
|
31
32
|
float do
|
|
32
33
|
bounding_box([rect.p1.x, rect.h + rect.p1.y], :width => rect.w, :height => rect.h) do
|
|
33
34
|
stroke_color meta_color
|
data/lib/laser-cutter/version.rb
CHANGED
data/spec/box_spec.rb
CHANGED
|
@@ -3,7 +3,7 @@ require_relative 'spec_helper'
|
|
|
3
3
|
module Laser
|
|
4
4
|
module Cutter
|
|
5
5
|
describe Box do
|
|
6
|
-
let(:config) { {'width' => 50, 'height' => 60, 'depth' => 70, 'margin' => 5, 'padding' => 3 } }
|
|
6
|
+
let(:config) { {'width' => 50, 'height' => 60, 'depth' => 70, 'margin' => 5, 'padding' => 3, 'units' => 'mm'} }
|
|
7
7
|
let(:box1) { Box.new(config.merge('thickness' => 6, 'notch' => 10)) }
|
|
8
8
|
let(:box2) { Box.new(config.merge('thickness' => 6, )) }
|
|
9
9
|
|
|
@@ -24,6 +24,11 @@ module Laser
|
|
|
24
24
|
expect(box1.notches).to_not be_nil
|
|
25
25
|
expect(box1.notches.size).to eql(320)
|
|
26
26
|
end
|
|
27
|
+
|
|
28
|
+
it 'should properly calculate enclosure' do
|
|
29
|
+
expect(box1.enclosure.to_a.flatten.map(&:round)).to eql([0,0, 232, 317])
|
|
30
|
+
expect(box2.enclosure.to_a.flatten.map(&:round)).to eql([0,0, 232, 317])
|
|
31
|
+
end
|
|
27
32
|
end
|
|
28
33
|
|
|
29
34
|
end
|
data/spec/configuration_spec.rb
CHANGED
|
@@ -24,7 +24,7 @@ module Laser
|
|
|
24
24
|
end
|
|
25
25
|
end
|
|
26
26
|
context 'zero options' do
|
|
27
|
-
let(:opts) { {"size" => "2.0x0.0x2/0.125/0.5",
|
|
27
|
+
let(:opts) { {"size" => "2.0x0.0x2/0.125/0.5", 'file' => '/tmp/a'} }
|
|
28
28
|
it 'should be able to validate missing options' do
|
|
29
29
|
expect(config.height).to eql(0.0)
|
|
30
30
|
expect { config.validate! }.to raise_error(Laser::Cutter::ZeroValueNotAllowed)
|
|
@@ -32,6 +32,15 @@ module Laser
|
|
|
32
32
|
end
|
|
33
33
|
end
|
|
34
34
|
|
|
35
|
+
context 'default values' do
|
|
36
|
+
let(:opts) { {"size" => "2.0x1.0x2/0.125", 'file' => '/tmp/a'} }
|
|
37
|
+
it 'should correctly default notch based on thickness' do
|
|
38
|
+
config.validate!
|
|
39
|
+
expect(config.thickness).to eql(0.125)
|
|
40
|
+
expect(config.notch).to eql(config.thickness * 3.0)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
35
44
|
context 'when invalid units are provided' do
|
|
36
45
|
let(:opts) { {"size" => "2x3x2/0.125/0.5", "units" => 'xx'} }
|
|
37
46
|
it 'should default to inches' do
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: laser-cutter
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.5.
|
|
4
|
+
version: 0.5.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Konstantin Gredeskoul
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2014-
|
|
11
|
+
date: 2014-10-05 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: prawn
|