b3dkit 0.4.0__tar.gz → 0.5.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- b3dkit-0.5.2/.coveragerc +20 -0
- {b3dkit-0.4.0 → b3dkit-0.5.2}/CHANGELOG.md +68 -1
- {b3dkit-0.4.0 → b3dkit-0.5.2}/CONTRIBUTING.md +5 -0
- {b3dkit-0.4.0 → b3dkit-0.5.2}/PKG-INFO +1 -1
- {b3dkit-0.4.0 → b3dkit-0.5.2}/docs/slide_box.md +3 -1
- {b3dkit-0.4.0 → b3dkit-0.5.2}/pyproject.toml +12 -3
- {b3dkit-0.4.0 → b3dkit-0.5.2}/src/b3dkit/antichamfer.py +10 -10
- {b3dkit-0.4.0 → b3dkit-0.5.2}/src/b3dkit/basic_shapes.py +1 -1
- {b3dkit-0.4.0 → b3dkit-0.5.2}/src/b3dkit/bolt_fittings.py +5 -5
- {b3dkit-0.4.0 → b3dkit-0.5.2}/src/b3dkit/dovetail.py +23 -31
- {b3dkit-0.4.0 → b3dkit-0.5.2}/src/b3dkit/high_top_slide_box.py +28 -23
- {b3dkit-0.4.0 → b3dkit-0.5.2}/src/b3dkit/slide_box.py +50 -16
- {b3dkit-0.4.0 → b3dkit-0.5.2}/src/b3dkit/twist_snap.py +4 -1
- {b3dkit-0.4.0 → b3dkit-0.5.2}/tests/test_antichamfer.py +4 -19
- {b3dkit-0.4.0 → b3dkit-0.5.2}/tests/test_ball_socket.py +4 -13
- {b3dkit-0.4.0 → b3dkit-0.5.2}/tests/test_basic_shapes.py +0 -15
- {b3dkit-0.4.0 → b3dkit-0.5.2}/tests/test_bolt_fittings.py +47 -20
- b3dkit-0.5.2/tests/test_click_fit.py +19 -0
- {b3dkit-0.4.0 → b3dkit-0.5.2}/tests/test_dovetail.py +137 -25
- {b3dkit-0.4.0 → b3dkit-0.5.2}/tests/test_hexwall.py +10 -19
- {b3dkit-0.4.0 → b3dkit-0.5.2}/tests/test_high_top_slide_box.py +1 -18
- {b3dkit-0.4.0 → b3dkit-0.5.2}/tests/test_public_api.py +1 -1
- b3dkit-0.5.2/tests/test_slide_box.py +185 -0
- {b3dkit-0.4.0 → b3dkit-0.5.2}/tests/test_twist_snap.py +11 -20
- 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/tests/test_slide_box.py +0 -83
- {b3dkit-0.4.0 → b3dkit-0.5.2}/.github/workflows/release.yml +0 -0
- {b3dkit-0.4.0 → b3dkit-0.5.2}/.github/workflows/test.yml +0 -0
- {b3dkit-0.4.0 → b3dkit-0.5.2}/.gitignore +0 -0
- {b3dkit-0.4.0 → b3dkit-0.5.2}/.pre-commit-config.yaml +0 -0
- {b3dkit-0.4.0 → b3dkit-0.5.2}/.vscode/settings.json +0 -0
- {b3dkit-0.4.0 → b3dkit-0.5.2}/LICENSE +0 -0
- {b3dkit-0.4.0 → b3dkit-0.5.2}/README.md +0 -0
- {b3dkit-0.4.0 → b3dkit-0.5.2}/docs/CONTRIBUTING.md +0 -0
- {b3dkit-0.4.0 → b3dkit-0.5.2}/docs/antichamfer.md +0 -0
- {b3dkit-0.4.0 → b3dkit-0.5.2}/docs/ball_socket.md +0 -0
- {b3dkit-0.4.0 → b3dkit-0.5.2}/docs/basic_shapes.md +0 -0
- {b3dkit-0.4.0 → b3dkit-0.5.2}/docs/bolt_fittings.md +0 -0
- {b3dkit-0.4.0 → b3dkit-0.5.2}/docs/click_fit.md +0 -0
- {b3dkit-0.4.0 → b3dkit-0.5.2}/docs/dovetail.md +0 -0
- {b3dkit-0.4.0 → b3dkit-0.5.2}/docs/dovetail.png +0 -0
- {b3dkit-0.4.0 → b3dkit-0.5.2}/docs/hexwall.md +0 -0
- {b3dkit-0.4.0 → b3dkit-0.5.2}/docs/high_top_slide_box.md +0 -0
- {b3dkit-0.4.0 → b3dkit-0.5.2}/docs/index.md +0 -0
- {b3dkit-0.4.0 → b3dkit-0.5.2}/docs/point.md +0 -0
- {b3dkit-0.4.0 → b3dkit-0.5.2}/docs/reference/antichamfer.md +0 -0
- {b3dkit-0.4.0 → b3dkit-0.5.2}/docs/reference/ball_socket.md +0 -0
- {b3dkit-0.4.0 → b3dkit-0.5.2}/docs/reference/basic_shapes.md +0 -0
- {b3dkit-0.4.0 → b3dkit-0.5.2}/docs/reference/bolt_fittings.md +0 -0
- {b3dkit-0.4.0 → b3dkit-0.5.2}/docs/reference/click_fit.md +0 -0
- {b3dkit-0.4.0 → b3dkit-0.5.2}/docs/reference/dovetail.md +0 -0
- {b3dkit-0.4.0 → b3dkit-0.5.2}/docs/reference/hexwall.md +0 -0
- {b3dkit-0.4.0 → b3dkit-0.5.2}/docs/reference/high_top_slide_box.md +0 -0
- {b3dkit-0.4.0 → b3dkit-0.5.2}/docs/reference/index.md +0 -0
- {b3dkit-0.4.0 → b3dkit-0.5.2}/docs/reference/point.md +0 -0
- {b3dkit-0.4.0 → b3dkit-0.5.2}/docs/reference/slide_box.md +0 -0
- {b3dkit-0.4.0 → b3dkit-0.5.2}/docs/reference/twist_snap.md +0 -0
- {b3dkit-0.4.0 → b3dkit-0.5.2}/docs/requirements.txt +0 -0
- {b3dkit-0.4.0 → b3dkit-0.5.2}/docs/twist_snap.md +0 -0
- {b3dkit-0.4.0 → b3dkit-0.5.2}/docs/twist_snap.png +0 -0
- {b3dkit-0.4.0 → b3dkit-0.5.2}/mkdocs.yml +0 -0
- {b3dkit-0.4.0 → b3dkit-0.5.2}/readthedocs.yaml +0 -0
- {b3dkit-0.4.0 → b3dkit-0.5.2}/src/b3dkit/__init__.py +0 -0
- {b3dkit-0.4.0 → b3dkit-0.5.2}/src/b3dkit/ball_socket.py +0 -0
- {b3dkit-0.4.0 → b3dkit-0.5.2}/src/b3dkit/click_fit.py +0 -0
- {b3dkit-0.4.0 → b3dkit-0.5.2}/src/b3dkit/hexwall.py +0 -0
- {b3dkit-0.4.0 → b3dkit-0.5.2}/src/b3dkit/point.py +0 -0
- {b3dkit-0.4.0 → b3dkit-0.5.2}/src/b3dkit/py.typed +0 -0
- {b3dkit-0.4.0 → b3dkit-0.5.2}/tests/test_docs.py +0 -0
- {b3dkit-0.4.0 → b3dkit-0.5.2}/tests/test_point.py +0 -0
b3dkit-0.5.2/.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,70 @@ ship in minor releases.
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- **build123d 0.12 and 0.13 support.** `TeardropBoltCutSinkhole`,
|
|
13
|
+
`BoltCutSinkhole`, `SquareNutSinkhole`, `TwistSnapSocket` and the
|
|
14
|
+
`high_top_slide_box` parts built the wrong geometry on build123d 0.12+, which
|
|
15
|
+
places geometry inside located builders differently. Every public object now
|
|
16
|
+
builds identically on 0.11, 0.12 and 0.13; 0.11 geometry is unchanged.
|
|
17
|
+
- **`Teardrop`'s `rotation` is a float**, defaulting to `0`. The old `(0, 0)`
|
|
18
|
+
default raised `TypeError` on build123d 0.12+.
|
|
19
|
+
|
|
20
|
+
## [0.5.2] - 2026-10-03
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
|
|
24
|
+
- **`slide_box` sliding edges are filleted** by `wall_thickness * 0.025` on
|
|
25
|
+
both the slot and the lid.
|
|
26
|
+
|
|
27
|
+
## [0.5.1] - 2026-10-01
|
|
28
|
+
|
|
29
|
+
### Changed
|
|
30
|
+
|
|
31
|
+
- **`slide_box` lids no longer intersect the box.** The lid was
|
|
32
|
+
`wall_thickness + tolerance` thick in a `wall_thickness` slot. `tolerance` is
|
|
33
|
+
now the total gap on every mating face, split evenly: the slot grows by half
|
|
34
|
+
and the lid shrinks by half, so the lid is `wall_thickness - tolerance / 2`
|
|
35
|
+
thick and its top stays flush. Box, lid and `slide_lid` geometry all change.
|
|
36
|
+
- **`slide_box` divots now line up.** The box detents and lid bumps were placed
|
|
37
|
+
from different tolerances and sat up to `tolerance` apart.
|
|
38
|
+
|
|
39
|
+
## [0.5.0] - 2026-09-09
|
|
40
|
+
|
|
41
|
+
### Changed
|
|
42
|
+
|
|
43
|
+
- **`NutCut` now sits on the origin rather than straddling it.** The recess ran
|
|
44
|
+
from `-head_depth` to `0` while the shaft began at `+head_depth`, so the two
|
|
45
|
+
never met: the cutter came back as two solids with a `head_depth` gap, and
|
|
46
|
+
subtracting it left a membrane the bolt could not pass through. It is now one
|
|
47
|
+
solid spanning `0` to `head_depth + shaft_length`.
|
|
48
|
+
|
|
49
|
+
**This moves the cutter.** It previously had to be positioned *below* a
|
|
50
|
+
surface to work; it can now be placed *on* one. Anything calling `NutCut` and
|
|
51
|
+
compensating for the old placement needs that compensation removed.
|
|
52
|
+
|
|
53
|
+
- **`dovetail_subpart` no longer adds each click-fit divot twice.** One copy
|
|
54
|
+
landed outside the part as a free-floating sliver. Only geometry using
|
|
55
|
+
`click_fit_radius` is affected; the tail's fused solid is unchanged and the
|
|
56
|
+
socket's moves by 0.02%.
|
|
57
|
+
|
|
58
|
+
- **A positive `vertical_offset` no longer cuts the dovetail socket in two.**
|
|
59
|
+
Tails, `vertical_offset=0` and negative offsets are all unchanged.
|
|
60
|
+
|
|
61
|
+
### Known issues
|
|
62
|
+
|
|
63
|
+
Found while adding solid-count assertions, all pre-existing and all invisible to
|
|
64
|
+
the `is_valid` checks that used to stand in for them:
|
|
65
|
+
|
|
66
|
+
- `TwistSnapConnector` returns five solids: a base and four snapfits that meet
|
|
67
|
+
only on a shared plane and never fuse.
|
|
68
|
+
- `HexWall` can return partial hexagons clipped at the bounds as separate
|
|
69
|
+
slivers, depending on how the grid divides the given width.
|
|
70
|
+
- `SNUGTAIL` splits its own tail into disconnected fins on parts wider than
|
|
71
|
+
about 30mm. Pinned by `TestSnugtailWidthLimit`.
|
|
72
|
+
|
|
73
|
+
|
|
10
74
|
## [0.4.0] - 2026-09-08
|
|
11
75
|
|
|
12
76
|
### Removed
|
|
@@ -111,6 +175,9 @@ the Python 3.11 floor.
|
|
|
111
175
|
See the git history. b3dkit began as `fb-library`; the rename accompanied a
|
|
112
176
|
rewrite that reworked names and usage to feel closer to native build123d.
|
|
113
177
|
|
|
114
|
-
[Unreleased]: https://github.com/x0pherl/b3dkit/compare/v0.
|
|
178
|
+
[Unreleased]: https://github.com/x0pherl/b3dkit/compare/v0.5.2...HEAD
|
|
179
|
+
[0.5.2]: https://github.com/x0pherl/b3dkit/releases/tag/v0.5.2
|
|
180
|
+
[0.5.1]: https://github.com/x0pherl/b3dkit/releases/tag/v0.5.1
|
|
181
|
+
[0.5.0]: https://github.com/x0pherl/b3dkit/releases/tag/v0.5.0
|
|
115
182
|
[0.4.0]: https://github.com/x0pherl/b3dkit/releases/tag/v0.4.0
|
|
116
183
|
[0.3.2]: https://github.com/x0pherl/b3dkit/releases/tag/v0.3.2
|
|
@@ -84,6 +84,11 @@ release, raise the floor in `pyproject.toml` in the same change. b3dkit 0.1.5
|
|
|
84
84
|
shipped code that could not run against its own advertised minimum, which is
|
|
85
85
|
what that CI job exists to prevent.
|
|
86
86
|
|
|
87
|
+
Inside a `BuildPart` that has a location, do not open another builder or
|
|
88
|
+
sketch, and give operations geometry from the builder (`builder.faces()`), not
|
|
89
|
+
from `builder.part`. build123d 0.11 and 0.12+ place geometry differently in
|
|
90
|
+
both cases.
|
|
91
|
+
|
|
87
92
|
Do not add an upper bound. Caps propagate into every downstream resolution and
|
|
88
93
|
convert a runtime failure into an install failure for everyone.
|
|
89
94
|
|
|
@@ -45,7 +45,9 @@ The `x_straighten_distance` parameter controls how much of the sliding mechanism
|
|
|
45
45
|
## Design Considerations
|
|
46
46
|
|
|
47
47
|
### Tolerances
|
|
48
|
-
The `tolerance` parameter
|
|
48
|
+
The `tolerance` parameter is the total gap between the lid and base on every mating face, split evenly between the two: the slot grows by half of it and the lid shrinks by half of it. The lid is `wall_thickness - tolerance / 2` thick, with its top flush with the box.
|
|
49
|
+
|
|
50
|
+
Typical values:
|
|
49
51
|
- **0.1mm**: Very tight fit, may require force to operate
|
|
50
52
|
- **0.15mm**: Standard fit (default) - smooth operation with minimal play
|
|
51
53
|
- **0.2-0.25mm**: Loose fit for rough printing or materials that swell
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "b3dkit"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.5.2"
|
|
4
4
|
authors = [
|
|
5
5
|
{ name="x0pherl"},
|
|
6
6
|
]
|
|
@@ -63,8 +63,17 @@ 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.*",
|
|
75
|
+
# build123d 0.13 renamed add() to insert(); 0.11 has no insert().
|
|
76
|
+
"ignore:add\\(\\) is deprecated:DeprecationWarning",
|
|
68
77
|
]
|
|
69
78
|
|
|
70
79
|
[tool.black]
|
|
@@ -11,7 +11,6 @@ from build123d import (
|
|
|
11
11
|
Face,
|
|
12
12
|
Iterable,
|
|
13
13
|
Location,
|
|
14
|
-
Mode,
|
|
15
14
|
Part,
|
|
16
15
|
add,
|
|
17
16
|
extrude,
|
|
@@ -78,18 +77,19 @@ def anti_chamfer(
|
|
|
78
77
|
if length == 0 or length2 == 0:
|
|
79
78
|
return target
|
|
80
79
|
|
|
80
|
+
taper = -degrees(atan(length2 / length))
|
|
81
|
+
if context is not None:
|
|
82
|
+
# extrude straight into the caller's builder: a builder opened here would
|
|
83
|
+
# nest inside it, and build123d 0.12 changed where nested builders place
|
|
84
|
+
# their geometry
|
|
85
|
+
for f in faces_list:
|
|
86
|
+
extrude(f.offset(-length), amount=length, taper=taper)
|
|
87
|
+
return Part(Compound([context.part]).wrapped)
|
|
88
|
+
|
|
81
89
|
with BuildPart() as new_part:
|
|
82
90
|
add(target)
|
|
83
91
|
for f in faces_list:
|
|
84
|
-
extrude(
|
|
85
|
-
f.offset(-length),
|
|
86
|
-
amount=length,
|
|
87
|
-
taper=-degrees(atan(length2 / length)),
|
|
88
|
-
)
|
|
89
|
-
if context is not None:
|
|
90
|
-
context._add_to_context(
|
|
91
|
-
Part(Compound([new_part.part]).wrapped), mode=Mode.REPLACE
|
|
92
|
-
)
|
|
92
|
+
extrude(f.offset(-length), amount=length, taper=taper)
|
|
93
93
|
return Part(Compound([new_part.part]).wrapped)
|
|
94
94
|
|
|
95
95
|
|
|
@@ -92,7 +92,7 @@ class TeardropBoltCutSinkhole(BasePartObject):
|
|
|
92
92
|
amount=extension_distance,
|
|
93
93
|
)
|
|
94
94
|
anti_chamfer(
|
|
95
|
-
sinkhole.
|
|
95
|
+
sinkhole.faces().sort_by(Axis.Z)[-1],
|
|
96
96
|
chamfer_radius,
|
|
97
97
|
)
|
|
98
98
|
super().__init__(
|
|
@@ -205,7 +205,7 @@ class SquareNutSinkhole(BasePartObject):
|
|
|
205
205
|
nut_length,
|
|
206
206
|
align=(Align.CENTER, Align.MIN, Align.CENTER),
|
|
207
207
|
)
|
|
208
|
-
extrude(nut.
|
|
208
|
+
extrude(nut.faces().sort_by(Axis.Z)[-1], amount=nut_depth)
|
|
209
209
|
if bolt_extension > 0:
|
|
210
210
|
with BuildPart(Location((0, bolt_depth + nut_height, 0))) as nut:
|
|
211
211
|
TeardropCylinder(
|
|
@@ -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(
|
|
@@ -83,10 +83,11 @@ def _slide_top_rail_cut(
|
|
|
83
83
|
-(part_depth - wall_thickness) / 2,
|
|
84
84
|
)
|
|
85
85
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
86
|
+
# every child below is a nested builder or sketch, so this builder takes no
|
|
87
|
+
# location of its own: build123d 0.11 ignores a parent's location in nested
|
|
88
|
+
# builders and 0.12 composes it, so only an unlocated parent means the same
|
|
89
|
+
# thing to both
|
|
90
|
+
with BuildPart() as rail_cut:
|
|
90
91
|
with BuildSketch(Location((0, wall_thickness / 2))):
|
|
91
92
|
with BuildLine():
|
|
92
93
|
|
|
@@ -224,13 +225,15 @@ def _high_top_slide_box_top(
|
|
|
224
225
|
)
|
|
225
226
|
):
|
|
226
227
|
add(base_part)
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
228
|
+
# outside the located builder above, so it sits at z=0 on every
|
|
229
|
+
# build123d version
|
|
230
|
+
Box(
|
|
231
|
+
part_max_dimension,
|
|
232
|
+
part_max_dimension,
|
|
233
|
+
part_max_dimension,
|
|
234
|
+
align=(Align.CENTER, Align.CENTER, Align.MIN),
|
|
235
|
+
mode=Mode.INTERSECT,
|
|
236
|
+
)
|
|
234
237
|
with BuildPart(
|
|
235
238
|
Location((0, wall_thickness / 2, rail_height)),
|
|
236
239
|
mode=Mode.SUBTRACT,
|
|
@@ -389,6 +392,19 @@ def high_top_slide_box_base(
|
|
|
389
392
|
part_depth = base_part.bounding_box().size.Y
|
|
390
393
|
part_height = base_part.bounding_box().size.Z
|
|
391
394
|
|
|
395
|
+
# built before any builder opens: inside one it would be a nested builder,
|
|
396
|
+
# and build123d 0.12 changed where nested builders place their geometry
|
|
397
|
+
top_cut = _high_top_slide_box_top(
|
|
398
|
+
base_part,
|
|
399
|
+
top_height,
|
|
400
|
+
rail_height,
|
|
401
|
+
wall_thickness,
|
|
402
|
+
rail_angle,
|
|
403
|
+
divot_radius,
|
|
404
|
+
tolerance,
|
|
405
|
+
cut_template=True,
|
|
406
|
+
)
|
|
407
|
+
|
|
392
408
|
with BuildPart() as boxbottom:
|
|
393
409
|
add(base_part)
|
|
394
410
|
with BuildPart(
|
|
@@ -423,18 +439,7 @@ def high_top_slide_box_base(
|
|
|
423
439
|
),
|
|
424
440
|
mode=Mode.SUBTRACT,
|
|
425
441
|
):
|
|
426
|
-
add(
|
|
427
|
-
_high_top_slide_box_top(
|
|
428
|
-
base_part,
|
|
429
|
-
top_height,
|
|
430
|
-
rail_height,
|
|
431
|
-
wall_thickness,
|
|
432
|
-
rail_angle,
|
|
433
|
-
divot_radius,
|
|
434
|
-
tolerance,
|
|
435
|
-
cut_template=True,
|
|
436
|
-
)
|
|
437
|
-
)
|
|
442
|
+
add(top_cut)
|
|
438
443
|
with BuildPart(
|
|
439
444
|
Location(
|
|
440
445
|
(
|
|
@@ -7,7 +7,7 @@ They are exempt from the compatibility promise in ``__all__`` until that is
|
|
|
7
7
|
settled.
|
|
8
8
|
"""
|
|
9
9
|
|
|
10
|
-
from math import radians, tan
|
|
10
|
+
from math import cos, radians, tan
|
|
11
11
|
|
|
12
12
|
from build123d import (
|
|
13
13
|
Align,
|
|
@@ -43,6 +43,13 @@ __all__ = [
|
|
|
43
43
|
]
|
|
44
44
|
|
|
45
45
|
|
|
46
|
+
def _tapered_inset(clearance: float) -> float:
|
|
47
|
+
"""
|
|
48
|
+
The horizontal shift that moves a tapered face ``clearance`` along its normal.
|
|
49
|
+
"""
|
|
50
|
+
return clearance / cos(radians(_SLIDER_TAPER_ANGLE))
|
|
51
|
+
|
|
52
|
+
|
|
46
53
|
def _divot_spacing(
|
|
47
54
|
sketch_width: float,
|
|
48
55
|
wall_thickness: float,
|
|
@@ -59,14 +66,18 @@ def _divot_spacing(
|
|
|
59
66
|
tapered face. A grazing intersection leaves needle-thin slivers behind the
|
|
60
67
|
boolean, so pull the pair far enough inboard that a divot only ever meets the
|
|
61
68
|
flat underside.
|
|
69
|
+
|
|
70
|
+
The spacing is measured against the lid's tapered face, the narrower of the
|
|
71
|
+
two, so the box and lid templates place their divots identically.
|
|
62
72
|
"""
|
|
63
73
|
half_width = sketch_width / 2
|
|
74
|
+
lid_clearance = -abs(tolerance) / 2
|
|
64
75
|
# depth below the top of the template at the widest point of the divot
|
|
65
|
-
widest_depth = max(wall_thickness - divot_radius, 0)
|
|
76
|
+
widest_depth = max(wall_thickness + lid_clearance - divot_radius, 0)
|
|
66
77
|
tapered_face = (
|
|
67
78
|
half_width
|
|
68
79
|
- wall_thickness
|
|
69
|
-
|
|
80
|
+
+ _tapered_inset(lid_clearance)
|
|
70
81
|
+ widest_depth * tan(radians(_SLIDER_TAPER_ANGLE))
|
|
71
82
|
)
|
|
72
83
|
return (
|
|
@@ -89,13 +100,21 @@ def _slider_template(
|
|
|
89
100
|
) -> Part:
|
|
90
101
|
"""
|
|
91
102
|
Create a slider part based on a sketch.
|
|
103
|
+
|
|
104
|
+
``tolerance`` is the total gap between lid and box; the cut template grows
|
|
105
|
+
by half of it and the lid template shrinks by half of it. The top of the
|
|
106
|
+
template stays at the top of the sketch either way.
|
|
92
107
|
"""
|
|
108
|
+
clearance = abs(tolerance) / 2 * (1 if cut_template else -1)
|
|
93
109
|
with BuildPart() as slider_part:
|
|
94
110
|
with BuildSketch() as top_sketch:
|
|
95
|
-
offset(
|
|
111
|
+
offset(
|
|
112
|
+
sketch,
|
|
113
|
+
amount=-abs(wall_thickness) + _tapered_inset(clearance),
|
|
114
|
+
)
|
|
96
115
|
extrude(
|
|
97
116
|
top_sketch.sketch,
|
|
98
|
-
amount=-wall_thickness -
|
|
117
|
+
amount=-wall_thickness - clearance,
|
|
99
118
|
taper=-_SLIDER_TAPER_ANGLE,
|
|
100
119
|
)
|
|
101
120
|
cross_section = section(
|
|
@@ -111,15 +130,20 @@ def _slider_template(
|
|
|
111
130
|
cross_section, amount=x_straighten_distance * 2 + wall_thickness * 2
|
|
112
131
|
)
|
|
113
132
|
)
|
|
133
|
+
fillet(
|
|
134
|
+
slider_part.edges() - slider_part.faces().filter_by(Plane.XY).edges(),
|
|
135
|
+
wall_thickness * 0.025,
|
|
136
|
+
)
|
|
137
|
+
fillet(slider_part.faces().sort_by(Axis.Z)[0].edges(), wall_thickness * 0.025)
|
|
138
|
+
|
|
114
139
|
if divot_radius > 0:
|
|
115
140
|
# a divot wider than half the wall would overhang the open front of
|
|
116
|
-
# the
|
|
117
|
-
divot_y = max(
|
|
118
|
-
|
|
119
|
-
slider_part.part.bounding_box().min.Y + divot_radius + divot_radius / 5,
|
|
141
|
+
# the part; keep it far enough back to meet only the underside
|
|
142
|
+
divot_y = sketch.bounding_box().min.Y + max(
|
|
143
|
+
wall_thickness / 2, divot_radius + divot_radius / 5
|
|
120
144
|
)
|
|
121
145
|
with BuildPart(
|
|
122
|
-
Location((0, divot_y, -wall_thickness), (180, 0, 0)),
|
|
146
|
+
Location((0, divot_y, -wall_thickness - clearance), (180, 0, 0)),
|
|
123
147
|
mode=Mode.ADD,
|
|
124
148
|
):
|
|
125
149
|
with GridLocations(
|
|
@@ -161,7 +185,7 @@ def slide_lid(
|
|
|
161
185
|
args:
|
|
162
186
|
- part: the part defining the box's outer dimensions
|
|
163
187
|
- wall_thickness: the thickness of the box walls
|
|
164
|
-
- tolerance: the
|
|
188
|
+
- tolerance: the total gap between the lid and the base; 0.1 is a tight
|
|
165
189
|
fit, 0.15 the default, 0.2 and above loose
|
|
166
190
|
- top_offset: how far below the top of the part the sliding section sits,
|
|
167
191
|
for parts with features at the top worth preserving
|
|
@@ -247,7 +271,8 @@ def slide_box(
|
|
|
247
271
|
- thumb_radius: the radius of the thumb-grip cutout; 0 disables it
|
|
248
272
|
- x_straighten_distance: how much of the slider stays straight at the
|
|
249
273
|
edges, which stops wide lids binding at the corners
|
|
250
|
-
- tolerance: the
|
|
274
|
+
- tolerance: the total gap between the lid and the base, split evenly
|
|
275
|
+
between the slot and the lid
|
|
251
276
|
- divot_radius: the radius of the click-fit positioning divots; 0
|
|
252
277
|
disables them
|
|
253
278
|
|
|
@@ -260,7 +285,7 @@ def slide_box(
|
|
|
260
285
|
lid_cut_template = _slider_template(
|
|
261
286
|
cross_section,
|
|
262
287
|
wall_thickness,
|
|
263
|
-
tolerance=
|
|
288
|
+
tolerance=tolerance,
|
|
264
289
|
top_offset=top_offset,
|
|
265
290
|
x_straighten_distance=x_straighten_distance,
|
|
266
291
|
divot_radius=divot_radius,
|
|
@@ -303,11 +328,20 @@ def slide_box(
|
|
|
303
328
|
|
|
304
329
|
|
|
305
330
|
if __name__ == "__main__":
|
|
306
|
-
from ocp_vscode import
|
|
331
|
+
from ocp_vscode import show
|
|
307
332
|
|
|
308
333
|
with BuildPart() as base_box:
|
|
309
334
|
Box(20, 44, 14, align=(Align.CENTER, Align.CENTER, Align.MIN))
|
|
310
335
|
fillet(base_box.part.edges().filter_by(Axis.Z), radius=1.5)
|
|
311
336
|
|
|
312
|
-
sb = slide_box(
|
|
313
|
-
|
|
337
|
+
sb = slide_box(
|
|
338
|
+
base_box.part,
|
|
339
|
+
wall_thickness=2,
|
|
340
|
+
thumb_radius=3.5,
|
|
341
|
+
divot_radius=0.5,
|
|
342
|
+
tolerance=0.3,
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
show(
|
|
346
|
+
sb,
|
|
347
|
+
)
|
|
@@ -204,8 +204,11 @@ class TwistSnapSocket(BasePartObject):
|
|
|
204
204
|
align=(Align.CENTER, Align.CENTER, Align.MIN),
|
|
205
205
|
mode=Mode.SUBTRACT,
|
|
206
206
|
)
|
|
207
|
+
# snap_socket.part, not snap_socket.edges(): build123d 0.12 reports a
|
|
208
|
+
# located builder's edges in its local frame, and this path is used
|
|
209
|
+
# outside it
|
|
207
210
|
trace_path = (
|
|
208
|
-
snap_socket.edges()
|
|
211
|
+
snap_socket.part.edges()
|
|
209
212
|
.filter_by(GeomType.CIRCLE)
|
|
210
213
|
.sort_by(Axis.Z, reverse=True)
|
|
211
214
|
.sort_by(SortBy.RADIUS, reverse=True)[-1]
|
|
@@ -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)))
|