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.
- {b3dkit-0.1.0 → b3dkit-0.1.1}/.coverage +0 -0
- {b3dkit-0.1.0 → b3dkit-0.1.1}/PKG-INFO +6 -3
- {b3dkit-0.1.0 → b3dkit-0.1.1}/pyproject.toml +6 -2
- {b3dkit-0.1.0 → b3dkit-0.1.1}/src/b3dkit/__init__.py +1 -0
- {b3dkit-0.1.0 → b3dkit-0.1.1}/src/b3dkit/bolt_fittings.py +20 -8
- {b3dkit-0.1.0 → b3dkit-0.1.1}/src/b3dkit/hexwall.py +3 -3
- b3dkit-0.1.1/tests/test_antichamfer.py +147 -0
- b3dkit-0.1.1/tests/test_ball_socket.py +105 -0
- b3dkit-0.1.1/tests/test_bolt_fittings.py +215 -0
- {b3dkit-0.1.0 → b3dkit-0.1.1}/tests/test_dovetail.py +0 -15
- {b3dkit-0.1.0 → b3dkit-0.1.1}/tests/test_hexwall.py +1 -1
- b3dkit-0.1.0/tests/test_antichamfer.py +0 -426
- b3dkit-0.1.0/tests/test_ball_socket.py +0 -249
- b3dkit-0.1.0/tests/test_bolt_fittings.py +0 -538
- {b3dkit-0.1.0 → b3dkit-0.1.1}/.coveragerc +0 -0
- {b3dkit-0.1.0 → b3dkit-0.1.1}/.gitignore +0 -0
- {b3dkit-0.1.0 → b3dkit-0.1.1}/.vscode/settings.json +0 -0
- {b3dkit-0.1.0 → b3dkit-0.1.1}/LICENSE +0 -0
- {b3dkit-0.1.0 → b3dkit-0.1.1}/README.md +0 -0
- {b3dkit-0.1.0 → b3dkit-0.1.1}/build.bat +0 -0
- {b3dkit-0.1.0 → b3dkit-0.1.1}/build.sh +0 -0
- {b3dkit-0.1.0 → b3dkit-0.1.1}/docs/Makefile +0 -0
- {b3dkit-0.1.0 → b3dkit-0.1.1}/docs/antichamfer.md +0 -0
- {b3dkit-0.1.0 → b3dkit-0.1.1}/docs/ball_socket.md +0 -0
- {b3dkit-0.1.0 → b3dkit-0.1.1}/docs/basic_shapes.md +0 -0
- {b3dkit-0.1.0 → b3dkit-0.1.1}/docs/bolt_fittings.md +0 -0
- {b3dkit-0.1.0 → b3dkit-0.1.1}/docs/click_fit.md +0 -0
- {b3dkit-0.1.0 → b3dkit-0.1.1}/docs/conf.py +0 -0
- {b3dkit-0.1.0 → b3dkit-0.1.1}/docs/dovetail.md +0 -0
- {b3dkit-0.1.0 → b3dkit-0.1.1}/docs/dovetail.png +0 -0
- {b3dkit-0.1.0 → b3dkit-0.1.1}/docs/hexwall.md +0 -0
- {b3dkit-0.1.0 → b3dkit-0.1.1}/docs/high_top_slide_box.md +0 -0
- {b3dkit-0.1.0 → b3dkit-0.1.1}/docs/index.md +0 -0
- {b3dkit-0.1.0 → b3dkit-0.1.1}/docs/point.md +0 -0
- {b3dkit-0.1.0 → b3dkit-0.1.1}/docs/slide_box.md +0 -0
- {b3dkit-0.1.0 → b3dkit-0.1.1}/docs/twist_snap.md +0 -0
- {b3dkit-0.1.0 → b3dkit-0.1.1}/docs/twist_snap.png +0 -0
- {b3dkit-0.1.0 → b3dkit-0.1.1}/mkdocs.yml +0 -0
- {b3dkit-0.1.0 → b3dkit-0.1.1}/mkdocs_new.yml +0 -0
- {b3dkit-0.1.0 → b3dkit-0.1.1}/readthedocs.yaml +0 -0
- {b3dkit-0.1.0 → b3dkit-0.1.1}/src/b3dkit/antichamfer.py +0 -0
- {b3dkit-0.1.0 → b3dkit-0.1.1}/src/b3dkit/ball_socket.py +0 -0
- {b3dkit-0.1.0 → b3dkit-0.1.1}/src/b3dkit/basic_shapes.py +0 -0
- {b3dkit-0.1.0 → b3dkit-0.1.1}/src/b3dkit/click_fit.py +0 -0
- {b3dkit-0.1.0 → b3dkit-0.1.1}/src/b3dkit/dovetail.py +0 -0
- {b3dkit-0.1.0 → b3dkit-0.1.1}/src/b3dkit/high_top_slide_box.py +0 -0
- {b3dkit-0.1.0 → b3dkit-0.1.1}/src/b3dkit/point.py +0 -0
- {b3dkit-0.1.0 → b3dkit-0.1.1}/src/b3dkit/slide_box.py +0 -0
- {b3dkit-0.1.0 → b3dkit-0.1.1}/src/b3dkit/twist_snap.py +0 -0
- {b3dkit-0.1.0 → b3dkit-0.1.1}/tests/conftest.py +0 -0
- {b3dkit-0.1.0 → b3dkit-0.1.1}/tests/test_basic_shapes.py +0 -0
- {b3dkit-0.1.0 → b3dkit-0.1.1}/tests/test_click_fit.py +0 -0
- {b3dkit-0.1.0 → b3dkit-0.1.1}/tests/test_high_top_slide_box.py +0 -0
- {b3dkit-0.1.0 → b3dkit-0.1.1}/tests/test_point.py +0 -0
- {b3dkit-0.1.0 → b3dkit-0.1.1}/tests/test_slide_box.py +0 -0
- {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.
|
|
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.
|
|
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",
|
|
@@ -85,12 +85,12 @@ class TeardropBoltCutSinkhole(BasePartObject):
|
|
|
85
85
|
sinkhole.faces().sort_by(Axis.Z)[-1],
|
|
86
86
|
amount=extension_distance,
|
|
87
87
|
)
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
88
|
+
anti_chamfer(
|
|
89
|
+
sinkhole.part.faces().sort_by(Axis.Z)[-1],
|
|
90
|
+
chamfer_radius,
|
|
91
|
+
)
|
|
92
92
|
super().__init__(
|
|
93
|
-
part=
|
|
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
|
-
|
|
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
|
-
|
|
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=
|
|
94
|
-
length=
|
|
93
|
+
width=20,
|
|
94
|
+
length=40,
|
|
95
95
|
height=2,
|
|
96
|
-
apothem=
|
|
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
|
)
|