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.
Files changed (104) 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 -5
  6. data/.plans/001.00-dry-cli-migration/plan.md +29 -0
  7. data/.relaxed_rubocop.yml +153 -0
  8. data/.rspec +1 -0
  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 +110 -87
  16. data/Rakefile +27 -13
  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/box.rb +173 -0
  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 +23 -22
  44. data/lib/laser/cutter/notching/infinite_iterator.rb +27 -0
  45. data/lib/{laser-cutter → laser/cutter}/notching/path_generator.rb +76 -94
  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 +35 -22
  78. data/spec/support/aruba.rb +10 -0
  79. metadata +202 -76
  80. data/.travis.yml +0 -26
  81. data/BOXMAKER.md +0 -51
  82. data/LICENSE +0 -22
  83. data/bin/laser-cutter +0 -19
  84. data/lib/laser-cutter/aggregator.rb +0 -57
  85. data/lib/laser-cutter/box.rb +0 -183
  86. data/lib/laser-cutter/cli/opt_parser.rb +0 -131
  87. data/lib/laser-cutter/cli/serializer.rb +0 -51
  88. data/lib/laser-cutter/configuration.rb +0 -83
  89. data/lib/laser-cutter/face.rb +0 -18
  90. data/lib/laser-cutter/geometry.rb +0 -11
  91. data/lib/laser-cutter/notching.rb +0 -9
  92. data/lib/laser-cutter/page_manager.rb +0 -43
  93. data/lib/laser-cutter/renderer/box_renderer.rb +0 -36
  94. data/lib/laser-cutter/renderer/layout_renderer.rb +0 -59
  95. data/lib/laser-cutter/renderer/line_renderer.rb +0 -17
  96. data/lib/laser-cutter/renderer/meta_renderer.rb +0 -77
  97. data/lib/laser-cutter/renderer.rb +0 -14
  98. data/lib/laser-cutter/version.rb +0 -5
  99. data/spec/aggregator_spec.rb +0 -65
  100. data/spec/box_spec.rb +0 -39
  101. data/spec/configuration_spec.rb +0 -81
  102. data/spec/edge_spec.rb +0 -43
  103. data/spec/renderer_spec.rb +0 -72
  104. /data/docs/{comparison.jpg → images/comparison.jpg} +0 -0
data/README.md CHANGED
@@ -1,138 +1,161 @@
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
- [![Maintainability](https://api.codeclimate.com/v1/badges/bea3225fd93ee84d078a/maintainability)](https://codeclimate.com/github/kigster/laser-cutter/maintainability)[![Test Coverage](https://api.codeclimate.com/v1/badges/bea3225fd93ee84d078a/test_coverage)](https://codeclimate.com/github/kigster/laser-cutter/test_coverage)
1
+ [![Gem Version](https://badge.fury.io/rb/laser-cutter.svg)](http://badge.fury.io/rb/laser-cutter) ![coverage](docs/badges/coverage_badge.svg)
4
2
 
5
- [![Join the chat at https://gitter.im/kigster/laser-cutter](https://badges.gitter.im/kigster/laser-cutter.svg)](https://gitter.im/kigster/laser-cutter?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
6
- [![GitHub issues](https://img.shields.io/github/issues/kigster/laser-cutter.svg)](https://github.com/kigster/laser-cutter/issues)
7
- [![GitHub forks](https://img.shields.io/github/forks/kigster/laser-cutter.svg)](https://github.com/kigster/laser-cutter/network)
8
- [![GitHub stars](https://img.shields.io/github/stars/kigster/laser-cutter.svg)](https://github.com/kigster/laser-cutter/stargazers)
9
- [![GitHub license](https://img.shields.io/github/license/kigster/laser-cutter.svg)](https://github.com/kigster/laser-cutter/blob/master/LICENSE)
3
+ ## LaserCutter and Make-A-Box.io
10
4
 
11
- [![Donate](https://www.paypalobjects.com/en_US/i/btn/btn_donate_SM.gif)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=FSFYYNEQ8RKWU)
12
-
13
- # LaserCutter
14
-
15
- **LaserCutter** is a ruby library for generating PDF designs for boxes of custom dimensions that suit your project, that are meant to be used as a cut template on a laser-cutter. The sides of the box snap together using alternating notches, that are deliberately laid 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.
16
6
 
17
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.
18
8
 
19
- [MakeABox.IO](http://makeabox.io) is an 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.
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.
20
10
 
21
11
  Use whatever suites you better.
22
12
 
23
- > NOTE: Please read our [feature comparison guide](BOXMAKER.md) of LaserCutter against an older tool called [BoxMaker](https://github.com/rahulbot/boxmaker).
13
+ ### Design Goals
14
+
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.
24
18
 
25
19
  ## Dependencies
26
20
 
27
- 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.
28
22
 
29
23
  ## Installation
30
24
 
31
25
  Add this line to your application's Gemfile:
32
26
 
33
- gem 'laser-cutter'
27
+ ```
28
+ gem 'laser-cutter'
29
+ ```
34
30
 
35
31
  And then execute:
36
32
 
37
- $ bundle
33
+ ```
34
+ $ bundle
35
+ ```
38
36
 
39
- Or install it manually:
37
+ Or install it yourself as:
40
38
 
41
- $ gem install laser-cutter
39
+ ```
40
+ $ gem install laser-cutter
41
+ ```
42
42
 
43
43
  ## Usage
44
44
 
45
- We'll start with some examples:
45
+ ```text
46
+ laser-cutter COMMAND [OPTIONS]
47
+
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
54
+ ```
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.
46
71
 
47
72
  ### Examples
48
73
 
49
- Create a box defined in inches, with kerf (cut width) set to `0.005in`, and open PDF in preview right after:
74
+ A box in inches, with the kerf set to 0.008", opened once it is written:
50
75
 
51
76
  ```bash
52
- laser-cutter -z 3x2x2/0.125 -k 0.005 -O -o box.pdf
53
- ```
77
+ laser-cutter generate -b 3x2x2/0.125 -k 0.008 -O -o box.pdf
78
+ ```
54
79
 
55
- Create a box defined in millimeters, print verbose info, and set page size to A3, and layout to landscape, and stroke width to `1/2mm`:
80
+ The same box as an SVG:
56
81
 
57
82
  ```bash
58
- laser-cutter -u mm -w70 -h20 -d50 -t4.3 -n5 -iA3 -l landscape -s0.5 -v -O -o box.pdf
59
- ```
83
+ laser-cutter generate -b 3x2x2/0.125 -f svg -o box.svg
84
+ ```
60
85
 
61
- List all possible page sizes in metric system:
86
+ A box in millimeters on a landscape A3 page, with a 0.5mm stroke:
62
87
 
63
88
  ```bash
64
- laser-cutter -L -u mm
65
- ```
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
+ ```
66
91
 
67
- Create a box with provided dimensions, and save the config to a file for later use:
92
+ Every page size, in millimeters:
68
93
 
69
94
  ```bash
70
- laser-cutter -z 1.1x2.5x1.5/0.125/0.125 -p 0.1 -O -o box.pdf -W box-settings.json
71
- ```
95
+ laser-cutter page-sizes -u mm
96
+ ```
72
97
 
73
- Read settings from a previously saved file:
98
+ Save the settings of a box, and use them again:
74
99
 
75
100
  ```bash
76
- laser-cutter -O -o box.pdf -R box-settings.json
77
- 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 -
78
104
  ```
79
105
 
80
- ### Complete Help
106
+ ## Feature Wish List
81
107
 
82
- ```bash
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.
113
+
114
+ ## LaserCutter vs BoxMaker
115
+
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.
117
+
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.
119
+
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.
121
+
122
+ Finally, BoxMaker's notch-drawing algorithm generates non-symmetric and sometimes purely broken designs (see picture below).
123
+
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.
127
+
128
+ ### Example Outputs
83
129
 
84
- Usage: laser-cutter [options] -o filename.pdf
85
- eg: laser-cutter -z 1x1.5x2/0.125 -O -o box.pdf
86
-
87
- Specific Options:
88
- -w, --width WIDTH Internal width of the box
89
- -h, --height HEIGHT Internal height of the box
90
- -d, --depth DEPTH Internal depth of the box
91
- -t, --thickness THICKNESS Thickness of the box material
92
- -n, --notch NOTCH Optional notch length (aka "tab width"), guide only
93
- -k, --kerf KERF Kerf - cut width (default is 0.0024in)
94
-
95
- -m, --margin MARGIN Margins from the edge of the document
96
- -p, --padding PADDING Space between the boxes on the page
97
- -s, --stroke WIDTH Numeric stroke width of the line
98
- -i, --page_size LETTER Document page size, default is autofit the box.
99
- -l, --page_layout portrait Page layout, other option is 'landscape'
100
-
101
- -O, --open Open generated file with system viewer before exiting
102
- -W, --write CONFIG_FILE Save provided configuration to a file, use '-' for STDOUT
103
- -R, --read CONFIG_FILE Read configuration from a file, or use '-' for STDIN
104
-
105
- -L, --list-all-page-sizes Print all available page sizes with dimensions and exit
106
- -M, --no-metadata Do not print box metadata on the PDF
107
- -v, --[no-]verbose Run verbosely
108
- -B, --inside-box Draw the inside boxes (helpful to verify kerfing)
109
- -D, --debug Show full exception stack trace on error
110
-
111
- --examples Show detailed usage examples
112
- --help Show this message
113
- --version Show version
114
-
115
- Common Options:
116
- -o, --file FILE Required output filename of the PDF
117
- -z, --size WxHxD/T[/N] Combined internal dimensions: W = width, H = height,
118
- D = depth, T = thickness, and optional N = notch length
119
-
120
- -u, --units UNITS Either 'in' for inches (default) or 'mm'
130
+ Below are two examples of boxes with identical dimensions produced with `laser-cutter` and `boxmaker`:
131
+
132
+ This is how you would make a box with Adam Phelp's fork of BoxMaker (which adds flags and a lot of niceties):
133
+
134
+ ```bash
135
+ git clone https://github.com/aphelps/boxmaker && cd boxmaker && ant
136
+ java -cp BOX.jar com.rahulbotics.boxmaker.BoxMaker \
137
+ -W 1 -H 2 -D 1.5 -T 0.125 -n 0.125 -o box.pdf
121
138
  ```
122
139
 
123
- ## Wish List
140
+ And laser-cutter:
124
141
 
125
- * Create T-style joins, using various standard sizes of nuts and bolts (such as common #4-40 and M2 sizes)
126
- * Extensibility with various layout strategies, notch drawing strategies, basically plug and play
127
- model for adding new algorithms for path creation and box joining
128
- * Support more shapes than just box, such as prisms
129
- * Supporting lids and front panels, that are larger than the box itself and have holes for notches.
130
- * Your brilliant idea can be here too! Please see [contributing](CONTRIBUTING.md) for more info.
142
+ ```bash
143
+ gem install laser-cutter
144
+ laser-cutter generate -b 1x1.5x2/0.125/0.125 -O -o box.pdf
145
+ ```
146
+
147
+ ![LaserCutter Comparison](docs/images/comparison.jpg).
131
148
 
132
149
  ## Contributing
133
150
 
134
- 1. Fork it ( https://github.com/kigster/laser-cutter/fork )
135
- 2. Create your feature branch (`git checkout -b my-new-feature`)
136
- 3. Commit your changes (`git commit -am 'Add some feature'`)
137
- 4. Push to the branch (`git push origin my-new-feature`)
138
- 5. Create a new Pull Request
151
+ 1. Fork it ( https://github.com/[my-github-username]/laser-cutter/fork )
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,29 +1,43 @@
1
- require 'bundler/gem_tasks'
2
- require 'rspec/core/rake_task'
3
- require 'yard'
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+ require "timeout"
6
+ require "yard"
4
7
 
5
8
  def shell(*args)
6
9
  puts "running: #{args.join(' ')}"
7
- system(args.join(' '))
10
+ system(args.join(" "))
8
11
  end
9
12
 
10
13
  task :clean do
11
- shell('rm -rf pkg/ tmp/ coverage/' )
14
+ shell("rm -rf pkg/ tmp/ coverage/ doc/ ")
15
+ end
16
+
17
+ task gem: [:build] do
18
+ shell("gem install pkg/*")
12
19
  end
13
20
 
14
- task :permissions => [ :clean ] do
15
- shell("chmod -v o+r,g+r * */* */*/* */*/*/* */*/*/*/* */*/*/*/*/*")
16
- shell("find . -type d -exec chmod o+x,g+x {} \\;")
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 {} +")
17
27
  end
18
28
 
19
- task :build => :permissions
29
+ task build: :permissions
20
30
 
21
31
  YARD::Rake::YardocTask.new(:doc) do |t|
22
- t.files = %w(lib/**/*.rb bin/* - README.md LICENSE.txt BOXMAKER.md)
23
- t.options.unshift('--title','LaserCutter Library')
24
- t.after = ->() { exec('open doc/index.html') }
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")
25
39
  end
26
40
 
27
41
  RSpec::Core::RakeTask.new(:spec)
28
42
 
29
- task :default => :spec
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 'yard'
26
- spec.add_development_dependency 'simplecov'
27
- spec.add_development_dependency 'bundler'
28
- spec.add_development_dependency 'rake'
29
- spec.add_development_dependency 'rspec'
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