b3dkit 0.1.0__py3-none-any.whl
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/__init__.py +10 -0
- b3dkit/antichamfer.py +79 -0
- b3dkit/ball_socket.py +177 -0
- b3dkit/basic_shapes.py +397 -0
- b3dkit/bolt_fittings.py +351 -0
- b3dkit/click_fit.py +78 -0
- b3dkit/dovetail.py +1338 -0
- b3dkit/hexwall.py +102 -0
- b3dkit/high_top_slide_box.py +504 -0
- b3dkit/point.py +196 -0
- b3dkit/slide_box.py +210 -0
- b3dkit/twist_snap.py +302 -0
- b3dkit-0.1.0.dist-info/METADATA +54 -0
- b3dkit-0.1.0.dist-info/RECORD +16 -0
- b3dkit-0.1.0.dist-info/WHEEL +4 -0
- b3dkit-0.1.0.dist-info/licenses/LICENSE +7 -0
b3dkit/__init__.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
from b3dkit.basic_shapes import *
|
|
2
|
+
from b3dkit.ball_socket import *
|
|
3
|
+
from b3dkit.click_fit import *
|
|
4
|
+
from b3dkit.dovetail import *
|
|
5
|
+
from b3dkit.hexwall import *
|
|
6
|
+
from b3dkit.high_top_slide_box import *
|
|
7
|
+
from b3dkit.point import *
|
|
8
|
+
from b3dkit.slide_box import *
|
|
9
|
+
from b3dkit.twist_snap import *
|
|
10
|
+
from b3dkit.antichamfer import *
|
b3dkit/antichamfer.py
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
from build123d import (
|
|
2
|
+
Align,
|
|
3
|
+
Axis,
|
|
4
|
+
BasePartObject,
|
|
5
|
+
Box,
|
|
6
|
+
BuildPart,
|
|
7
|
+
Builder,
|
|
8
|
+
Compound,
|
|
9
|
+
Face,
|
|
10
|
+
Iterable,
|
|
11
|
+
Location,
|
|
12
|
+
Mode,
|
|
13
|
+
Part,
|
|
14
|
+
add,
|
|
15
|
+
extrude,
|
|
16
|
+
fillet,
|
|
17
|
+
flatten_sequence,
|
|
18
|
+
validate_inputs,
|
|
19
|
+
)
|
|
20
|
+
from math import atan, degrees, tan, radians
|
|
21
|
+
from ocp_vscode import show, Camera
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def anti_chamfer(
|
|
25
|
+
face: Face | Iterable[Face],
|
|
26
|
+
length: float,
|
|
27
|
+
length2: float | None = None,
|
|
28
|
+
) -> Part:
|
|
29
|
+
faces_list = flatten_sequence(face)
|
|
30
|
+
if len(faces_list) == 0:
|
|
31
|
+
raise ValueError("No faces provided to anti_chamfer")
|
|
32
|
+
if not all([isinstance(obj, Face) for obj in faces_list]):
|
|
33
|
+
raise ValueError("anti_chamfer operation takes only Faces")
|
|
34
|
+
|
|
35
|
+
context: Builder | None = Builder._get_context("chamfer")
|
|
36
|
+
validate_inputs(context, "chamfer", faces_list)
|
|
37
|
+
|
|
38
|
+
if length2 is None:
|
|
39
|
+
length2 = length
|
|
40
|
+
|
|
41
|
+
if context is not None:
|
|
42
|
+
target = context._obj
|
|
43
|
+
else:
|
|
44
|
+
target = faces_list[0].topo_parent
|
|
45
|
+
if target is None:
|
|
46
|
+
raise ValueError("face does not seem to belong to a Part")
|
|
47
|
+
# Convert BasePartObject in Part so casting into Part during construction works
|
|
48
|
+
target = Part(target.wrapped) if isinstance(target, BasePartObject) else target
|
|
49
|
+
|
|
50
|
+
if length == 0 or length2 == 0:
|
|
51
|
+
return target
|
|
52
|
+
|
|
53
|
+
with BuildPart() as new_part:
|
|
54
|
+
add(target)
|
|
55
|
+
for f in faces_list:
|
|
56
|
+
extrude(
|
|
57
|
+
f.offset(-length),
|
|
58
|
+
amount=length,
|
|
59
|
+
taper=-degrees(atan(length2 / length)),
|
|
60
|
+
)
|
|
61
|
+
if context is not None:
|
|
62
|
+
context._add_to_context(
|
|
63
|
+
Part(Compound([new_part.part]).wrapped), mode=Mode.REPLACE
|
|
64
|
+
)
|
|
65
|
+
return Part(Compound([new_part.part]).wrapped)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
if __name__ == "__main__":
|
|
69
|
+
with BuildPart(Location((33, 11, 0))) as bkt:
|
|
70
|
+
Box(
|
|
71
|
+
60,
|
|
72
|
+
10,
|
|
73
|
+
20,
|
|
74
|
+
rotation=(0, 0, 45),
|
|
75
|
+
align=(Align.CENTER, Align.CENTER, Align.MIN),
|
|
76
|
+
)
|
|
77
|
+
fillet(bkt.edges().filter_by(Axis.Z), 3)
|
|
78
|
+
anti_chamfer(bkt.faces().filter_by(Axis.Z), 1, 1)
|
|
79
|
+
show(bkt, reset_camera=Camera.KEEP)
|
b3dkit/ball_socket.py
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
from math import sqrt
|
|
2
|
+
from typing import Union
|
|
3
|
+
|
|
4
|
+
from build123d import (
|
|
5
|
+
BasePartObject,
|
|
6
|
+
Box,
|
|
7
|
+
BuildPart,
|
|
8
|
+
BuildSketch,
|
|
9
|
+
Circle,
|
|
10
|
+
Cylinder,
|
|
11
|
+
Location,
|
|
12
|
+
Mode,
|
|
13
|
+
Part,
|
|
14
|
+
Plane,
|
|
15
|
+
PolarLocations,
|
|
16
|
+
RotationLike,
|
|
17
|
+
Sphere,
|
|
18
|
+
Align,
|
|
19
|
+
loft,
|
|
20
|
+
fillet,
|
|
21
|
+
Axis,
|
|
22
|
+
tuplify,
|
|
23
|
+
)
|
|
24
|
+
from ocp_vscode import show, Camera
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class BallMount(BasePartObject):
|
|
28
|
+
|
|
29
|
+
def __init__(
|
|
30
|
+
self,
|
|
31
|
+
ball_radius: float,
|
|
32
|
+
rotation: RotationLike = (0, 0, 0),
|
|
33
|
+
align: Union[None, Align, tuple[Align, Align, Align]] = None,
|
|
34
|
+
mode: Mode = Mode.ADD,
|
|
35
|
+
) -> Part:
|
|
36
|
+
"""
|
|
37
|
+
Creates a ball mount component for a ball-and-socket joint system.
|
|
38
|
+
|
|
39
|
+
The ball mount consists of a spherical ball attached to a tapered shaft. The shaft
|
|
40
|
+
is designed to be inserted into another part or component, while the ball mates
|
|
41
|
+
with a corresponding ball socket to create a flexible joint that allows rotation
|
|
42
|
+
in multiple axes. The shaft has a sophisticated tapered design that transitions from
|
|
43
|
+
the full ball radius at the base to approximately 36% of the ball radius at the
|
|
44
|
+
insertion point, providing strength while allowing for easy integration into
|
|
45
|
+
mounting systems.
|
|
46
|
+
|
|
47
|
+
args:
|
|
48
|
+
- ball_radius: the radius of the spherical ball in millimeters. This determines
|
|
49
|
+
the overall size of the joint system and must match the ball_radius used
|
|
50
|
+
for the corresponding ball_socket.
|
|
51
|
+
- rotation (RotationLike, optional): angles to rotate about axes. Defaults to (0, 0, 0)
|
|
52
|
+
- align (Align | tuple[Align, Align, Align] | None, optional): align MIN, CENTER,
|
|
53
|
+
or MAX of object. Defaults to (Align.CENTER, Align.CENTER, Align.CENTER)
|
|
54
|
+
- mode (Mode, optional): combine mode. Defaults to Mode.ADD
|
|
55
|
+
"""
|
|
56
|
+
with BuildPart() as ballmount:
|
|
57
|
+
with BuildPart(Location((0, 0, ball_radius * 2.5))):
|
|
58
|
+
Sphere(
|
|
59
|
+
radius=ball_radius, align=(Align.CENTER, Align.CENTER, Align.CENTER)
|
|
60
|
+
)
|
|
61
|
+
with BuildPart() as shaft:
|
|
62
|
+
with BuildSketch():
|
|
63
|
+
Circle(
|
|
64
|
+
radius=ball_radius,
|
|
65
|
+
align=(Align.CENTER, Align.CENTER),
|
|
66
|
+
)
|
|
67
|
+
with BuildSketch(Plane.XY.offset(ball_radius * 0.5)):
|
|
68
|
+
Circle(
|
|
69
|
+
radius=ball_radius / 2.75,
|
|
70
|
+
align=(Align.CENTER, Align.CENTER),
|
|
71
|
+
)
|
|
72
|
+
height_ratio = 0.25
|
|
73
|
+
with BuildSketch(Plane.XY.offset(ball_radius * (2 + height_ratio))):
|
|
74
|
+
Circle(
|
|
75
|
+
radius=ball_radius * (sqrt(1 - height_ratio**2)),
|
|
76
|
+
align=(Align.CENTER, Align.CENTER),
|
|
77
|
+
)
|
|
78
|
+
loft()
|
|
79
|
+
|
|
80
|
+
ballmount.part.label = "Ball Mount"
|
|
81
|
+
super().__init__(
|
|
82
|
+
part=ballmount.part, rotation=rotation, align=tuplify(align, 3), mode=mode
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class BallSocket(BasePartObject):
|
|
87
|
+
|
|
88
|
+
def __init__(
|
|
89
|
+
self,
|
|
90
|
+
ball_radius: float,
|
|
91
|
+
wall_thickness: float = 2,
|
|
92
|
+
tolerance: float = 0.1,
|
|
93
|
+
rotation: RotationLike = (0, 0, 0),
|
|
94
|
+
align: Union[None, Align, tuple[Align, Align, Align]] = None,
|
|
95
|
+
mode: Mode = Mode.ADD,
|
|
96
|
+
):
|
|
97
|
+
"""
|
|
98
|
+
Creates a ball socket component for a ball-and-socket joint system.
|
|
99
|
+
|
|
100
|
+
The ball socket is designed to receive and hold a ball mount, creating a flexible
|
|
101
|
+
joint that allows rotation in multiple axes. The socket features a hemispherical
|
|
102
|
+
cavity to house the ball, a cylindrical outer shell for strength, and flexible
|
|
103
|
+
cuts that allow the socket to grip the ball while still permitting smooth rotation.
|
|
104
|
+
The socket includes a flange at the top with a smooth filleted edge for comfortable
|
|
105
|
+
operation and four radial flex cuts that allow the socket walls to compress slightly
|
|
106
|
+
for ball retention while maintaining smooth rotation.
|
|
107
|
+
|
|
108
|
+
args:
|
|
109
|
+
- ball_radius: the radius of the spherical ball that will be inserted into
|
|
110
|
+
this socket, in millimeters. Must match the ball_radius of the corresponding
|
|
111
|
+
ball_mount for proper fit.
|
|
112
|
+
- wall_thickness: the thickness of the socket walls in millimeters. Affects
|
|
113
|
+
both strength and flexibility. Thicker walls provide more strength but may
|
|
114
|
+
reduce flexibility.
|
|
115
|
+
- tolerance: additional clearance around the ball in millimeters. Positive
|
|
116
|
+
values create looser fits, negative values create tighter fits.
|
|
117
|
+
- rotation (RotationLike, optional): angles to rotate about axes. Defaults to (0, 0, 0)
|
|
118
|
+
- align (Align | tuple[Align, Align, Align] | None, optional): align MIN, CENTER,
|
|
119
|
+
or MAX of object. Defaults to (Align.CENTER, Align.CENTER, Align.CENTER)
|
|
120
|
+
- mode (Mode, optional): combine mode. Defaults to Mode.ADD
|
|
121
|
+
|
|
122
|
+
"""
|
|
123
|
+
with BuildPart() as socket:
|
|
124
|
+
Cylinder(
|
|
125
|
+
radius=ball_radius + wall_thickness,
|
|
126
|
+
height=ball_radius + wall_thickness * 2.5,
|
|
127
|
+
align=(Align.CENTER, Align.CENTER, Align.MIN),
|
|
128
|
+
)
|
|
129
|
+
with BuildPart(
|
|
130
|
+
Plane.XY.offset(wall_thickness), mode=Mode.SUBTRACT
|
|
131
|
+
) as bowl_cut:
|
|
132
|
+
Sphere(
|
|
133
|
+
radius=ball_radius + tolerance,
|
|
134
|
+
align=(Align.CENTER, Align.CENTER, Align.MIN),
|
|
135
|
+
)
|
|
136
|
+
# Fillet the top edge if inner wires exist (after sphere subtraction they may not)
|
|
137
|
+
top_face = socket.faces().sort_by(Axis.Z)[-1]
|
|
138
|
+
if top_face.inner_wires():
|
|
139
|
+
fillet(
|
|
140
|
+
top_face.inner_wires().edge(),
|
|
141
|
+
min(wall_thickness * 0.4, ball_radius * 0.1), # Limit fillet radius
|
|
142
|
+
)
|
|
143
|
+
with BuildPart(
|
|
144
|
+
Plane.XY.offset(ball_radius * 0.75 + wall_thickness), mode=Mode.SUBTRACT
|
|
145
|
+
) as flexcuts:
|
|
146
|
+
with PolarLocations(0, 4):
|
|
147
|
+
Box(
|
|
148
|
+
ball_radius,
|
|
149
|
+
(ball_radius + wall_thickness) * 2,
|
|
150
|
+
ball_radius / 2 + wall_thickness,
|
|
151
|
+
align=(Align.CENTER, Align.CENTER, Align.MIN),
|
|
152
|
+
)
|
|
153
|
+
Cylinder(
|
|
154
|
+
radius=ball_radius / 2,
|
|
155
|
+
height=(ball_radius + wall_thickness) * 2,
|
|
156
|
+
align=(Align.CENTER, Align.CENTER, Align.CENTER),
|
|
157
|
+
rotation=(90, 0, 0),
|
|
158
|
+
)
|
|
159
|
+
socket.part.label = "Ball Socket"
|
|
160
|
+
super().__init__(
|
|
161
|
+
part=socket.part, rotation=rotation, align=tuplify(align, 3), mode=mode
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
if __name__ == "__main__":
|
|
166
|
+
show(
|
|
167
|
+
BallMount(
|
|
168
|
+
24.24871131,
|
|
169
|
+
),
|
|
170
|
+
BallSocket(
|
|
171
|
+
24.24871131,
|
|
172
|
+
# 15,
|
|
173
|
+
wall_thickness=2,
|
|
174
|
+
tolerance=-0.05,
|
|
175
|
+
),
|
|
176
|
+
reset_camera=Camera.KEEP,
|
|
177
|
+
)
|
b3dkit/basic_shapes.py
ADDED
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Utility functions for creating and manipulating basic 3D shapes
|
|
3
|
+
and geometric calculations. These functions extend build123d's
|
|
4
|
+
capabilities with shapes and operations commonly used in 3D design.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from math import sqrt, radians, cos, sin, tan
|
|
8
|
+
from tempfile import template
|
|
9
|
+
from typing import Union
|
|
10
|
+
|
|
11
|
+
from build123d import (
|
|
12
|
+
Align,
|
|
13
|
+
Axis,
|
|
14
|
+
BasePartObject,
|
|
15
|
+
BaseSketchObject,
|
|
16
|
+
Box,
|
|
17
|
+
BuildLine,
|
|
18
|
+
BuildPart,
|
|
19
|
+
BuildSketch,
|
|
20
|
+
Builder,
|
|
21
|
+
Circle,
|
|
22
|
+
Compound,
|
|
23
|
+
Cylinder,
|
|
24
|
+
GridLocations,
|
|
25
|
+
JernArc,
|
|
26
|
+
Line,
|
|
27
|
+
Location,
|
|
28
|
+
Mode,
|
|
29
|
+
Part,
|
|
30
|
+
Plane,
|
|
31
|
+
PolarLocations,
|
|
32
|
+
RadiusArc,
|
|
33
|
+
RegularPolygon,
|
|
34
|
+
RotationLike,
|
|
35
|
+
Shape,
|
|
36
|
+
Sketch,
|
|
37
|
+
Sphere,
|
|
38
|
+
add,
|
|
39
|
+
extrude,
|
|
40
|
+
fillet,
|
|
41
|
+
loft,
|
|
42
|
+
make_face,
|
|
43
|
+
scale,
|
|
44
|
+
sweep,
|
|
45
|
+
tuplify,
|
|
46
|
+
validate_inputs,
|
|
47
|
+
)
|
|
48
|
+
from ocp_vscode import Camera, show
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def radius_to_apothem(radius: float, side_count: int = 6) -> float:
|
|
52
|
+
"""
|
|
53
|
+
calculates the apothem of a regular polygon given its circumradius
|
|
54
|
+
-------
|
|
55
|
+
arguments:
|
|
56
|
+
- radius: the circumradius of the polygon
|
|
57
|
+
"""
|
|
58
|
+
return radius * cos(radians(360 / (2 * side_count)))
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def apothem_to_radius(apothem: float, side_count: int = 6) -> float:
|
|
62
|
+
"""
|
|
63
|
+
calculates the circumradius of a regular polygon given its apothem
|
|
64
|
+
-------
|
|
65
|
+
arguments:
|
|
66
|
+
- apothem: the apothem of the polygon
|
|
67
|
+
"""
|
|
68
|
+
return apothem / cos(radians(360 / (2 * side_count)))
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def opposite_length(angle: float, adjacent_length: float) -> float:
|
|
72
|
+
"""calculate the opposite side length of a right triangle given the angle and adjacent side length
|
|
73
|
+
----------
|
|
74
|
+
Arguments:
|
|
75
|
+
- angle: float
|
|
76
|
+
The angle in degrees.
|
|
77
|
+
- adjacent_length: float
|
|
78
|
+
The length of the adjacent side."""
|
|
79
|
+
angle_rad = radians(angle)
|
|
80
|
+
return adjacent_length * tan(angle_rad)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def adjacent_length(angle: float, opposite_length: float) -> float:
|
|
84
|
+
"""calculate the adjacent side length of a right triangle given the angle and opposite side length
|
|
85
|
+
----------
|
|
86
|
+
Arguments:
|
|
87
|
+
- angle: float
|
|
88
|
+
The angle in degrees.
|
|
89
|
+
- opposite_length: float
|
|
90
|
+
The length of the opposite side."""
|
|
91
|
+
angle_rad = radians(angle)
|
|
92
|
+
return opposite_length / tan(angle_rad)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def distance_to_circle_edge(radius, point, angle) -> float:
|
|
96
|
+
"""
|
|
97
|
+
for a circle with the given radius, find the distance from the
|
|
98
|
+
given point to the edge of the circle in the direction determined
|
|
99
|
+
by the given angle
|
|
100
|
+
"""
|
|
101
|
+
x1, y1 = point
|
|
102
|
+
theta = radians(angle)
|
|
103
|
+
|
|
104
|
+
a = 1
|
|
105
|
+
b = 2 * (x1 * cos(theta) + y1 * sin(theta))
|
|
106
|
+
c = x1**2 + y1**2 - radius**2
|
|
107
|
+
|
|
108
|
+
discriminant = b**2 - 4 * a * c
|
|
109
|
+
|
|
110
|
+
if discriminant < 0:
|
|
111
|
+
raise ValueError(f"Error: discriminant calculated as < 0 ({discriminant})")
|
|
112
|
+
t1 = (-b + sqrt(discriminant)) / (2 * a)
|
|
113
|
+
t2 = (-b - sqrt(discriminant)) / (2 * a)
|
|
114
|
+
|
|
115
|
+
t = max(t1, t2)
|
|
116
|
+
|
|
117
|
+
return t
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def circular_intersection(radius: float, coordinate: float) -> float:
|
|
121
|
+
"""
|
|
122
|
+
given a positive position along the axis of a circle, find the intersection
|
|
123
|
+
along the other axis of the perimeter of the circle
|
|
124
|
+
-------
|
|
125
|
+
arguments:
|
|
126
|
+
- radius: the radius of the circle
|
|
127
|
+
- coordinate: a coordinate along one axis of the circle (must be a
|
|
128
|
+
positive value less than the radius)
|
|
129
|
+
"""
|
|
130
|
+
if 0 > coordinate > radius:
|
|
131
|
+
raise ValueError("The x-coordinate cannot be greater than the radius.")
|
|
132
|
+
return sqrt(radius**2 - coordinate**2)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
class DiamondTorus(BasePartObject):
|
|
136
|
+
def __init__(
|
|
137
|
+
self,
|
|
138
|
+
major_radius: float,
|
|
139
|
+
minor_radius: float,
|
|
140
|
+
stretch: tuple = (1, 1),
|
|
141
|
+
rotation: RotationLike = (0, 0, 0),
|
|
142
|
+
align: Union[None, Align, tuple[Align, Align, Align]] = None,
|
|
143
|
+
mode: Mode = Mode.ADD,
|
|
144
|
+
):
|
|
145
|
+
"""
|
|
146
|
+
sweeps a regular diamond along a circle defined by major_radius
|
|
147
|
+
-------
|
|
148
|
+
arguments:
|
|
149
|
+
- major_radius: the radius of the circle to sweep the diamond along
|
|
150
|
+
- minor_radius: the radius of the diamond
|
|
151
|
+
- stretch: scales the diamond shape
|
|
152
|
+
- rotation: the rotation of the torus
|
|
153
|
+
- align: the alignment of the torus
|
|
154
|
+
- mode: the mode of the torus
|
|
155
|
+
"""
|
|
156
|
+
|
|
157
|
+
context: BuildPart = BuildPart._get_context()
|
|
158
|
+
validate_inputs(context, self)
|
|
159
|
+
|
|
160
|
+
with BuildPart() as torus:
|
|
161
|
+
with BuildLine():
|
|
162
|
+
l1 = JernArc(
|
|
163
|
+
start=(major_radius, 0),
|
|
164
|
+
tangent=(0, 1),
|
|
165
|
+
radius=major_radius,
|
|
166
|
+
arc_size=360,
|
|
167
|
+
)
|
|
168
|
+
with BuildSketch(l1 ^ 0):
|
|
169
|
+
RegularPolygon(radius=minor_radius, side_count=4)
|
|
170
|
+
scale(by=(stretch[0], stretch[1], 1))
|
|
171
|
+
sweep()
|
|
172
|
+
super().__init__(
|
|
173
|
+
part=torus.part, rotation=rotation, align=tuplify(align, 3), mode=mode
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
class RoundedCylinder(BasePartObject):
|
|
178
|
+
def __init__(
|
|
179
|
+
self,
|
|
180
|
+
radius: float,
|
|
181
|
+
height: float,
|
|
182
|
+
rotation: RotationLike = (0, 0, 0),
|
|
183
|
+
align: Union[None, Align, tuple[Align, Align, Align]] = None,
|
|
184
|
+
mode: Mode = Mode.ADD,
|
|
185
|
+
):
|
|
186
|
+
"""
|
|
187
|
+
sweeps a regular diamond along a circle defined by major_radius
|
|
188
|
+
-------
|
|
189
|
+
arguments:
|
|
190
|
+
- radius: the radius of the cylinder
|
|
191
|
+
- height: the height of the cylinder
|
|
192
|
+
- rotation: the rotation of the cylinder
|
|
193
|
+
- align: the alignment of the cylinder
|
|
194
|
+
- mode: the mode of the cylinder
|
|
195
|
+
"""
|
|
196
|
+
|
|
197
|
+
context: BuildPart = BuildPart._get_context()
|
|
198
|
+
validate_inputs(context, self)
|
|
199
|
+
|
|
200
|
+
if height <= radius * 2:
|
|
201
|
+
raise ValueError("height must be greater than radius * 2")
|
|
202
|
+
with BuildPart() as cylinder:
|
|
203
|
+
Cylinder(radius=radius, height=height, align=align)
|
|
204
|
+
fillet(
|
|
205
|
+
cylinder.faces().sort_by(Axis.Z)[-1].edges()
|
|
206
|
+
+ cylinder.faces().sort_by(Axis.Z)[0].edges(),
|
|
207
|
+
radius=radius,
|
|
208
|
+
)
|
|
209
|
+
super().__init__(
|
|
210
|
+
part=cylinder.part, rotation=rotation, align=tuplify(align, 3), mode=mode
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
class PolygonalCylinder(BasePartObject):
|
|
215
|
+
|
|
216
|
+
def __init__(
|
|
217
|
+
self,
|
|
218
|
+
radius: float,
|
|
219
|
+
height: float,
|
|
220
|
+
side_count: int = 6,
|
|
221
|
+
stretch: tuple = (1, 1, 1),
|
|
222
|
+
rotation: RotationLike = (0, 0, 0),
|
|
223
|
+
align: Union[None, Align, tuple[Align, Align, Align]] = None,
|
|
224
|
+
mode: Mode = Mode.ADD,
|
|
225
|
+
):
|
|
226
|
+
"""
|
|
227
|
+
creates an extruded polygon that behaves like a cylinder
|
|
228
|
+
-------
|
|
229
|
+
arguments:
|
|
230
|
+
- radius: the radius of the cylinder
|
|
231
|
+
- height: the height of the cylinder
|
|
232
|
+
- side_count: the number of sides of the polygonal base (default is 6)
|
|
233
|
+
- stretch: scales the base polygon
|
|
234
|
+
- rotation: the rotation of the cylinder
|
|
235
|
+
- align: the alignment of the cylinder
|
|
236
|
+
- mode: the mode to use when
|
|
237
|
+
"""
|
|
238
|
+
context: BuildPart = BuildPart._get_context()
|
|
239
|
+
validate_inputs(context, self)
|
|
240
|
+
|
|
241
|
+
with BuildPart() as tube:
|
|
242
|
+
with BuildSketch():
|
|
243
|
+
RegularPolygon(
|
|
244
|
+
radius=radius, side_count=side_count, align=tuplify(align, 2)
|
|
245
|
+
)
|
|
246
|
+
extrude(amount=height * stretch[2])
|
|
247
|
+
super().__init__(
|
|
248
|
+
part=tube.part, rotation=rotation, align=tuplify(align, 3), mode=mode
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
class DiamondCylinder(PolygonalCylinder):
|
|
253
|
+
|
|
254
|
+
def __init__(
|
|
255
|
+
self,
|
|
256
|
+
radius: float,
|
|
257
|
+
height: float,
|
|
258
|
+
stretch: tuple = (1, 1, 1),
|
|
259
|
+
rotation: RotationLike = (0, 0, 0),
|
|
260
|
+
align: Union[None, Align, tuple[Align, Align, Align]] = None,
|
|
261
|
+
mode: Mode = Mode.ADD,
|
|
262
|
+
):
|
|
263
|
+
"""
|
|
264
|
+
creates an extruded diamond that behaves like a cylinder
|
|
265
|
+
-------
|
|
266
|
+
arguments:
|
|
267
|
+
- radius: the radius of the cylinder
|
|
268
|
+
- height: the height of the cylinder
|
|
269
|
+
- rotation: the rotation of the cylinder
|
|
270
|
+
- align: the alignment of the cylinder (default
|
|
271
|
+
is (Align.CENTER, Align.CENTER, Align.CENTER) )
|
|
272
|
+
- mode: the mode to use when adding the part
|
|
273
|
+
"""
|
|
274
|
+
super().__init__(
|
|
275
|
+
radius=radius,
|
|
276
|
+
height=height,
|
|
277
|
+
side_count=4,
|
|
278
|
+
stretch=stretch,
|
|
279
|
+
rotation=rotation,
|
|
280
|
+
align=align,
|
|
281
|
+
mode=mode,
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
class Teardrop(BaseSketchObject):
|
|
286
|
+
|
|
287
|
+
def __init__(
|
|
288
|
+
self,
|
|
289
|
+
radius: float,
|
|
290
|
+
peak_distance: float,
|
|
291
|
+
rotation: RotationLike = (0, 0),
|
|
292
|
+
align: Union[None, Align, tuple[Align, Align]] = None,
|
|
293
|
+
mode: Mode = Mode.ADD,
|
|
294
|
+
):
|
|
295
|
+
"""
|
|
296
|
+
Create a teardrop shape sketch;
|
|
297
|
+
this can be useful when creating holes along the Z axis
|
|
298
|
+
to compensate for overhang issues with FDM printers.
|
|
299
|
+
----------
|
|
300
|
+
Arguments:
|
|
301
|
+
- radius: float
|
|
302
|
+
The radius of the teardrop.
|
|
303
|
+
- peak_distance: float
|
|
304
|
+
The distance from the center of the teardrop circle to the peak of the
|
|
305
|
+
teardrop shape.
|
|
306
|
+
- align: tuple
|
|
307
|
+
The alignment of the teardrop. Note that the Y alignment is to the center of the cylinder, ignoring the peak distance
|
|
308
|
+
- mode: Mode
|
|
309
|
+
The mode of the teardrop.
|
|
310
|
+
"""
|
|
311
|
+
context: BuildSketch = BuildSketch._get_context()
|
|
312
|
+
validate_inputs(context, self)
|
|
313
|
+
|
|
314
|
+
x = radius * sqrt(1 - (radius**2 / peak_distance**2))
|
|
315
|
+
y = radius**2 / peak_distance
|
|
316
|
+
with BuildSketch() as teardrop:
|
|
317
|
+
if peak_distance == radius:
|
|
318
|
+
Circle(radius)
|
|
319
|
+
else:
|
|
320
|
+
with BuildLine() as outline:
|
|
321
|
+
Line((-x, -y), (0, -peak_distance))
|
|
322
|
+
Line((0, -peak_distance), (x, -y))
|
|
323
|
+
RadiusArc((x, -y), (-x, -y), radius, short_sagitta=False)
|
|
324
|
+
make_face()
|
|
325
|
+
super().__init__(
|
|
326
|
+
obj=teardrop.sketch,
|
|
327
|
+
rotation=rotation,
|
|
328
|
+
align=tuplify(align, 2),
|
|
329
|
+
mode=mode,
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
class TeardropCylinder(BasePartObject):
|
|
334
|
+
|
|
335
|
+
def __init__(
|
|
336
|
+
self,
|
|
337
|
+
radius: float,
|
|
338
|
+
height: float,
|
|
339
|
+
peak_distance: float,
|
|
340
|
+
rotation: RotationLike = (0, 0, 0),
|
|
341
|
+
align: Union[None, Align, tuple[Align, Align, Align]] = None,
|
|
342
|
+
mode: Mode = Mode.ADD,
|
|
343
|
+
):
|
|
344
|
+
"""
|
|
345
|
+
Create a cylinder with a teardrop shape;
|
|
346
|
+
this can be useful when creating holes along the Z axis
|
|
347
|
+
to compensate for overhang issues with FDM printers.
|
|
348
|
+
----------
|
|
349
|
+
Arguments:
|
|
350
|
+
- radius: float
|
|
351
|
+
The radius of the cylinder.
|
|
352
|
+
- height: float
|
|
353
|
+
The height of the cylinder.
|
|
354
|
+
- peak_distance: float
|
|
355
|
+
The distance from the center of the cylinder to the peak of the
|
|
356
|
+
teardrop shape.
|
|
357
|
+
- rotation: tuple
|
|
358
|
+
The rotation of the cylinder.
|
|
359
|
+
- align: tuple
|
|
360
|
+
The alignment of the cylinder.
|
|
361
|
+
- mode: Mode
|
|
362
|
+
The mode of the cylinder.
|
|
363
|
+
"""
|
|
364
|
+
context: BuildPart = BuildPart._get_context()
|
|
365
|
+
validate_inputs(context, self)
|
|
366
|
+
|
|
367
|
+
with BuildPart() as cylinder:
|
|
368
|
+
with BuildSketch():
|
|
369
|
+
Teardrop(radius, peak_distance, align=tuplify(align, 2))
|
|
370
|
+
extrude(amount=height)
|
|
371
|
+
super().__init__(
|
|
372
|
+
part=cylinder.part,
|
|
373
|
+
rotation=rotation,
|
|
374
|
+
align=tuplify(align, 3),
|
|
375
|
+
mode=mode,
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
if __name__ == "__main__":
|
|
380
|
+
|
|
381
|
+
show(
|
|
382
|
+
DiamondTorus(
|
|
383
|
+
50,
|
|
384
|
+
2,
|
|
385
|
+
),
|
|
386
|
+
reset_camera=Camera.KEEP,
|
|
387
|
+
)
|
|
388
|
+
show(
|
|
389
|
+
DiamondCylinder(
|
|
390
|
+
radius=50,
|
|
391
|
+
height=200,
|
|
392
|
+
stretch=(1, 1, 1),
|
|
393
|
+
rotation=(0, 0, 0),
|
|
394
|
+
align=(Align.MIN, Align.MIN, Align.CENTER),
|
|
395
|
+
),
|
|
396
|
+
reset_camera=Camera.KEEP,
|
|
397
|
+
)
|