laser-cutter 1.0.3 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. checksums.yaml +5 -5
  2. data/.envrc +2 -0
  3. data/.github/workflows/lint.yml +22 -0
  4. data/.github/workflows/rspec.yml +22 -0
  5. data/.gitignore +5 -4
  6. data/.plans/001.00-dry-cli-migration/plan.md +29 -0
  7. data/.relaxed_rubocop.yml +153 -0
  8. data/.rspec +1 -1
  9. data/.rubocop.yml +31 -0
  10. data/.rubocop_todo.yml +90 -0
  11. data/.ruby-version +1 -0
  12. data/CLAUDE.md +62 -0
  13. data/CONTRIBUTING.md +2 -4
  14. data/Gemfile +14 -0
  15. data/README.md +83 -111
  16. data/Rakefile +41 -0
  17. data/docs/badges/coverage_badge.svg +21 -0
  18. data/exe/laser-cutter +8 -0
  19. data/exe/lc +1 -0
  20. data/justfile +97 -0
  21. data/laser-cutter.gemspec +26 -14
  22. data/lib/laser/cutter/aggregator.rb +101 -0
  23. data/lib/{laser-cutter → laser/cutter}/box.rb +44 -52
  24. data/lib/laser/cutter/cli/command.rb +46 -0
  25. data/lib/laser/cutter/cli/completion.rb +21 -0
  26. data/lib/laser/cutter/cli/config_file.rb +36 -0
  27. data/lib/laser/cutter/cli/examples.rb +42 -0
  28. data/lib/laser/cutter/cli/generate.rb +90 -0
  29. data/lib/laser/cutter/cli/help.rb +20 -0
  30. data/lib/laser/cutter/cli/page_sizes.rb +19 -0
  31. data/lib/laser/cutter/cli/version.rb +15 -0
  32. data/lib/laser/cutter/cli.rb +49 -0
  33. data/lib/laser/cutter/configuration.rb +101 -0
  34. data/lib/{laser-cutter → laser/cutter}/geometry/dimensions.rb +2 -3
  35. data/lib/{laser-cutter/geometry/shape → laser/cutter/geometry}/line.rb +19 -18
  36. data/lib/{laser-cutter → laser/cutter}/geometry/point.rb +4 -2
  37. data/lib/{laser-cutter/geometry/shape → laser/cutter/geometry}/rect.rb +6 -7
  38. data/lib/{laser-cutter → laser/cutter}/geometry/shape.rb +7 -7
  39. data/lib/{laser-cutter → laser/cutter}/geometry/tuple.rb +40 -36
  40. data/lib/laser/cutter/launcher.rb +40 -0
  41. data/lib/laser/cutter/missing_option.rb +7 -0
  42. data/lib/laser/cutter/notching/base.rb +19 -0
  43. data/lib/{laser-cutter → laser/cutter}/notching/edge.rb +21 -20
  44. data/lib/laser/cutter/notching/infinite_iterator.rb +27 -0
  45. data/lib/{laser-cutter → laser/cutter}/notching/path_generator.rb +27 -60
  46. data/lib/laser/cutter/notching/shift.rb +17 -0
  47. data/lib/laser/cutter/page_manager.rb +50 -0
  48. data/lib/{laser-cutter → laser/cutter}/renderer/base.rb +3 -2
  49. data/lib/laser/cutter/renderer/box_renderer.rb +40 -0
  50. data/lib/laser/cutter/renderer/layout_renderer.rb +61 -0
  51. data/lib/laser/cutter/renderer/line_renderer.rb +21 -0
  52. data/lib/laser/cutter/renderer/meta_renderer.rb +83 -0
  53. data/lib/{laser-cutter → laser/cutter}/renderer/rect_renderer.rb +3 -1
  54. data/lib/laser/cutter/renderer/svg_renderer.rb +65 -0
  55. data/lib/laser/cutter/renderer.rb +24 -0
  56. data/lib/laser/cutter/units_converter.rb +15 -0
  57. data/lib/laser/cutter/version.rb +7 -0
  58. data/lib/laser/cutter/zero_value_not_allowed.rb +7 -0
  59. data/lib/laser/cutter.rb +29 -0
  60. data/lib/laser-cutter.rb +2 -14
  61. data/lib/laser_cutter.rb +3 -0
  62. data/spec/laser/cutter/aggregator_spec.rb +70 -0
  63. data/spec/laser/cutter/box_spec.rb +61 -0
  64. data/spec/laser/cutter/cli/config_file_spec.rb +31 -0
  65. data/spec/laser/cutter/cli_spec.rb +186 -0
  66. data/spec/laser/cutter/configuration_spec.rb +101 -0
  67. data/spec/{dimensions_spec.rb → laser/cutter/dimensions_spec.rb} +8 -6
  68. data/spec/laser/cutter/edge_spec.rb +64 -0
  69. data/spec/{line_spec.rb → laser/cutter/line_spec.rb} +26 -20
  70. data/spec/{page_manager_spec.rb → laser/cutter/page_manager_spec.rb} +27 -11
  71. data/spec/{path_generator_spec.rb → laser/cutter/path_generator_spec.rb} +18 -14
  72. data/spec/{point_spec.rb → laser/cutter/point_spec.rb} +27 -22
  73. data/spec/{rect_spec.rb → laser/cutter/rect_spec.rb} +9 -5
  74. data/spec/laser/cutter/renderer/layout_renderer_spec.rb +102 -0
  75. data/spec/laser/cutter/renderer/svg_renderer_spec.rb +51 -0
  76. data/spec/laser/cutter/renderer_spec.rb +19 -0
  77. data/spec/spec_helper.rb +34 -25
  78. data/spec/support/aruba.rb +10 -0
  79. metadata +206 -80
  80. data/.travis.yml +0 -11
  81. data/LICENSE +0 -22
  82. data/bin/laser-cutter +0 -19
  83. data/lib/laser-cutter/aggregator.rb +0 -57
  84. data/lib/laser-cutter/cli/opt_parser.rb +0 -131
  85. data/lib/laser-cutter/cli/serializer.rb +0 -51
  86. data/lib/laser-cutter/configuration.rb +0 -83
  87. data/lib/laser-cutter/geometry.rb +0 -11
  88. data/lib/laser-cutter/notching/base.rb +0 -13
  89. data/lib/laser-cutter/notching.rb +0 -10
  90. data/lib/laser-cutter/page_manager.rb +0 -43
  91. data/lib/laser-cutter/renderer/box_renderer.rb +0 -36
  92. data/lib/laser-cutter/renderer/layout_renderer.rb +0 -59
  93. data/lib/laser-cutter/renderer/line_renderer.rb +0 -17
  94. data/lib/laser-cutter/renderer/meta_renderer.rb +0 -77
  95. data/lib/laser-cutter/renderer.rb +0 -14
  96. data/lib/laser-cutter/version.rb +0 -5
  97. data/spec/aggregator_spec.rb +0 -65
  98. data/spec/box_spec.rb +0 -40
  99. data/spec/configuration_spec.rb +0 -81
  100. data/spec/edge_spec.rb +0 -43
  101. data/spec/renderer_spec.rb +0 -72
  102. /data/{doc → docs/images}/comparison.jpg +0 -0
data/README.md CHANGED
@@ -1,169 +1,135 @@
1
- [![Gem Version](https://badge.fury.io/rb/laser-cutter.svg)](http://badge.fury.io/rb/laser-cutter)
2
- [![Build status](https://secure.travis-ci.org/kigster/laser-cutter.png)](http://travis-ci.org/kigster/laser-cutter)
3
- [![Code Climate](https://codeclimate.com/github/kigster/laser-cutter.png)](https://codeclimate.com/github/kigster/laser-cutter)
4
- [![Test Coverage](https://codeclimate.com/github/kigster/laser-cutter/badges/coverage.svg)](https://codeclimate.com/github/kigster/laser-cutter)
1
+ [![Gem Version](https://badge.fury.io/rb/laser-cutter.svg)](http://badge.fury.io/rb/laser-cutter) ![coverage](docs/badges/coverage_badge.svg)
5
2
 
6
3
  ## LaserCutter and Make-A-Box.io
7
4
 
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.
5
+ `laser-cutter` is a ruby library for generating PDF designs for boxes of custom dimensions that suit your project, that can be cut from wood or acrylic using a laser-cutter. The sides of the box snap together using alternating notches, that are deliberately layed out in a symmetric form.
12
6
 
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.
7
+ To use `laser-cutter` you need to have a recent version of ruby interpreter, install it as a gem, and use command line to generate PDFs.
15
8
 
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.
9
+ [Make-A-Box](http://makeabox.io) is a online web application that uses `laser-cutter` library and provides a straight-forward user interface for generating PDF designs without the need to install the gem or use command line.
19
10
 
20
11
  Use whatever suites you better.
21
12
 
22
13
  ### Design Goals
23
14
 
24
- One of the design goals of this project is to provide a highly extensible platform for creating
25
- laser-cut designs, where alternative strategies can be added over time, and supported by various
26
- command line options, and perhaps a light weight web application. If you are interested in
27
- contributing to the project, please see [contributing](CONTRIBUTING.md) for more details.
15
+ One of the design goals of this project is to provide a highly extensible platform for creating laser-cut designs, where alternative strategies can be added over time, and supported by various command line options, and perhaps a light weight web application. If you are interested in contributing to the project, please see [contributing](CONTRIBUTING.md) for more details.
16
+
17
+ `laser-cutter` supports many flexible command line options that allow setting dimensions, stroke width, page size, layout, margins, padding (spacing between the boxes), and many more.
28
18
 
29
- ```laser-cutter``` supports many flexible command line options that allow setting dimensions,
30
- stroke width, page size, layout, margins, padding (spacing between the boxes), and many more.
31
-
32
19
  ## Dependencies
33
20
 
34
- The gem depends primarily on [Prawn](http://prawnpdf.org) – a fantastic PDF generation library.
21
+ The gem depends primarily on [Prawn](http://prawnpdf.org) – a fantastic PDF generation library. SVG output uses [Victor](https://github.com/DannyBen/victor), and the command line is built on [dry-cli](https://dry-cli.tools/). It needs Ruby 4.0 or newer.
35
22
 
36
23
  ## Installation
37
24
 
38
25
  Add this line to your application's Gemfile:
39
26
 
40
- gem 'laser-cutter'
27
+ ```
28
+ gem 'laser-cutter'
29
+ ```
41
30
 
42
31
  And then execute:
43
32
 
44
- $ bundle
33
+ ```
34
+ $ bundle
35
+ ```
45
36
 
46
37
  Or install it yourself as:
47
38
 
48
- $ gem install laser-cutter
39
+ ```
40
+ $ gem install laser-cutter
41
+ ```
49
42
 
50
43
  ## Usage
51
44
 
52
- ```bash
45
+ ```text
46
+ laser-cutter COMMAND [OPTIONS]
53
47
 
54
- Usage: laser-cutter [options] -o filename.pdf
55
- eg: laser-cutter -z 1x1.5x2/0.125 -O -o box.pdf
56
-
57
- Specific Options:
58
- -w, --width WIDTH Internal width of the box
59
- -h, --height HEIGHT Internal height of the box
60
- -d, --depth DEPTH Internal depth of the box
61
- -t, --thickness THICKNESS Thickness of the box material
62
- -n, --notch NOTCH Optional notch length (aka "tab width"), guide only
63
- -k, --kerf KERF Kerf - cut width (default is 0.007in)
64
-
65
- -m, --margin MARGIN Margins from the edge of the document
66
- -p, --padding PADDING Space between the boxes on the page
67
- -s, --stroke WIDTH Numeric stroke width of the line
68
- -i, --page_size LETTER Document page size, default is autofit the box.
69
- -l, --page_layout portrait Page layout, other option is 'landscape'
70
-
71
- -O, --open Open generated file with system viewer before exiting
72
- -W, --write CONFIG_FILE Save provided configuration to a file, use '-' for STDOUT
73
- -R, --read CONFIG_FILE Read configuration from a file, or use '-' for STDIN
74
-
75
- -L, --list-all-page-sizes Print all available page sizes with dimensions and exit
76
- -M, --no-metadata Do not print box metadata on the PDF
77
- -v, --[no-]verbose Run verbosely
78
- -B, --inside-box Draw the inside boxes (helpful to verify kerfing)
79
- -D, --debug Show full exception stack trace on error
80
-
81
- --examples Show detailed usage examples
82
- --help Show this message
83
- --version Show version
84
-
85
- Common Options:
86
- -o, --file FILE Required output filename of the PDF
87
- -z, --size WxHxD/T[/N] Combined internal dimensions: W = width, H = height,
88
- D = depth, T = thickness, and optional N = notch length
89
-
90
- -u, --units UNITS Either 'in' for inches (default) or 'mm'
48
+ generate, g Draw the panels of a box into a PDF or an SVG file
49
+ page-sizes List every page size, with its dimensions
50
+ examples Show detailed usage examples
51
+ help Show help, for the program or for one command
52
+ version Print the version
53
+ completion Print a bash or zsh completion script
91
54
  ```
92
55
 
56
+ `laser-cutter help generate` lists every option. The common ones:
57
+
58
+ | Option | Meaning |
59
+ | :--------------------- | :------------------------------------------------------------- |
60
+ | `-b`, `--box` | `WxHxD/T[/N]`: width, height, depth, thickness, optional notch |
61
+ | `-w`, `-H`, `-d`, `-t` | Width, height, depth and thickness, one at a time |
62
+ | `-n`, `--notch` | Notch length, a guide only |
63
+ | `-k`, `--kerf` | Kerf, the width of the cut |
64
+ | `-u`, `--units` | `in` (default) or `mm` |
65
+ | `-o`, `--file` | File to write, required |
66
+ | `-f`, `--format` | `pdf` (default) or `svg`, in either case |
67
+ | `-B`, `--inside-box` | Also draw the box without kerf, in red |
68
+ | `-W`, `-R` | Save the configuration to a file, or read it from one |
69
+
70
+ Height is `-H`, because `-h` prints help.
71
+
93
72
  ### Examples
94
73
 
95
- Create a box defined in inches, with kerf (cut width) set to 0.008", and open PDF in preview right after:
74
+ A box in inches, with the kerf set to 0.008", opened once it is written:
96
75
 
97
76
  ```bash
98
- laser-cutter -z 3x2x2/0.125 -k 0.008 -O -o box.pdf
99
- ```
77
+ laser-cutter generate -b 3x2x2/0.125 -k 0.008 -O -o box.pdf
78
+ ```
100
79
 
101
- Create a box defined in millimeters, print verbose info, and set
102
- page size to A3, and layout to landscape, and stroke width to 1/2mm:
80
+ The same box as an SVG:
103
81
 
104
82
  ```bash
105
- laser-cutter -u mm -w70 -h20 -d50 -t4.3 -n5 -iA3 -l landscape -s0.5 -v -O -o box.pdf
106
- ```
83
+ laser-cutter generate -b 3x2x2/0.125 -f svg -o box.svg
84
+ ```
107
85
 
108
- List all possible page sizes in metric system:
86
+ A box in millimeters on a landscape A3 page, with a 0.5mm stroke:
109
87
 
110
88
  ```bash
111
- laser-cutter -L -u mm
112
- ```
89
+ laser-cutter generate -u mm -w 70 -H 20 -d 50 -t 4.3 -n 5 -i A3 -l landscape -s 0.5 -o box.pdf
90
+ ```
113
91
 
114
- Create a box with provided dimensions, and save the config to a file for later use:
92
+ Every page size, in millimeters:
115
93
 
116
94
  ```bash
117
- laser-cutter -z 1.1x2.5x1.5/0.125/0.125 -p 0.1 -O -o box.pdf -W box-settings.json
118
- ```
95
+ laser-cutter page-sizes -u mm
96
+ ```
119
97
 
120
- Read settings from a previously saved file:
98
+ Save the settings of a box, and use them again:
121
99
 
122
100
  ```bash
123
- laser-cutter -O -o box.pdf -R box-settings.json
124
- cat box-settings.json | laser-cutter -O -o box.pdf -R -
101
+ laser-cutter generate -b 1.1x2.5x1.5/0.125/0.125 -p 0.1 -o box.pdf -W box-settings.json
102
+ laser-cutter generate -o box.pdf -R box-settings.json
103
+ cat box-settings.json | laser-cutter generate -o box.pdf -R -
125
104
  ```
126
105
 
127
106
  ## Feature Wish List
128
107
 
129
- * Create T-style joins, using various standard sizes of nuts and bolts (such as common #4-40 and M2 sizes)
130
- * Extensibility with various layout strategies, notch drawing strategies, basically plug and play
131
- model for adding new algorithms for path creation and box joining
132
- * Support more shapes than just box, such as prisms
133
- * Supporting lids and front panels, that are larger than the box itself and have holes for notches.
134
- * Your brilliant idea can be here too! Please see [contributing](CONTRIBUTING.md) for more info.
108
+ - Create T-style joins, using various standard sizes of nuts and bolts (such as common #4-40 and M2 sizes)
109
+ - Extensibility with various layout strategies, notch drawing strategies, basically plug and play model for adding new algorithms for path creation and box joining
110
+ - Support more shapes than just box, such as prisms
111
+ - Supporting lids and front panels, that are larger than the box itself and have holes for notches.
112
+ - Your brilliant idea can be here too! Please see [contributing](CONTRIBUTING.md) for more info.
135
113
 
136
114
  ## LaserCutter vs BoxMaker
137
115
 
138
- [Rahulbot](https://github.com/rahulbot/)-made [BoxMaker](https://github.com/rahulbot/boxmaker/) is a
139
- functional generator of notched designs, similar to ```laser-cutter```, and generously open sourced
140
- by the author, and so in no way this project disputes BoxMaker's viability. In fact BoxMaker was an
141
- inspiration for this project.
116
+ [Rahulbot](https://github.com/rahulbot/)-made [BoxMaker](https://github.com/rahulbot/boxmaker/) is a functional generator of notched designs, similar to `laser-cutter`, and generously open sourced by the author, and so in no way this project disputes BoxMaker's viability. In fact BoxMaker was an inspiration for this project.
142
117
 
143
- Laser-Cutter library attempts to further advance the concept of programmatically creating
144
- laser-cut box designs, provides additional fine tuning, many more options, strategies and most
145
- importantly – extensibility.
118
+ Laser-Cutter library attempts to further advance the concept of programmatically creating laser-cut box designs, provides additional fine tuning, many more options, strategies and most importantly – extensibility.
146
119
 
147
- Unlike ```BoxMaker```, this gem has a suit of automated tests (rspecs) around the core functionality.
148
- In addition, new feature contributions are highly encouraged, and in that
149
- regard having existing test suit offers confidence against regressions, and thus welcomes colaboration.
120
+ Unlike `BoxMaker`, this gem has a suit of automated tests (rspecs) around the core functionality. In addition, new feature contributions are highly encouraged, and in that regard having existing test suit offers confidence against regressions, and thus welcomes colaboration.
150
121
 
151
- Finally, BoxMaker's notch-drawing algorithm generates non-symmetric and sometimes purely broken designs
152
- (see picture below).
153
-
154
- ```laser-cutter```'s algorithm will create a _symmetric design for most panels_, but it might sacrifice
155
- identical notch length. Depending on the box dimensions you may end up with a slightly different notch
156
- length on each side of the box.
122
+ Finally, BoxMaker's notch-drawing algorithm generates non-symmetric and sometimes purely broken designs (see picture below).
157
123
 
158
- The choice ultimately comes down to the preference and feature set, so here I show you two boxes made with
159
- each program, so you can pick what you prefer.
124
+ `laser-cutter`'s algorithm will create a _symmetric design for most panels_, but it might sacrifice identical notch length. Depending on the box dimensions you may end up with a slightly different notch length on each side of the box.
125
+
126
+ The choice ultimately comes down to the preference and feature set, so here I show you two boxes made with each program, so you can pick what you prefer.
160
127
 
161
128
  ### Example Outputs
162
129
 
163
- Below are two examples of boxes with identical dimensions produced with ```laser-cutter``` and ```boxmaker```:
130
+ Below are two examples of boxes with identical dimensions produced with `laser-cutter` and `boxmaker`:
164
131
 
165
- This is how you would make a box with Adam Phelp's fork of BoxMaker (which adds flags and a lot of
166
- niceties):
132
+ This is how you would make a box with Adam Phelp's fork of BoxMaker (which adds flags and a lot of niceties):
167
133
 
168
134
  ```bash
169
135
  git clone https://github.com/aphelps/boxmaker && cd boxmaker && ant
@@ -175,15 +141,21 @@ And laser-cutter:
175
141
 
176
142
  ```bash
177
143
  gem install laser-cutter
178
- laser-cutter -z 1x1.5x2/0.125/0.125 -O -o box.pdf
144
+ laser-cutter generate -b 1x1.5x2/0.125/0.125 -O -o box.pdf
179
145
  ```
180
146
 
181
- ![LaserCutter Comparison](doc/comparison.jpg).
147
+ ![LaserCutter Comparison](docs/images/comparison.jpg).
182
148
 
183
149
  ## Contributing
184
150
 
185
151
  1. Fork it ( https://github.com/[my-github-username]/laser-cutter/fork )
186
- 2. Create your feature branch (`git checkout -b my-new-feature`)
187
- 3. Commit your changes (`git commit -am 'Add some feature'`)
188
- 4. Push to the branch (`git push origin my-new-feature`)
189
- 5. Create a new Pull Request
152
+ 1. Create your feature branch (`git checkout -b my-new-feature`)
153
+ 1. Commit your changes (`git commit -am 'Add some feature'`)
154
+ 1. Create a new Pull Request
155
+ 1. Push to the branch (`git push origin my-new-feature`)
156
+
157
+ ## License
158
+
159
+ MIT License (MIT). Please see [LICENSE](LICENSE) for more information.
160
+
161
+ Author: © 2015-2024 Konstantin Gredeskoul [@kigster](https://github.com/kigster)
data/Rakefile CHANGED
@@ -1,2 +1,43 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+ require "timeout"
6
+ require "yard"
7
+
8
+ def shell(*args)
9
+ puts "running: #{args.join(' ')}"
10
+ system(args.join(" "))
11
+ end
12
+
13
+ task :clean do
14
+ shell("rm -rf pkg/ tmp/ coverage/ doc/ ")
15
+ end
16
+
17
+ task gem: [:build] do
18
+ shell("gem install pkg/*")
19
+ end
20
+
21
+ task permissions: [:clean] do
22
+ # One traversal replaces a six-level glob chain that printed "No such file
23
+ # or directory" for every level this project does not have, skipped dotfiles
24
+ # entirely, and silently stopped at depth six. .git is pruned: its objects
25
+ # have no business being group-readable.
26
+ shell("find . -path ./.git -prune -o -type d -exec chmod o+rx,g+rx {} + -o -type f -exec chmod o+r,g+r {} +")
27
+ end
28
+
29
+ task build: :permissions
30
+
31
+ YARD::Rake::YardocTask.new(:doc) do |t|
32
+ t.files = %w[lib/**/*.rb - README.md LICENSE.txt CHANGELOG.md]
33
+ t.options.unshift("--title", '"dry-cli-ui: runtime terminal UI for dry-cli commands"')
34
+ t.after = -> { exec("open doc/index.html") } if RUBY_PLATFORM =~ /darwin/
35
+
36
+ require "fileutils"
37
+ FileUtils.mkdir_p("doc/docs/badges")
38
+ FileUtils.cp("docs/badges/coverage_badge.svg", "doc/docs/badges")
39
+ end
40
+
41
+ RSpec::Core::RakeTask.new(:spec)
2
42
 
43
+ task default: :spec
@@ -0,0 +1,21 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <svg xmlns="http://www.w3.org/2000/svg" width="99" height="20">
3
+ <linearGradient id="b" x2="0" y2="100%">
4
+ <stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
5
+ <stop offset="1" stop-opacity=".1"/>
6
+ </linearGradient>
7
+ <mask id="a">
8
+ <rect width="99" height="20" rx="3" fill="#fff"/>
9
+ </mask>
10
+ <g mask="url(#a)">
11
+ <path fill="#555" d="M0 0h63v20H0z"/>
12
+ <path fill="#4c1" d="M63 0h36v20H63z"/>
13
+ <path fill="url(#b)" d="M0 0h99v20H0z"/>
14
+ </g>
15
+ <g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11">
16
+ <text x="31.5" y="15" fill="#010101" fill-opacity=".3">coverage</text>
17
+ <text x="31.5" y="14">coverage</text>
18
+ <text x="80" y="15" fill="#010101" fill-opacity=".3">97%</text>
19
+ <text x="80" y="14">97%</text>
20
+ </g>
21
+ </svg>
data/exe/laser-cutter ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # No Bundler here: a Gemfile in the user's directory must not decide which gems load.
5
+ $LOAD_PATH.unshift(File.expand_path('../lib', __dir__))
6
+ require 'laser/cutter'
7
+
8
+ Laser::Cutter::Launcher.new(ARGV).execute!
data/exe/lc ADDED
@@ -0,0 +1 @@
1
+ laser-cutter
data/justfile ADDED
@@ -0,0 +1,97 @@
1
+ set shell := ["bash", "-c"]
2
+
3
+ version := `gawk -F'"' '/VERSION/ { printf "%s", $2 }' lib/laser/cutter/version.rb`
4
+ rbenv := 'eval "$(rbenv init - bash 2>/dev/null || true)"; bundle exec '
5
+ repo := 'git@github.com:kigster/laser-cutter.git'
6
+
7
+ gem_name := 'laser-cutter'
8
+ gem_file := 'pkg/' + gem_name + '-' + version + '.gem'
9
+ gem_url := 'https://rubygems.org/gems/' + gem_name
10
+
11
+ [no-exit-message]
12
+ recipes:
13
+ just --choose
14
+
15
+ # Lint Ruby
16
+ lint:
17
+ {{ rbenv }} rubocop
18
+
19
+ # Autocorrect Ruby (pass -A for unsafe corrections) and format Markdown
20
+ format *args: format-markdown
21
+ {{ rbenv }} rubocop -a {{ args }}
22
+
23
+ # Format every Markdown file
24
+ format-markdown:
25
+ fd .md -X mdformat --wrap no
26
+
27
+ # Run all the tests; a full run enforces 100% line and branch coverage
28
+ test *args:
29
+ {{ rbenv }} rspec {{ args }}
30
+
31
+ # Run all tests with --documentation
32
+ test-docs *args:
33
+ {{ rbenv }} rspec --format documentation {{ args }}
34
+
35
+ # Run tests and measure coverage even for a partial run
36
+ test-coverage *args:
37
+ export COVERAGE=true; {{ rbenv }} rspec {{ args }}
38
+
39
+ ci: lint test-coverage
40
+
41
+ alias check-all := ci
42
+
43
+ # Remove .DS_Store files and tmp/
44
+ clean:
45
+ fd --hidden --no-ignore --type file --glob .DS_Store --exec rm -v
46
+ rm -rf tmp coverage
47
+
48
+ # Run all lefthook pre-commit hooks against every file
49
+ lefthook:
50
+ lefthook run pre-commit --all-files
51
+
52
+ # Print current gem version
53
+ version:
54
+ @echo "{{ version }}"
55
+
56
+ # Remove every generated file
57
+ clobber:
58
+ {{ rbenv }} rake clobber
59
+
60
+ # Generate YARD documentation
61
+ doc:
62
+ {{ rbenv }} rake doc
63
+
64
+ # Builds the gem for distribution
65
+ build:
66
+ {{ rbenv }} rake build
67
+
68
+ # `gem push` rather than `rake release`: release also tags and pushes git,
69
+ # which `just release` does separately, and it gives no way to pass a 2FA code.
70
+ #
71
+ # just publish # gem push prompts for the code if 2FA needs one
72
+ # just publish 123456 # use this code
73
+ #
74
+ # Build the .gem and push it to RubyGems
75
+ publish otp="": build
76
+ #!/usr/bin/env bash
77
+ set -euo pipefail
78
+ eval "$(rbenv init - bash 2>/dev/null || true)"
79
+
80
+ otp="{{ otp }}"
81
+ if [[ -n "${otp}" ]]; then
82
+ gem push "{{ gem_file }}" --otp "${otp}"
83
+ else
84
+ gem push "{{ gem_file }}"
85
+ fi
86
+
87
+ # Only reachable when the push succeeded: `set -e` aborts on a failed push.
88
+ echo "published {{ gem_name }} {{ version }} → {{ gem_url }}"
89
+ open "{{ gem_url }}" 2>/dev/null || xdg-open "{{ gem_url }}" 2>/dev/null || true
90
+
91
+ # Tag v{{ version }} and publish the GitHub release
92
+ release:
93
+ git fetch --tags
94
+ git tag -f "v{{ version }}"
95
+ git push -f --tags
96
+ gh release delete -y "v{{ version }}" --repo {{ repo }} 2>/dev/null || true
97
+ gh release create "v{{ version }}" --generate-notes --repo {{ repo }}
data/laser-cutter.gemspec CHANGED
@@ -1,30 +1,42 @@
1
- # coding: utf-8
2
- lib = File.expand_path('../lib', __FILE__)
1
+ # frozen_string_literal: true
2
+
3
+ lib = File.expand_path('./lib', __dir__)
3
4
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
- require 'laser-cutter/version'
5
+
6
+ require_relative 'lib/laser/cutter/version'
5
7
 
6
8
  Gem::Specification.new do |spec|
7
9
  spec.name = "laser-cutter"
8
10
  spec.version = Laser::Cutter::VERSION
9
11
  spec.authors = ['Konstantin Gredeskoul']
10
12
  spec.email = ["kigster@gmail.com"]
11
- spec.summary = %q{Creates notched box outlines for laser-cut boxes which are geometrically symmetric and pleasing to the eye.}
12
- spec.description = %q{Similar to the older BoxMaker, this ruby gem generates PDFs that can be used as a basis for cutting boxes on a typical laser cutter. The intention was to create an extensible, well tested, and modern ruby gem for generating PDF templates used in laser cutting.}
13
+ spec.summary = 'Creates notched box outlines for laser-cut boxes which are geometrically symmetric and pleasing to the eye.'
14
+ spec.description = 'Similar to the older BoxMaker, this ruby gem generates PDFs that can be used as a basis for cutting boxes on a typical laser cutter. The intention was to create an extensible, well tested, and modern ruby gem for generating PDF templates used in laser cutting.'
13
15
  spec.homepage = "https://github.com/kigster/laser-cutter"
14
16
  spec.license = "MIT"
15
17
 
16
18
  spec.files = `git ls-files -z`.split("\x0")
17
- spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
- spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.bindir = 'exe'
20
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
19
21
  spec.require_paths = ["lib"]
22
+ spec.required_ruby_version = '>= 4.0'
20
23
 
21
- spec.add_dependency 'prawn'
24
+ spec.add_dependency 'dry-cli', '~> 1.4.1'
25
+ spec.add_dependency 'dry-cli-autocomplete'
26
+ spec.add_dependency 'dry-cli-help'
27
+ spec.add_dependency 'dry-cli-ui'
22
28
  spec.add_dependency 'hashie'
23
- spec.add_dependency 'colored'
29
+ spec.add_dependency 'matrix'
30
+ spec.add_dependency 'pastel'
31
+ spec.add_dependency 'prawn'
32
+ spec.add_dependency 'tty-cursor'
33
+ spec.add_dependency 'tty-progressbar'
34
+ spec.add_dependency 'tty-prompt'
35
+ spec.add_dependency 'tty-screen'
36
+ spec.add_dependency 'tty-spinner'
37
+ spec.add_dependency 'tty-table'
38
+ spec.add_dependency 'victor'
39
+ spec.add_dependency 'zeitwerk'
24
40
 
25
- spec.add_development_dependency 'bundler', "~> 1.6"
26
- spec.add_development_dependency 'rake'
27
- spec.add_development_dependency 'rspec'
28
- spec.add_development_dependency 'rspec-legacy_formatters'
29
- spec.add_development_dependency 'codeclimate-test-reporter'
41
+ spec.metadata['rubygems_mfa_required'] = 'true'
30
42
  end
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Laser
4
+ module Cutter
5
+ # Merges the lines of one face into the outline to cut.
6
+ #
7
+ # Each edge draws its own notch path and its own corner boxes, so where
8
+ # two of them meet the same stretch is drawn twice, once from each side.
9
+ # That stretch runs through the material and must not be cut. So every
10
+ # collinear group of lines is combined as a symmetric difference: a point
11
+ # is cut when an odd number of lines cover it. Two identical lines cancel,
12
+ # two overlapping lines keep only the parts they do not share, and lines
13
+ # that touch or overlap end to end join into one.
14
+ #
15
+ # Lines are grouped by axis and offset, sorted, and swept once, so a face
16
+ # of n lines takes n log n. Lines on neither axis pass through untouched.
17
+ class Aggregator
18
+ # Coordinates this close together count as the same.
19
+ TOLERANCE = Math.sqrt(Geometry::Tuple::PRECISION)
20
+
21
+ # @return [Array<Geometry::Line>] the outline, sorted
22
+ attr_reader :lines
23
+
24
+ # @param lines [Array<Geometry::Line>]
25
+ def initialize(lines = [])
26
+ @lines = (sweep(lines, 0) + sweep(lines, 1) + skewed(lines)).sort
27
+ end
28
+
29
+ private
30
+
31
+ # Lines running along one axis: 0 for horizontal (constant y), 1 for vertical (constant x).
32
+ #
33
+ # @return [Array<Geometry::Line>]
34
+ def sweep(lines, axis)
35
+ across = (axis + 1) % 2
36
+ along = lines.select { |line| along?(line, axis) }
37
+ groups(along, across).flat_map { |offset, group| cut_intervals(group, axis, offset, across) }
38
+ end
39
+
40
+ def skewed(lines)
41
+ lines.reject { |line| along?(line, 0) || along?(line, 1) }
42
+ end
43
+
44
+ def along?(line, axis)
45
+ across = (axis + 1) % 2
46
+ (line.p1[across] - line.p2[across]).abs <= TOLERANCE && (line.p1[axis] - line.p2[axis]).abs > TOLERANCE
47
+ end
48
+
49
+ # Lines on the same row or column, keyed by the offset of the first one seen.
50
+ def groups(lines, across)
51
+ lines.sort_by { |line| line.p1[across] }.slice_when { |a, b| b.p1[across] - a.p1[across] > TOLERANCE }
52
+ .to_h { |group| [group.first.p1[across], group] }
53
+ end
54
+
55
+ # Sweeps the endpoints of one collinear group, keeping the stretches an
56
+ # odd number of lines cover.
57
+ def cut_intervals(group, axis, offset, across)
58
+ events = group.flat_map do |line|
59
+ from, to = [line.p1[axis], line.p2[axis]].sort
60
+ [[from, 1], [to, -1]]
61
+ end
62
+ events.sort_by! { |position, step| [position, step] }
63
+
64
+ intervals = []
65
+ covered = 0
66
+ started = nil
67
+ events.each do |position, step|
68
+ was = covered
69
+ covered += step
70
+ if was.even? && covered.odd?
71
+ started = position
72
+ elsif was.odd? && covered.even?
73
+ extend_or_add(intervals, started, position)
74
+ end
75
+ end
76
+ intervals.map { |from, to| line_between(from, to, axis, offset, across) }
77
+ end
78
+
79
+ # Joins an interval onto the last one when they touch, and drops one with no length.
80
+ def extend_or_add(intervals, from, to)
81
+ return if to - from <= TOLERANCE
82
+
83
+ last = intervals.last
84
+ if last && from - last[1] <= TOLERANCE
85
+ last[1] = to
86
+ else
87
+ intervals << [from, to]
88
+ end
89
+ end
90
+
91
+ def line_between(from, to, axis, offset, across)
92
+ a = []
93
+ b = []
94
+ a[axis] = from
95
+ b[axis] = to
96
+ a[across] = b[across] = offset
97
+ Geometry::Line.new(Geometry::Point[*a], Geometry::Point[*b])
98
+ end
99
+ end
100
+ end
101
+ end