b3dkit 0.4.0__tar.gz → 0.5.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.
- b3dkit-0.5.0/.coveragerc +20 -0
- {b3dkit-0.4.0 → b3dkit-0.5.0}/CHANGELOG.md +37 -1
- {b3dkit-0.4.0 → b3dkit-0.5.0}/PKG-INFO +1 -1
- {b3dkit-0.4.0 → b3dkit-0.5.0}/pyproject.toml +10 -3
- {b3dkit-0.4.0 → b3dkit-0.5.0}/src/b3dkit/bolt_fittings.py +3 -3
- {b3dkit-0.4.0 → b3dkit-0.5.0}/src/b3dkit/dovetail.py +23 -31
- {b3dkit-0.4.0 → b3dkit-0.5.0}/tests/test_antichamfer.py +4 -19
- {b3dkit-0.4.0 → b3dkit-0.5.0}/tests/test_ball_socket.py +4 -13
- {b3dkit-0.4.0 → b3dkit-0.5.0}/tests/test_basic_shapes.py +0 -15
- {b3dkit-0.4.0 → b3dkit-0.5.0}/tests/test_bolt_fittings.py +47 -20
- b3dkit-0.5.0/tests/test_click_fit.py +19 -0
- {b3dkit-0.4.0 → b3dkit-0.5.0}/tests/test_dovetail.py +137 -25
- {b3dkit-0.4.0 → b3dkit-0.5.0}/tests/test_hexwall.py +10 -19
- {b3dkit-0.4.0 → b3dkit-0.5.0}/tests/test_high_top_slide_box.py +0 -18
- {b3dkit-0.4.0 → b3dkit-0.5.0}/tests/test_slide_box.py +10 -21
- {b3dkit-0.4.0 → b3dkit-0.5.0}/tests/test_twist_snap.py +10 -19
- b3dkit-0.4.0/.coveragerc +0 -6
- b3dkit-0.4.0/tests/conftest.py +0 -12
- b3dkit-0.4.0/tests/test_click_fit.py +0 -32
- {b3dkit-0.4.0 → b3dkit-0.5.0}/.github/workflows/release.yml +0 -0
- {b3dkit-0.4.0 → b3dkit-0.5.0}/.github/workflows/test.yml +0 -0
- {b3dkit-0.4.0 → b3dkit-0.5.0}/.gitignore +0 -0
- {b3dkit-0.4.0 → b3dkit-0.5.0}/.pre-commit-config.yaml +0 -0
- {b3dkit-0.4.0 → b3dkit-0.5.0}/.vscode/settings.json +0 -0
- {b3dkit-0.4.0 → b3dkit-0.5.0}/CONTRIBUTING.md +0 -0
- {b3dkit-0.4.0 → b3dkit-0.5.0}/LICENSE +0 -0
- {b3dkit-0.4.0 → b3dkit-0.5.0}/README.md +0 -0
- {b3dkit-0.4.0 → b3dkit-0.5.0}/docs/CONTRIBUTING.md +0 -0
- {b3dkit-0.4.0 → b3dkit-0.5.0}/docs/antichamfer.md +0 -0
- {b3dkit-0.4.0 → b3dkit-0.5.0}/docs/ball_socket.md +0 -0
- {b3dkit-0.4.0 → b3dkit-0.5.0}/docs/basic_shapes.md +0 -0
- {b3dkit-0.4.0 → b3dkit-0.5.0}/docs/bolt_fittings.md +0 -0
- {b3dkit-0.4.0 → b3dkit-0.5.0}/docs/click_fit.md +0 -0
- {b3dkit-0.4.0 → b3dkit-0.5.0}/docs/dovetail.md +0 -0
- {b3dkit-0.4.0 → b3dkit-0.5.0}/docs/dovetail.png +0 -0
- {b3dkit-0.4.0 → b3dkit-0.5.0}/docs/hexwall.md +0 -0
- {b3dkit-0.4.0 → b3dkit-0.5.0}/docs/high_top_slide_box.md +0 -0
- {b3dkit-0.4.0 → b3dkit-0.5.0}/docs/index.md +0 -0
- {b3dkit-0.4.0 → b3dkit-0.5.0}/docs/point.md +0 -0
- {b3dkit-0.4.0 → b3dkit-0.5.0}/docs/reference/antichamfer.md +0 -0
- {b3dkit-0.4.0 → b3dkit-0.5.0}/docs/reference/ball_socket.md +0 -0
- {b3dkit-0.4.0 → b3dkit-0.5.0}/docs/reference/basic_shapes.md +0 -0
- {b3dkit-0.4.0 → b3dkit-0.5.0}/docs/reference/bolt_fittings.md +0 -0
- {b3dkit-0.4.0 → b3dkit-0.5.0}/docs/reference/click_fit.md +0 -0
- {b3dkit-0.4.0 → b3dkit-0.5.0}/docs/reference/dovetail.md +0 -0
- {b3dkit-0.4.0 → b3dkit-0.5.0}/docs/reference/hexwall.md +0 -0
- {b3dkit-0.4.0 → b3dkit-0.5.0}/docs/reference/high_top_slide_box.md +0 -0
- {b3dkit-0.4.0 → b3dkit-0.5.0}/docs/reference/index.md +0 -0
- {b3dkit-0.4.0 → b3dkit-0.5.0}/docs/reference/point.md +0 -0
- {b3dkit-0.4.0 → b3dkit-0.5.0}/docs/reference/slide_box.md +0 -0
- {b3dkit-0.4.0 → b3dkit-0.5.0}/docs/reference/twist_snap.md +0 -0
- {b3dkit-0.4.0 → b3dkit-0.5.0}/docs/requirements.txt +0 -0
- {b3dkit-0.4.0 → b3dkit-0.5.0}/docs/slide_box.md +0 -0
- {b3dkit-0.4.0 → b3dkit-0.5.0}/docs/twist_snap.md +0 -0
- {b3dkit-0.4.0 → b3dkit-0.5.0}/docs/twist_snap.png +0 -0
- {b3dkit-0.4.0 → b3dkit-0.5.0}/mkdocs.yml +0 -0
- {b3dkit-0.4.0 → b3dkit-0.5.0}/readthedocs.yaml +0 -0
- {b3dkit-0.4.0 → b3dkit-0.5.0}/src/b3dkit/__init__.py +0 -0
- {b3dkit-0.4.0 → b3dkit-0.5.0}/src/b3dkit/antichamfer.py +0 -0
- {b3dkit-0.4.0 → b3dkit-0.5.0}/src/b3dkit/ball_socket.py +0 -0
- {b3dkit-0.4.0 → b3dkit-0.5.0}/src/b3dkit/basic_shapes.py +0 -0
- {b3dkit-0.4.0 → b3dkit-0.5.0}/src/b3dkit/click_fit.py +0 -0
- {b3dkit-0.4.0 → b3dkit-0.5.0}/src/b3dkit/hexwall.py +0 -0
- {b3dkit-0.4.0 → b3dkit-0.5.0}/src/b3dkit/high_top_slide_box.py +0 -0
- {b3dkit-0.4.0 → b3dkit-0.5.0}/src/b3dkit/point.py +0 -0
- {b3dkit-0.4.0 → b3dkit-0.5.0}/src/b3dkit/py.typed +0 -0
- {b3dkit-0.4.0 → b3dkit-0.5.0}/src/b3dkit/slide_box.py +0 -0
- {b3dkit-0.4.0 → b3dkit-0.5.0}/src/b3dkit/twist_snap.py +0 -0
- {b3dkit-0.4.0 → b3dkit-0.5.0}/tests/test_docs.py +0 -0
- {b3dkit-0.4.0 → b3dkit-0.5.0}/tests/test_point.py +0 -0
- {b3dkit-0.4.0 → b3dkit-0.5.0}/tests/test_public_api.py +0 -0
b3dkit-0.5.0/.coveragerc
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
[run]
|
|
2
|
+
source = src/b3dkit
|
|
3
|
+
# `*test*` also matched any source path containing "test", so it was scoped to
|
|
4
|
+
# the test directory instead.
|
|
5
|
+
omit =
|
|
6
|
+
tests/*
|
|
7
|
+
|
|
8
|
+
[report]
|
|
9
|
+
show_missing = True
|
|
10
|
+
# CONTRIBUTING.md declares a 90% floor with no ratchet. Enforced here so the
|
|
11
|
+
# number is a gate rather than a suggestion.
|
|
12
|
+
fail_under = 90
|
|
13
|
+
exclude_lines =
|
|
14
|
+
pragma: no cover
|
|
15
|
+
# The __main__ blocks are demos, not library code. They were previously
|
|
16
|
+
# executed by ten tests that asserted nothing, purely so these lines would
|
|
17
|
+
# count toward coverage; deleting those tests is what this excludes for.
|
|
18
|
+
if __name__ == .__main__.:
|
|
19
|
+
# Raised only when the package is not installed, e.g. run from a source tree.
|
|
20
|
+
except PackageNotFoundError:
|
|
@@ -7,6 +7,41 @@ ship in minor releases.
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.5.0] - 2026-09-09
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- **`NutCut` now sits on the origin rather than straddling it.** The recess ran
|
|
15
|
+
from `-head_depth` to `0` while the shaft began at `+head_depth`, so the two
|
|
16
|
+
never met: the cutter came back as two solids with a `head_depth` gap, and
|
|
17
|
+
subtracting it left a membrane the bolt could not pass through. It is now one
|
|
18
|
+
solid spanning `0` to `head_depth + shaft_length`.
|
|
19
|
+
|
|
20
|
+
**This moves the cutter.** It previously had to be positioned *below* a
|
|
21
|
+
surface to work; it can now be placed *on* one. Anything calling `NutCut` and
|
|
22
|
+
compensating for the old placement needs that compensation removed.
|
|
23
|
+
|
|
24
|
+
- **`dovetail_subpart` no longer adds each click-fit divot twice.** One copy
|
|
25
|
+
landed outside the part as a free-floating sliver. Only geometry using
|
|
26
|
+
`click_fit_radius` is affected; the tail's fused solid is unchanged and the
|
|
27
|
+
socket's moves by 0.02%.
|
|
28
|
+
|
|
29
|
+
- **A positive `vertical_offset` no longer cuts the dovetail socket in two.**
|
|
30
|
+
Tails, `vertical_offset=0` and negative offsets are all unchanged.
|
|
31
|
+
|
|
32
|
+
### Known issues
|
|
33
|
+
|
|
34
|
+
Found while adding solid-count assertions, all pre-existing and all invisible to
|
|
35
|
+
the `is_valid` checks that used to stand in for them:
|
|
36
|
+
|
|
37
|
+
- `TwistSnapConnector` returns five solids: a base and four snapfits that meet
|
|
38
|
+
only on a shared plane and never fuse.
|
|
39
|
+
- `HexWall` can return partial hexagons clipped at the bounds as separate
|
|
40
|
+
slivers, depending on how the grid divides the given width.
|
|
41
|
+
- `SNUGTAIL` splits its own tail into disconnected fins on parts wider than
|
|
42
|
+
about 30mm. Pinned by `TestSnugtailWidthLimit`.
|
|
43
|
+
|
|
44
|
+
|
|
10
45
|
## [0.4.0] - 2026-09-08
|
|
11
46
|
|
|
12
47
|
### Removed
|
|
@@ -111,6 +146,7 @@ the Python 3.11 floor.
|
|
|
111
146
|
See the git history. b3dkit began as `fb-library`; the rename accompanied a
|
|
112
147
|
rewrite that reworked names and usage to feel closer to native build123d.
|
|
113
148
|
|
|
114
|
-
[Unreleased]: https://github.com/x0pherl/b3dkit/compare/v0.
|
|
149
|
+
[Unreleased]: https://github.com/x0pherl/b3dkit/compare/v0.5.0...HEAD
|
|
150
|
+
[0.5.0]: https://github.com/x0pherl/b3dkit/releases/tag/v0.5.0
|
|
115
151
|
[0.4.0]: https://github.com/x0pherl/b3dkit/releases/tag/v0.4.0
|
|
116
152
|
[0.3.2]: https://github.com/x0pherl/b3dkit/releases/tag/v0.3.2
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "b3dkit"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.5.0"
|
|
4
4
|
authors = [
|
|
5
5
|
{ name="x0pherl"},
|
|
6
6
|
]
|
|
@@ -63,8 +63,15 @@ build-backend = "hatchling.build"
|
|
|
63
63
|
[tool.pytest.ini_options]
|
|
64
64
|
pythonpath = ["src"]
|
|
65
65
|
addopts = "--strict-markers"
|
|
66
|
-
|
|
67
|
-
|
|
66
|
+
filterwarnings = [
|
|
67
|
+
# b3dkit's own warnings are failures; we control those.
|
|
68
|
+
"error",
|
|
69
|
+
# Everything below is raised inside the dependency stack, not by b3dkit.
|
|
70
|
+
# ezdxf, reached through build123d, calls pyparsing's pre-snake_case API.
|
|
71
|
+
"ignore::DeprecationWarning:ezdxf.*",
|
|
72
|
+
"ignore::DeprecationWarning:pyparsing.*",
|
|
73
|
+
"default::DeprecationWarning:build123d.*",
|
|
74
|
+
"default::PendingDeprecationWarning:build123d.*",
|
|
68
75
|
]
|
|
69
76
|
|
|
70
77
|
[tool.black]
|
|
@@ -251,13 +251,13 @@ class NutCut(BasePartObject):
|
|
|
251
251
|
context: BuildPart = BuildPart._get_context()
|
|
252
252
|
validate_inputs(context, self)
|
|
253
253
|
|
|
254
|
-
with BuildPart(
|
|
254
|
+
with BuildPart() as cut:
|
|
255
255
|
with BuildSketch():
|
|
256
256
|
RegularPolygon(radius=head_radius, side_count=6)
|
|
257
|
-
extrude(amount
|
|
257
|
+
extrude(amount=head_depth)
|
|
258
258
|
Cylinder(
|
|
259
259
|
radius=shaft_radius,
|
|
260
|
-
height=shaft_length,
|
|
260
|
+
height=shaft_length + head_depth,
|
|
261
261
|
align=(Align.CENTER, Align.CENTER, Align.MIN),
|
|
262
262
|
)
|
|
263
263
|
|
|
@@ -528,18 +528,10 @@ def _traditional_subpart_divots(
|
|
|
528
528
|
),
|
|
529
529
|
mode=topmode,
|
|
530
530
|
):
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
extend_base=True,
|
|
536
|
-
)
|
|
537
|
-
.rotate(
|
|
538
|
-
Axis.X,
|
|
539
|
-
(90 * (-1 if vertical_offset < 0 else 1))
|
|
540
|
-
+ adjusted_top_divot_angle,
|
|
541
|
-
)
|
|
542
|
-
.rotate(Axis.Y, cut_angle),
|
|
531
|
+
Divot(
|
|
532
|
+
click_fit_radius,
|
|
533
|
+
positive=topmode == Mode.ADD,
|
|
534
|
+
extend_base=True,
|
|
543
535
|
)
|
|
544
536
|
#####################################
|
|
545
537
|
# Bottom divots
|
|
@@ -562,29 +554,20 @@ def _traditional_subpart_divots(
|
|
|
562
554
|
),
|
|
563
555
|
mode=bottommode,
|
|
564
556
|
):
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
extend_base=True,
|
|
570
|
-
)
|
|
571
|
-
.rotate(
|
|
572
|
-
Axis.X,
|
|
573
|
-
(90 * (1 if vertical_offset < 0 else -1)) + scarf_angle,
|
|
574
|
-
)
|
|
575
|
-
.rotate(Axis.Y, cut_angle),
|
|
557
|
+
Divot(
|
|
558
|
+
click_fit_radius,
|
|
559
|
+
positive=bottommode == Mode.ADD,
|
|
560
|
+
extend_base=True,
|
|
576
561
|
)
|
|
577
562
|
with BuildPart(
|
|
578
563
|
Location((end_side.x, end_side.y, click_fit_radius * 2)),
|
|
579
564
|
mode=bottommode,
|
|
580
565
|
):
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
)
|
|
587
|
-
.rotate(Axis.Y, cut_angle),
|
|
566
|
+
Divot(
|
|
567
|
+
click_fit_radius,
|
|
568
|
+
# was hardcoded True while the sibling divot above derives it
|
|
569
|
+
positive=bottommode == Mode.ADD,
|
|
570
|
+
extend_base=True,
|
|
588
571
|
)
|
|
589
572
|
|
|
590
573
|
return divotedpart.part
|
|
@@ -954,7 +937,16 @@ def dovetail_subpart(
|
|
|
954
937
|
current_floor = (
|
|
955
938
|
part.bounding_box().min.Z
|
|
956
939
|
if vertical_offset <= 0
|
|
957
|
-
|
|
940
|
+
# Must match the straightened slab's top_z above, which uses
|
|
941
|
+
# vertical_tolerance_adjustment. Using the raw vertical_tolerance
|
|
942
|
+
# here agreed for the tail, where the adjustment is +tolerance, and
|
|
943
|
+
# disagreed for the socket, where it is -tolerance: the two slabs
|
|
944
|
+
# then missed each other by 2 * vertical_tolerance, cutting a slot
|
|
945
|
+
# clean through the socket and leaving it in two pieces. The
|
|
946
|
+
# vertical_offset < 0 branch below already uses the adjustment.
|
|
947
|
+
else part.bounding_box().min.Z
|
|
948
|
+
+ abs(vertical_offset)
|
|
949
|
+
+ vertical_tolerance_adjustment
|
|
958
950
|
)
|
|
959
951
|
add(
|
|
960
952
|
_subpart_slab(
|
|
@@ -1,7 +1,3 @@
|
|
|
1
|
-
from importlib.machinery import SourceFileLoader
|
|
2
|
-
from importlib.util import module_from_spec, spec_from_loader
|
|
3
|
-
from unittest.mock import patch
|
|
4
|
-
|
|
5
1
|
import pytest
|
|
6
2
|
from build123d import (
|
|
7
3
|
Align,
|
|
@@ -16,7 +12,6 @@ from build123d import (
|
|
|
16
12
|
)
|
|
17
13
|
|
|
18
14
|
from b3dkit.antichamfer import anti_chamfer
|
|
19
|
-
from conftest import module_path
|
|
20
15
|
|
|
21
16
|
|
|
22
17
|
class TestAntiChamfer:
|
|
@@ -122,7 +117,10 @@ class TestAntiChamfer:
|
|
|
122
117
|
Box(10, 10, 10)
|
|
123
118
|
|
|
124
119
|
ac_part = anti_chamfer(bp.part.faces().filter_by(Axis.Z)[-1], -1.0, -0.5)
|
|
125
|
-
assert ac_part.
|
|
120
|
+
assert len(ac_part.solids()) == 1
|
|
121
|
+
# negative lengths still extend the face outward rather than cutting in
|
|
122
|
+
assert ac_part.volume == pytest.approx(1110.3333, rel=1e-4)
|
|
123
|
+
assert ac_part.volume > bp.part.volume
|
|
126
124
|
|
|
127
125
|
def test_anti_chamfer_zero_length_values(self):
|
|
128
126
|
"""Test anti_chamfer with negative length values"""
|
|
@@ -136,19 +134,6 @@ class TestAntiChamfer:
|
|
|
136
134
|
assert ac2.is_valid
|
|
137
135
|
assert bp.part.volume == ac1.volume == ac2.volume
|
|
138
136
|
|
|
139
|
-
def test_direct_run(self):
|
|
140
|
-
|
|
141
|
-
with (
|
|
142
|
-
patch("build123d.export_stl"),
|
|
143
|
-
patch("pathlib.Path.mkdir"),
|
|
144
|
-
patch("pathlib.Path.exists"),
|
|
145
|
-
patch("pathlib.Path.is_dir"),
|
|
146
|
-
patch("ocp_vscode.show"),
|
|
147
|
-
patch("ocp_vscode.save_screenshot"),
|
|
148
|
-
):
|
|
149
|
-
loader = SourceFileLoader("__main__", module_path("antichamfer"))
|
|
150
|
-
loader.exec_module(module_from_spec(spec_from_loader(loader.name, loader)))
|
|
151
|
-
|
|
152
137
|
|
|
153
138
|
class TestAntiChamferBuilderContract:
|
|
154
139
|
"""anti_chamfer applies to BuildPart only, and must say so.
|
|
@@ -1,13 +1,9 @@
|
|
|
1
1
|
import math
|
|
2
|
-
from importlib.machinery import SourceFileLoader
|
|
3
|
-
from importlib.util import module_from_spec, spec_from_loader
|
|
4
|
-
from unittest.mock import patch
|
|
5
2
|
|
|
6
3
|
import pytest
|
|
7
4
|
from build123d import Part
|
|
8
5
|
|
|
9
6
|
from b3dkit.ball_socket import BallMount, BallSocket
|
|
10
|
-
from conftest import module_path
|
|
11
7
|
|
|
12
8
|
|
|
13
9
|
# ---------- Helpers ----------
|
|
@@ -45,8 +41,11 @@ class TestBallSocket:
|
|
|
45
41
|
@pytest.mark.parametrize("r,w", [(3, 2), (5, 1), (10, 2), (20, 4), (12.5, 3.5)])
|
|
46
42
|
def test_ball_socket_param_dimensions(self, r, w):
|
|
47
43
|
socket = BallSocket(r, wall_thickness=w)
|
|
48
|
-
assert socket.
|
|
44
|
+
assert len(socket.solids()) == 1
|
|
49
45
|
bbox = socket.bounding_box()
|
|
46
|
+
# the socket encloses the ball plus a wall on each side
|
|
47
|
+
assert bbox.size.X == pytest.approx(2 * r + 2 * w)
|
|
48
|
+
assert socket.volume > 0
|
|
50
49
|
|
|
51
50
|
def test_ball_socket_custom_wall_thickness(self):
|
|
52
51
|
r, w = 10.0, 3.0
|
|
@@ -97,11 +96,3 @@ class TestEdgeCases:
|
|
|
97
96
|
assert tight.is_valid
|
|
98
97
|
assert loose.is_valid
|
|
99
98
|
assert loose.volume < BallSocket(10.0, tolerance=0.0).volume
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
# ---------- Direct Run ----------
|
|
103
|
-
class TestDirectRun:
|
|
104
|
-
def test_direct_run(self):
|
|
105
|
-
with patch("ocp_vscode.show"):
|
|
106
|
-
loader = SourceFileLoader("__main__", module_path("ball_socket"))
|
|
107
|
-
loader.exec_module(module_from_spec(spec_from_loader(loader.name, loader)))
|
|
@@ -1,7 +1,3 @@
|
|
|
1
|
-
from importlib.machinery import SourceFileLoader
|
|
2
|
-
from importlib.util import module_from_spec, spec_from_loader
|
|
3
|
-
from unittest.mock import patch
|
|
4
|
-
|
|
5
1
|
import pytest
|
|
6
2
|
from build123d import Align, Part
|
|
7
3
|
|
|
@@ -19,7 +15,6 @@ from b3dkit.basic_shapes import (
|
|
|
19
15
|
opposite_length,
|
|
20
16
|
radius_to_apothem,
|
|
21
17
|
)
|
|
22
|
-
from conftest import module_path
|
|
23
18
|
|
|
24
19
|
|
|
25
20
|
class TestApothemConversions:
|
|
@@ -265,13 +260,3 @@ class TestDiamondCylinder:
|
|
|
265
260
|
assert db.Y == pytest.approx(pb.Y)
|
|
266
261
|
assert db.Z == pytest.approx(pb.Z)
|
|
267
262
|
assert diamond.volume == pytest.approx(poly4.volume)
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
class TestBareExecution:
|
|
271
|
-
def test_bare_execution(self):
|
|
272
|
-
with (
|
|
273
|
-
patch("pathlib.Path.mkdir"),
|
|
274
|
-
patch("ocp_vscode.show"),
|
|
275
|
-
):
|
|
276
|
-
loader = SourceFileLoader("__main__", module_path("basic_shapes"))
|
|
277
|
-
loader.exec_module(module_from_spec(spec_from_loader(loader.name, loader)))
|
|
@@ -9,10 +9,6 @@ These tests cover all functions and their parameters to ensure:
|
|
|
9
9
|
- Mutation testing coverage
|
|
10
10
|
"""
|
|
11
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
|
-
|
|
16
12
|
import pytest
|
|
17
13
|
from build123d import (
|
|
18
14
|
Part,
|
|
@@ -26,7 +22,6 @@ from b3dkit.bolt_fittings import (
|
|
|
26
22
|
SquareNutSinkhole,
|
|
27
23
|
TeardropBoltCutSinkhole,
|
|
28
24
|
)
|
|
29
|
-
from conftest import module_path
|
|
30
25
|
|
|
31
26
|
|
|
32
27
|
class TestTeardropBoltCutSinkhole:
|
|
@@ -34,19 +29,27 @@ class TestTeardropBoltCutSinkhole:
|
|
|
34
29
|
"""Test teardrop bolt cut with default parameters"""
|
|
35
30
|
sinkhole = TeardropBoltCutSinkhole()
|
|
36
31
|
|
|
37
|
-
assert sinkhole.
|
|
32
|
+
assert len(sinkhole.solids()) == 1
|
|
33
|
+
assert sinkhole.volume == pytest.approx(3226.9861, rel=1e-4)
|
|
34
|
+
assert sinkhole.bounding_box().size.Z == pytest.approx(107.0)
|
|
38
35
|
|
|
39
36
|
def test_teardrop_bolt_cut_custom_shaft(self):
|
|
40
37
|
"""Test teardrop bolt cut with custom shaft dimensions"""
|
|
41
38
|
sinkhole = TeardropBoltCutSinkhole(shaft_radius=2.0, shaft_depth=5.0)
|
|
42
39
|
|
|
43
|
-
assert sinkhole.
|
|
40
|
+
assert len(sinkhole.solids()) == 1
|
|
41
|
+
assert sinkhole.volume == pytest.approx(3273.1276, rel=1e-4)
|
|
42
|
+
# a deeper shaft makes the cutter taller
|
|
43
|
+
assert sinkhole.bounding_box().size.Z == pytest.approx(110.0)
|
|
44
44
|
|
|
45
45
|
def test_teardrop_bolt_cut_custom_head(self):
|
|
46
46
|
"""Test teardrop bolt cut with custom head dimensions"""
|
|
47
47
|
sinkhole = TeardropBoltCutSinkhole(head_radius=4.0, head_depth=3.0)
|
|
48
48
|
|
|
49
|
-
assert sinkhole.
|
|
49
|
+
assert len(sinkhole.solids()) == 1
|
|
50
|
+
assert sinkhole.volume == pytest.approx(5255.4069, rel=1e-4)
|
|
51
|
+
# a wider head widens the cutter beyond the default 8.2mm
|
|
52
|
+
assert sinkhole.bounding_box().size.X == pytest.approx(10.0)
|
|
50
53
|
|
|
51
54
|
def test_teardrop_bolt_cut_with_chamfer(self):
|
|
52
55
|
"""Test teardrop bolt cut with various chamfer radii"""
|
|
@@ -89,7 +92,11 @@ class TestBoltCutSinkhole:
|
|
|
89
92
|
"""Test bolt cut with default parameters"""
|
|
90
93
|
boltcut = BoltCutSinkhole()
|
|
91
94
|
|
|
92
|
-
assert boltcut.
|
|
95
|
+
assert len(boltcut.solids()) == 1
|
|
96
|
+
assert boltcut.volume == pytest.approx(3197.9162, rel=1e-4)
|
|
97
|
+
# cylindrical, so square in plan unlike the teardrop
|
|
98
|
+
bbox = boltcut.bounding_box()
|
|
99
|
+
assert bbox.size.X == pytest.approx(bbox.size.Y)
|
|
93
100
|
|
|
94
101
|
def test_bolt_cut_with_chamfer(self):
|
|
95
102
|
"""Test bolt cut with various chamfer radii"""
|
|
@@ -112,7 +119,10 @@ class TestBoltCutSinkhole:
|
|
|
112
119
|
"""Test bolt cut with zero extension (blind hole)"""
|
|
113
120
|
boltcut = BoltCutSinkhole(extension_distance=0)
|
|
114
121
|
|
|
115
|
-
assert boltcut.
|
|
122
|
+
assert len(boltcut.solids()) == 1
|
|
123
|
+
assert boltcut.volume == pytest.approx(178.8456, rel=1e-4)
|
|
124
|
+
# a blind hole is only as deep as the head and shaft
|
|
125
|
+
assert boltcut.bounding_box().size.Z == pytest.approx(7.0)
|
|
116
126
|
|
|
117
127
|
def test_bolt_cut_vs_teardrop(self):
|
|
118
128
|
"""Test that bolt_cut and TeardropBoltCutSinkhole with default ratio produce different results"""
|
|
@@ -158,7 +168,10 @@ class TestSquareNutSinkhole:
|
|
|
158
168
|
"""Test square nut sinkhole with default parameters"""
|
|
159
169
|
sinkhole = SquareNutSinkhole()
|
|
160
170
|
|
|
161
|
-
assert sinkhole.
|
|
171
|
+
assert len(sinkhole.solids()) == 1
|
|
172
|
+
assert sinkhole.volume == pytest.approx(1267.7482, rel=1e-4)
|
|
173
|
+
# the nut trap sets the width: 5.6mm across flats for an M3 nut
|
|
174
|
+
assert sinkhole.bounding_box().size.X == pytest.approx(5.6)
|
|
162
175
|
|
|
163
176
|
def test_square_nut_with_extension(self):
|
|
164
177
|
"""Test square nut sinkhole with bolt extension"""
|
|
@@ -171,7 +184,9 @@ class TestSquareNutSinkhole:
|
|
|
171
184
|
"""Test square nut sinkhole with zero extension"""
|
|
172
185
|
sinkhole = SquareNutSinkhole(bolt_extension=0)
|
|
173
186
|
|
|
174
|
-
assert sinkhole.
|
|
187
|
+
assert len(sinkhole.solids()) == 1
|
|
188
|
+
assert sinkhole.volume == pytest.approx(1259.1175, rel=1e-4)
|
|
189
|
+
assert sinkhole.volume < SquareNutSinkhole().volume
|
|
175
190
|
|
|
176
191
|
def test_square_nut_different_nut_sizes(self):
|
|
177
192
|
"""Test with different nut sizes"""
|
|
@@ -193,8 +208,27 @@ class TestScrewCut:
|
|
|
193
208
|
assert screw.bounding_box().size.Z == pytest.approx(20)
|
|
194
209
|
|
|
195
210
|
def test_nut_cut(self):
|
|
211
|
+
"""The cutter sits on the origin, in one piece.
|
|
212
|
+
|
|
213
|
+
It used to straddle it: the recess ran from -head_depth to 0 while the
|
|
214
|
+
shaft began at +head_depth, so the two never met and the cutter had to
|
|
215
|
+
be positioned below a surface rather than on it.
|
|
216
|
+
"""
|
|
196
217
|
nut = NutCut(5, 1, 2, 10)
|
|
197
|
-
assert nut.
|
|
218
|
+
assert len(nut.solids()) == 1
|
|
219
|
+
bbox = nut.bounding_box()
|
|
220
|
+
assert bbox.min.Z == pytest.approx(0.0)
|
|
221
|
+
assert bbox.max.Z == pytest.approx(11.0)
|
|
222
|
+
|
|
223
|
+
@pytest.mark.parametrize("head_depth", [0.5, 1, 5])
|
|
224
|
+
@pytest.mark.parametrize("shaft_length", [6, 10, 20])
|
|
225
|
+
def test_nut_cut_dimensions(self, head_depth, shaft_length):
|
|
226
|
+
"""The shaft extends shaft_length beyond the recess, from the origin."""
|
|
227
|
+
nut = NutCut(5, head_depth, 2, shaft_length)
|
|
228
|
+
assert len(nut.solids()) == 1
|
|
229
|
+
bbox = nut.bounding_box()
|
|
230
|
+
assert bbox.min.Z == pytest.approx(0.0)
|
|
231
|
+
assert bbox.max.Z == pytest.approx(head_depth + shaft_length)
|
|
198
232
|
|
|
199
233
|
def test_invalid_screw_cut(self):
|
|
200
234
|
with pytest.raises(ValueError):
|
|
@@ -206,10 +240,3 @@ class TestScrewCut:
|
|
|
206
240
|
assert heatsink.bounding_box().size.X == pytest.approx(20)
|
|
207
241
|
assert heatsink.bounding_box().size.Y == pytest.approx(20)
|
|
208
242
|
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__", module_path("bolt_fittings"))
|
|
215
|
-
loader.exec_module(module_from_spec(spec_from_loader(loader.name, loader)))
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
from b3dkit.click_fit import Divot
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class TestClickfit:
|
|
7
|
+
def test_divot(self):
|
|
8
|
+
hole = Divot(10, False)
|
|
9
|
+
assert hole.is_valid
|
|
10
|
+
bump = Divot(10, True)
|
|
11
|
+
assert bump.is_valid
|
|
12
|
+
assert hole.volume > bump.volume
|
|
13
|
+
|
|
14
|
+
def test_divot_extend_base(self):
|
|
15
|
+
bump = Divot(10, True, extend_base=True)
|
|
16
|
+
assert len(bump.solids()) == 1
|
|
17
|
+
assert bump.volume == pytest.approx(3676.0264, rel=1e-4)
|
|
18
|
+
# extending the base makes it taller than the plain divot
|
|
19
|
+
assert bump.bounding_box().size.Z > Divot(10, True).bounding_box().size.Z
|
|
@@ -1,7 +1,3 @@
|
|
|
1
|
-
from importlib.machinery import SourceFileLoader
|
|
2
|
-
from importlib.util import module_from_spec, spec_from_loader
|
|
3
|
-
from unittest.mock import patch
|
|
4
|
-
|
|
5
1
|
import pytest
|
|
6
2
|
from build123d import Align, Box, BuildPart, Mode, Part, add
|
|
7
3
|
|
|
@@ -14,23 +10,10 @@ from b3dkit.dovetail import (
|
|
|
14
10
|
dovetail_subpart,
|
|
15
11
|
)
|
|
16
12
|
from b3dkit.point import Point
|
|
17
|
-
from conftest import module_path
|
|
18
13
|
|
|
19
14
|
|
|
20
15
|
class TestDovetail:
|
|
21
16
|
|
|
22
|
-
def test_direct_run(self):
|
|
23
|
-
with (
|
|
24
|
-
patch("build123d.export_stl"),
|
|
25
|
-
patch("pathlib.Path.mkdir"),
|
|
26
|
-
patch("pathlib.Path.exists"),
|
|
27
|
-
patch("pathlib.Path.is_dir"),
|
|
28
|
-
patch("ocp_vscode.show"),
|
|
29
|
-
patch("ocp_vscode.save_screenshot"),
|
|
30
|
-
):
|
|
31
|
-
loader = SourceFileLoader("__main__", module_path("dovetail"))
|
|
32
|
-
loader.exec_module(module_from_spec(spec_from_loader(loader.name, loader)))
|
|
33
|
-
|
|
34
17
|
def test_start_end_match(self):
|
|
35
18
|
with BuildPart(mode=Mode.PRIVATE) as test:
|
|
36
19
|
Box(10, 50, 2, align=(Align.CENTER, Align.CENTER, Align.MIN))
|
|
@@ -86,7 +69,10 @@ class TestDovetail:
|
|
|
86
69
|
click_fit_radius=0.5,
|
|
87
70
|
),
|
|
88
71
|
)
|
|
89
|
-
assert tail.part.
|
|
72
|
+
assert len(tail.part.solids()) == 1
|
|
73
|
+
assert tail.part.volume == pytest.approx(505.6255, rel=1e-4)
|
|
74
|
+
# the tail keeps the tongue, so it is longer in Y than the socket
|
|
75
|
+
assert tail.part.bounding_box().size.Y == pytest.approx(26.8917, rel=1e-4)
|
|
90
76
|
|
|
91
77
|
def test_valid_socket(self):
|
|
92
78
|
with BuildPart(mode=Mode.PRIVATE) as test:
|
|
@@ -103,7 +89,8 @@ class TestDovetail:
|
|
|
103
89
|
vertical_offset=-0.5,
|
|
104
90
|
),
|
|
105
91
|
)
|
|
106
|
-
assert socket.part.
|
|
92
|
+
assert len(socket.part.solids()) == 1
|
|
93
|
+
assert socket.part.volume == pytest.approx(650.0268, rel=1e-4)
|
|
107
94
|
|
|
108
95
|
def test_raises_invalid_style_for_snugtail(self):
|
|
109
96
|
with pytest.raises(ValueError):
|
|
@@ -130,7 +117,8 @@ class TestDovetail:
|
|
|
130
117
|
vertical_offset=-0.5,
|
|
131
118
|
),
|
|
132
119
|
)
|
|
133
|
-
assert socket.part.
|
|
120
|
+
assert len(socket.part.solids()) == 1
|
|
121
|
+
assert socket.part.volume == pytest.approx(509.5927, rel=1e-4)
|
|
134
122
|
|
|
135
123
|
def test_valid_tslot_tail(self):
|
|
136
124
|
with BuildPart(mode=Mode.PRIVATE) as test:
|
|
@@ -148,7 +136,8 @@ class TestDovetail:
|
|
|
148
136
|
vertical_offset=-0.5,
|
|
149
137
|
),
|
|
150
138
|
)
|
|
151
|
-
assert tail.part.
|
|
139
|
+
assert len(tail.part.solids()) == 1
|
|
140
|
+
assert tail.part.volume == pytest.approx(492.0199, rel=1e-4)
|
|
152
141
|
|
|
153
142
|
def test_valid_snugtail_tail(self):
|
|
154
143
|
with BuildPart(mode=Mode.PRIVATE) as test:
|
|
@@ -165,7 +154,10 @@ class TestDovetail:
|
|
|
165
154
|
click_fit_radius=1,
|
|
166
155
|
),
|
|
167
156
|
)
|
|
168
|
-
assert tail.part.
|
|
157
|
+
assert len(tail.part.solids()) == 1
|
|
158
|
+
assert tail.part.volume == pytest.approx(346.5381, rel=1e-4)
|
|
159
|
+
# the click-fit divot stands proud of the 2mm part
|
|
160
|
+
assert tail.part.bounding_box().max.Z == pytest.approx(2.5)
|
|
169
161
|
|
|
170
162
|
def test_valid_snugtail_socket(self):
|
|
171
163
|
with BuildPart(mode=Mode.PRIVATE) as test:
|
|
@@ -184,7 +176,10 @@ class TestDovetail:
|
|
|
184
176
|
click_fit_radius=1,
|
|
185
177
|
),
|
|
186
178
|
)
|
|
187
|
-
assert socket.part.
|
|
179
|
+
assert len(socket.part.solids()) == 1
|
|
180
|
+
assert socket.part.volume == pytest.approx(648.4621, rel=1e-4)
|
|
181
|
+
# the snugtail socket wraps three sides, so it is much the larger half
|
|
182
|
+
assert socket.part.volume > 600
|
|
188
183
|
|
|
189
184
|
def test_snugtail_ratios_exceed_max(self):
|
|
190
185
|
with BuildPart(mode=Mode.PRIVATE) as test:
|
|
@@ -377,11 +372,15 @@ class TestStyleConditionalArguments:
|
|
|
377
372
|
],
|
|
378
373
|
)
|
|
379
374
|
def test_applicable_argument_accepted(self, style, param, value):
|
|
380
|
-
|
|
375
|
+
part = _subpart(style=style, **{param: value})
|
|
376
|
+
assert part.volume > 0
|
|
377
|
+
assert part.bounding_box().size.Z > 0
|
|
381
378
|
|
|
382
379
|
@pytest.mark.parametrize("style", list(DovetailStyle))
|
|
383
380
|
def test_defaults_never_raise(self, style):
|
|
384
|
-
|
|
381
|
+
part = _subpart(style=style)
|
|
382
|
+
assert part.volume > 0
|
|
383
|
+
assert part.bounding_box().size.Z > 0
|
|
385
384
|
|
|
386
385
|
|
|
387
386
|
class TestDovetailSplit:
|
|
@@ -449,3 +448,116 @@ class TestDefaultGeometryUnchanged:
|
|
|
449
448
|
):
|
|
450
449
|
part = _subpart(style=style, subpart=section)
|
|
451
450
|
assert part.volume == pytest.approx(expected_volume, abs=1e-4)
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
def _narrow_box() -> Part:
|
|
454
|
+
"""A part narrow enough that snugtail's fins meet.
|
|
455
|
+
|
|
456
|
+
Snugtail splits its own tail into disconnected fins once the part is about
|
|
457
|
+
30mm wide -- see TestSnugtailWidthLimit -- so the single-solid tests use a
|
|
458
|
+
width below that, to guard the defects they are about is_valid rather than
|
|
459
|
+
that separate one.
|
|
460
|
+
"""
|
|
461
|
+
with BuildPart(mode=Mode.PRIVATE) as test:
|
|
462
|
+
Box(20, 50, 10, align=(Align.CENTER, Align.CENTER, Align.MIN))
|
|
463
|
+
return test.part
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
def _narrow_subpart(**kwargs) -> Part:
|
|
467
|
+
return dovetail_subpart(_narrow_box(), Point(-10, 0), Point(10, 0), **kwargs)
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
class TestSubpartsAreSingleSolids:
|
|
471
|
+
"""A subpart must come back in one piece, whatever the arguments.
|
|
472
|
+
|
|
473
|
+
Two defects hid here for the library's whole history, both invisible to an
|
|
474
|
+
is_valid assertion because every piece was individually a valid solid.
|
|
475
|
+
|
|
476
|
+
A positive vertical_offset built the two Z-slabs with mismatched tolerance
|
|
477
|
+
terms, cutting a slot clean through the socket and leaving it in two pieces
|
|
478
|
+
with a tenth of its volume gone.
|
|
479
|
+
|
|
480
|
+
click_fit_radius added each divot twice: a Divot is a BasePartObject, so it
|
|
481
|
+
registers into the enclosing builder on construction, and the code then
|
|
482
|
+
added a rotated copy as well. Because rotate() turns about the global
|
|
483
|
+
origin, the builder's Location translated that copy again and it landed
|
|
484
|
+
outside the part as a free-floating sliver.
|
|
485
|
+
"""
|
|
486
|
+
|
|
487
|
+
@pytest.mark.parametrize("style", list(DovetailStyle))
|
|
488
|
+
@pytest.mark.parametrize("vertical_offset", [0.0, 0.5, -0.5])
|
|
489
|
+
@pytest.mark.parametrize("click_fit_radius", [0.0, 0.5])
|
|
490
|
+
@pytest.mark.parametrize("subpart", [DovetailSubpart.TAIL, DovetailSubpart.SOCKET])
|
|
491
|
+
def test_subpart_is_one_solid(
|
|
492
|
+
self, style, vertical_offset, click_fit_radius, subpart
|
|
493
|
+
):
|
|
494
|
+
part = _narrow_subpart(
|
|
495
|
+
style=style,
|
|
496
|
+
subpart=subpart,
|
|
497
|
+
vertical_offset=vertical_offset,
|
|
498
|
+
click_fit_radius=click_fit_radius,
|
|
499
|
+
)
|
|
500
|
+
solids = part.solids()
|
|
501
|
+
assert len(solids) == 1, (
|
|
502
|
+
f"{style.name} {subpart.name} at vertical_offset={vertical_offset}, "
|
|
503
|
+
f"click_fit_radius={click_fit_radius} came back as {len(solids)} "
|
|
504
|
+
f"solids: {sorted(round(s.volume, 4) for s in solids)}"
|
|
505
|
+
)
|
|
506
|
+
|
|
507
|
+
@pytest.mark.parametrize("style", list(DovetailStyle))
|
|
508
|
+
@pytest.mark.parametrize("vertical_offset", [0.0, 0.5, -0.5])
|
|
509
|
+
def test_tail_and_socket_account_for_the_whole_part(self, style, vertical_offset):
|
|
510
|
+
"""Together the two subparts should be the original, less tolerance.
|
|
511
|
+
|
|
512
|
+
The vertical_offset defect showed up here first: the pair summed to 899
|
|
513
|
+
of a 1000mm3 box, because the slot cut through the socket removed
|
|
514
|
+
material that ended up in neither half.
|
|
515
|
+
"""
|
|
516
|
+
kwargs = dict(style=style, vertical_offset=vertical_offset)
|
|
517
|
+
total = sum(
|
|
518
|
+
_narrow_subpart(subpart=s, **kwargs).volume
|
|
519
|
+
for s in (DovetailSubpart.TAIL, DovetailSubpart.SOCKET)
|
|
520
|
+
)
|
|
521
|
+
box_volume = 20 * 50 * 10
|
|
522
|
+
assert total == pytest.approx(box_volume, rel=0.02), (
|
|
523
|
+
f"{style.name} at vertical_offset={vertical_offset}: tail and socket "
|
|
524
|
+
f"sum to {total:.1f}, not the {box_volume} of the part they split"
|
|
525
|
+
)
|
|
526
|
+
|
|
527
|
+
|
|
528
|
+
class TestSnugtailWidthLimit:
|
|
529
|
+
"""Snugtail splits its own tail into fins on parts wider than about 30mm.
|
|
530
|
+
|
|
531
|
+
Not a regression: v0.1.5 produces byte-identical geometry. The tail comes
|
|
532
|
+
back as two or three disconnected pieces with no sliver involved, so it is
|
|
533
|
+
unrelated to the divot and vertical_offset defects fixed alongside these
|
|
534
|
+
tests. It lives in the snugtail outline maths, which was tuned against
|
|
535
|
+
physical prints, so it is recorded here rather than guessed at.
|
|
536
|
+
|
|
537
|
+
These tests pin the current boundary. If a fix lands, they should fail and
|
|
538
|
+
be replaced by an assertion that wide parts work.
|
|
539
|
+
"""
|
|
540
|
+
|
|
541
|
+
@staticmethod
|
|
542
|
+
def _tail_solids(width):
|
|
543
|
+
with BuildPart(mode=Mode.PRIVATE) as test:
|
|
544
|
+
Box(width, 60, 30, align=(Align.CENTER, Align.CENTER, Align.MIN))
|
|
545
|
+
return len(
|
|
546
|
+
dovetail_subpart(
|
|
547
|
+
test.part,
|
|
548
|
+
Point(-width / 2, 0),
|
|
549
|
+
Point(width / 2, 0),
|
|
550
|
+
subpart=DovetailSubpart.TAIL,
|
|
551
|
+
style=DovetailStyle.SNUGTAIL,
|
|
552
|
+
click_fit_radius=0,
|
|
553
|
+
).solids()
|
|
554
|
+
)
|
|
555
|
+
|
|
556
|
+
@pytest.mark.parametrize("width", [20, 24, 28])
|
|
557
|
+
def test_narrow_parts_give_one_solid(self, width):
|
|
558
|
+
assert self._tail_solids(width) == 1
|
|
559
|
+
|
|
560
|
+
@pytest.mark.parametrize("width", [30, 36, 40])
|
|
561
|
+
def test_wide_parts_currently_fragment(self, width):
|
|
562
|
+
"""Documents a known defect. Delete this when snugtail is fixed."""
|
|
563
|
+
assert self._tail_solids(width) > 1
|
|
@@ -1,14 +1,10 @@
|
|
|
1
1
|
from collections import defaultdict
|
|
2
|
-
from importlib.machinery import SourceFileLoader
|
|
3
|
-
from importlib.util import module_from_spec, spec_from_loader
|
|
4
2
|
from math import atan2, cos, degrees, sin
|
|
5
|
-
from unittest.mock import patch
|
|
6
3
|
|
|
7
4
|
import pytest
|
|
8
5
|
from build123d import Align, Box, Cone, Cylinder
|
|
9
6
|
|
|
10
7
|
from b3dkit.hexwall import HexCylindrical, HexWall
|
|
11
|
-
from conftest import module_path
|
|
12
8
|
|
|
13
9
|
|
|
14
10
|
def _cone():
|
|
@@ -19,26 +15,21 @@ def _cone():
|
|
|
19
15
|
class TestHexWall:
|
|
20
16
|
def test_hexwall(self):
|
|
21
17
|
pattern = HexWall(10, 10, 1, 1, 0.2)
|
|
22
|
-
assert pattern.
|
|
18
|
+
assert pattern.volume == pytest.approx(18.5680, rel=1e-4)
|
|
19
|
+
assert pattern.bounding_box().size.X == pytest.approx(10)
|
|
20
|
+
assert pattern.bounding_box().size.Y == pytest.approx(10)
|
|
21
|
+
# 3 solids: the field, plus a 0.1mm3 partial hexagon clipped at each of
|
|
22
|
+
# the left and right bounds. Suspect, but pinned rather than changed.
|
|
23
|
+
assert len(pattern.solids()) == 3
|
|
23
24
|
|
|
24
25
|
def test_hexwall_forces_odd_column_count(self):
|
|
25
26
|
# length=13/apothem=1 yields an even raw column count, exercising the
|
|
26
27
|
# branch that bumps it odd so the hex grid stays centered
|
|
27
28
|
pattern = HexWall(13, 10, 1, 1, 0.2)
|
|
28
|
-
assert pattern.
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
with (
|
|
33
|
-
patch("build123d.export_stl"),
|
|
34
|
-
patch("pathlib.Path.mkdir"),
|
|
35
|
-
patch("pathlib.Path.exists"),
|
|
36
|
-
patch("pathlib.Path.is_dir"),
|
|
37
|
-
patch("ocp_vscode.show"),
|
|
38
|
-
patch("ocp_vscode.save_screenshot"),
|
|
39
|
-
):
|
|
40
|
-
loader = SourceFileLoader("__main__", module_path("hexwall"))
|
|
41
|
-
loader.exec_module(module_from_spec(spec_from_loader(loader.name, loader)))
|
|
29
|
+
assert pattern.volume == pytest.approx(23.2053, rel=1e-4)
|
|
30
|
+
assert pattern.bounding_box().size.X == pytest.approx(13)
|
|
31
|
+
# the odd column count leaves no clipped hexagons at the bounds
|
|
32
|
+
assert len(pattern.solids()) == 1
|
|
42
33
|
|
|
43
34
|
|
|
44
35
|
class TestHexCylindrical:
|
|
@@ -1,7 +1,3 @@
|
|
|
1
|
-
from importlib.machinery import SourceFileLoader
|
|
2
|
-
from importlib.util import module_from_spec, spec_from_loader
|
|
3
|
-
from unittest.mock import patch
|
|
4
|
-
|
|
5
1
|
import pytest
|
|
6
2
|
from build123d import Align, Axis, Box, BuildPart, Compound, Part, fillet
|
|
7
3
|
|
|
@@ -12,7 +8,6 @@ from b3dkit.high_top_slide_box import (
|
|
|
12
8
|
high_top_slide_box_base,
|
|
13
9
|
high_top_slide_box_lid,
|
|
14
10
|
)
|
|
15
|
-
from conftest import module_path
|
|
16
11
|
|
|
17
12
|
|
|
18
13
|
def assert_single_solid(part, name="part"):
|
|
@@ -284,19 +279,6 @@ class TestHighTopSlideBox:
|
|
|
284
279
|
assert_single_solid(result.children[0], "lid")
|
|
285
280
|
assert_single_solid(result.children[1], "base")
|
|
286
281
|
|
|
287
|
-
def test_direct_run(self):
|
|
288
|
-
"""Test that the module can be run directly without errors."""
|
|
289
|
-
with (
|
|
290
|
-
patch("ocp_vscode.show"),
|
|
291
|
-
(
|
|
292
|
-
patch("build123d.export_stl")
|
|
293
|
-
if hasattr(__import__("build123d"), "export_stl")
|
|
294
|
-
else patch("builtins.open")
|
|
295
|
-
),
|
|
296
|
-
):
|
|
297
|
-
loader = SourceFileLoader("__main__", module_path("high_top_slide_box"))
|
|
298
|
-
loader.exec_module(module_from_spec(spec_from_loader(loader.name, loader)))
|
|
299
|
-
|
|
300
282
|
def test_parameter_validation_edge_cases(self, small_base_part):
|
|
301
283
|
"""Test edge cases for parameter validation."""
|
|
302
284
|
# Test with very thin walls
|
|
@@ -1,12 +1,7 @@
|
|
|
1
|
-
from importlib.machinery import SourceFileLoader
|
|
2
|
-
from importlib.util import module_from_spec, spec_from_loader
|
|
3
|
-
from unittest.mock import patch
|
|
4
|
-
|
|
5
1
|
import pytest
|
|
6
2
|
from build123d import Align, Axis, Box, BuildPart, Plane, fillet, section
|
|
7
3
|
|
|
8
4
|
from b3dkit.slide_box import _slider_template, slide_box
|
|
9
|
-
from conftest import module_path
|
|
10
5
|
|
|
11
6
|
|
|
12
7
|
class TestSlideBox:
|
|
@@ -18,21 +13,14 @@ class TestSlideBox:
|
|
|
18
13
|
sb = slide_box(
|
|
19
14
|
base_box.part, wall_thickness=2, thumb_radius=3.5, divot_radius=0.5
|
|
20
15
|
)
|
|
21
|
-
|
|
22
|
-
assert
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
patch("pathlib.Path.exists"),
|
|
30
|
-
patch("pathlib.Path.is_dir"),
|
|
31
|
-
patch("ocp_vscode.show"),
|
|
32
|
-
patch("ocp_vscode.save_screenshot"),
|
|
33
|
-
):
|
|
34
|
-
loader = SourceFileLoader("__main__", module_path("slide_box"))
|
|
35
|
-
loader.exec_module(module_from_spec(spec_from_loader(loader.name, loader)))
|
|
16
|
+
box, lid = sb.children
|
|
17
|
+
assert (box.label, lid.label) == ("box", "lid")
|
|
18
|
+
assert len(box.solids()) == 1
|
|
19
|
+
assert len(lid.solids()) == 1
|
|
20
|
+
assert box.volume == pytest.approx(4632.0405, rel=1e-4)
|
|
21
|
+
assert lid.volume == pytest.approx(1498.3503, rel=1e-4)
|
|
22
|
+
# the lid slides inside the box, so it must be narrower
|
|
23
|
+
assert lid.bounding_box().size.X < box.bounding_box().size.X
|
|
36
24
|
|
|
37
25
|
|
|
38
26
|
class TestSliderDivots:
|
|
@@ -47,7 +35,8 @@ class TestSliderDivots:
|
|
|
47
35
|
"""
|
|
48
36
|
|
|
49
37
|
@pytest.fixture(scope="class")
|
|
50
|
-
|
|
38
|
+
@classmethod
|
|
39
|
+
def sketch(cls):
|
|
51
40
|
with BuildPart() as blank:
|
|
52
41
|
Box(60, 100, 20, align=(Align.CENTER, Align.CENTER, Align.MIN))
|
|
53
42
|
fillet(blank.part.edges().filter_by(Axis.Z), radius=1.5)
|
|
@@ -1,29 +1,12 @@
|
|
|
1
|
-
from importlib.machinery import SourceFileLoader
|
|
2
|
-
from importlib.util import module_from_spec, spec_from_loader
|
|
3
|
-
from unittest.mock import patch
|
|
4
|
-
|
|
5
1
|
import pytest
|
|
6
2
|
|
|
7
3
|
from b3dkit.twist_snap import (
|
|
8
4
|
TwistSnapConnector,
|
|
9
5
|
TwistSnapSocket,
|
|
10
6
|
)
|
|
11
|
-
from conftest import module_path
|
|
12
7
|
|
|
13
8
|
|
|
14
9
|
class TestTwistSnap:
|
|
15
|
-
def test_bare_execution(self):
|
|
16
|
-
with (
|
|
17
|
-
patch("build123d.export_stl"),
|
|
18
|
-
patch("pathlib.Path.mkdir"),
|
|
19
|
-
patch("pathlib.Path.exists"),
|
|
20
|
-
patch("pathlib.Path.is_dir"),
|
|
21
|
-
patch("ocp_vscode.show"),
|
|
22
|
-
patch("ocp_vscode.save_screenshot"),
|
|
23
|
-
):
|
|
24
|
-
loader = SourceFileLoader("__main__", module_path("twist_snap"))
|
|
25
|
-
loader.exec_module(module_from_spec(spec_from_loader(loader.name, loader)))
|
|
26
|
-
|
|
27
10
|
def test_twist_snap_connector(self):
|
|
28
11
|
connector = TwistSnapConnector(
|
|
29
12
|
connector_radius=4.5,
|
|
@@ -31,7 +14,12 @@ class TestTwistSnap:
|
|
|
31
14
|
snapfit_radius_extension=2 * (2 / 3),
|
|
32
15
|
wall_depth=2,
|
|
33
16
|
)
|
|
34
|
-
assert connector.
|
|
17
|
+
assert connector.volume == pytest.approx(275.6677, rel=1e-4)
|
|
18
|
+
assert connector.bounding_box().size.X == pytest.approx(11.5451, rel=1e-4)
|
|
19
|
+
# Known issue: the base and its four snapfits meet only on a shared
|
|
20
|
+
# plane, so they never fuse and this returns five solids. Pinned rather
|
|
21
|
+
# than asserted as correct; when it is fixed this should read == 1.
|
|
22
|
+
assert len(connector.solids()) == 5
|
|
35
23
|
|
|
36
24
|
def test_connector_takes_no_clearance_arguments(self):
|
|
37
25
|
"""All fit clearance lives on the socket; the connector is nominal.
|
|
@@ -80,4 +68,7 @@ class TestTwistSnap:
|
|
|
80
68
|
wall_width=2,
|
|
81
69
|
wall_depth=2,
|
|
82
70
|
)
|
|
83
|
-
assert socket.
|
|
71
|
+
assert len(socket.solids()) == 1
|
|
72
|
+
assert socket.volume == pytest.approx(630.1939, rel=1e-4)
|
|
73
|
+
# the socket has to swallow the connector, so it is the wider part
|
|
74
|
+
assert socket.bounding_box().size.X > 11.5451
|
b3dkit-0.4.0/.coveragerc
DELETED
b3dkit-0.4.0/tests/conftest.py
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import os
|
|
2
|
-
|
|
3
|
-
# Absolute path to the package source, so tests that load a module by file path
|
|
4
|
-
# work regardless of the directory pytest was invoked from.
|
|
5
|
-
PACKAGE_DIR = os.path.abspath(
|
|
6
|
-
os.path.join(os.path.dirname(__file__), "..", "src", "b3dkit")
|
|
7
|
-
)
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
def module_path(module_name: str) -> str:
|
|
11
|
-
"""Absolute path to a b3dkit source file, e.g. module_path("dovetail")."""
|
|
12
|
-
return os.path.join(PACKAGE_DIR, f"{module_name}.py")
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
from importlib.machinery import SourceFileLoader
|
|
2
|
-
from importlib.util import module_from_spec, spec_from_loader
|
|
3
|
-
from unittest.mock import patch
|
|
4
|
-
|
|
5
|
-
from b3dkit.click_fit import Divot
|
|
6
|
-
from conftest import module_path
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
class TestClickfit:
|
|
10
|
-
def test_divot(self):
|
|
11
|
-
hole = Divot(10, False)
|
|
12
|
-
assert hole.is_valid
|
|
13
|
-
bump = Divot(10, True)
|
|
14
|
-
assert bump.is_valid
|
|
15
|
-
assert hole.volume > bump.volume
|
|
16
|
-
|
|
17
|
-
def test_divot_extend_base(self):
|
|
18
|
-
bump = Divot(10, True, extend_base=True)
|
|
19
|
-
assert bump.is_valid
|
|
20
|
-
|
|
21
|
-
def test_direct_run(self):
|
|
22
|
-
|
|
23
|
-
with (
|
|
24
|
-
patch("build123d.export_stl"),
|
|
25
|
-
patch("pathlib.Path.mkdir"),
|
|
26
|
-
patch("pathlib.Path.exists"),
|
|
27
|
-
patch("pathlib.Path.is_dir"),
|
|
28
|
-
patch("ocp_vscode.show"),
|
|
29
|
-
patch("ocp_vscode.save_screenshot"),
|
|
30
|
-
):
|
|
31
|
-
loader = SourceFileLoader("__main__", module_path("click_fit"))
|
|
32
|
-
loader.exec_module(module_from_spec(spec_from_loader(loader.name, loader)))
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|