b3dkit 0.5.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.0 → b3dkit-0.5.2}/CHANGELOG.md +32 -1
- {b3dkit-0.5.0 → b3dkit-0.5.2}/CONTRIBUTING.md +5 -0
- {b3dkit-0.5.0 → b3dkit-0.5.2}/PKG-INFO +1 -1
- {b3dkit-0.5.0 → b3dkit-0.5.2}/docs/slide_box.md +3 -1
- {b3dkit-0.5.0 → b3dkit-0.5.2}/pyproject.toml +3 -1
- {b3dkit-0.5.0 → b3dkit-0.5.2}/src/b3dkit/antichamfer.py +10 -10
- {b3dkit-0.5.0 → b3dkit-0.5.2}/src/b3dkit/basic_shapes.py +1 -1
- {b3dkit-0.5.0 → b3dkit-0.5.2}/src/b3dkit/bolt_fittings.py +2 -2
- {b3dkit-0.5.0 → b3dkit-0.5.2}/src/b3dkit/high_top_slide_box.py +28 -23
- {b3dkit-0.5.0 → b3dkit-0.5.2}/src/b3dkit/slide_box.py +50 -16
- {b3dkit-0.5.0 → b3dkit-0.5.2}/src/b3dkit/twist_snap.py +4 -1
- {b3dkit-0.5.0 → b3dkit-0.5.2}/tests/test_high_top_slide_box.py +1 -0
- {b3dkit-0.5.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.5.0 → b3dkit-0.5.2}/tests/test_twist_snap.py +1 -1
- b3dkit-0.5.0/tests/test_slide_box.py +0 -72
- {b3dkit-0.5.0 → b3dkit-0.5.2}/.coveragerc +0 -0
- {b3dkit-0.5.0 → b3dkit-0.5.2}/.github/workflows/release.yml +0 -0
- {b3dkit-0.5.0 → b3dkit-0.5.2}/.github/workflows/test.yml +0 -0
- {b3dkit-0.5.0 → b3dkit-0.5.2}/.gitignore +0 -0
- {b3dkit-0.5.0 → b3dkit-0.5.2}/.pre-commit-config.yaml +0 -0
- {b3dkit-0.5.0 → b3dkit-0.5.2}/.vscode/settings.json +0 -0
- {b3dkit-0.5.0 → b3dkit-0.5.2}/LICENSE +0 -0
- {b3dkit-0.5.0 → b3dkit-0.5.2}/README.md +0 -0
- {b3dkit-0.5.0 → b3dkit-0.5.2}/docs/CONTRIBUTING.md +0 -0
- {b3dkit-0.5.0 → b3dkit-0.5.2}/docs/antichamfer.md +0 -0
- {b3dkit-0.5.0 → b3dkit-0.5.2}/docs/ball_socket.md +0 -0
- {b3dkit-0.5.0 → b3dkit-0.5.2}/docs/basic_shapes.md +0 -0
- {b3dkit-0.5.0 → b3dkit-0.5.2}/docs/bolt_fittings.md +0 -0
- {b3dkit-0.5.0 → b3dkit-0.5.2}/docs/click_fit.md +0 -0
- {b3dkit-0.5.0 → b3dkit-0.5.2}/docs/dovetail.md +0 -0
- {b3dkit-0.5.0 → b3dkit-0.5.2}/docs/dovetail.png +0 -0
- {b3dkit-0.5.0 → b3dkit-0.5.2}/docs/hexwall.md +0 -0
- {b3dkit-0.5.0 → b3dkit-0.5.2}/docs/high_top_slide_box.md +0 -0
- {b3dkit-0.5.0 → b3dkit-0.5.2}/docs/index.md +0 -0
- {b3dkit-0.5.0 → b3dkit-0.5.2}/docs/point.md +0 -0
- {b3dkit-0.5.0 → b3dkit-0.5.2}/docs/reference/antichamfer.md +0 -0
- {b3dkit-0.5.0 → b3dkit-0.5.2}/docs/reference/ball_socket.md +0 -0
- {b3dkit-0.5.0 → b3dkit-0.5.2}/docs/reference/basic_shapes.md +0 -0
- {b3dkit-0.5.0 → b3dkit-0.5.2}/docs/reference/bolt_fittings.md +0 -0
- {b3dkit-0.5.0 → b3dkit-0.5.2}/docs/reference/click_fit.md +0 -0
- {b3dkit-0.5.0 → b3dkit-0.5.2}/docs/reference/dovetail.md +0 -0
- {b3dkit-0.5.0 → b3dkit-0.5.2}/docs/reference/hexwall.md +0 -0
- {b3dkit-0.5.0 → b3dkit-0.5.2}/docs/reference/high_top_slide_box.md +0 -0
- {b3dkit-0.5.0 → b3dkit-0.5.2}/docs/reference/index.md +0 -0
- {b3dkit-0.5.0 → b3dkit-0.5.2}/docs/reference/point.md +0 -0
- {b3dkit-0.5.0 → b3dkit-0.5.2}/docs/reference/slide_box.md +0 -0
- {b3dkit-0.5.0 → b3dkit-0.5.2}/docs/reference/twist_snap.md +0 -0
- {b3dkit-0.5.0 → b3dkit-0.5.2}/docs/requirements.txt +0 -0
- {b3dkit-0.5.0 → b3dkit-0.5.2}/docs/twist_snap.md +0 -0
- {b3dkit-0.5.0 → b3dkit-0.5.2}/docs/twist_snap.png +0 -0
- {b3dkit-0.5.0 → b3dkit-0.5.2}/mkdocs.yml +0 -0
- {b3dkit-0.5.0 → b3dkit-0.5.2}/readthedocs.yaml +0 -0
- {b3dkit-0.5.0 → b3dkit-0.5.2}/src/b3dkit/__init__.py +0 -0
- {b3dkit-0.5.0 → b3dkit-0.5.2}/src/b3dkit/ball_socket.py +0 -0
- {b3dkit-0.5.0 → b3dkit-0.5.2}/src/b3dkit/click_fit.py +0 -0
- {b3dkit-0.5.0 → b3dkit-0.5.2}/src/b3dkit/dovetail.py +0 -0
- {b3dkit-0.5.0 → b3dkit-0.5.2}/src/b3dkit/hexwall.py +0 -0
- {b3dkit-0.5.0 → b3dkit-0.5.2}/src/b3dkit/point.py +0 -0
- {b3dkit-0.5.0 → b3dkit-0.5.2}/src/b3dkit/py.typed +0 -0
- {b3dkit-0.5.0 → b3dkit-0.5.2}/tests/test_antichamfer.py +0 -0
- {b3dkit-0.5.0 → b3dkit-0.5.2}/tests/test_ball_socket.py +0 -0
- {b3dkit-0.5.0 → b3dkit-0.5.2}/tests/test_basic_shapes.py +0 -0
- {b3dkit-0.5.0 → b3dkit-0.5.2}/tests/test_bolt_fittings.py +0 -0
- {b3dkit-0.5.0 → b3dkit-0.5.2}/tests/test_click_fit.py +0 -0
- {b3dkit-0.5.0 → b3dkit-0.5.2}/tests/test_docs.py +0 -0
- {b3dkit-0.5.0 → b3dkit-0.5.2}/tests/test_dovetail.py +0 -0
- {b3dkit-0.5.0 → b3dkit-0.5.2}/tests/test_hexwall.py +0 -0
- {b3dkit-0.5.0 → b3dkit-0.5.2}/tests/test_point.py +0 -0
|
@@ -7,6 +7,35 @@ 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
|
+
|
|
10
39
|
## [0.5.0] - 2026-09-09
|
|
11
40
|
|
|
12
41
|
### Changed
|
|
@@ -146,7 +175,9 @@ the Python 3.11 floor.
|
|
|
146
175
|
See the git history. b3dkit began as `fb-library`; the rename accompanied a
|
|
147
176
|
rewrite that reworked names and usage to feel closer to native build123d.
|
|
148
177
|
|
|
149
|
-
[Unreleased]: https://github.com/x0pherl/b3dkit/compare/v0.5.
|
|
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
|
|
150
181
|
[0.5.0]: https://github.com/x0pherl/b3dkit/releases/tag/v0.5.0
|
|
151
182
|
[0.4.0]: https://github.com/x0pherl/b3dkit/releases/tag/v0.4.0
|
|
152
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.5.
|
|
3
|
+
version = "0.5.2"
|
|
4
4
|
authors = [
|
|
5
5
|
{ name="x0pherl"},
|
|
6
6
|
]
|
|
@@ -72,6 +72,8 @@ filterwarnings = [
|
|
|
72
72
|
"ignore::DeprecationWarning:pyparsing.*",
|
|
73
73
|
"default::DeprecationWarning:build123d.*",
|
|
74
74
|
"default::PendingDeprecationWarning:build123d.*",
|
|
75
|
+
# build123d 0.13 renamed add() to insert(); 0.11 has no insert().
|
|
76
|
+
"ignore:add\\(\\) is deprecated:DeprecationWarning",
|
|
75
77
|
]
|
|
76
78
|
|
|
77
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(
|
|
@@ -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]
|
|
@@ -90,6 +90,7 @@ class TestHighTopSlideBox:
|
|
|
90
90
|
assert isinstance(lid, Part)
|
|
91
91
|
assert_single_solid(lid, "lid")
|
|
92
92
|
assert lid.label == "box top"
|
|
93
|
+
assert lid.volume == pytest.approx(1664.0654, rel=1e-4)
|
|
93
94
|
|
|
94
95
|
def test_high_top_slide_box_lid_with_params(self, small_base_part):
|
|
95
96
|
"""Test high_top_slide_box_lid with various parameters."""
|
|
@@ -107,7 +107,7 @@ class TestPartObjectContract:
|
|
|
107
107
|
against a shape it is given, so rotation and align cannot mean anything
|
|
108
108
|
for it -- both were measurably inert when it still offered them.
|
|
109
109
|
"""
|
|
110
|
-
params = list(inspect.signature(getattr(b3dkit, name)).parameters)
|
|
110
|
+
params = list(inspect.signature(getattr(b3dkit, name).__init__).parameters)
|
|
111
111
|
if name in POSITIONED_AGAINST_INPUT:
|
|
112
112
|
assert params[-1] == "mode", f"{name} should still end with mode"
|
|
113
113
|
assert (
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
from math import cos, radians
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
from build123d import (
|
|
5
|
+
Align,
|
|
6
|
+
Axis,
|
|
7
|
+
Box,
|
|
8
|
+
BuildPart,
|
|
9
|
+
Edge,
|
|
10
|
+
Location,
|
|
11
|
+
Plane,
|
|
12
|
+
fillet,
|
|
13
|
+
section,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
from b3dkit.slide_box import _slider_template, slide_box
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class TestSlideBox:
|
|
20
|
+
def test_slide_box(self):
|
|
21
|
+
with BuildPart() as base_box:
|
|
22
|
+
Box(20, 44, 14, align=(Align.CENTER, Align.CENTER, Align.MIN))
|
|
23
|
+
fillet(base_box.part.edges().filter_by(Axis.Z), radius=1.5)
|
|
24
|
+
|
|
25
|
+
sb = slide_box(
|
|
26
|
+
base_box.part, wall_thickness=2, thumb_radius=3.5, divot_radius=0.5
|
|
27
|
+
)
|
|
28
|
+
box, lid = sb.children
|
|
29
|
+
assert (box.label, lid.label) == ("box", "lid")
|
|
30
|
+
assert len(box.solids()) == 1
|
|
31
|
+
assert len(lid.solids()) == 1
|
|
32
|
+
assert box.volume == pytest.approx(4604.7926, rel=1e-4)
|
|
33
|
+
assert lid.volume == pytest.approx(1344.8752, rel=1e-4)
|
|
34
|
+
# the lid slides inside the box, so it must be narrower
|
|
35
|
+
assert lid.bounding_box().size.X < box.bounding_box().size.X
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class TestSliderDivots:
|
|
39
|
+
"""
|
|
40
|
+
The divots sit against the tapered sliding faces of the template. Placing one
|
|
41
|
+
so that it merely grazes a tapered face leaves needle-thin slivers behind the
|
|
42
|
+
boolean, which show up as a nick in the rail the lid slides through.
|
|
43
|
+
|
|
44
|
+
A pair of divots must therefore contribute exactly two solids, and each must
|
|
45
|
+
be a real divot rather than a sliver -- the two failure modes are a doubled
|
|
46
|
+
solid count and a solid orders of magnitude too small.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
@pytest.fixture(scope="class")
|
|
50
|
+
@classmethod
|
|
51
|
+
def sketch(cls):
|
|
52
|
+
with BuildPart() as blank:
|
|
53
|
+
Box(60, 100, 20, align=(Align.CENTER, Align.CENTER, Align.MIN))
|
|
54
|
+
fillet(blank.part.edges().filter_by(Axis.Z), radius=1.5)
|
|
55
|
+
return section(obj=blank.part, section_by=Plane.XY.offset(20))
|
|
56
|
+
|
|
57
|
+
@staticmethod
|
|
58
|
+
def _divot_solids(sketch, wall, radius, tolerance, cut_template=True):
|
|
59
|
+
"""the geometry the divots add to an otherwise identical template"""
|
|
60
|
+
with_divots = _slider_template(
|
|
61
|
+
sketch,
|
|
62
|
+
wall,
|
|
63
|
+
tolerance=tolerance,
|
|
64
|
+
divot_radius=radius,
|
|
65
|
+
cut_template=cut_template,
|
|
66
|
+
)
|
|
67
|
+
without_divots = _slider_template(
|
|
68
|
+
sketch, wall, tolerance=tolerance, divot_radius=0, cut_template=cut_template
|
|
69
|
+
)
|
|
70
|
+
return (with_divots - without_divots).solids()
|
|
71
|
+
|
|
72
|
+
@pytest.mark.parametrize("cut_template", [True, False])
|
|
73
|
+
@pytest.mark.parametrize("wall", [2.0, 3.0, 4.0])
|
|
74
|
+
@pytest.mark.parametrize("radius", [0.5, 0.75, 0.9, 1.0, 1.25])
|
|
75
|
+
def test_divots_contribute_exactly_two_solids(
|
|
76
|
+
self, sketch, wall, radius, cut_template
|
|
77
|
+
):
|
|
78
|
+
# the sweep matters -- the tangency only bites on some wall/radius
|
|
79
|
+
# combinations, so any single pairing passes on its own
|
|
80
|
+
solids = self._divot_solids(
|
|
81
|
+
sketch, wall, radius, tolerance=0.2, cut_template=cut_template
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
assert len(solids) == 2
|
|
85
|
+
assert min(s.volume for s in solids) > radius**3 * 1e-2
|
|
86
|
+
|
|
87
|
+
def test_wide_divot_clears_the_open_front(self, sketch):
|
|
88
|
+
# a divot wider than half the wall used to overhang the front face of
|
|
89
|
+
# the template, slivering against it the same way the taper did
|
|
90
|
+
solids = self._divot_solids(sketch, wall=1.5, radius=1.25, tolerance=0.3)
|
|
91
|
+
|
|
92
|
+
assert len(solids) == 2
|
|
93
|
+
assert min(s.volume for s in solids) > 1.25**3 * 1e-2
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class TestSlideBoxFit:
|
|
97
|
+
"""
|
|
98
|
+
The tolerance is the total gap between lid and box on every mating face:
|
|
99
|
+
the slot grows by half of it and the lid shrinks by half of it. The lid
|
|
100
|
+
top stays flush with the box top.
|
|
101
|
+
"""
|
|
102
|
+
|
|
103
|
+
WALL = 2
|
|
104
|
+
TOP = 14
|
|
105
|
+
|
|
106
|
+
@pytest.fixture(scope="class")
|
|
107
|
+
@classmethod
|
|
108
|
+
def base(cls):
|
|
109
|
+
with BuildPart() as base_box:
|
|
110
|
+
Box(20, 44, cls.TOP, align=(Align.CENTER, Align.CENTER, Align.MIN))
|
|
111
|
+
fillet(base_box.part.edges().filter_by(Axis.Z), radius=1.5)
|
|
112
|
+
return base_box.part
|
|
113
|
+
|
|
114
|
+
def _fit(self, base, tolerance, divot_radius=0):
|
|
115
|
+
"""the box and the lid flipped back into place on it"""
|
|
116
|
+
box, lid = slide_box(
|
|
117
|
+
base,
|
|
118
|
+
wall_thickness=self.WALL,
|
|
119
|
+
thumb_radius=0,
|
|
120
|
+
tolerance=tolerance,
|
|
121
|
+
divot_radius=divot_radius,
|
|
122
|
+
).children
|
|
123
|
+
return box, lid.moved(Location((0, 0, 0), (0, 180, 0)))
|
|
124
|
+
|
|
125
|
+
@staticmethod
|
|
126
|
+
def _along_x(part, z):
|
|
127
|
+
"""the edges where a line along +x at y=0 passes through the part"""
|
|
128
|
+
return (Edge.make_line((0, 0, z), (20, 0, z)) & part).edges()
|
|
129
|
+
|
|
130
|
+
@pytest.mark.parametrize("tolerance", [0.1, 0.15, 0.3])
|
|
131
|
+
def test_lid_thickness(self, base, tolerance):
|
|
132
|
+
_, lid = self._fit(base, tolerance)
|
|
133
|
+
|
|
134
|
+
assert lid.bounding_box().max.Z == pytest.approx(self.TOP, abs=1e-4)
|
|
135
|
+
assert lid.bounding_box().size.Z == pytest.approx(
|
|
136
|
+
self.WALL - tolerance / 2, abs=1e-4
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
@pytest.mark.parametrize("tolerance", [0.1, 0.15, 0.3])
|
|
140
|
+
def test_slot_depth(self, base, tolerance):
|
|
141
|
+
box, _ = self._fit(base, tolerance)
|
|
142
|
+
floor = self.TOP - self.WALL - tolerance / 2
|
|
143
|
+
|
|
144
|
+
def first_material(z):
|
|
145
|
+
return min(e.bounding_box().min.X for e in self._along_x(box, z))
|
|
146
|
+
|
|
147
|
+
# the slot floor is a ledge: just above it the opening reaches out to
|
|
148
|
+
# the rail, just below it only to the inner wall of the box
|
|
149
|
+
cavity_wall = 10 - self.WALL - tolerance
|
|
150
|
+
assert first_material(floor - 0.01) == pytest.approx(cavity_wall, abs=1e-4)
|
|
151
|
+
assert first_material(floor + 0.01) > cavity_wall + 0.5
|
|
152
|
+
|
|
153
|
+
@pytest.mark.parametrize("tolerance", [0.1, 0.15, 0.3])
|
|
154
|
+
def test_tapered_face_gap(self, base, tolerance):
|
|
155
|
+
box, lid = self._fit(base, tolerance)
|
|
156
|
+
z = self.TOP - 0.5
|
|
157
|
+
|
|
158
|
+
slot_face = min(e.bounding_box().min.X for e in self._along_x(box, z))
|
|
159
|
+
lid_face = max(e.bounding_box().max.X for e in self._along_x(lid, z))
|
|
160
|
+
# measured horizontally, a normal gap of `tolerance` on a face leaning
|
|
161
|
+
# 22.5 degrees
|
|
162
|
+
assert slot_face - lid_face == pytest.approx(
|
|
163
|
+
tolerance / cos(radians(22.5)), abs=1e-4
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
@pytest.mark.parametrize("divot_radius", [0, 0.5])
|
|
167
|
+
@pytest.mark.parametrize("tolerance", [0.1, 0.15, 0.3])
|
|
168
|
+
def test_no_interference(self, base, tolerance, divot_radius):
|
|
169
|
+
box, lid = self._fit(base, tolerance, divot_radius)
|
|
170
|
+
|
|
171
|
+
overlap = box & lid
|
|
172
|
+
assert sum(s.volume for s in overlap.solids()) == pytest.approx(0, abs=1e-6)
|
|
173
|
+
|
|
174
|
+
@pytest.mark.parametrize("tolerance", [0.1, 0.15, 0.3])
|
|
175
|
+
def test_divots_align(self, base, tolerance):
|
|
176
|
+
plain_box, plain_lid = self._fit(base, tolerance)
|
|
177
|
+
box, lid = self._fit(base, tolerance, divot_radius=0.5)
|
|
178
|
+
|
|
179
|
+
detents = sorted(
|
|
180
|
+
(s.center().X, s.center().Y) for s in (plain_box - box).solids()
|
|
181
|
+
)
|
|
182
|
+
bumps = sorted((s.center().X, s.center().Y) for s in (lid - plain_lid).solids())
|
|
183
|
+
assert len(detents) == len(bumps) == 2
|
|
184
|
+
for detent, bump in zip(detents, bumps, strict=True):
|
|
185
|
+
assert detent == pytest.approx(bump, abs=1e-4)
|
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
import pytest
|
|
2
|
-
from build123d import Align, Axis, Box, BuildPart, Plane, fillet, section
|
|
3
|
-
|
|
4
|
-
from b3dkit.slide_box import _slider_template, slide_box
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
class TestSlideBox:
|
|
8
|
-
def test_slide_box(self):
|
|
9
|
-
with BuildPart() as base_box:
|
|
10
|
-
Box(20, 44, 14, align=(Align.CENTER, Align.CENTER, Align.MIN))
|
|
11
|
-
fillet(base_box.part.edges().filter_by(Axis.Z), radius=1.5)
|
|
12
|
-
|
|
13
|
-
sb = slide_box(
|
|
14
|
-
base_box.part, wall_thickness=2, thumb_radius=3.5, divot_radius=0.5
|
|
15
|
-
)
|
|
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
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
class TestSliderDivots:
|
|
27
|
-
"""
|
|
28
|
-
The divots sit against the tapered sliding faces of the template. Placing one
|
|
29
|
-
so that it merely grazes a tapered face leaves needle-thin slivers behind the
|
|
30
|
-
boolean, which show up as a nick in the rail the lid slides through.
|
|
31
|
-
|
|
32
|
-
A pair of divots must therefore contribute exactly two solids, and each must
|
|
33
|
-
be a real divot rather than a sliver -- the two failure modes are a doubled
|
|
34
|
-
solid count and a solid orders of magnitude too small.
|
|
35
|
-
"""
|
|
36
|
-
|
|
37
|
-
@pytest.fixture(scope="class")
|
|
38
|
-
@classmethod
|
|
39
|
-
def sketch(cls):
|
|
40
|
-
with BuildPart() as blank:
|
|
41
|
-
Box(60, 100, 20, align=(Align.CENTER, Align.CENTER, Align.MIN))
|
|
42
|
-
fillet(blank.part.edges().filter_by(Axis.Z), radius=1.5)
|
|
43
|
-
return section(obj=blank.part, section_by=Plane.XY.offset(20))
|
|
44
|
-
|
|
45
|
-
@staticmethod
|
|
46
|
-
def _divot_solids(sketch, wall, radius, tolerance):
|
|
47
|
-
"""the geometry the divots add to an otherwise identical template"""
|
|
48
|
-
with_divots = _slider_template(
|
|
49
|
-
sketch, wall, tolerance=tolerance, divot_radius=radius, cut_template=True
|
|
50
|
-
)
|
|
51
|
-
without_divots = _slider_template(
|
|
52
|
-
sketch, wall, tolerance=tolerance, divot_radius=0, cut_template=True
|
|
53
|
-
)
|
|
54
|
-
return (with_divots - without_divots).solids()
|
|
55
|
-
|
|
56
|
-
@pytest.mark.parametrize("wall", [2.0, 3.0, 4.0])
|
|
57
|
-
@pytest.mark.parametrize("radius", [0.5, 0.75, 0.9, 1.0, 1.25])
|
|
58
|
-
def test_divots_contribute_exactly_two_solids(self, sketch, wall, radius):
|
|
59
|
-
# the sweep matters -- the tangency only bites on some wall/radius
|
|
60
|
-
# combinations, so any single pairing passes on its own
|
|
61
|
-
solids = self._divot_solids(sketch, wall, radius, tolerance=0.2)
|
|
62
|
-
|
|
63
|
-
assert len(solids) == 2
|
|
64
|
-
assert min(s.volume for s in solids) > radius**3 * 1e-2
|
|
65
|
-
|
|
66
|
-
def test_wide_divot_clears_the_open_front(self, sketch):
|
|
67
|
-
# a divot wider than half the wall used to overhang the front face of
|
|
68
|
-
# the template, slivering against it the same way the taper did
|
|
69
|
-
solids = self._divot_solids(sketch, wall=1.5, radius=1.25, tolerance=0.3)
|
|
70
|
-
|
|
71
|
-
assert len(solids) == 2
|
|
72
|
-
assert min(s.volume for s in solids) > 1.25**3 * 1e-2
|
|
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
|
|
File without changes
|