b3dkit 0.1.2__tar.gz → 0.1.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.1.2 → b3dkit-0.1.3}/.coverage +0 -0
- {b3dkit-0.1.2 → b3dkit-0.1.3}/PKG-INFO +13 -13
- {b3dkit-0.1.2 → b3dkit-0.1.3}/docs/basic_shapes.md +28 -14
- b3dkit-0.1.3/docs/hexwall.md +128 -0
- {b3dkit-0.1.2 → b3dkit-0.1.3}/pyproject.toml +14 -14
- {b3dkit-0.1.2 → b3dkit-0.1.3}/src/b3dkit/basic_shapes.py +27 -19
- b3dkit-0.1.3/src/b3dkit/hexwall.py +230 -0
- {b3dkit-0.1.2 → b3dkit-0.1.3}/tests/test_basic_shapes.py +159 -108
- {b3dkit-0.1.2 → b3dkit-0.1.3}/tests/test_dovetail.py +0 -130
- b3dkit-0.1.3/tests/test_hexwall.py +96 -0
- {b3dkit-0.1.2 → b3dkit-0.1.3}/tests/test_high_top_slide_box.py +12 -1
- b3dkit-0.1.2/docs/hexwall.md +0 -50
- b3dkit-0.1.2/mkdocs_new.yml +0 -29
- b3dkit-0.1.2/src/b3dkit/hexwall.py +0 -102
- b3dkit-0.1.2/tests/test_hexwall.py +0 -30
- {b3dkit-0.1.2 → b3dkit-0.1.3}/.coveragerc +0 -0
- {b3dkit-0.1.2 → b3dkit-0.1.3}/.gitignore +0 -0
- {b3dkit-0.1.2 → b3dkit-0.1.3}/.vscode/settings.json +0 -0
- {b3dkit-0.1.2 → b3dkit-0.1.3}/LICENSE +0 -0
- {b3dkit-0.1.2 → b3dkit-0.1.3}/README.md +0 -0
- {b3dkit-0.1.2 → b3dkit-0.1.3}/build.bat +0 -0
- {b3dkit-0.1.2 → b3dkit-0.1.3}/build.sh +0 -0
- {b3dkit-0.1.2 → b3dkit-0.1.3}/docs/Makefile +0 -0
- {b3dkit-0.1.2 → b3dkit-0.1.3}/docs/antichamfer.md +0 -0
- {b3dkit-0.1.2 → b3dkit-0.1.3}/docs/ball_socket.md +0 -0
- {b3dkit-0.1.2 → b3dkit-0.1.3}/docs/bolt_fittings.md +0 -0
- {b3dkit-0.1.2 → b3dkit-0.1.3}/docs/click_fit.md +0 -0
- {b3dkit-0.1.2 → b3dkit-0.1.3}/docs/conf.py +0 -0
- {b3dkit-0.1.2 → b3dkit-0.1.3}/docs/dovetail.md +0 -0
- {b3dkit-0.1.2 → b3dkit-0.1.3}/docs/dovetail.png +0 -0
- {b3dkit-0.1.2 → b3dkit-0.1.3}/docs/high_top_slide_box.md +0 -0
- {b3dkit-0.1.2 → b3dkit-0.1.3}/docs/index.md +0 -0
- {b3dkit-0.1.2 → b3dkit-0.1.3}/docs/point.md +0 -0
- {b3dkit-0.1.2 → b3dkit-0.1.3}/docs/slide_box.md +0 -0
- {b3dkit-0.1.2 → b3dkit-0.1.3}/docs/twist_snap.md +0 -0
- {b3dkit-0.1.2 → b3dkit-0.1.3}/docs/twist_snap.png +0 -0
- {b3dkit-0.1.2 → b3dkit-0.1.3}/mkdocs.yml +0 -0
- {b3dkit-0.1.2 → b3dkit-0.1.3}/readthedocs.yaml +0 -0
- {b3dkit-0.1.2 → b3dkit-0.1.3}/src/b3dkit/__init__.py +0 -0
- {b3dkit-0.1.2 → b3dkit-0.1.3}/src/b3dkit/antichamfer.py +0 -0
- {b3dkit-0.1.2 → b3dkit-0.1.3}/src/b3dkit/ball_socket.py +0 -0
- {b3dkit-0.1.2 → b3dkit-0.1.3}/src/b3dkit/bolt_fittings.py +0 -0
- {b3dkit-0.1.2 → b3dkit-0.1.3}/src/b3dkit/click_fit.py +0 -0
- {b3dkit-0.1.2 → b3dkit-0.1.3}/src/b3dkit/dovetail.py +0 -0
- {b3dkit-0.1.2 → b3dkit-0.1.3}/src/b3dkit/high_top_slide_box.py +0 -0
- {b3dkit-0.1.2 → b3dkit-0.1.3}/src/b3dkit/point.py +0 -0
- {b3dkit-0.1.2 → b3dkit-0.1.3}/src/b3dkit/slide_box.py +0 -0
- {b3dkit-0.1.2 → b3dkit-0.1.3}/src/b3dkit/twist_snap.py +0 -0
- {b3dkit-0.1.2 → b3dkit-0.1.3}/tests/conftest.py +0 -0
- {b3dkit-0.1.2 → b3dkit-0.1.3}/tests/test_antichamfer.py +0 -0
- {b3dkit-0.1.2 → b3dkit-0.1.3}/tests/test_ball_socket.py +0 -0
- {b3dkit-0.1.2 → b3dkit-0.1.3}/tests/test_bolt_fittings.py +0 -0
- {b3dkit-0.1.2 → b3dkit-0.1.3}/tests/test_click_fit.py +0 -0
- {b3dkit-0.1.2 → b3dkit-0.1.3}/tests/test_point.py +0 -0
- {b3dkit-0.1.2 → b3dkit-0.1.3}/tests/test_slide_box.py +0 -0
- {b3dkit-0.1.2 → b3dkit-0.1.3}/tests/test_twist_snap.py +0 -0
|
Binary file
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: b3dkit
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.3
|
|
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
|
|
@@ -10,21 +10,21 @@ License-File: LICENSE
|
|
|
10
10
|
Classifier: License :: OSI Approved :: MIT License
|
|
11
11
|
Classifier: Operating System :: OS Independent
|
|
12
12
|
Classifier: Programming Language :: Python :: 3
|
|
13
|
-
Requires-Python: >=3.
|
|
14
|
-
Requires-Dist: build123d>=0.0
|
|
15
|
-
Requires-Dist: ocp-vscode
|
|
13
|
+
Requires-Python: >=3.10
|
|
14
|
+
Requires-Dist: build123d>=0.10.0
|
|
15
|
+
Requires-Dist: ocp-vscode>=3.1.1
|
|
16
16
|
Provides-Extra: dev
|
|
17
|
-
Requires-Dist: pytest; extra == 'dev'
|
|
18
|
-
Requires-Dist: pytest
|
|
17
|
+
Requires-Dist: pytest-cov>=7.0.0; extra == 'dev'
|
|
18
|
+
Requires-Dist: pytest>=9.0.2; extra == 'dev'
|
|
19
19
|
Provides-Extra: docs
|
|
20
|
-
Requires-Dist: markdown-include; extra == 'docs'
|
|
21
|
-
Requires-Dist: mkdocs; extra == 'docs'
|
|
22
|
-
Requires-Dist: mkdocs
|
|
20
|
+
Requires-Dist: markdown-include>=0.8.1; extra == 'docs'
|
|
21
|
+
Requires-Dist: mkdocs-material>=9.7.6; extra == 'docs'
|
|
22
|
+
Requires-Dist: mkdocs>=1.6.1; extra == 'docs'
|
|
23
23
|
Provides-Extra: maintain
|
|
24
|
-
Requires-Dist: build; extra == 'maintain'
|
|
25
|
-
Requires-Dist: hatch-vcs; extra == 'maintain'
|
|
26
|
-
Requires-Dist: hatchling; extra == 'maintain'
|
|
27
|
-
Requires-Dist: twine; extra == 'maintain'
|
|
24
|
+
Requires-Dist: build>=1.4.0; extra == 'maintain'
|
|
25
|
+
Requires-Dist: hatch-vcs>=0.5.0; extra == 'maintain'
|
|
26
|
+
Requires-Dist: hatchling>=1.29.0; extra == 'maintain'
|
|
27
|
+
Requires-Dist: twine>=6.2.0; extra == 'maintain'
|
|
28
28
|
Description-Content-Type: text/markdown
|
|
29
29
|
|
|
30
30
|
# b3dkit Overview
|
|
@@ -111,18 +111,26 @@ Creates an extruded diamond (4-sided polygon) that behaves like a cylinder. This
|
|
|
111
111
|
DiamondCylinder(
|
|
112
112
|
radius: float,
|
|
113
113
|
height: float,
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
114
|
+
arc_size: float = 360,
|
|
115
|
+
stretch: tuple = (1, 1, 1),
|
|
116
|
+
rotation: RotationLike = (0, 0, 0),
|
|
117
|
+
align: Align | tuple[Align, Align, Align] | None = None,
|
|
118
|
+
mode: Mode = Mode.ADD,
|
|
117
119
|
)
|
|
118
120
|
```
|
|
119
121
|
|
|
120
122
|
**Arguments:**
|
|
121
123
|
- `radius` (float): The radius of the circumscribed circle
|
|
122
124
|
- `height` (float): The height of the extrusion
|
|
123
|
-
- `
|
|
124
|
-
- `align` (tuple, default=(Align.CENTER, Align.CENTER, Align.CENTER)): Alignment
|
|
125
|
+
- `arc_size` (float, default=360): Angular sweep in degrees for the circular clipping sector used to intersect the base profile
|
|
125
126
|
- `stretch` (tuple, default=(1, 1, 1)): Scaling factors (X, Y, Z)
|
|
127
|
+
- `rotation` (RotationLike, default=(0, 0, 0)): Rotation angles (X, Y, Z) in degrees
|
|
128
|
+
- `align` (Align | tuple[Align, Align, Align] | None, default=None): Alignment along X, Y, Z axes
|
|
129
|
+
- `mode` (Mode, default=Mode.ADD): Boolean combination mode
|
|
130
|
+
|
|
131
|
+
**Arc Size Behavior:**
|
|
132
|
+
- `arc_size=360` produces the full diamond profile
|
|
133
|
+
- `arc_size<360` clips the XY profile and reduces volume while preserving Z behavior from `height` and `stretch[2]`
|
|
126
134
|
|
|
127
135
|
**Returns:**
|
|
128
136
|
- `Part`: The diamond cylinder
|
|
@@ -165,10 +173,12 @@ Creates an extruded regular polygon that behaves like a cylinder.
|
|
|
165
173
|
PolygonalCylinder(
|
|
166
174
|
radius: float,
|
|
167
175
|
height: float,
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
176
|
+
side_count: int = 6,
|
|
177
|
+
arc_size: float = 360,
|
|
178
|
+
stretch: tuple = (1, 1, 1),
|
|
179
|
+
rotation: RotationLike = (0, 0, 0),
|
|
180
|
+
align: Align | tuple[Align, Align, Align] | None = None,
|
|
181
|
+
mode: Mode = Mode.ADD,
|
|
172
182
|
)
|
|
173
183
|
```
|
|
174
184
|
|
|
@@ -176,12 +186,16 @@ PolygonalCylinder(
|
|
|
176
186
|
**Arguments:**
|
|
177
187
|
- `radius` (float): The radius of the circumscribed circle
|
|
178
188
|
- `height` (float): The height of the extrusion
|
|
179
|
-
- `
|
|
189
|
+
- `side_count` (int, default=6): Number of sides of the polygon
|
|
190
|
+
- `arc_size` (float, default=360): Angular sweep in degrees for the circular clipping sector used to intersect the base profile
|
|
180
191
|
- `stretch` (tuple, default=(1, 1, 1)): Scaling factors (X, Y, Z)
|
|
181
|
-
- `rotation` (RotationLike,
|
|
182
|
-
- `align` (Align | tuple[Align, Align, Align] | None,
|
|
183
|
-
|
|
184
|
-
|
|
192
|
+
- `rotation` (RotationLike, default=(0, 0, 0)): Angles to rotate about axes
|
|
193
|
+
- `align` (Align | tuple[Align, Align, Align] | None, default=None): Align MIN, CENTER, or MAX on each axis
|
|
194
|
+
- `mode` (Mode, default=Mode.ADD): Boolean combination mode
|
|
195
|
+
|
|
196
|
+
**Arc Size Behavior:**
|
|
197
|
+
- `arc_size=360` keeps the full regular polygonal profile
|
|
198
|
+
- `arc_size<360` clips the XY profile and reduces volume while preserving Z behavior from `height` and `stretch[2]`
|
|
185
199
|
|
|
186
200
|
|
|
187
201
|
### RoundedCylinder
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# Hexagonal Patterns
|
|
2
|
+
|
|
3
|
+
This module provides two part objects for creating hexagonal (honeycomb) patterns:
|
|
4
|
+
|
|
5
|
+
- [`HexWall`](#hexwall) — a flat box with a hexagonal cutout pattern.
|
|
6
|
+
- [`HexCylindrical`](#hexcylindrical) — a honeycomb of hexagonal holes wrapped around the curved surface of a cone or cylinder.
|
|
7
|
+
|
|
8
|
+
## HexWall
|
|
9
|
+
|
|
10
|
+
Part Object: `HexWall`
|
|
11
|
+
|
|
12
|
+
Create a box with a hexagonal cutout pattern defined by length, width, and height, apothem, and wall thickness. This Part is useful for generating hexagonal patterns in 3D models.
|
|
13
|
+
|
|
14
|
+
### Arguments
|
|
15
|
+
|
|
16
|
+
- `length` (float): The length of the hexagonal wall.
|
|
17
|
+
- `width` (float): The width of the hexagonal wall.
|
|
18
|
+
- `height` (float): The height of the hexagonal wall.
|
|
19
|
+
- `apothem` (float): The apothem (distance from the center to the midpoint of a side) of the hexagons.
|
|
20
|
+
- `wall_thickness` (float): The thickness of the walls of the hexagons.
|
|
21
|
+
- `align` (Union[Align, tuple[Align, Align, Align]], default=(Align.CENTER, Align.CENTER, Align.CENTER)): The alignment of the hexagonal wall. Can be a single `Align` value or a tuple of three `Align` values for x, y, and z alignment.
|
|
22
|
+
- `inverse` (bool, default=False): If `True`, creates an inverse hexagonal pattern.
|
|
23
|
+
|
|
24
|
+
### Returns
|
|
25
|
+
|
|
26
|
+
- `Part`: The created hexagonal wall part.
|
|
27
|
+
|
|
28
|
+
### Example
|
|
29
|
+
|
|
30
|
+
```python
|
|
31
|
+
from b3dkit import HexWall
|
|
32
|
+
from build123d import Align
|
|
33
|
+
|
|
34
|
+
# Create a hexagonal wall with specified dimensions and properties
|
|
35
|
+
hex_wall = HexWall(
|
|
36
|
+
length=100,
|
|
37
|
+
width=100,
|
|
38
|
+
height=10,
|
|
39
|
+
apothem=5,
|
|
40
|
+
wall_thickness=1,
|
|
41
|
+
align=(Align.CENTER, Align.CENTER, Align.CENTER),
|
|
42
|
+
inverse=False
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
# Create an inverse hexagonal wall
|
|
46
|
+
inverse_hex_wall = HexWall(
|
|
47
|
+
length=100,
|
|
48
|
+
width=100,
|
|
49
|
+
height=10,
|
|
50
|
+
apothem=5,
|
|
51
|
+
wall_thickness=1,
|
|
52
|
+
align=(Align.CENTER, Align.CENTER, Align.CENTER),
|
|
53
|
+
inverse=True
|
|
54
|
+
)
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## HexCylindrical
|
|
58
|
+
|
|
59
|
+
Part Object: `HexCylindrical`
|
|
60
|
+
|
|
61
|
+
Wraps a honeycomb of hexagonal holes around the curved surface of a `Cone` or `Cylinder`. It builds the hole geometry *only* — a `Part` containing one cutter per hole — so you subtract it from your solid to vent or lighten it.
|
|
62
|
+
|
|
63
|
+
The holes are laid out row by row: `horizontal_count` holes run around the surface at each height, `vertical_count` rows stack up the slope starting `z_distance` above the base, and alternate rows are offset half a step for the honeycomb stagger. Each hex is projected onto the true surface so it follows the curvature, then thickened into a cutter.
|
|
64
|
+
|
|
65
|
+
Each row is centered on a fixed meridian, so the pattern stays left/right symmetric as a cone tapers instead of drifting to one side. Rows that would climb past the top edge of the surface are dropped automatically.
|
|
66
|
+
|
|
67
|
+
### Arguments
|
|
68
|
+
|
|
69
|
+
- `cylindrical` (Cone | Cylinder): The cone or cylinder to wrap holes around. Assumed upright on the Z axis with its base at the bottom; its circular edges define the radius at each height. A pointed cone (top radius 0) is supported — its apex radius is inferred from the bounding box.
|
|
70
|
+
- `hole_radius` (float): Radius of each hexagonal hole (the `RegularPolygon` radius).
|
|
71
|
+
- `spacing_radius` (float): Hex cell radius controlling spacing; nearest holes are `2 * spacing_radius` apart (matching `HexLocations(radius=...)`).
|
|
72
|
+
- `horizontal_count` (int): Number of holes around the surface in each row.
|
|
73
|
+
- `vertical_count` (int): Number of rows stacked up the slope. Rows that fall off the top edge are skipped, so the actual number of rows may be fewer.
|
|
74
|
+
- `thickness` (float): How far each cutter extends in/out of the surface. Set `thickness >= wall_thickness` to cut clean through a hollow wall.
|
|
75
|
+
- `z_distance` (float): Height of the lowest row of holes, measured up from the bottom of the surface (not an absolute Z coordinate).
|
|
76
|
+
- `rotation` (RotationLike, default=(0, 0, 0)): Angles to rotate about the axes.
|
|
77
|
+
- `align` (Union[None, Align, tuple[Align, Align, Align]], default=None): Align MIN, CENTER, or MAX of the object.
|
|
78
|
+
- `mode` (Mode, default=Mode.ADD): Combine mode used when the object is created inside an active build context.
|
|
79
|
+
|
|
80
|
+
### Returns
|
|
81
|
+
|
|
82
|
+
- `Part`: A part whose children are the hole cutters. Subtract it from your solid (e.g. `solid - HexCylindrical(...)`, or pass `mode=Mode.SUBTRACT` inside a build context) to make the holes.
|
|
83
|
+
|
|
84
|
+
### Raises
|
|
85
|
+
|
|
86
|
+
- `ValueError`: If `cylindrical` has no circular edge to derive a radius profile from (e.g. a `Box`).
|
|
87
|
+
- `ValueError`: If no holes fit on the surface (e.g. `z_distance` starts at or above the top edge, or the surface is too short for the given spacing).
|
|
88
|
+
|
|
89
|
+
### Example
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
from b3dkit import HexCylindrical
|
|
93
|
+
from build123d import Cone, Cylinder, Align
|
|
94
|
+
|
|
95
|
+
# Vent a tapering cone with a honeycomb pattern
|
|
96
|
+
cone = Cone(
|
|
97
|
+
bottom_radius=40,
|
|
98
|
+
top_radius=20,
|
|
99
|
+
height=60,
|
|
100
|
+
align=(Align.CENTER, Align.CENTER, Align.MIN),
|
|
101
|
+
)
|
|
102
|
+
holes = HexCylindrical(
|
|
103
|
+
cone,
|
|
104
|
+
hole_radius=2,
|
|
105
|
+
spacing_radius=4,
|
|
106
|
+
horizontal_count=16,
|
|
107
|
+
vertical_count=6,
|
|
108
|
+
thickness=4, # >= wall thickness so the holes cut all the way through
|
|
109
|
+
z_distance=8, # start the first row 8mm up from the base
|
|
110
|
+
)
|
|
111
|
+
vented_cone = cone - holes
|
|
112
|
+
|
|
113
|
+
# The same call works on a straight cylinder (constant radius)
|
|
114
|
+
cylinder = Cylinder(
|
|
115
|
+
radius=30,
|
|
116
|
+
height=60,
|
|
117
|
+
align=(Align.CENTER, Align.CENTER, Align.MIN),
|
|
118
|
+
)
|
|
119
|
+
vented_cylinder = cylinder - HexCylindrical(
|
|
120
|
+
cylinder,
|
|
121
|
+
hole_radius=2,
|
|
122
|
+
spacing_radius=4,
|
|
123
|
+
horizontal_count=20,
|
|
124
|
+
vertical_count=8,
|
|
125
|
+
thickness=4,
|
|
126
|
+
z_distance=6,
|
|
127
|
+
)
|
|
128
|
+
```
|
|
@@ -1,37 +1,37 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "b3dkit"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.3"
|
|
4
4
|
authors = [
|
|
5
5
|
{ name="x0pherl"},
|
|
6
6
|
]
|
|
7
7
|
description = "build123d libraries and utilities"
|
|
8
8
|
readme = "README.md"
|
|
9
|
-
requires-python = ">=3.
|
|
9
|
+
requires-python = ">=3.10"
|
|
10
10
|
classifiers = [
|
|
11
11
|
"Programming Language :: Python :: 3",
|
|
12
12
|
"License :: OSI Approved :: MIT License",
|
|
13
13
|
"Operating System :: OS Independent",
|
|
14
14
|
]
|
|
15
15
|
dependencies = [
|
|
16
|
-
"build123d>=0.0
|
|
17
|
-
"ocp_vscode",
|
|
16
|
+
"build123d>=0.10.0",
|
|
17
|
+
"ocp_vscode>=3.1.1",
|
|
18
18
|
]
|
|
19
19
|
|
|
20
20
|
[project.optional-dependencies]
|
|
21
21
|
dev = [
|
|
22
|
-
"pytest",
|
|
23
|
-
"pytest-cov",
|
|
22
|
+
"pytest>=9.0.2",
|
|
23
|
+
"pytest-cov>=7.0.0",
|
|
24
24
|
]
|
|
25
25
|
maintain =[
|
|
26
|
-
"twine",
|
|
27
|
-
"hatch-vcs",
|
|
28
|
-
"hatchling",
|
|
29
|
-
"build",
|
|
26
|
+
"twine>=6.2.0",
|
|
27
|
+
"hatch-vcs>=0.5.0",
|
|
28
|
+
"hatchling>=1.29.0",
|
|
29
|
+
"build>=1.4.0",
|
|
30
30
|
]
|
|
31
31
|
docs = [
|
|
32
|
-
"mkdocs",
|
|
33
|
-
"mkdocs-material",
|
|
34
|
-
"markdown-include",
|
|
32
|
+
"mkdocs>=1.6.1",
|
|
33
|
+
"mkdocs-material>=9.7.6",
|
|
34
|
+
"markdown-include>=0.8.1",
|
|
35
35
|
]
|
|
36
36
|
|
|
37
37
|
[project.urls]
|
|
@@ -40,7 +40,7 @@ Issues = "https://github.com/x0pherl/b3dkit/issues"
|
|
|
40
40
|
Documentation = "https://b3dkit.readthedocs.org"
|
|
41
41
|
|
|
42
42
|
[build-system]
|
|
43
|
-
requires = ["hatchling", "hatch-vcs"]
|
|
43
|
+
requires = ["hatchling>=1.29.0", "hatch-vcs>=0.5.0"]
|
|
44
44
|
|
|
45
45
|
build-backend = "hatchling.build"
|
|
46
46
|
|
|
@@ -25,6 +25,7 @@ from build123d import (
|
|
|
25
25
|
JernArc,
|
|
26
26
|
Line,
|
|
27
27
|
Location,
|
|
28
|
+
Locations,
|
|
28
29
|
Mode,
|
|
29
30
|
Part,
|
|
30
31
|
Plane,
|
|
@@ -127,7 +128,7 @@ def circular_intersection(radius: float, coordinate: float) -> float:
|
|
|
127
128
|
- coordinate: a coordinate along one axis of the circle (must be a
|
|
128
129
|
positive value less than the radius)
|
|
129
130
|
"""
|
|
130
|
-
if 0
|
|
131
|
+
if not (0 <= coordinate <= radius):
|
|
131
132
|
raise ValueError("The x-coordinate cannot be greater than the radius.")
|
|
132
133
|
return sqrt(radius**2 - coordinate**2)
|
|
133
134
|
|
|
@@ -218,6 +219,7 @@ class PolygonalCylinder(BasePartObject):
|
|
|
218
219
|
radius: float,
|
|
219
220
|
height: float,
|
|
220
221
|
side_count: int = 6,
|
|
222
|
+
arc_size: float = 360,
|
|
221
223
|
stretch: tuple = (1, 1, 1),
|
|
222
224
|
rotation: RotationLike = (0, 0, 0),
|
|
223
225
|
align: Union[None, Align, tuple[Align, Align, Align]] = None,
|
|
@@ -230,6 +232,7 @@ class PolygonalCylinder(BasePartObject):
|
|
|
230
232
|
- radius: the radius of the cylinder
|
|
231
233
|
- height: the height of the cylinder
|
|
232
234
|
- side_count: the number of sides of the polygonal base (default is 6)
|
|
235
|
+
- arc_size: (float, optional) – angular size of cone. Defaults to 360.
|
|
233
236
|
- stretch: scales the base polygon
|
|
234
237
|
- rotation: the rotation of the cylinder
|
|
235
238
|
- align: the alignment of the cylinder
|
|
@@ -239,14 +242,21 @@ class PolygonalCylinder(BasePartObject):
|
|
|
239
242
|
validate_inputs(context, self)
|
|
240
243
|
|
|
241
244
|
with BuildPart() as tube:
|
|
242
|
-
with BuildSketch():
|
|
245
|
+
with BuildSketch() as poly:
|
|
243
246
|
RegularPolygon(
|
|
244
247
|
radius=radius, side_count=side_count, align=tuplify(align, 2)
|
|
245
248
|
)
|
|
249
|
+
center = poly.sketch.face().center()
|
|
250
|
+
with Locations((center.X, center.Y)):
|
|
251
|
+
Circle(
|
|
252
|
+
radius=radius,
|
|
253
|
+
arc_size=arc_size,
|
|
254
|
+
align=None,
|
|
255
|
+
mode=Mode.INTERSECT,
|
|
256
|
+
),
|
|
246
257
|
extrude(amount=height * stretch[2])
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
)
|
|
258
|
+
scale(tube.part, by=(stretch[0], stretch[1], 1))
|
|
259
|
+
super().__init__(part=tube.part, rotation=rotation, align=align, mode=mode)
|
|
250
260
|
|
|
251
261
|
|
|
252
262
|
class DiamondCylinder(PolygonalCylinder):
|
|
@@ -255,6 +265,7 @@ class DiamondCylinder(PolygonalCylinder):
|
|
|
255
265
|
self,
|
|
256
266
|
radius: float,
|
|
257
267
|
height: float,
|
|
268
|
+
arc_size: float = 360,
|
|
258
269
|
stretch: tuple = (1, 1, 1),
|
|
259
270
|
rotation: RotationLike = (0, 0, 0),
|
|
260
271
|
align: Union[None, Align, tuple[Align, Align, Align]] = None,
|
|
@@ -264,17 +275,19 @@ class DiamondCylinder(PolygonalCylinder):
|
|
|
264
275
|
creates an extruded diamond that behaves like a cylinder
|
|
265
276
|
-------
|
|
266
277
|
arguments:
|
|
267
|
-
- radius: the radius of the cylinder
|
|
268
|
-
- height: the height of the cylinder
|
|
269
|
-
-
|
|
270
|
-
-
|
|
278
|
+
- radius: (float) - the radius of the cylinder
|
|
279
|
+
- height: (float) - the height of the cylinder
|
|
280
|
+
- arc_size: (float, optional) – angular size of cone. Defaults to 360.
|
|
281
|
+
- rotation: (RotationLike) - the rotation of the cylinder
|
|
282
|
+
- align: (Union[None, Align, tuple[Align, Align, Align]]) - the alignment of the cylinder (default
|
|
271
283
|
is (Align.CENTER, Align.CENTER, Align.CENTER) )
|
|
272
|
-
- mode: the mode to use when adding the part
|
|
284
|
+
- mode: (Mode) - the mode to use when adding the part
|
|
273
285
|
"""
|
|
274
286
|
super().__init__(
|
|
275
287
|
radius=radius,
|
|
276
288
|
height=height,
|
|
277
289
|
side_count=4,
|
|
290
|
+
arc_size=arc_size,
|
|
278
291
|
stretch=stretch,
|
|
279
292
|
rotation=rotation,
|
|
280
293
|
align=align,
|
|
@@ -379,19 +392,14 @@ class TeardropCylinder(BasePartObject):
|
|
|
379
392
|
if __name__ == "__main__":
|
|
380
393
|
|
|
381
394
|
show(
|
|
382
|
-
|
|
383
|
-
50,
|
|
384
|
-
2,
|
|
385
|
-
),
|
|
386
|
-
reset_camera=Camera.KEEP,
|
|
387
|
-
)
|
|
388
|
-
show(
|
|
389
|
-
DiamondCylinder(
|
|
395
|
+
PolygonalCylinder(
|
|
390
396
|
radius=50,
|
|
391
397
|
height=200,
|
|
398
|
+
side_count=6,
|
|
399
|
+
arc_size=270,
|
|
392
400
|
stretch=(1, 1, 1),
|
|
393
401
|
rotation=(0, 0, 0),
|
|
394
|
-
align=(Align.
|
|
402
|
+
align=(Align.CENTER, Align.CENTER, Align.MAX),
|
|
395
403
|
),
|
|
396
404
|
reset_camera=Camera.KEEP,
|
|
397
405
|
)
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
"""
|
|
2
|
+
HexWall
|
|
3
|
+
|
|
4
|
+
name: hexwall.py
|
|
5
|
+
by: x0pherl
|
|
6
|
+
date: Jan 19th 2025
|
|
7
|
+
|
|
8
|
+
desc:
|
|
9
|
+
This build123d python module creates a hexagonal pattern within a box, resulting
|
|
10
|
+
in a grid of hexagons.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from math import sqrt
|
|
14
|
+
from typing import Union
|
|
15
|
+
|
|
16
|
+
from build123d import (
|
|
17
|
+
Align,
|
|
18
|
+
Axis,
|
|
19
|
+
BasePartObject,
|
|
20
|
+
Box,
|
|
21
|
+
BuildPart,
|
|
22
|
+
BuildSketch,
|
|
23
|
+
Cone,
|
|
24
|
+
Cylinder,
|
|
25
|
+
Edge,
|
|
26
|
+
GeomType,
|
|
27
|
+
HexLocations,
|
|
28
|
+
Locations,
|
|
29
|
+
Mode,
|
|
30
|
+
Part,
|
|
31
|
+
Plane,
|
|
32
|
+
RegularPolygon,
|
|
33
|
+
RotationLike,
|
|
34
|
+
extrude,
|
|
35
|
+
thicken,
|
|
36
|
+
tuplify,
|
|
37
|
+
)
|
|
38
|
+
from ocp_vscode import Camera, show
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class HexWall(BasePartObject):
|
|
42
|
+
|
|
43
|
+
def __init__(
|
|
44
|
+
self,
|
|
45
|
+
length: float,
|
|
46
|
+
width: float,
|
|
47
|
+
height: float,
|
|
48
|
+
apothem: float,
|
|
49
|
+
wall_thickness: float,
|
|
50
|
+
inverse=False,
|
|
51
|
+
rotation: RotationLike = (0, 0, 0),
|
|
52
|
+
align: Union[None, Align, tuple[Align, Align, Align]] = None,
|
|
53
|
+
mode: Mode = Mode.ADD,
|
|
54
|
+
):
|
|
55
|
+
"""
|
|
56
|
+
Part Object: hexwall
|
|
57
|
+
-------
|
|
58
|
+
arguments:
|
|
59
|
+
- length (float): box size
|
|
60
|
+
- width (float): box size
|
|
61
|
+
- height (float): box size
|
|
62
|
+
- apothem (float): the distance between two paralel edges of the hexagon
|
|
63
|
+
- rotation (RotationLike, optional): angles to rotate about axes. Defaults to (0, 0, 0)
|
|
64
|
+
- align (Align | tuple[Align, Align, Align] | None, optional): align MIN, CENTER,
|
|
65
|
+
or MAX of object. Defaults to (Align.CENTER, Align.CENTER, Align.CENTER)
|
|
66
|
+
- mode (Mode, optional): combine mode. Defaults to Mode.ADD
|
|
67
|
+
"""
|
|
68
|
+
with BuildPart() as wall:
|
|
69
|
+
hexwall_radius = 2 * sqrt(3) / 3 * apothem
|
|
70
|
+
hexwall_xcount = int(length // ((sqrt(3) / 2 * apothem) / 2)) + 2
|
|
71
|
+
if hexwall_xcount % 2 == 0:
|
|
72
|
+
hexwall_xcount += 1
|
|
73
|
+
Box(length=length, width=width, height=height, align=tuplify(align, 3))
|
|
74
|
+
combine_mode = Mode.INTERSECT if inverse else Mode.SUBTRACT
|
|
75
|
+
with BuildPart(mode=combine_mode):
|
|
76
|
+
with BuildSketch(wall.faces().sort_by(Axis.Z)[0]) as sk:
|
|
77
|
+
with HexLocations(
|
|
78
|
+
radius=hexwall_radius,
|
|
79
|
+
x_count=hexwall_xcount,
|
|
80
|
+
y_count=int(width // apothem / 2) + 2,
|
|
81
|
+
align=(Align.CENTER, Align.CENTER),
|
|
82
|
+
):
|
|
83
|
+
RegularPolygon(
|
|
84
|
+
radius=2 * sqrt(3) / 3 * (apothem - wall_thickness / 2),
|
|
85
|
+
major_radius=False,
|
|
86
|
+
side_count=6,
|
|
87
|
+
)
|
|
88
|
+
extrude(sk.sketch, -height)
|
|
89
|
+
part = wall.part
|
|
90
|
+
part.label = "hexwall"
|
|
91
|
+
|
|
92
|
+
super().__init__(
|
|
93
|
+
part=part, rotation=rotation, align=tuplify(align, 3), mode=mode
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class HexCylindrical(BasePartObject):
|
|
98
|
+
|
|
99
|
+
def __init__(
|
|
100
|
+
self,
|
|
101
|
+
cylindrical: Cone | Cylinder,
|
|
102
|
+
hole_radius: float,
|
|
103
|
+
spacing_radius: float,
|
|
104
|
+
horizontal_count: int,
|
|
105
|
+
vertical_count: int,
|
|
106
|
+
thickness: float,
|
|
107
|
+
z_distance: float,
|
|
108
|
+
rotation: RotationLike = (0, 0, 0),
|
|
109
|
+
align: Union[None, Align, tuple[Align, Align, Align]] = None,
|
|
110
|
+
mode: Mode = Mode.ADD,
|
|
111
|
+
):
|
|
112
|
+
"""
|
|
113
|
+
Part Object: HexCylindrical
|
|
114
|
+
-------
|
|
115
|
+
Wraps a honeycomb of hexagonal holes around the cylindrical surface of a Cylinder
|
|
116
|
+
or Cone and returns the hole geometry *only* (the cutters), so the caller can remove
|
|
117
|
+
them elsewhere with ``mode=Mode.SUBTRACT``.
|
|
118
|
+
|
|
119
|
+
The holes are laid out row by row: ``horizontal_count`` holes run around the cone at each
|
|
120
|
+
height, ``vertical_count`` rows stack up the slope from ``z_distance``, and alternate
|
|
121
|
+
rows are offset half a step for a honeycomb stagger. Each hex is projected onto
|
|
122
|
+
the true cone surface (so it follows the curvature) and thickened into a cutter.
|
|
123
|
+
|
|
124
|
+
arguments:
|
|
125
|
+
- cylindrical (Part): the cone or cylinder to wrap holes around. Assumed upright on the
|
|
126
|
+
Z axis with its base at the bottom; its circular edges define the radius
|
|
127
|
+
at each height.
|
|
128
|
+
- hole_radius (float): radius of each hexagonal hole (RegularPolygon radius)
|
|
129
|
+
- spacing_radius (float): hex cell radius controlling spacing; nearest holes
|
|
130
|
+
are ``2 * spacing_radius`` apart (matches ``HexLocations(radius=...)``)
|
|
131
|
+
- horizontal_count (int): number of holes around the cone in each row
|
|
132
|
+
- vertical_count (int): number of rows stacked up the slope
|
|
133
|
+
- thickness (float): how far each cutter extends in/out of the surface; set
|
|
134
|
+
``thickness >= wall_thickness`` to cut clean through a hollow cone wall
|
|
135
|
+
- z_distance (float): height of the lowest row of holes, measured up
|
|
136
|
+
from the bottom of the cone (not an absolute Z coordinate)
|
|
137
|
+
- rotation (RotationLike, optional): angles to rotate about axes. Defaults to
|
|
138
|
+
(0, 0, 0)
|
|
139
|
+
- align (Align | tuple[Align, Align, Align] | None, optional): align MIN,
|
|
140
|
+
CENTER, or MAX of object. Defaults to None
|
|
141
|
+
- mode (Mode, optional): combine mode. Defaults to Mode.ADD
|
|
142
|
+
"""
|
|
143
|
+
|
|
144
|
+
# Derive the cone's radius-vs-height from its circular edges so any upright
|
|
145
|
+
# cone/frustum works (a pointed cone reduces to radius 0 at its apex).
|
|
146
|
+
circles = cylindrical.edges().filter_by(GeomType.CIRCLE).sort_by(Axis.Z)
|
|
147
|
+
if not circles:
|
|
148
|
+
raise ValueError(
|
|
149
|
+
"cone must have at least one circular edge to wrap holes around"
|
|
150
|
+
)
|
|
151
|
+
bottom = circles[0]
|
|
152
|
+
z_bottom, r_bottom = bottom.arc_center.Z, bottom.radius
|
|
153
|
+
if len(circles) >= 2:
|
|
154
|
+
z_top, r_top = circles[-1].arc_center.Z, circles[-1].radius
|
|
155
|
+
else:
|
|
156
|
+
z_top, r_top = cylindrical.bounding_box().max.Z, 0.0
|
|
157
|
+
|
|
158
|
+
def radius_at(z: float) -> float:
|
|
159
|
+
return r_bottom + (r_top - r_bottom) * (z - z_bottom) / (z_top - z_bottom)
|
|
160
|
+
|
|
161
|
+
# Honeycomb pitches: nearest-neighbour distance == 2 * spacing_radius, matching
|
|
162
|
+
# HexLocations(radius=spacing_radius). Rows run around (x), stacked up slope (y).
|
|
163
|
+
x_pitch = 2 * spacing_radius
|
|
164
|
+
y_pitch = spacing_radius * sqrt(3)
|
|
165
|
+
|
|
166
|
+
def hex_row(count: int) -> list[Part]:
|
|
167
|
+
"""A flat row of ``count`` hex faces spaced by x_pitch along +X."""
|
|
168
|
+
with BuildSketch() as row_sketch:
|
|
169
|
+
with Locations(*[(i * x_pitch, 0) for i in range(count)]):
|
|
170
|
+
RegularPolygon(hole_radius, side_count=6, major_radius=False)
|
|
171
|
+
return row_sketch.sketch.faces()
|
|
172
|
+
|
|
173
|
+
# Wrap one row onto the surface at each height, then thicken into cutters.
|
|
174
|
+
# Stop once a row climbs off the cone's surface: above the top edge the
|
|
175
|
+
# radius would extrapolate to zero/negative, which make_circle rejects.
|
|
176
|
+
cutters: list[Part] = []
|
|
177
|
+
for row in range(vertical_count):
|
|
178
|
+
z = z_bottom + z_distance + row * y_pitch
|
|
179
|
+
if not z_bottom <= z <= z_top:
|
|
180
|
+
break
|
|
181
|
+
r = radius_at(z)
|
|
182
|
+
if r <= 0: # pragma: no cover - defensive: z-bound above already
|
|
183
|
+
break # keeps r >= 0; only an exact-apex row could reach here
|
|
184
|
+
path = Edge.make_circle(r, Plane.XY.offset(z))
|
|
185
|
+
|
|
186
|
+
faces = hex_row(horizontal_count)
|
|
187
|
+
# project_faces anchors the row at faces[0].min.X and lays it out in one
|
|
188
|
+
# direction. Center the row's mean position on a fixed meridian so it grows
|
|
189
|
+
# symmetrically from the center instead of drifting to one side up the taper.
|
|
190
|
+
bboxes = [f.bounding_box() for f in faces]
|
|
191
|
+
first_min_x = bboxes[0].min.X
|
|
192
|
+
mean_center_x = sum((b.min.X + b.max.X) / 2 for b in bboxes) / len(bboxes)
|
|
193
|
+
center_offset = (mean_center_x - first_min_x) / path.length
|
|
194
|
+
stagger = (x_pitch / 2) / path.length if row % 2 else 0.0
|
|
195
|
+
|
|
196
|
+
projected = cylindrical.project_faces(
|
|
197
|
+
faces, path=path, start=0.5 - center_offset + stagger
|
|
198
|
+
)
|
|
199
|
+
cutters.extend(
|
|
200
|
+
thicken(face, amount=-thickness, both=False) for face in projected
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
# No rows fit on the surface (e.g. z_distance starts at/above the top, or
|
|
204
|
+
# the surface is too short for the given spacing). Fail clearly instead of
|
|
205
|
+
# letting BasePartObject choke on an empty shape downstream.
|
|
206
|
+
if not cutters:
|
|
207
|
+
raise ValueError(
|
|
208
|
+
"no hex holes fit on the surface; check z_distance, spacing_radius, "
|
|
209
|
+
"and the cone/cylinder height"
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
# Return the cutters as a single (un-fused) Part so the whole pattern can be
|
|
213
|
+
# subtracted in one boolean by the caller.
|
|
214
|
+
part = Part(children=cutters)
|
|
215
|
+
part.label = "hexcone"
|
|
216
|
+
|
|
217
|
+
super().__init__(
|
|
218
|
+
part=part, rotation=rotation, align=tuplify(align, 3), mode=mode
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
if __name__ == "__main__":
|
|
223
|
+
# cone = Cone(100, 50, 100, align=(Align.CENTER, Align.CENTER, Align.MIN))
|
|
224
|
+
# cylinder = Cylinder(100, 100)
|
|
225
|
+
|
|
226
|
+
# part = cone
|
|
227
|
+
# hexsample = HexCylindrical(part, 1, 2, 20, 10, 2, 20)
|
|
228
|
+
|
|
229
|
+
# show(part, hexsample)
|
|
230
|
+
show(HexWall(100, 200, 3, 4, 1))
|