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.
Files changed (69) hide show
  1. {b3dkit-0.5.0 → b3dkit-0.5.2}/CHANGELOG.md +32 -1
  2. {b3dkit-0.5.0 → b3dkit-0.5.2}/CONTRIBUTING.md +5 -0
  3. {b3dkit-0.5.0 → b3dkit-0.5.2}/PKG-INFO +1 -1
  4. {b3dkit-0.5.0 → b3dkit-0.5.2}/docs/slide_box.md +3 -1
  5. {b3dkit-0.5.0 → b3dkit-0.5.2}/pyproject.toml +3 -1
  6. {b3dkit-0.5.0 → b3dkit-0.5.2}/src/b3dkit/antichamfer.py +10 -10
  7. {b3dkit-0.5.0 → b3dkit-0.5.2}/src/b3dkit/basic_shapes.py +1 -1
  8. {b3dkit-0.5.0 → b3dkit-0.5.2}/src/b3dkit/bolt_fittings.py +2 -2
  9. {b3dkit-0.5.0 → b3dkit-0.5.2}/src/b3dkit/high_top_slide_box.py +28 -23
  10. {b3dkit-0.5.0 → b3dkit-0.5.2}/src/b3dkit/slide_box.py +50 -16
  11. {b3dkit-0.5.0 → b3dkit-0.5.2}/src/b3dkit/twist_snap.py +4 -1
  12. {b3dkit-0.5.0 → b3dkit-0.5.2}/tests/test_high_top_slide_box.py +1 -0
  13. {b3dkit-0.5.0 → b3dkit-0.5.2}/tests/test_public_api.py +1 -1
  14. b3dkit-0.5.2/tests/test_slide_box.py +185 -0
  15. {b3dkit-0.5.0 → b3dkit-0.5.2}/tests/test_twist_snap.py +1 -1
  16. b3dkit-0.5.0/tests/test_slide_box.py +0 -72
  17. {b3dkit-0.5.0 → b3dkit-0.5.2}/.coveragerc +0 -0
  18. {b3dkit-0.5.0 → b3dkit-0.5.2}/.github/workflows/release.yml +0 -0
  19. {b3dkit-0.5.0 → b3dkit-0.5.2}/.github/workflows/test.yml +0 -0
  20. {b3dkit-0.5.0 → b3dkit-0.5.2}/.gitignore +0 -0
  21. {b3dkit-0.5.0 → b3dkit-0.5.2}/.pre-commit-config.yaml +0 -0
  22. {b3dkit-0.5.0 → b3dkit-0.5.2}/.vscode/settings.json +0 -0
  23. {b3dkit-0.5.0 → b3dkit-0.5.2}/LICENSE +0 -0
  24. {b3dkit-0.5.0 → b3dkit-0.5.2}/README.md +0 -0
  25. {b3dkit-0.5.0 → b3dkit-0.5.2}/docs/CONTRIBUTING.md +0 -0
  26. {b3dkit-0.5.0 → b3dkit-0.5.2}/docs/antichamfer.md +0 -0
  27. {b3dkit-0.5.0 → b3dkit-0.5.2}/docs/ball_socket.md +0 -0
  28. {b3dkit-0.5.0 → b3dkit-0.5.2}/docs/basic_shapes.md +0 -0
  29. {b3dkit-0.5.0 → b3dkit-0.5.2}/docs/bolt_fittings.md +0 -0
  30. {b3dkit-0.5.0 → b3dkit-0.5.2}/docs/click_fit.md +0 -0
  31. {b3dkit-0.5.0 → b3dkit-0.5.2}/docs/dovetail.md +0 -0
  32. {b3dkit-0.5.0 → b3dkit-0.5.2}/docs/dovetail.png +0 -0
  33. {b3dkit-0.5.0 → b3dkit-0.5.2}/docs/hexwall.md +0 -0
  34. {b3dkit-0.5.0 → b3dkit-0.5.2}/docs/high_top_slide_box.md +0 -0
  35. {b3dkit-0.5.0 → b3dkit-0.5.2}/docs/index.md +0 -0
  36. {b3dkit-0.5.0 → b3dkit-0.5.2}/docs/point.md +0 -0
  37. {b3dkit-0.5.0 → b3dkit-0.5.2}/docs/reference/antichamfer.md +0 -0
  38. {b3dkit-0.5.0 → b3dkit-0.5.2}/docs/reference/ball_socket.md +0 -0
  39. {b3dkit-0.5.0 → b3dkit-0.5.2}/docs/reference/basic_shapes.md +0 -0
  40. {b3dkit-0.5.0 → b3dkit-0.5.2}/docs/reference/bolt_fittings.md +0 -0
  41. {b3dkit-0.5.0 → b3dkit-0.5.2}/docs/reference/click_fit.md +0 -0
  42. {b3dkit-0.5.0 → b3dkit-0.5.2}/docs/reference/dovetail.md +0 -0
  43. {b3dkit-0.5.0 → b3dkit-0.5.2}/docs/reference/hexwall.md +0 -0
  44. {b3dkit-0.5.0 → b3dkit-0.5.2}/docs/reference/high_top_slide_box.md +0 -0
  45. {b3dkit-0.5.0 → b3dkit-0.5.2}/docs/reference/index.md +0 -0
  46. {b3dkit-0.5.0 → b3dkit-0.5.2}/docs/reference/point.md +0 -0
  47. {b3dkit-0.5.0 → b3dkit-0.5.2}/docs/reference/slide_box.md +0 -0
  48. {b3dkit-0.5.0 → b3dkit-0.5.2}/docs/reference/twist_snap.md +0 -0
  49. {b3dkit-0.5.0 → b3dkit-0.5.2}/docs/requirements.txt +0 -0
  50. {b3dkit-0.5.0 → b3dkit-0.5.2}/docs/twist_snap.md +0 -0
  51. {b3dkit-0.5.0 → b3dkit-0.5.2}/docs/twist_snap.png +0 -0
  52. {b3dkit-0.5.0 → b3dkit-0.5.2}/mkdocs.yml +0 -0
  53. {b3dkit-0.5.0 → b3dkit-0.5.2}/readthedocs.yaml +0 -0
  54. {b3dkit-0.5.0 → b3dkit-0.5.2}/src/b3dkit/__init__.py +0 -0
  55. {b3dkit-0.5.0 → b3dkit-0.5.2}/src/b3dkit/ball_socket.py +0 -0
  56. {b3dkit-0.5.0 → b3dkit-0.5.2}/src/b3dkit/click_fit.py +0 -0
  57. {b3dkit-0.5.0 → b3dkit-0.5.2}/src/b3dkit/dovetail.py +0 -0
  58. {b3dkit-0.5.0 → b3dkit-0.5.2}/src/b3dkit/hexwall.py +0 -0
  59. {b3dkit-0.5.0 → b3dkit-0.5.2}/src/b3dkit/point.py +0 -0
  60. {b3dkit-0.5.0 → b3dkit-0.5.2}/src/b3dkit/py.typed +0 -0
  61. {b3dkit-0.5.0 → b3dkit-0.5.2}/tests/test_antichamfer.py +0 -0
  62. {b3dkit-0.5.0 → b3dkit-0.5.2}/tests/test_ball_socket.py +0 -0
  63. {b3dkit-0.5.0 → b3dkit-0.5.2}/tests/test_basic_shapes.py +0 -0
  64. {b3dkit-0.5.0 → b3dkit-0.5.2}/tests/test_bolt_fittings.py +0 -0
  65. {b3dkit-0.5.0 → b3dkit-0.5.2}/tests/test_click_fit.py +0 -0
  66. {b3dkit-0.5.0 → b3dkit-0.5.2}/tests/test_docs.py +0 -0
  67. {b3dkit-0.5.0 → b3dkit-0.5.2}/tests/test_dovetail.py +0 -0
  68. {b3dkit-0.5.0 → b3dkit-0.5.2}/tests/test_hexwall.py +0 -0
  69. {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.0...HEAD
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
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: b3dkit
3
- Version: 0.5.0
3
+ Version: 0.5.2
4
4
  Summary: build123d libraries and utilities
5
5
  Project-URL: Homepage, https://github.com/x0pherl/b3dkit
6
6
  Project-URL: Issues, https://github.com/x0pherl/b3dkit/issues
@@ -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 controls the fit between the lid and base:
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.0"
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
 
@@ -313,7 +313,7 @@ class Teardrop(BaseSketchObject):
313
313
  self,
314
314
  radius: float,
315
315
  peak_distance: float,
316
- rotation: RotationLike = (0, 0),
316
+ rotation: float = 0,
317
317
  align: None | Align | tuple[Align, Align] = None,
318
318
  mode: Mode = Mode.ADD,
319
319
  ):
@@ -92,7 +92,7 @@ class TeardropBoltCutSinkhole(BasePartObject):
92
92
  amount=extension_distance,
93
93
  )
94
94
  anti_chamfer(
95
- sinkhole.part.faces().sort_by(Axis.Z)[-1],
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.part.faces().sort_by(Axis.Z)[-1], amount=nut_depth)
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
- with BuildPart(
87
- Location((0, wall_thickness / 2, 0)),
88
- mode=Mode.SUBTRACT,
89
- ) as rail_cut:
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
- with BuildPart(mode=Mode.INTERSECT):
228
- Box(
229
- part_max_dimension,
230
- part_max_dimension,
231
- part_max_dimension,
232
- align=(Align.CENTER, Align.CENTER, Align.MIN),
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
- - abs(tolerance)
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(sketch, amount=-abs(tolerance) - (abs(wall_thickness)))
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 - abs(tolerance),
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 template; keep it far enough back to meet only the underside
117
- divot_y = max(
118
- sketch.bounding_box().min.Y + wall_thickness / 2,
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 clearance between the lid and the base; 0.1 is a tight
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 clearance between the lid and the base
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=0,
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 Camera, show
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(base_box.part, wall_thickness=2, thumb_radius=3.5, divot_radius=0.5)
313
- show(sb, reset_camera=Camera.KEEP)
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)
@@ -31,7 +31,7 @@ class TestTwistSnap:
31
31
  """
32
32
  import inspect
33
33
 
34
- params = inspect.signature(TwistSnapConnector).parameters
34
+ params = inspect.signature(TwistSnapConnector.__init__).parameters
35
35
  assert "tolerance" not in params
36
36
  assert "wall_width" not in params
37
37
 
@@ -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