laser-cutter 1.0.5 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +5 -5
- data/.envrc +2 -0
- data/.github/workflows/lint.yml +22 -0
- data/.github/workflows/rspec.yml +22 -0
- data/.gitignore +5 -5
- data/.plans/001.00-dry-cli-migration/plan.md +29 -0
- data/.plans/002.00-lid-options/plan.md +45 -0
- data/.plans/003.00-ruby-api/plan.md +37 -0
- data/.relaxed_rubocop.yml +153 -0
- data/.rspec +1 -0
- data/.rubocop.yml +31 -0
- data/.rubocop_todo.yml +90 -0
- data/.ruby-version +1 -0
- data/CLAUDE.md +75 -0
- data/CONTRIBUTING.md +2 -4
- data/Gemfile +14 -0
- data/README.md +207 -79
- data/Rakefile +27 -13
- data/docs/badges/coverage_badge.svg +21 -0
- data/docs/images/box-lid-back.avif +0 -0
- data/docs/images/box-lid-full.avif +0 -0
- data/docs/images/box-lid-plain.avif +0 -0
- data/docs/images/box-metric.avif +0 -0
- data/docs/images/box-tray.avif +0 -0
- data/exe/laser-cutter +8 -0
- data/exe/lc +1 -0
- data/justfile +97 -0
- data/laser-cutter.gemspec +27 -14
- data/lib/laser/cutter/aggregator.rb +101 -0
- data/lib/laser/cutter/box.rb +251 -0
- data/lib/laser/cutter/cli/command.rb +46 -0
- data/lib/laser/cutter/cli/completion.rb +21 -0
- data/lib/laser/cutter/cli/config_file.rb +36 -0
- data/lib/laser/cutter/cli/examples.rb +47 -0
- data/lib/laser/cutter/cli/generate.rb +95 -0
- data/lib/laser/cutter/cli/help.rb +20 -0
- data/lib/laser/cutter/cli/page_sizes.rb +19 -0
- data/lib/laser/cutter/cli/version.rb +15 -0
- data/lib/laser/cutter/cli.rb +49 -0
- data/lib/laser/cutter/configuration.rb +107 -0
- data/lib/{laser-cutter → laser/cutter}/geometry/dimensions.rb +2 -3
- data/lib/{laser-cutter/geometry/shape → laser/cutter/geometry}/line.rb +19 -18
- data/lib/{laser-cutter → laser/cutter}/geometry/point.rb +4 -2
- data/lib/{laser-cutter/geometry/shape → laser/cutter/geometry}/rect.rb +6 -7
- data/lib/{laser-cutter → laser/cutter}/geometry/shape.rb +7 -7
- data/lib/{laser-cutter → laser/cutter}/geometry/tuple.rb +40 -36
- data/lib/laser/cutter/invalid_option.rb +8 -0
- data/lib/laser/cutter/launcher.rb +40 -0
- data/lib/laser/cutter/missing_option.rb +7 -0
- data/lib/laser/cutter/notching/base.rb +19 -0
- data/lib/{laser-cutter → laser/cutter}/notching/edge.rb +36 -20
- data/lib/laser/cutter/notching/infinite_iterator.rb +27 -0
- data/lib/laser/cutter/notching/path_generator.rb +187 -0
- data/lib/laser/cutter/notching/shift.rb +17 -0
- data/lib/laser/cutter/options.rb +154 -0
- data/lib/laser/cutter/page_manager.rb +50 -0
- data/lib/{laser-cutter → laser/cutter}/renderer/base.rb +10 -3
- data/lib/laser/cutter/renderer/box_renderer.rb +40 -0
- data/lib/laser/cutter/renderer/layout_renderer.rb +61 -0
- data/lib/laser/cutter/renderer/line_renderer.rb +21 -0
- data/lib/laser/cutter/renderer/meta_renderer.rb +83 -0
- data/lib/{laser-cutter → laser/cutter}/renderer/rect_renderer.rb +3 -1
- data/lib/laser/cutter/renderer/svg_renderer.rb +65 -0
- data/lib/laser/cutter/renderer.rb +24 -0
- data/lib/laser/cutter/types.rb +57 -0
- data/lib/laser/cutter/units_converter.rb +15 -0
- data/lib/laser/cutter/version.rb +7 -0
- data/lib/laser/cutter/zero_value_not_allowed.rb +7 -0
- data/lib/laser/cutter.rb +67 -0
- data/lib/laser-cutter.rb +2 -14
- data/lib/laser_cutter.rb +3 -0
- data/spec/laser/cutter/aggregator_spec.rb +70 -0
- data/spec/laser/cutter/box_lid_spec.rb +142 -0
- data/spec/laser/cutter/box_spec.rb +61 -0
- data/spec/laser/cutter/cli/config_file_spec.rb +31 -0
- data/spec/laser/cutter/cli_spec.rb +213 -0
- data/spec/laser/cutter/configuration_spec.rb +118 -0
- data/spec/{dimensions_spec.rb → laser/cutter/dimensions_spec.rb} +8 -6
- data/spec/laser/cutter/edge_spec.rb +64 -0
- data/spec/{line_spec.rb → laser/cutter/line_spec.rb} +26 -20
- data/spec/laser/cutter/options_spec.rb +120 -0
- data/spec/{page_manager_spec.rb → laser/cutter/page_manager_spec.rb} +27 -11
- data/spec/{path_generator_spec.rb → laser/cutter/path_generator_spec.rb} +18 -14
- data/spec/{point_spec.rb → laser/cutter/point_spec.rb} +27 -22
- data/spec/{rect_spec.rb → laser/cutter/rect_spec.rb} +9 -5
- data/spec/laser/cutter/renderer/layout_renderer_spec.rb +85 -0
- data/spec/laser/cutter/renderer/svg_renderer_spec.rb +51 -0
- data/spec/laser/cutter/renderer_spec.rb +19 -0
- data/spec/laser/cutter_spec.rb +114 -0
- data/spec/spec_helper.rb +35 -22
- data/spec/support/aruba.rb +10 -0
- data/spec/support/outline.rb +53 -0
- metadata +230 -76
- data/.travis.yml +0 -26
- data/BOXMAKER.md +0 -51
- data/LICENSE +0 -22
- data/bin/laser-cutter +0 -19
- data/lib/laser-cutter/aggregator.rb +0 -57
- data/lib/laser-cutter/box.rb +0 -183
- data/lib/laser-cutter/cli/opt_parser.rb +0 -131
- data/lib/laser-cutter/cli/serializer.rb +0 -51
- data/lib/laser-cutter/configuration.rb +0 -83
- data/lib/laser-cutter/face.rb +0 -18
- data/lib/laser-cutter/geometry.rb +0 -11
- data/lib/laser-cutter/notching/path_generator.rb +0 -234
- data/lib/laser-cutter/notching.rb +0 -9
- data/lib/laser-cutter/page_manager.rb +0 -43
- data/lib/laser-cutter/renderer/box_renderer.rb +0 -36
- data/lib/laser-cutter/renderer/layout_renderer.rb +0 -59
- data/lib/laser-cutter/renderer/line_renderer.rb +0 -17
- data/lib/laser-cutter/renderer/meta_renderer.rb +0 -77
- data/lib/laser-cutter/renderer.rb +0 -14
- data/lib/laser-cutter/version.rb +0 -5
- data/spec/aggregator_spec.rb +0 -65
- data/spec/box_spec.rb +0 -39
- data/spec/configuration_spec.rb +0 -81
- data/spec/edge_spec.rb +0 -43
- data/spec/renderer_spec.rb +0 -72
- /data/docs/{comparison.jpg → images/comparison.jpg} +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
|
-
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 140ec85d7822779ce6cf52830e4d462252bda1d9b03f0f45c433a68c381e08ac
|
|
4
|
+
data.tar.gz: 6f103d783a2b0de67eeba6491fac047029e52fec5ffe824dd0508481d8fcd41a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1e465b0c64a35fbbc397e0d98fccb520ae1fb0e8bb6fc073be86cf4fa80b141461ee238f7dcd8d77e8380f9c4043ac86a86efbecf9a380a4b1db2bcd0a7a0fd5
|
|
7
|
+
data.tar.gz: 9946ba8635c743ca01a7d458d3622037ea7bf811631bcda87c96a8c94398941ff6931809a46ec57624eefd1d1827036fff50155b8dd8492d60b23265bb3f1376
|
data/.envrc
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
name: Ruby
|
|
2
|
+
|
|
3
|
+
on: [push,pull_request]
|
|
4
|
+
|
|
5
|
+
jobs:
|
|
6
|
+
build:
|
|
7
|
+
runs-on: ubuntu-latest
|
|
8
|
+
strategy:
|
|
9
|
+
matrix:
|
|
10
|
+
ruby-version: [4.0.6]
|
|
11
|
+
|
|
12
|
+
steps:
|
|
13
|
+
- uses: actions/checkout@v2
|
|
14
|
+
- name: Set up Ruby ${{ matrix.ruby-version }}
|
|
15
|
+
uses: ruby/setup-ruby@v1
|
|
16
|
+
with:
|
|
17
|
+
ruby-version: ${{ matrix.ruby-version }}
|
|
18
|
+
bundler-cache: true
|
|
19
|
+
- name: Run Specs
|
|
20
|
+
run: |
|
|
21
|
+
bundle check || bundle install -j 4
|
|
22
|
+
bundle exec rubocop
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
name: Ruby
|
|
2
|
+
|
|
3
|
+
on: [push,pull_request]
|
|
4
|
+
|
|
5
|
+
jobs:
|
|
6
|
+
build:
|
|
7
|
+
runs-on: ubuntu-latest
|
|
8
|
+
strategy:
|
|
9
|
+
matrix:
|
|
10
|
+
ruby-version: [4.0.6]
|
|
11
|
+
|
|
12
|
+
steps:
|
|
13
|
+
- uses: actions/checkout@v2
|
|
14
|
+
- name: Set up Ruby ${{ matrix.ruby-version }}
|
|
15
|
+
uses: ruby/setup-ruby@v1
|
|
16
|
+
with:
|
|
17
|
+
ruby-version: ${{ matrix.ruby-version }}
|
|
18
|
+
bundler-cache: true
|
|
19
|
+
- name: Run Specs
|
|
20
|
+
run: |
|
|
21
|
+
bundle check || bundle install -j 4
|
|
22
|
+
bundle exec rspec --format documentation
|
data/.gitignore
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
**.pdf
|
|
2
2
|
**.DS_Store
|
|
3
|
+
.rspec_status
|
|
3
4
|
**/*.swp
|
|
4
5
|
*.gem
|
|
5
6
|
*.rbc
|
|
@@ -9,8 +10,6 @@
|
|
|
9
10
|
Gemfile.lock
|
|
10
11
|
InstalledFiles
|
|
11
12
|
_yardoc
|
|
12
|
-
coverage
|
|
13
|
-
lib/bundler/man
|
|
14
13
|
pkg
|
|
15
14
|
rdoc
|
|
16
15
|
spec/reports
|
|
@@ -23,6 +22,7 @@ tmp
|
|
|
23
22
|
*.a
|
|
24
23
|
mkmf.log
|
|
25
24
|
.idea/
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
25
|
+
.ruby-lsp
|
|
26
|
+
/coverage
|
|
27
|
+
!docs/coverage/
|
|
28
|
+
/doc
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# 001.00 Migrate the CLI to dry-cli, load with Zeitwerk
|
|
2
|
+
|
|
3
|
+
Branch `kig/migrate-to-dry-cli`, off `kig/2.0.0-alpha`; the PR targets `kig/2.0.0-alpha`.
|
|
4
|
+
|
|
5
|
+
## Decisions
|
|
6
|
+
|
|
7
|
+
- `lib/laser_cutter/` moves to `lib/laser/cutter/`, loaded by `Zeitwerk::Loader.for_gem_extension(Laser)`, eager-loaded. `lib/laser-cutter.rb` and `lib/laser_cutter.rb` remain as shims.
|
|
8
|
+
|
|
9
|
+
- `generate` takes height as `-H` / `--height`: dry-cli owns `-h`, and `-v` means verbose. `version` answers to `-V` / `--version`.
|
|
10
|
+
|
|
11
|
+
- Ruby 4.0.6, pinned in `.ruby-version`.
|
|
12
|
+
|
|
13
|
+
- The executables move from `bin/` to `exe/` and call `Laser::Cutter::Launcher`.
|
|
14
|
+
|
|
15
|
+
- Help and boxes share one width, 90 columns or fewer; a progress bar is 60 cells.
|
|
16
|
+
|
|
17
|
+
- `--inside-box` draws the unkerfed outline, which `--debug` used to do by accident; `--debug` is gone, `--verbose` prints the backtrace.
|
|
18
|
+
|
|
19
|
+
## Work
|
|
20
|
+
|
|
21
|
+
- [x] Zeitwerk: one constant per file (split `configuration.rb` and `path_generator.rb`, move `geometry/shape/{line,rect}.rb` up), eager load
|
|
22
|
+
- [x] dry-cli with dry-cli-help, dry-cli-ui and dry-cli-autocomplete; commands `generate`, `page-sizes`, `examples`, `help`, `version`, `completion`
|
|
23
|
+
- [x] `generate -f/--format pdf|svg`, PDF by default
|
|
24
|
+
- [x] Green progress bar, at most 60 cells wide, advancing once per line drawn
|
|
25
|
+
- [x] SVG output through `victor`
|
|
26
|
+
- [x] Launcher plus in-process Aruba suite
|
|
27
|
+
- [x] Bug: a notch that divides the sides exactly (4x3x2, notch 0.5) makes adjacent sides mismatch
|
|
28
|
+
- [x] README and CLAUDE.md updated
|
|
29
|
+
- [x] Bug: PageManager read the default Symbol units as millimeters
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# 002.00 Lid options: notched on one side, or plain
|
|
2
|
+
|
|
3
|
+
Branch `kig/add-lid-options`, off `master`; the PR targets `master`. Released as 2.0.1.
|
|
4
|
+
|
|
5
|
+
## Goal
|
|
6
|
+
|
|
7
|
+
A box whose lid lifts off. Today the lid (the `top` panel) is notched on all four sides and can only be glued shut.
|
|
8
|
+
|
|
9
|
+
## Decisions
|
|
10
|
+
|
|
11
|
+
Answered by Konstantin on 2026-09-30:
|
|
12
|
+
|
|
13
|
+
- The lid rests on top of the walls. Where a lid edge is plain, the wall edge under it is straight at the inner height `H`, and the lid reaches the outer footprint.
|
|
14
|
+
- The lid notched on one side joins the **back** wall, always.
|
|
15
|
+
- The option is `--lid full|back|plain`, `-L`, `full` by default. `full` draws exactly what 2.0.0 drew.
|
|
16
|
+
|
|
17
|
+
Decided without an answer (he had gone to bed), so worth a look in review:
|
|
18
|
+
|
|
19
|
+
- In `back` and `plain` the lid owns all four top corner squares, so no wall rises above `H` at a corner. In `back` the lid's two back corners are the feet either side of its notched edge.
|
|
20
|
+
- The internal height stays `H` in every mode: the underside of the lid is at `H`.
|
|
21
|
+
- The layout does not move. A wall without its tabs leaves its old space unused.
|
|
22
|
+
|
|
23
|
+
## Geometry
|
|
24
|
+
|
|
25
|
+
Faces are laid out in a column (`top`, `front`, `bottom`, `back`) with `left` and `right` beside `front`. A `Rect`'s sides run 0 bottom, 1 right, 2 top, 3 left. The lid's joints:
|
|
26
|
+
|
|
27
|
+
| Lid side | Wall | Wall side |
|
|
28
|
+
| :------- | :------ | :-------- |
|
|
29
|
+
| 0 | `back` | 2 |
|
|
30
|
+
| 1 | `right` | 0 |
|
|
31
|
+
| 2 | `front` | 0 |
|
|
32
|
+
| 3 | `left` | 0 |
|
|
33
|
+
|
|
34
|
+
- A plain wall edge is the edge's **inside** line. The sides next to it lose the corner box, and its kerf fix-ups, at that end only.
|
|
35
|
+
- A plain lid edge is the edge's **outside** line.
|
|
36
|
+
- Kerf grows every outline by half the kerf, as it does for notches.
|
|
37
|
+
|
|
38
|
+
## Work
|
|
39
|
+
|
|
40
|
+
- [x] `Configuration`: `lid`, default `full`, rejected by `validate!` when unknown
|
|
41
|
+
- [x] `Notching::Edge` and `PathGenerator`: a corner box per end, not per edge
|
|
42
|
+
- [x] `Box`: per-face outlines; plain and back lids; straight wall edges
|
|
43
|
+
- [x] `generate --lid`, README, CLAUDE.md
|
|
44
|
+
- [x] Specs: each outline against an independent even-odd oracle, with and without kerf
|
|
45
|
+
- [x] Version 2.0.1
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# 003.00 A Ruby API for MakeABox.io
|
|
2
|
+
|
|
3
|
+
Branch `kig/add-ruby-api-facade`, off `master` once 002.00 had merged; the API carries the lid that plan added.
|
|
4
|
+
|
|
5
|
+
## Goal
|
|
6
|
+
|
|
7
|
+
MakeABox.io, a Rails application, draws boxes in-process. It must not shell out to `laser-cutter`, and it must be able to ask for any lid.
|
|
8
|
+
|
|
9
|
+
## What 2.0.0 already allowed
|
|
10
|
+
|
|
11
|
+
`Configuration.new(hash)`, `validate!`, `Renderer::LayoutRenderer.new(config).render` and `PageManager#page_size_values` are what the website calls today against 1.0.3, and all still work. Two things were missing:
|
|
12
|
+
|
|
13
|
+
- A renderer could only write to `config.file`. A controller wants the bytes.
|
|
14
|
+
- `validate!` demanded `file`, so a caller with no file had to invent one.
|
|
15
|
+
- `Configuration` is a Mash: it takes any key and any value, and says nothing until a renderer trips over it.
|
|
16
|
+
|
|
17
|
+
## Decisions
|
|
18
|
+
|
|
19
|
+
- Asked for by Konstantin mid-way: the facade takes every option as a **typed class**. That is `Laser::Cutter::Options`, a strict `Dry::Struct` with its types in `Laser::Cutter::Types`. dry-struct becomes a runtime dependency.
|
|
20
|
+
- `Laser::Cutter.render(options)` returns the document as a String. No file is needed or written.
|
|
21
|
+
- `Laser::Cutter.write(options)` writes it to `options.file`. Without a format, the extension of the file decides.
|
|
22
|
+
- Both take an `Options`, or a Hash they turn into one, and pass a block through as the per-line callback.
|
|
23
|
+
- `Options` coerces Strings, reads a blank String as left out, and refuses unknown keys. A missing dimension raises `MissingOption`, with the message `Configuration#validate!` gives, since the website rewrites that message. Anything else raises `InvalidOption`.
|
|
24
|
+
- The `--box` shorthand is not an attribute: it is a way to type four options, not a fifth.
|
|
25
|
+
- Renderers answer `document`, the String; `render` writes it to `config.file` as before.
|
|
26
|
+
- `Configuration` also reads a blank lid as the default, for callers still building one by hand.
|
|
27
|
+
- "The two lid types (or none)" is read as `back`, `plain`, or no lid setting at all, which is `full`. A box with no lid panel is not part of this.
|
|
28
|
+
- Requiring the gem still loads dry-cli and the commands. Harmless in a Rails process; splitting the CLI out of the default require is left for later.
|
|
29
|
+
- The command line keeps building a `Configuration` itself; it is not moved onto `Options` here.
|
|
30
|
+
|
|
31
|
+
## Work
|
|
32
|
+
|
|
33
|
+
- [x] `document` on both renderers
|
|
34
|
+
- [x] `Types` and `Options`
|
|
35
|
+
- [x] `Laser::Cutter.render` and `Laser::Cutter.write`
|
|
36
|
+
- [x] Specs, including Strings from a form and the calls the website makes today
|
|
37
|
+
- [x] README section, CLAUDE.md
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# Relaxed.Ruby.Style
|
|
2
|
+
## Version 2.5
|
|
3
|
+
|
|
4
|
+
Style/Alias:
|
|
5
|
+
Enabled: false
|
|
6
|
+
StyleGuide: https://relaxed.ruby.style/#stylealias
|
|
7
|
+
|
|
8
|
+
Style/AsciiComments:
|
|
9
|
+
Enabled: false
|
|
10
|
+
StyleGuide: https://relaxed.ruby.style/#styleasciicomments
|
|
11
|
+
|
|
12
|
+
Style/BeginBlock:
|
|
13
|
+
Enabled: false
|
|
14
|
+
StyleGuide: https://relaxed.ruby.style/#stylebeginblock
|
|
15
|
+
|
|
16
|
+
Style/BlockDelimiters:
|
|
17
|
+
Enabled: false
|
|
18
|
+
StyleGuide: https://relaxed.ruby.style/#styleblockdelimiters
|
|
19
|
+
|
|
20
|
+
Style/CommentAnnotation:
|
|
21
|
+
Enabled: false
|
|
22
|
+
StyleGuide: https://relaxed.ruby.style/#stylecommentannotation
|
|
23
|
+
|
|
24
|
+
Style/Documentation:
|
|
25
|
+
Enabled: false
|
|
26
|
+
StyleGuide: https://relaxed.ruby.style/#styledocumentation
|
|
27
|
+
|
|
28
|
+
Layout/DotPosition:
|
|
29
|
+
Enabled: false
|
|
30
|
+
StyleGuide: https://relaxed.ruby.style/#layoutdotposition
|
|
31
|
+
|
|
32
|
+
Style/DoubleNegation:
|
|
33
|
+
Enabled: false
|
|
34
|
+
StyleGuide: https://relaxed.ruby.style/#styledoublenegation
|
|
35
|
+
|
|
36
|
+
Style/EndBlock:
|
|
37
|
+
Enabled: false
|
|
38
|
+
StyleGuide: https://relaxed.ruby.style/#styleendblock
|
|
39
|
+
|
|
40
|
+
Style/FormatString:
|
|
41
|
+
Enabled: false
|
|
42
|
+
StyleGuide: https://relaxed.ruby.style/#styleformatstring
|
|
43
|
+
|
|
44
|
+
Style/IfUnlessModifier:
|
|
45
|
+
Enabled: false
|
|
46
|
+
StyleGuide: https://relaxed.ruby.style/#styleifunlessmodifier
|
|
47
|
+
|
|
48
|
+
Style/Lambda:
|
|
49
|
+
Enabled: false
|
|
50
|
+
StyleGuide: https://relaxed.ruby.style/#stylelambda
|
|
51
|
+
|
|
52
|
+
Style/ModuleFunction:
|
|
53
|
+
Enabled: false
|
|
54
|
+
StyleGuide: https://relaxed.ruby.style/#stylemodulefunction
|
|
55
|
+
|
|
56
|
+
Style/MultilineBlockChain:
|
|
57
|
+
Enabled: false
|
|
58
|
+
StyleGuide: https://relaxed.ruby.style/#stylemultilineblockchain
|
|
59
|
+
|
|
60
|
+
Style/NegatedIf:
|
|
61
|
+
Enabled: false
|
|
62
|
+
StyleGuide: https://relaxed.ruby.style/#stylenegatedif
|
|
63
|
+
|
|
64
|
+
Style/NegatedWhile:
|
|
65
|
+
Enabled: false
|
|
66
|
+
StyleGuide: https://relaxed.ruby.style/#stylenegatedwhile
|
|
67
|
+
|
|
68
|
+
Style/NumericPredicate:
|
|
69
|
+
Enabled: false
|
|
70
|
+
StyleGuide: https://relaxed.ruby.style/#stylenumericpredicate
|
|
71
|
+
|
|
72
|
+
Style/ParallelAssignment:
|
|
73
|
+
Enabled: false
|
|
74
|
+
StyleGuide: https://relaxed.ruby.style/#styleparallelassignment
|
|
75
|
+
|
|
76
|
+
Style/PercentLiteralDelimiters:
|
|
77
|
+
Enabled: false
|
|
78
|
+
StyleGuide: https://relaxed.ruby.style/#stylepercentliteraldelimiters
|
|
79
|
+
|
|
80
|
+
Style/PerlBackrefs:
|
|
81
|
+
Enabled: false
|
|
82
|
+
StyleGuide: https://relaxed.ruby.style/#styleperlbackrefs
|
|
83
|
+
|
|
84
|
+
Style/Semicolon:
|
|
85
|
+
Enabled: false
|
|
86
|
+
StyleGuide: https://relaxed.ruby.style/#stylesemicolon
|
|
87
|
+
|
|
88
|
+
Style/SignalException:
|
|
89
|
+
Enabled: false
|
|
90
|
+
StyleGuide: https://relaxed.ruby.style/#stylesignalexception
|
|
91
|
+
|
|
92
|
+
Style/SingleLineBlockParams:
|
|
93
|
+
Enabled: false
|
|
94
|
+
StyleGuide: https://relaxed.ruby.style/#stylesinglelineblockparams
|
|
95
|
+
|
|
96
|
+
Style/SingleLineMethods:
|
|
97
|
+
Enabled: false
|
|
98
|
+
StyleGuide: https://relaxed.ruby.style/#stylesinglelinemethods
|
|
99
|
+
|
|
100
|
+
Layout/SpaceBeforeBlockBraces:
|
|
101
|
+
Enabled: false
|
|
102
|
+
StyleGuide: https://relaxed.ruby.style/#layoutspacebeforeblockbraces
|
|
103
|
+
|
|
104
|
+
Layout/SpaceInsideParens:
|
|
105
|
+
Enabled: false
|
|
106
|
+
StyleGuide: https://relaxed.ruby.style/#layoutspaceinsideparens
|
|
107
|
+
|
|
108
|
+
Style/SpecialGlobalVars:
|
|
109
|
+
Enabled: false
|
|
110
|
+
StyleGuide: https://relaxed.ruby.style/#stylespecialglobalvars
|
|
111
|
+
|
|
112
|
+
Style/StringLiterals:
|
|
113
|
+
Enabled: false
|
|
114
|
+
StyleGuide: https://relaxed.ruby.style/#stylestringliterals
|
|
115
|
+
|
|
116
|
+
Style/TrailingCommaInArguments:
|
|
117
|
+
Enabled: false
|
|
118
|
+
StyleGuide: https://relaxed.ruby.style/#styletrailingcommainarguments
|
|
119
|
+
|
|
120
|
+
Style/TrailingCommaInArrayLiteral:
|
|
121
|
+
Enabled: false
|
|
122
|
+
StyleGuide: https://relaxed.ruby.style/#styletrailingcommainarrayliteral
|
|
123
|
+
|
|
124
|
+
Style/TrailingCommaInHashLiteral:
|
|
125
|
+
Enabled: false
|
|
126
|
+
StyleGuide: https://relaxed.ruby.style/#styletrailingcommainhashliteral
|
|
127
|
+
|
|
128
|
+
Style/SymbolArray:
|
|
129
|
+
Enabled: false
|
|
130
|
+
StyleGuide: http://relaxed.ruby.style/#stylesymbolarray
|
|
131
|
+
|
|
132
|
+
Style/WhileUntilModifier:
|
|
133
|
+
Enabled: false
|
|
134
|
+
StyleGuide: https://relaxed.ruby.style/#stylewhileuntilmodifier
|
|
135
|
+
|
|
136
|
+
Style/WordArray:
|
|
137
|
+
Enabled: false
|
|
138
|
+
StyleGuide: https://relaxed.ruby.style/#stylewordarray
|
|
139
|
+
|
|
140
|
+
Lint/AmbiguousRegexpLiteral:
|
|
141
|
+
Enabled: false
|
|
142
|
+
StyleGuide: https://relaxed.ruby.style/#lintambiguousregexpliteral
|
|
143
|
+
|
|
144
|
+
Lint/AssignmentInCondition:
|
|
145
|
+
Enabled: false
|
|
146
|
+
StyleGuide: https://relaxed.ruby.style/#lintassignmentincondition
|
|
147
|
+
|
|
148
|
+
Layout/LineLength:
|
|
149
|
+
Enabled: false
|
|
150
|
+
|
|
151
|
+
Metrics:
|
|
152
|
+
Enabled: false
|
|
153
|
+
|
data/.rspec
CHANGED
data/.rubocop.yml
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
inherit_from:
|
|
2
|
+
- .rubocop_todo.yml
|
|
3
|
+
- .relaxed_rubocop.yml
|
|
4
|
+
|
|
5
|
+
AllCops:
|
|
6
|
+
NewCops: enable
|
|
7
|
+
SuggestExtensions: false
|
|
8
|
+
TargetRubyVersion: 4.0
|
|
9
|
+
|
|
10
|
+
Layout/LineLength:
|
|
11
|
+
Max: 120
|
|
12
|
+
|
|
13
|
+
Metrics/BlockLength:
|
|
14
|
+
Exclude:
|
|
15
|
+
- "spec/**/*"
|
|
16
|
+
|
|
17
|
+
Lint/ConstantDefinitionInBlock:
|
|
18
|
+
Enabled: false
|
|
19
|
+
|
|
20
|
+
Lint/StructNewOverride:
|
|
21
|
+
Enabled: false
|
|
22
|
+
|
|
23
|
+
Layout/HashAlignment:
|
|
24
|
+
EnforcedColonStyle: table
|
|
25
|
+
EnforcedHashRocketStyle: table
|
|
26
|
+
|
|
27
|
+
Layout/ArgumentAlignment:
|
|
28
|
+
EnforcedStyle: with_first_argument
|
|
29
|
+
|
|
30
|
+
Naming/MethodParameterName:
|
|
31
|
+
Enabled: false
|
data/.rubocop_todo.yml
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# This configuration was generated by
|
|
2
|
+
# `rubocop --auto-gen-config`
|
|
3
|
+
# on 2026-09-29 22:33:21 UTC using RuboCop version 1.91.0.
|
|
4
|
+
# The point is for the user to remove these configuration records
|
|
5
|
+
# one by one as the offenses are removed from the code base.
|
|
6
|
+
# Note that changes in the inspected code, or installation of new
|
|
7
|
+
# versions of RuboCop, may require this file to be generated again.
|
|
8
|
+
|
|
9
|
+
# Offense count: 1
|
|
10
|
+
Gemspec/RequiredRubyVersion:
|
|
11
|
+
Exclude:
|
|
12
|
+
- 'laser-cutter.gemspec'
|
|
13
|
+
|
|
14
|
+
# Offense count: 1
|
|
15
|
+
# This cop supports safe autocorrection (--autocorrect).
|
|
16
|
+
Lint/CopDirectiveSyntax:
|
|
17
|
+
Exclude:
|
|
18
|
+
- 'spec/spec_helper.rb'
|
|
19
|
+
|
|
20
|
+
# Offense count: 1
|
|
21
|
+
# Configuration parameters: DelegatingMethods, AllowedCrossFilePaths.
|
|
22
|
+
# DelegatingMethods: delegate
|
|
23
|
+
Lint/DuplicateMethods:
|
|
24
|
+
Exclude:
|
|
25
|
+
- 'lib/laser/cutter/geometry/shape.rb'
|
|
26
|
+
|
|
27
|
+
# Offense count: 2
|
|
28
|
+
# Configuration parameters: AllowedParentClasses.
|
|
29
|
+
Lint/MissingSuper:
|
|
30
|
+
Exclude:
|
|
31
|
+
- 'lib/laser/cutter/geometry/line.rb'
|
|
32
|
+
- 'lib/laser/cutter/renderer/meta_renderer.rb'
|
|
33
|
+
|
|
34
|
+
# Offense count: 1
|
|
35
|
+
# This cop supports safe autocorrection (--autocorrect).
|
|
36
|
+
Lint/RedundantCopDisableDirective:
|
|
37
|
+
Exclude:
|
|
38
|
+
- 'spec/spec_helper.rb'
|
|
39
|
+
|
|
40
|
+
# Offense count: 3
|
|
41
|
+
Lint/RescueException:
|
|
42
|
+
Exclude:
|
|
43
|
+
- 'lib/laser/cutter/cli/serializer.rb'
|
|
44
|
+
- 'spec/laser/cutter/renderer/layout_renderer_spec.rb'
|
|
45
|
+
|
|
46
|
+
# Offense count: 1
|
|
47
|
+
# Configuration parameters: ExpectMatchingDefinition, CheckDefinitionPathHierarchy, CheckDefinitionPathHierarchyRoots, Regex, IgnoreExecutableScripts, AllowedAcronyms.
|
|
48
|
+
# CheckDefinitionPathHierarchyRoots: lib, spec, test, src
|
|
49
|
+
# AllowedAcronyms: CLI, DSL, ACL, API, ASCII, CPU, CSS, DNS, EOF, GUID, HTML, HTTP, HTTPS, ID, IP, JSON, LHS, QPS, RAM, RHS, RPC, SLA, SMTP, SQL, SSH, TCP, TLS, TTL, UDP, UI, UID, UUID, URI, URL, UTF8, VM, XML, XMPP, XSRF, XSS
|
|
50
|
+
Naming/FileName:
|
|
51
|
+
Exclude:
|
|
52
|
+
- 'Rakefile.rb'
|
|
53
|
+
- 'lib/laser-cutter.rb'
|
|
54
|
+
|
|
55
|
+
# Offense count: 3
|
|
56
|
+
# Configuration parameters: ForbiddenDelimiters.
|
|
57
|
+
# ForbiddenDelimiters: (?i-mx:(^|\s)(EO[A-Z]{1}|END)(\s|$))
|
|
58
|
+
Naming/HeredocDelimiterNaming:
|
|
59
|
+
Exclude:
|
|
60
|
+
- 'lib/laser/cutter/cli/opt_parser.rb'
|
|
61
|
+
- 'lib/laser/cutter/renderer/meta_renderer.rb'
|
|
62
|
+
|
|
63
|
+
# Offense count: 3
|
|
64
|
+
# This cop supports safe autocorrection (--autocorrect).
|
|
65
|
+
# Configuration parameters: MaxUnannotatedPlaceholdersAllowed, Mode, AllowedMethods, AllowedPatterns.
|
|
66
|
+
# SupportedStyles: annotated, template, unannotated
|
|
67
|
+
Style/FormatStringToken:
|
|
68
|
+
EnforcedStyle: unannotated
|
|
69
|
+
|
|
70
|
+
# Offense count: 1
|
|
71
|
+
# This cop supports unsafe autocorrection (--autocorrect-all).
|
|
72
|
+
# Configuration parameters: EnforcedStyle.
|
|
73
|
+
# SupportedStyles: always, always_true, never
|
|
74
|
+
Style/FrozenStringLiteralComment:
|
|
75
|
+
Exclude:
|
|
76
|
+
- '**/*.arb'
|
|
77
|
+
- 'lib/laser/cutter/page_manager.rb'
|
|
78
|
+
|
|
79
|
+
# Offense count: 1
|
|
80
|
+
# This cop supports unsafe autocorrection (--autocorrect-all).
|
|
81
|
+
Style/MapToHash:
|
|
82
|
+
Exclude:
|
|
83
|
+
- 'lib/laser/cutter/configuration.rb'
|
|
84
|
+
|
|
85
|
+
# Offense count: 1
|
|
86
|
+
# This cop supports unsafe autocorrection (--autocorrect-all).
|
|
87
|
+
# Configuration parameters: Mode.
|
|
88
|
+
Style/StringConcatenation:
|
|
89
|
+
Exclude:
|
|
90
|
+
- 'lib/laser/cutter/renderer/meta_renderer.rb'
|
data/.ruby-version
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
4.0.6
|
data/CLAUDE.md
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
4
|
+
|
|
5
|
+
## What this is
|
|
6
|
+
|
|
7
|
+
`laser-cutter` is a Ruby gem and CLI that generates a PDF of the six notched panels of a box. You cut the panels on a laser cutter and they snap together. Prawn renders the PDF. The notch algorithm favors **symmetric** panels over identical notch lengths.
|
|
8
|
+
|
|
9
|
+
## Commands
|
|
10
|
+
|
|
11
|
+
The `justfile` drives everything. Its recipes wrap `bundle exec` in `eval "$(rbenv init - bash)"`. `.ruby-version` and CI pin Ruby **4.0.6**; dry-cli-help and dry-cli-ui need 4.0.
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
just lint # rubocop
|
|
15
|
+
just format # rubocop -a, plus mdformat on every *.md
|
|
16
|
+
just test # rspec (progress format, random order)
|
|
17
|
+
just test spec/laser/cutter/edge_spec.rb:42 # one file or example
|
|
18
|
+
just test-docs # rspec --format documentation
|
|
19
|
+
just ci # lint + test-coverage
|
|
20
|
+
just doc # YARD docs
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
- `bundle exec rspec`, `bundle exec rubocop` and `bundle exec rake` (default task `spec`) also work.
|
|
24
|
+
- Every rspec run rewrites `docs/badges/coverage_badge.svg` through the SimpleCov `at_exit` hook in `spec/spec_helper.rb`. That file shows up in `git status` after any test run.
|
|
25
|
+
- `spec_helper.rb` fails the run under 95% line coverage. The Aruba suite in `spec/laser/cutter/cli_spec.rb` counts towards it.
|
|
26
|
+
|
|
27
|
+
## Architecture
|
|
28
|
+
|
|
29
|
+
Everything lives under `Laser::Cutter`, in `lib/laser/cutter/`. `lib/laser/cutter.rb` requires the third-party gems, then sets up `Zeitwerk::Loader.for_gem_extension(Laser)` and **eager-loads** it. So: one constant per file, the file name matches the constant, and no `require` between the gem's own files. `lib/laser-cutter.rb` and `lib/laser_cutter.rb` are shims.
|
|
30
|
+
|
|
31
|
+
Pipeline, from config to file:
|
|
32
|
+
|
|
33
|
+
1. **`Configuration`** is a `Hashie::Mash` with symbolized keys. It parses the `--box WxHxD/T[/N]` shorthand, casts numeric strings to floats, and merges per-unit defaults for kerf, margin, padding and stroke. Notch defaults to `3 × thickness`. `validate!` raises `MissingOption` / `ZeroValueNotAllowed`, and `InvalidOption` for a lid it does not know. `units` defaults to the Symbol `:in`, and arrives as a String from the command line, so compare it with `to_s` or `to_sym`.
|
|
34
|
+
1. **`Renderer.for(format, config)`** picks `LayoutRenderer` (PDF, Prawn) or `SvgRenderer` (SVG, Victor). Both answer `total` (lines to draw) and `render { |line| }`, which yields after each line. That block drives the progress bar.
|
|
35
|
+
- `LayoutRenderer` draws a `BoxRenderer`, a `MetaRenderer` when `config.metadata` is set, and a second red `BoxRenderer` without kerf when `config.debug` is set. It sizes the page from the box enclosure unless `page_size` is given.
|
|
36
|
+
- `SvgRenderer` fits the page to the box, flips y (SVG counts down from the top), and writes the metadata as a `<desc>`.
|
|
37
|
+
1. **`Box`** models the six faces as `Geometry::Rect`s. `position_faces!` lays them out in a cross (see the ASCII diagram in that method). `generate_notches` pairs each side of a face with the matching side of its outer bounding rect (face grown by `thickness`) as a **`Notching::Edge`**. The `conf` table sets per-face alignment: `valign`/`halign` decide whether a side's center notch points `:out` or `:in`. `corners` plus `pick_corners_face` decide which face fills the corner squares.
|
|
38
|
+
- `lid` (`full`, `back`, `plain`) sets how the `top` panel joins the walls. A plain lid edge is the edge's **outside** line, and the wall side under it (`LID_SIDES`) is the edge's **inside** line, so the lid lies on walls that end at the inner height. The lid then owns the corners above the walls: `corner_ends` strips the corner box from the wall sides that touch it.
|
|
39
|
+
- `outlines` holds the merged lines of each face by name; `notches` is all of them, flattened.
|
|
40
|
+
1. **`Notching::Edge`** holds the inside and outside lines of one side, both shifted by `kerf / 2`. `calculate_notch_width!` forces an **odd** notch count of at least 3 and recomputes the real notch width, so the requested notch is only a guide. It rounds `length / notch` to `RATIO_DIGITS` before `ceil`: two panels meeting at a joint must get the same count, and float noise used to split them when the notch divided the side exactly.
|
|
41
|
+
1. **`Notching::PathGenerator`** turns an `Edge` into `Geometry::Line`s. It zigzags between the inside and outside lines using `Shift` deltas from two alternating `InfiniteIterator`s. It widens or narrows notches by `kerf` and adds the corner boxes, at the ends the edge names in `corner_ends`. Kerf grows every outline by half the kerf on every side; `spec/laser/cutter/box_lid_spec.rb` checks exactly that, point by point, through `spec/support/outline.rb`.
|
|
42
|
+
1. **`Aggregator`** merges the lines of a face into the outline to cut. Neighbouring edges draw shared stretches twice, and a shared stretch runs through the material, so each collinear group is combined as a symmetric difference: a point is cut when an odd number of lines cover it. It groups lines by axis and offset and sweeps each group once, so a face takes n log n; the old pairwise version made a 100×80×60 box take 55 seconds.
|
|
43
|
+
|
|
44
|
+
Geometry is unitless, in the config's units. Conversion to PDF points happens only at render time (`value.send(:in)` / `.send(:mm)`). `PageManager#value_from_units` converts PDF points back.
|
|
45
|
+
|
|
46
|
+
### Ruby API
|
|
47
|
+
|
|
48
|
+
- `Laser::Cutter.render(options)` returns the document as a String, and `Laser::Cutter.write(options)` writes it to `options.file`. Both live in `lib/laser/cutter.rb`, take an `Options` or a Hash, and pass a block through as the per-line callback. MakeABox.io calls these; keep them working without a file, a terminal or the CLI classes.
|
|
49
|
+
- `Options` (`options.rb`) is a strict `Dry::Struct`, one attribute per `generate` option, with its types in `Types` (`types.rb`). It raises `MissingOption` for a missing dimension and `InvalidOption` for anything else, and turns into a `Configuration` with `to_configuration`. A new `generate` option needs an attribute here too.
|
|
50
|
+
- Renderers answer `document`, the String. `Renderer::Base#render` writes it to `config.file`.
|
|
51
|
+
|
|
52
|
+
### Command line
|
|
53
|
+
|
|
54
|
+
- `exe/laser-cutter` (and `exe/lc`) call `Laser::Cutter::Launcher.new(ARGV).execute!`. The Launcher takes argv, the three streams and `kernel`, and exits only through `kernel.exit`. Aruba runs it in-process (`spec/support/aruba.rb`), so commands must write to `out` and `err`, never to `$stdout`.
|
|
55
|
+
- `CLI` (`cli.rb`) is the dry-cli registry and the `Dry::CLI::Help.configure` block. Commands live in `cli/`: `generate`, `page-sizes`, `examples`, `help`, `version`, `completion`.
|
|
56
|
+
- Every message goes through `ui`: errors in `ui.error` boxes on STDERR (the Launcher draws them), `generate` opens with `ui.info` and closes with `ui.success`.
|
|
57
|
+
- `CLI::Command` is the base: it includes `Dry::CLI::UI`, declares `-v`, and offers `progress(label, total:)`, a green bar of 60 cells. Help and boxes share one width, `CLI.help_width`: the terminal's less 6, and 90 at most.
|
|
58
|
+
- `-h` belongs to help, so height is `-H`.
|
|
59
|
+
|
|
60
|
+
## Known gaps
|
|
61
|
+
|
|
62
|
+
- `generate` declares defaults for `--units` and `--page-layout`, and they override what `-R` reads from a saved configuration. `--lid` has no dry-cli default for that reason.
|
|
63
|
+
|
|
64
|
+
- `just build` is an empty recipe, so `just publish` builds nothing. `rake build` is the real build.
|
|
65
|
+
|
|
66
|
+
- The `Rakefile` YARD title is copied from another project, and it references a `CHANGELOG.md` that does not exist.
|
|
67
|
+
|
|
68
|
+
- The gemspec lists `tty-*` and `pastel` directly, though only dry-cli-ui uses them.
|
|
69
|
+
|
|
70
|
+
- `Box` still carries the comment "badly needs refactoring and tests".
|
|
71
|
+
|
|
72
|
+
## Conventions
|
|
73
|
+
|
|
74
|
+
- `.rubocop.yml` inherits `.relaxed_rubocop.yml` and `.rubocop_todo.yml`, with a 120-column line limit and table-aligned hashes.
|
|
75
|
+
- Specs mirror `lib/` under `spec/laser/cutter/` and use `rspec-its`, `disable_monkey_patching!` and random order.
|
data/CONTRIBUTING.md
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
Contributing to Laser-Cutter
|
|
2
|
-
======================
|
|
1
|
+
# Contributing to Laser-Cutter
|
|
3
2
|
|
|
4
3
|
You're encouraged to submit [pull requests](https://github.com/kigster/laser-cutter/pulls), [propose features and discuss issues](https://github.com/kigster/laser-cutter/issues).
|
|
5
4
|
|
|
@@ -34,8 +33,7 @@ bundle exec rake
|
|
|
34
33
|
|
|
35
34
|
#### Write Tests
|
|
36
35
|
|
|
37
|
-
Try to write a test that reproduces the problem you're trying to fix or describes a
|
|
38
|
-
feature that you want to build. Add to [spec/laser-cutter](spec/laser-cutter).
|
|
36
|
+
Try to write a test that reproduces the problem you're trying to fix or describes a feature that you want to build. Add to [spec/laser-cutter](spec/laser-cutter).
|
|
39
37
|
|
|
40
38
|
We definitely appreciate pull requests that highlight or reproduce a problem, even without a fix.
|
|
41
39
|
|
data/Gemfile
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
source 'https://rubygems.org'
|
|
2
4
|
|
|
3
5
|
# Specify your gem's dependencies in laser-cutter-cutter.gemspec
|
|
4
6
|
gemspec
|
|
5
7
|
|
|
8
|
+
group 'development', 'test' do
|
|
9
|
+
gem 'aruba'
|
|
10
|
+
gem 'coverage-badge'
|
|
11
|
+
gem 'rake'
|
|
12
|
+
gem 'rspec'
|
|
13
|
+
gem 'rspec-its'
|
|
14
|
+
gem 'rubocop', require: false
|
|
15
|
+
gem 'rubocop-rake', require: false
|
|
16
|
+
gem 'rubocop-rspec', require: false
|
|
17
|
+
gem 'simplecov'
|
|
18
|
+
gem 'yard'
|
|
19
|
+
end
|