b3dkit 0.5.0__tar.gz → 0.5.3__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.3}/CHANGELOG.md +42 -1
- {b3dkit-0.5.0 → b3dkit-0.5.3}/CONTRIBUTING.md +5 -0
- {b3dkit-0.5.0 → b3dkit-0.5.3}/PKG-INFO +1 -1
- {b3dkit-0.5.0 → b3dkit-0.5.3}/docs/slide_box.md +3 -1
- {b3dkit-0.5.0 → b3dkit-0.5.3}/pyproject.toml +3 -1
- {b3dkit-0.5.0 → b3dkit-0.5.3}/src/b3dkit/antichamfer.py +10 -10
- {b3dkit-0.5.0 → b3dkit-0.5.3}/src/b3dkit/basic_shapes.py +1 -1
- {b3dkit-0.5.0 → b3dkit-0.5.3}/src/b3dkit/bolt_fittings.py +2 -2
- {b3dkit-0.5.0 → b3dkit-0.5.3}/src/b3dkit/high_top_slide_box.py +28 -23
- {b3dkit-0.5.0 → b3dkit-0.5.3}/src/b3dkit/slide_box.py +60 -30
- {b3dkit-0.5.0 → b3dkit-0.5.3}/src/b3dkit/twist_snap.py +4 -1
- {b3dkit-0.5.0 → b3dkit-0.5.3}/tests/test_high_top_slide_box.py +1 -0
- {b3dkit-0.5.0 → b3dkit-0.5.3}/tests/test_public_api.py +1 -1
- b3dkit-0.5.3/tests/test_slide_box.py +185 -0
- {b3dkit-0.5.0 → b3dkit-0.5.3}/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.3}/.coveragerc +0 -0
- {b3dkit-0.5.0 → b3dkit-0.5.3}/.github/workflows/release.yml +0 -0
- {b3dkit-0.5.0 → b3dkit-0.5.3}/.github/workflows/test.yml +0 -0
- {b3dkit-0.5.0 → b3dkit-0.5.3}/.gitignore +0 -0
- {b3dkit-0.5.0 → b3dkit-0.5.3}/.pre-commit-config.yaml +0 -0
- {b3dkit-0.5.0 → b3dkit-0.5.3}/.vscode/settings.json +0 -0
- {b3dkit-0.5.0 → b3dkit-0.5.3}/LICENSE +0 -0
- {b3dkit-0.5.0 → b3dkit-0.5.3}/README.md +0 -0
- {b3dkit-0.5.0 → b3dkit-0.5.3}/docs/CONTRIBUTING.md +0 -0
- {b3dkit-0.5.0 → b3dkit-0.5.3}/docs/antichamfer.md +0 -0
- {b3dkit-0.5.0 → b3dkit-0.5.3}/docs/ball_socket.md +0 -0
- {b3dkit-0.5.0 → b3dkit-0.5.3}/docs/basic_shapes.md +0 -0
- {b3dkit-0.5.0 → b3dkit-0.5.3}/docs/bolt_fittings.md +0 -0
- {b3dkit-0.5.0 → b3dkit-0.5.3}/docs/click_fit.md +0 -0
- {b3dkit-0.5.0 → b3dkit-0.5.3}/docs/dovetail.md +0 -0
- {b3dkit-0.5.0 → b3dkit-0.5.3}/docs/dovetail.png +0 -0
- {b3dkit-0.5.0 → b3dkit-0.5.3}/docs/hexwall.md +0 -0
- {b3dkit-0.5.0 → b3dkit-0.5.3}/docs/high_top_slide_box.md +0 -0
- {b3dkit-0.5.0 → b3dkit-0.5.3}/docs/index.md +0 -0
- {b3dkit-0.5.0 → b3dkit-0.5.3}/docs/point.md +0 -0
- {b3dkit-0.5.0 → b3dkit-0.5.3}/docs/reference/antichamfer.md +0 -0
- {b3dkit-0.5.0 → b3dkit-0.5.3}/docs/reference/ball_socket.md +0 -0
- {b3dkit-0.5.0 → b3dkit-0.5.3}/docs/reference/basic_shapes.md +0 -0
- {b3dkit-0.5.0 → b3dkit-0.5.3}/docs/reference/bolt_fittings.md +0 -0
- {b3dkit-0.5.0 → b3dkit-0.5.3}/docs/reference/click_fit.md +0 -0
- {b3dkit-0.5.0 → b3dkit-0.5.3}/docs/reference/dovetail.md +0 -0
- {b3dkit-0.5.0 → b3dkit-0.5.3}/docs/reference/hexwall.md +0 -0
- {b3dkit-0.5.0 → b3dkit-0.5.3}/docs/reference/high_top_slide_box.md +0 -0
- {b3dkit-0.5.0 → b3dkit-0.5.3}/docs/reference/index.md +0 -0
- {b3dkit-0.5.0 → b3dkit-0.5.3}/docs/reference/point.md +0 -0
- {b3dkit-0.5.0 → b3dkit-0.5.3}/docs/reference/slide_box.md +0 -0
- {b3dkit-0.5.0 → b3dkit-0.5.3}/docs/reference/twist_snap.md +0 -0
- {b3dkit-0.5.0 → b3dkit-0.5.3}/docs/requirements.txt +0 -0
- {b3dkit-0.5.0 → b3dkit-0.5.3}/docs/twist_snap.md +0 -0
- {b3dkit-0.5.0 → b3dkit-0.5.3}/docs/twist_snap.png +0 -0
- {b3dkit-0.5.0 → b3dkit-0.5.3}/mkdocs.yml +0 -0
- {b3dkit-0.5.0 → b3dkit-0.5.3}/readthedocs.yaml +0 -0
- {b3dkit-0.5.0 → b3dkit-0.5.3}/src/b3dkit/__init__.py +0 -0
- {b3dkit-0.5.0 → b3dkit-0.5.3}/src/b3dkit/ball_socket.py +0 -0
- {b3dkit-0.5.0 → b3dkit-0.5.3}/src/b3dkit/click_fit.py +0 -0
- {b3dkit-0.5.0 → b3dkit-0.5.3}/src/b3dkit/dovetail.py +0 -0
- {b3dkit-0.5.0 → b3dkit-0.5.3}/src/b3dkit/hexwall.py +0 -0
- {b3dkit-0.5.0 → b3dkit-0.5.3}/src/b3dkit/point.py +0 -0
- {b3dkit-0.5.0 → b3dkit-0.5.3}/src/b3dkit/py.typed +0 -0
- {b3dkit-0.5.0 → b3dkit-0.5.3}/tests/test_antichamfer.py +0 -0
- {b3dkit-0.5.0 → b3dkit-0.5.3}/tests/test_ball_socket.py +0 -0
- {b3dkit-0.5.0 → b3dkit-0.5.3}/tests/test_basic_shapes.py +0 -0
- {b3dkit-0.5.0 → b3dkit-0.5.3}/tests/test_bolt_fittings.py +0 -0
- {b3dkit-0.5.0 → b3dkit-0.5.3}/tests/test_click_fit.py +0 -0
- {b3dkit-0.5.0 → b3dkit-0.5.3}/tests/test_docs.py +0 -0
- {b3dkit-0.5.0 → b3dkit-0.5.3}/tests/test_dovetail.py +0 -0
- {b3dkit-0.5.0 → b3dkit-0.5.3}/tests/test_hexwall.py +0 -0
- {b3dkit-0.5.0 → b3dkit-0.5.3}/tests/test_point.py +0 -0
|
@@ -7,6 +7,44 @@ ship in minor releases.
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.5.3] - 2026-10-08
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- **`slide_box` cavity walls are `wall_thickness` thick.** The cavity follows
|
|
15
|
+
the part's outline at `top_offset` below the top, so lettering or other top
|
|
16
|
+
features no longer shape it, and `top_offset` and `tolerance` no longer thin
|
|
17
|
+
the walls.
|
|
18
|
+
|
|
19
|
+
## [0.5.2] - 2026-10-03
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
|
|
23
|
+
- **`slide_box` sliding edges are filleted** by `wall_thickness * 0.025` on
|
|
24
|
+
both the slot and the lid.
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
|
|
28
|
+
- **build123d 0.12 and 0.13 support.** `TeardropBoltCutSinkhole`,
|
|
29
|
+
`BoltCutSinkhole`, `SquareNutSinkhole`, `TwistSnapSocket` and the
|
|
30
|
+
`high_top_slide_box` parts built the wrong geometry on build123d 0.12+, which
|
|
31
|
+
places geometry inside located builders differently. Every public object now
|
|
32
|
+
builds identically on 0.11, 0.12 and 0.13; 0.11 geometry is unchanged.
|
|
33
|
+
- **`Teardrop`'s `rotation` is a float**, defaulting to `0`. The old `(0, 0)`
|
|
34
|
+
default raised `TypeError` on build123d 0.12+.
|
|
35
|
+
|
|
36
|
+
## [0.5.1] - 2026-10-01
|
|
37
|
+
|
|
38
|
+
### Changed
|
|
39
|
+
|
|
40
|
+
- **`slide_box` lids no longer intersect the box.** The lid was
|
|
41
|
+
`wall_thickness + tolerance` thick in a `wall_thickness` slot. `tolerance` is
|
|
42
|
+
now the total gap on every mating face, split evenly: the slot grows by half
|
|
43
|
+
and the lid shrinks by half, so the lid is `wall_thickness - tolerance / 2`
|
|
44
|
+
thick and its top stays flush. Box, lid and `slide_lid` geometry all change.
|
|
45
|
+
- **`slide_box` divots now line up.** The box detents and lid bumps were placed
|
|
46
|
+
from different tolerances and sat up to `tolerance` apart.
|
|
47
|
+
|
|
10
48
|
## [0.5.0] - 2026-09-09
|
|
11
49
|
|
|
12
50
|
### Changed
|
|
@@ -146,7 +184,10 @@ the Python 3.11 floor.
|
|
|
146
184
|
See the git history. b3dkit began as `fb-library`; the rename accompanied a
|
|
147
185
|
rewrite that reworked names and usage to feel closer to native build123d.
|
|
148
186
|
|
|
149
|
-
[Unreleased]: https://github.com/x0pherl/b3dkit/compare/v0.5.
|
|
187
|
+
[Unreleased]: https://github.com/x0pherl/b3dkit/compare/v0.5.3...HEAD
|
|
188
|
+
[0.5.3]: https://github.com/x0pherl/b3dkit/releases/tag/v0.5.3
|
|
189
|
+
[0.5.2]: https://github.com/x0pherl/b3dkit/releases/tag/v0.5.2
|
|
190
|
+
[0.5.1]: https://github.com/x0pherl/b3dkit/releases/tag/v0.5.1
|
|
150
191
|
[0.5.0]: https://github.com/x0pherl/b3dkit/releases/tag/v0.5.0
|
|
151
192
|
[0.4.0]: https://github.com/x0pherl/b3dkit/releases/tag/v0.4.0
|
|
152
193
|
[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.3"
|
|
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 (
|
|
@@ -82,20 +93,27 @@ def _slider_template(
|
|
|
82
93
|
sketch: Sketch,
|
|
83
94
|
wall_thickness: float = 2,
|
|
84
95
|
tolerance: float = 0.2,
|
|
85
|
-
top_offset: float = 0,
|
|
86
96
|
x_straighten_distance: float = 0,
|
|
87
97
|
divot_radius: float = 0,
|
|
88
98
|
cut_template: bool = True,
|
|
89
99
|
) -> Part:
|
|
90
100
|
"""
|
|
91
101
|
Create a slider part based on a sketch.
|
|
102
|
+
|
|
103
|
+
``tolerance`` is the total gap between lid and box; the cut template grows
|
|
104
|
+
by half of it and the lid template shrinks by half of it. The top of the
|
|
105
|
+
template stays at the top of the sketch either way.
|
|
92
106
|
"""
|
|
107
|
+
clearance = abs(tolerance) / 2 * (1 if cut_template else -1)
|
|
93
108
|
with BuildPart() as slider_part:
|
|
94
109
|
with BuildSketch() as top_sketch:
|
|
95
|
-
offset(
|
|
110
|
+
offset(
|
|
111
|
+
sketch,
|
|
112
|
+
amount=-abs(wall_thickness) + _tapered_inset(clearance),
|
|
113
|
+
)
|
|
96
114
|
extrude(
|
|
97
115
|
top_sketch.sketch,
|
|
98
|
-
amount=-wall_thickness -
|
|
116
|
+
amount=-wall_thickness - clearance,
|
|
99
117
|
taper=-_SLIDER_TAPER_ANGLE,
|
|
100
118
|
)
|
|
101
119
|
cross_section = section(
|
|
@@ -111,15 +129,20 @@ def _slider_template(
|
|
|
111
129
|
cross_section, amount=x_straighten_distance * 2 + wall_thickness * 2
|
|
112
130
|
)
|
|
113
131
|
)
|
|
132
|
+
fillet(
|
|
133
|
+
slider_part.edges() - slider_part.faces().filter_by(Plane.XY).edges(),
|
|
134
|
+
wall_thickness * 0.025,
|
|
135
|
+
)
|
|
136
|
+
fillet(slider_part.faces().sort_by(Axis.Z)[0].edges(), wall_thickness * 0.025)
|
|
137
|
+
|
|
114
138
|
if divot_radius > 0:
|
|
115
139
|
# 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,
|
|
140
|
+
# the part; keep it far enough back to meet only the underside
|
|
141
|
+
divot_y = sketch.bounding_box().min.Y + max(
|
|
142
|
+
wall_thickness / 2, divot_radius + divot_radius / 5
|
|
120
143
|
)
|
|
121
144
|
with BuildPart(
|
|
122
|
-
Location((0, divot_y, -wall_thickness), (180, 0, 0)),
|
|
145
|
+
Location((0, divot_y, -wall_thickness - clearance), (180, 0, 0)),
|
|
123
146
|
mode=Mode.ADD,
|
|
124
147
|
):
|
|
125
148
|
with GridLocations(
|
|
@@ -161,7 +184,7 @@ def slide_lid(
|
|
|
161
184
|
args:
|
|
162
185
|
- part: the part defining the box's outer dimensions
|
|
163
186
|
- wall_thickness: the thickness of the box walls
|
|
164
|
-
- tolerance: the
|
|
187
|
+
- tolerance: the total gap between the lid and the base; 0.1 is a tight
|
|
165
188
|
fit, 0.15 the default, 0.2 and above loose
|
|
166
189
|
- top_offset: how far below the top of the part the sliding section sits,
|
|
167
190
|
for parts with features at the top worth preserving
|
|
@@ -179,7 +202,6 @@ def slide_lid(
|
|
|
179
202
|
cross_section,
|
|
180
203
|
wall_thickness,
|
|
181
204
|
tolerance=tolerance,
|
|
182
|
-
top_offset=top_offset,
|
|
183
205
|
x_straighten_distance=x_straighten_distance,
|
|
184
206
|
divot_radius=divot_radius,
|
|
185
207
|
cut_template=False,
|
|
@@ -247,36 +269,35 @@ def slide_box(
|
|
|
247
269
|
- thumb_radius: the radius of the thumb-grip cutout; 0 disables it
|
|
248
270
|
- x_straighten_distance: how much of the slider stays straight at the
|
|
249
271
|
edges, which stops wide lids binding at the corners
|
|
250
|
-
- tolerance: the
|
|
272
|
+
- tolerance: the total gap between the lid and the base, split evenly
|
|
273
|
+
between the slot and the lid
|
|
251
274
|
- divot_radius: the radius of the click-fit positioning divots; 0
|
|
252
275
|
disables them
|
|
253
276
|
|
|
254
277
|
returns a Compound labelled "slide box" with two children, "box" and "lid"
|
|
255
278
|
"""
|
|
256
279
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
)
|
|
280
|
+
top = part.bounding_box().max.Z
|
|
281
|
+
cross_section = section(obj=part, section_by=Plane.XY.offset(top - top_offset))
|
|
260
282
|
lid_cut_template = _slider_template(
|
|
261
283
|
cross_section,
|
|
262
284
|
wall_thickness,
|
|
263
|
-
tolerance=
|
|
264
|
-
top_offset=top_offset,
|
|
285
|
+
tolerance=tolerance,
|
|
265
286
|
x_straighten_distance=x_straighten_distance,
|
|
266
287
|
divot_radius=divot_radius,
|
|
267
288
|
)
|
|
268
289
|
|
|
269
|
-
extrusion_height =
|
|
290
|
+
extrusion_height = top - wall_thickness
|
|
270
291
|
with BuildPart() as box_part:
|
|
271
292
|
add(part)
|
|
272
|
-
|
|
293
|
+
# the cavity follows the outline below any top features, raised to the
|
|
294
|
+
# top of the part
|
|
295
|
+
with BuildSketch(Plane.XY.offset(top)):
|
|
273
296
|
offset(
|
|
274
|
-
|
|
275
|
-
amount=-abs(
|
|
276
|
-
)
|
|
277
|
-
|
|
278
|
-
mode=Mode.SUBTRACT,
|
|
279
|
-
)
|
|
297
|
+
cross_section.moved(Location((0, 0, -(top - top_offset)))),
|
|
298
|
+
amount=-abs(wall_thickness),
|
|
299
|
+
)
|
|
300
|
+
extrude(amount=-extrusion_height, mode=Mode.SUBTRACT)
|
|
280
301
|
add(
|
|
281
302
|
lid_cut_template.move(Location((0, 0, extrusion_height + wall_thickness))),
|
|
282
303
|
mode=Mode.SUBTRACT,
|
|
@@ -303,11 +324,20 @@ def slide_box(
|
|
|
303
324
|
|
|
304
325
|
|
|
305
326
|
if __name__ == "__main__":
|
|
306
|
-
from ocp_vscode import
|
|
327
|
+
from ocp_vscode import show
|
|
307
328
|
|
|
308
329
|
with BuildPart() as base_box:
|
|
309
330
|
Box(20, 44, 14, align=(Align.CENTER, Align.CENTER, Align.MIN))
|
|
310
331
|
fillet(base_box.part.edges().filter_by(Axis.Z), radius=1.5)
|
|
311
332
|
|
|
312
|
-
sb = slide_box(
|
|
313
|
-
|
|
333
|
+
sb = slide_box(
|
|
334
|
+
base_box.part,
|
|
335
|
+
wall_thickness=2,
|
|
336
|
+
thumb_radius=3.5,
|
|
337
|
+
divot_radius=0.5,
|
|
338
|
+
tolerance=0.3,
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
show(
|
|
342
|
+
sb,
|
|
343
|
+
)
|
|
@@ -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(4439.0809, 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
|
|
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
|