weldbox 0.1.0__tar.gz

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 (116) hide show
  1. weldbox-0.1.0/.github/workflows/ci.yml +15 -0
  2. weldbox-0.1.0/.github/workflows/publish.yml +34 -0
  3. weldbox-0.1.0/.gitignore +7 -0
  4. weldbox-0.1.0/LICENSE +21 -0
  5. weldbox-0.1.0/PKG-INFO +13 -0
  6. weldbox-0.1.0/PRD.md +49 -0
  7. weldbox-0.1.0/README.md +146 -0
  8. weldbox-0.1.0/docs/FUNCTIONALITY.md +323 -0
  9. weldbox-0.1.0/docs/images/weldbox_sample_1.jpg +0 -0
  10. weldbox-0.1.0/docs/images/weldbox_sample_2.jpg +0 -0
  11. weldbox-0.1.0/docs/samples/fabtech/material_list.md +1 -0
  12. weldbox-0.1.0/docs/samples/oshcut/material_list.md +1 -0
  13. weldbox-0.1.0/docs/samples/rfmg/material_list.md +204 -0
  14. weldbox-0.1.0/docs/samples/rfmg/pipe_1.25_sch40.step +131 -0
  15. weldbox-0.1.0/docs/samples/rfmg/pipe_1.5_sch40.step +132 -0
  16. weldbox-0.1.0/docs/samples/rfmg/rect_2x1x0.125.step +781 -0
  17. weldbox-0.1.0/docs/samples/rfmg/rect_3x1.5x0.083.step +1518 -0
  18. weldbox-0.1.0/docs/samples/rfmg/rect_3x2x0.125.step +781 -0
  19. weldbox-0.1.0/docs/samples/rfmg/rect_4x2x0.120.step +1518 -0
  20. weldbox-0.1.0/docs/samples/rfmg/rect_4x2x0.188.step +1517 -0
  21. weldbox-0.1.0/docs/samples/rfmg/rect_4x3x0.250.step +1518 -0
  22. weldbox-0.1.0/docs/samples/rfmg/rect_al_2x1x0.065.step +781 -0
  23. weldbox-0.1.0/docs/samples/rfmg/rect_al_3x1x0.125.step +781 -0
  24. weldbox-0.1.0/docs/samples/rfmg/round_2x0.125.step +131 -0
  25. weldbox-0.1.0/docs/samples/rfmg/round_4130_1.25x0.065.step +245 -0
  26. weldbox-0.1.0/docs/samples/rfmg/round_4130_1.25x0.083.step +245 -0
  27. weldbox-0.1.0/docs/samples/rfmg/round_4130_1.5x0.083.step +245 -0
  28. weldbox-0.1.0/docs/samples/rfmg/round_4130_1.5x0.095.step +245 -0
  29. weldbox-0.1.0/docs/samples/rfmg/round_4130_1.5x0.120.step +245 -0
  30. weldbox-0.1.0/docs/samples/rfmg/round_4130_1.75x0.095.step +245 -0
  31. weldbox-0.1.0/docs/samples/rfmg/round_4130_1.75x0.120.step +245 -0
  32. weldbox-0.1.0/docs/samples/rfmg/round_4130_1x0.049.step +244 -0
  33. weldbox-0.1.0/docs/samples/rfmg/round_4130_1x0.065.step +244 -0
  34. weldbox-0.1.0/docs/samples/rfmg/round_al_1.5x0.065.step +245 -0
  35. weldbox-0.1.0/docs/samples/rfmg/round_al_1.5x0.125.step +245 -0
  36. weldbox-0.1.0/docs/samples/rfmg/round_al_1x0.065.step +244 -0
  37. weldbox-0.1.0/docs/samples/rfmg/round_al_2x0.250.step +131 -0
  38. weldbox-0.1.0/docs/samples/rfmg/round_dom_1.75x0.065.step +245 -0
  39. weldbox-0.1.0/docs/samples/rfmg/round_dom_1.75x0.083.step +245 -0
  40. weldbox-0.1.0/docs/samples/rfmg/round_dom_1.75x0.095.step +245 -0
  41. weldbox-0.1.0/docs/samples/rfmg/round_dom_1.75x0.120.step +245 -0
  42. weldbox-0.1.0/docs/samples/rfmg/round_dom_1.75x0.156.step +245 -0
  43. weldbox-0.1.0/docs/samples/rfmg/round_dom_2x0.083.step +244 -0
  44. weldbox-0.1.0/docs/samples/rfmg/round_dom_2x0.095.step +244 -0
  45. weldbox-0.1.0/docs/samples/rfmg/round_dom_2x0.120.step +244 -0
  46. weldbox-0.1.0/docs/samples/rfmg/round_ss304_1.5x0.065.step +245 -0
  47. weldbox-0.1.0/docs/samples/rfmg/round_ss304_1x0.065.step +244 -0
  48. weldbox-0.1.0/docs/samples/rfmg/round_ss304_1x0.083.step +244 -0
  49. weldbox-0.1.0/docs/samples/rfmg/round_ss304_2x0.065.step +244 -0
  50. weldbox-0.1.0/docs/samples/rfmg/shipping.md +31 -0
  51. weldbox-0.1.0/docs/samples/rfmg/square_1.5x1.5x0.083.step +1518 -0
  52. weldbox-0.1.0/docs/samples/rfmg/square_1.5x1.5x0.120.step +1518 -0
  53. weldbox-0.1.0/docs/samples/rfmg/square_1.75x1.75x0.083.step +1518 -0
  54. weldbox-0.1.0/docs/samples/rfmg/square_1x1x0.0625.step +782 -0
  55. weldbox-0.1.0/docs/samples/rfmg/square_1x1x0.065.step +1518 -0
  56. weldbox-0.1.0/docs/samples/rfmg/square_2.5x2.5x0.250.step +1518 -0
  57. weldbox-0.1.0/docs/samples/rfmg/square_2x2x0.083.step +1517 -0
  58. weldbox-0.1.0/docs/samples/rfmg/square_2x2x0.095.step +1518 -0
  59. weldbox-0.1.0/docs/samples/rfmg/square_2x2x0.120.step +1517 -0
  60. weldbox-0.1.0/docs/samples/rfmg/square_2x2x0.125.step +782 -0
  61. weldbox-0.1.0/docs/samples/rfmg/square_2x2x0.1875.step +1518 -0
  62. weldbox-0.1.0/docs/samples/rfmg/square_2x2x0.25.step +1517 -0
  63. weldbox-0.1.0/docs/samples/rfmg/square_3x3x0.120.step +1518 -0
  64. weldbox-0.1.0/docs/samples/rfmg/square_3x3x0.1875.step +1518 -0
  65. weldbox-0.1.0/docs/samples/rfmg/square_4x4x0.125.step +781 -0
  66. weldbox-0.1.0/docs/samples/rfmg/square_al_1x1x0.065.step +782 -0
  67. weldbox-0.1.0/docs/samples/rfmg/square_al_1x1x0.125.step +782 -0
  68. weldbox-0.1.0/docs/samples/rfmg/square_al_3x3x0.125.step +781 -0
  69. weldbox-0.1.0/docs/samples/rfmg/square_ss304_1.5x1.5x0.065.step +782 -0
  70. weldbox-0.1.0/docs/samples/rfmg/square_ss304_1x1x0.065.step +782 -0
  71. weldbox-0.1.0/docs/samples/sendcutsend/.gitkeep +0 -0
  72. weldbox-0.1.0/docs/samples/sendcutsend/material_list.md +0 -0
  73. weldbox-0.1.0/examples/epoxy_machine_cell.yaml +25 -0
  74. weldbox-0.1.0/examples/winding_machine_cell.yaml +45 -0
  75. weldbox-0.1.0/examples/winding_machine_cell_1x1.yaml +45 -0
  76. weldbox-0.1.0/pyproject.toml +31 -0
  77. weldbox-0.1.0/src/weldbox/__init__.py +3 -0
  78. weldbox-0.1.0/src/weldbox/blocking.py +188 -0
  79. weldbox-0.1.0/src/weldbox/catalog.py +153 -0
  80. weldbox-0.1.0/src/weldbox/cli.py +98 -0
  81. weldbox-0.1.0/src/weldbox/consolidate.py +116 -0
  82. weldbox-0.1.0/src/weldbox/dedupe.py +73 -0
  83. weldbox-0.1.0/src/weldbox/features.py +281 -0
  84. weldbox-0.1.0/src/weldbox/frame.py +352 -0
  85. weldbox-0.1.0/src/weldbox/generate.py +180 -0
  86. weldbox-0.1.0/src/weldbox/geometry/__init__.py +0 -0
  87. weldbox-0.1.0/src/weldbox/geometry/assembly.py +177 -0
  88. weldbox-0.1.0/src/weldbox/geometry/cutters.py +106 -0
  89. weldbox-0.1.0/src/weldbox/geometry/member.py +49 -0
  90. weldbox-0.1.0/src/weldbox/geometry/profile.py +34 -0
  91. weldbox-0.1.0/src/weldbox/manifest.py +93 -0
  92. weldbox-0.1.0/src/weldbox/panels/__init__.py +0 -0
  93. weldbox-0.1.0/src/weldbox/panels/dxf.py +47 -0
  94. weldbox-0.1.0/src/weldbox/panels/layout.py +178 -0
  95. weldbox-0.1.0/src/weldbox/shipping.py +89 -0
  96. weldbox-0.1.0/src/weldbox/spec.py +174 -0
  97. weldbox-0.1.0/src/weldbox/units.py +78 -0
  98. weldbox-0.1.0/src/weldbox/vendors/__init__.py +3 -0
  99. weldbox-0.1.0/src/weldbox/vendors/base.py +99 -0
  100. weldbox-0.1.0/src/weldbox/vendors/data/fabtech.yaml +2 -0
  101. weldbox-0.1.0/src/weldbox/vendors/data/oshcut.yaml +2 -0
  102. weldbox-0.1.0/src/weldbox/vendors/data/rfmg.yaml +70 -0
  103. weldbox-0.1.0/src/weldbox/wizard.py +178 -0
  104. weldbox-0.1.0/tests/fixtures/winding_machine_cell.yaml +45 -0
  105. weldbox-0.1.0/tests/test_acceptance_winding_cell.py +111 -0
  106. weldbox-0.1.0/tests/test_catalog.py +55 -0
  107. weldbox-0.1.0/tests/test_consolidate.py +75 -0
  108. weldbox-0.1.0/tests/test_dedupe.py +108 -0
  109. weldbox-0.1.0/tests/test_features.py +139 -0
  110. weldbox-0.1.0/tests/test_frame.py +257 -0
  111. weldbox-0.1.0/tests/test_geometry.py +138 -0
  112. weldbox-0.1.0/tests/test_panels.py +151 -0
  113. weldbox-0.1.0/tests/test_shipping.py +79 -0
  114. weldbox-0.1.0/tests/test_spec.py +60 -0
  115. weldbox-0.1.0/tests/test_units.py +34 -0
  116. weldbox-0.1.0/uv.lock +1296 -0
@@ -0,0 +1,15 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ jobs:
9
+ fast-tests:
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - uses: actions/checkout@v4
13
+ - uses: astral-sh/setup-uv@v5
14
+ - name: Run fast tests (frame math, features, dedupe — no CAD kernel)
15
+ run: uv run pytest -m "not slow" -q
@@ -0,0 +1,34 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+ workflow_dispatch:
7
+
8
+ jobs:
9
+ build:
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - uses: actions/checkout@v4
13
+ - uses: astral-sh/setup-uv@v5
14
+ - name: Build sdist and wheel
15
+ run: uv build
16
+ - uses: actions/upload-artifact@v4
17
+ with:
18
+ name: dist
19
+ path: dist/
20
+
21
+ publish:
22
+ needs: build
23
+ runs-on: ubuntu-latest
24
+ environment:
25
+ name: pypi
26
+ url: https://pypi.org/p/weldbox
27
+ permissions:
28
+ id-token: write # OIDC for PyPI trusted publishing — no API token needed
29
+ steps:
30
+ - uses: actions/download-artifact@v4
31
+ with:
32
+ name: dist
33
+ path: dist/
34
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,7 @@
1
+ __pycache__/
2
+ *.pyc
3
+ .venv/
4
+ out/
5
+ dist/
6
+ .pytest_cache/
7
+ .DS_Store
weldbox-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Texabama Labs
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
weldbox-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,13 @@
1
+ Metadata-Version: 2.4
2
+ Name: weldbox
3
+ Version: 0.1.0
4
+ Summary: Generate vendor-ready tube laser cut lists (STEP) and sheet panel DXFs for welded square-tube boxes
5
+ License-File: LICENSE
6
+ Requires-Python: <3.13,>=3.10
7
+ Requires-Dist: build123d>=0.8
8
+ Requires-Dist: ezdxf>=1.3
9
+ Requires-Dist: pydantic>=2.7
10
+ Requires-Dist: pyyaml>=6.0
11
+ Requires-Dist: questionary>=2.0
12
+ Requires-Dist: rich>=13.0
13
+ Requires-Dist: typer>=0.12
weldbox-0.1.0/PRD.md ADDED
@@ -0,0 +1,49 @@
1
+ Tube Box Generator
2
+
3
+ Many laser cutting services are exposing tube cutting as a service. Tubes, particularly square tube can be cut to length so that they can be welded together to form machinery boxes/cabinet bases.
4
+
5
+ Services that expose this service include:
6
+
7
+ https://www.fabworks.com/services/tube-laser-cutting
8
+ https://tube.oshcut.com/
9
+ https://www.rmfg.com/laser-tube-cutting
10
+
11
+
12
+ I would like to be able to spec the outer dimentions of a box (larger than 12" x 12" x 12") and have the pieces automatically generated as a cut list (multiple step files) for easy upload to the services. I have attached step and/or dfx patterns for RFMG and OshCut in /docs/samples/<vendor_name>/**
13
+
14
+ I have also put tables (partial) of the materials offered as a markdown file in /docs/samples/<vendor_name>/material_list.md
15
+
16
+ I think the experience would be improved by a user interface, but a terminal interface with questions like inclusion of gussets, or additional supports (blocking?) would be helpful. We might also want to spec sheet metal siding that can easily be tack welded or rivited (we pre-cut the rivet holes in the tube for sheet metal adhesion).
17
+
18
+ We have freecad installed, I'm ok with leveraging that as a partial visual for the final assembly. If possible, I would like to avoid creating a 3d viewer in javascript - that seems like a waste of time.
19
+
20
+ We want to have some some tab/slot setup to improve assembly and welding:
21
+
22
+ In tube laser cutting, tab-and-slot connections (or hook-in-slot designs) allow two pieces of metal to interlock and self-locate. This eliminates the need for expensive jigs, clamps, and manual layout during assembly. Tabs are typically designed to be slightly smaller than the slot width to create an easy slip-fit for welding.Design Best PracticesClearance: To achieve a proper slip fit, make your slot's width and length at least 0.010 inches (or roughly 0.25 mm) larger than the thickness of the mating tab.Radius and Corner Relief: Right angles and square corners create localized stress concentration points in the metal tubing. Always incorporate small radii or "dog-bone" reliefs in the corners of your slots to prevent cracking during use.Self-Fixturing: Design the tabs so the parts interlock perfectly at the correct angle (e.g., 90° for frames). This allows the connected tubes to hold themselves in position while being tack-welded.
23
+
24
+
25
+ Example Use Case:
26
+
27
+ Name: Winding Machine Cell
28
+ Vendor: RFMG
29
+ Material: 1.5 x 1.5 x .120 in Square Tube Square 1.5" × 1.5" wall-thickness:0.12 corner-radus:0.24
30
+ Exterior Size:
31
+ height: 2000mm
32
+ width: 1000mm
33
+ depth: 800mm
34
+ Blocking:
35
+ a. perpedicular to height axis at 1000mm ("work surface"). 3x evenly spaced blocking of work surface extending from front to back(depth)
36
+ b. vertical supports between base and this at midpoints
37
+ c. midpoint of top extending across the width
38
+ d. midpoint of bottom extending across the width
39
+
40
+ Sheet metal siding:
41
+ attachment: rivet 100mm spacing 1/4" rivet
42
+ pieces:
43
+ a. sides (stainless - 0.038")
44
+ b. back (stainless - 0.038")
45
+ quantity: 5
46
+
47
+
48
+
49
+
@@ -0,0 +1,146 @@
1
+ # weldbox
2
+
3
+ Vendor-ready tube-laser cut lists for jigless welded frames — STEP + DXF
4
+ from a YAML spec.
5
+
6
+ | Enclosure with riveted siding (`full_height_posts`) | Bench frame (`top_bottom_frames`) |
7
+ |---|---|
8
+ | ![Winding machine cell: tube frame with work surface and riveted stainless siding](docs/images/weldbox_sample_1.jpg) | ![Epoxy machine cell: bench frame with solid full-width rails and posts butting up into them](docs/images/weldbox_sample_2.jpg) |
9
+
10
+ Generate laser tube cut lists for welded square-tube boxes and machine
11
+ bases. From a single YAML spec, weldbox produces:
12
+
13
+ - **`parts/*.step`** — one STEP file per unique tube part (deduplicated with
14
+ quantities), ready to upload to a tube laser service (RFMG, OshCut, Fabworks)
15
+ - **`panels/*.dxf`** — flat patterns for riveted sheet-metal siding, with
16
+ rivet holes that match the holes pre-cut in the tubes
17
+ - **`assembly.step`** — the full assembly for visual review (open in FreeCAD)
18
+ - **`cutlist.md` / `cutlist.csv`** — the manifest with lengths and quantities
19
+
20
+ Frames are self-fixturing: tee joints get through-wall tab-and-slot features
21
+ (slip fit +0.25mm, dog-bone corner reliefs per vendor best practice) so parts
22
+ interlock at 90° for tack welding without jigs.
23
+
24
+ **[Read the functionality deep dive →](docs/FUNCTIONALITY.md)** — topologies,
25
+ blocking primitives, the tab/slot system, part-count consolidation, siding,
26
+ shipping checks, and the full spec reference.
27
+
28
+ ## Quick start
29
+
30
+ ```sh
31
+ uv sync
32
+
33
+ # browse a vendor's tube catalog
34
+ uv run weldbox catalog list --vendor rfmg --shape square
35
+
36
+ # check the cut list without generating CAD (fast)
37
+ uv run weldbox generate examples/winding_machine_cell.yaml --dry-run
38
+
39
+ # full generation
40
+ uv run weldbox generate examples/winding_machine_cell.yaml -o out
41
+ open out/winding-machine-cell/assembly.step # opens in FreeCAD
42
+
43
+ # author a new spec interactively
44
+ uv run weldbox wizard
45
+ ```
46
+
47
+ ## The spec file
48
+
49
+ The YAML spec is the source of truth (the wizard just writes one). Lengths
50
+ accept unit suffixes (`2000mm`, `1.5in`, `0.038"`, `1/4in`); bare numbers are
51
+ millimetres. See `examples/winding_machine_cell.yaml` for a complete example:
52
+
53
+ ```yaml
54
+ name: Winding Machine Cell
55
+ vendor: rfmg
56
+ material: {shape: square, size: [1.5in], wall: 0.120in, family: A500}
57
+ exterior: {height: 2000mm, width: 1000mm, depth: 800mm}
58
+ topology: full_height_posts
59
+ blocking:
60
+ - type: level # horizontal frame; top surface at `height`
61
+ name: work-surface
62
+ height: 1000mm
63
+ cross_members: {count: 3, axis: depth}
64
+ - type: supports # verticals between two layers, at rail midpoints
65
+ between: [base, work-surface]
66
+ - type: spanner # single member across a face
67
+ face: top
68
+ axis: width
69
+ siding:
70
+ attachment: {method: rivet, rivet: 0.25in, spacing: 100mm}
71
+ panels:
72
+ - {faces: [left, right, back], material: {alloy: "304", thickness: 0.038"}}
73
+ quantity: 5
74
+ ```
75
+
76
+ ### Conventions
77
+
78
+ - Box coordinates: X = width, Y = depth, Z = height; the exterior dimensions
79
+ are outer envelope dimensions.
80
+ - `full_height_posts`: 4 posts run full height; all rails butt between posts.
81
+ - `level` heights place the level's **top surface** at the given height (it
82
+ is a work surface); use `height_ref: centerline|bottom_face` to override.
83
+ - Rivet holes: rivet diameter + 0.15mm clearance, evenly pitched at
84
+ `spacing`, on every member whose outer face lies on a sided box face.
85
+ Panel DXF holes are derived from the same list, so they always line up.
86
+ - All joints get tab/slot by default, including box corners — corner slots
87
+ that land flush with a post end become open hook-in notches. Disable with
88
+ `joints: {corner_tabs: false}`. `joints.weld_gap` shortens butting ends.
89
+ - Sheet panels get a `siding.corner_radius` (default 5mm) applied to the
90
+ DXF outline and the assembly solids; panels carry their rivet holes in 3D.
91
+ - Same-size panels consolidate into one flat part (vendors price multiples
92
+ cheaper): a sheet with through-holes can be flipped or rotated 180 in
93
+ plane, so left/right, front/back, and top/bottom pairs share one DXF
94
+ (e.g. `left-right.dxf`, qty 2). When hole patterns differ, the union is
95
+ cut into both so the blank stays reversible; conflicting patterns stay
96
+ separate parts.
97
+ - In the assembly STEP, members are colored by role (posts slate, rails
98
+ gray, level rails teal, crosses orange, supports green, spanners purple)
99
+ and panels are translucent. The assembly is written as flat named
100
+ products (one per member/panel) with colors on every face, which FreeCAD
101
+ imports as individually selectable/hideable colored objects regardless
102
+ of its STEP import preferences.
103
+ - `consolidate: true` (default) adds sacrificial slots/holes so same-length
104
+ members collapse into one part number — the example box goes from 13
105
+ unique parts to 3. Set `consolidate: false` if you don't want unused
106
+ cuts on visible faces.
107
+
108
+ ## Vendors
109
+
110
+ `rfmg` is fully encoded (from `docs/samples/rfmg/material_list.md`); `oshcut`
111
+ and `fabtech` are stubs awaiting material lists. Vendor shipping rules
112
+ (RFMG's LTL freight thresholds, see `docs/samples/rfmg/shipping.md`) drive
113
+ an order-level estimate: the generator weighs the cut list analytically and
114
+ warns — in the console and on `cutlist.md` — when a part dimension or the
115
+ order weight will trigger freight, with the specific reasons and the flat
116
+ surcharge. Vendor catalogs live in
117
+ `src/weldbox/vendors/data/*.yaml` — dimensions in inches as published, with
118
+ corner radius falling back to 2 x wall where unpublished.
119
+
120
+ ## Development
121
+
122
+ ```sh
123
+ uv run pytest -m "not slow" # fast tests: frame math, features, dedupe (<1s)
124
+ uv run pytest -m slow # geometry tests (build123d/OpenCascade)
125
+ uv run pytest # everything
126
+ ```
127
+
128
+ Pipeline: `spec.py` (YAML schema) → `frame.py`/`blocking.py` (member/joint
129
+ math, no CAD) → `features.py` (tab/slot/hole placement, no CAD) →
130
+ `dedupe.py` (symmetry-aware part grouping) → `geometry/` (build123d solids,
131
+ STEP) + `panels/` (DXF) → `manifest.py`.
132
+
133
+ All dimension and placement math is CAD-free and covered by fast tests;
134
+ geometry tests verify solids against analytic volumes and RFMG's published
135
+ stock STEP files, and assert zero interference at assembled joints.
136
+
137
+ ## License and attribution
138
+
139
+ weldbox is released under the [MIT License](LICENSE).
140
+
141
+ The RFMG reference data in `docs/samples/rfmg/` (tube profile
142
+ specifications, stock STEP/DXF geometry, and shipping thresholds) is
143
+ published by RMFG as design-against reference material for their laser tube
144
+ cutting service: <https://www.rmfg.com/docs/services/laser-tube-cutting>.
145
+ It is included here for interoperability and remains theirs; weldbox is not
146
+ affiliated with or endorsed by RMFG.
@@ -0,0 +1,323 @@
1
+ # weldbox — Functionality Deep Dive
2
+
3
+ weldbox turns a short YAML description of a welded square-tube box — a
4
+ machine base, enclosure, cart, bench, or cabinet frame — into everything a
5
+ laser tube cutting service needs to make the parts, and everything you need
6
+ to weld them together without a jig.
7
+
8
+ ```
9
+ spec.yaml ──▶ frame math ──▶ features ──▶ consolidation ──▶ outputs
10
+ (source of members + tabs/slots/ fewer unique STEP per part
11
+ truth) joints rivet holes part numbers DXF per panel
12
+ assembly STEP
13
+ cut list + shipping
14
+ ```
15
+
16
+ Everything up to geometry is pure math (no CAD kernel), so `--dry-run`
17
+ gives you a complete priced-out cut list in under a second.
18
+
19
+ ---
20
+
21
+ ## 1. The workflow
22
+
23
+ ```sh
24
+ uv run weldbox catalog list --vendor rfmg # 1. pick stock
25
+ uv run weldbox wizard # 2. author a spec (or write YAML by hand)
26
+ uv run weldbox generate my-box.yaml --dry-run # 3. sanity-check the cut list + shipping
27
+ uv run weldbox generate my-box.yaml -o out # 4. produce files
28
+ open out/my-box/assembly.step # 5. review in FreeCAD
29
+ # 6. upload out/my-box/parts/*.step and panels/*.dxf to the vendor
30
+ ```
31
+
32
+ The YAML spec is the source of truth. The wizard is only a convenience that
33
+ writes one; you can always edit the file directly and regenerate.
34
+
35
+ ---
36
+
37
+ ## 2. The spec file, field by field
38
+
39
+ All lengths accept unit suffixes — `2000mm`, `1.5in`, `0.038"`, `1/4in`,
40
+ `3 1/2 in`, `2cm`, `1m` — and bare numbers are millimetres. Unknown fields
41
+ are **rejected with an error**, never silently ignored, so a typo cannot
42
+ quietly change your build.
43
+
44
+ ```yaml
45
+ name: Epoxy Machine Cell # used for output folder and file names
46
+ vendor: rfmg # rfmg | oshcut | fabtech (stubs)
47
+ material: # catalog lookup key, not free-form:
48
+ shape: square # must match a profile the vendor stocks
49
+ size: [1.5in] # [w] or [w, h]
50
+ wall: 0.120in
51
+ family: A500 # disambiguates A500 vs 304 vs 6061 at the same size
52
+ exterior: # OUTER envelope of the frame
53
+ height: 860mm # Z
54
+ width: 2000mm # X
55
+ depth: 660mm # Y
56
+ topology: top_bottom_frames # see section 3
57
+ joints: # all optional; defaults shown
58
+ style: through_wall_tab # or plain_butt (no tabs anywhere)
59
+ slot_clearance: 0.25mm # slip fit: slot = tab + this
60
+ tab_width_fraction: 0.5 # tab width as fraction of the receiving flat
61
+ dogbone_radius: 1.0mm # corner relief circles in slots
62
+ weld_gap: 0mm # shortens every butting end
63
+ corner_tabs: true # tab/slot at box corners too
64
+ blocking: [...] # see section 4
65
+ siding: {...} # see section 5
66
+ quantity: 5 # assemblies; multiplies the cut list
67
+ consolidate: true # see section 6
68
+ ```
69
+
70
+ `material` must resolve to a real vendor profile — weldbox models the
71
+ vendor's actual stock (including the published outside corner radius) so
72
+ the STEP files you upload match what the service quotes against.
73
+
74
+ ---
75
+
76
+ ## 3. Topologies — how the box is framed
77
+
78
+ ### `full_height_posts`
79
+
80
+ Four posts run the full height; every horizontal rail butts between them.
81
+ The classic cabinet/enclosure frame — vertical loads go straight down the
82
+ posts to the floor.
83
+
84
+ ```
85
+ ┌─post──────post─┐ posts: height H x4
86
+ │ ═══rails═══ │ rails: width − 2×tube
87
+ ║ ║ depth − 2×tube
88
+ │ ═══rails═══ │
89
+ └─post──────post─┘
90
+ ```
91
+
92
+ ### `top_bottom_frames`
93
+
94
+ The top and bottom are complete rectangular frames whose **width rails run
95
+ solid across the full exterior width**; the depth rails butt between them,
96
+ and the posts butt *up into* the frames, tabbing into the full-width rails.
97
+ Right for benches and long tables where you want unbroken members across
98
+ the span (and two frames you can weld up flat on a table first).
99
+
100
+ ```
101
+ ╔════ solid width rail ════╗ top frame
102
+ ║ post post post post ║ posts: height − 2×tube, tab up/down
103
+ ╚════ solid width rail ════╝ bottom frame
104
+ ```
105
+
106
+ ---
107
+
108
+ ## 4. Blocking — interior structure
109
+
110
+ Three primitives, freely combined in the `blocking:` list.
111
+
112
+ ### `level` — a horizontal frame at any height
113
+
114
+ ```yaml
115
+ - type: level
116
+ name: work-surface # referenced by supports
117
+ height: 1000mm
118
+ height_ref: top_face # top_face (default) | centerline | bottom_face
119
+ cross_members: {count: 3, axis: depth}
120
+ ```
121
+
122
+ A level is a full perimeter frame (4 rails teed into the posts) with
123
+ optional evenly spaced cross members butted between its rails.
124
+ `height_ref: top_face` means the *surface you'd put something on* is at the
125
+ given height — it's a work surface. Heights are validated against the box:
126
+ if a level can't fit, the error tells you the exact legal range for your
127
+ tube size and box height.
128
+
129
+ ### `supports` — verticals between two layers
130
+
131
+ ```yaml
132
+ - type: supports
133
+ between: [base, work-surface] # any two of: base, top, or a level name
134
+ at: midpoints
135
+ ```
136
+
137
+ Places a vertical member at the midpoint of each of the four rail pairs of
138
+ the two layers, butted between the lower rails' top faces and the upper
139
+ rails' bottom faces — load paths under a work surface without blocking the
140
+ interior.
141
+
142
+ ### `spanner` — members across the top or bottom face
143
+
144
+ ```yaml
145
+ - type: spanner
146
+ face: [top, bottom] # one face or a list
147
+ axis: depth # width | depth
148
+ count: 3 # evenly spaced at k/(count+1)
149
+ position: 0.5 # used only when count == 1
150
+ ```
151
+
152
+ Spanners butt between the two face rails perpendicular to their axis —
153
+ mounting rails for equipment, caster plates, or deck support.
154
+
155
+ ---
156
+
157
+ ## 5. Sheet metal siding
158
+
159
+ ```yaml
160
+ siding:
161
+ attachment: {method: rivet, rivet: 1/4in, spacing: 100mm, hole_clearance: 0.15mm}
162
+ panels:
163
+ - {faces: [left, right, back], material: {alloy: "304", thickness: 0.038"}}
164
+ panel_margin: 0mm # inset from the frame edge
165
+ corner_radius: 5mm # sheet corner radius (DXF + 3D)
166
+ ```
167
+
168
+ What you get:
169
+
170
+ - **Rivet holes pre-cut in the tubes.** Every member whose outer face lies
171
+ on a sided box face gets holes — rivet diameter + clearance (1/4" →
172
+ 6.5mm), evenly pitched at `spacing`, symmetric about the member's middle
173
+ with a 20mm end margin. That includes mid-frame members (a level rail or
174
+ support on a sided face gets a rivet row too).
175
+ - **A DXF flat pattern per unique panel** — closed polyline outline with
176
+ true arc corners at `corner_radius`, CIRCLE holes, millimetre units
177
+ (`$INSUNITS = 4`), DXF R2000 — the format flat-laser vendors ingest
178
+ directly (SendCutSend, OshCut, Fabworks flat service).
179
+ - **Guaranteed alignment.** Panel holes are derived from the *same* planned
180
+ holes as the tube cuts — they cannot drift apart.
181
+ - Panels appear in the assembly STEP as translucent solids with their holes
182
+ and rounded corners, sitting proud on the frame face.
183
+
184
+ ---
185
+
186
+ ## 6. Part-count consolidation
187
+
188
+ Vendors price multiples of the same part dramatically cheaper, so weldbox
189
+ works hard to minimize unique part numbers (`consolidate: true`, the
190
+ default).
191
+
192
+ **Tubes.** Members with the same profile and cut length often differ only
193
+ in *which faces* carry slots or holes. weldbox aligns every member of such
194
+ a group through the square tube's 8 symmetries (4 rotations × end-for-end
195
+ flip), unions the feature sets, and cuts the union into all of them. The
196
+ extra cuts are sacrificial — unused slots or holes over solid tube — and
197
+ the pass is guarded: it only ever *adds cuts* (never tabs), it never lets
198
+ two features come closer than a 0.5mm web, and a member that can't merge
199
+ safely keeps its own part number.
200
+
201
+ **Panels.** A flat sheet with through-holes can be flipped over or rotated
202
+ 180° in plane, so left/right, front/back, and top/bottom panels of the same
203
+ size merge into one blank (`left-right.dxf`, qty 2). Differing hole
204
+ patterns are unioned under the best alignment; patterns that would collide
205
+ stay separate.
206
+
207
+ Real numbers: the PRD's winding machine cell is 25 members that would
208
+ naively be 13 unique parts — consolidation ships it as **3**. The epoxy
209
+ cell's 22 members also ship as **3** (its posts and supports turn out to be
210
+ literally the same part).
211
+
212
+ Set `consolidate: false` if you don't want sacrificial cuts on visible
213
+ faces — only exactly-identical parts merge then.
214
+
215
+ ---
216
+
217
+ ## 7. The tab/slot system (jigless welding)
218
+
219
+ Every joint self-locates for tack welding — no jigs, clamps, or layout:
220
+
221
+ - At each butting end, the two walls parallel to the receiving member's
222
+ axis each grow a **tab**: the wall's own thickness, half the receiving
223
+ flat width wide, protruding exactly one receiving-wall thickness so it
224
+ finishes **flush with the far side of the wall** after welding.
225
+ - The receiving face gets matching **through-wall slots** at
226
+ tab + 0.25mm (the 0.010" slip fit tube-laser vendors recommend), with
227
+ **dog-bone relief circles** in the corners so the slot can't start a
228
+ crack.
229
+ - Slots always land on the flat region between the tube's corner radii.
230
+ - At box corners, the outer tab's slot lands flush with the post end and
231
+ becomes an **open hook-in notch** — drop the rail in, it self-squares.
232
+ - Everything is tunable per spec (`joints:`): clearance, tab width
233
+ fraction, dog-bone radius, weld gap, corner tabs on/off, or
234
+ `style: plain_butt` for plain saw-style cuts.
235
+
236
+ Correctness is enforced by the test suite: assembled members are checked
237
+ for **zero boolean interference** — every tab passes exactly through its
238
+ slot void.
239
+
240
+ ---
241
+
242
+ ## 8. Outputs
243
+
244
+ ```
245
+ out/<name>/
246
+ ├── cutlist.md # human cut list + shipping estimate
247
+ ├── cutlist.csv # same data for spreadsheets
248
+ ├── parts/
249
+ │ └── <name>_<part>_<length>mm_x<qty>.step
250
+ ├── panels/
251
+ │ └── <name>_<faces>.dxf
252
+ └── assembly.step # open in FreeCAD
253
+ ```
254
+
255
+ - **Part STEPs** are built in the vendor-stock convention (cross-section
256
+ centered on origin, extruded along +Z) and modeled against the vendor's
257
+ published geometry — the generated 1.5×1.5×0.120 profile matches RFMG's
258
+ own reference STEP within 0.5%.
259
+ - **The assembly STEP** contains one *named product per member and panel*
260
+ (`post-fl`, `work-surface-cross-2`, `panel-back`, …), colored by role —
261
+ posts slate, rails gray, level rails teal, crosses orange, supports
262
+ green, spanners purple, panels translucent — with colors on every face,
263
+ so FreeCAD shows an inspectable, hideable, colored model regardless of
264
+ its STEP import preferences.
265
+ - **The cut list** shows per-part length (mm and inches), quantity per
266
+ assembly, and total quantity across the order, plus total stock meters.
267
+
268
+ ### Design-rule and shipping checks
269
+
270
+ Every run (including `--dry-run`) checks the vendor's design rules
271
+ (minimum hole diameter, dog-bone radius, maximum part length) and estimates
272
+ the **order shipping**: analytic part weights (section area × length ×
273
+ material density; sheet area × thickness) against the vendor's parcel
274
+ limits. For RFMG that means LTL freight is flagged — with the specific
275
+ trigger and the $200 flat surcharge — when any part exceeds 100 lb or 60"
276
+ (or 48"×30" on two dimensions), or the order exceeds 200 lb.
277
+
278
+ ---
279
+
280
+ ## 9. Vendors
281
+
282
+ Vendor data lives in `src/weldbox/vendors/data/*.yaml` behind a small
283
+ `Vendor` interface (catalog, design rules, shipping rules, file naming):
284
+
285
+ - **rfmg** — fully encoded: 56 profiles (A500/4130/DOM steel, 304
286
+ stainless, 6061/6063 aluminum) with published corner radii, design notes,
287
+ and LTL freight thresholds. Source:
288
+ <https://www.rmfg.com/docs/services/laser-tube-cutting>
289
+ - **oshcut**, **fabtech** — registered stubs awaiting material lists.
290
+
291
+ To add a vendor: drop a `data/<slug>.yaml` (dimensions in inches as
292
+ published; omit corner radius where unpublished and weldbox assumes
293
+ 2×wall), subclass `Vendor` with its rules, and register it. Catalog lookups
294
+ are tolerant (±0.02mm) and demand a `family` when a size/wall exists in
295
+ several alloys.
296
+
297
+ ---
298
+
299
+ ## 10. Validation and errors
300
+
301
+ - Unknown/typo'd spec fields → error naming the exact path
302
+ (`blocking.1.spanner.cross_members: Extra inputs are not permitted`).
303
+ - Level heights → validated against the box with the legal range in the
304
+ message, in *your* `height_ref` terms.
305
+ - Exterior too small for the tube, unknown vendors/layers, ambiguous
306
+ catalog matches → specific errors.
307
+ - The CLI prints one clean error line and exits 1 — no tracebacks.
308
+
309
+ ---
310
+
311
+ ## 11. Current limitations
312
+
313
+ - Frame math supports **square tube** only (rect/round profiles exist in
314
+ the catalog for future use; round stock is not applicable to this joint
315
+ system).
316
+ - One tube profile per box.
317
+ - Gussets are not yet generated.
318
+ - Blocking members can be placed at positions that collide with each other
319
+ (e.g. a spanner through a support); member-vs-member placement collision
320
+ checking is on the roadmap — the bounding-box overlap tests catch this in
321
+ the standard layouts.
322
+ - `oshcut`/`fabtech` catalogs are empty until their material lists are
323
+ transcribed.