b3dkit 0.1.0__tar.gz → 0.1.1__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 (56) hide show
  1. {b3dkit-0.1.0 → b3dkit-0.1.1}/.coverage +0 -0
  2. {b3dkit-0.1.0 → b3dkit-0.1.1}/PKG-INFO +6 -3
  3. {b3dkit-0.1.0 → b3dkit-0.1.1}/pyproject.toml +6 -2
  4. {b3dkit-0.1.0 → b3dkit-0.1.1}/src/b3dkit/__init__.py +1 -0
  5. {b3dkit-0.1.0 → b3dkit-0.1.1}/src/b3dkit/bolt_fittings.py +20 -8
  6. {b3dkit-0.1.0 → b3dkit-0.1.1}/src/b3dkit/hexwall.py +3 -3
  7. b3dkit-0.1.1/tests/test_antichamfer.py +147 -0
  8. b3dkit-0.1.1/tests/test_ball_socket.py +105 -0
  9. b3dkit-0.1.1/tests/test_bolt_fittings.py +215 -0
  10. {b3dkit-0.1.0 → b3dkit-0.1.1}/tests/test_dovetail.py +0 -15
  11. {b3dkit-0.1.0 → b3dkit-0.1.1}/tests/test_hexwall.py +1 -1
  12. b3dkit-0.1.0/tests/test_antichamfer.py +0 -426
  13. b3dkit-0.1.0/tests/test_ball_socket.py +0 -249
  14. b3dkit-0.1.0/tests/test_bolt_fittings.py +0 -538
  15. {b3dkit-0.1.0 → b3dkit-0.1.1}/.coveragerc +0 -0
  16. {b3dkit-0.1.0 → b3dkit-0.1.1}/.gitignore +0 -0
  17. {b3dkit-0.1.0 → b3dkit-0.1.1}/.vscode/settings.json +0 -0
  18. {b3dkit-0.1.0 → b3dkit-0.1.1}/LICENSE +0 -0
  19. {b3dkit-0.1.0 → b3dkit-0.1.1}/README.md +0 -0
  20. {b3dkit-0.1.0 → b3dkit-0.1.1}/build.bat +0 -0
  21. {b3dkit-0.1.0 → b3dkit-0.1.1}/build.sh +0 -0
  22. {b3dkit-0.1.0 → b3dkit-0.1.1}/docs/Makefile +0 -0
  23. {b3dkit-0.1.0 → b3dkit-0.1.1}/docs/antichamfer.md +0 -0
  24. {b3dkit-0.1.0 → b3dkit-0.1.1}/docs/ball_socket.md +0 -0
  25. {b3dkit-0.1.0 → b3dkit-0.1.1}/docs/basic_shapes.md +0 -0
  26. {b3dkit-0.1.0 → b3dkit-0.1.1}/docs/bolt_fittings.md +0 -0
  27. {b3dkit-0.1.0 → b3dkit-0.1.1}/docs/click_fit.md +0 -0
  28. {b3dkit-0.1.0 → b3dkit-0.1.1}/docs/conf.py +0 -0
  29. {b3dkit-0.1.0 → b3dkit-0.1.1}/docs/dovetail.md +0 -0
  30. {b3dkit-0.1.0 → b3dkit-0.1.1}/docs/dovetail.png +0 -0
  31. {b3dkit-0.1.0 → b3dkit-0.1.1}/docs/hexwall.md +0 -0
  32. {b3dkit-0.1.0 → b3dkit-0.1.1}/docs/high_top_slide_box.md +0 -0
  33. {b3dkit-0.1.0 → b3dkit-0.1.1}/docs/index.md +0 -0
  34. {b3dkit-0.1.0 → b3dkit-0.1.1}/docs/point.md +0 -0
  35. {b3dkit-0.1.0 → b3dkit-0.1.1}/docs/slide_box.md +0 -0
  36. {b3dkit-0.1.0 → b3dkit-0.1.1}/docs/twist_snap.md +0 -0
  37. {b3dkit-0.1.0 → b3dkit-0.1.1}/docs/twist_snap.png +0 -0
  38. {b3dkit-0.1.0 → b3dkit-0.1.1}/mkdocs.yml +0 -0
  39. {b3dkit-0.1.0 → b3dkit-0.1.1}/mkdocs_new.yml +0 -0
  40. {b3dkit-0.1.0 → b3dkit-0.1.1}/readthedocs.yaml +0 -0
  41. {b3dkit-0.1.0 → b3dkit-0.1.1}/src/b3dkit/antichamfer.py +0 -0
  42. {b3dkit-0.1.0 → b3dkit-0.1.1}/src/b3dkit/ball_socket.py +0 -0
  43. {b3dkit-0.1.0 → b3dkit-0.1.1}/src/b3dkit/basic_shapes.py +0 -0
  44. {b3dkit-0.1.0 → b3dkit-0.1.1}/src/b3dkit/click_fit.py +0 -0
  45. {b3dkit-0.1.0 → b3dkit-0.1.1}/src/b3dkit/dovetail.py +0 -0
  46. {b3dkit-0.1.0 → b3dkit-0.1.1}/src/b3dkit/high_top_slide_box.py +0 -0
  47. {b3dkit-0.1.0 → b3dkit-0.1.1}/src/b3dkit/point.py +0 -0
  48. {b3dkit-0.1.0 → b3dkit-0.1.1}/src/b3dkit/slide_box.py +0 -0
  49. {b3dkit-0.1.0 → b3dkit-0.1.1}/src/b3dkit/twist_snap.py +0 -0
  50. {b3dkit-0.1.0 → b3dkit-0.1.1}/tests/conftest.py +0 -0
  51. {b3dkit-0.1.0 → b3dkit-0.1.1}/tests/test_basic_shapes.py +0 -0
  52. {b3dkit-0.1.0 → b3dkit-0.1.1}/tests/test_click_fit.py +0 -0
  53. {b3dkit-0.1.0 → b3dkit-0.1.1}/tests/test_high_top_slide_box.py +0 -0
  54. {b3dkit-0.1.0 → b3dkit-0.1.1}/tests/test_point.py +0 -0
  55. {b3dkit-0.1.0 → b3dkit-0.1.1}/tests/test_slide_box.py +0 -0
  56. {b3dkit-0.1.0 → b3dkit-0.1.1}/tests/test_twist_snap.py +0 -0
Binary file
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: b3dkit
3
- Version: 0.1.0
3
+ Version: 0.1.1
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
@@ -14,14 +14,17 @@ Requires-Python: >=3.8
14
14
  Requires-Dist: build123d>=0.0.1
15
15
  Requires-Dist: ocp-vscode
16
16
  Provides-Extra: dev
17
- Requires-Dist: build; extra == 'dev'
18
- Requires-Dist: hatchling; extra == 'dev'
19
17
  Requires-Dist: pytest; extra == 'dev'
20
18
  Requires-Dist: pytest-cov; extra == 'dev'
21
19
  Provides-Extra: docs
22
20
  Requires-Dist: markdown-include; extra == 'docs'
23
21
  Requires-Dist: mkdocs; extra == 'docs'
24
22
  Requires-Dist: mkdocs-material; extra == 'docs'
23
+ Provides-Extra: maintain
24
+ Requires-Dist: build; extra == 'maintain'
25
+ Requires-Dist: hatch-vcs; extra == 'maintain'
26
+ Requires-Dist: hatchling; extra == 'maintain'
27
+ Requires-Dist: twine; extra == 'maintain'
25
28
  Description-Content-Type: text/markdown
26
29
 
27
30
  # b3dkit Overview
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "b3dkit"
3
- version = "0.1.0"
3
+ version = "0.1.1"
4
4
  authors = [
5
5
  { name="x0pherl"},
6
6
  ]
@@ -21,9 +21,13 @@ dependencies = [
21
21
  dev = [
22
22
  "pytest",
23
23
  "pytest-cov",
24
+ ]
25
+ maintain =[
26
+ "twine",
27
+ "hatch-vcs",
24
28
  "hatchling",
25
29
  "build",
26
- ]
30
+ ]
27
31
  docs = [
28
32
  "mkdocs",
29
33
  "mkdocs-material",
@@ -1,5 +1,6 @@
1
1
  from b3dkit.basic_shapes import *
2
2
  from b3dkit.ball_socket import *
3
+ from b3dkit.bolt_fittings import *
3
4
  from b3dkit.click_fit import *
4
5
  from b3dkit.dovetail import *
5
6
  from b3dkit.hexwall import *
@@ -85,12 +85,12 @@ class TeardropBoltCutSinkhole(BasePartObject):
85
85
  sinkhole.faces().sort_by(Axis.Z)[-1],
86
86
  amount=extension_distance,
87
87
  )
88
- final_template = anti_chamfer(
89
- sinkhole.part.faces().sort_by(Axis.Z)[-1],
90
- chamfer_radius,
91
- )
88
+ anti_chamfer(
89
+ sinkhole.part.faces().sort_by(Axis.Z)[-1],
90
+ chamfer_radius,
91
+ )
92
92
  super().__init__(
93
- part=final_template,
93
+ part=sinkhole.part,
94
94
  rotation=rotation,
95
95
  align=tuplify(align, 3),
96
96
  mode=mode,
@@ -132,7 +132,7 @@ class BoltCutSinkhole(BasePartObject):
132
132
  - mode: the mode to use when adding the sinkhole
133
133
  Returns:
134
134
  - Part: A cylindrical bolt hole part with countersink"""
135
- pt = TeardropBoltCutSinkhole(
135
+ sinkhole = TeardropBoltCutSinkhole(
136
136
  shaft_radius=shaft_radius,
137
137
  shaft_depth=shaft_depth,
138
138
  head_radius=head_radius,
@@ -140,8 +140,17 @@ class BoltCutSinkhole(BasePartObject):
140
140
  chamfer_radius=chamfer_radius,
141
141
  extension_distance=extension_distance,
142
142
  teardrop_ratio=1.0,
143
+ rotation=rotation,
144
+ align=tuplify(align, 3),
145
+ mode=mode,
146
+ )
147
+
148
+ super().__init__(
149
+ part=sinkhole,
150
+ rotation=rotation,
151
+ align=tuplify(align, 3),
152
+ mode=mode,
143
153
  )
144
- super().__init__(part=pt, rotation=rotation, align=tuplify(align, 3), mode=mode)
145
154
 
146
155
 
147
156
  class SquareNutSinkhole(BasePartObject):
@@ -348,4 +357,7 @@ class HeatsinkCut(BasePartObject):
348
357
 
349
358
 
350
359
  if __name__ == "__main__":
351
- show(SquareNutSinkhole(), reset_camera=Camera.KEEP)
360
+ with BuildPart() as tst:
361
+ Box(20, 20, 20)
362
+ BoltCutSinkhole(mode=Mode.SUBTRACT)
363
+ show(tst.part, reset_camera=Camera.KEEP)
@@ -90,10 +90,10 @@ class HexWall(BasePartObject):
90
90
  if __name__ == "__main__":
91
91
  show(
92
92
  HexWall(
93
- width=200,
94
- length=400,
93
+ width=20,
94
+ length=40,
95
95
  height=2,
96
- apothem=10,
96
+ apothem=9,
97
97
  wall_thickness=2,
98
98
  inverse=True,
99
99
  align=(Align.CENTER, Align.CENTER, Align.MIN),
@@ -0,0 +1,147 @@
1
+ import pytest
2
+ from unittest.mock import patch
3
+ from importlib.machinery import SourceFileLoader
4
+ from importlib.util import module_from_spec, spec_from_loader
5
+ from math import atan, degrees
6
+ from build123d import (
7
+ Align,
8
+ Axis,
9
+ Box,
10
+ BuildPart,
11
+ Cylinder,
12
+ Face,
13
+ Part,
14
+ Plane,
15
+ )
16
+ from b3dkit.antichamfer import anti_chamfer
17
+
18
+
19
+ class TestAntiChamfer:
20
+ def test_anti_chamfer_in_build_context(self):
21
+ """Test anti_chamfer within a BuildPart context"""
22
+
23
+ with BuildPart() as bkt:
24
+ Box(
25
+ 10,
26
+ 10,
27
+ 10,
28
+ )
29
+ anti_chamfer(bkt.faces().filter_by(Axis.Z), 2, 1)
30
+
31
+ assert bkt.part.is_valid
32
+ original_volume = 10 * 10 * 10
33
+ assert bkt.part.volume > original_volume
34
+
35
+ def test_anti_chamfer_single_face(self):
36
+ """Test anti_chamfer with a single face"""
37
+ with BuildPart() as bp:
38
+ Box(10, 10, 10)
39
+ original_part = bp.part
40
+
41
+ top_face = original_part.faces().filter_by(Axis.Z)[-1]
42
+
43
+ ac = anti_chamfer(top_face, 2.0, 1.0)
44
+
45
+ assert ac.is_valid
46
+ assert ac.volume > original_part.volume
47
+
48
+ def test_anti_chamfer_empty_face(self):
49
+ """Test anti_chamfer with an empty face list"""
50
+ with pytest.raises(ValueError):
51
+ anti_chamfer([], 2.0, 1.0)
52
+
53
+ def test_anti_chamfer_float_face(self):
54
+ """Test anti_chamfer with a non-Face input"""
55
+ with pytest.raises(ValueError):
56
+ anti_chamfer([3.2], 2.0, 1.0)
57
+
58
+ def test_contextless_face(self):
59
+ """Test anti_chamfer with a Face that has no context Part"""
60
+ test_face = Face(Plane.XY)
61
+ with pytest.raises(ValueError):
62
+ anti_chamfer(test_face, 1.0, 1.0)
63
+
64
+ def test_anti_chamfer_multiple_faces(self):
65
+ """Test anti_chamfer with multiple faces (iterable)"""
66
+ with BuildPart() as bp:
67
+ Box(10, 10, 10)
68
+ original_part = bp.part
69
+
70
+ ac = anti_chamfer(original_part.faces().filter_by(Axis.Z), 1.5, 1.0)
71
+ assert ac.is_valid
72
+ assert ac.volume > original_part.volume
73
+
74
+ def test_anti_chamfer_length2_none_default(self):
75
+ """Test anti_chamfer with length2=None (should default to length)"""
76
+ with BuildPart() as bp:
77
+ Box(10, 10, 10)
78
+ original_part = bp.part
79
+
80
+ top_face = original_part.faces().filter_by(Axis.Z)[-1]
81
+
82
+ ac1 = anti_chamfer(top_face, 2.0, None)
83
+ ac2 = anti_chamfer(top_face, 2.0, 2.0)
84
+
85
+ assert pytest.approx(0) == abs(ac1.volume - ac2.volume)
86
+
87
+ def test_anti_chamfer_different_length_values(self):
88
+ """Test anti_chamfer with different length and length2 values"""
89
+ with BuildPart() as bp:
90
+ Box(10, 10, 10)
91
+ original_part = bp.part
92
+
93
+ top_face = original_part.faces().filter_by(Axis.Z)[-1]
94
+
95
+ ac1 = anti_chamfer(top_face, 1.0, 0.5)
96
+ ac2 = anti_chamfer(top_face, 2.0, 1.0)
97
+ ac3 = anti_chamfer(top_face, 1.0, 2.0)
98
+
99
+ assert ac1.is_valid
100
+ assert ac2.is_valid
101
+ assert ac3.is_valid
102
+
103
+ assert ac1.volume != ac2.volume != ac3.volume
104
+
105
+ def test_anti_chamfer_with_cylinder(self):
106
+ """Test anti_chamfer works with a round face"""
107
+ with BuildPart() as bp:
108
+ Cylinder(5, 10)
109
+ original_part = bp.part
110
+
111
+ ac_part = anti_chamfer(bp.part.faces().filter_by(Axis.Z)[-1], 1.0, 0.8)
112
+
113
+ assert ac_part.is_valid
114
+ assert ac_part.volume > original_part.volume
115
+
116
+ def test_anti_chamfer_negative_length_values(self):
117
+ """Test anti_chamfer with negative length values"""
118
+ with BuildPart() as bp:
119
+ Box(10, 10, 10)
120
+
121
+ ac_part = anti_chamfer(bp.part.faces().filter_by(Axis.Z)[-1], -1.0, -0.5)
122
+ assert ac_part.is_valid
123
+
124
+ def test_anti_chamfer_zero_length_values(self):
125
+ """Test anti_chamfer with negative length values"""
126
+ with BuildPart() as bp:
127
+ Box(10, 10, 10)
128
+
129
+ ac1 = anti_chamfer(bp.part.faces().filter_by(Axis.Z)[-1], 1, 0)
130
+ ac2 = anti_chamfer(bp.part.faces().filter_by(Axis.Z)[-1], 0, 1)
131
+
132
+ assert ac1.is_valid
133
+ assert ac2.is_valid
134
+ assert bp.part.volume == ac1.volume == ac2.volume
135
+
136
+ def test_direct_run(self):
137
+
138
+ with (
139
+ patch("build123d.export_stl"),
140
+ patch("pathlib.Path.mkdir"),
141
+ patch("pathlib.Path.exists"),
142
+ patch("pathlib.Path.is_dir"),
143
+ patch("ocp_vscode.show"),
144
+ patch("ocp_vscode.save_screenshot"),
145
+ ):
146
+ loader = SourceFileLoader("__main__", "src/b3dkit/antichamfer.py")
147
+ loader.exec_module(module_from_spec(spec_from_loader(loader.name, loader)))
@@ -0,0 +1,105 @@
1
+ from importlib.machinery import SourceFileLoader
2
+ from importlib.util import module_from_spec, spec_from_loader
3
+ from unittest.mock import patch
4
+ import math
5
+ import pytest
6
+ from build123d import Part
7
+
8
+ from b3dkit.ball_socket import BallMount, BallSocket
9
+
10
+
11
+ # ---------- Helpers ----------
12
+ def expected_socket_height(r: float, w: float) -> float:
13
+ # Matches current implementation: cylinder height = ball_radius + wall_thickness * 2.5
14
+ return r + w * 2.5
15
+
16
+
17
+ def expected_socket_diameter(r: float, w: float) -> float:
18
+ return 2 * (r + w)
19
+
20
+
21
+ # ---------- Ball Mount Tests ----------
22
+ class TestBallMount:
23
+ def test_ball_mount_basic(self):
24
+ mount = BallMount(10.0)
25
+ assert isinstance(mount, Part)
26
+ assert mount.is_valid
27
+ bbox = mount.bounding_box()
28
+ assert bbox.size.X == pytest.approx(20.0, abs=0.1)
29
+ assert bbox.size.Y == pytest.approx(20.0, abs=0.1)
30
+ # Height = 3.5 * radius (shaft from 0 to 2.25R, sphere center at 2.5R -> top 3.5R)
31
+ assert bbox.size.Z == pytest.approx(35.0, abs=0.5)
32
+
33
+
34
+ class TestBallSocket:
35
+ def test_ball_socket_basic(self):
36
+ r = 10.0
37
+ w = 2.0
38
+ socket = BallSocket(r)
39
+ assert isinstance(socket, Part)
40
+ assert socket.is_valid
41
+ assert socket.label == "Ball Socket"
42
+
43
+ @pytest.mark.parametrize("r,w", [(3, 2), (5, 1), (10, 2), (20, 4), (12.5, 3.5)])
44
+ def test_ball_socket_param_dimensions(self, r, w):
45
+ socket = BallSocket(r, wall_thickness=w)
46
+ assert socket.is_valid
47
+ bbox = socket.bounding_box()
48
+
49
+ def test_ball_socket_custom_wall_thickness(self):
50
+ r, w = 10.0, 3.0
51
+ socket = BallSocket(r, wall_thickness=w)
52
+
53
+ def test_ball_socket_tolerance_does_not_change_outer_size(self):
54
+ r, w = 10.0, 2.0
55
+ base_bbox = BallSocket(r).bounding_box()
56
+ bbox = BallSocket(r, tolerance=0.2).bounding_box()
57
+ assert bbox.size.X == pytest.approx(base_bbox.size.X, abs=0.05)
58
+ assert bbox.size.Z == pytest.approx(base_bbox.size.Z, abs=0.05)
59
+
60
+ def test_ball_socket_wall_thickness_volume_growth(self):
61
+ r = 10.0
62
+ thin = BallSocket(r, wall_thickness=1.0)
63
+ thick = BallSocket(r, wall_thickness=5.0)
64
+ assert thin.volume < thick.volume
65
+
66
+ def test_ball_socket_centered(self):
67
+ socket = BallSocket(10.0)
68
+ bbox = socket.bounding_box()
69
+ assert abs(bbox.center().X) < 0.01
70
+ assert abs(bbox.center().Y) < 0.01
71
+ assert bbox.min.Z == pytest.approx(0.0, abs=0.01)
72
+
73
+ def test_ball_socket_small_radius(self):
74
+ r, w = 3.0, 2.0
75
+ socket = BallSocket(r)
76
+ bbox = socket.bounding_box()
77
+ assert bbox.size.X == pytest.approx(expected_socket_diameter(r, w), abs=0.1)
78
+ assert bbox.size.Z == pytest.approx(expected_socket_height(r, w), abs=0.1)
79
+
80
+ def test_ball_socket_has_flex_cuts_volume_reduction(self):
81
+ r, w = 10.0, 2.0
82
+ socket = BallSocket(r)
83
+ assert socket.volume > 0
84
+ # Compare to solid cylinder of same outer size
85
+ solid_volume = math.pi * (r + w) ** 2 * expected_socket_height(r, w)
86
+ assert socket.volume < solid_volume * 0.9 # should be noticeably reduced
87
+
88
+
89
+ # ---------- Edge / Extreme Cases ----------
90
+ class TestEdgeCases:
91
+
92
+ def test_extreme_tolerance_values(self):
93
+ tight = BallSocket(10.0, tolerance=-0.1)
94
+ loose = BallSocket(10.0, tolerance=1.0)
95
+ assert tight.is_valid
96
+ assert loose.is_valid
97
+ assert loose.volume < BallSocket(10.0, tolerance=0.0).volume
98
+
99
+
100
+ # ---------- Direct Run ----------
101
+ class TestDirectRun:
102
+ def test_direct_run(self):
103
+ with patch("ocp_vscode.show"):
104
+ loader = SourceFileLoader("__main__", "src/b3dkit/ball_socket.py")
105
+ loader.exec_module(module_from_spec(spec_from_loader(loader.name, loader)))
@@ -0,0 +1,215 @@
1
+ """
2
+ Comprehensive tests for the bolt_fittings module.
3
+
4
+ These tests cover all functions and their parameters to ensure:
5
+ - Correct geometry creation
6
+ - Parameter variations and edge cases
7
+ - Return type validation
8
+ - Dimensional accuracy
9
+ - Mutation testing coverage
10
+ """
11
+
12
+ from importlib.machinery import SourceFileLoader
13
+ from importlib.util import module_from_spec, spec_from_loader
14
+ from unittest.mock import patch
15
+ import pytest
16
+ from build123d import (
17
+ Align,
18
+ Axis,
19
+ BuildPart,
20
+ Part,
21
+ )
22
+ from b3dkit.bolt_fittings import (
23
+ TeardropBoltCutSinkhole,
24
+ ScrewCut,
25
+ NutCut,
26
+ BoltCutSinkhole,
27
+ HeatsinkCut,
28
+ SquareNutSinkhole,
29
+ )
30
+
31
+
32
+ class TestTeardropBoltCutSinkhole:
33
+ def test_teardrop_bolt_cut_default_parameters(self):
34
+ """Test teardrop bolt cut with default parameters"""
35
+ sinkhole = TeardropBoltCutSinkhole()
36
+
37
+ assert sinkhole.is_valid
38
+
39
+ def test_teardrop_bolt_cut_custom_shaft(self):
40
+ """Test teardrop bolt cut with custom shaft dimensions"""
41
+ sinkhole = TeardropBoltCutSinkhole(shaft_radius=2.0, shaft_depth=5.0)
42
+
43
+ assert sinkhole.is_valid
44
+
45
+ def test_teardrop_bolt_cut_custom_head(self):
46
+ """Test teardrop bolt cut with custom head dimensions"""
47
+ sinkhole = TeardropBoltCutSinkhole(head_radius=4.0, head_depth=3.0)
48
+
49
+ assert sinkhole.is_valid
50
+
51
+ def test_teardrop_bolt_cut_with_chamfer(self):
52
+ """Test teardrop bolt cut with various chamfer radii"""
53
+ sinkhole1 = TeardropBoltCutSinkhole(chamfer_radius=0.5)
54
+ sinkhole2 = TeardropBoltCutSinkhole(chamfer_radius=2.0)
55
+
56
+ assert sinkhole1.volume != sinkhole2.volume
57
+
58
+ def test_teardrop_bolt_cut_with_extension(self):
59
+ """Test teardrop bolt cut with extension distance"""
60
+ sinkhone_with = TeardropBoltCutSinkhole(extension_distance=50)
61
+ sinkhone_without = TeardropBoltCutSinkhole(extension_distance=0)
62
+
63
+ assert sinkhone_with.volume > sinkhone_without.volume
64
+
65
+ def test_teardrop_bolt_cut_zero_extension(self):
66
+ """Test teardrop bolt cut with zero extension (blind hole)"""
67
+ sinkhone = TeardropBoltCutSinkhole(extension_distance=0)
68
+
69
+ assert isinstance(sinkhone, Part)
70
+ assert sinkhone.volume > 0
71
+
72
+ def test_teardrop_bolt_cut_custom_teardrop_ratio(self):
73
+ """Test teardrop bolt cut with custom teardrop_ratio"""
74
+ boltcut1 = TeardropBoltCutSinkhole(teardrop_ratio=1.0) # Cylindrical
75
+ boltcut2 = TeardropBoltCutSinkhole(teardrop_ratio=1.1) # Default teardrop
76
+ boltcut3 = TeardropBoltCutSinkhole(
77
+ teardrop_ratio=1.2
78
+ ) # More pronounced teardrop
79
+
80
+ assert isinstance(boltcut1, Part)
81
+ assert isinstance(boltcut2, Part)
82
+ assert isinstance(boltcut3, Part)
83
+ # Larger ratios should produce larger volumes
84
+ assert boltcut1.volume < boltcut2.volume < boltcut3.volume
85
+
86
+
87
+ class TestBoltCutSinkhole:
88
+ def test_bolt_cut_default_parameters(self):
89
+ """Test bolt cut with default parameters"""
90
+ boltcut = BoltCutSinkhole()
91
+
92
+ assert boltcut.is_valid
93
+
94
+ def test_bolt_cut_with_chamfer(self):
95
+ """Test bolt cut with various chamfer radii"""
96
+ boltcut1 = BoltCutSinkhole(chamfer_radius=0.5)
97
+ boltcut2 = BoltCutSinkhole(chamfer_radius=2.0)
98
+
99
+ assert isinstance(boltcut1, Part)
100
+ assert isinstance(boltcut2, Part)
101
+ # Different chamfer radii should produce different volumes
102
+ assert boltcut1.volume != boltcut2.volume
103
+
104
+ def test_bolt_cut_with_extension(self):
105
+ """Test bolt cut with extension distance"""
106
+ boltcut_with = BoltCutSinkhole(extension_distance=50)
107
+ boltcut_without = BoltCutSinkhole(extension_distance=0)
108
+
109
+ assert boltcut_with.volume > boltcut_without.volume
110
+
111
+ def test_bolt_cut_zero_extension(self):
112
+ """Test bolt cut with zero extension (blind hole)"""
113
+ boltcut = BoltCutSinkhole(extension_distance=0)
114
+
115
+ assert boltcut.is_valid
116
+
117
+ def test_bolt_cut_vs_teardrop(self):
118
+ """Test that bolt_cut and TeardropBoltCutSinkhole with default ratio produce different results"""
119
+ params = {
120
+ "shaft_radius": 1.65,
121
+ "shaft_depth": 2.0,
122
+ "head_radius": 3.1,
123
+ "head_depth": 5.0,
124
+ "chamfer_radius": 1.0,
125
+ "extension_distance": 10.0,
126
+ }
127
+
128
+ boltcut = BoltCutSinkhole(**params)
129
+ teardropcut = TeardropBoltCutSinkhole(**params)
130
+
131
+ # Teardrop with default ratio should have more volume than cylindrical
132
+ assert boltcut.is_valid
133
+ assert teardropcut.is_valid
134
+ assert teardropcut.volume > boltcut.volume
135
+
136
+ def test_bolt_cut_is_wrapper_for_teardrop(self):
137
+ """Test that BoltCutSinkhole is a wrapper for TeardropBoltCutSinkhole with ratio=1.0"""
138
+ params = {
139
+ "shaft_radius": 2.0,
140
+ "shaft_depth": 3.0,
141
+ "head_radius": 4.0,
142
+ "head_depth": 6.0,
143
+ "chamfer_radius": 1.5,
144
+ "extension_distance": 20.0,
145
+ }
146
+
147
+ boltcut = BoltCutSinkhole(**params)
148
+ teardropcut = TeardropBoltCutSinkhole(**params, teardrop_ratio=1.0)
149
+
150
+ # Should produce identical results
151
+ assert boltcut.is_valid
152
+ assert teardropcut.is_valid
153
+ assert abs(boltcut.volume - teardropcut.volume) < 1e-6
154
+
155
+
156
+ class TestSquareNutSinkhole:
157
+ def test_square_nut_default_parameters(self):
158
+ """Test square nut sinkhole with default parameters"""
159
+ sinkhole = SquareNutSinkhole()
160
+
161
+ assert sinkhole.is_valid
162
+
163
+ def test_square_nut_with_extension(self):
164
+ """Test square nut sinkhole with bolt extension"""
165
+ sinkhole_with = SquareNutSinkhole(bolt_extension=5)
166
+ sinkhole_without = SquareNutSinkhole(bolt_extension=0)
167
+
168
+ assert sinkhole_with.volume > sinkhole_without.volume
169
+
170
+ def test_square_nut_zero_extension(self):
171
+ """Test square nut sinkhole with zero extension"""
172
+ sinkhole = SquareNutSinkhole(bolt_extension=0)
173
+
174
+ assert sinkhole.is_valid
175
+
176
+ def test_square_nut_different_nut_sizes(self):
177
+ """Test with different nut sizes"""
178
+ small_nut = SquareNutSinkhole(nut_legnth=4.0, nut_height=1.5)
179
+ large_nut = SquareNutSinkhole(nut_legnth=8.0, nut_height=3.0)
180
+
181
+ assert isinstance(small_nut, Part)
182
+ assert isinstance(large_nut, Part)
183
+ # Larger nut should have more volume
184
+ assert large_nut.volume > small_nut.volume
185
+
186
+
187
+ class TestScrewCut:
188
+ def test_screw_cut(self):
189
+ screw = ScrewCut(5, 1, 2, 10, 10)
190
+ assert screw.is_valid
191
+ assert screw.bounding_box().size.X == pytest.approx(10)
192
+ assert screw.bounding_box().size.Y == pytest.approx(10)
193
+ assert screw.bounding_box().size.Z == pytest.approx(20)
194
+
195
+ def test_nut_cut(self):
196
+ nut = NutCut(5, 1, 2, 10)
197
+ assert nut.is_valid
198
+
199
+ def test_invalid_screw_cut(self):
200
+ with pytest.raises(ValueError):
201
+ ScrewCut(head_radius=5, shaft_radius=6)
202
+
203
+ def test_heatsink_cut(self):
204
+ heatsink = HeatsinkCut(10, 1, 5, 10)
205
+ assert heatsink.is_valid
206
+ assert heatsink.bounding_box().size.X == pytest.approx(20)
207
+ assert heatsink.bounding_box().size.Y == pytest.approx(20)
208
+ assert heatsink.bounding_box().size.Z == pytest.approx(11)
209
+
210
+
211
+ class TestBareExecution:
212
+ def test_bare_execution(self):
213
+ with (patch("ocp_vscode.show"),):
214
+ loader = SourceFileLoader("__main__", "src/b3dkit/bolt_fittings.py")
215
+ loader.exec_module(module_from_spec(spec_from_loader(loader.name, loader)))
@@ -14,7 +14,6 @@ from b3dkit.point import Point
14
14
  from b3dkit.dovetail import (
15
15
  DovetailPart,
16
16
  DovetailStyle,
17
- dovetail_split_line,
18
17
  dovetail_subpart,
19
18
  snugtail_subpart_outline,
20
19
  dovetail_subpart_outline,
@@ -44,10 +43,6 @@ class TestDovetail:
44
43
  test.part,
45
44
  Point(5, 0),
46
45
  Point(5, 0),
47
- # scarf_distance=0.5,
48
- section=DovetailPart.TAIL,
49
- # tilt=20,
50
- vertical_offset=-100,
51
46
  ),
52
47
  )
53
48
 
@@ -60,9 +55,7 @@ class TestDovetail:
60
55
  test.part,
61
56
  Point(-5, 0),
62
57
  Point(5, 0),
63
- # scarf_distance=0.5,
64
58
  section=DovetailPart.TAIL,
65
- # tilt=20,
66
59
  vertical_offset=100,
67
60
  ),
68
61
  )
@@ -76,9 +69,7 @@ class TestDovetail:
76
69
  test.part,
77
70
  Point(-5, 0),
78
71
  Point(5, 0),
79
- # scarf_distance=0.5,
80
72
  section=DovetailPart.TAIL,
81
- # tilt=20,
82
73
  vertical_offset=-100,
83
74
  ),
84
75
  )
@@ -92,10 +83,8 @@ class TestDovetail:
92
83
  test.part,
93
84
  Point(-5, 0),
94
85
  Point(5, 0),
95
- # scarf_distance=0.5,
96
86
  section=DovetailPart.TAIL,
97
87
  style=DovetailStyle.TRADITIONAL,
98
- # tilt=20,
99
88
  vertical_offset=0.5,
100
89
  click_fit_radius=0.5,
101
90
  ),
@@ -173,10 +162,8 @@ class TestDovetail:
173
162
  test.part,
174
163
  Point(-5, 0),
175
164
  Point(5, 0),
176
- # scarf_distance=0.5,
177
165
  section=DovetailPart.TAIL,
178
166
  style=DovetailStyle.SNUGTAIL,
179
- # tilt=20,
180
167
  vertical_offset=0.5,
181
168
  click_fit_radius=1,
182
169
  ),
@@ -226,7 +213,6 @@ class TestDovetail:
226
213
  Point(5, 0),
227
214
  taper_angle=-1,
228
215
  section=DovetailPart.TAIL,
229
- # tilt=20,
230
216
  vertical_offset=-0.5,
231
217
  ),
232
218
  )
@@ -238,7 +224,6 @@ class TestDovetail:
238
224
  Point(5, 0),
239
225
  taper_angle=0.5,
240
226
  section=DovetailPart.TAIL,
241
- # tilt=20,
242
227
  vertical_offset=0.5,
243
228
  ),
244
229
  )
@@ -11,7 +11,7 @@ from build123d import BuildPart, Box, Part, Sphere, Align, Mode, Location
11
11
  from b3dkit.hexwall import HexWall
12
12
 
13
13
 
14
- class TestClickfit:
14
+ class TestHexWall:
15
15
  def test_hexwall(self):
16
16
  pattern = HexWall(10, 10, 1, 1, 0.2)
17
17
  assert pattern.is_valid