b3dkit 0.1.5__tar.gz → 0.3.2__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 (59) hide show
  1. b3dkit-0.3.2/.github/workflows/release.yml +72 -0
  2. b3dkit-0.3.2/.github/workflows/test.yml +81 -0
  3. b3dkit-0.3.2/.pre-commit-config.yaml +22 -0
  4. {b3dkit-0.1.5 → b3dkit-0.3.2}/PKG-INFO +4 -4
  5. {b3dkit-0.1.5 → b3dkit-0.3.2}/docs/basic_shapes.md +11 -11
  6. b3dkit-0.3.2/docs/dovetail.md +121 -0
  7. {b3dkit-0.1.5 → b3dkit-0.3.2}/docs/high_top_slide_box.md +11 -0
  8. {b3dkit-0.1.5 → b3dkit-0.3.2}/docs/slide_box.md +0 -28
  9. {b3dkit-0.1.5 → b3dkit-0.3.2}/pyproject.toml +30 -4
  10. b3dkit-0.3.2/src/b3dkit/__init__.py +92 -0
  11. {b3dkit-0.1.5 → b3dkit-0.3.2}/src/b3dkit/antichamfer.py +8 -3
  12. {b3dkit-0.1.5 → b3dkit-0.3.2}/src/b3dkit/ball_socket.py +12 -9
  13. {b3dkit-0.1.5 → b3dkit-0.3.2}/src/b3dkit/basic_shapes.py +28 -26
  14. {b3dkit-0.1.5 → b3dkit-0.3.2}/src/b3dkit/bolt_fittings.py +18 -11
  15. {b3dkit-0.1.5 → b3dkit-0.3.2}/src/b3dkit/click_fit.py +6 -5
  16. {b3dkit-0.1.5 → b3dkit-0.3.2}/src/b3dkit/dovetail.py +209 -150
  17. {b3dkit-0.1.5 → b3dkit-0.3.2}/src/b3dkit/hexwall.py +9 -5
  18. {b3dkit-0.1.5 → b3dkit-0.3.2}/src/b3dkit/high_top_slide_box.py +46 -5
  19. {b3dkit-0.1.5 → b3dkit-0.3.2}/src/b3dkit/point.py +15 -5
  20. b3dkit-0.3.2/src/b3dkit/py.typed +0 -0
  21. {b3dkit-0.1.5 → b3dkit-0.3.2}/src/b3dkit/slide_box.py +33 -30
  22. {b3dkit-0.1.5 → b3dkit-0.3.2}/src/b3dkit/twist_snap.py +8 -10
  23. b3dkit-0.3.2/tests/conftest.py +12 -0
  24. {b3dkit-0.1.5 → b3dkit-0.3.2}/tests/test_antichamfer.py +6 -6
  25. {b3dkit-0.1.5 → b3dkit-0.3.2}/tests/test_ball_socket.py +4 -2
  26. {b3dkit-0.1.5 → b3dkit-0.3.2}/tests/test_basic_shapes.py +6 -17
  27. {b3dkit-0.1.5 → b3dkit-0.3.2}/tests/test_bolt_fittings.py +7 -7
  28. {b3dkit-0.1.5 → b3dkit-0.3.2}/tests/test_click_fit.py +2 -7
  29. {b3dkit-0.1.5 → b3dkit-0.3.2}/tests/test_dovetail.py +159 -26
  30. {b3dkit-0.1.5 → b3dkit-0.3.2}/tests/test_hexwall.py +5 -4
  31. {b3dkit-0.1.5 → b3dkit-0.3.2}/tests/test_high_top_slide_box.py +136 -47
  32. {b3dkit-0.1.5 → b3dkit-0.3.2}/tests/test_point.py +1 -3
  33. {b3dkit-0.1.5 → b3dkit-0.3.2}/tests/test_slide_box.py +7 -9
  34. {b3dkit-0.1.5 → b3dkit-0.3.2}/tests/test_twist_snap.py +3 -4
  35. b3dkit-0.1.5/.coverage +0 -0
  36. b3dkit-0.1.5/build.bat +0 -44
  37. b3dkit-0.1.5/build.sh +0 -44
  38. b3dkit-0.1.5/docs/dovetail.md +0 -81
  39. b3dkit-0.1.5/src/b3dkit/__init__.py +0 -11
  40. b3dkit-0.1.5/tests/conftest.py +0 -14
  41. {b3dkit-0.1.5 → b3dkit-0.3.2}/.coveragerc +0 -0
  42. {b3dkit-0.1.5 → b3dkit-0.3.2}/.gitignore +0 -0
  43. {b3dkit-0.1.5 → b3dkit-0.3.2}/.vscode/settings.json +0 -0
  44. {b3dkit-0.1.5 → b3dkit-0.3.2}/LICENSE +0 -0
  45. {b3dkit-0.1.5 → b3dkit-0.3.2}/README.md +0 -0
  46. {b3dkit-0.1.5 → b3dkit-0.3.2}/docs/Makefile +0 -0
  47. {b3dkit-0.1.5 → b3dkit-0.3.2}/docs/antichamfer.md +0 -0
  48. {b3dkit-0.1.5 → b3dkit-0.3.2}/docs/ball_socket.md +0 -0
  49. {b3dkit-0.1.5 → b3dkit-0.3.2}/docs/bolt_fittings.md +0 -0
  50. {b3dkit-0.1.5 → b3dkit-0.3.2}/docs/click_fit.md +0 -0
  51. {b3dkit-0.1.5 → b3dkit-0.3.2}/docs/conf.py +0 -0
  52. {b3dkit-0.1.5 → b3dkit-0.3.2}/docs/dovetail.png +0 -0
  53. {b3dkit-0.1.5 → b3dkit-0.3.2}/docs/hexwall.md +0 -0
  54. {b3dkit-0.1.5 → b3dkit-0.3.2}/docs/index.md +0 -0
  55. {b3dkit-0.1.5 → b3dkit-0.3.2}/docs/point.md +0 -0
  56. {b3dkit-0.1.5 → b3dkit-0.3.2}/docs/twist_snap.md +0 -0
  57. {b3dkit-0.1.5 → b3dkit-0.3.2}/docs/twist_snap.png +0 -0
  58. {b3dkit-0.1.5 → b3dkit-0.3.2}/mkdocs.yml +0 -0
  59. {b3dkit-0.1.5 → b3dkit-0.3.2}/readthedocs.yaml +0 -0
@@ -0,0 +1,72 @@
1
+ name: release
2
+
3
+ on:
4
+ push:
5
+ tags: ["v*"]
6
+ workflow_dispatch:
7
+
8
+ jobs:
9
+ build:
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - uses: actions/checkout@v7
13
+
14
+ - uses: actions/setup-python@v7
15
+ with:
16
+ python-version: "3.12"
17
+ cache: pip
18
+
19
+ - name: Install and test
20
+ run: |
21
+ python -m pip install --upgrade pip build
22
+ python -m pip install -e ".[dev]"
23
+ pytest -q
24
+
25
+ - name: Check the tag matches the packaged version
26
+ run: |
27
+ tag="${GITHUB_REF_NAME#v}"
28
+ version=$(python -c "import re,pathlib;print(re.search(r'^version = \"([^\"]+)\"',pathlib.Path('pyproject.toml').read_text(),re.M).group(1))")
29
+ if [ "$tag" != "$version" ]; then
30
+ echo "::error::tag $GITHUB_REF_NAME does not match pyproject version $version"
31
+ exit 1
32
+ fi
33
+ echo "tag and version agree: $version"
34
+
35
+ - name: Build
36
+ run: python -m build
37
+
38
+ - uses: actions/upload-artifact@v7
39
+ with:
40
+ name: dist
41
+ path: dist/
42
+
43
+ publish:
44
+ needs: build
45
+ runs-on: ubuntu-latest
46
+ environment: pypi
47
+ permissions:
48
+ id-token: write # required for PyPI Trusted Publishing
49
+ steps:
50
+ - uses: actions/download-artifact@v7
51
+ with:
52
+ name: dist
53
+ path: dist/
54
+ - uses: pypa/gh-action-pypi-publish@release/v1
55
+
56
+ smoke-test:
57
+ # Install the published artifact from PyPI and build something, mirroring the
58
+ # post-upload check the retired build.sh performed by hand.
59
+ needs: publish
60
+ runs-on: ubuntu-latest
61
+ steps:
62
+ - uses: actions/setup-python@v7
63
+ with:
64
+ python-version: "3.12"
65
+ - name: Wait for PyPI to serve the new version
66
+ run: sleep 60
67
+ - name: Install from PyPI and build a part
68
+ run: |
69
+ version="${GITHUB_REF_NAME#v}"
70
+ python -m pip install --upgrade pip
71
+ python -m pip install "b3dkit==$version"
72
+ python -c "from b3dkit import Divot; assert Divot().is_valid; print('smoke test OK')"
@@ -0,0 +1,81 @@
1
+ name: test
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ workflow_dispatch:
8
+
9
+ concurrency:
10
+ group: test-${{ github.ref }}
11
+ cancel-in-progress: true
12
+
13
+ jobs:
14
+ test:
15
+ runs-on: ubuntu-latest
16
+ strategy:
17
+ fail-fast: false
18
+ matrix:
19
+ python-version: ["3.11", "3.12", "3.13"]
20
+ build123d: [latest]
21
+ include:
22
+ # One job pinned at the declared dependency floor. b3dkit 0.1.5 shipped
23
+ # code that could not run against its own advertised minimum; this job
24
+ # is what stops that from happening again.
25
+ - python-version: "3.12"
26
+ build123d: floor
27
+
28
+ name: py${{ matrix.python-version }} / build123d ${{ matrix.build123d }}
29
+
30
+ steps:
31
+ - uses: actions/checkout@v7
32
+
33
+ - uses: actions/setup-python@v7
34
+ with:
35
+ python-version: ${{ matrix.python-version }}
36
+ cache: pip
37
+
38
+ - name: Install
39
+ run: |
40
+ python -m pip install --upgrade pip
41
+ python -m pip install -e ".[dev]"
42
+
43
+ - name: Pin build123d to the declared floor
44
+ if: matrix.build123d == 'floor'
45
+ run: |
46
+ floor=$(python -c "import re,pathlib;print(re.search(r'build123d>=([0-9.]+)',pathlib.Path('pyproject.toml').read_text()).group(1))")
47
+ echo "Installing build123d==$floor"
48
+ python -m pip install "build123d==$floor"
49
+
50
+ - name: Show resolved versions
51
+ run: python -c "import sys,build123d;print(sys.version);print('build123d',build123d.__version__)"
52
+
53
+ - name: Run tests
54
+ run: pytest --cov=b3dkit --cov-report=term-missing --junitxml=junit.xml
55
+
56
+ - name: Fail if any test was skipped
57
+ # A green suite that silently skips is how the broken 0.10.0 floor reached
58
+ # PyPI: 27 tests were skipped and nothing complained.
59
+ if: always()
60
+ run: |
61
+ python - <<'PY'
62
+ import sys, xml.etree.ElementTree as ET
63
+ suite = ET.parse("junit.xml").getroot()
64
+ skipped = sum(int(s.get("skipped", 0)) for s in suite.iter("testsuite"))
65
+ if skipped:
66
+ print(f"::error::{skipped} test(s) skipped; the skip budget is zero")
67
+ sys.exit(1)
68
+ print("No skipped tests.")
69
+ PY
70
+
71
+ lint:
72
+ runs-on: ubuntu-latest
73
+ steps:
74
+ - uses: actions/checkout@v7
75
+ - uses: actions/setup-python@v7
76
+ with:
77
+ python-version: "3.12"
78
+ cache: pip
79
+ - run: python -m pip install --upgrade pip ruff black
80
+ - run: ruff check .
81
+ - run: black --check .
@@ -0,0 +1,22 @@
1
+ repos:
2
+ - repo: https://github.com/astral-sh/ruff-pre-commit
3
+ rev: v0.14.5
4
+ hooks:
5
+ - id: ruff
6
+ args: [--fix]
7
+ - id: ruff-format
8
+ stages: [manual]
9
+
10
+ - repo: https://github.com/psf/black
11
+ rev: 25.9.0
12
+ hooks:
13
+ - id: black
14
+
15
+ - repo: https://github.com/pre-commit/pre-commit-hooks
16
+ rev: v6.0.0
17
+ hooks:
18
+ - id: end-of-file-fixer
19
+ - id: trailing-whitespace
20
+ - id: check-toml
21
+ - id: check-yaml
22
+ - id: check-merge-conflict
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: b3dkit
3
- Version: 0.1.5
3
+ Version: 0.3.2
4
4
  Summary: build123d libraries and utilities
5
5
  Project-URL: Homepage, https://github.com/x0pherl/b3dkit
6
6
  Project-URL: Issues, https://github.com/x0pherl/b3dkit/issues
@@ -10,8 +10,8 @@ License-File: LICENSE
10
10
  Classifier: License :: OSI Approved :: MIT License
11
11
  Classifier: Operating System :: OS Independent
12
12
  Classifier: Programming Language :: Python :: 3
13
- Requires-Python: >=3.10
14
- Requires-Dist: build123d>=0.10.0
13
+ Requires-Python: >=3.11
14
+ Requires-Dist: build123d>=0.11.0
15
15
  Requires-Dist: ocp-vscode>=3.1.1
16
16
  Provides-Extra: dev
17
17
  Requires-Dist: pytest-cov>=7.0.0; extra == 'dev'
@@ -28,7 +28,7 @@ def apothem_to_radius(apothem: float, side_count: int = 6) -> float
28
28
 
29
29
  **Arguments**
30
30
  - `apothem` (float): The apothem of the polygon
31
- - `side_count` (float): The number of sides of the poygon.
31
+ - `side_count` (int): The number of sides of the poygon.
32
32
 
33
33
  **Returns:**
34
34
  - `float`: The radius of the polygon
@@ -56,7 +56,7 @@ Finds the intersection point along one axis given a coordinate on the other axis
56
56
  ### distance_to_circle_edge
57
57
 
58
58
  ```python
59
- distance_to_circle_edge(radius: float, point: tuple, angle: float) -> float
59
+ distance_to_circle_edge(radius: float, point: tuple[float, float], angle: float) -> float
60
60
  ```
61
61
 
62
62
  Calculates the distance from a given point to the edge of a circle in a specified direction.
@@ -87,15 +87,15 @@ Calculates the opposite side length of a right triangle given the angle and adja
87
87
  **Returns:**
88
88
  - `float`: The length of the opposite side
89
89
 
90
- ### radius_to_appothem
90
+ ### radius_to_apothem
91
91
 
92
92
  ```python
93
- def radius_to_appothem(radius: float, side_count: int = 6) -> float
93
+ def radius_to_apothem(radius: float, side_count: int = 6) -> float
94
94
  ```
95
95
 
96
96
  **Arguments:**
97
97
  - `radius` (float): the radius of the polygon
98
- - `side_count` (float): the number of sides of the polygon
98
+ - `side_count` (int): the number of sides of the polygon
99
99
 
100
100
  **Returns:**
101
101
  - `float`: The apothem of the polygon
@@ -112,7 +112,7 @@ DiamondCylinder(
112
112
  radius: float,
113
113
  height: float,
114
114
  arc_size: float = 360,
115
- stretch: tuple = (1, 1, 1),
115
+ stretch: tuple[float, float, float] = (1, 1, 1),
116
116
  rotation: RotationLike = (0, 0, 0),
117
117
  align: Align | tuple[Align, Align, Align] | None = None,
118
118
  mode: Mode = Mode.ADD,
@@ -123,7 +123,7 @@ DiamondCylinder(
123
123
  - `radius` (float): The radius of the circumscribed circle
124
124
  - `height` (float): The height of the extrusion
125
125
  - `arc_size` (float, default=360): Angular sweep in degrees for the circular clipping sector used to intersect the base profile
126
- - `stretch` (tuple, default=(1, 1, 1)): Scaling factors (X, Y, Z)
126
+ - `stretch` (tuple[float, float, float], default=(1, 1, 1)): Scaling factors (X, Y, Z)
127
127
  - `rotation` (RotationLike, default=(0, 0, 0)): Rotation angles (X, Y, Z) in degrees
128
128
  - `align` (Align | tuple[Align, Align, Align] | None, default=None): Alignment along X, Y, Z axes
129
129
  - `mode` (Mode, default=Mode.ADD): Boolean combination mode
@@ -145,7 +145,7 @@ Creates a torus by sweeping a diamond (square rotated 45°) along a circular pat
145
145
  DiamondTorus(
146
146
  major_radius: float,
147
147
  minor_radius: float,
148
- stretch: tuple = (1, 1)
148
+ stretch: tuple[float, float] = (1, 1)
149
149
  )
150
150
  ```
151
151
 
@@ -154,7 +154,7 @@ Creates a torus by sweeping a diamond (square rotated 45°) along a circular pat
154
154
  **Arguments:**
155
155
  - `major_radius` (float): The radius of the circular sweep path
156
156
  - `minor_radius` (float): The radius of the diamond cross-section
157
- - `stretch` (tuple, default=(1, 1)): Scaling factors for the diamond shape
157
+ - `stretch` (tuple[float, float], default=(1, 1)): Scaling factors for the diamond shape
158
158
  - `rotation` (RotationLike, optional): angles to rotate about axes. Defaults to (0, 0, 0)
159
159
  - `align` (Align | tuple[Align, Align, Align] | None, optional): align MIN, CENTER,
160
160
  or MAX of object. Defaults to (Align.CENTER, Align.CENTER, Align.CENTER)
@@ -175,7 +175,7 @@ PolygonalCylinder(
175
175
  height: float,
176
176
  side_count: int = 6,
177
177
  arc_size: float = 360,
178
- stretch: tuple = (1, 1, 1),
178
+ stretch: tuple[float, float, float] = (1, 1, 1),
179
179
  rotation: RotationLike = (0, 0, 0),
180
180
  align: Align | tuple[Align, Align, Align] | None = None,
181
181
  mode: Mode = Mode.ADD,
@@ -188,7 +188,7 @@ PolygonalCylinder(
188
188
  - `height` (float): The height of the extrusion
189
189
  - `side_count` (int, default=6): Number of sides of the polygon
190
190
  - `arc_size` (float, default=360): Angular sweep in degrees for the circular clipping sector used to intersect the base profile
191
- - `stretch` (tuple, default=(1, 1, 1)): Scaling factors (X, Y, Z)
191
+ - `stretch` (tuple[float, float, float], default=(1, 1, 1)): Scaling factors (X, Y, Z)
192
192
  - `rotation` (RotationLike, default=(0, 0, 0)): Angles to rotate about axes
193
193
  - `align` (Align | tuple[Align, Align, Align] | None, default=None): Align MIN, CENTER, or MAX on each axis
194
194
  - `mode` (Mode, default=Mode.ADD): Boolean combination mode
@@ -0,0 +1,121 @@
1
+ # Dovetail
2
+
3
+ ## Overview
4
+
5
+ Dovetail is intended for breaking large parts into a dovetail and socketed part that can be easily fitted together with very tight and precise tolerances. This might be useful, for example, when designing parts that cannot fit onto common 3d-printer beds, and must be broken into multiple parts.
6
+
7
+ ![example of a part split into a dovetail and a socket](dovetail.png)
8
+
9
+ The `dovetail_subpart` function takes a build123d part and the necessary parameters to break it into either the dovetail or the socket component of the split. Call it twice with the same arguments, changing only `subpart`, to produce a mating pair.
10
+
11
+ All linear dimensions are in millimeters and all angles are in degrees.
12
+
13
+ ## Terminology
14
+
15
+ - **subpart** — one of the two pieces produced by splitting a part. This is what `dovetail_subpart` returns, and the `subpart=` argument selects which one you get.
16
+ - **tail** — the subpart carrying the protruding tongue (`DovetailSubpart.TAIL`).
17
+ - **socket** — the subpart carrying the matching recess (`DovetailSubpart.SOCKET`).
18
+
19
+ The two subparts are not equal halves: the tongue belongs to the tail, and for `SNUGTAIL` the joint wraps around three sides, so the socket is typically several times the volume of the tail.
20
+
21
+ ## Styles
22
+
23
+ `style` selects the joint geometry, and **it determines which of the other arguments have any effect**:
24
+
25
+ - `DovetailStyle.SNUGTAIL` *(default)* — an updated design for 3d printing that wraps around three sides of the object for a tighter fit and a large glue/friction surface.
26
+ - `DovetailStyle.TRADITIONAL` — a traditional woodworking dovetail.
27
+ - `DovetailStyle.T_SLOT` — a T-shaped tail, sized by slot count and depth rather than by tongue ratios.
28
+
29
+ ## Arguments
30
+
31
+ ### Always applicable
32
+
33
+ - `part` (Part): The part to split into a dovetail or socket part. The part should be oriented along the XY plane.
34
+ - `start` (Point): The start point along the XY Plane for the dovetail line.
35
+ - `end` (Point): The end point along the XY Plane for the dovetail line.
36
+ - `subpart` (DovetailSubpart, default=`DovetailSubpart.TAIL`): Which subpart to create — `DovetailSubpart.TAIL` or `DovetailSubpart.SOCKET`.
37
+ - `style` (DovetailStyle, default=`DovetailStyle.SNUGTAIL`): The dovetail style. See [Styles](#styles).
38
+ - `tolerance` (float, default=0.025): The clearance between tail and socket, in mm.
39
+ - `vertical_tolerance` (float, default=0.2): Additional tolerance for vertical offset, given that in printing, supports or bridging introduce additional volume.
40
+ - `scarf_angle` (float, default=0): Places the entire cut and dovetail at an angle along the Z-axis. Likely to improve stability in some parts.
41
+ - `taper_angle` (float, default=0): Tapers the dovetail by the given angle. Even a small taper angle can allow for easier assembly.
42
+ - `vertical_offset` (float, default=0): Offsets the dovetail along the Z axis, producing a straight cut on one side that acts as a hard stop when fitting. A positive value gives a straight cut on the bottom of the part, a negative value on the top.
43
+ - `click_fit_radius` (float, default=0): The radius of the click-fit divots. `0` disables them.
44
+
45
+ ### Style-conditional
46
+
47
+ The table below reflects what each parameter actually changes. Passing a parameter to a style that does not use it is currently accepted and silently ignored.
48
+
49
+ | Argument | Default | TRADITIONAL | SNUGTAIL | T_SLOT |
50
+ |---|---|---|---|---|
51
+ | `length_ratio` | 1/3 | ✅ | ✅ | — |
52
+ | `depth_ratio` | 1/6 | ✅ | — (see below) | — |
53
+ | `tail_angle_offset` | 15 | ✅ | ✅ | — |
54
+ | `linear_offset` | 0 | ✅ | — | — |
55
+ | `slot_count` | 1 | — | — | ✅ |
56
+ | `depth` | 2 | — | — | ✅ |
57
+
58
+ - `length_ratio` (float, default=1/3): The ratio of the length of the tongue to the total length of the cut.
59
+ - `depth_ratio` (float, default=1/6): The ratio of the depth of the tongue to the total length of the cut.
60
+ - `tail_angle_offset` (float, default=15): The adjustment pitch of the angle of the dovetail. `0` results in a square dovetail.
61
+ - `linear_offset` (float, default=0): Offsets the center of the tail or socket along the line by the amount specified. This slides the joint along the cut without changing its volume.
62
+ - `slot_count` (int, default=1): The number of slots to be added.
63
+ - `depth` (float, default=2): The depth of the T-slot into the socket.
64
+
65
+ !!! note "`depth_ratio` and SNUGTAIL"
66
+
67
+ SNUGTAIL does not accept `depth_ratio` from `dovetail_subpart`; it uses its own
68
+ prototyped value of `0.15`. This is deliberate, not an oversight. The snugtail depth
69
+ model was rewritten after physical prototyping so that `depth_ratio` no longer means
70
+ what it means for TRADITIONAL, and forwarding the shared value would change the
71
+ geometry of every snugtail joint. `length_ratio` and `tail_angle_offset` *are*
72
+ honored for SNUGTAIL.
73
+
74
+ ## Returns
75
+
76
+ - `Part`: The requested subpart — the tail or the socket, per `subpart`.
77
+
78
+ ## Example
79
+
80
+ ```python
81
+ from build123d import Align, Box, BuildPart, Mode
82
+ from b3dkit import Point, DovetailSubpart, DovetailStyle, dovetail_subpart
83
+
84
+ with BuildPart(mode=Mode.PRIVATE) as longbox:
85
+ Box(50, 40, 50, align=(Align.CENTER, Align.CENTER, Align.MIN))
86
+
87
+ start = Point(0, -20)
88
+ end = Point(0, 20)
89
+
90
+ # A mating pair with default parameters (SNUGTAIL).
91
+ tail = dovetail_subpart(longbox.part, start, end, subpart=DovetailSubpart.TAIL)
92
+ socket = dovetail_subpart(longbox.part, start, end, subpart=DovetailSubpart.SOCKET)
93
+ ```
94
+
95
+ Both subparts of a joint must be built from the same arguments — only `subpart` may
96
+ differ. Any other divergence produces two subparts that are individually valid and do
97
+ not fit together.
98
+
99
+ ```python
100
+ # A traditional dovetail with custom proportions.
101
+ joint = dict(
102
+ style=DovetailStyle.TRADITIONAL,
103
+ tolerance=0.1,
104
+ scarf_angle=5,
105
+ taper_angle=2.0,
106
+ length_ratio=0.7,
107
+ depth_ratio=1 / 4,
108
+ click_fit_radius=0.2,
109
+ )
110
+
111
+ tail = dovetail_subpart(longbox.part, start, end, subpart=DovetailSubpart.TAIL, **joint)
112
+ socket = dovetail_subpart(longbox.part, start, end, subpart=DovetailSubpart.SOCKET, **joint)
113
+ ```
114
+
115
+ ## Raises
116
+
117
+ - `ValueError`: if `start` and `end` are the same point.
118
+ - `ValueError`: if `abs(vertical_offset)` exceeds the part's height.
119
+ - `ValueError`: if `vertical_offset` is negative and `taper_angle` is negative.
120
+ - `ValueError`: if `vertical_offset` is positive and `taper_angle` is positive.
121
+ - `ValueError`: for SNUGTAIL, if `length_ratio + depth_ratio` exceeds 1.
@@ -84,6 +84,17 @@ Creates only the base component of the box. This includes the hollowed-out inter
84
84
  **Returns:**
85
85
  - `Part`: The base part with hollowed interior and rail channels
86
86
 
87
+ ## Dimension requirements
88
+
89
+ All three functions validate their inputs and raise `ValueError` if the part cannot be
90
+ built as a single solid:
91
+
92
+ - `top_height`, `rail_height` and `wall_thickness` must all be greater than 0.
93
+ - `wall_thickness * 2` must be less than the smaller of the part's width and depth.
94
+ - At least `wall_thickness` of the part's height must remain below the rails, i.e.
95
+ `part_height - top_height - rail_height >= wall_thickness`. With less than that, the
96
+ divots have too little material to fuse into and break away as separate solids.
97
+
87
98
  ## Design Considerations
88
99
 
89
100
  ### Rail System
@@ -64,34 +64,6 @@ Creates only the sliding lid component of the box. This is useful when you need
64
64
  **Returns:**
65
65
  - `Part`: The sliding lid part with taper and optional thumb grip
66
66
 
67
- ### slider_template
68
-
69
- ```python
70
- def slider_template(
71
- sketch: Sketch,
72
- wall_thickness: float = 2,
73
- tolerance: float = 0.2,
74
- top_offset: float = 0,
75
- x_straighten_distance: float = 0,
76
- divot_radius: float = 0,
77
- cut_template: bool = True,
78
- ) -> Part
79
- ```
80
-
81
- Creates a slider template part based on a 2D sketch. This is an internal function used to generate the sliding mechanism geometry.
82
-
83
- **Arguments:**
84
- - `sketch` (Sketch): 2D sketch defining the slider cross-section
85
- - `wall_thickness` (float, default=2): Thickness of the walls
86
- - `tolerance` (float, default=0.2): Clearance for the sliding fit
87
- - `top_offset` (float, default=0): Vertical offset from the top
88
- - `x_straighten_distance` (float, default=0): Distance for straight sections at edges
89
- - `divot_radius` (float, default=0): Radius of divots
90
- - `cut_template` (bool, default=True): Whether this is for cutting (True) or building (False)
91
-
92
- **Returns:**
93
- - `Part`: The slider template part
94
-
95
67
  ## Design Principles
96
68
 
97
69
  ### Tapered Sliding Mechanism
@@ -1,19 +1,19 @@
1
1
  [project]
2
2
  name = "b3dkit"
3
- version = "0.1.5"
3
+ version = "0.3.2"
4
4
  authors = [
5
5
  { name="x0pherl"},
6
6
  ]
7
7
  description = "build123d libraries and utilities"
8
8
  readme = "README.md"
9
- requires-python = ">=3.10"
9
+ requires-python = ">=3.11"
10
10
  classifiers = [
11
11
  "Programming Language :: Python :: 3",
12
12
  "License :: OSI Approved :: MIT License",
13
13
  "Operating System :: OS Independent",
14
14
  ]
15
15
  dependencies = [
16
- "build123d>=0.10.0",
16
+ "build123d>=0.11.0",
17
17
  "ocp_vscode>=3.1.1",
18
18
  ]
19
19
 
@@ -48,6 +48,32 @@ build-backend = "hatchling.build"
48
48
  exclude = [".venv-1"]
49
49
 
50
50
  [tool.pytest.ini_options]
51
+ pythonpath = ["src"]
52
+ addopts = "--strict-markers"
51
53
  markers = [
52
54
  "manual: marks tests that can only be executed manually",
53
- ]
55
+ ]
56
+
57
+ [tool.black]
58
+ line-length = 88
59
+ target-version = ["py311"]
60
+
61
+ [tool.ruff]
62
+ line-length = 88
63
+ target-version = "py311"
64
+ src = ["src", "tests"]
65
+
66
+ [tool.ruff.lint]
67
+ select = ["E", "F", "I", "UP", "B"]
68
+ ignore = [
69
+ "E501", # line length is black's job
70
+ "B008", # build123d builder-context idioms call in argument defaults
71
+ # build123d's builder pattern is `with BuildPart() as p:` where the binding
72
+ # is often unused -- the context manager does the work. 10 of the 12 F841
73
+ # hits in this package are that idiom, so the rule costs more than it earns.
74
+ "F841",
75
+ ]
76
+
77
+ [tool.ruff.lint.per-file-ignores]
78
+ # demo blocks under __main__ import the viewer after the module body starts
79
+ "src/b3dkit/*.py" = ["E402"]
@@ -0,0 +1,92 @@
1
+ """build123d libraries and utilities.
2
+
3
+ The names listed in ``__all__`` are b3dkit's public API; anything else is an
4
+ implementation detail.
5
+ """
6
+
7
+ from b3dkit.antichamfer import anti_chamfer
8
+ from b3dkit.ball_socket import BallMount, BallSocket
9
+ from b3dkit.basic_shapes import (
10
+ DiamondCylinder,
11
+ DiamondTorus,
12
+ PolygonalCylinder,
13
+ RoundedCylinder,
14
+ Teardrop,
15
+ TeardropCylinder,
16
+ adjacent_length,
17
+ apothem_to_radius,
18
+ circular_intersection,
19
+ distance_to_circle_edge,
20
+ opposite_length,
21
+ radius_to_apothem,
22
+ )
23
+ from b3dkit.bolt_fittings import (
24
+ BoltCutSinkhole,
25
+ HeatsinkCut,
26
+ NutCut,
27
+ ScrewCut,
28
+ SquareNutSinkhole,
29
+ TeardropBoltCutSinkhole,
30
+ )
31
+ from b3dkit.click_fit import Divot
32
+ from b3dkit.dovetail import DovetailStyle, DovetailSubpart, dovetail_subpart
33
+ from b3dkit.hexwall import HexCylindrical, HexWall
34
+ from b3dkit.high_top_slide_box import (
35
+ high_top_slide_box,
36
+ high_top_slide_box_base,
37
+ high_top_slide_box_lid,
38
+ )
39
+ from b3dkit.point import Point, midpoint, shifted_midpoint
40
+ from b3dkit.slide_box import slide_box, slide_lid
41
+ from b3dkit.twist_snap import TwistSnapConnector, TwistSnapSocket
42
+
43
+ __all__ = [
44
+ # antichamfer
45
+ "anti_chamfer",
46
+ # ball_socket
47
+ "BallMount",
48
+ "BallSocket",
49
+ # basic_shapes
50
+ "DiamondCylinder",
51
+ "DiamondTorus",
52
+ "PolygonalCylinder",
53
+ "RoundedCylinder",
54
+ "Teardrop",
55
+ "TeardropCylinder",
56
+ "adjacent_length",
57
+ "apothem_to_radius",
58
+ "circular_intersection",
59
+ "distance_to_circle_edge",
60
+ "opposite_length",
61
+ "radius_to_apothem",
62
+ # bolt_fittings
63
+ "BoltCutSinkhole",
64
+ "HeatsinkCut",
65
+ "NutCut",
66
+ "ScrewCut",
67
+ "SquareNutSinkhole",
68
+ "TeardropBoltCutSinkhole",
69
+ # click_fit
70
+ "Divot",
71
+ # dovetail
72
+ "DovetailSubpart",
73
+ "DovetailStyle",
74
+ "dovetail_subpart",
75
+ # hexwall
76
+ "HexCylindrical",
77
+ "HexWall",
78
+ # high_top_slide_box
79
+ "high_top_slide_box",
80
+ "high_top_slide_box_base",
81
+ "high_top_slide_box_lid",
82
+ # point
83
+ "Point",
84
+ "midpoint",
85
+ "shifted_midpoint",
86
+ # slide_box
87
+ "slide_box",
88
+ "slide_lid",
89
+ # twist_snap
90
+ "TwistSnapConnector",
91
+ "TwistSnapSocket",
92
+ ]
@@ -1,10 +1,12 @@
1
+ from math import atan, degrees
2
+
1
3
  from build123d import (
2
4
  Align,
3
5
  Axis,
4
6
  BasePartObject,
5
7
  Box,
6
- BuildPart,
7
8
  Builder,
9
+ BuildPart,
8
10
  Compound,
9
11
  Face,
10
12
  Iterable,
@@ -17,8 +19,11 @@ from build123d import (
17
19
  flatten_sequence,
18
20
  validate_inputs,
19
21
  )
20
- from math import atan, degrees, tan, radians
21
- from ocp_vscode import show, Camera
22
+ from ocp_vscode import Camera, show
23
+
24
+ __all__ = [
25
+ "anti_chamfer",
26
+ ]
22
27
 
23
28
 
24
29
  def anti_chamfer(