diff --git a/examples/roller_coaster.py b/examples/roller_coaster.py index d6d25b5..75be1d9 100644 --- a/examples/roller_coaster.py +++ b/examples/roller_coaster.py @@ -26,6 +26,7 @@ license: limitations under the License. """ from build123d import * +from ocp_vscode import show_object with BuildLine() as roller_coaster: powerup = Spline( @@ -40,5 +41,4 @@ with BuildLine() as roller_coaster: Spline(corner @ 1, screw @ 0, tangents=(corner % 1, screw % 0)) Spline(screw @ 1, (-100, 30, 10), powerup @ 0, tangents=(screw % 1, powerup % 0)) -if "show_object" in locals(): - show_object(roller_coaster.line.wrapped, name="roller_coaster") +show_object(roller_coaster, name="roller_coaster") diff --git a/src/build123d/objects_curve.py b/src/build123d/objects_curve.py index 4b6ee94..9637dd6 100644 --- a/src/build123d/objects_curve.py +++ b/src/build123d/objects_curve.py @@ -340,7 +340,7 @@ class Helix(BaseLineObject): validate_inputs(context, self) center_pnt = WorkplaneList.localize(center) - helix = Wire.make_helix( + helix = Edge.make_helix( pitch, height, radius, center_pnt, direction, cone_angle, lefthand ) super().__init__(helix, mode=mode) diff --git a/src/build123d/topology.py b/src/build123d/topology.py index 0ca4bef..9956727 100644 --- a/src/build123d/topology.py +++ b/src/build123d/topology.py @@ -162,7 +162,7 @@ from OCP.Geom import ( Geom_Surface, Geom_TrimmedCurve, ) -from OCP.Geom2d import Geom2d_Curve, Geom2d_Line +from OCP.Geom2d import Geom2d_Curve, Geom2d_Line, Geom2d_TrimmedCurve from OCP.Geom2dAdaptor import Geom2dAdaptor_Curve from OCP.Geom2dAPI import Geom2dAPI_InterCurveCurve from OCP.GeomAbs import GeomAbs_C0, GeomAbs_Intersection, GeomAbs_JoinType @@ -4528,6 +4528,70 @@ class Edge(Shape, Mixin1D): ).Edge() ) + @classmethod + def make_helix( + cls, + pitch: float, + height: float, + radius: float, + center: VectorLike = (0, 0, 0), + normal: VectorLike = (0, 0, 1), + angle: float = 0.0, + lefthand: bool = False, + ) -> Wire: + """make_helix + + Make a helix with a given pitch, height and radius. By default a cylindrical surface is + used to create the helix. If the :angle: is set (the apex given in degree) a conical + surface is used instead. + + Args: + pitch (float): distance per revolution along normal + height (float): total height + radius (float): + center (VectorLike, optional): Defaults to (0, 0, 0). + normal (VectorLike, optional): Defaults to (0, 0, 1). + angle (float, optional): conical angle. Defaults to 0.0. + lefthand (bool, optional): Defaults to False. + + Returns: + Wire: helix + """ + # 1. build underlying cylindrical/conical surface + if angle == 0.0: + geom_surf: Geom_Surface = Geom_CylindricalSurface( + gp_Ax3(Vector(center).to_pnt(), Vector(normal).to_dir()), radius + ) + else: + geom_surf = Geom_ConicalSurface( + gp_Ax3(Vector(center).to_pnt(), Vector(normal).to_dir()), + angle * DEG2RAD, + radius, + ) + + # 2. construct an segment in the u,v domain + + # Determine the length of the 2d line which will be wrapped around the surface + line_sign = -1 if lefthand else 1 + line_dir = Vector(line_sign * 2 * pi, pitch).normalized() + line_len = (height / line_dir.Y) / cos(radians(angle)) + + # Create an infinite 2d line in the direction of the helix + helix_line = Geom2d_Line(gp_Pnt2d(0, 0), gp_Dir2d(line_dir.X, line_dir.Y)) + # Trim the line to the desired length + helix_curve = Geom2d_TrimmedCurve( + helix_line, 0, line_len, theAdjustPeriodic=True + ) + + # 3. Wrap the line around the surface + edge_builder = BRepBuilderAPI_MakeEdge(helix_curve, geom_surf) + topods_edge = edge_builder.Edge() + + # 4. Convert the edge made with 2d geometry to 3d + BRepLib.BuildCurves3d_s(topods_edge) + + return cls(topods_edge) + def distribute_locations( self: Union[Wire, Edge], count: int, @@ -5753,8 +5817,8 @@ class Solid(Shape, Mixin3D): # make an auxiliary spine pitch = 360.0 / angle * normal.length - aux_spine_w = Wire.make_helix( - pitch, normal.length, 1, center=center, normal=normal + aux_spine_w = Wire.make_wire( + [Edge.make_helix(pitch, normal.length, 1, center=center, normal=normal)] ).wrapped # extrude the outer wire @@ -6546,66 +6610,6 @@ class Wire(Shape, Mixin1D): return cls(wire_builder.Wire()) - @classmethod - def make_helix( - cls, - pitch: float, - height: float, - radius: float, - center: VectorLike = (0, 0, 0), - normal: VectorLike = (0, 0, 1), - angle: float = 0.0, - lefthand: bool = False, - ) -> Wire: - """make_helix - - Make a helix with a given pitch, height and radius. By default a cylindrical surface is - used to create the helix. If the :angle: is set (the apex given in degree) a conical - surface is used instead. - - Args: - pitch (float): distance per revolution along normal - height (float): total height - radius (float): - center (VectorLike, optional): Defaults to (0, 0, 0). - normal (VectorLike, optional): Defaults to (0, 0, 1). - angle (float, optional): conical angle. Defaults to 0.0. - lefthand (bool, optional): Defaults to False. - - Returns: - Wire: helix - """ - # 1. build underlying cylindrical/conical surface - if angle == 0.0: - geom_surf: Geom_Surface = Geom_CylindricalSurface( - gp_Ax3(Vector(center).to_pnt(), Vector(normal).to_dir()), radius - ) - else: - geom_surf = Geom_ConicalSurface( - gp_Ax3(Vector(center).to_pnt(), Vector(normal).to_dir()), - angle * DEG2RAD, - radius, - ) - - # 2. construct an segment in the u,v domain - if lefthand: - geom_line = Geom2d_Line(gp_Pnt2d(0.0, 0.0), gp_Dir2d(-2 * pi, pitch)) - else: - geom_line = Geom2d_Line(gp_Pnt2d(0.0, 0.0), gp_Dir2d(2 * pi, pitch)) - - # 3. put it together into a wire - u_start = geom_line.Value(0.0) - u_stop = geom_line.Value((height / pitch) * sqrt((2 * pi) ** 2 + pitch**2)) - geom_seg = GCE2d_MakeSegment(u_start, u_stop).Value() - - topo_edge = BRepBuilderAPI_MakeEdge(geom_seg, geom_surf).Edge() - - # 4. Convert to wire and fix building 3d geom from 2d geom - wire = BRepBuilderAPI_MakeWire(topo_edge).Wire() - BRepLib.BuildCurves3d_s(wire, 1e-6, MaxSegment=2000) # NB: preliminary values - - return cls(wire) - def stitch(self, other: Wire) -> Wire: """Attempt to stich wires diff --git a/tests/test_direct_api.py b/tests/test_direct_api.py index 3beb4f6..f744a3a 100644 --- a/tests/test_direct_api.py +++ b/tests/test_direct_api.py @@ -908,6 +908,10 @@ class TestEdge(DirectApiTestCase): with self.assertRaises(ValueError): edge.param_at_point((-1, 1)) + def test_conical_helix(self): + helix = Edge.make_helix(1, 4, 1, normal=(-1, 0, 0), angle=10, lefthand=True) + self.assertAlmostEqual(helix.bounding_box().min.X, -4, 5) + class TestFace(DirectApiTestCase): def test_make_surface_from_curves(self): @@ -3016,10 +3020,6 @@ class TestWire(DirectApiTestCase): ) self.assertAlmostEqual(full_ellipse.area / 2, half_ellipse.area, 5) - def test_conical_helix(self): - helix = Wire.make_helix(1, 4, 1, normal=(-1, 0, 0), angle=10, lefthand=True) - self.assertAlmostEqual(helix.length, 34.102023034708374, 5) - def test_stitch(self): half_ellipse1 = Wire.make_ellipse( 2, 1, start_angle=0, end_angle=180, closed=False